反射

本文深入探讨了Java反射机制,包括其在运行时获取类信息、实例化对象、调用方法等能力。通过示例展示了如何通过反射访问类的属性和方法,以及在Spring IOC容器、工厂模式和JDBC中的应用。同时,讨论了反射带来的灵活性、封装性破坏和性能损耗等特性,帮助开发者更好地理解和运用反射。
摘要由CSDN通过智能技术生成

反射

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

Java反射机制提供的功能

  • 在运行时判断任意一个对象所属的类
  • 在运行是构造任意一个类的对象
  • 在运行是判断任意一个类所有的成员变量和方法
  • 在运行是调用任意一个对象的方法

实现一个反射类

定义一个人类

public class Person {

    public int age;
    public String name;

    public Person(){
        super();
    }

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

    public String showInfo(){
        return "name="+name+" "+ "age="+age;
    }
}

定义一个学生类并继承Person类

public class Student extends Person implements Study{
    public String className;
    private String address;

    public Student(){
        super();
    }

    public Student(String className) {
        this.className = className;
    }

    public Student(int age, String name, String className, String address) {
        super(age, name);
        this.className = className;
        this.address = address;
    }

    public String getAddress() {
        return address;
    }

    public void setAddress(String address) {
        this.address = address;
    }

    public String toString(){
        return "年龄:"+age+"姓名:"+name+"班级:"+className+"住址:"+address;
    }
}

通过反射拿到学生这个类里的所有方法属性

public class TestReflect {
    public static void main(String[] args) {
        Class student = null;
        try {
            student = Class.forName("com.lanou.reflect.Student");
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }

        System.out.println("1--------------------------------");

        //获取对象的所有公有属性
        Field[] fields = student.getFields();
        for (Field field : fields) {
            System.out.println(field);
        }

        System.out.println("2--------------------------------");

        //获取对象所有属性,但不包含继承的属性
        Field[] declaredFields = student.getDeclaredFields();
        for (Field df : declaredFields) {
            System.out.println(df);
        }
        System.out.println("3--------------------------------");

        //获取对象中所有的公共方法
        Method[] methods = student.getMethods();
        for (Method method : methods) {
            System.out.println(method);
        }
        System.out.println("4--------------------------------");

        //获取对象中所有方法,不包含继承的方法
        Method[] declaredMethods = student.getDeclaredMethods();
        for (Method dm : declaredMethods) {
            System.out.println(dm);
        }
        System.out.println("5--------------------------------");

        //获取对象所有的公共构造方法
        Constructor[] constructors = student.getConstructors();
        for (Constructor constructor : constructors) {
            System.out.println(constructor);
        }
        System.out.println("6--------------------------------");
        //获取对象所有构造方法
        Constructor[] declaredConstructors = student.getDeclaredConstructors();
        for (Constructor dc : declaredConstructors) {
            System.out.println(dc);
        }

        System.out.println("***********************************");

        try {
            Class s = Class.forName("com.lanou.reflect.Student");
            Student stu1 = (Student) s.newInstance();
            //第一种方法,实例化默认构造方法,调用set赋值
            stu1.setAddress("北京");
            System.out.println(stu1);
            System.out.println("-----------------------------");
            //第二种方法,取得全部的构造函数,使用构造函数赋值
            Constructor<Student> constructor = s.getConstructor(int.class,
                    String.class,String.class,String.class);
            Student stu2 = constructor.newInstance(23,"jll","二班","北京");
            System.out.println(stu2);
            Method show = s.getMethod("showInfo");
            Object o = show.invoke(stu2);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
    }
}

运行结果

1--------------------------------
public java.lang.String com.lanou.reflect.Student.className
public int com.lanou.reflect.Person.age
public java.lang.String com.lanou.reflect.Person.name
2--------------------------------
public java.lang.String com.lanou.reflect.Student.className
private java.lang.String com.lanou.reflect.Student.address
3--------------------------------
public java.lang.String com.lanou.reflect.Student.toString()
public java.lang.String com.lanou.reflect.Student.getAddress()
public void com.lanou.reflect.Student.setAddress(java.lang.String)
public java.lang.String com.lanou.reflect.Person.showInfo()
public final void java.lang.Object.wait() throws java.lang.InterruptedException
public final void java.lang.Object.wait(long,int) throws java.lang.InterruptedException
public final native void java.lang.Object.wait(long) throws java.lang.InterruptedException
public boolean java.lang.Object.equals(java.lang.Object)
public native int java.lang.Object.hashCode()
public final native java.lang.Class java.lang.Object.getClass()
public final native void java.lang.Object.notify()
public final native void java.lang.Object.notifyAll()
4--------------------------------
public java.lang.String com.lanou.reflect.Student.toString()
public java.lang.String com.lanou.reflect.Student.getAddress()
public void com.lanou.reflect.Student.setAddress(java.lang.String)
5--------------------------------
public com.lanou.reflect.Student(int,java.lang.String,java.lang.String,java.lang.String)
public com.lanou.reflect.Student(java.lang.String)
public com.lanou.reflect.Student()
6--------------------------------
public com.lanou.reflect.Student(int,java.lang.String,java.lang.String,java.lang.String)
public com.lanou.reflect.Student(java.lang.String)
public com.lanou.reflect.Student()
***********************************
年龄:0姓名:null班级:null住址:北京
-----------------------------
年龄:23姓名:jll班级:二班住址:北京

Process finished with exit code 0

与Java有关的类主要有

Class类

在Java中,每定义一个java class实体都会产生一个Class对象。也就是说,当我们编写一个类,编译完成后,在生成的 .class文件中,就会产生一个 Class 对象,这个 Class 对象用于表示这个类的类型信息。Class 中没有公共的构造器,也就是说 Class 对象不能被实例化。下面来简单看一下 Class 类都包括了哪些方法。

toString()

public String toString() {
  return (isInterface() ? "interface " : (isPrimitive() ? "" : "class "))
    + getName();
}

toString() 方法能够将对象转换为字符串,toString() 首先会判断 Class 类型是否是接口类型,也就是说,普通类和接口都能够用 Class 对象来表示,然后再判断是否是基本数据类型,这里判断的都是基本数据类型和包装类,还有 void类型。

所有的类型如下

  • java.lang.Boolean : 代表 boolean 数据类型的包装类
  • java.lang.Character: 代表 char 数据类型的包装类
  • java.lang.Byte: 代表 byte 数据类型的包装类
  • java.lang.Short: 代表 short 数据类型的包装类
  • java.lang.Integer: 代表 int 数据类型的包装类
  • java.lang.Long: 代表 long 数据类型的包装类
  • java.lang.Float: 代表 float 数据类型的包装类
  • java.lang.Double: 代表 double 数据类型的包装类
  • java.lang.Void: 代表 void 数据类型的包装类

然后是 getName() 方法,这个方法返回类的全限定名称。

  • 如果是引用类型,比如 String.class.getName() -> java.lang.String
  • 如果是基本数据类型,byte.class.getName() -> byte
  • 如果是数组类型,new Object[3]).getClass().getName() -> [Ljava.lang.Object

toGenericString()

这个方法会返回类的全限定名称,而且包括类的修饰符和类型参数信息。

forName()

根据类名获得一个 Class 对象的引用,这个方法会使类对象进行初始化。

例如 Class t = Class.forName("java.lang.Thread") 就能够初始化一个 Thread 线程对象

在 Java 中,一共有三种获取类实例的方式

  • Class.forName(java.lang.Thread)
  • Thread.class
  • thread.getClass()

newInstance()

创建一个类的实例,代表着这个类的对象。上面 forName() 方法对类进行初始化,newInstance 方法对类进行实例化。

getClassLoader()

获取类加载器对象。

getTypeParameters()

按照声明的顺序获取对象的参数类型信息。

getPackage()

返回类的包

getInterfaces()

获得当前类实现的类或是接口,可能是有多个,所以返回的是 Class 数组。

Cast

把对象转换成代表类或是接口的对象

asSubclass(Class clazz)

把传递的类的对象转换成代表其子类的对象

getClasses()

返回一个数组,数组中包含该类中所有公共类和接口类的对象

getDeclaredClasses()

返回一个数组,数组中包含该类中所有类和接口类的对象

getSimpleName()

获得类的名字

getFields()

获得所有公有的属性对象

获得所有公有的属性对象

getField(String name)

获得某个公有的属性对象

getDeclaredField(String name)

获得某个属性对象

getDeclaredFields()

获得所有属性对象

getAnnotation(Class annotationClass)

返回该类中与参数类型匹配的公有注解对象

getAnnotations()

返回该类所有的公有注解对象

getDeclaredAnnotation(Class annotationClass)

返回该类中与参数类型匹配的所有注解对象

getDeclaredAnnotations()

返回该类所有的注解对象

getConstructor(Class…<?> parameterTypes)

获得该类中与参数类型匹配的公有构造方法

getConstructors()

获得该类的所有公有构造方法

getDeclaredConstructor(Class…<?> parameterTypes)

获得该类中与参数类型匹配的构造方法

getDeclaredConstructors()

获得该类所有构造方法

getMethod(String name, Class…<?> parameterTypes)

获得该类某个公有的方法

getMethods()

获得该类所有公有的方法

getDeclaredMethod(String name, Class…<?> parameterTypes)

获得该类某个方法

getDeclaredMethods()

获得该类所有方法

Field 类

Field 类提供类或接口中单独字段的信息,以及对单独字段的动态访问。

几个常用的方法

equals(Object obj)

属性与obj相等则返回true

get(Object obj)

获得obj中对应的属性值

set(Object obj, Object value)

设置obj中对应属性值

Method 类

invoke(Object obj, Object… args)

传递object对象及参数调用该对象对应的方法

ClassLoader 类

反射中,还有一个非常重要的类就是 ClassLoader 类,类装载器是用来把类(class) 装载进 JVM的。ClassLoader 使用的是双亲委托模型来搜索加载类的,这个模型也就是双亲委派模型。ClassLoader 的类继承图如下

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JG7IzGrh-1613656709187)(C:\Users\jiang\Pictures\Saved Pictures\image-20210218195854863.png)]

反射应用场景

反射常见的应用场景这里介绍3个:

  • Spring 实例化对象:当程序启动时,Spring 会读取配置文件applicationContext.xml并解析出里面所有的 标签实例化到IOC容器中。
  • 反射 + 工厂模式:通过反射消除工厂中的多个分支,如果需要生产新的类,无需关注工厂类,工厂类可以应对各种新增的类,反射可以使得程序更加健壮。
  • JDBC连接数据库:使用JDBC连接数据库时,指定连接数据库的驱动类时用到反射加载驱动类

Spring 的 IOC 容器

在 Spring 中,经常会编写一个上下文配置文件applicationContext.xml,里面就是关于bean的配置,程序启动时会读取该 xml 文件,解析出所有的 <bean>标签,并实例化对象放入IOC容器中。

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
    <bean id="smallpineapple" class="com.bean.SmallPineapple">
        <constructor-arg type="java.lang.String" value="小菠萝"/>
        <constructor-arg type="int" value="21"/>
    </bean>
</beans>

在定义好上面的文件后,通过ClassPathXmlApplicationContext加载该配置文件,程序启动时,Spring 会将该配置文件中的所有bean都实例化,放入 IOC 容器中,IOC 容器本质上就是一个工厂,通过该工厂传入 标签的id属性获取到对应的实例。

public class Main {
    public static void main(String[] args) {
        ApplicationContext ac =
                new ClassPathXmlApplicationContext("applicationContext.xml");
        SmallPineapple smallPineapple = (SmallPineapple) ac.getBean("smallpineapple");
        smallPineapple.getInfo(); // [小菠萝的年龄是:21]
    }
}

Spring 在实例化对象的过程经过简化之后,可以理解为反射实例化对象的步骤:

  • 获取Class对象的构造器
  • 通过构造器调用 newInstance() 实例化对象

当然 Spring 在实例化对象时,做了非常多额外的操作,才能够让现在的开发足够的便捷且稳定

在之后的文章中会专门写一篇文章讲解如何利用反射实现一个简易版IOC容器,IOC容器原理很简单,只要掌握了反射的思想,了解反射的常用 API 就可以实现,我可以提供一个简单的思路:利用 HashMap 存储所有实例,key 代表 标签的 id,value 存储对应的实例,这对应了 Spring IOC容器管理的对象默认是单例的。

反射 + 抽象工厂模式

传统的工厂模式,如果需要生产新的子类,需要修改工厂类,在工厂类中增加新的分支

public class MapFactory {
    public Map<Object, object> produceMap(String name) {
        if ("HashMap".equals(name)) {
            return new HashMap<>();
        } else if ("TreeMap".equals(name)) {
            return new TreeMap<>();
        } // ···
    }
}

利用反射和工厂模式相结合,在产生新的子类时,工厂类不用修改任何东西,可以专注于子类的实现,当子类确定下来时,工厂也就可以生产该子类了。

反射 + 抽象工厂的核心思想是:

  • 在运行时通过参数传入不同子类的全限定名获取到不同的 Class 对象,调用 newInstance() 方法返回不同的子类。**细心的读者会发现提到了**子类这个概念,所以反射 + 抽象工厂模式,一般会用于有继承或者接口实现关系。

例如,在运行时才确定使用哪一种 Map 结构,我们可以利用反射传入某个具体 Map 的全限定名,实例化一个特定的子类。

public class MapFactory {
    /**
     * @param className 类的全限定名
     */
    public Map<Object, Object> produceMap(String className) {
        Class clazz = Class.forName(className);
        Map<Object, Object> map = clazz.newInstance();
        return map;
    }
}

className 可以指定为 java.util.HashMap,或者 java.util.TreeMap 等等,根据业务场景来定。

JDBC 加载数据库驱动类

在导入第三方库时,JVM不会主动去加载外部导入的类,而是等到真正使用时,才去加载需要的类,正是如此,我们可以在获取数据库连接时传入驱动类的全限定名,交给 JVM 加载该类。

public class DBConnectionUtil {
    /** 指定数据库的驱动类 */
    private static final String DRIVER_CLASS_NAME = "com.mysql.jdbc.Driver";
    
    public static Connection getConnection() {
        Connection conn = null;
        // 加载驱动类
        Class.forName(DRIVER_CLASS_NAME);
        // 获取数据库连接对象
        conn = DriverManager.getConnection("jdbc:mysql://···", "root", "root");
        return conn;
    }
}

在我们开发 SpringBoot 项目时,会经常遇到这个类,但是可能习惯成自然了,就没多大在乎,我在这里给你们看看常见的application.yml中的数据库配置,我想你应该会恍然大悟吧。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZUDNJk6N-1613656709191)(https://camo.githubusercontent.com/1ce83d8cfc1441c93c29237d9bd665aeb87d91796d88def863ead37b7ead4e5d/68747470733a2f2f63646e2e6e6c61726b2e636f6d2f79757175652f302f323032302f706e672f313639343032392f313539373333323430363136382d35313033663333642d373966352d343536622d393936322d3066373739626162353137342e706e67)]

这里的 driver-class-name,和我们一开始加载的类是不是觉得很相似,这是因为MySQL版本不同引起的驱动类不同,这体现使用反射的好处:不需要修改源码,仅加载配置文件就可以完成驱动类的替换

反射的优势及缺陷

反射的优点

  • 增加程序的灵活性:面对需求变更时,可以灵活地实例化不同对象

但是,有得必有失,一项技术不可能只有优点没有缺点,反射也有两个比较隐晦的缺点

  • 破坏类的封装性:可以强制访问 private 修饰的信息
  • 性能损耗:反射相比直接实例化对象、调用方法、访问变量,中间需要非常多的检查步骤和解析步骤,JVM无法对它们优化。

增加程序的灵活性

这里不再用 SmallPineapple 举例了,我们来看一个更加贴近开发的例子:

  • 利用反射连接数据库,涉及到数据库的数据源。在 SpringBoot 中一切约定大于配置,想要定制配置时,使用application.properties配置文件指定数据源

角色1 - Java的设计者:我们设计好DataSource接口,你们其它数据库厂商想要开发者用你们的数据源监控数据库,就得实现我的这个接口

角色2 - 数据库厂商

  • MySQL 数据库厂商:我们提供了 com.mysql.cj.jdbc.MysqlDataSource 数据源,开发者可以使用它连接 MySQL。
  • 阿里巴巴厂商:我们提供了 com.alibaba.druid.pool.DruidDataSource 数据源,我这个数据源更牛逼,具有页面监控慢SQL日志记录等功能,开发者快来用它监控 MySQL吧!
  • SQLServer 厂商:我们提供了 com.microsoft.sqlserver.jdbc.SQLServerDataSource 数据源,如果你想实用SQL Server 作为数据库,那就使用我们的这个数据源连接吧

角色3 - 开发者:我们可以用配置文件指定使用DruidDataSource数据源

spring.datasource.type=com.alibaba.druid.pool.DruidDataSource

需求变更:某一天,老板来跟我们说,Druid 数据源不太符合我们现在的项目了,我们使用 MysqlDataSource 吧,然后程序猿就会修改配置文件,重新加载配置文件,并重启项目,完成数据源的切换。

spring.datasource.type=com.mysql.cj.jdbc.MysqlDataSource

在改变连接数据库的数据源时,只需要改变配置文件即可,无需改变任何代码,原因是:

  • Spring Boot 底层封装好了连接数据库的数据源配置,利用反射,适配各个数据源。

下面来简略的进行源码分析。我们用ctrl+左键点击spring.datasource.type进入 DataSourceProperties 类中,发现使用setType() 将全类名转化为 Class 对象注入到type成员变量当中。在连接并监控数据库时,就会使用指定的数据源操作。

private Class<? extends DataSource> type;

public void setType(Class<? extends DataSource> type) {
    this.type = type;
}

Class对象指定了泛型上界DataSource,我们去看一下各大数据源的类图结构

image.png

上图展示了一部分数据源,当然不止这些,但是我们可以看到,无论指定使用哪一种数据源,我们都只需要与配置文件打交道,而无需更改源码,这就是反射的灵活性!

破坏类的封装性

很明显的一个特点,反射可以获取类中被private修饰的变量、方法和构造器,这违反了面向对象的封装特性,因为被 private 修饰意味着不想对外暴露,只允许本类访问,而setAccessable(true)可以无视访问修饰符的限制,外界可以强制访问。

还记得单例模式一文吗?里面讲到反射破坏饿汉式和懒汉式单例模式,所以之后用了枚举避免被反射KO。

回到最初的起点,SmallPineapple 里有一个 weight 属性被 private 修饰符修饰,目的在于自己的体重并不想给外界知道。

public class SmallPineapple {
    public String name;
    public int age;
    private double weight; // 体重只有自己知道
    
    public SmallPineapple(String name, int age, double weight) {
        this.name = name;
        this.age = age;
        this.weight = weight;
    }
    
}

虽然 weight 属性理论上只有自己知道,但是如果经过反射,这个类就像在裸奔一样,在反射面前变得一览无遗

SmallPineapple sp = new SmallPineapple("小菠萝", 21, "54.5");
Clazz clazz = Class.forName(sp.getClass());
Field weight = clazz.getDeclaredField("weight");
weight.setAccessable(true);
System.out.println("窥觑到小菠萝的体重是:" + weight.get(sp));
// 窥觑到小菠萝的体重是:54.5 kg

性能损耗

在直接 new 对象并调用对象方法和访问属性时,编译器会在编译期提前检查可访问性,如果尝试进行不正确的访问,IDE会提前提示错误,例如参数传递类型不匹配,非法访问 private 属性和方法。

而在利用反射操作对象时,编译器无法提前得知对象的类型,访问是否合法,参数传递类型是否匹配。只有在程序运行时调用反射的代码时才会从头开始检查、调用、返回结果,JVM也无法对反射的代码进行优化。

虽然反射具有性能损耗的特点,但是我们不能一概而论,产生了使用反射就会性能下降的思想,反射的慢,需要同时调用上100W次才可能体现出来,在几次、几十次的调用,并不能体现反射的性能低下。所以不要一味地戴有色眼镜看反射,在单次调用反射的过程中,性能损耗可以忽略不计。如果程序的性能要求很高,那么尽量不要使用反射。

反射基础篇文末总结

  • 反射的思想:反射就像是一面镜子一样,在运行时才看到自己是谁,可获取到自己的信息,甚至实例化对象。
  • 反射的作用:在运行时才确定实例化对象,使程序更加健壮,面对需求变更时,可以最大程度地做到不修改程序源码应对不同的场景,实例化不同类型的对象。
  • 反射的应用场景常见的有3个:Spring的 IOC 容器,反射+工厂模式 使工厂类更稳定,JDBC连接数据库时加载驱动类
  • 反射的3个特点:增加程序的灵活性、破坏类的封装性以及性能损耗

和访问属性时,编译器会在编译期提前检查可访问性,如果尝试进行不正确的访问,IDE会提前提示错误,例如参数传递类型不匹配,非法访问 private 属性和方法。**

而在利用反射操作对象时,编译器无法提前得知对象的类型,访问是否合法,参数传递类型是否匹配。只有在程序运行时调用反射的代码时才会从头开始检查、调用、返回结果,JVM也无法对反射的代码进行优化。

虽然反射具有性能损耗的特点,但是我们不能一概而论,产生了使用反射就会性能下降的思想,反射的慢,需要同时调用上100W次才可能体现出来,在几次、几十次的调用,并不能体现反射的性能低下。所以不要一味地戴有色眼镜看反射,在单次调用反射的过程中,性能损耗可以忽略不计。如果程序的性能要求很高,那么尽量不要使用反射。

反射基础篇文末总结

  • 反射的思想:反射就像是一面镜子一样,在运行时才看到自己是谁,可获取到自己的信息,甚至实例化对象。
  • 反射的作用:在运行时才确定实例化对象,使程序更加健壮,面对需求变更时,可以最大程度地做到不修改程序源码应对不同的场景,实例化不同类型的对象。
  • 反射的应用场景常见的有3个:Spring的 IOC 容器,反射+工厂模式 使工厂类更稳定,JDBC连接数据库时加载驱动类
  • 反射的3个特点:增加程序的灵活性、破坏类的封装性以及性能损耗
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值