Java是如何让我们在运行时识别对象和类的信息的。主要有两种方式:一种是“传统的”RTTI,它假定我们在编译时已经知道了所有的类型;另一种是“反射”机制,它允许我们在运行时发现和使用类的信息。
RTTI和反射之间真正的区别只在于,对RTTI来说,编译器在编译时打开和检查.class文件。而对于反射机制来说,.class文件在编译时是不可获取的,所以是在运行时打开和检查.class文件。
一、RTTI
1、RTTI的含义:Run Time Type Information,在运行时,识别一个对象的类型。在Java中,所有的类型转换都是在运行时进行正确性检查(p314)
要理解RTTI在Java中的工作原理,首先必须知道类型信息在运行时是如何表示的。RTTI在Java中有三种形式:Class对象、类字面变量(即类名.class)、instanceof。2、Class
Class它包含了与类有关的信息。事实上,Class对象就是用来创建类的所有的“常规”对象(参考p313 Shape的例子理解,Class相当于Shape)。Java使用Class对象来执行其RTTI,即使你正在执行的是类似转型这样的操作。
类是程序的一部分,每个类都有一个Class对象。换言之,每当编写并且编译一个新类,就会产生一个Class对象(更恰当地说,是被保存在一个同名的.class文件中)。只要是在源程序中出现的类型,都会有各自的Class实例对象,
例如:int.class,int[].class,void.class。
为了生成这个类的对象,运行这个程序的Java虚拟机(JVM)将使用被称为“类加载器”的子系统。类加载器子系统实际上可以包含一条类加载器链,但是只有一个原生类加载器,它是JVM实现的一部分。
如何得到某个class文件对应的class对象呢?有三种方式可以得到以String为例
注意:不管这个类生成了多少个对象,它们所对应的Class对象只有唯一的一个在内存中
public static void main(String[] args) throws Exception {
//第一种:类名.class
Class c1 = String.class;
//第二种:对象.getClass()
Class c2 = new String().getClass();
//第三种:Class.forName()
Class c3 = Class.forName("java.lang.String"); //全路径
System.out.println(c1==c2);//true
System.out.println(c1==c3);//true
System.out.println(c3==c2);//true
}
Class.forName返回的是Class对象的引用,调用forName方法后,如果该类没有被加载就加载它
getClass返回的是该对象的实际类型的Class引用,这个方法属于根类Object的一部分
(1)泛化的Class引用
Class泛化即给Class加上泛型,如下:
public static void main(String[] args) {
Class<?> intclass = int.class;
Class<? extends Number> intclass2 = int.class;//创建一个Class引用并限定为某种类型
}
(2)新的转型语法,即cast()方法
Class<House> houseType = House.class;
House h = houseType.cast(b);
3、类字面变量(类名.class)
通过“类名.class”来生成Class对象不仅更简单,也更安全,因为在编译时就会受到检查(因此不需要置于try语句块中),并且根除了对forName()方法的调用,所以更高效。(p318)
注意,使用“.class”来创建对Class对象的引用时,不好自动地初始化该Class对象
类字面变量不仅可以应用于普通的类,也可以应用于接口、数组以及基本数据类型。另外,对于基本数据类型的包装器类,还有一个标准字段TYPE。TYPE字段是一个引用,指向对应的基本数据类型的Class对象,如下所示:
但是需注意,int.class不等价于Integer.class。
public static void main(String[] args) {
System.out.println(int.class == Integer.class);//false
System.out.println(int.class == Integer.TYPE);//true
}
4、Instanceof 和 isInstance
Instanceof有比较严格的限制:只能将其与命名类型进行比较,而不能与Class对象作比较。如果程序中编写了许多的instanceof表达式,就说明你的设计可能存在瑕疵。
Instanceof和isInstance保存了类型的概念,它指的是“你是这个类吗,或者你是这个类的派生类吗?”,而如果欧勇==比较实际的Class对象,就没有考虑继承——它或者是这个确切的类型,或者不是。
二、反射
Class类与java.lang.reflect类库一起对反射的概念进行了支持,该类库包含了Field(类的成员变量,属性)、Method(类的方法)以及Constructor(类的构造方法),每个类都实现了Member接口。这些类型的对象是由JVM在运行时创建的,用以表示未知类里对应的成员。(p335)
反射就是把JAVA类中的各种成分映射成一个个的JAVA对象。例如,一个类有:成员变量,方法,构造方法,包等信息,利用反射技术可以对一个类进行解剖,把各个组成部分映射成一个个对象。通俗的理解反射就像建大厦时的设计图纸,Class就是用来描述类的。
(1)、通过反射获取类的属性(Field)
public class Person {
public String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
public static void main(String[] args) throws Exception {
// 取得Person类对应的字节码对象Class
Class c = Class.forName("com.cyqg.test.Person");
// 取得该类的构造方法,创建一个实例
Constructor con = c.getConstructor();
User p = (User) con.newInstance();
// 获取name属性
Field f = c.getField("name");
// f.getName();//获取属性的名字
// Field[] fields = c.getFields();//获取所有字段
f.set(p, "杰克");// p.setName("杰克") [对象,对象属性]设置属性的值
System.out.println("用户名:" + p.getName());
}
注意:私有的属性也是通过getDeclared...来实现,同时要设置可访问性
(2)、通过反射获取类里面的方法
public class Person {
//无参
public void show(){
System.out.println("public void show()");
}
//有参
public void show(String[] likes,double salary){
System.out.println("public void show(String[] likes,double salary)");
}
//有返回值
public int count(int i,int j){
System.out.println("public int count(int i,int j)");
return i+j;
}
}
public static void main(String[] args) throws Exception {
Class c = Class.forName("com.cyqg.test.Person");
Constructor con = c.getConstructor();
Person p = (Person) con.newInstance();
Method m1 = c.getMethod("show");//无参[这里有两个参数:方法名,参数类型(可变参数)]
m1.invoke(p);//执行方法 [两个参数:谁执行,可变参数]
Method m2 = c.getMethod("show", String[].class,double.class);//有参[这里有两个参数:方法名,参数类型(可变参数)]
m2.invoke(p, new String[]{"A","B","C"},5000);
Method m3 = c.getMethod("count",int.class,int.class);//带返回值
Integer returnValue = (Integer) m3.invoke(p,3,8);//获取返回值
System.out.println(returnValue);
}
注意:私有的属性也是通过getDeclared...来实现,同时要设置可访问性
(3)、Constructor.
Constructor类的实例对象代表类的一个构造方法。通过反射怎样获取一个类的构造方法呢?
public class Person {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
// 无参构造方法
public Person() {
System.out.println("无参构造方法");
}
// 有参构造方法
public Person(String name, int age) {
System.out.println("有参构造方法");
}
public void show() {
System.out.println("public void show()");
}
}
public static void main(String[] args) throws Exception {
//取得Person类对应的字节码对象Class
Class clazz = Class.forName("com.cyqg.test.Person");
//取得该类的唯一构造方法
Constructor c1 = clazz.getConstructor();//clazz.getConstructor(null) 获取无参构造方法
Constructor c2 = clazz.getConstructor(String.class,int.class); //获取有参的构造方法
//创建实例
Person p1 = (Person) c1.newInstance();//c1.newInstance(null)
Person p2 = (Person) c2.newInstance("berry",10); //创建实例时要传参
//执行方法
p1.show();
p2.show();
}
访问私有的构造方法
public class Person {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
// 无参私有构造方法 protected也是一样
private Person() {
System.out.println("private Person()");
}
public void show() {
System.out.println("public void show()");
}
}
public static void main(String[] args) throws Exception {
//取得Person类对应的字节码对象Class
Class clazz = Class.forName("com.cyqg.test.Person");
//取得该类的唯一构造方法
Constructor c = clazz.getDeclaredConstructor(null);
//设置非public成员的访问性,默认false即不可访问性
c.setAccessible(true);
//创建实例
Person p = (Person) c.newInstance(null);
//执行方法
p.show();
}
注意:通过getConstructor()只能取得该类public的类型
通过getDeclaredConstructor()可以取得该类非public的类型,同时要设置非public类型的可访问性,默认为false,不可访问(c.setAccessible(true))
(4)、用反射执行某个类中的main方法
public static void main(String[] args) throws Exception {
Class c = Class.forName("com.cyqg.test.Person");
Constructor con = c.getConstructor();
Method m1 = c.getMethod("main",String[].class);
//这种方式去执行会报错:提示参数不对?
m1.invoke(null,new String[]{"A","B","C","D","E","F"});//由于main方法是静态的,所以这里的第一个参数(执行对象)可以不写
//在反射main方法时,编译器会将数组自动拆分,取第一个值,所以上面的写法会报错,有以下两种解决方式:
m1.invoke(null,(Object)new String[]{"A","B","C","D"});//将数组当作对象,此时编译器不进行拆分
m1.invoke(null,new Object[]{new String[]{"A1","B1","C1","D1"}});//在数组中嵌入另一个数组
}
例子:
public static Object copy(Object obj) throws Exception{
//获得对象类型
Class classType = obj.getClass();
System.out.println(classType);//String的输出结果 class java.lang.String
System.out.println(classType.getName());//String的输出结果 java.lang.String
//通过默认构造方法创建一个新的对象
Object objectCopy = classType.getConstructor(new Class[]{})//后面new Class这个参数表示的是这个构造方式是不带参数的
.newInstance(new Object[]{});//不带参数的实例
//获得对象的所有属性
Field[] fields = classType.getDeclaredFields();
for(int i=0;i<fields.length;i++){
Field field = fields[i];
//获取属性的名字
String fieldName = field.getName();
//获取这个属性的GET方法
String firstLetter = fieldName.substring(0,1).toUpperCase();
String getMethodName = "get" + firstLetter + fieldName.substring(1);//从1开始截取
Method getMethod = classType.getMethod(getMethodName, new Class[]{});
Object value = getMethod.invoke(obj, new Object[]{});//去调用get方法,获取属性的值
//获取这个属性的SET方法
String setMethodName = "set" + firstLetter + fieldName.substring(1);
Method setMethod = classType.getMethod(setMethodName, new Class[]{});
setMethod.invoke(objectCopy, new Object[]{value});//调用set方法,将value设值进去
}
return objectCopy;
}
三、其他
1、你可能会认为,可以通过只发布编译后的代码来阻止这种情况,但是这并不解决问题。因为只需运行javap,一个随JDK发布的反编译器即可突破这一限制。下面是一个使用它的命令行:
javap -private C
-private 标识表示所有的成员都应该显示,甚至包括私有成员。(p348)
![](http://static.oschina.net/uploads/img/201506/29134200_E7kS.jpg)
2、面向对象编程中基本的目的是:让代码只操作对基类的引用。这样,如果要添加一个新类来扩展程序,就不会影响到原来的代码。(p313)
3、不要太早地关注程序的效率问题,这是个诱人的陷阱,最好首先让程序运作起来,然后再考虑它的速度。