Java8中通过Stream对列表进行去重的几种方法

几种列表去重的方法


一、Stream的distinct()方法

1、介绍

distinct()是Java8中Stream提供的方法,返回的是该流中不同元素组成的流。distinct()使用hashCode()和eqauls()方法来获取不同的元素。因此,需要去重的类必须实现hashCode()和equals()方法。换句话说,我们可以通过重写定制hashCode()和equals()方法来达到某些特殊需求去重。
Stream.distinct()方法声明如下:。。。

Stream<T> distinct()
2、对于String列表去重

因为String类已经覆写了equals()和hashCode()方法,所以可以去重成功。

package com.concretepage;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class DistinctSimpleDemo {
    public static void main(String[] args) {
        List<String> list = Arrays.asList("AA", "BB", "CC", "BB", "CC", "AA", "AA");
        long l = list.stream().distinct().count();
        System.out.println("No. of distinct elements:"+l);
        String output = list.stream().distinct().collect(Collectors.joining(","));
        System.out.println(output);
    }
}

输出结果为:

No. of distinct elements:3
AA,BB,CC
3、对于实体类列表的去重

注:代码中我们使用了 Lombok 插件的 @Data注解,可自动覆写 equals() 以及 hashCode() 方法。

@Data
public class Student {
  private String stuNo;
  private String name;
  }



@Test
public void listDistinctByStreamDistinct() throws JsonProcessingException {
    ObjectMapper objectMapper = new ObjectMapper();
    // 1. 对于 Student 列表去重
    List<Student> studentList = getStudentList();
    out.print("去重前:");
    out.println(objectMapper.writeValueAsString(studentList));
    studentList = studentList.stream().distinct().collect(Collectors.toList());
    out.print("去重后:");
    out.println(objectMapper.writeValueAsString(studentList));
}

结果如下:

去重前:[{"stuNo":"001","name":"Tom"},{"stuNo":"002","name":"Mike"},{"stuNo":"001","name":"Tom"}]
去重后:[{"stuNo":"001","name":"Tom"},{"stuNo":"002","name":"Mike"}]

我们也可自己重写equals()和hashCode()方法。
例如我们有一个Book对象列表,为了对列表进行去重,将该类重写hashCode()和equals()方法


package com.concretepage;
public class Book {
    private String name;
    private int price;
    public Book(String name, int price) {
	this.name = name;
	this.price = price;
    }
    public String getName() {
	return name;
    }
    public int getPrice() {
	return price;
    }
    @Override
    public boolean equals(final Object obj) {
      if (obj == null) {
         return false;
      }
      final Book book = (Book) obj;
      if (this == book) {
         return true;
      } else {
         return (this.name.equals(book.name) && this.price == book.price);
      }
    }
    @Override
    public int hashCode() {
      int hashno = 7;
      hashno = 13 * hashno + (name == null ? 0 : name.hashCode());
      return hashno;
    }
}
package com.concretepage;
import java.util.ArrayList;
import java.util.List;
public class DistinctWithUserObjects {
    public static void main(String[] args) {
        List<Book> list = new ArrayList<>();
        {
           list.add(new Book("Core Java", 200));
           list.add(new Book("Core Java", 200));
           list.add(new Book("Learning Freemarker", 150));        	
           list.add(new Book("Spring MVC", 300));
           list.add(new Book("Spring MVC", 300));
        }
        long l = list.stream().distinct().count();
        System.out.println("No. of distinct books:"+l);
        list.stream().distinct().forEach(b -> System.out.println(b.getName()+ "," + b.getPrice()));
    }
}

输出结果如下:

No. of distinct books:3
Core Java,200
Learning Freemarker,150
Spring MVC,300
4、distinctBykey

distinct()不提供按照属性对对象列表进行去重的直接实现。它是基于hashCode()和equals()工作的。如果我们想要按照对象的属性,对对象的列表进行去重,我们可以通过其他方法来实现。如下代码所示:

static <T> Predicate<T> distinctByKey(Function<? super T, ?> keyExtractor) {
        Map<Object,Boolean> seen = new ConcurrentHashMap<>();
        return t -> seen.putIfAbsent(keyExtractor.apply(t), Boolean.TRUE) == null;
} 

上面的方法可以被Stream接口的 filter()接收为参数,如下所示:

list.stream().filter(distinctByKey(b -> b.getName()));

distinctByKey()方法返回一个使用ConcurrentHashMap 来维护先前所见状态的 Predicate 实例,如下是一个完整的使用对象属性来进行去重的示例

package com.concretepage;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
import java.util.function.Predicate;
public class DistinctByProperty {
    public static void main(String[] args) {
        List<Book> list = new ArrayList<>();
        {
        	list.add(new Book("Core Java", 200));
        	list.add(new Book("Core Java", 300));
        	list.add(new Book("Learning Freemarker", 150));
        	list.add(new Book("Spring MVC", 200));
        	list.add(new Book("Hibernate", 300));
        }
        list.stream().filter(distinctByKey(b -> b.getName()))
              .forEach(b -> System.out.println(b.getName()+ "," + b.getPrice()));   
    }
    private static <T> Predicate<T> distinctByKey(Function<? super T, ?> keyExtractor) {
        Map<Object,Boolean> seen = new ConcurrentHashMap<>();
        return t -> seen.putIfAbsent(keyExtractor.apply(t), Boolean.TRUE) == null;
    }
} 

输出结果如下:

Core Java,200
Learning Freemarker,150
Spring MVC,200
Hibernate,300

注:栗子摘于:https://blog.csdn.net/haiyoung/article/details/80934467


二、根据List中Object某个属性去重

1、利用TreeSet去重
 @Test
  public void distinctByProperty1() throws JsonProcessingException {
    // 这里第一种方法我们通过新创建一个只有不同元素列表来实现根据对象某个属性去重
    ObjectMapper objectMapper = new ObjectMapper();
    List<Student> studentList = getStudentList();
 
    out.print("去重前        :");
    out.println(objectMapper.writeValueAsString(studentList));
    studentList = studentList.stream().distinct().collect(Collectors.toList());
    out.print("distinct去重后:");
    out.println(objectMapper.writeValueAsString(studentList));
    // 这里我们引入了两个静态方法,以及通过 TreeSet<> 来达到获取不同元素的效果
    // 1. import static java.util.stream.Collectors.collectingAndThen;
    // 2. import static java.util.stream.Collectors.toCollection;
    studentList = studentList.stream().collect(
      collectingAndThen(
        toCollection(() -> new TreeSet<>(Comparator.comparing(Student::getName))), ArrayList::new)
    );
    out.print("根据名字去重后 :");
    out.println(objectMapper.writeValueAsString(studentList));
  }

输出结果如下:

去重前        :[{"stuNo":"001","name":"Tom"},{"stuNo":"001","name":"Tom"},{"stuNo":"003","name":"Tom"}]
distinct去重后:[{"stuNo":"001","name":"Tom"},{"stuNo":"003","name":"Tom"}]
根据名字去重后 :[{"stuNo":"001","name":"Tom"}]

2、利用TreeSet部分数据无法去重原因分析

在使用TreeSet去重时,当我们自定义比较器,倘若比较器中没有返回1,0,-1这三种情况,可能出现无法去重的情况!!!
举个栗子:

  Set<List<Integer>> set = new TreeSet<List<Integer>>(new Comparator<List<Integer>>() {
            @Override
            public int compare(List<Integer> o1, List<Integer> o2) {
                return o1.size()==o2.size()&&o1.containsAll(o2)?0:1;
               
            }
        });

当元素不多时,的确可以达到去重的效果,但是当元素过多时,就不能去重了。如下面这个用例:

 set.add(Arrays.asList(new Integer[]{2,-2,0}));
        set.add(Arrays.asList(new Integer[]{0,2,-2}));
        set.add(Arrays.asList(new Integer[]{-4, 0, 4}));
        set.add(Arrays.asList(new Integer[]{0, -4, 4}));
        set.add(Arrays.asList(new Integer[]{-1, -2, 3}));
        set.add(Arrays.asList(new Integer[]{1, -4, 3}));
        set.add(Arrays.asList(new Integer[]{1, -1, 0}));
        set.add(Arrays.asList(new Integer[]{1, 2, -3}));
        set.add(Arrays.asList(new Integer[]{0, -3, 3}));
        set.add(Arrays.asList(new Integer[]{0, -4, 4}));
        set.add(Arrays.asList(new Integer[]{0, 2, -2}));
        set.add(Arrays.asList(new Integer[]{0, 1, -1}));
        set.add(Arrays.asList(new Integer[]{-1, -3, 4}));
        set.add(Arrays.asList(new Integer[]{-1, -2, 3}));
        set.add(Arrays.asList(new Integer[]{-1, 2, -1}));
        set.add(Arrays.asList(new Integer[]{-1, 0, 1}));

执行结果如下:


[-4, 0, 4]
[-1, 0, 1]
[-1, 2, -1]
[-1, -3, 4]
[-1, -2, 3]
[0, 1, -1]
[0, 2, -2]
[0, -4, 4]
[0, -3, 3]
[1, 2, -3]
[1, -1, 0]
[1, -4, 3]
[2, -2, 0]

明显没去重。原因是在compare方法中没有返回-1,就可能存在在树的左边和树的右边同时存在同一个元素的情况。大概如下图所示(随手一画),大概就是树的左右都可能出现同一个元素,因为没有比较到。
在这里插入图片描述
解决方法:用下面的代码重写compare方法:

@Override
            public int compare(List<Integer> l1, List<Integer> l2) {
                Collections.sort(l1);
                Collections.sort(l2);
                if(l1.equals(l2)) {
                    return 0;
                } else {
                    if(l1.isEmpty())
                        return 1;
                    if(l2.isEmpty())
                        return -1;
                    int i = 0;
                    while (l1.get(i).equals(l2.get(i))) i++;
                    if(l1.get(i) > l2.get(i))
                        return 1;
                    else
                        return -1;
                }

TreeSet的底层是用TreeMap来实现的,这里附上TreeMap里面比较节点是否相等的代码:

public V put(K key, V value) {
    ...
    do {  
        parent = t;  
        cmp = cpr.compare(key, t.key); 
        if (cmp < 0)  
            // 如果比根节点小,就与小于根节点的值比
            t = t.left;  
        else if (cmp > 0)  
            // 如果比根节点大,就与大于根节点的值比
            t = t.right;  
        else  
            // 如果相等,就覆盖旧的值并返回
            return t.setValue(value);  
    } while (t != null);
    ...
}

再举个栗子,若比较器中没有返回全0,1,-1会导致部分数据无法去重的原因:
假如你写入的数据分别是:A、B、C、A
插入A:树没有节点,所以A作为根节点
插入B:存在根节点A,compare方法返回1,表示B比A大,所以B作为A的右节点
插入C:比较A和C,C比A大,继续跟A的右节点(比A大的值)比较,最终插到B的右节点。然后,红黑树调整,B作为根节点,A在B的左边的子节点,C在B的右边的子节点
插入A,这时会先跟根节点B比较,A比B大,所以继续跟B右侧的子节点比较(第一次写入的A在B的左侧,所以新写入的A不会再跟原来的A作比较),最终A作为C的右侧子节点


  • 2
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值