Java异常

异常概述   

  异常会被包装成对象,这些对象都是可抛出的,java.lang.Throwable类下有Error和Exception两个子类,Error代表严重系统错误,如JVM错误、内存不足等,这类错误我们基本上不用处理,应当任其传至JVM,因为虽然也可以使用 try/catch来处理Error但对这种错误我们也是无能为力,最多是留下日志信息。Exception就是Java异常类,其子类有RuntimeException、IOException、SQLException,其中RuntimeException及其子类称为执行时期异常,又称Unchecked Exception非受检异常,其它异常统称为受检异常(Checked异常)。

    Java认为Checked异常都是可以在编译阶段被处理的异常,所以它强制程序处理所有的Checked异常。比如如下所示的read()方法必须在try/catch中,或者其所在的方法使用throws进行声明(表明该方法中的异常交由上级处理),否则编译不通过。对于Unchecked异常,如果当前方法没有对其进行try/catch捕获,或者当前方法在try/catch中又对异常进行了throw,就会将异常抛给上级调用,如果上级调用都没有处理的话那么JVM会输出异常的信息并结束程序,如下所示。

    public static void main(String[] args){
        try{
            int ch = System.in.read(); //read()会抛出受检异常
        }
        catch(java.io.IOException ex)
        {
            System.out.println(ex.getMessage()); //输出异常消息
            ex.printStackTrace(); //输出栈信息
        }
     }


  	public static void main(String[] args) throws Exception
	{	
		int ch = System.in.read();
	}

  

public static void main(String[] args)
{	
	try {
		func();
	}catch(Exception ex) {
		System.out.println("case Exception");
	}
}

static void func() 
{
	int i = 1/0; //产生非受检异常
}

   RuntimeException的子类有NullPointerException(空指针异常)、IndexOutOfBoundsException(数组下标越界异常)、ClassCastException(类型转换异常)、NumberFormatException(算数异常)、ArithmeticException(数字格式异常)等。IOException是IO异常,SQLException是数据库异常。如果throw抛出的是Checked异常,则该throw语句要么处于try块里(显示捕获该异常),要么处于带throws声明的方法中(表明该异常由上级处理)。

public class Main {
    public static void func()throws java.io.IOException
    {
        int ch = System.in.read();
    }
    public static void main(String[] args){
       try{
           func();
       }catch(java.io.IOException ex)
       {
           System.out.println(ex.getMessage()); //输出异常消息
           ex.printStackTrace(); //输出栈信息
       }
     }
}

    用throws声明的方法表示该方法内抛出的异常类型交由调用者处理,即方法内不会拦截该异常。如果当前函数内调用了一个带throws声明的方法,要么将其放至try-catch中,即在当前函数中处理该方法抛出的异常,要么将当前函数也添加throws声明,即将该函数的异常再抛出交由上级处理。当main函数带throws声明的时候表明产生的异常交由JVM处理,而JVM的做法是打印异常的跟踪栈信息并结束程序。在代码中添加了try-catch捕获异常机制后,当try中代码产生异常后会在catch中进行捕获后的处理,而不再交给上级处理(除非捕获处理中再次使用throw抛出异常),这样就避免了异常产生的程序退出。throws也可以处理多个异常,比如:

import java.sql.SQLException;

public class Main {
        public static void main(String[] args)throws ClassNotFoundException, SQLException
        {

        }
}

Runtime异常的处理   

  对于产生Runtime异常的处理,一般是修改程序,如果真要以try、catch处理,建议是日志或呈现友好信息。比如下面的使用Scanner获得用户输入的数字,当用户输入非数字的字符后会产生InputMismatchException异常,而推荐的做法是取得用户的输入后主动检查是否为数字:

import java.util.*;
public class Test {

	public static void main(String[] args) {
		Scanner console = new Scanner(System.in);
		while(true)
		{
			try {
				int number = console.nextInt();
				//ToTo
			}
			catch(InputMismatchException ex) {
				System.out.printf("%s为非数字!", console.next());
			}
		}
	}
}

import java.util.*;
public class Test {

	public static void main(String[] args) {
		Scanner console = new Scanner(System.in);
		while(true)
		{
			/*try {
				int number = console.nextInt();
				//ToTo
			}
			catch(InputMismatchException ex) {
				System.out.printf("%s为非数字!", console.next());
			}*/
			
			String input = console.next();
			if(!input.matches("\\d*"))
			{
				System.out.println("请输入数字");
			}
			else
			{
				int number = Integer.parseInt(input);
				//ToDo
			}
		}
	}
}

自动关闭资源的try语句   

  为了防止产生异常后未关闭相关资源,可以在try块后再加一个finally块,不管是否出现异常都会执行finally块里的代码,除非调用了退出虚拟机的方法,如System.exit(1)。如果是正常结束的话finally块会先于try中的return语句执行,如果是发生异常的话会先执行catch块中代码,然后执行finally块中内容,而且此时的异常 默认不会再抛出。示例如下:

        java.util.Scanner sc = null;
        try{
            sc = new java.util.Scanner(new FileInputStream("name"));
            ......
        }
        catch(Exception e){
            ......
        }
        finally
        {
            if(sc != null)
                sc.close();
        }

  finally中也可以包含try语句:

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;

public class Main {
        public static void main(String[] args)
        {
            Connection conn = null;
            SQLException ex = null;

            try{
                conn = DriverManager.getConnection("jdbcUrl");
                
                //......
            }catch (SQLException e){
               ex = e;
            }

            finally {
                if(conn != null){
                    try{
                        conn.close();
                    }catch (SQLException e){
                        if(ex == null){
                            ex = e;
                        }else{
                            ex.addSuppressed(e); //添加新的异常信息
                        }
                    }
                }

                if(ex != null) {
                    throw new RuntimeException(ex);
                }
            }
        }
}

  可以在try后跟一个括号,在括号内进行资源的声明或定义(在try块中可以直接使用该资源),该资源类应该是实现了Closeable或AutoCloseable接口(实现其close()方法),这样try语句结束时自动关闭这些资源(文件IO类、Connection、Statement等几乎所有的资源类都实现了Closeable或AutoCloseable接口,我们还可以将自定义的类实现Closeable或AutoCloseable接口来自动关闭我们自定义的资源)。自动关闭资源的try语句相当于隐式包含了finally块。需要注意的一点,在自动关闭资源的try块内越是后面定义的资源会越早被关闭。

        try(java.util.Scanner sc = new java.util.Scanner(new FileInputStream("name"))){
           ......
        }

         //JDK9的话只要变量是final,可以不在括号内定义变量对象,如
        java.util.Scanner sc = new java.util.Scanner(new FileInputStream("name"));
        try(sc){
            ......
        }

	static void dump(InputStream src, OutputStream dest)
	{
		try(src; dest){
			......
		}
	}

需要注意的地方    

    子类重写父类的方法时抛出的异常应该与父类方法相同或是其子类或不抛出异常。

    如果父类异常对象在子类异常对象前被捕获,则子类异常对象的catch块不再会被执行。

    异常处理机制中应该将不可预期异常的处理代码与正常的业务逻辑代码分离。

    可以自定义异常类,通过继承Throwable或Error或Exception(及其子类),通常建议是继承自Exception或其子类,如果不是继承自RuntimeException或Error,那么就是受检异常。随着应用程序的演化,异常也可以考虑演化,如Hibernate 2中的HibernateException是受检异常,而Hibernate 3中变成了非受检异常。

package xu;
import java.util.*;

class MyException extends Exception //可以自定义异常,然后抛出
{
	public MyException(){}
	public MyException(String msg)
	{ 
		super(msg); //传入的字符串msg是getMessage()的返回值 
	} 
	public MyException(Throwable cause)
	{
		super(cause);
	}
}

public class Test
{
	public static void func1()
	{
		try //try块后必须跟一个catch或finally块
		{
			int s = 3 / 0;
		}
		catch(IndexOutOfBoundsException ie) //数组越界
		{
			Throwable t = ie.fillInStackTrace(); //不使用原异常栈信息,将异常出错的栈信息设置为当前位置
            throw (IndexOutOfBoundsException)t;
		}
		catch(NumberFormatException | ArithmeticException ne) //数字格式异常、算数异常(捕获多种异常时异常对象为final类型,且不能有继承关系)
		{
			System.out.println(ne.getMessage()); //输出为 "/ by zero"
			ne.printStackTrace(); 
			//输出为:
			//java.lang.ArithmeticException: / by zer
			//at xu.Test.func1(Test.java:23)
			//at xu.Test.main(Test.java:77)
		}
		catch(NullPointerException ne) //空指针异常
		{
			
		}
		catch(ClassCastException ce) //类转换异常
		{
			
		}
		catch(Exception e) //这个异常处理应该在最后,否则所有异常都会在此catch后不再进入其它catch
		{
			//e.getMessage(); //获得异常信息
			//e.printStackTrace(); //输出异常的栈信息到标准错误输出
			//e.printStackTrace(/*PrintStream ps*/); //将异常的栈信息输出到指定输出流
			//StackTraceElement[] ary = e.getStackTrace(); //获得异常的栈信息,索引0为异常根源信息
		}
		finally
		{
			//可以在finally块中关闭打开的物理资源: 数据库连接、网络连接、磁盘文件等。
			//不要在finally块里使用return、throw等终止方法的语句,以避免覆盖try块中的return、throw语句
			
		}
	}
	public void func2()throws MyException
	{
		try
		{
			//业务逻辑
		}
		catch(NullPointerException ne)
		{
			//职责链模式(链式处理模式):
			//... //原始异常保存下来,留给管理员处理
			//throw new MyException("访问数据库异常"); //向上抛出新的异常,隐藏原始异常信息,提供新的异常信息
			
			throw new MyException(ne); //将原始异常传递给新的异常,通过新的异常也可以追踪到原始的异常信息
		}
		
	}
	public static void main(String[] args)
	{
		func1();
	}
}

堆栈追踪 

  调用异常对象的printStackTrace()输出堆栈追踪信息的时候,如下图所示,显示的最顶层的是异常的根源,printStackTrace()还可以接受PrintStream、PrintWriter的版本,可以将追踪的堆栈信息以指定方式输出至指定目的地,如文档。

  在编译位码文档的时候,默认会记录原始码行数等除错信息,以供printStackTrace()等使用(printStackTrace()会输出异常所在的行),可以在javac编译时候指定-g:none取消记录除错信息,这样编译出来的位码文件会小一点。

  也可以调用异常对象的getStackTrace()来获得堆栈追踪信息的StackTraceElement数组,数组中索引为0的项是异常的根源信息,可以通过StackTraceElement中的getMethodName()、getLineNumber()等方法获得当前堆栈信息,或者是使用JDK9中新增的getModuleName()等方法。

  使用throw重抛异常时,异常的追踪堆栈起点仍然是异常的发生根源,如果想要让异常堆栈起点为重抛异常的地方,可以调用异常的fillInStackTrace()重新装填异常堆栈,然后再调用throw抛出异常,如下所示:


			try {
				
			}catch(NullPointerException ex) {
				Throwable t = ex.fillInStackTrace();
				throw (NullPointerException)t;
			}

assert  

  还可以使用assert断言assert boolean_expression,boolean_expression为真的话断言成立,boolean_expression为假则断言不成立会发生java.lang.AssertionError。还可以添加断言不成立后要显示的字符串,如下所示,其中字符串还可以用对象替换,这时候会显示其toString()方法的文本。默认断言是不启动的,要启动的话在执行java时候指定-enableassertions或-ea。

assert n >= 0 : "n不能为负数!";

堆栈追踪

  除了在发生异常的时候进行堆栈追踪,也可以自行建立Throwable或获取Thread,通过getStackTrace()方法来获取堆栈追踪,每个线程都有自己的JVM Stack,每调用一个方法,JVM就会建立一个Stack Frame(存储局部变量、方法、类等信息)到JVM Stack,JVM Stack是先进后出,方法调用结束后Stack Frame就从JVM Stack中跳出销毁。

 还可以使用JDK9的StackWalker来进行堆栈追踪,使用它可以单独获得类名、方法名等,下面的代码因为调用了getCallerClass()、getDeclaringClass()获得类名,所以必须给StackWalker的getInstance()方法传入RETAIN_CLASS_REFERENCE:

如果只对某几个StackFrame感兴趣,那么可以调用StackWalker的walk()方法来进行过滤,如下所示,walk()接受一个Function,该Function接受一个Stream<StackWalker.StackFrame>,返回一个T类型对象,而在walk()中就是返回的该Function返回的T对象。

            StackWalker stackWalker = StackWalker.getInstance();
            Optional<StackWalker.StackFrame> frame = stackWalker.walk(frameSream->frameSream.findFirst()); /*只获得第一个StackFrame*/

            StackWalker stackWalker = StackWalker.getInstance();
            Stream<StackWalker.StackFrame> frame = stackWalker.walk(stackFrameStream ->
                    stackFrameStream.filter(item->item.getDeclaringClass().getName().equals("Main"))/*获得类名是""Main的StackFrame*/
            );

 默认情况下一些与反射相关的StackFrame是获取不到或者被隐藏的,如果想要显示反射相关的StackFrame,可以给StackWalker.getInstance()方法传入SHOW_REFLECT_FRAMES或SHOW_HIDDEN_FRAMES。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值