跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客我的书AI学习GitHub 精选镜像AI工具UI配色美学关于
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
Javajava

Java 泛型:编译期类型安全与实战应用

Java 泛型通过参数化类型实现编译期检查,减少强制转换和运行时错误。本文解析泛型类、接口及方法的定义语法,探讨类型擦除机制对字节码的影响,以及通配符(?)的上限与下限约束逻辑。结合数组存储、集合操作等实战案例,说明如何编写高复用性且类型安全的代码,避免常见陷阱如泛型数组实例化问题。掌握泛型边界与推导规则,能显著提升 Java 程序的健壮性与可维护性。

beaabea发布于 2026/3/29更新于 2026/10/2488 浏览
Java 泛型:编译期类型安全与实战应用

什么是泛型

**泛型(Generics)**是 Java 编程语言中引入的强大特性,它提供了编译时的类型安全检测机制。这意味着编译器可以在编译期间检测到非法的类型操作,从而减少程序中的强制类型转换和运行时错误的可能性。

在泛型出现之前,一般的类和方法只能使用具体的类型:要么是基本类型,要么是自定义的类。如果要编写可以应用于多种类型的代码,这种刻板的限制对代码的束缚很大。因此,JDK 1.5 引入了泛型语法。

通俗来讲,泛型就是适用于许多类型。从代码上讲,就是对类型实现了参数化。

引出泛型

假设我们需要实现一个类,类中包含一个数组成员,希望数组中可以存放任何类型的数据,同时也能根据下标返回特定元素。以前我们通常会将数组定义为 Object 类型。

class MyArray {
    public Object[] array = new Object[10];
    
    public Object getPos(int pos) {
        return this.array[pos];
    }
    
    public void setVal(int pos, Object val) {
        this.array[pos] = val;
    }
}

public class TestDemo {
    public static void main(String[] args) {
        MyArray myArray = new MyArray();
        myArray.setVal(0, 10);
        myArray.setVal(1, "hello");
        String ret = (String) myArray.getPos(1); // 必须强制类型转换
        System.out.println(ret);
    }
}

虽然上述代码能运行,但存在明显问题:

  1. 任何类型数据都可以存放,失去了类型约束。
  2. 获取数据时必须进行强制类型转换,容易引发 ClassCastException。

我们希望容器只能够持有一种数据类型,让编译器去做检查。此时就需要把类型作为参数传递。需要什么类型,就传入什么类型。

泛型类

定义与语法

定义类时,在类名后加上用尖括号括起来的类型形参,这个类就是泛型类。

  • 创建泛型类的实例对象时传入不同的类型实参,就可以动态生成任意多个该泛型类的子类。
  • JDK 类包中泛型类最典型的应用就是各种容器类,如 ArrayList、HashMap 等。

定义泛型类:

class 泛型类名称 < 类型形参列表 > { 
    // 这里可以使用类型参数 
}

class 泛型类名称 < 类型形参列表 > extends 继承类 {
    // 这里可以使用类型参数 
}

实例化:

泛型类 < 类型实参 > 变量名; 
new 泛型类 < 类型实参 > (构造方法实参);

类名 <类型形参变量> 是一个整体的数据类型,通常称为泛型类型。为了提高可读性,建议使用有意义的字母:

  • E: Element(元素),常用在 Collection 中,如 List<E>。
  • K, V: Key 和 Value(Map 的键值对)。
  • N: Number(数字)。
  • T: Type(类型),如 String、Integer 等。

注意:泛型只能接受类,所有的基本数据类型必须使用包装类。

改进示例

让我们用泛型改进之前的 MyArray 类:

class MyArray<T> {
    // 注意:不能直接 new T[5],因为泛型擦除后 T 是 Object
    public T[] array = (T[]) new Object[10];
    
    public T getPos(int pos) {
        return this.array[pos];
    }
    
    public void setVal(int pos, T val) {
        this.array[pos] = val;
    }
}

public class TestDemo {
    public static void main(String[] args) {
        // 指定类型为 Integer
        MyArray<Integer> myArray = new MyArray<>();
        
        myArray.setVal(0, 10);
        myArray.setVal(1, 12);
        
        // 不需要进行强制类型转换
        int ret = myArray.getPos(1);
        System.out.println(ret);
        
        // 下面这行会编译报错,因为类型被限制了
        // myArray.setVal(2, "Island1314"); 
    }
}

关键点说明:

  1. 类名后的 <T> 代表占位符,表示当前类是一个泛型类。
  2. 实例化时加入 <Integer> 指定当前类型,或者使用菱形运算符 <> 进行类型推导。
  3. 获取数据时不需要强制类型转换,编译器已保证类型安全。
  4. 如果尝试存入不兼容的类型,编译器会在编译阶段报错。

对于集合的使用也是同理,创建集合时传入对应的类型即可:

public static void main(String[] args) {
    // 只创建一个只保存 Integer 类型的 List 集合
    List<Integer> intList = new ArrayList<>();
    intList.add(1);
    
    // 下面的代码就会编译报错
    // intList.add('2');
    
    for (int i = 0; i < intList.size(); i++) {
        // 无需进行强制类型转换的事情
        Integer num = intList.get(i);
    }
}
类型推导

当编译器可以根据上下文推导出类型实参时,可以省略类型实参的填写,这就是类型推导(Type Inference)。

MyArray<Integer> list = new MyArray<>(); // 可以推导出实例化需要的类型实参为 Integer
自定义泛型类演示
// 定义泛型类 Goods
class Goods<T> {
    // 类型形参变量作用于属性的类型
    private T info;
    
    // 类型形参变量作用于构造方法的参数类型
    public Goods(T info) {
        this.info = info;
    }
    
    // 类型形参变量作用于方法的参数类型
    public void setInfo(T info) {
        this.info = info;
    }
    
    // 类型形参变量作用于方法的返回值类型
    public T getInfo() {
        return this.info;
    }
}

public class Example {
    public static void main(String[] args) {
        // 创建 Goods 对象
        Goods<Integer> goods = new Goods<>(666);
        
        System.out.println(goods.getInfo() + "..." + goods.getInfo().getClass());
        
        // 传入实参为 String 类型
        goods.setInfo("热卖商品");
        System.out.println(goods.getInfo() + "..." + goods.getInfo().getClass());
    }
}

由输出可以看出:属性 info 的值在初始化为 666 时,类型为 Integer;当调用 setInfo("热卖商品") 之后,由于泛型类实例化时确定了类型,此处实际上会报错或保持原类型(取决于具体实现逻辑,但在本例中若未重新声明泛型类型则无法改变 T 的实际绑定)。更准确的理解是:类型形参会根据类型实参来确定。一旦实例化完成,T 就被固定了。

泛型是如何编译的?

类型擦除

Java 的泛型机制是在编译级别实现的。通过命令 javap -c 查看字节码文件,可以看到所有的 T 都被替换为了 Object。

在编译过程中,将所有的 T 替换为 Object 这种机制,我们称为擦除机制。

  1. 编译器生成的字节码在运行期间并不包含泛型的类型信息。
  2. 这解释了为什么泛型只能在编译期提供类型检查,而不能在运行时获取泛型的具体类型。
泛型类型数组为什么不能实例化
class MyArray<T> {
    public T[] array = (T[]) new Object[10];
    public T getPos(int pos) { return this.array[pos]; }
    public T[] getArray() { return array; }
}

public class Example {
    public static void main(String[] args) {
        MyArray<Integer> myArray = new MyArray<>();
        Integer[] num = myArray.getArray(); // 抛出 ClassCastException
    }
}

原因: 替换后的方法签名实际上是 public Object[] getArray()。将 Object[] 分配给 Integer[] 引用,程序报错。

通俗讲就是:返回的 Object 数组里面,可能存放的是任何的数据类型,可能是 String,可能是 Person,运行的时候,直接转给 Integer 类型的数组,编译器认为是不安全的。

正确的方式(了解即可):

class MyArray<T> {
    public T[] array;
    
    /**
     * 通过反射创建,指定类型的数组
     */
    public MyArray(Class<T> clazz, int capacity) {
        array = (T[]) Array.newInstance(clazz, capacity);
    }
    
    public T getPos(int pos) { return this.array[pos]; }
    public void setVal(int pos, T val) { this.array[pos] = val; }
    public T[] getArray() { return array; }
}

泛型接口

概述

定义泛型接口和定义泛型类的语法格式类似,在接口名称后面加上用尖括号括起来类型形参即可。与集合相关的很多接口也是泛型接口,如 Collection、List 等。

定义泛型接口的基本语法格式如下:

【访问权限】interface 接口名称 <类型形参变量> ()

按照上面格式定义一个泛型接口:

interface Info<T> {
    public T getVar();
}

泛型接口的实现类有两种定义方式:

  1. 使用非泛型类实现泛型接口。
  2. 使用泛型类实现泛型接口。
非泛型类实现泛型接口

当使用非泛型类实现接口时,需要明确接口的泛型类型,也就是需要将类型实参传入接口。

public interface Inter<T> {
    public abstract void show(T t);
};

public class InterImpl implements Inter<String> {
    @Override
    public void show(String s) {
        System.out.println(s);
    }
}

public static void main(String[] args) {
    Inter<String> inter = new InterImpl();
    inter.show("Hello Island1314");
}

在上面代码中,在接口后面传入的类型实参类型为 String。这样,在 InterImpl 实现类中重写 Inter 接口中的 show() 方法时,就需要指明 show() 方法的参数类型为 String。

泛型类实现泛型接口

当使用泛型类实现泛型接口时,需要将泛型的声明加在实现类中,并且泛型类和泛型接口使用的必须是同一个类型形参变量。

public class InterImpl<T> implements Inter<T> {
    @Override
    public void show(T t) {
        System.out.println(t);
    }
}

public static void main(String[] args) {
    Inter<String> inter = new InterImpl<>();
    inter.show("IsLand~");
    
    Inter<Integer> i = new InterImpl<>();
    i.show(1314);
}

泛型上界

在定义泛型类时,有时需要对传入的类型变量做一定的约束,可以通过类型边界来约束。

语法
class 泛型类名称 < 类型形参 extends 类型边界 > { ... }
示例

案例一:

public class MyArray<E extends Number> { ... }

只接受 Number 的子类型作为 E 的类型实参。

MyArray<Integer> l1; // 正常,因为 Integer 是 Number 的子类型
MyArray<String> l2; // 编译错误,因为 String 不是 Number 的子类型

如果没有指定类型边界 E,可以视为 E extends Object。

案例二:

public class MyArray<E extends Comparable<E>> { ... }

E 必须是实现了 Comparable 接口的。

泛型方法

概述

泛型方法是将类型形参的声明放在修饰符和返回值类型之间的方法。在 Java 程序中,定义泛型方法常用的格式如下:

[访问权限修饰符] [static] [final] <类型形参> 返回值类型 方法名 (形参列表) { ... }

定义泛型方法时候,需要注意以下要点:

  • 访问权限修饰符、static 和 final 都必须写在类型形参的前面。
  • 返回值类型必须写在类型形参的后面。
  • 泛型方法可以在泛型类中,也可以用在普通类中。
  • 泛型类中的任何方法本质上都是泛型方法,所以在实际使用中很少会在泛型类中显式地用上面的形式定义泛型方法。
  • 类型形参可以用在方法体中修饰局部变量,也可以修饰方法的返回值。
  • 泛型方法可以是实例方法,也可以是静态方法。

泛型方法能提高代码的重用性和程序的安全性。如果设计泛型方法可以取代整个类的泛型化,就应该优先采用泛型方法。

示例如下:

public class Util {
    // 静态的泛型方法 需要在 static 后用 <> 声明泛型类型参数
    public static <E> void swap(E[] array, int i, int j) {
        E t = array[i];
        array[i] = array[j];
        array[j] = t;
    }
}

// 使用如下:
// 情况一:可以类型推导
Integer[] a = {1, 2};
swap(a, 0, 1);
String[] b = {"a", "b"};
swap(b, 0, 1);

// 情况二:不使用类型推导
Util.<Integer>swap(a, 0, 1);
泛型方法的使用

泛型方法的使用通常有如下两种形式:

对象名 | 类名.<类型实参>方法名 (实参列表)
对象名 | 类名.方法名 (类型实参列表)

注意:

  1. 如果泛型方法是实例方法,则需要使用对象名进行调用。
  2. 如果泛型方法是静态方法,可以使用类名进行调用。

上述两种调用泛型方法的形式的差别在于方法名之前是否显式地指定类型实参。调用时是否需要显式地指定了类型实参,要根据泛型方法的声明形式以及调用时编译器能否从实参列表中获得足够的类型信息决定。如果编译器能够根据实参推断出参数类型,就可以不指定类型实参;反之则需要指定类型实参。

案例演示:

class Student {
    // 静态泛型方法
    public static <T> void staticMethod(T t) {
        System.out.println(t + "..." + t.getClass());
    }
    
    // 泛型方法
    public <T> void otherMethod(T t) {
        System.out.println(t + "..." + t.getClass());
    }
}

public class Demo {
    public static void main(String[] args) {
        // 使用形式一调用静态泛型方法
        Student.staticMethod("staticMethod");
        
        // 使用形式二调用静态泛型方法
        Student.<String>staticMethod("staticMethod");
        
        Student stu = new Student();
        
        // 形式一调用普通的泛型方法
        stu.otherMethod(666);
        
        // 形式二调用普通的泛型方法
        stu.<Integer>otherMethod(666);
    }
}

我们发现形式一和形式二的输出结果一样。这说明泛型方法可以在非泛型类中定义,并且可以在调用泛型方法的时候确定泛型的具体类型。虽然输出一致,但形式一需要隐式传入类型实参,不能直观地看出调用的方法是泛型方法,不利于代码的阅读和维护。因此,通常建议使用第二种形式调用泛型方法。

类型通配符

一般情况下,创建泛型类的实例对象时,应该为泛型类传入一个类型实参,以确定该泛型类的泛型类型。有时候,使用泛型类或者接口时传递的类型实参是不确定的,使用固定的类型形参接收类型实参存在局限性,此时就可以使用类型通配符接收不同的类型实参。

概述

类型通配符用一个问号(?)表示,类型通配符可以匹配任何类型的类型实参。

// 定义泛型类 Person
class Person<T> {
    private T info;
    public Person(T info) { this.info = info; }
    public void setInfo(T info) { this.info = info; }
    public T getInfo(){ return info; }
}

public class Demo {
    public static void main(String[] args) {
        // 创建 Person 对象,传入 String 类型的类型实参
        Person<?> person = new Person<String>("IsLand~");
        System.out.println(person.getInfo() + "..." + person.getInfo().getClass());
        
        // 创建 Person 对象,传入 Integer 类型的类型实参
        person = new Person<Integer>(1314);
        System.out.println(person.getInfo() + "..." + person.getInfo().getClass());
    }
}

可以看出,控制台成功输出了两条信息,说明泛型类 Person 的对象接收了两种不同的类型实参。

注意: 如果创建 Person 对象时不使用类型通配符,而是使用知道的类型实参,则会出现编译时异常。这个时候,可能有些读者会觉得可以使用 Object 来代替类型通配符接收所有类型。那么下面使用 Object 代替上述代码的类型通配符,修改后也出现了编译时异常。

原因: 在泛型中类名和泛型的声明是一个整体类型,Person<Object> 并不是 Person<String> 的父类。

因此我们可以通过使用类型通配符,来接收所有的泛型类型,而且可以不让用户随意修改。

在 ? 的基础上又产生了两个子通配符:

  • ? extends 类:设置通配符上限。
  • ? super 类:设置通配符下限。
类型通配符的限定

前面使用类型通配符的时候,实际上是任意设置的,只要是类就可以设置。但是有时候需要对类型通配符的使用进行限定,主要限定类型通配符的上限和下限。

设定类型通配符的上限

当使用 Person<?> 时,表示泛型类 Person 可以接收所有类型的类型实参。但有时不想让某个泛型类接收所有类型的类型实参,只想接收指定的类型及其子类,这时可以为类型通配符设定上限。

设定类型通配符的上限的语法格式如下:

<? extends 类>

案例一:

// 设定类型通配符的上限,此时传入的类型实参必须是 Number 类型 或者 其子类
public static void getElement(Collection<? extends Number> coll){}

public static void main(String[] args) {
    // 创建 Collection 对象,传入 Number 类型的类型实参
    Collection<Number> l1 = new ArrayList<Number>();
    // 创建 Collection 对象,传入 Integer 类型的类型实参
    Collection<Integer> l2 = new ArrayList<Integer>();
    // 创建 Collection 对象,传入 String 类型的类型实参
    Collection<String> l3 = new ArrayList<String>();
    
    getElement(l1); // OK
    getElement(l2); // OK
    getElement(l3); // 编译错误,String 不是 Number 的子类
}

案例二:

class Food {}
class Fruit extends Food {}
class Apple extends Fruit {}
class Banana extends Fruit {}

class Message<T> {
    private T message;
    public T getMessage() { return message; }
    public void setMessage(T message) { this.message = message; }
}

public class Demo {
    public static void main(String[] args) {
        Message<Apple> message = new Message<>();
        message.setMessage(new Apple());
        fun(message);
        
        Message<Banana> message2 = new Message<>();
        message2.setMessage(new Banana());
        fun(message2);
    }
    
    // 此时使用通配符"?"描述的是它可以接收任意类型,但是由于不确定类型,所以无法修改
    public static void fun(Message< ? extends Fruit> temp) {
        //temp.setMessage(new Banana()); // 仍然无法修改!
        //temp.setMessage(new Apple()); // 仍然无法修改!
        System.out.println(temp.getMessage());
    }
}

此时无法在 fun 函数中对 temp 进行添加元素,因为 temp 接收的是 Fruit 和他的子类,此时存储的元素应该是哪个子类无法确定。所以添加会报错!但是可以获取元素。

  • 注意:通配符的上界,不能进行写入数据,只能进行读取数据。
设定类型通配符的下限

设定类型通配符时,除了可以设定类型通配符的上限,也可以对类型通配符的下限进行设定。设定类型通配符的下限后,类型实参只能是设定的类型或其父类型。

设定类型通配符的下限的语法格式如下:

<? super 类>

案例一:

// 设定类型通配符的下限,此时传入的类型实参必须是 Number 类型 或者 其父类
public static void getElement(Collection< ? super Number> coll) {}

public static void main(String[] args) {
    Collection<Number> l1 = new ArrayList<Number>();
    Collection<Object> l2 = new ArrayList<Object>();
    Collection<Integer> l3 = new ArrayList<Integer>();
    
    getElement(l1); // OK
    getElement(l2); // OK
    getElement(l3); // 编译错误,Integer 不是 Number 的父类
}

案例二:

class Food {}
class Fruit extends Food {}
class Apple extends Fruit {}

class Plate<T> {
    private T plate;
    public T getPlate() { return plate; }
    public void setPlate(T plate) { this.plate = plate; }
}

public class Demo {
    public static void main(String[] args) {
        Plate<Fruit> plate1 = new Plate<>();
        plate1.setPlate(new Fruit());
        fun(plate1);
        
        Plate<Food> plate2 = new Plate<>();
        plate2.setPlate(new Food());
        fun(plate2);
    }
    
    public static void fun(Plate< ? super Fruit> temp) {
        // 此时可以修改!!添加的是 Fruit 或者 Fruit 的子类
        temp.setPlate(new Apple()); // 这个是 Fruit 的子类
        temp.setPlate(new Fruit()); // 这个是 Fruit 的本身
        
        // Fruit fruit = temp.getPlate(); // 不能接收,这里无法确定是哪个父类
        System.out.println(temp.getPlate()); // 只能直接输出
    }
}
  • 注意:通配符的下界,不能进行读取数据(指强转为具体子类),只能写入数据。

小结

  1. 泛型是将数据类型参数化,进行传递。
  2. 使用 <T> 表示当前类是一个泛型类。
  3. 泛型目前为止的优点:数据类型参数化,编译时自动进行类型检查和转换。

使用泛型的好处

(1)提高类型的安全性 使用泛型后,将类型的检查从运行期提前到编译期。在编译期进行类型检查,可以更早、更容易地找出因为类型限制而导致的类型转换异常,从而提高程序的可靠性。

(2)消除强制类型转换 使用泛型后,程序会记住当前的类型形参,从而无须对传入的实参值进行强制类型转换,使得代码更加清晰和简洁,可读性更高。

(3)提高代码复用性 使用泛型后,可以更好地将程序中通用的代码提取出来,在使用时传入不同类型的参数,避免了多次编写相同功能的代码,提高了代码的复用性。

(4)拥有更高的运行效率 使用泛型前,传入的实际参数值作为 Object 类型传递时,需要进行封箱和拆箱操作,会增加程序运行的开销;使用泛型后,类型形参中都需要使用引用数据类型,即传入的实际参数的类型都是对应的引用数据类型,避免了封箱和拆箱操作,减少了程序运行的开销,提高了程序的运行效率。

目录

  1. 什么是泛型
  2. 引出泛型
  3. 泛型类
  4. 定义与语法
  5. 改进示例
  6. 类型推导
  7. 自定义泛型类演示
  8. 泛型是如何编译的?
  9. 类型擦除
  10. 泛型类型数组为什么不能实例化
  11. 泛型接口
  12. 概述
  13. 非泛型类实现泛型接口
  14. 泛型类实现泛型接口
  15. 泛型上界
  16. 语法
  17. 示例
  18. 泛型方法
  19. 概述
  20. 泛型方法的使用
  21. 类型通配符
  22. 概述
  23. 类型通配符的限定
  24. 设定类型通配符的上限
  25. 设定类型通配符的下限
  26. 小结

更多推荐文章

查看全部
  • Faster-Whisper-GUI 日语语音识别异常问题解析与实战方案
  • 字符串模拟题精选:思维与实现解析
  • GitHub Copilot 学生认证申请流程与常见问题排查
  • QTTabBar:为 Windows 资源管理器添加标签页功能
  • Pandoc 跨平台安装与配置指南:Windows/macOS/Linux
  • 为 OpenClaw 构建双层记忆系统:QMD 与 Mem0 混合架构实战
  • Spring AI:Java 生态的 AI 赋能与企业级应用实践
  • Stable Diffusion v1.5 核心原理与部署实战
  • C++ STL 源码解析:基于红黑树实现 map 和 set
  • AI Agent 开发入门:零基础也能学吗
  • 文心一言大模型本地部署与微调实战指南
  • AI 小说生成器:从零打造智能写作助手
  • 宇树 Unitree 机器人 ROS 2 Humble 环境部署指南 (Go2/B2/H1)
  • GPT-OSS-20B 多用户并发 WEBUI 压力测试
  • Python PDF 生成实战指南:fpdf2 全方位应用
  • LLM 大模型技术实战:入门大模型开发框架 LangChain
  • One API 统一调用 ChatGLM/文心一言等 20+ 大模型
  • 本地运行 Qwen-Image:Windows 上 ComfyUI 部署记录
  • Sora 2 技术解析:AI 视频生成核心突破与应用场景
  • RTK 免像控验证:无人机摄影测量精度对比与代码实现

相关免费在线工具

  • Keycode 信息

    查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online

  • Escape 与 Native 编解码

    JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online

  • JavaScript / HTML 格式化

    使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online

  • JavaScript 压缩与混淆

    Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online

  • Base64 字符串编码/解码

    将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online

  • Base64 文件转换器

    将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online