深入理解对象的创建

我们已经知道了类与对象的关系,我们来试验下看看具体效果如何:

案例一:

创建对象之前,我们要先创建类,代码如下:

public class Student {
    String name;
    int age;
    char gender;

    public void intro(){
        System.out.println("大家好,我是"+name+",性别"+gender+"今年"+age+"岁了,");
    }
}

我们再来创建和使用对象,代码如下:

        //创建对象
        Student stu1 = new Student();
        Student stu2 = new Student();
        Student stu3 = new Student();

        //对象属性赋值
        stu1.name = "熊大";
        stu1.gender = '男';
        stu1.age = 22;

        stu2.name = "熊二";
        stu2.gender = '女';
        stu2.age = 23;

        //对象调用方法
        stu1.intro();
        stu2.intro();
        stu3.intro();

运行效果如下:

大家好,我是熊大,性别男今年22岁了,
大家好,我是熊二,性别女今年23岁了,
大家好,我是null,性别 今年0岁了,

可以看出,

1.不同对象的属性不同,调用各自方法的结果也不同。
2.如果没有赋值对象属性,会默认赋值与属性数据类型相同的数据。引用类型数据默认是null,字符类型数据默认为' '(一个空格),整数型数据默认为0。


案例二:

我们沿用案例一的Student类,改变对象的使用方式,代码如下:

        //创建对象stu1
        Student stu1 = new Student();

        //stu1对象属性赋值
        stu1.name = "熊大";
        stu1.gender = '男';
        stu1.age = 22;

        //创建Student类型变量stu2,将stu1的引用给stu2
        Student stu2 = stu1;

        //stu2对象属性赋值
        stu2.name = "熊二";
        stu2.gender = '女';
        stu2.age = 23;

        //对象调用方法
        stu1.intro();
        stu2.intro();

不要着急看实际结果,可以先猜想一下运行的结果

如果你认为得到的结果和案例一中stu1对象和stu2对象调用方法的结果一致,那么很遗憾,你答错了。
实际运行结果如下:

大家好,我是熊二,性别女今年23岁了,
大家好,我是熊二,性别女今年23岁了,

答错的不要灰心,答对的不要骄傲,且听我慢慢道来。

我们需要从java虚拟机创建对象的内存分配开始说起...

内存分区的介绍

JVM的内存结构:

  • 栈(Stack)

    • 栈与线程的关系:

    Java中一个线程一个栈区,每一个栈中的元素都是私有的,不被其他栈所访问。

    • 栈的储存特点:

    后进先出。

    • 栈储存的缺点:

    大小和生存期都是确定的,缺乏灵活性。

    • 栈储存的优点:

    储存速度快,仅次于寄存器。

    • 栈存储速度快的原因:

    通过栈指针的移动创建和释放空间。

    但也因为栈指针移动储存的特点,限制了移动的大小和范围,进而影响了程序的灵活性。所以JVM将更大容量的数据储存在堆中。

    • 栈中存放的数据种类:

    基本数据类型的数据、引用数据类型的引用。
    例如:

        int a = 10;
        Student stu1 = new Student();
    

    值10、对象的引用(stu1)储存在栈内存中

  • 堆(heap)

    • 堆储存较栈储存的优缺点:

    开辟空间和生存期不受约束,但储存速度较慢。

    • 堆中存放的数据种类:

    类的对象,通过new创建出的对象。

    • 堆内存的释放原理:

    当堆内存中不存在某对象的引用时,通过垃圾回收机制(GC)完成内存空间的释放。

  • 方法区(method area)

    • 方法区存放的数据类型:

    1.类信息
    2.静态的变量
    3.常量
    4.成员方法

    方法区中包含一个特殊区域——常量池(储存的是static静态修饰符修饰的成员变量)。

  • 寄存器(program counter register)

    • 寄存器存放的数据种类:

    当前正在执行的JVM指令地址。
    java程序中,每个线程启动时,都会创建一个寄存器。

  • 本地方法栈(native method stack)

    • 本地方法栈存放的数据种类:

    本地方法的地址。

了解完这些内存分区的知识之后,我们回到案例二,从内存的角度分析代码的执行。

案例二的内存分析

首先,Student的类会被加载到方法区,结果如图所示:

其次,新建的对象引用会被加载到栈中、对象则在堆中创建,堆中对象的内存地址赋值给了栈中的变量、栈中的对象引用指向堆中的对象(0x1234是假设的内存地址)。结果如图所示:

注意上图中对象的属性均为默认值,接下来是对stu1对象属性的赋值,结果如图所示:

接着定义Student类型的变量stu2,并将stu1的引用赋值给stu2。

之前做错的小伙伴注意了哈,是把内存地址赋值给stu2。

内存地址相同的两个对象会发生什么呢?结果如图所示:

所以,当对stu2对象属性赋值时,其实在对堆中储存的stu1的对象属性赋值,这会覆盖掉原有的属性。结果如图所示:

这也就解释了为什么调用两个不同的对象,却得到相同结果的原因,因为变量引用的堆中数据是相同的。


案例三

觉得学的差不多了?我们继续沿用案例一的类,我们再来看另一段使用对象的代码:

        //创建对象stu1
        Student stu1 = new Student();

        //stu1对象属性赋值
        stu1.name = "熊大";
        stu1.gender = '男';
        stu1.age = 22;

        //创建对象stu2
        Student stu2 = new Student();

        //将stu1的引用给stu2
        stu2 = stu1;

        //stu2对象属性赋值
        stu2.name = "熊二";
        stu2.gender = '女';
        stu2.age = 23;

        //对象调用方法
        stu1.intro();
        stu2.intro();

什么,这和案例二一样?再仔细看看!相信眼尖的小伙伴已经看出来端倪了,区别就在于多了这行代码:

         //创建对象stu2
        Student stu2 = new Student();

小伙伴们不妨再猜一猜运行的结果,这里先卖个关子,先讲解,最后再揭晓答案!

与案例二相同的我不再赘述,这里我从直接从两者不同的地方开始。

栈中开辟空间创建变量stu2,堆中开辟空间创建对象stu2,并把内存地址赋值给栈中的变量stu2,栈中的引用指向堆中的数据。结果如图所示:

随后将stu1的内存地址赋值给stu2,栈中的stu2变量引用指向堆中的stu1对象,堆中的stu2对象失去引用,被当作垃圾回收,结果如图所示:

对stu2的属性赋值就相当于对堆中的stu1对象属性赋值,结果如图所示:

因此,案例三的结果水落石出,看看小伙伴们答对了没有:

大家好,我是熊二,性别女今年23岁了,
大家好,我是熊二,性别女今年23岁了,

实际结果和案例二一模一样呢,但是其中的过程可是大不相同呢!

欢迎小伙伴们的留言~

posted on 2021-10-01 02:15  技术小伙伴  阅读(114)  评论(0)    收藏  举报