KUSAMAKURA

智に働けば角が立つ。情に棹させば流される。意地を通せば窮屈だ。とかくに人の世は住みにくい。

Stream の API 見てみたけど、 「Predicate<? super T> predicate」 とかぼんやり眺めてあきらめた人向けに、Stream を使えるように、関数インターフェースを説明してみた

関数インターフェースとは

ラムダ式やメソッド参照の対象となる型を提供するインターフェースのこと。関数メソッドと呼ばれるそのインターフェース内で単一となる抽象メソッドが含まれている。Java8 から java.util.function が導入され、ラムダ式やメソッド参照のターゲットとなる型を提供している。

もうひと砕き

ラムダ式やメソッド参照の対象となる型を提供する」とは、どういうことだろうか。 StreamforEach を見てみよう。

forEach
void forEach(Consumer<? super T> action)
このストリームの各要素に対してアクションを実行します。
これは終端操作です。

この操作の動作は明らかに非決定論的です。並列ストリーム・パイプラインの場合、この操作は、ストリームの検出順序を考慮することを保証しません。保証すると並列性のメリットが犠牲になるからです。与えられた任意の要素に対し、ライブラリが選択した任意のタイミングで任意のスレッド内でアクションが実行される可能性があります。アクションが共有状態にアクセスする場合、必要な同期を提供する責任はアクションにあります。

パラメータ:
action - 要素に対して実行する非干渉アクション

forEach メソッドは、「Consumer<? super T>」の関数インターフェースを引数に取る。Consumer は単一のパラメータを受け取り、値を返さない関数インターフェースである。そして、関数インターフェースであるため、「ラムダ式やメソッド参照の対象となる型を提供する」つまり、ラムダ式や、メソッド参照が利用できるのである。
下記のコードが、forEach 中では利用できて、for 文の中では利用できないのは、このためである。

@Test
public void stream_forEach() {
  List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
  numbers.stream().forEach(System.out::println);

  for (int i = 0; i < numbers.size(); i++) {
    System.out.println(numbers.get(i));
    // System.out::println // 関数インターフェースがないため、NG
  }
}

パッケージ java.util.function の分類

Consumer

「消費者」。引数を受取り、値を返さない。値を返さないので、副作用を起こす目的で利用される。

@Test
public void stream_consumer() {
  List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
  numbers.stream().forEach(System.out::println);
Function<T,R>

「関数」。引数を受取り、演算した結果を返却する。

@Test
public void stream_function() {
  List<String> alphabet = Arrays.asList("a", "b", "c", "d", "e", "f");
  List<String> upperCase = alphabet.stream()
      .map(v -> v.toUpperCase())  // function. v を受取り、大文字にして返す
      .collect(Collectors.toList());

  assertThat(upperCase.get(0)).isEqualTo("A");
  assertThat(upperCase.get(1)).isEqualTo("B");
  assertThat(upperCase.get(2)).isEqualTo("C");
  assertThat(upperCase.get(3)).isEqualTo("D");
  assertThat(upperCase.get(4)).isEqualTo("E");
  assertThat(upperCase.get(5)).isEqualTo("F");
}
Predicate

「述部、断定する」。引数を受取り、真偽値を返却する。Stream では、boolean anyMatch(Predicate<? super T> predicate) などで利用されている。

@Test
public void stream_predicate() {
  List<String> alphabet = Arrays.asList("a", "b", "c", "d", "e", "f");
  List<String> actual = alphabet.stream()
      .filter(v -> v.compareTo("c") <= 0) // v を受取り、真偽値を返却する
      .collect(Collectors.toList());

  assertThat(actual).containsAll(Arrays.asList("a", "b", "c"));
}
Supplier

「供給者」。引数を取らず、何かを返却する関数インターフェース。Stream では、generate(Supplier s) などで利用されている。

@Test
public void stream_generate() {
  Stream<Double> stream = Stream.generate(Math::random);
  stream.limit(10).forEach(System.out::println);
}

副作用

副作用とは、関数に対して値を投入した(または、投入しなかった)際に、入力として受け付けた値以外の何かが変化を起こすことを言う。 Stream の forEach は、Consumer を引数に取る。入力として値が投入されないので、そのままでは何の効果もないメソッドになってしまう。そのため、良し悪しは置いておいて、何らかの副作用を期待して利用される。

@Test
public void stream_consumer() {
  List<String> alphabet = Arrays.asList("a", "b", "c", "d", "e", "f");
  alphabet.stream().forEach(System.out::println); // 外部出力に対して影響を及ぼしている→副作用を与えている
}

また、副作用は無い方が良いプログラムとされている。例えば、次の例は悪い副作用の例と言えるだろう。 外部の変数を使用するのではなく、コメントのように、collect を利用して新しく生成されるべきである。

@Test
public void stream_side_effects() {
  List<String> alphabet = Arrays.asList("a", "b", "c", "d", "e", "f");
  List<String> actual = new ArrayList<>();

  alphabet.stream()
      .filter(v -> v.compareTo("c") <= 0)
      .forEach(v -> actual.add(v)); // No side-effects!

  // List<String> actual = alphabet.stream()  // 新しく生成した方が副作用がない
  //     .filter(v -> v.compareTo("c") <= 0)
  //     .collect(Collectors.toList());

  assertThat(actual).containsAll(Arrays.asList("a", "b", "c"));
}

まとめ

関数インターフェース自体、副作用や、状態と振る舞いなどの話があるが、ここでは、Stream を軸に、関数インターフェースとはどんなものかを説明してみた。「パッケージ java.util.function の分類」を理解すれば、Stream 中、map にどのようなラムダ式を書けば良いのかわかるようになったはずだ。

余談

Supplier 「供給者」!とか、日本語にすると、なんか能力者っぽい感じがして好き。