@@ 运行时类型信息使得你可以在程序运行时发现和使用类型信息。
@@ Java 是如何让我们在运行时识别对象和类的信息的(主要有两种方式)
----- 一种是“传统的”RTTI , 它假定我们在编译时已经知道了所有的类型
---- 另一种是“反射”机制,它允许我们在运行时发现和使用类的信息
》》为什么需要 RTTI(Run-Time Type Information)
@@ 面向对象编程中基本的目的是:让代码只操纵对基类的引用。这样,如果
要添加一个新类来扩展程序,就不会影响到原来的代码。
@@ 要理解:什么是多态、什么是动态绑定
@@ 如果某个对象出现在字符串表达式中(涉及“+” 和字符串对象的表达式),
toString() 方法就会被调用,以生成表示该对象的 String 。
@@ 在Java 中,所有的类型转换都是在运行时进行正确性检查的。这也是 RTTI
名字的含义: 在运行时,识别一个对象的类型。
》》 Class对象
@@ 要理解 RTTI 在 Java 中的工作原理,首先必须知道类型信息在运行时是如何表示的。
这项工作是由称为 Class 对象的特殊对象完成的,它包含了与类有关的信息。
@@ Class 对象是用来创建类的所有的“常规”对象的。
@@ Java 使用 Class 对象来执行其 RTTI ,即使你正在执行的是类似转型这样的操作。
@@ 类是程序的一部分,每个类都有一个 Class 对象。换言之,每当编写并且编译一个新类,
就会产生一个 Class 对象(更恰当地说,是被保存在一个同名的 .class 文件中)。
为了生成 Class 类的对象,运行这个程序的 Java 虚拟机(JVM)将使用被称为“类加载器”
的子系统。
@@ 类加载器子系统实际上可以包含一条类加载器链,但是只有一个原生类加载器,它是 JVM
实现的一部分。
@@ 原生类加载器加载的是所谓的可信类,包括 Java API 类,它们通常是从本地盘加载的。
@@ 在类加载器链中,通常不需要添加额外的类加载器,但是如果你有特殊需求(例如以某种特殊
的方式加载类,以支持 Web 服务器应用,或者在网络中下载类),那么你有一种方式可以挂接
额外的类加载器。
@@ 所有类都是在对其第一次使用时,动态加载到 JVM 中的。
@@ Java 程序在它开始运行之前并非被完全加载,其各个部分是在必需时才加载的。
@@ 类加载器首先检查这个类的 Class 对象是否已经加载。如果尚未加载,默认的类加载器就会根据
类名查找 .class 文件。在这个类的字节码被加载时,它们会接受验证,以确保其没有被破坏,并且
不包含不良 Java 代码(这是 Java 中用于安全防范目的的措施之一)。
@@ 一旦某个类的 Class 对象被加载入内存,它就被用来创建这个类的所有对象。
@@ Class 对象仅在需要的时候才被加载,static 初始化是在类加载时进行的。
@@ 例如:
Class.forName("Gum");
说明:(1)、Class 对象就和其他对象一样,我们可以获取并操作它的引用(这也就是类加载器
的工作)。
(2)、forName() 是取得 Class 对象引用的一种方法。它是用一个包含目标类的文本名
(注意拼音和大小写)的 String 作为输入参数,返回的是一个 Class 对象的引用。
(3)、对 forName() 的调用是为了它产生的 “ 副作用 ” : 如果类 Gum 还没有被加载就
加载它。在加载的过程中,Gum 的 static 子句被执行。
(4)、如果 Class.forName() 找不到你要加载的类,它会抛出异常 ClassNotFoundException。
@@ 无论何时,只要你想在运行时使用类型信息,就必须首先获得对恰当的 Class 对象的引用。
Class.forName() 就是实现此功能的便捷途径,因为你不需要为了获得 Class 引用而持有该类型的
对象。
@@ 如果你已经拥有了一个感兴趣的类型的对象,那就可以通过调用 getClass() 方法来获得 Class 引用,
这个方法属于根类 Object 的一部分,它将返回表示该对象的是实际类型的 Class 引用。
@@ 在 main() 中调用的 Class.getInterfaces() 方法返回的是 Class 对象,它们表示在感兴趣的 Class
对象中所包含的接口。
@@ 如果你有一个 Class 对象,还可以使用 getSuperclass() 方法来查询其直接基类。因此,你可以在
运行时发现一个对象完整的类继承结构。
@@ Class 的 newInstance() 方法是实现 “ 虚拟构造器 ” 的一种途径,虚拟构造器允许你声明:“我不知道
你的确切类型,但是无论如何要正确地创建你自己 ” 。 使用 newInstance() 创建新实例时,会的到
Object 引用,但是这个引用是指向 Class 所表示的类的具体的对象。
@@ 使用 newInstance() 方法来创建的类,必须带有默认的构造器。
@@ 使用 Java 的反射 API ,可以用任意的构造器来动态地创建类的对象。
## 类字面常量
@@ Java 还提供了另一种方法来生成对 Class 对象的引用,即使用类字面常量。例如:
FancyToy.class
这样做不仅更简单,而且更安全,因为它在编译时就会受到检查(因此不需要置于 try 语句块中)。
并且它根除了对 forName() 方法的调用,所以也更高效。
@@ 类字面常量不仅可以应用于普通的类,也可以应用于接口、数组以及基本数据类型。
@@ 对于基本数据类型的包装器类,还有一个标准字段 TYPE 。TYPE 字段是一个引用,指向
对应的基本数据类型的 Class 对象,如下所示:
说明:(1)、 建议使用“ .class ” 的形式,以保持与普通类的一致性。
注意:
当使用“ .class ” 来创建对 Class 对象的引用时,不会自动地初始化该 Class 对象。为了
使用类而做的准备工作实际包含三个步骤:
a. 加载。这是由类加载器执行的。该步骤将查找字节码(通常在 classpath 所指定的路径中查找,
但这并非是必需的),并从这些字节码中创建一个 Class 对象。
b. 链接。在链接阶段将验证类中的字节码,为静态域分配存储空间,并且如果必需的话,将解析
这个类创建的对其他类的所有引用。
c. 初始化。如果该类具有超类,则对其初始化,执行静态初始化器和静态初始化块。
《
初始化被延迟到了对静态方法(构造器隐式地是静态的)或者非常数静态域进行首次引用时
才执行。
》
@@ 如果一个 static final 值是“ 编译期常量 ”,那么这个值不需要进行初始化就可以被读取。
@@ 如果一个 static final 值不是“编译器期常量” , 那么这个值被访问之前必须进行强制的初始化。
@@ 如果一个 static 域不是 final 的,那么在对它访问时,总是要求在它被读取之前,要先进行链接
(为这个域分配存储空间)和初始化(初始化该存储空间)。
## 泛化的 Class 引用
@@ Class 引用总是指向某个 Class 对象,它可以制造类的实例,并包含可作用于这些实例的所有
方法的代码。它还包含该类的静态成员,因此,Class 引用表示的就是它所指向的对象的确切类型,
而该对象便是 Class 类的一个对象。
@@ 通过对 Class 引用所指向的 Class 对象的类型进行限定,可以使得 Class 对象更具体。这里
可以使用泛型语法。
@@ 例如:
Class intClass = int.class ;
intClass = double.class;
Class<Integer> genericIntClass = int.class ;
说明:(1)、泛型类引用只能赋值为指向其声明的类型。
(2)、普通的类引用可以重新赋值为指向任何其他的 Class 对象
(3)、通过使用泛型语法,可以让编译器强制执行额外的类型检查
注意: Class<Number> genericNumberClass = int.class;
上面的代码看起来似乎是起作用的,因为 Integer 继承自 Number 。但是它是无法
工作的,因为 Integer Class 对象不是 Number Class 对象的子类。
@@ 为了在使用泛化的 Class 引用时放松限制,可以使用通配符,它是 Java 泛型的一部分。
通配符就是 “ ? ” ,表示 “ 任何事物 ” 。
例如:
public static void main(String[] args){
Class<?> intClass = int.class ;
intClass = double.class ;
}
说明: (1)、Class<?> 优于平凡的 Class
@@ 为了创建一个 Class 引用,它被限定为某种类型,或该类型的任何子类型,你需要将
通配符与 extends 关键字结合,创建一个范围。因此,与仅仅声明 Class<Number >
不同,如下:
public static void main(String[] args){
Class<? extends Number> bounded = int.class;
bounded = double.class;
bounded = Number.class;
}
@@ 向 Class 引用添加泛型语法的原因仅仅是为了提供编译期类型检查。
## 新的转型语法
@@ Java SE 5 添加了用于 Class 引用的转型语法,即 case() 方法。
@@ 例如:
class Building{
}
class House extends Building {
}
public static void main(){
Building b = new House() ;
Class<House> houseType = House.class ;
House h = houseType.cast(b) ; // 新的转型方法
h = (House) b ; // 普通转型方法
}
说明:(1)、cast( ) 方法接受参数对象,并将其转型为 Class 引用的类型。
(2)、 新的转型语法(cast( ) 方法)对于无法使用普通转型的情况显得非常有用。
(3)、 在你编写泛型代码时,如果你存储了 Class 引用,并希望以后通过这个引用来
执行转型,(2)中的情况就会时有发生。
》》类型转换前先做检查
@@ 我们已知的 RTTI 形式包括:
形式一:传统的类型转换,由 RTTI 确保类型转换的正确性,如果执行一个错误的类型转换,
就会抛出一个 ClassCastException 异常
形式二:代表对象的类型的 Class 对象。通过查询 Class 对象可以获取运行时所需的信息。
形式三: 关键字 instanceof ,它返回一个布尔值,告诉我们对象是不是某个特定类型的实例。
@@ Java 要执行类型检查,这通常被称为 “ 类型安全的向下转型 ” 。 之所以叫 “ 向下转型 ” ,
是由于类层次结构图从来就是这么排列的。
@@ RTTI 的第三种形式,关键字 instanceof
-----例如:
if ( x instanceof Dog){
( (Dog) x).bark();
}
说明:
进行向下转型前,如果没有其他信息可以告诉你这个对象是什么类型,那么使用
instanceof 是非常重要的,否则会得到一个 ClassCastException 异常。
@@ 对 instanceof 有比较严格的限制:只可将其与命名类型进行比较,而不能与 Class 对象
对象作比较。
## 使用类字面常量
@@ 使用 .class
## 动态的 instanceof
@@ 使用 Class.isInstance () 方法提供了一种动态地测试对象的途径。
## 递归计数
》》注册工厂
## 使用工厂方法设计模式。
》》instanceof 与 Class的等价性
## 在查询类型信息时,以 instanceof 的形式(即以 instanceof 的形式或 isInstance() 的形式,它们
产生的结果相同)与直接比较 Class 对象有一个很重要的差别。
----- instanceof 和 isInstance() 生成的结果是一样的
instanceof 保持了类型的概念,它指的是“ 你是这个类吗,或者你是这个类的派生类吗?”
----- equals() 和 == 生成的结果是一样的
没有考虑继承------它或者是这个确切的类型,或者不是
》》反射:运行时的类信息
@@ 如果不知道某个对象的确切类型,RTTI 可以告诉你。但是有一个限制:这个类型在编译时必须
已知,这样才能使用 RTTI 识别它,并利用这些信息做一些有用的事。换句话说,在编译时,编
译器必须知道所有要通过 RTTI 来处理的类。
@@ 反射提供一种机制---------用来检查可用的方法,并返回方法名。
@@ Java 通过 JavaBeans 提供了基于构件的编程架构。
@@ 远程方法调用(RMI):提供在跨网络的远程平台上创建和运行对象的能力;
允许一个 Java 程序将对象分布到多台机器上
@@ Class 类和 java.lang.reflect 类库一起对反射概念进行了支持,该类库包含了 Field 、
Method 、 Constructor 类(每个类都实现了 Member 接口)
------ 这些类型的对象是由 JVM 在运行时创建的,用以表示未知类里对应的成员
------ 这样就可以使用 Constructor 创建新的对象:
%% 使用 get() 和 set() 方法读取和修改与 Field 对象关联的字段
%% 使用 invoke() 方法调用与 Method 对象关联的方法
%% 可以调用 getFields() 、 getMethods() 、 getConstructors() 以返回
表示字段 、 方法、构造器的对象的数组。
------- 这样,匿名对象的类信息就能在运行时被完全确定下来,而在编译时不需要
知道任何事情。
@@ 当通过反射与一个未知类型的对象打交道时,JVM 只是简单地检查这个对象,看
它属于哪个特定的类(就像 RTTI 那样 )。在用它做其他事情之前必须先加载那个
类的 Class 对象 。 因此,那个类的 .class 文件对于 JVM 来说必须是可获取的:
要么在本地机器上,要么可以通过网络取得。
@@ RTTI 和 反射之间真正的区别只在于:
-----对 RTTI 来说,编译器在编译时打开和检查 .class 文件。(换句话说,我们
可以用 “ 普通 ” 方式调用对象的所有方法 )
-----对于反射机制来说, .class 文件在编译时是不可获取的,所以是在运行时打开
和检查 .class 文件
## 类方法提取器
@@ 通常你不需要直接使用反射工具,但是它们在你需要创建更加动态的代码时会很有
用。
@@ 反射在 Java 中是用来支持其他特性的,例如对象序列化和JavaBean 。但是,如果能
动态地提取某个类的信息有的时候还是很有用的。
@@ 反射提供了一种方法,使我们能够编写可以自动展示完整接口的简单工具。
------ Class 提供了 getMethods() 和 getConstructors() 方法分别返回 Method 对象的数组
和 Constructor 对象的数组。Method 和 Constructor 这两个类都提供了深层的方法,
用以解析其对象所代表的方法,并获取其名字、输入参数以及返回值。
@@ Class.forName( ) 生成的结果在编译时是不可知的,因此所有的方法特征签名信息都是在执行
时被提取出来的。
@@ 研究一下 JDK文档中关于反射的部分,就会发现,反射机制提供了足够的支持,使得能够
创建一个在编译时完全未知的对象,并调用此对象的方法。
》》动态代理
@@ 代理是基本的设计模式之一,它是你为了提供额外的或不同的操作,而插入的用来代替
“ 实际 ” 对象的对象。这些操作通常涉及与 “ 实际 ” 对象的通信,因此代理通常充当着
中间人的角色。
@@ 在任何时刻,只要你想要将额外的操作从 “ 实际 ” 对象中分离到不同的地方,特别是当你
希望能够很容易地做出修改,从没有使用额外操作转为使用这些操作,或者反过来时,代理
就显得很有用。(设计模式的关键就是封装修改)。
@@ Java 的动态代理比代理的思想更向前迈进了一步,因为它可以动态地创建代理并动态地处理
对所代理方法的调用。在动态代理上所做的所有调用都会被重定向到单一的调用处理器上,
它的工作是揭示调用的类型并确定相应的对策。
@@ 例如:
interface Interface {
}
class RealObject {
}
class Dynamic implements InvocationHandler {
private Object proxied ;
public Dynamic( Object proxied){
this.proxied = proxied ;
}
}
public static void main(String[] args){
RealObject real = new RealObject() ;
Interface proxy = (Interface)Proxy.newProxyInstance(
Interface.class.getClassLoader() , //参数一
new Class[] { Interface.class} , // 参数二
new Dynamic(real) // 参数三
);
}
说明: (1)、通过调用静态方法Proxy.newProxyInstance()可以创建动态代理,这个方法
有三个参数:
----参数一: 类加载器(可以从已经被加载的对象中获取其类加载器,然后传递给它)
----参数二: 希望该代理实现的接口列表(不是类或抽象类)
----参数三:InvocationHandler 接口(真正的调用处理器)的一个实现
(2)、动态代理可以将所有调用重定向到调用处理器,因此通常会向调用处理器的
构造器传递给一个 “ 实际 ” 对象的引用,从而使得调用处理器在执行其中介任务时,
可以将请求转发。
@@ 在 invoke() 内部,在代理上调用方法时要格外小心,因为对接口的调用将被重定向为对代理
调用。
@@ 通常你会执行代理的操作,然后使用 Method.invoke() 将请求转发给代理对象,并传入必需
的参数。
@@ 动态代理并非是你日常使用的工具,但是它可以非常好地解决某些类型的问题。
》》空对象
@@ 空对象最有用之处在于它更靠近数据,因为对象表示的是问题空间内的实体。
@@ 空对象都是单例,要将其作为静态 final 实例创建。
@@
补充:
------极限编程(XP)的原则之一:在你的设计草案的初稿中,应该努力使用最简单且可以工作的
事物,直至程序的某个方面要求你添加额外的特性,而不是一开始就假设它是必需的。
## 模拟对象与桩
@@ 空对象的逻辑变体是模拟对象和桩。与空对象一样,它们都表示在最终的程序中所使用的
“ 实际 ” 对象。但是,模拟对象和桩都只是假扮可以传递实际信息的存活对象,而不是像空
那样可以成为 null 的一种更加智能化的替代物。
@@ 模拟对象和桩之间的差异在于程度不同。
----- 模拟对象往往是轻量级和自测试的,通常很多模拟对象被创建出来是为了处理各种不同的测试
情况。
------ 桩只是返回数据,它通常是重量级的,并且经常在测试之间被复用。
------ 桩可以根据它们被调用的方式,通过配置进行修改,因此桩是一种复杂对象,它要做很多事。
------ 对于模拟对象,如果你需要做很多事情,通常会创建大量小而简单的模拟对象。
》》接口与类型信息
@@ interface 关键字的一种重要目标就是允许程序员隔离构件,进而降低耦合性。
@@ final 域在遭遇修改时是安全的。
@@ 运行时系统会在不抛异常的情况下接受任何修改尝试,但是实际上不会发生任何修改。
》》小结
@@ RTTI 允许通过匿名基类的引用来发现类型信息。
@@ 面向对象编程语言的目的是让我们在凡是可以使用的地方都使用多态机制,只在必需的时候
使用 RTTI 。
@@ 使用多态机制的方法调用,要求我们拥有基类定义的控制权,因为在你扩展程序的时候,可能
会发现基类并未包含我们想要的方法。
如果基类自来别人的类,或者由别人控制,这时候 RTTI 便是一种解决之道:可继承一个新类,
然后添加你需要的方法。在代码的其他地方,可以检查自己特定的类型,并调用你自己的方法。
这样做不会破坏多态性以及程序的扩展能力。
@@ 如果在程序主体中添加需要的新特性的代码,就必须使用 RTTI 来检查你的特定的类型。
@@ RTTI 有时能解决效率问题。
注意: 不要太早地关注程序的效率问题。最好首先让程序运作起来,然后再考虑它的速度。
@@ 反射允许更加动态的编程风格(反射的动态特性)。