Um Erfahrungen mit den neuen Java-Streams zu sammeln, habe ich ein Framework für den Umgang mit Spielkarten entwickelt. Hier ist die erste Version meines Codes zum Erstellen einer Map
, die die Anzahl der Karten jeder Farbe in einer Hand enthält (Suit
ist eine enum
):
Map<Suit, Long> countBySuit = contents.stream() // contents is ArrayList<Card>
.collect( Collectors.groupingBy( Card::getSuit, Collectors.counting() ));
Das hat super funktioniert und ich war glücklich. Dann habe ich überarbeitet und separate Karten-Unterklassen für "Suit Cards" und Joker erstellt. Daher wurde die getSuit()
-Methode von der Card
-Klasse in ihre Unterklasse SuitCard
verschoben, da Jokers keine Farbe haben. Neuer 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() ) );
Beachten Sie das clevere Einsetzen eines Filters, um sicherzustellen, dass die betrachtete Karte tatsächlich eine Suit Card und kein Joker ist. Aber es geht nicht! Anscheinend erkennt die collect
-Zeile nicht, dass das übergebene Objekt GARANTIERT ist, eine SuitCard
zu sein.
Nachdem ich eine Weile lang darüber nachgedacht hatte, versuchte ich verzweifelt, einen Funktionsaufruf map
einzufügen, und erstaunlicherweise funktionierte es!
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() ) );
Ich hatte keine Ahnung, dass das Casting eines Typs als ausführbare Anweisung betrachtet wurde. Warum funktioniert das? Und warum macht der Compiler es notwendig?
Denken Sie daran, dass eine filter
-Operation den Kompilierzeittyp der Stream
-Elemente nicht ändert. Ja, logischerweise sehen wir, dass alles, was über diesen Punkt hinausgeht, ein SuitCard
ist. Alles, was das filter
sieht, ist ein Predicate
. Wenn sich dieses Prädikat später ändert, kann dies zu anderen Problemen bei der Kompilierung führen.
Wenn Sie es in einen Stream<SuitCard>
ändern möchten, müssen Sie einen Mapper hinzufügen, der einen Cast für Sie ausführt:
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() ) );
Ich verweise Sie auf das Javadoc für die vollständigen Details.
Nun, map()
erlaubt die Umwandlung eines Stream<Foo>
in einen Stream<Bar>
mit einer Funktion, die eine Foo
als Argument nimmt und eine Bar
zurückgibt. Und
card -> (SuitCard) card
ist eine solche Funktion: Sie nimmt eine Karte als Argument und gibt eine SuitCard zurück.
Sie könnten es so schreiben, wenn Sie wollten, vielleicht macht es Ihnen dies klarer:
new Function<Card, SuitCard>() {
@Override
public SuitCard apply(Card card) {
SuitCard suitCard = (SuitCard) card;
return suitCard;
}
}
Der Compiler macht das notwendig, weil filter () einen Stream<Card>
in einen Stream<Card>
umwandelt. Sie können also keine Funktion anwenden, die nur SuitCard akzeptiert, und zwar auf die Elemente dieses Streams, die jede Art von Card enthalten könnten: Der Compiler kümmert sich nicht darum, was Ihr Filter tut. Es ist nur wichtig, welchen Typ es zurückgibt.
Der Inhaltstyp ist Card
. contents.stream()
gibt Stream<Card>
zurück. Filter garantiert, dass jedes Element im resultierenden Stream eine SuitCard
ist. Der Typ des Streams wird jedoch nicht durch den Filter geändert. card -> (SuitCard)card
ist funktional äquivalent zu card -> card
, aber der Typ ist Function<Card,Suitcard>
. Der Aufruf .map()
gibt also einen Stream<SuitCard>
zurück.
Tatsächlich besteht das Problem darin, dass Sie einen Stream<Card>
-Typ haben, obwohl Sie nach dem Filtern ziemlich sicher sind, dass der Stream nur SuitCard
-Objekte enthält. Sie wissen das, aber der Compiler nicht. Wenn Sie keinen ausführbaren Code in Ihren Stream einfügen möchten, können Sie stattdessen eine ungeprüfte Umwandlung in Stream<SuitCard>
vornehmen:
Map<Suit, Long> countBySuit = ((Stream<SuitCard>)contents.stream()
.filter( card -> card instanceof SuitCard ))
.collect( Collectors.groupingBy( SuitCard::getSuit, Collectors.counting() ) );
Auf diese Weise werden dem kompilierten Bytecode keine Anweisungen hinzugefügt. Leider sieht das ziemlich hässlich aus und erzeugt eine Compiler-Warnung. In meiner StreamEx - Bibliothek versteckte ich diese Hässlichkeit in der Bibliotheksmethode select()
, so dass Sie mit StreamEx schreiben können
Map<Suit, Long> countBySuit = StreamEx.of(contents)
.select( SuitCard.class )
.collect( Collectors.groupingBy( SuitCard::getSuit, Collectors.counting() ) );
Oder noch kürzer:
Map<Suit, Long> countBySuit = StreamEx.of(contents)
.select( SuitCard.class )
.groupingBy( SuitCard::getSuit, Collectors.counting() );
Wenn Sie keine Bibliotheken von Drittanbietern verwenden möchten, sieht Ihre Lösung mit zusätzlichen map
-Schritten in Ordnung aus. Obwohl es etwas mehr Aufwand verursacht, ist es normalerweise nicht sehr wichtig.