(本章主要讲解Java里面比较核心的一块内容——异常处理,Java异常处理机制,一致都是比较复杂的一块,而很多时候我们如果写程序的时候能够适当地注意对应的一些异常处理情况,那么我们就会在开发过程节省一大部分时间,最常见的情况就是辅助我们进行调试以及维护工作以及提高系统的容错性和稳定性。这一章和前边类和对象章节不一样,这一章可能涵盖的内容没有前边那章多,但是我会尽量保证在整篇文章里面把开发过程中需要注意到的与异常有关的细节问题以及对应的开发经验写入本文,而本章的出发点是异常处理,核心内容还涵盖了测试、调试、以及部署等相关内容以及。如果有笔误的地方,请来Email指点:silentbalanceyh@126.com,谢谢)
本章目录
1.Java异常处理
2.异常处理心得
3.断言的使用
4.Java中的日志(JDK1.4 Logging Framework)
5.第三方日志库(Log4J、Commons Logging Framework)
1.Java异常处理
i.异常的概念和Java里面的异常体系结构
1)基本概念:
程序中的异常,一般成为例外情况,可以理解为是非正常情况,其他编程语言里面也有这样的情况,Java里面同样存在这样一个体系结构,这里需要分清楚的是异常和错误不是一个概念。异常并非是真正的错误,因为他们是一些例外情况,这些情况有可能不会导致系统直接崩溃掉,但是它的存在只能说是程序的某种缺陷,或者说是非必然缺陷,而Java里面提供的异常体系结构就是为了解决这些缺陷而存在的。
在异常处理机制诞生之前,传统的异常处理方式多数是为了采用返回值来标识程序出现异常的情况,这种方式都很熟悉,如同在调试过程即是有良好的调试工具,但是常用的手段就是System.out.println的方式,但是这样的方式隐含一定的缺点。
[1]一个API可以返回任意的值,但是这些返回值本身不能解释返回值是否代表一个异常发生了,也不能描述异常的详细情况,若要知道该API出现异常的一些内容,还需要调用它的某些方法;
[2]没有一种结构可以确保异常情况能够得到处理,如果使用第一种方法,就会使得代码的可读性很差,而且很多时候并不能因为某些情况的存在就终止程序,就程序本身而言是应该提供一定的反馈情况。假设这样一个场景,如果你去输入用户名和密码登陆,如果你的用户名和密码输入错误了,但是界面上没有任何反应,这种情况是不是很糟糕,当然这只是个比方,这里不一定是出现了异常。
在开发过程中,当一个程序本身抛出了异常过后,程序会从程序导致异常的地方跳出来,在java语言里面,使用try和catch块来实现,当JVM碰到这个语句块的时候,它会监听整个Java程序,一旦出现了任何异常情况,它会将整个程序的执行权交给catch块来执行。
先看一段简单的代码:
import java.io.File;
import java.io.FileReader;
/**
*一个简单的文件操作
**/
public class CustomerFileReader
{
public static void main(String args[])
{
File file = new File("C:/read.txt");
FileReader reader = new FileReader(file); //这句话不能通过JVM的编译器
}
}
上边这段代码如果使用的是javac的命令,那么编译器就会报错,可能错误信息如下:
unreported exception java.io.FileNotFoundException; must be caught or declared to be thrown
那么如果要保证上边这句代码是能够通过编译的,如何简单修改呢,有两种方式:
[1]加入try和catch块,使用JVM的异常体系结构去捕捉该异常;
[2]直接throws这个异常,让程序显式抛出该异常
2)Java里面的异常体系结构:
先看下边的异常体系结构图:
![](https://p-blog.csdn.net/images/p_blog_csdn_net/silentbalanceyh/EntryImages/20090913/exception.jpg)
Java里面的异常分类按照下边的结构来分:
Throwable是所有异常的基类,程序中一般不会直接抛出Throwable对象,Throwable本身存在两个子类实例,一个是Error、一个是Exception;
[1]Error:在Java里面Error表示程序在运行期间出现了十分严重的问题以及不可以恢复的错误,这种情况唯一的办法是中止程序运行,JVM一般不会检查Error是否被处理,而本身在程序中也不能捕获Error类型的异常,因为Error一旦产生,该程序基本会处于需要中止的状态。
[2]Exception:在Java里面Exception指的就是在编程过程可能会遇到的异常的概念,也属于Java程序里面允许出现的例外的状况,而Exception本身分为以下两种:
RuntimeException:该异常继承于Exception类,这种类型的异常可以这样理解,为不可估计的异常,一般称为运行时异常,从字面意思上讲就是在程序正式运行的时候会碰到的偶发性异常,这种异常因为是在运行时抛出一般情况下不需要进行捕获操作。
CheckedException:该类异常不存在继承于Exception类的说法,因为Java里面没有CheckedException异常类,而之所以这样区分,因为CheckedException类的异常是在编程过程中经常会遇到的异常,可以翻译为“可检测异常”或者“捕获型异常”,该类异常往往属于编译期异常,一般开发过程会针对系统进行CheckedException的设计。
【*:JVM在处理Java程序的时候,Runtime Exception和Checked Exception两种类型的异常在运行机制上是不一样的,而仔细考虑两种异常的使用场合也会发现,其两种异常在设计上所提及的使用目的也大相径庭。从程序开发角度可以这样理解:Checked Exception的设计目的在于这个Exception是必须被处理的,在程序设计过程应该知道这类异常会出现,所以要针对这一类型的异常进行不同的处理操作,这些异常也可以认为是在程序设计之初可以考虑到的异常;而RuntimeException可能理解起来隐晦一点,不能说不能考虑到这种异常的存在,反而是即使能够考虑到,也不能进行良性的程序处理,它往往是暗示着程序可能会出现某种错误,这种错误有可能根程序本身无关,也有可能有关,是在设计程序之初是无法预知处理方式的,而有时候甚至会造成程序中止的情况。】
这里提供一个简单的关于RuntimeException的例子:
public class RunExpTester
{
public static void main(String args[])
{
try
{
//……
}
}
catch(Exception ex)
{
//……
}
}
}
}
考虑一下上边这段代码,上边这段代码使用了面对CheckedException的程序处理方式,使用了try和catch块来处理有可能存在的Exception,但是这样就出现了一个缺点:很可能不知道发生了什么异常,这些异常的源头,而且如果整个程序段里面还包含了必须处理的CheckedException,那么这种设计方法反而给开发带来了不必要的成本开销。
一般情况下,在测试阶段,如果遇到了RuntimeException可以让它这样存在或者发生,然后再逐渐去修改的代码,让它尽量避免掉,否则面对任何一个Exception,都要确定不会轻易出现或者说更加完美一定不出现RuntimeException为之。这里提供一个常用的编程习惯作为参考,在使用Exception的catch过程的时候,一般可以这样来书写:
catch(Exception ex)
{
// TODO:书写该异常的说明
ex.printStackTrace();
}
}
当然这种情况是使用IDE的时候的一种习惯,一般IDE都提供了TODO:标记,使用该标记和所有TaskList的前缀标记不仅仅可以在开发过程了解到底有多少地方存在手写的异常,而且在针对CheckedException的处理过程中,不是每一个异常都需要使用ex.printStackTrace()方法将该异常的堆栈信息全部打印出来,有时候需要在catch块里面书写更加实用的异常处理代码。
3)深入了解Throwable类:【参考API文档】
Throwable类是Java语言中所有错误(Error)或异常(Exception)的超类,只有当某个对象是该类的子类实例的时候,才能通过JVM或者Java本身编写过程的throw语句抛出,按照这种逻辑区判断,只有此类或者它的子类才可以是catch子句中的参数类型。Throwable类有两个子类Error和Exception,上边已经简单介绍过这两种类型的区别了。Throwable类本身包含:
[1]线程创建的时候执行堆栈的快照
[2]有关错误的消息字符串,比如该异常出现的位置以及代码里面的哪一行
[3]它指出了这个异常的原因:该异常是由哪个异常导致的或者说是由哪个异常抛出的Throwable导致的这个Throwable的产生
从JDK 1.4开始,出现了一个异常处理的新概念:异常链(Cause机制)。异常链机制可以这样理解:如果某个程序出现了异常,那么该异常本身也会有个原因,这个原因可能是自身的,也可能是外界的,以此类推就形成了一个异常链,简单讲:每个异常都是由另外一个异常引起的。
而什么内容导致了throwable cause呢,查阅官方的API文档有以下两种解释:
[1]导致throwable cause的一个理由是,抛出它的类构建在低层抽象之中,而高层操作由于低层操作的失败而失败。让低层抛出的throwable向外传播是一种糟糕的设计方法,因为它通常与高层提供的抽象不相关。此外,这样做将高层API与其实现细节关联起来,假定低层异常是经过检查的异常。抛出“经过包装的异常”(即包含cause的异常)允许高层与其调用方交流失败详细信息,而不会招致上述任何一个缺点。这种方式保留了改变高层实现而不改变其 API 的灵活性
[2]导致throwable cause的另一个cause是,抛出它的方法必须符合通用接口,而通用接口不允许方法直接抛出cause。例如,假定持久集合符合
Collection
接口,而其持久性在java.io的基础上实现。假定add方法的内部可以抛出
IOException
。实现可以与其调用方交流IOException的详细消息,同时通过以一种合适的未检查的异常来包装IOException,使其符合Collection接口。
4)特殊类AssertionError:
这里提供一段代码:
public class AssertionErrorTester
{
public static void main(String args[])
{
try
{
assert args.length < 0:"Args Length Error!";
}
catch(AssertionError e)
{
String message = e.getMessage();
System.out.println("Error Source:" + message);
}
}
}
【*:这里需要提及的一个异常类是AssertionError类,因为上边已经讲过了,所有的Error都是不能进行catch的处理的,但是AssertionError属于一个比较特殊的类,因为JVM针对AssertionError类是可以进行catch处理的,该Error和普通的Error可能存在本质的区别】
使用断言编译方式编译以上代码,然后打开断言执行该编译好的class文件,会出现以下输出:
Error Source:Args Length Error!
关于如何使用断言编译以及断言本身的使用规则在断言章节会涉及到,这里先不做详细讲解
ii.异常的基本语法
前边介绍了Java异常体系结构、分类以及基本概念,这一小节需要介绍的就是Java里面异常的基本语法。在Java里面,异常处理机制的编程部分需要使用到几个关键字:try、catch、finally、throw、throws
1)try、catch、finally关键字:
/**
*测试Exception关键字的代码
**/
public class ExceptionTester
{
public static void main(String args[])
{
//这一块被成为异常的正常执行块,也就是如果没有异常抛出的话,try块会一直这样正常执行到最末位
try
{
//判断输入参数的长度
if( args.length == 0 )
{
System.out.println("Args length is zore");
}else{
String inputString = args[0];
int inputNumber = Integer.parseInteger(inputString);
System.out.println("Input number is " + inputNumber);
}
}
//这一块是异常处理块,一旦当try代码块里面抛出了异常的时候,直接从try块中断,直接进行catch代码块的执行
catch(Exception ex)
{
ex.printStackTrace();
}
//finally代码块,不论try中是否抛出异常,也不论catch里面是否真正能够捕捉到异常,finally里面的代码都会执行(有例外)
finally
{
System.out.println("Testing finishing...");
}
}
}
这段代码出现了三个关键字,try、catch、finally,这里先对这三个关键字简单讲解:
try语句:
该语句块属于代码的正常执行块,如果这段代码里面不会出现任何异常,那么try语句块会按照顺序一直执行下去
catch语句:
该语句块的参数类似于平时在代码书写中的方法声明,包含了一个异常类型以及一个异常对象。这里结合第一节讲到的,异常类型必须是Throwable的子类型,它指出了catch语句处理的异常类型,异常对象则有可能在try语句块里面生成,而且被对应的catch块里面的异常类型所捕获,大括号里面的内容就是当你捕获了对应的异常过后如何进行处理。
在异常处理里面,catch语句块可以有多个,分别处理不同的异常。Java运行的时候一旦抛出了异常就从catch块从上往下检索,一旦匹配对应的类型就执行catch块里面的内容,所以这里有一点需要注意:
catch块里面的异常类型的顺序,一般情况是从特殊到一般,然后是从子类到父类,否则会造成代码不可达的无用代码块
finally语句:
该语句块可以指定一个段代码块,不论try块也好、catch块也好,也不论异常是否抛出,最终都会执行finally块里面的内容,可以这样理解:finally块里面是异常处理机制的统一出口,只要存在这样的一段代码块最终出口都是执行完finally块里面的内容了再继续。【*:但是有一个特殊的情况,如果try块里面出现了return语句,那么finally块里面的内容是不会执行的,但是这种做法不提倡。】
该语句块可以指定一个段代码块,不论try块也好、catch块也好,也不论异常是否抛出,最终都会执行finally块里面的内容,可以这样理解:finally块里面是异常处理机制的统一出口,只要存在这样的一段代码块最终出口都是执行完finally块里面的内容了再继续。【*:但是有一个特殊的情况,如果try块里面出现了return语句,那么finally块里面的内容是不会执行的,但是这种做法不提倡。】
2)throw、throws关键字:
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
/**
*提供throw和throws关键字的代码块
**/
public class ThrowInstance
{
public static void main(String args[]) throws Exception
{
try
{
readFile("C:/read.txt");
}
catch(IOException ex)
{
throw new Exception(ex);
}
}
public static void readFile(String path) throws IOException
{
File file = new File(path);
FileReader reader = new FileReader(file);
}
}
从上边这段代码理解throw和throws关键字:
throw关键字:
throw关键字总是出现在函数体内部,用来抛出一个异常,程序会在throw语句后立即终止执行,也就是说位于throw语句之后的语句块不会执行,一旦它抛出了一个异常过后,JVM会在包含它的try块所对应的catch里面根据抛出的异常类型匹配操作,如果能匹配就直接被捕捉,一旦不能匹配就继续往执行体的外层抛出该异常。
throws关键字:
throws关键字总是出现在函数头部,用来表明该函数有可能会抛出某种异常,有几点需要注意:
[1]函数可以抛出多个不同类型的异常,直接使用,将每种抛出的不同异常分开;
[2]如果函数体里面存在throw语句,而且函数体本身没有进行捕捉的话,那么必须使用throws在函数头里面添加对应的异常抛出语句,否则无法通过编译
[3]如果编写代码的时候需要明确抛出一个RuntimeException,那么必须显示使用throws语句来声明它的类型
[4]以上的规则主要是针对CheckedException,针对Error和RuntimeException或者它们的子类,这些规则不起作用
3)关键字的搭配:
try+catch:
这是常用的代码结构,这种情况类似下边这种情况:
/**
*try+catch语句块
**/
public class TryCatch
{
public static void main(String args[])
{
try
{
//正常执行语句块
}
catch(Exception ex)
{
//抛出异常过后的异常捕捉语句块,捕捉到异常了就执行
}
}
}
这种语句块的执行流程为:运行try块中的代码,如果有异常抛出,就会转到catch语句块中执行,当然前提是catch中的异常类型和try块中抛出的异常类型匹配。
try+catch+finally
/**
*try+catch+finally语句块
**/
public class TryCatch
{
public static void main(String args[])
{
try
{
//正常执行语句块
}
catch(Exception ex)
{
//抛出异常过后的异常捕捉语句块,捕捉到异常了就执行
}
finally
{
//统一出口代码块
}
}
}
这种结构的语句执行流程为:
先运行try语句块中的语句,如果有异常抛出,则转入catch语句块中执行,执行完毕过后,就执行finally语句块里面的代码,然后再执行finally后边的语句块;如果没有异常抛出,执行完try语句块过后,就执行finally块中的语句,然后再执行finally语句块后边的内容。
try+finally
/**
*try+finally语句块
**/
public class TryCatch
{
public static void main(String args[]) throws Exception
{
try
{
//正常执行语句块
}
finally
{
//统一出口代码块
}
}
}
该语句块的执行流程为:
先运行try语句块中的语句,如果有异常抛出,程序转入finally块里面执行finally块中的代码,这种情况与上边不同的是,执行完finally语句块里面的内容过后程序会以异常的方式抛出,不会继续执行finally语句块后边的内容,而这种情况需要注意一点就是代码里面的红色部分,因为没有执行异常捕捉,该情况需要从方法声明里面抛出对应的异常。
4)在这三种结构里面需要注意以下几个问题
简单总结几点:
[1]try、catch、finally三个语句块均不能单独使用,三者可以组成try+catch、try+catch+finally、try+finally三种结构,catch语句可以有一个或多个,而且都处于平级,finally语句最多一个
[2]try、catch、finally三个代码块中变量的作用域为代码块内部,即变量作用于为局部变量作用域,分别独立而不能相互访问。如果要在三个块中都可以访问,则需要将变量定义到这些块的外面,即在{}之外声明
[3]多个catch块时候,只会匹配其中一个异常类并执行catch块代码,而不会再执行别的catch块,并且匹配catch语句的顺序是由上到下,在声明过程中子类需要放在前边捕捉
至于throw和throws关键字,这里再摘录网上一篇BLOG里面的说明:
throw关键字是用于方法体内部,用来抛出一个Throwable类型的异常。如果抛出了检查异常,则还应该在方法头部声明方法可能抛出的异常类型,该方法的调用者也必须检查处理抛出的异常。如果所有方法都层层上抛获取的异常,最终JVM会进行处理,处理也很简单,就是打印异常消息和堆栈信息。如果抛出的是Error或RuntimeException,则该方法的调用者可选择处理该异常。
throws关键字用于方法体外部的方法声明部分,用来声明方法可能会抛出某些异常。仅当抛出了检查异常,该方法的调用者才必须处理或者重新抛出该异常。当方法的调用者无力处理该异常的时候,应该继续抛出,而不是囫囵吞枣一般在catch块中打印一下堆栈信息做个勉强处理。
最终补充一个复杂的概念说明代码:
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
/**
* 关于异常的概念说明代码
*/
public class ExceptionTester {
public static void main(String args[]) throws IOException{
try{
processException();
}
finally{
System.out.println("Main body");
}
}
public static void processException() throws IOException{
try{
System.out.println("Inner Try");
File file = new File("C:/read.txt");
FileReader reader = new FileReader(file);
}catch(IOException ex){
System.out.println("Inner Exception Sub");
throw new IOException();
}catch(Exception ex){
System.out.println("Inner Exception");
throw new FileNotFoundException();
}finally{
try{
System.out.println("File Not Found Try");
throw new FileNotFoundException();
}catch(FileNotFoundException ex){
System.out.println("File Not Found Exception");
}
System.out.println("Inner Last");
}
System.out.println("Outer Last");
}
}
先看看这个程序的输出,然后再详细分析:
Inner Try
Inner Exception Sub
File Not Found Try
File Not Found Exception
Inner Last
Main body
Exception in thread "main" java.io.IOException
at ExceptionTester.processException(ExceptionTester.java:29)
at ExceptionTester.main(ExceptionTester.java:14)
[1]首先程序进入processException()方法执行System.out.println("Inner Try"),所以第一行输出为Innser Try
[2]异常发生,因为C:/read.txt文件不存在,所以此处会抛出异常java.io.FileNotFoundException,抛出代码的行为:
FileReader reader = new FileReader(file);
[3]抛出异常过后,首先捕捉到了该异常的是第一个catch块,因为IOException是FileNotFoundException的父类,所以该catch块捕捉了抛出的异常输出了Inner Exception Sub
[4]catch捕捉到异常执行过后继续往外抛异常,这样catch块就执行完了,然后执行finally块的内容
[5]在finally块中先执行try块的内容,所以输出为:File Not Found Try
[6]紧接着又从该块里面抛出了FileNotFoundException异常,抛出该异常过后又被内部的catch块捕捉到,所以继续输出:File Not Found Exception
[7]内部的catch块捕捉到异常过后,该异常就不往外继续抛,那么就继续执行finally后边的语句,所以输出Inner Last
[8]然后这里需要注意的是,外层的异常并没有处理,因为外层catch里面的内容是继续抛出异常,这个时候finally执行完毕,就不会输出Outer Last了,而是直接把异常抛出到方法外面
[9]最外层继续往外抛出异常,而最外层是try+finally结构,所以执行完finally语句块过后,继续往外抛异常,从main方法里面抛出,所以程序最后的输出是:
Main body
Exception in thread "main" java.io.IOException
at ExceptionTester.processException(ExceptionTester.java:29)
at ExceptionTester.main(ExceptionTester.java:14)
iii.Java常用异常API全表【不包含JDBC部分,java.sql包里的Exception】
接下来针对常见的异常类型进行一个简单的讲解,使得初学者更加明白[]中为继承树往上继续直到介绍的根类异常,该文档整理于Java API 6.0文档,这里不整理Error部分,因为Error部分一般情况不能被catch,也就是不能被捕捉,这里整理的主要是RuntimeException和CheckedException两种类型的异常
1)java.lang包:【*:java.lang.RuntimeException的父类是java.lang.Exception,()里面标识着从什么时候的JDK版本号开始拥有该类】
RuntimeException(1.0):
|—ArithmeticException(1.0):[->RuntimeException]
|:当出现运算条件的异常的时候,就抛出该异常,常见情况:计算除法的时候除数为0,该异常为RuntimeException
|—ArrayStoreException(1.0):[->RuntimeException]
|:试图将错误类型的对象存储到一个对象数组的时候抛出该异常,例如一个String的数组里面添加一个Integer对象的时候就会抛出该错误
|—ClassCastException(1.0):[->RuntimeException]
|:试图将一个对象强制转换成为不是该实例的子类的时候,强制转换类型会抛出该异常
|—EnumConstantNotPresentException(1.5):[->RuntimeException]
|:当应用程序试图通过名称和枚举类型访问那些不包含具有指定名称的常量或者枚举量的时候,抛出该异常
|—IllegalArgumentException(1.0):[->RuntimeException]
|:如果方法传递了一个不合法或者不正确的参数的时候,会抛出该异常
|—IllegalThreadStateException(1.0):[->IllegalArgumentException->RuntimeException]
|:当指示线程没有处于请求操作所要求的适当状态的时候抛出的异常,关联于Thread类中的suspend和resume方法
|—NumberFormatException(1.0):[->IllegalArgumentException->RuntimeException]
|:当应用程序试图将字符串转换成一种数值类型,但是该字符串不能转换为适当格式时,抛出该异常。
|—NegativeArraySizeException(1.0):[->RuntimeException]
|:如果应用程序试图创建大小为负的数组的时候,抛出该异常【*:需要区分于ArrayIndexOutOfBoundsException,一个创建数组的时候为负,一个是数组的索引为负,一个是创建的时候一个是使用的时候】
|—IndexOutOfBoundsException(1.0):[->RuntimeException]
|:指示某个排序索引超出范围的时候抛出,针对数组、字符串和向量排序
|—ArrayIndexOutOfBoundsException(1.0):[->IndexOutOfBoundsException->RuntimeException]
|:当访问数组的索引非法越界的时候,就抛出该异常,如果索引为负或者索引大于数组大小的时候,索引为非法索引,注意数组的最大索引为数组的length-1
|—StringIndexOutOfBoundsException(1.0):[->IndexOutOfBoundsException->RuntimeException]
|:此异常由String方法抛出,指示索引或者为负,或者超出字符串的大小。一般情况在charAt的方法调用的时候,当索引等于或者大于字符串的大小的时候抛出该异常
|—NullPointerException(1.0):[->RuntimeException]
|:当应用程序试图在需要对象的地方使用null时,抛出该异常,该异常为程序开发中的常用异常:
[1]调用null对象的实例方法
[2]访问或修改null对象的字段
[3]将null作为一个数组,获得其长度
[4]将null作为一个数组,访问或修改其时间片
[5]将null作为Throwable值抛出
|—SecurityException(1.0):[->RuntimeException]
|:此异常由安全管理器抛出,指示存在一定的安全侵犯
|—TypeNotPresentException(1.5):[->RuntimeException]
|:当应用程序试图使用表示类型名称的字符串对类型进行访问,但无法找到带有指定名称的类型定义的时候抛出该异常,区别于ClassNotFoundException的是,该异常未经过类型检查,而ClassNotFoundException是经过类型检查的异常
|—UnsupportedOperationException(1.2):[->RuntimeException]
|:如果不支持请求的操作的时候,就抛出该异常
Exception(1.0):
|—InstantiationException(1.0):[->Exception]
|:当应用程序试图使用Class中的newInstance方法创建类的实例的时候,指定的类对象没有办法被实例化,抛出该异常,一般有以下状况:
[1]类对象标识一个抽象类、接口、数组类、基本类型、void
[2]类没有非null的构造方法
|—NoSuchFieldException(1.1):[->Exception]
|:当某个类不存在该名称的字段的时候抛出该异常,一般情况在Java反射中使用的时候会抛出该异常
|—NoSuchMethodException(1.0):[->Exception]
|:在反射过程不能找到某一特定方法的时候,抛出该异常
|—InterruptedException(1.0):[->Exception]
|:如果线程在活动之前或者活动期间处于正在等待、休眠或者占用状态且该线程被中断的时候,抛出该异常。
|—ClassNotFoundException(1.0):[->Exception]
|:当JVM试图加载累的时候,抛出该异常,一般情况为调用以下方法:
[1]Class类中的forName方法
[2]ClassLoader类中的findSystemClass方法
[3]ClassLoader类中的loadClass方法
|—CloneNotSupportedException(1.0):[->Exception]
|:当调用Object类中的clone方法的时候,前边已经讲过了对象拷贝,当调用该方法的对象无法实现Cloneable接口的时候,抛出该异常,而且在方法重写的时候可能会抛出该异常
|—IllegalAccessException(1.0):[->Exception]
|:当应用程序在使用反射的时候创建一个实例、设置或者获取一个字段、调用一个方法的时候,正在执行的方法无法访问指定的类、方法、字段或构造方法的时候抛出该异常
【*:在书写继承树的时候,如果该类不属于该介绍包里面的内容这里使用全称】
2)java.net包:
Exception(1.0):
|—URISyntaxException(1.4):[->Exception]
|:抛出经过检查的指示字符串不能解析为URI引用的异常
|—java.io.IOException(1.0):[->Exception]【后边会介绍】
|—SocketTimeoutException(1.4):[->java.io.InterruptedIOException->java.io.IOException->Exception]
|:如果在读取或者接受套子节的时候发生超时,则抛出此异常
|—UnknownServiceException(1.0):[->java.io.IOException->Exception]
|:抛出这一异常指示出现了未知的服务异常,可能是URL连接返回的MIME类型无意义,或者应用程序试图进行对只读的URL连接写入。
|—UnknownHostException(1.0):[->java.io.IOException->Exception]
|—指示主机IP地址无法确定而抛出的异常
|—ProtocolException(1.0):[->java.io.IOException->Exception]
|:抛出此异常指示在底层协议中存在错误,如TCP错误。
|—HttpRetryException(1.5):[->java.io.IOException->Exception]
|:抛出此异常指示HTTP请求需要重试,但由于启用了流模式不能自动重拨,重点是进行HTTP请求的重试操作
|—MalformedURLException(1.0):[->java.io.IOException->Exception]
|:抛出这一串异常指示出现了错误的URL地址,或者说在进行网络访问的时候在字符串里面找不到任何合法协议,或者无法解析传入的字符串,URI不合法等
|—SocketException(1.0):[->java.io.IOException->Exception]
|:抛出此异常指示在底层协议中存在错误,如TCP错误。
|—NoRouteToHostException(1.1):[->SocketException->java.io.IOException->Exception]
|:试图将套接字连接到远程地址和端口时发生错误的情况下,抛出此异常。通常为无法到达远程主机,原因是防火墙干扰或者中间路由器停机。
|—PortUnreachableException(1.4):[->SocketException->java.io.IOException->Exception]
|:在连接的数据报上已接收到 ICMP Port Unreachable消息时,抛出该异常。
|—BindException(1.1):[->SocketException->java.io.IOException->Exception]
|:试图将套字节绑定到本地地址和端口的时发生错误的情况下,抛出该异常,这些错误通常发生在端口正在使用中或者无法分配所请求的本地地址的时候抛出
|—ConnectException(1.1):[->SocketException->java.io.IOException->Exception]
|:试图将套字节连接到远程地址和端口时发生错误的情况下,抛出该异常,这些错误通常发生情况为拒绝远程连接的时候,例如远程地址/端口上没有进行侦听操作
3)java.io包:
Exception(1.0):
|—IOException(1.0):[->Exception]
|:当发生某种I/O异常的时候,抛出该异常,此类是失败或中断的I/O操作生成的异常通用类
|—CharConversionException(1.1):[->IOException->Exception]
|:当字符进行转换的时候发生的异常的基类
|—EOFException(1.0):[->IOException->Exception]
|:当输入过程中意外到达文件或流的末尾时,抛出此异常,此异常主要用于被数据输入流用来表明到达了流的末尾。有时候很多输入操作是返回了某个特殊值而不是抛出该异常
|—FileNotFoundException(1.0):[->IOException->Exception]
|:在不存在具有指定路径名的文件时,此异常将由FileInputStream、FileOutputStream和RandomAccessFile构造方法抛出。如果该文件存在,但是由于某些原因不可访问,比如试图打开一个只读文件进行写入,则此时这些构造方法仍然会抛出该异常。
|—InterruptedIOException(1.0):[->IOException->Exception]
|:I/O 操作已中断信号,抛出此异常。抛出 InterruptedIOException 指示输入或输出传输已经终止,原因是执行此操作的线程中断。字段bytesTransferred指示在发生中断之前已成功传输了多少字节。
|—ObjectStreamException(1.1):[->IOException->Exception]
|:特定于ObjectStream类的所有异常的超类。
|—ObjectStreamException(1.1):[->IOException->Exception]
|—InvalidClassException(1.1):[->ObjectStreamException->IOException->Exception]
|:当Serialization运行时检测到某个类具有以下几个问题的时候,抛出该异常
[1]该类的序列版本号与从流中读取的类描述符的版本号不匹配
[2]该类包含未知数据类型
[3]该类没有可访问的无参数构造方法
|—InvalidObjectException(1.1):[->ObjectStreamException->IOException->Exception]
|:指示一个或多个反序列化对象未通过验证测试,该参数应该提供失败的原因
|—NotActiveException(1.1):[->ObjectStreamException->IOException->Exception]
|:当序列化和反序列化不活动的时,抛出该异常
|—NotSerializableException(1.1):[->ObjectStreamException->IOException->Exception]
|:当实例需要具有序列化接口时,抛出此异常。序列化运行时或实例的类会抛出此异常。参数应该为类的名称。
|—OptionalDataException(1.1):[->ObjectStreamException->IOException->Exception]
|:指示对象读取操作失败的异常,原因是无法读取流中的基本数据或已序列化对象的数据末尾。以下两种情况都可能抛出此异常:
[1]流中下一个元素是基本数据时试图读取对象。在这种情况下,OptionalDataException的length字段设置成了可从流中直接读取的基本数据的字节数,而eof字段设置成了false。
[2]试图使用类定义的 readObject 或 readExternal 方法读取数据末尾的后面。在这种情况下,OptionalDataException 的eof字段设置成了true,而length字段设置成了0。
|—StreamCorruptedException(1.1):[->ObjectStreamException->IOException->Exception]
|:当从对象流中读取的控制信息与内部一致性检查相冲突时,抛出此异常。
|—WriteAbortedException(1.1):[->ObjectStreamException->IOException->Exception]
|:在写入操作过程中抛出ObjectStreamExceptions之一时,在读取操作过程中将抛出此异常。终止写入的异常可在详细信息字段中找到。该流被重置为初始状态,而且对已经反序列化的对象的所有引用都被丢弃。
|—SyncFailedException(1.1):[->IOException->Exception]
|:当sync操作失败时,抛出该异常
|—UnsupportedEncodingException(1.1):[->IOException->Exception]
|:当使用的字符编码不支持的时候会抛该异常,一般在进行字符串编码的时候会出现该异常
|—UTFDataFormatException(1.0):[->IOException->Exception]
|:在数据输入流中或由实现该数据输入接口的任何类中以UTF-8修改版格式读取错误字符串时,抛出此异常。有关读取和写入UTF-8修改版字符串的格式。
4)java.text包:
Exception(1.0)
|—ParseException(1.0):[-Exception]
|:解析的时候出现以外错误的时候就会抛出该异常,而且该包里面只有这个异常
5)java.util包:
RuntimeException(1.0):
|—ConcurrentModificationException(1.2):[->RuntimeException]
|:当方法检测到对象的并发修改,但不允许这种修改时,抛出此异常。需要注意的是:
[1]此异常不会始终指出对象已经由不同线程并发修改。如果单线程发出违反对象协定的方法调用序列,则该对象可能抛出此异常。
[2]迭代器的快速失败行为无法得到保证,因为一般来说,不可能对是否出现不同步并发修改做出任何硬性保证。快速失败操作会尽最大努力抛出ConcurrentModificationException。因此,为提高此类操作的正确性而编写一个依赖于此异常的程序是错误的做法。
[3]某个线程在 Collection 上进行迭代时,通常不允许另一个线性修改该Collection。通常在这些情况下,迭代的结果是不确定的。如果检测到这种行为,一些迭代器实现(包括 JRE 提供的所有通用 collection 实现)可能选择抛出此异常。执行该操作的迭代器称为快速失败迭代器,因为迭代器很快就完全失败,而不会冒着在将来某个时间任意发生不确定行为的风险。
|—EmptyStackException(1.0):[->RuntimeException]
|:该异常由Stack类中的方法抛出,以表明堆栈为空
|—java.lang.IllegalArgumentException(1.5):[->RuntimeException]【前边已经讲过该类了】
|—IllegalFormatException(1.5):[->IllegalArgumentException->RuntimeException]
|:当格式字符串包含非法语法,或者包含与给定参数不兼容的格式说明符时,将抛出未经检查的异常。只应当实例化此异常对应于具体错误的显式子类型。
|—DuplicateFormatFlagsException(1.5):[->IllegalFormatException->IllegalArgumentException->RuntimeException]
|:格式说明符中提供重复标志时抛出的未经检查的异常。
|—FormatFlagsConversionMismatchException(1.5):[->IllegalFormatException->IllegalArgumentException->RuntimeException]
|:转换与标志不兼容时抛出未经检查的异常。
|—IllegalFormatCodePointException(1.5):[->IllegalFormatException->IllegalArgumentException->RuntimeException]
|:将具有Character.isValidCodePoint(int)所定义的无效Unicode代码点的字符传递给Formatter时,抛出未经检查的异常。
|—IllegalFormatConversionException(1.5):[->IllegalFormatException->IllegalArgumentException->RuntimeException]
|:当对应于格式说明符的参数为不兼容的类型时,抛出未经检查的异常。
|—IllegalFormatFlagsException(1.5):[->IllegalFormatException->IllegalArgumentException->RuntimeException]
|:当给出非法组合标志时,抛出未经检查的异常。
|—IllegalFormatPrecisionException(1.5):[->IllegalFormatException->IllegalArgumentException->RuntimeException]
|:当精度为除-1以外的负值、转换类型不支持某个精度或者值在其他方面不受支持时,将抛出未经检查的异常。
|—IllegalFormatWidthException(1.5):[->IllegalFormatException->IllegalArgumentException->RuntimeException]
|:当格式宽度为除-1以外的负值或其他不受支持的值时,将抛出未经检查的异常。
|—MissingFormatArgumentException(1.5):[->IllegalFormatException->IllegalArgumentException->RuntimeException]
|:如果格式说明符没有相应的参数,或者参数索引引用了不存在的参数时,则抛出未经检查的异常。
|—MissingFormatWidthException(1.5):[->IllegalFormatException->IllegalArgumentException->RuntimeException]
|:请求格式宽度时抛出未经检查的异常。
|—UnknownFormatConversionException(1.5):[->IllegalFormatException->IllegalArgumentException->RuntimeException]
|:给定未知的转换时所抛出的未经检查的异常。
|—UnknownFormatFlagsException(1.5):[->IllegalFormatException->IllegalArgumentException->RuntimeException]
|:给定未知标志时所抛出的未经检查的异常。
|—MissingResourceException(1.1):[->RuntimeException]
|:缺少资源时抛出此异常。
|—java.lang.IllegalStateException(1.5):[->RuntimeException]【前边已经讲过该类了】
|—FormatterClosedException(1.5):[->java.lang.IllegalStateException->RuntimeException]
|:格式器已关闭时抛出的未经检查的异常。
|—NoSuchElementException(1.0):[->RuntimeException]
|:由Enumeration的nextElement方法抛出,表明枚举中没有更多的元素。
|—InputMismatchException(1.5):[->NoSuchElementException->RuntimeException]
|:由Scanner抛出,表明获取的标记与期望类型的模式不匹配,或者该标记超出期望类型的范围。
Exception(1.0)
|—TooManyListenersException(1.1):[->Exception]
|:TooManyListenersException异常用作Java Event模型的一部分来注释和实现多播Event Source的单播特例。
|—java.io.IOException(1.0):[->Exception]【前边已经讲过】
|—InvalidPropertiesFormatException(1.5):[->IOException->Exception]
|:当按照Properties规范,输入内容不符合属性集的正确XML文档类型,从而无法完成操作时,抛出此异常。
6)整理下来的异常树快照为:【异常列表:java.io、java.net、java.lang、java.text、java.util。】
java.lang.Exception(1.0):
|—java.lang.RuntimeException(1.0)
|—java.lang.ArithmeticException(1.0)
|—java.lang.ArrayStoreException(1.0)
|—java.lang.ClassCastException(1.0)
|—java.lang.EnumConstantNotPresentException(1.5)
|—java.lang.IllegalArgumentException(1.0)
|—java.lang.IllegalThreadStateException(1.0)
|—java.lang.NumberFormatException(1.0)
|—java.util.FormatterClosedException(1.5)
|—java.util.IllegalFormatException(1.5)
|—java.util.DuplicateFormatFlagsException(1.5)
|—java.util.FormatFlagsConversionMismatchException(1.5)
|—java.util.IllegalFormatCodePointException(1.5)
|—java.util.IllegalFormatConversionException(1.5)
|—java.util.IllegalFormatFlagsException(1.5)
|—java.util.IllegalFormatPrecisionException(1.5)
|—java.util.IllegalFormatWidthException(1.5)
|—java.util.MissingFormatArgumentException(1.5)
|—java.util.MissingFormatWidthException(1.5)
|—java.util.UnknownFormatConversionException(1.5)
|—java.util.UnknownFormatFlagsException(1.5)
|—java.lang.NegativeArraySizeException(1.0)
|—java.lang.IndexOutOfBoundsException(1.0)
|—java.lang.ArrayIndexOutOfBoundsException(1.0)
|—java.lang.StringIndexOutOfBoundsException(1.0)
|—java.lang.NullPointerException(1.0)
|—java.lang.SecurityException(1.0)
|—java.lang.TypeNotPresentException(1.5)
|—java.lang.UnsupportedOperationException(1.2)
|—java.util.ConcurrentModificationException(1.2)
|—java.util.EmptyStackException(1.0)
|—java.util.MissingResourceException(1.1)
|—java.util.NoSuchElementException(1.0)
|—java.util.InputMismatchException(1.5)
|—java.lang.InstantiationException(1.0)
|—java.lang.NoSuchFieldException(1.1)
|—java.lang.NoSuchMethodException(1.0)
|—java.lang.InterruptedException(1.0)
|—java.lang.ClassNotFoundException(1.0)
|—java.lang.CloneNotSupportedException(1.0)
|—java.lang.IllegalAccessException(1.0)
|—java.lang.URISyntaxException(1.4)
|—java.io.IOException(1.0)
|—java.util.InvalidPropertiesFormatException(1.5)
|—java.io.CharConversionException(1.1)
|—java.io.EOFException(1.0)
|—java.io.FileNotFoundException(1.0)
|—java.io.InterruptedIOException(1.0)
|—java.io.ObjectStreamException(1.1)
|—java.io.InvalidClassException(1.1)
|—java.io.InvalidObjectException(1.1)
|—java.io.NotActiveException(1.1)
|—java.io.NotSerializableException(1.1)
|—java.io.OptionalDataException(1.1)
|—java.io.StreamCorruptedException(1.1)
|—java.io.WriteAbortedException(1.1)
|—java.io.SyncFailedException(1.1)
|—java.io.UnsupportedEncodingException(1.1)
|—java.io.UTFDataFormatException(1.0)
|—java.net.UnknownServiceException(1.0)
|—java.net.UnknownHostException(1.0)
|—java.net.ProtocolException(1.0)
|—java.net.HttpRetryException(1.5)
|—java.net.MalformedURLException(1.0)
|—java.net.SocketException(1.0)
|—java.net.NoRouteToHostException(1.1)
|—java.net.PortUnreachableException(1.4)
|—java.net.BindException(1.1)
|—java.net.ConnectException(1.1)
|—java.text.ParseException(1.0)
|—java.util.TooManyListenersException(1.1)
这里对Checked Exception做个简单补充,这种“可检查异常”本身具有这样一些特性:
[1]该异常发生过后,是可以恢复的,比如当使用Swing界面去读取一个文件的时候,路径里面的文件不存在换一个路径继续读取
[2]的程序运行的时候有时候会对它运行的环境有依赖性,这种依赖是程序本身不可估计的,这里针对程序环境的依赖条件是允许出错的,比如最常见的IO问题和网络问题
[3]该异常并不会导致程序错误不能运行,进行一些适当的内部处理过后,该异常可以继续进行后续的操作
【*:也就是说,在使用Java异常机制的时候,往往考虑得最多的就是针对CheckedException的设计,因为这种异常往往不是由于程序本身造成的,而是由于程序所运行的环境造成的,可以这样理解,CheckedException的产生很多时候都是由于客观条件产生的,对程序本身而言,只是针对例外的客观环境进行了一种处理方式的编写,并不是程序本身存在什么问题,所以这种异常在设计的时候是需要考虑的。例如前边说的:当的程序运行的时候从网上下东西,突然网线被拔了,那么该程序不会终止,因为有个异常抛出,折中异常可以通知程序这会儿网络不通,那么程序针对这种情况就会采取一定的处理措施。】
iv.自定义异常
当然也可以在项目开发过程设计一个属于自己的异常类,其定义方式用以下一段代码来说明:
class DreadfulProblemException extends ArithmeticException {
public DreadfulProblemException() {
}
public DreadfulProblemException(String s) {
super(s);
}
}
public DreadfulProblemException() {
}
public DreadfulProblemException(String s) {
super(s);
}
}
public class MainClass
{
public static void main(String args[])
{
int[] array = new int[]{1,0,2};
int index = 0;try
{
System.out.println("First try block in divide() entered");
array[index + 2] = array[index]/array[index + 1];
System.out.println("Code at end of first try block in divide()");
System.out.println("First try block in divide() entered");
array[index + 2] = array[index]/array[index + 1];
System.out.println("Code at end of first try block in divide()");
}
catch(ArithmeticException e)
{
System.out.println("Arithmetic exception caught in divide()");
throw new DreadfulProblemException("index + 1"); // Throw new exception
}
System.out.println("Arithmetic exception caught in divide()");
throw new DreadfulProblemException("index + 1"); // Throw new exception
}
catch(ArrayIndexOutOfBoundsException e)
{
System.out.println("Index-out-of-bounds index exception caught in divide()");
}
System.out.println("Executing code after try block in divide()");
System.out.println("Index-out-of-bounds index exception caught in divide()");
}
System.out.println("Executing code after try block in divide()");
}
}
以上的代码就定义了一个属于程序自身的异常类,这种用法在开源框架的开发和设计中比较普遍,就像在使用Hibernate和Spring的时候经常会遇到类似HibernateException以及其他相关异常,这种异常都是其框架在设计和开发过程中的自定义异常。
【*:一般情况在开发过程中最好使用自定义的异常结构,异常本身的命名行为使用与业务有关的异常行为,这种设计方式比较规范,而且便于自己进行管理,在提供了规范文档的前提下,使用自定义异常是开发产品必不可少的步骤,而项目开发,根据客观情况而定,关于异常的使用心得和开发我在下边一个章节进行说明。】
2.异常处理心得
i.关于异常的一些开发心得总结:
——◆编程心得[1]:针对不正常条件使用异常机制——
针对这点先看一段简单的代码:
/**
*滥用异常的概念说明代码
**/
public class ExceptionUse
{
public static void main(String args[]) throws Exception
{
List<String> testList = new ArrayList<String>();
List<String> resultList = new ArrayList<String>();
try
{
int index = 0;
while(true){
String tempString = new String(testList[index++].getBytes("UTF-8"),"GB2312");
System.out.println(tempString);
resultList.add(tempString);
}
// ...针对resultList的一些相关操作
}
catch(ArrayIndexOutOfBoundException ex)
{
ex.printStackTrace();
}
}
}
上边这段代码是为了针对某个字符串数组里面的每一个字符串进行转码的操作,将一个UTF-8编码的字符串列表转化成为GB2312的字符串列表,这样直接看起来这段代码是没有问题的,但是这段代码却遇到了一个很致命的概念问题,怎么讲呢?仔细注意上边这段代码,注意语句while(true)这句话,这是一个检测数组越界的条件,这种检测方式在整整运行的时候是不会有问题的,因为一旦数组越界就会通过异常的方式检测出来,但是这是一个很不良好的做法。很多时候依赖JVM的异常来辅助进行业务的数据规范,实际上这样的做法是很不合理的,但是为什么有时候会优先使用基于异常的方式,而不使用比较可靠和可行的编程模式?这样做其实是在编程过程养成了一个依赖心理,JVM确实提供了很多异常机制可以直接检测的一些数据是否合法,但是这种做法应该是不被提倡的。主要原因在于:
[1]异常机制的设置应该是JVM编程里面的最后一道防线,最初目的就是为了用于不正常的情况,所以很多时候JVM实现是不会对这样的方式进行优化的,这样的方式来检测业务数据的开销是很昂贵的;
[2]把代码块放在try-catch里面对于JVM本身运行而言就已经阻止了某些优化操作了,异常的目的是防止出错,而这种做法是故意去依赖这种错误;
实际上,去分析JVM内部原理就可以发现,基于异常的运行模式本身比标准模式要慢很多。这段代码仅仅说明了一个问题:异常只能应用于不正常条件,在正常模式里面不应该使用异常来参与业务流程的运行,即在正常控制流里面不可以使用异常来做辅助操作。从整个系统的设计上讲,良好的API设计不应该强迫它的运行时为了正常的控制流程而使用异常。
以下这种情况也是不提倡的:
public class ExceptionUseTwo
{
// ……定义一些类里面的方法
public static void main(String args[])
{
String temp = getInputString();//从某个定义的方法里面去读取输入
try
{
Integer tempNumber = Integer.parseInteger(temp);
// 继续往下走
}
catch(NumberFormatException ex)
{
System.out.println("Sorry,the value of input is not a number!");
}
}
}
上边使用了NumberFormatException的异常检测来检测输入是否是个数字,这种方式其实最好的方式是使用正则表达式来检测,而不是通过这样的方式来检测输入格式是否正常。
——◆编程心得[2]:合理使用CheckedException和RuntimeException——
从上边的讲述可以知道,JVM里面的异常机制本身分为三种:Error、CheckedException和RuntimeException,对于Error不用去考虑使用问题,因为这种情况往往是不可恢复的情况,可以不作理睬。在考虑使用一个CheckedException或者是使用一个RuntimeException的时候,主要原则在于:如果期望调用者能够恢复,对于这种异常考虑使用CheckedException;这种情况下,可以直接抛出一个异常用catch来处理或者直接使用throw语句将这个异常往外层抛出。对于这样的异常,一旦捕捉到了过后需要对其进行一定的处理操作,但是这种异常在设计的时候最好不要忽略掉。在编程过程最容易忽略的异常就是NullPointerException,做过开发的人都能够感受,经常调试的工作遇到的就是NullPointerException这个异常。
那么如何合理使用RuntimeException呢?一种典型的做法就是:用RuntimeException来指明程序错误!大多数运行时异常都是一种前提违例【Precondition Violation】,这种方式指代的是在编写程序库以及一些API的时候没有去遵循某种约定,比如:NumberFormatException一旦出现了表明的是程序在运行的时候违背了这样一种约定。虽然很多时候,正规编程没有强制性要求,但是按照惯例,这种错误往往会被JVM保留下来,比如资源不足或者出现了约束越界等各种问题。所以按照的通常对于JVM异常机制的运用,有一种常见的约定:所实现的只要是UnCheckedException的结构都应该是JVM里面定义的RuntimeException的子类。
【*:可以仔细地思考一下,JVM本身在设计异常体系结构的时候是经过了严格思考的,可以这样讲,CheckedException的出现的本身目的是为了检测内部的一些问题,这种问题是通过编译器检测出来的。而RuntimeException却是定义了程序使用的一种域,这种域的实则是告诉在编程过程尽量使得自己不要去触犯这种域定义的规则。这些异常的定义是在于防止触犯这种规则,在运行时的过程里面一旦的程序触犯了这种规则,就会被JVM进行RuntimeException的指示。而Error本身是系统出现不可恢复错误,所以这种情况是不能够被处理的,这种情况只能对程序说抱歉,进行重新设计。合理使用JVM里面提供的两种错误是程序设计很好的方法。】
从概念上讲CheckedException往往指代的是可以恢复的错误,所以在处理这种异常的时候往往提供相关的辅助函数来操作,通过这些操作,可以针对不同的CheckedException来进行相关处理操作。对于一个软件系统本身而言,可以恢复的异常是值得深思的,从现实生活的角度上讲,如果你去寻找一个人,打了电话,而对方没有接通,事情很着急,最好的办法是找个时间重播一次,这种做法跟网络连接异常SocketException的做法有点类似,而且这种做法应该是软件本身应该被允许的。
——◆编程心得[3]:尽量使用标准异常——
软件工程有一个核心的概念就是“软件复用”,在设计系统的时候往往会从细处考虑到代码重用问题。代码复用本身应该是值得提倡的,否则就不会进行模块化设计和接口设计了,而在处理一些系统异常本身的时候,为了保证一定的通用性个规范性,尽量采取官方提供的标准异常。Java的异常定义里面,有很多基本的CheckedException,它已经从概念上覆盖了很多需要的异常类型,这种情况下尽量采取“复用技术”。
这里提供在使用标准库里面比较常用的异常快照,虽然不完整:
异常 | 使用场合 |
IllegalArgumentException | 传入的参数的值不合适 |
IllegalStateException | 对于这个方法的调用,对象状态有问题 |
NullPointerException | 在null被禁止的情况下参数值为null |
IndexOutOfBoundException | 越界 |
ConcurrentModificationException | 禁止并发修改,被对象检测到的 |
UnsupportedOperationException | 对象不支持某种操作 |
所以,在程序开发过程中,尽量使用标准异常!
——◆编程心得[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连接关闭、网络连接关闭、对象回收等