不管是类变量还是实例变量,你都不能引用一个还没有定义的变量,或者在引用之前没有定义的变量,如下图所示:
但以下代码是完全正确的:
因为:类变量总是比实例变量先初始化
看如下代码:
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 的值
通过实例变量访问或修改类变量,操作的都是类的内存空间中的变量
在 Java 中,可以通过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]
分析:
把程序中定义实例变量时初始化的语句和费静态代码块调换位置:
// 定义时静态变量时指定初始值
double weight = 2.3;
{
System.out.println("执行非静态代码块...");
// 在非静态代码块中初始化实例变量
weight = 2.0;
}
再运行程序结果为:
执行非静态代码块...
执行构造器...
Cat [name=Tom, age=2, weight=2.0]
分析:
总结:
类变量属于类本身,程序初始化类的时候会一并为该类的类变量分配内存空间并执行初始化,JVM 对一个类只初始化一次,因此 Java 程序每运行一次,系统只为类变量分配一次内存空间,执行一次初始化,程序可以在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
分析:
有如下继承结构:
如果在程序中创建 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
说明:
子类的方法可以访问父类的实例变量,这是因为子类继承父类就会获得父类的实例变量和方法,但父类不能访问子类的实例变量,因为父类根本不知道它将被哪个子类继承,但在极端的情况下,可能出现父类访问子类变量的情况,看如下代码:
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
分析:
修改一下 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
分析:
我们来证明上述代码中 this 的编译时类型和运行时类型不同:
在 B 类中增加一个方法 sub()
public void sub() {}
而通过运行程序打印 this 的类型,结果却是 B
当变量的编译时类型和运行时类型不同时,调用它的实例方法和实例变量存在这种差异的原因,会在详解 Java 对象与内存控制(下) 继续讨论
与父类访问子类的实例变量的情况一样,一般情况下,父类不能调用子类重写的方法,但在某种特殊情况下是可以的,看如下代码
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]
分析:
修改 Animal 类:保证对 name 和 weight 的赋值在 getDesc() 方法之前执行
class Animal {
public String getDesc() {
return "Animal";
}
@Override
public String toString() {
return getDesc();
}
}
可以避免之前的那种父类调用了子类重写的方法的情形