Java Goldへの道:紫本「関数型インタフェースとラムダ式」学習備忘録

はじめに

このブログ記事は”Oracle Certified Java Programmer, Gold SE 11認定資格(Java Gold SE11)”合格に向けた学習備忘録のまとめです。

< この学習備忘録について >
内容や重要な点が抜けていた場合には適宜更新していきます

Java Gold合格において重要な単元

Java Goldの学習においては、下記の単元が非常に重要となってきます。

  • 関数型インタフェース
  • ラムダ式
  • JavaストリームAPI
  • 並列処理

今回は太字になっている部分、つまり「関数型インタフェースとラムダ式」について私がこの章の学習で特に重要だと感じたポイントや、つまずきやすかった箇所を、具体的なコード例とともに備忘録としてまとめていきます。

私と同じようにJava Goldの合格を目指して勉強されている皆様にとって、少しでもお役に立てれば幸いです。

大前提!!!ラムダ式の「実質的final」とジェネリックスの基本

ラムダ式がアクセスするローカル変数は「実質的にfinal」

ラムダ式を扱う上で、まず頭に入れておくべき重要なルールがあります。それは、

ラムダ式がキャプチャする(外側のスコープの変数にアクセスする)ローカル変数は「実質的にfinal」として扱われるため、ラムダ式内で再代入することはできない

という点です 。もちろん、引数を使って計算したり、別のメソッドに渡したりといった処理を行うことは問題ありません 。

ジェネリックスの型パラメータについて

また、関数型インタフェースの定義でよく使われるジェネリクスの型パラメータについても整理しておきましょう

  • T:1つ目の引数の型
  • U:2つ目の引数の型
  • R:戻り値の型

これを理解しておくと、参考書や公式のドキュメントを確認するときもスムーズに読み解くことが出来ると思います!

< 疑問 >
Tは"Type"、Rは"Return"これは分かるのですが、なぜいきなり"U"が出てくるのか、、、
T1、T2で良くないですか???

関数型インタフェースの命名パターンについて

Java標準ライブラリには多くの関数型インタフェースがありますが、その命名には一定のパターンがあります。このパターンを把握しておくと、初見のインタフェースでもその役割を推測しやすくなります 。

  • 引数を引数を2つ持つ場合:接頭辞が「Bi」
    ┗ 例:BiFunction<T, U, R>、BiConsumer<T, U>
  • 「引数の型」が決まっている場合:接頭辞が「引数の型名」
    ┗ 例:IntFunction<R>、DoubleFunction<R>
  • 「戻り値の型」が決まっている場合:接頭辞が「To戻り値の型名」
    ┗ 例:ToIntFunction<T>、ToDoubleFunction<T>
  • 「引数の型と戻り値の型」が決まっている場合:接頭辞が「引数の型名To戻り値の型名」
    ┗ 例:IntToDoubleFunction、DoubleToIntFunction
      ※引数の型も戻り値の型も決まっているため、ジェネリックス<>は不要

ラムダ式の記法・省略ルールについて

ラムダ式の魅力はその簡潔さにありますが、そのためには省略記法を正しく理解する必要があります。

基本形:(実装するメソッドの引数)->{ 処理 };

左辺(引数部分)の省略ルール

元の式:(String str)-> 左辺

┗ 引数の型は省略可能
  省略ver1:(str) -> 左辺

┗ 引数が1つの場合のみ、括弧も省略可能
  省略ver2:str -> 左辺

┗ 括弧を省略できない場合
  
  ┗ 引数が無い場合
    () -> 左辺
    
  ┗ 複数の引数がある場合
    (a, b) -> 左辺

右辺(処理部分)の省略ルール

元の式:右辺 ->return str.toUpperCase();

┗ 処理が1文のみで、かつ、return文の場合はreturnと波括弧{}を省略可能
  省略ver:右辺 -> str.toUpperCase();

省略例

Function<String, String> func = (String str) -> {
	return str.toUpperCase(); 
};

// これを省略すると
Function<String, String> func = str -> str.toUpperCase();

// もしくは引数の型と戻り値の型が同じなのでUnaryOperator<T>も使える
UnaryOperator<String> ope = str -> str.toUpperCase();

このように、ラムダ式は非常に柔軟な記述が可能です。試験では、これらの省略ルールを正しく適用できるかが問われます。

適切な関数型インタフェースの選択

ラムダ式のシグネチャ(引数の数と型、戻り値の型)に基づいて、最も適切な標準関数型インタフェースを選ぶという、基礎的ながらも重要な内容です。

import java.util.function.*;
public class Test {
  public static void main(String[] args) {
  【 1 】 obj1 = String::new; //コンストラクタ参照を使わない場合は () -> new String();
  【 2 】 obj2 = (a, b) -> System.out.println(a+b);
  【 3 】 obj3 = a -> a.toUpperCase();
  }
}

  • 【 1 】 obj1 = String::new;
    ・引数:無し
    ・戻り値:String型のオブジェクトを返す
     ┗ Supplier<T>が適切
      ┗ 例:Supplier<String> obj1 = String::new;
  • 【 2 】 obj2 = (a, b) -> System.oun.println(a+b);
    ・引数:2つ
    ・戻り値:無し
     ┗ BiConsumer<T, U>が適切
      ┗ 例:BiConsumer<String, String> obj2 = (a, b) -> System.out.println(a+b);
    (例としてString型の場合)
  • 【 3 】 obj3 = a -> a.toUpperCase();
    ・引数:1つ
    ・戻り値:引数と同じ型の値を1つ返す
     ┗ UnaryOperator<T>が適切
      ┗ 例:UnaryOperator<String> obj3 = a -> a.toUpperCase();
    ※これでもOK:Function<String, String> obj3 = a -> a.toUpperCase();

UnaryOperator<T>とは
 ┗ 引数の型と戻り値の型が同じ時に使えるFunction<T, R>の特殊系
※さらに特殊系として引数が2つ、戻り値が1つで、かつ同じ型の時に使うBinaryOperator<T>もあります

ラムダ式と外部変数

下記のコードは、ラムダ式がどのように外部のオブジェクトのプロパティを参照するか、そしてPredicateインタフェースがどのように真偽値を返すかを理解する上で重要です。

コード例

import java.util.function.*;
public class Foo {
  int val;
  public static void main(String[] args) {
    Foo obj = new Foo();
    obj.val = 20;
    method(obj, a -> a.val < 100); 
// methodメソッドには第一引数にはobjインスタンス、第二引数にはPredicate<Foo>型のラムダ式(戻り値はtrue)が渡される
  }
  static void method(Foo obj, Predicate<Foo> p) {
    String ans = p.test(obj) ? "hello" : "bye";
    // 1:methodメソッドの右辺はtrue(20 < 100のため)になり、p.test(obj)はtrueになる
    // 2:三項演算子は条件がtrueなので"hello"がString型の変数ansに代入される
    System.out.println(ans);
  }
}
// 出力結果:hello 

出力結果がhelloになる理由

methodメソッドに渡されたラムダ式 a -> a.val < 100 は、objインスタンスのvalが100未満であるかを評価します 。
obj.valが20なので 20 < 100true となり、三項演算子によって"hello"が出力されます

※ラムダ式の”a”にはFoo型のオブジェクトが代入されることが期待されているため、

static void method(Foo obj, Predicate<Foo> p) {
 String ans = p.test(obj.val) ? "hello" : "bye";
 System.out.println(ans);
}

はコンパイルエラーになります

関数型インタフェースの合成

composeメソッドの挙動を理解する

Function系のインタフェースには、複数の関数を結合する便利な合成メソッドがあります。compose()メソッドはその一つで、その動作を正しく理解することが、複雑なラムダ式の連鎖を読み解く上で不可欠です。

コード例

import java.util.function.*;

public class Test {
  public static void main(String[] args) {
    IntUnaryOperator iu1 = a -> a * 2; // (1)引数を2倍する
    IntUnaryOperator iu2 = a -> a - 5; // (2)引数から5を引く
    IntUnaryOperator iu3 = iu1.compose(iu2); // iu3は iu2 を先に実行し、その結果を iu1 に渡す
    System.out.println(iu3.applyAsInt(10));
  }
}
// 出力結果:10 

出力結果が10になる理由

composeメソッドは合成メソッドであり、メソッドの引数を先に処理して、その結果を自身に渡す

つまり、iu3は「まずiu2を実行し、その結果をiu1に渡す」という処理になるため

  • 処理の流れ
  1. iu3.applyAsInt(10) が呼び出される。
  2. composeの定義により、まず iu2.applyAsInt(10) が実行される -> 10 – 5 = 5
  3. その結果、 5 が iu1.applyAsInt() に渡される -> 5 * 2 = 10
  4. したがって、最終的な出力は 10 となる。

compose(before)とandThen(after)について

  • compose(before)メソッドは「引数に渡された関数(before)を先に実行し、その結果を自分自身の関数に渡す」という順序で処理を進めます 。

対照的に

  • andThen(after)メソッドは「自分自身の処理を先に実行し、その結果を引数に渡された関数(after)に渡す」という順序になります。

この順序の違いがポイントです。

まとめ

紫本の第5章「関数型インタフェースとラムダ式」は、Java Gold試験において非常に重要な単元であり、Java 8以降のプログラミングを理解する上で不可欠な知識です。

問題演習を通じて単に正解を覚えるだけでなく

  • ラムダ式の引数の「実質的final」の概念
  • 関数型インタフェースの命名パターン
  • ラムダ式の省略ルール
  • 関数合成メソッドの挙動

といった、細かいながらも重要なポイントを深く理解することができました。
Java Gold合格を目指す皆さん、一緒に頑張りましょう!

次回の学習備忘録は、ただいま絶賛苦戦中のJavaストリームAPIを取り上げます!!!

櫟原侑祐

 櫟原侑祐

スキル:Java/SpringBoot/Bootstrap/Thymeleaf/HTML/CSS/JavaScript/TypeScript/Angular/MySQL/Eclipse

コメント

この記事へのコメントはありません。

関連記事