Table of contents
1 重溫 Java collections
List
:
List<String> names = new ArrayList<>();
names.add("Michael");
names.add("Peter");
names.add("Peter");
System.out.println(names); // [Michael, Peter, Peter]
解釋:List
支持重複既 elements。
Set
:
Set<String> names = new HashSet<>();
names.add("Michael");
names.add("Peter");
names.add("Peter");
System.out.println(names); // [Michael, Peter]
解釋:Set
裡面既 elements 都係獨一無二,唔會重複。如果 Set
既 generic type 係自定義既 class,咁建議喺自定義既 class 裡面再定義 equals(obj)
同 hashCode()
既邏輯,畀 Set
判決呢個自定義既 class 既唔同既 objects 係咪重複。
Map
:
Map<String, Integer> nameAgeMap = new HashMap<>();
nameAgeMap.put("Alice", 15);
nameAgeMap.put("Bob", 24);
nameAgeMap.put("Alice", 22);
System.out.println(nameAgeMap); // {Bob=24, Alice=22}
解釋:Map
既 keys 都係獨一無二,唔會重複,一般情況都會用 Java 內建既 primitive wrapper classes 或者 String
作為 key 既 generic type,而 value 就更可以用自定義既 class 做 generic type。
2 介紹 Java generic type
上面既 List
、Set
、Map
例子裡面,我地都見到佢地有個 diamond operator <>
,而裡面有一個 type,我地會叫 <T>
裡面既 type 做 generic type,咁 Clazz<T>
成個夾埋係表達緊 class of type,例如 List<String>
解 List
of String
,因為已經 substitute 左 String
落 List<T>
既 T
,所以 List<String>
喺呢個情況就係一個 parameterized type。
Generic type 係 Java 5 加入既功能,compiler 會喺 compile time 既時候幫我 check 啲 code 寫得合唔合理,係 for type safety 既用途。例如,明明我地 declare 既係 List<String> list
,但我地又 call list.add(1)
,咁就好唔合理,而 compiler 就會喺 compile time 出 error,示意我地要睇下有冇寫錯 code。不過呢個功能只會應用喺 compile time,runtime 係發揮唔到任何作用,因為有 type erasure。
因為 Java 既新版都會支持返舊式既 syntax,所以就算唔寫 <Type>
都唔會引致 compilation error。即係咁樣寫都冇問題:
List rawList = new ArrayList();
Set rawSet = new HashSet();
Map rawMap = new HashMap();
呢啲冇畀 generic type 既 objects 既 class 我地會叫做
raw type。
有啲 IDE 例如 Eclipse 默認既設定下會顯示黃線 warning,提示 raw type 係舊式寫法,應該跟足新式寫法,令我地既 code 更 strongly typed。
以下係複雜少少,牽涉 subclass:
List<Number> nums = new ArrayList<>();
nums.add(1); // 1 係 int,會被 autobox 成 Integer object
nums.add(1L); // 1 係 long,會被 autobox 成 Long object
nums.add(1.5F); // 1.5F 係 float,會被 autobox 成 Float object
nums.add(1.5D); // 1.5D 係 double,會被 autobox 成 Double object
解釋:List<Number>
可以 add()
既 argument 可以係任何 extends Number
既 object。
但咁樣寫會有 compilation error:
List<Number> nums = new ArrayList<>();
nums = new ArrayList<Integer>(); // compilation error
nums = new ArrayList<Long>(); // compilation error
nums = new ArrayList<Float>(); // compilation error
nums = new ArrayList<Double>(); // compilation error
解釋:List<Number>
既 variable 只限 assign 返 List<Number>
既 object。
但如果換成咁樣寫,反而就冇 compilation error(Java 8+):
1public static void main(String[] args) throws Exception {
2
3 List<Number> nums = new ArrayList<>();
4
5 // 下面既 createList(T...) 同用 Arrays.asList(T...) 一樣
6 nums = createList(); // List<Number>
7 nums = createList(1, 2); // List<Integer>
8 nums = createList(1L, 2L); // List<Long>
9 nums = createList(1.5F, 2.5F); // List<Float>
10 nums = createList(1.5D, 2.5D); // List<Double>
11}
12
13private static <T> List<T> createList(T... nums) {
14 final List list = Arrays.asList(nums);
15 return list;
16}
解釋:createList(T...)
或者 Arrays.asList(T...)
做緊既野一樣,都係根據傳入既 T...
varargs 既 T
去 infer return type List<T>
裡面既 T
。然後我地將 createList(T...)
既 result assign 落 List<Number>
度。就咁睇好似同上面出 error 既例子一樣,但其實情況有啲唔同,因為 Java 8+ 會睇埋 assignment 既被 assign 果邊(即係左手邊既 nums
)既 type,再去決定右手邊既 type,而呢個情況下句 createList(T...)
expression 就係 poly expression,出現左喺一個 poly context 裡面。類似既做法有 Java 7 既 type inference,例如 List<String> list = new ArrayList<>()
既 <>
唔洗寫 <String>
係因為 compiler 知道呢個係 poly expression,就會睇埋 assignment 既 context,會睇被 assign 果邊去決定 <>
裡面係咩 type。相反,standalone expression 就唔會理個 context。
2.1 Type erasure
其實 Java 既 generic type 只不過係 for compile time 既 type safety 用,而去到 runtime(個 JVM 幫你執行緊你啲 code)既時候,所有 type information 就會冇曬。
呢個亦係點解我地寫 utility methods 既時候,係冇得寫 T.class
或者用 reflection 既方法黎 reference 返一個 parameterized type 既 <T>
裡面既 T
喺 runtime 既 value,而一定要用一個 parameter Class<?> type
既 variable 先可以知道係咩 type。
public static <T> void foo(T obj) {
// 唔知 T 係咩黎,冇得用 T.class 或者 T.newInstance()
}
要改成:
public static <T> void foo(T obj, Class<T> type) {
T newObj = type.newInstance();
System.out.println(type);
}
只要唔係 unbounded wildcard 既 parameterized type,都係 non-reifiable type,type information 都會喺 runtime 度 lost 左。
A reifiable type is a type whose type information is fully available at runtime. This includes primitives, non-generic types, raw types, and invocations of unbound wildcards.
Non-reifiable types are types where information has been removed at compile-time by type erasure — invocations of generic types that are not defined as unbounded wildcards. A non-reifiable type does not have all of its information available at runtime. Examples of non-reifiable types are List<String>
and List<Number>
; the JVM cannot tell the difference between these types at runtime. As shown in Restrictions on Generics, there are certain situations where non-reifiable types cannot be used: in an instanceof
expression, for example, or as an element in an array.
舉個例,即係就算我地咁樣寫,唔單止喺 compile time 冇 error,而到左 runtime 都唔會有任何 exception:
1List nums = new ArrayList<Integer>();
2nums.add(1);
3nums.add("Hi");
4
5System.out.println(nums.get(0)); // 1
6System.out.println(nums.get(1)); // Hi
解釋:
- 上面段 code 冇 compile time exception 好正常,因為
List
係 raw type,而之前已經講過,想要 compiler 幫我地 check type,就要寫埋個 generic type,例如 List<String>
;
- 而點解上面段 code 喺 runtime 執行
add()
唔同 types 既 objects 都冇任何 exception?咁係因為 type erasure 既關係,就算我地 assign 既 object 係 ArrayList<Integer>
,其實呢個 <Integer>
喺 compile 完出黎既 bytecode 裡面係會抹走左,咁 runtime 自然就冇 type information,即係 new ArrayList<Integer>()
到左 runtime 其實同一個冇 generic type 既 new ArrayList()
完全一樣,所以點解話 generics 只會喺 compile time 發揮作用,就係呢個原因。
但如果我地將 List nums
改成 List<Integer> nums
,nums.add("Hi")
就會出 compile time error,咁係因為 compiler 幫我地喺 compile time check 左,List<Integer>
係加唔到 String
object。
2.1.1 Explicit type casting
因為 type erasure 既關係,喺 runtime 既時候如果要用 generic type <T>
黎做 explicit type casting,就唔可以用 (T) obj
既寫法:
1public class Main {
2
3 public static void main(String[] args) {
4 final GenericClass<Integer> list = castToGenericClass("123");
5 System.out.println(list.getData() instanceof Integer); // false
6 System.out.println(list.getData().getClass()); // ClassCastException
7 }
8
9 private static <T> GenericClass<T> castToGenericClass(Object obj) {
10
11 final T cast = (T) obj;
12
13 final GenericClass<T> list = new GenericClass<>();
14 list.setData(cast);
15
16 return list;
17 }
18}
19
20@Data
21class GenericClass<T> {
22 T data;
23}
應該改為傳入 Class<T> clazz
,然後用 clazz.cast(obj)
既方式:
1public class Main {
2
3 public static void main(String[] args) {
4 final GenericClass<String> list = castToGenericClass("123", String.class);
5 System.out.println(list.getData().getClass()); // class java.lang.String
6
7 final GenericClass<Integer> list2 = castToGenericClass(123, Integer.class);
8 System.out.println(list2.getData().getClass()); // class java.lang.Integer
9 }
10
11 private static <T> GenericClass<T> castToGenericClass(Object obj, Class<T> clazz) {
12
13 final T cast = clazz.cast(obj);
14
15 final GenericClass<T> list = new GenericClass<>();
16 list.setData(cast);
17
18 return list;
19 }
20}
21
22@Data
23class GenericClass<T> {
24 T data;
25}
參考資料:
如果個 generic type 係喺 class definition 層面,例如以下既例子,咁係可以 reference 到個 type,但一定要係 type declaration 先可以,如果唔係 type declaration 既情況下 invoke generic type,最後都係會令 type information lost 左。
首先我地寫一個 utility class,喺下面既例子會用到:
1public final class TypeUtils {
2
3 private TypeUtils() {}
4
5 public static List<String> getGenericTypeNames(Class<?> clazz) {
6
7 if (!(clazz.getGenericSuperclass() instanceof ParameterizedType)) {
8 return Collections.emptyList();
9 }
10
11 return Arrays
12 .stream(((ParameterizedType) clazz.getGenericSuperclass()).getActualTypeArguments())
13 .map(Type::getTypeName)
14 .collect(Collectors.toList());
15 }
16
17 public static List<Class<?>> getGenericTypes(Class<?> clazz) {
18 return getGenericTypeNames(clazz).stream()
19 .map(e -> e.replaceAll("<.*?>", ""))
20 .map(e -> {
21 try {
22 return Class.forName(e);
23 } catch (Exception ex) {
24 ex.printStackTrace(); // handle exception
25 return null;
26 }
27 })
28 .filter(Objects::nonNull)
29 .collect(Collectors.toList());
30 }
31}
2.1.2.1 Anonymous class
Anonymous class 係 type declaration,所以可以拎到 parameterized type 裡面既 generic type information 出黎。
1Map raw = new HashMap<Integer, String>() {}; // 注意後面既 {} 令佢成為 anonymous class
2
3List<String> typeNames = TypeUtils.getGenericTypeNames(raw.getClass());
4System.out.println(typeNames);
5// [java.lang.Integer, java.util.List<java.lang.String>]
6
7List<Class<?>> types = TypeUtils.getGenericTypes(raw.getClass());
8System.out.println(types);
9// [class java.lang.Integer, interface java.util.List]
2.1.2.2 Subclass
Subclass 係 type declaration,所以可以拎到 parameterized type 裡面既 generic type information 出黎。但係如果直接用 superclass invoke generic type,咁就拎唔到 generic type information 出黎。
1public class Main {
2
3 public static void main(String[] args) {
4
5 new Sub();
6 // [java.lang.String, java.util.List<java.lang.Integer>]
7 // [class java.lang.String, interface java.util.List]
8
9 new Super<Integer, List<String>>();
10 // [] 拎唔到
11 // [] 拎唔到
12 }
13}
14
15class Super<T1, T2> {
16 public Super() {
17 System.out.println(TypeUtils.getGenericTypeNames(getClass()));
18 System.out.println(TypeUtils.getGenericTypes(getClass()));
19 }
20}
21
22class Sub extends Super<String, List<Integer>> {}
2.2 Unbounded wildcard generic type
我地可以用問號 ?
黎代表 generic type 係 wildcard type:
1List<?> list = new ArrayList<>();
2
3list = Arrays.asList("item");
4// wildcard generic type 既 List variable 可以 assign 任何 generic type 既 List object
5
6list.add(null);
7list.add(new Object()); // compilation error
8// 因為 Java 唔知個 List 係咩 generic type,我地係 add 唔到 element(除左乜野 type 都唔係既 null)
2.3 Upper bounded wildcard generic type
List<? extends Number> nums = new ArrayList<>();
解釋:呢一個 List
既 variable 只能夠接受 list of Number
或者 list of extends Number
既 class。下面既都可以:
List<? extends Number> nums;
nums = new ArrayList<Number>();
nums = new ArrayList<Integer>();
nums = new ArrayList<Double>();
需要注意既係,因為唔知個 List
係幾 specific 既 generic type,List<? extends Number>
可以 assign List<Number>
、List<Integer>
,甚至 list of Integer
既 subclass(自定義或者 3rd party library 裡面提供既)既 object,而如果 assign 既係 List<Integer>
既 object,理論上係唔應該畀 add()
一個 Integer
以外既 element,所以 compiler 穩陣起見,就喺 compile time 唔畀我地 add()
任何 element(除左乜野 type 都唔係既 null
):
List<? extends Number> nums = new ArrayList<>();
nums.add(null);
nums.add(1); // compilation error
2.4 Lower bounded wildcard generic type
List<? super Number> nums = new ArrayList<>();
解釋:呢一個 List
既 variable 只能夠接受 list of Number
或者 list of Number
既 superclass。下面既都可以:
List<? super Number> nums;
nums = new ArrayList<Number>();
nums = new ArrayList<Object>();
需要注意既係,lower bounded wildcard 同 upper bounded wildcard 唔同,因為知道個 List
唔會比 List<Number>
更 specific,可以 assign List<Number>
或者 List<Object>
既 object 都得,如果 assign 既係 List<Number>
既 object,係可以 add()
任何 Number
或者 extends Number
既 object,而如果 assign 既係 List<Object>
既 object 既話更加係 add()
乜野 object 都可以,所以 lower bounded wildcard 係畀我地 add()
element,但 List<? super Number>
只限 add()
到 Number
或者 extends Number
既 object,以及乜野 type 都唔係既 null
。
咁樣寫係冇問題:
List<? super Number> nums = new ArrayList<>();
nums.add(null);
nums.add(new Number() { /* add unimplemented methods */ }); // anonymous class
nums.add(1);
nums.add(1.5D);
3 Covariance 問題
因為 generic type 可以有繼承,即係我地可以寫 class Sub extends Super
,咁即係話我地可以有 List<Super>
同 List<Sub>
,咁到底兩者既 variables 同 objects 係咪互相相容(in terms of polymorphism)?呢個就係 covariance 問題。
註:除左 extends
,covariance 既規則對 implements
(interface)都一樣適用。
4 Covariance 規則
到底 Clazz<T>
可以 assign 返乜野 type 既 object?
假設我地有以下既自定義 classes:
1class Wrapper<T> {
2 public void foo(T obj) {}
3}
4class SubWrapper<T> extends Wrapper<T> {}
5
6class Super {}
7class Sub extends Super {}
如果係特定 generic type,例如 Wrapper<Super>
,特定左 generic type 係 Super
,咁只能 assign 同樣特定 generic type 既 Wrapper
或者 Wrapper
既 subclass 既 object:
1Wrapper<Super> wrapper;
2wrapper = new Wrapper<Super>();
3wrapper = new SubWrapper<Super>();
4
5wrapper.foo(null);
6wrapper.foo(new Super());
7wrapper.foo(new Sub());
8
9// 唔可以用特定 generic type 既 subclass 做個 generic type
10wrapper = new Wrapper<Sub>(); // compilation error
11wrapper = new SubWrapper<Sub>(); // compilation error
如果係 upper bounded wildcard,例如 Wrapper<? extends Super>
,咁可以 assign 相同既 generic type 或者佢既 subclass 既 Wrapper
或者 Wrapper
既 subclass 既 object:
1Wrapper<? extends Super> wrapper;
2wrapper = new Wrapper<Super>();
3wrapper = new SubWrapper<Super>();
4wrapper = new Wrapper<Sub>();
5wrapper = new SubWrapper<Sub>();
6
7wrapper.foo(null);
8
9// 唔可以 call 任何 parameter 有 T 既 method
10wrapper.foo(new Super()); // compilation error
11wrapper.foo(new Sub()); // compilation error
如果係 lower bounded wildcard,例如 Wrapper<? super Super>
,咁可以 assign 相同既 generic type 或者佢既 superclass 既 Wrapper
或者 Wrapper
既 subclass 既 object:
1Wrapper<? super Super> wrapper;
2wrapper = new Wrapper<Super>();
3wrapper = new SubWrapper<Super>();
4wrapper = new Wrapper<Object>();
5wrapper = new SubWrapper<Object>();
6
7wrapper.foo(null);
8wrapper.foo(new Super());
9wrapper.foo(new Sub());
如果係 wildcard,例如 Wrapper<?>
,咁可以 assign 任何 generic type 既 Wrapper
或者 Wrapper
既 subclass 既 object:
1Wrapper<?> wrapper;
2wrapper = new Wrapper<Super>();
3wrapper = new SubWrapper<Super>();
4wrapper = new Wrapper<Sub>();
5wrapper = new SubWrapper<Sub>();
6wrapper = new Wrapper<Object>();
7wrapper = new SubWrapper<Object>();
8
9wrapper.foo(null);
10
11// 唔可以 call 任何 parameter 有 T 既 method
12wrapper.foo(new Super()); // compilation error
13wrapper.foo(new Sub()); // compilation error
14wrapper.foo(new Object()); // compilation error