ThinkingInJava(四):异常、字符串、RTTI

第十二章:通过异常处理错误

1.概念
  • 异常处理是Java中唯一正式的错误报告机制
  • 使用异常能降低错误处理代码的复杂度
  • 异常处理机制使完成任务的代码与错误检查的代码没有混在一起
2.基本异常
  • 异常情形是指阻止当前方法或作用域继续执行的问题。

  • 当抛出异常后,有几件事会随之发生:

  1. 同Java中其他对象的创建一样,将使用new在堆上创建异常对象。
  2. 然后当前的执行路径被终止,并且从当前环境中弹出对异常对象的引用。此时,异常处理机制接管程序,并开始寻找一个恰当的地方来继续执行程序。这个恰当的地方就是 异常处理程序(每个catch子句) ,它的任务是将程序从错误状态中恢复,以使程序能要么换一种方法运行,要么继续运行下去。
  • 所有标准异常类都有两个构造器:一个是默认构造器;另一个是接受字符串作为参数,以便把相关信息放入异常对象的构造器。
throw new NullPointerExeception("t = null");

在使用new创建了异常对象之后,此对象的引用将传给throw。可以简单地把异常处理看成一种不同的返回机制。另外还能用抛出异常的方式从当前的作用域退出。这两种情况下,将会返回一个异常对象,然后退出方法或作用域。

  • Throwable是异常类型的根类。
3.捕获异常

监控区域(guarded region) 是一段可能产生异常的代码,并且后面跟着处理这些异常的代码。

异常处理理论上有两种基本模型:

  • 终止模型 :一旦异常被抛出,就表明错误已无法挽回,也不能回来继续执行。
  • 恢复模型 :异常处理程序的工作是修正错误,然后重新尝试调用出问题的方法,并认为第二次能成功。

偏向使用终止模型,恢复模型会导致代码的耦合,即恢复性的处理程序需要了解异常抛出的地点,这势必要包含依赖于抛出位置的非通用性代码。

4.捕获所有异常
  • printStackTrace() 方法所提供的信息可以通过 getStackTrace() 方法来直接访问,这个方法返回一个由栈轨迹中的元素所构成的数组,其中每个元素都表示栈中的一帧。
  • 重新抛出异常会把异常抛给上一级环境中的异常处理程序。异常对象的所有信息都得以保持,所以高一级环境中捕获此异常的处理程序可以从这个异常对象中的所有信息。
  • 若想更新抛出点的信息,可以调用fillInStackTrace()方法,但有关原来异常发生点的信息会丢失。
  • 异常对象都是用new在堆上创建的对象,故垃圾回收器会自动把它们清理掉。
  • 在捕获一个异常后抛出一个异常,并且希望把原始异常的信息保存下来,这被称为 异常链 。在Throwable的子类中,只有三种基本的异常类提供了带cause参数的构造器,分别是Error、Exception及RuntimeException。如果要把其他类型的异常链接起来,应该使用initCause()方法而不是构造器。
5.Java标准异常
  • Throwable对象可分为两种类型:
  1. Error:表示编译时和系统错误,一般程序员不关心
  2. Exception:可以被抛出的基本类型,在Java类库,用户方法以及运行时故障中都可能抛出Exception异常
  • RuntimeException 属于运行时异常,会自动被JVM抛出。它们也被称为“不受检查异常”,这种异常属于错误,将被自动捕获。

  • 只能在代码中忽略 RuntimeException 及其子类的异常,其他类型异常的处理都是有编译器强制实施的。究其原因, RuntimeException 是编程错误。

6.异常的限制
  • 当覆盖方法时,只能抛出在基类方法的异常说明里列出的异常。通过强制派生类遵守基类方法的异常说明,对象的可替换性得到了保证。
  • 异常限制对构造器不起作用。派生类构造器的异常说明必须包含基类构造器的异常说明。
  • 当父类与接口具有相同的方法而且方法同时抛出不同的异常,这个时候是不允许的。
  • 当父类的构造方法抛出异常,基类必须有一个构造方法是抛出相同异常或者此异常的父类。
  • 当父类方法没有抛出异常,子类覆盖的方法不能够抛出异常
  • 当父类方法抛出异常,子类覆盖的方法可以不抛出异常
7.构造器

在大多数情况下,异常发生所有东西都能被正常清理。但是在涉及构造器时,由于构造器会把对象设置成安全的初始状态,还会有别的动作,如打开文件等。这些动作只有在对象使用完毕并且用户调用了特殊的清理方法之后才能得以清理。如果在构造器内抛出异常,这些清理行为也许不能正常工作了。如果构造器在其执行过程中半途而废,也许该对象的某些部分还没有被成功创建,而这些部分在finally子句中却是要被清理的。

解决方法可使用嵌套的try子句。

第十三章:字符串

1.不可变的String
  • 字符串对象是不可变的。String类中每一个看起来会修改String值的方法,实际上都是创建了一个全新的String对象,以包含修改后的字符串内容。
2.重载“+”与StringBuilder
  • String对象是不可变的,可以给一个String对象加任意多的别名。因为String对象具有可读特性,所以指向它的任何引用都不可能改变它的值,因此,也就不会对其他的什么影响。
  • 操作符“+”可以用来连接String。我们使用“+”来连接String字符,编译期自动引入了 java.lang.StringBuilder 类,但是如果StringBuilder是在循环内构造的,会创建多个StringBuilder。所以直接使用 StringBuilder类,它只会生成一个StringBuilder类,同时最好预先指定StringBuilder的大小,避免多次重新分配缓冲。
3. 无意识的递归
  • Java中的每一个类都是从根本上继承Object,标准容器类也不例外。调用容器的toString方法会遍历容器中所有元素的toString方法。
  • 如果想打印出对象的内存地址,应该调用Object.toString()方法,不应该使用this,而是使用super.toString()方法。
public class InfiniteRecursion{
    public String toString(){
        //应该调用Object.toString()方法,所以此处应为super.toString()。
        return " InfiniteRecursion address: " + this + "\n"; 
    }
    public static void main(String[] args){
        List<InfiniteRecursion> v = new ArrayList<InfiniteRecursion>();
        for(int i = 0; i < 10; i++)
            v.add(new InfiniteRecursion());
        System.out.println(v);
    }
}
4.String上的操作
  • 当需要改变字符串的内容时,String类的方法都会返回一个新的String对象。同时,如果内容没有发生改变,String方法只是返回指向原对象的引用而已。这可以节约存储空间以及避免额外的开销。
5.Formatter转换
  • 类型转化字符

    d 整数型(十进制)e 浮点数(科学记数)
    c unicode字符x 整数(16进制)
    b Booleanh 散列码(16进制)
    s String% 字符%
    f 浮点数(10进制)

第十四章:类型信息(Run-Time Type Identification)

运行时类型信息使得可以在程序运行时发现和使用类型信息。

Java让我们能在运行时识别对象和类的信息的两种方式:

  1. 传统的RTTI:它假定我们在编译时已经知道了所有的类型;
  2. 反射机制:它允许我们在运行时发现和使用类信息。
1.为什么需要RTTI
  • 当把 Shape 对象放入 List<Shape>的数组时会向上转型。但在向上转型为 Shape 的时候也丢失了 Shape对象 的具体类型。对于数组而言,它们只是 Shape 类的对象。

  • 当从数组中取出元素时,这种容器——实际上他将所有的事物当作 Object 持有——会自动将结果转型回 Shape 。这是RTTI最基本的使用形式,因为在Java中,所有类型转换都是在运行时进行正确性检查的。即在运行时,识别一个对象的类型。

  • 接下来就是多态机制的事情了,Shape 对象实际执行什么样的代码,是由引用所指向的具体对象 CircleSquareTriangle 而决定的。

多态中表现的类型转换是RTTI最基本的使用形式,但这种转换并不彻底。如数组容器实际上将所有元素当作Object持有,取用时再自动将结果转型回声明类型。而数组在填充(持有)对象时,具体类型可能是声明类型的子类,这样放到数组里就会向上转型为声明类型,持有的对象就丢失了具体类型。而取用时将由Object只转型回声明类型,并不是具体的子类类型,所以这种转型并不彻底。

多态中表现了具体类型的行为,但那只是“多态机制”的事情,是由引用所指向的具体对象而决定的,并不等价于在运行时识别具体类型
  以上揭示了一个问题就是具体类型信息的丢失!有了问题,就要解决问题,这就是RTTI的需要,即在运行时确定对象的具体类型

2.Class对象

Class对象包含了与类有关的信息,是用来创建类的所有的"常规"对象。Java使用Class对象来执行其RTTI,Class类有大量的使用RTTI的方式,包括转型操作。

类是程序的一部分,每一个类都有一个Class对象。为了生成这个类的对象,运行这个程序的Java虚拟机将使用被称为 类加载器 的子系统。

类加载器子系统实际上可以包含一条类加载器链,但是只有一个原生类加载器,其加载的是所谓的可信类,包括Java API类,它们通常是从本地盘加载的。在这条链中,通常不需要添加额外的类加载器,如有特殊需求,可以挂接额外的类加载器。

所有的类都是在对其第一次使用时,动态加载到JVM的。因此Java程序在它开始运行之前并非被完全加载,其各个部分是在必需时才加载的。

类加载器首先检查这个类的Class对象是否已经加载,如果未加载,默认的类加载器会根据类名查找.class文件,在这个类的字节码被加载时,接受验证,以确保其没有被破坏,并且不包含不良Java代码。一旦某个类的Class对象被载入内存,它就可被用来创建这个类的所有对象。

Class.forName() 可以获取Class对象的引用。getName()产生全限定的类名,并分别使用 getSimpleName()getCanonicalName() 来产生不含包名的类名和全限定名。isInterface() 可以告诉你这个Class象是否表示某个接口,getInterfaces()返回的是Class对象中所包含的接口。还可以使用 getSuperclass() 方法查询其直接基类。

2.1 类字面常量
  • 使用字面常量也可以生成对Class对象的引用。类字面常量不仅可以应用于普通的类,也可以应用于接口、数组以及基本数据类型。
FancyToy.class

**当使用 .class 来创建对Class对象的引用时,不会自动地初始化该Class对象。**为使用类而做的准备工作实际包含三个步骤:

  • 加载:这是由类加载器执行的,该步骤将查找字节码,并从字节码中创建一个Class对象。
  • 链接:在链接阶段将验证类中的字节码,为静态域分配存储空间,并且如果必需的话,将解析这个类创建的对其他类的所有引用。
  • 初始化:如果该类具有超类,则对其初始化,执行静态初始化器和静态初始化块。初始化被延迟到了对静态方法(构造器隐式地是静态的)或者非常数静态域进行首次引用时才执行。

Class.forName() 会立即执行初始化。编译期常量在类未初始化之前即可读取。非final的静态域在对其访问前要先进行链接和初始化。

2.2 泛化的Class引用

Class引用总是指向某个Class对象,它可以制造类的示例,并包含可作用于这些实例的所有方法代码。它还包含该类的静态成员,因此,Class引用表示的就是它所指向的对象的确切类型。而该对象便是Class类的一个对象。

尽管泛型类引用只能赋值为指向其声明的类型,但是普通类引用可以被重新赋值为指向任何其他的Class对象。通过泛型语法可以让编译器强制执行额外的类型检查。

  • 使用通配符 ? 放松限制,?代表任何事物。
Class<?> intClass = int.class;
         intClass = double.class;
  • 通配符?extends关键字结合,限定创建Class引用的范围
Class<? extends Numner> bounded = int.class;
                        bounded = double.class;
  • 如果手头只有超类,那编译器只允许这样声明超类引用:
Class<? super FancyToy> up = ftClass.getSuperclass();
  • newInstance()方法返回的不是精确类型,而只是Object。
2.3 新的转型语法

cast()用于Class引用的转型语法,新的转型语法对于无法使用普通转型的情况显得非常有用。

3.类型转换前先做检查

RTTI的第三种形式,就是关键字 instanceof。它返回一个布尔值,告诉我们对象是不是某个特定类型的实例。

使用类字面常量更安全,因为它在编译时就会受到检查,因此不需要置于try语句块中,这与Class.forName()不一样。

4.instanceof与Class的等价性
  • instanceofisInstance() 保持了类型的概念,它指的是“你是这个类吗,或者你是这个类的派生类吗?
  • ==equals() 没有考虑继承——它要么是这个确切的类型,要么不是。
5.反射:运行时的类信息
  • 如果不知道某个对象的确切类型,RTTI可以告诉你。但这个类型在编译时必须已知。即在编译时,编译器必须知道所有要通过RTTI来处理的类。

  • 人们想要在运行时获取类的信息的另一动机,便是希望提供在跨网络的远程平台上创建和运行对象的能力。这种被称为远程方法调用,它允许一个Java程序将对象分布到多台机器上。

  • RTTI和反射之间真正的区别在于:对于RTTI来说,编译器在编译时打开和检查 .class 文件。而对于反射机制来说,在运行时打开和检查 .class 文件。

6.动态代理

Java的动态代理比代理的思想更向前迈进了一步,因为它可以动态地创建代理并动态地处理对所代理方法的调用。

在动态代理上所做的所有调用都会被重定向到单一的调用处理器上,它的工作是揭示调用的类型并确定相应的对策。

通过调用静态方法Proxy.newProxyInstance()可以创建动态代理,需要三个参数:

  • ClassLoader loader 一个类加载器,通常可以从已经被加载的对象中获取其类加载器
  • Class<?>[] interfaces 一个希望代理要实现的接口列表(不是类或抽象类)
  • InvocationHandler h 一个调用处理器接口的实现

动态代理可以将所有调用重定向到调用处理器,因此通常会向调用处理器传递一个“实际”对象(即被代理的对象)的引用,从而使得调用处理器在执行其中介任务时,可以将请求转发(即去调用实际对象)。

6.1 动态代理的优点及美中不足
  • 优点:动态代理与静态代理相较,最大的好处是接口中声明的所有方法都被转移到调用处理器一个集中的方法(InvocationHandler.invoke)中处理。这样,在接口方法数量比较多的时候,我们可以进行灵活处理,而不需要像静态代理那样每一个方法进行中转。

用处理器,因此通常会向调用处理器传递一个“实际”对象(即被代理的对象)的引用,从而使得调用处理器在执行其中介任务时,可以将请求转发(即去调用实际对象)。**

6.1 动态代理的优点及美中不足
  • 优点:动态代理与静态代理相较,最大的好处是接口中声明的所有方法都被转移到调用处理器一个集中的方法(InvocationHandler.invoke)中处理。这样,在接口方法数量比较多的时候,我们可以进行灵活处理,而不需要像静态代理那样每一个方法进行中转。

  • 美中不足:它始终无法摆脱仅支持interface代理的桎梏,因为它的设计注定了这个遗憾。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值