Java泛型

Java泛型

作者:没有四次元口袋的蓝胖

日期:2026-06-15

标签:Java, 泛型, 通配符, 类型擦除

一、为什么需要泛型

没有泛型的时代,集合存什么都是 Object,取出来要强制转换------转错了运行时才报错。

java 复制代码
// 没有泛型(Java 5 之前)
List list = new ArrayList();
list.add("hello");
list.add(123);       // 编译不报错!什么都能存
String s = (String) list.get(1);  // 运行时 ClassCastException!

// 有泛型
List<String> list = new ArrayList<>();
list.add("hello");
list.add(123);       // ❌ 编译就报错!类型安全
String s = list.get(0);  // 不用强转

泛型的两大作用:

  1. 编译期类型检查------存错类型编译就报错,不用等到运行时
  2. 消除强制转换------取值不用手动转型,代码更干净

二、集合中的泛型

2.1 基本用法

java 复制代码
// List
List<String> names = new ArrayList<>();  // 菱形推断,右边不用写类型
names.add("张三");
String name = names.get(0);  // 不用转型

// Map
Map<String, Integer> scores = new HashMap<>();
scores.put("张三", 90);
int score = scores.get("张三");  // 自动拆箱

// Set
Set<Integer> ids = new HashSet<>();
ids.add(1);

2.2 泛型只能用引用类型

java 复制代码
List<int> list = new ArrayList<>();    // ❌ 编译错误!
List<Integer> list = new ArrayList<>(); // ✅ 用包装类

// 自动装箱拆箱
list.add(1);        // 自动装箱:int → Integer
int val = list.get(0);  // 自动拆箱:Integer → int

2.3 泛型集合的遍历

java 复制代码
List<String> list = new ArrayList<>(Arrays.asList("A", "B", "C"));

// 增强for(类型安全)
for (String s : list) {
    System.out.println(s.toUpperCase());  // 直接调String方法,不用转型
}

// Iterator
Iterator<String> it = list.iterator();
while (it.hasNext()) {
    String s = it.next();  // 返回String,不是Object
}

// Stream
list.stream().map(String::toUpperCase).forEach(System.out::println);

2.4 集合泛型的坑

坑:泛型不支持继承

java 复制代码
// Object 是 String 的父类,但 List<Object> 不是 List<String> 的父类
List<String> strList = new ArrayList<>();
List<Object> objList = strList;  // ❌ 编译错误!

// 原因:如果允许,就能往 objList 里放任意 Object,strList 就不安全了
objList.add(123);  // 如果上面不报错,这里就能通过
String s = strList.get(0);  // ClassCastException!

正确做法:用通配符(后面细讲)

java 复制代码
List<?> list = strList;  // ✅ 通配符可以

三、方法中的泛型

3.1 定义泛型方法

在方法的返回值前用 <T> 声明类型参数,这个 T 就可以在方法的参数、返回值、方法体中使用。

java 复制代码
// 泛型方法:交换数组中两个元素
public <T> void swap(T[] arr, int i, int j) {
    T temp = arr[i];
    arr[i] = arr[j];
    arr[j] = temp;
}

// 调用时自动推断类型
String[] names = {"A", "B", "C"};
swap(names, 0, 2);  // T 被推断为 String

Integer[] nums = {1, 2, 3};
swap(nums, 0, 2);   // T 被推断为 Integer

3.2 多个类型参数

java 复制代码
// 两个类型参数
public <K, V> void printEntry(K key, V value) {
    System.out.println(key + "=" + value);
}

printEntry("name", "张三");      // K=String, V=String
printEntry("age", 20);           // K=String, V=Integer
printEntry(1, "第一条记录");      // K=Integer, V=String

3.3 泛型方法与可变参数

java 复制代码
// 接收多个同类型参数
@SafeVarargs  // 抑制堆污染警告
public final <T> List<T> toList(T... items) {
    List<T> list = new ArrayList<>();
    for (T item : items) {
        list.add(item);
    }
    return list;
}

List<String> names = toList("张三", "李四", "王五");
List<Integer> nums = toList(1, 2, 3);

3.4 泛型方法的类型推断

java 复制代码
// 大多数情况编译器能自动推断
List<String> list = toList("A", "B");  // T=String,自动推断

// 推断不出来时可以显式指定
Collections.<String>emptyList();  // 显式指定T=String

3.5 静态方法也可以是泛型方法

java 复制代码
public class Utils {
    // ✅ 静态泛型方法------自己声明 <T>
    public static <T> T getFirst(List<T> list) {
        return list.isEmpty() ? null : list.get(0);
    }
}

注意: 静态方法不能使用类的泛型参数,只能用自己声明的。因为类的泛型参数是在创建对象时确定的,而静态方法属于类不属于对象。

java 复制代码
public class Box<T> {
    private T value;
    
    // ❌ 编译错误!静态方法不能用类的T
    public static T getValue() { ... }
    
    // ✅ 自己声明泛型参数
    public static <E> E process(E item) { ... }
}

四、类中的泛型

4.1 定义泛型类

在类名后用 <T> 声明类型参数,整个类中都可以使用这个 T。

java 复制代码
public class Box<T> {
    private T value;
    
    public Box(T value) {
        this.value = value;
    }
    
    public T getValue() {
        return value;
    }
    
    public void setValue(T value) {
        this.value = value;
    }
}

// 使用时指定具体类型
Box<String> strBox = new Box<>("hello");
String s = strBox.getValue();  // 不用转型

Box<Integer> intBox = new Box<>(123);
int n = intBox.getValue();     // 不用转型

4.2 多个类型参数

java 复制代码
public class Pair<K, V> {
    private K key;
    private V value;
    
    public Pair(K key, V value) {
        this.key = key;
        this.value = value;
    }
    
    public K getKey() { return key; }
    public V getValue() { return value; }
}

Pair<String, Integer> entry = new Pair<>("张三", 90);
String name = entry.getKey();    // String
Integer score = entry.getValue(); // Integer

4.3 泛型类的实例化

java 复制代码
// Java 7+ 菱形推断:右边不用写类型
Box<String> box = new Box<>("hello");   // ✅
Box<String> box = new Box<String>("hello");  // 也可以,但多余

// 不指定类型参数 → 原始类型(Raw Type),T 全部变成 Object
Box box = new Box("hello");  // ⚠️ 警告,不推荐
Object obj = box.getValue(); // 返回 Object

4.4 泛型类的继承

java 复制代码
// 子类保持泛型
public class SpecialBox<T> extends Box<T> {
    public SpecialBox(T value) {
        super(value);
    }
}

// 子类指定具体类型
public class StringBox extends Box<String> {
    public StringBox(String value) {
        super(value);
    }
}

// 使用
SpecialBox<Integer> sBox = new SpecialBox<>(42);
StringBox strBox = new StringBox("hello");

五、接口中的泛型

5.1 定义泛型接口

java 复制代码
public interface Repository<T> {
    void save(T entity);
    T findById(int id);
    List<T> findAll();
    void delete(T entity);
}

5.2 实现方式一:指定具体类型

实现接口时把泛型确定下来,实现类不再是泛型类。

java 复制代码
public class UserRepository implements Repository<User> {
    @Override
    public void save(User entity) { ... }
    
    @Override
    public User findById(int id) { ... }
    
    @Override
    public List<User> findAll() { ... }
    
    @Override
    public void delete(User entity) { ... }
}

特点: 方法参数和返回值都是确定的 User 类型,不需要再写泛型。

5.3 实现方式二:保持泛型

实现时不指定类型,让子类或使用时再确定。

java 复制代码
public class BaseRepository<T> implements Repository<T> {
    private List<T> data = new ArrayList<>();
    
    @Override
    public void save(T entity) {
        data.add(entity);
    }
    
    @Override
    public T findById(int id) {
        return data.get(id);
    }
    
    @Override
    public List<T> findAll() {
        return data;
    }
    
    @Override
    public void delete(T entity) {
        data.remove(entity);
    }
}

// 使用时确定类型
Repository<User> userRepo = new BaseRepository<>();
Repository<Product> productRepo = new BaseRepository<>();

特点: 实现类也是泛型类,灵活性更高,可以复用代码。

5.4 两种实现方式对比

对比 指定具体类型 保持泛型
实现类 普通类 泛型类
灵活性 低(只能用一种类型) 高(可以指定任意类型)
代码量 每种类型写一个实现 写一个通用实现
适用 类型固定不变 通用框架/工具

六、泛型的向上转型与向下转型

6.1 向上转型(子类 → 父类方向)

普通类的向上转型没问题:

java 复制代码
Object obj = "hello";    // String → Object ✅
Number num = 123;         // Integer → Number ✅

但泛型集合的向上转型不行!

java 复制代码
List<String> strList = new ArrayList<>();
List<Object> objList = strList;  // ❌ 编译错误!

// 为什么?如果允许:
objList.add(123);                // 往"Object列表"里放Integer
String s = strList.get(0);      // 从"String列表"里取出了123 → 炸了

核心规则:List<String> 不是 List<Object> 的子类型。 泛型不支持协变。

6.2 用通配符实现"向上转型"

java 复制代码
// ? extends T:T 或 T 的子类
List<String> strList = new ArrayList<>();
List<? extends Object> list = strList;  // ✅ 可以!

// 但只能读,不能写!
Object obj = list.get(0);  // ✅ 可以读(一定是Object的子类)
list.add("hello");         // ❌ 不能写(编译器不知道具体是哪个子类)

为什么不能写? 因为 ? extends Object 可能是 String、Integer、任何类型------编译器无法保证你 add 的类型是对的,所以干脆禁止。

6.3 向下转型(父类 → 子类方向)

普通向下转型要强制转换,可能 ClassCastException:

java 复制代码
Object obj = "hello";
String s = (String) obj;    // ✅ 运行时obj确实是String

Object obj2 = 123;
String s2 = (String) obj2;  // ❌ ClassCastException!

泛型的向下转型更危险------因为类型擦除:

java 复制代码
// 泛型集合的运行时类型都是 Raw Type
List<String> strList = new ArrayList<>();
List<Integer> intList = new ArrayList<>();

// 运行时两者的 .getClass() 一样!都是 ArrayList
System.out.println(strList.getClass() == intList.getClass());  // true!

// 所以这种"转型"编译器直接拦住
List<Object> objList = (List<Object>) strList;  // ❌ 编译错误

6.4 用通配符实现"向下转型"

java 复制代码
// ? super T:T 或 T 的父类
List<Object> objList = new ArrayList<>();
List<? super String> list = objList;  // ✅

// 可以写(因为String一定是 ? super String 的子类,add String 安全)
list.add("hello");  // ✅

// 但只能读成 Object(不知道具体是哪个父类)
Object obj = list.get(0);  // ✅ 只能当 Object
String s = (String) list.get(0);  // ⚠️ 需要强转,不安全

6.5 转型规则速查

场景 能否转型 方式
List<String>List<Object> 泛型不支持协变
List<String>List<?> 无界通配符
List<String>List<? extends Object> 上界通配符(只读)
List<Object>List<? super String> 下界通配符(只写)
List<Object>List<String> 不安全

七、通配符

7.1 三种通配符

通配符 含义 能读吗 能写吗 场景
<?> 任意类型 只能读成 Object ❌ 不能写 未知类型
<? extends T> T 或 T 的子类 读成 T ❌ 不能写 从集合取数据(生产者)
<? super T> T 或 T 的父类 只能读成 Object ✅ 写 T 往集合放数据(消费者)

7.2 无界通配符 <?>

java 复制代码
// 可以接受任意类型的集合
public void printList(List<?> list) {
    for (Object item : list) {  // 只能当 Object 读
        System.out.println(item);
    }
    // list.add("hello");  // ❌ 不能写(除了null)
}

printList(new ArrayList<String>());
printList(new ArrayList<Integer>());

<?> vs 原始类型(Raw Type):

java 复制代码
// Raw Type:没有类型检查,不安全
List rawList = new ArrayList();
rawList.add("hello");
rawList.add(123);        // 不报错

// <?>:有类型安全限制
List<?> wildcardList = new ArrayList<String>();
wildcardList.add("hello");  // ❌ 编译错误!不能写

结论: <?> 比 Raw Type 更安全------它明确表示"我不知道具体类型,所以我不让你写"。

7.3 上界通配符 <? extends T>

记忆:extends = 生产者 = 只读

java 复制代码
// 读取集合中最大的元素
public double sum(List<? extends Number> list) {
    double total = 0;
    for (Number n : list) {  // ✅ 可以读成 Number
        total += n.doubleValue();
    }
    // list.add(123);  // ❌ 不能写
    return total;
}

// 可以传 Number 的任意子类
sum(new ArrayList<Integer>());   // ✅
sum(new ArrayList<Double>());    // ✅
sum(new ArrayList<Number>());    // ✅
sum(new ArrayList<String>());    // ❌ String 不是 Number 子类

为什么不能写? 因为编译器只知道"这是 Number 的某个子类的列表",不知道具体是 Integer 还是 Double。如果你 add(123),而实际是 ArrayList,类型就不匹配了。

7.4 下界通配符 <? super T>

记忆:super = 消费者 = 只写

java 复制代码
// 往集合中添加元素
public void addNumbers(List<? super Integer> list) {
    list.add(1);     // ✅ 可以写 Integer
    list.add(2);     // ✅
    list.add(3);     // ✅
    // Integer i = list.get(0);  // ❌ 不能读成 Integer,只能读成 Object
}

// 可以传 Integer 的父类集合
addNumbers(new ArrayList<Integer>());   // ✅
addNumbers(new ArrayList<Number>());    // ✅
addNumbers(new ArrayList<Object>());    // ✅
addNumbers(new ArrayList<Double>());    // ❌ Double 不是 Integer 的父类

为什么能写 Integer? 因为不管实际是 List<Integer>List<Number> 还是 List<Object>,Integer 都是它们允许的元素类型,add 一定是安全的。

7.5 PECS 原则

Producer Extends, Consumer Super------Java 泛型通配符的黄金法则:

  • **从集合读取数据(生产者)**→ 用 <? extends T>
  • **往集合写入数据(消费者)**→ 用 <? super T>
  • 既要读又要写 → 不用通配符,直接用 <T>
java 复制代码
// 经典例子:Collections.copy
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
    for (int i = 0; i < src.size(); i++) {
        dest.set(i, src.get(i));  // src 是生产者(读),dest 是消费者(写)
    }
}

// 可以把 List<Integer> 拷贝到 List<Number>
List<Integer> src = Arrays.asList(1, 2, 3);
List<Number> dest = new ArrayList<>(Arrays.asList(0, 0, 0));
Collections.copy(dest, src);  // ✅

7.6 通配符对比总结

复制代码
                 读能力        写能力
<?>             Object        ❌
<? extends T>   T             ❌       → 生产者(只取不存)
<? super T>     Object        T        → 消费者(只存不取)
<T>             T             T        → 既能读又能写

八、泛型的擦除问题

8.1 什么是类型擦除

Java 泛型是编译期特性,运行时泛型信息被擦除。

编译后 <String><Integer> 全部变成原始类型(Raw Type),T 变成 Object(或有界类型)。

java 复制代码
// 源码
List<String> strList = new ArrayList<>();
List<Integer> intList = new ArrayList<>();

// 编译后(字节码层面)
List strList = new ArrayList();
List intList = new ArrayList();
// 两者完全一样!泛型信息没了

验证:

java 复制代码
List<String> strList = new ArrayList<>();
List<Integer> intList = new ArrayList<>();

System.out.println(strList.getClass() == intList.getClass());  // true!
// 运行时根本分不清 List<String> 和 List<Integer>

8.2 擦除规则

泛型声明 擦除后
<T> Object
<T extends Number> Number(擦除为上界)
List<String> List
Map<String, Integer> Map
java 复制代码
// 源码
public class Box<T extends Number> {
    private T value;
    public T getValue() { return value; }
    public void setValue(T value) { this.value = value; }
}

// 擦除后(等价于)
public class Box {
    private Number value;
    public Number getValue() { return value; }
    public void setValue(Number value) { this.value = value; }
}

8.3 擦除带来的问题

问题1:不能创建泛型数组

java 复制代码
T[] arr = new T[10];              // ❌ 编译错误
List<String>[] arr = new List<String>[10];  // ❌ 编译错误

// 原因:擦除后变成 Object[],类型不安全
Object[] arr = new String[10];
arr[0] = 123;  // 运行时 ArrayStoreException

// 解决:用 Object[] + 强转,或用 List<List<String>>
T[] arr = (T[]) new Object[10];    // ⚠️ 可以但有Unchecked警告
List<List<String>> list = new ArrayList<>();  // ✅ 推荐

问题2:不能实例化类型参数

java 复制代码
public class Box<T> {
    // new T()              // ❌ 编译错误
    // new T[]              // ❌ 编译错误
    // T.class              // ❌ 编译错误
    // instanceof T         // ❌ 编译错误
}

问题3:不能用于基本类型

java 复制代码
List<int> list = new ArrayList<>();     // ❌
List<Integer> list = new ArrayList<>(); // ✅ 用包装类

// 原因:擦除后变成 Object,而基本类型不是 Object 的子类

问题4:运行时无法获取泛型信息

java 复制代码
List<String> list = new ArrayList<>();
// list instanceof List<String>   // ❌ 编译错误
// list instanceof List<Integer>  // ❌ 编译错误
list instanceof List               // ✅ 只能检查原始类型

8.4 桥方法

编译器为了保持多态,会自动生成桥方法。

java 复制代码
// 源码
public class StringBox extends Box<String> {
    @Override
    public void setValue(String value) { ... }
}

// 擦除后 Box.setValue 变成 setValue(Object)
// StringBox.setValue(String) 无法覆盖 setValue(Object)
// 编译器自动生成桥方法:
public void setValue(Object value) {
    setValue((String) value);  // 桥方法,调用真正的 setValue(String)
}

8.5 为什么 Java 选择类型擦除

核心原因:向后兼容。 Java 5 引入泛型时,要保证老代码(没有泛型的代码)和新代码(有泛型的代码)能互相调用。擦除让泛型代码编译后和老代码的字节码格式一致。

代价: 运行时丢失泛型信息,导致上面一堆限制。

对比其他语言:

  • C# 的泛型是具化泛型(reified generics),运行时保留类型信息
  • Java 的泛型是擦除泛型(erased generics),运行时类型信息丢失

8.6 如何在运行时获取泛型信息

虽然擦除了,但有些场景还是能拿到泛型信息的------通过反射的 ParameterizedType

java 复制代码
// 继承时指定泛型类型,信息保留在父类的 class 文件中
abstract class TypeRef<T> {}

TypeRef<List<String>> ref = new TypeRef<>() {};  // 匿名子类
Type type = ref.getClass().getGenericSuperclass();  // ParameterizedType
// 可以拿到 List<String> 这个完整类型信息

// 实际应用:Gson / Jackson 反序列化
Type type = new TypeToken<List<String>>() {}.getType();
List<String> list = gson.fromJson(json, type);

原理: 匿名子类在继承时把泛型类型写进了 class 文件的签名信息(Signature 属性),虽然运行时擦除了,但反射可以读到签名。


九、面试高频题

Q1:什么是泛型擦除?有什么影响?

Java 泛型只在编译期存在,编译后泛型信息被擦除为 Object 或上界类型。影响:不能 new T()、不能 new T\[\]、不能 instanceof T、基本类型不能做泛型参数、运行时无法获取泛型类型信息。

Q2:List<String>List<Object> 是什么关系?

没有任何继承关系。List<String> 不是 List<Object> 的子类型。如果允许,就能往 List 里放 Object,破坏类型安全。需要"向上转型"时用 List<?>List<? extends Object>

Q3:<? extends T><? super T> 的区别?

extends 是上界通配符,只能读不能写(生产者);super 是下界通配符,只能写不能读成具体类型(消费者)。PECS 原则:频繁读取用 extends,频繁写入用 super。

Q4:为什么不能创建泛型数组?

因为擦除后泛型数组会变成 Object\[\],无法保证类型安全。如果允许 new List<String>[10],擦除后变成 List[],可以存入 List<Integer>,取用时强转成 List<String> 就会出错。

Q5:泛型方法和泛型类的类型参数有什么区别?

泛型类的类型参数在创建对象时确定,整个类共享;泛型方法的类型参数在调用方法时确定,每次调用可以不同。静态方法不能用类的泛型参数,必须自己声明。


思维导图速览

复制代码
Java泛型详解
├── 为什么需要泛型
│   ├── 编译期类型检查
│   └── 消除强制转换
├── 集合中的泛型
│   ├── 菱形推断
│   ├── 只能用引用类型
│   └── List<String> 不是 List<Object> 子类型
├── 方法中的泛型
│   ├── <T> 声明在返回值前
│   ├── 多类型参数 <K, V>
│   └── 静态方法要自己声明 <T>
├── 类中的泛型
│   ├── class Box<T>
│   ├── 多类型参数 class Pair<K, V>
│   └── 继承 → 保持泛型 / 指定具体类型
├── 接口中的泛型
│   ├── interface Repository<T>
│   ├── 实现 → 指定具体类型 / 保持泛型
│   └── 两种方式对比
├── 向上转型与向下转型
│   ├── 泛型不支持协变
│   ├── extends 实现只读转型
│   └── super 实现只写转型
├── 通配符
│   ├── <?> → 任意类型,只读Object
│   ├── <? extends T> → 上界,只读(PECS-Producer)
│   ├── <? super T> → 下界,只写(PECS-Consumer)
│   └── PECS原则
├── 类型擦除
│   ├── 编译后泛型信息消失
│   ├── T → Object / 上界类型
│   ├── 问题 → 不能new T/不能泛型数组/不能instanceof
│   ├── 桥方法 → 保持多态
│   ├── 原因 → 向后兼容
│   └── 反射获取泛型 → ParameterizedType / TypeToken
└── 面试必背五题
    ├── 泛型擦除及影响
    ├── List<String> vs List<Object>
    ├── extends vs super
    ├── 为什么不能创建泛型数组
    └── 泛型方法 vs 泛型类

写在最后

  1. 泛型怎么用------集合泛型、方法泛型、类泛型、接口泛型,从简单到复杂
  2. 泛型怎么转------向上转型用 extends(只读),向下转型用 super(只写),PECS 原则
  3. 泛型的代价------类型擦除是 Java 的历史包袱,理解擦除才能理解那些"不能做"的限制