Типы приведения в потоках Java 8

#java #lambda #casting #java-8

#java #лямбда #Кастинг #java-8

Вопрос:

Чтобы получить некоторый опыт работы с новыми потоками Java, я разрабатывал фреймворк для обработки игральных карт. Вот первая версия моего кода для создания a Map , содержащего количество карт каждой масти в руке ( Suit является enum ):

 Map<Suit, Long> countBySuit = contents.stream() // contents is ArrayList<Card>  
        .collect( Collectors.groupingBy( Card::getSuit, Collectors.counting() ));
  

Это отлично сработало, и я был счастлив. Затем я провел рефакторинг, создав отдельные подклассы карт для «Мастей» и джокеров. Таким getSuit() образом, метод был перенесен из Card класса в его подкласс SuitCard , поскольку у джокеров нет масти. Новый код:

 Map<Suit, Long> countBySuit = contents.stream() // contents is ArrayList<Card>  
        .filter( card -> card instanceof SuitCard ) // reject Jokers
        .collect( Collectors.groupingBy( SuitCard::getSuit, Collectors.counting() ) );
  

Обратите внимание на умную вставку фильтра, чтобы убедиться, что рассматриваемая карта на самом деле является картой масти, а не джокером. Но это не работает! По-видимому collect , строка не понимает, что передаваемый объект гарантированно будет a SuitCard .

После долгого размышления над этим, в отчаянии я попытался вставить map вызов функции, и, что удивительно, это сработало!

 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() ) );
  

Я понятия не имел, что приведение типа считается исполняемым оператором. Почему это работает? И почему компилятор делает это необходимым?

Комментарии:

1. Вы меняете a Stream<Card> на a Stream<SuitCard> , поэтому он отлично компилируется при выполнении SuitCard метода для SuitCard элементов. Подумайте, если бы вы сделали это вручную, у вас все равно были бы Card объекты, и вам пришлось бы приводить их в свой цикл

2. В основном это сводится к следующему: почему компилятор хочет, чтобы вы выполняли приведение после instanceof вызова, поскольку он никак не может завершиться неудачей, если вы присвоите переменной тип, который вы только что проверили, является экземпляром. Вероятно, для этого есть веские причины, например, действительно ли это сделает код более понятным и коротким? Стоит ли для этого подключать компилятор?

Ответ №1:

Помните, что filter операция не изменит тип элементов Stream ‘s во время компиляции. Да, логически мы видим, что все, что проходит мимо этой точки, будет a SuitCard , все, что filter видит a Predicate . Если этот предикат изменится позже, это может привести к другим проблемам во время компиляции.

Если вы хотите изменить его на a Stream<SuitCard> , вам нужно будет добавить mapper, который выполняет приведение для вас:

 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() ) );
  

Я отсылаю вас к Javadoc для получения полной информации.

Комментарии:

1. Для instanceof мне также больше нравится ссылка на метод: .filter(SuitCard.class::isInstance)

2. Верно, но, учитывая, что instanceof это была ключевая часть вопроса, было неуместно менять его в данном конкретном случае.

3. SuitCard.class::cast это хорошо, но, к сожалению, менее эффективно, чем c -> (SuitCard) c потому Class.cast , что выполняет избыточную isInstance проверку.

4. @KonradRudolph Ты уверен? Потому что обычная операция приведения также выполняет проверку экземпляра (как иначе ClassCastException было бы выбрано a?).

5. @JoeC Совершенно уверен, просто проверьте реализацию (OpenJDK). И да, приведение внутренне также выполняет такую проверку (или, скорее, JRE делает). Но Class<T>.cast выполняет дополнительную , избыточную проверку.

Ответ №2:

Ну, map() позволяет преобразовать a Stream<Foo> в a Stream<Bar> , используя функцию, которая принимает a Foo в качестве аргумента и возвращает a Bar . И

 card -> (SuitCard) card
  

есть такая функция: она принимает карточку в качестве аргумента и возвращает карточку масти.

Вы могли бы написать это таким образом, если бы захотели, возможно, это сделает это более понятным для вас:

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

Компилятор делает это необходимым, потому что filter() преобразует a Stream<Card> в a Stream<Card> . Таким образом, вы не можете применить функцию, принимающую только SuitCard, к элементам этого потока, который может содержать любые карты: компилятору все равно, что делает ваш фильтр. Он заботится только о том, какой тип он возвращает.

Комментарии:

1. Отличное объяснение @JB . На самом деле, мне будет понятнее, если я представлю лямбду как написанную card -> {return (SuitCard)card;}

Ответ №3:

На самом деле проблема в том, что у вас есть Stream<Card> тип, хотя после фильтрации вы почти уверены, что поток не содержит ничего, кроме SuitCard объектов. Вы это знаете, но компилятор этого не делает. Если вы не хотите добавлять исполняемый код в свой поток, вы можете вместо этого выполнить непроверенное приведение к Stream<SuitCard> :

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

Таким образом, приведение не добавит никаких инструкций к скомпилированному байт-коду. К сожалению, это выглядит довольно некрасиво и выдает предупреждение компилятора. В моей библиотеке StreamEx я спрятал это уродство внутри библиотечного метода select() , поэтому, используя StreamEx, вы можете написать

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

Или даже короче:

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

Если вам не нравится использовать сторонние библиотеки, ваше решение, включающее дополнительный map шаг, выглядит нормально. Несмотря на то, что это добавляет некоторые накладные расходы, обычно это не очень существенно.

Комментарии:

1. Ссылка на метод SuitCard::getSuit SuitCard в любом случае будет содержать приведение типа к, что является причиной того, что непроверенное приведение потока к Stream<SuitCard> работает. Поэтому при замене SuitCard::getSuit на x -> ((SuitCard)x).getSuit() , вы ничем не жертвуете…

2. @Tagir Valeev Спасибо за библиотеку StreamEx. Ты мужчина!

Ответ №4:

Тип содержимого Card , поэтому contents.stream() возвращает Stream<Card> . Фильтр гарантирует, что каждый элемент в результирующем потоке является a SuitCard , однако фильтр не изменяет тип потока. card -> (SuitCard)card функционально эквивалентно card -> card , но его тип равен Function<Card,Suitcard> , поэтому .map() вызов возвращает a Stream<SuitCard> .