Reputation: 1907
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
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
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
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
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