java 8 stream toMap key的重复问题

解决java 8 stream toMap key的重复问题

问题描述

工作中遇到需要将某个List里面的实体类的两个属性对应起来,比如根据姓名找到年龄,就是将List里面的entity属性解析之后放到Map里。
实体类:

public class Person {
        private String name;
        private Integer age;
        ...省略getter、setter
    }

逻辑:

		Person person = new Person("张三",20);
        Person person1 = new Person("李四",20);
        Person person2 = new Person("张三",20);

        List<Person> list = new ArrayList<Person>(){{
            add(person);
            add(person1);
            add(person2);
        }};

        Map<String, Integer> map = new HashMap<>();
        //常规写法
        for (Person p : list) {
            map.put(p.getName(), p.getAge());
        }
        System.out.println(map);
        //stream 流写法
        Map<String, Integer> collect = list.stream().collect(Collectors.toMap(Person::getName, Person::getAge));
        System.out.println(collect);

单纯常规写法肯定没什么问题,就是后面的key会被覆盖,但是stream流不行,会报错。

Exception in thread "main" java.lang.IllegalStateException: Duplicate key 20

原因分析:

Collectors.toMap这个方法其实是有三个参数的,第一个是key,第二个是value,第三个是发生冲突的合并规则。
默认采用的就是冲突之后抛出异常的处理。

public static <T, K, U>
    Collector<T, ?, Map<K,U>> toMap(Function<? super T, ? extends K> keyMapper,
                                    Function<? super T, ? extends U> valueMapper) {
        return toMap(keyMapper, valueMapper, throwingMerger(), HashMap::new);
    }

解决方案:

只需要加上第三个参数即可

public static <T, K, U>
    Collector<T, ?, Map<K,U>> toMap(Function<? super T, ? extends K> keyMapper,
                                    Function<? super T, ? extends U> valueMapper,
                                    BinaryOperator<U> mergeFunction) {
        return toMap(keyMapper, valueMapper, mergeFunction, HashMap::new);
    }

新代码只需要更改第三个参数即可,会跟常规写法一个效果

        Map<String, Integer> collect = list.stream().collect(Collectors.toMap(Person::getName, Person::getAge, (oldValue, newValue) -> newValue));

当然还可以采用Collectors.groupingBy(keyFunction)的写法,但是返回值会是Map<key, List<value>>的形式。这就是新的业务场景了。
附上代码:

Map<String, List<Person>> collect1 = list.stream().collect(Collectors.groupingBy(Person::getName));
<think>我们讨论的是Java Stream中的`toMap`收集器。它用于将流中的元素转换为一个Map。常见的用法是提供两个函数:一个用于生成键(key),一个用于生成值(value)。需要注意的是,当键重复时,默认会抛出异常,因此我们通常需要提供合并函数(merge function)来处理冲突。 下面是一个使用`toMap`的示例。假设我们有一个`Person`类,包含id和name属性,我们想将流转换为一个以id为键、name为值的Map。 ```java import java.util.*; import java.util.stream.Collectors; public class ToMapExample { public static void main(String[] args) { List<Person> personList = Arrays.asList( new Person(1, "Alice"), new Person(2, "Bob"), new Person(3, "Charlie") ); // 使用toMap,指定键和值的提取函数 Map<Integer, String> idToName = personList.stream() .collect(Collectors.toMap( Person::getId, // 键:从Person对象中提取id Person::getName // 值:从Person对象中提取name )); System.out.println(idToName); } static class Person { private int id; private String name; public Person(int id, String name) { this.id = id; this.name = name; } public int getId() { return id; } public String getName() { return name; } } } ``` 但是,如果列表中有两个Person对象具有相同的id,那么上面的代码会抛出`IllegalStateException`,因为键重复了。为了避免这种情况,我们可以提供一个合并函数来处理冲突。例如,当id重复时,我们可以选择保留先出现的值,或者合并两个值(比如连接字符串)。 下面是一个处理重复键的例子: ```java // 假设有重复id的情况 List<Person> personListWithDuplicate = Arrays.asList( new Person(1, "Alice"), new Person(2, "Bob"), new Person(2, "Bobby") // 重复id ); // 使用合并函数,当键重复时,选择第一个值(或根据需求选择其他策略) Map<Integer, String> idToNameWithDup = personListWithDuplicate.stream() .collect(Collectors.toMap( Person::getId, Person::getName, (oldValue, newValue) -> oldValue // 当键重复时,保留旧值(先出现的) // 也可以选择保留新值:(oldValue, newValue) -> newValue // 或者合并:String::concat 或 (s1, s2) -> s1 + ", " + s2 )); System.out.println(idToNameWithDup); // 输出:{1=Alice, 2=Bob} 因为重复时保留了第一次出现的值 ``` 另外,`toMap`还有一个重载方法,可以指定生成的Map的具体类型(例如,我们需要一个`TreeMap`而不是默认的`HashMap`): ```java // 指定Map类型为TreeMap TreeMap<Integer, String> treeMap = personList.stream() .collect(Collectors.toMap( Person::getId, Person::getName, (oldValue, newValue) -> oldValue, // 合并函数 TreeMap::new // 指定Map类型 )); ``` ### 总结: - `Collectors.toMap` 有三个参数: 1. 键提取器(Function) 2. 值提取器(Function) 3. 合并函数(BinaryOperator) - 可选,用于处理键冲突 4. Map工厂(Supplier) - 可选,用于指定生成的Map类型 - 如果没有提供合并函数,当出现重复键时,会抛出`IllegalStateException`。 - 合并函数允许我们决定当两个值对应同一个键时如何处理(保留旧值、新值或合并值)。 ### 注意事项: 1. 键和值都不能为null,否则会抛出`NullPointerException`。 2. 如果流是并行的,合并函数可能会被多次调用,需要确保它是无状态且可结合的。 §§相关问题§§: 1. Java Stream中`toMap`和`groupingBy`有什么区别? 2. 当值为null时,`toMap`会如何处理? 3. 如何在`toMap`中处理键重复时合并多个值(例如,将值收集到列表中)? 4. 使用`toMap`时如何保持插入顺序?</think># Java Stream 中的 `toMap` 收集器详解 `Collectors.toMap()` 是 Java Stream API 中一个强大的收集器,用于将流元素转换为 Map 结构。它允许开发者灵活地指定键和值的提取逻辑,并处理键冲突等情况。 ## 基础用法 ```java import java.util.*; import java.util.stream.Collectors; public class ToMapExample { public static void main(String[] args) { List<Person> people = Arrays.asList( new Person(1, "Alice"), new Person(2, "Bob"), new Person(3, "Charlie") ); // 基本用法:提取ID作为键,姓名作为值 Map<Integer, String> idToName = people.stream() .collect(Collectors.toMap( Person::getId, // 键提取器 Person::getName // 值提取器 )); System.out.println(idToName); // {1=Alice, 2=Bob, 3=Charlie} } } class Person { private int id; private String name; public Person(int id, String name) { this.id = id; this.name = name; } public int getId() { return id; } public String getName() { return name; } } ``` ## 处理键冲突 当键可能重复时,需要提供合并函数来处理冲突: ```java List<Person> peopleWithDuplicates = Arrays.asList( new Person(1, "Alice"), new Person(2, "Bob"), new Person(2, "Robert") // 重复ID ); // 处理键冲突:保留第一个值 Map<Integer, String> firstValueWins = peopleWithDuplicates.stream() .collect(Collectors.toMap( Person::getId, Person::getName, (first, second) -> first // 合并函数:保留第一个值 )); // 处理键冲突:合并值 Map<Integer, List<String>> mergedValues = peopleWithDuplicates.stream() .collect(Collectors.toMap( Person::getId, person -> new ArrayList<>(Collections.singletonList(person.getName())), (list1, list2) -> { list1.addAll(list2); return list1; } )); System.out.println(firstValueWins); // {1=Alice, 2=Bob} System.out.println(mergedValues); // {1=[Alice], 2=[Bob, Robert]} ``` ## 指定 Map 实现类型 ```java // 指定具体的 Map 实现类型 TreeMap<Integer, String> sortedMap = people.stream() .collect(Collectors.toMap( Person::getId, Person::getName, (first, second) -> first, TreeMap::new )); System.out.println(sortedMap.getClass()); // class java.util.TreeMap ``` ## 复杂值提取 ```java // 提取复杂对象作为值 Map<Integer, Person> idToPerson = people.stream() .collect(Collectors.toMap( Person::getId, person -> person, // 值可以是整个对象 (first, second) -> first )); // 计算值 Map<Integer, Integer> nameLengthMap = people.stream() .collect(Collectors.toMap( Person::getId, person -> person.getName().length() )); ``` ## 注意事项 1. **键不能为 null**:如果键提取器返回 null,会抛出 NullPointerException 2. **值不能为 null**:如果值提取器返回 null,也会抛出 NullPointerException 3. **键冲突处理**:如果不提供合并函数且出现重复键,会抛出 IllegalStateException 4. **并行流**:合并函数需要满足结合律(associative)才能在并行流中正确工作 ## 替代方案 当需要更复杂的分组逻辑时,可以使用 `groupingBy`: ```java // 分组收集器 Map<Integer, List<Person>> groupedById = people.stream() .collect(Collectors.groupingBy(Person::getId)); ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

梦幻D开始

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

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

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

打赏作者

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

抵扣说明:

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

余额充值