JAVA学习笔记 10 - 异常和错误

本文是Java基础课程的第十课。主要介绍Java中的异常处理机制,包括异常与错误、常见异常、try…catch语句块、throw\throws关键字、自定义异常等内容

一、Java中的异常与错误

1、什么是异常与错误

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

在Java中,这些非正常现象可以分为异常错误

异常一般指在程序运行过程中,可以预料非正常现象,异常一般是开发人员可控的,异常可以、也应当在程序中被捕获并进行相应的处理,以保证程序的健壮。

错误一般指在程序运行过程中,不可预料非正常现象,错误对于程序来说往往是致命的,一般是开发人员很难处理无法控制的,因此也不需要开发人员进行处理。

2、相关类继承关系

由于异常和错误总是难免的,良好的应用程序除了具备用户所要求的基本功能外,还应该具备准确定义并描述错误和异常,及预见并处理可能发生的各种异常的能力。Java定义了一系列用以描述错误和异常的类,并且引进了一套用以捕获、抛出、处理异常的机制。

在Java中,异常错误直接或间接继承Throwable类,Throwable类有两个直接派生类,分别是Error类和Exception类,Error类及其派生类用来描述错误Exception类及其派生类用来描述异常。下面是图示:
在这里插入图片描述
错误一般发生在严重故障时。虚拟机会捕获错误、实例化相应Error类的派生类对象并抛出。通常发生错误的情况脱离开发人员控制,也无法预料,所以在开发过程中通常不用刻意考虑。但开发人员应该认识一些可能会遇到的Error类的派生类,方便在发生错误时定位、理解所发生的问题。

异常Exception类及其派生类来表示,Java中的异常也可以分成两部分,一部分是检查性异常,一部分是运行时异常。具体说明如下:

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

二、异常

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

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

package com.codeke.java.test;

public class Test {
    public static void main(String[] args) {
        int num1 = 10;
        int num2 = 0;
        System.out.println("begin");
        int result = num1 / num2;
        System.out.println("end");
    }
}

执行输出结果:

begin
Exception in thread "main" java.lang.ArithmeticException: / by zero
	at com.codeke.java.test.Test.main(Test.java:8)

说明:

  • 本例中,变量num2的值为0,当num2作为除数时,出现ArithmeticException异常,程序退出,语句System.out.println("end")不再执行。

1、JDK中常见的异常类

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

下面是一些JDK中常见的代表运行时异常的类:

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

下面是一些JDK中常见的代表检查性异常的类:

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

2、捕获和处理异常

2.1、try…catch语句块

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

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

说明:

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

下面是一个示例:

package com.codeke.java.test;

public class Test {
    public static void main(String[] args) {
        int num1 = 10;
        int num2 = 0;
        System.out.println("begin");
        // 使用try...catch 包裹可能发生异常的代码
        try {
            int result = num1 / num2;
        } catch (ArithmeticException e) {
            e.printStackTrace();  // 对异常的处理
        }
        // 后续代码仍将得到执行
        System.out.println("end");
    }
}

执行输出结果:

begin
java.lang.ArithmeticException: / by zero
	at com.codeke.java.test.Test.main(Test.java:10)
end

说明:

  • 本例中,用try语句块包裹了可能出现异常的代码int result = num1 / num2
  • try语句块中出现算术运算异常时,系统实例化了一个ArithmeticException类的对象,并检查该对象是否 is acatch关键字后面所声明异常类型的对象,如果是,将对象传入catch语句块。
  • catch语句块中,异常类对象e调用了printStackTrace()方法,该方法可以向控制台打印异常信息。
  • 异常被捕获处理后,程序没有退出,catch语句块之后的后续代码得以执行,本例中语句System.out.println("end")被执行。

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

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

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的示例:

package com.codeke.java.test;

import java.io.*;

public class Test {
    public static void main(String[] args) {
        System.out.println("begin");
        // 实例化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("end");
    }
}

执行输出结果:

begin
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.main(Test.java:12)
end

说明:

  • 本例中,语句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.3、finally语句块

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

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

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

下面是一个示例:

package com.codeke.java.test;

import java.io.*;

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

执行输出结果:

begin
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.main(Test.java:13)
finally
end

说明:

  • 本例完善了上一示例,在获取输入输出流,进行完读写操作后,应当将输入输出流关闭,故在本例中,使用了finally语句块,无论是否发生异常,finally语句块中的语句in.close()都会将输入流关闭。
  • 细心的开发者可能会考虑这样一个问题:如果不使用finally语句块,而是直接将finally语句块中的语句放在catch语句块外的后续代码中,无论try语句块中是否发生异常,这些语句不是仍然会执行吗?finally语句块又有什么使用的必要呢?事实上,考虑try语句块catch语句块中有return语句的情况,catch语句块外的后续代码不一定能得到执行的机会,而就算try语句块catch语句块中有return语句,finally语句块中的代码仍然会被执行,甚至,如果finally语句块中也有return语句时,会覆盖try语句块catch语句块中的返回值,因此,使用finally语句块来执行收尾善后工作是必要的,也是开发人员应该养成的一个良好的编码习惯。

3、抛出异常

3.1、throw关键字

通常,异常是自动抛出的。但开发人员也可以通过throw关键字抛出异常throw语句抛出异常的语法格式如下:

throw new 异常类名([异常描述]);

下面是一个示例:

package com.codeke.java.test;

import java.util.Scanner;

/**
 * 年满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关键字将该异常对象抛出。

3.2、throws关键字

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

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

下面是一个示例:

package com.codeke.java.test;

import java.io.*;

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()方法中可能出现FileNotFoundExceptionIOException,但在readFile()方法并不想直接捕获处理这些异常,故可以在方法声明时使用throws关键字抛出异常给主调方法(由于FileNotFoundExceptionIOException的派生类,故抛出IOException即可),此时,在主调方法中仍然需要捕获并处理被调方法抛出的异常。

4、自定义异常

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

下面是一个示例:
InputException类的源码:

package com.codeke.java.test;

/**
 * 输入异常
 */
public class InputException extends RuntimeException {
    public InputException(String message) {
        super(message);
    }
}

测试类Test类的源码:

package com.codeke.java.test;

import java.util.Scanner;

/**
 * 校验输入的姓名不为空且长度是否不小于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关键字将该异常对象抛出。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章