反射(Reflection)
1、什么是反射
反射是一种非常强大的机制。反射跟注解配合,就天衣无缝了。
动态语言和静态语言
**动态语言:**是一类在运行时可以改变其结构的语言.例如新的函数、对象、甚至代码可以被引进,已有的函数可以被删除或是其他结构上的变化。通俗点说就是在运行时代码可以根据某些条件改变自身结构。动态语言主要有JavaScript、python、c#等。
**静态语言:**与动态语言相对应,运行时不能改变的语言称为静态语言。静态语言主要有C、C++、Java。
Java不是动态语言,但是它可以被称为”准动态语言“,Java具有一定的动态性,通过反射机制可以获得类似于动态语言的特性。反射让编程变得更加灵活。
Reflection(反射)是 Java 被视为动态语言的关键,反射机制允许程序在执行期借助于 Reflection API 取得任何类的内部信息,并能直接操作任意对象的内部属性及方法。
2、反射的基础——Class对象
加载完类之后,HotSpot虚拟机中,在堆内存的方法区中就产生了一个 InstanceKlass对象用来存储类的所有信息,该对象必定不能被开发者通过任何方式获取。与此同时,堆中会产生一个Class类型的对象(一个类只有一个 Class 对象,这个对象就包含了InstanceKlass对象中的元数据),我们可以通过这个对象看到类的结构。
一个类加载完在堆中生成对应的Class对象,类的实例对象是通过Class对象创建的,通过类的实例对象可以获取到Class对象(getClass()方法),从而得到类的元数据,这被形象地称为”反射“。
所以可以说,反射就是通过获取Class对象,并操作Class对象达到各种目的。
可以通过多种方式获得Class对象。
//第一种方法,通过Class类的静态方法通过全限定名获取某个类的Class对象
Class c=Class.forName("java.lang.String");
//第二种方法,通过实例化对象的getClass方法获取它的Class对象
String s1=new String("haha");
Class c=s1.getClass();
//第三种方法,已知一个类的全类名,可以通过全类名获得它的Class对象
Class c=Person.class;
//第四种方法,通过类加载器,由类的全限定名加载某个类并获得Class对象
Class类有很多方法:
3、反射获得类信息
反射第一个用途就是获得类的各种信息了。
通过Class对象,可以获得运行时类的完整结构:Field
(字段),Method
(方法),Constructor
(构造器),SuperClass
(父类),Annotation
(注解)。
package main.test;
class User{
private String id;
public void printMessage(){
System.out.println(this.id);
}
}
public class Test {
public static void main(String[] args) throws ClassNotFoundException, NoSuchFieldException {
Class c = Class.forName("main.test.User");
//获得类名
System.out.println(c.getName());
//获得类的字段 打印信息:[] 只能找到public的属性
System.out.println(Arrays.toString(c.getFields()));
//获得类的字段 打印信息:[private java.lang.String main.test.User.id] 所有属性都能得到
System.out.println(Arrays.toString(c.getDeclaredFields()));
//获得特定属性名 打印信息:private java.lang.String main.test.User.id
System.out.println(c.getDeclaredField("id"));
//获得类的方法 打印信息:[public void main.test.User.printMessage()]
System.out.println(Arrays.toString(c.getDeclaredMethods()));
//此外,还可以获得类中的构造器、注解,都是一样的套路,非常简单
}
}
4、反射操作方法和字段
第二个用途,Class对象可以获得类的一个实例化对象,也可以通过Class对象调用方法和设置字段值。
package main.test;
public class Test {
public static void main(String[] args) throws Exception {
Class c = Class.forName("main.test.User");
//=======================================================
//通过newInstance()获得实例化对象,该方法本质上是调用了User的无参构造方法,如果方法没有无参构造,会报错,所以这个方法已经过时(被@Deprecated)了
User user = (User)c.newInstance();
//打印信息:User{id='null'}
System.out.println(user);
//可以通过先获得构造器,再用指定构造器构造一个对象,这样就不会出错了
User o1 = (User)c.getDeclaredConstructor(String.class).newInstance("传参");
User o2 = (User)c.getDeclaredConstructor().newInstance();
//打印信息:User{id='传参'}
System.out.println(o1);
//打印信息:User{id='null'}
System.out.println(o2);
//=======================================================
//通过Class对象获得方法,再进行激活
User user1 = (User)c.getDeclaredConstructor(String.class).newInstance("张三");
//第一次打印user1对象中的信息:张三
System.out.println(user1.getId());
//为何这里第二个参数要传入String.class?因为同一个方法名可以重载不同类型的参数,所以必须把参数类型标清楚
Method setId = c.getDeclaredMethod("setId", String.class);
//激活方法,传参要传入User对象,表明激活的是哪一个对象中的方法
setId.invoke(user1,"李四");
//第二次打印user1对象中的信息:李四
System.out.println(user1.getId());
//=======================================================
//通过Class对象获得字段,再设置字段的值
User user2 = (User)c.getDeclaredConstructor(String.class).newInstance("张三");
//第一次打印user1对象中的信息:张三
System.out.println(user2.getId());
Field id = c.getDeclaredField("id");
//直接调用id.set(user2,"李四")会报错,因为id这个字段是私有的,所以需要先把id的权限放开
id.setAccessible(true);
id.set(user2,"李四");
//第二次打印user1对象中的信息:李四
System.out.println(user2.getId());
//这个看起来有点儿多此一举,但是之后就知道它的用处了~~~
}
}
class User{
private String id;
public User() {
}
public User(String id) {
this.id = id;
}
public void printMessage(){
System.out.println(this.id);
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
@Override
public String toString() {
return "User{" +
"id='" + id + '\'' +
'}';
}
}
通过反射获得对象的速度非常非常慢。
public static void main(String[] args) throws Exception {
int num=100000000;
Class c = Class.forName("main.test.User");
long begin1 = System.currentTimeMillis();
for (int i = 0; i <num ; i++) {
User o1 = (User)c.getDeclaredConstructor(String.class).newInstance("传参");
}
//花费时间:4179
System.out.println(System.currentTimeMillis() - begin1);
long begin2 = System.currentTimeMillis();
for (int i = 0; i < num; i++) {
User o1 = new User("传参");
}
//花费时间:0
System.out.println(System.currentTimeMillis() - begin2);
//通过反射实例化对象速度慢了超级多倍哦,想清楚再使用
}
Class对象中的Field、Method、Constructor中都有setAccessible
这个方法,可以突破private的限制。setAccessible
设置为true可以关闭Java对public、private的检测,是可以提高反射的效率的。如果发现反射的效率实在是不尽人意,可以尝试关闭语法检测,大概可以减少3-5倍的运行时间。
5、反射操作泛型
反射操作泛型,这个听起来有点儿奇怪。Java的泛型在编译器中实现,类型检查和类型推断由编译器执行,然后生成普通的非泛型的字节码,虚拟机完全不感知泛型的存在。编译器使用泛型类型信息保证类型安全,然后在生成字节码之前将其清除 。因此,字节码文件中不存在泛型信息。换而言之,Class对象中按理来说是不存在泛型的信息的。
为了通过反射操作这些类型, Java新增了ParameterizedType、GenericArrayType、TypeVariable 和 WildcardType 几种类型来代表不能被归一到 Class 类中的类型但是又和原始类型齐名的类型。
-
ParameterizedType 表示一种参数化类型比如 Collection
-
GenericArrayType 表示一种元素类型是参数化类型或者类型变量的数组类型
-
TypeVariable 是各种类型变量的公共父接口
-
WildcardType 代表一种通配符类型表达式
6、反射操作注解
实现一个小功能:实体-表关系映射。
通过Java的实体和实体中的字段,可以判断出该实体对应MySQL中的哪张表,以及表中有哪些字段。
package main.test;
import java.lang.annotation.*;
import java.lang.reflect.Field;
import java.util.Arrays;
public class Test {
public static void main(String[] args) throws Exception {
Class c = Class.forName("main.test.Student");
//通过反射获得类的注解
Annotation[] cAnnotations = c.getAnnotations();
//打印信息:[@main.test.TableMapping("db_student")]
System.out.println(Arrays.toString(cAnnotations));
//获得注解的value的值
TableMapping annotation = (TableMapping) cAnnotations[0];
//打印信息:db_student
System.out.println(annotation.value());
//同理,获得类中字段的注解的参数值
Field[] fields = c.getDeclaredFields();
FieldMapping[] fieldAnnotations = new FieldMapping[fields.length];
for (int i = 0; i < fields.length; i++) {
//已知类型,不需要强转
fieldAnnotations[i] = fields[i].getAnnotation(FieldMapping.class);
}
//打印信息: db_id,int,256
// db_age,int,3
// db_name,varchar,256
for (FieldMapping fieldAnnotation : fieldAnnotations) {
System.out.print(fieldAnnotation.columnName() + ",");
System.out.print(fieldAnnotation.type() + ",");
System.out.println(fieldAnnotation.length());
}
}
}
/**
* @author xcy
* 自定义一个类名的注解
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@interface TableMapping {
String value();
}
/**
* @author xcy
* 自定义一个属性的注解
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@interface FieldMapping {
//列名
String columnName();
//类型
String type();
//长度
int length();
}
@TableMapping("db_student")
class Student {
@FieldMapping(columnName = "db_id", type = "int", length = 256)
private int id;
@FieldMapping(columnName = "db_age", type = "int", length = 3)
private int age;
@FieldMapping(columnName = "db_name", type = "varchar", length = 256)
private String name;
public Student(int id, int age, String name) {
this.id = id;
this.age = age;
this.name = name;
}
public Student() {
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "Student{" +
"id=" + id +
", age=" + age +
", name='" + name + '\'' +
'}';
}
}
不同的注解就像是不同颜色的标签,注解的参数就像是标签中可以记录的内容。使用一个注解作用于类/字段/方法,就是为类/字段/方法添加了额外的补充信息,这些信息可以通过反射获取到。补充信息被获取到,就可以对这些信息进行处理、操作,从而让被注解的类/字段/方法有了额外的功能,而且这些功能不容易被感知。
Spring、Springboot、mybatis-plus等框架正是因为引用了大量注解,才让开发者能够用简单的几行代码实现复杂的逻辑操作。