Java 関数型プログラミングの詳細な分析

WBOY
リリース: 2022-11-10 16:17:22
転載
1307 人が閲覧しました

この記事は、java に関する関連知識を提供するもので、主に関数型プログラミングに関する関連コンテンツを紹介しています。Java は当初関数型プログラミングをサポートしていませんでしたが、Java8 ではこのメジャー バージョンで関数型プログラミングをサポートするために、 , Java には多くの重要な機能が紹介されています。一緒に見ていきましょう。皆さんのお役に立てれば幸いです。

Java 関数型プログラミングの詳細な分析

推奨学習: 「java ビデオ チュートリアル

Java は当初、関数型プログラミングをサポートしていませんでした。理解することは良い考えです。 , なぜなら、Class は Java の第一級市民であるため、Java でプログラミングを実装するのは簡単ではありません。しかし、難しいとはいえ、結果はすでにわかっています。Java 8 のメジャー バージョンでは、関数型プログラミングをサポートするために、 Java には多くの重要な機能が導入されています。これまでの記事では、Stream API のラムダ式とさまざまなストリーム操作について学習しました。今日の記事では、組み込みの Java 関数インターフェイスを整理します。

この記事の概要は次のとおりです。

Java は、開発者が Function## などの一般的な需要シナリオのユースケースに基づいて使用できるように、いくつかの組み込み関数インターフェイスを抽象化します。 #, Supplier 待ってください。Stream のさまざまな操作メソッドのパラメータや戻り値の型は、多くの場合、これらの組み込み関数インターフェイスです。

たとえば、Stream のマップ操作メソッドのパラメーターの型は

Function

<R> Stream<R> map(Function<? super T, ? extends R> mapper);
ログイン後にコピー
ログイン後にコピー
です。通常、マップ メソッドを使用するときにこの型が宣言されていないのはなぜですかストリーム操作のパラメータはどうなるのでしょうか?ストリーム API 操作の記事で、map メソッドの使用例を確認できます。たとえば、map メソッドを使用してストリーム内の各要素を大文字に変換する次の例などです。

List<String> list = new ArrayList<String>();
Stream<String> stream = list.stream();

Stream<String> streamMapped = stream.map((value) -> value.toUpperCase());
ログイン後にコピー
map メソッドのパラメータは直接 Lambada 式です:

(value) -> value.toUpperCase()
ログイン後にコピー
この Lambda 式は

Function インターフェイスの実装です。

関数型インターフェイスのキャリアは通常、ラムダ式です。コンパイラはラムダ式を通じて、ラムダ式のパラメータと戻り値に基づいて実装する関数型インターフェイスを推測します。 Lambda 式を使用してインターフェイスを実装すると、匿名内部クラスのようにクラスによって実装されるインターフェイスを指定する必要がなくなり、Stream 操作の多くのパラメーターや戻り値の型は Java の組み込み関数インターフェイスになりますが、匿名クラスを使用して実装します。

ラムダ式は非常に使いやすいですが、Java に組み込まれている関数型インターフェイスの型を見ると、「これは何ですか? これは何ですか?」と少し混乱する原因にもなります。 ?" フィーリング。

まず、関数型プログラミング、Java の関数型インターフェイス、Lambda が関数型インターフェイスしか実装できない理由について話しましょう。これらのことを明確にした後、Java が提供する組み込みの関数型インターフェイスを整理します。

関数型プログラミング

関数型プログラミングには、次の 2 つの重要な概念が含まれています。

    関数は第一級市民です
  • 関数は次の制約を満たす必要があります。
    • 関数の戻り値は、関数に渡される入力パラメーターのみに依存します。
    • 関数の実行には副作用はありません。
#プログラムを作成するときにこれらのルールすべてに常に従うわけではない場合でも、関数型プログラミングのアイデアを使用してプログラムを作成することで多くの恩恵を受けることができます。

次に、Java 関数プログラミングにおけるこれら 2 つの主要な概念の実装を見てみましょう。

関数は第一級市民です

関数型プログラミング パラダイムでは、関数は言語の第一級市民です。これは、文字列、マップ、その他のオブジェクトへの参照と同様に、関数の「インスタンス」と関数インスタンスへの変数参照を作成できることを意味します。関数は、引数として他の関数に渡すこともできます。

Java では、関数は明らかに第一級市民ではありませんが、クラスは第一級市民です。 Java がラムダ式を導入したのはそのためです。この糖衣構文により、Java はプレゼンテーション層から関数を持つことができ、変数やメソッドのパラメーターなどへの参照として関数を使用できるようになります。なぜプレゼンテーション層からだと言えるのでしょうか?実際には、Java コンパイラはコンパイル中にラムダ式をクラスにコンパイルするためです。

純粋関数

関数型プログラミングには純粋関数という概念があり、次の条件を満たす関数は純粋関数です。

##この関数の実行には副作用はありません。

    関数の戻り値は、関数に渡された入力パラメーターのみに依存します。
  • 以下は、Java の純粋な関数 (メソッド) の例です。
public class ObjectWithPureFunction{    public int sum(int a, int b) {        return a + b;
    }
}
ログイン後にコピー
上記の

sum()

メソッドの戻り値は、そのメソッドの戻り値にのみ依存します。入力パラメータ、そして

sum() には副作用はなく、関数の外部の状態 (変数) を変更することはありません。 代わりに、非純粋関数の例を次に示します:

public class ObjectWithNonPureFunction{    private int value = 0;    public int add(int nextValue) {        this.value += nextValue;        return this.value;
    }
}
ログイン後にコピー

add()方法使用成员变量value来计算其返回值,并且它还修改了value成员变量的状态,这代表它有副作用,这两个条件都导致add方法不是一个纯函数

正如我们看到的,函数式编程并不是解决所有问题的银弹。尤其是“函数是没有副作用的”这个原则就使得在一些场景下很难使用函数式编程,比如要写入数据库的场景,写入数据库就算是一个副作用。所以,我们需要做的是了解函数式编程擅长解决哪些问题,把它用在正确的地方。

函数式接口

Java中的函数式接口在 Lambda 表达式那篇文章里提到过,这里再详细说说。函数式接口是只有一个抽象方法的接口(抽象方法即未实现方法体的方法)。一个 Interface 接口中可以有多个方法,其中默认方法和静态方法都自带实现,但是只要接口中有且仅有一个方法没有被实现,那么这个接口就可以被看做是一个函数式接口

下面这个接口只定义了一个抽象方法,显然它是一个函数式接口:

public interface MyInterface {    public void run();
}
ログイン後にコピー

下面这个接口中,定义了多个方法,不过它也是一个函数式接口:

public interface MyInterface2 {
    public void run();

    public default void doIt() {
        System.out.println("doing it");
    }

    public static void doItStatically() {
        System.out.println("doing it statically");
    }
}
ログイン後にコピー

因为doIt方法在接口中定义了默认实现,静态方法也有实现,接口中只有一个抽象方法run没有提供实现,所以它满足函数式接口的要求。

这里要注意,如果接口中有多个方法没有被实现,那么接口将不再是函数式接口,因此也就没办法用 Java 的 Lambda 表达式实现接口了

编译器会根据 Lambda 表达式的参数和返回值类型推断出其实现的抽象方法,进而推断出其实现的接口,如果一个接口有多个抽象方法,显然是没办法用 Lambda 表达式实现该接口的。

@FunctionalInterface 注解

这里扩充一个标注接口是函数式接口的注解@FunctionalInterface

@FunctionalInterface 
// 标明接口为函数式接口
public interface MyInterface {    public void run(); 
//抽象方法}
ログイン後にコピー

一旦使用了该注解标注接口,Java 的编译器将会强制检查该接口是否满足函数式接口的要求:“确实有且仅有一个抽象方法”,否则将会报错。

需要注意的是,即使不使用该注解,只要一个接口满足函数式接口的要求,那它仍然是一个函数式接口,使用起来都一样。该注解只起到--标记接口指示编译器对其进行检查的作用。

Java 内置的函数式接口

Java 语言内置了一组为常见场景的用例设计的函数式接口,这样我们就不必每次用到Lambda 表达式、Stream 操作时先创建函数式接口了,Java 的接口本身也支持泛型类型,所以基本上 Java 内置的函数式接口就能满足我们平时编程的需求,我自己在开发项目时,印象里很少见过有人自定义函数式接口。

在接下来的部分中,我们详细介绍下 Java 内置为我们提供了的函数式接口。

Function

Function接口(全限定名:java.util.function.Function)是Java中最核心的函数式接口。 Function 接口表示一个接受单个参数并返回单个值的函数(方法)。以下是 Function 接口定义的:

@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));
    }

    default <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;
    }
ログイン後にコピー

Function接口本身只包含一个需要实现的抽象方法apply,其他几个方法都已在接口中提供了实现,这正好符合上面我们讲的函数式接口的定义:“有且仅有一个抽象方法的接口”。

Function 接口中的其他三个方法中compseandThen 这两个方法用于函数式编程的组合调用,identity用于返回调用实体对象本身,我们之前在把对象 List 转换为 Map 的内容中提到过,可以回看前面讲 List 的文章复习。

Function接口用Java 的类这么实现

public class AddThree implements Function<Long, Long> {

    @Override
    public Long apply(Long aLong) {
        return aLong + 3;
    }

    public static void main(String[] args) {
        Function<Long, Long> adder = new AddThree();
		Long result = adder.apply(4L);
		System.out.println("result = " + result);
    }
}
ログイン後にコピー

不过现实中没有这么用的,前面说过 Lambda 表达式是搭配函数式接口使用的,用Lambda表达式实现上Function 接口只需要一行,上面那个例子用 Lambda 实现的形式是:

Function<Long, Long> adder = (value) -> value + 3;Long resultLambda = adder.apply(8L);
System.out.println("resultLambda = " + resultLambda);
ログイン後にコピー

是不是简洁了很多。后面的接口示例统一用 Lambda 表达式举例,不再用类实现占用太多篇幅。

Function接口的常见应用是 Stream API 中的 map 操作方法,该方法的参数类型是Function接口,表示参数是一个“接收一个参数,并返回一个值的函数”。

<R> Stream<R> map(Function<? super T, ? extends R> mapper);
ログイン後にコピー
ログイン後にコピー

所以我们在代码里常会见到这样使用 map 操作:

stream.map((value) -> value.toUpperCase())
ログイン後にコピー

Predicate

Predicate 接口 (全限定名:java.util.function.Predicate)表示一个接收单个参数,并返回布尔值 true 或 false 的函数。以下是 Predicate 功能接口定义:

public interface Predicate<T> {    boolean test(T t);
}
ログイン後にコピー

Predicate 接口里还有几个提供了默认实现的方法,用于支持函数组合等功能,这里不再赘述。 用 Lambda 表达式实现 Predicate 接口的形式如下:

Predicate predicate = (value) -> value != null;
ログイン後にコピー

Stream API 中的 filter 过滤操作,接收的就是一个实现了 Predicate 接口的参数。

Stream<T> filter(Predicate<? super T> predicate);
ログイン後にコピー

写代码时,会经常见到这样编写的 filter 操作:

Stream<String> longStringsStream = stream.filter((value) -> {    
// 元素长度大于等于3,返回true,会被保留在 filter 产生的新流中。
    return value.length() >= 3;
});
ログイン後にコピー

Supplier

Supplier 接口(java.util.function.Supplier),表示提供某种值的函数。其定义如下:

@FunctionalInterfacepublic interface Supplier<T> {
    T get();
}
ログイン後にコピー

Supplier接口也可以被认为是工厂接口,它产生一个泛型结果。与 Function 不同的是,Supplier 不接受参数。

Supplier<Integer> supplier = () -> new Integer((int) (Math.random() * 1000D));
ログイン後にコピー

上面这个 Lambda 表达式的 Supplier 实现,用于返回一个新的 Integer 实例,其随机值介于 0 到 1000 之间。

Consume

Consumer 接口(java.util.function.Consume)表示一个函数,该函数接收一个参数,但是不返回任何值。

@FunctionalInterfacepublic interface Consumer<T> {    void accept(T t);
}
ログイン後にコピー

Consumer 接口常用于表示:要在一个输入参数上执行的操作,比如下面这个用Lambda 表达式实现的 Consumer,它将作为参数传递给它的value变量的值打印到System.out标准输出中。

Consumer<Integer> consumer = (value) -> System.out.println(value);
ログイン後にコピー

Stream API 中的 forEach、peek 操作方法的参数就是 Consumer 接口类型的。

Stream<T> peek(Consumer<? super T> action);
void forEach(Consumer<? super T> action);
ログイン後にコピー

比如,Stream API 中的 forEach 操作,会像下面这样使用 Consume 接口的实现

Stream<String> stream = stringList.stream();
// 下面是Lambda 的简写形式
// 完整形式为:value -> System.out.println(value);
stream.forEach(System.out::println);
ログイン後にコピー

Optional

最后再介绍一下 Optional 接口,Optional 接口并不是一个函数式接口,这里介绍它主要是因为它经常在一些 Stream 操作中出现,作为操作的返回值类型,所以趁着学习函数式编程的契机也学习一下它。

Optional 接口是预防NullPointerException的好工具,它是一个简单的容器,其值可以是 null 或非 null。比如一个可能返回一个非空结果的方法,方法在有些情况下返回值,有些情况不满足返回条件返回空值,这种情况下使用 Optional 接口作为返回类型,比直接无值时返回 Null 要更安全。 接下来我们看看 Optional 怎么使用:

// of 方法用于构建一个 Optional 容器
Optional<String> optional = Optional.of("bam");
// 判断值是否为空
optional.isPresent();           // true
// 取出值,如果不存在直接取会抛出异常
optional.get();                 // "bam"
// 取值,值为空时返回 orElse 提供的默认值
optional.orElse("fallback");    // "bam"
// 如果只存在,执行ifPresent参数中指定的方法
optional.ifPresent((s) -> System.out.println(s.charAt(0)));// "b"
ログイン後にコピー

Stream 操作中像 findAny、 findFirst这样的操作方法都会返回一个 Optional 容器,意味着结果 Stream 可能为空,因此没有返回任何元素。我们可以通过 Optional 的 isPresent() 方法检查是否找到了元素。Java 编程那些绕不开的接口 这个子系列的文章已经更新完毕,感兴趣的请持续关注,后面还有更多实用、精彩的内容。


推荐学习:《java视频教程

以上がJava 関数型プログラミングの詳細な分析の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

ソース:juejin.im
このウェブサイトの声明
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。
人気のチュートリアル
詳細>
最新のダウンロード
詳細>
ウェブエフェクト
公式サイト
サイト素材
フロントエンドテンプレート