Java Goldの勉強も兼ねて。Java 17を基準にしています。
はじめに
Javaのインナークラスは、言語仕様によると以下のように定義されています。
- 明示的または暗黙的に
static
でないメンバークラス - 暗黙的に
static
でないローカルクラス - 匿名クラス
メンバークラスについて、ネットに転がっている記事や書籍の一部ではstatic
なメンバークラスについてもインナークラス(staticインナークラス)と呼称していることがありますが、厳密には誤りです。
本記事では3つ目の匿名クラスについて取り上げます。
1つ目のメンバークラス、2つ目のローカルクラスについても取り上げています。
記法
基本の書き方は以下の通りです。
public class Main {
public static void main(String[] args) {
final var sample = new Sample() {
@Override
public void display() {
System.out.println("Hello from anonymous class");
}
public void displayAnonymous() {
System.out.println("Hello from anonymous class method");
}
};
sample.display();
sample.displayAnonymous();
}
}
class Sample {
public void display() {
System.out.println("Hello from Sample class");
}
}
new クラス/インタフェース() { 匿名クラスのメンバー }
の形で書きます。匿名クラスはクラスまたはインタフェースを継承していると考えることができ、new
した型がもつアクセス可能なメンバーを利用することができます。基本的には関数をオーバーライドしたり、その場限りで使いたい独自の関数を定義して利用することになります。
ただし、宣言した匿名クラスを代入する際、型推論を使わないと独自の関数は利用できないことに注意が必要です。これは、例えばList
型の変数にArrayList
のインスタンスを代入するとList
型には定義されていないArrayList
のclone
メソッドが使用できないのと同様に、匿名クラスのベースとなる型に存在しない関数は呼び出せないためです。
型推論のvar
が導入されたのはJava 10からなので、それ以前のバージョンでは独自の関数を定義してもインスタンスをベースとなる型に代入してしまうと外部から呼び出す手段は無く、内部で使用しない限りデッドコードになってしまいます。
public class Main {
final Sample sample2 = new Sample() {
@Override
public void display() {
System.out.println("Hello from another anonymous class");
}
public void displayAnonymous() {
System.out.println("Hello from another anonymous class method");
}
};
sample2.display();
// sample2.displayAnonymous(); // 型を明示して変数を宣言しているため、外部からは利用できない。
}
class Sample {
public void display() {
System.out.println("Hello from Sample class");
}
}
なお、通常のクラスと同様にnew
した直後にメソッドを参照できます。この方法であればJava 10より前のバージョンでも独自に作成した関数の呼び出しは可能です。
new Object() {
public void display() {
System.out.println("Hello from anonymous class");
}
}.display();
インタフェースは通常new
できませんが、匿名クラスを記述する場合は可能です(厳密には、インタフェースを実現した匿名クラスをインスタンス化しているので、インタフェースそのものをnew
しているわけではありません)。
また、抽象メソッドを持つクラスやインタフェースを用いた場合、通常の継承と同様に実装すべき関数をすべて実装しなければコンパイルエラーとなります。
public class Main {
public static void main(String[] args) {
final Greeting greeting = new Greeting() {
// sayHello()の実装は必須
@Override
public void sayHello() {
System.out.println("Hello from anonymous class");
}
};
greeting.sayHello();
}
}
interface Greeting {
void sayHello();
}
制約
匿名クラスは名前を持ちません。そのため、次の2つのことが出来ません。
- クラスの再利用
- コンストラクタを明示的に宣言すること
前者については、変数として宣言して使いまわすためにはフィールドかローカル変数に定義する必要がありますし、独自の関数を利用するにはvar
による型推論が必要なため、必然的にローカルスコープでしか使えません。後者は、自動で暗黙的なコンストラクタが追加されることになっていますが、コンストラクタの宣言に型の名称を必要とするため、明示的な実装は出来ません。どうしても初期化処理が必要であればイニシャライザ(初期化子)を使うことはできます。
また、メンバークラスの場合と同様に、Java 15まではメンバーやイニシャライザにstatic
を使用することができませんでした(static final
な定数変数は可能)。
public class Main {
public static void main(String[] args) {
Greeting greeting = new Greeting() {
// Java 15以降でコンパイル可能
static {
System.out.println("Anonymous class");
}
static String name = "static field";
interface Test {
void sayHello();
}
@Override
public void sayHello() {
System.out.println(name);
System.out.println("Hello from anonymous class");
}
};
greeting.sayHello();
}
}
interface Greeting {
void sayHello();
}
匿名クラス内から外部の変数を参照することができますが、ローカル変数は実質的にfinalでなければなりません。匿名クラス内で参照しているろーかる変数に変更を加えるコードを書こうとするとコンパイルエラーとなります。
public class Main {
private static String message = "Hello";
public static void main(String[] args) {
var effectivelyFinal = "Hello from effectively final"; // 他の個所で変更していないため、実質的にfinal
var notFinal = "Hello from not final";
final var anonymous = new Object() {
public void sayHello() {
System.out.println(effectivelyFinal);
}
public void sayHello2() {
System.out.println(notFinal);
}
public void sayHello3() {
System.out.println(message);
}
};
anonymous.sayHello();
// notFinal = "Hello from not final changed"; // 匿名クラス内で参照しているため変更できない
anonymous.sayHello2();
message = "Hello from not final changed";
anonymous.sayHello3();
}
}
具体的な使用例
リストをソートしたいときに、java.util.Collections
のsort
メソッドが使えます。sort
メソッドにはリストを引数に受け取り自然順序でソートを実行するものと、第二引数にComparator
インタフェースを受け取り独自のロジックでソートを実行する2種類があります。後者は第二引数を渡す際に匿名クラスを使います。
下記の例では、Personクラスの姓と名を使った独自の比較ロジックを定義してsort
メソッドに渡しています。
public class Main {
public static void main(String[] args) {
// ソート対象のリスト
final List<Person> people = new ArrayList<>();
people.add(new Person("佐藤", "太郎", 28));
people.add(new Person("田中", "花子", 22));
people.add(new Person("鈴木", "一郎", 35));
// 名前でソートするComparator(匿名クラス)
Collections.sort(people, new Comparator<Person>() {
@Override
public int compare(Person p1, Person p2) {
// 姓で比較し、同じ場合は名で比較
final int lastNameComparison = p1.getLastName().compareTo(p2.getLastName());
if (lastNameComparison != 0) {
return lastNameComparison;
}
return p1.getFirstName().compareTo(p2.getFirstName());
}
});
System.out.println("\n姓名順にソート:");
for (final var person : people) {
System.out.println(person);
}
}
}
Personクラス
class Person {
private String lastName;
private String firstName;
private int age;
public Person(String lastName, String firstName, int age) {
this.lastName = lastName;
this.firstName = firstName;
this.age = age;
}
public String getLastName() { return lastName; }
public String getFirstName() { return firstName; }
public int getAge() { return age; }
@Override
public String toString() {
return lastName + " " + firstName + " (" + age + "歳)";
}
}
Java 8以降の場合、この例の匿名クラスはラムダ式に置き換え可能です。詳しくは関数型インタフェースとラムダ式を確認してください。
まとめ
- 匿名クラスは名前を持たず、ベースとなるクラスまたはインタフェースを継承する形になっている
- 独自のメンバーを追加できるが、変数の型からはアクセスできない
- 実質的に
final
な外部の変数にアクセスできる