第八章 异常处理
第八章 异常处理
1 认识 Java 的异常
1.1、什么是异常
在使用计算机语言进行项目开发的过程中,即使程序员把代码写得尽善尽美,在系统的运行过程中仍然会遇到一些问题,因为很多问题不是靠代码能够避免的,比如:客户输入数据的格式问题,读取文件是否存在,网络是否始终保持通畅等等。
- 异常 :指的是程序在执行过程中,出现的非正常的情况,如果不处理最终会导致 JVM 的非正常停止。
异常指的并不是语法错误,语法错了,编译不通过,不会产生字节码文件,根本不能运行。
异常也不是指逻辑代码错误而没有得到想要的结果,例如:求a与b的和,你写成了a-b
1.2、如何对待异常
程序员在编写程序时,就应该充分考虑到各种可能发生的异常和错误,极力预防和避免,实在无法避免的,要编写相应的代码进行异常的检测、异常消息的提示,以及异常的处理。
1.3、异常的抛出机制
Java 中是如何表示不同的异常情况,又是如何让程序员得知,并处理异常的呢?
Java 中把不同的异常用不同的类表示,一旦发生某种异常,就通过创建该异常类型的对象,并且抛出,然后程序员可以 catch 到这个异常对象,并处理,如果无法 catch 到这个异常对象,那么这个异常对象将会导致程序终止。
运行下面的程序,程序会产生一个数组索引越界异常 ArrayIndexOfBoundsException 。
1 | public class ArrayTools { |
Java 运行时系统会沿着调用栈向上查找,看是否有代码块可以处理这个异常。这个查找过程从抛出异常的方法开始,一直向上直到 main 方法。如果有可以处理异常的代码块,就把异常对象交给这个代码块处理。这个代码块被称为“异常处理器”或“catch 块”。
1.4、异常的继承体系
Java 的异常继承体系是基于Java类继承机制构建的,所有的异常类都继承自java.lang.Throwable
类。Throwable
类有两个主要的直接子类:java.lang.Error
和java.lang.Exception
。
java.lang.Error
:这是Java运行时系统内部错误和资源耗尽错误,比如OutOfMemoryError
(内存溢出错误)和StackOverflowError
(栈溢出错误)等。这些错误通常是Java虚拟机(JVM)无法或不应该尝试捕获的严重问题。java.lang.Exception
:这是应用程序需要捕获并处理的异常。Exception
类及其子类表示程序本身可以预见的、可以处理的问题,比如IOException
(输入/输出异常)、SQLException
(SQL异常)等。
Exception
类进一步细分为两类:
检查型异常(Checked Exceptions):这是编译器要求必须捕获或声明的异常。检查型异常通常表示程序员的错误,比如文件找不到、网络不可用等。常见的检查型异常有
IOException
、ClassNotFoundException
等。运行时异常(Runtime Exceptions):这是编译器不要求必须捕获的异常,它们通常是程序运行时的逻辑错误,比如
NullPointerException
(空指针异常)、ArrayIndexOutOfBoundsException
(数组越界异常)等。运行时异常是RuntimeException
类或其子类的实例。
在 Java 的异常处理中,使用try
块来包含可能抛出异常的代码,catch
块来捕获并处理异常,finally
块来包含无论是否发生异常都需要执行的代码。此外,还可以使用 throw
关键字来显式抛出异常,以及使用 throws
关键字在方法签名中声明方法可能抛出的异常。
2 异常的处理
2.1 概述
在 Java 中,处理异常通常涉及到使用try
, catch
, finally
, 和 throw
关键字。下面是如何处理异常的几个步骤:
使用
try
块来包围可能抛出异常的代码:
当你预期某个代码块可能会抛出异常时,你应该将该代码块放在try
块中。使用
catch
块来捕获并处理异常:catch
块紧跟在try
块之后,用于捕获try
块中抛出的异常。你可以指定要捕获的异常类型,并在catch
块中处理该异常。使用多个
catch
块来处理不同类型的异常:
你可以使用多个catch
块来捕获和处理不同类型的异常。每个catch
块应该处理一种特定类型的异常。使用
finally
块来执行清理操作:finally
块是可选的,它包含的代码无论是否发生异常都会被执行。通常用于资源清理,如关闭文件、数据库连接等。使用
throw
关键字来抛出异常:
如果你认为某个方法无法处理特定的异常,你可以使用throw
关键字来抛出该异常,将其传递给调用者处理。
下面是一个简单的异常处理示例:
1 | public class ExceptionHandlingExample { |
在这个示例中,divide
方法会检查除数是否为零,如果是,则抛出一个ArithmeticException
。在main
方法中,我们尝试调用divide
方法,并使用try-catch
块来捕获可能抛出的异常。finally
块确保无论是否发生异常,都会执行清理代码。
注意,如果异常没有在try-catch
块中被捕获,它将被传递给调用者,直到被捕获或最终由Java运行时系统处理。如果异常类型匹配某个catch
块,那么该catch
块的代码将被执行,异常得到处理。如果没有catch
块匹配抛出的异常类型,或者没有catch
块来处理异常,程序将终止,并打印堆栈跟踪信息。
2.2 try…catch…
捕获异常语法如下:
1 | try{ |
try{} 中编写可能发生某些异常的业务逻辑代码。
catch 分支,分为两个部分,catch() 中编写异常类型和异常参数名,{}中编写如果发生了这个异常,要做什么处理的代码。如果有多个 catch 分支,并且多个异常类型有父子类关系,必须保证小的子异常类型在上,大的父异常类型在下。
当某段代码可能发生异常,不管这个异常是编译时异常(受检异常)还是运行时异常(非受检异常),我们都可以使用 try 块将它括起来,并在 try 块下面编写 catch 分支尝试捕获对应的异常对象。
- 如果在程序运行时,try 块中的代码没有发生异常,那么 catch 所有的分支都不执行。
- 如果在程序运行时,try 块中的代码发生了异常,根据异常对象的类型,将从上到下选择第一个匹配的catch 分支执行。此时 try 中发生异常的语句下面的代码将不执行,而整个 try…catch 之后的代码可以继续运行。
- 如果在程序运行时,try块中的代码发生了异常,但是所有 catch 分支都无法匹配(捕获)这个异常,那么 JVM 将会终止当前方法的执行,并把异常对象“抛”给调用者。如果调用者不处理,程序就挂了。
示例:
1 | public class TestTryCatch { |
2.3 finally
因为异常会引发程序跳转,从而会导致有些语句执行不到。而程序中有一些特定的代码无论异常是否发生,都需要执行。例如,IO流的关闭,数据库连接的断开等。这样的代码通常就会放到 finally 块中。
1 | try{ |
注意: finally 不能单独使用。
当只有在 try 或者 catch 中调用退出 JVM 的相关方法,例如 System.exit(0),此时 finally 才不会执行,否则 finally 永远会执行。
示例:
1 | import java.util.InputMismatchException; |
finally 与 return 的关系
在 Java 中,finally
块与 return
语句之间的关系是一个常见的误解点。让我们首先明确这两者是如何工作的,然后详细讨论它们之间的关系。
finally
块:
finally
块是try-catch-finally
结构的一部分。- 无论
try
块或catch
块中的代码是否抛出异常或正常执行完毕,finally
块中的代码总是会被执行。 - 这意味着,即使
try
或catch
块中有return
语句,finally
块中的代码也会执行。
return
语句:
return
语句用于从方法中返回一个值。- 当
return
语句被执行时,方法会立即结束,并返回指定的值(如果有的话)。
现在,让我们讨论 finally
块和 return
语句之间的关系:
- **在
try
或catch
块中的return
**:
当你在 try
或 catch
块中使用 return
语句时,方法会开始退出过程。这意味着,在方法退出之前,finally
块会被执行。
这意味着,finally
块中的任何代码都可能改变 return
语句返回的值。这通常是不推荐的,因为它可能会导致代码难以理解和维护。但是,Java 确实允许这样做。
1 | public int exampleMethod() { |
在上述示例中,尽管 try
块尝试返回 10
,但由于 finally
块中的 return 20
,方法最终返回 20
。
- **在
finally
块中的return
**:
如果在 finally
块中使用了 return
语句,那么它会覆盖 try
或 catch
块中的任何 return
语句。这意味着,无论 try
或 catch
块中的代码如何,finally
块中的 return
语句都会决定方法的最终返回值。
1 | public int exampleMethod() { |
在上述示例中,无论 try
或 catch
块中的代码如何,方法最终都会返回 30
,因为 finally
块中的 return
语句决定了最终的结果。
总之,finally
块和 return
语句之间的关系是,finally
块总是会被执行,并且它中的 return
语句可以覆盖 try
或 catch
块中的 return
语句。因此,在设计代码时,应该小心处理这种关系,以避免意外的行为或难以追踪的错误。
2.4 throw 与 throws
在 Java 中,抛出异常通常涉及两个步骤:首先,你需要创建一个异常对象,然后,你需要使用 throw
关键字来抛出这个异常对象。异常通常是由程序中的错误或异常情况触发的,它们可以是由 Java 运行时环境自动抛出的,也可以是由程序员显式抛出的。
下面是一个简单的例子,展示了如何在 Java 中抛出异常:
1 | public class CustomExceptionExample { |
在这个例子中,methodThatThrowsException
方法可能会抛出一个 CustomException
。当 condition
不满足时,我们创建了一个 CustomException
对象,并使用 throw
关键字将其抛出。
注意,如果一个方法可能会抛出异常,但又不处理它,那么该方法必须使用 throws
关键字来声明它可能会抛出的异常。这样,调用这个方法的代码就知道需要处理或继续向上抛出这个异常。
在上面的 main
方法中,我们调用了 methodThatThrowsException
并使用 try-catch
块来处理 CustomException
。如果 methodThatThrowsException
抛出了异常,异常将被捕获并打印出异常信息。
此外,异常类通常继承自 java.lang.Exception
(表示检查型异常)或 java.lang.RuntimeException
(表示未检查型异常)。自定义异常类可以根据需要选择继承自这两者之一。
区别
在 Java 中,throw
和 throws
是两个与异常处理相关的关键字,它们之间有着明显的区别:
用途:
throw
关键字用于在方法内部显式地抛出一个异常对象。它表示一个具体的异常抛出动作,用于在代码中主动抛出异常。throws
关键字则用于在方法签名中声明该方法可能会抛出的异常类型。它表示一种状态,告诉方法的调用者该方法可能会抛出哪些异常,让调用者做好相应的异常处理准备。
位置:
throw
关键字出现在方法体内部,用于在适当的时候抛出异常。throws
关键字出现在方法头(函数头)中,紧跟在方法名后面,用于声明方法可能抛出的异常类型。
异常处理:
throw
关键字后面通常跟着一个异常对象,表示抛出该异常对象。执行到throw
语句时,程序会立即停止执行当前方法,并将异常对象传递给调用者处理。throws
关键字用于声明方法可能会抛出的异常类型,但它不会直接抛出异常。它只是一种声明,告诉方法的调用者该方法可能会抛出哪些异常,具体的异常处理由调用者负责。
异常类型:
throw
可以抛出任何类型的异常对象,包括自定义异常。throws
用于声明方法可能会抛出的异常类型,可以声明一个或多个异常类型,多个异常类型之间用逗号分隔。
总结来说,throw
是用于在方法内部具体抛出异常的关键字,而 throws
是用于在方法签名中声明该方法可能会抛出的异常类型的关键字。在编写 Java 程序时,根据具体需求选择使用 throw
还是 throws
,以确保异常得到妥善处理。