1.多型
1.多型初識
什么是多型呢?通俗地說“一種形式多種形態”,這樣回答肯定不會讓人滿意,下面這段代碼會告訴你什么是多型
class Animal{
public String name;
public int age;
}
class Dog extends Animal{
}
class Bird extends Animal{
}
public class TestDemo {
private static void test(){
Dog dog = new Dog();// 普通的創建一個 dog 物件
Animal animal = new Dog();// 一個 Animal 型別的參考指向 Dog 的物件
// Bird也可以
Animal animal1 = new Bird();
}
public static void main(String[] args) {
test();
}
}
- 我們發現 new Dog() 物件不僅可以通過 Dog 型別的參考指向還可以使用它的父類 Animal 型別參考進行指向
- 父類變數存盤子類變數就是開頭所說的“一種形式多種形態”的意思,一個 Animal 型別變數可以存盤阿貓阿狗,鳥等動物
思考:能否 Bird 指向 Dog 呢?

我們發現 IDEA 已經提示我們錯誤
原因是:Dog和Bird兩個類不兼容,打個比方說:狗能是鳥嗎?這一定是打破自然規律了吧,所以并不能指著狗說它是一只小鳥這樣的錯誤,編譯器也同樣無法將 Bird 類指向 Dog 類,只能是它們的父類 Animal 指向 Dog 類,Bird 類等動物類,
2.向上轉型
class Animal{
String name = "Animal";
int age = 1;
Animal(String name) {
this.name = name;
}
void eat(){
System.out.println(this.name + "Animal:eat()");
}
}
class Dog extends Animal{
Dog(String name) {
super(name);
}
void eat(){
System.out.println(this.name + "Dog:eat()");
}
}
class Bird extends Animal{
Bird(String name) {
super(name);
}
void eat(){
System.out.println(this.name + "Bird:eat()");
}
}
public class TestDemo {
private static void test(){
Dog dog = new Dog("二哈");
dog.eat();
Animal animal = new Dog("二哈");
animal.eat();
}
public static void main(String[] args) {
test();
}
}
二哈Dog:eat()
二哈Dog:eat()
這里我們加入了構造方法,給了每個類一個name="Animal"和age=1成員資料,我們輸出eat方法看看結果
我們發現無論呼叫 dog 還是 animal 的eat方法,都是呼叫的 Dog 類的eat方法
// 生成一個 Dog 參考物件可以這樣寫
Dog dog = new Dog("二哈");
// 上方代碼也可以這樣寫
Dog dog1 = new Dog("二哈");
Animal animal = dog1;// 或者簡化為:Animal animal = new Dog("二哈");【推薦這樣寫】
- 此時 dog1 是一個父類 (Animal) 的參考, 指向一個子類 (Bird) 的實體. 這種寫法稱為向上轉型.
- 向上轉型這樣的寫法可以結合 is - a 語意來理解
- 例如:我說“今天喂 小狗 了嗎?”或者說“今天喂 二哈 了嗎?”,因為二哈確實是一條狗,也確實是一個動物
3.向上轉型的方法傳參
1.直接賦值
上述舉例
Dog dog = new Dog("二哈");
Animal animal = new Dog("二哈");
就是直接賦值
2. 方法傳參
public class TestDemo {
private static void test_eat(Animal animal){
animal.eat();
}
private static void test(){
Dog dog = new Dog("二哈");
test_eat(dog);
Animal animal = new Dog("二哈");
test_eat(animal);
}
public static void main(String[] args) {
test();
}
}
此時形參 animal 的型別是 Animal (基類), 實際上對應到 Dog(父類)的實體
3. 方法回傳
public class TestDemo {
public static Animal findMyAnimal() {
Dog dog = new Dog("二哈");
return dog;
}
private static void test_eat(Animal animal) {
animal.eat();
}
private static void test() {
test_eat(findMyAnimal());
}
public static void main(String[] args) {
test();
}
}
二哈Dog:eat()
此時方法 findMyAnimal 回傳的是一個 Animal 型別的參考, 但是實際上對應到 Bird 的實體.
4. 動態系結
說到多型,就離不開向上轉型,向上轉型的運行結果又離不開動態系結,
源代碼如下:
class Animal {
String name = "Animal";
int age = 1;
Animal(String name) {
this.name = name;
}
void eat() {
System.out.println(this.name + "Animal:eat()");
}
}
class Dog extends Animal {
Dog(String name) {
super(name);
}
void eat() {
System.out.println(this.name + "Dog:eat()");
}
}
class Bird extends Animal {
Bird(String name) {
super(name);
}
void eat() {
System.out.println(this.name + "Bird:eat()");
}
}
public class TestDemo {
public static void main(String[] args) {
Animal animal = new Dog("二哈");
animal.eat();
}
}
匯編代碼如下:
cxf@cxfdeMacBook-Pro Gao % javap -c TestDemo
警告: 二進制檔案TestDemo包含bit.basis.Gao.TestDemo
Compiled from "TestDemo.java"
public class bit.basis.Gao.TestDemo {
public bit.basis.Gao.TestDemo();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: new #2 // class bit/basis/Gao/Dog
3: dup
4: ldc #3 // String 二哈
6: invokespecial #4 // Method bit/basis/Gao/Dog."<init>":(Ljava/lang/String;)V
9: astore_1
10: aload_1
11: invokevirtual #5 // Method bit/basis/Gao/Animal.eat:()V
14: return
}
cxf@cxfdeMacBook-Pro Gao %
- 其中 invokespecial 指代的是:呼叫無須動態系結的實體方法【呼叫實體方法;對超類、私有和實體初始化方法呼叫進行特殊處理】,通俗易懂地說就是呼叫父類的構造方法
- 程式從 main 函式開始,所以會首先構造父類的構造方法也就是Object無參自帶的構造方法【 1: invokespecial #1 // Method java/lang/Object."": ()V】
- 程式在接著完成內部的構造方法也就是Dog【 6: invokespecial #4 // Method bit/basis/Gao/Dog."":(Ljava/lang/String;)V】
- 程式呼叫 Animal 的,eat() 的函式【 11: invokevirtual #5 // Method bit/basis/Gao/Animal.eat:()V】
明明呼叫的 Animal.eat() 的函式放啊,為何列印出Dog.eat(0方法?
二哈Animal:eat()
卻列印出了
二哈Dog:eat()
這就是上文中解釋的動態系結了–呼叫無需動態系結的實體方法,而Dog.eat()需要動態系結因此會呼叫父類 Animal.eat() 的方法,然后再運行時 JVM 會進行特殊處理掉用 子類 的方法,因此會列印出
二哈Dog:eat()
這樣的意外結果,
因此, 在 Java 中, 呼叫某個類的方法, 究竟執行了哪段代碼 (是父類方法的代碼還是子類方法的代碼) , 要看究竟這個引 用指向的是父類物件還是子類物件. 這個程序是程式運行時決定的(而不是編譯期), 因此稱為 動態系結.
5.方法重寫
針對剛才的 eat 方法來說:
子類實作父類的同名方法, 并且引數的型別和個數完全相同, 這種情況稱為 覆寫/重寫/覆寫(Override).
關于重寫的注意事項
- 重寫和多載完全不一樣. 不要混淆(思考一下, 多載的規則是啥?)
- 普通方法可以重寫, static 修飾的靜態方法不能重寫.
- 重寫中子類的方法的訪問權限不能低于父類的方法訪問權限.
- 重寫的方法回傳值型別不一定和父類的方法相同(但是建議最好寫成相同, 特殊情況除外).
方法權限示例: 將子類的 eat 改成 private
class Animal {
String name = "Animal";
int age = 1;
Animal(String name) {
this.name = name;
}
void eat() {
System.out.println(this.name + "Animal:eat()");
}
}
class Dog extends Animal {
Dog(String name) {
super(name);
}
private eat() {
System.out.println(this.name + "Dog:eat()");
}
}
Dog中的eat()無法覆寫bit.basis.Gao.Animal中的eat()正在嘗試分配更低的訪問權限; 以前為package
另外, 針對重寫的方法, 可以使用 @Override 注解來顯示指定【快捷鍵:command+o或者ctr+o】
class Bird extends Animal {
Bird(String name) {
super(name);
}
@Override
void eat() {
System.out.println(this.name + "Bird:eat()");
}
}
有了這個注解能幫我們進行合法性檢驗,列入不小心把 eat 寫成了 ate ,那么此時編譯器就會發現父類中沒有 ate 方法,就會報錯提示無法完成重寫
推薦在代碼中進行重寫方法時顯式加上 @Override 注解
6.多載和重寫區別
| No | 區別 | 多載(overload) | 重寫(override) |
|---|---|---|---|
| 1 | 概念 | 方法名相同,引數個數和型別不同,回傳值不做要求 | 方法名相同,引數個數和型別相同,回傳值盡量相同 |
| 2 | 范圍 | 一個類 | 繼承關系中 |
| 3 | 限制 | 沒有權限要求 | 被覆寫的方法不能擁有比父類更嚴格的訪問控制權限 |
事實上, 方法重寫是 Java 語法層次上的規則, 而動態系結是方法重寫這個語法規則的底層實作. 兩者本質上描述 的是相同的事情, 只是側重點不同
7.理解多型
有了面的向上轉型, 動態系結, 方法重寫之后, 我們就可以使用 多型(polypeptide) 的形式來設計程式了.
我們可以寫一些只關注父類的代碼, 就能夠同時兼容各種子類的情況.
代碼示例: 列印多種形狀
class Shape{
void draw(){
}
}
class Cycle extends Shape{
@Override
void draw() {
System.out.println("畫一個圓");
}
}
class Rect extends Shape{
@Override
void draw() {
System.out.println("畫一個矩形");
}
}
class Flower extends Shape{
@Override
void draw() {
System.out.println("畫一個花");
}
}
public class TestDemo {
private static void func(Shape s){
s.draw();
}
private static void test(){
Shape[] shapes = new Shape[]{new Cycle(), new Rect(), new Flower()};
for (Shape s: shapes) {
func(s);
}
}
public static void main(String[] args) {
test();
}
}
畫一個圓
畫一個矩形
畫一個花
- 創建1個父類 Shape,寫一個 draw 方法,什么都不用干
- 創建一些圖案類,繼承父類 Shape,則每個子類都會有 draw 方法
- 每個子類重寫 draw 方法
- func 函式形參是 Shape 型別,在 test 函式中的foreach回圈的s都會進入 Shape 中進行向上轉型
- 每個子類重寫 draw 方法在運行 s.draw() 的時候進行動態系結,不用考慮型別是否兼容
當類的呼叫者在撰寫 draw 這個方法的時候, 引數型別為 Shape (父類), 此時在該方法內部并不知道, 也不關注當 前的 shape 參考指向的是哪個型別(哪個子類)的實體. 此時 shape 這個參考呼叫 draw 方法可能會有多種不同的表現 (和 shape 對應的實體相關), 這種行為就稱為 多型
多型顧名思義, 就是 “一個參考, 能表現出多種不同形態”
8.多型的好處
1.類呼叫者對類的使用成本進一步降低
- 封裝是為了讓類的呼叫者不需要知道累的實作細節
- 多型能讓類的呼叫者無需考慮這個類的型別,只需要知道物件的某個方法即可
因此可以理解多型是封裝的更進一步,讓類的呼叫者對類的使用成本進一步降低
2.能夠降低代碼的 “圈復雜度”, 避免使用大量的 if - else
如下列印的形狀需要進行if-else匹配才行
private static void test() {
Rect rect = new Rect();
Cycle cycle = new Cycle();
Flower flower = new Flower();
String[] shapes = {"cycle", "rect", "cycle", "rect", "flower"};
for (String shape : shapes) {
if (shape.equals("cycle")) {
cycle.draw();
}else if (shape.equals("rect")){
rect.draw();
}else if (shape.equals("flower")){
flower.draw();
}
}
}
如果換用多型,只需要一行代碼解決
private static void func(Shape s) {
s.draw();
}
private static void test() {
Rect rect = new Rect();
Cycle cycle = new Cycle();
Flower flower = new Flower();
String[] shapes = {"cycle", "rect", "cycle", "rect", "flower"};
for (String shape : shapes) {
if (shape.equals("cycle")) {
cycle.draw();
}
}
}
什么叫 “圈復雜度” ?
圈復雜度是一種描述一段代碼復雜程度的方式. 一段代碼如果平鋪直敘, 那么就比較簡單容易理解. 而如果有很多的條件分支或者回圈陳述句, 就認為理解起來更復雜. 因此我們可以簡單粗暴的計算一段代碼中條件陳述句和回圈陳述句出現的個數, 這個個數就稱為 “圈復雜度”. 如果一個方法的圈復雜度太高, 就需要考慮重構
不同公司對于代碼的圈復雜度的規范不一樣. 一般不會超過 10 .
3.可擴展能力更強.
如果要新增一種新的形狀, 使用多型的方式代碼改動成本也比較低.
class Triangel extends Shape{
@Override
void draw() {
System.out.println("畫一個三角");
}
}
- 對于類的呼叫者來說(drawShapes方法), 只要創建一個新類的實體就可以了, 改動成本很低.
- 而對于不用多型的情況, 就要把 drawShapes 中的 if - else 進行一定的修改, 改動成本更高.
9.向下轉型
向上轉型就是子類物件轉為父類物件;向下轉型就是父類物件轉為字類物件,相較于向上轉型,向下轉型用的不多但是也有一定用途
package bit.basis.Gao;
class Animal {
String name;
Animal(String name) {
this.name = name;
}
void eat(String food) {
System.out.println("Animal:eat()" + this.name + food);
}
}
class Bird extends Animal {
Bird(String name) {
super(name);
}
void eat(String food) {
System.out.println("Bird:eat()" + this.name + food);
}
void fly() {
System.out.println("Bird:fly()" + this.name + "正在飛");
}
}
public class TestDemo {
public static void main(String[] args) {
Animal animal = new Bird("鸚鵡");
animal.eat("食物");
}
}
我們讓 animal 飛起來
animal.fly();
// 編譯出錯
java: 找不到符號
符號: 方法 fly()
位置: 型別為bit.basis.Gao.Animal的變數 animal
注意事項
編譯程序中, animal 的型別是 Animal, 此時編譯器只知道這個類中有一個 eat 方法, 沒有 fly 方法. 雖然 animal 實際參考的是一個 Bird 物件, 但是編譯器是以 animal 的型別來查看有哪些方法的. 對于 Animal animal = new Bird(“鸚鵡”) 這樣的代碼
- 編譯器檢查哪些方法存在,檢查的是 Animal 型別
- 執行的時候究竟執行父類還是子類的方法,看的是 Bird 這個型別
因此,要想讓上述代碼實作剛才的效果必須進行向下轉型
Bird bird = (Bird) animal;
bird.fly();
// 執行結果
Bird:eat()鸚鵡食物// 呼叫 eat 方法,運行的是子類的 eat 方法而不是父類的 eat 方法
Bird:fly()鸚鵡正在飛
但是這樣的向下轉型是不太可靠的,例如
class Animal {
String name;
Animal(String name) {
this.name = name;
}
void eat(String food) {
System.out.println("Animal:eat()" + this.name + food);
}
}
class Bird extends Animal {
Bird(String name) {
super(name);
}
void eat(String food) {
System.out.println("Bird:eat()" + this.name + food);
}
void fly() {
System.out.println("Bird:fly()" + this.name + "正在飛");
}
}
class Cat extends Animal{
Cat(String name) {
super(name);
}
@Override
void eat(String food) {
System.out.println("Cat:eat()" + this.name + food);
}
}
public class TestDemo {
public static void main(String[] args) {
Animal animal = new Cat("咪咪");
Bird bird = (Bird) animal;
bird.fly();
}
}
// 運行出錯
Exception in thread "main" java.lang.ClassCastException
animal 本質上參考的是一個 Cat 物件, 是不能轉成 Bird 物件的. 運行時就會拋出例外.
所以, 為了讓向下轉型更安全, 我們可以先判定一下看看 animal 本質上是不是一個 Bird 實體, 再來轉換
public static void main(String[] args) {
Animal animal = new Cat("咪咪");
if (animal instanceof Bird) {
Bird bird = (Bird) animal;
bird.fly();
}
}
// 運行不報錯
instanceof 可以判定一個參考是否是某個類的實體. 如果是, 則回傳 true,這時在進行向下轉型就安全了
10.super關鍵字
前面的代碼中由于使用了重寫機制, 呼叫到的是子類的方法. 如果需要在子類內部呼叫父類方法怎么辦? 可以使用 super 關鍵字.
super 表示獲取到父類實體的參考. 涉及到兩種常見用法
Java技術核心卷中解釋 super 為特殊用法而不是父類參考,把它當為父類參考來理解的話更容易理解 super 的運用
- 使用 super 來構造父類的構造器
Bird(String name) {
super(name);
}
- 使用 super 來呼叫父類的普通方法
class Bird extends Animal {
Bird(String name) {
super(name);
}
@Override
void eat(String food) {
System.out.println("Bird:eat()" + this.name + food);
super.eat(food);
}
void fly() {
System.out.println("Bird:fly()" + this.name + "正在飛");
}
}
public static void main(String[] args) {
Animal animal = new Bird("鸚鵡");
Bird bird = (Bird) animal;
bird.eat("食物");
}
Bird:eat()鸚鵡食物//呼叫的 Bird 子類的 eat 方法
Animal:eat()鸚鵡食物//呼叫的 Animal 父類的 eat 方法【super.eat(food)】
這個代碼中,如果在子類中直接呼叫 eat (不加super),那么此時就默認呼叫子類的 eat 方法(也就是重寫了),而加上 super 才是呼叫的父類方法
注意:super 和 this 功能相似但還是有些區別
| No | 區別 | this | super |
|---|---|---|---|
| 1 | 概念 | 訪問本類中的屬性和方法 | 子類訪問父類中的屬性和方法 |
| 2 | 查找范圍 | 先查找本類如果本類沒有就呼叫父類 | 不查找本類直接呼叫父類 |
| 3 | 特殊 | 表示當前物件 | 無 |
11.在構造方法中呼叫重寫的方法(一個坑)
class B {
B() {
func();
}
void func() {
System.out.println("B.func()");
}
}
class D extends B {
private int num = 1;
D() {
super();
}
@Override
void func() {
System.out.println("D.func()" + num);
}
}
public class TestDemo {
public static void main(String[] args) {
D d = new D();
}
}
通過 javap -c 位元組碼檔案 命令來查看
cxf@cxfdeMacBook-Pro Gao % javap -c TestDemo
警告: 二進制檔案TestDemo包含bit.basis.Gao.TestDemo
Compiled from "TestDemo.java"
public class bit.basis.Gao.TestDemo {
public bit.basis.Gao.TestDemo();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: new #2 // class bit/basis/Gao/D
3: dup
4: invokespecial #3 // Method bit/basis/Gao/D."<init>":()V
7: astore_1
8: return
}
我們查看 main 函式中的匯編代碼如下:
首先 new 了一個物件, 后邊的注釋說明 new 的是 D 型別物件
再看 invokespecial 來查看最終運行的是哪一個型別的 func 函式,注釋說明的是 D 的func也就是子類的 func函式
到這里讀懂之后再分析代碼的運行程序如下:
D d = new D();
子類 D 繼承父類 B,而 B 的構造方法
B 的構造方法會觸發動態系結,會呼叫到 D 的 func
此時 D 自身還沒有構造,所以此時 num 為未初始化狀態,值為 0
12.總結
多型是面向物件程式設計中比較難理解的部分. 我們會在后面的抽象類和介面中進一步體會多型的使用. 重點是多型帶來的編碼上的好處.
另一方面, 如果拋開 Java, 多型其實是一個更廣泛的概念, 和 “繼承” 這樣的語法并沒有必然的聯系.
- C++ 中的 “動態多型” 和 Java 的多型類似. 但是 C++ 還有一種 “靜態多型”(模板), 就和繼承體系沒有關系了.
- Python 中的多型體現的是 “鴨子型別”, 也和繼承體系沒有關系.
- Go 語言中沒有 “繼承” 這樣的概念, 同樣也能表示多型.
無論是哪種編程語言, 多型的核心都是讓呼叫者不必關注物件的具體型別. 這是降低用戶使用成本的一種重要方式.
2.抽象類
1.語法規則
在剛才的列印圖形例子中, 我們發現, 父類 Shape 中的 draw 方法好像并沒有什么實際作業, 主要的繪制圖形都是由 Shape 的各種子類的 draw 方法來完成的. 像這種沒有實際作業的方法, 我們可以把它設計成一個 抽象方法(abstract method), 包含抽象方法的類我們稱為 抽象類(abstract class).
JDK 1.8 之前,抽象類的方法默認權限是 protected
JDK 1.8時,抽象類的方法默認權限是default
abstract class Shape{
abstract void draw();
}
注意事項
- 抽象類不能實體化
- 抽象方法不能是 private
- 抽象類中可以包含其他的非抽象方法, 也可以包含欄位. 這個非抽象方法和普通方法的規則都是一樣的, 可以被重寫, 也可以被子類直接呼叫
abstract class Shape{
abstract void draw();
}
public class TestDemo {
public static void main(String[] args) {
Shape shape = new Shape();// 抽象類的實體化
}
}
java: bit.basis.Gao.Shape是抽象的; 無法實體化
private abstract class Shape {
abstract void draw();
}
public class TestDemo {
public static void main(String[] args) {
}
}
java: 此處不允許使用修飾符private
abstract class Shape {
abstract void draw();
void func() {
System.out.println("Rect: func()");
}
}
class Rect extends Shape {
@Override
void draw() {
System.out.println("畫一個矩形");
}
}
public class TestDemo {
public static void main(String[] args) {
Shape shape = new Rect();
shape.func();
}
}
Rect: func()
2.抽象類的作用
抽象類存在的最大意義就是為了被繼承
抽象類本身不能實體化,必須創建該抽象類的子類,子類然后必須重寫抽象類中的抽象方法【快速重寫: ctrl+o】
普通的類也可以被繼承呀, 普通的方法也可以被重寫呀, 為啥非得用抽象類和抽象方法呢?
確實如此. 但是使用抽象類相當于多了一重編譯器的校驗.
使用抽象類的場景就如上面的代碼,實際作業并不應該由父類完成,而應該由子類來完成,那么此時如果不小心誤用成父類了, 使用普通類編譯器是不會報錯的. 但若父類是抽象類就會在實體化的時候提示錯誤, 讓我們盡早發現問題.
很多語法存在的意義都是為了 “預防出錯”, 例如我們曾經用過的 final 也是類似. 創建的變數用戶不去修改, 不就 相當于常量嘛? 但是加上 final 能夠在不小心誤修改的時候, 讓編譯器及時提醒我們.
充分利用編譯器的校驗, 在實際開發中是非常有意義的.
3.介面
1.語法規則
JDK1.8之前,介面中的方法必須是 public
JDK1.8時,介面中的方法可以是 public 也可以是 default
JDK1.9是,介面中的方法可以是 private
介面是抽象類的更進一步. 抽象類中還可以包含非抽象方法, 和欄位. 而介面中包含的方法都是抽象方法, 欄位只能包含
靜態常量
- 使用 interface 定義一個介面
- 介面中的方法一定是抽象方法,所以 abstract 可以省略
- 介面中的方法一定是 public 因此可以省略 public
- Rect 使用的 implements 繼承介面,此時表達的含義不是 “繼承” 而是 “實作”
- 在創建的時候可以同樣可以創建一個介面的參考,對應到子類的一個實體
- 介面同樣不能被單獨實體化
- 呼叫介面的類必須重寫介面中的抽象方法【快速重寫: ctrl+o】
擴展(extends) vs 實作(implements)
擴展指的是當前已經有一定的功能了, 進一步擴充功能.
實作指的是當前啥都沒有, 需要從頭構造出來.
介面中只能包含抽象方法;介面中也只能包含靜態常量
方法+屬性的完整代碼
public interface IOperation {
public abstract void draw();// void draw();
public static final int a = 10;// int a = 10;
}
灰色部分代表代碼可以省略

其中的 public, static, final 的關鍵字都可以省略. 省略后的 a 仍然表示 public 的靜態常量
- 我們創建介面的時候, 介面的命名一般以大寫字母 I 開頭.
- 介面的命名一般使用 “形容詞” 詞性的單詞.
- 阿里編碼規范中約定, 介面中的方法和屬性不要加任何修飾
2.一個錯誤的代碼
public interface IOperation {
void draw();
}
class Circle implements IOperation{
@Override
void draw() {
System.out.println("畫一個圓");
}
}
正在嘗試分配更低的訪問權限; 以前為public
完整格式
public interface IOperation {
public abstract void draw();
public static final int a = 10;
}
簡化格式
public interface IOperation {
void draw();
int a = 10;
}
3.實作多個介面
有的時候我們需要讓一個類同時繼承自多個父類. 這件事情在有些編程語言通過 多繼承 的方式來實作的.
然而 Java 中只支持單繼承, 一個類只能 extends 一個父類. 但是可以同時實作多個介面, 也能達到多繼承類似的效果. 現在我們通過類來表示一組動物.
interface IFlying{
void flying();
}
interface ISwimming{
void swimm();
}
interface IRunning{
void running();
}
class Animal{
String name = "Animal";
public Animal(String name) {
this.name = name;
}
}
// 跑
class Cat extends Animal implements IRunning{
public Cat(String name) {
super(name);
}
@Override
public void running() {
System.out.println(this.name + "Cat:running()");
}
}
// 游
class Fish extends Animal implements ISwimming{
public Fish(String name) {
super(name);
}
@Override
public void swimm() {
System.out.println(this.name + "Fish: swimming()");
}
}
// 跑+游
class Frog extends Animal implements IRunning, ISwimming{
public Frog(String name) {
super(name);
}
@Override
public void swimm() {
System.out.println(this.name + "Frog: swimming()");
}
@Override
public void running() {
System.out.println(this.name + "Frog: swimming()");
}
}
// 跑+游+飛
class Duck extends Animal implements IFlying, IRunning, ISwimming{
public Duck(String name) {
super(name);
}
@Override
public void flying() {
System.out.println(this.name + "Duck: swimming()");
}
@Override
public void swimm() {
System.out.println(this.name + "Duck: swimming()");
}
@Override
public void running() {
System.out.println(this.name + "Duck: swimming()");
}
}
public class TestDemo {
public static void main(String[] args) {
}
}
上面的代碼展示了 Java 面向物件編程中最常見的用法: 一個類繼承一個父類, 同時實作多種介面. 繼承表達的含義是 is - a 語意, 而介面表達的含義是 具有 xxx 特性 .
貓是一種動物, 具有會跑的特性
青蛙也是一種動物, 既能跑, 也能游泳
鴨子也是一種動物, 既能跑, 也能游, 還能飛
這樣設計有什么好處呢?
時刻牢記多型的好處: 忘記型別. 有了介面之后, 類的使用者就不必關注具體型別, 而只關注某個類是否具備某種能力.
例如, 現在實作一個方法, 叫 "散步"
public class TestDemo {
private static void walk(IRunning running) {
System.out.println("我帶著伙伴去散步");
running.running();
}
public static void main(String[] args) {
Cat cat = new Cat("小貓");
walk(cat);
Frog frog = new Frog("小青蛙");
walk(frog);
}
}
我帶著伙伴去散步
小貓Cat:running()
我帶著伙伴去散步
小青蛙Frog: swimming()
甚至引數可以不是 “動物”, 只要會跑!
class Robot implements IRunning {
String name;
public Robot(String name) {
this.name = name;
}
@Override
public void running() {
System.out.println(this.name + "Robot: running()");
}
}
public class TestDemo {
public static void main(String[] args) {
IRunning roboot = new Robot("機器人");
roboot.running();
}
}
機器人Robot: running()
4.介面使用實體
1.compareable
class Student{
String name;
float score;
public Student(String name, float score) {
this.name = name;
this.score = score;
}
@Override
public String toString() {
return "[" + this.name + ":" + this.score + "]";
}
}
public class TestDemo {
public static void main(String[] args) {
Student[] students = {new Student("A", 1.1f), new Student("D", 4.4f), new Student("C", 3.3f), new Student("B", 2.3f)};
}
}
按照我們之前的陣列有一個現成的 sort 方法,能否直接呼叫這個方法呢?
Arrays.sort(students);
Exception in thread "main" java.lang.ClassCastException Student cannot be cast to java.lang.Comparable
仔細思考, 不難發現, 和普通的整數不一樣, 兩個整數是可以直接比較的, 大小關系明確.
而兩個學生物件的大小關系怎 么確定? 需要我們額外指定.
讓我們的 Student 類實作 Comparable 介面, 并實作其中的 compareTo 方法
class Student implements Comparable<Student> {
String name;
float score;
public Student(String name, float score) {
this.name = name;
this.score = score;
}
@Override
public String toString() {
return "[" + this.name + ":" + this.score + "]";
}
@Override
public int compareTo(Student o) {
return (int) (this.score - o.score);
}
}
public class TestDemo {
public static void main(String[] args) {
Student[] students = {new Student("A", 1.1f), new Student("D", 4.4f), new Student("C", 3.3f), new Student("B", 2.3f)};
Arrays.sort(students);
System.out.println(Arrays.toString(students));
}
}
[[A:1.1], [B:2.3], [C:3.3], [D:4.4]]
分析 Comparable 原始碼

public interface Comparable: 一個泛型資料介面
public int compareTo(T o);介面內部的一個抽象方法
因此我們的 Student 類在實作 Comparable 介面的時候可以填充這個泛型的資料型別后再重寫 compareTo 方法
| 回傳值 | 含義 |
|---|---|
| <0 | 當前物件排在引數物件之前 |
| >0 | 當前物件排在引數物件之后 |
| ==0 | 當前物件與引數物件不分先后 |
在 sort 方法中會自動呼叫 compareTo 方法. compareTo 的引數是 Object , 其實傳入的就是 Student 型別的物件. 然后比較當前物件和引數物件的大小關系(按分數來算)
2. comparator【比較器,類似于C語言中的qsort】
C語言中的 qsort 需要額外撰寫一個比較函式,comparator也可以通過撰寫函式的形式實作排序【以冒泡為例】
public class TestDemo {
private static void comparable_BubbleSort(){
Student[] students = {new Student("A", 1.1f), new Student("D", 4.4f), new Student("C", 3.3f), new Student("B", 2.2f)};
for (int i = 0; i < students.length-1; i++) {
boolean flg = true;
for (int j = 0; j < students.length-i-1; j++) {
if(students[j].compareTo(students[j+1])>0){
Student tmp = students[j];
students[j] = students[j+1];
students[j+1] = tmp;
flg = false;
}
}
if (flg){
break;
}
}
System.out.println(Arrays.toString(students));
}
public static void main(String[] args) {
comparable_BubbleSort();
}
}
[[A:1.1], [B:2.2], [C:3.3], [D:4.4]]
細心的朋友可能會發現上述代碼中我還重寫了 comparator 函式,這個函式是根據分數排名的
@Override
public int compareTo(Student o) {
return (int) (this.score - o.score);
}
有了 comparable 為什么還有 comparator呢?
我們發現 comparable 需要在 資料型別的類中修改 return 回傳值,所以不方便于后續的呼叫,如果想要多種方案的排序,豈不是每次都要修改很麻煩,因此 comparator 誕生了,他可以當作一個 “比較器” 來使用
原始碼中我們得知回傳的是整數int: -1,0,1這樣的關系,標號1,2,3說明了它們不同數值所代表的關系
class ScoreRank implements Comparator<Student> {
@Override
public int compare(Student o1, Student o2) {
return (int) (o1.score-o2.score);
}
}
class NameRank implements Comparator<Student>{
@Override
public int compare(Student o1, Student o2) {
return o1.name.compareTo(o2.name);
}
}
public static void main(String[] args) {
Student[] students = {new Student("A", 1.1f), new Student("D", 4.4f), new Student("C", 3.3f), new Student("B", 2.2f)};
Arrays.sort(students, new ScoreRank());
System.out.println("Score rank: " + Arrays.toString(students));
Arrays.sort(students, new NameRank());
System.out.println("Name rank: " + Arrays.toString(students));
}
Score rank: [[A:1.1], [B:2.2], [C:3.3], [D:4.4]]
Name rank: [[A:1.1], [B:2.2], [C:3.3], [D:4.4]]
我們發現,comparator 確實在其中起到了 “比較器” 的作用,以后按照方案排序只需要寫一個類 class,然后 Arrays.sort(datas, new class()) 即可排序任何資料型別的元素
3. Cloneable 介面和深拷貝
Java內置了很多介面, Cloneable 就是其中之一
Object 類就存在一個 Clone 方法, 呼叫這個方法即可實作物件的 “拷貝”, 但是要想合法呼叫 clone 方法必須要實作 cloneable 介面. 否則就會出現 CloneNotSupportedException 錯誤
沒有實作介面的錯誤代碼:
class Person{
String name = "abc";
}
public class TestDemo {
public static void main(String[] args) {
Person p1 = new Person();
Person p_clone = (Person) p1.clone();
}
}
java: clone() 在 java.lang.Object 中是 protected 訪問控制

根據提示,我們修改一下成員權限再看看
class Person{
protected String name = "abc";
}
public class TestDemo {
public static void main(String[] args) {
Person p1 = new Person();
Person p_clone = (Person) p1.clone();
}
}
java: 未報告的例外錯誤java.lang.CloneNotSupportedException; 必須對其進行捕獲或宣告以便拋出
我們發現 p1.clone() 被編譯器提示錯誤,我們 option+enter 試試例外捕獲
public static void main(String[] args) {
Person p1 = new Person();
try {
Person p_clone = (Person) p1.clone();
System.out.println("src: " + p1 + "hash: " + p1.hashCode());
System.out.println("clone: " + p_clone + "hash: " + p_clone.hashCode());
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
}
src: Person{name='abc'}hash: 1846274136
clone: Person{name='abc'}hash: 1639705018
列印出的 hash 值不同,說明是一個深拷貝,

思考
上訴代碼只是拷貝的一個 String 型別的資料,如果拷貝的是一個物件呢?
我們給 Person 增加一個物件資料元素:Money物件
Cloneable 拷貝出的物件是一份 "淺拷貝"
class Money implements Cloneable{
protected float money = 12.5f;
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
class Person extends Money implements Cloneable {
protected String name = "abc";
Money money = new Money();
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
'}';
}
}
public class TestDemo {
public static void main(String[] args) {
Person p1 = new Person();
try {
Person p_clone = (Person) p1.clone();
System.out.println("p1_hash: " + p1.hashCode() + "; p1_hash_money: " + p1.money.hashCode());
System.out.println("p_clone_hash: " + p_clone.hashCode() + "; p_clone_hash_money: " + p_clone.money.hashCode());
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
}
}
p1_hash: 1846274136; p1_hash_money: 1639705018
p_clone_hash: 1627674070; p_clone_hash_money: 1639705018
我們發現 p1 和 p_clone 地址不同,說明記憶體中拷貝了兩份【深拷貝】;而 money 這個物件地址相同,說明 p1 和 p_clone 一起共用記憶體中同一塊地址【淺拷貝】

如何使物件資料型別也進行 深拷貝 呢?
需要重寫 Person 類中的 clone 方法
- 首先克隆一個副本
- 然后副本中的 money 物件在進行克隆
- 最后回傳克隆好的副本
缺點: 每次都要手動撰寫克隆的資料
優點: 實作了深拷貝
@Override
protected Object clone() throws CloneNotSupportedException {
Person personClone = (Person) super.clone();
personClone.money = (Money) personClone.money().clone();
return personClone;
}
完整代碼如下:
class Money implements Cloneable{
protected float money = 12.5f;
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
class Person extends Money implements Cloneable {
protected String name = "abc";
Money money = new Money();
@Override
protected Object clone() throws CloneNotSupportedException {
Person personClone = (Person) super.clone();
personClone.money = (Money) personClone.money.clone();
return personClone;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
'}';
}
}
public class TestDemo {
public static void main(String[] args) {
Person p1 = new Person();
try {
Person p_clone = (Person) p1.clone();
System.out.println("p1_hash: " + p1.hashCode() + "; p1_hash_money: " + p1.money.hashCode());
System.out.println("p_clone_hash: " + p_clone.hashCode() + "; p_clone_hash_money: " + p_clone.money.hashCode());
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
}
}
p1_hash: 1846274136; p1_hash_money: 1639705018
p_clone_hash: 1627674070; p_clone_hash_money: 1360875712

此時就實作了深拷貝,new Money 的地址也不一樣
5.介面間的繼承
interface IRunning {
void run();
}
interface ISwimming {
void swim();
}
// 兩棲的動物, 既能跑, 也能游
interface IAmphibious extends IRunning, ISwimming {
}
class Frog implements IAmphibious {
}
通過介面繼承創建一個新的介面 IAmphibious 表示 “兩棲的”. 此時實作介面創建的 Frog 類, 就繼續要實作 run 方法, 也需要實作 swim 方法.
介面間的繼承相當于把多個介面合并在一起
6.總結
抽象類和介面都是 Java 中多型的常見使用方式. 都需要重點掌握. 同時又要認清兩者的區別
核心區別: 抽象類中可以包含普通方法和普通欄位, 這樣的普通方法和欄位可以被子類直接使用(不必重寫), 而介面中不 能包含普通方法, 子類必須重寫所有的抽象方法.
如之前寫的 Animal 例子. 此處的 Animal 中包含一個 name 這樣的屬性, 這個屬性在任何子類中都是存在的. 因此此 處的 Animal 只能作為一個抽象類, 而不應該成為一個介面.
class Animal {
protected String name;
public Animal(String name) {
this.name = name;
}
}
再次提醒:
抽象類存在的意義是為了讓編譯器更好的校驗, 像 Animal 這樣的類我們
并不會直接使用, 而是使用它的子類. 萬一不小心創建了 Animal 的實體, 編譯器會及時報錯提醒我們.
| No | 區別 | 抽象類 | 介面 |
|---|---|---|---|
| 1 | 結構組成 | 普通類+抽象方法 | 抽象方法+全域變數 |
| 2 | 權限 | 各種權限 | 僅能public |
| 3 | 子類使用 | extends | implements |
| 4 | 關系 | 一個抽象類可以實作若干介面 | 介面不能繼承抽象類,但可以使用 extends 擴展介面功能 |
| 5 | 子類限制 | 一個子類只能繼承一個抽象類 | 一個子類可以實作多個介面 |
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/303028.html
標籤:java
下一篇:聊聊redis分布式鎖的8大坑

