如果操作会使对象进入非法状态,应该抛出什么异常?

What exception to throw if an operation would bring the object into an illegal state?

考虑一个 Product 和一个 quantity,它可以增加和减少一个给定的 amount。数量绝对不能变成负数,如果出现负数,必须禁止操作并警告用户。

public class Product{
    
    private int quantity;
    
    public Product() {
        quantity = 10;
    }
    
    public void decreaseQuantity(int amount) {
        int decreasedQuantity = quantity - amount;
        if(decreasedQuantity < 0 )
            throw new RuntimeException(String.format("Decrease quantity (%s) exceeds avaiable quantity (%s)",
                    amount, quantity));
        
        quantity = decreasedQuantity;
    }
}

例如,如果产品的数量为 10,而我尝试删除 20,则会抛出 RuntimeException。 SonarCloud 建议将 RuntimeException 替换为自定义异常,但我想知道是否有适合这种情况的标准异常(有效Java:赞成使用标准异常) .

最合适的例外似乎是IllegalStateException。来自 javadoc

Signals that a method has been invoked at an illegal or inappropriate time. In other words, the Java environment or Java application is not in an appropriate state for the requested operation.

并且来自有效Java

IllegalStateException: This exception is used if, when a method is called, the state of the object is not valid for that operation. Potentially you have a filehandle and you call read before it has been opened.

但是,在我看来,我的示例与文档中假设的示例之间存在细微差别:不是对象 自身的状态 操作不合法,是对象的状态和输入参数的值。阅读使用示例(如What's the intended use of IllegalStateException?)无论输入参数如何,对象始终处于拒绝操作的状态。

java.lang.IllegalArgumentException 是这种情况下的正确答案。

此示例等同于 Effective Java - 第三版(第 301 页)中的以下示例:

Consider the case of an object representing a deck of cards, and suppose there were a method to deal a hand from the deck that took as an argument the size of the hand. If the caller passed a value larger than the number of cards remaining in the deck, it could be construed as an IllegalArgumentException (the handSize parameter value is too high) or an IllegalStateException (the deck contains too few cards). Under these circumstances, the rule is to throw IllegalStateException if no argument values would have worked, otherwise throw IllegalArgumentException.

IllegalStateException不正确,因为对象的状态不会阻止调用decreaseQuantity:您可以调用它,只需使用适当的输入值.

核心 java 库中没有任何东西是灌篮高手。然而,让你自己的例外可能是你最好的选择,这是在使用 IllegalArgumentException.

之间的一场势均力敌的比赛

RuntimeException

出于模糊的原因,我建议您。不使用它的主要原因是 linter 工具会对你大喊大叫。他们对你大吼大叫的原因有两个,要确定正确的做法是告诉 linters 闭嘴,还是采纳他们的建议,就要关注这两个原因:

  1. 异常的类型名本身就是信息。例如,throw new NullPointerException("x is null") 是编写的愚蠢代码(即使它很常见)。这是多余的 - new NullPointerException("x") 是合适的。 RuntimeException 几乎不传达任何信息。你主要是在避免这个陷阱:虽然 RuntimeException 确实几乎没有传达任何信息,但异常消息说明了一切,因此,linter 试图阻止发生的事情(抛出一个没有正确传达问题性质的异常) 没有发生,因此你应该考虑告诉 linter 停止抱怨......除了:

  2. 您需要正确异常类型的第二个原因是您真的、真的不希望旨在捕获此异常的代码必须对异常消息进行字符串分析。因此,如果在任何世界中您可以预见某些代码想要调用您的方法,然后以除破坏所有上下文之外的任何方式对条件(试图减去超过可用的)做出反应,那么您应该抛出一个exception 表示此特定条件,仅此而已。 RuntimeException 从不 如果您的意图是捕捉特定条件(这几乎不适合,句号 - 有时您想 运行 编码并做出反应对于任何问题,无论其性质如何,但是 catch (Exception e) 是合适的 catch 块,甚至 Throwable。例如应用服务器等应该这样做。

IllegalArgumentException

linter 不会大喊大叫,但您仍然没有真正获得次要好处(也就是说,允许调用者捕获这个特定问题而不是参数的其他问题)。它也 'mentally' 有点可疑:您最初无视它的理由并没有错。通常 IAE 被理解为暗示非法参数是非法的,无论该对象的状态如何。但这并没有写在 IAE 的 javadoc 中,也没有被普遍应用。

因此,缺点是:

  • 迫使呼叫者 catch (IllegalArgumentException e) 来处理想要对减去比那里更多的反应的反应。 IAE还是太'general'.
  • 这可能有点令人困惑。

上升空间很简单:

  • 它已经存在了。

IllegalStateException

我认为这比 IAE 差很多。这基本上是同一个故事,除了这里的混淆是 ISE 通常用于标记对象的当前状态使得您尝试调用的操作根本不可用。以一种经过调整的方式实际上是正确的(对象的状态是有 3 个项目;因此它现在不处于 decreaseQuantity(5) 是可用操作的状态),但感觉比 IAE 更令人困惑:ISE 感觉像Product 对象本身处于无效状态。我假设该产品是遗留产品,现在没有或永远不会再次有货,或者是某种虚拟产品类型(代表 'unknown product' 或类似异国情调的产品对象)。

但是,与 IAE 的交易相同:javaISE 的文档没有明确说明您不能这样做。因此,如果你更愿意抛出这个,你可以,它不能证明是不正确的,最坏的情况是它只是糟糕的代码风格。一个 linter 工具永远不会因此而责备你,如果是,则 linter 工具是错误的。

写你自己的

优点:

  • 您图书馆的用户不可能提出错误的假设(IAE 意味着:无论状态如何,论点都是非法的);这样就不会那么混乱了。
  • 如果代码打算调用您的方法并专门针对删除多于存在的条件编写一个 catch 子句,那么这绝对是最好的答案。这个:
try {
  product.decreaseQuantity(5);
} catch (QuantityInsufficientException e) {
  ui.showOrderingError("Regretfully we don't have this item in stock at the quantity you want");
}

catch (IllegalArgumentException) 的可读性稍好,更重要的是,它更可靠:IAE 是一种经常使用的异常,你有一天会编辑 decreaseQuantity 方法并引入由于某些其他原因抛出 IAE 的代码路径,现在您很难找到错误。

结论

我会写你自己的。是的,必须编写源文件有点麻烦,但您可以让 IDE(或 Project Lombok 最大样板破坏)生成整个文件,您可能永远不必查看InsufficientQuantityException.java 再一次。