《Java编程思想》第四版读书笔记 第十四章 类型信息

14.2

RTTI运行时类型识别。

Class对象包含了与类有关的信息,Java使用Class对象来执行其RTTI。每个类都有一个Class对象,每当编写并且编译了一个新类,就会产生一个Class对象。为了生成这个类的对象,JVM使用类加载器子系统。类加载器子系统可以包含一条类加载器链:

  • 启动类加载器是根上的加载器,它负责加载rt.jar文件中所有的Java类,即Java的核心类都由该类加载器加载,在Sun JDK中,它是由C++实现的。在Java语言中无法获得它的引用;
  • 系统类加载器负责加载启动参数中指定的classpath中的jar包及目录,自己写 的类也由该ClassLoader加载。在Sun JDK中,系统类加载器的名字叫AppClassLoader;
  • 用户自定义类加载器由用户自己定义类的加载规则,例如可能会在数据库中查找字节码而不是检查.class文件。

所有的类都是在对其第一次使用时,动态加载到JVM中的。当程序创建第一个对类的静态成员引用时,就会加载这个类。注意构造函数也是类的静态方法。使用new操作符创建类的新对象也会被当作对类的静态成员的引用。

因此,Java程序在它开始运行之前并非被完全加载,其各个部分是在必需时才加载的,C++是静态加载。

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

Class类的静态方法forName()接受目标类的名字为参数获取该类Class对象的引用。如果找不到要加载的类会抛出ClassNotFoundException。无论何时,只要你想在运行时使用类型信息,就必须首先获得恰当的Class对象的引用。Class.forName()是获取Class对象引用的便捷途径,因为不需要为了获得Class引用而持有该类型的对象。但是,如果已经有了一个该对象的引用,可以通过调用它的getClass()方法来获取Class对象的引用了,这个方法属于Object的一部分。第三种生成Class对象引用的方法是类字面常量,例如:

FancyToy.class;

这样做不仅简单而且更安全,因为它在编译时就会受到检查,因此也不会抛出异常。类字面常量不仅可以应用于普通的类,也可以应用于接口、数组以及基本数据类型。另外对于基本数据类型的包装器类还有一个标准字段TYPE。它是指向对应基本数据类型的Class对象,例如:

boolean.class 与Boolean.TYPE等价。

作者建议使用boolean.class来保持与普通类的一致性。

作者提到奇怪的一点是,Class.forName()得到Class的对象会使得此类进行初始化步骤(此处说的初始化步骤并不是指初始化Class的对象,关于使用类而准备的三个步骤见下文),而字面常量**.class得到Class的对象并不会使此类进行初始化步骤。

为了使用类而做的准备工作实际包含三个步骤:

(1)加载,由类加载器执行,该步骤查询字节码(通常在classpath所指定的路径中查找),并从这些字节码中创建一个Class对象;

(2)链接,此阶段将验证类中的字节码,为静态域分配存储空间,并且如果必须的话,将解析这个类创建的对其他类的所有引用;

(3)初始化,如果该类具有超类,则对其初始化,执行静态初始化函数和静态初始化快。初始化被延迟到了对静态方法或者非常数静态域进行首次引用时才执行。

例子的总结:

  • 仅使用.class来获取Class对象的引用不会引发初始化步骤,而使用Class.forName()立即就进行了初始化;
  • 如果一个static final域的值是“编译期常量”,那么这个值不需要对类进行初始化步骤就可以被读取;但是如果static和final的值不是编译期常量,就会进行初始化步骤;
  • 如果一个static域不是final,那么对它访问也将进行初始化步骤。

Class类对象的getName()来产生全限定的类名,getSimpleName()和getCanonicalName()来产生不含包名的类名和全限定的类名。getName()和getCanonicalName()(1.5引入)方法的区别,前者返回的是虚拟机里面的class表示,后者返回的是更容易理解的表示,比如:

byte[]类型,前者[B,后者是byte[];

byte[][]类型,前者是[[B,后者是byte[][]。

isInterface()方法判断这个Class对象是否是一个借口。getInterfaces()方法返回的是该对象实现的接口。getSuperclass()查询Class对象的基类。newInstance()方法得到该类的一个新对象,使用它来创建对象必须带有默认的构造函数。

练习10让我们编译一个程序,使它能过判断char数组是基本类型还是一个对象。

char c = new char[3];
System.out.println(c.getClass().getCanonicalName());
System.out.println(c.getClass().getSuperclass().getCanonicalName());

输出显示c的基类是Object,说明他是一个对象。

从1.5开始为Class添加了泛型化处理,如果事先不知道Class的具体类型,可以写Class,也可以写Class表示它就是一个Number的子类,具体类型暂时不详。

向Class引用添加泛型语法的原因是为了提供编译期类型检查。

对类FancyToy的Class对象的getSuperClass()方法仅仅返回的是Class的对象,? super FancyToy表示某个类,它是FancyToy超类。

在1.5中,Class添加了cast()方法,该方法接受对象,并将其转型为Class引用的类型。

14.3

P325代码后面:“抽象的getTypes()方法在导出类中实现”和本页最后一段“getTypes()方法通常只返回对一个静态List的引用”,方法名getTypes()错了应为types()。

Class的newInstance()方法需要处理两个异常:

  1. InstantiationException 如果此 Class 表示一个抽象类、接口、数组类、基本类型或 void; 或者该类没有空构造方法; 或者由于其他某种原因导致实例化失败。
  2. IllegalAccessException 表示违反了Java的安全机制,这里主要指默认构造器为private的情况。

instanceof有比较严格的限制,只能将对象与类的名字进行比较,而不能与Class对象做比较。

作者认为如果程序中编写了许多的instanceof表达式,说明程序的设计存在瑕疵。

Class对象的isInstance()方法与instanceof表达式的作用相同。

作者在本节最后的例子中使用了Class对象的isAssignableFrom(Class)方法,判定此 Class 对象所表示的类或接口与指定的 Class 参数所表示的类或接口是否相同,或是否是其超类或超接口。

14.5

用instanceof和Class.isInstance方法可以完美的判断类的继承的情况,而用==或者equals()比较两个Class对象,则不能正确的判断继承的情况。例如:

class Child extends Parent {

}

Child c = new Child();
System.out.println(c.getClass == Child.class);
System.out.ptintln(c.getClass == Parent.class);

System.out.println(c instanceof Child);
System.out.println(c instanceof Parent);

输出的结果为

true

false

true

true

14.6

Class类与java.lang.reflect包中的类一起对反射进行了支持,该库包含了Field、Method以及Constructor类(每个类都实现了Member接口)。

注意getConstructor()和getConstructors()方法只能获取public的构造函数。

14.7

在任何时刻,只要想要将额外的操作从“实际”对象中分离到不同的地方,特别是当洗完容易做出修改,从没有使用额外操作转为使用这些操作,或者反过来时,代理显得特别有用。

Java的动态代理,可以动态的创建代理并动态的处理对所代理方法的调用。在动态代理商所做的所有调用都会被重定向到单一的调用处理器上。java.lang.reflect.Proxy的静态方法newProxyInstance()可以动态创建代理,这个方法需要得到一个类加载器(通常可以从已经被加载的对象中获取类加载器,然后传递给它),一个希望代理实现的接口列表(不是类或者抽象类),以及一个InvocationHandler接口的一个实现(即调用处理器)。动态代理可以将所有调用重定向到调用处理器,因此通常会向调用处理器的构造函数传递一个实际对象的引用,从而使得调用处理器在执行其中介任务时可以将请求转发。

InvocationHandler的invoke()方法中第一个参数时代理对象,用来需要时区分请求的资源,但是在许多情况下并不需要它。

练习21和练习22翻译有误,原文写的是so that it measures method-call times,第一眼我也觉得应该翻译成调用的次数,但是看答案的代码应该是调用的时间。

练习中让我们System.out.println(proxy),导致了异常,看起来应该是循环调用了,之前书中有一句话:“在invoke()内部,在代理上调用方法时需要格外当心,因为对接口的调用将被重定向为对代理的调用”。此错误应该跟这句话有关,但我并不是很理解。

14.8

空对象可以接受传递给它的代表对象的信息,但是将返回表示为实际上并不存在任何“真是”对象的值。通过这种方法,可以假设所有的对象都是有效的,而不必浪费编程精力去检查Null。

为了创建空对象,最简单的方式使创建一个标记接口:

public interface Null {
}

这使得instanceof可以探测空对象,而并不要求在所有的类中都添加isNull()方法。

通常可以将空对象设置为单例,这样就可以使用equals()方法和==来判断是否是空对象。如果使用接口取代具体的空类,还可以使用动态代理机制来自动创建空对象。

14.9

作者通过几个例子向我们展示了Java的反射机制破坏了封装特性,通过反射你可以接触到普通类、包访问权限的类、私有内部类、匿名内部类的任何访问权限的域和方法,但是有一个特殊的地方,final域在遭遇反射修改时是安全的,运行时系统会在不抛出异常的情况下接受任何修改尝试,但是实际上不会发生任何修改。

14.10

作者在总结中提到他的一个编程思想:不要太早的关注程序的效率问题,最好首先让程序运作起来,然后再考虑它的效率。

如果要解决解决效率问题可以使用profiler工具。

转载于:https://my.oschina.net/u/2453016/blog/689407

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值