• Java面向对象(高级)-- 类中属性赋值的位置及过程


    一、赋值顺序

    (1)赋值的位置及顺序

    • 可以给类的非静态的属性(即实例变量)赋值的位置有:

    ① 默认初始化

    ② 显式初始化

    ⑤ 代码块中初始化

    ③ 构造器中初始化

    #############################

    ④ 有了对象以后,通过"对象.属性"或"对象.方法"的方法进行赋值

    (造对象之前叫初始化,造对象之后叫赋值)

    • 执行的先后顺序:

    ① - ② - ③ - ④

    代码块中初始化应该放在哪?

    image.png

    (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;
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    输出结果:

    image.png

    这个很简单,显而易见。

    现在整一个代码块,看一下它和显式赋值谁先谁后。

    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
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    输出结果:

    image.png

    那么一定是先有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;
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    输出结果:

    image.png

    结果是3,所以3将2覆盖了。所以代码块初始化在构造器初始化前面

    所以目前来看,执行顺序是这样的:① - ② - ⑤ - ③ - ④

    (3)字节码文件

    将光标放在Order类中,看一下字节码文件。

    插件在这里:

    image.png

    先运行然后重新编译一下,确保生成的字节码文件和代码一致。

    image.png

    然后点击这个即可:

    image.png

    构造器会以方法的方式呈现在字节码文件中,如下:

    image.png

    看一下代码:

    image.png

    方法里面对应的是个栈帧,栈帧里面会放局部变量,

    aload_0 就是指局部变量第0个位置–this,表示当前正在创建的对象,通过aload_0调用现在的方法。

    如下:

    image.png

    画个图解释一下Code的意思:

    image.png

    (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
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    输出结果:

    image.png

    怎么是1了呢?肯定是先有2,后有1。

    看字节码文件:

    image.png

    这样来看,代码块赋值又先于了显示赋值。

    刚才的① - ② - ⑤ - ③ - ④ 明显不太对。

    ②和⑤就是看谁先声明,谁就先执行。

    所以应该是这样的:① - ②/⑤ - ③ - ④


    💬为啥将代码块写在显示赋值上面,不会报错,这时候变量还未声明啊?

    其实这个地方一直有个误区,举个例子:

    image.png

    可以看到,在eat()方法中可以调用sleep()方法。

    若是按照刚才的说法,先有eat(),后有sleep(),怎么一上来就可以sleep(),此时sleep()还没有声明啊,但是怎么没有报错?

    我们只需要考虑,编译的时候,会看到eat()里面调用了sleep()方法,这个方法找一下有没有,发现有,那能确保调用sleep()的时候,内存中有吗?

    其实在加载类的时候(将类放入了方法区),其实sleep()也好,eat()也好,方法都加载了的。

    所以只需要确保调用这个方法之前,这个方法加载了就行

    回到这里:

    //代码块赋值
    {
        orderId=2;
    }
    
    //显示赋值
    int orderId=1;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    现在这种情况也可以用类似的方式去解释,以后再说类加载的详细过程,现在就说最核心的点。

    orderId在整个类的加载中有一个过程,在其中某一个环节,就已经将orderId给加载了,而且还给了一个默认赋值0,这个时候orderId属性就已经有了。在后续的环节中,才开始做显示赋值和代码块的赋值

    现在是先有代码块的赋值,那么就将orderId改为2,后面又显示赋值,将它改为1。

    一般习惯将代码块写显示赋值的下面

    (5)最终赋值顺序

    可以给类的非静态的属性(即实例变量)赋值的位置有:

    ① 默认初始化

    ② 显式初始化 或 ⑤ 代码块中初始化

    ③ 构造器中初始化

    #############################

    ④ 有了对象以后,通过"对象.属性"或"对象.方法"的方法进行赋值

    (造对象之前叫初始化,造对象之后叫赋值)

    执行的先后顺序

    ① - ②/⑤ - ③ - ④

    (6)实际开发如何选

    💬 给实例变量赋值的位置很多,开发中如何选?

    • 显示赋值:比较适合于每个对象的属性值相同的场景。
    • 构造器中赋值:比较适合于每个对象的属性值不相同的场景(通过形参的方式给它赋值)。
    • 非静态代码块:用的比较少,在构造器里面基本能完成。
    • 静态代码块:静态(与类相关)属性不会选择在构造器(与对象相关)中赋值。静态的变量要么默认赋值,要么显示赋值,要么代码块中赋值。

    二、(超纲)关于字节码文件中的

    (超纲)关于字节码文件中的的简单说明(通过插件jclasslib bytecode viewer查看)

    刚才查看字节码文件的时候,可以看到,这里做个简单说明,便于大家理解。

    🚗说明

    方法在字节码文件中可以看到。每个方法都对应着一个类的构造器。(类中声明了几个构造器就会有几个

    既然构造器和一 一对应,在字节码文件中也看不到“构造器”这一项。

    image.png

    所以构造器就是以方法的形式呈现在字节码文件中的。

    比如这里声明了俩构造器:

    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(){
    
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    看一下字节码文件有两个,如下:

    image.png

    点开第二个,一起来看一下它的Code:

    image.png

    角标为1的值:

    image.png

    所以通过第二个有参构造器去造对象的时候,也会有显示赋值和代码块的执行,然后才是构造器。对应字节码文件中就是方法。

    ②编写的代码中的构造器在编译以后就会以方法的方式呈现。(方法和构造器不是一回事)

    方法内部的代码包含了实例变量的显示赋值、代码块中的赋值和构造器中的代码

    方法用来初始化当前创建的对象的信息的

    构造器和方法不是一回事,字节码文件中没有“构造器”,是以方法的形式呈现的。

    三、面试题

    (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();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66

    🤸分析

    new Leaf(); 涉及到当前类,以及它的父类、父类的父类的加载包括相应功能的执行。

    分析先后执行的顺序。

    上面的类中,分别都有静态代码块、非静态代码块和构造器。

    首先应该是静态代码块进行类加载的时候,一定先加载父类的,然后才是子类

    之前说的方法的重写,一定是先有父类的方法,才能覆盖它。(先加载父类)

    当我们通过leaf()造对象,首先会通过super()找到父类。(没有写也是super)

    画个图看一下逻辑:

    image.png

    所以最先加载的类是Object,只不过改不了代码,也没有输出语句,

    所以看似好像没加载,其实是先加载它,其次是Root类,然后就是Root类里面的static代码块,下面的非静态代码块和无参构造器就别先执行了,因为要先将类的加载都执行了

    如下:

    image.png

    所以,看一下执行结果:(前面三行是“静态初始化块”)

    image.png

    类的加载就完成了。

    下面才涉及造对象。

    静态加载之后,先去new了一个leaf(),然后执行super(),一直到最上层的Root类,先考虑它的构造器的加载(涉及到非静态结构的加载,然后才是子类),代码块的执行又早于构造器,所以会先输出代码块中的内容。

    刚才说到调用的过程如下:

    image.png

    输出的话,就是反过来:

    image.png

    运行结果如下:

    image.png

    技巧:由父及子,静态先行。(先加载父类,后加载子类,静态结构早于非静态(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();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35

    🤸分析

    画个图演示一下:
    image.png

    执行输出顺序:①->②->③->④->⑤->⑥

    先将类的加载搞定。

    HelloA中,有静态先调用静态,输出“static A”,

    然后回到HelloA中,调用静态,输出“static B”。

    然后考虑当前要创建的对象的构造器HelloB(),此时第一行会调用super(),

    调用HelloA()构造器。

    再HelloA()构造器中,有非静态代码块,先执行它,输出“I’m A Class”,

    然后输出构造器中“HelloA”。

    super()执行结束之后,回到HelloB(),此时HelloB类中也有非静态代码块,

    所以先输出代码块中“I’m B Class”,最后输出HelloB()构造器中“HelloB”。

    👻代码运行结果

    image.png

    (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));
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    🤸分析

    画个图:(执行顺序:①->②->③->④->⑤->⑥)

    image.png

    👻输出结果:

    image.png

    (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);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    🤸分析

    画个图:(执行顺序:①->②->③->④->⑤->⑥->⑦->⑧)

    image.png

    🚗调试

    大家也可以自行调试,这里就做个示范。

    问题4.gif

    👻输出结果:

    image.png

  • 相关阅读:
    VMWare workstation虚拟机 转kvm qemu 的Qcow2格式
    JS逆向 Frida - 夜神模拟器安装配置 基本使用
    创建vue3项目、链式调用、setup函数、ref函数、reactive函数、计算和监听属性、vue3的生命周期、torefs的使用、vue3的setup写法
    地图:leaflet基本使用
    配置web&数据库开发环境
    Docker和Pycharm
    Android Studio常见问题
    编写一款2D CAD/CAM软件(十五)封装交互操作类
    反编译SpringBoot项目
    科创人·味多美CIO胡博:数字化是不流血的革命,正确答案藏在业务的田间地头
  • 原文地址:https://blog.csdn.net/m0_55746113/article/details/134556099