10.3 Java反射Reflect



日常开发用的不多,但是反射Reflect是后期学习各种框架的基础技能。

反射Reflect非高深难懂,是底层的技术。

一、课程内容

(1)反射及其作用

(2)反射的四个核心类

(3)反射在项目中的应用

二、反射及其作用

2.1 概述

1.对比

反射是运行时动态决定访问类与对象的技术。

是JDK1.2后提供的特性,隶属于java.lang.reflect

创建对象的以前反射
方式要实例化一个对象,要new+指定的类。
这种使用new关键字的方式,是把实例化对象的工作,写死在程序中
将一个有效的类名保存为一个字符串,通过用户输入来灵活创建哪个类
时机编译阶段运行阶段
特点写死了,不灵活更灵活、易管理

2.重要性:

大部分Java框架都是基于反射来实现参数配置、动态注入等特性。

程序启动时,会动态的读取配置文件信息,同时利用反射技术,在运行时去创建不同的对象。

2.2 编写第一段反射的程序

在接口MathOperation.java中:

/**
 * 四则运算接口
 */
public interface MathOperation {
    public float operate(int a , int b);
}

以下是三个类,分别实现了上述的接口:

/**
 * 加法
 */
public class Addition implements MathOperation {
    @Override
    public float operate(int a , int b) {
        System.out.println("执行加法运算");
        return a + b;
    }
}
/**
 * 减法运算
 */
public class Subtraction implements MathOperation {
    @Override
    public float operate(int a, int b) {
        System.out.println("执行减法运算");
        return a - b;
    }
}

在入口类ReflectSample.java中case1方法::

/**
 * 初识反射的作用
 */
public class ReflectSample {
    /**
     * 传统的创建对象方式
     */
    public static void case1(){
        Scanner scanner = new Scanner(System.in);
        System.out.print("请输入计算类名:");
        String op = scanner.next();
        System.out.print("请输入a:");
        int a = scanner.nextInt();
        System.out.print("请输入b:");
        int b = scanner.nextInt();
        MathOperation mathOperation = null;
        if(op.equals("Addition")){                    // 加法
            mathOperation = new Addition();
        }else if(op.equals("Subtraction")) {         // 减法
            mathOperation = new Subtraction();
        }else{
            System.out.println("无效的计算类");
            return;
        }
        float result = mathOperation.operate(a, b);
        System.out.println(result);
    }

   
    public static void main(String[] args) {
        ReflectSample.case1();
    }
}

--->
请输入计算类名:Addition
请输入a:5
请输入b:3
执行加法运算
8.0
    
请输入计算类名:Subtraction
请输入a:5
请输入b:3
执行减法运算
2.0

在入口类ReflectSample.java中case2方法:

/**
 * 初识反射的作用
 */
public class ReflectSample {

    /**
     * 利用反射创建对象更加灵活
     */
    public static void case2(){
        Scanner scanner = new Scanner(System.in);
        System.out.print("请输入计算类名:");
        String op = scanner.next();
        System.out.print("请输入a:");
        int a = scanner.nextInt();
        System.out.print("请输入b:");
        int b = scanner.nextInt();
        MathOperation mathOperation = null;

        try {       // Class.forName()用于加载找到指定的类      // 类完整路径:包名+上面用户输入的类名      // 对该类进行实例化(同new)
            mathOperation = (MathOperation) Class.forName("com.imooc.reflect." + op).newInstance(); //将Object对象,转换为接口
        }catch(Exception e){
            System.out.println("无效的计算类");
            return;     // 中断程序
        }
        float result = mathOperation.operate(a, b);
        System.out.println(result);
    }


    public static void main(String[] args) {
        ReflectSample.case2();
    }
}

--->
请输入计算类名:Addition
请输入a:5
请输入b:3
执行加法运算
8.0
    
请输入计算类名:Subtraction
请输入a:5
请输入b:3
执行减法运算
2.0

以上的结果,跟传统方式一样。这说明,我们的case2的定义和调用是正确的。

目前,暂时还看不出case2的优势在哪里。

2.3 新增需求时,发挥反射的优势

针对当前代码,如果想增加乘法运算,怎么办?

1.传统的方式:

  • 既要新增Multiplication.java
/**
 * 乘法运算
 */
public class Multiplication implements MathOperation {
    
    @Override
    public float operate(int a, int b) {
        return a * b;
    }
}
  • 又要在入口类ReflectSample.java的源代码中:增加以下if-else代码:

在真正的企业应用中,一旦代码重新编写,就必须重新测试、重新打包上线,这个过程非常麻烦。

那么,如何在不修改源代码的情况,让程序支持新的功能呢?

2.利用反射技术,在运行时来决定去创建哪些对象

如果项目需求有新增,要有乘法、除法等运算,是不是必须要在上述源代码中增加if-else呢?

不是。应该利用反射技术,在运行时动态创建对应的对象。

本质:因为下面要实例化的类,即op的值,是用户输入的,运行时才能知道。

  • 只需要新增Multiplication.java
public class Multiplication implements MathOperation {
    
    @Override
    public float operate(int a, int b) {
        return a * b;
    }
}
  • 在入口类ReflectSample.java中case2方法:

​ 不需要做任何改变

/**
 * 初识反射的作用
 */
public class ReflectSample {

    /**
     * 利用反射创建对象更加灵活
     */
    public static void case2(){
        Scanner scanner = new Scanner(System.in);
        System.out.print("请输入计算类名:");
        String op = scanner.next();
        System.out.print("请输入a:");
        int a = scanner.nextInt();
        System.out.print("请输入b:");
        int b = scanner.nextInt();
        MathOperation mathOperation = null;

        try {       
            mathOperation = (MathOperation) Class.forName("com.imooc.reflect." + op).newInstance(); 
        }catch(Exception e){
            System.out.println("无效的计算类");
            return;    
        }
        float result = mathOperation.operate(a, b);
        System.out.println(result);
    }


    public static void main(String[] args) {
        ReflectSample.case2();
    }
}

--->
请输入计算类名:Multiplication
请输入a:5
请输入b:3
15.0

三、反射的核心类

根据类的完整结构,可以有以下分类:

3.1 分类

(1)Class类

(2)Constructor构造方法类

(3)Method方法类

(4)Field成员变量类

四、Class类及对象实例化

4.1 概述

(1)Class类是JVM中代表“类和接口”的指代

(2)Class类对象是包含了某个特定类的结构信息。

通过Class类对象,可以获得对应类的构造方法/方法/成员变量

4.2 Class类的常用方法

即最常用的api

其中:

  • 之前JDBC用过class.forName()方法,用于加载驱动类,就已用过反射的技术

4.3 语法

4.4 代码演示

主要是演示:

  • Class.forName( )方法
  • classObj.newInstance( )方法

先创建工程:reflect

在员工实体类Employee.java中:

/**
 * 员工实体类
 */
public class Employee {

    // 通过静态块,来检验是是否已加载和初始化
    static {
        System.out.println("Employee类已被加载到jvm,并已初始化");
    }

    private Integer eno;
    private String ename;
    private Float salary;
    private String dname;

    // 为让演示过程更加清晰
    public Employee() {
        System.out.println("Employee默认构造方法已被执行");
    }

    public Integer getEno() {
        return eno;
    }

    public void setEno(Integer eno) {
        this.eno = eno;
    }

    public String getEname() {
        return ename;
    }

    public void setEname(String ename) {
        this.ename = ename;
    }

    public Float getSalary() {
        return salary;
    }

    public void setSalary(Float salary) {
        this.salary = salary;
    }

    public String getDname() {
        return dname;
    }

    public void setDname(String dname) {
        this.dname = dname;
    }
}

在入口类ClassSample.java中:

import com.imooc.reflect.entity.Employee;        // 调用上面刚写好的员工实体类

public class ClassSample {
    public static void main(String[] args) {

        try {   // 1.Class.forName()方法将指定的类加载到jvm,并返回对应的Class类
            Class employeeClass = Class.forName("com.imooc.reflect.entity.Employee");     //forName是Class类的静态方法,所以可以这样调用
            System.out.println("Employee已被加载到jvm"); // 是否真的加载了这个类?
                // 2.newInstance()通过默认的构造方法,来创建新的对象
            Employee emp = (Employee) employeeClass.newInstance();  // 返回的是Object,是远古父类,因此实际使用时要强制类型转换
            System.out.println(emp);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        }
    }
}

--->
Employee类已被加载到jvm,并已初始化
Employee已被加载到jvm
Employee默认构造方法已被执行
com.imooc.reflect.entity.Employee@1b6d3586

其中:

  • 是否真的加载了这个类?

​ 作为一个类,当被加载完以后,要经过初始化的工作。类的初始化,是通过静态块完成的。

4.5 关于三个异常

1.抛出“类无法找到”的异常:当类名与类路径书写错误时

2.抛出“非法访问异常”:当在作用域外访问对象方法时或成员变量时

3.抛出“实例化异常”:当对象无法被实例化时

上述的案例中,实体类中只有一个默认的构造方法。

但是,如果还有其他的带参构造方法,此时如何创建新的对象?

五、Constructor构造方法类

5.1 概述

(1)Constructor构造方法类是Java类中构造方法的指代、抽象。

(2)Constructor类的对象,是某个具体类的某个构造方法的声明。

可以通过上述对象,调用带参的构造方法,来创建对象

5.2 常用方法

5.3 语法

5.4 代码演示

在员工实体类Employee.java中:

/**
 * 员工实体类
 */
public class Employee {       

    static {
        System.out.println("Employee类已被加载到jvm,并已初始化");
    }

    private Integer eno;
    private String ename;
    private Float salary;
    private String dname;

    public Employee() {
        System.out.println("Employee默认构造方法已被执行");
    }

    // 带参构造方法:为简化属性赋值的过程
    public Employee(Integer eno, String ename, Float salary, String dname) {
        this.eno = eno;
        this.ename = ename;
        this.salary = salary;
        this.dname = dname;
        System.out.println("Employee带参构造方法已被执行");
    }

    public Integer getEno() {
        return eno;
    }

    public void setEno(Integer eno) {
        this.eno = eno;
    }

    public String getEname() {
        return ename;
    }

    public void setEname(String ename) {
        this.ename = ename;
    }

    public Float getSalary() {
        return salary;
    }

    public void setSalary(Float salary) {
        this.salary = salary;
    }

    public String getDname() {
        return dname;
    }

    public void setDname(String dname) {
        this.dname = dname;
    }

    @Override
    public String toString() {                  // 为了演示的方便,重写toString方法
        return "Employee{" +
                "eno=" + eno +
                ", ename='" + ename + '\'' +
                ", salary=" + salary +
                ", dname='" + dname + '\'' +
                '}';
    }
}

在入口类ConstructorSample.java中:

/**
 * 利用带参构造方法,创建对象
 */
public class ConstructorSample {
    public static void main(String[] args) {

        try {   // 1.所有反射在操作之前,都需要获得对应的类
            Class employeeClass=Class.forName("com.imooc.reflect.entity.Employee");
                // 2.获取指定格式的带参构造方法      // 通过一个数组,来描述构造方法中参数的数量、类型,以作为构造方法的标识
            Constructor constructor=employeeClass.getConstructor(new Class[]{
                    Integer.class,String.class,Float.class,String.class
            }); // 3.创建对象
            Employee employee = (Employee) constructor.newInstance(new Object[]{
                    100, "李磊", 3000f, "研发部"
            });
            System.out.println(employee);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            // 没有找到与之对应格式的方法
            e.printStackTrace();
        } catch (IllegalAccessException e) {    // 无法访问异常:当构造方法是private时
            e.printStackTrace();
        } catch (InstantiationException e) {    // 实例化异常:当实体类是抽象类时
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            // “目标调用异常”:当被调用的方法内部抛出了异常,但是没有被捕获时(较少)
            e.printStackTrace();
        }
    }
}

--->
Employee类已被加载到jvm,并已初始化        
Employee带参构造方法已被执行
Employee{eno=100, ename='李磊', salary=3000.0, dname='研发部'}

这说明,通过带参构造方法,成功创建了Emplyee这个对象。

5.5 关于异常

没有找到与之对应格式的方法的异常:

六、Method方法类

6.1 概述

(1)Method方法类指代某个类中的方法

(2)Method方法类的对象,使用classObj.getMethod()方法获取

(3)用途:通过Method方法类的对象,调用指定对象的对应方法

6.2 常用方法

invoke:

use,put into use

6.3 语法

其中:

  • 上面的是形参列表
  • 下面的是实参列表

6.4 代码演示

在员工实体类Employee.java中:

/**
 * 员工实体类
 */
public class Employee {       

    static {
        System.out.println("Employee类已被加载到jvm,并已初始化");
    }

    private Integer eno;
    private String ename;
    private Float salary;
    private String dname;
  
    public Employee() {
        System.out.println("Employee默认构造方法已被执行");
    }

    public Employee(Integer eno, String ename, Float salary, String dname) {
        this.eno = eno;
        this.ename = ename;
        this.salary = salary;
        this.dname = dname;
        System.out.println("Employee带参构造方法已被执行");
    }

    public Integer getEno() {
        return eno;
    }

    public void setEno(Integer eno) {
        this.eno = eno;
    }

    public String getEname() {
        return ename;
    }

    public void setEname(String ename) {
        this.ename = ename;
    }

    public Float getSalary() {
        return salary;
    }

    public void setSalary(Float salary) {
        this.salary = salary;
    }

    public String getDname() {
        return dname;
    }

    public void setDname(String dname) {
        this.dname = dname;
    }

    @Override
    public String toString() {               
        return "Employee{" +
                "eno=" + eno +
                ", ename='" + ename + '\'' +
                ", salary=" + salary +
                ", dname='" + dname + '\'' +
                '}';
    }

    // 为员工调薪的方法
    public Employee updateSalary(Float val) {
        this.salary = this.salary + val;
        System.out.println(this.ename+"调薪至"+this.salary+"元");
        return this;                                    // 返回的是当前的对象。如果没有,就会报错
    }
}

在入口类MethodSample.java中:

/**
 * 利用Method方法类进行调用
 */
public class MethodSample {
    public static void main(String[] args) {
        try {       // 1.加载指定的类
            Class employeeClass = Class.forName("com.imooc.reflect.entity.Employee");
                    // 2.获取指定格式的构造方法
            Constructor constructor = employeeClass.getConstructor(new Class[]{
                    Integer.class, String.class, Float.class, String.class
            });     // 3.创建对象
            Employee employee = (Employee) constructor.newInstance(new Object[]{
                    100, "李磊", 3000f, "研发部"
            });

            // 4. 获取方法
            Method updateSalaryMethod = employeeClass.getMethod("updateSalary", new Class[]{
                    Float.class
            });
            // 5.执行上述方法
            Employee employee1 = (Employee) updateSalaryMethod.invoke(employee, new Object[]{       // 参数是一个对象数组
                    1000f
            });
            System.out.println(employee1);      // 会自动调用toString方法,进行输出
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
    }
}

--->
Employee类已被加载到jvm,并已初始化
Employee带参构造方法已被执行
李磊调薪至4000.0元
Employee{eno=100, ename='李磊', salary=4000.0, dname='研发部'}

# 七、Field成员变量类

7.1 概述

(1)Field成员变量类,指代某个类中的成员变量

(2)Field成员变量类的对象,通过classObj.getField()方法获取

(3)用途:通过上述对象,可以为成员变量赋值、取值

7.2 常用方法

7.3 语法

7.4 代码演示:获取成员变量值

在计算机术语中,属性也叫成员变量、字段,三者都是一个东西。

在员工实体类Employee.java中:

在入口类FieldSample.java中:

public class FieldSample {

    public static void main(String[] args) {
        try {
            // 1.加载指定的类
            Class employeeClass = Class.forName("com.imooc.reflect.entity.Employee");
            // 2.找到指定的构造方法
            Constructor constructor = employeeClass.getConstructor(new Class[]{
                    Integer.class, String.class, Float.class, String.class
            });
            // 3. 实例化一个对象
            Employee employee = (Employee)constructor.newInstance(new Object[]{
                    100, "李磊", 3000f, "研发部"
            });
            // 4.通过反射,得到员工的姓名(属性的值)
            Field enameField = employeeClass.getField("ename");    // 先得到属性名
            String ename = (String) enameField.get(employee);    // 再得到属性值:直接get()方法
            System.out.println("ename:" + ename);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        } catch (NoSuchFieldException e) {
            // 没有找到对应成员变量时抛出的异常
            e.printStackTrace();
        }
    }
}

--->
Employee类已被加载到jvm,并已初始化
Employee带参构造方法已被执行
ename:李磊        // 成功通过反射,得到了员工的姓名

7.5 关于异常

没有找到对应成员变量时抛出的异常:

7.6 代码演示:更改成员变量值

结果为:

Employee类已被加载到jvm,并已初始化
Employee带参构造方法已被执行
ename:李雷                // 属性值已经被更改

7.7 异常

上面的get开头的获取的方法,比如,getMethod、getField()都是针对的是public修饰的公开变量。

如果是私有属性,就会报错:

但是,在实际工作中,难免会有获取私有属性的场景。那么,该怎么做呢?

见下节。

八、getDeclared系列方法说明

8.1 概述

(1)getConstructor(s)|Method(s)|Field(s)只能获取public对象

(2)getDeclaredConstructor(s)|Method(s)|Field(s)可以获取任意对象

(3)访问非作用域内的构造方法、方法、成员变量,会抛出异常

8.2 案例应用

要求:希望运行时,将对象所有的成员变量都打印输出?

分析:如果是public属性,直接获取;如果是private等非public的属性,那就需要调用getter方法来提取。

public class getDeclaredSample {

    public static void main(String[] args) {
        try {
            // 1.加载指定的类
            Class employeeClass = Class.forName("com.imooc.reflect.entity.Employee");
            // 2.找到指定的构造方法
            Constructor constructor = employeeClass.getConstructor(new Class[]{
                    Integer.class, String.class, Float.class, String.class
            });
            // 3. 实例化一个对象
            Employee employee = (Employee)constructor.newInstance(new Object[]{
                    100, "李磊", 3000f, "研发部"
            });

            // 4.获取当前类所有的成员变量,并放到一个数组中
            
            Field[] fields = employeeClass.getDeclaredFields();        // getDeclaredFields()方法
            
            for (Field field : fields) {
                
                // 5.如何区分public和private
                if (field.getModifiers() == 1) {                // public修饰
                    Object val = field.get(employee);    // 此处的val其实是String类型,调用的是String类改写的toString方法
                    System.out.println(field.getName() + ":" + val);
                }else if(field.getModifiers()==2){               // private修饰
                    // 先组织得到方法名
                    String methodName="get"+field.getName().substring(0, 1).toUpperCase()+
                            field.getName().substring(1);
                    // 得到方法的对象
                    Method getmethod = employeeClass.getMethod(methodName);
                    Object ret=getmethod.invoke(employee);      // 此处不能类型转换,因为属性类型有很多
                    System.out.println(field.getName()+":"+ret);
                }
            }
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
    }
}

--->

开发场景中,你的同事传过来一个对象,但是你对该对象的结构不了解。此时,你就可以使用上述代码对所有的数据进行打印。

九、反射的项目应用案例

9.1 编码

通过反射,加载配置文件,以输出不同版本的名人名言

在接口I18N.java中:

public interface I18N {
    public String say();
}

Zhcn.java中:

// 简体中文
public class Zhcn implements I18N {            // 实现上面的接口

    @Override
    public String say() {
        return "生命不息,奋斗不止";
    }
}

在配置文件config.properties中:

language=com.imooc.i18n.Zhcn        // 将上面的中文类放进来

在入口类Application.java中:

public class Application {
    // 通过反射,加载配置文件,以输出不同版本的名人名言
    public static void say(){
        Properties properties = new Properties();
        String configPath = Application.class.getResource("/config.properties").getPath();
        try {
            configPath = new URLDecoder().decode(configPath, "UTF-8");
            properties.load(new FileInputStream(configPath));
            String language = properties.getProperty("language");
                        // 这里的language,等同于"com.imooc.i18n.Zhcn".开始使用反射技术
            I18N i18N = (I18N)Class.forName(language).newInstance();
            System.out.println(i18N.say());

        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        Application.say();
    }
}

--->
生命不息,奋斗不止

上面演示的是通过反射技术,实现的网站的中文版本。

如果想增加英文版本,就很简单:不需要修改源代码,只需要增加新的国家语言,并将其放置在配置文件中即可。

即:

En.java中:

public class En implements I18N {

    @Override
    public String say() {
        return "Cease to the struggle and cease to the life";
    }
}

在配置文件config.properties中:

language=com.imooc.i18n.En

结果为:

--->
Cease to the struggle and cease to the life

最终实现了运行时,来决定去实例化哪个类

声明:Jerry's Blog|版权所有,违者必究|如未注明,均为原创|本网站采用BY-NC-SA协议进行授权

转载:转载请注明原文链接 - 10.3 Java反射Reflect


Follow excellence, and success will chase you.