Robert Lewis
Robert Lewis

Reputation: 1907

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?

Upvotes: 25

Views: 66019

Answers (4)

Tagir Valeev
Tagir Valeev

Reputation: 100209

Actually the problem is that you have a Stream<Card> type, even though after filtering you are pretty sure that the stream contains nothing but SuitCard objects. You know that, but compiler does not. If you don't want to add an executable code into your stream, you can do instead an unchecked cast to Stream<SuitCard>:

Map<Suit, Long> countBySuit = ((Stream<SuitCard>)contents.stream()
    .filter( card -> card instanceof SuitCard ))
    .collect( Collectors.groupingBy( SuitCard::getSuit, Collectors.counting() ) ); 

This way casting will not add any instructions to compiled bytecode. Unfortunately this looks quite ugly and produces a compiler warning. In my StreamEx library I hid this ugliness inside library method select(), so using StreamEx you can write

Map<Suit, Long> countBySuit = StreamEx.of(contents)
    .select( SuitCard.class )
    .collect( Collectors.groupingBy( SuitCard::getSuit, Collectors.counting() ) ); 

Or even shorter:

Map<Suit, Long> countBySuit = StreamEx.of(contents)
    .select( SuitCard.class )
    .groupingBy( SuitCard::getSuit, Collectors.counting() ); 

If you don't like using third-party libraries, your solution involving additional map step looks ok. Even though it adds some overhead, usually it's not very significant.

Upvotes: 4

JB Nizet
JB Nizet

Reputation: 691755

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 SuitCard 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.

Upvotes: 16

Joe C
Joe C

Reputation: 15684

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.

Upvotes: 63

Andrew Rueckert
Andrew Rueckert

Reputation: 5215

The type of contents is Card, so contents.stream() returns Stream<Card>. Filter does guarantee that each item in the resulting stream is a SuitCard, however, filter does not change the type of the stream. card -> (SuitCard)card is functionally equivalent to card -> card, but it's type is Function<Card,Suitcard>, so the .map() call returns a Stream<SuitCard>.

Upvotes: 2

Related Questions