文章目录
- 继承概述
- 共性抽取
- 继承的特点
- 继承的好处
- 继承的格式
- 属性的访问特点
- 如何区分三种变量重名时
- 方法的访问特点
- 方法的重写
- 重写与重载的区别
- 覆盖重写的特点
- 覆盖重写的应用
- 覆盖重写注意事项(重要)
- 构造器的访问特点
- 两个关键字
- super
- this
- Java继承中的三个特点
- 子类对象实例化的过程
- 从结果上来看(继承性)
- 从过程上来看
继承概述
继承是面向对象的三大特性之一,继承从字面可以理解为继承了某种事物或者能力。而在Java中继承发生在子父类(或子父接口)的关系中,当子类继承父类后子类具有了父类所有的属性和方法。
共性抽取
继承主要解决的问题就是:共性抽取。
我们之前学过类和对象的内容,关于类我们可以看作是对一类具备相关属性和行为的事物的描述。而当多个类之间具有相同性同时也具有相异性,显然无法将其归为一类。此时我们可以抽取这些类之间的相同属性和行为,得到一个具备了这些相同属性和行为的类。那么其它的类就无需在本类中定义这些相同的属性和行为,只需要继承那一个类即可。
比如下图:
兔子可以归为一类,绵羊也可以归为一类,它们都是吃草,那么通过这个共同的特性可以将它们同时归为食草动物一类。同样狮子和豹子也可以同时被归为食肉动物一类。而食草动物和食肉动物也具备相同的属性和行为,比如:进食、活动、交配…,那么就可以将食草动物和食肉动物归为一类。这种一级一级的关系就像是继承,兔子继承了食草动物的特性,食草动物继承了动物的特性。
回到Java中,多个具备相同属性和行为的类被称作子类或派生类,而通过抽取这些属性和行为得到的类被称为父类、超类或者基类。继承描述的是事物之间的所属关系,这种关系是: is-a 的关系。
继承的特点
子类继承父类后,就继承父类了所有的属性和方法。使得子类可以直接访问父类中的非私有的属性和方法。
- 子类可以拥有父类的“内容”。
- 子类还可以拥有自己专有的内容,实现功能的拓展。
- 父类中私有的属性和方法,子类也会继承,但是不能直接进行访问,可以通过继承父类中公共的方法来访问父类的私有属性或方法。
继承的好处
- 减少了代码的冗余,提高代码的复用性。
- 便于功能的扩展。
- 类与类之间产生了关系,是多态的前提。
继承的格式
在继承的关系中,子类就是一个父类。也就是说,子类可以被当作父类看待(这很重要,多态前提)。
例如:父类是员工,子类是讲师,那么讲师就是一个员工。
关系:is-a
// 定义父类的格式 (一个普通的类定义)
public class 父类名称{
// ...
}
// 定义子类的格式
public class 子类名称 extends 父类名称{
// ...
}
例如:创建一个员工的父类,和它的一些子类。
// 定义一个父类,员工
public class Employee{
public void method() {
System.out.println("方法执行");
}
}
// 定义一个员工的子类,讲师
public class Teacher extends Employee {
// 子类会继承父类的方法
}
// 定义一个员工的子类,助教
public class Assistant extends Employee {
// 子类会继承父类的方法
}
public class Demo01Extends {
public static void main(String[] args) {
// 创建子类讲师的对象
Teacher teacher = new Teacher();
// 子类讲师调用父类的方法
teacher.method();
// 创建子类助教的对象
Assistant assistant = new Assistant();
// 子类助教调用父类的方法
assistant.method();
}
}
// 通过继承的方法,可以起到代码复用的作用
属性的访问特点
在父子类的继承关系当中,如果子类和父类中的成员变量重名,则创建子类对象时,访问有两种方式:
-
直接通过子类对象访问成员变量。
等号左边是谁(对象引用),就优先用谁,没有则向上找。
-
间接通过成员方法访问成员变量。
该方法属于谁,就用优先用谁的成员变量,没有则向上找。
// 定义父类
public class Father {
int numF = 10;
// 与子类成员变量名相同
int num = 100;
public void methodF()
{
// 此方法内部需要一个num变量,优先用本类的变量
System.out.println(num);
}
}
// 定义子类
public class Son extends Father{
int numS = 20;
// 与父类成员变量名相同
int num = 200;
public void methodZ()
{
// 此方法内部需要一个num变量,优先用本类的变量
System.out.println(num);
}
}
// 创建对象
public class Demo01ExtendsField {
public static void main(String[] args) {
// 创建父类对象
Father fa = new Father();
// 父类只能使用父类的成员变量和方法
System.out.println(fa.numF); // 10
// 创建子类对象
Son son = new Son();
// 子类可以使用父类和子类的成员变量和方法
System.out.println(son.numF); // 10
System.out.println(son.numS); // 20
// 变量名重名,直接方法
// 【等号左边是谁,就有用谁】
System.out.println(son.num); // 200
// 变量名重名,间接方法
// 【调用方法,方法属于谁,优先用谁的】
son.methodF(); // 100
son.methodZ(); // 200
}
}
如何区分三种变量重名时
三种变量:父成员变量,子成员变量,局部变量
-
局部变量:直接在作用域的{}中写变量名,根据就近原则会优先使用该局部变量。
-
子类成员变量:使用this.变量名来调用子类成员变量,this可以理解为当前对象的引用。
-
父类成员变量:使用super.变量名来调用父类成员变量,super可以理解为当前对象的父类引用。
// 局部变量 直接写变量名
// 子类成员变量 this.变量名
// 父类成员变量 super.变量名
// 父类
public class Father {
int num = 10;
}
// 子类
public class Son extends Father {
int num = 20;
public void method(){
int num = 30;
System.out.println(num); // 30,访问局部变量
System.out.println(this.num); // 20,访问本类成员变量
System.out.println(super.num); // 10,访问父类成员变量
}
}
// 使用
public class Demo01ExtendsField {
public static void main(String[] args) {
Son son = new Son();
// 调用子类的成员方法,查看输出结果
son.method();
}
}
方法的访问特点
在父子类的继承关系当中,创建子类对象,访问成员方法的规则是:
创建的对象是谁,就优先用谁,如果没有则向上找。
单以继承举例,子类对象在调用方法时,会优先执行本类中相应的方法,如果子类中没有该方法则会执行父类相应的方法。
注意事项:
无论是成员方法还是成员变量,如果没有都是向上找父类,绝不会向下找子类。
// 父类
public class Fu {
public void methodFu(){
System.out.println("父类方法执行!");
}
public void method()
{
System.out.println("父类重名方法执行了");
}
}
// 子类
public class Zi extends Fu{
public void methodZi(){
System.out.println("子类方法执行!");
}
public void method()
{
System.out.println("子类重名方法执行了");
}
}
// 使用
public class Demo01ExtendsMethod {
public static void main(String[] args) {
Zi zi = new Zi();
zi.methodFu();
zi.methodZi();
// 调用重名成员方法,查看输出结果
zi.method();
}
}
方法的重写
如果子类父类中出现重名的成员方法,这时的访问是一种特殊情况,叫做方法重写 (Override)。
方法重写 :子类中出现与父类一模一样的方法时(返回值类型,方法名和参数列表都相同),会出现覆盖效果,也称为重写或者复写。声明不变,重新实现。
class Fu {
public void show() {
System.out.println("Fu show");
}
} class Zi extends Fu {
//子类重写了父类的show方法
@Override
public void show() {
System.out.println("Zi show");
}
} public class ExtendsDemo05{
public static void main(String[] args) {
Zi z = new Zi();
// 子类中有show方法,只执行重写后的show方法
z.show(); // Zi show
}
}
重写与重载的区别
重写(Override
):在继承关系当中,方法的名称一样,参数列表也一样。
重载(OverLoad
):方法的名称一样,参数列表不一样。
覆盖重写的特点
创建的是子类对象,则优先用子类方法。
覆盖重写的应用
子类可以根据需要,定义特定于自己的行为。既沿袭了父类的功能名称,又根据子类的需要重新实现父类方法,从而进行扩展增强。比如新的手机增加来电显示头像的功能,代码如下:
// 老手机
public class Phone {
public void call() {
System.out.println("打电话");
}
public void send() {
System.out.println("发短信");
}
public void show() {
System.out.println("显示号码");
}
}
// 定义一个新手机,使用老手机作为父类
public class NewPhone extends Phone {
@Override
public void show() {
super.show(); // 把父类的show方法拿过来使用
// 自己再增加新功能
System.out.println("显示姓名");
System.out.println("显示头像");
}
}
public class Demo01Phone {
public static void main(String[] args) {
Phone phone = new Phone();
// 父类手机
phone.call();
phone.send();
phone.show();
System.out.println("==========");
NewPhone newPhone = new NewPhone();
newPhone.call();
newPhone.send();
newPhone.show();
}
}
覆盖重写注意事项(重要)
-
必须保证父子类之间方法的名称相同,参数列表也相同。
@Override
写在方法前,用来检测是不是有效的正确覆盖重写。
@Override // 如果不是有效覆盖会报错
public void method()
{
// ...
}
-
子类方法的返回值类型必须小于等于父类方法的返回值范围。
例如:
java
中java.lang.Object
类是所有类的公共最高父类,如果父类的返回值是String
,子类的返回值是Object
,这是错误写法会编译报错!
-
父类被重写方法的返回值类型是void,则子类重写的方法返回值类型也必须是void。
-
子类方法的权限修饰符必须大于等于父类方法的权限修饰符。
权限修饰符:
public
>protected
>(default)
>private
备注:
(default)
不是关键字default
,而是什么都不写,留空。
4. 子类不能重写父类中声明为private权限的方法。
-
子类方法抛出的异常不能大于父类被重写方法的异常 。
-
子类和父类中的同名同参数的方法,要么声明为非static的(重写),要么都声明为static的(不是重写)。
构造器的访问特点
- 子类构造器当中,有一个默认隐含的
super()
调用,子类在创建对象时会默认先执行父类构造器,再执行子类构造器。
// 父类
public class Fu {
public Fu(){
System.out.println("父类构造器!");
}
}
// 子类
public class Zi extends Fu{
public Zi(){
// super(); 默认隐含调用无参父类构造,不写也会有
System.out.println("子类构造器!");
}
}
public class Demo01Constructor {
public static void main(String[] args) {
Zi zi = new Zi();
}
}
2. 可以通过**super
**关键字,调用父类重载的构造器。
// 父类
public class Fu {
public Fu(){
System.out.println("父类无参构造器!");
}
public Fu(int num){
System.out.println("父类有参构造器!");
}
}
// 子类
public class Zi extends Fu{
public Zi(){
super(10); // 调用父类有参构造器
System.out.println("子类构造器!");
}
}
public class Demo01Constructor {
public static void main(String[] args) {
Zi zi = new Zi(); // 此时会调用父类有参构造
}
}
- 子类的构造器中必须要调用父类的构造器,如果父类只定义了一个有参构造器,且子类没有在构造器中调用父类的有参构造器,编译会报错。
public class Fu{
public Fu(int param){
System.out.println(param);
}
}
public class Zi{
public Zi(){ // 编译不通过,父类没有空参构造器,子类必须调用父类的有参构造器
}
}
// 修改方式如下:
// 1. 在子类构造器中调用父类的带参构造器
public class Zi{
public Zi(){
super(123);
}
}
// 2. 给父类提供有参构造器
public class Fu{
public Fu(){
}
public Fu(int param){
System.out.println(param);
}
}
super
的父类调用,必须是子类构造器的第一个语句。
错误写法:
public void method(){
super(); // 错误写法!只有子类构造器,才能调用父类构造器
}
public class Zi extends Fu{
public Zi(){
super();
super(10); // 错误写法!只能调用一个父类构造
System.out.println("子类构造器!");
}
}
public class Zi extends Fu{
public Zi(){
System.out.println("子类构造器!");
super(10); // 错误写法!super() 必须是第一个语句
}
}
两个关键字
super
super
关键字用来访问父类内容,代表父类的内存空间的标识。
父类的成员变量不会被覆盖重写,如果父类和子类中有同名的成员变变量,各自归属于不同的类,如果想在子类中调用父类的成员变量则也需要使用super
关键字。
super
关键字的用法有三种:
- 在子类的成员方法中,访问父类的成员变量。
- 在子类的成员方法中,访问父类的成员方法。
- 在子类的构造器中,调用父类的构造器。
注意:
- 子类中使用
super([形参列表])
的方式调用父类构造器,必须声明在子类构造器中的首行。 - 子类的构造器中会默认隐含一个
super()
,如果再定义了一个super([参数列表]),则不会在默认隐含。 - 在类的多个构造器中,至少有一个类的构造器中使用了
super([形参列表])
,调用父类的构造器(没有直接父类,那么还有Object接盘)。 - super的追溯不仅限于直接父类。
// 父类
public class Fu {
int num = 10; // 父类私有成员变量
public Fu(int num){
System.out.println("父类构造");
}
public void method() {
System.out.println("父类方法!");
}
}
// 子类
public class Zi extends Fu{
int num = 20;
public Zi() {
super(10); // 调用父类构造器
}
public void methodZi() {
System.out.println(super.num); // 父类的num
}
public void method() {
super.method(); // 访问父类中的method
System.out.println("子类方法!");
}
}
this
this
关键字用来访问本类内容
this关键字的三种用法:
-
在本类的成员方法中,访问本类的成员变量。
-
在本类的成员方法中,访问本类的另一个成员方法。
-
在本类的构造器中,访问本类的另一个构造器。
this(...)
调用也必须是构造器的第一个语句,唯一一个。
注意:
super(...)
和this(...)
两种构造调用,不能同时使用,因为this(…) 和 super(…) 都必须是构造器中的第一个语句
// 父类
public class Fu {
int num = 30;
}
// 子类
public class Zi extends Fu{
int num = 20;
// 无参构造
public Zi() {
this(10); // 本类无参构造调用本类有参构造
// 必须是构造器的第一条(唯一)语句
}
// 有参构造
public Zi(int n) {
}
public void showNum(){
int num = 10;
System.out.println(num); // 局部变量
System.out.println(this.num); // 本类成员变量
System.out.println(super.num); // 父类中的成员变量
}
public void methodA() {
System.out.println("AAA");
}
public void methodB(){
methodA();
this.methodA(); // 两种效果系相同,使用this关键字强调本类方法
System.out.println("BBB");
}
}
Java继承中的三个特点
-
Java语言是单继承的,一个类的直接父类只有唯一一个。
-
Java语言可以多级继承,即可以有父亲,爷爷,祖宗…。
-
子类的直接父类是唯一的,但是父类可以拥有多个子类。
子类对象实例化的过程
从结果上来看(继承性)
子类继承父类以后,就获取了父类中声明的属性或方法。
创建子类的对象,在堆空间中,就会加载所有父类中声明的属性(不是创造父类对象)。
从过程上来看
通过子类的构造器创建子类对象时,一定会直接或间接的调用父的构造器,进而调用父类的父类的构造器,直到调用了java.lang.Object
类中的空参构造器。这也就是为什么子类会继承所有父类的属性或方法。
注意:虽然创建子类对象的过程中,调用了父类的构造器,但是自始至终只创建了一个对象,即为new的子类对象。