Edited at

関数型インターフェースとは何か?(Java)

More than 3 years have passed since last update.

プログラム初心者なのでさらっと機能だけ説明な備忘録。


関数型インターフェースって?

 こちらのリンクが大変参考になりました。

 Java関数型インターフェースメモ(Hishidama's Java8 Functional Interface Memo)


関数型インターフェースの条件は、大雑把に言って、定義されている抽象メソッドが1つだけあるインターフェース。

staticメソッドやデフォルトメソッドは含まれていても構わない(関数型インターフェースの条件としては無視される)。

また、Objectクラスにあるpublicメソッドがインターフェース内に抽象メソッドとして定義されている場合、そのメソッドも無視される。


 インターフェースでは全て抽象メソッドで定義されますが、そのメソッドが1つのみ用意されているものです。

 これを使うと何がいいのか、というのも、またリンクですが以下のページでこう説明されています。

 関数型プログラミングって何、ラムダってなんだよ - Qiita


・記述自体が簡潔になることが多く、コードが読みやすくなる

・テストがしやすくなる

・関数同士を組み合わせても相互に影響しない

・渡す値にある程度見通しを持てるためバグを少なくできる


 1つの抽象メソッドのみを実装するので、コードが簡潔で見やすくなるようです。

 以下実装例です。これも上記のリンク先のものです。


public class exlambda{

public static void main(String... args){
hello h = (String name) -> { //ここの段階で抽象メソッドのメソッド定義を式として扱う
return "hello"+name;
};
System.out.println(h.sayHi("duke"));//出力:hello,duke
}
}
@FunctionalInterface
interface hello{
public String sayHi(String name);//抽象メソッド
}```

 ここではhelloインターフェースのsayHiメソッドをmain関数内で実装しています。

 main関数ではラムダ式という方法で実装されています。ラムダ式では

(インターフェース名) ex = (抽象メソッドの引数) -> {メソッドの実装};

 という記述方法でインターフェースを実装します。

 メソッドの名前は記述しませんが、抽象メソッドが1つであることが明白なので省略できるわけです。便利ですね。

 ちなみに、例でのメソッド引数の型も明白なので省略できます。

 RunnnableとかActionListenerを使ってる人はもうとっくに知ってるよ、となるかもしれません。


コードを確認する

 という前提を置いて、西森のコードを改善してくださったshiracamusさんのコードを見てみたいと思います。

        void dig(int row, int col) {

// 指定された位置を道にする。
map[row][col] = ROAD;

// 掘り進む4方向。Collectionsでシャッフルしたかったのでリストにしました。
ArrayList<BiConsumer<Integer, Integer>> direction = new ArrayList<>();
direction.add(this::digRight);
direction.add(this::digLeft);
direction.add(this::digDown);
direction.add(this::digUp);

// ランダムに方向を決め、進める方向に2マス進んでさらに掘り進める。
Collections.shuffle(direction);
direction.stream().forEach(dig -> dig.accept(row, col));
}

// 左方向が壁なら道を作り、更に壁を掘り進む
void digLeft(int row, int col) {
if (col - 2 > 0 && map[row][col - 2] == WALL) {
map[row][--col] = ROAD;
dig(row, --col);
}
}

// 右方向が壁なら道を作り、更に壁を掘り進む
void digRight(int row, int col) {
if (col + 2 < width - 1 && map[row][col + 2] == WALL) {
map[row][++col] = ROAD;
dig(row, ++col);
}
}

// 上方向が壁なら道を作り、更に壁を掘り進む
void digDown(int row, int col) {
if (row + 2 < height - 1 && map[row + 2][col] == WALL) {
map[++row][col] = ROAD;
dig(++row, col);
}
}

// 下方向が壁なら道を作り、更に壁を掘り進む
void digUp(int row, int col) {
if (row - 2 > 0 && map[row - 2][col] == WALL) {
map[--row][col] = ROAD;
dig(--row, col);
}
}

 まず、ここで関数型インターフェースのリストを作成しています。

 ArrayList<BiConsumer<Integer, Integer>> direction = new ArrayList<>();

 ここでBiConsumerというものが関数型インターフェースです。

 このインターフェースは、2つの引数をとり、戻り値を返さないものです。なので、メソッド内で何かしらの副作用を引き起こすために使うものとなります。

 それに以下のような式で、メソッドの参照を与えてあげます。

direction.add(this::digRight);

direction.add(this::digLeft);
direction.add(this::digDown);
direction.add(this::digUp);

 ここでまたthis::digRightのような見慣れない記述が出てきましたが、これがメソッド参照です。メソッド参照は以下のように置き換えられます。

 this::digRight ⇔ (int row, int col) -> { digRight(row, col) };

 このようにメソッド参照でインターフェースの実装ができるわけですね。

 このリストに対して、今度はストリームと呼ばれるもので処理を行っていきます。

 direction.stream().forEach(dig -> dig.accept(row, col));

 ストリームと書いて申し訳ないんですが、ここでstream()を入れている理由が西森には分かりませんでした。

 stream()を入れると、forEachまでの間に中間処理ができるようになります。例えば、filterを使って処理を限定したり、mapで出力を加工したりなどです(ここまでは説明しません)。

 stream()がなくても動いたので、ここはどういう意味があるんでしょうか……。

 また、リストに対してforEachしてあげることは以下と同じです。

for(BiConsumer dig : direction){

dig.accept(row, col);
}

 拡張for文がコンパクトになった形です。

 forEachにして何がうれしいかというと、型を書かなくてもよいことです。

 forEachでは型推定がなされるので、BiConsumerなどを明示しなくてもいいわけですね。

 

 また、dig.accept()では、関数型インターフェースで実装された処理が実行されるようになってます。

 メソッドが1つなので、適応するということだけで事足りるんですね。

 ここらへんで、私的な振り返りは終了します。

 関数型インターフェースを理解するためにほかにも色々なことを調べる必要がありましたが、いかんせん肝心なところ(どう使うか)を理解できていない気がします。

 間違いが多々あると思いますが、その場合はコメントで訂正お願いします。