33

考虑 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"!!!
4

6 回答 6

36

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

顺便说一句,如果你愿意,你可以再次确定它的范围:

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

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

现在 KEY 将返回“c”;

因为原版重读时很烂,所以编辑了。

于 2008-10-15T15:46:07.480 回答
20

尽管您正在隐藏变量,但知道您可以更改 java 中的 final 字段是非常有趣的,您可以在此处阅读:

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

来自挪威 Machina Networks 的 Narve Saetre 昨天给我发了条消息,提到很遗憾我们可以将手柄更改为最终阵列。我误解了他的意思,开始耐心地解释说我们不能让数组成为常量,而且没有办法保护数组的内容。“不,”他说,“我们可以使用反射来更改最终句柄。”

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

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 类。如果我们针对与我们编译的类不同的类运行,则在运行时没有访问检查。但是,我们无法在运行时使用类交换或反射重新绑定最终字段。

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

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

JDK 1.2.x

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

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

声明时基元的最终字段,如果类型是基元或字符串,则值是内联的。

JDK 1.3.x 和 1.4.x

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

java 版本“1.3.1_12”:异常线程“main”IllegalAccessException:字段在 java.lang.reflect.Field.set(Native Method) at FinalFieldChange.change(FinalFieldChange.java:8) 在 FinalFieldChange.main(FinalFieldChange.爪哇:12)

java 版本 "1.4.2_05" 异常线程 "main" IllegalAccessException: 字段在 java.lang.reflect.Field.set(Field.java:519) 在 FinalFieldChange.change(FinalFieldChange.java:8) 在 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。然而,我们都觉得这不太可能,尤其是这样一个根本性的错误。经过一番搜索,我找到了 JSR-133:Java 内存模型和线程规范。大部分规范都很难阅读,让我想起了我的大学时代(我曾经这样写;-)但是,JSR-133 非常重要,以至于所有 Java 程序员都应该阅读它。(祝你好运)

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

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

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

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

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 = 新值

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

于 2008-10-15T16:31:34.673 回答
4

看起来您的类只是隐藏变量,而不是覆盖它:

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

    public static void main(String args[])
    {
        A t = new A();
        System.out.println(t.KEY);
        System.out.println(((I) t).KEY);
    }
}

如您所见,这将打印“B”和“A”。您甚至可以分配给它,因为 A.KEY 变量未定义为 final。

 A.KEY="C" <-- this compiles.

但 -

public class C implements I{

    public static void main (String args[])
    {
        C t = new C();
        c.KEY="V"; <--- compiler error ! can't assign to final

    }
}
于 2008-10-15T15:52:02.897 回答
3

作为设计考虑,

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

静态方法总是返回父键。

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

    public String getKey() {
        return KEY; // returns "b"
    }

    public static String getParentKey(){
        return KEY; // returns "a"
    }
}

就像乔姆注意到的那样。使用重新定义的接口成员设计静态方法可能是一个严重的问题。一般来说,尽量避免对常量使用相同的名称。

于 2013-02-04T16:18:37.630 回答
2

您不应该以这种方式访问​​常量,而是使用静态引用:

I.KEY //returns "a"
B.KEY //returns "b"
于 2008-10-15T16:14:19.107 回答
1

静态字段和方法附加到声明它们的类/接口(尽管接口不能声明静态方法,因为它们是需要实现的完全抽象类)。

因此,如果您有一个带有公共静态 (vartype) (varname) 的接口,则该字段将附加到该接口。

如果您有一个实现该接口的类,编译器技巧会将 (this.)varname 转换为 InterfaceName.varname。但是,如果您的类重新定义了 varname,则将一个名为 varname 的新常量附加到您的类,并且编译器现在知道将 (this.)varname 转换为 NewClass.varname。这同样适用于方法:如果新类没有重新定义方法,(this.)methodName 被翻译成 SuperClass.methodName,否则,(this.)methodName 被翻译成 CurrentClass.methodName。

这就是为什么您会遇到警告“应以静态方式访问 x 字段/方法”的原因。编译器告诉你,虽然它可能使用这个技巧,但它更喜欢你使用 ClassName.method/fieldName,因为它更明确地提高了可读性。

于 2008-10-15T15:58:21.323 回答