从一道算法题发现的泛型问题
算法题回顾
我在重刷 LeetCode hot100时 写了这样的两段代码: 请简单看几分钟思考一下,这两段代码有什么区别? 其中一段存在问题,你能看出来吗?
java
class Solution {
public int[] maxSlidingWindow(int[] nums, int k) {
List<Integer> result =new ArrayList<>();
//...
return result.toArray(new int[result.size()]);
}
}
java
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 - 方法声明的返回值类型看起来也完全匹配
但结果却是:
- 滑动窗口 ❌ 编译失败

- 合并区间 ✅ 编译通过,运行正常
从错误信息可以看出,编译器在推断泛型参数 T 时,推导出了一个非法的 T,这是因为 int 为基本类型,不是一个合法的T。
源码分析:toArray 的泛型机制
这里就需要贴出ArrayList的源码(简化)了。
toArray(T[] a)方法的实现是:
java
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[]的类型是传入数组的类型推断出来的!
java
// 调用示例
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[] 行?
现在回头看通过的代码:
java
List<int[]> result = new ArrayList<>();
return result.toArray(new int[result.size()][]);
- 参数
new int[result.size()][]的类型是int[][] - 组件类型是
int[] - 编译器推断
T = int[]数组类型是引用类型!
本质:Java 的泛型与基本类型
我误以为:
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[] 写起来太像基本类型了。
② 数组语法会"隐藏"组件类型
我写的是:
java
new int[result.size()][]
我的大脑理解成了:
"这是一个二维数组"
但编译器问的是:
这个数组里放的是什么东西?
放的是 int[](引用类型) ✅
这一下就戳穿了泛型的一个"伪装"
我们通常学习泛型时,得到一个结论:
泛型不支持基本类型。
这句话我们背得很熟,但从来没被真正"刺痛"过。
直到 toArray 把我们架在了一个不得不推断基本类型的位置上。
这里真正值得反思的是: 我们为什么会在不知不觉中,把 int 塞进了泛型推断链路?
这里的int[] 作为参数,恰巧变成了我们把基本类型送进泛型的传送门。
基本类型为什么不是泛型的真相(原理讲解)
Java 的泛型是编译时的语法糖,运行时会被擦除:
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
java
int[] arr = new int[5];
System.out.println(arr instanceof Object); // true
说明:
数组就是 JVM 创建的一个特殊对象,有明显的对象特征:
数组具备典型"对象行为":
- 有方法
java
arr.getClass()
- 有运行时类型
java
arr.getClass().getComponentType()
- 可以作为引用传递
java
void foo(int[] a) { ... }
这些都说明:它是"对象引用"。
需求解决
但是我们的需求怎么解决?我们自然知道如果这样写是对的:
java
List<Integer> list = new ArrayList<>();
Integer[] arr = list.toArray(new Integer[0]); // ✅
可是题目的返回值是 int[],我们的需求怎么解决呢?我们知道Integer是int对应的引用类型。Java是会帮我们自动拆箱的,那可不可以写:
java
List<Integer> list = new ArrayList<>();
Integer[] arr = list.toArray(new Integer[0]);
int[] arr2 = arr;
直觉上这非常自然:
我已经用
Integer存了,你帮我转成int[]怎么了?
答案是:不可以!因为 Java 不会自动拆箱数组。
正确的转换方式
如果要用 List<Integer> 转 int[],有以下几种正确写法:
方式1:Stream(最简洁)
java
return result.stream().mapToInt(Integer::intValue).toArray();
方式2:手动循环(性能最好)
java
int[] arr = new int[result.size()];
for (int i = 0; i < result.size(); i++) {
arr[i] = result.get(i);
}
return arr;
方式3:先转 Integer[] 再手动拆箱
java
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 泛型的"坑"!