2015-09-18 33 views
23

は、次のクラスを考えてみましょう:再帰的使用

public class Order { 

    private String id; 

    private List<Order> orders = new ArrayList<>(); 

    @Override 
    public String toString() { 
     return this.id; 
    } 

    // getters & setters 
} 

注:私がこのクラスを変更できないことを、私は、外部からそれを消費していますので、注意することが重要であるAPI。

はまた、注文の次の階層を考慮してください。

視覚的にこのように表すことができ
Order o1 = new Order(); 
o1.setId("1"); 
Order o11 = new Order(); 
o11.setId("1.1"); 
Order o111 = new Order(); 
o111.setId("1.1.1"); 
List<Order> o11Children = new ArrayList<>(Arrays.asList(o111)); 
o11.setOrders(o11Children); 

Order o12 = new Order(); 
o12.setId("1.2"); 
List<Order> o1Children = new ArrayList<>(Arrays.asList(o11, o12)); 
o1.setOrders(o1Children); 

Order o2 = new Order(); 
o2.setId("2"); 
Order o21 = new Order(); 
o21.setId("2.1"); 
Order o22 = new Order(); 
o22.setId("2.2"); 
Order o23 = new Order(); 
o23.setId("2.3"); 
List<Order> o2Children = new ArrayList<>(Arrays.asList(o21, o22, o23)); 
o2.setOrders(o2Children); 

List<Order> orders = new ArrayList<>(Arrays.asList(o1, o2)); 

:今

1 
1.1 
1.1.1 
1.2 
2 
2.1 
2.2 
2.3 

が、私はそのようにListに注文のこの階層をフラット化したいが私は以下を取得します:

[1, 1.1, 1.1.1, 1.2, 2, 2.1, 2.2, 2.3] 

これは、ヘルパークラスである

List<Order> flattened = orders.stream() 
    .flatMap(Helper::flatten) 
    .collect(Collectors.toList()); 

:次のように、再帰的に(ヘルパークラスと一緒に)flatMap()を使用することによってそれを行う

public final class Helper { 

    private Helper() { 
    } 

    public static Stream<Order> flatten(Order order) { 
     return Stream.concat(
      Stream.of(order), 
      order.getOrders().stream().flatMap(Helper::flatten)); // recursion here 
    } 
} 

次の行:

System.out.println(flattened); 

が生成します次の出力:

[1, 1.1, 1.1.1, 1.2, 2, 2.1, 2.2, 2.3] 

これまでのところとても良いです。結果は絶対に正しいです。

しかし、after reading this question、再帰的な方法でflatMap()の使用に関するいくつかの懸念がありました。特に、ストリームがどのように拡張されているのかを知りたかったのです(それが用語の場合)。だから私はHelperクラスを変更し、これを確認するためにpeek(System.out::println)を使用:

public static final class Helper { 

    private Helper() { 
    } 

    public static Stream<Order> flatten(Order order) { 
     return Stream.concat(
      Stream.of(order), 
      order.getOrders().stream().flatMap(Helper::flatten)) 
     .peek(System.out::println); 
    } 
} 

そして、出力されました:

1 
1.1 
1.1 
1.1.1 
1.1.1 
1.1.1 
1.2 
1.2 
2 
2.1 
2.1 
2.2 
2.2 
2.3 
2.3 

私はこれを印刷しなければならない出力であるかはわかりません。

中間ストリームに繰り返し要素が含まれるようにしても構いません。さらに、このアプローチの賛否両論は何ですか?結局のところ、この方法でflatMap()を使用するのは正しいですか?同じことを達成するより良い方法はありますか?

+0

ちょっと不思議なことに、なぜそれをコンストラクタの引数にするのではなく、オーダーを作成した後にidを設定するのですか? – sprinter

+0

@sprinter 'Order'クラスを変更することはできません。これは、消費しているAPIの一部であるためです。 –

+1

ああ、私は参照してください。それから、私の答えのほとんどはかなり役に立たない。あなたの質問にその情報を追加する価値があるかもしれません。 – sprinter

答えて

15

まあ、一般的なTreeクラスで同じパターンを使いましたが、間違った感じはありませんでした。唯一の違いは、Treeクラス自体がchildren()allDescendants()メソッドを提供しており、いずれもStreamと後者のビルディングを返すことです。これは“Should I return a Collection or a Stream?”“Naming java methods that return streams”に関連しています。

Streamの観点からは、異なるタイプの子(つまり、プロパティをトラバースするとき)と同じタイプの子にflatMapの違いはありません。ストリームの要素間に関係がないため、返されたストリームに同じ要素が再び含まれていても問題はありません。原則として、flatMap(x -> condition? Stream.of(x): Stream.empty())を使用して、filter操作としてflatMapを使用できます。 this answerのような要素を複製するためにも使用できます。

+0

私の懸念はここから来ます:[http://stackoverflow.com/questions/29229373/why-filter-after-flatmap-is-not-completely-lazy-in-java-streams](http://stackoverflow.com/questions/29229373/why-filter-after-flatmap-are-not-lazy-in-java-streams)質問が見つかるまでに時間がかかりました –

+2

これは実装の弱点ですバグ)、それはあなたの解決策を無効にしません。あなたのアプローチは間違っていません。短絡操作に関する関連するパフォーマンスの問題は、JREの保守担当者が修正する必要があります。 – Holger

+0

ありがとうございました、私が知りたいのはそれだけです。 –

10

このようにflatMapを使用しても問題はありません。ストリームの各中間ステップは(設計によって)かなり独立しているため、再帰のリスクはありません。あなたが気を付けなければならない主なことは、あなたがストリーミングしている間、基礎となるリストを変更する可能性のあるものです。あなたの場合、それは危険ではないようです。

理想的には、Orderクラス自体のこの再帰部分になるだろう:

class Order { 
    private final List<Order> subOrders = new ArrayList<>(); 

    public Stream<Order> streamOrders() { 
     return Stream.concat(
      Stream.of(this), 
      subOrders.stream().flatMap(Order::streamOrders)); 
    } 
} 

次にあなたがもう少し自然な私にはヘルパークラスを使用するよりも思われるorders.stream().flatMap(Order::streamOrders)を使用することができます。

私は、フィールドのゲッターではなくコレクションフィールドの使用を許可するために、これらのタイプのメソッドを使用する傾向があります。メソッドのユーザーが基になるコレクションについて何も知る必要がない場合や、変更する必要がある場合は、ストリームを返すのが便利で安全です。

あなたのデータ構造には、次のことに注意する必要があることに注意してください。注文はいくつかの他の注文の一部であり、それ自体の一部であってもかまいません。これは、無限再帰とスタックオーバーフローを引き起こすことが非常に簡単だということを意味します

Order o1 = new Order(); 
o1.setOrders(Arrays.asList(o1)); 
o1.streamOrders(); 

あなたがその領域でいくつかの助けをしたいのであれば質問してください問題のこれらの種類を避けるために、利用可能な優れたパターンがたくさんあります。

Orderクラスを変更することはできません。その場合、私はあなたがあなた自身のより安全なバージョンを作成し、それを拡張勧め:あなたは、ユーザーがaddOrderを使用することを期待しているため

class SafeOrder extends Order { 
    public SafeOrder(String id) { 
     setId(id); 
    } 

    public void addOrder(SafeOrder subOrder) { 
     getOrders().add(subOrder); 
    } 

    public Stream<SafeOrder> streamOrders() { 
     return Stream.concat(Stream.of(this), subOrders().flatMap(SafeOrder::streamOrders)); 
    } 

    private Stream<SafeOrder> subOrders() { 
     return getOrders().stream().map(o -> (SafeOrder)o); 
    } 
} 

これはかなり安全なキャストです。彼らはまだgetOrdersと呼ぶことができ、SafeOrderではなくOrderを追加することができます。あなたが興味を持っているなら、それを防ぐためのパターンもあります。

+0

私の懸念はここから来ます:[http://stackoverflow.com/questions/29229373/why-filter-after-flatmap-is-not-completely-lazy-in-java-streams](http://stackoverflow.com/questions/29229373/why-filter-after-flatmap-are-not-lazy-in-java-streams)質問が見つかるにはしばらく時間がかかりました –

+3

@FedericoPeraltaSchaffnerこの問題はこのソリューションの正しさには影響しませんまったく。ストリームが短絡する可能性のある条件です。その場合のOPでは、ストリーム操作が終了する必要があることが示されています。答えはまだ正しいですが、JREが必要以上に処理を行うことがあることもあります。 – sprinter

+0

ありがとうございます。私は、リンクされた質問に関する私のアプローチの正しさをチェックしたいが、このシナリオで正しく設計する方法に関するあなたの答えはまた非常に便利です。 –