java 数组去重怎么写?附 4 种方法+对象数组场景清单

编程狮(w3cschool.cn) 2026-08-31 16:24:24 浏览数 (50)
反馈

Java 数组去重最省事的写法是 new LinkedHashSet<>(Arrays.asList(数组)),一行就能去掉重复元素,还保留原来的先后顺序。不在乎顺序时用 HashSet,追求一行流用 Streamdistinct()。但有个前提必须知道:int[] 这种基本类型数组不能直接转成集合,要先装箱成 Integer[] 或者走流。

很多初学者第一次写数组去重,照着网上的例子抄了 Arrays.asList(nums),结果要么编译不过,要么得到一个只有一个元素的集合,查半天不知道错在哪。其实问题都出在基本类型和包装类型的区别上。今天这篇文章,编程狮就把 Java 数组去重的 4 种写法连同对象数组按字段去重的正确做法一次讲清,最后给你一张对比表。

Java 数组去重方法示意图

一、Java 数组去重到底难在哪

Python 一行能搞定的事,Java 写起来绕,根源在于数组长度固定、且基本类型不能进集合。Java 的集合(Set、List)只装对象,intdouble 这些基本类型必须先变成 IntegerDouble 这样的包装类型才能放进去。这个"装箱"动作就是大部分坑的来源。

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

踩坑清单:

  1. int[] 不能直接进集合。 Arrays.asList(int数组) 得到的是只有一个元素的 List<int[]>,要先装箱或用流。
  2. 自定义对象不重写 equals()hashCode(),去重无效。 两个方法必须用同一批字段,只写一个也不行。
  3. Collectors.toMap() 不写合并函数会抛异常。 键重复时它不会自动覆盖,必须显式声明保留谁。
  4. HashSet 的输出顺序不要依赖。 它按哈希值排列,看着像有序只是巧合。
  5. 去重不改原数组。 所有写法都是返回新数组或新集合,原数据保持原样。

数组去重往往是数据清洗的第一步,后面常接着排序、分组、转 Map 这些操作。想把这些集合操作串起来系统学一遍,Java 教程 的集合框架章节是合适的入口。

Java 数组去重方法怎么选

总结

Java 数组去重记住三句话就够:Java 8 以上优先用 Stream 的 distinct(),一行搞定且保留顺序;需要集合继续处理就用 LinkedHashSetint[] 这类基本类型数组不能直接进集合,先装箱或走流。 至于对象数组,本质是"按什么判重"的问题,靠重写 equals()hashCode(),或者用 toMap() 的合并函数来回答。

  • 是否保留顺序是选型的第一分水岭,LinkedHashSetHashSet 用法完全相同;
  • 基本类型数组要先装箱,Arrays.asList() 直接传 int[] 会得到只有一个元素的集合;
  • 对象去重必须重写 equals()hashCode(),且两者字段要一致;
  • 双重循环写法复杂度是 O(n²),只适合小数据量或教学示例。

延伸学习

  1. Java 基础配套课程 —— 从语法基础一路讲到集合框架与泛型。
  2. Java 集合底层原理笔记 —— HashSet 与 LinkedHashSet 的底层结构对照。
  3. 在线运行 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。两者 addcontainsremove 的写法完全一致,性能差距极小,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 写法在基本类型数组上更顺手的原因。

0 人点赞