Java的异常分类

下图是Java异常层次结构的示意图

Java异常分类
Java异常分类

从图中我们可以发现所有的异常都是继承自Throwable,下一层又分两个分支Error和Exception。
Error类描述了java运行时系统的内部错误和资源耗尽错误。应用程序不应该抛出这种类型的对象。如果出现这样的的内部错误,除了告知用户,并尽力使程序安全终止之外,再也无能为力。但是这种情况一般很少出现。

我们在写程序的时候更关注于Exception层次结构。而这个分支又有两个异常(忽略JSONException):一个分支派生出RuntimeException;另一个分支包含了其他异常。划分这两个分支的规则是:由程序错误导致的异常属于RuntimeException;而程序本身没有问题,但由于像I/O错误此类的问题导致的异常属于其他异常。

Java规范将派生于Error类或者RuntimeException类的所有异常称为未检查(unchecked)异常,所有其他的异常称为已检查(checked)异常。

一个方法必须声明所有可能抛出已检查异常,而未检查异常要么不可控制要么就应该避免发生。

如果父类没有抛出已检查异常,那么子类也不能抛出此类异常。

使用异常的技巧:

  1. 异常处理不能代替简单的测试
    例如在对空栈进行退栈操作,退栈之前需要先判断是否为空栈,应该使用if判断,而不是使用捕获异常的方式,因为捕获异常运行时间会大于直接判断的执行时间。
  2. 不要过分地细化异常
    比如将每个语句都装在独立的try语句块中,这种编程方式会导致代码量的急剧膨胀,且排除问题也十分困难,因此有必要将整个任务包装在一个try语句块中,这样任何一个操作出现问题的时候,整个任务都可以取消。
  3. 利用异常的层次结构
    不要只抛出RuntimeException异常,应该要寻找合适的子类异常类或者新建一个异常类。
    不要只捕获Throwable异常,否则会使程序代码难读,也难维护。
  4. 不要压制异常
    不要认为某个方法可能很久不出现异常就不选择捕获。
  5. 检测错误,苛刻要比放任更好
    例如程序检测到错误的时候,程序猿担心抛出异常,可能会把异常吃掉转成null,那么显然把 异常抛出去可能会比转null更优说不定后面就出现空指针异常。
  6. 不要羞于传递异常
    传递异常要比捕获异常更好,让高层次的方法通知用户发生了错误,或者放弃不成功的命令会更加适宜。

断言

默认情况下,断言被禁用。可以在运行程序时用enableassertions或者-ea选项启用它。

启用禁用断言并不需要重新编译程序,他们是类加载器的功能。禁用断言,类加载器会跳过断言代码,因此也不会降低程序的运行速度。

断言应该用于测试阶段确定程序内部的错误位置。而不应该向程序其他部分通告发生了可恢复性的错误或者作为向用户通告问题的手段。

泛型

一个泛型类( generic class ) 就是具有一个或多个类型变量的类。类型变量使用大写形式,且比较短, 这是很常见的。在Java 库中, 使用变量E 表示集合的元素类型, K 和V 分别表示表的关键字与值的类型。T ( 需要时还可以用临近的字母U 和S) 表示“ 任意类型”。

类型变量的限定

有时,类或方法需要对类型变量加以约束,可以通过对类型变量T 设置限定(bound) 实现这一点,例如:

public static <T extends Comparab1e> T min(a) .

表示T 应该是绑定类型的子类型(subtype)。T 和绑定类型可以是类, 也可以是接口。选择关键字extends 的原因是更接近子类的概念, 并且Java 的设计者也不打算在语言中再添加一个新的关键字(如sub)。一个类型变量或通配符可以有多个限定, 例如:

T extends Comparable & Serializable

限定类型用“ &” 分隔,而逗号用来分隔类型变量。

在Java 的继承中, 可以根据需要拥有多个接口超类型, 但限定类型中至多有一个类。如果用一个类作为限定,它必须是限定列表中的第一个。

类型擦除

无论何时定义一个泛型类型, 都自动提供了一个相应的原始类型( raw type )。原始类型的名字就是删去类型参数后的泛型类型名。擦除(erased) 类型变量, 并替换为限定类型(无限定的变量用Object)。原始类型用第一个限定的类型变量来替换, 如果没有给定限定就用Object 替换

1564198545097.png
1564198545097.png

如果切换限定: class Interval<T extends Serializable & Comparable>原始类型用Serializable 替换T, 而编译器在必要时要向Comparable 插入强制类型转换。为了提高效率, 应该将标签( tagging) 接口(即没有方法的接口)放在边界列表的末尾。

在Java多态中,泛型可能会带来两个问题: 1.类型擦除与多态的冲突;2.方法签名冲突。

Datelnterval interval = new Datelnterval(. . .);
Pair<Loca1Date> pair = interval; // OK assignment to superclass
pair.setSecond(aDate);

要解决这个问题, 就需要编译器在子类中生成一个桥方法(bridge method),用到了强制装换类型。

public void setSecond(Object second) { setSecond((Date) second); }

虚拟机调用桥方法,桥方法再调用子类的真正复写的方法。虚拟机用pair 引用的对象调用这个方法。这个对象是
Datelnterval 类型的, 因而将会调用Datelnterval.setSecond(Object) 方法。这个方法是合成的
桥方法。它调用Datelnterval.setSecond(Date)。

桥方法可能有时候会出现和子类方法参数类型一致,例如get方法。在Java代码编写中如果两个参数类型一样的同名方法是不允许的,但是在虚拟机中,用参数类型和返回类型确定一个方法。因此, 编译器可能产生两个仅返回类型不同的方法字节码, 虚拟机能够正确地处理这一情。

Java 泛型转换的事实:

  • 虚拟机中没有泛型, 只有普通的类和方法。
  • 所有的类型参数都用它们的限定类型替换。
  • 桥方法被合成来保持多态。
  • 为保持类型安全性,必要时插人强制类型转换
  • 限定类型用“ &” 分隔,而逗号用来分隔类型变量。

tip:不能用基本类型实例化类型参数,运行时类型查询只适用于原始类型,不能创建参数化类型的数组,不能实例化一个泛型实例,也不能构造一个泛型数组,不能在静态域或方法中引用类型变量,泛型类的静态上下文中类型变量无效,不能抛出或捕获泛型类的实例