web-dev-qa-db-ja.com

Javaストリームのあるマップ

Java変換とフィルタリングを行うマップがあります。簡単な例として、すべての値を整数に変換し、奇数のエントリを削除するとします。

_Map<String, String> input = new HashMap<>();
input.put("a", "1234");
input.put("b", "2345");
input.put("c", "3456");
input.put("d", "4567");

Map<String, Integer> output = input.entrySet().stream()
        .collect(Collectors.toMap(
                Map.Entry::getKey,
                e -> Integer.parseInt(e.getValue())
        ))
        .entrySet().stream()
        .filter(e -> e.getValue() % 2 == 0)
        .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));


System.out.println(output.toString());
_

これは正しく、次のようになります:_{a=1234, c=3456}_

ただし、.entrySet().stream()を2回呼び出すことを回避する方法があるのではないかと考えずにはいられません。

変換操作とフィルター操作の両方を実行し、.collect()を最後に1回だけ呼び出す方法はありますか?

35
Paul I

はい。各エントリを、キーと解析された整数値を保持する別の一時エントリにマップできます。次に、値に基づいて各エントリをフィルタリングできます。

Map<String, Integer> output =
    input.entrySet()
         .stream()
         .map(e -> new AbstractMap.SimpleEntry<>(e.getKey(), Integer.valueOf(e.getValue())))
         .filter(e -> e.getValue() % 2 == 0)
         .collect(Collectors.toMap(
             Map.Entry::getKey,
             Map.Entry::getValue
         ));

parseIntの代わりにInteger.valueOfを使用したことに注意してください。実際には、ボックス化されたintが必要だからです。


StreamEx ライブラリを使用する余裕がある場合は、非常に簡単に実行できます。

Map<String, Integer> output =
    EntryStream.of(input).mapValues(Integer::valueOf).filterValues(v -> v % 2 == 0).toMap();
41
Tunaki

より少ないオーバーヘッドで問題を解決する1つの方法は、マッピングとフィルタリングをコレクターに移動することです。

_Map<String, Integer> output = input.entrySet().stream().collect(
    HashMap::new,
    (map,e)->{ int i=Integer.parseInt(e.getValue()); if(i%2==0) map.put(e.getKey(), i); },
    Map::putAll);
_

これは、中間の_Map.Entry_インスタンスの作成を必要とせず、さらに良いことに、値が実際にintに追加される時点までMap値のボックス化を延期します。フィルターによって拒否された値は、ボックス化されていません。

Collectors.toMap(…)の動作と比較して、ここではキーの衝突を処理する必要がないことを事前に知っているので、_Map.put_ではなく_Map.merge_を使用することで操作も簡略化されます。

ただし、並列実行を利用したくない場合は、通常のループも考慮することができます

_HashMap<String,Integer> output=new HashMap<>();
for(Map.Entry<String, String> e: input.entrySet()) {
    int i = Integer.parseInt(e.getValue());
    if(i%2==0) output.put(e.getKey(), i);
}
_

または内部反復バリアント:

_HashMap<String,Integer> output=new HashMap<>();
input.forEach((k,v)->{ int i = Integer.parseInt(v); if(i%2==0) output.put(k, i); });
_

後者は非常にコンパクトで、少なくともシングルスレッドパフォーマンスに関する他のすべてのバリアントと同等です。

11
Holger

グアバ の友達:

Map<String, Integer> output = Maps.filterValues(Maps.transformValues(input, Integer::valueOf), i -> i % 2 == 0);

outputinputの変換され、フィルタリングされたviewであることに注意してください。それらを個別に操作する場合は、コピーを作成する必要があります。

5
shmosel

Stream.collect(supplier, accumulator, combiner) メソッドを使用して、エントリを変換し、条件付きで蓄積できます。

Map<String, Integer> even = input.entrySet().stream().collect(
    HashMap::new,
    (m, e) -> Optional.ofNullable(e)
            .map(Map.Entry::getValue)
            .map(Integer::valueOf)
            .filter(i -> i % 2 == 0)
            .ifPresent(i -> m.put(e.getKey(), i)),
    Map::putAll);

System.out.println(even); // {a=1234, c=3456}

ここでは、アキュムレータ内で、Optionalメソッドを使用して変換と述語の両方を適用し、オプションの値がまだ存在する場合は、収集中のマップに追加します。

これを行う別の方法は、変換されたMapから不要な値を削除することです。

Map<String, Integer> output = input.entrySet().stream()
        .collect(Collectors.toMap(
                Map.Entry::getKey,
                e -> Integer.parseInt(e.getValue()),
                (a, b) -> { throw new AssertionError(); },
                HashMap::new
         ));
output.values().removeIf(v -> v % 2 != 0);

これは、結果として可変のMapが必要であることを前提としています。そうでない場合は、outputから不変のMapを作成できます。


値を同じ型に変換していて、replaceAllをその場で変更したい場合、これは input でより短くなる可能性があります。

input.replaceAll((k, v) -> v + " example");
input.values().removeIf(v -> v.length() > 10);

また、これはMapが可変であると想定しています。


すべての有効なHashMap実装で動作するわけではなく、将来replaceAllで動作しなくなる可能性があるため、これを行うことはお勧めしませんが、現在HashMapとキャストを使用できますMapを使用して、値のタイプを変更します。

((Map)input).replaceAll((k, v) -> Integer.parseInt((String)v));
Map<String, Integer> output = (Map)input;
output.values().removeIf(v -> v % 2 != 0);

これにより、タイプの安全性に関する警告が表示され、次のような古いタイプの参照を介してClassCastExceptionから値を取得しようとすると、

String ex = input.get("a");

oldをスローします。


最初の変換部分をメソッドに移動して、ボイラープレートをたくさん使用する場合は、ボイラープレートを回避できます。

public static <K, VO, VN, M extends Map<K, VN>> M transformValues(
        Map<? extends K, ? extends VO> old, 
        Function<? super VO, ? extends VN> f, 
        Supplier<? extends M> mapFactory){
    return old.entrySet().stream().collect(Collectors.toMap(
            Entry::getKey, 
            e -> f.apply(e.getValue()), 
            (a, b) -> { throw new IllegalStateException("Duplicate keys for values " + a + " " + b); },
            mapFactory));
}

そして、次のように使用します。

    Map<String, Integer> output = transformValues(input, Integer::parseInt, HashMap::new);
    output.values().removeIf(v -> v % 2 != 0);

たとえば、MapIdentityHashMapmapFactoryであり、HashMapが_ [$ var]を作成する場合、重複キー例外がスローされることに注意してください。 _。

3
Alex

AbacusUtil によるコードです

Map<String, String> input = N.asMap("a", "1234", "b", "2345", "c", "3456", "d", "4567");

Map<String, Integer> output = Stream.of(input)
                          .groupBy(e -> e.getKey(), e -> N.asInt(e.getValue()))
                          .filter(e -> e.getValue() % 2 == 0)
                          .toMap(Map.Entry::getKey, Map.Entry::getValue);

N.println(output.toString());

宣言:私はAbacusUtilの開発者です。

0
user_3380739