Java异常处理
异常(exception)是在运行程序时产生的一致异常情况,大部分主流编程语言都提供了异常处理机制。
java中的异常又称为意外,是一个程序在他的执行器渐渐发生的实践,它终端正在执行程序的正常流。
在Java内部中一个异常的产生,主要有以下三个方面:
- Java内部错误发生异常,虚拟机产生的异常。
- 编写程序中代码的错误所产生的异常,例如:空指针异常、数组越界异常等。
- 通过throe语句手动产生的异常,一般用来告知该方法的调用者一些必要信息。
我们把在生成对象,并把它交给运行时系统的过程称为抛出(throw)异常。运行时在系统的方法调用栈中查找,直到能找到处理该类型异常的对象,这一个过程称之为捕获(catch)异常
异常类型
为了能有效的处理程序中的运行错误,Java专门引入了异常类。子啊Java中所有的异常类都是内置类java.lang.Throwable类的子类,即Throwable类位于异常类层次结构的顶层。
图10-0 异常结构图
运行时异常都是 RuntimeException 类及其子类异常,如 NullPointerException、IndexOutOfBoundsException 等,这些异常是不检查异常,程序中可以选择捕获处理,也可以不处理。这些异常一般由程序逻辑错误引起,程序应该从逻辑角度尽可能避免这类异常的发生。
非运行时异常是指 RuntimeException 以外的异常,类型上都属于 Exception 类及其子类。从程序语法角度讲是必须进行处理的异常,如果不处理,程序就不能编译通过。如 IOException、ClassNotFoundException 等以及用户自定义的 Exception 异常(一般情况下不自定义检查异常)。
Java中的Error和Exception
Error和Exception都是java.lang.Throwable类的子类,所以在Java代码中只有继承了Throwable类的实例才能被throw或者catch。
关于Exception和Error体现了Java平台设计者对不同异常情况的分类,Exception之程序在正常运行过程中可以预料到的意外情况,并且是可以被开发者捕获的,并对其进行相应的处理。Error是指正常情况下 不大可能出现的情况,绝大部分的Error都会导致程序出于非正常、不可恢复的状态。多以不需要开发者捕获。
如下时常见的Error和Exception:
- 运行时异常(RuntimeException):
- NullPropagation:空指针异常;
- ClassCastException:类型强制转换异常
- IllegalArgumentException:传递非法参数异常
- IndexOutOfBoundsException:下标越界异常
- NumberFormatException:数字格式异常
- 非运行时异常:
- ClassNotFoundException:找不到指定 class 的异常
- IOException:IO 操作异常
- 错误(Error):
- NoClassDefFoundError:找不到 class 定义异常
- StackOverflowError:深递归导致栈被耗尽而抛出的异常
- OutOfMemoryError:内存溢出异常
以下异常会导致Java产生栈溢出异常错误:
1 | // 通过无限递归来演示 |
Java异常处理机制及异常处理的基本结构
Java的异常处理通过5个关键字来实现:try、catch、throws和finally。try catch用于捕获并处理异常,finally语句用于在任何情况下(除特殊情况外)都必须执行的代码,throw语句用于抛出异常,throws语句用于声明可能会出现的异常。
Java 的异常处理机制提供了一种结构性和控制性的方式来处理程序执行期间发生的事件。异常处理的机制如下:
- 在方法中用 try catch 语句捕获并处理异常,catch 语句可以有多个,用来匹配多个异常。
- 对于处理不了的异常或者要转型的异常,在方法的声明处通过 throw 语句拋出异常,即由上层的调用方法来处理。
以下代码是异常处理程序的基本结构:
1 | try{ |
Java try-catch语句详解
try-catch的语法格式如下:
1 | try{ |
try-catch与if-else语句不一样,try后面的花括号{}
不可以省略,即使try块里的只有一行代码,也不可以省略这个花括号。与之类似的是,catch块后的花括号{}
也不可以省略。另外,try块中声明的变量只是代码块内的局部变量,它旨在try块内有效,其他地方不能访问该变量。
在上面语法的处理代码块 1 中,可以使用以下 3 个方法输出相应的异常信息。
- printStackTrace() 方法:指出异常的类型、性质、栈层次及出现在程序中的位置。
- getMessage() 方法:输出错误的性质。
- toString() 方法:给出异常的类型与性质。
下面给出打印错误信息的案例:
1 | import java.util.Scanner; |
上述代码在 main() 方法中使用 try catch 语句来捕获异常,将可能发生异常的age = scanner.nextlnt();
代码放在了 try 块中,在 catch 语句中指定捕获的异常类型为 Exception,并调用异常对象的 printStackTrace() 方法输出异常信息。运行结果如下所示。
1 | ---------学生信息录入--------------- |
多重catch语句
当try语句块中用很多语句会发生异常,且异常的种类很多,这时候我们就需要使用多重catch来捕获异常。多重catch代码块的语法如下:
1 | try{ |
在多重catch块中,当一个catch块捕获到一个异常时,其它的catch代码块就不再进行匹配。
注意:当捕获的多个异常类之间存在父子关系时,捕获异常时一般先捕获子类,在捕获父类。所以子类异常须在父类异常前面,否则子类将无法捕获该异常。
下面给出一个从I/O输入流中读取字符串的异常处理:
1 | public class Test03 { |
在 try 代码块中第 12 行代码调用 FileInputStream
构造方法可能会发生 FileNotFoundException
异常。第 16 行代码调用 BufferedReader
输入流的 readLine()
方法可能会发生 IOException
异常。FileNotFoundException
异常是 IOException
异常的子类,应该先捕获 FileNotFoundException
异常,见代码第 23 行;后捕获 IOException
异常,见代码第 26 行。
如果将 FileNotFoundException
和 IOException
捕获顺序调换,那么捕获 FileNotFoundException
异常代码块将永远不会进入,FileNotFoundException
异常处理永远不会执行。 上述代码第 29 行 ParseException
异常与 IOException
和 FileNotFoundException
异常没有父子关系,所以捕获 ParseException
异常位置可以随意放置。
Java项目实战之计算平均成绩
1 | import java.util.InputMismatchException; |
上述代码中可能捕获的异常类型由:InputMismatchException异常、ArithmeticException异常以及其他类型的异常。
当用户输入的总人数或者总成绩不为数值类型时,程序将拋出 InputMismatchException 异常,从而执行第 15 行代码,输出结果如下所示:
1 | 请输入班级总人数: |
当输入的总人数为 0 时,计算平均成绩就会出现被除数为 0 的情况,此时会拋出 ArithmeticException 异常,从而执行第 17 行代码,输出结果如下所示:
1 | 请输入班级总人数: |
如下所示的是当输入的总人数和总成绩均为正常数值类型时的输出结果:
1 | 请输入班级总人数: |
Java try-catch-finally语句
在实际开发中,try-catch语句中的某些代码可能不会被完全执行,但也存在一些处理代码要求必须被执行。例如:在try块中打开了一些物理资源,而这些物理资源都是必须被显示回收的。
Java的垃圾回收机制不会回收任何物理资源,垃圾回收机制只回收堆内存中对象所占用的内存。
为了确保try块中打开的物理资源都能被回收,异常处理机制为此提供了finally代码开,并且在Java 7 之后提供了自动资源管理(Automatic Resource Management)技术。
finally代码块可以和try-catch搭配使用,语法格式如下:
1 | try{ |
无论异常是否发生或者被捕获(特殊情况除外),finally语句块中的代码都会被执行此外,finally语句也可以和try语句匹配使用,其格式如下:
1 | try{ |
使用try-catch-finally语句时要注意以下几点:
- 异常处理语法结构中只有try语句块是必须的,也就是说,如果么有try语句块,那么后面也不会跟有catch和finally语句块。
- catch块和finally块都是可选的,但catch块和finally块至少出现其中一个,也可以同时出现。
- 可以有多个catch块,捕获父类异常的catch块必须在子类异常块的后面。
- 不能只有try块。
- 多个catch块必须位于try块后面,finally块必须位于所有catch块后面。
- finally与try语句块匹配的语法格式,此种情况会导致异常丢失,所以不常见。
整体的try-catch-finally异常处理流程如下图所示
图10-1 try-catch-finally 语句执行流程图
除非在 try 块、catch 块中调用了退出虚拟机的方法System.exit(int status)
,否则不管在 try 块或者 catch 块中执行怎样的代码,出现怎样的情况,异常处理的 finally 块总会执行。
通常情况下不在 finally 代码块中使用 return 或 throw 等导致方法终止的语句,否则将会导致 try 和 catch 代码块中的 return 和 throw 语句失效
Java finally和return的执行顺序
我们很清楚Java虚拟机在处理try-catcch-finallly语句时的顺序,即try->catch->finally。但当return也加入其中的时候,很多人就会混淆不清,下面对集中可能的情况一一进行讨论。
try和catch中带有return
try中带有return
1 | public class tryDeme{ |
执行结果如下:
1 | 执行finally模块 |
try和catch中都带有return
1 | public class tryDemo { |
输出结果为:
1 | 执行finally模块 |
当 try 代码块或者 catch 代码块中有 return 时,finally 中的代码总会被执行,且 finally 语句 return 返回之前执行。
finally中带有return
1 | public class tryDemo { |
输出结果如下:
1 | 执行finally模块 |
当 finally 有返回值时,会直接返回该值,不会去返回 try 代码块或者 catch 代码块中的返回值。
注意:finally 代码块中最好不要包含 return 语句,否则程序会提前退出。
finally中改变返回值
下面先来看 try 代码块或者 catch 代码块中的返回值是普通变量时,代码如下:
1 | public class tryDemo{ |
输出结果为:
1 | 执行finally模块 |
由输出结果可以看出,在 finally 代码块中改变返回值并不会改变最后返回的内容。
当返回值类型是引用类型时,结果也是一样的,代码如下:
1 | public class tryDemo { |
输出结果为:
1 | 执行finally模块 |
当try代码块或catch代码块中的return返回值类型为普通变量或引用变量时,即使在后面finally代码中对返回值重新赋值,也不会影响最后的返回值。因为finally被设计的主要用途还是清理物理资源。
总结为以下几条:
- 当 try 代码块和 catch 代码块中有 return 语句时,finally 仍然会被执行。
- 执行 try 代码块或 catch 代码块中的 return 语句之前,都会先执行 finally 语句。
- 无论在 finally 代码块中是否修改返回值,返回值都不会改变,仍然是执行 finally 代码块之前的值。
- finally 代码块中的 return 语句一定会执行。
Java 9增强的自动资源管理
未有较好的理解,等待日后更新…
Java的声明和抛出异常
Java处理捕获和处理异常之外还包括声明异常和抛出异常。实现声明和抛出异常的关键字非常相似,它们时throws
和throw
。可以通过 throws
关键字在方法上声明该方法要拋出的异常,然后在方法内部通过 throw
拋出异常对象。
throws声明异常
当一个方法产生一个它不处理的异常时,那么就需要在该方法的头部声明这个异常,以便将该异常传递到方法的外部进行处理,使用throws声明的方法表示此方法不处理异常。throws具体格式如下:
1 | returnType meyhod_name(paramList) throwsException 1,Exception 2,...{...} |
- returnType:返回值类型
- method_name:方法名
- paramList:参数列表
- Exception 1,Exception 2:异常类
使用 throws 声明抛出异常的思路是,当前方法不知道如何处理这种类型的异常,该异常应该由向上一级的调用者处理;如果 main 方法也不知道如何处理这种类型的异常,也可以使用 throws 声明抛出异常,该异常将交给 JVM 处理。JVM 对异常的处理方法是,打印异常的跟踪栈信息,并中止程序运行,这就是前面程序在遇到异常后自动结束的原因。
下面给出案例
创建一个 readFile() 方法,该方法用于读取文件内容,在读取的过程中可能会产生 IOException 异常,但是在该方法中不做任何的处理,而将可能发生的异常交给调用者处理。在 main() 方法中使用 try catch 捕获异常,并输出异常信息。代码如下:
1 | import java.io.FileInputStream; |
以上代码,首先在定义 readFile() 方法时用 throws 关键字声明在该方法中可能产生的异常,然后在 main() 方法中调用 readFile() 方法,并使用 catch 语句捕获产生的异常。
方法重写时声明抛出异常的限制
使用 throws 声明抛出异常时有一个限制,是方法重写中的一条规则:子类方法声明抛出的异常类型应该是父类方法声明抛出的异常类型的子类或相同,子类方法声明抛出的异常不允许比父类方法声明抛出的异常多。
1 | public class OverrideThrows { |
上面程序中 Sub 子类中的 test() 方法声明抛出 Exception,该 Exception 是其父类声明抛出异常 IOException 类的父类,这将导致程序无法通过编译。
所以在编写类继承代码时要注意,子类在重写父类带 throws 子句的方法时,子类方法声明中的 throws 子句不能出现父类对应方法的 throws 子句中没有的异常类型,因此 throws 子句可以限制子类的行为。也就是说,子类方法拋出的异常不能超过父类定义的范围。
throw抛出异常
与throws不同的是,throw语句用来直接抛出一个异常,后接一个可抛出的异常类对象,其语法格式如下:
throw ExceptionObject;
其中ExceptionObject必须是Throwable类或其子类的对象。如果是自定义异常类,也必须是Throwable的直接或间接子类。例如,以下代码会出现错误:
1 | throw new String("抛出异常"); // String类不是Throwable类的子类 |
当 throw 语句执行时,它后面的语句将不执行,此时程序转向调用者程序,寻找与之相匹配的 catch 语句,执行相应的异常处理程序。如果没有找到相匹配的 catch 语句,则再转向上一层的调用程序。这样逐层向上,直到最外层的异常处理程序终止程序并打印出调用栈情况。
throw 关键字不会单独使用,它的使用完全符合异常的处理机制,但是,一般来讲用户都在避免异常的产生,所以不会手工抛出一个新的异常类的实例,而往往会抛出程序中已经产生的异常类的实例。
throws 关键字和 throw 关键字在使用上的几点区别如下:
- throws 用来声明一个方法可能抛出的所有异常信息,表示出现异常的一种可能性,但并不一定会发生这些异常;throw 则是指拋出的一个具体的异常类型,执行 throw 则一定抛出了某种异常对象。
- 通常在一个方法(类)的声明处通过 throws 声明方法(类)可能拋出的异常信息,而在方法(类)内部通过 throw 声明一个具体的异常信息。
- throws 通常不用显示地捕获异常,可由系统自动将所有捕获的异常信息抛给上级方法; throw 则需要用户自己捕获相关的异常,而后再对其进行相关包装,最后将包装后的异常信息抛出。
Java 7新特性:多异常捕获
前面我们总结的多重catch的使用虽然客观上提高了程序的健壮性,但也导致了程序代码量大大增加。如果有些异常种类不同,但捕获后的处理方式时相同的,例如以下代码:
1 | try{ |
4个不同类型的异常,但捕获后的处理都是调用methodA方法。为了解决这种问题,Java 7推出了多异常捕获技术,可以把这些异常合并处理,上述代码可修改如下:
1 | try{ |
注意:由于FileNotFoundException属于IOException异常,IOException异常可以捕获它的所有子类异常。所以不能写成FileNotFoundException|IOException|ParseException
。
使用一个catch块捕获多种异常类型时要注意的地方如下:
- 捕获多种类型异常时,多种类型的异常应用竖线
|
分隔。 - 捕获多种类型异常时,异常变量有隐式的final修饰,因此重新不能对异常变量重新赋值。
下面出现提供了Java 7多异常捕获的示范:
1 | public class ExceptionTest{ |
Java自定义异常
如果Java提供的内置异常类型不能满足我们实际开发时的需求,这时我们可以自己设计Java类库或框架,其中包括异常类型。实现自定义异常类需要继承Exception类或其子类,如果自定义异常类需要继承RuntimeException类或其子类。
自定义异常的语法格式是:
1 | <class><自定义异常名><extends><Exception> |
在编码规范上,一般将自定义异常类的类名命名为 XXXException,其中 XXX 用来代表该异常的作用。
自定义异常类一般包含两个构造方法:一个是无参的默认构造方法,另一个构造方法以字符串的形式接收一个定制的异常消息,并将该消息传递给超类的构造方法。
例如,以下代码创建一个名称为 IntegerRangeException 的自定义异常类:
1 | class IntegerRangeException extends Exception { |
以上代码创建的自定义异常类 IntegerRangeException 类继承自 Exception 类,在该类中包含两个构造方法。
提示:因为自定义异常继承自 Exception 类,因此自定义异常类中包含父类所有的属性和方法。
Java异常处理规则
成功的异常处理应该实现如下4个目标:
- 使重新代码混乱最小化
- 捕获并保留诊断信息
- 通知合适的人员
- 采用合适的方式结束异活动
不要过度使用异常
过度使用异常主要有以下两个方面:
- 把异常和普通错误混淆在一起,不再编写任何错误处理代码,而是以简单地抛出异常来代替所有的错误处理。
- 使用异常处理来代替流程控制。
对于一些可预知的错误,我们可以编写相应的代码来进行避免,而不是一味的将其处理为异常抛出。
不要使用过度庞大的try块
过度庞大的try块会导致其中出现异常的可能性大大增加,从而导致分析异常的难度也大大增加。而且当try块过于庞大时,catch块无疑也会急剧增加,其处理异常的逻辑难度也会大大增加。
避免使用Catch All语句
所谓的Catch All语句指的是一种异常捕获板块,它可以处理程序发生的所有可能的异常。例如,如下片段代码:
1 | try{ |
不可否认,每个程序员都曾经用过这种异常处理方式,但在编写关键程序时就应避免使用这种异常处理方式。这种处理方式有如下两点不足之处。
- 所有的异常都采用相同的处理方式,这将导致无法对不同的异常分情况处理,如果要分情况处理,则需要在 catch 块中使用分支语句进行控制,这是得不偿失的做法。
- 这种捕获方式可能将程序中的错误、Runtime 异常等可能导致程序终止的情况全部捕获到,从而“压制”了异常。如果出现了一些“关键”异常,那么此异常也会被“静悄悄”地忽略。
实际上,Catch All 语句不过是一种通过避免错误处理而加快编程进度的机制,应尽量避免在实际应用中使用这种语句。
不要忽略捕获到的异常
对于捕获到的异常,应对其进行合适的秀发,然后绕过异常发生的地方继续执行;或用别的数据进行计算,以代替期望的方法返回值。
Java的异常跟踪栈
异常对象的 printStackTrace() 方法用于打印异常的跟踪栈信息,根据 printStackTrace() 方法的输出结果,开发者可以找到异常的源头,并跟踪到异常一路触发的过程。
看下面用于测试 printStackTrace 的例子程序。
1 | class SelfException extends RuntimeException { |
上面程序中 main 方法调用 firstMethod,firstMethod 调用 secondMethod,secondMethod 调用 thirdMethod,thirdMethod 直接抛出一个 SelfException 异常。运行上面程序,会看到如下所示的结果。
1 | Exception in thread "main" Test.SelfException: 自定义异常信息 |
上面运行结果的第 2 行到第 5 行之间的内容是异常跟踪栈信息,从打印的异常信息我们可以看出,异常从 thirdMethod 方法开始触发,传到 secondMethod 方法,再传到 firstMethod 方法,最后传到 main 方法,在 main 方法终止,这个过程就是 Java 的异常跟踪栈。
异常跟踪栈信息的第一行一般详细显示异常的类型和异常的详细消息,接下来是所有异常的发生点,各行显示被调用方法中执行的停止位置,并标明类、类中的方法名、与故障点对应的文件的行。一行行地往下看,跟踪栈总是最内部的被调用方法逐渐上传,直到最外部业务操作的起点,通常就是程序的入口 main 方法或 Thread 类的 run 方法(多线程的情形)。
下面例子程序示范了多线程程序中发生异常的情形。
1 | public class ThreadExceptionTest implements Runnable { |
运行上面程序,会看到如下运行结果。
1 | Exception in thread "Thread-0" java.lang.ArithmeticException: / by zero |
多线程异常的跟踪栈,从发生异常的方法开始,到线程的 run 方法结束。从上面的运行结果可以看出,程序在 Thread 的 run 方法中出现了 ArithmeticException 异常,这个异常的源头是 ThreadExcetpionTest 的 secondMethod 方法,位于 ThreadExcetpionTest.java 文件的 14 行。这个异常传播到 Thread 类的 run 方法就会结束(如果该异常没有得到处理,将会导致该线程中止运行)。
Java.util.logging:JDK自带记录日志类
日志用来记录程序的运行轨迹,方便查找关键信息,也方便快速定位解决问题。下面介绍 Java 自带的日志工具类 java.util.logging 的使用。
如果要生成简单的日志记录,可以使用全局日志记录器并调用其 info 方法,代码如下:
1 | Logger.getGlobal().info("打印信息"); |
JDK Logging 把日志分为如下表 7 个级别,等级依次降低。
级别 | SEVERE | WARNING | INFO | CONFIG | FINE | FINER | FINEST |
---|---|---|---|---|---|---|---|
调用方法 | severe() | warning() | info() | config() | fine() | finer() | finest() |
含义 | 严重 | 警告 | 信息 | 配置 | 良好 | 较好 | 最好 |
Logger 的默认级别是 INFO,比 INFO 级别低的日志将不显示。Logger 的默认级别定义在 jre 安装目录的 lib 下面。
\# Limit the message that are printed on the console to INFO and above.
java.util.logging.ConsoleHandler.level = INFO
所以在默认情况下,日志只显示前三个级别,对于所有的级别有下面几种记录方法:
logger.warning(message);
logger.fine(message);
同时,还可以使用 log 方法指定级别,例如:
logger.log(Level.FINE, message);
修改日志管理器配置
可以通过编辑配置文件来修改日志系统的各种属性。在默认情况下,配置文件存在于 jre 安装目录下“jre/lib/logging.properties”。要想使用另一个配置文件,就要将 java.util.logging.config.file 特性设置为配置文件的存储位置,并用下列命令启动应用程序。
java -Djava.util.logging.config.file = configFile MainClass
日志管理器在 JVM 启动过程中初始化,这在 main 执行之前完成。如果在 main 中调用System.setProperty("java.util.logging.config.file",file)
,也会调用LogManager.readConfiguration()
来重新初始化日志管理器。
要想修改默认的日志记录级别,就需要编辑配置文件,并修改以下命令行。
.level=INFO
可以通过添加以下内容来指定自己的日志记录级别
Test.Test.level=FINE
也就是说,在日志记录器名后面添加后缀 .level。
在稍后可以看到,日志记录并不将消息发送到控制台上,这是处理器的任务。另外,处理器也有级别。要想在控制台上看到 FINE 级别的消息,就需要进行下列设置。
java.util.logging.ConsoleHandler.level=FINE
注意:在日志管理器配置的属性设置不是系统属性,因此,用-Dcom.mycompany.myapp.level=FINE
启动应用程序不会对日志记录器产生任何影响。