2015-09-18 13 views
23

Si consideri il seguente classe:ricorsiva uso di Stream.flatMap()

public class Order { 

    private String id; 

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

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

    // getters & setters 
} 

NOTA: E 'importante notare che io non riesco a modificare questa classe, perché sto consumando da una esterna API.

Considera anche la seguente gerarchia di ordini:

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

Che potrebbe essere rappresentato visivamente in questo modo:

1 
1.1 
1.1.1 
1.2 
2 
2.1 
2.2 
2.3 

Ora, voglio appiattire questa gerarchia di ordini in un List, in modo che Prendo il seguente:

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

Sono riuscito a farlo ricorsivamente usando flatMap() (insieme ad una classe di supporto), come segue:

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

Questa è la classe helper:

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 
    } 
} 

La seguente riga:

System.out.println(flattened); 

produce la seguente output:

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

Fin qui tutto bene. Il risultato è assolutamente corretto.

Tuttavia, after reading this question, ho avuto alcune preoccupazioni riguardanti l'utilizzo di flatMap() all'interno di un metodo ricorsivo. In particolare, volevo sapere come si espandeva il flusso (se è il termine). Così ho modificato la classe Helper e utilizzato peek(System.out::println) di controllare questo:

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

e la produzione ammontava:

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 

Non sono sicuro se questo è l'uscita che deve essere stampato.

Quindi, mi chiedo se è OK lasciare che i flussi intermedi contengano elementi ripetuti. Inoltre, quali sono i pro e i contro di questo approccio? Dopotutto, è corretto utilizzare flatMap() in questo modo? C'è un modo migliore per ottenere lo stesso?

+0

Solo curioso: perché si imposta l'id dopo aver creato l'ordine anziché renderlo un argomento al costruttore? – sprinter

+0

@sprinter Non posso modificare la classe 'Order' dato che è parte di un'API che sto consumando. –

+1

Ah, capisco. Quindi la maggior parte della mia risposta è abbastanza inutile. Potrebbe valere la pena aggiungere quell'informazione alla tua domanda. – sprinter

risposta

15

Bene, ho utilizzato lo stesso modello con una classe generica Tree e non ho avuto una sensazione errata. L'unica differenza è che la classe Tree ha offerto i metodi children() e allDescendants(), restituendo entrambi uno Stream e quest'ultimo edificio sul primo. Questo è relativo a “Should I return a Collection or a Stream?” e “Naming java methods that return streams”.

Da un Stream s prospettiva, non c'è differenza tra un flatMap per bambini di tipo diverso (cioè quando attraversa una proprietà) e un flatMap ai bambini dello stesso tipo. Inoltre, non c'è alcun problema se il flusso restituito contiene nuovamente lo stesso elemento, in quanto non esiste alcuna relazione tra gli elementi degli stream. In linea di principio, è possibile utilizzare flatMap come operazione filter, utilizzando il modello flatMap(x -> condition? Stream.of(x): Stream.empty()). È anche possibile utilizzarlo per duplicare elementi come in this answer.

+0

Le mie preoccupazioni provengono da qui: [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-is-not-completamente-lazy-in-java-streams) Mi ci è voluto un po 'di tempo per trovare la domanda –

+2

Questa è una debolezza nell'implementazione (molte persone la considerano un bug), ma non invalida la tua soluzione. Il tuo approccio non è sbagliato. E i problemi relativi alle prestazioni relativi alle operazioni di cortocircuito dovrebbero essere risolti dai manutentori JRE. – Holger

+0

Grazie, è tutto ciò che volevo sapere. –

10

In questo modo non c'è alcun problema con l'utilizzo di flatMap. Ciascuno dei passaggi intermedi in un flusso è abbastanza indipendente (in base alla progettazione) quindi non c'è alcun rischio nella ricorsione. La cosa principale a cui devi prestare attenzione è tutto ciò che potrebbe alterare l'elenco sottostante mentre lo stai trasmettendo. Nel tuo caso non sembra essere un rischio.

Idealmente si potrebbe rendere questa parte ricorsione della classe Order stessa:

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

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

quindi è possibile utilizzare orders.stream().flatMap(Order::streamOrders) che sembra un po 'più naturale per me rispetto all'utilizzo di una classe di supporto.

Per motivi di interesse, tendo ad utilizzare questi tipi di metodi stream per consentire l'utilizzo dei campi di raccolta anziché di un getter per il campo. Se l'utente del metodo non ha bisogno di sapere nulla sulla collezione sottostante o ha bisogno di essere in grado di cambiarlo, restituire un flusso è comodo e sicuro.

Noterò che c'è un rischio nella struttura dati di cui dovresti essere a conoscenza: un ordine potrebbe essere parte di molti altri ordini e può anche essere parte di se stesso. Ciò significa che è abbastanza banale per causare una ricorsione infinita e un overflow dello stack:

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

Ci sono un sacco di buoni modelli disponibili per evitare questi tipi di problemi vi preghiamo di chiedere se hai bisogno di aiuto in quella zona.

Si ricorda che non è possibile modificare la classe Order. In questo caso vi consiglio di estendo a creare la propria versione più sicura:

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

Si tratta di un cast abbastanza sicuro perché ci si aspetta agli utenti di utilizzare addOrder. Non infallibile in quanto potrebbero ancora chiamare getOrders e aggiungere uno Order anziché uno SafeOrder. Ancora una volta ci sono schemi per impedirlo, se sei interessato.

+0

Le mie preoccupazioni provengono da qui: [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-is-not-completamente-lazy-in-java-streams) Mi ci è voluto del tempo per trovare la domanda –

+3

@FedericoPeraltaSchaffner che il problema non influisce sulla correttezza di questa soluzione affatto. Riguarda le condizioni in cui un flusso può essere cortocircuitato - l'OP in quel caso sta mostrando che in alcune condizioni l'operazione di streaming dovrebbe terminare ma non lo fa.La risposta è ancora corretta, è solo che a volte il JRE fa più elaborazione del necessario. – sprinter

+0

Grazie. Volevo verificare la correttezza del mio approccio rispetto alla domanda collegata, ma la tua risposta su come progettare correttamente su questo scenario è anche molto utile. –