集合去重是高頻業務需求。核心在於**正確實現 `equals`/`hashCode` 契約**,配合 `Set` 特性或 Stream API 實現高效去重。本文系統闡述去重原理、5 大方案對比、效能測試與最佳實踐。
public class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
// Getter...
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Person person = (Person) o;
return age == person.age && Objects.equals(name, person.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
}
List list = Arrays.asList(
new Person("Alice", 20), new Person("Bob", 25),
new Person("Alice", 20), new Person("Charlie", 30)
);
Set set = new HashSet<>(list); // 去重
List distinct = new ArrayList<>(set); // 轉回 List
List distinct = new ArrayList<>(new LinkedHashSet<>(list));
// 保持首次出現順序
List distinct = list.stream().distinct().collect(Collectors.toList());
// 內部用 LinkedHashSet 實現,保序
// Person 需實現 Comparable 或提供 Comparator
List distinct = new ArrayList<>(new TreeSet<>(list)); // 按自然序/比較器排序
public static List distinctManual(List list) {
List result = new ArrayList<>();
for (T item : list) {
if (!result.contains(item)) { // O(n²) 效能差
result.add(item);
}
}
return result;
}
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
@State(Scope.Thread)
public class DistinctBenchmark {
@Param({"100", "1000", "10000", "100000"})
int size;
List list;
@Setup
public void setup() {
list = new ArrayList<>(size);
for (int i = 0; i < size; i++) {
list.add(new Person("Name" + (i % (size / 10)), i % 100)); // 10% 重複率
}
}
@Benchmark public List hashSet() { return new ArrayList<>(new HashSet<>(list)); }
@Benchmark public List linkedHashSet() { return new ArrayList<>(new LinkedHashSet<>(list)); }
@Benchmark public List stream() { return list.stream().distinct().collect(Collectors.toList()); }
@Benchmark public List treeSet() { return new ArrayList<>(new TreeSet<>(list)); }
/* 典型結果(微秒):
size=100 hashSet: 15 linkedHashSet: 18 stream: 20 treeSet: 45
size=1000 hashSet: 120 linkedHashSet: 140 stream: 160 treeSet: 450
size=10000 hashSet: 1.2ms linkedHashSet: 1.4ms stream: 1.6ms treeSet: 5ms
size=100000 hashSet: 12ms linkedHashSet: 14ms stream: 16ms treeSet: 55ms
*/
}
// 方案 1:Stream + 狀態收集器
List distinctById = list.stream()
.collect(Collectors.collectingAndThen(
Collectors.toCollection(() -> new TreeSet<>(Comparator.comparing(Person::getId))),
ArrayList::new
));
// 方案 2:自定義 Predicate(線程不安全,單執行緒可用)
Set seenIds = ConcurrentHashMap.newKeySet();
List distinct = list.stream()
.filter(p -> seenIds.add(p.getId()))
.collect(Collectors.toList());
int[] arr = {1, 2, 3, 2, 4, 1, 5};
// 方案 1:排序後相鄰去重
Arrays.sort(arr);
int[] distinct = IntStream.of(arr).distinct().toArray(); // Stream 需裝箱
// 方案 2:Bitset(值域小,如 0-1000)
BitSet bitSet = new BitSet();
for (int v : arr) bitSet.set(v);
int[] distinct = new int[bitSet.cardinality()];
int idx = 0;
for (int i = bitSet.nextSetBit(0); i >= 0; i = bitSet.nextSetBit(i + 1)) {
distinct[idx++] = i;
}
// 併發安全 Set
Set concurrentSet = ConcurrentHashMap.newKeySet();
// 或
Set copyOnWriteSet = new CopyOnWriteArraySet<>(); // 讀多寫少
**正確實現 `equals`/`hashCode` 是去重前提**。通用場景**首選 `LinkedHashSet` 或 `Stream.distinct()`**,兼顧效能與保序。按屬性去重用 `TreeSet` + 比較器或 `filter` + `seenSet`。大數據量下游至資料庫層或流式處理框架。理解 `Set` 底層雜湊表機制、雜湊碰撞處理、紅黑樹排序特性,才能在複雜業務中游刃有餘。