JAVA基础| 反射

目录

1、反射定义

2、class 类

class 类的本质

JVM 对 class 的加载

Class 实例与 class 的关系

获取 Class 实例的方法

Class 实例比较与 instanceof 的区别

通过 Class 实例获取基本信息

通过 Class 实例创建对象

3、访问字段、调用方法

访问字段

调用方法

4、调用构造方法、获取继承关系

调用构造方法

获取继承关系

5、小结


1、反射定义

在 Java 中,只有 JVM 能创建Class实例,我们自己的 Java 程序无法直接创建。当 JVM 加载一个 class 时,会为其创建一个对应的Class类型实例,该实例保存了该 class 的所有完整信息,包括类名、包名、父类、实现的接口、所有方法、字段等。通过Class实例获取 class 信息的方法被称为反射(Reflection)。利用反射,我们可以在运行时动态获取对象的类型信息、操作对象的字段和方法,甚至创建对象实例,极大地增强了 Java 程序的灵活性。

2、class 类

class 类的本质

除了intdouble等基本类型外,Java 的其他类型(包括接口)全部都是class。例如:StringObjectRunnableException等。从本质上讲,class(包括interface)是数据类型(Type),不同数据类型之间若没有继承关系则无法进行赋值操作,如:

Number n = new Double(123.456); // 正确,Double是Number的子类
String s = new Double(123.456); // 编译错误,String与Double无继承关系

JVM 对 class 的加载

JVM 在执行过程中会动态加载class。当第一次读取到某一class类型时,JVM 会将其加载进内存,并为该class创建一个Class类型的实例,建立起关联。Class类是 JDK 中的一个特殊类,其定义如下:

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

可以看到,Class类的构造方法是私有的,这意味着只有 JVM 能创建Class实例,我们自己的 Java 程序无法直接创建。

Class 实例与 class 的关系

JVM 持有的每个Class实例都唯一指向一个数据类型(class 或 interface)。一个Class实例包含了对应class的所有完整信息,例如对于String类的Class实例,其包含的信息可能有:

  • 类名:java.lang.String
  • 包名:java.lang
  • 父类:java.lang.Object
  • 实现的接口:CharSequence
  • 字段:value[]hash
  • 方法:indexOf()

获取 Class 实例的方法

获取一个classClass实例有三种方法:

1、直接通过 class 的静态变量 class 获取

Class cls = String.class;

2、通过实例变量的 getClass () 方法获取

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

3、通过 Class.forName () 方法获取

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

需要注意的是,由于Class实例在 JVM 中是唯一的,因此通过上述三种方法获取的Class实例是同一个实例,可以使用==进行比较:

Class cls1 = String.class;
String s = "Hello";
Class cls2 = s.getClass();
boolean sameClass = cls1 == cls2; // 结果为true

Class 实例比较与 instanceof 的区别

Class实例比较(使用==)和instanceof操作符在功能上有明显差别:

  • instanceof:用于判断对象是否是某个类型或其子类型的实例,具有继承关系的判断能力。
Integer n = new Integer(123);
boolean b1 = n instanceof Integer; // true,n是Integer类型
boolean b2 = n instanceof Number; // true,n是Number类型的子类
  • ==判断Class实例:用于精确判断数据类型是否完全一致,不能进行子类型比较。
Integer n = new Integer(123);
boolean b3 = n.getClass() == Integer.class; // true
Class c1 = n.getClass();
Class c2 = Number.class;
boolean b4 = c1 == c2; // false,Integer.class与Number.class不同

通常情况下,我们使用instanceof判断数据类型,因为它更符合面向对象编程中面向抽象的思想。只有在需要精确判断一个类型是否为某个具体class时,才使用==判断Class实例。

通过 Class 实例获取基本信息

当我们拿到一个Class实例后,可以通过它获取该class的基本信息,以下是一个示例代码:

// reflection
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[]的类名是[Ljava.lang.String;
  • JVM 为每一种基本类型(如int)也创建了对应的Class实例,可以通过int.class等方式访问。

通过 Class 实例创建对象

获取到Class实例后,我们可以通过它来创建对应类型的实例:

// 获取String的Class实例
Class cls = String.class;
// 创建一个String实例,相当于new String()
String s = (String) cls.newInstance();

需要注意的是,Class.newInstance()方法存在一定的局限性:它只能调用类的public无参数构造方法,带参数的构造方法或非public的构造方法无法通过该方法调用。

3、访问字段、调用方法

访问字段

通过反射,我们可以在运行时访问和修改对象的字段,即使这些字段是私有的。下面是一个访问和修改字段的示例:

import java.lang.reflect.Field;

class Person {
    private String name;
    public int age;

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

public class FieldAccess {
    public static void main(String[] args) throws Exception {
        // 创建Person实例
        Person person = new Person("张三", 20);
        
        // 获取Class实例
        Class cls = person.getClass();
        
        // 获取public字段age
        Field ageField = cls.getField("age");
        System.out.println("修改前age: " + ageField.getInt(person));
        ageField.setInt(person, 25);
        System.out.println("修改后age: " + ageField.getInt(person));
        
        // 获取private字段name
        Field nameField = cls.getDeclaredField("name");
        // 由于name是private字段,需要设置访问权限
        nameField.setAccessible(true);
        System.out.println("修改前name: " + nameField.get(person));
        nameField.set(person, "李四");
        System.out.println("修改后name: " + nameField.get(person));
    }
}

在上述代码中:

  • getField(String name):用于获取类的public字段(包括从父类继承的字段)。
  • getDeclaredField(String name):用于获取类自身声明的字段(无论访问修饰符是什么),但不能获取从父类继承的字段。
  • setAccessible(true):用于设置字段的访问权限,使我们可以访问私有的字段。

调用方法

通过反射,我们还可以在运行时调用对象的方法,同样可以调用私有的方法。下面是一个调用方法的示例:

import java.lang.reflect.Method;

class Calculator {
    private int add(int a, int b) {
        return a + b;
    }
    
    public int multiply(int a, int b) {
        return a * b;
    }
}

public class MethodInvocation {
    public static void main(String[] args) throws Exception {
        // 创建Calculator实例
        Calculator calculator = new Calculator();
        
        // 获取Class实例
        Class cls = calculator.getClass();
        
        // 调用public方法multiply
        Method multiplyMethod = cls.getMethod("multiply", int.class, int.class);
        int result1 = (int) multiplyMethod.invoke(calculator, 3, 4);
        System.out.println("3 * 4 = " + result1);
        
        // 调用private方法add
        Method addMethod = cls.getDeclaredMethod("add", int.class, int.class);
        // 设置访问权限
        addMethod.setAccessible(true);
        int result2 = (int) addMethod.invoke(calculator, 5, 6);
        System.out.println("5 + 6 = " + result2);
    }
}

在上述代码中:

  • getMethod(String name, Class... parameterTypes):用于获取类的public方法(包括从父类继承的方法),需要指定方法名和参数类型。
  • getDeclaredMethod(String name, Class... parameterTypes):用于获取类自身声明的方法(无论访问修饰符是什么),但不能获取从父类继承的方法。
  • invoke(Object obj, Object... args):用于调用方法,第一个参数是方法的调用者,后面的参数是方法的参数。

4、调用构造方法、获取继承关系

调用构造方法

通过反射,我们可以在运行时调用类的构造方法,包括私有的构造方法,这比使用Class.newInstance()方法更加灵活。下面是一个调用构造方法的示例:

import java.lang.reflect.Constructor;

class Person {
    private String name;
    private int age;

    // 无参构造方法
    public Person() {
        this.name = "默认姓名";
        this.age = 0;
    }

    // 有参构造方法
    private Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
}

public class ConstructorInvocation {
    public static void main(String[] args) throws Exception {
        // 获取Person的Class实例
        Class cls = Person.class;
        
        // 调用无参构造方法
        Constructor defaultConstructor = cls.getConstructor();
        Person person1 = (Person) defaultConstructor.newInstance();
        System.out.println("通过无参构造方法创建的Person: " + person1);
        
        // 调用私有的有参构造方法
        Constructor parameterizedConstructor = cls.getDeclaredConstructor(String.class, int.class);
        // 设置访问权限
        parameterizedConstructor.setAccessible(true);
        Person person2 = (Person) parameterizedConstructor.newInstance("张三", 20);
        System.out.println("通过有参构造方法创建的Person: " + person2);
    }
}

在上述代码中:

  • getConstructor(Class... parameterTypes):用于获取类的public构造方法,需要指定参数类型。
  • getDeclaredConstructor(Class... parameterTypes):用于获取类自身声明的构造方法(无论访问修饰符是什么),但不能获取从父类继承的构造方法。
  • newInstance(Object... args):用于调用构造方法创建对象,参数是构造方法的参数。

获取继承关系

通过反射,我们可以获取类的继承关系,包括父类和实现的接口。下面是一个获取继承关系的示例:

public class InheritanceDemo {
    public static void main(String[] args) {
        // 获取String的Class实例
        Class cls = String.class;
        
        System.out.println("类名: " + cls.getName());
        
        // 获取父类
        Class superClass = cls.getSuperclass();
        System.out.println("父类: " + (superClass != null ? superClass.getName() : "没有父类"));
        
        // 获取实现的接口
        Class[] interfaces = cls.getInterfaces();
        System.out.println("实现的接口:");
        for (Class iface : interfaces) {
            System.out.println("  " + iface.getName());
        }
        
        // 递归获取所有父类
        System.out.println("所有父类:");
        printSuperclasses(cls);
    }
    
    // 递归打印所有父类
    private static void printSuperclasses(Class cls) {
        if (cls != null && cls != Object.class) {
            System.out.println("  " + cls.getName());
            printSuperclasses(cls.getSuperclass());
        }
    }
}

在上述代码中:

  • getSuperclass():用于获取类的直接父类,如果没有父类(如Object类),则返回null
  • getInterfaces():用于获取类实现的所有接口。

通过反射获取类的继承关系,我们可以在运行时动态了解类的层次结构,这对于一些需要动态处理类关系的场景非常有用。

5、小结

  • JVM 为每个加载的classinterface创建对应的Class实例,该实例保存了classinterface的所有信息。
  • 获取Class实例后,我们可以通过反射获取class的所有信息,包括字段、方法、构造方法、继承关系等。
  • 反射使我们能够在运行时动态操作对象,极大地增强了 Java 程序的灵活性,但同时也会带来一定的性能开销,因此在实际开发中应合理使用反射。
  • JVM 采用动态加载class的方式,我们可以在运行期根据条件来控制加载class,这为许多 Java 框架和库的实现提供了基础。

参考链接:

Class类 - Java教程 - 廖雪峰的官方网站

你可能感兴趣的:(开发语言,java,jvm)