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

Почему этот тип вывода не работает с этим сценарием Lambda?

У меня странный сценарий, когда вывод типа не работает, как я ожидал бы при использовании выражения лямбда. Здесь приведен пример моего реального сценария:

static class Value<T> {
}

@FunctionalInterface
interface Bar<T> {
  T apply(Value<T> value); // Change here resolves error
}

static class Foo {
  public static <T> T foo(Bar<T> callback) {
  }
}

void test() {
  Foo.foo(value -> true).booleanValue(); // Compile error here
}

Ошибка компиляции, которую я получаю от второй до последней строки,

Метод booleanValue() - undefined для объекта Object

если я отбросил лямбда до Bar<Boolean>:

Foo.foo((Bar<Boolean>)value -> true).booleanValue();

или если я изменяю подпись метода Bar.apply для использования необработанных типов:

T apply(Value value);

тогда проблема уходит. То, как я ожидаю, что это сработает, это то, что:

  • Foo.foo вызов должен выводить возвращаемый тип boolean
  • value в лямбда следует вывести на Value<Boolean>.

Почему этот вывод не работает должным образом и как я могу изменить этот API, чтобы он работал должным образом?

4b9b3361

Ответ 1

Под капюшоном

Используя некоторые скрытые функции javac, мы можем получить дополнительную информацию о том, что происходит:

$ javac -XDverboseResolution=deferred-inference,success,applicable LambdaInference.java 
LambdaInference.java:16: Note: resolving method foo in type Foo to candidate 0
    Foo.foo(value -> true).booleanValue(); // Compile error here
       ^
  phase: BASIC
  with actuals: <none>
  with type-args: no arguments
  candidates:
      #0 applicable method found: <T>foo(Bar<T>)
        (partially instantiated to: (Bar<Object>)Object)
  where T is a type-variable:
    T extends Object declared in method <T>foo(Bar<T>)
LambdaInference.java:16: Note: Deferred instantiation of method <T>foo(Bar<T>)
    Foo.foo(value -> true).booleanValue(); // Compile error here
           ^
  instantiated signature: (Bar<Object>)Object
  target-type: <none>
  where T is a type-variable:
    T extends Object declared in method <T>foo(Bar<T>)
LambdaInference.java:16: error: cannot find symbol
    Foo.foo(value -> true).booleanValue(); // Compile error here
                          ^
  symbol:   method booleanValue()
  location: class Object
1 error

Это много информации, пусть сломается.

LambdaInference.java:16: Note: resolving method foo in type Foo to candidate 0
    Foo.foo(value -> true).booleanValue(); // Compile error here
       ^
  phase: BASIC
  with actuals: <none>
  with type-args: no arguments
  candidates:
      #0 applicable method found: <T>foo(Bar<T>)
        (partially instantiated to: (Bar<Object>)Object)
  where T is a type-variable:
    T extends Object declared in method <T>foo(Bar<T>)

phase: применимость метода actuals: фактические аргументы, переданные в
type-args: аргументы явного типа
кандидаты: потенциально применимые методы

actuals <none>, потому что наша неявно типизированная лямбда не применима к применимости.

Компилятор решает ваш вызов foo единственным методом с именем foo в foo. Он частично был создан для Foo.<Object> foo (поскольку не было никаких фактических данных или типов-аргументов), но это может измениться на стадии отложенного вывода.

LambdaInference.java:16: Note: Deferred instantiation of method <T>foo(Bar<T>)
    Foo.foo(value -> true).booleanValue(); // Compile error here
           ^
  instantiated signature: (Bar<Object>)Object
  target-type: <none>
  where T is a type-variable:
    T extends Object declared in method <T>foo(Bar<T>)

экземплярная подпись: полностью созданная подпись foo. Это результат этого шага (в этот момент больше не будет сделано вывод о типе подписи foo).
target-type: контекст, в который делается вызов. Если вызов метода является частью назначения, он будет левым. Если вызов метода сам по себе является частью вызова метода, это будет тип параметра.

Поскольку ваш вызов метода свисает, нет целевого типа. Поскольку нет целевого типа, больше не может быть сделано вывод на foo, а T - Object.


Анализ

Компилятор не использует неявно типизированные lambdas во время вывода. В определенной степени это имеет смысл. В общем случае, учитывая param -> BODY, вы не сможете скомпилировать BODY, пока не получите тип для param. Если вы попытались вывести тип param из BODY, это может привести к проблеме типа курица и яйцо. Возможно, некоторые улучшения будут сделаны в этом в будущих выпусках Java.


Решение

Foo.<Boolean> foo(value -> true)

Это решение предоставляет явный аргумент типа foo (обратите внимание на раздел with type-args ниже). Это изменяет частичную реализацию сигнатуры метода на (Bar<Boolean>)Boolean, что вы хотите.

LambdaInference.java:16: Note: resolving method foo in type Foo to candidate 0
    Foo.<Boolean> foo(value -> true).booleanValue(); // Compile error here
       ^
  phase: BASIC
  with actuals: <none>
  with type-args: Boolean
  candidates:
      #0 applicable method found: <T>foo(Bar<T>)
        (partially instantiated to: (Bar<Boolean>)Boolean)
  where T is a type-variable:
    T extends Object declared in method <T>foo(Bar<T>)
LambdaInference.java:16: Note: resolving method booleanValue in type Boolean to candidate 0
    Foo.<Boolean> foo(value -> true).booleanValue(); // Compile error here
                                    ^
  phase: BASIC
  with actuals: no arguments
  with type-args: no arguments
  candidates:
      #0 applicable method found: booleanValue()

Foo.foo((Value<Boolean> value) -> true)

Это решение явно вводит вашу лямбду, что позволяет ей быть применимой к применимости (примечание with actuals ниже). Это изменяет частичную реализацию сигнатуры метода на (Bar<Boolean>)Boolean, что вы хотите.

LambdaInference.java:16: Note: resolving method foo in type Foo to candidate 0
    Foo.foo((Value<Boolean> value) -> true).booleanValue(); // Compile error here
       ^
  phase: BASIC
  with actuals: Bar<Boolean>
  with type-args: no arguments
  candidates:
      #0 applicable method found: <T>foo(Bar<T>)
        (partially instantiated to: (Bar<Boolean>)Boolean)
  where T is a type-variable:
    T extends Object declared in method <T>foo(Bar<T>)
LambdaInference.java:16: Note: Deferred instantiation of method <T>foo(Bar<T>)
    Foo.foo((Value<Boolean> value) -> true).booleanValue(); // Compile error here
           ^
  instantiated signature: (Bar<Boolean>)Boolean
  target-type: <none>
  where T is a type-variable:
    T extends Object declared in method <T>foo(Bar<T>)
LambdaInference.java:16: Note: resolving method booleanValue in type Boolean to candidate 0
    Foo.foo((Value<Boolean> value) -> true).booleanValue(); // Compile error here
                                           ^
  phase: BASIC
  with actuals: no arguments
  with type-args: no arguments
  candidates:
      #0 applicable method found: booleanValue()

Foo.foo((Bar<Boolean>) value -> true)

То же, что и выше, но с немного отличающимся вкусом.

LambdaInference.java:16: Note: resolving method foo in type Foo to candidate 0
    Foo.foo((Bar<Boolean>) value -> true).booleanValue(); // Compile error here
       ^
  phase: BASIC
  with actuals: Bar<Boolean>
  with type-args: no arguments
  candidates:
      #0 applicable method found: <T>foo(Bar<T>)
        (partially instantiated to: (Bar<Boolean>)Boolean)
  where T is a type-variable:
    T extends Object declared in method <T>foo(Bar<T>)
LambdaInference.java:16: Note: Deferred instantiation of method <T>foo(Bar<T>)
    Foo.foo((Bar<Boolean>) value -> true).booleanValue(); // Compile error here
           ^
  instantiated signature: (Bar<Boolean>)Boolean
  target-type: <none>
  where T is a type-variable:
    T extends Object declared in method <T>foo(Bar<T>)
LambdaInference.java:16: Note: resolving method booleanValue in type Boolean to candidate 0
    Foo.foo((Bar<Boolean>) value -> true).booleanValue(); // Compile error here
                                         ^
  phase: BASIC
  with actuals: no arguments
  with type-args: no arguments
  candidates:
      #0 applicable method found: booleanValue()

Boolean b = Foo.foo(value -> true)

Это решение обеспечивает явную цель для вызова вашего метода (см. ниже target-type). Это позволяет отложенному-инстанцированию заключить, что параметр типа должен быть Boolean вместо Object (см. Ниже instantiated signature).

LambdaInference.java:16: Note: resolving method foo in type Foo to candidate 0
    Boolean b = Foo.foo(value -> true);
                   ^
  phase: BASIC
  with actuals: <none>
  with type-args: no arguments
  candidates:
      #0 applicable method found: <T>foo(Bar<T>)
        (partially instantiated to: (Bar<Object>)Object)
  where T is a type-variable:
    T extends Object declared in method <T>foo(Bar<T>)
LambdaInference.java:16: Note: Deferred instantiation of method <T>foo(Bar<T>)
    Boolean b = Foo.foo(value -> true);
                       ^
  instantiated signature: (Bar<Boolean>)Boolean
  target-type: Boolean
  where T is a type-variable:
    T extends Object declared in method <T>foo(Bar<T>)

Отказ

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

Это также не полностью объясняет, почему изменение Bar для использования raw Value могло бы устранить эту проблему:

LambdaInference.java:16: Note: resolving method foo in type Foo to candidate 0
    Foo.foo(value -> true).booleanValue();
       ^
  phase: BASIC
  with actuals: <none>
  with type-args: no arguments
  candidates:
      #0 applicable method found: <T>foo(Bar<T>)
        (partially instantiated to: (Bar<Object>)Object)
  where T is a type-variable:
    T extends Object declared in method <T>foo(Bar<T>)
LambdaInference.java:16: Note: Deferred instantiation of method <T>foo(Bar<T>)
    Foo.foo(value -> true).booleanValue();
           ^
  instantiated signature: (Bar<Boolean>)Boolean
  target-type: <none>
  where T is a type-variable:
    T extends Object declared in method <T>foo(Bar<T>)
LambdaInference.java:16: Note: resolving method booleanValue in type Boolean to candidate 0
    Foo.foo(value -> true).booleanValue();
                          ^
  phase: BASIC
  with actuals: no arguments
  with type-args: no arguments
  candidates:
      #0 applicable method found: booleanValue()

По какой-то причине изменение его для использования raw Value позволяет отложенному экземпляру сделать вывод, что T - Boolean. Если бы мне пришлось размышлять, я бы предположил, что, когда компилятор пытается привязать лямбда к Bar<T>, он может сделать вывод, что T есть Boolean, просматривая тело лямбда. Это означает, что мой предыдущий анализ неверен. Компилятор может выполнять вывод типа в теле лямбда, но только для переменных типа, которые появляются только в возвращаемом типе.

Ответ 2

Вывод по типу лямбда-параметра не может зависеть от тела лямбды.

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

    foo( value -> GIBBERISH )

Тип value должен быть выведен первым перед компиляцией GIBBERISH, поскольку в общем случае интерпретация GIBBERISH зависит от определения value.

(В вашем специальном случае GIBBERISH оказывается простой константой, не зависящей от value.)

Javac должен сначала вывести Value<T> для параметра value; нет никаких ограничений в контексте, поэтому T=Object. Затем тело лямбда true компилируется и распознается как булево, совместимое с T.

После внесения изменений в функциональный интерфейс, тип параметра лямбда не требует вывода; T остается неосновным. Затем тело лямбда скомпилируется, и тип возврата выглядит как логический, который устанавливается как нижняя граница для T.


Еще один пример, демонстрирующий проблему

<T> void foo(T v, Function<T,T> f) { ... }

foo("", v->42);  // Error. why can't javac infer T=Object ?

T определяется как String; тело лямбды не принимало участия в выводе.

В этом примере поведение javac кажется нам очень разумным; это, вероятно, предотвратило ошибку программирования. Вы не хотите, чтобы вывод был слишком сильным; если все, что мы пишем, как-то скомпилируем, мы потеряем уверенность в том, что мы нашли ошибки в компиляторе.


Существуют и другие примеры, в которых тело лямбда, как представляется, обеспечивает однозначные ограничения, однако компилятор не может использовать эту информацию. В Java сначала должны быть определены типы параметров лямбда, прежде чем можно будет рассмотреть тело. Это преднамеренное решение. Напротив, С# хочет попробовать разные типы параметров и посмотреть, что делает компиляцию кода. Java считает это слишком рискованным.

В любом случае, когда неявная лямбда не выполняется, что происходит довольно часто, укажите явные типы для лямбда-параметров; в вашем случае (Value<Boolean> value)->true

Ответ 3

Простым способом исправить это объявление типа в вызове метода foo:

Foo.<Boolean>foo(value -> true).booleanValue();

Изменить: я не могу найти конкретную документацию о том, почему это необходимо, почти так же, как и все остальные. Я подозревал, что это может быть из-за примитивных типов, но это было неправильно. Независимо от того, этот синтаксис вызывается с помощью Target Type. Также Тип цели в Lambdas. Однако причины не ускользают от меня, я не могу найти документацию в любом месте, почему этот конкретный случай использования необходим.

Изменить 2: я нашел этот вопрос:

Общий вывод типа не работает с цепочкой методов?

Похоже, это потому, что вы цепляете методы здесь. Согласно комментариям JSR, указанным в принятом ответе, это было преднамеренное упущение функциональности, потому что у компилятора нет способа передать полученную информацию общего типа между цепными вызовами метода в обоих направлениях. В результате весь тип стираемого по времени до вызова booleanValue. Добавление целевого типа удаляет это поведение, предоставляя ограничение вручную, а не позволяя компилятору принять решение, используя правила, изложенные в JLS §18, который, кажется, не упоминает об этом вообще. Это единственная информация, которую я мог бы придумать. Если кто-нибудь найдет что-нибудь лучше, я бы хотел его увидеть.

Ответ 4

Как и другие ответы, я также надеюсь, что кто-то умнее может указать , почему компилятор не может сделать вывод, что T есть Boolean.

Один из способов помочь компилятору сделать правильные вещи, не требуя каких-либо изменений в существующем дизайне класса/интерфейса, заключается в явном объявлении формального типа параметра в вашем выражении лямбда. Итак, в этом случае, явно объявив, что тип параметра value равен Value<Boolean>.

void test() {
  Foo.foo((Value<Boolean> value) -> true).booleanValue();
}

Ответ 5

Я не знаю, почему, но вам нужно добавить отдельный тип возвращаемого значения:

public class HelloWorld{
static class Value<T> {
}

@FunctionalInterface
interface Bar<T,R> {
      R apply(Value<T> value); // Return type added
}

static class Foo {
  public static <T,R> R foo(Bar<T,R> callback) {
      return callback.apply(new Value<T>());
  }
}

void test() {
  System.out.println( Foo.foo(value -> true).booleanValue() ); // No compile error here
}
     public static void main(String []args){
         new HelloWorld().test();
     }
}

какой-нибудь умный парень, вероятно, может это объяснить.

Ответ 6

Проблема

Значение выводит тип Value<Object>, потому что вы неправильно интерпретировали лямбда. Подумайте об этом, как вы называете лямбдой непосредственно методом применения. Итак, что вы делаете:

Boolean apply(Value value);

и это правильно выведено:

Boolean apply(Value<Object> value);

так как вы не указали тип значения.

Простое решение

Вызовите лямбда правильно:

Foo.foo((Value<Boolean> value) -> true).booleanValue();

это будет выведено на:

Boolean apply(Value<Boolean> value);

(My) Рекомендуемое решение

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

Я создал общий интерфейс обратного вызова, общий класс Value и UseClass, чтобы показать, как его использовать.

Интерфейс обратного вызова

/**
 *
 * @param <P> The parameter to call
 * @param <R> The return value you get
 */
@FunctionalInterface
public interface Callback<P, R> {

  public R call(P param);
}

Класс значения

public class Value<T> {

  private final T field;

  public Value(T field) {
    this.field = field;
  }

  public T getField() {
    return field;
  }
}

Использование класса Class

public class UsingClass<T> {

  public T foo(Callback<Value<T>, T> callback, Value<T> value) {
    return callback.call(value);
  }
}

TestApp с основным

public class TestApp {

  public static void main(String[] args) {
    Value<Boolean> boolVal = new Value<>(false);
    Value<String> stringVal = new Value<>("false");

    Callback<Value<Boolean>, Boolean> boolCb = (v) -> v.getField();
    Callback<Value<String>, String> stringCb = (v) -> v.getField();

    UsingClass<Boolean> usingClass = new UsingClass<>();
    boolean val = usingClass.foo(boolCb, boolVal);
    System.out.println("Boolean value: " + val);

    UsingClass<String> usingClass1 = new UsingClass<>();
    String val1 = usingClass1.foo(stringCb, stringVal);
    System.out.println("String value: " + val1);

    // this will give you a clear and understandable compiler error
    //boolean val = usingClass.foo(boolCb, stringVal);
  }
}