Java中的异常处理用法及其架构和使用建议_java

Java异常是Java提供的一种识别及响应错误的一致性机制。
Java异常机制可以使程序中异常处理代码和正常业务代码分离,保证程序代码更加优雅,并提高程序健壮性。在有效使用异常的情况下,异常能清晰的回答what, where, why这3个问题:异常类型回答了“什么”被抛出,异常堆栈跟踪回答了“在哪“抛出,异常信息回答了“为什么“会抛出。
Java异常机制用到的几个关键字:try、catch、finally、throw、throws。

关键字 说明
try 用于监听。将要被监听的代码(可能抛出异常的代码)放在try语句块之内,当try语句块内发生异常时,异常就被抛出。
catch 用于捕获异常。catch用来捕获try语句块中发生的异常。
finally finally语句块总是会被执行。它主要用于回收在try块里打开的物力资源(如数据库连接、网络连接和磁盘文件)。只有finally块,执行完成之后,才会回来执行try或者catch块中的return或者throw语句,如果finally中使用了return或者throw等终止方法的语句,则就不会跳回执行,直接停止。
throw 用于抛出异常。
throws 用在方法签名中,用于声明该方法可能抛出的异常。

下面通过几个示例对这几个关键字进行简单了解。
示例一: 了解try和catch基本用法

public class Demo1 {

 public static void main(String[] args) {
  try {
   int i = 10/0;
    System.out.println("i="+i);
  } catch (ArithmeticException e) {
    System.out.println("Caught Exception");
   System.out.println("e.getMessage(): " + e.getMessage());
   System.out.println("e.toString(): " + e.toString());
   System.out.println("e.printStackTrace():");
   e.printStackTrace();
  }
 }
}

运行结果:

Caught Exception
e.getMessage(): / by zero
e.toString(): java.lang.ArithmeticException: / by zero
e.printStackTrace():
java.lang.ArithmeticException: / by zero
 at Demo1.main(Demo1.java:6)

结果说明:在try语句块中有除数为0的操作,该操作会抛出java.lang.ArithmeticException异常。通过catch,对该异常进行捕获。
观察结果我们发现,并没有执行System.out.println("i="+i)。这说明try语句块发生异常之后,try语句块中的剩余内容就不会再被执行了。
示例二: 了解finally的基本用法
在"示例一"的基础上,我们添加finally语句。

public class Demo2 {

 public static void main(String[] args) {
  try {
   int i = 10/0;
    System.out.println("i="+i);
  } catch (ArithmeticException e) {
    System.out.println("Caught Exception");
   System.out.println("e.getMessage(): " + e.getMessage());
   System.out.println("e.toString(): " + e.toString());
   System.out.println("e.printStackTrace():");
   e.printStackTrace();
  } finally {
   System.out.println("run finally");
  }
 }
}

运行结果:

Caught Exception
e.getMessage(): / by zero
e.toString(): java.lang.ArithmeticException: / by zero
e.printStackTrace():
java.lang.ArithmeticException: / by zero
 at Demo2.main(Demo2.java:6)
run finally

结果说明:最终执行了finally语句块。
示例三: 了解throws和throw的基本用法
throws是用于声明抛出的异常,而throw是用于抛出异常。

class MyException extends Exception {
 public MyException() {}
 public MyException(String msg) {
  super(msg);
 }
}

public class Demo3 {

 public static void main(String[] args) {
  try {
   test();
  } catch (MyException e) {
   System.out.println("Catch My Exception");
   e.printStackTrace();
  }
 }
 public static void test() throws MyException{
  try {
   int i = 10/0;
    System.out.println("i="+i);
  } catch (ArithmeticException e) {
   throw new MyException("This is MyException");
  }
 }
}

运行结果:

Catch My Exception
MyException: This is MyException
 at Demo3.test(Demo3.java:24)
 at Demo3.main(Demo3.java:13)

结果说明:MyException是继承于Exception的子类。test()的try语句块中产生ArithmeticException异常(除数为0),并在catch中捕获该异常;接着抛出MyException异常。main()方法对test()中抛出的MyException进行捕获处理。

Java异常框架
Java异常架构图:

1. Throwable
Throwable是 Java 语言中所有错误或异常的超类。
Throwable包含两个子类: Error 和 Exception。它们通常用于指示发生了异常情况。
Throwable包含了其线程创建时线程执行堆栈的快照,它提供了printStackTrace()等接口用于获取堆栈跟踪数据等信息。

2. Exception
Exception及其子类是 Throwable 的一种形式,它指出了合理的应用程序想要捕获的条件。

3. RuntimeException
RuntimeException是那些可能在 Java 虚拟机正常运行期间抛出的异常的超类。
编译器不会检查RuntimeException异常。例如,除数为零时,抛出ArithmeticException异常。RuntimeException是ArithmeticException的超类。当代码发生除数为零的情况时,倘若既"没有通过throws声明抛出ArithmeticException异常",也"没有通过try...catch...处理该异常",也能通过编译。这就是我们所说的"编译器不会检查RuntimeException异常"!
如果代码会产生RuntimeException异常,则需要通过修改代码进行避免。例如,若会发生除数为零的情况,则需要通过代码避免该情况的发生!

4. Error
和Exception一样,Error也是Throwable的子类。它用于指示合理的应用程序不应该试图捕获的严重问题,大多数这样的错误都是异常条件。
和RuntimeException一样,编译器也不会检查Error。
Java将可抛出(Throwable)的结构分为三种类型:被检查的异常(Checked Exception),运行时异常(RuntimeException)和错误(Error)。

(1) 运行时异常
定义: RuntimeException及其子类都被称为运行时异常。
特点: Java编译器不会检查它。也就是说,当程序中可能出现这类异常时,倘若既"没有通过throws声明抛出它",也"没有用try-catch语句捕获它",还是会编译通过。例如,除数为零时产生的ArithmeticException异常,数组越界时产生的IndexOutOfBoundsException异常,fail-fail机制产生的ConcurrentModificationException异常等,都属于运行时异常。
     虽然Java编译器不会检查运行时异常,但是我们也可以通过throws进行声明抛出,也可以通过try-catch对它进行捕获处理。
     如果产生运行时异常,则需要通过修改代码来进行避免。例如,若会发生除数为零的情况,则需要通过代码避免该情况的发生!

(2) 被检查的异常
定义: Exception类本身,以及Exception的子类中除了"运行时异常"之外的其它子类都属于被检查异常。
特点: Java编译器会检查它。此类异常,要么通过throws进行声明抛出,要么通过try-catch进行捕获处理,否则不能通过编译。例如,CloneNotSupportedException就属于被检查异常。当通过clone()接口去克隆一个对象,而该对象对应的类没有实现Cloneable接口,就会抛出CloneNotSupportedException异常。
     被检查异常通常都是可以恢复的。

(3) 错误
定义: Error类及其子类。
特点: 和运行时异常一样,编译器也不会对错误进行检查。
     当资源不足、约束失败、或是其它程序无法继续运行的条件发生时,就产生错误。程序本身无法修复这些错误的。例如,VirtualMachineError就属于错误。
     按照Java惯例,我们是不应该是实现任何新的Error子类的!
对于上面的3种结构,我们在抛出异常或错误时,到底该哪一种?《Effective Java》中给出的建议是:对于可以恢复的条件使用被检查异常,对于程序错误使用运行时异常。

关于异常处理的几条建议

第1条: 只针对不正常的情况才使用异常
建议:异常只应该被用于不正常的条件,它们永远不应该被用于正常的控制流。
通过比较下面的两份代码进行说明。
代码1

try {
 int i=0;
 while (true) {
  arr[i]=0;
  i++;
 }
} catch (IndexOutOfBoundsException e) {
}
代码2
for (int i=0; i<arr.length; i++) {
 arr[i]=0;
}

两份代码的作用都是遍历arr数组,并设置数组中每一个元素的值为0。代码1的是通过异常来终止,看起来非常难懂,代码2是通过数组边界来终止。我们应该避免使用代码1这种方式,主要原因有三点:
异常机制的设计初衷是用于不正常的情况,所以很少会会JVM实现试图对它们的性能进行优化。所以,创建、抛出和捕获异常的开销是很昂贵的。
把代码放在try-catch中返回阻止了JVM实现本来可能要执行的某些特定的优化。
对数组进行遍历的标准模式并不会导致冗余的检查,有些现代的JVM实现会将它们优化掉。
实际上,基于异常的模式比标准模式要慢得多。测试代码如下:

public class Advice1 {

 private static int[] arr = new int[]{1,2,3,4,5};
 private static int SIZE = 10000;

 public static void main(String[] args) {

  long s1 = System.currentTimeMillis();
  for (int i=0; i<SIZE; i++)
   endByRange(arr);
  long e1 = System.currentTimeMillis();
  System.out.println("endByRange time:"+(e1-s1)+"ms" );

  long s2 = System.currentTimeMillis();
  for (int i=0; i<SIZE; i++)
   endByException(arr);
  long e2 = System.currentTimeMillis();
  System.out.println("endByException time:"+(e2-s2)+"ms" );
 }

 // 遍历arr数组: 通过异常的方式
 private static void endByException(int[] arr) {
  try {
   int i=0;
   while (true) {
    arr[i]=0;
    i++;
    //System.out.println("endByRange: arr["+i+"]="+arr[i]);
   }
  } catch (IndexOutOfBoundsException e) {
  }
 }

 // 遍历arr数组: 通过边界的方式
 private static void endByRange(int[] arr) {
  for (int i=0; i<arr.length; i++) {
   arr[i]=0;
   //System.out.println("endByException: arr["+i+"]="+arr[i]);
  }
 }
}

运行结果:

endByRange time:8ms
endByException time:16ms

结果说明:通过异常遍历的速度比普通方式遍历数组慢很多!

第2条: 对于可恢复的条件使用被检查的异常,对于程序错误使用运行时异常

异常 说明
运行时异常 RuntimeException类及其子类都被称为运行时异常。
被检查的异常 Exception类本身,以及Exception的子类中除了"运行时异常"之外的其它子类都属于被检查异常

它们的区别是:Java编译器会对"被检查的异常"进行检查,而对"运行时异常"不会检查。
也就是说,对于被检查的异常,要么通过throws进行声明抛出,要么通过try-catch进行捕获处理,否则不能通过编译。而对于运行时异常,倘若既"没有通过throws声明抛出它",也"没有用try-catch语句捕获它",还是会编译通过。当然,虽说Java编译器不会检查运行时异常,但是,我们同样可以通过throws对该异常进行说明,或通过try-catch进行捕获。
rithmeticException(例如,除数为0),IndexOutOfBoundsException(例如,数组越界)等都属于运行时异常。对于这种异常,我们应该通过修改代码进行避免它的产生。而对于被检查的异常,则可以通过处理让程序恢复运行。例如,假设因为一个用户没有存储足够数量的前,所以他在企图在一个收费电话上进行呼叫就会失败;于是就将一个被检查异常抛出。

第3条: 避免不必要的使用被检查的异常
"被检查的异常"是Java语言的一个很好的特性。与返回代码不同,"被检查的异常"会强迫程序员处理例外的条件,大大提高了程序的可靠性。
但是,过分使用被检查异常会使API用起来非常不方便。如果一个方法抛出一个或多个被检查的异常,那么调用该方法的代码则必须在一个或多个catch语句块中处理这些异常,或者必须通过throws声明抛出这些异常。 无论是通过catch处理,还是通过throws声明抛出,都给程序员添加了不可忽略的负担。
适用于"被检查的异常"必须同时满足两个条件:第一,即使正确使用API并不能阻止异常条件的发生。第二,一旦产生了异常,使用API的程序员可以采取有用的动作对程序进行处理。

第4条: 尽量使用标准的异常
代码重用是值得提倡的,这是一条通用规则,异常也不例外。重用现有的异常有几个好处:
第一,它使得你的API更加易于学习和使用,因为它与程序员原来已经熟悉的习惯用法是一致的。
第二,对于用到这些API的程序而言,它们的可读性更好,因为它们不会充斥着程序员不熟悉的异常。
第三,异常类越少,意味着内存占用越小,并且转载这些类的时间开销也越小。
Java标准异常中有几个是经常被使用的异常。如下表格:

异常 使用场合
IllegalArgumentException 参数的值不合适
IllegalStateException 参数的状态不合适
NullPointerException 在null被禁止的情况下参数值为null
IndexOutOfBoundsException 下标越界
ConcurrentModificationException 在禁止并发修改的情况下,对象检测到并发修改
UnsupportedOperationException 对象不支持客户请求的方法

虽然它们是Java平台库迄今为止最常被重用的异常,但是,在许可的条件下,其它的异常也可以被重用。例如,如果你要实现诸如复数或者矩阵之类的算术对象,那么重用ArithmeticException和NumberFormatException将是非常合适的。如果一个异常满足你的需要,则不要犹豫,使用就可以,不过你一定要确保抛出异常的条件与该异常的文档中描述的条件一致。这种重用必须建立在语义的基础上,而不是名字的基础上!
最后,一定要清楚,选择重用哪一种异常并没有必须遵循的规则。例如,考虑纸牌对象的情形,假设有一个用于发牌操作的方法,它的参数(handSize)是发一手牌的纸牌张数。假设调用者在这个参数中传递的值大于整副牌的剩余张数。那么这种情形既可以被解释为IllegalArgumentException(handSize的值太大),也可以被解释为IllegalStateException(相对客户的请求而言,纸牌对象的纸牌太少)。

第5条: 抛出的异常要适合于相应的抽象
如果一个方法抛出的异常与它执行的任务没有明显的关联关系,这种情形会让人不知所措。当一个方法传递一个由低层抽象抛出的异常时,往往会发生这种情况。这种情况发生时,不仅让人困惑,而且也"污染"了高层API。
为了避免这个问题,高层实现应该捕获低层的异常,同时抛出一个可以按照高层抽象进行介绍的异常。这种做法被称为"异常转译(exception translation)"。
例如,在Java的集合框架AbstractSequentialList的get()方法如下(基于JDK1.7.0_40):

public E get(int index) {
 try {
  return listIterator(index).next();
 } catch (NoSuchElementException exc) {
  throw new IndexOutOfBoundsException("Index: "+index);
 }
}

listIterator(index)会返回ListIterator对象,调用该对象的next()方法可能会抛出NoSuchElementException异常。而在get()方法中,抛出NoSuchElementException异常会让人感到困惑。所以,get()对NoSuchElementException进行了捕获,并抛出了IndexOutOfBoundsException异常。即,相当于将NoSuchElementException转译成了IndexOutOfBoundsException异常。

第6条: 每个方法抛出的异常都要有文档
要单独的声明被检查的异常,并且利用Javadoc的@throws标记,准确地记录下每个异常被抛出的条件。
如果一个类中的许多方法处于同样的原因而抛出同一个异常,那么在该类的文档注释中对这个异常做文档,而不是为每个方法单独做文档,这是可以接受的。

第7条: 在细节消息中包含失败 -- 捕获消息
简而言之,当我们自定义异常或者抛出异常时,应该包含失败相关的信息。
当一个程序由于一个未被捕获的异常而失败的时候,系统会自动打印出该异常的栈轨迹。在栈轨迹中包含该异常的字符串表示。典型情况下它包含该异常类的类名,以及紧随其后的细节消息。

第8条: 努力使失败保持原子性
当一个对象抛出一个异常之后,我们总期望这个对象仍然保持在一种定义良好的可用状态之中。对于被检查的异常而言,这尤为重要,因为调用者通常期望从被检查的异常中恢复过来。
一般而言,一个失败的方法调用应该保持使对象保持在"它在被调用之前的状态"。具有这种属性的方法被称为具有"失败原子性(failure atomic)"。可以理解为,失败了还保持着原子性。对象保持"失败原子性"的方式有几种:
(1) 设计一个非可变对象。
(2) 对于在可变对象上执行操作的方法,获得"失败原子性"的最常见方法是,在执行操作之前检查参数的有效性。如下(Stack.java中的pop方法):

public Object pop() {
 if (size==0)
  throw new EmptyStackException();
 Object result = elements[--size];
 elements[size] = null;
 return result;
}

(3) 与上一种方法类似,可以对计算处理过程调整顺序,使得任何可能会失败的计算部分都发生在对象状态被修改之前。
(4) 编写一段恢复代码,由它来解释操作过程中发生的失败,以及使对象回滚到操作开始之前的状态上。
(5) 在对象的一份临时拷贝上执行操作,当操作完成之后再把临时拷贝中的结果复制给原来的对象。
虽然"保持对象的失败原子性"是期望目标,但它并不总是可以做得到。例如,如果多个线程企图在没有适当的同步机制的情况下,并发的访问一个对象,那么该对象就有可能被留在不一致的状态中。
即使在可以实现"失败原子性"的场合,它也不是总被期望的。对于某些操作,它会显著的增加开销或者复杂性。
总的规则是:作为方法规范的一部分,任何一个异常都不应该改变对象调用该方法之前的状态,如果这条规则被违反,则API文档中应该清楚的指明对象将会处于什么样的状态。

第9条: 不要忽略异常
当一个API的设计者声明一个方法会抛出某个异常的时候,他们正在试图说明某些事情。所以,请不要忽略它!忽略异常的代码如下:

try {
 ...
} catch (SomeException e) {
}

空的catch块会使异常达不到应有的目的,异常的目的是强迫你处理不正常的条件。忽略一个异常,就如同忽略一个火警信号一样 -- 若把火警信号器关闭了,那么当真正的火灾发生时,就没有人看到火警信号了。所以,至少catch块应该包含一条说明,用来解释为什么忽略这个异常是合适的。

以上是小编为您精心准备的的内容,在的博客、问答、公众号、人物、课程等栏目也有的相关内容,欢迎继续使用右上角搜索按钮进行搜索java
, 异常
异常处理
java异常用法、java 异常架构、公司组织架构调整建议、组织架构调整的建议、三层架构异常处理设计,以便于您获取更多的相关知识。

时间: 2024-09-02 15:02:03

Java中的异常处理用法及其架构和使用建议_java的相关文章

java中final的用法

许多程序设计语言都有自己的办法告诉编译器某个数据是"常数".常数主要应用于下述两个方面: (1) 编译期常数,它永远不会改变 (2) 在运行期初始化的一个值,我们不希望它发生变化 对于编译期的常数,编译器(程序)可将常数值"封装"到需要的计算过程里.也就是说,计算可在编译期间提前执行,从而节省运行时的一些开销.在Java中,这些形式的常数必须属于基本数据类型(Primitives),而且要用final关键字进行表达.在对这样的一个常数进行定义的时候,必须给出一个值.

Java中getResourceAsStream的用法小结

Java中getResourceAsStream的用法小结 一.Java中的getResourceAsStream主要有以下三种用法:1.Class.getResourceAsStream(String path)      path 不以'/'开头时默认是从此类所在的包下取资源,以'/'开头则是从ClassPath根下获取.     其实是通过path构造一个绝对路径,最终还是由ClassLoader获取资源. 2.Class.getClassLoader.getResourceAsStrea

详解Java中final的用法_java

概念 final 具有"不可改变的"的含义,可以修饰 非抽象类.非抽象成员方法和变量. 用 final 修饰的类不能被继承,没有子类. 用 final 修饰的方法不能被子类的方法覆盖(重写). 用 final 修饰的变量表示常量,只能被赋一次值(声明变量的时候). 注: final 不能用来修饰构造方法,因为"方法覆盖"这一概念仅适用于类的成员方法,而不适用于类的构造方法,父类的构造方法和子类的构造方法之间不存在覆盖的关系,因此用final修饰构造方法没有任何意义.

class-关于java中的super()用法

问题描述 关于java中的super()用法 class Insect { private int size; private String color; public Insect(int size, String color) { this.size = size; this.color = color; } public int getSize() { return size; } public void setSize(int size) { this.size = size; } pu

Java中的多态用法实例分析_java

本文实例讲述了Java中的多态用法.分享给大家供大家参考.具体分析如下: 多态,是面向对象的程序设计语言最核心的特征.封装性.继承性都比较简单,所以这里只对多态做一个小小的笔记... 1.什么是多态? 多态意味着一个对象可以多重特征,可以在特定的情况下,表现出不同的状态,从而应对不同的属性和方法.在Java中,多态的实现指的是使用同一个实现接口,以实现不同的对象实例. 例如,我们定义一个Parent类,再定义一个getName()方法返回一个字符串,定义一个形参为Parent类型的成员方法doS

Java 中Map 的用法详解_java

Map简介 将键映射到值的对象.一个映射不能包含重复的键:每个键最多只能映射到一个值.此接口取代 Dictionary 类,后者完全是一个抽象类,而不是一个接口. Map 接口提供三种collection 视图,允许以键集.值集或键-值映射关系集的形式查看某个映射的内容.映射顺序 定义为迭代器在映射的 collection 视图上返回其元素的顺序.某些映射实现可明确保证其顺序,如 TreeMap 类:另一些映射实现则不保证顺序,如HashMap 类. 注:将可变对象用作映射键时必须格外小心.当对

Java中的字符串用法小结_java

本文实例总结了Java中的字符串用法.分享给大家供大家参考.具体分析如下: 字符串的本质是char类型的数组,但在java中,所有用双引号""声明的字符串都是一个String类的对象.这也正体现了Java完全面向对象的语言特点. String 类 1.String类对象表示的是一个常量字符串.它是不可变长度的.也就是说,一旦创建了一个String类的实例,那么这个实例所表示的串是不可改变的.类似于 str = str + "Hello"; 这样的操作,实质上是将 s

浅析java中stringBuilder的用法_java

String对象是不可改变的.每次使用 System.String类中的方法之一时,都要在内存中创建一个新的字符串对象,这就需要为该新对象分配新的空间.在需要对字符串执行重复修改的情况下,与创建新的 String对象相关的系统开销可能会非常昂贵.如果要修改字符串而不创建新的对象,则可以使用System.Text.StringBuilder类.例如,当在一个循环中将许多字符串连接在一起时,使用 StringBuilder类可以提升性能. 通过用一个重载的构造函数方法初始化变量,可以创建 Strin

java中enum的用法_java

本文实例讲述了java中enum的用法.分享给大家供大家参考.具体分析如下: 1. 基本用法 复制代码 代码如下: enum Day {     SUNDAY, MONDAY, TUESDAY, WENDSDAY, THURSDAY, FRIDAY, SATURDAY; } 枚举是常量,所以应该用大写. 2. 枚举是对象 枚举隐含地继承了java.lang.Enum,所以它具有java.lang.Enum的属性和方法.遍历枚举: 复制代码 代码如下: public class Main {