Java入門基礎之抽象類與接口
一.抽象類
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(); } }
我們發(fā)現(xiàn), 父類 Shape 中的 draw 方法好像并沒有什么實際工作,主要的繪制圖形都是由 Shape 的各種子類的 draw 方法來完成的。
像這種沒有實際工作的方法, 我們可以把它設計成一個 抽象方法(abstractmethod),包含抽象方法的類我們稱為 抽象類(abstract class)
2.語法規(guī)則
那么,抽象類到底怎么寫呢?請看代碼:
abstract class Shape { abstract public void draw(); }
在 draw 方法前加上 abstract 關鍵字, 表示這是一個抽象方法。 同時抽象方法沒有方法體(沒有 { },不能執(zhí)行具體代碼)
對于包含抽象方法的類, 必須加上 abstract 關鍵字表示這是一個抽象類
注意事項:
抽象類不能直接實例化:
Shape shape = new Shape(); // 編譯出錯 Error:(30, 23) java: Shape是抽象的; 無法實例化
抽象方法不能是 private 的:
abstract class Shape { abstract private void draw(); } // 編譯出錯 Error:(4, 27) java: 非法的修飾符組合: abstract和private
抽象類中可以包含其他的非抽象方法,也可以包含字段。這個非抽象方法和普通方法的規(guī)則都是一樣的,可以被重寫,也可以被子類直接調用:
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(); } } // 執(zhí)行結果 func
3.抽象類的作用
抽象類存在的最大意義就是為了被繼承
抽象類本身不能被實例化,要想使用,只能創(chuàng)建該抽象類的子類,然后讓子類重寫抽象類中的抽象方法。
那大家可能有一個疑問,普通的類也可以被繼承, 普通的方法也可以被重寫呀,為啥非得用抽象類和抽象方法呢?
確實如此,但是使用抽象類相當于多了一重編譯器的校驗:
使用抽象類的場景就如上面的代碼, 實際工作不應該由父類完成, 而應由子類完成。
那么此時如果不小心誤用成父類了,使用普通類編譯器是不會報錯的。 但是父類是抽象類就會在實例化的時候提示錯誤,讓我們盡早發(fā)現(xiàn)問題。
很多語法存在的意義都是為了 “預防出錯”,例如我們曾經用過的 final 也是類似。 創(chuàng)建的變量用戶不去修改, 不就相當于常量嘛? 但是加上 final 能夠在不小心誤修改的時候,讓編譯器及時提醒我們。
充分利用編譯器的校驗, 在實際開發(fā)中是非常有意義的。
二.接口
1.什么是接口
接口是抽象類的更進一步。抽象類中還可以包含 非抽象方法 和字段。而接口中包含的方法都是抽象方法, 字段只能包含靜態(tài)常量
2.語法規(guī)則
在剛才的打印圖形的示例中,我們的父類 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 繼承接口。此時表達的含義不再是 “擴展”, 而是 “實現(xiàn)”
- 在調用的時候同樣可以創(chuàng)建一個接口的引用,對應到一個子類的實例
- 接口不能單獨被實例化
- 從jdk1.8開始,接口中的普通方法可以有具體實現(xiàn),但這個方法必須是default修飾的。
擴展(extends) vs 實現(xiàn)(implements):
- 擴展指的是當前已經有一定的功能了,進一步擴充功能
- 實現(xiàn)指的是當前啥都沒有,需要從頭構造出來
注意事項:
接口中只能包含抽象方法。 對于字段來說, 接口中只能包含靜態(tài)常量(final static):
interface IShape { void draw(); public static final int num = 10; }
其中的 public, static, final 的關鍵字都可以省略.省略后的 num 仍然表示 public 的靜態(tài)常量
總結:
- 我們創(chuàng)建接口的時候, 接口的命名一般以大寫字母 I 開頭
- 接口的命名一般使用 “形容詞” 詞性的單詞
- 阿里編碼規(guī)范中約定,接口中的方法和屬性不要加任何修飾符號,保持代碼的簡潔性
一段易錯的代碼:
interface IShape { abstract void draw() ; // 即便不寫public,也是public } class Rect implements IShape { void draw() { System.out.println("□") ; //權限更加嚴格了,所以無法重寫 } }
3.實現(xiàn)多個接口
有的時候我們需要讓一個類同時繼承多個父類。這件事情在有些編程語言通過 多繼承 的方式來實現(xiàn)的。
然而 Java 中只支持單繼承, 一個類只能 extends 一個父類。但是可以同時實現(xiàn)多個接口 ,也能達到多繼承類似的效果。
現(xiàn)在我們通過類來表示一組動物:
class Animal { protected String name; public Animal(String name) { this.name = name; } }
另外我們再提供一組接口,分別表示 “會飛的” “會跑的” “會游泳的” :
interface IFlying { void fly(); } interface IRunning { void run(); } interface ISwimming { void swim(); }
接下來我們創(chuàng)建幾個具體的動物
貓,是會跑的 :
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 快速實現(xiàn)接口
還有一種神奇的動物,水陸空三棲,叫做 “鴨子” :
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 面向對象編程中最常見的用法 : 一個類繼承一個父類,同時實現(xiàn)多種接口
繼承表達的含義是 is - a
語義,而接口表達的含義是 具有 xxx 特性
貓是一種動物,具有會跑的特性
青蛙也是一種動物,既能跑,也能游泳
鴨子也是一種動物, 既能跑, 也能游,還能飛
這樣設計有什么好處呢?
時刻牢記多態(tài)的好處,讓我們忘記類型.有了接口之后,類的使用者就不必關注具體類型, 而只關注某個類是否具備某種能力
例如, 現(xiàn)在實現(xiàn)一個方法, 叫 “散步”:
public static void walk(IRunning running) { System.out.println("我?guī)е锇槿ド⒉?); running.run(); }
在這個 walk 方法內部,我們并不關注到底是哪種動物,只要參數(shù)是會跑的, 就行:
Cat cat = new Cat("小貓"); walk(cat); Frog frog = new Frog("小青蛙"); walk(frog); // 執(zhí)行結果 我?guī)е锇槿ド⒉? 小貓正在用四條腿跑 我?guī)е锇槿ド⒉? 小青蛙正在往前跳
甚至參數(shù)可以不是 “動物”,只要會跑!
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); // 執(zhí)行結果 機器人正在用輪子跑
4.接口之間的繼承
接口可以繼承一個接口,達到復用的效果.使用 extends 關鍵字:
interface IRunning { void run(); } interface ISwimming { void swim(); } // 兩棲的動物, 既能跑, 也能游 interface IAmphibious extends IRunning, ISwimming { } class Frog implements IAmphibious { }
通過接口繼承創(chuàng)建一個新的接口 IAmphibious 表示 “兩棲的”
此時實現(xiàn)接口創(chuàng)建的 Frog 類, 就繼續(xù)要實現(xiàn) run 方法,也需要實現(xiàn) swim 方法,接口間的繼承相當于把多個接口合并在一起
三.接口的使用實例
1. Comparable 接口
剛才的例子比較抽象, 我們再來一個更能實際的例子,給對象數(shù)組排序 :
給定一個學生類
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 + "]"; } }
再給定一個學生對象數(shù)組, 對這個對象數(shù)組中的元素進行排序(按分數(shù)降序):
Student[] students = new Student[] { new Student("張三", 95), new Student("李四", 96), new Student("王五", 97), new Student("趙六", 92), };
按照我們之前的理解, 數(shù)組我們有一個現(xiàn)成的 sort 方法,我們來試試能否直接用sort方法進行排序:
仔細思考, 不難發(fā)現(xiàn)學生和普通的整數(shù)不一樣, 兩個整數(shù)是可以直接比較的, 大小關系明確. 而兩個學生對象的大小關系怎么確定? 需要我們額外指定
讓我們的 Student 類實現(xiàn) Comparable 接口, 并實現(xiàn)其中的 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 的參數(shù)是 Object , 其實傳入的就是 Student 類型的對象
然后比較當前對象和參數(shù)對象的大小關系(按分數(shù)來算):
- 如果當前對象應排在參數(shù)對象之前, 返回小于 0 的數(shù)字
- 如果當前對象應排在參數(shù)對象之后, 返回大于 0 的數(shù)字
- 如果當前對象和參數(shù)對象不分先后, 返回 0
我們再次執(zhí)行一下:
這時候結果就符合我們預期了( ̄▽ ̄)*
compareTo其實就是一個比較規(guī)則 , 如果我們想自定義比較類型的話 , 一定要實現(xiàn)可以比較的接口 . 但是 , 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方法中傳入要排列的數(shù)組和我們寫的比較器對象 :
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 方法, 調用這個方法可以創(chuàng)建一個對象的 “拷貝”. 但是要想合法調用 clone 方法, 必須要先實現(xiàn) Clonable 接口, 否則就會拋出 CloneNotSupportedException 異常
實現(xiàn)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接口就是只能實現(xiàn)淺拷貝呢?
答案也是否 , 決定深淺拷貝的并不是 方法的用途 , 而是代碼的實現(xiàn) !
我們來看看此時的內存分布圖:
要想實現(xiàn)深拷貝,我們拷貝person的時候就要把person對象里的money也拷貝一份,讓person2的money指向 新拷貝出來的money ,這時候咱們就實現(xiàn)了深拷貝
具體的操作實現(xiàn)只需要將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); }
這樣就成功實現(xiàn)了深拷貝 !
四.總結
抽象類和接口都是 Java 中多態(tài)的常見使用方式
抽象類中可以包含普通方法和普通字段, 這樣的普通方法和字段可以被子類直接使用(不必重寫), 而接口中不能包含普通方法, 子類必須重寫所有的抽象方法
到此這篇關于Java入門基礎之抽象類與接口的文章就介紹到這了,更多相關Java抽象類與接口內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
SpringBoot中的multipartResolver上傳文件配置
這篇文章主要介紹了SpringBoot中的multipartResolver上傳文件配置,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-10-10Spring需要三個級別緩存解決循環(huán)依賴原理解析
這篇文章主要為大家介紹了Spring需要三個級別緩存解決循環(huán)依賴原理解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-02-02