Java的构造函数与默认构造函数(深刻版)

前言

咱们知道在建立对象的时候,通常会经过构造函数来进行初始化。在Java的继承(深刻版)有介绍到类加载过程当中的验证阶段,会检查这个类的父类数据,但为何要怎么作?构造函数在类初始化和实例化的过程当中发挥什么做用?html

(若文章有不正之处,或难以理解的地方,请多多谅解,欢迎指正)java

构造函数与默认构造函数

构造函数

构造函数,主要是用来在建立对象时初始化对象,通常会跟new运算符一块儿使用,给对象成员变量赋初值。程序员

class Cat{  
     String sound;  
     public Cat(){  
        sound = "meow";  
     }  
}  
public class Test{  
     public static void main(String[] args){  
        System.out.println(new Cat().sound);  
     }  
}

运行结果为:segmentfault

meow

构造函数的特色

  1. 构造函数的名称必须与类名相同,并且还对大小写敏感。
  2. 构造函数没有返回值,也不能用void修饰。若是跟构造函数加上返回值,那这个构造函数就会变成普通方法。
  3. 一个类能够有多个构造方法,若是在定义类的时候没有定义构造方法,编译器会自动插入一个无参且方法体为空的默认构造函数
  4. 构造方法能够重载

等等,为何无参构造函数和默认构造函数要分开说?它们有什么不一样吗?是的微信

默认构造函数

咱们建立一个显式声明无参构造函数的类,以及一个没有显式声明构造函数的类:函数

class Cat{  
 public Cat(){}  
}  
class CatAuto{}

而后咱们编译一下,获得它们的字节码:
默认构造函数.pngthis

《Java的多态(深刻版)》介绍了invokespecial指令是用于调用实例化<init>方法、私有方法和父类方法。咱们能够看到,即便没有显式声明构造函数,在建立CatAuto对象的时候invokespecial指令依然会调用<init>方法。那么是谁建立的无参构造方法呢?是编译器spa

前文咱们能够得知,在类加载过程当中的验证阶段会调用检查类的父类数据,也就是会先初始化父类。但毕竟验证父类数据跟建立父类数据,从动做的目的上看两者并不相同,因此类会在java文件编译成class文件的过程当中,编译器就将自动向无构造函数的类添加无参构造函数,即默认构造函数.net

为何能够编译器要向没有定义构造函数的类,添加默认构造函数?

构造函数的目的就是为了初始化,既然没有显式地声明初始化的内容,则说明没有能够初始化的内容。为了在JVM的类加载过程当中顺利地加载父类数据,因此就有默认构造函数这个设定。那么两者的不一样之处在哪儿?3d

两者在建立主体上的不一样。无参构造函数是由开发者建立的,而默认构造函数是由编译器生成的。

两者在建立方式上的不一样。开发者在类中显式声明无参构造函数时,编译器不会生成默认构造函数;而默认构造函数只能在类中没有显式声明构造函数的状况下,由编译器生成。

两者在建立目的上也不一样。开发者在类中声明无参构造函数,是为了对类进行初始化操做;而编译器生成默认构造函数,是为了在JVM进行类加载时,可以顺利验证父类的数据信息。

噢...那我想分状况来初始化对象,能够怎么作?实现构造函数的重载便可

构造函数的重载

《Java的多态(深刻版)》中介绍到了实现多态的途径之一,重载。因此重载本质上也是

同一个行为具备不一样的表现形式或形态能力。

举个栗子,咱们在领养猫的时候,通常这只猫是没有名字的,它只有一个名称——猫。当咱们领养了以后,就会给猫起名字了:

class Cat{  
     protected String name;  
     public Cat(){  
        name = "Cat";  
     }  
     public Cat(String name){  
        this.name = name;  
     }  
}

在这里,Cat类有两个构造函数,无参构造函数的功能就是给这只猫附上一个统称——猫,而有参构造函数的功能是定义主人给猫起的名字,但由于主人想法比较多,过几天就换个名称,因此猫的名字不能是常量。

当有多个构造函数存在时,须要注意,在建立子类对象、调用构造函数时,若是在构造函数中没有特地声明,调用哪一个父类的构造函数,则默认调用父类的无参构造函数(一般编译器会自动在子类构造函数的第一行加上super()方法)。

若是父类没有无参构造函数,或想调用父类的有参构造方法,则须要在子类构造函数的第一行用super()方法,声明调用父类的哪一个构造函数。举个栗子:

class Cat{  
     protected String name;  
     public Cat(){  
        name = "Cat";  
     }  
     public Cat(String name){  
        this.name = name;  
     }  
}  
class MyCat extends Cat{  
     public MyCat(String name){  
        super(name);  
     }  
}  
public class Test{  
     public static void main(String[] args){  
         MyCat son = new MyCat("Lucy");  
         System.out.println(son.name);  
     }  
}

运行结果为:

Lucy

总结一下,构造函数的做用是用于建立对象的初始化,因此构造函数的“方法名”与类名相同,且无须返回值,在定义的时候与普通函数稍有不一样;且从建立主体、方式、目的三方面可看出,无参构造函数和默认构造函数不是同一个概念;除了Object类,全部类在加载过程当中都须要调用父类的构造函数,因此在子类的构造函数中,**须要使用super()方法隐式或显式地调用父类的构造函数**。

构造函数的执行顺序

在介绍构造函数的执行顺序以前,咱们来作个题

public class MyCat extends Cat{  
     public MyCat(){  
        System.out.println("MyCat is ready");  
     }  
     public static void main(String[] args){  
        new MyCat();  
     }  
}  
class Cat{  
     public Cat(){  
        System.out.println("Cat is ready");  
     }  
}

运行结果为:

Cat is ready  
MyCat is ready

这个简单嘛,只要知道类加载过程当中会对类的父类数据进行验证,并调用父类构造函数就能够知道答案了。

那么下面这个题呢?

public class MyCat{  
     MyCatPro myCatPro = new MyCatPro();  
     public MyCat(){  
        System.out.println("MyCat is ready");  
     }  
     public static void main(String[] args){  
        new MyCat();  
     }  
}  
class MyCatPro{  
     public MyCatPro(){  
        System.out.println("MyCatPro is ready");  
     }  
}

运行结果为:

MyCatPro is ready  
MyCat is ready

嘶......这里就是在建立对象的时候会先实例化成员变量的初始化表达式,而后再调用本身的构造函数
ok,结合上面的已知项来作作下面这道题

public class MyCat extends Cat{  
     MyCatPro myCatPro = new MyCatPro();  
     public MyCat(){  
        System.out.println("MyCat is ready");  
     }  
     public static void main(String[] args){  
        new MyCat();  
     }  
}  
class MyCatPro{  
     public MyCatPro(){  
        System.out.println("MyCatPro is ready");  
     }  
}  
class Cat{  
     CatPro cp = new CatPro();  
     public Cat(){  
        System.out.println("Cat is ready");  
     }  
}  
class CatPro{  
     public CatPro(){  
        System.out.println("CatPro is ready");  
     }  
}

3,2,1,运行结果以下:

CatPro is ready  
Cat is ready  
MyCatPro is ready  
MyCat is ready

经过这个例子咱们能看出,类在初始化时构造函数的调用顺序是这样的:

  1. 按顺序调用父类成员变量和实例成员变量的初始化表达式;
  2. 调用父类构造函数
  3. 按顺序分别调用成员变量和实例成员变量的初始化表达式;
  4. 调用类构造函数

嘶......为何会是这种顺序呢

Java对象初始化中的构造函数

咱们知道,一个对象在被使用以前必须被正确地初始化。本文采用最多见的建立对象方式:使用new关键字建立对象,来为你们介绍Java对象初始化的顺序。new关键字建立对象这种方法,在Java规范中被称为由执行类实例建立表达式而引发的对象建立

Java对象的建立过程(详见《深刻理解Java虚拟机》)

当虚拟机遇到一条new指令时,首先会去检查这个指令的参数是否能在常量池(JVM运行时数据区域之一)中定位到这个类的符号引用,而且检查这个符号引用是否已被加载、解释和初始化过。若是没有,则必须执行相应的类加载过程(这个过程在Java的继承(深刻版)有所介绍)。

类加载过程当中,准备阶段中为类变量分配内存并设置类变量初始值,而类初始化阶段则是执行类构造器<clinit>方法的过程。而<clinit>方法是由编译器自动收集类中的类变量赋值表达式和静态代码块(static{})中的语句合并产生的,其收集顺序是由语句在源文件中出现的顺序所决定。

其实在类加载检查经过后,对象所须要的内存大小已经能够彻底肯定过了。因此接下来JVM将为新生对象分配内存,以后虚拟机将分配到的内存空间都初始化为零值。接下来虚拟机要对对象进行必要的设置,并这些信息放在对象头。最后,再执行<init>方法,把对象按程序员的意愿进行初始化。
对象建立过程2.jpg
以上就是Java对象的建立过程,那么类构造器<clinit>方法与实例构造器<init>方法有何不一样?

  1. 类构造器<clinit>方法不须要程序员显式调用,虚拟机会保证在子类构造器<clinit>方法执行以前,父类的类构造器<clinit>方法执行完毕。
  2. 在一个类的生命周期中,类构造器<clinit>方法最多会被虚拟机调用一次,而实例构造器<init>方法则会被虚拟机屡次调用,只要程序员还在建立对象。

等等,构造函数呢?跑题了?莫急,在了解Java对象建立的过程以后,让咱们把镜头聚焦到这里“对象初始化”:
对象初始化3.jpg

在对象初始化的过程当中,涉及到的三个结构,实例变量初始化实例代码块初始化构造函数

咱们在定义(声明)实例变量时,还能够直接对实例变量进行赋值或使用实例代码块对其进行赋值,实例变量和实例代码块的运行顺序取决于它们在源码的顺序

编译器中,实例变量直接赋值和实例代码块赋值会被放到类的构造函数中,而且这些代码会被放在父类构造函数的调用语句以后,在实例构造函数代码以前

举个栗子:

class TestPro{  
     public TestPro(){  
        System.out.println("TestPro");  
     }  
}  
public class Test extends TestPro{  
     private int a = 1;  
     private int b = a+1;  
    ​  
     public Test(int var){  
         System.out.println(a);  
         System.out.println(b);  
         this.a = var;  
         System.out.println(a);  
         System.out.println(b);  
     }  
     {  
        b+=2;  
     }  
     public static void main(String[] args){  
        new Test(10);  
     }  
}

运行结果为:

TestPro  
1  
4  
10  
4

总结一下,Java对象建立时有两种类型的构造函数:类构造函数<clinit>方法、实例构造函数<init>方法,而整个Java对象建立过程是这样:
Java建立对象流程.jpg

结语

如今是快阅读流行的时代,短小精悍的文章更受欢迎。但我的认为回顾知识点最重要的是温故知新,因此采用深刻版的写法,不过每次写完我都以为我都不像是一个小甜甜了,却是有点像下图的那颗葱头......

若是以为文章不错,请点一个赞吧,这会是我最大的动力~
微信图片_20200307103322.jpg

参考资料:

Java里的构造函数(构造方法)

java无参构造函数(默认构造函数)

Java 构造函数的详解

一个之前没有注意的问题:java构造函数的执行顺序

深刻理解Java对象的建立过程:类的初始化与实例化

相关文章
相关标签/搜索