Ch7. ラムダ式

関数型インターフェース

関数型インターフェースはRubyでいうところのProcオブジェクトの型となるようなもので、

  • 引数が必要か
  • 戻り値が存在するか
  • 戻り値が引数と同じ型か

などによって別の型が存在する。また、この関数インターフェースで実装した処理に対して引数を渡して実行するのに、それぞれ別のメソッド名が存在する。

インターフェース メソッド(Call) 使い方
Function<T,R> apply(T) 引数としてT型を受け取り、R型を返却する
UnaryOperator apply(T) 引数としてT型を受け取り、T型を返却する
Consumer accept(T) 引数としてT型を受け取り、結果は返さない
Predicate test(T) 引数としてT型を受け取り、booleanで結果を返す
Supplier get() 引数は受け取らず、T型を返却する

ラムダ式の中身の実装は以下のように表記できる。

import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.function.UnaryOperator;

public class Chapter8 {

    public static void main(String[] args) {
        // TODO 自動生成されたメソッド・スタブ

        Consumer<String>       proc  = (String str) -> System.out.println(str + " !!!");
        //lambdaの引数の型は変数宣言時に明記してあるので省略可能
        Predicate<Integer>     proc2 = (number) -> number % 2 == 0;
        //引数が一つだけなら()も省略可能
        UnaryOperator<Integer> proc3 = number -> number * number;

        ArrayList<String> content = new ArrayList<>();
        Collections.addAll(content, "Heart", "Beat", "Moters");
        content.forEach(proc);
        System.out.println(content);

        ArrayList<Integer> content2 = new ArrayList<>();
        Collections.addAll(content2, 11,20,31,40,51);
        content2.removeIf(proc2);
        System.out.println(content2);//'11,31,51'

        ArrayList<Integer> content3 = new ArrayList<>();
        Collections.addAll(content3, 10,20,30,40,50);//'[100, 400, 900, 1600, 2500]'
        content3.replaceAll(proc3);
        System.out.println(content3);

        //lambdaをforEachメソッドの引数内で宣言
        ArrayList<String> content4 = new ArrayList<>();
        Collections.addAll(content4, "目の", "付けどころが", "シャープでしょ");
        content4.forEach(str -> System.out.println(str + "!!"));
    }

}

通常、配列はList<>を利用することが多い。(この慣習に反して上記の例ではArrayListうぃ利用している。) List自体がArrayListのインターフェースなのだが、ArrayList固有のメソッドが利用されることが少ないという理由で、配列オブジェクトはList型の宣言が多用される。

ただしListには、内部の要素を変更するメソッドが実装されていないため、下記のようにListで宣言した変数に代入された配列に対して下記のデフォルトメソッドを利用すると実行時例外となる。

       List<Integer> content5 = Arrays.asList(1,2,3,4,5,6,7,8,9,10);//ListObject(固定リスト)
        content5.removeIf(proc2); //'Exception in thread "main" java.lang.UnsupportedOperationException'

また関数型インターフェースは次のようにIf文の条件分岐で利用することもできる。 条件分の中で実行されたlmbdaは実行完了されるとTrueとなり、実行結果が{}内で得られる。

条件に当てはまる要素が存在しない場合、条件分岐自体がfalseとなり、括弧内が実行されない点に注意する。

       ArrayList<String> nameList = new ArrayList<>();
        nameList.add("AAAA");
        nameList.add("BBB");
        nameList.add("CCC");

        if(nameList.removeIf(name -> name.length() > 3)) {
            System.out.println(nameList);//'[BBB, CCC]'
        }

        nameList.add("AAA");
        nameList.add("BBB");
        nameList.add("CCC");

        if(nameList.removeIf(name -> name.length() < 3)) {
            System.out.println(nameList);
        }

GoF Strategy

Stragtegyパターンとは、

処理全体の流れを担当するクラスと、具体的なアルゴリズムを担当するクラスに分けて考える

ことで

付け替え可能なアリゴリズムを実現する

方法。複数のアリゴリズムが実装されることを想定し、各アリゴリズムに対して共通のinterfaceを定義して実装させる。 Interfaceにはperformという抽象メソッドを宣言し、アリゴリズムの中身はそのメソッドに対して実装させる。

処理全体を担当するクラス(Service)クラスは、アリゴリズムをインスタンス変数として保持した上でinterfaceのperformメソッドを実行する。

新しいアリゴリズムと交換する際、新しいアリゴリズムがinterfaceを実装していさえすればServiceクラスは一行も変える必要がない。

具体例として、Threadクラスの例が挙げられている。 Threadクラスではコンストラクタに新しいスレッドで実行したい処理を渡して、処理させる仕組みになっている。

その際、実行させたい処理にはRunnableというrun()という抽象メソッドを宣言したInterfaceを実装させ、Threadクラス内で渡されたオブジェクトのrun()メソッドを呼ぶ仕組みになっている。

class Todo implements Runnable{
    public void run() {
        System.out.println("Hello World");
    }
}

public class GoF_Strategy {
    public static void main(String[] args) {
        Thread thread = new Thread(new Todo());
        thread.start();
    }
}

しかし、上の例を見てもわかるようにいちいちクラスを宣言するのは面倒なので 匿名クラスという、一つの抽象メソッドしか宣言されていないInterfaceを実装したクラスのオブジェクトを簡単に生成できる方法が導入された。

匿名クラスでは、わざわざinterfaceを実装したクラスを宣言させず、interfaceで宣言した抽象メソッドだけを実装したオブジェクトを作ることが出来た。

       Thread thread2 = new Thread(new Runnable() {
            public void run() {
                System.out.println("Hello World from 匿名クラス");
            }
        });
        thread2.start();

上の例でもまだ短縮する余地がある。 それはRunnableinterfaceが実装しなくてはならないメソッドはrun()一つであり、それをわざわざ明示的に宣言している冗長さが残る点である。

lambdaではこれを更に短縮してこの冗長さを排除できる。

       Runnable lambda = () -> {System.out.println("Hello World from lambda1");};
        Thread thread3 = new Thread(lambda);
        thread3.start();
        

        Thread thread4 = new Thread(()-> {System.out.println("Hello World from lambda2");});
        thread4.start();

下記の例では、Thread()の引数にはRunnableのinterfaceを実装したオブジェクトが必ず必要になる事から、Runnableの宣言も不要になっている。

lambda式の記法

lambdaは様々な省略記法を用意しているが、以下のような制約が存在する。 * lambdaに中括弧を付けた場合、複数の処理を記述できる。その代わり戻り値を設定したければreturnは省略できない。 * lambdaに中括弧を付けない場合、一つの処理しか記述できない。更に戻り値を設定したければreturnは記述できない。

        UnaryOperator<String> uo = (name) -> name + " !!";
        //UnaryOperator<String> uo = (name) -> return name + " !!"; 'これはコンパイルエラー'

lambda式は式が定義されているスコープに宣言されているローカル変数にアクセスできる。 ただし、それをlambda式外で操作しようとすると、コンパイルエラーになる。

変数にfinal宣言があれば変数に対する操作をしようがないのでlambda内でも自由に動かせるが、「事実上のfinal宣言」であってもlambda式内から参照が可能である。(つまりlambda式内で特に変数の操作が無ければ)

        int variable = 1000;

        Function<Integer, String> newFunc = (number) -> {
            variable += 100; //Local variable variable defined in an enclosing scope must be final or effectively final
            return "finish";
        };

また、lambdaで宣言する変数の名前は、lambdaを宣言したスコープにあるローカル変数とかぶってはいけない。

        int variableName = 100;
        UnaryOperator<Integer> uo2 = (variableName) -> 1000;
        //'Lambda expression's parameter variableName cannot redeclare another local variable defined in an enclosing scope. '

        UnaryOperator<Integer> uo2 = (variableName2) -> {
            int variableName = 200; //'Lambda expression's local variable variableName cannot redeclare another local variable defined in an enclosing scope. '
            return 1000;
        };