异常
1 异常的概念
异常是指在程序运行期可能出现的非正常情况,这些情况将导致程序出错,这种出错不同于代码编写错误或程序算法出错,代码编写错误无法完成编译,而程序算法出错计算将取得错误的结果。程序出现异常时,缺省会直接造成程序的中断运行,提前预知这种异常的可能性可以补充异常处理的逻辑,从这个角度出发,异常也是一种行之有效的逻辑处理机制,在程序中声明异常或主动抛出异常也很常见。
简而言之,为了提高程序的健壮性,异常需要由程序员处理。
2 异常的体系
异常是指由于各种不期而至的情况,导致程序中断运行的一种指令流,如:文件找不到、非法参数、网络超时等。为了保证正序正常运行,在设计程序时必须考虑到各种异常情况,并正确的对异常进行处理。异常也是一种对象,java当中定义了许多异常类,并且定义了基类java.lang.Throwable作为所有异常的超类。Java语言设计者将异常划分为两类:Error和Exception,其体系结构大致如下图所示:
Throwable:有两个重要的子类:Exception(异常)和Error(错误),两者都包含了大量的异常处理类。
1、Error(错误):是程序中无法处理的错误,表示运行应用程序中出现了严重的错误。此类错误一般表示代码运行时JVM出现问题。通常有Virtual MachineError(虚拟机运行错误)、NoClassDefFoundError(类定义错误)等。比如说当jvm耗完可用内存时,将出现OutOfMemoryError。此类错误发生时,JVM将终止线程。
这些错误是不可查的,非代码性错误。因此,当此类错误发生时,应用不应该去处理此类错误。
2、Exception(异常):程序本身可以捕获并且可以处理的异常。
Exception这种异常又分为两类:运行时异常和编译异常。
-
1、运行时异常(不受检异常):RuntimeException类极其子类表示JVM在运行期间可能出现的错误。比如说试图使用空值对象的引用(NullPointerException)、数组下标越界(ArrayIndexOutBoundException)。此类异常属于不可查异常,一般是由程序逻辑错误引起的,在程序中可以选择捕获处理,也可以不处理。
-
2、编译异常(受检异常):Exception中除RuntimeException极其子类之外的异常。如果程序中出现此类异常,比如说IOException,必须对该异常进行处理,否则编译不通过。在程序中,通常不会自定义该类异常,而是直接使用系统提供的异常类。
3、可查异常与不可查异常:java的所有异常可以分为可查异常(checked exception)和不可查异常(unchecked exception)。
-
1、可查异常:编译器要求必须处理的异常。正确的程序在运行过程中,经常容易出现的、符合预期的异常情况。一旦发生此类异常,就必须采用某种方式进行处理。除RuntimeException及其子类外,其他的Exception异常都属于可查异常。编译器会检查此类异常,也就是说当编译器检查到应用中的某处可能会此类异常时,将会提示你处理本异常——要么使用try-catch捕获,要么使用throws语句抛出,否则编译不通过。
-
2、不可查异常:编译器不会进行检查并且不要求必须处理的异常,也就说当程序中出现此类异常时,即使我们没有try-catch捕获它,也没有使用throws抛出该异常,编译也会正常通过。该类异常包括运行时异常(RuntimeException极其子类)和错误(Error)。
3 异常产生过程解析
例子:数组越界异常
public class Exception {
public static void main(String[] args) {
int[] arr = {1, 2, 3, 4};
int e = arr[4];
System.out.println(e);
}
}
分析过程:访问数组arr中的4索引时,因为数组没有没有4的索引,这时候jvm就会检测出程序会出现的异常,jvm会做下面两件事。
- 1 jvm会根据异常产生的原因创建一个异常对象,这个异常对象包含了异常产生的内容、原因、位置。这里创建的是数组下标越界对象,
new ArrayIndexOutOfBoundsException("4");
- 2 如果当前方法中,没有异常的处理逻辑(try…catch),那么JVM就会把异常对象抛出给方法的调用者MAIN方法来处理这个异常.
- 3 MAIN方法接受到这个异常对象,MAIN方法也没有异常的处理逻辑,继续把对象抛出给MAIN方法的调用者JVM处理(MAIN方法把异常对象抛出给JVM)
- 4 JVM把异常对象(内容,原因,位置)以红色的字体打印在控制台
- 5 JVM会终止当前正在执行的java程序 --中断处理
4 异常处理
4.1 异常关键词
- try – 用于监听。将要被监听的代码(可能抛出异常的代码)放在try语句块之内,当try语句块内发生异常时,异常就被抛出。
- catch – 用于捕获异常。catch用来捕获try语句块中发生的异常。
- finally – finally语句块总是会被执行。它主要用于回收在try块里打开的物力资源(如数据库连接、网络连接和磁盘文件)。只有finally块,执行完成之后,才会回来执行try或者catch块中的return或者throw语句,如果finally中使用了return或者throw等终止方法的语句,则就不会跳回执行,直接停止。
- throw – 用于方法内抛出异常。
- throws – 用在方法签名中,用于声明该方法可能抛出的异常。主方法上也可以使用throws抛出。如果在主方法上使用了throws抛出,就表示在主方法里面可以不用强制性进行异常处理,如果出现了异常,就交给JVM进行默认处理,则此时会导致程序中断执行。
4.2 关键词的使用
4.2.1 throw关键词
格式: throw new XXXException(“XXXXXX”);
例子:
public static void main(String[] args) {
int[] arr = {1, 2, 3, 4};
int length = 5;
if(arr.length<0 || arr.length<length){
throw new ArrayIndexOutOfBoundsException("数组越界");
}
}
输出结果:
4.2.2 throws关键词
异常处理的第一种方式,抛出异常,交给别人处理。
作用:
- 当方法内部抛出异常对象的时候,那么我们就必须处理这个异常对象
- 可以使用throws关键字处理异常对象,会把异常对象声明抛出给方法的调用者处理(自己不处理,给别人处理)
格式 :
修饰符 返回值类型 方法名(参数列表) throws AAAException,BBBException...{
throw new AAAException("产生原因");
throw new BBBException("产生原因");
}
注意:
- throws关键字必须写在方法声明处
- throws关键字后边声明的异常必须是Exception或者是Exception的子类
- 方法内部如果抛出多个异常对象,那么throws后边必须也声明多个异常如果抛出的多个异常对象有子父类关系,那么直接声明父类异常即可
- 调用了一个声明抛出异常的方法,我们就必须的处理声明的异常,要么继续使用throws声明抛出,交给方法的调用者处理,最终交给JVM,要么try…catch自己处理
例子:
public class Exception1 {
public static void main(String[] args) throws FileNotFoundException {
readFiles("c");
}
public static void readFiles(String fileName) throws FileNotFoundException {
if (!fileName.equals("c:\\")){
throw new FileNotFoundException("传递的文件路径不对");
}
}
}
输出结果:
解释:
readFiles方法抛出异常FileNotFoundException交给main方法处理,main方法中没有对FileNotFoundException异常进行try…catch处理,所以继续抛出该异常,main方法抛出该异常交给jvm处理,jvm将该异常在控制台中打印出来,并中断程序,不在执行后面的程序。
4.2.3 try…catch
- 如果异常出现的话,会立刻终止程序,所以我们得处理异常
- try…catch的方式就是捕获异常,捕获异常就是对异常针对性的语句进行捕获其可能出现的异常,可以对出现的异常进行指定方式的处理。
格式:
try {
编写可能会出现异常的代码
}catch(异常类型 e){
处理异常的代码
}
注意:
- try中可能会做出多个异常对象,那么就可以使用多个catch来处理这些异常对象
- 如果try中产生了异常,那么就会执行catch中的异常处理逻辑,执行完毕catch中的处理逻辑,继续执行try…catch之后的代码,如果try中没有产生异常,那么就不好执行catch中异常的处理逻辑,执行完try中的代码
例子:
public class Exception1 {
public static void main(String[] args) {
readFiles("c");
}
public static void readFiles(String fileName){
try{//可能出现异常的代码
if (!fileName.equals("c:\\")){
throw new FileNotFoundException("传递的文件路径不对");
}
}catch (FileNotFoundException e){
//出现异常后的处理代码
System.out.println(e);
}
System.out.println("后续代码");
}
}
输出结果:
解析:
try…catch捕获异常并处理后,程序并不会像throws抛出异常后中断程序,而是处理完异常后继续运行后续的代码。
4.2.4 finally关键词
finally:有一些特定的代码无论异常是否发生,都需要执行。另外,因为异常会引发程序跳转,导致有些语句执行不到,而finally就是解决这个问题的,在finally代码块中存放的代码都是一定会被执行的。
通常什么时候需要用到这个finally呢?
当我们在try语句块打开了一些物理资源(磁盘文件/网络连接/数据库连接)使用完之后进行关闭打开的资源。
格式:
try {
编写可能会出现异常的代码
}catch(异常类型 e){
处理异常的代码
}finally{
必须执行的代码
}
例子:
public class Exception1 {
public static void main(String[] args) {
readFiles("c");
}
public static void readFiles(String fileName){
try{
System.out.println("打开资源");
if (!fileName.equals("c:\\")){
throw new FileNotFoundException("传递的文件路径不对");
}
}catch (FileNotFoundException e){
System.out.println(e);
}finally {
System.out.println("释放资源");
}
System.out.println("后续代码");
}
}
输出结果:
finally块和return
- 首先一个不容易理解的事实:在 try块中即便有return,break,continue等改变执行流的语句,finally也会执行。
- finally中的return 会覆盖 try 或者catch中的返回值。
- finally中的return或异常会抑制(消灭)前面try或者catch块中的异常。
5 常见异常以及出现原因
1、java.lang.NullpointerException(空指针异常)
原因:这个异常经常遇到,异常的原因是程序中有空指针,即程序中调用了未经初始化的对象或者是不存在的对象。
经常出现在创建对象,调用数组这些代码中,比如对象未经初始化,或者图片创建时的路径错误等等。对数组代码
中出现空指针,是把数组的初始化和数组元素的初始化搞混淆了。数组的初始化是对数组分配空间,而数组元素的
初始化,是给数组中的元素赋初始值
2、 java.lang.ClassNotFoundException(指定的类不存在)
原因:当试图将一个String类型数据转换为指定的数字类型,但该字符串不满足数值型数据的要求时,就抛出这个异
常。例如将String类型的数据"123456"转换为数值型数据时,是可以转换的的。但是如果String类型的数据中包含了
非数字型的字符,如123*56,此时转换为数值型时就会出现异常。系统就会捕捉到这个异常,并进行处理
3、java.lang.ClassNotFoundExceptio(指定的类不存在)
原因:是因为类的名称和路径不正确,通常都是程序试图通过字符串来加载某个类的时候可能会引发异常。例如:
调用Class.forName()、或者调用ClassLoad的finaSystemClass()、或者是LoadClass()时出现异常
4、java.lang.IndexOutOfBoundsException(数组下标越界异常)
原因:查看程序中调用的数组或者字符串的下标值是不是超出了数组的范围,一般来说,显示调用数组不太容易出这
样的错,但隐式调用就有可能出错了,还有一种情况,是程序中定义的数组的长度是通过某些特定方法决定的,不是
事先声明的,这个时候可以先查看一下数组的length,以免出现这个异常
5、java.lang.IllegalArgumentException(方法的参数错误)
例如g.setColor(int red,int green,int blue)这个方法中的三个值,如果有超过255的会出现这个异常,如果程
序中存在这个异常,就要去检查一下方法调用中的参数传递或参数值是不是有错
6、java.lang.IllegalAccessException(没有访问权限)
当程序要调用一个类,但当前的方法即没有对该类的访问权限便会出现这个异常。如果程序中用了Package的情况下
有可能出现这个异常
7、java.lang.ArithmeticException(数学运算异常)
当数学运算中出现了除以零这样的运算就会出这样的异常。
8、java.lang.ClassCastException(数据类型转换异常)
当试图将对某个对象强制执行向下转换,但该对象又不可转换或又不可转换为其子类的实例时将出现该异常
9、 java.lang.FileNotFoundException(文件未找到异常)
当程序打开一个不存在的文件来进行读写时将会引发该异常。该异常由FileInputStream,FileOutputStream,
RandomAccessFile的构造器声明抛出,即使被操作的文件存在,但是由于某些原因不可访问,比如打开一个
只有只读权限的文件并向其中写入数据,以上构造方法依然会引发异常
10、java.lang.ArrayStoreException(数组存储异常)
当试图将类型为不兼容类型的对象存入一个Object[]数组时将引发异常
11、java.lang.NoSuchMethodException(方法不存在异常)
当程序试图通过反射来创建对象,访问(修改或读取)某个方法,但是该方法不存在就会引发异常。
12、 java.lang.EOFException(文件已结束异常)
当程序在输入的过程中遇到文件或流的结尾时,引发异常。因此该异常用于检查是否达到文件或流的结尾
13、java.lang.InstantiationException(实例化异常)
当试图通过Class的newInstance()方法创建某个类的实例,但程序无法通过该构造器来创建该对象时引发。
Class对象表示一个抽象类,接口,数组类,基本类型 。该Class表示的类没有对应的构造器。
14、java.lang.InterruptedException(被中止异常)
当某个线程处于长时间的等待、休眠或其他暂停状态,而此时其他的线程通过Thread的interrupt方法终止该线程时抛出该异常。
15、java.lang.CloneNotSupportedException (不支持克隆异常)
当没有实现Cloneable接口或者不支持克隆方法时,调用其clone()方法则抛出该异常
16、java.lang.OutOfMemoryException (内存不足错误)
当可用内存不足以让Java虚拟机分配给一个对象时抛出该错误
17、java.lang.NoClassDefFoundException (未找到类定义错误)
当Java虚拟机或者类装载器试图实例化某个类,而找不到该类的定义时抛出该错误
6 自定义异常
在 Java 中你可以自定义异常。如果要自定义异常类,则扩展Exception类即可,因此这样的自定义异常都属于检查异常(checked exception)。如果要自定义非检查异常,则扩展自RuntimeException。
格式:
public class XXXException extends Exception / RuntimeException{
添加一个空参数的构造方法
添加一个带异常信息的构造方法
}
注意:
- 自定义异常类一般都是以Exception结尾,说明改类是一个异常类
- 自定义异常类,必须继承Exception或者RuntimeException
例子:
自定义异常类RegisterException
public class RegisterException extends Exception{
public RegisterException(){
super();
}
public RegisterException(String msg){
super(msg);
}
}
使用自定义异常类
public class Register {
static String[] username = {"张三", "李四", "王五"};
public static void main(String[] args) throws RegisterException {
String name = "张三";
checkUser(name);
}
public static void checkUser(String name) throws RegisterException {
for (String n:username){
if (n.equals(name)){
throw new RegisterException("该用户已经被注册");
}
}
}
}
输出结果:
7 异常常见面试问题
Error与Exception的区别:
Error(错误)是系统中的错误,程序员是不能改变的和处理的,是在程序编译时出现的错误,只能通过修改程序才能修正。一般是指与虚拟机相关的问题,如系统崩溃,虚拟机错误,内存空间不足,方法调用栈溢等。对于这类错误的导致的应用程序中断,仅靠程序本身无法恢复和和预防,遇到这样的错误,建议让程序终止。
Exception(异常)表示程序可以处理的异常,可以捕获且可能恢复。遇到这类异常,应该尽可能处理异常,使程序恢复运行,而不应该随意终止异常。
在catch捕获异常时,为什么不考虑使用Throwable类型,而只是使用Exception来进行接收?
Throwable表示的范围要比Exception大。实际上程序使用Throwable来进行处理,没有任何语法问题,但是却会存在逻辑问题。因为此时出现的(或者说用户能够处理的)只有Exception类型,而如果使用Throwable接收,还会表示可以处理Error的错误,而用户是处理不了Error错误的,所以在开发中用户可以处理的异常都要求以Exception类为主。
异常是一起处理好还是分开处理好?
根据实际的开发要求是否严格来决定。在实际的项目开发项目工作中,所有的异常是统一使用Exception处理还是分开处理,完全根据开发者的项目开发标准来决定。如果项目开发环境严谨,基本上要求针对每一种异常分别进行处理,并且要详细记录下异常产生的时间以及产生的位置,这样可以方便程序维护人员进行代码的维护。再次注意:处理多个异常时,捕获范围小的异常要放在捕获范围大的异常之前处理。
throw和throws的区别?
throw和throws都是在异常处理中使用的关键字,区别如下:
- throw:指的是在方法中人为抛出一个异常对象(这个异常对象可能是自己实例化或者抛出已存在的);
- throws:在方法的声明上使用,表示此方法在调用时必须处理异常。
检查型异常(Checked Exception)与非检查型异常(Unchecked Exception)区别?
- 所有的检查性异常都继承自java.lang.Exception;所有的非检查性异常都继承自java.lang.RuntimeEx ception。
- 检查性异常和非检查性异常最主要的区别在于其处理异常的方式:检查性异常必须使用try catch或者throws等关键字进行处理,否则编译器会报错;非检查性异常一般是程序代码写的不够严谨而导致的问题,可以通过修改代码来规避。
- 常见的运行时异常:空指针异常(NullPointerException)、除零异常(ArithmeticException)、数组越界异常(ArrayIndexOutOfBoundsException)等;
- 常见的检查性异常:输入输出异常(IOException)、文件不存在异常(FileNotFoundException)、SQL语句异常(SQLException)等。
参考文章