为什么 Java 中的最终常量可以被覆盖?

2022-09-01 08:19:48

考虑以下 Java 中的接口:

public interface I {
    public final String KEY = "a";
}

以及以下类:

public class A implements I {
    public String KEY = "b";

    public String getKey() {
        return KEY;
    }
}

为什么类 A 会出现并覆盖接口 I 的最终常量?

自己试试:

A a = new A();
String s = a.getKey(); // returns "b"!!!

答案 1

您正在隐藏它,这是“范围”的一个功能。任何时候,只要你在较小的范围内,你可以重新定义所有你喜欢的变量,外部范围变量将被“阴影化”

顺便说一句,如果您愿意,可以再次限定其范围:

public class A implements I {
    public String KEY = "b";

    public String getKey() {
        String KEY = "c";
        return KEY;
    }
}

现在KEY将返回“c”;

编辑,因为原文在重读时很糟糕。


答案 2

尽管您正在跟踪变量,但有趣的是,您可以更改java中的最终字段,您可以在此处阅读:

Java 5 - “最终”不再是最终的

挪威Machina Networks的Narve Saetre昨天给我发了一张纸条,提到我们可以将句柄更改为最终数组,这很遗憾。我误解了他,并开始耐心地解释我们不能使数组恒定,并且没有办法保护数组的内容。“不,”他说,“我们可以用反射来改变最终的句柄。

我尝试了Narve的示例代码,令人难以置信的是,Java 5允许我修改最终句柄,甚至是原始字段的句柄!我知道它曾经在某个时候被允许,但后来被禁止了,所以我用旧版本的Java运行了一些测试。首先,我们需要一个包含最终字段的类:

public class Person {
  private final String name;
  private final int age;
  private final int iq = 110;
  private final Object country = "South Africa";

  public Person(String name, int age) {
    this.name = name;
    this.age = age;
  }

  public String toString() {
    return name + ", " + age + " of IQ=" + iq + " from " + country;
  }
}

JDK 1.1.x

在 JDK 1.1.x 中,我们无法使用反射访问私有字段。但是,我们可以创建另一个具有公共字段的 Person,然后针对该字段编译我们的类,并交换 Person 类。如果我们针对与编译的类不同的类运行,则在运行时没有访问检查。但是,我们无法在运行时使用类交换或反射重新绑定最终字段。

JDK 1.1.8 JavaDocs for java.lang.reflect.Field有以下说法:

  • 如果此 Field 对象强制实施 Java 语言访问控制,并且基础字段不可访问,则该方法将引发非法访问异常。
  • 如果基础字段是 final,则该方法将引发非法访问异常。

JDK 1.2.x

在JDK 1.2.x中,这发生了一些变化。现在,我们可以使用 setAccessible(true) 方法使私有字段可访问。现在在运行时检查了字段的访问,因此我们无法使用类交换技巧来访问私有字段。但是,我们现在可以突然重新绑定最终字段!看看这个代码:

import java.lang.reflect.Field;

public class FinalFieldChange {
  private static void change(Person p, String name, Object value)
      throws NoSuchFieldException, IllegalAccessException {
    Field firstNameField = Person.class.getDeclaredField(name);
    firstNameField.setAccessible(true);
    firstNameField.set(p, value);
  }
  public static void main(String[] args) throws Exception {
    Person heinz = new Person("Heinz Kabutz", 32);
    change(heinz, "name", "Ng Keng Yap");
    change(heinz, "age", new Integer(27));
    change(heinz, "iq", new Integer(150));
    change(heinz, "country", "Malaysia");
    System.out.println(heinz);
  }
}

当我在JDK 1.2.2_014中运行它时,我得到了以下结果:

Ng Keng Yap, 27 of IQ=110 from Malaysia    Note, no exceptions, no complaints, and an incorrect IQ result. It seems that if we set a

在声明时基元的最后一个字段,如果类型是基元或 String,则值是内联的。

JDK 1.3.x 和 1.4.x

在 JDK 1.3.x 中,Sun 稍微收紧了访问,并阻止我们修改带有反射的最终字段。JDK 1.4.x 也是如此。如果我们尝试运行 FinalFieldChange 类,以便在运行时使用反射重新绑定最终字段,我们将得到:

java 版本 “1.3.1_12”: 异常线程 “main” IllegalAccessException: field is final at java.lang.reflect.Field.set(Native Method) at FinalFieldChange.change(FinalFieldChange.java:8) at FinalFieldChange.main(FinalFieldChange.java:12)

java 版本 “1.4.2_05” 异常线程 “main” IllegalAccessException: Field is final at java.lang.reflect.Field.set(Field.java:519) at FinalFieldChange.change(FinalFieldChange.java:8) at FinalFieldChange.main(FinalFieldChange.java:12)

JDK 5.x

现在我们来看看 JDK 5.x。FinalFieldChange 类具有与 JDK 1.2.x 中相同的输出:

Ng Keng Yap, 27 of IQ=110 from Malaysia    When Narve Saetre mailed me that he managed to change a final field in JDK 5 using

倒想,我希望一个错误已经悄悄进入了JDK。但是,我们都觉得这不太可能,尤其是这样一个根本性的bug。经过一番搜索,我找到了JSR-133:Java内存模型和线程规范。大多数规范都是难以阅读的,让我想起了我的大学时代(我曾经这样写过;-)但是,JSR-133 非常重要,因此所有 Java 程序员都应该需要阅读它。(祝你好运)

从第 25 页的第 9 章“最终字段语义”开始。具体而言,请阅读第9.1.1节“最终字段的施工后修改”。允许更新最终字段是有意义的。例如,我们可以放宽在 JDO 中具有非 final 字段的要求。

如果我们仔细阅读第 9.1.1 节,我们会发现,作为构建过程的一部分,我们只应修改最终字段。用例是,我们反序列化一个对象,然后一旦我们构造了该对象,我们就会初始化最终的字段,然后再将其传递。一旦我们将对象提供给另一个线程,我们就不应该使用反射来更改最终字段。结果是无法预测的。

它甚至这样说:如果在字段声明中将最终字段初始化为编译时常量,则可能不会观察到对最终字段的更改,因为在编译时,该最终字段的使用将替换为编译时常量。这就解释了为什么我们的iq字段保持不变,但国家/地区会发生变化。

奇怪的是,JDK 5 与 JDK 1.2.x 略有不同,因为您无法修改静态最终字段。

import java.lang.reflect.Field;

public class FinalStaticFieldChange {
  /** Static fields of type String or primitive would get inlined */
  private static final String stringValue = "original value";
  private static final Object objValue = stringValue;

  private static void changeStaticField(String name)
      throws NoSuchFieldException, IllegalAccessException {
    Field statFinField = FinalStaticFieldChange.class.getDeclaredField(name);
    statFinField.setAccessible(true);
    statFinField.set(null, "new Value");
  }

  public static void main(String[] args) throws Exception {
    changeStaticField("stringValue");
    changeStaticField("objValue");
    System.out.println("stringValue = " + stringValue);
    System.out.println("objValue = " + objValue);
    System.out.println();
  }
}

当我们使用JDK 1.2.x和JDK 5.x运行时,我们得到以下输出:

java 版本 “1.2.2_014”: stringValue = 原始值 objValue = new Value

java 版本 “1.5.0” 异常线程 “main” IllegalAccessException: Field is final at java.lang.reflect.Field.set(Field.java:656) at FinalStaticFieldChange.changeStaticField(12) at FinalStaticFieldChange.main(16)

那么,JDK 5 就像 JDK 1.2.x 一样,只是不一样吗?

结论

你知道 JDK 1.3.0 是什么时候发布的吗?我很难找到答案,所以我下载并安装了它。自述文件.txt日期为 2000/06/02 13:10。所以,它已经超过4岁了(天哪,感觉就像昨天一样)。JDK 1.3.0 在我开始编写 Java(tm) 专家通讯之前的几个月就发布了!我认为可以肯定地说,很少有Java开发人员能够记住JDK1.3.0之前的细节。啊,怀旧已今非昔比!您是否还记得第一次运行Java并收到此错误:“无法初始化线程:找不到类java / lang / Thread”?