Pour acquérir de l'expérience avec les nouveaux flux Java, j'ai développé un framework pour gérer les cartes à jouer. Voici la première version de mon code pour créer une Map
contenant le nombre de cartes de chaque couleur dans une main (Suit
est une enum
):
Map<Suit, Long> countBySuit = contents.stream() // contents is ArrayList<Card>
.collect( Collectors.groupingBy( Card::getSuit, Collectors.counting() ));
Cela a très bien fonctionné et j'étais heureux. Ensuite, j'ai refactoré en créant des sous-classes de cartes distinctes pour les "Suit Cards" et les Jokers. Ainsi, la méthode getSuit()
a été déplacée de la classe Card
vers sa sous-classe SuitCard
, car les jokers n'ont pas de costume. Nouveau code:
Map<Suit, Long> countBySuit = contents.stream() // contents is ArrayList<Card>
.filter( card -> card instanceof SuitCard ) // reject Jokers
.collect( Collectors.groupingBy( SuitCard::getSuit, Collectors.counting() ) );
Notez l'insertion astucieuse d'un filtre pour vous assurer que la carte considérée est bien une carte de costume et non un joker. Mais ça ne marche pas! Apparemment, la ligne collect
ne réalise pas que l'objet en cours de transmission est GARANTI être un SuitCard
.
Après avoir longuement réfléchi à cette question pendant un bon bout de temps, j'ai désespérément essayé d'insérer un appel de fonction map
et, étonnamment, cela a fonctionné!
Map<Suit, Long> countBySuit = contents.stream() // contents is ArrayList<Card>
.filter( card -> card instanceof SuitCard ) // reject Jokers
.map( card -> (SuitCard)card ) // worked to get rid of error message on next line
.collect( Collectors.groupingBy( SuitCard::getSuit, Collectors.counting() ) );
Je ne savais pas que la conversion d'un type était considérée comme une instruction exécutable. Pourquoi ça marche? Et pourquoi le compilateur le rend-il nécessaire?
N'oubliez pas qu'une opération filter
ne modifiera pas le type de compilation des éléments de Stream
. Oui, logiquement, nous voyons que tout ce qui permet de dépasser ce point sera une SuitCard
, tout ce que la filter
voit est une Predicate
. Si ce prédicat change plus tard, cela pourrait entraîner d'autres problèmes lors de la compilation.
Si vous voulez le changer en Stream<SuitCard>
, vous devez ajouter un mappeur qui effectue un casting pour vous:
Map<Suit, Long> countBySuit = contents.stream() // Stream<Card>
.filter( card -> card instanceof SuitCard ) // still Stream<Card>, as filter does not change the type
.map( SuitCard.class::cast ) // now a Stream<SuitCard>
.collect( Collectors.groupingBy( SuitCard::getSuit, Collectors.counting() ) );
Je vous renvoie au Javadoc pour tous les détails.
map()
permet de transformer un Stream<Foo>
en Stream<Bar>
en utilisant une fonction qui prend un Foo
en argument et renvoie un Bar
. Et
card -> (SuitCard) card
est une telle fonction: il prend une carte comme argument et retourne une SuitCard.
Vous pourriez l'écrire de cette façon si vous le vouliez, peut-être que cela vous le rendrait plus clair:
new Function<Card, SuitCard>() {
@Override
public SuitCard apply(Card card) {
SuitCard suitCard = (SuitCard) card;
return suitCard;
}
}
Le compilateur rend cela nécessaire car filter () transforme un Stream<Card>
en un Stream<Card>
. Vous ne pouvez donc pas appliquer une fonction qui accepte uniquement SuitCard aux éléments de ce flux, qui pourrait contenir n’importe quel type de Card: le compilateur ne se soucie pas de ce que fait votre filtre. Il se soucie seulement de quel type il retourne.
Le type de contenu est Card
, donc contents.stream()
renvoie Stream<Card>
. Le filtre garantit que chaque élément du flux résultant est une SuitCard
; toutefois, le filtre ne modifie pas le type du flux. card -> (SuitCard)card
est fonctionnellement équivalent à card -> card
, mais son type est Function<Card,Suitcard>
; l'appel .map()
renvoie donc un Stream<SuitCard>
.
En réalité, le problème est que vous avez un type Stream<Card>
, même si, après filtrage, vous êtes à peu près sûr que le flux ne contient que des objets SuitCard
. Vous le savez, mais le compilateur ne le sait pas. Si vous ne souhaitez pas ajouter de code exécutable dans votre flux, vous pouvez effectuer une conversion non contrôlée en Stream<SuitCard>
:
Map<Suit, Long> countBySuit = ((Stream<SuitCard>)contents.stream()
.filter( card -> card instanceof SuitCard ))
.collect( Collectors.groupingBy( SuitCard::getSuit, Collectors.counting() ) );
De cette manière, le casting n’ajoutera aucune instruction au bytecode compilé. Malheureusement, cela semble assez moche et produit un avertissement du compilateur. Dans mon StreamEx bibliothèque j'ai caché cette laideur à l'intérieur de la méthode de bibliothèque select()
, de sorte que vous pouvez utiliser StreamEx
Map<Suit, Long> countBySuit = StreamEx.of(contents)
.select( SuitCard.class )
.collect( Collectors.groupingBy( SuitCard::getSuit, Collectors.counting() ) );
Ou même plus court:
Map<Suit, Long> countBySuit = StreamEx.of(contents)
.select( SuitCard.class )
.groupingBy( SuitCard::getSuit, Collectors.counting() );
Si vous n'aimez pas utiliser les bibliothèques tierces, votre solution impliquant l'étape map
supplémentaire semble correcte. Même si cela ajoute des frais généraux, ce n'est généralement pas très important.