Greg Rogers
Greg Rogers

Reputation: 36439

Java type inference of generic exception type

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

Answers (3)

Klitos Kyriacou
Klitos Kyriacou

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

Optional
Optional

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 :

  • 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.

Upvotes: 0

Pedro
Pedro

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

Related Questions