JavaSE 基础语法 - 继承 - ②
接上节 JavaSE 基础语法 - 继承 - ① 的内容。
子类不仅可以拥有父类的成员变量,也可以拥有父类的方法。
那么新的问题来了。
如果:子类自己也有:String name ,这时候怎么办?如果子类自己也有:eat(),又怎么办?
如果子类构造对象的时候,需要先初始化父类,应该怎么办?
Java 为了解决这些问题,引入了:super关键字。
8、为什么需要 super
第一次学习继承的时候,应该都有一个疑惑:子类已经继承了父类,为什么还需要 super?
其实,super的出现并不是为了增加一种新的语法,而是为了解决继承过程中"成员冲突"和"父类初始化"的问题。
8.1 子类为什么需要访问父类
假设有一个 Person 类:
classPerson{Stringname="张三";publicvoideat(){System.out.println("Person 正在吃饭");}}定义 Student:
classStudentextendsPerson{publicvoidstudy(){System.out.println("学习");}}测试:
Studentstu=newStudent();System.out.println(stu.name);stu.eat();输出:
张三Person正在吃饭这是没有问题的。
因为 Student 没有 name ,也没有 eat() 。
Java自然向父类查找。
但是,如果 Student 自己也定义:
classStudentextendsPerson{Stringname="李四";// 自己也定义有了name}测试:
Studentstu=newStudent();System.out.println(stu.name);输出:
李四为什么?因为:成员变量遵循就近原则。
那么问题来了,如果我就是想访问 Person 里面那个 name ,怎么办?
Java于是设计了super,专门表示父类对象部分。
8.2 为什么会出现成员重名
再来看一个例子。
Person:
classPerson{Stringname="张三";}Student:
classStudentextendsPerson{Stringname="李四";}对象实际上应该长这样:
很多同学看到这里会问:一个对象里面怎么会有两个 name?答案是:可以。
因为父类部分属于 Person ,子类部分属于 Student 。
它们分别存储。只是访问的时候遵循:先找子类,再找父类。
因此 stu.name 得到李四,而父类那个张三仍然存在,只是访问不到,所以Java设计super。
表示:访问父类中的 name。
8.3 super 诞生的原因
现在我们已经知道对象实际上包含:父类部分 + 子类部分。
因此Java需要一种方式告诉 JVM:不要找子类,请直接访问父类。
于是有了super,可以把 super 理解成:父类对象部分的引用。
例如:
super.name 表示 Person中的name,不是 Student自己的name。
同样 super.eat() 表示调用父类的方法。而不是子类自己的方法。
另外,super() 表示调用父类构造方法。
所以 super 实际上有三种用途:
super.成员变量
super.成员方法
super()
9、super 关键字
9.1 什么是 super
super是 Java 提供的一个关键字。它表示:当前对象中的父类部分。
很多人会把 super 理解成父类对象,这种说法并不准确。
因为程序中并没有重新创建一个父类对象。
真正存在的是只有一个对象,这个对象里面存着父类部分还有子类部分。
所以,更准确的理解:super 用来引用当前对象中的父类部分。
例如:
classPerson{Stringname="张三";}classStudentextendsPerson{Stringname="李四";publicvoidprint(){System.out.println(name);System.out.println(super.name);}}输出:
李四 张三说明:name 访问子类,而 super.name 访问的是父类。
9.2 super 访问成员变量
这是 super 最简单的用法。
例如:
classPerson{Stringname="张三";}classStudentextendsPerson{Stringname="李四";publicvoidshow(){System.out.println(name);System.out.println(super.name);}}测试:
Studentstu=newStudent();stu.show();输出:
李四 张三因此:super 可以解决成员变量隐藏的问题。
9.3 super 访问成员方法
不仅成员变量可以重名,成员方法也可以。
例如:
classPerson{publicvoideat(){System.out.println("Person 吃饭");}}classStudentextendsPerson{@Overridepublicvoideat(){System.out.println("Student 吃饭");}publicvoidtest(){eat();super.eat();}}测试:
Studentstu=newStudent();stu.test();输出:
Student吃饭Person吃饭解释:
- eat():调用当前类(Student)的实现。
- super.eat():调用父类(Person)的实现。
这种写法在方法重写中非常常见,例如子类希望在保留父类逻辑的基础上扩展功能:
@Overridepublicvoideat(){super.eat();// 执行父类逻辑System.out.println("饭后散步");}9.4 super 调用父类构造方法
除了访问成员变量和成员方法,super 还有第三种用途:调用父类构造方法
例如:
classPerson{publicPerson(){System.out.println("Person 构造方法");}}classStudentextendsPerson{publicStudent(){super();System.out.println("Student 构造方法");}}测试:
newStudent();输出:
Person构造方法Student构造方法可以看到创建子类对象时:父类构造方法一定先执行。
为什么?因为:子类对象中包含父类部分。
只有先完成父类部分初始化,才能继续初始化子类部分。
9.5 super 使用注意事项
使用 super 时,需要记住以下几点:
1、super 可以访问父类成员变量、成员方法和构造方法
super.name;super.eat();super();2、super() 只能出现在构造方法中
错误示例:
publicvoidtest(){// 这是一个普通的的方法,不是构造方法super();}编译错误。
3、super() 必须是构造方法中的第一条语句
正确:
publicStudent(){super();System.out.println("Student");}错误:
publicStudent(){System.out.println("Hello");super();}编译错误。
4、this() 与 super() 不能同时出现
例如:
publicStudent(){this("Tom");super();}编译错误。
原因是:
- this() 用于调用本类其他构造方法。
- super() 用于调用父类构造方法。
- 两者都必须位于构造方法第一行,因此不能同时出现。
10、this 与 super
学习完 super 之后,都会有一个疑问:this 和 super 到底有什么区别?
例如:
this.name和super.name到底有什么不同?
再比如:
this();和super();为什么都必须写在构造方法第一行?
10.1 什么是 this
在前面,我们已经讲过:this表示当前对象的引用。
例如:
classStudent{Stringname="张三";publicvoidprint(){System.out.println(this.name);}}这里的this.name表示当前Student对象中的name
10.2 什么是 super
而super表示:当前对象中的父类部分。
例如:
classPerson{Stringname="父类";}classStudentextendsPerson{Stringname="子类";}this.name得到的是子类,而super.name得到父类
10.3 this 与 super 相同点
两者都有引用作用。
都不能脱离对象。
例如下面都可以访问成员变量:
this.name;super.name;都可以访问成员方法:
this.eat();super.eat();都可以调用构造方法:
this();super();因此它们都属于Java提供的特殊引用。
10.4 this 与 super 不同点
虽然写法非常相似。
但是作用完全不同。
this表示当前对象。
例如:
this.name的查找,会先在子类中查找,子类没有才去父类。
super.name的查找,会直接在父类中查找,不会在子类。
因此两者访问对象不同。
10.5 this 与 super 对比表
| 对比项 | this | super |
|---|---|---|
| 表示对象 | 当前对象 | 当前对象中的父类部分 |
| 访问成员变量 | ✔ | ✔ |
| 访问成员方法 | ✔ | ✔ |
| 调用构造方法 | ✔ | ✔ |
| 调用位置 | 普通方法、构造方法 | 普通方法、构造方法(super()仅构造方法) |
| 是否必须第一行 | this()必须 | super()必须 |
| 是否可同时出现 | ✘ | ✘ |
注意:
this 和 super(成员访问)可以同时出现:
publicvoidtest(){System.out.println(this.name);System.out.println(super.name);// 这样是可以同时使用}但 this() 和 super() 不能同时出现,因为两者都必须位于构造方法第一行。
10.6 使用场景
场景一:访问当前对象成员
this.name场景二:访问父类成员
super.name场景三:调用当前类其他构造方法
this("张三");场景四:调用父类构造方法
super();这是最常见也是最重要的使用方式。
11、protected 访问权限
之前我们介绍了四种访问权限:
- public
- protected
- 默认(default)
- private
但是当时并没有详细介绍protected。
因为它真正发挥作用是在继承。
11.1 为什么需要 protected
先来看一个例子。
Person:
classPerson{privateStringname;}Student:
classStudentextendsPerson{publicvoidshow(){System.out.println(name);}}编译报错,因为 private 只能本类访问。
如果改成protected String name;
Student 立即可以访问。
因为 protected 就是专门为继承设计。
11.2 protected 访问规则
protected 允许下面两种情况:
同一个包
不同包
例如:
1、都在同一个包中,可以访问;
2、分别在不同包,但是需要继承关系才可以访问;
3、如果分别在不同包,也没有继承关系的话,就不能访问。
11.3 同包访问
例如:
protectedintage;同一个包任何类都可以访问。
例如:
Studentstu=newStudent();System.out.println(stu.age);合法。
11.4 不同包继承访问
例如:
Person 位于 com.person,
Student 位于 com.student,
只要:class Student extends Person
仍然可以访问:age
这也是 protected 最大的作用。
11.5 四种访问权限总结
这是Java面试经常考的。
| 修饰符 | 本类 | 同包 | 不同包子类 | 其他包 |
|---|---|---|---|---|
| public | ✔ | ✔ | ✔ | ✔ |
| protected | ✔ | ✔ | ✔ | ✘ |
| default | ✔ | ✔ | ✘ | ✘ |
| private | ✔ | ✘ | ✘ | ✘ |
12、构造方法
我们已经讲过构造方法。
那么在继承以后,构造方法又会发生什么变化?
来看下面例子。
12.1 父类构造方法
classPerson{publicPerson(){System.out.println("Person");}}12.2 子类构造方法
classStudentextendsPerson{publicStudent(){System.out.println("Student");}}测试:
newStudent();很多人猜输出:
Student实际上输出:
PersonStudent因为创建Student对象。
首先必须完成:
Person部分初始化。
因此父类构造方法一定先执行。
12.3 默认 super()
我们发现代码没有写super(),父类构造方法还是会执行。
原因:
Java自动补充:
publicStudent(){super();System.out.println("Student");}因此即使没有写,仍然调用父类构造方法。
12.4 显示调用 super()
如果父类只有有参构造。
例如:
classPerson{publicPerson(Stringname){}}那么必须自己写:
super("张三");否则编译报错。
12.5 为什么 super() 必须放第一行
这是Java规定的。
原因:
只有父类初始化完成,子类才能继续初始化。
否则子类可能使用一个尚未初始化完成的父类成员。
因此Java从语言层面规定:super() 必须位于构造方法第一行。
13、对象初始化过程
前面我们已经知道,子类对象中实际上包含两部分:
父类部分+子类部分例如:
classPerson{Stringname;intage;}classStudentextendsPerson{intscore;}创建:
Studentstu=newStudent();此时 stu 所代表的对象中,可以理解为包含:
Person部分 name ageStudent部分 score那么问题来了:创建 Student 对象时,父类部分和子类部分到底谁先初始化?
答案是:先初始化父类部分,再初始化子类部分。
也就是说:
创建Student对象 ↓ 初始化Person部分 ↓ 执行Person构造方法 ↓ 初始化Student部分 ↓ 执行Student构造方法 ↓ 对象创建完成但是,真正的初始化过程比这个更加详细。
13.1 成员变量默认初始化
当 JVM 为对象分配内存后,对象中的成员变量会先获得默认值。
例如:
classPerson{Stringname;intage;booleanflag;}创建:
Personp=newPerson();此时成员变量会先获得默认值:
name->nullage->0flag->false注意:这里说的是成员变量。
局部变量不会自动进行默认初始化。
例如:
publicstaticvoidmain(String[]args){intage;System.out.println(age);}会编译错误。
所以:
成员变量->有默认值 局部变量->必须手动初始化13.2 成员变量显式初始化
如果成员变量在定义的时候进行了初始化:
classPerson{Stringname="张三";intage=18;}那么在默认初始化之后,显式初始化按照成员变量在类中定义的顺序依次执行。
可以简单理解为:
name →nullage →0先执行默认初始化 然后再到下边的赋值 name →"张三"age →1813.3 父类部分先初始化
假设:
classPerson{Stringname="Person";publicPerson(){System.out.println("Person 构造方法");}}classStudentextendsPerson{Stringschool="Java";publicStudent(){System.out.println("Student 构造方法");}}执行:
Studentstu=newStudent();首先处理的是:
Person部分也就是:
name 默认初始化 ↓ name 显式初始化为"Person"↓Person构造方法然后才开始处理:
Student部分13.4 子类部分再初始化
接下来处理 Student 部分:
Stringschool="Java";先进行默认初始化:
school->null然后进行显式初始化:
school->"Java"最后执行 Student 构造方法:
publicStudent(){System.out.println("Student 构造方法");}所以整个过程可以理解为:
Person部分 默认初始化->显式初始化->Person构造方法Student部分 默认初始化->显式初始化->Student构造方法14、综合案例
下面通过一个完整案例,把 super、构造方法和对象初始化过程结合起来。
14.1 定义 Person
classPerson{protectedStringname="Person";protectedintage=20;publicPerson(){System.out.println("Person 无参构造方法");}publicPerson(Stringname,intage){this.name=name;this.age=age;System.out.println("Person 有参构造方法");}publicvoideat(){System.out.println("Person 吃饭");}}14.2 定义 Student
classStudentextendsPerson{privatedoublescore=100;publicStudent(Stringname,intage,doublescore){super(name,age);this.score=score;System.out.println("Student 构造方法");}@Overridepublicvoideat(){super.eat();System.out.println("Student 吃饭");}publicvoidshow(){System.out.println("姓名:"+this.name);System.out.println("年龄:"+this.age);System.out.println("成绩:"+this.score);}}14.3 测试
publicclassTest{publicstaticvoidmain(String[]args){Studentstu=newStudent("张三",18,95);stu.show();stu.eat();}}运行结果:
Person有参构造方法Student构造方法 姓名:张三 年龄:18成绩:95.0Person吃饭Student吃饭14.4 分析对象创建过程
执行:
Studentstu=newStudent("张三",18,95);首先创建 Student 对象,然后进入 Student 构造方法:
publicStudent(Stringname,intage,doublescore){super(name,age);this.score=score;System.out.println("Student 构造方法");}第一条语句:
super(name,age);调用:
Person(Stringname,intage)所以父类部分得到:
name="张三"age=18然后返回到 Student 构造方法,又执行:
this.score=score;得到:
score=95最后输出:
Student构造方法14.5 分析 super.eat()
执行:
stu.eat();Student 中重写了:
publicvoideat(){super.eat();System.out.println("Student 吃饭");}首先:
super.eat();调用父类:
Person.eat()输出:
Person吃饭然后执行:
System.out.println("Student 吃饭");输出:
Student吃饭最终:
Person吃饭Student吃饭这是一种非常典型的继承使用方式:子类重写父类方法,同时通过 super 保留父类原有功能,再增加自己的功能。
