1. 無名クラスを掘り下げる
無名クラスとは、使用箇所でその場で作成される、名前のないサブクラスまたはインターフェースの実装です。ラムダ登場以前(Java 8)は、インターフェースや抽象クラスを「使い切り」で実装する最も便利な方法でした。
定番の例:
Runnable r = new Runnable() {
@Override
public void run() {
System.out.println("無名クラスからこんにちは!");
}
};
r.run();
ここでは、Runnable インターフェースを別ファイルやクラス名なしで、その場で宣言・実装しています。このような実装は、イベントハンドラやコンパレータ、スレッドなど、手早く「振る舞い」を差し込みたい場面でよく使われていました。
ラムダが「その場で書ける式」だとすれば、無名クラスは「名前のない小さな役者」。端役を演じたら消えていきます。
2. ラムダ式との比較
構文
無名クラス:
Comparator<String> comp = new Comparator<String>() {
@Override
public int compare(String a, String b) {
return a.length() - b.length();
}
};
ラムダ式:
Comparator<String> comp = (a, b) -> a.length() - b.length();
違いは明らかです。ラムダの方がコンパクトで、処理が単純なら型やメソッド名を明示したり、余分な波かっこを書く必要がありません。
機能面
- 無名クラス は完全なオブジェクトです。フィールドや追加メソッドを宣言でき、Object のメソッド(toString、equals など)をオーバーライドできます。
- ラムダ式 は関数型インターフェースの1つの抽象メソッドの実装です。内部で独自のフィールドや追加メソッドを宣言することはできません。
どちらを選ぶべきか?
- ラムダ — 関数型インターフェースの1メソッドを手短に実装したいとき。
- 無名クラス — 次のような場合:
- 複数のメソッドを実装したい(例: 抽象クラス);
- 状態を保つためのフィールドを宣言したい;
- Object のメソッド(例: toString)をオーバーライドしたい;
- 継承/アクセスの特性を使いたい(例: スーパークラスの protected メンバーにアクセス)。
3. スコープとキーワード this
ここはよくある落とし穴です:
- 無名クラス 内では、this は無名クラスのインスタンスを指します;
- ラムダ式 内では、this はラムダを宣言した外側のクラスを指します。
例: 挙動を比較
public class Outer {
String name = "外側のクラス";
void test() {
Runnable anon = new Runnable() {
String name = "無名クラス";
@Override
public void run() {
System.out.println(this.name); // "無名クラス"
}
};
Runnable lambda = () -> System.out.println(this.name); // "外側のクラス"
anon.run();
lambda.run();
}
}
出力:
無名クラス
外側のクラス
無名クラスでは this は無名クラス自身(そのフィールド name)を参照します。ラムダでは this は Outer を指します。
4. 無名クラスを使うべきとき
複数のメソッドを実装する必要がある場合
ラムダは関数型インターフェース(抽象メソッドがちょうど1つ)にしか使えません。インターフェースや抽象クラスで複数メソッドの実装が必要な場合は、無名クラスが必要です。
abstract class Animal {
abstract void say();
abstract void jump();
}
Animal cat = new Animal() {
@Override
void say() {
System.out.println("ニャー!");
}
@Override
void jump() {
System.out.println("ピョン!");
}
};
状態(フィールド)を保持したい場合
Runnable r = new Runnable() {
int counter = 0;
@Override
public void run() {
counter++;
System.out.println("呼び出し " + counter + " 回");
}
};
r.run(); // 1 回呼び出し
r.run(); // 2 回呼び出し
Object のメソッドをオーバーライドする必要がある場合
Comparator<String> comp = new Comparator<String>() {
@Override
public int compare(String a, String b) {
return a.length() - b.length();
}
@Override
public String toString() {
return "文字列の長さによるコンパレータ";
}
};
System.out.println(comp); // 文字列の長さによるコンパレータ
5. 例: Comparator と Runnable — ラムダ vs 無名クラス
文字列を長さでソート
無名クラス:
List<String> words = Arrays.asList("ネコ", "ゾウ", "ネズミ", "トラ");
words.sort(new Comparator<String>() {
@Override
public int compare(String a, String b) {
return a.length() - b.length();
}
});
System.out.println(words);
ラムダ式:
List<String> words = Arrays.asList("ネコ", "ゾウ", "ネズミ", "トラ");
words.sort((a, b) -> a.length() - b.length());
System.out.println(words);
結果は同じですが、ラムダの方が短く、読みやすいコードになります。
Runnable: スレッドの起動
無名クラス:
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("無名クラス経由のスレッド");
}
});
t1.start();
ラムダ式:
Thread t2 = new Thread(() -> System.out.println("ラムダ経由のスレッド"));
t2.start();
フィールドを持つ無名クラス
Runnable r = new Runnable() {
int count = 0;
@Override
public void run() {
count++;
System.out.println("呼び出し " + count + " 回");
}
};
r.run(); // 1 回呼び出し
r.run(); // 2 回呼び出し
ラムダではこれはできません — フィールドを宣言できないためです。
6. 注意点: スコープ、変数、そして final
無名クラスでもラムダ式でも、外側メソッドのローカル変数は final または「実質的に final」(初期化後に変更されない)である場合にのみ使用できます。ただし名前に関して違いがあります:
- 無名クラスでは、外側スコープと同じ名前の変数を宣言できます(シャドーイング)。
- ラムダではできません。同名の外側変数と衝突してはいけません。
例:
int x = 10;
Runnable r = new Runnable() {
@Override
public void run() {
int x = 20; // OK: 外側の変数をシャドーイング
System.out.println(x); // 20
}
};
r.run();
Runnable l = () -> {
// int x = 30; // コンパイルエラー: 変数はすでに定義されています
System.out.println(x); // 10
};
l.run();
7. いつラムダが最適で、いつ無名クラスが不可欠か?
次のような場合はラムダを選びましょう:
- 関数型インターフェース用の短い関数を実装したい;
- 状態を保持する必要がない;
- Object のメソッドをオーバーライドする必要がない;
- 「ここで今すぐ」使う簡単な実装である。
次のような場合は無名クラスが必要です:
- 複数メソッドを持つインターフェースや抽象クラスを実装したい;
- フィールドや追加メソッドを宣言したい;
- toString、equals、hashCode をオーバーライドしたい;
- スーパークラスの protected メンバーにアクセスしたい。
8. 実践: 例で比較
課題1: Predicate でリストをフィルタ
無名クラス:
List<String> animals = Arrays.asList("ネコ", "ゾウ", "ネズミ", "トラ");
animals.removeIf(new Predicate<String>() {
@Override
public boolean test(String s) {
return s.length() < 4;
}
});
System.out.println(animals); // [ゾウ, ネズミ, トラ]
ラムダ式:
List<String> animals = Arrays.asList("ネコ", "ゾウ", "ネズミ", "トラ");
animals.removeIf(s -> s.length() < 4);
System.out.println(animals); // [ゾウ, ネズミ, トラ]
課題2: this のスコープを比較
public class Demo {
String name = "Demo";
void check() {
Runnable anon = new Runnable() {
String name = "Anon";
@Override
public void run() {
System.out.println(this.name); // "Anon"
}
};
Runnable lambda = () -> System.out.println(this.name); // "Demo"
anon.run();
lambda.run();
}
public static void main(String[] args) {
new Demo().check();
}
}
9. 無名クラスとラムダを使うときの典型的なミス
エラー №1: ラムダで複数メソッドを実装できると期待してしまう。 ラムダは関数型インターフェース(抽象メソッドが1つ)にしか使えません。メソッドが複数あるなら無名クラスを使ってください。
エラー №2: this のスコープの取り違え。 ラムダでは this は外側のクラス、無名クラスでは無名クラス自身を指します。これにより、意図しないフィールドや値を参照してしまうことがあります。
エラー №3: ラムダでフィールドを宣言しようとする。 ラムダでは独自のフィールドを宣言できません — 使えるのは外側コンテキストの変数(final/「実質的 final」)のみです。状態が必要なら無名クラスを使いましょう。
エラー №4: 変数のシャドーイング。 無名クラスでは外側と同名のローカル変数を宣言できます(シャドーイング)。ラムダではできません — コンパイラエラーになります。
エラー №5: ラムダのロジックが複雑すぎる。 ラムダ本体が 3〜5 行を超えるようなら可読性が落ちます。コードを別メソッドに切り出すか、(状態や複数メソッドが必要なら)無名クラスを使いましょう。
GO TO FULL VERSION