掌握Java中的异常处理机制,并学会定义异常
异常定义
在Java语言中,将程序执行中发生的不正常情况都可以称为异常。异常发生在程序运行期间,它影响了正常的程序执行流程。比如:用户输入不符合要求,程序打开文件时文件不存在等。
Java异常机制的作用:可以使程序异常处理代码和正常业务代码分离,提高程序健壮性。
示例一个简单的算数异常:
public class Demo01 {
public static void main(String[] args) {
int a = 10;
int b = 0;
System.out.println(a/b);
}
}
由于除数不能为0,程序出现异常,控制台输出异常信息:
异常在Java中以类和对象存在,根据异常信息,可以发现有一个类名java.lang.ArithmeticException,点开看源码可知它是一个类,有有参构造方法和无参构造方法,继承RuntimeException,代表所有的算数异常类。根据异常信息“at com.lic.exception.Demo01.main(Demo01.java:8)”可知在源码第8行发生了这个异常事件。实际上代码在执行到第8行,在底层调用了ArithmeticException类里面的构造方法创建了对象,将这个异常对象抛出。
这个信息会被我们看到,我们就会对其有相应的异常措施,会根据异常信息的描述对程序进行改进,提高程序的健壮性,改进如下:
public class Demo01 {
public static void main(String[] args) {
int a = 10;
int b = 0;
if(b==0){
System.out.println("输入错误,除数不能为0");
}else{
System.out.println(a/b);
}
}
}
改进后的代码增添了if判断语句,提高了程序的健壮性。
继承结构及分类
Java中异常以类和对象的形式存在,自然有自己的继承结构。程序在运行过程中出现的问题无论是异常还是错误都是可抛出的,Error和Exception的父类都是Throwable。
Error表示错误,在Java中只要发生错误,是无法处理的,最终只有一个结果就是JVM停止执行,程序退出。例如:Java虚拟机运行错误,当JVM不再有继续执行操作所需的内存资源时,将出现OutOfMemoryError。这些异常发生时,JVM一般会选择线程终止。
Exception表示异常,异常是可处理的,有两个处理结果,要么程序终止运行,要么处理完异常向下继续执行。
Exception继续向下分两大类:
- 受控异常(检查性异常):Exception类的直接子类就是受控异常。比如用户错误或问题引起的异常,是程序员无法预见的,例如打开一个不存在的文件时,一个异常就发生了,所有的受控异常要求在程序编写阶段预处理,如果不处理,编译器就会报错。
- 非受控异常(运行时异常):RuntimeException类的直接子类就是非受控异常。非受控异常是可能被程序员避免的异常。与检查性异常相反,运行时异常可以在编译时被忽略。
自定义异常
异常类要么继承Exception类,要么继承RuntimeException类,然后提供两个构造方法,一个无参,一个带有String参数的。
接下来自定义一个异常类:非法名称异常(IllegalNameException)。比如用户在注册的时候,要求用户名长度必须在[6-14]位之间,其他长度当作异常处理。自定义异常如下:
public class IllegalNameException extends RuntimeException{
public IllegalNameException() {
}
public IllegalNameException(String str) {
super(str);
}
}
手动抛出异常
异常的发生首先需要new异常对象,然后使用throw关键字抛出异常对象,异常就发生了,异常发生后,之后的代码就终止了。
假设用户在注册的时候,要求用户名长度必须在[6-14]位之间,就让异常发生。
先编写自定义异常类,再编写一个处理用户相关业务的UserService类,提供注册方法。
/**
* 处理用户相关业务
*/
public class UserService {
/**
* 用户注册
* @param username
*/
public void register(String username){
if(username==null||username.length()<6||username.length()>14){
//满足以上条件代表异常发生
//抛出异常对象,让异常发生
throw new IllegalNameException("非法用户名");
/*
这一句代码不使用异常处理机制,这样写:
System.out.println("非法用户名");
return;
return一旦执行,程序不继续往下执行了;
异常处理机制的代码既中断了程序的继续执行,又将异常信息传递给了调用者
*/
//异常发生处之后的代码不会执行
}
//异常未发生
System.out.println(username+"注册成功");
}
}
编写测试程序调用UserService类的register方法完成测试。
public class UserClient {
public static void main(String[] args) {
//输入用户名
String username = "Jack";
//用户注册
new UserService().register(username);
}
运行结果如下:
程序运行到UserService.java的第14行出现异常,然后UserService的register方法执行结束,并将这个异常对象抛出给调用者“UserClient.java的第8行”,所以异常信息中描述在UserClient.java的第8行发生了IllegalNameException这个异常。
假设用户名合法,如下:
public class UserClient {
public static void main(String[] args) {
//输入用户名
String username = "Jackson";
//用户注册
new UserService().register(username);
}
结果如下:
异常处理
受控异常要求在程序编写阶段预处理,如果不处理,编译器就会报错,处理方法有两种:
- 在方法声明位置使用throws关键字进行声明以便抛出
- 使用try catch进行异常的捕捉
对之前自定义的异常类进行修改,继承Exception修改之后其他程序出现问题
public class IllegalNameException extends Exception{
public IllegalNameException() {
}
public IllegalNameException(String str) {
super(str);
}
}
出现错误:
错误是未处理异常,使用try catch解决:
public class UserService {
public void register(String username){
if(username==null||username.length()<6||username.length()>14){
try{
throw new IllegalNameException("非法用户名");
}catch (IllegalNameException e){
e.printStackTrace();
}
}
//异常未发生
System.out.println(username+"注册成功");
}
}
通过上面代码可以看出,自己采用throw new IllegalNameException(“非法用户名”)让异常发生,又用try catch捕获,没有意义,自己手动抛出异常是希望调用者处理异常,所以应采用throws,声明以便抛出的方式。
代码如下:
public class UserService {
public void register(String username) throws IllegalNameException{
if(username==null||username.length()<6||username.length()>14){
throw new IllegalNameException("非法用户名");
}
System.out.println(username+"注册成功");
}
}
但是方法使用throws声明后其他程序出现错误
出现语法错误,因为register方法声明位置上有throws IllegalNameException的代码,所以编译器知道register方法会可能发生IllegalNameException,调用者就知晓并能够处理。怎样解决未处理异常呢?使用try catch捕获还是采用throws,声明以便抛出呢?
假设使用throws,代码如下:
public class UserClient {
public static void main(String[] args) throws Exception{
//输入用户名
String username = "Jackson";
//用户注册
new UserService().register(username);
}
}
语法合法,但是这样做不好,如果出现异常,采用throws处理异常,将此异常抛给调用者处理,JVM调用main方法,JVM会终止执行,所有程序全部结束。这显然不是我想要的,我希望处理完异常,程序继续执行,提高程序的健壮性。所以使用try catch捕获。
public class UserClient {
public static void main(String[] args) {
//输入用户名
String username = "Jack";
//用户注册
try {
new UserService().register(username);
} catch (IllegalNameException e) {
System.out.println("用户名不合法");
}
}
}
运行结果如下:
try catch
try表示尝试执行大括号里面的代码,如果某一行代码出现异常,发生异常之后的代码不执行进入catch语块中执行,catch表示捕获异常,可以编写多个,进入哪个catch取决于catch小括号的异常类型,并且try语句块发生异常后创建的异常对象的内存地址会赋给catch后面的e 变量。也就是e引用了刚发生的异常对象。
catch分支如果有多个,只能有一个分支执行,那么catch语句写多个的时候,有自上而下的顺序匹配的。
案例:定义两个异常类AException,BException,再编写一个测试类,里面有一个接收整数的方法。
写两个自定义异常类:
public class AException extends Exception {
public AException() {
}
public AException(String message) {
super(message);
}
}
public class BException extends Exception {
public BException() {
}
public BException(String message) {
super(message);
}
}
写有test方法的类MathService:
public class MathService {
public void test(int i)throws AException,BException{
if(i==1){
throw new AException("a类异常");
}else if(i==2){
throw new BException("b类异常");
}
System.out.println("没有异常");
}
}
测试类MathClient:
public class MathClient {
public static void main(String[] args) {
int i = 2;
try {
new MathService().test(i);
System.out.println("如果前面代码发生异常,此代码不会执行");
} catch (AException e) {
System.out.println("A类异常");
} catch (BException e) {
System.out.println("B类异常");
}
System.out.println("如果前面代码发生异常,此代码会执行!!!try catch后");
}
运行结果:
finally语句块
finally语句块必须和try语句联合使用,不能独立使用,放在finally里面的语句必须执行的。
例如:
public class Demo01 {
public static void main(String[] args) {
int a = 10;
int b = 0;
try {
System.out.println(a/b);
} catch (Exception e) {
System.out.println("除数不能为0");
}
finally {
System.out.println("finally语句块");
}
}
}
结果如下:
假设在try语句块写了return语句,那么finally语句块中的程序还会执行。
public class Demo01 {
public static void main(String[] args) {
try {
System.out.println("try");
return;
}
finally {
System.out.println("finally语句块");
}
}
}
结果如下:
try语句中虽然有return语句,但是finally语句块还是会执行,return语句一旦执行,main方法就结束了,所以finally语句块的执行实在return之前。
但是,下面这个例子结果不是finally语句块先执行。
public class Demo02 {
public static void main(String[] args) {
System.out.println(test());
}
public static int test(){
int i = 10;
try {
return i;
} finally {
i +=100;
}
}
}
结果是10而不是110
由于此程序比较特殊,它实际是将i变量中存储的值又重新存入到一个新的临时变量,return返回的值是新的临时变量的值,代码等同于:
public class Demo02 {
public static void main(String[] args) {
System.out.println(test());
}
public static int test(){
int i = 10;
try {
int j = 10;
return j;
} finally {
i +=100;
}
}
}
所以结果是10不是110。
接下来再测试把return语句换成System.exit(0);
public class Demo01 {
public static void main(String[] args) {
try {
System.out.println("try");
System.exit(0);
}
finally {
System.out.println("finally语句块");
}
}
}
结果如下:
发现finally语句块没有执行,如果是采用System.exit(0)的方式退出JVM,那么finally语句块不会执行。
方法覆盖与异常
方法覆盖之后不能抛出比父类方法更宽泛的异常,可以一样或变小。
测试:
编写UserService:
public class UserService {
public void register(String username) throws IllegalNameException{
if(username==null||username.length()<6||username.length()>14){
throw new IllegalNameException("非法用户名");
}
System.out.println(username+"注册成功");
}
}
编写SubUserService继承UserService并重写register方法:
public class SubUserService extends UserService{
@Override
public void register(String username) throws IllegalNameException {
super.register(username);
}
}
编译通过,说明语法正确,重写之后的方法抛出的异常与父类一样,如果重写后不抛出异常,编译也通过,说明重写后可以更少。
public class SubUserService extends UserService{
@Override
public void register(String username) {
}
}
如果重写后更多呢,抛出Exception异常
就错误,由于父类没有抛出Exception异常,子类重写也不能抛出。
记住一条规则:当子类覆盖父类方法时,子类重写的方法不能比父类方法抛出更宽泛的异常,可以一样或者更少。