LoginSignup
7
2

More than 3 years have passed since last update.

【第二回】Stream APIを使う前に関数型インターフェースを理解する

Last updated at Posted at 2019-11-17

はじめに

この投稿は、Java8におけるStream APIを活用する為の事前知識として書いています。
Streamを理解するためには複数の知識が必要なので、下記流れで投稿していこうと思っています!
今回はその二回目です!

Java8におけるインターフェース

関数型インターフェースに入る前に、Java7までのインターフェースとJava8になってからのインターフェースの違いを確認します。

要素 Java7までのインターフェース Java8からのインターフェース
抽象メソッド 複数定義可能 複数定義可能
staticメソッド 定義不可 複数定義可能
defaultメソッド 定義不可 複数定義可能
オブジェクト生成 生成不可 生成可能

Java7まではインターフェースに実装を持つことはできませんでしたが、
Java8以降はstaticメソッドやdefaultメソッドを定義することが可能になっています。

関数型インターフェースの定義

ここから本題の関数型インターフェースです。
まずは関数型インターフェースの定義を見てみましょう。

要素 関数型インターフェース
抽象メソッド 1つのみ
staticメソッド 複数定義可能
defaultメソッド 複数定義可能
オブジェクト生成 生成可能

ここでのポイントは「抽象メソッドは1つしか持つことができない」ということです。
通常のインターフェースと同様にstaticメソッドやdefaultメソッドは複数持っても構いませんが、
抽象メソッドは1つしか持つことはできません。

関数型インターフェースの利用箇所

  • 無名クラスを渡す時
  • ラムダ式やメソッド参照を渡す時(代入先となる時)
  • Streamの処理において引数として渡す時

正直なところ、これだけじゃイメージつかないですよね?
それなので、ラムダ式に関しては第三回の投稿で説明します。
Streamに関しては第四回の投稿で説明します。

この段階では関数型インターフェースとは

  • 実装すべき抽象メソッドが一つしか定義されていないインターフェース
  • 1つのメソッドしか記述できず、状態を持てないという制約(インターフェースであるという性質)を課すことで、副作用のない関数を実装できる
  • 関数とは入力と出力(引数と戻り値)が定義されており、それ自体が独立して呼び出しできるもの

といったイメージだけ抑えておけばいいのかなと思います。

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

前置きがだいぶ長くなりました。
イメージを掴むには実際の使用例を見た方が早いですね!

まずは関数型インターフェースの定義からです

SampleIf.java

@FunctionalInterface
public interface SampleIf {
    public abstract Integer countLength(String str);
}

これは、String型の引数を受け取って、戻り値としてInteger型を返す抽象メソッド(countLength)を持つ関数型インターフェースです。
(実装としては文字列を受け取って、その文字列の数を数える形にしていきます)
抽象メソッドなのでここでは当然実装していません。

次に実装を行います。
今回は無名クラスを用いて実装します。

Sample.java

public static void main(String[] args) {
    SampleIf count = new SampleIf() {
        //抽象メソッド(countLength)を実装
        @Override
        public Integer countLength(String str) {
            return str.length();
        };
    };
    //実行
    System.out.println("カウント結果:" + count.countLength("テスト")); // -> カウント結果:3
}

引数として文字列を受け取って、その文字数を返すだけの実装です。

わざわざ関数型インターフェースを定義し、実装クラスでnewして抽象メソッドをOverride(実装)しています。
正直面倒ですよね?

これはラムダ式を用いることで遥かに簡単に記述することができますが、ラムダ式は第三回の投稿に任せる為、現時点ではあえて無名クラスを使った冗長な記述にしています。

ここではラムダ式の説明は行わないことで、関数型インターフェースそのものにフォーカスを当てていければと思います。

@FunctionalInterface

ところで、先程のコードに@FunctionalInterfaceという記述が出てきました。
このアノテーションは付けなくても問題はないですが、付けることでこのインターフェースは関数型インターフェースの条件を満たしているということを明示することができます(仮に満たしていない場合はコンパイルエラーが出ます)。
コンパイル時に検証することができるので、基本的に付けるようにしたいですね!

java.util.functionパッケージを使う

ここまではSampleIfという独自の関数型インターフェースを定義し、実装を行ってきました。
実はこれだけの処理であれば独自に定義する必要はありません。

というのも、Java8からは便利で汎用的な関数型インターフェースが既に用意されているためです。

この、汎用的に使える関数型インターフェースはjava.util.functionパッケージで用意されていて、importするだけで利用することができます。

試しにFunctionという関数型インターフェースをJava APIリファレンスで見てみましょう。

関数型インターフェースの宣言

@FunctionalInterface
public interface Function<T,R>

抽象メソッド

R apply(T t)

ここからわかることは

  • @FunctionalInterfaceが付いていることからもわかるように関数型インターフェースであり
  • TRという仮型パラメータを使用し
  • 1つしか定義されていない抽象メソッド(apply)はT型の引数を1つ受け取り、R型の戻り値を返すということです

T,Rって何?という方は第一回の投稿をご確認下さい!

実はこの関数型インターフェース(Function)は、上記で独自に定義した関数型インターフェース「SampleIf」と全く同じ処理内容です。

SampleIfはこのような関数型インターフェースでした

  • @FunctionalInterfaceが付いていることからもわかるように関数型インターフェースであり
  • StringIntegerという型を使用し
  • 1つしか定義されていない抽象メソッド(countLength)はString型の引数を1つ受け取り、Integer型の戻り値を返す

一緒ですよね?

要は独自に関数型インターフェースを定義する必要は全くありませんでした...
Functionを使えばよかったのです。

実際に使ってみましょう。

FuncSample.java

public static void main(String[] args) {
    Function<String, Integer> countFnc = new Function<String, Integer>() {
        //関数型インターフェースFunctionの抽象メソッド(apply)を実装
        @Override
        public Integer apply(String str) {
            return str.length();
        };
    };
    //実行
    System.out.println("カウント結果:" + countFnc.apply("テスト")); // -> カウント結果:3
}

処理結果は上記で独自に定義した関数型インターフェースと変わらないことが確認できるかと思います。

java.util.function内で覚えるべき関数型インターフェース

Functionのようにjava.util.function内には既に汎用的に使える関数型インターフェースが準備されています。
その中からよく使われるものをピックアップしてみました。

基本パターン

種類 抽象メソッド 概要
Consumer<T> void accept(T t) T型の引数を受け取り、戻り値を返さない
Supplier<T> T get() 引数がなく、T型の戻り値を返す
Function<T,R> R apply(T t) T型の引数を受け取り、R型の戻り値を返す
UnaryOperator<T> T apply(T t) T型の引数を受け取り、同じT型の戻り値を返す
Predicate<T> boolean test(T t) T型の引数を受け取り、booleanの値を結果として返す

※受け取れる引数はどれも1つです

仮に

  • 文字列を渡して
  • その文字列をただ表示させる

といった処理をしたい場合はどれになるでしょうか。

処理の内容としては
引数としてString型の文字列を渡してSystem.out.println()で表示させるだけなので、
引数が1つ、戻り値がないパターン

答えはConsumer<String>になりますね。

また、引数を2つ渡したい場合もあるかと思います。
その時は先頭にBiを付けるというパターンになっています。
※UnaryOperatorは先頭にBinaryに変わりBinaryOperatorとなります。

引数が2つのパターン

種類 抽象メソッド 概要
BiConsumer<T,U> void accept(T t, U u) T型とU型の引数を受け取り、戻り値を返さない
BiFunction<T,U,R> R apply(T t, U u) T型とU型の引数を受け取り、R型の戻り値を返す
BinaryOperator<T> T apply(T t, T t) T型の引数を2つ受け取り、同じT型の戻り値を返す
BiPredicate<T,U> boolean test(T t, U u) T型の引数を受け取り、booleanの値を結果として返す

他にも、
「引数としてintを受け取り、R型の戻り値を返す」IntFunction<R>
「T型の引数を受け取りintの戻り値を返す」ToIntFunction<T>

といった、プリミティブ型を扱える関数型インターフェース等も多数用意されていますが、基本的には最初の5つをしっかり覚えることが大事かと思います!

関数型インターフェースのまとめ

  • 関数型インターフェースとは抽象メソッドが一つしか定義されていないインターフェースのこと
  • 関数型インターフェースは自分で定義する前に既に用意されているもの(java.util.function)が利用できないのかを考えてみる
  • 関数型インターフェースの実装はラムダ式を用いると簡単に行える ※ 第三回を見て下さい

おわりに

Stream APIの為の事前知識ということで、全体的に深いところは突っ込まないようにして書いてみました。
この投稿だけだといまいちイメージを掴めないところもあるかと思うので、続きの投稿をご覧頂ければと思います!

7
2
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
7
2