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

相关推荐
武子康几秒前
Java-02 深入浅出MyBatis 3 快速入门:环境配置、项目创建与 CRUD 操作
java·后端
Don.TIk4 分钟前
ChapterOne-搭建项目骨架
java·spring·spring cloud·mybatis
Don.TIk5 分钟前
ChaperTwo-整合 SaToken 实现 JWT 登录功能
java·开发语言
qq_2518364575 分钟前
基于java Web汽车销售管理系统设计与实现
java·前端·汽车
南极企鹅9 分钟前
事务&@Transactional注解
java·数据库·spring·oracle·mybatis
yaoxin52112321 分钟前
406. Java 文件操作基础 - 字符与二进制流
java·开发语言·python
江屿风27 分钟前
C++OJ题经验总结(竞赛)1
开发语言·c++·笔记·算法
happymaker062629 分钟前
SpringBoot学习日记——DAY02(SpringBoot整合Swagger3)
java·spring boot·学习
阿坤带你走近大数据40 分钟前
Java中的JVM、类加载记住、多线程、性能优化的概念
java·jvm·性能优化
鱼鳞_1 小时前
苍穹外卖-Day04
java