一,疑问
从学习java至今,我一直对构造方法存在很多疑问,在此将我的疑问throw,你们可以catch到吗?
面试官 :你说下构造方法吧!
我 :((⊙o⊙)… ,构造方法有什么聊的,对象的new不是一直在用吗?) 构造方法是一种特殊的方法,它是一个与类同名且没有返回值类型的方法。对象的创建就是通过构造方法来完成,其功能主要是完成对象的初始化。当类实例化一个对象时会自动调用构造方法。构造方法和其他方法一样也可以重载。
面试官 :你说构造方法功能是完成对象的初始化,类默认的构造方法怎么完成对象的初始化,它里面有没有代码哦!!!
我:(是啊,默认的构造方法没有代码,有也是super,怎么初始化的)这个... 属性不是都有默认值得吗? int类型是0,引用类型是null....
面试官 :解释下 private int i = 100; 这个i的值也是 0吗? 如果不是 0,是100,什么时候给赋的值? 或者是构造方法赋的值吗? 如果不是构造方法赋的值,代码里能看到那条代码给赋的值吗?
我 :代码里的int i = 100;不是给赋了100吗,就是这条代码给赋的值。
面试官:额,方法的调用,执行估计你也应该清楚,没有特殊逻辑基本都是顺序执行,Student stu =new Student(); 调用了Student的无参构造方法,这个构造方法基本没什么代码,你也清楚,请问 private int i = 100; 这条代码什么时候执行,构造方法显然没有调用这个代码!对吧!
我 :(挠头,代码没有执行这条复制语句啊?)可能是Java的设计者在构造方法中隐式执行了属性的赋值语句....(我tm机智了,哈哈哈)
面试官 :正确与否先不说,你的说法可以解释通,还有个问题,我要在构造方法里面打印这个 i ,会不会存在赋值与打印并发的现象,或者我打印在前,赋值在后引起了错误?
我 : 我想规则应该是赋值永远在代码最前面,不会出现这个问题...(松了口气,应该可以了吧!)
面试官:根据你的逻辑,能否解释下 public static String stuClass = "码农大学-颈椎病康复班";
我 : 解释什么???(一头雾水)
面试官: 我的意思是,这stuClass是个静态属性,我可以直接通过类名调用,我并没有new Studnet(),没有调用构造方法,这个stuClass是怎么赋值的?你的构造方法隐式赋值理论貌似解释不通啊?
我: 这个...... (我的心在滴血.....)
我的部分疑问,通过面试中的博弈呈现出来(面试过程为自己思考中的博弈,并非真实发生的),您能否解释这些问题....
- new Student() 的过程发生了什么?
- 构造方法干了什么?
- 属性在什么时候初始化(赋值)?
- 静态属性在什么时候初始化(赋值)?
- .............
二,分析
接下来我们通过代码分析。
2.1 静态变量的赋值与静态代码块的执行
父类相关代码
//父类
public class Parent {
/**父类的静态属性*/
public static String preson;
public static String presonn = "人类";
/**父类的属性*/
protected String name;
protected String namee = "码农中的吴秀波";
protected int age;
protected int agee = 18;
/**
* 父类的初始化代码块
*/
{
System.out.println("Parent init ...");
System.out.println("Parent init age:" + age);
System.out.println("Parent init name:"+ name);
System.out.println("Parent init namee:"+ namee);
namee = "码农中的吴彦祖";
}
/**
* 父类中的静态初始化代码块
*/
static{
System.out.println("Parent cinit ...");
System.out.println("Parent cinit preson:"+preson);
System.out.println("Parent cinit presonn:" + presonn );
presonn = "人";
}
/**
* 父类的无参构造方法
*/
public Parent() {
// TODO Auto-generated constructor stub
System.out.println("Parent constructor ...");
System.out.println("Parent constructor namee:"+ namee);
}
}
子类的相关代码
//子类
public class Student extends Parent{
/**子类的静态属性*/
public static String stuclass = "码农大学-颈椎病康复班";
/**子类的属性*/
private int stuNum = 10001;
/**
* 子类的初始化代码块
*/
{
System.out.println("Student init ...");
System.out.println("Student init stuNum :"+ stuNum);
}
/**
* 子类的静态代码块
*/
static{
System.out.println("Student cinit ...");
System.out.println("Student cinit stuclass:"+ stuclass);
}
/**
* 子类无参构造方法
*/
public Student() {
// TODO Auto-generated constructor stub
System.out.println("Student constructor ...");
}
public static void main(String[] args) {
}
}
代码相对比较简单,如果只关心程序入口,即子类Student的main方法,运行这个main方法后,您能预计结果是什么吗?
//console 输出
Parent cinit ...
Parent cinit preson:null
Parent cinit presonn:人类
Student cinit ...
Student cinit stuclass:码农大学-颈椎病康复班
输出的结果和您预期的一样吗?
通过输出结果我们可知,一个空的main方法的运行,执行顺序为:
- 父类静态变量的初始化(静态代码块可以输出静态变量的值);
- 父类静态代码块的执行(静态代码块可以重新给静态变量赋值,暂不演示了);
- 子类静态变量的初始化;
- 子类静态代码块的执行;
疑问一 :main 方法中并未出现代码,main方法的执行为什么会有如上的输出???
2.2 变量的赋值、初始化代码的运行以及构造方法的运行
public static void main(String[] args) {
Student stu = new Student();
}
执行如上代码,您能预测出输出结果吗?
//console 输出
Parent cinit ...
Parent cinit preson:null
Parent cinit presonn:人类
Student cinit ...
Student cinit stuclass:码农大学-颈椎病康复班
--------------------疯哥线---------------------------
Parent init ...
Parent init age:0
Parent init name:null
Parent init namee:码农中的吴秀波
Parent constructor ...
Parent constructor namee:码农中的吴彦祖
Student init ...
Student init stuNum :10001
Student constructor ...
和您预期的一样吗?
通过输出结果可知,mian 方法以及 Student stu = new Student();代码,执行了
- 静态相关输出,同空的mian 方法
- 父类变量的初始化
- 父类的初始化代码块
- 父类的构造方法
- 子类变量的初始化
- 子类的初始化代码块
- 子类的构造方法
疑问二: Student stu = new Student(); 此语句按常理应该只执行父类和子类的构造方法,为什么会有如此的输出(执行流程)?
三,解释-结论-注意
3.1 疑问一的解释
Java程序语言 -> 编译器 -> .class -> JVM运行.class ;
Java编译器会把静态变量的初始化和静态代码块顺序在字节码中生成为<clinit>方法,此方法称之为类的构造器;
虚拟机运行字节码有如下步骤(想要深入了解可以学习JVM、字节码相关知识):加载->连接(验证、准备、解析)->初始化->使用->卸载
初始化(执行<clinit>方法):
- 遇到new、getstatic、putstatic、invokestatic执行初始化操作
- 使用java.lang.reflect包的方法对类进行调用时,如果类没有进行过初始化,则需要先触发其初始化
- 当初始化一个类时,如果发现父类还没有进行过初始化过,则需要先触发其父类的初始化
- 当JVM启动时,用户需要指定一个要执行的类(包含main()方法的那个类),虚拟机会先初始化这个类
如上的初始化(加粗)步骤,完美解释了 疑问一:main 方法中并未出现代码,main执行为什么如上的输出???
虚拟机启动,我们指定了student类作为启动类(main方法),虚拟机加载...初始化这个类,即运行字节码中的<clinit>方法,发现父类也没有初始化,触发其父类的初始化;
问题:您能举出不被初始化的例子吗???
通过 javap -verbose Student.class 命令查看类的字节码
![](https://i-blog.csdnimg.cn/blog_migrate/cbd45258d9e09a294b1ad9425ec3c6a2.png)
3.2 疑问二的解释
![](https://i-blog.csdnimg.cn/blog_migrate/c4bd20f716c8aeb2f35e2c2b39b50625.png)
通过main()的字节码可以看出,Student stu = new Student();转换成字节码指令显然不是一条(JVM是以一条条指令为单位运行的),指令invokespecial:调用实例初始化,父类初始化和私有方法(调用了<init>方法)。
<init>:会将变量初始化,初始化语句块,构造器方法等操作顺序封装到该方法中(父类先于子类运行);
这样就可以解释疑问二了,Student stu = new Student();虽然只有一条代码,但虚拟机运行的字节码指令可不单单一条指令;
3.3 总结
<clinit>方法是在类加载过程中执行的,而<init>是在对象实例化执行的,所以<clinit>一定比<init>先执行。即执行顺序为:
- 父类的静态变量初始化
- 父类的静态代码块
- 子类的静态变量初始化
- 子类的静态代码块
- 父类的变量初始化
- 父类的初始化代码块
- 父类的构造方法
- 子类的变量初始化
- 子类的初始化代码块
- 子类的构造方法