异常是什么
过程式编程中函数往往用返回特殊的值来表示出现异常情况,比如fread函数,会返回读取的字节数
但是实际编程中程序员可能会忘记检查函数的返回值。这就导致了程序中处理异常情况的缺失。在Java中,编译器会强制让程序员处理程序中出现的某些异常情况。这是基于在Java中当异常发生时,会抛出相应的异常对象,而程序员用捕获这个异常对象来处理异常的机制。
首先来了解下抛出异常对象,下面的代码
class ExcDemo1 {
public static void main(String args[]) {
int nums[] = new int[4];
nums[7] = 10;
System.out.println("this won't be displayed");
}
}
编译没有错误,但是运行时会让程序崩溃,并显示出现了ArrayIndexOutOfBoundsException
异常。ArrayIndexOutOfBoundsException
是Java中定义好的一种异常类型,当访问数组元素下标越界时虚拟机JVM会抛出这个异常类型的对象并终止程序的继续运行。如果我们不在程序中处理它,最终虚拟机JVM会捕获它,并显示它里面存储的信息。这里程序运行的结果并没有什么意外,在C语言中出现了这种问题程序也会崩溃,不过在Java中对于运行时产生的异常对象,我们可以用try-catch语句捕获它。
Java中定义好了各种异常类来对应异常情况的发生。
其中IndexOutOfBoundsException
是ArrayIndexOutOfBoundsException
和StringIndexOutOfBoundsException
的共同父类。
try-catch基础
Java中可以用try-catch语句捕获运行时出现的异常类的对象。
class ExcDemo1 {
public static void main(String args[]) {
int nums[] = new int[4];
try {
System.out.println("Before exception is generated.");
// Generate an index out-of-bounds exception.
nums[7] = 10;
System.out.println("this won't be displayed");
}
catch (ArrayIndexOutOfBoundsException exc) {
// catch the exception
System.out.println("Index out-of-bounds!");
}
System.out.println("After catch statement.");
}
}
程序运行结果显示:
Before exception is generated.
Index out-of-bounds!
After catch statement.
当try块里面的语句执行到nums[7] = 10;
语句,虚拟机JVM就会抛出相应的ArrayIndexOutOfBoundsException类的异常对象,并将执行流程转移到与异常对象类型相应的catch块中执行,执行完成相当于处理完异常,执行流程来到try-catch之后继续执行,就像没有发生过异常一样。
如果try块中没有发生异常,那么就不会产生异常对象,catch中的代码也不会执行,就像没有try-catch那样。
class ExcDemo1_2 {
public static void main(String args[]) {
int nums[] = new int[4];
try {
System.out.println("Before exception is generated.");
// not Generate an index out-of-bounds exception.
nums[2] = 10;
System.out.println("this will be displayed");
}
catch (ArrayIndexOutOfBoundsException exc) {
// catch the exception
System.out.println("Index out-of-bounds!");
}
System.out.println("After catch statement.");
}
}
程序运行结果:
Before exception is generated.
this will be displayed
After catch statement.
catch多个异常
一个try-catch里面可能会抛出多种异常,想要捕获它们,就要提供多个catch来捕获它们。
// Use multiple catch statements.
class ExcDemo3 {
public static void main(String args[]) {
// Here, numer is longer than denom.
int numer[] = { 4, 8, 16, 32, 64, 128, 256, 512 };
int denom[] = { 2, 0, 4, 4, 0, 8 };
for(int i=0; i<numer.length; i++) {
try {
System.out.println(numer[i] + " / " +
denom[i] + " is " +
numer[i]/denom[i]);
}
catch (ArithmeticException exc) {
// catch the exception
System.out.println("Can't divide by Zero!");
}
catch (ArrayIndexOutOfBoundsException exc) {
// catch the exception
System.out.println("No matching element found.");
}
}
}
}
程序运行结果:
4 / 2 is 2
Can’t divide by Zero!
16 / 4 is 4
32 / 4 is 8
Can’t divide by Zero!
128 / 8 is 16
No matching element found.
No matching element found.
因为try块中一旦出现某个异常就会跳转到catch中,那么一次异常发生时也只有一个catch会被执行。
异常对象沿着调用函数调用栈反向传递
异常可能发生在被调函数中,如果在该函数中没有try-catch块来捕获相应产生的异常对象,那可以在调用函数中捕获它。
class ExcTest {
// Generate an exception.
static void genException() {
int nums[] = new int[4];
System.out.println("Before exception is generated.");
// generate an index out-of-bounds exception
nums[7] = 10;
System.out.println("this won't be displayed");
}
}
class ExcDemo2 {
public static void main(String args[]) {
try {
ExcTest.genException(); // genException()函数中发生异常
System.out.println("this won't be displayed if genException throw an exception");
} catch (ArrayIndexOutOfBoundsException exc) {
// catch the exception
System.out.println("Index out-of-bounds!");
}
System.out.println("After catch statement.");
}
}
如果有多个函数形成了调用关系,那么异常对象会沿着函数调用栈的反方向尝试被其中的try-catch异常处理块捕获,直到在某个函数中被捕获,异常就在那个函数中得到处理,程序执行流程就沿着那个函数中的try-catch块后的代码继续执行。
异常怎么用
大家可以回忆一下,我们在使用数组的地方也没写过try-catch语句来捕获可能产生的ArrayIndexOutOfBoundsException异常,我们应该怎样利用try-catch异常捕获机制和那些已经定义好的异常类呢?这要从Java对异常类的分类说起。
异常的分类
Throwable是所有异常的根类,它有两个子类Error和Exception。Error类及其子类表示由虚拟机JVM本身的问题产生的异常。这类异常类似电脑本身软/硬件出现错误,程序一般不能也不需要解决。
Exception类及其子类表示程序自己或者运行时环境出现了异常,像访问数组时下标越界产生的ArrayIndexOutOfBoundsException异常和打开和读取文件时可能产生的IOException异常都属于Exception异常的子类。Exception类又可以分为两大部分,一类是以Exception类的子类RuntimeException及RuntimeException的子类组成的异常,一类是其它类型的异常。
RuntimeException称为运行时异常,这个名字有点让人产生混淆,因为所有的异常都是在运行时产生的。RuntimeException的子类包括了数组越界(IndexOutOfBoundsException)、空指针(NullPointerException)、除零(ArithmeticException)等异常类型,它们通常是由虚拟机JVM抛出的。
异常的处理
RuntimeException、Error,以及它们的子类属于非检查异常,其它的异常,即Exception类中除RuntimeException及其子类的异常类,属于检查异常。非检查异常和检查异常,Java对它们有不同的处理要求。
非检查异常
RuntimeException、Error,以及它们的子类属于非检查异常。非检查异常是指编译器不强制程序处理的异常。程序处理异常的是说或者用try-catch进行捕获,或者在所在的方法中不捕获,但是方法要声明可能会抛出这种异常(使用throws语句)。
首先Error及其子类一般是由于JVM的问题而抛出的,一般不需要关注。而从RuntimeException及其子类的类型可以看到,它们基本是由程序的逻辑错误引起的,可能发生的地方太多了,如果在程序需要对它们都要处理,那么程序中处理异常的代码就会满天飞。而且它们一旦发生也很难恢复。因此Java把它们和Error类型的异常归于非检查异常,程序员不需要在程序中假设它们会出现并处理,程序员要做的是写好代码,避免在程序中出现这些异常。比如,写代码的过程中就避免数组下标越界。
检查异常
与非检查异常对应的是检查异常。Exception类及其子类中不属于RuntimeException及其子类的那些类都被归于检查异常,比如在读写文件过程中硬盘损坏导致程序无法读取文件而抛出的IOException类型的异常就是检查异常。检查异常的特点是这些异常的发生往往无法预期,不可能通过编码来避免,因此这类异常编译器强制程序员在写程序的时候要做最坏的打算,即必须进行异常处理。或者用try-catch进行捕获,或者让所在的方法声明(可能会)抛出此类异常,这样调用方法就知道也要进行相应的处理。比如,假设方法p1调用方法p2,而p2可能抛出一个检查异常(例如IOException),那么我们就需要采用下面两种编码方式的任何一种
- p1捕获异常
void p1(){
try{
p2();
}
catch(IOException ex){
...
}
}
- p1不捕获异常,但是要用throws语句声明它可能会抛出IOException。
throws
是Java关键字。
void p1() throws IOException{
...
p2();
...
}
无论采用哪种方案,程序员都会(强迫)了解到p2会抛出异常这件事情。Java通过这种方式,强制程序员来处理异常。
我们假设p2本身是可能抛出IOException异常的,它的定义是什么样子的呢?同样的,它需要用throws来声明会抛出异常,而真正的异常对象是通过throw语句来抛出。
void p2() throws IOException{
...
if(...){ // 异常情况出现,抛出异常对象
throw new IOException("file read exception");
}
}
注意区别throw和throws关键字的区别。throw用来抛出异常对象,throws用来声明函数可能会抛出某个异常。如果方法会抛出多个异常,那么方法声明的throws语句也需要声明多个异常类型,各类型之间用逗号’,’隔开。比如
void p2() throws IOException, AnotherExceptionType{
...
}
异常处理小结
可以说编码中应该关注的真正的”异常“应该是Exception类及其子类(排除RuntimeException及其子类)的检查异常。程序中必须处理它们,或者是用try-catch捕获它们,或者是声明方法会抛出此类异常,将处理的任务交给调用方法。
异常类型 | 归类 | 处理方式 |
---|---|---|
Exception类及其子类(排除RuntimeException及其子类) | 检查异常 | 可能会出现的异常,一定要处理 |
RuntimeException及其子类 | 非检查异常 | 可以避免的异常,不需要处理,编码中注意避免它们的出现 |
Error及其子类 | 非检查异常 | 不是代码问题,不需要处理 |
异常处理要注意的情况
注意,为了方便起见,示例代码都是处理非检查异常,实际编码中是不需要处理它们的,而是需要处理检查异常的。
多个子类异常可以用父类异常统一捕获
因为父类对象变量可以引用子类对象,因此如果try块中可能会出现多种异常,但是它们有共同的父类,那可以只用一个父类的catch来捕获它们。
// Use parent exception to catch child exception.
class ExcDemo4 {
public static void main(String args[]) {
// Here, numer is longer than denom.
int numer[] = { 4, 8, 16, 32, 64, 128, 256, 512 };
int denom[] = { 2, 0, 4, 4, 0, 8 };
for(int i=0; i<numer.length; i++) {
try {
System.out.println(numer[i] + " / " +
denom[i] + " is " +
numer[i]/denom[i]);
}
catch (RuntimeException exc) {
// catch the exception
System.out.println("run time exception occour");
}
}
}
}
如果catch异常类型有父子关系,那么要将子类异常的catch放在父类的前面
这里要注意一点,如果catch中捕获的异常类型具有父子关系,那么在catch中要注意它们的顺序,子类异常捕获catch语句要放在父类之前。
这是因为将try抛出的异常对象与各个catch异常类型相匹配时是从上往下顺序匹配的。而子类异常对象的类型是可以视为父类异常类型的。如果捕获父类异常类型的catch放在前面,子类异常对象就会被它捕获,这样的话真正捕获这个子类异常类型的catch永远得不到被执行的机会。
// Use parent exception to catch child exception.
class ExcDemo5_Wrong {
public static void main(String args[]) {
// Here, numer is longer than denom.
int numer[] = { 4, 8, 16, 32, 64, 128, 256, 512 };
int denom[] = { 2, 0, 4, 4, 0, 8 };
for(int i=0; i<numer.length; i++) {
try {
System.out.println(numer[i] + " / " +
denom[i] + " is " +
numer[i]/denom[i]);
}
catch (RuntimeException exc) { // 父类异常在前,错误的写法
// catch the exception
System.out.println("run time exception occour");
}
catch (ArithmeticException exc) {
// catch the exception
System.out.println("Can't divide by Zero!");
}
}
}
}
上面代码中第二个catch就是无法被运行到的代码,因为RuntimeException是ArithmeticException的父类,try抛出的ArithmeticException异常对象会先被第一个catch捕获,这样放在后面的真正捕获ArithmeticException的catch就无法被执行。
必须要这样安排catch的顺序
// Use parent exception to catch child exception.
class ExcDemo5_Right {
public static void main(String args[]) {
// Here, numer is longer than denom.
int numer[] = { 4, 8, 16, 32, 64, 128, 256, 512 };
int denom[] = { 2, 0, 4, 4, 0, 8 };
for(int i=0; i<numer.length; i++) {
try {
System.out.println(numer[i] + " / " +
denom[i] + " is " +
numer[i]/denom[i]);
}
catch (ArithmeticException exc) { // 子类异常在前,正确的写法
// catch the exception
System.out.println("Can't divide by Zero!");
}
catch (RuntimeException exc) {
// catch the exception
System.out.println("run time exception occour");
}
}
}
}
可以理解成,异常类型越具体就越要放在catch列表中的前面。
finally
为了在离开try-catch时执行一些语句,可以将这些语句放到finally块中,不论是否发生异常,finally语句块中的代码都会执行。它是try-catch-finally模块最后执行的语句。
class ExcDemo6 {
public static void main(String args[]) {
int nums[] = new int[4];
try {
System.out.println("Before exception is generated.");
// Generate an index out-of-bounds exception.
nums[7] = 10;
System.out.println("this won't be displayed");
}
catch (ArrayIndexOutOfBoundsException exc) {
// catch the exception
System.out.println("Index out-of-bounds!");
}finally{
System.out.println("Leaving try");
}
System.out.println("After catch statement.");
}
}
程序运行结果显示:
Before exception is generated.
Index out-of-bounds!
Leaving try
After catch statement.
如果异常没有发生,
class ExcDemo6_2 {
public static void main(String args[]) {
int nums[] = new int[4];
try {
System.out.println("Before exception is generated.");
// not Generate an index out-of-bounds exception.
nums[2] = 10;
System.out.println("this will be displayed");
}
catch (ArrayIndexOutOfBoundsException exc) {
// catch the exception
System.out.println("Index out-of-bounds!");
}finally{
System.out.println("Leaving try");
}
System.out.println("After catch statement.");
}
}
程序运行结果:
Before exception is generated.
this will be displayed
Leaving try
After catch statement.
不论异常是否发生,finally
里面的代码都会被运行。
关于检查异常的一个例子
下面代码用来复制文件,其中对文件的创建和读写均可能会产生检查异常IOException,
```java
public class CheckedExcDemo1{
public static void main(String args[]){
File source = new File("from.txt");
File dest = new File("to.txt");
copyFileUsingFileStreams(source, dest);
}
private static void copyFileUsingFileStreams(File source, File dest) {
InputStream input = null;
OutputStream output = null;
try {
input = new FileInputStream(source);
output = new FileOutputStream(dest);
byte[] buf = new byte[1024];
int bytesRead;
while ((bytesRead = input.read(buf)) > 0) {
output.write(buf, 0, bytesRead);
}
System.out.println("copy done!");
}catch(IOException e) {
System.out.println("Error open or read/write file");
} finally {
try {
if (input != null) {
input.close();
}
if (output != null) {
output.close();
}
}catch(IOException exc) {
System.out.println("Error Closing File");
}
}
}
```
除了在copyFileUsingFileStreams()方法中处理IOException,还可以不处理,但是需要在方法头声明方法会抛出异常。
```java
import java.io.*;
public class CheckedExcDemo2{
public static void main(String args[]) {
File source = new File("from.txt");
File dest = new File("to.txt");
try {
copyFileUsingFileStreams(source, dest);
}catch(IOException exc) {
System.out.println("Error open or read/write or close file");
}
}
private static void copyFileUsingFileStreams(File source, File dest)
throws IOException {
InputStream input = null;
OutputStream output = null;
try {
input = new FileInputStream(source);
output = new FileOutputStream(dest);
byte[] buf = new byte[1024];
int bytesRead;
while ((bytesRead = input.read(buf)) > 0) {
output.write(buf, 0, bytesRead);
}
System.out.println("copy done!");
} finally {
if (input != null) {
input.close();
}
if (output != null) {
output.close();
}
}
}
}
```