5.1 异常概述 5.1.1 什么是异常 生活中经常会遇到一些不正常的现象,比如人会生病、机器会坏、计算机会死机等。而 你写的代码也并不是完美的,比如要读取一个文件,如果这个文件不存在该怎么办,如果这 个文件不可读又该怎么办,等等。程序在运行过程中可能出现的这些不正常现象就称作异 常。异常(Exception),意思是例外,怎么让写的程序做出合理的处理,安全地退出,而不至 于程序崩溃。 Java是采用面向对象的方式处理异常的。当程序出现问题时,就会创建异常类对象并 抛出异常相关的信息(如异常出现的位置、原因等),从而能够更迅速地定位到问题原因。 5.1.2 异常与错误 异常与错误是很容易混淆的两个概念,异常指的是程序运行过程中出现的不正常现象, 比如文件读不到、链接打不开,影响了正常的程序执行流程,但不至于程序崩溃,这些情况对 于程序员而言是可以处理的。而错误则是程序脱离了程序员的控制,一般指程序运行时遇 到的硬件或操作系统的错误,如内存溢出、不能读取硬盘分区、硬件驱动错误等。错误是致 命的,将导致程序无法运行,同时也是程序本身不能处理的。 比如代码清单5.1就分别演示了异常和错误的区别。 代码清单5.1 Demo1Exception package com.yyds.unit5.demo; public class Demo1Exception { public static void main(String[]args) { 124 //除数为0,抛出异常。这种情况程序员可以控制,把计算时除数为0 的情况排除即可 int num = 3 / 0; //内存溢出,这是错误。这种情况程序员无法处理,因为内存溢出可能与硬件设备相关, //比如计算机内存只有256MB int[]arr = new int[1024*1024*1024]; } } 分别运行异常和错误的两行代码,程序(代码清单5.1)运行结果如图5.1所示。 图5.1 程序运行结果 5.1.3 Throwable 与异常体系 Java中,异常(Exception)与错误(Error)都继承自Throwable类。Java中定义了大量 的异常类,这些类对应了各种各样可能出现的异常事件,这些异常类都直接或间接地继 承了Exception。Exception分为运行时异常(RuntimeException)和编译时异常,Error和 RuntimeException由于在编译时不会进行检查,因此又称为不检查异常(UncheckedException), 而编译时异常会在编译时进行检测,又称为可检查异常(CheckedException)。 Java中的异常体系结构如图5.2所示。 图5.2 Java中的异常体系结构 Throwable中定义了所有异常都会用到的3个重要方法,如表5.1所示。 表5.1 Throwable主要方法 方法签名方法描述 StringgetMessage() 返回此Throwable的详细消息字符串 StringtoString() 返回此Throwable的简短描述 voidprintStackTrace() 打印异常的堆栈跟踪信息 125 5.1.4 Exception Exception是所有异常的父类,其本身是编译时异常,而它的一个子类RuntimeException 则是运行时异常。 RuntimeException和它的所有子类都是运行时异常,比如NullPointerException、 ArrayIndexOutOfBoundsException等,这些异常在程序编译时不能被检查出,往往是由逻 辑错误引起的,因此在编写程序时,应该从逻辑的角度尽可能避免这种异常情况,如代码清 单5.2所示,这个程序就是因为没有对用户输入的值进行判断,从而导致出现了异常。 代码清单5.2 Demo2RuntimeException package com.yyds.unit5.demo; import java.util.Scanner; public class Demo2RuntimeException { public static void main(String[]args) { Scanner scanner = new Scanner(System.in); String[]arr = {"北京", "上海", "武汉", "广州", "深圳"}; System.out.println("请输入要获取的城市索引"); int index = scanner.nextInt(); System.out.println("您获取的城市为: " + arr[index]); } } 程序(代码清单5.2)运行结果如图5.3所示。 图5.3 程序运行结果 Exception和它的子类(不包括RuntimeException及其子类)统称为编译时异常,比如 IOException、SQLException。这些异常在编译时会被检查出,程序员必须对其进行处理, 如代码清单5.3就是典型的编译时异常,如果不对这个异常进行处理,程序编译就不会 通过。代 码清单5.3 Demo3CheckedException package com.yyds.unit5.demo; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.InputStream; public class Demo3CheckedException { public static void main(String[]args) throws FileNotFoundException { //I/O 流,后面章节会涉及 InputStream is = new FileInputStream("D:\\abc.txt"); } } 扫一扫 126 5.2 异常处理 5.2.1 抛出异常 编写程序时,需要考虑程序可能出现的各种问题,比如编写一个方法,对于方法中的参 数就需要进行一定程度的校验。如果参数校验通不过,需要告诉调用者问题原因所在,这时 候就需要抛出异常。 Java中提供了一个throw关键字,该关键字用于抛出异常。Java中的异常本质上也是 类,抛出异常时,实际上是抛出一个异常的对象,并提供异常文本给调用者,最后结束该方法 的运行。抛出异常的语法格式如下。 throw new 异常名称(参数列表); 比如对于数组的操作,可以定义一个方法用来获取数组指定索引的值,当索引不合法 时,通过抛出异常的方式通知调用者,如代码清单5.4所示。 代码清单5.4 Demo4Throw package com.yyds.unit5.demo; import java.util.Scanner; public class Demo4Throw { public static void main(String[]args) { Scanner scanner = new Scanner(System.in); String[]arr = {"北京", "上海", "武汉", "广州", "深圳"}; System.out.println("请输入要获取的城市索引"); int index = scanner.nextInt(); String value = getValue(arr, index); System.out.println("您获取的城市为: " + value); } private static String getValue(String[]arr, int index) { //对参数校验 if(arr == null) { //抛出非法参数异常,并指定提示文本 throw new IllegalArgumentException("数组不能为空!"); } if(index < 0) { throw new IllegalArgumentException("索引不能为负数!"); } if(index >= arr.length) { //抛出数组索引越界异常 throw new ArrayIndexOutOfBoundsException("索引不可以超过数组长度!"); } return arr[index]; } } 扫一扫 127 运行程序,输入一个非法的参数,比如索引为-1,程序(代码清单5.4)运行结果如图5.4 所示。 图5.4 程序运行结果 可以看到,当参数通不过校验时,就会执行抛出异常的代码,并且提示文本也是在代码 中定义好的。 5.2.2 声明异常 程序仅仅抛出异常,而不对异常进行处理,是没有任何意义的,但处理异常之前,需要知 道调用的方法可能会抛出哪些异常,从而有针对性地处理。因此,需要将异常声明到方法 上,让调用者知道这个方法可能会抛出什么异常。 声明异常使用throws关键字(与throw非常像,需要注意),语法格式如下。 修饰符返回值类型方法名(参数列表) throws 异常类名1, 异常类名2, … { } 其中,如果方法中抛出的是运行时异常,编译期就不会强制要求开发者将异常声明在方 法上,而如果抛出的是编译时异常,则必须将这些异常全部声明在方法上。比如上面的程序 中,就可以将异常声明到方法上,如代码清单5.5所示。 代码清单5.5 Demo5Throws package com.yyds.unit5.demo; import java.util.Scanner; public class Demo5Throws { public static void main(String[]args) { Scanner scanner = new Scanner(System.in); String[]arr = {"北京", "上海", "武汉", "广州", "深圳"}; System.out.println("请输入要获取的城市索引"); int index = scanner.nextInt(); String value = getValue(arr, index); System.out.println("您获取的城市为: " + value); } //在方法上声明异常 private static String getValue(String[]arr, int index) throws IllegalArgumentException, ArrayIndexOutOfBoundsException { //参数校验 if(arr == null) { //抛出非法参数异常,并指定提示文本 throw new IllegalArgumentException("数组不能为空!"); } if(index < 0) { throw new IllegalArgumentException("索引不能为负数!"); 扫一扫 128 } if(index >= arr.length) { //抛出数组索引越界异常 throw new ArrayIndexOutOfBoundsException("索引不可以超过数组长度!"); } return arr[index]; } } 5.2.3 捕获异常 如果程序出现了异常,自己又解决不了,就需要将异常声明出来,交给调用者处理。上 面已经将异常进行了声明,此时调用者如果已经知道被调用方法可能出现哪些异常,就可以 针对这些异常进行不同的处理。 处理异常使用try…catch…finally 结构,try用于包裹住可能出现异常的代码块,在 catch中进行异常捕获,并处理异常,finally则是在抛出异常或者异常处理后执行。当异常 不进行处理时,发生异常的方法就会立即结束运行,而如果使用try…catch处理,程序就会 继续运行下去。接下来再对上面的代码进行修改,对两个异常进行处理,如代码清单5.6 所示。代 码清单5.6 Demo6TryCatch package com.yyds.unit5.demo; import java.util.Scanner; public class Demo6TryCatch { public static void main(String[]args) { Scanner scanner = new Scanner(System.in); String[]arr = {"北京", "上海", "武汉", "广州", "深圳"}; System.out.println("请输入要获取的城市索引"); int index = scanner.nextInt(); try { //可能出现异常的代码,用try 包裹住 String value = getValue(arr, index); System.out.println("您获取的城市为: " + value); }catch(IllegalArgumentException e) { //这个catch 中处理IllegalArgumentException //输出异常文本 System.out.println("程序发生了非法参数异常: "+e.getMessage()); }catch(ArrayIndexOutOfBoundsException e) { //这个catch 中处理ArrayIndexOutOfBoundsException //打印异常 e.printStackTrace(); }finally { //不管是否发生异常,最终都会执行finally System.out.println("程序运行完毕"); } } 扫一扫 129 //在方法上声明异常 private static String getValue(String[]arr, int index) throws IllegalArgumentException, ArrayIndexOutOfBoundsException { //对参数进行校验 if(arr == null) { //抛出非法参数异常,并指定提示文本 throw new IllegalArgumentException("数组不能为空!"); } if(index < 0) { throw new IllegalArgumentException("索引不能为负数!"); } if(index >= arr.length) { //抛出数组索引越界异常 throw new ArrayIndexOutOfBoundsException("索引不可以超过数组长度!"); } return arr[index]; } } 程序(代码清单5.6)运行结果如图5.5所示。 图5.5 程序运行结果 一个try必须跟随至少一个catch或者finally关键字,即try…catch结构、try…finally 结构和try…catch…finally 结构都是合法的。异常的处理是链式的,如果存在main-> methodA->methodB->methodC的调用链,此时methodC发生了异常,就会抛给methodB 处理。如果methodB处理不了或者没有处理,就会再抛给methodA 处理,如果methodA 依然没法处理,就会抛给main()方法处理,也就是交给虚拟机处理,而虚拟机处理异常的方 式简单、粗暴:直接打印异常堆栈信息并停止程序的运行。因此,在开发中为了防止死机, 程序能进行处理的异常要尽可能交由程序处理。 此外,如果一个异常并没有在方法上声明,则在代码中依然可以捕获这个异常。声明异 常只是为了方便开发者知道要处理哪些异常。 130 5.3 异常进阶 5.3.1 自定义异常 尽管Java中已经定义了大量的异常,但实际开发中这些异常并不能完全涵盖所有的业 务场景,不能通过异常文本判断业务逻辑问题所在。 例如,登录场景中,绝大多数网站为了防止暴力撞库,对于用户不存在和密码错误两种 场景的提示文本都是“用户名或密码错误”,这是为了防止不法分子根据提示文本而推断出 网站的用户。 但是,对于开发者而言,需要在日志中能够区分某个“用户名或密码错误”究竟是用户不 存在,还是密码错误,此时就需要UserNotFoundException和PasswordWrongException两 个异常。很明显,Java中不可能事先定义好这些异常,因此就需要用户自定义一些与业务 场景相关的异常。 自定义异常语法很简单,就是创建一个类并继承Exception或者RuntimeException,提 供相应的构造方法,如下所示。 修饰符class 自定义异常名extends Exception 或RuntimeException { public 自定义异常名() { //默认调用父类无参构造方法 } public 自定义异常名(String msg) { //调用父类具有异常信息的构造方法 super(msg); } } 大多数情况下,不需要在构造方法中定义其他逻辑,直接调用父类异常对应的构造方法 即可,自定义异常就是这么简单。 下面编写一个登录案例,用户输入用户名和密码,对于用户名不存在和密码错误两种场 景,均提示“用户名或密码错误”,但抛出异常时则抛出不同的异常。 首先定义UserNotFoundException 和PasswordWrongException 两个异常类,如下 所示。 package com.yyds.unit5.demo; public class UserNotFoundException extends Exception{ public UserNotFoundException() { } public UserNotFoundException(String message) { super(message); } } 扫一扫 131 package com.yyds.unit5.demo; public class PasswordWrongException extends RuntimeException{ public PasswordWrongException() { } public PasswordWrongException(String message) { super(message); } } 尽管它们继承的异常不同,但内部的代码都是一模一样的。接下来编写登录逻辑,如代 码清单5.7所示。 代码清单5.7 Demo7Login package com.yyds.unit5.demo; import java.util.Scanner; public class Demo7Login { public static void main(String[]args) { Scanner scanner = new Scanner(System.in); System.out.print("请输入用户名: "); String username = scanner.next(); System.out.print("请输入密码: "); String password = scanner.next(); try { login(username, password); //如果方法成功运行完毕,说明两个异常都没有触发,登录成功 System.out.println("登录成功"); }catch(UserNotFoundException e) { e.printStackTrace(); System.out.println(e.getMessage()); }catch(PasswordWrongException e) { e.printStackTrace(); System.out.println(e.getMessage()); } } private static void login(String username, String password) throws UserNotFoundException, PasswordWrongException { if(!"admin".equals(username)) { //用户名不存在,对于用户而言要提示用户名或密码错误,但开发者需要知道真正的 //原因 throw new UserNotFoundException("用户名或密码错误!"); } if(!"123456".equals(password)) { //密码错误 throw new PasswordWrongException("用户名或密码错误!"); } } } 程序(代码清单5.7)运行结果如图5.6所示。 132 图5.6 程序运行结果 5.3.2 方法重写中的异常 当一个类的方法声明了一个编译时异常后,它的子类如果重写该方法,重写方法声明的 异常不能超过父类的异常,这里只遵循如下两点即可。运行时异常不受这两点约束。 (1)父类方法没有声明异常,子类重写该方法不能声明异常,如下所示。 class Parent { public void method1() {} }c lass Child extends Parent { //编译错误 @Override public void method1() throws Exception {} } (2)父类方法声明了异常,子类重写该方法可以不声明异常,或者只声明父类的异常或 该异常的子类,如下所示。 class Parent { public void method1() throws IOException {} }c lass Child extends Parent { //编译错误 @Override public void method1() throws FileNotFoundException {} } 扫一扫