何时应该抛出IllegalArgumentException?

118

我担心这是运行时异常,因此应该谨慎使用。
标准用例:

void setPercentage(int pct) {
    if( pct < 0 || pct > 100) {
         throw new IllegalArgumentException("bad percent");
     }
}

但这似乎会强制采用以下设计:

public void computeScore() throws MyPackageException {
      try {
          setPercentage(userInputPercent);
      }
      catch(IllegalArgumentException exc){
           throw new MyPackageException(exc);
      }
 }

让它重新成为一个已检查的异常。

好的,但我们来看看这个。如果您提供错误的输入,您将获得运行时错误。首先,这实际上是一个相当难以统一实现的策略,因为您可能需要进行完全相反的转换:

public void scanEmail(String emailStr, InputStream mime) {
    try {
        EmailAddress parsedAddress = EmailUtil.parse(emailStr);
    }
    catch(ParseException exc){
        throw new IllegalArgumentException("bad email", exc);
    }
}

而更糟糕的是-虽然客户端代码可以预期静态地检查0 <= pct && pct <= 100,但对于更高级的数据(如电子邮件地址或更糟糕的是必须针对数据库进行检查的内容),通常情况下客户端代码无法进行预验证。因此,基本上我想说的是我没有看到一个有意义的一致的使用IllegalArgumentException的策略。似乎不应该使用它,我们应该坚持使用自己的已检查异常。什么情况下抛出这个异常是一个好的用例?
6个回答

95
IllegalArgumentException 的 API 文档如下:
抛出以指示方法已传递非法或不适当的参数。
从 JDK 库中 how it is used in the JDK libraries 的使用情况来看,我会说:
- 它似乎是一种防御性措施,用于在输入进入程序并导致无意义的错误消息时,在明显的坏输入上进行投诉。 - 它用于那些抛出已检查异常太烦人的情况(尽管它出现在 java.lang.reflect 代码中,在那里关于可笑的检查异常抛出水平的担忧并不明显)。
我会使用 IllegalArgumentException 来进行最后的防御性参数检查,用于常见实用程序(尝试保持与 JDK 使用的一致性)。或者在预期一个错误的参数是程序员错误的情况下使用,类似于 NullPointerException。我不会将其用于业务代码的验证。我当然也不会将其用于电子邮件示例。

10
我认为“当预期是错误的程序员错误时,一个糟糕的参数是错误的”这条建议最符合我在实践中的使用情况,因此接受这个答案。 - djechlin

28
当提到“错误输入”时,您应该考虑输入来源。
如果该输入是由用户或您无法控制的其他外部系统输入的,则应该预期该输入无效,并始终进行验证。在这种情况下,抛出一个已检查的异常是完全可以的。您的应用程序应通过向用户提供错误消息来“恢复”此异常。
如果输入来源于您自己的系统,例如数据库或应用程序的某些其他部分,则应该能够信任其有效性(在到达那里之前应该已经进行了验证)。在这种情况下,抛出未检查的异常(例如IllegalArgumentException),这种异常不应该被捕获(通常您永远不应该捕获未检查的异常)。使无效值首先到达该位置是程序员的错误;) 您需要修复它。

4
为什么“你不应该捕获未经检查的异常”? - Koray Tugay
12
未经检查的异常是由于编程错误而抛出的。调用方法抛出此类异常的调用方不能合理地预期从中恢复,因此通常没有意义捕获它们。 - Tom
3
"因为未经检查的异常是由于编程错误而导致抛出的",这句话帮助我理清了很多东西,谢谢 :) - svarog

18
"稀少地"抛出运行时异常并不是一个好的策略 - 《Effective Java》建议在“调用者可以合理地预期恢复”时使用已检查异常。(程序员错误是一个具体的例子:如果特定情况表示程序员错误,则应抛出未经检查的异常;您希望程序员拥有逻辑问题发生的堆栈跟踪,而不是尝试自己处理它。)
如果没有恢复的希望,那么可以放心使用未经检查的异常;没有捕获它们的意义,因此这完全没问题。
从您的示例中,并不完全清楚此示例在您的代码中属于哪种情况。"

我认为“可合理预期恢复”一词有些含糊不清。任何操作foo(data)都可能作为for(Data data:list)foo(data);的一部分发生,调用者可能希望尽可能多地成功,即使某些数据格式不正确。这也包括程序错误,如果我的应用程序失败意味着交易无法完成,那么这可能更好,但如果核冷却失效,那就很差劲了。 - djechlin
StackOverflowError等错误是调用者无法合理地恢复的情况。但似乎任何数据或应用程序逻辑层面的情况都应该进行检查。这意味着要进行空指针检查! - djechlin
5
在核冷却应用中,我宁愿在测试中彻底失败,也不愿让程序员认为不可能出现的情况被忽视。 - Louis Wasserman
Boolean.parseBoolean(..) 会抛出 IllegalArgumentException,即使 "调用者可以合理地预期恢复"。因此......由你的代码来处理它或者回溯到调用者。 - Jeryl Cook

8

IllegalArgumentException视为一个前置条件检查,并考虑设计原则:公共方法应该知道并公开记录其自身的前置条件。

我同意这个例子是正确的:

void setPercentage(int pct) {
    if( pct < 0 || pct > 100) {
         throw new IllegalArgumentException("bad percent");
     }
}

如果EmailUtil是不透明的,也就是说有一些原因无法向最终用户描述前提条件,那么使用已检查异常是正确的。对于这种设计进行了修正的第二个版本:
import com.someoneelse.EmailUtil;

public void scanEmail(String emailStr, InputStream mime) throws ParseException {
    EmailAddress parsedAddress = EmailUtil.parseAddress(emailStr);
}

如果EmailUtil是透明的,例如它可能是由所讨论的类拥有的私有方法,则只有当其前提条件可以在函数文档中描述时,IllegalArgumentException才是正确的。以下也是正确的版本:
/** @param String email An email with an address in the form abc@xyz.com
 * with no nested comments, periods or other nonsense.
 */
public String scanEmail(String email)
  if (!addressIsProperlyFormatted(email)) {
      throw new IllegalArgumentException("invalid address");
  }
  return parseEmail(emailAddr);
}
private String parseEmail(String emailS) {
  // Assumes email is valid
  boolean parsesJustFine = true;
  // Parse logic
  if (!parsesJustFine) {
    // As a private method it is an internal error if address is improperly
    // formatted. This is an internal error to the class implementation.
    throw new AssertError("Internal error");
  }
}

这个设计可能有两种选择。

  • 如果预置条件难以描述,或者类意图被客户端使用但客户端不知道他们的电子邮件地址是否有效,则使用ParseException。这里的顶级方法名为scanEmail,暗示最终用户打算通过此方法发送未经研究的电子邮件,因此使用ParseException是正确的。
  • 如果可以在函数文档中描述预置条件,并且该类不意图接受无效输入,因此指示程序员错误,则使用IllegalArgumentException。虽然它不是“检查过的”,但“检查”移动到了记录函数的Javadoc中,期望客户端遵循此规范。对于客户端无法事先确定其参数是否非法的情况,使用IllegalArgumentException是错误的。(注意:IllegalArgumentException是指传入参数不合法而导致的异常)

关于IllegalStateException的说明: 这意味着“此对象的内部状态(私有实例变量)不能执行此操作”。最终用户无法看到私有状态,所以笼统地说,在客户调用没有办法知道对象状态不一致时,它比IllegalArgumentException更优先。我无法给出一个好的解释,当它比检查异常更优先时,例如两次初始化,或者丢失未恢复的数据库连接。


6
任何API在执行公共方法之前,都应该检查每个参数的有效性:
void setPercentage(int pct, AnObject object) {
    if( pct < 0 || pct > 100) {
        throw new IllegalArgumentException("pct has an invalid value");
    }
    if (object == null) {
        throw new IllegalArgumentException("object is null");
    }
}

这些错误在应用程序中占99.9%,因为它正在请求不可能的操作,所以最终它们是应该使应用程序崩溃的错误(因此是不可恢复的错误)。

在这种情况下,遵循快速失败的方法,您应该让应用程序完成以避免破坏应用程序状态。


相反地,如果API客户端给我错误的输入,我不应该使整个API服务器崩溃。 - djechlin
2
当然,它不应该使您的API服务器崩溃,而是向调用者返回异常。它不应该导致任何问题,只有客户端可能会崩溃。 - Ignacio Soler Garcia
你在评论中写的内容和你在答案中写的不一样。 - djechlin
1
让我解释一下,如果第三方客户端使用错误参数(即有 bug)调用 API,则客户端应该崩溃。如果是 API 服务器自身存在 bug 并使用错误参数调用方法,则 API 服务器应该崩溃。请参考:https://en.wikipedia.org/wiki/Fail-fast - Ignacio Soler Garcia

6
根据Oracle官方教程的规定,如果客户端可以合理地从异常中恢复,则将其设为检查异常。 如果客户端无法从异常中恢复,则将其设置为无检查异常。
如果我有一个应用程序使用JDBC与数据库交互,并且我有一个方法将参数作为int item和double price。 相应项目的价格从数据库表中读取。 我只需将购买的item总数与price值相乘并返回结果。 虽然我总是确定在表中price字段值永远不会为负数。 但是如果价格值为负数怎么办? 这显示数据库侧存在严重问题。 可能是操作员错误输入了价格。 这是其他调用该方法的应用程序部分无法预料和无法恢复的问题。 这是你的数据库中的一个BUG。 因此,在这种情况下应该抛出IllegalArgumentException(),“价格不能为负数”。 我希望我已经清楚地表达了我的意见。

我不喜欢这个(Oracle的)建议,因为异常处理是关于如何恢复,而不是是否恢复。例如,一个格式错误的用户请求并不值得使整个Web服务器崩溃。 - djechlin
@djechlin . . . 有时应用程序无法恢复。当传递非法参数时,通常情况下就是这种情况。这是调用者应该修复而不是被调用者的问题。这就是运行时异常的作用。关于你的例子,我同意,格式错误的用户请求不应该使整个Web服务器崩溃。但我认为格式错误的用户请求首先并不是无效的参数。Web服务器应该简要分析无效请求,并将垃圾友好地抛回给调用者(可能带有有关如何修复它的提示)。问题解决了。 - Bart Hofland

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