第九章 Java 异常和错误

异常:异于常态

一、Java中的异常与错误

1.1、什么是异常与错误

大多数情况下,程序运行过程中不可能一帆风顺,不可避免的会出现一些非正常的现象,比如用户输入了非法数据、程序要读取的文件并不存在、某一步运算出现除数是0的情况、访问的数组下标越界了、网络临时中断、甚至内存不够用而产生内存溢出等等。引起这些非正常现象的原因不一而足,有的是因为用户错误引起,有的是程序错误引起的,还有其它一些是因为物理错误引起的。如果程序中出现了非正常的现象而没有得到及时的处理,程序可能会挂起、自动退出、甚至崩溃,程序的健壮性会大大降低。
在Java中,这些非正常现象可以分为异常错误

  • 异常:一般指在程序运行过程中,可以预料的非正常现象,异常一般是开发人员可控的,异常可以、也应当在程序中被捕获并进行相应的处理,以保证程序的健壮。
  • 错误:一般指在程序运行过程中,不可预料的非正常现象,错误对于程序来说往往是致命的,一般是开发人员很难处理、无法控制的,因此也不需要开发人员进行处理。

1.2、相关类继承关系

由于异常和错误总是难免的,良好的应用程序除了具备用户所要求的基本功能外,还应该具备准确定义并描述错误和异常,及预见并处理可能发生的各种异常的能力。Java定义了一系列用以描述错误和异常的类,并且引进了一套用以捕获、抛出、处理异常的机制。
在Java中,异常和错误都直接或间接继承自Throwable类,Throwable类有两个直接派生类,分别是Error类和Exception类,Error类及其派生类用来描述错误,Exception类及其派生类用来描述异常。下面是图示:
在这里插入图片描述

错误一般发生在严重故障时。虚拟机会捕获错误、实例化相应Error类的派生类对象并抛出。通常发生错误的情况脱离开发人员的控制,也无法预料,所以在开发过程中通常不用刻意考虑。但开发人员应该认识一些可能会遇到的Error类的派生类,方便在发生错误时定位、理解所发生的问题。

异常Exception类及其派生类来表示,Java中的异常也可以分成两部分:

  • 检查性异常
  • 运行时异常

说明:

  • 检查性异常:除了RuntimeException类及其派生类所代表的异常之外,其他Exception类的派生类所代表的异常都是检查性异常。检查性异常在编译时不能简单的忽略,必须在源码中进行捕获处理,这是编译检查的一部分。检查性异常也被称作设计时异常
  • 运行时异常RuntimeException类及其派生类所代表的异常都是运行时异常。运行时异常是可以通过开发人员的努力而避免的。与检查性异常相反的是,运行时异常可以在编译时忽略。运行时异常也被称作非检查性异常

二、异常

Java中的异常分为检查性异常运行时异常(非检查性异常),检查性异常编译时不能忽略,强制要求开发人员在开发阶段捕获处理;运行时异常不强制要求在代码中捕获并处理,但开发人员应在编码过程中仔细完善代码逻辑,尽量避免运行时异常的发生。

下面是一个代码逻辑错误而导致运行时异常的示例
例1:

import java.util.Scanner;

public class Test {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        System.out.println("请输入被除数与除数:");
        int num1 = scanner.nextInt();
        int num2 = scanner.nextInt();

        System.out.println("开始");
        int result = num1 / num2;
        System.out.println("结果:"+num1+"/"+num2+" = "+result);
        System.out.println("结束");

    }
}

执行输出结果:
(1)、正常数据

  • 被除数:8
  • 除数:2
请输入被除数与除数:
8 2
开始
结果:8/2 = 4
结束

(2)、非正常数据

  • 被除数:8
  • 除数:0
请输入被除数与除数:
8 0
开始
Exception in thread "main" java.lang.ArithmeticException: / by zero
	at cn.edu.luas.exception.Math.main(Math.java:13)

说明:
变量num2的值为0,当num2作为除数时,出现ArithmeticException异常,程序退出,语句

    System.out.println("结束");

不再执行。

(3)、非正常数据

  • 被除数:8
  • 除数:0.0
请输入被除数与除数:
8 0.0
开始
结果:Infinity
结束

解释:结果显示为无穷大

(4)、非正常数据

  • 被除数:0
  • 除数:0.0
请输入被除数与除数:
0 0.0
开始
结果:NaN
结束

解释:结果显示为NaN(not a number)。

2.1、JDK中常见的异常类

JDK中已经内置了很多异常类,开发人员在开发调式代码的过程中应该逐步认识并熟悉它们。

JDK中常见的运行时异常(非检查性异常)的类:

序号异常类名说明
1ArithmeticException当出现异常的运算条件时,抛出此异常。例如,一个整数"除以零"
2ArrayIndexOutOfBoundsException用非法索引访问数组时抛出的异常。如果索引为负或大于等于数组大小,则该索引为非法索引
3ArrayStoreException试图将错误类型的对象存储到一个对象数组时抛出的异常
4ClassCastException当试图将对象强制转换为不是实例的子类时,抛出该异常
5IllegalArgumentException抛出的异常表明向方法传递了一个不合法或不正确的参数
6IllegalThreadStateException线程没有处于请求操作所要求的适当状态时抛出的异常
7IndexOutOfBoundsException指示某排序索引(例如对数组、字符串或向量的排序)超出范围时抛出
8NullPointerException当应用程序试图在需要对象的地方使用 null 时,抛出该异常
9NumberFormatException当应用程序试图将字符串转换成一种数值类型,但该字符串不能转换为适当格式时,抛出该异常
10StringIndexOutOfBoundsException此异常由字符串方法抛出,指示索引或者为负,或者超出字符串的大小
11UnsupportedOperationException当不支持请求的操作时,抛出该异常

JDK中常见的代表检查性异常的类:

序号异常类名说明
1FileNotFoundException文件操作时,找不到文件,抛出该异常
2ClassNotFoundException应用程序试图加载类时,找不到相应的类,抛出该异常
3IllegalAccessException拒绝访问一个类的时候,抛出该异常
4NoSuchFieldException请求的字段不存在,抛出该异常
5NoSuchMethodException请求的方法不存在,抛出该异常
6InterruptedException一个线程被另一个线程中断,抛出该异常

2.2、捕获和处理异常

2.2.1、try…catch语句块

Java代码中,使用try...catch语句块可以捕获异常并进行处理,try...catch语句块放在异常可能发生的地方,try...catch语句块中的代码称为保护代码。使用try...catch语句块的语法如下:

try {
   // 程序代码
} catch (ExceptionName e) {
   // catch 块
}

说明:

  • 1、可能发生异常的程序代码放在try语句块中。
  • 2、catch关键字后面紧跟的()中包含要捕获异常类型的声明,catch语句块中包含的代码一般为对异常的处理。
  • 3、程序运行过程中,如果try语句块内的代码没有出现任何异常,后面的catch语句块不执行;而当try语句块内的代码发生一个异常时,try语句块中的后续代码不再执行,系统会实例化一个该异常对应的异常类对象,后面的catch语句块会被检查,如果该异常类对象 is a catch关键字后面所声明异常类的对象,该对象会被传递到catch语句块中,该catch语句块中的代码将被执行。

例2:

import java.util.Scanner;

public class Test {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        System.out.println("请输入被除数与除数:");
        int num1 = scanner.nextInt();
        int num2 = scanner.nextInt();

        System.out.println("开始");
        int result = 0;
        try {
            result = num1 / num2;
            System.out.println("结果:"+num1+"/"+num2+" = "+result);
        } catch (ArithmeticException e) {
            e.printStackTrace();
        }
        System.out.println("结束");
    }
}

输出结果:

请输入被除数与除数:
8 0
开始
结束
java.lang.ArithmeticException: / by zero
	at cn.edu.luas.exception.Test.main(Test.java:15)

说明:
try语句块包裹了可能出现异常的代码result = num1 / num2
try语句块中出现算术运算异常时,系统实例化了一个ArithmeticException类的对象,并检查该对象是否 is a catch关键字后面所声明异常类型的对象,如果是,将对象传入catch语句块。

catch语句块中,异常类对象e调用了printStackTrace()方法,该方法可以向控制台打印异常信息

异常被捕获处理后,程序没有退出,catch语句块之后的后续代码得以执行,本例中语句System.out.println("结束")被执行。

在异常发生时,所有的异常信息都被封装成为一个个异常类的对象,异常类从Throwable类继承了一些常用的方法,用以获取异常信息,下面列出异常类常用的API

方法返回值类型方法说明
getMessage()String返回关于发生的异常的详细信息。这个消息在Throwable 类的构造函数中初始化了
getCause()Throwable返回一个Throwable 对象代表异常原因
printStackTrace()void打印toString()结果和栈层次到System.err,即错误输出流
getStackTrace()StackTraceElement []返回一个包含堆栈层次的数组。下标为0的元素代表栈顶,最后一个元素代表方法调用堆栈的栈底
fillInStackTrace()Throwable用当前的调用栈层次填充Throwable 对象栈层次,添加到栈层次任何先前信息中
toString()String使用getMessage()的结果返回类的串级名字

2.2.2、多重 catch

一个 try语句块后面可以跟随多个 catch语句块,用于对try语句块中可能发生的多个异常进行捕获,这种情况也被称作多重捕获。

多重catch语句块的语法格式:

try {
   // 程序代码
} catch (ExceptionName1 e1){
  // catch 块1
} catch (ExceptionName2 e2){
  // catch 块2
} catch (ExceptionName3 e3){
  // catch 块3
}

在有多重catch语句块的情况下,如果try语句块中发生异常,try语句块中的后续代码不再执行,系统会实例化一个相应异常类型的对象,并检查从上往下第一个catch关键字后面声明的异常类型,符合 is a 关系时,将对象传入catch语句块,否则继续往下检查第二个catch关键字后面声明的异常类型,直到找到对应的catch语句块或通过所有的catch语句块为止。
下面是一个针对多个可能发生的检查性异常,使用多重catch的示例:
例3:

import java.io.*;
public class Test {
    public static void main(String[] args) {
        System.out.println("开始");
        // 实例化file对象
        File file = new File("D:\\test.txt");
        try {
            // 获取file对象的输入流
            FileInputStream in = new FileInputStream(file);
            // 读取输入流中的第一个字节
            int i = in.read();
        } catch (FileNotFoundException e) {  // 第一个catch语句块
            e.printStackTrace();
        } catch (IOException e) {    // 第二个catch语句块
            e.printStackTrace();
        }
        System.out.println("结束");
    }
}

执行输出结果:

开始
结束
java.io.FileNotFoundException: D:\test.txt (系统找不到指定的文件。)
	at java.io.FileInputStream.open0(Native Method)
	at java.io.FileInputStream.open(FileInputStream.java:195)
	at java.io.FileInputStream.<init>(FileInputStream.java:138)
	at cn.edu.luas.exception.Test.main(Test.java:14)

说明:
语句FileInputStream in = new FileInputStream(file)可能会发生FileNotFoundException,语句int i = in.read();可能发生IOException,针对可能发生的这两个异常,使用了两个catch语句块。

如果要捕获的异常类之间没有继承关系,各类的catch语句块顺序无关紧要,但当它们之间有继承关系时,应该将派生类的catch语句块放在基类的catch语句块之前。本例中,FileNotFoundExceptionIOException的派生类,故应该写在前面。
多重捕获也可以合并写在一个catch语句块中,语法格式如下:

try {
	// 程序代码
} catch (ExceptionName1 | ExceptionName2 [| ExceptionName3 ... | ExceptionNameN] e){
	// catch 块
}

需要注意的是,这种写法仅限于要捕获的各异常类之间没有继承关系的情况。

2.2.3、finally语句块

try...catch语句块后,可以使用finally语句块,无论try语句块中的代码是否发生异常,finally语句块中的代码总是会被执行,也因此,finally语句块中适合进行清理回收资源收尾善后性质的工作。

try...catch语句块后跟随finally语句块需要使用finally关键字,语法格式:

try {
   // 程序代码
} catch (ExceptionName1 e1){
  // catch 块1
} catch (ExceptionName2 e2){
  // catch 块2
} finally {
  // 必须执行的代码,适合收尾、善后等
}

例4:

public class Test {
    public static void main(String[] args) {
        System.out.println("开始");
        // 实例化file对象
        File file = new File("D:\\test.txt");
        FileInputStream in = null;
        try {
            // 获取file对象的输入流
            fis = new FileInputStream(file);
            // 读取输入流中的第一个字节
            int i = fis.read();
        } catch (FileNotFoundException e) {  // 第一个catch语句块
            e.printStackTrace();
        } catch (IOException e) {    // 第二个catch语句块
            e.printStackTrace();
        } finally {                 // finally语句块
            try {
                if (fis != null) {
                    fis.close();  // 关闭输入流,这个操作本身也可能发生IOException,要求强制检查
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
            System.out.println("finally");
        }
        System.out.println("结束");
    }
}

执行输出结果:

开始
finally
结束
java.io.FileNotFoundException: D:\test.txt (系统找不到指定的文件。)
	at java.io.FileInputStream.open0(Native Method)
	at java.io.FileInputStream.open(FileInputStream.java:195)
	at java.io.FileInputStream.<init>(FileInputStream.java:138)
	at cn.edu.luas.exception.Test.main(Test.java:15)

说明
本例完善了上一示例,在获取输入输出流,进行完读写操作后,应当将输入输出流关闭,故在本例中,使用了finally语句块,无论是否发生异常,finally语句块中的语句in.close()都会将输入流关闭。

细心的开发者可能会考虑这样一个问题:如果不使用finally语句块,而是直接将finally语句块中的语句放在catch语句块外的后续代码中,无论try语句块中是否发生异常,这些语句不是仍然会执行吗?finally语句块又有什么使用的必要呢?

事实上,考虑try语句块或catch语句块中有return语句的情况,catch语句块外的后续代码不一定能得到执行的机会,而就算try语句块或catch语句块中有return语句,finally语句块中的代码仍然会被执行,甚至,如果finally语句块中也有return语句时,会覆盖try语句块或catch语句块中的返回值,因此,使用finally语句块来执行收尾善后工作是必要的,也是开发人员应该养成的一个良好的编码习惯。

2.3、抛出异常

2.3.1、throw关键字

通常,异常是自动抛出的。但开发人员也可以通过throw关键字抛出异常。(在方法中抛出一异常。)
throw语句抛出异常的语法格式:
throw new 异常类名([异常描述]);
下面是一个示例:
/**

  • 年满18周岁可报考驾校,如果年龄不满18周岁不允许包括驾校。
  • 从键盘上输入年龄,如果年龄不足18岁,抛出异常
    */
public class Test {
    public static void main(String[] args) {
        System.out.println("请输入年龄:");
        int age = new Scanner(System.in).nextInt();
        validateAge(age);
        System.out.println("年龄超过18岁,允许报考驾校");
    }
/**
 * 校验年龄是否不足18岁的方法
 * @param age 要检验的年龄
 */
public static void validateAge(int age){
    if(age < 18){
        throw new RuntimeException("年龄不足18岁,不允许考驾校");
    }
}

}
执行输出结果:
请输入年龄:

16
Exception in thread "main" java.lang.RuntimeException: 年龄不足18岁,不允许考驾校
	at com.codeke.java.test.Test.validateAge(Test.java:24)
	at com.codeke.java.test.Test.main(Test.java:14)

说明:
本例的validateAge(int age)方法中,当传入的参数age不足18时,由开发人员实例化了一个运行时异常,并使用throw关键字将该异常对象抛出。

2.3.2、throws关键字

对于需要捕获的异常(基本上是检查性异常),如果一个方法中没有捕获,调用该方法的主调方法应该捕获并处理该异常。为了明确某个方法不捕获某个异常,而让调用该方法的主调方法捕获异常,可以在方法声明的时候,使用throws关键字抛出该类异常。
throws语句在方法声明中抛出某类型异常的语法:

[修饰符] 返回值类型 方法名([参数列表]) throws 异常类型名 {
	// 方法体
}

下面是一个示例:

public class Test {
    public static void main(String[] args) {
        try {
            System.out.println("main start");
            readFile();
            System.out.println("main end");
        } catch (IOException e) {
            e.printStackTrace();
            System.out.println("main catched");
        }
        System.out.println("over");
    }
/**
 * 读取文件
 * @throws IOException IO异常
 */
public static void readFile() throws IOException {
    File file = new File("D:\\test.txt");
    // 获取file对象的输入流
    FileInputStream in = new FileInputStream(file);
    // 读取输入流中的第一个字节
    int i = in.read();
}

}
执行输出结果:

main start
java.io.FileNotFoundException: D:\test.txt (系统找不到指定的文件。)
	at java.io.FileInputStream.open0(Native Method)
	at java.io.FileInputStream.open(FileInputStream.java:195)
	at java.io.FileInputStream.<init>(FileInputStream.java:138)
	at com.codeke.java.test.Test.readFile(Test.java:26)
	at com.codeke.java.test.Test.main(Test.java:10)
main catched
over

说明:
本例中,readFile()方法中可能出现FileNotFoundException和IOException,但在readFile()方法并不想直接捕获处理这些异常,故可以在方法声明时使用throws关键字抛出异常给主调方法(由于FileNotFoundException是IOException的派生类,故抛出IOException即可),此时,在主调方法中仍然需要捕获并处理被调方法抛出的异常。

2.4、自定义异常

系统定义的异常不能代表应用程序中所有的异常,有时开发人员需要声明自定义异常。声明自定义异常非常简单,将系统定义的异常类作为基类,声明派生类即可。一般在声明自定义异常时,会选择继承Exception类或RuntimeException类。从Exception类继承的自定义异常是检查性异常,在应用程序中必须使用try...catch语句块捕获并处理;不过自定义异常一般是可控的异常,大部分情况下不需要捕获,因此让自定义异常直接继承自RuntimeException类是开发人员更多情况下的选择。
下面是一个示例:
InputException类的源码:
package com.codeke.java.test;
/**

  • 输入异常
    */
public class InputException extends RuntimeException {
    public InputException(String message) {
        super(message);//super()是什么作用???
    }
}

测试类Test类的源码:

/**
 * 校验输入的姓名不为空且长度是否不小于6位
 */
public class Test {
    public static void main(String[] args) {
        System.out.println("请输入用户名");
        String name = new Scanner(System.in).next();
        validateName(name);
    }

    /**
     * 校验姓名是否存在并且长度不小于6位
     * @param name 要校验的姓名
     */
    public static void validateName(String name) {
        if (name == null || name.length() < 6) {
            throw new InputException("用户名必须填写,长度不小于6位");
        }
    }
}

执行输出结果:
请输入用户名:

tom
Exception in thread "main" com.codeke.java.test.InputException: 用户名必须填写,长度不小于6位
	at com.codeke.java.test.Test.validateName(Test.java:22)
	at com.codeke.java.test.Test.main(Test.java:13)

说明:
本例中,创建了一个自定义异常InputException,它继承自RuntimeException类,故是一个运行时异常,不强制要求使用try...catch语句块捕获并处理。
在本例的validateName(String name)方法中,当传入的参数name为空或长度小于6位时,实例化了一个自定义异常,即InputException类的对象,并使用throw关键字将该异常对象抛出。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值