繼承
// Animal.java
public class Animal {
public String name;
public Animal(String name) {
this.name = name;
}
public void eat(String food) {
System.out.println(this.name + "is eating" + food);
}
}
// Cat.java
class Cat {
public String name;
public Cat(String name) {
this.name = name;
}
public void eat(String food) {
System.out.println(this.name + "is eating" + food);
}
public void jump() {
System.out.println(this.name + "is jumping");
}
}
// Bird.java
class Bird {
public String name;
public Bird(String name) {
this.name = name;
}
public void eat(String food) {
System.out.println(this.name + "is eating" + food);
}
public void fly() {
System.out.println(this.name + "is flying");
}
}
觀察可見:
- Animal,Cat,Bird三個類都有相同的方法eat(),且實現的功能一樣
- 三個類都具有同樣的屬性name
- 從邏輯上講,Cat,Bird都是Animal的一種(他們之間是is-a關係)
所以我們可以讓Cat,Bird繼承Animal類,實現代碼複用。本質上來講繼承就是爲了代碼的複用
繼承的語法規則
extends
用關鍵字extends來實現繼承,
class 子類/派生類 extends 父類/基類/超類{
}
(像Cat,Bird這種類就叫做子類/派生類,而Animal這種被繼承的類叫做父類/基類/超類)
注意:
- 使用 extends 指定父類.
- Java 是單繼承,也就是說一個子類只能繼承一個父類 (而C++/Python等語言支持多繼承).
- 子類會繼承父類的所有 public 的字段和方法.
- 對於父類的 private 的字段和方法, 子類中是無法訪問的.
- 子類的實例中, 也包含着父類的實例. 可以使用 super 關鍵字得到父類實例的引用.
super
super();//調用父類的構造方法 必須放在第一行 因爲要構造子類要先構造父類
super.func();//調用父類的方法func()
super.data; //調用父類的數據成員data
父類只能訪問自己的成員 或者是方法
但子類可以通過關鍵字super訪問父類的成員和方法
所以我們可以將Animal,Cat,Bird的代碼優化如下:
// Animal.java
public class Animal {
public String name;
public Animal(String name) {
this.name = name;
}
public void eat(String food) {
System.out.println(this.name + "is eating" + food);
}
}
// Cat.java
class Cat extends Animal{
public Cat(String name) {
super(name);
}
public void jump() {
System.out.println(this.name + "is jumping");
}
}
// Bird.java
class Bird extends Animal{
public Bird(String name) {
super(name);
}
public void fly() {
System.out.println(this.name + "is flying");
}
}
此時只需將Cat,Bird類中Animal有的字段和方法刪除即可
注意:
在子類的構造方法中一定要先用super()構造父類,構造完後再構造子類自己
但是此時如果想要實現封裝,將父類的name屬性變爲private,那麼編譯就會出錯,因爲子類無法訪問private修飾的方法和字段,此時應該如何實現封裝呢?
protected關鍵字
使用protected關鍵字就很好的解決了這個問題:
- 對於類的調用者來說,並不能訪問protected修飾的字段和方法
- 但對於子類來說,protected所修飾的字段和方法是可以訪問的
此處拓展幾個權限修飾關鍵字的修飾範圍:(default是什麼關鍵字都不加)
範圍 | private | default | protected | public |
---|---|---|---|---|
同一包中同一類 | √ | √ | √ | √ |
同一包中不同類 | × | √ | √ | √ |
不同包中的子類 | × | × | √ | √ |
不同包中非子類 | × | × | × | √ |
注意:
final所修飾的類不可被繼承
注意區分繼承和組合
- 繼承是一種
is-a
關係 - 組合是一種
has-a
關係,是一種包含關係
組合並沒有涉及到特殊的語法(諸如 extends 這樣的關鍵字), 僅僅是將一個類的實例作爲另外一個類的字段.比如:
class Student{
...
}
class Teacher{
...
}
class School{
public Student[] students;
public Teacher[] teacher;
...
}
多態
向上轉型
即將子類的值賦值給父類 /父類引用子類對象,比如:
Cat cat = new Cat("大不妞");
可以寫成
Animal cat = new Cat("大不妞");
此時 cat 是父類 (Animal) 的引用, 指向子類 (Cat) 的實例. 這種寫法稱爲 向上轉型
向上轉型發生的時機 :
- 直接賦值
- 方法傳參
- 方法返回
上面列舉的是直接賦值,方法傳參和方法返回見下:
方法傳參的形式()
public class Test {
public static void main(String[] args) {
Cat cat = new Cat("大不妞");
feed(cat);
}
public static void feed(Animal animal) {
animal.eat(" fish");
}
}
方法返回
public class Test {
public static void main(String[] args) {
Animal animal = findMyAnimal();
}
public static Animal findMyAnimal() {
Cat cat = new Cat("大不妞");
return cat;
}
}
動態綁定
將前面示例的代碼稍作改動,讓子類Cat和Animal有一個同名但實現功能不同的方法eat();
// Animal.java
public class Animal {
public String name;
public Animal(String name) {
this.name = name;
}
public void eat(String food) {
System.out.println("我是一隻小動物");
System.out.println(this.name + "is eating" + food);
}
}
// Cat.java
class Cat extends Animal{
public Cat(String name) {
super(name);
}
public void eat(String food) {
System.out.println("我是一隻小貓咪");
System.out.println(this.name + "is eating" + food);
}
}
class Demo0223 {
public static void main(String[] args) {
Animal animal1 = new Animal("大不妞");
animal1.eat(" fish");
Animal animal2 = new Cat("大不妞");
animal2.eat(" fish");
}
}
執行結果:
此時, 我們發現:
- animal1 和 animal2 雖然都是 Animal 類型的引用, 但是 animal1 指向 Animal 類型的實例, 而animal2 指向的是Cat 類型的實例.
- animal1 和 animal2 分別調用 eat 方法, 發現 animal1.eat() 實際調用了父類的方法, 而animal2.eat() 實際調用了子類的方法.
由此可得:
在 Java 中, 子類和父類擁有同名方法,此時調用該方法時究竟執行的是子類的方法還是父類的方法 , 要看究竟這個引用指向的是子類對象還是父類對象. 這個過程是程序運行時決定的(而不是編譯期), 因此稱爲 動態綁定
反彙編發現程序編譯時確實調用的是父類的方法 但是運行時卻調用的子類的方法 這就是運行時綁定(也叫動態綁定),這就是所謂的 編譯看左,運行看右
方法重寫(override)
像是上述代碼當中的eat():
子類實現父類的同名方法, 並且參數的類型和個數完全相同, 這種情況稱爲 方法覆寫/重寫/覆蓋(Override)
方法重寫的注意事項:
- 普通方法可以重寫, static 修飾的靜態方法不能重寫
- 重寫中子類的方法的訪問權限不能低於父類的方法訪問權限(也就是說如果父類方法是用protected修飾,那麼子類方法肯定不能是public修飾)
對於重寫的方法可以顯示的給一個註解@override
class Cat extends Animal{
public Cat(String name) {
super(name);
}
@override
public void eat(String food) {
System.out.println("我是一隻小貓咪");
System.out.println(this.name + "is eating" + food);
}
}
這樣做的好處在於這個註解能幫我們進行一些合法性校驗.
例如不小心將方法名字拼寫錯了 (比如寫成 aet), 那麼此時編譯器就會發現父類中沒有 aet 方法, 就會編譯報錯, 提示無法構成重寫.
重寫和重載的區別:
方法重寫 | 方法重載 | |
---|---|---|
方法名 | 相同 | 相同 |
參數列表 | 相同 | 不同 |
返回值 | 相同 | 不做要求 |
範圍 | 繼承 | 同一個類 |
限制 | 被重寫的方法不能擁有比父類更嚴格的訪問控制權限 | 沒有訪問控制權限要求 |
發生多態要滿足兩個條件:(這個多態叫做運行時多態)
- 父類需要引用子類對象(即向上轉型)
- 通過父類的引用調用子類和父類同名的覆蓋方法
class對象存儲位置在方法區
反射: 獲取class對象(用三種方法會發現class對象地址一樣==》class對象只有一個)
向下轉型
向下轉型是將子類對象轉給父類,一般不太常見,下面將介紹他的作用
還是剛剛這段代碼
// Animal.java
public class Animal {
public String name;
public Animal(String name) {
this.name = name;
}
public void eat(String food) {
System.out.println("我是一隻小動物");
System.out.println(this.name + "is eating" + food);
}
}
// Cat.java
class Cat extends Animal{
public Cat(String name) {
super(name);
}
@override
public void eat(String food) {
System.out.println("我是一隻小貓咪");
System.out.println(this.name + "is eating" + food);
}
public void jump() {
System.out.println(this.name + "is jumping");
}
}
讓貓咪吃東西
Animal animal = new Cat("大不妞");
animal.eat(" fish");
//執行結果
//大不妞 is eating fish
如果我們想讓貓咪跑起來
animal.jump();
此時編譯出錯,找不到jump();
方法
因爲編譯看左,運行看右,編譯時期編譯器先在Animal類中看有沒有jump方法,沒有所以直接編譯出現錯誤
那如果想要讓貓咪跑起來就只能
Animal animal = new Cat("大不妞");
Cat cat = (Cat)animal;
animal.jump();
這種就是向下轉型,但是向下轉型存在風險,比如:
Animal animal = new Bird("啾啾");
Cat cat = (Cat)animal;
animal.jump();
//此時執行會拋出類型轉換異常 java.lang.ClassCastException
因爲本質上animal是一個Bird類型的,和Cat直接沒有關係,所以就會出現類型轉換異常
==》要發生向下轉型最好先判斷是否是一個實例
instanseof
instanseof可以判定一個引用是否是某個類的實例
if(Animal instanseof Cat){
Cat cat=(Cat)animal;
cat.jump();
}
構造方法內是否可以發生運行時綁定?
答案是可以,例子見下
class A {
public A() {
func();
}
public void func() {
System.out.println("A.func()");
}
}
class B extends A {
private int num = 1;
@Override
public void func() {
System.out.println("B.func() " + num);
}
}
public class Test {
public static void main(String[] args) {
B b = new B();
}
}
// 執行結果
B.func() 0
爲什麼執行出來的num會是0?
構造子類對象前要先構造父類
所以構造 B 對象的同時, 會調用 A 的構造方法.
A 的構造方法中調用了 func 方法, 此時會觸發動態綁定, 會調用到 B 中的 func
此時 B 對象自身還沒有構造, 此時 num 處在未初始化的狀態, 值爲 0.
使用多態的好處是什麼?
-
類調用者對類的使用成本進一步降低.
封裝 是讓類的調用者不需要知道類的實現細節.
多態 能讓類的調用者連這個類的類型是什麼都不必知道, 只需要知道這個對象具有某個方法即可.
因此, 多態可以理解成是封裝的更進一步, 讓類調用者對類的使用成本進一步降低. -
能夠降低代碼的 “圈複雜度”(一段代碼中的分支和循環語句越多,圈複雜度越高), 避免使用大量的 if - else
-
可擴展能力更強,使用多態的方式代碼改動成本也比較低.