在【Java结构化梳理】泛型-初步了解-上 ,我们认识了泛型、泛型擦除,还留了几个小问题到这篇博客,是泛型擦除带来的影响。这篇文章,我们将解决结合代码解决一下。
一、无法直接 new T() / new T[] - 泛型擦除
java
public class Box<T> {
T value = new T(); // ❌ 编译错误
}
java
public class Box<T> {
T[] arr = new T[10]; // ❌ 编译错误(原因1)
}
1、原因一:泛型擦除
推导:
------>编译时T被擦除为Object
------>运行时JVM 不知道 T 的具体 类型
------>无法调用具体类型的 构造器
------>无法实例化
------>无法直接 new T()/ new T[]
解决方案:传入 Class<T>
java
public class Generic<T> {
private T instance;
public Generic(Class<T> clazz) throws Exception {
this.instance = clazz.getConstructor().newInstance();
}
}
// 使用
Generic<String> g = new Generic<>(String.class);
2、不能new T[]的额外原因:+ 数组协变
推导:
数组协变------>可能传入与实际内存中类型不符的数据------>类型污染。
假设允许new[],实际内存中创建的 Object[]
------>通过数组协变可以赋值给 Object[]引用
------>可以向内存中存入非 T 类型的元素
------>运行时因为T 已被擦除为 Object
------>JVM无法区分出 List<String>和 List<Integer>
------>类型污染,取出时报错 ClassCastException
------>泛型禁止掉了创建泛型数组
------>无法直接new T[]
java
// 假设允许创建泛型数组
List<String>[] stringLists = new ArrayList<String>[1];
// 数组协变,可以赋值给 Object[]
Object[] objects = stringLists;
// 危险!运行时无法区分 List<String> 和 List<Integer>
objects[0] = new ArrayList<Integer>(); // 编译通过,运行时也通过!
// 但编译器以为 stringLists[0] 是 List<String>
String s = stringLists[0].get(0); // ❌ ClassCastException!
解决方案:使用 Object[] + 强制类型转换
java
public class GenericArray<T> {
private Object[] array; // 底层用 Object[]
@SuppressWarnings("unchecked")
public GenericArray(int size) {
array = new Object[size];
}
public void set(int index, T item) {
array[index] = item;
}
@SuppressWarnings("unchecked")
public T get(int index) {
return (T) array[index]; // 取出时强转
}
@SuppressWarnings("unchecked")
public T[] toArray() {
return (T[]) array; // 需要时转换
}
}
或传入 Class<T> 利用反射创建。
二、无法使用 instanceof 判断泛型类型 :泛型擦除
推导:
------>编译时擦除T的类型,List 和 List都是List
------>运行时JVM 不知道泛型参数
------>无法判断泛型类型
------>无法使用 instanceof
解决方案:使用 Class.isInstance()
java
public <T> boolean isInstance(Object obj, Class<T> clazz) {
return clazz.isInstance(obj);
}
// 使用
isInstance("hello", String.class); // true
三&四、通配符
通配符 ? extends T 容器只能读、不能写,元素类型是 T 或 T 的子集;
通配符 ? super T 容器只能写,只能使用 Object 接。元素类型是 T 或 T 的父集。
这两个通配符是为了解决同一类问题:泛型不变性问题,在有限的范围内最大提高泛型的可复用性。
1、不变性vs协变 vs 逆变
前提:String 是 Object 的子类
不变性
如果 A 是 B 的子类,Container<A> 和 Container<B> 之间没有任何继承关系。
java
// 数组:协变 ✅
String[] strArr = new String[10];
Object[] objArr = strArr; // ✅ 合法
// 泛型:不变性 ❌
List<String> strList = new ArrayList<>();
List<Object> objList = strList; // ❌ 编译错误!
可以看出泛型不变性太死板,如果一个计算集合大小的方法,因为泛型的不变性,接 List<String>,List<Object>,List<integer>三种类型的参数要写三遍。
但如果不限制泛型的可变性,就可能导致赋值类型错误的问题,而且这个问题只能在运行时被发现。这是 Java 为了把类型错误从运行时提前到编译期发现的体现。
协变(Covariance)→ 方向相同
String是 Object 的子类,String[] 也是 Object[]的子类
java
String[] arr = new Object[]; ❌ 不行
Object[] arr = new String[]; ✅ 可以
逆变(Contravariance)→ 方向反转
java
父类可以赋值给子类的通配符
List<? super String> list = new ArrayList<Object>();✅
一句话总结:

2、通配符 ? extends T
------>通配符 ? extends T
------>加入"有限协变"灵活赋值
------>引入 ? extend 提供协变
------>集合元素是 T 或 T 的子集
------>只读不写(安全)
------>写入可能有类型错误
? extends 的含义 :"我接收一个 T 或 T 的子类 的列表"
List<Number> ┐
List<Integer> ├── 都可以传给 List<? extends Number>
List<Double> │
List<Long> ┘
java
// 我想写一个方法,打印任何数字列表
// 用通配符实现
void printNumbers(List<? extends Number> list) {
for (Number n : list) { // ✅ 取出来是 Number
System.out.println(n);
}
}
// 调用
printNumbers(new ArrayList<Integer>()); // ✅ Integer 是 Number 子类
printNumbers(new ArrayList<Double>()); // ✅ Double 是 Number 子类
printNumbers(new ArrayList<Long>()); // ✅ Long 是 Number 子类
但只能读不能写:
java
List<? extends Number> list = new ArrayList<Integer>();
list.add(100); // ❌ 不能写!编译器不知道底层具体是什么子类型
Number n = list.get(0); // ✅ 可以读!取出来至少是 Number
为什么不能写?
java
// 假设允许 add
List<? extends Number> list = new ArrayList<Integer>();
list.add(100.5); // 如果允许这行...
// 底层是 ArrayList<Integer>,但你存了 Double → 类型污染!
3、通配符 ? super T
------>通配符 ? super T
------>加入"有限逆变"灵活赋值
------>引入 ? super 提供逆变
------>集合元素是 T 或 T 的父集
------>只写不读或安全读(安全)
? super 的含义 :"我接收一个 T 或 T 的父类 的列表"
List<Object> ┐
List<Number> ├── 都可以传给 List<? super Integer>
List<Integer> ┘
java
// 我想写一个方法,往某个容器里存整数
void fillIntegers(List<? super Integer> list) {
list.add(1); // ✅ 能存!Integer 及其子类都可以
list.add(100);
}
// 调用
fillIntegers(new ArrayList<Integer>()); // ✅ 本身就是 Integer
fillIntegers(new ArrayList<Number>()); // ✅ Number 是 Integer 的父类
fillIntegers(new ArrayList<Object>()); // ✅ Object 也是父类
只能写不能读(只能用 Object 接):
java
List<? super Integer> list = new ArrayList<Number>();
list.add(100); // ✅ 能存!
Object o = list.get(0); // ✅ 只能用 Object 接
Number n = list.get(0); // ❌ 不安全
为什么读出来只能是 Object?
因为底层可能是 ArrayList<Object>,里面可能什么都有。
总结:
遗留的 4 个问题,前两个问题是泛型的类型擦除导致,后两个问题是同一类型问题,是对泛型的不变性扩展导致。
前面我们从根源上解答了这 4 个问题。最后,我们再想一下为什么会出现这 4 个问题呢?------>我想,因为有场景需要:场景需求 → 想用某种写法 → 受到限制 → 理解原因 。
当有场景需要需要创建泛型类型的实例或数组(通用工厂模式等),我们可能想到去 new T()或 new T[];当有个通用方法想根据泛型的类型决策动作时,可能会想到用 instanceof T;当需要方法能接收多种子类型的列表时,需要方法能向多种父类型容器写入数据时,会想到向泛型集合读写数据。泛型的不可变性是为了将类型检查由运行时提前到编译期,但因为限制的太严格,降低了代码的复用性。使用通配符划定了范围,既保证了安全性,又提升了灵活性。这 4 个问题正是我们使用 Java 新功能(泛型)受限时提出的问题。
所有问题的根源都是"需求 vs 安全"的平衡。泛型系统的设计就是在"灵活"和"安全"之间找折中。