Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Lambdas and functional interfaces with generic throw clauses

Consider this snippet of java 8 code:

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

The lambda at the call to f compiles fine, whereas the lambda at the call to g does not compile, but rather gives this compile error:

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

Why is this?

It seems to me that both the CheckedCallable1.call and CheckedCallable2.call methods are equivalent: by the rules of type erasure, V becomes Object as it is unbounded, and E becomes Exception, as that's the upper type bound. So why does the compiler think the overridden method does not throw java.lang.Exception?

Even disregarding type erasure, which is likely not relevant here because this is all happening at compile time, it still does not make sense to me: I don't see a reason why this pattern, if allowed, would result in, say, unsound java code.

So can someone enlighten me as to why this isn't allowed?

Update:

So I found something that's maybe even more interesting. Take the above file, change each occurrence of Exception to IOException and add throws clause to main. Compile works! Change back to Exception: compile breaks!

This compiles fine:

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

At this point it's starting to look more and more like a java bug...

like image 273
Tobi Avatar asked Jun 13 '14 06:06

Tobi


People also ask

How does lambda expression and functional interface work together?

A lambda expression (lambda) is a short-form replacement for an anonymous class. Lambdas simplify the use of interfaces that declare single abstract methods. Such interfaces are known as functional interfaces. A functional interface can define as many default and static methods as it requires.

What is lambda and functional interface?

A functional interface is an interface that contains only one abstract method. They can have only one functionality to exhibit. From Java 8 onwards, lambda expressions can be used to represent the instance of a functional interface. A functional interface can have any number of default methods.

What is generic functional interface?

A lambda expression can't specify type parameters, so it's not generic. However, a functional interface associated with lambda expression is generic. In this case, the target type of lambda expression has determined by the type of argument(s) specified when a functional interface reference is declared.

Can functional interface throw exception?

A lambda expression cannot throw any checked exception until its corresponding functional interface declares a throws clause. An exception thrown by any lambda expression can be of the same type or sub-type of the exception declared in the throws clause of its functional interface.


1 Answers

I don’t think that there is a rule forbidding this pattern. It’s rather likely you found a compiler bug.

It’s easy to show that this pattern does not result in unsound code by just writing down the equivalent inner class code of g(() -> 1);:

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

This compiles and runs without any problems, even under Java 6 (I assume it would even run on Java 5 but I had no JDK to test it) and there is no reason why it shouldn’t work when doing the same with a lambda. Writing down this code in Netbeans results even in the recommendation to convert it to a lambda.

There is also no runtime restriction which would forbid such a construct. Besides the fact that under the hood there are no exception rules enforced and everything relies on the compile-time checks, we can even prove that it would work if the compiler accepted our code by creating the code manually that the compiler would create:

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

This code runs and produces 1 as expected, regardless of which interface we use to call(). Only the exceptions we have to catch differ. But as said, that’s a compile-time artifact.

like image 164
Holger Avatar answered Sep 28 '22 10:09

Holger