Java的基本理念是“结构不佳的代码不能运行” --- Java编程思想
最理想的是在编译时期就发现错误,但一些错误要在运行时才会暴露出来。对于这些错误咱们固然不能置之不理。对于错误而言的两个关键是发现和处理错误。Java提供了统一的异常机制来发现和处理错误。java
不考虑异常存在来看一下这个场景:编程
public void showObject(Object obj) { if (obj == null) { System.out.println("error obj is null"); } else { System.out.println(obj.toString()); } }
对于showObject来讲obj为null是一个错误,要在输出以前作错误判断,发生错误的话把错误打印出来做为错误报告和处理。这里把错误的发现、报告处理和正常业务逻辑放在了一块儿。但一些错误每每比这复杂且不仅一个,若是咱们为每个错误都去定义一个独特错误报告的形式且都将错误处理代码和正常业务代码牢牢的耦合在一块儿那咱们代码会变得难以维护。数组
合理的使用异常不只能使咱们的代码更加健壮,还能简化开发提高开发效率。spa
java使用使用异常机制来报告错误。异常也是普通的类类型。Java自身已经定义好了使用java时可能会产生的异常,使用java时java会自动去检查异常的发生,当异常发生时,java会自动建立异常对象的实例并将其抛出。咱们常常看到的NullPointerException即是java已经定义好的异常。code
除了java自身定义的异常外咱们能够自定义异常,但自定义的异常须要咱们本身去检查异常情形的发生,并本身建立异常对象和抛出。固然也能够建立java自定义的异常并抛出,抛出异常使用throw关键字:对象
throw new Exception();
咱们使用的第三方库大多封装了本身的异常,并在异常情形发生时将自定义异常经过throw抛出。全部的异常类型都继承自Throwable类,全部Throwable类型的对象均可被抛出。继承
异常发生,系统自动建立异常实例并抛出,或咱们本身建立异常实例抛出异常时,代码的正常执行流程将会被终止,转而去执行异常处理代码。内存
当异常抛出时,天然抛出的异常应该获得处理,这就须要将抛出的捕获异常。但一个异常类型可能在不少地方被抛出,那么怎么去对特定的地方编写特定的异常处理程序那?java采用一个最方便和合理的方式,即对可能产生异常的代码区域进行监控,并在该区域后添加处理程序。资源
监控的代码区域放在try{}中,而异常处理代码紧跟在try后的catch中:开发
try { /***/ } catch (ExceptionType e) { /* *** */ }
catch相似方法的申明括号中为异常的类型和该类类型的实例。代表当前catch块处理的是什么类型的异常,而e即是该异常抛出的实例对象。
当try内的代码抛出异常时,就会中止当前流程,去匹配第一个catch中申明的异常类型与抛出类型相同的catch,若是匹配到则执行其内代码。一个try中可能会抛出多种类型的异常,能够用多个catch去匹配。
注意catch中声明的异常若是为当前抛出异常的父类型也能够匹配。因此通常将基类的异常类型放在后面。
由于全部能够进行捕获的异常都继承自Exception,全部能够catch中申明Exception类型的异常来捕获全部异常,但最后将其放在最后防止将其余异常拦截了。
先看一下异常的类层次结构图:
咱们能够将异常分为检查和非检查两种类型:
catch中的语句执行完成后会继续执行try-catch后的其余语句。因此当try-catch后还有语句时,必定要保证但异常发生时在catch中已经对异常进行了正确处理,后面的代码能够获得正常的运行,若是不能保证则应该终止代码向后的执行或再次抛出异常。
一些异常在当前的方法中不须要或没法进行处理时,能够将其抛出到上一层。要在方法中将异常抛出须要在方法中对要抛出的异常进行声明,这样方法的调用者才能知道哪些异常可能会抛出,从而在调用方法时添加异常处理代码。
非检查异常抛出到上一级时能够不用进行声明,合理的使用非检查异常能够简化代码。
在方法声明的参数列表以后使用throws进行异常声明,多个异常类型使用逗号隔开:
void t () thrwos ExcptionTyep1, ExceptionType2 { }
在方法中声明了的异常在方法中能够不进行捕获,直接被抛出到上一级。异常声明父类异常类型能够匹配子类异常类型,这样当有多个子类异常抛出时,只用声明一个父类异常便可,子类异常将被自动转换为父类型。
要建立本身的异常必须得继承自其它的异常,通常继承Exception建立检测异常,继承RumtimeException建立非检查异常。
通常状况下异常提供了默认构造器和一个接受String参数的构造器。对于通常自定义的异常来讲,只须要实现这两个构造方法就足够了,由于定义异常来讲最有意义的是异常的类型,即异常类的名字,但当异常发生时只需看到这个异常的类型就知道发生了什么,而其余一些操做在Throwable中已经有定义。因此除非有一些特殊操做,否则在自定义异常时只需只需简单的实现构造方法便可。
因此异常的根类Throwable定义了咱们须要的大多数方法:
// 获取建立异常时传入的字符串 String getMessage() // 使用System.err输出异常发生的调用栈轨迹 void printStackTrace() // 使用传入的PrintStream打印异常调用栈 void printStackTrace(PrintStream s) // 使用PrintStreamOrWriter打印异常调用栈 void printStackTrace(PrintStreamOrWriter s)
获取调用栈实例
StackTraceElement[] getStackTrace()
该方法放回StackTraceElement数组,StackTraceElement为调用方法栈的实例,改类型有如下经常使用方法:
// 返回栈代码所在的文件名 String getFileName() // 返回异常抛出地的行号 int getLineNumber() // 返回栈的类名 String getClassName() // 放回栈的方法名 String getMethodName()
当咱们捕获到一个异常时可能想将他在次抛出,但这样直接抛出的话异常的栈信息是该异常原来的栈信息,不会是最新的再次抛出的异常的栈信息。以下:
class SimpleException extends Exception { public SimpleException() { } public SimpleException(String msg) { super(msg); } } public class Test { public void s() throws SimpleException { throw new SimpleException(); } public void s2() throws SimpleException { try { s(); } catch(SimpleException e) { throw e; } } public static void main(String[] args) { Test t = new Test(); try { t.s2(); } catch (SimpleException e) { e.printStackTrace(); } } }
上面代码输出为:
com.ly.test.javatest.exceptiontest.SimpleException at com.ly.test.javatest.exceptiontest.Test.s(Test.java:19) at com.ly.test.javatest.exceptiontest.Test.s2(Test.java:24) at com.ly.test.javatest.exceptiontest.Test.main(Test.java:33)
能够看到异常抛出最终地为 com.ly.test.javatest.exceptiontest.Test.s(Test.java:19),但若是咱们想让异常抛出地变为s2那?毕竟咱们在这里本身抛出了异常。
Thrwoable类的fillInStackTrac建立一个新的Throwable对象,并将当前栈信息作新建立的Throwable异常的异常栈信息,而后返回。
上面的作法又有另一个问题,若是咱们使用fillInStackTrace得到新的异常,那原来的异常信息也就丢失了,若是咱们想抛出新的异常当又得包含原来的异常那?
Error、Exception和RuntimeException都含有一个接受Throwable对象的构造方法,在建立新的异常时时传入原来异常,便可保存原来异常。须要时使用getCause来获取到。除了使用构造方法传入异常,还可以使用initCase方法传入异常。这其中的潜台词是“改异常是由什么异常形成的”。以下:
public class Test { public void s() throws Exception { throw new Exception(); } public void s2() throws Exception { try { s(); } catch(Exception e) { Exception ne = (Exception)e.fillInStackTrace(); ne.initCause(e); throw ne; } } public static void main(String[] args) { Test t = new Test(); try { t.s2(); } catch (Exception e) { e.printStackTrace(); } } }
看一下面的代码:
public class Test { public static void s() throws IOException { throw new IOException(); } public static void main(String[] args) { String fileName = "C:\\temp\\test.txt"; File file = new File(fileName); InputStream in = null; try { in = new FileInputStream(file); s(); int tempbyte = in.read(); in.close(); } catch (IOException e) { if (in != null) { System.out.println("in"); } e.printStackTrace(); } } }
能够看到要对in进行close但正常的流程中发生了异常,致使正常流程中的in.close没法执行,便跳到cattch中去执行,上面因而又在catch中写了一个关闭。着只是一个简单清理操做,但若是须要执行的清理操做不止一行而是很是多那?也是在正常流程和catch中写两遍吗,这样是很是不友好的,因此java提供了finally,以下
public class Test { public static void s() throws IOException { throw new IOException(); } public static void main(String[] args) { String fileName = "C:\\temp\\test.txt"; File file = new File(fileName); InputStream in = null; try { in = new FileInputStream(file); s(); int tempbyte = in.read(); } catch (IOException e) { e.printStackTrace(); } finally { if (in != null) { System.out.println("in"); } } } }
finally中的代码不管异常是否发生都会被执行,即便try中包含return语句,也会在放回以前执行finally语句。
对于清理操做和一些异常发生也必获得执行的代码都应该放到finally中。
上面介绍使用finally来释放资源,但看下面这个情形:
public void test() { try { in = new BufferedReader(new FileReader()); String s = in.readLine(); } catch (FileNotFoundException e) { } catch (Exception e) { try { in.close(); } catch (IOException e2) { System.out.println("in class false"); } } finally { //in.close(); } }
这个例子能够看到若是new FileReader抛出了FileNotFoundException,那么in是不会被建立的,若是此时还在finally中执行in.close()那么天然是行不一样的。但若是抛出了IOExceptin异常,那么说明in成功建立但在readLine时发生错,因此在catch中进行close时in确定已经被建立。这种情形资源的释放应该放到catch中。
public class Test { public static void main(String[] args) { try { int i = throwException(); System.out.println(i); } catch (Exception e) { e.printStackTrace(); } } public static int throwException () throws Exception { try { throw new Exception(); } catch (Exception e) { throw e; } finally { return 1; } } }
上面代码输出:1
public class Test { public static void main(String[] args) { try { int i = throwException(); System.out.println(i); } catch (Exception e) { e.printStackTrace(); } } public static int throwException () throws Exception { try { throw new Exception(); } catch (Exception e) { throw e; } finally { throw new NullPointerException(); } } }
上面代码输出为:
java.lang.NullPointerException at com.ly.test.javatest.Test.throwException(Test.java:20) at com.ly.test.javatest.Test.main(Test.java:7)
能够看到main中捕获到的是NullPointerException,首先抛出的Exception异常丢失了。
在开发中非特殊情形应避免以上两种状况的出现。
父类构造器中声明的异常在基类的构造器中必须也声明,由于父类的构造器老是会显示会隐式(默认构造器)的被调用,而在子类构造器中是没法捕获父类异常的。但子类能够添加父类中没有声明的异常。
重载方法时子类只可抛出父类中声明的异常,由于咱们会将子类对象去替换基类,这时若是重载的方法添加类新的异常声明,那么原来的异常处理代码将没法再正常工做。但子类方法能够减小或不抛出父类方法声明的异常。