致读者
对于看到这篇博客的你们,其实不需要刻意的去记这些知识,可以收藏起来,有事没事翻开看看,这些其实看得多了,自然就烂熟于心了,如果你只是刻意的记忆了一遍,那其实你很快就会忘记,即使你当时看完可能觉得懂了,但事实上你很快就会忘了,过几个星期,你根本回忆不起来,感觉好像没看过这篇文章似的。所以说,赶紧先收藏,随时拿出来看,不然真的吃亏。
前言
Java语言中,类的构造方法是一种很特殊的方法。关于构造方法要记忆和理解的知识点其实挺多的,这个知识点其实初学的时候也一直困扰着我,因此接下来,我就来详细的讲讲构造方法,相信看过这篇文章之后,读者会对构造方法有一个比较深刻的认识和理解。
正文
首先来说说构造方法的三个特点:
一、构造方法的名称必须与类的名称相同。比如类的名称叫A,那么它的构造方法必须也叫A。
二、构造方法的前面不能声明返回值类型,即便是void也不行。只有满足了这两个条件,编译器才会认定这个方法是构造方法。
三、如果程序员没有在类中定义构造方法,那么在编译阶段,编译器会“免费赠送”给这个类一个构造方法。编译器给类赠送的这个构造方法是一个没有参数的构造方法。至于说编译器送给我们的这个构造方法里面有什么内容,咱们一会儿再说。
说完了构造方法的特点,我们再来说说构造方法的作用。很多书或者一些B站的视频上都把构造方法的作用说成是为了创建一个对象,其实这种理解是有问题的。必须承认,我们创建一个类的对象必须要调用构造方法,但构造方法的作用其实并不是为了创建对象,而是为了“初始化对象的内部状态”。“初始化对象的内部状态”这句话听起来不太好理解,其实就是为了给对象的各个属性赋初始值。我们来看一个例子:
我们定义了一个Person类,并且给Person类定义了两个属性,分别是String类型的name和int类型的age,并且还定义了一个printInfo()方法,用来打印这两个属性的值。接下来我们在main方法中创建两个Person类对象
执行程序的结果如下图:
我们发现,这两个对象的name属性和age属性的值都是默认的那个初始值,这种初始值其实没有太大意义。如果我们希望在创建对象的时候给就对象的两个属性赋上有意义的值,此时我们就可以把给属性赋值的语句写到构造方法当中。
有了构造方法之后,我们再次执行main方法,得到的结果是这样的:
从运行结果上来看,两个对象的属性都被赋予了有意义的值。但是问题来了:按照这样的写法,我们所创建的每个Person对象,name属性都被赋值为“张三”,而age属性都被赋值为20。这说明我们创建的对象是“千篇一律”的,并且从情理上也说不通,毕竟每个人都有属于自己的名字和年龄,不可能每个人都叫张三年龄20岁。
那么,如何在创建对象的时候,为每个对象都初始化属于自己的真实数据呢?这时候,我们就要用有参数的构造方法来搞定这个问题了。我们可以给构造方法添加两个参数,通过参数把数据传递给对象的属性
当我们给构造方法添加了两个参数之后,却发现main方法中原来本来正确的代码出现了语法错误
这是为什么呢?就是因为我们给Person类的构造方法添加了参数,现在Person类当中已经没有无参数的构造方法了,既然Person类当中已经没有了无参数的构造方法,那么我们在main方法中调用Person类无参数的构造方法,肯定会报错。
有人会问:编译器不是会“免费赠送”给每个类一个无参数的构造方法吗?那个送来的构造方法哪去了?这里需要特别说明一下:编译器“赠送”给类无参数构造方法是有条件的,这个条件就是:程序员没有为类定义构造方法。也就是说:只有程序员没有为类添加构造方法的情况下,编译器才会在编译的时候给这个类去自动添加一个无参数的构造方法,现在,程序员已经给Person类定义了构造方法,那么编译器就不会再给这个类添加构造方法了。
好,回归正题,现在我们想修改这个语法错误很简单,只要在main方法中给Person类的构造方法传递适当的参数就可以了
给构造方法传递了参数之后,语法错误自然消失。再次运行程序,会得到这样的结果
大家可以看到,这一次,我们就能够按照我们的意愿,创建出有自己个性化的对象了。通过这个例子我们可以看出:构造方法的作用是给对象的各个属性赋上合理的初始值,从而使得我们所创建的对象不再是“千篇一律”,而是“千姿百态”。
那么,现在我们可以再来思考两个问题:第一个问题:构造方法可以像普通方法那样实现重载吗?这是完全没有问题的,我们可以在一个类中定义多个构造方法,只要这些构造方法参数不同,就构成了重载。第二个问题:在一个构造方法中,可以调用另外一个构造方法吗?也没有问题,但调用的时候,需要注意:不能像调用普通方法那样,通过类名去调用,而是需要用一个关键字this。但是这种调用也有两个条件:
一、调用构造构造方法的语句必须放在第一行。
二、两个构造方法不能形成相互调用关系。为了方便表述,我们把一个类中的两个构造方法代称为X和Y。如果我们在X中调用了Y,那么就不能在Y中去调用X了。否则就会形成循环依赖关系,我们来看下面的例子
我们在一个构造方法中调用了另一个构造方法,调用的时候,需要用到this关键字,并且把调用语句写到第一行,这样才能顺利通过编译。
以上我们讲解的都是关于构造方法的基本常识,在这个讲解的过程中,并没有设计到类的继承关系。如果涉及继承关系,构造方法在定义和调用的过程中也有一些必须了解的知识点。
首先必须清楚,如果我们创建的是一个子类的对象,那么在创建这个子类对象的时候,虚拟机会先调用父类的构造方法,之后才去调用子类的构造方法。这个顺序不能错,否则会出现语法错误。为了说明问题:我们先来给Person类添加一个无参数的构造方法,并在构造方法中输出一句”父类的构造方法”
之后我们再定义出一个Person类的子类Student,并且在子类中也定义一个无参数的构造方法,在构造方法中输出一句”子类的构造方法“
之后,我们在main方法中创建一个子类对象
运行main方法,得到结果如下图:
很多人不明白,在子类的构造方法中,只是输入了“子类的构造方法”这样一句话,控制台上为什么同时还输出了“父类的构造方法”?原因就是我们刚才所说的:调用子类构造方法的时候,会首先调用父类的构造方法。即使程序员没有写代码去调用父类的构造方法,编译器也会把调用父类构造方法的语句补充添加到代码中。
那么,补充添加调用父类构造方法的代码,是如何实现的?这里必须先讲一下子类调用父类构造方法的语法细节:
一、子类调用父类构造方法的时候,不能通过构造方法本身的名称来调用,必须使用super关键字。
二、子类在它的普通方法中不能调用父类的构造方法,只能在它自身的构造方法中才能调用。
三、子类调用父类构造方法的语句,必须写在自身构造方法的第一行。
这三条语法规则至关重要,请牢记。按照这个语法规则,编译器看到程序员没有在子类构造方法中调用父类构造方法,会按下图所示的方式把调用父类构造方法的语句添加到代码中
通过上图可以看到,如果程序员没有在子类构造方法中添加调用父类构造方法的语句,编译器会自动把那条调用语句补充进来,并且放到子类构造方法的第一行。在文章一开始提出了一个问题:编译器会在赠送给我们的构造方法中添加什么内容,此时你应该明白了吧?就是因为在子类的构造方法中调用了父类的构造方法,所以我们才会在控制台上会看到两条输出语句。
现在又冒出一个问题:父类如果有好几个构造方法,编译器会自动调用哪一个呢?这里必须明确:编译器只会调用那个无参数的构造方法,不会调用有参数的构造方法。这个规则又会引发一个新的问题,那就是:如果父类中压根就没有无参数的构造方法,那怎么办?在这种情况下,编译器就会强制子类定义一个构造方法,并且在它的构造方法中,通过手动调用的形式去调用父类的构造方法,如果你不那么干,编译器就会使出它的杀手锏:划红线!来看下图:
既然无论程序员是否愿意,子类在它的构造方法当中必须要调用父类的构造方法,那么,通常情况下我们应该怎样调用父类构造方法才算合理呢?一般来讲,子类都会比父类拥有更多的属性。就本文而言,父类(Person)有2个属性,分别是name和age,而子类(Student)有3个属性,分别是name、age和num。当然,子类的3个属性当中,有2个是从父类那里继承过来的。在创建一个Student对象的时候,必须对这3个属性进行初始化。所以通常子类的构造方法会定义3个参数,这3个参数分别用来初始化子类的3个属性。既然子类的3个属性当中,有2个是继承于父类的,那么就可以用父类的构造方法去初始化那2个继承来的属性,而剩下的那个由子类自身所定义的属性num,可以在子类自身构造方法中实现初始化,看下图
通常来讲,子类就是这样调用父类构造方法的。
最后再回答一个问题:普通方法的名称可以与类名相同吗?答案是:可以!在这种情况下,编译器区分该方法是普通方法还是构造方法,就看方法的前面有没有声明返回值类型。这就是为什么Java语言规定构造方法不能有声明返回值类型的原因。