RTTI
RTTI
(Run-Time Type Identification)是一种在运行时确定对象类型的机制。它是一种编程语言特性,主要用于在运行时动态识别和处理对象的实际类型。
通过 RTTI
,可以在运行时根据对象的实际类型执行相应的操作。这在处理多态对象和基于继承的代码逻辑时非常有用。
Java中的RTTI
在Java中,RTTI
主要通过以下两种方式实现:
-
instanceof
运算符:假定我们在编译时已经知道了所有的类型,可以使用instanceof
运算符来检查对象是否是特定类的实例或其子类的实例。它返回一个布尔值来指示对象是否可以转换为指定的类型。例如:
if (obj instanceof MyClass) {
// 对象是MyClass类型或其子类的实例
}
-
getClass()
方法:利用反射机制,在运行时发现和使用类的信息每个Java对象都有
getClass()
方法,它返回一个表示对象所属类的 Class 对象。可以使用getClass()
方法来获取对象的实际运行时类型。例如:
Class clazz = obj.getClass(); // 获取对象的实际类型
反射机制
反射机制是指在运行时动态地获取类的信息并操作类和对象的方法和属性。它是一种 Java 语言的特性,可以让程序在运行时获取类的信息,包括类型、属性、方法等,并且可以在运行时动态地创建对象、调用对象的方法、修改属性等。
Java反射机制的实现是通过 Java.lang.Class
类来实现的,Class类提供了许多方法来获取类的信息。通过 Class 类可以获取类的名称、继承关系、方法、属性等,还可以创建对象、调用方法等。
优点
Java 反射机制的优点是可以动态地获取和使用类的信息,这使得 Java 程序的灵活性更高,开发人员可以根据需要在运行时动态地调整程序,让程序更加灵活、可扩展。
缺点
-
破坏封装:由于反射允许访问私有字段和私有方法,所以可能会破坏封装而导致安全问题。
-
性能开销:由于反射涉及到动态解析,因此无法执行 Java 虚拟机优化,再加上反射的写法的确要复杂得多,所以性能要比“正射(直接使用类)”差很多,在一些性能敏感的程序中应该避免使用反射。
Java 反射机制的性能相对较低,使用不当还可能会导致程序运行失败或者出现安全漏洞,需要根据具体情况仔细使用。
应用场景
-
开发通用框架:例如 Spring,为了保持通用性,通过配置文件来加载不同的对象,调用不同的方法。
-
动态代理:在 面向切面编程(AOP)中,需要拦截特定的方法,就会选择动态代理的方式,而动态代理的底层技术就是反射。
-
注解:注解本身只是起到一个标记符的作用,它需要利用反射机制,根据标记符去执行特定的行为。
-
RPC框架:RPC(远程过程调用)框架通过反射机制动态地生成类和方法调用,实现远程服务的调用。
-
单元测试:在单元测试中,可以使用反射机制来调用私有方法或访问私有成员变量,从而进行更全面的测试。
反射机制的使用
Class 类
在Java中,Class类是反射机制的核心类之一,与class关键字不同。每个类都有一个与之关联的Class对象,该对象包含了有关类的元数据信息(如类的名称、字段、方法、构造函数等)。
Class类,位于 JDK 的 java.lang
包中。Class类的实例表示 java 应用运行时的 类 class ans enum
或 接口 interface and annotation
java.lang.class
是所有反射API的入口
-
手动编写的类被编译后(JVM 加载类时)会产生一个 Class对象,其表示的是创建的类的元数据,这个 Class对象保存在
同名.class
的文件中(字节码文件) -
每个通过关键字
class
标识的类,在 JVM 中有且只有一个与之对应的Class对象来描述其类型信息,无论创建多少个实例对象,其依据的都是用一个 Class对象。 -
Class类 只存私有构造函数,因此对应Class对象只能由JVM创建和加载
-
Class类 的对象作用是运行时提供或获得某个对象的类型信息,这点对于反射技术很重要
源代码
数组同样也被映射为 class 对象的一个类,所有具有相同元素类型和维数的数组都共享该 Class 对象。
基本类型 boolean
,byte
,char
,short
,int
,long
,float
,double
和关键字 void
同样表现为 class 对象。
每个 java类 运行时都在 JVM 里表现为一个 class对象,可通过类名.class
、类型.getClass()
、Class.forName("ClassName")
等方法获取class对象。
Class类 没有公共构造函数。相反,当一个类是类文件的字节中派生类时,Java虚拟机会通过调用以下方法之一,自动构造Class对象:
ClassLoader::defineClass
java.lang.invoke.MethodHandles.Lookup::defineClass
java.lang.invoke.MethodHandles.Lookup::defineHiddenClass
ClassLoader
在Java中,每个类都有一个与之关联的 ClassLoader
对象,用于在运行时动态加载类和资源。
ClassLoader
是 Java 的一个能够动态加载类和资源的机制,它是 Java 虚拟机的重要组成部分。ClassLoader
根据特定的策略从文件系统、Jar
包、网络等地方加载字节码文件到内存中,并根据需要链接、解析和初始化类。
每个 ClassLoader
对象都有一个 父ClassLoader
,它们被组织成ClassLoader树形结构,Java虚拟机通过使用 ClassLoader树 来确保每个类只被加载和链接一次,避免对同一个类重复创建多个实例。
对于Java中的类Class,它也有一个 ClassLoader 对象,用于加载该类对应的字节码文件。
getClassLoader()
:获取加载该类的ClassLoader对象。
使用Java类时,如果没有显示地指定一个 ClassLoader
,那么该类将由系统默认的 ClassLoader
进行加载。
如果需要自定义 ClassLoader
来实现特定的功能,例如从自定义的资源中加载类或者版本控制,可以通过继承 URLClassLoader
或者ClassLoader类来实现自己的 ClassLoader。
binaryname
在Java中,每个类都有一个二进制名称(binary name),用于唯一标识一个类。二进制名称是类的全限定名(fully qualified name),其中包括类所在的包路径和类名。
二进制名称的格式遵循以下约定:
-
使用"."来分隔包路径和类名。例如,类
com.example.MyClass
的二进制名称为com.example.MyClass
。 -
对于内部类,使用
$
来分隔内部类和外部类。例如,内部类MyClass
在外部类com.example.OuterClass
中,其二进制名称为com.example.OuterClass$MyClass
。
二进制名称在Java中广泛应用于反射、类加载等场景中,可以通过Class对象的 getName()
方法获取一个类的二进制名称。
需要注意的是,二进制名称与Java源代码中的类名是相对应的,但在某些情况下可能会有差异。
如果类是通过编译时注解处理器生成的,二进制名称可能会包含特殊字符或编码,这与源代码中的名称不完全一致。
对于数组类型,我们可以通过在类名后添加[]
来表示多维数组,如int[]
的二进制名称为[I]
在使用Java类时,我们通常要使用完整的二进制名称来标识一个类,特别是在动态加载和热部署等场景中,以避免类名冲突和不一致问题。
Class 类方法
Class类提供了许多用于操作和查询类信息的方法,如下所示:
-
实例化Class对象:
-
Class.forName(String className)
:通过类的全限定名获取Class对象。 -
obj.getClass()
:通过对象获取其所属的Class对象。
-
-
获取类的相关信息:
-
getName()
:获取类的名称。 -
getSimpleName()
:只获取类名 -
getCanonicalName()
:主要用于输出(toString)或log打印,大多数情况下和 getName() 一样,输出内部类、数组等其他类型表现形式不同。
不能用getCanonicalName去加载类对象,必须用getName -
getPackage()
:获取类所在的包。 -
getModifiers()
:获取修饰符的整数表示。 -
getField(String name)
:获取指定名称的公共字段。 -
getFields()
:获得某个类的所有的公共(public)的字段,包括继承自父类的所有公共字段。 -
getDeclaredFields()
:获取所有字段的数组,包括私有字段。 -
getMethod(String name, Class<?>... parameterTypes)
:获取指定名称和参数类型的公共方法。 -
getDeclaredMethods()
:获取所有方法的数组,包括私有方法。 -
getConstructor(Class<?>... parameterTypes)
:获取指定参数类型的公共构造函数。 -
getDeclaredConstructors()
:获取所有构造函数的数组,包括私有构造函数。 -
getInterfaces()
:返回Class对象数组,表示Class对象所引用的类所实现的所有接口。 -
getSuperClass()
:返回目标类的父类对应的Class对象
-
-
创建对象和实例化:
-
newInstance()
:通过默认构造函数创建对象。 -
newInstance(Object... initargs)
:通过指定构造函数创建对象。
-
-
类型判断和转换:
-
isAssignableFrom(Class cls)
:判断Class对象是否可以赋值给指定的Class。 -
isInstance(Object obj)
:判断对象是否是Class的实例。 -
cast(Object obj)
:将对象强制转换为Class的类型。
-
获取 class 对象
获取 class对象
的方式的主要有三种
-
根据类名:
类名.class
-
根据对象:
对象.getClass()
-
根据全限定类名:
Class.forName("全限定类名")
类加载机制
Constructor类
Constructor
类是一个反射类,它提供了对构造器的封装和操作。通过Constructor
类,可以动态地获取、创建和调用类的构造器。
使用 Constructor
类,可以在运行时通过反射机制获取类的构造器,并动态地创建和使用类的实例。这在某些特定的场景下非常有用,例如动态加载类、实例化对象和调用私有构造器等。
常用方法
Constructor
类的常用方法包括:
-
getModifiers()
:获取构造器的修饰符。 -
isAccessible()
、setAccessible(boolean flag)
:控制构造器的可访问性。
若未显式地声明类的 public 构造函数时,应使用
getDeclaredConstructor()
获取类的构造器
与Class类相关的方法
Method类
Method
类是一个反射类,它提供了对类或接口的方法的封装和操作。通过 Method
类,可以动态地获取和调用类的方法。反射的方法可能是类方法或实例方法(包括抽象方法)。
常用方法
与Class类相关的方法
Field类
Field
类是一个反射类,它提供了对类或接口的成员字段(属性)的封装和操作。通过 Field
类,可以动态地获取和修改类的字段的值。反射的字段可能是一个类(静态)字段或实例字段。
常用方法
与Class类相关的方法
可以通过 Class
类的提供的方法来获取代表字段信息的 Field
对象
获取 指定字段 名称的Field类时,注意字段修饰符必须为 public
且存在该字段,否则会抛出 NoSuchFieldException
在设置值的方法上,Field类还提供了专门针对基本数据类型的方法,如setInt()/getInt()
、setBoolean()/getBoolean
、setChar()/getChar()
等等方法
Type 接口
java.lang.reflect.Type
接口,用于表示 Java 类型的通用接口。Type
接口是整个 Java 反射机制的核心,它提供了表示各种类型(如类、接口、数组、参数化类型、类型变量等)的方法和属性。
Type
接口是一个顶级接口,定义了一些通用的方法,同时还有一些子接口和实现类来具体表示不同的类型。
除了 Type
接口之外,以下是一些常见的 Type
接口的子接口和实现类:
-
Class
:表示类或接口类型。 -
ParameterizedType
:表示参数化类型,例如List<String>
。 -
TypeVariable
:表示类型变量,例如泛型类型中的<T>
。 -
WildcardType
:表示通配符类型,例如? extends Number
。 -
GenericArrayType
:表示泛型数组类型,例如T[]
。
通过使用 Type
接口及其子接口和实现类,可以在反射期间获取和操作不同类型的信息。这对于处理泛型、读取类型参数、获取类型的父类或实现的接口等操作非常有用。
Modifier
java.lang.reflect.Modifier
是 Java 反射库中的一个实用类,它提供了一组用于操作和解析修饰符的静态方法。修饰符是用于描述类、方法、字段等元素的特征和访问级别的标志。
获取修饰符
getModifiers()
用于获取一个类、字段、方法或构造函数的修饰符。它返回一个表示修饰符的整数值。
修饰符常量 | 含义 |
---|---|
Modifier.PUBLIC | public 修饰符 |
Modifier.PRIVATE | private 修饰符 |
Modifier.PROTECTED | protected 修饰符 |
Modifier.STATIC | static 修饰符 |
Modifier.FINAL | final 修饰符 |
Modifier.ABSTRACT | abstract 修饰符 |
Modifier.SYNCHRONIZED | synchronized 修饰符 |
Modifier.VOLATILE | volatile 修饰符 |
Modifier.TRANSIENT | transient 修饰符 |
Modifier.NATIVE | native 修饰符 |
Modifier.INTERFACE | 接口修饰符 |
Modifier.ANNOTATION | 注解修饰符 |
Modifier.ENUM | 枚举修饰符 |
常用方法
Modifier
类提供了一些常用的静态方法,可以用于检查和解析修饰符。下面是一些常用的方法:
方法名 | 描述 |
---|---|
isPublic(int mod) | 检查修饰符是否为 public。 |
isPrivate(int mod) | 检查修饰符是否为 private。 |
isProtected(int mod) | 检查修饰符是否为 protected。 |
isStatic(int mod) | 检查修饰符是否为 static。 |
isFinal(int mod) | 检查修饰符是否为 final。 |
isAbstract(int mod) | 检查修饰符是否为 abstract。 |
isSynchronized(int mod) | 检查修饰符是否为 synchronized。 |
isVolatile(int mod) | 检查修饰符是否为 volatile。 |
isTransient(int mod) | 检查修饰符是否为 transient。 |
isNative(int mod) | 检查修饰符是否为 native。 |
isInterface(int mod) | 检查修饰符是否为接口。 |
isAnnotation(int mod) | 检查修饰符是否为注解类型。 |
isEnum(int mod) | 检查修饰符是否为枚举类型。 |
这些方法接受一个表示修饰符的整数值,可以使用位操作符 &
和 |
来组合和操作修饰符的整数值。
反射机制的执行流程
-
反射类及反射方法的获取,都是通过从列表中搜寻查找匹配的方法,所以查找性能会随类的大小方法多少而变化;
-
每个类都会有一个与之对应的
Class
实例,从而每个类都可以获取method
反射方法,并作用到其他实例身上; -
反射本身不是线程安全的,并且在使用反射进行对象操作时需要确保线程安全。在多线程环境下,如果多个线程同时对同一个类或对象进行反射操作,可能会导致并发竞争和数据不一致的问题。
-
反射使用软引用
relectionData
缓存class信息,避免每次重新从 JVM 获取带来的开销; -
反射调用多次生成新代理 Accessor, 而通过字节码生存的则考虑了卸载功能,所以会使用独立的类加载器;
-
当找到需要的方法,都会copy一份出来,而不是使用原来的实例,从而保证数据隔离;
-
调度反射方法,最终是由 JVM 执行 invoke0()执行.
forName()
java.lang.Class.forName()
反射获取类信息,并没有将实现留给 java,而是交给了 JVM 去加载。
主要是先获取 ClassLoader
, 然后调用 native
方法,获取信息,加载类则是回调 java.lang.ClassLoader
.
最后,JVM 又会回调 ClassLoader
进类加载。
newInstance()
newInstance()
主要做了三件事:
-
权限检测,如果不通过直接抛出异常;
-
查找无参构造器,并将其缓存起来;
-
调用具体方法的无参构造方法,生成实例并返回
getConstructor()
getConstructor0()
为获取匹配的构造方器;分三步:
-
先获取所有的 constructors, 然后通过进行参数类型比较;
-
找到匹配后,通过
ReflectionFactory
copy一份constructor返回; -
否则抛出
NoSuchMethodException
;
getMethods()
一旦你有了Class
对象,就可以使用反射API来查询类的信息,如字段、方法、构造器等。getMethods()
和 getMethod(String name, Class<?>... parameterTypes)
就是用来查询方法信息的。
invoke()
MethodAccessor
MethodAccessor
是 Java 反射 API 中的一个接口,用于提供对方法的快速调用,在一定程度上可以提升方法调用的性能。
在 Java 中,通过反射调用方法时,通常使用 Method
对象的 invoke()
方法来执行方法调用。每次调用 invoke()
方法都需要进行一系列的检查和动态分派,这会带来一定的性能开销。
为了优化方法调用的性能,Java 提供了 MethodAccessor
接口。它是 sun.reflect
包下的一个内部接口,用于快速执行方法调用。 MethodAccessor
接口只定义了一个方法 invoke()
,它可以直接执行方法的调用,无需每次都进行反射操作。
在 sun.reflect
包下的实现类 MethodAccessorImpl
实现了 MethodAccessor
接口,并通过一些优化手段提升了方法调用的性能。但是需要注意的是,sun.reflect
包是非标准的、非公共的 API,它的使用不被官方推荐,也不建议直接使用。
MethodAccessor 和 invoke()
invoke()
方法实际上是委派给 MethodAccessor
接口来完成的。
MethodAccessor 接口有三个实现类,其中的 MethodAccessorImpl
是一个抽象类,另外两个具体的实现类继承了这个抽象类。
-
NativeMethodAccessorImpl
:通过本地方法来实现反射调用; -
DelegatingMethodAccessorImpl
:通过委派模式来实现反射调用;
通过 debug 的方式进入 invoke()
方法后,可以看到第一次反射调用会生成一个委派实现 DelegatingMethodAccessorImpl
,它在生成的时候会传递一个本地实现 NativeMethodAccessorImpl
。
即,invoke()
方法在执行的时候,会先调用 DelegatingMethodAccessorImpl
,然后调用 NativeMethodAccessorImpl
,最后再调用实际的方法。
委派模式
采用委派实现而不直接调用本地实现,是为了能够在本地实现和动态实现之间切换。动态实现是另外一种反射调用机制,它通过生成字节码的形式来实现。
如果反射调用的次数比较多,动态实现的效率就会更高,因为本地实现需要经过 Java 到 C/C++ 再到 Java 之间的切换过程,而动态实现不需要;但如果反射调用的次数比较少,反而本地实现更快一些。
调用次数的临界点默认值为 15,可以通过
-Dsun.reflect.inflationThreshold
参数类调整
若次数小于15次,由 JVM 调用;大于15次,由 java 代码生成
第 16 次执行的时候,会进入到 if 条件分支中,改变 DelegatingMethodAccessorImpl 的委派模式 delegate 为 (MethodAccessorImpl)(new MethodAccessorGenerator()).generateMethod()
,而之前的委派模式 delegate 为 NativeMethodAccessorImpl
。
总结
Java反射机制是Java语言的一个重要特性,它提供了在运行时动态检查和操作类的能力。虽然反射机制在灵活性和降低耦合度方面具有优势,但也存在性能、安全和可读性问题。因此,在使用反射机制时需要权衡其优缺点,并谨慎使用。