プロジェクト

全般

プロフィール

Javaプログラミング ラムダ式とStream API

Java SE 8で導入されたラムダ式と、ラムダ式を使用するコレクションAPI(Stream API)を使うとかなり高レベルなデータ処理ができるようになりますが、そのプログラミングはパラダイムシフトを少し必要とします。慣れるのに練習が必要と思われます。

メモ集

Lamda道場

Streamの生成

コレクション(Mapを除く)からStreamの生成

Javaのコレクションフレームワークでは、基底となるインタフェースCollection型にStreamを返すstreamメソッドが用意されています。

コレクション(Map)からStreamの生成

MapはCollectionを継承していないので、streamメソッドがありません。というより、Mapはストリーム(列)をなしていないので、entrySetを取り出してstreamを生成するのかと思います。

Map countries = ...
countries.entrySet().stream()
                    .map(e -> e.getValue().getName())
                    .forEach(System.out::println);

配列をStreamにする

ArraysからStreamの生成
Arrays.stream(args)

でStreamを生成できます。引数に指定した型により、いくつか戻り値のパターンがあります。
戻り値型 引数の型 戻り値型 引数の型
IntStream int[] DoubleStream double[]
LongStream long[] Stream<T> T[]
  • byte[]はこの方法ではStreamを生成できません ので、次のofでStreamを生成します
byte[] data = ...
IntStream.range(0, data.length)
         .map(i -> data[i])
         ...
Streamのofで生成

Streamクラスのstaticメソッド of は、可変長引数で受けた引数のStreamを生成します。可変長引数は配列として扱われるので、ofメソッドに配列を渡すことでStreamを生成することができます。

Point[] points = ...
Stream.of(points).forEach(...);

また、プリミティブ型に対応するIntStream、LongStream、DoubleStreamのそれぞれのofメソッドで、プリミティブ型の配列からStreamを生成します。

整数のStream生成

IntStream.range(1, 10)

1から9までの整数の順列のStreamを生成できます。

new Random().ints(-5, 5)

-5以上5未満の整数の無限Streamを生成します。

IntStream.iterate(10, i -> i + 10)

10から開始し、20, 30, ・・・と10ずつ増える整数の無限Streamを生成します。

EnumerationからStreamの生成

Javaの古いAPIでは、java.util.Enumeration をリストのように扱うものがあります。このEnumerationからStreamを生成する例です。

Enumeration<String> enu = ... ;
Stream<String> str = StreamSupport.stream(
    Spliterators.spliteratorUnknownSize(enu.asIterator(), Spliterator.ORDERED), false
);
  • java.util.CollectionsのlistメソッドにEnumerationを渡すことでListが生成され、そこからStreamを生成することもできますが、一度すべての要素をメモリに展開するのでEnumerationが大きいときなどに計算リソースが割かれてしまいます。
  • Spliteratorsには、spliteratorメソッドとspliteratorUnknownSizeメソッドがあり、前者はあらかじめイテレータのサイズがわかるとき、後者はイテレータのサイズが不明のときに使います。
  • Enumerationは、asIterator()でイテレータに変換できるので、spliteratorUnknownSizeに渡すことができます。

Streamの並列化(Parallel)

Streamに対してparallel()を呼ぶことで、処理を並列化できます。

オーバーヘッドとトレードオフ

Streamを並列化するには初期オーバーヘッドが発生します。おおよそ100μ秒といわれています。

  • 処理時間が100μ秒以下なら並列化すべきではない
  • N × Q >= 10,000なら並列化を考慮する
    • Nは要素数
    • Qは要素あたりの処理コスト(整数加算を1とした)
      • 3次元デカルト座標系の距離計算=7、3次元デカルト座標系の大きさ計算(平方根処理なしの距離計算)=3.3
  • Streamのソースは分割可能で要素の処理は独立している

コード

ラムダ式

Stream APIを使わないラムダ式によるコード例は次のページに独立。

検索

UNIXコマンドのgrep的な処理です。

anyMatch

com.sun.javadoc.Tagクラスの配列から、text()で取り出した文字列が"1.8"、"8"、または"JDK1.8"のいずれかの文字列と一致したらtrueを、いずれとも一致しなければfalseを返すメソッドを記述します。

従来の書き方 Stream APIを使った書き方
private static boolean isSinceTagMatches(Tag[] tags) {
    for (Tag tag : tags) {
        if ("1.8".equals(tag.text())) {
            return true;
        } else if ("8".equals(tag.text())) {
            return true;
        } else if ("JDK1.8".equals(tag.text())) {
            return true;
        }
    }
    return false;
}
private static boolean isSinceTagMatches(Tag[] tags) {
    return Arrays.stream(tags)
        .map(tag -> tag.text())
        .anyMatch(text -> "1.8".equals(text) || "8".equals(text) || "JDK1.8".equals(text));
}

distinct

1件1行で出力されたログファイルがあり、各行は先頭にリクエスト元のクライアントのIPアドレスと、続いて各要素が空白区切りで出力されています。
このファイルを読み、IPアドレス項を取得し、重複をなくして標準出力に出力します。

従来の書き方 Stream APIを使った書き方
BufferedReader reader = null;
try {
    reader = new BufferedReader(
        new InputStreamReader(
            new FileInputStream(new File(fileName)), "UTF-8"));
    String line;
    Set<String> ipAddresses = new HashSet<>();
    while ((line = reader.readLine()) != null) {
        String[] words = line.split(" ");
        ipAddresses.add(words[0]);
    }
    for (String ipAddress : ipAddresses) {
        System.out.println(ipAddress);
    }
} catch (IOException ex) {
    // ファイル読み込み時のエラー処理
} finally {
    if (reader != null) {
        try {
            reader.close();
        } catch (IOException ex) {
            // ignored
        }
    }
}
try (BufferedReader reader = Files.newBufferedReader(
    Paths.get(fileName), StandardCharsets.UTF_8)) {
    reader.lines()
          .map(s -> s.split(" ")[0])
          .distinct()
          .forEach(System.out::println);
} catch (IOException ex) {
    // ファイル読み込み時のエラー処理
}

allMatch

素数を判定するコード

従来の書き方 Stream APIを使った書き方
boolean isPrime(int number) {
    for (int i = 2; i < number; i++) {
        if (number % i == 0) {
            return false;
        }
    }
    return true;
}
boolean isPrime(int number) {
    return IntStream.range(2, number)
                    .allMatch(x -> (number % x) != 0);
}

データ生成

配列にデータを出力

従来の書き方 Stream APIを使った書き方
int[] predicted = new int[NUM_DATA];
for (int i = 0; i < NUM_DATA; i++) {
    predicted[i] = classifier.predict(testData[i]);
}
int[] predicted = IntStream.range(0, NUM_DATA)
        .map(i -> classifier.predict(testData[i]))
        .toArray();

文字列にデータを結合して出力

String text = IntStream.range(0, NUM_DATA)
        .mapToObj(String::valueOf)
        .collect(joining(", ", "", ""));
  • Collectors.joining を使うと、文字列ストリームを、セパレーターと接頭辞・接尾辞を付けて結合した1つの文字列を終端として生成します。
  • IntStream等のプリミティブ型(int)ストリームからオブジェクト型(String)にmapするときは、mapToObjを使います。

JavaFXのObserverListにデータを出力

従来の書き方 Stream APIを使った書き方
ObservableList<XYChart.Data<Float, Float> list =
    FXCollections.observableArrayList();
for (int i = 0; i < numData; i++) {
    var point = new XYChart.Data<Float, Float>(x[i], y[i]);
    list.add(point);
}
ObservableList<XYChart.Data<Float, Float> list = 
    IntStream.range(0, numData)
        .mapToObj(i -> new XYChart.Data<>(x[i], y[i]))
        .collect(toCollection(FXCollections::observableArrayList));

Mapにデータを出力(groupingBy)

Streamに流れる要素を、要素の一部を取り出しキーとし、キーに該当する要素のリストを値とするMapとしてデータを出力します。

List<Employee> employees = ...
Map<String, List<Employee>> map = employees.stream()
    .filter(Employee::isPermanent)
    .collect(Collectors.groupingBy(Employee::getSection));

2つのデータ列から出力を生成

HttpServletのリクエストパラメータ

HttpServletのdoGet/doPostメソッドの実装で、HttpServletRequestからリクエストパラメータを取得し、リクエストパラメータの名前と値の組み合わせを文字列として生成します。一つの名前に複数の値が想定されます。

リクエストパラメータの取得には次のAPIを呼び出します。

  • Enumeration<String> getParameterNames()
  • String[] getParameterValues​(String name)

拡張for文を使うコーディングは次となります。

List<String> names = Collections.list(req.getParameterNames());
for (String name : names) {
    String[] values = req.getParameterValues(name);
    for (String value : values) {
        out.println("<p>" + name + "=" + value + "</p>");
    }
}

Stream APIを使うコーディングは次となります。

StreamSupport.stream(Spliterators.spliteratorUnknownSize(req.getParameterNames().asIterator(), Spliterator.ORDERED),false)
        .flatMap(name -> Stream.of(req.getParameterValues(name))
                .map(value -> "<p>" + name + "=" + value + "</p>"))
        .forEach(out::println);

まず、名前一覧のEnumerator<String>をStreamSupportクラスを使ってstreamにします。
次に、名前から取得した値一覧を別なstreamにして各要素を文字列として、flatMapで展開します。
最後に文字列を出力します。

リンク集

オンライン記事

ITPro連載:Java技術最前線

セミナー資料

ブログから


6ヶ月前に更新