☕ 第八章 泛型
📄 一句话概括

8.1 认过泛型方法的脸(<T> 写在返回类型前);这节讲透三件新货:类型推断(调用时不用写尖括号)、静态泛型方法(8.2"static 用不了 T"那颗雷的官方正解)、泛型类 vs 泛型方法怎么选——学完你会发现 JDK 工具类清一色全是这套。

1. 先认门:方法私有的占位符

泛型类里的 T 是全类共用的占位符;泛型方法的 T 是只归这一个方法的临时占位符——声明位置在返回类型之前:

修饰符 <T> 返回类型 方法名(参数列表) {
    // 方法体里 T 随便用
}

// 三个岗位一起看:
public static <T> T pick(T[] arr) {   // ①<T>声明  ②返回值  ③参数
    return arr[new Random().nextInt(arr.length)];
}

规矩没有新的,就是 8.1 的组合:T 只能当引用类型用;new T() 不行;T[] 能收但别自己造。

2. 类型推断:调用时不用写尖括号

泛型方法最大的福利是编译器帮你填空——从实参的类型反推 T 是谁:

String[] names  = {"波仔", "墨池", "AI"};
Integer[] scores = {88, 95, 76};

String s = pick(names);      // 实参 String[] → 推断 T = String,返回 String
Integer i = pick(scores);    // 实参 Integer[] → 推断 T = Integer

// 也能显式指定(语法怪:尖括号在点号和方法名之间),少见,会认即可:
String s2 = this.<String>pick(names);
💡 推断的依据

看实参:传 String[] 进去,T 就是 String。所以同一个方法,今天服务 String、明天服务 Integer——一份代码通吃,这正是"泛"字的含义。

3. 头号正戏:静态泛型方法

8.2 埋的雷在这收网:static 成员用不了类上声明的 T(类加载时 T 未定)——但 static 方法自己声明一个 <T> 就完全合法:

class Util<T> {                              // 类上的 T
    // private static T shared;   ✗ 8.2 的雷
    // public static T get() { } ✗ 同一颗雷

    public static <E> E first(E[] arr) {     // ✓ 方法自己声明 E,与类上的 T 无关
        return arr[0];
    }
}

两个细节:

  1. 方法上的占位符和类上的占位符是两码事——哪怕都叫 T,方法里的 T 也遮住(优先于)类上的 T;惯例上方法自己另起一个名字(E、U)避免误会。
  2. 这就是 JDK 工具类的标准长相:Collections.swap、Arrays.asList、Collections.singleton……全是 static <T> 开头——工具类不能 new(6.3 的套路),想泛型就只剩这一条路。

4. 泛型类 vs 泛型方法怎么选

对比项 泛型类 class Box<T> 泛型方法 static <T> T pick
T 声明在哪 类名后 返回类型前
T 的作用范围 全类共享(字段、方法都能用) 只归本方法
static 能不能用 T ✗ ✓(方法自带声明)
T 什么时候定型 new Box<String>() 时 调用时从实参推断
适合 装东西的容器 干活的工具(一次定型用完就扔)

选型口诀:到处都用 T → 泛型类;只有一个方法用 → 泛型方法。 拿不准就想想 ArrayList(容器,泛型类)和 Collections.swap(工具,静态泛型方法)。

5. 实战主场:参数之间共用一个 T

泛型方法的杀手锏是让多个参数和返回值对齐到同一个类型——编译器替你把关:

// 交换:arr 里两个位置必须是同一数组的,天然成立
public static <T> void swap(T[] arr, int i, int j) {
    T temp = arr[i];
    arr[i] = arr[j];
    arr[j] = temp;
}

// 数组转 List:进什么类型,出什么类型
public static <T> List<T> toList(T[] arr) {
    List<T> list = new ArrayList<>();
    for (T item : arr) { list.add(item); }
    return list;
}

swap(strs, 0, 1) 合法;swap 里想 arr[i] = 42 而 arr 是 String[]?编译器当场拦下——这就是 8.1"把错误提前到编译期"思想的方法版。

💡 认亲戚

Collections.swap(list, i, j)、Collections.sort(list) 底层都是这套。7.7 用的工具,今天看清了身份证:静态泛型方法 + 泛型接口(Comparable,8.3)的组合拳。

6. 动手时间(Demo04)

import java.util.*;

public class Demo04 {
    // 泛型方法 1:随机抽一个
    public static <T> T pick(T[] arr) {
        return arr[new Random().nextInt(arr.length)];
    }

    // 泛型方法 2:交换
    public static <T> void swap(T[] arr, int i, int j) {
        T temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }

    // 泛型方法 3:数组转 List
    public static <T> List<T> toList(T[] arr) {
        return new ArrayList<>(Arrays.asList(arr));
    }

    public static void main(String[] args) {
        String[] names = {"波仔", "墨池", "AI"};
        Integer[] scores = {88, 95, 76};

        // 类型推断:不用写尖括号
        System.out.println("点名: " + pick(names));
        System.out.println("抽分: " + pick(scores));

        // 交换后类型依旧被安检盯着
        swap(names, 0, 2);
        System.out.println("换位后: " + Arrays.toString(names));

        // 数组转 List
        List<Integer> list = toList(scores);
        list.add(99);                       // 转成 List 才能加元素
        System.out.println("转集合加分: " + list);
    }
}

7. 30 秒实验

实验 1:把 pick 的 <T> 删掉——编译报错 cannot find symbol: class T。占位符没声明就上岗,必炸。

实验 2:在 class Util<T> 里写 public static T get() { }(不声明方法自己的 T)——编译报错,8.2 那颗雷重现;改成 static <E> E get() 立刻合法——正解收网。

实验 3:swap(names, 0, 1) 后再写 names[0] = 42——编译报错。T 已被推断为 String,别想塞 Integer。

实验 4:pick(scores) 的返回值赋给 String s——编译报错 incompatible types。推断出的 Integer 不会偷偷变身。

实验 5:Arrays.asList("a", "b", "c") 按住 Ctrl 点进去——JDK 源码签名就是 static <T> List<T> asList(T... a),亲眼确认工具类套路。

8. 常见误区

  1. <T> 写错位置——必须紧贴在返回类型之前(static <T> T pick),写在 static 前面或方法名后面都报错。
  2. 以为 static 从此能随便用 T——静态泛型方法合法的是方法自己声明的 T;类上的 T 照旧与 static 无缘。
  3. 以为方法上的 T 和类上的 T 是同一个——方法自己声明的 T 优先(遮住类上的);惯例方法内另起名 E/U,望文生义不踩坑。
  4. 调用时手写 <String> 当万能开关——推断已经够用;显式指定是给编译器糊涂时的备用手段(比如传 null),日常别写。
  5. 以为泛型方法比泛型类高级——两者分工不同:容器用类、工具用方法;一个类里完全可以两种并存(ArrayList 就是一堆泛型方法住在泛型类里)。
  6. 基本类型直接推断——pick(new int[]{1,2,3}) 的 T 会被推断成 int[] 而不是 Integer,返回的是"整个数组"——想逐个操作请传包装类型数组(8.1 铁规矩第一条)。

9. 折叠自测题

❓ Q1:`` 声明在方法的哪个位置?

修饰符(static 等)之后、返回类型之前:static <T> T pick(...)。口诀:尖括号贴着返回类型的脸。

❓ Q2:调用泛型方法时要写 `pick(names)` 吗?

不用——编译器从实参自动推断 T = String。显式写法 this.<String>pick(names) 存在但极少用。推断是常态,显式是备用。

❓ Q3:static 方法想用泛型,正确的做法是什么?

方法自己声明占位符:static <E> E get(...)。类上的 T 对 static 不可见(8.2 的雷),方法自带 <E> 是唯一合体姿势——JDK 工具类全这么写。

❓ Q4:泛型方法和泛型类里都有个 T,它们是一回事吗?

不是。方法声明自己的 <T> 时,它遮住类上的同名占位符,优先按方法自己的算。所以惯例上方法内另起名(E、U),避免自己糊涂。

❓ Q5:写一个泛型方法 `join`,把任意数组的元素用 "-" 连成一个字符串返回。
public static <T> String join(T[] arr) {
    StringBuilder sb = new StringBuilder();
    for (int i = 0; i < arr.length; i++) {
        if (i > 0) sb.append("-");
        sb.append(arr[i]);
    }
    return sb.toString();
}

要点:<T> 贴返回类型前;arr 里是 Object 一族,append 什么类型都收(6.2 StringBuilder 的 toString 收尾)。

10. 下一步学什么

相关笔记