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

Lambdas и функциональные интерфейсы с общими предложениями throw

Рассмотрим этот фрагмент кода java 8:

public class Generics {
  public static <V, E extends Exception> V f(CheckedCallable1<V, E> callable) throws E {
    return callable.call();
  }
  public static <V, E extends Exception> V g(CheckedCallable2<V, E> callable) throws E {
    return callable.call();
  }
  public static void main(String[] args) {
    f(() -> 1);
    g(() -> 1);
  }
}

interface Callable<V> {
  V call() throws Exception;
}

interface CheckedCallable1<V, E extends Exception> {
  V call() throws E;
}

interface CheckedCallable2<V, E extends Exception> extends Callable<V> {
  @Override V call() throws E;
}

Лямбда при вызове f компилируется отлично, тогда как лямбда при вызове g не компилируется, а скорее дает эту ошибку компиляции:

Error:(10, 7) java: call() in <anonymous Generics$> cannot implement call() in CheckedCallable2
  overridden method does not throw java.lang.Exception

Почему это?

Мне кажется, что оба метода CheckedCallable1.call и CheckedCallable2.call эквивалентны: по правилам стирания типа V становится Object, поскольку он неограничен, а E становится Exception, as что верхний тип связан. Итак, почему компилятор считает, что переопределенный метод не бросает java.lang.Exception?

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

Так кто-нибудь может рассказать мне, почему это не разрешено?

Update:

Итак, я нашел кое-что, что может быть еще интереснее. Возьмите вышеупомянутый файл, измените каждое вхождение Exception на IOException и добавьте предложение throw к main. Компиляция работает! Вернитесь к Exception: скомпилируйте breaks!

Это компилируется отлично:

import java.io.IOException;

public class Generics {
  public static <V, E extends IOException> V f(CheckedCallable1<V, E> callable) throws E {
    return callable.call();
  }
  public static <V, E extends IOException> V g(CheckedCallable2<V, E> callable) throws E {
    return callable.call();
  }
  public static void main(String[] args) throws IOException {
    f(() -> 1);
    g(() -> 1);
  }
}

interface Callable<V> {
  V call() throws IOException;
}

interface CheckedCallable1<V, E extends IOException> {
  V call() throws E;
}

interface CheckedCallable2<V, E extends IOException> extends Callable<V> {
  @Override V call() throws E;
}

В этот момент он начинает все больше выглядеть как ошибка java...

4b9b3361

Ответ 1

Я не думаю, что существует правило, запрещающее этот шаблон. Скорее всего, вы обнаружили ошибку компилятора.

Легко показать, что этот шаблон не приводит к неправильному коду, просто записав эквивалентный внутренний код класса g(() -> 1);:

g(new CheckedCallable2<Integer, RuntimeException>() {
    public Integer call() {
        return 1;
    }
});

Это компилируется и запускается без каких-либо проблем даже под Java 6 (я предполагаю, что он даже будет работать на Java 5, но у меня не было JDK для его проверки), и нет причин, по которым он не должен работать, если делать то же самое с лямбдой, Записывая этот код в результатах Netbeans даже в рекомендации, чтобы преобразовать его в лямбда.

Существует также ограничение времени исполнения, которое запрещало бы такую ​​конструкцию. Помимо того факта, что при капоте нет правил исключения, и все зависит от проверок времени компиляции, мы можем даже доказать, что это сработает, если компилятор примет наш код, создав код, который будет создан вручную компилятором:

CheckedCallable2<Integer,RuntimeException> c;
try
{
  MethodHandles.Lookup l = MethodHandles.lookup();
  c=(CheckedCallable2)
    LambdaMetafactory.metafactory(l, "call",
      MethodType.methodType(CheckedCallable2.class),
      MethodType.methodType(Object.class),
      l.findStatic(Generics.class, "lambda$1", MethodType.methodType(int.class)),
      MethodType.methodType(Integer.class)).getTarget().invokeExact();
} catch(Throwable t) { throw new AssertionError(t); }
int i=g(c);
System.out.println(i);
// verify that the inheritance is sound:
Callable<Integer> x=c;
try { System.out.println(x.call()); }// throws Exception
catch(Exception ex) { throw new AssertionError(ex); }

…
static int lambda$1() { return 1; }// the synthetic method for ()->1

Этот код запускается и создает 1, как и ожидалось, независимо от того, какой interface мы используем для call(). Только исключения, которые мы должны улавливать, различаются. Но, как сказано, это артефакт времени компиляции.