抽象類
抽象類概念
在面向對象的概念中,所有的對象都是通過類來描繪的,但是反過來,并不是所有的類都是用來描繪對象的,如果 一個類中沒有包含足夠的信息來描繪一個具體的對象,這樣的類就是抽象類。 比如:
?
?
在打印圖形例子中, 我們發現, 父類 Shape 中的 draw 方法好像并沒有什么實際工作, 主要的繪制圖形都是由 Shape 的各種子類的 draw 方法來完成的. 像這種沒有實際工作的方法, 我們可以把它設計成一個 抽象方法(abstract method), 包含抽象方法的類我們稱為 抽象類(abstract class).?
抽象類語法?
在Java中,一個類如果被?abstract 修飾稱為抽象類,抽象類中被 abstract 修飾的方法稱為抽象方法,抽象方法不用給出具體的實現體。
// 抽象類:被abstract修飾的類 public abstract class Shape {// 抽象方法:被abstract修飾的方法,沒有方法體 abstract public void draw(); abstract void calcArea();// 抽象類也是類,也可以增加普通方法和屬性 public double getArea(){ return area; }protected double area; // 面積}
注意:抽象類也是類,內部可以包含普通方法和屬性,甚至構造方法
抽象類特性
1. 抽象類不能直接實例化對象
Shape shape = new Shape();// 編譯出錯Error:(30, 23) java: Shape是抽象的; 無法實例化
2. 抽象方法不能是 private 的
abstract class Shape {abstract private void draw();}// 編譯出錯Error:(4, 27) java: 非法的修飾符組合: abstract和private
3. 抽象方法不能被?nal和static修飾,因為抽象方法要被子類重寫
public abstract class Shape {abstract ?nal void methodA();abstract public static void methodB();}// 編譯報錯:// Error:(20, 25) java: 非法的修飾符組合: abstract和?nal// Error:(21, 33) java: 非法的修飾符組合: abstract和static
4.抽象類必須被繼承,并且繼承后子類要重寫父類中的抽象方法,否則子類也是抽象類,必須要使用修飾abstract
// 矩形類public class Rect extends Shape {private double length;private double width;Rect(double length, double width){this.length = length;this.width = width;}public void draw(){System.out.println("矩形: length= "+length+" width= " + width);}public void calcArea(){area = length * width;}}// 圓類:public class Circle extends Shape{private double r;?nal private static double PI = 3.14;public Circle(double r){this.r = r;}public void draw(){System.out.println("圓:r = "+r); }public void calcArea(){area = PI * r * r;}}// 三角形類:public abstract class Triangle extends Shape {private double a;private double b;private double c;@Overridepublic void draw() {System.out.println("三角形:a = "+a + " b = "+b+" c = "+c);}// 三角形:直角三角形、等腰三角形等,還可以繼續細化//@Override//double calcArea(); // 編譯失敗:要么實現該抽象方法,要么將三角形設計為抽象類}
5. 抽象類中不一定包含抽象方法,但是有抽象方法的類一定是抽象類
6. 抽象類中可以有構造方法,供子類創建對象時,初始化父類的成員變量
抽象類的作用
抽象類本身不能被實例化, 要想使用, 只能創建該抽象類的子類. 然后讓子類重寫抽象類中的抽象方法.
有些同學可能會說了, 普通的類也可以被繼承呀, 普通的方法也可以被重寫呀, 為啥非得用抽象類和抽象方法 呢?
確實如此. 但是使用抽象類相當于多了一重編譯器的校驗.
使用抽象類的場景就如上面的代碼, 實際工作不應該由父類完成, 而應由子類完成. 那么此時如果不小心誤用成父類 了, 使用普通類編譯器是不會報錯的. 但是父類是抽象類就會在實例化的時候提示錯誤, 讓我們盡早發現問題.
很多語法存在的意義都是為了 "預防出錯", 例如我們曾經用過的 ?nal 也是類似. 創建的變量用戶不去修改, 不 就相當于常量嘛? 但是加上 ?nal 能夠在不小心誤修改的時候, 讓編譯器及時提醒我們.
充分利用編譯器的校驗, 在實際開發中是非常有意義的.
接口
接口的概念
在現實生活中,接口的例子比比皆是,比如:筆記本上的USB口,電源插座等。
電腦的USB口上,可以插:U盤、鼠標、鍵盤...所有符合USB協議的設備
電源插座插孔上,可以插:電腦、電視機、電飯煲...所有符合規范的設備
通過上述例子可以看出:接口就是公共的行為規范標準,大家在實現時,只要符合規范標準,就可以通用。
在Java中,接口可以看成是:多個類的公共規范,是一種引用數據類型。
語法規則
接口的定義格式與定義類的格式基本相同,將class關鍵字換成interface關鍵字,就定義了一個接口。
public interface 接口名稱{// 抽象方法public abstract void method1(); // public abstract 是固定搭配,可以不寫public void method2();abstract void method3();void method4();// 注意:在接口中上述寫法都是抽象方法,跟推薦方式4,代碼更簡潔}
提示:
1. 創建接口時, 接口的命名一般以大寫字母 I 開頭.
2. 接口的命名一般使用 "形容詞" 詞性的單詞.
3. 阿里編碼規范中約定, 接口中的方法和屬性不要加任何修飾符號, 保持代碼的簡潔性.
接口使用?
接口不能直接使用,必須要有一個"實現類"來"實現"該接口,實現接口中的所有抽象方法。
public class 類名稱 implements 接口名稱{// ...}
注意:子類和父類之間是extends 繼承關系,類與接口之間是 implements 實現關系。
請實現筆記本電腦使用USB鼠標、USB鍵盤的例子
1. USB接口:包含打開設備、關閉設備功能
2. 筆記本類:包含開機功能、關機功能、使用USB設備功能
3. 鼠標類:實現USB接口,并具備點擊功能
4. 鍵盤類:實現USB接口,并具備輸入功能
// USB接口public interface USB {void openDevice();void closeDevice();}// 鼠標類,實現USB接口public class Mouse implements USB {@Overridepublic void openDevice() {System.out.println("打開鼠標");}@Override public void closeDevice() { System.out.println("關閉鼠標"); }public void click(){ System.out.println("鼠標點擊"); }}// 鍵盤類,實現USB接口public class KeyBoard implements USB {@Override public void openDevice() {System.out.println("打開鍵盤");}@Overridepublic void closeDevice() {System.out.println("關閉鍵盤");}public void inPut(){System.out.println("鍵盤輸入");}}// 筆記本類:使用USB設備public class Computer {public void powerOn(){System.out.println("打開筆記本電腦");}public void powerO?(){System.out.println("關閉筆記本電腦");}public void useDevice(USB usb){usb.openDevice();if(usb instanceof Mouse){Mouse mouse = (Mouse)usb;mouse.click();}else if(usb instanceof KeyBoard){KeyBoard keyBoard = (KeyBoard)usb;keyBoard.inPut(); }usb.closeDevice();}}// 測試類:public class TestUSB {public static void main(String[] args) {Computer computer = new Computer();computer.powerOn();// 使用鼠標設備computer.useDevice(new Mouse());// 使用鍵盤設備computer.useDevice(new KeyBoard());computer.powerO?();}}
接口特性
1. 接口類型是一種引用類型,但是不能直接new接口的對象
public class TestUSB {public static void main(String[] args) {USB usb = new USB();} }// Error:(10, 19) java: day20210915.USB是抽象的; 無法實例化
2. 接口中每一個方法都是public的抽象方法, 即接口中的方法會被隱式的指定為 public abstract(只能是 public abstract,其他修飾符都會報錯)
public interface USB {// Error:(4, 18) java: 此處不允許使用修飾符private private void openDevice();void closeDevice();}
3. 接口中的方法是不能在接口中實現的,只能由實現接口的類來實現
public interface USB {void openDevice();// 編譯失敗:因為接口中的方式默認為抽象方法// Error:(5, 23) java: 接口抽象方法不能帶有主體void closeDevice(){System.out.println("關閉USB設備");}}
4. 重寫接口中方法時,不能使用默認的訪問權限
public interface USB {void openDevice(); // 默認是public的void closeDevice(); // 默認是public的}public class Mouse implements USB {@Overridevoid openDevice() {System.out.println("打開鼠標");}// ...}// 編譯報錯,重寫USB中openDevice方法時,不能使用默認修飾符 // 正在嘗試分配更低的訪問權限; 以前為public
5. 接口中可以含有變量,但是接口中的變量會被隱式的指定為 public static ?nal 變量
public interface USB {double brand = 3.0; // 默認被:?nal public static修飾void openDevice();void closeDevice();}public class TestUSB {public static void main(String[] args) {System.out.println(USB.brand); // 可以直接通過接口名訪問,說明是靜態的// 編譯報錯:Error:(12, 12) java: 無法為最終變量brand分配值 USB.brand = 2.0;// 說明brand具有?nal屬性}}
6. 接口中不能有靜態代碼塊和構造方法
public interface USB {// 編譯失敗 public USB(){}{}// 編譯失敗void openDevice();void closeDevice();}
7. 接口雖然不是類,但是接口編譯完成后字節碼文件的后綴格式也是.class
8. 如果類沒有實現接口中的所有的抽象方法,則類必須設置為抽象類
9. jdk8中:接口中還可以包含default方法。
實現多個接口
在Java中,類和類之間是單繼承的,一個類只能有一個父類,即Java中不支持多繼承,但是一個類可以實現多個接口。下面通過類來表示一組動物.
class Animal {protected String name;public Animal(String name) {this.name = name;}}
另外我們再提供一組接口, 分別表示 "會飛的", "會跑的", "會游泳的".
interface IFlying { void ?y(); }interface IRunning { void run(); }interface ISwimming { void swim(); }
接下來我們創建幾個具體的動物 貓, 是會跑的.
class Cat extends Animal implements IRunning {public Cat(String name) {super(name);}@Overridepublic void run() {System.out.println(this.name + "正在用四條腿跑");}}
魚, 是會游的.
class Fish extends Animal implements ISwimming { public Fish(String name) {super(name);}@Overridepublic void swim() {System.out.println(this.name + "正在用尾巴游泳");}}
青蛙, 既能跑, 又能游(兩棲動物)
class Frog extends Animal implements IRunning, ISwimming {public Frog(String name) {super(name);}@Overridepublic void run() {System.out.println(this.name + "正在往前跳");}@Overridepublic void swim() {System.out.println(this.name + "正在蹬腿游泳");}}
注意:一個類實現多個接口時,每個接口中的抽象方法都要實現,否則類必須設置為抽象類。
提示, IDEA 中使用 ctrl + i 快速實現接口 還有一種神奇的動物, 水陸空三棲, 叫做 "鴨子"
class Duck extends Animal implements IRunning, ISwimming, IFlying { public Duck(String name) {super(name);}@Overridepublic void ?y() {System.out.println(this.name + "正在用翅膀飛");}@Overridepublic 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;}@Overridepublic void run() {System.out.println(this.name + "正在用輪子跑");} }Robot robot = new Robot("機器人");walk(robot);// 執行結果 機器人正在用輪子跑
接口間的繼承?
在Java中,類和類之間是單繼承的,一個類可以實現多個接口,接口與接口之間可以多繼承。即:用接口可以達到 多繼承的目的。
接口可以繼承一個接口, 達到復用的效果. 使用 extends 關鍵字.
interface IRunning { void run(); }interface ISwimming { void swim(); }// 兩棲的動物, 既能跑, 也能游 interface IAmphibious extends IRunning, ISwimming {}class Frog implements IAmphibious { ...}
通過接口繼承創建一個新的接口 IAmphibious 表示 "兩棲的". 此時實現接口創建的 Frog 類, 就繼續要實現 run 方 法, 也需要實現 swim 方法.
接口間的繼承相當于把多個接口合并在一起.
接口使用實例
給對象數組排序
class Student {private String name;private int score;public Student(String name, int score) {this.name = name;this.score = score;}@Overridepublic String toString() {return "[" + this.name + ":" + this.score + "]";}}
再給定一個學生對象數組, 對這個對象數組中的元素進行排序(按分數降序).
Student[] students = new Student[] {new Student("張三", 95),new Student("李四", 96),new Student("王五", 97),new Student("趙六", 92),};
按照我們之前的理解, 數組我們有一個現成的 sort 方法, 能否直接使用這個方法呢?
Arrays.sort(students);System.out.println(Arrays.toString(students));// 運行出錯, 拋出異常.Exception in thread "main" java.lang.ClassCastException: Student cannot be cast to java.lang.Comparable
仔細思考, 不難發現, 和普通的整數不一樣, 兩個整數是可以直接比較的, 大小關系明確. 而兩個學生對象的大小關系 怎么確定? 需要我們額外指定.
讓我們的 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;}@Overridepublic String toString() {return "[" + this.name + ":" + this.score + "]";}@Overridepublic 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;
再次執行程序, 結果就符合預期了.
// 執行結果
[[王五:97], [李四:96], [張三:95], [趙六:92]]
注意事項: 對于 sort 方法來說, 需要傳入的數組的每個對象都是 "可比較" 的, 需要具備 compareTo 這樣的能力. 通 過重寫 compareTo 方法的方式, 就可以定義比較規則.
為了進一步加深對接口的理解, 我們可以嘗試自己實現一個 sort 方法來完成剛才的排序過程(使用冒泡排序)
public static void sort(Comparable[] array) {for (int bound = 0; bound < array.length; bound++) {for (int cur = array.length - 1; cur > bound; cur--) {if (array[cur - 1].compareTo(array[cur]) > 0) {// 說明順序不符合要求, 交換兩個變量的位置Comparable tmp = array[cur - 1];array[cur - 1] = array[cur];array[cur] = tmp;}}}}
再次執行代碼
sort(students);System.out.println(Arrays.toString(students));// 執行結果[[王五:97], [李四:96], [張三:95], [趙六:92]]
Clonable 接口和深拷貝?
Java 中內置了一些很有用的接口, Clonable 就是其中之一.
Object 類中存在一個 clone 方法, 調用這個方法可以創建一個對象的 "拷貝". 但是要想合法調用 clone 方法, 必須要 先實現 Clonable 接口, 否則就會拋出 CloneNotSupportedException 異常.
class Animal implements Cloneable {private String name;@Overridepublic Animal clone() {Animal o = null;try {o = (Animal)super.clone();} catch (CloneNotSupportedException e) {e.printStackTrace();}return o;}}public class Test {public static void main(String[] args) {Animal animal = new Animal();Animal animal2 = animal.clone();System.out.println(animal == animal2);}}// 輸出結果// false
淺拷貝 VS 深拷貝
Cloneable 拷貝出的對象是一份 "淺拷貝"
觀察以下代碼:
class Money {
public double m = 99.99;
}class Person implements Cloneable{ public Money money = new Money();@Override
protected Object clone() throws CloneNotSupportedException { return super.clone(); }}public class TestDemo3 {public static void main(String[] args) throws CloneNotSupportedException { Person person1 = new Person(); Person person2 = (Person) person.clone(); System.out.println("通過person2修改前的結果"); System.out.println(person1.money.m); System.out.println(person2.money.m); person2.money.m = 13.6; System.out.println("通過person2修改后的結果"); System.out.println(person1.money.m); System.out.println(person2.money.m);}}// 執行結果 通過person2修改前的結果99.9999.99通過person2修改后的結果13.613.6
如上代碼,我們可以看到,通過clone,我們只是拷貝了Person對象。但是Person對象中的Money對象,并 沒有拷貝。通過person2這個引用修改了m的值后,person1這個引用訪問m的時候,值也發生了改變。這里 就是發生了淺拷貝。那么同學們想一下如何實現深拷貝呢?
抽象類和接口的區別
抽象類和接口都是 Java 中多態的常見使用方式. 都需要重點掌握. 同時又要認清兩者的區別(重要!!! 常見面試題).
核心區別: 抽象類中可以包含普通方法和普通字段, 這樣的普通方法和字段可以被子類直接使用(不必重寫), 而接口中 不能包含普通方法, 子類必須重寫所有的抽象方法.
如之前寫的 Animal 例子. 此處的 Animal 中包含一個 name 這樣的屬性, 這個屬性在任何子類中都是存在的. 因此此 處的 Animal 只能作為一個抽象類, 而不應該成為一個接口.
class Animal {
protected String name;public Animal(String name) { this.name = name;
}}
再次提醒:
抽象類存在的意義是為了讓編譯器更好的校驗, 像 Animal 這樣的類我們并不會直接使用, 而是使用它的子類. 萬一不小心創建了 Animal 的實例, 編譯器會及時提醒我們.
Object類?
Object是Java默認提供的一個類。Java里面除了Object類,所有的類都是存在繼承關系的。默認會繼承Object父 類。即所有類的對象都可以使用Object的引用進行接收。
范例:使用Object接收所有類的對象
class Student{}
public class Test {public static void main(String[] args) { function(new Person());
function(new Student());
} public static void function(Object obj) {System.out.println(obj); }}
//執行結果:
Person@1b6d3586
Student@4554617c
所以在開發之中,Object類是參數的最高統一類型。但是Object類也存在有定義好的一些方法。如下:
對于整個Object類中的方法需要實現全部掌握。
本小節當中,我們主要來熟悉這幾個方法:toString()方法,equals()方法,hashcode()方法?
獲取對象信息?
如果要打印對象中的內容,可以直接重寫Object類中的toString()方法,之前已經講過了,此處不再累贅。
// Object類中的toString()方法實現:public String toString() {return getClass().getName() + "@" + Integer.toHexString(hashCode());}
對象比較equals方法
在Java中,==進行比較時:
a.如果==左右兩側是基本類型變量,比較的是變量中值是否相同
b.如果==左右兩側是引用類型變量,比較的是引用變量地址是否相同
c.如果要比較對象中內容,必須重寫Object中的equals方法,因為equals方法默認也是按照地址比較的:
// Object類中的equals方法public boolean equals(Object obj) {return (this == obj); // 使用引用中的地址直接來進行比較 }class Person{private String name ;private int age ;public Person(String name, int age) {this.age = age ;this.name = name ;}}public class Test {public static void main(String[] args) {Person p1 = new Person("gaobo", 20) ;Person p2 = new Person("gaobo", 20) ;int a = 10;int b = 10;System.out.println(a == b); // 輸出trueSystem.out.println(p1 == p2); // 輸出falseSystem.out.println(p1.equals(p2)); // 輸出false}}
Person類重寫equals方法后,然后比較:
class Person{...@Overridepublic boolean equals(Object obj) {if (obj == null) {return false ;} if(this == obj) {return true ;}// 不是Person類對象if (!(obj instanceof Person)) {return false ;}Person person = (Person) obj ; // 向下轉型,比較屬性值return this.name.equals(person.name) && this.age==person.age ;}}
結論:比較對象中內容是否相同的時候,一定要重寫equals方法。
hashcode方法
回憶剛剛的toString方法的源碼:
public String toString() { return getClass().getName() + "@" + Integer.toHexString(hashCode());}
我們看到了hashCode()這個方法,他幫我算了一個具體的對象位置,這里面涉及數據結構,但是我們還沒學數據結 構,沒法講述,所以我們只能說它是個內存地址。然后調用Integer.toHexString()方法,將這個地址以16進制輸 出。
hashcode方法源碼:
public native int hashCode();
該方法是一個native方法,底層是由C/C++代碼寫的。我們看不到。 我們認為兩個名字相同,年齡相同的對象,將存儲在同一個位置,如果不重寫hashcode()方法,我們可以來看示例 代碼:
class Person {public String name; public int age; public Person(String name, int age) {this.name = name;this.age = age; }} public class TestDemo4 {public static void main(String[] args) {Person per1 = new Person("gaobo", 20) ;Person per2 = new Person("gaobo", 20) ;System.out.println(per1.hashCode());System.out.println(per2.hashCode());}}//執行結果4601419581163157884
注意事項:兩個對象的hash值不一樣。
像重寫equals方法一樣,我們也可以重寫hashcode()方法。此時我們再來看看。
class Person {public String name;public int age;public Person(String name, int age) {this.name = name;this.age = age;}@Overridepublic int hashCode() {return Objects.hash(name, age);}}public class TestDemo4 {public static void main(String[] args) {Person per1 = new Person("gaobo", 20) ;Person per2 = new Person("gaobo", 20) ;System.out.println(per1.hashCode());System.out.println(per2.hashCode());}} //執行結果 460141958 460141958
注意事項:哈希值一樣。
結論:
1、hashcode方法用來確定對象在內存中存儲的位置是否相同
2、事實上hashCode() 在散列表中才有用,在其它情況下沒用。在散列表中hashCode() 的作用是獲取對象的 散列碼,進而確定該對象在散列表中的位置。