「Java 路线」| 反射机制(含 Kotlin)

简介: 「Java 路线」| 反射机制(含 Kotlin)

前言


  • 反射(Reflection)是一种在运行时 动态访问类型信息 的机制。
  • 在这篇文章里,我将带你梳理Java & Kotlin反射的使用攻略,追求简单易懂又不失深度,如果能帮上忙,请务必点赞加关注!


相关文章



目录

image.png

1. 类型系统的基本概念


首先,梳理一一下类型系统的基础概念:

  • 问:什么是强 / 弱类型语言?

答:强 / 弱类型语言的区分,关键在于变量是否 (倾向于) 类型兼容。例如,Java 是强类型语言,变量有固定的类型,以下代码在 Java 中是非法的:


public class MyRunnable {
    public abstract void run();
}
// 编译错误:Incompatible types
java.lang.Runnable runnable = new MyRunnable() { 
    @Override
    public void run() {
    }
}
runnable.run(); // X
复制代码


相对地,JavaScript 是弱类型语言,一个变量没有固定的类型,允许接收不同类型的值:


function MyRunnable(){
    this.run = function(){
    }
}
function Runnable(){
    this.run = function(){
    }
}
var ss = new MyRunnable();
ss.run(); // 只要对象有相同方法签名的方法即可
ss = new Runnable();
ss.run();
复制代码


更具体地描述,Java的强类型特性体现为:变量仅允许接收相同类型或子类型的值。 嗯(黑人问号脸)?和你的理解一致吗?请看下面代码,哪一行是有问题的:


注意,请读者假设 1 ~ 4 号代码是单独运行的
long numL = 1L;
int numI = 0;
numL = numI; // 1
numI = (int)numL; // 2
Integer integer = new Integer(0);
Object obj = new Object();
integer = (Integer) obj; // 3 ClassCastException
obj = integer; // 4
复制代码


在这里,第 3 句代码会发生运行时异常,结论:

  • 1:调用字节码指令 i2l,将 int 值转换为 long 值。(此时,numL 变量接收的是相同类型的值,命题正确)
  • 2:调用字节码指令 l2i,将 long 值转换为 int 值。(此时,numI 变量接收的是相同类型的值,命题正确)
  • 3:调用字节码指令 checkcast,发现 obj 变量的值不是 Integer 类型,抛出 ClassCastException。(此时,Integer 变量不允许接收 Object 对象,命题正确)
  • 4:integer 变量的值是 obj 变量的子类型,可以接收。(此时,Object 变量允许接收 Integer 对象,命题正确)


用一张图概括一下:


image.png


  • 问:什么是静态 / 动态类型语言?


答:静态 / 动态类型语言的区分,关键在于类型检查是否 (倾向于) 编译时执行。例如, Java & C/C++ 是静态类型语言,而 JavaScript 是动态类型语言。需要注意的是,这个定义并不是绝对的,例如 Java 也存在运行时类型检查的方式,例如上面提到的 checkcast 指令本质上是在运行时检查变量的类型与对象的类型是否相同。 那么 Java 是如何在运行时获得类型信息的呢?这就是我们下一节要讨论的问题。


2. 反射的基本概念


  • 问:什么是反射?为什么要使用反射?


答:反射(Reflection)是一种在运行时 动态访问类型信息 的机制。Java 是静态强类型语言,它倾向于在编译时进行类型检查,因此当我们访问一个类时,它必须是编译期已知的,而使用反射机制可以解除这种限制,赋予 Java 语言动态类型的特性。例如:


void func(Object obj) {
    try {
        Method method = obj.getClass().getMethod("run",null);
        method.invoke(obj,null);
    } 
    ... 省略 catch 块
}
func(runnable); 调用 Runnale#run()
func(myRunnable); 调用 MyRunnale#run()
复制代码
  • 问:Java 运行时类型信息是如何表示的?

所有的类在第一次使用时动态加载到内存中,并构造一个 Class 对象,其中包含了与类有关的所有信息,Class 对象是运行时访问类型信息的入口。需要注意的是,每个类 / 内部类 / 接口都拥有各自的 Class 对象。

  • 问:获取 Class 对象有几种方式,有什么区别?答:获取 Class 对象是反射的起始步骤,具体来说,分为以下三种方式:

image.png

  • 问:为什么反射性能差,怎么优化?


答:主要有以下原因:


性能差原因 优化方法
产生大量中间变量 缓存元数据对象
增加了检查可见性操作 调用Method#setAccessible(true),减少不必要的检查
Inflation 机制会生成字节码,而这段字节码没有经过优化 /
缺少编译器优化,普通调用有一系列优化手段,例如方法内联,而反射调用无法应用此优化 /
增加了装箱拆箱操作,反射调用需要构建包装类 /

3. 反射调用的 Inflation 机制


反射调用是反射的一个较为常用的场景,这里我们来分析下反射调用的源码。反射调用需要使用Method#invoke(...),源码如下:


Method.java


public Object invoke(Object obj, Object... args) {
    MethodAccessor ma = methodAccessor;
    if (ma == null) {
        ma = acquireMethodAccessor();
    }
    return ma.invoke(obj, args);
}
复制代码


NativeMethodAccessorImpl.java


class NativeMethodAccessorImpl extends MethodAccessorImpl {
    private final Method method;
    private DelegatingMethodAccessorImpl parent;
    private int numInvocations;
    NativeMethodAccessorImpl(Method var1) {
        this.method = var1;
    }
    public Object invoke(Object var1, Object[] var2) {
        1. 检查调用次数是否超过阈值
        if (++this.numInvocations > ReflectionFactory.inflationThreshold() && !ReflectUtil.isVMAnonymousClass(this.method.getDeclaringClass())) {
            2. ASM 生成新类
            MethodAccessorImpl var3 = (MethodAccessorImpl)(new MethodAccessorGenerator()).generateMethod(this.method.getDeclaringClass(), this.method.getName(), this.method.getParameterTypes(), this.method.getReturnType(), this.method.getExceptionTypes(), this.method.getModifiers());
            3. 设置为代理
            this.parent.setDelegate(var3);
        }
        4. 调用 native 方法
        return invoke0(this.method, var1, var2);
    }
    void setParent(DelegatingMethodAccessorImpl var1) {
        this.parent = var1;
    }
    private static native Object invoke0(Method var0, Object var1, Object[] var2);
}
复制代码

ReflectionFactory.java


public class ReflectionFactory {
    private static int inflationThreshold = 15;
    static int inflationThreshold() {
        return inflationThreshold;
    }
}
复制代码


可以看到,反射调用最终会委派给 NativeMethodAccessorImpl ,要点如下:

  • 当反射调用执行次数较少时,直接通过 native 方法调用;
  • 当反射调用执行次数较多时,则通过 ASM 字节码生成技术生成新的类,以后的反射调用委派给新生成的类来处理。


提示: 为什么不一开始就生成新类呢?因为生成字节码的时间成本高于执行一次 native 方法的时间成本,所以在反射调用执行次数较少时,就直接调用 native 方法了。


4. 反射的应用场景


4.1 类型判断


image.png

4.2 创建对象


  • 1、使用 Class.newInstance(),适用于类拥有无参构造方法
Class<?> classType = Class.forName("java.lang.String");
String str= (String) classType.newInstance();
复制代码
  • 2、Constructor.newInstance(),适用于使用带参数的构造方法


Class<?> classType = Class.forName("java.lang.String");
Constructor<?> constructor = classType.getConstructor(new Class[]{String.class});
constructor.setAccessible(true);
String employee3 = (String) constructor.newInstance(new Object[]{"123"});
复制代码

4.3 创建数组


创建数组需要元素的 Class 对象作为 ComponentType:

  • 1、创建一维数组


Class<?> classType = Class.forName("java.lang.String");
 String[] array = (String[]) Array.newInstance(classType, 5);  长度为5
Array.set(array, 3, "abc");  设置元素
String string = (String) Array.get(array,3);  读取元素
复制代码
  • 2、创建多维数组


Class[] dimens = {3, 3};
Class[][] array = (Class[][]) Array.newInstance(int.class, dimens);
复制代码


4.3 访问字段、方法


Editting...

4.4 获取泛型信息


我们知道,编译期会进行类型擦除,Code 属性中的类型信息会被擦除,但是在类常量池属性(Signature属性、LocalVariableTypeTable属性)中还保留着泛型信息,因此我们可以通过反射来获取这部分信息。在这篇文章里,我们详细讨论:《Java | 关于泛型能问的都在这里了(含Kotlin)》,请关注!


4.5 获取运行时注解信息


注解是一种添加到声明上的元数据,而RUNTIME注解在类加载后会保存在 Class 对象,可以反射获取。在这篇文章里,我们详细讨论:《Java | 这是一篇全面的注解使用攻略(含 Kotlin)》,请关注!

目录
相关文章
|
人工智能 缓存 安全
Java中的反射机制:深入探索与应用
Java反射机制是程序运行时动态获取类信息并操作类成员的特性,具备高度灵活性,但也伴随性能与安全风险。本文详解反射的基本用法、高级应用及最佳实践,助你掌握这一强大工具的正确使用方式。
254 0
|
人工智能 Java
Java中的反射机制:深入探索与应用
本文介绍了Java反射机制的基本概念、用途及其实现方式。反射机制允许程序在运行时动态获取类的属性和方法,并调用它们,适用于处理私有成员或权限受限的情况。文章详细讲解了`Class`类的功能,包括获取类的方法、属性、注解、构造器等信息,以及通过四种方式获取`Class`对象的示例代码。此外,还探讨了类加载器、继承关系判断、动态代理等高级内容,展示了如何在运行时创建接口实例并处理方法调用。文末提供了完整的代码示例以加深理解。
275 0
Java中的反射机制:深入探索与应用
|
人工智能 安全 Java
掌握Java反射:在项目中高效应用反射机制
Java反射是一种强大功能,允许程序在运行时动态获取类信息、创建对象、调用方法和访问字段,提升程序灵活性。它在框架开发、动态代理、注解处理等场景中广泛应用,如Spring和Hibernate。但反射也存在性能开销、安全风险和代码复杂性,应谨慎使用。
276 0
|
IDE Java 开发工具
JetBrains IntelliJ IDEA 2025.1 发布 - 领先的 Java 和 Kotlin IDE
JetBrains IntelliJ IDEA 2025.1 (macOS, Linux, Windows) - 领先的 Java 和 Kotlin IDE
969 2
|
数据采集 前端开发 JavaScript
如何利用Java和Kotlin实现动态网页内容抓取
如何利用Java和Kotlin实现动态网页内容抓取
|
Java 编译器 测试技术
Kotlin31 协程如何与 Java 进行混编?
Kotlin31 协程如何与 Java 进行混编?
434 2
Kotlin31 协程如何与 Java 进行混编?
|
安全 Java 编译器
Kotlin教程笔记(27) -Kotlin 与 Java 共存(二)
Kotlin教程笔记(27) -Kotlin 与 Java 共存(二)
|
Java 开发工具 Android开发
Kotlin教程笔记(26) -Kotlin 与 Java 共存(一)
Kotlin教程笔记(26) -Kotlin 与 Java 共存(一)
|
安全 Java 编译器
Kotlin教程笔记(27) -Kotlin 与 Java 共存(二)
Kotlin教程笔记(27) -Kotlin 与 Java 共存(二)
181 2
|
Java 编译器 Android开发
Kotlin教程笔记(28) -Kotlin 与 Java 混编
Kotlin教程笔记(28) -Kotlin 与 Java 混编
305 2