Java反射进阶—聊聊反射的几个问题

点击上方蓝字关注我,天天一见,给你力量html


前言

昨天有朋友反映好多反射知识没说到,因此今天算是补充篇,一块儿看看反射的进阶知识点。java

反射能够修改final类型成员变量吗?

final咱们应该都知道,修饰变量的时候表明是一个常量,不可修改。那利用反射能不能达到修改的效果呢?git

咱们先试着修改一个用final修饰的String变量。github

public class User {
    private final String name = "Bob";
    private final Student student = new Student();
    
    public String getName() {
        return name;
    }

    public Student getStudent() {
        return student;
    }
}


    User user = new User();
    Class clz = User.class;
    Field field1 = null;
    try{
        field1=clz.getDeclaredField("name");
        field1.setAccessible(true);
        field1.set(user,"xixi");
        System.out.println(user.getName());
    }catch(NoSuchFieldException e){
        e.printStackTrace();
    }catch(IllegalAccessException e){
        e.printStackTrace();
    }

打印出来的结果,仍是Bob,也就是没有修改到。web

咱们再修改下student变量试试:面试

field1 = clz.getDeclaredField("student");
field1.setAccessible(true);
field1.set(user, new Student());

打印:
修改前com.example.studynote.reflection.Student@77459877
修改后com.example.studynote.reflection.Student@72ea2f77

能够看到,对于正常的对象变量即便被final修饰也是能够经过反射进行修改的。缓存

这是为何呢?为何String不能被修改,而普通的对象变量能够被修改呢?安全

先说结论,其实String值也被修改了,只是咱们没法经过这个对象获取到修改后的值。微信

这就涉及到JVM的内联优化了:架构

内联函数,编译器将指定的函数体插入并取代每一处调用该函数的地方(上下文),从而节省了每次调用函数带来的额外时间开支。

简单的说,就是JVM在处理代码的时候会帮咱们优化代码逻辑,好比上述的final变量,已知final修饰后不会被修改,因此获取这个变量的时候就直接帮你在编译阶段就给赋值了。

因此上述的getName方法通过JVM编译内联优化后会变成:

    public String getName() {
        return "Bob";
    }

因此不管怎么修改,都获取不到修改后的值。

有的朋友可能提出直接获取name呢?好比这样:

//修改成public
public final String name = "Bob";

//反射修改后,打印user.name
field1=clz.getDeclaredField("name");
field1.setAccessible(true);
field1.set(user,"xixi");
System.out.println(user.name);

很差意思,仍是打印出来Bob。这是由于System.out.println(user.name)这一句在通过编译后,会被写成:

System.out.println(user.name)

//通过内联优化

System.out.println("Bob")

因此:

「反射是能够修改final变量的,可是若是是基本数据类型或者String类型的时候,没法经过对象获取修改后的值,由于JVM对其进行了内联优化。」

那有没有办法获取修改后的值呢?

有,能够经过反射中的Field.get(Object obj)方法获取:

//获取field对应的变量在user对象中的值
System.out.println("修改后"+field.get(user));

反射获取static静态变量

说完了final,再说说static,怎么修改static修饰的变量呢?

咱们知道,静态变量是在类的实例化以前就进行了初始化(类的初始化阶段),因此静态变量是跟着类自己走的,跟具体的对象无关,因此咱们获取变量就不须要传入对象,直接传入null便可:

public class User {
 public static String name;
}


field2 = clz.getDeclaredField("name");
field2.setAccessible(true);
//获取静态变量
Object getname=field2.get(null);
System.out.println("修改前"+getname);

//修改静态变量
field2.set(null"xixi");
System.out.println("修改后"+User.name);

如上述代码:

  • Field.get(null) 能够获取静态变量。
  • Field.set(null,object) 能够修改静态变量。

怎么提高反射效率

  • 一、缓存重复用到的对象

利用缓存,其实我不说你们也都知道,在平时项目中用到屡次的对象也会进行缓存,谁也不会屡次去建立。

可是,这一点在反射中尤其重要,好比Class.forName方法,咱们作个测试:

    long startTime = System.currentTimeMillis();
    Class clz = Class.forName("com.example.studynote.reflection.User");
    User user;
    int i = 0;
    while (i < 1000000) {
        i++;
        //方法1,直接实例化
        user = new User();
        //方法2,每次都经过反射获取class,而后实例化
        user = (User) Class.forName("com.example.studynote.reflection.User").newInstance();
        //方法3,经过以前反射获得的class进行实例化
        user = (User) clz.newInstance();
    }

    System.out.println("耗时:" + (System.currentTimeMillis() - startTime));

打印结果:

1、直接实例化
耗时:15

2、每次都经过反射获取class,而后实例化
耗时:671

三、经过以前反射获得的class进行实例化
耗时:31

因此看出来,只要咱们合理的运用这些反射方法,好比Class.forName,Constructor,Method,Field等,尽可能在循环外就缓存好实例,就能提升反射的效率,减小耗时。

  • 二、setAccessible(true)

以前咱们说过当遇到私有变量和方法的时候,会用到setAccessible(true)方法关闭安全检查。这个安全检查其实也是耗时的。

因此咱们在反射的过程当中能够尽可能调用setAccessible(true)来关闭安全检查,不管是不是私有的,这样也能提升反射的效率。

  • 三、ReflectASM

ReflectASM 是一个很是小的 Java 类库,经过代码生成来提供高性能的反射处理,自动为 get/set 字段提供访问类,访问类使用字节码操做而不是 Java 的反射技术,所以很是快。

ASM是一个通用的Java字节码操做和分析框架。它能够用于修改现有类或直接以二进制形式动态生成类。

简单的说,这是一个相似反射,可是不一样于反射的高性能库。他的原理是经过ASM库,生成了一个新的类,而后至关于直接调用新的类方法,从而完成反射的功能。

感兴趣的能够去看看源码,实现原理比较简单——https://github.com/EsotericSoftware/reflectasm。

「小总结:」通过上述三种方法,我想反射也不会那么可怕到大大影响性能的程度了,若是真的发现反射影响了性能以及实际使用的状况,也许能够研究下,是不是由于没用对反射和没有处理好反射相关的缓存呢?

反射原理

若是咱们试着查看这些反射方法的源码,会发现最终都会走到native方法中,好比

getDeclaredField方法会走到

 public native Field getDeclaredField(String name) throws NoSuchFieldException;

那么在底层,是怎么获取到类的相关信息的呢?

首先回顾下JVM加载Java文件的过程:

  • 编译阶段,.java文件会被编译成.class文件,.class文件是一种二进制文件,内容是JVM可以识别的机器码。
  • .class文件里面依次存储着类文件的各类信息,好比:版本号、类的名字、字段的描述和描述符、方法名称和描述、是否是public、类索引、字段表集合,方法集合等等数据。
  • 而后,JVM中的类加载器会读取字节码文件,取出二进制数据,加载到内存中,而且解析 .class文件的信息。
  • 类加载器会获取类的二进制字节流,在内存中生成表明这个类的 java.lang.Class对象。
  • 最后会开始类的生命周期,好比 链接、初始化等等。

而反射,就是去操做这个 java.lang.Class对象,这个对象中有整个类的结构,包括属性方法等等。

总结来讲就是,.class是一种有顺序的结构文件,而Class对象就是对这种文件的一种表示,因此咱们能从Class对象中获取关于类的全部信息,这就是反射的原理。

说点无关本文的

最近有一些关于文章中分析源码部分的想法,之前总想把源码原封不动的搬上来,好让你们线下也能找到相关的源码而后通读。

可是可能这样不大现实?并且也形成了不少朋友读文章的障碍,极可能当时只知其一;不知其二,下来所有忘记,至少我就是这样的哈哈。

因此可能在写文章中涉及到源码解析部分,尽可能精简写出来,或者直接贴上伪代码能更方便你们理解吧~

之后试一试。

拜拜

Android体系架构(连载文章、脑图、面试专题):https://github.com/JiMuzz/Android-Architecture

参考

https://juejin.cn/post/6844903905483030536 https://www.zhihu.com/question/46883050 https://juejin.cn/post/6917984253360177159 https://blog.csdn.net/PiaoMiaoXiaodao/article/details/79871313 https://www.cnblogs.com/coding-night/p/10772631.html


感谢你们的阅读,有一块儿学习的小伙伴能够关注下公众号—码上积木❤️

每日一个知识点,创建完总体系架构。



在看你最好看


本文分享自微信公众号 - 码上积木(Lzjimu)。
若有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一块儿分享。

相关文章
相关标签/搜索