Java語法之 Java 的多態、抽象類和接口

上一篇文章:Java 基礎語法之解析 Java 的包和繼承

今天這章主要介紹多態和抽象類,希望接下來的內容對你有幫助!

一、多態

在瞭解多態之前我們先瞭解以下以下的知識點

1. 向上轉型

什麼是向上轉型呢?簡單講就是

把子類對象賦值給瞭父類對象的引用

這是什麼意思呢,我們可以看下列代碼

// 假設 Animal 是父類,Dog 是子類
public class TestDemo{
    public static void main(String[] args){
        Animal animal=new Animal("動物");
        Dog dog=new Dog("二哈");
        animal=dog;
    }
}

其中將子類引用 dog 的對象賦值給瞭父類的引用,而上述代碼也可以簡化成

public class TestDemo{
    public static void main(String[] args){
        Animal animal=new Dog("二哈");
    }
}

這個其實和上述代碼一樣,這種寫法都叫“向上轉型”,將子類對象的引用賦值給瞭父類的引用

其實向上轉型以後可能用到的比較多,那麼我們什麼時候需要用它呢?

  • 直接賦值
  • 方法傳參
  • 方法返回

其中直接賦值就是上述代碼的樣子,接下來讓我們看一下方法傳參的實例

// 假設 Animal 是父類,Dog 是子類
public class TestDemo{
    public static void main(String[] args){
        Animal animal=new Dog("二哈");
        func(animal);
    }
    public static void func1(Animal animal){
        
    }
}

我們寫瞭一個函數,形參就是父類的引用,而傳遞的實參就是子類引用的對象。也可以寫成

public class TestDemo{
    public static void main(String[] args){
        Animal animal=new Animal("動物");
        Dog dog=new Dog("二哈");
        func(dog);
    }
    public static void func1(Animal animal){
        
    }
}

那麼方法返回又是啥樣的呢?其實也很簡單,如

// 假設 Animal 是父類,Dog 是子類
public class TestDemo{
    public static void main(String[] args){
        
    }
    public static Animal func2(){
        Dog dog=new Dog("二哈");
        return dog;
    }
}

其中在 func2 方法中,將子類的對象返回給父類的引用。還有一種也算是方法返回

public class TestDemo{
    public static void main(String[] args){
        Animal animal=func2();
    }
    public static Dog func2(){
        Dog dog=new Dog("二哈");
        return dog;
    }
}

方法的返回值是子類的引用,再將其賦值給父類的對象,這種寫法也叫“向上轉型”。

那麼既然我們父類的引用指向瞭子類引用的對象,那麼父類可以使用子類的一些方法嗎?試一試

class Animal{
    public String name;
    public Animal(String name){
        this.name=name;
    }
    public void eat(){
        System.out.println(this.name+"吃東西"+"(Animal)");
    }
}
class Dog extends Animal{
    public Dog(String name){
        super(name);
    }
    public void eatDog(){
        System.out.println(this.name+"吃東西"+"(Dog)");
    }
}
public class TestDemo{
    public static void main(String[] args){
        Animal animal1=new Animal("動物");
        Animal animal2=new Dog("二哈");
        animal1.eat();
        animal2.eatdog();
    }
}

結果是不可以

因為本質上 animal 的引用類型是 Animal,所以隻能使用自己類裡面的成員和方法

2. 動態綁定

那麼我們的 animal2 可以使用 Dog 類中的 eatDog 方法嗎?其實是可以的,隻要我們將這個 eatDog 改名叫 eat 就行

class Dog extends Animal{
    public Dog(String name){
        super(name);
    }
    public void eat(){
        System.out.println(this.name+"吃東西"+"(Dog)");
    }
}

修改後的部分代碼如上,此時,我們之前的 animal2 直接調用 eat,就可以得到下面的結果

這也就是說明此時

  • animal1.eat() 實際調用的是父類的方法
  • animal2.eat() 實際調用的是子類的方法

那麼為什麼將 eatDog 改成 eat 之後,animal2.eat 調用的就是子類的方法呢?

這就是我們接下來要講的重寫

3. 方法重寫

什麼叫做重寫呢?

子類實現父類的同名方法,並且

  • 方法名相同
  • 方法的返回值一般相同
  • 方法的參數列表相同

滿足上述的情況就稱為:重寫、覆寫、覆蓋(Override)

註意事項:

  • 重寫的方法不能為密封方法(即被 final 修飾的方法)。我們之前瞭解過關鍵字 final,而被他修飾的方法就叫做密封方法,該方法則不能再被重寫,如
// 假如這是父類中的方法
public final void eat(){
    System.out.println(this.name+"要吃東西");
}

此類方法是不能被重寫的

  • 子類的訪問修飾限定符權限一定要大於等於父類的權限,但是父類不能是被 private修飾
  • 方法不能被 static 修飾
  • 一般針對重寫的方法,可以使用 @Override 註解來顯示指定。加瞭他有什麼好處呢?看下面代碼
// 假如下面的 eat 是被重寫的方法
class Dog extends Animal{
    @Override
    private void eat(){
        // ...
    }
}

當我們如出現 eat 被寫成瞭 ate 時候,那麼編譯器就會發現父類中是沒有 ate 方法的,就會編譯報錯,提示無法構成重寫

  • 重寫時可以修改返回值,方法名和參數類型及個數都不可以修改。僅當返回值為類類型時,重寫的方法才可以修改返回值類型,且必須是父類方法返回值的子類;要麼就不修改,與父類返回值類型相同

瞭解到這,大傢對於重寫肯定有瞭一個概念。此時我們再回憶一下之前學過的重載,可以做一個表格來進行對比

區別 重載(Overload) 重寫(Override)
概念 方法名稱相同、參數列表不同、返回值無要求 方法名稱相同、參數列表相同、返回類型一般相同
范圍 重載不是必須在一個類當中(繼承) 繼承關系
限制 沒有權限要求 被覆寫的方法不能擁有比父類更嚴格的訪問控制權限

比較結果就是,兩者沒啥關系呀

講到這裡,我們好像一直沒有說明上一小節的標題動態綁定是啥

那麼什麼叫做動態綁定呢?發生的條件如下

  • 發生向上轉型(父類引用需要引用子類對象)
  • 通過父類引用,來調用子類和父類的同名覆蓋方法

那為啥是叫動態的呢?經過反匯編我們可以發現

編譯的時候: 調用的是父類的方法
但是運行的時候: 實際上調用的是子類的方法

因此這其實是一個動態的過程,也可以叫其運行時綁定

4. 向下轉型

既然介紹瞭向上轉型,那肯定也缺不瞭向下轉型呀!什麼時向下轉型呢?想想向上轉型就可以猜到它就是

把父類對象賦值給瞭子類對象的引用

那麼換成代碼就是

// 假設 Animal 是父類,Dog 是子類
public class TestDemo{
    public static void main(String[] args){
        Animal animal=new Animal("動物");
        Dog dog=animal;
    }
}

但是隻是上述這樣寫是不行的,會報錯

 

為什麼呢?我們可以這樣想一下

狗是動物,但是動物不能說是狗,這相當於是一個包含的關系。

因此可以將狗的對象直接賦值給動物,但是不能將動物的對象賦值給狗

我們就可以使用強制類型轉換,這樣上述代碼就不會報錯瞭

public class TestDemo{
    public static void main(String[] args){
        Animal animal=new Animal("動物");
        Dog dog=(Dog)animal;
    }
}

我們接著用 dog 引用去運行一下 eat 方法

public class TestDemo{
    public static void main(String[] args){
        Animal animal=new Animal("動物");
        Dog dog=(Dog)animal;
        dog.eat();
    }
}

運行後出現瞭錯誤

動物不能被轉換成狗!

 那我們該怎麼做呢?我們要記住一點:

使用向下轉型的前提是:一定要發生瞭向上轉型

public class TestDemo{
    public static void main(String[] args){
        Animal animal=new Dog("二哈");
        Dog dog=(Dog)animal;
        dog.eat();
    }
}

這樣就沒問題啦!

像上述我們提到使用向下轉型的前提是要發生向上轉型。我們其實可以理解為,我們在使用向上轉型的時候,有些功能無法做到,故我們再使用向下轉型來完善代碼(emmm,純屬個人愚見啦)。就比如

// 假設我的 Dog 類中有一個看傢的方法 guard
public class TestDemo{
    public static void main(String[] args){
        Animal animal=new Dog("二哈");
        animal.guard();
    }
}

上述代碼就會報錯,因為 Animal 類中是沒有 guard 方法的。因此我們就要借用向下轉型

public class TestDemo{
    public static void main(String[] args){
        Animal animal=new Dog("二哈");
        Dog dog =animal;
        dog.guard();
    }
}

註意:

其實向下轉型不常使用,使用它可能會不小心犯一些錯誤。如果我們上述的代碼又要繼續使用一些其他動物的特有方法,如果忘瞭它們沒有發生向上轉型,就會報錯。

為瞭避免這種錯誤: 我們可以使用 instanceof

instanceof:可以判定一個引用是否是某個類的實例,如果是則返回 true,不是則返回 false,如

public class TestDemo{
    public static void main(String[] args){
        Animal animal=new Dog("二哈");
        if(animal instanceof Bird){
            Bird bird=(Bird)animal;
            bird.fly();
        }
    }
}

上述代碼就是先判斷 Animal 的引用是否是 Bird 的實例,我們知道它應該是 Dog 的實例,故返回 false

5. 關鍵字 super

其實上章就講解過瞭 super 關鍵字,這裡我再用一個表格比較下 this 和 super,方便理解

6. 在構造方法中調用重寫方法(坑)

接下來我們看一段代碼,大傢可以猜猜結果是啥哦!

class Animal{
    public  String name;
    public Animal(String name){
        eat();
        this.name=name;
    }
    public void eat(){
        System.out.println(this.name+"在吃食物(Animal)");
    }
}
class Dog extends Animal{
    public Dog(String name){
        super(name);
    }
    public void eat(){
        System.out.println(this.name+"在吃食物(Dog)");
    }
}
public class TestDemo{
    public static void main(String[] args){
        Dog dog=new Dog("二哈");
    }
}

結果就是

 

 如果沒猜對的,一般有兩個疑惑:

  • 沒有調用 eat 方法,但為什麼結果是這樣的?
  • 為啥是 null?

解答:

疑惑一: 因為子類繼承父類需要幫父類構造方法,所以子類創建對象時,就構造瞭父類的構造方法,就執行瞭父類的 eat 方法
疑惑二: 由於父類構造方法是先執行 eat 方法,而 name 的賦值在後面一步,多以此時的 name 是 null

結論:

構造方法中可以調用重寫的方法,並且發生瞭動態綁定

7. 理解多態

介紹到這裡,我們終於要開始正式介紹我們今天的一大重點多態瞭!那什麼是多態呢?其實他和繼承一樣是一種思想,我們可以先看一段代碼

class Shape{
    public void draw(){

    }
}
class Cycle extends Shape{
    @Override
    public void draw() {
        System.out.println("畫一個圓⚪");
    }
}
class Rect extends Shape{
    @Override
    public void draw() {
        System.out.println("畫一個方片♦");
    }
}
class Flower extends Shape{
    @Override
    public void draw() {
        System.out.println("畫一朵花❀");
    }
}
public class TestDemo{
    public static void main(String[] args) {
        Cycle shape1=new Cycle();
        Rect shape2=new Rect();
        Flower shape3=new Flower();
        drawMap(shape1);
        drawMap(shape2);
        drawMap(shape3);
    }
    public static void drawMap(Shape shape){
        shape.draw();
    }
}

我們發現 drawMap 這個方法被調用者使用時,都是經過父類調用瞭其中的 draw 方法,並且最終的表現形式是不一樣的。而這種思想就叫做多態。

更簡單的說,多態就是

一個引用能表現出多種不同的形態

而多態是一種思想,實現它的前提有兩點

  • 向上轉型
  • 調用同名的覆蓋方法

而一種思想的傳承總有它獨到的好處,那麼使用多態有什麼好處呢?

1)類調用者對類的使用成本進一步降低

  • 封裝是讓類的調用者不需要知道類的實現細節
  • 多態能讓類的調用者連這個類的類型是什麼都不必知道,隻需要這個對象具有某種方法即可

2)能夠降低代碼的“圈復雜度”,避免使用大量的 if-else 語句

圈復雜度:

是一種描述一段代碼復雜程度的方式。可以將一段代碼中條件語句和循環語句出現的個數看作是“圈復雜度”,這個個數越多,就認為理解起來更復雜。

我們可以看一段代碼

public static void drawShapes(){
    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(); 
   }
    }
}

這段代碼的意思就是要分別打印圓、方片、圓、方片、花,如果不使用多態的話,我們一般就會寫出上面這種方法。而使用多態的話,代碼就會顯得很簡單,如

public static void drawShapes() { 
    // 我們創建瞭一個 Shape 對象的數組. 
    Shape[] shapes = {new Cycle(), new Rect(), new Cycle(), new Rect(), new Flower()}; 
    for (Shape shape : shapes) { 
     shape.draw(); 
    } 
}

我們可以通過下面這種圖理解上面的代碼

 

而整體看起來,使用瞭多態的代碼就簡單瞭很多

3)可擴展能力強

如上述畫圖的代碼,如果我們要新增一種新的形狀,使用多態的方式改動成本也比較低,如

// 增加三角形
class Triangle extends Shape { 
    @Override 
    public void draw() { 
     System.out.println("△"); 
    } 
}

運用多態的話,我們擴展的代碼增加一個新類就可以。而對於不使用多態的情況,就還需要對 if-else 語句進行一定的修改,故改動成本會更高

8. 小結

到此為止,面向對象的三大特點:封裝、繼承、多態已經全部介紹完瞭。由於我個人的理解也有限,所以講的可能不好、不足,希望大傢多多理解呀。

 接下來將會介紹抽象類和接口,其中也會進一步運用到多態,大傢可以多多練習,加深思想的理解。

二、抽象類

1. 概念

我們上面剛寫過一個畫圖型的代碼,其中父類的定義是這樣的

class Shape{
    public void draw(){

    }
}

我們發現,父類中的 draw 方法裡面沒有內容,而繪圖都是通過各種子類的 draw 方法完成的。

像上述代碼,這種沒有實際工作的方法,我們可以通過 abstract 來設計設計成一個抽象方法,而包含抽象方法的類就是抽象類

設計之後的代碼就是這樣的

abstract class Shape{
    public abstract void draw();
}

2. 註意事項

方法和類都要由 abstract 修飾

抽象類中可以定義其他數據成員和成員方法,如

abstract class Shape{
    public int a;
    public void b(){
        // ...
    }
    public abstract void draw();
}

但要使用這些成員和方法,需要靠子類通過 super 才能使用

  • 抽象類不可以被實例化
  • 抽象方法不能是被 private 修飾的
  • 抽象方法不能是被 final 修飾的,它與 abstract 不能被共存
  • 如果子類繼承瞭抽象類,但不需要重寫父類的抽象方法,則可以將子類用 abstract 修飾,如
abstract class Shape{
    public abstract void draw();
}
abstract Color extends Shape{
    
}

此時該子類中既可以定義普通方法也可以定義抽象方法

一個抽象類 A 可以被另外的抽象類 B 繼承,但是如果有其他的普通類繼承瞭抽象類 B,則該普通類需要重寫 A 和 B 中的所有抽象方法

3. 抽象類的意義

我們要知道抽象類的意義就是為瞭被繼承

從註意事項中就知道抽象類本身是不能被實例化的,要想使用它,隻能創建子類去繼承,就比如

abstract class Shape{
    public int a;
    public void b(){
        // ...
    }
    public abstract void draw();
}
class Cycle extends Shape{
    @Override
    public void draw(){
        System.out.println("畫一個⚪");
    }
}
public class TestDemo{
    public static void main(String[] args){
        Shape shape=new Cycle();
    }
}

要註意子類需要重寫父類的所有抽象方法,不然代碼就會報錯

3. 抽象類的作用

那麼抽象類既然不能被實例化,那為什麼要用它呢?

使用瞭抽象類就相當於多瞭一重編譯器的效驗

啥意思呢?就比如按照上述畫圖的代碼,實際工作其實是由子類完成的,如果不小心誤用瞭父類,父類不是抽象類的話是不會報錯的,因此將父類設計成抽象類,它會在父類被實例化的時候報錯,讓我們盡早地發現錯誤

三、接口

我們上面介紹瞭抽象類,抽象類中除瞭抽象方法還可以包含普通的方法和成員。

而接口中也可包含方法和字段,但隻能是抽象方法和靜態常量。

1. 語法規則

我們可以將上述 Shape 改寫成一個 接口,代碼如下

interface IShape{
    public static void draw();
}

具體的語法規則如下:

接口是使用 interface 定義的

接口的命名一般以大寫字母 I 開頭

接口中的方法一定是抽象的、被 public 修飾的方法,因此其中抽象方法可以簡化代碼為

interface IShape{
    void draw();
}

這樣寫默認是 public abstract

接口中也可以包含被 public 修飾的靜態常量,並且可以省略 public static final,如

interface IShape{
    public static final int a=10;
    public static int b=10;
    public int c=10;
    int d=10;
}

接口不能被單獨實例化,和抽象類一樣需要被子類繼承使用,但是接口中使用 implements 繼承,如

interface IShape{
    public static void draw();
}
class Cycle implements IShape{
    @Override
    public void draw(){
        System.out.println("畫一個圓預⚪");
    }
}

extends 表達含義是”擴展“不同,implements 表達的是”實現“,即表示當前什麼都沒有,一切需要從頭構造

基礎接口的類需要重寫接口中的全部抽象方法

一個類可以使用 implements 實現多個接口,每個接口之間使用逗號分隔開就可以,如

interface A{
    void func1();
}
interface B{
    void func2();
}
class C implements A,B{
    @Override
    public void func1(){
        
    }
    @Override
    public void func2{
        
    }
}

註意這個類要重寫所有繼承的接口的所有抽象方法,在 IDEA 中使用 ctrl + i ,快速實現接口

接口和接口之間的關系可以使用 extends 來維護,這是意味著”擴展“,即某個接口擴展瞭其他接口的功能,如

interface A{
    void func1();
}
interface B{
    void func2();
}
interface D implements A,B{
    @Override
    public void func1(){
          
    }
    @Override
    public void func2{
          
    }
    void func3();
}

註意:

在 JDK1.8 開始,接口當中的方法可以是普通方法,但前提是:這個方法是由 default 修飾的(即是這個接口的默認方法),如

interface IShape{
    void draw();
    default public void func(){
        System.out.println("默認方法");
    }
}

2. 實現多個接口

我們之前介紹過,Java 中的繼承是單繼承,即一個類隻能繼承一個父類

但是可以同時實現多個接口,故我們可以通過多接口去達到多繼承類似的效果

接下來通過代碼來理解吧!

class Animal{
    public String name;
    public Animal(String name){
        this.name=name;
    }
}
class Bird extends Animal{
    public Bird(String name){
        super(name);
    }
}

此時子類 Bird 繼承瞭父類 Animal,但是不能再繼承其他類瞭,但是還可以繼續實現其他的接口,如

class Animal{
    public String name;
    public Animal(String name){
        this.name=name;
    }
}
interface ISwing{
    void swing();
}
interface IFly{
    void fly();
}
class Bird extends Animal implements ISwing,IFly{
    public Bird(String name){
        super(name);
    }
    @Override
    public void swing(){
        System.out.println(this.name+"在遊");
    }
    @Override
    public void fly(){
        System.out.println(this.name+"在飛");
    }
}

上述代碼就相當於實現瞭多繼承,因此接口的出現很好的解決瞭 Java 單繼承的問題

並且我們可以感受到,接口表達的好像是具有瞭某種屬性,因此有瞭接口以後,類的使用者就不必關註具體的類型瞭,而隻要關註該類是否具備某個能力,比如

public class TestDemo {
    public static void fly(IFly flying){
        flying.fly();
    }
    public static void main(String[] args) {
        IFly iFly=new Bird("飛鳥");
        fly(iFly);
    }
}

因為飛鳥本身具有飛的屬性,所以我們不必關註具體的類型,因為隻要會飛的都可以實現飛的屬性,如超人也會飛,就可以定義一個超人的類

class SuperMan implements IFly{
    @Override
    public void fly(){
        System.out.println("超人在飛");
    }
}
public class TestDemo {
    public static void fly(IFly flying){
        flying.fly();
    }
    public static void main(String[] args) {
        fly(new SuperMan());
    }
}

註意:

子類先繼承父類再實現接口

3. 接口的繼承

語法規則裡面就介紹瞭,接口和接口之間可以使用 extends 來維護,可以使某個接口擴展其他接口的功能

這裡就不再重述瞭

下面我們再學習一些接口,來加深對於接口的理解

4. Comparable 接口

我們之前介紹過 Arrays 類中的 sort 方法,它可以幫我們進行排序,比如

public class TestDemo {
    public static void main(String[] args) {
        int[] array={2,9,4,1,7};
        System.out.println("排序前:"+Arrays.toString(array));
        Arrays.sort(array);
        System.out.println("排序後:"+Arrays.toString(array));

    }
}

而接下來我想要對一個學生的屬性進行排序。

首先我實現一個 Student 類,並對 toString 方法進行瞭重寫

class Student{
    private String name;
    private int age;
    private double score;

    public Student(String name, int age, double score) {
        this.name = name;
        this.age = age;
        this.score = score;
    }

    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", score=" + score +
                '}';
    }
}

接下來我寫瞭一個數組,並賦予瞭學生數組一些屬性

public class TestDemo {
    public static void main(String[] args) {
        Student[] student=new Student[3];
        student[0]=new Student("張三",18,96.5);
        student[0]=new Student("李四",19,99.5);
        student[0]=new Student("王五",17,92.0);
    }
}

那麼我們可以直接通過 sort 函數進行排序嗎?我們先寫如下代碼

public class TestDemo {
    public static void main(String[] args) {
        Student[] student=new Student[3];
        student[0]=new Student("張三",18,96.5);
        student[1]=new Student("李四",19,99.5);
        student[2]=new Student("王五",17,92.0);
        System.out.println("排序前:"+student);
        Arrays.sort(student);
        System.out.println("排序後:"+student);
    }
}

最終結果卻是

我們來分析一下

ClassCastException:類型轉換異常,說 Student 不能被轉換為 java.lang.Comparable

這是什麼意思呢?我們思考由於 Student 是我們自定義的類型,裡面包含瞭多個類型,那麼 sort 方法怎麼對它進行排序呢?好像沒有一個依據。

此時我通過報錯找到瞭 Comparable

 

可以知道這個應該是一個接口,那我們就可以嘗試將我們的 Student 類繼承這個接口,其中後面的 < T > 其實是泛型的意思,這裡改成 < Student > 就行

class Student implements Comparable<Student>{
    public String name;
    public int age;
    public double score;

    public Student(String name, int age, double score) {
        this.name = name;
        this.age = age;
        this.score = score;
    }

    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", score=" + score +
                '}';
    }
}

但此時還不行,因為繼承需要重寫接口的抽象方法,所以經過查找,我們找到瞭

增加的重寫方法就是

@Override
public int compareTo(Student o) {
    // 新的比較的規則
}

這裡應該就是比較規則的設定地方瞭,我們再看看 sort 方法中的交換

也就是說如果此時的左值大於右值,則進行交換

那麼如果我想對學生的年齡進行排序,重寫後的方法應該就是

@Override
public int compareTo(Student o) {
    // 新的比較的規則
    return this.age-o.age;
}

此時再運行代碼,結果就是

而到這裡我們可以更深刻的感受到,接口其實就是某種屬性或者能力,而上述 Student 這個類繼承瞭這個比較的接口,就擁有瞭比較的能力

缺點:

當我們比較上述代碼的姓名時,就要將重寫的方法改為

@Override
public int compareTo(Student o) {
    // 新的比較的規則
    return this.name.compareTo(o.name);
}

當我們比較上述代碼的分數時,就要將重寫的方法改為

@Override
public int compareTo(Student o) {
    // 新的比較的規則
    return int(this.score-o.score);
}

我們發現當我們要修改比較的東西時,就可能要重新修改重寫的方法。這個局限性就比較大

為瞭解決這個缺陷,就出現瞭下面的接口 Comparator

4. Comparator 接口

我們進入 sort 方法的定義中還可以看到一個比較方法,其中有兩個參數數組與 Comparator 的對象

這裡就用到瞭 Comparator 接口

這個接口啥嘞?我們可以先定義一個年齡比較類 AgeComparator,就是專門用來比較年齡,並讓他繼承這個類

class AgeCompartor implements Comparator<Student>{

}

再通過按住 ctrl 並點擊它,我們可以跳轉到它的定義,此時我們可以發現它裡面有一個方法是

 

這個與上述 Comparable 中的 compareTo 不同,那我先對它進行重寫

class AgeCompartor implements Comparator<Student>{
    @Override
    public int compare(Student o1, Student o2) {
        return o1.age-o2.age;
    }
}

我們再按照 sort 方法的描述,寫如下代碼

public class TestDemo {
    public static void main(String[] args) {
        Student[] student=new Student[3];
        student[0]=new Student("張三",18,96.5);
        student[1]=new Student("李四",19,99.5);
        student[2]=new Student("王五",17,92.0);
        
        System.out.println("排序前:"+Arrays.toString(student));
        AgeComparator ageComparator=new AgeComparator();
        Arrays.sort(student,ageComparator);
        System.out.println("排序後:"+Arrays.toString(student));
    }
}

這樣就可以正常的對學生的年齡進行比較瞭,而此時我們要再對姓名進行排序,我們就可以創建一個姓名比較類 NameComparator

class NameComparator implements Comparator<Student>{
    @Override
    public int compare(Student o1, Student o2) {
        return o1.name.compareTo(o2.name);
    }
}

而我們也隻需要將 sort 方法的參數 ageComparator 改成 nameComparator 就可以瞭

 我們可以將上述 AgeComparator NameComparator 理解成比較器 ,而使用 Comparator 這個接口比 Comparable 的局限性小很多,我們如果要對某個屬性進行比較隻要增加它的比較器即可

5. Cloneable 接口和深拷貝

首先我們可以看這樣的代碼

class Person{
    public String name ="LiXiaobo";

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                '}';
    }
}
public class TestDemo {
    public static void main(String[] args) {
        Person person=new Person();
    }
}

那什麼是克隆呢?應該就是搞一個副本出來,比如

那麼既然這次講 Cloneable 接口,我就對其進行繼承唄!

class Person implements Cloneable{
    public String name ="LiXiaobo";

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                '}';
    }
}

但是我們發現就算繼承之後,我們也不能通過創建的引用去找到一個克隆的方法。此時我們可以點到 Cloneable的定義看看

 

太牛瞭,啥都沒有!

  • 我們發現,Cloneable 這個接口是一個空接口(也叫標記接口),而這個接口的作用就是:如果一個類實現瞭這個接口,就證明它是可以被克隆的
  • 而在使用它之前,我們還需要重寫 Object 的克隆方法(所有的類默認繼承於 Object 類)

怎樣重寫克隆方法呢?通過 ctrl + o,就可以看到

再選擇 clone 就🆗,重寫後的代碼就變成

class Person implements Cloneable{
    public String name ="LiXiaobo";

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                '}';
    }
    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

而此時我們就可以看到一個 clone 方法

點擊之後,我們發現居然還是報錯

 

原因是由於重寫的 clone 方法會拋出異常,針對這個就有兩種方式,今天介紹簡單一點的方式

方式一: 將鼠標放到 clone 上,按住 Alt + enter,你就會看到

點擊紅框框就行,但是你會發現點擊後還是報錯,這是由於重寫的方法的返回值是 Object,而編譯器會認為這是不安全的,因此將它強制轉換成 Person 就可以瞭。此時我們再將克隆的副本輸出發現結果沒問題

並且通過地址的打印,副本和原來的地址是不一樣的

介紹到這裡,簡單的克隆流程就已經介紹完瞭。但是接下來我們再深入一點思考,在 Person 類原有代碼的基礎上增加整形 a

class Person implements Cloneable{
    public String name ="LiXiaobo";
    public int a=10;
    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                '}';
    }
    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

此時我們再通過 person 和 person 分別打印,代碼如下

public class TestDemo3 {
    public static void main(String[] args) throws CloneNotSupportedException {
        Person person=new Person();
        Person person1=(Person)person.clone();
        System.out.println(person.a);
        System.out.println(person1.a);
        System.out.println("#############");
        person1.a=50;
        System.out.println(person.a);
        System.out.println(person1.a);
    }
}

結果如下

 

我們發現這種情況 person1 就完全是一個副本,對它進行修改是與 person 無關的。

但是我們再看下面這種情況,我們定義一個 Money 類,並在 Person 創建它

class Money{
    public int money=10;
}
class Person implements Cloneable{
    public String name ="LiXiaobo";
    public Money money=new Money();
    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                '}';
    }
    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

然後我們再修改 person1 中的 money 的值,代碼如下

public class TestDemo3 {
    public static void main(String[] args) throws CloneNotSupportedException {
        Person person=new Person();
        Person person1=(Person)person.clone();
        System.out.println(person.money.money);
        System.out.println(person1.money.money);
        System.out.println("#############");
        person.money.money=50;
        System.out.println(person.money.money);
        System.out.println(person1.money.money);
    }
}

這次的結果是

 

這是為什麼呢?我們可以分析下面的圖片

由於克隆的是 person 的對象,所以隻克隆瞭(0x123)的 money,而(0x456)的 money 沒有被克隆,所以就算前面的 money 被克隆的副本也指向它,所以改變副本的 money,它也會被改變

而上述這種情況其實叫做淺拷貝,那麼怎麼將其變成深拷貝呢?

我們隻要將 money 引用所指向的對象也克隆一份

步驟:

將 Money 類也實現 Cloneable 接口,並重寫克隆方法

class Money implements Cloneable{
    public int money=10;
    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

修改 Person 中的克隆方法

@Override
protected Object clone() throws CloneNotSupportedException {
    Person personClone=(Person)super.clone();
    personClone.money=(Money)this.money.clone();
    return personClone;
}

到此這篇關於Java語法之 Java 的多態、抽象類和接口的文章就介紹到這瞭,更多相關 Java 的多態、抽象類和接口內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: