异常的基本描述
异常是指阻止程序继续执行下去的事件,异常一旦出现并且没有进行合理处理的话,那么程序就会被中断。
输出异常信息的描述
(摘自:http://www.cnblogs.com/lulipro/p/7504267.html#finally_return)
异常是在执行某个函数时引发的,而函数又是层级调用,形成调用栈的。所以,只要一个函数发生了异常,那么他的所有的caller都会被异常影响。当这些被影响的函数以异常信息输出时,就形成了异常追踪栈。而异常最先发生的地方,叫做异常抛出点。
可以看出,当devide函数发生除0异常时,devide函数将抛出ArithmeticException异常,因此调用他的CMDCalculate函数也无法正常完成,因此也发送异常,而CMDCalculate的caller–main因为CMDCalculate抛出异常,也发生了异常,这样一直向调用栈的栈底回溯。这种行为叫做异常的冒泡,异常的冒泡是为了在当前发生异常的函数或者这个函数的caller中找到最近的异常处理程序。由于这个例子中没有使用任何异常处理机制,因此异常最终由main函数抛给jvm,导致程序终止。
异常类结构(jdk 10.0.1)下
所有的异常类型最高的继承类是Throwable,Throwable下有两个子类:
- Error:指的是JVM错误,这个时候的程序并没有执行,无法处理;
- Exception:指的是程序之中出现的错误信息,可以进行异常处理。
public class Throwable implements Serializable{}
Error类(不全)
Exception类(不全)
非受查异常,包括运行异常(RuntimeException)和 Error,非受查异常仅在程序运行的时候报错。对于这些异常,我们应该修正代码,而不是去通过异常处理器处理 。这样的异常发生的原因多半是代码写的有问题。
其他的都是受查异常,受查异常编译器编译的时候就报错。这样的异常一般是由程序的运行环境导致的。因为程序可能被运行在各种未知的环境下,而程序员无法干预用户如何使用他编写的程序。
异常处理机制的作用
Java的异常处理机制强制我们去考虑程序的健壮性和安全性。异常处理不应用来作为程序的流程控制。异常处理的主要作用是捕获程序在运行时发生的异常并进行相应的处理。
异常处理机制之一:try…catch…finally
当程序遇到异常时会终止程序的运行(即后面的代码不在执行),控制权交由异常处理机制处理。
try {
可能出现异常的语句 ;
} [ catch (异常类型 异常对象) {
处理异常 ;
} catch (异常类型 异常对象) {
处理异常 ;
} ... ] [finally {
不管是否出现异常,都执行此代码 ;
}]
try+catch+finally or try+finally or try+catch
单独只有 try:error
public class Test {
public static void main(String[] args){
try {
System.out.println(4/0);
}catch(Exception e) {
System.out.println("Operation");
}
System.out.println("nextLine"); //处理异常后继续执行这里的语句
}
}
Output:
Operation
nextLine
- 如果程序中产生了异常,那么JVM根据异常的类型,实例化一个该异常类的对象;
- 如果这时程序中没有任何的异常处理操作,则这个异常类的实例化对象将交给JVM进行处理,而JVM的默认处理方式就是进行异常信息的输出,而后中断程序的执行;
- 如果程序中存在了异常处理,则会由try语句捕获产生的异常类对象;
- 与try之后的每一个catch进行匹配,如果匹配成功,则使用指定的catch进行处理,如果没有匹配成功,则向后面的catch继续匹配,如果没有任何的catch匹配成功,则这个时候将交给JVM执行默认处理;
- 不管是否有异常都会执行finally程序,如果此时没有异常,执行完finally,则会继续执行程序之中的其他代码;如果此时有异常没有能够处理(没有一个catch可以满足),那么也会执行finally,但是执行完finally之后,将默认交给JVM进行异常的信息输出,并且程序中断。
一、通常对异常对象常用的3个操作如下。
- getMessage()函数:获取异常原因的说明文字。
- toString()函数:将异常对象转换为字符串,其中包括异常类型与异常原因的说明文字。
- printStackTrace()函数:输出异常的堆栈跟踪信息。指出了异常的类型、性质、栈层次及出现在程序中的位置。
- / by zero (执行语句:System.out.println( e.getMessage()); )
- java.lang.ArithmeticException: / by zero (执行语句:System.out.println( e.toString()); )
- 异常信息的全部描述 (执行语句: e.printStackTrace(); )
二、在以下3种特殊情况下,finally块不会被执行。
- 在前面的代码中用了System.exit()退出程序。
- 当一个线程在执行 try 语句块或者 catch 语句块时被打断(interrupted)、被终止(killed)、突然死机、断电时。
- 在执行try语句块之前已经返回或抛出异常。
三、注意事项
- 在Java SE 7 中,同一个catch可以捕获多个异常类型,当异常处理动作一样时,可以合并,但合并后,异常变量隐含为final。
try{} catch (异常类型1 | 异常类型2… 异常对象e ) { //此时e是final的,不能在catch里赋给e值 } finally{}
- try…catch语句中,多个catch的顺序一定要遵循子类在上父类在下的规则。匹配时,不仅支持精确匹配,也支持父类匹配,所以父类在上子类就会执行不到。
- 见后面finally块与return 块;
- try块中的局部变量和catch块中的局部变量(包括异常变量),以及finally中的局部变量,他们之间不可共享使用。
- 、java中,异常处理的任务就是将执行控制流从异常发生的地方转移到能够处理这种异常的地方去。也就是说:当一个函数的某条语句发生异常时,这条语句的后面的语句不会再执行。执行流跳转到最近的匹配的异常处理catch代码块去执行,异常被处理完后,执行流会接着在“处理了这个异常的catch代码块”后面接着执行。
有的编程语言当异常被处理后,控制流会恢复到异常抛出点接着执行,这种策略叫做:resumption model of exception handling(恢复式异常处理模式 )
而Java则是让执行流恢复到处理了异常的catch块后接着执行,这种策略叫做:termination model of exception handling(终结式异常处理模式) - 有时,最好在try块中打开资源,在finally块中清理释放这些资源。
异常处理机制之二:抛出(throws)
一、throws关键字
thrwos关键字主要使用在方法的定义上,表示:此方法中不进行异常的处理,而交给被调用处(调用它的地方)处理。如果一个方法内部的代码会抛出检查异常,而方法自己又没有完全处理掉,则必须在方法的签名上使用throws关键字声明这些可能抛出的异常,否则编译不通过。
class MyMath {
public int div(int x, int y) throws Exception {
return x / y;
}
}
public class Test {
public static void main(String args[]) {
try {
System.out.println(new MyMath().div(10, 0));
} catch (Exception e) {
e.printStackTrace();
}
}
}
Output:
java.lang.ArithmeticException: / by zero
at MyMath.div(Test.java:3)
at Test.main(Test.java:9)
throw 抛出非受查类型时,可以不进行异常声明,但抛出受查类型时,一定要进行异常声明。
public void g(){
try {
} catch (Exception e) {
throw new Error("02",e);
}
}
or:
public void g() throws Exception{
try {
} catch (Exception e) {
throw new Exception("02",e);
}
}
但有一个技巧:将受查类型e,用RuntimeException包装。之后用getCause解出被包装的那个异常。
import java.io.FileNotFoundException;
import java.io.IOException;
public class Test {
public void g(int type) {
try {
switch(type) {
case 0: throw new FileNotFoundException("I am zero");
case 1: throw new IOException("I am one");
case 2: throw new RuntimeException("I am two");
}
} catch (Exception e) {
throw new RuntimeException("TEST",e);
}
}
public static void main(String[] args) throws Throwable {
try{
Test t = new Test();
t.g(2);
}catch(RuntimeException re){
try {
//注意这里的re不能改成e,会与块内部的e冲突
throw re.getCause();
}
catch(FileNotFoundException e){
System.out.println("FileNotFoundException:"+e);
}
catch(IOException e){
System.out.println("IOException:"+e);
}
catch(RuntimeException e){
System.out.println("RuntimeException:"+e);
}
}
}
}
二、throws注意事项
- 在调用throws声明的方法的时候,一定要使用异常处理操作对受查异常进行处理,这属于强制性的处理。
- 如果是非受查异常(Error、RuntimeException或它们的子类),使不使用throws关键字来声明要抛出的异常都可以,声明后在被调用处有没有try…catch…finally语句都可以。编译仍能顺利通过,但出现异常之后将交给JVM默认进行处理。
- 使用throws关键字将异常抛给上一级方法调用者后,如果不想处理该异常,还可以继续向上抛出,但最终要有能够处理该异常的代码。
- 如果在主方法中使用了throws声明,则会将异常交给JVM进行异常的处理,也就是采用默认的方式,输出异常信息,而后结束程序执行。
- 可以在一个方法中声明抛出多个异常。
三、throw关键字
之前所有异常类对象都是由JVM自动进行实例化操作的,而继承Throwable类或其子类也可以自定义异常。在编写自定义的方法和工具类时,可能会出现不确定的程序错误和暂时不能解决的问题,这时用户可以使用throw抛出自定义异常类的实例化对象(当然也可以抛出系统给定的异常的对象)。
public class K {
public static void main(String args[]) {
try {
throw new Exception("自定义的异常"); //该语句抛出一个异常,之后被catch捕获,处理
} catch (Exception e) {
System.out.println("test");
e.printStackTrace();
}
}
}
Output:
test
java.lang.Exception: 自定义的异常
at K.main(K.java:4)
class MyException extends Exception {
private double price;
public MyException(double price) {
this.price=price;
}
public double getPrice() {
return price;
}
@Override
public String getMessage() {
String message="错误的数值,商品单价不能是负数。";
return message;
}
}
public class Test {
private double sum = 0;
public static void main(String[] args) {
double priceSum = 0;
Test demo = new Test();
try {
priceSum = demo.computeProductSum(5.10);
priceSum = demo.computeProductSum(12);
priceSum = demo.computeProductSum(-12);
} catch (MyException e) {
e.printStackTrace();
System.out.println("单价"+e.getPrice()+"非法,请核对后,重新计算。");
}
System.out.println("商品单价的总和是:" + demo.getSum());
}
public double computeProductSum(double price) throws MyException {
if (price < 0)
throw new MyException(price);
else {
sum += price;
}
return sum;
}
public double getSum() {
return sum;
}
}
Output:
MyException: 错误的数值,商品单价不能是负数。
at Test.computeProductSum(Test.java:32)
at Test.main(Test.java:23)
单价-12.0非法,请核对后,重新计算。
商品单价的总和是:17.1
四、throw和throws的区别?
- throw:在方法体内使用,表示人为的抛出一个异常类对象(这个对象可以是自己实例化的,也可以是已存在的);
- throws:在方法的声明上使用,表示在此方法中不进行异常的处理,而交给被调用处处理。
五、声明 or 不声明?
不管对不对非受查类型异常声明,只要调用处不捕获,则抛出异常时会显示发生异常的线程出处,
一旦声明了异常,同时在调用处捕获,则抛出异常时就不会显示发生异常的线程出处,
自定义异常
自定义的异常应该总是包含如下的构造函数:
- 一个无参构造函数
- 一个带有String参数的构造函数,并传递给父类的构造函数。
- 一个带有String参数和Throwable参数,并都传递给父类构造函数
- 一个带有Throwable 参数的构造函数,并传递给父类的构造函数。
IOException类的完整源代码:jdk 10.0.1
package java.io;
public class IOException extends Exception {
static final long serialVersionUID = 7818375828146090155L;
public IOException() {
super();
}
public IOException(String message) {
super(message);
}
public IOException(String message, Throwable cause) {
super(message, cause);
}
public IOException(Throwable cause) {
super(cause);
}
}
异常链
假设B模块完成自己的逻辑需要调用A模块的方法,如果A模块发生异常,则B也将不能完成而发生异常。但是B在抛出异常时,会将A的异常信息掩盖掉(因为此时抛出的是一个新的异常对象,不含有旧异常信息),这将使得异常的根源信息丢失。异常的链化可以将多个模块的异常串联起来,使得异常信息不会丢失。
异常链化:以一个异常对象为参数构造新的异常对象。新的异常对象将包含先前异常的信息。这项技术主要是异常类的一个带Throwable参数的函数来实现的。这个当做参数的异常,我们叫他根源异常(cause)。
用异常链:
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.util.Scanner;
class MyException extends Exception {
public MyException() {
super();
}
public MyException(String message) {
super(message);
}
public MyException(String message, Throwable cause) {
super(message, cause);
}
public MyException(Throwable cause) {
super(cause);
}
}
public class Test2 {
public void f() throws MyException{
try {
FileReader reader = new FileReader("D:\\test1.txt"); //5
Scanner in = new Scanner(reader);
System.out.println(in.next());
} catch (FileNotFoundException e) {
//e 保存异常信息。
//这里抛出新的异常,但却是因为e异常对象才抛出的。
throw new MyException("文件没有找到--01",e); //3
}
}
public void g() throws MyException{
try {
f(); //4
} catch (MyException e) {
//e 保存异常信息
throw new MyException("文件没有找到--02",e); //1
}
}
public static void main(String[] args) {
Test2 t = new Test2();
try {
t.g(); //2
} catch (MyException e) {
e.printStackTrace();
}
}
}
Output:
MyException: 文件没有找到--02
at Test2.g(Test2.java:35)
at Test2.main(Test2.java:41)
Caused by: MyException: 文件没有找到--01
at Test2.f(Test2.java:27)
at Test2.g(Test2.java:32)
... 1 more
Caused by: java.io.FileNotFoundException: D:\test1.txt (系统找不到指定的文件。)
at java.base/java.io.FileInputStream.open0(Native Method)
at java.base/java.io.FileInputStream.open(FileInputStream.java:220)
at java.base/java.io.FileInputStream.<init>(FileInputStream.java:158)
at java.base/java.io.FileInputStream.<init>(FileInputStream.java:113)
at java.base/java.io.FileReader.<init>(FileReader.java:58)
at Test2.f(Test2.java:22)
... 2 more
仔细看下输出顺序。。。
解析:自底向上
抛出点1,由于2调用点;
异常1处的e由4处的e引起;
抛出点3,由于4调用点;
异常4处的e由5处的异常引起;
不用异常链:
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.util.Scanner;
public class Test {
public void f() throws MyException{
try {
FileReader reader = new FileReader("D:\\test1.txt");
Scanner in = new Scanner(reader);
System.out.println(in.next());
} catch (FileNotFoundException e) {
System.out.println("*****");
throw new MyException("ddddd"); //这里抛出的异常被g()中的catch接收。
}
}
public void g() throws MyException{
try {
f();
} catch (MyException e) {
System.out.println("-------");
throw new MyException("kkkkkk");//catch接收了以后,却又抛出一个新的异常对象 //1
//new MyException("kkkkkk");
//而new MyException("ddddd")对象被隐藏了。
}
}
public static void main(String[] args) {
Test t = new Test();
try {
t.g(); //2
} catch (MyException e) { //catch接收到异常对象new MyException("kkkkkk");
System.out.println("ttttt");
e.printStackTrace();//输出e的信息
//这样,e,也就是new MyException("kkkkkk")抛出点是1处,调用它的2处也会出现异常
//只会输出这两条信息。
}
}
}
Output:
*****
-------
ttttt
MyException: kkkkkk
at Test.g(Test.java:26)
at Test.main(Test.java:32)
异常的注意事项
当子类重写父类的带有 throws声明的函数时,其throws声明的异常必须在父类异常的可控范围内——用于处理父类的throws方法的异常处理器,必须也适用于子类的这个带throws方法 。这是为了支持多态。
- 一个方法被重写时,重写它的方法必须抛出相同的异常或异常的子类。
- 如果父类方法抛出多个异常,那么子类重写该方法时必须抛出父类抛出的所有异常的并集下的一个子集,且不能抛出新异常。因为父类抛出意味着这些异常可以被处理,那么子类抛出父类异常的并集下的一个子集也可以被处理。
例子摘自:http://www.cnblogs.com/lulipro/p/7504267.html#finally_return
class Father
{
public void start() throws IOException
{
throw new IOException();
}
}
class Son extends Father
{
public void start() throws Exception
{
throw new SQLException();
}
}
/**********************假设上面的代码是允许的(实质是错误的)***********************/
class Test
{
public static void main(String[] args)
{
Father[] objs = new Father[2];
objs[0] = new Father();
objs[1] = new Son();
for(Father obj:objs)
{
//因为Son类抛出的实质是SQLException,而IOException无法处理它。
//那么这里的try。。catch就不能处理Son中的异常。
//!!!!!!!!!!!!多态此时就不能实现了。
try {
obj.start();
}catch(IOException)
{
//处理IOException
}
}
}
}
- Java的异常处理流程是线程独立的,线程之间没有影响。
Java程序可以是多线程的。每一个线程都是一个独立的执行流,拥有独立的函数调用栈。如果程序只有一个线程,那么没有被任何代码处理的异常会导致程序终止。如果是多线程的,那么没有被任何代码处理的异常仅仅会导致异常所在的线程结束。也就是说,Java中的异常是线程独立的,线程的问题应该由线程自己来解决,而不要委托到外部,也不会直接影响到其它线程的执行。
finally与return
下面前三个例子摘自:http://www.cnblogs.com/lulipro/p/7504267.html#finally_return
在 try,catch块中即便有 return,break,continue等改变执行流的语句,finally也会执行。
try…catch…finally中的return 只要能执行,就都执行了,他们共同向同一个内存地址写入返回值,后执行的将覆盖先执行的数据,而真正被调用者取的返回值就是最后一次写入的。
例一:finally中的return 会覆盖 try 或者catch中的返回值。
public class Test {
@SuppressWarnings("finally") //告诉编译器忽略指定的警告
public static int foo(){
try{
int a = 5 / 0;
} catch (Exception e){
return 1;
} finally{
return 2;
}
}
@SuppressWarnings("finally")
public static int bar(){
try {
return 1;
}finally {
return 2;
}
}
public static void main(String[] args){
int result;
result = Test.foo();
System.out.println(result); //输出2
result = Test.bar();
System.out.println(result); //输出2
}
}
Output:
2
2
例二:finally中的return会抑制(消灭)前面try或者catch块中的异常,使原异常代码正常。
public class Test{
@SuppressWarnings("finally")
public static int foo() throws Exception{
try {
int a = 5/0;
return 1;
}catch(ArithmeticException amExp) { //catch中的异常被抑制
throw new Exception("我将被忽略,因为下面的finally中使用了return");
}finally {
return 100;
}
}
@SuppressWarnings("finally")
public static int bar() throws Exception{
try {
int a = 5/0; //try中的异常被抑制
return 1;
}finally {
return 100;
}
}
public static void main(String[] args){
int result;
try{
result = Test.foo();
System.out.println(result); //输出100
} catch (Exception e){
System.out.println(e.getMessage()); //没有捕获到异常
}
try{
result = Test.bar();
System.out.println(result); //输出100
} catch (Exception e){
System.out.println(e.getMessage()); //没有捕获到异常
}
}
}
Output:
100
100
例三:finally中的异常会覆盖(消灭)前面try或者catch中的异常。
public class Test{
@SuppressWarnings("finally")
public static int foo() throws Exception{
try {
int a = 5/0;
return 1;
}catch(ArithmeticException amExp) { //catch中的异常被抑制
throw new Exception("我将被忽略,因为下面的finally中抛出了新的异常");
}finally {
throw new Exception("我是finaly中的Exception");
}
}
@SuppressWarnings("finally")
public static int bar() throws Exception{
try {
int a = 5/0; //try中的异常被抑制
return 1;
}finally {
throw new Exception("我是finaly中的Exception");
}
}
public static void main(String[] args){
int result = 100;
try{
result = Test.foo();
} catch (Exception e){
assert result == 100;
System.out.println(e.getMessage()); //输出:我是finaly中的Exception
}
try{
result = Test.bar();
} catch (Exception e){
assert result == 100;
System.out.println(e.getMessage()); //输出:我是finaly中的Exception
}
}
}
Output:
我是finaly中的Exception
我是finaly中的Exception
例四:finally中的异常会覆盖(消灭)前面try或者catch中的return。
public class Test1 {
@SuppressWarnings("finally")
int test() throws Exception{
try {return 1;} //返回1
catch(Exception e) {
throw new Exception("我将被忽略,因为下面的finally中抛出了新的异常");
}
finally { throw new Exception("我是finaly中的Exception");}
}
public static void main(String[]args) {
try {
System.out.println(new Test1().test()); //返回的1并没有输出
}catch (Exception e){
System.out.println(e.getMessage());
}
}
}
Output:
我是finaly中的Exception
例五:
finally块后执行,之前的return语句会向存放返回值的内存地址里写入返回值。等到finally执行完毕后,再将最后一次更新的返回值返回给caller。
public class Test {
public static int test() {
int x = 1;
try
{
return (x+100);
}
finally
{
++x;
System.out.println(x);
x++;
System.out.println(x);
}
}
public static void main(String[] args) {
System.out.println( Test.test());
}
}
Output:
2
3
101
所以:存放返回值的内存地址中最后更新的返回值是101,但此时还没有返回,执行完finally后才返回101。
例六:证明finally块后执行。并会将返回值更新。
public class Test {
@SuppressWarnings("finally")
int test() {
try {return func1();}
finally {return func2();}
}
int func1() {
System.out.println("func1");
return 1;
}
int func2() {
System.out.println("func2");
return 2;
}
public static void main(String[]args) {
System.out.println(new Test().test());;
}
}
Output:
func1
func2
2