wake-up-neo.com

Casting-Typen in Java 8-Streams

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? 

11
Robert Lewis

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.

22
Joe C

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. 

13
JB Nizet

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.

1
Andrew Rueckert

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.

0
Tagir Valeev