24

有人可以解释这段代码吗?

public class SneakyThrow {


  public static void sneakyThrow(Throwable ex) {
    SneakyThrow.<RuntimeException>sneakyThrowInner(ex);
  }

  private static <T extends Throwable> T sneakyThrowInner(Throwable ex) throws T {
    throw (T) ex;
  }



  public static void main(String[] args) {
    SneakyThrow.sneakyThrow(new Exception());
  }


}

这可能看起来很奇怪,但这不会产生强制转换异常,并且允许抛出已检查的异常而无需在签名中声明它,或者将其包装在未检查的异常中。

请注意,无论是sneakyThrow(...)或 main 都没有声明任何已检查的异常,但输出是:

Exception in thread "main" java.lang.Exception
    at com.xxx.SneakyThrow.main(SneakyThrow.java:20)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:601)
    at com.intellij.rt.execution.application.AppMain.main(AppMain.java:120)

这个 hack 在 Lombok 中使用,带有注解 @SneakyThrow,它允许在不声明的情况下抛出已检查的异常。


我知道它与类型擦除有关,但我不确定是否理解黑客的每个部分。


编辑: 我知道我们可以Integer在 a中插入一个List<String>检查/未检查异常的区别是编译时功能。

当从非泛型类型List转换为泛型类型List<XXX>时,编译器会产生警告。(T) ex但是像上面的代码那样直接转换为泛型类型不太常见。

如果您愿意,对我来说似乎很奇怪的部分是,我理解在 JVM 内部 aList<Dog>List<Cat>看起来一样,但上面的代码似乎意味着最终我们也可以将 Cat 类型的值赋给 Dog 类型的变量或类似的东西。

4

4 回答 4

19

如果你编译它,-Xlint你会得到一个警告:

c:\Users\Jon\Test>javac -Xlint SneakyThrow.java
SneakyThrow.java:9: warning: [unchecked] unchecked cast
    throw (T) ex;
              ^
  required: T
  found:    Throwable
  where T is a type-variable:
    T extends Throwable declared in method <T>sneakyThrowInner(Throwable)
1 warning

这基本上是说“在执行时并没有真正检查这个演员”(由于类型擦除) - 所以编译器不情愿地假设你在做正确的事情,因为它知道它实际上不会被检查。

现在只有编译器关心已检查和未检查的异常——它根本不是 JVM 的一部分。所以一旦你通过了编译器,你就可以回家了。

我强烈建议您避免这样做。

在许多情况下,当您使用泛型时会进行“真正的”检查,因为某些东西使用了所需的类型——但情况并非总是如此。例如:

List<String> strings = new ArrayList<String>();
List raw = strings;
raw.add(new Object()); // Haha! I've put a non-String in a List<String>!
Object x = strings.get(0); // This doesn't need a cast, so no exception...
于 2012-12-26T09:45:40.333 回答
9

上面的代码似乎意味着最终我们还可以将 Cat 类型的值赋给 Dog 类型的变量或类似的东西。

您必须考虑类的结构。T extends Throwable你正在传递Exception给它。这就像分配DogAnimalnot Dogto Cat

基于继承,编译器有关于检查哪些 Throwable 和哪些不检查的规则。这些是在编译时应用的,并且可能会混淆编译器以允许您抛出检查异常。在运行时,这没有影响。


检查异常是编译时特性(如泛型)

BTW Throwable 也是一个检查异常。如果您对它进行子类化,它将被检查,除非它是 Error 或 RuntimeException 的子类。

另外两种在编译器不知道您正在执行此操作的情况下引发检查异常的方法。

Thread.currentThread().stop(throwable);

Unsafe.getUnsafe().throwException(throwable);

唯一的区别是两者都使用本机代码。

于 2012-12-26T10:07:44.690 回答
7

从 Java 8 开始,sneakyThrowInner不再需要辅助方法。 sneakyThrow可以写成:

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

请参阅“ Java 8 中异常类型推断的特殊特性”一文。

sneakyThrow 的 T 被推断为 RuntimeException。这可以从关于类型推断的语言规范(http://docs.oracle.com/javase/specs/jls/se8/html/jls-18.html)中遵循

注意:该sneakyThrow函数声明为 return RuntimeException,因此可以按如下方式使用:

int example() { 
   if (a_problem_occurred) {
      throw sneakyThrow(new IOException("An I/O exception occurred"));
      // Without the throw here, we'd need a return statement!
   } else {
      return 42;
   }
}

通过抛出RuntimeException返回的 by sneakyThrow,Java 编译器知道该执行路径终止。(当然,sneakyThrow它本身不会返回。)

于 2016-04-12T21:30:14.640 回答
0

让我们看看下面的代码:

public class SneakyThrow {


  public static void sneakyThrow(Throwable ex) {
    SneakyThrow.<RuntimeException>sneakyThrowInner(ex);
  }

  private static <T extends Throwable> T sneakyThrowInner(Throwable ex) throws T {
    throw (T) ex;
  }



  public static void main(String[] args) {
    SneakyThrow.sneakyThrow(new Exception());
  }


}

让我们看看它为什么会这样。

首先,任何转换为​​编译器忽略的类型参数。JVM会直接转换它。因此,throw (T) ex;编译器不会检查类型安全性。但是,当代码到达 JVM 时,就会发生类型擦除。因此代码将类似于: [注意:实际代码将是字节码。下面只是解释类型擦除的作用。]

public class SneakyThrow {


    public static void sneakyThrow(Throwable ex) {
        SneakyThrow.sneakyThrowInner(ex); // Note : Throwable is checked exception but we don't mention anything here for it
    }

    private static  Throwable sneakyThrowInner(Throwable ex) throws Throwable {
        throw (Throwable) ex;
    }


    public static void main(String[] args) {
        SneakyThrow.sneakyThrow(new Exception());

    }


}

首先要注意的是每件事都会顺利进行,并且不会抛出 ClassCastException,因为 JVM 很容易将类型ex转换为Throwable.

这里要注意的第二件事是编译器总是强迫我们捕获或传递收到的检查异常。JVM 不进行此类检查。在这里,在我们类型转换之后,理想情况下,SneakyThrow.sneakyThrowInner(ex);应该强制处理Throwable异常,但请记住,这个版本的字节码到达 JVM。因此,我们以某种方式欺骗了编译器。

这样做:

public class SneakyThrow {


    public static void sneakyThrow(Throwable ex) {
        SneakyThrow.<RuntimeException>sneakyThrowInner(ex);
    }

    private static <T extends Throwable> T sneakyThrowInner(Throwable ex) throws T {
        throw (T) ex;
    }


    public static void main(String[] args) {
        try {
            SneakyThrow.sneakyThrow(new Exception());
        }catch (Throwable ex){
            System.out.println("Done Succesfully"); // Added to show everything runs fine
        }
    }


}

输出:成功完成

于 2018-07-22T14:06:30.467 回答