一、为什么需要使用泛型
使用泛型的主要目的是为了增强编译期强制检查,提前发现错误,防止数据污染。
例:我需要使用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 Parameter(类型形参)是泛型定义时的占位符,如 class Box 中的 T
- Type Argument(类型实参)是泛型使用时的具体类型,如 Box 中的 String
-
类比理解: 就像方法的形参和实参:
- void method(String param) ← param 是形参
- method("actual") ← "actual" 是实参
-
特殊形式: 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 及其子类 |
上界 extends
顾名思义,上界就是划定了一个天花板,上面的例子中天花板就是Fruit,编译器知道所有的元素都必须在这个天花板之下,这样可以保证读取出来的每一个数据都至少是一个Fruit,可以保证读取安全。
但是对于写入来说,我们不知道它写入的具体是什么类型,所以除了null以外不能写入任何值
下界 super
下界就是地板,所有的元素都在地板之上,这样可以保证我们的空间至少是大于等于我们要写入的元素的,可以保证安全写入。
但是对于读取来说,因为每一个类都最终继承自Object,所以读出来会按Object读取,那么我们使用时就可能会需要强制转换,所以读取是不安全的。
PECS定律
Producer Extends Consumer Super
生产者读取使用extends,消费者写入用super
小思考:当我们有一个集合,里面的数据既需要读有需要写时该如何定义泛型?
答案是:需要使用具体类型 T
这里需要补充声明:
通配符可以支持上界和下界,但不支持多上界
具体泛型可以定义多个上界,但不支持下界
<T extend A & B & C>
如果定义的上界中包含类和接口,必须将类定义为第一个上界,(并且只能有一个类,其余只能是接口)
在类型擦除时,会被擦除为第一个上界
<? extends Number> 和 <T extends Number> 有什么区别?
-
本质区别:
- 是通配符上界,用于"使用泛型"时,表示某个未知的 Number 子类
- 是类型参数上界,用于"定义泛型"时,声明 T 必须是 Number 子类
-
核心差异:
- ? 是匿名的,无法在代码中引用;T 是有名的,可以在方法体内使用
- ? 只能用于方法参数或变量类型,不能用于创建实例;T 可以创建 List 实例
- 返回类型上, 可以返回精确的 List,而 ? 只能返回 List<? extends Number>
-
使用场景:
- 只读操作(Producer)用 ? extends T
- 需要返回精确类型、或需要创建泛型实例时,必须用
-
记忆口诀(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>