We have code similar to the following:
import java.io.Closeable;
public class Test {
@FunctionalInterface
public interface Action1<E extends Exception> {
void run() throws E;
}
public interface Action2 {
void run();
}
static class MyClass implements Action1<Exception>, Action2 {
@Override
public void run() {
}
}
public static <C extends Action1<Exception> & Action2> C emptyAction() {
return (C) new MyClass();
}
public static void main(String[] args) throws Exception {
// Compiler says: "error: incompatible types: MyClass cannot be converted to Closeable"
// Closeable onClose1 = new MyClass();
// onClose1.close();
// Compiles but ClassCastException at runtime:
// Exception in thread "main" java.lang.ClassCastException: class Test$MyClass cannot be cast to class java.io.Closeable (Test$MyClass is in unnamed module of loader // com.sun.tools.javac.launcher.Main$MemoryClassLoader @62fdb4a6; java.io.Closeable is in module java.base of loader 'bootstrap')
// at Test.main(Test.java:27)
Closeable onClose2 = emptyAction();
onClose2.close();
}
}
Notice how directly calling the constructor is correctly rejected by the compiler (onClose1
), while the method return type is not rejected, but then proceeds to throw a ClassCastException
at runtime (onClose2
). I tested with both Java 11 and Java 14 and got the same result both times.
I don't understand why the compiler accepts to compile onClose2
? The generic type signature does not indicate that the returned type is indeed Closeable
, it is not creating a lambda either which would act as a bridge. I even wondered about erasure, but I don't see which role it could have here...
Does anybody have any explanation?
First of all: you are doing strange things with interface. I strongly recommend not to have interfaces with equal method names and return types only differing in throws-statement. Default methods and order of implements-statements create a degree of freedom. Your contract expressed with the help of these two interfaces is not reliable anymore.
It also wouldn't be necessary for MyClass to implement the @FunctionInterface
. This what lambdas are for: auto checking.
You should try to understand two issues of your code:
I don't think that your code is meant to run at all – you already pointed out the lack of implements Closeable
. You want to understand what lambdas and generics mean. This is absolutely beyond the possibilities of a short answer but I can give you two important hints:
List<Number>
will never accept a Long
. And a List<? extends Number>
doesn't accept anything at all.So your C and MyClass don't share anything except the same characters in the type arguments. Now the method suggests, any assignment will propagate backward to MyClass, like your attempt with Closeable. What you meant instead was:
public interface SuperInterface extends Action1, Action2 {
}
public static SuperInterface emptyAction() {
return new MyClass();
}
Generics can't go beyond classic interfaces. They only offer a more detailed expression like what a List
really contains. Never try to express inheritance with generics. That's why you can't collect factory classes in a Map<String,Factory<T>>
with being a flexible friend depending on the assignment.
Angelika Langer has a great set of articles and resources regarding generics and why they don't work as you expect. I don't link any particular page and this is more a journey than a read.
And for lambdas try to ingest the Java Language Specification. I wrote the relevant paragraph in #2. There is no Lambda-expression at all. It also won't fix your code. Lambdas and Exceptions don't work together. There is no Exception-path in a Lambda-expression except for the local/ smallest context where the exception is to be thrown.