在java开发中,我们经常需要根据一组数据构建映射(map),以便快速查找和关联信息。一个常见场景是,我们有一个productkey列表,需要为每个productkey查找并关联一个productdetail。然而,并非所有的productkey都能找到对应的productdetail,这意味着在最终的映射中,某些productkey可能对应的值是null。此外,productkey本身也可能需要通过某种逻辑(如findproductkey方法)来确认其有效性或获取正确的实例。
我们的目标是:
为了实现这一目标,我们将利用Java 8的Stream API,特别是Collectors.toMap()方法。
构建目标映射的关键在于合理设计Stream管道,以处理ProductKey的查找和潜在的空值。
假设我们有以下基本类定义(为简洁,省略了部分Lombok注解和getter/setter):
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.math.BigDecimal;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.Function;
import java.util.stream.Collectors;
@Data
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
static class ProductKey {
@EqualsAndHashCode.Include
private Long productCode;
@EqualsAndHashCode.Include
private Long productDetailCode; // 假设此字段可能为null,或不用于查找
}
@Data
static class Product {
@EqualsAndHashCode.Include
private ProductKey productKey;
private ProductDetail productDetail; // 初始可能为null
}
@Data
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
static class ProductDetail {
@EqualsAndHashCode.Include
private Long productCode;
private String description;
private BigDecimal price;
private String category;
}
// 辅助方法,模拟查找ProductKey,可能返回Optional.empty()
public static Optional findProductKey(Long productCode, List productKeys) {
return productKeys.stream()
.filter(productKey -> productCode.equals(productKey.getProductCode()))
// takeWhile(productKey -> productKey != null) 在这里是冗余的,
// 因为filter已经确保了productKey非null,且Optional.findFirst()会处理空流
.findFirst();
}
// 辅助方法,模拟将ProductDetail列表转换为Map
public static Map mapProductCodeToProductDetail(List productDetailList) {
return productDetailList.stream()
.collect(Collectors.toMap(
ProductDetail::getProductCode,
Function.identity(),
(existing, replacement) -> existing // 处理重复productCode的情况
));
} 现在,我们来构建Map
// 模拟数据初始化 ListproductKeyList = List.of( new ProductKey() {{ setProductCode(101L); setProductDetailCode(1L); }}, new ProductKey() {{ setProductCode(102L); setProductDetailCode(2L); }}, new ProductKey() {{ setProductCode(103L); setProductDetailCode(3L); }}, // 假设103没有对应的ProductDetail new ProductKey() {{ setProductCode(101L); setProductDetailCode(4L); }} // 模拟重复的productCode,但ProductKey不同 ); List productDetailRawList = List.of( new ProductDetail() {{ setProductCode(101L); setDescription("Detail A"); setPrice(BigD ecimal.valueOf(10.0)); }}, new ProductDetail() {{ setProductCode(102L); setDescription("Detail B"); setPrice(BigDecimal.valueOf(20.0)); }} ); // 首先,将原始的ProductDetail列表转换为以productCode为键的Map,方便查找 Map
productDetailMap = mapProductCodeToProductDetail(productDetailRawList); // 构建 Map Map prodDetailByKey = productKeyList.stream() // 1. 调用 findProductKey 模拟查找,将每个 ProductKey 转换为 Optional .map(productKey -> findProductKey(productKey.getProductCode(), productKeyList)) // 2. 过滤掉空的 Optional,只保留成功找到 ProductKey 的元素 .filter(Optional::isPresent) // 3. 从 Optional 中提取实际的 ProductKey 对象 .map(Optional::get) // 4. 使用 Collectors.toMap() 进行收集 .collect(Collectors.toMap( Function.identity(), // keyMapper: ProductKey 本身就是我们想要的键 productKey -> productDetailMap.get(productKey.getProductCode()), // valueMapper: 根据 ProductKey 的 productCode 从 productDetailMap 中获取 ProductDetail (existing, replacement) -> existing // mergeFunction: 处理如果 ProductKey 列表有重复 ProductKey 导致键冲突的情况,这里选择保留旧值 )); System.out.println("生成的映射 (prodDetailByKey):"); prodDetailByKey.forEach((key, value) -> System.out.println(" Key: " + key.getProductCode() + ", Value: " + (value != null ? value.getDescription() : "null")));
代码解析:
一旦我们有了Map
// 模拟 Product 列表 Listproducts = List.of( new Product() {{ setProductKey(new ProductKey() {{ setProductCode(101L); setProductDetailCode(1L); }}); }}, new Product() {{ setProductKey(new ProductKey() {{ setProductCode(102L); setProductDetailCode(2L); }}); }}, new Product() {{ setProductKey(new ProductKey() {{ setProductCode(103L); setProductDetailCode(3L); }}); }} // 103没有对应的ProductDetail ); System.out.println("\n更新前的 Product 列表:"); products.forEach(p -> System.out.println(" ProductKey: " + p.getProductKey().getProductCode() + ", Detail: " + (p.getProductDetail() != null ? p.getProductDetail().getDescription() : "null"))); // 使用 forEach 循环更新 Product 对象的 ProductDetail products.forEach(product -> { ProductDetail detail = prodDetailByKey.get(product.getProductKey()); product.setProductDetail(detail); }); System.out.println("\n更新后的 Product 列表:"); products.forEach(p -> System.out.println(" ProductKey: " + p.getProductKey().getProductCode() + ", Detail: " + (p.getProductDetail() != null ? p.getProductDetail().getDescription() : "null")));
代码解析:
Map.get()返回null的含义: 当我们从一个Map中通过get(key)方法获取值时,如果Map中不包含该key,或者该key对应的值就是null,get()方法都会返回null。在我们的场景中,prodDetailByKey.get(product.getProductKey())会返回null,如果productDetailMap中没有product.getProductKey().getProductCode()对应的详情。这意味着Product对象的productDetail属性将被设置为null,这正是我们处理“Product没有ProductDetail”情况的方式。
移除现有映射中的空值: 如果在某些场景下,你希望从一个已经存在的映射中移除所有值为null的条目,可以使用以下方法:
import java.util.Objects; // ... // 假设 productDetailMap 包含一些值为 null 的条目 productDetailMap.values().removeIf(Objects::isNull); // 或者如果你想移除键值对,可以迭代 entrySet productDetailMap.entrySet().removeIf(entry -> entry.getValue() == null);
但请注意,这通常是在Map构建完成后进行的清理操作,而不是构建过程中处理潜在null值的方式。在上述教程的构建过程中,Collectors.toMap允许将null作为值存储。
ProductKey的equals()和hashCode(): 作为Map的键,ProductKey类必须正确实现equals()和hashCode()方法。Lombok的@EqualsAndHashCode注解(并指定onlyExplicitlyIncluded = true)是一个方便的解决方案,它确保只有被@EqualsAndHashCode.Include标记的字段才参与到这两个方法的计算中,这对于Map的正确行为至关重要。
Optional的正确使用: Optional是为了避免直接返回null并强制调用者处理“值可能不存在”的情况。在Stream管道中,它常用于表示中间结果的存在性,并通过filter(Optional::isPresent)和map(Optional::get)来安全地提取值。
通过Java 8的Stream API和Collectors.toMap(),我们可以优雅且高效地构建复杂的映射,即使键或值可能涉及Optional或null。关键在于理解Stream管道的各个操作符(map、filter)如何协同工作,以及Collectors.toMap()的keyMapper、valueMapper和mergeFunction参数如何定义映射的构建逻辑。在更新对象属性时,对于带有副作用的操作,Iterable.forEach()往往能提供更简洁直观的代码。正确处理equals()和hashCode()以及理解Optional的语义,是确保这些操作正确性的基础。