Help us understand the problem. What is going on with this article?

[Java] ラムダ式入門

緒言

Javaでスレッドを実行するためには,Threadクラスを継承したサブクラスを用いる方法と,
Runnableインタフェースを実装したサブクラスを用いる方法があります.
今回,ラムダ式の簡易的な説明のために後者を例にします.

Function.java
public class Function implements Runnable{
    @Override
    public void run() {
        // スレッドで処理する内容
        System.out.println("hello!");
    }
}
Main.java
public class Main {
    public static void main(String[] args) {
        Thread thread = new Thread(new Function());
        thread.start();
        System.out.println("finish!");
    }
}

出力はこんな感じになると思います.

finish!
hello!

ThreadクラスとRunnableインタフェースの関係は
GoFデザインパターンのStrategyパターン設計が使われています.
Strategyパターンは大まかにいえば「付け替えできるアルゴリズムを実現する」設計で,
もともと1つだったクラスを処理の全体の流れを担当するクラスと,
具体的なアルゴリズムを担当するクラスに分けて考えます.
Strategyパターンを使うことで,使う側(Threadクラス)と
使われる側(Runnableインタフェースを実現したクラス)を
直接関係せずに済むため,あとから付け替えできるアルゴリズム実現することができます.
(Strategyパターンを詳しく!という方はこちらを見ると良いかも)
UML2.png
しかしながら,インタフェースを実現したクラスには複雑なもの(UML図のFunctionHardクラス)と
簡単なコードで済むクラス(UML図のFunctionEasyクラス)のようなものがあります.

簡単なコードで済むのに,逐一新しいクラスを
定義しなくてはならないのめんどくないですか?ということです.

ラムダ式の効力

緒言で提示した問題点を解決するために,Java SE8から導入されたラムダ式を利用します.
以下のコードはFunctionEasyクラスを作る代わりにラムダ式に置換したものです.

Lambda.java
public class Lambda {
    public static void main(String[] args) {
        Runnable r = () -> System.out.println("hello!");
        Thread thread = new Thread(r);
        thread.start();
        System.out.println("finish!");
    }
}

このように1行で済みます.スッキリ!
ラムダ式は「関数型インタフェースをインスタンス化するときに
めんどい記述しなくても済むような記法」と思えばよいです.

実装が必要なメソッドを1つだけ持つインタフェースを「関数型インタフェース」や
「SAM(Single Abstarct Method)インタフェース」と呼んだりします.

SAMということからわかる通り,抽象メソッドを1つだけ持つため,
ラムダ式がどのメソッドを実装するのか?引数と戻り値はどうなのか?
推論が可能になります.逐一メソッド名を決める必要がないです.
インタフェースを実現したクラスを用意せずに,ポリモーフィズムを実現できます.

匿名クラスとラムダ式

Java8以前は匿名クラスで実現されていました.
匿名クラスは無名クラスとも呼ばれることがあります.

        Runnable r = new Runnable() {
            @Override
            public void run() {
                System.out.println("hello!");
            }
        }

長くて可読性が悪いですね.

Java8でラムダ式が登場し,これをもっと簡単に記述できるようになりました.

        Runnable r = () -> {
            System.out.println("hello!");
        }

ラムダ式の宣言は,以下のように引数の変数宣言と処理ブロックで構成されます.

     ( 引数 ) -> { 処理; };

「->」をアロー演算子と呼びます.

ラムダ式省略記法

ラムダ式では省略できるものがあります.
以下の原型を例にたどってみましょう.

原型
        Sample sample = (String a) -> { System.out.println(a);};

省略記法1

引数が1つのときだけの場合,カッコ「()」を省略できます.
また,引数の型は型推論できるため,省略することができます.

省略記法1
        Sample sample = a -> { System.out.println(a);};

省略記法2

また,メソッド本体が1行の場合は,中カッコ「{}」と文末のセミコロン「;」を省略できます.
return文の場合はreturnも省略できます.

省略記法2
        Sample sample = a -> System.out.println(a);

省略記法3

さらに,引数が推論できる場合,メソッド呼び出しは
 クラス名::メソッド名
 オブジェクト名::メソッド名
のように省略することができてしまいます.

省略記法3
        Sample sample = System.out::println;

ラムダ式の変数スコープ

注意1

メソッド内で宣言したローカル変数と同じ名前の変数をラムダ式の引数名として使えない.
以下はコンパイルエラーになります.

public class Sample {
    public static void main(String[] args) {
        String a = "sample";
        Function f = a -> System.out.println(a); // エラー
        // 略
    }
}

すでにaという変数名がローカル変数として宣言されているので,ラムダ式の引数名として使えません.

注意2

ラムダ式外で宣言されたローカル変数にラムダ式内からアクセスするには、ローカル変数がfinalでなければいけない.
final修飾子が付いていない場合は実質的finalでなければいけない.

ラムダ式からその式を囲むメソッドのローカル変数にアクセスすることはできます.
(ラムダ式に変換前のことを考えればそれはそうという感じです.)
以下のように,ラムダ式内から,式を宣言しているメソッドのローカル変数にアクセスできます.

「実質的にfinal」というのは,finalで修飾されていなくても,変更されない変数という意味です.
以下のように,ラムダ式内でローカル変数の値を変更してしまうとコンパイルエラーになります.

public class Sample {
    public static void main(String[] args) {
        String a = "sample";
        Function f = () -> {
            a = "change"; // エラー
            System.out.println(a);
        };
        // 略
    }
}

java.util.functionパッケージとCollection API

頻繁に使われる関数型インタフェースはjava.util.functionパッケージにあります.
特に,有名どころは以下の5つの関数型インタフェースです.

関数型インタフェース メソッド 引数 戻り値 説明
Consumer<T> void accept(T) あり なし 値を返さない「消費者」
Supplier<T> T get() なし あり 値を返す「供給者」
Predicate<T> boolean test(T) あり あり 真偽値を返す「断定」
Function<T, R> R apply(T) あり あり 引数を受け取り指定の型の結果を返す「処理」
UnaryOperator<T> T apply(T t) 2つあり あり 演算結果を返す「操作」

Collection APIにはListやMapを操作するデフォルトメソッドがたくさんあります.

例えば,java.util.ListではremoveIf(Predicate filter)というのメソッドがあり,filterに一致する要素を削除します.
ラムダ式を使えば1行で済んでしまいます.

RemoveIf.java
import java.util.ArrayList;
import java.util.Arrays;

public class RemoveIf {
    public static void main(String[] args) {

        ArrayList<String> list = new ArrayList<String>(Arrays.asList("aaaa", "bbb", "cc"));

        list.removeIf(v -> v.length() > 3);

        System.out.println(list); // 結果: [bbb, cc]
    }
}

他にもラムダ式を指定できるメソッドがたくさん用意されています.
各自で調べてラムダ式で実装して慣れていきましょう.

結言

サーバーサイドがJava8で動いているWEBアプリケーションがたくさん存在しています.
そのため,現在Javaをやる上でラムダ式 & Stream APIは避けては通れない知識だと思っています.
にもかかわらず,Java新人研修では教わらないところもある模様なので,
新人向けにできるだけわかりやすくまとめてみました.わかりにくい部分があったら申し訳ないです.

次回,再来週あたりにStream API入門についてまとめようと思います.

(2019.12.22更新) 続編的なSomething → [Java] Stream API 入門

xrdnk
基本はブログに投稿してます.
https://xrdnk.hateblo.jp/
tis
創業40年超のSIerです。
https://www.tis.co.jp/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした