Types de casting dans les flux Java 8


Pour acquérir de l'expérience avec les nouveaux flux de Java, j'ai développé un framework pour gérer les cartes à jouer. Voici la première version de mon code pour la création d'un Map contenant le nombre de cartes de chaque couleur dans une main (Suit est un 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 refactorisé, en créant des sous-classes de cartes séparées pour les "Cartes de costume" et les Jokers. Ainsi, la méthode getSuit() a été déplacée de la classe Card à 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 intelligente d'un filtre pour vous assurer que la carte considérée est en fait une carte de costume et non un Joker. Mais il ne fonctionne pas! Apparemment, la ligne collect ne se rend pas compte que l'objet qu'elle passe est GARANTI d'être un SuitCard.

Après avoir dérouté cela pendant un bon moment, en désespoir de cause, j'ai 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 n'avais aucune idée que le casting d'un type était considéré comme un instruction exécutable. Pourquoi ce travail? Et pourquoi le compilateur rendre nécessaire?

Author: Robert Lewis, 2016-10-04

4 answers

Rappelez-vous qu'une opération filter ne changera pas le type de compilation des éléments Stream. Oui, logiquement, nous voyons que tout ce qui le fait passer au-delà de ce point sera un SuitCard, tout ce que le filter voit est un Predicate. Si ce prédicat change plus tard, cela pourrait entraîner d'autres problèmes de compilation.

Si vous voulez le changer pour un Stream<SuitCard>, vous devez ajouter un mappeur qui fait de voter 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.

 15
Author: Joe C, 2016-10-03 21:51:50

Eh Bien, map() permet de transformer un Stream<Foo> en Stream<Bar> à l'aide d'une fonction qui prend un Foo comme argument et retourne un Bar. Et

card -> (SuitCard) card

Est une telle fonction: elle prend une carte comme argument et renvoie une SuitCard.

Vous pourriez l'écrire de cette façon si vous le vouliez, peut-être que cela vous le rend 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 acceptant uniquement SuitCard aux éléments de ce flux, qui pourrait contenir n'importe quel type de carte: le compilateur ne se soucie pas de ce que fait votre filtre. Il ne se soucie que du type qu'il renvoie.

 13
Author: JB Nizet, 2016-10-04 04:58:36

Le type de contenu est Card, donc contents.stream() retourne Stream<Card>. Filter garantit que chaque élément du flux résultant est un SuitCard, cependant, filter ne change pas le type du flux. card -> (SuitCard)card est fonctionnellement équivalent à card -> card, mais son type est Function<Card,Suitcard>, donc l'appel .map() renvoie un Stream<SuitCard>.

 1
Author: Andrew Rueckert, 2016-10-03 21:39:32

En fait, le problème est que vous avez un type Stream<Card>, même si après le 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 fait pas. Si vous ne souhaitez pas ajouter de code exécutable dans votre flux, vous pouvez effectuer à la place une conversion non coché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 façon, le casting n'ajoutera aucune instruction au bytecode compilé. Malheureusement, cela semble assez laid et produit un avertissement du compilateur. Dans ma bibliothèque StreamEx Je caché cette laideur à l'intérieur de la méthode de la bibliothèque select(), donc, en utilisant StreamEx, vous pouvez écrire

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 des bibliothèques tierces, votre solution impliquant une étape supplémentaire map semble ok. Même si cela ajoute des frais généraux, ce n'est généralement pas très significatif.

 0
Author: Tagir Valeev, 2016-10-04 06:02:18