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

list的轉map的另外一種猜測

Java8使用lambda表達式進行函數式編程能夠對集合進行很是方便的操做。一個比較常見的操做是將list轉換成map,通常使用Collectors的toMap()方法進行轉換。一個比較常見的問題是當list中含有相同元素的時候,若是不指定取哪個,則會拋出異常。所以,這個指定是必須的。Java面試寶典PDF完整版java

固然,使用toMap()的另外一個重載方法,能夠直接指定。這裏,咱們想討論的是另外一種方法:在進行轉map的操做以前,能不能使用distinct()先把list的重複元素過濾掉,而後轉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<VideoInfo> 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<String, VideoInfo> 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<String, VideoInfo> 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裏總共有三個元素,其中有兩個咱們認爲是重複的。第一種轉換是使用toMap()直接指定了對重複key的處理狀況,所以能夠正常轉換成map。而第二種轉換是想先對list進行去重,而後再轉換成map,結果仍是失敗了,拋出了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.app

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

可是Object的equals()方法只有在兩個對象徹底相同時才返回true。而咱們想要的效果是隻要VideoInfo的id/width/height均相同,就認爲兩個videoInfo對象是同一個。因此咱們好比重寫屬於videoInfo的equals()方法。函數式編程

重寫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()方法!必須!沒有商量的餘地!this

必須使得重寫後的equals()知足以下條件:

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

  • 根據equals()進行比較,不相等的兩個對象,hashCode()的值能夠相同,也能夠不一樣;

由於這是Java的規定,違背這些規定將致使Java程序運行再也不正常。

具體更多的細節,建議你們讀讀原書,一定獲益匪淺。強烈推薦!

最終,我按照神書的指導設計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轉換成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()是調用equals()進行比較的,那按照個人理解,list的3個元素至少須要比較3次吧。那是否是就調用了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;
}

結果:

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)>

結果發現才調用了一次equals()。爲何不是3次呢?仔細想一想,根據hashCode()進行比較,hashCode()相同的狀況就一次,就是list的第一個元素和第三個元素(都是VideoInfo(id=123, width=1, height=2))會出現hashCode()相同的狀況。

因此咱們是否是能夠這麼猜測:只有當hashCode()返回的hashCode相同的時候,纔會調用equals()進行更進一步的判斷。若是連hashCode()返回的hashCode都不一樣,那麼能夠認爲這兩個對象必定就是不一樣的了!

驗證猜測:

更改hashCode()以下:

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

這樣一來,全部的對象的hashCode()返回值都是相同的。固然,這樣搞是符合Java規範的,由於Java只規定equals()相同的對象的hashCode必須相同,可是不一樣的對象的hashCode未必會不一樣。

結果:

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()調用了三次!看來的確只有hashCode相同的時候纔會調用equal()進一步判斷兩個對象到底是否相同;若是hashCode不相同,兩個對象顯然不相同。猜測是正確的。

結論

  1. list轉map推薦使用toMap(),而且不管是否會出現重複的問題,都要指定重複後的取捨規則,不費功夫但受益無窮;

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

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

  4. 雖然設計出一個hashCode()能夠簡單地讓其return 1,這樣並不會違反Java規定,可是這樣作會致使不少惡果。好比將這樣的對象存入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 << 5) - 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<VideoInfo> 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<String, VideoInfo> 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<String, VideoInfo> 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是咱們引用的依賴中的一個類,咱們無權對其進行修改,那麼是否是就沒辦法使用distinct()按照某些元素是否相同,對對象進行自定義的過濾了呢?

使用wrapper

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

假設在一個依賴中(咱們無權修改該類),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<VideoInfo> 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<String, VideoInfo> 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的思路無非就是構造另外一個類VideoInfoWrapper,把hashCode()和equals()添加到wrapper中,這樣即可以按照自定義規則對wrapper對象進行自定義的過濾。

搜索Java知音公衆號,回覆「後端面試」,送你一份Java面試題寶典.pdf

咱們無法自定義過濾VideoInfo,可是咱們能夠自定義過濾VideoInfoWrapper啊!

以後要作的,就是將VideoInfo所有轉化爲VideoInfoWrapper,而後過濾掉某些VideoInfoWrapper,再將剩下的VideoInfoWrapper轉回VideoInfo,以此達到過濾VideoInfo的目的。很巧妙!

使用「filter() + 自定義函數」取代distinct()

另外一種更精妙的實現方式是自定義一個函數:

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

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

這個函數傳入一個函數(lambda),對傳入的對象提取key,而後嘗試將key放入concurrentHashMap,若是能放進去,說明此key以前沒出現過,函數返回false;若是不能放進去,說明這個key和以前的某個key重複了,函數返回true。

這個函數最終做爲filter()函數的入參。根據Java API可知filter(func)過濾的規則爲:若是func爲true,則過濾,不然不過濾。所以,經過「filter() + 自定義的函數」,凡是重複的key都返回true,並被filter()過濾掉,最終留下的都是不重複的。Java面試寶典PDF完整版

最終實現的程序以下

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<VideoInfo> list = Arrays.asList(new VideoInfo("123", 1, 2),
                new VideoInfo("456", 4, 5), new VideoInfo("123", 1, 2));

        // Get distinct only
        Map<String, VideoInfo> 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 <T> the type of the input elements
     * @return true if key is duplicated; else return false
     */
    private static <T> Predicate<T> distinctByKey(Function<? super T, Object> keyExtractor) {
        Map<Object, Boolean> 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;
}
相關文章
相關標籤/搜索