在程序运行期间,由于程序的错误或者环境的影响造成用户的数据丢失等,为了避免这些错误的发生,系统应该做到以下几点:
异常——因为错误的输入或者网络的连接出现问题等;
断言——在测试期间,需要各种检测以验证程序操作的正确性。然而在测试程序时,这些检测将会浪费特别多的时间,为了方便程序的测试,略过各种铺垫性的检测程序,引入断言;
日志——当程序出现错误的时候,将错误的信息记录下来,方便用户的检查。
异常处理的任务是将控制权从错误产生的地方转移到能够处理这种情况的错误处理器。
常见的异常情况:
传统的方法是返回一个特殊的错误码,由调用方法分析。但是并不所有的情况都适合返回一个错误码,在这种情况下,方法并不返回任何值,而是抛出(throw)一个封装了错误信息的对象。并且这个方法将会立即退出,调用这个方法的其他方法也将无法继续执行。取而代之的是,异常处理机制开始搜索能够处理这种异常情况的异常处理器(exception handler)。
异常具有自己的语法和特定的继承结构。
在Java中,异常对象都是派生于Throwable类的一个实例。如果Java内置的异常类不能满足需求,用户还可以创建自己的异常类,该异常类要继承于Exception类。
Throwable: 有两个重要的子类:Exception(异常)和 Error(错误),二者都是 Java 异常处理的重要子类,各自都包含大量子类。异常和错误的区别是:异常能被程序本身处理,错误则无法处理。
error类层次结构描述了Java运行时系统内部的错误和资源的耗尽错误。Error是程序无法处理的错误,大多数错误与代码编写者执行的操作无关,而表示代码运行时 JVM(Java 虚拟机)出现的问题。
exception类分两大类:运行时异常和非运行时异常(编译异常)。程序中应当尽可能去处理这些异常。
运行时异常:RuntimeException及其子类,这些异常是不检查异常,程序中可以选择捕获处理,也可以不处理。这些异常一般是由程序逻辑错误引起的,程序应该从逻辑角度尽可能避免这类异常的发生。运行时异常的特点是Java编译器不会检查它,也就是说,当程序中可能出现这类异常,即使没有用try-catch语句捕获它,也没有用throws子句声明抛出它,也会编译通过,但是运行时会报错。
编译时异常:是RuntimeException以外的异常,类型上都属于Exception类及其子类。是指编译器要求必须处置的异常。即程序在运行时由于外界因素造成的一般性异常。 编译器要求Java程序必须捕获或声明所有编译时异常。对于这类异常,如果程序不处理,可能会带来意想不到的结果。
JAVA语言规范将派生于Error类和RuntimeException类的所有异常类都称为非受查(unchecked)异常。其他的异常成为受查异常(checked)。
- 受查异常:必须要捕获的异常,即强制要求开发人员在代码中进行显式的声明(throw)和捕获(try/catch),否则无法编译通过
- 非受查异常:如果不捕获异常,不会出现编译错误,而该异常会在运行时打印日志
常见的做法是自定义一个BaseException作为“根异常”,然后,派生出各种业务类型的异常。
在使用时,使用throw抛出。
BaseException需要从一个适合的Exception派生,通常建议从RuntimeException派生,并提供多个构造方法:
public class BaseException extends RuntimeException { public BaseException() { super(); } public BaseException(String message, Throwable cause) { super(message, cause); } public BaseException(String message) { super(message); } public BaseException(Throwable cause) { super(cause); } } //其他业务类型的异常从BaseException再继续派生
注:RuntimeException这个可以抛出异常,并准确定位,缺点是不能处理这个异常,自定义异常的话可以捕获并且处理,如可以使用 throw new GuliException(10000,”不存在”),创建一个自定义异常对象,交由自定义异常类来进行处理。
在接口开发的过程中,为了程序的健壮性,我们经常考虑到代码执行的异常,并给前端一个友好的展示,这里就用到自定义异常,继承RuntimeException类。这个RuntimeException和普通的Exception有什么区别呢?
(1)Exception:非运行时异常,在项目运行之前必须处理掉。一般由程序员 try…catch掉。
(2)RuntimeException:运行时异常,在项目运行之后出错则直接终止运行,异常由JVM虚拟机处理。
在接口的逻辑判断出现异常时,可能会影响后面的代码,或者说绝不允许该代码块出错,那么我们就用RuntimeException,但是我们又需要在系统挂掉后,返回给前端出现异常的原因。因此接口的自定义异常作用就体现了出来。
根据异常规范,在方法的首部必须声明该方法可能抛出的所有异常。每个异常类之间用逗号分开。但是不需要声明Java的内部错误,即从error继承的错误,也不能声明从RuntimeException继承的那些非受查异常。
一个方法必须声明所有可能抛出的受查异常(IOException),而非受查异常要么不可控制(error),要么就应该避免发生(RuntimeException)。
如果在子类中覆盖了超类的一个方法,子类方法中声明的受查异常不能比超类方法中声明的异常更通用,即子类中的可以抛出更特定的异常,或者不用抛出任何异常。但是若超类方法没有抛出任何受查异常,子类也不能抛出任何异常。JAVA中的throws说明符与C++中的throw说明符基本类似。
区别:
在C++中,throw说明符在运行时执行,而不是在编译时执行,即C++编译器将不处理任何异常规范;
在Java中,如果方法没有声明所有可能发生发生的受查异常,编译器将会发出一个错误消息;
在C++中,如果没有给出throw声明,函数可能会抛出任何异常;
在Java中,没有throw说明符的方法将不能抛出任何受查异常;
在C++中,抛出的可以是任何类型的值;
在Java中,只能抛出Throwable子类的对象;
当某个方法抛出了异常时,如果当前方法没有捕获异常,异常就会被抛到上层调用方法,直到遇到某个try … catch被捕获为止:
public class Main { public static void main(String[] args) { try { process1(); } catch (Exception e) { e.printStackTrace(); } } static void process1() { process2(); } static void process2() { Integer.parseInt(null); // parseInt()会抛出NumberFormatException } }
1)找到一个合适的异常类,也可自定义派生于Exception的异常类
2)创建这个类的一个对象
3)用throw语句将对象抛出(被调用的方法一般已经实现了throw异常)如文件读取时的异常:
public static void main(String[] args) { String s = "abc"; if(s.equals("abc")) { throw new NumberFormatException(); // 使用throw抛出异常 } else { System.out.println(s); } //function(); }
4)使用throws在方法头声明异常,也可实现对异常的抛出,将异常交由上层处理函数处理。
如果一个方法捕获了某个异常后,又在catch子句中抛出新的异常,就相当于把抛出的异常类型“转换”了:
public class Main { public static void main(String[] args) { try { process1(); } catch (Exception e) { e.printStackTrace(); } } static void process1() { try { process2(); } catch (NullPointerException e) { throw new IllegalArgumentException(); } } static void process2() { throw new NullPointerException(); } }
当process2()抛出NullPointerException后,被process1()捕获,然后抛出IllegalArgumentException(),然后在main()中捕获的是IllegalArgumentException异常。这说明新的异常丢掉了原始的异常,即实现了”转换“异常。
若要跟踪到原始的异常,则需要在抛出新的异常的时候,把原始异常的对象存入到新异常中:
static void process1() { try { process2(); } catch (NullPointerException e) { throw new IllegalArgumentException(e); //保存原始异常对象e } }
如果某个异常发生的时候没有任何地方进行捕获,那程序就会终止执行,并在控制台打印异常信息;
要想捕获一个异常,必须设置try/catch语句块;
若在try语句块中任何代码抛出了一个在catch子句说明的异常类,那么
1)程序将跳过try语句块的其他代码
2)程序将执行catch子句中的处理器代码
3)如果抛出了一个在catch子句中没有声明的异常类型,那么该方法将立即退出,并提示用户补充代码
若没有抛出任何异常,将跳过catch子句。
被检测的部分必须放在try块中;try与catch作为一个整体出现,catch必须紧跟在try块之后,不能单独使用,在二者之间也不能插入其他语句;一个try-catch结构中只能有一个try块,但是可以有多个catch块,以便于不同的异常信息匹配。
可以使用多个catch语句,每个catch分别捕获对应的Exception及其子类。JVM在捕获到异常后,会从上到下匹配catch语句,匹配到某个catch后,执行catch代码块,然后不再继续匹配。
存在多个catch的时候,由于其中的异常类是继承关系,所以catch的顺序非常重要:子类必须写在前面。
public static void main(String[] args) { try { process1(); process2(); process3(); } catch (IOException e) { System.out.println("IO error"); } catch (UnsupportedEncodingException e) { // 永远捕获不到,因为该类是IO的子类 System.out.println("Bad encoding"); } }
当代码抛出一个异常后,就会终止方法中剩余代码的处理,并退出这个方法的执行。finally是用来保证一些代码必须执行的。
finally子句:不管是否有异常被捕获,finally子句中的代码都将被执行,为程序提供了一个统一的出口。
// 输出-1 try { int a = 5/0; } catch (Exception e) { return 1; } finally { return -1; }
如果没有发生异常,就正常执行try { … }语句块,然后执行finally。如果发生了异常,就中断执行try { … }语句块,然后跳转执行匹配的catch语句块,最后执行finally。
在C++中,在catch中使用删节号(···)来捕获任何类型的异常信息,这段处理程序必须是try-catch块的最后一段处理程序。
如果某些异常的处理逻辑相同,但是异常本身不存在继承关系,那么就得编写多条catch子句。
public static void main(String[] args) { try { process1(); process2(); process3(); } catch (IOException e) { System.out.println("Bad input"); } catch (NumberFormatException e) { System.out.println("Bad input"); } catch (Exception e) { System.out.println("Unknown error"); } } //=============================================================================== //IOException是受查异常,NumberFormatException是RuntimeException非受查异常 //但是处理IOException和NumberFormatException的代码是相同的,所以我们可以把它两用|合并到一起: public static void main(String[] args) { try { process1(); process2(); process3(); } catch (IOException | NumberFormatException e) { // IOException或NumberFormatException System.out.println("Bad input"); } catch (Exception e) { System.out.println("Unknown error"); } }
堆栈轨迹(stack trace)是一个方法调用过程的列表,包含了程序执行过程中方法调用的特定位置。可以调用Throwable类的printStackTrace方法访问堆栈轨迹的文本描述信息。
异常处理的两种方法:
/** * 使用try-catch处理异常 * @author Administrator */ public class Test02 { public static void main(String[] args) { readMyFile(); } //使用try-catch块包裹起来由方法自己处理异常 public static void readMyFile(){ FileReader reader = null; try{ reader = new FileReader("d:/b.txt"); System.out.println("step1"); char c1 = (char)reader.read(); System.out.println(c1); }catch(FileNotFoundException e){ //子类异常在父类异常前面 System.out.println("step2"); e.printStackTrace(); } catch (IOException e) { throw new MyException(e); //使用throw向上抛出自定义异常 e.printStackTrace(); }finally{ System.out.println("step3"); try { if(reader!=null){ reader.close(); } } catch (IOException e) { e.printStackTrace(); } } } }
/** * 使用throws声明异常 * @author Administrator */ public class Test03 { //主方法可以选择处理异常,或者继续抛出异常交给JRE处理 public static void main(String[] args) throws IOException { readMyFile(); } //子方法自己不处理异常,而是抛出异常 public static void readMyFile() throws IOException { FileReader reader = null; reader = new FileReader("d:/b.txt"); System.out.println("step1"); char c1 = (char) reader.read(); System.out.println(c1); if (reader != null) { reader.close(); } } }
假设确信某个属性符合要求,并且代码的执行依赖于这个属性。比如一个函数的参数要求是非负数,而该参数是另一个函数返回的结果,并且该结果可以保证是非负的,但是程序还是希望去检查该参数的合法性,所以可以写一个if判断语句,并抛出一个异常;但是若在程序存在大量的此类检查,程序运行起来将变得相当慢。
断言机制允许在测试期间向代码中插入一些检查语句,当代码发布时,这些插入的检测语句将会被自动移走。
断言是一种软件调试的方法,提供了一种在代码中进行正确性检查的机制,目前很多开发语言都支持这种机制。它的主要作用是对一个 boolean 表达式进行检查,一个正确运行的程序必须保证这个boolean表达式的值为 true,若 boolean 表达式的值为 false ,则说明程序已经 处于一种不正确的状态下,系统需要提供告警信息并且退出程序。在实际开发中,断言主要用来保证程序的正确性,通常在程序开发与测试中使用。
断言(Assertion)是一种调试程序的方式。在Java中,使用assert关键字来实现断言。断言条件x >= 0预期为true。如果计算结果为false,则断言失败,抛出AssertionError。
注:断言很少使用,更好的方法是编写单元测试
在编写程序的过程中,发现程序运行结果与预期不符,怎么办?当然是用System.out.println()
打印出执行过程中的某些变量,观察每一步的结果与代码逻辑是否符合,然后有针对性地修改代码。
代码改好了怎么办?当然是删除没有用的System.out.println()
语句了。
如果改代码又改出问题怎么办?再加上System.out.println()
。
反复这么搞几次,很快大家就发现使用System.out.println()
非常麻烦。
怎么办?解决方法是使用日志。
System.out.println()
。默认级别是INFO,因此,INFO级别以下的日志,不会被打印出来。使用日志级别的好处在于,调整级别,就可以屏蔽掉很多调试相关的日志输出。
因此,Java标准库内置的Logging使用并不是非常广泛。
和Java标准库提供的日志不同,Commons Logging是一个第三方日志库,它是由Apache创建的日志模块。
Commons Logging的特色是,它可以挂接不同的日志系统,并通过配置文件指定挂接的日志系统。默认情况下,Commons Logging自动搜索并使用Log4j(Log4j是另一个流行的日志系统),如果没有找到Log4j,再使用JDK Logging。
Log4j是一个组件化设计的日志系统,它的架构大致如下:
当我们使用Log4j输出一条日志时,Log4j自动通过不同的Appender把同一条日志输出到不同的目的地。例如:
在实际使用的时候,并不需要关心Log4j的API,而是通过配置文件来配置它(添加依赖+填写配置信息)。