类型信息
本章讨论Java如何让我们在运行时识别对象和类的信息 。主要有两种方式:一种是传统的RTTI (Run-Time Type Information )(运行时识别类型信息),它假定我们在编译时已经知道了所有的类型。另一种是反射 机制,它允许我们在运行时发现和使用类的信息。
RTTI
其实之前讲过的多态 就是使用了RTTI 的特性。我们可以不必知道对象的具体类型,而使用更加通用的类型(基类 )与这些对象打交道。运行时自有RTTI 帮我们去识别对象的具体类型,然后绑定需要执行的方法。
Class对象
要理解RTTI 在Java中的工作原理,首先必须知道类型信息在运行时是如何表示的。这项工作是由成为Class 对象的特殊对象完成的,它包含了与类有关的信息。事实上,Class 对象就是用来创建类的所有常规对象 的。 类是程序的一部分,每个类都有一个Class 对象。为了生成这个对象,运行这个程序的Java虚拟机 将使用被称为类加载器 的子系统。 类加载器子系统实际上可以包含一条类加载器链 ,但是只有一个原生类加载器 ,它是JVM 实现的一部分。原生类加载器加载的是所谓的可信类 ,包括Java API 类,它们通常是从本地盘加载的。在这条链中,通常不需要添加额外的类加载器 。但是如果你有特殊需求(例如以某种特殊的方式加载类,以支持Web服务器应用,或者在网络中加载类),那么你有一种方式可以挂接额外的类加载器 。 所有的类都是在对其第一次使用时,动态加载 到JVM 中。首次使用即指调用类的静态属性或静态方法 。首次使用构造器 (可以看成是静态方法)也会执行类加载 。 类加载器首先检查这个类的Class对象是否已经加载。如果尚未加载,默认的类加载器(原生类加载器 )就会根据类名查找.class 文件(如果是附加类加载器,就可以去数据库读取字节码 )。接下来字节码会被验证是否损坏,并且不包含不良代码(这部分可以去看另外一本书《深入理解Java虚拟机》 )。 一旦某个类的Class 对象被载入内存,它就会被用来创建这个类的所有对象。
public class ClassTest {
static {
System.out .println("ClassTest loaded" );
}
public static void main (String args[]) {
System.out .println("run main()" );
System.out .println("before create A" );
new A();
System.out .println("before load B" );
try {
Class.forName("test.B" );
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
class A {
static {
System.out .println("A loaded" );
}
}
class B {
static {
System.out .println("B loaded" );
}
}
-----------------运行结果:
ClassTest loaded
run main()
before create A
A loaded
before load B
B loaded
接下来我们稍稍了解一下Class类 的一些常用方法:
class AB extends Test implements AC , AD {
public AB(int i) {}
}
interface AC { }
interface AD { }
public class Test {
public static void main (String args[]) {
try {
Class AB = Class.forName("test.AB" );
printInfo(AB);
for (Class face : AB.getInterfaces()) {
printInfo(face);
}
Class test = AB.getSuperclass();
printInfo(test);
try {
Object obj = AB.newInstance();
} catch (InstantiationException e) {
System.out.println("can't instantiate" );
} catch (IllegalAccessException e) {
System.out.println("can't access" );
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
public static void printInfo(Class cc) {
System.out.println("Class name: " + cc.getName());
System.out.println("is Interface?: " + cc.isInterface());
System.out.println("Simple name ?: " + cc.getSimpleName());
System.out.println("Canonical name ?: " + cc.getCanonicalName());
System.out.println("------------------" );
}
}
类字面常量
我们可以用Class cc = Integer.class;
的方式去获得某个类型的Class对象 的引用,不过该操作不会自动地初始化 (下面的第三个步骤)该Class对象。为了使用类而做的准备工作实际包含三个步骤:
加载 ,这是由类加载器执行的。该步骤将查找字节码,并从这些字节码中创建一个Class对象。链接 ,在该阶段将验证类中的字节码,为静态域分配存储空间,如果必须的话,将解析这个类创建的对其他类的所有引用。初始化 ,如果该类具有超类,则对其初始化,执行静态初始化器和静态初始化块。 初始化 被延迟到了对静态方法 (构造器隐式地是静态的)或者非常数静态域 ( static final int i = 1;//这个是常数静态域
)进行首次引用时才执行。看了上面的介绍你可能会云里雾里的,下面来看一个例子:
package test;
import java.util.Random;
public class Test {
public static void main (String args[]) throws ClassNotFoundException {
Class t1 = T1.class ;
System.out .println("after T1.class" );
System.out .println(T1.a);
System.out .println(T1.b);
System.out .println("------------------" );
Class t2 = T2.class ;
System.out .println(T2.a);
System.out .println("------------------" );
Class t3 = Class.forName("test.T3" );
System.out .println("after Class.forName(\"test.T3\")" );
System.out .println(T3.a);
}
}
class Tfather {
static {
System.out .println("Tfather 静态块" );
}
}
class T1 extends Tfather {
static final int a = 1 ;
static final int b = new Random().nextInt(20 );
static {
System.out .println("T1 静态块" );
}
}
class T2 {
static int a = 2 ;
static {
System.out .println("T2 静态块" );
}
}
class T3 {
static int a = 3 ;
static {
System.out .println("T3 静态块" );
}
}
------------------执行结果:
after T1.class
1
Tfather 静态块
T1 静态块
14
------------------
T2 静态块
2
------------------
T3 静态块
after Class.forName("test.T3" )
3
泛化的Class引用
之前我们都是直接Class cc;
去声明一个Class类,在Java SE5 引入泛型后,我们可以利用泛型对Class引用所指向的Class对象的类型进行限定。来看下面的例子:
public class Test {
public static void main (String args[]) {
Class intclass = int .class;
Class<Integer> genericIntClass = int .class;
genericIntClass = Integer.class;
intclass = double .class;
}
}
如果你希望放松一下这种限制,我们可以使用通配符?
加上super 和extends 关键字做一些限制。
class O { }
class A extends O { }
class A1 extends A { }
public class Test {
public static void main(String args[]) {
Class <? extends O > t1 = A .class ;//利用通配符和extends 即可
t1 = A1 .class ;//隔两级以上也没事
Class <? super A1 > t2 = A .class ;//利用通配符和super 即可
t2 = O .class ;//隔两级以上也没事
}
}
使用泛型的好处除了能使编译器为我们进行检查外,通过newInstance()
也能自动为我们向上转型。
class O { }
class A extends O { }
class A1 extends A { }
public class Test {
public static void main(String args[]) {
Class <?> t0 = A.class;
Class<? extends O > t1 = A1 .class ;//利用通配符和extends 即可
Class <A1 > t2 = A1 .class ;//利用通配符和extends 即可
//Class <A > t3 = t2 .getSuperclass ();//这样写是不允许的
Class <? super A1 > t3 = t2 .getSuperclass ();
try {
O o1 = (O) t0.newInstance();
System.out.println(o1.getClass().getName());
O o2 = t1.newInstance();
System.out.println(o2.getClass().getName());
O o3 = (O) t3.newInstance();
System.out.println(o3.getClass().getName());
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
除此之外,Class类还有一个cast方法,它能代替强转去完成一些无法直接通过强转的转换。
import java.util.ArrayList;
public class Test {
public static void main (String args[]) throws Exception {
ArrayList<Integer> a = new ArrayList<Integer>();
a.add(1 );
a.getClass().getMethod("add" , Object.class).invoke(a, "ssss" );
System.out .println(a.get (1 ));
String ssss = String.class.cast(a.get (1 ));
System.out .println(ssss);
}
}
类型转换前先做检查
在C++ 中,经典的类型转换“(ClassName)” 并不使用RTTI。它只是简单的告诉编译器将这个对象作为新的类型对待。而Java要执行类型检查,这通常被称为“类型安全的向下转型” 。如果执行了一个错误的类型转换,就会抛出一个ClassCaseException 。所以为了防止错误发生,我们可以使用instanceof 进行类型检查。
import java.util .ArrayList
public class Test {
public static void main(String args[]) throws Exception {
ArrayList<Integer> a = new ArrayList<Integer>()
a.add (1 )
a.getClass ().getMethod ("add" , Object.class ).invoke (a, "ssss" )
System.out .println (a.get (1 ))
System.out .println (a.get (1 ) instanceof Integer)
//String ssss = (String) a.get (1 )
//Integer ssss = a.get (1 )
String ssss = String.class .cast (a.get (1 ))
System.out .println (String.class .isInstance (ssss))
System.out .println (ssss)
}
}
递归计数
instanceof 除了可以检测一个对象的类型是否是右值的类型或子类型外,还可以用Class.isAssignableFrom(Class c)
去检测传入的类型是否是相同或是其子类型。下面就举例对一个继承结构的类型进行计数。
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
class O { }
class A extends O { }
class B extends O { }
class A1 extends A { }
class A2 extends A { }
class B1 extends B { }
class B2 extends B { }
public class Test {
private static List<O> createData(int length) {
List<O> l = new ArrayList<O>(length);
Random r = new Random();
for (int i = 0 ; i < length; i++) {
switch (r.nextInt(7 )) {
case 0 :
l.add(new O());
break ;
case 1 :
l.add(new A());
break ;
case 2 :
l.add(new A1());
break ;
case 3 :
l.add(new A2());
break ;
case 4 :
l.add(new B());
break ;
case 5 :
l.add(new B1());
break ;
case 6 :
l.add(new B2());
break ;
}
}
return l;
}
public static void main(String args[]) throws Exception {
MyMap map = new MyMap(O.class);
List<O> l = createData(20 );
for (O o : l) {
map.count (o);
}
System.out.println(map);
}
private static class MyMap extends HashMap <Class <?>, Integer > {
private Class<?> baseType;
public MyMap(Class c) {
baseType = c;
}
public void count (Object obj) {
Class<?> type = obj.getClass();
if (!baseType.isAssignableFrom(type)) {
throw new RuntimeException(obj + " incorrect type:" +
type + ", should be type or subtype of " + baseType);
}
countClass(type);
}
private void countClass(Class<?> type) {
Integer quantity = get(type);
put(type, quantity == null ? 1 : 1 + quantity);
Class<?> superClass = type.getSuperclass();
if (superClass != null &&
baseType.isAssignableFrom(superClass)) {
countClass(superClass);
}
}
public String toString() {
StringBuilder sb = new StringBuilder("{" );
for (Map.Entry<Class<?>, Integer> pair : entrySet()) {
sb.append(pair.getKey().getSimpleName());
sb.append("=" );
sb.append(pair.getValue());
sb.append(", " );
}
sb.delete(sb.length() - 2 , sb.length());
sb.append("}" );
return sb.toString();
}
}
}
反射:运行时的类信息
如果不知道某个对象的确切类型,RTTI 可以告诉你。但是有一个限制:这个类型在编译时必须已知,这样才能使用RTTI 去识别它,并利用这些信息做一些有用的事。换句话说,在编译时,编译器必须知道所有要通过RTTI 来处理的类。 初看起来这似乎不是个限制,但是假设你获取了一个指向某个并不在你的程序空间中的对象的引用。事实上,在编译时你的程序根本没法获知这个对象所属的类。例如,假如你从磁盘文件,或者网络连接中获取了一串字节,并且你被告知这些字节代表了一个类。那么怎样才能使用这个类呢? 基于构件的编程方式 便是一个例子。它是一种可视化编程方法,可用通过将代表不同组件的图标拖拽到表单中来创建程序。然后在编程时通过设置构件的属性值来配置它们。这种设计时的配置,要求构件都是可实例化的,并且暴露其部分信息,以允许程序员读取和修改构件的属性。反射 提供了一种机制——用来检查可用的方法,并返回方法名。Java通过JavaBeans提供了基于构件的编程架构 (22章)。Class类 与java.lang.reflect 类库一起对反射 概念进行了支持,该类库包含了Field 、Method 以及Constructor 类(每个类都实现了Member 接口)。这些类型的对象是由JVM 在运行时创建的,用以表示未知类里对应的成员。这样你就可以使用Constructor 创建新的对象,用get() 和set() 方法读取和修改与Field 对象关联的字段,用invoke() 方法调用与Method对象关联的方法。另外,还可以调用getFields() 、getMethods() 和getConstructors() 等很便利的方法,以返回表示字段、方法以及构造器的对象的数组。这样,匿名对象的类信息就能在运行时被完全确定下来,而在编译时不需要知道任何事情。重要的是,要认识到反射机制并没有什么神奇之处。当通过反射与一个未知类型的对象打交道时,我们同样还是得先知道它属于哪个类(就像RTTI 一样),并且加载其Class对象 。所以反射 和RTTI 的根本区别在于,对RTTI 来说,编译器在编译时打开和检查.class文件 ,对反射机制 来说,.class文件在编译时是不可获取的,所以是运行时打开和检查.class文件。
类方法提取器
在进行了那么长的概念介绍后,我们来做几个有关反射的实验。平常我们在查阅某个具体类的定义时,只能看见其新定义 或重写 的方法。而有些方法我们可以需要去父类或者其父类的父类去查阅。在反射机制 中,它给我们提供了一个很便利的方法,下面是例子(当然对于开发工具如此便利的现在来说,根本不需要这样的方法):
package test;
import java.lang.reflect.*;
public class Test {
private void test1 () {}
protected void test2 () {}
void test3() {}
public void test4 () {}
public static void main (String[] args) {
analysis("test.Test" );
System.out .println("---------" );
analysis("java.lang.String" );
}
public static void analysis (String path) {
try {
Class<?> c = Class.forName(path);
Method[] methods = c.getMethods();
Constructor[] constructors = c.getConstructors();
for (Method method : methods) {
System.out .println(method.toString().replaceAll("\\w+\\." , "" ));
}
for (Constructor constructor : constructors) {
System.out .println(constructor.toString().replaceAll("\\w+\\." , "" ));
}
} catch (ClassNotFoundException e) {
System.out .println("No such class:" + e);
}
}
}
其他反射样例
书上关于反射的用法就到此为止了,下面我再举一个例子,以熟悉反射中的一些方法。
package test;
import java.lang.annotation.Annotation;
import java.lang.reflect.*;
import java.util.Arrays;
class Father {
private String father_field1 = "father_field1" ;
String father_field2 = "father_field2" ;
protected String father_field3 = "father_field3" ;
public String father_field4 = "father_field4" ;
private void father_method1 () {
System.out.println("father_method1" );
}
void father_method2() {
System.out.println("father_method2" );
}
protected void father_method3 () {
System.out.println("father_method3" );
}
public void father_method4 () {
System.out.println("father_method4" );
}
}
class Son extends Father {
private String field1 = "field1" ;
String field2 = "field2" ;
protected String field3 = "field3" ;
public String field4 = "field4" ;
public static String field5 = "field5" ;
@Deprecated
private void method1 (int i) {
System.out.println("method1" );
}
void method2() {
System.out.println("method2" );
}
protected void method3 () {
System.out.println("method3" );
}
public void method4 () {
System.out.println("method4" );
}
public static void method5 () {
System.out.println("method5" );
}
private Son (int i) {
System.out.println("constructor1 number is: " + i);
}
Son(String i) {
System.out.println("constructor2" );
}
protected Son (double i) {
System.out.println("constructor3" );
}
public Son () {}
}
public class Test {
/**
* 自定义打印数组
* @param objs
*/
private static void printArray (Object[] objs) {
System.out.println(Arrays.toString(objs).replaceAll("\\w+\\." , "" ));
}
/**
* 自定义打印某对象
* @param obj
*/
private static void printObj (Object obj) {
System.out.println(obj.toString().replaceAll("\\w+\\." , "" ));
}
private static void analysis (Object obj)
throws NoSuchFieldException, SecurityException,
IllegalArgumentException, IllegalAccessException,
NoSuchMethodException, InvocationTargetException,
InstantiationException {
Class<?> c = obj.getClass();
Field[] fields1 = c.getDeclaredFields();
Field[] fields2 = c.getFields();
printArray(fields1);
printArray(fields2);
Field field1 = c.getDeclaredField("field1" );
Field field3 = c.getField("father_field4" );
field1.setAccessible(true );
printObj(field1.get(obj));
printObj(field3.get(obj));
printObj(c.getField("field5" ).get(null ));
if (String.class.isAssignableFrom(field1.getType())) {
field1.set(obj, "123" );
System.out.println(field1.get(obj));
}
Method[] methods1 = c.getDeclaredMethods();
Method[] methods2 = c.getMethods();
printArray(methods1);
printArray(methods2);
Method method1 = c.getDeclaredMethod("method1" , int .class);
Annotation[] as = method1.getDeclaredAnnotations();
printArray(as);
method1.setAccessible(true );
method1.invoke(obj, 1 );
c.getDeclaredMethod("method5" ).invoke(null );
Constructor[] constructors1 = c.getConstructors();
Constructor[] constructors2 = c.getDeclaredConstructors();
Constructor constructor1 = c.getDeclaredConstructor(int .class);
constructor1.setAccessible(true );
Object obj2 = constructor1.newInstance(1 );
}
public static void main (String args[]) throws Exception {
analysis(new Son());
}
}
动态代理
代理 之前我们已经说过了,那么什么是动态代理 呢,请看下面的例子:
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Arrays;
interface A {
void a();
}
interface B {
String b(int i);
void b(int i, int j);
}
class ABImpl implements A , B {
public String b(int i) {
return i + "" ;
}
public void b(int i, int j) {
}
public void a() {
}
}
class BImpl implements B {
public String b(int i) {
return i + "" ;
}
public void b(int i, int j) {
for (; i<j; i++);
}
}
class MethodSelector implements InvocationHandler {
Object obj;
public MethodSelector(Object obj) {
this .obj = obj;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
long time = System.currentTimeMillis();
Object o = method.invoke(obj, args);
System.out.printf("method:%s args:%s\nruntime: %d\n" ,
method.getName(), Arrays.toString(args),
System.currentTimeMillis() - time);
return o;
}
}
public class Test {
public static void consumerA(A a) {
a.a();
}
public static void consumerB(B b) {
System.out.println("return value: " + b.b(1 ));
b.b(1 , 100000000 );
}
public static void main(String args[]) throws Exception {
Object o1 = Proxy.newProxyInstance(
Test.class.getClassLoader(),
new Class[]{A.class, B.class},
new MethodSelector(new ABImpl()));
consumerA((A) o1);
consumerB((B) o1);
Object o2 = Proxy.newProxyInstance(
Test.class.getClassLoader(),
new Class[]{B.class},
new MethodSelector(new BImpl()));
consumerB((B) o2);
}
}
Proxy.newProxyInstance()的方法能动态生成接口,好处就是中间走了一层代理。有兴趣的可以再研究研究。
接口与类型信息
我们都知道,interface关键字的一个重要目标是允许程序隔离构件,进而降低耦合性。比如A类和B类都实现了C接口。我们将其认定为C接口的类型,就可以写出共用的代码。但是这是不是意味着我们在只知道它实现了C接口就无法去调用A类的特有方法了呢?
interface A {
void a();
}
class Son1 implements A {
public void b() {System.out.println("b1" );}
public void a() {System.out.println("a1" );}
}
public class Test {
public static void main(String args[]) {
A a = new Son1();
if (a instanceof Son1) {
((Son1) a).b();
}
}
}
那这可不行啊,我既然告诉你是A类型,你就不能使用除A类型以外的方法,不然不是不安全了吗。于是我们想到只要把Son1 定义成私有的内部类不就行了么?更严格一点,我定义成匿名内部类 ,这下你连强转都没法写了!不仅如此,我还要把方法定义成private 的,看你还怎么调用!
interface A {
void a();
}
class Helper {
public static A getA () {
return new A() {
private final int a = 1 ;
private int b = 2 ;
private void b () {
System.out .println(b);
}
public void a () {
System.out .println(a);
}
};
}
}
public class Test {
public static void main (String args[]) {
A a = Helper.getA();
}
}
那是否我们就没办法操作了呢?可别忘了,我们刚刚学过反射呀!只要我们知道你有b 方法(通过javap反编译,或者反射获得所有的Method ),那要调你不是简简单单吗?
import java.lang.reflect.*;
interface A {
void a();
}
class Helper {
public static A getA () {
return new A() {
private final int a = 1 ;
private int b = 2 ;
private void b () {
System.out .println(b);
}
public void a () {
System.out .println(a);
}
};
}
}
public class Test {
public static void main (String args[]) throws Exception {
A a = Helper.getA();
Class<?> c = a.getClass();
Method method_b = c.getDeclaredMethod("b" );
method_b.setAccessible(true );
method_b.invoke(a);
Field field_b = c.getDeclaredField("b" );
field_b.setAccessible(true );
field_b.set (a, 520 );
method_b.invoke(a);
Field field_a = c.getDeclaredField("a" );
field_a.setAccessible(true );
field_a.set (a, 520 );
a.a();
}
}
综上所述,并没有绝对的安全。然后对于最后一个修改final值的例子,我遇到了一个很诡异的问题,我个人是猜测两种初始化的方式和运行前绑定造成的。大家可以帮忙解决这个问题 。