上一次学完了Java的核心类与API,这次来学习异常处理。我们都知道,Java语言具有健壮性和安全性,而异常处理机制就是其重要保证。如下

一、类型

错误(Error)和异常(Exception)。这里只讨论 Exception 类型的异常处理。

1、错误(Error)

Error 的异常通常是灾难性的致命错误,不是程序可以控制的。正常情况下不大可能出现,绝大部分的 Error 都会导致程序处于非正常、不可恢复状态。所以不需要被开发者捕获。常见错误类型如下

NoClassDefFoundError (找不到 class 定义异常)
StackOverflowError (深递归导致栈被耗尽而抛出的异常)
OutOfMemoryError (内存溢出异常)

2、异常(Exception)

Exception 是程序正常运行过程中可以预料到的意外情况,并且应该被开发者捕获并进行异常处理。异常又可分为运行时异常非运行时异常。常见异常类型如下
1)运行时异常

NullPropagation (空指针异常)
ClassCastException (类型强制转换异常)
IllegalArgumentException (传递非法参数异常)
IndexOutOfBoundsException (下标越界异常)
NumberFormatException (数字格式异常)等

2)非运行时异常

ClassNotFoundException (找不到指定 class 的异常)
IOException (IO 操作异常)等

二、解决

异常处理关键字:try、catch、throw、throws 和 finally。

try catch 语句用于捕获并处理异常
throw 语句用于拋出异常
throws 语句用于声明可能会出现的异常
finally 语句用于在任何情况下(除特殊情况外)都必须执行的代码。

1、异常的捕获与抛出

1.1 try catch捕获异常

捕获异常:找到合适的 catch 块,并把该异常对象交给该 catch 块处理。
1)几点注意

处理多种异常类型时,必须先捕获子类类型异常,后捕获父类类型异常,否则编译报错(最后捕获 Exception 类型异常,确保异常对象能被捕获到)

不管 try 块中的代码是否出现异常及catch 块是否被执行,甚至在 try 块或 catch 块中执行了 return 语句,finally 块总会被执行(除非在 try 块或会执行的 catch 块中调用退出 JVM 的相关方法)

异常处理语法结构中只有 try 块是必需的,catch 块和 finally 块都是可选的,但 catch 块和 finally 块至少出现其一,也可以同时出现(如try…catch、try…catch…finally、try…finally)

当程序执行 try 块、catch 块时遇到 return 或 throw 语句时,系统不会立即结束该方法,而是去寻找该异常处理流程中是否包含 finally 块,如果有 finally 块,系统立即开始执行 finally 块——只有当 finally 块执行完成后,系统才会再次跳回来执行 try 块、catch 块里的 return 或 throw 语句;如果 finally 块里也使用了 return 或 throw 等导致方法终止的语句,finally 块已经终止了方法,系统将不会跳回去执行 try 块、catch 块里的任何代码(有点绕,注意理解)

2)流程

  • try中发生异常,直接从异常处跳到catch语句进行捕获
  • try中没有异常,try块正常结束后跳过catch,执行catch后的语句(没有则结束)

3)输出异常信息方法

  • printStackTrace():指出异常的类型、性质、栈层次及出现在程序中的位置
  • getMessage():输出错误的性质。
  • toString():给出异常的类型与性质。

4)案例

1
2
3
4
5
6
7
8
9
10
11
12
13
public class ExceptionDemo {
public static void main(String[] args){
try { // 可能发生异常的语句
int[] arr={1,3,5,8};
System.out.println(arr[5]); //创建一个异常
} catch (Exception e){ // 处理异常语句
e.printStackTrace(); //跟踪异常,指出异常的类型、性质、栈层次及出现在程序中的位置
System.out.println("数组下标越界");
}finally { //无论是否发生异常,finally中的代码总会被执行
System.out.println("over!");
}
}
}

运行结果

1
2
3
4
java.lang.ArrayIndexOutOfBoundsException: Index 5 out of bounds for length 4
at test2.ExceptionDemo.main(ExceptionDemo.java:7)
数组下标越界
over!

再看一个多重捕获块(即多重catch语句)的示例

原则:存在父子,先子后父。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import java.util.InputMismatchException;
import java.util.Scanner;
public class ExceptionDemo {
public static void main(String[] args){
try {
Scanner sc = new Scanner(System.in);
System.out.println("请录入第一个数:");
int num1 = sc.nextInt();
System.out.println("请录入第二个数:");
int num2 = sc.nextInt();
System.out.println("商:" + num1 / num2);
} catch (ArithmeticException e) {
System.out.println("除数不能为0");
} catch (InputMismatchException e) {
System.out.println("本数不是int类型的数据!");
} catch (Exception e) {
System.out.println("对不起,你的程序出现异常");
} finally {
System.out.println("感谢您使用计算器!");
}
}
}

运行结果

1
2
3
4
5
6
请录入第一个数:
1
请录入第二个数:
0
除数不能为0
感谢您使用计算器!

5)try…catch…finally

Java的垃圾回收机制不会回收任何物理资源,只回收堆内存中对象所占用的内存。为了确保一定能回收 try 块中打开的物理资源,异常处理机制提供了 finally 代码块。

Java 7 之后提供了自动资源管理(Automatic Resource Management)技术。

案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import java.util.Scanner;
public class ExceptionDemo {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
System.out.println("Windows 系统已启动!");
String[] arr = { "记事本", "计算器", "浏览器" };
try {
for (int i = 0; i < arr.length; i++) { // 循环输出pros数组中的元素
System.out.println(i + 1 + ":" + arr[i]);
}
System.out.println("是否运行程序:");
String answer = sc.next();
if (answer.equals("y")) {
System.out.println("请输入程序编号:");
int no = sc.nextInt();
System.out.println("正在运行程序[" + arr[no - 1] + "]");
}
} catch (Exception e) {
e.printStackTrace();
} finally {
System.out.println("谢谢使用!");
}
}
}

运行结果

1
2
3
4
5
6
7
8
9
10
11
Windows 系统已启动!
1:记事本
2:计算器
3:浏览器
是否运行程序:
y
请输入程序编号:
8
java.lang.ArrayIndexOutOfBoundsException: Index 7 out of bounds for length 3
at test2.ExceptionDemo.main(ExceptionDemo.java:18)
谢谢使用!

总结

finally 与 try 语句块匹配的语法格式会导致异常丢失,所以不常见。

1.2 抛出异常

抛出异常:生成异常对象,并把它提交给运行时系统的过程。
throws :方法上声明要拋出的异常,表示此方法不处理异常,而交给方法调用处进行处理。
1)几点注意

任何方法都可以使用throws关键字声明异常类型,包括抽象方法。

子类重写父类中的方法,子类方法不能声明抛出比父类类型更大的异常。

使用了throws的方法,调用时必须处理声明的异常,要么使用try-catch,要么继续使用throws声明。

throw:方法内部拋出异常对象
1)几点注意

throw关键字用于显式抛出异常,抛出的是一个异常类的实例化对象。

throw用于方法体中,要么使用try/catch捕获异常,要么throws异常。

案例

1
2
3
4
5
6
7
8
9
10
import java.rmi.RemoteException;
public class ExceptionDemo {
public void deposit() throws RemoteException {
throw new RemoteException();
}
public static void main(String[] args) throws RemoteException {
ExceptionDemo ec=new ExceptionDemo();
ec.deposit();
}
}

运行结果

1
2
3
Exception in thread "main" java.rmi.RemoteException
at ExceptionDemo.deposit(ExceptionDemo.java:7)
at ExceptionDemo.main(ExceptionDemo.java:11)

2、自定义异常

实现自定义异常类需要继承 Exception 类或其子类,如果自定义运行时异常类需继承 RuntimeException 类或其子类。
1)形式:

<自定义异常名>,规范上自定义异常名为XXXException(XXX表示异常的作用)

2)构造方法(两个)

  • 无参构造方法(默认)
  • String类型的构造方法(以字符串的形式接收一个定制的异常消息,并将该消息传递给超类的构造方法)

3)案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
//编写一个程序,对用户注册时的年龄进行验证,检测是否在 0~100 岁。
import java.util.InputMismatchException;
import java.util.Scanner;
class MyException extends Exception{ //自定义异常类MyException
public MyException(){ //无参构造
super();
}
public MyException(String str){ //String类型的构造方法
super(str);
}
}
public class MyExceptionDemo{ //测试类
public static void main(String[] args){
int age; //成员变量
Scanner sc=new Scanner(System.in);
System.out.println("请输入您的年龄:");
try{
age=sc.nextInt(); //获取年龄
if(age<0){
throw new MyException("您输入的年龄为负数!输入有误!");
}else if(age>100){
throw new MyException("您输入的年龄大于100!输入有误!");
}else {
System.out.println("您的年龄为:"+age);
}
} catch (InputMismatchException e1) { //捕获输入不匹配异常
System.out.println("输入的年龄不是数字!");
} catch (MyException e2){ //捕获自定义的异常
System.out.println(e2.getMessage()); //调用getMessage()方法输出异常信息
}
}
}

运行结果(3种情况)

1
2
3
4
5
6
7
8
9
10
11
12
-----------
请输入会员注册时的年龄:
acr
输入的年龄不是数字!
-----------
请输入您的年龄:
-1
您输入的年龄为负数!输入有误!
-----------
请输入您的年龄:
101
您输入的年龄大于100!输入有误!

注:因为自定义异常继承自 Exception 类,因此自定义异常类中包含父类所有的属性和方法。(可以调用)

3、断言(assert)

3.1 几点注意

  • java断言assert是jdk1.4引入的。
  • jvm断言默认是关闭的。(要手动开启)
    • 开启:在vm虚拟机中输入参数-ea
    • 关闭:输入-da,或删除-ea
  • 断言可以局部开启的,如:父类禁止断言,而子类开启断言,所以一般说“断言不具有继承性”。


3.2 作用及使用注意

断言主要使用在代码开发和测试时期,用于对某些关键数据的判断,如果这个关键数据不是程序所预期的数据,程序就提出警告或退出。

断言一般用于程序执行结构的判断,千万不要让断言处理业务流程。

3.3 使用方法

1)assert<boolean表达式>

表达式为true,程序继续执行,为false,抛出AssertionError,并终止执行。

2)assert<boolean表达式><msg> (msg为错误信息)

为true同上,为false,抛出AssertionError,输出错误信息并终止程序。

3.4 案例

1)示例1

1
2
3
4
5
6
7
8
9
public class AssertTest {
public static void main(String[] args) {
assert true; //断言1为true,则继续往下执行
System.out.println("断言1没有问题,Go!");
System.out.println("-----------------");
assert false : "断言失败,此表达式的信息将会在抛出异常的时候输出!"; //断言2为false,抛出AssertionError,输出错误信息并终止程序。
System.out.println("断言2没有问题,Go!"); //不会执行
}
}

运行结果

1
2
3
4
断言1没有问题,Go!
-----------------
Exception in thread "main" java.lang.AssertionError: 断言失败,此表达式的信息将会在抛出异常的时候输出!
at test2.AssertTest.main(AssertTest.java:8)

2)示例2

1
2
3
4
5
6
7
8
9
public class AssertTest {
public static void main(String[] args) {
assert true; //断言1为true,则继续往下执行
System.out.println("猜猜我叫什么名字?");
String name="zhangsan"; //初始化name
assert name=="lishi":"断言错误,我不叫lishi";//该断言为false,抛出AssertionError,输出错误信息并终止程序。
System.out.println("断言正确,我叫zhangsan"); //不会执行
}
}

运行结果

1
2
3
猜猜我叫什么名字?
Exception in thread "main" java.lang.AssertionError: 断言错误,我不叫lishi
at test2.AssertTest.main(AssertTest.java:8)

3.5 assert陷阱总结(尽量少用)

1)优点
可以帮助我们在开发和测试中提示哪部分的代码有问题,使用断言时需按需求设置好一个表达式,才能在我们放松警惕时提示“你这代码有问题”。
2)陷阱(了解)

assert关键字需要在运行时候显式开启才能生效,否则断言就没有任何意义。而现在主流的 Java IDE工具默认都没有开启-ea断言检查功能。意味着如果使用 IDE工具编码,调试运行时候会有一定的麻烦。并且,对于 Java Web应用,程序代码都是部署在容器里面,没法直接去控制程序的运行,如果一定要开启 -ea的开关,则需要更改Web容器的运行配置参数。这对程序的移植和部署都带来很大的不便。

用assert代替 if是陷阱之二。assert的判断和 if语句差不多,但两者的作用有着本质的区别:assert关键字本意上是为测试调试程序时使用的,但如果不小心用 assert来控制了程序的业务流程,那在测试调试结束后去掉 assert关键字就意味着修改了程序的正常的逻辑。

assert断言失败将面临程序的退出。这在一个生产环境下的应用是绝不能容忍的。一般都是通过异常处理来解决程序中潜在的错误。但是使用断言就很危险,一旦失败系统就挂了。

3.5 assert反思

  • 使用更好的测试 JUint(单元测试),用 assert只是为了调试测试程序用,不在正式生产环境下用。
  • 尽量避免在 Java中使用 assert关键字,除非哪天 Java默认支持开启 -ea的开关。

4、logging记录日志类

4.1 概述

日志用来记录程序的运行轨迹,方便查找关键信息和快速定位解决问题。

logging是JDK自带的记录日志类,目的是为了取代System.out.println()

4.2 优点

  • 可以设置输出样式,避免自己每次都写”ERROR: “ + var;
  • 可以设置输出级别,禁止某些级别输出。例如,只输出错误日志;
  • 可以被重定向到文件,这样可以在程序运行结束后查看日志;
  • 可以按包名控制日志级别,只输出某些包打的日志;等等。

4.3 方法

  • log():指定级别,logger.log(Level.FINE, message);
  • setLevel():设置级别,logger.setLevel(Level.FINE);
  • Level.ALL():开启所有级别的记录。
  • Level.OFF():关闭所有级别的记录。

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import java.util.logging.Logger; //导包
public class LoggingTest {
private static Logger log=Logger.getLogger(LoggingTest.class.toString());
public static void main(String[] args) {
//级别依次升高,后面的日志级别会屏蔽之前的级别,默认显示最高的3个
//使用日志级别的好处在于,调整级别,就可以屏蔽掉很多调试相关的日志输出。
log.finest("finest"); //最好
log.finer("finer"); //较好
log.fine("fine"); //良好
log.config("config"); //配置
log.info("info"); //信息
log.warning("warning"); //警告
log.severe("server"); //严重
}
}

运行结果

1
2
3
4
5
6
116, 2023 10:59:31 上午 test2.LoggingTest main
信息: info
116, 2023 10:59:31 上午 test2.LoggingTest main
警告: warning
116, 2023 10:59:31 上午 test2.LoggingTest main
严重: server