Calms blog

CALMSブログ

5分で理解できぬ! Java8 ラムダ式 まとめ

備忘録も兼ねて、Java8で導入されたラムダ式周りを簡単な例と一緒に「とりあえずこれだけ知っていれば十分」というレベルでまとめてみた。

Java8については下のページでよくまとまっているけど、ラムダ関係だけ切り出し&補完しています。

大刷新リリース Java 8の新機能 (1) Java 8の目玉、ラムダ式(1) - ラムダ式の基本 | マイナビニュース

テストコードの実行にはJava REPLを使用。

Javaプログラマ必須 - 対話型にJavaを実行できる「Java REPL」 - Calms blog

ラムダ式

Javaプログラマにはなじみがないが、あくまで「Javaラムダ式」だからラムダに慣れたスクリプト系言語の人も注意が必要。

ラムダ式はSAM TypeなInterface……つまり 抽象メソッドが一つのみのInterfaceを実装した匿名クラス の生成を簡潔に書けるようにするものだ。 つまりこういうこと。

java> ActionListener a = e -> System.out.println("Hello, Lambda!")
java.awt.event.ActionListener a = Evaluation$1wypb4tu3ocfrv8xiq7s$$Lambda$4/123229219@39f1ee5d

java> a.actionPerformed(null)
Hello, Lambda!

正式には引数型を書き、処理ブロックを定義する。

java> ActionListener l = (ActionEvent e) -> {
    |     System.out.println("Hello, Lambda!");
    | }

ただ、引数型は自明な場合省略出来るため基本的にいらない。引数を囲む括弧も引数が一つの場合いらない。処理が一文のみの場合処理ブロックもいらない。処理ブロックを作らなければreturn も不要で、一文の結果が返却される。

これによってイベントハンドラやコールバックの記述が簡潔になる。

button.addActionListener(e -> System.out.println("Push more!"));

ちなみにラムダ式内で使用出来る外側の変数はfinalのみで、これは匿名クラスのときと同じ。

とはいっても、実際にはラムダ式自体は匿名クラス文のシンタックスシュガーではない。が、インスタンスを生成しているのには変わりない。当然Objectの子クラスが生成されるわけだから下のようなことが出来るはずだけど、怒られる。

java> ((String s) -> s).toString()
ERROR: ここではラムダ式は予期されていません
    (s -> s).toString();

だからObjectにも代入出来ない。

java> Object o = (String s) -> s
ERROR: 不適合な型: java.lang.Objectは機能インタフェースではありません

どういうことかと思えば、キャストしないと入らないらしい。

ラムダ式単体だとどのInterfaceを対象にしたものかわからないようだ。

// これはOK
Object o = (Runnable) () -> System.out.println("hello!")

前述したSAM TypeのInterface以外には、ラムダ式の引数と戻り値の型によって機能インターフェース型がjava.util.functionに定義されていて、ラムダ式はそこに代入することが出来る。

  • Function<引数型, 戻り値型>
    • 単一の引数を受け、戻り値を返すラムダ。二つの引数を受けるBiFunctionもある。
    • 例: Function<String, Integer> getLength = s -> s.length()
    • 実行: int len = getLength.apply("text")
  • Operator<型>
    • 引数型と戻り値型が同じラムダ。 Functionの引数型と戻り値型に同じ物を入れた感じ。BiFunctionに対応するのはBinaryOperator。
    • 例:Operator echo = s -> s
    • 実行:String s = echo.apply("Yahoo!")
  • Consumer<引数型>
    • 単一の引数を受け、戻り値を返さない。引数を消費するだけのまさにConsumer(消費者)のラムダ。こっちにも二つ引数を受けるBiConsumerがいる
    • 例: Consumer sayHello = name -> System.out.println("Hello, " + name)
    • 実行: sayHello.accept("John")
  • Supplier<戻り値型>
    • 消費者がいれば提供者もいる。引数なしで戻り値を返すラムダ。
    • 例: Supplier getNow = () -> LocalDateTime.now()
    • 実行: LocalDateTime now = getNow.get()
  • Predicate<引数型>
    • 単一の引数を受けてBooleanを返すラムダ。主にstream.filter()で使う。こっちも二つの引数を受けるBiPredicateがいる
    • 例: Predicate<List> isAllOK = list -> !list.contains(Boolean.FALSE)
    • 実行:isAllOK.test(Arrays.asList(true, false, true))

あとは上記のものにInt型を扱うInt~、Doubleを扱うDouble~、Longを扱うLong~があるくらい。

型によって実行メソッド名が違うから微妙にわかりにくいが、ちゃんと動作を表したメソッド名だから覚えやすいはず。

基本的に上記の型で変数を作って取り回すことはあまりないだろうけど、自分でラムダ式を使ったクラスを定義するときには知っておかないといけない。

メソッド参照

これはラムダ式の親戚で、どこかで定義済みのメソッドを機能インターフェース型のインスタンスとして抜き出すことが出来る。

 // listの全要素を表示
java> Arrays.asList("A", "B", "C").forEach(System.out::println)
A
B
C
クラス名(インスタンス)::メソッド名

メソッドが参照される。

クラスメソッドの場合はクラス名、インスタンスメソッドの場合はインスタンスを左側につける。これは通常のメソッドコールと一緒。

作られるものはラムダ式と一緒だから、ラムダ式の実装を外に出したものという考え方も出来る。

java> Supplier<Integer> s = "12345"::length
java.util.function.Supplier<java.lang.Integer> s = Evaluation$nrlvcyfgmj7i4302tpbh$$Lambda$16/961765955@3e8afc21

java> s.get()
java.lang.Integer res17 = 5

自分のクラスに定義したメソッドも this::method で参照出来るし、コンストラクタも MyClass::new で参照出来る。

このメソッド参照を使えば、これまで親抽象クラスを継承して大量の実装クラスを作っていたような設計が、単一クラス内の複数メソッド実装に収めることが出来たりする。そういった新しいクラス設計が求められる場面はそんなにないと思うけど、そのうちガチガチに利用したライブラリが出てくるかもしれない。

ラムダ式を使ったコレクションの操作

便利なメソッドがたくさん追加されたけど、とりあえず以下の3つを覚えておけば良い。

  • forEach
  • filter
  • map

他の言語で同じ名前の関数に覚えがある人は、同じものだと思ってもらって間違いない。

基本的にjava.util.stream.Streamに定義されたメソッドだけど、forEachだけはjava.lang.IterableにもあるからCollectionクラスは素で使える。それ以外は stream() メソッドを挟んでストリーム化しなければいけない。

forEach

java> Arrays.asList(1, 2, 3).forEach(
    e -> System.out.println(LocalDate.now().plusDays(e)))
2014-03-27
2014-03-28
2014-03-29

要素全てを処理するときに使用する。

おなじみのfor each文 for ( ... in ...) と比べて何が良いかといえば、要素の型を省略出来ることくらいか。

普通のループとして使うというよりは、filterやmapと組み合わせたメソッドチェインで使うのが正しいのかもしれない。

filter

java> Arrays.asList(1, 2, 3).stream().filter(e -> e % 2 ==0)
    .forEach(System.out::println)
2

条件に合致した要素だけ抜き出す。待ってました、という感じのメソッド

filterで返ってくるのはStreamだから、Listとして戻したいなら filter(...).toArray()を使う。

map

java> Arrays.asList(1, 2, 3).stream().map(i -> i * 2).toArray()
java.lang.Object[] res0 = [2, 4, 6]

要素全てに同じ計算をして、その結果値で新たにリストを作るメソッド

リストに何らかの処理をしてリストを作る、というものを書くなら、filterとmapを組み合わせればけっこう簡潔に書ける。

どういう状況で使うかはケースバイケースだけど、メソッド参照やラムダをうまく使えば少ないコード量でわかりやすい処理を書けるし、parallelをかませば並列処理してくれるし、慣れればかなり便利。

Arrays.asList("A", "ABC", "BCDE", "BDEFG")
    .stream()
    .parallel()
    .filter(s -> s.startsWith("B"))
    .map(String::length)
    .forEach(System.out::println);
// => 4, 5
// ※streamのfilterやmapでは String::length のように
//   要素のインスタンスメソッドを参照できる

ラムダ式の連鎖

ラムダ式は前述したように、基本的に単一の引数を受け取るものか、Biのついた二つの引数を受け取るものしか型が用意されていない。

じゃあ引数を三つ以上つけたいときはどうするんだって話に当然なる。

もちろん自分で新しく機能インタフェースを定義しても良いが、ラムダ式は繋げて実行させられる。

java> String token = "moon,fire,water"
java> Function<String, String[]> split = token::split

java> Function<String[], String> countToken= 
            tokens -> tokens.length + " tokens found."

java> split.andThen(countToken).apply(",")
java.lang.String res4 = "3 tokens found."

上記のようにandThenで繋ぐことで、一つ目のラムダ式の結果を次のラムダ式に渡して連続実行することが出来る。

(andThenとは実行順を逆にするcomposeもあるけど可読性低くなるから覚えなくていい。引数の流れはそっちのが直感的だからcomposeのほうがって意見もあるけど、少なくともどちらかに統一を)

こうしてandThenで繋げていけば、大体は新しい機能インタフェースを定義しなくても処理がかけるようになる。

andThenはFunctionとConsumerに用意されているが、Function同士、Consumer同士でなければ連鎖出来ない。

// Function x Consumerは無理
java> split.andThen(System.out::println).apply(",")
ERROR: 不適合な型: 型変数Vを推論できません
    (引数の不一致: メソッド参照の戻り型が不正です
      voidをVに変換できません:)
    split.andThen(System.out::println).apply(",");

Predicateは否定、AND、ORを制御するメソッドがある。

// 偶数ならtrue
java> IntPredicate p = i -> i % 2 == 0

java> p.test(2)
java.lang.Boolean res7 = true

java> p.negate().test(2)  // 否定
java.lang.Boolean res9 = false

// 奇数ならtrue
java> IntPredicate p2 = i -> i % 2 != 0

java> p.and(p2).test(2)  // AND
java.lang.Boolean res12 = false

java> p.or(p2).test(2)  // OR
java.lang.Boolean res13 = true

andThenやorなんかで繋いで作ったラムダを、そのままfilterとかに渡すことも出来る。

// filterにIntPredicateは入らないから再定義
java> Predicate<Integer> p = i -> i % 2 == 0
java> Predicate<Integer> p2 = i -> i % 2 != 0

java> Arrays.asList(1, 2, 3).stream().filter(p.or(p2)).toArray()
java.lang.Object[] res2 = [1, 2, 3]

定義済みの判定メソッドを組み合わせてfilterにかける、という用途であれば使う機会はありそう。ただ、その場で型が自明ではないラムダ式はキャストしないとコンパイルエラーが出るから、次のように簡潔に書くことは出来ない。

// (Predicate<型>)にキャストしないとダメ
list.stream().filter( (this::isEnabled).and(this::isActive) )

使い勝手がいいかと言われるとちょっと苦しいところ。

とりあえず、こういう構文・メソッドがあるってことだけ把握して、誰かのソースコードで出で来たときに戸惑わないようにしておけばOKか。


以上、ラムダ式まとめでした。 5分で把握はちょっときつい。

ラムダ式は基本的にコーディング量を少なくするためのものなので、自分でコーディングしていて面倒な部分に適用しながら覚えていけば良いかと。

他にも交差型キャストでラムダ式シリアライズする、みたいな変態チックなことも出来たりするので、興味がある人は深めていって変態的なコーディングをしてみると良いと思います。