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

前言

我们知道在创建对象的时候,一般会通过构造函数来进行初始化。在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);
    }
}

运行结果为:

meow

构造函数的特点

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

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

默认构造函数

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

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

然后我们编译一下,得到它们的字节码:
在这里插入图片描述
《Java的多态(深入版)》介绍了invokespecial指令是用于调用实例化方法、私有方法和父类方法。我们可以看到,即使没有显式声明构造函数,在创建CatAuto对象的时候invokespecial指令依然会调用方法。那么是谁创建的无参构造方法呢?是编译器

前文我们可以得知,在类加载过程中的验证阶段会调用检查类的父类数据,也就是会先初始化父类。但毕竟验证父类数据跟创建父类数据,从动作的目的上看二者并不相同,所以类会在java文件编译成class文件的过程中,编译器就将自动向无构造函数的类添加无参构造函数,即默认构造函数

为什么可以编译器要向没有定义构造函数的类,添加默认构造函数?

构造函数的目的就是为了初始化,既然没有显式地声明初始化的内容,则说明没有可以初始化的内容。为了在JVM的类加载过程中顺利地加载父类数据,所以就有默认构造函数这个设定。那么二者的不同之处在哪儿?

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

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

二者在创建目的上也不同。开发者在类中声明无参构造函数,是为了对类进行初始化操作;而编译器生成默认构造函数,是为了在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的继承(深入版)有所介绍)。

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

其实在类加载检查通过后,对象所需要的内存大小已经可以完全确定过了。所以接下来JVM将为新生对象分配内存,之后虚拟机将分配到的内存空间都初始化为零值。接下来虚拟机要对对象进行必要的设置,并这些信息放在对象头。最后,再执行方法,把对象按程序员的意愿进行初始化。

在这里插入图片描述
以上就是Java对象的创建过程,那么类构造器方法与实例构造器方法有何不同?

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

等等,构造函数呢?跑题了?莫急,在了解Java对象创建的过程之后,让我们把镜头聚焦到这里“对象初始化”:
在这里插入图片描述
在对象初始化的过程中,涉及到的三个结构,实例变量初始化实例代码块初始化构造函数

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

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

举个栗子:

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对象创建时有两种类型的构造函数:类构造函数方法、实例构造函数方法,而整个Java对象创建过程是这样:
在这里插入图片描述

结语

现在是快阅读流行的时代,短小精悍的文章更受欢迎。但个人认为回顾知识点最重要的是温故知新,所以采用深入版的写法,不过每次写完我都觉得我都不像是一个小甜甜…

如果觉得文章不错,请点一个赞吧,这会是我最大的动力~

参考资料:

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

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

Java 构造函数的详解

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

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

posted @ 2020-03-07 22:39  NYfor2018  阅读(1219)  评论(0编辑  收藏  举报