Javaのファーストクラスオブジェクトはクラスである。なので引数はクラスかインスタンス(クラスのオブジェクト)で渡す必要がある。
例えばリストの中身を1つずつ取り出して処理するために forEach というメソッドがある。
//こんな感じで使える。悪くない。
list.forEach(リストの中身を1つずつ取り出してそれぞれに対してしたいこと);
さて「リストの中身を1つずつ取り出してそれぞれに対してしたいこと」を、Javaのコードとしてどう書くのか?
forEach メソッドのシグネチャを見てみると、Consumer クラスを引数に取るらしい。
//Consumerを引数に取り、戻り値はない
void java.lang.Iterable.forEach(Consumer<T> action)
Consumerクラスの実装はこんな感じ。
//インターフェースでした
@FunctionalInterface
public interface Consumer<T> {
//任意の型の引数を1つ取り、戻り値はない
void accept(T t);
}
つまり forEach メソッドを使って、リストの中身を1つずつ取り出して出力したいとなると、下記のような Consumer オブジェクトを引数に渡す必要がある。
//名前の一覧
List<String> list = Arrays.asList("foo", "bar");
//名前を1つずつ出力するために、Consumerクラスの無名オブジェクトを引数に渡す
list.forEach(new Consumer<String>() {
@Override
public void accept(final String name){
System.out.println(name);
}
});
ファーストクラスオブジェクトが関数な言語、たとえば JavaScript であれば、このように記述するでしょう。
var list = ['foo', 'bar'];
//何をしたいのか無名関数として渡す
list.forEach(function(name){
console.log(name);
});
Javaはファーストクラスオブジェクトがクラスなので、とにかく何をしたくてもクラスで渡してもらわないと駄目なのです。
ラムダ式とは無名クラスを簡単に記述するためのシンタックスシュガー糖衣構文(ほんとはちょっと違うけどね)。
ラムダ式を使うと先のコードをこう書けるようになる。
//これが
list.forEach(new Consumer<String>() {
@Override
public void accept(final String name){
System.out.println(name);
}
});
//こう書ける
list.forEach(name -> System.out.println(name));
シンプル!振る舞いをそのまま引数として渡せる!
つまり関数をファーストクラスオブジェクトして扱えるようになる!
いろいろなところで紹介されているので自習ください。
ラムダ式の仕組みを見ていく。
ラムダ式を受けるには、関数型インターフェースを持つことが条件。関数型インターフェースとは「実装すべきメソッドを一つだけ持っているインターフェース」
引数があるかないか、戻り値があるかないかの組み合わせで4つの代表的な関数インターフェースがある。
実装:引数として任意の型を1つ取り、戻り値なし
@FunctionalInterface
//インターフェースである
public interface Consumer<T> {
//実装すべきメソッドは1つだけ
void accept(T t);
}
代表的な使われ方:
void forEach(Consumer<? super T> action) { // <= 引数として渡して
Objects.requireNonNull(action);
for (T t : this) {
action.accept(t); // <= ここで使われる
}
}
実装:引数として任意の型を1つ取り、戻り値がboolean
@FunctionalInterface
public interface Predicate<T> {
boolean test(T t);
}
代表的な使われ方:
public Optional<T> filter(Predicate<? super T> predicate) { // <= 引数として渡して
Objects.requireNonNull(predicate);
if (!isPresent())
return this;
else
return predicate.test(value) ? this : empty(); // <= ここで使われる
}
実装:引数として任意の型を1つ取り、戻り値は任意の型
@FunctionalInterface
public interface Function<T, R> {
R apply(T t);
}
代表的な使われ方:
public<U> Optional<U> map(Function<? super T, ? extends U> mapper) { // <= 引数として渡して
Objects.requireNonNull(mapper);
if (!isPresent())
return empty();
else {
return Optional.ofNullable(mapper.apply(value)); // <= ここで使われる
}
}
実装:引数なし、戻り値は任意の型
@FunctionalInterface
public interface Supplier<T> {
T get();
}
代表的な使われ方:
public T orElseGet(Supplier<? extends T> other) { // <= 引数として渡して
return value != null ? value : other.get(); // <= ここで使われる
}
実装:引数として任意の型を2つ取り、戻り値がboolean
@FunctionalInterface
public interface BiPredicate<T, U> {
boolean test(T t, U u);
}
代表的な使われ方:
public static Stream<Path> find(Path start,
int maxDepth,
BiPredicate<Path, BasicFileAttributes> matcher, // <= 引数として渡して
FileVisitOption... options) throws IOException {
FileTreeIterator iterator = new FileTreeIterator(start, maxDepth, options);
try {
return StreamSupport.stream(Spliterators.spliteratorUnknownSize(iterator, Spliterator.DISTINCT), false)
.onClose(iterator::close)
.filter(entry -> matcher.test(entry.file(), entry.attributes())) // <= ここで使われる
.map(entry -> entry.file());
} catch (Error|RuntimeException e) {
iterator.close();
throw e;
}
}
動的に処理内容を変更する実装としてストラテジーパターンがあるが、Predicate関数インターフェースを利用することで、ストラテジーパターン同様にロジックを外出しできる。
//リストの抽出条件を引数として受ける
public List<Integer> filter(final Predicate<Integer> predicate, final List<Integer> numbers) {
return numbers
.stream()
.filter(predicate)
.collect(Collectors.toList());
}
@Test
//テスト
public void testCalcLambda() throws Exception {
List<Integer> list = filter(
num -> num % 2 == 0, // <= 偶数のみを抽出する条件を引数に渡す
Arrays.<Integer> asList(1, 2, 3, 4, 5));
assertThat(list.size(), is(2));
assertThat(list.get(0), is(2));
assertThat(list.get(1), is(4));
}
Predicate関数インターフェースのデフォルト実装である and / or 関数を使うことで、抽出条件を論理演算することができる。
public static class UserCollection {
//内部で利用する抽出条件
private final Predicate<User> 正社員 = user -> user.userType.equals(UserType.正社員);
private final Predicate<User> 役員 = user -> user.title.equals(Title.役員);
private final Predicate<User> 部長 = user -> user.title.equals(Title.部長);
private final List<User> list;
public UserCollection(final List<User> list) {
this.list = list;
}
//外部公開する抽出条件
public UserCollection エグゼクティブなひとたち() {
return filterBy(正社員.and(役員));
}
//外部公開する抽出条件
public UserCollection 管理職なひとたち() {
return filterBy(正社員.and(役員.or(部長)));
}
private UserCollection filterBy(final Predicate<User> pridicate) {
final List<User> newList = list.stream()
.filter(pridicate)
.collect(Collectors.toList());
return new UserCollection(newList);
}
}
関数型インターフェースには、ラムダ式だけでなく、既存のメソッドを渡せるようになっている。
渡せるのは関数型インターフェースと引数の型と個数が一致しているメソッド。
//これが
list.forEach(name -> System.out.println(name));
//こう書ける
list.forEach(System.out::println);
内部的なクラスのインスタンスを生成して、いままで同様にクラスベースで処理されている。
既存のJDKライブラリにも互換性を持たせられる!
またAPIなどライブラリ提供者からすると、インスタンスを受け取る形で実装するので、いままでと何も違いがない!
ラムダ式で記述した場合、コンパイル時ではなく実行時に動的にクラス生成する。 なんとなく無名クラスの糖衣構文に見えるけど、実際には違うよ。
なぜそんな面倒なことしているかというとパフォーマンスのため。 StreamAPIではクラスを大量に作る場面が多くなり、コンパイル時に生成するとクラスロードに時間がかかるから、動的に処理している(らしい)。
//Java
list.forEach(new Consumer<String>() {
@Override
public void accept(final String name){
System.out.println(name);
}
});
//JavaScript
list.forEach(function(name){
console.log(name);
});
ぶっちゃけこの例では、無名関数を渡すか無名オブジェクトを渡すかの違いしかない。コードはやや冗長であるが、IDE使えば変わらないよね。
しかし。関数型プログラミングという新しいパラダイムでのコーディングを実現するには、引数に関数を渡すことが必要不可欠!
関数型プログラミングについてはまた別の回にするとして、ここに見た目以上の違いがあるということだけ理解しましょう。