什么是异常
程序运行时,发生的不被期望的事件,它阻止了程序按照程序员的预期正常执行,这就是异常。异常发生时,是任程序自生自灭,立刻退出终止,还是输出错误给用户?Java 提供了更加优秀的解决办法:异常处理机制。
异常处理机制能让程序在异常发生时,按照代码的预先设定的异常处理逻辑,针对性地处理异常,让程序尽最大可能恢复正常并继续执行,且保持代码的清晰。
Java 中的异常可以是函数中的语句执行时引发的,也可以是程序员通过 throw 语句手动抛出的,只要在Java程序中产生了异常,就会用一个对应类型的异常对象来封装异常,JRE 就会试图寻找异常处理程序来处理异常。
Throwable 类是 Java 异常类型的顶层父类,一个对象只有是 Throwable 类的(直接或者间接)实例,它才是一个异常对象,才能被异常处理机制识别。JDK 中内建了一些常用的异常类,我们也可以自定义异常。
异常分类
1、所有的异常都是从 Throwable
继承而来的,是所有异常的共同祖先。
2、Throwable
有两个子类,Error
和Exception
。
其中 Error
是错误,对于所有的编译时期的错误以及系统错误都是通过 Error 抛出的。这些错误表示故障发生于虚拟机自身、或者发生在虚拟机试图执行应用时,如 Java 虚拟机运行错误(Virtual MachineError)、类定义错误(NoClassDefFoundError)等。一般发生这种异常,JVM 会选择终止程序。因此我们编写程序时不需要关心这类异常。
Exception
它规定的异常是程序本身可以处理的异常。异常和错误的区别是,异常是可以被处理的,而错误是没法处理的。
3、Exception
类的异常包括checked exception
和unchecked exception
Checked Exception
即可检查的异常,都是需要在代码中处理的。它们的发生是可以预测的,比如 IOException,或者一些自定义的异常。除了 RuntimeException 及其子类以外,都是 checked exception。必须对该异常进行处理,要么使用 try-catch 捕获,要么使用 throws 语句抛出,否则编译不通过。
Unchecked Exception
包括 RuntimeException 及其子类都。比如空指针异常,除数为0的算数异常 (ArithmeticException) 等等,这种异常是运行时发生,无法预先捕捉处理的。Error 也是 unchecked exception,也是无法预先处理的。对于这些异常,我们应该修正代码,而不是去通过异常处理器处理 。这样的异常发生的原因多半是代码写的有问题。
初识异常
封装错误信息的对象包括:类名、错误信息、行号
下面的代码会演示几个异常类型
ArrayIndexOutOfBoundsException 下标越界
public class Main {
public static void main(String[] args) {
f();
}
private static void f() {
System.out.println("输入逗号隔开的两个整数");
String s = new Scanner(System.in).nextLine();
String[] a = s.split(",");
int n1 = Integer.parseInt(a[0]);
int n2 = Integer.parseInt(a[1]);
System.out.println(n1 / n2);
}
}
运行程序,程序要求我们输入用逗号隔开的两个整数,我们只输入一个后会报错
类型:java.lang.ArrayIndexOutOfBoundsException(下标越界)
错误信息:index 1 out of bounds(a[1]没有)
行号:Main.java 16行(int n2 = Integer.parseInt(a[1]);)
NumberFormatException 数字格式错误
类型:java.lang.NumberFormatException(数字格式错误)
错误信息:abc(abc不是数字)
行号:Main.java:15(int n1 = Integer.parseInt(a[0]);)
ArithmeticException 异常的运算条件
类型:java.lang.ArithmeticException(异常的运算条件)
错误信息:/ by zero(除数为0)
行号:Main.java:17(System.out.println(n1 / n2);)
上面的代码不使用异常处理机制,也可以顺利编译,因为3个异常都是非检查异常。
在下面捕获异常一节中的例子就必须使用异常处理机制,因为异常是检查异常。
捕获异常
事实上,程序崩溃是件体验非常不好的事情,因此,我们可以让异常不穿过 main() 方法。我们可以接住这个异常,并且处理它。这个接住异常的过程称为捕获异常。我们先看栗子。
我们修改上面的代码
public class Main {
public static void main(String[] args) {
while (true) {
try {
//f()方法出现异常,直接捕获异常
f();
//如果f()方法正常执行完,执行break跳出循环
break;
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("两个整数,不是1个");
} catch (NumberFormatException e) {
System.out.println("输入整数");
} catch (Exception e) {
System.out.println("出错请重试!");
} finally {
System.out.println("~~~~~~~~~~");
}
}
System.out.println("出异常就不会执行");
}
private static void f() {
System.out.println("输入逗号隔开的两个整数");
String s = new Scanner(System.in).nextLine();
String[] a = s.split(",");
int n1 = Integer.parseInt(a[0]);
int n2 = Integer.parseInt(a[1]);
System.out.println(n1 / n2);
}
}
运行结果:
异常处理的基本语法
在编写代码处理异常时,对于检查异常,有 2 种不同的处理方式:
① 使用try...catch...finally
语句块处理它。
② 在函数签名中使用throws
声明交给函数调用者caller 去解决。
try…catch…finally
try {
//try块中放可能发生异常的代码。
//如果执行完try且不发生异常,则接着去执行finally块和finally后面的代码(如果有的话)。
//如果发生异常,则尝试去匹配catch块。
} catch (AException e) {
//每一个catch块用于捕获并处理一个特定的异常,或者这异常类型的子类。Java7中可以将多个异常声明在一个catch中。
//catch后面的括号定义了异常类型和异常参数。如果异常与之匹配且是最先匹配到的,则虚拟机将使用这个catch块来处理异常。
//在catch块中可以使用这个块的异常参数来获取异常的相关信息。异常参数是这个catch块中的局部变量,其它块不能访问。
//如果当前try块中发生的异常在后续的所有catch中都没捕获到,则先去执行finally,然后到这个函数的外部caller中去匹配异常处理器。
//如果try中没有发生异常,则所有的catch块将被忽略。
} catch (BException e) {
//捕获B类型异常
} catch (父类型Exception e) {
//其他异常可以直接捕获父类型异常
//因为多个异常,只会捕获一个,后边的捕获不会执行,所以父类型要放到最后
//父类型放到开始,执行完后边的异常捕获就不执行了
} finally {
//finally块通常是可选的。
//无论出不出错,都会执行
//捕获了一个异常,会执行
//正常执行了程序,会执行
//一个try至少要有一个catch块,否则, 至少要有1个finally块。但是finally不是用来处理异常的,finally不会捕获异常。
//finally主要做一些清理工作,如流的关闭,数据库连接的关闭等。
}
需要注意的地方
1、try 块中的局部变量和 catch 块中的局部变量(包括异常变量),以及 finally 中的局部变量,它们之间不可共享使用。
2、每一个 catch 块用于处理一个异常。异常匹配是按照 catch 块的顺序从上往下寻找的,只有第一个匹配的 catch 会得到执行。匹配时,不仅运行精确匹配,也支持父类匹配,因此,如果同一个 try 块下的多个 catch 异常类型有父子关系,应该将子类异常放在前面,父类异常放在后面,这样保证每个 catch 块都有存在的意义。
3、java 中,异常处理的任务就是将执行控制流从异常发生的地方转移到能够处理这种异常的地方去。也就是说:当一个函数的某条语句发生异常时,这条语句的后面的语句不会再执行,它失去了焦点。执行流跳转到最近的匹配的异常处理 catch 代码块去执行,异常被处理完后,执行流会接着在"处理了这个异常的 catch 代码块"后面接着执行。
4、良好的编程习惯是:在 try 块中打开资源,在 finally 块中清理释放这些资源。
有的编程语言当异常被处理后,控制流会恢复到异常抛出点接着执行,这种策略叫做:resumption model of exception handling(恢复式异常处理模式 )
而 Java 则是让执行流恢复到处理了异常的 catch 块后接着执行,这种策略叫做:termination model of exception handling(终结式异常处理模式)
throws
throws
声明:如果一个方法内部的代码会抛出检查异常(checked exception),而方法自己又没有完全处理掉,则 javac 保证你必须在方法的签名上使用 throws
关键字声明这些可能抛出的异常,否则编译不通过。
throws
是另一种处理异常的方式,它不同于try...catch...finally,throws
,仅仅是将函数中可能出现的异常向调用者声明,而自己则不具体处理。
采取这种异常处理的原因可能是:方法本身不知道如何处理这样的异常,或者说让调用者处理更好,调用者需要为可能发生的异常负责。
写如下代码,输入生日日期,创建一个时间戳名称的 txt 文件,保存到 D 盘:
现在代码是报错状态,因为 parse 这个方法设置了异常抛出的管道,f() 方法应该做相应的处理,可以设置相应的异常抛出管道,或者捕获异常。
查看需要设置的异常抛出管道,点击 Add Exception to method signature,自动添加
throws 后面用逗号隔开可以设置多个管道,添加完之后,f() 方法处报错,因为刚才处理的两个管道不是 RuntimeException,所以需要添加管道或者捕获异常
我们这里增加 try…catch
public class Main {
public static void main(String[] args) {
try {
f();
} catch (ParseException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
private static void f() throws ParseException, IOException {
System.out.println("输入生日");
String s = new Scanner(System.in).nextLine();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
Date d = sdf.parse(s);
File f = new File("d:/"+d.getTime()+".txt");
f.createNewFile();
}
}
如果输入格式不正确,运行结果
如果在代码中将文件保存到不存在的盘符:“k:/”,运行结果
throw
程序员可以通过 throw
语句手动显式的抛出一个异常。throw
语句的后面必须是一个异常对象。
throw
语句必须写在函数中,执行 throw
语句的地方就是一个异常抛出点,它和由JRE 自动形成的异常抛出点没有任何差别。
看以下代码:
public class Main {
public static void main(String[] args) {
f();
}
private static void f() {
System.out.println("输入两个浮点数:");
Double a = new Scanner(System.in).nextDouble();
Double b = new Scanner(System.in).nextDouble();
System.out.println(divide(a, b));
}
private static double divide(double a, double b) {
return a / b;
}
}
正常情况下程序运行结果
如果除数是 0,运行结果:
程序不会报错,但会返回 Infinity,这时可以手动创建一个异常对象,我们只需修改 divide 方法:
private static double divide(double a, double b) {
//手动创建异常对象
if (b == 0) {
//可以加提示消息,给程序猿看
throw new ArithmeticException("/ by zero");
}
return a / b;
}
当除数再次输入 0:
手动添加了异常对象后,还可以在调用方法的地方捕获异常,来提示用户,完整代码如下
public class Main {
public static void main(String[] args) {
f();
}
private static void f() {
System.out.println("输入两个浮点数:");
Double a = new Scanner(System.in).nextDouble();
Double b = new Scanner(System.in).nextDouble();
//可以在这里捕获异常
try {
System.out.println(divide(a, b));
} catch (ArithmeticException e) {
//提示用户
System.out.println("不能除0是我们的错,请鞭挞我们");
//打印错误信息,给程序猿看
e.printStackTrace();
}
}
private static double divide(double a, double b) {
//手动创建异常对象
if (b == 0) {
//可以加提示消息,给程序猿看
throw new ArithmeticException("/ by zero");
}
return a / b;
}
}
运行结果
抛出异常的过程
我们来分析下上面的程序抛出异常的过程:
1、main() 方法中调用 f() 方法
f() 中产生的错误会封装成一个对象传递给 main() 方法
f() 方法产生错误后的代码不再执行,返回 main() 方法
这个动作叫做抛出
2、如果 main() 不做处理,会穿过 main() 方法,抛给虚拟机
3、虚拟机会打印异常信息,并且退出
4、虚拟机退出,程序直接崩溃
异常包装
捕获一个异常对象,把它包装在另一种类型异常对象中,再抛出
为什么包装
不能抛出的异常,包装成能抛出的异常,再抛出
多种类型异常包装成一种异常,再抛出,简化异常
栗子
public class Main {
public static void main(String[] args) {
f();
}
private static void f() {
List<String> list = new ArrayList<>();
Collections.addAll(list, "2015-8-15", "2012-9-15", "2021-9-28");
System.out.println(list);
Collections.sort(list, new Comparator<String>() {
@Override
public int compare(String arg0, String arg1) {
SimpleDateFormat f = new SimpleDateFormat("yyyy-MM-dd");
try {
Date d1 = f.parse(arg0);
Date d2 = f.parse(arg1);
return d1.compareTo(d2);
} catch (ParseException e) {
throw new RuntimeException(e);
}
}
});
System.out.println(list);
}
}
运行结果
代码中,f.parse(arg01);
、f.parse(arg02);
有 ParseException 这样一个管道,现在必须二选一,要么 try…catch,要么抛出异常。
然后 throws ParseException 是不行的,如果有两个类,父类中有一个方法,子类重写这个方法,父类的异常管道是个上限,子类添加的异常管道不能比父类多,而Comparator 的方法 compare 只有一个默认管道 RuntimeException,所以子类重写这个方法不能增加一个管道,比父类多,所以只能 try…catch
而 catch 中没有办法修复这个错误,返回正数说明,arg0大,返回负数说明,arg0小,返回 0 说明相等,这样都不合适,所以依然在 catch 中抛出异常