Java泛型介绍

1. 泛型简介

泛型(Generics) 是 JDK 5 引入的特性,它允许在定义类、接口和方法时使用类型参数 ,从而在编译时提供类型安全检查,并避免强制类型转换。

1.1 为什么需要泛型?

泛型的出现不是为了增加语言复杂性,而是为了解决真实存在的工程问题

  1. 提高程序的健壮性(减少运行时异常)
  2. 提升开发效率(减少样板代码,更好的工具支持)
  3. 增强代码可读性(类型信息显式表达)
  4. 支持类型安全的通用编程(编写一次,多处安全使用)

1.1.1. 泛型出现前的世界

在JDK 5 之前,没有泛型的时代,Java 集合类只能存储 Object 类型:

java 复制代码
// JDK 5 之前的代码(没有泛型)
List list = new ArrayList();
list.add("Hello");
list.add(new Integer(42));
list.add(new Date());

// 取出元素时需要强制转换
String str = (String) list.get(0);        // OK
Integer num = (Integer) list.get(1);      // OK  
String wrong = (String) list.get(2);      // 运行时错误!ClassCastException

**痛点1.类型不安全:**编译器无法检查类型错误,类型错误只能在运行时发现

java 复制代码
public void processNumbers(List numbers) {
    // 开发者期望传入 List<Integer>
    // 但实际可能传入任何类型的 List
    
    for (int i = 0; i < numbers.size(); i++) {
        Integer num = (Integer) numbers.get(i); // 可能抛出 ClassCastException
        System.out.println(num * 2);
    }
}

// 调用者可能犯错
List strings = Arrays.asList("a", "b", "c");
processNumbers(strings); // 编译通过,但运行时崩溃!

痛点2. 强制转换繁琐:每次取值都要强制转换,代码冗余和可读性差

java 复制代码
Map employeeMap = new HashMap();
employeeMap.put("001", new Employee("Alice"));
employeeMap.put("002", new Employee("Bob"));

// 每次使用都要转换
Employee emp1 = (Employee) employeeMap.get("001");
Employee emp2 = (Employee) employeeMap.get("002");
Employee emp3 = (Employee) employeeMap.get("003"); // 如果 key 不存在,返回 null,转换没问题
// 但如果 map 中混入了其他类型...
employeeMap.put("004", "Not an employee");
Employee emp4 = (Employee) employeeMap.get("004"); // 运行时错误!

**痛点3. API 设计困难:**没有泛型时,工具方法很难设计

java 复制代码
public static Object max(Object[] array) {
    // 如何比较?需要额外的 Comparator 参数
    // 返回 Object,调用者需要转换
}

// 调用时
Integer[] numbers = {1, 2, 3};
Object result = max(numbers);
Integer maxNum = (Integer) result; // 必须转换

1.1.2 没有泛型的解决方案

使用 **[继承]**解决类型安全和强制转换问题,但:

  • 需要为每种类型都创建类,代码爆炸!
  • 而且无法处理自定义类型(每个用户都要创建自己的 XxxList)
java 复制代码
// 方案:为每种类型创建专门的集合类
class StringList extends ArrayList {
    public void add(String s) { /* ... */ }
    public String get(int i) { /* ... */ }
}

class IntegerList extends ArrayList {
    public void add(Integer i) { /* ... */ }
    public Integer get(int i) { /* ... */ }
}

使用 **[接口标记]**方式来解决:仍然无法避免强制转换,类型安全问题依然存在

java 复制代码
// 方案:使用标记接口
interface StringContainer {}
class MyList extends ArrayList implements StringContainer {}

1.1.3. 泛型如何解决这些问题?

解决方案一:编译时类型检查。类型错误在编译时就被发现,而不是等到运行时崩溃。

java 复制代码
// 有了泛型后
List<String> stringList = new ArrayList<>();
stringList.add("Hello");
stringList.add("World");
// stringList.add(42); // 编译错误!类型不匹配

String first = stringList.get(0); // 无需强制转换!

**解决方案二:消除强制转换。**代码更简洁,可读性更好,减少样板代码。

java 复制代码
// 泛型集合
Map<String, Employee> employeeMap = new HashMap<>();
employeeMap.put("001", new Employee("Alice"));

Employee emp = employeeMap.get("001"); // 直接获得正确类型,无需转换

解决方案三:类型安全的 API。

java 复制代码
// 泛型工具方法
public static <T extends Comparable<T>> T max(T[] array) {
    if (array.length == 0) return null;
    T max = array[0];
    for (T item : array) {
        if (item.compareTo(max) > 0) {
            max = item;
        }
    }
    return max;
}

// 使用
Integer[] numbers = {1, 2, 3};
Integer maxNum = max(numbers); // 类型安全,无需转换

String[] words = {"apple", "banana", "cherry"};
String maxWord = max(words); // 同样的方法,不同类型

1.1.4 泛型的核心价值总结

维度 没有泛型 有泛型
安全性 运行时才发现类型错误 编译时检查类型安全
代码质量 大量强制转换,容易出错 无强制转换,类型明确
开发效率 需要小心处理类型转换 IDE 提供准确的类型推断和补全
API 设计 接口模糊,文档依赖注释 接口自文档化,类型约束明确
维护成本 类型错误难以追踪 类型错误在编译时暴露

1.2. 泛型的基本语法

1.2.1 泛型类

java 复制代码
// 定义泛型类
public class Box<T> {
    private T value;
    
    public void set(T value) {
        this.value = value;
    }
    
    public T get() {
        return value;
    }
}

// 使用泛型类
Box<String> stringBox = new Box<>();
stringBox.set("Hello");
String str = stringBox.get(); // 无需强制转换

Box<Integer> intBox = new Box<>();
intBox.set(42);
Integer num = intBox.get();

1.2.2 泛型接口

java 复制代码
// 定义泛型接口
public interface Comparable<T> {
    int compareTo(T other);
}

// 实现泛型接口
public class Person implements Comparable<Person> {
    private String name;
    private int age;
    
    @Override
    public int compareTo(Person other) {
        return Integer.compare(this.age, other.age);
    }
}

1.2.3 泛型方法

java 复制代码
// 泛型方法(与类是否泛型无关)
public class GenericUtils {
    // 静态泛型方法
    public static <T> void printArray(T[] array) {
        for (T item : array) {
            System.out.println(item);
        }
    }
    
    // 实例泛型方法
    public <T extends Number> double sum(T[] numbers) {
        double sum = 0;
        for (T num : numbers) {
            sum += num.doubleValue();
        }
        return sum;
    }
}

// 使用泛型方法
String[] names = {"Alice", "Bob", "Charlie"};
GenericUtils.printArray(names);

Integer[] nums = {1, 2, 3, 4, 5};
double total = new GenericUtils().sum(nums);

对比泛型类中的方法,我们可以看到这个例子里的泛型方法返回值前多了泛型参数 <T> 或 <T extends Number>。那什么时候需要加泛型参数呢?

1.2.3.1 需加泛型参数的情况

case1.静态方法 需要声明自己的泛型参数,因为静态方法不能使用类的泛型参数(最常见需要 <T> 的情况)。

java 复制代码
// 非泛型类中的静态泛型方法 - 要加泛型参数
public class Collections {
    
    // ✅ 必须有 <T> - 这是方法自己的泛型参数
    public static <T> void sort(List<T> list) {}
    
    // ✅ 必须有 <T> - 带约束的泛型参数
    public static <T extends Comparable<T>> T max(List<T> list) {}
}
java 复制代码
// 泛型类中的静态泛型方法 - 要加泛型参数
public class Box<T> {
    
    // ✅ 必须有 <E> - 静态方法不能使用类的泛型参数 T
    // 静态方法属于类本身,而不是类的实例
    public static <E> Box<E> createEmptyBox() {
        return new Box<E>();
    }
    
    // ❌ 错误!静态方法不能使用类的泛型参数
    // public static Box<T> createEmptyBox() { } // 编译错误
}

case2. 泛型类的实例方法 声明新的泛型参数,新的泛型参数必须声明

java 复制代码
public class MyList<T> {  // 类声明了泛型参数 T
    
    // 不需要 <T> - 使用类的泛型参数
    public void add(T item) {}
    
    // 不需要 <T> - 使用类的泛型参数  
    public T get(int index) { return null; }
    
    // ❌ 错误!不能重复声明 T
    // public <T> void badMethod(T item) {} // 编译错误:重复的类型参数
    
    // ✅ 声明新的泛型参数 E(用不同名字)
    public <E> void copyTo(MyList<E> other) {}
}

case2.非泛型类的方法声明泛型参数(较少见但合法)

java 复制代码
public class Processor {
    
    // ✅ 非泛型类中的实例泛型方法
    public <T> void process(List<T> items) {
        // T 是这个方法特有的泛型参数
    }
    
    // ✅ 可以同时使用多个泛型参数
    public <T, U> Map<T, U> zip(List<T> keys, List<U> values) {
        return null;
    }
}
1.2.3.1 不需加泛型参数的情况

泛型类的实例方法使用类的泛型参数,泛型参数已在类级别声明过了,无需再次声明。

java 复制代码
public class Box<T> {  // ← T 是在类定义时声明的
    
    public void set(T value) {}        // 使用类的泛型参数 T
    public T get() { return value; }   // 使用类的泛型参数 T
}

当方法使用的是类上定义的泛型参数 时,不需要在方法返回值前加 <T>

  • T 已经在类定义 Box<T> 中声明过了
  • 这些方法直接使用类的泛型参数
  • 编译器知道 T 指的是什么

1.3. 类型参数命名约定

字母 含义 示例
T Type List<T>
E Element ArrayList<E>
K Key Map<K, V>
V Value Map<K, V>
N Number <N extends Number>
S, U, V 第二、第三、第四类型 <T, S extends T>

2. 泛型的类型约束

2.1 上界通配符extends

上界通配符(Upper Bounded Wildcards):使用 extends 关键字限制类型参数的上限。

2.1.1 用法

java 复制代码
// 只能接受 Number 及其子类
public static void processNumbers(List<? extends Number> list) {
    for (Number num : list) {
        System.out.println(num.doubleValue());
    }
    // list.add(10); // 编译错误!不能添加元素; 写入是不安全操作,故编译器禁止
}

// 使用示例
List<Integer> integers = Arrays.asList(1, 2, 3);
List<Double> doubles = Arrays.asList(1.0, 2.0, 3.0);
processNumbers(integers); // OK
processNumbers(doubles);  // OK

2.1.2 上界通配符:可读不可写

方法参数类型使用上界通配符后**,** 可读不可写。 例子中,方法参数类型为List<? extends Number>,参数 list 可以获取元素,不能添加元素。记忆:extends --> 出口,用于输出。

List<? extends Number> list,编译器只知道 list 中的元素是 Number 的某个子类型,但不知道具体是哪个子类型:

java 复制代码
List<? extends Number> list = new ArrayList<Integer>();
// 或者
List<? extends Number> list = new ArrayList<Double>();
// 或者  
List<? extends Number> list = new ArrayList<Float>();

为什么可以读取?

  • 无论 list 实际存储的是 IntegerDouble 还是 Float,它们都是 Number 的子类,所以赋值给 Number 类型变量总是安全的。
java 复制代码
Number num = list.get(0); // ✅ 安全!

为什么不能写入?

  • 编译器不知道这个列表到底是什么类型的,可能是 Integer 列表,也可能是 Double 列表。如果允许你添加任何 Number 的子类,就可能破坏列表的类型一致性。为了安全起见,什么都不让你添加。
  • 假设 list 实际是 ArrayList<Integer>,如果允许 list.add(new Double(3.14)),就会把 Double 放进 Integer 列表中,这违反了类型安全。
java 复制代码
list.add(new Integer(42));    // ❌ 编译错误!
list.add(new Double(3.14));   // ❌ 编译错误!
list.add(new Number());       // ❌ 编译错误!(这个例子中Number是抽象类,但即使不是抽象类也会报错)

2.2 下界通配符super

下界通配符(Lower Bounded Wildcards):使用 super 关键字限制类型参数的下限。

2.2.1 用法

java 复制代码
// 只能接受 Integer 及其父类
public static void addNumbers(List<? super Integer> list) {
    list.add(10);    // OK - 可以添加 Integer
    list.add(20);    // OK
    // Integer num = list.get(0); // 编译错误!只能获取 Object
}

// 使用示例
List<Integer> integers = new ArrayList<>();
List<Number> numbers = new ArrayList<>();
List<Object> objects = new ArrayList<>();

addNumbers(integers); // OK
addNumbers(numbers);  // OK  
addNumbers(objects);  // OK

2.2.1 下界通配符:可写不可读

List<? super Integer> list,编译器只知道 list 中的元素类型是 Integer 的某个父类型,但不知道具体是哪个父类型。 记忆:super -->入口,用于输入。

java 复制代码
List<? super Integer> list = new ArrayList<Integer>();
// 或者
List<? super Integer> list = new ArrayList<Number>();
// 或者
List<? super Integer> list = new ArrayList<Object>();

为什么可以写入?

  • 无论 list 实际是 Integer 列表、Number 列表还是 Object 列表,Integer 都可以安全地赋值给这些类型(因为 Integer 是它们的子类)。
java 复制代码
list.add(new Integer(42)); // ✅ 安全!
list.add(100);             // ✅ 自动装箱,也是 Integer

为什么不能安全读取?

  • 编译器知道这个列表可以接受 Integer,但它可能是一个 Object 列表,里面可能有各种类型的对象。只能保证取出的对象是 Object 类型,更具体的类型无法确定
  • 假设 list 实际是 ArrayList<Object>,里面可能存储了 StringDate 等任意对象。如果允许 Integer num = list.get(0),而实际取出的是 String,就会导致类型错误。编译器无法保证取出的元素一定是 IntegerNumber
java 复制代码
Integer num = list.get(0); // ❌ 编译错误!
Number num = list.get(0);  // ❌ 编译错误!
Object obj = list.get(0);  // ✅ 只能作为 Object 读取

2.3 无界通配符?

无界通配符(Unbounded Wildcards):使用 <?> 表示未知类型。

java 复制代码
// 接受任何类型的 List
public static void printListSize(List<?> list) {
    System.out.println("Size: " + list.size());
    // Object obj = list.get(0); // OK,但只能作为 Object 处理
    // list.add("hello"); // 编译错误!
}

// 使用示例
List<String> strings = Arrays.asList("a", "b");
List<Integer> integers = Arrays.asList(1, 2);
printListSize(strings);  // OK
printListSize(integers); // OK

2.4 PECS 原则

PECS 原则(Producer Extends, Consumer Super)是理解和使用通配符的关键原则:

  • Producer Extends :如果你需要从集合中读取 数据(生产者),使用 <? extends T>
  • Consumer Super :如果你需要向集合中写入 数据(消费者),使用 <? super T>
java 复制代码
// 经典例子:Collections.copy()
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
    // dest 是消费者(写入),所以用 super
    // src 是生产者(读取),所以用 extends
    for (int i = 0; i < src.size(); i++) {
        dest.set(i, src.get(i));
    }
}

// 使用示例
List<Integer> src = Arrays.asList(1, 2, 3);
List<Number> dest = new ArrayList<>(Arrays.asList(0.0, 0.0, 0.0));
Collections.copy(dest, src); // 完美工作!

为什么这样设计是安全的?

  • src.get(i) 返回 ? extends T,可以安全赋值给 T
  • dest.set(i, T) 接受 T,而 dest? super T,可以安全接受 T

总结: 这种设计看似限制了灵活性,但实际上保证了类型安全,这正是泛型存在的根本目的

通配符类型 可以做什么 为什么
<? extends T> 读取T 类型 所有元素都是 T 的子类,向上转型安全
<? extends T> 不能写入 不知道具体类型,写入可能破坏类型安全
<? super T> 写入 T 类型 T 可以向上转型为任何父类型
<? super T> 只能读取Object 不知道具体父类型,无法确定更具体的类型

3.泛型的类型擦除

3.1 什么是类型擦除?

Java 泛型是通过类型擦除实现的,这意味着:

  • 泛型信息只在编译时存在
  • 运行时所有的泛型类型都被擦除为原始类型(Raw Type)
java 复制代码
// 编译后,这两个 List 在运行时是相同的类型,均为原始类型List
List<String> stringList = new ArrayList<>();
List<Integer> integerList = new ArrayList<>();

// 运行时类型检查
System.out.println(stringList.getClass() == integerList.getClass()); // true

3.2 类型擦除的影响

3.2.1 影响1:无法进行 instanceof 检查

java 复制代码
// 编译错误!
if (obj instanceof List<String>) { }

// 只能检查原始类型
if (obj instanceof List) { }

3.2.2 影响2:无法创建泛型数组

java 复制代码
// 编译错误!
T[] array = new T[size]; // 不允许

// 解决方案:使用反射或 Object 数组
@SuppressWarnings("unchecked")
T[] array = (T[]) new Object[size];

3.2.3 影响3:无法抛出或捕获泛型异常

java 复制代码
// 编译错误!
class GenericException<T> extends Exception { }

// 但可以这样:
class MyException extends Exception { }

4. 泛型的实际应用示例

4.1 自定义泛型容器

java 复制代码
public class Stack<T> {
    private List<T> elements = new ArrayList<>();
    
    public void push(T item) {
        elements.add(item);
    }
    
    public T pop() {
        if (elements.isEmpty()) {
            throw new IllegalStateException("Stack is empty");
        }
        return elements.remove(elements.size() - 1);
    }
    
    public boolean isEmpty() {
        return elements.isEmpty();
    }
}

// 使用
Stack<String> stringStack = new Stack<>();
stringStack.push("Hello");
stringStack.push("World");
String top = stringStack.pop(); // "World"

4.2 泛型工具方法

java 复制代码
public class CollectionUtils {
    
    // 查找列表中的最大值
    public static <T extends Comparable<T>> T max(List<T> list) {
        if (list.isEmpty()) {
            throw new IllegalArgumentException("List is empty");
        }
        
        T max = list.get(0);
        for (T item : list) {
            if (item.compareTo(max) > 0) {
                max = item;
            }
        }
        return max;
    }
    
    // 合并两个列表
    public static <T> List<T> merge(List<T> list1, List<T> list2) {
        List<T> result = new ArrayList<>(list1);
        result.addAll(list2);
        return result;
    }
}

// 使用
List<Integer> numbers = Arrays.asList(3, 1, 4, 1, 5);
Integer maxNum = CollectionUtils.max(numbers);

List<String> list1 = Arrays.asList("A", "B");
List<String> list2 = Arrays.asList("C", "D");
List<String> merged = CollectionUtils.merge(list1, list2);

4.3 泛型与继承

java 复制代码
// 泛型类的继承
class Animal {}
class Dog extends Animal {}
class Cat extends Animal {}

// 注意:List<Dog> 不是 List<Animal> 的子类型!
public void processAnimals(List<Animal> animals) {
    // 不能传入 List<Dog>
}

// 正确的方式:使用通配符
public void processAnimals(List<? extends Animal> animals) {
    // 现在可以传入 List<Dog> 或 List<Cat>
    for (Animal animal : animals) {
        // 处理动物
    }
}

4.4 最佳实践

**原始类型 vs 泛型类型:**尽可能使用泛型而不是原始类型

java 复制代码
// 危险!使用原始类型
List rawList = new ArrayList();
rawList.add("Hello");
rawList.add(42); // 编译器不会检查!

// 安全!使用泛型
List<String> stringList = new ArrayList<>();
stringList.add("Hello");
// stringList.add(42); // 编译错误!

泛型数组创建

java 复制代码
// 错误方式
public static <T> T[] toArray(List<T> list) {
    // T[] array = new T[list.size()]; // 编译错误
    return array;
}

// 正确方式
public static <T> T[] toArray(List<T> list, T[] a) {
    return list.toArray(a);
}
相关推荐
程序媛徐师姐2 小时前
Java基于SSM的即时空教室查询小程序,附源码+文档说明
java·微信小程序·小程序·ssm·即时空教室查询小程序·java即时空教室查询小程序·即时空教室查询微信小程序
努力长头发的程序猿2 小时前
在Unity当中使用GameFrameworkX框架的知识点
java·unity·游戏引擎
飞Link2 小时前
告别复杂调参:Prophet 加法模型深度解析与实战
开发语言·python·数据挖掘
季明洵2 小时前
二叉树的最小深度、完全二叉树的节点个数、平衡二叉树、路径总和、从中序与后序遍历序列构造二叉树
java·数据结构·算法·leetcode·二叉树
zh_xuan2 小时前
测试go语言函数和结构体
开发语言·golang
AD钙奶-lalala2 小时前
SpringBoot 4.0.3配置Swagger
java·spring boot·后端
小龙报3 小时前
【算法通关指南:算法基础篇】二分算法: 1.A-B 数对 2.烦恼的高考志愿
c语言·开发语言·数据结构·c++·vscode·算法·二分
seven97_top3 小时前
NIO:解开非阻塞I/O高并发编程的秘密
java
小六溜了3 小时前
模块二十.双列集合
java