enum インナーインターフェース PUZZLER


なつかしの Java PUZZLER 風で、

Java Puzzlers 罠、落とし穴、コーナーケース

Java Puzzlers 罠、落とし穴、コーナーケース


【問題】 ヒューズが半分おかしくなった

変体的な enum 内のインナーインターフェースを使ったコードがあります。各 enum 毎に Op インターフェースの apply メソッドの振る舞いを定義してます。enum 値は定数固有メソッドにてインターフェースを返却するように定義しています。

package etc9;

public enum Foo {
    PLUS {
        public Op getOp() {
            return new Op() {
              public int apply(int arg) { return arg + 1; }
          };
        }
    },
    MINUS{
        public Op getOp() {
            return new Op() {
              public int apply(int arg) { return arg - 1; }
          };
        }
    },
    ;
    public abstract Op getOp();
    
    interface Op {
        int apply(int arg);
    }
}


上のコードを以下のように実行した場合、何が表示されるでしょうか?

package etc9.pkg;
import etc9.Foo;

public class Main {
    public static void main(String[] args) {
        System.out.println( Foo.PLUS.getOp().apply(1) );
        System.out.println( Foo.MINUS.getOp().apply(1) );
    }
}

【回答】

コードは特に問題なく以下のように表示されるように見えます。

2
0

しかし実際にはコンパイルエラーが発生します。いくぶん変体的ではありますが、文法的にも問題がなさそうに見えます。コンパイラは main メソッド中の apply が不可視と文句を言っています。インナーインターフェースを enum 内で定義することができないのでしょうか。


インナーインターフェースは Map.Entry などで十分一般的であるはずです。例えば以下のコードを見てください。

package etc9;

public interface Foo {

    public static Op plus = new Op() {
        public int apply(int arg) { return arg + 1; }
    };

    public static Op minus = new Op() {
        public int apply(int arg) { return arg - 1; }
    };

    interface Op {
        int apply(int arg);
    }
}


Mainメソッドは以下のように書けます。

package etc9.pkg;
import etc9.Foo;

public class Main {
    public static void main(String[] args) {
        System.out.println( Foo.plus.apply(1) );
        System.out.println( Foo.minus.apply(1) );
    }
}

問題なくコンパイルされて「2」と「0」が出力されます。


もう少し形を近づけてみて、問題のコードを以下のように定義してみます。

package etc9;

public enum Foo {
    ;
    public static Op plus = new Op() {
        public int apply(int arg) { return arg + 1; }
    };

    public static Op minus = new Op() {
        public int apply(int arg) { return arg - 1; }
    };

    interface Op {
        int apply(int arg);
    }
}


ほぼ同じ形となりました。実行してみましょう。

package etc9.pkg;
import etc9.Foo;

public class Main {
    public static void main(String[] args) {
        System.out.println( Foo.plus.apply(1) );
        System.out.println( Foo.minus.apply(1) );
    }
}


ところがこちらもコンパイルエラーとなります。やはり enum 内にはインナーインターフェースを定義できないのでしょうか?

通常のクラスではどうか?

インターフェースはOKでenumはNG。念のためクラスについても見ておきましょう。

package etc9;

public class Foo {

    public static Op plus = new Op() {
        public int apply(int arg) { return arg + 1; }
    };

    public static Op minus = new Op() {
        public int apply(int arg) { return arg - 1; }
    };

    interface Op {
        int apply(int arg);
    }
}
package etc9.pkg;
import etc9.Foo;

public class Main {
    public static void main(String[] args) {
        System.out.println( Foo.plus.apply(1) );
        System.out.println( Foo.minus.apply(1) );
    }
}

おっと、、コンパイルエラーです??
何となく見えてきました。クラス内のインナーインターフェースにアクセス修飾子が付いていないのが気になります。

    public interface Op {

ですね。
インターフェースのメンバはデフォルトで public になるのに対して、enum やクラスでアクセス修飾子を省略した場合はパッケージ外から見られないのは当然でした。


以下のように修正すれば、当然のごとく動きます。

package etc9;

public enum Foo {
    PLUS {
        public Op getOp() {
            return new Op() {
              public int apply(int arg) { return arg + 1; }
          };
        }
    },
    MINUS{
        public Op getOp() {
            return new Op() {
              public int apply(int arg) { return arg - 1; }
          };
        }
    },
    ;
    public abstract Op getOp();
    
    public interface Op {
        int apply(int arg);
    }
}


「だからヒューズの半分だって言ったでしょう」*1

ちなみに

問題の enum は変体的で使いたくないのでもう少し簡素にしてみます。enum はインターフェースを実装できるので以下の形の方がきれいかと思います。

package etc9;

public enum Foo implements Foo.Op {
    PLUS {
        public int apply(int arg) { return arg + 1; }
    },
    MINUS {
        public int apply(int arg) { return arg - 1; }
    },
    ;
    interface Op {
        int apply(int arg);
    }
}

しかしこれでは循環参照でコンパイルできません。


コンパイルを通すにはインターフェースを外に出す必要があります。

package etc9;

interface Op {
    int apply(int arg);
}

public enum Foo implements Op {
    PLUS {
        public int apply(int arg) { return arg + 1; }
    },
    MINUS {
        public int apply(int arg) { return arg - 1; }
    },
    ;
}


すると以下のように使えます。

package etc9.pkg;
import etc9.Foo;

public class Main {
    public static void main(String[] args) {
        System.out.println( Foo.PLUS.apply(1) );
        System.out.println( Foo.MINUS.apply(1) );
    }
}

*1:コンサルタントの道具箱 1969年の雪嵐