java 面向对象三大特性之继承 万字详解(超详细)
目录
⑥关于this(); 语句对本类构造器的调用(了解即可) :
前言 :
Hi,guys.这篇博文算是Java面向对象三大特性篇的第二篇———继承。博文内容包括但不限于继承的介绍,super关键字详解,属性、行为,以及构造器在子父类中的使用详解,方法重写,Java中四种访问权限修饰符等等。注意:①代码中的注释往往包含了讲解补充,别错过。②文章讲解详细,因此篇幅较长,大家可以有选择性的点击目录跳转查看。③标注(重要)的地方是重点内容。感谢阅读!
一、为什么需要继承:
类用来模拟现实事物。对于一切现实生活中存在的事物,比如猫,狗,鼠,人,飞船,树木......我们都可以定义一个对应的类,然后定义它们的属性和行为,再通过实例化该类的方式来模拟一个具体的这类事物,比如我家养了一只🐖叫佩奇,那么我就可以在实例化Pig类时,初始化Pig对象的属性name为"佩奇"。
但是,现实事物无穷无尽呀!就光说动物,目前世界上存在的动物就有超过上百万种!家喻户晓的一些动物像是🐖,🐕,🐱,🐀,🐍,🐟,🐅,👽,👻,🧟♂️等等乱七八糟的也有几十种甚至上百种。如果我们每模拟一种动物,我们都要在类中定义它的各种属性(比如物种名,颜色,地域,食性,栖息地等等)和行为(比如吃喝拉撒,衣食住行等等),那不是很麻烦吗?
实际上,所有的动物都有物种名,都有颜色,都有栖息地,都要吃喝拉撒!鉴于所有的动物都有这些共同点,我们就诞生出这么个想法来 : 我们直接上升一个维度,不再模拟零零散散的诸如🐖呀,🐎呀,🐏呀各种动物,而是直接去模拟动物本身!没错!为什么不直接定义一个Animal类(动物类)呢,直接在Animal类中定义诸如name,age,color,habitat这些属性,还有诸如eat(),drink(),sleep()这些行为,然后让每个模拟某种特定动物的类去和Animal类产生一种特殊关系,使得每一个和Animal类有特殊关系的类,都可以直接或间接访问Animal类中的这些属性和行为。
如此这般,以后我们再定义类来模拟某种动物时,岂不是省去了反复定义这些公共属性和行为的麻烦?我们只需要定义这种动物的特有的属性和行为,比如变色龙会变色,蜣螂会滚粪球等等,而像物种名呀,寿命呀,颜色呀,吃喝拉撒呀这些公有的属性和行为,就让它用Animal类的不就完了。其实,我们说的这种所谓特殊的关系,已经在Java中实现!我们把这种特殊关系,称为“继承”,Java中通过extends关键字实现。当一个类“继承”了另一个类,我们称前者为子类,后者为父类,对应于我们方才的说法,即子类与父类发生了这种特殊关系,这种特殊关系就是继承。
二、什么是继承(Warning : 篇幅较长)?
1.继承概述:
继承就是让类与类之间产生父子关系,即出现了父类和子类。继承实际就是一种关系。
我们来对比一下类的定义 : 类是一系列具有相同属性和行为的事物的统称。而将一系列相关事物共同的属性和行为提取出来的过程,称为抽象。现在,我们把定义中的“事物”换作“类”,即,将多个类中相同的属性和行为抽象出来,并定义在同一类中,这便是父类。而所有通过extends关键字来声明继承父类的类,都叫做该父类的子类。简单来说 :
被继承的类叫做父类(也叫超类,基类)。
继承的类叫做子类(也叫派生类)。
2.子类继承父类之后达到的效果 :
①如何使用继承?
Java中如果使用继承,需要用到extends关键字,在定义子类时,子类类名后跟上extends + 要继承的父类类名,即可。举个栗子,假设Cat类继承了Animal类,如下 :
public class Cat extends Animal {
//Cat类继承了Animal类
}
那么,子类继承父类后有什么效果呢?
Ⅰ子类拥有了父类的所有非私有成员(成员变量、成员方法),并可以直接访问它们。
Ⅱ对于父类的私有成员(成员变量、成员方法),子类只能通过父类提供的公有方法来间接访问父类的私有成员。
Ⅲ同时,子类也可以有自己特有的属性(成员变量)和行为(成员方法)。
②代码演示 :
我们以Fruit类来模拟水果。水果的种类也是数不胜数,葡萄🍇就是万千水果中的一种,我们以Grape类来模拟葡萄。水果是不是都有水果名、甜度、大小(尺寸)、营养等等这些共性的属性;而且水果都需要进行光合作用,呼吸作用这些共性的行为。葡萄显然是水果的一种,因此我们想让Grape类继承Fruit类,这样Grape类中就不必再重复定义这些共性的属性和行为了,使代码简洁高效。继承情况如下图所示 :
演示效果Ⅰ:
在effect包下,我们先新建一个Fruit类当作父类,再新建一个Grape类当作子类,让子类Grape继承父类Fruit,最后建一个TestEffect类作为演示类。在Fruit类中,我们定义species_name(物种名)、size(尺寸)、sweetness(甜度),nutrition(营养)这些成员变量,定义photosynthesis([ˌfoʊtoʊˈsɪnθəsɪs] 光合作用), respiratory_action([ˈrespərətɔːri] 呼吸作用)这些成员方法。
Fruit类代码如下 :
package knowledge.succeed.effect;
//父类:Fruit
public class Fruit {
//成员变量
String species_name; //物种名
String size; //尺寸
double sweetness; //甜度
String nutrition; //营养
//成员方法
public void photosynthesis() { //光合作用
System.out.println("光合作用消耗二氧化碳和水,生成氧气和有机物");
}
public void respiratory_action() { //呼吸作用
System.out.println("呼吸作用消耗葡萄糖和氧气,生成水,二氧化碳和ATP");
}
}
可以看到,Fruit类中的成员变量和成员方法均为非私有,那么Grape类继承Fruit类后必然拥有了Fruit类所有的成员。Grape类代码如下 :
package knowledge.succeed.effect;
//子类 : Grape
public class Grape extends Fruit{
//啥都没写捏🤗
}
TestEffect类代码如下 :
package knowledge.succeed.effect;
public class TestEffect {
public static void main(String[] args) {
Grape grape = new Grape();
//1.测试成员变量
grape.sweetness = 11;
grape.species_name = "葡萄";
grape.size = "单个葡萄:small;整串葡萄:normal";
grape.nutrition = "糖类,矿物质,维生素";
System.out.println("species_name = " + grape.species_name);
System.out.println("size = " + grape.size);
System.out.println("sweetness = " + grape.sweetness);
System.out.println("nutrition = " + grape.nutrition);
System.out.println("----------------------------------------");
//2.测试成员方法
grape.photosynthesis(); //葡萄光合作用
grape.respiratory_action(); //葡萄呼吸作用
}
}
运行结果 :
可以看到,由于Fruit类中没有私有成员,Grape类继承Fruit类后,随即拥有了Fruit类中的所有非私有成员,也就是拥有了Fruit类中的所有成员。我们在TestEffect类中通过"对象."的形式可以直接调用Fruit类中的属性和行为,IDEA并未报错,并且输出结果也在预期之内。
演示效果Ⅱ:
以上是Fruit类的成员都是非私有的情况,现在,我们为Fruit类中的size和species_name成员变量添加private修饰符,将这两个属性改为私有成员。如下图所示 :
但是,刚改完,IDEA立马给出了报错,错误出现在TestEffect类,如下图所示 学完封装后,相信大家对这个问题早已司空见惯。在封装中,如果类中有私有成员,那么实例化该类后,不能通过"对象."的形式来直接调用该类的私有成员,仅能通过公共的方法来间接访问这些私有成员(比如setter,getter方法)。现在就是这么个情况,如出一辙😎!只不过这里Grape类对象调用的私有成员是它的父类Fruit类的成员,仅此而已。
因此,我们也是老办法招呼,在Fruit类中给出这两个属性的setter,getter方法,然后在TestEffect类调用setter,getter方法就⭐了?Fruit类代码如下 :
package knowledge.succeed.effect;
//父类:Fruit
public class Fruit {
//成员变量
private String species_name; //物种名
private String size; //尺寸
double sweetness; //甜度
String nutrition; //营养
//两个私有成员species_name和size的setter,getter方法
public String getSpecies_name() {
return species_name;
}
public void setSpecies_name(String species_name) {
this.species_name = species_name;
}
public String getSize() {
return size;
}
public void setSize(String size) {
this.size = size;
}
//成员方法
public void photosynthesis() {
System.out.println("光合作用消耗二氧化碳和水,生成氧气和有机物");
}
public void respiratory_action() {
System.out.println("呼吸作用消耗葡萄糖和氧气,生成水,二氧化碳和ATP");
}
}
Grape类代码不变,我们略过。TestEffect类代码如下 :
package knowledge.succeed.effect;
public class TestEffect {
public static void main(String[] args) {
Grape grape = new Grape();
//1.测试成员变量(注意species_name和size这两个属性在调用时的变动)
grape.setSpecies_name("葡萄");
grape.setSize("单个葡萄:small;整串葡萄:normal");
grape.sweetness = 11;
grape.nutrition = "糖类,矿物质,维生素";
System.out.println("species_name = " + grape.getSpecies_name());
System.out.println("size = " + grape.getSize());
System.out.println("sweetness = " + grape.sweetness);
System.out.println("nutrition = " + grape.nutrition);
System.out.println("----------------------------------------");
//2.测试成员方法
grape.photosynthesis(); //葡萄光合作用
grape.respiratory_action(); //葡萄呼吸作用
}
}
运行结果 :
演示效果Ⅲ:
我们已经看到了,对于父类的私有成员和非私有成员,子类继承后使用效果的不同。但别忘了,我们上面的第三条还说了:子类继承父类后,除了拥有了父类的非私有成员,子类还可以拥有自己的特有成员。
现在,我们来想一想,葡萄有没有其他水果没有的一些特有的属性,或者行为?up想了很久🤔:
葡萄特有的行为的话,up想到的是葡萄可以用来酿葡萄酒,其他的任何一种水果,都没法酿葡萄酒捏🤗。葡萄特有的属性,up是真想不出来,硬要说的话,葡萄一定是葡萄😋!因此,我们可以在Grape类中定义一个成员变量grape,一个成员方法grape(grape也有葡萄酒的意思😂)。
Fruit类代码不变。Grape类代码如下 :
package knowledge.succeed.effect;
//子类 : Grape
public class Grape extends Fruit{
//Grape类的特有属性
String grape;
//grape属性的getter,setter方法
public String getGrape() {
return grape;
}
public void setGrape(String grape) {
this.grape = grape;
}
//Grape类的特有行为
public void grape() {
System.out.println("除了葡萄!还有谁?能酿葡萄酒!");
}
}
TestEffect类代码如下 :
package knowledge.succeed.effect;
public class TestEffect {
public static void main(String[] args) {
Grape grape = new Grape();
//1.测试Grape类的特有成员变量
grape.setGrape("葡萄一定是葡萄!😅");
System.out.println("grape = " + grape.getGrape());
System.out.println("----------------------------------------");
//2.测试Grape类特有成员方法
grape.grape();
}
}
输出结果 :
3.继承的使用场景:
当多个类中存在相同的属性和行为时,可以将这些相同的内容提取出来并放到一个新的类中(也就是父类),然后再让这些类和父类产生继承关系,以实现代码复用。
eg1 :
Dog类,Pig类,Cat类,Cow类都是在模拟动物,因而它们都会有species_name(物种名),average_age(平均寿命),以及sex(性别)等相同的属性,也都会有sleep(睡觉),eat(吃),drink(喝)等相同的行为。这时我们就可以将这些相同的属性或行为提取到Animal类中,然后让这些类分别与Animal类(动物类)发生继承关系。
关系图如下 :
代码如下 :
复习我们之前讲过的JavaBean标准代码规范😎,这里的Animal类我们以标准JavaBean格式敲(子父类之间属性,行为,构造器的使用我们下面会讲到),即Animal类成员变量全部私有化并提供访问它们的公共方法;成员方法全部公共化。这里我们也可以继续贯彻上文提到的——子类继承父类后达到的效果——我们可以分别在Dog类,Cat类,Pig类和Cow类定义它们的特有属性和行为。比如:
Dog🐶可以看家,因此可以在Dog类中定义一个look_after_the_house()方法;
Cat🐱可以上树,因此可以在Cat类中定义一个climb_tree()方法;
Pig🐖可以打鼾,因此可以在Pig类中定义一个snore()方法;
Cow🐂可以生产牛奶,因此可以在Cow中定义一个milk()方法。
一个父类四个子类,一共五个类up都放在同一个.java文件里了(省事儿)。我们在situation(使用情景)包下创建一个Animal类,接着在Animal类的源文件中继续敲完四个子类,就不用新建新的源文件了。注意:此时四个子类均不可以用public修饰!
Animal类,Dog类,Cat类,Pig类,Cow类代码如下 :
package knowledge.succeed.situation;
public class Animal { /**父类 : Animal类*/
//父类属性(私有)
private String species_name; //物种名
private double average_age; //平均寿命
private String sex; //性别
//父类构造器
public Animal() {
}
public Animal(String species_name, double average_age, String sex) {
this.species_name = species_name;
this.average_age = average_age;
this.sex = sex;
}
//父类私有属性的获取方法
public String getSpecies_name() {
return species_name;
}
public void setSpecies_name(String species_name) {
this.species_name = species_name;
}
public double getAverage_age() {
return average_age;
}
public void setAverage_age(double average_age) {
this.average_age = average_age;
}
public String getSex() {
return sex;
}
public void setSex(String sex) {
this.sex = sex;
}
//父类行为(公有)
public void sleep() {
System.out.println("都给👴睡睡睡睡睡😴😴😴😴😴");
}
public void eat() {
System.out.println("吃喝拉撒,吃排第一位,什么地位不用我说了吧?");
}
}
//-----------------------------------------------------------
class Dog extends Animal { /**子类 : Dog类*/
public void look_after_the_house() {
System.out.println("👴虽然🐶,但👴能看家!");
}
}
class Cat extends Animal { /**子类 : Cat类*/
public void climb_tree() {
System.out.println("👴虽然🐱,但👴能爬树!");
}
}
class Pig extends Animal { /**子类 : Pig类*/
public void snore(){
System.out.println("👴虽然🐖,但👴能打鼾!");
}
}
class Cow extends Animal { /**子类 : Cow类*/
public void milk(){
System.out.println("👴虽然🐂,但👴能挤奶!");
}
}
接着,up还是在situation包下,新建一个Test类,测试我们敲好的类的继承效果。
Test类代码如下 :
package knowledge.succeed.situation;
//继承的使用情景:测试Animal类的四个子类。
public class Test {
public static void main(String[] args) {
//1.测试第一个子类:Dog类
Dog dog = new Dog();
dog.setSpecies_name("狗");
dog.setAverage_age(12.5);
dog.setSex("公的");
System.out.println("dog's species_name = " + dog.getSpecies_name());
System.out.println("dog's average_age = " + dog.getAverage_age());
System.out.println("dog's sex = " + dog.getSex());
System.out.println("------------------------------------------");
dog.sleep();
dog.eat();
dog.look_after_the_house();
System.out.println("==========================================");
System.out.println();
//2.测试第二个子类:Cat类
Cat cat = new Cat();
cat.setSpecies_name("猫");
cat.setAverage_age(14);
cat.setSex("母的");
System.out.println("cat's species_name = " + cat.getSpecies_name());
System.out.println("cat's average_age = " + cat.getAverage_age());
System.out.println("cat's sex = " + cat.getSex());
System.out.println("------------------------------------------");
cat.sleep();
cat.eat();
cat.climb_tree();
System.out.println("==========================================");
System.out.println();
//3.测试第三个子类:Pig类
Pig pig = new Pig();
pig.setSpecies_name("猪");
pig.setAverage_age(20);
pig.setSex("雄的");
System.out.println("pig's species_name = " + pig.getSpecies_name());
System.out.println("pig's average_age = " + pig.getAverage_age());
System.out.println("pig's sex = " + pig.getSex());
System.out.println("------------------------------------------");
pig.sleep();
pig.eat();
pig.snore();
System.out.println("==========================================");
System.out.println();
//4.测试第四个子类:Cow类
Cow cow = new Cow();
cow.setSpecies_name("牛");
cow.setAverage_age(25);
cow.setSex("雌的");
System.out.println("cow's species_name = " + cow.getSpecies_name());
System.out.println("cow's average_age = " + cow.getAverage_age());
System.out.println("cow's sex = " + cow.getSex());
System.out.println("------------------------------------------");
cow.sleep();
cow.eat();
cow.milk();
}
}
输出结果:(太多了截图放不下,因此这里以GIF图展示)
eg2 :
坤坤,相信大家都不陌生吧。坤坤凭借男团选秀节目《偶像练习生》巨C出道😍!随后开始疯狂圈粉,路人对人坤坤的态度也是纷纷好转🤗。如今在某浪超话榜上,坤坤总是稳稳坐在前三,而且坤坤也是唯一一个超话粉丝打破千万的明星🤩!甚至在2019年,坤坤还担任了热血淋漓👊,肌肉碰撞💪的NBA🏀的新春大使!但是,如此丰功伟绩🏆总会引起一些人的分外眼红🤬。因此,坤坤的粉丝群体中难免有少许的黑粉,我们称为小黑子,也就是little_black;而真爱党,我们称为ikun;还有一些人既不是真爱党也不是小黑子,我们称为路人党,也就是road_man。
因此,要模拟坤坤粉丝团体,我们可以定义一个Kunkun_Fans类,表示坤坤的粉丝群体,作为父类。而小黑子,真爱党,路人党都属于坤坤的粉丝群体,因此可以分别定义Little_black类,Ikun类,Road_man类来继承Kunkun_Fans类。因为三个群体都是粉丝,因此它们会有相同的属性如love_degree(爱的程度),love_year(粉坤坤的时间,按年计),love_slogan()等等;以及相同的行为如beat_call(为坤坤打call)等。
关系图如下 :
代码如下 :
我们在eg2包下创建父类Kunkun_Fans,仍旧以JavaBean类标准来敲父类。和eg1类似,仍旧将父类和子类放在一个.java源文件里,省事儿,即敲完父类后接着敲子类,但是子类不可以再定义成public类型,因为源文件有且仅能有一个public关键字修饰的类,且只要源文件中存在public修饰的类,该类的类名就必须与源文件名相同(这个我们之后统一讲面向对象时会讲到)。
Kunkun_Fans类,Little_black类,Ikun类,Road_man类代码如下 :
package knowledge.succeed.situation.eg2;
public class Kunkun_Fans { /** 父类 : Kunkun_Fans类*/
//父类的成员变量(私有)
private String love_degree;
private String love_year;
private String love_slogan;
//父类的构造器
public Kunkun_Fans() {
}
public Kunkun_Fans(String love_degree, String love_year, String love_slogan) {
this.love_degree = love_degree;
this.love_year = love_year;
this.love_slogan = love_slogan;
}
//父类私有属性的获取方法
public String getLove_degree() {
return love_degree;
}
public void setLove_degree(String love_degree) {
this.love_degree = love_degree;
}
public String getLove_year() {
return love_year;
}
public void setLove_year(String love_year) {
this.love_year = love_year;
}
public String getLove_slogan() {
return love_slogan;
}
public void setLove_slogan(String love_slogan) {
this.love_slogan = love_slogan;
}
//父类的成员方法(公开)
public void beat_call(String call) {
System.out.println(call);
}
}
//---------------------------------------------------------------
class Little_black extends Kunkun_Fans { /** 子类 : Little_black*/
}
class Ikun extends Kunkun_Fans{ /** 子类 : Ikun*/
}
class Road_man extends Kunkun_Fans { /** 子类 : Road_man*/
}
接着,我们仍在eg2包下,创建一个Test2类,用来测试继承的使用情景eg2。
Test2类代码如下 :
package knowledge.succeed.situation.eg2;
//测试继承的使用情景之eg2:
public class Test2 {
public static void main(String[] args) {
//1.测试第一个子类 : Little_black
Little_black lb = new Little_black();
lb.setLove_degree("最爱kunkun😍,没有之一");
lb.setLove_year("永远!forever!🤗");
lb.setLove_slogan("🐔你太美~~🐔你实在是太美~");
System.out.println("lb's love_degree = " + lb.getLove_degree());
System.out.println("lb's love_year = " + lb.getLove_year());
System.out.println("lb's love_slogan = " + lb.getLove_slogan());
System.out.println("----------------------------------------------------");
lb.beat_call("即使全世界都与KunKun为敌,我们也会毫不犹豫地站在全世界这边!");
System.out.println("=====================================================");
System.out.println();
//2.测试第二个子类 : Ikun
Ikun ikun = new Ikun();
ikun.setLove_degree("挚爱kunkun");
ikun.setLove_year("maybe,forever too.");
ikun.setLove_slogan("微博不倒,陪kun到老!");
System.out.println("ikun's love_degree = " + ikun.getLove_degree());
System.out.println("ikun's love_year = " + ikun.getLove_year());
System.out.println("ikun's love_slogan = " + ikun.getLove_slogan());
System.out.println("----------------------------------------------------");
ikun.beat_call("你知不知道我们家icon多么努力!");
System.out.println("=====================================================");
System.out.println();
//3.测试第三个子类 : Road_man
Road_man road_man = new Road_man();
road_man.setLove_degree("随缘,忽高忽低");
road_man.setLove_year("随缘,忽长忽短");
road_man.setLove_slogan("随便");
System.out.println("road_man's love_degree = " + road_man.getLove_degree());
System.out.println("road_man's love_year = " + road_man.getLove_year());
System.out.println("road_man's love_slogan = " + road_man.getLove_slogan());
System.out.println("----------------------------------------------------");
road_man.beat_call("你干嘛~");
}
}
输出结果 : (GIF图)
4.继承的优点和缺点 :
Δ优点 :
①提高了代码的复用性,实现了功能复用。
②结构清晰,简化认识。(类于类之间的关系简洁明了)
③便于扩展新功能(子类可以重写父类的方法,还可以定义自己的方法)
④易维护性
Δ缺点 :
①打破了封装性(父类向子类暴露了实现细节)
②高耦合性,类与类之间的依赖性高了,不符合“低耦合,高内聚”的程序设计要求.
三、 继承关系中成员变量的使用(重点) :
Δ前言 :
继承关系中成员变量的使用,和继承关系中成员方法的使用,其实是大同小异!因此,up着重演示前者,尽可能详细地讲解。希望大家注意这个模块。
1.Java中查找变量的原则:
“就近原则”
2.Java中查找变量的顺序:
局部变量—>成员变量—>父类—>更高的父类—>......—>Object
人话 : 强龙不压地头蛇,儿子没有找爹要。
Δ注意 :
①Object类是Java中所有类的顶层父类!(之后我们讲到API时会详细阐释)
②如果本类没有该变量,父类中有该变量但是为父类私有时,IDEA会报错!因为私有属性无法在其他类直接访问。(已讲过🌶)
③如果从局部位置开始一直找到Object类也没有找到该变量,IDEA会报错!
④第②种情况和第③种情况的报错性质是不同的。前者是因为找到了但没法直接用,后者是因为干脆没找到。这是两回事儿了。
3.关于super关键字 :
类似于this关键字(封装篇我们已讲过捏🤗),super关键字也是一个指针,当然了,在Java中叫做引用。我们知道,this是对象的隐藏属性,是一个指向当前对象的引用。而super,则是指向当前对象父类的引用(即父类内容内存空间的标识)。当new关键字创建子类对象时,子类对象的堆空间中会有一部分用于存放父类的内容,即继承自父类的非私有成员。super就指向这么一部分。可以理解为,super指向的部分是在this指向的部分的范围内。在使用时,this从本类开始找,super从父类开始找。光嘴上说多少还是太抽象,来张内存图直观清晰的展示一下 :
4.直接访问父类变量的方式 :
super.父类变量名 (仅能访问非私有属性,所以是直接访问)
5.new关键字创建对象后,对象初始化顺序 :
先初始化父类内容,再初始化子类内容。(原因是创建子类对象时,优先调用父类的构造器,构造器在继承中的使用——文章后面会讲到。)
6.代码演示 :
声明 :
up以Father类作为演示父类,以Son类作为演示子类,以TestVariable类作为演示测试类。以下代码演示均围绕这三个类展开。
①查找变量原则的演示 :
其实我们在封装篇中,讲到this关键字的强龙地头蛇布局定式时,就已经讲过Java中查找变量的原则了。因此,封装篇掌握的小伙伴儿们可以跳过①②演示。
我们在Father类中定义一个成员变量temp,并给它显式赋值为11;在Son类中也定义一个变量temp,并给它显式赋值为5;然后在Son类中定义一个printTemp方法用来打印temp变量,并且,在printTemp方法内部,我们再定义一个局部变量,也叫temp。
接着我们就可以在TestVariable类中创建子类对象并通过子类对象调用printTemp方法。如果按照就近原则,当然是优先输出printTemp方法内部——局部位置的temp😎!那到底是不是这样捏🤗?我们拭目以待:
Father类代码如下 :
package knowledge.succeed.aboutfield;
//父类 : Father (仅作为演示,无实际意义)
public class Father {
int temp = 11;
}
Son类代码如下 :
package knowledge.succeed.aboutfield;
//子类 : Son (仅作为演示,无实际意义)
public class Son extends Father {
int temp = 5;
public void printTemp() {
int temp = 55555;
System.out.println("按照就近原则,默认输出的temp = " + temp);
}
}
TestVariable类代码如下 :
package knowledge.succeed.aboutfield;
//测试类 : TestVariable
public class TestVariable {
public static void main(String[] args) {
Son son = new Son();
son.printTemp();
}
}
输出结果 :
很明显,与我们的猜测完全一致😎!
②查找变量顺序的演示 :
在演示①的代码基础上,我们注释掉printTemp方法中局部变量的定义,其余代码不变。如下图所示 :
根据Java中查找变量的顺序,如果局部位置没有变量,要接着扩大到本类查找,恰好!我们在本类中也有temp变量😋。那么,不出意外的话,默认输出的temp就会从55555变成5。
好的,我们还是运行TestVariable类,输出结果如下 :
🐂,果不其然!Go on,我们趁热打铁,把Son类中的temp变量也给注释掉!其余代码不变。如下图所示 :
那么,根据变量查找原则,局部位置没有!本类成员位置也没有!接下来就该找他爹算账了,你别说!他爹Father类还真定义了temp变量。因此,打印出的temp变量应该由5变成11。 继续运行TestVariable类,运行结果如下 :
🆗,看来Java中默认的查找变量的顺序还真是这样!诶,这时候就要有p小将(personable小将,指风度翩翩的人)要挑刺儿提问了:有本事你把Father类的temp变量也给注释掉!敢不?
啧,不愧是p小将,6😅。好滴,刚准备引出我们常见的两个问题嘞,我们就从容应战,如下图所示,我们把父类的temp变量也注释掉,其余代码不变。看看会怎么样:
根据我们刚刚的注意事项,算求,估计大家也记不住😂。给大家再搬过来,如下图:
没错,如果我们把Father类的temp变量也注释掉,而Father类默认继承Object类(Object类是所有类的顶层父类),Object类是不会存在这种我们人为定义的成员变量的。因此,再次运行TestVariable类,IDEA该报错了捏🤗。运行结果如下 :
果不其然,IDEA报错 : 显示找不到符号temp。这就是我们之前说过的——如果从局部位置开始,一直找到Object类都没有找到该变量,IDEA会报错。
当然,还有一种错误,也是初学者使用继承时经常遇到的:父类成员变量根据JavaBean标准用了private修饰符修饰,这时候如果找到了父类,即使父类中有该变量IDEA也会报错,因为父类私有啊!你不能跨类直接使用。🆗,接下来,我们给父类中的temp变量增加private修饰符,看看效果如何,如下图所示 :
龟龟!刚改完,还没运行呢,就给报错了😂。如下图所示 :
是的,这就是我们方才说的——如果本类没有该变量,父类中有该变量但是为父类私有时,IDEA会报错!因为私有属性无法在其他类直接访问。
③super关键字使用父类成员变量的演示 :
在三大特性之封装篇里,我们讲过,this关键字可以解决“强龙🐉不压地头蛇🐍”的布局定式,其实就是解决了局部变量和本类成员变量的命名冲突问题。this关键字使得我们可以在局部变量存在的情况下,避开Java就近原则的约束,在局部位置使用成员变量。
而super关键字,在作用上与this关键字有着异曲同工之妙。无非super就是从父类开始找么!在Java变量查找顺序中直接跳到了父类这一环节,避开Java查找顺序的约束,在局部位置使用父类成员变量。因此,我们可以认为:父类成员变量和子类局部变量的命名冲突问题,可以理解为强龙🐉不压地头蛇🐍的一个变式。而super关键字可以轻松解决这个问题。
🆗,正片开始!
我们在Father类中定义3个成员变量,分别为temp1,temp2,temp3。其中,temp1~3均不添加修饰符(即非私有)。然后分别在子类Son类中的成员位置和局部位置(printTemp方法内)也定义temp1,temp2,temp3的同名变量。仍然使用printTemp方法来打印变量,但这次,我们想利用该方法同时打印出printTemp方法内局部变量、Son本类成员变量,以及Son类的父类Father类的成员变量。
Father类代码如下:
package knowledge.succeed.aboutfield;
//父类 : Father (仅作为演示,无实际意义)
public class Father {
int temp1 = 11;
int temp2 = 12;
int temp3 = 13;
}
Son类代码如下 :
package knowledge.succeed.aboutfield;
//子类 : Son (仅作为演示,无实际意义)
public class Son extends Father {
public void printTemp() {
int temp1 = 55555;
int temp2 = 5555;
int temp3 = 555;
System.out.println("按照就近原则,直接输出的temp1 = " + temp1);
System.out.println("按照就近原则,直接输出的temp2 = " + temp2);
System.out.println("按照就近原则,直接输出的temp3 = " + temp3);
System.out.println("----------------------------------------------------------");
System.out.println("使用this关键字解决强龙地头蛇布局定式,temp1 = " + this.temp1);
System.out.println("使用this关键字解决强龙地头蛇布局定式,temp2 = " + this.temp2);
System.out.println("使用this关键字解决强龙地头蛇布局定式,temp3 = " + this.temp3);
System.out.println("----------------------------------------------------------");
System.out.println("强龙地头蛇变式之输出父类成员变量temp1 = " + super.temp1);
System.out.println("强龙地头蛇变式之输出父类成员变量temp2 = " + super.temp2);
System.out.println("强龙地头蛇变式之输出父类成员变量temp3 = " + super.temp3);
}
}
TestVariable类代码如下 :
package knowledge.succeed.aboutfield;
//测试类 : TestVariable
public class TestVariable {
public static void main(String[] args) {
Son son = new Son();
son.printTemp();
}
}
输出结果 :
四、继承关系中成员方法的使用 :
1.Java中查找方法的原则 :
“就近原则”(和查找变量的原则一样)
2.Java中查找方法的顺序 :
本类方法—>父类方法—>更高一级的父类—>......Object(顶层父类)
(整体和查找变量的顺序略有出入。因为Java中方法不能嵌套,因此不存在所谓“局部方法”,直接就从本类中开始找了。)
3.super关键字访问父类成员方法 :
通过"super.方法名" 的形式调用(注意: 仅能访问父类的非私有方法)
4.子父类中有定义重名方法的情况 :
当父类方法无法满足实际需求,需要拓展父类方法的功能时;或者当父类方法的功能需要被重新实现时,我们可以在子类中定义一个与父类方法同名的方法。
注意 :
这里的“重名方法”,有两种情况,
一种就是子类方法和父类方法的返回值类型、方法名,参数列表都相同,达到了方法重写的效果。(其实就是我们后面要讲到的方法重写,仅方法体内的语句不同)
另一种就是子类方法和父类方法的方法名相同而参数列表不同,这么做IDEA不会报错,实际使用时可以达到方法重载的效果。(然而这并不符合方法重载的定义,仅仅是因为子类拥有了父类的非私有成员,而这么类比一下)。
5.代码演示 :
声明 :
up以Animal类作为演示父类,以Chicken类作为演示子类,以TestMethod类作为演示测试类。以下代码演示均围绕这三个类展开。
①Java中查找方法的原则及顺序演示 :
我们在父类Animal类和子类Chicken类中都定义一个公有的eat方法,然后在TestMethod类创建子类对象,并通过“对象.”的形式调用eat方法。根据就近原则,默认调用的肯定是Chicken类的eat方法。
Animal类代码如下 :
package knowledge.succeed.aboutmethod;
//父类 : Animal类(JavaBean标准)
public class Animal {
//成员变量
private String name;
private int age;
private String sex;
//构造器
public Animal() {
}
public Animal(String name, int age, String sex) {
this.name = name;
this.age = age;
this.sex = sex;
}
//成员变量的getter和setter方法
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getSex() {
return sex;
}
public void setSex(String sex) {
this.sex = sex;
}
//成员方法
public void eat() {
System.out.println("来看看热量是多少?妹说就是0卡!");
}
}
Chicken类代码如下 :
package knowledge.succeed.aboutmethod;
//子类 : Chicken类
public class Chicken extends Animal {
public void eat() {
System.out.println("怎么这鸡胸到我嘴里变成泡面了?😭");
System.out.println("我被诅咒啦!😭");
}
}
TestMethod类代码如下 :
package knowledge.succeed.aboutmethod;
//测试类 : TestMethod类
public class TestMethod {
public static void main(String[] args) {
Chicken chicken = new Chicken();
chicken.eat();
}
}
运行结果 :
根据运行结果来看,确实优先调用了子类的eat方法,符合我们预期。
接下来,我们将子类中的eat方法注释掉,其他代码不变,如下图所示 :
然后我们再次运行TestMethod类,根据Java中查找方法的顺序,本类找不到,就要去它的父类找。 因此,这次调用eat方法的输出结果,肯定是父类eat方法中的内容。
输出结果如下 :
②super关键字访问父类成员方法的演示 :
演示Ⅰ:
根据①中的代码,父类的eat方法中,仅告诉我们妹说就是0卡!但它没说让我们吃啥呀。因此,我们想对这个方法做一些补充。up在子类中定义一个zeroCalorie(0卡)方法,先在zeroCalorie方法中通过"super.eat();" 来调用父类的eat方法,然后在zeroCalorie方法中补充一条输出语句,告诉我们吃的是鸡肉(左旋溜达🐔)。
Animal类代码不变。
Chicken类代码如下 :
package knowledge.succeed.aboutmethod;
//子类 : Chicken类
public class Chicken extends Animal {
public void zeroCalorie(String thin) {
super.eat();
System.out.println("快吃吧!左旋溜达🐔!\n" + thin);
}
}
TestMethod代码如下 :
package knowledge.succeed.aboutmethod;
//测试类 : TestMethod类
public class TestMethod {
public static void main(String[] args) {
Chicken chicken = new Chicken();
chicken.zeroCalorie("少吃点,要不明天得瘦死┭┮﹏┭┮");
}
}
运行结果 :
诶,确实,通过super关键字,我们成功在子类方法中调用了父类方法。
演示Ⅱ :
设若父类的eat方法为私有方法。我们怎么办?很简单。通过this关键字和super关键字的配合,就可以轻松解决这个问题。首先在父类中定义一个公开的eatEX方法,然后在eatEX方法中通过this关键字调用本类的eat方法。而在子类zeroCalorie方法中," super.eat(); " 要变成 " super.eatEX(); "。
Animal类代码如下 :
package knowledge.succeed.aboutmethod;
//父类 : Animal类(JavaBean标准)
public class Animal {
//成员变量
private String name;
private int age;
private String sex;
//构造器
public Animal() {
}
public Animal(String name, int age, String sex) {
this.name = name;
this.age = age;
this.sex = sex;
}
//成员变量的getter和setter方法
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getSex() {
return sex;
}
public void setSex(String sex) {
this.sex = sex;
}
//成员方法
private void eat() {
System.out.println("来看看热量是多少?妹说就是0卡!");
}
public void eatEX() {
this.eat();
}
}
Chiken类代码如下 :
package knowledge.succeed.aboutmethod;
//子类 : Chicken类
public class Chicken extends Animal {
public void zeroCalorie(String thin) {
super.eatEX();
System.out.println("快吃吧!左旋溜达🐔!\n" + thin);
}
}
TestMethod类代码不变。
TestMethod类运行结果如下 :
③子父类重名方法的演示 :
前面我们也说了,子父类重名方法有两种情况。在演示Java中方法的查找原则及顺序时,我们在子父类中定义的eat方法的方法名,参数列表,返回值类型都相同,这其实就是第一种情况,也就是方法重写的情况。因此这里不再赘述。(方法重写在文章后面还会详细讲到)
重点说一下第二种情况 : 就是子类和父类方法的方法名相同但参数列表不同。参数列表不同显然不符合方法重写了,但是由于子类也拥有父类的非私有方法,因此我们不由得联想到了方法重载的效果。我们仍以eat方法为栗,让子类中的eat方法增加String类型的形参,以便告诉我们吃得0卡的是什么东西。
Animal类代码不变。
Chicken类代码如下 :
package knowledge.succeed.aboutmethod;
//子类 : Chicken类
public class Chicken extends Animal {
public void eat(String food) {
System.out.println("怎么这鸡胸到我嘴里变成泡面了?😭");
System.out.println("我被诅咒啦!😭");
System.out.println("吃得什么东西?" + food);
}
}
TestMethod类代码如下 :
package knowledge.succeed.aboutmethod;
//测试类 : TestMethod类
public class TestMethod {
public static void main(String[] args) {
Chicken chicken = new Chicken();
chicken.eat("左旋儿溜达🐔");
}
}
运行结果 :
五、继承关系中构造器的使用(重点):
1.前言 :
按理说,我们应该先讲构造器在继承中的使用,再讲成员变量,成员方法在继承关系中的使用。但是吧,构造器这玩意儿,太tm抽象。所以,up才决定放在最后去讲。废话少说,正片如下 :
大家是否还记得(多半是不记得了🤗)我们在前面“继承关系中成员变量的使用”中说过:继承关系中,对象的初始化顺序为 : 先初始化父类内容,后初始化子类内容。那么,大家想想:谁来初始化对象的内容?没错,构造器呀!这也是继承设计中的基本思想 : 父类的构造器初始化父类内容,子类的构造器初始化子类内容。
但这时候就要有p小将(personable小将,指风度翩翩的人)出来问了 : 吹牛逼谁也会!看你前面写的代码。子类中连个构造器都懒得写,创建子类对象都是无参构造,哦,就算是子类有系统默认给的无参构造,可以初始化子类内容,那你父类内容怎么初始化捏?🤗
不愧是p小将,6。但是,风度翩翩的人,你也先别急。
下面,up直接将继承关系中构造器使用的几个结论告诉大家。然后再一一进行代码演示,保证你看完就🆗,正片如下 :
2.结论 :
结论① :
创建子类对象时,优先调用父类的构造器。这句话对应了我们的“先初始化父类内容,后初始化子类内容”。但是,这究竟是如何做到的?请看结论②
结论② :
子类构造器的第一行,默认隐含语句super(); 用于调用父类的默认无参构造。
结论③ :
根据我们在封装篇对构造器的理解,父类默认含有无参构造。但是,如果你在父类中只定义了有参构造,父类就没有了无参构造,这时在子类构造器中,可以用super(参数) 的形式来访问父类的某个指定的带参构造。你可能会问:为什么要这么做?
因为方才我们也说了,继承关系中,对象的初始化顺序为 : 先初始化父类内容,后初始化子类内容,是!没错!子类构造器第一行默认的" super(); " 可以调用父类的无参构造,从而达到初始化父类内容的效果。但是现在,父类中没有无参构造!如果你不调用父类有参构造来初始化父类内容,你就没做到继承关系中对象的初始化顺序,就要报错!如下图所示 :
所以,这么做是迫不得已,你不这么干,是不会编译通过的!。对了,up在这里还要再次强调一点:此处父类是用JavaBean标准敲的,子类只能通过继承到的公有的setter,getter方法来修改或获取父类的私有属性的值。因此,从本质上来说,除子类特有属性外,子类访问的属性根本上都是父类的属性,只不过借你一用罢了。而且,大家想想带参构造的存在的意义是什么?当然是为了在对属性进行构造器初始化时,就将属性的值修改为我们想要的值,从而省去了无参构造初始化之后,再次调用setter方法修改属性值的步骤。之前我们在非继承关系中,使用带参构造创建子类对象时,本质和setter方法一样,即通过this语句将形参赋值给本类的属性。然而在继承关系中,因为子类要访问父类的属性,因此比平时多了一个步骤,多在了子类构造器中的"super(参数);"语句上,super语句将形参赋值给了父类的构造器。因此,若是通过含有super(参数);语句的子类带参构造来创建对象,参数的传递途径是 : 调用子类构造时传入的形参——> super(参数)对应父类构造器中的形参——> 赋值给父类构造器中的this.xxx(即赋值给父类的属性)。
为什么up要在这里好端端的要强调这一点?
大家想想,super()也好,super(参数)也好,调用父类构造器的目的是什么?是为了优先初始化父类内容,是为了满足继承中对象的初始化条件,满足继承设计的基本思想。对于子类无参构造来说,本来呢,子类无参构造器第一行隐含语句super(),无参对应无参,明明白白,整整齐齐,服服帖帖,按理说初始化之后,父类的属性的值,仍然是属性对应数据类型的默认值。但是,在上述情况——父类仅有带参构造的情况下,我们在子类的无参构造中也必须调用super(参数)了!大家有没有想过后果是什么?原先,无参对应无参,调用super() 初始化之后,再通过setter方法修改父类属性的值,现在,tmd一个super(参数)的调用就解决了,我一个无参构造啊,居然达到了有参构造的效果😎!
同理,如果父类仅有空参构造,没有带参构造,那子类的带参构造就无实际意义了,只能当空参构造用。当然!这里强调的内容既是铺垫,也是总结,现在初看可能感觉略显晦涩,没关系!马上下面就是代码演示了,大家可以在代码演示后,再次翻回来看看,绝对柳暗花明!
结论④ :
看过封装篇的小伙伴儿应该记得,我们再讲到this关键字时,提到了一嘴this()调用本类构造器,我们下面就会进行代码演示。注意 : this() 和 super() 不可以同时出现在同一构造器中。因为——this() 和 super() 语句都必须位于构造器的首句!
3.代码演示 :
Δ前言 :
up将代码演示部分分为六(1+4+1)部分!首先,会给大家演示一下所谓创建子类对象时优先调用父类构造器,以及子类构造方法第一行默认隐含语句super();,这是第一部分。然后,我们知道,继承关系中子父类构造器的定义情况,无非四种——(父类有参无参都有;父类有参有无参无;父类有参无无参有;父类有参无参都无),第二 ~ 四部分up会对这四种情况进行一一演示,并进行规律的总结。最后,就是this() 和 super()的使用问题,相比起来这就不算一个重点了,这是第三部分。
PS : up以Person类作为演示父类,以Worker类作为演示子类,以TestConstructor类作为演示测试类。以下代码演示均围绕这三个类展开。
①结论1,2演示 :
我们仍以JavaBean标准来敲父类,在父类的无参构造和有参构造中,分别增加一条输出语句,使得构造器被调用时可以给出提示。对于子类Worker类,我们先什么都不写。没错,让它空空如也😎。然后我们在TestConstructor类中,通过系统默认给出的无参构造来创建Worker类对象,看看运行结果如何。
Person类代码如下 :
package knowledge.succeed.aboutconstructor;
/**
* 父类 : Person类
*/
public class Person {
//成员变量
private String name;
private int age;
//无参构造
public Person() {
System.out.println("这句话输出,说明Person父类的空参构造被调用");
}
//有参构造
public Person(String name, int age) {
this.name = name;
this.age = age;
System.out.println("这句话输出,说明Person父类的带参构造被调用,且" + "name = " + name);
}
//getter,setter方法
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
Worker类代码如下 :
package knowledge.succeed.aboutconstructor;
/* ΔPS :
因为父类中的name属性和age属性都是私有属性,
所以子类继承后,并没有这两个属性,但拥有有父类的可以访问这两个属性的公有方法,
因此,子类对象通过setter,getter方法交互的属性,本质都是父类的属性。
*/
/** 子类 : Worker类 */
public class Worker extends Person{
}
TestConstrutor类代码如下 :
package knowledge.succeed.aboutconstructor;
//测试类 : TestConstructor类
public class TestConstructor {
public static void main(String[] args) {
Worker worker1 = new Worker();
}
}
运行结果 :
可以看到,明明我们用的是子类的默认无参,创建的也是子类对象,却显式父类的无参构造被成功调用了。因此我们可以证明结论①——创建子类对象时,优先调用父类的构造器。以及结论②——子类构造器的第一行,默认隐含语句super(); 。
当然,使用默认的无参构造显然对比不明显!
接下来,我们在子类Worker类中,自己定义一个Worker类的无参构造,并且要求同父类一样,再被调用时给出提示。Person类和TestConstructor类代码不变。Worker类代码如下 :
package knowledge.succeed.aboutconstructor;
/** 子类 : Worker类 */
public class Worker extends Person{
public Worker() {
//super(); //子类构造器首句默认隐含super();语句,因此可写可不写。
System.out.println("这句话输出,说明Worker子类的无参构造被调用");
}
}
运行效果如下 :
看这次经过对比以后,是不是明显多了——父类无参构造中的输出语句优先输出,说明创建子类对象时,优先调用父类构造器。而我们在子类构造器中并没有主动去写有效的super();语句,这说明子类构造器的第一行隐含语句super();。
这里还要提前和大家强调一下,只要是在子类构造器中使用了"super();"语句,该语句必须位于构造器首句,否则报错!如下图所示 :
②当父类有参无参都有,子类构造器的使用情况 :
父类有参无参都有,往往是最理想的情况。因为这个时候可以无参对无参,有参对有参。即,在子类无参构造中通过“super();” 调用父类的无参构造;在子类有参构造中通过“super(参数);” 调用父类的有参构造。
父类Person类代码仍旧不变,子类Worker类代码如下 :
package knowledge.succeed.aboutconstructor;
/** 子类 : Worker类 */
public class Worker extends Person{
public Worker() {
super(); //无参对无参
System.out.println("这句话输出,说明Worker子类的无参构造被调用");
}
public Worker(String name, int age) {
super(name, age); //有参对有参
System.out.println("这句话输出,说明Worker子类的有参构造被调用");
}
}
TestConstructor类代码如下 :
package knowledge.succeed.aboutconstructor;
// 测试类,用来演示构造方法的调用的
public class TestConstructor {
public static void main(String[] args) {
Worker worker1 = new Worker();
worker1.setName("Sun");
System.out.println();
Worker worker2 = new Worker("Moon", 11);
System.out.println();
System.out.println("worker1'name = " + worker1.getName());
System.out.println("worker2'name = " + worker2.getName());
}
}
运行结果 :
当然,以上是最常见,最舒服的形式。但是大家知道,现实往往是荒诞的😅!在父类有参无参都有的情况下,你也可以选择在子类空参中通过 super(参数) 来调用父类带参,以及在子类带参中通过super调用父类空参。这么做会造成一个效果 : 调用子类空参最终改变了父类的属性值,空参达到了带参的效果(是不是我们上面强调过的);而子类带参最终无法改变父类的属性值,带参没有了实际意义,带参达到了空参的效果。斯国一!
但是有一点需要注意,子类无参构造调用super(参数)时,因为子类构造的参数列表为空,因此,此时的super(参数)中传入的参数必须为常量,而不能传入变量。
Person类代码仍旧不变!Worker类代码如下 :
package knowledge.succeed.aboutconstructor;
/** 子类 : Worker类 */
public class Worker extends Person{
public Worker() {
super("我是谁?", 30); //无参对有参了
System.out.println("这句话输出,说明Worker子类的无参构造被调用");
}
public Worker(String name, int age) {
super(); //有参对无参了
System.out.println("这句话输出,说明Worker子类的有参构造被调用");
}
}
TestConstructor类代码如下:
package knowledge.succeed.aboutconstructor;
// 测试类,用来演示构造方法的调用的
public class TestConstructor {
public static void main(String[] args) {
Worker worker1 = new Worker();
System.out.println();
System.out.println("调用setter方法之前,worker1'name = " + worker1.getName());
worker1.setName("Sun");
System.out.println();
Worker worker2 = new Worker("Moon", 11);
System.out.println();
System.out.println("worker1'name = " + worker1.getName());
System.out.println("worker2'name = " + worker2.getName());
}
}
运行结果 :
结果符合预期——子类无参构造中super传入的"我是谁?"成功赋值给了父类私有属性name;而子类有参构造中,构造器参数列表的形参无法传递给父类构造器,成了摆设。
③当父类有参有无参无,子类构造器的使用情况 :
这**不就是结论③一开篇便提出的情况嘛!(大家看我的博客会发现——在分享某个东西时,我喜欢先让大家了解个大概,即剧透一下,后面再详细解释。我觉得这么挺好,有助于大家加深理解!)
我们来分析一下这种情况: 首先,父类定义了有参构造,系统不再提供默认的无参构造,因此父类仅有参构造可调用。什么意思呢?意思就是你子类构造器第一行不能再使用super()来调用父类无参构造了,原因也很简单——人家父类现在就没得无参,你上哪儿调用啊?
那我们怎么办?没无参你调用有参不就完了么!多好理解的事儿!有啥用啥,物尽其用!
好滴,我们先把父类的空参构造给注释掉,其他代码不变,如下图所示 :
如果注释掉父类的无参构造后,有头铁娃依然坚持要在子类构造中使用super (),就会报错,如下图所示 :
因此,这时候我们只能使用有参,不然你编译就不通过呀。
Worker类代码如下 :
package knowledge.succeed.aboutconstructor;
/** 子类 : Worker类 */
public class Worker extends Person{
public Worker() {
super("工人一号", 100); //无参构造形参列表为空,super() 中只能直接传入常量
System.out.println("这句话输出,说明Worker子类的无参构造被调用");
}
public Worker(String name, int age) {
super(name, age); //有参构造形参列表中就有传入的name和age,可以传入变量。
System.out.println("这句话输出,说明Worker子类的有参构造被调用");
}
}
TestConstuctor类代码如下 :
package knowledge.succeed.aboutconstructor;
// 测试类,用来演示构造方法的调用的
public class TestConstructor {
public static void main(String[] args) {
Worker worker1 = new Worker();
System.out.println();
Worker worker2 = new Worker("Moon", 11);
System.out.println();
System.out.println("worker1'name = " + worker1.getName());
System.out.println("worker2'name = " + worker2.getName());
}
}
运行结果 :
编辑
④当父类有参无无参有,子类构造器的使用情况 :
其实吧,这个我们在演示②中已经演示过🌶。无妨,大家现在已经差不多知道个回事儿了。父类仅含无参,那我们子类构造中就只能调用super(),此时子类带参构造无实际意义,因为你无法通过super(参数)来传递形参。
这次,我们将Person类中的无参构造恢复,把带参构造注释掉,其他代码不变。如下图所示 :
Worker类代码如下 :
package knowledge.succeed.aboutconstructor;
/** 子类 : Worker类 */
public class Worker extends Person{
public Worker() {
super(); //无参对无参
System.out.println("这句话输出,说明Worker子类的无参构造被调用");
}
public Worker(String name, int age) {
super(); //有参此时无实际意义
System.out.println("这句话输出,说明Worker子类的有参构造被调用");
}
}
TestConstructor类代码如下 :
package knowledge.succeed.aboutconstructor;
// 测试类,用来演示构造方法的调用的
public class TestConstructor {
public static void main(String[] args) {
Worker worker1 = new Worker();
worker1.setName("Sun");
System.out.println();
Worker worker2 = new Worker("Moon", 11);
System.out.println();
System.out.println("worker1'name = " + worker1.getName());
System.out.println("worker2'name = " + worker2.getName());
}
}
运行结果 :
通过运行结果可以看出,子类有参构造创建的对象,输出name属性的值却为默认值null,所以我们才说,有参构造此时无实际意义。
⑤当父类有参无参都无,子类构造器的使用情况 :
父类有参无参都没有,实际和上一种情况——父类有参无无参有一致。只不过这里的无参不再是自定义的无参构造,而是系统默认的了,因此创建子类对象时不再有关于父类构造器的提示语句了。
将Person类中的两个构造器都给注释掉,如下图所示 :
Worker类代码不变,TestConstructor类代码也不变🤡。
运行结果如下 :
Δ人话(②~⑤总结):
根据父类构造器的四种定义情况的演示,我们可以总结出以下规律 :
1° 父类有参√ 无参√,子类无参随便,有参必须super(对应参数),否则无实际意义
2° 父类有参√ 无参×,子类无参有参都只能super(参数)
3° 父类有参× 无参√,子类只能无参,有参无实际意义
4° 父类有参× 无参×,(默认含无参,同上一种情况)
⑥关于this(); 语句对本类构造器的调用(了解即可) :
就像super();或super(参数);语句可以调用父类构造器,this();或this(参数);语句用于调用本类构造器。使用时,需要注意两点 :
1°同super语句一样,this语句也必须位于构造器的第一行。
2°this()和seper()不能同时存在于同一构造器中。(原因就是它们都必须在构造器第一行)
我们先恢复父类Person类的两个构造,如下如图所示 :
接着,我们在子类Worker类中定义一个子类特有属性hobby,表示工人的爱好。注意!这时候如果我们想利用带参构造初始化子类对象,就需要传入三个参数,而且其中两个参数最终赋值给了父类的成员变量,还有一个参数则最终赋值给了子类成员变量hobby。其实这时候我们只要在子类定义一个三个形参的有参构造,然后先通过super(参数);来调用父类带参构造,完成对父类属性的初始化,然后再补充上一句this.hobby = hobby; 完成对子类特有属性的初始化,也就算大功告成了。
但是这毕竟是this调用本类构造的演示😂,还是要用一下this()语句的。方法也很简单 :子类要定义两个带参构造,其中一个构造器有两个形参,调用super(参数)完成对父类对象的初始化,另一个构造器有三个形参,先通过this(参数) 调用本类带参构造(即两个形参的那个带参构造),完成对父类属性的初始化。再通过this语句完成对本类特有属性hobby的初始化。
Person类代码如下 :
package knowledge.succeed.aboutconstructor;
/**
* 父类 : Person类
*/
public class Person {
//成员变量
private String name;
private int age;
//无参构造
public Person() {
System.out.println("这句话输出,说明Person父类的空参构造被调用");
}
// 有参构造
public Person(String name, int age) {
this.name = name;
this.age = age;
System.out.println("这句话输出,说明Person父类的带参构造被调用,且" + "name = " + name);
}
//getter,setter方法
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
Worker类代码如下 :
package knowledge.succeed.aboutconstructor;
/** 子类 : Worker类 */
public class Worker extends Person{
private String hobby;
public Worker() {
//super(); //默认隐含super();
System.out.println("这句话输出,说明Worker子类的无参构造被调用");
}
public Worker(String name, int age) {
super(name, age);
System.out.println("这句话输出,说明Worker子类的有参构造被调用");
}
public Worker(String name, int age, String hobby) {
this(name, age);
//super(name, age);
this.hobby = hobby;
}
public String getHobby() {
return hobby;
}
public void setHobby(String hobby) {
this.hobby = hobby;
}
}
/*
关于this() :
与super()调用父类构造器不同,this()可以调用本类的构造器,
但是this() 和 super() 在同一构造器中只能同时出现一个,且必须置于构造器首句。
*/
TestConstructor类代码如下 :
package knowledge.succeed.aboutconstructor;
// 测试类,用来演示构造方法的调用的
public class TestConstructor {
public static void main(String[] args) {
Worker worker1 = new Worker();
worker1.setName("Sun");
System.out.println();
Worker worker2 = new Worker("Moon", 11);
System.out.println();
Worker worker3 = new Worker("Star", 88, "木大木大木大木大木大");
System.out.println("----------------------------");
System.out.println("worker1'name = " + worker1.getName());
System.out.println("worker2'name = " + worker2.getName());
System.out.println("worker3'name = " + worker3.getName());
System.out.println("Worker3'hobby = " + worker3.getHobby());
//PS : 创建子类对象时,实际内部相当于这么写(C语言):
//Worker * pWorker = (Worker * ) malloc(sizeof(Worker));
}
}
运行结果如下 :
六、Java中继承的特点 :
1.Java仅支持单继承。
单继承指每个类最多有且只能继承一个类,不能同时继承多个类。同时继承多个类会报错,举个栗子吧:假设up同时拥有华为,苹果和Oppo手机,定义Huawei类,Apple类,以及Oppo类,再定义Up类去同时继承这三各类,如下GIF图所示 :
报错图如下所示 :
但是,Java支持多层继承。 多层继承指当父类衍生出一个子类后,该子类可以作为父类继续衍生出它的子类,如此往复,循环不止。比如:假设up有一部Vivo手机。定义Phone类作为父类,衍生出Vivo子类,而Vivo子类可以继续衍生出Up子类,如下图GIF图所示 :
2.父类私有成语子类无法继承。
这就不用说了吧,什么是继承和继承的使用场景我们演示过很多次🌶。发生继承关系后,子类拥有了父类的非私有成员。
3.父类构造器子类不能继承。
这也挺好理解的 : 构造器用于初始化对象。父类构造器用于初始化父类对象,子类构造器用于初始化子类对象。你就是继承了父类构造器你也没个J8用啊😅。只不过为了初始化子类对象中的父类内容,我们才在子类构造器中通过super来调用父类构造器。
4.继承关系往往体现了" is a " 的关系。
只有在子类符合“ is a ”父类的情况下才使用继承,其他情况下不建议使用。像up之前举过的栗子:grape(葡萄)满足“ is a ”fruit(水果);Dog,Pig,Cat等符合“is a” animal,等等均是如此。
七、继承的本质——java 继承内存图解(重要):
仅仅会使用继承是远远不够的。我们还必须了解到在使用继承时,jvm内存中究竟发生了什么。up之前写过的一篇博文详细介绍了Java使用继承时,在内存中实际发生了什么,有助于初学者加深对Java继承以及内存模型的印象,有兴趣的小伙伴儿可以点击链接查看,链接如下 : https://blog.csdn.net/TYRA9/article/details/128729074?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522167625371116800211569247%2522%252C%2522scm%2522%253A%252220140713.130102334.pc%255Fblog.%2522%257D&request_id=167625371116800211569247&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~blog~first_rank_ecpm_v1~rank_v31_ecpm-1-128729074-null-null.blog_rank_default&utm_term=%E7%BB%A7%E6%89%BF&spm=1018.2226.3001.4450
八、方法重写 :
1.定义 :
如果子类中定义了一个返回值类型、方法名,参数列表都与父类中某个方法相同的方法,只是方法体中的语句不同,我们称为:子类重写了父类的某个方法。方法重写也叫方法的复写、覆盖。
那我们什么时候要用到方法重写呢? 其实就是我们在之前定义重名方法时说过的——当父类方法不满足现实需求,需要拓展父类方法的功能时,就要重写父类方法,以重新实现父类功能。
2.关键 :
①想达到方法重写,必须满足返回值类型、方法名,参数列表都相同!即——外壳不变,内部重写。(实际子类返回值类型也可以是父类返回值类型的子类)
②子类的重写方法可以用@Override注解来标注。(up以后会单独出博文讲解Java注解)
3.注意事项 :
①父类私有方法不能被重写。
②子类方法访问权限不能小于父类方法,即访问权限 : 子类 ≥ 父类。(本文后面我们就会讲到Java四大访问修饰符)
③子类不能比父类抛出更大的异常。因为看这篇文章的估计多数是初学者,还不知道什么是异常,这里推荐去看up写得Java万字详解系列之异常篇。
4.方法重写和方法重载的区别 :
这是初学者容易混淆的地方。其实压根儿没那么多事。直接给大家一张图,一目了然:
如果要再解释的通俗易懂一些的话,你可以理解为: up现在手里有一张弓剑,就随便一张弓吧,比如下面这张破弓 :
那么,方法重写就相当于——本来我每次射一支普通箭,现在我给箭头上装了微型炸药,使得我可以每次射一支爆破箭。而方法重载就相当于还是原来那普通箭,只不过我每次可以射出多支箭了。
其实你就把二者的定义都记住不就⭐了么,也没多少😋。
5.代码演示 :
①方法重写演示 :
我们定义父类Phone类,仍然以JavaBean标准来敲,并在其中定义一个call方法,代表打电话。定义子类为Vivo类来继承Phone类。你可能会有疑惑——为什么是Vivo?一张图告诉你为什么:
没错,我们要在子类中重写父类的call方法,来给坤坤打电话🤩,当然就要用坤坤同款啦😋。
Person类代码如下 :
package knowledge.succeed.re_method;
public class Phone {
//成员变量
private String brand; //手机品牌
private double price; //手机发售价格
//构造器
public Phone() {
}
public Phone(String brand, double price) {
this.brand = brand;
this.price = price;
}
//getter,setter方法
public String getBrand() {
return brand;
}
public void setBrand(String brand) {
this.brand = brand;
}
public double getPrice() {
return price;
}
public void setPrice(double price) {
this.price = price;
}
//成员方法
public void call() {
System.out.println("手机都可以打电话📞.");
}
}
Vivo类代码如下 :
package knowledge.succeed.re_method;
public class Vivo extends Phone {
@Override
public void call() { //重写了父类的call方法
super.call();
System.out.println("🐔坤哥在吗?");
}
}
TestOverride类代码如下 :
package knowledge.succeed.re_method;
public class TestOverride {
public static void main(String[] args) {
Vivo vivo = new Vivo();
vivo.call();
}
}
运行结果如下 :
②@Override注解演示 :
@Override 注解可以强制一个子类必须重写父类方法或者实现接口的方法,否则就会编译出错。
现在,我们修改子类Vivio类中call方法的形参列表,使其不满足重写父类call方法的条件,如下图所示 :
这时候,如果你给子类call方法添加@Override注解,IDEA会报错。如下图所示 :
③方法重写的注意事项演示 :
我们在Phone类新建一个私有的music方法,如下图所示 :
然后我们在Vivo类试图重写music方法,如下图所示 :
大家注意看子类中的music方法,与父类music方法相比,满足返回值类型,方法名,参数列表都相同的条件,而且子类使用public修饰,毫无疑问public的访问权限 > private的访问权限,因此访问权限也满足重写的条件。但是奇怪的是,方法旁边居然没有给出提示!这时有人可能会有疑惑:什么提示?要知道我们使用的可是目前智能程度T0级别的集成编辑器,子类重写父类方法,IDEA会在方法旁边给出提示,就拿我们在演示①中重写的call方法为栗,如下图所示:
而且,你将鼠标悬浮在提示图标上,还会跳出提示窗口,如下GIF所示 :
现在子父类的music方法压根没有任何提示,这说明子类中的music方法和父类私有的music方法没有重写关系。这相当于子类重新定义了一个它特有的方法。其实也好理解,你子类都不能通过super关键字访问父类私有方法,怎么重写?
再来说说修饰符的问题,子类的修饰符要求访问权限 ≥ 父类的,否则报错。我们仍以music方法为栗,将父类music方法改为public修饰符,子类music方法改为默认修饰符(即什么都不写)。如下图所示 :
不出所料,子类music方法立刻报错,如下图所示 :
九、Java四大访问权限修饰符 :
1.介绍 :
访问权限修饰符指的是用来修饰成员变量,成员方法和类,来确定它们的访问权限的修饰符。Java四大访问权限修饰符分别是private、默认、protected,public。
private和public在封装篇已经讲过了,这里我们来简单复习一下 : private修饰的成员只能在本类使用,而public修饰的成员可以被所有类使用。重点说一下默认和protected。
默认修饰符,指的就是没写修饰符。默认修饰符修饰的成员允许在本包下使用。
protected修饰符修饰的成员除了可以在本包下使用,在其子类中也可以使用。
2.权限 :
四大修饰符按照根据访问权限从小到大的原则依次是 : private < 默认 < protected < public。给大家来张图更清晰直观,如下图所示 :
3.演示 :
①private修饰符演示 :
新建一个Private_demo类,并在同一包下创建TestAccess类作为演示类。如下图所示 :
Private_demo类代码如下 :
package knowledge.succeed.access;
public class Private_demo {
private int temp_1 = 11;
public void printTemp() {
temp_1 = 5;
System.out.println("temp_1 = " + temp_1);
}
}
TestAccess类代码如下 :
package knowledge.succeed.access;
public class TestAccess {
public static void main(String[] args) {
Private_demo private_demo = new Private_demo();
//private_demo.temp_1 = 3; 直接调用temp_1变量会报错。
}
}
在TestAccess类中直接调用temp_1变量, IDEA报错,如下图所示 :
在Private_demo的本类方法printTemp中直接调用temp_1变量是可行的,不会报错。接着我们在TestAccess类中调用此方法,可以成功输出temp_1变量。
TestAccess类代码如下 :
package knowledge.succeed.access;
public class TestAccess {
public static void main(String[] args) {
Private_demo private_demo = new Private_demo();
private_demo.printTemp();
}
}
运行结果 :
②默认修饰符 :
默认比private的访问权限高一级,本包下的类都可以访问默认修饰符修饰的成员。
如下图所示 :
Default类代码如下 :
package knowledge.succeed.access;
public class Default_demo {
int temp_2 = 11;
}
TestAccess类代码如下 :
package knowledge.succeed.access;
public class TestAccess {
public static void main(String[] args) {
Default_demo default_demo = new Default_demo();
default_demo.temp_2 = 1111;
System.out.println("temp_2 = " + default_demo.temp_2);
}
}
我们在本包下的TestAccess类中,可以直接调用temp_2变量,也可以直接输出它。
运行结果:
但是,当我们在其他包下的类中调用temp_2变量时,IDEA仍会报错,如下图所示:
IDEA会提示我们,不能跳出本包下访问默认修饰符修饰的成员。
③protected修饰符 :
protected修饰符则比默认修饰符还高一级。使得——即使是跳出本包外,只要是其子类仍可以直接调用protected修饰的成员。
我们在access包下的Protected_demo类中定义temp_3变量,然后在access_2包下的测试类中尝试调用temp_3变量(注意,测试类要继承Protected_demo类),如下图所示 :
Protected_demo类代码如下:
package knowledge.succeed.access;
public class Protected_demo {
protected int temp_3 = 1000;
}
TestAccess_2类代码如下 :
package knowledge.succeed.access_2;
import knowledge.succeed.access.Protected_demo;
public class TestAccess_2 extends Protected_demo {
public static void main(String[] args) {
TestAccess_2 testAccess_2 = new TestAccess_2();
testAccess_2.temp_3 = 222;
System.out.println("temp_3 = " + testAccess_2.temp_3);
}
}
运行结果 :
④public修饰符 :
public修饰的成员,在任何类中都可以使用。在access包下新建Public_demo类,并在其中建立temp_4变量;然后在access_2包下的测试类中调用temp_4变量。注意:测试类既非本包下的类,又非Public_demo的子类,如下图所示 :
Public_demo类代码如下 :
package knowledge.succeed.access;
public class Public_demo {
public int temp_4 = 666;
}
TestAccess_2类代码如下 :
package knowledge.succeed.access_2;
import knowledge.succeed.access.Public_demo;
public class TestAccess_2 {
public static void main(String[] args) {
Public_demo public_demo = new Public_demo();
public_demo.temp_4 = 11;
System.out.println("temp_4 = " + public_demo.temp_4);
}
}
运行结果 :
4.总结 :
①四大修饰符总结 :
private修饰符 : 强调自己使用,只能在本类下使用。
默认 : 强调本包使用,只能在本类,同包下的类中使用。
protected : 强调子类使用,只能在本类,同包下的类,以及其子类中使用。
public : 强调公开使用,没有只能,谁都可以用。
②关于修饰类 :
修饰类时,只能使用默认修饰符和public修饰符。(下面的延申中会更详细讲解)
5.延申(关于类和源文件的关系) :
①一个Java源文件中可以定义多个类,源文件的基本组成部分是类。
②源文件中定义的类,最多只能有一个类被public修饰,其他类的个数不限。
③如果源文件中有被public修饰的类,那么源文件名必须与该类类名保持一致。
④如果源文件中没有被public修饰的类,那么源文件名只要符合命名规范就可以。
⑤main函数不一定非得写在public修饰的类中,也可以将main函数写在非public修饰的类中,然后通过指定运行非public类,这样入口方法就是非public类的main方法。
十、总结 :
🆗,费了这么大功夫总算是把面向对象三大特性的第二关BOSS给干掉了😄。回顾一下,我们从继承的引入开始,到继承的举栗;成员变量、成员方法,以及构造器在继承关系中的使用;以及继承的内存图解等等,中间这几个标注了"重点"字样的都做了很详细的讲解,最后我们又说到了方法重写和Java四大访问修饰符。
在Java面向对象三大特性里有句话说的好:封装是继承的前提,继承是多态的前提。相信之后的多态篇,我们能完美结局。好滴,感谢阅读!
System.out.println("END------------------------------------------------------------");
- 点赞
- 收藏
- 关注作者
评论(0)