Utilisation récursive du flux.flatMap()
Considérons la classe suivante:
public class Order {
private String id;
private List<Order> orders = new ArrayList<>();
@Override
public String toString() {
return this.id;
}
// getters & setters
}
NOTE: Il est important de noter que je ne peux pas modifier cette classe, Car je la consomme à partir d'une API externe.
Considérons également la hiérarchie des ordres suivante:
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));
Qui pourrait être représenté visuellement de cette façon:
1
1.1
1.1.1
1.2
2
2.1
2.2
2.3
Maintenant, je veux aplatir cette hiérarchie des ordres dans un List
, de sorte que j'obtient:
[1, 1.1, 1.1.1, 1.2, 2, 2.1, 2.2, 2.3]
J'ai réussi à le faire en utilisant récursivement flatMap()
(avec une classe d'aide), comme suit:
List<Order> flattened = orders.stream()
.flatMap(Helper::flatten)
.collect(Collectors.toList());
C'est la classe d'aide:
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 ligne suivante:
System.out.println(flattened);
Produit la sortie suivante:
[1, 1.1, 1.1.1, 1.2, 2, 2.1, 2.2, 2.3]
Jusqu'à présent tout va bien. Le résultat est absolument correcte.
Cependant, après avoir lu cette question, j'ai eu quelques préoccupations concernant l'utilisation de flatMap()
dans une méthode récursive. En particulier, je voulais savoir comment le flux était en cours d'expansion (si c'est le terme). J'Ai Donc modifié la classe Helper
et utilisé peek(System.out::println)
pour vérifier ceci:
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);
}
}
Et la sortie était:
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
Je ne suis pas sûr si c'est la sortie qui devrait être imprimée.
Donc, je me demande s'il est correct de laisser les flux intermédiaires contenir des éléments répétés. En outre, quels sont les avantages et les inconvénients de cette approche? Est-il correct, après tout, d'utiliser flatMap()
de cette façon? Est-il une meilleure façon d'obtenir les mêmes?
2 réponses
Eh bien, j'ai utilisé le même modèle avec une classe Tree
générique et je n'ai pas eu un mauvais sentiment avec elle. La seule différence est que la classe Tree
elle-même offrait des méthodes children()
et allDescendants()
, les deux renvoyant un Stream
et ce dernier construisant sur le premier. Ceci est lié à "devrais-je retourner une Collection ou un flux?" et "nommer les méthodes java qui renvoient des flux" .
Du point de vue d'un Stream
, il n'y a pas de différence entre un flatMap
pour les enfants d'un autre type (c'est-à-dire lors de la traversée d'une propriété) et a flatMap
aux enfants du même type. Il n'y a pas non plus de problème si le flux retourné contient à nouveau le même élément, car il n'y a pas de relation entre les éléments des flux. En principe, vous pouvez utiliser flatMap
comme filter
opération, en utilisant le modèle flatMap(x -> condition? Stream.of(x): Stream.empty())
. Il est également possible de l'utiliser pour dupliquer des éléments comme dans cette réponse.
Il n'y a vraiment aucun problème avec l'utilisation de flatMap
de cette façon. Chacune des étapes intermédiaires d'un flux est assez indépendante (par conception), donc il n'y a aucun risque dans votre récursivité. La principale chose que vous devez surveiller est tout ce qui pourrait modifier la liste sous-jacente pendant que vous la diffusez. Dans votre cas, cela ne semble pas être un risque.
Idéalement, vous feriez de cette récursivité une partie de la classe Order
elle-même:
class Order {
private final List<Order> subOrders = new ArrayList<>();
public Stream<Order> streamOrders() {
return Stream.concat(
Stream.of(this),
subOrders.stream().flatMap(Order::streamOrders));
}
}
, Alors vous pouvez utiliser orders.stream().flatMap(Order::streamOrders)
, qui semble un peu plus naturel pour moi que d'utiliser une classe d'assistance.
En tant que question d'intérêt, j'ai tendance à utiliser ces types de méthodes stream
pour permettre l'utilisation de champs de collection plutôt qu'un getter pour le champ. Si l'utilisateur de la méthode n'a pas besoin de savoir quoi que ce soit sur la collection sous-jacente ou doit être capable de la changer, le retour d'un flux est pratique et sûr.
Je noterai qu'il y a un risque dans votre structure de données dont vous devriez être conscient: une commande pourrait faire partie de plusieurs d'autres commandes et peut même faire partie de lui-même. Cela signifie qu'il est assez trivial de provoquer une récursivité infinie et un débordement de pile:
Order o1 = new Order();
o1.setOrders(Arrays.asList(o1));
o1.streamOrders();
Il y a beaucoup de bons modèles disponibles pour éviter ce genre de problèmes, alors demandez si vous voulez de l'aide dans ce domaine.
Vous faites remarquer que vous ne pouvez pas modifier la classe Order
. Dans ce cas, je vous suggère de l'étendre pour créer votre propre version plus sûre:
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);
}
}
C'est une distribution assez sûre car vous attendez des utilisateurs à utiliser addOrder
. Pas infaillible car ils pourraient toujours appeler getOrders
et Ajouter un Order
plutôt qu'un SafeOrder
. Encore une fois, il existe des modèles pour éviter que si vous êtes intéressé.