使用Java Stream,将集合转换为一对一Map

在日常的开发工作中,我们经常使用到Java Stream,特别是Stream API中提供的Collectors.toList()收集器,

但有些场景下,我们需要将集合转换为Map,这时候就需要使用到Stream API中提供的另一个收集器:

Collectors.toMap,它可以将流中的元素映射为键值对,并收集到一个Map中。

1. 三种主要的重载方法

Collectors.toMap有3种重载方法,分别是:

1)两个参数的重载方法(最简单的形式)

java 复制代码
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);
}

2)三个参数的重载方法(包含冲突处理)

java 复制代码
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);
}

3)四个参数的重载方法(指定Map实现)

java 复制代码
public static <T, K, U, M extends Map<K, U>>
   Collector<T, ?, M> toMap(Function<? super T, ? extends K> keyMapper,
                            Function<? super T, ? extends U> valueMapper,
                            BinaryOperator<U> mergeFunction,
                            Supplier<M> mapSupplier) {
    BiConsumer<M, T> accumulator
            = (map, element) -> map.merge(keyMapper.apply(element),
                                          valueMapper.apply(element), mergeFunction);
    return new CollectorImpl<>(mapSupplier, accumulator, mapMerger(mergeFunction), CH_ID);
}

接下来,我们结合使用示例详细讲解。

2. 使用示例

2.1 将对象的某些属性转换为Map

假设有一个城市列表,需要将其转换为Map,其中Key为城市ID、Value为城市名称,转换方法如下所示:

java 复制代码
@Getter
@Setter
public class City {
    private Integer cityId;

    private String cityName;

    public City(Integer cityId, String cityName) {
        this.cityId = cityId;
        this.cityName = cityName;
    }
}
java 复制代码
List<City> cityList = Arrays.asList(
        new City(1, "北京"),
        new City(2, "上海"),
        new City(3, "广州"),
        new City(4, "深圳")
);
Map<Integer, String> cityMap = cityList.stream()
        .collect(Collectors.toMap(City::getCityId, City::getCityName));
System.out.println(cityMap);

输出结果:

{1=北京, 2=上海, 3=广州, 4=深圳}

2.2 将对象列表转换为Map(ID -> 对象)

仍然使用上面的城市列表,需要将其转换为Map,其中Key为城市ID、Value为城市对象,转换方法如下所示:

java 复制代码
List<City> cityList = Arrays.asList(
        new City(1, "北京"),
        new City(2, "上海"),
        new City(3, "广州"),
        new City(4, "深圳")
);
Map<Integer, City> cityMap = cityList.stream()
        .collect(Collectors.toMap(City::getCityId, city -> city));
City city = cityMap.get(1);
System.out.println("城市ID: " + city.getCityId());
System.out.println("城市名称: " + city.getCityName());

输出结果如下所示:

城市ID: 1 城市名称: 北京

上面的写法等价于:

java 复制代码
Map<Integer, City> cityMap = cityList.stream()
    	.collect(Collectors.toMap(City::getCityId, Function.identity()));

因为Function.identity()内部实现是下面这样的:

java 复制代码
static <T> Function<T, T> identity() {
    return t -> t;
}

2.3 键冲突处理

假设上面的城市列表中有一个ID重复的城市:

java 复制代码
List<City> cityList = Arrays.asList(
        new City(1, "北京"),
        new City(2, "上海"),
        new City(3, "广州"),
        new City(4, "深圳"),
        new City(4, "天津")
);
Map<Integer, String> cityMap = cityList.stream()
        .collect(Collectors.toMap(City::getCityId, City::getCityName));
System.out.println("城市ID: 4, 城市名称: " + cityMap.get(4));

此时运行代码,会抛出java.lang.IllegalStateException异常,如下图所示:

有3种常见的键冲突处理方式,分别是保留旧值、使用新值和合并值,接下来一一讲解。

1)方式一:保留旧值

java 复制代码
Map<Integer, String> cityMap = cityList.stream()
        .collect(Collectors.toMap(City::getCityId, City::getCityName, (oldValue, newValue) -> oldValue));

输出结果:

城市ID: 4, 城市名称: 深圳

2)方式二:使用新值

java 复制代码
Map<Integer, String> cityMap = cityList.stream()
        .collect(Collectors.toMap(City::getCityId, City::getCityName, (oldValue, newValue) -> newValue));

输出结果:

城市ID: 4, 城市名称: 天津

3)方式三:合并值

java 复制代码
Map<Integer, String> cityMap = cityList.stream()
        .collect(Collectors.toMap(City::getCityId, City::getCityName, 
        		(oldValue, newValue) -> oldValue + ", " + newValue));

输出结果:

城市ID: 4, 城市名称: 深圳, 天津

2.4 数据分组聚合

假设有一个销售记录列表,需要将其转换为Map,其中Key为销售员、Value为该销售员的总销售额,转换方法如下所示:

java 复制代码
@Getter
@Setter
public class SalesRecord {
    private String salesPerson;

    private BigDecimal amount;

    public SalesRecord(String salesPerson, BigDecimal amount) {
        this.salesPerson = salesPerson;
        this.amount = amount;
    }
}
java 复制代码
List<SalesRecord> salesRecordList = Arrays.asList(
        new SalesRecord("张三", new BigDecimal("1000")),
        new SalesRecord("李四", new BigDecimal("2000")),
        new SalesRecord("张三", new BigDecimal("980"))
);

Map<String, BigDecimal> salesRecordMap = salesRecordList.stream()
        .collect(Collectors.toMap(SalesRecord::getSalesPerson, SalesRecord::getAmount, BigDecimal::add));
System.out.println(salesRecordMap);

输出结果:

{李四=2000, 张三=1980}

上面的例子是销售额累加,也可以只取最小值:

java 复制代码
Map<String, BigDecimal> salesRecordMap = salesRecordList.stream()
        .collect(Collectors.toMap(SalesRecord::getSalesPerson, SalesRecord::getAmount, BigDecimal::min));

此时的输出结果:

{李四=2000, 张三=980}

或者只取最大值:

java 复制代码
Map<String, BigDecimal> salesRecordMap = salesRecordList.stream()
        .collect(Collectors.toMap(SalesRecord::getSalesPerson, SalesRecord::getAmount, BigDecimal::max));

此时的输出结果:

{李四=2000, 张三=1000}

2.5 指定Map实现

默认情况下,Collectors.toMap是将结果收集到HashMap中,如果有需要,我们也可以指定成TreeMap或者LinkedHashMap。

如果想要保持插入顺序,可以指定使用LinkedHashMap:

java 复制代码
List<City> cityList = Arrays.asList(
        new City(2, "上海"),
        new City(1, "北京"),
        new City(4, "深圳"),
        new City(3, "广州")
);
Map<Integer, String> cityMap = cityList.stream()
        .collect(Collectors.toMap(City::getCityId, City::getCityName,
                (existing, replacement) -> existing, LinkedHashMap::new));
System.out.println(cityMap);

输出结果:

{2=上海, 1=北京, 4=深圳, 3=广州}

如果想要按键排序,可以指定使用TreeMap:

java 复制代码
List<City> cityList = Arrays.asList(
        new City(2, "上海"),
        new City(1, "北京"),
        new City(4, "深圳"),
        new City(3, "广州")
);
Map<Integer, String> cityMap = cityList.stream()
        .collect(Collectors.toMap(City::getCityId, City::getCityName,
                (existing, replacement) -> existing, TreeMap::new));
System.out.println(cityMap);

输出结果:

{1=北京, 2=上海, 3=广州, 4=深圳}

3. 注意事项

3.1 空异常

如果valueMapper中取出的值有null值,会抛出java.lang.NullPointerException异常,如下示例:

java 复制代码
List<City> cityList = Arrays.asList(
        new City(1, "北京"),
        new City(2, "上海"),
        new City(3, "广州"),
        new City(4, "深圳"),
        new City(5, null)
);
Map<Integer, String> cityMap = cityList.stream()
        .collect(Collectors.toMap(City::getCityId, City::getCityName));
System.out.println(cityMap);

运行以上代码会抛出异常,如下图所示:

有两种解决方案,第一种解决方案是过滤null值:

java 复制代码
Map<Integer, String> cityMap = cityList.stream()
        .filter(city -> city.getCityName() != null)
        .collect(Collectors.toMap(City::getCityId, City::getCityName));

第二种解决方案是提供默认值:

java 复制代码
Map<Integer, String> cityMap = cityList.stream()
        .collect(Collectors.toMap(City::getCityId,
                city -> Optional.ofNullable(city.getCityName()).orElse("未知")));

3.2 键重复异常

如果出现重复键,且没有提供mergeFunction参数,会抛出java.lang.IllegalStateException异常,如下示例:

java 复制代码
List<City> cityList = Arrays.asList(
        new City(1, "北京"),
        new City(2, "上海"),
        new City(3, "广州"),
        new City(4, "深圳"),
        new City(4, "天津")
);
Map<Integer, String> cityMap = cityList.stream()
        .collect(Collectors.toMap(City::getCityId, City::getCityName));
System.out.println(cityMap);

运行以上代码会抛出异常,如下图所示:

解决方案见本篇文章2.3 键冲突处理部分。

4. 总结

Collectors.toMap是Stream API中提供的一个非常方便的收集器,它可以将流中的元素映射为键值对,并收集到一个Map中。

它适用于一对一映射的场景,但在使用时,要注意避免java.lang.NullPointerException异常和

java.lang.IllegalStateException异常。

相关推荐
num_killer1 小时前
小白的Langchain学习
java·python·学习·langchain
期待のcode2 小时前
Java虚拟机的运行模式
java·开发语言·jvm
程序员老徐2 小时前
Tomcat源码分析三(Tomcat请求源码分析)
java·tomcat
a程序小傲2 小时前
京东Java面试被问:动态规划的状态压缩和优化技巧
java·开发语言·mysql·算法·adb·postgresql·深度优先
仙俊红2 小时前
spring的IoC(控制反转)面试题
java·后端·spring
阿湯哥2 小时前
AgentScope Java 集成 Spring AI Alibaba Workflow 完整指南
java·人工智能·spring
小楼v2 小时前
说说常见的限流算法及如何使用Redisson实现多机限流
java·后端·redisson·限流算法
与遨游于天地3 小时前
NIO的三个组件解决三个问题
java·后端·nio
czlczl200209253 小时前
Guava Cache 原理与实战
java·后端·spring
yangminlei3 小时前
Spring 事务探秘:核心机制与应用场景解析
java·spring boot