Reputation: 36439
I'm trying to have a functor F which may throw multiple exceptions (in the example below Checked and SQLException). I want to be able to call a function with F as an argument, such that whatever checked exceptions F throws (except SQLException which would be handled internally) get rethrown.
import java.sql.Connection;
import java.sql.SQLException;
class Checked extends Exception {
public Checked() {
super();
}
}
@FunctionalInterface
interface SQLExceptionThrowingFunction<T, U, E extends Exception> {
U apply(T t) throws E, SQLException;
}
class ConnectionPool {
public static <T, E extends Exception> T call(Class<E> exceptionClass, SQLExceptionThrowingFunction<Connection, T, E> f) throws E {
throw new UnsupportedOperationException("unimportant");
}
}
class Test {
static Void mayThrow0(Connection c) {
throw new UnsupportedOperationException("unimportant");
}
static <E extends Exception> Void mayThrow1(Connection c) throws E {
throw new UnsupportedOperationException("unimportant");
}
static <E1 extends Exception, E2 extends Exception> Void mayThrow2(Connection c) throws E1, E2 {
throw new UnsupportedOperationException("unimportant");
}
public static void main(String[] args) throws Exception {
// Intended code, but doesn't compile
ConnectionPool.call(RuntimeException.class, Test::<SQLException>mayThrow1);
ConnectionPool.call(Checked.class, Test::<Checked, SQLException>mayThrow2);
// Type inference works if the function doesn't actually throw SQLException (doesn't help me)
ConnectionPool.call(RuntimeException.class, Test::mayThrow0);
ConnectionPool.call(Checked.class, Test::<Checked>mayThrow1);
// Can workaround by manually specifying the type parameters to ConnectionPool.call (but is tedious)
ConnectionPool.<Void, RuntimeException>call(RuntimeException.class, Test::<SQLException>mayThrow1);
ConnectionPool.<Void, Checked>call(Checked.class, Test::<Checked, SQLException>mayThrow2);
}
}
Intuitively, I would expect the above example to compile but it doesn't. Is there a way to get this to work, or is the workaround of specifying the type arguments the only way? The compile error is:
Test.java:34: error: incompatible types: inference variable E has incompatible bounds
ConnectionPool.call(RuntimeException.class, Test::<SQLException>mayThrow1); // doesn't compile
^
equality constraints: RuntimeException
lower bounds: SQLException
where E,T are type-variables:
E extends Exception declared in method <T,E>call(Class<E>,SQLExceptionThrowingFunction<Connection,T,E>)
T extends Object declared in method <T,E>call(Class<E>,SQLExceptionThrowingFunction<Connection,T,E>)
Test.java:35: error: incompatible types: inference variable E has incompatible bounds
ConnectionPool.call(Checked.class, Test::<Checked, SQLException>mayThrow2); // doesn't compile
^
equality constraints: Checked
lower bounds: SQLException,Checked
where E,T are type-variables:
E extends Exception declared in method <T,E>call(Class<E>,SQLExceptionThrowingFunction<Connection,T,E>)
T extends Object declared in method <T,E>call(Class<E>,SQLExceptionThrowingFunction<Connection,T,E>)
2 errors
Upvotes: 7
Views: 539
Reputation: 11621
There is a strange peculiarity of the Java parser (in jdk 1.8u152 and 9.0.1, but not the compiler built into Eclipse) so when you have
@FunctionalInterface
interface SQLExceptionThrowingFunction<T, U, E extends Exception> {
U apply(T t) throws E, SQLException;
}
and you pass Test::<SQLException>mayThrow1
it binds E to SQLException when it creates an instance of the interface.
You can make it not do that by simply swapping the declared exceptions in the interface, i.e. just do
@FunctionalInterface
interface SQLExceptionThrowingFunction<T, U, E extends Exception> {
U apply(T t) throws SQLException, E;
}
and then it compiles!
The relevant part of the JLS is section 18.2.5. But I can't see where it explains the above behaviour.
Upvotes: 6
Reputation: 4507
When you see public static <T, E extends Exception> T call(Class<E> exceptionClass, SQLExceptionThrowingFunction<Connection, T, E> f) throws E
that tells :
E
in Class<E>
(your first argument, exceptionClass) andE
in SQLExceptionThrowingFunction<Connection, T, E> f) throws E
shall be of same type/subtype.
Hence the E
( i.e SQLException
) in SQLExceptionThrowingFunction
is expected to be of subtype of E
(exceptionClass), which is passed as RuntimeException
). (this happens when you call ConnectionPool.call(RuntimeException.class, Test::<SQLException>mayThrow1);
Since this expectation fails, you get compilation error.
You can validate this by changing...
ConnectionPool.call(RuntimeException.class, Test::<SQLException>mayThrow1);
toConnectionPool.call(
Exception.class, fitest.Test::<SQLException>mayThrow1);
which will remove the error on that line.Not sure if that is what your intention is initially.
1: What you can do to use generic stuff (if you don't care about declaring exceptions is change call method as below and then all your code will work.
public static <T> T call2(Class exceptionClass, SQLExceptionThrowingFunction<Connection,T, Exception> f)
{
throw new UnsupportedOperationException("unimportant");
}
2: Or you can just call like without defining the type. e.g
ConnectionPool.call(RuntimeException.class, Test::mayThrow0);
ConnectionPool.call(Checked.class, Test::mayThrow1);
I am not sure if that solves your question. If you have different intention, when you said Is there a way to get this to work, or is the workaround of specifying the type arguments the only way
what actually you wanted` then please share the pseduo syntax how would you like stuff to work.
Upvotes: 0
Reputation: 1072
Sorry for my comment, it didn't actually compile, but it somehow ran on Eclipse. I think the compilation error is actually expected. The signature of the call method is:
public static <T, E extends Exception> T call(Class<E> exceptionClass, SQLExceptionThrowingFunction<Connection, T, E> f) throws E
and you are using it as:
ConnectionPool.call(RuntimeException.class, Test::<SQLException>mayThrow1);
By the signature of the method, the class of the first parameter(RuntimeException) must match the generic of mayThrow1(SQLException), since they are both E in the signature.
Upvotes: 1