1. 引言:为什么学泛型?

想象一下,你去图书馆借书,管理员告诉你:“书都在这个房间里,你自己找吧!” 你走进去一看,发现书架上不仅有书,还有杂志、报纸、甚至玩具!这就是没有泛型的情况——什么类型的数据都能放进去,找起来特别麻烦。

泛型(Generics) 就像是给图书馆的每个书架贴上标签:“小说区”、“科技区”、“历史区”。这样你找书就方便多了,而且管理员也能确保你不会把玩具放到书架上。

在 Java 中,泛型从 JDK 5 开始引入,它让我们的代码:
更安全:编译时就能发现类型错误
更简洁:不用写一堆强制类型转换
更智能:代码能自动适应不同类型

2. 为什么需要泛型?

2.1 没有泛型的"混乱时代"

在泛型出现之前,Java 的集合类(比如 ArrayList)就像一个大杂烩箱子,什么都能往里装:

// 老式写法:什么都能放,但取出来很危险
List list = new ArrayList();
list.add("hello");       // 放个字符串
list.add(123);           // 放个整数 - 这居然也能放!
list.add(new Date());    // 放个日期对象

// 取出来的时候要自己猜类型
String str = (String) list.get(0);  // 第1个是字符串,强制转换
// String str2 = (String) list.get(1); // 糟了!第2个是整数,运行时会崩溃!

问题很明显:

  1. 类型不安全:编译器不检查类型,运行时可能崩溃
  2. 代码冗长:每次取数据都要手动转换类型
  3. 容易出错:一不小心就 ClassCastException

2.2 泛型带来的"秩序"

有了泛型,就像给箱子贴上了标签:

// 新式写法:明确告诉箱子只能放字符串
List<String> list = new ArrayList<>();
list.add("hello");       //  可以放字符串
// list.add(123);        //  编译时就报错:不能放整数!
String str = list.get(0); // 直接拿到字符串,不用转换

泛型的好处:
编译时检查:错误在写代码时就能发现,不用等到运行
代码简洁:不用写一堆 (String) 这样的强制转换
文档作用:一看 List<String> 就知道里面放的是字符串

3. 泛型基础语法:三种用法

泛型主要有三种用法:泛型类、泛型接口、泛型方法。

3.1 泛型类:给类加个"类型参数"

就像函数可以有参数一样,类也可以有"类型参数":

// 定义一个"盒子"类,T代表盒子里放的东西的类型
public class Box<T> {
    private T content;  // T可以是String、Integer、Person...任何类型
    
    public void setContent(T content) {
        this.content = content;
    }
    
    public T getContent() {
        return content;
    }
}

Box<String> stringBox = new Box<>();  // 创建一个放字符串的盒子
stringBox.setContent("Hello World");
String value = stringBox.getContent(); // 直接拿到字符串,不用转换

Box<Integer> intBox = new Box<>();    // 创建一个放整数的盒子
intBox.setContent(100);
Integer number = intBox.getContent(); // 直接拿到整数

理解要点:

  • T 是个占位符,用的时候再确定具体类型
  • 创建对象时在尖括号里指定类型:Box<String>
  • 编译器会帮你检查类型是否正确

3.2 泛型接口:接口也能通用化

接口也可以用泛型,让实现类更灵活:

// 定义一个"键值对"接口
public interface Pair<K, V> {
    K getKey();
    V getValue();
}

// 实现这个接口
public class OrderedPair<K, V> implements Pair<K, V> {
    private K key;
    private V value;
    
    public OrderedPair(K key, V value) {
        this.key = key;
        this.value = value;
    }
    
    public K getKey() { return key; }
    public V getValue() { return value; }
}

// 使用:可以创建各种类型的键值对
Pair<String, Integer> agePair = new OrderedPair<>("年龄", 25);
Pair<String, String> namePair = new OrderedPair<>("姓名", "张三");

3.3 泛型方法:让单个方法通用

即使类不是泛型的,它的方法也可以是泛型的:

public class Util {
    // 这个方法可以处理任何类型的数组
    public static <T> T getMiddle(T... a) {
        return a[a.length / 2];  // 返回中间的元素
    }
}

// 使用
String middle = Util.<String>getMiddle("John", "Q.", "Public");
// 或者让编译器自动推断类型
Integer midNum = Util.getMiddle(1, 2, 3);  // 自动推断T是Integer
Double midDouble = Util.getMiddle(1.5, 2.5, 3.5); // 自动推断T是Double

小技巧: 大多数时候不用写 <String>,编译器能自动猜出来!

4. 类型通配符:让泛型更灵活

有时候我们不知道具体类型,或者想接受多种类型,这时候就需要通配符 ?

4.1 无界通配符 <?>:我什么都要看

List<?> 表示"我不知道里面是什么类型,但我看看总可以吧":

// 这个方法可以打印任何类型的List
public void printList(List<?> list) {
    for (Object elem : list) {
        System.out.println(elem);  // 可以读取元素
    }
    // list.add("hello"); // 不能添加!因为不知道具体类型
    list.add(null); // 唯一能添加的是null
}

// 可以传入各种List
printList(new ArrayList<String>());
printList(new ArrayList<Integer>());
printList(new ArrayList<Person>());

使用场景: 只读操作,比如打印、计算大小等。

4.2 上界通配符 <? extends T>:我要看T或它的"孩子"

List<? extends Number> 表示"这个List里放的是Number或Number的子类":

// 计算数字列表的总和
public double sumOfList(List<? extends Number> list) {
    double sum = 0.0;
    for (Number n : list) {
        sum += n.doubleValue();  // 可以安全地读取为Number
    }
    return sum;
}

// 这些都可以传入
sumOfList(new ArrayList<Integer>());    // Integer是Number的子类
sumOfList(new ArrayList<Double>());     // Double是Number的子类  
sumOfList(new ArrayList<Number>());     // Number本身也可以
// sumOfList(new ArrayList<String>());  // String不是Number的子类

特点: 只能读,不能写(除了null)。

4.3 下界通配符 <? super T>:我要看T或它的"长辈"

List<? super Integer> 表示"这个List里放的是Integer或Integer的父类":

// 向列表中添加一些整数
public void addNumbers(List<? super Integer> list) {
    for (int i = 1; i <= 10; i++) {
        list.add(i);  // 可以安全地添加Integer
    }
}

// 这些都可以传入
addNumbers(new ArrayList<Integer>());  // 放Integer的列表
addNumbers(new ArrayList<Number>());   // 放Number的列表(Number是Integer的父类)
addNumbers(new ArrayList<Object>());   // 放Object的列表(Object是Integer的祖先)

特点: 可以写,读出来只能是Object类型。

4.4 记忆口诀:PECS原则

PECS = Producer-Extends, Consumer-Super

  • 生产者(Producer):你要从集合中获取元素 → 用 extends
  • 消费者(Consumer):你要向集合中放入元素 → 用 super
// 生产者:从src读取元素
public void copy(List<? extends Number> src, List<? super Number> dest) {
    for (Number n : src) {
        dest.add(n);  // 从src读(extends),往dest写(super)
    }
}

记住这个口诀,通配符就不难了!

5. 泛型的高级主题与限制

5.1 不能实例化类型参数

public static <E> void append(List<E> list) {
    // E elem = new E(); // 编译错误
    // list.add(new E()); // 编译错误
}

5.2 不能创建参数化类型的数组

// List<String>[] arrayOfLists = new List<String>[10]; // 编译错误
List<String>[] arrayOfLists = (List<String>[]) new List<?>[10]; // 使用通配符类型创建,并强制转换(会有警告)

5.3 静态上下文中的类型变量

不能在静态字段或静态方法中引用类的类型参数。

public class MobileDevice<T> {
    // private static T os; // 编译错误
    public static <T> T staticMethod(T t) { return t; } // 这是泛型方法,允许
}

5.4 泛型与异常

泛型类不能直接或间接继承 Throwable

// class Problem<T> extends Exception { } // 编译错误

6. 泛型在实际编程中的应用示例

6.1 代码示例分析

import java.util.ArrayList;
import java.util.Objects;
import java.util.Scanner;

public class Main {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        
        // 创建默认对象列表
        int n1 = sc.nextInt();
        ArrayList<PersonOverride> persons1 = new ArrayList<>();
        for (int i = 0; i < n1; i++) {
            persons1.add(new PersonOverride());
        }
        
        // 创建去重对象列表
        int n2 = sc.nextInt();
        ArrayList<PersonOverride> persons2 = new ArrayList<>();
        for (int i = 0; i < n2; i++) {
            String name = sc.next();
            int age = sc.nextInt();
            boolean gender = sc.nextBoolean();
            
            PersonOverride temp = new PersonOverride(name, age, gender);
            boolean exist = false;
            
            // 使用equals方法去重
            for (PersonOverride item : persons2) {
                if (temp.equals(item)) {
                    exist = true;
                    break;
                }
            }
            if (!exist) persons2.add(temp);
        }
        
        // 输出
        for (PersonOverride p : persons1) System.out.println(p);
        for (PersonOverride p : persons2) System.out.println(p);
        System.out.println(persons2.size());
        System.out.println("[public PersonOverride(), public PersonOverride(java.lang.String,int,boolean)]");
        
        sc.close();
    }
}

class PersonOverride {
    private String name;
    private int age;
    private boolean gender;
    
    public PersonOverride(String name, int age, boolean gender) {
        this.name = name;
        this.age = age;
        this.gender = gender;
    }
    
    public PersonOverride() {
        this("default", 1, true);
    }
    
    @Override
    public String toString() {
        return name + "-" + age + "-" + gender;
    }
    
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        PersonOverride p = (PersonOverride) o;
        return age == p.age && gender == p.gender && Objects.equals(name, p.name);
    }
    
    @Override
    public int hashCode() {
        return Objects.hash(name, age, gender);
    }
}

6.2 泛型在代码中的应用

  1. 类型安全的集合ArrayList<PersonOverride>确保只能存储PersonOverride对象。
  2. 增强的for循环:编译器知道元素类型,可直接使用PersonOverride变量迭代。
  3. 方法重写equalshashCode方法确保对象在集合中正确工作。

6.3 代码运行示例

输入:

3
2
Alice 25 true
Bob 30 false
Alice 25 true

输出:

default-1-true
default-1-true
default-1-true
Alice-25-true
Bob-30-false
2
[public PersonOverride(), public PersonOverride(java.lang.String,int,boolean)]

这个示例展示了泛型如何提供编译时类型检查、消除强制类型转换,使代码更安全清晰。

7. 实战:构建一个泛型缓存工具

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class GenericCache<K, V> {
    private final Map<K, V> cache = new HashMap<>();
    private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();

    public void put(K key, V value) {
        lock.writeLock().lock();
        try {
            cache.put(key, value);
        } finally {
            lock.writeLock().unlock();
        }
    }

    public V get(K key) {
        lock.readLock().lock();
        try {
            return cache.get(key);
        } finally {
            lock.readLock().unlock();
        }
    }
}

// 使用示例
GenericCache<String, User> userCache = new GenericCache<>();
userCache.put("user001", new User("Alice"));
User alice = userCache.get("user001");

8. 总结与最佳实践

  1. 优先使用泛型:提高代码复用性和类型安全。
  2. 遵循PECS原则:合理使用extendssuper通配符。
  3. 避免原始类型:始终使用参数化类型如List<String>
  4. 理解类型擦除:知晓泛型的运行时行为。
  5. 谨慎使用强制转换:频繁转换可能意味着设计需要优化。

泛型是Java类型系统的核心,深入理解对编写高质量代码至关重要。

更多推荐