在 java.lang.Object
中實作過一個ArrayList
,由於事先不知道被收集物件之形態,因此內部實作時,都是使用Object
來參考被收集之物件,取回物件時也是以Object
型態傳回,若你想針對某類別定義的行為操作時,必須告訴編譯器,讓物件重新扮演該型態。例如:
...
static void printUpperCase(ArrayList names) {
for(int i = 0; i < names.size(); i++) {
String name = (String) names.get(i);
System.out.println(name.toUpperCase());
}
}
...
收集物件時,考慮到收集各種物件之需求,因而內部實作採用Object
參考收集之物件,所以執行時期被收集的物件會失去形態資訊,也因此你取回物件之後,必須自行記得物件的真正型態,並在語法上告訴編譯器讓物件重新扮演為自己的型態。
實際上,有收集物件的需求時,多半會收集同一種類型的物件,例如都是收集字串物件,因此從JDK5之後,新增了泛型(Generics)語法,讓你在設計API時可以指定類別或方法支援泛型,而使用API的客戶端在語法上會更為簡潔,並得到編譯時期檢查。
以在 java.lang.Object
中實作過的ArrayList
為例,,可加入泛型語法:
package cc.openhome;
import java.util.Arrays;
public class ArrayList<E> {
private Object[] list;
private int next;
public ArrayList(int capacity) {
list = new Object[capacity];
}
public ArrayList() {
this(16);
}
public void add(E e) {
if(next == list.length) {
list = Arrays.copyOf(list, list.length * 2);
}
list[next++] = e;
}
public E get(int index) {
return (E) list[index];
}
public int size() {
return next;
}
}
注意到範例中粗體字部份,首先類別名稱旁出現了角括號<E>
,這表示此類別支援泛型,實際加入ArrayList
的物件會客戶端宣告的E
型態,E只是一個型態參數(表示Element),高興的話,你可以用T、K、V等參數名稱。
由於使用<E>
定義型態參數,在需要編譯器檢查型態的地方,都可以使用E
,像是add()
方法必須檢查傳入的物件型態是E
,get()
方法必須轉換為E
型態。
使用泛型語法,會對設計API造成一些語法上的麻煩,但對客戶端會多一些友善。例如:
...
ArrayList<String> names = new ArrayList<String>();
names.add("Justin");
names.add("Monica");
String name1 = names.get(0);
String name2 = names.get(1);
...
宣告與建立物件時,可使用角括號告知編譯器,這個物件收集的都會是String
,而取回之後也會是String
,不用再使用括號轉換型態。如果實際上加入了不是String
的東西會如何呢?
由於你告訴編譯器,這個
ArrayList
會收集的物件都是String
,若你收集非String
的物件,編譯器就會檢查出這個錯誤。使用了宣告泛型的類別而不做型態宣告,型態部份會使用Object
,也就是回歸沒有使用泛型前的做法,例如:...
ArrayList names = new ArrayList();
names.add("Justin");
names.add("Monica");
String name1 = (String) names.get(0);
String name2 = (String) names.get(1);
...
編譯時會出現警告訊息:
Note: Recompile with -Xlint:unchecked for details.
使用
javac
時再加上-Xlint:unchecked
就會告訴你詳細原因,主要是編譯器發現這個類別可以使用泛型,貼心地提醒你,是不是要使用,以避免發生非受檢(unckecked)的ClassCastExcetpion
例外:the raw type ArrayList
names.add("Justin");
^
where E is a type-variable:
E extends Object declared in class ArrayList
Main.java:14: warning: [unchecked] unchecked call to add(E) as a member of
the raw type ArrayList
names.add("Monica");
^
where E is a type-variable:
E extends Object declared in class ArrayList
2 warnings
實際上你在編譯上頭的
ArrayList
時,也會出現警告訊息,因為在使用泛型時,get()
方法傳回物件時還是用了CAST語法,編譯時加上-Xlint:unchecked
時就可以看到原因:return (E) list[index];
^
required: E
found: Object
where E is a type-variable:
E extends Object declared in class ArrayList
1 warning
這個部份的CAST是必要的,如果想避免編譯時看到這個警訊,可以在
get()
上標註@SuppressWarnings("unchecked")
,告訴編譯器忽略這個可能的錯誤:...
@SuppressWarnings("unchecked")
public E get(int index) {
return (E) list[index];
}
...
使用這邊的
ArrayList
如下宣告時:...
ArrayList<String> words = new ArrayList<String>();
words.add("one");
String word = words.get(0);
...
實際上,泛型語法有一部份是編譯器蜜糖(一部份是記錄於位元碼中的資訊),若組譯以上程式片段,可以看到還是展開為JDK1.4之前的寫法:
...
ArrayList arraylist = new ArrayList();
arraylist.add("one");
String s = (String)arraylist.get(0);
...
正因為展開後會有粗體字部份的語法,因而以下會編譯錯誤:
...
ArrayList<String> words = new ArrayList<String>();
words.add("one");
Integer number = words.get(0); // 編譯錯誤
...
編譯器展開程式碼後,實際上會如下:
...
ArrayList words = new ArrayList();
words.add("one");
Integer number = (String) words.get(0); // 編譯錯誤
...
若介面支援泛型,在實作時也會比較方便,例如想排序陣列的話,可以使用
java.util.Arrays
的靜態sort()
方法,若想指定元素順序,可以指定Comparator
實作物件,Comparator
中有關泛型的宣告是這樣的:...
public interface Comparator<T> {
int compare(T o1, T o2);
...
}
這表示實作介面時,可以指定
T
代表的型態,而compare()
就可以直接套用T
型態。例如:package cc.openhome;
import java.util.*;
class ReversedStringOrder implements Comparator<String> {
@Override
public int compare(String s1, String s2) {
return -s1.compareTo(s2);
}
}
public class Main {
public static void main(String[] args) {
String[] words = {"B", "X", "A", "M", "F", "W", "O"};
Arrays.sort(words, new ReversedStringOrder());
for(String word : words) {
System.out.println(word);
}
}
}
Arrays.sort()
該行,如果想使用匿名內部類別來實現,可以如下: Arrays.sort(words, new Comparator<String>() {
@Override
public int compare(String s1, String s2) {
return -s1.compareTo(s2);
}
});
可以看到,可讀性並不好,實際上我們只關心
s1
與s2
的順序,這個寫法在JDK8支援Lambda之後,可以簡化為以下: Arrays.sort(words, (s1, s2) -> -s1.compareTo(s2));
這在可讀性上會有很大的助益,有關於Lambda會在之後詳述。
再來看一下以下程式片段:
ArrayList<String> words = new ArrayListList<String>();
你會不會覺得有點囉嗦呢?明明宣告
words
已經使用ArrayList<String>
告訴編譯器,words
參考的物件中,都會是String
了,為什麼建構ArrayList
時,還要用ArrayList<String>
再告知呢?這個問題從JDK7之後有了點改善,你可以如下撰寫就可以了:ArrayList<String> words = new ArrayList<>();
只要宣告參考時有指定型態,那麼建構物件時就不用再寫型態了,就語法簡潔度上不無小補。
適當地使用泛型語法,語法上可以簡潔一些,編譯器也可以事先作型態檢查,但泛型語法也可以用到很複雜(有人稱之為魔幻,這是個貼切的新形容詞),使用時以不影響可讀性與維護性為主要考量。