Java8: HashMap <X, Y> sang HashMap <X, Z> bằng cách sử dụng Stream / Map-Giảm / Collector


209

Tôi biết cách "biến đổi" một Java đơn giản Listtừ Y-> Z, tức là:

List<String> x;
List<Integer> y = x.stream()
        .map(s -> Integer.parseInt(s))
        .collect(Collectors.toList());

Bây giờ tôi muốn làm về cơ bản giống với Bản đồ, nghĩa là:

INPUT:
{
  "key1" -> "41",    // "41" and "42"
  "key2" -> "42      // are Strings
}

OUTPUT:
{
  "key1" -> 41,      // 41 and 42
  "key2" -> 42       // are Integers
}

Giải pháp không nên giới hạn ở String-> Integer. Giống như trong Liství dụ trên, tôi muốn gọi bất kỳ phương thức (hoặc hàm tạo) nào.

Câu trả lời:


372
Map<String, String> x;
Map<String, Integer> y =
    x.entrySet().stream()
        .collect(Collectors.toMap(
            e -> e.getKey(),
            e -> Integer.parseInt(e.getValue())
        ));

Nó không đẹp như mã danh sách. Bạn không thể tạo các Map.Entrys mới trong một map()cuộc gọi để công việc được trộn vào collect()cuộc gọi.


59
Bạn có thể thay thế e -> e.getKey()bằng Map.Entry::getKey. Nhưng đó là vấn đề của hương vị / phong cách lập trình.
Holger

5
Trên thực tế đó là vấn đề về hiệu suất, đề xuất của bạn là hơi vượt trội so với 'phong cách' lambda
Jon Burgin

36

Dưới đây là một số biến thể về câu trả lời của Sotirios Delimanolis , khá hay khi bắt đầu bằng (+1). Hãy xem xét những điều sau đây:

static <X, Y, Z> Map<X, Z> transform(Map<? extends X, ? extends Y> input,
                                     Function<Y, Z> function) {
    return input.keySet().stream()
        .collect(Collectors.toMap(Function.identity(),
                                  key -> function.apply(input.get(key))));
}

Một vài điểm ở đây. Đầu tiên là việc sử dụng các ký tự đại diện trong các tổng quát; Điều này làm cho chức năng có phần linh hoạt hơn. Ví dụ, một ký tự đại diện sẽ là cần thiết nếu bạn muốn bản đồ đầu ra có một khóa đó là siêu lớp của khóa bản đồ đầu vào:

Map<String, String> input = new HashMap<String, String>();
input.put("string1", "42");
input.put("string2", "41");
Map<CharSequence, Integer> output = transform(input, Integer::parseInt);

(Ngoài ra còn có một ví dụ cho các giá trị của bản đồ, nhưng nó thực sự bị chiếm đoạt và tôi thừa nhận rằng việc có ký tự đại diện giới hạn cho Y chỉ giúp ích trong các trường hợp cạnh.)

Điểm thứ hai là thay vì chạy luồng trên bản đồ đầu vào entrySet, tôi đã chạy nó qua keySet. Điều này làm cho mã sạch hơn một chút, tôi nghĩ, với chi phí phải lấy các giá trị ra khỏi bản đồ thay vì từ mục nhập bản đồ. Ngẫu nhiên, ban đầu tôi đã có key -> keyđối số đầu tiên toMap()và điều này đã thất bại với một lỗi suy luận kiểu vì một số lý do. Thay đổi nó để (X key) -> keylàm việc, như đã làm Function.identity().

Một biến thể khác như sau:

static <X, Y, Z> Map<X, Z> transform1(Map<? extends X, ? extends Y> input,
                                      Function<Y, Z> function) {
    Map<X, Z> result = new HashMap<>();
    input.forEach((k, v) -> result.put(k, function.apply(v)));
    return result;
}

Điều này sử dụng Map.forEach()thay vì các luồng. Điều này thậm chí còn đơn giản hơn, tôi nghĩ, bởi vì nó phân phối với các nhà sưu tập, có phần vụng về để sử dụng với bản đồ. Lý do là Map.forEach()cung cấp khóa và giá trị dưới dạng tham số riêng biệt, trong khi luồng chỉ có một giá trị - và bạn phải chọn sử dụng khóa hoặc mục nhập bản đồ làm giá trị đó. Về mặt trừ, điều này thiếu đi sự tốt đẹp, phong phú của các phương pháp khác. :-)


11
Function.identity()có thể trông rất tuyệt nhưng vì giải pháp đầu tiên yêu cầu tra cứu bản đồ / băm cho mọi mục trong khi tất cả các giải pháp khác thì không, tôi không khuyến nghị.
Holger

13

Một giải pháp chung chung như vậy

public static <X, Y, Z> Map<X, Z> transform(Map<X, Y> input,
        Function<Y, Z> function) {
    return input
            .entrySet()
            .stream()
            .collect(
                    Collectors.toMap((entry) -> entry.getKey(),
                            (entry) -> function.apply(entry.getValue())));
}

Thí dụ

Map<String, String> input = new HashMap<String, String>();
input.put("string1", "42");
input.put("string2", "41");
Map<String, Integer> output = transform(input,
            (val) -> Integer.parseInt(val));

Cách tiếp cận tốt đẹp sử dụng thuốc generic. Tôi nghĩ rằng nó có thể được cải thiện một chút mặc dù - xem câu trả lời của tôi.
Stuart Marks

13

Chức năng của ổi Maps.transformValueslà những gì bạn đang tìm kiếm và nó hoạt động độc đáo với các biểu thức lambda:

Maps.transformValues(originalMap, val -> ...)

Tôi thích cách tiếp cận này, nhưng hãy cẩn thận để không vượt qua java.util.Function. Vì nó mong đợi com.google.common.base.Feft, Eclipse đưa ra một lỗi không có ích - nó nói Hàm không thể áp dụng cho Hàm, điều này có thể gây nhầm lẫn: "Phương thức biến đổi Giá trị (Bản đồ <K, V1>, Hàm <? Super V1 , V2>) trong loại Bản đồ không áp dụng cho các đối số (Bản đồ <Foo, Bar>, Hàm <Bar, Baz>) "
mskfisher

Nếu bạn phải vượt qua a java.util.Function, bạn có hai lựa chọn. 1. Tránh vấn đề bằng cách sử dụng lambda để cho phép suy luận kiểu Java tìm ra nó. 2. Sử dụng tham chiếu phương thức như javaFunction :: áp dụng để tạo lambda mới mà kiểu suy luận có thể tìm ra.
Joe


5

Thư viện StreamEx của tôi giúp tăng cường API luồng tiêu chuẩn cung cấp một EntryStreamlớp phù hợp hơn để chuyển đổi bản đồ:

Map<String, Integer> output = EntryStream.of(input).mapValues(Integer::valueOf).toMap();

4

Một giải pháp thay thế luôn tồn tại cho mục đích học tập là xây dựng trình thu thập tùy chỉnh của bạn thông qua Collector.of () mặc dù trình thu thập JDK của toMap () ở đây ngắn gọn (+1 ở đây ).

Map<String,Integer> newMap = givenMap.
                entrySet().
                stream().collect(Collector.of
               ( ()-> new HashMap<String,Integer>(),
                       (mutableMap,entryItem)-> mutableMap.put(entryItem.getKey(),Integer.parseInt(entryItem.getValue())),
                       (map1,map2)->{ map1.putAll(map2); return map1;}
               ));

Tôi đã bắt đầu với trình thu thập tùy chỉnh này làm cơ sở và muốn thêm rằng, ít nhất là khi sử dụngallelStream () thay vì stream (), binaryOperator nên được viết lại thành một cái gì đó gần giống với map2.entrySet().forEach(entry -> { if (map1.containsKey(entry.getKey())) { map1.get(entry.getKey()).merge(entry.getValue()); } else { map1.put(entry.getKey(),entry.getValue()); } }); return map1hoặc các giá trị sẽ bị mất khi giảm.
dùng691154

3

Nếu bạn không phiền khi sử dụng các thư viện của bên thứ 3, lib phản ứng cyclops của tôi có các phần mở rộng cho tất cả các loại Bộ sưu tập JDK , bao gồm cả Bản đồ . Chúng ta chỉ có thể chuyển đổi bản đồ trực tiếp bằng toán tử 'bản đồ' (theo mặc định bản đồ tác động lên các giá trị trong bản đồ).

   MapX<String,Integer> y = MapX.fromMap(HashMaps.of("hello","1"))
                                .map(Integer::parseInt);

bimap có thể được sử dụng để chuyển đổi các khóa và giá trị cùng một lúc

  MapX<String,Integer> y = MapX.fromMap(HashMaps.of("hello","1"))
                               .bimap(this::newKey,Integer::parseInt);

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.