2015-09-21 5 views
13

のリストを減らします。私はこれをJava 8ストリームのみを使用して一意のオブジェクトのリストに減らしたいと思っています(これは古いスキルの手段でこれを行う方法ですが、これは実験です)。グループと私がマージされる必要があり、多くの重複や、いくつかのフィールドを持つオブジェクトのリストを持っているオブジェクト

これは私が今持っているものです。マップ建物は無関係のようで、コレクションはバッキング・マップの図であり、()の値、およびあなたはより具体的なコレクションを取得するために新しいArrayList<>(...)でそれをラップする必要があるので、私は本当にこのことを好きではありません。おそらくより一般的な削減操作を使用するより良いアプローチはありますか?

@Test 
public void reduce() { 
    Collection<Foo> foos = Stream.of("foo", "bar", "baz") 
        .flatMap(this::getfoos) 
        .collect(Collectors.toMap(f -> f.name, f -> f, (l, r) -> { 
         l.ids.addAll(r.ids); 
         return l; 
        })).values(); 

    assertEquals(3, foos.size()); 
    foos.forEach(f -> assertEquals(10, f.ids.size())); 
} 

private Stream<Foo> getfoos(String n) { 
    return IntStream.range(0,10).mapToObj(i -> new Foo(n, i)); 
} 

public static class Foo { 
    private String name; 
    private List<Integer> ids = new ArrayList<>(); 

    public Foo(String n, int i) { 
     name = n; 
     ids.add(i); 
    } 
} 
+2

中間マップを使用せずにこの "古いスキール"(従来はラムダ/ストリームなし)を実装することは可能ですか?私は重複が潜在的に入力のどこでも発生する可能性があるので、すべての入力が処理されるまで、それらはすべてどこかでバッファリングされなければならないと思います。 –

答えて

6

あなたはグループ化を解除し、手順を減らし、あなたはクリーンな何かを得ることができる場合:すでに

public Foo(String n, List<Integer> ids) { 
    this.name = n; 
    this.ids.addAll(ids); 
} 

public static Foo merge(Foo src, Foo dest) { 
    List<Integer> merged = new ArrayList<>(); 
    merged.addAll(src.ids); 
    merged.addAll(dest.ids); 
    return new Foo(src.name, merged); 
} 
+1

それはほぼ同じことになります。途中でたくさんの新しい 'Foo'オブジェクトを作成するだけです。あなたのリストは' Foo'のリストではなく、 'Optional 'のリストです。 – RealSkeptic

+0

新しいFooを作成するのではなく、dest fooのIDをsrcに追加するだけでいいですか? – ryber

+3

@ryber確かに、実際の世界のシナリオでは、特にあなたの削減が並行して実行されている場合に、予期せぬ問題を容易に引き起こす可能性があります。ストリーミング操作の変更を減らすことをお勧めします。 https://docs.oracle.com/javase/8/docs/api/java/util/stream/package-summary.html#Reductionを参照してください。 –

2

:これはあなたのFooクラスで、いくつかの便利なメソッドを想定してい

Stream<Foo> input = Stream.of("foo", "bar", "baz").flatMap(this::getfoos); 

Map<String, Optional<Foo>> collect = input.collect(Collectors.groupingBy(f -> f.name, Collectors.reducing(Foo::merge))); 

Collection<Optional<Foo>> collected = collect.values(); 

をコメントで指摘されているように、マップは一意のオブジェクトを識別するときに使用するのが非常に自然なものです。一意のオブジェクトを見つける必要がある場合は、Stream::distinctメソッドを使用できます。この方法は、関係マップがあるという事実を隠しますが、どうやらそれはあなたがhashCodeメソッドを実装する必要がありますかdistinctが正しく動作しない場合があります示していthis questionによって示唆として、内部的にマップを使用して行います。

distinctメソッドの場合、マージが必要ない場合、すべての入力が処理される前にいくつかの結果を返すことができます。あなたのケースでは、質問に記載されていない入力についての追加の仮定がない限り、結果を返す前にすべての入力の処理を完了する必要があります。したがって、この答えは地図を使用します。

それはしかし、マップの値を処理し、ArrayListのに戻ってそれを回すためにストリームを使用して簡単に十分です。私はこの答えで、また他の答えの1つに現れるOptional<Foo>の出現を避ける方法を提供することを示します。

public void reduce() { 
    ArrayList<Foo> foos = Stream.of("foo", "bar", "baz").flatMap(this::getfoos) 
      .collect(Collectors.collectingAndThen(Collectors.groupingBy(f -> f.name, 
      Collectors.reducing(Foo.identity(), Foo::merge)), 
      map -> map.values().stream(). 
       collect(Collectors.toCollection(ArrayList::new)))); 

    assertEquals(3, foos.size()); 
    foos.forEach(f -> assertEquals(10, f.ids.size())); 
} 

private Stream<Foo> getfoos(String n) { 
    return IntStream.range(0, 10).mapToObj(i -> new Foo(n, i)); 
} 

public static class Foo { 
    private String name; 
    private List<Integer> ids = new ArrayList<>(); 

    private static final Foo BASE_FOO = new Foo("", 0); 

    public static Foo identity() { 
     return BASE_FOO; 
    } 

    // use only if side effects to the argument objects are okay 
    public static Foo merge(Foo fooOne, Foo fooTwo) { 
     if (fooOne == BASE_FOO) { 
      return fooTwo; 
     } else if (fooTwo == BASE_FOO) { 
      return fooOne; 
     } 
     fooOne.ids.addAll(fooTwo.ids); 
     return fooOne; 
    } 

    public Foo(String n, int i) { 
     name = n; 
     ids.add(i); 
    } 
} 
+1

なぜこのすべて 'map.values()。stream()。collect(blahblah)'?古き良き 'map - >新しいArrayList <>(map.values())'はより簡単で速くなります。 –

+0

@Tagir Valeev:結果に適用される操作が 'size()'と 'forEach()'のみである場合、 'map.values()'コレクションを新しいリストにまったくコピーする必要はありません。 – Holger

1

入力要素がランダムな順序で指定されている場合は、おそらく中間のマップを持つ方が最適な解決策です。あなたが事前にわかっている場合は、同じ名前を持つすべてのFOOSは(この条件は実際にあなたのテストで満たされている)隣接していることが、アルゴリズムが大幅に簡素化することができます:あなたはちょうど前のもので、現在の要素を比較してマージする必要があります名前が同じであれば

残念ながら、あなたは簡単かつ効果的にそのようなことを行うことが可能になる何のストリームAPIの方法はありません。

public static List<Foo> withCollector(Stream<Foo> stream) { 
    return stream.collect(Collector.<Foo, List<Foo>>of(ArrayList::new, 
      (list, t) -> { 
       Foo f; 
       if(list.isEmpty() || !(f = list.get(list.size()-1)).name.equals(t.name)) 
        list.add(t); 
       else 
        f.ids.addAll(t.ids); 
      }, 
      (l1, l2) -> { 
       if(l1.isEmpty()) 
        return l2; 
       if(l2.isEmpty()) 
        return l1; 
       if(l1.get(l1.size()-1).name.equals(l2.get(0).name)) { 
        l1.get(l1.size()-1).ids.addAll(l2.get(0).ids); 
        l1.addAll(l2.subList(1, l2.size())); 
       } else { 
        l1.addAll(l2); 
       } 
       return l1; 
      })); 
} 

私のテスト

は、このコレクタが、両方のシーケンシャルおよびパラレルモードでは(名前の重複の平均数に応じて最大2倍)をマッピングするために集めるよりも、常に高速であることを示しています。一つの可能​​な解決策は、このようなカスタムコレクタを作成することです。

別のアプローチは、collapse含む「部分還元」方法の束を提供し、私のStreamExライブラリを使用することです:隣接する二つの要素に適用されると、返す必要がありますBiPredicate

public static List<Foo> withStreamEx(Stream<Foo> stream) { 
    return StreamEx.of(stream) 
      .collapse((l, r) -> l.name.equals(r.name), (l, r) -> { 
       l.ids.addAll(r.ids); 
       return l; 
      }).toList(); 
} 

この方法は2つの引数を受け付けます要素をマージする必要がある場合はtrue、マージを実行する場合はBinaryOperatorこのソリューションは、シーケンシャルモードでカスタムコレクタよりも少し遅く(結果は非常に似ています)、toMapソリューションよりもかなり速く、collapseがより簡単で多少柔軟性があり、収集することができます他の方法で。

再び、これらの両方のソリューションは、同じ名前のfoosが隣接していることがわかっている場合にのみ機能します。ソートではパフォーマンスが大幅に低下し、toMapソリューションよりも処理速度が遅くなるため、foo名で入力ストリームをソートしてからこれらのソリューションを使用するのは悪い考えです。

1

すでに他の人に指摘されているように、中間のMapは避けられません。それがマージするオブジェクトを見つける方法です。さらに、削減中にソースデータを変更しないでください。

それでも、あなたは複数のFooのインスタンス作成することなく、両方を達成することができます。これは、Fooが本当にあるならば、それは持つべきであるとして、あなたが、あなたのFooクラスにコンストラクタ

public Foo(String n, List<Integer> l) { 
     name = n; 
     ids=l; 
    } 

を追加することを前提としてい

List<Foo> foos = Stream.of("foo", "bar", "baz") 
       .flatMap(n->IntStream.range(0,10).mapToObj(i -> new Foo(n, i))) 

       .collect(collectingAndThen(groupingBy(f -> f.name), 
        m->m.entrySet().stream().map(e->new Foo(e.getKey(), 
         e.getValue().stream().flatMap(f->f.ids.stream()).collect(toList()))) 
        .collect(toList()))); 

をIDのリストを保持することができるはずです。ちなみに、単一のアイテムとして機能するタイプと、マージされた結果のコンテナを持つことは、私にとっては不自然なようです。これはまさにコーディングが非常に複雑であることが判明した理由です。

ソース項目にidというものが1つあり、groupingBy(f -> f.name, mapping(f -> id, toList())のようなものを使用した後、(String, List<Integer>)のエントリをマージした項目にマッピングするだけで十分でした。

これは当てはまりません。Java 8にはflatMappingコレクタが存在しないため、フラットマッピングステップは2番目のステップに移り、より複雑に見えます。

どちらの場合でも、結果項目が実際に作成され、目的のリストタイプにマップを変換するのが自由であるため、2番目の手順は廃止されません。

+0

不変オブジェクトは確かに良いですが、現在のソリューションはOPコードより約2倍遅いことに注意してください。 'flatMapping'コレクタを使うと、おそらくもっと良いでしょう... –

+1

@Tagir Valeev:この場合、オブジェクトが不変であるかどうかではありません。ソースオブジェクトを変更しないでください。私は、あなたがソースオブジェクトがまだ使用されている場合、これがどのように裏目に出るのか想像することができます... – Holger

関連する問題