今天在写构造器方法的时候,遇到了一个小问题,由这个问题引发了一连串的思考,在一个Java类中变量与类的初始化执行顺序是什么样的呢?
## 发现问题
class Student{
private String name;
void setName(String name){
this.name = name;
}
String getName(){
return name;
}
Student(){
//this(this.name);
this(name);
System.out.println("题目要求写一个无参的构造器");
}
Student(String name){
this.name = name;
}
}
class TestStudent{
public static void main(String[] args){
Student stu1 = new Student();
System.out.print(stu1.getName());
Student stu2 = new Student("老大");
System.out.println(stu2.getName());
}
}
此时会报错:无法在调用超类型构造器之前引用name.
在使用构造器创建对象时,此时的成员变量name的值是否已经完成初始化,无参构造中调用它时报的这个错意味着什么。我们本篇博客就来讨论一下,一个类创建对象时到底做了哪些事?
## 思考问题
首先,对于一个类来说加载分为五个部分,分别是静态变量,静态代码块,非静态变量,非静态代码块以及构造器。
### 单个类成员加载顺序
测试代码:
class Student{
// 静态变量
static String name;
// 静态代码块
static{
System.out.println("刚运行到静态代码块时的静态变量值:"+name);
name = "静态name值";
System.out.println("静态代码块结束时的静态变量值:"+name);
}
//定义一个无参构造器
Student(){
System.out.println("刚运行到构造器时的静态变量值:"+name);
name = "这是一个无参的构造器";
System.out.println("构造器结束时的静态变量值:"+name);
}
//定义一个非静态变量
String name2;
//定义一个非静态代码块
{
System.out.println("刚运行到非静态代码块时的非静态变量值:"+name2);
name2 = "非静态name值";
System.out.println("非静态代码块结束时的非静态变量值:"+name2);
}
}
class TestStudent{
public static void main(String[] args){
Student stu = new Student();
}
}
此时编译代码执行的结果是:
```
刚运行到静态代码块时的静态变量值:null
静态代码块结束时的静态变量值:静态name值
刚运行到非静态代码块时的非静态变量值:null
非静态代码块结束时的非静态变量值:非静态name值
刚运行到构造器时的静态变量值:静态name值
构造器结束时的静态变量值:这是一个无参的构造器
```
由此可以看出,当我们声明的类成员变量是一个静态成员变量的时候,在调用构造器之前,我们的静态成员变量已经生成并初始化成相应的数据类型的默认值(即此处String对象的默认值位null)。
然后在静态代码块中,我们将静态变量赋值,然后程序跳转到非静态变量声明与赋值。再执行非静态代码块,最后直行到程序的无参构造器。
所以,通过此程序代码,我们得出结论:单个类的程序加载顺序是:静态变量-->静态代码块-->非静态变量-->非静态代码块-->构造器。
也就是说调用构造器时,静态与非静态的属性都已经完成初始化工作了,this(name)调用报错与name属性本身没有关系。
### 深入思考类加载顺序
既然说到加载顺序,那么我们继续完成类成员的加载顺序。关于变量与代码块之间的关系,或者说根据我们上面的这段代码得出这个初步的结论我们还有待商榷,因为,我们的程序加载的顺序是自上而下的,也就是说,我们的得到的这个结论有可能是因为我们习惯性的排版导致的,我们声明各部分的顺序偶可能影响我们得出的结论。为了确定我们程序的严谨性,我们需要进一步的调整代码的顺序,来加强验证我们代码实验的逻辑严谨性。
public class Student2 {
// 静态代码块放到前面,此时name还未声明,所以会报错
static{
System.out.println("刚运行到静态代码块时的静态变量值:"+name);
name = "静态name值";
System.out.println("静态代码块结束时的静态变量值:"+name);
}
// 静态变量
static String name;
//定义一个无参构造器
Student2(){
System.out.println("刚运行到构造器时的静态变量值:"+name);
name = "这是一个无参的构造器";
System.out.println("构造器结束时的静态变量值:"+name);
}
//定义一个非静态代码块
{
System.out.println("刚运行到非静态代码块时的非静态变量值:"+name2);
name2 = "非静态name值";
System.out.println("非静态代码块结束时的非静态变量值:"+name2);
}
//定义一个非静态变量
String name2;
}
class TestStudent{
public static void main(String[] args){
Student stu = new Student();
Student2 stu2 = new Student2();
}
}
上面代码运行的结果:
```
Error:(6, 48) java: 非法前向引用
```
此时将代码块拿到变量声明的前面我们的代码出现了错误提示,这说明了我们一开始得到的结论并不严谨,我们这里可以得出代码块的执行是在变量声明之前的。
所以,我们可以根据常识大胆的猜想,单个类程序加载的顺序是静态-->非静态-->构造器,其中变量声明与代码块的执行顺序与代码前后位置有关,并没有严格的前后之分,程序员将代码写在前边的的先执行。
### 验证猜想
public class Student3 {
//定义一个无参构造器
Student3(){
System.out.println("刚运行到构造器时的静态变量值:"+name);
name = "这是一个无参的构造器";
System.out.println("构造器结束时的静态变量值:"+name);
}
//定义一个非静态代码块
{
name2 = "非静态name值";
System.out.println("非静态代码块结束时的静态变量值:"+name);
}
//定义一个非静态变量
String name2;
// 静态代码块
static{
System.out.println("运行到静态代码块");
// name = "静态代码块里赋的值";
}
// 静态变量
static String name;
}
class TestStudent{
public static void main(String[] args){
Student3 stu3 = new Student3();
}
}
上面代码执行的结果:
```
运行到静态代码块
非静态代码块结束时的静态变量值:null
刚运行到构造器时的静态变量值:null
构造器结束时的静态变量值:这是一个无参的构造器
```
基本验证了我们的猜想是正确的,但是在结尾我又做了一个有趣的测试。
### 测试
public class Student3 {
//定义一个无参构造器
Student3(){
System.out.println("刚运行到构造器时的静态变量值:"+name);
name = "这是一个无参的构造器";
System.out.println("构造器结束时的静态变量值:"+name);
}
//定义一个非静态代码块
{
name2 = "非静态name值";
System.out.println("非静态代码块结束时的静态变量值:"+name);
}
//定义一个非静态变量
String name2;
// 静态代码块
static{
System.out.println("运行到静态代码块");
name = "静态代码块里赋的值";// 按道理说,我们这里没有声明就直接赋值操作了
}
// 静态变量
static String name;
}
class TestStudent{
public static void main(String[] args){
Student3 stu3 = new Student3();
}
}
上面代码执行的结果:
```
运行到静态代码块
非静态代码块结束时的静态变量值:静态代码块里赋的值
刚运行到构造器时的静态变量值:静态代码块里赋的值
构造器结束时的静态变量值:这是一个无参的构造器
```
也就是说在静态代码块里,我们无法引用后面的静态变量,但是我们编译之前可以对他进行赋值,并且在后面的非静态代码块里我们还可以取到里面的值,再次做出假设,这是java虚拟机在编译时不让向前引用,此时的变量其实已经完成了声明初始化等一系列操作(都是存在方法区),只是通过不了编译而已。
所以我认为,我们最早得到的结论应该才是正确的Java程序整个加载流程的顺序。
这中间也可能时JDK和IDE一起努力做了点什么,但是实际上也不影响,我们的代码块的存在本来就是为了完成变量的初始化工作的,所以将代码块放到属性声明之前是毫无意义的操作,所以这里只是遇到了测试一下而已,实际操作中毫无意义。
## 得出结论
总结:在一个类中,初始化顺序为:
1. 静态变量,静态变量初始化;
2. 静态代码块;
3. 非静态变量初始化;
4. 非静态代码块;
5. 构造器。
##最后
到这里,我们理清楚了单个类中的各部分的加载顺序,但是我们文章一开始提到的问题并没有解决,如果构造器执行是在静态和非静态属性及代码块之后的话,此时的成员变量应当已经有了初始化值了,再不济成员变量还有一个初始的null值,但是这里报了无法在调用超类型构造器之前引用name。
这说明这里调用name关系到了这个类的父类构造器,所以我们后面继续探讨类加载在继承中的加载顺序,就可以解决这个问题了。