Bits of Java (トップ)

VM   JDK と JRE
Language   オブジェクトとインスタンス   this と super   static   フィールドアクセス
IO   Serializable の実装
Swing   MetalLookAndFeel   イメージパネル
JavaBeans   プロパティ名について   XMLEncoder で保存
その他   正規表現テストアプリ   秀丸の強調表示   Ant のインストール   文字セット変換 Ant タスク   XAMPP + Tomcat

フィールドアクセス

Java におけるフィールドとはクラスやインターフェイスで宣言された変数の事で、インスタンス変数かクラス変数のどちらかになります。 クラス変数はキーワード static で修飾されて宣言されたフィールドで "static フィールド"、 "static 変数" などとも呼ばれます。 インターフェイスで宣言されたフィールドは暗黙で public static final なのですべて定数になりますが、定数は static なのでクラス変数に分類されます。 クラスにおいて static 指定されずに宣言されたフィールドはすべてインスタンス変数です。 実際のプログラミングでフィールドアクセスについて疑問が生じる事は少ないと思いますが以下の項目別にまとめてみました。
(このページの Java 言語規定 第3版 の日本語訳やリンクは Java 言語規定 第2版の日本語訳ですが、引用している部分の原文が第3版と第2版で同じである事を確認しています。)


フィールドの隠蔽と継承

フィールドの "隠蔽 (hide)" と "継承" に関しては Java言語規定 第3版 8.3 ( 日本語訳 ) に以下のような記述があります。

8.3 フィールド宣言
...
...

クラスがある特定の名前をもつフィールドを宣言した場合,そのクラスの上位クラス及び上位インタフェースの中の,それと同じ名前をもつありとあらゆるアクセス可能なフィールドの宣言を隠ぺい(hide)すると呼ぶ。フィールド宣言は,また,取り囲むクラス又はインタフェースの任意のアクセス可能フィールド,ならびに,取り囲むブロックの同じ名前をもつ局所変数,形式メソッド仮引数及び例外ハンドラ仮引数の宣言をおおい隠す(6.3.1)。

あるフィールド宣言が他のフィールド宣言を隠ぺいする場合,その二つのフィールドが同じ型をもつ必要はない。

クラスは,その直接的上位クラス及び直接的上位インタフェースから,そのクラス内のコードでアクセス可能であり,そのクラス内の宣言で隠ぺいされていない,上位クラス及び上位インタフェースの中のすべての非privateフィールドを継承する。

隠蔽 (hide) に関してはインスタンス変数なのかクラス変数なのか、また変数の型が同じかどうかなどは関係なく、あるクラスにおけるフィールドの宣言はその上位クラスや上位インターフェイス (実装しているすべてのインターフェイス) の同名のフィールドをすべて隠蔽します。 フィールドを宣言したクラスがネストされたクラスの場合にはそのクラスの外側に宣言されていた単純名でアクセス可能であったフィールド、ローカル変数、メソッドの仮引数などはおおい隠されます (shadowed) 。 単純名によるフィールドアクセスとは限定式名がオブジェクトの参照を表す式やクラス名やインターフェイス名とそれに続く . (ドット) とフィールド名で構成されるのに対し、フィールド名のみでアクセスする事を指します。 コンパイラは単純名によるアクセスは新たに宣言された方のフィールドに対するものと解釈し、隠された方のフィールドは単純名ではコンパイラから見えなくなります。 "隠蔽" と "おおい隠し" の違いですが "隠蔽" の方は本来なら継承されるはずの上位クラスや上位インターフェイスのフィールドと同名のフィールドを宣言してそれらを隠す場合に使われます。 隠蔽されたフィールドは継承されません。 "おおい隠し" の方は変数の宣言によってその変数のスコープでは同じ名前の変数 (フィールド,局所変数,メソッド仮引数,コンストラクタ仮引数,例外ハンドラ仮引数) が存在する場合にそれらを隠し、単純名でのアクセスをできなくします。

下のプログラムは上位クラスのフィールドを隠蔽する例です。

public class A {
    public String value = "100";
}

public class B extends A {
    public int value = 100;
}

public class C extends B {}

public class Main {
    public static void main(String[] args) {
        C c = new C();
        String value = c.value;
    }
}
上のクラスをまとめてコンパイルすると Sun の jdk1.5.0 のコンパイラでは以下のようなエラーメッセージが表示されます。
D:\pg\java\temp\field\hide>javac *.java
Main.java:4: 互換性のない型
検出値  : int
期待値  : java.lang.String
        String value = c.value;
                        ^
エラー 1 個
B におけるフィールド value の宣言で A の value は隠蔽されています。 隠蔽されたフィールドは継承されない事から C 型の変数を通して value にアクセスしている場合には C では value を宣言していないので B から継承した value に対するアクセスになります。 上の場合には B の value が int 型なのに String 型の変数に代入しようとしているのでコンパイルエラーとなっています。


オブジェクトのコンパイル時の型について

インスタンス変数はオブジェクトが保持しているので同じクラスのインスタンスでもそれぞれのオブジェクトごとに独自の値や参照を格納できますが、クラス変数はロードしたクラスごとに JVM 上に1つしか存在しません。 通常、クラス変数へのアクセスは自身のクラスで宣言しているか、継承している場合は単純名でアクセスしますがそれ以外の場合にはクラス名やインターフェイス名を使ってアクセスします。

下はクラス変数にクラス名を使ってアクセスしているプログラムです。

public class A {
    public static String name = "A";
}

public class B extends A {
    public static String name = "B";
}

public class C extends B {}

public class Main {
    public static void main(String[] args) {
        System.out.println(A.name);
        System.out.println(B.name);
        System.out.println(C.name);
    }
}
Main を実行すると以下が標準出力に表示されます。
D:\pg\java\temp\field\static>java Main
A
B
B
クラス名やインターフェイス名を使ってクラス変数にアクセスする場合は指定したクラスやインターフェイスがコンパイラや JVM によるフィールド検索の起点となります。検索の起点というのは指定したクラスやインターフェイスで宣言している場合の他に上位クラスや上位インターフェイスから継承している場合もあるからです。

例えば

    Foo.someField
これはクラスまたはインターフェイス Foo で宣言しているか、継承しているフィールド someField を表します。 もし宣言も継承もしていなければコンパイルエラーとなり、実行時の場合は java.lang.NoSuchFieldError が発生します。 Foo がインターフェイスではなくクラスの場合には上位クラスと Foo のクラス宣言において implements 節で実装したインターフェイスの両方から someField を継承している場合があります。 このような場合には上のアクセスは曖昧なフィールド指定としてコンパイルエラーになります。 someField を宣言しているクラスやインターフェイスでアクセスすることで、このような曖昧性はなくなります。 コンパイル時のフィールドの検索は名前のみで行われ、フィールドの型やアクセスレベル、クラス変数かインスタンス変数かなどは検索時には無視され、名前により someField を特定した後でそれらをチェックし、不正な場合にはコンパイルエラーとなります。 実行時のフィールド検索はフィールド名とフィールドの型によって行われます。

インスタンス変数へのアクセスではアクセス対象のオブジェクトのコンパイル時の型がコンパイラや JVM によるフィールド検索の起点となります。 オブジェクトの "コンパイル時の型" とはインスタンス生成時に雛形となったクラスを "実行時の型" と呼ぶのに対して、そのオブジェクトの参照を保持する変数や、仮引数、戻り値の型、あるいはキャスト演算子でキャストされた型などのソースコードにおける静的な型を指して呼びます。

public class Main {
    public static void show(Object obj) {
        System.out.println(obj.toString());
    }
    public static void main(String[] args) {
        Integer value = new Integer(10);
        show(value);
    }
}
show メソッドの obj の "コンパイル時の型" は常に仮引数として宣言されている型である Object 型です。 これに対して実行時に main メソッドから呼び出している show メソッドの obj が参照しているのは main メソッド内で生成された Integer オブジェクトです。 この場合に obj が参照しているオブジェクトの "実行時の型" は Integer 型になります。

下のプログラムはインスタンス変数の値を取得する場合にオブジェクトのコンパイル時の型に合わせてアクセス対象の変数が違ってくる例です。

public class A { public int value = 1; }

public class B extends A { public int value = 10; }

public class C extends B { public int value = 100; }

public class D extends C { public int value = 1000; }

public class Main {
    private static void show(A a) {
        System.out.println(a.value);
    }
    private static B toB(D d) {
        return d;
    }
    public static void main(String[] args) {
        D d = new D();
        System.out.println(d.value);
        System.out.println(((C)d).value);
        System.out.println(toB(d).value);
        show(d);
    }
}
Main を実行すると以下が標準出力に表示されます。
D:\pg\java\temp\field\hide>java Main
1000
100
10
1
main メソッドでは D のインスタンスを生成してローカル変数 d にその参照を保持させています。 この D オブジェクトは A, B, C, D のそれぞれのクラスで宣言された4つの value というインスタンス変数を持っています。 前述のようにオブジェクトを通したフィールドアクセスではオブジェクトのコンパイル時の型がフィールド検索の起点となるので、 D オブジェクトのコンパイル時の型を A や B あるいは C にすることでそれぞれのクラスで宣言した value にアクセスできます。
        System.out.println(d.value);
ローカル変数 d は D 型として宣言されているのでコンパイル時の型は D になります。 よって d.value は d が参照している D オブジェクトから "D で宣言しているか D が継承している value " の値を取得する事になり、標準出力に 1000 が表示されます。
        System.out.println(((C)d).value);
キャストした場合にはキャスト後の型がコンパイル時の型になるので (C)d のコンパイル時の型は C になります。 ((C)d).value は d が参照している D オブジェクトから "C で宣言しているか C が継承している value " の値を取得する事になり、標準出力に 100 が表示されます。
        System.out.println(toB(d).value);
toB メソッドは引数をそのまま返すだけですが、戻り値の型は B なので toB(d) が返すオブジェクトのコンパイル時の型も B になります。 その結果 toB(d).value は d が参照している D オブジェクトから "B で宣言しているか B が継承している value " の値を取得する事になり、標準出力に 10 が表示されます。
        show(d);
show メソッドは仮引数の型が A でその value の値を標準出力に表示します。 a のコンパイル時の型は仮引数の A なので main メソッドの show(d) は d が参照している D オブジェクトから "A で宣言しているか A が継承している value " の値を取得する事になり、標準出力に 1 が表示されます。

少し話しがそれますが D において value を宣言する事で A や B や C で宣言された value は隠蔽されています。 このような場合の A, B, C で宣言された value に対する表現に関して Java言語規定 第3版 8.3.3.2 ( 日本語訳 ) に以下のような記述がありました。

その理由は,クラスTest内のxの宣言は,クラスPoint内のxの定義を隠ぺいし,クラスTestは,その上位クラスPointからフィールドxを継承しないためである。しかしながら,クラスPointのフィールド x は,クラスTestによって継承(inherited)されていないが,それにもかかわらず,クラス Testのインスタンスで実装(implemented)されていることに注意しなければならない。
これにならうと D のインスタンスは A や B や C の value を継承 (inherited) していないが実装 (implemented) している と、表現するようです。

次にオブジェクトを通してクラス変数にアクセスしているコードに関して触れたいと思います。 クラス変数にはそれを宣言しているクラス名やインターフェイス名を使ってアクセスする事が推奨されています。 (Java言語コーディング規約  日本語訳)
言語として禁止しているわけではないのでオブジェクトを通したクラス変数へのアクセスも可能であり、その場合にはオブジェクトのコンパイル時の型が検索の起点となる型になります。

下はオブジェクトを通したクラス変数へのアクセスを行っているプログラムです。

public class A { public static String name = "A"; }

public class B extends A { public static String name = "B"; }

public class Main {
    public static void main(String[] args) {
        B b = new B();
        A a = b;
        System.out.println(b.name);
        System.out.println(a.name);
        a = null;
        System.out.println(a.name);
        System.out.println(A.name);
    }
}
Main を実行すると以下が標準出力に表示されます。
D:\pg\java\temp\field\static>java Main
B
A
A
A
ローカル変数 b は宣言している型が B なのでコンパイル時の型も B です。 よって b.name は b が参照している B オブジェクトから B で宣言されているか、 B が継承している name という名前のフィールドを表します。

ローカル変数 a は代入によって b が保持しているのと同じ B オブジェクトの参照を保持しますが、宣言している型は A なのでコンパイル時の型は A になります。 よって a.name は a が参照している B オブジェクトから A で宣言されているか、 A が継承している name という名前のフィールドを表します。

        a = null;
        System.out.println(a.name);
main メソッドの上の部分では NullPointerException は発生しません。 ソースコードでオブジェクトを通したクラス変数や static メソッドへのアクセスを記述しても実行時のアクセスではオブジェクトの参照は必要なく、使用されていません。よって参照が null かどうかの検査も行われない為、 NullPointerException もスローされません。 ( Java言語規定 第3版 15.11.1 )

        System.out.println(A.name);
前述のようにクラス変数や static メソッドへのアクセスする際にはそれらを宣言しているクラス名やインターフェイス名を使ってアクセスする事が推奨されています。 クラス名やインターフェイス名を使ったアクセスの場合はソースコードを読む際にクラス変数や static メソッドへのアクセスである事が明白で、コンパイラも見つかったのがインスタンス変数やインスタンスメソッドの場合にはエラーで知らせてくれます。 オブジェクトを通したクラス変数や static メソッドへアクセスするコードはプログラマが想定していたクラス変数や static メソッドを隠蔽している別のフィールドや static メソッドにアクセスしてしまう可能性があります。 また仕様なのかコンパイラの実装に依存する部分なのかは分かりませんが Sun の jdk1.5 のコンパイラによって生成されたクラスファイルのバイトコードでは a.name は A.name というコードと比べると、変数 a の中身をオペランドスタックにプッシュして、すぐポップするという無駄な2つの命令が行われています。 これはオブジェクトを通した static メソッドへのアクセスの場合も同様です。 このような余計な処理を行わないという点からもクラス変数や static メソッドへのアクセスする際には自身のクラスで宣言しているか、継承している場合は単純名でアクセスしますが、それ以外の場合にはそれらを宣言しているクラス名やインターフェイス名を使ってアクセスするべきです。


単純名によるフィールドアクセスの解決

単純名によるフィールドへのアクセスはコンパイラが解決しています。 あるクラス Foo において単純名で value という変数 にアクセスしている場合にコンパイラはまず Foo クラスに value がないか探します。 Foo クラスに value があるというのは Foo クラスに value という名前のフィールドが宣言されているか上位クラスや上位インターフェイスから継承している場合です。 Foo クラスに value があればアクセス対象はその value と解釈されます。 Foo クラスに value がなければ Foo のすぐ外側のクラスから順にトップクラスである一番外側のクラスまで Foo と同様に value がないか探していき、最初に見つかった value がアクセス対象と解釈されます。 見つかった value の型がもし予期していた型でなければコンパイルエラーとなります。 一番外側のトップクラスまで調べても value が見つからなければコンパイルエラーとなります。

下は外側のクラスのフィールドに単純名でアクセスしているプログラムです。

/******************** Bar.java ********************/
package bar;

public class Bar {
    protected String barName = "Bar";
}

/******************** Foo.java ********************/
public class Foo extends bar.Bar {
    String fooName = "Foo";
    class Inner {
        String name = "Inner";
        public void show() {
            System.out.println(name);
            System.out.println(fooName);
            System.out.println(barName);
        }
    }

    public static void main(String[] args) {
        new Foo().new Inner().show();
    }
}
カレントディレクトリに Bar.java を配置した bar ディレクトリと Foo.java ファイルがあるとしてコンパイル、実行すると以下のようになります。
D:\pg\java\temp\field\simpleName>javac bar/Bar.java Foo.java D:\pg\java\temp\field\simpleName>java Foo Inner Foo Bar
Foo.Inner#show メソッド内の barName は Foo.Inner では宣言されておらず、 Object クラスから継承もしていないので外側の Foo で宣言あるいは継承していないか、コンパイラは探します。 Foo ではスーパークラスの bar.Bar からフィールド barName を継承していますのでこの barName に決定し、コンパイラによる検索は終了します。 ここで Bar では barName を protected 指定していますが Foo.Inner は Bar と同じパッケージでもありませんし、サブクラスでもない為にアクセスできないのでは? という疑問が起きないでしょうか。 結論から言うとコンパイルエラーにならない事からも分かりますが問題ありません。 protected は同じパッケージ内とサブクラスからアクセス可能と解釈されていますが、厳密には同じパッケージ内とサブクラスの宣言内からアクセス可能になります。 よって Bar で protected 指定された barName はサブクラス Foo にネストされた Foo.Inner からもアクセス可能です。

このアクセス制御に関する定義は以下のように記述されています。 Java言語規定 第3版 6.6.1 Determining Accessibility ( 日本語訳 )

6.6.1 アクセス可能性の決定

private も宣言したクラスからのみアクセス可能と解釈される事が多いですが、厳密にはトップクラスの本体内であればどの private メンバにもアクセス可能です。 下はその例です。
public class Outer {
    private class Inner {
        private int value;
    }
    class Inner2 extends Inner {
        void getValue() {
            System.out.println(value);
            //System.out.println(((Inner)this).value);
        }
    }
}
このコードは以下のようにコンパイルエラーとなります。
D:\pg\java\temp\innerClass\accessTest>javac Outer.java
Outer.java:9: value は Outer.Inner で private アクセスされます。
            System.out.println(value);
                               ^
エラー 1 個
Inner の value は private 宣言されているのでサブクラスの Inner2 は value を継承していません。 よって上のコードのように単純名でアクセスしようとするとコンパイルエラーになります。 ですが Inner も Inner2 も同じトップクラス Outer 内に定義されているので private メンバでもアクセス可能です。 つまり Inner2 において Inner の value は実装はしているが継承はしていない状態と言えます。 よってコメントアウトしている方のコードのように this の型を Inner にキャストすることでアクセス可能になります。


コンスタント変数のインライン展開

Java で定数 (constant) というと Java言語規定 第3版 6.8.6 ( 日本語訳 ) では static final なフィールドを指し、大文字とアンダースコアで構成される名前が推奨されています。 Effective Java(書籍 ピアソンエデュケーション刊)ではさらにフィールドの型がプリミティブ型であるか、参照型の場合にはイミュータブルオブジェクト (生成後に内部状態を変更できないオブジェクト→ String や Integer オブジェクトなど) への参照を保持する場合のみ定数フィールドとする、と記述があります。 通常 Java で定数といえば Effective Java で定義されている方を指すと思います。

定数とは別にコンスタント変数 (constant variable) というものもあります。 コンスタント変数とは宣言時に定数式で初期化されている final フィールドを指します (コンスタント変数という表現はどう訳せばよいのか分からなかったので私がテキトーに付けた名前なので使わない方が無難です)。 Java言語規定では定数フィールド (constant field) という用語も使われていますが、こちらはフィールドを定数である事実で修飾した表現でコンスタント変数とは別の意味になります。

コンスタント変数を参照している部分はコンパイル時にコンパイラによって値に置き換えられます Java言語規定 第3版 13.1 には以下のような記述があります。

References to fields that are constant variables are resolved at compile time to the constant value that is denoted.
コンスタント変数であるフィールドへの参照はコンパイル時に示されている定数値に解決される。
定数式に関しては以下のように定義されています。
15.28 定数式
コンパイル時の定数式 (constant expression) は,次のものだけを使用して構成されるプリミティブ型の値又は String を表す式とする。

下はコンスタント変数のインライン展開に関するプログラムです。

public class Foo {
    public static final int CONST_EXPR_1 = 1;
    public        final int constExpr2   = 11;
    public        final int constExpr3   = constExpr2 * 10 + CONST_EXPR_1;
    public static final int CONST_EXPR_4 = (CONST_EXPR_1 > 0) ? 1111 : 0;

    public static final int INCONST_EXPR_1;
    public        final int inconstExpr2;
    public        final int inconstExpr3   = this.CONST_EXPR_1 + 1111110;
    public static final int INCONST_EXPR_4 = getCONST_EXPR_1() + 11111110;
    public static final int INCONST_EXPR_5 = (CONST_EXPR_1 > 0)
                                             ? 111111111
                                             : getCONST_EXPR_1();

    static {
        INCONST_EXPR_1 = 11111;
    }

    public Foo() {
        inconstExpr2 = 111111;
    }

    public static int getCONST_EXPR_1() {
        return CONST_EXPR_1;
    }
}

public class Main {
    public static void main(String[] args) {
        System.out.println(Foo.CONST_EXPR_1);
        Foo foo = new Foo();
        System.out.println(foo.constExpr2);
        System.out.println(foo.constExpr3);
        System.out.println(Foo.CONST_EXPR_4);
        System.out.println(Foo.INCONST_EXPR_1);
        System.out.println(foo.inconstExpr2);
        System.out.println(foo.inconstExpr3);
        System.out.println(Foo.INCONST_EXPR_4);
        System.out.println(Foo.INCONST_EXPR_5);
    }
}
コンパイルして Main を実行すると以下が標準出力に表示されます。

D:\pg\java\temp\field\const>javac Foo.java Main.java

D:\pg\java\temp\field\const>java Main
1
11
111
1111
11111
111111
1111111
11111111
111111111
次に Foo のソースコードを以下のように修正します。
public class Foo {
    public static final int CONST_EXPR_1   = 0;
    public        final int constExpr2     = 0;
    public        final int constExpr3     = 0;
    public static final int CONST_EXPR_4   = 0;
    public static final int INCONST_EXPR_1 = 0;
    public        final int inconstExpr2   = 0;
    public        final int inconstExpr3   = 0;
    public static final int INCONST_EXPR_4 = 0;
    public static final int INCONST_EXPR_5 = 0;
}

Foo.java のみコンパイルして Main を実行すると以下が標準出力に表示されます。
D:\pg\java\temp\field\const>javac Foo.java

D:\pg\java\temp\field\const>java Main
1
11
111
1111
0
0
0
0
0
CONST_EXPR_1, constExpr2, constExpr3, CONST_EXPR_4 は宣言時に定数式で初期化されている final フィールドなのでコンスタント変数です。
    public static final int CONST_EXPR_1 = 1;
    public        final int constExpr2   = 11;
    public        final int constExpr3   = constExpr2 * 10 + CONST_EXPR_1;
    public static final int CONST_EXPR_4 = (CONST_EXPR_1 > 0) ? 1111 : 0;
Main のソースコードでこれらのコンスタント変数を参照している部分はコンパイル時に変数の値そのものに置き換えられます。 よって Foo のソースコードでこれらのコンスタント変数の値を変更してから Foo のみを再コンパイルしても Main に展開された Foo のコンスタント変数の値は以前のままなので、変更はプログラムに反映されません。

残りのフィールドはすべてコンスタント変数ではないので Main のクラスファイルにはインライン展開されておらず、 Foo のソースにおけるこれらのフィールドの値の変更は Foo のみのコンパイルでプログラムに反映されます。

    public static final int INCONST_EXPR_1;
    public        final int inconstExpr2;
INCONST_EXPR_1, inconstExpr2 はブランク (blank) final 指定されています。 Java ではもともと final フィールドは宣言時に必ず初期化する必要がありましたが jdk 1.1 から初期化を宣言時ではなく、 static ブロック (静的初期化子) やコンストラクタ、インスタンス初期化子などで行う事ができる final 指定が可能になり、これをブランク final と呼びます。 このブランク final として宣言されたフィールドはコンスタント変数にはなりません。 (この事はプログラム実行により確認した事で Java 言語規定 第3版では明確な記述が見つけられませんでした。あるいはブランク final は条件分岐やループ、コンストラクタの引数を使うなど変数初期化子で記述できないような処理を行う為に用意されているので、定数式で初期化する場合には変数初期化子で初期化を行うのが当たり前であり、ブランク final である変数をコンスタント変数に含めないのは特にことわる必要のないくらい当然の感覚なのでしょうか。)
        public final int inconstExpr3 = this.CONST_EXPR_1 + 1111110;
CONST_EXPR_1 はコンスタント変数ですが this を通してアクセスしており、単純名ではないので定数式の定義に反し、 inconstExpr3 はコンスタント変数にはなりません。 もし右辺が
    CONST_EXPR_1 + 1111110
あるいは
    Foo.CONST_EXPR_1 + 1111110
であればコンスタント変数となります。 同様の理由で switch 文の case にコンスタント変数を指定する場合には単純名か クラス名またはインターフェイス名を使った限定名でなければコンパイルエラーになります (this も使えません) 。
    public static final int INCONST_EXPR_4 = getCONST_EXPR_1() + 11111110;
メソッドの呼び出しは定数式の定義に反するので INCONST_EXPR_4 はコンスタント変数ではありません。
    public static final int INCONST_EXPR_5 = (CONST_EXPR_1 > 0)
                                             ? 111111111
                                             : getCONST_EXPR_1();
定数式の定義にあるように三項演算子を使うことは可能ですが三項演算子で使用する3つの式はすべて定数式である必要があります。 INCONST_EXPR_5 はメソッドの呼び出しを行っているのでコンスタント変数にはなりません。


バイトコードにおけるフィールドアクセスを行う命令

Java ではフィールドに対するアクセスは以下の4つに分類されます。 ( ) 内はバイトコードにおける命令です。

プログラム実行時の JVM におけるフィールドアクセスではアクセス対象のフィールドの指定は検索の起点となるクラスまたはインターフェイスの型とフィールド名とフィールドの型の3つで行われます。 起点となるクラスまたはインターフェイスの型はコンパイラが決定しています。 アクセス対象のフィールドの型もコンパイル時にコンパイラが見つけたフィールドの型に決定されています。 コンパイル時は起点となるクラスまたはインターフェイスの型とフィールド名の2つの情報でフィールドを検索しましたが、実行時はこれに加えてフィールドの型も検索条件に含まれます。 (Java仮想マシン仕様 第2版 5.4.3.2) よって JVM におけるフィールドアクセスに関しては、フィールド名のみで検索しフィールドの型を考慮しない "継承や隠蔽" というコンパイル時の概念は適用できないと思います (コンパイル時にはフィールドの型のチェックはフィールド名による検索後に行われ、不正ならコンパイルエラーになります) 。 通常はコンパイラが検索して見つけたフィールドと実行時に JVM が見つけるフィールドは同じなので問題は無いのですが、一部のクラスファイルだけを置き換えた場合などには両者の検索方法の違いが検索結果のフィールドの違いとして表れる場合があります。 またフィールドアクセスの命令自体がクラス変数へのアクセスとインスタンス変数へのアクセスに区別されているので、クラスファイルの置き換えによってアクセス対象の変数がクラス変数からインスタンス変数へ、あるいはその逆になる場合にはフィールド名やフィールドの型が正しくても実行時に例外が発生します。

下はフィールドアクセスとクラスファイルの置き換えに関するプログラムです。

public class Bar {
    public int value = 100;
}

public class Foo extends Bar {}

public class Main {
    public static void main(String[] args) {
        Foo foo = new Foo();
        System.out.println(foo.value);
    }
}
コンパイルして Main を実行すると以下のようになります。
    D:\pg\java\temp\field\bcel\temp3>javac *.java

    D:\pg\java\temp\field\bcel\temp3>java Main
    100
まず Main を逆アセンブルしての main メソッドをのぞいてみます。 javap は Sun の JDK に含まれているツールで javac などと同じディレクトリにあります。 javap の使い方は Java のドキュメントの "JDK ツールとユーティリティ" → "基本的なツール" に各プラットフォーム用の javap のリファレンスへのリンクがあります。
    D:\pg\java\temp\field\bcel\temp3>javap -c Main
    Compiled from "Main.java"
    public class Main extends java.lang.Object{
    public Main();
      Code:
       0:   aload_0
       1:   invokespecial   #1; //Method java/lang/Object."<init>":()V
       4:   return

    public static void main(java.lang.String[]);
      Code:
       0:   new     #2; //class Foo
       3:   dup
       4:   invokespecial   #3; //Method Foo."<init>":()V
       7:   astore_1
       8:   getstatic       #4; //Field java/lang/System.out:Ljava/io/PrintStream;
       11:  aload_1
       12:  getfield        #5; //Field Foo.value:I
       15:  invokevirtual   #6; //Method java/io/PrintStream.println:(I)V
       18:  return

    }
main ではまず new で Foo のインスタンスを生成します。
dup は生成した Foo オブジェクトの参照を次の invokespecial で実行する Foo のコンストラクタの引数とする為にコピーします。
Foo のコンストラクタはデフォルトコンストラクタなので引数はありませんが、 JVM ではコンストラクタを実行するオブジェクトの参照がソースコード上の引数と共に渡されており、ソースコード上の this や super が参照するのはこの実行オブジェクトとなります。 また単純名によるインスタンス変数やインスタンスメソッドへのアクセスは実行オブジェクトかそのエンクロージングオブジェクトへのアクセスとなります。
コンストラクタ実行後に Foo オブジェクトの参照を astore_1 で1番目のローカル変数として保持しておきます。
次に getstatic で System クラスのクラス変数の out の参照を取得しています。
次の aload_1 と getfield で foo.value を取得しています。
まず aload_1 で1番目のローカル変数として保持しておいた Foo オブジェクトの参照を取得します。
getfield はオブジェクトからインスタンスフィールドの値を取得します。
       12:  getfield        #5; //Field Foo.value:I
この部分で 12 はメソッドの code配列 (バイトコード配列) へのインデックスです。 2 バイトの引数を取るので次の invokevirtual はインデックスが 3 増えて 15 になっています。
#5 は 2 バイトの引数で表しているコンスタントプールへのインデックスで // 以降にそのフィールド情報を表示してくれています。 (I はフィールドの型が int である事を示しています)
上の getfield では aload_1 で取得した Foo オブジェクトから Foo クラスを起点として Object クラスまで走査し、最初に見つかった value という名前の int 型のフィールドの値を取得する事になります。 もし見つかったフィールドがインスタンス変数でなく、クラス変数であった場合には実行時エラーとなります。
次の invokevirtual で System.out の println メソッドが実行され getfield で取得した値を標準出力に表示します。

次に Foo を以下のように変更します。

public class Foo extends Bar {
    public int value = 10000;
}
Foo のみをコンパイルして再度 Main を実行すると以下のようになります。
    D:\pg\java\temp\field\bcel\temp3>javac Foo.java

    D:\pg\java\temp\field\bcel\temp3>java Main
    10000
Main の main メソッドにおける foo.field の部分の処理は foo が参照している Foo オブジェクトから Foo クラスを起点として value という名前の int 型のフィールドを検索してその値を取得する動作です。 上のように Foo のクラスファイルのみを置き換えてもこの動作に従って処理を行い、その結果 Foo に宣言している value を見つけて標準出力に 10000 が表示されます。

次は Foo の value の型を String に変更してみます。

public class Foo extends Bar {
    public String value = "10000";
}
Foo のみをコンパイルして再度 Main を実行すると以下のようになります。
    D:\pg\java\temp\field\bcel\temp3>javac Foo.java

    D:\pg\java\temp\field\bcel\temp3>java Main
    100
今回のフィールド検索ではFoo で宣言されている value はフィールドの検索時に int 型ではないのでスキップされ Bar の value に解決されます。

今度は Foo の value を static にしてみます。

public class Foo extends Bar {
  public static int value = 10000;
}
Foo のみをコンパイルして再度 Main を実行すると以下のようになります。
    D:\pg\java\temp\field\bcel\temp3>javac Foo.java

    D:\pg\java\temp\field\bcel\temp3>java Main
    Exception in thread "main" java.lang.IncompatibleClassChangeError at Main.main(Main.java:4)
IncompatibleClassChangeError はクラス定義に互換性のない変更があった場合にスローされます。 Main の main メソッドにおける foo.field の部分は前回と同様の検索で Foo に宣言されている value に決定しますが、 getfield はインスタンス変数を対象にする命令なので、対象フィールドの検索結果がクラス変数の場合には上記のようなエラーになります。

次にコンスタント変数がインライン展開されているのを javap で確認してみます。

public class Foo {
    public static final int VALUE_1 = 111;
    public final int value2 = VALUE_1 * 2;
}

public class Bar {
  Foo foo = new Foo();
    public int test() {
        return foo.value2;
    }
    public void test2(int i) {
    switch (i) {
            case Foo.VALUE_1:
                break;
            /*
            case foo.value2:
                break;
            */
        }
    }
}
Foo の VALUE_1 と value2 はコンスタント変数です。 Bar を javap で逆アセンブルすると以下のようになります。

D:\pg\java\temp\field\bcel\temp3>javap -c Bar
Compiled from "Bar.java"
public class Bar extends java.lang.Object{
Foo foo;

public Bar();
  Code:
   0:   aload_0
   1:   invokespecial   #1; //Method java/lang/Object."<init>":()V
   4:   aload_0
   5:   new     #2; //class Foo
   8:   dup
   9:   invokespecial   #3; //Method Foo."<init>":()V
   12:  putfield        #4; //Field foo:LFoo;
   15:  return

public int test();
  Code:
   0:   aload_0
   1:   getfield        #4; //Field foo:LFoo;
   4:   invokevirtual   #5; //Method java/lang/Object.getClass:()Ljava/lang/Class;
   7:   pop
   8:   sipush  222
   11:  ireturn

public void test2(int);
  Code:
   0:   iload_1
   1:   lookupswitch{ //1
                111: 20;
                default: 20 }
   20:  return

}


test メソッドでは命令の引数として 222 という値そのものが使用されているのでフィールドの foo が null でなければ foo にアクセスすることなく常に 222 が返されます。 またソースコードの test2 メソッドのコメントアウトしている部分のコメントをはずすとコンパイルエラーになります。foo.value2 はコンスタント変数への参照なのでインライン展開されるはずですが、switch 文の case ラベルは 定数式でなければならないという規定があるので定数式ではない foo.value2 は使えません。

これ以降はフィールドアクセスからは話しがそれ、コンスタントプールをのぞいてみるという内容なので興味がなければとばしてください。

下は前出の javap によって Main を逆アセンブルして出力された内容の一部です。 #5 はコンスタントプールへのインデックスで // 以降は javap がコンスタントプールの 5 番目のインデックスから取得できるフィールド情報をコメントとして表示してくれています。

       12:  getfield        #5; //Field Foo.value:I
javap ではコンスタントプールの内容の一部や全部を指定して取得することはできません。 バイトコード操作ツールである JakartaBCEL (日本語訳) を使ってコンスタントプールの内容を表示させてみたいと思います。 ここでは BCEL のライブラリーとして bcel-5.1.jar を使っています。 BCEL のライブラリーは上記 BCEL のサイトのダウンロードのリンクをたどれば取得できます。

Main.class ファイルと同じディレクトリに bcel-5.1.jar と 以下の CPViewer.java を配置します。

import org.apache.bcel.*;
import org.apache.bcel.classfile.*;

public class CPViewer {
    public static void main(String[] args) throws Exception {
        if (args.length == 0) {
            System.out.println("コンスタントプールを表示します。\n"
                               + "使用法:\n"
                               + "java CPViewer クラス名\n");
            System.exit(0);
        }
        JavaClass clazz = Repository.lookupClass(args[0]);
        ConstantPool cp = clazz.getConstantPool();
        System.out.println(cp);
    }
}
コンパイルして実行すると以下のようになります。
    D:\pg\java\temp\field\bcel\temp3>javac -cp .;bcel-5.1.jar CPViewer.java

    D:\pg\java\temp\field\bcel\temp3>java -cp .;bcel-5.1.jar CPViewer Main
    1)CONSTANT_Methodref[10](class_index = 8, name_and_type_index = 17)
    2)CONSTANT_Class[7](name_index = 18)
    3)CONSTANT_Methodref[10](class_index = 2, name_and_type_index = 17)
    4)CONSTANT_Fieldref[9](class_index = 19, name_and_type_index = 20)
    5)CONSTANT_Fieldref[9](class_index = 2, name_and_type_index = 21)
    6)CONSTANT_Methodref[10](class_index = 22, name_and_type_index = 23)
    7)CONSTANT_Class[7](name_index = 24)
    8)CONSTANT_Class[7](name_index = 25)
    9)CONSTANT_Utf8[1]("<init>")
    10)CONSTANT_Utf8[1]("()V")
    11)CONSTANT_Utf8[1]("Code")
    12)CONSTANT_Utf8[1]("LineNumberTable")
    13)CONSTANT_Utf8[1]("main")
    14)CONSTANT_Utf8[1]("([Ljava/lang/String;)V")
    15)CONSTANT_Utf8[1]("SourceFile")
    16)CONSTANT_Utf8[1]("Main.java")
    17)CONSTANT_NameAndType[12](name_index = 9, signature_index = 10)
    18)CONSTANT_Utf8[1]("Foo")
    19)CONSTANT_Class[7](name_index = 26)
    20)CONSTANT_NameAndType[12](name_index = 27, signature_index = 28)
    21)CONSTANT_NameAndType[12](name_index = 29, signature_index = 30)
    22)CONSTANT_Class[7](name_index = 31)
    23)CONSTANT_NameAndType[12](name_index = 32, signature_index = 33)
    24)CONSTANT_Utf8[1]("Main")
    25)CONSTANT_Utf8[1]("java/lang/Object")
    26)CONSTANT_Utf8[1]("java/lang/System")
    27)CONSTANT_Utf8[1]("out")
    28)CONSTANT_Utf8[1]("Ljava/io/PrintStream;")
    29)CONSTANT_Utf8[1]("value")
    30)CONSTANT_Utf8[1]("I")
    31)CONSTANT_Utf8[1]("java/io/PrintStream")
    32)CONSTANT_Utf8[1]("println")
    33)CONSTANT_Utf8[1]("(I)V")

org.apache.bcel.classfile.ConstantPool#toString メソッドはコンスタントプールの内容を分かり易く表現してくれます。
ここでは 5 番目のインデックスから取得できるフィールド情報に関連する部分を色付けして強調しています。
    5)CONSTANT_Fieldref[9](class_index = 2, name_and_type_index = 21)
一番左の 5) がコンスタントプール内でのインデックスです。 0 は JVM が内部的に使うものでクラスファイルで使用するのは 1 からだそうです。 CONSTANT_Fieldref[9]は の CONSTANT_Fieldref は定数タイプでフィールド参照に使われます。 () 内の class_index と name_and_type_index もコンスタントプールへのインデックスで、これらを追っていくことでフィールド情報が得られます。 上のフィールド参照では Foo を起点として検索する value という名前の int 型のフィールドであることが分かります。

定数タイプには以下ようなものがあります。 Value の値は定数タイプを表す数値でコンスタントプールではこの値によって定数タイプを識別しています。
Constant Type Value
CONSTANT_Class 7
CONSTANT_Fieldref 9
CONSTANT_Methodref 10
CONSTANT_InterfaceMethodref 11
CONSTANT_String 8
CONSTANT_Integer 3
CONSTANT_Float 4
CONSTANT_Long 5
CONSTANT_Double 6
CONSTANT_NameAndType 12
CONSTANT_Utf8 1

下の部分は型に関する情報を文字列で表しています。

    30)CONSTANT_Utf8[1]("I")
() 内の文字列が型を表し、以下のように分類されています。
BaseType Character Type Interpretation
B byte signed byte
C char Unicode character
D double double-precision floating-point value
F float single-precision floating-point value
I int integer
J long long integer
L<classname>; reference an instance of class <classname>
S short signed short
Z boolean true or false
[ reference one array dimension