Java反射

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

API使用

Java反射提供了以下核心类:

  • Class类:代表Java中的类或接口。通过Class类,我们可以获取类的构造函数、方法、字段等信息。

  • Constructor类:代表类的构造函数。通过Constructor类,我们可以创建对象。

  • Method类:代表类的方法。通过Method类,我们可以调用方法。

  • Field类:代表类的字段。通过Field类,我们可以访问和修改字段的值。

Class对象创建

获取Class的三种方式

public static void main(){
    //1.Class.forName
    try{
        Class clz=Class.ForName("java.lang.String");
    } catch (ClassNotFoundException e){
        e.printStackTrace();
    }
    
    //2.类实例.getclass()
    String s=new String("abc");
    Class clz=s.getClass();
    
    //3.类名.class
    Class clz=String.class;
}

demo演示

先创建一个简单的类Book,用于Java反射功能的演示

class Book{
    private String name;
    private int id;

    public Book() {
    }

    public Book(String name, int id) {
        this.name = name;
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    @Override
    public String toString() {
        return "[name:"+name+" id:"+id+"]";
    }

    public static void hi(String name){
        System.out.println("hi "+name);
    }
}

Constructor

为了能够动态获取对象构造方法的信息,首先需要通过下列方法之一创建一个 Constructor 类型的对象或者数组。

//返回一个由这个类所有public构造函数组成的构造函数对象数组
getConstructors()
////返回一个由参数指定的public构造函数对象
getConstructor(Class<?>…parameterTypes)
//返回一个由这个类所有构造函数组成的构造函数对象数组
getDeclaredConstructors()
///返回一个由参数指定的构造函数对象
getDeclaredConstructor(Class<?>...parameterTypes)

通过对比可以看出这一系列函数的特点:

  1. getXXXgetDeclaredXXXgetXXX获得的都是public修饰的对象,而getDelcaredXXX获得的是所有对象,无论是否含有public修饰

  2. 如果最后有s而且参数为空表示返回所有对象所构成的数组,而最后没有s且有参数表示返回由参数所指定的那个对象

Construor类常用方法

下面展示分别调用Book类的两个构造方法生成两个Book对象

    public static void ConstructorTest(){
        Class<Book> book_class=Book.class;
        try{
            Constructor<Book> c1=book_class.getConstructor(String.class,int.class);
            Book book1=c1.newInstance("java",1);

            Constructor<Book> c2=book_class.getConstructor();
            Book book2=c2.newInstance();

            System.out.println(book1);
            System.out.println(book2);
        }catch (NoSuchMethodException | InstantiationException | IllegalAccessException | InvocationTargetException e){
            e.printStackTrace();
        }
    }

Method

和获取构造方法相似,要动态获取一个对象方法的信息,首先需要通过下列方法之一创建一个 Method 类型的对象或者数组。

getMethod()
getMethods(String name,Class<?> …parameterTypes)
getDeclaredMethod()
getDeclaredMethods(String name,Class<?>...parameterTypes)

值得注意的是,带参数的getMethod/getDeclaredMethod还需要输入函数名

Method类常用方法

下面展示调用一个book对象的toString()方法,使用invoke()方法并且参数中必须有一个该类的对象

    public static void MethodTest() {
        Book book=new Book("java",1);
        Class book_class=book.getClass();
        try{
            Method m1=book_class.getMethod("toString");
            System.out.println(m1.invoke(book));

            Method m2=book_class.getMethod("hi",String.class);
            m2.invoke(book,"java");
        } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
            e.printStackTrace();
        }
    }

Field

通过下列任意一个方法访问成员变量时将返回 Field 类型的对象或数组。

getFields()
getField(String name)
getDeclaredFields()
getDeclaredField(String name)

Field类常用方法

这里强行访问了book对象private修饰的name和id

 public static void FieldTest(){
        Book book=new Book("java",1);
        Class book_class=book.getClass();
        try{
            Field f1=book_class.getDeclaredField("name");
            f1.setAccessible(true);
            System.out.println(f1.getName()+" "+f1.get(book));

            Field f2=book_class.getDeclaredField("id");
            f2.setAccessible(true);
            f2.set(book,2);
            System.out.println(f2.getName()+" "+f2.get(book));
        } catch (IllegalAccessException | NoSuchFieldException e) {
            e.printStackTrace();
        }
    }

反射应用

案例一 JDBC

  • JDBC就是使用Java语言操作关系型数据库的API。

  • 全称:Java DataBase Conectivity,Java数据库连接

  • 正常情况下,一套Java代码不能操作不同的关系型数据库,因为每种数据库底层都不同。JDBC就是为了解决这一问题而出现,一套JDBC代码可以操作MySQL、Oracle、IBM等不同的数据库。

  • JDBC只是定义了一套接口(也就是规则),具体的实现类由不同的数据库厂商自己定义,厂商们将实现类称为驱动。

public class JDBC {
    public static void main(String[] args) throws Exception {
        // 1. 注册驱动,此步骤在MySQL5.0版本后可省,因为在lib/META-INF/services/java.sql.Driver文件里记录了驱动的名称,会自动加载。
        Class.forName("com.mysql.Driver");

        // 2.获取连接
        String url = "jdbc:mysql://ip地址(域名)/端口号/数据库名称[?参数键值对1&参数键值对2]";
        // String url = "jdbc:mysql://ip地址(域名)/端口号/数据库名称?useSSl=false"解决控制栏警告提示
        String username = "数据库账户名";
        String password = "数据库密码";
        Connection connection = DriverManager.getConnection(url,username,password);

		// 此处可开启事务

        // 3.定义sql
        String sql = "update account set money = 20000 where id = 1";

        // 4.获取执行sql的对象 statement
        Statement statement = connection.createStatement();

        // 5.执行sql
        int count = statement.executeUpdate(sql);

        // 6.处理结果
        System.out.println(count);

		// 此处可提交事务

        // 7.释放资源
        statement.close();
        connection.close();
    }
}

开发中获得连接的4个参数(驱动、URL、用户名、密码)通常都存在配置文件中,方便后期维护,程序如果需要更换数据库,只需要修改配置文件即可,而不用修改程序本身。

Java配置文件常为.properties文件,格式为文本文件,文件的内容的格式是“键=值”的格式。

#例如
driverClass=com.mysql.jdbc.Driver
url=jdbc:mysql://localhost:3306/mydatabase
username=root
password=7894561230
public class DBMySQL {
    private static Properties properties = null;
    private Connection connection = null;

    static{ //静态初始化器
        properties = new Properties();
        ClassLoader classLoader = DBMySQL.class.getClassLoader();
        InputStream is = classLoader.getResourceAsStream("jdbc.properties");

        try {
            properties.load(is);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    //1.定义方法:获取数据库的连接对象
    public Connection getMysqlConnection(){
        String driver = properties.getProperty("mysql.driver").trim();
        String url = properties.getProperty("mysql.url").trim();
        String user = properties.getProperty("mysql.user").trim();
        String password = properties.getProperty("mysql.password").trim();

        //1.加载驱动
        try {
            Class.forName(driver);
            //2.连接数据库,获取连接对象
            if(connection == null){
                connection = DriverManager.getConnection(url,user,password);
            }
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (SQLException e) {
            e.printStackTrace();
        }

        return connection;
    }

    //2.定义方法:用于关闭数据库的连接对象
    public void releaseConnection(){
        if(connection != null){
            try {
                connection.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    }
}

案例二 修改配置文件来创建不同类

假设我们有如下需求:在不改变类的代码的前提下,我们能够创建任意类的对象,并执行其中的方法。

假设我们有两个类,一个 Student,一个 Teacher,两者的定义如下;

package com.cunyu;

class Teacher {
    private String name;
    private int age;

    public void teach() {
        System.out.println("教书育人……");
    }
}

class Student {
    private String name;
    private float score;

    public void study() {
        System.out.println("好好学习,天天向上……");
    }
}

此时,我们可以通过 配置文件 + 反射 的方式来实现这一效果,而这也就是我们现在所用框架中的基础,当我们使用反射后,只需要通过修改配置文件中的内容就能够不用去改代码就实现对应的功能。

将要创建对象的全类名和要执行的方法都配置在配置文件中

className=com.cunyu.Student
methodName=study

然后在主方法中加载读取配置文件

import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Properties;

public class ReflectTest {
    public static void main(String[] args) throws IOException, ClassNotFoundException, IllegalAccessException, InstantiationException, InvocationTargetException, NoSuchMethodException {
        //创建配置文件对象
        Properties properties = new Properties();
        //加载配置文件
        ClassLoader classLoader = ReflectTest.class.getClassLoader();
        InputStream inputStream = classLoader.getResourceAsStream("prop.properties");
        properties.load(inputStream);
        //获取配置文件中定义的数据
        String className = properties.getProperty("className");
        String methodName = properties.getProperty("methodName");

        //加载进内存
        Class name = Class.forName(className);

        //创建实例
        Object object = name.newInstance();

        //获取并执行方法
        Method method = name.getMethod(methodName);
        method.invoke(object);
    }
}

反射的意义及思考

优点:

  • 在程序运行过程中可以操作类对象,增加了程序的灵活性

  • 解耦,从而提高程序的可扩展性,提高代码的复用率,方便外部调用

缺点:

  • 性能问题:Java 反射中包含了一些动态类型,JVM 无法对这些动态代码进行优化,因此通过反射来操作的方式要比正常操作效率更低

  • 安全问题:使用反射时要求程序必须在一个没有安全限制的环境中运行,如果程序有安全限制,就不能使用反射


本文章使用limfx的vscode插件快速发布