继承是面向对象的三大特性之一,继承从字面可以理解为继承了某种事物或者能力。而在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([形参列表])
,调用父类的构造器(没有直接父类,那么还有Object接盘)。// 父类 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(...)
调用也必须是构造器的第一个语句,唯一一个。
注意:
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.lang.Object
类中的空参构造器。这也就是为什么子类会继承所有父类的属性或方法。
注意:虽然创建子类对象的过程中,调用了父类的构造器,但是自始至终只创建了一个对象,即为new的子类对象。