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集合去除重复数据的方法

1. 循环list中的所有元素然后删除重复

public  static  List removeDuplicate(List list) {
 for ( int i =  0 ; i < list.size() -  1 ; i ++ ) {
   for ( int j = list.size() -  1 ; j > i; j -- ) {
      if (list.get(j).equals(list.get(i))) {
       list.remove(j);
      }
    }
   }
  return list;
} 

2. 通过HashSet踢除重复元素

public static List removeDuplicate(List list) {
HashSet h = new HashSet(list);
list.clear();
list.addAll(h);
return list;
}  

3. 删除ArrayList中重复元素,保持顺序

// 删除ArrayList中重复元素,保持顺序
 public static void removeDuplicateWithOrder(List list) {
  Set set = new HashSet();
   List newList = new ArrayList();
  for (Iterator iter = list.iterator(); iter.hasNext();) {
     Object element = iter.next();
     if (set.add(element))
      newList.add(element);
   }
   list.clear();
   list.addAll(newList);
  System.out.println( " remove duplicate " + list);
 }  

4.把list里的对象遍历一遍,用list.contain(),如果不存在就放入到另外一个list集合中

public static List removeDuplicate(List list){
    List listTemp = new ArrayList();
    for(int i=0;i<list.size();i++){
      if(!listTemp.contains(list.get(i))){
        listTemp.add(list.get(i));
      }
    }
    return listTemp;
  } 
(0)

相关推荐

  • 简单总结Java IO中stream流的使用方法

    Java语言的输入输出功能是十分强大而灵活的,对于数据的输入和输出操作以"流"(stream)的方式进行.J2SDK提供了各种各样的"流"类,用以获取不同种类的数据,定义在包java.io中.程序中通过标准的方法输入或输出数据. Java中的流可以从不同的角度进行分类: 按照流的方向不同:分为输入流和输出流. 按照处理数据单位的不同:分为字节流(8位)和字符流(16位). 按照功能不同:分为节点流和处理流. 节点流:是可以从一个特定的数据源(节点)读写数据的流(例如

  • Java8中利用stream对map集合进行过滤的方法

    前言 Stream 是用函数式编程方式在集合类上进行复杂操作的工具,其集成了Java 8中的众多新特性之一的聚合操作,开发者可以更容易地使用Lambda表达式,并且更方便地实现对集合的查找.遍历.过滤以及常见计算等. 最近公司在大张旗鼓的进行代码审核,从中也发现自己写代码的不好习惯.一次无意的点到了公司封装的对map集合过滤的方法,发现了stream.于是研究了一下.并对原有的代码再次结合Optional进行重构下 原有方法说明 主要处理过滤条件Map对象,过滤掉了null和空字符串 等操作 这

  • Java8处理集合的优雅姿势之Stream

    前言 在Java中,集合和数组是我们经常会用到的数据结构,需要经常对他们做增.删.改.查.聚合.统计.过滤等操作.相比之下,关系型数据库中也同样有这些操作,但是在Java 8之前,集合和数组的处理并不是很便捷. 不过,这一问题在Java 8中得到了改善,Java 8 API添加了一个新的抽象称为流Stream,可以让你以一种声明的方式处理数据.本文就来介绍下如何使用Stream.特别说明一下,关于Stream的性能及原理不是本文的重点,如果大家感兴趣后面会出文章单独介绍. 1.Stream介绍

  • 详解Java8 Collect收集Stream的方法

    Collection, Collections, collect, Collector, Collectos Collection是Java集合的祖先接口. Collections是java.util包下的一个工具类,内涵各种处理集合的静态方法. java.util.stream.Stream#collect(java.util.stream.Collector<? super T,A,R>)是Stream的一个函数,负责收集流. java.util.stream.Collector 是一个收

  • Java8新特性Stream流实例详解

    什么是Stream流? Stream流是数据渠道,用于操作数据源(集合.数组等)所生成的元素序列. Stream的优点:声明性,可复合,可并行.这三个特性使得stream操作更简洁,更灵活,更高效. Stream的操作有两个特点:可以多个操作链接起来运行,内部迭代. Stream可分为并行流与串行流,Stream API 可以声明性地通过 parallel() 与sequential() 在并行流与顺序流之间进行切换.串行流就不必再细说了,并行流主要是为了为了适应目前多核机器的时代,提高系统CP

  • 举例讲解Java中的Stream流概念

    1.基本的输入流和输出流 流是 Java 中最重要的基本概念之一.文件读写.网络收发.进程通信,几乎所有需要输入输出的地方,都要用到流. 流是做什么用的呢?就是做输入输出用的.为什么输入输出要用"流"这种方式呢?因为程序输入输出的基本单位是字节,输入就是获取一串字节,输出就是发送一串字节.但是很多情况下,程序不可能接收所有的字节之后再进行处理,而是接收一点处理一点.比方你下载魔兽世界,不可能全部下载到内存里再保存到硬盘上,而是下载一点就保存一点.这时,流这种方式就非常适合. 在 Jav

  • java中double类型运算结果异常的解决方法

    问题: 对两个double类型的值进行运算,有时会出现结果值异常的问题.比如: System.out.println(19.99+20); System.out.println(1.0-0.66); System.out.println(0.033*100); System.out.println(12.3/100); 输出: 39.989999999999995 0.33999999999999997 3.3000000000000003 0.12300000000000001 解决方法: J

  • Java中URL传中文时乱码的解决方法

    前言 Java中URL传中文时乱码的问题相信不少朋友都遇到过,最近就遇到一个问题,就是在Action当中把一条中文信息绑定在URL的后面,ActionForward到别一个页面时,用reqeust.getParameter取出是出现乱码的问题. 解决办法 1.对要进行URL传递的中文字符进行编码: String message = java.net.URLEncoder.encode("中文字符","utf-8"); 2.在取URL传递中文的页面对字符进行解码: S

  • Java中典型的内存泄露问题和解决方法

    Q:在Java中怎么可以产生内存泄露?A:Java中,造成内存泄露的原因有很多种.典型的例子是一个没有实现hasCode和equals方法的Key类在HashMap中保存的情况.最后会生成很多重复的对象.所有的内存泄露最后都会抛出OutOfMemoryError异常,下面通过一段简短的通过无限循环模拟内存泄露的例子说明一下. 复制代码 代码如下: import java.util.HashMap;import java.util.Map; public class MemoryLeak { pu

  • java 中如何实现 List 集合去重

    目录 1.自定义去重 2.利用 Set 集合去重 3.使用 Stream 去重 总结 前言: List 去重指的是将 List 中的重复元素删除掉的过程.此题目考察的是对 List 迭代器.Set 集合和 JDK 8 中新特性的理解与灵活运用的能力. List 去重有以下 3 种实现思路: 自定义方法去重,通过循环判断当前的元素是否存在多个,如果存在多个,则删除此重复项,循环整个集合最终得到的就是一个没有重复元素的 List: 使用 Set 集合去重,利用 Set 集合自身自带去重功能的特性,实

  • java中实现list或set转map的方法

    java中实现list或set转map的方法 在开发中我们有时需要将list或set转换为map(比如对象属性中的唯一键作为map的key,对象作为map的value),一般的想法就是new一个map,然后把list或set中的值一个个push到map中. 类似下面的代码: List<String> stringList = Lists.newArrayList("t1", "t2", "t3"); Map<String, St

  • Java中EasyPoi导出复杂合并单元格的方法

    前言: 上星期做了一个Excel的单元格合并,用的是EasyPoi,我之前合并单元格都是原生的,第一次使用EasyPoi合并也不太熟悉,看着网上自己套用,使用后发现比原生的方便些,贡献一下,也给其他用到合并而且用的是EasyPoi的小伙伴节省下时间. 导出模板: 坐标: 版本号,自己来定,可以去官网查看:EasyPoi官网 <!-- easypoi 导入包 --> <dependency> <groupId>cn.afterturn</groupId> &l

  • java中javamail发送带附件的邮件实现方法

    本文实例讲述了java中javamail发送带附件的邮件实现方法.分享给大家供大家参考.具体分析如下: JavaMail,顾名思义,提供给开发者处理电子邮件相关的编程接口.它是Sun发布的用来处理email的API.它可以方便地执行一些常用的邮件传输,JavaMail是可选包,因此如果需要使用的话你需要首先从java官网上下载.目前最新版本是JavaMail1.5.0,下面我们来看看javamail发送带附件的邮件实例 mail.java 代码: 复制代码 代码如下: package mail;

  • Java中checkbox实现跨页多选的方法

    最近要实现一个功能,就是checkbox跨页多选,在网上看了一下,资料很少,而且大多是不完全的.不过经过我的努力,终于做出来了. JSP页面: 1,定义三个Hidden变量: <INPUT type="hidden" name="all_selected"> <INPUT type="hidden" name="now_selected"> <INPUT type="hidden&quo

  • java中利用反射调用另一类的private方法的简单实例

    我们知道,Java应用程序不能访问持久化类的private方法,但Hibernate没有这个限制,它能够访问各种级别的方法,如private, default, protected, public. Hibernate是如何实现该功能的呢?答案是利用JAVA的反射机制,如下: import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; public class ReflectDemo {

  • Java中关于控制台读取数字或字符串的方法

    Java中,int a = System.in.read();此句读取的是一个字符,然后返回的是对应字符的ASCII, 例如,控制台输入123,只读取一个字符1,对应的ASCII为49,则输出49,输入abc则读取a,对应的ASCII是97,则输出97: Scanner sc = new Scanner(System.in) int n = sc.nextInt();从控制台读取一个数. String c = sc.next();//从控制台读取字符串 以上就是小编为大家带来的Java中关于控制

随机推荐