Java中对List去重, Stream去重

问题
当下互联网技术成熟,越来越多的趋向去中心化、分布式、流计算,使得很多以前在数据库侧做的事情放到了Java端。今天有人问道,如果数据库字段没有索引,那么应该如何根据该字段去重?大家都一致认为用Java来做,但怎么做呢?
解答
忽然想起以前写过list去重的文章,找出来一看。做法就是将list中对象的hashcode和equals方法重写,然后丢到HashSet里,然后取出来。这是最初刚学Java的时候像被字典一样背写出来的答案。就比如面试,面过号称做了3年Java的人,问Set和HashMap的区别可以背出来,问如何实现就不知道了。也就是说,初学者只背特性。但真正在项目中使用的时候你需要确保一下是不是真的这样。因为背书没用,只能相信结果。你需要知道HashSet如何帮我做到去重了。换个思路,不用HashSet可以去重吗?最简单,最直接的办法不就是每次都拿着和历史数据比较,都不相同则插入队尾。而HashSet只是加速了这个过程而已。
首先,给出我们要排序的对象User
@Data
@Builder
@AllArgsConstructor
public class User {
private Integer id;
private String name;
}
List<User> users = Lists.newArrayList(
new User(1, "a"),
new User(1, "b"),
new User(2, "b"),
new User(1, "a"));
目标是取出id不重复的user,为了防止扯皮,给个规则,只要任意取出id唯一的数据即可,不用拘泥id相同时算哪个。
用最直观的办法
这个办法就是用一个空list存放遍历后的数据。
@Test
public void dis1() {
List<User> result = new LinkedList<>();
for (User user : users) {
boolean b = result.stream().anyMatch(u -> u.getId().equals(user.getId()));
if (!b) {
result.add(user);
}
}
System.out.println(result);
}
用HashSet
背过特性的都知道HashSet可以去重,那么是如何去重的呢? 再深入一点的背过根据hashcode和equals方法。那么如何根据这两个做到的呢?没有看过源码的人是无法继续的,面试也就到此结束了。
事实上,HashSet是由HashMap来实现的(没有看过源码的时候曾经一直直观的以为HashMap的key是HashSet来实现的,恰恰相反)。这里不展开叙述,只要看HashSet的构造方法和add方法就能理解了。
public HashSet() {
map = new HashMap<>();
}
/**
* 显然,存在则返回false,不存在的返回true
*/
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
那么,由此也可以看出HashSet的去重复就是根据HashMap实现的,而HashMap的实现又完全依赖于hashcode和equals方法。这下就彻底打通了,想用HashSet就必须看好自己的这两个方法。
在本题目中,要根据id去重,那么,我们的比较依据就是id了。修改如下:
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
User user = (User) o;
return Objects.equals(id, user.id);
}
@Override
public int hashCode() {
return Objects.hash(id);
}
//hashcode
result = 31 * result + (element == null ? 0 : element.hashCode());
其中, Objects调用Arrays的hashcode,内容如上述所示。乘以31等于x<<5-x。
最终实现如下:
@Test
public void dis2() {
Set<User> result = new HashSet<>(users);
System.out.println(result);
}
使用Java的Stream去重
回到最初的问题,之所以提这个问题是因为想要将数据库侧去重拿到Java端,那么数据量可能比较大,比如10w条。对于大数据,采用Stream相关函数是最简单的了。正好Stream也提供了distinct函数。那么应该怎么用呢?
users.parallelStream().distinct().forEach(System.out::println);
没看到用lambda当作参数,也就是没有提供自定义条件。幸好Javadoc标注了去重标准:
Returns a stream consisting of the distinct elements
(according to {@link Object#equals(Object)}) of this stream.
我们知道,也必须背过这样一个准则:equals返回true的时候,hashcode的返回值必须相同. 这个在背的时候略微有些逻辑混乱,但只要了解了HashMap的实现方式就不会觉得拗口了。HashMap先根据hashcode方法定位,再比较equals方法。
所以,要使用distinct来实现去重,必须重写hashcode和equals方法,除非你使用默认的。
那么,究竟为啥要这么做?点进去看一眼实现。
<P_IN> Node<T> reduce(PipelineHelper<T> helper, Spliterator<P_IN> spliterator) {
// If the stream is SORTED then it should also be ORDERED so the following will also
// preserve the sort order
TerminalOp<T, LinkedHashSet<T>> reduceOp
= ReduceOps.<T, LinkedHashSet<T>>makeRef(LinkedHashSet::new, LinkedHashSet::add,
LinkedHashSet::addAll);
return Nodes.node(reduceOp.evaluateParallel(helper, spliterator));
}
内部是用reduce实现的啊,想到reduce,瞬间想到一种自己实现distinctBykey的方法。我只要用reduce,计算部分就是把Stream的元素拿出来和我自己内置的一个HashMap比较,有则跳过,没有则放进去。其实,思路还是最开始的那个最直白的方法。
@Test
public void dis3() {
users.parallelStream().filter(distinctByKey(User::getId))
.forEach(System.out::println);
}
public static <T> Predicate<T> distinctByKey(Function<? super T, ?> keyExtractor) {
Set<Object> seen = ConcurrentHashMap.newKeySet();
return t -> seen.add(keyExtractor.apply(t));
}
当然,如果是并行stream,则取出来的不一定是第一个,而是随机的。
上述方法是至今发现最好的,无侵入性的。但如果非要用distinct。只能像HashSet那个方法一样重写hashcode和equals。
小结
会不会用这些东西,你只能去自己练习过,不然到了真正要用的时候很难一下子就拿出来,不然就冒险用。而若真的想大胆使用,了解规则和实现原理也是必须的。比如,LinkedHashSet和HashSet的实现有何不同。
附上贼简单的LinkedHashSet源码:
public class LinkedHashSet<E>
extends HashSet<E>
implements Set<E>, Cloneable, java.io.Serializable {
private static final long serialVersionUID = -2851667679971038690L;
public LinkedHashSet(int initialCapacity, float loadFactor) {
super(initialCapacity, loadFactor, true);
}
public LinkedHashSet(int initialCapacity) {
super(initialCapacity, .75f, true);
}
public LinkedHashSet() {
super(16, .75f, true);
}
public LinkedHashSet(Collection<? extends E> c) {
super(Math.max(2*c.size(), 11), .75f, true);
addAll(c);
}
@Override
public Spliterator<E> spliterator() {
return Spliterators.spliterator(this, Spliterator.DISTINCT | Spliterator.ORDERED);
}
}
Java中对List去重, Stream去重的更多相关文章
- Java中的文件和stream流的操作代码
1.Java中FileRead方法的运用代码及详解 package example2;import java.io.FileReader;import java.io.IOException;clas ...
- Java中5种List的去重方法及它们的效率对比,你用对了吗?
01.使用两个for循环实现List去重(有序) /**使用两个for循环实现List去重(有序) * * @param list * */ public static ...
- java中List对象列表去重或取出以及排序
面试碰到几次list的去重和排序.下面介绍一种做法: 1. list去重 1.1 实体类Student List<Student>容量10k以上,要求去重复.这里Student的重复标准是 ...
- Java中list<Object>集合去重实例
一:Java中list去重的方法很多,下面说一下其中一种方法:把list里的对象遍历一遍,用list.contain(),如果不存在就放入到另外一个list集合中: 二:实例 这里需要注意的是:使用c ...
- java中list集合的几种去重方式
public class ListDistinctExample { public static void main(String[] args) { List<Integer> list ...
- Java去重字符串的两种方法以及java中冒号的使用
package com.removesamestring; import java.io.BufferedWriter; import java.util.ArrayList; import java ...
- 关于java中Stream理解
关于java中Stream理解 Stream是什么 Stream:Java 8新增的接口,Stream可以认为是一个高级版本的Iterator.它代表着数据流,流中的数据元素的数量可以是有限的, 也可 ...
- List集合去重的一些方法(常规遍历、Set去重、java8 stream去重、重写equals和hashCode方法)
1. 常规元素去重 碰到List去重的问题,除了遍历去重,我们常常想到利用Set集合不允许重复元素的特点,通过List和Set互转,来去掉重复元素. // 遍历后判断赋给另一个list集合,保持原来顺 ...
- 面试中常问的List去重问题,你都答对了吗?
面试中经常被问到的list如何去重,用来考察你对list数据结构,以及相关方法的掌握,体现你的java基础学的是否牢固. 我们大家都知道,set集合的特点就是没有重复的元素.如果集合中的数据类型是基本 ...
随机推荐
- Mysql自连接的一些用法
自连接是连接的一种用法,但并不是连接的一种类型,因为他的本质是把一张表当成两张表来使用. mysql有时在信息查询时需要进行对自身连接(自连接),所以我们需要为表定义别名. 我们举例说明,下面是商品采 ...
- python爬微信公众号前10篇历史文章(2)-拼接URL&发送http请求
如何拼接想要的url http://weixin.sogou.com/weixin?type=1&page=1&ie=utf8&query=%E5%A4%A7%E7%BA%BD ...
- 前端水印图片及文字js教程
前端水印图片文字教程如下,复制代码修改图片地址即可看到效果,工作中遇到总结的,喜欢就关注下哦: <!DOCTYPE html><html> <head> <m ...
- bat脚本:windows下一键启动zookeeper+kafka
bat脚本:windows下一键启动zookeeper+kafka 把下面两行代码存为bat文件,双击执行即可.注意更改相应的目录 这里用ping来控制时间(先zookeeper,ping 4 次后 ...
- DB2开发系列之一——基本语法
最近看了些db2开发方面的资料,现做摘要,以供自己和大家参考: 1.变量声明 DECLARE v_salary DEC(9,2) DEFAULT 0.0; DECLARE v_status char( ...
- vue技术解析二之组件通信
--------------------------动画效果----------------------------------- <transition name="router-f ...
- 彻底弄懂CommonJS和AMD/CMD!
JS中的模块规范(CommonJS,AMD,CMD),如果你听过js模块化这个东西,那么你就应该听过或CommonJS或AMD甚至是CMD这些规范咯,我也听过,但之前也真的是听听而已. 现在就看看吧, ...
- 套接字API
Q:套接字特点 A:管道,消息队列,信号量,共享内存这些通信机制只能允许同一计算机上运行的进程相互通信,而套接字不仅可以提供在同一计算机上的进程间通信,还可以提供不同计算机上的进程间通信. 服务器端: ...
- android中与SQLite数据库相关的类
为什么要在应用程序中使用数据库?数据库最主要的用途就是作为数据的存储容器,另外,由于可以很方便的将应用程序中的数据结构(比如C语言中的结构体)转化成数据库的表,这样我们就可以通过操作数据库来替代写一堆 ...
- 实现win的on程序数据更新
枚举是一组描述性的名称定义一组有限的值,不包含方法对可能的值进行约束枚举是一组指定的常数,对可能的值进行约束枚举使用时直观方便.更易于维护 pictureBox控件属性名称 说明image ...