【Java】無名クラスとラムダ式をていねいに解説

アイキャッチ画像

必要となる前提知識

この記事の内容を理解するために必要な前提知識です。

・Javaの基本構文(if文、for文など)

・インターフェースと実現

無名クラス

無名クラスとは、その名の通り無名(名前が存在しない)クラスのことです。
1回限りの処理をするためにその場でクラスを定義してインスタンス化する、というのが無名クラスの役割です。

以下の具体例で見てみましょう。
まず、インタフェースを定義します。

interface TestIF {    public void test();}

この後、通常であればインタフェースを実現化するクラスを定義するのですが、
1度ぐらいしか使わないようなクラスをわざわざ定義するのは無駄ではないか、という考えから
以下のような記述をすることで「その場でクラスを定義してインスタンス化」ができるようになりました。

public class Main {
   public static void main(String[] args) {
        // 無名クラスを定義
        TestIF t1 = new TestIF() {
            public void test() {
                System.out.println(“無名クラスでオーバーライドしました”);
            }
        };
        // メソッド実行
        t1.test();
    }
}

// 表示結果
// クラスでオーバーライドしました

ポイントはmainメソッドの中でクラスの定義を行っている点です。ここも普通のクラス定義とは大きく異なります。

また、コードを見ればわかる通り「クラスの名前」がありません。TestIFはインタフェースの名前なので違います。このように名前をつける必要もないため「無名クラス」と呼ばれます。

なお、使用できるメソッドはインタフェースに定義した抽象メソッドをオーバーライドしてある必要があります。

もちろん、全く同じ処理を以下のように普通にクラス定義でもできます。

// 実現化クラスを普通に定義
class TestImpl implements TestIF{
    public void test(){
        System.out.println(“普通にオーバーライドしました”);
    }
}

public class Main {
    public static void main(String[] args) {
        TestImpl t2 = new TestImpl();

        // メソッド実行
        t2.test();
    }
}

// 表示結果
// 普通にオーバーライドしました

これでも別にいいのですが、無名クラスを使うことで主に以下のメリットがあります。

  • コードが短くなる

大幅に短くなるケースは稀ですが、普通にクラス定義するよりは基本的に文字数は少なくなります。

  • クラスファイルを増やす必要がない

1度だけの使い捨てなので、クラスファイルが不要です。特に、規模が大きいシステム開発では元々クラスの数が多いため、無名クラスを用いることでクラスファイル数増加を抑制することができます。

  • その場で処理の中身を直感的に書ける

無名クラスは使う場所ですぐ処理を書くため、流れを理解しやすいです。

ラムダ式と関数型インタフェース

概要

ラムダ式は、無名クラスのさらに簡略化された書き方として導入された記述法です。

ただし、ラムダ式が使えるのは元となるインタフェースが「関数型インタフェース」の場合のみです。

関数型インタフェース

抽象メソッドが1つだけ定義されているインタフェースを「関数型インタフェース」といいます。

メソッドが存在しない、または2つ以上であったり、1つだけの場合でもそれがdeafultメソッドやprivateメソッドの場合は関数型インタフェースとは見なされません。

無名クラスの説明で作成したTestIFは、抽象メソッドであるtest()メソッドだけが定義されているインタフェースなので、関数型インタフェースです。

@FunctionalInterfaceinterface TestFuncIF {
    public void funcTest();
    // メソッドがないor2つ以上だとコンパイルエラー
}

上のコード例では@FunctinalInterfaceというアノテーションを付与しています。
このアノテーションは、関数型インタフェースの定義に従わないときにコンパイルエラーとなります。
付与しても実行結果は変わらないので必須ではありませんが、明示してくれてわかりやすいという利点もあるのでアノテーションをつけることを推奨します。

次に、ラムダ式の記述方法です。以下のコードをご覧ください。

public class Main {
    public static void main(String[] args) {
        // ラムダ式で実装
        TestFuncIF t2 = () ->
            { System.out.println(“ラムダ式で実装”); };
                // メソッド実装
                t2.funcTest();

        // 表示結果
        // ラムダ式で実装

詳しい説明は後述しますが、無名クラスより更に文字数が減ったのがわかると思います。

無名クラスではオーバーライドのメソッドを記述していましたが、ラムダ式の場合は自動で実装されるため、その記述は不要です。

しかし、逆に考えると通常の無名クラスでは可能だった、アクセス修飾子や戻り値を変更してオーバーライドすることは不可能ということになります。

以上を考慮して、ラムダ式を使用するかどうかを検討する必要があります。

ラムダ式の構文パターン

ラムダ式は条件によって括弧や波括弧を省略したりできるため、様々な記述パターンがあり初学者には厄介です。

まずは、省略をしないパターンから学習しましょう。

①引数なし

() -> { //処理内容 };

@FunctionalInterfaceinterface TestFuncIFA {
    public void testA();
}
class Main {
     public static void main(String[] args) {
              TestFuncIFA a = () ->
               { System.out.println(“testA”); };
              a.testA();
    }
}

(例1:戻り値なし)

testA

(例2:戻り値あり)

戻り値がある場合は通常のメソッドのようにreturnで返します。

@FunctionalInterfaceinterface TestFuncIFB {
    public String testB();
}

class Main {
    public static void main(String[] args) {
        TestFuncIFB b = () ->
             { return “testB”; };
        System.out.println(b.testB());
    }
}
testB

実行結果

②引数あり(1つ)

(変数) -> { //処理内容 };

@FunctionalInterfaceinterface TestFuncIFC {
    public String testC(int n);
}

class Main {
   public static void main(String[] args) {
            TestFuncIFC c = (int x) ->
             { if (x < 0) {
                return “負の数です”;
             } else {
                return “正の数です”;
            }
};
 
System.out.println(c.testC(-10));    }}
負の数です

実行結果

③引数あり(2つ)

(変数1, 変数2) -> { //処理内容 };

@FunctionalInterfaceinterface TestFuncIFD {
    public int testD(int n1, int n2);
}

class Main {
    public static void main(String[] args) {
       // 第1引数を10倍した数に第2引数を足す
       TestFuncIFD d = (int x, int y) ->
             { return (x * 10) + y; };
       System.out.println(d.testD(5, 2));    }}
52

実行結果

ラムダ式の構文パターン(省略版)

特定の記述を省略するパターンです。
変数の型は省略された形で使用されることが多いです。

変数の型の省略

変数の型はすべてのパターンにおいて省略できます。
これは、インタフェースのメソッドで型は定義されているので書かなくても自動で判断できるからです。
なお、引数が複数ある場合、型表記ありとなしが混在するとコンパイルエラーになります。

// 型表記あり
Func f = (int x, int y) -> { //処理内容 }; //OK

// 型表記なし
Func f = (x, y) -> { //処理内容 }; //OK

// これはコンパイルエラー
Func f = (int x, y) -> { //処理内容 }; //NG

括弧の省略

括弧の省略は引数が一つだけの場合のみ可能です。
なお、括弧を省略した場合は型も省略する必要があります。

// 括弧あり、型なし
Func f = (name) -> { //処理内容 }; //OK

// 括弧省略版
Func f = name -> { //処理内容 }; //OK

// 括弧なしで型を書くとエラー
Func f = String name -> { //処理内容 }; //NG

// 引数2つ以上で括弧なしだとエラー
Func f = x, y -> { x + y }; //NG

波括弧の省略

処理部分(矢印の後)をかこんでいる波括弧も省略することができます。

波括弧の省略は処理が1文の場合のみ可能です。

なお、波括弧を省略した場合はreturnも省略する必要があります。

//波括弧あり
TestFuncIFD f = name -> {    name = “Hello, ” + name + “!”;    return name;    };  //OK

//波括弧なし ※戻り値無しの場合
Func f = () -> System.out.println(“Hello”);  //OK

//波括弧なし ※戻り値ありの場合
Func f = () -> return “error”;  //returnを書くとNG

//波括弧なし ※2文以上の場合
Func f = num -> num *= 2; num += 1; //NG

まとめ

無名クラス、ラムダ式、関数型インタフェースについて一通り解説しました。
はじめはさまざまなパターンに戸惑うこともあるかもしれませんが、慣れてくると非常に便利に感じられるようになります。ぜひマスターしてみてください。
そのためには、コードを読むだけでなく、実際に手を動かして書いてみることが何より大切です。