Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Java type inference of generic exception type

Tags:

java

generics

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
like image 604
Greg Rogers Avatar asked Nov 02 '17 20:11

Greg Rogers


2 Answers

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.

like image 105
Klitos Kyriacou Avatar answered Oct 05 '22 08:10

Klitos Kyriacou


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.

like image 40
Pedro Avatar answered Oct 05 '22 10:10

Pedro