今天分享Java学习的第七个专题——反射

反射概述

Java 中的反射(Reflection)机制是指,Java 程序在运行期间可以获取到一个对象的全部信息。

反射机制一般用来解决Java 程序运行期间,对某个实例对象一无所知的情况下,如何调用该对象内部的方法问题。

反射机制允许 Java 程序在运行时调用Reflection API取得任何类的内部信息(比如成员变量、构造器、成员方法等),并能操作类的实例对象的属性以及方法。

在Java 程序中,JVM 加载完一个类后,在堆内存中就会产生该类的一个 Class 对象,一个类在堆内存中最多只会有一个 Class 对象,这个Class 对象包含了该类的完整结构信息,我们通过这个 Class 对象便可以得到该类的完整结构信息,将获取Class对象的过程称为:反射

精通Java系列|Java反射机制_Java

实现Java反射的基础

Java 提供反射机制,依赖于 Class 类 java.lang.reflect 类库。其主要的类如下:

  • Class:表示类或者接口
  • Field:表示类中的成员变量
  • Method:表示类中的方法
  • Constructor:表示类的构造方法
  • Array:该类提供了动态创建数组和访问数组元素的静态方法

Class类

在Java语言中,Class类是一个特殊的类,它代表运行时的类型信息。事实上,Java中的每一个类都与一个Class对象相关联,在编写并编译任何新类时,该类的信息被封装并保存到一个.class文件中当我们通过关键字new创建一个对象实例或者访问一个类的静态成员时,Java虚拟机(JVM)的类加载器子系统会将相应的Class对象加载到JVM的内存空间中。紧接着,JVM利用这些加载的Class对象信息来实例化新的对象或者返回静态成员的引用。

我们通常将Class类称之为“类类型”(class type),而每个类关联的Class对象称之为“类类型对象”(class type object)。Class类具有一些特殊的特性:

  • Class本身也是一个类,而class则是Java语言中的一个关键字。
  • Class类有一个私有的构造函数,因而它的实例只能由JVM内部创建。
  • 对于同一个类(即具有相同的完全限定名,并由相同的类加载器加载的类),在JVM中只存在一个唯一的Class对象来描述其类型信息。

.class文件包含了一个类的完整定义,包括它的所有方法、构造函数、字段(成员变量)等。当JVM启动时,它会通过读取.class文件来将对应的类加载到内存中。

在Class类的源码中,有一个私有构造器

/*
 * 私有构造器,只有JVM能够创造Class对象。
 */
private Class(ClassLoader loader) {
    classLoader = loader;
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.

提供了几个获取类实例的方法:

(1)forName():

// 获取类的实例方法:通过forName获取Class实例,其中类的名称要写类的完整路径
// 该方法只能用于获取引用类型的类类型对象
// 这种方式会使用当前的类的加载器加载,并且会将Class类实例初始化
Class<?> aClass = Class.forName("java.lang.String");
  • 1.
  • 2.
  • 3.
  • 4.

使用该方法可能会抛出 ClassNotFoundException 异常,这个异常发生在类的加载阶段,原因如下:

  • 类加载器在类路径中没有找到该类(检查:查看所在加载的类以及其所依赖的包是否在类路径下)
  • 该类已经被某个类加载器加载到 JVM 内存中,另外一个类加载器又尝试从同一个包中加载

(2)Object.getClass() 方法

如果我们有一个类的对象,那么我们可以通过 Object.getClass 方法获得该类的 Class 对象。

Class<? extends String> aClass1 = "string".getClass();
  • 1.

(3)class属性

// 类类型的名称.class: 使用 class 语法获取该类类型的对象
Class<Integer> integerClass = Integer.class;
  • 1.
  • 2.

总结一下,Class也是一个类,其类名就叫Class,因此它也继承 Object 类;

Class类对象不是由我们程序员创建(new)出来的,而是在类加载时由 JVM 自动创建的。

在堆内存中最多只会存在某个类的唯一的Class对象,因为类只会加载一次,每个类的实例对象都会知道自己对应的Class对象,通过Class类对象可以完整地得到其对应的类的信息,通过一系列反射 API。

类class是由 JVM 在执行过程中动态加载的。JVM在第一次读取到一种类class时,会将其加载进内存。

每加载一种class,JVM就为其创建一个Class类的对象,并将两者关联起来。注意:这里的Class类是一个名字叫Class的类class。

以String类为例,当 JVM 加载String类时,它首先读取String.class文件到内存,然后,在堆中为String类创建一个Class类对象并将两者关联起来:

Class cls = new Class(String);
  • 1.

注意:这个Class类对象是 JVM 内部创建的,如果我们查看 JDK 源码,可以发现Class类的构造方法是private,即只有 JVM 能创建Class类对象,我们程序员自己的 Java 程序是无法创建Class类对象的。

由于JVM为每个加载的类class创建了对应的Class类对象,并在实例中保存了该类class的所有信息,包括类名、包名、父类、实现的接口、所有方法、字段等,因此,如果获取了某个Class类对象,我们就可以通过这个Class类对象获取到其对应的类class的所有信息。

这种通过Class实例获取类class信息的方法称为反射(Reflection)。

// 获取Class类的一个实例
// 方式1
Class cls = String.class;
//方式2
String s = "Hello";
Class cls2 = s.getClass();
//方式3
Class cls3 = Class.forName("java.lang.String");//里面填写的是类的完整类名
//方式4
Class intClass = int.class;//基本数据类型
//方式5
Class type1 = Integer.TYPE;//包装类
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.

获取类的私有属性:

//对于私有属性,可以通过getDeclaredField方式获取
Field fname =  clazz.getDeclaredField("name");
//取到对应的值
Object value = fname.get(p);
System.out.println(value);
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.

这里可能有小伙伴发现了一个问题,如果使用反射可以获取private字段的值,那么类的封装还有什么意义?

答案是一般情况下,我们总是通过p.name来访问Person的name字段,编译器会根据public、protected和private这些访问权限修饰符决定是否允许访问字段,这样就达到了数据封装的目的。

而反射是一种非常规的用法,使用反射,首先代码非常繁琐;其次,它更多地是给工具或者底层框架来使用,目的是在不知道目标对象任何信息的情况下,获取特定字段的值。

封装性存在的意义是隐藏私有内容,为了给使用者提供更方便、更好用的公共接口。但是调用者应该有权限获取对象中私有的内容,因此需要用到反射机制。

Field

Field 提供了有关类或接口的单个属性的信息,以及对它的动态访问的能力。

Class<?> student = Class.forName("com.wangbucuo.demo03.Student");
Field name = student.getField("name");
  • 1.
  • 2.

此外,还有MethodConstructor两个能力:

Method 提供了有关类或接口的单个方法的信息,以及对它的动态访问的能力。

Constructor 提供了有关类的构造方法的信息,以及对它的动态访问的能力。

由于与Field用法相同,这里就不在赘述了。

动态代理

有没有可能不编写实现类,直接在运行期创建某个interface的实例呢?

这是可能的,因为 Java 标准库提供了一种动态代理(Dynamic Proxy)的机制:可以在运行期动态创建某个interface的实例。

什么叫运行期动态创建?

动态代码,我们仍然先定义了接口,但是我们并不去编写实现类,而是直接通过 JDK 提供的一个Proxy.newProxyInstance()方法创建了一个接口对象。这种没有实现类但是在运行期动态创建了一个接口对象的方式,我们称为动态代理。JDK 提供的动态创建接口对象的方式,就叫动态代理