final/Object/instanceof
June 6, 2022如果在指定變數值之後,就不想再改變變數值,可以在宣告變數時加上 final
限定,如果後續撰寫程式時,自己或別人不經意想修改 final
變數,就會出現編譯錯誤。
final 與類別定義
如果物件資料成員被宣告為 final
,但沒有明確使用 =
指定值,那表示延遲物件成員值的指定,在建構式執行流程中,一定要有對該資料成員指定值的動作,否則編譯錯誤。
class
前也可以加上 final
關鍵字,如果 class
前使用了 final
關鍵字定義,那麼表示這個類別是最後一個了,不會再有子類別,也就是不能被繼承。有沒有實際的例子呢?有的!String
在定義時就限定為 final
了。
定義方法時,也可以限定該方法為 final
,這表示最後一次定義方法了,也就是子類別不可以重新定義 final
方法。有沒有實際的例子呢?有的! java.lang.Object
就有幾個 final
方法,例如 notify
、notifyAll
,如果嘗試在繼承父類別後,重新定義 final
方法,會發生編譯錯誤。
在 Java SE API 會宣告為 final
的類別或方法,通常與 JVM 物件或作業系統資源管理有密切相關,因此不希望 API 使用者繼承或重新定義。
java.lang.Object
在 Java 中,子類別只能繼承一個父類別,如果定義類別時沒有使用 extends
關鍵字指定繼承任何類別,那一定是繼承 java.lang.Object
,也就是說,如果你如下定義類別:
public class Some {
...
}
那就相當於如下撰寫:
public class Some extends Object {
...
}
因此在 Java,任何類別追溯至最上層父類別,一定就是 java.lang.Object
,也就是所有物件,都「是一種」Object
,如下撰寫程式是合法的:
Object o1 = "Justin";
Object o2 = new Date();
String
是一種 Object
,Date
是一種 Object
,任何型態的物件,都可以使用 Object
宣告的名稱來參考。這有什麼好處?如果有個需求是使用陣列收集各種物件,那該宣告為什麼型態呢?答案是 Object[]
。例如:
Object[] objs = {"Monica", new Date(), new SwordsMan()};
var name = (String) objs[0];
var date = (Date) objs[1];
var swordsMan = (SwordsMan) objs[2];
因為陣列長度有限,使用陣列來收集物件不是那麼地方便,以下定義的 ArrayList
類別,則可以不限長度地收集物件:
package cc.openhome;
import java.util.Arrays;
public class ArrayList {
private Object[] list;
private int next;
public ArrayList(int capacity) {
list = new Object[capacity];
}
public ArrayList() {
this(16);
}
public void add(Object o) {
if(next == list.length) {
list = Arrays.copyOf(list, list.length * 2);
}
list[next++] = o;
}
public Object get(int index) {
return list[index];
}
public int size() {
return next;
}
}
自定義的 ArrayList
類別,內部使用 Object
陣列來收集物件,每一次收集的物件會放在 next
指定的索引處,在建構 ArrayList
實例時,可以指定內部陣列初始容量,如果使用無參數建構式,則預設容量為 16。
如果要收集物件,可透過 add
方法,參數型態為 Object
,可以接收任何物件,如果內部陣列原長度不夠,就使用 Arrays.copyOf
方法自動建立原長度兩倍的陣列並複製元素,如果想取得收集之物件,可以使用 get
指定索引取得,如果想知道已收集的物件個數,則透過 size
方法得知。
以下使用自定義的 ArrayList
類別,可收集訪客名稱,並將名單轉為大寫後顯示:
package cc.openhome;
import java.util.Scanner;
import static java.lang.System.out;
public class Guest {
public static void main(String[] args) {
var names = new ArrayList();
collectNameTo(names);
out.println("訪客名單:");
printUpperCase(names);
}
static void collectNameTo(ArrayList names) {
var scanner = new Scanner(System.in);
while(true) {
out.print("訪客名稱:");
var name = scanner.nextLine();
if(name.equals("quit")) {
break;
}
names.add(name);
}
}
static void printUpperCase(ArrayList names) {
for(var i = 0; i < names.size(); i++) {
var name = (String) names.get(i);
out.println(name.toUpperCase());
}
}
}
一個執行結果如下所示:
訪客名稱:Justin
訪客名稱:Monica
訪客名稱:Irene
訪客名稱:quit
訪客名單:
JUSTIN
MONICA
IRENE
java.lang.Object
是所有類別的頂層父類別,這代表了 Object
定義的方法都繼承下來了,只要不是被定義為 final
的方法,都可以重新定義。
重新定義 toString
舉例來說,在〈protected/super〉的範例中,SwordsMan
等類別曾定義過 toString
方法,其實 toString
是 Object
定義的方法,Object
的 toString
預設定義為:
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
實際上〈protected/super〉的範例中,SwordsMan
等類別,是重新定義了 toString
,許多方法若傳入物件,預設都會呼叫 toString
,例如 System.out.print
等方法就會呼叫 toString
以取得字串描述來顯示,〈protected/super〉的這個程式片段:
var swordsMan = new SwordsMan();
...略
System.out.println(swordsMan.toString());
var magician = new Magician();
...略
System.out.printf(magician.toString());
實際上只要這麼撰寫就可以了:
var swordsMan = new SwordsMan();
...略
System.out.println(swordsMan);
var magician = new Magician();
...略
System.out.printf(magician);
重新定義 equals
要比較兩個物件的實質相等性,並不是使用 ==
,而是透過 equals
方法,你看過 Integer
等包裹器,以及字串相等性比較時,都是使用 equals
方法。
實際上 equals
方法是 Object
類別就有定義的方法,其程式碼實作是:
public boolean equals(Object obj) {
return (this == obj);
}
如果沒有重新定義 equals
,使用 equals
方法時,作用等同於 ==
,要比較實質相等性,必須自行重新定義。一個簡單的例子是比較,兩個 Cat
物件是否實際上代表同一隻 Cat
的資料:
public class Cat {
...
public boolean equals(Object other) {
// other 參考的就是這個物件,當然是同一物件
if(this == other) {
return true;
}
/* other 參考的物件是不是 Cat 建構出來的
例如若是 Dog 建構出來的當然就不用比了 */
if(other instanceof Cat) {
var cat = (Cat) other;
// 定義如果名稱與生日,表示兩個物件實質上相等
return getName().equals(cat.getName()) &&
getBirthday().equals(cat.getBirthday());
}
return false;
}
}
這個程式片段示範了 equals
實作的基本概念,相關說明都以註解方式呈現了,這邊也看到了 instanceof
運算子,它可以用來判斷物件是否由某個類別建構,左運算元是物件,右運算元是類別,在使用 instanceof
時,編譯器還會來幫點忙,會檢查左運算元型態是否在右運算元型態的繼承架構中(或介面實作架構中,之後會說明介面)。
執行時期,並非只有左運算元物件為右運算元類別直接實例化才傳回 true
,只要左運算元型態是右運算元型態的子類型,instanceof
也是傳回 true
。
這邊僅示範了 equals
實作的基本概念,實際上實作 equals
並非這麼簡單,實 equals
時通常也要實作 hashCode
,這之後還會說明。
instanceof 模式比對
Java SE 16 為 instanceof
增加了模式比對(Pattern matching)的功能,右運算元的型態右方,可以指定名稱,若型態比對符合,物件就會指定給該名稱,例如:
public class Cat {
...
public boolean equals(Object other) {
// other參考的就是這個物件,當然是同一物件
if(this == other) {
return true;
}
// 使用Java SE 16模式比對
if(other instanceof Cat cat) {
return getName().equals(cat.getName()) &&
getBirthday().equals(cat.getBirthday());
}
return false;
}
}
instanceof
模式比對時指定的名稱,只有在 instanceof
判斷為 true
的場合才能存取,例如:
public class Cat {
...
public boolean equals(Object other) {
if(this == other) {
return true;
}
if(!(other instanceof Cat cat)) {
// 因為other instanceof Cat 為 false,這邊不能存取 cat
return false;
}
// 這邊可以存取 cat
return getName().equals(cat.getName()) &&
getBirthday().equals(cat.getBirthday());
}
}
在上例中,雖然if中的條件判斷式 !(other instanceof Cat cat)
結果為 true
,然而因為 other instanceof Cat為false
,if
區塊中無法存取 cat
,這很合理,畢竟 other instanceof Cat
為 false
,本來就不該當成 Cat
來使用,編譯器就直接阻止你使用 cat
了。
不過這只是示範,就這個例子來說,反相 instanceof
的結果,也只是讓可讀性變差了,請別這麼做!
instanceof
模式比對指定的名稱,結合 &&
、||
或 ?:
三元運算子時,也要留意範圍問題。