Java8中异常类型推断的一个特殊特性

当我在这个网站上为另一个答案编写代码时,我发现了一个特点:

static void testSneaky() {
final Exception e = new Exception();
sneakyThrow(e);    //no problems here
nonSneakyThrow(e); //ERRROR: Unhandled exception: java.lang.Exception
}


@SuppressWarnings("unchecked")
static <T extends Throwable> void sneakyThrow(Throwable t) throws T {
throw (T) t;
}


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

首先,我很困惑为什么对编译器的 sneakyThrow调用是 OK 的。当没有提到任何未检查的异常类型时,它为 T推断出了什么可能的类型?

第二,接受这个工作原理,那么为什么编译器要在 nonSneakyThrow调用中抱怨呢。

6469 次浏览

With sneakyThrow, the type T is a bounded generic type variable without a specific type (because there is no where the type could come from).

With nonSneakyThrow, the type T is the same type as the argument, thus in your example, the T of nonSneakyThrow(e); is Exception. As testSneaky() does not declare a thrown Exception, an error is shown.

Note that this is a known interference of Generics with checked exceptions.

The T of sneakyThrow is inferred to be RuntimeException. This can be followed from the langauge spec on type inference (http://docs.oracle.com/javase/specs/jls/se8/html/jls-18.html)

Firstly, there's a note in section 18.1.3:

A bound of the form throws α is purely informational: it directs resolution to optimize the instantiation of α so that, if possible, it is not a checked exception type.

This doesn't affect anything, but it points us to the Resolution section (18.4), which has got more information on inferred exception types with a special case:

... Otherwise, if the bound set contains throws αi, and the proper upper bounds of αi are, at most, Exception, Throwable, and Object, then Ti = RuntimeException.

This case applies to sneakyThrow - the only upper bound is Throwable, so T is inferred to be RuntimeException as per the spec, so it compiles. The body of the method is immaterial - the unchecked cast succeeds at runtime because it doesn't actually happen, leaving a method that can defeat the compile-time checked exception system.

nonSneakyThrow does not compile as that method's T has got a lower bound of Exception (ie T must be a supertype of Exception, or Exception itself), which is a checked exception, due to the type it's being called with, so that T gets inferred as Exception.

If type inference produces a single upper bound for a type variable, typically the upper bound is chosen as the solution. For example, if T<<Number, the solution is T=Number. Although Integer, Float etc. could also satisfy the constraint, there's no good reason to choose them over Number.

That was also the case for throws T in java 5-7: T<<Throwable => T=Throwable. (Sneaky throw solutions all had explicit <RuntimeException> type arguments, otherwise <Throwable> is inferred.)

In java8, with the introduction of lambda, this becomes problematic. Consider this case

interface Action<T extends Throwable>
{
void doIt() throws T;
}


<T extends Throwable> void invoke(Action<T> action) throws T
{
action.doIt(); // throws T
}

If we invoke with an empty lambda, what would T be inferred as?

    invoke( ()->{} );

The only constraint on T is an upper bound Throwable. In earlier stage of java8, T=Throwable would be inferred. See this report I filed.

But that is pretty silly, to infer Throwable, a checked exception, out of an empty block. A solution was proposed in the report (which is apparently adopted by JLS) -

If E has not been inferred from previous steps, and E is in the throw clause,
and E has an upper constraint E<<X,
if X:>RuntimeException, infer E=RuntimeException
otherwise, infer E=X. (X is an Error or a checked exception)

i.e. if the upper bound is Exception or Throwable, choose RuntimeException as the solution. In this case, there is a good reason to choose a particular subtype of the upper bound.