Java 数组去重最省事的写法是 new LinkedHashSet<>(Arrays.asList(数组)),一行就能去掉重复元素,还保留原来的先后顺序。不在乎顺序时用 HashSet,追求一行流用 Stream 的 distinct()。但有个前提必须知道:int[] 这种基本类型数组不能直接转成集合,要先装箱成 Integer[] 或者走流。
很多初学者第一次写数组去重,照着网上的例子抄了 Arrays.asList(nums),结果要么编译不过,要么得到一个只有一个元素的集合,查半天不知道错在哪。其实问题都出在基本类型和包装类型的区别上。今天这篇文章,编程狮就把 Java 数组去重的 4 种写法连同对象数组按字段去重的正确做法一次讲清,最后给你一张对比表。

一、Java 数组去重到底难在哪
Python 一行能搞定的事,Java 写起来绕,根源在于数组长度固定、且基本类型不能进集合。Java 的集合(Set、List)只装对象,int、double 这些基本类型必须先变成 Integer、Double 这样的包装类型才能放进去。这个"装箱"动作就是大部分坑的来源。
1.1 一个最小可运行示例
下面这段代码可以直接编译运行,两种集合的差别一眼可见:
import java.util.*;
public class DedupDemo {
public static void main(String[] args) {
Integer[] nums = {102, 101, 103, 101, 102};
Set<Integer> byHashSet = new HashSet<>(Arrays.asList(nums));
System.out.println(byHashSet); // [101, 102, 103]
Set<Integer> byLinked = new LinkedHashSet<>(Arrays.asList(nums));
System.out.println(byLinked); // [102, 101, 103]
}
}
同一个数组,HashSet 输出 [101, 102, 103],顺序变了;LinkedHashSet 输出 [102, 101, 103],和原数组中每个元素第一次出现的顺序完全一致。这就是选型的第一分水岭。
1.2 常见误解
最常见的误解就是把 int[] 直接丢给 Arrays.asList():
int[] nums = {102, 101, 103, 101, 102};
System.out.println(Arrays.asList(nums).size()); // 1,不是 5
它不会报错,但结果是 1。原因是 Arrays.asList() 接收的是可变参数 T...,传进去一个 int[],Java 会把整个 int[] 当成"一个元素"(类型推断为 List<int[]>),而不是把 5 个数字当成 5 个元素。这个坑非常隐蔽,因为代码能编译、能运行,只是结果不对。
正确做法是先装箱:
int[] nums = {102, 101, 103, 101, 102};
Integer[] boxed = Arrays.stream(nums).boxed().toArray(Integer[]::new);
System.out.println(Arrays.toString(boxed)); // [102, 101, 103]
第二个误解是"去重会修改原数组"。和大多数数组操作一样,上面所有写法都是生成新数组或新集合,原数组内容不变。第三,Set 判断重复靠的是 equals() 和 hashCode(),自定义对象不重写这两个方法,去重就会失效——这是第四节的重点。
二、方法一:HashSet,最常用但不保留顺序
如果你的需求只是"知道有哪些不同的值",比如统计去重后的个数、判断有没有重复,HashSet 是最常见的选择:
import java.util.*;
public class DedupByHashSet {
public static void main(String[] args) {
int[] nums = {102, 101, 103, 101, 102};
Set<Integer> set = new HashSet<>();
for (int n : nums) {
set.add(n); // 这里自动装箱,int 变成 Integer
}
int[] result = set.stream().mapToInt(Integer::intValue).toArray();
System.out.println(Arrays.toString(result)); // [101, 102, 103]
}
}
这里用了 for 循环逐个 add,循环里的 set.add(n) 会自动把 int 装箱成 Integer,所以不需要手动转 Integer[],这是绕过基本类型限制最省事的写法。
它的优点是查询快:判断是否重复的平均时间复杂度是 O(1),整体复杂度是 O(n)。缺点同样明确——遍历顺序由哈希值决定,不保留原数组顺序。另外要注意,最后转回 int[] 时得用 mapToInt 拆箱,set.toArray() 直接拿到的是 Integer[]。
如果你好奇基本类型和包装类型在内存与比较上的差异,Java 虚拟机教程 里有更底层的说明。
三、方法二:LinkedHashSet,保留插入顺序
只要把 HashSet 换成 LinkedHashSet,顺序问题就解决了,其他代码一个字都不用改:
import java.util.*;
public class DedupByLinkedHashSet {
public static void main(String[] args) {
int[] nums = {102, 101, 103, 101, 102};
Set<Integer> set = new LinkedHashSet<>();
for (int n : nums) {
set.add(n);
}
int[] result = set.stream().mapToInt(Integer::intValue).toArray();
System.out.println(Arrays.toString(result)); // [102, 101, 103]
}
}
LinkedHashSet 在哈希表的基础上额外维护了一条链表,记录元素的插入先后,所以遍历时按插入顺序输出。它和 HashSet 的用法完全一致,复杂度同样是 O(n),代价只是多占一点点内存。
这也是编程狮最推荐的默认选择:绝大多数业务场景里,你都希望去重结果和原始顺序对得上(比如按时间先后提取出的编号列表,去重后还得能看出谁先谁后)。既然两个类的写法一模一样,没有理由为了省那一点内存去冒顺序错乱的风险。
如果最终结果想要 Integer[] 而不是 int[],可以省掉流式转换,直接:
Integer[] result = set.toArray(new Integer[0]);
四、方法三和方法四:Stream 去重与双重循环
4.1 方法三:Stream 一行流(Java 8 以上)
Java 8 引入的 Stream 让数组去重变得非常简洁,而且基本类型数组可以直接处理,不用手动装箱:
import java.util.*;
import java.util.stream.*;
public class DedupByStream {
public static void main(String[] args) {
int[] nums = {102, 101, 103, 101, 102};
int[] result = Arrays.stream(nums).distinct().toArray();
System.out.println(Arrays.toString(result)); // [102, 101, 103]
String[] names = {"Tom", "Alice", "Tom", "Bob"};
String[] uniq = Arrays.stream(names).distinct().toArray(String[]::new);
System.out.println(Arrays.toString(uniq)); // [Tom, Alice, Bob]
}
}
Arrays.stream(int[]) 得到的是 IntStream,它的 distinct() 直接按基本类型值比较,既没有装箱开销,也保留原顺序,最后 toArray() 拿回 int[]。写起来最短,读起来也清楚,是 Java 8 以上环境的首选。
4.2 方法四:双重循环,能保序但不推荐
如果环境受限(比如老项目还在用 Java 7),或者你想看清去重的完整过程,可以手写双重循环:
import java.util.*;
public class DedupByLoop {
public static int[] dedup(int[] nums) {
int[] temp = new int[nums.length];
int size = 0;
for (int n : nums) {
boolean exists = false;
for (int i = 0; i < size; i++) {
if (temp[i] == n) {
exists = true;
break;
}
}
if (!exists) {
temp[size++] = n;
}
}
return Arrays.copyOf(temp, size);
}
public static void main(String[] args) {
System.out.println(Arrays.toString(dedup(new int[]{102, 101, 103, 101, 102})));
// [102, 101, 103]
}
}
思路是:准备一个临时数组,每来一个元素就回头扫一遍已存的部分,没出现过才追加。它保留原顺序、不需要任何集合类,但时间复杂度是 O(n²)——元素个数翻倍,耗时会变成四倍。一万条数据就要做五千万次比较,所以只适合数据量很小或者作为理解原理的示例,实际项目别用。
4.3 典型场景:对象数组按某个字段去重
这才是真正容易出错的地方。假设有一批 User 对象,你想按 name 去重(名字相同的只留一个)。因为 Set 判重依赖 equals() 和 hashCode(),所以必须在类里重写这两个方法:
import java.util.*;
class User {
private String name;
private int age;
public User(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() { return name; }
public int getAge() { return age; }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof User)) return false;
User other = (User) o;
return Objects.equals(name, other.name); // 只按 name 判相等
}
@Override
public int hashCode() {
return Objects.hash(name); // 必须用同样的字段
}
@Override
public String toString() {
return name + "(" + age + ")";
}
}
有了这两个方法,去重写法和前面完全一样,用 LinkedHashSet 保留第一条出现的记录:
User[] users = {
new User("Tom", 18),
new User("Alice", 20),
new User("Tom", 25)
};
List<User> uniq = new ArrayList<>(new LinkedHashSet<>(Arrays.asList(users)));
System.out.println(uniq); // [Tom(18), Alice(20)]
注意留下的是 Tom(18),也就是第一次出现的那条。
如果你不想为了去重去改类的 equals(),或者这次要保留最后一条,可以用 Collectors.toMap() 的第三个参数(合并函数)来决定保留谁:
import java.util.stream.*;
List<User> keepOld = new ArrayList<>(
Arrays.stream(users).collect(Collectors.toMap(
User::getName,
u -> u,
(oldVal, newVal) -> oldVal, // 键冲突时保留先出现的
LinkedHashMap::new // 第四个参数指定 Map 实现,保证顺序
)).values()
);
System.out.println(keepOld); // [Tom(18), Alice(20)]
List<User> keepNew = new ArrayList<>(
Arrays.stream(users).collect(Collectors.toMap(
User::getName,
u -> u,
(oldVal, newVal) -> newVal, // 键冲突时保留后出现的
LinkedHashMap::new
)).values()
);
System.out.println(keepNew); // [Tom(25), Alice(20)]
这里有个必踩的坑:不写第三个参数(合并函数)时,键重复会直接抛 IllegalStateException,而不是默默覆盖。第四个参数传 LinkedHashMap::new 也很重要,默认实现不保证顺序,输出结果的先后就随缘了。
五、怎么选:4 种方法对比表与踩坑清单
| 方法 | 保留顺序 | 适用类型 | 时间复杂度 | 适用场景 |
|---|---|---|---|---|
| HashSet | 否 | 包装类型 | O(n) | 只关心有哪些值、统计个数 |
| LinkedHashSet | 是 | 包装类型 | O(n) | 要保留原顺序,默认首选 |
| Stream 的 distinct() | 是 | 基本类型与对象均可 | O(n) | Java 8 以上,写法最短 |
| 双重循环 | 是 | 基本类型 | O(n²) | 数据量极小或理解原理用 |
一句话决策:Java 8 以上优先用 Stream 的 distinct(),简洁又保序;需要中间过程可控或要转成集合继续操作时,用 LinkedHashSet。
踩坑清单:
int[]不能直接进集合。Arrays.asList(int数组)得到的是只有一个元素的List<int[]>,要先装箱或用流。- 自定义对象不重写
equals()和hashCode(),去重无效。 两个方法必须用同一批字段,只写一个也不行。 Collectors.toMap()不写合并函数会抛异常。 键重复时它不会自动覆盖,必须显式声明保留谁。HashSet的输出顺序不要依赖。 它按哈希值排列,看着像有序只是巧合。- 去重不改原数组。 所有写法都是返回新数组或新集合,原数据保持原样。
数组去重往往是数据清洗的第一步,后面常接着排序、分组、转 Map 这些操作。想把这些集合操作串起来系统学一遍,Java 教程 的集合框架章节是合适的入口。

总结
Java 数组去重记住三句话就够:Java 8 以上优先用 Stream 的 distinct(),一行搞定且保留顺序;需要集合继续处理就用 LinkedHashSet;int[] 这类基本类型数组不能直接进集合,先装箱或走流。 至于对象数组,本质是"按什么判重"的问题,靠重写 equals() 和 hashCode(),或者用 toMap() 的合并函数来回答。
- 是否保留顺序是选型的第一分水岭,
LinkedHashSet与HashSet用法完全相同; - 基本类型数组要先装箱,
Arrays.asList()直接传int[]会得到只有一个元素的集合; - 对象去重必须重写
equals()和hashCode(),且两者字段要一致; - 双重循环写法复杂度是 O(n²),只适合小数据量或教学示例。
延伸学习
- Java 基础配套课程 —— 从语法基础一路讲到集合框架与泛型。
- Java 集合底层原理笔记 —— HashSet 与 LinkedHashSet 的底层结构对照。
- 在线运行 Java 代码工具 —— 不用配环境,直接编译运行本文的示例代码。
常见问题
Q:为什么 Arrays.asList(nums) 得到的集合只有一个元素?
A:因为 Arrays.asList() 的参数是可变参数 T...,而 T 只能是引用类型。传一个 int[] 进去,Java 无法把基本类型拆成多个对象,只能把整个数组当作一个元素,于是得到 List<int[]>,长度自然是 1。解决办法是先转成 Integer[],或者直接用 Arrays.stream(nums).boxed() 走流。
Q:HashSet 和 LinkedHashSet 该怎么选?
A:默认选 LinkedHashSet。两者 add、contains、remove 的写法完全一致,性能差距极小,LinkedHashSet 只是多用一点内存来维护插入顺序。除非你明确不在乎顺序、且数据量特别大,否则没有理由为了省内存去冒顺序错乱的风险。
Q:对象数组按字段去重,怎么保留重复项里的最后一条?
A:用 Collectors.toMap() 并在第三个参数(合并函数)里返回后出现的那个值,也就是写成 (oldVal, newVal) -> newVal。如果想保留第一条,就返回 oldVal。记得同时传入第四个参数 LinkedHashMap::new,否则默认 Map 不保证输出顺序。
Q:去重之后我想拿回 int[] 而不是 Integer[],该怎么写?
A:如果手里是 Set<Integer>,用 set.stream().mapToInt(Integer::intValue).toArray() 转成 int[];如果本来就用的 IntStream(也就是 Arrays.stream(int数组) 那条路),末尾的 toArray() 直接返回 int[],不需要任何转换。这也是 Stream 写法在基本类型数组上更顺手的原因。

免费 AI IDE



