定義與使用泛型
June 21, 2022在〈final/Object/instanceof〉實作過一個 ArrayList
,由於事先不知道被收集物件之形態,因此內部實作時,都是使用 Object
來參考被收集之物件,取回物件時也是以 Object
型態傳回,若你想針對某類別定義的行為操作時,必須告訴編譯器,讓物件重新扮演該型態。例如:
...
static void printUpperCase(ArrayList names) {
for(var i = 0; i < names.size(); i++) {
var name = (String) names.get(i);
System.out.println(name.toUpperCase());
}
}
...
收集物件時,考慮到收集各種物件之需求,因而內部實作採用 Object
參考收集之物件,執行時期被收集的物件會失去形態資訊,也因此取回物件之後,必須自行記得物件的真正型態,並在語法上告訴編譯器讓物件重新扮演為自己的型態。
定義泛型類別
實際上,有收集物件的需求時,多半會收集同一種類型的物件,泛型(Generics)語法讓你在設計 API 時可以指定類別或方法支援泛型,而使用 API 的客戶端在語法上會更為簡潔,並得到編譯時期檢查。
以〈final/Object/instanceof〉實作過的 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
,也就是回歸沒有使用泛型前的做法,例如:
...
var names = new ArrayList();
names.add("Justin");
names.add("Monica");
var name1 = (String) names.get(0);
var name2 = (String) names.get(1);
...
編譯時會出現警告訊息:
Note: Main.java uses unchecked or unsafe operations.
Note: Recompile with -Xlint:unchecked for details.
使用 javac
時再加上 -Xlint:unchecked
就會告訴你詳細原因,主要是編譯器發現這個類別可以使用泛型,貼心地提醒你,是不是要使用,以避免發生非受檢(unckecked)的 ClassCastExcetpion
例外:
Main.java:13: warning: [unchecked] unchecked call to add(E) as a member of
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
時就可以看到原因:
ArrayList.java:23: warning: [unchecked] unchecked cast
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];
}
...
介面與泛型
若介面支援泛型,在實作時也會比較方便,例如想排序陣列的話,可以使用 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
的順序,可以使用 Lambda 語法簡化為以下:
Arrays.sort(words, (s1, s2) -> -s1.compareTo(s2));
這在可讀性上會有很大的助益,有關於 Lambda 會在之後詳述。
再來看一下以下程式片段:
ArrayList<String> words = new ArrayListList<String>();
你會不會覺得有點囉嗦呢?明明宣告 words
已經使用 ArrayList<String>
告訴編譯器,words
參考的物件中,都會是 String
了,為什麼建構 ArrayList
時,還要用 ArrayList<String>
再告知呢?確實,如下撰寫就可以了:
ArrayList<String> words = new ArrayList<>();
如果是 Java 11 以後的版本,可以使用 var
:
var words = new ArrayListList<String>();
靜態方法與泛型
如果現在想寫個 asArrayList
方法,可指定不定長度引數,將之轉換為 ArrayList
,可以如下:
package cc.openhome;
public class Util {
public static <T> ArrayList<T> asList(T... a) {
ArrayList<T> arrLt = new ArrayList<>();
for(T t : a) {
arrLt.add(t);
}
return arrLt;
}
}
想使用這個 asList
方法,完整的泛型宣告語法如下:
ArrayList<String> arrLt = Util.<String>asList("B", "X", "A", "M", "F", "W", "O");
實際上,編譯器可以從 asList
的引數,瞭解到型態參數 T
實際上是 String
型態,因此,可以簡化撰寫為:
ArrayList<String> arrLt = Util.asList("B", "X", "A", "M", "F", "W", "O");
如果使用 var
的話,還可以再簡化:
var arrLt = Util.asList("B", "X", "A", "M", "F", "W", "O");
某些方法宣告下,編譯器無法從引數推斷型態參數的實際型態,那就可能從其他管道來進行推斷。例如,你可能如下定義:
...
public class BeanUtil {
public static <T> T getBean(Map<String, Object> data, String clzName)
throws Exception {
Class clz = Class.forName(clzName);
...略
return (T) bean;
}
}
想使用這個程式片段中的 getBean
方法,完整語法可以如下:
Student student = BeanUtil.<Student>getBean(data, "cc.openhome.Student");
就以上片段來說,其實編譯器可以從 student
的型態推斷,型態參數 T
應該是 Student
,可以簡化撰寫為:
Student student = BeanUtil.getBean(data, "cc.openhome.Student");
編譯器會自動推斷 T
代表的型態,就不用額外指定 <Student>
,完整語法是想在鏈狀操作時使用。例如:
String name = BeanUtil.<Student>getBean(
data, "cc.openhome.Student").getName();
在上例,如果沒有指定 <Student>
,那麼就無法呼叫傳回的 Student
物件 getName
方法,因為編譯器會將 getBean
傳回的物件型態當作 Object
處理,而 Object
並沒有 getName
方法,因而發生錯誤,這跟上面的 Util
的 asList
可以比較一下:
Util.asList("B", "X", "A", "M", "F", "W", "O").get(10).toUpperCase();
這個語法不會發生錯誤,因為編譯器可以從 asList
的引數,瞭解到型態參數 T
實際上是 String
型態,因而傳回型態會是 ArrayList<String>
,而呼叫 get
會傳回 String
,因而最後可以呼叫 toUpperCase
。