Почему параметр типа сильнее параметра метода

12

Почему

public <R, F extends Function<T, R>> Builder<T> withX(F getter, R returnValue) {...}

более строгим, чем

public <R> Builder<T> with(Function<T, R> getter, R returnValue) {...}

Это продолжение того, почему лямбда-тип возврата не проверяется во время компиляции . Я нашел с помощью метода, withX()как

.withX(MyInterface::getLength, "I am not a Long")

выдает искомую ошибку времени компиляции:

Тип getLength () из типа BuilderExample.MyInterface является длинным, это несовместимо с возвращаемым типом дескриптора: String

при использовании метода with()нет.

полный пример:

import java.util.function.Function;

public class SO58376589 {
  public static class Builder<T> {
    public <R, F extends Function<T, R>> Builder<T> withX(F getter, R returnValue) {
      return this;
    }

    public <R> Builder<T> with(Function<T, R> getter, R returnValue) {
      return this;
    }

  }

  static interface MyInterface {
    public Long getLength();
  }

  public static void main(String[] args) {
    Builder<MyInterface> b = new Builder<MyInterface>();
    Function<MyInterface, Long> getter = MyInterface::getLength;
    b.with(getter, 2L);
    b.with(MyInterface::getLength, 2L);
    b.withX(getter, 2L);
    b.withX(MyInterface::getLength, 2L);
    b.with(getter, "No NUMBER"); // error
    b.with(MyInterface::getLength, "No NUMBER"); // NO ERROR !!
    b.withX(getter, "No NUMBER"); // error
    b.withX(MyInterface::getLength, "No NUMBER"); // error !!!
  }
}

javac SO58376589.java

SO58376589.java:32: error: method with in class Builder<T> cannot be applied to given types;
    b.with(getter, "No NUMBER"); // error
     ^
  required: Function<MyInterface,R>,R
  found: Function<MyInterface,Long>,String
  reason: inference variable R has incompatible bounds
    equality constraints: Long
    lower bounds: String
  where R,T are type-variables:
    R extends Object declared in method <R>with(Function<T,R>,R)
    T extends Object declared in class Builder
SO58376589.java:34: error: method withX in class Builder<T> cannot be applied to given types;
    b.withX(getter, "No NUMBER"); // error
     ^
  required: F,R
  found: Function<MyInterface,Long>,String
  reason: inference variable R has incompatible bounds
    equality constraints: Long
    lower bounds: String
  where F,R,T are type-variables:
    F extends Function<MyInterface,R> declared in method <R,F>withX(F,R)
    R extends Object declared in method <R,F>withX(F,R)
    T extends Object declared in class Builder
SO58376589.java:35: error: incompatible types: cannot infer type-variable(s) R,F
    b.withX(MyInterface::getLength, "No NUMBER"); // error
           ^
    (argument mismatch; bad return type in method reference
      Long cannot be converted to String)
  where R,F,T are type-variables:
    R extends Object declared in method <R,F>withX(F,R)
    F extends Function<T,R> declared in method <R,F>withX(F,R)
    T extends Object declared in class Builder
3 errors

Расширенный пример

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

import java.util.function.Consumer;
import java.util.function.Supplier;
interface TypeInference {

  Number getNumber();

  void setNumber(Number n);

  @FunctionalInterface
  interface Method<R> {
    TypeInference be(R r);
  }

  //Supplier:
  <R> R letBe(Supplier<R> supplier, R value);
  <R, F extends Supplier<R>> R letBeX(F supplier, R value);
  <R> Method<R> let(Supplier<R> supplier);  // return (x) -> this;

  //Consumer:
  <R> R lettBe(Consumer<R> supplier, R value);
  <R, F extends Consumer<R>> R lettBeX(F supplier, R value);
  <R> Method<R> lett(Consumer<R> consumer);


  public static void main(TypeInference t) {
    t.letBe(t::getNumber, (Number) 2); // Compiles :-)
    t.lettBe(t::setNumber, (Number) 2); // Compiles :-)
    t.letBe(t::getNumber, 2); // Compiles :-)
    t.lettBe(t::setNumber, 2); // Compiles :-)
    t.letBe(t::getNumber, "NaN"); // !!!! Compiles :-(
    t.lettBe(t::setNumber, "NaN"); // Does not compile :-)

    t.letBeX(t::getNumber, (Number) 2); // Compiles :-)
    t.lettBeX(t::setNumber, (Number) 2); // Compiles :-)
    t.letBeX(t::getNumber, 2); // !!! Does not compile  :-(
    t.lettBeX(t::setNumber, 2); // Compiles :-)
    t.letBeX(t::getNumber, "NaN"); // Does not compile :-)
    t.lettBeX(t::setNumber, "NaN"); // Does not compile :-)

    t.let(t::getNumber).be(2); // Compiles :-)
    t.lett(t::setNumber).be(2); // Compiles :-)
    t.let(t::getNumber).be("NaN"); // Does not compile :-)
    t.lett(t::setNumber).be("NaN"); // Does not compile :-)
  }
}
jukzi
источник
1
Из-за заключения с последним. Хотя оба они основаны на сценарии использования, который необходимо реализовать. Для вас первый может быть строгим и хорошим. Для гибкости кто-то другой может предпочесть последнее.
Наман
Вы пытаетесь скомпилировать это в Eclipse? Поиск строк ошибок в формате, который вы вставили, предполагает, что это ошибка, специфичная для Eclipse (ecj). Вы сталкиваетесь с такой же проблемой при компиляции с помощью raw javacили инструмента сборки, такого как Gradle или Maven?
user31601
@ user31601 я добавил полный пример с выводом javac. Сообщения об ошибках немного различаются, но затмения и javac
ведут

Ответы:

12

Это действительно интересный вопрос. Боюсь, ответ сложный.

ТЛ; др

Выяснение различий включает в себя довольно глубокое чтение спецификации логического вывода типов Java , но в основном сводится к следующему:

  • При прочих равных, компилятор выводит наиболее конкретный тип, который он может.
  • Однако, если он может найти на замену для параметра типа , который удовлетворяет все требования, то компиляция будет добиться успеха, однако расплывчатой замена оказывается.
  • Ибо withесть (по-видимому, расплывчатая) замена, которая удовлетворяет всем требованиям R:Serializable
  • Для withX, введение дополнительного параметра типа Fвынуждает компилятор Rсначала разрешать , не принимая во внимание ограничение F extends Function<T,R>. Rразрешается до (гораздо более конкретно), Stringчто означает, что вывод Fнеудач.

Эта последняя точка пули является самой важной, но также и самой волнистой. Я не могу придумать лучшего и краткого формулировки, поэтому, если вам нужны более подробные сведения, я предлагаю вам прочитать полное объяснение ниже.

Это намеренное поведение?

Я собираюсь выйти на конечности здесь и сказать нет .

Я не предполагаю, что в спецификации есть ошибка, более того, что (в случае withX) разработчики языка подняли руки и сказали: «В некоторых ситуациях вывод типов становится слишком сложным, поэтому мы просто потерпим неудачу» . Несмотря на то, что поведение компилятора по отношению к withXтому, что вам нужно, я бы посчитал, что это побочный побочный эффект текущей спецификации, а не положительно задуманное дизайнерское решение.

Это важно, потому что это дает вопрос: следует ли мне полагаться на такое поведение в дизайне моего приложения? Я бы сказал, что вы не должны этого делать, потому что вы не можете гарантировать, что будущие версии языка будут продолжать вести себя таким образом.

Хотя разработчики языка очень стараются не ломать существующие приложения при обновлении своих спецификаций / дизайна / компилятора, проблема заключается в том, что поведение, на которое вы хотите положиться, - это то, где компилятор в настоящее время терпит неудачу (то есть не существующее приложение ). Обновления Langauge постоянно превращают некомпилируемый код в компилируемый. Например, следующий код может быть гарантировано не компилировать в Java 7, но будет компилировать в Java 8:

static Runnable x = () -> System.out.println();

Ваш вариант использования не отличается.

Еще одна причина, по которой я буду осторожен при использовании вашего withXметода, - это Fсам параметр. Как правило, параметр универсального типа в методе (который не указан в возвращаемом типе) существует для связывания типов нескольких частей подписи вместе. Это говорит:

Мне все равно, что Tесть, но хочу быть уверенным, что везде, где я использую, Tэто один и тот же тип.

Таким образом, логично, что мы ожидаем, что каждый параметр типа появится как минимум дважды в сигнатуре метода, в противном случае «он ничего не делает». Fв вашей withXподписи появляется только один раз, что подсказывает мне использование параметра типа, не соответствующего цели этой функции языка.

Альтернативная реализация

Один из способов реализовать это немного более «намеченным образом» - разделить ваш withметод на цепочку из 2:

public class Builder<T> {

    public final class With<R> {
        private final Function<T,R> method;

        private With(Function<T,R> method) {
            this.method = method;
        }

        public Builder<T> of(R value) {
            // TODO: Body of your old 'with' method goes here
            return Builder.this;
        }
    }

    public <R> With<R> with(Function<T,R> method) {
        return new With<>(method);
    }

}

Это может быть использовано следующим образом:

b.with(MyInterface::getLong).of(1L); // Compiles
b.with(MyInterface::getLong).of("Not a long"); // Compiler error

Это не включает посторонний параметр типа, как у вас withX. Разбивая метод на две подписи, он также лучше выражает намерение того, что вы пытаетесь сделать, с точки зрения безопасности типов:

  • Первый метод устанавливает class ( With), который определяет тип на основе ссылки на метод.
  • Метод scond ( of) ограничивает тип valueсовместимости с тем, что вы ранее настроили.

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

И последнее замечание, чтобы сделать всю эту вещь неактуальной: я думаю, что Mockito (и, в частности, его функциональность заглушки ), в принципе, может уже делать то, что вы пытаетесь достичь с помощью вашего «универсального конструктора безопасных типов». Может быть, вы могли бы просто использовать это вместо этого?

Полное (ish) объяснение

Я собираюсь проработать процедуру вывода типа для обоих withи withX. Это довольно долго, поэтому принимайте это медленно. Несмотря на то, что я долго, я все еще оставил довольно много деталей. Вы можете обратиться к спецификации для получения более подробной информации (перейдите по ссылкам), чтобы убедиться, что я прав (возможно, я допустил ошибку).

Также, чтобы немного упростить ситуацию, я собираюсь использовать более минимальный пример кода. Основное отличие заключается в том , что она меняет местами вне Functionдля Supplier, так что есть меньше типов и параметров в игре. Вот полный фрагмент, который воспроизводит поведение, которое вы описали:

public class TypeInference {

    static long getLong() { return 1L; }

    static <R> void with(Supplier<R> supplier, R value) {}
    static <R, F extends Supplier<R>> void withX(F supplier, R value) {}

    public static void main(String[] args) {
        with(TypeInference::getLong, "Not a long");       // Compiles
        withX(TypeInference::getLong, "Also not a long"); // Does not compile
    }

}

Работа Давайте через тип вывод применимости и логический вывод типа процедуру для каждого вызова метода , в своей очереди:

with

У нас есть:

with(TypeInference::getLong, "Not a long");

Начальное связанное множество, B 0 , равно:

  • R <: Object

Все выражения параметров имеют отношение к применимости .

Следовательно, исходный набор ограничений для вывода применимости , С , является:

  • TypeInference::getLong совместим с Supplier<R>
  • "Not a long" совместим с R

Это сводит к ограниченному набору B 2 из:

  • R <: Object(от B 0 )
  • Long <: R (из первого ограничения)
  • String <: R (из второго ограничения)

Так как это не содержит связанный « ЛОЖЬ », и (я предполагаю) разрешение на Rпреуспевает (предоставление Serializable), то вызов применим.

Итак, мы переходим к выводу типа вызова .

Новый набор ограничений C со связанными входными и выходными переменными:

  • TypeInference::getLong совместим с Supplier<R>
    • Входные переменные: нет
    • Выходные переменные: R

Это не содержит взаимозависимостей между входными и выходными переменными, поэтому может быть уменьшено за один шаг, и окончательный набор границ, B 4 , такой же, как B 2 . Следовательно, разрешение преуспевает как прежде, и компилятор вздыхает с облегчением!

withX

У нас есть:

withX(TypeInference::getLong, "Also not a long");

Начальное связанное множество, B 0 , равно:

  • R <: Object
  • F <: Supplier<R>

Только выражение второго параметра имеет отношение к применимости . Первый ( TypeInference::getLong) нет, потому что он соответствует следующему условию:

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

Следовательно, исходный набор ограничений для вывода применимости , С , является:

  • "Also not a long" совместим с R

Это сводит к ограниченному набору B 2 из:

  • R <: Object(от B 0 )
  • F <: Supplier<R>(от B 0 )
  • String <: R (из ограничения)

Опять же , так как это не содержит связанную « ЛОЖЬ », и разрешение на Rпреуспевает (предоставление String), то вызов применим.

Вывод типа вызова еще раз ...

На этот раз новый набор ограничений C с соответствующими входными и выходными переменными:

  • TypeInference::getLong совместим с F
    • Входные переменные: F
    • Выходные переменные: нет

Опять же, у нас нет взаимозависимостей между входными и выходными переменными. Однако на этот раз, то есть входной переменный ( F), поэтому мы должны решить это , прежде чем снижение . Итак, начнем с нашего связанного множества B 2 .

  1. Мы определяем подмножество Vследующим образом:

    Учитывая набор переменных логического вывода для разрешения, позвольте Vбыть объединением этого набора и всех переменных, от которых зависит разрешение по крайней мере одной переменной в этом наборе.

    По второй оценке в B 2 разрешение Fзависит от R, т V := {F, R}.

  2. Мы выбираем подмножество в Vсоответствии с правилом:

    позвольте { α1, ..., αn }быть непустым подмножеством необоснованных переменных в Vтакой, что i) для всех i (1 ≤ i ≤ n), если αiзависит от разрешения переменной β, то либо βимеет экземпляр, либо есть некоторые jтакие, которые β = αj; и ii) не существует непустого собственного подмножества { α1, ..., αn }с этим свойством.

    Единственное подмножество, Vкоторое удовлетворяет этому свойству, это {R}.

  3. Используя третий bound ( String <: R), мы создаем его R = Stringи включаем в наш ограниченный набор. Rтеперь решен, и вторая граница эффективно становится F <: Supplier<String>.

  4. Используя (пересмотренную) вторую оценку, мы создаем экземпляр F = Supplier<String>. Fсейчас решено.

Теперь, когда Fэто решено, мы можем приступить к сокращению , используя новое ограничение:

  1. TypeInference::getLong совместим с Supplier<String>
  2. ... сводится к Long совместим с String
  3. ... который сводится к ложному

... и мы получаем ошибку компилятора!


Дополнительные примечания к «Расширенному примеру»

Расширенный Пример в вопросе выглядит на несколько интересных случаях, которые непосредственно не охвачены выше выработками:

  • Где тип значения является подтипом метода, возвращаемого типом ( Integer <: Number)
  • Где функциональный интерфейс является контравариантным в предполагаемом типе (то есть, Consumerа не Supplier)

В частности, 3 из данных вызовов выделяются как потенциально предполагающие «отличное» поведение компилятора от описанного в пояснениях:

t.lettBe(t::setNumber, "NaN"); // Does not compile :-)

t.letBeX(t::getNumber, 2); // !!! Does not compile  :-(
t.lettBeX(t::setNumber, 2); // Compiles :-)

Второй из этих 3 будет проходить точно такой же процесс вывода, как withXуказано выше (просто заменить Longна Numberи Stringс Integer). Это иллюстрирует еще одну причину , почему вы не должны полагаться на это не смогло поведения типа логического вывода для вашего дизайна класса, как неспособность собрать здесь, скорее всего , не желательное поведение.

Для других 2 (и, действительно, для любых других вызовов, связанных с тем, через что Consumerвы хотите работать), поведение должно быть очевидным, если вы работаете с процедурой вывода типа, изложенной для одного из методов выше (то есть withдля первого, withXдля в третьих). Есть только одно маленькое изменение, на которое нужно обратить внимание:

  • Ограничение по первому параметру ( t::setNumber совместимо с Consumer<R> ) будет уменьшено до R <: Numberтого, Number <: Rчто было сделано для Supplier<R>. Это описано в связанной документации по сокращению.

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

user31601
источник
Очень глубокий, хорошо проработанный и сформулированный. Спасибо!
Забузард
@ user31601 Можете ли вы указать, где разница между поставщиком и потребителем вступает в игру. Я добавил расширенный пример в исходный вопрос для этого. Он показывает ковариантное, контравариантное и инвариантное поведение для разных версий letBe (), letBeX () и let (). Be () в зависимости от поставщика / потребителя.
Джукзи
@jukzi Я добавил несколько дополнительных заметок, но у вас должно быть достаточно информации для самостоятельной работы с этими новыми примерами.
user31601
Это интересно: так много особых случаев в 18.2.1. для лямбд и ссылок на методы, где я не ожидал бы никакого особого случая для них от моего наивного понимания. И, вероятно, ни один обычный разработчик не ожидает.
Джукзи
Ну, я думаю, причина в том, что с лямбдами и ссылками на методы, компилятор должен решить, какой тип лямбда должен реализовать - он должен сделать выбор! Например, TypeInference::getLongможет реализовывать Supplier<Long>или Supplier<Serializable>или и Supplier<Number>т. Д., Но , главное , он может реализовать только один из них (как и любой другой класс)! Это отличается от всех других выражений, где все реализованные типы известны заранее, и компилятору нужно только определить, соответствует ли одно из них требованиям ограничения.
user31601