第18章 泛型入门

第18章 泛型入门

集合里能装任何类型的对象,但取出时要强转,容易出错。泛型(Generics)解决了"类型安全"问题:在编译期就限定集合装什么类型。本节掌握泛型的基本用法。

一、为什么需要泛型

不使用泛型的集合:可以放任何对象,取出必须强转,运行期才可能报错

java 复制代码
List list = new ArrayList();
list.add("Java");
list.add(123);            // 什么都能放

String s = (String) list.get(0);   // 要强转
String t = (String) list.get(1);   // 运行期报错:ClassCastException!
// Integer 强转 String 编译期发现不了

使用泛型的集合:编译期就限定类型,编译报错而不是运行崩溃:

java 复制代码
List<String> list = new ArrayList<>();
list.add("Java");
// list.add(123);          // 编译错误:String 列表不能放 Integer

String s = list.get(0);    // 不用强转,自动就是 String

泛型也不只用在集合上:Map<String, Integer> 取出来就是 Integer,不用猜类型、不用强转,类型一目了然。

泛型的好处:

  1. 编译期类型检查,把错误提前
  2. 省去强转代码,更简洁
  3. 代码可复用:一套代码处理多种类型

二、泛型类

自己定义带类型的类:

java 复制代码
// T 是类型参数,可以是任意标识(惯例用 T、E、K、V)
public class Box<T> {
    private T value;

    public void set(T value) {
        this.value = value;
    }

    public T get() {
        return value;
    }
}
java 复制代码
Box<String> strBox = new Box<>();
strBox.set("hello");
String s = strBox.get();          // 自动是 String

Box<Integer> intBox = new Box<>();
intBox.set(100);
int n = intBox.get();             // 自动是 Integer

细心的你可能注意到 new Box<>() 只写了一次类型,后面的空尖括号叫钻石操作符 (diamond)。这个写法在 JDK 8、11、17 三个版本里都完全一样,不用担心差异;老项目里也常看到 new Box<String>() 这种写全类型的版本。

同一个类,不同类型参数就是不同类型,但它们内部都是同一个类 (泛型的擦除机制,第七节细讲)。一个类可以有多个类型参数,如 Pair<K, V>(两元组),写法与 Box 相同。

三、泛型方法

方法级别使用泛型,类型参数写在返回值前面:

java 复制代码
public class Utils {
    // 泛型方法:打印数组,任何类型都能用
    public static <T> void printArray(T[] arr) {
        for (T item : arr) {
            System.out.print(item + " ");
        }
        System.out.println();
    }
}
java 复制代码
String[] names = {"张三", "李四"};
Integer[] nums = {1, 2, 3};

Utils.printArray(names);   // 张三 李四
Utils.printArray(nums);    // 1 2 3

调用时类型由实参自动推断,不用显式指定。再看一个更实用的例子------交换数组中的两个元素:

java 复制代码
public class Utils2 {
    public static <T> void swap(T[] arr, int i, int j) {
        T temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }
}
java 复制代码
String[] words = {"甲", "乙", "丙"};
Utils2.swap(words, 0, 2);                     // 类型由实参推断
System.out.println(Arrays.toString(words));   // [丙, 乙, 甲]

Utils2.<String>swap(words, 0, 1);             // 也可以显式指定(一般不用写)

四、泛型接口

java 复制代码
// 泛型接口
interface Generator<T> {
    T next();
}

// 实现类指定具体类型
class NumberGenerator implements Generator<Integer> {
    @Override
    public Integer next() {
        return (int) (Math.random() * 100);
    }
}
java 复制代码
Generator<Integer> gen = new NumberGenerator();
System.out.println(gen.next());   // 随机数

实现类也可以继续保留泛型 ,由使用方决定具体类型,写法就是 class MyGenerator<T> implements Generator<T>

五、类型通配符 ?

通配符用于方法参数,解决"泛型类型之间的继承"问题:

java 复制代码
// 只能接收 Box<Number>
static void printNum(Box<Number> box) { ... }

// 可以接收 Box<Integer>、Box<Double> 等所有 Number 子类
static void printAny(Box<? extends Number> box) { ... }
java 复制代码
Box<Integer> intBox = new Box<>();
intBox.set(100);

// printNum(intBox);      // 编译错误!Box<Integer> 不是 Box<Number>
printAny(intBox);         // 正确:? extends Number

三个通配符:

写法 含义
<?> 任意类型
<? extends T> T 或 T 的子类(上限)
<? super T> T 或 T 的父类(下限)

上限通配符的应用

java 复制代码
// 计算任意数字集合的和
static double sum(List<? extends Number> nums) {
    double total = 0;
    for (Number n : nums) {
        total += n.doubleValue();
    }
    return total;
}
java 复制代码
sum(List.of(1, 2, 3));               // List<Integer>:6.0
sum(List.of(1.5, 2.5));              // List<Double>:4.0

上面例子里的 List.of 是 JDK 11 和 17 的快捷写法(需 JDK 11/17 才能运行),JDK 8 里用 Arrays.asList(1, 2, 3) 效果一样。

下限通配符的应用

? super T 常用于"往里写"的场景:

java 复制代码
// 接收所有能装 Integer 的集合:List<Integer>、List<Number>、List<Object> 都行
static void addOne(List<? super Integer> list) {
    list.add(1);              // 可以安全地写入 Integer
}

List<Number> numbers = new ArrayList<>();
addOne(numbers);
System.out.println(numbers);   // [1]

注意读写限制:? extends 只能读不能写(null 除外),? super 只能写、读出来只能当 Object(易错点 4 有完整对照)。

六、泛型与继承的注意点(高频考点)

java 复制代码
Box<Integer> intBox = new Box<>();
Box<Number> numBox;

// numBox = intBox;   // 编译错误!Box<Integer> 与 Box<Number> 没有继承关系

即使 Integer extends NumberBox<Integer> 也不是 Box<Number> 的子类 ------泛型参数之间没有继承关系,所以才需要 ? extends

对比数组的协变------恰恰相反,能编译通过,代价是运行期才报错:

java 复制代码
String[] strs = new String[3];
Object[] objs = strs;        // ✅ 数组协变:String[] 是 Object[] 的子类
objs[0] = 123;               // 运行期才报错:ArrayStoreException

List<String> list = new ArrayList<>();
// List<Object> objs2 = list;   // ❌ 编译错误:泛型不变

七、泛型的擦除(了解原理)

Java 泛型是编译期机制:编译后类型参数被擦除(替换为 Object 或边界类型),生成的字节码和普通类一样。

java 复制代码
List<String> a = new ArrayList<>();
List<Integer> b = new ArrayList<>();
System.out.println(a.getClass() == b.getClass());   // true:运行时都是 ArrayList

所以:泛型信息在运行期不存在,不能 new T()、不能 T\[\] 数组直接创建、不能 instanceof T

java 复制代码
// 以下写法全部编译错误(擦除后 T 在运行期没有具体类型):
// T obj = new T();           // ❌ 不能 new 类型参数
// T[] arr = new T[10];       // ❌ 不能直接创建泛型数组
// if (x instanceof T) { }    // ❌ 不能 instanceof 类型参数

有边界时擦除成边界类型(如 <T extends Number> 擦除成 Number)。

八、扩展知识

1. 擦除带来的"重载冲突"

既然 List<String>List<Integer> 运行时都是 List,它们不能构成方法重载

java 复制代码
// ❌ 编译错误:擦除后签名都是 print(List),冲突!
// void print(List<String> list) { }
// void print(List<Integer> list) { }
// ✅ 改方法名即可:printString / printInt

同理,泛型类不能继承 Throwable(异常机制在运行期工作,而运行期没有泛型)。

2. 通配符进阶:PECS 原则

什么时候用 ? extends、什么时候用 ? super?记住口诀 PECS:Producer Extends, Consumer Super(生产者用 extends,消费者用 super)。

  • 方法只读取 元素 → 参数用 ? extends T(生产者)
  • 方法只写入 元素 → 参数用 ? super T(消费者)

经典例子------把 src 全部拷到 dest:

java 复制代码
// src 只读(生产者)用 extends,dest 只写(消费者)用 super
static <T> void copy(List<? extends T> src, List<? super T> dest) {
    for (T item : src) {
        dest.add(item);
    }
}
java 复制代码
List<Integer> src = Arrays.asList(1, 2, 3);        // JDK 8 写法(JDK 11/17 可用 List.of)
List<Number> dest = new ArrayList<>();
copy(src, dest);
System.out.println(dest);   // [1, 2, 3]

copy 能同时接收 List<Integer>List<Number>,读写都安全------这就是通配符的意义。

3. 泛型方法 vs 泛型类,怎么选

  • 泛型类 :整个类的多个字段/方法共用同一个 T,比如 Box<T>Pair<K, V>
  • 泛型方法 :只有这一个方法用泛型。static 方法拿不到类的泛型,只能用泛型方法
java 复制代码
class Box<T> {
    private T value;

    // ❌ static 成员不能用类上的泛型
    // static T count;            // 编译错误
    // static void show(T v) { }  // 编译错误

    // ✅ 静态方法用"自己的"泛型(换个字母避免混淆)
    static <E> void show(E v) {
        System.out.println(v);
    }
}

类型参数可有多个上限,用 & 连接,如 <T extends Number & Comparable<T>>

九、易错点

1. 基本类型不能做类型参数

java 复制代码
// ❌ List<int> list = new ArrayList<>();   // 编译错误
// ✅ 用包装类:自动装箱/拆箱
List<Integer> list = new ArrayList<>();
list.add(10);               // int → Integer(装箱)
int x = list.get(0);        // Integer → int(拆箱)
System.out.println(x);      // 10

包装类对照:int→Integer、long→Long、double→Double、boolean→Boolean、char→Character。

2. 泛型数组创建报错

java 复制代码
// ❌ 以下都编译错误(Generic array creation)
// List<String>[] arr = new List<String>[10];
// T[] arr = new T[10];

// ✅ 用 List 代替数组
List<List<String>> list = new ArrayList<>();
list.add(new ArrayList<>());
System.out.println(list.size());   // 1

3. 静态成员不能用类泛型

java 复制代码
class Box<T> {
    private T value;             // ✅ 实例字段可用类泛型

    // ❌ static T count;        // 编译错误
    // ❌ static void f(T v) {}  // 编译错误:静态成员不能用类泛型

    // ✅ 静态方法要自己声明泛型
}

4. 通配符的读写限制搞反

java 复制代码
List<? extends Number> nums = new ArrayList<Integer>();
// ❌ nums.add(1);            // 编译错误:? extends 只能读不能写(null 除外)
Number n = nums.get(0);       // ✅ 读出来当 Number

List<? super Integer> ints = new ArrayList<Number>();
ints.add(1);                  // ✅ 可以写 Integer
// ❌ Integer x = ints.get(0);  // 编译错误:? super 读出来只能当 Object
Object o = ints.get(0);       // ✅

记住:extends 偏读(上限),super 偏写(下限)

5. instanceof 不能用于泛型类型

java 复制代码
List<String> list = new ArrayList<>();

// ❌ list instanceof List<String>   // 编译错误:运行期没有泛型
// ✅ 用无界通配符
if (list instanceof List<?>) {
    System.out.println("是一个 List");    // 是一个 List
}

十、小结

  • 泛型限定集合/类的类型,编译期检查,免强转
  • 泛型类 class Box<T>、泛型方法 <T> 返回类型 方法名、泛型接口
  • ? extends T 上限(偏读),? super T 下限(偏写),PECS 记读写方向
  • Box<Integer> 不是 Box<Number> 的子类,数组协变但泛型不变
  • 泛型运行期擦除:不能 new T()、不能建泛型数组、不能 instanceof T
  • 基本类型用包装类;static 成员不能用类泛型,要用泛型方法

下一节学习 IO 流。

下一篇:第19章 IO 流

相关推荐
我命由我123451 小时前
人脸识别 - 判断人脸是否在画面中央
java·人工智能·python·java-ee·人脸识别·android jetpack·android runtime
程序猿乐锅1 小时前
从 dsh 源码看「一切皆插件」与 Spring IoC
java·网络·数据库·人工智能·后端·spring
LINgZone21 小时前
Maven多模块依赖管理与私有JAR包策略
java·maven·jar
珍珠先生1 小时前
第17章 集合框架:Map
java
伯远医学1 小时前
RIP-seq 高分文献案例分享
java·前端·javascript·人工智能·算法·eclipse·html
吃杠碰小鸡1 小时前
VsCode中使用Java
java·vscode
lq_bog1 小时前
spring事务失效
java·数据库·spring
API快乐传递者1 小时前
淘宝京东数据分析实战指南:从指标体系到决策落地的全链路方法论
java·大数据·数据分析
vancece1 小时前
JS实现堆 & 215.数组中的第K个最大元素
java·前端·javascript