List的去重, Java8 中distinct的使用

常规List转Map

Java8使用lambda表达式进行函数式编程可以对集合进行非常方便的操作。一个比较常见的操作是将list转换成map,一般使用Collectors的toMap()方法进行转换。一个比较常见的问题是当list中含有相同元素的时候,如果不指定取哪一个,则会抛出异常。因此,这个指定是必须的。

public class ListToDistinctTest {

    @AllArgsConstructor
    @NoArgsConstructor
    @ToString
    @Data
    private static class UserInfo {

        int id;
        int width;
        int height;
    }


    public static void main(String[] args) {

List<UserInfo> userInfoList = Arrays.asList(new UserInfo(100, 2, 3), new UserInfo(101, 2, 4),
                new UserInfo(100, 2, 3));
        //常规list转map去重
        Map<Integer, UserInfo> userInfoMap1 = userInfoList.stream().collect(Collectors.toMap(UserInfo::getId, userInfo -> userInfo, (u1, u2) -> u1));
        System.out.println("常规list转map去重");
        userInfoMap1.forEach((x, y) -> System.out.println("<" + x + ", " + y + ">"));
    }
}

输出结果

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Fzcw9nRP-1615181699678)(C:\Users\fei\AppData\Local\Temp\1615173748751.png)]

当然,使用toMap()的另一个重载方法,可以直接指定。这里,还有另一种方法:在进行转map的操作之前,能不能使用distinct()先把list的重复元素过滤掉,然后转map的时候就不用考虑重复元素的问题了

接下来测试一下

##使用distinct()给list去重


package com.cyc.basic.test.jdk8;

import lombok.*;

import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

/**
 * @author chenyunchang
 * @title
 * @date 2021/3/8 10:45
 * @Description:
 */
public class ListToDistinctTest {

    @AllArgsConstructor
    @NoArgsConstructor
    @ToString
    @Getter
    private static class UserInfo {

        int id;
        int width;
        int height;
    }


    public static void main(String[] args) {

        List<UserInfo> userInfoList = Arrays.asList(new UserInfo(100, 2, 3), new UserInfo(101, 2, 4),
                new UserInfo(100, 2, 3));
        //常规list转map去重
        Map<Integer, UserInfo> userInfoMap1 = userInfoList.stream().collect(Collectors.toMap(UserInfo::getId, userInfo -> userInfo, (u1, u2) -> u1));
        System.out.println("常规list转map去重");
        userInfoMap1.forEach((x, y) -> System.out.println("<" + x + ", " + y + ">"));


        // 使用list.stream.distinct给集合去重

        Map<Integer, UserInfo> userInfoMap2 = userInfoList.stream().distinct().collect(
                Collectors.toMap(UserInfo::getId, x -> x)
        );
        System.out.println("使用list.stream.distinct给集合去重");
        userInfoMap2.forEach((x, y) -> System.out.println("<" + x + ", " + y + ">"));
    }
}


[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wV6kPY7m-1615181699682)(C:\Users\fei\AppData\Local\Temp\1615173143817.png)]

可以看到,直接去重,报错

list里总共有三个元素,其中有两个我们认为是重复的。第一种转换是使用toMap()直接指定了对重复key的处理情况,因此可以正常转换成map。而第二种转换是想先对list进行去重,然后再转换成map,结果还是失败了,抛出了IllegalStateException,所以distinct()应该是失败了。

错误信息

Exception in thread "main" java.lang.IllegalStateException: Duplicate key ListToDistinctTest.UserInfo(id=100, width=2, height=3)
	at java.util.stream.Collectors.lambda$throwingMerger$0(Collectors.java:133)
	at java.util.HashMap.merge(HashMap.java:1254)
	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 com.cyc.basic.test.jdk8.ListToDistinctTest.main(ListToDistinctTest.java:42)
Disconnected from the target VM, address: '127.0.0.1:55598', transport: 'socket'

原因:distinct()依赖于equals()

Returns a stream consisting of the distinct elements (according to {@link Object#equals(Object)}) of this stream.

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

但是Object的equals()方法只有在两个对象完全相同时才返回true。而我们想要的效果是只要UserInfo的id/width/height均相同,就认为两个UserInfo对象是同一个。所以我们比如重写属于UserInfo的equals()方法。

注意: 重写equals方法后, 一定要重写该对象的hashcode方法

必须使得重写后的equals()满足如下条件:

  • 根据equals()进行比较,相等的两个对象,hashCode()的值也必须相同;
  • 根据equals()进行比较,不相等的两个对象,hashCode()的值可以相同,也可以不同;

因为这是Java的规定,违背这些规定将导致Java程序运行不再正常。

最简单解决方法

在对象UserInfo类上将@Getter注解改为@Data注解

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-snfTnyGt-1615181699684)(C:\Users\fei\AppData\Local\Temp\1615174433587.png)]

查看结果

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NtYstOfD-1615181699688)(C:\Users\fei\AppData\Local\Temp\1615174449720.png)]

因为@Data注解, 已经将equals和hashCode方法重写,

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FBKvotVP-1615181699689)(C:\Users\fei\AppData\Local\Temp\1615174576302.png)]

假设类是别人的,不能修改

以上,UserInfo使我们自己写的类,我们可以往里添加equals()和hashCode()方法。如果VideoInfo是我们引用的依赖中的一个类,我们无权对其进行修改,那么是不是就没办法使用distinct()按照某些元素是否相同,对对象进行自定义的过滤了呢?

使用wrapper

我们可以找到一个可行的方法:使用wrapper。

假设在一个依赖中(我们无权修改该类),VideoInfo定义如下:

 @AllArgsConstructor
    @NoArgsConstructor
    @ToString
    @Getter
    private static class UserInfo {
        String id;
        int width;
        int height;
    }

Wrapper 如下

    private static class UserInfoWrapper {

        private final UserInfo userInfo;



        private UserInfoWrapper(UserInfo userInfo) {
            this.userInfo = userInfo;
        }

        public UserInfo unwrap() {
            return userInfo;
        }

        @Override
        public boolean equals(Object obj) {
            if (!(obj instanceof UserInfo)) {
                return false;
            }
            UserInfo vi = (UserInfo) obj;
            return userInfo.id.equals(vi.id)
                    && userInfo.width == vi.width
                    && userInfo.height == vi.height;
        }

        @Override
        public int hashCode() {
            int n = 31;
            n = n * 31 + userInfo.id.hashCode();
            n = n * 31 + userInfo.height;
            n = n * 31 + userInfo.width;
            return n;
        }
    }

整个wrapper的思路无非就是构造另一个类UserInfoWrapper,把hashCode()和equals()添加到wrapper中,这样便可以按照自定义规则对wrapper对象进行自定义的过滤。

我们没法自定义过滤UserInfo,但是我们可以自定义过滤UserInfoWrapper啊!

之后要做的,就是将UserInfo全部转化为UserInfoWrapper,然后过滤掉某些UserInfoWrapper,再将剩下的UserInfoWrapper转回UserInfo,以此达到过滤UserInfo的目的。很巧妙

使用“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 com.cyc.basic.test.jdk8;

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<UserInfo> list = Arrays.asList(new UserInfo("123", 1, 2),
                new UserInfo("456", 4, 5), new UserInfo("123", 1, 2));

        // Get distinct only
        Map<String, UserInfo> userInfoMap = list.stream().filter(distinctByKey(UserInfo::getId)).collect(
                Collectors.toMap(UserInfo::getId, x -> x,
                        (oldValue, newValue) -> newValue)
        );

        userInfoMap.forEach((x, y) -> System.out.println("<" + x + ", " + y + ">"));
    }

    /**
     * 自定义过滤函数
     * @param keyExtractor
     * @param <T>
     * @return
     */
    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 UserInfo {
    @Getter
    String id;
    int width;
    int height;
}
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
有多种方法可以在JavaList进行去。 一种方法是使用for循环来去。你可以遍历List的每个元素,然后再遍历剩下的元素,如果发现复的元素,就将其从List移除。这样就可以实现去。例如,在给定的代码,通过双for循环去的方法被用来去除List复项。 另一种方法是使用Java 8的Stream API。你可以将List转换为一个Stream,然后使用distinct()方法去除复项,最后将结果收集到一个新的List。这样就能得到一个没有复项的List。在给定的代码,通过Stream API的方式去的方法被用来从List删除复项。 还有一种方法是使用HashSet。你可以将List转换为HashSet,因为HashSet具有去的特性,然后再将HashSet转换回List。这样就能得到一个没有复项的List。在给定的代码,通过使用HashSet去的方法被用来去除List复项。 根据你的需求和具体情况,你可以选择其一种方法来进行list操作。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* [java List去除复数据的五种方式](https://blog.csdn.net/m0_67900727/article/details/123422447)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] - *3* [JavaList】去的 6种方法](https://blog.csdn.net/weixin_43825761/article/details/127778880)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

意田天

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值