构造函数、析构函数、finalize( )函数、Java程序在执行构造函数初始化到底是在干嘛、构造器、实例构造器<init>()和类构造器<clinit>()、实例构造器 与构造方法的关系、


以前在学JavaSE的时候就碰见了,构造函数,一直不解构造函数是干什么的,只知道学、用、没深刻理解;今天回过头把它吃透。 💪 💪 💪 💪 💪

构造函数

构造函数:Java规定了我们在创建类时,必须有一个构造函数,这个构造函数可以重写,如果我们在创建类的时候没有写构造函数,Java会默认为我们提供一个无参的构造函数。

public class Dog{
	String name;
	public Dog(){   //无参构造方法
	}
	public Dog(String name){ //有参构造方法
		this.name = name;
	}
}
我们在编写代码时,如果没有写构造函数,那么编译器会自动生成一个无参构造方法。

那为什么一定要有构造函数呢,是因为我们在创建对象的时候,要初始化成员变量,即为所有属性和方法都设置成默认值(数字为 0,字符为 null,布尔为 false,而所有引用被设置成 null)。构造函数的任务是初始化一个对象的内部状态,所以用new操作符创建一个实例后,立刻就会得到一个清楚、可用的对象。
还有小白会问的,Java 为什么必须要初始化赋一个null值?
在c语言里面,如果我们不给变量赋值的话,它就会产生一个随机指针,而Java不会。jvm会自动为成员变量分配内存空间,设置默认值(String类型为null,boolea类型为false),然后将这个成员变量引用指向这个内存。

构造函数的特点:

(1)构造方法名和类名相同。
(2)构造方法没有返回类型,也不能在方法名前定义void。
(3)构造方法的主要作用是完成对象的初始化工作,它能够把定义对象时的参数传给对象的域。
(4)构造方法可以重载。
(5)构造方法只能由编译器调用,程序员不可以。

构造函数的执行顺序

tip:方法优先存在于任何变量或者对象,存在于类中,而不是对象中。即构造对象前,方法就存在。

在java中,实例化一个子类的对象,首先会调用父类的无参构造函数。如果父类没有显式定义构造函数,那么会调用缺省构造函数,这个缺省构造函数是由编译器自动产生的。如果父类显式定义了构造函数,那么编译器就不再为父类生成缺省默认构造函数。
假设父类中定义了一个带参数的构造函数,而没有定义无参构造函数,这时候实例化一个子类的对象,就会出现编译错误,因为子类首先要调用父类的无参构造函数,但是父类没有显式定义,编译器又不为父类产生缺省构造函数。这时候可以通过在父类中显示定义无参构造函数来解决这个错误。
this()和super()都可以用来调用构造函数,而this()用于在同一个类内调用其他的构造函数,比如首先在Student类中定义了一个构造函数Student(name,age),又另外定义了一个构造函数Student(name,age,school),那么在第二个构造函数中可以通过this(name,age)的形式来调用第一个构造函数,注意这里this(name,age)必须写在第二个构造函数的首行。而super用于从子类的构造方法中调用父类的构造方法。比如父类Person有构造函数Person(String name, int age),而子类有构造函数Student(String name, int age, String school, String grade),那么就可以在子类构造函数中通过super(name,age)来调用父类构造函数。
建议:最好为每个类都显示定义无参构造函数。

  1. 调用父类构造函数;
  2. 按代码顺序分别调用实例成员变量的初始化表达式(如果代码块在前,优先执行代码块;如果变量在前,优先变量赋值);
  3. 调用本身构造函数。
public class Dollar extends Money{
	Rmb r=new Rmb();
	public Dollar(){
		System.out.println("Dollar is construct!");
	}
	
	public static void main(String[] args){
		new Dollar();
	}
}

class Money{
	public Money(){
		System.out.println("Money is construct");
	}
}

class Rmb{
	public Rmb(){
		System.out.println("RMB is construct");
	}
}

输出结果:
Money is construct
RMB is construct
Dollar is construct!

从返回结果来看,我们在执行main()函数时,先调用父类的构造函数Money();
接着执行实例成员变量Rmb,因为Rmb实例未存在,我们要执行Rmb r=new Rmb();这时就要执行Rmb的构造方法Rmb();
之后,没有其他成员变量了,我们就执行它自己的最构造方法Dollar();
到此,Dollar对象就真正存在jvm运行态内存中了。

虽说Java与C++的构造器是一样的,但是Java与C++有一个不同之处:Java不但有构造函数,还有一个”初始化块“(Initialization Block)。请大家思考一下,Java的初始化块、静态初始化块、构造函数的执行顺序是什么样的?
如果不知道的话,看看这里👉👉:Java 初始化的深认知(第二篇)

析构函数

在了解了构造函数之后,与它相对的析构函数我们也要了解一下

析构方法
首先,我要声明,Java是没有析构函数的,C++才有。
与 Java 不同,C++ 支持局部对象(基于栈)和全局对象(基于堆)。因为这一双重支持,C++ 也提供了自动构造和析构,这导致了对构造函数和析构函数的调用,(对于堆对象)就是内存的分配和释放。
在 Java 中,所有对象都驻留在堆内存,因此局部对象就不存在。结果,Java 的设计者觉得不需要析构函数(像 C++ 中所实现的)。
取而代之,Java 定义了一个特殊的方法叫做finalize() ,它提供了 C++ 析构函数的一些功能。但是,finalize() 并不完全与 C++ 的析构函数一样,并可以假设它会导致一系列的问题。finalize() 方法作用的一个关键元素是 Java 的垃圾回收器。

析构函数特点

  • 析构函数没有参数,也没有返回值。不能重载,也就是说,一个类中只可能定义一个析构函数
  • 如果一个类中没有定义析构函数,系统也会自动生成一个默认的析构函数,为空函数,什么都不做
  • 调用条件:
    1.在函数体内定义的对象,当函数执行结束时,该对象所在类的析构函数会被自动调用;
    2.用new运算符动态构建的对象,在使用delete运算符释放它时。

析构函数的调用顺序

对象是由“底层向上”开始构造的,当建立一个对象时,首先调用基类的构造函数,然后调用下一个派生类的构造函数,依次类推,直至到达派生类次数最多的派生次数最多的类的构造函数为止。因为,构造函数一开始构造时,总是要调用它的基类的构造函数,然后才开始执行其构造函数体,调用直接基类构造函数时,如果无专门说明,就调用直接基类的默认构造函数。在对象析构时,其顺序正好相反。

每创建一个类的实例都去初始化它的所有变量是乏味的。如果第一个对象在被创建时就完成了所有的初始工作,第二个对象在创建时只需,将是简单的和简洁的。

new 关键字被jvm识别后,就会在堆上创建对应类的对象,生成一个返回值(对象的内存地址),并且把类变量(静态成员变量)进行第一次初始化,有值的赋值,没有值的就赋0或者null,然后才是调用构造器方法,再进行对象变量的第二次初始化,之后,new动作产生的地址值返回给栈内存中 类的变量,存储起来。 ps new动作产生对象之后,会加载对象变量,以及非静态方法的地址值,这个地址值指向方法区中对应类的实际的非静态方法,也就是说 静态方法 和 非静态方法都存在于方法区的类当中。

finalize( )函数

Java 允许使用 finalize() 方法在gc回收前做必要的清理工作。

某些情况下,当 gc 回收一个对象时,需要完成一些操作。例如,如果一个对象正在处理的是非Java 资源,如文件句柄或window 字符字体,这时你要确认在这个对象被回收之前,这些资源被释放。为处理这样的状况,Java 提供了收尾机制(finalization )。使用该机制可以定义一些特殊的操作,这些操作在一个对象被垃圾回收程序释放前执行。要给一个类增加收尾(finalizer ),你要定义finalize ( ) 方法。Java 回收该类的一个对象时,会先调用这个方法。
finalize()是在Object 类中定义的,因此所有的类都继承了它,子类要覆盖 finalize() 方法才能执行清理工作。

protected void finalize() throws java.lang.Throwable {
		super.finalize();
		System.out.println("非Java资源已释放");
		}

当垃圾回收器(garbage colector)决定回收某对象时,就会运行该对象的finalize()方法。但是在Java中,如果内存总是充足的,那么垃圾回收可能永远不会进行,也就是说filalize()可能永远不被执行,显然指望它做收尾工作是靠不住的。

那么finalize()究竟是做什么的呢?它最主要的用途是回收特殊渠道申请的内存。Java程序有垃圾回收器,所以一般情况下内存问题不用程序员操心。但有一种JNI(Java Native Interface)调用non-Java程序(C或C++),finalize()的工作就是回收这部分的内存。

finalize()特点:

  1. 垃圾回收器准备释放内存的时候,会先调用finalize()。
  2. Java中所有类都从Object类中继承finalize()方法。
  3. 垃圾回收和finalize()都是靠不住的,只要JVM还没有快到耗尽内存的地步,它是不会浪费时间进行垃圾回收的。

理解finalize( ) 正好在垃圾回收以前被调用非常重要。例如当一个对象超出了它的作用域时,finalize( ) 并不被调用。这意味着你不可能知道何时、甚至是否、finalize( ) 被调用。因此,你的程序应该提供其他的方法来释放由对象使用的系统资源,而不能依靠finalize( ) 来完成程序的正常操作。

Java程序在执行构造函数初始化到底是在干嘛

在了解了构造函数功能之后,我们往深入一点在思考,jvm是如何执行构造函数的?

众所周知,每一个Java中的对象都至少会有一个构造函数,如果我们没有显式定义构造函数,那么它将会有一个默认无参的构造函数。在编译生成的字节码中,这些构造函数会被命名成<init>()方法,参数列表与Java语言书写的构造函数的参数列表相同。

类的生命周期为:加载、连接(验证、准备、解析)、初始化、使用和卸载。
Java程序执行构造函数就是在初始化这个阶段,在这个阶段,jvm会调用 实例构造器<init>() 收集实例代码块实例变量的赋值语句,还有构造函数里面的代码,将其一起执行并初始化赋初值。

构造器

概念:构造器是javac编译生成的一个函数,是在字节码层面存在的“函数”。它其实对一些代码的整合后生成的函数。和都是构造器,是编译器生成的。
(1)实例构造器<init>,是生成对象时执行的。我们在写代码new一个对象,其实执行的就是构造器。整合了父类的实例构造器、实例代码块{}、变量初始化和构造函数的代码。
(2)类构造器<clinit>,是虚拟机类加载时调用。
<clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态代码块static{}中的语句合并产生的,其中编译器收集的顺序是由语句在源文件中出现的顺序所决定。
<init>()是指收集类中的所有实例变量的赋值动作、实例代码块和构造函数合并产生的。

实例<init>构造器与构造函数的关系:

构造器是由javac 命令生成.而构造函数则实质为我们写的java代码构造方法(也叫构造函数), <init>构造器的生成和构造函数相互对应,如果我们的java代码没有显示的构造函数,那编译器将会添加一个没有参数的、访问性(public、protected或private)与当前类一致的默认构造函数。并根据默认构造函数生成<init>。

类构造器()与实例构造器()

类构造器()与实例构造器()不同,它不需要程序员进行显式调用,虚拟机会保证在子类类构造器()执行之前,父类的类构造()执行完毕。由于父类的构造器()先执行,也就意味着父类中定义的静态语句块/静态变量的初始化要优先于子类的静态语句块/静态变量的初始化执行。特别地,类构造器()对于类或者接口来说并不是必需的,如果一个类中没有静态语句块,也没有对类变量的赋值操作,那么编译器可以不为这个类生产类构造器()。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值