title: 【転載】Java Lambda 表現の解明
date: 2021-08-14 08:35:12
comment: false
toc: true
category:
- Java
tags: - 転載
- Java
- Lambda
- 解明
この記事は[訳] Java Lambda 表現の解明から転載されています。
- 原文アドレス:Demystifying Java Lambda Expressions
- 原文著者:Randal Kamradt Sr
- 訳文出典:掘金翻訳計画
- この記事の永久リンク:github.com/xitu/gold-m…
- 訳者:samyu2000
- 校正者:Kimhooo,1autodidact
Java Lambda 表現の解明#
私は Java における関数型プログラミングについて多くの時間を費やしているようです。実際、特に難解なことはありません。特定の関数の機能を使用するためには、関数の中に関数をネストして定義する必要があります。なぜそうするのでしょうか?オブジェクト指向の方法で開発を行うとき、すでに関数型プログラミングの手法を使用しているのですが、それは制御された方法で使用されているだけです。Java におけるポリモーフィズムは、サブクラスでオーバーライド可能な複数の関数を保持することによって実現されています。こうすることで、そのクラスの他の関数はオーバーライドされた関数を呼び出すことができ、外部関数がオーバーライドされていなくても、その動作が変わります。
ポリモーフィズムの例を挙げ、それを lambda 式を使用した関数に変換してみましょう。コードは以下の通りです:
@Slf4j
public class Application {
static abstract class Pet {
public abstract String vocalize();
public void disturb() { log.info(vocalize()); }
}
static class Dog extends Pet {
public String vocalize() { return "bark"; }
}
static class Cat extends Pet {
public String vocalize() { return "meow"; }
}
public static void main(String [] args) {
Pet cat = new Cat();
Pet dog = new Dog();
cat.disturb();
dog.disturb();
}
}
これは非常に古典的なオブジェクト指向の例であり、Dog
クラスとCat
クラスがPet
クラスを継承して実装している例です。聞き覚えがありますか?この非常にシンプルな例で、disturb () メソッドを実行すると、プログラムはどのように動作するでしょうか?結果は:猫と犬がそれぞれ自分の鳴き声を発します。
しかし、もし蛇が必要な場合はどうしますか?新しいクラスを作成する必要がありますが、もし 1000 個のクラスが必要な場合はどうでしょう?各クラスにはテンプレートファイルが必要です。もしPet
インターフェースが単純なメソッドを 1 つだけ持っているなら、Java もそれを関数として扱います。私はPet
をdisturb
インターフェースから移動させました(最初はここにいなかったかもしれませんが、それはPet
の属性ではありません)。以下のようになります:
@Slf4j
public class Application {
interface Pet {
String vocalize();
}
static void disturbPet(Pet p) {
log.info(p.vocalize());
}
public static void main(String [] args) {
Pet cat = () -> "meow";
Pet dog = () -> "bark";
Pet snake = () -> "hiss";
disturbPet(cat);
disturbPet(dog);
disturbPet(snake);
}
}
この奇妙な構文() -> something
は驚くべきものです。しかし、これは引数を持たず、オブジェクトを返す関数を定義しているだけです。Pet
インターフェースには 1 つのメソッドしかないため、開発者はこの方法でメソッドを呼び出すことができます。技術的には、これはPet
インターフェースを実装し、vocalize
関数をオーバーライドしています。しかし、私たちの議論のテーマに関しては、これは他の関数に埋め込むことができる関数です。
Supplier
インターフェースがPet
インターフェースの代わりに使用できるため、このコードはさらに簡素化できます。以下のようになります:
@Slf4j
public class Application {
static void disturbPet(Supplier<String> petVocalization) {
log.info(petVocalization.get());
}
public static void main(String [] args) {
disturbPet(() -> "meow");
disturbPet(() -> "bark");
disturbPet(() -> "hiss");
}
}
Supplier
は公共インターフェースであり、java.util.function
パッケージにあります。
これらの lambda 関数は、私たちが空想で作り出したように見えます。しかし、その背後では、単一の関数を使用してインターフェースを実装し、単一の関数の具体的な実装を提供しています。
別の公共関数Consumer
についても議論しましょう。これは、ある値を入力パラメータとして受け取り、戻り値を持たず、本質的にはその値を消費します。リストやストリームオブジェクトのforEach
メソッドを使用したことがある場合、ここでConsumer
を使用できます。私たちはすべてのペットの鳴き声を収集し、リストに保存し、1 つずつ呼び出します。以下のようになります:
@Slf4j
public class Application {
static void disturbPet(Supplier<String> petVocalization) {
log.info(petVocalization.get());
}
public static void main(String [] args) {
List<Supplier<String>> yourPetVocalizations = List.of(
() -> "bark",
() -> "meow",
() -> "hiss");
yourPetVocalizations.forEach(v -> disturbPet(v));
}
}
今、もし鳥を追加したい場合は、リストに() -> "chirp"
を追加するだけです。注意:式v -> disturbPet(v)
の最初の v の両側には括弧を付けません。単一の引数を持つ lambda 式の場合、括弧は省略できます。
さて、私が示した例は直感的ではありません。私の目的は、ポリモーフィック関数から始めて、lambda 式に関連する内容を紹介することです。lambda 式を実際に使用できるのはいつでしょうか?いくつかの例があり、それらは一般的であり、繰り返し研究すべきです。これらの例は Stream ライブラリにも含まれています。
この例は比較的直感的であり、私は一連のファイルを取得し、ドットで始まらないファイルを削除し、ファイル名とファイルサイズを取得します。まず、現在のディレクトリからファイルの配列を取得し、それをStream
型に変換する必要があります。File
クラスを使用して実装できます:
File dir = new File(".");
Stream s = Arrays.stream(dir.listFiles());
ディレクトリもファイルオブジェクトであるため、特定のファイルオブジェクトの操作を実行できます。同時に、「.」はディレクトリを示し、listFiles
メソッドを呼び出すこともできます。しかし、それは配列を返すため、ストリームを使用して処理する必要があり、配列をストリームオブジェクトに変換するためにArrays.stream
メソッドを使用する必要があります。
今、ドットで始まるファイルを削除し、File
オブジェクトをその名前とサイズから成る文字列に変換し、アルファベット順に並べ替え、ログに書き込みます。
public class Application {
public static void main(String [] args) {
File dir = new File(".");
Arrays.stream(dir.listFiles())
.filter(f -> f.isFile())
.filter(f -> !f.getName().startsWith("."))
.map(f -> f.getName() + " " + f.length())
.sorted()
.forEach(s -> log.info(s));
}
}
lambda 式を処理するための 2 つの新しいメソッドはfilter
とmap
です。filter
メソッドはPredicate
型に相当し、map
メソッドはFunction
型に相当し、どちらもjava.util.function
パッケージに属しています。これらは、オブジェクトを操作するための一般的なメソッドを提供し、Predicate
はオブジェクトの特定の特性をテストするために使用され、Function
はオブジェクト間の変換に使用されます。
注意:私は 1 つのファイルのケースも考慮しました。ディレクトリをどのように処理しますか?再帰を使用してディレクトリに入るとどうなりますか?ストリームオブジェクトを使用してそれを処理するにはどうすればよいでしょうか?内部ストリームオブジェクトを外部ストリームオブジェクトに追加できる特別な map があります。これは何を意味するのでしょうか?以下の例を見てみましょう:
public static Stream<File> getFiles(File file) {
return Arrays.stream(file.listFiles())
.filter(f -> !f.getName().startsWith("."))
.flatMap(f -> {
if(f.isDirectory()) {
return getFiles(f);
} else {
return Stream.of(f);
}
});
}
ご覧の通り、ファイルオブジェクトがディレクトリである場合、flatMap
メソッド内で使用される lambda 式は再帰を実行し、そうでない場合はファイルオブジェクト自体を返します。flatMap
メソッドの lambda 式の戻り値は、あるストリームオブジェクトでなければなりません。したがって、単一のファイルの場合、Stream.of()
を使用して戻り値の型を一致させる必要があります。また、lambda 式は波括弧内に含まれているため、特定のオブジェクトを返す必要がある場合は、return 文を追加する必要があることにも注意してください。
getFiles
メソッドを使用するために、それを main メソッドに追加できます。
public static void main(String [] args) {
File dir = new File(".");
getFiles(dir)
.map(f -> f.getAbsolutePath()
.substring(dir.getAbsolutePath().length())
+ " " + f.length())
.sorted()
.forEach(s -> log.info(s));
}
全パスがない場合、ファイル名の相対パスを取得するために何らかのメカニズムを使用する必要があります。しかし、今はそれほど複雑ではありません。
他の関数を引数として持つ関数は一般に高階関数と呼ばれます。私たちはすでにいくつかの高階関数を理解しています:forEach
、filter
、map
、およびflatMap
。これらの各関数は、パラメータと戻り値とは異なる抽象的な方法でオブジェクトを操作する方法を表しています。私たちが lambda を使用するのは、明確な操作を行うためです。この方法を利用して、複数の操作を一連のオブジェクトに連結して、必要な結果を得ることができます。
この記事が読者に lambda 関数の神秘を明らかにすることを願っています。最初にこのトピックを導入したとき、それ自体が少し怖いものでした。もちろん、これは Alonzo Church の lambda 計算から借用されたものですが、それはまた別の話です。今、あなたは理解すべきです:このシンプルな構文を使用することで、関数も空から生み出すことができるのです。
訳文に誤りや改善点がある場合は、掘金翻訳計画で訳文を修正し PR を行うことができ、相応の報酬ポイントを得ることができます。記事の冒頭のこの記事の永久リンクは、この記事の GitHub 上の MarkDown リンクです。
掘金翻訳計画は、質の高いインターネット技術記事を翻訳するコミュニティであり、出典は掘金上の英語の共有記事です。内容はAndroid、iOS、フロントエンド、バックエンド、ブロックチェーン、製品、デザイン、人工知能などの分野をカバーしており、より多くの質の高い訳文を見たい場合は、掘金翻訳計画、公式微博、知乎専欄を引き続きご覧ください。