龙空技术网

「Java基础」详解Java中异常体系

猿学堂 129

前言:

眼前我们对“java的错误代码”都比较讲究,看官们都想要分析一些“java的错误代码”的相关内容。那么小编同时在网摘上收集了一些有关“java的错误代码””的相关文章,希望我们能喜欢,朋友们快快来了解一下吧!

如果看一门编程语言是否健壮,那么是否提供异常处理机制就是很重要的判断标准之一,除了传统的C语言以外,目前主流的编程语言都提供了成熟的异常机制,例如C#,Python等。异常机制可以使程序中的异常处理代码和业务代码分离,保证程序更加的优雅,提高程序的健壮性。

Java 的异常机制主要依赖干 try、catch、finally、throw 和 throws 五个关键字,其中 try 关键字后紧跟一个花括号扩起来的代码块(花括号不可省略),简称 try 块,它里面放置可能引发异常的代码。catch后对应异常类型和一个代码块,用于表明该 catch 块用于处理这种类型的代码块。多个 catch 块后还可以跟一个 finally 块,finally 块用于回收在 try 块里打开的物理资源,异常机制会保证 finally 块总被执行。 throws 关键字主要在方法签名中使用,用于声明该方法可能抛出的异常;而 throw 用于抛出—个实际的异常,throw 可以单独作为语句使用,抛出一个具体的异常对象。

Java 7 进一步增强了异常处理机制的功能,包括带资源的 try 语句、捕获多异常的 catch 两个新功能,这两个功能可以极好地简化异常处理。开发者希望所有的错误都能在编译阶段被发现,就是在试图运行程序之前排除所有错误,但这是不现实的,余下的问题必须在运行期间得到解决。Java将异常分为两种,Checked 异常和 Runtime 异常, Java 认为 Checked 异常都是可以在编译阶段被处理的异常,所以它强制程序运行前处理所有的 Checked 异常;而 Runtime 异常则无须处理。

异常类的继承体系

Java提供了丰富的异常类,这些类有着严格的继承关系,如下图:

从图示中可以看出,Java把所有的非正常情况分为两大类:异常(Exception)和错误(Error),他们都是Throwable的子类。

Error:和虚拟机有关的问题,如系统崩溃、虚拟机错误等,这些错误无法恢复或者不可能被抓取,将导致应用程序中断,应用程序通常无法处理这些错误,因此不应该用异常处理方法来处理Error。同样,也不能在throws字句中声明该方法可能抛出Error。Exception:程序中出现的非正常情况,其中异常又分为两类,RuntimeException(运行时异常)和CheckedException(检查时异常),这两类异常都是Exception的子类,其中RuntimeException及其子类不需要开发者显示处理。如果一个Exception没有继承RuntimeException类,则属于CheckedException,这类异常通常需要开发者显示处理。否则会出现编译错误。

下面,先通过示例来演示运行时异常和检查时异常的区别

package cn.bytecollege;public class Student {	/**	 * 定义方法,该方法抛出算术异常	 * 算术异常是运行时异常	 * @throws ArithmeticException	 */	public void say() throws ArithmeticException{			}	/**	 * 定义方法,该方法抛出检查时异常	 * 检查时异常要求开发者必须显示处理	 * @throws ClassNotFoundException	 */	public void hello() throws ClassNotFoundException{			}		public static void main(String[] args) {				Student s = new Student();		//因为该方法抛出的是运行时异常,即使不处理也不会发生编译错误		s.say();		//该方法抛出的是检查时异常,要求开发者显示处理		//如果不处理则编译错误		s.hello();	}}

在上面的示例中,定义了Student类以及两个实例方法:

其中say()方法抛出了一个ArithmeticException,因为ArithmeticException继承了RuntimeException,所以它是一个运行时异常,因此,在上述代码第25行调用该方法时,即使没有显式的处理该异常,程序也不会出现编译错误。

hello()方法抛出了ClassNotFoundException,ClassNotFoundException没有继承RuntimeException,因此是一个检查时异常,而检查时异常需要开发者显式处理,如果不处理,则会出现编译错误。

异常处理机制

对于计算机程序而言,没有人能保证自己写的程序永远不会出错!就算程序没有错误,你能保证用户总是按你的意愿来输入? 就算用户都是非常"聪明而且配合"的,你能保证运行该程序的操作系统永远稳定?你能保证运行该程序的硬件不会突然坏掉?你能保证网络永远通畅?

对于一个程序设计人员,需要尽可能地预知所有可能发生的情况,尽可能地保证程序在所有糟糕的情形下都可以运行。也就是说要时时刻刻考虑程序的健壮性。

Java 的异常处理机制可以让程序具有极好的容错性,让程序更加健壮。当程序运行出现意外情形时,系统会自动生成一个 Exception 对象来通知程序,从而实现将"业务功能实现代码"和"错误处理代码"分离,提供更好的可读性。

try...catch捕获异常

正如前面示例中编写的代码,在代码28行如果不做任何处理,将会出现编译错误,代码也就无法执行。为了解决这个问题,可以使用try...catch捕获异常,并对异常进行处理,如果执行try语句块中的代码出现异常,系统会自动生成一个异常对象,该对象被提交给Java运行时环境,这个过程被称为throw(抛出)异常。

当Java运行时环境受到异常对象时,会寻找对应处理该异常对象的catch块,如果找到合适的catch块,则把该异常对象交给catch块处理,这个过程叫做catch(捕获)异常;如果Java运行时环境找不到捕获异常的catch块,则运行时环境终止,Java程序也将退出。

下面将演示上例中代码中异常的处理方式:

public static void main(String[] args) {				Student s = new Student();		//因为该方法抛出的是运行时异常,即使不处理也不会发生编译错误		s.say();		//该方法抛出的是检查时异常,要求开发者显示处理		//如果不处理则编译错误		try {			s.hello();		} catch (ClassNotFoundException e) {			e.printStackTrace();		}	}

从上面的程序可以看出,将第9行可能抛出异常的代码放入了try语句块中,当这一行代码发生异常时,就会到后续的catch语句块中寻找与之对应的异常,如果找到则进入catch语句块中继续执行。避免了程序的中断。

接下来,通过示例来学习当代码发生异常时如果catch中有对应的异常和没有对应的异常时发生的情况。

package cn.bytecollege;import java.lang.reflect.Field;public class ReflectDemo {	public static void main(String[] args) {		//创建Student对象		Student s = new Student();		Class clazz = s.getClass();		try {			Field field = clazz.getDeclaredField("name");		} catch (NoSuchFieldException e) {			e.printStackTrace();			System.out.println("未找到该属性");		} 	}}

在上面的程序中,使用了反射获取Student中的name变量,因为在Student中并没有定义name变量,所有当代码运行至第9行时,一定会发生NoSuchFieldException,并且在try后的catch中就存在与之对应的异常处理语句。程序将进入对应的catch语句块中,并向下执行。运行结果如下图所示:

如果程序发生异常时在catch语句中没有找到对应的异常,那么程序将终止运行,通过下面的示例来演示这种情况。

package cn.bytecollege.ExceptionDemo;import java.lang.reflect.Field;public class ReflectDemo {        public static void main(String[] args) {            //创建Student对象            Student s = new Student();            Class clazz = s.getClass();            try {                //此时会出现编译错误,提示必须对NoSuchFieldException进行捕获或者声明以便抛出                Field field = clazz.getDeclaredField("name");            } catch (NullPointerException e) {                e.printStackTrace();                System.out.println("未找到该属性");            }    }}
Java 7提供的多异常捕获

在Java 7以前,每个catch块只能捕获一种类型的异常;但是从Java 7开始,一个catch块可以捕获多种类型的异常。

使用catch块捕获多种类型异常时需要注意以下两点:

捕获多种类型异常时,多种异常类型之间使用 | 隔开捕获多种类型异常时,异常变量有隐式的final修饰,因此程序不能对异常变量重新赋值

我们重构上一小节的程序:

package cn.bytecollege;import java.lang.reflect.Field;public class ReflectDemo {	public static void main(String[] args) {		//创建Student对象		Student s = new Student();		Class clazz = s.getClass();		try {			Field field = clazz.getDeclaredField("name");		} catch (NoSuchFieldException | SecurityException e) {			//因为e是隐式final修饰的,所以不能重新赋值//			e = new ArithmeticException();			e.printStackTrace();		}	}}

通过重构代码可以发现在一个catch中捕获了NoSuchFieldException和SecurityException两个异常,在代码12行重新对e赋值发现编译出错,这是因为变量e是使用隐式的final修饰的,final修饰的变量初始化后不能被重新赋值。所以会编译错误。

使用finally回收资源

在开发中经常会打开一些资源,例如数据库连接,网络连接和磁盘文件等,这些资源在打开后都必须要显式回收。并且这些资源必须要进行回收,否则会持续占用内存资源。而且不会释放。

为了保证资源一定能被回收,异常处理机制提供了finally块,不管try块中的代码是否出现异常,也不管哪个catch语句块被执行,甚至在try块或者catch块中执行了return语句,finally中的代码总会被执行。其语法结构如下:

try{    //代码}catch(Exception e1){ 	//代码   }catch(Exception e2){ 	//代码   }finally{ 	//资源回收   }

异常处理语法结构中只有 try 块是必需的,也就是说,如果没有 try 块,则不能有后面的 catch 块和 finally 块;catch 块和 finally 块都是可选的,但 catch 块和 finally 块至少出现其中之一,也可以同时出现;可以有多个 catch 块,捕获父类异常的 catch 块必须位于捕获子类异常的后面;但不能只有 try 块,既没有 catch 块,也没有 finally 块;多个 catch 块必须位于 try 块之后,finally 块必须位于所有的 catch块之后。并且finally语句块不能单独出现,只能和try...catch搭配使用。

下面,通过示例来了解finally的作用:

package cn.bytecollege;/** * 本例将演示finally语句块一定会执行 * @author MR.W */public class FinallyDemo {	public static void main(String[] args) {		try {			//此处会抛出ArithmeticException			System.out.println("try");			int i = 5/0;		}catch (ArithmeticException e) {			//catch语句块捕获了该异常			System.out.println("catch");			e.printStackTrace();		}finally {			//进入finally语句块			System.out.println("finally");		}	}}

在上面的程序中第10行因为用整数除0,所以会抛出ArithmeticException,在try后的catch语句块中可以捕获该异常,并执行catch中的代码,catch后还有finally语句块,finally语句块的特点是不管是否会抛出异常,finally中的语句都会执行,所以程序结果运行如下图:

修改上例中的代码,即使try语句块中的代码不抛出异常,finally中的代码也会执行。

package cn.bytecollege;/** * 本例将演示finally语句块一定会执行 * @author MR.W */public class FinallyDemo {	public static void main(String[] args) {		try {			System.out.println("try");			int i = 5/1;		}catch (ArithmeticException e) {			//catch语句块捕获了该异常			System.out.println("catch");			e.printStackTrace();		}finally {			//进入finally语句块			System.out.println("finally");		}	}}

上面的代码中try语句块中被不会抛出异常,所以代码不会进入catch语句块中,但是仍然会进入finally语句块中并执行代码,结果如下图:

除此以外Java还提供了增强版的try-with-resource语法,这节内容将在下一章中详细讲解。

使用throws声明抛出异常

throws用于方法头中抛出异常,当定义一个方法并且该方法不知道如何处理这种类型的异常时,该异常交由调用者处理;如果main方法也不知道如何处理这种类型的异常,也可以使用throws声明抛出异常,该异常将交有JVM处理。JVM对异常的处理方法是,打印异常的跟踪栈信息,并终止程序的运行。

throws声明抛出异常只能在方法签名中使用,throws可以声明抛出一个异常类,多个异常类之间用逗号隔开。语法格式如下:

throws Exception1,Exception2...

如果某段代码中调用了一个带throws声明的方法,该方法抛出了Checked异常,则表明该方法要求调用者来处理该异常。也就是说方法的调用者要么在try语句块中显示捕获该异常,要么放在另一个带throws声明抛出的方法中。下面的示例将演示这种用法。

package cn.bytecollege;import java.sql.SQLException;public class Teacher {		public void say() throws ClassNotFoundException,SQLException{			}	public static void main(String[] args) {		Teacher t = new Teacher();				try {			t.say();		} catch (ClassNotFoundException e) {			e.printStackTrace();		} catch (SQLException e) {			e.printStackTrace();		}	}}

上面的程序中定义了Teacher类,在Teacher中定义了say()方法,并且该方法在方法头中使用throws抛出了ClassCastException,SQLException,在main方法中创建了Teacher对象t,并调用了say方法,因为ClassNotFoundException,SQLException都是Checked异常,所以调用say方法时,必须显示的处理这两个异常,在上例中使用了try...catch对异常进行了处理。当然也可以在调用say方法代码所在的方法头继续使用throws抛出异常,但是由于say方法是在main方法中调用的,也就是说如果继续throws异常的话,会抛给虚拟机,但是并不建议将异常抛给虚拟机。下面的程序将演示这种情况

package cn.bytecollege;import java.sql.SQLException;public class Teacher {	public void say() throws ClassCastException,SQLException{	}	public static void main(String[] args) throws ClassCastException, SQLException {		Teacher t = new Teacher();		t.say();	}}

上面的程序中第8行say方法抛出了异常,但是并没有处理,而是直接向上抛出,因为say方法是在main方法中调用的,如果继续抛出异常,main方法只能将异常抛给虚拟机。

使用throw抛出异常

当程序出现错误时,系统会自动抛出异常;除此之外,Java也允许程序自行抛出异常,自行抛出异常使用throw语句来完成(注意此处的throw没有后面的s,与前面声明抛出的throws是有区别的)。

抛出异常

很多时候,系统是否要抛出异常,可能需要根据应用的业务需求来决定,如果程序中的数据、执行与既定的业务需求不符,这就是一种异常。

由于与业务需求不符合产生的异常,必须由程序员来决定抛出,系统无法抛出这种异常。如果需要在程序中自行抛出异常,则应使用throw语句,throw语句可以单独使用,throw语句抛出的不是异常类,而是一个异常实例,而且每次只能抛出一个异常实例。throw语句的语法格式如下:

throw ExceptionInstance;

在开发中登录是常见的业务,当用户输入用户名和密码后,开发者需要对用户名进行校验其合法性,例如长度符合规定,如果不符合规定,就可以抛出一个异常。下面的程序将演示throw的用法:

package cn.bytecollege;import java.util.Scanner;public class StringValid {	public static void main(String[] args) {		Scanner sc = new Scanner(System.in);		String username = sc.next();		valid(username);	}	public static void valid(String username) {		int length = username.length();		if(length>12||length<6) {			throw new RuntimeException("用户名长度不合法");		}	}}

上面的程序中,在valid方法中,如果用户输入的用户名长度小于6或者大于12就使用throw在方法内抛出异常,也就是说当用户名长度小于6或者大于12程序认定这是异常。当Java运行时接收到用户自行抛出的异常时,同样会中止当前的执行流,跳到该异常对应的catch块,由该catch块来处理该异常。也就是说,不管是系统自动抛出的异常,还是程序员手动抛出的异常,Java运行时环境对异常的处理没有任何差别。

如果throw语句抛出的异常是Checked异常,则该throw语句要么处于try块里,显式捕获该异常,要么放在一个带throws声明抛出的方法中,即把该异常交给该方法的调用者处理;如果throw语句抛出的异常是Runtime异常,则该语句无须放在try块里,也无须放在带throws声明抛出的方法中;程序既可以显式使用try...catch来捕获并处理该异常,也可以完全不理会该异常,把该异常交给该方法调用者处理。例如下面例子程序。

package cn.bytecollege;import java.util.Scanner;public class StringValid2 {	public static void main(String[] args) {		Scanner sc = new Scanner(System.in);		String username = sc.next();		try {			valid(username);		} catch (Exception e) {			e.printStackTrace();		}	}	public static void valid(String username) throws Exception {		int length = username.length();		if(length>12||length<6) {			throw new Exception("用户名长度不合法");		}	}}

在上面的程序中修改了valid方法,当用户输入的用户名长度不合法时,使用throw抛出了一个Checked异常,因此在方法头上需要使用throws声明抛出异常(当然,也可以在valid方法中使用try...catch处理,但是这么做没有意义,相当于自己的异常自己处理了,那么调用者也不清楚输入的数据是否合法)。当该方法抛出Checked异常后,也要求方法的调用者显示的处理,所以在main方法中显示了处理了异常。

自定义异常类

在上一小节学习了如何使用throw在方法内抛出异常,但是在程序开发选择抛出异常时,应该选择合适的异常类,从而可以明确地描述该异常情况。在这种情形下,应用程序常常需要抛出自定义异常。用户自定义异常都应该继承Exception基类,如果希望自定义Runtime异常,则应该继承RuntimeException基类。定义异常类时通常需要提供两个构造器:一个是无参数的构造器;另一个是带一个字符串参数的构造器,这个字符串将作为该异常对象的描述信息(也就是异常对象的getMessage()方法的返回值)。例如上一小节的示例,直接使用了RuntimeException或者Exception,这样并不能很清晰的描述异常信息,因此,可以自定义一个异常,专门描述校验用户名过程中发生的异常。

package cn.bytecollege;/** * 本例将演示自定义异常 * @author MR.W * */public class UsernameException extends Exception{	public UsernameException() {		super();	}	public UsernameException(String msg){		super(msg);	}}

上面程序创建了AuctionException异常类,并为该异常类提供了两个构造器。尤其是第11行代码部分创建的带一个字符串参数的构造器,其执行体也非常简单,仅通过super来调用父类的构造器,正是这行super调用可以将此字符串参数传给异常对象的message属性,该message属性就是该异常对象的详细描述信息。

如果需要自定义Runtime异常,只需将AuctionException.java程序中的Exception基类改为RuntimeException基类,其他地方无须修改。

异常转义

前面介绍的异常处理方式有如下两种。

在出现异常的方法内捕获并处理异常,该方法的调用者将不能再次捕获该异常。该方法签名中声明抛出该异常,将该异常完全交给方法调用者处理。

在实际应用中往往需要更复杂的处理方式——当一个异常出现时,单靠某个方法无法完全处理该异常,必须由几个方法协作才可完全处理该异常。也就是说,在异常出现的当前方法中,程序只对异常进行部分处理,还有些处理需要在该方法的调用者中才能完成,所以应该再次抛出异常,让该方法的调用者也能捕获到异常。为了实现这种通过多个方法协作处理同一个异常的情形,可以在catch块中结合throw语句来完成。如下例子程序示范了这种catch和throw同时使用的方法。

package cn.bytecollege.ExceptionDemo;//自定义UsenameCheckedException异常类,继承Exception类public class UsenameCheckedException extends Exception{    public UsenameCheckedException (String msg){        //调用父类的构造方法        super(msg);    }}class User{    String username;    public String checked(String username)throws UsenameCheckedException{        if (username.equals("")){            throw new UsenameCheckedException("用户名不能为空");        }else {            return username;        }    }}class Demo{    public static void main(String[] args) {        User user = new User();        try{            user.checked("");        }catch (UsenameCheckedException e){            throw new RuntimeException("用户名不能为空");        }    }}
访问异常信息

如果程序需要在 catch 块中访问异常对象的相关信息,则可以通过访问 catch 块的后异常形参来获得。当 Java运行时决定调用某个 catch 块来处理该异常对象时,会将异常对象赋给 catch 块后的异常参数,程序即可通过该参数来获得异常的相关信息。所有的异常对象都包含了如下几个常用方法。

getMessage()∶ 返回该异常的详细描述字符串。printStackTrace()∶将该异常的跟踪栈信息输出到标准错误输出。getStackTrace()∶ 返回该异常的跟踪栈信息。下面例子程序演示了程序如何访问异常信息。

package cn.bytecollege.ExceptionDemo;public class ExceptionMethod {    public static void main(String[] args) {        try{            int i = 5/0;        } catch (ArithmeticException e) {            //将该异常的跟踪栈信息输出到标准错误输出            e.printStackTrace();            //返回该异常的详细描述字符串            e.getMessage();            //返回该异常的跟踪栈信息            e.getStackTrace();        }    }}

上面程序调用了 Exception 对象的 getMessage()方法来得到异常对象的详细信息,也使用了 printStackTrace()方法来打印该异常的跟踪信息。运行上面程序,会看到下图所示的界面。

标签: #java的错误代码