Java 基礎篇之類與對象
- 2019 年 10 月 3 日
- 筆記
類與對象
類、對象和引用的關係
類和對象的關係
- 類是對象的模版,對象是類的一個實例,一個類可以有很多對象
- 一個Java程式中類名相同的類只能有一個,也就是類型不會重名
- 一個對象只能根據一個類來創建
引用和類以及對象的關係
- 引用只能指向其所屬的類型的類的對象
- 相同類型的引用之間可以賦值
- 只能通過指向一個對象的引用,來操作一個對象,比如訪問某個成員變數
方法
參數傳遞方式
Java 總是採用按值調用。
基本類型的值傳遞
public class PrimitiveTransferTest { public static void swap(int a, int b) { int tmp = a; a = b; b = tmp; System.out.println("swap 方法里 a 的值為: " + a + " b的值為: " + b); } public static void main(String[] args) { int a = 6; int b = 9; swap(a, b); System.out.println("交換結束後 a 的值為 " + a + " b的值為 " + b); } } /** 運行結果: swap 方法里 a 的值為: 9 b的值為: 6 交換結束後 a 的值為 6 b的值為 9 */
分析圖:
java 程式總是從 main() 方法開始執行,main() 方法定義了 a、b 兩個局部變數,兩個變數在 main 棧區中。在 main() 方法中調用 swap() 方法時,main() 方法此時還未結束,因此系統為 main 方法和 swap 方法分配了兩塊棧區,用於保存 main 方法和 swap 方法的局部變數。main 方法中的 a、b 變數作為參數傳入 swap 方法,實際上是在 swap 方法棧區中重新產生了兩個變數 a、b,並將 main 方法棧區中 a、b 變數的值分別賦給 swap 方法棧區中的 a、b 參數(這就是初始化)。此時系統記憶體中有兩個 a 變數、兩個 b 變數,只是存在於不同的方法棧區中而已。
引用類型的參數傳遞
public class ReferenceTransferTest { public static void swap(DataWrap dw) { int tmp = dw.a; dw.a = dw.b; dw.b = tmp; System.out.println("swap 方法里, a 成員變數的的值為: " + dw.a + " b 成員變數的值為: " + dw.b); } public static void main(String[] args) { DataWrap dw = new DataWrap(); dw.a = 6; dw.b = 9; swap(dw); System.out.println("交換結束後, a 成員變數的的值為: " + dw.a + " b 成員變數的值為: " + dw.b); } } /** swap 方法里, a 成員變數的的值為: 9 b 成員變數的值為: 6 交換結束後, a 成員變數的的值為: 9 b 成員變數的值為: 6 */
你可能會疑問,dw 對象的成員變數 a、b的值也被替換了,這跟前面基本類型的傳遞完全不一樣。這非常容易讓人覺得,調用傳入 swap 方法的就是 dw 對象本身,而不是它的複製品。其實傳遞的依然是 dw 的值。
分析圖:
系統一樣賦值了 dw 的副本,只是關鍵在於 dw 只是一個引用變數,它存儲的值只是一段記憶體地址,將該記憶體地址傳遞給 swap 棧區,此時 swap 棧區的 dw 和 main 棧區的 dw 的值也就是記憶體地址相同,該段記憶體地址指向堆記憶體中的 DataWrap 對象。對 swap 棧區的 dw 操作,也就是對 DataWrap 對象操作。
重載
重載:同一個類中,方法名相同,參數列表不同。
當調用被重載的方法時,根據參數的個數和類型判斷應該調用哪個重載方法,參數完全匹配的方法將被執行。
構造器
默認無參構造器
僅當類沒有定義任何構造器的時候,系統才會提供一個默認的構造器。這個構造器將所有的實例域設置為默認值。
自定義構造器
當類中有自定義構造器時,系統不會再提供默認的構造器
靜態常量
public static final double PI = 3.1415926
靜態方法
在類載入的時候就存在了,不依賴於任何類的任何實例。
建議通過類名調用,而不是通過實例對象調用,否則很容易混淆概念。
繼承
java 使用 extends 作為繼承的關鍵字,有趣的是 extends 是擴展的意思,並不是繼承。但是 extends 很好體現了子類和父類的關係,子類是對父類的擴展,子類是一種特殊的父類。擴展更加準確。ps:這個理解真的是流弊啊。
子類重寫父類方法(覆蓋)
方法的重寫遵循 「兩同兩小一大」規則:
-
方法名相同、形參列表相同
-
子類方法返回的值類型應比父類方法返回值類型更小或相等,子類方法聲明拋出的異常類應該比父類方法聲明拋出的異常類更小或相等
-
子類方法訪問許可權應該比父類方法的訪問許可權更大或相等
子類中調用父類被覆蓋的方法
- 如果被覆蓋的方法是實例方法,使用 super 關鍵字
- 如果被覆蓋的方法是類方法,使用父類類名
- 子類不能調用父類中被 private 修飾的方法和屬性
子類調用父類構造器
-
子類不能繼承父類的構造器。在子類的構造器中,如果沒有顯式使用 super 調用父類的構造函數,那麼系統一定會在子類構造器執行之前,隱式的調用父類的無參構造器
-
在子類構造器中,可以使用 super 顯式調用父類構造器,但 super 語句必須在第一行
多態
向上類型轉換
Java 引用變數有兩個類型。如果編譯時類型和運行時類型不一致,就可能出現多態。
-
編譯時類型:由聲明該變數時使用的類型決定
-
運行時類型:由實際賦給該變數的對象決定
示例程式碼:
public class BaseClass { public int book = 6; public void base() { System.out.println("父類的普通方法"); } public void test() { System.out.println("父類的test方法"); } } public class SubClass extends BaseClass { public String book = "輕量級 Java EE"; public void test() { System.out.println("子類的test方法"); } public void sub() { System.out.println("子類的sub方法"); } public static void main(String[] args) { BaseClass ploymophicBc = new SubClass(); System.out.println(ploymophicBc.book); ploymophicBc.base(); ploymophicBc.test(); // 因為 ploymophicBc 的編譯時類型是 BaseClass // BaseClass 類沒有提供 sub 方法,所以下面程式碼編譯時會出錯 // ploymophicBc.sub(); } }
上面的例子中,引用變數 ploymophicBc 比較特殊,它的編譯時類型是 BaseClass,而運行時類型是 SubClass。
ploymophicBc.sub() 這行程式碼會在編譯時報錯,因為 ploymophicBc 編譯時類型為 BaseClass,而 BaseClass 中沒有定義 sub 方法,因此編譯時無法通過。
但是注意,ploymophicBc.book 的值為 6, 而不是 」輕量級 Java EE「。因為對象的實例變數不具備多態性,系統總是試圖訪問它編譯時類型所定義的成員變數,而非運行時。
子類其實是一種特殊的父類,因此 java 允許把父類的引用指向子類對象,這被稱為向上轉型(upcasting),向上轉型由系統自動完成。
可以調用哪些方法,取決於引用類型(編譯時)。
具體調用哪個方法,取決於引用指向的實例對象(運行時)。
向下類型轉換
問題:引用變數在程式碼編譯過程中,只能調用它編譯時類型具備的方法,而不能調用它運行時類型具備的方法
解決:強制轉換成運行時類型
方法:引用類型之間的轉換隻能在有繼承關係的兩個類型之間進行,否則編譯出錯。如果想把一個父類引用變數的編譯時類型轉換成子類類型,則這個引用變數的運行時類型得是子類類型,否則引發 ClassCastException
示例程式碼:
//創建子類對象 Dog dog = new Dog(); // 向上類型轉換(類型自動提升),不存在風險 Animal animal = dog; // 風險演示 animal 指向 Dog 類型對象,沒有辦法轉化成 Cat 對象,編譯階段不會報錯,但是運行會報錯 Cat cat = (Cat)animal; // 1.編譯時按 Cat 類型 2. 運行時 Dog 類型,類型不匹配,直接報錯
instanceof
為了解決強制類型轉換,可能引發的 ClassCastException 異常,引入 instanceof 運算符。
instanceof 運算符的含義:用於判斷左邊的對象(運行時類型或者叫實際類型)是否是右邊的類或者其子類、實現類的實例。如果是返回 true,否則返回 false。
在之前的程式碼中,強制類型轉換前使用 instanceof 判斷:
if (anmial instanceof Cat) { Cat cat = (Cat)animal; }
final 修飾符
final 修飾類
不能被繼承
final 修飾方法
不可被子類覆蓋
final 修飾變數
特徵:變數一旦被初始化,便不可改變
初始化:定義時直接賦值、藉助構造函數
對於基本類型域而言,其值是不可變的。
對於引用類型變數而言,它保存的僅僅只是個引用。final 只保證這個變數所引用的地址不會改變,即一直引用同一個對象。但這個對象自身內容完全可以發生改變。
Object 類
toString
toString 用於輸出對象的自我描述資訊。
Object 類提供的 toString 返回該對象實現類的 "類名 + @ + hashCode"。通常需要重寫該方法。
==
對於數值類型的基本變數,只要兩個變數的值相等(不需要數據類型完全相同),就返回 true。
對於兩個引用類型的變數,只有它們指向同一個對象時,== 判斷才會返回 true。
equals
equals 方法是 Object 類提供的一個實例方法。對於引用變數,只有指向同一個對象時才返回 true。一般需要重寫 equals 方法。
重寫 equals 方法的示例:
public boolean equals(Object obj) { if (this == obj) { return true; } if (obj !=null && obj.getClass() == Person.class) { Person personObj = (Person)obj; if (this.getIdStr().equals(personObj.getIdStr())) { return true; } } return false; }
equals 為 true,hashCode 就應該相等,這是一種約定俗稱的規範。即 equals 為 true 是 hashCode 相等的充分非必要條件。
介面
設計思想
-
介面體現的是規範和實現分離的設計哲學,讓軟體系統的各組件之間面向介面耦合,是一種松耦合的設計
-
介面定義的是多個類共同的公共行為規範,這些行為是與外部交流的通道,意味著介面通常是定義一組公共方法
定義
-
介面的修飾符,只能是 public 或者 default
-
由於介面定義的是一種規範,所以介面里不能包含構造器和初始化塊定義,只能包含靜態常量、方法(只能是抽象方法,類方法和默認方法)以及內部類、內部介面、內部枚舉
-
介面里的常量只能是靜態常量,默認使用 public static final 修飾
-
介面里的內部類、內部介面、內部枚舉,默認使用 public static 修飾
-
介面里的抽象方法不能有方法體,但類方法和默認方法必須有方法體。
方法說明
介面中定義抽象方法可以省略 abstract 關鍵字和修飾符,默認修飾符為 public。
Java 8 新增允許在介面中定義默認方法,使用 default 修飾。默認情況下,系統使用 public 修飾默認方法。
Java 8 新增允許在介面中定義私有方法。
Java 8 新增允許在介面中定義靜態方法。靜態方法可以被實現的介面的類繼承。
使用
一個類可以實現一個或多個介面。
一個類實現一個或多個介面,這個類必須重寫所實現的介面中的所有抽象方法。否則,該類必須被定義成抽象類,保留從父介面繼承到的抽象方法。
介面不能用來創建實例,但是可以用於聲明引用類型的變數,該變數必須指向實現該介面的類的實例對象。
抽象類
抽象類與普通類的區別,可以概括為 「有得有失」。
得,是指抽象類多了一個能力,抽象類可以包含抽象方法
失,是指抽象類失去了一個能力,抽象類不能用於創建實例
抽象類和普通類的區別:
-
抽象類使用 abstract 修飾
-
抽象類可以和普通類一樣可以包含成員變數、方法、構造器、初始化塊、內部類。但抽象類不能被實例化,抽象類的構造器主要用來被子類調用
-
抽象類可以不包含抽象方法,但是含有抽象方法的類必須被定義為抽象類
抽象類的設計思想:抽象類是模板模式的設計模式體現。抽象類是從多個具體類中抽象出來的父類,具有更高層次的抽象。從多個具有相同特徵的類中抽象出一個抽象類,以這個抽象類為其子類的模板,避免子類設計的隨意性
內部類
成員內部類
非靜態內部類
public class Cow { private double weight; public Cow() { } public Cow(double weight) { this.weight = weight; } // 定義一個非靜態內部類 private class CowLeg { private double length; private String color; public CowLeg() {} public CowLeg(double length, String color) { this.length = length; this.color = color; } public double getLength() { return this.length; } public void setLength(double length) { this.length = length; } public String getColor() { return this.color; } public void setColor(String color) { this.color = color; } public void info() { System.out.println("當前牛腿的顏色是 " + this.color + ", 長 " + this.length); // 直接訪問外部類的 private 修飾的成員變數 System.out.println("該牛腿所屬的奶牛重: " + weight); } } public void test() { CowLeg cl = new CowLeg(1.12, "黑白相間"); cl.info(); } public static void main(String[] args) { Cow cow = new Cow(378.9); cow.test(); } }
在非靜態內部類里可以直接訪問外部類的 private 成員,這是因為在非靜態內部類對象里,保存了一個它所寄生的外部類對象的引用。如下圖:
如果外部類成員變數、內部類成員變數與內部類里方法的局部變數名同名
-
直接訪問局部變數
-
this,訪問內部類實例的變數
-
外部類類名.this.varName 訪問外部類實例變數
外部類不能直接訪問非靜態內部類的成員,無論非靜態內部類的成員是什麼修飾符修飾的。只能顯示創建非靜態內部類對象來訪問其實例成員。
靜態內部類(類內部類)
如果用 static 修飾一個內部類,則這個內部類就屬於外部類本身,而不屬於外部類的某個對象。因此也叫做類內部類。即靜態內部類是外部類的一個靜態成員。
靜態內部類可以包含靜態成員,也可以包含非靜態成員。
靜態內部類不能訪問外部類的實例成員,只能訪問外部類的類成員。
外部類依然不能直接訪問靜態內部類的成員,但可以使用靜態內部類的類名作為調用者來訪問靜態內部類的類成員,也可以使用靜態內部類對象作為調用者來訪問靜態內部類的實例成員。
在外部類之外訪問內部類
在外部類以外的地方訪問內部類(包括靜態和非靜態兩種),則內部類不能使用 private 修飾,private 修飾的內部類只能在外部類內部使用。對於使用其他訪問修飾符的內部類,按照訪問修飾符範圍訪問。
-
省略訪問控制符的內部類,只能被與外部類處於同一個包中的其他類所訪問
-
使用 protected 修飾的內部類,可被與外部類處於同一個包中的其他類和外部類的子類訪問
-
使用 public 修飾符的內部類,可在任何地方被訪問
在外部類之外使用非靜態內部類
由於非靜態內部類的對象必須寄生在外部類的對象里,因此在創建非靜態內部類對象之前,必須先創建其外部類對象。
示例程式碼,如下:
public class Out { // 使用默認訪問控制符,同一個包中的其他類可以訪問該內部類 class In { public In(String msg) { System.out.println(msg); } } }
public class CreateInnerInstance { public static void main(String[] args) { Out.In in = new Out().new In("Test Msg"); /* 上面程式碼可以改為如下三行程式碼 使用 OutterClass.InnerClass 的形式定義內部類變數 Out.In in; 創建外部類實例,非靜態內部類實例將寄生在該實例中 Out out = new Out(); 通過外部類實例和new來調用內部類構造器創建非靜態內部類實例 in = out.new In("Test Msg"); */ } }
下面定義了一個子類繼承了 Out 類的非靜態內部類 In 類
public class SubClass extends Out.In{ // 顯示定義 SubClass 的構造器 public SubClass(Out out){ out.super("hello"); } }
上面的程式碼可能看起來很怪,其實很正常:非靜態內部類 In 類的構造器必須使用外部類對象來調用,程式碼中 super 代表調用 In 類的構造器,而 out 則代表外部類對象。
如果需要創建 SubClass 對象時,必須創建一個 Out 對象。因為 SubClass 是非靜態內部類 In 的子類,非靜態內部類 In 對象里必須有一個對 Out 對象的引用,其子類 SubClass 對象里也應該持有對 Out 對象的引用。當創建 SubClass 對象時傳給該構造器的 Out 對象,就是 SubClass 對象里 Out 對應引用所指向的對象。
結合上面兩段程式碼,非靜態內部類 In 對象和 SubClass 對象都必須持有指向 Outer 對象的引用,區別是創建兩種對象時傳入 Out 對象的方式不同:當創建非靜態內部類 In 類的對象時,必須通過 Outer 對象來調用 new 關鍵字;當創建 SubClass 類的對象時,必須使用 Outer 對象作為調用者來調用 In 類的構造器
在外部類之外使用靜態內部類
因為靜態內部類是外部類類相關的,因此創建靜態內部類對象時無需創建外部類對象。
public class CreateStaticInnerInstance { public static void main(String[] args) { StaticOut.StaticIn in = new StaticOut.StaticIn(); /* 上面的程式碼可改為如下兩行程式碼 使用 OuterClass.InnerClass 的形式定義內部類變數 StaticOut.StaticIn in; 通過 new 調用內部類構造器創建靜態內部類實例 in = new StaticOut.StaticIn(); */ } }
因為調用靜態內部類的構造器時不需要使用外部類對象,所以創建靜態內部類的子類也比較簡單。下面程式碼為靜態靜態內部類 StaticIn 定義了一個空的子類
public class StaticSubClass extends StaticOut.StaticIn {}
局部內部類
匿名內部類
匿名內部類適合創建只需要一次使用的類,創建匿名內部類時會立即創建一個該類的實例,這個類定義立即消失,匿名類不能重複使用。
匿名類是用來創建介面或者抽象類的實例的。
匿名內部類不能定義構造器。因為匿名內部類沒有類名,所有無法定義構造器。但匿名內部類可以定義初始化塊,可以通過實例初始化塊來完成構造器需要完成的事情。
定義匿名內部類格式如下:
new 實現介面 | 抽象父類構造器(實參列表) { 匿名內部類的類體部分 }
最常用的創建匿名內部類的方式是需要創建某個介面類型的對象,如下
public interface ProductA { public double getPrice(); public String getName(); }
public class AnonymousTest { public void test(ProductA p) { System.out.println("Buy a" + p.getName() + "Cost " + p.getPrice()); } public static void main(String[] args) { AnonymousTest ta = new AnonymousTest(); // 調用 test() 方法時,需要傳入一個 Product 參數 // 此處傳入其匿名實現類的實例 ta.test(new ProductA() { @Override public double getPrice() { return 567.8; } @Override public String getName() { return "APG Card"; } }); } }
通過繼承抽象父類來創建匿名內部類時,匿名內部類將擁有和父類相同形參列表的構造器。看下面一段程式碼
public abstract class Device { private String name; public abstract double getPrice(); public Device() {}; public Device(String name) { this.name = name; } public String getName() { return this.name; } public void setName(String name) { this.name = name; } }
public class AnonymousInner { public void test(Device d) { System.out.println("Buy a" + d.getName()+ "Cost" + d.getPrice()); } public static void main(String[] args) { AnonymousInner ai = new AnonymousInner(); // 調用有參數的構造器創建 Device 匿名實現類的對象 ai.test(new Device("電子顯示器") { @Override public double getPrice() { return 67.8; } }); // 調用無參數的構造器創建 Device 匿名實現類的對象 Device d = new Device() { // 初始化塊 { System.out.println("匿名內部類的初始化塊"); } // 實現抽象方法 @Override public double getPrice() { return 56.2; } // 重寫父類的實例方法 public String getName() { return "keyboard"; } }; ai.test(d); } }
歡迎關注我的公眾號