« 第68期名人戦第四局。 | トップページ | 再帰的なMakefileの例。 »

内部クラスをめぐる冒険。

Javaの内部クラスに関して、メモとか。

今書いているプログラムで、ちょこっと内部クラスを使ったりしているのですが、今日コンパイルしたときに、見知らぬクラスファイルが出来ていることに気がついて。
具体的には、


class Pieces {
    class PiecesOnBoard {
        ...
    }
    class PiecesInHand {
        ...
    }
    ...
}

という感じのコードなのですが、正しくコンパイルが通れば、Pieces.classとPieces$PiecesOnBoard.class、Pieces$PiecesInHand.classの3つのクラスファイルが出来るはずなのに、なぜかPieces$1.classというクラスファイルも。
どこにも無名クラスを使っていないはずだったので、どこかでなんかミスしたんじゃないかとずっとソースコードを見直してみたものの、ミスが見つからず。

この解答は最後に回すとして、内部クラスに関していくつかメモを。

----
まずは、内部クラスと外部クラス、さらにその外側のクラスとのアクセス制御に関して。

まず、内部クラスと外部クラスとの関係は、内部クラスは外部クラスのメンバにアクセスし放題、外部クラスも(インスタンスを通して)内部クラスのメンバにアクセスし放題というもの。
privateな属性だろうと操作だろうと、関係なし。
なので、例えば、


class Outer {
    class Inner {
        private int a = 5;
        private int num() {
            return a + b; // access outer's private member
        }
    }
    private int b = 10;
    private int num() {
        Inner in = new Inner();
        return in.a * in.num(); // access inner's private member via a inner
    }
}

といったコードも可能。
(内部クラスにとって外部クラスのインスタンスはただ1つなので、メンバ名をただ書けばアクセス出来ますが、外部クラスにとって内部クラスのインスタンスは1つとは限らないので、インスタンス経由でアクセスします。)

そして、内部クラスとさらに外側のクラス、外部クラスとさらにその外側のクラスの関係はというと、これは普通のクラスと全く同じ。
ルール通りではあるのだけれど、内部クラスを使わない場合はありえないのでちょこっと混乱する点といえば、内部クラスがprivateで定義されていた場合は、外側のクラスからは使えないということ。
例えば、


class Outer {
    class Inner1 {
        public int num() {
            return 10;
        }
    }
    private class Inner2 {
        public int num() {
            return 15;
        }
    }
    public Inner1 in1() {
        return new Inner1();
    }
    public Inner2 in2() {
        return new Inner2();
    }
}

というクラスを作ったとき、同じパッケージ内で次のようなコード

Outer out = new Outer();
Outer.Inner1 in1 = out.in1(); in1.num(); // OK!
Outer.Inner2 in2 = out.in2(); in2.num(); // Compile Error!

を書くと、Outer.Inner1クラスはパッケージローカルなのでアクセス出来ますが、Ouer.Inner2クラスはOuterクラスローカルであるのでパッケージからはアクセス出来ず、コメントの通りの結果となります。

----
次に、特殊な記法に関して。

1つ目は、内部クラスが「このメンバは外部クラスのものですよ」と明示したい場合にどのように記述すればいいのかについて。
例えば、クラスのコンストラクタで


class Hoge {
    private int number;
    Hoge(int number) {
        this.number = number;
    }
}

というように、「このインスタンスのメンバ」であることを明示的に表すthisを使って引数のnumberとこのメンバのnumberを区別するというのはよくあることですが、同様に、「このメンバは外部クラスのものですよ」ということを明示的に表したい場合があるかもしれません。
この場合、
  外部クラス名.this.メンバ名
というふうにするようです。
例えば、

class Outer {
    class Inner {
        private int number;
        Inner() {
            this.number = Outer.this.number;
        }
        ...
    }
    private int number;
    ...
}

といった使い方が可能です。

2つ目は、外側のクラスが内部クラスのインスタンスを作りたい場合、どうすればいいのかについて。
内部クラスが外側のクラスからアクセス出来て、さらにコンストラクタもアクセス可能であれば、外側のクラスからも内部クラスのインスタンスを作ることは可能です。
この場合、まず外部クラスのインスタンスを作成して、
  外部クラス名.内部クラス名 変数名 = 外部クラスインスタンス.new 内部クラス名(引数);
という変態な書き方になります。
例えば、先程のOuterとInner1、Inner2がいるようなクラスを作った場合、


Outer out = new Outer();
Outer.Inner1 in = out.new Inner1();

とすることで、外側からも内部クラスのインスタンスを作れます。

----
そして、内部クラスは内部クラスでも、ちょっと違うのがstaticな内部クラス。
staticというくらいなのだから、外部クラスのインスタンスが存在しなくてもそのクラスは存在しているわけで、そういう意味では普通のクラスとおんなじ。
staticなメンバなので、他のstaticなメンバと同様に、staticでないメンバにはアクセス不可能(というか、アクセスのしようがない)。
パッケージの代わりに外部クラスを使っているくらいの感覚で捉えておくとよさそう。
(一応、外部クラスのstaticなメンバにはアクセス可能であるという違いはあるけれど)
インスタンスを作るのも、普通にnewでOK。
例えば、


class Outer {
    static class Inner {
        ...
    }
    ...
}

となっていれば、

Outer.Inner obj = new Outer.Inner();

で、新しいインスタンスが作られます。

----
さて、冒頭の話に戻って。

内部クラスを作った場合、コンパイルをすると
  外部クラス名$内部クラス名.class
というクラスファイルが作られます。
それと、ちょっと特殊な内部クラスである無名クラスが作られた場合には、
  外部クラス名$数字.class
というクラスファイルが作られます。

冒頭のPiecesクラスの場合、作った内部クラスはPiecesOnBoardとPiecesInHandだけで、無名クラスを作っている場所はなかったので、出来るのはPieces.classとPieces$PiecesOnBoard.class, Pieces$PiecesInHand.classのみのはず。
けれど、実際にはPieces$1.classという無名クラスのクラスファイルも作成されていた・・・これはなぜなのか?

答えは、javapというプログラムを使うことで分かりました。
出てきたPieces$PiecesOnBoardについて、javapをかけてみると、


Compiled from "Pieces.java"
class sakura.model.board.Pieces$PiecesOnBoard extends java.lang.Object{
    final sakura.model.board.Pieces this$0;
    ...
    sakura.model.board.Pieces$PiecesOnBoard(sakura.model.board.Pieces, sakura.model.board.Pieces$1);
    ...
}

という出力で、確かにPieces$1というクラスが。
けど、そんな引数のコンストラクタ、作ってないぞと思い、よくよくjavapの説明を読むとデフォルトではパッケージローカル以上のシンボルを表示するということだったので、プライベートなメンバも表示させるようにしたら、

Compiled from "Pieces.java"
class sakura.model.board.Pieces$PiecesOnBoard extends java.lang.Object{
    ...
    final sakura.model.board.Pieces this$0;
    private sakura.model.board.Pieces$PiecesOnBoard(sakura.model.board.Pieces);
    ...
    sakura.model.board.Pieces$PiecesOnBoard(sakura.model.board.Pieces, sakura.model.board.Pieces$1);
    ...
}

という結果が!

つまり、実は、内部クラスのコンストラクタをprivateにしていたのですが、それとは別のパッケージローカルのコンストラクタが自動生成されていたようです。
そして、おんなじ引数だとシグネチャが同じになってしまって区別できなくなるので、シグネチャを変えるために自動生成されたコンストラクタの方にはダミーでPieces$1というクラスの引数をとるようにされるみたいです。
そのため、Pieces$1.classというクラスファイルが生成されたようです。

もしかして、外部クラスは内部クラスのprivateなメソッドも呼べるけれど、コンストラクタだけはまずはオブジェクトを作らないといけないのでprivateなものは直接呼べず、自動生成されたパッケージローカルのコンストラクタを通ってprivateなコンストラクタを呼ぶ必要があるんじゃないかと思い、検証で次のようなコードを書いてみました。


public class Test {
    class In1 {
        int a;
        private In1() {
            (new Exception()).printStackTrace();
            this.a = 5;
        }
        private int num() {
            return this.a;
        }
    }
    class In2 {
        int b;
        In2() {
            (new Exception()).printStackTrace();
            this.b = 1;
        }
        int num() {
            return this.b + Test.this.c;
        }
    }
    private int c = 5;
    public static void main(String[] args) {
        Test test = new Test();
        System.out.println((test.new In1()).num());
        System.out.println((test.new In2()).num());
    }
}

これを実行してみると、

java.lang.Exception
at Test$In1.(Test.java:5)
at Test$In1.(Test.java:2)
at Test.main(Test.java:25)
5
java.lang.Exception
at Test$In2.(Test.java:15)
at Test.main(Test.java:26)
6

となり、確かにTest.In1の方は自動生成されたパッケージローカルのコンストラクタをまずは呼び出して、そこからprivateなコンストラクタを呼び出していることが分かります。

原因が判明して、スッキリ!

|

« 第68期名人戦第四局。 | トップページ | 再帰的なMakefileの例。 »

Prog...」カテゴリの記事

コメント

コメントを書く



(ウェブ上には掲載しません)




トラックバック


この記事へのトラックバック一覧です: 内部クラスをめぐる冒険。:

« 第68期名人戦第四局。 | トップページ | 再帰的なMakefileの例。 »