Java泛型
泛型主要解决了代码的复用问题。 提高了代码的抽象性。
(1) 为什么会引入泛型
泛型的意义:适用于多种数据类型执行相同的代码(代码复用)
通过一个例子来阐述,先看下下面的代码:
public static int add(byte x, byte y) {
return x + y;
}
public static int add(short x, short y) {
return x + y;
}
public static int add(int x, int y) {
return x + y;
}
如果没有泛型,要实现不同类型的加法,每种类型都需要重载一个add方法。
通过泛型,我们可以复用为一个方法:
private static <T extends Number> int add(T x, T y) {
return x.intValue() + y.intValue();
}
(1.1) 泛型类型约束
Java泛型这个特性是从JDK 1.5才开始加入的,因此为了兼容之前的版本,Java泛型的实现采取了“伪泛型”的策略,即Java在语法上支持泛型,但是在编译阶段会进行所谓的“类型擦除”(Type Erasure),将所有的泛型表示(尖括号中的内容)都替换为具体的类型(其对应的原生态类型),就像完全没有泛型一样。
import java.util.ArrayList;
import java.util.List;
/**
* @author weikeqin
*/
public class JavaGenericsTest1 {
public static void main(String[] args) {
List list = new ArrayList<>();
list.add(1);
list.add(true);
list.add("a");
list.add(new Object());
int[] arr = new int[0];
list.add(arr);
System.out.println(list); // [1, true, a, java.lang.Object@421faab1, [I@2b71fc7e]
}
}
我们在使用上述list中,list中的元素都是Object类型(无法约束其中的类型)
在取出集合元素时需要人为的强制类型转化到具体的目标类型,可能出现java.lang.ClassCastException异常。 像弱类型语言Python PHP等经常出类似的问题,也是这个原因。
// list中只能放String, 不能放其它类型的元素
List<String> list = new ArrayList<String>();
(2) 泛型的基本使用
泛型有三种使用方式,分别为:泛型类、泛型接口、泛型方法。
(2.1) 泛型类
1.简单泛型类
/**
* @author weikeqin
*/
public class DictModel<T> { // 此处类型可以自定义,常用的是 T、K、E,这里用的是T,T是type的简称
private Map<String, T> dict;
public DictModel() {
dict = new HashMap<>();
}
/**
* @param key
* @param value
*/
public void addDict(String key, T value) {
dict.put(key, value);
}
/**
* @param key
* @return
*/
public T getDict(String key) {
return dict.get(key);
}
}
2.多元泛型
/**
* @author weikeqin
*/
public class DictModelV2<K, V> { // 此处指定了两个泛型类型
private Map<K, V> dict;
public DictModelV2() {
dict = new HashMap<>();
}
/**
* @param key
* @param value
*/
public void addDict(K key, V value) {
dict.put(key, value);
}
/**
* @param key
* @return
*/
public V getDict(K key) {
return dict.get(key);
}
}
2.2 泛型接口
/**
* @author weikeqin
*/
public interface DatasourceParser<E> {
/**
* @return
*/
List<E> parserData();
}
/**
* @author weikeqin
*/
public class MysqlDatasourceParser<E> implements DatasourceParser<E>{
@Override
public List<E> parserData() {
return null;
}
}
(2.3) 泛型方法
泛型方法,是在调用方法的时候指明泛型的具体类型。
/**
* @author weikeqin
*/
public class CacheClient {
public <V> void setCache(String key, V v) { // 定义泛型方法时,必须在返回值前边加一个<>,来声明这是一个泛型方法
}
public <V> V getCache(String key) {
V value = null;
// ....
return value;
}
}
泛型方法可以在调用的时候指明类型,更加灵活。
(3) 泛型的上下限
(3.1) 使用泛型遇到的问题cannot be converted to
/**
* @author weikeqin
*/
public class A {
public static void funA(A a) {
// ...
}
public static void funC(List<A> list) {
// ...
}
}
/**
* @author weikeqin
*/
public class B extends A {
public static void funB(B b) {
funA(b);
// ...
}
public static void funD(List<B> list) {
// java: incompatible types: java.util.List<xxx.B> cannot be converted to java.util.List<xxx.A>
funC(list);
}
}
为了解决泛型中隐含的转换问题,Java泛型加入了类型参数的上下边界机制。
<? extends Number>
表示该类型参数可以是Number(上边界)或者Number的子类类型。
代码里修改 List<? extends A> list
后解决
/**
* @author weikeqin
*/
public class A {
public static void funA(A a) {
// ...
}
public static void funC(List<? extends A> list) {
// ...
}
}
/**
* @author weikeqin
*/
public class B extends A {
public static void funB(B b) {
funA(b);
}
public static void funD(List<B> list) {
funC(list);
}
}
(3.2) 泛型上下限的引入
在使用泛型的时候,我们可以为传入的泛型类型实参进行上下边界的限制,
如:类型实参只准传入某种类型的父类或某种类型的子类。
上限
/**
* @author weikeqin
*/
public class DictModel<T extends Number> { // 此处泛型T只能是数字类型
private Map<String, T> dict;
public DictModel() {
dict = new HashMap<>();
}
/**
* @param key
* @param value 类型由T指定
*/
public void addDict(String key, T value) {
dict.put(key, value);
}
/**
* @param key
* @return 返回值的类型由外部决定
*/
public T getDict(String key) {
return dict.get(key);
}
}
下限
// 只能接收Integer或Object类型的泛型,Integer类的父类只有Object类
public static void fun(DictModel<? super Integer> dict) {
System.out.print(dict);
}
(3.3) 小结
<?> 无限制通配符
<? extends E> extends 关键字声明了类型的上界,表示参数化的类型可能是所指定的类型,或者是此类型的子类
<? super E> super 关键字声明了类型的下界,表示参数化的类型可能是指定的类型,或者是此类型的父类
// 使用原则《Effictive Java》
// 为了获得最大限度的灵活性,要在表示 生产者或者消费者 的输入参数上使用通配符,使用的规则就是:生产者有上限、消费者有下限
1. 如果参数化类型表示一个 T 的生产者,使用 < ? extends T>;
2. 如果它表示一个 T 的消费者,就使用 < ? super T>;
3. 如果既是生产又是消费,那使用通配符就没什么意义了,因为你需要的是精确的参数类型。
(3.4) 实际例子
private <E extends Comparable<? super E>> E max(List<? extends E> e1) {
if (e1 == null){
return null;
}
//迭代器返回的元素属于 E 的某个子类型
Iterator<? extends E> iterator = e1.iterator();
E result = iterator.next();
while (iterator.hasNext()){
E next = iterator.next();
if (next.compareTo(result) > 0){
result = next;
}
}
return result;
}
上述代码中的类型参数 E 的范围是
<E extends Comparable<? super E>>
要进行比较,所以 E 需要是可比较的类,因此需要extends Comparable<…>
(注意这里不要和继承的 extends 搞混了,不一样)
Comparable< ? super E> 要对 E 进行比较,即 E 的消费者,所以需要用 super
而参数 List< ? extends E> 表示要操作的数据是 E 的子类的列表,指定上限,这样容器才够大
多个限制
//工资低于2500元的上斑族并且站立的乘客车票打8折
public static <T extends Staff & Passenger> void discount(T t){
if(t.getSalary()<2500 && t.isStanding()){
System.out.println("恭喜你!您的车票打八折!");
}
}
(4) 泛型数组
泛型数组的坑比较多,看看下面几个例子,能否理解。
List[] arr1 = new List[3]; // 编译通过,执行通过
arr1[0] = null;
arr1[1] = Collections.EMPTY_LIST;
arr1[2] = Collections.singletonList("abc");
System.out.println(Arrays.toString(arr1)); // [null, [], [abc]]
List<String>[] lsa = new List[10]; // 编译通过
List<String>[] arr2 = new ArrayList[3]; // 编译通过,执行报错 Exception in thread "main" java.lang.ArrayStoreException: java.util.Collections$EmptyList
arr2[0] = null;
arr2[1] = Collections.EMPTY_LIST;
System.out.println(Arrays.toString(arr2));
List<String>[] arr3 = new ArrayList<String>[3]; // 编译错误 java: generic array creation
List<String>[] arr4 = new ArrayList<?>[3]; // 编译错误 java: incompatible types: java.util.ArrayList<?>[] cannot be converted to java.util.List<java.lang.String>[]
List<String>[] arr5 = new ArrayList<>[10]; // 编译错误 java: cannot create array with '<>'
List<String>[] arr6 = (List<String>[]) new ArrayList<?>[3]; // 编译通过 执行通过
arr6[0] = null;
List<String> list = new ArrayList<>();
list.add("abc");
arr6[1] = list;
System.out.println(Arrays.toString(arr6)); // [null, [abc], null]
List<?>[] arr7 = new ArrayList<String>[3]; // 编译错误 java: generic array creation
List<?>[] arr8 = new ArrayList<?>[3]; // 编译通过
arr8[0] = null;
List<String> list = new ArrayList<>();
list.add("abc");
arr8[1] = list ;
List<Integer> list2 = new ArrayList<>();
list2.add(1);
list2.add(1000);
arr8[2] = list2 ;
System.out.println(Arrays.toString(arr8)); // [null, [abc], [1, 1000]]
List<String>[] arr9 = new ArrayList[3]; // 编译通过,执行通过
arr9[0] = null;
List<String> list = new ArrayList<>();
list.add("abc");
arr9[1] = list ;
System.out.println(Arrays.toString(arr9)); // [null, [abc], null]
JVM在创建int数组时,用的是 newarray 命令,对应的class类型是 int
JVM在创建String数组时,用的是 anewarray 命令,对应的class类型是 class java/lang/String
JVM在创建List数组时用的是 anewarray 命令,对应的class类型是 class java/util/List
合理使用
public ArrayWithTypeToken(Class<T> type, int size) {
array = (T[]) Array.newInstance(type, size);
}
(5) 深入理解泛型
(5.1) 泛型擦除
Java泛型这个特性是从JDK 1.5才开始加入的,因此为了兼容之前的版本,Java泛型的实现采取了“伪泛型”的策略,即Java在语法上支持泛型,但是在编译阶段会进行所谓的“类型擦除”(Type Erasure),将所有的泛型表示(尖括号中的内容)都替换为具体的类型(其对应的原生态类型),就像完全没有泛型一样。
泛型的类型擦除原则:
- 消除类型参数声明,即删除<>及其包围的部分。
- 根据类型参数的上下界推断并替换所有的类型参数为原生态类型:如果类型参数是无限制通配符或没有上下界限定则替换为Object,如果存在上下界限定则根据子类替换原则取类型参数的最左边限定类型(即父类)。
- 为了保证类型安全,必要时插入强制类型转换代码。
- 自动产生“桥接方法”以保证擦除类型后的代码仍然具有泛型的“多态性”。
(5.2) 如何进行擦除
(5.2.1) 擦除类定义中的类型参数
1.无限制类型擦除
当类定义中的类型参数没有任何限制时,在类型擦除中直接被替换为Object,即形如
2.有限类型擦除
当类定义中的类型参数存在限制(上下界)时,在类型擦除中替换为类型参数的上界或者下界,比如形如<T extends Number>
和<? extends Number>
的类型参数被替换为Number,<? super Number>
被替换为Object
(5.2.2) 擦除接口及实现类中的类型参数 桥接方法和泛型的多态
/**
* @author weikeqin
*/
public interface Info<T> {
T info(T var);
}
/**
* @author weikeqin
*/
public class BridgeMethodTest implements Info<Integer> {
@Override
public Integer info(Integer var) {
return null;
}
}
[weikeqin@weikeqin u3 ]$javac Info.java
[weikeqin@weikeqin u3 ]$
[weikeqin@weikeqin u3 ]$javac BridgeMethodTest.java
[weikeqin@weikeqin u3 ]$
[weikeqin@weikeqin u3 ]$javap Info.class
Compiled from "Info.java"
public interface Info<T> {
public abstract T info(T);
}
[weikeqin@weikeqin u3 ]$
[weikeqin@weikeqin u3 ]$javap BridgeMethodTest.class
Compiled from "BridgeMethodTest.java"
public class BridgeMethodTest implements Info<java.lang.Integer> {
public BridgeMethodTest();
public java.lang.Integer info(java.lang.Integer);
public java.lang.Object info(java.lang.Object);
}
[weikeqin@weikeqin u3 ]$
通过javap命令反编译,可以看到接口Info
的方法反编译后其实是抽象方法 public abstract T info(T);
而接口的实现类BridgeMethodTest
原来定义的一个方法 public Integer info(Integer var)
变成了2个方法 public java.lang.Integer info(java.lang.Integer)
public java.lang.Object info(java.lang.Object)
。
Java编译器在BridgeMethodTest中自动增加了两个方法:默认构造方法和参数为Object的info方法,参数为Object的info方法就是“桥接方法”。
(5.2.3) 擦除方法定义中的类型参数
擦除方法定义中的类型参数原则和擦除类定义中的类型参数是一样的
(5.3) 如何证明泛型擦除
import java.util.ArrayList;
import java.util.List;
/**
* @author weikeqin
*/
public class GenericsErasureTest {
public static void main(String[] args) {
List<String> list1 = new ArrayList<String>();
list1.add("abc");
List<Integer> list2 = new ArrayList<Integer>();
list2.add(123);
System.out.println(list1.getClass() == list2.getClass()); // true
}
}
通过这个例子可以看到打印的结果是true
看看JVM底层是怎么做的
[weikeqin@weikeqin u4 ]$
[weikeqin@weikeqin u4 ]$java GenericsErasureTest.java
true
[weikeqin@weikeqin u4 ]$
[weikeqin@weikeqin u4 ]$javap GenericsErasureTest.class
Compiled from "GenericsErasureTest.java"
public class GenericsErasureTest {
public GenericsErasureTest();
public static void main(java.lang.String[]);
}
[weikeqin@weikeqin u4 ]$
[weikeqin@weikeqin u4 ]$javap -v GenericsErasureTest.class
Classfile /Users/weikeqin/WorkSpaces/wkq/java-study/src/main/java/cn/wkq/java/generics/u4/GenericsErasureTest.class
Last modified May 29, 2022; size 750 bytes
SHA-256 checksum 8ba88fabd745b783d104df1e2e7b96bb3d8ef33c3091ac2ff1c88aad5219fba1
Compiled from "GenericsErasureTest.java"
public class GenericsErasureTest
minor version: 0
major version: 61
flags: (0x0021) ACC_PUBLIC, ACC_SUPER
this_class: #38 // GenericsErasureTest
super_class: #2 // java/lang/Object
interfaces: 0, fields: 0, methods: 2, attributes: 1
Constant pool:
#1 = Methodref #2.#3 // java/lang/Object."<init>":()V
#2 = Class #4 // java/lang/Object
#3 = NameAndType #5:#6 // "<init>":()V
#4 = Utf8 java/lang/Object
#5 = Utf8 <init>
#6 = Utf8 ()V
#7 = Class #8 // java/util/ArrayList
#8 = Utf8 java/util/ArrayList
#9 = Methodref #7.#3 // java/util/ArrayList."<init>":()V
#10 = String #11 // abc
#11 = Utf8 abc
#12 = Methodref #7.#13 // java/util/ArrayList.add:(Ljava/lang/Object;)Z
#13 = NameAndType #14:#15 // add:(Ljava/lang/Object;)Z
#14 = Utf8 add
#15 = Utf8 (Ljava/lang/Object;)Z
#16 = Methodref #17.#18 // java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
#17 = Class #19 // java/lang/Integer
#18 = NameAndType #20:#21 // valueOf:(I)Ljava/lang/Integer;
#19 = Utf8 java/lang/Integer
#20 = Utf8 valueOf
#21 = Utf8 (I)Ljava/lang/Integer;
#22 = Fieldref #23.#24 // java/lang/System.out:Ljava/io/PrintStream;
#23 = Class #25 // java/lang/System
#24 = NameAndType #26:#27 // out:Ljava/io/PrintStream;
#25 = Utf8 java/lang/System
#26 = Utf8 out
#27 = Utf8 Ljava/io/PrintStream;
#28 = Methodref #2.#29 // java/lang/Object.getClass:()Ljava/lang/Class;
#29 = NameAndType #30:#31 // getClass:()Ljava/lang/Class;
#30 = Utf8 getClass
#31 = Utf8 ()Ljava/lang/Class;
#32 = Methodref #33.#34 // java/io/PrintStream.println:(Z)V
#33 = Class #35 // java/io/PrintStream
#34 = NameAndType #36:#37 // println:(Z)V
#35 = Utf8 java/io/PrintStream
#36 = Utf8 println
#37 = Utf8 (Z)V
#38 = Class #39 // GenericsErasureTest
#39 = Utf8 GenericsErasureTest
#40 = Utf8 Code
#41 = Utf8 LineNumberTable
#42 = Utf8 main
#43 = Utf8 ([Ljava/lang/String;)V
#44 = Utf8 StackMapTable
#45 = Class #46 // "[Ljava/lang/String;"
#46 = Utf8 [Ljava/lang/String;
#47 = Utf8 SourceFile
#48 = Utf8 GenericsErasureTest.java
{
public GenericsErasureTest();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 8: 0
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=3, args_size=1
0: new #7 // class java/util/ArrayList
3: dup
4: invokespecial #9 // Method java/util/ArrayList."<init>":()V
7: astore_1
8: aload_1
9: ldc #10 // String abc
11: invokevirtual #12 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z
14: pop
15: new #7 // class java/util/ArrayList
18: dup
19: invokespecial #9 // Method java/util/ArrayList."<init>":()V
22: astore_2
23: aload_2
24: bipush 123
26: invokestatic #16 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
29: invokevirtual #12 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z
32: pop
33: getstatic #22 // Field java/lang/System.out:Ljava/io/PrintStream;
36: aload_1
37: invokevirtual #28 // Method java/lang/Object.getClass:()Ljava/lang/Class;
40: aload_2
41: invokevirtual #28 // Method java/lang/Object.getClass:()Ljava/lang/Class;
44: if_acmpne 51
47: iconst_1
48: goto 52
51: iconst_0
52: invokevirtual #32 // Method java/io/PrintStream.println:(Z)V
55: return
LineNumberTable:
line 12: 0
line 13: 8
line 15: 15
line 16: 23
line 18: 33
line 19: 55
StackMapTable: number_of_entries = 2
frame_type = 255 /* full_frame */
offset_delta = 51
locals = [ class "[Ljava/lang/String;", class java/util/ArrayList, class java/util/ArrayList ]
stack = [ class java/io/PrintStream ]
frame_type = 255 /* full_frame */
offset_delta = 0
locals = [ class "[Ljava/lang/String;", class java/util/ArrayList, class java/util/ArrayList ]
stack = [ class java/io/PrintStream, int ]
}
SourceFile: "GenericsErasureTest.java"
[weikeqin@weikeqin u4 ]$
9: ldc #10 // String abc
11: invokevirtual #12 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z
这个是 list1.add(“abc”); 对应的指令
26: invokestatic #16 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
29: invokevirtual #12 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z
这个是 list2.add(123);
对应的指令
可以看到在进行list.add时都是使用 Method java/util/ArrayList.add:(Ljava/lang/Object;)Z
public static void main(String[] args) throws Exception {
ArrayList<Integer> list = new ArrayList<Integer>();
list.add(1); //这样调用 add 方法只能存储整形,因为泛型类型的实例为 Integer
list.getClass().getMethod("add", Object.class).invoke(list, "asd");
for (int i = 0; i < list.size(); i++) {
System.out.println(list.get(i));
}
}
如何理解泛型的编译期检查?
类型变量会在编译的时候擦除掉,那为什么我们往 ArrayList 创建的对象中添加整数会报错呢?不是说泛型变量String会在编译的时候变为Object类型吗?为什么不能存别的类型呢?既然类型擦除了,如何保证我们只能使用泛型变量限定的类型呢?
Java编译器是通过先检查代码中泛型的类型,然后在进行类型擦除,再进行编译。
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<String>();
list.add("123");
list.add(123);//编译错误
}
在上面的程序中,使用add方法添加一个整型,在IDE中,直接会报错,说明这就是在编译之前的检查,因为如果是在编译之后检查,类型擦除后,原始类型为Object,是应该允许任意引用类型添加的。可实际上却不是这样的,这恰恰说明了关于泛型变量的使用,是会在编译之前检查的。
这个类型检查是针对谁的呢
public static void main(String[] args) {
ArrayList<String> list1 = new ArrayList();
list1.add("1"); //编译通过
list1.add(1); //编译错误
String str1 = list1.get(0); //返回类型就是String
ArrayList list2 = new ArrayList<String>();
list2.add("1"); //编译通过
list2.add(1); //编译通过
Object object = list2.get(0); //返回类型就是Object
}
类型检查就是针对引用的,谁是一个引用,用这个引用调用泛型方法,就会对这个引用调用的方法进行类型检测,而无关它真正引用的对象。
new ArrayList<String>().add("11"); //编译通过
new ArrayList<String>().add(22); //编译错误
String str2 = new ArrayList<String>().get(0); //返回类型就是String
如何理解泛型的多态?
类似 (5.2.2) 擦除接口及实现类中的类型参数 桥接方法和泛型的多态
import java.util.HashMap;
import java.util.Map;
/**
* @author weikeqin
*/
public class DictModelV1<T> {
private Map<String, T> dict;
public DictModelV1() {
dict = new HashMap<>();
}
/**
* @param key
* @param value 类型由T指定
*/
public void addDict(String key, T value) {
dict.put(key, value);
}
/**
* @param key
* @return 返回值的类型由外部决定
*/
public T getDict(String key) {
return dict.get(key);
}
}
/**
* @author weikeqin
*/
public class DictModelV6 extends DictModelV1<Integer> {
public DictModelV6() {
super();
}
/**
* @param key
* @param value
*/
@Override
public void addDict(String key, Integer value) {
super.addDict(key, value);
}
/**
* @param key
* @return
*/
@Override
public Integer getDict(String key) {
return super.getDict(key);
}
}
[weikeqin@weikeqin u1 ]$javac DictModelV1.java
[weikeqin@weikeqin u1 ]$
[weikeqin@weikeqin u1 ]$javac DictModelV6.java
[weikeqin@weikeqin u1 ]$
[weikeqin@weikeqin u1 ]$
[weikeqin@weikeqin u1 ]$javap DictModelV1.class
Compiled from "DictModelV1.java"
public class DictModelV1<T> {
public DictModelV1();
public void addDict(java.lang.String, T);
public T getDict(java.lang.String);
}
[weikeqin@weikeqin u1 ]$
[weikeqin@weikeqin u1 ]$javap DictModelV6.class
Compiled from "DictModelV6.java"
public class DictModelV6 extends DictModelV1<java.lang.Integer> {
public DictModelV6();
public void addDict(java.lang.String, java.lang.Integer);
public java.lang.Integer getDict(java.lang.String);
public java.lang.Object getDict(java.lang.String);
public void addDict(java.lang.String, java.lang.Object);
}
[weikeqin@weikeqin u1 ]$
父类反编译 可以看到有2个方法
子类反编译 addDict方法 变成 public void addDict(java.lang.String, java.lang.Integer);
public void addDict(java.lang.String, java.lang.Object);
getDict 方法变成 public java.lang.Integer getDict(java.lang.String);
public java.lang.Object getDict(java.lang.String);
虚拟机并不能将泛型类型变为Integer,只能将类型擦除掉,变为原始类型Object。这样,我们的本意是进行重写,实现多态。可是类型擦除后,只能变为了重载。
这样,类型擦除就和多态有了冲突。
JVM采用了一个特殊的方法,来完成这项功能,那就是桥方法。
如何理解基本类型不能作为泛型类型?
比如,我们没有ArrayList
因为当类型擦除后,ArrayList的原始类型变为Object,ArrayList只能使用包装类,只能引用Integer的值。
另外需要注意,我们能够使用list.add(1)是因为Java基础类型的自动装箱拆箱操作 java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
。
如何理解泛型类型不能实例化
因为在Java编译期没法确定泛型参数化类型,也就找不到对应的类字节码文件,所以自然就不行了。
泛型数组:能不能采用具体的泛型类型进行初始化?
public static void main(String[] args) {
List<String>[] lsa = new List[10]; // Not really allowed.
Object o = lsa;
Object[] oa = (Object[]) o;
List<Integer> li = new ArrayList<Integer>();
li.add(new Integer(3));
oa[1] = li; // Unsound, but passes run time store check
String s = lsa[1].get(0); // Run-time error java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
}
由于 JVM 泛型的擦除机制,所以上面代码可以给 oa[1] 赋值为 ArrayList 也不会出现异常,但是在取出数据的时候却要做一次类型转换,所以就会出现 ClassCastException,如果可以进行泛型数组的声明则上面说的这种情况在编译期不会出现任何警告和错误,只有在运行时才会出错,但是泛型的出现就是为了消灭 ClassCastException,所以如果 Java 支持泛型数组初始化操作就是搬起石头砸自己的脚。
泛型数组:如何正确的初始化泛型数组实例?
尽量别用泛型数组,坑比较多。
如果要用,指明类型,用反射去创建。
public static void main(String[] args) {
Class clz = Integer.class;
Integer[] arr = (Integer[]) Array.newInstance(clz, 100);
}
public class GenericsArrayUseTest {
public static void main(String[] args) {
ArrayWithTypeToken<Integer> arrayToken = new ArrayWithTypeToken<Integer>(Integer.class, 100);
Integer[] array = arrayToken.create();
}
}
public class ArrayWithTypeToken<T> {
private T[] array;
public ArrayWithTypeToken(Class<T> type, int size) {
array = (T[]) Array.newInstance(type, size);
}
public void put(int index, T item) {
array[index] = item;
}
public T get(int index) {
return array[index];
}
public T[] create() {
return array;
}
}
所以使用反射来初始化泛型数组算是优雅实现,因为泛型类型 T在运行时才能被确定下来,我们能创建泛型数组也必然是在 Java 运行时想办法,而运行时能起作用的技术最好的就是反射了。
参考资料
[1] Java 基础 - 泛型机制详解
[2] Java“禁止”泛型数组
[3] generics
[4] generics-extra