问题

当下互联网技术成熟,越来越多的趋向去中心化、分布式、流计算,使得很多以前在数据库侧做的事情放到了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去重的更多相关文章

  1. Java中的文件和stream流的操作代码

    1.Java中FileRead方法的运用代码及详解 package example2;import java.io.FileReader;import java.io.IOException;clas ...

  2. Java中5种List的去重方法及它们的效率对比,你用对了吗?

    01.使用两个for循环实现List去重(有序) /**使用两个for循环实现List去重(有序)     *     * @param list     * */    public static  ...

  3. java中List对象列表去重或取出以及排序

    面试碰到几次list的去重和排序.下面介绍一种做法: 1. list去重 1.1 实体类Student List<Student>容量10k以上,要求去重复.这里Student的重复标准是 ...

  4. Java中list<Object>集合去重实例

    一:Java中list去重的方法很多,下面说一下其中一种方法:把list里的对象遍历一遍,用list.contain(),如果不存在就放入到另外一个list集合中: 二:实例 这里需要注意的是:使用c ...

  5. java中list集合的几种去重方式

    public class ListDistinctExample { public static void main(String[] args) { List<Integer> list ...

  6. Java去重字符串的两种方法以及java中冒号的使用

    package com.removesamestring; import java.io.BufferedWriter; import java.util.ArrayList; import java ...

  7. 关于java中Stream理解

    关于java中Stream理解 Stream是什么 Stream:Java 8新增的接口,Stream可以认为是一个高级版本的Iterator.它代表着数据流,流中的数据元素的数量可以是有限的, 也可 ...

  8. List集合去重的一些方法(常规遍历、Set去重、java8 stream去重、重写equals和hashCode方法)

    1. 常规元素去重 碰到List去重的问题,除了遍历去重,我们常常想到利用Set集合不允许重复元素的特点,通过List和Set互转,来去掉重复元素. // 遍历后判断赋给另一个list集合,保持原来顺 ...

  9. 面试中常问的List去重问题,你都答对了吗?

    面试中经常被问到的list如何去重,用来考察你对list数据结构,以及相关方法的掌握,体现你的java基础学的是否牢固. 我们大家都知道,set集合的特点就是没有重复的元素.如果集合中的数据类型是基本 ...

随机推荐

  1. ASP.NET Core 2.0 : 七.一张图看透启动背后的秘密

    为什么我们可以在Startup这个 “孤零零的” 类中配置依赖注入和管道? 它是什么时候被实例化并且调用的? 参数中的IServiceCollection services是怎么来的? 处理管道是怎么 ...

  2. 二叉搜索树(Java实现)

    二叉搜索树基本操作 求树中的结点个数 判断节点是否为空 向树中插入新结点key-value 树中是否存在key 返回树中key对应的value值 先序遍历 中序遍历 后续遍历 层序遍历 求树中key最 ...

  3. springMVC正确使用GET POST PUT和DELETE方法,如何传递参数

    1. 向服务器请求数据:GET 这是标准的http的GET最擅长的, 应该使用GET请求,但是在使用时候我们会需要传递一个或多个参数给服务器, 这些出参数可能是基本数据类型页可能是对象,get方法可以 ...

  4. JAVA基础之序列化与反序列化

    序列化和反序列化: 把对象转化为字节序列的过程称为序列化: 把字节序列恢复为对象的过程称为对象的反序列化: 方法: Java.io.ObjectOutputStream代表对象的输出流,writeOb ...

  5. 理解JAVA内存模型

    实际上java内存模型是如上图所示一样 每个线程有自己的栈内存,存放共享对象的副本,本地变量 每个线程自己的本地变量是不可见的,但是共享对象对每个线程都是可见的. 如果想实现线程通信的话, 线程对共享 ...

  6. 20165226 2017-2018-3 《Java程序设计》第5学习总结

    20165226 2017-2018-3 <Java程序设计>第5周学习总结 教材学习内容总结 第七章 内部类与异常类 匿名类创建对象: new Bank() { 匿名类的类体 }: 异常 ...

  7. CSS2Properties doesn't have an indexed property setter for '0'

    使用React时,发现chrome浏览器没事,firefox火狐浏览器报了一个CSS2Properties doesn't have an indexed property setter for '0 ...

  8. java 5并发中的阻塞队列ArrayBlockingQueue的使用以及案例实现

    演示一个阻塞队列的使用 public class BlockingQueueTest { public static void main(String[] args) { //创建一个包含三个元素的阻 ...

  9. 用 Go 编写一个简单的 WebSocket 推送服务

    用 Go 编写一个简单的 WebSocket 推送服务 本文中代码可以在 github.com/alfred-zhong/wserver 获取. 背景 最近拿到需求要在网页上展示报警信息.以往报警信息 ...

  10. JavaScript(第二十七天)【错误处理与调试】

    JavaScript在错误处理调试上一直是它的软肋,如果脚本出错,给出的提示经常也让人摸不着头脑.ECMAScript第3版为了解决这个问题引入了try...catch和throw语句以及一些错误类型 ...