Java中的例外处理
最后修改:3月7日,2020年3月7日
1.概述
在本教程中,我们将通过Java以及一些Gotchas的异常处理的基础知识。
2.第一原理
2.1。它是什么?
为了更好地了解异常和例外处理,让我们做出真实的比较。
想象一下,我们在网上订购了一件产品,但在途中出现了送货失败的情况。一个好的公司可以处理这个问题,优雅地重新安排我们的包裹,使它仍然按时到达。
同样,在Java中,代码在执行我们的指令时也会出现错误。好异常处理能处理错误并优雅地重新路由程序,给用户一个积极的体验吗。
2.2。为什么使用它呢?
我们通常在一个理想化的环境中编写代码:文件系统总是包含我们的文件,网络是健康的,JVM总是有足够的内存。有时我们称之为“幸福之路”。
但是,在生产环境中,文件系统可能会损坏,网络会崩溃,jvm会耗尽内存。代码的好坏取决于它如何处理“不愉快的路径”。
我们必须处理这些情况,因为它们会对应用程序的流程产生负面影响异常:
public static List getPlayers() throws IOException {Path Path = Paths.get(" Player .dat");List player = Files.readAllLines(path);return players.stream() .map(Player::new) .collect(Collectors.toList());}
这个代码选择不处理IoException.,而是将其传递到调用堆栈。在理想的环境中,代码运行良好。
但是生产过程中会发生什么players.dat不见了吗?
——player .dat文件在sun.nio.fs.WindowsException中不存在。在sun.nio.fs.WindowsException。rethrowAsIOException(未知源)//…更多的堆栈跟踪在java.nio.file.Files。readAllLines(未知来源)在java.nio.file.Files。<——getPlayers()被main()调用,在第19行
如果不处理此异常,其他运行正常的程序可能会一起停止运行!我们需要确保我们的代码在出现问题时有一个计划。
这里还要注意异常的另一个好处,那就是堆栈跟踪本身。由于这种堆栈跟踪,我们经常可以在不附加调试器的情况下查明有问题的代码。
3.异常层次结构
最终,异常是否所有的Java对象都是从Throwable:
——> Throwable <——| (checked) | | | | |——>异常错误| (checked) (unchecked) | RuntimeException (unchecked)
有三种主要的例外情况:
- 已检查的异常
- 未检查异常/运行时异常
- 错误
运行时异常和未检查异常指的是同一件事。我们经常可以互换使用它们。
3.1。已检查的异常
检查异常是Java编译器要求我们处理的例外情况。我们必须声明地抛出呼叫堆栈,或者我们必须自己处理。在片刻中的两者都有更多。
Oracle的文档当我们可以合理地期望方法的调用者能够恢复时,告诉我们使用受控异常。
检查异常的两个例子是IoException.和ServletException。
3.2。未经检查的异常
未检查异常是Java编译器执行的异常不需要我们来处理。
简单地说,如果我们创建了一个扩展的异常RuntimeException,它将不受控制;否则,它将被检查。
虽然这听起来很方便,Oracle的文档告诉我们,这两个概念都有很好的理由,例如区分情境错误(选中)和使用错误(未选中)。
一些未检查异常的例子是NullPointerException,IllegalArgumentException,和SecurityException。
3.3。错误
错误代表严重的,通常是无法恢复的条件,如库不相容,无限递归或内存泄漏。
即使它们没有延伸RuntimeException,他们也不受控制。
在大多数情况下,对我们来说处理、实例化或扩展是很奇怪的错误。通常,我们想让它们一直向上传播。
一些错误的例子是StackOverflowError和OutOfMemoryError。
4.处理异常
在Java API中,有足够的地方可能出错,其中一些地方标有例外,无论是签名还是javadoc
/** * @exception FileNotFoundException…*/ public Scanner(String fileName) throws FileNotFoundException{//…}
正如前面提到的,当我们称之为"危险"方法时,我们必须处理受控异常,然后我们五月处理未检查的。Java为我们提供了几种方法:
4.1。抛出
“处理”异常最简单的方法是重新抛出它:
public int getPlayerScore(String playerFile) throws FileNotFoundException{扫描器内容= new扫描器(new File(playerFile));返回Integer.parseInt (contents.nextLine ());}
因为filenotfoundException.是checked异常,这是最简单的满足编译器的方法,但是这意味着任何调用我们方法的人现在也需要处理它!
方法用于可以抛出一个NumberFormatException但因为它没有被检查,我们就没有必要去处理它。
4.2。try - catch
如果我们想要自己尝试处理异常,我们可以使用一个try - catch块。我们可以通过重新来处理我们的例外:
public int getPlayerScore(String playerFile) {try {Scanner contents = new Scanner(new File(playerFile));返回Integer.parseInt (contents.nextLine ());} catch (FileNotFoundException noFile){抛出新的IllegalArgumentException("File not found");}}
或通过执行恢复步骤:
public int getPlayerScore(String playerFile) {try {Scanner contents = new Scanner(new File(playerFile));返回Integer.parseInt (contents.nextLine ());} catch (FileNotFoundException noFile) {logger。warn("未找到文件,正在重置分数");返回0;}}
4.3。最后
现在,有些时候我们需要执行代码不管是否发生异常,这就是最后关键字进来。
在我们的例子中到目前为止,阴影中潜伏的令人讨厌的错误,这是默认情况下的Java不会将文件处理返回到操作系统。
当然,无论我们是否可以读取文件,我们都要确保我们做了适当的清理!
让我们首先尝试“懒惰”的方式:
public int getPlayerScore(String playerFile) throws FileNotFoundException{扫描器内容= null;try {contents = new Scanner(new File(playerFile));返回Integer.parseInt (contents.nextLine ());} finally {if (contents != null) {contents.close();}}}
在这里,这是最后块表示我们希望Java运行什么代码,而不管试图读取文件时发生了什么。
即使一个filenotfoundException.是抛出调用栈,Java会调用的内容吗最后之前这样做。
我们也都可以处理异常和确保我们的资源关闭:
public int getPlayerScore(String playerFile){扫描器内容;try {contents = new Scanner(new File(playerFile));返回Integer.parseInt (contents.nextLine ());} catch (FileNotFoundException noFile) {logger。warn("未找到文件,正在重置分数");返回0;} finally {try {if (contents != null) {contents.close();}} catch (IOException io) {logger。error("无法关闭阅读器!",io);}}}
因为关闭也是一个“有风险的”方法,我们还需要捕获它的异常!
这看起来可能相当复杂,但我们需要每一块来处理可能正确出现的每个潜在问题。
4.4。试一试与资源
幸运的是,在使用Java 7时,我们可以简化上面的语法AutoCloseable:
public int getPlayerScore(String playerFile) {try (Scanner contents = new Scanner(new File(playerFile))) {return Integer.parseInt(contents. nextline ());} catch (FileNotFoundException e) {logger。warn("未找到文件,正在重置分数");返回0;}}
当我们放置引用时AutoClosable在试一试声明,那么我们不需要自己关闭资源。
我们仍然可以用a最后但是,阻塞来执行我们想要的任何其他类型的清理。
查看我们的文章献给试一试与资源要学习更多的知识。
4.5。多个抓块
有时,代码可以抛出多个异常,我们可以拥有多个抓每个单独的块句柄:
public int getPlayerScore(String playerFile) {try (Scanner contents = new Scanner(new File(playerFile))) {return Integer.parseInt(contents. nextline ());catch(ioException e){logger.warn(“播放器文件不会加载!”,e);返回0;catch(numberformatexception e){logger.warn(“播放器文件已损坏!”,e);返回0;}}
多个捕获使我们有机会以不同的方式处理每个异常,如果需要的话。
还要注意我们没有抓住filenotfoundException.,这是因为它扩展IOException。因为我们捕获了IoException., Java将认为它的任何子类也被处理。
但是,我们说,我们需要对待filenotfoundException.不同于更一般的IoException.:
public int getPlayerScore(String playerFile) {try (Scanner contents = new Scanner(new File(playerFile))) {return Integer.parseInt(contents. nextline ());} catch (FileNotFoundException e) {logger。警告("播放器文件未找到!",e);返回0;catch(ioException e){logger.warn(“播放器文件不会加载!”,e);返回0;catch(numberformatexception e){logger.warn(“播放器文件已损坏!”,e);返回0;}}
Java让我们单独处理子类异常,记住把它们放在渔获列表的较高位置。
4.6。联盟抓块
当我们知道我们处理错误的方式将是相同的,但是,Java 7引入了在同一块中捕获多个异常的能力:
public int getPlayerScore(String playerFile) {try (Scanner contents = new Scanner(new File(playerFile))) {return Integer.parseInt(contents. nextline ());} catch (IOException | NumberFormatException e) {logger。warn("加载分数失败!",e);返回0;}}
5.投掷例外
如果我们不想自己处理异常,或者我们想生成异常供他人处理,那么我们需要熟悉扔关键词。
让我们说我们有以下检查异常,我们创建了自己:
public class TimeoutException extends Exception {public TimeoutException(String message) {super(message);}}
我们有一种方法可能需要很长时间才能完成:
公共列表<播放器> LoadAllPlayers(String Playersfile){// ...潜在的长操作}
5.1。抛出检查的异常
就像从方法返回一样扔在任何时候。
当然,当我们试图表示某事出了问题时,我们应该抛出:
公共列表<播放器> LoadAllPlayers(String PlayersFile)抛出超时频率{while(!toolong){// ...潜在的长操作}抛出新的超时曝光(“这个操作太长”);}
因为TimeoutException是查过了,我们还要用吗抛出关键字,以便方法的调用者知道如何处理它。
5.2。扔处理未检查的异常
如果我们想做一些类似的事情,例如,验证输入,我们可以使用未经检查的异常:
public List loadAllPlayers(String playersFile) throws TimeoutException {if(!isFilenameValid(playersFile)) {throw new IllegalArgumentException("Filename is not valid!");} / /……}
因为IllegalArgumentException.是未检查的,我们不需要标记方法,尽管我们欢迎这样做。
无论如何,一些标记方法作为文档的形式。
5.3。包裹和rethrowing.
我们也可以选择重新抛出已经捕获的异常:
public List loadAllPlayers(String playersFile) throws IOException {try{//…} catch (IOException io){抛出io;}}
或者做一个wrap和rethrow:
public List loadAllPlayers(String playersFile) throws playerloadexeption {try{//…} catch (IOException io){抛出新的PlayerLoadException(io);}}
这可以很好地将许多不同的例外合并为一个。
5.4。将Throwable或异常
现在来看一个特例。
如果给定代码块唯一可能引发的异常是无节制的异常,然后我们可以捕获并重新抛出Throwable或异常不需要将它们添加到我们的方法签名:
public List loadAllPlayers(String playersFile) {try {throw new NullPointerException();} catch(可抛出的t){抛出t;}}
虽然简单,但上面的代码不能抛出一个检查过的异常,因此,即使我们重新抛出一个检查过的异常,我们也不必用标记标记签名抛出条款。
这对于代理类和方法很方便。更多相关信金宝搏官网188be息可以在此找到在这里。
5.5。遗产
当我们用抛出关键字,它会影响子类如何覆盖我们的方法。
在我们的方法抛出检查异常的情况下:
public class Exceptions {public List loadAllPlayers(String playersFile) throws TimeoutException{//…}}
子类可以有一个“风险较小”的签名:
public class FewerExceptions extends Exceptions {@Override public List loadAllPlayers(String playersFile){//重载}}
但不是"更多的冒险者“签名:
public class MoreExceptions extends Exceptions {@Override public List loadAllPlayers(String playersFile) throws MyCheckedException {// override}}
这是因为契约是在编译时由引用类型确定的。如果我创建一个MoreExceptions并保存到例外:
Exceptions = new MoreExceptions();exceptions.loadAllPlayers(“文件”);
然后JVM只会告诉我抓这TimeoutException既然我这么说了,那就错了MoreExceptions # loadAllPlayers抛出一个不同的异常。
简单地说,子类可以抛出更少的比他们的超类检查了异常,但不是更多的。
6.反模式
6.1。吞咽例外
现在,还有另一种方法可以满足编译器的要求:
public int getPlayerScore(String playerFile) {try{//…} catch(异常e) {} // <== catch和swallow返回0;}
上面的叫做吞咽一个例外。大多数时候,这样做对我们来说并不意味着什么,因为这并没有解决问题和它也使其他代码无法处理这个问题。
有时会出现一个我们确信不会发生的检查异常。在这些情况下,我们至少应该添加一条注释,说明我们有意地吃掉了异常:
public int getPlayerScore(String playerFile) {try{//…} catch (IOException e){//这永远不会发生}}
另一种“吞下”异常的方法是简单地将异常打印到错误流中:
public int getPlayerScore(String playerFile) {try{//…} catch(异常e) {e. printstacktrace ();}返回0;}
我们已经稍微改善了我们的情况,至少把错误写出来供以后诊断。
不过,我们最好还是用伐木工:
public int getplayerscore(string playerfile){try {// ...} catch(ioException e){logger.error(“无法加载得分”,e);返回0;}}
虽然以这种方式处理异常非常方便,但我们需要确保没有吞食代码的调用者可以用来纠正问题的重要信息。
最后,当抛出一个新的异常时,我们可以通过不把它作为原因而无意中吞下一个异常:
public int getPlayerScore(String playerFile) {try{//…} catch (IOException e){抛出新的PlayerScoreException();}}
在这里,我们赞扬自己提醒调用者错误,但是我们没有包括IoException.的原因。因此,我们丢失了呼叫者或运营商可以用于诊断问题的重要信息。
我们最好这样做:
public int getplayerscore(string playerfile){try {// ...} catch(ioException e){投掷新的playerscoreexception(e);}}
请注意包含的细微差别IoException.作为导致的PlayerScoreException。
6.2。使用返回在一个最后块
另一种吞咽异常的方法是返回从最后块。这是不好的,因为通过突然返回,JVM将删除异常,即使它被我们的代码抛出:
public int getPlayerScore(String playerFile) {int score = 0;try{抛出新的IOException();} finally {return score;// <== IOException被删除}}
根据Java语言规范:
如果由于任何其他原因,try块的执行突然结束R.,然后执行finally块,然后有一个选择。
如果最后块正常完成,然后由于原因R, try语句突然完成。
如果最后block由于原因S突然完成,然后try语句由于原因S突然完成(原因R被丢弃)。
6.3。使用扔在一个最后块
类似于使用返回在一个最后块中抛出的异常最后块将优先于catch块中出现的异常。
函数的初始异常将被“擦除”试一试布洛克,我们就失去了所有有价值的信息
public int getplayerscore(string playerfile){try {// ...} catch(ioException io){oppl nefilealstateexception(io);// <==最终被吃掉了}最后{抛出新的otherexception();}}
6.4。使用扔作为一个转到
一些人也屈服于使用的诱惑扔作为一个转到声明:
public void doSomething() {try{//一堆代码抛出新的MyException();catch (MyException e){//第三束代码}}
这很奇怪,因为代码试图使用异常进行流控制,而不是错误处理。
7.常见异常和错误
下面是一些我们经常会遇到的例外和错误:
7.1。已检查的异常
- IoException.-这个异常通常是一种表示网络、文件系统或数据库上的某些东西失败的方式。
7.2。runtimeexception
- ArrayIndexOutOfBoundsException- 此异常意味着我们尝试访问不存在的数组索引,如尝试从长度3的数组获取索引5时。
- ClassCastException。这个异常意味着我们试图执行非法的类型转换,例如试图转换细绳成一个列表。我们通常可以通过防御来避免它运算符检查前铸造。
- IllegalArgumentException.- 此异常是一种通用方式,可以说提供的方法或构造函数参数之一无效。
- IllegalStateException- 此异常是一种通用方式,我们可以说我们的内部状态,如我们对象的状态,无效。
- NullPointerException- 这个例外意味着我们尝试参考a零对象。我们通常可以通过采取防御措施来避免它零支票或使用可选的。
- NumberFormatException- 这个例外意味着我们试图转换a细绳进入一个数字,但字符串包含非法字符,例如尝试将“5f3”转换为数字。
7.3。错误
- StackOverflowError -此异常意味着堆栈跟踪太大。这种情况有时会发生在大规模应用程序中;然而,这通常意味着我们的代码中发生了无限递归。
- noclassdeffounderror- 此异常意味着由于未在类路径上或由于静态初始化的故障而无法加载,因此无法加载。
- OutOfMemoryError—这个异常意味着JVM没有更多的内存可以分配给更多的对象。有时,这是由于内存泄漏造成的。
8.结论
在本文中,我们介绍了异常处理的基础知识,以及一些好的和不好的实践示例。
一如既往,可以找到本文中的所有代码在GitHub!!