Java 范型

119 阅读12分钟

Java 范型

一个范型问题 引发的,想要再次了解下 Java 范型。

一、什么是 Java 泛型?

Java 泛型(Generics)是 JDK 1.5 引入的一项语言特性,允许在类、接口和方法中使用类型参数,从而实现类型参数化。泛型的主要目的是在编译期提供类型安全,避免强制类型转换,提高代码的复用性和可读性。


二、泛型的基本用法

1. 泛型类

public class Box<T> {
    private T value;
    public void set(T value) { this.value = value; }
    public T get() { return value; }
}

使用时指定类型:

Box<String> box = new Box<>();
box.set("hello");
String s = box.get();

2. 泛型接口

public interface Converter<F, T> {
    T convert(F from);
}

3. 泛型方法

public <T> void printArray(T[] array) {
    for (T t : array) {
        System.out.println(t);
    }
}

三、常见泛型集合

  • List<T>
  • Map<K, V>
  • Set<T>
  • Optional<T>
  • Comparable<T>

四、通配符(Wildcard)

1. 无限定通配符 ?

List<?> list = new ArrayList<String>();

表示元素类型未知,可以是任何类型。

2. 上界通配符 ? extends T

List<? extends Number> list = new ArrayList<Integer>();

表示元素类型是 Number 或其子类(如 Integer、Double)。

3. 下界通配符 ? super T

List<? super Integer> list = new ArrayList<Number>();

表示元素类型是 Integer 或其父类(如 Number、Object)。


五、泛型的类型擦除

  • Java 泛型在编译后会进行类型擦除,即泛型参数会被替换为限定类型(如 Object 或上界类型)。
  • 运行时不会保留泛型类型信息,这就是为什么不能用 new T()、不能直接获取泛型类型的 Class 对象。

示例:

List<String> list1 = new ArrayList<>();
List<Integer> list2 = new ArrayList<>();
System.out.println(list1.getClass() == list2.getClass()); // true

六、泛型的限制

  1. 不能用基本类型作为泛型参数
    如 List<int> 是非法的,必须用包装类型 List<Integer>
  2. 不能创建泛型数组
    如 T[] arr = new T[10]; 是非法的。
  3. 不能实例化泛型类型参数
    如 new T() 是非法的。
  4. 不能捕获泛型类型的异常
    如 catch (T e) 是非法的。
  5. 静态成员不能引用类的泛型参数
    因为静态成员属于类,不属于实例。

七、泛型方法的类型推断

Java 编译器可以根据方法参数自动推断泛型类型:

public static <T> T pick(T a, T b) { return a; }
String s = pick("hello", "world"); // T 被推断为 String

八、泛型与反射

由于类型擦除,反射时无法直接获取泛型参数类型。但可以通过 TypeParameterizedType 等接口间接获取声明时的泛型信息。


九、泛型的高级用法

1. 边界限定

class NumberBox<T extends Number> { ... }

T 只能是 Number 或其子类。

2. 通配符捕获

有时需要将 List<?> 转为具体类型,可以用泛型方法实现“通配符捕获”:

public static void printList(List<?> list) {
    printHelper(list);
}
private static <T> void printHelper(List<T> list) {
    for (T t : list) {
        System.out.println(t);
    }
}

3. 泛型接口的实现

class StringConverter implements Converter<String, Integer> {
    public Integer convert(String from) { return Integer.valueOf(from); }
}

十、泛型的好处

  • 类型安全:编译期检查类型,避免 ClassCastException。
  • 代码复用:同一份代码可用于多种类型。
  • 可读性强:代码意图更明确。

十一、常见面试题与陷阱

为什么 Java 泛型采用类型擦除?

Java 泛型采用类型擦除(Type Erasure)是为了兼容历史版本(向下兼容),即保证泛型引入后,老版本(如 JDK1.4 及之前)编译的字节码依然能在新 JVM 上运行。类型擦除是在编译阶段将泛型参数去除或替换为限定类型(如 Object 或上界),这样 JVM 运行时无需感知泛型类型,保证了二进制兼容性和运行时性能。

泛型和协变、逆变的区别

  • 泛型:允许类、接口、方法参数化类型,提高类型安全和代码复用。
  • 协变(Covariant) :子类型可以赋值给父类型(如数组、List<? extends T>),只读安全,不能写入。
  • 逆变(Contravariant) :父类型可以赋值给子类型(如 List<? super T>),只写安全,不能读取为具体类型。
  • 区别:泛型本身是类型参数化,协变/逆变描述的是类型参数之间的赋值兼容关系。Java 泛型通过通配符 ? extends T(协变)、? super T(逆变)实现。

泛型数组为什么不能直接创建?

Java 泛型在运行时会类型擦除,无法保留实际的类型参数信息。而数组在运行时需要知道元素的具体类型(如 String[]Integer[]),否则类型检查无法实现。
如果允许直接创建泛型数组(如 T[] arr = new T[10];),会导致类型安全问题和运行时异常。因此 Java 语法禁止直接创建泛型数组。

泛型方法和泛型类的区别?

  • 泛型类:类定义时声明类型参数,整个类的成员都可以使用该类型参数。例如:class Box<T> { ... }
  • 泛型方法:方法定义时声明类型参数,类型参数只在该方法内有效。可以在泛型类、普通类中定义泛型方法。例如:<T> void print(T t) { ... }
  • 区别:泛型类的类型参数作用于整个类,泛型方法的类型参数只作用于方法本身,且泛型方法可以独立于泛型类存在。

十二、总结

Java 泛型是强类型语言实现类型安全和代码复用的重要机制。理解泛型的声明、使用、通配符、类型擦除及其限制,是写好高质量 Java 代码的基础。

为什么不直接使用具体的类

使用泛型(Generics)相比直接使用具体类,有以下几个重要优势:


1. 类型安全

泛型在编译期就能检查类型错误,避免了强制类型转换和 ClassCastException。例如:

List<String> list = new ArrayList<>();
list.add("hello");
// list.add(123); // 编译时报错

如果不用泛型,List 只能用 Object,容易出错:

List list = new ArrayList();
list.add("hello");
list.add(123); // 运行时才报错

2. 代码复用

泛型让同一份代码可以适用于多种类型,无需为每种类型写重复代码。例如:

public class Box<T> {
    private T value;
    public void set(T value) { this.value = value; }
    public T get() { return value; }
}

可以用 Box<String>Box<Integer> 等,提升复用性。


3. 可读性和可维护性

泛型让代码意图更清晰,看到 List<User> 就知道这个集合里只能放 User 类型,易于理解和维护。


4. 与集合框架等标准库兼容

Java 集合框架等大量标准库都基于泛型设计,使用泛型可以无缝集成和扩展。


总结:
泛型提升了类型安全、代码复用性和可读性,是现代 Java 编程的重要基础。直接用具体类会导致类型不安全、代码重复和维护困难。

范型和 Object

1. 泛型和 Object 的区别

  • Object 是所有类的父类,任何类型都可以赋值给 Object。
  • 泛型 是一种在编译期约束类型的机制,可以让类、接口、方法支持类型参数化。

2. 只用 Object 有什么问题?

  • 类型不安全:用 Object 时,集合里可以放任何类型,取出时需要强制类型转换,容易出错。
List list = new ArrayList();
list.add("hello");
list.add(123);
String s = (String) list.get(1); // 运行时报 ClassCastException
  • 可读性差:看到 List 不知道里面是什么类型,代码难以理解和维护。
  • 代码冗余:每次取值都要强转,代码繁琐。

3. 泛型的优势

  • 类型安全:编译期检查类型,避免 ClassCastException。
List<String> list = new ArrayList<>();
list.add("hello");
// list.add(123); // 编译时报错

无需强制类型转换:取出元素时直接是目标类型。

String s = list.get(0); // 不需要强转
  • 代码复用:泛型类/方法可以适用于多种类型,无需重复写代码。
  • 可读性强List<User> 一看就知道只能存 User 类型。

4. 总结

  • 用 Object 只能实现“万能容器”,但没有类型约束,容易出错。
  • 泛型让代码更安全、简洁、易维护,是现代 Java 编程的推荐方式。

所以,泛型不是 Object 的替代品,而是对类型安全和代码复用的提升。

范型擦除

一、什么是泛型擦除(Type Erasure)?

泛型擦除是指:Java 编译器在编译阶段会移除所有的泛型类型信息,将泛型参数替换为限定类型(如没有限定就替换为 Object,有上界就替换为上界类型),并在必要时插入类型转换代码。
运行时的 JVM 并不知道泛型的存在。

示例:

List<String> list = new ArrayList<>();
list.add("hello");
String s = list.get(0);

编译后,实际上变成了:

List list = new ArrayList();
list.add("hello");
String s = (String) list.get(0); // 编译器自动加了强转

二、为什么要进行泛型擦除?

1. 兼容历史版本(向下兼容)

  • Java 泛型是在 JDK 1.5 引入的。为了让老版本(如 JDK 1.4)编译的类库和新版本泛型代码能一起运行,Java 采用了泛型擦除。
  • 这样,老的 JVM 和类库无需修改就能和泛型代码兼容。

2. 简化 JVM 实现

  • 泛型擦除后,JVM 不需要感知和处理泛型类型,所有类型检查都在编译期完成,运行时只处理普通类型。
  • 避免了 JVM 层面的复杂性和性能损耗。

3. 减少字节码膨胀

  • 如果每种泛型类型都生成一份字节码(像 C++ 模板那样),会导致类文件数量和体积急剧增加。
  • 泛型擦除后,所有泛型实例共用一份字节码,节省空间。

三、泛型擦除的好处

  1. 保证了 Java 的二进制兼容性
    泛型代码和非泛型代码可以无缝协作,老代码无需重编译。
  2. JVM 实现简单高效
    JVM 只需处理普通类型,类型安全由编译器保证。
  3. 节省内存和类加载开销
    所有泛型实例共用一份字节码,避免类爆炸。

四、泛型擦除的副作用

  • 运行时无法获取泛型类型信息,如不能直接 new T()、不能用 instanceof 检查泛型类型、不能创建泛型数组等。
  • 类型安全只能在编译期保证,运行时类型信息丢失。

五、总结

  • 泛型擦除是 Java 泛型实现的核心机制,保证了向下兼容、JVM 简单高效和节省资源。
  • 但也带来了一些限制,如运行时无法获取泛型类型信息。

范型擦除原理

1. 泛型擦除是在什么阶段做的?

泛型擦除是在 Java 源代码编译阶段(由 javac 编译器)完成的。
也就是说,在编译生成 .class 字节码文件时,所有泛型相关的信息就被移除了


2. 泛型擦除是在哪里做的?

泛型擦除的具体实现是在**Java 编译器(javac)**内部完成的。
JVM 运行时(即加载和执行 .class 文件时)完全不知道泛型的存在,只处理普通类型。


3. 泛型擦除的原理细节

1)类型参数替换

  • 如果泛型没有上界(如 <T>),编译器会把所有的 T 替换为 Object
  • 如果有上界(如 <T extends Number>),会替换为上界类型(如 Number)。

2)插入类型转换

  • 在取出泛型对象时,编译器会自动插入强制类型转换代码,保证类型安全。

3)移除泛型方法和类的类型参数

  • 泛型方法和类的类型参数在字节码中被移除,方法签名和类签名都不再包含泛型信息。

4)桥方法(Bridge Method)

  • 为了保证多态和重写的正确性,编译器有时会自动生成桥方法。例如泛型方法重写时,生成一个无泛型签名的方法,内部调用带泛型的方法。

4. 示例

源码:

public class Box<T> {
    private T value;
    public void set(T value) { this.value = value; }
    public T get() { return value; }
}

编译后(伪代码):

public class Box {
    private Object value;
    public void set(Object value) { this.value = value; }
    public Object get() { return value; }
}

5. 总结

  • 泛型擦除发生在javac 编译阶段,由编译器实现。
  • JVM 运行时不感知泛型,所有泛型类型信息在字节码中已被移除。
  • 类型参数被替换为上界或 Object,必要时插入类型转换,保证类型安全。

桥方法

一、什么是桥方法(Bridge Method)?

桥方法是 Java 编译器在泛型类型擦除后,为了保证多态和方法重写的正确性,自动生成的合成方法(synthetic method)
它的作用是让泛型方法在类型擦除后依然能正确地实现方法重写和多态。


二、为什么需要桥方法?

泛型擦除后,子类和父类的方法签名可能不同,导致多态失效。
桥方法就是为了解决这种方法签名不一致的问题,让 JVM 能正确识别重写关系。


三、举例说明

1. 示例代码

class Parent<T> {
    public T get() { return null; }
}

class Child extends Parent<String> {
    @Override
    public String get() { return "hello"; }
}

2. 泛型擦除后

  • Parent<T> 擦除后变成 Parent,方法签名为 Object get()
  • Child 的 get() 方法签名为 String get()

此时,Child 并没有重写 Parent 的 Object get(),而是新增了一个 String get(),多态会失效。


3. 编译器如何处理?

编译器会为 Child 自动生成一个桥方法:

// 这是桥方法
public Object get() {
    return get();
}

// 这是你写的重写方法
public String get() {
    return "hello";
}

桥方法的作用是:

  • 保证 Child 既有 Object get()(和父类签名一致),也有 String get()(你实际实现的)。
  • 当父类引用调用 get() 时,JVM 能正确找到子类的实现。

4. 调用过程

Parent p = new Child();
Object o = p.get(); // 实际调用的是 Child 的桥方法,再转调 String get()

四、桥方法的特点

  • 由编译器自动生成,源码中看不到。
  • 标记为 synthetic,可用反射 API 识别。
  • 只在泛型、协变返回类型等场景下出现。

五、桥方法的应用场景

  1. 泛型重写:如上例。
  2. 协变返回类型:子类重写父类方法时返回更具体的类型,也会生成桥方法。
class Parent { Object get() {...} }
class Child extends Parent { @Override String get() {...} }

六、总结

  • 桥方法是编译器为保证泛型擦除后多态和重写正确性自动生成的合成方法。
  • 让子类和父类的方法签名在擦除后依然兼容,保证 Java 多态机制正常工作。
  • 源码不可见,但可通过反射或 javap 工具看到。