博客 / 詳情

返回

使用Java Stream,將集合轉換為一對一Map

在日常的開發工作中,我們經常使用到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異常。

文章持續更新,歡迎關注微信公眾號「申城異鄉人」第一時間閲讀!

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.