To gain some experience with Java's new streams, I've been developing a framework for handling playing cards. Here's the first version of my code for creating a Map
containing the number of cards of each suit in a hand (Suit
is an enum
):
Map<Suit, Long> countBySuit = contents.stream() // contents is ArrayList<Card>
.collect( Collectors.groupingBy( Card::getSuit, Collectors.counting() ));
This worked great and I was happy. Then I refactored, creating separate Card subclasses for "Suit Cards" and Jokers. So the getSuit()
method was moved from the Card
class to its subclass SuitCard
, since Jokers don't have a suit. New 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() ) );
Notice the clever insertion of a filter to make sure that the card being considered is in fact a Suit Card and not a Joker. But it doesn't work! Apparently the collect
line doesn't realize that the object it's being passed is GUARANTEED to be a SuitCard
.
After puzzling over this for a good while, in desperation I tried inserting a map
function call, and amazingly it worked!
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() ) );
I had no idea that casting a type was considered an executable statement. Why does this work? And why does the compiler make it necessary?
Remember that a filter
operation will not change the compile-time type of the Stream
's elements. Yes, logically we see that everything that makes it past this point will be a SuitCard
, all that the filter
sees is a Predicate
. If that predicate changes later, then that could lead to other compile-time issues.
If you want to change it to a Stream<SuitCard>
, you'd need to add a mapper that does a cast for you:
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() ) );
I refer you to the Javadoc for the full details.