九韵洋、Java 抽象類
當我們要完成的任務是確定的竿刁,但具體的方式需要隨后開個會投票的話,Java 的抽象類就派上用場了麻献。這句話怎么理解呢们妥?搬個小板凳坐好猜扮,聽我來給你講講勉吻。
01、抽象類的 5 個關(guān)鍵點
1)定義抽象類的時候需要用到關(guān)鍵字 abstract
旅赢,放在 class
關(guān)鍵字前齿桃。
public abstract class AbstractPlayer {
}
關(guān)于抽象類的命名,阿里出品的 Java 開發(fā)手冊上有強調(diào)煮盼,“抽象類命名要使用 Abstract 或 Base 開頭”短纵,記住了哦。
2)抽象類不能被實例化僵控,但可以有子類香到。
嘗試通過 new
關(guān)鍵字實例化的話,編譯器會報錯报破,提示“類是抽象的悠就,不能實例化”。
通過 extends
關(guān)鍵字可以繼承抽象類充易,繼承后梗脾,BasketballPlayer 類就是 AbstractPlayer 的子類。
public class BasketballPlayer extends AbstractPlayer {
}
3)如果一個類定義了一個或多個抽象方法盹靴,那么這個類必須是抽象類辕狰。
當在一個普通類(沒有使用 abstract
關(guān)鍵字修飾)中定義了抽象方法爆价,編譯器就會有兩處錯誤提示。
第一處在類級別上哄酝,提醒你“這個類必須通過 abstract
關(guān)鍵字定義”吟策,or 的那個信息沒必要儒士,見下圖。
第二處在方法級別上檩坚,提醒你“抽象方法所在的類不是抽象的”着撩,見下圖。
4)抽象類可以同時聲明抽象方法和具體方法匾委,也可以什么方法都沒有拖叙,但沒必要。就像下面這樣:
public abstract class AbstractPlayer {
abstract void play();
public void sleep() {
System.out.println("運動員也要休息而不是挑戰(zhàn)極限");
}
}
5)抽象類派生的子類必須實現(xiàn)父類中定義的抽象方法赂乐。比如說薯鳍,抽象類中定義了 play()
方法,子類中就必須實現(xiàn)挨措。
public class BasketballPlayer extends AbstractPlayer {
@Override
void play() {
System.out.println("我是張伯倫挖滤,籃球場上得過 100 分");
}
}
如果沒有實現(xiàn)的話,編譯器會提醒你“子類必須實現(xiàn)抽象方法”浅役,見下圖斩松。
02、什么時候用抽象類
與抽象類息息相關(guān)的還有一個概念觉既,就是接口惧盹,我們留到下一篇文章中詳細說,因為要說的知識點還是蠻多的奋救。你現(xiàn)在只需要有這樣一個概念就好岭参,接口是對行為的抽象反惕,抽象類是對整個類(包含成員變量和行為)進行抽象尝艘。
(是不是有點明白又有點不明白,別著急姿染,翹首以盼地等下一篇文章出爐吧)
除了接口之外背亥,還有一個概念就是具體的類,就是不通過 abstract
修飾的普通類悬赏,見下面這段代碼中的定義狡汉。
public class BasketballPlayer {
public void play() {
System.out.println("我是詹姆斯,現(xiàn)役第一人");
}
}
有接口闽颇,有具體類盾戴,那什么時候該使用抽象類呢?
1)我們希望一些通用的功能被多個子類復用兵多。比如說尖啡,AbstractPlayer 抽象類中有一個普通的方法 sleep()
橄仆,表明所有運動員都需要休息,那么這個方法就可以被子類復用衅斩。
public abstract class AbstractPlayer {
public void sleep() {
System.out.println("運動員也要休息而不是挑戰(zhàn)極限");
}
}
雖然 AbstractPlayer 類可以不是抽象類——把 abstract
修飾符去掉也能滿足這種場景盆顾。但 AbstractPlayer 類可能還會有一個或者多個抽象方法。
BasketballPlayer 繼承了 AbstractPlayer 類畏梆,也就擁有了 sleep()
方法您宪。
public class BasketballPlayer extends AbstractPlayer {
}
BasketballPlayer 對象可以直接調(diào)用 sleep()
方法:
BasketballPlayer basketballPlayer = new BasketballPlayer();
basketballPlayer.sleep();
FootballPlayer 繼承了 AbstractPlayer 類,也就擁有了 sleep()
方法奠涌。
public class FootballPlayer extends AbstractPlayer {
}
FootballPlayer 對象也可以直接調(diào)用 sleep()
方法:
FootballPlayer footballPlayer = new FootballPlayer();
footballPlayer.sleep();
2)我們需要在抽象類中定義好 API宪巨,然后在子類中擴展實現(xiàn)。比如說溜畅,AbstractPlayer 抽象類中有一個抽象方法 play()
揖铜,定義所有運動員都可以從事某項運動,但需要對應子類去擴展實現(xiàn)达皿。
public abstract class AbstractPlayer {
abstract void play();
}
BasketballPlayer 繼承了 AbstractPlayer 類天吓,擴展實現(xiàn)了自己的 play()
方法。
public class BasketballPlayer extends AbstractPlayer {
@Override
void play() {
System.out.println("我是張伯倫峦椰,我籃球場上得過 100 分龄寞,");
}
}
FootballPlayer 繼承了 AbstractPlayer 類,擴展實現(xiàn)了自己的 play()
方法汤功。
public class FootballPlayer extends AbstractPlayer {
@Override
void play() {
System.out.println("我是C羅物邑,我能接住任意高度的頭球");
}
}
3)如果父類與子類之間的關(guān)系符合 is-a
的層次關(guān)系,就可以使用抽象類滔金,比如說籃球運動員是運動員色解,足球運動員是運動員。
03餐茵、具體示例
為了進一步展示抽象類的特性科阎,我們再來看一個具體的示例。假設(shè)現(xiàn)在有一個文件忿族,里面的內(nèi)容非常簡單——“Hello World”锣笨,現(xiàn)在需要有一個讀取器將內(nèi)容讀取出來,最好能按照大寫的方式道批,或者小寫的方式错英。
這時候,最好定義一個抽象類隆豹,比如說 BaseFileReader:
public abstract class BaseFileReader {
protected Path filePath;
protected BaseFileReader(Path filePath) {
this.filePath = filePath;
}
public List<String> readFile() throws IOException {
return Files.lines(filePath)
.map(this::mapFileLine).collect(Collectors.toList());
}
protected abstract String mapFileLine(String line);
}
filePath 為文件路徑椭岩,使用 protected 修飾,表明該成員變量可以在需要時被子類訪問。
readFile()
方法用來讀取文件判哥,方法體里面調(diào)用了抽象方法 mapFileLine()
——需要子類擴展實現(xiàn)大小寫的方式氮唯。
你看邪媳,BaseFileReader 設(shè)計的就非常合理拥知,并且易于擴展,子類只需要專注于具體的大小寫實現(xiàn)方式就可以了烟央。
小寫的方式:
public class LowercaseFileReader extends BaseFileReader {
protected LowercaseFileReader(Path filePath) {
super(filePath);
}
@Override
protected String mapFileLine(String line) {
return line.toLowerCase();
}
}
大寫的方式:
public class UppercaseFileReader extends BaseFileReader {
protected UppercaseFileReader(Path filePath) {
super(filePath);
}
@Override
protected String mapFileLine(String line) {
return line.toUpperCase();
}
}
你看夺荒,從文件里面一行一行讀取內(nèi)容的代碼被子類復用了——抽象類 BaseFileReader 類中定義的普通方法 readFile()
瞒渠。與此同時,子類只需要專注于自己該做的工作技扼,LowercaseFileReader 以小寫的方式讀取文件內(nèi)容伍玖,UppercaseFileReader 以大寫的方式讀取文件內(nèi)容。
接下來剿吻,我們來新建一個測試類 FileReaderTest:
public class FileReaderTest {
public static void main(String[] args) throws URISyntaxException, IOException {
URL location = FileReaderTest.class.getClassLoader().getResource("helloworld.txt");
Path path = Paths.get(location.toURI());
BaseFileReader lowercaseFileReader = new LowercaseFileReader(path);
BaseFileReader uppercaseFileReader = new UppercaseFileReader(path);
System.out.println(lowercaseFileReader.readFile());
System.out.println(uppercaseFileReader.readFile());
}
}
項目的 resource 目錄下有一個文本文件窍箍,名字叫 helloworld.txt。
可以通過 ClassLoader.getResource()
的方式獲取到該文件的 URI 路徑丽旅,然后就可以使用 LowercaseFileReader 和 UppercaseFileReader 兩種方式讀取到文本內(nèi)容了椰棘。
輸出結(jié)果如下所示:
[hello world]
[HELLO WORLD]
十、Java 接口
對于面向?qū)ο缶幊虂碚f榄笙,抽象是一個極具魅力的特征邪狞。如果一個程序員的抽象思維很差,那他在編程中就會遇到很多困難茅撞,無法把業(yè)務變成具體的代碼帆卓。在 Java 中,可以通過兩種形式來達到抽象的目的米丘,一種是抽象類剑令,另外一種就是接口。
如果你現(xiàn)在就想知道抽象類與接口之間的區(qū)別拄查,我可以提前給你說一個:
- 一個類只能繼承一個抽象類吁津,但卻可以實現(xiàn)多個接口。
當然了靶累,在沒有搞清楚接口到底是什么腺毫,它可以做什么之前,這個區(qū)別理解起來會有點難度挣柬。
01、接口是什么
接口是通過 interface 關(guān)鍵字定義的睛挚,它可以包含一些常量和方法邪蛔,來看下面這個示例。
public interface Electronic {
// 常量
String LED = "LED";
// 抽象方法
int getElectricityUse();
// 靜態(tài)方法
static boolean isEnergyEfficient(String electtronicType) {
return electtronicType.equals(LED);
}
// 默認方法
default void printDescription() {
System.out.println("電子");
}
}
1)接口中定義的變量會在編譯的時候自動加上 public static final
修飾符扎狱,也就是說 LED 變量其實是一個常量侧到。
Java 官方文檔上有這樣的聲明:
Every field declaration in the body of an interface is implicitly public, static, and final.
換句話說勃教,接口可以用來作為常量類使用,還能省略掉 public static final
匠抗,看似不錯的一種選擇故源,對吧?
不過汞贸,這種選擇并不可取绳军。因為接口的本意是對方法進行抽象,而常量接口會對子類中的變量造成命名空間上的“污染”矢腻。
2)沒有使用 private
门驾、default
或者 static
關(guān)鍵字修飾的方法是隱式抽象的,在編譯的時候會自動加上 public abstract
修飾符多柑。也就是說 getElectricityUse()
其實是一個抽象方法奶是,沒有方法體——這是定義接口的本意。
3)從 Java 8 開始竣灌,接口中允許有靜態(tài)方法聂沙,比如說 isEnergyEfficient()
方法。
靜態(tài)方法無法由(實現(xiàn)了該接口的)類的對象調(diào)用初嘹,它只能通過接口的名字來調(diào)用逐纬,比如說 Electronic.isEnergyEfficient("LED")
。
接口中定義靜態(tài)方法的目的是為了提供一種簡單的機制削樊,使我們不必創(chuàng)建對象就能調(diào)用方法豁生,從而提高接口的競爭力。
4)接口中允許定義 default
方法也是從 Java 8 開始的漫贞,比如說 printDescription()
甸箱,它始終由一個代碼塊組成,為實現(xiàn)該接口而不覆蓋該方法的類提供默認實現(xiàn)迅脐,也就是說芍殖,無法直接使用一個“;”號來結(jié)束默認方法——編譯器會報錯的。
允許在接口中定義默認方法的理由是很充分的谴蔑,因為一個接口可能有多個實現(xiàn)類豌骏,這些類就必須實現(xiàn)接口中定義的抽象類,否則編譯器就會報錯隐锭。假如我們需要在所有的實現(xiàn)類中追加某個具體的方法窃躲,在沒有 default
方法的幫助下,我們就必須挨個對實現(xiàn)類進行修改钦睡。
來看一下 Electronic 接口反編譯后的字節(jié)碼吧蒂窒,你會發(fā)現(xiàn),接口中定義的所有變量或者方法,都會自動添加上 public
關(guān)鍵字——假如你想知道編譯器在背后都默默做了哪些輔助洒琢,記住反編譯字節(jié)碼就對了秧秉。
public interface Electronic
{
public abstract int getElectricityUse();
public static boolean isEnergyEfficient(String electtronicType)
{
return electtronicType.equals("LED");
}
public void printDescription()
{
System.out.println("\u7535\u5B50");
}
public static final String LED = "LED";
}
有些讀者可能會問,“二哥衰抑,為什么我反編譯后的字節(jié)碼和你的不一樣象迎,你用了什么反編譯工具?”其實沒有什么秘密呛踊,微信搜「沉默王二」回復關(guān)鍵字「JAD」就可以免費獲取了砾淌,超級好用。
02恋技、定義接口的注意事項
由之前的例子我們就可以得出下面這些結(jié)論:
- 接口中允許定義變量
- 接口中允許定義抽象方法
- 接口中允許定義靜態(tài)方法(Java 8 之后)
- 接口中允許定義默認方法(Java 8 之后)
除此之外拇舀,我們還應該知道:
1)接口不允許直接實例化。
需要定義一個類去實現(xiàn)接口蜻底,然后再實例化骄崩。
public class Computer implements Electronic {
public static void main(String[] args) {
new Computer();
}
@Override
public int getElectricityUse() {
return 0;
}
}
2)接口可以是空的,既不定義變量薄辅,也不定義方法要拂。
public interface Serializable {
}
Serializable 是最典型的一個空的接口,我之前分享過一篇文章《Java Serializable:明明就一個空的接口嘛》站楚,感興趣的讀者可以去我的個人博客看一看脱惰,你就明白了空接口的意義。
http://www.itwanger.com/java/2019/11/14/java-serializable.html
3)不要在定義接口的時候使用 final 關(guān)鍵字窿春,否則會報編譯錯誤拉一,因為接口就是為了讓子類實現(xiàn)的,而 final 阻止了這種行為旧乞。
4)接口的抽象方法不能是 private蔚润、protected 或者 final。
5)接口的變量是隱式 public static final
尺栖,所以其值無法改變嫡纠。
03、接口可以做什么
1)使某些實現(xiàn)類具有我們想要的功能延赌,比如說除盏,實現(xiàn)了 Cloneable 接口的類具有拷貝的功能,實現(xiàn)了 Comparable 或者 Comparator 的類具有比較功能挫以。
Cloneable 和 Serializable 一樣者蠕,都屬于標記型接口,它們內(nèi)部都是空的屡贺。實現(xiàn)了 Cloneable 接口的類可以使用 Object.clone()
方法蠢棱,否則會拋出 CloneNotSupportedException锌杀。
public class CloneableTest implements Cloneable {
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
public static void main(String[] args) throws CloneNotSupportedException {
CloneableTest c1 = new CloneableTest();
CloneableTest c2 = (CloneableTest) c1.clone();
}
}
運行后沒有報錯∷φ唬現(xiàn)在把 implements Cloneable
去掉泻仙。
public class CloneableTest {
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
public static void main(String[] args) throws CloneNotSupportedException {
CloneableTest c1 = new CloneableTest();
CloneableTest c2 = (CloneableTest) c1.clone();
}
}
運行后拋出 CloneNotSupportedException:
Exception in thread "main" java.lang.CloneNotSupportedException: com.cmower.baeldung.interface1.CloneableTest
at java.base/java.lang.Object.clone(Native Method)
at com.cmower.baeldung.interface1.CloneableTest.clone(CloneableTest.java:6)
at com.cmower.baeldung.interface1.CloneableTest.main(CloneableTest.java:11)
至于 Comparable 和 Comparator 的用法,感興趣的讀者可以參照我之前寫的另外一篇文章《來吧量没,一文徹底搞懂Java中的Comparable和Comparator》玉转。
http://www.itwanger.com/java/2020/01/04/java-comparable-comparator.html
2)Java 原則上只支持單一繼承,但通過接口可以實現(xiàn)多重繼承的目的殴蹄。
可能有些讀者會問究抓,“二哥,為什么 Java 只支持單一繼承袭灯?”簡單來解釋一下刺下。
如果有兩個類共同繼承(extends)一個有特定方法的父類,那么該方法會被兩個子類重寫稽荧。然后橘茉,如果你決定同時繼承這兩個子類,那么在你調(diào)用該重寫方法時姨丈,編譯器不能識別你要調(diào)用哪個子類的方法畅卓。這也正是著名的菱形問題,見下圖蟋恬。
ClassC 同時繼承了 ClassA 和 ClassB翁潘,ClassC 的對象在調(diào)用 ClassA 和 ClassB 中重載的方法時,就不知道該調(diào)用 ClassA 的方法歼争,還是 ClassB 的方法拜马。
接口沒有這方面的困擾。來定義兩個接口沐绒,F(xiàn)ly 會飛俩莽,Run 會跑。
public interface Fly {
void fly();
}
public interface Run {
void run();
}
然后讓一個類同時實現(xiàn)這兩個接口洒沦。
public class Pig implements Fly,Run{
@Override
public void fly() {
System.out.println("會飛的豬");
}
@Override
public void run() {
System.out.println("會跑的豬");
}
}
這就在某種形式上達到了多重繼承的目的:現(xiàn)實世界里豹绪,豬的確只會跑,但在雷軍的眼里申眼,站在風口的豬就會飛瞒津,這就需要賦予這只豬更多的能力,通過抽象類是無法實現(xiàn)的括尸,只能通過接口巷蚪。
3)實現(xiàn)多態(tài)。
什么是多態(tài)呢濒翻?通俗的理解屁柏,就是同一個事件發(fā)生在不同的對象上會產(chǎn)生不同的結(jié)果啦膜,鼠標左鍵點擊窗口上的 X 號可以關(guān)閉窗口,點擊超鏈接卻可以打開新的網(wǎng)頁淌喻。
多態(tài)可以通過繼承(extends
)的關(guān)系實現(xiàn)僧家,也可以通過接口的形式實現(xiàn)。來看這樣一個例子裸删。
Shape 是表示一個形狀八拱。
public interface Shape {
String name();
}
圓是一個形狀。
public class Circle implements Shape {
@Override
public String name() {
return "圓";
}
}
正方形也是一個形狀涯塔。
public class Square implements Shape {
@Override
public String name() {
return "正方形";
}
}
然后來看測試類肌稻。
List<Shape> shapes = new ArrayList<>();
Shape circleShape = new Circle();
Shape squareShape = new Square();
shapes.add(circleShape);
shapes.add(squareShape);
for (Shape shape : shapes) {
System.out.println(shape.name());
}
多態(tài)的存在 3 個前提:
1、要有繼承關(guān)系匕荸,Circle 和 Square 都實現(xiàn)了 Shape 接口
2爹谭、子類要重寫父類的方法,Circle 和 Square 都重寫了 name()
方法
3榛搔、父類引用指向子類對象诺凡,circleShape 和 squareShape 的類型都為 Shape,但前者指向的是 Circle 對象药薯,后者指向的是 Square 對象绑洛。
然后,我們來看一下測試結(jié)果:
圓
正方形
也就意味著童本,盡管在 for 循環(huán)中真屯,shape 的類型都為 Shape,但在調(diào)用 name()
方法的時候穷娱,它知道 Circle 對象應該調(diào)用 Circle 類的 name()
方法绑蔫,Square 對象應該調(diào)用 Square 類的 name()
方法。
04泵额、接口與抽象類的區(qū)別
好了配深,關(guān)于接口的一切,你應該都搞清楚了〖廾ぃ現(xiàn)在回到讀者春夏秋冬的那條留言篓叶,“兄弟,說說抽象類和接口之間的區(qū)別羞秤?”
1)語法層面上
- 接口中不能有 public 和 protected 修飾的方法缸托,抽象類中可以有。
- 接口中的變量只能是隱式的常量瘾蛋,抽象類中可以有任意類型的變量俐镐。
- 一個類只能繼承一個抽象類,但卻可以實現(xiàn)多個接口哺哼。
2)設(shè)計層面上
抽象類是對類的一種抽象佩抹,繼承抽象類的類和抽象類本身是一種 is-a
的關(guān)系叼风。
接口是對類的某種行為的一種抽象,接口和類之間并沒有很強的關(guān)聯(lián)關(guān)系棍苹,所有的類都可以實現(xiàn) Serializable
接口无宿,從而具有序列化的功能。
就這么多吧廊勃,能說道這份上懈贺,我相信面試官就不會為難你了经窖。
十一坡垫、Java 繼承
在 Java 中,一個類可以繼承另外一個類或者實現(xiàn)多個接口画侣,我想這一點冰悠,大部分的讀者應該都知道了。還有一點配乱,我不確定大家是否知道溉卓,就是一個接口也可以繼承另外一個接口,就像下面這樣:
public interface OneInterface extends Cloneable {
}
這樣做有什么好處呢搬泥?我想有一部分讀者應該已經(jīng)猜出來了桑寨,就是實現(xiàn)了 OneInterface 接口的類,也可以使用 Object.clone()
方法了忿檩。
public class TestInterface implements OneInterface {
public static void main(String[] args) throws CloneNotSupportedException {
TestInterface c1 = new TestInterface();
TestInterface c2 = (TestInterface) c1.clone();
}
}
除此之外尉尾,我們還可以在 OneInterface 接口中定義其他一些抽象方法(比如說深拷貝),使該接口擁有 Cloneable 所不具有的功能燥透。
public interface OneInterface extends Cloneable {
void deepClone();
}
看到了吧沙咏?這就是繼承的好處:子接口擁有了父接口的方法,使得子接口具有了父接口相同的行為班套;同時肢藐,子接口還可以在此基礎(chǔ)上自由發(fā)揮,添加屬于自己的行為吱韭。
以上吆豹,把“接口”換成“類”,結(jié)論同樣成立理盆。讓我們來定義一個普通的父類 Wanger:
public class Wanger {
int age;
String name;
void write() {
System.out.println("我寫了本《基督山伯爵》");
}
}
然后,我們再來定義一個子類 Wangxiaoer熏挎,使用關(guān)鍵字 extends
來繼承父類 Wanger:
public class Wangxiaoer extends Wanger{
@Override
void write() {
System.out.println("我寫了本《茶花女》");
}
}
我們可以將通用的方法和成員變量放在父類中速勇,達到代碼復用的目的;然后將特殊的方法和成員變量放在子類中坎拐,除此之外烦磁,子類還可以覆蓋父類的方法(比如write()
方法)养匈。這樣,子類也就煥發(fā)出了新的生命力都伪。
Java 只支持單一繼承呕乎,這一點,我在上一篇接口的文章中已經(jīng)提到過了陨晶。如果一個類在定義的時候沒有使用 extends
關(guān)鍵字猬仁,那么它隱式地繼承了 java.lang.Object
類——在我看來,這恐怕就是 Java 號稱萬物皆對象的真正原因了先誉。
那究竟子類繼承了父類的什么呢湿刽?
子類可以繼承父類的非 private 成員變量,為了驗證這一點褐耳,我們來看下面這個示例诈闺。
public class Wanger {
String defaultName;
private String privateName;
public String publicName;
protected String protectedName;
}
父類 Wanger 定義了四種類型的成員變量,缺省的 defaultName铃芦、私有的 privateName雅镊、共有的 publicName、受保護的 protectedName刃滓。
在子類 Wangxiaoer 中定義一個測試方法 testVariable()
:
可以確認仁烹,除了私有的 privateName,其他三種類型的成員變量都可以繼承到咧虎。
同理卓缰,子類可以繼承父類的非 private 方法,為了驗證這一點老客,我們來看下面這個示例僚饭。
public class Wanger {
void write() {
}
private void privateWrite() {
}
public void publicWrite() {
}
protected void protectedWrite() {
}
}
父類 Wanger 定義了四種類型的方法,缺省的 write胧砰、私有的 privateWrite()鳍鸵、共有的 publicWrite()、受保護的 protectedWrite()尉间。
在子類 Wangxiaoer 中定義一個 main 方法偿乖,并使用 new 關(guān)鍵字新建一個子類對象:
可以確認,除了私有的 privateWrite()哲嘲,其他三種類型的方法都可以繼承到贪薪。
不過,子類無法繼承父類的構(gòu)造方法眠副。如果父類的構(gòu)造方法是帶有參數(shù)的画切,代碼如下所示:
public class Wanger {
int age;
String name;
public Wanger(int age, String name) {
this.age = age;
this.name = name;
}
}
則必須在子類的構(gòu)造器中顯式地通過 super 關(guān)鍵字進行調(diào)用,否則編譯器將提示以下錯誤:
修復后的代碼如下所示:
public class Wangxiaoer extends Wanger{
public Wangxiaoer(int age, String name) {
super(age, name);
}
}
is-a 是繼承的一個明顯特征囱怕,就是說子類的對象引用類型可以是一個父類類型霍弹。
public class Wangxiaoer extends Wanger{
public static void main(String[] args) {
Wanger wangxiaoer = new Wangxiaoer();
}
}
同理毫别,子接口的實現(xiàn)類的對象引用類型也可以是一個父接口類型。
public interface OneInterface extends Cloneable {
}
public class TestInterface implements OneInterface {
public static void main(String[] args) {
Cloneable c1 = new TestInterface();
}
}
盡管一個類只能繼承一個類典格,但一個類卻可以實現(xiàn)多個接口岛宦,這一點,我在上一篇文章也提到過了耍缴。另外砾肺,還有一點我也提到了踢故,就是 Java 8 之后诽里,接口中可以定義 default 方法蜜宪,這很方便揪荣,但也帶來了新的問題:
如果一個類實現(xiàn)了多個接口,而這些接口中定義了相同簽名的 default 方法公黑,那么這個類就要重寫該方法钮呀,否則編譯無法通過。
FlyInterface 是一個會飛的接口荣德,里面有一個簽名為 sleep()
的默認方法:
public interface FlyInterface {
void fly();
default void sleep() {
System.out.println("睡著飛");
}
}
RunInterface 是一個會跑的接口,里面也有一個簽名為 sleep()
的默認方法:
public interface RunInterface {
void run();
default void sleep() {
System.out.println("睡著跑");
}
}
Pig 類實現(xiàn)了 FlyInterface 和 RunInterface 兩個接口童芹,但這時候編譯出錯了涮瞻。
原本,default 方法就是為實現(xiàn)該接口而不覆蓋該方法的類提供默認實現(xiàn)的假褪,現(xiàn)在署咽,相同方法簽名的 sleep()
方法把編譯器搞懵逼了,只能重寫了生音。
public class Pig implements FlyInterface, RunInterface {
@Override
public void fly() {
System.out.println("會飛的豬");
}
@Override
public void sleep() {
System.out.println("只能重寫了");
}
@Override
public void run() {
System.out.println("會跑的豬");
}
}
類雖然不能繼承多個類宁否,但接口卻可以繼承多個接口,這一點缀遍,我不知道有沒有觸及到一些讀者的知識盲區(qū)慕匠。
public interface WalkInterface extends FlyInterface,RunInterface{
void walk();
}
十二、this 關(guān)鍵字
在 Java 中域醇,this 關(guān)鍵字指的是當前對象(它的方法正在被調(diào)用)的引用台谊,能理解吧,各位親譬挚?不理解的話锅铅,我們繼續(xù)往下看。
看完再不明白减宣,你過來捶爆我盐须,我保證不還手,只要不打臉漆腌。
01贼邓、消除字段歧義
我敢賭一毛錢姨蟋,所有的讀者,不管男女老少立帖,應該都知道這種用法眼溶,畢竟寫構(gòu)造方法的時候經(jīng)常用啊。誰要不知道晓勇,過來堂飞,我給你發(fā)一毛錢紅包,只要你臉皮夠厚绑咱。
public class Writer {
private int age;
private String name;
public Writer(int age, String name) {
this.age = age;
this.name = name;
}
}
Writer 類有兩個成員變量绰筛,分別是 age 和 name,在使用有參構(gòu)造函數(shù)的時候描融,如果參數(shù)名和成員變量的名字相同铝噩,就需要使用 this 關(guān)鍵字消除歧義:this.age 是指成員變量,age 是指構(gòu)造方法的參數(shù)窿克。
02骏庸、引用類的其他構(gòu)造方法
當一個類的構(gòu)造方法有多個,并且它們之間有交集的話年叮,就可以使用 this 關(guān)鍵字來調(diào)用不同的構(gòu)造方法具被,從而減少代碼量。
比如說只损,在無參構(gòu)造方法中調(diào)用有參構(gòu)造方法:
public class Writer {
private int age;
private String name;
public Writer(int age, String name) {
this.age = age;
this.name = name;
}
public Writer() {
this(18, "沉默王二");
}
}
也可以在有參構(gòu)造方法中調(diào)用無參構(gòu)造方法:
public class Writer {
private int age;
private String name;
public Writer(int age, String name) {
this();
this.age = age;
this.name = name;
}
public Writer() {
}
}
需要注意的是一姿,this()
必須是構(gòu)造方法中的第一條語句,否則就會報錯跃惫。
03叮叹、作為參數(shù)傳遞
在下例中,有一個無參的構(gòu)造方法爆存,里面調(diào)用了 print()
方法蛉顽,參數(shù)只有一個 this 關(guān)鍵字。
public class ThisTest {
public ThisTest() {
print(this);
}
private void print(ThisTest thisTest) {
System.out.println("print " +thisTest);
}
public static void main(String[] args) {
ThisTest test = new ThisTest();
System.out.println("main " + test);
}
}
來打印看一下結(jié)果:
print com.cmower.baeldung.this1.ThisTest@573fd745
main com.cmower.baeldung.this1.ThisTest@573fd745
從結(jié)果中可以看得出來终蒂,this 就是我們在 main()
方法中使用 new 關(guān)鍵字創(chuàng)建的 ThisTest 對象蜂林。
04、鏈式調(diào)用
學過 JavaScript拇泣,或者 jQuery 的讀者可能對鏈式調(diào)用比較熟悉噪叙,類似于 a.b().c().d()
,仿佛能無窮無盡調(diào)用下去霉翔。
在 Java 中睁蕾,對應的專有名詞叫 Builder 模式,來看一個示例。
public class Writer {
private int age;
private String name;
private String bookName;
public Writer(WriterBuilder builder) {
this.age = builder.age;
this.name = builder.name;
this.bookName = builder.bookName;
}
public static class WriterBuilder {
public String bookName;
private int age;
private String name;
public WriterBuilder(int age, String name) {
this.age = age;
this.name = name;
}
public WriterBuilder writeBook(String bookName) {
this.bookName = bookName;
return this;
}
public Writer build() {
return new Writer(this);
}
}
}
Writer 類有三個成員變量子眶,分別是 age瀑凝、name 和 bookName,還有它們仨對應的一個構(gòu)造方法臭杰,參數(shù)是一個內(nèi)部靜態(tài)類 WriterBuilder粤咪。
內(nèi)部類 WriterBuilder 也有三個成員變量,和 Writer 類一致渴杆,不同的是寥枝,WriterBuilder 類的構(gòu)造方法里面只有 age 和 name 賦值了,另外一個成員變量 bookName 通過單獨的方法 writeBook()
來賦值磁奖,注意囊拜,該方法的返回類型是 WriterBuilder,最后使用 return 返回了 this 關(guān)鍵字比搭。
最后的 build()
方法用來創(chuàng)建一個 Writer 對象冠跷,參數(shù)為 this 關(guān)鍵字,也就是當前的 WriterBuilder 對象身诺。
這時候蜜托,創(chuàng)建 Writer 對象就可以通過鏈式調(diào)用的方式。
Writer writer = new Writer.WriterBuilder(18,"沉默王二")
.writeBook("《Web全棧開發(fā)進階之路》")
.build();
05戚长、在內(nèi)部類中訪問外部類對象
說實話盗冷,自從 Java 8 的函數(shù)式編程出現(xiàn)后,就很少用到 this 在內(nèi)部類中訪問外部類對象了同廉。來看一個示例:
public class ThisInnerTest {
private String name;
class InnerClass {
public InnerClass() {
ThisInnerTest thisInnerTest = ThisInnerTest.this;
String outerName = thisInnerTest.name;
}
}
}
在內(nèi)部類 InnerClass 的構(gòu)造方法中,通過外部類.this 可以獲取到外部類對象柑司,然后就可以使用外部類的成員變量了迫肖,比如說 name。
十三攒驰、super 關(guān)鍵字
簡而言之蟆湖,super 關(guān)鍵字就是用來訪問父類的。
先來看父類:
public class SuperBase {
String message = "父類";
public SuperBase(String message) {
this.message = message;
}
public SuperBase() {
}
public void printMessage() {
System.out.println(message);
}
}
再來看子類:
public class SuperSub extends SuperBase {
String message = "子類";
public SuperSub(String message) {
super(message);
}
public SuperSub() {
super.printMessage();
printMessage();
}
public void getParentMessage() {
System.out.println(super.message);
}
public void printMessage() {
System.out.println(message);
}
}
1)super 關(guān)鍵字可用于訪問父類的構(gòu)造方法
你看玻粪,子類可以通過 super(message)
來調(diào)用父類的構(gòu)造方法∮缃颍現(xiàn)在來新建一個 SuperSub 對象,看看輸出結(jié)果是什么:
SuperSub superSub = new SuperSub("子類的message");
new 關(guān)鍵字在調(diào)用構(gòu)造方法創(chuàng)建子類對象的時候劲室,會通過 super 關(guān)鍵字初始化父類的 message伦仍,所以此此時父類的 message 會輸出“子類的message”。
2)super 關(guān)鍵字可以訪問父類的變量
上述例子中的 SuperSub 類中就有很洋,getParentMessage()
通過 super.message
方法父類的同名成員變量 message充蓝。
3)當方法發(fā)生重寫時,super 關(guān)鍵字可以訪問父類的同名方法
上述例子中的 SuperSub 類中就有,無參的構(gòu)造方法 SuperSub()
中就使用 super.printMessage()
調(diào)用了父類的同名方法谓苟。
十四官脓、重寫和重載
先來看一段重寫的代碼吧。
class LaoWang{
public void write() {
System.out.println("老王寫了一本《基督山伯爵》");
}
}
public class XiaoWang extends LaoWang {
@Override
public void write() {
System.out.println("小王寫了一本《茶花女》");
}
}
重寫的兩個方法名相同涝焙,方法參數(shù)的個數(shù)也相同卑笨;不過一個方法在父類中,另外一個在子類中仑撞。就好像父類 LaoWang 有一個 write()
方法(無參)赤兴,方法體是寫一本《基督山伯爵》;子類 XiaoWang 重寫了父類的 write()
方法(無參)派草,但方法體是寫一本《茶花女》搀缠。
來寫一段測試代碼。
public class OverridingTest {
public static void main(String[] args) {
LaoWang wang = new XiaoWang();
wang.write();
}
}
大家猜結(jié)果是什么近迁?
小王寫了一本《茶花女》
在上面的代碼中艺普,們聲明了一個類型為 LaoWang 的變量 wang。在編譯期間鉴竭,編譯器會檢查 LaoWang 類是否包含了 write()
方法歧譬,發(fā)現(xiàn) LaoWang 類有,于是編譯通過搏存。在運行期間瑰步,new 了一個 XiaoWang 對象,并將其賦值給 wang璧眠,此時 Java 虛擬機知道 wang 引用的是 XiaoWang 對象缩焦,所以調(diào)用的是子類 XiaoWang 中的 write()
方法而不是父類 LaoWang 中的 write()
方法,因此輸出結(jié)果為“小王寫了一本《茶花女》”责静。
再來看一段重載的代碼吧袁滥。
class LaoWang{
public void read() {
System.out.println("老王讀了一本《Web全棧開發(fā)進階之路》");
}
public void read(String bookname) {
System.out.println("老王讀了一本《" + bookname + "》");
}
}
重載的兩個方法名相同,但方法參數(shù)的個數(shù)不同灾螃,另外也不涉及到繼承题翻,兩個方法在同一個類中。就好像類 LaoWang 有兩個方法腰鬼,名字都是 read()
嵌赠,但一個有參數(shù)(書名),另外一個沒有(只能讀寫死的一本書)熄赡。
來寫一段測試代碼姜挺。
public class OverloadingTest {
public static void main(String[] args) {
LaoWang wang = new LaoWang();
wang.read();
wang.read("金瓶梅");
}
}
這結(jié)果就不用猜了。變量 wang 的類型為 LaoWang本谜,wang.read()
調(diào)用的是無參的 read()
方法初家,因此先輸出“老王讀了一本《Web全棧開發(fā)進階之路》”;wang.read("金瓶梅")
調(diào)用的是有參的 read(bookname)
方法,因此后輸出“老王讀了一本《金瓶梅》”溜在。在編譯期間陌知,編譯器就知道這兩個 read()
方法時不同的,因為它們的方法簽名(=方法名稱+方法參數(shù))不同掖肋。
簡單的來總結(jié)一下:
1)編譯器無法決定調(diào)用哪個重寫的方法仆葡,因為只從變量的類型上是無法做出判斷的,要在運行時才能決定志笼;但編譯器可以明確地知道該調(diào)用哪個重載的方法沿盅,因為引用類型是確定的,參數(shù)個數(shù)決定了該調(diào)用哪個方法纫溃。
2)多態(tài)針對的是重寫腰涧,而不是重載。
哎紊浩,后悔啊窖铡,早年我要是能把這道面試題吃透的話,也不用被老馬刁難了坊谁。吟一首詩感慨一下人生吧费彼。
青青園中葵,朝露待日晞口芍。
陽春布德澤箍铲,萬物生光輝。
橱尥郑恐秋節(jié)至颠猴,焜黃華葉衰。
百川東到海小染,何時復西歸?
少壯不努力芙粱,老大徒傷悲
另外,我想要告訴大家的是氧映,重寫(Override)和重載(Overload)是 Java 中兩個非常重要的概念,新手經(jīng)常會被它們倆迷惑脱货,因為它們倆的英文名字太像了岛都,中文翻譯也只差一個字。難振峻,太難了臼疫。
十五、static 關(guān)鍵字
先來個提綱挈領(lǐng)(唉呀媽呀扣孟,成語區(qū)博主上線了)吧:
static 關(guān)鍵字可用于變量烫堤、方法、代碼塊和內(nèi)部類,表示某個特定的成員只屬于某個類本身鸽斟,而不是該類的某個對象拔创。
01、靜態(tài)變量
靜態(tài)變量也叫類變量富蓄,它屬于一個類剩燥,而不是這個類的對象。
public class Writer {
private String name;
private int age;
public static int countOfWriters;
public Writer(String name, int age) {
this.name = name;
this.age = age;
countOfWriters++;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
其中立倍,countOfWriters 被稱為靜態(tài)變量灭红,它有別于 name 和 age 這兩個成員變量,因為它前面多了一個修飾符 static
口注。
這意味著無論這個類被初始化多少次变擒,靜態(tài)變量的值都會在所有類的對象中共享。
Writer w1 = new Writer("沉默王二",18);
Writer w2 = new Writer("沉默王三",16);
System.out.println(Writer.countOfWriters);
按照上面的邏輯寝志,你應該能推理得出娇斑,countOfWriters 的值此時應該為 2 而不是 1。從內(nèi)存的角度來看澈段,靜態(tài)變量將會存儲在 Java 虛擬機中一個名叫“Metaspace”(元空間悠菜,Java 8 之后)的特定池中。
靜態(tài)變量和成員變量有著很大的不同败富,成員變量的值屬于某個對象悔醋,不同的對象之間,值是不共享的兽叮;但靜態(tài)變量不是的芬骄,它可以用來統(tǒng)計對象的數(shù)量,因為它是共享的鹦聪。就像上面例子中的 countOfWriters账阻,創(chuàng)建一個對象的時候,它的值為 1泽本,創(chuàng)建兩個對象的時候淘太,它的值就為 2。
簡單小結(jié)一下:
1)由于靜態(tài)變量屬于一個類规丽,所以不要通過對象引用來訪問蒲牧,而應該直接通過類名來訪問;
2)不需要初始化類就可以訪問靜態(tài)變量赌莺。
public class WriterDemo {
public static void main(String[] args) {
System.out.println(Writer.countOfWriters); // 輸出 0
}
}
02冰抢、靜態(tài)方法
靜態(tài)方法也叫類方法,它和靜態(tài)變量類似艘狭,屬于一個類挎扰,而不是這個類的對象翠订。
public static void setCountOfWriters(int countOfWriters) {
Writer.countOfWriters = countOfWriters;
}
setCountOfWriters()
就是一個靜態(tài)方法,它由 static 關(guān)鍵字修飾遵倦。
如果你用過 java.lang.Math 類或者 Apache 的一些工具類(比如說 StringUtils)的話尽超,對靜態(tài)方法一定不會感動陌生。
Math 類的幾乎所有方法都是靜態(tài)的骇吭,可以直接通過類名來調(diào)用橙弱,不需要創(chuàng)建類的對象。
簡單小結(jié)一下:
1)Java 中的靜態(tài)方法在編譯時解析燥狰,因為靜態(tài)方法不能被重寫(方法重寫發(fā)生在運行時階段棘脐,為了多態(tài))。
2)抽象方法不能是靜態(tài)的龙致。
3)靜態(tài)方法不能使用 this 和 super 關(guān)鍵字蛀缝。
4)成員方法可以直接訪問其他成員方法和成員變量。
5)成員方法也可以直接方法靜態(tài)方法和靜態(tài)變量目代。
6)靜態(tài)方法可以訪問所有其他靜態(tài)方法和靜態(tài)變量屈梁。
7)靜態(tài)方法無法直接訪問成員方法和成員變量。
03榛了、靜態(tài)代碼塊
靜態(tài)代碼塊可以用來初始化靜態(tài)變量在讶,盡管靜態(tài)方法也可以在聲明的時候直接初始化,但有些時候霜大,我們需要多行代碼來完成初始化构哺。
public class StaticBlockDemo {
public static List<String> writes = new ArrayList<>();
static {
writes.add("沉默王二");
writes.add("沉默王三");
writes.add("沉默王四");
System.out.println("第一塊");
}
static {
writes.add("沉默王五");
writes.add("沉默王六");
System.out.println("第二塊");
}
}
writes 是一個靜態(tài)的 ArrayList,所以不太可能在聲明的時候完成初始化战坤,因此需要在靜態(tài)代碼塊中完成初始化曙强。
簡單小結(jié)一下:
1)一個類可以有多個靜態(tài)代碼塊。
2)靜態(tài)代碼塊的解析和執(zhí)行順序和它在類中的位置保持一致途茫。為了驗證這個結(jié)論碟嘴,可以在 StaticBlockDemo 類中加入空的 main 方法,執(zhí)行完的結(jié)果如下所示:
第一塊
第二塊
04囊卜、靜態(tài)內(nèi)部類
Java 允許我們在一個類中聲明一個內(nèi)部類娜扇,它提供了一種令人信服的方式,允許我們只在一個地方使用一些變量栅组,使代碼更具有條理性和可讀性袱衷。
常見的內(nèi)部類有四種,成員內(nèi)部類笑窜、局部內(nèi)部類、匿名內(nèi)部類和靜態(tài)內(nèi)部類登疗,限于篇幅原因排截,前三種不在我們本次文章的討論范圍嫌蚤,以后有機會再細說。
public class Singleton {
private Singleton() {}
private static class SingletonHolder {
public static final Singleton instance = new Singleton();
}
public static Singleton getInstance() {
return SingletonHolder.instance;
}
}
以上這段代碼是不是特別熟悉断傲,對脱吱,這就是創(chuàng)建單例的一種方式,第一次加載 Singleton 類時并不會初始化 instance认罩,只有第一次調(diào)用 getInstance()
方法時 Java 虛擬機才開始加載 SingletonHolder 并初始化 instance箱蝠,這樣不僅能確保線程安全也能保證 Singleton 類的唯一性。不過垦垂,創(chuàng)建單例更優(yōu)雅的一種方式是使用枚舉宦搬。
簡單小結(jié)一下:
1)靜態(tài)內(nèi)部類不能訪問外部類的所有成員變量。
2)靜態(tài)內(nèi)部類可以訪問外部類的所有靜態(tài)變量劫拗,包括私有靜態(tài)變量间校。
3)外部類不能聲明為 static。
十六页慷、Java 枚舉
開門見山地說吧憔足,enum(枚舉)是 Java 1.5 時引入的關(guān)鍵字,它表示一種特殊類型的類酒繁,默認繼承自 java.lang.Enum滓彰。
為了證明這一點,我們來新建一個枚舉 PlayerType:
public enum PlayerType {
TENNIS,
FOOTBALL,
BASKETBALL
}
兩個關(guān)鍵字帶一個類名州袒,還有大括號揭绑,以及三個大寫的單詞,但沒看到繼承 Enum 類拔任觥洗做?別著急,心急吃不了熱豆腐啊彰居。使用 JAD 查看一下反編譯后的字節(jié)碼诚纸,就一清二楚了。
public final class PlayerType extends Enum
{
public static PlayerType[] values()
{
return (PlayerType[])$VALUES.clone();
}
public static PlayerType valueOf(String name)
{
return (PlayerType)Enum.valueOf(com/cmower/baeldung/enum1/PlayerType, name);
}
private PlayerType(String s, int i)
{
super(s, i);
}
public static final PlayerType TENNIS;
public static final PlayerType FOOTBALL;
public static final PlayerType BASKETBALL;
private static final PlayerType $VALUES[];
static
{
TENNIS = new PlayerType("TENNIS", 0);
FOOTBALL = new PlayerType("FOOTBALL", 1);
BASKETBALL = new PlayerType("BASKETBALL", 2);
$VALUES = (new PlayerType[] {
TENNIS, FOOTBALL, BASKETBALL
});
}
}
看到?jīng)]陈惰?PlayerType 類是 final 的畦徘,并且繼承自 Enum 類。這些工作我們程序員沒做抬闯,編譯器幫我們悄悄地做了井辆。此外,它還附帶幾個有用靜態(tài)方法溶握,比如說 values()
和 valueOf(String name)
杯缺。
01、內(nèi)部枚舉
好的睡榆,小伙伴們應該已經(jīng)清楚枚舉長什么樣子了吧萍肆?既然枚舉是一種特殊的類袍榆,那它其實是可以定義在一個類的內(nèi)部的,這樣它的作用域就可以限定于這個外部類中使用塘揣。
public class Player {
private PlayerType type;
public enum PlayerType {
TENNIS,
FOOTBALL,
BASKETBALL
}
public boolean isBasketballPlayer() {
return getType() == PlayerType.BASKETBALL;
}
public PlayerType getType() {
return type;
}
public void setType(PlayerType type) {
this.type = type;
}
}
PlayerType 就相當于 Player 的內(nèi)部類包雀,isBasketballPlayer()
方法用來判斷運動員是否是一個籃球運動員。
由于枚舉是 final 的亲铡,可以確保在 Java 虛擬機中僅有一個常量對象(可以參照反編譯后的靜態(tài)代碼塊「static 關(guān)鍵字帶大括號的那部分代碼」)才写,所以我們可以很安全地使用“==”運算符來比較兩個枚舉是否相等,參照 isBasketballPlayer()
方法奖蔓。
那為什么不使用 equals()
方法判斷呢赞草?
if(player.getType().equals(Player.PlayerType.BASKETBALL)){};
if(player.getType() == Player.PlayerType.BASKETBALL){};
“==”運算符比較的時候,如果兩個對象都為 null锭硼,并不會發(fā)生 NullPointerException
房资,而 equals()
方法則會。
另外檀头, “==”運算符會在編譯時進行檢查轰异,如果兩側(cè)的類型不匹配,會提示錯誤暑始,而 equals()
方法則不會搭独。
02、枚舉可用于 switch 語句
這個我在之前的一篇我去的文章中詳細地說明過了廊镜,感興趣的小伙伴可以點擊鏈接跳轉(zhuǎn)過去看一下牙肝。
switch (playerType) {
case TENNIS:
return "網(wǎng)球運動員費德勒";
case FOOTBALL:
return "足球運動員C羅";
case BASKETBALL:
return "籃球運動員詹姆斯";
case UNKNOWN:
throw new IllegalArgumentException("未知");
default:
throw new IllegalArgumentException(
"運動員類型: " + playerType);
}
03、枚舉可以有構(gòu)造方法
如果枚舉中需要包含更多信息的話嗤朴,可以為其添加一些字段配椭,比如下面示例中的 name扼倘,此時需要為枚舉添加一個帶參的構(gòu)造方法色迂,這樣就可以在定義枚舉時添加對應的名稱了。
public enum PlayerType {
TENNIS("網(wǎng)球"),
FOOTBALL("足球"),
BASKETBALL("籃球");
private String name;
PlayerType(String name) {
this.name = name;
}
}
04借卧、EnumSet
EnumSet 是一個專門針對枚舉類型的 Set 接口的實現(xiàn)類吱雏,它是處理枚舉類型數(shù)據(jù)的一把利器敦姻,非常高效(內(nèi)部實現(xiàn)是位向量,我也搞不懂)歧杏。
因為 EnumSet 是一個抽象類镰惦,所以創(chuàng)建 EnumSet 時不能使用 new 關(guān)鍵字。不過犬绒,EnumSet 提供了很多有用的靜態(tài)工廠方法:
下面的示例中使用 noneOf()
創(chuàng)建了一個空的 PlayerType 的 EnumSet旺入;使用 allOf()
創(chuàng)建了一個包含所有 PlayerType 的 EnumSet。
public class EnumSetTest {
public enum PlayerType {
TENNIS,
FOOTBALL,
BASKETBALL
}
public static void main(String[] args) {
EnumSet<PlayerType> enumSetNone = EnumSet.noneOf(PlayerType.class);
System.out.println(enumSetNone);
EnumSet<PlayerType> enumSetAll = EnumSet.allOf(PlayerType.class);
System.out.println(enumSetAll);
}
}
程序輸出結(jié)果如下所示:
[]
[TENNIS, FOOTBALL, BASKETBALL]
有了 EnumSet 后凯力,就可以使用 Set 的一些方法了:
05眨业、EnumMap
EnumMap 是一個專門針對枚舉類型的 Map 接口的實現(xiàn)類急膀,它可以將枚舉常量作為鍵來使用。EnumMap 的效率比 HashMap 還要高龄捡,可以直接通過數(shù)組下標(枚舉的 ordinal 值)訪問到元素。
和 EnumSet 不同慷暂,EnumMap 不是一個抽象類聘殖,所以創(chuàng)建 EnumMap 時可以使用 new 關(guān)鍵字:
EnumMap<PlayerType, String> enumMap = new EnumMap<>(PlayerType.class);
有了 EnumMap 對象后就可以使用 Map 的一些方法了:
和 HashMap 的使用方法大致相同,來看下面的例子:
EnumMap<PlayerType, String> enumMap = new EnumMap<>(PlayerType.class);
enumMap.put(PlayerType.BASKETBALL,"籃球運動員");
enumMap.put(PlayerType.FOOTBALL,"足球運動員");
enumMap.put(PlayerType.TENNIS,"網(wǎng)球運動員");
System.out.println(enumMap);
System.out.println(enumMap.get(PlayerType.BASKETBALL));
System.out.println(enumMap.containsKey(PlayerType.BASKETBALL));
System.out.println(enumMap.remove(PlayerType.BASKETBALL));
程序輸出結(jié)果如下所示:
{TENNIS=網(wǎng)球運動員, FOOTBALL=足球運動員, BASKETBALL=籃球運動員}
籃球運動員
true
籃球運動員
06行瑞、單例
通常情況下奸腺,實現(xiàn)一個單例并非易事,不信血久,來看下面這段代碼
public class Singleton {
private volatile static Singleton singleton;
private Singleton (){}
public static Singleton getSingleton() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
但枚舉的出現(xiàn)突照,讓代碼量減少到極致:
public enum EasySingleton{
INSTANCE;
}
完事了,真的超級短氧吐,有沒有讹蘑?枚舉默認實現(xiàn)了 Serializable 接口,因此 Java 虛擬機可以保證該類為單例筑舅,這與傳統(tǒng)的實現(xiàn)方式不大相同座慰。傳統(tǒng)方式中,我們必須確保單例在反序列化期間不能創(chuàng)建任何新實例翠拣。
07版仔、枚舉可與數(shù)據(jù)庫交互
我們可以配合 Mybatis 將數(shù)據(jù)庫字段轉(zhuǎn)換為枚舉類型。現(xiàn)在假設(shè)有一個數(shù)據(jù)庫字段 check_type 的類型如下:
`check_type` int(1) DEFAULT NULL COMMENT '檢查類型(1:未通過误墓、2:通過)',
它對應的枚舉類型為 CheckType蛮粮,代碼如下:
public enum CheckType {
NO_PASS(0, "未通過"), PASS(1, "通過");
private int key;
private String text;
private CheckType(int key, String text) {
this.key = key;
this.text = text;
}
public int getKey() {
return key;
}
public String getText() {
return text;
}
private static HashMap<Integer,CheckType> map = new HashMap<Integer,CheckType>();
static {
for(CheckType d : CheckType.values()){
map.put(d.key, d);
}
}
public static CheckType parse(Integer index) {
if(map.containsKey(index)){
return map.get(index);
}
return null;
}
}
1)CheckType 添加了構(gòu)造方法,還有兩個字段谜慌,key 為 int 型然想,text 為 String 型。
2)CheckType 中有一個public static CheckType parse(Integer index)
方法畦娄,可將一個 Integer 通過 key 的匹配轉(zhuǎn)化為枚舉類型又沾。
那么現(xiàn)在,我們可以在 Mybatis 的配置文件中使用 typeHandler
將數(shù)據(jù)庫字段轉(zhuǎn)化為枚舉類型熙卡。
<resultMap id="CheckLog" type="com.entity.CheckLog">
<id property="id" column="id"/>
<result property="checkType" column="check_type" typeHandler="com.CheckTypeHandler"></result>
</resultMap>
其中 checkType 字段對應的類如下:
public class CheckLog implements Serializable {
private String id;
private CheckType checkType;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public CheckType getCheckType() {
return checkType;
}
public void setCheckType(CheckType checkType) {
this.checkType = checkType;
}
}
CheckTypeHandler 轉(zhuǎn)換器的類源碼如下:
public class CheckTypeHandler extends BaseTypeHandler<CheckType> {
@Override
public CheckType getNullableResult(ResultSet rs, String index) throws SQLException {
return CheckType.parse(rs.getInt(index));
}
@Override
public CheckType getNullableResult(ResultSet rs, int index) throws SQLException {
return CheckType.parse(rs.getInt(index));
}
@Override
public CheckType getNullableResult(CallableStatement cs, int index) throws SQLException {
return CheckType.parse(cs.getInt(index));
}
@Override
public void setNonNullParameter(PreparedStatement ps, int index, CheckType val, JdbcType arg3) throws SQLException {
ps.setInt(index, val.getKey());
}
}
CheckTypeHandler 的核心功能就是調(diào)用 CheckType 枚舉類的 parse()
方法對數(shù)據(jù)庫字段進行轉(zhuǎn)換杖刷。
恕我直言,我覺得小伙伴們肯定會用 Java 枚舉了驳癌,如果還不會滑燃,就過來砍我!
十七颓鲜、final 關(guān)鍵字
盡管繼承可以讓我們重用現(xiàn)有代碼表窘,但有時處于某些原因典予,我們確實需要對可擴展性進行限制,final 關(guān)鍵字可以幫助我們做到這一點乐严。
01瘤袖、final 類
如果一個類使用了 final 關(guān)鍵字修飾,那么它就無法被繼承昂验。如果小伙伴們細心觀察的話捂敌,Java 就有不少 final 類,比如說最常見的 String 類既琴。
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence,
Constable, ConstantDesc {}
為什么 String 類要設(shè)計成 final 的呢占婉?原因大致有以下三個:
- 為了實現(xiàn)字符串常量池
- 為了線程安全
- 為了 HashCode 的不可變性
更詳細的原因,可以查看我之前寫的一篇文章甫恩。
任何嘗試從 final 類繼承的行為將會引發(fā)編譯錯誤逆济,為了驗證這一點,我們來看下面這個例子磺箕,Writer 類是 final 的奖慌。
public final class Writer {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
嘗試去繼承它,編譯器會提示以下錯誤滞磺,Writer 類是 final 的升薯,無法繼承。
不過击困,類是 final 的涎劈,并不意味著該類的對象是不可變的。
Writer writer = new Writer();
writer.setName("沉默王二");
System.out.println(writer.getName()); // 沉默王二
Writer 的 name 字段的默認值是 null阅茶,但可以通過 settter 方法將其更改為“沉默王二”蛛枚。也就是說,如果一個類只是 final 的脸哀,那么它并不是不可變的全部條件蹦浦。
如果,你想了解不可變類的全部真相撞蜂,請查看我之前寫的文章這次要說不明白immutable類盲镶,我就怎么地。突然發(fā)現(xiàn)蝌诡,寫系列文章真的妙啊溉贿,很多相關(guān)性的概念全部涉及到了。我真服了自己了浦旱。
把一個類設(shè)計成 final 的宇色,有其安全方面的考慮,但不應該故意為之,因為把一個類定義成 final 的宣蠕,意味著它沒辦法繼承例隆,假如這個類的一些方法存在一些問題的話,我們就無法通過重寫的方式去修復它抢蚀。
02镀层、final 方法
被 final 修飾的方法不能被重寫。如果我們在設(shè)計一個類的時候皿曲,認為某些方法不應該被重寫鹿响,就應該把它設(shè)計成 final 的。
Thread 類就是一個例子谷饿,它本身不是 final 的,這意味著我們可以擴展它妈倔,但它的 isAlive()
方法是 final 的:
public class Thread implements Runnable {
public final native boolean isAlive();
}
需要注意的是博投,該方法是一個本地(native)方法,用于確認線程是否處于活躍狀態(tài)盯蝴。而本地方法是由操作系統(tǒng)決定的毅哗,因此重寫該方法并不容易實現(xiàn)。
Actor 類有一個 final 方法 show()
:
public class Actor {
public final void show() {
}
}
當我們想要重寫該方法的話捧挺,就會出現(xiàn)編譯錯誤:
如果一個類中的某些方法要被其他方法調(diào)用虑绵,則應考慮事被調(diào)用的方法稱為 final 方法,否則闽烙,重寫該方法會影響到調(diào)用方法的使用翅睛。
一個類是 final 的,和一個類不是 final黑竞,但它所有的方法都是 final 的捕发,考慮一下,它們之間有什么區(qū)別很魂?
我能想到的一點扎酷,就是前者不能被繼承,也就是說方法無法被重寫遏匆;后者呢法挨,可以被繼承,然后追加一些非 final 的方法幅聘。沒毛病吧凡纳?看把我聰明的。
03喊暖、final 變量
被 final 修飾的變量無法重新賦值惫企。換句話說,final 變量一旦初始化,就無法更改狞尔。之前被一個小伙伴問過丛版,什么是 effective final,什么是 final偏序,這一點页畦,我在之前的文章也有闡述過,所以這里再貼一下地址:
http://www.itwanger.com/java/2020/02/14/java-final-effectively.html
1)final 修飾的基本數(shù)據(jù)類型
來聲明一個 final 修飾的 int 類型的變量:
final int age = 18;
嘗試將它修改為 30研儒,結(jié)果編譯器生氣了:
2)final 修飾的引用類型
現(xiàn)在有一個普通的類 Pig豫缨,它有一個字段 name:
public class Pig {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
在測試類中聲明一個 final 修飾的 Pig 對象:
final Pig pig = new Pig();
如果嘗試將 pig 重新賦值的話,編譯器同樣會生氣:
但我們?nèi)匀豢梢匀バ薷?Pig 的字段值:
final Pig pig = new Pig();
pig.setName("特立獨行");
System.out.println(pig.getName()); // 特立獨行
3)final 修飾的字段
final 修飾的字段可以分為兩種端朵,一種是 static 的好芭,另外一種是沒有 static 的,就像下面這樣:
public class Pig {
private final int age = 1;
public static final double PRICE = 36.5;
}
非 static 的 final 字段必須有一個默認值冲呢,否則編譯器將會提醒沒有初始化:
static 的 final 字段也叫常量舍败,它的名字應該為大寫,可以在聲明的時候初始化敬拓,也可以通過 static 代碼塊初始化邻薯。
- final 修飾的參數(shù)
final 關(guān)鍵字還可以修飾參數(shù),它意味著參數(shù)在方法體內(nèi)不能被再修改:
public class ArgFinalTest {
public void arg(final int age) {
}
public void arg1(final String name) {
}
}
如果嘗試去修改它的話乘凸,編譯器會提示以下錯誤:
厕诡。。营勤。灵嫌。。冀偶。
有些小伙伴可能就忍不住了醒第,這份小白手冊有沒有 PDF 版可以白嫖啊,那必須得有啊进鸠,直接微信搜「沉默王二」回復「小白」就可以了稠曼,不要手軟,覺得不錯的客年,請多多分享——贈人玫瑰霞幅,手有余香哦。
本文已收錄 GitHub量瓜,傳送門~ 司恳,里面更有大廠面試完整考點,歡迎 Star绍傲。
我是沉默王二扔傅,一枚有顏值卻靠才華茍且的程序員耍共。關(guān)注即可提升學習效率,別忘了三連啊猎塞,點贊试读、收藏、留言荠耽,我不挑钩骇,嘻嘻。