java.lang.Object


在Java中,子類別只能繼承一個父類別,如果定義類別時沒有使用extends關鍵字指定繼承任何類別,那一定是繼承java.lang.Object,也就是說,如果你如下定義類別:

public class Some {
   ...
}

那就相當於如下撰寫:

public class Some extends Object {
   ...
}

因此在Java中,任何類別追溯至最上層父類別,一定就是java.lang.Object,也就是Java中所有物件,一定「是一種」Object,所以如下撰寫程式是合法的:

Object o1 = "Justin";
Object o2 = new Date();

String是一種ObjectDate是一種Object,任何型態的物件,都可以使用Object宣告的名稱來參考。這有什麼好處?如果有個需求是使用陣列收集各種物件,那該宣告為什麼型態呢?答案是Object[]。例如:

Object[] objs = {"Monica", new Date(), new SwordsMan()};
String name = (String) objs[0];
Date date = (Date) objs[1];
SwordsMan 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) {
        ArrayList names = new ArrayList();
        collectNameTo(names);
        out.println("訪客名單:");
        printUpperCase(names);
    }

    static void collectNameTo(ArrayList names) {
        Scanner scanner = new Scanner(System.in);
        String name;
        while(true) {
            out.print("訪客名稱:");
            name = scanner.nextLine();
            if(name.equals("quit")) {
                break;
            }
            names.add(name);
        }
    }

    static void printUpperCase(ArrayList names) {
        for(int i = 0; i < names.size(); i++) {
            String name = (String) names.get(i);
            out.println(name.toUpperCase());
        }        
    }
}

一個執行結果如下所示:

訪客名稱:Justin
訪客名稱:Monica
訪客名稱:Irene
訪客名稱:quit
訪客名單:
JUSTIN
MONICA
IRENE

java.lang.Object是所有類別的頂層父類別,這代表了Object上定義的方法,所有物件都繼承下來了,只要不是被定義為final的方法,都可以重新定義。

重新定義toString()

舉例來說,在 protected 成員 的範例中,SwordsMan等類別曾定義過toString()方法,其實toString()Object上定義的方法,ObjecttoString()預設定義為:

public String toString() {
        return getClass().getName() + "@" + Integer.toHexString(hashCode());
}

目前你不用特別知道這段程式碼詳細內容,總之傳回的字串包括了類別名稱以及16進位雜湊碼,通常這並沒有什麼閱讀上的意義。實際上 protected 成員 的範例中,SwordsMan等類別,是重新定義了toString(),許多方法若傳入物件,預設都會呼叫toString(),例如System.out.print()等方法就會呼叫toString()以取得字串描述來顯示,所以 protected 成員 的這個程式片段:

SwordsMan swordsMan = new SwordsMan();
...略
System.out.println(swordsMan.toString());
Magician magician = new Magician();
...略
System.out.printf(magician.toString());

實際上只要這麼撰寫就可以了:

SwordsMan swordsMan = new SwordsMan();
...略
System.out.println(swordsMan);
Magician magician = new Magician();
...略
System.out.printf(magician);

重新定義equals()
在Java中要比較兩個物件的實質相等性,並不是使用==,而是透過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) {
            Cat cat = (Cat) other;
            // 定義如果名稱與生日,表示兩個物件實質上相等
            return getName().equals(cat.getName()) && getBirthday().equals(cat.getBirthday());
        }

        return false;
    }
}
   
這個程式片段示範了equals()實作的基本概念,相關說明都以註解方式呈現了,這邊也看到了instanceof運算子,它可以用來判斷物件是否由某個類別建構,左運算元是物件,右運算元是類別,在使用instanceof時,編譯器還會來幫點忙,會檢查左運算元型態是否在右運算元型態的繼承架構中(或介面實作架構中,之後會說明介面)。例如:

String跟Date在繼承架構上一點關係也沒有


執行時期,並非只有左運算元物件為右運算元類別直接實例化才傳回true,只要左運算元型態是右運算元型態的子類型,instanceof也是傳回true

不過,這邊的equals()並不安全,如果getName()getBirthday()傳回null的話,那麼就會噴出NullPointerException了,自行加些檢查是否為null的程式碼是可以,不過知道有Objects.equals()可以協助(除了equals()外,Objects上還有一些不錯用的方法,請參考API文件),為什麼不拿來用?

import static java.util.Objects.equals;

public class Cat {
    ...
    public boolean equals(Object other) {
        // other參考的就是這個物件,當然是同一物件
        if (this == other) {
            return true;
        }

        /* other參考的物件是不是Cat建構出來的
            例如若是Dog建構出來的當然就不用比了 */
        if (other instanceof Cat) {
            Cat cat = (Cat) other;
            // 定義如果名稱與生日,表示兩個物件實質上相等
            return equals(getName(), cat.getName()) && equals(getBirthday(), cat.getBirthday());
        }

        return false;
    }
}

來看一下Objectsequals()原始碼,比較能安心使用:

    public static boolean equals(Object a, Object b) {
        return (a == b) || (a != null && a.equals(b));
    }


這邊僅示範了equals()實作的基本概念,實際上實作equals()並非這麼簡單,實作equals()時通常也會實作hashCode(),原因會等到學習Collection時再說明,如果現在你就想知道equals()hashCode()實作時要注意的一些事項,可以先參考 物件相等性

2007年研究文獻"Declarative Object Identity Using Relation Types"中指出,在考察大量Java程式碼之後,作者發現幾乎所有equals()方法都實作錯誤。