1. ストリームとは何ですか?
ストリームは Java 8 の重要な新機能です。関数型プログラミングのサポートを提供し、パイプライン操作でコレクションを操作できるようにします。ストリーム操作はデータ ソースを横断し、パイプライン操作を使用してデータを処理し、結果コレクションを生成します。このプロセスは通常、データソースに影響を与えます。
同時に、ストリームはデータ構造ではなく、配列、 Javaコンテナ、I/O チャネルなどの特定のデータ ソースの単なるビューです。Stream の各操作は新しいストリームを生成します。内部的には、値は通常のコレクション操作のようにすぐには取得されません。代わりに、値は遅延して取得され、ユーザーが実際に結果を必要とするまで実行されません。
ストリームはデータ ストリームを表し、ストリーム内のデータ要素の数は制限されている場合もあれば、無制限である場合もあります。
ストリームとコレクションの違い
-
データは保存されません。ストリームはデータ ソースに基づくオブジェクトであり、データ要素自体は格納されませんが、データ ソースの要素がパイプラインを通じて操作に渡されます。
-
関数型プログラミング。ストリーム操作はデータ ソースを変更しません。たとえば、
filter
データ ソース内のデータを削除しません。 -
遅延動作。フィルター、マップ、その他の中間操作など、ストリームの多くの操作は遅延し、操作は終了操作に達したときにのみ順次実行されます。
-
解くことができます。
limit(n)
ストリームの数が無制限の場合、や など の一部の操作は限られた時間内に完了できます。findFirst()
これらの操作は「ショートサーキット」を実装し、限られた数の要素にアクセスした後に戻ることができます。 -
純粋な消費。Iterator と同様に、ストリームの要素にアクセスできるのは 1 回だけであり、操作を戻ることはできません。ストリームの要素を最初から再確認したい場合は、申し訳ありませんが、新しいストリームを再生成する必要があります。
コレクションはデータに関するものであり、ストリームは計算に関するものです
Java 8 のストリームは、コレクション オブジェクトの機能を拡張したもので、コレクション オブジェクトに対するさまざまな非常に便利で効率的な集約操作 (集約操作) またはバルク データ操作 (バルク データ操作) の実行に重点を置いています。Stream API は、新しく登場した Lambda 式の助けを借りて、プログラミングの効率とプログラムの読みやすさを大幅に向上させます。
同時に、集計操作にはシリアル モードとパラレル モードが用意されており、コンカレント モードではマルチコア プロセッサの利点を最大限に活用し、フォーク/ジョイン並列メソッドを使用してタスクを分割し、処理プロセスを高速化できます。並列コードの作成は通常難しく、エラーが発生しやすくなりますが、Stream API を使用すると、マルチスレッド コードを 1 行も記述することなく、高パフォーマンスの並列プログラムを簡単に作成できます。したがって、Java 8 で初めて登場した java.util.stream は、関数型言語 + マルチコア時代の影響が組み合わさって生み出されたものです。
2. ストリーム操作の概要
ストリーム操作には、中間操作、終了操作、および短縮操作の 2 種類があります。
1. 中間操作
ストリームの後には、0 個以上の中間操作を続けることができます。その目的は主に、ストリームを開き、ある程度のデータ マッピング/フィルタリングを実行し、次の操作で使用するために新しいストリームを返すことです。このタイプの操作は遅延的です。つまり、このタイプのメソッドを呼び出すだけでは、実際にはストリームの走査は開始されません。
map (mapToInt, flatMap 等)、 filter、 distinct、 sorted、 peek、 limit、 skip、 parallel、 sequential、 unordered
2. 操作を終了する
ストリームは 1 つの端末操作のみを持つことができ、この操作が実行されると、ストリームは「点灯」し、操作できなくなります。したがって、これがストリーム上の最後の操作である必要があります。ターミナル操作を実行すると、実際にストリームの走査が開始され、結果または副作用が生成されます。
forEach、 forEachOrdered、 toArray、 reduce、 collect、 min、 max、 count、 anyMatch、 allMatch、 noneMatch、 findFirst、 findAny、 iterator
3. 短絡動作
-
中間操作の場合、無限ストリームを受け入れる場合、有限の新しいストリームを返すことができます。
-
ターミナル操作の場合、無限のストリームを受け入れるが、限られた時間内で結果を計算できる場合。
anyMatch、 allMatch、 noneMatch、 findFirst、 findAny、 limit
基本型ストリーム
IntStream、LongStream、DoubleStream、java は、これら 3 つの基本的な数値型に対応する Stream を提供します。
他の数値ストリームは、コンテンツがさらに増幅されることになるため、Java 8 では提供されません。従来の数値集計操作は、上記の 3 つのストリームを通じて実行できます。
数値ストリームの構築
IntStream.of(new int[]{1, 2, 3}).forEach(System.out::println);
IntStream.range(1, 3).forEach(System.out::println);
IntStream.rangeClosed(1, 3).forEach(System.out::println);
range() は、開始ノードと終了ノードの 2 つのパラメーターを渡す必要があり、順序付けされた LongStream を返します。開始ノードと終了ノード間のすべてのパラメータが 1 の間隔で含まれます。
rangeClosed の機能は range と似ています。違いは、rangeClosed には最終終了ノードが含まれるのに対し、range には含まれないことです。
3. 流れの特徴
平行度
すべてのストリーム操作はシリアルまたはパラレルで実行できます。
並列ストリームを明示的に作成しない限り、すべての Java ライブラリはシリアル ストリームを作成します。 Collection.stream()
コレクション用のシリアル ストリームとCollection.parallelStream()
コレクション用の並列ストリームを作成します。IntStream.range(int, int)
作成されるのはシリアル ストリームです。メソッドを使用してparallel()
シリアル ストリームを並列ストリームに変換したり、sequential()
メソッドを使用してストリームをシリアル ストリームに変換したりできます。
メソッドの Javadoc で、メソッドの結果が並列実行 (findAny、forEach など) で不確実であることが示されていない限り、シリアル実行と並列実行の結果は同じになるはずです。
干渉できない
ストリームは非スレッド セーフ コレクションから作成できます。ストリームのパイプラインの実行中に非同時データ ソースを変更しないでください。
次のコードはjava.util.ConcurrentModificationException
例外をスローします。
List<String> l = new ArrayList(Arrays.asList("one", "two"));
Stream<String> sl = l.stream();
sl.forEach(s -> l.add("three"));
中間操作を設定する場合、データ ソースを変更できます。同時実行性の問題 (例外のスロー、または予期しない結果) は、エンドポイント操作の実行時にのみ発生する可能性があります。たとえば、次のコードは例外をスローしません。
List l = new ArrayList(Arrays.asList("one", "two"));
Stream sl = l.stream();
l.add("three");
sl.forEach(System.out::println);
同時データ ソースの場合、このような問題は発生しません。たとえば、次のコードは正常です。
List l = new CopyOnWriteArrayList<>(Arrays.asList("one", "two"));
Stream sl = l.stream();
sl.forEach(s -> l.add("three"));
上記の例は端末操作で非同時データ ソースを変更することですが、非同時データ ソースは他のスレッドでも変更される可能性があり、同時実行の問題も発生します。
ステタスはありません
ほとんどのストリーム操作のパラメーターは関数インターフェイスであり、ラムダ式を使用して実装できます。これらはユーザーの行動を記述するために使用され、行動パラメータと呼ばれます。
これらの動作パラメータがステートフルである場合、次のコードのように、ストリーム操作の結果が未定義になる可能性があります。
List<String> l = new ArrayList(Arrays.asList("one", "two", ……));
class State {
boolean s;
}
final State state = new State();
Stream<String> sl = l.stream().map(e -> {
if (state.s)
return "OK";
else {
state.s = true;
return e;
}
});
sl.forEach(System.out::println);
上記のコードを複数回実行した場合、並列実行すると結果が異なる場合があります。これは、このラムダ式がステートフルであるためです。
副作用
パイプライン上のすべての操作が実行された後、ユーザーが必要とする結果 (ある場合) はどこにあるのでしょうか? 最初に説明することは、すべての Stream 終了操作が結果を返す必要があるわけではないということです。一部の操作は副作用 ( Side-Effects ) を使用するだけです。たとえば、Stream.forEach()
結果を出力するメソッドの使用は、サイド使用の一般的なシナリオです。効果。
副作用のある 。実際、印刷以外のシナリオでは副作用の使用を避ける必要があります。以下のコードのように、Stream.forEach() で要素を収集するのが良い選択だと思うかもしれませんが、残念ながら、Stream は並列で実行される可能性があるため、そのような使用の正確性と効率性は保証できません。副作用のほとんどの使用は、リダクション操作を使用することでより安全かつ効率的に行うことができます。
副作用のある多くの動作パラメータを副作用のない実装に変換できます。
ArrayList<String> list = Lists.newArrayList();
for (int i = 0;i<1000;i++) {
list.add(i+"");
}
ArrayList<String> list2 = Lists.newArrayList();
// 副作用代码
list.parallelStream().forEach(s -> list2.add(s));
System.out.println(list2);
上記のコードの結果は、マルチスレッド状態での ArrayList 操作でエラーが発生したことになります。同時に、list2 のサイズが指定されていない場合、リストの実行時に ArrayIndexOutOfBoundsException 添字範囲外例外が報告される可能性があります。拡張されます。
副作用なしで次のコードに変更することも、代わりに同時コレクション クラス CopyOnWriteArrayList を使用することもできます。
1 2 3 4 5 6 |
コピーArrayList<String> リスト = Lists.newArrayList(); for (int i = 0;i<1000;i++) { list.add(i+""); List<String> list2 = list.ParallelStream().collect(Collectors.toList()) ; System.out.println(list2); |
選別
一部のストリームによって返される要素は、特定の順序になっており、これを 「遭遇順序」と呼びます。この順序は、ストリームがその要素を提供する順序です。たとえば、配列の出現順序はその要素の並べ替え順序であり、リストはその反復順序です。HashSet の場合、それ自体に出現順序はありません。
ストリームがエンカウント順序であるかどうかは、主にデータ ソースとその中間操作に依存します。たとえば、データ ソースの List および Array で作成されたストリームは順序付け (順序付け) されますが、HashSet で作成されたストリームは順序付けされません。
このメソッドは、 sorted()
ストリームを遭遇順序に変換したり、unordered
ストリームを遭遇順序に変換したりできます。
このメソッドは要素を並べ替えたり分散したりするのではなく、遭遇順序のストリームを返すことに注意してください。
さらに、ストリーム内のmap
要素を別の値や型に置き換えるメソッドなどの操作は、ストリームの順序に影響を与える可能性があるため、入力要素の順序は無意味になってしまいますが、 forfilter
メソッド つまり、一部の値が破棄されるだけで、入力要素の順序は引き続き保証されます。
シリアル ストリームの場合、ストリームが順序付けされているかどうかはパフォーマンスには影響しませんが、決定性に影響します。順序付けされていないストリームの結果は、複数回実行すると異なる場合があります。
並列ストリームの場合、順序制約を削除すると、たとえばこれらの集計操作のパフォーマンスが向上する可能性がありdistinct
ますgroupingBy
。
結合性
op
次の条件を満たす場合、演算または関数は結合的です。
1 |
コピー(a 対 b) オン c == a 対 (b 対 c) |
同時ストリームの場合、操作が結合性を満たしていれば、並列で計算できます。
1 |
コピーa から b へ c から d == (a から b) から (c から d) |
たとえばmin
、max
と文字列の連結はすべて結合します。
関数オブジェクト
関数プログラミングに Stream を使用する場合、多くの場合、操作をパラメーターとしてストリーム メソッドに渡す必要があります。関数オブジェクトはメソッドまたはオブジェクトとしてのラムダ式です。
1 |
コピーList<String> strArray = Arrays.asList(stringArrays).stream().filter(x>x.contains("Tomas")).collect(Collectors.toList()); |
上の例のfilter
パラメーターはx>x.contains("Tomas")
ラムダ式です。
ストリームの作成
ストリームはさまざまな方法で作成できます。
コレクションからストリームを作成する
Java8 の Collection インターフェースは、ストリームを取得するための 2 つのメソッドを提供するように拡張されました。
- ストリーム stream() : 順次ストリームを返します。
- ストリーム ParallelStream() : 並列ストリームを返します。
1 2 |
コピーStream<Integer> stream1 = Arrays.asList(1,2,3,4).stream(); Stream<Integer> stream2 = Arrays.asList(1,2,3,4).ParallelStream(); |
java.util.stream.Stream
これは 1 つでありinterface
、さまざまなパイプライン中間操作の戻り値はその実装クラスであり、パラメーターを簡単に渡すことができます。
配列からストリームを作成する
Java8 の Arrays の静的メソッド stream() は、配列ストリームを取得できます。static Stream stream( T[] array ) : 対応する基本型の配列を処理できるストリームのオーバーロードされた形式を返します。
配列也提供了创建流的静态方法
ストリーム():
1 |
コピーArrays.stream(new int[]{1,2,3}) |
値からストリームを作成する
静的メソッド Stream.of() を使用すると、任意の数のパラメーターを受け取ることができる値を表示することでストリームを作成できます。
public static Stream of(T…values) : ストリームを返します。
Stream
静的メソッドをof()
使用してストリームを作成することもできます。
1 |
コピーStream<String> stream3 = Stream.of(new String[]{"1","2","3","4"}); |
メソッドからストリームを作成する
静的メソッド Stream.iterate() および Stream.generate() を使用して無限ストリームを作成できます。
迭代流 : public static Stream iterate(final Tシード、final UnaryOperator f)
1 2 |
コピー//初期値 1 の無限幾何学的シーケンス Stream.iterate(1, n -> n * 2); |
生成流 : public static Stream generate(Supplier s)
1 2 |
コピー//無限乱数ストリーム Stream.generate(Math::random) |
IntStream、LongStream、DoubleStream の静的メソッドを使用して有限ストリームを作成する
1 2 3 |
コピーIntStream.of(new int[]{1, 2, 3}); IntStream.range(1, 3); IntStream.rangeClosed(1, 3); |
乱数クラスのints()メソッドを使用して値の無限ストリームを作成する
1 2 |
コピーランダム ランダム = new Random(); IntStream ints = ランダム.ints(); |
ファイルからストリームを取得
BufferedReader の Lines メソッドを使用して、ファイルから行のストリームを取得します。
1 2 |
コピーBufferedReaderbufferedReader = new BufferedReader(newInputStreamReader(new FileInputStream("file.txt"))); Stream<String> 行 =bufferedReader.lines(); |
Files
类的操作路径的方法,如list
、find
、walk
等。
其他类提供的创建流
一些类也提供了创建流的方法:
BitSet数值流
1 |
COPY IntStream stream = new BitSet().stream(); |
Pattern 将字符串分隔成流
1 2 3 |
COPY Pattern pattern = compile(","); Stream<String> stringStream = pattern.splitAsStream("a,b,c,d"); stringStream.forEach(System.out::println); |
JarFile 读取jar文件流
1 |
COPY Stream<JarEntry> stream = new JarFile("").stream(); |
更底层的使用StreamSupport
,它提供了将Spliterator
转换成流的方法,目前还不了解
中间操作
流操作是惰性执行的, 中间操作会返回一个新的流对象, 当执行终点操作时才会真正进行计算,下面介绍流的中间操作,除非传入的操作函数有副作用, 函数本身不会对数据源进行任何修改。
这个Scala集合的转换操作不同,Scala集合转换操作会生成一个新的中间集合,显而易见Java的这种设计会减少中间对象的生成。
distinct 唯一
distinct
保证数据源中的重复元素在结果中只出现一次, 它使用equals()
方法判断两个元素是否相等.
1 2 |
COPY Stream<String> stream3 = Stream.of(new String[]{"1", "2", "3", "4", "1", "2", "3", "4"}); System.out.println(stream3.distinct().collect(Collectors.toList())); |
filter 过滤
filter
根据传入的断言函数对所有元素进行检查, 只有使断言函数返回真的元素才会出现在结果中.filter
不会对数据源进行修改.
1 2 3 |
COPY Stream<String> stream3 = Stream.of(new String[]{"1", "2", "3", "4", "6", "7", "8", "9"}); List<String> stringList = stream3.filter(x-> Integer.parseInt(x)%2==0).collect(Collectors.toList()); System.out.println(stringList); |
map 映射
map方法根据传入的mapper函数对元素进行一对一映射, 即数据源中的每一个元素都会在结果中被替换(映射)为mapper函数的返回值,也可以根据处理返回不同的数据类型。
1 2 3 |
COPY Stream<String> stream3 = Stream.of(new String[]{"1", "2", "3", "4", "6", "7", "8", "9"}); List<Integer> integerList = stream3.map(x -> Integer.parseInt(x)).collect(Collectors.toList()); System.out.println(integerList); |
flatmap 映射汇总
flatmap方法混合了map + flattern的功能,同时扩展flatMapToDouble、flatMapToInt、flatMapToLong提供了转换成特定流的方法。它将映射后的流的元素全部放入到一个新的流中。它的方法定义如下:
1 |
COPY <R> Stream<R> flatMap(Function<? super T,? extends Stream<? extends R>> mapper) |
flatmap适用于多对多或者一对多的映射关系,mapper函数会将每一个元素转换成一个流对象,而flatMap方法返回的一个流包含所有mapper转换后的元素。
下面举个例子来详细说明:
给定一个列表{“aaa”,”bbb”,”ddd”,”eee”,”ccc”}。需要在控制台直接输出aaabbbdddeeeccc字样采用map来做
1 2 3 4 5 6 7 8 9 10 |
COPY List<String> list = Arrays.asList("aaa", "bbb", "ddd", "eee", "ccc"); //这里采用了两次forEach循环进行输出,显然不太优雅 list.stream().map(x -> { List<Character> characterList = new ArrayList<>(); char[] chars = x.toCharArray(); for (char c : chars) { characterList.add(c); } return characterList.stream(); }).forEach(xStream -> xStream.forEach(System.out::print)); //aaabbbdddeeeccc |
采用flatMap来做
1 2 3 4 5 6 7 8 9 10 |
COPY List<String> list = Arrays.asList("aaa", "bbb", "ddd", "eee", "ccc"); //采用flatMap来做 体会一下flatMap的魅力吧 list.stream().flatMap(x -> { List<Character> characterList = new ArrayList<>(); char[] chars = x.toCharArray(); for (char c : chars) { characterList.add(c); } return characterList.stream(); }).forEach(System.out::print); //aaabbbdddeeeccc |
limit 截断
limit
方法指定数量的元素的流。对于串行流,这个方法是有效的,这是因为它只需返回前n个元素即可,但是对于有序的并行流,它可能花费相对较长的时间,如果你不在意有序,可以将有序并行流转换为无序的,可以提高性能。
limit(int n)
当流中元素数大于n时丢弃超出的元素, 否则不进行处理, 达到限制流长度的目的.
1 2 3 |
COPY Stream<Integer> stream3 = Stream.of(3,5,1,4,2,6,8,7); List<Integer> integerList = stream3.limit(3).collect(Collectors.toList()); System.out.println(integerList); |
peek 观察者
生成一个包含原Stream的所有元素的新Stream,同时会提供一个消费函数(Consumer实例),新Stream每个元素被消费的时候都会执行给定的消费函数;这里所说的消费函数有点类似于钩子,每个元素被消费时都会执行这个钩子
peek方法会对数据源中所有元素进行给定操作, 但在结果中仍然是数据源中的元素. 通常我们利用操作的副作用, 修改其它数据或进行输入输出.
peek接收一个没有返回值的λ表达式,可以做一些输出,外部处理等。map接收一个有返回值的λ表达式,之后Stream的泛型类型将转换为map参数λ表达式返回的类型
1 2 3 |
COPY Stream<Integer> stream3 = Stream.of(1,2,3,4,5,6,7,8,9); List<Integer> integerList = stream3.peek(x-> System.out.println(x)).collect(Collectors.toList()); System.out.println(integerList); |
sorted 排序
sorted()将流中的元素按照自然排序方式进行排序,如果元素没有实现Comparable,则终点操作执行时会抛出java.lang.ClassCastException异常。
sorted(Comparator<? super T> comparator)可以指定排序的方式。
对于有序流,排序是稳定的。对于非有序流,不保证排序稳定。
sorted
方法用于对数据源进行排序:
1 2 3 |
COPY Stream<Integer> stream3 = Stream.of(4, 5, 2, 6, 9, 0, 1, 3, 6, 8); List<Integer> integerList = stream3.sorted((x, y) -> x - y).collect(Collectors.toList()); System.out.println(integerList); |
使用
java.util.Comparator
是更方便的方法, 默认进行升序排序:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
COPY class Item { public Item(int value) { this.value = value; } private int value; public int getValue() { return value; } public void setValue(int value) { this.value = value; } } Stream<Item> stream3 = Stream.of(new Item(4), new Item(3), new Item(6), new Item(9)); List<Item> itemList = stream3.sorted(Comparator.comparingInt(Item::getValue)).collect(Collectors.toList()); itemList.forEach(x -> System.out.print(x.getValue()+",")); |
使用
reversed()
方法进行降序排序:
1 2 3 |
COPY Stream<Item> stream3 = Stream.of(new Item(4), new Item(3), new Item(6), new Item(9)); List<Item> itemList = stream3.sorted(Comparator.comparingInt(Item::getValue).reversed()).collect(Collectors.toList()); itemList.forEach(x -> System.out.print(x.getValue()+",")); |
skip 跳过
skip(int)
返回丢弃了前n个元素的流. 如果流中的元素小于或者等于n,则返回空的流
1 2 3 |
COPY Stream<Integer> stream3 = Stream.of(3,5,1,4,2,6,8,7); List<Integer> integerList = stream3.skip(3).collect(Collectors.toList()); System.out.println(integerList); |
终点操作
match 断言
1 2 3 |
COPY public boolean allMatch(Predicate<? super T> predicate) public boolean anyMatch(Predicate<? super T> predicate) public boolean noneMatch(Predicate<? super T> predicate) |
这一组方法用来检查流中的元素是否满足断言。
-
allMatch
只有在所有的元素都满足断言时才返回true,否则flase,流为空时总是返回true -
anyMatch
只有在任意一个元素满足断言时就返回true,否则flase, -
noneMatch
只有在所有的元素都不满足断言时才返回true,否则flase,
1 2 3 4 5 6 |
COPY System.out.println(Stream.of(1,2,3,4,5).allMatch( i -> i > 0)); //true System.out.println(Stream.of(1,2,3,4,5).anyMatch( i -> i > 0)); //true System.out.println(Stream.of(1,2,3,4,5).noneMatch( i -> i > 0)); //false System.out.println(Stream.<Integer>empty().allMatch( i -> i > 0)); //true System.out.println(Stream.<Integer>empty().anyMatch( i -> i > 0)); //false System.out.println(Stream.<Integer>empty().noneMatch( i -> i > 0)); //true |
count 计数
count方法返回流中的元素的数量。
1 2 |
COPY String[] arr = new String[]{"a","b","c","d"}; long count = Arrays.stream(arr).count(); |
你也可以手动来实现它
1 2 |
COPY String[] arr = new String[]{"a","b","c","d"}; long count = Arrays.stream(arr).mapToLong(x->1L).sum(); |
collect 收集
collect(Collector c) 将流转换为其他形式。接收一个 Collector接口的实现,用于给Stream中元素做汇总的方法。辅助类Collectors提供了很多的collector收集器,可以满足我们日常的需求,你也可以创建新的collector实现特定的需求。它是一个值得关注的类,你需要熟悉这些特定的收集器,如聚合类averagingInt、最大最小值maxBy minBy、计数counting、分组groupingBy、字符串连接joining、分区partitioningBy、汇总summarizingInt、化简reducing、转换toXXX等。
收集器
Collectors里常用搜集器介绍:
方法 | 返回类型 | 作用 |
---|---|---|
toList() | List | 把流中元素收集到List |
List result = list.stream().collect(Collectors.toList()); | ||
toSet() | Set | 把流中元素收集到Set |
Set result = list.stream().collect(Collectors.toSet()); | ||
toCollection() | Collection | 把流中元素收集到集合 |
Collection result = lsit.stream().collect(Collectors.toCollection(ArrayListL::new)); | ||
counting() | Long | 计算流中元素的个数 |
long count = lsit.stream().collect(Collectors.counting()); | ||
summingInt() | Integer | 对流中元素的整数属性求和 |
int total = lsit.stream().collect(Collectors.counting()); | ||
averagingInt | Double | 计算元素Integer属性的均值 |
double avg = lsit.stream().collect(Collectors.averagingInt(Student::getAge)); | ||
summarizingInt | IntSummaryStatistics | 收集元素Integer属性的统计值 |
IntSummaryStatistics result = list.stream().collect(Collectors.summarizingInt(Student::getAge)); | ||
**joining ** | Stream | 连接流中的每个字符串 |
String str = list.stream().map(Student::getName).collect(Collectors.joining()); | ||
**maxBy ** | Optional | 根据比较器选择最大值 |
Opetional max = list.stream().collect(Collectors.maxBy(comparingInt(Student::getAge))) | ||
**minBy ** | Optional | 根据比较器选择最小值 |
Optional min= list.stream().collect(Collectors.minBy(comparingInt(Student::getAge))); | ||
**reducing ** | 规约产生的类型 | 从一个作为累加器的初始值开始,利用BinaryOperator与流中元素逐个结合,从而归约成单个值 |
int total = list.stream().collect(Collectors.reducing(0, Student::getAge, Integer::sum)); | ||
collectingAndThen | 转换函数返回的类型 | 包裹另一个收集器,对其结果转换 |
int how = list.stream().collect(Collectors.collectingAndThen(Collectors.toList(), List::size)); | ||
groupingBy | Map<K, List> | 根据某属性值对流分组,属性为K,结果为V |
Map<Integer, List> map = list.stream().collect(Collectors.groupingBy(Student::getStatus)); | ||
partitioningBy | Map<Boolean, List> | 根据true或false进行分区 |
Map<Boolean, List> map = list.stream().collect(Collectors.partitioningBy(Student::getPass)); |
示例
collect
是使用最广泛的终点操作, 也上文中多次出现:
1 2 3 |
COPY List<String> list = Stream.of("a","b","c","b") .distinct() .collect(Collectors.toList()) |
toList()
将流转换为List
实例, 是最常见的用法, java.util.Collectors
类中还有求和, 计算均值, 取最值, 字符串连接等多种收集方法。
find 返回
findAny()
返回任意一个元素,如果流为空,返回空的Optional,对于并行流来说,它只需要返回任意一个元素即可,所以性能可能要好于findFirst(),但是有可能多次执行的时候返回的结果不一样。
findFirst()
返回第一个元素,如果流为空,返回空的Optional。
forEach 遍历
forEach遍历流的每一个元素,执行指定的action。它是一个终点操作,和peek方法不同。这个方法不担保按照流的encounter order顺序执行,如果对于有序流按照它的encounter order顺序执行,你可以使用forEachOrdered方法。
forEach
方法对流中所有元素执行给定操作, 没有返回值.
1 |
COPY Stream.of(1,2,3,4,5).forEach(System.out::println); |
嵌套遍历(不推荐)
如果要对两个集合进行遍历操作,可以将流嵌套,但是这种遍历的性能跟跟foreach嵌套一样,而且不能进行更复杂的操作,不推荐。
1 2 3 4 5 6 7 |
COPY ArrayList<String> list = Lists.newArrayList("1", "2"); ArrayList<String> list2 = Lists.newArrayList("一", "二"); list.stream().forEach(str1->{ list2.stream().forEach(str2->{ System.out.println(str1+str2); }); }); |
max、min 最大最小值
max返回流中的最大值,
min返回流中的最小值。
1 2 3 4 5 6 7 |
COPY ArrayList<Integer> list = Lists.newArrayList(3,5,2,1); Integer max = list.stream().max(new Comparator<Integer>() { @Override public int compare(Integer o1, Integer o2) { return o1 - o2; } }).get(); |
concat 组合
concat(Stream a, Stream b)用来连接类型一样的两个流。
1 2 3 |
COPY List<Integer> list1 = Arrays.asList(1,2,3); List<Integer> list2 = Arrays.asList(4,3,2); Stream.concat(list1.stream(),list2.stream()).forEach(System.out::print); |
toXXX 转换
toArray方法将一个流转换成数组,而如果想转换成其它集合类型,西需要调用collect方法,利用Collectors.toXXX方法进行转换。
toArray()
将流中的元素放入到一个数组中,默认为Object数组
他还有一个重载方法可以返回指定类型的数组
1 2 |
COPY Object[] objects = Stream.of(1, 2, 3, 4, 5).toArray(); Integer[] integers = Stream.of(1, 2, 3, 4, 5).toArray(Integer[]::new); |
reduce 归约
reduce是常用的一个方法,事实上很多操作都是基于它实现的。
方法重载
它有几个重载方法:
方法 | 描述 |
---|---|
reduce(BinaryOperator b) | 可以将流中元素反复结合起来,得到一个值,返回 Optional |
reduce(T iden, BinaryOperator b) | 可以将流中元素反复结合起来,得到一个值,返回 T |
reduce(U identity, BiFunction a, BinaryOperator combiner) | 可以将流中元素反复结合起来,得到 |
PS: BinaryOperator 函数式接口,也即Lambada表达式
reduce思想
reduce是很重要的一种编程思想。这里重点介绍一下。reduce的作用是把stream中的元素给组合起来。至于怎么组合起来:
它需要我们首先提供一个起始种子,然后依照某种运算规则使其与stream的第一个元素发生关系产生一个新的种子,这个新的种子再紧接着与stream的第二个元素发生关系产生又一个新的种子,就这样依次递归执行,最后产生的结果就是reduce的最终产出,这就是reduce的算法最通俗的描述;
所以运用reduce我们可以做sum,min,max,average,所以这些我们称之为针对具体应用场景的reduce,这些常用的reduce,stream api已经为我们封装了对应的方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
COPY //求和 sum List<Integer> integers = Arrays.asList(1, 2, 3, 4, 5); // 没有起始值时返回为Optional类型 Optional<Integer> sumOptional = integers.stream().reduce(Integer::sum); System.out.println(sumOptional.get()); //15 // 可以给一个起始种子值 Integer sumReduce = integers.stream().reduce(0, Integer::sum); System.out.println(sumReduce); //15 //直接用sum方法 Integer sum = integers.stream().mapToInt(i -> i).sum(); System.out.println(sum); //15 |
第三个重载
前面两个方法比较简单,重点说说三个参数的reduce(U identity, BiFunction a, BinaryOperator combiner)
三个参数时是最难以理解的。 分析下它的三个参数:
- identity: 一个初始化的值;这个初始化的值其类型是泛型U,与Reduce方法返回的类型一致;注意此时Stream中元素的类型是T,与U可以不一样也可以一样,这样的话操作空间就大了;不管Stream中存储的元素是什么类型,U都可以是任何类型,如U可以是一些基本数据类型的包装类型Integer、Long等;或者是String,又或者是一些集合类型ArrayList等;后面会说到这些用法。
- accumulator: 其类型是BiFunction,输入是U与T两个类型的数据,而返回的是U类型;也就是说返回的类型与输入的第一个参数类型是一样的,而输入的第二个参数类型与Stream中元素类型是一样的
- combiner: 其类型是BinaryOperator,支持的是对U类型的对象进行操作,第三个参数combiner主要是使用在并行计算的场景下;如果Stream是非并行时,它实际上是不生效的。
因此针对这个方法的分析需要分并行与非并行两个场景。
就是因为U和T不一样,所以给了我们更多的发挥。比如设U的类型是ArrayList,那么可以将Stream中所有元素添加到ArrayList中再返回了,如下示例:
1 2 3 4 5 6 |
COPY ArrayList<String> result = Stream.of("aa", "ab", "c", "ad").reduce(new ArrayList<>(), (u, s) -> { u.add(s); return u; }, (strings, strings2) -> strings); System.out.println(result); //[aa, ab, c, ad] |
注意由于是非并行的,第三个参数实际上没有什么意义,可以指定r1或者r2为其返回值,甚至可以指定null为返回值。下面看看并行的情况:
当Stream是并行时,第三个参数就有意义了,它会将不同线程计算的结果调用combiner做汇总后返回。注意由于采用了并行计算,前两个参数与非并行时也有了差异! 看个例子:
1 2 3 4 5 |
COPY Integer reduce = Stream.of(1, 2, 3).parallel().reduce( 4, (integer, integer2) -> integer + integer2, (integer, integer2) -> integer + integer2); System.out.println(reduce); //18 |
输出:18
omg,结果竟然是18。显然串行的话结果是10;这个不太好理解,但是我下面写一个等价的方式,可以帮助很好的理解这个结果:
1 2 |
COPY Optional<Integer> reduce = Stream.of(1, 2, 3).map(n -> n + 4).reduce((s1, s2) -> s1 + s2); System.out.println(reduce.get()); //18 |
这种方式有助于理解并行三个参数时的场景,实际上就是第一步使用accumulator进行转换(它的两个输入参数一个是identity, 一个是序列中的每一个元素),由N个元素得到N个结果;第二步是使用combiner对第一步的N个结果做汇总。
reduce能干什么
好了,三个参数的reduce先介绍到这。下面继续看看reduce能为我们做什么?
1 2 3 4 5 6 7 8 |
COPY //构造字符串流 List<String> strs = Arrays.asList("H", "E", "L", "L", "O"); // reduce String concatReduce = strs.stream().reduce("", String::concat); System.out.println(concatReduce); //HELLO Stream<Integer> integerStream = Stream.of(1, 2, 3, 4, 5); Integer minReduce = integerStream.reduce(Integer.MAX_VALUE, Integer::min); System.out.println(minReduce); //1 |
并发问题
除非显式地创建并行流, 否则默认创建的都是串行流.Collection.stream()
为集合创建串行流,而Collection.parallelStream()
创建并行流.
stream.parallel()
方法可以将串行流转换成并行流,stream.sequential()
方法将流转换成串行流.
1 2 |
COPY Stream<Integer> stream3 = Stream.of(1,2,3,4,5,6,7,8,9); stream3.forEach(x-> System.out.print(x+",")); |
输出
1,2,3,4,5,6,7,8,9,
流可以在非线程安全的集合上创建, 流操作不应该对非线程安全的数据源产生任何副作用, 否则将发生java.util.ConcurrentModificationException
异常.
1 2 |
COPY List<String> list = new ArrayList(Arrays.asList("x", "y")); list.stream().forEach(x-> list.add("z")); |
输出
1 2 3 4 |
COPY Exception in thread "main" java.util.ConcurrentModificationException at java.util.ArrayList$ArrayListSpliterator.forEachRemaining(ArrayList.java:1388) at java.util.stream.ReferencePipeline$Head.forEach(ReferencePipeline.java:580) at com.test.lambda.LambdaTest.main(LambdaTest.java:15) |
对于线程安全的容器不会存在这个问题:
1 2 3 4 5 |
COPY List<String> list = new CopyOnWriteArrayList(Arrays.asList("x", "y")); list.stream().forEach(x->{ list.add("z"); System.out.println(list); }); |
输出
[x, y, z]
[x, y, z, z]
当然作者建议Stream操作不要对数据源进行任何修改. 当然, 修改其它数据或者输入输出是允许的:
1 2 3 4 5 |
COPY Set<String> set = new HashSet<String>(); List<String> list = new CopyOnWriteArrayList(Arrays.asList("x", "y")); list.stream().forEach(x->{ set.add(x); }); |
理想的管道操作应该是无状态且与访问顺序无关的. 无状态是指操作的结果只与输入有关, 下面即是一个有状态的操作示例:
1 2 3 4 5 6 7 8 9 10 |
COPY State state = getState(); List<String> list = new ArrayList(Arrays.asList("a", "b")); list = list.stream().map(s -> { if (state.isReady()) { return s; } else { return null; } }); |
无状态的操作保证无论系统状态如何管道的行为不变, 与顺序无关则有利于进行并行计算.
函数式接口
函数式接口会将签名匹配的函数对象(lambda表达式或方法)视作接口的实现。
1 2 3 4 5 |
COPY @FunctionalInterface interface Greeter { void hello(String message); } |
函数式接口中有且只有一个非抽象方法。
1 |
COPY Greeter greeter = message -> System.out.println("Hello " + message); |
这在 Java 8 之前通常使用匿名内部类实现的:
1 2 3 4 5 6 |
COPY Greeter greeter = new Greeter() { @Override public void hello(String message) { System.out.println("Hello " + message); } }; |
Java 8 将已有的一些接口实现为函数式接口:
- java.lang.Runnable
- java.util.concurrent.Callable
- java.util.Comparator
- java.lang.reflect.InvocationHandler
- java.io.FileFilter
- java.nio.file.PathMatcher
java.util.function
中定义了一些常用的函数式接口:
- Consumer: 接受参数无返回
Consumer<T>
->void accept(T t)
;BiConsumer<T,U>
->void accept(T t, U u);
DoubleConsumer
->void accept(double value);
- Supplier: 不接受参数有返回
Supplier<T>
->T get();
DoubleSupplier
->double getAsDouble();
- Function: 接受参数并返回
Function<T, R>
->R apply(T t);
BiFunction<T, U, R>
->R apply(T t, U u);
DoubleFunction<R>
->R apply(double value);
DoubleToIntFunction
->int applyAsInt(double value);
BinaryOperator<T>
extendsBiFunction<T,T,T>
- Predicate: 接受参数返回boolean
Predicate<T>
->boolean test(T t);
BiPredicate<T, U>
->boolean test(T t, U u);
DoublePredicate
->boolean test(double value);
默认构造器可以作为supplier: Supplier<Item> supplier = Item::new;