JDK 8のLambda式勉強する素材なんか無いもんかね、と適当に検索したらコレがひっかかった。なのでテキトーに訳してみた。
Overview
所要時間
約1時間
はじめに
Lambda式はJava SE 8に含まれる重要な新機能です。Lambda式は、式で単一のメソッドからなるインターフェースを表現するための簡潔な方法を提供します。Lambda式はまた、Collectionライブラリのフィルタ・Collectionからのデータ抽出・イテレートを簡単にします。加えて、並行性に関する新機能がマルチコア環境でのパフォーマンスを向上させます。
このOracle by Example (OBE) は、Java SE 8に含まれるLambda式の初めての人向けの位置付けです。この文章anonymous inner functionsの紹介に続けて、関数型インタフェース(functional interfaces)とLambda式のシンタックスについて述べます。その後、Lambda式導入前後でよくある使い方がどのように異なるかを示します。
その次のセクションでは、よくある探索の書き方と、JavaのコードがLambda式の導入によってどのように改良されるかを検討します。また、common functional interfacesのいくつか、java.util.functionパッケージのPredicateとFunctionの使い方も示します。
最後に、JavaのコレクションがLambda式でどのように更新されたかを紹介します。
本文中のサンプルのソースコードはすべてダウンロード可能です(※原文中のリンクを参照)。
Hardware and Software Requirements
- Java Development Kit (JDK 8) built with lambda support.
- An early access version of NetBeans 8 that supports lambda expressions.
必須要件
サンプルのソースコードを実行するためには、Lambdaサポート版のNetBeans 8とJDK 8が必須です。リンクはOpenJDK: Project Lambdaから辿れます。または、下記のリンクから飛んでください。
- Javaプログラミングの基礎知識
- Java Platform, Standard Edition 8 Early Access Releases — Project Kenai
- http://bertram2.netbeans.org:8080/job/jdk8lambda/lastSuccessfulBuild/artifact/nbbuild/
- 注意:ビルドはたいていのOS向けに提供されています。この文章はLinux Mint 13 (Ubuntu/Debian)を使用して書かれています。
- これを書いている人間(id:kagamihoge)の環境はこちら→JDK 8のLambdaをEclipseで学ぶ - kagamihogeの日記
Background
無名内部クラス (Anonymous Inner Class)
Javaでは、無名内部クラスはアプリケーション内で一箇所にだけ現れるクラスを実装する方法を提供します。たとえば、SwingやJavaFXアプリケーションの多くのイベントハンドラはキーボードとマウスイベントにとって必要です。各イベントごとにイベントハンドラのクラスを分離させるよりも、たいていの人は下記のように書くはずです。
import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import javax.swing.JButton; public class Main { public static void main(String[] args) { JButton testButton = new JButton("Test Button"); testButton.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { System.out.println("Click Detected by Anon Class"); } }); } }
もしくは、各イベントごとに要求されるActionListenerを実装するクラスを作成します。インタフェースが要求される所定の位置でクラスを作成することによって、コードは簡潔になります。ただ一つのメソッドを定義するためだけにかなりのコードが要求されるのはエレガントではありません。
関数型インタフェース (Functional Interfaces)
下記のコードはActionListenerから抜粋してきたコードです。
package java.awt.event; import java.util.EventListener; public interface ActionListener extends EventListener { public void actionPerformed(ActionEvent e); }
このActionListenerは、ただ一つのメソッドのみによって構成されたinterfaceの一例です。Java SE 8では、このようなinterfaceは、関数型インタフェース("functional interface")と呼ばれるパターンです。
note:このようなinterfaceは、以前はSingle Abstract Method type (SAM)と呼ばれていました。
無名内部クラスに関数型インタフェースを使用することは、Javaでの共通パターンです。EventListenerに加えて、RunnableやComparatorでも同様な使われ方をします。それゆえに、 関数型インタフェースはLambda式の主要な用途になっています。
Lambda式の構文 (Lambda Expression Syntax)
Lambda式は、無名クラスの長ったらしい5行のコードを単一のステートメントへ組み替えます。このシンプルな水平的解決(horizontal solution)は内部クラスの長ったらしさ問題(vertical problem)を解決します*2。
Lambda式は三つのパーツで構成されます。
Argument List | Arrow Token | Body |
(int x, int y) | -> | x + y |
bodyは、単一式もしくは複数ステートメントのブロック、どちらでも可能です。単一式のときは、ただ単純にbodyが評価されて返されます。複数ステートメントのブロックのときは、bodyはメソッドボディと無名メソッドの呼び出し元へ制御を返すreturn文のように評価されます。breakとcontinueキーワードはトップレベルではコンパイルエラーですが、ループ内では使用可能です。もしbodyが結果を返すなら、何らかの値を返すか例外をスローしなければなりません。
以下にサンプルを示します。
(int x, int y) -> x + y () -> 42 (String s) -> { System.out.println(s); }
最初の式は、二つのintの引数(x, y)を取りreturn x+yを表現してします。次の式は、引数を取らず単にreturn 42することを表現しています。三つ目の例は、文字列を引数に取りそれを標準出力に出力し、何もreturnしません。
構文の基本的な部分は以上なので、次のセクションからいくつかの例を見て行きます。
Lambda Examples
Runnable Lambda
下記のコードはLambda式を使用したRunnableの例です。
public class RunnableTest { public static void main(String[] args) { System.out.println("=== RunnableTest ==="); // Anonymous Runnable Runnable r1 = new Runnable() { @Override public void run() { System.out.println("Hello world one!"); } }; // Lambda Runnable Runnable r2 = () -> { System.out.println("Hello world two!"); }; // Run em! r1.run(); r2.run(); } }
どちらの場合でも、引数を取らないし何も返り値を取りません。Lambda式のRunnableでは、ブロック形式({...})を使用し、従来の無名クラスでは5行要したのを1行で書けています。
Comparator Lambda
Javaでは、Comparatorクラスはコレクションのソートに使用されます。下記の例では、Personオブジェクトで構成されたArrayListがsurName(姓)に基づいてソートされます。
public class Person { private String givenName; private String surName; private int age; private Gender gender; private String eMail; private String phone; private String address; //以下、getter //適当なPersonインスタンス複数個作ってリストに入れて返すメソッド public static List<Person> createShortList() { List<Person> people = new ArrayList<>(); people.add(new Person.Builder().givenName("Bob").surName("Baker").age(21).gender(Gender.MALE) .email("bob.baker@example.com").phoneNumber("201-121-4678").address("44 4th St, Smallville, KS 12333") .build()); ...
無名内部クラスとLambda式を使用してComparatorに適用したコードの例。
public class ComparatorTest { public static void main(String[] args) { List<Person> personList = Person.createShortList(); // Sort with Inner Class Collections.sort(personList, new Comparator<Person>() { public int compare(Person p1, Person p2) { return p1.getSurName().compareTo(p2.getSurName()); } }); System.out.println("=== Sorted Asc SurName ==="); for (Person p : personList) { p.printName(); } // Use Lambda instead Comparator<Person> SortSurNameAsc = (Person p1, Person p2) -> p1.getSurName().compareTo(p2.getSurName()); Comparator<Person> SortSurNameDesc = (p1, p2) -> p2.getSurName().compareTo(p1.getSurName()); // Print Asc System.out.println("=== Sorted Asc SurName ==="); Collections.sort(personList, SortSurNameAsc); for (Person p : personList) { p.printName(); } // Print Desc System.out.println("=== Sorted Desc SurName ==="); Collections.sort(personList, SortSurNameDesc); for (Person p : personList) { p.printName(); } } }
Sort with Inner Classコメントの無名内部クラスのコードを、Lambda式で置き換えたのがUse Lambda insteadコメントの部分です。Lambda式の最初の例は、Lambda式へ渡す引数を宣言しています。その次のLambda式の例を見ると、その宣言はオプショナルであると分かります。Lambdaは"target typing"をサポートしており、これはコンテキストからオブジェクトの型を推論します。我々は、ジェネリクスで定義されたComparatorを返り値に割り当てているので、コンパイラは二つのパラメータをPerson型と推論できます。
Listener Lambda
最後に、ActionListenerの例を再検討します。note:この例はSwingを使用しているが、将来的にはJavaFXに更新される予定です。
import java.awt.BorderLayout; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import javax.swing.JButton; import javax.swing.JFrame; public class ListenerTest { public static void main(String[] args) { JButton testButton = new JButton("Test Button"); testButton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent ae) { System.out.println("Click Detected by Anon Class"); } }); testButton.addActionListener(e -> { System.out.println("Click Detected by Lambda Listner"); }); // Swing stuff JFrame frame = new JFrame("Listener Test"); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.add(testButton, BorderLayout.CENTER); frame.pack(); frame.setVisible(true); } }
Lambda式は引数として渡せます。Target typingは以下を含むコンテキストで使用されています:
Improving Code with Lambda Expressions
このセクションでは、どのようにLambda式がコードを改良するのかを示すために以前の例に沿って進めます。LambdaはDRY原則をサポートするための良き手段であり、コードをシンプルかつ読みやすくします。
A Common Query Use Case
プログラムでの良くある使い方は、コレクションを通して検索条件にマッチするデータを探索することです。
JavaOne 2012での素晴らしいプレゼンテーションJump-Starting Lambda Programming - YouTubeでは、Stuart MarksとMike Duigouがそのような事例についてウォークスルーを行いました。適当な人数のリストが与えられて、様々な検索条件がRobocall(automated phone calls*3 *4)をかけるためのマッチングに使われます。
この例では、我々のメッセージをUnited Statesの三つの異なるグループにお知らせする必要があります。
- 運転手 : 16歳以上
- 召集兵 : 18歳から25歳の男性
- パイロット(特に民間航空会社のパイロット): 23歳から65歳
実際にはこのロボットはまだ我々のビジネス環境に達していません*5。なので、電話・郵送・電子メールの代わりに、メッセージをコンソールに出力します。メッセージは、対象者の人名・年齢とターゲット媒体の具体的な情報(例えば、電信メールのときはメールアドレスないし電話のときは電話番号)を含めます。
Person Class
Personはこんな感じ。
public class Person { private String givenName; private String surName; private int age; private Gender gender; private String eMail; private String phone; private String address; //以下、getter //適当なPersonインスタンス複数個作ってリストに入れて返すメソッド public static List<Person> createShortList() { List<Person> people = new ArrayList<>(); people.add(new Person.Builder().givenName("Bob").surName("Baker").age(21).gender(Gender.MALE) .email("bob.baker@example.com").phoneNumber("201-121-4678").address("44 4th St, Smallville, KS 12333") .build()); ...
Personクラスは新しいオブジェクトを生成するためにBuilderを使用しています。PersonのリストはcreateShortListメソッドで生成されます。下記はそのメソッドの一部のみを載せています。note:チュートリアルの全ソースコードはこのセクションの末尾にNetBeansプロジェクト形式でまとめたもののリンクが置いてあります*6。
Person Class
public static List<Person> createShortList() { List<Person> people = new ArrayList<>(); people.add(new Person.Builder() .givenName("Bob").surName("Baker") .age(21) .gender(Gender.MALE) .email("bob.baker@example.com") .phoneNumber("201-121-4678") .address("44 4th St, Smallville, KS 12333") .build()); //以下、いくつかPersonインスタンス作る処理が続く。
A First Attempt
Personクラスと検索条件を使用して、RoboContactクラスを作成します。各検索条件を一つずつ実現するために、一つずつメソッドを定義します。
RoboContactsMethods.java
public class RoboContactMethods { public void callDrivers(List<Person> pl) { for (Person p : pl) { if (p.getAge() >= 16) { roboCall(p); } } } public void emailDraftees(List<Person> pl) { for (Person p : pl) { if (p.getAge() >= 18 && p.getAge() <= 25 && p.getGender() == Gender.MALE) { roboEmail(p); } } } public void mailPilots(List<Person> pl) { for (Person p : pl) { if (p.getAge() >= 23 && p.getAge() <= 65) { roboMail(p); } } } public void roboCall(Person p) { System.out.println("Calling " + p.getGivenName() + " " + p.getSurName() + " age " + p.getAge() + " at " + p.getAge()); } public void roboEmail(Person p) { System.out.println("EMailing " + p.getGivenName() + " " + p.getSurName() + " age " + p.getAge() + " at " + p.getEmail()); } public void roboMail(Person p) { System.out.println("Mailing " + p.getGivenName() + " " + p.getSurName() + " age " + p.getAge() + " at " + p.getAddress()); } }
上記のクラスには、検索条件それぞれの振る舞いを記述しているメソッド(callDrivers, emailDraftees, mailPilots)があります。検索条件は明確に示されており、適切な呼び出しがそれぞれのrobo actionに対して行われます。しかしながら、この設計には負の側面が見られます。
Refactor the Methods
どのようにクラスを修正すべきか? 検索条件から手を付けるのが良さそうです。条件のチェック部分をメソッドに切り出せば、改善しそうです。
RoboContactMethods2.java
public class RoboContactMethods2 { public void callDrivers(List<Person> pl) { for (Person p : pl) { if (isDriver(p)) { roboCall(p); } } } public void emailDraftees(List<Person> pl) { for (Person p : pl) { if (isDraftee(p)) { roboEmail(p); } } } public void mailPilots(List<Person> pl) { for (Person p : pl) { if (isPilot(p)) { roboMail(p); } } } public boolean isDriver(Person p) { return p.getAge() >= 16; } public boolean isDraftee(Person p) { return p.getAge() >= 18 && p.getAge() <= 25 && p.getGender() == Gender.MALE; } public boolean isPilot(Person p) { return p.getAge() >= 23 && p.getAge() <= 65; } public void roboCall(Person p) { System.out.println("Calling " + p.getGivenName() + " " + p.getSurName() + " age " + p.getAge() + " at " + p.getPhone()); } public void roboEmail(Person p) { System.out.println("EMailing " + p.getGivenName() + " " + p.getSurName() + " age " + p.getAge() + " at " + p.getEmail()); } public void roboMail(Person p) { System.out.println("Mailing " + p.getGivenName() + " " + p.getSurName() + " age " + p.getAge() + " at " + p.getAddress()); } }
検索条件はメソッドにカプセル化され、一つ前のサンプルよりかはマシになりました。条件のチェック部分は使いまわしが可能になり、ここを変更することでクラス全体に変更が波及するようになりました。しかしながら、未だ繰り返し現れているコードが存在していて、切り出したメソッドは依然として各ユースケースのために必要です。メソッドに検索条件を渡すより良い方法は無いでしょうか?
Anonymous Classes
Lambda式以前は、無名内部クラスが選択肢の一つでした。例えば、booleanを返す一つのメソッドtestからなるインタフェース(MyTest.java)を作ることが解の一つでした。検索条件はメソッドが呼ばれた時に渡されます。そのインタフェースはこんな感じ。
public interface MyTest<T> { public boolean test(T t); }
ロボットクラス本体は下記のように書き換えられます。
RoboContactsAnon.java
import java.util.List; public class RoboContactAnon { public void phoneContacts(List<Person> pl, MyTest<Person> aTest) { for (Person p : pl) { if (aTest.test(p)) { roboCall(p); } } } public void emailContacts(List<Person> pl, MyTest<Person> aTest) { for (Person p : pl) { if (aTest.test(p)) { roboEmail(p); } } } public void mailContacts(List<Person> pl, MyTest<Person> aTest) { for (Person p : pl) { if (aTest.test(p)) { roboMail(p); } } } public void roboCall(Person p) { System.out.println("Calling " + p.getGivenName() + " " + p.getSurName() + " age " + p.getAge() + " at " + p.getPhone()); } public void roboEmail(Person p) { System.out.println("EMailing " + p.getGivenName() + " " + p.getSurName() + " age " + p.getAge() + " at " + p.getEmail()); } public void roboMail(Person p) { System.out.println("Mailing " + p.getGivenName() + " " + p.getSurName() + " age " + p.getAge() + " at " + p.getAddress()); } }
これは間違いなくもう一つの改良案であり、ロボットに対する命令を出すメソッドは三つのみになりました。しかしながら、これらのメソッドが呼び出されるときに長ったらしさという若干の問題が表れます。下記のこのクラスを使ってみたコードを見てみて下さい。
RoboCallTest03.java
public class RoboCallTest03 { public static void main(String[] args) { List<Person> pl = Person.createShortList(); RoboContactAnon robo = new RoboContactAnon(); System.out.println("\n==== Test 03 ===="); System.out.println("\n=== Calling all Drivers ==="); robo.phoneContacts(pl, new MyTest<Person>() { @Override public boolean test(Person p) { return p.getAge() >= 16; } }); System.out.println("\n=== Emailing all Draftees ==="); robo.emailContacts(pl, new MyTest<Person>() { @Override public boolean test(Person p) { return p.getAge() >= 18 && p.getAge() <= 25 && p.getGender() == Gender.MALE; } }); System.out.println("\n=== Mail all Pilots ==="); robo.mailContacts(pl, new MyTest<Person>() { @Override public boolean test(Person p) { return p.getAge() >= 23 && p.getAge() <= 65; } }); } }
これこそ、まさしく"長ったらしさ"問題("vertical" problem) の一例です。このコードを読み解くのは少々骨が折れます。加えて、それぞれのユースケースごとにカスタムした検索条件を書かなければなりません。
Lambda Expressions Get it Just Right
Lambda式はここまでに発生したすべての問題を解決します。But first a little housekeeping.*7
java.util.function
以前の例では、関数型インタフェースMyTestはメソッドに無名クラスを渡していました。しかしながら、このインタフェースを作成する必要は無くなりました。Java SE 8は標準的な関数型インタフェースの多数を備えるjava.util.functionパッケージを提供します。この例の場合、Predicateが我々の要求にマッチします。
@FunctionalInterface public interface Predicate<T> { boolean test(T t);
このtestメソッドはジェネリックなクラスを引数に取りbooleanを返します。このインタフェースは、何かの中から条件に合致したものを選び出すことを表現しています。以下に、ロボットクラスの最後のバージョンを示します。
RoboContactsLambda.java
public class RoboContactLambda { public void phoneContacts(List<Person> pl, Predicate<Person> pred) { for (Person p : pl) { if (pred.test(p)) { roboCall(p); } } } public void emailContacts(List<Person> pl, Predicate<Person> pred) { for (Person p : pl) { if (pred.test(p)) { roboEmail(p); } } } public void mailContacts(List<Person> pl, Predicate<Person> pred) { for (Person p : pl) { if (pred.test(p)) { roboMail(p); } } } public void roboCall(Person p) { System.out.println("Calling " + p.getGivenName() + " " + p.getSurName() + " age " + p.getAge() + " at " + p.getPhone()); } public void roboEmail(Person p) { System.out.println("EMailing " + p.getGivenName() + " " + p.getSurName() + " age " + p.getAge() + " at " + p.getEmail()); } public void roboMail(Person p) { System.out.println("Mailing " + p.getGivenName() + " " + p.getSurName() + " age " + p.getAge() + " at " + p.getAddress()); } }
このアプローチによって、各媒体ごとにコンタクトを取るメソッドを合計三種類作るだけで良くなりました。Lambda式は、それらのメソッドに、条件チェックに合致するPersonインスタンスを選択する関数を渡します。
Vertical Problem Solved
Lambda式は長ったらしさ問題(vertical problem)を解消し、式の使いまわしを容易にします。
RoboCallTest04.java
public class RoboCallTest04 { public static void main(String[] args) { List<Person> pl = Person.createShortList(); RoboContactLambda robo = new RoboContactLambda(); // Predicates Predicate<Person> allDrivers = p -> p.getAge() >= 16; Predicate<Person> allDraftees = p -> p.getAge() >= 18 && p.getAge() <= 25 && p.getGender() == Gender.MALE; Predicate<Person> allPilots = p -> p.getAge() >= 23 && p.getAge() <= 65; System.out.println("\n==== Test 04 ===="); System.out.println("\n=== Calling all Drivers ==="); robo.phoneContacts(pl, allDrivers); System.out.println("\n=== Emailing all Draftees ==="); robo.emailContacts(pl, allDraftees); System.out.println("\n=== Mail all Pilots ==="); robo.mailContacts(pl, allPilots); // Mix and match becomes easy System.out.println("\n=== Mail all Draftees ==="); robo.mailContacts(pl, allDraftees); System.out.println("\n=== Call all Pilots ==="); robo.phoneContacts(pl, allPilots); } }
Predicateは、三つの異なるグループ向けに構築されています:allDrivers, allDraftees, allPilots. これらのPredicateインタフェースは各媒体ごとのコンタクトメソッドのいずれにも渡せます。コードはコンパクトになり、読みやすく、重複することもなくなりました。
The java.util.function Package
もちろん、PredicateだけがJava SE 8で用意されている関数型インタフェースというわけではありません。標準的なインタフェースの多くが開発者のために用意されています。
Predicate: 引数として渡されるオブジェクトです。
Consumer: 引数として渡されるオブジェクトを使用して実行するアクションです。
Function: TからUへ変換します。
Supplier: (Factoryのように)Tのインスタンスを生成します
UnaryOperator: 単項演算子 T -> T。
BinaryOperator: 二項演算子 (T, T) -> T。
加えて、これらのインタフェースはプリミティブ型バージョンも持ち合わせています*9。
Asian Style Names and Function
以前の例を作成していたとき、私はPersonクラスに柔軟な印字システムを持つと良いだろうと考えていました。この機能要求は洋風と和風の両方で氏名を表示できるというものです。西洋では、氏名は名が先で姓はその次です。多くの東洋文化圏では、氏名は姓が先で名は後です。
An Old Style Example
Lambda式を使わないでPersonクラスに出力の要件を実装したのが下記のコードです。
PersonWriterOld.java
public class PersonWriterOld { public void writeShortWesternName(List<Person> pl) { System.out.println("=== Short Western List ==="); for (Person p : pl) { System.out.println("\nName: " + p.getGivenName() + " " + p.getSurName() + "\n" + "EMail: " + p.getEmail() + "\n" + "Phone: " + p.getPhone()); } } public void writeShortAsianName(List<Person> pl) { System.out.println("=== Short Asian List ==="); for (Person p : pl) { System.out.println("\nName: " + p.getSurName() + " " + p.getGivenName() + "\n" + "EMail: " + p.getEmail() + "\n" + "Phone: " + p.getPhone()); } } public void writeFullWesternName(List<Person> pl) { System.out.println("=== Full Western List ==="); for (Person p : pl) { System.out.println("\nName: " + p.getGivenName() + " " + p.getSurName() + "\n" + "Age: " + p.getAge() + " " + "Gender: " + p.getGender() + "\n" + "EMail: " + p.getEmail() + "\n" + "Phone: " + p.getPhone() + "\n" + "Address: " + p.getAddress()); } } public void writeFullAsianName(List<Person> pl) { System.out.println("=== Full Asian List ==="); for (Person p : pl) { System.out.println("\nName: " + p.getSurName() + " " + p.getGivenName() + "\n" + "Age: " + p.getAge() + " " + "Gender: " + p.getGender() + "\n" + "EMail: " + p.getEmail() + "\n" + "Phone: " + p.getPhone() + "\n" + "Address: " + p.getAddress()); } } }
The Function Interface
Functionインタフェースはこの問題に役立ちます。このインタフェースは下記のシグネチャのapplyメソッドのみ持っています
public R apply(T t){ }
ジェネリックなクラスTと返り値のジェネリッククラスRを持ちます。この例では、TにPersonクラスとRでStringを返します。PersonWriterクラスを書き直すと下記のようになります。
PersonWriterNew.java
import java.util.List; import java.util.function.Function; public class PersonWriterNew { public void printLambdaList(List<Person> pl, Function<Person, String> f, String description) { System.out.println(description); for (Person p : pl) { System.out.println(f.apply(p)); } } }
大変シンプルになりました。ただ一つのメソッドがあり、ListとFunctionとリストのタイトルを引数に取ります。applyメソッドは、渡されたFunctionインスタンスに基づいてメソッドが、Personそれぞれの印字を行います。
Functionはどのように定義されているのでしょうか?
NameTestNew.java
import java.util.List; import java.util.function.Function; public class NameTestNew { public static void main(String[] args) { System.out.println("\n==== NameTestNew02 ==="); List<Person> list1 = Person.createShortList(); PersonWriterNew pw = new PersonWriterNew(); // Define Lambda Functions Function<Person, String> shortWestern = p -> { return "\nName: " + p.getGivenName() + " " + p.getSurName() + "\n" + "EMail: " + p.getEmail() + "\n" + "Phone: " + p.getPhone(); }; Function<Person, String> shortAsian = p -> "\nName: " + p.getSurName() + " " + p.getGivenName() + "\n" + "EMail: " + p.getEmail() + "\n" + "Phone: " + p.getPhone(); Function<Person, String> fullWestern = p -> { return "\nName: " + p.getGivenName() + " " + p.getSurName() + "\n" + "Age: " + p.getAge() + " " + "Gender: " + p.getGender() + "\n" + "EMail: " + p.getEmail() + "\n" + "Phone: " + p.getPhone() + "\n" + "Address: " + p.getAddress(); }; Function<Person, String> fullAsian = p -> "\nName: " + p.getSurName() + " " + p.getGivenName() + "\n" + "Age: " + p.getAge() + " " + "Gender: " + p.getGender() + "\n" + "EMail: " + p.getEmail() + "\n" + "Phone: " + p.getPhone() + "\n" + "Address: " + p.getAddress(); // print list pw.printLambdaList(list1, fullWestern, "\n==== Full Western Style ===="); pw.printLambdaList(list1, shortAsian, "\n==== Short Asian Style ===="); } }
それぞれの印字の振る舞いがFunctionに割り当てられています。どのようにPersonとStringクラスが変数定義の一部となっているかに注意してみてください。印字を行うためのコードはLambda式の文あるいはreturn文を持つブロックとして定義されています。
Lambda式によって、Mapに自身を使いまわししやすくする仕組みを組み込めます。
Lambda Expressions and Collections
前のセクションではFunctionインタフェースとLambda式の基本的な文法を紹介しました。このセクションでは、Lambda式がCollectionクラスをどのように改良するかについて見ていきます。
Lambda Expressions and Collections
これまでに作成した例では、コレクションのクラスはソレナリに使用しました。しかしながら、新しいLambda式の機能の多くによってコレクションの使用方法が別モンになります。このセクションではいくらかの新機能を紹介します。
Improvements to Person
Lambdaのコレクションの機能を学んでいく前に、これ以降のサンプル用に手を加えられたPersonクラスに対する変更について見ておきます。まず、Personクラスに前の章の印字機能を格納するためのMapが含まれています。前の章では、すべてのLambda式はアドホックでしたが、Personが望んだときにはいつでもMapに格納されたLambda式の使い回しによる出力が出来るようになりました。
まず、Mapを含むその辺のコードを見て行きます。
Person.java
public class Person { private String givenName; private String surName; private int age; private Gender gender; private String eMail; private String phone; private String address; private final Map<String, Function> printMap = new HashMap<>();
Lambda Functionを識別するためにStringを使用しています。必要なときに、印字スタイルはそのStringを使用して取得されます。
印字スタイルの初期化と取得は以下の通りです。
private void initPrintMap() { // Print name and phone western style Function<Person, String> westernNameAgePhone = p -> "\n" + p.getGivenName() + " " + p.getSurName() + "\n" + "Age: " + p.getAge() + "\n" + "Phone: " + p.getPhone(); Function<Person, String> asianNameAgePhone = p -> "\n" + p.getSurName() + " " + p.getGivenName() + "\n" + "Age: " + p.getAge() + "\n" + "Phone: " + p.getPhone(); Function<Person, String> fullWestern = p -> { return "\nName: " + p.getGivenName() + " " + p.getSurName() + "\n" + "Age: " + p.getAge() + " " + "Gender: " + p.getGender() + "\n" + "EMail: " + p.getEmail() + "\n" + "Phone: " + p.getPhone() + "\n" + "Address: " + p.getAddress(); }; Function<Person, String> fullAsian = p -> "\nName: " + p.getSurName() + " " + p.getGivenName() + "\n" + "Age: " + p.getAge() + " " + "Gender: " + p.getGender() + "\n" + "EMail: " + p.getEmail() + "\n" + "Phone: " + p.getPhone() + "\n" + "Address: " + p.getAddress(); printMap.put("westernNameAgePhone", westernNameAgePhone); printMap.put("asianNameAgePhone", asianNameAgePhone); printMap.put("fullWestern", fullWestern); printMap.put("fullAsian", fullAsian); } public Function<Person, String> getPrintStyle(String functionName) { return printMap.get(functionName); }
変数名がちょろっと変更されているものの、前のセクションの印字インタフェースと本質的には変わりありません。それぞれのFunctionはMapに格納されて、getPrintStyleメソッドによって簡単に取得できます。
印字の振る舞いに加えて、グループ(運転手・パイロット・召集兵)選択のロジックもカプセル化される必要が出てきました。
import java.util.HashMap; import java.util.Map; import java.util.function.Predicate; /** * * @author MikeW */ public class SearchCriteria { private final Map<String, Predicate> searchMap = new HashMap<>(); private SearchCriteria() { super(); initSearchMap(); } private void initSearchMap() { Predicate<Person> allDrivers = p -> p.getAge() >= 16; Predicate<Person> allDraftees = p -> p.getAge() >= 18 && p.getAge() <= 25 && p.getGender() == Gender.MALE; Predicate<Person> allPilots = p -> p.getAge() >= 23 && p.getAge() <= 65; searchMap.put("allDrivers", allDrivers); searchMap.put("allDraftees", allDraftees); searchMap.put("allPilots", allPilots); } public Predicate<Person> getCriteria(String PredicateName) { Predicate<Person> target; target = searchMap.get(PredicateName); if (target == null) { System.out.println("Search Criteria not found... "); System.exit(1); } return target; } public static SearchCriteria getInstance() { return new SearchCriteria(); } }
このクラスは、検索条件に基づくPredicateを格納しておきます。
Looping
最初の機能は、どんなコレクションクラスにも適用可能なforEachメソッドを見て行きます。
Test01ForEach.java
import java.util.List; import java.util.Map; import java.util.function.Function; /** * * @author MikeW */ public class Test01ForEach { public static void main(String[] args) { List<Person> pl = Person.createShortList(); System.out.println("\n=== Western Phone List ==="); pl.forEach(p -> { p.printl(p.getPrintStyle("westernNameAgePhone")); }); System.out.println("\n=== Asian Phone List ==="); pl.forEach(p -> { p.printl(p.getPrintStyle("asianNameAgePhone")); }); } }
どんなコレクションクラスでもこの方法で繰り返し処理が可能です。基本的な構造はforループの拡張したものに見えます。しかしながら、クラスに組み込まれた反復処理のメカニズムは多くの利点を提供します。
Chaining and Filters
コレクションの内容をループ処理することに加えて、メソッドを連鎖させられます。まず最初に、引数としてPredicateインタフェースを取るfilterを見て行きます。
Test02Filter.java
import java.util.List; /** * * @author MikeW */ public class Test02Filter { public static void main(String[] args) { List<Person> pl = Person.createShortList(); SearchCriteria search = SearchCriteria.getInstance(); System.out.println("\n=== Western Pilot Phone List ==="); pl.stream().filter(search.getCriteria("allPilots")) .forEach(p -> { p.printl(p.getPrintStyle("westernNameAgePhone")); }); System.out.println("\n=== Asian Phone List ==="); pl.forEach(p -> { p.printl(p.getPrintStyle("asianNameAgePhone")); }); System.out.println("\n=== Asian Draftee Phone List ==="); pl.stream().filter(search.getCriteria("allDraftees")) .forEach(p -> { p.printl(p.getPrintStyle("asianNameAgePhone")); }); } }
上のコードの最初と最後のループが、検索条件に基づいてListをフィルタする処理の例です。最後のループの出力は下記のようになります。
=== Asian Draftee Phone List === Baker Bob Age: 21 Phone: 201-121-4678 Doe John Age: 25 Phone: 202-123-4678
Getting Lazy
この新機能は便利ですが、なぜ、すでに充分動くforループが存在するのにコレクションクラスに機能追加が行われたのでしょうか? イテレーション機能をライブラリに移動することにより、Javaの開発者により多くのコード最適化が行えるようになります。より詳しく説明するために、二つの用語の定義を示します。
- Laziness: プログラミングにおいて、遅延(Laziness)とは、オブジェクトを処理する段階になった時点で対象となるオブジェクトのみを処理する方法を指します。前の例で言えば、最後のループが"遅延(lazy)"で、このループはListがフィルターされた後に残された二つのPersonオブジェクトのみをループ処理しているためです。最後の処理ステップは選択されたオブジェクトのみを対象とするので、コードはより効率的でなければなりません。
- Eagerness: リスト内の全オブジェクトに対する操作を実行するコードは"先行(eager)"と見なされます。例えば、拡張for文は二つのオブジェクトを処理するためにリスト全体を走査しますが、これは"先行(eager)"アプローチと見なされます。*10
コレクションライブラリのループ部分を構築することで、コードを"遅延(lazy)"操作に最適化が可能な場合であればそう出来るようになります。eagerアプローチが理にかなっているとき(例えば、合計や平均の計算)は、eager操作が適用されます。このアプローチは、eagerアプローチを常に使用するよりかははるかに効率的で柔軟です。
The stream Method
前のサンプルコードでは、フィルタリングとループを開始する前にstreamメソッドが呼ばれていることに気付かれたかと思います。このメソッドは入力として*11Collectionを取り、出力としてjava.util.stream.Streamインタフェースを返します。Streamは様々なメソッドをチェーンさせられるエレメントのシーケンスを表現しています。デフォルトでは、streamから利用可能なオブジェクトが無くなるまでエレメントを生成します。それゆえ、操作の連鎖がStreamで一度だけ引き起こされます。加えて、Streamはメソッドが呼ばれる方法によってシリアル(デフォルト)でもパラレルでも動作します。
Mutation and Results
前述のように、Streamは使用後に破棄されます。それゆえ、コレクション内の要素はStreamによって変更やミューテートは出来ません。しかしながら、もしチェーンさせた操作から返された要素のリストを保持したい場合にはどうすればよいのでしょうか? 新しいコレクションにそれらの要素を保存すればOKです。
Test03toList.java
import java.util.List; import java.util.stream.Collectors; /** * * @author MikeW */ public class Test03toList { public static void main(String[] args) { List<Person> pl = Person.createShortList(); SearchCriteria search = SearchCriteria.getInstance(); // Make a new list after filtering. List<Person> pilotList = pl .stream() .filter(search.getCriteria("allPilots")) .collect(Collectors.<Person>toList()); System.out.println("\n=== Western Pilot Phone List ==="); pilotList.forEach(p -> { p.printl(p.getPrintStyle("westernNameAgePhone")); }); } }
collectメソッドが一つの引数(Collectorsクラス)で呼ばれています。Collectorsクラスはstreamの結果に基づいてListかSetを返すことが出来ます。この例は、イテレート後にstreamの結果を新しいListに割り当てる方法を示しています。
Calculating with map
mapメソッドは、通常filterと共に使用します。このメソッドは、クラスのプロパティとそれをどう使うか、を引数に取ります。下記の例では、年齢に基づいて計算を実行するコードを示しています。
Test04Map.java
import java.util.List; import java.util.OptionalDouble; /** * * @author MikeW */ public class Test04Map { public static void main(String[] args) { List<Person> pl = Person.createShortList(); SearchCriteria search = SearchCriteria.getInstance(); // Calc average age of pilots old style System.out.println("== Calc Old Style =="); int sum = 0; int count = 0; for (Person p : pl) { if (p.getAge() >= 23 && p.getAge() <= 65) { sum = sum + p.getAge(); count++; } } long average = sum / count; System.out.println("Total Ages: " + sum); System.out.println("Average Age: " + average); // Get sum of ages System.out.println("\n== Calc New Style =="); long totalAge = pl .stream() .filter(search.getCriteria("allPilots")) .mapToInt(p -> p.getAge()) .sum(); // Get average of ages OptionalDouble averageAge = pl .parallelStream() .filter(search.getCriteria("allPilots")) .mapToDouble(p -> p.getAge()) .average(); System.out.println("Total Ages: " + totalAge); System.out.println("Average Age: " + averageAge.getAsDouble()); } }
*12
出力はこんな感じ。
== Calc Old Style == Total Ages: 150 Average Age: 37 == Calc New Style == Total Ages: 150 Average Age: 37.5
このプログラムはリスト内のパイロットの年齢の平均を計算します。最初のループはforループを使用した計算処理を行う古いスタイルの例です。次のループはシリアルストリームで各personの年齢を取得するためにmapを使用しています。totalAgeはlongな点に注意してください。mapメソッドはIntStreamオブジェクトを返します*13。このクラスはsumメソッドを含んでおりlongを返します。
note:二つ目のループで平均を計算するとき、再度年齢の合計を計算する必要はありません。が、sumメソッドでの例を示すためには有益なのでそのまんまにしています。
最後のループはstreamから年齢の平均を計算しています。parallelStreamメソッドがパラレルストリームを取得するために使われます。そのため、この計算処理は並行(concurrently)に行われます。戻り値の型も同様にちょっと異なったものになります。
Summary
このチュートリアルでは下記の使い方を学習しました:
- Javaの無名内部クラス
- Java SE 8で無名内部クラスを置き換えるLambda式
- Lambda式のシンタックス
- リスト上で探索を実行するPredicateインタフェース
- オブジェクトを処理して異なる型のオブジェクトを生成するFunction
- Java SE 8のLambda式をサポートするCollectionsに追加された新機能
Resources
Java SE 8とLambda式の詳細な情報は下記のリンクからどうぞ。
Credits
- Lead Curriculum Developer: Michael Williams
- QA: Juan Quesada Nunez
- Editor: Susan Moxley
感想とか
マジ疲れた。技術書の翻訳とかしてる人マジ尊敬するわ。英文として理解できても、それを日本語として意味ある文章に変換するのはホントしんどい。いやまぁ、見た感じそんな難しい文章でもなさそーだし分量もそーでもないから翻訳してみっかぁ〜と軽いノリで挑んだらこのザマといいますか。でもまぁ良い経験にはなりました。
*1:上記コードはSwingの場合。といっても、実行しても何も表示されないけど、そこはまぁ、多少はね?
*2:あんま上手く訳せないが、おそらく、無名内部クラスのタテに長ったらしくなる(最低でも5行になる)のを、Lambda式でヨコ1行に短くできるようになったのを、視覚的にverticalからhorizontalと表現しているのだと思われる。タブン。
*3:Robocallとは何ぞ? http://en.wikipedia.org/wiki/Robocall によると、テレマーケティングの一種。事前に録音したメッセージを、対象者リストに沿って電話をコンピュータ制御で自動でかける、というものらしい。テレマーケティングの他、政治キャンペーンでも使われるとかなんとか
*4:直訳すれば「自動電話」だが、Robocallの内容を踏まえると「自動発信」が近いのだろうか?
*5:The actual robot that does all this work has not yet arrived at our place of business. タブンあんま意味の無い文章。練習問題の「まくら」に相当するものだと思われ。実際にはそんなもんは無いから、コンソールへの出力で済ませるよ〜的なもの、だと思う
*6:原文の方を要参照
*7:「少々の家事」ってどゆ意味? 訳せない……
*8:実際のソースからコピーしてきたので、原文とはちょっと異なりFunctionalInterfaceアノテーションがついている
*9:参考:http://download.java.net/jdk8/docs/api/java/util/function/package-summary.html
*10:この辺、ヒジョーにビミョーな日本語な自覚があるのでカンベンして頂きたく……
*11:java.util.Collection
*12:手元の環境 http://d.hatena.ne.jp/kagamihoge/20131112/1384255400 では map となっているところを mapToInt, mapToDouble を変更しないとコンパイルできなかったので修正した
*13:http://download.java.net/jdk8/docs/api/java/util/stream/Stream.html#mapToInt-java.util.function.ToIntFunction-