toMap () 的默认行为是,如果遇到重复的键,就直接抛出IllegalStateException。这就好比你在玩消消乐,好不容易凑齐三个相同的元素,结果游戏直接闪退了。这种设计在大多数情况下是合理的,因为 Map 的键必须唯一。但在实际开发中,数据重复的情况并不少见,比如从数据库查询数据时,可能会因为业务逻辑问题导致重复记录。
举个栗子:
less 体验AI代码助手 代码解读复制代码List<Product> products = Arrays.asList(
new Product(1L, "苹果"),
new Product(1L, "香蕉")
);
Map<Long, String> productMap = products.stream()
.collect(Collectors.toMap(Product::getId, Product::getName));
这段代码会抛出Duplicate key异常,因为两个 Product 对象的 id 都是 1L。这时候,你可能会想:“我只是想保留最后一个出现的值,或者合并它们,难道就这么难吗?”
为了应对重复键的问题,toMap () 提供了一个三参数的重载方法,允许你自定义合并策略。例如,你可以选择保留旧值、替换新值,或者将两个值合并。
rust 体验AI代码助手 代码解读复制代码Map<Long, String> productMap = products.stream()
.collect(Collectors.toMap(
Product::getId,
Product::getName,
(oldValue, newValue) -> oldValue // 保留旧值
));
rust 体验AI代码助手 代码解读复制代码Map<Long, String> productMap = products.stream()
.collect(Collectors.toMap(
Product::getId,
Product::getName,
(oldValue, newValue) -> newValue // 替换新值
));
rust 体验AI代码助手 代码解读复制代码Map<Long, String> productMap = products.stream()
.collect(Collectors.toMap(
Product::getId,
Product::getName,
(oldValue, newValue) -> oldValue + "," + newValue // 合并值
));
这样,当遇到重复键时,toMap () 就会按照你定义的合并策略来处理,而不是直接抛出异常。但问题来了,这种方法需要你在代码中显式处理重复键,增加了代码的复杂性。而且,如果你的业务逻辑比较复杂,合并策略可能会变得难以维护。
Map 的键是不允许为 null 的(HashMap 允许,但 ConcurrentHashMap 不允许)。如果你在使用 toMap () 时,某个元素的键映射结果为 null,就会抛出NullPointerException。
举个栗子:
sql 体验AI代码助手 代码解读复制代码List<User> users = Arrays.asList(
new User(null, "张三"),
new User(1L, "李四")
);
Map<Long, String> userMap = users.stream()
.collect(Collectors.toMap(User::getId, User::getName));
这段代码会抛出NullPointerException,因为第一个 User 对象的 id 为 null。这时候,你可能会想:“我只是想过滤掉 id 为 null 的用户,难道就这么难吗?”
Map 的值是允许为 null 的,但在某些情况下,值为 null 可能会导致后续操作出现问题。例如,当你使用map.get(key)获取值时,如果值为 null,就需要进行判空处理。
举个栗子:
sql 体验AI代码助手 代码解读复制代码List<User> users = Arrays.asList(
new User(1L, null),
new User(2L, "李四")
);
Map<Long, String> userMap = users.stream()
.collect(Collectors.toMap(User::getId, User::getName));
String name = userMap.get(1L); // name为null,需要判空
为了避免这种情况,你可以在映射值时进行非空处理,或者在收集完成后过滤掉值为 null 的键值对。
Stream 的并行流可以充分利用多核 CPU 的优势,提高数据处理效率。但在使用 toMap () 时,并行流可能会导致性能问题,甚至数据混乱。
举个栗子:
ini 体验AI代码助手 代码解读复制代码List<User> users = generateLargeUserList(10_000_000);
Map<Long, User> userMap = users.parallelStream()
.collect(Collectors.toMap(User::getId, Function.identity()));
这段代码在并行流中使用 toMap (),可能会因为线程安全问题导致数据混乱。因为 toMap () 默认使用的是 HashMap,而 HashMap 在多线程环境下是非线程安全的。这时候,你可能会想:“我只是想提高处理效率,难道就这么难吗?”
为了解决并行流中的线程安全问题,Java 提供了Collectors.toConcurrentMap()方法。这个方法返回的是 ConcurrentHashMap,支持并发操作,性能更好。
举个栗子:
ini 体验AI代码助手 代码解读复制代码Map<Long, User> userMap = users.parallelStream() .collect(Collectors.toConcurrentMap(User::getId, Function.identity()));
这样,即使在并行流中使用 toConcurrentMap (),也能保证数据的一致性和线程安全。但需要注意的是,toConcurrentMap () 的性能并不一定比 toMap () 好,具体取决于数据量和并发程度。
既然 toMap () 有这么多坑,那有没有更好的替代方案呢?答案是肯定的。下面,我将为大家介绍几种常用的替代方法。
Collectors.groupingBy()是一个非常强大的收集器,它可以将流中的元素按照某个字段进行分组,返回一个 Map,其中键是分组字段的值,值是该分组下的元素列表。
举个栗子:
ini 体验AI代码助手 代码解读复制代码List<Order> orders = ...;
Map<String, List<Order>> orderMap = orders.stream()
.collect(Collectors.groupingBy(Order::getUserId));
这样,orderMap 的键是用户 id,值是该用户的所有订单列表。这种方法不仅可以避免重复键的问题,还可以方便地进行后续的统计和分析。
groupingBy () 还支持多级分组,即先按一个字段分组,再按另一个字段分组,返回一个嵌套的 Map。
举个栗子:
vbnet 体验AI代码助手 代码解读复制代码Map<String, Map<String, List<Order>>> orderMap = orders.stream()
.collect(Collectors.groupingBy(
Order::getUserId,
Collectors.groupingBy(Order::getStatus)
));
这样,orderMap 的结构是Map<用户id, Map<订单状态, List<订单>>>,可以方便地统计每个用户不同状态的订单数量。
groupingBy () 还可以与其他收集器结合使用,进行统计聚合操作。例如,统计每个用户的订单总数、总金额等。
举个栗子:
less 体验AI代码助手 代码解读复制代码Map<String, Long> orderCountMap = orders.stream()
.collect(Collectors.groupingBy(
Order::getUserId,
Collectors.counting()
));
Map<String, Double> totalAmountMap = orders.stream()
.collect(Collectors.groupingBy(
Order::getUserId,
Collectors.summingDouble(Order::getAmount)
));
这样,orderCountMap 的键是用户 id,值是该用户的订单总数;totalAmountMap 的键是用户 id,值是该用户的订单总金额。
前面已经介绍过,toMap () 的三参数重载方法可以自定义合并策略,处理重复键的问题。例如,保留旧值、替换新值或合并值。
为了避免键或值为 null 的问题,可以在流处理过程中进行过滤,或者在映射时提供默认值。
sql 体验AI代码助手 代码解读复制代码Map<Long, String> userMap = users.stream()
.filter(user -> user.getId() != null)
.collect(Collectors.toMap(User::getId, User::getName));
rust 体验AI代码助手 代码解读复制代码Map<Long, String> userMap = users.stream()
.collect(Collectors.toMap(
User::getId,
User::getName,
(oldValue, newValue) -> oldValue,
() -> new HashMap<>()
));
这里,第四个参数() -> new HashMap<>()是一个 Map 的供应商,用于指定返回的 Map 类型。如果不指定,默认返回的是 HashMap。
虽然 Java 提供的内置收集器已经能够满足大多数需求,但在某些情况下,我们可能需要更灵活的收集逻辑。例如,将流中的元素收集到一个自定义的 Map 中,或者在收集过程中进行复杂的转换和聚合操作。
自定义收集器需要实现Collector接口,该接口定义了四个方法:supplier()、accumulator()、combiner()和finisher(),以及一个characteristics()方法。
举个栗子:
typescript 体验AI代码助手 代码解读复制代码public class CustomCollector<T, K, V> implements Collector<T, Map<K, V>, Map<K, V>> {
privatefinalFunction<T, K> keyMapper;
privatefinalFunction<T, V> valueMapper;
privatefinal BinaryOperator<V> mergeFunction;
public CustomCollector(Function<T, K> keyMapper, Function<T, V> valueMapper, BinaryOperator<V> mergeFunction) {
this.keyMapper = keyMapper;
this.valueMapper = valueMapper;
this.mergeFunction = mergeFunction;
}
@Override
public Supplier<Map<K, V>> supplier() {
return HashMap::new;
}
@Override
public BiConsumer<Map<K, V>, T> accumulator() {
return (map, element) -> {
K key = keyMapper.apply(element);
V value = valueMapper.apply(element);
map.merge(key, value, mergeFunction);
};
}
@Override
public BinaryOperator<Map<K, V>> combiner() {
return (map1, map2) -> {
map2.forEach((key, value) -> map1.merge(key, value, mergeFunction));
return map1;
};
}
@Override
publicFunction<Map<K, V>, Map<K, V>> finisher() {
returnFunction.identity();
}
@Override
public Set<Characteristics> characteristics() {
return Collections.unmodifiableSet(EnumSet.of(Characteristics.IDENTITY_FINISH));
}
}
这个自定义收集器可以将流中的元素收集到一个 Map 中,支持自定义键映射、值映射和合并策略。使用时,可以像这样调用:
rust 体验AI代码助手 代码解读复制代码Map<Long, String> userMap = users.stream()
.collect(new CustomCollector<>(User::getId, User::getName, (oldValue, newValue) -> oldValue));
这样,就可以避免使用 toMap () 时的重复键和 null 值问题,同时保持代码的灵活性和可读性。
某电商平台需要分析用户的购买行为,统计每个用户的购买次数和总金额。要求将结果存储到一个 Map 中,其中键是用户 id,值是一个包含购买次数和总金额的对象。
css 体验AI代码助手 代码解读复制代码List<Order> orders = ...;
Map<Long, PurchaseStats> purchaseStatsMap = orders.stream()
.collect(Collectors.toMap(
Order::getUserId,
order -> new PurchaseStats(1, order.getAmount()),
(oldStats, newStats) -> new PurchaseStats(
oldStats.getCount() + newStats.getCount(),
oldStats.getTotalAmount() + newStats.getTotalAmount()
)
));
这段代码使用 toMap () 的三参数重载方法,自定义了合并策略,将每个用户的购买次数和总金额进行累加。
css 体验AI代码助手 代码解读复制代码Map<Long, PurchaseStats> purchaseStatsMap = orders.stream()
.filter(order -> order.getUserId() != null)
.collect(Collectors.toMap(
Order::getUserId,
order -> new PurchaseStats(1, order.getAmount()),
(oldStats, newStats) -> new PurchaseStats(
oldStats.getCount() + newStats.getCount(),
oldStats.getTotalAmount() + newStats.getTotalAmount()
)
));
css 体验AI代码助手 代码解读复制代码Map<Long, PurchaseStats> purchaseStatsMap = orders.parallelStream()
.filter(order -> order.getUserId() != null)
.collect(Collectors.toConcurrentMap(
Order::getUserId,
order -> new PurchaseStats(1, order.getAmount()),
(oldStats, newStats) -> new PurchaseStats(
oldStats.getCount() + newStats.getCount(),
oldStats.getTotalAmount() + newStats.getTotalAmount()
)
));
这样,可以提高处理效率,同时避免线程安全问题。
某系统需要分析日志数据,统计每个日志级别(如 INFO、WARN、ERROR)的日志数量,并将结果存储到一个 Map 中,其中键是日志级别,值是日志数量。
ini 体验AI代码助手 代码解读复制代码List<Log> logs = ...;
Map<String, Long> logCountMap = logs.stream()
.collect(Collectors.groupingBy(
Log::getLevel,
Collectors.counting()
));
这段代码使用 groupingBy () 和 counting () 收集器,简单高效地统计了每个日志级别的日志数量。
ini 体验AI代码助手 代码解读复制代码Map<String, Long> logCountMap = logs.parallelStream()
.collect(Collectors.groupingByConcurrent(
Log::getLevel,
Collectors.counting()
));
使用groupingByConcurrent()可以在并行流中高效地进行分组统计,提高处理效率。
toMap () 是一个强大的工具,但也是一个危险的工具。它的简单易用性可能会掩盖潜在的问题,导致代码在生产环境中出现意想不到的错误。因此,在使用 toMap () 时,一定要谨慎处理重复键、null 值和性能问题,或者选择更合适的替代方案。
本文系转载,前往查看
如有侵权,请联系 cloudcommunity@tencent.com 删除。
本文系转载,前往查看
如有侵权,请联系 cloudcommunity@tencent.com 删除。