Java 8中异常类型推断的一个特殊特征

88

在为这个站点编写另一个答案的代码时,我遇到了这个奇怪的问题:

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调用是可以的。当没有任何未经检查异常类型的提及时,它可能为T推断了什么样的类型呢?

其次,假设这是可行的,那么为什么编译器会抱怨nonSneakyThrow调用呢?它们看起来非常相似。

3个回答

70
sneakyThrow中的T被推断为RuntimeException。可以从类型推断的语言规范中得出这一点(http://docs.oracle.com/javase/specs/jls/se8/html/jls-18.html)。首先,在18.1.3节中有一个注释: throws α形式的限定信息仅仅是指导解析器优化α的实例化,以便如果可能的话,它不是一个已检查的异常类型。这并不影响任何东西,但它指向了解析部分(18.4),其中包含有关推断异常类型的更多信息,其中有一个特殊情况:...否则,如果限定集合包含throws αi,且αi的适当上限最多为ExceptionThrowableObject,则Ti=RuntimeException。这种情况适用于sneakyThrow——唯一的上限是Throwable,因此根据规范,T被推断为RuntimeException,因此它编译通过。方法体是无关紧要的——未经检查的转换在运行时成功,因为它实际上并没有发生,留下了一个可以打败编译时检查异常系统的方法。nonSneakyThrow不会编译,因为该方法的T具有Exception的下限(即T必须是Exception的超类型,或者是Exception本身),这是由于它被调用的类型所致,因此T被推断为Exception

1
@Maksym 你一定是指sneakyThrow调用。有关throws T形式推断的特殊规定在Java 7的规范中并不存在。 - Marko Topolnik
1
小问题:在nonSneakyThrow中,T必须是Exception,而不是Exception的"一个超类型",因为它恰好是在调用时声明的参数的类型。 - llogiq
1
如果我正确地阅读了规范,它的下限是 Exception,上限是 Throwable,因此最小上界(即推断出的结果类型)是 Exception - thecoop
1
@llogiq 注意,参数的类型仅设置了较低的类型界限,因为参数的任何超类型也是可以接受的。 - Marko Topolnik
2
“or Exception itself”这个短语可能对读者有所帮助,但一般来说,应该注意规范总是在“包括自身”的意义上使用“子类型”和“超类型”这些术语... - Holger
@Maksym - 请看我的回答。 - ZhongYu

19
如果类型推断为类型变量产生单个上限,通常会选择上限作为解决方案。例如,如果 T<<Number,则解决方案为 T=Number。尽管 IntegerFloat 等也可以满足约束条件,但没有选择它们而不选择 Number 的好理由。
这也是 java 5-7 中 throws T 的情况: T<<Throwable => T=Throwable。(Sneaky throw solutions 所有都有显式的 <RuntimeException> 类型参数,否则将推断为 <Throwable>。)
在 Java8 中,随着 lambda 的引入,这变得棘手起来。考虑以下情况。
interface Action<T extends Throwable>
{
    void doIt() throws T;
}

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

如果我们使用空lambda调用,T将被推断为什么?
    invoke( ()->{} ); 
T 的唯一限制是上限 Throwable。在 Java 8 的早期阶段,会推断出 T = Throwable。请参见我提交的报告
但是这样做非常愚蠢,从空块中推断出一个受检异常 Throwable。该报告提出了一种解决方案(显然被 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)

即,如果上限是ExceptionThrowable,则选择RuntimeException作为解决方案。在这种情况下,选择上限的特定子类型确实有很好的理由。


你最后一个代码片段中 X:>RuntimeException 是什么意思? - marsouf
@marsouf - X的下限是RuntimeException。 - ZhongYu

1
使用,类型T是有限制的泛型类型变量,没有具体的类型(因为没有任何地方可以确定类型)。
使用,类型T与参数的类型相同,因此在你的示例中,nonSneakyThrow(e);的T是Exception。由于testSneaky()没有声明抛出异常,因此会显示错误信息。
请注意,这是泛型与受检异常之间已知的干扰现象。

那么对于sneakyThrow,它实际上没有被推断为任何特定的类型,而“转换”是到这样一个未定义的类型?我想知道这实际上会发生什么。 - Marko Topolnik

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接