【Java】インナークラスについて（匿名クラス編）

Java Goldの勉強も兼ねて。Java 17を基準にしています。

# はじめに
Javaのインナークラスは、[言語仕様](https://docs.oracle.com/javase/specs/jls/se17/html/jls-8.html#jls-8.1.3)によると以下のように定義されています。

* 明示的または暗黙的に`static`**でない**メンバークラス
* 暗黙的に`static`**でない**ローカルクラス
* 匿名クラス

メンバークラスについて、ネットに転がっている記事や書籍の一部では`static`なメンバークラスについてもインナークラス（staticインナークラス）と呼称していることがありますが、厳密には誤りです。<br>
本記事では3つ目の匿名クラスについて取り上げます。
1つ目の[メンバークラス](https://qiita.com/KaZu_rtr/items/5e26e257ed4aac64474e)、2つ目の[ローカルクラス](https://qiita.com/KaZu_rtr/items/1da97b077d210841e85e)についても取り上げています。
# 記法
基本の書き方は以下の通りです。
```java:Main.java
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`した型がもつアクセス可能なメンバーを利用することができます。基本的には関数をオーバーライドしたり、その場限りで使いたい独自の関数を定義して利用することになります。<br>
ただし、宣言した匿名クラスを代入する際、型推論を使わないと独自の関数は利用できないことに注意が必要です。これは、例えば`List`型の変数に`ArrayList`のインスタンスを代入すると`List`型には定義されていない`ArrayList`の`clone`メソッドが使用できないのと同様に、匿名クラスのベースとなる型に存在しない関数は呼び出せないためです。
型推論の`var`が導入されたのはJava 10からなので、それ以前のバージョンでは独自の関数を定義してもインスタンスをベースとなる型に代入してしまうと外部から呼び出す手段は無く、内部で使用しない限りデッドコードになってしまいます。
```java:Main.java
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より前のバージョンでも独自に作成した関数の呼び出しは可能です。
```java
new Object() {
    public void display() {
        System.out.println("Hello from anonymous class");
    }
}.display();
```

インタフェースは通常`new`できませんが、匿名クラスを記述する場合は可能です（厳密には、インタフェースを実現した匿名クラスをインスタンス化しているので、インタフェースそのものを`new`しているわけではありません）。
また、抽象メソッドを持つクラスやインタフェースを用いた場合、通常の継承と同様に実装すべき関数をすべて実装しなければコンパイルエラーとなります。
```java:Main.java
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`による型推論が必要なため、必然的にローカルスコープでしか使えません。後者は、自動で暗黙的なコンストラクタが追加されることになっていますが、コンストラクタの宣言に型の名称を必要とするため、明示的な実装は出来ません。どうしても初期化処理が必要であればイニシャライザ（初期化子）を使うことはできます。<br>
また、メンバークラスの場合と同様に、Java 15まではメンバーやイニシャライザに`static`を使用することができませんでした（`static final`な定数変数は可能）。
```java:Main.java
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でなければなりません。匿名クラス内で参照しているろーかる変数に変更を加えるコードを書こうとするとコンパイルエラーとなります。
```java:Main.java
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`メソッドに渡しています。
```java:Main.java
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);
		}
	}
}
```
<details><summary>Personクラス</summary>

```java:Person.java
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 + "歳)";
	}
}
```
</details>

:::note info
Java 8以降の場合、この例の匿名クラスはラムダ式に置き換え可能です。詳しくは[関数型インタフェース](https://docs.oracle.com/javase/specs/jls/se17/html/jls-9.html#jls-9.8)と[ラムダ式](https://docs.oracle.com/javase/specs/jls/se17/html/jls-15.html#jls-15.27)を確認してください。
:::

# まとめ
* 匿名クラスは名前を持たず、ベースとなるクラスまたはインタフェースを継承する形になっている
* 独自のメンバーを追加できるが、変数の型からはアクセスできない
* 実質的に`final`な外部の変数にアクセスできる