今天,让我们来看一下类初始化的问题。
为了引出问题,我们先上代码,我们来看看他的输出结果时怎样的。
package wangcc.initialorder;
class Father{
private static int i=1;
private int t=2;
public int f=10;
static{
System.out.println("进入Father的static块");
System.out.println("i的值:"+i);
System.out.println("结束Father的static块");
}
public Father(){
System.out.println("===进入Father的无参构造方法");
System.out.println("t的值:"+t);
System.out.println("f的值:"+f);
System.out.println("i的值:"+i);
test();
System.out.println("===结束Father的无参构造方法");
}
public void test(){
System.out.println("Father的test()方法执行");
System.out.println("t的值:"+t);
System.out.println("Father的test()方法结束");
}
}
class Son extends Father{
private static int i=5;
private int t=10;
private int s=11;
static{
System.out.println("进入Son的static块");
System.out.println("i的值:"+i);
System.out.println("结束Son的static块");
}
public Son(){
System.out.println("===进入Son的无参构造方法");
System.out.println("t的值"+t);
System.out.println("f的值:"+f);
test();
System.out.println("===结束Son的无参构造方法");
}
public void test(){
System.out.println("Son的test()方法执行");
System.out.println("t的值:"+t);
System.out.println("f的值:"+f);
System.out.println("s的值:"+s);
System.out.println("Son的test()方法结束");
}
}
//1.父类静态成员和静态初始化快,按在代码中出现的顺序依次执行。2.子类静态成员和静态初始化块,按在代码中出现的顺序依次执行。
//3. 父类的实例成员和实例初始化块,按在代码中出现的顺序依次执行。4.执行父类的构造方法。
//5.子类实例成员和实例初始化块,按在代码中出现的顺序依次执行。6.执行子类的构造方法。
/**
* @ClassName: Test
* @Description: TODO(这里用一句话描述这个类的作用)
* @author wangcc
* @date 2017-3-29 上午11:12:10
* 成员初始化顺序:父静态->子静态->父变量->父初始化区->父构造–>子变量->子初始化区->子构造
*/
public class Test {
/**
* @Title: main
* @Description: TODO(这里用一句话描述这个方法的作用)
* @param @param args
* @return void 返回类型
* @throws
*/
public static void main(String[] args) {
// TODO Auto-generated method stub
Father f=new Son();
//Son s=new Son();
}
}
如果这是一道笔试题,你会怎样写答案呢,只需要写int类型的输出值即可,
按顺序写出,你会给出怎样的答案呢?
我们先给出类初始化的顺序
1.父类静态成员和静态初始化快,按在代码中出现的顺序依次执行。
2.子类静态成员和静态初始化块,按在代码中出现的顺序依次执行。
3. 父类的实例成员和实例初始化块,按在代码中出现的顺序依次执行
4. 执行父类的构造方法。
5. 子类实例成员和实例初始化块,按在代码中出现的顺序依次执行。
6. 执行子类的构造方法。
我们运行这个代码看一下,得到的输出是
进入Father的static块
i的值:1
结束Father的static块
进入Son的static块
i的值:5
结束Son的static块
===进入Father的无参构造方法
t的值:2
f的值:10
i的值:1
Son的test()方法执行
t的值:0
f的值:10
s的值:0
Son的test()方法结束
===结束Father的无参构造方法
===进入Son的无参构造方法
t的值10
f的值:10
Son的test()方法执行
t的值:10
f的值:10
s的值:11
Son的test()方法结束
===结束Son的无参构造方法
从输出结果,我们可以看出,先执行了父类的static块,然后执行了子类的static块,然后执行父类的构造方法,执行父类的构造方法时调用了test()方法,父类和子类中都有test()方法,我们很明显的看到调用的是子类的test()方法。最后进入子类的构造方法,其中调用了自己的test()方法。显然是符合这个初始化顺序的。
现在我们再对具体的代码进行分析一下:
首先,为什么调用父类的构造方法时调用的是子类的方法呢:
我们看下main方法中的这句代码:
Father f=new Son();
将一个子类对象赋给一个父类的引用。
之所以调用子类的test()方法其实很简单,因为动态绑定,这个我们之前有讲过。
子类的test()方法和父类的test()方法此时已经在方法区了,至于到底调用哪个方法,取决于你到底new了哪个对象,这里很明显new的是子类的对象。自然会调用子类的test()方法。
我们再关注下调用父类构造方法时test()的输出:
我们发现
Son的test()方法执行
t的值:0
f的值:10
s的值:0
Son的test()方法结束
t和s的值都为0,为什么是0呢?
你可能会说因为由于你告诉我子类的实例化成员在这时还没初始化呀,这里父类的构造方法都还没走完呢。
对,这里确实是没有对子类的实例化成员初始化。
那为什么是零,你会说因为如果没有赋值,那成员变量基本类型中的int类型就默认为0呀,对象类型就为null呀。对,没错,但是你仅仅一个默认说服不了我。
默认肯定也是初始化了的,只不过初始化为0了而已。
那我们现在来看下什么时候执行默认初始化。
我们看下面一段话:
在java中我们都知道绝大部分对象的创建时通过new 这个关键字来完成,当我们在自己的代码中写上new ClassName();//创建 ClassName类的一个实例
时,解释器当截取new这个关键字时,就会为ClassName量身定做一个内存空间,这个时候也就是为该类中的所有成员变量分配内存空间之时,
并对其进行最原始的初始化,所有引用类型将其制成null 基本数据类型为0;之后解释器会继续解释执行到 ClassName();这句话,也就是该类的
构造器,调用指定的类的构造方法(根据用户的需求初始化对象)。
这段话来自网络,按照这样的说法能够解释的通。(但是正确与否还有待确定,但是确实能解释)。
当我们new一个对象的时候,解释器当截取new这个关键字时,就会为ClassName量身定做一个内存空间,这个时候也就是为该类中的所有成员变量分配内存空间之时,并对其进行最原始的初始化,所有引用类型将其制成null 基本数据类型为0。
也就说这个时候以及执行了默认初始化了,然后才开始执行我们探讨的类初始化顺序。这时很明显,子类中的t,s均为0。
接下来我们说一下有父类的类的初始化过程
当我们试图去创建一个子类时,java解释器发现该类继承了其他类,所以就会先去创建其父类,切记这个时候并没有为子类分配任何的内存空间,
而是直接越过自己的创建过程去创建父类,如果检查到父类也继承了其他类,java解释器就会依此类推继续创建父类的父类。直到最后一个根父类
被分配内存后才会创建子类。而构造方法的调用则是从子类开始的,但是在子类的构造方法中必须去调用父类的构造方法。但是大多情况我们并不能显式的看到。
因为子类的无参构造方法会默认的调用父类的构造方法。
拿上述代码来讲。其实是省略了这一句
放在子类构造方法中的第一句super();调用了父类的无参构造方法。
public Son() {
super();
System.out.println("===进入Son的无参构造方法");
System.out.println("t的值" + t);
System.out.println("f的值:" + f);
test();
System.out.println("===结束Son的无参构造方法");
}
那我们什么时候需要显式的调用父类的构造方法呢。
package wangcc.fatherson;
class Father {
private String name;
private int id;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public Father(String name) {
System.out.println("Father有参构造方法");
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
class Son extends Father {
private int age;
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public Son(int age) {
this.age = age;
}
}
public class Test {
public static void main(String[] args) {
// TODO Auto-generated method stub
Son s = new Son(20);
s.setAge(21);
System.out.println(s.getAge());
}
}
上述的代码会报错,在这一行 public Son(int age)
为什么呢?
首先,当父类有显示的有参构造方法时,该类的默认的无参构造方法就没有了,子类无法隐式的调用父类的无参构造方法了,这个时候如果不显式的调用父类的构造方法会编译不通过。
为了解决这个问题:
1.在父类显式的实现无参构造方法,这样子类的构造方法依旧会隐式的实现父类的构造方法。其实我们只需要记住一点,子类的构造方法必须调用至少一个父类的构造方法,且在第一句调用,无论隐式还是显式。这样才能满足在初始化子类之前先初始化父类。
2.子类调用一个显式的父类构造方法。
通过研究类初始化的顺序,我们可以对类变量,实例变量,局部变量的分配内存空间和初始化时间做一个总结了。
类变量在链接的时候分配存储空间,但是不赋值,在类初始化的时候赋值初始化。
实例变量在,解释器当截取new这个关键字时,就会为ClassName量身定做一个内存空间,这个时候也就是为该类中的所有成员变量分配内存空间,此时执行默认初始化,普通对象类型为null。在类初始化时赋值初始化。
局部变量,在编译时就分配好了内存空间:局部变量表(位于虚拟机栈),在方法执行时空间不会被改变。之所以可以再编译时就分配好内存空间是因为虚拟机栈是线程私有的,在任一时刻只可能有一个方法进入该局部变量表。所以可以提前分配内存空间,赋值的时候当然就是方法进入的时候赋值。