美文网首页
Java中对象的生与灭- 核心篇

Java中对象的生与灭- 核心篇

作者: 汤圆学Java | 来源:发表于2021-04-12 21:14 被阅读0次
    1096379

    前言

    大家好啊,我是汤圆,今天给大家带来的是《Java中对象的生与灭- 核心篇》,希望对大家有帮助,谢谢

    文章纯属原创,个人总结难免有差错,如果有,麻烦在评论区回复或后台私信,谢啦

    比个耶

    简介

    前面我们了解了Java的三大特性,其中介绍了类的继承、重载等,这里我们就基于这些知识点,把对象的创建和回收进行一个简单的介绍

    这篇不是很长,只是介绍核心的几个知识点,相信大家很快就可以看完,真的

    目录

    • 堆和栈
    • 构造函数(生)
    • 对象的回收(灭)

    正文

    堆(heap)和栈(stack)

    堆是一块内存,用来存放对象

    栈是另一块内存,用来执行方法并存储局部变量,遵循后进先出的原则;

    PS:栈并不存储方法,只是执行方法,执行完方法后,会将方法弹出栈(方法存在方法区)

    下面我们用实际代码,来看下堆和栈的区别

    代码如下:

    
    public class LiveAndDeathDemo {
        // 基本类型属性
        private int a;
    
        public static void main(String[] args) {
            LiveAndDeathDemo live = new LiveAndDeathDemo(1);
            live.fun();
        }
        public void fun(){
            int temp = 10;
            System.out.println(temp);
        }
    
        public LiveAndDeathDemo(int a) {
            this.a = a;
        }
        public int getA() {
            return a;
        }
    
        public void setA(int a) {
            this.a = a;
        }
    }
    
    

    可以看到,有一个实例变量a(堆), 两个方法main和fun,其中fun有一个局部变量temp(栈)

    它们的区别如下所示:

    <img src="https://tva1.sinaimg.cn/large/008eGmZEly1gpfqb6nb8mj30lq0d2wf4.jpg" alt="栈 堆" />

    这里简单介绍一下上面的流程

    1. main方法压入栈中,创建局部变量live(对象的引用)
    2. 创建对象live,在堆中开辟内存,将live放入堆中
    3. live调用fun方法,将fun压入栈中(此时fun在栈顶)
    4. fun执行完成,出栈,继续执行main方法
    5. 最后main方法执行完成,也出栈,程序结束

    这里可能有朋友要问了,那如果属性是一个引用呢?它要存放在哪里?

    引用存放在堆里,引用指向的对象也存放在堆里,只不过是堆的另一个地方

    如下图所示:堆中 live对象的属性 liveRef 指向了另一个对象(live对象2)

    堆 引用

    为啥要先介绍堆和栈呢?

    因为堆和栈跟对象的生活息息相关

    如果用人来比作对象的话,那堆就是人的家,栈就是外面的世界

    我们出生在家里,跟外面的世界打交道,最后在家里。。。

    对象的创建(生)

    生存还是毁灭,这是一个问题。

                              -- 莎士比亚《哈姆莱特》
    

    在Java的花花世界中,这也是个问题,不过是个有答案的问题;

    答案就在下面。。。

    往下看2

    这里我们先把问题简化

    因为我们最常见的创建对象是通过new创建,而new对象的核心就是通过构造函数来实现,所以我们这里简单起见,着重介绍构造函数,其他的后面等到虚拟机部分再介绍

    构造函数的分类:

    • 无参构造函数

    • 有参构造函数

    构造函数和普通方法的区别

    1. 构造函数没有返回类型

    2. 构造函数名与类名一致

    关于编译器的默认操作

    1. 如果没有定义构造函数,编译器会默认创建一个无参构造函数
    2. 如果子类定义了有参构造函数,且没有显示调用父类的构造函数,则编译器默认调用父类的无参构造函数

    当你自己有创建构造函数(无参或有参)时,编译器都不会再去创建构造函数

    构造函数的重载:

    很常用,一般用来设置属性的默认值

    具体做法就是多个构造函数层层调用(又来套娃了)

    下面举个例子:

    
    public class LiveAndDeathDemo {
        private int a;
        private String name;
            
        public LiveAndDeathDemo(){
            this(1);
        }
        public LiveAndDeathDemo(int a) {
            this(a, "JavaLover");
        }
        public LiveAndDeathDemo(int a, String name) {
            this.a = a;
            this.name = name;
        }
      // 省略getter,setter
    }
    

    用图表示的话,就是下面这个样子

    <img src="https://tva1.sinaimg.cn/large/008eGmZEly1gpfrldwz7wj30mm0fkt99.jpg" alt="构造函数 调用层级" />

    构造函数的私有化

    如果构造函数私有化,那么它要怎么用呢?

    私有化说明只有类本身可以调用,这种主要用在工厂方法中

    比如Java中的LocalDate,源码如下:

    public final class LocalDate
            implements Temporal, TemporalAdjuster, ChronoLocalDate, Serializable {
        // 构造函数私有化 
      private LocalDate(int year, int month, int dayOfMonth) {
            this.year = year;
            this.month = (short) month;
            this.day = (short) dayOfMonth;
        }
      // 对外提供一个静态方法,用来创建对象实例
       public static LocalDate of(int year, int month, int dayOfMonth) {
            YEAR.checkValidValue(year);
            MONTH_OF_YEAR.checkValidValue(month);
            DAY_OF_MONTH.checkValidValue(dayOfMonth);
            return create(year, month, dayOfMonth);
        }
    }
    

    这种用法在LocalDate这种工具类中用的比较多,还有就是单例模式(后面设计模式时再介绍)

    上面介绍的构造函数没有介绍到父类,下面开始介绍

    如果有父类,构造函数的有哪些不一样的地方

    this和super:

    在介绍父类的构造函数之前,有必要介绍下这个super

    • this指向当前类

    • super指向父类

    super用来显式调用父类相关属性和方法(包括构造函数)

    比如super.filedA,super.fun()

    这里有个特例,如果是在子类的构造函数中或者覆写方法中,则直接调用super()即可调用父类对应的构造函数或方法(下面代码有演示)

    构造函数的执行顺序

    1. 如果子类Dog继承父类Animal,那么会先调用父类的构造函数,再调用子类的构造函数

    2. 如果父类Animal上面还有父类,会继续往上调用

    3. 上面这个过程就叫做“构造函数链

    这个关系有点像是:子女和父母的关系,子女要想出生,必须先让爷爷奶奶把父母生出来,然后父母才能生子女

    所以这里如果我们要构造子类,必须先构造父类;如果父类还有父类,则继续延伸,一直到Object超类为止

    下面用代码演示下这个层层调用的过程:

    
    public class SuperDemo extends Father{
        public SuperDemo() {
            // 1.1 这里显示调用父类的构造函数
            // 1.2 Super必须放在构造函数的第一行
            super();
            System.out.println("sub construct");
        }
    
        public static void main(String[] args) {
            SuperDemo demo = new SuperDemo();
        }
    }
    class Father{
        public Father() {
            // 2. 这里没有显示调用父类(Object)的构造函数,编译器会自己去调用
            System.out.println("father construct");
        }
    }
    /** 假设下面这个Object就是我们的超类
    class Object{
        public Object(){
                // 3. 最终的构造函数,会调到这里为止
        } 
    }
    **/
    
    

    输出如下:

    father construct
    sub construct
    

    可以看到,先调用父类Father的构造函数,再调用子类的构造函数

    他们之间的继承关系如下:

    <img src="https://tva1.sinaimg.cn/large/008eGmZEly1gpfz2zqrknj30pa0hmabg.jpg" alt="SuperDemo的继承关系" style="zoom:;" />

    图示说明:

    左边的虚线表示层层往上调用,直到超类Object

    右边的实现表示上面的构造完成会回到下面那一层,继续构造,直到当前类

    好了,构造的过程大致就是这个样子了,还有很多其他方面的细节(比如类的初始化等)这里先不介绍了,太多了,放到后面介绍

    对象的回收(灭)

    对象的回收是在程序内存不够用时,将没用的对象(可回收)进行释放的一种操作,这个操作是由垃圾收集器GC来完成的

    什么是没用的对象?

    没用的对象就是可回收的对象

    说人话:当指向对象A的最后一个引用ref消失时,这个对象A就会变成没用的对象,等待着垃圾收集器的回收

    那怎么才算引用消失呢?

    基本分为两种情况:

    • 如果引用是局部变量,那当引用所在的方法执行完毕时,引用就会被释放,那么该对象随即也就会被标记为没用的对象,等待回收

    • 当引用指向其他对象或者null时,该对象会被标记为没用的对象,等待回收

    上面都是假设引用是指向对象的最后一个引用的情况,如果有多个引用指向同一个对象,那么要等到引用都消失,对象才会被标记为可回收,即没用的东西

    总结

    1. 堆和栈

    堆存放对象,栈用来执行方法并存放局部变量

    1. 对象的创建

    主要通过构造函数来创建,比如new对象

    如果是反序列化来创建对象,则不会构造,因为构造后,对象的属性会被重新初始化,那么序列化的属性值就被抹掉了(前面的Java中的IO流有涉及)

    如果子类有父类,则先调用父类的构造函数,如果父类还有父类,则依次类推,直到Object超类

    1. 对象的回收

    当指向对象的最后一个引用消失时,这个对象就会变成没用的对象,等待着垃圾收集器的回收

    后记

    最后,感谢大家的观看,谢谢

    相关文章

      网友评论

          本文标题:Java中对象的生与灭- 核心篇

          本文链接:https://www.haomeiwen.com/subject/dbqnkltx.html