一次List对象去重失败,引发对Java8中distinct()的思考
作者:puppylpg blog.csdn.net/puppylpg/article/details/78556730 list的转map的另一种猜想 Java8使用lambda表达式进行函数式编程可以对集合进行非常方便的操作。一个比较常见的操作是将list转换成map,一般使用Collectors的toMap()方法进行转换。一个比较常见的问题是当list中含有相同元素的时候,如果不指定取哪一个,则会抛出异常。因此,这个指定是必须的。 当然,使用toMap()的另一个重载方法,可以直接指定。这里,我们想讨论的是另一种方法:在进行转map的操作之前,能不能使用distinct()先把list的重复元素过滤掉,然后转map的时候就不用考虑重复元素的问题了。 使用distinct()给list去重 直接使用distinct(),失败 packageexample.mystream;importlombok.AllArgsConstructor;importlombok.Getter;importlombok.NoArgsConstructor;importlombok.ToString;importjava.util.Arrays;importjava.util.List;importjava.util.Map;importjava.util.stream.Collectors;publicclassListToMap{@AllArgsConstructor@NoArgsConstructor@ToStringprivatestaticclassVideoInfo{@GetterStringid;intwidth;intheight;}publicstaticvoidmain(String[]args){List<VideoInfo>list=Arrays.asList(newVideoInfo("123",1,2),newVideoInfo("456",4,5),newVideoInfo("123",1,2));//preferred:handleduplicateddatawhentoMap()Map<String,VideoInfo>id2VideoInfo=list.stream().collect(Collectors.toMap(VideoInfo::getId,x->x,(oldValue,newValue)->newValue));System.out.println("NoDuplicated1:");id2VideoInfo.forEach((x,y)->System.out.println("<"+x+","+y+">"));//handleduplicateddatausingdistinct(),beforetoMap()Map<String,VideoInfo>id2VideoInfo2=list.stream().distinct().collect(Collectors.toMap(VideoInfo::getId,x->x));System.out.println("NoDuplicated2:");id2VideoInfo2.forEach((x,y)->System.out.println("<"+x+","+y+">"));}} list里总共有三个元素,其中有两个我们认为是重复的。第一种转换是使用toMap()直接指定了对重复key的处理情况,因此可以正常转换成map。而第二种转换是想先对list进行去重,然后再转换成map,结果还是失败了,抛出了IllegalStateException,所以distinct()应该是失败了。 NoDuplicated1:<123,ListToMap.VideoInfo(id=123,width=1,height=2)><456,ListToMap.VideoInfo(id=456,width=4,height=5)>Exceptioninthread"main"java.lang.IllegalStateException:DuplicatekeyListToMap.VideoInfo(id=123,width=1,height=2)atjava.util.stream.Collectors.lambda$throwingMerger$0(Collectors.java:133)atjava.util.HashMap.merge(HashMap.java:1253)atjava.util.stream.Collectors.lambda$toMap$58(Collectors.java:1320)atjava.util.stream.ReduceOps$3ReducingSink.accept(ReduceOps.java:169)atjava.util.stream.DistinctOps$1$2.accept(DistinctOps.java:175)atjava.util.Spliterators$ArraySpliterator.forEachRemaining(Spliterators.java:948)atjava.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:481)atjava.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:471)atjava.util.stream.ReduceOps$ReduceOp.evaluateSequential(ReduceOps.java:708)atjava.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)atjava.util.stream.ReferencePipeline.collect(ReferencePipeline.java:499)atexample.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()对对象进行去重时,是根据对象的equals()方法去处理的。如果我们的VideoInfo类不overrride超类Object的equals()方法,就会使用Object的。 但是Object的equals()方法只有在两个对象完全相同时才返回true。而我们想要的效果是只要VideoInfo的id/width/height均相同,就认为两个videoInfo对象是同一个。所以我们比如重写属于videoInfo的equals()方法。相关文章:一次性搞清楚equals和hashCode 重写equals()的注意事项 我们设计VideoInfo的equals()如下: @Overridepublicbooleanequals(Objectobj){if(!(objinstanceofVideoInfo)){returnfalse;}VideoInfovi=(VideoInfo)obj;returnthis.id.equals(vi.id)&&this.width==vi.width&&this.height==vi.height;} 这样一来,只要两个videoInfo对象的三个属性都相同,这两个对象就相同了。欢天喜地去运行程序,依旧失败!why? 《Effective Java》是本好书,连Java之父James Gosling都说,这是一本连他都需要的Java教程。在这本书中,作者指出,如果重写了一个类的equals()方法,那么就必须一起重写它的hashCode()方法!必须!没有商量的余地! 必须使得重写后的equals()满足如下条件: 根据equals()进行比较,相等的两个对象,hashCode()的值也必须相同; 根据equals()进行比较,不相等的两个对象,hashCode()的值可以相同,也可以不同; 因为这是Java的规定,违背这些规定将导致Java程序运行不再正常。 具体更多的细节,建议大家读读原书,必定获益匪浅。强烈推荐! 最终,我按照神书的指导设计VideoInfo的hashCode()方法如下: @OverridepublicinthashCode(){intn=31;n=n*31+this.id.hashCode();n=n*31+this.height;n=n*31+this.width;returnn;} 终于,distinct()成功过滤了list中的重复元素,此时使用两种toMap()将list转换成map都是没问题的: NoDuplicated1:<123,ListToMap.VideoInfo(id=123,width=1,height=2)><456,ListToMap.VideoInfo(id=456,width=4,height=5)>NoDuplicated2:<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()如下: @Overridepublicbooleanequals(Objectobj){if(!(objinstanceofVideoInfo)){returnfalse;}VideoInfovi=(VideoInfo)obj;System.out.println("<===>Invokeequals()==>"+this.toString()+"vs."+vi.toString());returnthis.id.equals(vi.id)&&this.width==vi.width&&this.height==vi.height;} 结果: NoDuplicated1:<123,ListToMap.VideoInfo(id=123,width=1,height=2)><456,ListToMap.VideoInfo(id=456,width=4,height=5)><===>Invokeequals()==>ListToMap.VideoInfo(id=123,width=1,height=2)vs.ListToMap.VideoInfo(id=123,width=1,height=2)NoDuplicated2:<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()如下: @OverridepublicinthashCode(){return1;} 这样一来,所有的对象的hashCode()返回值都是相同的。当然,这样搞是符合Java规范的,因为Java只规定equals()相同的对象的hashCode必须相同,但是不同的对象的hashCode未必会不同。 结果: NoDuplicated1:<123,ListToMap.VideoInfo(id=123,width=1,height=2)><456,ListToMap.VideoInfo(id=456,width=4,height=5)><===>Invokeequals()==>ListToMap.VideoInfo(id=456,width=4,height=5)vs.ListToMap.VideoInfo(id=123,width=1,height=2)<===>Invokeequals()==>ListToMap.VideoInfo(id=456,width=4,height=5)vs.ListToMap.VideoInfo(id=123,width=1,height=2)<===>Invokeequals()==>ListToMap.VideoInfo(id=123,width=1,height=2)vs.ListToMap.VideoInfo(id=123,width=1,height=2)NoDuplicated2:<123,ListToMap.VideoInfo(id=123,width=1,height=2)><456,ListToMap.VideoInfo(id=456,width=4,height=5)> 果然,equals()调用了三次!看来的确只有hashCode相同的时候才会调用equal()进一步判断两个对象究竟是否相同;如果hashCode不相同,两个对象显然不相同。猜想是正确的。 搜索Java知音公众号,回复“后端面试”,送你一份Java面试题宝典.pdf 结论 list转map推荐使用toMap(),并且无论是否会出现重复的问题,都要指定重复后的取舍规则,不费功夫但受益无穷; 对一个自定义的class使用distinct(),切记覆写equals()方法; 覆写equals(),一定要覆写hashCode(); 虽然设计出一个hashCode()可以简单地让其return 1,这样并不会违反Java规定,但是这样做会导致很多恶果。比如将这样的对象存入hashMap的时候,所有的对象的hashCode都相同,最终所有对象都存储在hashMap的同一个桶中,直接将hashMap恶化成了一个链表。从而O(1)的复杂度被整成了O(n)的,性能自然大大下降。 好书是程序猿进步的阶梯。——高尔基。比如《Effecctive Java》。 最终参考程序: packageexample.mystream;importlombok.AllArgsConstructor;importlombok.Getter;importlombok.NoArgsConstructor;importlombok.ToString;importjava.util.Arrays;importjava.util.List;importjava.util.Map;importjava.util.stream.Collectors;publicclassListToMap{@AllArgsConstructor@NoArgsConstructor@ToStringprivatestaticclassVideoInfo{@GetterStringid;intwidth;intheight;publicstaticvoidmain(String[]args){System.out.println(newVideoInfo("123",1,2).equals(newVideoInfo("123",1,2)));}@Overridepublicbooleanequals(Objectobj){if(!(objinstanceofVideoInfo)){returnfalse;}VideoInfovi=(VideoInfo)obj;returnthis.id.equals(vi.id)&&this.width==vi.width&&this.height==vi.height;}/***Ifequals()isoverride,hashCode()mustbeoverride,too.*1.ifaequalsb,theymusthavethesamehashCode;*2.ifadoesn'tequalsb,theymayhavethesamehashCode;*3.hashCodewritteninthiswaycanbeaffectedbysequenceofthefields;*3.2^5-1=31.So31willbefasterwhendothemultiplication,*becauseitcanbereplacedbybit-shifting:31*i=(i<<5)-i.*@return*/@OverridepublicinthashCode(){intn=31;n=n*31+this.id.hashCode();n=n*31+this.height;n=n*31+this.width;returnn;}}publicstaticvoidmain(String[]args){List<VideoInfo>list=Arrays.asList(newVideoInfo("123",1,2),newVideoInfo("456",4,5),newVideoInfo("123",1,2));//preferred:handleduplicateddatawhentoMap()Map<String,VideoInfo>id2VideoInfo=list.stream().collect(Collectors.toMap(VideoInfo::getId,x->x,(oldValue,newValue)->newValue));System.out.println("NoDuplicated1:");id2VideoInfo.forEach((x,y)->System.out.println("<"+x+","+y+">"));//handleduplicateddatausingdistinct(),beforetoMap()//Notethatdistinct()reliesonequals()intheobject//ifyouoverrideequals(),hashCode()mustbeoverridetogetherMap<String,VideoInfo>id2VideoInfo2=list.stream().distinct().collect(Collectors.toMap(VideoInfo::getId,x->x));System.out.println("NoDuplicated2:");id2VideoInfo2.forEach((x,y)->System.out.println("<"+x+","+y+">"));}} 再拓展 假设类是别人的,不能修改 以上,VideoInfo使我们自己写的类,我们可以往里添加equals()和hashCode()方法。如果VideoInfo是我们引用的依赖中的一个类,我们无权对其进行修改,那么是不是就没办法使用distinct()按照某些元素是否相同,对对象进行自定义的过滤了呢? 使用wrapper 在stackoverflow的一个回答上,我们可以找到一个可行的方法:使用wrapper。 假设在一个依赖中(我们无权修改该类),VideoInfo定义如下: @AllArgsConstructor@NoArgsConstructor@ToStringpublicclassVideoInfo{@GetterStringid;intwidth;intheight;} 使用刚刚的wrapper思路,写程序如下(当然,为了程序的可运行性,还是把VideoInfo放进来了,假设它就是不能修改的,不能为其添加任何方法): packageexample.mystream;importlombok.AllArgsConstructor;importlombok.Getter;importlombok.NoArgsConstructor;importlombok.ToString;importjava.util.Arrays;importjava.util.List;importjava.util.Map;importjava.util.stream.Collectors;publicclassDistinctByWrapper{privatestaticclassVideoInfoWrapper{privatefinalVideoInfovideoInfo;publicVideoInfoWrapper(VideoInfovideoInfo){this.videoInfo=videoInfo;}publicVideoInfounwrap(){returnvideoInfo;}@Overridepublicbooleanequals(Objectobj){if(!(objinstanceofVideoInfo)){returnfalse;}VideoInfovi=(VideoInfo)obj;returnvideoInfo.id.equals(vi.id)&&videoInfo.width==vi.width&&videoInfo.height==vi.height;}@OverridepublicinthashCode(){intn=31;n=n*31+videoInfo.id.hashCode();n=n*31+videoInfo.height;n=n*31+videoInfo.width;returnn;}}publicstaticvoidmain(String[]args){List<VideoInfo>list=Arrays.asList(newVideoInfo("123",1,2),newVideoInfo("456",4,5),newVideoInfo("123",1,2));//VideoInfo--map()-->VideoInfoWrapper---->distinct():VideoInfoWrapper--map()-->VideoInfoMap<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+">"));}}/***AssumethatVideoInfoisaclassthatwecan'tmodify*/@AllArgsConstructor@NoArgsConstructor@ToStringclassVideoInfo{@GetterStringid;intwidth;intheight;} 整个wrapper的思路无非就是构造另一个类VideoInfoWrapper,把hashCode()和equals()添加到wrapper中,这样便可以按照自定义规则对wrapper对象进行自定义的过滤。 搜索Java知音公众号,回复“后端面试”,送你一份Java面试题宝典.pdf 我们没法自定义过滤VideoInfo,但是我们可以自定义过滤VideoInfoWrapper啊! 之后要做的,就是将VideoInfo全部转化为VideoInfoWrapper,然后过滤掉某些VideoInfoWrapper,再将剩下的VideoInfoWrapper转回VideoInfo,以此达到过滤VideoInfo的目的。很巧妙! 使用“filter() + 自定义函数”取代distinct() 另一种更精妙的实现方式是自定义一个函数: privatestatic<T>Predicate<T>distinctByKey(Function<?superT,Object>keyExtractor){Map<Object,Boolean>map=newConcurrentHashMap<>();returnt->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()过滤掉,最终留下的都是不重复的。 最终实现的程序如下 packageexample.mystream;importlombok.AllArgsConstructor;importlombok.Getter;importlombok.NoArgsConstructor;importlombok.ToString;importjava.util.Arrays;importjava.util.List;importjava.util.Map;importjava.util.concurrent.ConcurrentHashMap;importjava.util.function.Function;importjava.util.function.Predicate;importjava.util.stream.Collectors;publicclassDistinctByFilterAndLambda{publicstaticvoidmain(String[]args){List<VideoInfo>list=Arrays.asList(newVideoInfo("123",1,2),newVideoInfo("456",4,5),newVideoInfo("123",1,2));//GetdistinctonlyMap<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+">"));}/***IfakeycouldnotbeputintoConcurrentHashMap,thatmeansthekeyisduplicated*@paramkeyExtractoramappingfunctiontoproducekeys*@param<T>thetypeoftheinputelements*@returntrueifkeyisduplicated;elsereturnfalse*/privatestatic<T>Predicate<T>distinctByKey(Function<?superT,Object>keyExtractor){Map<Object,Boolean>map=newConcurrentHashMap<>();returnt->map.putIfAbsent(keyExtractor.apply(t),Boolean.TRUE)==null;}}/***AssumethatVideoInfoisaclassthatwecan'tmodify*/@AllArgsConstructor@NoArgsConstructor@ToStringclassVideoInfo{@GetterStringid;intwidth;intheight;} END 推荐好文 强大,10k+点赞的 SpringBoot 后台管理系统竟然出了详细教程! 为什么MySQL不推荐使用uuid或者雪花id作为主键? 为什么建议大家使用 Linux 开发?爽(外加七个感叹号) IntelliJ IDEA 15款 神级超级牛逼插件推荐(自用,真的超级牛逼) 炫酷,SpringBoot+Echarts实现用户访问地图可视化(附源码) 记一次由Redis分布式锁造成的重大事故,避免以后踩坑! 十分钟学会使用 Elasticsearch 优雅搭建自己的搜索系统(附源码) 本文分享自微信公众号 - Java知音(Java_friends)。如有侵权,请联系 support@oschina.cn 删除。本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。