参考:
《尚硅谷Java入门视频教程》https://www.bilibili.com/video/BV1Kb411W75N
《【狂神说Java】Java零基础学习视频通俗易懂》https://www.bilibili.com/video/BV12J41137hu
推荐看尚硅谷的视频,更加详细
动态/静态语言
动态语言是在运行时能动态改变结构的语言,例如PHP,所以可以用一句话木马执行很多命令。
静态语言是在运行时不能动态改变结构的语言,如Java、C等。
反射机制能让静态语言Java获得一些动态特性,变为准动态语言。
反射是什么
Reflection(反射)是Java被视为动态语言的关键,反射机制允许程序在执行期间借助于Reflection API取得任何类的内部信息,并能直接操作任意对象的内部属性及方法。
加载完类之后,在堆内存的方法区中就产生了一个Class类型的对象(一个类只有一个Class对象),这个对象就包含了完整的类的结构信息。我们可以通过这个对象看到类的结构。这个对象就像一面镜子,透过这个镜子看到类的结构,所以,形象地称之为:反射。
反射,要注意这个“反”字,例如下图:
优缺点
优点:可以实现动态创建对象和编译,体现出很大的灵活性
缺点:对性能有影响。使用反射基本上是一种解释操作,我们告诉JVM,我们希望做什么并且它满足我们的要求。这类操作总是慢于直接执行相同的操作。
下面是一个测试性能的例子:
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
// 分析性能问题
public class Test10 {
// 普通方式调用
public static void test01() {
User user = new User();
long startTime = System.currentTimeMillis();
for (int i = 0; i < 1000000000; i++) {
user.getName();
}
long endTime = System.currentTimeMillis();
System.out.println("普通方式执行10亿次:" + (endTime - startTime) + "ms");
}
// 反射方式调用
public static void test02() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
User user = new User();
Class c1 = user.getClass();
Method getName = c1.getDeclaredMethod("getName", null);
long startTime = System.currentTimeMillis();
for (int i = 0; i < 1000000000; i++) {
getName.invoke(user, null);
}
long endTime = System.currentTimeMillis();
System.out.println("反射方式执行10亿次:" + (endTime - startTime) + "ms");
}
// 反射方式调用,关闭检测
public static void test03() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
User user = new User();
Class c1 = user.getClass();
Method getName = c1.getDeclaredMethod("getName", null);
getName.setAccessible(true);
long startTime = System.currentTimeMillis();
for (int i = 0; i < 1000000000; i++) {
getName.invoke(user, null);
}
long endTime = System.currentTimeMillis();
System.out.println("关闭检测执行10亿次:" + (endTime - startTime) + "ms");
}
public static void main(String[] args) throws InvocationTargetException, NoSuchMethodException, IllegalAccessException {
test01();
test02();
test03();
}
}
执行结果:
Class对象
要学会反射,首先要学习一下Class对象,要学习Class对象,首先要简单复习一下类的加载过程。
一个Class对象就是java.lang.Class
类的一个实例。
.java文件(源代码)经过javac.exe编译后生成一个或多个.class文件(字节码文件),使用java.exe对某个字节码文件进行解释运行,相当于将某个字节码文件加载到内存中,这就是类的加载。这个加载到内存中的类,就是java.lang.Class
的一个实例,也称为运行时类。
上面说过,这个java.lang.Class
对象包含了完整的类的结构信息。我们可以通过这个对象看到类的结构。通过这个对象,可以获得其对应类的属性、方法、构造器、实现的接口等。那我们怎么获取这个对象呢?有三种方式可以获取一个类的Class对象:
-
调用运行时类的class属性:
Class clazz1 = User.class;
-
通过运行时类的对象,调用getClass():
User u1 = new User(); Class clazz2 = u1.getClass();
-
调用Class的静态方法:forName(String classPath)
Class clazz3 = Class.forName("com.a.reflection.User");
。forName方法既可以获取我们自己写的类的Class对象,也可以获取系统类的Class对象。
上面三种方式如果获取的是同一个类的Class对象,则它们获取的是同一个对象,也就是 clazz1 == clazz2 == clazz3,因为一个类同一时间只会在内存中生成一个Class对象。加载到内存中的运行时类,会缓存一定的时间,在此时间之内,我们可以通过不同的方式来获取此运行时类。
除了类,还可以有Class对象的类型有:interface(接口)、数组(对于数组来说,只要数组的元素类型与维度一样,就是同一个Class)、enum(枚举)、annotation(注解)、基本数据类型、void(void也可以看作一种类型)。
下面是Class对象的一些注意点:
- Class对象只能由系统建立
- 一个加载的类在JVM中只会有一个Class实例
- 一个Class对象对应的是一个加载到JVM中的.class文件
- 每个类的实例都会记得自己是由哪个Class实例所生成
- 通过Class对象可以完整地得到一个类中的所有被加载的结构
- Class类是Reflection的根源,针对任何想动态加载、运行的类,唯有先获得相应的Class对象。
类的加载
下面简单讲一下类的加载过程,类的加载分为3个过程(加载、链接、初始化):
- 加载:使用java.exe将字节码文件内容加载到内存中,并生成一个代表这个类的
java.lang.Class
对象。 - 链接:为类变量(static)分配内存并设置默认初始值。
- 初始化:使用类构造器<clinit>()方法对类进行初始化,顺序是:静态变量显式赋值/静态代码块 => 匿名代码块 => 构造方法。初始化一个类时,如果发现其父类还没有被初始化,则会先初始化它的父类。
反射的实际应用
创建运行时类的对象
newInstance()
: 创建对应的运行时类的对象
Class<Person> clazz = Person.class;
Person person = clazz.newInstance();
只有通过构造器才能构造对象,newInstance()
内部也是调用了对应类的无参构造器。
newInstance()
能正常地创建运行时类的对象的条件:
- 运行时类必须提供空参的构造器。
- 运行时类的空参构造器可被访问,通常设置为
public
。
获取运行时类的完整结构
包括获取运行时类的所有方法、构造器、父类、接口、所在包、注解等,都是用形如getXxx()
的方法来实现的。
Class clazz = Person.class;
// getFields(): 获取当前类及其父类的所有public属性
Field[] fields = clazz.getFields();
// getDeclaredFields(): 获取当前运行时类声明的所有属性,包括所有权限(不包含父类中声明的属性)。
Field[] declaredFields = clazz.getDeclaredFields();
// getMethods(): 获取当前运行时类及其父类声明的所有public方法。
Method[] methods = clazz.getMethods();
// getDeclaredMethods(): 获取当前运行时类声明的所有方法,包括所有权限(不包含父类中声明的方法)。
Method[] declaredMethods = clazz.getDeclaredMethods();
// getConstructors(): 获取当前运行时类中声明为public的构造器
Method[] methods = clazz.getConstructors();
// getDeclaredConstructors(): 获取当前运行时类中声明的所有构造器
Method[] declaredMethods = clazz.getDeclaredConstructors();
// getSuperclass(): 获取运行时类的父类
Class superclass = clazz.getSuperclass();
Class[] interfaces = clazz.getInterfaces();
// 要获取父类的接口,可以先获取父类,再获取接口
Class[] interfaces1 = clazz.getSuperclass().getInterfaces();
// 获取当前运行时类所在的包
Package pack = clazz.getPackage();
// 获取运行时类声明的注解
Annotation[] annotations = clazz.getAnnotations();
调用运行时类的指定结构
操作运行时类中的指定属性
Class clazz = Person.class;
// 创建运行时类的对象
Person p = (Person) clazz.newInstance();
// 获取指定的public属性
Field id = clazz.getField("id");
// getDeclaredField(String fieldName): 获取运行时类中指定的属性
Field name = clazz.getDeclaredField("name");
// 使当前属性可访问
name.setAccessible(true);
/*
设置当前属性的值
set(): 参数1:指明设置哪个对象的属性 参数2:将此属性值设置为多少
*/
id.set(p, 1001);
// 设置静态属性的值
id.set(null, 1001);
/*
获取当前属性的值
get(): 参数1:获取哪个对象的当前属性值
*/
int pId = (int) id.get(p);
// 获取静态属性的值
int pId2 = (int) id.get(null);
操作运行时类中的指定方法
Class clazz = Person.class;
// 创建运行时类的对象
Person p = (Person) clazz.newInstance();
/*
1. 获取指定的某个方法
getDeclaredMethod(): 参数1:指明获取的方法的名称 参数2:指明获取的方法的形参列表
*/
Method show = clazz.getDeclaredMethod("show", String.class);
// 2. 保证当前方法是可访问的
show.setAccessible(true);
/*
3. invoke(): 参数1:方法的调用者 参数2:给方法形参赋值的实参
invoke()的返回值即为对应类中调用的方法的返回值。
*/
Object returnValue = show.invoke(p, "CHN");
// 调用静态方法
// private static void showDesc()
Method showDesc = clazz.getDeclaredMethod("showDesc");
showDesc.setAccessible(true);
// 如果调用的运行时类中的方法没有返回值,则此invoke()返回null
Object returnVal = showDesc.invoke(null);
调用运行时类中的指定构造器
Class clazz = Person.class;
// private Person(String name)
/*
1. 获取指定的构造器
getDeclaredConstructor(): 参数:指明构造器的参数列表
*/
Constructor constructor = clazz.getDeclaredConstructor(String.class);
// 2. 保证此构造器是可访问的
constructor.setAccessible(true);
// 3. 调用此构造器创建运行时类的对象
Person per = (Person) constructor.newInstance("Tom");