5.Java中的异常、断言、日志【草稿上】

(本章主要讲解Java里面比较核心的一块内容——异常处理,Java异常处理机制,一致都是比较复杂的一块,而很多时候我们如果写程序的时候能够适当地注意对应的一些异常处理情况,那么我们就会在开发过程节省一大部分时间,最常见的情况就是辅助我们进行调试以及维护工作以及提高系统的容错性和稳定性。这一章和前边类和对象章节不一样,这一章可能涵盖的内容没有前边那章多,但是我会尽量保证在整篇文章里面把开发过程中需要注意到的与异常有关的细节问题以及对应的开发经验写入本文,而本章的出发点是异常处理,核心内容还涵盖了测试、调试、以及部署等相关内容以及。如果有笔误的地方,请来Email指点:[email protected],谢谢)

本章目录
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语言里面,使用trycatch来实现,当JVM碰到这个语句块的时候,它会监听整个Java程序,一旦出现了任何异常情况,它会将整个程序的执行权交给catch块来执行。
  先看一段简单的代码:
importjava.io.File;
importjava.io.FileReader;
/**
*一个简单的文件操作
**/
public classCustomerFileReader
{
public static voidmain(Stringargs[])
{
File file =newFile("C:/read.txt");
FileReader reader =newFileReader(file);//这句话不能通过JVM的编译器
}
}
  上边这段代码如果使用的是javac的命令,那么编译器就会报错,可能错误信息如下:
unreported exception java.io.FileNotFoundException; must be caught or declared to be thrown
  那么如果要保证上边这句代码是能够通过编译的,如何简单修改呢,有两种方式:
  [1]加入try和catch块,使用JVM的异常体系结构去捕捉该异常;
  [2]直接throws这个异常,让程序显式抛出该异常
  2)Java里面的异常体系结构:
  先看下边的异常体系结构图:
5.Java中的异常、断言、日志【草稿上】
  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 classRunExpTester
{
public static voidmain(Stringargs[])
{
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 classAssertionErrorTester
{
public static voidmain(Stringargs[])
{
try
{
assertargs.length < 0:"Args Length Error!";
}
catch(AssertionError e)
{
Stringmessage = 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 classExceptionTester
{
public static voidmain(Stringargs[])
{
//这一块被成为异常的正常执行块,也就是如果没有异常抛出的话,try块会一直这样正常执行到最末位
try 
{
//判断输入参数的长度
if( args.length == 0 )
{
System.out.println("Args length is zore");
}else{
StringinputString = 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块里面的内容是不会执行的,但是这种做法不提倡。】
  2)throw、throws关键字:
importjava.io.File;
importjava.io.FileReader;
importjava.io.IOException;
/**
*提供throw和throws关键字的代码块
**/
public classThrowInstance
{
public static voidmain(Stringargs[])throwsException
{
try
{
readFile("C:/read.txt");
}
catch(IOException ex)
{
throw newException(ex);
}
}

public static voidreadFile(Stringpath)throwsIOException
{
File file =newFile(path);
FileReader reader =newFileReader(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 classTryCatch
{
public static voidmain(Stringargs[])
{
try
{
//正常执行语句块
}
catch(Exception ex)
{
//抛出异常过后的异常捕捉语句块,捕捉到异常了就执行
}
}
}
  这种语句块的执行流程为:运行try块中的代码,如果有异常抛出,就会转到catch语句块中执行,当然前提是catch中的异常类型和try块中抛出的异常类型匹配
  try+catch+finally
/**
*try+catch+finally语句块
**/
public classTryCatch
{
public static voidmain(Stringargs[])
{
try
{
//正常执行语句块
}
catch(Exception ex)
{
//抛出异常过后的异常捕捉语句块,捕捉到异常了就执行
}
finally
{
//统一出口代码块
}
}
}
  这种结构的语句执行流程为:
  先运行try语句块中的语句,如果有异常抛出,则转入catch语句块中执行,执行完毕过后,就执行finally语句块里面的代码,然后再执行finally后边的语句块;如果没有异常抛出,执行完try语句块过后,就执行finally块中的语句,然后再执行finally语句块后边的内容。
  try+finally
/**
*try+finally语句块
**/
public classTryCatch
{
public static voidmain(Stringargs[])throwsException
{
try
{
//正常执行语句块
}
finally
{
//统一出口代码块
}
}
}
  该语句块的执行流程为:
  先运行try语句块中的语句,如果有异常抛出,程序转入finally块里面执行finally块中的代码,这种情况与上边不同的是,执行完finally语句块里面的内容过后程序会以异常的方式抛出,不会继续执行finally语句块后边的内容,而这种情况需要注意一点就是代码里面的红色部分,因为没有执行异常捕捉,该情况需要从方法声明里面抛出对应的异常。
  4)在这三种结构里面需要注意以下几个问题
  简单总结几点:
  [1]try、catch、finally三个语句块均不能单独使用,三者可以组成try+catchtry+catch+finallytry+finally三种结构,catch语句可以有一个或多个,而且都处于平级,finally语句最多一个
  [2]try、catch、finally三个代码块中变量的作用域为代码块内部,即变量作用于为局部变量作用域,分别独立而不能相互访问。如果要在三个块中都可以访问,则需要将变量定义到这些块的外面,即在{}之外声明
  [3]多个catch块时候,只会匹配其中一个异常类并执行catch块代码,而不会再执行别的catch块,并且匹配catch语句的顺序是由上到下,在声明过程中子类需要放在前边捕捉
  至于throwthrows关键字,这里再摘录网上一篇BLOG里面的说明:
  throw关键字是用于方法体内部,用来抛出一个Throwable类型的异常。如果抛出了检查异常,则还应该在方法头部声明方法可能抛出的异常类型,该方法的调用者也必须检查处理抛出的异常。如果所有方法都层层上抛获取的异常,最终JVM会进行处理,处理也很简单,就是打印异常消息和堆栈信息。如果抛出的是Error或RuntimeException,则该方法的调用者可选择处理该异常。
  throws关键字用于方法体外部的方法声明部分,用来声明方法可能会抛出某些异常。仅当抛出了检查异常,该方法的调用者才必须处理或者重新抛出该异常。当方法的调用者无力处理该异常的时候,应该继续抛出,而不是囫囵吞枣一般在catch块中打印一下堆栈信息做个勉强处理。
  最终补充一个复杂的概念说明代码:
importjava.io.File;
importjava.io.FileNotFoundException;
importjava.io.FileReader;
importjava.io.IOException;

/**
* 关于异常的概念说明代码
*/
public classExceptionTester {
public static voidmain(Stringargs[])throwsIOException{
try{
processException();
}
finally{
System.out.println("Main body");
}
}

public static voidprocessException()throwsIOException{
try{
System.out.println("Inner Try");
File file =newFile("C:/read.txt");
FileReader reader =newFileReader(file);
}catch(IOException ex){
System.out.println("Inner Exception Sub");
throw newIOException();
}catch(Exception ex){