#java #generics #lambda #java-8
#java #общие #лямбда #java-8
Вопрос:
Рассмотрим этот фрагмент кода 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;
}
Лямбда-выражение при вызове to f
компилируется нормально, тогда как лямбда-выражение при вызове to g
не компилируется, а скорее выдает эту ошибку компиляции:
Error:(10, 7) java: call() in <anonymous Generics$> cannot implement call() in CheckedCallable2
overridden method does not throw java.lang.Exception
Почему это так?
Мне кажется, что оба метода CheckedCallable1.call
and CheckedCallable2.call
эквивалентны: по правилам стирания типов V
Object
становится неограниченным и E
становится Exception
, поскольку это верхняя граница типа. Итак, почему компилятор считает, что переопределенный метод не выдает java.lang.Исключение?
Даже если не учитывать стирание типов, которое, вероятно, здесь неуместно, потому что все это происходит во время компиляции, для меня это все равно не имеет смысла: я не вижу причины, по которой этот шаблон, если он разрешен, приведет, скажем, к неправильному Java-коду.
Итак, может кто-нибудь просветить меня относительно того, почему это запрещено?
Обновить:
Итак, я нашел кое-что, возможно, даже более интересное. Возьмите приведенный выше файл, измените каждое вхождение Exception
to IOException
и добавьте предложение throws to main
. Компиляция работает! Измените обратно на Exception
: перерывы компиляции!
Это отлично компилируется:
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…
Комментарии:
1. 1 также за то, что я узнал о поддержке общих типов исключений в java 8
2. Этот вопрос, наконец, побудил меня заставить Java 8 работать на моем ПК. Я только что установил KeplerSR2, jdk1.8.0_05 … и я получаю исключение во время выполнения, а не во время компиляции, в том же месте: в Generics.main(Generics.java: 10), вызванное: java.lang . Ошибка classformatererror: дублирование имени и подписи метода в общих файлах класса $$Lambda $ 2 Что дает?
3. Интересно, что компилятор Eclipse не выдает ошибок. Понятия не имею, почему.
4. При получении действительно странных ошибок я бы не доверял компилятору Eclipse (пока). Попробуйте перепроверить его с другой IDE или компилятором javac. Поддержка Java 8 в Eclipse, похоже, довольно глючит в edgecases.
5. Это попало в средство отслеживания ошибок Java: bugs.java.com/bugdatabase/view_bug.do?bug_id=8047338
Ответ №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()
. Отличаются только исключения, которые мы должны перехватывать. Но, как уже было сказано, это артефакт времени компиляции.
Комментарии:
1. Круто, спасибо, что попробовали это! Я буду сообщать об ошибке в Oracle.