Ярб-выражения Java, литье и компараторы

Я просматривал исходный код Java для интерфейса Map и сталкивался с этим небольшим фрагментом кода:

    /**
     * Returns a comparator that compares {@link Map.Entry} in natural order on value.
     *
     * <p>The returned comparator is serializable and throws {@link
     * NullPointerException} when comparing an entry with null values.
     *
     * @param <K> the type of the map keys
     * @param <V> the {@link Comparable} type of the map values
     * @return a comparator that compares {@link Map.Entry} in natural order on value.
     * @see Comparable
     * @since 1.8
     */
    public static <K, V extends Comparable<? super V>> Comparator<Map.Entry<K,V>> comparingByValue() {
        return (Comparator<Map.Entry<K, V>> & Serializable)
            (c1, c2) -> c1.getValue().compareTo(c2.getValue());
    }

Из объявления метода я получаю, что это общий метод, который возвращает Компаратор типа, который либо выводится из записей карты, переданных ему, либо явно предоставленных в методе.

То, что действительно меня выбрасывает, - это возвращаемая стоимость. По-видимому, выражение лямбда

(c1, c2) -> c1.getValue().compareTo(c2.getValue());

явно Comparator<Map.Entry<K, V>>. Это правильно?

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

((SubClass & AnInterface) anObject).interfaceMethod();

Хотя следующее не работает:

public class Foo {
    public static void main(String[] args) {
        Object o = new Foo() {
            public void bar() {
                System.out.println("nope");
            }
        };
        ((Foo & Bar) o).bar();
    }   
}

interface Bar {
    public void bar();
}

Итак, два вопроса:

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

  2. Можете ли вы применить выражение Lambda к Comparator? Что еще их можно отличить? Или это лямбда-выражение, по существу, просто Comparator? Может ли кто-нибудь прояснить все это?

Ответ 1

Как добавить интерфейс к роли, который должен работать?

У этого есть синтаксис приведения, однако он фактически определяет тип lambda, который вы создаете через интерфейс типа. Т.е. вы не создаете экземпляр объекта, который затем передается другому типу.

Означает ли это только принудительный тип возвращаемого метода интерфейса?

Это фактически определяет тип, который лямбда будет построена как во время выполнения. Существует LambdaMetaFactory, который получает этот тип во время выполнения и генерирует дополнительный код, если тип включает Serializable.

Можете ли вы применить выражение Lambda к компаратору?

Вы можете ссылаться только на тип, который уже есть. В этом случае вы определяете, что создаваемая лямбда должна быть Comparator. Вы можете использовать любой тип, который имеет только один абстрактный метод.

Или это лямбда-выражение, по существу, просто компаратор?

Один и тот же лямбда-код можно было бы использовать (копировать + вставлять) в разных контекстах и разных интерфейсах без изменений. Он не обязательно должен быть Comparator как вы увидите во многих других примерах в JDK.

Я считаю интересным метод count в Stream.

Ответ 2

Хотя Питер дал отличный ответ, позвольте мне добавить больше для дальнейшей ясности.

Лямбда получает свой точный тип только при инициализации. Это зависит от типа цели. Например:

Comparator<Integer> comp = (Integer c1, Integer c2) -> c1.compareTo(c2);
BiFunction<Integer, Integer, Integer> c = (Integer c1, Integer c2) -> c1.compareTo(c2);
Comparator<Integer> compS = (Comparator<Integer> & Serializable) (Integer c1, Integer c2) -> c1.compareTo(c2);

Над его же lambda во всех 3 случаях, но он получает свой тип на основе ссылочного типа, который вы предоставили. Следовательно, вы можете установить одну и ту же лямбду на 3 разных типа в каждом случае.

Но помните, как только тип установлен (при инициализации), то он больше не может быть изменен. Он впитывается на уровне байт-кода. Поэтому, очевидно, вы не можете передать c методу, который ожидает Comparator потому что после инициализации они похожи на обычные Java-объекты. (Вы можете посмотреть на этот класс, чтобы поиграть и создать лямбды на ходу)


Поэтому в случае:

(Comparator<Map.Entry<K, V>> & Serializable)
            (c1, c2) -> c1.getValue().compareTo(c2.getValue());

Лямбда инициализируется целевым типом как Компаратор и Сериализуемый. Обратите внимание, что тип возвращаемого метода - это просто Comparator, но поскольку Serializable также вписан в него во время инициализации, он всегда может быть сериализован, даже если это сообщение потеряно в сигнатуре метода.

Обратите внимание, что отливка от лямбда отличается от ((Foo & Bar) o).bar(); , В случае лямбды вы инициализируете лямбду с ее типом в качестве объявленного целевого типа. Но с ((Foo & Bar) o).bar(); , вы вводите тип переменной o в Foo и Bar. В первом случае вы устанавливаете тип. В последнем случае у него уже есть тип, и вы пытаетесь применить его к чему-то другому. Следовательно, в первом случае он выбрасывает ClassCastException, потому что он не может преобразовать o в Bar

Как добавить интерфейс к роли, который должен работать?

Для объекта, как и обычно. Для лямбда, описанных выше.

Означает ли это только принудительный тип возвращаемого метода интерфейса?

Нет. Java не имеет структурных типов. Так что никакого специального типа, основанного на методе. Он просто пытается отличить o как от SO1 и от Bar и он терпит неудачу из-за последних

Можете ли вы применить выражение Lambda к компаратору? Что еще их можно отличить? Или это лямбда-выражение, по существу, просто компаратор? Может ли кто-нибудь прояснить все это?

Как объяснялось выше. Лямбда может быть инициализирована на любой функциональный интерфейс, основанный на том, что все интерфейсы имеют право на эту лямбду. В приведенных выше примерах вы, очевидно, не можете инициализировать (c1, c2) → c1.compareTo(c2) в предикат

Ответ 3

В соответствии с Спецификацией языка Java оператор трансляции (независимо от того, что находится в скобках) может быть ссылочным типом, за которым следует один или несколько дополнительных условий, например один или несколько типов интерфейса. Кроме того, спецификация также заявляет, что она является ошибкой времени компиляции, если тип экземпляра типа компиляции никогда не может быть применен к типу, указанному оператором литья, в соответствии с правилами преобразования каста.

В вашем случае Foo не реализует Bar, но этот факт может не проявляться во время компиляции, поэтому вы получаете ClassCastException потому что хотя метод, определенный в анонимном классе, имеет ту же подпись, что и ClassCastException в Bar, объект явно не реализует Bar. Более того, методы, определенные в анонимных классах, скрыты, если не вызываются в том же самом утверждении, что и при их определении,

new MyClass() {
  void doSomethingAwesome() { /* ... */ }
}.doSomethingAwesome();

работает, но это не так:

MyClass awesome = new MyClass() {
  void doSomethingAwesome() { /* ... */ }
};
// undefined method, does not compile!
awesome.doSomethingAwesome();