让我们看看这两个例子。
首先
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”或其他任何内容,那么现在不是重点。我想知道哪种做法总体上更好 - “检查,然后执行”或“做,然后处理异常,如果发生”?
答案 0 :(得分:29)
两者都不应该只检查系统之间的边界。
系统之间的界限是什么?
在库代码中,由于公共API可能被外部代码调用,因此您应该始终检查可由外部代码调用的任何内容。无需检查仅限内部的方法(即使其访问修饰符是公共的)。然后,异常或返回代码可以根据个人喜好来发出参数错误信号,但是不能忽略已检查的异常,因此通常是向外部代码发送错误信号的首选方法。
在应用程序代码中(与库相对),只应在接收用户输入,加载URL,读取文件等时进行检查。不需要在公共方法中进行输入检查,因为它们只能由您自己的应用程序代码调用。
在您自己的代码中,您应该首先避免需要检查的情况。例如,您可以使用"null object pattern",而不是检查null
。如果你仍然需要做一个检查,尽可能早地做,这通常意味着在外面做,但试着找到更早的点。
即使没有正式记录,即使没有强制执行,所有方法,无论是内部还是面向公众,都应该有contract。不同之处在于,在面向外部的代码中,您应该将合同强制执行作为错误检查的一部分,而在仅限内部的代码中,您可以并且应该依赖于调用者知道要传递什么和不传递什么。
简而言之,由于只在系统边界上进行检查,如果您实际上有选择是否检查方法内部或外部,那么您可能不应该检查那里这不是系统边界。
在系统边界中,双方始终必须检查。调用者必须检查外部调用是否成功,并且被调用者必须检查调用者是否提供了合理的输入/参数。忽略调用者中的错误几乎总是一个错误。 Robustness principle适用,始终检查从外部系统收到的所有内容,但只发送外部系统可以接受的内容。
在非常大的项目中,通常需要将项目划分为小模块。在这种情况下,项目中的其他模块可以视为外部系统,因此您应该在API中对项目中的其他模块进行检查。
TLDR; 检查很难。
答案 1 :(得分:8)
正如@LieRyan所说,你应该只检查系统之间的界限。但是一旦你进入你自己的代码,你仍然需要能够发现意外的问题,主要是因为:< / p>
清楚地记录哪些方法接受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 help
答案 2 :(得分: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
}
答案 3 :(得分:3)
这取决于。两种方法都足够好。当您的功能将被其他客户使用时,首先是好的。在这种情况下,您的检查会因非法参数而阻止失败。作为作者,即使遇到意外的错误输入参数,您也可以保证代码正常工作。 当您不是该函数的作者或者不想处理该方法抛出的异常时,可以使用第二个。
答案 4 :(得分:3)
根据Joshua Bloch
Effective Java Second Edition
,第38项,更好的方法是检查有效内部 public
方法的参数。所以,第一种方法要好得多,除了事实,在这种情况下你必须基本抛出NullPointerException
,即RuntimeException
:
// 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
}
答案 5 :(得分:2)
第二个更好,因为第一个,任何异常都可以触发,你会认为这是预期的。
请记住,使用异常来控制程序流程在构建异常成本性能时计算成本很高。例如,在hibernate中,最好做一个SELECT COUNT(*)
来检查一行是否存在,而不是看一个recordNotFoundException是否触发并将你的应用程序逻辑基于该异常触发。
答案 6 :(得分:2)
第二种方法更好,因为它更容易阅读和维护。
您还应该记住,抛出异常有时只会将您的问题从A移动到B,并且它无法解决问题。
答案 7 :(得分:2)
最好从调用者那里做,因为你可以,例如,如果数据来自用户,如果不合适则请求新数据,或者如果数据不好则不调用函数。 因此,您可以保持您的方法“干净”,并且某些方法只会执行它的简单目的功能。因此,它将更容易在其他领域转移和重复使用。您可以,如果提供给方法的数据抛出异常。
E.G。如果它是一种将数据写入文件的方法,并且它将文件路径作为输入。您可以抛出FileNotFound
例外。
通过这种方式,使用您的方法的程序员将知道他必须提供正确的文件路径并事先进行所有必要的检查。
答案 8 :(得分:2)
如果要创建库,则只需提供API而不是某些可运行的代码。您不知道用户是否会从调用者进行检查,因此您可以(不一定,有时if语句执行该作业)从您的方法中抛出异常以确保稳健性。
如果要创建应用程序,第二种方法要好得多。使用try-catch子句仅用于检查简单条件是不好的做法。
事实上,异常抛出语句和if-checks可以共存,我推荐它。但是在你的代码中,你应该尽可能避免引起异常。
异常 会在某些情况发生但不会发生时抛出。通常,它对于调试目的很有用,因为您可以通过评估Exception
实例来跟踪堆栈。
If-clause 在所有情况下都很有用。实际上,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几乎什么都不做。你的第一个例子也是如此,从不拆分属于不同地方的一个方法的代码。
答案 9 :(得分:1)
exceptional conditions
应使用例外;你不希望发生的事情。验证输入不是很特别。
答案 10 :(得分:1)
应该尽可能地努力确保如果从方法中抛出异常,则失败的操作将没有副作用。虽然Java或.net中没有标准手段,异常可以表明它代表“干净的副作用免费故障”,但有很多情况(例如,试图反序列化数据结构)很难预测所有类型的故障都可能发生(例如,由于尝试加载损坏的文件)但几乎所有类型的无副作用故障都可以以相同的方式处理(例如,通知用户无法加载文档) ,很可能是因为它腐败或格式错误。)
即使一个方法仅在一个人自己的代码中使用,在对它们做任何事情之前验证参数通常是好的,这可能会对其他代码产生负面影响。例如,假设代码使用一个延迟排序的列表(即它保留一个可能排序或不排序的列表,以及一个指示它是否已排序的标志);将项添加到列表时,它只是将其附加到末尾并设置“需要排序”标志。如果给出了一个空引用或一个无法与列表中的项进行比较的对象,“添加项”例程可能会“成功”完成,但添加这样的项会导致以后的排序操作失败。即使“添加项”方法不必关心项是否有效,“添加项”方法抛出异常会比将列表保留在以后操作可能的状态更好。失败(此时,即使有人发现排序因列表中的空项而失败,也可能无法确定它是如何到达那里的。)
然后解决问题代码,如果方法foo
收到一个参数值,该值将立即传递给bar
并导致bar
抛出,{{1}抛出异常}而不是foo
可能有点帮助,但是异常最终会立即抛出并且没有副作用的情况 - 即使是通过嵌套方法 - 远远不如{{} {1}}会在抛出异常之前引起一些副作用,或者 - 甚至更糟 - 完成“正常”但会导致将来的操作失败。