java 为什么Java中的final常量可以被覆盖?

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/205239/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-10-29 11:25:04  来源:igfitidea点击:

Why can final constants in Java be overridden?

javainterfaceoverridingfinal

提问by Yuval Adam

Consider the following interface in Java:

考虑以下 Java 接口:

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

And the following class:

以及以下课程:

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

    public String getKey() {
        return KEY;
    }
}

Why is it possible for class A to come along and override interface I's final constant?

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

Try for yourself:

自己试试:

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

采纳答案by André

Despite the fact that you are shadowing the variable it's quite interesting to know that you can change final fields in java as you can read here:

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

Java 5 - "final" is not final anymore

Narve Saetre from Machina Networks in Norway sent me a note yesterday, mentioning that it was a pity that we could change the handle to a final array. I misunderstood him, and started patiently explaining that we could not make an array constant, and that there was no way of protecting the contents of an array. "No", said he, "we can change a final handle using reflection."

I tried Narve's sample code, and unbelievably, Java 5 allowed me to modify a final handle, even a handle to a primitive field! I knew that it used to be allowed at some point, but that it was then disallowed, so I ran some tests with older versions of Java. First, we need a class with final fields:

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

In JDK 1.1.x, we were not able to access private fields using reflection. We could, however, create another Person with public fields, then compile our class against that, and swap the Person classes. There was no access checking at runtime if we were running against a different class to the one that we compiled against. However, we could not rebind final fields at runtime using either class swapping or reflection.

The JDK 1.1.8 JavaDocs for java.lang.reflect.Field had the following to say:

  • If this Field object enforces Java language access control, and the underlying field is inaccessible, the method throws an IllegalAccessException.
  • If the underlying field is final, the method throws an IllegalAccessException.

JDK 1.2.x

In JDK 1.2.x, this changed a bit. We could now make private fields accessible with the setAccessible(true) method. Access of fields was now checked at runtime, so we could not use the class swapping trick to access private fields. However, we could now suddenly rebind final fields! Look at this code:

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);
  }
}

When I ran this in JDK 1.2.2_014, I got the following result:

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

final field of a primitive at declaration time, the value is inlined, if the type is primitive or a String.

JDK 1.3.x and 1.4.x

In JDK 1.3.x, Sun tightened up the access a bit, and prevented us from modifying a final field with reflection. This was also the case with JDK 1.4.x. If we tried running the FinalFieldChange class to rebind the final fields at runtime using reflection, we would get:

java version "1.3.1_12": Exception thread "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 version "1.4.2_05" Exception thread "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

Now we get to JDK 5.x. The FinalFieldChange class has the same output as in 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

reflection, I was hoping that a bug had crept into the JDK. However, we both felt that to be unlikely, especially such a fundamental bug. After some searching, I found the JSR-133: Java Memory Model and Thread Specification. Most of the specification is hard reading, and reminds me of my university days (I used to write like that ;-) However, JSR-133 is so important that it should be required reading for all Java programmers. (Good luck)

Start with chapter 9 Final Field Semantics, on page 25. Specifically, read section 9.1.1 Post-Construction Modification of Final Fields. It makes sense to allow updates to final fields. For example, we could relax the requirement to have fields non-final in JDO.

If we read section 9.1.1 carefully, we see that we should only modify final fields as part of our construction process. The use case is where we deserialize an object, and then once we have constructed the object, we initialise the final fields, before passing it on. Once we have made the object available to another thread, we should not change final fields using reflection. The result would not be predictable.

It even says this: If a final field is initialized to a compile-time constant in the field declaration, changes to the final field may not be observed, since uses of that final field are replaced at compile time with the compile-time constant. This explains why our iq field stays the same, but country changes.

Strangely, JDK 5 differs slightly from JDK 1.2.x, in that you cannot modify a static final field.

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();
  }
}

When we run this with JDK 1.2.x and JDK 5.x, we get the following output:

java version "1.2.2_014": stringValue = original value objValue = new Value

java version "1.5.0" Exception thread "main" IllegalAccessException: Field is final at java.lang.reflect.Field.set(Field.java:656) at FinalStaticFieldChange.changeStaticField(12) at FinalStaticFieldChange.main(16)

So, JDK 5 is like JDK 1.2.x, just different?

Conclusion

Do you know when JDK 1.3.0 was released? I struggled to find out, so I downloaded and installed it. The readme.txt file has the date 2000/06/02 13:10. So, it is more than 4 years old (goodness me, it feels like yesterday). JDK 1.3.0 was released several months before I started writing The Java(tm) Specialists' Newsletter! I think it would be safe to say that very few Java developers can remember the details of pre-JDK1.3.0. Ahh, nostalgia isn't what it used to be! Do you remember running Java for the first time and getting this error: "Unable to initialize threads: cannot find class java/lang/Thread"?

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

java.lang.reflect.Field 的 JDK 1.1.8 JavaDocs 有以下内容:

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

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

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

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) at FinalFieldChange.main(FinalFieldChange.爪哇:12)

java版本“1.4.2_05”异常线程“main”IllegalAccessException:Field是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。但是,我们都认为这不太可能,尤其是这样一个基本错误。经过一番搜索,我找到了 JSR-133:Java 内存模型和线程规范。大部分规范都很难阅读,让我想起了我的大学时代(我曾经这样写 ;-) 然而,JSR-133 非常重要,所有 Java 程序员都应该阅读它。(祝你好运)

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

如果我们仔细阅读第 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是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 在我开始撰写 The Java(tm) Specialists' Newsletter 前几个月发布了!我认为可以肯定地说,很少有 Java 开发人员能记住 JDK1.3.0 之前的细节。啊,怀旧已经不是以前的样子了!你还记得第一次运行 Java 并收到这个错误:“无法初始化线程:找不到类 java/lang/Thread”?

回答by Bill K

You are hiding it, it's a feature of "Scope". Any time you are in a smaller scope, you can redefine all the variables you like and the outer scope variables will be "Shadowed"

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

By the way, you can scope it again if you like:

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

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

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

Now KEY will return "c";

现在KEY将返回“c”;

Edited because the original sucked upon re-reading.

编辑是因为原版在重新阅读时很烂。

回答by Steve B.

It looks like your class is simply hiding the variable, not overwriting it:

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

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);
    }
}

This will print "B", and "A", as you found. You can even assign to it, as the A.KEY variable is not defined as final.

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

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

But -

但 -

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

    }
}

回答by Jorn

You should not access your constant in this way, use the static reference instead:

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

I.KEY //returns "a"
B.KEY //returns "b"

回答by Arnaldo Ignacio Gaspar Véjar

As a design consideration,

作为设计考虑,

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

The static methods always returns the parent key.

静态方法总是返回父键。

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

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

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

Just like Jom has noticed. The design of static methods using re-defined interface members could be a heavy problem. In general, try to avoid use the same name for the constant.

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

回答by MetroidFan2002

Static fields and methods are attached to the class/interface declaring them (though interfaces cannot declare static methods as they are wholly abstract classes which need to be implemented).

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

So, if you have an interface with a public static (vartype) (varname), that field is attached to that interface.

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

If you have a class implement that interface, the compiler trick transforms (this.)varname into InterfaceName.varname. But, if your class redefines varname, a new constant named varname is attached to your class, and the compiler knows to now translate (this.)varname into NewClass.varname. The same applies for methods: if the new class does not re-define the method, (this.)methodName is translated into SuperClass.methodName, otherwise, (this.)methodName is translated into CurrentClass.methodName.

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

This is why you will encounter the warning "x field/method should be accessed in a static way". The compiler is telling you that, although it may use the trick, it would prefer that you used ClassName.method/fieldName, because it is more explicit for readability purposes.

这就是为什么您会遇到警告“应以静态方式访问 x 字段/方法”的原因。编译器告诉您,尽管它可能会使用该技巧,但它更愿意您使用 ClassName.method/fieldName,因为出于可读性目的,它更明确。