java默认参数构造函数_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

构造函数的特点

构造函数的名称必须与类名相同,而且还对大小写敏感。

构造函数没有返回值,也不能用void修饰。如果跟构造函数加上返回值,那这个构造函数就会变成普通方法。

一个类可以有多个构造方法,如果在定义类的时候没有定义构造方法,编译器会自动插入一个无参且方法体为空的默认构造函数。

构造方法可以重载。

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

默认构造函数

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

class Cat{

public Cat(){}

}

class CatAuto{}

然后我们编译一下,得到它们的字节码:

904da91c9f831f9221c621bc4afa902e.png

在《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

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

按顺序调用父类成员变量和实例成员变量的初始化表达式;

调用父类构造函数;

按顺序分别调用类成员变量和实例成员变量的初始化表达式;

调用类构造函数。

嘶…为什么会是这种顺序呢?

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

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

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

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

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

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

da325f60e19c46a2f7a2cf6c8dbf9634.png

以上就是Java对象的创建过程,那么类构造器方法与实例构造器方法有何不同?

类构造器方法不需要程序员显式调用,虚拟机会保证在子类构造器方法执行之前,父类的类构造器方法执行完毕。

在一个类的生命周期中,类构造器方法最多会被虚拟机调用一次,而实例构造器方法则会被虚拟机多次调用,只要程序员还在创建对象。

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

ff571a23b82a853430365b21e082d212.png

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

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

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

举个栗子:

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对象创建过程是这样:

541fc98b4f45e8e56fda099cd30bba5e.png

结语

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

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

参考资料:

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值