主頁 > 後端開發 > 【Java 基礎語法】兩萬字決議 Java 的多型、抽象類和介面

【Java 基礎語法】兩萬字決議 Java 的多型、抽象類和介面

2021-09-21 09:41:21 後端開發

文章目錄

    • 一、多型
      • 1. 向上轉型
      • 2. 動態系結
      • 3. 方法重寫
      • 4. 向下轉型
      • 5. 關鍵字 super
      • 6. 在構造方法中呼叫重寫方法(坑)
      • 7. 理解多型
      • 8. 小結
    • 二、抽象類
      • 1. 概念
      • 2. 注意事項
      • 3. 抽象類的意義
      • 3. 抽象類的作用
    • 三、介面
      • 1. 語法規則
      • 2. 實作多個介面
      • 3. 介面的繼承
      • 4. Comparable 介面
      • 4. Comparator 介面
      • 5. Cloneable 介面和深拷貝
    • 四、總結

上節介紹了 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)
概念方法名稱相同、引數串列不同、回傳值無要求方法名稱相同、引數串列相同、回傳型別一般相同
范圍多載不是必須在一個類當中(繼承)繼承關系
限制沒有權限要求被覆寫的方法不能擁有比父類更嚴格的訪問控制權限

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

在這里插入圖片描述

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

那么什么叫做動態系結呢?發生的條件如下

  1. 發生向上轉型(父類參考需要參考子類物件)
  2. 通過父類參考,來呼叫子類和父類的同名覆寫方法

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

  • 編譯的時候: 呼叫的是父類的方法
  • 但是運行的時候: 實際上呼叫的是子類的方法

因此這其實是一個動態的程序,也可以叫其運行時系結

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,方便理解

區別thissuper
概念訪問本類中的屬性和方法由子類訪問父類中的屬性和方法
查找范圍先查找本類,如果本類沒有就呼叫父類直接呼叫父類
表示表示當前物件
共性1不能被放在 static 修飾的方法中不能被放在 static 修飾的方法中
共性2要放在第一行(不能和 super 一起使用)要放在第一行(不能和 this 一起使用)

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 參考所指向的物件也克隆一份

步驟:

  1. 將 Money 類也實作 Cloneable 介面,并重寫克隆方法

    class Money implements Cloneable{
        public int money=10;
        @Override
        protected Object clone() throws CloneNotSupportedException {
            return super.clone();
        }
    }
    
  2. 修改 Person 中的克隆方法

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

此時便是深拷貝了!

四、總結

以上便是個人對于多型、抽象類和介面的認知了,可能講的不是很好,但是我已經盡力去詮釋了,希望對大家有所幫助!

在這里插入圖片描述

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/301750.html

標籤:java

上一篇:STL六大組件中演算法模塊sort為啥采用快速排序作為底層思想

下一篇:Git學習——Git基本作業原理(入門級教程,通過玩轉Git本地倉庫,幫助新手快速入手Git)

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【C++】Microsoft C++、C 和匯編程式檔案

    ......

    uj5u.com 2020-09-10 00:57:23 more
  • 例外宣告

    相比于斷言適用于排除邏輯上不可能存在的狀態,例外通常是用于邏輯上可能發生的錯誤。 例外宣告 Item 1:當函式不可能拋出例外或不能接受拋出例外時,使用noexcept 理由 如果不打算拋出例外的話,程式就會認為無法處理這種錯誤,并且應當盡早終止,如此可以有效地阻止例外的傳播與擴散。 示例 //不可 ......

    uj5u.com 2020-09-10 00:57:27 more
  • Codeforces 1400E Clear the Multiset(貪心 + 分治)

    鏈接:https://codeforces.com/problemset/problem/1400/E 來源:Codeforces 思路:給你一個陣列,現在你可以進行兩種操作,操作1:將一段沒有 0 的區間進行減一的操作,操作2:將 i 位置上的元素歸零。最終問:將這個陣列的全部元素歸零后操作的最少 ......

    uj5u.com 2020-09-10 00:57:30 more
  • UVA11610 【Reverse Prime】

    本人看到此題沒有翻譯,就附帶了一個自己的翻譯版本 思考 這一題,它的第一個要求是找出所有 $7$ 位反向質數及其質因數的個數。 我們應該需要質數篩篩選1~$10^{7}$的所有數,這里就不慢慢介紹了。但是,重讀題,我們突然發現反向質數都是 $7$ 位,而將它反過來后的數字卻是 $6$ 位數,這就說明 ......

    uj5u.com 2020-09-10 00:57:36 more
  • 統計區間素數數量

    1 #pragma GCC optimize(2) 2 #include <bits/stdc++.h> 3 using namespace std; 4 bool isprime[1000000010]; 5 vector<int> prime; 6 inline int getlist(int ......

    uj5u.com 2020-09-10 00:57:47 more
  • C/C++編程筆記:C++中的 const 變數詳解,教你正確認識const用法

    1、C中的const 1、區域const變數存放在堆疊區中,會分配記憶體(也就是說可以通過地址間接修改變數的值)。測驗代碼如下: 運行結果: 2、全域const變數存放在只讀資料段(不能通過地址修改,會發生寫入錯誤), 默認為外部聯編,可以給其他源檔案使用(需要用extern關鍵字修飾) 運行結果: ......

    uj5u.com 2020-09-10 00:58:04 more
  • 【C++犯錯記錄】VS2019 MFC添加資源不懂如何修改資源宏ID

    1. 首先在資源視圖中,添加資源 2. 點擊新添加的資源,復制自動生成的ID 3. 在解決方案資源管理器中找到Resource.h檔案,編輯,使用整個專案搜索和替換的方式快速替換 宏宣告 4. Ctrl+Shift+F 全域搜索,點擊查找全部,然后逐個替換 5. 為什么使用搜索替換而不使用屬性視窗直 ......

    uj5u.com 2020-09-10 00:59:11 more
  • 【C++犯錯記錄】VS2019 MFC不懂的批量添加資源

    1. 打開資源頭檔案Resource.h,在其中預先定義好宏 ID(不清楚其實ID值應該設定多少,可以先新建一個相同的資源項,再在這個資源的ID值的基礎上遞增即可) 2. 在資源視圖中選中專案資源,按F7編輯資源檔案,按 ID 型別 相對路徑的形式添加 資源。(別忘了先把檔案拷貝到專案中的res檔案 ......

    uj5u.com 2020-09-10 01:00:19 more
  • C/C++編程筆記:關于C++的參考型別,專供新手入門使用

    今天要講的是C++中我最喜歡的一個用法——參考,也叫別名。 參考就是給一個變數名取一個變數名,方便我們間接地使用這個變數。我們可以給一個變數創建N個參考,這N + 1個變數共享了同一塊記憶體區域。(參考型別的變數會占用記憶體空間,占用的記憶體空間的大小和指標型別的大小是相同的。雖然參考是一個物件的別名,但 ......

    uj5u.com 2020-09-10 01:00:22 more
  • 【C/C++編程筆記】從頭開始學習C ++:初學者完整指南

    眾所周知,C ++的學習曲線陡峭,但是花時間學習這種語言將為您的職業帶來奇跡,并使您與其他開發人員區分開。您會更輕松地學習新語言,形成真正的解決問題的技能,并在編程的基礎上打下堅實的基礎。 C ++將幫助您養成良好的編程習慣(即清晰一致的編碼風格,在撰寫代碼時注釋代碼,并限制類內部的可見性),并且由 ......

    uj5u.com 2020-09-10 01:00:41 more
最新发布
  • Rust中的智能指標:Box<T> Rc<T> Arc<T> Cell<T> RefCell<T> Weak

    Rust中的智能指標是什么 智能指標(smart pointers)是一類資料結構,是擁有資料所有權和額外功能的指標。是指標的進一步發展 指標(pointer)是一個包含記憶體地址的變數的通用概念。這個地址參考,或 ” 指向”(points at)一些其 他資料 。參考以 & 符號為標志并借用了他們所 ......

    uj5u.com 2023-04-20 07:24:10 more
  • Java的值傳遞和參考傳遞

    值傳遞不會改變本身,參考傳遞(如果傳遞的值需要實體化到堆里)如果發生修改了會改變本身。 1.基本資料型別都是值傳遞 package com.example.basic; public class Test { public static void main(String[] args) { int ......

    uj5u.com 2023-04-20 07:24:04 more
  • [2]SpinalHDL教程——Scala簡單入門

    第一個 Scala 程式 shell里面輸入 $ scala scala> 1 + 1 res0: Int = 2 scala> println("Hello World!") Hello World! 檔案形式 object HelloWorld { /* 這是我的第一個 Scala 程式 * 以 ......

    uj5u.com 2023-04-20 07:23:58 more
  • 理解函式指標和回呼函式

    理解 函式指標 指向函式的指標。比如: 理解函式指標的偽代碼 void (*p)(int type, char *data); // 定義一個函式指標p void func(int type, char *data); // 宣告一個函式func p = func; // 將指標p指向函式func ......

    uj5u.com 2023-04-20 07:23:52 more
  • Django筆記二十五之資料庫函式之日期函式

    本文首發于公眾號:Hunter后端 原文鏈接:Django筆記二十五之資料庫函式之日期函式 日期函式主要介紹兩個大類,Extract() 和 Trunc() Extract() 函式作用是提取日期,比如我們可以提取一個日期欄位的年份,月份,日等資料 Trunc() 的作用則是截取,比如 2022-0 ......

    uj5u.com 2023-04-20 07:23:45 more
  • 一天吃透JVM面試八股文

    什么是JVM? JVM,全稱Java Virtual Machine(Java虛擬機),是通過在實際的計算機上仿真模擬各種計算機功能來實作的。由一套位元組碼指令集、一組暫存器、一個堆疊、一個垃圾回收堆和一個存盤方法域等組成。JVM屏蔽了與作業系統平臺相關的資訊,使得Java程式只需要生成在Java虛擬機 ......

    uj5u.com 2023-04-20 07:23:31 more
  • 使用Java接入小程式訂閱訊息!

    更新完微信服務號的模板訊息之后,我又趕緊把微信小程式的訂閱訊息給實作了!之前我一直以為微信小程式也是要企業才能申請,沒想到小程式個人就能申請。 訊息推送平臺🔥推送下發【郵件】【短信】【微信服務號】【微信小程式】【企業微信】【釘釘】等訊息型別。 https://gitee.com/zhongfuch ......

    uj5u.com 2023-04-20 07:22:59 more
  • java -- 緩沖流、轉換流、序列化流

    緩沖流 緩沖流, 也叫高效流, 按照資料型別分類: 位元組緩沖流:BufferedInputStream,BufferedOutputStream 字符緩沖流:BufferedReader,BufferedWriter 緩沖流的基本原理,是在創建流物件時,會創建一個內置的默認大小的緩沖區陣列,通過緩沖 ......

    uj5u.com 2023-04-20 07:22:49 more
  • Java-SpringBoot-Range請求頭設定實作視頻分段傳輸

    老實說,人太懶了,現在基本都不喜歡寫筆記了,但是網上有關Range請求頭的文章都太水了 下面是抄的一段StackOverflow的代碼...自己大修改過的,寫的注釋挺全的,應該直接看得懂,就不解釋了 寫的不好...只是希望能給視頻網站開發的新手一點點幫助吧. 業務場景:視頻分段傳輸、視頻多段傳輸(理 ......

    uj5u.com 2023-04-20 07:22:42 more
  • Windows 10開發教程_編程入門自學教程_菜鳥教程-免費教程分享

    教程簡介 Windows 10開發入門教程 - 從簡單的步驟了解Windows 10開發,從基本到高級概念,包括簡介,UWP,第一個應用程式,商店,XAML控制元件,資料系結,XAML性能,自適應設計,自適應UI,自適應代碼,檔案管理,SQLite資料庫,應用程式到應用程式通信,應用程式本地化,應用程式 ......

    uj5u.com 2023-04-20 07:22:35 more