13

总是有人告诉我,Java 异常处理非常昂贵。

我在问在程序开始时创建特定类型的异常实例而不创建新实例是否是一种好习惯,总是抛出相同的异常对象。

我只是想举个例子。常用代码:

if (!checkSomething(myObject))
   throw new CustomException("your object is invalid");

选择:

static CustomException MYEXP = new CustomException("your object is invalid");

//somewhere else
if (!checkSomething(myObject))
    throw MYEXP;

当然,我在这里做了一些假设:

  1. MyCustomException没有参数
  2. 客户端代码,无论何时是一个好的实践,都很大程度上基于异常处理,重构不是一种选择。

所以问题是:

  1. 这是一个好习惯吗?
  2. 这会损坏一些 JVM 机制吗?
  3. 如果 1 是肯定的,是否有可能获得性能提升?(我认为不是,但不确定)
  4. 如果 1 和 3 是肯定的,为什么不作为实践赞助?
  5. 如果 1 不是,为什么 Martin Odersky 在他对 Scala 的介绍中说 Scala 在某些情况下是这样工作的?(在 28.30 分钟,他告诉执行 break 时抛出了异常,观众说这很耗时,他回答说不是每次都创建异常)Fosdem 2009

我希望这不是一个无聊/愚蠢的问题,我对此很好奇。我认为异常处理的真正成本是处理而不是创建。

编辑 添加了关于 FOSDEM 演示文稿的精确讨论的参考

免责声明:我的代码都没有像提议的那样工作,我也无意管理这样的异常,我只是在做一个“假设”问题,这种好奇心是从该视频的确认中产生的。我想:如果它是在 Scala 中完成的,为什么不在 Java 中呢?

4

4 回答 4

18

不,不要那样做。昂贵的部分不是处理异常,而是生成堆栈跟踪。不幸的是,堆栈跟踪也是有用的部分。如果你抛出一个保存的异常,你将传递一个误导性的堆栈跟踪。

可能在 Scala 的实现中,在某些情况下这样做是有意义的。(也许他们正在做一些递归的事情,并且想要预先生成一个异常对象,所以如果他们用完内存,他们仍然可以产生一个异常。)他们也有很多关于他们正在做什么的信息,所以他们有更好的机会做对了。但是 JVM 语言实现者所做的优化是一个非常特殊的情况。

所以你不会破坏任何东西,除非你认为提供误导性信息构成破坏。这对我来说似乎是一个很大的风险。

尝试 Thomas Eding 关于如何创建没有堆栈跟踪的异常的建议似乎有效:

groovy:000> class MyException extends Exception {
groovy:001>     public Throwable fillInStackTrace() {}}
===> true
groovy:000> e = new MyException()
===> MyException
groovy:000> Arrays.asList(e.stackTrace)
===> []

另请查看JLS

由方法 blowUp 引发的 NullPointerException(这是一种 RuntimeException)不会被 main 中的 try 语句捕获,因为 NullPointerException 不能分配给 BlewIt 类型的变量。这会导致 finally 子句执行,之后执行 main 的线程(测试程序的唯一线程)由于未捕获的异常而终止,这通常会导致打印异常名称和简单的回溯。但是,本规范不需要回溯。

强制回溯的问题在于,可以在程序中的某一点创建异常并在以后抛出异常。将堆栈跟踪存储在异常中是非常昂贵的,除非它实际上被抛出(在这种情况下,可能会在展开堆栈时生成跟踪)。因此,我们不要求在每个异常中都进行回溯。

于 2013-01-23T21:28:45.240 回答
3

Q1。这是一个好习惯吗?

我的书中没有。它增加了复杂性并阻碍了诊断(请参阅我对 Q2 的回答)。

Q2。这会损坏一些 JVM 机制吗?

您不会从这样的异常对象中获得有意义的堆栈跟踪。

Q3。如果1是yes,有性能增益吗?(我认为不是,但不确定)

Q4。如果 1 和 3 是肯定的,为什么不作为实践赞助?

由于上述问题。

Q5。如果 1 不是,为什么 Martin Odersky 在他对 Scala 的介绍中说 Scala 在某些情况下是这样工作的?(对不起,我现在不记得这个肯定的上下文)Fosdem 2009

没有上下文很难回答。

于 2013-01-23T21:30:38.557 回答
3

你可以这样做,但例外

  1. 必须没有堆栈跟踪,因为初始堆栈跟踪只会在后续使用中混淆。

  2. 不能接受被抑制的异常。如果多个线程试图向它添加抑制的异常,事情就会被破坏。

所以你的异常构造函数必须做

super(msg, cause, /*enableSuppression*/false, /*writableStackTrace*/false);

http://docs.oracle.com/javase/7/docs/api/java/lang/Throwable.html#Throwable%28java.lang.String,%20java.lang.Throwable,%20boolean,%20boolean%29


现在,有用吗?是的,否则为什么这两个布尔标志首先存在?:)

在一些复杂的情况下,异常可以作为一种流程控制装置,它可以产生更简单、更快的代码。这种异常被称为“控制异常”。

如果异常确实是为了表明程序异常错误,那么使用传统的异常。

于 2013-01-23T23:10:27.760 回答
2

尽管异常相对昂贵并且应该保持在最低限度,但它们不会花费太多,以至于您应该“出于性能目的”做一些迟钝的事情这通常是一个糟糕的借口,甚至有人认为过早的优化应该不惜一切代价避免。虽然这并不完全正确,但您可以衡量异常的速度有多慢。

long start = System.nanoTime();
int exceptionCount = 0;
for (int i = 0; i < 20000; i++)
    try {
        int j = i / (i & 1);
    } catch (ArithmeticException ae) {
        exceptionCount++;
    }
long time = System.nanoTime() - start;
System.out.printf("Each exception took average of %,d ns%n", time / exceptionCount);

打印我认为合理的估计。

Each exception took average of 3,064 ns

注意:随着循环次数的增加,异常会被优化掉。即 10 倍的迭代次数

Each exception took average of 327 ns

以及 10 倍以上

Each exception took average of 35 ns

以及 10 倍以上

Each exception took average of 5 ns

如果抛出的异常足够多,那么 JIT 似乎足够聪明,可以优化掉异常。

于 2013-01-23T21:59:15.353 回答