➜ Old React website
Chung Cheuk Hang MichaelJava Web Developer
SQL 基礎 - SQL Server低級 task scheduling 錯誤

Java collections 進階 - generic type 同 covariance

Table of contents

1 重溫 Java collections

如果想重溫返關於 Java CollectionListSet subinterfaces 以及 Map,可以睇返呢篇:Java 開發筆記(三) - Array / Collection / Map
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

上面既 ListSetMap 例子裡面,我地都見到佢地有個 diamond operator <>,而裡面有一個 type,我地會叫 <T> 裡面既 type 做 generic type,咁 Clazz<T> 成個夾埋係表達緊 class of type,例如 List<String>List of String,因為已經 substitute 左 StringList<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> numsnums.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}
參考資料:

2.1.2 Declared type information

如果個 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