本章目錄
- 溫馨提示
- 開篇介紹( 知識點比較多 務必耐心看完!)
- 本章重點
- 正文開始
- 1. 包
- 1.1 匯入包中的類
- 1.2 靜態匯入
- 1.3 將類放到包中
- 1.4 包的訪問權限控制
- 1.5 常見的系統包
- 2. 繼承
- 2.1 了解繼承
- 2.2 語法規則(比較枯燥)
- 2.3 子類的構造方法
- 2.4 super 和 this 的區別
- 2.5 訪問權限
- 2.6 更復雜的繼承關系
- 2.7 final 關鍵字
- 3. 多型
- 3.1 向上轉型
- 知識點補充:父類參考訪問成員
- 3.2 動態系結
- 知識點補充1:在構造方法中呼叫重寫的方法
- 知識點補充2:靜態系結
- 3.3 方法重寫
- 3.4 重寫和多載的區別(重新整理)
- 3.5 向下轉型
- 3.6 理解多型
- 4. 抽象類
- 4.1 了解抽象類
- 4.2 語法規則
- 4.3 抽象類的作用
- 5. 介面
- 5.1 了解介面及簡單語法規則
- 5.2 實作多個介面及其他語法規則
- 5.3 介面之間的繼承
- 5.4 介面使用實體
- Comparable介面
- Comparator介面
- Cloneable介面及深拷貝和淺拷貝
- 5.5 抽象類和介面的區別
- 全文結束
溫馨提示
大家好我是Cbiltps,在我的博客中如果有難以理解的句意,難以用文字表達的重點,我會有配圖,所以我的博客配圖非常重要!!!
而且很多知識在代碼的注釋里,所以代碼注釋也非常重要!!!
這篇文章前后邏輯順序非常重要,一定要從前往后看,慢慢看!!!
開篇介紹( 知識點比較多 務必耐心看完!)
等后面我會寫關于類和物件的博客,其實在學習本篇博客之前一定要對類和物件有一定的了解!就是大家先學習類和物件的博客,然后再看這篇面向物件文章,你就會有一個階梯式的理解!
緊接著,在這篇文章的尾部,我會展開一個簡單且知識點全面的一個關于面向物件的訓練(最后面直接貼鏈接),大家拭目以待吧!學習完后你就會對面向物件有一個初步的認識!
還有一點:這篇文章橫向拓展比較多,全程三萬多字!大家在學習的時候會看到豐富的知識點,覆寫面也比較全!
那么,今天這篇博客的知識點在小米、阿里巴巴、百度、VIVO、 騰訊、攜程、貝殼、美團、頭條、網易、京東、滴滴等公司常考!!!
本章重點
- 包
- 繼承
- 多型
- 抽象類
- 介面
正文開始
1. 包
包 (package) 是組織類的一種方式,使用包的主要目的是保證類的唯一性,
上面的話晦澀難懂,我們先不要糾結這個,直接舉例子演示(看下面)!
1.1 匯入包中的類
比如說列印陣列的時候就匯入了包中的類:
package com.company;
import java.util.Arrays;//呼叫了util這個包中的Arrays這個類
public class Main {
public static void main(String[] args) {
int[] array = {1,2,3,4,5};
System.out.println(Arrays.toString(array));//用字串的方式列印陣列
}
}
注意一個問題: toString這個方法是由類名(Arrays)呼叫的,所以這個方法就是一個靜態方法(按住CTRL后滑鼠點進去,進入Arrays.java檔案,如下圖)!

所以所有通過類名直接呼叫的方法就一定是靜態方法,我們不用管這個方法是怎么執行的,這個是官方寫好的,直接呼叫即可!
那么,它在哪個包底下呢?
在上面打開檔案(Arrars.java)的最前面就可以看到的,如下

我們可以找到這個檔案(Arrars.java)的路徑:

關于上面的package關鍵字是什么,一會說!而且再看下面一點點(Arrars.java中)還有import關鍵字!

然后提出疑問:import和package有什么區別?
- package 是你運行程式后生成的
.class檔案全部放在了你所定義的包中,便于以后呼叫管理, - import 則是在撰寫程式的時候需要呼叫某個包中的類,
也就是說,如果要用到一些Java類別庫里面代碼的時候,我們都需要import來匯入的!
然后再舉一個匯入例子:
//匯入方式1
package com.company;
import java.util.Date;//在這里匯入
public class Main {
public static void main(String[] args) {
Date date = new Date();
}
}
//匯入方式2
package com.company;
public class Main {
public static void main(String[] args) {
java.util.Date date = new java.util.Date();//這匯入也可以,但是比較麻煩,推薦方式1
}
}
記住:只能匯入一個具體的類,不能匯入一個具體的包!

然后緊接著下一個問題:我們看到有人是這樣匯入包的,這里的*是什么意思?
import java.util.*;
*是一個通配符:意思是匯入這個包底下所有的類!
疑問:util下面有很多的類,難道一下子全部匯入了嗎?
不是的,Java處理的時候,需要誰,它才會拿誰!
在C語言里面,通過include關鍵字匯入之后,會把頭檔案里面的內容全部拿過來!
但是,這樣子(import java.util.*;)匯入的范圍太廣了,你有可能把握不住,有時候會有沖突(如下),建議匯入具體的類名!

但是,假設你想用另一個包底下的Date,就識別不了了(看下圖):

解決方法就是:使用完整的類名匯入!

//代碼組織如下:
package com.company;
import java.util.*;
import java.sql.*;
//以上兩個包中都有Date類,為了避免沖突,使用如下操作
public class Main {
public static void main(String[] args) {
java.util.Date date = new java.util.Date();//使用完整的類名匯入
}
}
1.2 靜態匯入
一般情況下,靜態匯入用的非常的少,了解即可!
package com.company;
import static java.lang.System.*;
import static java.lang.Math.*;
public class Main {
public static void main(String[] args) {
out.println("123");//靜態匯入寫起來方便
out.println(max(12, 23));//但是,不提倡這樣寫,不方便閱讀,稀奇古怪
}
}
1.3 將類放到包中
廢話不多說,直接上步驟:
- 新建一個包

- 創建類

同時可以看到磁盤上的目錄結構已經被 IDEA 自動創建出來了

基本規則(注意以下幾點):
- 在檔案的最上方加上一個 package 陳述句指定該代碼在哪個包中
- 包名必須是小寫的
- 包名需要盡量指定成唯一的名字, 通常會用公司的域名的顛倒形式(例如:
com.xxxxx.www) - 包名要和代碼路徑相匹配,例如創建
com.cbiltps的包, 那么會存在一個對應的路徑com/cbiltps來存盤代碼 - 如果一個類沒有 package 陳述句, 則該類被放到一個默認包中
經過上面的步驟后,回傳第一節的第一句話:包 (package) 是組織類的一種方式,使用包的主要目的是保證類的唯一性,
如何保證類的唯一性呢?
其實就是磁盤上的同一目錄下(同一個包下)只能有一個同名的檔案(類),并且包名不一樣了,路徑也不一樣,也就互相不干擾了!
1.4 包的訪問權限控制
包訪問權限:顧名思義,就是只能在當前包中使用,
舉個例子:當你的成員變數不加任何的訪問修飾限定詞的時候,就是包訪問權限,
由于在不同的包中展示,不方便直接代碼展示,所以就截圖展示了(比較亂,見諒!)

由此可見,cbiltps包中無法訪問company包中的val值,所以包訪問權限只能在當前包中使用!
1.5 常見的系統包
- java.lang:系統常用基礎類(String、Object),
此包從JDK1.1后自動匯入,不需要import進行匯入! - java.lang.reflect:java反射編程包,
- java.net:進行網路編程開發包,
- java.sql:進行資料庫開發的支持包,
- java.util:是java提供的工具程式包,(集合類等) 非常重要
- java.io:I/O編程開發包,
2. 繼承
在學習本小節之前先來復習一下知識點:面向物件的基本特征(共四點 先說兩點)
- 封裝:不必要公開的資料成員和方法,使用
private關鍵字修飾(為了安全性!) - 繼承:對共性的抽取,使用
extends進行處理(代碼可以重復使用!)
2.1 了解繼承
代碼中創建的類, 主要是為了抽象現實中的一些事物(包含屬性和方法),
有的時候客觀事物之間就存在一些關聯關系, 那么在表示成類和物件的時候也會存在一定的關聯,
例如, 設計一個類表示動物(直接上代碼):
class Animal {
//動物的名字和年齡都是共有的!
public String name;
public int age;
//包括吃也是共有的行為!
public void eat() {
System.out.println(name + "eat()");
}
}
class Dog extends Animal {
//抽取完后大大減少了代碼量!
}
class Bird extends Animal {
public String wing;
public void fly() {
System.out.println(name+"fly()" + age);
}
}
此時,Animal 這樣被繼承的類, 我們稱為父類,基類 或超類,對于像 Dog 和 Bird 這樣的類,我們稱為子類,派生類;
從邏輯上講,Dog 和 Bird 都是一種 Animal (is - a 語意);
和現實中的兒子繼承父親的財產類似,子類也會繼承父類的欄位和方法,以達到代碼重用的效果,

extends 英文原意指 “擴展”,而我們所寫的類的繼承,也可以理解成基于父類進行代碼上的 “擴展”;
例如我們寫的 Bird 類, 就是在 Animal 的基礎上擴展出了 fly 方法,
2.2 語法規則(比較枯燥)
基本語法:
class 子類 extends 父類 {
}
注意:
-
使用 extends 指定父類
-
Java 中一個子類
只能繼承一個父類 (而C++/Python等語言支持多繼承)

-
子類會繼承父類的所有
public的欄位和方法 -
對于父類的
private的欄位和方法,子類中是無法訪問的

-
子類的實體中,也包含著父類的實體,可以使用 super 關鍵字得到父類實體的參考
上面簡單的總結后看另外一個問題:

得出結論:子類和父類欄位同名的情況下優先訪問的是子類的 !
而它的記憶體圖是這樣子的:

2.3 子類的構造方法
看一個問題:根據上面的表示動物類的代碼中添加父類的構造方法后為什么會產生下面的錯誤?

根據上面的報錯,我們得到下面的結論:
- 父類構造方法不能被子類繼承
- 子類的構造方法中必須要呼叫父類的構造方法
而且注意:呼叫父類構造方法的時候
- 子類通過 super(引數串列)呼叫父類構造方法,呼叫super的陳述句必須放在子類構造方法的第一行
- 若子類構造方法中沒有顯示呼叫父類構造方法,則系統默認呼叫父類無參的構造方法;若父類中沒有定義無引數的構造方法,編譯出錯!
所以,按照下面的樣子改即可:

上面的知識點明白之后,我們補充完主類后,畫一下記憶體圖(加強理解):
//主類的代碼如下:
public class TestDemo {
public static void main(String[] args) {
Dog dog = new Dog("哈士奇",19);
System.out.println(dog.name);
dog.eat();
Bird bird = new Bird("喜鵲",18,"我要的飛翔");
System.out.println(bird.name);
bird.eat();
bird.fly();
}
}

2.4 super 和 this 的區別
在之前的學習中已經遇見 super 和 this 兩個關鍵字,博主根據自身所學及博客參考做出如下總結:
super: 可以理解為父類物件的參考(是依賴物件的),不能出現在靜態環境(包括:static變數,static方法,static陳述句塊)中(因為 static 不依賴物件)!
super(); //呼叫父類的構造方法super.func(); //呼叫父類的普通方法super.data; //呼叫父類的成員屬性
this: 可以理解為指向本物件的指標,它代表當前物件名(在程式中易產生二義性之處,應使用 this 來指明當前物件;如果函式的形參與類中的成員資料同名,這時需用 this 來指明成員變數名)!
this(); //呼叫本類中另一種形成的構造方法
注意點與區別總結:
super();和this();區別是:super();從子類中呼叫父類的構造方法,this();在同一類內呼叫其它方法super();和this();均需放在構造方法內第一行- 有時候
this和super不能同時出現在一個建構式里面,因為 this 必然會呼叫其它的建構式,其它的建構式必然也會有 super 陳述句的存在,所以在同一個建構式里面有相同的陳述句,就失去了陳述句的意義,編譯器也不會通過 this();和super();都指的是物件,所以,均不可以在static環境中使用(包括:static變數,static方法,static陳述句塊)

2.5 訪問權限
Java中對于欄位和方法共有四種訪問權限:
- private: 類內部能訪問, 類外部不能訪問
- 默認(也叫包訪問權限): 類內部能訪問, 同一個包中的類可以訪問, 其他類不能訪問
- protected: 類內部能訪問, 子類和同一個包中的類可以訪問, 其他類不能訪問
- public : 類內部和類的呼叫者都能訪問
重要的是下面的范圍圖:

private關鍵字(補充的一點):
這里有一個問題:
父類的 private 修飾的成員變數是否被繼承了?
這個問題的答案有點模糊,有的書上說是繼承了的;
但有的書上說不是,建議我們采用沒有沒繼承的答案!
原因看下面:
//父類
class A {
private int a;//使用private修飾
}
//子類 這段代碼是錯
class B extends A { //繼承
public void func() {
System.out.println(this.a);//這里無法訪問就說是沒有沒繼承的!
}
}
這個知識點先就寫到這里,后期有需要的話經過百度之后有可能會補充!
大家也可以在評論區自由發揮!
protected關鍵字:
剛才我們發現,如果把欄位設為 private,子類不能訪問;
但是設成 public,又違背了我們 “封裝” 的初衷;
兩全其美的辦法就是 protected 關鍵字,
- 對于類的呼叫者來說,protected 修飾的欄位和方法是不能訪問的
- 對于類的 子類 和 同一個包的其他類 來說,protected 修飾的欄位和方法是可以訪問的
什么時候下用哪一種呢?
我們希望類要盡量做到 “封裝”,即隱藏內部實作細節,只暴露出必要的資訊給類的呼叫者,
因此我們在使用的時候應該盡可能的使用比較嚴格的訪問權限,
例如如果一個方法能用 private,就盡量不要用 public,
另外,還有一種簡單粗暴的做法:將所有的欄位設為 private, 將所有的方法設為public,
不過這種方式屬于是對訪問權限的濫用,還是更希望同學們能寫代碼的時候認真思考,該類提供的欄位方法到底給 “誰” 使用(是類內部自己用,還是類的呼叫者使用,還是子類使用),
2.6 更復雜的繼承關系
這里有個不成文的規則:繼承層次最好不要超過三個!
時刻牢記,我們寫的類是現實事物的抽象,而我們真正在公司中所遇到的專案往往業務比較復雜,可能會涉及到一
系列復雜的概念,都需要我們使用代碼來表示,所以我們真實專案中所寫的類也會有很多,類之間的關系也會更加
復雜,
但是即使如此,我們并不希望類之間的繼承層次太復雜,一般我們不希望出現超過三層的繼承關系,如果繼承層
次太多,就需要考慮對代碼進行重構了,
如果想從語法上進行限制繼承,就可以使用 final 關鍵字 !
2.7 final 關鍵字
曾經我們學習過 final 關鍵字,修飾一個變數或者欄位的時候,表示常量 (不能修改)
final int a = 10;
a = 20; //編譯出錯
final 關鍵字也能修飾類(叫做密封類),此時表示被修飾的類就不能被繼承 !
final 關鍵字的功能是限制類被繼承,“限制” 這件事情意味著 “不靈活”,
在編程中,靈活往往不見得是一件好事,靈活可能意味著更容易出錯,
而我們常見的String類就是final修飾的:

3. 多型
從字面上理解,就是一種事物多種形態,
但是,面試官問的時候不能這樣回答!
了解多型需要一個程序!
3.1 向上轉型
根據上面的繼承關系,我們進行探討:
public static void main(String[] args) {
/*Dog dog = new Dog("旺財",20);
Animal animal = dog;*/
//把上面的代碼簡化一下
Animal animal = new Dog("旺財",23);//向上轉型
//其實就是:父類參考 參考 子類物件
}
知識點補充:父類參考訪問成員
學到這里我在添加一個知識點(這一個知識點作為 繼承 和 訪問的補充),
然后我們再次重新展示一下代碼(為了方便我直接把知識點寫寫進了代碼里,請大家注意查收):
//Animal類
class Animal {
public String name = "動物";
public int age;
protected int count;
public Animal(String name,int age) {
this.name = name;
this.age = age;
}
public void eat() {
System.out.println(name+" eat()");
}
}
//Bird類 繼承于 Animal
lass Bird extends Animal {
public String wing;
public String name = "鳥類";
public Bird(String name, int age, String wing) {
super(name, age);
this.wing = wing;
}
public void fly() {
System.out.println(super.name + "fly()");
}
}
//主類的主方法
public static void main(String[] args) {
Animal animal2 = new Bird("wuya",12,"wuya fly!");//這里發生向上轉型
animal2.eat();//可以呼叫eat方法
System.out.println(animal2.name);//這里其實訪問的是父類的name
// 注意了:重點知識在這里!!!
// System.out.println(animal2.wing); 無法訪問的
// 因為animal的型別是Animal型別
// 意思就是:通過父類參考,只能訪問父類自己的成員!
}
什么情況下會發生向上轉型?
- 直接賦值(就是上面的情況)
public static void main(String[] args) {
/*Dog dog = new Dog("旺財",20);
Animal animal = dog;*/
//把上面的代碼簡化一下
Animal animal = new Dog("旺財",23);//向上轉型
//其實就是:父類參考 參考 子類物件
}
- 作為函式的引數
public class TestDemo2 {
public static void func(Animal animals) {
}
public static void main(String[] args) {
Dog dog = new Dog("金毛",20);
func(dog);//在這里發生向上轉型
}
}
- 作為方法的回傳值
public static Animal fun2(Animal animalss) {
Dog dog = new Dog("huahua",23);
return dog;//在這里發生向上轉型
}
3.2 動態系結
Java中有兩種多型,運行時多型(動態系結)和編譯時多型(靜態系結)!
編譯時多型就是多載來實作的,根據你給的引數以及個數的不同,來推匯出你呼叫那個函式!
那運行時多型是怎樣的呢?往下看…
首先一個問題:

因為此時發生了動態系結!
發生動態系結的兩個條件:
- 父類參考
參考子類物件 - 通過這個父類參考,
呼叫父類和子類同名的覆寫方法
大家注意了:動態系結是多型的基礎 !!!
這個時候是不是還是有點不明白這個動態是啥意思,接著往下看…
那就來看看下面的位元組碼檔案:
在此之前,我們來看看Java中如何打開反匯編代碼?

我們來看一下這個反匯編代碼(main 方法):

我們看到反匯編代碼中呼叫的是 Animal.eat; (父類的 eat 方法),但是運行的時候呼叫的為啥是 dog.eat(); (參考的子類物件的 eat 方法)?

在這里,在編譯的時候,不能夠確定此時呼叫誰的方法;在運行的時候,才知道呼叫誰的方法!
這個叫運行時系結,也叫動態系結!
知識點補充1:在構造方法中呼叫重寫的方法
直接看代碼:
//Animal類
class Animal {
public String name = "動物";
public int age;
public Animal(String name,int age) {
eat();//在父類中呼叫父類和子類重寫的eat方法,也會發生所謂的動態系結!
this.name = name;
this.age = age;
}
public void eat() {
System.out.println(name+" eat()");
}
}
//Dog類 繼承于 Animal類
class Dog extends Animal {
public Dog(String name, int age) {
super(name, age);//顯示呼叫構造方法
}
@Override //這個是注解
public void eat() {
System.out.println(name+"crazy eat()");
}
}
//主類的主方法
public static void main(String[] args) {
Dog dog = new Dog("wawa",23);//這里創建物件,直接運行,看下面的結果
}
運行結果如下:

說明:在父類中呼叫父類和子類重寫的eat方法,也會發生所謂的動態系結!
知識點補充2:靜態系結
class Dog {
public void func(int a) {
System.out.println("int");
}
public void func(int a,int b) {
System.out.println("int,int");
}
public void func(int a,int b,int c) {
System.out.println("int,int,int");
}
}
//主類的主方法:
public static void main(String[] args) {
Dog dog = new Dog("haha",19);
dog.func(10);
}
在這里我們打開 PowerShell 視窗查看反匯編代碼:

此時這里發生的就是:靜態系結!
就是根據你給的引數的型別和個數,推匯出你呼叫的那個函式!
3.3 方法重寫
而這個所謂的父類和子類同名的覆寫方法就是覆寫/重寫/覆寫(Override)!
此時的重寫要滿足下面的條件:
- 方法名相同
- 引數串列相同(個數與型別)
- 回傳值相同
- 必須是父子類的情況下
而且注意:
- 靜態的方法
不能重寫 - 子類的訪問修飾限定符
要大于等于父類的訪問修飾限定 - 被 private 修飾的方法不能重寫
- 被 final 修飾的方法不能重寫
但是有一個 special time(很少有書上寫,考試也很少出現):
重寫的時候,回傳值可以不一樣!
但是要滿足下面的情況:

如果你遇見選擇題的時候,選擇最正確的一個就可以辣!
這里有一點要講一下 :
上面的 Animal類 回傳的是 Animal ,Dog類 回傳的是 Dog,
它們的回傳值構成了一種型別,叫協變型別!
如果你的回傳值發生了協變型別,我們也說發生了重寫!
3.4 重寫和多載的區別(重新整理)
重寫(Override): 子類繼承了父類原有的方法,但有時子類并不想原封不動的繼承父類中的某個方法,所以在方法名,引數串列,回傳型別(除子類中方法的回傳值是父類中方法回傳值的子類時)都相同的情況下, 對方法體進行修改或覆寫,即外殼不變,核心重寫!
- 發生方法重寫的兩個方法回傳值(
除了上面寫到的special time和被重寫方法回傳值型別的子類)、方法名、引數串列必須完全一致(子類重寫父類的方法) - 子類方法的訪問級別不能低于父類相應方法的訪問級別
- 覆寫的方法所拋出的例外和被覆寫方法的所拋出的例外一致,或者是其子類(子類例外不能大于父類例外)
- 被 private 、final 和 static 修飾的方法不能重寫
多載(Overload): 在一個類中,同名的方法如果有不同的引數串列(引數型別不同、引數個數不同甚至是引數順序不同)則視為多載,同時,多載對回傳型別沒有要求,可以相同也可以不同,所以不能通過回傳型別是否相同來判斷多載,
- 方法名相同,引數串列不同(引數順序、個數、型別)
- 方法回傳值、訪問修飾符任意
注意點與區別總結:
- 重寫實作的是
運行時的多型,而多載實作的是編譯時的多型 - 重寫的方法引數串列必須相同(
一般情況下);而多載的方法引數串列必須不同 - 重寫的方法的回傳值型別只能是父型別別或者父型別別的子類,而多載的方法對回傳值型別沒有要求

3.5 向下轉型
直接看下面代碼(知識點全部寫進了注釋里):
public static void main(String[] args) {
Animal animal3 = new Bird("lala",12,"flyyyyy");
Bird bird = (Bird)animal3;//強行轉換
bird.fly();//這里可以呼叫fly方法
//在這里不建議這樣寫,有的時候是錯的(不是非常的安全)!
//因為不是所有的動物都是鳥,邏輯上就是顛覆認知的!
//你可以向下轉型;
//前提是:這個參考(animal3) 參考了 你將要向下轉型的這個物件(bird)!
}
那為什么說不是非常安全的呢?
//代碼這樣子寫是錯的!
public static void main(String[] args) {
Animal animal4 = new Dog("aa",23);
Bird bird = (Bird)animal4;//這里就會報型別轉換例外
//因為:不是所有的動物都是鳥!
bird.fly();
}
所以,為了讓向下轉型更安全,我們可以先判定一下看看 animal 本質上是不是一個 Bird 實體,再來轉換:
public static void main(String[] args) {
Animal animal4 = new Dog("aa",23);
if (animal4 instanceof Bird) { //這里if陳述句沒進來
Bird bird = (Bird)animal4;
bird.fly();
}
//運行之后就是什么都沒有
}
instanceof 可以判定一個參考是否是某個類的實體:
如果是,則回傳 true!
這時再進行向下轉型就比較安全了,
3.6 理解多型
我們先來寫一段代碼:
class Shape {
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 TestDemo1 {
public static void drawMap(Shape shape) {
shape.draw();//動態系結(運行時),這里呼叫了重寫的draw方法
}
public static void main(String[] args) {
//它的類以及下面物件的類都是Shape類的子類,目的就是為了發生向上轉型!
Rect rect = new Rect();
drawMap(rect);
Flower flower = new Flower();
drawMap(flower);
}
}
簡單的定義一下:
通過一個參考,呼叫一個 draw方法(父類和子類覆寫的方法),會有不同的表現形式(取決于參考誰的物件),這就是多型!
多型的大前提就是一定要向上轉型,且呼叫一個父類方法(子類覆寫,呼叫父類)!
使用多型的好處是什么?
1:類呼叫者對類的使用成本進一步降低
- 封裝是讓類的呼叫者不需要知道類的實作細節
- 多型能讓類的呼叫者連這個類的型別是什么都不必知道, 只需要知道這個物件具有某個方法即可
因此,多型可以理解成是封裝的更進一步,讓類呼叫者對類的使用成本進一步降低!
這也貼合了《代碼大全》中關于 “管理代碼復雜程度” 的初衷!
2:能夠降低代碼的 “圈復雜度”, 避免使用大量的 if - else
看代碼:
//不基于多型
public static void main3(String[] args) {
Rect rect = new Rect();
Flower flower = new Flower();
Triangle triangle = new Triangle();
String[] shapes = {"triangle", "rect", "triangle", "rect", "flower"};
for (String s : shapes) {
if(s.equals("triangle")) {
triangle.draw();
}else if(s.equals("rect")) {
rect.draw();
}else {
flower.draw();
}
}
}
//基于多型:
//明顯感覺這樣子的代碼更更高級,量更少!
public static void main4(String[] args) {
Rect rect = new Rect();
Flower flower = new Flower();
Triangle triangle = new Triangle();
Shape[] shapes = {triangle,rect,triangle,rect,flower,};
for (Shape shape : shapes) {
shape.draw();
}
}
什么叫 “圈復雜度” ?
圈復雜度是一種描述一段代碼復雜程度的方式,一段代碼如果平鋪直敘,那么就比較簡單容易理解,而如果有很
多的條件分支或者回圈陳述句,就認為理解起來更復雜,
因此我們可以簡單粗暴的計算一段代碼中條件陳述句和回圈陳述句出現的個數,這個個數就稱為 “圈復雜度”,如果一
個方法的圈復雜度太高,就需要考慮重構,
不同公司對于代碼的圈復雜度的規范不一樣,一般不會超過 10 ,
3:可擴展能力更強
如果要新增一種新的形狀,使用多型的方式代碼改動成本也比較低!
class Triangle extends Shape{
@Override
public void draw() {
System.out.println("△");
}
}
對于類的呼叫者來說,只要創建一個新類的實體就可以了,改動成本很低,
而對于不用多型的情況,就要把 drawShapes 中的 if - else 進行一定的修改,改動成本更高,
4. 抽象類
4.1 了解抽象類
文章寫到這里,其實是有一點瑕疵的,不知道大家有沒有發現!狗頭保命!!!
我們回到列印圖形的代碼(父類):
class Shape {
public void draw() {
System.out.println("列印圖形中...");
}
}
我們來思考一下,
其實System.out.println("列印圖形中...");這一段代碼是沒有意義的,因為后面就是類的繼承和方法的重寫(沒有實際作業)!
那接下來的代碼是不是可以這樣寫呢?
//其實這樣寫是錯誤的
class Shape {
public void draw();
}
然后就有了下面的寫法:
//這里加上 abstract 關鍵字表示這是一個抽象類
abstract class Shape {
//而這里表示的是一個抽象方法
abstract public void draw();//抽象方法沒有方法體(沒有 { } , 不能執行具體代碼)!
//同時也注意:包含抽象方法的類叫抽象類
}
4.2 語法規則
- 抽象類不能直接實體化(會直接報錯)

- 抽象類中可以有普通的方法和成員
- 普通類繼承了抽象類,這個普通類中必須重寫抽象類的所有抽象方法(可以被重寫和呼叫)
- 抽象方法不能是
private修飾的
不僅如此,還有一些特殊的規定!
- 一個抽象類B繼承了抽象類A,那么這個抽象類A中可以不實作抽象類A的抽象方法!
- 在上條繼承關系的基礎上,普通類C繼承了抽象類B,那么A和B中的抽象方法必須被重寫!
- 抽象類和抽象方法是不能被
final修飾!
看代碼是這樣子的:
abstract class A {
abstract public void draw();//抽象方法
}
abstract class B extends A{ //這里繼承于Shape
public abstract void funcA();//這里也是抽象方法
//注意:一個抽象類B繼承了抽象類A,那么這個抽象類A中可以不實作抽象類A的抽象方法!
}
class C extends B {
//在上面繼承關系的基礎上,普通類C繼承了抽象類B,那么A和B中的抽象方法必須被重寫!
@Override
public void funcA() {
}
@Override
public void draw() {
}
}
4.3 抽象類的作用
抽象類存在的最大意義就是為了被繼承!
有些人可能會說普通的類也可以被繼承呀,普通的方法也可以被重寫呀,為啥非得用抽象類和抽象方法呢?
使用抽象類的場景就如上面的代碼,實際作業不應該由父類完成,而應由子類完成,
那么此時如果不小心誤用成父類了,使用普通類編譯器是不會報錯的!
這里其實有一個提示報錯的功能,父類是抽象類就會在實體化的時候提示錯誤,讓我們盡早發現問題!
5. 介面
這一節的知識點非常繁瑣(語法比較多),任何知識點都在代碼注釋里(方便直接理解)!
知識點都在代碼注釋里!
知識點都在代碼注釋里!
知識點都在代碼注釋里!
//這個是抽象類借此引出下面的介面!
abstract class Shape {
abstract public void draw();
}
介面是抽象類的更進一步!
抽象類中還可以包含非抽象方法和欄位;
而介面中包含的方法都是抽象方法,欄位只能包含靜態常量!
5.1 了解介面及簡單語法規則
//使用 interface 定義一個介面
interface IShape {
//abstract public void draw();//這里不加abstract public也是可以的!
//而且注意:介面里面的所有的方法都是 pubilc 的!
void draw();//抽象方法
//介面中的普通方法不能有具體的實作!
//public void func() { //這個方法是錯誤的!
//} //error
//如果要實作,就要使用default關鍵字修飾這個方法!
default public void func() {
System.out.println("介面中的普通方法...");
}
//介面中可以有靜態方法!
public static void funcStatic() {
System.out.println("介面中的靜態方法...");
}
}
介面小總結:
- 介面中的方法一定是抽象方法,因此可以省略 abstract
- 介面中的方法一定是 public,因此可以省略 public
//類和介面之間的關系是通過 implements關鍵字 實作的!
class Rect implements IShape {
//當一個類實作了一個介面,就必須重寫介面中的抽象方法!
@Override
public void draw() {
System.out.println("?");
}
@Override
public void func() {
System.out.println("重寫介面當中的默認方法");
}
}
class Flower implements IShape {
@Override
public void draw() {
System.out.println("?");
}
}
class Triangle implements IShape {
@Override
public void draw() {
System.out.println("△");
}
}
class Cycle implements IShape {
@Override
public void draw() {
System.out.println("●");
}
}
//測驗主類:
public class TestDemo3 {
public static void drawMap(IShape iShape) {
iShape.draw();//動態系結(運行時),這里呼叫了重寫的draw方法
}
public static void main(String[] args) {
//IShape iShape = new IShape();//錯誤的,介面不能實體化!
IShape iShape = new Rect();//但是可以發生向上轉型!
iShape.draw();
//它的類以及下面物件的類都是Shape類的子類,目的就是為了發生向上轉型!
Rect rect = new Rect();
Flower flower = new Flower();
drawMap(rect);
drawMap(flower);
}
}
小總結:
- 介面不能單獨被實體化
- 在呼叫的時候同樣可以創建一個介面的參考,對應到一個子類的實體
- Rect 使用 implements 繼承介面,此時表達的含義不再是 “擴展”,而是 “實作”
擴展(extends) 和 實作(implements)區分:
- 擴展指的是當前已經有一定的功能了,進一步擴充功能
- 實作指的是當前啥都沒有,需要從頭構造出來
5.2 實作多個介面及其他語法規則
同樣的,直接上代碼:
//定義介面 IA
interface IA {
//介面中的成員變數,默認是 public static final 的!
//public static final int a = 10;//可以寫成下面的樣子!
int A = 10;//相當于是常量
void funcA();//方法就這樣寫,默認是 pubilc abstract 的!
}
//定義介面 IB
interface IB {
void funcB();
}
//定義抽象類 B
abstract class B {
//抽象類B
}
有的時候我們需要讓一個類同時繼承自多個父類,
這件事情在有些編程語言通過 多繼承 的方式來實作的,
然而 Java 中只支持單繼承,一個類只能 extends 一個父類!
但是可以同時實作多個介面,也能達到多繼承類似的效果!
//下面的類A繼承了抽象類B(普通類也可以),但是只能單繼承!
//同時,也可以實作多介面,介面之間用逗號隔開!
class A extends B implements IA,IB {
@Override
public void funcA() { //當一個類實作一個介面并在重寫方法的時候,方法必須是 pubilc 的!
//如果不是 pubilc 權限更加嚴格了,所以無法覆寫
System.out.println("Override funcA");
System.out.println(A);//當然也可以訪問介面中的東西!
}
@Override
public void funcB() {
System.out.println("Override funcB");
}
}
這樣設計有什么好處呢?
時刻牢記多型的好處,讓程式猿忘記型別,
有了介面之后,類的使用者就不必關注具體型別,
而只關注某個類是否具備某種能力,
補充提示:
- 我們創建介面的時候,介面的命名一般以大寫字母 I 開頭
- 介面的命名一般使用 “形容詞” 詞性的單詞(介面表達的含義是
具有 xxx 特性) - 阿里編碼規范中約定,介面中的方法和屬性不要加任何修飾符號,保持代碼的簡潔性
5.3 介面之間的繼承
剛剛說了,類和介面之間的關系是 implements 操作的,
我想提出的問題是:
那么介面和介面之間會存在什么樣的關系呢?
interface IA1 {
void funcA();
}
//介面和介面之間可以使用extends來操作他們的關系,此時,這里面意為:拓展,
interface IB1 extends IA1 {
void funcB();
}
class C implements IB1 {
@Override
public void funcB() {
System.out.println("光重寫B還不夠!");
}
@Override
public void funcA() {
System.out.println("還要重寫A!");
}
}
一個介面IB1通過extends來拓展另一個介面IA1的功能,
此時當一個類C通過implements實作這個介面IB1的時候,
此時重寫的方法不僅僅是IB1的抽象方法,還有他從IA1介面拓展來的功能(方法),
5.4 介面使用實體
在這一個小結給大家介紹三個常用的介面!
Comparable介面
直接上代碼:
class Student {
public int age;
public String name;
public double score;
public Student(int age, String name, double score) {
this.age = age;
this.name = name;
this.score = score;
}
@Override
public String toString() {
return "Student{" +
"age=" + age +
", name='" + name + '\'' +
", score=" + score +
'}';
}
}
public class TestDemo7 {
public static void main(String[] args) {
Student[] students = new Student[3];
students[0] = new Student(12,"huahua",56);
students[1] = new Student(23,"wewe",34);
students[2] = new Student(32,"rous",78);
System.out.println(Arrays.toString(students));
Arrays.sort(students);//問題就出在sort這個方法里
System.out.println(Arrays.toString(students));
}
}
按照上面運行會報錯的,因為沒有一個可以排序的依靠(沒有一個東西作比較)!
大家感興趣可以多看看底層的代碼!

把代碼修改一下后是這樣子的:
//這里直接實作一個Comparable介面
class Student implements Comparable<Student> {
public int age;
public String name;
public double score;
//這里要重寫下面的方法,就是依靠什么條件來排序的,下面是依靠年齡舉例!
@Override
public int compareTo(Student o) { //誰呼叫這個方法 誰就是this
/*if(this.age > o.age) {
return 1;
}else if(this.age == o.age) {
return 0;
}else {
return -1;
}*/
//更簡單的實作(從小到大)
return this.age - o.age;
}
public Student(int age, String name, double score) {
this.age = age;
this.name = name;
this.score = score;
}
@Override
public String toString() {
return "Student{" +
"age=" + age +
", name='" + name + '\'' +
", score=" + score +
'}';
}
}
public class TestDemo7 {
//了解compareTo是如何用的!
/* public static void main(String[] args) {
Student student1 = new Student(98,"huahua",56);
Student student2 = new Student(45,"wewe",34);
System.out.println(student1.compareTo(student2));//這里是一個大于零的數字
}*/
public static void main(String[] args) {
Student[] students = new Student[3];
students[0] = new Student(12,"huahua",56);
students[1] = new Student(45,"wewe",34);
students[2] = new Student(32,"rous",78);
System.out.println(Arrays.toString(students));
Arrays.sort(students);//默認是從小到大排序
System.out.println(Arrays.toString(students));
}
總結一下就是:如果要進行自定義型別大小的比較,一定要實作可以比較的介面!
但是上面的Comparable介面有一個缺點,如果要換成分數比較代碼改動就比較大!
缺點:對類的侵入性非常強,一旦寫好了,不敢輕易改動!
Comparator介面
所以,有一種更好的方式!
就是所說的比較器,直接貼代碼:
class Student {
public int age;
public String name;
public double score;
public Student(int age, String name, double score) {
this.age = age;
this.name = name;
this.score = score;
}
@Override
public String toString() {
return "Student{" +
"age=" + age +
", name='" + name + '\'' +
", score=" + score +
'}';
}
}
//這里有一個快捷鍵 alt+7 就是看里面有啥方法!
class AgeComparator implements Comparator<Student> {
@Override
public int compare(Student o1, Student o2) {
return o1.age - o2.age;
}
}
class Score implements Comparator<Student> {
@Override
public int compare(Student o1, Student o2) {
return (int)(o1.score - o2.score);//強制轉化成int型別
}
}
//主要使用的介面就是這個!
//同樣是今天講解的第二個常用介面!
class NameComparator implements Comparator<Student> {
@Override
public int compare(Student o1, Student o2) {
return o1.name.compareTo(o2.name);
}
}
public class TestDemo7 {
//了解compareTo是如何用的!
/* public static void main(String[] args) {
Student student1 = new Student(98,"huahua",56);
Student student2 = new Student(45,"wewe",34);
//了解compareTo是如何用的!
// System.out.println(student1.compareTo(student2));//這里是一個大于零的數字
//了解compare是如何用的!
AgeComparator ageComparator = new AgeComparator();
System.out.println(ageComparator.compare(student1,student2));//這里是一個大于零的數字
}*/
public static void main(String[] args) {
Student[] students = new Student[3];
students[0] = new Student(12,"huahua",56);
students[1] = new Student(45,"wewe",34);
students[2] = new Student(32,"rous",78);
System.out.println(Arrays.toString(students));//排序前列印
AgeComparator ageComparator = new AgeComparator();
Score score = new Score();
NameComparator nameComparator = new NameComparator();
//這里可以使用不同的比較器!
Arrays.sort(students,nameComparator);//這里ageComparator(傳的就是一個比較器),建議看原始碼!
System.out.println(Arrays.toString(students));//排序后列印
}
}
所以,我們就可以推匯出比較器的好處就是:靈活!
對類的侵入性非常弱!
那以后是用Comparable介面還是Comparator介面,取決于你的業務,一般推薦比較器!
Cloneable介面及深拷貝和淺拷貝
/**
* 現在講第三個介面!
*
* 我們繼續來探討一下 創建物件的方式:
* 1:new關鍵字
* 2:實作Cloneable介面
*/
//要想一個類被克隆就要實作Cloneable介面
class Person implements Cloneable{
public int age;
public void eat() {
System.out.println("Eatting!");
}
@Override
public String toString() {
return "Person{" +
"age=" + age +
'}';
}
/**
* clone方法比較特殊,底層是用C/C++實作的,如果要使用它就必須override
* @return
* @throws CloneNotSupportedException
*/
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();//這里沒有具體的重寫實作,其實就是呼叫的C/C++代碼!
}
}
public class Main {
public static void main(String[] args) throws CloneNotSupportedException {
Person person = new Person();
person.age = 99;//這里賦值
Person person1 = (Person) person.clone();//在記憶體上,拷貝的age也是99!
//如果是修改拷貝(person1)的值,也不會影響person的值!
}
//決定是深拷貝還是淺拷貝,不是方法所決定的,而是代碼的實作!
//所以說Clone不能說是深拷貝,
//但是,我們要想辦法讓它變成深拷貝!
}
它的記憶體圖是這樣的:

在說如何深拷貝之前,先了解一下Cloneable介面!
我們點開Cloneable介面看看代碼:

在這里就會牽扯一道面試題:你知道Cloneable介面嗎?為啥這個介面是一個空介面?有啥用?
很簡單,因為這個介面是空的,所以是空介面,
但是它是一個標志介面,代表當前類是可以被克隆的!
然后說一下這個介面咋用:
第一次用的時候,它就會報錯,需要拋例外解決!
按住ALT+ENTER,點擊選項就可以了!

接著往下看:
/**
* 現在來說如何深拷貝!
* 下面將上面的代碼,進行升華!
* 在此之前了解什么是淺拷貝!
*/
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();
public void eat() {
System.out.println("Eatting!");
}
@Override
public String toString() {
return "Person{" +
"age=" + age +
'}';
}
@Override
protected Object clone() throws CloneNotSupportedException {
//return super.clone();//這里沒有具體的重寫實作,其實就是呼叫的C/C++代碼!
Person tmp = (Person)super.clone();
tmp.money = (Money) this.money.clone();
return tmp;
//上面的操作就是一個 深拷貝 的作用!
}
}
public class Main {
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 = 99.99;
System.out.println(person.money.m);
System.out.println(person2.money.m);
//列印出來后都是一樣的值,為什么呢?看記憶體圖!
//所以這里是淺拷貝!
//如何深拷貝呢?
//決定是深拷貝還是淺拷貝,不是方法所決定的,而是代碼的實作!
}
}
它的記憶體圖是這樣的:

如何深拷貝呢?
//就是重寫這個類!
@Override
protected Object clone() throws CloneNotSupportedException {
// return super.clone();//這里沒有具體的重寫實作,其實就是呼叫的C/C++代碼!
Person tmp = (Person)super.clone();
tmp.money = (Money) this.money.clone();
return tmp;
//上面的操作就是一個 深拷貝 的作用!
}
}
它的記憶體圖是這樣的:

很重要的一句話:決定是深拷貝還是淺拷貝,不是方法所決定的,而是代碼的實作!
5.5 抽象類和介面的區別
抽象類: 一個類被 abstract 修飾,就直接叫抽象類(定義不重要!)
-
抽象類不能直接實體化(會直接報錯)
-
抽象類中可以有普通的方法和成員
-
普通類繼承了抽象類,這個普通類中必須重寫抽象類的所有抽象方法(可以被重寫和呼叫)
-
抽象方法不能是
private修飾的 -
一個抽象類B繼承了抽象類A,那么這個抽象類A中可以不實作抽象類A的抽象方法!
-
在上條繼承關系的基礎上,普通類C繼承了抽象類B,那么A和B中的抽象方法必須被重寫!
-
抽象類和抽象方法是不能被
final修飾!
介面: 在一個類中,同名的方法如果有不同的引數串列(引數型別不同、引數個數不同甚至是引數順序不同)則視為多載,同時,多載對回傳型別沒有要求,可以相同也可以不同,所以不能通過回傳型別是否相同來判斷多載,
- 介面不能單獨被實體化
- 介面中包含的方法都是
抽象方法,欄位只能包含靜態常量! - 介面中的普通方法不能有具體的實作,如果要實作,就要使用 default 關鍵字修飾這個方法!
- 介面中可以有靜態方法,這個靜態方法中可以有方法體
- 介面中的方法一定是抽象方法,因此可以省略 abstract
- 介面中的方法一定是 public,因此可以省略 public
注意點與區別總結:
- 都不能被單獨實體化
- 抽象類使用 extends 關鍵字來繼承抽象類;子類使用關鍵字 implements 來實作介面
- 抽象方法可以有 public 、protected 和 default 這些修飾符;介面方法默認修飾符是 public,不可以使用其它修飾符,
- 抽象類只能被單繼承;介面可以多實作
- 抽象類中可以有普通的方法和成員;介面中包含的方法都是
抽象方法,欄位只能包含靜態常量!

全文結束
這一篇文章到這里就結束了,期間訪問了大量文獻,包括各種課件、博客、檔案等等!
對了,前面說的一個關于面向物件的訓練,鏈接在左邊!
寫作實屬不易,你們的支持就是我最大的動力!跪求三連!!!
累!
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/413398.html
標籤:java
