Почему параметр типа сильнее параметра метода - PullRequest
12 голосов
/ 14 октября 2019

Почему

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 !!!
  }
}

javacSO58376589.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 :-)
  }
}

1 Ответ

12 голосов
/ 16 октября 2019

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

tl; dr

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

  • При прочих равных, компилятор выводит наиболее специфичный тип, который он может.
  • Однако, если он может найти a замена для параметра типа, который удовлетворяет всем требованиям, тогда компиляция будет успешной, однако расплывчато замена оказывается.
  • Для 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 появляется только один раз в подписи, что предлагает мне использовать параметр типа, не совпадающий с намерением этой функции языка.

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

Один из способов реализовать это немного более "намеченным образом" - разделить ваш метод 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. Разбивая метод на две подписи, он также лучше выражает намерение того, что вы пытаетесь сделать, с точки зрения безопасности типов:

  • Первый метод устанавливает класс (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

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

Следовательно, начальное ограничение, установленное для логического вывода , C , равно:

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

This уменьшает до связанного множества B 2 из:

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

Поскольку это не содержит границу ' false ' и (я предполагаю) разрешение из R успешно (дает Serializable), тогдавызов применим.

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

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

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

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

withX

У нас есть:

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

Начальный ограниченный набор, B 0 , составляет:

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

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

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

Следовательно, начальное ограничение установлено для вывод применимости , C , это:

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

Это уменьшает до ограниченного набора B 2 из:

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

Опять же, поскольку это не содержит границы ' false ' и разрешение из R успешно (дает String), тогда вызов применим.

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

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

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

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

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

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

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

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

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

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

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


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

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

  • Где тип значения - подтип типа возврата метода (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>. Это описано в сопроводительной документации по сокращению.

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

...