首页 > 编程语言 >Java 反射

Java 反射

时间:2025-01-23 16:11:53浏览次数:1  
标签:反射 Java String 对象 class Class 获取 public

目录

  • 反射:框架设计的灵魂
  • 框架:半成品软件。可以在框架的基础上进行软件开发,简化编码
  • 反射:将类的各个组成部分封装为其他对象,这就是反射机制
  • 优点
    1. 可以动态地创建和使用对象,在程序运行过程中,操作这些对象。
    2. 可以解耦,提高程序的可扩展性。
  • 缺点
    1. 使用反射基本是解释执行,对程序执行速度有影响。
    2. 反射相当于一系列解释操作,通知 JVM 要做的事情,性能比直接的 java 代码要慢很多。
  • 获取 Class 对象的方式:
  1. Class.forName("全类名"):将字节码文件加载进内存,返回 Class 对象
    • 多用于配置文件,将类名定义在配置文件中。读取文件,加载类
  2. 类名.class:通过类名的属性 class 获取
    • 多用于参数的传递
  3. 对象.getClass():getClass() 方法在 Object 类中定义着。
    • 多用于对象的获取字节码的方式
  • 结论:
    同一个字节码文件(*.class)在一次程序运行过程中,只会被加载一次,不论通过哪一种方式获取的 Class 对象都是同一个。

概述

反射(Reflection),Java 中的反射机制是指,Java 程序在运行期间可以获取到一个对象的全部信息。
反射机制一般用来解决 Java 程序运行期间,对某个实例对象一无所知的情况下,如何调用该对象内部的方法问题。

反射机制

反射机制是 Java 实现动态语言的关键,也就是通过反射实现类的动态加载。
Java 反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为 Java 语言的反射机制。

  1. 静态加载: 编译时就加载相关的类,确定类型,绑定对象,如果程序中不存在该类则编译报错,依赖性太强。
  2. 动态加载: 运行时加载相关的类,确定类型,绑定对象,即使程序中不存在该类,但如果运行时未使用到该类,也不会编译错误,依赖性较弱。

反射机制原理

反射机制允许 Java 程序在运行时调用 Reflection API 取得任何类的内部信息(比如成员变量、构造器、成员方法等),并能操作类的实例对象的属性以及方法。
在 Java 程序中,JVM  加载完一个类后,在堆内存中就会产生该类的一个  Class  对象,一个类在堆内存中最多只会有一个  Class  对象,这个 Class  对象包含了该类的完整结构信息,我们通过这个 Class 对象便可以得到该类的完整结构信息
这个  Class  对象就像是一面镜子,我们透过这面镜子可以清楚地看到类的结构信息。因此,我们形象的将获取 Class 对象的过程称为:反射
Java 反射机制原理示意图:
[0ca225bba4c27119aa4e4c092cd120ba.png]

类加载概述

[1971784-20200318213539251-923984642.png]
例子:

public class ClassLoad {
    public static void main(String[] args) {
		Scanner sc = new Scanner(System.in);
        int key = sc.nextInt();
        switch(key) {
            case 0:
                Cat cat = new Cat();
                break;
            case 1:
                // 通过反射创建一个 Dog 类对象,不提供代码,只是文字说明
                break;
        }
    }
}

上面代码中,根据 key 的值选择创建 Cat/Dog 对象,但是在代码编译时,编译器会先检查程序中是否存在 Cat 类,如果没有,则会编译报错;编译器不会检查是否存在 Dog 类,因为 Dog 类是使用反射的方式创建的,所以即使程序中不存在 Dog 类,也不会编译报错,而是等到程序运行时,我们真正选择了 key = 1 后,才会去检查 Dog 类是否存在。

类加载的时机:

  1. 静态加载
    • 当新创建一个对象时(new),该类会被加载;
    • 当调用类中的静态成员时,该类会被加载;
    • 当子类被加载时,其超类也会被加载;
  2. 动态加载
    • 通过反射的方式,在程序运行时使用到哪个类,该类才会被加载;
      类加载的过程图:
      [08ee6cdb35f86b168a4ba1004ed4bde5.png]

类加载各阶段完成的功能

  1. 加载阶段: 将类的 class 文件读入内存,并为之创建一个 java.lang.Class 对象,此过程由类加载器完成。
  2. 连接阶段: 又分为验证、准备、解析三个小阶段,此阶段会将类的二进制数据合并到 JRE 中。
  3. 初始化阶段:JVM 负责对类的静态成员进行初始化。
    如下图所示:
    [1804ec7b2f9dc309ab8a0624746f8f56.png]

加载阶段

JVM 在该阶段的主要目的是将字节码从不同的数据源(可能是 class 文件、jar 包、甚至网络文件)转换为二进制字节流加载到内存中,并生成一个代表该类的 java.lang.Class 对象。

连接阶段——验证

  1. 目的是为了确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
  2. 包括:文件格式验证(是否以魔数 oxcafebabe 开头)、元数据验证、字节码验证和符号引用验证 [举例说明]
  3. 可以考虑使用 -Xverify:none 参数来关闭大部分的类验证措施,缩短虚拟机类加载的时间。

连接阶段——准备

JVM 会在该阶段对静态变量分配内存并进行默认初始化(不同数据类型会有其默认初始值,如:int ---- 0,boolean ---- false 等)。这些变量的内存空间会在方法区中分配。
例子:

public class ClassLoad {
    public static void main(String[] args) {
		// 属性=成员变量=字段
    	// 类加载的连接阶段-准备,属性是如何加载的

    	public int n1 = 10;
    	public static  int n2 = 20;
    	public static final  int n3 = 30;
    }
}
  • 代码说明:
    1. n1 是实例属性, 不是静态变量,因此在准备阶段,是不会分配内存

    2. n2 是静态变量,在该阶段 JVM 会为其分配内存,n2 默认初始化的值为 0 ,而不是 20

    3. n3 被 static final 修饰,是常量, 它和静态变量不一样, 其一旦赋值后值就不变,因此其默认初始化 n3 = 30

连接阶段——解析

JVM 将常量池内的符号引用替换为直接引用的过程。

初始化阶段

  1. 在初始化阶段,JVM 才会真正执行类中定义的 Java程序代码,此阶段是执行<clinit>() 方法的过程。
  2. <clinit>() 方法是由编译器按语句在源文件中出现的顺序,依次自动收集类中的所有静态变量的赋值操作和静态代码块中的语句,并进行合并的过程。
  3. JVM 会保证一个类的 <clinit>() 方法 在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的 <clinit>() 方法,其他线程都要阻塞等待,直到活动线程执行 <clinit>() 方法完毕。
    例子:
public class ClassLoad {
    public static void main(String[] args) throws ClassNotFoundException {
        System.out.println(B.num);// 直接使用类的静态属性,也会导致类的加载
    }
}

class B {
    static { // 静态代码块
        System.out.println("B 静态代码块被执行");
        num = 300;
    }

    static int num = 100;// 静态变量

    public B() {// 构造器
        System.out.println("B() 构造器被执行");
    }
}

输出如下:

B 静态代码块被执行
100
  • 代码说明:
    1. 加载阶段: 加载 B 类,并生成 B 的 class 对象
    2. 连接阶段: 进行默认初始化 num = 0
    3. 初始化阶段: 执行 <clinit>() 方法,该方法会依次自动收集类中的所有静态变量的赋值操作和静态代码块中的语句,并合并。如下:
clinit() {
	System.out.println("B 静态代码块被执行");
    num = 300;
    num = 100;
}
  • 合并后: num = 100
    注意:加载类的时候,具有同步机制控制。如下:
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
	//正因为有这个机制,才能保证某个类在内存中, 只有一份Class对象
    synchronized (getClassLoadingLock(name)) {
    	//....
    }
}

Class 类

  1. Class 也是一个类,其类名就叫Class,因此它也继承 Object 类
  2. Class 类对象不是由我们程序员创建(new)出来的,而是在类加载时由 JVM 自动创建的
  3. 堆内存中最多只会存在某个类的唯一的 Class 对象,因为类只会加载一次
  4. 每个类的实例对象都会知道自己对应的 Class 对象
  5. 通过 Class 类对象可以完整地得到其对应的类的信息,通过一系列反射 API
  6. 类的字节码二进制数据,是存放在方法区的,又称为类的元数据(包括方法代码、变量名、方法名、访问权限等等)
    除了 int 等基本类型外,Java 的其他类型全部都是 class(包括 interface)。例如:
  • String
  • Object
  • Runnable
  • Exception

  • class(包括接口 interface)的本质是数据类型(Type)。无继承关系的数据类型无法赋值:
Number n = new Double(123.456); // 编译成功
String s = new Double(123.456); // 编译错误

而类 class 是由 JVM 在执行过程中动态加载的。JVM 在第一次读取到一种类 class 时,会将其加载进内存。
每加载一种 class,JVM 就为其创建一个 Class 类的对象,并将两者关联起来。注意:这里的 Class 类是一个名字叫 Class 的类 class。它长这样:

public final class Class {
    private Class() {}
}

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

Class cls = new Class(String);
  • 注意:这个 Class 类对象是 JVM 内部创建的,如果我们查看 JDK 源码,可以发现 Class 类的构造方法是 private,即只有 JVM 能创建 Class 类对象,我们程序员自己的 Java 程序是无法创建 Class 类对象的。
    所以,JVM 持有的每个 Class 类对象都指向一个数据类型(classinterface):
┌───────────────────────────┐
│      Class Instance       │──────> String
├───────────────────────────┤
│name = "java.lang.String"  │
└───────────────────────────┘
┌───────────────────────────┐
│      Class Instance       │──────> Random
├───────────────────────────┤
│name = "java.util.Random"  │
└───────────────────────────┘
┌───────────────────────────┐
│      Class Instance       │──────> Runnable
├───────────────────────────┤
│name = "java.lang.Runnable"│
└───────────────────────────┘

一个 Class 类对象包含了其对应的类 class 的所有完整信息:

┌───────────────────────────┐
│      Class Instance       │──────> String
├───────────────────────────┤
│name = "java.lang.String"  │
├───────────────────────────┤
│package = "java.lang"      │
├───────────────────────────┤
│super = "java.lang.Object" │
├───────────────────────────┤
│interface = CharSequence...│
├───────────────────────────┤
│field = value[],hash,...   │
├───────────────────────────┤
│method = indexOf()...      │
└───────────────────────────┘

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

方法一:直接通过一个类 class 中的静态变量 class 获取:

Class cls = String.class;// class 是 String 类中的一个静态变量

方法二:如果我们有一个类 class 的对象,可以通过该对象引用提供的 getClass() 方法获取:

String s = "Hello";
Class cls = s.getClass();// 调用 String类对象 s的 getClass() 方法获取

方法三:如果知道一个类 class 的完整类名,可以通过 Class 类的静态方法 Class.forName() 获取:

Class cls = Class.forName("java.lang.String");// java.lang.String 是 String 类的完整类名

方法四:对于基本数据类型(int、char、boolean、float 等),通过 基本数据类型 .class 获取:

Class integerClass = int.class;
Class characterClass = char.class;
Class booleanClass = boolean.class;
System.out.println(integerClass);// int

方法五:对于基本数据类型对应的包装类,可以通过类中的静态变量 TYPE 获取到 Class 类对象:

Class type1 = Integer.TYPE;
Class type2 = Character.TYPE;
System.out.println(type1);// int
  • 注意:对于基本数据类型获取到的Class类对象和基本数据类型对应的包装类获取到的 Class 类对象,是同一个 Class 类对象:
System.out.println(integerClass.hashCode());
System.out.println(type1.hashCode());// 两者相等,说明都是指向 int

因为 Class 类对象在 JVM 中是唯一的,所以,上述方法获取的 Class 类对象是同一个对象。可以用 == 比较两个 Class 类对象:

Class cls1 = String.class;

String s = "Hello";
Class cls2 = s.getClass();

boolean sameClass = cls1 == cls2; // true

注意一下用 == 比较 Class 类对象和用 instanceof 的差别:

Integer n = new Integer(123);

boolean b1 = n instanceof Integer; // true,因为 n 是 Integer  类型
boolean b2 = n instanceof Number; // true,因为 n 是 Number 类型的子类

boolean b3 = n.getClass() == Integer.class; // true,因为 n.getClass() 返回 Integer.class
boolean b4 = n.getClass() == Number.class; // false,因为 Integer.class != Number.class
  • instanceof不但匹配指定类型,还匹配指定类型的子类。而用 == 比较class类对象可以精确地判断数据类型,但不能用作子类型比较。
    • 通常情况下,我们应该用instanceof判断数据类型,因为面向抽象编程的时候,我们不关心具体的子类型。
    • 只有在需要精确判断一个类型是不是某个 class 的时候,我们才使用 ==判断 class 实例。
      因为反射的目的是为了获得某个类的实例对象的信息。因此,当我们拿到某个 Object 对象时,可以通过反射直接获取该 Objectclass 信息,而不需要使用向下转型
void printObjectInfo(Object obj) {
    Class cls = obj.getClass();
}

要从 Class 实例获取获取的基本信息,参考下面的代码(只是简单示范,后面会具体介绍):

public class Main {
    public static void main(String[] args) {
        printClassInfo("".getClass());
        printClassInfo(Runnable.class);
        printClassInfo(java.time.Month.class);
        printClassInfo(String[].class);
        printClassInfo(int.class);
    }

    static void printClassInfo(Class cls) {
        System.out.println("Class name: " + cls.getName());
        System.out.println("Simple name: " + cls.getSimpleName());
        
        if (cls.getPackage() != null) {
            System.out.println("Package name: " + cls.getPackage().getName());
        }
        
        System.out.println("is interface: " + cls.isInterface());
        System.out.println("is enum: " + cls.isEnum());
        System.out.println("is array: " + cls.isArray());
        System.out.println("is primitive: " + cls.isPrimitive());
    }
}
  • 注意到数组(例如 String[])也是一种类,而且不同于String.class,它的类名是 [Ljava.lang.String;。此外,JVM 为每一种基本类型如 int 也创建了 Class 实例,通过 int.class 访问。
    如果获取到了一个 Class 类对象,我们就可以通过该 Class 类对象来创建其对应类的实例对象:
// 获取 String 的 Class 类对象:
Class cls = String.class;
// 通过 String 的 Class 类对象创建一个 String 类的实例对象:
String s = (String) cls.newInstance();
  • 上述代码相当于 new String()。通过 Class.newInstance() 可以创建类的实例对象,它的局限是:只能调用 public 的无参数构造方法。带参数的构造方法,或者非 public 的构造方法都无法通过 Class.newInstance() 被调用。

动态加载

JVM 在执行 Java 程序的时候,并不是一次性把所有用到的 class 全部加载到内存,而是第一次需要用到 class 时才加载。例如:

public class Main {
    public static void main(String[] args) {
        if (args.length > 0) {
            create(args[0]);
        }
    }

    static void create(String name) {
        Person p = new Person(name);
    }
}
  • 当执行Main.java时,由于用到了Main类,因此,JVM 首先会把Main类对应的Class类对象Main.class加载到内存中。然而,并不会加载Person.class,除非程序执行到create()方法,JVM 发现需要加载Person类时,才会首次加载Person类对应的Class类对象Person.class。如果没有执行create()方法,那么Person.class根本就不会被加载。
  • 这就是 JVM动态加载class的特性。
    动态加载类class的特性对于 Java 程序非常重要。利用 JVM 动态加载class的特性,我们才能在运行期根据条件去加载不同的实现类。例如,Commons Logging 总是优先使用 Log4j,只有当 Log4j 不存在时,才使用 JDK 的 logging。利用 JVM 动态加载特性,大致的实现代码如下:
// Commons Logging 优先使用 Log4j:
LogFactory factory = null;

if (isClassPresent("org.apache.logging.log4j.Logger")) {
    factory = createLog4j();
} else {
    factory = createJdkLog();
}

boolean isClassPresent(String name) {
    try {
        Class.forName(name);
        return true;
    } catch (Exception e) {
        return false;
    }
}
  • 这就是为什么我们只需要把 Log4j 的 jar 包放到 classpath 中,Commons Logging 就会自动使用 Log4j 的原因。

小结

  1. JVM 为每个加载的类class及接口interface创建了对应的Class类对象来保存classinterface的所有信息;
  2. 获取一个类class对应的Class类对象后,就可以获取该类class的所有信息;
  3. 通过 Class类对象获取class信息的方法称为反射(Reflection);
  4. JVM 总是动态加载class,可以在运行期根据条件来控制加载类class

访问字段

对任意的一个Object实例,只要我们获取了它对应的Class类对象,就可以获取它的一切信息。
我们先看看如何通过Class类对象获取其对应的类定义的字段信息。Class类提供了以下几个方法来获取字段:

  1. Field getField(name):根据字段名获取某个 public 的 field(成员变量)(包括父类)
  2. Field getDeclaredField(name):根据字段名获取当前类的某个 field(成员变量)(不包括父类)
  3. Field[] getFields():获取所有 public 的 field(成员变量)(包括父类)
  4. Field[] getDeclaredFields():获取当前类的所有 field(成员变量)(不包括父类)
    我们来看一下示例代码:
public class Main {
    public static void main(String[] args) throws Exception {
        Class stdClass = Student.class;
        // 获取 public 字段 "score":
        System.out.println(stdClass.getField("score"));
        // 获取继承的 public 字段 "name":
        System.out.println(stdClass.getField("name"));
        // 获取 private 字段 "grade":
        System.out.println(stdClass.getDeclaredField("grade"));
    }
}

class Student extends Person {
    public int score;
    private int grade;
}

class Person {
    public String name;
}
  • 上述代码首先获取StudentClass实例,然后,分别获取public字段、继承的public字段以及private字段,打印出的Field类似下面:
public int Student.score
public java.lang.String Person.name
private int Student.grade
  • 一个Field对象包含了一个字段的所有信息:
    • getName():返回字段名称,例如,"name"
    • getType():返回字段类型,也是一个Class类对象,例如,String.class
    • getModifiers():返回字段的修饰符,它是一个int,不同的 bit 表示不同的含义。
      String类的value字段为例,它的定义是:
public final class String {
    private final byte[] value;
}

我们用反射获取该字段的信息,代码如下:

Field f = String.class.getDeclaredField("value");
f.getName(); // "value"
f.getType(); // class [B 表示 byte[]类型

int m = f.getModifiers();
Modifier.isFinal(m); // true
Modifier.isPublic(m); // false
Modifier.isProtected(m); // false
Modifier.isPrivate(m); // true
Modifier.isStatic(m); // false

获取字段值

利用反射拿到字段的一个Field类对象只是第一步,我们还可以拿到一个实例对象对应的该字段的值。
例如,对于一个Person类对象,我们可以先拿到其name字段对应的Field,再获取这个Person类对象的name字段的 值:

import java.lang.reflect.Field;
public class Main {
    public static void main(String[] args) throws Exception {
        Person p = new Person("Xiao Ming");
        Class c = p.getClass();
        Field f = c.getDeclaredField("name");// 获取 private String name;
        Object value = f.get(p);
        System.out.println(value); // "Xiao Ming"
    }
}

class Person {
    private String name;

    public Person(String name) {
        this.name = name;
    }
}
  • 上述代码先获取Person类对应的Class类对象,再通过该Class类对象获取Field类对象,然后,用Field.get(Object)获取指定Person类对象的指定字段的值。
  • 运行代码,如果不出意外,会得到一个IllegalAccessException异常,这是因为name被定义为一个private字段,正常情况下,Main类无法访问Person类的private字段。要修复错误,可以将private改为public,或者,在调用Object value = f.get(p);前,先写一句:
f.setAccessible(true);
  • 调用Field.setAccessible(true)的意思是,别管这个字段是不是public,一律允许访问。
  • 可以试着加上上述语句,再运行代码,就可以打印出private字段的值。
    **小问题:如果使用反射可以获取private字段的值,那么类的封装还有什么意义?
  • 答案是一般情况下,我们总是通过p.name来访问Personname字段,编译器会根据publicprotectedprivate这些访问权限修饰符决定是否允许访问字段,这样就达到了数据封装的目的。
  • 而反射是一种非常规的用法,使用反射,首先代码非常繁琐;其次,它更多地是给工具或者底层框架来使用,目的是在不知道目标对象任何信息的情况下,获取特定字段的值。
    此外,setAccessible(true)可能会失败。 如果 JVM 运行期存在SecurityManager,那么它会根据规则进行检查,有可能阻止setAccessible(true)。例如,某个SecurityManager可能不允许对javajavax开头的package的类调用setAccessible(true),这样可以保证 JVM 核心库的安全。

设置字段值

通过 Field 类对象既然可以获取到指定对象的字段值,自然也可以设置字段的值。
设置字段值是通过Field.set(Object, Object)实现的,其中第一个Object参数是指定的对象,第二个Object参数是待修改的值。示例代码如下:

import java.lang.reflect.Field;

public class Main {
    public static void main(String[] args) throws Exception {
        Person p = new Person("Xiao Ming");
        System.out.println(p.getName()); // "Xiao Ming"
        Class c = p.getClass();
        Field f = c.getDeclaredField("name");// 获取 private String name;
        f.setAccessible(true);// 允许对 private 字段进行访问
        f.set(p, "Xiao Hong");// 设置 p 的 name 的值
        System.out.println(p.getName()); // "Xiao Hong"
    }
}

class Person {
    private String name;

    public Person(String name) {
        this.name = name;
    }

    public String getName() {
        return this.name;
    }
}
  • 运行上述代码,输出的name字段从Xiao Ming变成了Xiao Hong,说明通过反射可以直接修改指定对象的字段的值。
  • 同样的,修改非public字段,需要调用setAccessible(true)

小结

  1. Java 的反射 API 提供的Field类封装了对应的类定义的全部字段的所有信息:
  2. 通过Class类对象的方法可以获取Field类对象:getField()getFields()getDeclaredField()getDeclaredFields()
  3. 通过Field类对象可以获取类定义字段信息:getName()getType()getModifiers()
  4. 通过Field类对象可以读取或设置某个对象的字段的值,如果存在访问限制,则需要调用setAccessible(true)来访问非public字段。
  5. 通过反射读写字段是一种非常规的方法,它会破坏对象的封装。

调用方法

我们已经能通过Class类的Field类对象获取其对应的类class中定义的所有字段信息,同样的,可以通过Class类获取所有Method信息。Class类提供了以下几个方法来获取类class中定义的Method

  1. Method getMethod(name, Class...):获取某个publicMethod(包括父类)
  2. Method getDeclaredMethod(name, Class...):获取当前类的某个Method(不包括父类)
  3. Method[] getMethods():获取所有publicMethod(包括父类)
  4. Method[] getDeclaredMethods():获取当前类的所有Method(不包括父类)
    我们来看一下示例代码:
public class Main {
    public static void main(String[] args) throws Exception {
        Class stdClass = Student.class;
        // 获取 public方法 getScore,形参类型为 String:
        System.out.println(stdClass.getMethod("getScore", String.class));
        // 获取继承的 public方法 getName,无参数:
        System.out.println(stdClass.getMethod("getName"));
        // 获取 private方法 getGrade,形参类型为 int:
        System.out.println(stdClass.getDeclaredMethod("getGrade", int.class));
    }
}

class Student extends Person {
    public int getScore(String type) {
        return 99;
    }
    private int getGrade(int year) {
        return 1;
    }
}

class Person {
    public String getName() {
        return "Person";
    }
}
  • 上述代码首先获取StudentClass类对象,然后,分别获取Student类中定义的public方法、继承的public方法以及private方法,打印出的Method类似:
public int Student.getScore(java.lang.String)
public java.lang.String Person.getName()
private int Student.getGrade(int)

一个Method类对象包含一个方法的所有信息:

  • getName():返回方法名称,例如:"getScore"
  • getReturnType():返回方法的返回值类型,也是一个Class实例,例如:String.class
  • getParameterTypes():返回方法的参数类型,是一个Class数组,例如:{String.class, int.class}
  • getModifiers():返回方法的修饰符,它是一个int,不同的 bit 表示不同的含义。

调用方法

当我们获取到一个Method类对象时,就可以对它进行调用。我们以下面的代码为例:

// 一般情况下调用 String 类的 substring() 方法
String s = "Hello world";
String r = s.substring(6); // "world"

如果用反射来调用substring方法,需要以下代码:

import java.lang.reflect.Method;

public class Main {
    public static void main(String[] args) throws Exception {
        // String 对象:
        String s = "Hello world";
        // 获取 String substring(int)方法,形参为 int:
        Method m = String.class.getMethod("substring", int.class);
        // 在 s 对象上调用该方法并获取结果:
        String r = (String) m.invoke(s, 6);
        // 打印调用结果:
        System.out.println(r);
    }
}
  • 注意到substring()有两个重载方法,我们获取的是String substring(int)这个方法(即形参类型为 int,且只有一个)。思考一下如何获取String substring(int, int)方法。
  • Method类对象调用invoke方法就相当于调用该substring(int)方法,invoke的第一个参数是实例对象(即在哪个实例对象上调用该方法),后面的实参要与方法参数的类型一致,否则将报错。

调用静态方法

如果获取到的Method表示一个静态方法,调用静态方法时,由于无需指定实例对象,所以invoke方法传入的第一个参数永远为null。我们以Integer.parseInt(String)方法为例:

import java.lang.reflect.Method;

public class Main {
    public static void main(String[] args) throws Exception {
        // 获取 Integer.parseInt(String) 方法,参数为 String:
        Method m = Integer.class.getMethod("parseInt", String.class);
        // 调用该静态方法并获取结果:
        Integer n = (Integer) m.invoke(null, "12345");
        // 打印调用结果:
        System.out.println(n);// 12345
    }
}

调用非 public方法

Field类对象类似,对于非 public 方法,我们虽然可以通过Class.getDeclaredMethod()获取该方法的实例对象,但直接对其调用将得到一个IllegalAccessException异常。为了调用非 public 方法,我们通过Method.setAccessible(true)允许其调用:

import java.lang.reflect.Method;

public class Main {
    public static void main(String[] args) throws Exception {
        Person p = new Person();
        Method m = p.getClass().getDeclaredMethod("setName", String.class);
        m.setAccessible(true);
        m.invoke(p, "Bob");
        System.out.println(p.name);// Bob
    }
}

class Person {
    String name;
    
    private void setName(String name) {
        this.name = name;
    }
}

同样,setAccessible(true)可能会失败。如果 JVM 运行期存在SecurityManager,那么它会根据规则进行检查,有可能阻止setAccessible(true)。例如,某个SecurityManager可能不允许对javajavax开头的package的类调用setAccessible(true),这样可以保证 JVM 核心库的安全。

多态

我们来考虑这样一种情况:一个Person类定义了hello()方法,并且它的子类Student也重写了hello()方法,那么,从Person.class获取的Method,作用于Student类对象时,调用的hello()方法到底是哪个?

import java.lang.reflect.Method;

public class Main {
    public static void main(String[] args) throws Exception {
        // 获取Person的 hello方法:
        Method h = Person.class.getMethod("hello");
        // 对 Student实例调用 hello方法:
        h.invoke(new Student());
    }
}

class Person {
    public void hello() {
        System.out.println("Person:hello");
    }
}

class Student extends Person {
    public void hello() {
        System.out.println("Student:hello");
    }
}
  • 运行上述代码,发现输出的是Student:hello,因此,使用反射调用方法时,仍然遵循多态原则:即总是调用实际类型的重写方法(如果存在)。 上述的反射代码:
Method m = Person.class.getMethod("hello");
m.invoke(new Student());
  • 实际上相当于:
Person p = new Student();
p.hello();

小结

  1. Java 的反射 API 提供的Method类对象封装了类定义的全部方法的所有信息:
  2. 通过Class类对象的方法可以获取Method类对象:getMethod()getMethods()getDeclaredMethod()getDeclaredMethods()
  3. 通过Method类对象可以获取方法信息:getName()getReturnType()getParameterTypes()getModifiers()
  4. 通过Method类对象可以调用某个对象的方法:Object invoke(Object instance, Object... parameters)
  5. 通过设置setAccessible(true)来访问非public方法;
  6. 通过反射调用方法时,仍然遵循多态原则。

调用构造方法

一般情况下,我们通常使用new操作符创建新的对象:

Person p = new Person();

如果通过反射来创建新的对象,可以调用Class提供的newInstance()方法:

Person p = Person.class.newInstance();
  • 调用Class.newInstance()的局限是,它只能调用该类的public无参构造方法。如果构造方法带有参数,或者不是public,就无法直接通过Class.newInstance()来调用。
    为了调用任意的构造方法,Java 的反射 API 提供了Constructor类对象,它包含一个构造方法的所有信息,通过Constructor类对象可以创建一个类的实例对象。Constructor类对象和Method类对象非常相似,不同之处仅在于它是一个构造方法,并且,调用结果总是返回一个类的实例对象:
import java.lang.reflect.Constructor;

public class Main {
    public static void main(String[] args) throws Exception {
        // 获取构造方法 Integer(int),形参为 int
        Constructor cons1 = Integer.class.getConstructor(int.class);
        // 调用构造方法:
        // 传入的形参必须与构造方法的形参类型相匹配
        Integer n1 = (Integer) cons1.newInstance(123);
        System.out.println(n1);

        // 获取构造方法 Integer(String),形参为 String
        Constructor cons2 = Integer.class.getConstructor(String.class);
        Integer n2 = (Integer) cons2.newInstance("456");
        System.out.println(n2);
    }
}

通过Class实例获取Constructor的方法如下:

  1. getConstructor(Class...):获取某个publicConstructor
  2. getDeclaredConstructor(Class...):获取某个Constructor
  3. getConstructors():获取所有publicConstructor
  4. getDeclaredConstructors():获取所有Constructor
    注意:Constructor类对象只含有当前类定义的构造方法,和父类无关,因此不存在多态的问题。
    同样,调用非publicConstructor时,必须首先通过setAccessible(true)设置允许访问。但setAccessible(true)也可能会失败。

小结

  1. Constructor类对象封装了其对应的类定义的构造方法的所有信息;
  2. 通过Class类对象可以获取Constructor类对象:getConstructor()getConstructors()getDeclaredConstructor()getDeclaredConstructors()
  3. 通过Constructor类对象可以创建一个对应类的实例对象:newInstance(Object... parameters); 通过设置setAccessible(true)来访问非public构造方法。

获取继承方法

当我们获取到某个Class类对象时,实际上就获取到了一个类的类型:

Class cls = String.class; // 获取到 String 的 Class 类对象

还可以用类对象的getClass()方法获取:

String s = "";
Class cls = s.getClass(); // s 是 String,因此获取到 String 的 Class

最后一种获取Class的方法是通过Class.forName(""),传入Class的完整类名获取:

Class s = Class.forName("java.lang.String");

这三种方式获取的Class类对象都是同一个对象,因为 JVM 对每个加载的Class只创建一个Class类对象来表示它的类型。

获取父类的 Class

有了Class类对象,我们还可以获取它的父类的Class类对象:

public class Main {
    public static void main(String[] args) throws Exception {
        Class i = Integer.class;
        Class n = i.getSuperclass();
        System.out.println(n);
        Class o = n.getSuperclass();
        System.out.println(o);
        System.out.println(o.getSuperclass());
    }
}
  • 运行上述代码,可以看到,Integer的父类类型是NumberNumber的父类是ObjectObject的父类是null。除Object外,其他任何非接口interfaceClass类对象都必定存在一个父类类型。

获取 interface

由于一个类可能实现一个或多个接口,通过Class我们就可以查询到实现的接口类型。例如,查询Integer实现的接口:

import java.lang.reflect.Method;

public class Main {
    public static void main(String[] args) throws Exception {
        Class s = Integer.class;
        Class[] is = s.getInterfaces();
        for (Class i : is) {
            System.out.println(i);
        }
    }
}

运行上述代码可知,Integer实现的接口有:

  • java.lang.Comparable
  • java.lang.constant.Constable
  • java.lang.constant.ConstantDesc
    要特别注意:getInterfaces()方法只返回当前类直接实现的接口类型,并不包括其父类实现的接口类型:
// reflection
import java.lang.reflect.Method;

public class Main {
    public static void main(String[] args) throws Exception {
        Class s = Integer.class.getSuperclass();
        Class[] is = s.getInterfaces();
        for (Class i : is) {
            System.out.println(i);
        }
    }
}
  • Integer的父类是NumberNumber类实现的接口是java.io.Serializable
    此外,对所有接口interfaceClass类对象调用getSuperclass()返回的是null,获取接口的父接口要用getInterfaces()
System.out.println(java.io.DataInputStream.class.getSuperclass()); 
// 输出 java.io.FilterInputStream。因为 DataInputStream 继承自 FilterInputStream

System.out.println(java.io.Closeable.class.getSuperclass()); 
// 输出 null。因为对接口调用 getSuperclass()总是返回 null,获取接口的父接口要用 getInterfaces()
  • 如果一个类没有实现任何interface,那么getInterfaces()返回空数组。

继承关系

当我们判断一个对象是否是某个类型时,正常情况下,使用instanceof操作符:

Object n = Integer.valueOf(123);
boolean isDouble = n instanceof Double; // false
boolean isInteger = n instanceof Integer; // true
boolean isNumber = n instanceof Number; // true
boolean isSerializable = n instanceof java.io.Serializable; // true

如果是两个Class类对象,要判断一个向上转型是否成立,可以调用isAssignableFrom()方法:

// Integer i = ?
Integer.class.isAssignableFrom(Integer.class); // true,因为Integer可以赋值给Integer
// Number n = ?
Number.class.isAssignableFrom(Integer.class); // true,因为Integer可以赋值给Number
// Object o = ?
Object.class.isAssignableFrom(Integer.class); // true,因为Integer可以赋值给Object
// Integer i = ?
Integer.class.isAssignableFrom(Number.class); // false,因为Number不能赋值给Integer

小结

  1. 通过Class对象可以获取继承关系:
    • Class getSuperclass():获取父类类型;
    • Class[] getInterfaces():获取当前类实现的所有接口。
  2. 通过Class对象的isAssignableFrom()方法可以判断一个向上转型是否可以实现。

动态代理

我们来比较 Java 的类class和接口interface的区别:

  • 可以实例化类class(非abstract);
  • 不能实例化接口interface
    所有接口interface类型的变量总是通过某个实现了接口的类的对象向上转型再赋值给接口类型的变量:
CharSequence cs = new StringBuilder();

有没有可能不编写实现类,直接在运行期创建某个interface的实例呢?
这是可能的,因为 Java 标准库提供了一种动态代理(Dynamic Proxy)的机制:可以在运行期动态创建某个interface的实例。
什么叫运行期动态创建?听起来好像很复杂。所谓动态代理,是和静态相对应的。我们来看静态代理代码怎么写:
一、定义接口:

public interface Hello {
    void morning(String name);
}

二、编写实现类:

public class HelloWorld implements Hello {
    public void morning(String name) {
        System.out.println("Good morning, " + name);
    }
}

三、创建实例,转型为接口并调用:

Hello hello = new HelloWorld();
hello.morning("Bob");
  • 这种方式就是我们通常编写代码的方式。
    还有一种方式是动态代码,我们仍然先定义了接口Hello,但是我们并不去编写实现类,而是直接通过 JDK 提供的一个Proxy.newProxyInstance()方法创建了一个Hello接口对象。这种没有实现类但是在运行期动态创建了一个接口对象的方式,我们称为动态代理。JDK 提供的动态创建接口对象的方式,就叫动态代理
    一个最简单的动态代理实现如下:
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

public class Main {
    public static void main(String[] args) {
        InvocationHandler handler = new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                System.out.println(method);
                if (method.getName().equals("morning")) {
                    System.out.println("Good morning, " + args[0]);
                }
                return null;
            }
        };
        Hello hello = (Hello) Proxy.newProxyInstance(
            Hello.class.getClassLoader(), // 传入 ClassLoader
            new Class[] { Hello.class }, // 传入要实现的接口
            handler); // 传入处理调用方法的 InvocationHandler
        hello.morning("Bob");
    }
}

interface Hello {
    void morning(String name);
}

在运行期动态创建一个interface实例的方法如下:

  1. 定义一个InvocationHandler实例,它负责实现接口的方法调用;
  2. 通过Proxy.newProxyInstance()创建interface实例,它需要3个参数:
    1. 使用的ClassLoader,通常就是接口类的ClassLoader
    2. 需要实现的接口数组,至少需要传入一个接口进去;
    3. 用来处理接口方法调用的InvocationHandler实例。
  3. 将返回的Object强制转型为接口。
    动态代理实际上是 JVM 在运行期动态创建 class 字节码并加载的过程,它并没有什么黑魔法,把上面的动态代理改写为静态实现类大概长这样:
public class HelloDynamicProxy implements Hello {
    InvocationHandler handler;
    public HelloDynamicProxy(InvocationHandler handler) {
        this.handler = handler;
    }
    public void morning(String name) {
        handler.invoke(
           this,
           Hello.class.getMethod("morning", String.class),
           new Object[] { name });
    }
}
  • 其实就是 JVM 帮我们自动编写了一个上述类(不需要源码,可以直接生成字节码),并不存在可以直接实例化接口的黑魔法。

小结

  1. Java 标准库提供了动态代理功能,允许在运行期动态创建一个接口的实例;
  2. 动态代理是通过Proxy创建代理对象,然后将接口方法“代理”给InvocationHandler完成的。

标签:反射,Java,String,对象,class,Class,获取,public
From: https://www.cnblogs.com/TMesh/p/18687942

相关文章

  • Java 泛型
    目录泛型概述使用泛型的好处泛型的定义与使用定义和使用含有泛型的类含有泛型的方法含有泛型的接口定义类时确定泛型的类型始终不确定泛型的类型,直到创建对象时,确定泛型的类型泛型通配符通配符基本使用通配符高级使用----受限泛型泛型概述在前面学习集合时,我们都知道集合中是可......
  • [新]Java8的新特性
    原文首发在我的博客:https://blog.liuzijian.com/post/86955c3b-9635-47a0-890c-f1219a27c269.html1.Lambda表达式lambda表达式是Java8的重要更新,lambda表达式可以用更简洁的代码来创建一个只有一个抽象方法的接口(函数式接口)的实例,从而更简单的创建匿名内部类的对象。语法和......
  • 基于Java的宠物医院管理系统 毕业设计源码14635
    目 录1绪论1.1选题背景1.2研究现状1.3论文结构与章节安排2 宠物医院管理系统系统分析2.1可行性分析2.1.1技术可行性分析2.1.2 操作可行性分析2.1.3 法律可行性分析2.2系统功能分析2.2.1功能性分析2.2.2非功能性分析2.3 系统用例分析......
  • JAVA 策略模式
    策略模式(strategypattern)的原始定义是:定义一系列算法,将每一个算法封装起来,并使它们可以相互替换。策略模式让算法可以独立于使用它的客户端而变化。不同国家发送短信验证码算法不同例如中国和哈萨克斯坦的短信模版、使用系统、签名不同策略模式实现策略模式的本质是通过Conte......
  • java基础Day6 java数组
    一、数组的定义二、数组的声明和创建dataType[]arrayRefVar;//首选方法dataTypearrayRefVar[];//效果相同,但不是首选方法int[]nums;//声明一个数组nums=newint[10];//创建一个数组//给数组元素赋值nums[0]=1;nums[1]=2;nums[2]=3;nums[3]=4;nums[4]=......
  • JAVA与数据结构-线性表
    目录一.线性表的概念二.线性表的关系及分类三.数组与顺序表四.链表1.静态链表(链表的的数组底层实现)2.循环链表3.双向链表五.栈1.栈的概念2.栈的底层实现3.共享空间栈4.逆波兰表达式(后缀表达式)5.栈与递归 六.队列1.队列概念2.队列的底层实现3.循环队列七.链......
  • java基于SSM框架的健康医疗体检管理系统
    Java基于SSM(Spring+SpringMVC+MyBatis)框架的健康医疗体检管理系统是一种专为医疗机构设计的信息化解决方案。一、系统背景与目的随着医疗行业的快速发展和人们对健康需求的日益增加,体检业务已成为医疗机构的重要组成部分。为了提高体检业务的管理效率和服务质量,基于Java和......
  • Java02-基础语法
    Java基础语法[任务列表]1.注释2.字面量3.变量4.关键字、标识符5.方法6.类型转换7.输入输出8.运算符9.其他知识点——————————————————————————————————————————————————1.注释注释:解释说明代码功能。单行注......
  • JAVA实战开源项目:在线旅游网站(Vue+SpringBoot) 附源码
    本文项目编号T025,文末自助获取源码\color{red}{T025,文末自助获取源码}......
  • JAVA实战开源项目:社区团购系统(Vue+SpringBoot) 附源码
    本文项目编号T024,文末自助获取源码\color{red}{T024,文末自助获取源码}......