web-dev-qa-db-ja.com

Java 8 Streams:異なるプロパティに基づいて同じオブジェクトを複数回マップします

私の同僚から興味深い問題が提示されましたが、きちんとしたきれいなJava 8ソリューションを見つけることができませんでした。問題は、POJOのリストをストリーミングして収集することです複数のプロパティに基づくマップ-マッピングにより、POJOが複数回発生します

次のPOJOを想像してください。

private static class Customer {
    public String first;
    public String last;

    public Customer(String first, String last) {
        this.first = first;
        this.last = last;
    }

    public String toString() {
        return "Customer(" + first + " " + last + ")";
    }
}

List<Customer>として設定します:

// The list of customers
List<Customer> customers = Arrays.asList(
        new Customer("Johnny", "Puma"),
        new Customer("Super", "Mac"));

代替1:「ストリーム」の外側(またはMapの外側)forEachを使用します。

// Alt 1: not pretty since the resulting map is "outside" of
// the stream. If parallel streams are used it must be
// ConcurrentHashMap
Map<String, Customer> res1 = new HashMap<>();
customers.stream().forEach(c -> {
    res1.put(c.first, c);
    res1.put(c.last, c);
});

代替案2:マップエントリを作成してストリームし、flatMapします。 IMOは少し冗長すぎて読みにくいです。

// Alt 2: A bit verbose and "new AbstractMap.SimpleEntry" feels as
// a "hard" dependency to AbstractMap
Map<String, Customer> res2 =
        customers.stream()
                .map(p -> {
                    Map.Entry<String, Customer> firstEntry = new AbstractMap.SimpleEntry<>(p.first, p);
                    Map.Entry<String, Customer> lastEntry = new AbstractMap.SimpleEntry<>(p.last, p);
                    return Stream.of(firstEntry, lastEntry);
                })
                .flatMap(Function.identity())
                .collect(Collectors.toMap(
                        Map.Entry::getKey, Map.Entry::getValue));

代替3:これは、これまでに「最も美しい」コードを思いついた別の1つですが、reduceおよび3番目のパラメーターは、この質問で見られるように少し危険です: Java 8関数型プログラミング)の 'reduce'関数の3番目の引数の目的 さらに、reduceは変化しているため、以下のアプローチでは並列ストリームが機能しない可能性があるため、この問題には適していません。

// Alt 3: using reduce. Not so pretty
Map<String, Customer> res3 = customers.stream().reduce(
        new HashMap<>(),
        (m, p) -> {
            m.put(p.first, p);
            m.put(p.last, p);
            return m;
        }, (m1, m2) -> m2 /* <- NOT USED UNLESS PARALLEL */);

上記のコードが次のように出力される場合:

System.out.println(res1);
System.out.println(res2);
System.out.println(res3);

結果は次のようになります。

{Super = Customer(Super Mac)、Johnny = Customer(Johnny Puma)、Mac = Customer(Super Mac)、Puma = Customer(Johnny Puma)}
{Super = Customer(Super Mac)、Johnny = Customer(Johnny Puma)、Mac = Customer(Super Mac)、Puma = Customer(Johnny Puma)}
{Super = Customer(Super Mac)、Johnny = Customer(Johnny Puma)、Mac = Customer(Super Mac)、Puma = Customer(Johnny Puma)}

だから、今私の質問に:Java 8整然とした方法で、List<Customer>を介してストリームし、それを分割するMap<String, Customer>としてどうにか収集する必要があります全体が2つのキー(first AND last)として、つまりCustomerが2回マッピングされます。サードパーティのライブラリを使用したくない、使用したくないalt 1のように、ストリーム外のマップ。他の素敵な代替手段はありますか?

完全なコードは hastebinにあります で、単純なコピーアンドペーストで全体を実行できます。

25
wassgren

あなたの選択肢2と3はより明確になるように書き直すことができると思います:

代替2

_Map<String, Customer> res2 = customers.stream()
    .flatMap(
        c -> Stream.of(c.first, c.last)
        .map(k -> new AbstractMap.SimpleImmutableEntry<>(k, c))
    ).collect(toMap(Map.Entry::getKey, Map.Entry::getValue));
_

代替:HashMapを変更することにより、コードがreduceを乱用します。可変リダクションを行うには、collectを使用します。

_Map<String, Customer> res3 = customers.stream()
    .collect(
        HashMap::new, 
        (m,c) -> {m.put(c.first, c); m.put(c.last, c);}, 
        HashMap::putAll
    );
_

これらは同一ではないことに注意してください。代替2は、重複キーがある場合に例外をスローしますが、代替3はサイレントにエントリを上書きします。

重複キーの場合にエントリを上書きすることが必要な場合、個人的には代替案3を好むでしょう。これは、反復ソリューションに最もよく似ています。代替案2では、すべてのフラットマッピングを使用して、顧客ごとに多数の割り当てを行う必要があるため、パフォーマンスが向上すると予想されます。

ただし、代替案2は、エントリの生成を集約から分離することにより、代替案3よりも大きな利点があります。これにより、柔軟性が大幅に高まります。たとえば、例外2を変更して、例外をスローする代わりに重複キーのエントリを上書きする場合は、_toMap(...)に_(a,b) -> b_を追加するだけです。一致するエントリをリストに収集したい場合は、toMap(...)groupingBy(...)などに置き換えるだけで済みます。

20
Misha