Javaは関数型プログラミングをどのようにサポートしていますか?

バックグラウンド

長い間、Javaはオブジェクト指向言語でした。すべてがオブジェクトであり、関数を呼び出す場合、関数はクラスまたはオブジェクトに属していて、そのクラスまたはオブジェクトを使用して呼び出す必要があります。しかし、JSやC ++などの他のプログラミング言語では、関数を直接記述して、必要なときに呼び出すことができます。これは、オブジェクト指向プログラミングまたは関数型プログラミングと言えます。機能の観点からは、オブジェクト指向プログラミングには何も悪いことはありませんが、開発の観点からは、オブジェクト指向プログラミングは繰り返される可能性のある多くのコード行を記述します。たとえば、Runnableの匿名クラスを作成する場合:

Runnable runnable = new Runnable(){  
    @Override  
    public void run(){  
        System.out.println( "do something ..."); 
    }  
}; 

このコードの唯一の有用な部分はrunメソッドの内容であり、残りの部分はJavaプログラミング言語の構造部分であり、役に立たないが、作成する必要がある。幸い、Java 8以降、関数型プログラミングインターフェイスとLambda式が導入され、エレガントなコードの記述を減らしています。

//一行即可
Runnable runnable  =()-> System.out.println( "do something ..."); 

主に3つの主流のプログラミングパラダイム、プロセス指向、オブジェクト指向、および関数型プログラミングがあります。

関数型プログラミングはそれほど新しいものではなく、50年以上前に登場しました。近年、関数型プログラミングはますます注目を集めており、Clojure、Scala、Erlangなどの多くの新しい関数型プログラミング言語が登場しています。一部の非関数型プログラミング言語には、Java、Python、Ruby、JavaScriptなどの関数型プログラミングをサポートするための多くの機能、構文、およびクラスライブラリも追加されています。さらに、Google Guavaでは関数型プログラミングも強化されています。

プログラミングの特殊性のため、関数型プログラミングは科学計算、データ処理、統計分析、およびその他の分野での利点を十分に発揮できるだけであり、より一般的なオブジェクト指向プログラミングパラダイムを完全に置き換えることはできません。しかし、補足として、それは存在、発達、学習にとっても大きな意味を持っています。

関数型プログラミングとは

関数型プログラミングの英語訳は、関数型プログラミングです。

では、関数型プログラミングとは正確には何ですか?実際、関数型プログラミングの厳密な公式の定義はありません。厳密に言うと、関数型プログラミングの「関数」は、プログラミング言語の「関数」の概念ではなく、指数関数的な「関数」または「式」を指します(例:y = f(x))。ただし、プログラミングの際、数学的な「関数」または「式」については、通常、それらを関数として習慣的に設計します。したがって、詳しく説明しなければ、関数型プログラミングの「関数」は、プログラミング言語の「関数」としても理解できます。

各プログラミングパラダイムには独自の場所があるため、パラダイムとして抽象化されています。オブジェクト指向プログラミングの最大の特徴は、クラスとオブジェクトをコード編成の単位として取ることと、その4つの特徴です。プロセス指向プログラミングの最大の特徴は、関数がコード編成の単位として使用され、データとメソッドが分離されていることです。関数型プログラミングの最もユニークな部分は何ですか?実際、関数型プログラミングの最もユニークな部分はプログラミングのアイデアにあります。関数型プログラミングでは、プログラムは一連の数学関数または数式の組み合わせで表すことができると考えています。関数型プログラミングとは、プログラミングを数学に低レベルで抽象化したもので、計算プロセスを式として記述します。しかし、あなたは間違いなくこのように疑問を抱くでしょうどのプログラムも一連の数式として表現できますか?

理論的には、それは可能です。ただし、すべてのプログラムがこれに適しているわけではありません。関数型プログラミングには、科学計算、データ処理、統計分析など、独自の適切なアプリケーションシナリオがあります。これらの分野では、多くの場合、プログラムは数式で表現する方が簡単であり、同じ機能を実現する非関数型プログラミングと比較して、関数型プログラミングは非常に少ないコードで実行できます。しかし、強いビジネスに関連する大規模なビジネスシステムの開発では、それを数式に抽象化することは難しく、関数型プログラミングを使用して実現することは明らかに困難を伴います。逆に、このアプリケーションシナリオでは、オブジェクト指向プログラミングがより適切であり、記述されたコードはより読みやすく、保守が容易です。

プログラミングに関しては、関数型プログラミングは、プロセス指向プログラミングと同様に、関数をコード編成の単位として使用します。ただし、手続き型プログラミングとは異なり、その機能はステートレスです。ステートレスとは簡単に言うと、関数に含まれる変数はすべてローカル変数であり、オブジェクト指向プログラミングのようなクラスメンバー変数も、プロセス指向プログラミングのようなグローバル変数も共有しません。関数の実行結果は入力パラメーターにのみ関連し、他の外部変数とは何の関係もありません。同じ入力は、どのように実行しても、結果は同じです。これは実際には、数学関数または数式の基本的な要件です。例えば:

//ステートフル関数:実行結果はbの値に依存します。入力パラメーターが同じであっても、
// bの値が異なる可能性があるため、関数が複数回実行される場合、関数の戻り値は異なる場合  があります。 
int b;  
int incremental(int a){  
  return a + b;  
 
//ステートレス関数:実行結果は外部変数の値に依存しない  
//入力パラメーターが同じである限り、何回実行されても、関数の戻り値は同じ  
int増加になります(int a、int b){  
  return a + b;  

異なるプログラミングパラダイムは完全に異なるわけではなく、常にいくつかの共通のプログラミングルールがあります。たとえば、プロセス指向、オブジェクト指向、関数型のいずれのプログラミングであっても、それらはすべて変数と関数の概念を持っています。最上位には、プログラミングユニット(クラス、関数など)をアセンブルするためのメイン関数実行エントリが必要です。ただし、オブジェクト指向プログラミングユニットはクラスまたはオブジェクトであり、プロセス指向プログラミングユニットは関数であり、関数型プログラミングのプログラミングユニットはステートレス関数です。

関数型プログラミングに対するJavaのサポート

オブジェクト指向プログラミングでは必ずしもオブジェクト指向プログラミング言語を使用する必要はなく、同様に関数型プログラミングでは必ずしも関数型プログラミング言語を使用する必要はありません。現在、多くのオブジェクト指向プログラミング言語は、関数型プログラミングをサポートするために、対応する構文とクラスライブラリも提供しています。

オブジェクト指向プログラミング言語であるJavaは、例を通して関数型プログラミングをサポートしています。

public class Demo {  
  public static void main(String [] args){  
    Optional <Integer> result = Stream.of( "a"、 "be"、 "hello"  
            )
            . map(s-> s.length())  。 filter(l-> l <= 3)  
            .max((o1、o2)-> o1-o2); 
    System.out.println(result.get()); //输出2  
  }  

このコードの機能は、一連の文字列配列から長さが3以下の文字列をフィルターで除外し、その中で最大長を見つけることです。

Javaは、関数型プログラミングの3つの新しい文法概念を導入しています。ストリームクラス、ラムダ式、関数型インターフェース(Functional Inteface)です。Streamクラスは、「。」を介して複数の関数操作をカスケードするコード記述メソッドをサポートするために使用されます。ラムダ式を導入する機能は、コード記述を簡素化することです。関数インターフェイスの関数は、関数を関数インターフェイスにラップして関数を実現することです。これをパラメーターとして使用します(JavaはCのような関数ポインターをサポートしていません。関数をパラメーターとして直接使用できます)。

ストリームクラス

このような式を計算するとします:(3-1)* 2 + 5。通常の関数呼び出しの方法で書くと、次のようになります。

add(multiply(subtract(3,1)、2)、5); 

ただし、このようなコードを書くことは理解するのが難しいようです。以下に示すように、より読みやすい記述方法に変更してみましょう。

subtract(3,1).multiply(2).add(5); 

Javaでは、「。」はオブジェクトのメソッドを呼び出すことを意味します。上記のカスケード呼び出しメソッドをサポートするために、各関数に汎用のStreamクラスオブジェクトを返させます。Streamクラスには、中間操作と終了操作の2つのタイプの操作があります。中間操作はStreamクラスオブジェクトを返し、終了操作は明確な値の結果を返します。

前の例をもう一度見て、コードを説明しましょう。その中でも、マップとフィルターは中間操作であり、Streamクラスオブジェクトを返します。他の操作のカスケードを続行できます。maxは終了操作であり、返されるのはStreamクラスオブジェクトではなく、カスケードダウンを継続できなくなります。

public class Demo {  
  public static void main(String [] args){  
    Optional <Integer> result = Stream.of( "f"、 "ba"、 "hello")// of return Stream <String>  
            object.map(s -> s.length())//マップはStream <Integer>オブジェクトを返します  
            filter(l-> l <= 3)//フィルターはStream <Integer>オブジェクトを返します  
            max((o1、o2)-> o1-o2 ); //最大終了操作:return Optional <Integer>  
    System.out.println(result.get()); //出力2  
  }  

ラムダ式

前述のように、JavaによるLambda式の導入の主な機能は、コードの記述を簡略化することです。実際、ラムダ式を使用せずにサンプルのコードを記述することもできます。map関数を例にとってみましょう。

次の3つのコードは、最初のコードがマップ関数の定義を示しています。実際、マップ関数が受け取るパラメーターは、関数インターフェイスでもある関数インターフェイスです。2番目のコードは、map関数の使用方法を示しています。3番目のコード部分は、ラムダ式を使用して2番目のコード部分を記述する簡単な方法です。実際、ラムダ式はJavaの単なる構文糖であり、最下層は2番目のコードが示すように、機能インターフェースに基づいて実装されています。

// Streamクラスのマップ関数の定義:  
public interface Stream <T> extends BaseStream <T、Stream <T >> {  
  <R> Stream <R> map(Function <?super T ,? Extends R> mapper);  
  / / ...他の関数を省略...  
 
// Streamクラスでマップを使用する方法の例:  
Stream.of( "fo"、 "bar"、 "hello")。map(new Function <String、Integer>() {  
  @Override  
  public Integer apply(String s){  
    return s.length();  
  }  
});  
 
// Lambda式を使用した簡略化された書き込み:  
Stream.of( "fo"、 "bar"、 "hello")。 map(s-> s.length()); 

ラムダ式には、入力、関数本体、出力の3つの部分があります。式は次のとおりです。

(a、b)-> {statement 1; statement 2; ...; return output;} // a、bは入力パラメーター 

実際、ラムダ式は非常に柔軟です。上記は標準的な表記であり、多くの簡略表記があります。たとえば、入力パラメーターが1つしかない場合は、()を省略してa-> {...}として直接書き込むことができます。入力パラメーターがない場合は、入力と矢印の両方を直接省略して、関数本体のみを保持できます。関数本体にステートメントが1つしかない場合は、 {}は省略できます。関数が値を返さない場合は、returnステートメントを省略できます。

オプション<Integer>結果= Stream.of( "f"、 "ba"、 "hello").  
        map(s-> s.length()).  
        filter(l-> l <= 3)  
        .max((o1 、o2)-> o1-o2); 
         
//还原为函数接口的实现方式  
オプションの<整数>結果2 = Stream.of( "FO"、 "バー"、 "こんにちは")  
        の.map(新機能<文字列、整数>(){  
          @Override  
          公共整数(適用されます文字列s){  
            戻りs.length();  
          }  
        })  
        .filter(新しい述語<整数>(){  
          @Override  
          公共ブーリアンテスト(整数L){  
            戻りL <= 3。 
          @Override 
          public int compare(Integer o1、Integer o2){  
            return o1-o2; 
          }  
        }); 

Lambda式と匿名クラスの類似点と相違点は、次の3つの点に集中しています。

  • ラムダは匿名の内部クラスを最適化するために誕生しました。ラムダは匿名のクラスよりもはるかに簡潔です。
  • Lambdaは関数型インターフェースにのみ適用でき、匿名クラスは制限されません。
  • つまり、匿名クラスのthisは「匿名クラスオブジェクト」自体であり、ラムダ式のthisは「ラムダ式を呼び出すオブジェクト」を指します。

関数インターフェース

実際、上記のコードの関数、述語、およびコンパレータはすべて機能的なインターフェイスです。C言語では、関数を変数として直接使用できる関数ポインターをサポートしています。

ただし、Javaには関数ポインタの構文がありません。そのため、関数インターフェイスを通じて関数をインターフェイスにラップし、変数として使用します。実際、機能インターフェースはインターフェースです。ただし、独自の特別な場所もあります。つまり、実装されていないメソッドが1つだけ必要です。このようにしてのみ、ラムダ式はどのメソッドが一致するかを明確に知ることができます。2つの実装されていないメソッドがあり、インターフェースの入力パラメーターと戻り値が同じである場合、JavaがLambda式を変換するときに、式がどのメソッドに対応するかがわかりません。

関数型インターフェースも一種のJavaインターフェースですが、次の条件を満たす必要もあります。

  • 関数型インターフェースには、1つの抽象メソッド(単一の抽象メソッド)しかありません。
  • Objectクラスのパブリック抽象メソッドは、単一の抽象メソッドとは見なされません。
  • 機能インターフェースには、デフォルトのメソッドと静的メソッドを含めることができます。
  • 関数型インターフェースは@FunctionalInterfaceアノテーションで装飾できます。

これらの条件を満たすインターフェースは、機能インターフェースと見なすことができます。たとえば、Java 8のコンパレータインターフェース:

@FunctionalInterface  
public interface Comparator <T> {  
    / **  
     *単一の抽象メソッド  
     * @since 1.8  
     * /  
    int compare(T o1、T o2); 
 
    / **  
     * Object 
     类中のパブリック抽象メソッド   * @since 1.8  
     * /  
    boolean equals(Object obj); 
 
    / **  
     *认认方法  
     * @since 1.8 
     * /  
    default Comparator <T> reverse  (){  
        return Collections.reverseOrder(this); 
    }  
 
     
    / **  
     *静态方法  
     * @since 1.8  
     * /   
    public static <T extends Comparable <?super T >> Comparator <T> reverseOrder(){ 
        return Collections.reverseOrder(); 
    } 
 
    //省略...  

関数型インターフェイスの用途は何ですか?つまり、関数型インターフェイスの最大の利点は、最小限のラムダ式を使用してインターフェイスをインスタンス化できることです。なぜそう言うのでしょうか?Runnable、ActionListener、Comparatorなどの抽象メソッドが1つしかない多かれ少なかれ使用されているインターフェイスがあります。たとえば、並べ替えアルゴリズムを実装するには、Comparatorを使用する必要があります。通常、処理メソッドは2つ以下です。

  • 並べ替えロジックをカプセル化するために、コンパレータインターフェースを適切に実装するJavaクラスを記述します。ビジネスで複数の並べ替え方法が必要な場合は、複数のクラスを作成して複数の実装を提供する必要があり、これらの実装は多くの場合1回だけ使用する必要があります。
  • 別の賢いアプローチは、必要に応じて、次のような匿名の内部クラスを作成することです。
public class Test {   
    public static void main(String args []){   
        List <Person> Person = new ArrayList <Person>(); 
        Collections.sort(人、新しいコンパレータの<person>(){  
            @Override  
            比較公共INT(人物O1、O2人){  
                リターンInteger.compareTo(o1.getAge()、o2.getAge());  
            }  
        })。 
    }   

匿名の内部クラスによって実装されるコードの量は多くなく、構造はかなり明確です。Jdk 1.8のComparatorインターフェースの実装により、FunctionalInterfaceアノテーションが追加されます。これは、Comparatorが機能的なインターフェースであり、ユーザーがラムダ式を通じて安全にインスタンス化できることを意味します。次に、ラムダ式を使用してカスタムコンパレーターをすばやく新規作成するために作成する必要があるコードを見てみましょう。

Comparator <Person>コンパレータ=(p1、p2)-> Integer.compareTo(p1.getAge()、p2.getAge()); 

->前面()は、Compareメソッドの比較メソッドのパラメーターリストで、背面->は、compareメソッドのメソッド本体です。

以下は、Javaが提供する2つの関数インターフェース(FunctionおよびPredicate)のソースコードの抜粋です。

@FunctionalInterface  
public interface Function <T、R> {  
    R apply(T t); //これだけでは実装されていない方法  
 
    default <V> Function <V、R> compose(Function <?super V、?extends T> before){  
        Objects.requireNonNull(before); 
        return(V v)-> apply(before.apply(v)); 
    }  
 
    デフォルト<V> Function <T、V> andThen(Function <?super R、?extends V> after){  
        Objects.requireNonNull(after); 
        return(T t)-> after.apply(apply(t)); 
    }  
 
    static <T> Function <T、T> identity(){  
        return t-> t; 
    }  
 
@FunctionalInterface  
public interface Predicate <T> 
 
    デフォルトのPredicate <T>および(Predicate <?super T> other){  
        Objects.requireNonNull(other); 
        return(t)-> test(t)&& other.test(t); 
    }  
 
    デフォルトのPredicate <T> negate(){  
        return(t)->!test(t); 
    }  
 
    デフォルトのPredicate <T>または(Predicate <?super T> other){  
        Objects.requireNonNull(other); 
        return(t)-> test(t)|| other.test(t); 
    }  
 
    static <T> Predicate <T> isEqual(Object targetRef){  
        return(null == targetRef)  
                ?Objects :: isNull  
                :object-> targetRef.equals(object); 
    }  

@FunctionalInterfaceアノテーションの使用シナリオ

インターフェースが1つの抽象メソッドのみの条件を満たす限り、@ FunctionalInterfaceの有無に関係なく、機能的なインターフェースとして使用できることがわかっています。ただし、jdkがこのアノテーションを定義するのには理由があるはずです。開発者にとって、続行する前にこのアノテーションの使用を2度考える必要があります。

このアノテーションが使用され、抽象メソッドがインターフェースに追加された場合、コンパイラーはエラーを報告し、コンパイルは失敗します。つまり、@ FunctionalInterfaceは、この抽象メソッドのみが世代のインターフェースに存在するという約束です。したがって、開発者はLambdaを使用して、この注釈を使用する任意のインターフェースをインスタンス化できます。もちろん、@ FunctionalInterfaceの誤用の結果は非常に悲惨です。ある日、このアノテーションを削除して抽象メソッドを追加すると、Lambdaを使用してインターフェイスをインスタンス化するすべてのクライアントコードが正しくコンパイルされません。

特に、インターフェイスに抽象メソッドが1つしかなく、@ FunctionalInterfaceアノテーションで装飾されていない場合、インターフェイスが将来抽象メソッドを追加しないことを他の人が約束していないため、Lambdaを使用してインスタンス化したり、前のメソッドを正直に使用したりしないことをお勧めしますこの方法は比較的安全です。

概要

関数型プログラミングは、数学的関数マッピングのアイデアとより一致しています。プログラミング言語レベルに固有であり、ラムダ式を使用して関数マッピングをすばやく記述でき、関数はチェーン呼び出しを通じて接続され、必要なビジネスロジックを完成させます。Javaのラムダ式は後で導入されましたが、並列処理における関数型プログラミングの利点により、ビッグデータコンピューティングの分野で広く使用されています。

おすすめ

転載: blog.csdn.net/sinat_37903468/article/details/108735418