文章目錄
- 一、多型
- 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) |
|---|---|---|
| 概念 | 方法名稱相同、引數串列不同、回傳值無要求 | 方法名稱相同、引數串列相同、回傳型別一般相同 |
| 范圍 | 多載不是必須在一個類當中(繼承) | 繼承關系 |
| 限制 | 沒有權限要求 | 被覆寫的方法不能擁有比父類更嚴格的訪問控制權限 |
比較結果就是,兩者沒啥關系呀

講到這里,我們好像一直沒有說明上一小節的標題動態系結是啥
那么什么叫做動態系結呢?發生的條件如下
- 發生向上轉型(父類參考需要參考子類物件)
- 通過父類參考,來呼叫子類和父類的同名覆寫方法
那為啥是叫動態的呢?經過反匯編我們可以發現
- 編譯的時候: 呼叫的是父類的方法
- 但是運行的時候: 實際上呼叫的是子類的方法
因此這其實是一個動態的程序,也可以叫其運行時系結
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,方便理解
| 區別 | this | super |
|---|---|---|
| 概念 | 訪問本類中的屬性和方法 | 由子類訪問父類中的屬性和方法 |
| 查找范圍 | 先查找本類,如果本類沒有就呼叫父類 | 直接呼叫父類 |
| 表示 | 表示當前物件 | 無 |
| 共性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 參考所指向的物件也克隆一份
步驟:
將 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; }
此時便是深拷貝了!
四、總結
以上便是個人對于多型、抽象類和介面的認知了,可能講的不是很好,但是我已經盡力去詮釋了,希望對大家有所幫助!

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






