作者: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(),失败

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@ToStringprivate static class VideoInfo {@GetterString 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.

显然,distinct()对对象进行去重时,是根据对象的equals()方法去处理的。如果我们的VideoInfo类不overrride超类Object的equals()方法,就会使用Object的。

但是Object的equals()方法只有在两个对象完全相同时才返回true。而我们想要的效果是只要VideoInfo的id/width/height均相同,就认为两个videoInfo对象是同一个。所以我们比如重写属于videoInfo的equals()方法。相关文章:一次性搞清楚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()满足如下条件:

  • 根据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不相同,两个对象显然不相同。猜想是正确的。

搜索Java知音公众号,回复“后端面试”,送你一份Java面试题宝典.pdf

结论

  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@ToStringprivate static class VideoInfo {@GetterString 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)));}@Overridepublic 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*/@Overridepublic 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 togetherMap<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 {@GetterString 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;}@Overridepublic 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;}@Overridepublic 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()--> 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 + ">"));}}/*** Assume that VideoInfo is a class that we can't modify*/
@AllArgsConstructor
@NoArgsConstructor
@ToString
class VideoInfo {@GetterString 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()过滤掉,最终留下的都是不重复的。

最终实现的程序如下

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 onlyMap<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 {@GetterString id;int width;int height;
}

END

推荐好文

强大,10k+点赞的 SpringBoot 后台管理系统竟然出了详细教程!

为什么MySQL不推荐使用uuid或者雪花id作为主键?

为什么建议大家使用 Linux 开发?爽(外加七个感叹号)

IntelliJ IDEA 15款 神级超级牛逼插件推荐(自用,真的超级牛逼)

炫酷,SpringBoot+Echarts实现用户访问地图可视化(附源码)

记一次由Redis分布式锁造成的重大事故,避免以后踩坑!

十分钟学会使用 Elasticsearch 优雅搭建自己的搜索系统(附源码)

一次List对象去重失败,引发对Java8中distinct()的思考相关推荐

  1. lambda 根据属性去重_扩展lamda表达中distinct按照字段去除重复

    首先,我们定义一个Student类来测试. public classStudent {public int ID { get; set; }public string Name { get; set; ...

  2. java集合对字符串或对象去重

    由于开发中遇到了集合中筛选重复字符串或对象的操作,下面记录几种常用的方法,以备后续查询使用,直接上代码, /*** list去掉重复元素* * @param datas* @return*/publi ...

  3. js中给数组中对象去重

    被问到如何给数组中对象去重,想到set答案肯定错误的,这个平时工作中用到的不多,查阅资料,找到给数组中对象去重的方法,利用对象属性名的唯一性,怪自己当时没想到. 方法一: let ary=[{id: ...

  4. java list按照某个字段排序_java相关:List对象去重和按照某个字段排序的实现方法...

    java相关:List对象去重和按照某个字段排序的实现方法 发布于 2020-6-8| 复制链接 下面小妖就为大家带来一篇List对象去重和按照某个字段排序的实现方法.小妖觉得挺不错的,现在就分享给大 ...

  5. js实现数组对象去重

    数组对象去重,可直接复用 /***数组对象去重** @param {Array} arr 去重数组* @param {String} key 唯一标识*/ deduplication(arr, key ...

  6. java set 对象去重_JAVA对象去重

    JAVA对象去重比较恶心的说 需求:我要做的是去重每个权限的菜单,菜单表结构是 ID RoleID MenuID  获取的菜单表 应该是MenuID不能重复的 所以写菜单表的实体类,重写equals ...

  7. es6 数组去重,数组里面的对象去重

    // 数组去重 {const arr = [1,2,3,4,1,23,5,2,3,5,6,7,8,undefined,null,null,undefined,true,false,true,'中文', ...

  8. lambda 对象去重_采用java8 lambda表达式 实现 java list 交集 并集 差集 去重复并集...

    采用java8 lambda表达式 实现java list 交集/并集/差集/去重并集 一般的javaList 交.并集采用简单的 removeAll retainAll 等操作,不过这也破坏了原始的 ...

  9. python 实现对象去重

    利用set()方法实现对象去重,重写__hash__方法和__eq__方法告诉程序什么样的对象是同一个对象 # 写一个类 拥有100个对象 # 拥有三个属性 name age sex # 如果两个对象 ...

最新文章

  1. 什么是泛型缓存和静态构造函数?
  2. 使用CA和CCA克隆账户
  3. mysql5.7.13.zip安装(windows)
  4. Python中的eval(),exec()以及其相关函数
  5. 以Spring方式构建企业Java应用程序
  6. 算法6:只有五行的Floyd最短路算法
  7. JavaScript的理解记录(6)
  8. 和Hibernate3.6相比,Hibernate 5.x中的增删改性能降低了
  9. 02-02 Python 读写文件 open|os|sys
  10. 对于大多数的中小微企业,产品是你成功的1.0阶段
  11. 第二次作业+105032014037
  12. 书单 | 专为程序员而写的数学书
  13. matlab取整函数记录
  14. iapp进度条倒计时_倒计时进度条app
  15. oracle分页优化
  16. 好用的PDF编辑软件有哪些?这几款工具建议收藏
  17. 听刘万祥老师讲“竖向折线图”、“点图”画法
  18. 【JAVA作业】异常、随机数、List
  19. 谁说淘宝客不赚钱?用此招,票子流量手到擒来!
  20. java策略模式使用场景,Java设计模式—策略模式

热门文章

  1. 华为已开始生产不含美国零部件的5G基站
  2. Redmi K20 Pro尊享版官宣:升级为骁龙855 Plus旗舰平台
  3. 马云卸任浙江阿里小额贷款股份有限公司法定代表人、董事长
  4. 马斯克扎心了!猎鹰重型火箭核心助推器运输过程中坠海
  5. 苹果将推出新款iPhone 7/8?为应对德国禁售令!
  6. 拳王虚拟项目公社:自动化的虚拟资源产品,唱歌教程赚地盆满钵满
  7. 驱动模块Makefile 编写【原创】
  8. getchar(关于键盘缓冲区)
  9. 基于RTP协议的数据通讯程序
  10. python django 数据库查询方法总结