五、繼承
簡要
1、說明
繼承(Inheritance)
是面向對象編程(OOP)
的一個核心概念,它允許一個類(子類)繼承另一個類(父類)的屬性和方法,從而實現代碼重用和結構化組織。通過繼承,子類可以擴展父類的功能或者對父類的方法進行重寫。
- 父類(超類、基類):
- 父類是被繼承的類,它包含子類可以使用的屬性和方法。
- 在Java中,使用
extends
關鍵字來實現繼承。
- 子類(派生類):
- 子類是繼承父類的類,它可以訪問父類的公共和受保護的成員(屬性和方法)。
- 子類可以添加新的屬性和方法,也可以重寫父類的方法。
2、繼承的優點
- 代碼重用:
- 子類可以直接使用父類中定義的屬性和方法,減少代碼的重復。
- 提高可維護性:
- 由于子類和父類的結構化關系,系統更加模塊化,修改父類的方法時,子類也會自動更新,從而提高了系統的可維護性。
- 實現多態:
- 繼承是實現多態(Polymorphism)的基礎,通過繼承和方法重寫,程序可以在運行時決定調用哪個類的方法。
3、繼承的實現
提供一個簡單的Java繼承示例:
//父類
class Animal{String name;public void eat(){System.out.println("This animal eat food.");}
}//子類
class Dog extend Animal{public void bark(){Sytem.out.println("The dog barks.");}public void eat(){System.out.println("The dog eats dog food.");}
}public class Main{public static void main(String[] args){Dog dog = new Dog();dog.name = "Buddy";//調用重寫的方法。dog.eat();//調用子類特有的方法。dog,bark();}
}
4、繼承的類型
- 單繼承:
- 一個子類只能繼承一個父類。Java中不支持多繼承(即一個子類繼承多個父類),但是可以通過接口來實現,類似多繼承的效果。
- 多層繼承:
- 一個類繼承另一個類,該類又繼承另一個類,形成繼承鏈。例如:類C繼承類B,類B繼承類A。
組合
1、組合 VS 繼承
繼承:
- 是一種
是一個
(is-a)關系。例如,Dog
繼承自Animal
,表示Dog
是一個Animal
。 - 子類繼承父類的所有屬性和方法,但這也導致子類與父類之間的耦合度較高。
- 如果子類發生變化,子類可能需要進行相應的修改。
- 繼承層次過深可能導致代碼復雜性增加。
組合:
- 是一種
有一個
(has - a)關系。例如,Car
有一個Engine
,表示car
包含一個Engine
對象。 - 通過將一個類的實例作為成員變量引入到另一個類中,來實現類之間的協作。
- 組合的類之間的耦合度較低,一個類的改變不會直接影響另一個類。
- 組合更靈活,可以在運行時動態改變組合對象的行為。
2、組合的示例
假設我們有一個場景,需要定義一個交通工具(Vehicle),每種交通工具有不同的移動方式(MoveStrategy)。我們可以使用組合來實現不同交通工具的行為,而不是通過繼承。
定義接口和實現類:
package base.inheritance.assembly;/*** @author: LiHao* @program: interview* @description: 敘述組合的示例(用于對比繼承)* @Date: 2024-06-10-17:34:10* thinking:*/
interface MoveStrategy {/*** 移動對象。* <p>* 該方法定義了對象的移動行為,但沒有指定移動的方式或方向。* 具體的移動邏輯應在方法體內實現,這里沒有提供實現是因為示例的限制。** @see #move(int, int) 如果需要更精確的控制移動距離和方向,可以使用帶參數的移動方法。*/void move();
}/*** 具體的移動策略實現:開車*/
class DriveStrategy implements MoveStrategy {/*** 實現移動方法。* 該方法具體實現了車輛在道路上的行駛行為。通過打印信息來模擬車輛的移動過程。* 由于這是一個抽象類的抽象方法的具體實現,所以這里使用了@Override注解來標明此方法是對父類抽象方法的實現。*/@Overridepublic void move() {System.out.println("Driving on the road.");}
}/*** 具體的移動策略實現:飛行*/
class FiyStrategy implements MoveStrategy {@Overridepublic void move() {System.out.println("Flying in the sky.");}
}/*** 具體的移動策略實現:行走*/
class WalkStrategy implements MoveStrategy {@Overridepublic void move() {System.out.println("Walking on the ground.");}
}/*** 交通工具類*/
class Vehicle {private MoveStrategy moveStrategy;/*** 構造函數,用于初始化車輛對象。** @param moveStrategy 移動策略對象,車輛將使用該策略來進行移動。* 通過傳入不同的移動策略,車輛可以實現不同的移動方式,* 提供了策略模式中的策略對象。*/public Vehicle(MoveStrategy moveStrategy) {this.moveStrategy = moveStrategy;}/*** 設置移動策略。** 本方法用于更換對象的移動策略,允許對象在運行時根據需要動態調整其移動方式。* 通過傳入不同的移動策略實例,對象可以實現不同的移動行為,從而提高代碼的靈活性和可擴展性。** @param moveStrategy 移動策略對象,用于定義對象的移動行為。*/public void setMoveStrategy(MoveStrategy moveStrategy){this.moveStrategy = moveStrategy;}/*** 實現移動操作。* 通過策略模式,調用指定的移動策略來執行移動操作。* 此方法的目的是為了封裝移動行為,具體的移動方式由包含的移動策略對象決定。*/public void move() {moveStrategy.move();}/*** 使用組合來實現不同的行為* @param args*/public static void main(String[] args) {//創建不同的移動策略MoveStrategy driveStrategy = new DriveStrategy();MoveStrategy fiyStrategy = new FiyStrategy();MoveStrategy walkStrategy = new WalkStrategy();//創建交通工具并設置移動策略Vehicle car = new Vehicle(driveStrategy);car.move();//動態改變交通工具的移動策略car.setMoveStrategy(fiyStrategy);car.move();//創建另一個交通工具并設置不同的移動策略Vehicle person = new Vehicle(walkStrategy);person.move();}
}
輸出:
Driving on the road.
Flying in the sky.
Walking on the ground.
3、組合的優點
- 靈活性:
- 可以在運行時動態改變對象的行為,而無需修改類的層次結構。
- 可以通過組合不同的策略對象來實現多種行為。
- 低耦合:
- 組合的類之間的耦合度較低,一個類的改變不會直接影響另一個類。
- 更容易維護和擴展系統,添加新的策略不需要修改現有的類。
- 遵循單一職責原則:
- 每一個類只需要關注一個特定的功能,職責更加準確。
通過理解和應用組合的模式,可以創建更靈活、易維護的系統,特別是在需求頻繁變化的場景下,組合模式的優勢更加明顯。
5、注意事項
-
訪問控制:
- 父類的
private
成員不能被子類直接訪問,但protected
和public
成員可以被子類訪問。
- 父類的
-
構造方法:
- 構造方法不能被繼承,但子類可以調用父類的構造方法(使用
super
關鍵字)。
- 構造方法不能被繼承,但子類可以調用父類的構造方法(使用
-
組合優于繼承:
- 在某些情況下,使用組合(即在一個類中包含另一個類的實例)可能比繼承更合適,因為它可以提供更好的靈活性和減少耦合度。
通過理解和應用繼承,可以創建更簡潔、可維護性強且擴展性好的代碼結構,這是面向對象編程不可或缺的一部分。
訪問權限
Java 中有三個訪問權限修飾符:private、protected 以及 public
-
如果不加訪問修飾符,表示包級可見。可以對類或類中的成員(字段和方法)加上訪問修飾符。
-
類可見表示其它類可以用這個類創建實例對象。
-
成員可見表示其它類可以用這個類的實例對象訪問到該成員;
protected 用于修飾成員,表示在繼承體系中成員對于子類可見,但是這個訪問修飾符對于類沒有意義。
設計良好的模塊會隱藏所有的實現細節,把它的 API
與它的實現清晰地隔離開來。模塊之間只通過它們的 API
進行通信,一個模塊不需要知道其他模塊的內部工作情況,這個概念被稱為信息隱藏或封裝。因此訪問權限應當盡可能地使每個類或者成員不被外界訪問。
如果子類的方法重寫了父類的方法,那么子類中該方法的訪問級別不允許低于父類的訪問級別。這是為了確保可以使用父類實例的地方都可以使用子類實例去代替,也就是確保滿足里氏替換原則。
**字段決不能是公有的,因為這么做的話就失去了對這個字段修改行為的控制,客戶端可以對其隨意修改。**例如下面的例子中,AccessExample
擁有 id 公有字段,如果在某個時刻,我們想要使用 int 存儲 id 字段,那么就需要修改所有的客戶端代碼。
public class AccessExample {public String id;
}
可以使用公有的 getter 和 setter 方法來替換公有字段,這樣的話就可以控制對字段的修改行為。
public class AccessExample {private int id;public String getId() {return id + "";}public void setId(String id) {this.id = Integer.valueOf(id);}
}
但是也有例外,如果是包級私有的類或者私有的嵌套類,那么直接暴露成員不會有特別大的影響。
public class AccessWithInnerClassExample {private class InnerClass {int x;}private InnerClass innerClass;public AccessWithInnerClassExample() {innerClass = new InnerClass();}public int getValue() {return innerClass.x; // 直接訪問}
}
抽象類與接口
一、抽象類
1.定義
抽象類是不能被實例化的類,它用來作為其它類的基類。抽象類可以包含抽象方法(沒有具體的方法)和具體方法(有方法體的方法)。
抽象類為什么不能被實例化?
設計目的:
- 不完整的實現:抽象類是用了作為其它類型的基類的,它包含抽象方法,這些方法沒有實現。因為抽象類本身并沒有提供所有方法的實現,它不完整,所以不能被實例化。實例化一個不完整的對象是沒有意義的。
特征與語言規范:
- 強制子類實現:抽象類中的抽象方法定義了子類必須實現的行為,這是一種設計模式,確保所有子類都提供具體實現。如果允許實例化對抽象類,那么這些抽象方法在沒有實現的情況下就會被調用,導致錯誤。
- 語法和編譯器要求:在Java語言規范中,抽象類被定義為不能被實例化。如果嘗試實例化一個抽象類,編譯器會報錯。這是為了確保編譯時就能發現設計上的錯誤。
示例代碼:
abstract class Animal {abstract void makeSound(); // 抽象方法,沒有方法體void eat() {System.out.println("This animal is eating.");}
}class Dog extends Animal {@Overridevoid makeSound() {System.out.println("Bark");}
}public class Main {public static void main(String[] args) {// Animal animal = new Animal(); // 編譯錯誤:Animal是抽象的;不能實例化Dog dog = new Dog();dog.makeSound(); // 輸出:Barkdog.eat(); // 輸出:This animal is eating.}
}
在上述示例中:
Animal
類是一個抽象類,包含一個抽象方法makeSound()
,沒有方法體。Dog
類繼承自Animal
并實現了makeSound()
方法。- 試圖實例化
Animal
類會導致編譯錯誤,因為Animal
是抽象的,不能直接創建其實例。
總結:
抽象類不能實例化的原因主要是:
- 不完整實現:抽象類本身不完整,包含未實現的方法。
- 設計模式:確保子類實現必要的方法,強制制定的設計模式。
- 語言規范:Java語言規范和編譯器的要求,防止設計錯誤。
通過這些機制,抽象類可以正確地作為其它類的基類,確保代碼的健壯性和設計的一致性。
2.關鍵字
使用abstract
關鍵字類定義一個抽象類的抽象方法。
3.特點
- 部分實現:抽象類可以包含具體的方法實現,也可以包含抽象方法。子類可以繼承抽象類并實現未實現的方法。
- 構造方法:可以有構造方法,但不能實例化對象。構造方法通常被用于子類的實例化過程中調用。
- 字段和方法:可以包含字段和方法(即可以是抽象的也可以是具體的)。
- 繼承:一個類可以繼承一個抽象類(單繼承)。
- 訪問修飾符:可以使用各種訪問修飾符(public,protected,private)。
抽象類和抽象方法都使用 abstract 關鍵字進行聲明。如果一個類中包含抽象方法,那么這個類必須聲明為抽象類。
抽象類和普通類最大的區別是:抽象類不能被實例化,只能被繼承。
public abstract class AbstractClassExample {protected int x;private int y;public abstract void func1();public void func2() {System.out.println("func2");}
}
public class AbstractExtendClassExample extends AbstractClassExample {@Overridepublic void func1() {System.out.println("func1");}
}
// AbstractClassExample ac1 = new AbstractClassExample(); // 'AbstractClassExample' is abstract; cannot be instantiated
AbstractClassExample ac2 = new AbstractExtendClassExample();
ac2.func1();
2. 接口
接口是抽象類的延伸,在 Java 8 之前,它可以看成是一個完全抽象的類,也就是說它不能有任何的方法實現。
從 Java 8 開始,接口也可以擁有默認的方法實現,這是因為不支持默認方法的接口的維護成本太高了。在 Java 8 之前,如果一個接口想要添加新的方法,那么要修改所有實現了該接口的類,讓它們都實現新增的方法。
接口的成員(字段 + 方法)默認都是 public 的,并且不允許定義為 private 或者 protected。從 Java 9 開始,允許將方法定義為 private,這樣就能定義某些復用的代碼又不會把方法暴露出去。
接口的字段默認都是 static 和 final 的。
public interface InterfaceExample {void func1();default void func2(){System.out.println("func2");}int x = 123;// int y; // Variable 'y' might not have been initializedpublic int z = 0; // Modifier 'public' is redundant for interface fields// private int k = 0; // Modifier 'private' not allowed here// protected int l = 0; // Modifier 'protected' not allowed here// private void fun3(); // Modifier 'private' not allowed here
}
public class InterfaceImplementExample implements InterfaceExample {@Overridepublic void func1() {System.out.println("func1");}
}
// InterfaceExample ie1 = new InterfaceExample(); // 'InterfaceExample' is abstract; cannot be instantiated
InterfaceExample ie2 = new InterfaceImplementExample();
ie2.func1();
System.out.println(InterfaceExample.x);
3. 比較
-
從設計層面上看,抽象類提供了一種 IS-A 關系,需要滿足里式替換原則,即子類對象必須能夠替換掉所有父類對象。而接口更像是一種 LIKE-A 關系,它只是提供一種方法實現契約,并不要求接口和實現接口的類具有 IS-A 關系。
-
從使用上來看,一個類可以實現多個接口,但是不能繼承多個抽象類。
-
接口的字段只能是 static 和 final 類型的,而抽象類的字段沒有這種限制。
-
接口的成員只能是 public 的,而抽象類的成員可以有多種訪問權限。
4. 使用選擇
使用接口:
- 需要讓不相關的類都實現一個方法,例如不相關的類都可以實現
Comparable
接口中的compareTo()
方法; - 需要使用多重繼承。
使用抽象類:
- 需要在幾個相關的類中共享代碼。
- 需要能控制繼承來的成員的訪問權限,而不是都為 public。
- 需要繼承非靜態和非常量字段。
在很多情況下,接口優先于抽象類。因為接口沒有抽象類嚴格的類層次結構要求,可以靈活地為一個類添加行為。并且從 Java 8 開始,接口也可以有默認的方法實現,使得修改接口的成本也變的很低。
- Abstract Methods and Classes(opens new window)
- 深入理解 abstract class 和 interface(opens new window)
- When to Use Abstract Class and Interface(opens new window)
- Java 9 Private Methods in Interfaces(opens new window)
super
- 訪問父類的構造函數:可以使用 super() 函數訪問父類的構造函數,從而委托父類完成一些初始化的工作。應該注意到,子類一定會調用父類的構造函數來完成初始化工作,一般是調用父類的默認構造函數,如果子類需要調用父類其它構造函數,那么就可以使用 super() 函數。
- 訪問父類的成員:如果子類重寫了父類的某個方法,可以通過使用 super 關鍵字來引用父類的方法實現。
public class SuperExample {protected int x;protected int y;public SuperExample(int x, int y) {this.x = x;this.y = y;}public void func() {System.out.println("SuperExample.func()");}
}
public class SuperExtendExample extends SuperExample {private int z;public SuperExtendExample(int x, int y, int z) {super(x, y);this.z = z;}@Overridepublic void func() {super.func();System.out.println("SuperExtendExample.func()");}
}
SuperExample e = new SuperExtendExample(1, 2, 3);
e.func();
SuperExample.func()
SuperExtendExample.func()
Using the Keyword super
重寫與重載
1. 重寫(Override)
存在于繼承體系中,指子類實現了一個與父類在方法聲明上完全相同的一個方法。
為了滿足里式替換原則,重寫有以下三個限制:
- 子類方法的訪問權限必須大于等于父類方法;
- 子類方法的返回類型必須是父類方法返回類型或為其子類型。
- 子類方法拋出的異常類型必須是父類拋出異常類型或為其子類型。
使用 @Override 注解,可以讓編譯器幫忙檢查是否滿足上面的三個限制條件。
下面的示例中,SubClass 為 SuperClass 的子類,SubClass 重寫了 SuperClass 的 func() 方法。其中:
- 子類方法訪問權限為 public,大于父類的 protected。
- 子類的返回類型為 ArrayList,是父類返回類型 List 的子類。
- 子類拋出的異常類型為 Exception,是父類拋出異常 Throwable 的子類。
- 子類重寫方法使用 @Override 注解,從而讓編譯器自動檢查是否滿足限制條件。
class SuperClass {protected List<Integer> func() throws Throwable {return new ArrayList<>();}
}class SubClass extends SuperClass {@Overridepublic ArrayList<Integer> func() throws Exception {return new ArrayList<>();}
}
在調用一個方法時:
- 先從本類中查找看是否有對應的方法
- 如果沒有再到父類中查看,看是否從父類繼承來。
- 否則就要對參數進行轉型,轉成父類之后看是否有對應的方法。總的來說,方法調用的優先級為:
this.func(this)
super.func(this)
this.func(super)
super.func(super)
/*A|B|C|D*/class A {public void show(A obj) {System.out.println("A.show(A)");}public void show(C obj) {System.out.println("A.show(C)");}
}class B extends A {@Overridepublic void show(A obj) {System.out.println("B.show(A)");}
}class C extends B {
}class D extends C {
}
public static void main(String[] args) {A a = new A();B b = new B();C c = new C();D d = new D();// 在 A 中存在 show(A obj),直接調用a.show(a); // A.show(A)// 在 A 中不存在 show(B obj),將 B 轉型成其父類 Aa.show(b); // A.show(A)// 在 B 中存在從 A 繼承來的 show(C obj),直接調用b.show(c); // A.show(C)// 在 B 中不存在 show(D obj),但是存在從 A 繼承來的 show(C obj),將 D 轉型成其父類 Cb.show(d); // A.show(C)// 引用的還是 B 對象,所以 ba 和 b 的調用結果一樣A ba = new B();ba.show(c); // A.show(C)ba.show(d); // A.show(C)
}
2. 重載(Overload)
存在于同一個類中,指一個方法與已經存在的方法名稱上相同,但是參數類型、個數、順序至少有一個不同。
應該注意的是,返回值不同,其它都相同不算是重載。
class OverloadingExample {public void show(int x) {System.out.println(x);}public void show(int x, String y) {System.out.println(x + " " + y);}
}
public static void main(String[] args) {OverloadingExample example = new OverloadingExample();example.show(1);example.show(1, "2");
}