Java异常处理
异常是程序执行期间发生的意外事件。它影响程序指令的流程,这可能导致程序异常终止。
出于多种原因可能会发生异常。他们之中有一些是:
- 无效的用户输入
- 设备故障
- 网络连接丢失
- 物理限制(磁盘内存不足)
- 代码错误
- 打开一个不可用的文件
Java异常层次结构
这是Java中异常层次结构的简化图:
Throwable
类是层次结构中的根类。请注意,层次结构分为两个分支:Error
和Exception
。
Errors
错误表示不可恢复的情况,例如Java虚拟机(JVM)内存不足,内存泄漏,堆栈溢出错误,库不兼容,无限递归等.
错误通常是程序员无法控制的,我们不应该尝试处理错误。
Exceptions
程序可以捕获并处理异常。
当方法中发生异常时,它将创建一个对象。该对象称为异常对象。
它包含有关异常的信息,例如,异常的名称和说明以及发生异常时程序的状态。
Java异常类型
异常层次结构还具有两个分支:RuntimeException
和IOException
。
RuntimeException
由于编程错误而发生运行时异常,它们也称为非检查异常。
这些异常不在编译时检查,而是在运行时检查.一些常见的运行时异常是:
- API使用不当-
IllegalArgumentException
- 空指针访问(缺少变量的初始化)-
NullPointerException
- 越界数组访问 -
ArrayIndexOutOfBoundsException
- 将数字除以0 -
ArithmeticException
你可以这么理解: “如果是运行时异常,那是您的错”。如果在使用变量之前检查了变量是否已初始化,则不会发生NullPointerException
。
如果针对数组范围测试了数组索引,则不会发生ArrayIndexOutOfBoundsException
。
IOException
IOException
也称为检查异常。它们由编译器在编译时检查,并提示程序员处理这些异常。
受检查的异常的一些示例是:
- 尝试打开不存在的文件会导致
FileNotFoundException
- 尝试读取文件末尾
Java异常处理
异常是程序执行期间发生的意外事件
捕捉和处理异常
在Java中,我们使用异常处理程序组件try
, catch
和finally
块来处理异常
为了捕获和处理异常,我们在可能会生成异常的代码周围放置try ... catch ... finally
块
,finally
块是可选的.
1 | try { |
try…catch block
可能产生异常的代码放在try
块中
每个try
块应紧随catch
或finally
块,发生异常时,它会被紧随其后的catch
块捕获。
catch
块不能单独使用,并且必须始终在try
块之前。
1 | class Main { |
- 我们在try块中将数字除以0。这将产生
ArithmeticException
- 发生异常时,程序将跳过
try
块中的其余代码 - 在这里,我们创建了一个
catch
块来处理ArithmeticException
,因此,执行catch
块内的语句。
如果try块中的所有语句均未生成异常,则将跳过catch块.
多个catch 模块
对于每个try
块,可以有零个或多个catch
块.
每个catch
块的参数类型指示其可以处理的异常类型,多个catch
块使我们能够以不同方式处理每个异常。
1 | class ListOfNumbers { |
在此示例中,声明了一个大小为10的整数arrayOfNumbers
数组,我们知道数组索引总是从0开始。因此,当我们尝试为索引10分配一个值时,就会发生IndexOutOfBoundsException
,因为arrayOfNumbers
的数组范围是0到9。
当try块中发生异常时:
- 异常被引发到第一个
catch
块。第一个catch
块不处理IndexOutOfBoundsException
,因此将其传递到下一个catch块。 - 上面示例中的第二个
catch
块是适当的异常处理程序,因为它处理IndexOutOfBoundsException
。
Java finally
对于每个try
块,只能有一个finally
块,finally
块是可选的。但是,如果已定义,它将始终执行(即使不会发生异常)。
如果发生异常,则在try ... catch
块之后执行该异常。如果没有异常发生,则在try
块之后执行。
1 | try { |
1 | class Main { |
在此示例中,数字除以0。这引发了一个由catch
块捕获的ArithmeticException
,finally
块始终执行。
进行最后finally
被认为是一种好习惯。这是因为在此可以进行代码清理的动作,例如:
- 返回,继续或中断语句可能意外跳过的代码
- 关闭文件或连接
我们已经提到,finally
总是执行,通常就是这种情况。但是,在某些情况下finally
块不执行:
- 使用
System.exit()
方法 finally
块中发生异常- 线程挂掉
举一个例子,我们尝试使用FileWriter
创建一个新文件,然后使用PrintWriter
将数据写入其中。
1 | import java.io.*; |
当您运行此程序时,可能会发生两种可能性:
try
块中发生异常try
块正常执行
创建新的FileWriter
时可能会发生异常。如果无法创建或写入指定的文件,则抛出IOException
。
当发生异常时,将获得以下输出:
1 | Entering try statement |
当未发生异常且try
块正常执行时,将获得以下输出:
1 | Entering try statement |
创建一个OutputFile.txt
,它将具有以下内容:
1 | Value at: 0 = 0 |
try … catch详细细节
上述示例的帮助下详细了解异常处理的流程。
上图描述了在创建新FileWriter
时发生异常时的程序执行流程。
- 为了进入发生异常的方法,
main
方法中调用writeList()
方法,然后再调用FileWriter()
方法来创建新的OutputFile.txt
文件。 - 发生异常时,运行时系统将跳过
try
块中的其余代码。 - 它开始以相反的顺序搜索调用堆栈,以找到合适的异常处理程序。
- 在这里,
FileWriter
没有异常处理程序,因此运行时系统检查调用堆栈中的下一个方法,即writeList
。 writeList
方法有两个异常处理程序:一个处理IndexOutOfBoundsException
,另一个处理IOException
。- 然后,系统按顺序处理这些处理程序。
- 此示例中的第一个处理程序处理
IndexOutOfBoundsException
这与try
块引发的IOException
不匹配。 - 因此,将检查下一个处理程序,即
IOException
处理程序这与抛出的异常类型匹配,因此执行catch
块中的代码。 - 执行异常处理程序后,将执行
finally
块。 - 在这种情况下,由于
FileWriter
中发生异常,因此PrintWriter
对象out
永远不会打开,因此不需要关闭。
现在,让我们假设在运行该程序时没有发生异常,并且try
块正常执行。在这种情况下,将创建并写入一个OutputFile.txt
。
众所周知,无论异常处理如何,都将执行finally
块。由于没有发生异常,因此PrintWriter
是打开的,则需要关闭。这是通过finally
块中的out.close()
语句完成的。
捕获多个异常
从Java SE 7和更高版本开始,我们现在可以通过一个catch块捕获不止一种类型的异常。
这样可以减少代码重复并提高代码的简单性和效率。
catch
块可以处理的每种异常类型都使用竖线|分隔。
1 | try { |
try-width-resources 语句
try-with-resources
语句是一种try语句,具有一个或多个资源声明。
1 | try (resource declaration) { |
资源是在程序结束时要关闭的对象。必须在try语句中声明和初始化它。
1 | try (PrintWriter out = new PrintWriter(new FileWriter(“OutputFile.txt”)) { |
try-with-resources
语句也称为自动资源管理。该语句在语句末尾自动关闭所有资源。
throw 和 throws
在Java中,异常可以分为两种类型:
Unchecked Exceptions: 它们不是在编译时检查的,而是在运行时检查的。例子:
ArithmeticException
,NullPointerException
,ArrayIndexOutOfBoundsException
,Error
类下的异常等Checked Exceptions: 它们在编译时检查.例子:
IOException
,InterruptedException
.
通常,我们不需要处理Unchecked Exceptions
的异常。因为Unchecked Exceptions
是代码逻辑有问题.最好的习惯是,解决它而不是捕获它.
throws 关键字
我们在方法声明中使用throws
关键字来声明其中可能发生的异常的类型。
1 | accessModifier returnType methodName() throws ExceptionType1, ExceptionType2 … { |
从上面的语法可以看到:使用throws
声明多个异常。
1 | import java.io.*; |
当我们运行该程序时,如果文件test.txt
不存在,则FileInputStream
抛出一个FileNotFoundException
,它扩展了IOException
类。
如果方法不处理异常,必须在throws
子句中指定其中可能发生的异常的类型,以便调用堆栈中更远的方法可以处理它们。
findFile()
方法指定可以抛出IOException
。main()
方法调用此方法并处理抛出的异常。
引发多个异常
这是我们可以使用throws
关键字引发多个异常的方法。
1 | import java.io.*; |
findFile()
方法指定它可以在其throws
子句中引发NullPointerException
,IOException
和InvalidClassException
。
请注意我们尚未处理NullPointerException
.因为它是unchecked exception
.不必在throws
子句中指定它并进行处理。
throws VS try…catch…finally
可能有几种方法可能导致异常。为每种方法编写try ... catch
会很乏味,并且代码会变得很长且可读性较差。
当你在当前方法并不想处理异常, 但你检测了有可能的异常,那么throws
是个很有用.
throw 关键字
throw
关键字用于显式引发单个异常
引发异常时,程序执行流程从try
块转移到catch
块
1 | throw throwableObject; |
Throwable
对象是Throwable
类或Throwable
类的子类的实例。
1 | class Main { |
输出:
1 | Exception in thread "main" java.lang.ArithmeticException: Trying to divide by 0 |
在此示例中,我们显式抛出ArithmeticException
。
注意: ArithmeticException
是unchecked exception
,通常没有必要处理。
1 | import java.io.*; |
输出:
1 | File not found |
findFile()
方法会引发IOException
以及我们传递给其构造函数的消息
请注意由于它是一个checked exception
,因此必须在throws
子句中指定它
调用此findFile()
方法的方法需要处理此异常,或者自己使用throws关键字指定它
我们已经在main()
方法中处理了这个异常。引发异常时程序执行流程从try
块转移到catch
块。因此将跳过try
块中的其余代码,并执行catch
块中的语句
捕获多个异常
在Java 7之前即使存在代码冗余,我们也必须针对不同类型的异常编写多个异常处理代码。
1 | class Main { |
在此的示例可能会发生两个异常:
ArithmeticException
因为我们试图将数字除以0.ArrayIndexOutOfBoundsException
因为我们已经声明了一个新的整数数组,其数组边界为0到9,并且我们试图为索引10分配一个值
我们正在两个catch块中打印出异常消息,即重复代码
一个catch 捕获多个异常
在Java SE 7和更高版本中,我们现在可以在单个catch块中捕获不止一种类型的异常
catch
块可以处理的每种异常类型都使用竖线或竖线|
分隔
1 | try { |
1 | class Main { |
在单·catch
块中捕获多个异常可以减少代码重复并提高效率。
编译该程序时生成的字节码将比具有多个捕获块的程序小,因为没有代码冗余。
注意: 如果catch
块处理多个异常,则catch
参数隐式为final
。这意味着我们无法分配任何值来捕获参数
捕获基础异常
当在单个catch
块中捕获多个异常时,该规则一般化为专门化。
这意味着,如果catch
块中存在异常的层次结构,我们只能捕获基本异常,而不是捕获多个专门的异常。
1 | class Main { |
我们知道所有异常类都是Exception
类的子类。因此我们不必捕获多个专门的异常,而只需捕获Exception
类。
如果在catch
块中已经指定了基本异常类,则不要在同一catch
块中使用子异常类。否则我们会得到一个编译错误。
1 | class Main { |
输出:
1 | Main.java:6: error: Alternatives in a multi-catch statement cannot be related by subclassing |
在此示例中,ArithmeticException
和`ArrayIndexOutOfBoundsException都是Exception类的子类,所以编译错误.
try-with-resources
try-with-resources
语句末尾自动关闭所有资源.资源是程序结束时要关闭的对象。
1 | try (resource declaration) { |
从上面的语法可以看出,我们通过以下方式声明try-with-resources
语句:
- 在
try
子句中声明和实例化资源。 - 指定并处理关闭资源时可能引发的所有异常。
注意: try-with-resources
语句关闭实现AutoCloseable
接口的所有资源。
1 | import java.io.*; |
如果test.txt
文件没找到:
1 | IOException in try-with-resources block =>test.txt (No such file or directory) |
如果test.text
文件找到
1 | Entering try-with-resources block |
在此示例中,使用BufferedReader
的实例从test.txt
文件读取数据
在try-with-resources
语句中声明并实例化BufferedReader
可以确保关闭其实例,而不管try
语句是正常完成还是引发异常。
如果发生异常,则可以使用异常处理块或throws
关键字对其进行处理。
被抑制异常
在上面的示例中,在以下情况下,可以从try-with-resources
语句引发异常:
- 找不到文件
test.txt
- 关闭
BufferedReader
对象
也可以从try
块中引发异常,因为文件读取可能随时因多种原因而失败。
如果try
块和try-with-resources
语句都抛出了异常,将抛出try
块中的异常,并抑制try-with-resources
语句中的异常。
检查被抑制的异常
在Java 7和更高版本中,可以通过从try
块引发的异常中调用Throwable.getSuppressed()
方法来检索受抑制的异常。
此方法返回所有抑制的异常的数组。我们在catch
块中得到了抑制的异常。
1 | catch(IOException e) { |
try-with-resources
的优势
finally
块不需要关闭资源
在Java 7引入此功能之前,我们必须使用finally
块来确保关闭资源以避免资源泄漏。
1 | import java.io.*; |
输出:
1 | Entering try block |
从上面的示例可以看出,使用finally
块来清理资源使代码更加复杂。
还要注意finally
块中的try ... catch
块吗?这是因为在关闭此finally
块内的BufferedReader
实例时也可能发生IOException
,因此也将其捕获并处理。
try-with-resources
语句执行自动资源管理,我们不需要显式关闭资源,因为JVM会自动关闭它们。这使代码更具可读性,更易于编写。
try-with-resources
获取多资源
我们可以在try-with-resources
语句中声明多个资源,方法是用;
将它们分开.
1 | import java.io.*; |
如果该程序执行时未生成任何异常,Scanner
对象从testRead.txt文件中读取一行并将其写入新的testWrite.txt文件中。
进行多个声明时,try-with-resources
语句以相反的顺序关闭这些资源。在此示例中,首先关闭PrintWriter
对象,然后关闭Scanner
对象。
Java 9 增强 try-with-resources
在Java 7中,try-with-resources
语句受到限制,该资源需要在其块内本地声明。
1 | try (Scanner scanner = new Scanner(new File("testRead.txt"))) { |
如果我们在Java 7中在块外声明资源,它会生成一条错误消息。
1 | Scanner scanner = new Scanner(new File("testRead.txt")); |
处理此错误,Java 9改进了try-with-resources
语句,因此即使未在本地声明资源的引用,也可以使用。上面的代码现在将执行,没有任何编译错误。
作者: Fynn
链接: https://fynn90.github.io/2020/11/14/Java%E5%BC%82%E5%B8%B8%E5%A4%84%E7%90%86/
本文采用知识共享署名-非商业性使用 4.0 国际许可协议进行许可