Skip to content

Instantly share code, notes, and snippets.

@asufana
Last active February 7, 2023 04:30
Show Gist options
  • Star 6 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save asufana/a41bcc70ee482b5a4551 to your computer and use it in GitHub Desktop.
Save asufana/a41bcc70ee482b5a4551 to your computer and use it in GitHub Desktop.
Java8 ラムダ式入門2

Java8 ラムダ式入門2

Javaのファーストクラスオブジェクト

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);
	}
});

なにこれJavaうざい。とモダンな方々からdisられるわけです。

ファーストクラスオブジェクトが関数な言語、たとえば 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));

シンプル!振る舞いをそのまま引数として渡せる!

つまり関数をファーストクラスオブジェクトして扱えるようになる!

Stream API

いろいろなところで紹介されているので自習ください。

関数型インターフェース

ラムダ式の仕組みを見ていく。

ラムダ式を受けるには、関数型インターフェースを持つことが条件。関数型インターフェースとは「実装すべきメソッドを一つだけ持っているインターフェース」

引数があるかないか、戻り値があるかないかの組み合わせで4つの代表的な関数インターフェースがある。

1. Consumer

実装:引数として任意の型を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); // <= ここで使われる
    }
}

2. Predicate

実装:引数として任意の型を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(); // <= ここで使われる
}

3. Function

実装:引数として任意の型を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)); // <= ここで使われる
    }
}

4. Supplier

実装:引数なし、戻り値は任意の型

@FunctionalInterface
public interface Supplier<T> {
    T get();
}

代表的な使われ方:

public T orElseGet(Supplier<? extends T> other) { // <= 引数として渡して
    return value != null ? value : other.get(); // <= ここで使われる
}

引数が2つの場合は、Bi + xxx となる

実装:引数として任意の型を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;
    }
}

関数型インターフェースの使い方

1. 抽出条件を外出しする

動的に処理内容を変更する実装としてストラテジーパターンがあるが、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));
}

2. 抽出条件の論理演算化

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として関数を引数に取れるようになっているわけではない

内部的なクラスのインスタンスを生成して、いままで同様にクラスベースで処理されている。

既存の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使えば変わらないよね。

しかし。関数型プログラミングという新しいパラダイムでのコーディングを実現するには、引数に関数を渡すことが必要不可欠!

関数型プログラミングについてはまた別の回にするとして、ここに見た目以上の違いがあるということだけ理解しましょう。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment