final/Object/instanceof

June 6, 2022

如果在指定變數值之後,就不想再改變變數值,可以在宣告變數時加上 final 限定,如果後續撰寫程式時,自己或別人不經意想修改 final 變數,就會出現編譯錯誤。

final 與類別定義

如果物件資料成員被宣告為 final,但沒有明確使用 = 指定值,那表示延遲物件成員值的指定,在建構式執行流程中,一定要有對該資料成員指定值的動作,否則編譯錯誤。

class 前也可以加上 final 關鍵字,如果 class 前使用了 final 關鍵字定義,那麼表示這個類別是最後一個了,不會再有子類別,也就是不能被繼承。有沒有實際的例子呢?有的!String 在定義時就限定為 final 了。

定義方法時,也可以限定該方法為 final,這表示最後一次定義方法了,也就是子類別不可以重新定義 final 方法。有沒有實際的例子呢?有的! java.lang.Object 就有幾個 final方法,例如 notifynotifyAll,如果嘗試在繼承父類別後,重新定義 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 是一種 ObjectDate 是一種 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 方法,其實 toStringObject 定義的方法,ObjecttoString 預設定義為:

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為falseif 區塊中無法存取 cat,這很合理,畢竟 other instanceof Catfalse,本來就不該當成 Cat 來使用,編譯器就直接阻止你使用 cat 了。

不過這只是示範,就這個例子來說,反相 instanceof 的結果,也只是讓可讀性變差了,請別這麼做!

instanceof 模式比對指定的名稱,結合 &&||?: 三元運算子時,也要留意範圍問題。

分享到 LinkedIn 分享到 Facebook 分享到 Twitter