LoginSignup
8
10

More than 3 years have passed since last update.

Javaと第一級関数 -関数型インタフェースをこえて-

Last updated at Posted at 2020-01-26

概要

Java8で関数型プログラミングまわりの機能が強化された。

  • 関数型インタフェース
  • メソッド参照
  • ラムダ式
  • ストリームAPI

関数型インタフェースやメソッド参照が、どのような機能なのかについてまとめる

Goal

こんなソースコードが読めて使えるようになる。

public static void main(String[] args) {
    List<String> strings = Arrays.asList("bravo", "", "charlie", "alpha");
    strings.stream()
            .filter(s -> !s.isEmpty())
            .map(s -> s.substring(0, 1).toUpperCase() + s.substring(1))
            .forEach(System.out::println);
             //  Bravo
             //  Charlie
             //  Alpha
}

関数型インタフェース

関数型インタフェースってなんなのか、なんのためにあるのか。
それを理解するためには第一級関数という概念の理解が必要。

第一級関数

第一級関数(だいいっきゅうかんすう、英: first-class function、ファーストクラスファンクション)[1]とは、関数を第一級オブジェクトとして扱うことのできるプログラミング言語の性質、またはそのような関数のことである。
-- 第一級関数 - Wikipedia

第一級オブジェクト?

第一級オブジェクト

第一級オブジェクト(ファーストクラスオブジェクト、first-class object)は、あるプログラミング言語において、たとえば生成、代入、演算、(引数・戻り値としての)受け渡しといったその言語における基本的な操作を制限なしに使用できる対象のことである。
-- 第一級オブジェクト - Wikipedia

なるほど

Javaは?

Javaの関数(メソッド)は変数に代入したり、戻り値・引数に使用することができない。
Javaは第一級関数の性質を満たしていない言語。

第一級関数だと何が嬉しいの?

第一級関数は関数型言語には必要不可欠であり、高階関数のような形で日常的に用いられる。
-- 第一級関数 - Wikipedia

外から関数によって振る舞いを操作することができる。
e.g. sortのルール

List<String> strings = Arrays.asList("bravo", "charlie", "alpha");

Collections.sort(strings);
System.out.println(strings); // -> [alpha, bravo, charlie]

Collections.sort(strings, Comparator.reverseOrder());
System.out.println(strings); // -> [charlie, bravo, alpha]

べんり。

ちょっと待て

今のコードJavaじゃないの?

Strategyパターン

第一級オブジェクトとして関数を利用できないがために考えられたデザインパターン。

Javaではクラスのメソッドオーバーライドによるポリモーフィズムを使ってStrategyパターンを実現することができる。
-- Strategy パターン - Wikipedia

詳細は割愛しますが、こんな感じ。

public static <T> void sort(List<T> list, Comparator<? super T> c) {
    if (c.compare(a, b) < 0) {
        // aの方が小さい
    }else {
        // bの方が小さい
    }
}

引数で受け取ったComparatorインタフェースの実装クラスに従って、ソートのルールを決定している。
メソッド自体を引数経由で渡してはいないが、インタフェースを通してメソッドの処理を外側から渡すことができるというプラクティス。

実際のsort処理はこんな感じのよう
jdk/TimSort.java at master · openjdk/jdk

このデザインパターンは、関数が第一級オブジェクトの言語では特に意識する必要がない。

このパターンは、関数が第一級オブジェクトである言語では暗黙のうちに使用されている。
-- Strategy パターン - Wikipedia

実質

Javaはインタフェースを利用することで、実質、関数(メソッド)を第一級オブジェクトのように扱える。

関数型インタフェースってなんなのか

先程のComparatorのように、関数(メソッド)を第一級オブジェクトとして扱うために利用しているインタフェースのことを、java8からは、関数型インタフェース(functional interface)と呼ぶことにした。
関数(メソッド)を第一級オブジェクトとして扱うというのが目的なので、関数型インタフェースの抽象メソッドは1つのみである。

ComparatorやRunnableのように過去から提供されていたインタフェースの一部に新しい名前が付いただけ。
機能は通常のインタフェースと同一である。
関数型インタフェースには、@FunctionalInterfaceというアノテーションが追加されるようになった。
e.g. java.lang.Runnable

@FunctionalInterface
public interface Runnable {
    /**
     * When an object implementing interface {@code Runnable} is used
     * to create a thread, starting the thread causes the object's
     * {@code run} method to be called in that separately executing
     * thread.
     * <p>
     * The general contract of the method {@code run} is that it may
     * take any action whatsoever.
     *
     * @see     java.lang.Thread#run()
     */
    public abstract void run();
}

jdk/FunctionalInterface.java at master · openjdk/jdk

@FunctionalInterface

関数型インタフェースのために追加されたアノテーション。
実行時は特に何も起きない(関数型インタフェースは機能的には通常のインタフェースなため特別な処理等は無い)。
コンパイル時には関数型インタフェースの性質を満たしているかを確認してくれる。

つまり関数型インタフェースの機能を利用する際に絶対必要なわけではない。
ただし、通常のインタフェースではなく関数型インタフェースということが明示できるため、関数型インタフェースを自作した際はつけるべきだと思われる。

標準的な関数型インタフェース

関数型インタフェースを利用して関数(メソッド)を扱う場合、扱うメソッドはJavaの型システムの影響を受ける。
具体的には、下記が異なるメソッドを扱う場合、それぞれ異なる関数型インタフェースを利用しなければならない。

  • 返り値の型
  • 引数の型
  • 引数の数

java8では、返り値の有無や引数の型などを抽象化し利用可能にした標準的な関数型インタフェースが提供されている。
基本的にはこのインタフェースで充分であり、このインタフェースを利用することを考えるべきと言われている。
(ただし、Runnableなどの慣習的なものは除く)

代表的な標準関数型インタフェース

インタフェース名 抽象メソッド名
Function<T,R> R apply(T t) Arrays::asList
Predicate boolean test(T t) Collections::isEmpty
Consumer<T> void accept(T t) System.out::println
Supplier<T> T get() Instant::now
UnaryOperator<T> T apply(T t) String::toLowerCase
BinaryOperator<T> T apply(T t1, T t2) BigInteger::add

java.util.function (Java Platform SE 8 )

実際に使ってみる

文字列を加工して出力する処理を組んでみる。
出力する処理を呼び出す際に、加工処理の関数を一緒に渡すことで、加工処理後の文字列を出力する。
すべて大文字に変換してから出力するプログラムを組んでみる。

文字列表示部分

渡された関数を実行した後に出力する。
関数型インタフェースは変更前文字列(String)を1つ受け取り、変更後文字列(String)を返すような関数を想定し、UnaryOperator<String>を利用する。

static void print(String word, UnaryOperator<String> operator) {
    String newWord = operator.apply(word);
    System.out.println(newWord);
}

渡す関数

大文字に変換する関数。
UnaryOperator<String>を実装する。

import java.util.function.UnaryOperator;

public class MyUpperOperator implements UnaryOperator<String> {
    @Override
    public String apply(String s) {
        return s.toUpperCase();
    }
}

main

先程作成した大文字変換の関数と、文字列をprintメソッドに渡すと、全て大文字となって出力される。

public static void main(String[] args) {
    UnaryOperator<String> upperOperator = new MyUpperOperator();
    print("hello world", upperOperator); // -> HELLO WORLD
}

まぁ

関数型インタフェースを使ってはいるが、ただのStrategyパターン。

メソッド参照

先程の例、実はもっと強力な書き方が可能。
メソッド参照(method reference)という機能を利用することで、関数型インタフェースを明示的に実装せずに、引数と返り値が一致しているメソッドを直接渡すことができる。
先程の例の場合、UnaryOperator<String>なので、引数がString1つからなり、返り値がStringとなるメソッドであれば直接メソッド参照として渡すことが可能。
StringクラスtoUpperCase()メソッドを直接UnaryOperator<String>インスタンスに代入することができる。
(インスタンスメソッドのメソッド参照の場合は、レシーバオブジェクトが第一引数として解釈される。後述する文法を参照)
こうなると呼び出し側のソースコードは、第一級オブジェクトとして関数を扱っているように見える。

public static void main(String[] args) {
    // UnaryOperator<String> upperOperator = new MyUpperOperator();
    UnaryOperator<String> upperOperator = String::toUpperCase;
    print("hello world", upperOperator); // -> HELLO WORLD
}

しくみは難しい

JVMのinvokeDynamicという命令を利用しているらしい。
コンパイル時ではなく、実行時にクラスを動的に生成して解決しているらしい。

文法

セミコロンを2つ連続する記法。

セミコロンの両側に記述する内容は、メソッドの種類によって大きく4つのパターンに分けられる。

対象 文法
クラスメソッド クラス名::クラスメソッド名 String::toString
インスタンスメソッド インスタンス名::インスタンスメソッド名 System.out::println
インスタンスメソッド※ クラス名::インスタンスメソッド名 String::toUpperCase
コンストラクタ クラス名::new String::new

※レシーバオブジェクトが第一引数として解決される

使ってみる

先程の例は非常に簡潔に記述できる。MyUpperOperatorクラスはもはや不要である。

public static void main(String[] args) {
    print("hello world", String::toUpperCase); // -> HELLO WORLD
}

また、java8で追加されたstream APIはその多くが関数型インタフェースを要求しているので、このような処理が組める。

public static void main(String[] args) {
    List<String> strings = Arrays.asList("bravo", "charlie", "alpha");
    strings.stream()
            .map(String::toUpperCase)// Function: 大文字に変換
            .forEach(System.out::println);// Consumer: 出力
            // BRAVO
            // CHARLIE
            // ALPHA
}

非常に便利だが、

扱うためにはメソッドを予め宣言する必要がある。
用意されているメソッドを使う分にはいいが、独自処理をいちいちメソッドとして宣言しなければならない。
リテラルのようにもメソッドを扱いたい。

ラムダ式

関数型インタフェースに代入可能な評価式の仕組み。
メソッドを宣言せずに、式としてメソッドを記述することができる。

e.g. 文字列にピリオドつける関数

public static void main(String[] args) {
//    UnaryOperator<String> upperOperator = String::toUpperCase;
    UnaryOperator<String> periodOperator = (String s) -> { return s + "."; };
    print("hello world", periodOperator); // -> hello world.
}

文法

これが基本形
(仮引数列) -> { 処理本体の文 }
e.g.
(String s) -> { return s + "."; }

特定の条件下でのみ様々な記述の省略が可能。
調べればいろいろ出てくると思うので割愛。
e.g. 今回の場合
UnaryOperator<String> periodOperator = s -> s + ".";

使ってみる

public static void main(String[] args) {
    print("hello world", s -> s + "."); // -> hello world.
}

public static void main(String[] args) {
    List<String> strings = Arrays.asList("bravo", "", "charlie", "alpha");
    strings.stream()
            .filter(s -> !s.isEmpty()) // Predicate: 空文字を削除
            .map(s -> s.substring(0, 1).toUpperCase() + s.substring(1)) // Function: 1文字目のみ大文字に変換
            .forEach(System.out::println); // Consumer: 出力
             //  Bravo
             //  Charlie
             //  Alpha
}

後者の例は本記事の冒頭で挙げた処理である。

こんなソースコードが読めて使えるようになる。

まとめ

java8から関数型プログラミングの機能が強化された。

  • 関数型インタフェース
  • メソッド参照
  • ラムダ式

参考

改訂2版 パーフェクトJava:書籍案内|技術評論社
Effective Java 第3版 - 丸善出版 理工・医学・人文社会科学の専門書出版社

8
10
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
10