反射

利用反射机制,可以通过外部文件配置,在不修改源码的情况下控制程序,符合设计模型中OCP原则

  1. 反射机制允许程序在执行期间借助于ReflectionAPI取得任何类的内部信息(成员变量、构造器、成员方法等),并能操作对象的属性及方法。
  2. 加载类之后,在堆中产生了一个Class类型的对象,这个对象包括了类的完整结构信息。这个对象就像一面镜子,通过这个镜子看到类的结构,所以称之为:反射
  3. 反射可以做到的事:
    • 在运行时判断任意一个对象所属的类
    • 在运行时构造任意一个类的对象
    • 在运行时得到任意一个类所具有的成员变量和方法
    • 在运行时调用任意一个对象的成员变量和方法
    • 生成动态代理
  4. 使用反射后,程序的执行速度会受到影响

反射相关的类

常用的反射相关的类都在java.lang包中

  1. Class:代表一个类,表示某个类加载后在堆中的对象
  2. reflect.Method:类的方法
  3. reflect.Field:类的成员变量,不能得到private属性
  4. reflect.Constructor:构造器

Class类

  1. Class类也是继承Object的类
  2. Class类对象不是new出来的,而是系统创建的
  3. 对于某个类的Class对象,在内存中只有一份,因为类只加载一次(比如在使用反射加载类对象之前已经实例化过一次该类,就不会再调用loadClass方法)
  4. Class对象存放在堆中
  5. 类的字节码二进制数据存放在方法区,称为类的元数据

常用方法

方法名 功能
static Class forName() 返回指定类名的Class对象
Object newInstance() 返回Class对象的一个实例
String gatName() 返回Class对象所表示的实体(类,接口,基本类型等)的名称
Class getSuperClass() 返回其父类
Constructor[] getConstructors() 返回本类的构造器
ClassLoader getClassLoader() 返回类的加载器
Field[] getFields() 返回所有public修饰的属性,包括本类及父类
Field[] getDeclaredFields() 返回本类所有属性
Method[] getMethods() 返回所有public修饰的方法,包括本类及父类
Method[] getDeclaredMethods() 返回本类所有方法

获取Class对象

  1. 若已知一个类的完整路径“包名.类名”,可以通过Class类的forName()获取。应用场景:配置文件,读取类全路径,加载类
  2. 若已知具体的类,可以通过类.class获取,该方式也可以获取基本数据类型的Class对象。应用场景:用于参数传递,如通过反射的对应构造器对象。
  3. 如果已经有该类的实例,可以通过对象.getClass()获取Class对象
  4. 通过类加载器获取:

ClassLoader classLoader = test.getClass().getClassLoader();

Class cls = classLoader.loadClass(classAllPath); // classAllPath为类的完整路径

  1. 基本数据类型的包装类(Integer,Boolean等)可以通过.TYPE获取Class对象

类加载

类加载分为静态加载动态加载

静态加载:编译时加载相关的类,如果没有则报错,哪怕不一定会用到这个类,也会加载

动态加载:运行时加载需要的类,如果运行时不用该类就不会报错,反射使用的就是动态加载方案

public class Test{
public static void main(String[] args){
Scanner scanf = new Scanner(System.in);
int num = scanf.nextInt();
switch(num){
case 1:
Dog dog = new Dog(); // 静态加载,哪怕可能不会使用到这个类也会加载,会直接报错
dog.method();
break;
case 2:
Class cls = Class.forName("Dog"); // 动态加载,当使用到的时候才会报错
Object obj = cls.newInstance();
Method m = cls.getMethod("method");
m.invoke();
break;
default:
System.exit(0);
}
}
}
类加载的过程

大体流程如下图所示。

​ 首先对源码进行编译,生成字节码文件,在运行时进入类加载,由类加载器将类的class文件读入内存,并创建一个Class对象。

然后进入连接阶段,把二进制数据合并到JRE中,验证:对文件安全性进行验证,准备:对静态变量进行默认初始化并分配空间,解析:把符号引用转成直接引用(地址引用)。最后进行初始化,由JVM负责,执行<clinit>()方法。

<clinit>()方法是由编译器按照语句在源文件中出现的顺序,依次自动收集类中所有静态变量的赋值动作和静态代码块中的语句,并进行合并。虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确的加锁,同步。如果多个线程同时去初始化一个类,同一时间内只允许一个线程执行此方法。

Field类常用方法

方法名 功能
getModifiers() 以int形式返回修饰符
getType() 以Class形式获取返回类型
getName() 返回属性名
getParameterTypes() 以Class[] 返回参数类型数组

注:在getModifiers()方法中,返回的int形式为:默认是0,public是1,private是2,protected是4,static是8,final是16。如果有多个修饰符就代数相加。

反射暴破

使用反射机制访问private属性、方法、构造器,称为暴破(暴力破解),可以破坏封装特性。

示例

public class Test{
public static void main(String[] args) throws Exception {
// 获取person类的Class对象
Class<?> person = Class.forName("Person");

Object o1 = person.getDeclaredConstructor().newInstance(); // 创建无参实例

// 创建private有参实例
Constructor<?> privateConstructor = person.getDeclaredConstructor(String.class, int.class);
privateConstructor.setAccessible(true); // 暴破
Object o = privateConstructor.newInstance("Mary", 18);

Field name = person.getDeclaredField("name"); // 获取name属性
name.set(o, "Jack"); // 修改name
Method toString = person.getMethod("toString"); // 获取toString方法
// 如果方法有返回值,统一返回Object,但是运行类型和方法定义的返回类型一致
System.out.println(toString.invoke(o));

// 获取private属性age
Field age = person.getDeclaredField("age");
age.setAccessible(true); // 暴破
age.set(o, 23); // 修改age
System.out.println(age.get(o)); // 获取age的值

// 获取private方法say,如果方法中要求传参,则需要在后面输入参数对应的Class类
Method say = person.getDeclaredMethod("say", String.class, int.class);
say.setAccessible(true); // 暴破
say.invoke(o1, "success", 1); // 激活方法并传入参数
}
}

// 测试类
class Person{
public String name;
private static int age;

public Person() {}

private Person(String name, int age) {
this.name = name;
this.age = age;
}

private void say(String word, int num){
System.out.println("调用say方法:" + word);
}

@Override
public String toString() {
return "Person [name=" + name + ", " + "age=" + age + "]";
}
}