经典台球
78.8MB · 2026-02-23
使用泛型的主要目的是为了增强编译期强制检查,提前发现错误,防止数据污染。
例:我需要使用ArrayList,用来存储每个人的姓名,在没有泛型的情况下会发生什么
ArrayList names = new ArrayList();
// 当没有泛型的时候,由于底层使用的是Object[],我们可以存入任何类型的数据
names.add("zhang san");
names.add("li si");
names.add(123);
// 由于我们本意是想让这个数组存放人命,但不知道是出于什么情况,错误的向其中添加了Integer,这时编译仍然是没有任何问题的,但当我们使用它时,就会发生错误
String name = (String) names.get(2); // 获取123
// 虽然编译期通过了,但运行到这里时会发生ClassCastException
// 这种在运行时才发现的问题是我们不想看到的
// 所以我们可以使用泛型,他会在编译期帮助我们提早发现错误
List<String> names = new ArrayList<>();
names.add(123); // 这里会报错,无法通过编译
类泛型定义
public class Box<T> { // 这里的T就是我们定义的泛型参数Type Parameter
// 我们可以在类中几乎任意地方使用泛型(静态的成员不可以直接使用类定义泛型)
// 在字段中使用泛型类型
private T value;
// 在构造器中使用泛型参数
public Box(T value) {
this.value = value;
}
// 在方法中使用泛型参数
public void setValue(T value) {
this.value = value;
}
}
方法泛型定义
// 对于方法定义的泛型,必须要在方法返回值之前完成定义,因为方法返回值也有可能使用到此泛型,在使用之前必须完成定义
public <T> T getValue(T v1, T v2) {
return v1 + v2;
}
关于静态成员的泛型问题
/*
java中的静态成员为什么不可以直接使用类定义的泛型
1、生命周期不同,静态成员在类加载时就已经初始化,而类定义泛型参数需要创建对象才能使用
2、类成员在类加载时创建,并且在类中之存在一份,如果允许类成员使用类定义泛型,那么,我们每新创建一个对象,都可能会提供一个新的类型参数Type arguments,这时无法确定类成员到底是使用哪个类型参数
类成员如果想要使用泛型,可以自定义泛型
*/
public class Box {
static <T> T value;
public static <T> T getValue() {
return value;
}
}
Type Parameter 和 Type Argument的区别
本质区别:
类比理解: 就像方法的形参和实参:
特殊形式: Type Argument 可以是具体类、接口,也可以是通配符(?, ? extends T, ? super T)
运行时差异: Type Parameter 会被类型擦除,Type Argument 在编译期用于类型检查
泛型通配符 ? 主要是用于在编译时,我们无法确定这里需要什么类型,来使用代替泛型参数的,主要是通过上界和下界对参数类型进行限制,达到程序正确运行的目的。
public static void print(List<? extends Number> list) {
for (Number n : list)
System.out.print(n + " ");
System.out.println();
}
这里用货车装载货物来举例
情况1、
假设我们在某地订购了一车水果,当货车来送货时,我们需要将水果从车上搬运下来并切检查货物,如果货物不是水果,说明出现了错误
情况2、
假设我们向某地发送一车水果,货主来取货时,我们发现它的车无法装下这些水果,那也是一种错误。
针对情况1和情况2
我们需要保证情况1 从车上搬下来的所有货物都是水果 <? extends Fruit>
保证情况2 这辆车可以装载水果 <? super Fruit>
| 特性 | 上界通配符 (? extends T) | 下界通配符 (? super T) |
|---|---|---|
| 语法 | <? extends Fruit> | <? super Apple> |
| 逻辑含义 | “货物”最高级别不能超过 Fruit | “车厢”最低规格必须能装下 Apple |
| 主要角色 | 生产者 (Producer) | 消费者 (Consumer) |
| 读取 (Get) | 安全:拿出来的全是 T 或其父类 | 不安全:拿出来的全是 Object |
| 写入 (Add) | 禁止:除了 null 什么都不能传 | 安全:可以传 T 及其子类 |
顾名思义,上界就是划定了一个天花板,上面的例子中天花板就是Fruit,编译器知道所有的元素都必须在这个天花板之下,这样可以保证读取出来的每一个数据都至少是一个Fruit,可以保证读取安全。
但是对于写入来说,我们不知道它写入的具体是什么类型,所以除了null以外不能写入任何值
下界就是地板,所有的元素都在地板之上,这样可以保证我们的空间至少是大于等于我们要写入的元素的,可以保证安全写入。
但是对于读取来说,因为每一个类都最终继承自Object,所以读出来会按Object读取,那么我们使用时就可能会需要强制转换,所以读取是不安全的。
Producer Extends Consumer Super
生产者读取使用extends,消费者写入用super
小思考:当我们有一个集合,里面的数据既需要读有需要写时该如何定义泛型?
答案是:需要使用具体类型 T
这里需要补充声明:
通配符可以支持上界和下界,但不支持多上界
具体泛型可以定义多个上界,但不支持下界
<T extend A & B & C>
如果定义的上界中包含类和接口,必须将类定义为第一个上界,(并且只能有一个类,其余只能是接口)
在类型擦除时,会被擦除为第一个上界
<? extends Number> 和 <T extends Number> 有什么区别?
本质区别:
核心差异:
使用场景:
记忆口诀(PECS): Producer Extends, Consumer Super 既读又写用 Type Parameter()
当类型没有边界时 如T 或?,类型会被替换为Object
当类型有上界时,类型会被替换为第一个上界,如 <T extends A & B> => A
当类型有下界时,类型会被替换为Object
桥接方法
public interface BoxInterface<T> {
T getT();
void setT(T t);
}
public class BoxImpl implements BoxInterface<Integer> {
@Override
public Integer getT() {
return 0;
}
@Override
public void setT(Integer integer) {
System.out.println("this is setT" + integer);
}
}
// 当子类实现或继承一个接口或类时,如果父类中有泛型,并且在子类中指定了具体类型,就会出现下面这种情况,这时BoxImpl类的字节码
public class generic.BoxImpl implements generic.BoxInterface<java.lang.Integer> {
public thrid_week.generic.BoxImpl();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public java.lang.Integer getT();
Code:
0: iconst_0
1: invokestatic #7 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
4: areturn
public void setT(java.lang.Integer);
Code:
0: getstatic #13 // Field java/lang/System.out:Ljava/io/PrintStream;
3: aload_1
4: invokedynamic #19, 0 // InvokeDynamic #0:makeConcatWithConstants:(Ljava/lang/Integer;)Ljava/lang/String;
9: invokevirtual #23 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
12: return
public void setT(java.lang.Object);
Code:
0: aload_0
1: aload_1
2: checkcast #8 // class java/lang/Integer
5: invokevirtual #29 // Method setT:(Ljava/lang/Integer;)V
8: return
public java.lang.Object getT();
Code:
0: aload_0
1: invokevirtual #35 // Method getT:()Ljava/lang/Integer;
4: areturn
}
// 我们发现在泛型消除后,父类中的方法泛型应该被擦除并替换为Object,但是子类中实现的是Integer类型的方法,这样就无法构成重写关系,于是编译期帮助我们在子类中生成了Object类型的方法,在此方法中调用我们自己实现的方法,这样就可以在保证类关系的同时可以实现我们自己的需求
由于类型擦除机制,泛型无法使用 instanceof
// 编译错误!无法对 Non-Reifiable 类型使用 instanceof
if (list instanceof List<String>) { // 非法!
// ...
}
无法创建泛型数组
// 编译错误!
T[] genericArray = new T[10]; // 非法!
无法创建泛型示例
public <T> T createInstance() {
// 编译错误!无法实例化类型参数
return new T(); // 非法!
// 编译错误!无法创建泛型数组
T[] array = new T[10]; // 非法!
}
Varargs 警告可能会产生堆污染
public static <T> void addAll(List<T> list, T... elements) {
for (T element : elements) {
list.add(element);
}
}
// 想要保证不产生堆污染需要满足两个条件
// 1、方法内部对 elements 只读不写
// 2、不会发生逃逸,也就是不让elements离开此方法作用域
// 满足这两种条件可以对方法添加@SafeVarargs注解
不能使用泛型作为异常处理
// 1. 不能 catch 泛型异常(因为擦除后可能相同)
try {
// ...
} catch (T e) { // 编译错误!不能catch类型参数
}
// 2. 泛型类不能继承 Throwable
class MyException<T> extends Exception { // 编译错误!
}
// 3. 但可以在 throws 声明中使用类型参数(有限制)
interface Task<T extends Throwable> {
void run() throws T; // 可以
}
数组协变与泛型不变性
// 数组协变
String[] strs = new String[10];
Object[] ojbs = strs;
// 泛型的不变性是指:对于任何两种不同的类型 A 和 B,无论 A 和 B 之间是否存在继承关系,List<A> 与 List<B> 之间都没有任何继承关系。
List<String> lists = new ArrayList<>();
List<Object> listObjs = lists;
// 错误
类型擦除绕过(也并非是真正绕过,只是通过一些办法来记录范型)
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
// 定义一个抽象类来捕获 T
public abstract class TypeReference<T> {
private final Type type;
protected TypeReference() {
// 获取带参数的父类类型:TypeReference<List<String>>
Type superClass = getClass().getGenericSuperclass();
// 提取真正的泛型参数:List<String>
type = ((ParameterizedType) superClass).getActualTypeArguments()[0];
}
public Type getType() { return type; }
}
// 使用方式:创建一个匿名内部类
TypeReference<List<String>> token = new TypeReference<List<String>>() {};
System.out.println(token.getType()); // 输出:java.util.List<java.lang.String>
OpenClaw高级进阶技巧分享!模型精选策略+记忆系统优化经验+深度搜索集成+Gateway崩溃自动修复!Claude Code自动读日志修Bug重启验证
解决 OpenClaw 飞书插件 API 过度调用问题
2026-02-23
2026-02-23