【Java结构化梳理】泛型-初步了解-中

【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 逆变

前提:StringObject 的子类

不变性

如果 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 安全"的平衡。泛型系统的设计就是在"灵活"和"安全"之间找折中。

相关推荐
CQU_JIAKE1 小时前
[q]4.25
java·开发语言·前端
涵涵(互关)1 小时前
语法大全-only-writer
开发语言·前端·vue.js·typescript
YaBingSec1 小时前
玄机网络安全靶场:GeoServer XXE 任意文件读取(CVE-2025-58360)
java·运维·网络·安全·web安全·tomcat·ssh
shehuiyuelaiyuehao1 小时前
算法12,滑动窗口,将x减到0的最小操作数
java·数据结构·算法
lulu12165440781 小时前
国内怎么用GPT5.5?基于weelinking零门槛合规接入GPT5.5全系列生产级能力
java·人工智能·python·gpt·ai编程
skywalk81631 小时前
lisp to 块编程 完全的中文编程思路:无空格编程
开发语言·lisp
liulian09161 小时前
【Flutter for OpenHarmony第三方库】Flutter for OpenHarmony 离线模式实现:让你的应用无网也能萌萌哒~
开发语言·flutter·华为·php·学习方法·harmonyos
南宫萧幕1 小时前
基于 DQN 与 Python-Simulink 联合仿真的 HEV 能量管理策略实战
开发语言·python·matlab·汽车·控制