从一道算法题发现的泛型问题

0 阅读1分钟

从一道算法题发现的泛型问题

算法题回顾

我在重刷 LeetCode hot100时 写了这样的两段代码: 请简单看几分钟思考一下,这两段代码有什么区别? 其中一段存在问题,你能看出来吗?

  class Solution {
    public int[] maxSlidingWindow(int[] nums, int k) {
        List<Integer> result =new ArrayList<>();
        //...
        return result.toArray(new int[result.size()]);
    }
}
class Solution {
    public int[][] merge(int[][] intervals) {
        List<int[]> result = new ArrayList<>();
        //...
        return result.toArray(new int[result.size()][]);
    }
}

场景反思

两段代码写得几乎一样:

  • 都是 List<T>
  • 都调用了 toArray(T[] a)
  • 都直接 return
  • 方法声明的返回值类型看起来也完全匹配

但结果却是:

  • 滑动窗口 ❌ 编译失败

image-50.png

  • 合并区间 ✅ 编译通过,运行正常

从错误信息可以看出,编译器在推断泛型参数 T 时,推导出了一个非法的 T,这是因为 int 为基本类型,不是一个合法的T


源码分析:toArray 的泛型机制

这里就需要贴出ArrayList的源码(简化)了。

  • toArray(T[] a) 方法的实现是:
public <T> T[] toArray(T[] a) {
    if (a.length < size)
        // 返回一个新的数组,类型与 a 相同
        return (T[]) Arrays.copyOf(elementData, size, a.getClass());
    System.arraycopy(elementData, 0, a, 0, size);
    if (a.length > size)
        a[size] = null;
    return a;
}

关键点:toArray 的返回类型是 T[],而是T[]的类型是传入数组的类型推断出来的!

// 调用示例
List<String> list = new ArrayList<>();
String[] arr = list.toArray(new String[0]);  
// 编译器推断 T = String(因为 new String[0] 的组件类型是 String)

List<Integer> list2 = new ArrayList<>();
Integer[] arr2 = list2.toArray(new Integer[0]);  
// 编译器推断 T = Integer
为什么 int[] 行?

现在回头看通过的代码:

List<int[]> result = new ArrayList<>();
return result.toArray(new int[result.size()][]);  
  • 参数 new int[result.size()][] 的类型是 int[][]
  • 组件类型是 int[]
  • 编译器推断 T = int[] 数组类型是引用类型!

本质:Java 的泛型与基本类型

我误以为:

new int[result.size()]       // 滑动窗口
new int[result.size()][]     // 合并区间

只是“多了一维”,但这是完全不同的类型系统级别差异

我们冷静拆一下:

代码参数类型数组的「组件类型」
new int[size]int[]int(基本类型)
new int[size][]int[][]int[](引用类型)

问题一下子就变清晰了:

toArray(T[] a) 的泛型参数 T取自数组的组件类型

  • 组件类型是引用类型 ✅ → T 合法
  • 组件类型是基本类型 ❌ → T 非法

所以不是“写法不同”,而是组件类型从引用类型退化成了基本类型

Java 在以下两个方面造成了“视觉欺骗”:

int[] 看起来像基本类型,但它是引用类型

这是 Java 早期设计留下的历史包袱。
对 JVM 来说:

  • int :值
  • int[] :对象头 + 连续内存

但对程序员来说,int[] 写起来太像基本类型了。

② 数组语法会“隐藏”组件类型

我写的是:

new int[result.size()][]

我的大脑理解成了:

“这是一个二维数组”

但编译器问的是:

这个数组里放的是什么东西?

放的是 int[](引用类型) ✅


这一下就戳穿了泛型的一个“伪装”

我们通常学习泛型时,得到一个结论:

泛型不支持基本类型。

这句话我们背得很熟,但从来没被真正“刺痛”过。
直到 toArray 把我们架在了一个不得不推断基本类型的位置上。

这里真正值得反思的是: 我们为什么会在不知不觉中,把 int 塞进了泛型推断链路?

这里的int[] 作为参数,恰巧变成了我们把基本类型送进泛型的传送门。


基本类型为什么不是泛型的真相(原理讲解)

Java 的泛型是编译时的语法糖,运行时会被擦除:

// 编译时
List<Integer> list = new ArrayList<>();
Integer num = list.get(0);

// 擦除后(运行时)
List list = new ArrayList();
Integer num = (Integer) list.get(0);  // 插入强制转换

关键限制:擦除后的上界是 Object,而 int 不是 Object 的子类,所以泛型参数不能是基本类型。

数组的特殊性

所有数组都继承自 Object

int[] arr = new int[5];

System.out.println(arr instanceof Object);  // true

说明:

数组就是 JVM 创建的一个特殊对象,有明显的对象特征:

数组具备典型“对象行为”:

  • 有方法
arr.getClass()
  • 有运行时类型
arr.getClass().getComponentType()
  • 可以作为引用传递
void foo(int[] a) { ... }

这些都说明:它是“对象引用”。


需求解决

但是我们的需求怎么解决?我们自然知道如果这样写是对的:

List<Integer> list = new ArrayList<>();
Integer[] arr = list.toArray(new Integer[0]);   // ✅

可是题目的返回值是 int[],我们的需求怎么解决呢?我们知道Integer是int对应的引用类型。Java是会帮我们自动拆箱的,那可不可以写:

List<Integer> list = new ArrayList<>();
Integer[] arr = list.toArray(new Integer[0]); 
int[] arr2 = arr;

直觉上这非常自然:

我已经用 Integer 存了,你帮我转成 int[] 怎么了?

答案是:不可以!因为 Java 不会自动拆箱数组。

正确的转换方式

如果要用 List<Integer>int[],有以下几种正确写法:

方式1:Stream(最简洁)

return result.stream().mapToInt(Integer::intValue).toArray();

方式2:手动循环(性能最好)

int[] arr = new int[result.size()];
for (int i = 0; i < result.size(); i++) {
    arr[i] = result.get(i);
}
return arr;

方式3:先转 Integer[] 再手动拆箱

Integer[] temp = result.toArray(new Integer[0]);
int[] arr = new int[temp.length];
for (int i = 0; i < temp.length; i++) {
    arr[i] = temp[i];
}
return arr;

💬 互动讨论

  • 你在项目中遇到过类似的泛型“坑”吗?
  • 有没有其他方法可以优雅地转换 List 到 int[]?

欢迎在评论区分享你的看法! 一个看似简单的 toArray() 调用,竟然暴露了 Java 泛型的“坑”!