<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          一次List對象去重失敗,引發(fā)對Java8中distinct()的思考

          共 10838字,需瀏覽 22分鐘

           ·

          2020-12-29 06:46


          作者:puppylpg

          blog.csdn.net/puppylpg/article/details/78556730

          list的轉(zhuǎn)map的另一種猜想

          Java8使用lambda表達式進行函數(shù)式編程可以對集合進行非常方便的操作。一個比較常見的操作是將list轉(zhuǎn)換成map,一般使用Collectors的toMap()方法進行轉(zhuǎn)換。一個比較常見的問題是當list中含有相同元素的時候,如果不指定取哪一個,則會拋出異常。因此,這個指定是必須的。

          當然,使用toMap()的另一個重載方法,可以直接指定。這里,我們想討論的是另一種方法:在進行轉(zhuǎn)map的操作之前,能不能使用distinct()先把list的重復元素過濾掉,然后轉(zhuǎn)map的時候就不用考慮重復元素的問題了。

          使用distinct()給list去重

          直接使用distinct(),失敗

          package?example.mystream;

          import?lombok.AllArgsConstructor;
          import?lombok.Getter;
          import?lombok.NoArgsConstructor;
          import?lombok.ToString;

          import?java.util.Arrays;
          import?java.util.List;
          import?java.util.Map;
          import?java.util.stream.Collectors;

          public?class?ListToMap?{

          ????@AllArgsConstructor
          ????@NoArgsConstructor
          ????@ToString
          ????private?static?class?VideoInfo?{
          ????????@Getter
          ????????String?id;
          ????????int?width;
          ????????int?height;
          ????}

          ????public?static?void?main(String?[]?args)?{
          ????????List?list?=?Arrays.asList(new?VideoInfo("123",?1,?2),
          ????????????????new?VideoInfo("456",?4,?5),?new?VideoInfo("123",?1,?2));

          ????????//?preferred:?handle?duplicated?data?when?toMap()
          ????????Map?id2VideoInfo?=?list.stream().collect(
          ????????????????Collectors.toMap(VideoInfo::getId,?x?->?x,
          ????????????????????????(oldValue,?newValue)?->?newValue)
          ????????);

          ????????System.out.println("No?Duplicated1:?");
          ????????id2VideoInfo.forEach((x,?y)?->?System.out.println("<"?+?x?+?",?"?+?y?+?">"));

          ????????//?handle?duplicated?data?using?distinct(),?before?toMap()
          ????????Map?id2VideoInfo2?=?list.stream().distinct().collect(
          ????????????????Collectors.toMap(VideoInfo::getId,?x?->?x)
          ????????);

          ????????System.out.println("No?Duplicated2:?");
          ????????id2VideoInfo2.forEach((x,?y)?->?System.out.println("<"?+?x?+?",?"?+?y?+?">"));
          ????}
          }

          list里總共有三個元素,其中有兩個我們認為是重復的。第一種轉(zhuǎn)換是使用toMap()直接指定了對重復key的處理情況,因此可以正常轉(zhuǎn)換成map。而第二種轉(zhuǎn)換是想先對list進行去重,然后再轉(zhuǎn)換成map,結(jié)果還是失敗了,拋出了IllegalStateException,所以distinct()應該是失敗了。

          No?Duplicated1:?
          <123,?ListToMap.VideoInfo(id=123,?width=1,?height=2)>
          <456,?ListToMap.VideoInfo(id=456,?width=4,?height=5)>
          Exception?in?thread?"main"?java.lang.IllegalStateException:?Duplicate?key?ListToMap.VideoInfo(id=123,?width=1,?height=2)
          ????at?java.util.stream.Collectors.lambda$throwingMerger$0(Collectors.java:133)
          ????at?java.util.HashMap.merge(HashMap.java:1253)
          ????at?java.util.stream.Collectors.lambda$toMap$58(Collectors.java:1320)
          ????at?java.util.stream.ReduceOps$3ReducingSink.accept(ReduceOps.java:169)
          ????at?java.util.stream.DistinctOps$1$2.accept(DistinctOps.java:175)
          ????at?java.util.Spliterators$ArraySpliterator.forEachRemaining(Spliterators.java:948)
          ????at?java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:481)
          ????at?java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:471)
          ????at?java.util.stream.ReduceOps$ReduceOp.evaluateSequential(ReduceOps.java:708)
          ????at?java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
          ????at?java.util.stream.ReferencePipeline.collect(ReferencePipeline.java:499)
          ????at?example.mystream.ListToMap.main(ListToMap.java:79)

          原因:distinct()依賴于equals()

          查看distinct()的API,可以看到如下介紹:

          Returns a stream consisting of the distinct elements (according to {@link Object#equals(Object)}) of this stream.

          顯然,distinct()對對象進行去重時,是根據(jù)對象的equals()方法去處理的。如果我們的VideoInfo類不overrride超類Object的equals()方法,就會使用Object的。

          但是Object的equals()方法只有在兩個對象完全相同時才返回true。而我們想要的效果是只要VideoInfo的id/width/height均相同,就認為兩個videoInfo對象是同一個。所以我們比如重寫屬于videoInfo的equals()方法。相關(guān)文章:一次性搞清楚equals和hashCode

          重寫equals()的注意事項

          我們設計VideoInfo的equals()如下:

          @Override
          public?boolean?equals(Object?obj)?{
          ????if?(!(obj?instanceof?VideoInfo))?{
          ????????return?false;
          ????}
          ????VideoInfo?vi?=?(VideoInfo)?obj;
          ????return?this.id.equals(vi.id)
          ??????????&&?this.width?==?vi.width
          ??????????&&?this.height?==?vi.height;
          }

          這樣一來,只要兩個videoInfo對象的三個屬性都相同,這兩個對象就相同了。歡天喜地去運行程序,依舊失??!why?

          《Effective Java》是本好書,連Java之父James Gosling都說,這是一本連他都需要的Java教程。在這本書中,作者指出,如果重寫了一個類的equals()方法,那么就必須一起重寫它的hashCode()方法!必須!沒有商量的余地!

          必須使得重寫后的equals()滿足如下條件:

          • 根據(jù)equals()進行比較,相等的兩個對象,hashCode()的值也必須相同;
          • 根據(jù)equals()進行比較,不相等的兩個對象,hashCode()的值可以相同,也可以不同;

          因為這是Java的規(guī)定,違背這些規(guī)定將導致Java程序運行不再正常。

          具體更多的細節(jié),建議大家讀讀原書,必定獲益匪淺。強烈推薦!

          最終,我按照神書的指導設計VideoInfo的hashCode()方法如下:

          @Override
          public?int?hashCode()?{
          ???int?n?=?31;
          ???n?=?n?*?31?+?this.id.hashCode();
          ???n?=?n?*?31?+?this.height;
          ???n?=?n?*?31?+?this.width;
          ???return?n;
          }

          終于,distinct()成功過濾了list中的重復元素,此時使用兩種toMap()將list轉(zhuǎn)換成map都是沒問題的:

          No?Duplicated1:?
          <123,?ListToMap.VideoInfo(id=123,?width=1,?height=2)>
          <456,?ListToMap.VideoInfo(id=456,?width=4,?height=5)>
          No?Duplicated2:?
          <123,?ListToMap.VideoInfo(id=123,?width=1,?height=2)>
          <456,?ListToMap.VideoInfo(id=456,?width=4,?height=5)>

          引申

          既然說distinct()是調(diào)用equals()進行比較的,那按照我的理解,list的3個元素至少需要比較3次吧。那是不是就調(diào)用了3次equals()呢?

          在equals()中加入一句打印,這樣就可以知道了。加后的equals()如下:

          @Override?
          public?boolean?equals(Object?obj)?{
          ????if?(!?(obj?instanceof?VideoInfo))?{
          ????????return?false;
          ????}
          ????VideoInfo?vi?=?(VideoInfo)?obj;

          ????System.out.println("<===>?Invoke?equals()?==>?"?+?this.toString()?+?"?vs.?"?+?vi.toString());

          ????return?this.id.equals(vi.id)?&&?this.width?==?vi.width?&&?this.height?==?vi.height;
          }

          結(jié)果:

          No?Duplicated1:?
          <123,?ListToMap.VideoInfo(id=123,?width=1,?height=2)>
          <456,?ListToMap.VideoInfo(id=456,?width=4,?height=5)>
          <===>?Invoke?equals()?==>?ListToMap.VideoInfo(id=123,?width=1,?height=2)?vs.?ListToMap.VideoInfo(id=123,?width=1,?height=2)
          No?Duplicated2:?
          <123,?ListToMap.VideoInfo(id=123,?width=1,?height=2)>
          <456,?ListToMap.VideoInfo(id=456,?width=4,?height=5)>

          結(jié)果發(fā)現(xiàn)才調(diào)用了一次equals()。為什么不是3次呢?仔細想想,根據(jù)hashCode()進行比較,hashCode()相同的情況就一次,就是list的第一個元素和第三個元素(都是VideoInfo(id=123, width=1, height=2))會出現(xiàn)hashCode()相同的情況。

          所以我們是不是可以這么猜想:只有當hashCode()返回的hashCode相同的時候,才會調(diào)用equals()進行更進一步的判斷。如果連hashCode()返回的hashCode都不同,那么可以認為這兩個對象一定就是不同的了!

          驗證猜想:

          更改hashCode()如下:

          @Override
          public?int?hashCode()?{
          ???return?1;
          }

          這樣一來,所有的對象的hashCode()返回值都是相同的。當然,這樣搞是符合Java規(guī)范的,因為Java只規(guī)定equals()相同的對象的hashCode必須相同,但是不同的對象的hashCode未必會不同。

          結(jié)果:

          No?Duplicated1:?
          <123,?ListToMap.VideoInfo(id=123,?width=1,?height=2)>
          <456,?ListToMap.VideoInfo(id=456,?width=4,?height=5)>
          <===>?Invoke?equals()?==>?ListToMap.VideoInfo(id=456,?width=4,?height=5)?vs.?ListToMap.VideoInfo(id=123,?width=1,?height=2)
          <===>?Invoke?equals()?==>?ListToMap.VideoInfo(id=456,?width=4,?height=5)?vs.?ListToMap.VideoInfo(id=123,?width=1,?height=2)
          <===>?Invoke?equals()?==>?ListToMap.VideoInfo(id=123,?width=1,?height=2)?vs.?ListToMap.VideoInfo(id=123,?width=1,?height=2)
          No?Duplicated2:?
          <123,?ListToMap.VideoInfo(id=123,?width=1,?height=2)>
          <456,?ListToMap.VideoInfo(id=456,?width=4,?height=5)>

          果然,equals()調(diào)用了三次!看來的確只有hashCode相同的時候才會調(diào)用equal()進一步判斷兩個對象究竟是否相同;如果hashCode不相同,兩個對象顯然不相同。猜想是正確的。

          搜索Java知音公眾號,回復“后端面試”,送你一份Java面試題寶典.pdf

          結(jié)論

          1. list轉(zhuǎn)map推薦使用toMap(),并且無論是否會出現(xiàn)重復的問題,都要指定重復后的取舍規(guī)則,不費功夫但受益無窮;

          2. 對一個自定義的class使用distinct(),切記覆寫equals()方法;

          3. 覆寫equals(),一定要覆寫hashCode();

          4. 雖然設計出一個hashCode()可以簡單地讓其return 1,這樣并不會違反Java規(guī)定,但是這樣做會導致很多惡果。比如將這樣的對象存入hashMap的時候,所有的對象的hashCode都相同,最終所有對象都存儲在hashMap的同一個桶中,直接將hashMap惡化成了一個鏈表。從而O(1)的復雜度被整成了O(n)的,性能自然大大下降。

          5. 好書是程序猿進步的階梯?!郀柣?。比如《Effecctive Java》。

          最終參考程序:

          package?example.mystream;

          import?lombok.AllArgsConstructor;
          import?lombok.Getter;
          import?lombok.NoArgsConstructor;
          import?lombok.ToString;

          import?java.util.Arrays;
          import?java.util.List;
          import?java.util.Map;
          import?java.util.stream.Collectors;

          public?class?ListToMap?{

          ????@AllArgsConstructor
          ????@NoArgsConstructor
          ????@ToString
          ????private?static?class?VideoInfo?{
          ????????@Getter
          ????????String?id;
          ????????int?width;
          ????????int?height;

          ????????public?static?void?main(String?[]?args)?{
          ????????????System.out.println(new?VideoInfo("123",?1,?2).equals(new?VideoInfo("123",?1,?2)));
          ????????}

          ????????@Override
          ????????public?boolean?equals(Object?obj)?{
          ????????????if?(!(obj?instanceof?VideoInfo))?{
          ????????????????return?false;
          ????????????}
          ????????????VideoInfo?vi?=?(VideoInfo)?obj;
          ????????????return?this.id.equals(vi.id)
          ????????????????????&&?this.width?==?vi.width
          ????????????????????&&?this.height?==?vi.height;
          ????????}

          ????????/**
          ?????????*?If?equals()?is?override,?hashCode()?must?be?override,?too.
          ?????????*?1.?if?a?equals?b,?they?must?have?the?same?hashCode;
          ?????????*?2.?if?a?doesn't?equals?b,?they?may?have?the?same?hashCode;
          ?????????*?3.?hashCode?written?in?this?way?can?be?affected?by?sequence?of?the?fields;
          ?????????*?3.?2^5?-?1?=?31.?So?31?will?be?faster?when?do?the?multiplication,
          ?????????*??????because?it?can?be?replaced?by?bit-shifting:?31?*?i?=?(i?<?????????*?@return
          ?????????*/

          ????????@Override
          ????????public?int?hashCode()?{
          ????????????int?n?=?31;
          ????????????n?=?n?*?31?+?this.id.hashCode();
          ????????????n?=?n?*?31?+?this.height;
          ????????????n?=?n?*?31?+?this.width;
          ????????????return?n;
          ????????}
          ????}

          ????public?static?void?main(String?[]?args)?{
          ????????List?list?=?Arrays.asList(new?VideoInfo("123",?1,?2),
          ????????????????new?VideoInfo("456",?4,?5),?new?VideoInfo("123",?1,?2));

          ????????//?preferred:?handle?duplicated?data?when?toMap()
          ????????Map?id2VideoInfo?=?list.stream().collect(
          ????????????????Collectors.toMap(VideoInfo::getId,?x?->?x,
          ????????????????????????(oldValue,?newValue)?->?newValue)
          ????????);

          ????????System.out.println("No?Duplicated1:?");
          ????????id2VideoInfo.forEach((x,?y)?->?System.out.println("<"?+?x?+?",?"?+?y?+?">"));

          ????????//?handle?duplicated?data?using?distinct(),?before?toMap()
          ????????//?Note?that?distinct()?relies?on?equals()?in?the?object
          ????????//?if?you?override?equals(),?hashCode()?must?be?override?together
          ????????Map?id2VideoInfo2?=?list.stream().distinct().collect(
          ????????????????Collectors.toMap(VideoInfo::getId,?x?->?x)
          ????????);

          ????????System.out.println("No?Duplicated2:?");
          ????????id2VideoInfo2.forEach((x,?y)?->?System.out.println("<"?+?x?+?",?"?+?y?+?">"));
          ????}
          }

          再拓展

          假設類是別人的,不能修改

          以上,VideoInfo使我們自己寫的類,我們可以往里添加equals()和hashCode()方法。如果VideoInfo是我們引用的依賴中的一個類,我們無權(quán)對其進行修改,那么是不是就沒辦法使用distinct()按照某些元素是否相同,對對象進行自定義的過濾了呢?

          使用wrapper

          在stackoverflow的一個回答上,我們可以找到一個可行的方法:使用wrapper。

          假設在一個依賴中(我們無權(quán)修改該類),VideoInfo定義如下:

          @AllArgsConstructor
          @NoArgsConstructor
          @ToString
          public?class?VideoInfo?{
          ????@Getter
          ????String?id;
          ????int?width;
          ????int?height;
          }

          使用剛剛的wrapper思路,寫程序如下(當然,為了程序的可運行性,還是把VideoInfo放進來了,假設它就是不能修改的,不能為其添加任何方法):

          package?example.mystream;

          import?lombok.AllArgsConstructor;
          import?lombok.Getter;
          import?lombok.NoArgsConstructor;
          import?lombok.ToString;

          import?java.util.Arrays;
          import?java.util.List;
          import?java.util.Map;
          import?java.util.stream.Collectors;

          public?class?DistinctByWrapper?{

          ????private?static?class?VideoInfoWrapper?{

          ????????private?final?VideoInfo?videoInfo;

          ????????public?VideoInfoWrapper(VideoInfo?videoInfo)?{
          ????????????this.videoInfo?=?videoInfo;
          ????????}

          ????????public?VideoInfo?unwrap()?{
          ????????????return?videoInfo;
          ????????}

          ????????@Override
          ????????public?boolean?equals(Object?obj)?{
          ????????????if?(!(obj?instanceof?VideoInfo))?{
          ????????????????return?false;
          ????????????}
          ????????????VideoInfo?vi?=?(VideoInfo)?obj;
          ????????????return?videoInfo.id.equals(vi.id)
          ????????????????????&&?videoInfo.width?==?vi.width
          ????????????????????&&?videoInfo.height?==?vi.height;
          ????????}

          ????????@Override
          ????????public?int?hashCode()?{
          ????????????int?n?=?31;
          ????????????n?=?n?*?31?+?videoInfo.id.hashCode();
          ????????????n?=?n?*?31?+?videoInfo.height;
          ????????????n?=?n?*?31?+?videoInfo.width;
          ????????????return?n;
          ????????}
          ????}

          ????public?static?void?main(String?[]?args)?{
          ????????List?list?=?Arrays.asList(new?VideoInfo("123",?1,?2),
          ????????????????new?VideoInfo("456",?4,?5),?new?VideoInfo("123",?1,?2));

          ????????//?VideoInfo?--map()-->?VideoInfoWrapper?---->?distinct():?VideoInfoWrapper?--map()-->?VideoInfo
          ????????Map?id2VideoInfo?=?list.stream()
          ????????????????.map(VideoInfoWrapper::new).distinct().map(VideoInfoWrapper::unwrap)
          ????????????????.collect(
          ????????????????Collectors.toMap(VideoInfo::getId,?x?->?x,
          ????????????????????????(oldValue,?newValue)?->?newValue)
          ????????);

          ????????id2VideoInfo.forEach((x,?y)?->?System.out.println("<"?+?x?+?",?"?+?y?+?">"));
          ????}

          }

          /**
          ?*?Assume?that?VideoInfo?is?a?class?that?we?can't?modify
          ?*/

          @AllArgsConstructor
          @NoArgsConstructor
          @ToString
          class?VideoInfo?{
          ????@Getter
          ????String?id;
          ????int?width;
          ????int?height;
          }

          整個wrapper的思路無非就是構(gòu)造另一個類VideoInfoWrapper,把hashCode()和equals()添加到wrapper中,這樣便可以按照自定義規(guī)則對wrapper對象進行自定義的過濾。

          搜索Java知音公眾號,回復“后端面試”,送你一份Java面試題寶典.pdf

          我們沒法自定義過濾VideoInfo,但是我們可以自定義過濾VideoInfoWrapper??!

          之后要做的,就是將VideoInfo全部轉(zhuǎn)化為VideoInfoWrapper,然后過濾掉某些VideoInfoWrapper,再將剩下的VideoInfoWrapper轉(zhuǎn)回VideoInfo,以此達到過濾VideoInfo的目的。很巧妙!

          使用“filter() + 自定義函數(shù)”取代distinct()

          另一種更精妙的實現(xiàn)方式是自定義一個函數(shù):

          ????private?static??Predicate?distinctByKey(Functionsuper?T,?Object>?keyExtractor)?{
          ????????Map?map?=?new?ConcurrentHashMap<>();
          ????????return?t?->?map.putIfAbsent(keyExtractor.apply(t),?Boolean.TRUE)?==?null;
          ????}

          (輸入元素的類型是T及其父類,keyExtracctor是映射函數(shù),返回Object,整個傳入的函數(shù)的功能應該是提取key的。distinctByKey函數(shù)返回的是Predicate函數(shù),類型為T。)

          這個函數(shù)傳入一個函數(shù)(lambda),對傳入的對象提取key,然后嘗試將key放入concurrentHashMap,如果能放進去,說明此key之前沒出現(xiàn)過,函數(shù)返回false;如果不能放進去,說明這個key和之前的某個key重復了,函數(shù)返回true。

          這個函數(shù)最終作為filter()函數(shù)的入?yún)ⅰ8鶕?jù)Java API可知filter(func)過濾的規(guī)則為:如果func為true,則過濾,否則不過濾。因此,通過“filter() + 自定義的函數(shù)”,凡是重復的key都返回true,并被filter()過濾掉,最終留下的都是不重復的。

          最終實現(xiàn)的程序如下

          package?example.mystream;

          import?lombok.AllArgsConstructor;
          import?lombok.Getter;
          import?lombok.NoArgsConstructor;
          import?lombok.ToString;

          import?java.util.Arrays;
          import?java.util.List;
          import?java.util.Map;
          import?java.util.concurrent.ConcurrentHashMap;
          import?java.util.function.Function;
          import?java.util.function.Predicate;
          import?java.util.stream.Collectors;


          public?class?DistinctByFilterAndLambda?{

          ????public?static?void?main(String[]?args)?{
          ????????List?list?=?Arrays.asList(new?VideoInfo("123",?1,?2),
          ????????????????new?VideoInfo("456",?4,?5),?new?VideoInfo("123",?1,?2));

          ????????//?Get?distinct?only
          ????????Map?id2VideoInfo?=?list.stream().filter(distinctByKey(vi?->?vi.getId())).collect(
          ????????????????Collectors.toMap(VideoInfo::getId,?x?->?x,
          ????????????????????????(oldValue,?newValue)?->?newValue)
          ????????);

          ????????id2VideoInfo.forEach((x,?y)?->?System.out.println("<"?+?x?+?",?"?+?y?+?">"));
          ????}

          ????/**
          ?????*?If?a?key?could?not?be?put?into?ConcurrentHashMap,?that?means?the?key?is?duplicated
          ?????*?@param?keyExtractor?a?mapping?function?to?produce?keys
          ?????*?@param??the?type?of?the?input?elements
          ?????*?@return?true?if?key?is?duplicated;?else?return?false
          ?????*/

          ????private?static??Predicate?distinctByKey(Functionsuper?T,?Object>?keyExtractor)?{
          ????????Map?map?=?new?ConcurrentHashMap<>();
          ????????return?t?->?map.putIfAbsent(keyExtractor.apply(t),?Boolean.TRUE)?==?null;
          ????}
          }

          /**
          ?*?Assume?that?VideoInfo?is?a?class?that?we?can't?modify
          ?*/

          @AllArgsConstructor
          @NoArgsConstructor
          @ToString
          class?VideoInfo?{
          ????@Getter
          ????String?id;
          ????int?width;
          ????int?height;
          }

          更多好文章

          1. Java高并發(fā)系列(共34篇)
          2. MySql高手系列(共27篇)
          3. Maven高手系列(共10篇)
          4. Mybatis系列(共12篇)
          5. 聊聊db和緩存一致性常見的實現(xiàn)方式
          6. 接口冪等性這么重要,它是什么?怎么實現(xiàn)?
          7. 泛型,有點難度,會讓很多人懵逼,那是因為你沒有看這篇文章!

          ?

          瀏覽 67
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  欧美一级在线免费 | 在线观看免费成人网站 | 99久久婷婷国产精品2020 | 亚洲三级视频网站 | 91ThePorn国产在线观看 |