最常用方法是调用目标列表的addall(),将源列表元素追加到可变目标列表末尾,不改变源列表;需注意目标列表必须可变,否则抛unsupportedoperationexception。

用 addAll() 合并两个 List 最常用也最直接
Java 中合并两个 List,最常见做法就是调用目标列表的 addAll() 方法。它会把源列表所有元素追加到目标列表末尾,不改变源列表本身。
注意:目标列表必须是可变的(比如 ArrayList),不能是 Collections.unmodifiableList() 或 Arrays.asList() 返回的固定大小列表,否则抛 UnsupportedOperationException。
示例:
List<String> list1 = new ArrayList<>(Arrays.asList("a", "b"));
List<String> list2 = Arrays.asList("c", "d"); // 不可变,但作为源没问题
list1.addAll(list2); // ✅ 成功,list1 变成 ["a", "b", "c", "d"]
-
addAll()返回boolean,表示是否发生了实际添加(源为空时返回false) - 如果目标列表是
LinkedList,频繁addAll()到末尾性能尚可;但若在中间插入,应考虑其他方式 - 不要对同一个列表对象既作源又作目标(如
list.addAll(list)),会导致无限扩容甚至OutOfMemoryError
需要去重合并?别直接用 addAll(),改用 Stream 或 Set
如果两个 List 合并后要去掉重复元素,addAll() 无法自动处理。硬编码遍历 + contains() 效率低,还可能破坏顺序。
立即学习“Java免费学习笔记(深入)”;
推荐用 Stream 保持顺序且简洁:
List<String> merged = Stream.concat(list1.stream(), list2.stream())
.distinct()
.collect(Collectors.toList());
-
distinct()依赖元素的equals()和hashCode(),自定义类务必重写这两个方法 - 如果原始顺序不重要,用
LinkedHashSet构造更快:new ArrayList(new LinkedHashSet(list1) {{ addAll(list2); }}) - 注意
Stream.concat()对空列表安全,但list1或list2为null会触发NullPointerException,需提前判空
不想修改原列表?用 Stream 或构造新 ArrayList
很多场景要求保留原始两个 List 不变,只生成一个新合并结果。这时不能用 addAll()(它会改目标),得创建新容器。
两种主流做法:
- 显式构造:
new ArrayList(list1) {{ addAll(list2); }}—— 简单直观,但双大括号语法有内存泄漏风险(隐式持有外部类引用),仅限局部临时使用 - 函数式风格:
Stream.concat(list1.stream(), list2.stream()).collect(Collectors.toList())—— 更安全,语义清晰,JDK 8+
性能上,显式构造略快(少一次遍历),但差异微乎其微;可读性和维护性上,Stream 方式更统一,尤其后续还要过滤或映射时。
合并带泛型擦除的 List?编译期类型检查可能失效
Java 泛型在运行时被擦除,所以 List<string></string> 和 List<integer></integer> 合并时,编译器不会阻止你写 listStr.addAll((List) listInt),但运行时可能出 ClassCastException。
- 始终确保两个
List的泛型类型兼容,尤其是用原始类型(List)混用时 - IDE 和静态检查工具(如 ErrorProne)能捕获部分问题,但不能完全替代逻辑校验
- 如果来源不可控(比如反射获取的列表),合并前建议用
instanceof或stream().allMatch(...)做运行时类型验证
泛型安全不是“有没有警告”,而是“会不会在下游某个 get() 调用时突然崩”。这点容易被忽略,尤其在封装工具方法时。










