2016-02-18 10 views
19

変換してフィルタリングしたいJavaマップがあります。簡単な例として、すべての値をIntegerに変換し、奇数エントリを削除したいとします。ストリームで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()を呼び出さないようにする方法があるかどうだろうことはできません。

変換処理とフィルタ処理の両方を実行できる方法はありますか?.collect()を最後に一度だけ呼び出しますか?

+0

私は可能ではないと思います。 javadocに基づいて "ストリームは中間ストリームまたは端末ストリームの操作を1回だけ操作する必要があります。たとえば、同じソースが2つ以上のパイプラインをフィードする" forked "ストリーム、または同じパイプラインの複数のトラバーサルストリーム実装は、ストリームが再利用されていることを検出すると、IllegalStateExceptionをスローする可能性があります。 – kosa

+0

@ Nambanそれは何の問題についてではありません。 – immibis

答えて

21

はい、キーと解析された整数値を保持する別の一時エントリに各エントリをマップできます。次に、その値に基づいて各エントリをフィルタリングできます。私たちが実際に箱入りintをしたいので、私はInteger.valueOf代わりのparseIntを使用

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

注意。


あなたがStreamExライブラリを使用する贅沢を持っている場合、あなたは非常に単純にそれを行うことができます。

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

これは可能なハッキング方法の1つですが、コードの数行でパフォーマンスと読みやすさを犠牲にしているような気がしますね。 – kosa

+3

@ Nambariなぜそれが「ハッキー」なのか分かりません。それは単なるマップフィルタです。 'AbstractMap.SimpleEntry'を明示的に使用している場合は、別の'ペア 'を作成することができますが、既にマップを扱っているので、ここで適切だと感じます。 – Tunaki

+1

私が追跡している「一時的なエントリ」のためだけに「ハッキー」を使用しましたが、正しい用語ではない可能性があります。 StreamExソリューションが好きだった。 – kosa

3

あなたがエントリを変換し、条件付きで、それらを蓄積する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メソッドを使用しています。オプションのv alueはまだ存在しています、私は収集されているマップに追加しています。これを行うには

+1

[私の最初の変種](http://stackoverflow.com/a/35490546/2711488)に非常に近いですが、私はここで勝つのは「オプション」の使用ではないと思います... – Holger

+0

@Holgerそれはすべてを保持するだけです一行でとにかく、私はあなたの答えを見ていませんでした。勝利(私たちが非常に微妙になった場合)は、 'Optional.ofNullable()'がヌルキーを許すかもしれません。 –

+1

私たちは同時に書いていました。 'Optional'チェインは実際には1行(または非常に大きなもの)ではなく、1つの式です。しかし、特定の式のサイズから始めて、文lambdaに必要な2つの中括弧は、それ以上のコストがかかりません。 – Holger

3

もう一つの方法は、あなたが変換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をしたいと仮定し1つはoutputです。


あなたは同じ型に値を変換し、これがreplaceAllとたくさん短くすることができます場所にMapを変更したい場合:

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

これもinputが可変である前提としています。


それはすべての有効なMapの実装のために動作しませんし、将来的にHashMapのために働いて停止することがありますので、私はこれを行うことはお勧めしませんが、あなたは、現在のタイプを変更するHashMapreplaceAllを使用してキャストすることができます値は:

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

これはまた、あなたがこのような古いタイプの参照を通じてMapから値を取得しようとする場合は、安全上の警告を入力して行います

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

ClassCastExceptionがスローされます。

あなたが最初にあなたがたくさんそれを使用することが予想される場合は定型を避けるための方法に一部を変換移動することができ

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

注意たとえば、oldMapIdentityHashMapであり、mapFactoryがである場合、重複キー例外がスローされることがあります。

+1

あなたの ''重複するキー "+ a +" "+ b'メッセージは誤解を招きます:' a'と 'b'実際にはキーではなく値です。 –

+0

@TagirValeevはい、私は気づきましたが、それは 'toMap'の2つの引数バージョンの' Collectors'と同じ方法です。コードボックスにスクロールバーを置くと思っていたものに変更して、それ。あなたが誤解を招くと思うので、私は今それを変更します。 – Alex

+1

はい、これは既に[Java-9](https://bugs.openjdk.java.net/browse/JDK-8040892)で修正されていますが(Java-8にバックポートされていない)既知の問題です。 Java-9は、例外メッセージに両方の値と同様に衝突するキーを表示します。 –

11

オーバーヘッドを大幅に削減して問題を解決する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インスタンスの作成を必要とせず、より良い値が実際Mapに追加された場合、フィルタによって拒否された値であることを意味する点にint値のボクシングを延期します箱入りではありません。私たちはここにキーの衝突を処理する必要はありませんので、あらかじめ知っているように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); }); 

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

+0

私はプレーンループのあなたの推薦のためにupvoted。あなたがストリームを使うことができるからといって、あなたがするべきではありません。 –

+2

@Jeffrey Bosboom:ええ、良い古いforループはまだ生きています。マップの場合、 '(k、v) - >は' Map.Entry '変数を宣言するよりもはるかに良いので、小さなループに対して' Map.forEach'メソッドを使用するのが好きです。おそらく実際のキーと値の別の2つの変数... – Holger

1

Guavaさんあなたの友人:

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

outputが変換されていることに注意してください、inputビューを濾過しました。個別に操作したい場合はコピーを作成する必要があります。ここで

0

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の開発者です。

関連する問題