kagamihogeの日記

kagamihogeの日記です。

Java SE 8: Lambda Quick Startを適当に訳した

Java SE 8: Lambda Quick Start

JDK 8のLambda式勉強する素材なんか無いもんかね、と適当に検索したらコレがひっかかった。なのでテキトーに訳してみた。



Overview

目的

このチュートリアルは、Java Platform Standard Edition 8 (Java SE 8)に含まれる新機能Lambda式を紹介します。

所要時間

約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パッケージのPredicateFunctionの使い方も示します。

最後に、JavaのコレクションがLambda式でどのように更新されたかを紹介します。

本文中のサンプルのソースコードはすべてダウンロード可能です(※原文中のリンクを参照)。

Hardware and Software Requirements
必須要件

サンプルのソースコードを実行するためには、Lambdaサポート版のNetBeans 8とJDK 8が必須です。リンクはOpenJDK: Project Lambdaから辿れます。または、下記のリンクから飛んでください。

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");
            }
        });
    }
}

*1

もしくは、各イベントごとに要求される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に加えて、RunnableComparatorでも同様な使われ方をします。それゆえに、 関数型インタフェースは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文のように評価されます。breakcontinueキーワードはトップレベルではコンパイルエラーですが、ループ内では使用可能です。もし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オブジェクトで構成されたArrayListsurName(姓)に基づいてソートされます。

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は以下を含むコンテキストで使用されています:

  • 変数宣言 Variable declarations
  • 代入 Assignments
  • return文 Return statements
  • 配列初期化 Array initializers
  • メソッドもしくはコンストラクタ引数 Method or constructor arguments
  • Lambda式のボディ Lambda expression bodies
  • 三項演算子 Conditional expressions ?:
  • キャスト Cast expressions

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に対して行われます。しかしながら、この設計には負の側面が見られます。

  • DRY原則に違反している。
    • それぞれのメソッドが同一のループ構造を取っている。
    • それぞれのメソッドごとに検索条件を書き換えなければならない。
  • ユースケースを実装するために要求されるメソッドの数が多すぎる
  • このコードは柔軟性が無い。もし検索条件が変更されたら、変更のためにコードの多くを変更しなければらならず、メンテナンス性を低下させている。
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);

*8

この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を持ちます。この例では、TPersonクラスとRStringを返します。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));
        }
    }
}

大変シンプルになりました。ただ一つのメソッドがあり、ListFunctionとリストのタイトルを引数に取ります。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に割り当てられています。どのようにPersonStringクラスが変数定義の一部となっているかに注意してみてください。印字を行うためのコードは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);
}

変数名がちょろっと変更されているものの、前のセクションの印字インタフェースと本質的には変わりありません。それぞれのFunctionMapに格納されて、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の結果に基づいてListSetを返すことが出来ます。この例は、イテレート後に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を使用しています。totalAgelongな点に注意してください。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のStream stream()というシグネチャなので、「入力として(as input )」ってのはなんかヘンな訳なのだが…意味は通じるから、まぁいっか(適当)

*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-