zoukankan      html  css  js  c++  java
  • Java探索之旅(16)——异常处理

    1.异常与异常处理

        在《java编程思想》中这样定义 异常:阻止当前方法或作用域继续执行的问题。虽然java中有异常处理机制,但是要明确一点,决不应该用"正常"的态度来看待异常。绝对一点说异常就是某种意义上的错误,就是问题,它可能会导致程序失败。之所以java要提出异常处理机制,就是要告诉开发人员,你的程序出现了不正常的情况,请注意。

        异常就是一个表示组织执行正常进行的错误(情况)。异常没有处理,程序将非正常终止。这是Java鲁棒性的一个体现。异常处理最根本的优势或者目的:即将检测错误和处理错误分离,被调用的方法检测错误而调用此方法的程序(方法)处理错误。

        常见的异常有2种构造方法,默认或者字符串参数。

       如下:两个数除法的运算,实现了被除数为0的异常处理

    import java.util.*;
    public class Exception {
    	public static void main(String args[]){
    		Scanner input=new Scanner(System.in);
    		int a,b;
    		boolean flag=true;
    		do{
    		   try{
    			     System.out.println("input a,b");
    			     a=input.nextInt();
    			     b=input.nextInt();
    			     int result=division(a,b);
    			     System.out.println("a/b = "+result);
    			     flag=false;
    		      }
    		  catch(ArithmeticException ex){//处理错误
    			    System.out.println(ex);
    			    input.nextLine();//清行,等待输入
    		    }
    		 }
    		while(flag);
    		input.close();
    	}
    	public static int division(int a,int b)//检错错误
    	{
    		if(b!=0)
    			return a/b;
    		else 
    			throw new ArithmeticException(" b cannot be zero ,input again");
    	}
    
    }


    2.异常类型与体系

     Throwable是所有异常的根。所有的异常类直接或者间接继承于它,体系如下。

    异常类一般分为如下3类

        ❶Error 系 统错误(system error)类:JVM生成和抛出。如LinkageError,编译产生的不兼容问题,内存泄露,死循环等。此种情况程序本身无法处理,只能由外来程序干预。
        ❷Exception 异常类:描叙程序和外部环境引起错误,这些错误能够被捕获。
        ❸Runtime Excepiton 运行时异常类:为免检异常。描叙程序编写错误,如错误类型、越界、空指针等。一般由JVM抛出。包括IllegalArgumentException、IllegalStateException、NullPointerException、IndexOutOfBoundsException等等

    也可以分为2类:运行时异常或者免检查异常,非运行时异常或者必检异常  
        ❶免检异常Unchecked Exception:仅仅包括Runtime Excepiton、Error及其子类。一般为程序设计上存在不可恢复的逻辑错误。对免检异常可以不做处理,JVM会处理。
        ❷必检异常Checked Exception:其他所有的异常,即狭义的异常,必须被被程序捕获和处理。如IOException,ClassNotFoundException及其子类。为try...catch...所显示的捕获,编译器强制程序员检查并处理即必检之含义(此就是免检或非免检的差别)。


    3.异常的捕获和处理 常捕yi获和处理框架:

      3.1处理和捕获框架   

        ❶try语句块:尝试运行代码,try语句块中代码受异常监控,其中代码(或者调用的方法)发生异常时,会抛出异常对象。 
        ❷catch语句块:会捕获try代码块中发生的异常并在其代码块中做异常处理,catch语句带一个Throwable类型的参数表示可捕获异常类型。当try中出现异常时,catch会捕获到发生的异常并匹配。catch语句可以有多个,用来匹配多个中的一个异常。一旦匹配上后,就不再尝试匹配别的catch块了。由于catch可以捕获某个父类异常及其所有派生子类。因此应该保证子类catch块在父类的catch块之前,否则产生编译错误

        ❸finally语句块:这一语句块可以省略。 是紧跟catch语句后的语句块,这个语句块总是会在方法返回前执行, 而不管是否try语句块是否发生异常。并且这个语句块总是在方法返回前执行。 目的是给程序一个补救的机会。这样做也体现了Java语言的健壮性,具体见4.3节

        try{
          //(尝试运行的)程序代码
        }catch(异常类型 异常的变量名){
          //异常处理代码
        }finally{
          //异常发生,方法返回之前,总是要执行的代码
        }
           三个语句块均不能单独使用,可以组成形如:
                      try...catch...finally
                      try...catch
                      try...finally
        3种结构,catch语句可以有一个或多个,finally语句最多一个。局部变量独立而不能相互访问。多个catch块时候,由上到下匹配,一旦成功某个异常类即执行catch块代码,且不再执行其他catch块。

     3.2throwthrows关键字 

       ❶throw关键字:用于方法体内部,用来抛出一个Throwable类型的异常。如果抛出了某种检查异常, 则还在方法头部用throws对应声明。该方法的调用者必须检查处理抛出的异常。如果所有方法都层层上抛获取的异常,最终JVM会进行处理,处理也很简单,就是打印异常消息和堆栈信息。如果抛出免检异常(ErrorRuntimeException),则该方法的调用者可选择是否处理该异常。 
         如果异常处理器没有处理某异常,Java容许异常处理器重新抛出该异常。
       ❷throws关键字:用于方法体外部的方法声明部分,用来声明方法可能会抛出某些异常。仅当抛出了检查异常,该方法的调用者才必须处理或者重新抛出该异常。当方法的调用者没有处理该异常的时候,应该继续抛出。注意,如果方法没有在父类中声明异常,那么不能在子类中对其进行覆盖来声明异常

    public static void test3() throws Exception{   
      //抛出一个检查异常   
            throw new Exception("方法test3中的Exception");   
        }   


     3.3 异常说明 

      常见异常类的成员函数(继承自Throwable)如下:   
        ❶String getCause():返回引起异常的原因(如引起该异常的异常)。如果cause不存在或未知则返回 null。 
        ❷String getMessage():返回异常的消息信息,即新建异常时候的String参数。 
        String toString();  返回“异常类全名:getMessage()方法返回字符串”
        void  printStackTrace():对象的堆栈跟踪输出至错误输出流
        StackTraceElement[]  getStackTrace():返回栈跟踪元素数组来表示可抛出的栈跟踪信息。可以调用子函数确定异常发生的类(getClassName()),发生异常的方法(getMeholdName()),发生的行号(getLineNumber())等信息。对于嵌套调用,元素0为栈顶元素,表示调用序列的最后一个调用(异常被抛出的地方),最后一个栈元素为调用序列的最后一个调用。如下面的非嵌套调用,则倒序输出所有的异常。
    public class NeverCaught {
    	public static void main(String[] args) throws Exception {
            try 
               { function1();} 
            catch (Exception ex) {
    			System.out.println("getCause()----"+ex.getCause());//此处异常是由“exception from function2”引起
                System.out.println("getMessage()----"+ex.getMessage());
                System.out.println("toString()----"+ex.toString()+"
    "); 
                
                ex.printStackTrace();//打印调用栈的跟踪信息
                
                
                StackTraceElement[] ste=ex.getStackTrace();
                for(int i=0;i<ste.length;i++)
                	{System.out.print("
    "+ste[i].getMethodName()+":");//出现异常的方法名
                     System.out.println(ste[i].getLineNumber());//出现异常的所属类名
                     System.out.println(ste[i].getClassName());}//出现异常的行号
            }
    }
    static void function1() throws Exception {
            try {function2();} 
            catch (Exception ex) {
            	Exception C= new Exception("exception from function1",ex);
                throw C;}
    }
    
    static void function2() throws Exception{
    	throw new Exception("exception from function2");
    	

       输出:




    4.常见的异常处理机制

     4.1异常处理器及执行的语句

       如果try中语句抛出异常,而本函数没有catch处理,则将传递异常给调用此函数的方法,且本函数的其他代码将不会执行。如果上一级函数仍旧没有catch处理,则这一级函数的其他代码不会执行的同时,传递该异常给再上一级调用函数。即所谓的反向传播检查。如果至main函数仍旧未有处理异常,JVM会进行处理。
       一旦某一级处理器捕获到这个异常,则仅仅执行该函数catch的代码和try-catch结构之外的代码。调用该方法的上一级函数的try和try-catch之外的代码将被执行。
      例如:
          ①function3抛出3中异常Exception3,Exception2,Exception1之一
          ②调用关系main<--function1<---function2<-----function3
          ③main,function1(),function2()分别处理异常Exception3,Exception2,Exception1

    public class MyException {
    
    	public static void main(String[] args) throws Exception3, Exception2 {//处理异常 Exception1
            try{
            	function1(3);//此处定义1抛出异常1,定义2抛出异常2,定义3抛出异常3
            	/*statement1 code block*/
            	System.out.println("执行 statement1 code block");
            }
            catch(Exception1 ex1){
            	System.out.println("检测到 Exception1");
            }
        	/*statement2 code block*/
            System.out.println("执行 statement2 code block");
    	}
    	
    	public static void function1(int a) throws Exception3,Exception2,Exception1//处理异常 Exception2
    	{
            try{
            	function2(a);
            	/*statement3 code block*/
            	System.out.println("执行 statement3 code block");
            }
            catch(Exception2 ex2){
            	System.out.println("检测到 Exception2");
            }
        	/*statement4 code block*/
            System.out.println("执行 statement4 code block");
    	}
    	
    	public static void function2(int a) throws Exception3,Exception2,Exception1//处理异常 Exception3
    	{
            try{
            	function3(a);
            	/*statement5 code block*/
            	System.out.println("执行 statement5  code block");
            }
            catch(Exception3 ex3){
            	System.out.println("检测到 Exception3");
            }
        	/*statement6 code block*/
            System.out.println("执行 statement6 code block");
    	}
    	
    	public static void function3(int a) throws Exception3,Exception2,Exception1//抛出异常
    	{
          if(a==1)
        	  throw new Exception1("Exception1 appears");
          else if(a==2)
        	  throw new Exception2("Exception2 appears");
          else
        	  throw new Exception3("Exception3 appears");
    	}
    }
    
    class Exception1 extends Exception
    {
       Exception1(String str)
    	{super(str);}
    }
    class Exception2 extends Exception
    {
       Exception2(String str)
    	{super(str);}
    }
    class Exception3 extends Exception
    {
       Exception3(String str)
    	{super(str);}
    }

       倘若function3()抛出异常Exception1。输出
            检测到 Exception1
            执行 statement2 code block
       倘若function3()抛出异常Exception2。输出
            检测到 Exception2
            执行 statement4 code block
            执行 statement1 code block
            执行 statement2 code block
       倘若function3()抛出异常Exception3。输出
            检测到 Exception3
            执行 statement6 code block
            执行 statement3 code block
            执行 statement4 code block
            执行 statement1 code block
            执行 statement2 code block

      4.2 异常链的使用及异常丢失

        参考2中提到了异常处理的丢失情况。假设这样的场景:倘若某个异常B被捕捉到,且继而抛出其他异常C,则异常B的信息将不会出现在C的跟踪栈StackTrace上,不利于检查。因此使用从Throwable继承的initCause()方法,即异常链特性

        ❶先定义异常类ExceptionB,ExceptionC

    public class ExceptionB extends Exception {
        public ExceptionB(String str) {super(str);}
    }
     
    public class ExceptionC extends Exception<strong>B</strong> {
        public ExceptionC(String str) {super(str);}
    }
                  使用异常链

    public class NeverCaught {
        public static void main(String[] args) {//处理异常C
            try 
               { function1();} 
            catch (ExceptionC ex) {
                ex.printStackTrace();}
    }
    static void function1() throws ExceptionC {//处理异常ExceptionB,抛出异常ExceptionC
            try {function2();} 
            catch (ExceptionB ex) {
            	ExceptionC C= new ExceptionC("exception C");
            	//异常链
            	C.initCause(ex);//初始化引起C异常的异常
                    throw C;}
    }

          倘若没有使用异常链C.initCause(ex);则main()中的ex.printStackTrace()仅仅显示出现异常ExceptionC,而添加之后,ExceptionC,ExceptionB二者先后显示。

       ❷异常链同原始异常一起抛出新的异常,也称为链式异常。还有一种利用构造法,如:

    public class NeverCaught {
        public static void main(String[] args) throws Exception {
            try 
               { function1();} 
            catch (Exception ex) {
                ex.printStackTrace();}
    }
    static void function1() throws Exception {
            try {function2();} 
            catch (Exception ex) {
            	Exception C= new Exception("exception from function1",ex);//包装成新的异常
                throw C;}
    }
    
    static void function2() throws Exception{
    	throw new Exception("exception from function2");
    	}
    }

        第11行代码利用构造法包装新的异常。控制台中先显示"exception from function1"后显示原始异常"exception from function2",2次异常都被检测到。


     4.3异常转译

        所谓的异常转译就是将一种异常转换另一种新的异常,也许这种新的异常更能准确表达程序发生异常。在Java中有个概念就是异常原因,异常原因导致当前抛出异常的那个异常对象,几乎所有带异常原因的异常构造方法都使用Throwable类型做参数,这也就为异常的转译提供了直接的支持,因为任何形式的异常和错误都是Throwable的子类。

        比如将SQLException转换为另外一个新的异常DAOException,先自定义异常DAOException。

    public class DAOException extends RuntimeException {   
    /(省略了部分代码)   
      public DAOException(String message, Throwable cause) {   
          super(message, cause);   
      }  
        有一个SQLException类型的异常对象sqle,要转换为DAOException,可以这么写

    DAOException daoEx = new DAOException ( "SQL异常", sqle); 

             异常转译是针对所有继承Throwable超类的类而言的,从编程的语法角度讲,其子类之间都可以相互转换。但是,从合理性和系统设计角度考虑,可将异常分为三类:Error、Exception、RuntimeException。参考1认为,合理的转译关系图应该如图:

        参考1的作者认为:对于一个应用系统来说, 系统所发生的任何异常或者错误对操作用户来说都是系统"运行时"异常,都是这个应用系统内部的异常。这也是异常转译和应用系统异常框架设计的指导原则。在系统中大量处理非检查异常的负面影响很多, 最重要的一个方面就是代码可读性降低,程序编写复杂,异常处理的代码也很苍白无力。 因此,很有必要将这些检查异常Exception和错误Error转换为RuntimeException异常让程序员根据情况来决定是否捕获和处理所发生的异常。 

        图中的三条线标识转换的方向,分三种情况: 
                  ①:Error到Exception:将错误转换为异常,并继续抛出。例如Spring WEB框架中将org.springframework.web.servlet.DispatcherServlet的doDispatch()方法中, 将捕获的错误转译为一个NestedServletException异常。这样做的目的是为了最大限度挽回因错误发生带来的负面影响。 因为一个Error常常是很严重的错误,可能会引起系统挂起。 
                  ②:Exception到RuntimeException:将必检异常转换为RuntimeException可以让程序代码变得更优雅, 让开发人员集中经理设计更合理的程序代码,反过来也增加了系统发生异常的可能性。 
                  ③:Error到RuntimeException:目的还是一样的。把所有的异常和错误转译为免检异常, 这样可以让代码更为简洁,还有利于对错误和异常信息的统一处理。 

     4.4 finally子句和文件读写清理

        不论异常是否出现或者捕获与否,finally块子句都会被执行。甚至在finally之前出现return语句,它也会被执行。当有除内存之外的资源回复初始状态时使用该子句。如已经打开的文件等。例如修改4.1中的代码如下:

    public class ExceptionList {
    
    	public static void main(String[] args) throws Exception3, Exception2 {//处理异常 Exception1
            .............
            finally {System.out.println("执行 statement2 code block");}
    	}
    	
    	public static void function1(int a) throws Exception3,Exception2,Exception1//处理异常 Exception2
    	{
             ............
        	/*statement4 code block*/
            finally {System.out.println("执行 statement4 code block");}
    	}
    	
    	public static void function2(int a) throws Exception3,Exception2,Exception1//处理异常 Exception3
    	{
             ............
        	/*statement6 code block*/
            finally {System.out.println("执行 statement6 code block");}
    	}
    	
    	public static void function3(int a) throws Exception3,Exception2,Exception1//抛出异常
    	{...........}
    }

      倘若function3()抛出Exception1。输出:
         执行 statement6 code block
         执行 statement4 code block
         检测到 Exception1
         执行 statement2 code block

           finally子句常见于I/O程序设计。为了确保文件在任何情况下得到关闭,建议在其中放入文件关闭语句。参考2中提到:Try...finally结构是保证资源正确关闭的一个手段。如果你不清楚代码执行过程中会发生什么异常情况会导致资源不能得到清理,那么你就用try对这段"可疑"代码进行包装,然后在finally中进行资源的清理。 

        例如始代码如下

    public void readFile() {
            BufferedReader reader = null;
            try {
                reader = new BufferedReader(new InputStreamReader(
                        new FileInputStream("file")));
                // do some other work
             
                //close reader
                reader.close();
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            } 
        }
            因为程序会在异常出现的地方跳出,故而后面的代码将不能执行。所以以上虽然及早的关闭reader,但是reader.close()以前异常随时可能发生,因此不能预防任何异常的出现。这时我们就可以用try...finally来改造成为:

    public void readFile() {
            BufferedReader reader = null;
            try {
                try {
                    reader = new BufferedReader(new InputStreamReader(
                            new FileInputStream("file")));
                    // do some other work
     
                    // close reader
                } finally {
                    reader.close();
                }
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

          异常缺失:finally子句会导致异常丢失。例如,在try.....finally模型中:finally中也抛出的新异常会是try中抛出的异常被取代。又例如,finally中出现retrun语句会导致导致try中抛出的异常被屏蔽

     4.5多个catch块和catch排列规则

        单个方法的catch检查中,从上到下检查各个catch块。若某个catch块父类异常的,则也能catch子类异常。因此应该保证子类catch块出现在父类catch块之前。否则出现编译错误

    public class Sequence {
    	public static void main(String[] args){
    		try{method(2);}
    		catch(ExceptionB b)
    		  {System.out.println("getMessage()----"+b.getMessage());}
    		catch(ExceptionA a)
    		  {System.out.println("getMessage()----"+a.getMessage());}
    	}
    public static void method(int a) throws ExceptionA//即抛出父类异常又抛出子类异常,声明抛出父类异常即可
        {
    	  if(a==1)
        	    throw new ExceptionA("ExceptionA appears");
    	  else
    	    throw new ExceptionB("ExceptionB appears");
        }
    }
    class ExceptionA extends Exception {
        public ExceptionA(String str) {super(str);}
    }
     
    class ExceptionB extends ExceptionA {
        public ExceptionB(String str) {super(str);}
    }

             如上代码。子类catch块在前,父类catch块在后。父类catch可以捕获子类异常处理。同样,倘若某个函数即抛出父类异常又抛出子类异常,声明抛出父类异常即可。


       4.6异常的限制

       ❶对于某一类。不同参数的构造函数可以抛出不同的异常。但是必须声明抛出基类异常。

       ❷基类函数未声明抛出异常,则子类重载函数不能声明抛出。

       ❷单若基类抛出异常,则可以重载不抛出异常或者抛出继承于基类异常之异常的函数。

       ❹接口同名函数不能改变基类已经存在的函数的异常类型。

       简言之:在继承和覆盖中,“异常说明”的接口不是变大而是变小了。例如出现在基类的异常可以不一定出现在子类。这样从侧面说明子类的功能更加细化。


     4.7构造器与异常

       使用构造器时,如打开文件。应使用规则:在最外层,使用try...catch块。try中创建成功后,立即进入再一级try..finally(或try...catch--finally)块,在finally语句中再使用释放功能。创建失败,由外层catch块处理。


    5.总结

     5.1何时使用异常

       ❶try包含正常情况执行的代码,catch包含异常情况下执行的代码。异常处理将错误处理代码从正常的程序设计分离出来,因此更易读和修改。但是由于异常处理需要初始化异常对象,需要从调用栈返回,而且还要沿方法调用链传播异常以便找到其他异常处理器,因此需要很多医院和时间。

       ❷尽量在发生异常的地方处理异常。共同的异常应当考虑将其包装成同一异常类。

       ❸异常是程序设计的一部分,不要为了使用异常而使用异常。特别是不要将异常处理当简单的逻辑测试使用,例如NullPointException某种情况下可以直接使用if-else判断。


     5.2异常处理的原则

       ❶能处理就早处理,抛出不去还不能处理的就想法消化掉或者转换为RuntimeException处理。 因为对于一个应用系统来说,抛出大量异常是有问题的,应该从程序开发角度尽可能的控制异常发生的可能。
       ❷对于检查异常,如果不能行之有效的处理,还不如转换为RuntimeException抛出。这样也让上层的代码有选择的余地――可处理也可不处理。  异常可以传播,也可以相互转译,但应该根据需要选择合理的异常转译的方向
       ❸对于一个应用系统来说,应该有自己的一套异常处理框架,这样当异常发生时,也能得到统一的处理风格,将优雅的异常信息反馈给用户。


     5.3异常的误用

       参考2中提到:

       ❶避免用一个Exception(Throwable)来捕捉所有的异常,

       ❷异常是程序处理意外情况的机制,当程序发生意外时,我们需要尽可能多的得到意外的信息,包括发生的位置,描述,原因等等。这些都是我们解决问题的线索。但是上面的例子都只是简单的printStackTrace()。如果我们自己写代码,就要尽可能多的对这个异常进行描述。比如说为什么会出现这个异常,什么情况下会发生这个异常。如果传入方法的参数不正确,告知什么样的参数是合法的参数,或者给出一个sample。

       ❸将try block写的简短,不要所有的东西都扔在这里,我们尽可能的分析出到底哪几行程序可能出现异常,只是对可能出现异常的代码进行try。尽量为每一个不同的异常写一个try...catch,避免异常丢失。在IO操作中,一个IOException也具有"一夫当关万夫莫开"的气魄。


      5.4 其它

           ❶在捕获某种异常后,直接抛出另外一类异常。则垃圾回收机制会将栈中的异常对象清理干净,printStackTrace()显示从异常由此抛出。见《Think in Java》p260 

           ❷相较于C和C++,异常能够容许程序在发生错误后强制停止并且换一条路径走,并告诉我们出了什么问题增加程序的稳健性,C则不行。

           ❸new在堆上创建异常对象,并返回该对象的引用。当前路径终止并在开始查找异常处理程序。

           ❹可以声明某个方法抛出异常,但实际不抛出。定义抽象基类和接口时往往需要实现抛出这些预声明的异常。

           ❺C++无finally子句,依赖析构函数达到同样的目的。

           ❻可以直接在main函数中声明抛出异常,但不做处理,异常信息自动传递给控制台。

           ❼可以使用RuntimeException(e)的形式包装必检异常,然后重新抛出。再使用.getCause()的方式取出。具体见《Thinking in Java》P280

           







    本文参考:
      1.Java异常体系结构

      2.浅谈java异常[Exception]

  • 相关阅读:
    VS2015生成64位dll文件
    gdb简单调试~core文件
    Ubuntu ENet 的下载和编译
    Android LIstView初次创建getview方法执行多次问题
    android ipc通信机制之之三,进程通讯方式。
    Android Stduio统计项目的代码行数
    Android开发遇到的坑(1):Java中List的安全删除问题
    Android Studio修改项目的包名
    类似IOS的滑动返回上一级,SwipeBackLayout-android的滑动返回类库
    [转]理解RESTful架构
  • 原文地址:https://www.cnblogs.com/engineerLF/p/5393078.html
Copyright © 2011-2022 走看看