什么是异常
作为一门面向对象的语言,用Java编写代码的过程,可以理解为创建、使用对象的过程。
普通对象是对象,异常对象也是对象。如果把普通对象比作常人,那么异常对象就可以理解为病人。普通对象的作用是为了让你的程序运行,而异常对象的作用恰好相反,它的出现就是为了告诉你程序“生病”了,你必须去“治疗”它,否则就无法正常运行。
Java的异常体系
一般来说,异常分为下面三类:
- 编译时异常:最具代表的检查性异常是用户错误或问题引起的异常,这是程序员无法预见的。例如要打开一个不存在文件时,一个异常就发生了,这些异常在编译时不能被简单地忽略。
- 运行时异常: 运行时异常是可能被程序员避免的异常。与检查性异常相反,运行时异常可以在编译时被忽略。
- 错误: 错误不是异常,而是脱离程序员控制的问题。错误在代码中通常被忽略。例如,当栈溢出时,一个错误就发生了,它们在编译也检查不到的。
Exception类继承于Throwable类。Throwable类有两个子类,还有一个就是Error类。Error一般是碰到致命错误才会出现,比如服务器宕机。Error不是我们程序员解决的问题,需要我们去解决的是Exception。
Exception可以分为两类,RuntimeException(运行时异常)和其余异常(编译时异常)。在运行代码时才出现的异常,就是运行时异常,运行时异常不会在编译时报错。下面通过一个例子演示一下异常:
public class Test {
public static void main(String[] args) {
int num = div(10, 0); // java.lang.ArithmeticException: / by zero
System.out.println(num);
}
public static int div(int a, int b) {
return a / b; // 除数不能为0
// 除数为0,创建异常对象new ArithmeticException(" / by zero")
}
}
运行上面的代码就会出现异常(ArithmeticException),这个异常是怎么出现的呢?在调用div()方法的时候,我们传入了除数参数0。很明显除数是不能为零的,于是div()方法就不会返回a/b,而是会创建ArithmeticException对象往上抛。而要去接住ArithmeticException对象的是num,num是int类型,自然接不住。num继续将ArithmeticException对象向上抛给main()方法,main()方法也接不住,main()方法接着把将ArithmeticException对象向上抛给JVM。JVM无法继续向上抛出异常,将异常信息打印出来。
总结:
- 默认异常处理的方式是层层向上抛
- 异常对象也是对象,只是该对象创建出来,程序就会停止。
异常处理
异常处理一共有两种方式,一种是前面提到的抛出,另一种是try...catch。
1. throws与throw
throws关键字用于方法签名上,后面接异常类名,throw关键字用于方法内,后面跟异常对象。
使用throws / throw抛出异常的类型是RuntimeException及其子类(运行时异常),JVM不会强制你处理抛出的异常,换而言之抛出运行时异常和不抛出异常的效果是一样的。
反之如果抛出异常的类型是除RuntimeException及其子类之外的异常(编译时异常),就必须处理抛出的异常。
1.1 运行时异常
public class ThrowException {
public int div(int a, int b) throws ArithmeticException {
return a/b;
}
}
public class Test {
public static void main(String[] args) {
ThrowException te = new ThrowException();
int x = te.div(10, 0);
System.out.println(x);
}
}
上面的例子中,调用div()方法可能出现异常的类型是ArithmeticException(运行时异常)。所以当调用div()方法时,就不是一定要处理div()方法抛出的异常。
1.2 编译时异常
编译时异常会在编译期报错,也就是说我们必须对编译时异常进行处理,否则代码根本无法运行。下面演示编译时异常的例子:
public class Test {
public static void main(String[] args) throws FileNotFoundException {
FileInputStream fs = new FileInputStream("E:\\test.txt");
}
}
FileInputStream接受一个文件路径作为参数,运行代码时JVM会根据这个路径找到对应的文件。例子中我给出的参数是“E:\\test.txt”,这个文件是真实存在的,那么我也可以给这样一个参数“Z:\\test.txt”,我的电脑中根本不存在Z盘,这样一来JVM就找不到相应的文件了。所以在例子中,我们必须处理异常(FileNotFoundException),处理的方式是继续向上抛出。
也就是说相较于运行时异常的“意料之外”,编译时异常则是“未雨绸缪”。编译时异常是我们可以预料到的,所以我们必须将这种异常处理掉。
1.3 关键字throw
写到这里,大家可能觉得出现异常一定都是坏事,其实并不尽然。看这样一个例子:
public class Person {
private String name;
private Integer age;
public void setName(String name) {
this.name = name;
}
public void setAge(Integer age) {
this.age = age;
}
@Override
public String toString() {
return "Person [name=" + name + ", age=" + age + "]";
}
}
public class Test {
public static void main(String[] args) {
Person p = new Person();
p.setName("张三");
p.setAge(-18);
System.out.println(p); // Person [name=张三, age=-18]
}
}
很明显一个人的年龄不可能是负数,所以当代码执行到p.setAge(-18)时,程序就不应该再往下继续执行了。那么这时候就应该出现异常来中断程序的执行,下面我们对Person类做出一点修改:
public class Person {
private String name;
private Integer age;
public void setName(String name) {
this.name = name;
}
public void setAge(Integer age) {
if (age > 0 && age < 200) {
this.age = age;
} else {
throw new ArithmeticException("/年龄错误");
}
}
@Override
public String toString() {
return "Person [name=" + name + ", age=" + age + "]";
}
}
再运行上面的代码就不会出现错误的打印结果了,而是会出现异常信息:“java.lang.ArithmeticException: /年龄错误”。例子中我们在setAge()方法中创建了一个ArithmeticException异常对象,由于ArithmeticException属于运行时异常,所以setAge()方法不用再继续抛出异常。
2. try...catch
然而只是将异常一味的向上抛是不合理的,因为即使出现了异常,我们的程序也要继续执行下去,所以大多数时候异常需要我们自己去解决。
解决异常的方法:try...catch...finally
- try : 检测异常
- catch : 捕获异常
- finally : 释放资源
finally这里先不说,下面我们使用try...catch来解决刚刚碰到的异常:
public class Test {
public static void main(String[] args) {
try {
int num = div(10, 0);
System.out.println(num);
} catch (ArithmeticException e) {
System.out.println("除数为0");
}
}
public static int div(int a, int b) {
return a / b;
}
}
代码中我们把可能出现问题的语句放在try代码块中,这样一来ArithmeticException对象一旦被创建出来,就会被catch和捕获,然后执行catch代码块中的语句,打印“除数为0”。
当然,catch的参数列表中也可以直接使用Exception类型,这样一来就变成父类引用(Exception)指向子类对象(ArithmeticException )。
有时候代码中会有多种异常,这时候就可以使用一个try可以和多个catch匹配,看下面一个例子:
public class Test {
public static void main(String[] args) {
int[] arr = {11,22,33};
int a = 10;
int b = 0;
try {
System.out.println(a/b);
System.out.println(arr[4]);
} catch (ArithmeticException e) {
System.out.println("除数为0");
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("索引越界");
}
System.out.println("finish");
}
}
运行上面的代码,可以看到打印结果是“除数为0”和“finish”。也就是说,同时出现多个异常只会捕获第一个出现的异常,而且异常被捕获之后,代码会继续向下执行。
那么如果上面的代码中有三种异常呢?这里言外之意是,我们有时候会没法把问题考虑的很全面。这里可以这样来解决:
public class Test {
public static void main(String[] args) {
int[] arr = {11,22,33};
int a = 10;
int b = 0;
try {
arr = null;
System.out.println(arr[4]);
} catch (ArithmeticException e) {
System.out.println("除数为0");
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("索引越界");
} catch (Exception e) {
System.out.println("出现问题");
}
}
}
把最后一个catch的参数设置为Exception类型,这样不管出现什么异常都可以接住。
既然这样的话,只使用一个catch(Exception e)就可以捕获所有的异常,为什么还要费劲的写这么多的catch用于捕获各式各样的异常呢?
如果我们只使用一个catch(Exception e)来捕获所有的异常,那么当我们看到异常信息,就无法第一时间判断出具体出现了什么问题,因为所有的异常信息都是“出现问题”,最后还是要通过重新检查代码来确定具体出现了什么问题。所以一开始就用catch捕获的可以明确类型的异常,可以省去后面很多没必要浪费的时间。
2.1异常信息
当catch捕获到异常对象时,我们可以用这个异常对象来做什么呢?看下面一个例子:
public class Test {
public static void main(String[] args) {
try {
System.out.println(1/0);
} catch (ArithmeticException e) {
System.out.println(e.getMessage()); // / by zero
System.out.println(e.toString()); // java.lang.ArithmeticException: / by zero
e.printStackTrace(); /* java.lang.ArithmeticException: / by zero
at org.hu.test.controller.Test.main(Test.java:41) */
}
}
}
我们可以通过异常对象提供三种方法来查看异常信息。
- getMessage() :异常信息
- toString():异常类型 + 异常信息
- printStackTrace():异常类型 + 异常信息 + 出现问题行号
不难发现,平常控制台打印的报错信息,就是JVM调用printStackTrace()方法而打印的信息。
2.2 finally
finally关键字无法单独使用,必须要配合try才能使用。使用finally关键字大多数情况下是用来释放资源的,例如关闭数据库连接。出现finally关键字,不管try代码块中是否出现异常,都一定会执行finally代码块中的代码。下面看这样一段代码:
public class Test {
public static void main(String[] args) throws Exception {
try {
System.out.println(1/0);
} catch (Exception e) {
System.out.println("catch执行");
return;
} finally {
System.out.println("finally执行");
}
}
}
打印结果会不会有点吃惊?是的。即使catch代码块中出现了return,finally代码块中的语句还是执行了。这就是为什么上面说finally关键字一般是用来释放资源的,因为不管是否出现异常,和数据库的连接一定要关闭,finally关键字就保证最后一定会执行关闭数据库连接的语句。
但是如果出现下面这种情况finally代码块中的语句就不会被执行:
public class Test {
public static void main(String[] args) throws Exception {
try {
System.out.println(1/0);
} catch (Exception e) {
System.out.println("catch执行");
System.exit(0); // 退出JVM
} finally {
System.out.println("finally执行");
}
}
}
下面再看这样一段代码,猜猜看return返回的结果是多少:
public class Test {
public static void main(String[] args) throws Exception {
System.out.println(getNum());
}
public static int getNum () {
int x = 10;
try {
System.out.println(1/0);
return x;
} catch (Exception e) {
x =20;
return x;
} finally {
x = 30;
System.out.println("finally执行");
}
}
}
return返回的结果是20。首先try代码块中的return是不会执行的,那么执行的return语句就是catch代码块中return。问题是finally中的语句确实执行了,但是返回x的值没有改变。其实当执行到catch代码块中的return语句的时候,返回值x就已经确定下来了,再去执行finally代码块中的语句的时候,已经无法改变返回值了。
自定义异常
除了系统定义的异常类,我们也可以自定义异常类。看下面一个例子:
public class AgeOutofBoundException extends Exception {
public AgeOutofBoundException () {
super();
}
public AgeOutofBoundException (String message) {
super(message);
}
}
public class Person {
private String name;
private Integer age;
public void setName(String name) {
this.name = name;
}
public void setAge(Integer age) throws AgeOutofBoundException {
if (age > 0 && age < 200) {
this.age = age;
} else {
throw new AgeOutofBoundException("/年龄错误");
}
}
@Override
public String toString() {
return "Person [name=" + name + ", age=" + age + "]";
}
}
public class Test {
public static void main(String[] args) throws Exception {
Person p = new Person();
p.setName("张三");
p.setAge(-18);
System.out.println(p);
}
}
通过控制台的打印信息可以发现,除了异常类名变成了我们自定义的异常类,其余的打印信息和平时出现的异常信息没有区别。其实我们需要的恰恰就是异常类名,当看到AgeOutofBoundException异常出现的时候,就可以很轻松的知道是在年龄使用上出现了问题。
继承与异常
如果子类重写父类的方法,并且父类方法抛出异常,那么
- 子类方法抛出异常类型必须是父类方法抛出异常类型的子类或者和父类抛出异常的类型一致(子类方法也可不抛出异常)
- 子类方法抛出异常的数量不得超过父类方法抛出异常的数量
- 父类方法没有抛出异常,子类不能抛出异常
class Fu {
public void print() throws FileNotFoundException {
System.out.println("Fu");
}
public void print2() throws FileNotFoundException {
System.out.println("Fu");
}
}
class Zi extends Fu {
/*public void print() throws Exception {
System.out.println("Zi"); // 错误
}*/
public void print() throws FileNotFoundException { // 子类重写父类方法,抛出异常类型必须和父类方法抛出异常类型一致或者是其子类
System.out.println("Zi");
}
public void print2() { // 不抛出异常
System.out.println("Zi");
}
}
class Fu {
public void print() {
System.out.println("Fu");
}
}
class Zi extends Fu{
@Override
public void print() { // 父类没有抛出异常,子类不能抛出异常
try {
FileInputStream fs = new FileInputStream("E:\\test.txt");
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
}
tip:上面所说的异常都是编译时异常,抛出运行时异常不必遵守上面的规则。
class Fu {
public void print() throws ArithmeticException {
System.out.println("Fu");
}
}
class Zi extends Fu {
public void print() throws ArrayIndexOutOfBoundsException, NullPointerException {
System.out.println("Zi");
}
}
参考: