Java数组去重:从基础到高级,全方位解析效率与技巧115

好的,作为一名专业的程序员,我将为您撰写一篇关于Java数组去重的专业文章,涵盖多种方法、性能考量及最佳实践,并附带代码示例。


在Java开发中,处理数组数据是日常任务之一。数组去重(即从一个数组中移除重复元素,只保留每个元素的第一个或唯一出现)是一个非常常见的需求,无论是在数据预处理、优化存储,还是在业务逻辑中确保数据唯一性,都扮演着重要角色。本文将深入探讨Java中实现数组去重的各种方法,从最基础的手动实现到利用现代Java特性,包括不同数据类型(基本类型、引用类型、自定义对象)的处理,以及它们在性能、空间和代码简洁性上的权衡。


一、理解数组去重的核心挑战


在深入具体方法之前,我们首先需要明确数组去重可能遇到的几个核心挑战:

数据类型: 数组中存储的是基本类型(如`int`, `long`, `char`)还是引用类型(如`String`, 自定义对象)?对于引用类型,Java如何判断两个对象是“相同”的?这通常涉及到`equals()`和`hashCode()`方法的实现。
性能要求: 数据量有多大?如果数组非常庞大,效率低下的去重算法可能导致程序卡顿甚至崩溃。
顺序保留: 去重后的数组是否需要保持原数组中元素的相对顺序?有些方法会打乱顺序,而有些则能很好地保留。
空间复杂度: 是否允许使用额外的存储空间来辅助去重?例如,创建新的集合或数组。
代码简洁性: 在满足前述要求的前提下,代码是否易于理解和维护?


二、Java数组去重的常用方法


我们将从多个角度介绍数组去重的方法,并提供详细的代码示例。


方法一:使用`HashSet`(最常用且高效)


`HashSet`是Java集合框架中实现`Set`接口的一个类,它不包含重复元素,并且不保证元素的顺序。其内部使用`HashMap`实现,通过元素的`hashCode()`和`equals()`方法来判断元素是否重复。这是在不关心元素顺序的情况下,最推荐的去重方法。


原理: 将数组元素逐个添加到`HashSet`中,`HashSet`会自动处理重复元素。


优点:

效率高,平均时间复杂度为O(N),其中N为数组长度。
代码简洁。


缺点:

不保留原数组元素的相对顺序。
需要额外空间存储`HashSet`。


代码示例(适用于基本类型包装类和引用类型):

import ;
import ;
import ;
import ;
import ;
public class ArrayDeduplicate {
/
* 使用HashSet对数组进行去重,不保留原始顺序。
*
* @param array 待去重的数组
* @param 数组元素的类型
* @return 去重后的新数组
*/
public static <T> T[] deduplicateWithHashSet(T[] array) {
if (array == null || == 0) {
return array;
}
// 将数组元素添加到HashSet中,HashSet会自动处理重复元素
Set<T> uniqueElements = new HashSet<>((array));
// 将HashSet转换回数组
// 需要根据原始数组的组件类型创建新数组
return ((T[]) (array, 0, ).getClass().getComponentType().cast(new Object[()]));
}
public static void main(String[] args) {
Integer[] intArray = {1, 2, 3, 2, 4, 1, 5, 3};
Integer[] uniqueIntArray = deduplicateWithHashSet(intArray);
("使用HashSet去重 (Integer): " + (uniqueIntArray)); // [1, 2, 3, 4, 5] (顺序可能不同)
String[] stringArray = {"apple", "banana", "orange", "apple", "grape", "banana"};
String[] uniqueStringArray = deduplicateWithHashSet(stringArray);
("使用HashSet去重 (String): " + (uniqueStringArray)); // [apple, banana, orange, grape] (顺序可能不同)
}
}


注意: 对于基本类型数组(如`int[]`),需要先将其转换为包装类型数组(如`Integer[]`)才能使用泛型`HashSet`。或者,可以使用流API(方法四)直接处理基本类型。


方法二:使用`LinkedHashSet`(保留插入顺序)


如果去重后需要保留元素在原数组中的相对顺序,`LinkedHashSet`是理想的选择。它继承自`HashSet`,但内部通过链表维护了元素的插入顺序。


原理: 同`HashSet`,但额外维护一个双向链表来记录元素的插入顺序。


优点:

效率高,平均时间复杂度O(N)。
保留元素在原数组中的相对顺序。
代码简洁。


缺点:

需要额外空间存储`LinkedHashSet`。
相比`HashSet`,内存占用略高。


代码示例(适用于基本类型包装类和引用类型):

import ;
import ;
import ;
import ;
import ;
public class ArrayDeduplicateOrdered {
/
* 使用LinkedHashSet对数组进行去重,保留原始插入顺序。
*
* @param array 待去重的数组
* @param 数组元素的类型
* @return 去重后的新数组
*/
public static <T> T[] deduplicateWithLinkedHashSet(T[] array) {
if (array == null || == 0) {
return array;
}
// 将数组元素添加到LinkedHashSet中,它会自动处理重复元素并保留插入顺序
Set<T> uniqueElements = new LinkedHashSet<>((array));
// 将LinkedHashSet转换回数组
return ((T[]) (array, 0, ).getClass().getComponentType().cast(new Object[()]));
}
public static void main(String[] args) {
Integer[] intArray = {1, 2, 3, 2, 4, 1, 5, 3};
Integer[] uniqueIntArray = deduplicateWithLinkedHashSet(intArray);
("使用LinkedHashSet去重 (Integer): " + (uniqueIntArray)); // [1, 2, 3, 4, 5] (顺序保留)
String[] stringArray = {"apple", "banana", "orange", "apple", "grape", "banana"};
String[] uniqueStringArray = deduplicateWithLinkedHashSet(stringArray);
("使用LinkedHashSet去重 (String): " + (uniqueStringArray)); // [apple, banana, orange, grape] (顺序保留)
}
}


方法三:手动遍历与`()`(低效,不推荐)


这是一种直观但效率较低的方法。通过遍历原数组,将不重复的元素添加到一个新的`ArrayList`中,并在添加前检查元素是否已存在。


原理: 遍历原数组,对于每个元素,如果新列表中不存在,则添加。


优点:

实现思路简单,易于理解。
保留原始插入顺序。


缺点:

效率低下,时间复杂度为O(N^2)。因为`()`的每次调用都需要遍历当前列表,最坏情况下N次调用,每次O(N)。
需要额外空间存储`ArrayList`。


代码示例:

import ;
import ;
import ;
public class ArrayDeduplicateManual {
/
* 手动遍历数组并使用()去重,保留原始插入顺序。
* 效率较低,不推荐用于大数据量。
*
* @param array 待去重的数组
* @param 数组元素的类型
* @return 去重后的新数组
*/
public static <T> T[] deduplicateManually(T[] array) {
if (array == null || == 0) {
return array;
}
List<T> uniqueList = new ArrayList<>();
for (T element : array) {
if (!(element)) { // 每次contains()都会遍历uniqueList
(element);
}
}
// 将List转换回数组
return ((T[]) (array, 0, ).getClass().getComponentType().cast(new Object[()]));
}
public static void main(String[] args) {
Integer[] intArray = {1, 2, 3, 2, 4, 1, 5, 3};
Integer[] uniqueIntArray = deduplicateManually(intArray);
("手动去重 (Integer): " + (uniqueIntArray)); // [1, 2, 3, 4, 5]
String[] stringArray = {"apple", "banana", "orange", "apple", "grape", "banana"};
String[] uniqueStringArray = deduplicateManually(stringArray);
("手动去重 (String): " + (uniqueStringArray)); // [apple, banana, orange, grape]
}
}


方法四:使用Java 8 Stream API `distinct()`(现代、简洁)


Java 8引入的Stream API提供了一种声明式处理数据集合的方式,其中`distinct()`方法专门用于去重。它内部实现通常依赖于元素的`equals()`和`hashCode()`方法,类似于`HashSet`,并且通常能够保留元素的相对顺序。


原理: 将数组转换为流,调用`distinct()`方法过滤重复元素,然后将流收集回数组或列表。


优点:

代码非常简洁,声明式风格。
内部实现高效,通常利用`HashSet`的原理。
通常保留原始插入顺序(取决于Stream的源和中间操作,对于数组源通常是保留的)。
支持基本类型流(`IntStream`, `LongStream`, `DoubleStream`),避免了包装类型转换。


缺点:

对于非常小的数组,可能存在一些Stream创建的开销。
对于不熟悉Stream API的开发者来说,可能需要一些学习曲线。


代码示例(适用于基本类型和引用类型):

import ;
import ;
import ;
public class ArrayDeduplicateStream {
/
* 使用Java 8 Stream API的distinct()方法对数组进行去重。
*
* @param array 待去重的数组
* @param 数组元素的类型
* @return 去重后的新数组
*/
public static <T> T[] deduplicateWithStream(T[] array) {
if (array == null || == 0) {
return array;
}
List<T> uniqueList = (array)
.distinct() // 核心去重操作
.collect(());
// 将List转换回数组
return ((T[]) (array, 0, ).getClass().getComponentType().cast(new Object[()]));
}
/
* 使用Java 8 Stream API对基本类型int数组去重。
*
* @param intArray 待去重的int数组
* @return 去重后的新int数组
*/
public static int[] deduplicateIntArrayWithStream(int[] intArray) {
if (intArray == null || == 0) {
return intArray;
}
return (intArray).distinct().toArray();
}
public static void main(String[] args) {
Integer[] intArray = {1, 2, 3, 2, 4, 1, 5, 3};
Integer[] uniqueIntArray = deduplicateWithStream(intArray);
("使用Stream API去重 (Integer): " + (uniqueIntArray)); // [1, 2, 3, 4, 5]
String[] stringArray = {"apple", "banana", "orange", "apple", "grape", "banana"};
String[] uniqueStringArray = deduplicateWithStream(stringArray);
("使用Stream API去重 (String): " + (uniqueStringArray)); // [apple, banana, orange, grape]
int[] primitiveIntArray = {10, 20, 10, 30, 40, 20, 50};
int[] uniquePrimitiveIntArray = deduplicateIntArrayWithStream(primitiveIntArray);
("使用Stream API去重 (int[]): " + (uniquePrimitiveIntArray)); // [10, 20, 30, 40, 50]
}
}


方法五:针对已排序数组的双指针法(空间优化)


如果数组在去重前可以被排序,或者数组本身就是有序的,那么可以使用双指针法进行原地去重(或者以较小的额外空间)。这种方法尤其适用于基本类型数组,因为它避免了装箱拆箱的开销。


原理: 先将数组排序,使得重复元素相邻。然后使用两个指针,一个指向当前唯一元素的位置(`i`),另一个遍历整个数组(`j`)。如果`array[j]`与`array[i]`不同,则说明`array[j]`是一个新的唯一元素,将其放到`array[++i]`的位置。


优点:

空间复杂度低,可以实现原地去重(O(1)额外空间,如果排序是原地排序)。
时间复杂度为O(N log N)(主要开销在排序),去重本身是O(N)。


缺点:

会改变原数组的顺序。
如果数组未排序,需要额外的排序步骤。


代码示例:

import ;
public class ArrayDeduplicateSorted {
/
* 对已排序的数组进行去重,返回去重后的有效长度。
* 数组的前N个元素将是去重后的结果。
*
* @param arr 待去重的已排序数组
* @return 去重后数组的有效长度
*/
public static int deduplicateSortedArrayInPlace(int[] arr) {
if (arr == null || == 0) {
return 0;
}
int i = 0; // i指向当前唯一元素的最新位置
for (int j = 1; j < ; j++) { // j遍历整个数组
if (arr[j] != arr[i]) {
i++;
arr[i] = arr[j]; // 将新的唯一元素放到i的下一个位置
}
}
return i + 1; // 返回去重后数组的有效长度
}
/
* 对未排序的数组进行排序后去重,返回去重后的新数组。
*
* @param array 待去重的数组
* @return 去重后的新数组
*/
public static int[] deduplicateUnsortedArrayBySorting(int[] array) {
if (array == null || == 0) {
return array;
}
// 1. 排序数组
(array); // N log N
// 2. 使用双指针去重
int uniqueLength = deduplicateSortedArrayInPlace(array);
// 3. 截取有效部分,创建新数组返回
return (array, uniqueLength);
}
public static void main(String[] args) {
int[] intArray = {1, 2, 3, 2, 4, 1, 5, 3};
("原始数组 (int[]): " + (intArray));
int[] uniqueIntArray = deduplicateUnsortedArrayBySorting(intArray);
("排序后去重 (int[]): " + (uniqueIntArray)); // [1, 2, 3, 4, 5] (已排序)
Integer[] objArray = {1, 2, 3, 2, 4, 1, 5, 3};
(objArray); // 对对象数组同样适用,但要确保元素可比较
int i = 0;
for (int j = 1; j < ; j++) {
if (!objArray[j].equals(objArray[i])) { // 对于对象需要用equals()
i++;
objArray[i] = objArray[j];
}
}
Integer[] uniqueObjArray = (objArray, i + 1);
("排序后去重 (Integer[]): " + (uniqueObjArray));
}
}


三、对象去重:`equals()` 和 `hashCode()` 的重要性


对于自定义对象数组的去重,无论是使用`HashSet`、`LinkedHashSet`还是Stream API的`distinct()`,都强烈依赖于对象正确地实现了`equals()`和`hashCode()`方法。


原因:

`Set`集合(包括其子类)在判断两个对象是否“相同”时,首先会比较它们的`hashCode()`,如果`hashCode()`不同,则认为它们不相同;如果`hashCode()`相同,则进一步调用`equals()`方法进行比较。
`()`的内部实现通常也遵循此逻辑。


`equals()`和`hashCode()`的约定(合同):

如果两个对象通过`equals()`方法比较是相等的,那么它们的`hashCode()`值必须相等。
如果两个对象的`hashCode()`值相等,它们不一定通过`equals()`方法比较也相等(可能发生哈希冲突)。
如果两个对象通过`equals()`方法比较是不相等的,那么它们的`hashCode()`值可以相等也可以不相等,但为了效率,最好不相等(减少哈希冲突)。


示例:自定义对象去重

import ;
import ;
import ;
import ;
class Person {
private String name;
private int age;
public Person(String name, int age) {
= name;
= age;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
@Override
public String toString() {
return "Person{" + "name='" + name + '\'' + ", age=" + age + '}';
}
// 正确实现equals()和hashCode()
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != ()) return false;
Person person = (Person) o;
return age == && (name, );
}
@Override
public int hashCode() {
return (name, age);
}
}
public class ObjectDeduplicateExample {
public static void main(String[] args) {
Person[] people = {
new Person("Alice", 30),
new Person("Bob", 25),
new Person("Alice", 30), // 重复
new Person("Charlie", 35),
new Person("Bob", 25) // 重复
};
// 使用HashSet去重
Set<Person> uniquePeopleSet = new HashSet<>((people));
Person[] uniquePeopleArray = (new Person[0]);
("原始人员列表: " + (people));
("去重后人员列表: " + (uniquePeopleArray));
// 输出:[Person{name='Alice', age=30}, Person{name='Bob', age=25}, Person{name='Charlie', age=35}] (顺序可能不同)
// 使用Stream API去重
Person[] uniquePeopleStream = (people)
.distinct()
.toArray(Person[]::new);
("使用Stream API去重后人员列表: " + (uniquePeopleStream));
// 输出:[Person{name='Alice', age=30}, Person{name='Bob', age=25}, Person{name='Charlie', age=35}] (顺序保留)
}
}


如果不正确实现`equals()`和`hashCode()`,即使两个对象在逻辑上是相同的,`HashSet`或`distinct()`也可能将它们视为不同的对象,导致去重失败。


四、性能考量与选择建议


在选择去重方法时,应根据具体场景和需求进行权衡:

数据量小: 任何方法都可以,甚至手动循环加`()`的性能瓶颈也不明显。但为了代码风格和未来扩展性,依然推荐使用`HashSet`或Stream API。
数据量大且不关心顺序: `HashSet`是最佳选择。其平均时间复杂度为O(N),空间复杂度为O(N)。
数据量大且需要保留顺序: `LinkedHashSet`或Java 8 Stream API的`distinct()`是最佳选择。它们同样具有O(N)的平均时间复杂度,O(N)的空间复杂度。Stream API在代码简洁性上更胜一筹。
空间受限或追求极致空间效率: 如果数组可以被排序,且可以容忍原始顺序被破坏,那么先排序再使用双指针法是最佳选择。时间复杂度为O(N log N)(排序开销),空间复杂度为O(1)(原地去重)。
基本类型数组: Java 8 Stream API的`()`等可以直接处理基本类型,避免了装箱拆箱的额外开销。


五、总结


Java数组去重是一个经典问题,有多种解决方案。作为专业的程序员,我们不仅要了解如何实现去重,更要理解每种方法的底层原理、性能特点、空间消耗以及对数据顺序的影响,从而根据具体的业务场景和性能要求选择最合适的方案。在多数情况下,利用Java集合框架(尤其是`HashSet`或`LinkedHashSet`)或Java 8的Stream API,可以简洁高效地完成数组去重任务。对于自定义对象,务必正确实现`equals()`和`hashCode()`方法,以确保去重逻辑的正确性。

2025-10-25


上一篇:Java代码实现形状建模与图形绘制:从2D基础到3D进阶

下一篇:PLC数据与Java应用集成:工业智能化的关键桥梁