Java8 : Stream / Map-Reduce / Collector를 사용하여 HashMap <X, Y>에서 HashMap <X, Z>로


209

-> List에서 간단한 Java를 "변환"하는 방법을 알고 있습니다 .YZ

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

이제 기본적으로 Map과 동일하게 수행하고 싶습니다.

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

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

솔루션은 String-> 로 제한되지 않아야합니다 Integer. List위 의 예와 마찬가지로 모든 메소드 (또는 생성자)를 호출하고 싶습니다.

답변:


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

목록 코드만큼 좋지는 않습니다. 통화 Map.Entry에서 새로운을 구성 할 수 없으므로 map()작업이 collect()통화에 혼합됩니다 .


59
당신은 대체 할 수 있습니다 e -> e.getKey()Map.Entry::getKey. 그러나 그것은 맛 / 프로그래밍 스타일의 문제입니다.
Holger

5
실제로 그것은 성능의 문제입니다. 귀하의 제안은 람다 '스타일'보다 약간 우수합니다.
Jon Burgin

36

다음은 Sotirios Delimanolis의 답변에 대한 변형입니다 . (+1)로 시작하는 것이 좋습니다. 다음을 고려하세요:

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))));
}

여기 몇 점이 있습니다. 첫 번째는 제네릭에 와일드 카드를 사용하는 것입니다. 이것은 기능을 다소 유연하게 만듭니다. 예를 들어, 출력 맵이 입력 맵 키의 수퍼 클래스 인 키를 갖도록하려면 와일드 카드가 필요합니다.

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

(지도의 값에 대한 예도 있지만 실제로 고안되어 있으며 Y에 대해 경계 와일드 카드를 사용하면 가장자리 경우에만 도움이된다는 것을 인정합니다.)

두 번째 포인트는 대신 입력지도의 이상 스트림을 실행하는 것입니다 entrySet, 나는 위를 달렸다 keySet. 이로 인해 맵 항목 대신 맵에서 값을 가져와야하는 비용으로 코드가 조금 더 깨끗해집니다. 우연히, 나는 처음 key -> key에 첫 번째 주장으로 toMap()있었고 어떤 이유로 타입 추론 오류로 실패했습니다. (X key) -> key작동했던 것처럼 변경했습니다 Function.identity().

또 다른 변형은 다음과 같습니다.

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;
}

Map.forEach()스트림 대신 사용 합니다. 지도와 함께 사용하기가 다소 어수선한 수집가가 필요 없기 때문에 훨씬 간단합니다. 그 이유는 Map.forEach()키와 값을 별도의 매개 변수로 제공하는 반면 스트림에는 하나의 값만 있으므로 키 또는 맵 항목을 해당 값으로 사용할지 여부를 선택해야하기 때문입니다. 마이너스 측면에서는 다른 접근 방식의 풍부하고 유선형의 장점이 부족합니다. :-)


11
Function.identity()멋지게 보일 수 있지만 첫 번째 솔루션에는 모든 항목에 대해 맵 / 해시 조회가 필요하지만 다른 모든 솔루션은 그렇지 않으므로 권장하지 않습니다.
Holger

13

이와 같은 일반적인 솔루션

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())));
}

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));

제네릭을 사용한 멋진 접근 방식. 그래도 조금 개선 될 수 있다고 생각합니다. 내 답변을 참조하십시오.
스튜어트 마크

13

구아바의 기능 Maps.transformValues은 당신이 찾고있는 것이며 람다 식과 잘 작동합니다.

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

이 접근 방식이 마음에 들지만 java.util.Function을 전달하지 않도록주의하십시오. com.google.common.base.Function이 필요하므로 Eclipse에서 도움이되지 않는 오류가 발생합니다. Function은 Function에 적용 할 수 없습니다. 혼란 스러울 수 있습니다. "transformValues ​​(Map <K, V1>, Function <? super V1 메소드 지도 유형의, V2>)는 인수 (Map <Foo, Bar>, Function <Bar, Baz>)에 적용 할 수 없습니다 "
mskfisher

를 전달해야하는 경우 java.util.Function두 가지 옵션이 있습니다. 1. 람다를 사용하여 Java 형식 유추로 알아낼 수 있으므로 문제를 피하십시오. 2. javaFunction :: apply와 같은 메소드 참조를 사용하여 유형 유추가 알아낼 수있는 새 람다를 생성하십시오.
Joe


5

표준 스트림 API를 향상시키는 My StreamEx 라이브러리는 EntryStream맵 변환에 더 적합한 클래스를 제공합니다 .

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

4

학습 목적으로 항상 존재하는 대안은 toMap () JDK 수집기가 간결 (+1 여기 ) 이지만 Collector.of ()를 통해 사용자 정의 수집기를 작성하는 것입니다 .

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;}
               ));

이 사용자 정의 수집기를 기본으로 시작하여 적어도 stream () 대신 parallelStream ()을 사용할 때 binaryOperator를 더 비슷하게 다시 작성해야 map2.entrySet().forEach(entry -> { if (map1.containsKey(entry.getKey())) { map1.get(entry.getKey()).merge(entry.getValue()); } else { map1.put(entry.getKey(),entry.getValue()); } }); return map1하거나 줄이면 값이 손실됩니다.
user691154

3

타사 라이브러리를 사용하는 것이 마음에 들지 않으면 cyclops-react lib에 Map을 포함한 모든 JDK Collection 유형에 대한 확장이 있습니다 . 'map'연산자를 사용하여지도를 직접 변환 할 수 있습니다 (기본적으로지도는지도의 값에 작용 함).

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

bimap을 사용하여 키와 값을 동시에 변환 할 수 있습니다

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

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.