@
异常
在程序设计和运行的过程中,发生错误是不可避免的。尽管 Java 语言的设计从根本上提供了便于写出整洁、安全代码的方法,并且程序员也尽量地减少错误的产生,但是使程序被迫停止的错误的存在仍然不可避免。为此,Java 提供了异常处理机制来帮助程序员检查可能出现的错误,以保证程序的可读性和可维护性。
Java 将异常封装到一个类中,出现错误时就会拋出异常。
什么是异常?
Java 中的异常又称为例外,是一个在程序执行期间发生的事件,它中断正在执行程序的正常指令流。为了能够及时有效地处理程序中的运行错误,必须使用异常类,这可以让程序具有极好的容错性且更加健壮。
异常发生的原因有很多,通常包含以下几大类:
- Java 内部错误发生异常,Java 虚拟机产生的异常。
- 编写的程序代码中的错误所产生的异常,例如空指针异常、数组越界异常等。
- 通过 throw 语句手动生成的异常,一般用来告知该方法的调用者一些必要信息。
异常(exception)是在运行程序时产生的一种异常情况,这些异常有的是因为用户错误引起,有的是程序错误引起的,还有其它一些是因为物理错误引起的。
要理解Java异常处理是如何工作的,需要掌握以下三种类型的异常:
- 检查性异常(check exception ):最具代表的检查性异常是用户错误或问题引起的异常,这是程序员无法预见的。例如要打开一个不存在文件时,一个异常就发生了,这些异常在编译时不能被简单地忽略。
- 运行时异常(RuntimeException):运行时异常是可能被程序员避免的异常。与检查性异常相反,运行时异常可以在编译时被忽略。
- 错误(error): 错误不是异常,而是脱离程序员控制的问题。错误在代码中通常被忽略。例如,当栈溢出时,一个错误就发生了,它们在编译也检查不到的。
Throwable 类
为了能够及时有效地处理程序中的运行错误,Java 专门引入了异常类。在 Java 中所有异常类型都是内置类 java.lang.Throwable 类的子类,即 Throwable 位于异常类层次结构的顶层。
Throwable 类是所有异常和错误的超类,**Throwable 类有两个子类 Error类 和 Exception 类 **,分别表示错误和异常;
其中异常类 Exception 又分为运行时异常和非运行时异常,这两种异常有很大的区别,也称为不检查异常(Unchecked Exception)和检查异常(Checked Exception)。
-
Error 类用来指示运行时环境发生的错误。例如,JVM 内存溢出。一般,程序不会从错误中恢复。
-
Exception 类用于用户程序可能出现的异常情况,它也是用来创建自定义异常类型类的类。
Java 内置异常类
运行时异常都是 RuntimeException 类及其子类异常,如 NullPointerException、IndexOutOfBoundsException 等,这些异常是不检查异常,程序中可以选择捕获处理,也可以不处理。这些异常一般由程序逻辑错误引起,程序应该从逻辑角度尽可能避免这类异常的发生。
非运行时异常是指 RuntimeException 以外的异常,类型上都属于 Exception 类及其子类。从程序语法角度讲是必须进行处理的异常,如果不处理,程序就不能编译通过。如 IOException、ClassNotFoundException 等以及用户自定义的 Exception 异常(一般情况下不自定义检查异常)
非检查性异常(运行时异常)
异常 | 描述 |
---|---|
ArithmeticException | 当出现异常的运算条件时,抛出此异常。例如,一个整数"除以零"时,抛出此类的一个实例。 |
ArrayIndexOutOfBoundsException | 用非法索引访问数组时抛出的异常。如果索引为负或大于等于数组大小,则该索引为非法索引。 |
ArrayStoreException | 试图将错误类型的对象存储到一个对象数组时抛出的异常。 |
ClassCastException | 当试图将对象强制转换为不是实例的子类时,抛出该异常。 |
IllegalArgumentException | 抛出的异常表明向方法传递了一个不合法或不正确的参数。 |
IllegalMonitorStateException | 抛出的异常表明某一线程已经试图等待对象的监视器,或者试图通知其他正在等待对象的监视器而本身没有指定监视器的线程。 |
IllegalStateException | 在非法或不适当的时间调用方法时产生的信号。换句话说,即 Java 环境或 Java 应用程序没有处于请求操作所要求的适当状态下。 |
IllegalThreadStateException | 线程没有处于请求操作所要求的适当状态时抛出的异常。 |
IndexOutOfBoundsException | 指示某排序索引(例如对数组、字符串或向量的排序)超出范围时抛出。 |
NegativeArraySizeException | 如果应用程序试图创建大小为负的数组,则抛出该异常。 |
NullPointerException | 当应用程序试图在需要对象的地方使用 null 时,抛出该异常 |
NumberFormatException | 当应用程序试图将字符串转换成一种数值类型,但该字符串不能转换为适当格式时,抛出该异常。 |
SecurityException | 由安全管理器抛出的异常,指示存在安全侵犯。 |
StringIndexOutOfBoundsException | 此异常由 String 方法抛出,指示索引或者为负,或者超出字符串的大小。 |
UnsupportedOperationException | 当不支持请求的操作时,抛出该异常。 |
检查性异常类 (非运行时异常)
异常 | 描述 |
---|---|
ClassNotFoundException | 应用程序试图加载类时,找不到相应的类,抛出该异常。 |
CloneNotSupportedException | 当调用 Object 类中的 clone 方法克隆对象,但该对象的类无法实现 Cloneable 接口时,抛出该异常。 |
IllegalAccessException | 拒绝访问一个类的时候,抛出该异常。 |
InstantiationException | 当试图使用 Class 类中的 newInstance 方法创建一个类的实例,而指定的类对象因为是一个接口或是一个抽象类而无法实例化时,抛出该异常。 |
InterruptedException | 一个线程被另一个线程中断,抛出该异常。 |
NoSuchFieldException | 请求的变量不存在 |
NoSuchMethodException | 请求的方法不存在 |
Error和Exception的异同
Error(错误)和 Exception(异常)都是 java.lang.Throwable 类的子类,在 Java 代码中只有继承了 Throwable 类的实例才能被 throw 或者 catch。
Exception 和 Error 体现了 Java 平台设计者对不同异常情况的分类,Exception 是程序正常运行过程中可以预料到的意外情况,并且应该被开发者捕获,进行相应的处理。Error 是指正常情况下不大可能出现的情况,绝大部分的 Error 都会导致程序处于非正常、不可恢复状态。所以不需要被开发者捕获。
Error 错误是任何处理技术都无法恢复的情况,肯定会导致程序非正常终止。并且 Error 错误属于未检查类型,大多数发生在运行时。Exception 又分为可检查(checked)异常和不检查(unchecked)异常,可检查异常在源码里必须显示的进行捕获处理,这里是编译期检查的一部分。不检查异常就是所谓的运行时异常,通常是可以编码避免的逻辑错误,具体根据需要来判断是否需要捕获,并不会在编译器强制要求。
如下是常见的 Error 和 Exception:
1)运行时异常(RuntimeException):
- NullPropagation:空指针异常;
- ClassCastException:类型强制转换异常
- IllegalArgumentException:传递非法参数异常
- IndexOutOfBoundsException:下标越界异常
- NumberFormatException:数字格式异常
2)非运行时异常:
- ClassNotFoundException:找不到指定 class 的异常
- IOException:IO 操作异常
3)错误(Error):
- NoClassDefFoundError:找不到 class 定义异常
- StackOverflowError:深递归导致栈被耗尽而抛出的异常
- OutOfMemoryError:内存溢出异常
异常方法
下面的列表是 Throwable 类的主要方法:
方法 | 说明 |
---|---|
public String getMessage() | 返回关于发生的异常的详细信息。这个消息在Throwable 类的构造函数中初始化了。 |
public Throwable getCause() | 返回一个Throwable 对象代表异常原因。 |
public String toString() | 使用getMessage()的结果返回类的串级名字。 |
public void printStackTrace() | 打印toString()结果和栈层次到System.err,即错误输出流。 |
public StackTraceElement [] getStackTrace() | 返回一个包含堆栈层次的数组。下标为0的元素代表栈顶,最后一个元素代表方法调用堆栈的栈底。 |
public Throwable filllnStackTrace() | 用当前的调用栈层次填充Throwable 对象栈层次,添加到栈层次任何先前信息中。 |
异常的处理机制
前面介绍了异常的产生是不可避免的,那么为了保证程序有效地执行,需要对发生的异常进行相应的处理。
Java 的异常处理通过 5 个关键字来实现:try、catch、throw、throws 和 finally。
- try catch 语句用于捕获并处理异常,
- finally 语句用于在任何情况下(除特殊情况外)都必须执行的代码,
- throw 语句用于拋出异常,
- throws 语句用于声明可能会出现的异常。
Java 的异常处理机制提供了一种结构性和控制性的方式来处理程序执行期间发生的事件。异常处理的机制如下:
- 在方法中用 try catch 语句捕获并处理异常,catch 语句可以有多个,用来匹配多个异常。
- 对于处理不了的异常或者要转型的异常,在方法的声明处通过 throws 语句拋出异常,即由上层的调用方法来处
以下代码是异常处理程序的基本结构:
try {
逻辑程序块
} catch(ExceptionType1 e) {
处理代码块1
} catch (ExceptionType2 e) {
处理代码块2
throw(e); // 再抛出这个"异常"
} finally {
释放资源代码块
}
捕获异常
在实际应用中,对于错误的处理是极其重要的,任何程序都很难做到百分百完美,程序中可能存在大量未知问题,所以程序开发时一定要对各种问题进行相应的处理,而 Java 提供的异常处理机制可以帮用户更好地解决这方面的问题。Java 的异常处理机制可以让程序具有极好的容错性,让程序更加健壮。
使用 try 和 catch 关键字可以捕获异常。使用 try/catch 的语法如下:
try{
// 程序代码
}catch(ExceptionName e1){
//Catch 块
}
以上语法中,把可能引发异常的语句封装在 try 语句块中,用以捕获可能发生的异常。catch 后的( )
里放匹配的异常类,指明 catch 语句可以处理的异常类型,发生异常时产生异常类的实例化对象。
如果 try 语句块中发生异常,那么一个相应的异常对象就会被拋出,然后 catch 语句就会依据所拋出异常对象的类型进行捕获,并处理。处理之后,程序会跳过 try 语句块中剩余的语句,转到 catch 语句块后面的第一条语句开始执行。
如果 try 语句块中没有异常发生,那么 try 块正常结束,后面的 catch 语句块被跳过,程序将从 catch 语句块后的第一条语句开始执行。
注意:try...catch 与 if...else 不一样,try 后面的花括号{ }
不可以省略,即使 try 块里只有一行代码,也不可省略这个花括号。与之类似的是,catch 块后的花括号{ }
也不可以省略。另外,try 块里声明的变量只是代码块内的局部变量,它只在 try 块内有效,其它地方不能访问该变量。
在上面语法的处理代码块 1 中,可以使用以下 3 个方法输出相应的异常信息。
- printStackTrace() 方法:指出异常的类型、性质、栈层次及出现在程序中的位置
- getMessage() 方法:输出错误的性质。
- toString() 方法:给出异常的类型与性质。
多重捕获块
如果 try 代码块中有很多语句会发生异常,而且发生的异常种类又很多。那么可以在 try 后面跟有多个 catch 代码块,一个 try 代码块后面跟随多个 catch 代码块的情况就叫多重捕获。
多重捕获块的语法如下所示:
try{ // 程序代码
}catch(异常类型1 异常的变量名1){
// 程序代码
}catch(异常类型2 异常的变量名2){
// 程序代码
}catch(异常类型2 异常的变量名2){
// 程序代码
}
上面的代码段包含了 3 个 catch块。
可以在 try 语句后面添加任意数量的 catch 块。
如果保护代码中发生异常,异常被抛给第一个 catch 块。
如果抛出异常的数据类型与 ExceptionType1 匹配,它在这里就会被捕获。
如果不匹配,它会被传递给第二个 catch 块。
如此,直到异常被捕获或者通过所有的 catch 块。
注意:当捕获的多个异常类之间存在父子关系时,捕获异常时一般先捕获子类,再捕获父类。所以子类异常必须在父类异常的前面,否则子类捕获不到。
finally关键字
finally 关键字用来创建在 try 代码块后面执行的代码块。
无论是否发生异常,finally 代码块中的代码总会被执行。
在 finally 代码块中,可以运行清理类型等收尾善后性质的语句。
finally 代码块出现在 catch 代码块最后,语法如下:
try{
// 程序代码
}catch(异常类型1 异常的变量名1){
// 程序代码
}catch(异常类型2 异常的变量名2){
// 程序代码
}finally{
// 程序代码
}
使用try-catch-finally 语句需注意:
- 异常处理语法结构中只有 try 块是必需的,也就是说,如果没有 try 块,则不能有后面的 catch 块和 finally 块;
- catch 块和 finally 块都是可选的,但 catch 块和 finally 块至少出现其中之一,也可以同时出现;
- 可以有多个 catch 块,捕获父类异常的 catch 块必须位于捕获子类异常的后面;
- 不能只有 try 块,既没有 catch 块,也没有 finally 块;
- 多个 catch 块必须位于 try 块之后,finally 块必须位于所有的 catch 块之后。
- finally 与 try 语句块匹配的语法格式,此种情况会导致异常丢失,所以不常见。
一般情况下,无论是否有异常拋出,都会执行 finally 语句块中的语句,执行流程如图所示。
try catch finally 语句块的执行情况可以细分为以下 3 种情况:
- 如果 try 代码块中没有拋出异常,则执行完 try 代码块之后直接执行 finally 代码块,然后执行 try catch finally 语句块之后的语句。
- 如果 try 代码块中拋出异常,并被 catch 子句捕捉,那么在拋出异常的地方终止 try 代码块的执行,转而执行相匹配的 catch 代码块,之后执行 finally 代码块。如果 finally 代码块中没有拋出异常,则继续执行 try catch finally 语句块之后的语句;如果 finally 代码块中拋出异常,则把该异常传递给该方法的调用者。
- 如果 try 代码块中拋出的异常没有被任何 catch 子句捕捉到,那么将直接执行 finally 代码块中的语句,并把该异常传递给该方法的调用者。
除非在 try 块、catch 块中调用了退出虚拟机的方法System.exit(int status)
,否则不管在 try 块或者 catch 块中执行怎样的代码,出现怎样的情况,异常处理的 finally 块总会执行。
通常情况下不在 finally 代码块中使用 return 或 throw 等导致方法终止的语句,否则将会导致 try 和 catch 代码块中的 return 和 throw 语句失效
throws/throw 关键字:
Java 中的异常处理除了捕获异常和处理异常之外,还包括声明异常和拋出异常。实现声明和抛出异常的关键字非常相似,它们是 throws 和 throw。可以通过 throws 关键字在方法上声明该方法要拋出的异常,然后在方法内部通过 throw 拋出异常对象。
throws 声明异常
当一个方法产生一个它不处理的异常时,那么就需要在该方法的头部声明这个异常,以便将该异常传递到方法的外部进行处理。使用 throws 声明的方法表示此方法不处理异常。throws 具体格式如下:
returnType method_name(paramList) throws Exception 1,Exception2,…{…}
其中,returnType 表示返回值类型;method_name 表示方法名;paramList 表示参数列表;Exception 1,Exception2,… 表示异常类。
如果有多个异常类,它们之间用逗号分隔。这些异常类可以是方法中调用了可能拋出异常的方法而产生的异常,也可以是方法体中生成并拋出的异常。
使用 throws 声明抛出异常的思路是,当前方法不知道如何处理这种类型的异常,该异常应该由向上一级的调用者处理;如果 main 方法也不知道如何处理这种类型的异常,也可以使用 throws 声明抛出异常,该异常将交给 JVM 处理。JVM 对异常的处理方法是,打印异常的跟踪栈信息,并中止程序运行,这就是前面程序在遇到异常后自动结束的原因。
方法重写时声明抛出异常的限制
使用 throws 声明抛出异常时有一个限制,是方法重写中的一条规则:子类方法声明抛出的异常类型应该是父类方法声明抛出的异常类型的子类或相同,子类方法声明抛出的异常不允许比父类方法声明抛出的异常多。看如下程序。
public class OverrideThrows {
public void test() throws IOException {
FileInputStream fis = new FileInputStream("a.txt");
}
}
class Sub extends OverrideThrows {
// 子类方法声明抛出了比父类方法更大的异常
// 所以下面方法出错
public void test() throws Exception {
}
}
上面程序中 Sub 子类中的 test() 方法声明抛出 Exception,该 Exception 是其父类声明抛出异常 IOException 类的父类,这将导致程序无法通过编译。
所以在编写类继承代码时要注意,子类在重写父类带 throws 子句的方法时,子类方法声明中的 throws 子句不能出现父类对应方法的 throws 子句中没有的异常类型,因此 throws 子句可以限制子类的行为。也就是说,子类方法拋出的异常不能超过父类定义的范围。
throw 拋出异常
与 throws 不同的是,throw 语句用来直接拋出一个异常,后接一个可拋出的异常类对象,其语法格式如下:
throw ExceptionObject;
其中,ExceptionObject 必须是 Throwable 类或其子类的对象。如果是自定义异常类,也必须是 Throwable 的直接或间接子类。例如,以下语句在编译时将会产生语法错误:
throw new String("拋出异常"); // String类不是Throwable类的子类
当 throw 语句执行时,它后面的语句将不执行,此时程序转向调用者程序,寻找与之相匹配的 catch 语句,执行相应的异常处理程序。如果没有找到相匹配的 catch 语句,则再转向上一层的调用程序。这样逐层向上,直到最外层的异常处理程序终止程序并打印出调用栈情况。
throw 关键字不会单独使用,它的使用完全符合异常的处理机制,但是,一般来讲用户都在避免异常的产生,所以不会手工抛出一个新的异常类的实例,而往往会抛出程序中已经产生的异常类的实例。
throws 关键字和 throw 关键字在使用上的几点区别如下:
- throws 用来声明一个方法可能抛出的所有异常信息,表示出现异常的一种可能性,但并不一定会发生这些异常;throw 则是指拋出的一个具体的异常类型,执行 throw 则一定抛出了某种异常对象。
- 通常在一个方法(类)的声明处通过 throws 声明方法(类)可能拋出的异常信息,而在方法(类)内部通过 throw 声明一个具体的异常信息。
- throws 通常不用显示地捕获异常,可由系统自动将所有捕获的异常信息抛给上级方法; throw 则需要用户自己捕获相关的异常,而后再对其进行相关包装,最后将包装后的异常信息抛出。
声明自定义异常
自定义异常类一般包含两个构造方法:一个是无参的默认构造方法,另一个构造方法以字符串的形式接收一个定制的异常消息,并将该消息传递给超类的构造方法。
在 Java 中可以自定义异常。编写自己的异常类时需要记住下面的几点。
- 所有异常都必须是 Throwable 的子类。
- 如果希望写一个检查性异常类,则需要继承 Exception 类。
- 如果你想写一个运行时异常类,那么需要继承 RuntimeException 类。
可以像下面这样定义自己的异常类:
class MyException extends Exception{ }
只继承Exception 类来创建的异常类是检查性异常类。一个异常类和其它任何类一样,包含有变量和方法。
提示:因为自定义异常继承自 Exception 类,因此自定义异常类中包含父类所有的属性和方法。
通用异常
在Java中定义了两种类型的异常和错误。
- JVM(Java虚拟机)异常:由 JVM 抛出的异常或错误。例如:NullPointerException 类,ArrayIndexOutOfBoundsException 类,ClassCastException 类。
- 程序级异常:由程序或者API程序抛出的异常。例 IllegalArgumentException 类,IllegalStateException 类。
Java.util.logging:JDK自带记录日志类
每个初学者都很熟悉在有问题的代码中使用 System.out.println 方法在控制台打印消息,来帮助观察程序运行的操作过程。
如果你使用 System.out.println 方法,一旦发现问题的根源,就要将这些语句从代码中删去。
如果接下来又出现了问题,就需要再插入几个调用 System.out.println 方法的语句,如此反复,增加了工作量。
日志用来记录程序的运行轨迹,方便查找关键信息,也方便快速定位解决问题。
如果要生成简单的日志记录,可以使用全局日志记录器并调用其 info 方法,代码如下:
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);
可以使用 setLevel 方法设置级别,例如logger.setLevel(Level.FINE);
可以将 FINE 和更高级别的都记录下来。另外,还可以使用 Level.ALL 开启所有级别的记录,或者使用 Level.OFF 关闭所有级别的记录。
注意:如果将记录级别设计为 INFO 或者更低,则需要修改日志处理器的配置。默认的日志处理器不会处理低于 INFO 级别的信息。
修改日志管理器配置
可以通过编辑配置文件来修改日志系统的各种属性。在默认情况下,配置文件存在于 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
启动应用程序不会对日志记录器产生任何影响。