文章目录
Java反射
今天来学习一下Java一个非常重要的特性:反射。
JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。
当然这里不理解没关系,我们来详细解释一下Java反射机制,在介绍之前,先了解几个基本概念:动态语言和静态语言。
动态语言VS静态语言
动态语言:
是一类在运行时可以改变其结构的语言:例如新的函数、对象、甚至代码可以 被引进,已有的函数可以被删除或是其他结构上的变化。
主要动态语言:Object-C、C#、JavaScript、PHP、Python、Erlang。
简单说就是动态语言在运行时代码可以根据某些条件改变自身结构,比如运行时新加个函数并调用。
静态语言:
与动态语言相对应的,运行时结构不可变的语言就是静态语言。如Java、C、C++。Java不是动态语言,但Java可以称之为“准动态语言”。即Java有一定的动态性,我们可以利用反射机制、字节码操作获得类似动态语言的特性。 Java的动态性让编程的时候更加灵活!
而静态语言就是编译完成后,运行时无法动态的添加新的函数新的变量之类的。我们可以看到Java不是动态语言,但是个准动态语言这是为什么呢?这就是反射的功劳了。
Java语言的编译和运行过程
ok,为了帮助理解,我们从Java编译运行开始介绍:
Java编译运行过程一般分为以下三个阶段:
第一阶段:源码阶段。通俗来说就是我们编写的demo.java
源文件,我们使用javac demo.java
可以生成字节码文件demo.class
,字节码文件中存放着我们编写的变量,方法等,此时字节码文件只是存在硬盘上,还没有进入内存调用。
第二阶段:Class类对象阶段。通过类加载器(ClassLoader
)把字节码文件demo.class
加载进内存。在Java语言中,万物皆可对象。那我们自己编写的demo.class
也是个对象喽,那这个Class对象的类是谁呢?当然就是Class类了。Class类是一个特殊类,它用于表示JVM运行时类或接口的信息。第二阶段就是通过Class这个类对象描述demo.class
这个字节码内存空间,Class类对象里面就有三个比较重要的东西,成员变量,成员方法和构造方法;把这三部分同样都封装为对象,成员变量封装为Field
对象,构造方法封装为Constructor
对象,成员方法封装为Method
对象。因为成员变量可能不止一个,所以一般都用数组Field[ ] fields
来描述成员变量,其他两部分也是用数组的形式。这实际上就是反射的基础,这就是为什么说反射就是把java类中的各种成分映射成一个个的Java对象。
第三阶段:Runtime运行时阶段。这个时候JVM内存中就有了我们编写的dome.class
的类对象了我们就可以new demo()
生成一个对象并调用执行啦。
经过上述三个阶段,你是不是会发现一些奇怪的东西:既然.class
文件是以Class类对象存在的,并且.class
中的方法也是一个对象,那我岂不是可以随便访问各个Class类对象,随便执行各个类方法喽?没错,实际上这就是反射,实现Java反射机制的类都位于java.lang.reflect
包中:
-
java.lang.Class类:代表整个字节码。代表一个类型,代表整个类。
-
java.lang.reflect.Field类:代表字节码中的属性字节码。代表类中的成员变量(静态变量+实例变量)。
-
java.lang.reflect.Method类:代表字节码中方法字节码。代表类的方法。
-
java.lang.reflect.Constructor类:代表字节码中的构造方法字节码。代表类的构造方法。
-
Array类:提供了动态创建数组,以及访问数组的元素的静态方法
当然我们首先需要拿到这个类对象,我们怎么拿到整个类对象呢???
Class加载器
在将硬盘文件.class
加载至JVM内存中时,我们使用到了ClassLoader
类加载器,类加载器主要分为三大类:
-
根加载器(null):
它是由本地代码(c/c++)实现的,你根本拿不到他的引用,但是他实际存在,并且加载一些重要的类,它加载
(%JAVA_HOME%\jre\lib)
,如rt.jar(runtime)、i18n.jar
等,这些是Java的核心类。 -
平台类加载器(PlatformClassLoader):
虽说能拿到,但是我们在实践中很少用到它,它主要加载扩展目录下的jar包,
%JAVA_HOME%\lib\ext
-
应用类加载器(appClassLoader):
它主要加载我们应用程序中的类,如Test,或者用到的第三方包,如jdbc驱动包等。这里的父类加载器与类中继承概念要区分,它们在class定义上是没有父子关系的。
当一个类被加载时,存在着委托加载机制,比如当我们加载demo.class
时,他首先会启动应用类加载器加载demo类,但是这个加载器不会直接去加载demo.class
,而是先看看是否有父加载器,结果有是扩展类加载器;扩展类加载器同样的,先查看是否有父加载器,结果是根类加载器。所以首先由根类加载器去加载这个类,根类加载器会在%JAVA_HOME%\jre\lib
寻找这个类,结果没找到,返回扩展加载器;扩展加载器在%JAVA_HOME%\lib\ext
寻找,没找到继续返回应用类加载器;应用加载器在当前应用的classpath中搜索,结果找到了。
这就死Java中著名的委托加载机制:
参考链接:
获取Class类对象
在上面三个阶段中,我们都可以获得Class类对象,只不过方式不同。
获取Class对象的方式:
1. Class.forName("全类名"):将字节码文件加载进内存,返回Class对象。(全类名:包名+类名)
- 多用于配置文件,将全类名定义在配置文件中。读取文件,加载类。
- 方法实际上也是调用的CLassLoader来实现的
2. 类名.class:通过类名的属性class获取
- 多用于参数的传递,有时候传参需要传一个Class对象
3. 对象.getClass():getClass方法在Object中定义着
- 多用于对象获取字节码的方式
4. 使用类加载器进行加载:classLoader.loadClass()。
- 与1类似,实际上就是1的内部实现
package main.test;
import java.io.IOException;
import main.test.Person;
public class demo {
/**
* @param args
* @throws ClassNotFoundException
*/
public static void main(String[] args) throws ClassNotFoundException {
// Class.forName("全类名")。
Class cls1 = Class.forName("main.test.Person");
System.out.println(cls1);
// 类名.class
Class cls2 = Person.class;
System.out.println(cls2);
// 对象.getClass()
Person person = new Person();
Class cls3 = person.getClass();
System.out.println(cls3);
//类加载器获取类对象
ClassLoader classLoader = demo.class.getClassLoader();
Class cls4 = classLoader.loadClass("main.test.Person");
System.out.println(cls4);
}
}
同一个字节码文件(*.class)在一次程序运行中,只会被加载一次,无论通过哪一种方式获取的Class对象都是同一个
总结一下,Java整个加载与反射机制如下图所示:demo.java
内容调用Student类时,JVM发现内存中不存在Student类对象,因此JVM会动态读取硬盘上Student.class
源文件,将Student.class
加载至内存中;同时会使用Class类对象描述Student.class
,当然同一个.class
只会生成一个Class对象,之后再次调用也是使用的这一个类对象。反射的本质就是通过获得Class对象后,可以反向获取Studnet对象的各种信息。
Class对象功能
获取成员变量
+ Field[ ] getFields() :获取所有public的成员变量。
+ Filed[ ] getField(String name): 获取指定名称的public修饰的成员变量。
+ Field[ ] getDeclaredFields() : 获取所有成员变量,无视修饰符。
+ Field[ ] getDeclaredField(String name): 获取指定名称的成员变量,无视修饰符。
获取或修改变量值
+ Object get(Object obj) :获取值
+ void set(Object obj, Object value) : 设置值
+ setAccessible(true) : 暴力反射,忽略访问权限修饰符的安全检查
获取构造方法
+ Constructor<?>[] getConstructors()
+ Constructor<T> getConstructor(类<?>... parameterTypes)
+ Constructor<T> getDeclaredConstructor(类<?>... parameterTypes)
+ Constructor<?>[] getDeclaredConstructors()
+ 创建对象
+ T newInstance(Object... initargs)
+ 如果使用空参数构造方法创建对象,操作可以简化:Class对象的newInstance方法
获取成员方法
+ Method[] getMethods() 获得所有public方法,包括从父类继承的
+ Method getMethod(String name, 类<?>... parameterTypes)
+ Method[] getDeclaredMethods() 获得本类声明的所有方法
+ Method getDeclaredMethod(String name, 类<?>... parameterTypes)
执行方法:
Object invoke(Object obj, Object... args)
获取方法名
Method.getName()
获取类名
+ String getName()
Field:成员变量
1. 设置值
void set(Object obj,Object value)
2. 获取值
get(Object obj)
3. 忽略权限访问修饰符的安全检查
setAccessible(true):暴力反射
Constructor:构造方法
# 创建对象
T newInstance(Object… initargs)
如果使用空参数构造方法创建对象,操作简化为:Class对象的newInstance方法。
Method:方法对象
调用方法:
Object invoke(Object object,Object… args)
获取方法的名称:
String getName()
以Class类型,返回方法类型【一般配合Class类的getSimpleName()方法使用】
public Class<?> getReturnType()
返回方法的修饰符列表(一个方法的参数可能会有多个。)【结果集一般配合Class类的getSimpleName()方法使用】
public Class<?>[] getParameterTypes()
源码:
package main.test;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class demo {
/**
* @param args
* @throws ClassNotFoundException
* @throws SecurityException
* @throws NoSuchFieldException
* @throws IllegalAccessException
* @throws IllegalArgumentException
* @throws InvocationTargetException
* @throws InstantiationException
* @throws NoSuchMethodException
*/
public static void main(String[] args) throws ClassNotFoundException, NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException, InstantiationException, InvocationTargetException, NoSuchMethodException {
// Class.forName("全类名"),获取Class对象
// Class.forName("全类名")。
Class cls1 = Class.forName("main.test.Person");
System.out.println("Class.forName获取类对象:"+cls1);
// 类名.class
Class cls2 = Person.class;
System.out.println("类名.class获取类对象:"+cls2);
// 对象.getClass()
Person person = new Person();
Class cls3 = person.getClass();
System.out.println("对象.getClass()获取对象:"+cls3);
// 类加载器获取类对象
ClassLoader classLoader = demo.class.getClassLoader();
Class cls4 = classLoader.loadClass("main.test.Person");
System.out.println("类加载器获取类对象:"+cls4);
// 获取public修饰的成员变量
Field[] fields = cls1.getFields();
for (Field field : fields) {
System.out.println("类对象公共成员变量:" + field);
}
// 获得所有声明的成员变量
Field[] fields2 = cls1.getDeclaredFields();
for (Field field : fields2) {
System.out.println("类对象所有声明成员变量:" + field);
}
// 获得变量值
Field name = cls1.getDeclaredField("name");
// 暴力反射,忽略访问权限修饰符的安全检查
name.setAccessible(true);
Person p=new Person();
Object value = name.get(p);
System.out.println("初始值:"+value);
name.set(p, "123");
value=name.get(p);
System.out.println("修改值:"+value);
// 获取类对象构造方法
Constructor[] constructors=cls1.getConstructors();
for (Constructor constructor : constructors) {
System.out.println("类对象构造方法" + constructor);
}
// 创建对象
Object o = constructors[1].newInstance("张三", 22, "未知");
System.out.println(o);
// 获取类对象成员方法
Method[] methods = cls1.getDeclaredMethods();
for (Method method : methods) {
System.out.println("类对象成员方法" + method);
}
// 调用类对象方法
Method show = cls1.getDeclaredMethod("show");
show.invoke(o);
}
}
反射有啥用?
看到这里会觉得反射很麻烦,没有体现出它的价值。接下来看个案例,就会一目了然了。这个案例就是写一个类,在不改变该类任何代码的前提下,可以帮助我们创建任意类的对象,并且执行其中任意方法。
步骤
1. 将需要创建的对象的全类名和需要执行的方法定义在配置文件
2. 在程序中加载读取配置文件
3. 使用反射技术来加载类文件进入内存
4. 创建对象
5. 执行方法
Person类
package main.test;
public class Person {
private String name = "tian";
private int age = 22;
public String sex = "男";
public Person() {
}
public Person(String n, int a, String s) {
name = n;
age = a;
sex = s;
}
public void eat() {
}
private void sleep() {
}
public void show() {
System.out.println(name+Integer.toString(age)+sex);
}
}
pro.properties配置文件:
className=main.test.Person
methodName=show
ReflectTest类:
package main.test;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Properties;
public class ReflectTest {
public static void main(String[] args)
throws ClassNotFoundException, NoSuchFieldException, SecurityException, IllegalArgumentException,
IllegalAccessException, InstantiationException, InvocationTargetException, NoSuchMethodException, IOException {
// 前提:不能修改该类的代码
// Person p = new Person();
// p.show();
// 1.加载配置文件
// 1.1 创建Properties对象
Properties pro = new Properties();
// 1.2 加载配置文件,转化为一个集合
// 1.2.1 获取class目录下的配置文件
ClassLoader classLoader = ReflectTest.class.getClassLoader();// 通过类加载器将字节码文件加载进内存
InputStream ins = classLoader.getResourceAsStream("pro.properties");
// 这里的pro.properties是包的位置,比如说我这个是main.test包内的,所以pro.properties是在/bin/内,/bin内存放是/main/test包
pro.load(ins);
// 2.获取配置文件中定义的数据
String className = pro.getProperty("className");
String methodName = pro.getProperty("methodName");
// 3.加载该类进内存
Class cls = Class.forName(className);
// 4.创建对象
Object o = cls.newInstance();
// 5.获取方法对象
Method method = cls.getMethod(methodName);
// 6.执行方法
method.invoke(o);
}
}
将需要生成的对象的信息放在配置文件中,这样我们不需要去修改ReflectTest的代码,只需要修改配置文件中className,methodName即可;如果不使用配置文件,就需要在ReflectTest类中写死,Person p = new Person(); p.show();那如果想要生成别的对象每次都需要去找到ReflectTest类,修改Student为别的类。使用反射后,增加了程序的灵活性,避免将代码写死,降低了耦合性。在框架的设计思想里用到的就是反射。