Java基础 - 反射
回忆一下我们刚刚进入Java的编程世界我们是怎样写代码的。有人会说,这问得是啥问题,不就是首先定义一个类,然后用new创建一个这个类的对象,然后我们持有了这个对象的引用来做一些操作。
是的,没错就是这样。
形如:
class Person {
private String name;
public void setName(String name) {
this.name = name;
}
public String name() {
return name;
}
public void say() {
System.out.println("My name is " + name);
}
}
public class World {
public static void main(String[] args) {
Person person = new Person();
person.setName("Rocky");
person.say();
}
}
类描述了对象的外观和行为,使得我们知道这个对象有哪些属性和方法。稍稍打住一下,问一个问题,这里说的"我们"是谁?
当前说的"我们"是写代码的人,比如你或者我,类是我定义的,我对这个类非常了解,我知道这个类创建的对象中有say这个方法,所以我就可以这样写:
person.say();
假设现在有这样的需求,请打印这个对象所拥有的属性和方法。
好的,这个需求很简单,我对这个对象了如指掌嘛,于是乎我就这样写了:
System.out.println("person这个对象有一个叫做name的属性");
System.out.println("person这个对象有一个叫做say的方法");
.......
写出这段代码之后,我觉得我有了这样的思考,我对这个对象非常了解,这个了解都在我的脑子里,但是我们是在写程序啊,程序本身就是用来代替脑力劳动,是否可以写一段代码,不注入我大脑主观的对这个对象的认识,然后一运行这段代码就可以输出这个对象的相关信息。
答案是可以的,Java作为一门强大的面向对象的语言,拥有也必须拥有这样的能力。我们知道所有的对象都是JVM来管理的,那么JVM对置于其中的对象就像我的大脑一样,对这些对象也是了如指掌的。
1.对象的内存布局
上面我们讨论的主体是对象,而对象终究是存在内存中的,那么它在内存中的结构是怎样的呢,或者说是如何布局的。
关于对象的表示,Java虚拟机规范是这样说的:
Java虚拟机规范不强制规定对象的内部结构应当如何表示。
HotSpot是我们常用的Java虚拟机规范的实现,我们来看看它的规定。
在HotSpot虚拟机中,对象在内存中存储的布局可以分为3块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。
HotSpot虚拟机的对象头包括两部分信息,第一部分用于存储对象自身运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。
对象头的另一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
对于对象的内存布局写到这里我们打住(详细的描述请查阅其他资料),我们现在讨论的是对象信息的问题,而上面说到了对象头中一个类型指针,虚拟机可以通过这个指针确定这个对象的类型。
JVM有能力对某个对象了如执掌的原因现在就已经明了了。JVM会对对象说:"小子,你逃不过我的法眼"。
2.类元数据
上面说到了对象头有一个类型指针,指向类元数据。
现在问题来了,类元数据存在哪里?这块数据有哪些内容?
Java虚拟机规范中规定类元数据放置在方法区中(Java SE7及以前版本规范),它是各条线程共享的运行时内存区域。虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做Non-Heap(非堆),目的应该是与Java堆区分开来。
当虚拟机装载某个类型时,它使用类装载器定位相应的class文件,然后读入这个class文件——一个线性二进制流数据——然后将它传输到虚拟机中。紧接着虚拟机提取其中的类型信息,并将这些信息存储到方法区中。
类元数据包括如下内容:
- 这个类型的全限定名
- 这个类型的直接超类的全限定名(除非这个类型是java.lang.Object,它没有超类)
- 这个类型是类类型还是接口类型
- 这个类型的访问修饰符(public、abstract或final的某个子集)
- 任何直接超接口的全限定名的有序列表
- 该类型的常量池
- 字段信息
- 方法信息
- 除了常量以外的所有类(静态)变量
- 一个到类ClassLoader的引用
- 一个到Class类的引用
有了这个类元数据,对象对于我们来说完全透明了。
不过,现在有个问题,对象头中是包含了指向类元数据的指针,指向了内存空间中表示类元数据的一块局域,但是它是个指针啊,Java中是没法操作指针的,那么在代码中我想获取对象的类元数据貌似是没有办法呢。
当然是有办法的。Java语言不会这么傻,而且还会为我们做很多。
3.Class对象
看看上面类元数据的内容,我们看到最后一项"一个到Class类的引用"。这个是什么?
指向Class类的引用:对于每一个被装载的类型(不管是类还是接口),虚拟机都会相应地为它创建一个java.lang.Class类的实例,而且虚拟机还必须以某种方式把这个实例和存储在方法区中的类型数据关联起来。给出一个指向Class对象的引用,就可以通过Class类中定义的方法来找出这个类型的相关信息。
为什么要这样通过Class对象来获取类元数据?
答案是:面向对象编程,Class对象被作为程序访问方法区中的类型数据的外部接口,封装了访问类型数据的一系列操作。
可以看出,获取对象的类元数据,绕了一个弯,也可以说是间接操作。通过对象头的指针获取类元数据,然后就知道了指向Class对象的引用,接着获取到这个对象,通过这个对象再反过来访问类元数据。
Class对象是什么时候创建的
虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。
类加载的过程分为:装载、连接和初始化。
在装载阶段,虚拟机需要完成以下3件事情:
1)通过一类的全限定名来获取定义此类的二进制字节流。
2)将这个字节流所代表静态存储结构转化为方法区的运行时数据结构。
3)在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据访问入口。
现在可以知道Class对象是在类加载过程中装载阶段创建的。
Class对象存放在哪里
Class对象是java.lang.Class类的一个实例,不过它虽然是一个对象,但是并没有明确确定是在Java堆中,对于HotSpot虚拟机而言,Class对象比较特殊,它虽然是对象,但是存放在方法区里面。
如何获取Class对象的引用
在Java程序中,获取某个类的Class对象有以下三种方法:
1)通过Class类的静态方法获取
Class类中的一个静态方法可以让用户得到任何已装载的类的Class实例引用。
public static Class<?> forName(String className) throws ClassNotFoundException
public static Class<?> forName(String name, boolean initialize, ClassLoader loader)
throws ClassNotFoundException
2)通过对象的getClass方法获取
可以调用任何对象引用的getClass()方法获取对象引用。这个方法被来自Object类本的所有对象继承:
public final native Class<?> getClass();
3) 类字面量方式
形如:
Person.class
这样做不仅更简单,而且更安全,因为它在编译时就会受到检查(因此不需要至于try语句块中)。
4.这就是反射
运行时获取对象的类型信息
上面我们已经讲到,获取Class对象引用,就可以知道对象的全貌。Class对象是java.lang.Class类的一个实例,Class类中定义了很多获取类型信息的方法,假设对这个类比较熟悉,现在来重新实现我们上面讲的那个获取person对象类型信息的需求。
代码如下:
Person person = new Person();
Class clazz = person.getClass();
// 打印person对象的属性
Field[] fields = clazz.getDeclaredFields();
System.out.println("======person对象的属性======");
for (Field field : fields) {
System.out.println(field.getName());
}
// 打印person对象的方法
System.out.println("======person对象的方法======");
Method[] methods = clazz.getDeclaredMethods();
for (Method method : methods) {
System.out.println(method.getName());
}
输出结果如下:
======person对象的属性======
name
======person对象的方法======
name
setName
say
借助于Class对象,运行代码,我们就可以知道对象的类型信息。
运行时创建对象
类元数据中包含了创建类对象所需要的信息。我们主动创建对象的时候通过构造方法来创建对象,但是我们也知道通过Class对象可以访问和操作元数据,那么通过Class对象来创建类对象是否可行呢?
答案是可行的,通过Class对象可以构造类的对象。
Class<Person> clazz = Person.class;
Person person = clazz.newInstance();
有人会问,我可以直接创建对象,为什么要用Class对象方法调用的方式创建对象呢,当然了,在实际开发中如果能直接创建对象就直接创建对象,之所以以这种方式创建对象是为了满足某些需求。
多态使得我们可以面向接口或者基类来进行编程,代码中只书写基类或者接口,运行时才会确定到底使用基类的哪个派生类或者接口的实现类。
比如像我们使用Spring,我们在xml文件中定义bean,这个bean定义会指定具体的Class类的全限定名,我们在代码中通过这个全限定名加载类,获取到Class对象后,就可以创建代表这个bean的对象。
还有比如说JDBC驱动的加载。
还有很多场景需要在运行时创建对象,而Java提供了这种支持。
运行时调用对象的方法
我们上面的代码中,Person类有一个say方法,我们创建对象后,通过主动调用的方式来调用这个方法:
person.say();
那么,在运行时动态调用是否可行呢?
答案是可行的,我们可以通过下面的方式调用:
Class<Person> clazz = Person.class;
Method method = clazz.getMethod("say");
Person person = new Person();
person.setName("Rocky");
method.invoke(person);
有的人还是会说,会什么要用这种方式调用,还是那句话能通过对象直接调用方法来操作就用直接方式,当你发现无法直接调用对象方法而用需要调用对象方法的时候通过这种运行时调用的方式可以帮助你解决问题。
小结
现在我们来概括一下反射是什么:
Java反射机制是在运行状态中,对于任意一个类,都能够获得这个类的所有属性和方法,对于任意一个对象都能够调用它的任意一个属性和方法。这种在运行时动态的获取类信息以及动态调用对象的方法的功能称为Java的反射机制。