反射概述
Java 反射就是在运行的状态下,对于任意一个类,都能知道这个类的任意的属性和方法,并且能调用这些方法或者改变这些类的属性,因此 Java 被称为准动态语言。
动态语言和静态语言
上面说了 Java 是准动态的语言,下面我们来了解动态语言和静态语言的区别:
- 动态语言是一类可以在运行的时候改变其结构的语言:例如新的函数、对象、甚至是代码可以被引进,已有的函数可以被删除或者是其他结构上的变化。通俗点说就是在运行时代码可以根据某些条件来改变自身的结构。
- 静态语言在运行时结构是不可变的,这些内容在编译的时候就已经确认了,Java 是准动态语言,是有一定的动态性的,我们可以利用反射机制中的特性实现在运行状态是时候对进行一些修改。
Java 反射提供的功能
- 在运行时判断一个对象所属的类
- 在运行时构造任意一个类的对象
- 在运行时判断一个类所具有的成员变量和方法
- 在运行时获取泛型信息
- 在运行时任意调用对象的成员变量和方法,不管是共有或是私有的
- 在运行时处理注解
- 生成动态代理
这篇文章主要介绍和理解前六条,关于动态代理放在 Spring 的 AOP 详解去仔细的介绍。
Java 反射介绍
了解了反射的意义和反射提供的功能后,我们来具体的介绍反射:
- 反射是 Java 可以被视为动态语言的关键,反射机制允许程序在执行的时候借助反射的 API 获取类的内部信息,并且能直接操控对象内部的属性以及方法。
- 加载完类后,在堆内存的方法区就产生了一个** Class 类型的对象**,这个对象包含了完整的类的结构信息,我们可以通过这个对象看到类的结构,就像一个镜子一样,所以这个机制被称为反射,需要注意的是一个类仅有一个 Class 对象。
Java 反射的优点和缺点
优点:
可以实现动态的创建对象和编译,体现出很大的灵活性
缺点:
对性能有影响。使用反射基本上是一种解释操作,我们可以告诉 JVM 我们希望做什么,并且它满足我们的要求。这类操作总是慢于直接执行的操作
反射的 API
Java 提供了一套反射的 API,允许在运行时检查和操作类、方法、字段等信息,主要涉及到的类包括 Class、Method、Field、Constructor 等。
这里先把常用的 API 罗列下来,方便后续的查阅和使用,这些类和方法后面都有具体的解析
在理解这些类的方法的时候,可以先从这些类的归属看起:
比如字段和方法是依托于对象的,所以我们使用字段或者调用方法的时候就需要传输是通过哪个对象来使用这个字段和调用这个方法,而构造器是属于整个类的,这些就不需要对象的承载。
Class
API | 作用 |
---|---|
Class.forName(String className) | 通过类的完整路径名获取 Class 对象。 |
object.getClass() | 获取对象的 Class 对象 |
ClassLoader.loadClass(String className) | 通过类加载器加载类 |
Method
API | 作用 |
---|---|
Class.getDeclaredMethods() | 获取所有声明的方法。 |
Class.getDeclaredMethod(String name, Class<?>… parameterTypes) | 获取指定声明的方法。 |
Method.invoke(Object obj, Object… args) | 调用方法。 |
Field
API | 作用 |
---|---|
Class.getDeclaredFields() | 获取所有声明的字段 |
Class.getDeclaredField(String name) | 获取指定声明的字段 |
Field.get(Object obj) | 获取字段的值 |
Field.set(Object obj, Object value) | 设置字段的值 |
Constructor
API | 作用 |
---|---|
Class.getDeclaredConstructors() | 获取所有声明的构造方法 |
Class.getDeclaredConstructor(Class<?>… parameterTypes) | 获取指定声明的构造方法 |
Constructor.newInstance(Object… initargs) | 通过构造方法创建新对象 |
获取反射对象
获取反射对象的方式
观察上面的 API 我们可以发现,不管是 Method、Field 或者是 Constructor 在获取的时候都需要得到 Class 对象,这个 Class 对象是反射的源头。
在 Object,也就是所有类的父类中我们可以很容易的发现一个方法
public final native Class<?> getClass();
这个方法将会被**所有的子类继承,**通过这些子类的对象我们可以调用这个方法去获取 Class 对象。
public class getClass {
public static void main(String[] args) {
Person person = new Person();
// 调用实例的 getClass 方法获取这个类的 Class 对象
Class<? extends Person> aClass = person.getClass();
}
}
class Person{}
除了这种方法我们还可以通过类的全路径来获取到类的 Class 对象
public class getClass {
public static void main(String[] args) throws ClassNotFoundException {
Person person = new Person();
Class<? extends Person> aClass = person.getClass();
// 这个实例类是我在根路径上写的,这就已经是全路径了
Class<?> person1 = Class.forName("Person");
System.out.println(person1.hashCode());
}
}
class Person{}
如果已经知道想要获取的类是哪一个,可以直接通过 类名.class 来获取对应的 Class 对象,这种方式最为安全可靠,程序性能也是最高的
public class getClass {
public static void main(String[] args) throws ClassNotFoundException {
Class<Person> personClass = Person.class;
}
}
class Person{}
还能获取到父类的 Class 对象
// 通过 Class 实例获取父类的 Class 实例
Class<?> superclass = personClass1.getSuperclass();
除了这些方法还能通过类加载器来获取类的 Class 对象,这个放在后面叙述,通过这些方法获取到 Class 类的时候,我们就可以在运行时对其进行操作了。
Class 类
通过这个类能得到的信息就是我们上面反射常用类中提到的属性,有类的属性、方法和构造器,某个类实现了哪些接口,对于每个类而言,都保留了一个不变的 Class 类型的对象,该对象包含了特定的某个结构的有关信息,比如 class、interface、enum、annotation、primitive、type / void、[] 等。
Class 详解
- Class 本身也是一个类,这个类只能由系统来进行实例化,内部没有提供给外部使用的构造器。
- 一个加载的类在 JVM 中只会有一个 Class 实例
public class getClass {
public static void main(String[] args) throws ClassNotFoundException {
Person person = new Person();
Class<? extends Person> personClass1 = person.getClass();
Class<? extends Person> personClass2 = person.getClass();
Class<? extends Person> personClass3 = person.getClass();
Class<? extends Person> personClass4 = person.getClass();
Class<? extends Person> personClass5 = person.getClass();
Class<? extends Person> personClass6 = person.getClass();
// 输出这些类的 hashcode
System.out.println(personClass1.hashCode());
System.out.println(personClass2.hashCode());
System.out.println(personClass3.hashCode());
System.out.println(personClass4.hashCode());
System.out.println(personClass5.hashCode());
System.out.println(personClass6.hashCode());
}
}
class Person{}
# 相同的哈希值,是同一个对象
460141958
460141958
460141958
460141958
460141958
460141958
- 一个 Class 对象对应的是一个加载到 JVM 中的一个 .class 文件
- 每个类的实例都会记得自己是哪个 Class 实例所生成的
- Class 类是反射的根源,任何像动态加载、运行的类,只能先获取响应的 Class 对象
Method 类
通过 Method 调用共有方法
我们可以通过 Class 类中的 getDeclaredMethod 来获取类中的所有方法,也可以通过 getMethod
来获取全部的方法,也可以通过 getMethods
来获取所有方法的数组。
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class getClass {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
Person person = new Person();
Class<Person> personClass = Person.class;
person.setName("张三");
Method getName = personClass.getDeclaredMethod("getName");
System.out.println(getName.invoke(person));
}
}
class Person{
public String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
通过 Method 调用私有方法
前面我们说过利用反射机制是可以调用类中的所有方法的,不论是共有还是私有,但如果直接通过上面的方法调用是会抛出:java.lang.IllegalAccessException
异常,我们需要手动开启访问权限才能调用私有方法。
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class getClass {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
Person person = new Person();
Class<Person> personClass = Person.class;
person.setName("张三");
Method getName = personClass.getDeclaredMethod("getName");
// 开放访问权限
getName.setAccessible(true);
System.out.println(getName.invoke(person));
}
}
class Person{
public String name;
private String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
Field 类
通过 Field 获取和修改对象的属性
getField 和 getDeclaredField 是用于获取类的字段(Field)的两个不同方法,它们之间有一些区别,这和上面 Method 是相同的,Method 同样也有这两种方法。
可见性:
- getField 用于获取类中的公有字段(public fields)。
- getDeclaredField 用于获取类中声明的所有字段,不论其可见性(包括公有、受保护、默认、私有等)。
继承关系:
- getField 会返回指定名称的公有字段,包括从父类继承而来的公有字段。
- getDeclaredField 只会返回当前类中声明的字段,不包括继承的字段。
import java.lang.reflect.Field;
public class getClass {
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
Person person = new Person();
Class<Person> personClass = Person.class;
person.setName("张三");
// 获取字段
Field name = personClass.getDeclaredField("name");
// 调用私有字段需要设置可见性
name.setAccessible(true);
Object o = name.get(person);
System.out.println(o);
}
}
class Person{
private String name;
private String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
Constructor 类
Constructor 相信大家都不陌生,就是我们经常使用的构造器,通过 Class 获取到的构造器同样可以构造对象,这个类可以帮助我们更方便的选择需要的构造器。
这是获取构造器的方法,我们可以通过指定**参数类型(Class)**的方式来指定构造器。
@CallerSensitive
public Constructor<T> getDeclaredConstructor(Class<?>... parameterTypes)
throws NoSuchMethodException, SecurityException {
checkMemberAccess(Member.DECLARED, Reflection.getCallerClass(), true);
return getConstructor0(parameterTypes, Member.DECLARED);
}
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
public class getClass {
public static void main(String[] args) throws IllegalAccessException, NoSuchMethodException, InvocationTargetException, InstantiationException {
Class<Person> personClass = Person.class;
Constructor<Person> constructor = personClass.getDeclaredConstructor(String.class);
Person person = constructor.newInstance("张三");
System.out.println(person.getName());
}
}
class Person{
private String name;
public String getName() {
return name;
}
public Person() {
}
public Person(String name) {
this.name = name;
}
public void setName(String name) {
this.name = name;
}
}
深入理解反射机制
为了更好的理解反射的机制,我们需要了解一些底层的知识,比如 Java 内存或者类的加载等等。
Java 内存
Java 的内存主要分为三块:堆、栈和方法区
堆
- 存放通过 new 创建的对象和数组,所有的对象都在堆中分配内存。
- 可以被所有线程共享,不会存放别的对象引用
栈
- 存放基本的变量类型,会存储这个类型的具体数值
- 存放引用对象的变量,存放这个引用在堆里面的具体地址
- 栈是线程私有的,每个线程都有自己的栈
方法区
- 方法区也是线程共享的,用于存储类的信息、静态变量、常量池、方法代码等。
- 在方法区中保存了类的结构信息,包括类的字段、方法、接口等。
- 常量池存储了编译期生成的常量,如字符串常量、基本数据类型的常量等。
类加载与 ClassLoader 的理解
加载:指的是将一个 class 字节码文件内容加载到内存中,并且将这些静态数据转换成方法区的运行时的数据结构,然后生成一个代表这个类的 java.lang.Class 对象,这个对象是存储在方法区的。
链接:将 Java 类的二进制代码合并到 JVM 运行状态之中的过程
- 验证:确保加载的类信息符合 JVM 的规范且没有安全方面的问题
- 准备:正式为类变量(static)分配内存并设置
- 解析:虚拟机常量池内符号引用替换为直接引用的过程
**初始化:**执行初始化类构造器 () 方法的过程。类构造器() 方法是由编译器自动收集类中的所有变量的赋值动作和静态代码块中的语句合并而成的。(类构造器构造类的信息,不是构造类的对象)
- 当初始化一个类的时候,如果发现父类还没有被初始化,则先调用父类的初始化。
- 虚拟机 会保证一个类的 () 方法在多线程环境中被正确的加锁和同步。