blog

Javaラムダ式がよくわかる

1.匿名の内部クラスの実装 匿名の内部クラスはまだクラスですが、ちょうどプログラマが指定されたクラス名を表示する必要はありません、コンパイラは自動的にクラスの名前を取ります。したがって、コードの次の形...

May 13, 2020 · 9 min. read
シェア

匿名内部クラスの実装

匿名内部クラスもクラスであることに変わりはありません。プログラマーが指定したクラス名を表示する必要がないだけで、コンパイラーは自動的にクラス名を付けます。コンパイラが自動的にクラス名を指定します。したがって、次のようなコードを書くと、コンパイル後に2つのクラス・ファイルが作成されます:

public class MainAnonymousClass {
	public static void main(String[] args) {
		new Thread(new Runnable(){
			@Override
			public void run(){
				System.out.println("Anonymous Class Thread run()");
			}
		}).start();;
	}
}

コンパイル後のファイルの配布は以下のようになり、2つのクラスファイルはそれぞれメインクラスと匿名内部クラスによって生成されます:

メインクラスMainAnonymousClass.classのバイトコードをさらに分析すると、匿名内部クラスのオブジェクトを作成していることがわかります:

// javap -c MainAnonymousClass.class
public class MainAnonymousClass {
 ...
 public static void main(java.lang.String[]);
 Code:
 0: new #2 // class java/lang/Thread
 3: dup
 4: new #3 // class MainAnonymousClass$1 /*内部クラスオブジェクトを作成する*/
 7: dup
 8: invokespecial #4 // Method MainAnonymousClass$1."<init>":()V
 11: invokespecial #5 // Method java/lang/Thread."<init>":(Ljava/lang/Runnable;)V
 14: invokevirtual #6 // Method java/lang/Thread.start:()V
 17: return
}

Lambda式の実装

ラムダ式はディレクティブによって実装され、ラムダ式を記述しても新しいクラスは生成されません。以下のようなコードであれば、コンパイル後のクラスファイルは1つだけになります:

public class MainLambda {
	public static void main(String[] args) {
		new Thread(
				() -> System.out.println("Lambda Thread run()")
			).start();;
	}
}

コンパイル後の結果:

ラムダ式の内部表現の違いは、javapでネーミングを逆コンパイルするとよくわかります:

// javap -c -p MainLambda.class
public class MainLambda {
 ...
 public static void main(java.lang.String[]);
 Code:
 0: new #2 // class java/lang/Thread
 3: dup
 4: invokedynamic #3, 0 // InvokeDynamic #0:run:()Ljava/lang/Runnable; /*invokedynamicコマンドを使って呼び出される*/
 9: invokespecial #4 // Method java/lang/Thread."<init>":(Ljava/lang/Runnable;)V
 12: invokevirtual #5 // Method java/lang/Thread.start:()V
 15: return
 private static void lambda$main$0(); /*Lambdaこの式はメイン・クラスのプライベート・メソッドとしてカプセル化されている。*/
 Code:
 0: getstatic #6 // Field java/lang/System.out:Ljava/io/PrintStream;
 3: ldc #7 // String Lambda Thread run()
 5: invokevirtual #8 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
 8: return
}

逆コンパイルした結果、ラムダ式はメイン・クラスのプライベート・メソッドにカプセル化され、ディレクティブを介して呼び出されていることがわかりました。

Streams API(I)

Javaが関数型プログラミングを重要視していることはあまり知られていないかもしれませんが、Java 8が関数型プログラミングを追加することでどれだけ機能を拡張したかを見てください:

  1. コードの簡素化関数型プログラミングは、クリーンで意図的なコードを書きます。
  2. マルチコアフレンドリーなJava関数型プログラミングは、並列プログラムをこれまで以上に簡単に書くことができます。

IntStream, LongStream, DoubleStream図中の4つのインターフェースはBaseStreamから継承されたもので、BaseStreamは3つの基本型に対応し、Streamは残りのすべての型のビューに対応します。データ型ごとに異なるインターフェースを設定することで、1. パフォーマンスを向上させ、2. 特定のインターフェース機能を追加することができます。

ほとんどの場合、Collection.stream()メソッドを呼び出して取得するのはコンテナですが、Collection.stream()メソッドには違いがあります:

  • ストレージはありません。データ構造ではなく、配列、Javaコンテナ、I/Oチャネルなど、何らかのデータソースのビューに過ぎません。
  • 関数型プログラミングのために作られました。例えば、フィルタリングを実行しても、フィルタリングされた要素は削除されず、フィルタリングされた要素を含まない新しい要素が生成されます。
  • 不活性実行。アクションはすぐに実行されず、ユーザーが実際に結果を必要とするときにのみ実行されます。
  • 消費可能。コンテナのイテレータと同じように、一度トラバースすると無効となり、再度トラバースするには再生成が必要です。

ペアに対する演算は2つのカテゴリーに分けられます

  1. 中間オペレーションは常に不活性に実行され、中間オペレーションを呼び出すと、そのオペレーションがマークされた新しいオペレーションが生成されるだけです。
  2. 終了操作が実際の計算のトリガーとなり、反復回数を減らすことができるように蓄積されたすべての中間操作を実行することによって計算が行われます。計算が完了すると終了します。

Apache Spark RDDに慣れていれば、この機能は目新しいものではありません。

以下の表は、Streamインターフェースの一般的なメソッドをまとめたものです:

中間操作

中間操作と終了操作を区別する最も簡単な方法は、メソッドの戻り値を見ることです。

flatMap()

<R> Stream<R> flatMap(Function<? super T,? extends Stream<? extends R>> mapper)の関数プロトタイプで、各要素の役割はマッパーによって指定された処理を実行することであり、マッパーによって返されたStreamの全要素が最終的な戻り結果として新しいStreamを形成します。平たく言えば、flatMap()の役割は、Streamの合成後、要素数の変換前と変換後で、すべての要素の元が「平ら」であることと等価であり、型が変わる可能性があります。

Stream<List<Integer>> stream = Stream.of(Arrays.asList(1,2), Arrays.asList(3, 4, 5));
stream.flatMap(list -> list.stream())
 .forEach(i -> System.out.println(i));

上記のコードは、元のストリームは、それぞれ2つの要素を持って、2つのリストは、flatMap()の実装後、各リストは、数字に "平坦化 "されますので、新しい5つの数字で構成されるストリームが生成されます。

今のところ、それほど難しくないストリームインターフェース関数を紹介してもらって、いい気分です。これで関数型プログラミングのすべてが終わったと思ったら、喜ぶのは早計です。次のStream制定操作のセクションで、現在の理解を新たにしましょう。

多面的なreduce()

演算は、要素の集合から値を生成するために達成することができ、合計()、最大()、最小()、カウント()などが演算であり、それらは一般的に使用されているだけに、関数として個別に設定されます。 3つの書き換えフォームのメソッド定義のreduce():

  • Optional<T> reduce(BinaryOperator<T> accumulator)
  • T reduce(T identity, BinaryOperator<T> accumulator)
  • <U> U reduce(U identity, BiFunction<U,? super T,U> accumulator, BinaryOperator<U> combiner)

関数定義はどんどん長くなっていますが、セマンティクスは変わりません。パラメータは初期値を指定したり、並列実行で複数のパートの結果をマージする方法を指定したりするだけです。reduce()は、値の束から値を生成したいシナリオで最もよく使われます。最大値や最小値を求めるためにこのような複雑な関数を使うのは、設計者が病気だと思いますか?そうでもありません。"big "と "small"、あるいは "sum "は時に異なる意味を持つからです。

需要:。ここでの "large "は "長い "という意味です。

// 最も長い単語を見つける
Stream<String> stream = Stream.of("I", "love", "you", "too");
Optional<String> longest = stream.reduce((s1, s2) -> s1.length()>=s2.length() ? s1 : s2);
//Optional<String> longest = stream.max((s1, s2) -> s1.length()-s2.length());
System.out.println(longest.get());

上記のコードでは、値の入れ物である最長の単語を選び出し、それを使うことで値のわずらわしさを回避しています。Stream.max(Comparator<? super T> comparator)もちろん、このメソッドを使って同じ効果を得ることもできますが、reduce() にはそれなりの存在意義があります。

要件これは「合計」操作で、オブジェクト入力タイプは次のとおり、結果タイプは次のとおりです。

// 単語の長さの和を求める
Stream<String> stream = Stream.of("I", "love", "you", "too");
Integer lengthSum = stream.reduce(0,&emsp;//  &emsp;// (1)
 (sum, str) -> sum+str.length(), // アキュムレーター //
 (a, b) -> a+b);&emsp;// パートとスプライサーは、並列実行時に使用される///。
// int lengthSum = stream.mapToInt(str -> str.length()).sum();
System.out.println(lengthSum);

マーカーにある上記のコードでは、i. 文字列を長さにマップし、ii. それを現在の累積和に加算しています。これは明らかに 2 段階の操作であり、 reduce() 関数を使用してこれら 2 つのステップを 1 つにまとめるほうがはるかに便利です。上記の目的で map() と sum() を組み合わせて使用したい場合は、それも可能です。

reduce()は値を生成するのが得意ですが、コレクションやその他の複雑なオブジェクトを生成したい場合、どうすればいいのでしょうか?最終兵器collect()はどこからともなく現れました!

最終兵器コレクト()

インターフェイス内の関数が見つからない場合は、十中八九collect()メソッドを介して達成することができると言っても過言ではありません。collect()は、インターフェイスメソッドの中で最も柔軟性があり、Javaの関数型プログラミングの真の入門と見なされることを学びます。最初にウォーミングアップのいくつかの小さな例を見てください:

// コンテナまたはマップにストリームする
Stream<String> stream = Stream.of("I", "love", "you", "too");
List<String> list = stream.collect(Collectors.toList()); // (1)
// Set<String> set = stream.collect(Collectors.toSet()); // (2)
// Map<String, Integer> map = stream.collect(Collectors.toMap(Function.identity(), String::length)); // (3)

上記のコードでは、それぞれをどのように変換するかを示しています。コードのセマンティクスは明確ですが、まだいくつかの疑問があります:

  1. Function.identity()何のために?
  2. String::lengthどういう意味ですか?
  3. それは何ですか?
インターフェースの静的メソッドとデフォルトメソッド

では、Function.identity()とはどういう意味でしょうか?これには2つの説明が必要です:

  1. Java 8 では、インターフェイス内の具体的なメソッドを使用できます。インターフェイスの具体的なメソッドには、メソッドとメソッドの2種類があります。
  2. Function.identity()は、出力が入力と同じラムダ式オブジェクトを返します。

以上の説明で疑問が深まりましたか?なぜインターフェイスに具象メソッドを持つことが可能なのか、また、identity()メソッドよりもt→tの方が直感的だと感じるのか、私に聞かないでください。Java 7以降では、明確に定義されたインターフェイスに新しい抽象メソッドを追加することは、不可能ではないにしても困難です。インターフェイスにstream()抽象メソッドを追加することを想像してみてください。メソッドは、新しく追加されたメソッドをインターフェイスに直接実装することで、この厄介な問題を解決するために使われます。メソッドが導入された今、メソッドを追加しないことで、特殊なツールクラスを避けるのはどうでしょう!

メソッドリファレンス

String::lengthのような構文形式はメソッド参照()と呼ばれ、ラムダ式の特定の形式を置き換えるために使用されます。Lambda式の要点が既存のメソッドを呼び出すことである場合、Lambda式の代わりにメソッド参照を使用することができます。メソッド参照は4つのカテゴリーに分けられます:

静的メソッドの参照 Integer::sum
オブジェクトのメソッドの参照 list::add
クラスのメソッドの参照 Integer::sum
参照コンストラクタ HashMap::new

は、後の例でメソッド参照を使用します。

コレクター

前回の面倒な内容で、Java関数型プログラミングを学ぶ意欲がすっかり失せてしまったと思いますが、残念ながら、次の内容はさらに面倒です。というのも、実装する関数自体が非常に複雑だからです。

コレクターには何が必要ですか?少なくとも2つのことが必要です:

  1. 対象となるコンテナとは?なのか、そうでないのか、それとも。
  2. 新しい要素はどのようにコンテナに追加されるのですか?List.add()でしょうか、それともMap.put()でしょうか。

もし並行して行われるのであれば、3.

上記の分析と組み合わせて、これらの3つのパラメータの単純なカプセル化ですので。たとえば、に制定したい場合は、次の2つの方法で実現することができます:

この記事は、マルチ記事出版プラットフォーム自動出版されました。

Read next

リートコード189 配列の回転

k回の右シフトを行い、O(1)個のスペースしか使用できないようにするには、最も単純な方法は、k回ループし、そのたびに変数tempに最後の位置の値を記録し、最後の添え字から添え字1の値までを前の要素の値とし、添え字0の値をtempとすることです。 これをk回実行した後、k回右シフトするループが完了します。 しかし、これではタイムアウトしてしまうので、もっと簡単な方法として、...

May 13, 2020 · 2 min read