在运行时获取成员字段的源顺序的保证方法?

A guaranteed way to get source-order of member fields at runtime?

我正在寻找一种方法(在运行时)按源顺序检索 class 的字段,以便我可以执行我自己的 "initialization processing",它基于声明的顺序。我知道 Javadoc for Class.getDeclaredFields() 明确指出不保证订单。

SO 上的一些答案指向 Javassist,但我找不到任何证据表明 javassist 在没有行号信息的情况下有任何此类保证。

然而 "source-order" 被 Java 编译器使用,因为此代码无法编译:

private int a = 10 * b;
private int b = 5;

显然,b 的值在声明 a 时未知。

这个初始化顺序也必须出现在字节码中,因为在运行时初始化必须以相同的顺序发生(当然,这只是这些边缘情况的要求:-(但这让我认为自然事情就是将源顺序存储在 .class 文件中。

问题:

  1. JVM/Byte代码如何按照声明的顺序初始化成员字段,这个信息是否可以用于重建字段的源顺序?

  2. 是否有任何其他有保证的方法可以实现相同的目标。 Javassist 等第三方工具可以,但必须是 "guaranteed" 或至少 "guaranteed under specific conditions".

  3. 是否有任何特定的 Java 实现可以保证 Class.getDeclaredFields() 上的顺序(可能在特定条件下(哪些))?

供您参考,我需要源代码顺序来重建顺序很重要的遗留语言的行为。我不喜欢明确地添加订单,例如通过添加数组或注释,因为我希望尽可能保持源代码的可读性。

-- 编辑 -- 一个重要的注意事项可能是我需要 "traverse" 的字段都将被注释,例如@MyComplexType(len = 4)。父 class 将需要此元信息来构建一种内存映射。然而,我不想将此注释与订购信息混为一谈,因为我发现这会妨碍可读性和可维护性。

This initialization order must also be present at runtime, for the same reason.

字段的声明顺序和它们的初始化顺序彼此之间没有任何关系。正如您提到的,也有例外情况,但这不是必需的。

So how does the JVM go about initializing member fields in declared order

JVM 仅将字段设置为其未初始化的 0、null 和 false 值。没有别的。

值有除此之外的唯一原因是因为有字节代码将每个字段设置为您设置的值。即没有魔法发生。

can this information perhaps be used to reconstruct source-order of fields?

根据字段在构造函数中设置的顺序,您可以推断出顺序可能是什么。然而,这是一个假设。您更有可能假设字段在 class 文件中出现的顺序是它们在源代码中出现的顺序。

如果有调试信息,这可以用来获取实际的行号,但是JVM 会忽略此信息。注意:这只会告诉您初始化字段的行,这可能不是它们声明的顺序。

例如

class A {
   int a;
   int b;
   int c;
   int d;

   A() {
      d = 1;
      //c not initialised
      b = 2;
      a = 3;
  }

}

所以你可以看到字段在构造函数中初始化的顺序与声明的顺序不匹配。事实上 c 根本不会被初始化并保留默认值 0

关于你的第二个和第三个问题,只能使用一种肮脏的技巧按顺序检索字段:

在字节码中,class文件的字段没有按顺序存储,方法也没有。我不知道 为什么 是这种情况(即使我制作了自己的 JVM 编译器),但我相信 Java 编译器只是决定这样做。 Class.getDeclaredFields returns 字段按照从字节码中读取的顺序排列,这就是为什么它声明不保证顺序的原因。

如果你仍然想按顺序获取它们,我会尝试以下方法:你使用字节码解析器库,例如 Javassist 或 ASM 来读取 class 文件,然后跳过所有内容但是构造函数(以及 static {} 如果您还想对静态字段进行排序)。一旦遇到 PUTFIELDPUTSTATIC 指令,其 owner 是您正在检查的 class ,您将获得当前行,该行可通过存储在字节码中的调试信息获得,并使用它对字段进行排序。这种技术的问题在于它的效率低下,而且它依赖于行号属性,而行号属性并不总是存在于 class 文件中。此外,您只会找到 PUT* 显式初始化字段的说明,默认字段如

protected int modifiers;

没有被编译器初始化,所以字节码中没有指令,因此没有行号信息。在这种情况下或通常没有 LineNumber 属性时,不幸的是你运气不好。那时,我唯一能想到的解决办法就是阅读 class.

的实际源代码

根据您尝试检查的 class,您可能无法获取 class 的实际字节码,但这本身就是一个问题。

如果您想在运行时检索有关编译器未为您存储的字段或方法的信息,请使用自定义注释。例如,

声明您的注释:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface FieldOrder {
    public int order() default 0;
}

注释您的字段:

@FieldOrder{order=1}
int field1 = 5;

@FieldOrder{order=2}
long field2 = field1 * 12;

使用反射来检索您的 Field 对象及其上的注释。

final Class<MyClass> obj = MyClass.class;
if (obj.isAnnotationPresent(FieldOrder.class)) {
  for (final Method method : obj.getDeclaredMethods()) {
    if (method.isAnnotationPresent(FieldOrder.class)) {
      final Annotation annotation = method.getAnnotation(FieldOrder.class);
      final FieldOrder fieldOrder = (FieldOrder) annotation;
      final int order = fieldOrder.order();
      // do something with the field; 
      // add to sorted collection using field order?
    }
  }
}