Sorry, your browser cannot access this site
This page requires browser support (enable) JavaScript
Learn more >

Java反射机制

1、反射是什么?

Java反射机制(Java Reflection)是Java语言中一种动态(运行时)访问、检测以及修改它本身的能力。主要作用是动态获取类完整的结构信息和调用对象的方法。

正射和反射的区别:
一般情况下,我们使用某个类,都会知道这个类,以及要用它来做什么,可以直接通过 new 实例化创建对象,然后使用这个对象对类进行操作,这个就属于正射。
而反射则是不知道要初始化的是什么类,无法使用 new 来实例化创建对象,主要通过 JDK 提供的反射 API 来实现,在运行时才知道要操作的是什么类,并且可以获取到类的完整构造以及调用对应的方法,这就是反射。

举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class Student {
private int id;

public void setId(int id) {
this.id = id;
}
public int getId() {
return id;
}

public static void main(String[] args) throws Exception{
//一、正射调用过程
Student student = new Student();
student.setId(1);
System.out.println("正射调用过程Student id:" + student.getId());

//二、反射调用过程
Class clazz = Class.forName("com.justin.java.lang.Student");
Constructor studentConstructor = clazz.getConstructor();
Object studentObj = studentConstructor.newInstance();

Method setIdMethod = clazz.getMethod("setId", int.class);
setIdMethod.invoke(studentObj, 2);
Method getIdMethod = clazz.getMethod("getId");
System.out.println("正射调用过程Student id:" + getIdMethod.invoke(studentObj));
}
}

This is a picture without description

2、反射获取 Class 的三种方式

在 Java 中,反射获取 Class 有三种方式,这三种方式各有各的优点。
以下是三种获取 Class 的方式:

  1. 通过 类名.class来获取。
  2. 通过Class.forName("类的全局路径")来获取。
  3. 通过new 类名().getClass()来获取。

使用第一种方式获取 Class,JVM 会使用 ClassLoader 类加载器将类加载到内存中,但并不会做任何类的初始化工作,返回 java.lang.Class 对象。
使用第二种方式获取 Class,类会被 JVM 加载到内存中,并且会进行类的静态初始化工作,返回 java.lang.Class 对象。
第三种获取 Class 的方式,它使用了 new 进行实例化操作,因此静态初始化和非静态初始化工作都会进行,getClass 方法属于顶级 Object 类中的方法,任何子类对象都可以调用,哪个子类调用,就返回那个子类的 java.lang.Class 对象。

3、反射的优缺点

优点:
在运行时获得类的各种内容,进行反编译,对于Java这种先编译再运行的语言,能够让我们很方便的创建灵活的代码,这些代码可以在运行时装配,无需在组件之间进行源代码的链接,更加容易实现面向对象。

缺点:
(1)反射会消耗一定的系统资源,因此,如果不需要动态地创建一个对象,那么就不需要用反射;
(2)反射调用方法时可以忽略权限检查,因此可能会破坏封装性而导致安全问题。

4、方法的反射

步骤思路:
1.获取该类的类类型
2.通过类类型获取类的方法(getMethods())
3.循环遍历所获取到的方法
4.通过这些方法的getReturnType()得到返回值类型的类类型,又通过该类类型得到返回值类型的名字
5.getName()得到方法的名称,getParameterTypes()获取这个方法里面的参数类型的类类型。

实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class ClassUtil {
public static void printClassMethodMessage(Object obj){
//想要获取类的信息,首先我们要获取类的类类型
Class c = obj.getClass();
//Object类是一切类的父类,所以我们传递的是哪个子类的对象,c就是该子类的类类型。
//然后获取类的名称
System.out.println("类的名称是:"+c.getName());
/*
*既然方法也是对象,那么是谁的对象呢?
* 在java里面,方法是Method类的对象
*一个成员方法就是一个Method的对象,那么Method就封装了对这个成员
*方法的操作
*/
//如果我们要获得所有的方法,可以用getMethods()方法,这个方法获取的是所有的Public的函数,包括父类继承而来的。如果我们要获取所有该类自己声明的方法,就可以用getDeclaredMethods()方法,这个方法是不问访问权限的。
Method[] ms = c.getMethods();//c.getDeclaredMethods()
//接下来我们拿到这些方法之后干什么?我们就可以获取这些方法的信息,比如方法的名字。
//首先我们要循环遍历这些方法
for(int i = 0; i < ms.length;i++){
//然后可以得到方法的返回值类型的类类型
Class returnType = ms[i].getReturnType();
//得到方法的返回值类型的名字
System.out.print(returnType.getName()+" ");
//得到方法的名称
System.out.print(ms[i].getName()+"(");
//获取参数类型--->得到的是参数列表的类型的类类型
Class[] paramTypes = ms[i].getParameterTypes();
for (Class class1 : paramTypes) {
System.out.print(class1.getName()+",");
}
System.out.println(")");
}
}
}

5、成员变量的反射

成员变量也是对象,是java.lang.reflect.Field类的对象,那么也就是说Field类封装了关于成员变量的操作。既然它封装了成员变量,我们又该如何获取这些成员变量呢?它有这么一个方法:

1
2
3
4
5
6
public class ClassUtil {
public static void printFieldMessage(Object obj){
Class c = obj.getClass();
Field[] fs = c.getFields();
}
}

这里的getFields()方法获取的所有的public的成员变量的信息。和方法的反射那里public的成员变量,也有一个获取所有自己声明的成员变量的信息:Field[] fs = c.getDeclaredFields();
我们得到它之后,可以进行遍历(既然封装了Field的信息,那么我们就可以得到Field类型)

1
2
3
4
5
6
7
8
for (Field field : fs) {
//得到成员变量的类型的类类型
Class fieldType = field.getType();
String typeName = fieldType.getName();
//得到成员变量的名称
String fieldName = field.getName();
System.out.println(typeName+" "+fieldName);
}

6、Class类的动态加载类

动态加载是啥意思?怎么样动态加载一个类呢?
首先我们需要明白的是,动态加载和静态加载的概念以及区别。
我们一般所理解的,在编译的时候所加载的类是静态加载类;而运行的时候加载的类是动态加载类。

1
2
3
4
5
6
7
8
9
10
11
12
Class A{
Public static void main(String[] args){
if("B".equal(args[0])){
B b=new B();
b.start();
}
if("C".equal(args[0])){
C c=new C();
C.start();
}
}
}

上面这一段代码,如果我们仅仅是在 IDEA 之中运行,是可以正常运行不发生错误的,而如果我们在 CMD 控制台运行的话,就会抛出问题。如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
A.java:7:错误:找不到符号
B b=new B();
符号: 类B
位置: 类A
A.java:7:错误:找不到符号
B b=new B();
符号: 类B
位置: 类A
A.java:12:错误:找不到符号
C c=new C();
符号: 类C
位置: 类A
A.java:12:错误:找不到符号
C c=new C();
符号: 类C
位置: 类A
4个错误

此时,因为B不存在,就应该是错误的。
但是,仔细想想,会发现B我们不一定使用,C我们也不一定使用。
现在我们来模拟一下B存在的情况。
也就是先编译B,然后再编译A。
但是此时还是有问题,C也是不存在的。
但是我们现在根本就不想用C,我们只想用B,那怎么办?
想要正常使用B,我们就不得不将C一起编译。而如果有ABCDEFGHIJKLMN个类,我们同样只想用B,那么我们是不是同样得全部编译他们呢?答案是肯定的。这样子肯定不是很好的。
以上这种说法就是所谓的静态加载类。
而解决这个问题的方法也很简单,就是改为动态加载类。即要使用的时候再创建。

我们新建一个类:

1
2
3
4
5
6
7
8
9
10
11
Class All{
Public static void start(){
try{
Class cl= Class.forName(args[0]);
//通过类类型,创建该类的对象
cl.newInstance();
}catch(Exception e){
e.printStackTrace();
}
}
}

Class.forName(“类的全称”),它不仅仅表示了类的类类型,还表示了动态加载类。当我们javac A.ava的时候,它不会报任何错误,也就是说在编译的时候是没有错误的。只有当我们具体用某个类的时候,那个类不存在,它才会报错。

如果我们要加载B,那么就需要:B bt = (B) cl.newInstance();
加载C,就是:C ct = (C) cl.newInstance();
DEFG等等都是这样。

但是如果有一百各类,我们不可能写一百句这个,太冗余了。
我们可以写一个统一处理的方法,统一一个标准,只要满足这个标准的都可以使用这个方法。
and s = (Stand) cl.newInstance();

例如B类:

1
2
3
4
5
Class B implements Stand{
Public void start(){
System.out.print("B...satrt");
}
}

即满足了前面的标准。就可以通过这个统一的标准来运行。

如果以后想用某一个类,不需要重新编译,只需要实现这个标准的接口即可。只需要动态的加载新的东西就行了。
这就是动态加载类