1. 概述
Java反射是个强大的特性,能让我们操作类、接口、字段和方法等成员。更重要的是,通过反射,我们可以在编译时不知道具体类型的情况下实例化类、调用方法和访问字段。
本文将首先探索JVM访问标志,然后看看如何使用它们,最后分析修饰符和访问标志的区别。
2. JVM访问标志
先理解下JVM访问标志的本质。
《Java虚拟机规范》定义了JVM中编译类的结构,包含一个ClassFile结构:
ClassFile {
u4 magic;
u2 minor_version;
u2 major_version;
u2 constant_pool_count;
cp_info constant_pool[constant_pool_count-1];
u2 access_flags;
u2 this_class;
u2 super_class;
u2 interfaces_count;
u2 interfaces[interfaces_count];
u2 fields_count;
field_info fields[fields_count];
u2 methods_count;
method_info methods[methods_count];
u2 attributes_count;
attribute_info attributes[attributes_count];
}
ClassFile中包含了access_flags项。简单说,access_flags是一个掩码,由多个标志位组成,定义了类的访问权限和其他属性。
此外,ClassFile还包含field_info和method_info项,它们各自也有access_flags项。
像Javassist和ASM这类库会使用JVM访问标志来操作Java字节码。
除Java外,JVM还支持Kotlin或Scala等其他语言。每种语言都有自己定义的修饰符。例如,Java中的Modifier类包含了所有Java编程语言特有的修饰符。使用反射时,我们通常依赖这些类提供的信息。
但当需要将修饰符转换为JVM访问标志时,问题就出现了。下面深入分析原因。
3. 修饰符的AccessFlag枚举
像varargs、transient或volatile、bridge这些修饰符会使用相同的整数位掩码。为了解决不同修饰符间的位冲突,Java 20引入了AccessFlag枚举,它包含了类、字段或方法中可用的所有修饰符。
该枚举对JVM访问标志进行了建模,简化了修饰符和访问标志间的映射。没有AccessFlag枚举时,我们需要考虑元素的上下文才能确定使用了哪个修饰符,特别是那些具有相同位表示的修饰符。
来看个实际例子,创建AccessFlagDemo类,包含几个使用不同修饰符的方法:
public class AccessFlagDemo {
public static final void staticFinalMethod() {
}
public void varArgsMethod(String... args) {
}
public strictfp void strictfpMethod() {
}
}
接下来检查staticFinalMethod()方法使用的访问标志:
@Test
void givenStaticFinalMethod_whenGetAccessFlag_thenReturnCorrectFlags() throws Exception {
Class<?> clazz = Class.forName(AccessFlagDemo.class.getName());
Method method = clazz.getMethod("staticFinalMethod");
Set<AccessFlag> accessFlagSet = method.accessFlags();
assertEquals(3, accessFlagSet.size());
assertTrue(accessFlagSet.contains(AccessFlag.PUBLIC));
assertTrue(accessFlagSet.contains(AccessFlag.STATIC));
assertTrue(accessFlagSet.contains(AccessFlag.FINAL));
}
这里调用了accessFlags()方法,它返回一个不可修改的EnumSet。方法内部使用了getModifiers(),并根据标志可应用的位置返回相应的访问标志。我们的方法包含三个访问标志:PUBLIC、STATIC和FINAL。
另外,从Java 17开始,strictfp修饰符已变得多余,不再被编译到字节码中:
@Test
void givenStrictfpMethod_whenGetAccessFlag_thenReturnOnlyPublicFlag() throws Exception {
Class<?> clazz = Class.forName(AccessFlagDemo.class.getName());
Method method = clazz.getMethod("strictfpMethod");
Set<AccessFlag> accessFlagSet = method.accessFlags();
assertEquals(1, accessFlagSet.size());
assertTrue(accessFlagSet.contains(AccessFlag.PUBLIC));
}
可以看到,strictfpMethod()只包含一个访问标志。
4. getModifiers()与accessFlags()方法对比
在Java反射中,我们常用getModifiers()方法获取类、接口、方法或字段上定义的所有修饰符:
@Test
void givenStaticFinalMethod_whenGetModifiers_thenReturnIsStaticTrue() throws Exception {
Class<?> clazz = Class.forName(AccessFlagDemo.class.getName());
Method method = clazz.getMethod("staticFinalMethod");
int methodModifiers = method.getModifiers();
assertEquals(25, methodModifiers);
assertTrue(Modifier.isStatic(methodModifiers));
}
getModifiers()返回一个整数值,表示编码后的修饰符标志。我们调用了Modifier类中的isStatic()方法检查方法是否包含static修饰符。Java在方法内部解码标志以确定方法是否为静态的。
⚠️ 注意:访问标志与Java中定义的修饰符并不完全相同。有些访问标志和修饰符是一一对应的(如public),但有些修饰符(如sealed)没有指定的访问标志。同样,有些访问标志(如synthetic)也无法映射到对应的修饰符值。
更进一步说,由于某些修饰符共享相同的位表示,如果不考虑使用上下文,可能会得出错误结论。
对varArgsMethod()调用Modifier.toString():
@Test
void givenVarArgsMethod_whenGetModifiers_thenReturnPublicTransientModifiers() throws Exception {
Class<?> clazz = Class.forName(AccessFlagDemo.class.getName());
Method method = clazz.getMethod("varArgsMethod", String[].class);
int methodModifiers = method.getModifiers();
assertEquals("public transient", Modifier.toString(methodModifiers));
}
方法返回"public transient"。如果不考虑上下文,我们可能错误地认为varArgsMethod()是transient的。
而访问标志会考虑位的来源上下文,因此能提供正确信息:
@Test
void givenVarArgsMethod_whenGetAccessFlag_thenReturnPublicVarArgsFlags() throws Exception {
Class<?> clazz = Class.forName(AccessFlagDemo.class.getName());
Method method = clazz.getMethod("varArgsMethod", String[].class);
Set<AccessFlag> accessFlagSet = method.accessFlags();
assertEquals("[PUBLIC, VARARGS]", accessFlagSet.toString());
}
5. 总结
本文介绍了JVM访问标志及其使用方法。
✅ 核心要点:
- JVM访问标志包含运行时成员(类、方法、字段)的访问权限和其他属性信息
- AccessFlag枚举解决了修饰符位冲突问题
- accessFlags()方法比getModifiers()更准确,因为它考虑了上下文
使用访问标志可以更精确地获取特定元素上的修饰符信息,避免踩坑。
完整代码示例可在GitHub上查看:https://github.com/eugenp/tutorials/tree/master/core-java-modules/core-java-20