Java 的反射机制

主要内容

  1. Class的基本概念以及创建方式
  2. 反射的基本概念
  3. 类的各个组成部分对应的反射类 Constructor Field Method
  4. 反射在框架中的应用

反射的基础 Class

Java当中的类用来表示具有相同属性和方法的对象的结合,是抽象的概念。对象是类创建的,同一个类的不同对象具有不同的属性值。

Java当中定义的所有类都属于同一类事物,可以用Class来表示。

对比理解

  • 不同的人可以用Person类来表示。人->Person
  • Java当中定义的不同类可以用Class来表示。 Java当中的类->Class

Class类的对象字节码
Class类的对象就是不同的类对应的字节码。
获取Class的对象

Person p1=new Person();
Class class1=new Class();//Error


三种方式:

//(1) 对象名.getClass() 返回该对象的运行时类对象。
Person p1 = new Person();
Class<?> clazz1 = p1.getClass();
System.out.println(clazz1); // 输出class Person

//(2) 类名.class 返回指定类的Class对象。
Class<?> clazz2 = Date.class;
System.out.println(clazz2); // 输出class java.util.Date

//(3) Class.forName()方法 通过类的全限定名加载该类,并返回该类的Class对象。
Class<?> clazz3 = Class.forName("java.lang.String");
System.out.println(clazz3); // 输出class java.lang.String

基本数据类型的Class对象

Java中的基本数据类型都有对应的Class对象。基本数据类型的Class对象可以用类似的方式获取:
int.class //表示int类型的Class对象
boolean.class //表示boolean类型的Class对象
short.class //表示short类型的Class对象
byte.class //表示byte类型的Class对象
char.class //表示char类型的Class对象
long.class //表示long类型的Class对象
float.class //表示float类型的Class对象
double.class //表示double类型的Class对象
另外,基本数据类型对应的封装类都有一个静态字段TYPE,表示该封装类所封装的基本类型的Class对象。
例如:
int.class == Integer.TYPE //返回true,表示int类型的Class对象和Integer类的TYPE是同一个对象
void.class == Void.TYPE //返回true,表示void类型的Class对象和Void类的TYPE是同一个对象

反射(Reflect)


反射(Reflect)是指在运行时动态地获取和操作类的信息。通过反射,可以在运行时获取类的属性、方法、构造方法等组成部分,并能够动态地调用或修改它们。



通过反射,可以实现以下功能:

  1. 获取类的Class对象:可以通过类的全限定名来获取类的Class对象,从而可以通过Class对象获取类的各种信息。
  2. 获取类的属性:可以通过Class对象获取类的所有属性,并可以动态地读取和修改属性的值。
  3. 获取类的方法:可以通过Class对象获取类的所有方法,并可以动态地调用方法。
  4. 获取类的构造方法:可以通过Class对象获取类的所有构造方法,并可以动态地创建类的对象。
  5. 动态创建对象:通过反射可以根据类的Class对象动态地创建对象实例。

反射在Java中的应用场景很广泛,例如常见的依赖注入框架、ORM框架、动态代码生成等都使用了反射技术。通过反射,程序可以在运行时根据需要动态地获取和操作类的信息,使代码更加灵活和可扩展。


Class类定义了一系列方法来获取java类的属性、方法、构造方法、包等信息,这些信息都有相应的类来表示,分别是Field、 Method、Constructor、Package等。

java.lang.reflect包中,有一系列用于描述和操作类的属性、方法、构造方法、包等的类,包括:

  • Field:用于描述类的属性(字段)。

  • Method:用于描述类的方法。

  • Constructor:用于描述类的构造方法。

  • Package:用于描述类的包信息。

  • AnnotatedElement:用于描述在类、方法、字段等元素上使用的注解信息。

  • Modifier:用于描述类、方法、字段等的修饰符信息。

  • Proxy:用于动态生成代理类的类。

这些类提供了丰富的方法和功能,能够帮助我们在运行时获取和操作类的各个组成部分。通过使用这些类,我们可以实现动态加载类、动态调用方法、修改类的属性等操作。

所以,请确保使用java.lang.reflect包中的相关类来描述和操作类的组成部分。


Constructor类

Constructor类是用来描述类中定义的构造方法的类。通过Constructor类,可以获取和操作类中定义的构造方法。

一些常用的方法包括:

(1) 获取类的所有构造方法:可以通过Class类的getConstructors()方法来获取类的所有公共构造方法。返回的是一个Constructor数组,每个元素代表一个构造方法。

Constructor<?>[] constructors = Class.forName("java.lang.String").getConstructors();

(2) 获取类中某个具体的构造方法:可以通过Class类的getConstructor()方法来获取类中指定参数类型的公共构造方法。需要传入构造方法参数类型的Class对象作为参数。

Constructor<?> constructor = Class.forName("java.lang.String").getConstructor(String.class);

Constructor类的一种常见应用是通过反射来动态创建类的对象。使用Constructor类的newInstance()方法可以创建类的实例。示例如下:

Constructor<?> constructor = Class.forName("java.lang.String").getConstructor(String.class);
String str = (String) constructor.newInstance("hello");

另外,对于类的无参构造方法,可以使用Class类的newInstance()方法直接创建对象,不需要使用Constructor类。

Date d = (Date) Class.forName("java.util.Date").newInstance();

需要注意的是,Java运行环境缓存了类的无参构造方法的Constructor对象,因此可以直接通过Class实例的newInstance()方法来创建对象。只有当要用某个类的无参构造方法创建该类对象时,可以省略创建Constructor类对象的这个过程。同时,使用反射创建对象需要处理异常,例如ClassNotFoundException、NoSuchMethodException和InstantiationException等。


Field类

Field类用来表示类中的属性(字段)。以下是对Field类常用的方法的解释:

  1. Class.getFields():获取Class对象中的所有公共字段,返回一个包含Field对象的数组。
  2. Class.getField(String name):根据指定的字段名称,返回一个公共字段的Field对象。
  3. Field对象代表某个类的属性(字段),而不是某个类的对象的属性。要获取某个对象对应的字段值,需要使用Field对象的get(Object obj)方法,并将具体的对象作为参数传递给它。
  4. 对于非公共(私有)字段,只能通过调用Class类的getDeclaredField(String fieldName)方法来获取。
  5. 对于私有字段,需要使用Field对象的setAccessible(boolean flag)方法将其设置为可访问,然后才能修改或获取其值。
  6. Field类的getType()方法用于获取字段的数据类型,返回一个Class对象,表示字段所属的类型。

Field类
 练习:扫描一个对象中所有的字符串类型的属性,并为每一个字符串属性的值后面添加
"hello"字符串,然后将这个对象输出。

public class Person {
    private final String firstName;
    private final String lastName;
    private final int age;
    private final String email;

    public Person(String firstName, String lastName, int age, String email) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.age = age;
        this.email = email;
    }

    @Override
    public String toString() {
        return "Person{" + "firstName='" + firstName + '\'' + ", lastName='" + lastName + '\'' + ", age=" + age + ", email='" + email + '\'' + '}';
    }
}

import org.junit.jupiter.api.Test;
import java.lang.reflect.Field;

public class ReflectTest {
    @Test
    public  void testFiled() throws IllegalAccessException {
        // 创建一个对象
        Person person = new Person("John", "Doe", 25, "example@example.com");

        // 获取对象的Class对象
        Class<?> clazz = person.getClass();

        // 获取对象的所有属性
        Field[] fields = clazz.getDeclaredFields();

        // 遍历属性
        for (Field field : fields) {
            // 判断属性是否为字符串类型
            if (field.getType() == String.class) {
                // 设置属性的可访问性,以便修改私有属性的值
                field.setAccessible(true);

                // 获取属性的原始值
                String value = (String) field.get(person);

                // 修改属性的值,添加"hello"字符串
                field.set(person, value + "hello");
            }
        }

        // 输出修改后的对象
        System.out.println(person);
    }
}

注意:对于字节码的比较一定要用"==",而不是equals()。
掌握上述例子对于理解Spring等框架技术具有重要作用。
 


Method类

Method类用于表示类中的方法。通过Class对象的如下方法可以获取到Method对象:

  • getMethod(String name, Class<?>... parameterTypes):按照方法名和参数类型,获取指定的公共方法(包括从父类或接口继承的方法)。

  • getMethods():获取所有的公共方法(包括从父类或接口继承的方法)。

  • getDeclaredMethods():获取所有的方法(不包括继承的方法)。

  • getDeclaredMethod(String name, Class<?>... parameterTypes):按照方法名和参数类型,获取指定的方法(不包括继承的方法)。

注意,上述方法需要传递方法名所对应的字符串以及该方法所需要的参数类型的字节码。

在Method类中,可以通过调用invoke(Object obj, Object... args)方法来调用方法。其中,第一个参数obj是方法所属的对象,如果是静态方法,则传递null;第二个参数args是方法的实际参数。

需要注意的是,如果调用的是一个静态方法,在invoke(Object obj, Object... args)方法中,第一个参数需要使用null表示。


对接受的数组参数的方法进行反射

问题:编写一个程序,这个程序能够接收用户提供的类名,然后去执行这个类中的public static void main(String[] args)方法。

注意:JDK1.4的编译器会把数组中的每一个元素作为一个参数,来调用此接收数组参数的方法,这样的话就出现了参数不对应的问题。

mainMethod.invoke(null ,new String[]{"aaa","bbb","ccc"});

JDK5为了兼容1.4,就必须将整个数组作为一个只有一个元素的Object数组进行传递。 以下两种方式正确:

(1) mainMethod.invoke(null,new Object[]{new String[]{"aaa","bbb","ccc"}); 
(2) mainMethod.invoke(null,(Object)new String[]{"aaa","bbb","ccc"});

要执行一个类中的public static void main(String[] args)方法,可以使用反射来实现。下面是一个示例程序,展示了如何根据用户提供的类名执行相应的main方法:

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
​
public class MainMethodExecutor {
    public static void main(String[] args) {
        // 获取用户输入的类名
        String className = "com.example.SampleClass";
​
        try {
            // 加载类
            Class<?> clazz = Class.forName(className);
​
            // 获取main方法
            Method mainMethod = clazz.getMethod("main", String[].class);
​
            // 构造传递给main方法的参数数组
            String[] arguments = new String[]{"aaa", "bbb", "ccc"};
​
            // 执行main方法
            mainMethod.invoke(null, new Object[]{arguments});
            // 或者:
            // mainMethod.invoke(null, (Object) arguments);
        } catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
            e.printStackTrace();
        }
    }
}

在上述示例中,我们首先根据用户提供的类名加载对应的类。然后通过getMethod()方法获取到main方法。接着构造传递给main方法的参数数组。最后使用invoke()方法来执行main方法,其中第一个参数为null表示静态方法,第二个参数为参数数组。

在JDK5之前,由于参数数组中的每个元素会被作为单独的参数传递,需要将整个数组作为一个只有一个元素的Object数组传递。在JDK5及以后的版本,可以直接将参数数组作为传递给invoke()方法的第二个参数。

需要注意的是,上述示例中的类名和方法名需要根据实际情况进行替换。并且在实际应用中,还需要进行异常处理等逻辑的优化。


数组的反射

Java中的数组是复合数据类型,具有相同元素类型和维度的数组被视为同一类型,它们具有相同的字节码对象。

数组的字节码对象可以调用getSuperclass()方法来获取其父类,对于数组来说,其父类是Object的字节码对象。

在Java中,基本数据类型的一维数组可以被视为Object类型使用,但不能当作Object[]类型使用;而基本数据类型的二维数组则可以当作Object[]类型使用;复合数据类型(对象类型)的一维数组既可以当作Object类型使用,也可以当作Object[]类型使用。

java.lang.reflect.Array类提供了用于完成对数组的反射操作的工具方法。以下是一些常用的方法:

  • Array.newInstance(Class<?> componentType, int length):根据元素类型和数组长度创建一个新数组实例。

  • Array.getLength(Object array):获取数组的长度。

  • Array.get(Object array, int index):获取数组中指定索引位置的元素值。

  • Array.set(Object array, int index, Object value):设置数组中指定索引位置的元素值。

通过以上方法,可以使用反射动态操作数组的创建、访问和修改。

需要注意的是,使用Array类进行操作时,需要注意元素的类型、数组的长度以及索引的合法范围,否则可能会导致IllegalArgumentExceptionArrayIndexOutOfBoundsException等异常。


反射的作用-实现框架(framework)

反射在框架开发中的作用非常重要。框架是一个半成品的软件,提供了一些基础的结构和功能,开发人员可以根据自己的需求进行扩展和定制,从而快速构建出符合特定需求的软件产品。

反射的作用在于实现框架的扩展性和灵活性。通过反射,框架可以动态地加载和使用类,无需在编译期间就确定要调用的类名。这使得开发人员能够在框架中使用自定义的类,并在运行时决定要调用的方法和属性,从而实现了框架的可插拔性和动态性。

反射还可以通过读写配置文件来实现框架的配置和定制化。Properties类是Java中读写属性文件的一种常用机制。通过使用Properties类,框架可以读取一个配置文件中的属性信息,例如类名、方法名、参数等,并根据这些配置信息来加载和调用对应的类和方法,从而实现框架的自定义配置和灵活性。

总结起来,反射在框架开发中的作用主要体现在实现动态加载和使用类、实现框架的可插拔性和配置化,提高软件系统的扩展性和灵活性。

框架和工具类的区别:面向对象的本质就是类与类之间的相互调用。你调用别人的类,那么别人写的类就是工具类;别人写的类来调用你的类,那么别人的类就是框架。

反射要实现的核心功能:无法知道将来要调用的类名,所以就不能使用new来创建一个对象,因此必须使用反射的机制。 

当涉及到读写配置文件、动态加载类和使用反射时,可以使用java.util.Properties类来实现。

读属性:

  1. 创建Properties对象:Properties prop = new Properties();

  2. 创建配置文件输入流:FileInputStream fis = new FileInputStream("config.properties");

  3. 加载配置文件:prop.load(fis);

  4. 关闭输入流:fis.close();

  5. 通过getProperty()方法获取属性值:String value = prop.getProperty("propertyName");

写属性:

  1. 使用setProperty()方法设置属性值:prop.setProperty("propertyName", "propertyValue");

  2. 创建配置文件输出流:FileOutputStream fos = new FileOutputStream("config.properties");

  3. 将配置写入文件:prop.store(fos, "Comments");

  4. 关闭输出流:fos.close();

从配置文件中读取类名和配置信息,使用反射来创建集合类对象并添加元素:

  1. 从配置文件中获取集合类名和元素类名。

  2. 使用Class.forName()方法动态加载集合类和元素类。

  3. 获取集合类和元素类的构造方法。

  4. 使用反射创建集合对象和元素对象。

  5. 使用集合对象的方法来操作和添加元素。

最后,可以遍历集合对象的元素,并进行输出。

练习:采用配置文件+反射的方式创建集合类对象,并在该集合中添加若干对象元素,遍历集合对象,输出其中的元素。 


通过类加载器加载配置文件 

实际项目开发中,配置文件肯定不会和源文件放在一起,肯定放在一定的ClassPath路径下,并且包含在某个包中。通过字节码文件(Class对象),可以得到类加载器,然后再通过
 public lnputStream getResourceAsStream(String name)读取配置文件。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

朝闻道 晨星

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值