34. 【Java教程】反射

本小节我们来学习一个 Java 语言中较为深入的概念 —— 反射(reflection),很多小伙伴即便参与了工作,可能也极少用到 Java 反射机制,但是如果你想要开发一个 web 框架,反射是不可或缺的知识点。本小节我们将了解到 什么是反射反射的使用场景,不得不提的 Class 类,如何通过反射访问类内部的字段、方法以及构造方法等知识点。

1. 什么是反射

Java 的反射(reflection)机制是指在程序的运行状态中,可以构造任意一个类的对象,可以了解任意一个对象所属的类,可以了解任意一个类的成员变量和方法,可以调用任意一个对象的属性和方法。这种动态获取程序信息以及动态调用对象的功能称为 Java 语言的反射机制。反射被视为动态语言的关键。

通常情况下,我们想调用一个类内部的属性或方法,需要先实例化这个类,然后通过对象去调用类内部的属性和方法;通过 Java 的反射机制,我们就可以在程序的运行状态中,动态获取类的信息,注入类内部的属性和方法,完成对象的实例化等操作。

概念可能比较抽象,我们来看一下结合示意图看一下:

图中解释了两个问题:

  1. 程序运行状态中指的是什么时刻Hello.java 源代码文件经过编译得到 Hello.class 字节码文件,想要运行这个程序,就要通过 JVM 的 ClassLoader (类加载器)加载 Hello.class,然后 JVM 来运行 Hello.class,程序的运行期间指的就是此刻;
  2. 什么是反射,它有哪些功能:在程序运行期间,可以动态获得 Hello 类中的属性和方法、动态完成 Hello 类的对象实例化等操作,这个功能就称为反射。

说到这里,大家可能觉得,在编写代码时直接通过 new 的方式就可以实例化一个对象,访问其属性和方法,为什么偏偏要绕个弯子,通过反射机制来进行这些操作呢?下面我们就来看一下反射的使用场景。

2. 反射的使用场景

Java 的反射机制,主要用来编写一些通用性较高的代码或者编写框架的时候使用。

通过反射的概念,我们可以知道,在程序的运行状态中,对于任意一个类,通过反射都可以动态获取其信息以及动态调用对象。

例如,很多框架都可以通过配置文件,来让开发者指定使用不同的类,开发者只需要关心配置,不需要关心代码的具体实现,具体实现都在框架的内部,通过反射就可以动态生成类的对象,调用这个类下面的一些方法。

下面的内容,我们将学习反射的相关 API,在本小节的最后,我将分享一个自己实际开发中的反射案例。

3. 反射常用类概述

学习反射就需要了解反射相关的一些类,下面我们来看一下如下这几个类:

  • ClassClass 类的实例表示正在运行的 Java 应用程序中的类和接口;
  • Constructor:关于类的单个构造方法的信息以及对它的权限访问;
  • Field:Field 提供有关类或接口的单个字段的信息,以及对它的动态访问权限;
  • Method:Method 提供关于类或接口上单独某个方法的信息。

字节码文件想要运行都是要被虚拟机加载的,每加载一种类,Java 虚拟机都会为其创建一个 Class 类型的实例,并关联起来。

例如,我们自定义了一个 MybjStudent.java 类,类中包含有构造方法、成员属性、成员方法等:

public class MybjStudent {
    // 无参构造方法
    public MybjStudent() {
    }

    // 有参构造方法
    public MybjStudent(String nickname) {
        this.nickname = nickname;
    }

    // 昵称
    private String nickname;
    

    // 定义getter和setter方法
    public String getNickname() {
        return nickname;
    }

    public void setNickname(String nickname) {
        this.nickname = nickname;
    }
}

源码文件 MybjStudent.java 会被编译器编译成字节码文件 MybjStudent.class,当 Java 虚拟机加载这个 MybjStudent.class 的时候,就会创建一个 Class 类型的实例对象:

Class cls = new Class(MybjStudent);

JVM 为我们自动创建了这个类的对象实例,因此就可以获取类内部的构造方法、属性和方法等 MybjStudent 的构造方法就称为 Constructor,可以创建对象的实例,属性就称为 Field,可以为属性赋值,方法就称为 Method,可以执行方法。

4. Class 类

4.1 Class 类和 class 文件的关系

java.lang.Class 类用于表示一个类的字节码(.class)文件。

4.2 获取 Class 对象的方法

想要使用反射,就要获取某个 class 文件对应的 Class 对象,我们有 3 种方法:

  1. 类名.class:即通过一个 Class 的静态变量 class 获取,实例如下:
Class cls = MybjStudent.class;
  1. 对象.getClass ():前提是有该类的对象实例,该方法由 java.lang.Object 类提供,实例如下:
MybjStudent mybjStudent = new MybjStudent("小码");
Class mybjStudent.getClass();
  1. Class.forName (“包名。类名”):如果知道一个类的完整包名,可以通过 Class 类的静态方法 forName() 获得 Class 对象,实例如下:
class cls = Class.forName("java.util.ArrayList");

4.3 实例

package com.mybj.reflect;

public class MybjStudent {
    // 无参构造方法
    public MybjStudent() {
    }

    // 有参构造方法
    public MybjStudent(String nickname) {
        this.nickname = nickname;
    }

    // 昵称
    private String nickname;


    // 定义getter和setter方法
    public String getNickname() {
        return nickname;
    }

    public void setNickname(String nickname) {
        this.nickname = nickname;
    }

    public static void main(String[] args) throws ClassNotFoundException {
        // 方法1:类名.class
        Class cls1 = MybjStudent.class;

        // 方法2:对象.getClass()
        MybjStudent student = new MybjStudent();
        Class cls2 = student.getClass();

        // 方法3:Class.forName("包名.类名")
        Class cls3 = Class.forName("com.mybj.reflect.MybjStudent");
    }

}

代码中,我们在 com.mybj.reflect 包下定义了一个 MybjStudent 类,并在主方法中,使用了 3 种方法获取 Class 的实例对象,其 forName() 方法会抛出一个 ClassNotFoundException

4.4 调用构造方法

获取了 Class 的实例对象,我们就可以获取 Contructor 对象,调用其构造方法了。

那么如何获得 Constructor 对象?Class 提供了以下几个方法来获取:

  • Constructor getConstructor(Class...):获取某个 public 的构造方法;
  • Constructor getDeclaredConstructor(Class...):获取某个构造方法;
  • Constructor[] getConstructors():获取所有 public 的构造方法;
  • Constructor[] getDeclaredConstructors():获取所有构造方法。

通常我们调用类的构造方法,这样写的(以 StringBuilder 为例):

// 实例化StringBuilder对象
StringBuilder name = new StringBuilder("Hello Mybj");

通过反射,要先获取 Constructor 对象,再调用 Class.newInstance() 方法:

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class ReflectionDemo {
    public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException, InstantiationException {
        // 获取构造方法
        Constructor constructor = StringBuffer.class.getConstructor(String.class);
        // 调用构造方法
        Object str = constructor.newInstance("Hello Mybj");
        System.out.println(str);
    }
}

运行结果:

Hello Mybj

5. 访问字段

前面我们知道了如何获取 Class 实例,只要获取了 Class 实例,就可以获取它的所有信息。

5.1 获取字段

Field 类代表某个类中的一个成员变量,并提供动态的访问权限。Class 提供了以下几个方法来获取字段:

  • Field getField(name):根据属性名获取某个 public 的字段(包含父类继承);
  • Field getDeclaredField(name):根据属性名获取当前类的某个字段(不包含父类继承);
  • Field[] getFields():获得所有的 public 字段(包含父类继承);
  • Field[] getDeclaredFields():获取当前类的所有字段(不包含父类继承)。

获取字段的实例如下:

package com.mybj.reflect;

import java.lang.reflect.Field;

public class MybjStudent1 {

    // 昵称 私有字段
    private String nickname;

    // 余额 私有字段
    private float balance;

    // 职位 公有字段
    public String position;

    public static void main(String[] args) throws NoSuchFieldException {
        // 类名.class 方式获取 Class 实例
        Class cls1 = MybjStudent1.class;
        // 获取 public 的字段 position
        Field position = cls1.getField("position");
        System.out.println(position);

        // 获取字段 balance
        Field balance = cls1.getDeclaredField("balance");
        System.out.println(balance);

        // 获取所有字段
        Field[] declaredFields = cls1.getDeclaredFields();
        for (Field field: declaredFields) {
            System.out.print("name=" + field.getName());
            System.out.println("\ttype=" + field.getType());
        }
    }

}

运行结果:

public java.lang.String com.mybj.reflect.MybjStudent1.position
private float com.mybj.reflect.MybjStudent1.balance
name=nickname	type=class java.lang.String
name=balance	type=float
name=position	type=class java.lang.String

MybjStudent1 类中含有 3 个属性,其中 position 为公有属性,nickname 和 balance 为私有属性。我们通过类名.class 的方式获取了 Class 实例,通过调用其实例方法并打印其返回结果,验证了获取字段,获取单个字段方法,在没有找到该指定字段的情况下,会抛出一个 NoSuchFieldException

调用获取所有字段方法,返回的是一个 Field 类型的数组。可以调用 Field 类下的 getName() 方法来获取字段名称,getType() 方法来获取字段类型。

5.2 获取字段值

既然我们已经获取到了字段,那么就理所当然地可以获取字段的值。可以通过 Field 类下的 Object get(Object obj) 方法来获取指定字段的值,方法的参数 Object 为对象实例,实例如下:

package com.mybj.reflect;

import java.lang.reflect.Field;

public class MybjStudent2 {

    public MybjStudent2() {
    }

    public MybjStudent2(String nickname, String position) {
        this.nickname = nickname;
        this.position = position;
    }

    // 昵称 私有字段
    private String nickname;

    // 职位 公有属性
    public String position;

    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
        // 实例化一个 MybjStudent2 对象
        MybjStudent2 mybjStudent2 = new MybjStudent2("小码", "架构师");
        Class cls = mybjStudent2.getClass();
        Field position = cls.getField("position");
        Object o = position.get(mybjStudent2);
        System.out.println(o);
    }

}

运行结果:

架构师

MybjStudent2 内部分别包含一个公有属性 position 和一个私有属性 nickname,我们首先实例化了一个 MybjStudent2 对象,并且获取了与其对应的 Class 对象,然后调用 getField() 方法获取了 position 字段,通过调用 Field 类下的实例方法 Object get(Object obj) 来获取了 position 字段的值。

这里值得注意的是,如果我们想要获取 nickname 字段的值会稍有不同,因为它是私有属性,我们看到 get() 方法会抛出 IllegalAccessException 异常,如果直接调用 get() 方法获取私有属性,就会抛出此异常。

想要获取私有属性,必须调用 Field.setAccessible(boolean flag) 方法来设置该字段的访问权限为 true,表示可以访问。在 main() 方法中,获取私有属性 nickname 的值的实例如下:

public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
    // 实例化一个 MybjStudent2 对象
    MybjStudent2 mybjStudent2 = new MybjStudent2("小码", "架构师");
    Class cls = mybjStudent2.getClass();
    Field nickname = cls.getDeclaredField("nickname");
    // 设置可以访问
    nickname.setAccessible(true);
    Object o = nickname.get(mybjStudent2);
    System.out.println(o);
}

此时,就不会抛出异常,运行结果:

小码

5.2 为字段赋值

为字段赋值也很简单,调用 Field.set(Object obj, Object value) 方法即可,第一个 Object 参数是指定的实例,第二个 Object 参数是待修改的值。我们直接来看实例:

package com.mybj.reflect;

import java.lang.reflect.Field;

public class MybjStudent3 {

    public MybjStudent3() {
    }

    public MybjStudent3(String nickname) {
        this.nickname = nickname;
    }

    // 昵称 私有字段
    private String nickname;

    public String getNickname() {
        return nickname;
    }

    public void setNickname(String nickname) {
        this.nickname = nickname;
    }

    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
        // 实例化一个 MybjStudent3 对象
        MybjStudent3 mybjStudent3 = new MybjStudent3("小码");
        Class cls = mybjStudent3.getClass();
        Field nickname = cls.getDeclaredField("nickname");
        nickname.setAccessible(true);
        // 设置字段值
        nickname.set(mybjStudent3, "Colorful");
        // 打印设置后的内容
        System.out.println(mybjStudent3.getNickname());
    }

}

运行结果:

Colorful

6. 调用方法

Method 类代表某一个类中的一个成员方法。

6.1 获取方法

Class 提供了以下几个方法来获取方法:

  • Method getMethod(name, Class...):获取某个 public 的方法(包含父类继承);
  • Method getgetDeclaredMethod(name, Class...):获取当前类的某个方法(不包含父类);
  • Method[] getMethods():获取所有 public 的方法(包含父类继承);
  • Method[] getDeclareMethods():获取当前类的所有方法(不包含父类继承)。

获取方法和获取字段大同小异,只需调用以上 API 即可,这里不再赘述。

6.2 调用方法

获取方法的目的就是调用方法,调用方法也就是让方法执行。

通常情况下,我们是这样调用对象下的实例方法(以 String 类的 replace() 方法为例):

String name = new String("Colorful");
String result = name.replace("ful", "");

改写成通过反射方法调用:

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class ReflectionDemo1 {
    public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        // 实例化字符串对象
        String name = new String("Colorful");
        // 获取 method 对象
        Method method = String.class.getMethod("replace", CharSequence.class, CharSequence.class);
        // 调用 invoke() 执行方法
        String result = (String) method.invoke(name,  "ful", "");
        System.out.println(result);
    }
}

运行结果:

Color

代码中,调用 Method 实例的 invoke(Object obj, Object...args) 方法,就是通过反射来调用了该方法。

其中 invoke() 方法的第一个参数为对象实例,紧接着的可变参数就是要调用方法的参数,参数要保持一致。

7. 反射应用

Tips: 理解此部分内容可能需要阅读者有一定的开发经验

学习完了反射,大家可能依然非常疑惑,反射似乎离我们的实际开发非常遥远,实际情况也的确是这样的。因为我们在实际开发中基本不会用到反射。下面我来分享一个实际开发中应用反射的案例。

场景是这样的:有一个文件上传系统,文件上传系统有多种不同的方式(上传到服务器本地、上传到七牛云、阿里云 OSS 等),因此就有多个不同的文件上传实现类。系统希望通过配置文件来获取用户的配置,再去实例化对应的实现类。因此,我们一开始的思路可能是这样的(伪代码):

public class UploaderFactory {
    
    // 通过配置文件获取到的配置,可能为 local(上传到本地) qiniuyun(上传到七牛) 
    private String uploader;
    
    // 创建实现类对象的方法
    public Uploader createUploader() {
        switch (uploader) {
            case "local":
                // 实例化上传到本地的实现类
                return new LocalUploader();
            case "qiniuyun":
                // 实例化上传到七牛云的实现类
                return new QiniuUploader();
            default:
                break;
        }
        return null;
    }
}

createUploader() 就是创建实现类的方法,它通过 switch case 结构来判断从配置文件中获取的 uploader 变量。

这看上去似乎没有什么问题,但试想,后续我们的实现类越来越多,就需要一直向下添加 case 语句,并且要约定配置文件中的字符串要和 case 匹配才行。这样的代码既不稳定也不健壮。

换一种思路考虑问题,我们可以通过反射机制来改写这里的代码。首先,约定配置文件的 uploader 配置项不再是字符串,改为类的全路径命名。因此,在 createUploader() 方法中不再需要 switch case 结构来判断,直接通过 Class.forName(uploader) 就可以获取 Class 实例,并调用其构造方法实例化对应的文件上传对象,伪代码如下:

public class UploaderFactory {
    
    // 通过配置文件获取到的配置,实现类的包名.类名
    private String uploader;
    
    // 创建实现类对象的方法
    public Uploader createUploader() {
        // 获取构造方法
		Constructor constructor = Class.forName(uploader).getConstructor();
        return (Uploader) constructor.newInstance();
    }
}

通过反射实例化对应的实现类,我们不需要再维护 UploaderFactory 下的代码,其实现类的命名、放置位置也不受约束,只需要在配置文件中指定类名全路径即可。

8. 小结

通过本小节的学习,我们知道了反射是 Java 提供的一种机制,它可以在程序的运行状态中,动态获取类的信息,注入类内部的属性和方法,完成对象的实例化等操作。获取 Class 对象有 3 种方法,通过学习反射的相关接口,我们了解到通过反射可以实现一切我们想要的操作。在本小节的最后,我也分享了一个我在实际开发中应用反射的案例,希望能对大家有所启发。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/674974.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

天诚学校物联网锁、公租房智能门锁亮相2024永康门博会

5月26-28日,全场景AIoT解决方案服务商——江苏新巢天诚智能技术有限公司(以下简称“天诚”)盛装出席第14届中国(永康)国际门业博览会(以下简称“门博会”)。 门博会亮点十足 作为享誉海内外的…

产品评测:Coolmuster Android Eraser - 安全彻底删除Android数据的利器

产品概述 在数字化时代,智能手机成为了个人敏感信息的集中地。当涉及到数据隐私和安全时,简单的删除操作并不能满足我们对数据彻底清除的需求。Coolmuster Android Eraser正是为了解决这一问题而生,它是一款专为Android设备设计的第三方软件&…

VUE封装-自定义权限控制指令

在实际开发中,会遇到很多的权限控制、资源位的场景,其实就是用来控制某个组件的展示与否,可以是一个按钮、一个报表、一个TAB页面等 例如下图,我想通过当前登录的用户控制谷歌的这个logo显示与否 因为设计到的权限、资源位控制比…

图片中线段和圆圈检测(python opencv)

前言 本文实现将一个图片中的线段和圆圈检测出来,效果就像这样 开始之前请先自行安装 opencv 另外还用到了一个用来检测直线: http://olena.pages.lre.epita.fr/pylena/index.html pip install pylena直线检测 先用 opencv 来检测直线, 因为下面代码…

天融信 2023 的年终奖。。

天融信 过去几天,最大的瓜,是天融信 2023 的年终奖脚踝砍。 "天融信"是国内首家网络安全企业,同时也是一家上市公司。 就在前些天,有网友爆料出,天融信年终奖到账只有几百元,甚至只有几十元&…

铝包木门窗性能优异 国内产量及需求量总体呈增长态势

铝包木门窗性能优异 国内产量及需求量总体呈增长态势 铝包木门窗是在保留纯实木门窗特性和功能的前提下,将隔热断桥铝合金型材和实木通过机械方法复合而成的框体。铝包木门窗具有良好的密封性、保温性、抗腐蚀性、隔音性等,能够满足市场对门窗质量要求不…

【Linux-阻塞,非阻塞,异步】

Linux-阻塞,非阻塞,异步 ■ Linux-阻塞-非阻塞 IO-异步■ Linux-阻塞IO■ 阻塞IO简介■ open■ 等待队列■ 示例一:APP阻塞IO读取进入睡眠后-等待队列唤醒进程■■ ■ Linux-非阻塞IO■ 非阻塞IO简介■ open■ 轮询■ 1、select 函数■ 2、po…

【轻触按键】开关机电路--填坑1

接上文,挖的坑 一、翻转电路 二、真值表 按键按下去,1G17会拉低,A端脚会掉电,下降沿;终到逻辑“0” 松开按键,1G17会拉高,A端脚充电,上升沿;终到逻辑“1”; …

[补题记录]LeetCode 6.Z字形变换

传送门:Z字形变换 转自:Z字形变换 Thought/思路 关键点在于,最后的答案是一行行连接起来的。 这样我们就会发现,这个 Z 字,实际上会让行数 不断加 1,然后又 不断减 1。每次按顺序选择 S 中的一个字符即…

visual studio打包qt算子时,只生成dll没有生成lib等文件

问题:在visual studio配置了qt项目,并打包成dll,原则上会生成一堆文件,包括dll,lib等文件。 解决办法: 挨个右击源代码的所有头文件-》属性-》项类型。改成qt头文件形式,如下。

961题库 北航计算机 计算机网络 附答案 简答题形式

有题目和答案,没有解析,不懂的题问大模型即可,无偿分享。 第1组 习题 某网络拓扑如题下图所示,其中 R 为路由器,主机 H1~H4 的 IP 地址配置以及 R 的各接口 IP 地址配置如图中所示。现有若干以太网交换机…

C#中使用Mapster

Mapster是一个开源的.NET对象映射库,它提供了一种简单而强大的方式来处理对象之间的映射。 多个映射框架的性能对比: 第一步安装Mapster 使用方法 public class Test {public string name { get; set; }public string sex { get; set; }public string…

光伏并网逆变器UL 1741:2021标准解析

光伏并网逆变器UL 1741:2021标准解析 不同国家的安规认证可以说是光伏逆变器走向国际市场的一张通行证,由于全球各国家的电网制式及并网政策的不同差异,这对逆变器测试顺利的通过安规测试认证 还是有一定的技术难度,也是中国光伏制造企业迫切…

在Linux中tomcat占用内存过高可以通过导出hprof日志来解决

自动导出hprof日志 第一种方法: Tomcat的hprof日志是一种用于分析Java堆内存使用情况的工具,它可以帮助开发人员找到内存泄漏的原因。 hprof日志可以在特定的时间点对Java堆内存进行快照,并生成详细的分析报告。 启用hprof日志导出的具体…

零基础写框架:从零设计一个模块化和自动服务注册框架

模块化和自动服务注册 基于 ASP.NET Core 开发的 Web 框架中,最著名的是 ABP,ABP 主要特点之一开发不同项目(程序集)时,在每个项目中创建一个模块类,程序加载每个程序集中,扫描出所有的模块类,然后通过模块…

“去员工化”这个潮流谁也挡不住,六大诱因分析。

去员工化→恐怕是未来工作的主流,一方面有成本的原因,另一方面也有技术进步、雇佣形式创新等原因,这个潮流有利也有弊,关键看我们是如何兴利除弊。 "去员工化"是指企业在运营中减少或取消传统雇佣制度,更多…

HOW - vscode 使用指南

目录 一、基本介绍1. 安装 VS Code2. 界面介绍3. 扩展和插件4. 设置和自定义 二、常用界面功能和快捷操作(重点)常用界面功能快捷操作 三、资源和支持 Visual Studio Code(VS Code)是一款由微软开发的免费、开源的代码编辑器&…

vue开发网站-使用插件element、vant 遇到的问题

1. js把两个字符串放进一个另字符串里,用逗号分隔 let string1 "Hello"; let string2 "World"; let result ${string1},${string2}; console.log(result); // 输出: Hello,World2.js将字符串转为数组 const str "Hello, world!"…

HBuilder中怎样修改每次输出的内容hello?每次修改之后再次“运行到”的时候,怎样保证每次都重新编译?

要修改每次输出的内容,可以在代码中找到输出的地方,并将内容进行修改。例如,在JavaScript中,可以使用console.log()函数输出内容。在修改内容后,保存代码。 为了保证每次运行都重新编译代码,可以按照以下步…

LeetCode 算法:接雨水c++

原题链接🔗:接雨水 难度:困难⭐️⭐️⭐️ 题目 给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。 示例 1: 输入:height [0,1,0,2,1,0,1,3,2,…