——◆编程心得[4]:在构造函数中抛出异常——
根据对构造子的理解,是可以知道构造函数本身属于一个特殊的数据结构,是不存在返回值的,那么如果在构造函数里面来获得异常信息就成为了很困难的事情。面对这样的情况如何进行相关处理呢?
【*:两年前我参与开发一个基于J2EE的CRM客户端的时候就遇到了这样一个问题:因为系统里面具有一个界面管理器,而在管理器最初需要针对初始化的内容进行监控,而且该管理器必须将这个过程的一些错误记录下来,而最开始使用普通的办法都没有做到,而且使用日志也不管用,主要原因是因为日志是正常运行的。所以针对这一点深有感触。】
有些技术可以实现所需要的这种情况的需求:
双阶段构造(two-stage construction):
将可能产生错误的代码移除到构造函数里面,这些函数能够返回一定的错误代码或者错误相关信息。而这样做就使得的调用顺序有了典型的不同:用户必须先调用构造函数、然后调用那些可能产生错误信息的函数、最后再检查返回代码以及捕捉相关异常。先提供一段代码,然后再进行详细的分析:
import java.io.FileReader;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.FileNotFoundException;
/**
*构造函数双阶段构造的概念说明
**/
public class FileScanner
{
public FileScanner(String filename) throws FileNotFoundException,IOException
{
FileReader reader = new FileReader(filename);
BufferedReader br = new BufferedReader(reader);
String str = br.readLine();
//……
}
public void scanFile(){}
public static void main(String args[])
{
FileScanner scanner = null;
try
{
scanner = new FileScanner("C:/text.txt");
}
catch(FileNotFoundException ex)
{
ex.printStackTrace();
}
catch(IOException ex)
{
ex.printStackTrace();
}
scanner.scanFile();
}
}
【*:仔细思考上边这段代码:FileScanner对象接受到了一个字符串参数,这个参数代表了一个文件路径,然后尝试着打开这个文件。若该文件不存在,构造函数就会引发一个FileNotFoundException异常,如果读取过程再出现问题,就会直接抛IOException。仔细考虑这段调用代码,如果构造失败,就会使得引用scanner为null,这种情况下,表示既然没有被构建,如果再访问该对象就会直接抛出NullPointerException。】
从这点上讲,即使构造函数不是一般函数,仍然可以使用异常处理或者throws语句,这样的方式来处理构造失败的情况是比较高效合理的一种方式。如果结合使用JVM里面深入引用,那么不仅仅可以在一个对象创建过程监控该对象,而且可以在对象毁灭一直到垃圾回收的过程里面监控整个对象的所有状态,在开发一些严格的程序的时候就会遇到这样各种各样的情况。
——◆编程心得[5]:深入理解throws——
throws是一种语言特性,用来列出[可以从某个函数传到外界]的所有可能的异常。JVM编译器会强迫必须在函数中捕捉这些列出的异常,否则就在该函数的throw子句中声明。如果某个项目已经完成了,但是有时候需要在里面为某些不良的设计添加一些新的可抛出的异常,这种情况如何来完成呢。当然和前边讲的异常处理一样的:
[1]使用try-catch将这个异常捕捉
[2]使用throws或者throw将这个异常抛出
【*:注意这里说的是在一个已经完善的系统里面,所以根据参考可以知道,这种worker method类型的低级系统或者不良设计里面,如果要使用try-catch的方式来处理,可能行不通,因为它没有办法独立处理这个异常,唯一的办法就是使用第二种方式,那么就需要理解throws的一些特性以及相关缺陷。要知道在一个已经完善的系统里面添加throw或者throws是一个大工作量,如果系统已经完成了过后才来考虑异常抛出,是一个很严重的问题。而且在添加过程不小心会影响系统的整体开销或者资源调配,假设在设计之初忽略了一个来自数据层的异常,那么添加和修改的时候不是进行的处理而是throws,注意这里说的是使用try和catch不能处理的时候,那么不仅仅会在业务逻辑层遇到该处理的问题,还会在其他层次遇到这种一直往外抛出的问题,那么在系统里面加入该异常的时候,难免会很有影响。】
其实最好的方式应该是在设计之初将这些可能的关键性因素考虑到,这样就使得throws的使用更加合理,记住一点:
在一个系统开发设计之初最好能够设计一个良好的异常体系处理结构,而这部分设计可能往往会成为最容易忽略的问题,因为最初没有办法考虑到一些系统本身的异常问题。设计异常本身对系统而言就是一个挑战,这种方式和TDD的设计方式是一个出发点,因为要在设计之初考虑到系统可能会出现和遇到的问题,所以这样的设计经常是出于积累和思考。
先考虑这样一段代码:
class ExceptionOne extends Exception{}
class ExceptionTwo extends ExceptionOne{}
class ExceptionThree extends ExceptionTwo{}
public class ExceptionThrowTester
{
public void testException(int i) throws ExceptionOne
{
if( i == 1 )
throw new ExceptionOne();
if( i == 2 )
throw new ExceptionTwo();
if( i == 3 )
throw new ExceptionThree();
}
}
注意上边这段代码,编译是合理的,但是这样的方法有一定的弱点。实际上这个地方抛出的所有异常都是属于ExceptionOne类型的,这里能够通过编译是因为符合throws语句的语法要求,这段代码可以这样理解:函数可能产生ExceptionOne的异常。可以稍稍修改一下testException函数的定义:
public void testException(int i) throws ExceptionOne,ExceptionTwo,ExceptionThree
但是这样改写过后,抛出的异常还是会这样理解:函数还是可能会产生一个隶属于ExceptionOne的异常,因为这里这三个自定义异常是存在继承关系的。如果想要知道ExceptionOne的派生异常类,就得查看源代码才能够办到,假设这个代码设计存在于已经编译好的jar库里面,其实这本身而言是不切实际的。这种做法使得真正在调用过程里面会出现下边的这种写法,这种写法在编程的时候很常见:
public class ExpThrowTesterTwo
{
public static void main(String args[])
{
try
{
//正常的逻辑代码
}
catch(ExceptionOne ex)
{
//异常的处理代码
}
}
}
上边这种写法是不会出现任何编译错误的,但是考虑一点,会使得养成了一个不良的习惯:正确地说,应该使得抛出的异常精确到某种类型,而不应该是它的父类型,这样真正在开发过程也方便进行调试工作。也就是说可以避免[试图精确推断这个函数可能产生哪种异常]的困境。再看一段代码:
import java.io.File;
import java.io.FileReader;
import java.io.FileNotFoundException;
public class ExpThrowTesterThree
{
public static void main(String args[])
{
try
{
File file = new File("C:/readme.txt");
FileReader reader = new FileReader(file);
}
catch(Exception ex)
{
//对应的异常处理方式
}
}
}
按照理论来讲这段代码没有任何问题,但是会有一点,比如try块里面出现了其他异常,那么只能从一个异常的堆栈信息来确定程序里面到底遇到了什么问题,其实最好的方法就是把其中的异常处理分开操作。比如会出现FileNotFoundException的地方使用对应的异常,而会出现NumberFormatException的地方写另外的catch语句来进行分类匹配,这样程序里面有什么异常就一目了然了。
总之:使用throws的时候,最好能够明确抛出某种派生类型的异常,而且使用catch捕捉的时候也尽量使对应的子类异常。
——◆编程心得[6]:避免资源泄漏,finally用法——
在JVM异常处理机制里面,比较出色的一点就是finally方法,finally方法里面的代码块总是会得以执行的,而且不论是否有异常,都能够执行,这在维护对象内部状态和资源清理上是很实用的:
对比两段代码:
import java.net.*;
import java.io.*;
/**
*不使用finally写法【*:概念说明代码里面可能存在很多不规范的内容】
**/
public class FinallyTester{
public static void main(String args[])
{
ServerSocket ss = new ServerSocket(0);
try{
Socket socket = ss.accept();
// 其他操作
}
catch(Exception ex)
{
ss.close();
throw ex;
}
ss.close();
}
}
以下是使用finally的代码:
import java.net.*;
import java.io.*;
/**
*使用finally写法
**/
public class FinallyTester{
public static void main(String args[])
{
ServerSocket ss = new ServerSocket(0);
try{
Socket socket = ss.accept();
// 其他操作
}
finally
{
ss.close();
}
}
}
【*:仔细思考一下上边两段代码,当然第二段更加规范一点。因为有可能程序员会忘记了close()的操作,这样就有可能造成忘记关闭而出现资源泄漏的情况,所以确保资源能够正常回收,最好的办法就是使用finally块防止资源泄漏,和try/catch块匹配使用,finally代码块里面的内容存在于异常处理机制里面作为统一的出口,就是任何情况都会执行的,而且会在异常处理机制里面确保退出之前执行。】
所以使用finally进行资源回收是最好的方式,例如:JDBC连接关闭、网络连接关闭、对象回收等
后续链接:
http://blog.csdn.net/silentbalanceyh/archive/2009/09/18/4564884.aspx