業務常見需求:List 中物件某屬性值重複時,只保留首個/末個。本文對比三種去重策略:HashSet 全物件去重、雙層循環逆序刪除、LinkedHashMap 屬性去重 (保留順序、O(n)),並給出生產環境推薦方案。
class Network {
int id;
String destIp;
// getter/setter
}
List list = Arrays.asList(
new Network(1, "192.168.1.3"),
new Network(2, "192.168.1.5"),
new Network(3, "192.168.1.3"), // destIp 重複
new Network(4, "192.168.1.4"),
new Network(5, "192.168.1.3") // destIp 重複
);
// 目標:按 destIp 去重,保留首次出現 → id=1,2,4
// 僅當整個物件完全相等 (equals/hashCode 一致) 才去重
Set set = new HashSet<>(list);
list.clear();
list.addAll(set);
❌ 無法按單一屬性去重。
// 從後向前遍歷,避免刪除導致索引錯亂
for (int i = 0; i < list.size() - 1; i++) {
for (int j = list.size() - 1; j > i; j--) {
if (list.get(j).getDestIp().equals(list.get(i).getDestIp())) {
list.remove(j);
}
}
}
| 優點 | 缺點 |
|---|---|
| 原地修改、無額外空間 | O(n²) 性能差、代碼易出錯、併發不安全 |
public static List distinctByProperty(List list, Function keyMapper) {
if (list == null || list.isEmpty()) return list;
Map map = new LinkedHashMap<>(list.size() * 2);
for (T item : list) {
K key = keyMapper.apply(item);
// putIfAbsent 保留首次出現;若要保留最後出現用 put
map.putIfAbsent(key, item);
}
return new ArrayList<>(map.values());
}
// 使用
List distinct = distinctByProperty(list, Network::getDestIp);
// 結果:id=1,2,4 (保留首次出現順序)
// 缺點:distinct() 依賴 equals/hashCode,無法指定屬性
// 變通:先分組再取首個 (但順序不穩定)
List distinct = list.stream()
.collect(Collectors.collectingAndThen(
Collectors.toCollection(() -> new TreeSet<>(Comparator.comparing(Network::getDestIp))),
ArrayList::new
));
// 正確保留順序的 Stream 寫法 (較繁瑣)
Map map = list.stream()
.collect(Collectors.toMap(
Network::getDestIp,
n -> n,
(existing, replacement) -> existing, // 衝突保留首個
LinkedHashMap::new
));
List result = new ArrayList<>(map.values());
| 方法 | 耗時 (ms) | 記憶體 | 備註 |
|---|---|---|---|
| 雙層循環 | ~4500 | 原地 | O(n²) 不可用 |
| LinkedHashMap | ~15 | O(n) | **推薦** |
| Stream toMap | ~25 | O(n) | 略慢於手寫 |
| Guava Streams.stream().distinctBy() | ~18 | O(n) | 需引入 Guava |
import com.google.common.collect.Streams;
List distinct = Streams.stream(list)
.distinctBy(Network::getDestIp)
.collect(Collectors.toList());
// 併發安全:用 ConcurrentHashMap (不保留插入順序) 或加鎖
public static List distinctConcurrent(List list, Function keyMapper) {
Map map = new ConcurrentHashMap<>();
list.forEach(item -> map.putIfAbsent(keyMapper.apply(item), item));
return new ArrayList<>(map.values()); // 順序不可預期
}
// 併發 + 保序:複製列表後單線程處理,或用 CopyOnWriteArrayList
List snapshot = new ArrayList<>(concurrentList);
return distinctByProperty(snapshot, Network::getDestIp);
keyMapper 返回 null 如何處理?建議過濾或拋異常。putIfAbsent (首個) 或 put (最後)。DISTINCT ON。new HashSet<>(list) (需正確實現 equals/hashCode)。LinkedHashMap.putIfAbsent 封裝為通用工具方法**。Streams.stream(list).distinctBy(keyMapper) 最簡潔。Collectors.toMap(keyMapper, v->v, (a,b)->a, LinkedHashMap::new)。