框架设计的灵魂(反射)

244 阅读11分钟

框架设计的灵魂(反射)

文章会同时发布在同名公众号《码可思》,一个适合学生党的公众号! 带你通俗易懂走进编程世界!

一、概述

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

简单来说,反射就是把java类中的各个成分 映射成一个个的Java对象

在我们日常的开发中,我们可以利用反射技术对一个类进行剖析,获取所需的私有成员或方法。

Java内存分析

Java内存包含 堆、栈、方法区。

  • 堆:用于存放 new 出来的对象和数据,它可以被所有的线程共享,不会存放别的对象引用
  • 栈:用于存放基本变量类型以及它的具体数值,还有引用对象的变量也放在栈中
  • 方法区:包含了所有的 class 和 static 变量,可以被所有的线程共享

那么,如果当程序要使用某个类时,这个类还没有被加载到内存中,则系统会通过三个步骤来进行初始化

  1. 类的加载 (Load)
    • 根据类的全名,生成一份二进制字节码来表示该类
    • 将二进制的字节码解析成方法区对应的数据结构
    • 生成一个 java.lang.Class 对象实例来表示该类。
    • 这三个过程由类加载器完成。
  2. 类的链接 (Link):将Java类的二进制代码合并到JVM的运行状态之中的过程。
    • 验证:确保加载的类信息符合JVM规范,没有安全方面的问题
    • 准备:正式为类变量(static) 分配内存并设置类变量默认初始值的阶段,这些内存都将在方法区中进行分配。
    • 解析:虚拟机常量池内的符号引用(常量名)替换为直接引用(地址)的过程。
  3. 类的初始化 (Initialize)
    • 执行类构造器< clinit> ()方法的过程。类构造器< clinit> ()方法是由编译期自动收集类中所有类变量的赋值动作和静态代码块中的语句合并产生的。(类构造器是构造类信息的,不是构造该类对象的构造器)。
    • 当初始化一个类的时候,如果发现其父类还没有进行初始化,则需要先触发其父类的初始化。
    • 虚拟机会保证一 个类的 ()方法在多线程环境中被正确加锁和同步。

双亲委托机制(类加载器的加载机制)

image-20200807203702950

工作过程:当一个类加载器接收到类加载的请求的时候,它自己不会先去加载这个类,而是把这个请求委托给它的父类加载器去加载,依次 类推,将这个请求传送到最顶层的类加载器,然后此时顶层的加载器再去搜索所需要加载的类,如果这个类加载器无法完成这个 加载请求,那么它就会反馈给下一层的类加载器去加载。

使用的好处:确保了每一个类的全局唯一性,当程序出现多个限定名一致的类时,类加载器始终只会加载其中的某一个类。因为假如你自己 去编辑一个java.lang.Object,我们都知道在java的包里面这个类是存在的,此时如果没有这个机制,那么显然这个类与系统库 中的类已经重名,这时候程序则会一片混乱,不知道去加载哪一个类了。

二、特点

2.1 优点

灵活性高。因为反射属于动态编译,即只有到运行时才动态创建 &获取对象实例。

编译方式说明:

  1. 静态编译:在编译时确定类型 & 绑定对象。如常见的使用new关键字创建对象
  2. 动态编译:运行时确定类型 & 绑定对象。动态编译体现了Java的灵活性、多态特性 & 降低类之间的藕合性

2.2 缺点

  • 执行效率低 因为反射的操作 主要通过JVM执行,所以时间成本会 高于 直接执行相同操作
  1. 因为接口的通用性,Java的invoke方法是传object和object[]数组的。基本类型参数需要装箱和拆箱,产生大量额外的对象和内存开销,频繁促发GC。
  2. 编译器难以对动态调用的代码提前做优化,比如方法内联。
  3. 反射需要按名检索类和方法,有一定的时间开销。
  • 容易破坏类结构 因为反射操作绕过了源码,容易干扰类原有的内部逻辑

三、反射机制相关的类

与Java反射相关的类如下:

类名用途
Class类代表类的实体,在运行的Java应用程序中表示类和接口
Field类代表类的成员变量(成员变量也称为类的属性)
Method类代表类的方法
Constructor类代表类的构造方法

Class类

Class 类十分特殊,它没有共有的构造方法,被jvm调用的(简单的理解:new对象或者被类加载器加载的时候),在Java中,每个class都有一个相应的Class对象。也就是说,当我们编写一个类,编译完成后,在生成的.class文件中,就会产生一个Class对象,用于表示这个类的类型信息。(记住,生成的这个Class对象是唯一的)

获取类对象的三种方法

1、类型.Class() 去获取类对象

image-20200807114758198

2、对象名.getClass() 去获取类对象

image-20200807114951805

3、Class.forName("类的路径") 已知类所在包的路径下,必须指明所在的包下

image-20200807120539510

  • 获得类相关的方法
方法用途
asSubclass(Class clazz)把传递的类的对象转换成代表其子类的对象
Cast把对象转换成代表类或是接口的对象
getClassLoader()获得类的加载器
getClasses()返回一个数组,数组中包含该类中所有公共类和接口类的对象
getDeclaredClasses()返回一个数组,数组中包含该类中所有类和接口类的对象
forName(String className)根据类名返回类的对象
getName()获得类的完整路径名字
newInstance()创建类的实例
getPackage()获得类的包
getSimpleName()获得类的名字
getSuperclass()获得当前类继承的父类的名字
getInterfaces()获得当前类实现的类或是接口
  • 获得类中属性相关的方法
方法用途
getField(String name)获得某个公有的属性对象
getFields()获得所有公有的属性对象
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()获得该类所有方法
  • 类中其他重要的方法
方法用途
isAnnotation()如果是注解类型则返回true
isAnnotationPresent(Class<? extends Annotation> annotationClass)如果是指定类型注解类型则返回true
isAnonymousClass()如果是匿名类则返回true
isArray()如果是一个数组类则返回true
isEnum()如果是枚举类则返回true
isInstance(Object obj)如果obj是该类的实例则返回true
isInterface()如果是接口类则返回true
isLocalClass()如果是局部类则返回true
isMemberClass()如果是内部类则返回true

上面就是与 Class 有关的全部方法,都有对应的解释,自己多练习或者用到的时候查看文档就可以

其中用到最多的可能就是 newInstance() 这个方法,能够得到类的实例化对象,下面演示一下

image-20200807132410288

image-20200807132240769

Field类

Field代表类的成员变量(成员变量也称为类的属性)。

方法用途
equals(Object obj)属性与obj相等则返回true
get(Object obj)获得obj中对应的属性值
set(Object obj, Object value)设置obj中对应属性值

下面进行代码演示

 public static void main(String[] args) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException, InstantiationException {

        User user1 = new User("Alan1号",3,"男");
        System.out.println(user1);

        Class user = Class.forName("pers.alan.pojo.User");
        Field name = user.getDeclaredField("name");
        System.out.println(name.get(user1));
     }

运行结果:报错了

image-20200807133842727

我们可以看到,报错的原因是我们不能去访问被 private 修饰的属性,即那些私有的属性

那么问题来了,我们难道要去把 User 类中的属性都改为 public 吗?

显然是不可取的,因为这样会大大降低它们的安全性。

所以,反射为我们提供了一个方法去关闭掉安全检查的开关

setAccessible(true/false):表示启用和禁用安全检查的开关。

当值为true时,指反射对象在使用时应该取消java语言访问检查,值为false则只是反射的对象应该开启java语言访问检查。当值设置为true时,不接受检查,可以提高反射的运行速度。(该安全检查的开关是默认开启的,即默认是false)

那么下面我们再试一次

public static void main(String[] args) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException, InstantiationException {

        User user1 = new User("Alan1号",3,"男");
        System.out.println(user1);

        Class user = Class.forName("pers.alan.pojo.User");
        Field name = user.getDeclaredField("name");
        name.setAccessible(true);   //关闭安全检查
        System.out.println(name.get(user1));
}

运行结果:

image-20200807134457299

这是一个需要注意的点,其他的两个方法的话,相信经过这个方法的演示,大家自己就能够去尝试了!

image-20200807134838697

Method类

Method代表类的方法。

方法用途
invoke(Object obj, Object... args)传递object对象及参数调用该对象对应的方法

代码演示

public class Test02 {

    public static void main(String[] args) throws Exception{

        User user1 = new User("Alan1号",3,"男");
        System.out.println(user1);

        Class user = Class.forName("pers.alan.pojo.User");

        Method setName = user.getDeclaredMethod("setName", String.class);
        									//获取名字为setName这个方法,后面接参数的类型
        
        setName.invoke(user1,"alan");    //运用invoke方法,将user1 中的 name设置为 alan
        								// 可以理解为  user1.setName("alan")
        System.out.println(user1);
        
    }
}

运行结果:

image-20200807135846853

Constructor类

Constructor代表类的构造方法。

方法用途
newInstance(Object... initargs)根据传递的参数创建类的对象

代码演示

public class Test02 {

    public static void main(String[] args) throws Exception{

        User user1 = new User("Alan1号",3,"男");
        System.out.println(user1);

        Class user = Class.forName("pers.alan.pojo.User");

        Constructor user2 = user.getDeclaredConstructor(String.class,int.class,String.class);
        User o = (User) user2.newInstance("Alan2号",3,"男");
        
        System.out.println(o.toString());
    }
}

运行结果:

image-20200807140534365

四、一些问题

Java反射可以访问和修改私有成员变量,那封装成private还有意义么

既然小偷可以访问和搬走私有成员家具,那封装成防盗门还有意义么?这是一样的道理,并且Java从应用层给我们提供了安全管理机制——安全管理器,每个Java应用都可以拥有自己的安全管理器,它会在运行阶段检查需要保护的资源的访问权限及其它规定的操作权限,保护系统免受恶意操作攻击,以达到系统的安全策略。所以其实反射在使用时,内部有安全控制,如果安全设置禁止了这些,那么反射机制就无法访问私有成员。

反射是否真的会让你的程序性能降低?

1.反射大概比直接调用慢50~100倍,但是需要你在执行100万遍的时候才会有所感觉

2.判断一个函数的性能,你需要把这个函数执行100万遍甚至1000万遍

3.如果你只是偶尔调用一下反射,请忘记反射带来的性能影响

4.如果你需要大量调用反射,请考虑缓存。

5.你的编程的思想才是限制你程序性能的最主要的因素

下面我们来简单的做一下性能测试(普通方法,利用反射,利用反射并关闭安全检查)

package pers.alan.test;

import pers.alan.pojo.User;

import java.lang.reflect.Method;

public class Test01 {


    /*普通方法执行10亿次*/
    public static void Test1(){

        User user = new User();
        long startTime = System.currentTimeMillis();

        for (int i = 0; i < 1000000000; i++) {
            user.getName();
        }

        long endTime = System.currentTimeMillis();

        System.out.println("普通方法调用10亿次:"+(endTime-startTime)+"ms");

    }

    /*利用反射 然后执行10亿次*/
    public static void Test2() throws Exception {

        User user = new User();
        Class c1 = user.getClass();
        Method getName = c1.getDeclaredMethod("getName", null);

        long startTime = System.currentTimeMillis();

        for (int i = 0; i < 1000000000; i++) {
            getName.invoke(user,null);
        }

        long endTime = System.currentTimeMillis();

        System.out.println("反射  调用10亿次:"+(endTime-startTime)+"ms");

    }

    /*反射 执行10亿次  关闭安全检测*/
    public static void Test3() throws Exception {

        User user = new User();
        Class c1 = user.getClass();
        Method getName = c1.getDeclaredMethod("getName", null);
        getName.setAccessible(true);   //关闭安全检查

        long startTime = System.currentTimeMillis();

        for (int i = 0; i < 1000000000; i++) {
            getName.invoke(user,null);
        }

        long endTime = System.currentTimeMillis();

        System.out.println("反射关闭安全检测  调用10亿次:"+(endTime-startTime)+"ms");

    }

    public static void main(String[] args) throws Exception {
        Test1();
        Test2();
        Test3();
    }
}

运行结果:

image-20200807142129106

可以看到利用调用 整整比 普通方法调用慢了 400多倍

所以可以看到当你调用的次数非常庞大的时候,就必须考虑性能和效率的问题了!!!

看到这里,你是否对反射有了进一步的了解呢!

END

衷心希望阿蓝以后的文章能够帮助到大家,书写笔记是一个持续良好积累的过程,不断的坚持写作也是挺难的!所以大家的支持才是我前进的动力!让我们一起加油吧! 奥力给!!!