21

让我们看看这两个例子。

第一的:

try {
    execute(testObj);
} catch(Exception e) {
    //do somethingwith that
}

public void execute(TestObj testObj) throws Exception {
    if (testObj == null){
        throw new Exception("No such object");
    }
    //do something with object
}

第二:

if (testObj != null){
    execute(testObj);
} else {
    //handle this differently
}

public void execute(TestObj testObj) {
    //do something with object
}

如果我们需要检查“is null”或其他任何内容,这不是现在的重点。我想知道哪种做法总体上更好——“检查,然后执行”或“执行,如果发生则处理异常”?

4

11 回答 11

29

也不,您应该只检查系统之间的边界

系统之间的界限是什么?

  1. 当您收到用户的输入时,
  2. 当您读取文件、网络或套接字时,
  3. 当您进行系统调用或进程间通信时,
  4. 在库代码中,在所有面向公众的 API 中。
  5. 等等

在库代码中,由于公共 API 可能被外部代码调用,因此您应该始终检查任何可以被外部代码调用的内容。无需检查仅限内部的方法(即使其访问修饰符是公共的)。然后,根据个人喜好,参数错误可能会通过异常或返回码发出信号,但是检查异常不能被忽略,因此它通常是向外部代码发出错误信号的首选方法。

在应用程序代码(相对于库)中,只应在接收用户输入、加载 URL、读取文件等时进行检查。在公共方法中无需进行输入检查,因为它们只是曾经被您自己的应用程序代码调用过。

在您自己的代码中,您应该避免首先需要检查的情况。例如null,您可以使用“空对象模式”而不是检查。如果您仍然需要进行检查,请尽早进行,这通常意味着在外面进行,但要尝试找到更早的点。

即使没有正式写下来,即使没有强制执行,所有方法,无论是内部的还是面向公众的,都应该有一个契约。不同之处在于,在面向外部的代码中,您应该将合约作为错误检查的一部分来执行,而在仅限内部的代码中,您可以并且应该依赖调用者知道要传递什么和不传递什么。

简而言之,由于仅在系统边界上进行检查,如果您实际上可以选择是在方法内部还是外部进行检查,那么您可能不应该在那里进行检查,因为那不是系统边界

在系统边界中,双方总是要检查. 调用者必须检查外部调用是否成功,而被调用者必须检查调用者是否提供了合理的输入/参数。忽略调用者中的错误几乎总是一个错误。健壮性原则适用,始终检查您从外部系统收到的所有内容,但只发送您知道外部系统可以肯定接受的内容。

但是,在非常大的项目中,通常需要将项目划分为小模块。在这种情况下,项目中的其他模块可以被视为外部系统,因此您应该检查暴露给项目中其他模块的 API。

TLDR;检查是困难的。

于 2013-03-01T11:38:01.537 回答
8

正如@LieRyan 所说,您应该只检查系统之间的边界。但是一旦你进入了自己的代码,你仍然需要能够检测到意想不到的问题,主要是因为:

  • 您(和您的同事)并不完美,可能会将 null 传递给无法处理它的方法。
  • 如果一行使用两个对象并且一个为空,则 NPE 不会告诉您应该归咎于哪一个 (System.out.println ("a.getName() " + "b.getName()") throws NPE...is a null 或 b null 或两者兼而有之?)

清楚地记录哪些方法接受 null 作为参数,或者可能返回 null。
如果您使用的是 Eclipse Juno,一个可以帮助您的好工具(我认为 IntelliJ-Idea 有类似的东西)是启用空检查分析。它允许您编写一些注释来进行编译时空检查。真是太棒了。你可以写类似

public @NonNull String method(@Nullable String a){
//since a may be null, you need to make the check or code will not compile
    if(a != null){
        return a.toUppercase();        
    }
    return ""; //if you write return null it won't compile, 
     //because method is marked NonNull 
}

它还有一个很好的@NonNullByDefault,它基本上说“没有方法接受或返回null,除非标记为@Nullable”这是一个很好的默认值,可以让你的代码保持干净和安全。

有关更多信息,请查看Eclipse 帮助

于 2013-03-01T14:55:00.147 回答
4

我会说这取决于异常的类型。如果异常归结为无效参数,请在方法中检查它们。否则,留给调用者处理。

例子:

public void doSomething(MyObject arg1, MyObject arg2) throw SomeException {
   if (arg1==null || arg2==null) {
       throw new IllegalArgumentException(); //unchecked
   }
   // do something which could throw SomeException
   return;
}

调用:

try {
    doSomething(arg1, arg2);
} catch (SomeException e) {
    //handle
}
于 2013-03-01T09:57:36.123 回答
3

这取决于。这两种方法都足够好。当您的功能将被其他客户使用时,首先是好的。在这种情况下,您的检查可以防止由于非法参数而导致的失败。作为作者,您保证您的代码即使在出现意外错误输入参数的情况下也能正常工作。当您不是该函数的作者或不想处理该方法抛出的异常时,可以使用第二个。

于 2013-03-01T10:04:49.087 回答
3

根据第 38 条Joshua Bloch Effective Java Second Edition,在方法内部检查参数的有效性是更好的方法。所以,第一种方法要好得多,除了事实是,在这种情况下,你必须基本上抛出,即: publicNullPointerExceptionRuntimeException

// do not need try...catch due to RuntimeException
execute(testObj);

public void execute(TestObj testObj) {
    if (testObj == null){
        throw new NullPointerException("No such object");
    }
    ;//do smth with object
}
于 2013-03-01T10:12:11.250 回答
2

第二个和第一个一样好,任何异常都可能触发,你会认为这是意料之中的。

请记住,使用异常来控制程序流的计算成本很高,因为构建异常会降低性能。例如,在休眠状态下,最好SELECT COUNT(*)检查一行是否存在,而不是查看 recordNotFoundException 是否触发并将应用程序逻辑基于该异常触发。

于 2013-03-01T09:54:28.430 回答
2

第二种方法更好,因为它更易于阅读和维护。

您还应该记住,抛出异常有时只会将您的问题从 A 转移到 B,并且不能解决问题。

于 2013-03-01T09:57:26.210 回答
2

最好从调用者那里进行,因为您可以,例如,如果数据是从用户那里提供的,如果它们不合适,则可以请求新数据,或者如果数据不好,则不调用该函数。因此,您可以保持您的方法“干净”,并且某些方法只会执行它的简单目的功能。因此,它将更容易在其他领域转移和重复使用。你可以,如果提供给方法的数据抛出异常。

例如,如果它是一种将数据写入文件的方法,并且它获取文件路径作为输入。你可以抛出FileNotFound异常。

这样,使用您的方法的程序员将知道他必须提供正确的文件路径并事先进行所有必要的检查。

于 2013-03-01T09:58:24.520 回答
2

按目的:

如果您正在创建一个库,那么您只是提供 API,而不是一些可运行的代码。您不知道用户是否会从调用者那里进行检查,因此您可以(不一定,有时 if 语句可以完成工作)从您的方法中抛出异常以确保稳健性。

如果您正在创建应用程序,则第二种方法要好得多。仅将 try-catch 子句用于检查简单条件是一种不好的做法。

事实上,异常抛出语句和 if-checks 可以共存,我推荐它。但是在您的代码中,您应该尽可能避免引发异常。


按用途:

当某些情况不是预期但可能发生时,会引发异常。通常它对调试很有用,因为您可以通过评估Exception实例来跟踪堆栈。

if 子句在所有情况下都很有用。事实上,throw语句通常包含在if子句中。这意味着您不想(或不能)处理这种情况,只需将其留给调试器(异常管理器)即可。或者,您可以随心所欲地编写代码。


在大多数情况下,您希望处理所有情况,尽管有些情况不太可能发生。正如我所说,尝试抛出尽可能少的异常,不仅是出于性能考虑,也是为了方便使用。

一个(坏的)例子表明有时异常是浪费的:

public void method1 () throws Exception1, Exception2 {
    if (condition) {
        throw new Exception1("condition match");
    } else {
        throw new Exception2("condition mismatch");
    }
}

public void method2 () {
    try {
        method1();
    } catch (Exception1 e1) {
        // do something
    } catch (Exception2 e2) {
        // do something
    }
}

通常,您不会编写这样的代码,但正如您所见,一个方法被分成两个,而 method1 几乎什么都不做。与您的第一个示例相同,切勿将属于一个方法的代码拆分到不同的地方。

于 2013-03-01T10:13:01.810 回答
1

例外应该用于exceptional conditions; 你不希望发生的事情。验证输入并不是很特别。

于 2013-03-01T10:00:38.050 回答
1

只要有可能,就应该努力确保如果从方法中抛出异常,失败的操作将没有副作用。虽然在 Java 或 .net 中没有标准方法可以通过它来指示异常表示它表示“干净的无副作用故障”,但在许多情况下(例如,尝试反序列化数据结构)很难预测所有类型的故障都可能发生(例如,由于尝试加载损坏的文件),但几乎所有类型的无副作用故障都可以以相同的方式处理(例如,通知用户无法加载文档,很可能是因为它已损坏或格式错误)。

即使一个方法只在自己的代码中使用,在对参数做任何可能对其他代码产生不利影响的事情之前验证参数通常是好的。例如,假设代码使用了一个惰性排序列表(即,它保留了一个可能已排序或未排序的列表,以及一个指示它是否已排序的标志);将项目添加到列表时,它只是将其附加到末尾并设置“需要排序”标志。如果给定一个空引用或一个无法与列表中的项目进行比较的对象,“添加项目”例程可能会“成功”完成,但添加这样一个项目会导致以后的排序操作失败。即使“添加项目”方法不必关心项目是否有效,“添加项目”会好得多

然后是故障排除代码,如果方法foo接收到将立即传递给bar并导致bar抛出的参数值,则抛出异常foo而不是bar可能会有所帮助,但异常最终会立即抛出的场景基本上没有副作用 - 即使通过嵌套方法 - 远不如foo在引发异常之前会导致一些副作用的场景重要,或者 - 更糟糕的是 - “正常”完成但导致未来操作失败.

于 2013-03-01T16:10:15.780 回答