Robert Lewis Robert Lewis - 2 months ago 14
Java Question

Casting types in Java 8 streams

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?

Answer

Well, map() allows transforming a Stream<Foo> into a Stream<Bar> using a function that takes a Foo as argument and returns a Bar. And

card -> (SuitCard) card

is such a function: it takes a Card as argument and returns a SuitCard.

You could write it that way if you wanted to, maybe that makes it clearer to you:

new Function<Card, SuitCard>() {
    @Override
    public void apply(Card card) {
        SuitCard suitCard = (SuitCard) card;
        return suitCard;
    }
}

The compiler makes that necessary because filter() transforms a Stream<Card> into a Stream<Card>. So you can't apply a function only accepting SuitCard to the elements of that stream, which could contain any kind of Card: the compiler doesn't care about what your filter does. It only cares about what type it returns.