本文共 10586 字,大约阅读时间需要 35 分钟。
java 异常--读《疯狂java》
1.正确关闭资源
public class CloseResource3 { public static void main(String[] args) throws Exception { Wolf w = new Wolf("灰太狼"); System.out.println("Wolf 对象创建完成~"); Wolf w2 = null; ObjectOutputStream oos = null; ObjectInputStream ois = null; try { //创建对象输出流 oos = new ObjectOutputStream( new FileOutputStream("a.bin")); //创建对象输入流 ois = new ObjectInputStream( new FileInputStream("a.bin")); //序列化输出Java对象 oos.writeObject(w); oos.flush(); //反序列化恢复Java对象 w2 = (Wolf)ois.readObject(); } //使用finally块来回收资源 finally { if (oos != null) { try { oos.close(); } catch (Exception ex) { ex.printStackTrace(); } } if (ois != null) { try { ois.close(); } catch (Exception ex) { ex.printStackTrace(); } } } } }
上面程序所示的资源关闭方式才是比较安全的,这种关闭方式主要保证如下3点:
- 使用finally块来关闭物理资源,保证关闭操作总是会被执行;
- 关闭每个资源之前首先保证引用该资源的引用变量不为null;
- 为每个物理资源使用单独try…catch块关闭资源,
保证关闭资源时引发的异常不会影响其他资源的关闭
。
2.finally块的陷阱
public class ExitFinally { public static void main(String[] args) throws IOException { FileOutputStream fos = null; try { fos = new FileOutputStream("a.bin"); System.out.println("程序打开物理资源!"); System.exit(0); } finally { //使用finally块关闭资源 if (fos != null) { try { fos.close(); } catch (Exception ex) { ex.printStackTrace(); } } System.out.println("程序关闭了物理资源!"); } } }
不论try块是正常结束,还是中途非正常地退出,finally块确实都会执行。
然而在这个程序中,try语句块根本就没有结束其执行过程,System.exit(0);
将停止当前线程和所有其他当场死亡的线程。finally块并不能让已经停止的线程继续执行。 当System.exit(0)被调用时,虚拟机退出前要执行两项清理工作:
- 执行系统中注册的所有关闭钩子;
- 如果程序调用了System.runFinalizerOnExit(true);,那么JVM会对所有还未结束的对象调用Finalizer。
第二种方式已经被证明是极度危险的,因此JDK API文档中说明第二个方式已经过时了,因此实际开发中不应该使用这种危险行为。
第一种方式则是一种安全的操作,程序可以将关闭资源的操作注册成为关闭钩子。在JVM退出之前,这些关闭钩子将会被调用,从而保证物理资源被正常关闭。public class ExitHook { public static void main(String[] args) throws IOException { final FileOutputStream fos; fos = new FileOutputStream("a.bin"); System.out.println("程序打开物理资源!"); //为系统注册关闭钩子 Runtime.getRuntime().addShutdownHook( new Thread() { public void run() { //使用关闭钩子来关闭资源 if (fos != null) { try { fos.close(); } catch (Exception ex) { ex.printStackTrace(); } } System.out.println("程序关闭了物理资源!"); } }); //退出程序 System.exit(0); } }
代码为系统注册了一个关闭钩子,关闭钩子负责在程序退出时回收系统资源。运行上面程序,将可以看到系统可以正常关闭物理资源。
返回值
public class FinallyFlowTest2 { public static void main(String[] args) { int a = test(); System.out.println(a); } public static int test() { int count = 5; try { //因为finally块中包含了return语句 //则下面的return语句不会立即返回 throw new RuntimeException("测试异常"); } finally { System.out.println("finally块被执行"); return count; } } }
上面程序的try块中抛出了RuntimeException异常,程序并未使用catch块来捕获这个异常。正常情况下,这个异常应该导致test()方法非正常中止,test()方法应该没有返回值。
finally 块 总结
当程序执行try块、catch块时遇到throw语句时,throw语句会导致该方法立即结束,系统执行throw语句时并不会立即抛出异常,而是去寻找该异常处理流程中是否包含finally块。
如果没有finally块,程序立即抛出异常; 如果有finally块,系统立即开始执行finally块——只有当finally块执行完成后,系统才会再次跳回来抛出异常。 如果finally块里使用return语句来结束方法,系统将不会跳回去执行try块、catch块去抛出异常。
catch块的顺序
由于异常处理机制中排在前面的catch(XxxException ex)块总是会优先获得执行的机会
,因此Java对try块后的多个catch块的排列顺序是有要求的。
public class CatchSequenceTest { public static void main(String[] args) throws Exception { FileInputStream fis = null; try { fis = new FileInputStream("a.bin"); fis.read(); } //捕捉IOExcetion异常 catch (IOException ex) { ex.printStackTrace(); } //捕捉FileNotFoundException异常 catch(FileNotFoundException fex) //① { fex.printStackTrace(); } finally { //简单方式关闭资源 if (fis != null) { fis.close(); } } } }
程序提示①行粗体字代码处有编译错误,编译器不允许程序在18行捕捉FileNotFound Exception异常,
因为Java的异常有非常严格的继承体系
,许多异常类之间有严格的父子关系,比如,程序FileNotFoundException异常就是IOException的子类。根据Java继承的特性,子类其实是一种特殊的父类,也就是说,FileNotFoundException只是一种特殊的IOException。程序前面的catch块已经捕捉了IOException,这意味着FileNotFoundException作为子类已经被捕捉过了,因此程序在后面再次试图捕捉FileNotFoundException纯属多此一举。 经过上面分析可以看出,在try块后使用catch块来捕捉多个异常时,程序应该小心多个catch块之前的顺序:捕捉父类异常的catch块都应该排在捕捉子类异常的catch块之后(简称为先处理小异常,再处理大异常),否则将出现编译错误。
上面这条规则和前面介绍的if…else分支语句的处理规则基本相似:if…else分支语句应该先处理范围小的条件
,后处理范围大的条件,否则将会导致if…else分支语句后面的分支得不到执行的机会。try…catch语句的处理规则也是如此:try…catch语句的多个catch块应该先捕获子类异常(子类代表的范围较小),后捕获父类异常(父类代表的范围较大),否则编译器会提示编译错误。
由于Exception是所有异常类的根父类,因此try…catch块应该把捕捉Exception的catch块排在所有catch块的最后面。否则,Java运行时将直接进入捕捉Exception的catch块(因为所有异常对象都是Exception或其子类的实例),而排在它后面的catch块将永远也不会获得执行的机会。当然,编译器还是比较智能的,当检测到程序员试图做这样一件“蠢事”时,编译器会直接提示编译错误,阻止这样的代码获得执行。
不要用catch代替流程控制
public class ExceptionFlowTest { public static void main(String[] args) { String[] books = { "疯狂Java讲义", "轻量级Java EE企业应用实战", "疯狂Ajax讲义" }; int i = 0; while(true) { try { System.out.println(books[i++]); } catch (IndexOutOfBoundsException ex) { //结束循环 break; } } } }
程序使用“死循环”来遍历数组,当遍历数组产生“数组越界”异常时,会自动捕捉该异常,并跳出死循环。程序看上去没有任何问题,尝试运行该程序,确实可以完全遍历该字符串数组里的每个字符串。
看来这个“别出心裁”的主意还不错,完全可以使用这种方式来遍历数组。问题是,这种做法真的好吗?实际上,这种遍历数组的方式不仅难以阅读
,而且运行速度还非常慢
。
切记:千万不要使用异常来进行流程控制。异常机制不是为流程控制而准备的,异常机制只是为程序的意外情况准备的
,因此程序只应该为异常情况使用异常机制。所以,不要使用这种“别出心裁”的方法来遍历数组。
不能用catch 捕获 不可能抛出的异常
public static void test1() { try { System.out.println("www.crazyit.org"); } catch (IndexOutOfBoundsException ex) { ex.printStackTrace(); } } public static void test2() { try { System.out.println("www.crazyit.org"); } catch (NullPointerException ex) { ex.printStackTrace(); } }
IndexOutOfBoundsException、NullPointerException两个异常类都是RuntimeException的子类,因此它们都属于运行时异常
,
Checked异常
。 如果一个catch子句试图捕获一个类型为XxxException的Checked异常,那么它对应的try子句必须可能抛出XxxException或其子类的异常
,否则编译器将提示该程序具有编译错误, 但在所有Checked异常中,Exception
是一个异类,无论try块是怎样的代码,catch(Exception ex)总是正确的。
如果一个代码可能抛出某个Checked异常
(这段代码调用的某个方法、构造器声明抛出了该Checked异常),那么程序必须处理这个Checked异常。对于Checked异常的处理方式有两种:
- 当前方法明确知道如何处理该异常,程序应该使用try…catch块来捕获该异常,然后在对应的catch块中修复该异常;
- 前方法不知道如何处理这种异常,应该在定义该方法时声明抛出该异常。
总之,程序使用catch捕捉异常时,其实并不能随心所欲地捕捉所有异常。程序可以在任意想捕捉的地方捕捉RuntimeException异常、Exception,但对于其他Checked异常,只有当try块可能抛出该异常时(try块中调用的某个方法声明抛出了该Checked异常),catch块才能捕捉该Checked异常。
public class DoFixThing2 { public static void main(String[] args) throws Exception { test(); } public static void test() throws ClassNotFoundException { try { //加载一个类 Class.forName("org.crazyit.learning.Student"); System.out.println("www.crazyit.org"); } finally { test(); } } }
它不可能抛出异常
。因为根据finally的执行流程,每当程序试图调用throw语句抛出异常时,程序总会先执行finally块中代码,这就导致程序进行无限递归
,即使程序实际已经发生了StackOverflowError错误
,依然不会非正常退出
。
运行上面程序,将看到程序直接进入“挂起”状态
,程序永远不会结束
,甚至会导致整个操作系统的运行都非常缓慢,只有通过强行结束java.exe进程来结束该程序。
这个程序给出的教训是:无论如何不要在finally块中递归调用可能引起异常的方法
,
继承得到的异常
Java语言规定,子类重写父类方法时
,不能声明抛出比父类方法类型更多、范围更大的异常。
也就是说,子类重写父类方法时,子类方法只能声明抛出父类方法所声明抛出的异常的子类。
转载地址:https://blog.csdn.net/ynchyong/article/details/112313417 如侵犯您的版权,请留言回复原文章的地址,我们会给您删除此文章,给您带来不便请您谅解!