Java: 使ってないのに作られる匿名インナークラス

コンパイルしたら、使ってないのに匿名クラスができた。なんで」と質問された。渡されたソースはだいたいこんなの。

public class Test {
   void test() {
      new Foo().say();
   }
   
   private class Foo {
      void say() {
      }
   }
}

J2SE5 でコンパイルしてみたところ、確かに、Test.class と Test$Foo.class の他に Test$1.class ができる。いろいろいじってみると、Foo の可視性を private 以外にした時には $1 が作られないようだ。その辺をヒントに検索。
最初に「これか」と思ったのは、JavaHouse-Brewers の投稿。が、今回はオーバーロードどころかコンストラクタ自体書いてないので、この説明は正しくない。
さらに検索してみると、↓のページが見つかった。

Trial and Error in Java #14 - anonymous class, Part 1
http://www.lake.its.hiroshima-cu.ac.jp/~mondo/Java/TnE/014.html

つまり、Java 言語仕様 (第 2 版) にある

  1. デフォルトコンストラクタがない時は、コンパイラが自動生成する。
  2. その時、作成するデフォルトコンストラクタの可視性は、クラス自体の可視性と同じにする。

という規則に従った結果、

  1. Foo にデフォルトコンストラクタがないので、コンパイラが自動生成する。
  2. Foo は private なので、デフォルトコンストラクタも private に設定される。
  3. あれ? Test から Foo を new できなくなっちゃった!

という状況に陥り、しょうがないので匿名クラスを新規に作ることでコンストラクタを増やし、そっちを経由してデフォルトコンストラクタを呼ぶかたちにしている、というわけだ。できた 3 つのクラスファイルを javap -c -private すると、そのことが確認できる。
ちなみに、インナークラスは JVM 上では普通のクラスと同様に扱われるので、言語仕様通りに動かすために、コンパイラがトリックでなんとかする場面は他にもある。有名なところでは、↓みたいなのがある。

public class Test2 {
   private String message = "Hello";
   
   class Foo {
      void say() {
         System.out.println(message);
      }
   }
}

言語仕様ではインナークラスから外側のクラスにある private フィールドにアクセスできることになっているのだが、Test2、Test2$Foo という 2 つのクラスにコンパイルされてしまうと、当然 Test$Foo から Test の private フィールドにはアクセスできない。これも、コンパイルしてできる .class ファイルをそれぞれ javap -c -private すると、どういうトリックが使われているか確認することができる。