W
e
l
c
o
m
e
: )

Java泛型

一、什么是泛型?

Java泛型是JDK 1.5引入的一个核心语法特性,本质上是 参数化类型 (Parameterized Type)。通俗地说,就是把类型(如String、Integer)当作参数一样传递给类、接口或方法。

先来看一个没有使用泛型的例子:

List list = new ArrayList();
list.add("Hello");
list.add(123); // 编译没问题,但容易误操作
String s = (String) list.get(0); // 必须强制转型,且转型风险高

有了泛型后:

List<String> list = new ArrayList<>();
list.add("Hello");
// list.add(123); // 编译报错,类型安全
String s = list.get(0); // 自动转型,无需强转

二、泛型的核心作用是什么?

泛型主要有三大核心作用 ,分别对应编程中的“安全、易用、重用”:

1. 编译时类型安全检查(最强优势)
泛型将运行时的 ClassCastException(类型转换异常)提前到 编译期间 。只要你往 List<String>里放Integer,IDE就会立刻报红线。这极大地提高了程序的稳定性和健壮性。

2. 消除强制类型转换(代码更简洁)
在没有泛型时,从集合中取元素必须手动 (String)强转。有了泛型,编译器自动帮你插入隐式的强转,代码更加干净利落。

3. 实现通用算法和代码复用(即“模板”思想)
开发者可以编写一套代码,适配多种数据类型。例如,你写一个排序方法,不需要为 Integer写一遍、为 Double写一遍,只需定义 <T>,传入什么类型就处理什么类型。

三、泛型具体怎么使用呢?

泛型的使用分为如下三种:

一、泛型类

泛型类通常用于封装不依赖具体类型的数据结构,比如集合、容器、POJO包装类。

定义语法class 类名 <T> { }

// 定义一个通用的“结果包装类”
public class Result<T> {
    private int code;
    private String message;
    private T data; // 数据字段类型不确定

    public Result(int code, String message, T data) {
        this.code = code;
        this.message = message;
        this.data = data;
    }

    public T getData() {
        return data; // 返回类型是 T
    }

    public void setData(T data) {
        this.data = data;
    }
}
//使用示例
public class Main {
    public static void main(String[] args) {
        // 实例化时指定 T 为 String
        Result<String> result1 = new Result<>(200, "成功", "Hello Java");
        String data1 = result1.getData(); // 直接拿 String,无需强转

        // 实例化时指定 T 为 Integer
        Result<Integer> result2 = new Result<>(200, "成功", 100);
        int data2 = result2.getData(); // 直接拿 int(自动拆箱)
    }
}

二、泛型接口

泛型接口常用于定义通用生产者和消费者接口,具体的实现类在实现时指定具体的类型。

定义语法interface 接口名 <T> { }

// 定义一个通用处理器接口
public interface Processor<T, R> {
    R process(T input); // 输入 T,输出 R
}
// -------- 实现方式1:实现时直接指定具体类型(类不再是泛型) ----------
public class StringToIntegerProcessor implements Processor<String,Integer>{
    @Override
    public Integer process(String input) {
        return Integer.parseInt(input); // 明确知道处理 String 返回 Integer
    }
}
// -------- 实现方式2:实现类也保持泛型(延迟到使用时确定) ----------
public class IdentityProcessor<T> implements Processor<T,T> {
    @Override
    public T process(T input) {
        return input;//输入是什么类型,返回就是什么类型
    }
}
//使用示例
public class Main {
    public static void main(String[] args) {
        StringToIntegerProcessor p1 = new StringToIntegerProcessor();
        Integer num = p1.process("123"); // 返回 Integer

        IdentityProcessor<Double> p2 = new IdentityProcessor<>();
        Double d = p2.process(3.14); // 返回 Double
    }
}

三、泛型方法

只要方法返回类型前面有一个独立的 <T>(或 <E>),它就是泛型方法。它和类本身是不是泛型没有必然关系

import java.util.Arrays;
import java.util.List;

public class GenericMethodUtils {
    // 这里的 <T> 声明在 static 和返回值之间,表明这是一个泛型方法
    public static <T> void swap(T[] array, int i, int j) {
        T temp = array[i];
        array[i] = array[j];
        array[j] = temp;
    }

    //泛型方法结合集合(将数组转成 List)
    public static <E> List<E> arrayToList(E[] array) {
        return Arrays.asList(array);
    }

    public static void main(String[] args) {
        String[] names = {"A", "B", "C"};
        swap(names, 0, 2); // 编译器自动推断 T 为 String
        System.out.println(Arrays.toString(names)); // 输出 [C, B, A]

        Integer[] nums = {1, 2, 3};
        List<Integer> list = arrayToList(nums); // 自动推断 E 为 Integer
    }
}

需要注意的是:假设你定义了一个泛型类,静态方法想使用类的泛型 T,这是语法不允许的!!!

// 错误示范
public class Container<T> {
    private T value;

    // 编译报错!因为静态方法随着类加载而存在,而 T 是 new 对象时才确定
    // public static T getDefaultValue() { return null; } 

    // 正确写法:静态方法自己声明自己的泛型,即便字母也叫 T,它和类的 T 也是两个完全不同的变量
    public static <T> T getDefaultValue(T defaultValue) {
        return defaultValue;
    }
}

四、泛型的类型擦除机制是什么?

简单来说,泛型只存在于编译期,在编译生成的字节码中,泛型类型信息会被“擦除”并替换为原始类型(Raw Type),JVM运行时根本不知道 List<String>List<Integer>的区别。

一、具体是怎么擦的呢?

编译器擦除时,遵循两条铁律:

  • 无限定类型(如 <T> :直接擦除为 Object
  • 有限定类型(如 <T extends Number> :擦除为第一个边界类型(这里是 Number)。

来看下面的代码示例:

//编译前
public class Box<T> {
    private T value;
    public T getValue() { return value; }
}

//编译后(等效于)
public class Box {
    private Object value;
    public Object getValue() { return value; } // 所有 T 都变成了 Object
}
//编译前(带边界的)
public class Box<T extends Number> {
    private T value;
    public T getValue() { return value; }
}

// 擦除后变成了 Number,而不是 Object
public class Box {
    private Number value;
    public Number getValue() { return value; }
}

二、桥接方法

桥接方法是擦除带来的编译器“魔法”,它保证了在泛型场景下的多态性。先来看下面的这段代码:

class Parent {
    public Object say(Object arg) { return arg; }
}

class Child extends Parent {
    public String say(String arg) { return arg; } // 注意:参数类型变了!
}

这其实不是重写 (参数不同),只是重载。但在泛型场景下,编译器为了保留多态,会偷偷生成桥接方法!!

class GenericParent<T> {
    public T say(T arg) { return arg; }
}

class GenericChild extends GenericParent<String> {
    @Override
    public String say(String arg) { return arg; }
}

擦除后,GenericParentsay 变成了 Object say(Object)。为了让 GenericChildString say(String) 正确重写它,编译器在 GenericChild 字节码中自动生成了一个桥接方法

// 这是编译器偷偷加在 GenericChild 字节码里的
public Object say(Object arg) {
    return say((String) arg); // 帮你强转,并调用你写的真实方法
}

三、擦除带来的限制

限制 错误示例 原因
不能 instanceof 具体泛型 if (list instanceof List<String>) 运行时都是 List,泛型参数不存在
不能 new T() T obj = new T(); 擦除后不知道 T 是什么,无法分配内存
不能创建泛型数组 T[] arr = new T[10]; 数组必须知道确切类型,擦除后无法保证类型安全
不能静态引用类的泛型 static T staticVar; 静态属性和类加载绑定,但 T 属于实例化时确定

还有一个需要注意的就是方法重载冲突

// 编译报错!擦除后两个方法的签名都是 print(List),在JVM中无法区分。
public void print(List<String> list) {} 
public void print(List<Integer> list) {} 

四、插入强制转型

既然类型会被“擦除”,那么编译时检查是怎么生效的呢?这就涉及擦除的另一项任务 : 插入强制转型

当你写 String s = list.get(0); 时:

  1. 编译期检查放进去的一定是 String
  2. 擦除后,list.get(0) 实际返回的是 Object
  3. 编译器自动在字节码中插入向下转型String s = (String) list.get(0);

所以泛型其实是一种 语法糖 ,它将“繁琐的手动强转”和“运行时的风险”提前转移给了编译器去自动处理。

五、反射获取泛型

既然泛型被擦除了,为什么还可以通过反射获取到泛型呢?这是因为虽然字段(Field) 和方法参数(Method) 上的泛型被擦除了,但源码中的泛型声明元数据被保留在了类的 Signature属性(字节码额外区域) 中。你可以通过反射的 getGenericParameterTypes()getGenericReturnType() 获取这些元数据。这就是为什么 GsonJackson 能够通过 TypeTokenParameterizedTypeReference 解析出 List<String> 中的 String 类型——它们利用了字节码中残留的签名信息,而非运行时实际持有的类型。

五、泛型通配符

Java泛型通配符(Wildcard)用 ? 表示,它的诞生是为了解决泛型中一个很“反直觉”的问题: List<String> 并不是 List<Object> 的子类型 。假设有一个方法 void print(List<Object> list),你无法传入 List<String>,因为编译器认为它们不兼容。为了给API增加灵活性 ,让方法能接受多种泛型类型的集合,通配符应运而生。

一、无界通配符(?)

表示类型未知,不做任何限制。

  • 语法List<?>
  • 读写权限 :只能读,且读出来都是 Object;不能写(除了写入 null)。
  • 使用场景 :方法只依赖于 Object 类的方法(如 toStringhashCode),或者不关心具体类型。
public class Main {
    public static void main(String[] args) {
        List<Integer> list = new ArrayList<>();
        list.add(1);
        list.add(2);
        list.add(3);
        printList(list);
    }

    public static void printList(List<?> list) {
        // 可以遍历,因为不管是什么类型,取出来总是 Object 的子类
        for (Object obj : list) {
            System.out.println(obj);
        }

        // 注意:不能往里面 add 元素!
        // list.add("hello"); // 编译报错!因为编译器不知道具体类型,无法保证类型安全
        list.add(null);      // 只有 null 是特例,可以加
    }
}

二、上界通配符(? extends T)

表示类型必须是 TT 的子类 (Upper Bound)。

  • 语法List<? extends Number> 表示可以是 List<Integer>List<Double>List<Number>
  • 读写权限 :只能读,读出来是 T 类型;不能写(除了 null
  • 原因 :你只知道它是 Number 的子类,但具体是 Integer 还是 Double 不确定。如果允许写入,写一个 Integer 进一个 List<Double> 就会出大问题,所以编译器 强制禁止写入
import java.util.ArrayList;
import java.util.List;

public class Main {
    public static void main(String[] args) {
        List<Integer> list = new ArrayList<>();
        list.add(1);
        list.add(2);
        list.add(3);
        System.out.println(sumOfList(list));
    }

    public static double sumOfList(List<? extends Number> list) {
        double sum = 0.0;
        // 可以安全读取,因为无论子类是什么,都能自动转型为 Number
        for (Number num : list) {
            sum += num.doubleValue();
        }

        // list.add(123); // 编译报错!不能写入具体元素
        return sum;
    }
}

三、下界通配符(? super T)

表示类型必须是 TT 的父类 (Lower Bound)。

  • 语法List<? super Integer> 表示可以是 List<Integer>List<Number>List<Object>
  • 读写权限 :可以写入(T 及其子类),但读出来全是 Object
  • 原因 :你只知道它是 Integer 的某种父类。往里面放 Integer 是绝对安全的(因为父类容器一定能装下子类)。但读的时候,编译器不知道具体是 Number 还是 Object,所以只能返回最顶层的 Object
public static void addIntegers(List<? super Integer> list) {
    // 可以安全地写入 Integer
    list.add(1);
    list.add(2);
    // list.add(3.5); // 编译报错!不能写入 Double,因为 Double 不是 Integer 的父类
  
    // 读取时,只能当做 Object
    Object obj = list.get(0);
}

四、黄金法则PECS

PECSProducer Extends, Consumer Super 的缩写。这是 Joshua Bloch(Effective Java作者)提出的最佳实践:

  • 如果集合是“生产者”(Producer) :即你从中读取数据来用,用 ? extends T
  • 如果集合是“消费者”(Consumer) :即你往里面写入数据,用 ? super T
  • 如果既要读又要写 :不要用通配符,直接用具体的类型参数 <T>
//JDK源码中利用PECS的标准写法:Collections.copy()
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
    int srcSize = src.size();
    if (srcSize > dest.size())
        throw new IndexOutOfBoundsException("Source does not fit in dest");

    if (srcSize < COPY_THRESHOLD ||
        (src instanceof RandomAccess && dest instanceof RandomAccess)) {
        for (int i=0; i<srcSize; i++)
            dest.set(i, src.get(i));
    } else {
        ListIterator<? super T> di=dest.listIterator();
        ListIterator<? extends T> si=src.listIterator();
        for (int i=0; i<srcSize; i++) {
            di.next();
            di.set(si.next());
        }
    }
}

五、注意事项

通配符只能出现在方法参数、局部变量和返回值中,不能在类声明和new中使用。

  • public class MyBox<?> {}(编译报错,声明类只能用 <T>
  • List<?> list = new ArrayList<?>();(编译报错,不能 new 通配符,右边必须写具体类型,如 new ArrayList<String>()
posted @ 2026-08-02 14:25  寒月静无光  阅读(5)  评论(0)    收藏  举报