文章目录
- 一、赋值顺序
- (1)赋值的位置及顺序
- (2)举例
- (3)字节码文件
- (4)进一步探索
- (5)最终赋值顺序
- (6)实际开发如何选
- 二、(超纲)关于字节码文件中的<init>
- 三、面试题
- (1)面试题1
- (2)面试题2
- (3)面试题3
- (4)面试题4
一、赋值顺序
(1)赋值的位置及顺序
- 可以给类的非静态的属性(即实例变量)赋值的位置有:
① 默认初始化
② 显式初始化
⑤ 代码块中初始化
③ 构造器中初始化
#############################
④ 有了对象以后,通过"对象.属性"或"对象.方法"的方法进行赋值
(造对象之前叫初始化,造对象之后叫赋值)
- 执行的先后顺序:
① - ② - ③ - ④
⑤ 代码块中初始化应该放在哪?
(2)举例
【举例】
先看一段代码:
package yuyi06;/*** ClassName: FieldTest* Package: yuyi06* Description:** @Author 雨翼轻尘* @Create 2023/11/19 0019 16:25*/
public class FieldTest {public static void main(String[] args) {Order o1=new Order();System.out.println(o1.orderId); //1}
}class Order{int orderId=1;}
输出结果:
这个很简单,显而易见。
现在整一个代码块,看一下它和显式赋值谁先谁后。
public class FieldTest {public static void main(String[] args) {Order o1=new Order();System.out.println(o1.orderId); //2}
}class Order{int orderId=1;{orderId=2;}}
输出结果:
那么一定是先有1,后有2。所以代码块初始化肯定是在显示初始化之后。
接下来是构造器和代码块。
创建一个空参构造器,那么在创建对象的时候一定会调用它。
ublic class FieldTest {public static void main(String[] args) {Order o1=new Order();System.out.println(o1.orderId); //3}
}class Order{int orderId=1;{orderId=2;}public Order(){orderId=3;}}
输出结果:
结果是3,所以3将2覆盖了。所以代码块初始化在构造器初始化前面。
所以目前来看,执行顺序是这样的:① - ② - ⑤ - ③ - ④
(3)字节码文件
将光标放在Order类中,看一下字节码文件。
插件在这里:
先运行然后重新编译一下,确保生成的字节码文件和代码一致。
然后点击这个即可:
构造器会以<init>
方法的方式呈现在字节码文件中,如下:
看一下代码:
方法里面对应的是个栈帧,栈帧里面会放局部变量,
aload_0
就是指局部变量第0个位置–this
,表示当前正在创建的对象,通过aload_0调用现在的方法。
如下:
画个图解释一下Code的意思:
(4)进一步探索
根据上面得出来的结论,代码块赋值在显式赋值之后,那么将它们俩的代码换个位置呢?
如下:
public class FieldTest {public static void main(String[] args) {Order o1=new Order();System.out.println(o1.orderId); }
}class Order{{orderId=2;}int orderId=1;public Order(){//orderId=3;}}
输出结果:
怎么是1了呢?肯定是先有2,后有1。
看字节码文件:
这样来看,代码块赋值又先于了显示赋值。
刚才的① - ② - ⑤ - ③ - ④ 明显不太对。
②和⑤就是看谁先声明,谁就先执行。
所以应该是这样的:① - ②/⑤ - ③ - ④
💬为啥将代码块写在显示赋值上面,不会报错,这时候变量还未声明啊?
其实这个地方一直有个误区,举个例子:
可以看到,在eat()
方法中可以调用sleep()
方法。
若是按照刚才的说法,先有eat(),后有sleep(),怎么一上来就可以sleep(),此时sleep()还没有声明啊,但是怎么没有报错?
我们只需要考虑,编译的时候,会看到eat()里面调用了sleep()方法,这个方法找一下有没有,发现有,那能确保调用sleep()的时候,内存中有吗?
其实在加载类的时候(将类放入了方法区),其实sleep()也好,eat()也好,方法都加载了的。
所以只需要确保调用这个方法之前,这个方法加载了就行。
回到这里:
//代码块赋值
{orderId=2;
}//显示赋值
int orderId=1;
现在这种情况也可以用类似的方式去解释,以后再说类加载的详细过程,现在就说最核心的点。
orderId
在整个类的加载中有一个过程,在其中某一个环节,就已经将orderId给加载了,而且还给了一个默认赋值0,这个时候orderId属性就已经有了。在后续的环节中,才开始做显示赋值和代码块的赋值。
现在是先有代码块的赋值,那么就将orderId改为2,后面又显示赋值,将它改为1。
一般习惯将代码块写显示赋值的下面
(5)最终赋值顺序
可以给类的非静态的属性(即实例变量)赋值的位置有:
① 默认初始化
② 显式初始化 或 ⑤ 代码块中初始化
③ 构造器中初始化
#############################
④ 有了对象以后,通过"对象.属性"或"对象.方法"的方法进行赋值
(造对象之前叫初始化,造对象之后叫赋值)
执行的先后顺序:
① - ②/⑤ - ③ - ④
(6)实际开发如何选
💬 给实例变量赋值的位置很多,开发中如何选?
显示赋值
:比较适合于每个对象的属性值相同的场景。构造器中赋值
:比较适合于每个对象的属性值不相同的场景(通过形参的方式给它赋值)。非静态代码块
:用的比较少,在构造器里面基本能完成。静态代码块
:静态(与类相关)属性不会选择在构造器(与对象相关)中赋值。静态的变量要么默认赋值,要么显示赋值,要么代码块中赋值。
二、(超纲)关于字节码文件中的
(超纲)关于字节码文件中的的简单说明(通过插件jclasslib bytecode viewer
查看)
刚才查看字节码文件的时候,可以看到,这里做个简单说明,便于大家理解。
🚗说明
①<init>
方法在字节码文件中可以看到。每个方法都对应着一个类的构造器。(类中声明了几个构造器就会有几个)
既然构造器和一 一对应,在字节码文件中也看不到“构造器”这一项。
所以构造器就是以方法的形式呈现在字节码文件中的。
比如这里声明了俩构造器:
class Order{{orderId=2;}int orderId=1;public Order(){//orderId=3;}public Order(int orderId){this.orderId=orderId;}public void eat(){sleep();}public void sleep(){}}
看一下字节码文件有两个,如下:
点开第二个,一起来看一下它的Code:
角标为1的值:
所以通过第二个有参构造器去造对象的时候,也会有显示赋值和代码块的执行,然后才是构造器。对应字节码文件中就是方法。
②编写的代码中的构造器在编译以后就会以<init>
方法的方式呈现。(方法和构造器不是一回事)
③<init>
方法内部的代码包含了实例变量的显示赋值、代码块中的赋值和构造器中的代码。
④<init>
方法用来初始化当前创建的对象的信息的。
构造器和方法不是一回事,字节码文件中没有“构造器”,是以方法的形式呈现的。
三、面试题
(1)面试题1
下面代码输出结果是?
package yuyi06;//技巧:由父及子,静态先行。class Root{//静态代码块static{System.out.println("Root的静态初始化块");}//非静态代码块{System.out.println("Root的普通初始化块");}//构造器public Root(){super();System.out.println("Root的无参数的构造器");}
}class Mid extends Root{static{System.out.println("Mid的静态初始化块");}{System.out.println("Mid的普通初始化块");}public Mid(){System.out.println("Mid的无参数的构造器");}public Mid(String msg){//通过this调用同一类中重载的构造器this();System.out.println("Mid的带参数构造器,其参数值:"+ msg);}
}class Leaf extends Mid{static{System.out.println("Leaf的静态初始化块");}{System.out.println("Leaf的普通初始化块");}public Leaf(){//通过super调用父类中有一个字符串参数的构造器super("雨翼轻尘");System.out.println("Leaf的构造器");}
}public class LeafTest{public static void main(String[] args){new Leaf(); //涉及到当前类,以及它的父类、父类的父类的加载包括相应功能的执行// System.out.println();// new Leaf();}
}
🤸分析
new Leaf();
涉及到当前类,以及它的父类、父类的父类的加载包括相应功能的执行。
分析先后执行的顺序。
上面的类中,分别都有静态代码块、非静态代码块和构造器。
首先应该是静态代码块
,进行类加载的时候,一定先加载父类的,然后才是子类。
之前说的方法的重写,一定是先有父类的方法,才能覆盖它。(先加载父类)
当我们通过leaf()
造对象,首先会通过super()
找到父类。(没有写也是super)
画个图看一下逻辑:
所以最先加载的类是Object
,只不过改不了代码,也没有输出语句,
所以看似好像没加载,其实是先加载它,其次是Root类,然后就是Root类里面的static代码块
,下面的非静态代码块和无参构造器就别先执行了,因为要先将类的加载都执行了。
如下:
所以,看一下执行结果:(前面三行是“静态初始化块”)
类的加载
就完成了。
下面才涉及造对象。
静态加载之后,先去new了一个leaf(),然后执行super(),一直到最上层的Root类,先考虑它的构造器的加载(涉及到非静态结构的加载,然后才是子类),代码块的执行又早于构造器,所以会先输出代码块中的内容。
刚才说到调用的过程如下:
输出的话,就是反过来:
运行结果如下:
技巧:由父及子,静态先行。(先加载父类,后加载子类,静态结构早于非静态(init方法)的,非静态代码块的执行又早于构造器的执行)
方法包括代码块的,每个构造器都默认调用父类的构造器。
方法不是通过对象.
去调用的,而是自动执行的。
(2)面试题2
下面代码输出结果是?
class HelloA {public HelloA() {System.out.println("HelloA");}{System.out.println("I'm A Class");}static {System.out.println("static A");}
}class HelloB extends HelloA {public HelloB() {System.out.println("HelloB");}{System.out.println("I'm B Class");}static {System.out.println("static B");}}public class Test01 {public static void main(String[] args) {new HelloB();}
}
🤸分析
画个图演示一下:
执行输出顺序:①->②->③->④->⑤->⑥
先将类的加载搞定。
HelloA中,有静态先调用静态,输出“static A”,
然后回到HelloA中,调用静态,输出“static B”。
然后考虑当前要创建的对象的构造器HelloB(),此时第一行会调用super(),
调用HelloA()构造器。
再HelloA()构造器中,有非静态代码块,先执行它,输出“I’m A Class”,
然后输出构造器中“HelloA”。
super()执行结束之后,回到HelloB(),此时HelloB类中也有非静态代码块,
所以先输出代码块中“I’m B Class”,最后输出HelloB()构造器中“HelloB”。
👻代码运行结果
(3)面试题3
下面代码输出结果是?
public class Test02 {static int x, y, z;static {int x = 5;x--;}static {x--;}public static void method() {y = z++ + ++z;}public static void main(String[] args) {System.out.println("x=" + x);z--;method();System.out.println("result:" + (z + y + ++z));}
}
🤸分析
画个图:(执行顺序:①->②->③->④->⑤->⑥)
👻输出结果:
(4)面试题4
下面代码输出结果是?
public class Test03 {public static void main(String[] args) {Sub s = new Sub();}
}
class Base{Base(){method(100);}{System.out.println("base");}public void method(int i){System.out.println("base : " + i);}
}
class Sub extends Base{Sub(){super.method(70);}{System.out.println("sub");}public void method(int j){System.out.println("sub : " + j);}
}
🤸分析
画个图:(执行顺序:①->②->③->④->⑤->⑥->⑦->⑧)
🚗调试
大家也可以自行调试,这里就做个示范。
👻输出结果: