X

A blog about Oracle Technology Network Japan

クイズに挑戦:ラムダ式の型(上級者向け)

Guest Author

ラムダ式はラムダ式を返せるのか

著者:Simon Roberts、Mikalai Zaikin

2020年2月27日

その他の設問はこちらから

過去にこのクイズの設問に挑戦したことがある方なら、どれ1つとして簡単な問題はないことをご存じでしょう。クイズの設問は、認定試験の中でも難しいものに合わせています。「中級者向け」「上級者向け」という指定は、設問の難易度ではなく、対応する試験による分類です。しかし、ほとんどすべての場合において、「上級者向け」の方が難しくなります。設問は認定試験対策として作成しており、認定試験と同じルールの適用を意図しています。文章は文字どおりに解釈してください。回答者を引っかけようとする設問ではなく、率直に言語の詳細な知識を試すものだと考えてください。

java.util.functionパッケージのインタフェースの正しい使用方法に関する知識を確認します。

次のコードについて:

public void doIt() {
    /* insert here */ v1 = () -> a -> {}; 
}

 

次のうち、コメント/* insert here */の部分を完全に置き換えることで、コードが正しく完成するものはどれですか。1つ選んでください。

  1. var
  2. Supplier<Consumer<?>>
  3. Function<Consumer<?>,?>
  4. Function<Predicate<?>,?>
  5. Consumer<Function<?,?>>

この設問は、ラムダ式の特徴、ネストされたラムダ式の複雑さ、ラムダ式を用いた関数型インタフェース・タイプ変数の宣言と初期化、さらにはjava.util.functionパッケージの関数型インタフェースの性質について問うものです。
ラムダ式の考え方に慣れていない場合、ラムダ式を含むコードは理解しづらいことがあります。また、状況によっては、かなりの経験を持つ人でも理解しにくい場合があります。そんなときに役立つ秘けつの1つが、物事を小さな部分に分解することです。すべてのラムダ式は、引数リストとメソッド本体でできています。メソッド本体が単なる1つの式である場合も、波括弧でくくられた完全なメソッド本体が存在する場合もあります。しかし、いずれにせよ、メソッド本体は必ず存在します。


そこで、まずは選択肢に記述されている型宣言を無視し、変数v1を初期化することになっている式だけを見てみます。次の式です。

() -> a -> {}

この構文は、Haskellを経験したことがある方にとっては非常になじみ深いものかもしれません。しかし本稿は、Java認定試験に出題されるJavaの構文について説明する、Java Magazineの記事であるため、ほとんどの方は初めて見ると考えるのが無難です。
それでは、どのように分解すればよいでしょうか。まず、すべてのラムダ式は引数リスト、アロー、メソッド本体という3つの部分でできていることがわかります。この場合、どこがそれぞれの部分に当てはまるのかを見てみます。

()         ->      a -> {}
引数リスト    アロー    メソッド本体

2つ目のアローがこのラムダ式のアローではないことがどうしてわかるのかというのは、もっともな疑問でしょう。理由は2つあります。1つ目は、引数リストにアローを含めることはできないからです。2つ目はさらに抽象的ですが、ほとんどのJava演算子と同じく、アローも左から右へとグループ化するからです。
この時点でわかるのは、全体としては引数が0個のラムダ式であることです。このことだけで多くのことがわかりますが、それはひとまず横に置き、メソッド本体を見てみることにします。


今回の場合、メソッド本体はもう1つのラムダ式になっていますが、短縮形で記述されています。ラムダ式には、2種類の形式が存在します。1つ目の形式は右側に波括弧があるもの、もう1つは波括弧がないものです。実を言えば、波括弧がないものは、波括弧があるものの特殊な形式にすぎません。次に示す、ブロックを使っているラムダ式について考えてみます。

(a, b) -> { return a + b; } 

ラムダ式の中にあるこのようなブロックは、実際には完全なメソッド本体です(そして、メソッド本体のすべての要件に従わなければなりません)。しかし、このブロックは非常にシンプルで、行っているのは、式を評価してその結果の値を返すことだけです。この場合(具体的に言えば、メソッド本体が式を評価してその結果を返すことしかしていない場合)、波括弧とreturnキーワード、そしてreturnキーワードに関連付けられているセミコロンを省略できます。つまり、右側にあるのは式だけになります。したがって、先ほどの例は次の短縮形と同じになります。

(a, b) -> a + b

ただし、少なくともJavaでは、ラムダ式はオブジェクトのインスタンスを表す式です(「内部的には」違う実装である可能性もありますが、ソース・コードから見れば、ラムダ式はあらゆる点でオブジェクトのように振る舞います)。そのため、最初のラムダ式がどのようなものであれ、そのラムダ式はオブジェクトを返します。オブジェクトの型は明らかではありませんが、何らかのインタフェースを実装したオブジェクトです。


それでは、返される値を分解してみます。

a          ->      {}
引数リスト    アロー    メソッド本体

今回のラムダ式は、厳密に1つの引数を受け取り、完全なメソッド本体が波括弧で囲まれている「ブロック・ラムダ」形式であることがわかります。ただし、このメソッド本体は空です。メソッド本体が空のブロックであることを考えれば、このラムダ式の戻りタイプはvoidであるとわかります。
そこから、もう一度式全体を考えてみると、0個の引数を受け取り、その戻り値が、1個の引数を受け取ってvoidを返すラムダ式であるラムダ式ということがわかるでしょう。
外側のラムダ式でもブロック形式を使った場合、式全体がどのように見えるかを考えればわかりやすいかもしれません。

() -> { return a -> {}; }

このラムダ式を同様に分解しても、同じ要素が得られます。

()         ->    { return a -> {}; }
引数リスト    アロー    メソッド本体

ここから、この式は0個の引数を受け取って「何か」を返すラムダ式であることがわかります。
返される「何か」を調べても、先ほどと同じ結果になります。

a          ->      {}
引数リスト    アロー    メソッド本体

先ほどとまったく同じになりました。


最初の問題に戻ります。この複雑でネストされたラムダ式には、どんな種類の宣言を使用できるでしょうか。
最初に、選択肢Aについて考えてみます。このようにvar疑似タイプと変数宣言を組み合わせて使う場合、変数の型が、その変数を初期化するために使う式(この場合はラムダ式)から決定可能であることが必要です。しかし当然ながら、ラムダ式が実装する関数型インタフェース・タイプは、そのラムダ式の代入先によって決まります。言い換えるなら、varが示す型を決めるためには、ラムダ式の型が決まっていなければなりません。しかし、ラムダ式の型を決めるためには、ラムダ式を代入する型がわかっていなければなりません。この場合、その型がvarになっています。このAはBであり、BはAであるという循環依存は論理的に解決できないため、選択肢Aのような形でラムダ式とvar疑似タイプを一緒に使うことはできません。そのため、選択肢Aは誤りであることがわかります。


その他の選択肢では、コア関数型インタフェースをどう組み合わせればラムダ式を正しく記述できるかについて確認することになります。ここで考える必要があるのは、設問にある4つの関数型インタフェースの型要件です。その4つとは、Supplier、Consumer、Function、Predicateです。APIドキュメントを見てみると、それぞれ次の形式の抽象メソッドが宣言されていることがわかります。

public interface Supplier<T> { T get(); }
public interface Consumer<T> { void accept(T t); }
public interface Function<T, R> { R apply(T t); }
public interface Predicate<T> { boolean test(T t); }

ラムダ式が有効であるためには、ラムダ式の引数リストと、そのラムダ式が実装しているインタフェースで要求される引数リストとが一致しなければなりません。さらに、ラムダ式の戻りタイプの振る舞いも適切なものでなければなりません(ここで適切というのは、自動変換が可能という意味です)。


思い出しておくと、ここで探しているのは、0個の引数を受け取り、その戻り値が、1個の引数を受け取ってvoidを返すラムダ式であるラムダ式を表す型です。それでは、戻りタイプを考えない場合、0個の引数を受け取るラムダ式に対応しているのは4つのうちのどれでしょうか。この条件に該当するのは、Supplierだけです。ここから、選択肢C、D、Eは誤りであることがわかります。残ったのはもちろん選択肢Bだけですが、解答をやめる前に、この選択肢が完全に正しいかどうかを確認する必要があります。


選択肢Bでは、Supplier<Consumer<?>>を定義しています。ラムダ式は0個の引数を受け取らなければならないと規定されていますが、実際にそのとおりになっています。ここまでは問題ありません。次に、戻りタイプについて考えてみます。選択肢Bでは、戻りタイプがConsumerであるとしています(厳密に言えば、Consumer<?>です。しかしこれは、Objectに代入できる何かのConsumerであると言っているだけであるため、Consumerの引数について何かがわかるわけではありません)。このSupplierの戻り値は(a) -> {}です。このラムダ式は、1つの引数を受け取ってvoidを返します。これはConsumerと一致します。そこから、選択肢Bのコードは完全に正しく、選択肢Bが確かに正解だと判断できます。

正解は選択肢Bです。

 

Java Magazine 日本版Vol.49の他の記事

Java 14でのJava Flight RecorderとJFR Event Streaming
多くの新機能を搭載したJava 14が登場
Java EEからJakarta EEへ
クイズに挑戦:ラムダ式の型(上級者向け)
クイズに挑戦:2次元配列(中級者向け)
クイズに挑戦:ラムダ式(上級者向け)
クイズに挑戦:関数型インタフェース(上級者向け)


Simon Roberts

Simon Roberts:Sun Microsystemsがイギリスで初めてJavaの研修を行う少し前にSunに入社し、Sun認定Javaプログラマー試験とSun認定Java開発者試験の作成に携わる。複数のJava認定ガイドを執筆し、現在はフリーランスでPearson InformITにおいて録画やライブによるビデオ・トレーニングを行っている(直接またはO'Reilly Safari Books Onlineサービス経由で視聴可能)。OracleのJava認定プロジェクトにも継続的に関わっている。

 

Mikalai Zaikin

Mikalai Zaikin:ベラルーシのミンスクを拠点とするIBA IT ParkのリードJava開発者。OracleによるJava認定試験の作成に携わるとともに、複数のJava認定教科書のテクニカル・レビューを行っている。Kathy Sierra氏とBert Bates氏による有名な学習ガイド『Sun Certified Programmer for Java』では、3版にわたってテクニカル・レビューを務めた。


※本記事は、Simon Roberts、Mikalai Zaikinによる”Quiz Yourself: Lambda Types (Advanced)“を翻訳したものです。

Be the first to comment

Comments ( 0 )
Please enter your name.Please provide a valid email address.Please enter a comment.CAPTCHA challenge response provided was incorrect. Please try again.