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();
}
}
可以避免之前的那种父类调用了子类重写的方法的情形