Java中理解已检查异常和未检查异常

758

《Effective Java》中的Joshua Bloch说:

对于可恢复的情况使用checked异常,对于编程错误使用runtime异常(第2版的第58项)。

让我们看看我是否正确理解了这个概念。

这是我对checked异常的理解:

try{
    String userInput = //read in user input
    Long id = Long.parseLong(userInput);
}catch(NumberFormatException e){
    id = 0; //recover the situation by setting the id to 0
}

1. 上述内容是否被认为是已检查异常?

2. RuntimeException 是否属于未检查异常?

以下是我对未检查异常的理解:

try{
    File file = new File("my/file/path");
    FileInputStream fis = new FileInputStream(file);   
}catch(FileNotFoundException e){

//3. What should I do here?
    //Should I "throw new FileNotFoundException("File not found");"?
    //Should I log?
    //Or should I System.exit(0);?
}

4. 现在,上面的代码难道也不是一个checked异常吗?我可以像这样尝试恢复情况吗?(注意:我的第三个问题在上面的catch中)

try{
    String filePath = //read in from user input file path
    File file = new File(filePath);
    FileInputStream fis = new FileInputStream(file);   
}catch(FileNotFoundException e){
    //Kindly prompt the user an error message
    //Somehow ask the user to re-enter the file path.
}

5. 为什么人们会这样做?

public void someMethod throws Exception{

}

为什么要让异常冒泡?处理错误的时候不是越早越好吗?为什么要冒泡呢?

6. 我应该冒泡精确的异常信息还是使用 Exception 进行掩盖?

以下是我的阅读材料:

Java 中,我何时应创建 checked exception,何时应该使用 runtime exception?

何时选择 checked 和 unchecked 异常


8
我有一个未被检查的异常很好的例子。我有一个DataSeries类,它保存的数据必须始终保持按时间排序。有一个方法将新的DataPoint添加到DataSeries的末尾。如果在整个项目中我的代码都正常工作,则不应该向末尾添加先前日期比当前末尾日期早的DataPoint。整个项目中的每个模块都是建立在这一真理之上的。但是,如果出现此情况,我会检查该条件并抛出未被检查的异常。为什么?如果发生这种情况,我想知道是谁在这样做,并进行修复。 - Erick Robertson
3
更加混乱的是,许多人在大约10年前曾主张使用“已检查异常”,但现今的观点越来越倾向于“已检查异常是不好的”。(然而,我并不同意这种观点) - Kaj
12
只有在你有有用的操作要处理异常时,它才是有用的,否则应该让调用者来处理。仅仅记录日志并假装它没发生通常是没有用的。简单地重新抛出异常也毫无意义。将其包装在RuntimeException中并不像一些人认为的那样有用,它只会让编译器停止帮助你。(个人看法) - Peter Lawrey
53
我们应该停止使用全面误导的“checked/unchecked”异常术语。 它们应该被称为“check-mandated”和“check-not-mandated”异常。 - Blessed Geek
3
我也思考过你的第五个点:public void method_name throws Exception{},为什么有些人要这样做呢? - Maveňツ
显示剩余6条评论
21个回答

518
很多人说,应该根本不使用检查异常(即那些你必须显式捕获或重新抛出的异常)。例如,在C#中就已经将其删除,而大多数语言都没有这个概念。因此,你总是可以抛出一个RuntimeException的子类(未检查异常)。
然而,我认为检查异常是有用的——当你想要强制API的使用者思考如何处理异常情况(如果它是可恢复的)时,就会使用它们。只是在Java平台上过度使用了检查异常,这让人们讨厌它们。
关于具体的问题:
1. NumberFormatException被视为已检查异常吗?
不是的。 NumberFormatException是未检查异常(=是RuntimeException的子类)。为什么呢?我不知道(但应该有一个isValidInteger(..)方法)。
2. RuntimeException是未检查异常吗?
是的,确实是。
3. 我应该在这里做什么?
这取决于代码所在的位置以及您想要发生什么。如果它在UI层 - 捕获它并显示警告;如果它在服务层 - 根本不要捕获它 - 让它冒泡。只是不要吞噬异常。如果发生异常,在大多数情况下,您应该选择以下之一:
- 记录并返回 - 重新抛出它(声明方法抛出异常) - 通过构造函数传递当前异常来构造一个新的异常
4. 现在,上面的代码也可以是一个受检异常吗?我可以尝试像这样恢复情况吗?可以吗? 可能是可以的。但是没有什么能阻止你捕获未检查的异常。
5. 为什么人们在throws子句中添加Exception类? 最常见的原因是人们懒得考虑什么该捕获,什么该重新抛出。抛出Exception是一种不好的做法,应该避免使用。
唉,没有一个单一的规则可以让你确定何时捕获,何时重新抛出,何时使用受检异常,何时使用未检查异常。我同意这会导致很多混乱和糟糕的代码。Bloch提出了一个总体原则(你引用了其中一部分)。总体原则是将异常重新抛出到可以处理它的层级。

39
关于抛出异常,它并不总是因为人们懒惰,实现框架时也常常让框架的使用者能够抛出任何异常。例如,您可以检查JSE中Callable接口的签名。 - Kaj
10
@Kaj - 是的,像Callable、拦截器等这样的通用事物是特殊情况。但在大多数情况下,这是因为人们懒惰 :) - Bozho
8
针对3.1中的“记录并返回”,请谨慎处理。这很接近于吃掉或隐藏异常。我只会在不表示问题、不是真正例外情况下采取此做法。日志容易被淹没和忽略。 - Chris
7
当你想要强制 API 使用者思考如何处理异常情况时,你无法强迫任何人去思考。如果他们不想思考,他们会编写一个无用的异常块,或者更糟糕的是删除或干扰关键错误信息。这就是为什么检查异常是失败的原因。 - adrianos
3
“如果一个人不想思考,你不能强迫他去思考。” 按照这种思路,我们也可以消除编译错误…… 我并不是针对你,我听过这种论点很多次,但仍然认为这是将已检查异常标记为失败的最差解释。 顺便说一下,我以前见过一种语言,在该语言中,通过设计有效地使编译(实际上还包括运行时)错误成为不可能。 但那条路导致了一些非常黑暗的地方。 - Newtopian
显示剩余19条评论

258
无论你是否捕获它,或在catch块中执行什么操作,某些东西是否为“checked exception”与此无关。这是异常类的一个属性。任何Exception子类都是checked exception, 但RuntimeException和它的子类除外。
Java编译器强制你要么捕获checked exception, 要么在方法签名中声明它们。它本来旨在提高程序的安全性,但大多数人认为这样做并不值得设计问题。
因为这就是异常的全部意义所在。如果没有这种可能性,你将不需要异常。它们使你能够在你选择的级别上处理错误,而不是被迫在最初发生错误的低级方法中处理它们。

3
谢谢!我有时会因为"垃圾进去,垃圾出来"的原则而在我的方法中抛出异常。如果我的团队中的任一开发人员想输入无效的XPath表达式,那么他们就需要自己处理异常。如果不幸地他们捕获了异常却未做任何处理,在代码审查中他们会被提醒。 - jeremyjjbrown
13
你的陈述是不正确的。除了 RuntimeException 及其子类之外,任何 Throwable 的子类都是已检查异常。但是,Error 也继承了 Throwable,它是一个未检查的异常。 - Bartzilla
9
基本上,异常的一个主要优势是它们允许你选择在调用栈中的哪个位置处理错误,这通常相当高,同时保持中间层完全不受错误处理工件的影响。但是,检查异常破坏了这种优势。另一个问题是检查/非检查异常的区别与异常类绑定在一起,后者代表异常的概念分类 - 混合两个可能没有任何关系的方面。 - Michael Borgwardt
2
“但大多数人的观点似乎是,它不值得它所创建的设计问题。” - 引用来源是什么? - kervin
3
是的。为了完整性,正如 Throwable 的 javadoc 所述:“Throwable 和任何不是同时也是 RuntimeExceptionError 子类的 Throwable 子类都被视为已检查异常”。 - Bart van Heukelom
显示剩余3条评论

78
  1. 上面的异常被认为是受检异常吗? 不是的 如果一个异常是 RuntimeException 类型的,则处理该异常并不会使它成为 Checked Exception

  2. RuntimeException 是否属于 unchecked exception? 是的

Checked Exceptions(受检异常)java.lang.Exception 的子类, Unchecked Exceptions(未受检异常)java.lang.RuntimeException 的子类。

调用可能抛出受检异常的函数需要用 try{} 包围或在调用方法的较高级别中进行处理。在这种情况下,当前方法必须声明它会抛出这些受检异常,以便调用者可以做出适当的处理。

希望这可以帮助到您。

问:我是否应该向上传递确切的异常或使用 Exception 进行掩盖?

答:是的,这是一个非常好的问题和重要的设计考虑因素。Exception 类是一个非常通用的异常类,可以用于包装内部低级别异常。您最好创建自定义异常并将其包装在其中。但是,请注意——绝对不要隐藏底层的原始根本原因。例如,绝对不要这样做——

try {
     attemptLogin(userCredentials);
} catch (SQLException sqle) {
     throw new LoginFailureException("Cannot login!!"); //<-- Eat away original root cause, thus obscuring underlying problem.
}

改为以下操作:

try {
     attemptLogin(userCredentials);
} catch (SQLException sqle) {
     throw new LoginFailureException(sqle); //<-- Wrap original exception to pass on root cause upstairs!.
}
吃掉原始根本原因会将实际原因埋得无法挽回,这对于只能获取应用程序日志和错误消息的生产支持团队来说是一场噩梦。虽然后者是更好的设计,但由于开发人员未能向调用者传递潜在的信息,许多人经常不使用它。所以请牢记:无论是否包装在任何特定于应用程序的异常中,始终传递实际异常
关于try-catching RuntimeExceptions: 通常情况下,不应该尝试先捕获RuntimeException。它们通常表示编程错误,应该被留下来。相反,程序员应该在调用可能导致RuntimeException的代码之前检查错误条件。例如:
try {
    setStatusMessage("Hello Mr. " + userObject.getName() + ", Welcome to my site!);
} catch (NullPointerException npe) {
   sendError("Sorry, your userObject was null. Please contact customer care.");
}

这是一种不好的编程实践。相反,应该像这样进行空值检查-

if (userObject != null) {
    setStatusMessage("Hello Mr. " + userObject.getName() + ", Welome to my site!);
} else {
   sendError("Sorry, your userObject was null. Please contact customer care.");
}

但有时进行这种错误检查是很昂贵的,比如数字格式化,考虑下面的例子:

try {
    String userAge = (String)request.getParameter("age");
    userObject.setAge(Integer.parseInt(strUserAge));
} catch (NumberFormatException npe) {
   sendError("Sorry, Age is supposed to be an Integer. Please try again.");
}

在这里,对调用前的错误检查并没有太大必要,因为实际上这意味着复制所有字符串转换为整数的代码到parseInt()方法中,如果由开发人员实现,则容易出错。因此最好去掉try-catch。

因此,NullPointerExceptionNumberFormatException都是RuntimeExceptions,捕获NullPointerException应该替换为一个优雅的null检查,而我建议显式地捕获NumberFormatException以避免可能引入容易出错的代码。


谢谢。还有一个问题,当您冒泡出“异常”时,我应该冒泡出精确的异常还是使用“Exception”掩盖它。我正在顶部编写一些遗留代码,并且“Exception”在所有地方都被冒泡出来。我想知道这是否是正确的行为? - Thang Pham
1
这是一个非常好而且重要的问题,我编辑了我的答案并加入了解释。 - d-live
非常感谢。您能否向我展示LoginFailureException(sqle)的内容? - Thang Pham
1
我没有相关的代码,只是编了一些名称等。如果你看到java.lang.Exception,它有4个构造函数,其中两个接受java.lang.Throwable。在上面的片段中,我假设LoginFailureException扩展了Exception并声明了一个构造函数public LoginFailureException(Throwable cause){ super(cause)} - d-live
关于这个话题,我认为不应该捕获运行时异常,因为这些异常是由于编程不良而引起的。我完全同意其中的一部分:“吞噬原始根本原因会将实际原因埋藏在无法恢复的地方,这对于生产支持团队来说是一场噩梦,因为他们只能访问应用程序日志和错误消息。” - huseyin

20

1. 如果您对某个异常不确定,可以查看API:

 java.lang.Object
 extended by java.lang.Throwable
  extended by java.lang.Exception
   extended by java.lang.RuntimeException  //<-NumberFormatException is a RuntimeException  
    extended by java.lang.IllegalArgumentException
     extended by java.lang.NumberFormatException

2. 是的,以及所有继承它的异常。

3. 没有必要捕获并抛出相同的异常。在这种情况下,您可以显示一个新的文件对话框。

4. FileNotFoundException已经是一个已检查的异常。

5. 如果预计调用someMethod的方法捕获异常,则可以抛出后者。它只是“传球”。如果您想在自己的私有方法中抛出它,并在公共方法中处理异常,则可以使用它。

一个好的阅读材料是Oracle文档本身:http://download.oracle.com/javase/tutorial/essential/exceptions/runtime.html

为什么设计者决定强制指定在其范围内可能抛出的所有未捕获的已检查异常的方法?任何方法可能抛出的异常都是该方法的公共编程接口的一部分。调用方法的人必须知道方法可能抛出的异常,以便他们可以决定如何处理这些异常。这些异常与该方法的参数和返回值一样重要,是该方法的编程接口的一部分。
下一个问题可能是:“如果记录方法的API(包括它可以抛出的异常)很好,为什么不指定运行时异常呢?”运行时异常代表编程问题的结果,因此,API客户端代码不能合理地期望从中恢复或以任何方式处理它们。这些问题包括算术异常,例如除以零;指针异常,例如尝试通过空引用访问对象;以及索引异常,例如尝试通过太大或太小的索引访问数组元素。
还有一个重要的信息在Java语言规范中:
“throws子句中命名的已检查异常类是实现者和方法或构造函数的用户之间的合同的一部分。”
我的底线是你可以捕获任何RuntimeException,但你不需要这样做,实际上实现不需要保持相同的非检查异常抛出,因为它们不是合同的一部分。

谢谢。还有一个问题,当您冒泡异常时,我应该冒泡精确的异常还是使用“Exception”掩盖它。我正在对一些旧代码进行编写,而且“All over the places”都在冒泡“Exception”,我想知道这是否是正确的行为? - Thang Pham
1
@Harry 我会让比我更有经验的人来回答这个问题:https://dev59.com/LXRC5IYBdhLWcg3wG9Nb - Aleadam

10

1) 不是,NumberFormatException是未检查的异常。即使你捕获了它(你不需要这样做),因为它是未检查的。这是因为它是IllegalArgumentException的子类,而IllegalArgumentException是RuntimeException的子类。

2) RuntimeException是所有未检查异常的根源。RuntimeException的每个子类都是未检查的。除了Error之外的所有其他异常和Throwable都是已检查的(Error属于Throwable)。

3/4) 您可以警告用户选择了不存在的文件并要求其提供一个新文件。或者只需退出并告知用户他们输入了无效内容。

5) 抛出和捕获'Exception'是不良实践。但更一般地说,您可能会抛出其他异常,以便调用者可以决定如何处理它。例如,如果您编写了一个库来处理读取某些文件输入的方法,并且该方法传递了一个不存在的文件,则您不知道如何处理它。调用者想要再问还是退出?因此,您将异常抛回到调用者的堆栈。

在许多情况下,未检查的异常发生是因为程序员没有验证输入(在第一个问题中的NumberFormatException)。这就是为什么捕获它们是可选的,因为有更优雅的方法避免生成这些异常。


谢谢。还有一个问题,当您冒泡exception时,我应该冒泡确切的异常还是使用Exception进行掩码处理。我正在对一些旧代码进行编写,而且Exception在各个地方都被冒泡了。我想知道这是否是正确的行为? - Thang Pham
你可以让你的方法抛出 Exception(这并不理想),或者捕获 Exception 并抛出更好的 Exception(比如 IOException 等)。所有的 Exceptions 都可以在它们的构造函数中接受一个 Exception 作为“cause”,所以你应该使用它。 - dontocsata

9

运行时异常: 运行时异常也被称为未检查异常。其他所有异常都是已检查异常,它们不是从java.lang.RuntimeException派生的。

已检查异常: 必须在代码中某处捕获已检查异常。如果调用抛出已检查异常的方法,但您没有在任何地方捕获已检查异常,则代码将无法编译。这就是它们被称为已检查异常的原因:编译器检查以确保它们被处理或声明。

Java API中的许多方法都会抛出已检查异常,因此您经常需要编写异常处理程序来应对由您未编写的方法生成的异常。


9

Checked - 易发生,编译时检查。

例如.. FileOperations

UnChecked - 由于坏数据,运行时检查。

例如..

String s = "abc";
Object o = s;
Integer i = (Integer) o;

Exception in thread "main" java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Integer
    at Sample.main(Sample.java:9)

这里的异常是由于糟糕的数据引起的,无论如何都不可能在编译时确定。


6

检查异常是由JVM在编译时检查的,与资源(文件/数据库/流/套接字等)相关。检查异常的目的是,如果在编译时资源不可用,应用程序应该定义替代行为来处理捕获/最终块中的异常。

未检查异常纯粹是程序错误,错误的计算、空数据甚至业务逻辑故障都可能导致运行时异常。在代码中处理/捕获未检查异常是完全可以的。

说明取自http://coder2design.com/java-interview-questions/


5
为了回答最后一个问题(其他问题似乎已经得到了彻底的回答),“我应该上抛确切的异常还是使用 Exception 进行掩盖?”我认为您指的是这样的情况:
public void myMethod() throws Exception {
    // ... something that throws FileNotFoundException ...
}

不要总是声明最精确的异常或一系列这样的异常。你声明方法能够抛出的异常是你的方法和调用者之间契约的一部分。抛出 "FileNotFoundException" 意味着文件名可能无效且找不到文件;调用者需要聪明地处理它。抛出 "Exception" 意味着“嘿,事情总会发生。自己处理吧。” 这是非常糟糕的API。
在第一篇文章的评论中,有一些例子表明 "throws Exception" 是一个有效和合理的声明,但对于大多数你编写的“正常”代码来说,这并不是这种情况。

确切地说,将您的检查异常声明作为代码文档的一部分,并帮助使用您软件的人。 - Salvador Valencia

5

我最喜欢的有关未检查异常和已检查异常区别的描述来自Java教程文章“未检查异常-争议”(很抱歉在这篇文章中变得很基础,但是,嘿,基础有时候是最好的):

以下是底线指南:如果客户端可以合理地从异常中恢复,则将其作为已检查异常。如果客户端无法从异常中恢复,请将其作为未检查异常

“应该抛出什么类型的异常”的核心是语义(在某种程度上),上述引用提供了一个很好的准则(因此,我仍然对C#取消了已检查异常的想法感到震惊 - 特别是Liskov认为它们很有用)。

然后剩下的就变得逻辑化:编译器期望我明确哪些异常?你期望客户端从中恢复的那些异常。


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