java8列表中通過(guò)stream流根據(jù)對(duì)象屬性去重的三種方式
一、簡(jiǎn)單去重
public class DistinctTest { /** * 沒(méi)有重寫(xiě) equals 方法 */ @Setter @Getter @ToString @AllArgsConstructor @NoArgsConstructor public static class User { private String name; private Integer age; } /** * lombok(@Data) 重寫(xiě)了 equals 方法 和 hashCode 方法 */ @Data @AllArgsConstructor @NoArgsConstructor public static class User2 { private String name; private Integer age; } @Test public void easyTest() { List<Integer> integers = Arrays.asList(1, 1, 2, 3, 4, 4, 5, 6, 77, 77); System.out.println("======== 數(shù)字去重 ========="); System.out.print("原數(shù)字列表:"); integers.forEach(x -> System.out.print(x + " ")); System.out.println(); System.out.print("去重后數(shù)字列表:"); integers.stream().distinct().collect(Collectors.toList()).forEach(x -> System.out.print(x + " ")); System.out.println(); System.out.println(); List<User> list = Lists.newArrayList(); User three = new User("張三", 18); User three2 = new User("張三", 18); User three3 = new User("張三", 24); User four = new User("李四", 18); list.add(three); list.add(three); list.add(three2); list.add(three3); list.add(four); System.out.println("======== 沒(méi)有重寫(xiě)equals方法的話,只能對(duì)相同對(duì)象(如:three)進(jìn)行去重,不能做到元素相同就可以去重) ========="); // 沒(méi)有重寫(xiě) equals 方法時(shí),使用的是超類 Object 的 equals 方法 // 等價(jià)于兩個(gè)對(duì)象 == 的比較,只能篩選同一個(gè)對(duì)象 System.out.println("初始對(duì)象列表:"); list.forEach(System.out::println); System.out.println("簡(jiǎn)單去重后初始對(duì)象列表:"); list.stream().distinct().collect(Collectors.toList()).forEach(System.out::println); System.out.println(); System.out.println(); List<User2> list2 = Lists.newArrayList(); User2 five = new User2("王五", 18); User2 five2 = new User2("王五", 18); User2 five3 = new User2("王五", 24); User2 two = new User2("二蛋", 18); list2.add(five); list2.add(five); list2.add(five2); list2.add(five3); list2.add(two); System.out.println("======== 重寫(xiě)了equals方法的話,可以做到元素相同就可以去重) ========="); // 所以如果只需要寫(xiě)好 equals 方法 和 hashCode 方法 也能做到指定屬性的去重 System.out.println("初始對(duì)象列表:"); list2.forEach(System.out::println); System.out.println("簡(jiǎn)單去重后初始對(duì)象列表:"); list2.stream().distinct().collect(Collectors.toList()).forEach(System.out::println); } }
二、根據(jù)對(duì)象某個(gè)屬性去重
0、User對(duì)象
/** * 沒(méi)有重寫(xiě) equals 方法 */ @Setter @Getter @ToString @AllArgsConstructor @NoArgsConstructor public static class User { private String name; private Integer age; }
1、使用filter進(jìn)行去重
@Test public void objectTest() { List<User> list = Arrays.asList( new User(null, 18), new User("張三", null), null, new User("張三", 24), new User("張三5", 24), new User("李四", 18) ); System.out.println("初始對(duì)象列表:"); list.forEach(System.out::println); System.out.println(); System.out.println("======== 使用 filter ,根據(jù)特定屬性進(jìn)行過(guò)濾(重不重寫(xiě)equals方法都不重要) ========="); System.out.println("根據(jù)名字過(guò)濾后的對(duì)象列表:"); // 第一個(gè) filter 是用于過(guò)濾 第二個(gè) filter 是用于去重 List<User> collect = list.stream().filter(o -> o != null && o.getName() != null) .filter(distinctPredicate(User::getName)).collect(Collectors.toList()); collect.forEach(System.out::println); System.out.println("根據(jù)年齡過(guò)濾后的對(duì)象列表:"); List<User> collect1 = list.stream().filter(o -> o != null && o.getAge() != null) .filter(distinctPredicate(User::getAge)).collect(Collectors.toList()); collect1.forEach(System.out::println); } /** * 列表對(duì)象去重 */ public <K, T> Predicate<K> distinctPredicate(Function<K, T> function) { // 因?yàn)閟tream流是多線程操作所以需要使用線程安全的ConcurrentHashMap ConcurrentHashMap<T, Boolean> map = new ConcurrentHashMap<>(); return t -> null == map.putIfAbsent(function.apply(t), true); }
測(cè)試
①、疑惑
- 既然 filter 里面調(diào)用的是 distinctPredicate 方法,而該方法每次都 new 一個(gè)新的 map 對(duì)象,那么 map 就是新的,怎么能做到可以過(guò)濾呢
②、解惑
- 先看一下 filter 的部分實(shí)現(xiàn)邏輯,他使用了函數(shù)式接口 Predicate ,每次調(diào)用filter時(shí),會(huì)使用 predicate 對(duì)象的 test 方法,這個(gè)對(duì)象的test 方法就是 null == map.putIfAbsent(function.apply(t), true)
- 而 distinctPredicate 方法作用就是生成了一個(gè)線程安全的 Map 集合,和一個(gè) predicate 對(duì)象,且該對(duì)象的 test 方法為 null == map.putIfAbsent(function.apply(t), true)
- 之后 stream 流的 filter 方法每次都只會(huì)使用 predicate 對(duì)象的 test 方法,而該 test 方法中的 map 對(duì)象在該流中是唯一的,并不會(huì)重新初始化
@Override public final Stream<P_OUT> filter(Predicate<? super P_OUT> predicate) { Objects.requireNonNull(predicate); return new StatelessOp<P_OUT, P_OUT>(this, StreamShape.REFERENCE, StreamOpFlag.NOT_SIZED) { @Override Sink<P_OUT> opWrapSink(int flags, Sink<P_OUT> sink) { return new Sink.ChainedReference<P_OUT, P_OUT>(sink) { @Override public void begin(long size) { downstream.begin(-1); } @Override public void accept(P_OUT u) { if (predicate.test(u)) downstream.accept(u); } }; } }; }
2、使用Collectors.toMap() 實(shí)現(xiàn)根據(jù)某一屬性去重(這個(gè)可以實(shí)現(xiàn)保留前一個(gè)還是后一個(gè))
要注意 Collectors.toMap(key,value) 中 value 不能為空,會(huì)報(bào)錯(cuò),key 可以為 null,但會(huì)被轉(zhuǎn)換為字符串的 “null”
@Test public void objectTest() { List<User> list = Arrays.asList( new User(null, 18), new User("張三", null), null, new User("張三", 24), new User("張三5", 24), new User("李四", 18) ); System.out.println("初始對(duì)象列表:"); list.forEach(System.out::println); System.out.println(); System.out.println("======== 使用 Collectors.toMap() 實(shí)現(xiàn)根據(jù)某一屬性去重 ========="); System.out.println("根據(jù)名字過(guò)濾后的對(duì)象列表 寫(xiě)法1:"); // (v1, v2) -> v1 的意思 兩個(gè)名字一樣的話(key一樣),存前一個(gè) value 值 Map<String, User> collect = list.stream().filter(Objects::nonNull).collect(Collectors.toMap(User::getName, o -> o, (v1, v2) -> v1)); // o -> o 也可以寫(xiě)為 Function.identity() ,兩個(gè)是一樣的,但后者可能比較優(yōu)雅,但閱讀性不高,如下 // Map<String, User> collect = list.stream().filter(Objects::nonNull).collect(Collectors.toMap(User::getName, Function.identity(), (v1, v2) -> v1)); List<User> list2 = new ArrayList<>(collect.values()); list2.forEach(System.out::println); System.out.println("根據(jù)名字過(guò)濾后的對(duì)象列表 寫(xiě)法2:"); Map<String, User> map2 = list.stream().filter(o -> o != null && o.getName() != null) .collect(HashMap::new, (m, o) -> m.put(o.getName(), o), HashMap::putAll); list2 = new ArrayList<>(map2.values()); list2.forEach(System.out::println); System.out.println("根據(jù)年齡過(guò)濾后的對(duì)象列表:"); // (v1, k2) -> v2 的意思 兩個(gè)年齡一樣的話(key一樣),存后一個(gè) value 值 Map<Integer, User> collect2 = list.stream().filter(Objects::nonNull).collect(Collectors.toMap(User::getAge, o -> o, (v1, v2) -> v2)); list2 = new ArrayList<>(collect2.values()); list2.forEach(System.out::println); }
測(cè)試
3、Collectors.toMap() 的變種 使用 Collectors.collectingAndThen()
Collectors.collectingAndThen()
函數(shù) 它可接受兩個(gè)參數(shù),第一個(gè)參數(shù)用于 reduce
操作,而第二參數(shù)用于 map
操作。
也就是,先把流中的所有元素傳遞給第一個(gè)參數(shù),然后把生成的集合傳遞給第二個(gè)參數(shù)來(lái)處理。
@Test public void objectTest() { List<User> list = Arrays.asList( new User(null, 18), new User("張三", null), null, new User("張三", 24), new User("張三5", 24), new User("李四", 18) ); System.out.println("初始對(duì)象列表:"); list.forEach(System.out::println); System.out.println(); System.out.println("======== 使用 Collectors.toMap() 實(shí)現(xiàn)根據(jù)某一屬性去重 ========="); System.out.println("根據(jù)名字過(guò)濾后的對(duì)象列表:"); ArrayList<User> collect1 = list.stream().filter(o -> o != null && o.getName() != null).collect( Collectors.collectingAndThen(Collectors.toMap(User::getName, o -> o, (k1, k2) -> k2), x-> new ArrayList<>(x.values()))); collect1.forEach(System.out::println); System.out.println("======== 或者 =========="); List<User> collect = list.stream().filter(o -> o != null && o.getName() != null).collect( Collectors.collectingAndThen(Collectors.toCollection( () -> new TreeSet<>(Comparator.comparing(User::getName))), ArrayList<User>::new)); collect.forEach(System.out::println); }
測(cè)試
三、測(cè)試哪個(gè)方法比較快
@Test public void objectTest() { List<User> list = new ArrayList<>(Arrays.asList( new User(null, 18), new User("張三", null), null, new User("張三", 24), new User("張三5", 24), new User("李四", 18) )); for (int i = 0; i < 100000; i++) { list.add(new User((Math.random() * 10) + "", (int) (Math.random() * 10))); } System.out.println("======== 測(cè)試速度 ========="); long startTime = System.currentTimeMillis(); List<User> list1 = list.stream().filter(o -> o != null && o.getName() != null) .filter(distinctPredicate(User::getName)).collect(Collectors.toList()); long endTime = System.currentTimeMillis(); System.out.println("filter 用時(shí) :" + (endTime - startTime)); System.out.println(); startTime = System.currentTimeMillis(); Map<String, User> map1 = list.stream().filter(o -> o != null && o.getName() != null) .collect(Collectors.toMap(User::getName, o -> o, (v1, v2) -> v1)); List<User> list2 = new ArrayList<>(map1.values()); endTime = System.currentTimeMillis(); System.out.println("map1 用時(shí) :" + (endTime - startTime)); System.out.println(); startTime = System.currentTimeMillis(); ArrayList<User> list3 = list.stream().filter(o -> o != null && o.getName() != null).collect( Collectors.collectingAndThen(Collectors.toMap(User::getName, o -> o, (k1, k2) -> k2), x -> new ArrayList<>(x.values()))); endTime = System.currentTimeMillis(); System.out.println("map2 用時(shí) :" + (endTime - startTime)); System.out.println(); startTime = System.currentTimeMillis(); List<User> list4 = list.stream().filter(o -> o != null && o.getName() != null).collect( Collectors.collectingAndThen(Collectors.toCollection( () -> new TreeSet<>(Comparator.comparing(User::getName))), ArrayList<User>::new)); endTime = System.currentTimeMillis(); System.out.println("map3 用時(shí) :" + (endTime - startTime)); System.out.println(); startTime = System.currentTimeMillis(); Map<String, User> map2 = list.stream().filter(o -> o != null && o.getName() != null) .collect(HashMap::new, (m, o) -> m.put(o.getName(), o), HashMap::putAll); List<User> list5 = new ArrayList<>(map2.values()); endTime = System.currentTimeMillis(); System.out.println("map4 用時(shí) :" + (endTime - startTime)); }
測(cè)試:
總結(jié)
1、去重最快
ArrayList<User> list3 = list.stream().filter(o -> o != null && o.getName() != null).collect( Collectors.collectingAndThen(Collectors.toMap(User::getName, o -> o, (k1, k2) -> k2), x -> new ArrayList<>(x.values()))); // 或者 Map<String, User> map2 = list.stream().filter(o -> o != null && o.getName() != null) .collect(HashMap::new, (m, o) -> m.put(o.getName(), o), HashMap::putAll); List<User> list5 = new ArrayList<>(map2.values());
2、其次
Map<String, User> map1 = list.stream().filter(o -> o != null && o.getName() != null) .collect(Collectors.toMap(User::getName, o -> o, (v1, v2) -> v1)); List<User> list2 = new ArrayList<>(map1.values()); // distinctPredicate 是一個(gè)方法 本文中有 ,可以 ctrl + f 查找 List<User> list1 = list.stream().filter(o -> o != null && o.getName() != null) .filter(distinctPredicate(User::getName)).collect(Collectors.toList());
3、最慢
List<User> list4 = list.stream().filter(o -> o != null && o.getName() != null).collect( Collectors.collectingAndThen(Collectors.toCollection( () -> new TreeSet<>(Comparator.comparing(User::getName))), ArrayList<User>::new));
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
Java springboot Mongodb增刪改查代碼實(shí)例
這篇文章主要介紹了Java springboot Mongodb增刪改查代碼實(shí)例,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-07-07Mybatis-Plus通過(guò)SQL注入器實(shí)現(xiàn)批量插入的實(shí)踐
本文主要介紹了Mybatis-Plus通過(guò)SQL注入器實(shí)現(xiàn)批量插入的實(shí)踐,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2022-08-08Java中的線程同步與ThreadLocal無(wú)鎖化線程封閉實(shí)現(xiàn)
這篇文章主要介紹了Java中的線程同步與ThreadLocal無(wú)鎖化線程封閉實(shí)現(xiàn),Synchronized關(guān)鍵字與ThreadLocal變量的使用是Java中線程控制的基礎(chǔ),需要的朋友可以參考下2016-03-03SpringBoot使用@Validated處理校驗(yàn)的方法步驟
@Validated?注解的主要目的是啟用和利用?Spring?的驗(yàn)證框架,它可以用于類上也可以用于方法參數(shù)上,本文給大家介紹了SpringBoot使用@Validated優(yōu)雅的處理校驗(yàn)的方法步驟,通過(guò)代碼示例介紹的非常詳細(xì),需要的朋友可以參考下2024-08-08Java正則驗(yàn)證正整數(shù)的方法分析【測(cè)試可用】
這篇文章主要介紹了Java正則驗(yàn)證正整數(shù)的方法,結(jié)合實(shí)例形式對(duì)比分析了java針對(duì)正整數(shù)的驗(yàn)證方法及相關(guān)注意事項(xiàng),需要的朋友可以參考下2017-08-08Java基礎(chǔ)入門(mén)總結(jié)之序列化和反序列化
序列化是一種對(duì)象持久化的手段,普遍應(yīng)用在網(wǎng)絡(luò)傳輸、RMI等場(chǎng)景中,下面這篇文章主要給大家介紹了關(guān)于Java基礎(chǔ)入門(mén)總結(jié)之序列化和反序列化的相關(guān)資料,需要的朋友可以參考下2022-02-02idea快速實(shí)現(xiàn)將SpringBoot項(xiàng)目打包Docker鏡像并部署
本文主要介紹了idea快速實(shí)現(xiàn)將SpringBoot項(xiàng)目打包Docker鏡像并部署,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2022-04-04解決springboot項(xiàng)目啟動(dòng)失敗Could not initialize class&
這篇文章主要介紹了解決springboot項(xiàng)目啟動(dòng)失敗Could not initialize class com.fasterxml.jackson.databind.ObjectMapper問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-06-06