ラムダ式で本領を発揮する関数型インターフェースとStream APIの基礎知識Java 8はラムダ式でここまで変わる(3)(2/3 ページ)

» 2014年04月30日 18時00分 公開
[長谷川智之株式会社ビーブレイクシステムズ]

Stream APIの概要

 Stream APIは主に配列やCollectionなどの集合体を基に、値の集計やデータを使った処理を行うAPIです。Stream APIでは、集合体を基にStreamのインスタンスを生成し、メソッドを呼び出して集計などの処理を行います。またStream APIは、ファイルの入出力などでも使うことが可能です。

Streamの特徴

 StreamとCollectionは集合体に対してのクラスです。しかしStreamの使用目的とCollectionの使用目的は根本的に違います。Collectionでは、それが保持する要素そのものの管理を目的にしているのに対し、Streamでは、保持する要素の値を使って何らかの結果の取得や処理を行うことを目的としています。

 またStreamで行う処理は大きく分けて2つあり、中間的な処理と最終的な処理に分けられます。

 中間的な処理では、Streamインスタンスの各要素を条件で絞った新たなStreamインスタンスの取得や、Streamインスタンスの各要素から別のデータを要素として持つStreamインスタンスを生成したりすることを行います。

 そして最終的な処理で受け取ったStreamインスタンスの要素から何らかの結果を取得したり要素を使って処理を行ったりします。Streamの処理の実装は0以上の中間的な処理と最後に1つの最終的な処理を順にメソッドで呼び出していく実装方法になっています。

 また注意事項として何らかの処理を実行したStreamに対して新たな処理を行うことはできません。

 例えば次のサンプルでは、Stream(originalStream)インスタンスからfilterメソッドで別のStream(stream1)インスタンスを取得した後に最初のStream(originalStream)インスタンスでさらにfilterメソッドを使った場合、例外が発生します。

Stream<String> originalStream = Arrays.stream(new String[]{"あ","い","う","え","お"});
Stream<String> stream1 = originalStream.filter(value -> !value.equals("あ"));
Stream<String> stream2 = originalStream.filter(value -> !value.equals("い")); // 例外発生
stream1.forEach(value -> System.out.println("value=" + value));
stream2.forEach(value -> System.out.println("value=" + value));
Exception in thread "main" java.lang.IllegalStateException: stream has already been operated upon or closed
実行結果

 また、最終的な処理を同じStreamインスタンスに対して複数回実行すると例外が発生します。例えば次の処理を実行すると、2回目のforEachメソッド実行時に例外が発生します。

Stream<String> stream = Arrays.stream(new String[]{"あ","い","う","え","お"});
stream.forEach(e -> System.out.println("1回目:e=" + e));
stream.forEach(e -> System.out.println("2回目:e=" + e)); // 例外発生
1回目:e=あ
1回目:e=い
1回目:e=う
1回目:e=え
1回目:e=お
Exception in thread "main" java.lang.IllegalStateException: stream has already been operated upon or closed
……

Streamの種類

 Stream APIには幾つかの種類のStreamインターフェースが用意されています。基盤となるStreamインターフェースの他に数値のプリミティブ型の場合、それに応じたStreamインターフェースがあります。そして通常使われるのは次の種類のStreamになります。

インターフェース 概要
Stream<T> 要素がTのインスタンスのStream
IntStream 要素がintの値のStream
LongStream 要素がlongの値のStream
DoubleStream 要素がdoubleの値のStream

Stream APIとラムダ式

 Stream APIで用意されているメソッドの多くは引数に関数型インターフェースを持っているものが多々あります。これにより基盤となる処理はStream APIで行い、細かい条件や要素を使った処理などを実装者に任せることができるようになります。ラムダ式はそのような実装をする際に効果を発揮します。

 それでは実際のソースを見てStream APIについて理解を深めていきましょう。例えば次のような売り上げを表すデータを持つクラスがあるとします。

public class SalesData {
    private Date date;        // 売上日
    private int salesAmount;  // 売上金額
    private String location;    // 地域
    
    public SalesData(Date date, int salesAmount, String location){
        this.date = date;
        this.salesAmount = salesAmount;
        this.location = location;
    }
……【略】…… ※ getterメソッドなど……【略】……
}
SalesData.java

 そして2014年1月1日から1日ずつ売上金額(SALES_AMOUNTS)の値と地域(LOCATIONS)の値が設定された次のSalesDataクラスのListがあったとします。

private static final int[] SALES_AMOUNTS = {
    1000, 2000, 3000, 4000, 5000, 6000, 7000, 8000, 9000
};
private static final String[] LOCATIONS = {
    "東京", "千葉", "埼玉", "神奈川", "茨城", "栃木", "群馬"
};
public static List<SalesData> createSalesDateList() {
    List<SalesData> salesDataList = new ArrayList();
    // 2014/1/1のDateを生成
    Calendar calendar = Calendar.getInstance();
    calendar.clear();
    calendar.set(2014, 1, 1);
    
    // 1,000,000日分のデータを作成する
    for (int i = 0; i < 1000000; i++) {
        Date salesDate = calendar.getTime();
        // 売上金額ごとのデータを作成する
        for (int salesAmount : SALES_AMOUNTS) {
            // 地域ごとのデータを作成する
            for (String location : LOCATIONS) {
                salesDataList.add(new SalesData(salesDate, salesAmount, location));
            }
        }
        calendar.add(Calendar.DAY_OF_MONTH, 1); // 日付を1日加算
    }
    return salesDataList;
}
SalesData.java

 このListの中から地域が「東京」で最も高い売上額を取得しようとする場合、Stream APIを使わずforループで書くと次のようにコードを書くことができます。

public static void main(String[] args) {
    // サンプルデータの作成
    List<SalesData> salesDataList = SalesData.createSalesDateList();
    
    int maxSalesAmount = -1;
    for (SalesData salesData : salesDataList) {
        // 地域が「東京」のもので絞る
        if (salesData.getLocation().equals("東京")) {
            // 最大の売り上げを取得する
            if (maxSalesAmount < 0) {
                maxSalesAmount = salesData.getSalesAmount();
            } else if (maxSalesAmount < salesData.getSalesAmount()) {
                maxSalesAmount = salesData.getSalesAmount();
            }
        }
    }
    System.out.println("maxSalesAmount = " + maxSalesAmount);
}

 実行すると、次の結果を得られます(※デフォルトのままJavaコマンドで実行するとGCが発生してかなり遅くなるため「-Xms」「-Xmx」オプションを指定して大きめのヒープサイズを設定してください)。

maxSalesAmount = 9000

 これをStream APIを使って書き換えると次のようになります。

public static void main(String[] args) {
    List<SalesData> salesDataList = SalesData.createSalesDateList();
    int maxSalesAmount = salesDataList.stream()  // 【1】
            .filter(salesData -> salesData.getLocation().equals("東京"))  // 【2】
            .mapToInt(salesData -> salesData.getSalesAmount())  // 【3】
            .max()  // 【4】
            .getAsInt();  // 【5】
    System.out.println("maxSalesAmount = " + maxSalesAmount);
}

 では実装されている内容を簡単に見てみましょう。

 【1】では、CollectionのstreamメソッドによってListの要素を基にしたStreamオブジェクトを生成しています。

 【2】では、Streamのfilterメソッドを使ってStreamのデータを対象のもの(地域が「東京」のもの)のみにしています。

 【3】では、mapToIntメソッドでSalesDataの売上値を取得し、新たにその値を使って新しいStreamを生成しています。また、ここで生成されたStreamはint値のStreamであるIntStreamになります。

 【4】では、IntStream のmaxメソッドで売り上げが最大のものを取得します。Stream APIのメソッドには結果の要素そのものを返さずにJava 8で新たに追加されたクラスであるOptionalを返すものがあります(Optionalクラスについては次回の連載で説明します。取りあえず結果を格納するクラスと考えてください)。

 ここでは、Optionalクラスの種類の1つであるint値を格納するOptionalIntとして結果を取得しています。

 【5】では、OptionalIntのgetAsIntメソッドでintの値を取得しています。

 このように、Stream APIは中間的メソッドを呼び出して最終的な結果を得たり処理を行ったりするようになっています。そしてメソッドの引数を関数型インターフェースにすることで、実装者に具体的な処理の記述を任せ、汎用的なメソッドとして幅広く使えるようになっています。

Copyright © ITmedia, Inc. All Rights Reserved.

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。