8.1 异常的概述
在程序中,错误可能产生于程序员没有预料到的各种情况,或者是超出了程序员可控范围的环境因素,如试图打开一个根本不存在的文件等,在Java中,这种在程序运行时可能出现的一些错误称为异常。
在 Java 中一个异常的产生,主要有如下三种原因:
- Java 内部错误发生异常,Java 虚拟机产生的异常。
- 编写的程序代码中的错误所产生的异常,例如空指针异常、数组越界异常等。
- 通过 throw 语句手动生成的异常,一般用来告知该方法的调用者一些必要信息。
创建Baulk类,再主方法中定义int型变量,将0作为除数赋值给该变量。
public class Baulk { //创建类Baulk
public static void main(String[] args) { //主方法
int result = 3 / 0; //定义int型变量并赋值
System.out.println(result); //将变量输出
}
}
8.2 异常的分类
Java类库的每个包中都定义了异常类,所有这些类都是Throwable类。
Throwable类派生了Error和Exception连个子类。
Error类及其子类用来描述Java运行系统中的内部错误以及资源耗尽的错误,这类错误比较严重。
Exception称为非致命类,可以通过捕捉处理使程序继续执行
Throwable 类是所有异常和错误的超类,下面有 Error 和 Exception 两个子类分别表示错误和异常
Exception 类用于用户程序可能出现的异常情况,它也是用来创建自定义异常类型类的类。
Error 定义了在通常环境下不希望被程序捕获的异常。一般指的是 JVM 错误,如堆栈溢出。
Error(错误)和 Exception(异常)都是 java.lang.Throwable 类的子类,在 Java 代码中只有继承了 Throwable 类的实例才能被 throw 或者 catch。
Exception 和 Error 体现了 Java 平台设计者对不同异常情况的分类,Exception 是程序正常运行过程中可以预料到的意外情况,并且应该被开发者捕获,进行相应的处理。Error 是指正常情况下不大可能出现的情况,绝大部分的 Error 都会导致程序处于非正常、不可恢复状态。所以不需要被开发者捕获。
1 系统错误——Error
Error类定义了常规环境下不希望由程序捕获的异常,比如:OutOfMemoryError、ThreadDeath等,这些错误发生时Java一般会选择终止。
public static void main(String[] args) { //主方法
System.out.println("梦想照亮现实!!!") //此处缺少必要的分号
}
2 异常---Exception
1)运行时异常(RuntimeException):
NullPropagation:空指针异常;
ClassCastException:类型强制转换异常
IllegalArgumentException:传递非法参数异常
IndexOutOfBoundsException:下标越界异常
NumberFormatException:数字格式异常
2)非运行时异常:
ClassNotFoundException:找不到指定 class 的异常
IOException:IO 操作异常
// 通过无限递归演示堆栈溢出错误
class StackOverflow {
public static void test(int i) {
if (i == 0) {
return;
} else {
test(i++);
}
}
}
public class ErrorEg {
public static void main(String[] args) {
// 执行StackOverflow方法
StackOverflow.test(5);
}
}
8.3 捕捉处理异常
在处理非运行时异常时,提到了系统会自动为非运行时异常提供两种解决方案,一种是使用throws关键字,一种是使用try...catch代码块。
try...catch代码块代码块主要用来对异常进行捕捉并处理。在实际使用时,改代码块还有一个可选的finally代码块。
try {undefined
//程序代码块
}
catch(Exceptiontype e) {undefined
//对Exceptiontype的处理
}
finally {undefined
//代码块
}
1 try...catch代码块
public class Baulk { //创建类
public static void main(String[] args) { //主方法
try {
String str = "lili"; //定义字符串
System.out.println(str + "年龄是:"); //输出的提示信息
int age = Integer.parseInt("20L"); //数据类型的转换
System.out.println(age); //输出信息
} catch (Exception e) { //catch代码块用来获取异常信息
e.printStackTrace(); //输出异常性质
}
System.out.println("program over"); //输出信息
}
}
注意:Exception是try代码块传递给catch代码块的类型,e是对象名。
上面代码中,在catch代码块中使用了Exception对象的printStackTrace()方法输出了异常的栈日志,除此之外,Excrption对象还提供了其他的方法用于获取异常的相关信息,其最常用的3个方法如下:
(1)getMessage()方法:获取有关异常事件的信息。
(2)toString()方法:获取异常的类型与性质。
(3)printStackTrace()方法:获取异常事件发生时执行堆栈的内容。
在遇到需要处理多种异常信息的情况时,可以在一个try代码块后面跟多个catch代码块。
public class FootballTeam {
public static void main(String[] args) {
// TODO Auto-generated method stub
try {
String str = "lili"; //定义字符串
System.out.println(str + "年龄是:"); //输出的提示信息
int age = Integer.parseInt("20L"); //数据类型的转换
System.out.println(age); //输出信息
} catch (NumberFormatException nfx) { //捕捉NumberFormatException异常
nfx.printStackTrace();
} catch (Exception e) { //Exception异常
e.printStackTrace(); //输出异常性质
}
System.out.println("program over"); //输出信息
}
}
这时将两个catch代码块的位置互换,代码块如下:
public class FootballTeam {
public static void main(String[] args) {
// TODO Auto-generated method stub
try {
String str = "lili"; //定义字符串
System.out.println(str + "年龄是:"); //输出的提示信息
int age = Integer.parseInt("20L"); //数据类型的转换
System.out.println(age); //输出信息
} catch (Exception e) { //Exception异常
e.printStackTrace(); //输出异常性质
} catch (NumberFormatException nfx) { //捕捉NumberFormatException异常
nfx.printStackTrace();
}
System.out.println("program over"); //输出信息
}
}
这时编译器
会出现以下错误提示,该错误就是由于多个catch代码块时,父异常类放在了子异常类的前面所引起的。Exception是所有异常类的父类。
2 finally代码块
根据 try catch 语句的执行过程,try 语句块和 catch 语句块有可能不被完全执行,而有些处理代码则要求必须执行。例如,程序在 try 块里打开了一些物理资源(如数据库连接、网络连接和磁盘文件等),这些物理资源都必须显式回收。
try {
// 可能会发生异常的语句
} catch(ExceptionType e) {
// 处理异常语句
} finally {
// 清理代码块
}
try {
// 逻辑代码块
} finally {
// 清理代码块
}
try catch finally 语句块的执行情况可以细分为以下 3 种情况:
如果 try 代码块中没有拋出异常,则执行完 try 代码块之后直接执行 finally 代码块,然后执行 try catch finally 语句块之后的语句。
如果 try 代码块中拋出异常,并被 catch 子句捕捉,那么在拋出异常的地方终止 try 代码块的执行,转而执行相匹配的 catch 代码块,之后执行 finally 代码块。如果 finally 代码块中没有拋出异常,则继续执行 try catch finally 语句块之后的语句;如果 finally 代码块中拋出异常,则把该异常传递给该方法的调用者。
如果 try 代码块中拋出的异常没有被任何 catch 子句捕捉到,那么将直接执行 finally 代码块中的语句,并把该异常传递给该方法的调用者
import java.util.Scanner;
public class Test04 {
public static void main(String[] args) {
Scanner input = new Scanner(System.in);
System.out.println("Windows 系统已启动!");
String[] pros = { "记事本", "计算器", "浏览器" };
try {
// 循环输出pros数组中的元素
for (int i = 0; i < pros.length; i++) {
System.out.println(i + 1 + ":" + pros[i]);
}
System.out.println("是否运行程序:");
String answer = input.next();
if (answer.equals("y")) {
System.out.println("请输入程序编号:");
int no = input.nextInt();
System.out.println("正在运行程序[" + pros[no - 1] + "]");
}
} catch (Exception e) {
e.printStackTrace();
} finally {
System.out.println("谢谢使用!");
}
}
}
8.4在方法中抛出异常
throws 关键字和 throw 关键字在使用上的几点区别如下:
throws 用来声明一个方法可能抛出的所有异常信息,表示出现异常的一种可能性,但并不一定会发生这些异常;throw 则是指拋出的一个具体的异常类型,执行 throw 则一定抛出了某种异常对象。
通常在一个方法(类)的声明处通过 throws 声明方法(类)可能拋出的异常信息,而在方法(类)内部通过 throw 声明一个具体的异常信息。
throws 通常不用显示地捕获异常,可由系统自动将所有捕获的异常信息抛给上级方法; throw 则需要用户自己捕获相关的异常,而后再对其进行相关包装,最后将包装后的异常信息抛出。
1 使用throws关键字抛出异常
当一个方法产生一个它不处理的异常时,那么就需要在该方法的头部声明这个异常,以便将该异常传递到方法的外部进行处理。使用 throws 声明的方法表示此方法不处理异常。throws 具体格式如下:
returnType method_name(paramList) throws Exception 1,Exception2,…{…}
import java.io.FileInputStream;
import java.io.IOException;
public class Test04 {
public void readFile() throws IOException {
// 定义方法时声明异常
FileInputStream file = new FileInputStream("read.txt"); // 创建 FileInputStream 实例对象
int f;
while ((f = file.read()) != -1) {
System.out.println((char) f);
f = file.read();
}
file.close();
}
public static void main(String[] args) {
Throws t = new Test04();
try {
t.readFile(); // 调用 readFHe()方法
} catch (IOException e) {
// 捕获异常
System.out.println(e);
}
}
}
public class OverrideThrows {
public void test() throws IOException {
FileInputStream fis = new FileInputStream("a.txt");
}
}
class Sub extends OverrideThrows {
// 子类方法声明抛出了比父类方法更大的异常
// 所以下面方法出错
public void test() throws Exception {
}
}
使用 throws 声明抛出异常的思路是,当前方法不知道如何处理这种类型的异常,该异常应该由向上一级的调用者处理;如果 main 方法也不知道如何处理这种类型的异常,也可以使用 throws 声明抛出异常,该异常将交给 JVM 处理。JVM 对异常的处理方法是,打印异常的跟踪栈信息,并中止程序运行,这就是前面程序在遇到异常后自动结束的原因。
2 使用throw关键字抛出异常
throw关键字通常用于在方法体中“制作”一个异常,程序在执行到throw语句时立即终止,它后面的语句都不执行。
throw new 异常类型名(异常信息)
通常用于在程序出现逻辑错误1时,由开发者主动抛出某个特定类型的异常。
public class ThrowTest {
public static void main(String[] args) { //主方法
// TODO Auto-generated method stub
int num1 = 25;
int num2 = 0;
int result;
if (num2 == 0) //判断num2是否等于0,如果等于0,抛出异常
{
//抛出ArithmeticException异常
throw new ArithmeticException("这都不会,小学生都知道:除数不能是0!!!");
}
result = num1 / num2; //计算int1除以int2的值
System.out.println("两个数的商为: " + result);
}
}
throws和throw的区别:
(1)throws用在方法声明后面,表示抛出异常,由方法的调用者处理。而throw用在方法体内,用来制造一个异常,由方法体内的语句处理。
(2)throws是声明这个方法会抛出这种类型的异常,以便使它的调用者知道要捕获这个异常,而throw是直接抛出一个异常实例
(3)Theo我是表示出现异常的一种可能性,并不一定会发生这些异常,如果使用throw,就一定会产生某种异常。
8.5 自定义异常
int age = -50;
System.out.println("王师傅今年 "+age+" 岁了!");
在程序中使用自定义异常类,大体可分为以下几个步骤。
(1)创建自定义异常类。
(2)在方法中通过throw关键字抛出异常对象。
(3)如果在当前抛出异常的方法中处理异常,可以使用try...catch代码块捕获并处理,否则,在方法的声明处通过throws关键字指明要抛给方法调用者的异常,继续进行下一步操作。
(4)在出现异常方法的调用者中捕获并处理异常。
public class MyException extends Exception { //创建自定义异常,继承Exception类
public MyException(String ErrorMessage) { //构造方法
super(ErrorMessage); //父类构造方法
}
}
public class Tran {
//定义方法,抛出自定义的异常
static void avg(int age) throws MyException {
if (age < 0) { //判断方法中参数是否满足指定条件
throw new MyException("年龄不可以使用负数"); //错误信息
} else {
System.out.println("王师傅今年 " + age + "岁了!"); //输出信息
}
}
public static void main(String[] args) { //主方法
// TODO Auto-generated method stub
try { //try代码块处理可能出现异常的代码
avg(-50);
} catch (MyException e) {
e.printStackTrace();
}
}
}
8.6 异常的使用原则
编写代码处理某个方法可能出现的异常时,可遵循以下原则:
(1)不要过度使用异常。虽然通过异常可以增强程序的健壮性,但如果使用过多不必要的异常处理,可能会影响程序的执行效率。
(2)不要使用过于庞大的try...catch块。在一个try块中放置大量的代码,这种写法看上去“很简单”,但是由于try块中的代码过于庞大,业务过于复杂,会造成try块中出现异常的可能性大大增加,从而导致分析异常原因的难度也大大增加。
(3)避免使用catch(Exception e)。因为如果所有异常都采用相同的处理方式,将无法对不同异常分情况处理;另外,这种捕获方式可能将程序中的全部错误、异常捕获到,这时如果出现一些“关键”异常,可能会被“悄悄地”忽略掉。
(4)不要忽略捕捉到的异常,遇到异常一定要及时处理。
(5)如果父类抛出多个异常,则覆盖方法必须抛出相同的异常或其异常的子类,不能抛出新异常。
class SelfException extends RuntimeException {
SelfException() {
}
SelfException(String msg) {
super(msg);
}
}
public class PrintStackTraceTest {
public static void main(String[] args) {
firstMethod();
}
public static void firstMethod() {
secondMethod();
}
public static void secondMethod() {
thirdMethod();
}
public static void thirdMethod() {
throw new SelfException("自定义异常信息");
}
}
多线程异常的跟踪栈,从发生异常的方法开始,到线程的 run 方法结束。从上面的运行结果可以看出,程序在 Thread 的 run 方法中出现了 ArithmeticException 异常,这个异常的源头是 ThreadExcetpionTest 的 secondMethod 方法,位于 ThreadExcetpionTest.java 文件的 14 行。这个异常传播到 Thread 类的 run 方法就会结束(如果该异常没有得到处理,将会导致该线程中止运行)。
前面已经讲过,调用 Exception 的 printStackTrace() 方法就是打印该异常的跟踪栈信息,也就会看到上面两个示例运行结果中的信息。当然,如果方法调用的层次很深,将会看到更加复杂的异常跟踪栈。
提示:虽然 printStackTrace() 方法可以很方便地用于追踪异常的发生情况,可以用它来调试程序,但在最后发布的程序中,应该避免使用它。应该对捕获的异常进行适当的处理,而不是简单地将异常的跟踪栈信息打印出来。