Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Lamdas that bypass try/catch blocks for checked exceptions

As a result of me trying to extract some common wrapping lambda routines that I use in most of my projects, I've been able to create CheckedFunction, subclassed by PermeableFunction FunctionalInterface that bypasses the need of try/catch blocks. I've tested that on Oracle jdks for windows(v1.8.0_251)/linux(v1.8.0_261) and several other online compilers(not sure which implementation is used there).

Wasn't sure if this actually violates the specification or is something allowed by the standard... According to my interpretaion of the docs this should not be possible:

More precisely, suppose that B is a class or interface, and A is a superclass or superinterface of B, and a method declaration n in B overrides or hides a method declaration m in A. Then:

  • If n has a throws clause that mentions any checked exception types, then m must have a throws clause, or a compile-time error occurs.
  • For every checked exception type listed in the throws clause of n, that same exception class or one of its supertypes must occur in the erasure (§4.6) of the throws clause of m; otherwise, a compile-time error occurs.
  • If the unerased throws clause of m does not contain a supertype of each exception type in the throws clause of n, a compile-time unchecked warning occurs.

Here is the sample code that I've used:

import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.function.Function;

public class Main {

    public static void main(String[] args) {

        PermeableFunction<Path, Long> function = PermeableFunction.from(Files::size);

        Path doesNotExist = Paths.get("/does/not/exist");

        // function.apply(doesNotExist); -> throws WrappedException
        function.applyChecked(doesNotExist); // throws NoSuchFileException without the need of a try/catch block!
    }
}

interface PermeableFunction<T,R> extends CheckedFunction<T, R, RuntimeException> {

    static <T, R> PermeableFunction<T, R> from(WrappedFunction<T, R> wrappedFunction) {

        return CheckedFunction.<T,R, RuntimeException>from(wrappedFunction)::applyChecked;
    }
}

interface CheckedFunction<T, R, E extends Exception> extends WrappedFunction<T, R> {

    @Override
    R applyChecked(T t) throws E;

    static <T, R, E extends Exception> CheckedFunction<T, R, E> from(WrappedFunction<T, R> wrappedFunction) {

        return wrappedFunction::applyChecked;
    }
}

interface WrappedFunction<T, R> extends Function<T, R> {

    R applyChecked(T t) throws Exception;

    @Override
    default R apply(T t) {

        try {

            return applyChecked(t);

        } catch (Exception e) {

            throw new WrappedException(e);
        }
    }
}

class WrappedException extends RuntimeException {

    public WrappedException(Throwable cause) {
        super(cause);
    }
}

CheckedFunction also allows shadowing of the throwable like so:

enter image description here

So here is my question:

Is this something that should be reported to the implementer(s) or it's a general issue imposed by the standard?

like image 273
dbl Avatar asked Aug 13 '20 09:08

dbl


1 Answers

Your method

static <T, R, E extends Exception> CheckedFunction<T, R, E> from(WrappedFunction<T, R> wrappedFunction) {
    return wrappedFunction::applyChecked;
}

was rejected by my Eclipse version, as well as javac of all JDKs from 9 to 14. Only JDK 8 accepted it, so it’s a bug, but not worth reporting, as newer versions do not have it.

That said, subverting the exception checking via the generic type system is possible.

When you change the method to

static <T, R, E extends Exception> CheckedFunction<T, R, E> from(WrappedFunction<T, R> wrappedFunction) {
    return (CheckedFunction)(CheckedFunction<T, R, Exception>)wrappedFunction::applyChecked;
}

all compilers will accept it, but produce an “unchecked” warning. This is a known thing.

You can simplify the example to:

public class Main {
    public static void main(String[] args) {
        CheckedFunction<Path, Long, RuntimeException> function = (CheckedFunction)
            (CheckedFunction<Path, Long, IOException>)Files::size;
        Path doesNotExist = Paths.get("/does/not/exist");

        function.applyChecked(doesNotExist); // throws NoSuchFileException without the need of a try/catch block!
    }

    interface CheckedFunction<T, R, E extends Exception> {
        R applyChecked(T t) throws E;
    }
}

There are several variations possible, even without lambda expressions. All it needs, is a throws declaration using a type parameter and an unchecked operation regarding this type parameter.

E.g.

public class Main {
    public static void main(String[] args) {
        try {
            Files.size(Paths.get("/does/not/exist"));
        }
        catch(IOException ex) {
            doThrow(ex); // throws undeclared IOException
        }
    }

    static <T extends Throwable> void doThrow(Throwable t) throws T {
        throw (T)t;
    }
}

As said, this is known and the takeaway is, you should never ignore “unchecked” warnings.

like image 149
Holger Avatar answered Oct 11 '22 10:10

Holger