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 で最終フィールドを変更できることを知ることは非常に興味深いことです。

Java 5 - 「最終」はもはや最終ではありません

昨日、ノルウェーの Machina Networks の Narve Saetre から、ハンドルを final 配列に変更できるのは残念だというメモが届きました。私は彼を誤解し、配列を定数にすることはできず、配列の内容を保護する方法がないことを辛抱強く説明し始めました。「いいえ」と彼は言いました。「リフレクションを使用して最終ハンドルを変更できます。」

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

JDK1.1.x

JDK 1.1.x では、リフレクションを使用してプライベート フィールドにアクセスできませんでした。ただし、パブリック フィールドを持つ別の Person を作成し、それに対してクラスをコンパイルして、Person クラスを交換することはできます。コンパイルしたクラスとは異なるクラスに対して実行している場合、実行時にアクセス チェックは行われませんでした。ただし、クラス スワッピングまたはリフレクションを使用して、実行時に final フィールドを再バインドできませんでした。

java.lang.reflect.Field の JDK 1.1.8 JavaDocs には、次のように記載されていました。

  • この Field オブジェクトが Java 言語アクセス制御を適用し、基になるフィールドにアクセスできない場合、メソッドは IllegalAccessException をスローします。
  • 基になるフィールドが final の場合、メソッドは IllegalAccessException をスローします。

JDK1.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

宣言時のプリミティブの final フィールド。型がプリミティブまたは文字列の場合、値はインライン化されます。

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: フィールドは、FinalFieldChange.main(FinalFieldChange.ジャワ:12)

Java バージョン "1.4.2_05" 例外スレッド "main" IllegalAccessException: フィールドは FinalFieldChange.main(FinalFieldChange.java:8) で java.lang.reflect.Field.set(Field.java:519) で最終ですFinalFieldChange.java:12)

JDK5.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 Memory Model and Thread Specification を見つけました。仕様の大部分は読みにくいので、大学時代を思い出します (以前はそのように書いていました ;-) しかし、JSR-133 は非常に重要であるため、すべての Java プログラマーが必ず読む必要があります。(幸運を)

25 ページの第 9 章「最終フィールド セマンティクス」から始めます。具体的には、セクション 9.1.1 最終フィールドの構築後の変更をお読みください。final フィールドの更新を許可することは理にかなっています。たとえば、JDO で非 final フィールドを持つという要件を緩和できます。

セクション 9.1.1 を注意深く読むと、構築プロセスの一部として final フィールドのみを変更する必要があることがわかります。ユースケースは、オブジェクトをデシリアライズし、オブジェクトを構築したら、それを渡す前に最終フィールドを初期化する場合です。オブジェクトを別のスレッドで使用できるようにしたら、リフレクションを使用して final フィールドを変更しないでください。結果は予測できません。

それは次のようにも言っています: final フィールドがフィールド宣言でコンパイル時の定数に初期化されている場合、その final フィールドの使用はコンパイル時にコンパイル時の定数に置き換えられるため、final フィールドへの変更は観察されない可能性があります。これは、iq フィールドが同じままであるのに、国が変わる理由を説明しています。

奇妙なことに、JDK 5 は JDK 1.2.x とは少し異なり、 static 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) Specialists' Newsletter を書き始める数か月前にリリースされました。JDK1.3.0 より前の詳細を覚えている Java 開発者はほとんどいないと言っても過言ではありません。ああ、懐かしさは以前のものではありません!初めて 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

静的フィールドとメソッドは、それらを宣言するクラス/インターフェイスにアタッチされます (ただし、実装する必要がある完全に抽象クラスであるため、インターフェイスは静的メソッドを宣言できません)。

そのため、public static (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 に答える