详解 Java 对象与内存控制(上)

1. 一些定义

2. 类变量总是比实例变量先初始化

不管是类变量还是实例变量,你都不能引用一个还没有定义的变量,或者在引用之前没有定义的变量,如下图所示:

但以下代码是完全正确的:

因为:类变量总是比实例变量先初始化

3.类变量与实例变量的内存分配

看如下代码:

class Person {
    
    public static int eyeNum;
    public String name;
    public int age;
    
}

public class Test {
    
    public static void main(String[] args) {
        
        
        Person.eyeNum = 2; // (1)
        System.out.println("通过类直接访问eyeNum, Person.eyeNum = " + Person.eyeNum);
        
        Person p1 = new Person();
        p1.name = "Tom";
        p1.age = 20;
        System.out.println("通过类直接访问eyeNum, Person.eyeNum = " + Person.eyeNum);
        // (2)
        System.out.println("通过对象访问eyeNum, p1.eyeNum = " + p1.eyeNum);  
        
        Person p2 = new Person();
        p2.name = "二郎神";
        p2.age = 18;
        p2.eyeNum = 3;  // (3)
        System.out.println("通过类直接访问eyeNum, Person.eyeNum = " + Person.eyeNum);
        System.out.println("通过对象访问eyeNum, p1.eyeNum = " + p1.eyeNum);
        System.out.println("通过对象访问eyeNum, p2.eyeNum = " + p2.eyeNum);
    }
    
}

运行结果为:
通过类直接访问eyeNum, Person.eyeNum = 2
通过类直接访问eyeNum, Person.eyeNum = 2
通过对象访问eyeNum, p1.eyeNum = 2
通过类直接访问eyeNum, Person.eyeNum = 3
通过对象访问eyeNum, p1.eyeNum = 3
通过对象访问eyeNum, p2.eyeNum = 3

内存分析:

Person 类也是 Class 类的一个对象,所以初始化 Person 类后,会在堆中为其分配一块内存,而静态变量是类的变量,所以类初始化后会直接为静态变量分配内存空间,这个内存空间就在类的内存空间中,所以执行完 (1) 语句后的内存分配如下图所示:

然后为 p1 这个实例变量分配内存,通过 p1 来访问 eyeNum 这个类变量,实际上就是访问 Person 类的内存空间中的 eyeNum

然后为 p2 这个实例变量分配内存,p2 修改了 eyeNum 的值,实际上就是直接修改了 Person 类的内存空间中的 eyeNum 的值

通过实例变量访问或修改类变量,操作的都是类的内存空间中的变量

4. 实例变量的初始化优先级

在 Java 中,可以通过3种方式对实例变量进行初始化:

  • (1) 定义实例变量时指定初始值
  • (2) 非静态代码块中指定初始值
  • (3) 构造器中指定初始值

以下代码测试这3种方式的优先级:

class Cat {
    
    // 定义两个实例变量
    public String name;
    public int age;
    
    // 使用构造器初始化 name 和 age 两个实例变量
    public Cat(String name, int age) {
        System.out.println("执行构造器...");
        this.name = name;
        this.age = age;
    }
    
    {
        System.out.println("执行非静态代码块...");
        // 在非静态代码块中初始化实例变量
        weight = 2.0;
    }
    
    // 定义时实例变量时指定初始值
    double weight = 2.3;

    @Override
    public String toString() {
        return "Cat [name=" + name + ", age=" + age + ", weight=" + weight + "]";
    }

}

public class Test {
    
    public static void main(String[] args) {
    
        Cat c1 = new Cat("Tom", 2);
        System.out.println(c1);
        
    }
    
}

程序运行结果为:
执行非静态代码块...
执行构造器...
Cat [name=Tom, age=2, weight=2.3]

分析:

  • 首先执行非静态代码块,weight 的值为 2.0
  • 然后执行定义实例变量时候的初始化语句,把 weight 的值覆盖为 2.3
  • 最后执行构造器,给 name 和 age 两个实例变量赋值

把程序中定义实例变量时初始化的语句和费静态代码块调换位置:

// 定义时静态变量时指定初始值
double weight = 2.3;

{
    System.out.println("执行非静态代码块...");
    // 在非静态代码块中初始化实例变量
    weight = 2.0;
}

再运行程序结果为:

执行非静态代码块...
执行构造器...
Cat [name=Tom, age=2, weight=2.0]

分析:

  • 首先执行定义实例变量时候的初始化语句, weight = 2.3
  • 然后执行非静态代码块,把 weight 的值覆盖为 2.0
  • 最后执行构造器,给 name 和 age 两个实例变量赋值

总结:

  • 非静态代码块和直接初始化实例变量的语句的执行顺序与它们在源代码中的出现顺序有关,谁写在前头,先执行谁
  • 这里有个疑问,我还没执行定义 weight 的语句,怎么就能在非静态代码块中为其赋值呢?实际上,底层的运行顺序是:(1)double weight; (2) 再根据非静态代码块和直接初始化语句出现的位置来决定先执行谁
  • 非静态代码块的优先级比构造器高

5. 类变量的初始化优先级

类变量属于类本身,程序初始化类的时候会一并为该类的类变量分配内存空间并执行初始化,JVM 对一个类只初始化一次,因此 Java 程序每运行一次,系统只为类变量分配一次内存空间,执行一次初始化,程序可以在2个地方对类变量执行初始化:

  • (1) 定义类变量时指定初始值
  • (2) 在静态代码块中指定初始值

这两种方式的执行顺序与它们在源程序中的排列顺序相同,看如下测试代码:

class Price {
    
    public static final Price INSTANCE = new Price(2.8);
    public static double initPrice = 20.0;
    public double currentPrice;
    
    public Price(double discount) {
        currentPrice = initPrice - discount;
    }
}

public class Test {
    
    public static void main(String[] args) {
        
        System.out.println(Price.INSTANCE.currentPrice);
        Price p = new Price(2.8);
        System.out.println(p.currentPrice);
        
    }
}

程序运行结果为:
-2.8
17.2

分析:

  • (1) 先给 Price 的两个类变量 INSTANCE 和 initPrice 分配内存空间,并设置默认初始值为 null 和 0.0
  • (2) 执行构造器器来初始化 INSTANCE,此时 initPrice = 0.0,所以 currentPrice = -2.8
  • (3) 初始化 initPrice = 20.0
  • (4) new Price(2.8),此时 initPrice = 20.0, 所以 currentPrice = 17.2

6. 创建 Java 对象的初始化过程

有如下继承结构:

如果在程序中创建 C 对象,会按如下步骤进行初始化

  • (1) 执行 Object 类的非静态代码块(如果有的话)
  • (2) 调用 Object 类的一个或多个构造器
  • (3) 执行 A 类的非静态代码块(如果有的话)
  • (4) 调用 A 类的一个或多个构造器
  • (5) 执行 B 类的非静态代码块(如果有的话)
  • (6) 调用 B 类的一个或多个构造器
  • (7) 执行 C 类的非静态代码块(如果有的话)
  • (8) 调用 C 类的一个或多个构造器

看如下代码:

class A {
    
    {
        System.out.println("执行A类的非静态代码块");
    }
    
    public A() {
        System.out.println("调用A类的无参构造器");
    }
    
    public A(String name) {
        this();  // 使用this()显示调用上面的无参构造器
        System.out.println("调用A类的有参构造器, name参数为:" + name);
    }
    
}

class B extends A {
    
    {
        System.out.println("执行B类的非静态代码块");
    }
    
    public B(String name) {
        super(name);  // 显示的调用了父类的构造器
        System.out.println("调用B类的有参构造器, name参数为:" + name);
    }
    
    public B(String name, int age) {
        this(name);  // 显示的调用上面的B(String, name)构造器
        System.out.println("调用B类的有参构造器, name参数为:" + name + ", age参数为:" + age);
    }
    
}

class C extends B {
    
    {
        System.out.println("执行C类的非静态代码块");
    }
    
    public C() {
        super("灰太狼", 3);
        System.out.println("调用C类的无参构造器");
    }
    
    public C(double weight) {
        this();
        System.out.println("调用C类的有参构造器, weight参数:" + weight);
    }
    
}

public class Test {
    
    public static void main(String[] args) {
        new C(5.6);
    }
}

程序运行结果为:
执行A类的非静态代码块
调用A类的无参构造器
调用A类的有参构造器, name参数为:灰太狼
执行B类的非静态代码块
调用B类的有参构造器, name参数为:灰太狼
调用B类的有参构造器, name参数为:灰太狼, age参数为:3
执行C类的非静态代码块
调用C类的无参构造器
调用C类的有参构造器, weight参数:5.6

说明:

  • (1) super 用于显式调用父类的构造器
  • (2) this 用于显式的调用本类中另一个重载的构造器
  • (3) 如果子类构造器中没有使用 super() 调用,也没有使用 this() 构造,那么将隐式的调用父类的无参构造器

7. 父类访问子类对象的实例变量

子类的方法可以访问父类的实例变量,这是因为子类继承父类就会获得父类的实例变量和方法,但父类不能访问子类的实例变量,因为父类根本不知道它将被哪个子类继承,但在极端的情况下,可能出现父类访问子类变量的情况,看如下代码:

class A {
    
    private int i = 2;
    
    public A() {
        this.display();
    }
    
    public void display() {
        System.out.println(i);
    }
    
}

class B extends A {
    
    private int i = 22;
    
    public B() {
        i = 222;
    }
    
    public void display() {
        System.out.println(i);
    }
    
}

public class Test {
    public static void main(String[] args) {
        new B();
    }
}

程序运行结果为:
0

分析:

  • (1) 程序执行开始执行 new B(),为 B 对象分配内存空间,此时需要为这个 B 对象分配两块内存,分别存放父类 A 的 i 变量和 B 对象的 i 变量,关于 Java 对象怎样拥有多个同名的实例变量,在详解 Java 对象与内存控制(下) 会有详细介绍
  • (2) 此时两个 i 变量还没有被赋值,它们拥有默认的初始值 0,需要说明的是,构造器只负责对 Java 对象的实例变量执行初始化操作,也就是赋初始值,因此在真正的赋值代码还没有运行的时候,这两个 i 的值为 0
  • (3) 在调用 B 的构造器之前,会先调用 A 的构造器,执行 A 类的 this.display(),此时的 this 是 B 对象,因为是 B() 构造器隐式的调用了 A(),因为 this 是 B,所以会打印 B 的 i 的值,但是 B 的 i 的值还没有赋值,因为给 B 的 i 赋值是在 B() 中进行,而此刻还没有执行到 B(),此刻是在执行 A(),所以打印出来 i 的值为 0,这是一个父类访问子类的实例变量的例子

修改一下 A 的构造器:

class A {
    
    private int i = 2;
    
    public A() {
        // 增加了一行代码:
        System.out.println(this.i);
        this.display();
    }
    
    public void display() {
        System.out.println(i);
    }
    
}

在运行整个程序,结果为:
2
0

分析:

  • (1) 在执行 A() 时,会先给 A 的 i 赋值为 2
  • (2) 上面我们说,A() 中 this 是 B 对象,但直接打印 this.i 的结果却是 2,是 A 的 i 的值
  • (3) 这是因为,this 虽然代表着 B 对象,但它的编译类型是 A,也是就说 A() 中 this 的编译类型为 A,实际引用了一个 B 对象
  • (4) 当变量的编译时类型和运行时类型不同时,通过该变量访问它引用的对象的实例变量时,该实例变量的值由声明该变量的类型决定,但通过该变量调用它引用的对象的实例方法时,该方法行为由它实际所引用的对象来决定,因此访问 this.i,将访问 A 的 i,执行 this.display(),将执行 B 的display()

我们来证明上述代码中 this 的编译时类型和运行时类型不同: 在 B 类中增加一个方法 sub()

public void sub() {}

而通过运行程序打印 this 的类型,结果却是 B

当变量的编译时类型和运行时类型不同时,调用它的实例方法和实例变量存在这种差异的原因,会在详解 Java 对象与内存控制(下) 继续讨论

8. 父类调用子类重写的方法

与父类访问子类的实例变量的情况一样,一般情况下,父类不能调用子类重写的方法,但在某种特殊情况下是可以的,看如下代码

class Animal {
    
    private String desc;
    
    public Animal() {
        this.desc = getDesc();
    }
    
    public String getDesc() {
        return "Animal";
    }

    @Override
    public String toString() {
        return desc;
    }
}

class Wolf extends Animal {
    
    private String name;
    private double weight;
    
    public Wolf(String name, double weight) {
        this.name = name;
        this.weight = weight;
    }

    @Override
    public String getDesc() {
        return "Wolf [name=" + name + ", weight=" + weight + "]";
    }   
}

public class Test {
    public static void main(String[] args) {
        System.out.println(new Wolf("灰太狼", 32.3));
    }
}

运行结果:
Wolf [name=null, weight=0.0]

分析:

  • (1) 程序执行 new Wolf("灰太狼", 32.3),会先调用 Animal(), 根据上一标题讨论的内容,我们知道在 Animal() 中调用的 getDesc() 为 Wolf 类的 getDesc()
  • (2) 但此刻还没有给 name 和 weight 赋值,它们具有默认初始值 null 和 0.0
  • (3) 在 执行完 Animal() 后,在执行 Wolf() 给 name 和 weight 赋值
  • (4) 打印 Wolf 对象,调用的是 Animal 类的 toString() 方法

修改 Animal 类:保证对 name 和 weight 的赋值在 getDesc() 方法之前执行

class Animal {
    
    public String getDesc() {
        return "Animal";
    }

    @Override
    public String toString() {
        return getDesc();
    }
}

可以避免之前的那种父类调用了子类重写的方法的情形

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏向治洪

前缀、中缀、后缀表达式

在函数式编程语言中,为了表示方便,出现了一些新的语法格式。所谓前缀、中缀、后缀表达式,它们之间的区别在于运算符相对与操作数的位置不同,为了说明它们的概念,首先来...

30550
来自专栏python成长之路

理解Python中的类对象、实例对象、属性、方法

17330
来自专栏V站

PHP常用系统内置函数,收藏以后别折磨自己写函数类了

15790
来自专栏鸿的学习笔记

有趣的Scala列表

其中Nil表示空列表,::符号表示在列表前面追加元素,所以如果没有后面的Nil,Scala就会报错。

6710
来自专栏Hongten

java开发_""和null的区别

18320
来自专栏鸿的学习笔记

Python和Scala的操作符

在聊完类和对象之后,我们要理解一件事,无论是在Scala还是Python,每一个值都是对象,在某种程度上来说,这两门语言都是更加纯粹的面向对象的语言。两者也都支...

6720
来自专栏mathor

1小时掌握c++面向对象编程

使用对象指针实参仅将对象的地址值传递给形参,而不进行副本的拷贝,这样可以提高运行效率,减少时间开销

8110
来自专栏北京马哥教育

Python Re 模块最全解读: 11703 字帮你彻底掌握

re模块下的函数 compile(pattern):创建模式对象 import re pat=re.compile('A') m=pat.search('CBA...

345100
来自专栏鸿的学习笔记

python的对象引用

Every object has an identity, a type and a value. An object’s identity never cha...

12830
来自专栏Java帮帮-微信公众号-技术文章全总结

String中的null,以及String s;等区别详解

1、判断一个引用类型数据是否null。 用==来判断。 2、释放内存,让一个非null的引用类型变量指向null。这样这个对象就不再被任何对象应用了。等待JVM...

37140

扫码关注云+社区

领取腾讯云代金券