咱们知道在建立对象的时候,通常会经过构造函数来进行初始化。在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
等等,为何无参构造函数和默认构造函数要分开说?它们有什么不一样吗?是的。微信
咱们建立一个显式声明无参构造函数的类,以及一个没有显式声明构造函数的类:函数
class Cat{ public Cat(){} } class CatAuto{}
而后咱们编译一下,获得它们的字节码:this
在《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
经过这个例子咱们能看出,类在初始化时构造函数的调用顺序是这样的:
嘶......为何会是这种顺序呢?
咱们知道,一个对象在被使用以前必须被正确地初始化。本文采用最多见的建立对象方式:使用new关键字建立对象,来为你们介绍Java对象初始化的顺序。new关键字建立对象这种方法,在Java规范中被称为由执行类实例建立表达式而引发的对象建立。
当虚拟机遇到一条new指令时,首先会去检查这个指令的参数是否能在常量池(JVM运行时数据区域之一)中定位到这个类的符号引用,而且检查这个符号引用是否已被加载、解释和初始化过。若是没有,则必须执行相应的类加载过程(这个过程在Java的继承(深刻版)有所介绍)。
类加载过程当中,准备阶段中为类变量分配内存并设置类变量初始值,而类初始化阶段则是执行类构造器<clinit>方法的过程。而<clinit>方法是由编译器自动收集类中的类变量赋值表达式和静态代码块(static{})中的语句合并产生的,其收集顺序是由语句在源文件中出现的顺序所决定。
其实在类加载检查经过后,对象所须要的内存大小已经能够彻底肯定过了。因此接下来JVM将为新生对象分配内存,以后虚拟机将分配到的内存空间都初始化为零值。接下来虚拟机要对对象进行必要的设置,并这些信息放在对象头。最后,再执行<init>方法,把对象按程序员的意愿进行初始化。
以上就是Java对象的建立过程,那么类构造器<clinit>方法与实例构造器<init>方法有何不一样?
等等,构造函数呢?跑题了?莫急,在了解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对象建立时有两种类型的构造函数:类构造函数<clinit>方法、实例构造函数<init>方法,而整个Java对象建立过程是这样:
如今是快阅读流行的时代,短小精悍的文章更受欢迎。但我的认为回顾知识点最重要的是温故知新,因此采用深刻版的写法,不过每次写完我都以为我都不像是一个小甜甜了,却是有点像下图的那颗葱头......
若是以为文章不错,请点一个赞吧,这会是我最大的动力~
参考资料: