在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
是一種Object
,Date
是一種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());
}
}
}
一個執行結果如下所示:
訪客名稱:Monica
訪客名稱:Irene
訪客名稱:quit
訪客名單:
JUSTIN
MONICA
IRENE
java.lang.Object
是所有類別的頂層父類別,這代表了Object
上定義的方法,所有物件都繼承下來了,只要不是被定義為final
的方法,都可以重新定義。重新定義toString()
舉例來說,在
protected
成員 的範例中,SwordsMan
等類別曾定義過toString()
方法,其實toString()
是Object
上定義的方法,Object
的toString()
預設定義為: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()
==
,而是透過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
時,編譯器還會來幫點忙,會檢查左運算元型態是否在右運算元型態的繼承架構中(或介面實作架構中,之後會說明介面)。例如:執行時期,並非只有左運算元物件為右運算元類別直接實例化才傳回
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;
}
}
來看一下
Objects
的equals()
原始碼,比較能安心使用: 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()
方法都實作錯誤。