7

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
2
  • 2
    Your code compiles for me in Eclipse, even the following compiled: ConnectionPool.call(RuntimeException.class, Test1::mayThrow1); ConnectionPool.call(Checked.class, Test1::mayThrow2); with no type hints. What compiler are you using? Commented Nov 3, 2017 at 0:30
  • @Pedro javac 1.8.0_152 (Oracle JDK) Commented Nov 3, 2017 at 14:26

3 Answers 3

6
+500

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.

Sign up to request clarification or add additional context in comments.

1 Comment

The JLS section on Resolution (18.4) might help explain this behavior too.
1

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.

2 Comments

Strange. I get the same thing. Eclipse compiles and runs the code just fine. But if I try to compile manually by calling javac or using gradle, I get a compilation error.
same here linux 1.8.0.152 compiles run fine on eclipse
0

When you see public static <T, E extends Exception> T call(Class<E> exceptionClass, SQLExceptionThrowingFunction<Connection, T, E> f) throws E that tells :

  • type of E in Class<E> (your first argument, exceptionClass) and
  • type of E 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); to
  • ConnectionPool.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.

13 Comments

This. E cannot stand for RuntimeException and SQLException at the same time. Without the type hints it works fine (but will not throw RuntimeException nor Checked, as these types are different from E).
Exactly @PetrJaneček and that was what I meant in explanation from part1. And telling OP to use "Exception" as Exception and SQLException are same E.
Also I cannot read properly, now I finally see what he's trying to achieve. Yeah, that's probably not possible without explicit generic "hints" (that are wrong, but will work because of erasure). The safest bet for him is to simply declare throws Exception or throws MyWrapperException that he'll wrap around whatever the inner function throws (except SQL exception, obviously).
How did you arrive at the conclusion "Hence the E ( i.e SQLException) in SQLExceptionThrowingFunction " - where does it say that E is an SQLException? E is just a generic parameter in the definition of interface SQLExceptionThrowingFunction.
@DodgyCodeException This is where he is "calling" the call function i.e here ConnectionPool.call(RuntimeException.class, Test::<SQLException>mayThrow1)
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.