Java的反射机制
反射(Reflection)的概念
反射的出现背景
Java程序中,所有的对象都有两种类型:编译时类型和运行时类型,而很多时候对象的编译时类型和运行时类型不一致。例如:
Object obj = new String("hello");
某些变量或形参的类型在编译时是Object,但是程序需要调用该对象运行时类型的方法,该方法不是Object中的方法,那么怎么办呢?
解决这个问题,有两种方案:
方案1
在编译和运行时都完全知道类型的具体信息,在这种情况下,我们可以直接使用instanceof
运算符进行判断,利用强类型转换符将其转换成运行时类型的变量即可。
方案2
编译时根本无法预知该对象和类的真实信息,程序只能依靠运行时信息来发现该对象和类的真实信息,这就必须使用反射。
反射的概述
-
Reflection(反射)是被视为动态语言的关键,反射机制允许程序在运行期间借助于Reflection API取得任何类的内部信息,并能直接操作任意对象的内部属性及方法。
-
加载完类之后,在堆内存的方法区中就产生了一个Class类型的对象(一个类只有一个Class对象),这个对象就包含了完整的类的结构信息。我们可以通过这个对象看到类的结构。
-
这个对象就像一面镜子,透过这个镜子看到类的结构,所以,我们形象的称之为:反射。
实例
创建person类
package apply;
public class Person {
private String name;
public int age;
public Person() {
System.out.println("person()");
}
public Person(String name) {
this.name = name;
}
public Person(int age) {
this.age = age;
}
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public void show() {
System.out.println("person");
}
private String showNation(String nation) {
return "nation"+nation;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
package apply;
import org.junit.Test;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class RelectionTest {
@Test
public void test1(){
// 创建person的对象
Person p1 = new Person();
// 调用属性和方法
p1.age = 10;
System.out.println(p1.age);
// 调用方法
p1.show();
}
// 使用反射完成上述操作
@Test
public void test2() throws InstantiationException, IllegalAccessException, NoSuchFieldException, NoSuchMethodException, InvocationTargetException {
//创建实例
Class<Person> clazz = Person.class;
Person p1 = clazz.newInstance();
System.out.println(p1);
Field ageField = clazz.getField("age");
ageField.set(p1, 10);
System.out.println(ageField.get(p1));
Method showMethod = clazz.getMethod("show");
showMethod.invoke(p1);
}
//通过反射调用person类中的私有属性、方法
@Test
public void Test02() throws Exception {
// 1.调用私有构造器,创建对象
Class clazz = Person.class;
Constructor cons = clazz.getDeclaredConstructor(String.class,int.class);
cons.setAccessible(true);
Person p1 = (Person) cons.newInstance("tom",12);
System.out.println(p1);
// 调用私有属性
Field nameFiled = clazz.getDeclaredField("name");
nameFiled.setAccessible(true);
nameFiled.set(p1, "jerry");
System.out.println(nameFiled.get(p1));
// 调用私有方法
Method showNation = clazz.getDeclaredMethod("showNation", String.class);
showNation.setAccessible(true);
String p2 = (String) showNation.invoke(p1, "CHN");
System.out.println(p2);
}
}
2.以前创建对象并调用方法的方式,与现在通过反射创建对象并调用方法的方式对比的话,哪种用的多?场景是什么?
从我们作为程序员开发者的角度来讲,我们开发中主要是完成业务代码,对于相关的对象、方法的调用都是确定的。所以,我们使用非反射方式多一些。
因为反射体现了动态性,(可以在运行时动态的获取对象所属的类,动态的调用相关的方法)所以我们在设计框架的时候,会大量使用反射
框架 = 注解 + 反射 + 设计模式
通过反射,可以调用类中私有的结构,是否与面向对象的封装性有冲突?是不是Java语言设计存在Bug? 不存在bug!
这个描述指出,尽管反射机制允许访问和操作类的私有成员(字段、方法等),这并不意味着Java语言设计存在缺陷。实际上,这是Java语言设计的一部分,允许在特定情况下绕过封装性原则。
面向对象编程的封装性原则主张隐藏对象的内部状态和实现细节,只通过公共接口暴露功能。然而,反射提供了一种机制,允许在运行时检查或修改类的私有成员,这在某些高级场景中非常有用,比如:
测试框架需要访问私有字段或方法来设置测试数据或验证内部状态。
框架或库可能需要访问或修改对象的私有成员来实现某些功能,例如对象映射、依赖注入等。
在某些情况下,开发者可能需要在运行时动态地访问或修改类的私有成员,尽管这通常不推荐作为常规做法。
反射的机制
Java反射机制的原理基于Java虚拟机(JVM)在运行时对类的内部信息的访问能力。以下是反射机制工作原理的几个关键点:
类的元数据信息
在Java中,每个类在加载到JVM时,都会生成一个对应的Class
对象,这个对象包含了类的元数据信息,如类名、方法、字段、构造函数等。这些信息被存储在方法区(Method Area)中。
Class对象
每个类都有一个唯一的Class
对象,可以通过类名.class
、对象.getClass()
或Class.forName("类名")
的方式获取。Class
对象是反射机制的核心,它允许程序在运行时动态地访问和操作类的信息。
访问和操作
通过Class
对象,可以使用反射API来访问和操作类的内部结构:
-
获取类信息 :使用
getFields()
,getMethods()
,getConstructors()
等方法获取类的字段、方法和构造函数。 -
创建对象 :使用
getConstructor().newInstance()
或getDeclaredConstructor().newInstance()
创建类的实例。 -
访问和修改字段 :使用
getField()
,getDeclaredField()
获取字段,并使用setAccessible(true)
访问私有字段,然后使用set()
和get()
方法来修改和获取字段值。 -
调用方法 :使用
getMethod()
或getDeclaredMethod()
获取方法,并使用invoke()
方法调用它们。 -
处理注解 :使用
getAnnotation()
等方法获取类、方法或字段上的注解信息。
动态性
反射的动态性体现在它允许程序在运行时检查和修改类的结构,而不需要在编译时确定。这使得反射非常适合于需要高度灵活性的场景,如框架开发、依赖注入、对象映射等。
性能考虑
虽然反射提供了强大的功能,但它也有一些缺点。反射操作通常比直接代码访问要慢,因为它需要在运行时解析类型信息,并且可能需要处理安全权限检查。因此,在性能敏感的应用中,应谨慎使用反射。
安全和封装
反射可以绕过Java的访问控制,访问和修改私有成员。这虽然提供了灵活性,但也可能破坏封装性原则,增加代码的复杂性和维护难度。因此,使用反射时应确保有充分的理由,并且要小心处理安全和封装问题。
-
Java反射机制提供的功能:
-
在运行时判断任意一个对象所属的类
-
在运行时构造任意一个类的对象
-
在运行时判断任意一个类所具有的成员变量和方法
-
在运行时获取泛型信息
-
在运行时调用任意一个对象的成员变量和方法
-
在运行时处理注解
-
生成动态代理
-
Java反射机制的优缺点如下:
优点:
-
1.动态性:
反射允许在运行时动态地创建对象、访问和修改属性、调用方法,这为程序提供了极大的灵活性。
- 可以在不知道具体类的情况下操作对象,这对于编写通用代码(如框架和库)非常有用。
-
2.解耦:
- 反射可以减少代码之间的耦合,因为它允许程序在运行时决定调用哪个类或方法,而不是在编译时就固定下来。
-
3.通用性:
- 反射使得某些通用功能(如对象序列化、ORM框架、依赖注入等)的实现成为可能,这些功能需要在运行时动态地处理类和对象。
-
4.扩展性:
- 可以在不修改现有代码的情况下,通过反射调用新的方法或访问新的属性,这对于插件系统和模块化设计特别有用。
缺点:
-
1.性能开销:
-
反射操作通常比直接代码访问要慢,因为它需要在运行时解析类型信息,并且可能需要处理安全权限检查。
-
反射调用方法或访问字段时,需要通过方法句柄进行间接调用,这比直接调用要慢。
-
-
2.安全问题:
- 反射可以绕过访问控制,访问和修改私有成员,这可能导致安全漏洞,尤其是当反射被用于不恰当的场景时。
-
3.代码复杂性:
-
使用反射的代码通常比直接代码更难以理解和维护,因为反射操作的动态性使得代码的意图不够明显。
-
调试时也更困难,因为反射调用可能隐藏在多层间接调用之后。
-
-
4.编译时检查缺失:
- 由于反射是在运行时解析类型信息,编译器无法对反射代码进行类型检查,这可能导致运行时错误。
-
5.封装性破坏:
- 反射破坏了面向对象编程中的封装性原则,因为它允许访问和修改类的私有成员
class类
在Java中,Class
类是所有类的根类,它用于表示Java程序运行时的类型信息。每个类在Java虚拟机(JVM)中都有一个对应的Class
对象,这个对象包含了类的元数据信息,如类名、字段、方法、构造函数等。Class
类是Java反射API的核心,它允许程序在运行时动态地访问和操作类的信息。
如何获取Class
对象
-
1.通过类名直接获取:
Class<?> clazz = MyClass.class;
-
2.通过实例获取:
MyClass myObject = new MyClass(); Class<?> clazz = myObject.getClass();
-
3.通过类的全名获取
try { Class<?> clazz = Class.forName("完整的类名"); } catch (ClassNotFoundException e) { e.printStackTrace(); }
Class
对象的用途
-
创建对象实例:
Class<?> clazz = MyClass.class; MyClass myObject = (MyClass) clazz.newInstance();
-
获取类的成员信息:
-
获取字段:
getFields()
,getField(String name)
,getDeclaredFields()
,getDeclaredField(String name)
-
获取方法:
getMethods()
,getMethod(String name, Class<?>... parameterTypes)
,getDeclaredMethods()
,getDeclaredMethod(String name, Class<?>... parameterTypes)
-
获取构造函数:
getConstructors()
,getConstructor(Class<?>... parameterTypes)
,getDeclaredConstructors()
,getDeclaredConstructor(Class<?>... parameterTypes)
-
-
访问和修改字段:
Field field = clazz.getField("fieldName"); Object value = field.get(myObject); field.set(myObject, newValue);
-
调用方法:
Method method = clazz.getMethod("methodName", 参数类型1.class, 参数类型2.class); Object result = method.invoke(myObject, 参数1, 参数2);
-
处理注解:
Annotation[] annotations = clazz.getAnnotations();
注意事项
-
使用反射时,需要处理
NoSuchFieldException
,NoSuchMethodException
,IllegalAccessException
,InvocationTargetException
等异常。 -
反射操作通常比直接代码访问要慢,因此在性能敏感的应用中应谨慎使用。
-
反射可以绕过访问控制,访问和修改私有成员,这可能导致安全问题。
- 1.Class类的理解(掌握) (如下以Java类的加载为例说明) 针对编写好的.java源文件进行编译(使用javac.exe),会生成一个或多个.class字节码文件。接着,我们使用java.exe命令对指定的.class文件进行解释运行。这个解释运行的过程中,我们需要将.class字节码文件加载(使用类的加载器)到内存中(存放在方法区)。加载到内存中的.class文件对应的结构即为Class的一个实例。
比如,加载到内存中的Person类或String类或User类,都作为Class的一个一个的实例
Class clazz1 = Person.class;
Class clazz2 = String.class;
Class clazz3 = User.class;
Class clazz4 = Comparable.class;
类的加载过程
1.类的装载(loading)
将类的class文件读入内存,并为之创建一个Java.long.class对象,此过程由类加载器完成
2.连接(linking)
验证(verify):确保加载的类信息符合JVM规范
准备(prepare):正式为类变量(static)分配内存并设置类变量默认初始值的阶段,这些内存都将在方法区中进行分配.
解析(resolve):虚拟机常量池的符号引用(常量名) 替换为直接引用(地址)的过程
3.初始化(Initialization)
执行类构造器<clinit>()方法的过程
类构造器<clinit>()方法是由编译器自动收集类中所有类变量的赋值动作和静态代码块中的语句合并产生
类构造器:负责类的加载,并对应于一个class的对象
-
bootstrapclassloader:启动类加载器,引导类加载器
-
使用c/c++语言编写,不能通过Java代码获取实例
-
负则加载Java核心库
-
-
继承于 classloader的类加载器
-
extensionclassloader:扩展类加载器
-
负责加载java.ext.dirs系统属性所指定的目录中加载类库
-
或者从jdk的安装目录的jre/lib/ext子目录下加载
-
-
systemclassloader/applicationclassloader:系统类加载器.应用程序类加载器
- 我们自定义的类,默认使用的类的加载器
-
用户自定义类的加载器
- 实现应用隔离,数据加密