倭マン's BLOG

くだらない日々の日記書いてます。 たまにプログラミング関連の記事書いてます。 書いてます。

ラムダ式で assert を使うときはブロックにしないといけない?

java.util.function.Consumer インターフェースは引数を1つ持ち、返り値のない (void) 抽象メソッド accept() を持ちます。 例えば以下のようにしてラムダ式を代入することができます:

Consumer<String> cons1 = s -> System.out.println(s);

さて、ここでラムダ式として assert 文を使ったテストを行ってみます:

Consumer<String> cons2 = s -> assert !s.isEmpty();

これをコンパイルすると「リリース1.4から'assert'はキーワードなので、識別子として使用することはできません」というコンパイル・エラーになります・・・ そんなこと知ってらぁw ラムダ式の本体をブロックにすれば問題なくコンパイルできます:

Consumer<String> cons3 = s -> { assert !s.isEmpty(); };

ちなみに無名クラスとして定義しても問題なし:

Consumer<String> cons4 = new Consumer<String>(){
    public void accept(String s){
        assert !s.isEmpty();
    }
};

本体がブロックでないラムダ式で assert 文によるテストが書けないのって、何か理由あるのかな? 単なるバグ?(Java 8 ea b90)

ちなみに何故ラムダ式を使って assert 文によるテストを書こうとしたかというと、Java8 で導入される java.util.stream.Stream インターフェースに定義されている peek() メソッドを使うと Map / Reduce 形式の処理の途中で容易にテストが書けそうだなと思ったからです:

import java.util.stream.*;

Stream<String> stream = 
    Stream.of("Java", "Groovy", "Scala", "Clojure", "Kotlin", "Jython", "JRuby");

stream.flatMap(s -> stringToStream(s))
      .peek(s -> { assert s.length() == 1; })    // キチンと変換されているかテストできる
      .map(s -> s + " ")
      .peek(s -> { assert s.length() == 2 && s.endsWith(" "); })    // テスト
      .forEach(System.out::print);    // 「J a v a G r ・・・ b y」

// 文字列を1文字ずつの Stream へ変換。
public static Stream<String> stringToStream(String s){
    return IntStream.range(0, s.length()).mapToObj(i -> s.substring(i, i+1));
}

中括弧 {} とセミコロン ; 邪魔だよ。 まぁ、JUnit のようなテスティング・フレームワーク使えばいいんだけどね。

追記
java8-ea-b120 でも assert はブロックで書かないとコンパイラに怒られました。 また、上記サンプルの stringToStream() メソッドをストリームばっちり使ってる風に書き換えました。

JUnit実践入門 ~体系的に学ぶユニットテストの技法 (WEB+DB PRESS plus)

JUnit実践入門 ~体系的に学ぶユニットテストの技法 (WEB+DB PRESS plus)

小川のせせらぎもやがてはうねる奔流に Stream インターフェース (3) - short-circuiting terminal operators -

Java の Stream インターフェースに定義されているメソッドを見ていくシリーズ(目次)。 今回は Stream インターフェースに定義されている short-circuiting terminal operator に分類されるメソッドを見ていきます。 terminal operator は前回まで扱っていた「終端演算子」ですが「short-circuiting」は Java の && 演算子のような「ショートカット(演算子)」であることを表しています。 つまり、必要な結果が得られればそれ以降の要素に対する処理をやめて制御を返すような終端演算子です。 扱うメソッドは次の5つ:

// Match
boolean allMatch(Predicate<? super T> predicate)
boolean anyMatch(Predicate<? super T> predicate)
boolean noneMatch(Predicate<? super T> predicate)

// Find
Optional<T> findFirst()
Optional<T> findAny()

では Match と Find に分けて見ていきましょう。

Match

Match は Predicate オブジェクト(述語)*1を引数にとり、Stream の各要素に対してその述語に対応するテストを行い、その結果をまとめるメソッドです:

メソッド true を返す場合 空 Stream に対して 備考
allMatch() 全ての要素に対して結果が true true を返す 各要素の結果値に対する論理積
anyMatch() 少なくとも1つの要素に対して結果が true false を返す 各要素の結果値に対する論理和
noneMatch() 全ての要素に対して結果が false true を返す anyMatch() の否定

返り値が false になるのは true を返す場合以外です。 空 Stream に対してもそれぞれ定められた boolean 値が返されます。 ではサンプルコード:

// Stream オブジェクトの生成メソッド
public static Stream<String> stream(){
    return Stream.of("Java", "Groovy", "Scala", "Clojure", "Kotlin", "Jython", "JRuby");
}

// Predicate オブジェクト(述語)
Predicate<String> starts_with_upper_case = s -> Character.isUpperCase(s.charAt(0));
Predicate<String> starts_with_J          = s -> s.startsWith("J");
Predicate<String> ends_with_J            = s -> s.endsWith("J");

// allMatch()
assert  stream().allMatch(starts_with_upper_case);
assert !stream().allMatch(starts_with_J);
assert !stream().allMatch(ends_with_J);

assert Stream.<Boolean>empty().allMatch(arg -> true);    // 常に true
assert Stream.<Boolean>empty().allMatch(arg -> false);

// anyMatch()
assert  stream().anyMatch(starts_with_upper_case);
assert  stream().anyMatch(starts_with_J);
assert !stream().anyMatch(ends_with_J);

assert !Stream.<Boolean>empty().anyMatch(arg -> true);    // 常に false
assert !Stream.<Boolean>empty().anyMatch(arg -> false);

// noneMatch()
assert !stream().noneMatch(starts_with_upper_case);
assert !stream().noneMatch(starts_with_J);
assert  stream().noneMatch(ends_with_J);

assert Stream.<Boolean>empty().noneMatch(arg -> true);    // 常に true
assert Stream.<Boolean>empty().noneMatch(arg -> false);

まぁ、結構分かりやすいメソッドではないでしょうか。 空 Stream に対する返り値がスッと頭に入ってくるかどうかってところかな。

Find

Find は要素を探索するメソッドです。 こちらは引数に何もとらず、それぞれ適切な要素の Optional を返します。 空 Stream に対しては空 Optional が返されます。

  • findFirst() ・・・ 出現順序に従って最初に位置する要素を返す
  • findAny() ・・・ Stream 内のどれか1つの要素を返す

これらのメソッドは通常それ単体では使わず、次回にやる filter() メソッドなどとともに用いることが多いと思います。 というか、メソッドの引数にフィルターに相当する Predicate オブジェクトをとるバージョンもオーバーロードしてくれといてくれたらいいと思うのだが・・・ まぁ filter() メソッド使うのと変わらんが。 サンプルコードいってみよう:

// Stream オブジェクトの生成メソッド
public static Stream<String> stream(){
    return Stream.of("Java", "Groovy", "Scala", "Clojure", "Kotlin", "Jython", "JRuby");
}

// テスト用の either() メソッド target が args の内のどれかと等しいなら true を、そうでなければ false を返します
public static boolean either(String target, String... args){
    for(String s : args){
        if(target.equals(s))return true;
    }
    return false;
}

// Predicate オブジェクト(述語)
Predicate<String> starts_with_J = s -> s.startsWith("J");
Predicate<String> ends_with_J  = s -> s.endsWith("J");

// findFirst()
assert stream().filter(starts_with_J).findFirst() .equals( Optional.of("Java") );
assert stream().filter(ends_with_J).findAny() .equals( Optional.empty() );
    	
// findAny()
String str = stream().filter(starts_with_J).findAny() .get();
assert either(str, "Java", "Jython", "JRuby");
assert stream().filter(ends_with_J).findAny() .equals( Optional.empty() );

結構使い勝手は良さそう。

次回は intermediate operators part 1 の予定。

Scalaスケーラブルプログラミング第2版

Scalaスケーラブルプログラミング第2版

*1:boolean 値を返す test() メソッドが定義されている。

Stream#collect() の型推論は correct ?

最近 Java 8 Project Lambda をちょっとイジってるんですが、そこでコンパイラに怒られた話(Java 8 ea-b90)。 本質的には Java 8 がどうこうというより Generics の話かと思いますが。

以前の記事java.util.stream.Stream インターフェースに定義されている collect() メソッドの使い方を見ました:

<R> R collect(Collector<? super T,R> collector)
<R> R collect(Supplier<R> resultFactory,
              BiConsumer<R,? super T> accumulator,
              BiConsumer<R,R> combiner)

特に2つ目の collect() メソッドに関して、これは(StringBuilder のような)ビルダーや(可変)コレクションを使って Stream の各要素を「足し上げて」いくメソッドでした。 例えば、StringBuilder を使って Stream<String> の要素を連結するには

Stream<String> stream = 
    Stream.of("Java", "Groovy", "Scala", "Clojure", "Kotlin", "Jython", "JRuby");

StringBuilder sb =
    stream.collect(StringBuilder::new,
                   StringBuilder::append,
                   StringBuilder::append);

System.out.println(sb.toString());
    // 「JavaGroovyScalaClojureKotlinJythonJRuby」と表示

のようにするのでした。 第1引数はビルダーのインスタンス生成(初期値)、第2引数はビルダーに要素を「足す」操作、第3引数はビルダー同士を「足す」操作です。 以下、stream オブジェクトとして、このコードとおなじオブジェクト(但し何の操作も走査も行っていない)とします。

一応、目的はランダムな順序にされた Stream オブジェクトを作ろうとして、(unordered() じゃ無理だったので)一度全要素を HashSet オブジェクトに格納してから Set#stream() メソッドで新たに Stream オブジェクトを生成することです。 まぁ、その目的自体はいいとして、この手順で新たに Stream オブジェクトを作ろうとしてコンパイラに怒られたお話。

まず、上記の手順をステップ・バイ・ステップで行うと、特に問題はありません:

Set<String> set= stream.collect(HashSet::new, Set::add, Set::addAll);
Stream<String> stream1 = set.stream();
stream1.forEach(System.out::print);

まぁでも、これ上手くいくなら、普通次のようにしたいですよね?

Stream<String> stream2 =
    stream.collect(HashSet::new, Set::add, Set::addAll).stream();
    // コンパイル・エラー

stream2.forEach(System.out::print);

でも、これはコンパイルを通りません。 メッセージは「不適合な型:Stream<Object>をStream<String>に変換できません」という旨のメッセージです。 それじゃあ、ということで

Stream<Object> stream3 =
    stream.collect(HashSet::new, Set::add, Set::addAll).stream();

stream3.forEach(System.out::println);

とすると確かにコンパイルは通って実行も出来ます。 「<Object>」と書くのは面倒なので

Stream stream4 =
    stream.collect(HashSet::new, Set::add, Set::addAll).stream();
    // 警告

stream4.forEach(System.out::print);

とすると、「未チェックまたは安全ではありません」という旨のジェネリックな型に生の型を使ったときによく見るメッセージが出て来ますがコンパイル&実行は可能です。 ただし、これら2つの場合は、生成した Stream の要素に対して Object のメソッドしか使っていないからうまくいったけど、要素を String 型として使いたい場合にはキャストの手間が必要です。 collect() がジェネリックなメソッドなので

Stream<String> stream5 = 
    stream().<String>collect(HashSet::new, Set::add, Set::addAll).stream();
    // コンパイル・エラー

stream5.forEach(System.out::println);

と、collect() の型パラメータに <String> を指定してみましたが、むしろもっとマジにコンパイラに怒られたw

さて、Generic なメソッド参照ができたらそこで今回のお話終了、なんですがやり方が分からないので、同じことをメソッド参照ではなくラムダ式で書いてみました:

Stream<String> stream6 =
    stream.collect(() -> new HashSet<String>(),
                   (s, e) -> s.add(e),
                   (s1, s2) -> s1.addAll(s2)).stream();

stream6.forEach(System.out::println);

ちょっと長くなりましたが、これは問題なくコンパイル&実行できました。 でも、実質に String 型の型指定を行っているのは第1引数の中だけなので第2引数はラムダ式で、第2、第3引数はメソッド参照で書いてみると

Stream<String> stream7 =
    stream().collect(() -> new HashSet<String>(), Set::add, Set::addAll).stream();

stream7.forEach(System.out::println);

これも OK。 ただし第1引数を型推論で書くのは NG:

Stream<String> stream8 =
    stream().collect(() -> new HashSet<>(), Set::add, Set::addAll).stream();
    // 警告

stream8.forEach(System.out::println);

以上をまとめると

Stream<String> stream7 =
    stream().collect(() -> new HashSet<String>(), Set::add, Set::addAll).stream();

stream7.forEach(System.out::println);

という風に、第1引数にはラムダ式、第2、第3引数にはメソッド参照を使うってのが無難そう。 そういえば、以前の記事で Stream#toArray(IntFunction) によって Stream オブジェクトを配列に変換しようとしたときも、配列オブジェクトの生成だけは手で書いてましたね。 それと同じことだと思えばいっか。 まぁ今後、型推論ができるようになったり(推論が可能なのか怪しいけど)、型パラメータを指定したメソッド参照ができるようになったり(すでにできる?)する可能性もありますが。

追記
java8-ea-b120 でも変わりありませんでした。

Scalaスケーラブルプログラミング第2版

Scalaスケーラブルプログラミング第2版

小川のせせらぎもやがてはうねる奔流に Stream インターフェース (2) - terminal operators part 2 -

Java の Stream インターフェースに定義されているメソッドを見ていくシリーズ(目次)。 今回は java.util.stream.Stream インターフェースに定義されている terminal operator (終端演算子) の残りを見ていきます。 どれも Stream の要素を集めた「コンテナ」風のオブジェクトを返します。 扱うメソッドは以下の4つ:

Iterator<T> iterator()

Object[] toArray()
<A> A[] toArray(IntFunction<A[]> generator)

Spliterator<T> spliterator()

Java 8 から新たに導入された java.util.Spliterator なるインターフェースがあります。 これは後ほど。

iterator() メソッド

まずは java.util.Iterator を返すメソッド iterator():

Iterator<T> iterator()

このメソッド自体は特に問題はないかと思います。 ただし Java 8 からは、Iterator インターフェースにデフォルト・メソッド forEachRemaining(Consumer) が新たに定義されました*1。 また、remove() メソッドはデフォルト・メソッドになりました。 デフォルトでは UnsupportedOperationException が投げられます:

package java.util;

pubilc interface Iterator<E>{
    boolean hasNext();
    E next();

    default void forEachRemaining(Consumer<? super E> action);
    default void remove();
}

使い方はこんな感じ:

Stream<String> stream = Stream.of("Java", "Groovy", "Scala", "Clojure");

// iterator()
Iterator<String> ite = stream.iterator();
ite.forEachRemaining(System.out::print);    // 「JavaGroovyScalaClojure」と表示

この場合 Iterator#forEachRemaining() を使うより Stream#forEach() を使えばいいんですけどネ。 まぁ、それはともかくとして、Stream は Iterator を返す iterator() メソッドがあるのに java.lang.Iteratable インターフェースを拡張しているわけではないので、拡張 for 文に使うことはできません:

// こんなことはできない    	
for(String s : stream)
    System.out.println(s);

別にできるようにしといてくれてもいいんじゃね? 昔のインターフェースで新しい Stream インターフェースを汚染したくなかったのかな? それならそれで言語拡張して(配列が Iterable でないのに拡張 for 文で使えるように) Stream もそれ自体で拡張 for で使えるようにしとくとかならんかな。 出来なくて困るというわけでもないけど・・・

toArray() メソッド

次は Stream を配列に変換するメソッド:

Object[] toArray()
<A> A[] toArray(IntFunction<A[]> generator)

1つ目の toArray() はいいでしょう。 2つ目の toArray() は配列のインスタンス生成が Generic にできないためにその部分だけ使用者に書かせようというメソッド。 IntFunction<T> は int 値を引数にとり T オブジェクトを返す apply(int) : T メソッドを抽象メソッドとして持つ関数インターフェースです。 使い方はこんな感じ:

Stream<String> stream = Stream.of("Java", "Groovy", "Scala", "Clojure");

// asArray()
String[] langs = stream.toArray(n -> new String[n]);    // 要素数 (int) から配列オブジェクトを生成
// String[] langs = stream.toArray(String[]::new);    // 配列のコンストラクタ参照を使う方が普通っぽい

for(String s : langs)
    System.out.print(s);
// 「JavaGroovyScalaClojure」と表示

使い方はなんてことないと思いますが、toArray() に渡す引数がほとんど決まりきった定型文なのでもうすこし何とかならないかと思うんですが・・・ 互換性の観点からはしかたないのかなぁ。

spliterator() メソッド

最後は java.util.Spliterator オブジェクトを返す spliterator() メソッド。 まずは Spilterator インターフェースの定義を見てみましょう:

package java.util;

public interface Spliterator<T>{

    static int CONCURRENT;
    static int DISTINCT;
    static int IMMUTABLE;
    static int NONNULL;
    static int ORDERED;
    static int SIZED;
    static int SORTED;
    static int SUBSIZED;

    int characteristics();
    long estimateSize();
    boolean tryAdvance(Consumer<? super T> action);
    Spliterator<T> trySplit();

    default void forEachRemaining(Consumer<? super T> action);
    default long getExactSizeIfKnown();
    default boolean hasCharacteristics(int characteristics);
    default Comparator<? super T> getComparator();
}

「spliterator」は見るからに「split + iterator」からきた造語なんでしょうね。 trySplit() メソッドによって2つの spliterator(自分自身の spliterator と trySplit() の返り値の spliterator)に分割するってのが使い方の胆なんでしょう。 これは後でサンプルコードで試してみます。

Spliterator インターフェースには Iterator と同じように forEachRemaining(Consumer) メソッドが定義されているので、このメソッドによって要素を列挙するのが通常の使い方なんでしょう。 Spliterator は Stream 同様、 Iterable を拡張しているワケではないので拡張 for 文に使ったりは出来ません。

ちょっと気になるのが、Spliterator の特性 (characteristics) を表す定数が int 型で定義されているところ。 普通に enum (と EnumSet)使おうよ・・・ なんか実装している人間が最近の Java に疎いんじゃないかと不安になるのは拙者だけ? まぁ、正式リリースまでに変更される可能性もないではないので、これはおいといて、サンプルコードを見ていきましょう:

import java.util.stream.Stream;
import java.util.Spliterator;
    	
Stream<String> stream = Stream.of("Java", "Groovy", "Scala", "Clojure");

// spliterator()
Spliterator<String> spl1 = stream.spliterator();
Spliterator<String> spl2 = spl1.trySplit();    // Spilterator を分割
  	    
spl1.forEachRemaining(System.out::print);    // 「ScalaClojure」 と表示。 後半分
System.out.println();
spl2.forEachRemaining(System.out::print);    // 「JavaGroovy」と表示。 前半分

spliterator() によって返される Spliterator オブジェクトは、元の Spliterator の前半分を返します。 spliterator() を呼び出された後は、元の Spliterator は後ろ半分が残った Spliterator になります。

素数が無限個ある場合は spliterator() の返り値の Spliterator オブジェクトは1024個(0番目~1023番目)の要素を持っているようです。 1024番目以降は元の Spliterator オブジェクトに含まれています:

Stream<Integer> stream = Stream.generate(new Supplier<Integer>(){
    private int count = 0;

    public synchronized Integer get(){
        return this.count++;
    }
});

Spliterator<Integer> spl1 = stream.spliterator();
Spliterator<Integer> spl2 = spl1.trySplit();
  	    
writeDown(spl1, "[Spl 1] ");
writeDown(spl2, "[Spl 2]      ");

// wrteDown() メソッド
public static <T> void writeDown(Spliterator<T> spl, String prefix)
        throws InterruptedException{
    Consumer<T> cons = (T t) -> {
        try{
            Thread.sleep(100);

        }catch(InterruptedException e){
            e.printStackTrace();
        }
        System.out.println(prefix + t);
    };

    Runnable run = () -> spl.forEachRemaining(cons);
    new Thread(run).start();
}
[Spl 2]      0
[Spl 1] 1024
[Spl 2]      1
[Spl 1] 1025
[Spl 2]      2
[Spl 1] 1026
[Spl 2]      3
[Spl 1] 1027
[Spl 2]      4
[Spl 1] 1028
[Spl 1] 1029
[Spl 2]      5
[Spl 2]      6
[Spl 1] 1030
[Spl 1] 1031
[Spl 2]      7

Spliterator は実質的に Stream を分割する型なので、タスクの並行実行などに重要な役割を果たすようになるかも知れませんが、それなら関数型言語のように Stream 自体を分割できるようにならんかったんかな・・・

次回は short-circuiting terminal operators の予定。

追記
Stream#toArray() メソッドの引数として配列のコンストラクタ参照を渡す部分を追加しました。

増補改訂版Java言語で学ぶデザインパターン入門

増補改訂版Java言語で学ぶデザインパターン入門

オブジェクト指向における再利用のためのデザインパターン

オブジェクト指向における再利用のためのデザインパターン

*1:以前のビルドでは forEachRemaining() ではなく forEach() だったかと。

小川のせせらぎもやがてはうねる奔流に Stream インターフェース (1) - terminal operators part 1 -

OpenJDK にて JDK 8 の Feature Complete がリリースされたので、暇を見つけて Java 8 の新機能をあれこれイジっていこうかなと。 使用する JDK のバージョンは 8-ea-b90 です。

ってことで、まずは java.util.stream.Stream インターフェースに定義されているメソッドを見ていきまーす。 Stream は、Java 7 まででいう java.util.Iterator が一番近い型でしょうか。 一定の型のオブジェクトのシーケンス(連なり)をオブジェクト化したものです。 Iterator と違うところは filter() や map() のような、要素オブジェクトの取捨選択や変換を容易に行えるメソッド(多くが関数インターフェースを引数にとる)が豊富に定義されているところです。 また、しばしばそれらの処理が遅延して行われるため、メモリの効率化や無限シーケンスの扱いなども可能な場合があります・・・みたいな説明でいいかな? 詳しくはドキュメント参照のこと(ダウンロードが必要)。 

目次
このシリーズの記事では、Stream インターフェースに定義されているメソッドを以下のように分類して見ていく予定。 Stream オブジェクトはいわゆる Map / Reduce 処理を行うよう設計されているので、それに従った分類になってます:

ちょっと小分けしすぎかな。 まぁ、いいでしょう。 Stream インターフェースは Scala のような関数型言語には大抵の場合似たようなものが定義されているほどありふれた型なので、使われている用語に適切な日本語訳があるかもしれませんが、拙者はそちら方面に素養がないもので適当な訳語を付けてます。 ご了承くださいませ。 ちょっと目次と順番が違ってますが、今回は terminal operators (part 1)

Terminal Operator

terminal operator って、それっぽく訳すと「終端演算子」というのかな? Stream オブジェクトは、filter() や map() のようなメソッドをチェーンのようにつなげて新たな Stream オブジェクトにどんどん変換していって、最後に何らかの別の型に変換する(もしくは値を返さずに副作用のみを行う)という使い方が基本ですが、terminal operator というのは、その「最後に何らかの別の型に変換する」という部分を担うメソッドのことです。

今回扱う terminal ooperator は以下の通り:

// 各要素に副作用のある処理を実行
void forEach(Consumer<? super T> action)
void forEachOrdered(Consumer<? super T> action)

// 要素数
long count()

// reduce
Optional<T> reduce(BinaryOperator<T> accumulator)
T reduce(T identity, BinaryOperator<T> accumulator)
<U> U reduce(U identity, BiFunction<U,? super T,U> accumulator, BinaryOperator<U> combiner)

// collect
<R> R collect(Collector<? super T,R> collector)
<R> R collect(Supplier<R> resultFactory, BiConsumer<R,? super T> accumulator, BiConsumer<R,R> combiner)

// 最小値・最大値
Optional<T> min(Comparator<? super T> comparator)
Optional<T> max(Comparator<? super T> comparator)

上記のうち forEach(), forEachOrdered() 以外のメソッドは forEach() を使って実装できますが、よく使う定型処理なので組み込みメソッドとして定義してあるんでしょう。 実際、関数型の言語には同名のメソッドが大抵定義されてたかと。

ちなみに、各メソッドの引数に Java 8 から導入された関数型インターフェース*1が多用されていますが、それぞれ

  • Consumer<T> ・・・ T オブジェクト1つを引数にとり、値を返さない (void) メソッド accept(T) : void を持つインターフェース。 必ず副作用を行う
  • BiConsumer<S, T> ・・・ S オブジェクト、T オブジェクトの2つを引数にとり、値を返さないメソッド accept(S, T) : void をもつインターフェース
  • BinaryOperartor<T> ・・・ T オブジェクト2つを引数にとり T オブジェクトを返すメソッド apply(T, T) : T を持つインターフェース。 二項演算子
  • BiFunction<S, T, U> ・・・S オブジェクト、T オブジェクトの2つを引数にとり、U オブジェクトを返すメソッド apply(S, T) : U を持つインターフェース。 2変数関数

という感じです。 詳しくは「こちら」参照。 Collector は java.util.stream パッケージ内のインターフェース。 関数型インターフェースではありません。

ではそれぞれの簡単なサンプルコードを見ていきましょう。

サンプルコード

forEach()
まずは一番基本の forEach() メソッド。 forEachOrdered() メソッドは要素の出現順が変更されないと保証されている以外は同じメソッドなので省略。 forEach() の返り値は void なので、このメソッドは副作用を起こすことが前提です。 それは引数が Consumer であることからもわかります。 とりあえず、Stream の要素を表示するサンプル:

Stream<String> stream = Stream.of("Java", "Groovy", "Scala", "Clojure");    // Stream オブジェクト生成
stream.forEach(System.out::println);

Java 8 から導入された、「::」演算子によるメソッド参照の取得なんかも使って見ました。 stream の要素(文字列 "Java", "Groovy" など)それぞれを引数にして System.out の println() メソッドを呼び出しています。

count()
次は Stream の要素を数えるメソッド。 Stream は通常、要素数を保持していないとかと思うので(実装知らないけど)、要素数を数えるのもコストがかかる覚悟が必要w というか、Stream は要素のシーケンスを逆に辿れないので、要素数を数えるだけでもそれでお仕舞い。 使い方はまぁ、簡単:

Stream<String> stream = Stream.of("Java", "Groovy", "Scala", "Clojure");
assert stream.count() == 4L;    // count() は long 値を返す

返り値は long 値です。 別に 4L にしなくても今の場合は4で大丈夫だけど。 ちなみに count() は

stream.mapToLong(e -> 1L).sum();

と同じだそうです。

reduce()
reduce() メソッドは、Stream の各要素をある意味で「足し上げる」メソッドです。 3つのオーバーロードされたシグニチャがありますが、引数が1つ、2つのものはそんなに難しくないと思います:

Optional<T> reduce(BinaryOperator<T> accumulator)
T reduce(T identity, BinaryOperator<T> accumulator)

引数の BinaryOperator は2つの要素が与えられたときに、それらをどう「足す」か?を指定します。 2つ目の reduce() はそれに加えて初期値を指定します。 ちょっと注意が必要なのは、1つ目の reduce() は返り値が Optional 型だというところです。 Optional は値がない(空)かも知れない場合に使う型で、Stream が空の場合に空の Optional が返されます。 2つ目の reduce() は Stream が空でも初期値が与えられているので値が空になることがないため、Optional が使われていません。

引数が2つのものは for文 を使って

// T reduce(T identity, BinaryOperator<T> accumulator)

T result = identity;
for (T element : this stream)
    result = accumulator.apply(result, element)
return result;

と書くのと同じだそうです。

引数が3つの reduce() は・・・シグニチャを見てもなんのことだかw JavaDoc には以下のコードと同じだと書かれてます:

// <U> U reduce(U identity, BiFunction<U,? super T,U> accumulator, BinaryOperator<U> combiner)

U result = identity;
for (T element : this stream)
    result = accumulator.apply(result, element)
return result;

んー、引数が2つの reduce() とほとんど同じですな。 ちょっと返り値の型が変わってるだけ。 えっ、第3引数の combiner 使ってないじゃん・・・ どうもこれは combiner を使わなくても同じ結果を返すように実装を行わないといけないということのようで。 例えば

1 + 2 + 3 + 4 + 5

を計算する場合、第2引数の accumulator だけを使うと

((((1 + 2) + 3) + 4) + 5)

というふうに計算しますが、これを

(((1 + 2) + 3) + (4 + 5))

のように計算しても同じ結果になるようにしましょう、ということでしょう(結合法則を満たす)。 ここで、((1 + 2) + 3) と (4 + 5) の計算は第2引数の accumulator によって計算されますが、それらの結果を足す際に combiner を使うんだと思います。 まぁ、とりあえず動くサンプル書ければいいんじゃね?ってことでサンプルコード:

import java.util.Optional;

Stream<String> stream = Stream.of("Java", "Groovy", "Scala", "Clojure");

// 値を「足し上げる」演算のみを指定。 返り値は Optional
assert stream.reduce(String::concat) .equals( Optional.of("JavaGroovyScalaClojure") );

// 初期値と「足し上げる」演算を指定。 返り値は Stream の要素の型と同じ
assert stream.reduce("", String::concat) .equals( "JavaGroovyScalaClojure" );

// 要素を変換して「足し上げる」
assert stream.reduce(0, (i, s) -> i + s.length(), (i1, i2) -> i1 + i2) == 22;

最後のものは mapToInt() と IntStream#sum() 使って書いた方が簡単ですけどね。

collect()
collect() は reduce() と結構似てますが、「mutable reduction」とドキュメントに書いてあるとおり、(StringBuilder のような)Builder やコレクションのように可変なオブジェクトに対して Stream の各要素を「足し上げて」いくメソッドです。 「足し上げ」メソッドは reduce() のように結果値を返さなくてよく、collect() の返り値は、「足し上げ」られた Builder やコレクションになります。 引数が1つの collect() は java.util.stream.Collection オブジェクトを引数としてとりますが、Collection インターフェースについてはそのうち機会があれば。 今は java.util.stream.Collectors クラスからビルドインの Collector を使えるというだけで OK です。 ではサンプルコード:

Stream<String> stream = Stream.of("Java", "Groovy", "Scala", "Clojure");

// Collector を1つとる collect
assert stream.collect(Collectors.toStringJoiner(", ")).toString() .equals( "Java, Groovy, Scala, Clojure" );

// 上記 reduce() の3つ目に対応するもの
assert stream.collect(StringBuilder::new,
                      StringBuilder::append,
                      StringBuilder::append).toString() .equals( "JavaGroovyScalaClojure" );

// 2つ目のものをメソッド参照を使わずに書き換えたもの
assert stream.collect(() -> new StringBuilder(),
                      (sb, s) -> sb.append(s),
                      (sb1, sb2) -> sb1.append(sb2)).toString() .equals( "JavaGroovyScalaClojure" );

2つ目の collect() では第2引数と第3引数に StringBuilder::append を渡してますが、実際に使われているのはオーバーロードされた別の append() です。 3つ目の collect() では、それを明示的(でもないか)に示すためにラムダ式を使って書き換えてみました。 うーむ、Collector が関数インターフェースじゃないので使い方があんまり便利な感じがしないなぁ*2

min() / max()
最後は最小値、最大値を取得するメソッド。 引数に大小関係を比較するための Comparator を指定する必要があります:

import java.util.Optional;

Stream<String> stream = Stream.of("Java", "Groovy", "Scala", "Clojure");

assert stream.min(String.CASE_INSENSITIVE_ORDER) .equals( Optional.of("Clojure") );    // 辞書順序
assert stream.max((s1, s2) -> s1.length() - s2.length()) .equals( Optional.of("Clojure") );    // 文字列長

Stream が空の場合、空の Optional が返されます。 min(), max() は reduce() メソッドと3項演算子 ? : でそんなに難しくなく書き換えられます。

メソッドの分類を小分けにしたつもりが、妙に長くなってしまいましたな・・・ とりあえず、本日はこの辺で。 次回は terminal operator part 2 の予定。

素数夜曲―女王陛下のLISP

素数夜曲―女王陛下のLISP

*1:Functional interface。 抽象メソッドが1つだけのインターフェース。 SAM 型。

*2:Groovy の collect() と比べて。

祝 Java8 Project Lambda リリース!につき、ラムダ式コトハジメ

まだまだ夜は寒い季節ですが、新年度とともに Java の世界にも一足早く新しい風が吹いてきました。 Java5ジェネリクスが導入されたときも結構インパクトがありましたが、Java8 でのラムダ式はそれ以上のインパクトがあり、プログラミングのパラダイム・シフトを余儀なくされることでしょう。

そうは言ってもラムダ式、関数オブジェクトは現代的なプログラミング言語では大抵サポートされている機能でもあるので、今更感の強い人もいることでしょう。 そういう方でも、多言語で実現されてた機能が Java8 ではどのように使うのか?ってことは確認しておく価値はあると思います。 ってことでラムダ式、関数オブジェクトに関して基本的な事項を Java8 のコードで見ていきまする。

内容

参考

ラムダ式


拙者は「ラムダ式」とか「関数型プログラミング」とかに関しては初心者なので、ラムダ式の定義とかに関しては深入りしないことにして、記法・文法だけに焦点をあてます。 Java8 でのラムダ式Scala のものとほとんど同じ様です。 ちょっとした違いは、引数と本体を分ける矢印が「=>」ではなく「->」になっているくらいです(引数の型を省略した場合)。

基本的な記法

まずは関数リテラルの記法を見ていきましょう。 int 値1つをを引数にとって、int 値を返す関数は

(int i) -> i * 2;

と書きます。 矢印「->」の前に関数の引数を「(int i)」のように書き、矢印の後に関数の処理内容を書きます(「i * 2」の部分)。 これは無名クラスを使うと次のように書いたのと同じです:

new AnonymousFunction1(){    // AnonymousFunction1 というクラスってないけどね
    public int apply(int i){
        return i * 2;
    }
}

無名クラスの場合に比べて、クラス名、メソッド名、返り値の型、return キーワードなど、あれこれいろいろ省略して、かなり簡略化して書けるようになってます。 引数が2つの場合

(int i0, int i1) -> i0 + i1;

となり、これは

new AnonymousFunction2(){
    public int apply(int i0, int i1){
        return i0 + i1;
    }
}

と同じです。 引数が3つ以上の場合は説明が不要でしょう。 引数がない場合は

() -> Math.PI * 2.0;

のように矢印「->」の前に「()」を付けます。 関数の処理を複数行にわたって書きたい場合*1

(int i) -> {
    int prod = 1;
    for(int n = 0; n < 5; n++) prod *= i;
    return prod;
};

のように、処理部分を中括弧「{}」で囲みます*2。 この場合は return キーワードは省略できません。

型推論

さて、上記の基本的な記法では引数の型を真面目に書いてましたが、大抵の場合は型推論(type inference)*3の機能によって型宣言を省略することができます。 例えば int 値2つをとり int 値を返す以下のような IntOperator インターフェース

@FunctionalInterface
interface IntOperator{
    int apply(int i0, int i1);
}

があった場合、この型の変数にラムダ式(によって作った関数オブジェクト)を代入するなら

IntOperator op = (i0, i1) -> i0 + i1;

のように、引数の型を省略することが出来ます。 引数が1つの場合は括弧「()」も省略できて

@FunctionalInterface
interface IntFunction{
    int apply(int i);
}

IntFunction f = i -> i * 2;

だけで OK です。

クロージャとしてのラムダ式

ラムダ式はローカル変数にそのままアクセスできるという意味でクロージャ(Closure 閉包)としての機能を持つことが一般的です。 まぁ言葉はともかく使い方は簡単。

int scale = 10;
IntFunction f = i -> i * scale;
System.out.println(f.apply(2));    // 「20」

ここでは scale というローカル変数をラムダ式の中で参照しています。 特に不自然なところはないですね。 ただ、ラムダ式の代わりに無名関数で同じことをしようとすると、今まで(Java7 まで)はローカル変数に final 宣言をつけていなければなりませんでした(「final int scale = 10;」とする)。 この辺りの齟齬をなくすために、Java8 からは無名関数の場合でも final でないローカル変数にアクセスできるようになりました(ただし、実質的に final な場合に限られます*4):

int scale = 10

IntFunction f = new IntFunction(){
    @Override
    public int apply(int i) {
        return i * scale;    // final でない変数でも無名クラス内で使えるようになったヨ。
    }
};

System.out.println(f.apply(3));    // 「30」

ラムダ式が導入された後にこの機能を使うことがあるかどうかは疑問ですがね*5

関数オブジェクト


次は関数をオブジェクトとして扱う方法を見ていきます。

ラムダ式を既存の型に代入する

Java8 では抽象メソッドが1つだけのインターフェースは「関数インターフェース」と見做され、引数のシグニチャと返り値の型が一致すればラムダ式を代入することができます。 例えば java.lang.Runnable インターフェースは

@FunctionalInterface
interface Runnable{
    void run();
}

という型宣言を持つので、

Runnable r = () -> System.out.println("Runnable by lambda expression.");

という代入ができます。 型が合わなければコンパイラがきちんとコンパイル・エラーを出してくれます。 わぁい。 Java8 からは Runnable に @FunctionalInterface アノテーションが付けられてますが、これは関数インターフェースと見做されるための必須条件ではありません。 インターフェースにこのアノテーションを付けておくと、そのインターフェースが関数インターフェースとみなせない場合(抽象メソッドが2つ以上あるなど)にコンパイル・エラーを出してくれます。

ラムダ式で生成したオブジェクトを関数オブジェクトとして扱いたい場合、見て関数とわかる型に代入したいこともよくあります。 ただ、個々人が独自に Function インターフェースみたいなのを定義していくのも何なので、Java8 から java.util.function パッケージが導入され、関数っぽいインターフェースがあれこれまとめて定義されています。 大雑把にはこちらの記事参照。

メソッドの参照

関数オブジェクトを取得するには、ラムダ式を使う以外に既存のメソッドをオブジェクトとして取得する方法もあります。 これにはコロンを2つ並べた演算子「::」を使います*6。 「::」の使い方は微妙に違う2通りがあります:

違いを意識する必要があるような、ないようなって感じですが。

具体例を見ていきましょう。 まずはオブジェクトに対して「::」演算子を使う場合。 このときはインスタンス・メソッドの参照を取得します。 例えばラムダ式で書いたとき

Function<Integer, Character> im0 = i -> "Lambda".charAt(i);
assert im0.apply(3) == 'b';

となるのと同じ関数オブジェクトをメソッドの参照で書いてみましょう。 ちなみに Function<String, Integer> は String オブジェクト1つを引数にとって Integer オブジェクトを返す関数オブジェクトです。 「::」演算子を使った場合

Function<Integer, Character> im1 = "Lambda"::charAt;
assert im1.apply(3) == 'b';

となります。 参照するメソッドには「()」をつけません。 まぁ、これは問題ないかと思います。

次はクラス(名)に「::」を使う場合。 クラス・メソッド(static メソッド、静的メソッド)の参照を取得するのは特に問題はないと思います:

Function<String, Double> sm = Double::parseDouble;
assert sm.apply("2.0") == 2.0;

コンストラクタの参照を取得する場合は、メソッド名の代わりには「new」を用います:

Function<char[], String> c = String::new;
assert c.apply(new char[]{ 'l', 'a', 'm', 'b', 'd', 'a'}) == "lambda";

最後は、第1引数のインスタンス・メソッドを参照する場合。 String を引数にして length() メソッドの返り値を返す関数オブジェクトを取得してみましょう。 ラムダ式で書くと

Function<String, Integer> f = s -> s.length();
assert f.apply("Lambda") == 6;

です。 これを「::」を使って書くと

Function<String, Integer> mc = String::length;
assert mc.apply("Lambda") == 6;

参照したメソッドを実際に持っているのは、メソッド(今の場合 apply() メソッド)を呼び出す際に渡した(第1)引数 "Lambda" になります。 呼び出すメソッドに引数を渡したい場合は以下のようになります:

BiFunction<String, Integer, Character> mc3 = String::charAt;
assert mc3.apply("Lambda", 2) == 'm';

BiFunction<String, Integer, Character> (2変数関数)は「Character apply(String, Integer)」というメソッドを持ちます。 このメソッド参照をこれはラムダ式で書くと

BiFunction<String, Integer, Character> mc2 = (s, i) -> s.charAt(i);
assert mc2.apply("Lambda", 2) == 'm';

第2引数(以降)は参照したメソッドの引数として渡されます。 どうでしょう? 2つの場合の違い分かったでしょうか?

メソッドのデフォルト実装

Java8 からはインターフェースに定義したメソッドにデフォルト実装を書くことができるようになりました。 このメソッドは抽象メソッドに数えられないので、関数インターフェースが抽象メソッドが1つしか持てないという制約があってもいろいろなインターフェースを関数インターフェースとして扱うことができるようになります。 デフォルトメソッドは、単にインターフェースのメソッドに「default キーワード」を付けておくだけです。 メソッドの実装は具象メソッドと同じように書けます。 例えばこんな感じです:

@FunctionalInterface
interface BDReducer{

    BigDecimal reduce(BigDecimal arg0, BigDecimal arg1);

    default BigDecimal reduce(BigDecimal arg0, BigDecimal arg1, BigDecimal arg2){
        return reduce(reduce(arg0, arg1), arg2);
    }
}

2引数の reduce() は抽象メソッド、3引数の reduce() はデフォルト実装を持ったメソッドです。 @FunctionalInterface アノテーションは、BDReducer が関数インターフェースに適合していることを(コンパイル時に)検証するためにつけてます。 なくても OK。 このインターフェースは例えば以下のように使います:

BigDecimal one = new BigDecimal(1), 
                 two = new BigDecimal(2),
                 three = new BigDecimal(3);

BDReducer a = BigDecimal::add;
assert a.reduce(one, two).intValue() == 3;    // 1 + 2
assert a.reduce(one, two, three).intValue() == 6;    // 1 + 2 + 3

BDReducer m = BigDecimal::multiply;
assert m.reduce(one, two).intValue() == 2;    // 1 * 2
assert m.reduce(one, two, three).intValue() == 6;    // 1 * 2 * 3

インターフェースにメソッド実装を許すと多重継承の問題が発生しますが、ここでは割愛。

高階関数


関数をオブジェクトとして扱えるようになると、その自然な帰結として関数を引数とする関数、すなわち高階関数 (higher-order function) が使えるようになります。 その最も簡単な例はコレクションの各要素におなじ処理を施すというメソッドであり、関数オブジェクトをサポートするプログラミング言語では、その言語内のコレクションにいろいろな高階関数が定義されているのが普通です。 ここでは Java8 で Java のコレクション・フレームワークに追加された機能を簡単に見ていきます。 また、独自に高階関数を定義する仕方も簡単に触れます(まぁ、普通の関数の定義なんですけど)。

Java Collection Framework API

Java8 ではコレクション・フレームワークのインターフェース群に高階関数が使えるような API 拡張があれこれ施されています。 ただ、Java7 までに存在したインターフェースに高階関数を追加しているというより、高階関数を多く持つ新たな型*7 java.util.stream.Stream インターフェースを定義し、Collection インターフェースに Stream オブジェクトを生成するメソッドを追加する、というアプローチをとっているようです。 まぁ、今後 Collection インターフェース(とそのサブタイプ)にも色々と高階関数が定義されていくのかも知れませんが。 ちなみに、java.util.Map インターフェースには、高階関数は定義されていますが、Stream オブジェクトを生成するメソッドは定義されていないようです。 entrySet() メソッドなどで Set オブジェクトを取得してから Stream オブジェクトにすればいいだけですけど。

既存の Collection オブジェクトから Stream オブジェクトを取得するメソッドは2つ定義されています

  • stream() : Stream<E>
  • parallelStream() : Stream<E>

parallelStream() は並列処理可能な Stream オブジェクトを返します(そのままw)。 ちなみに、どちらもデフォルト・メソッドとして定義されています。

ちなみに、Iterable<E> インターフェースには forEach(Consumer) メソッド、Iterator<E> インターフェースには forEachRemaining(Consumer) メソッドという高階関数が定義されています(どちらもデフォルト・メソッド)。 Consumer<E> インターフェースは引数 E、返り値 void の関数インターフェースです。 Map インターフェースにも同様のメソッドが定義されています(引数が Consumer ではなく BiConsumer ですが)。 まぁ、forEach() メソッドがあれば大抵の高階関数は定義できる*8かと思うので、このメソッド超重要。

まぁ、具体的にコード見た方が分かりやすいでしょう。 まずは既存のコレクションの forEach() メソッド。

List<Integer> list = Arrays.asList(0, 1, 2, 3, 4, 5, 6);
list.forEach(System.out::print);    // 「0123456」

2行目では PrintStream 型のSystem.out オブジェクトについて print() メソッドの参照を取得し、それを List#forEach() メソッドに渡しています。 まぁ、なんてことないですね。 次は Stream オブジェクトを使うコード。

List<Integer> list = Arrays.asList(0, 1, 2, 3, 4, 5, 6);
Stream<Integer> stream = list.stream();
Stream<Integer> even = stream.filter(i -> i % 2 == 0);    // 偶数だけを取り出す
even.forEach(System.out::print);    // 「0246」

偶数だけを取り出して表示しています。 3行目ではラムダ式で関数オブジェクトを生成しています。 まぁ、通常はメソッドを数珠繋ぎに呼び出して使う方が自然:

List<Integer> list = Arrays.asList(0, 1, 2, 3, 4, 5, 6);
list.stream().filter(i -> i % 2 == 0).forEach(System.out::print);    // 「0246」

おぉー、すっきり。 Java もだいぶ見易くなったもんだ。 次はもうちょっと複雑(demonai?)コード。 文字列の List を長さでソートしています:

List<String> lang = Arrays.asList("Java", "Groovy", "Scala", "Clojure");
lang.sort((s0, s1) -> s0.length() - s1.length());
lang.forEach(System.out::println);

2行目ではラムダ式によって Comparator オブジェクトを生成しています。 関数型言語と違ってメソッドが呼び出された List の要素自体が並び替えられているのに注意。

高階関数を定義する

最後に、簡単な高階関数を作って終わりにしましょう。 高階関数はいきなりキチンとした実装を作ろうとしても混乱して時間を無駄にすることが多いので(拙者だけ?)、具体的なメソッド実装から順々に汎化していった方が無難かと。

ここでは、String の List に対して、各要素に何らかの変換を施して表示する関数を作ってみます。 手始めに文字列の長さを表示する関数を書いてみると

List<String> lang = Arrays.asList("Java", "Groovy", "Scala", "Clojure");
printlnForEach(lang)

public static void printlnForEach(List<String> list){
    list.stream().map(String::length).forEach(System.out::println);
}

Stream#map() メソッドを使って String の Stream から int (Integer) の Stream に変換しています。 変換部分を関数オブジェクトにして、メソッドの引数に追加すると

List<String> lang = Arrays.asList("Java", "Groovy", "Scala", "Clojure");
printlnForEach(lang, String::length)

public static void printlnForEach(List<String> list, Function<String, Integer> mapper){
    list.stream().map(mapper).forEach(System.out::println);
}

今の場合、printlnForEach() メソッドの最後で forEach() に渡している println() メソッドは任意の型のオブジェクトを渡せるので、printlnForEach() メソッドの第2引数は Function<String, ?> で充分ですね。 また、List<String> と Function<String, ?> の String は型パラメータとして抽出しておきましょう。

List<String> lang = Arrays.asList("Java", "Groovy", "Scala", "Clojure");
printlnForEach(lang, String::length)

public static <T> void printlnForEach(List<T> list, Function<T, ?> mapper){
    list.stream().map(mapper).forEach(System.out::println);
}

この printlnForEach() メソッドを使えば

List<String> lang = Arrays.asList("Java", "Groovy", "Scala", "Clojure");
printlnForEach(lang, s -> s.charAt(0));

として書く文字列の先頭文字だけを抜き出して表示させることが出来ます。 String 以外の List でも処理できますが本日はこの辺で。

まとめ


ザッと Java8 Project Lambda の概要を見てきましたが、ほとんどの機能は既存の関数型言語で既に実装されているものばかりとも言えますが、逆に言えば多言語で出来ていたことが Java でも出来るようになったとも言えます。 シンタックスは Scala のものによく似ているようですが、Groovy のものとは結構違いますね。 完全に Java の文法を捨てた Scala に似ていて、Java の痕跡が残るような文法にしている Groovy と全く異なるという、皮肉なところもある Java8 のラムダ式ですが、Groovy の今後はどうなっちゃうんでしょー*9。 まぁ、それはともかく、本記事が Java8 のラムダ式の理解に役立って無上の喜びでございまする。

Happy, April 1st!

追記

「::」演算子を使ってクラス・メソッド、コンストラクタの参照を取得する部分を追記しました。

追記2

Java8 のラムダ式で参照できる外部変数は実質的に final な場合だ、って部分を追記しました。 「Java8のlambda構文がどのようにクロージャーではないか」参照のこと。
Scalaスケーラブルプログラミング第2版

Scalaスケーラブルプログラミング第2版


プログラミングGROOVY

プログラミングGROOVY

*1:セミコロンで区切る場合も含む

*2:Groovy のクロージャとは中括弧の使い方が違うので注意。

*3:「target typing」と呼んでた記事とかがあった気もします。

*4:Java8のlambda構文がどのようにクロージャーではないか

*5:オブジェクトに状態を持たせたいときとかに使うかも。

*6:Groovy での「.&」演算子と似てます。 インスタンスメソッドを参照する場合は同じと思ってよさそう。

*7:Collection のサブタイプではないようで。

*8:Scala の Traversal トレイト参照。

*9:既にロードマップあたりに書かれてるかも知れないけど。

java.util.function パッケージ 【追記・修正あり】

拙者、あんまり正式リリースされてない API って扱わない保守的な人間なんですが、ふと Java8 で導入されるラムダ式ってのがどんな感じになるのか気になったのでドキュメントを探してみました。 ラムダ式に対応したドキュメントや JavaDoc は普通の「JDK〓 8 Early Access Releases」にはまだ含まれてないようで、「Java〓 Platform, Standard Edition 8 Early Access with Lambda Support」 からダウンロードが必要。

ちなみに、今回は java.util.function パッケージに含まれるインターフェースを見ていく記事で、実際にラムダ式を用いた関数オブジェクトの生成などコードは書いてません。 また、当然のことながら最終的に仕様が変更になってるかも知れませんのでご注意を。

参考

追記
ea-b120 で試してみたらチラホラと変更があったので所々修正しました(2014年1月1日)。

  • メソッドの追加・削除・変更
    • Function に andThen(), identity() を追加
    • BiFunction に andThen() を追加
    • UnaryOperator に identity() を追加
    • BinaryOperator に maxBy(), minBy() を追加
    • Predicate に isEqual() を追加、xor() を削除
    • BiPredicate から xor() を削除
    • Consumer の chain() を andThen() に変更
    • BiConsumer の chain() を andThen() に変更
  • インターフェースの追加
    • IntToLongFuncion
    • IntToDoubleFunction
    • LongToIntFuncion
    • LongToDoubleFunction
    • DoubleToIntFuncion
    • DoubleToLongFunction

andThen() メソッドは Function, BiFunction, Consumer, BiConsumer に追加されてますが、これらは先に自分を作用させる合成を行う、という意味で統一されてるようです。 また、プリミティブ型を返り値に持つ関数インターフェースの抽象メソッドが applyAsInt() だったりするところを apply() と書いてたところを修正しました。

Overview

java.util.function パッケージに定義されているインターフェースを大雑把に分類するとこんな感じ:

各インターフェースには抽象メソッドが1つずつ定義されています。 また、「Bi」が付いているのはその抽象メソッドの引数が「2つ」になってます。 引数、返り値がともに Object のもの(プリミティブ型でないもの)を表にまとめると以下の通り:

インターフェース 抽象メソッド 引数型 → 返り値型
Function
BiFunction
apply(...)  T   → R
T × U → R
UnaryOperator
BinaryOperator
apply(...)  T   → T
T × T → T
Predicate
BiPredicate
test(...)  T   → boolean
T × U → boolean
Supplier get()  ()   → T
Consumer
BiConsumer
accept(...)  T   → ()
T × U → ()

Supplier は引数なし(よって BiSupplier というインターフェースはない)、Consumer は返り値が void です。 これらの他に、引数もしくは返り値がプリミティブ型のインターフェースがあれこれ定義されてますが、現在のところ全プリミティブ型には対応してなくて

  • int
  • long
  • double
  • boolean (Supplier のみ)

にのみ対応してるようです。

継承関係は Function 系と Operator 系との間にのみにあり*1、Operator は引数と返り値が同じ型の Function という関係になってます。 また、プリミティブ型が異なれば(int と long というように)やはり継承関係はありません。

java.util.function パッケージに定義されているクラスは Function 系, Predicate 系に対するユーティリティ・クラス Functions, Predicates があります。

では、それぞれのインターフェースをもう少し詳しく見ていきましょう。

Function / BiFunction (関数)

Function / BiFunction は関数を表すインターフェースです。 定義されている抽象メソッドは apply() です。

Function (1引数)

  • XxxFunction
    • Function
    • IntFunction
    • LongFunction
    • DoubleFunction
  • ToXxxFunction
    • ToIntFunction
    • ToLongFunction
    • ToDoubleFunction
  • XxxToYyyFunction
    • IntToLongFuncion
    • IntToDoubleFunction
    • LongToIntFuncion
    • LongToDoubleFunction
    • DoubleToIntFuncion
    • DoubleToLongFunction

型宣言は以下の通り:

package java.util.function;

@FunctionalInterface
public interface Function<T,R>{

    R apply(T t);

    default <V> Function<T,V> andThen(Function<? super R,? extends V> after);
    default <V> Function<V,R> compose(Function<? super V,? extends T> before);

    static <T> Function<T,T> identity()
}

andThen(), compose() メソッドは関数の合成を行うためのメソッドです。 andThen() メソッドはこの Function オブジェクトを先に適用してその結果に引数の関数(Function オブジェクト)を作用させる、という Function オブジェクトを返します。 compose() メソッドはそれとは適用順序が逆の合成関数を返します。 identity() メソッド (static) は恒等関数(何もしない関数、引数をそのまま返す関数)に対応する Function オブジェクトを返します。

インターフェース 抽象メソッド 引数型 → 返り値型
Function<T, R> R apply(T) T → R
IntFunction<R>
LongFunction<R>
DoubleFunction<R>
apply(int)
apply(long)
apply(double)
int → R
long → R
double → R
ToIntFunction<T>
ToLongFunction<T>
ToDoubleFunction<T>
applyAsInt(T)
applyAsLong(T)
applyAsDouble(T)
T → int
T → long
T → double
IntToLongFunction
IntToDoubleFunction
LongToIntFunction
LongToDoubleFunction
DoubleToIntFunction
DoubleToLongFunction
applyAsLong(int)
applyAsDouble(int)
applyAsInt(long)
applyAsDouble(long)
applyAsInt(double)
applyAsLong(double)
int → long
int → double
long → int
long → double
double → int
double → long

抽象メソッドが applyAsXxx() の場合、返り値が xxx 型だということに注意。 引数と返り値の型が同じプリミティブ型の関数を使いたい場合は、下記の XxxUnaryOperator を使って下さい。 Function インターフェース以外には andThen(), compose(), identity() メソッドはないようです。 プリミティブ型の関数にまで合成関数を返すメソッドを定義してたら、それこそ組み合わせ論的にメソッド数が爆発してしまいそうだし。 なんでまた、こんな仕様にしようとしたんだか。

BiFunction (2引数)
BiFunction は引数が2つの関数です。

  • BiFunction
  • ToIntBiFunction
  • ToLongBiFunction
  • ToDoubleBiFunction
package java.util.function;

@FunctionalInterface
public interface BiFunction<T,U,R>{

    R apply(T t, U u);

    default <V> BiFunction<T,U,V> andThen(Function<? super R,? extends V> after);
}

andThen() メソッドは関数の合成を行うためのメソッドです。

インターフェース 抽象メソッド 引数型 → 返り値型
BiFunction<T, U, R>
ToIntBiFunction<T, U>
ToLongBiFunction<T, U>
ToDoubleBiFunction<T, U>
apply(T, U)
applyAsInt(T, U)
applyAsLong(T, U)
applyAsDouble(T, U)
T × U → R
T × U → int
T × U → long
T × U → double

UnaryOperator / BinaryOperator (演算子

UnaryOperator は1項演算子*2、BinaryOperator は2項演算子*3です。 UnaryOperator は型パラメータが全て同じの Function、BinaryOperator は型パラメータが全て同じの BiFunction です。 抽象メソッドは Function / BiFunction と同じく apply() です。

UnaryOperator (1引数)

  • UnaryOperator
  • IntUnaryOperator
  • LongUnaryOperator
  • DoubleUnaryOperator
package java.util.function;

@FunctionalInterface
public interface UnaryOperator<T> extends Function<T,T>{

    static <T> UnaryOperator<T> identity();
}

identity() メソッド (static) は恒等演算(何もしない演算、引数をそのまま返す演算)を行う UnaryOperator オブジェクトを返します。 UnaryOperator は Function のサブタイプなので andThen(), compose() メソッドも使えます。

インターフェース 抽象メソッド 引数型 → 返り値型
UnaryOperator apply(T) T → T
IntUnaryOperator applyAsInt(int) int → int
LongUnaryOperator applyAsLong(long) long → long
DoubleUnaryOperator applyAsDouble(double) double → double

BinaryOperator (2引数)

  • BinaryOperator
  • IntBinaryOperator
  • LongBinaryOperator
  • DoubleBinaryOperator
package java.util.function;

@FunctionalInterface
public interface BinaryOperator<T> extends BiFunction<T,T,T>{

    static <T> BinaryOperator<T> maxBy(Comparator<? super T> comparator);
    static <T> BinaryOperator<T> minBy(Comparator<? super T> comparator);
}

maxBy(), minBy() メソッド (static) は、それぞれ、引数の2つの値のうち大きい方、小さい方を返す二項演算子を返します。 引数は比較を行う Comparator です。 BiFunction から継承した andThen() メソッドもあります。

BinaryOperator apply(T, T) T × T → T
IntBinaryOperator applyAsInt(int, int) int × int → int
LongBinaryOperator applyAsLong(long, long) long × long → long
DoubleBinaryOperator applyAsDouble(double, double) double × double → double

Predicate / BiPredicate (述語)

Prediate / BiPredicate は述語を表すインターフェースです。 「述語」とは主語等にあたる変数の値が確定すれば真偽が確定するテストと言えばいいんでしょうかね。 気持ち的には「ToBooleanFunction / ToBooleanBiFunction」と言えます。 ただし、Function / BiFunction インターフェースとの関係(継承関係など)はありません。 定義されている抽象メソッドは test() です。

Predicate (1引数)

  • Predicate
  • IntPredicate
  • LongPredicate
  • DoublePredicate
package java.util.function;

@FunctionalInterface
public interface Predicate<T>{

    boolean test(T t);

    default Predicate<T> negate();
    default Predicate<T> and(Predicate<? super T> p);
    default Predicate<T> or(Predicate<? super T> p);

    static <T> Predicate<T> isEqual(Object targetRef);
}

negate(), and(), or() メソッドは、この Predicate オブジェクトと引数の Predicate オブジェクトの評価結果に対して論理演算を行う Predicate オブジェクトを生成します。 and(), or() はショートカット演算子 &&, || のように、不必要な評価は行われません。 isEqual() メソッド (static) は引数のオブジェクトと同じ(Object#equals() によって)オブジェクトに対して test() を呼ばれたときに true を返すような Predicate オブジェクトを返します。

インターフェース 抽象メソッド 引数型 → 返り値型
Predicate test(T) T → boolean
IntPredicate test(int) int → boolean
LongPredicate test(long) long → boolean
DoublePredicate test(double) double → boolean

BiPredicate (2引数)

  • BiPredicate
package java.util.function;

@FunctionalInterface
public interface BiPredicate<T,U>{

    boolean test(T t, U u);

    default BiPredicate<T,U> negate();
    default BiPredicate<T,U> and(BiPredicate<? super T,? super U> p);
    default BiPredicate<T,U> or(BiPredicate<? super T,? super U> p);
}

negate(), and(), or() メソッドは、この BiPredicate オブジェクトと引数の BiPredicate オブジェクトの評価結果に対して論理演算を行う BiPredicate オブジェクトを生成します。 and(), or() はショートカット演算子 &&, || のように、不必要な評価は行われません。

インターフェース 抽象メソッド 引数型 → 返り値型
BiPredicate test(T, U) T × U → boolean

Supplier

Supplier は何らかの値を供給 (supply) するインターフェースです。 引数のない Function と思えばいいでしょう。 当然のことながら、2引数の BiSupplier なんてのはありません。 プリミティブ型のサポートとして、int, long, double 値以外にも、boolean 値を返す BooleanSupplier というのも定義されています。 抽象メソッドは get() です。

  • Supplier
  • BooleanSupplier
  • IntSupplier
  • LongSupplier
  • DoubleSupplier
package java.util.function;

@FunctionalInterface
public interface Supplier<T>{
    T get();
}
インターフェース 抽象メソッド 引数型 → 返り値型
Supplier get() () → T
BooleanSupplier getAsBoolean() () → boolean
IntSupplier getAsInt() () → int
LongSupplier getAsLong() () → long
DoubleSupplier getAsDouble() () → double

Consumer / BiConsumer

Consumer インターフェースは値を受け取って処理をするが、返り値を返さないインターフェースです。 返り値のない(返り値が void の) Function と思えばいいでしょう。 ただし、返り値がないということはメソッド内でなんらかの副作用を伴う処理をしていることになるので、関数型プログラミングを行うときは注意が必要ってことにんるんでしょうね。 定義されている抽象メソッドは accept() です。
Consumer (1引数)

  • Consumer
  • IntConsumer
  • LongConsumer
  • DoubleConsumer
package java.util.function;

@FunctionalInterface
public interface Consumer<T>{

    void accept(T t);

    default Consumer<T> andThen(Consumer<? super T> after);
}

chain() メソッドは、この Consumer オブジェクトと引数の Consumer オブジェクトの処理を連続して行う Consumer オブジェクトを生成します。 処理の順序はこの Consumer オブジェクトの方が先に行われます。

インターフェース 抽象メソッド 引数型 → 返り値型
Consumer accept(T) T → ()
IntConsumer accept(int) int → ()
LongConsumer accept(long) long → ()
DoubleConsumer accept(double) double → ()

BiConsumer (2引数)

  • BiConsumer
  • ObjIntConsumer
  • ObjLongConsumer
  • ObjDoubleConsumer
package java.util.function;

@FunctionalInterface
public interface BiConsumer<T,U>{

    void accept(T t, U u);

    default BiConsumer<T,U> andThen(BiConsumer<? super T,? super U> after);
}

chain() メソッドは、この BiConsumer オブジェクトと引数の BiConsumer オブジェクトの処理を連続して行う BiConsumer オブジェクトを生成します。 処理の順序はこの BiConsumer オブジェクトの方が先に行われます。

インターフェース 抽象メソッド 引数型 → 返り値型
BiConsumer accept(T, U) T × U → ()
ObjIntConsumer accept(T, int) T × int → ()
ObjLongConsumer accept(T, long) T × long → ()
ObjDoubleConsumer accept(T, double) T × double → ()

@FunctionalInterface

上記の各インターフェースには共通するスーパーインターフェースはありませんが、関数型であることを表すマーカーインターフェース @FunctionalInterface アノテーションが付与されています。 定義はこんなの:

package java.lang;

@Documented
@Retention(value=RUNTIME)
@Target(value=TYPE)
public @interface FunctionalInterface{}

ちなみに、サブタイプには継承されないようなので、(Function インターフェースのサブインターフェースである) UnaryOperator インターフェースなどにも付与されてます。

オブジェクト指向プログラマが次に読む本 ?Scalaで学ぶ関数脳入門

オブジェクト指向プログラマが次に読む本 ?Scalaで学ぶ関数脳入門

実践 F# 関数型プログラミング入門

実践 F# 関数型プログラミング入門

*1:継承関係と言うよりは型パラメータの代入。

*2:符号の+, -、否定の !、ビット反転院の ~ など

*3:int 値どうしの和 + など。