首頁 > 軟體

Java入門基礎之抽象類與介面

2022-02-11 13:02:20

一.抽象類

1.什麼是抽象類

首先我們來回顧一下上一篇文章提到的一個例子:列印圖形

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 Test { 
 	public static void main(String[] args) { 
 		Shape shape1 = new Flower(); 
 		Shape shape2 = new Cycle(); 
 		Shape shape3 = new Rect(); 
 		drawMap(shape1); 
 		drawMap(shape2); 
 		drawMap(shape3); 
 	} 
 	// 列印單個圖形
 	public static void drawShape(Shape shape) { 
 		shape.draw(); 
 	} 
}

我們發現, 父類別 Shape 中的 draw 方法好像並沒有什麼實際工作,主要的繪製圖形都是由 Shape 的各種子類的 draw 方法來完成的。
像這種沒有實際工作的方法, 我們可以把它設計成一個 抽象方法(abstractmethod),包含抽象方法的類我們稱為 抽象類(abstract class)

2.語法規則

那麼,抽象類到底怎麼寫呢?請看程式碼:

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

在 draw 方法前加上 abstract 關鍵字, 表示這是一個抽象方法。 同時抽象方法沒有方法體(沒有 { },不能執行具體程式碼)

對於包含抽象方法的類, 必須加上 abstract 關鍵字表示這是一個抽象類

注意事項:

抽象類不能直接範例化:

Shape shape = new Shape(); 
// 編譯出錯
Error:(30, 23) java: Shape是抽象的; 無法範例化

抽象方法不能是 private 的:

abstract class Shape { 
 	abstract private void draw(); 
} 
// 編譯出錯
Error:(4, 27) java: 非法的修飾符組合: abstract和private

抽象類中可以包含其他的非抽象方法,也可以包含欄位。這個非抽象方法和普通方法的規則都是一樣的,可以被重寫,也可以被子類直接呼叫:

abstract class Shape { 
 	abstract public void draw(); 
 	void func() { 
 		System.out.println("func"); 
 	} 
} 
class Rect extends Shape { 
 
} 
public class Test { 
 	public static void main(String[] args) { 
 		Shape shape = new Rect(); 
 		shape.func(); 
 	} 
} 
// 執行結果
func

3.抽象類的作用

抽象類存在的最大意義就是為了被繼承

抽象類本身不能被範例化,要想使用,只能建立該抽象類的子類,然後讓子類重寫抽象類中的抽象方法。

那大家可能有一個疑問,普通的類也可以被繼承, 普通的方法也可以被重寫呀,為啥非得用抽象類和抽象方法呢?

確實如此,但是使用抽象類相當於多了一重編譯器的校驗:

使用抽象類的場景就如上面的程式碼, 實際工作不應該由父類別完成, 而應由子類完成。

那麼此時如果不小心誤用成父類別了,使用普通類編譯器是不會報錯的。 但是父類別是抽象類就會在範例化的時候提示錯誤,讓我們儘早發現問題。

很多語法存在的意義都是為了 “預防出錯”,例如我們曾經用過的 final 也是類似。 建立的變數使用者不去修改, 不就相當於常數嘛? 但是加上 final 能夠在不小心誤修改的時候,讓編譯器及時提醒我們。
充分利用編譯器的校驗, 在實際開發中是非常有意義的。

二.介面

1.什麼是介面

介面是抽象類的更進一步。抽象類中還可以包含 非抽象方法 和欄位。而介面中包含的方法都是抽象方法, 欄位只能包含靜態常數

2.語法規則

在剛才的列印圖形的範例中,我們的父類別 Shape 並沒有包含別的非抽象方法,也可以設計成一個介面:

interface IShape { 
 	void draw(); 
} 
class Cycle implements IShape { 
 	@Override 
 	public void draw() { 
 		System.out.println("○"); 
 	} 
} 
public class Test { 
 	public static void main(String[] args) { 
 		IShape shape = new Rect(); 
 		shape.draw(); 
 	} 
}
  • 使用 interface 定義一個介面
  • 介面中的方法一定是抽象方法,因此可以省略 abstract
  • 介面中的方法一定是 public ,因此可以省略 public
  • Cycle 使用 implements 繼承介面。此時表達的含義不再是 “擴充套件”, 而是 “實現”
  • 在呼叫的時候同樣可以建立一個介面的參照,對應到一個子類的範例
  • 介面不能單獨被範例化
  • 從jdk1.8開始,介面中的普通方法可以有具體實現,但這個方法必須是default修飾的。

擴充套件(extends) vs 實現(implements):

  • 擴充套件指的是當前已經有一定的功能了,進一步擴充功能
  • 實現指的是當前啥都沒有,需要從頭構造出來

注意事項:

介面中只能包含抽象方法。 對於欄位來說, 介面中只能包含靜態常數(final static):

interface IShape { 
 	void draw(); 
 	public static final int num = 10; 
}

其中的 public, static, final 的關鍵字都可以省略.省略後的 num 仍然表示 public 的靜態常數

總結:

  • 我們建立介面的時候, 介面的命名一般以大寫字母 I 開頭
  • 介面的命名一般使用 “形容詞” 詞性的單詞
  • 阿里編碼規範中約定,介面中的方法和屬性不要加任何修飾符號,保持程式碼的簡潔性

一段易錯的程式碼:

interface IShape { 
 	abstract void draw() ; // 即便不寫public,也是public 
} 
class Rect implements IShape { 
 	void draw() { 
 		System.out.println("□") ; //許可權更加嚴格了,所以無法重寫
 	} 
}

3.實現多個介面

有的時候我們需要讓一個類同時繼承多個父類別。這件事情在有些程式語言通過 多繼承 的方式來實現的。

然而 Java 中只支援單繼承, 一個類只能 extends 一個父類別。但是可以同時實現多個介面 ,也能達到多繼承類似的效果。

現在我們通過類來表示一組動物:

class Animal { 
 	protected String name; 
 
 	public Animal(String name) { 
 		this.name = name; 
 	} 
}

另外我們再提供一組介面,分別表示 “會飛的” “會跑的” “會游泳的” :

interface IFlying { 
 	void fly(); 
} 
interface IRunning { 
 	void run(); 
} 
interface ISwimming { 
 	void swim(); 
}

接下來我們建立幾個具體的動物

貓,是會跑的 :

class Cat extends Animal implements IRunning { 
 	public Cat(String name) { 
 		super(name); 
 	} 
 	@Override 
 	public void run() { 
 		System.out.println(this.name + "正在用四條腿跑"); 
 	} 
}

魚,是會遊的 :

class Fish extends Animal implements ISwimming { 
 	public Fish(String name) { 
 		super(name); 
 	} 
 	@Override 
 	public void swim() { 
 		System.out.println(this.name + "正在用尾巴游泳"); 
 	} 
}

青蛙,既能跑,又能遊 :

class Frog extends Animal implements IRunning, ISwimming { 
 	public Frog(String name) { 
 		super(name); 
 	} 
 	@Override 
 	public void run() { 
 		System.out.println(this.name + "正在往前跳"); 
 	} 
 	@Override 
 	public void swim() { 
 		System.out.println(this.name + "正在蹬腿游泳"); 
 	} 
}

PS : IDEA 中使用 ctrl + i 快速實現介面

還有一種神奇的動物,水陸空三棲,叫做 “鴨子” :

class Duck extends Animal implements IRunning, ISwimming, IFlying { 
 	public Duck(String name) { 
 		super(name); 
 	} 
 	@Override 
 	public void fly() { 
 		System.out.println(this.name + "正在用翅膀飛"); 
 	} 
 	@Override 
 	public void run() { 
 		System.out.println(this.name + "正在用兩條腿跑"); 
 	} 
 	@Override 
 	public void swim() { 
 		System.out.println(this.name + "正在漂在水上"); 
 	} 
}

上面的程式碼展示了 Java 物件導向程式設計中最常見的用法 : 一個類繼承一個父類別,同時實現多種介面

繼承表達的含義是 is - a 語意,而介面表達的含義是 具有 xxx 特性

貓是一種動物,具有會跑的特性
青蛙也是一種動物,既能跑,也能游泳
鴨子也是一種動物, 既能跑, 也能遊,還能飛

這樣設計有什麼好處呢?

時刻牢記多型的好處,讓我們忘記型別.有了介面之後,類的使用者就不必關注具體型別, 而只關注某個類是否具備某種能力

例如, 現在實現一個方法, 叫 “散步”:

public static void walk(IRunning running) { 
 	System.out.println("我帶著夥伴去散步"); 
 	running.run(); 
}

在這個 walk 方法內部,我們並不關注到底是哪種動物,只要引數是會跑的, 就行:

Cat cat = new Cat("小貓"); 
walk(cat); 
Frog frog = new Frog("小青蛙"); 
walk(frog); 
// 執行結果
我帶著夥伴去散步
小貓正在用四條腿跑
我帶著夥伴去散步
小青蛙正在往前跳

甚至引數可以不是 “動物”,只要會跑!

class Robot implements IRunning { 
 	private String name; 
 	public Robot(String name) { 
 		this.name = name; 
 	} 
 	@Override 
 	public void run() { 
 		System.out.println(this.name + "正在用輪子跑"); 
 	} 
} 
Robot robot = new Robot("機器人"); 
walk(robot); 
// 執行結果
機器人正在用輪子跑

4.介面之間的繼承

介面可以繼承一個介面,達到複用的效果.使用 extends 關鍵字:

interface IRunning { 
 	void run(); 
} 
interface ISwimming { 
 	void swim(); 
} 
// 兩棲的動物, 既能跑, 也能遊
interface IAmphibious extends IRunning, ISwimming { 

} 
class Frog implements IAmphibious { 
 
}

通過介面繼承建立一個新的介面 IAmphibious 表示 “兩棲的”

此時實現介面建立的 Frog 類, 就繼續要實現 run 方法,也需要實現 swim 方法,介面間的繼承相當於把多個介面合併在一起

三.介面的使用範例

1. Comparable 介面

剛才的例子比較抽象, 我們再來一個更能實際的例子,給物件陣列排序 :

給定一個學生類

class Student { 
 	private String name; 
 	private int score; 
 	public Student(String name, int score) { 
 		this.name = name; 
 		this.score = score; 
 	} 

 	@Override 
 	public String toString() { 
 		return "[" + this.name + ":" + this.score + "]"; 
 	} 
}

再給定一個學生物件陣列, 對這個物件陣列中的元素進行排序(按分數降序):

Student[] students = new Student[] { 
 new Student("張三", 95), 
 new Student("李四", 96), 
 new Student("王五", 97), 
 new Student("趙六", 92), 
};

按照我們之前的理解, 陣列我們有一個現成的 sort 方法,我們來試試能否直接用sort方法進行排序:

仔細思考, 不難發現學生和普通的整數不一樣, 兩個整數是可以直接比較的, 大小關係明確. 而兩個學生物件的大小關係怎麼確定? 需要我們額外指定

讓我們的 Student 類實現 Comparable 介面, 並實現其中的 compareTo 方法:

class Student implements Comparable { 
 	private String name; 
 	private int score; 
 	public Student(String name, int score) { 
 		this.name = name; 
 		this.score = score; 
 	} 
 	@Override 
 	public String toString() { 
 		return "[" + this.name + ":" + this.score + "]"; 
 	} 
 	@Override 
 	public int compareTo(Object o) { 
 		Student s = (Student)o; 
 		if (this.score > s.score) { 
 			return -1; 
 		} else if (this.score < s.score) { 
 			return 1; 
 		} else { 
 			return 0; 
 		} 
 	} 
}

在 sort 方法中會自動呼叫 compareTo 方法. compareTo 的引數是 Object , 其實傳入的就是 Student 型別的物件

然後比較當前物件和引數物件的大小關係(按分數來算):

  • 如果當前物件應排在引數物件之前, 返回小於 0 的數位
  • 如果當前物件應排在引數物件之後, 返回大於 0 的數位
  • 如果當前物件和引數物件不分先後, 返回 0

我們再次執行一下:


這時候結果就符合我們預期了( ̄▽ ̄)*

compareTo其實就是一個比較規則 , 如果我們想自定義比較型別的話 , 一定要實現可以比較的介面 . 但是 , Comparable介面有個很大的缺點 , 那就是對類的侵入性很強 , 所以我們一般不輕易改動

2.Comparator介面

剛才我們提到了Comparable介面對類的侵入性很強 , 那麼有沒有一個比較靈活的介面供我們使用呢? 答案是肯定的 , 那就是Comparator介面

我們先來寫一個用年齡進行比較的比較器:

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

再來寫一個用姓名進行比較的比較器:

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

這時候,我們範例化這兩個比較器,並且在sort方法中傳入要排列的陣列和我們寫的比較器物件 :

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

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

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

    public static void main(String[] args) {
        Student[] students = new Student[3];
        students[0] = new Student(12,"af");
        students[1] = new Student(6,"be");
        students[2] = new Student(18,"zhangsan");

        System.out.println("按年齡排序:");
        AgeComparator ageComparator = new AgeComparator();
        Arrays.sort(students,ageComparator);
        System.out.println(Arrays.toString(students));
        System.out.println("---------------------------");
        System.out.println("按姓名排序:");
        NameComparator nameComparator = new NameComparator();
        Arrays.sort(students,nameComparator);
        System.out.println(Arrays.toString(students));
    }
}

執行結果:

所以 Comparator介面 只需要根據自己的需求重新寫比較器就 ok 了, 靈活很多, 而不是像Comparable介面直接就寫死了

3.Clonable介面

Java 中內建了一些很有用的介面 , Clonable 就是其中之一

Object 類中存在一個 clone 方法, 呼叫這個方法可以建立一個物件的 “拷貝”. 但是要想合法呼叫 clone 方法, 必須要先實現 Clonable 介面, 否則就會丟擲 CloneNotSupportedException 異常

實現Clonable介面
別忘了要丟擲異常
重寫Object的clone方法

我們來看一個例子 :

class Person implements Cloneable{
    public int age;

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

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

public class TestDemo {
    public static void main(String[] args) throws CloneNotSupportedException{
        Person person = new Person();
        person.age = 99;
        Person person2 = (Person) person.clone();
        System.out.println(person2);
    }
}

執行結果:

此時記憶體如下:

這時候,我們再來加一個Money類,並且在Person類中範例化它:

class Money implements Cloneable{
    public double m = 12.5;
    }
}
class Person implements Cloneable{
    public int age;
    public Money money = new Money();

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

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

我們在person2中拷貝一份money的值,這時候修改person2中的money,那麼person1的money是否改變呢?

public class TestDemo {
    
    public static void main(String[] args) throws CloneNotSupportedException{
        Person person = new Person();
        Person person2 = (Person) person.clone();
        System.out.println(person.money.m);
        System.out.println(person2.money.m);
        System.out.println("-------------------------");
        person2.money.m = 13.5;
        System.out.println(person.money.m);
        System.out.println(person2.money.m);
    }
}

答案是不會改變!

那麼是否說明Clonable介面就是隻能實現淺拷貝呢?

答案也是否 , 決定深淺拷貝的並不是 方法的用途 , 而是程式碼的實現 !

我們來看看此時的記憶體分佈圖:

要想實現深拷貝,我們拷貝person的時候就要把person物件裡的money也拷貝一份,讓person2的money指向 新拷貝出來的money ,這時候咱們就實現了深拷貝

具體的操作實現只需要將Money類重寫clone方法(方便克隆),然後將Person中的clone方法進行修改 ,將money也進行拷貝即可

具體程式碼如下 :

class Money implements Cloneable{
    public double m = 12.5;

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

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

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

我們來測試一下 :

public class TestDemo {

    public static void main(String[] args) throws CloneNotSupportedException{
        Person person = new Person();
        Person person2 = (Person) person.clone();
        System.out.println(person.money.m);
        System.out.println(person2.money.m);
        System.out.println("-------------------------");
        person2.money.m = 13.5;
        System.out.println(person.money.m);
        System.out.println(person2.money.m);
    }

這樣就成功實現了深拷貝 !

四.總結

抽象類和介面都是 Java 中多型的常見使用方式

抽象類中可以包含普通方法和普通欄位, 這樣的普通方法和欄位可以被子類直接使用(不必重寫), 而介面中不能包含普通方法, 子類必須重寫所有的抽象方法

到此這篇關於Java入門基礎之抽象類與介面的文章就介紹到這了,更多相關Java抽象類與介面內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com