Java基础(24)——异常详解
版权声明
- 本文原创作者:清风不渡
- 博客地址:https://blog.csdn.net/WXKKang
一、异常体系
1、概述
异常,即不正常;在软件开发领域它指程序发生了不正常的情况
每种异常都是使用一个异常对象来表示,所有的异常类都是直接或间接的继承自Throwable类,它们组成了一个完整的类继承结构,其中根类是Throwable,在Java中,异常体系中的类在java.lang中定义,这个包是默认被导入的
2、异常的根类(Throwable)
(1)构造函数
(2)主要方法
getMessage()方法
输出异常信息,实际上就是通过构造方法传入的字符串,例如:
package cn.com;
public class Demo {
public static void main(String[] args) {
Throwable throwable = new Throwable("这是一串异常消息");
System.out.println(throwable.getMessage());
}
}
执行结果如下:
toString()方法
以字符串形式显示异常,如下:
package cn.com;
public class Demo {
public static void main(String[] args) {
Throwable throwable = new Throwable("这是一串异常消息");
System.out.println(throwable.toString());
}
}
执行结果如下:
printStackTrace()方法
打印异常消息和异常栈,有以下三种使用方法:
方法 | 注释 |
---|---|
printStackTrace() | 将异常信息输出到控制台 |
printStackTrace(PrintStream) | 将异常信息输出到一个字节流中 |
printStackTrace(PrintWriter) | 将异常信息输出到一个字符流中 |
3、错误(Error)
这类问题是有Java虚拟机抛出的,一般都是很严重的,比如说系统崩溃、内存溢出、加载动态链接库失败、栈溢出等等,这类问题是我们在应用程序的层面无法处理和解决的,常见的如:StackOverflowError,OutOfMemory(OOM)
4、Exception
Exception分为两种:
运行时异常(RuntimeException)
RuntimeException类及其子类都是程序可以通过编译;但存在安全隐患;为了避免这种情况的发生建议提高代码的健壮性;在必要时刻也可采用try…catch处理,以下是常见的编译时异常:
编译时异常
除了RuntimeException以外Exception的其他子类都属于编译时异常。这种异常必须要处理,否则将不能通过编译,以下是几种常见的编译时异常:
二、异常的处理方式
1、默认的异常处理方式
我们知道,在做除法运算时,除数是不能等于零的,现在我们就利用这一特点来制造一个异常,看看Java默认的处理异常的方式是怎样的,代码如下:
package cn.com;
public class Demo {
public static void main(String[] args) {
int a =5;
int b =0;
System.out.println("a的值为"+a+",b的值为"+b);
int c=a/b;
System.out.println("a除以b的结果为:"+c);
}
}
执行结果如下:
如上代码所述,当除数为零的时候,将发生算术异常,因此代码System.out.println(“a除以b的结果为:”+c);将不会被执行
那么,我们是否可以自行的处理异常呢,答案是可以的,有如下几种方式:
2、try…catch方式
(1)基本知识
try…catch语法如下:
//执行try里面的代码,如果有异常则跳至catch中执行其内的代码
try{
可能出现问题的代码;
}catch(异常名 变量){
针对问题的处理;
}finally{
释放资源;
}
//或者
try{
可能出现问题的代码;
}catch(异常名 变量){
针对问题的处理;
}
注意:
第一点:try里面的代码越少越好
第二点:catch里面必须要有内容,哪怕是给出一个简单的提示
例子: 下面我们就将上面的例子用try…catch方式来自行的处理异常,代码如下:
package cn.com;
public class Demo {
public static void main(String[] args) {
int a =5;
int b =0;
try {
System.out.println("a除以b的结果为:"+a/b);
} catch (ArithmeticException e) {
if (b==0) {
System.out.println("除数不能为零,请重新输入");
}
}
System.out.println("除法运算结束!!!");
}
}
执行结果如下:
如果没有使用try…catch进行处理,那么出现算术异常之后,程序就会异常终止,而使用try…catch处理异常之后,程序会继续执行try…catch之后的代码,因此会执行System.out.println(“除法运算结束!!!”);
如果代码中可能抛出多种异常 ,例如除数为零、数组下标越界、空指针等在一起该如何处理呢?只需要我们将可能发生异常的代码放入try语句中,然后为其添加多个catch语句,每个catch语句中处理一种异常即可
总结:
1、程序中的功能代码放在try块中,这些功能代码中可以有多个语句可能发生异常。
2、在try块中的代码抛出的异常后,这行代码之后的try 块中的代码都将不会再执行,现在将查找匹配的catch块继续处理。
3、JVM将会按catch块的代码顺序依次匹配异常类型。如果有任何一个catch块被匹配,那么其它的catch将不会再次被匹配。在catch块代码执行完成后,执行try/catch块后面的代码。
如果没有匹配任何一个catch块,那么异常将被拋出到函数外面,参见(3)小节内容。
如果有任何一个catch被匹配,那么拋出的异常对象将被赋给catch中定义的异常变量。例如,catch (NullPointerException e)将异常对象赋给e。
(2)使用多态机制处理异常
前面的例子中,如果try块中可能抛出多个异常,则需要为每个异常都创建一个catch块;这样的话,当抛出的异常类型比较多的时候,catch块就会很多,这样代码就会比较臃肿且不易维护,那么怎么办呢?有以下两种方式:
a、使用父类捕获
在实际项目中,很多时候并不需要详细区分每种异常类型,这时希望提供某种简化处理方式,使一个catch块可以捕获多种异常,我们就可以使用多态的特性来解决这个问题,使用这些异常的父类接收,例如:
try {
...
} catch (Exception e) {
...
}
现在,catch(Exception e)分支将可以匹配Exception及其所有的子类,下面就来举例说明:
package qfbd.com;
public class Demo {
public static void main(String[] args) {
int a = 5;
int b = 0;
int[] arr = {1,2};
Demo demo = new Demo();
demo.demo1(a, b, arr);
}
public void demo1(int a,int b,int[] arr){
try {
System.out.println(arr[5]); //数组下标越界
System.out.println(a/b); //除数为零
} catch (Exception e) {
// TODO: handle exception
System.out.println("出错啦");
}
System.out.println("函数执行完毕");
}
}
执行结果:
从上面的例子中我们可以看出,因为所有的异常类都是Exception类的子类,所以当使用父类捕获异常之后,所有在try{}中发生的异常都会被捕获而显示出catch (Exception e) {}中的异常信息,这就是父类捕获
b、同时使用父类和子类捕获
通常,我们会使用父类来匹配捕获它本身和它所有的子类,这样代码就会比较简洁。但是有时候我们需要单独的处理某个具体的子类,那么又该这么办呢?我们可以同时使用父类和子类捕获异常来实现这一需求,例如:
package qfbd.com;
public class Demo {
public static void main(String[] args) {
int a = 5;
int b = 0;
int[] arr = {1,2};
Demo demo = new Demo();
demo.demo1(a, b, arr);
}
public void demo1(int a,int b,int[] arr){
try {
System.out.println(arr[5]); //数组下标越界
System.out.println(a/b); //除数为零
} catch (ArrayIndexOutOfBoundsException e) {
// TODO: handle exception
e.toString();
e.getMessage();
e.printStackTrace();
System.out.println("数组下标越界异常");
}catch (Exception e) {
// TODO: handle exception
System.out.println("出错啦");
}
System.out.println("函数执行完毕");
}
}
执行结果:
看到上面的代码我们或许有这样一个疑问,那就是在try{}中还有一个算术异常(除数为零),它在下方的catch中对应的应该是其父类的异常信息,但是它并没有执行,是因为当顺序执行到数组下标越界的语句时发生了异常,从而直接跳到了catch (ArrayIndexOutOfBoundsException e) {}中,而该句后面的语句都没有被执行,所以才只会显示出了数组下标越界异常。所以若要实现单独的处理某个异常信息,则需要满足以下的语法格式:
多个catch语句之间的执行顺序
1、catch块是按照代码顺序进行匹配,从上到下
2、如果多个catch内的异常有父子类关系。那么要按照子类在前,父类在后的顺序编写
如果子类异常在前,父类在后,那么编译没有问题
如果父类异常在前,子类在后,则编译不通过。因为父类可以处理子类的异常,那么子类的catch将执行不到
(3)finally
在ctr…catch之后我们通常会加上一个finally,在其内进行资源的清除工作,如关闭打开的文件等。在处理异常的时候最多只能有一个finally语句块,但是在try…carch中可以没有finally语句块
(1)通常使用finally的作用——在finally中进行资源回收
例如:在程序中打开了一个文件,在try中捕获到了一个异常,使用catch处理后程序正常执行
package qfbd.com;
import java.io.FileInputStream;
import java.io.InputStream;
public class Demo {
public static void main(String[] args) {
Demo demo = new Demo();
demo.demo1();
}
public void demo1(){
InputStream fis = null;
try {
fis = new FileInputStream("d:/test.txt");
int a = 5/0; //除数为零
} catch (Exception e) {
// TODO: handle exception
System.out.println("出错啦");
}
System.out.println("函数执行完毕");
}
}
执行结果如下:
由于我们没有及时的关掉io流,并且JVM中使用垃圾回收机制来收回不再使用的对象,所以这里的InputStream对象将由垃圾回收线程来回收,但是垃圾回收的执行时间是不确定的,因此,在上面的代码中我们将无法及时回收为读取文件而分配的资源
为了解决这个问题,Java中引入了 finally语句块,在try/catch执行完成后,其后的finally块一定会得到执行,这样就可以在finally中执行资源释放工作,例如:
package qfbd.com;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
public class Demo {
public static void main(String[] args) {
Demo demo = new Demo();
demo.demo1();
}
public void demo1(){
InputStream fis = null;
try {
fis = new FileInputStream("d:/test.txt");
int a = 5/0; //除数为零
} catch (Exception e) {
// TODO: handle exception
System.out.println("出错啦");
}finally{
if(fis!=null){
try {
fis.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
System.out.println("函数执行完毕");
}
}
这样就会在出现异常并执行catch块语句之后第一时间来关闭开启的资源,从而达到资源及时释放的目的
(2)finally是否永远都执行?
a、无论程序正常还是异常,都会执行finally
b、return都不能停止finally的执行过程,例如:
try {
return;
} finally {
// 即使try块中执行了return,finally中的代码也会在函数返回前执行
}
c、只有一种情况可以阻止finally块的执行——如果JVM退出了(System.exit(0)),finally就不会执行
try {
System.exit(0);
} finally {
// 这种情况下finally中的代码将不会执行
}
3、throws
1、基础知识及示例
处理异常的时候,除了try…catch方式之外还有一种方式,那就是将异常抛出,我们利用throws可以将异常抛出本函数,如果我们不想在本函数中处理异常,或是处理不了,就可以利用它将异常抛出到函数的外部
语法格式:
throws+异常类名
(1)在demo1函数中声明异常并抛出
例如,在函数demo1(int a,int b)中,如果除数(b)为0,使用throw new Exception(“除数不能为0”);抛出Exception类型的异常,那么在函数声明上使用throws声明抛出的异常,告知函数调用者必须处理。如果在函数的声明上不使用throws声明抛出异常类型,编译将会报错
如下,有一个函数中使用了throws的方法抛出了一个异常
public void demo1(int a,int b) throws Exception{ //声明异常,告知调用者必须处理
if(b==0){
throw new Exception("除数不能为零"); //throw关键字后跟的是具体的异常对象
}
System.out.println("ab相除的结果为:"+a/b);
System.out.println("除法运算");
}
(2)在函数中调用demo1函数
当一个函数调用这个函数的时候,编译时会报错,提示需要处理这个异常,如下:
我们可以选择继续抛出异常,或是使用try…catch的方式捕获这个异常,如下:
//try...catch方式捕获异常
public void test (){
int a = 6;
int b = 0;
Demo demo = new Demo();
try {
demo.demo1(a, b);
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
//throws方式抛出异常
public void test () throws Exception{
int a = 6;
int b = 0;
Demo demo = new Demo();
demo.demo1(a, b);
}
(3)使用main函数调用demo1函数
当main函数调用这个含有抛出异常的函数时,我们也可以选择使用throws的方式抛出异常或是使用try…catch的方式捕获异常,但是需要注意的一点是:当在main函数中使用throws抛出异常时,如果在运行过程中不论发生并抛出了任何异常,都会导致JVM异常中止!!!
package qfbd.com;
public class Demo {
public static void main(String[] args) throws Exception {
int a = 6;
int b = 0;
Demo demo = new Demo();
demo.demo1(a,b);
System.out.println("over");
}
public void demo1(int a,int b) throws Exception{ //声明异常,告知调用者必须处理
if(b==0){
throw new Exception("除数不能为零"); //throw关键字后跟的是具体的异常对象
}
System.out.println("ab相除的结果为:"+a/b);
System.out.println("除法运算");
}
}
(4)throw和throws的区别
a、相同点
两者都是用于做异常的抛出处理的
b、不同点
使用的位置不同:在函数上使用throws声明被抛出的异常类型,在函数内使用throw抛出异常对象
c、后面接受的内容不同
throws后是抛出的异常类型,可以有多个,使用逗号隔开。例如:
//throws后是抛出的异常类型
public void demo2() throws IOException,FileNotFoundException {
//throw后是一个被抛出的异常对象
throw new IOException("IO异常");
}
2、常见问题
(1)子类的方法重写
我们知道,当一个子类继承了一个父类的时候,便继承了父类的方法,如果子类中重写了父类的方法,并且父类中对应的方法有使用throws抛出异常的行为,则需要注意:
子类中重写方法中抛出的异常类型不能大于父类中被重写方法抛出的异常类型,否则会编译失败,例如:
总结:
1、父类方法抛出异常,子类的重写方法可以不抛出异常,或者抛出父类方法的异常,或者该父类方法异常的子类
2、父类没有抛出异常,则子类不可抛出异常
(2)Throwable
如果在程序中希望捕获所有的异常或错误,需使用Throwable进行捕获。这种结构通常是在服务器中使用的,为了保证服务器中发生的任何异常都不会抛出到JVM,从而导致JVM异常终止,语法如下:
try {
} catch (Throwable th) {
// 异常处理
}
4、异常总结
Java中的异常处理是通过三个关键字来实现的:try——catch——finally,语法如下:
try {
//接收监视的程序块,在此区域内发生的异常,有catch中指定的程序处理
} catch (要处理的异常类型 标识符) {
// 处理异常
}catch (要处理的异常类型 标识符) {
// 处理异常
}
...
finally {
//最终处理
}
2、try、catch、finally 这三个关键字均不能单独使用,可以组成try…catch、 try…finally、try…catch…finally三种结构。
3、catch 语句可以有一个或多个,finally 语句最多-一个。
4、try、 catch、 finally 三个代码块中变量的作用域分别独立而不能相互访问。如果要在三个块中都可以访问,则需要将变量定义到这些块的外面。
5、有多个catch块时候,如果匹配就仅仅执行这个catch块,不会再执行别的catch块。
6、throw语句后不允许紧跟其他语句,因为这些语句没有机会执行,编译时会报错。
7、如果一个方法调用了另外一个声明抛出异常的方法,那么这个方法要么处理异常,要么重新声明抛出。
8、有多个catch 块时,如果其中异常类型有父子类关系时,则子类在前面,父类在后面。
三、自定义异常类
通常,自定义异常继承于Exception 类。自定义异常类名的命名规则是“异常名字+Exception”,例如: NoMoneyException。 .
例子,在支付账单时没有足够的钱:
这是一种典型的业务异常,我们需要定制一个继承于Exception 的NoMoneyException,在这个异常类中提供二种构造函数:有参构造函数和无参构造函数。
class NoMoneyException extends Exception{
public NoMoneyException() {
// TODO Auto-generated constructor stub
}
public NoMoneyException(String message) {
// TODO Auto-generated constructor stub
super(message);
}
}
注意:通常自定义异常类就是这种类型的结果,只需要简单的定义。我们的目的是在catch语句中使用异常类型对不同的错误进行分类,并不需要异常类型提供其他的功能。
在应用程序中,直接创建异常对象并抛出,然后按照常规的异常处理编程,例如:
如果money小于10元,则抛出异常,提示余额不足,如下:
package qfbd.com;
public class Demo {
public static void main(String[] args) throws Exception {
double money = 5;
pay(money);
}
public static void pay(double money) throws NoMoneyException{
if(money<10){
throw new NoMoneyException("余额不足,请充值");
}
System.out.println("支付成功!!");
}
}
class NoMoneyException extends Exception{
public NoMoneyException() {
// TODO Auto-generated constructor stub
}
public NoMoneyException(String message) {
super(message);
}
}
执行结果:
如果money大于10元,则显示支付成功,如下:
package qfbd.com;
public class Demo {
public static void main(String[] args) throws Exception {
double money = 20;
pay(money);
}
public static void pay(double money) throws NoMoneyException{
if(money<10){
throw new NoMoneyException("余额不足,请充值");
}
System.out.println("支付成功!!");
}
}
class NoMoneyException extends Exception{
public NoMoneyException() {
// TODO Auto-generated constructor stub
}
public NoMoneyException(String message) {
super(message);
}
}
执行结果: