在日常的開發工作中,我們經常使用到Java Stream,特別是Stream API中提供的Collectors.toList()收集器,
但有些場景下,我們需要將集合轉換為Map,這時候就需要使用到Stream API中提供的另一個收集器:
Collectors.toMap,它可以將流中的元素映射為鍵值對,並收集到一個Map中。
1. 三種主要的重載方法
Collectors.toMap有3種重載方法,分別是:
1)兩個參數的重載方法(最簡單的形式)
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)三個參數的重載方法(包含衝突處理)
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實現)
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為城市名稱,轉換方法如下所示:
@Getter
@Setter
public class City {
private Integer cityId;
private String cityName;
public City(Integer cityId, String cityName) {
this.cityId = cityId;
this.cityName = cityName;
}
}
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);
輸出結果:
2.2 將對象列表轉換為Map(ID -> 對象)
仍然使用上面的城市列表,需要將其轉換為Map,其中Key為城市ID、Value為城市對象,轉換方法如下所示:
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
城市名稱: 北京
上面的寫法等價於:
Map<Integer, City> cityMap = cityList.stream()
.collect(Collectors.toMap(City::getCityId, Function.identity()));
因為Function.identity()內部實現是下面這樣的:
static <T> Function<T, T> identity() {
return t -> t;
}
2.3 鍵衝突處理
假設上面的城市列表中有一個ID重複的城市:
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)方式一:保留舊值
Map<Integer, String> cityMap = cityList.stream()
.collect(Collectors.toMap(City::getCityId, City::getCityName, (oldValue, newValue) -> oldValue));
輸出結果:
城市ID: 4, 城市名稱: 深圳
2)方式二:使用新值
Map<Integer, String> cityMap = cityList.stream()
.collect(Collectors.toMap(City::getCityId, City::getCityName, (oldValue, newValue) -> newValue));
輸出結果:
城市ID: 4, 城市名稱: 天津
3)方式三:合併值
Map<Integer, String> cityMap = cityList.stream()
.collect(Collectors.toMap(City::getCityId, City::getCityName,
(oldValue, newValue) -> oldValue + ", " + newValue));
輸出結果:
城市ID: 4, 城市名稱: 深圳, 天津
2.4 數據分組聚合
假設有一個銷售記錄列表,需要將其轉換為Map,其中Key為銷售員、Value為該銷售員的總銷售額,轉換方法如下所示:
@Getter
@Setter
public class SalesRecord {
private String salesPerson;
private BigDecimal amount;
public SalesRecord(String salesPerson, BigDecimal amount) {
this.salesPerson = salesPerson;
this.amount = amount;
}
}
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);
輸出結果:
上面的例子是銷售額累加,也可以只取最小值:
Map<String, BigDecimal> salesRecordMap = salesRecordList.stream()
.collect(Collectors.toMap(SalesRecord::getSalesPerson, SalesRecord::getAmount, BigDecimal::min));
此時的輸出結果:
或者只取最大值:
Map<String, BigDecimal> salesRecordMap = salesRecordList.stream()
.collect(Collectors.toMap(SalesRecord::getSalesPerson, SalesRecord::getAmount, BigDecimal::max));
此時的輸出結果:
2.5 指定Map實現
默認情況下,Collectors.toMap是將結果收集到HashMap中,如果有需要,我們也可以指定成TreeMap或者LinkedHashMap。
如果想要保持插入順序,可以指定使用LinkedHashMap:
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);
輸出結果:
如果想要按鍵排序,可以指定使用TreeMap:
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);
輸出結果:
3. 注意事項
3.1 空異常
如果valueMapper中取出的值有null值,會拋出java.lang.NullPointerException異常,如下示例:
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值:
Map<Integer, String> cityMap = cityList.stream()
.filter(city -> city.getCityName() != null)
.collect(Collectors.toMap(City::getCityId, City::getCityName));
第二種解決方案是提供默認值:
Map<Integer, String> cityMap = cityList.stream()
.collect(Collectors.toMap(City::getCityId,
city -> Optional.ofNullable(city.getCityName()).orElse("未知")));
3.2 鍵重複異常
如果出現重複鍵,且沒有提供mergeFunction參數,會拋出java.lang.IllegalStateException異常,如下示例:
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異常。
文章持續更新,歡迎關注微信公眾號「申城異鄉人」第一時間閲讀!