Подтвердить что ты не робот

Внедрение функциональных интерфейсов Java

Как всегда я просматривал источники JDK 8 и нашел очень интересный код:

@Override
default void forEachRemaining(Consumer<? super Integer> action) {
    if (action instanceof IntConsumer) {
        forEachRemaining((IntConsumer) action);
    } 
}

Возникает вопрос: каким образом Consumer<? super Integer> может быть экземпляром IntConsumer? Потому что они находятся в разных иерархиях.

Я сделал аналогичный фрагмент кода для тестирования кастинга:

public class InterfaceExample {
    public static void main(String[] args) {
        IntConsumer intConsumer = i -> { };
        Consumer<Integer> a = (Consumer<Integer>) intConsumer;

        a.accept(123);
    }
}

Но он выбрасывает ClassCastException:

Exception in thread "main" 
    java.lang.ClassCastException: 
       com.example.InterfaceExample$$Lambda$1/764977973 
     cannot be cast to 
       java.util.function.Consumer

Вы можете найти этот код на java.util.Spliterator.OfInt # forEachRemaining (java.util.function.Consumer)

4b9b3361

Ответ 1

Посмотрите код ниже, тогда вы можете понять, почему?

class IntegerConsumer implements Consumer<Integer>, IntConsumer {
   ...
}

Любой класс может реализовывать мультиинтерфейсы, один из которых Consumer<Integer> может реализовывать другой, IntConsumer. Иногда происходит, когда мы хотим адаптировать IntConsumer к Consumer<Integer> и сохранить его тип начала (IntConsumer), тогда код выглядит следующим образом:

class IntConsumerAdapter implements Consumer<Integer>, IntConsumer {

    @Override
    public void accept(Integer value) {
        accept(value.intValue());
    }

    @Override
    public void accept(int value) {
        // todo
    }
}

Примечание: это использование шаблона проектирования адаптера класса.

THEN вы можете использовать IntConsumerAdapter как Consumer<Integer> и IntConsumer, например:

Consumer<? extends Integer> consumer1 = new IntConsumerAdapter();
IntConsumer consumer2 = new IntConsumerAdapter();

Sink.OfInt - это конкретное использование шаблона проектирования адаптера класса в jdk-8. Недостатком Sink.OfInt#accept(Integer) является то, что JVM будет бросать NullPointerException когда он принимает значение null, поэтому Sink является видимым пакетом.

 189  интерфейс OfInt   extends Sink < Integer>, IntConsumer {
190 @Override
191 void accept ( int);
193 @ Override
194 default void accept (Integer i) {
195 if (Tripwire.ENABLED)
196 Tripwire. trip (getClass(), "{0} вызов Sink.OfInt.accept(Integer)" );
197 accept (т.е. intValue());
198}
199}

Я нашел, почему нужно придать Consumer<Integer> в IntConsumer, если передать потребителю, например, как IntConsumerAdapter?

Одна из причин заключается в том, что мы используем Consumer для принятия int, чтобы компилятор должен был автоматически привязать его к Integer. И в методе accept(Integer) вам необходимо вручную удалить Integer в int. Другими словами, каждый accept(Integer) выполняет 2 дополнительные операции для бокса/распаковки. Он должен улучшить производительность, поэтому он проводит специальную проверку в библиотеке алгоритмов.

Другая причина заключается в повторном использовании фрагмента кода. Тело OfInt # forEachRemaining (Consumer) является хорошим примером применения Шаблон проектирования адаптера для повторного использования OfInt # forEachRenaming (IntConsumer).

default void forEachRemaining(Consumer<? super Integer> action) {
    if (action instanceof IntConsumer) {
    //   action implementation is an example of Class Adapter Design Pattern
    //                                   |
        forEachRemaining((IntConsumer) action);
    }
    else {
    //  method reference expression is an example of Object Adapter Design Pattern
    //                                        |
        forEachRemaining((IntConsumer) action::accept);
    }
}

Ответ 2

Поскольку класс реализации может реализовывать оба интерфейса.

Любое поведение любого типа интерфейса законно, , если передаваемый объект может реализовать интерфейс назначения. Это известно во время компиляции как false, когда тип источника является конечным классом, который не реализует интерфейс, или когда может быть доказано, что он имеет различную параметризацию типов, что приводит к такому же стиранию. Во время выполнения, если объект не реализует интерфейс, вы получите ClassCastException. Проверка с помощью instanceof перед тем, как попытаться выполнить бросок, позволяет избежать исключения.

Из спецификации языка Java, 5.5.1: Тип ссылки:

5.5.1 Тип ссылочного типа С учетом ссылочного типа времени компиляции S (источник) и ссылочного типа времени компиляции T (target), преобразование каста существует от S до T, если ошибки времени компиляции не происходят из-за следующих правил.

...

• Если T - тип интерфейса: - Если S не является конечным классом (§8.1.1), то, если существует супертип X из T и супертип Y из S, такой, что и X, и Y являются доказуемо различными параметризованными типами, и что стирания X и Y одинаковы, возникает ошибка времени компиляции.

В противном случае листинг всегда легален во время компиляции (потому что даже если S не реализует T, может быть подкласс S).

Ответ 3

Обратите внимание, что вы могли бы найти это поведение, не заглядывая в исходный код, просто посмотрев официальную документацию API, вы связались сами

Требования к реализации:

Если действие представляет собой экземпляр IntConsumer, то оно передается в IntConsumer и передается forEachRemaining (java.util.function.IntConsumer); в противном случае действие адаптируется к экземпляру IntConsumer, поместив аргумент IntConsumer, а затем передается в forEachRemaining (java.util.function.IntConsumer ).

Итак, в любом случае будет вызываться forEachRemaining(IntConsumer), что является фактическим методом реализации. Но когда это возможно, создание бокс-адаптера будет опущено. Причина в том, что a Spliterator.OfInt также является Spliterator<Integer>, который предлагает только метод forEachRemaining(Consumer<Integer>). Специальное поведение позволяет обрабатывать типичные экземпляры Spliterator и их примитивные (Spliterator.OfPrimitive) аналоги с автоматическим выбором наиболее эффективного метода.

Как говорят другие, вы можете реализовать несколько интерфейсов с обычным классом. Кроме того, вы можете реализовать несколько интерфейсов с помощью выражения лямбда, если вы создаете вспомогательный тип, например

interface UnboxingConsumer extends IntConsumer, Consumer<Integer> {
    public default void accept(Integer t) {
        System.out.println("unboxing "+t);
        accept(t.intValue());
    }
}
public static void printAll(BaseStream<Integer,?> stream) {
    stream.spliterator().forEachRemaining((UnboxingConsumer)System.out::println);
}
public static void main(String[] args) {
    System.out.println("Stream.of(1, 2, 3):");
    printAll(Stream.of(1, 2, 3));
    System.out.println("IntStream.range(0, 3)");
    printAll(IntStream.range(0, 3));
}
Stream.of(1, 2, 3):
unboxing 1
1
unboxing 2
2
unboxing 3
3
IntStream.range(0, 3)
0
1
2