🐕

Javaの「関数型インターフェースの歴史的経緯」をざっくりまとめてみた[Java入門]

に公開

はじめに

こんにちは。
プログラミング初心者Wakinozaと申します。
Java勉強中に調べたことを記事にまとめています。

十分気をつけて執筆していますが、なにぶん初心者が書いた記事なので、理解が浅い点などあるかと思います。
記事を参考にされる方は、初心者の記事であることを念頭において、お読みいただけると幸いです。
間違い等あれば、指摘いただけると助かります。

対象読者

  • Javaを勉強中の方
  • Java Silver試験を勉強中の方
  • 関数型インターフェースの意義や歴史的経緯を知りたい方。

結論

参考書を読んでもよくわからなかった「関数型インターフェース」の意義が、歴史的経緯を調べたらストンと腑に落ちたので、記事にまとめてみました。

要約すると、以下のような内容です。

  • Javaは命令型プログラミングだったが、時代の要請で、関数型プログラミングの方式を取り入れたかった
  • しかし従来のJavaは、メソッドを値として渡すことができなかったので、関数型プログラミング方式を実現しようとする、コードが長くなってしまう欠点があった
  • その欠点を解消するためにJava SE 8で実装されたのが「関数型インターフェース」
  • 「関数型インターフェース」が「メソッドの型」を提供した事で、メソッドを値のように受け渡しできるようになった

目次

1. 命令型プログラミングとは
2. 関数型プログラミングの台頭
3. Javaで関数型プログラミングを実現するには
4. 関数型インターフェースの導入

本文

1. 命令型プログラミングとは

初めに、命令型プログラミングと関数型プログラミングの違いから説明します。

まずは、命令型プログラミングについてです。

「命令型プログラミング」は、「どうやって(How)」計算するのかを記述するプログラミング方式です。「命令」の具体的な処理と順序を記述することで、変数やメモリの値を変化させ、結果を得ます。

一般的に「プログラミング」と聞いてイメージするのはこの「命令型プログラミング」方式であることが多いです。
Javaも、誕生時から「命令型プログラミング」を主軸としてきました。

命令型プログラミングは、以下の利点があります。

  • 日常的な思考プロセスに近いため、理解しやすい
  • 制御構文を使って、実行手順を細かく定義できる

その一方で、欠点もあります。

  • 頻繁に状態を変化させるため、副作用が起こりやすい
  • コードが長くなるにつれて複雑化し、可読性と保守性が低下する
  • 並列処理・並行処理をすると、競合状態を起こしやすい

特に2000年代以降は、複数のコアを持つコンピュータが普及するようになったため、並列処理や並行処理を安全に効率よく行えることが重要になってきました。
しかし、命令型プログラミングでは、前述したように、競合状態などのバグを起こす可能性があります。

そこで、「関数型プログラミング」と言う別の方式が注目されるようになってきました。

2. 関数型プログラミングの台頭

次に関数型プログラミングについて説明します。

「関数型プログラミング」は、プログラムが「何を(What)」計算するかを記述するプログラミング方式です。状態の変更を避け、数学的な関数のように入力に対して常に同じ出力を返す「関数」を組み合わせて、結果を得ます。

命令型プログラミングと関数型プログラミングとの違いは、以下の通りです。

命令型プログラミング 関数型プログラミング
考え方 「どのように(How)」計算するか。状態の変更と命令の順序を記述する 「何を(What)」を計算するか。関数を適用し合成する
状態 状態を積極的に変更する 状態の変更を避ける
副作用 副作用を持つことが多い 副作用を避ける
並行処理 競合状態を起こす可能性がある 競合状態が起こりにくい
テスト 状態が変化するため、テストが複雑になる 関数は独立しているため、テストが容易

また、関数型プログラミングには、3つの原則があります。

1. 純粋関数

  • 同じ入力が与えられれば、常に同じ出力が返る関数
  • 関数の外部にある状態を変更させないため、副作用が起こりにくい。

2. 不変性

  • 一度生成されたデータは変更されない。データの変更が必要な場合は、元のデータを変更するのではなく、新しいデータを作成して返す
  • 不変性によって競合状態を防ぎ、並列処理を容易にする

3. 関数を第一級オブジェクトとして扱う

  • 関数を変数に代入したり、他の関数の引数として渡したり、関数の戻り値として返したりできる
  • これにより、抽象的で柔軟なコードを書くことができ、処理の共通化や再利用がしやすくなる

「関数型プログラミング」の3つの原則を守ることで、コードが簡潔で、副作用が少なく、テストも容易で、並列処理をしても安全性が高いコードを記述することができるのです。

しかし、Javaに関数型プログラミングの方式を導入するには、1つ問題がありました。
当時のJavaでは、メソッドを「値」として扱えなかったのです。

3. Javaで関数型プログラミングを実現するには

当時のJavaのメソッドは、C++の関数ポインタやJavaScriptの関数のように、変数に代入したり、他のメソッドの引数や戻り値として扱うことはできませんでした。
仮に、あるメソッドAを別のメソッドBに渡したい場合、メソッドAを実装するクラスの「インスタンス」を生成し、そのインスタンスを渡さなければなりません。
そのため、たった一度しか使わない処理のためにクラスを定義しなければならない、という状況も生じていました。
当時も簡潔に記述したいというニーズがあったため、「匿名クラス」という方法が利用されていました。

「匿名クラス」とは、クラス名を指定せずに、クラス定義とインスタンス化を1つの式で記述する方法です。
例えば、特定のインターフェースを実装して利用したい場合があるとします。もし一度しか使わないクラスだった場合、1回の利用のためにクラスを定義するのは煩雑です。そのため、「匿名クラス」を利用して必要な実装とインスタンス化をその場で記述していました。

匿名クラスの例は、以下をご覧ください。

import java.util.function.Function;

public class Main{
  public static void main(String[] args){
    String str = new Function<String String>(){
      //1,3 string str = の後から.apply("tanaka");の前までが匿名クラスの部分
      public String apply(String s){ 
        return "Hello" + s;                   
      }.apply("tanaka");             //2
      System.out.println(str);       //4
    }
  }
}

1,匿名クラスによって、クラス名を指定せずに、apply()を実装し、インスタンス化している。
2,.apply("tanaka"); の記述によって、定義されたapply()メソッドが呼び出される
3,apply()メソッドの戻り値がstrに代入される
4,System.out.println()でstrの内容を表示する

クラスの記述を簡略化できる「匿名クラス」は確かに便利なシステムでしたが、お決まりのボイラープレートコードを書く必要があり、コードが長くなる傾向にありました。
そこで導入されたのが、「関数型インターフェース」です

4. 関数型インターフェースの導入

先述のとおり、従来のJavaにはメソッドとして渡す仕組みがありません。
「匿名クラス」など簡略に記述する仕組みはありましたが、それでもコードが冗長となり、「関数型プログラミング」に求められる簡潔性は実現しなかったのです。

「オブジェクト指向の枠組みの中で、メソッドを値をして渡したい」「関数型プログラミングの方式を取り入れて、安全で簡潔なコードを実現したい」そんな要望に応えるために導入されたのが「関数型インターフェース」です。

「関数型インターフェース」とは、抽象メソッドが一つだけ定義されているインターフェースです。staticメソッドやデフォルトメソッドを定義することもできますが、重要なのは「抽象メソッドが1つだけ」という点です。

関数型インターフェースによって実現したことは、主に2点です。

1. メソッドの「型」を提供する

Javaは静的型付け言語です。言い換えれば、すべての変数・メソッドの引数・戻り値の「型」が、プログラム実行前にコンパイラによってチェックされるという事です。
しかし、従来のJavaのメソッドには「型」がありませんでした。「型」は、どのような種類のデータであるのか、どのような操作が可能なのか、どんな例外や副作用を起こすのかをコンパイラがチェックするために必要な情報です。
もし「型」がない値を扱うと、誤ったコードを事前にチェックできず、実行時の挙動が予測しにくくなります。「型」のないメソッドを値として扱うと、Javaの安全性や堅牢性を損ねてしまうのです。

そのような事態を防ぐため、「関数型インターフェース」はメソッドの「型」を新たに提供しました。メソッドの型とは「入力型と出力型のペア」の事です。
そもそも、関数型プログラミングの「関数」は、数学的に入力型(引数)を受け取り、出力型(戻り値)を返すものと定義しています。そのためJavaでも、メソッドを関数のように扱えるよう、メソッドの型を「入力型と出力型のペア」として提供しています。

例を上げると、Function関数型インターフェースのR apply(T t) という抽象メソッドは、「T型の引数を1つ受け取り、R型の値を1つ返す」という型を表しています。

「関数型インターフェース」がメソッドの「型」を提供することで、メソッドを値として扱いながら、コンパイラチェックによる安全性と堅牢性を維持することができるのです。

2. メソッドをオブジェクトとして受け渡す

せっかく「関数型インターフェース」がメソッドの型を提供しても、1つのインターフェースに抽象メソッドが複数あれば、コンパイラはどの型か判断できなくなります。
そのため「関数型インターフェース」では、型を表現する抽象メソッドが1つだけ定義すると厳密に決められています。

まず、特定の型を持つメソッドが1つしかないインターフェースを定義します。そして、そのインターフェースを実装し、オブジェクトを生成します。これにより、「特定の型を持つメソッド」を「オブジェクト」として表現できるようになるのです。オブジェクトであれば、変数に代入したり、引数や戻り値として利用できます。

先ほどの匿名クラスで記述したコードを、関数型インターフェースで記述します。

import java.util.function.Function;

class MyFunc implements Function<String String>{ 
  //関数型インターフェースを実装
  public String apply(String str){  //抽象メソッドを実装
    return "Hello" + str;
  }
}

public class Main{
  public static void main(String[] args){
    MyFunc obj = new MyFunc(); 
    //関数型インターフェースを実装したクラスでオブジェクトを生成
    String str = obj.apply("tanaka");                    
    System.out.println(str); 
  }
}

上を見て、匿名クラスと比べて全然コードが簡潔になっていないと思った方、普通のインターフェースと何が違うのかわからないと思った方。そのとおりです。

実は「関数型インターフェース」は、あくまで型を提供する仕組みです。
単体で利用するものではなく、ラムダ式やメソッド参照、ストリームの土台を築くものなのです。
ラムダ式やメソッド参照を扱うことで、「関数型インターフェース」の凄さがわかります。

今回は、「関数型インターフェース」が導入されるに至った歴史的経緯や、その意義についてまとめました。
次回は、「関数型インターフェース」と共に導入された新しい仕組みを説明します。
具体的には、ラムダ式やメソッド参照についてまとめる予定です。
ストリームは、また別の機会で取り上げたいです。


記事は以上です。
最後までお読みいただき、ありがとうございました。

参考情報一覧

この記事は以下の情報を参考にして執筆しました。

GitHubで編集を提案

Discussion