流的基本概念以及常見應用

2022-06-16 12:03:42

流的基本概念

流是一種指定的計算檢視。流遵循「做什麼而非怎麼做」的原則,它比迴圈更易於閱讀。可以讓你以一種宣告的方式處理資料。

例如程式碼:有一個變數名為 words,它是一個集合,裡面一本書中所有的單詞,需要找出單詞長度大於12的單詞數量。

迴圈

long count = 0;

for (String w : words) {
	if (w.length > 12) count++;
}

let count = words.stream()
	.filter(w -> w.length > 12)
	.count();

典型流程

由上述流範例程式碼可知,流的典型流程:

  1. 建立一個流:words.stream()
  2. 指定將初始化流轉換為其它流的中間操作(可能包含多個步驟):.filter(w -> w.length > 12)
  3. 應用終止操作,從而產生結果。這個操作會強制執行之前的惰性操作。從此之後,這個流就在也不能使用了:.count();

流與集合的區別

  1. 流並不儲存元素,這些元素可能儲存在底層的集合中,或者是按需生成的。
  2. 流的操作不會修改其資料來源 例如,filter() 方法不會從新的流中移除元素,而是會生成一個新的流,其中不包含被過濾掉的元素。
  3. 流的操作是儘可能惰性執行的這意味著直至需要其結果時,操作才會執行。

流的常見應用

建立流

流有很多建立方式,舉幾個例子:

建立任意數量的流

Stream<String> word = Stream. of("1231254135".split("1"));

從陣列的指定位置建立流

Stream<String> song = Array.stream(words, 1, 3);

建立空流

Stream<String> silence = Stream.emty();

Function

Supplier<T>

Stream<String> echos = Stream.generate(() ->"Echo");
Stream<Double> randoms = Stream.generate(Math::random);

UnaryOperation<T>

Stream<BigInteger> intergers = Stream.iterate(BigInteger.ZERO, n -> n.add(BigInteger.ONE));

上述程式碼使用iterate方法建立了一個無限序列(0 1 2 3 4 5...)。第一個引數是種子 BigInteger.ZERO,第二個元素是 f(seed),即1,下一個元素是f(f(seed)),依次類推。

Pattern

正則分割產生流。

Stream<String> words = Pattern.compile("\\PL+") .splitAsStream(contents);

操作流

常用的幾個操作流的方法

filter

過濾流中的元素

Stream<String> longWords = words.stream().filter(w -> w.length > 12);

map

轉換流中元素

// 流中所有的單詞轉換為小寫
Stream<String> 1 owe caseWords = words.stream().map(String::toLowerCase);
// 流中所有的單詞,通過擷取字串,轉換為首字母
Stream<Strirng> firstLette = wordds.stream().map(s -> s.substring(O, 1));

flatMap

通過傳入引數(Function<? super T , ? extends R> mapper),將流中所有的元素產生的結果連線在一起產生一個新的流。

例如:letters方法,將單詞轉為字母流返回。letters("boat")的返回值是:流["b", "o", "a", "t"]。

public static Stream<String> letters(String s) { 
	List<String> result = new ArrayList<>();
	for (int i = 0; i < s.length); i++)
		result.add(s.substring(i, i +1));
	}
	return result.stream();
}

使用words單詞流呼叫letters方法,將會返回一個包含流的流Stream<Stream<String>>

Stream<Stream<String>> result = words.stream().map(w -> letters(w));

上面程式碼的結果並不是我們想要的,我們想要的是單詞流轉為字母流,而不是一個流中還包含另一個流。

這時就需要使用到 flatMap 方法,此方法會攤平流中包含的字母流。將流:[["y", 「o」, "u", "r"], ["b", "o", "a", "t"]] 攤平為 ["y", 「o」, "u", "r","b", "o", "a", "t"]。

Stream<String> result = words.stream().flatMap(w -> letters(w));

limit(n)

丟棄第 n 個位置之後的元素

// 截止到第 100 個元素,建立流
Stream<Double> randoms = Stream.generate(Math::random).limit(100)

skip(n)

與 limit 相反,丟棄第 n 個位置之前的元素

// 跳過第1個元素,建立流
Stream<String> words = Stream.of(content.split("\\PL+")).skip(1)

concat

連線流

Stream<String> word = Stream.concat(lettes("Hello"), letters("World"))

distinct

去重

Stream<String> uniqueWords = Stream.of("a", "a", "b").distinct()

sort

排序

// 按單詞長度,從短到長升序
Stream<String> longesFirst = words.stream().sorted(Comparator.comparing(String::length))

peek

在每次存取一個元素時,都會呼叫peek方法中的函數,對於偵錯來說非常方便。

Object[] powers = Stream.iterate(1.O, p -> p * 2)
	.peek(e -> System.out.println("Fetching " + e))
	.limit(20).toArray();

簡單約簡/終止操作

約簡是一種終結操作( terminal operation ),它們會將流約簡為可以在程式中使用的非流值。

count

返回流中元素的數量

max

返回流中最大的元素

min

返回流中最小的元素

findFirst

找到流中的第一個元素

findAny

找到流中的任意一個元素

anyMatch

根據指定引數(匹配條件),判斷流中是否含有元素符合

allMatch

根據指定引數(匹配條件),判斷流中是否所有元素符合

noneMatch

根據指定引數(匹配條件),判斷流中是否所有元素都不符合

收集結果

forEach

此方法會將傳入的函數,應用於每個元素

stream.forEach(System.out::println);

toArray

返回 Object[] 陣列

String[] result = stream.toArray(String[]::new)

collect

將流中的元素收集到另一個目標

stream.collect(Collectors.tolist());

控制獲得結果集的型別

TreeSet<String> result = stream.collect(Collectors.toCollection(TreeSet::new));

收集流中的字串

String result = stream.collect(Collectors.joining())

元素間加入分隔符收集結果

String result = stream.collect(Collectors.joining("、"))

將其它型別物件,轉為字串收集

String result = stream.map(Object::toString).collect(Collectors.joining("、"))

如果要將流的結果約簡為總和、平均值、最大值或最小值,可以使用summarizing(Int|Long|Double)方法中的某一個。

IntSummaryStatistics summary = stream.collect(Collectors.summarizingInt(String::length));
double averageWordLength = summary.getAverage();
double maxWordLength = summary.getMax();

收集至Map

使用 Collectors.toMap,可以將想要的元素收集至 Map 中

Map<Integer, String> idToName = people.collect(Collectors.toMap(Person::getId, Person::getName));
Map<Integer, Person> idToPerson = people.collect(Collectors.toMap(Person::getId, Function.identity()));

一個key,多個value

通過第三個引數,傳入的函數,控制當一個key,存在多種value的情況。

Stream<Locale> locales = Stream.of(Locale.getAvailableLocales());

Map<String, String> languageNames = locales.collect(

Collectors.toMap(

Locale::getDisplayLanguage,

l -> l.getDisplayLanguage(l),

(existingValue, newValue) -> existingValue))

控制指定收集 TreeMap 型別資料

Map<Integer, Person> idToPerson = people.collect(

Collectors.toMap(

Person::getId,

Function.identity(),

(existingValue, newValue) -> { throw new IllegalStateException(); },

TreeMap::new));

對於每一個 toMap 方法,都有一個等價的可以產生並行對映表的 toConcurrentMap方法。單個並行對映表可以用於並行集合處理。當使用並行流時,共用的對映表比合並對映表要更高效。 注意,元素不再是按照流中的順序收集的,但是通常這不會有什麼問題

群組與分割區

將需要相同值的元素,分成一組。可以使用 groupingBy 方法。

Stream<Locale> locales = Stream.of(Locale.getAvailableLocales());

Map<String, List<Locale>> countryToLocales = locales.collect(

Collectors.groupingBy(Locale::getCountry));

當要分組的 key 為 boolean 型別時,使用 partitioningBy 更加高效。

Map<Boolean, List<Locale>> englishAndOtherLocales = locales.collect(

Collectors.partitioningBy(l -> l.getLanguage().equals("en")));

List<Locale> englishLocales = englishAndOtherLocales.get(true);

如果呼叫 groupingByConcurrent 方法,就會在使用並行流時獲得一個被並行組裝的並行對映表。這與 toConcurrentMap 方法完全類似。

下游收集器

如果要控制分組的 value 時,需要提供一個「下游收集器(downstream collector)」。例如我們想收集的value 為 set 型別,而非列表list。

Map<String, Set<Locale>> countryToLocaleSet = locales.collect(Collectors.groupingBy(Locale::getCountry, Collectors.toSet()));

除了可以使用 toSet(),也可以使用 counting、summingInt、maxBy 等約簡方法。

mapping

此方法會將傳入的函數,應用到下游收集器的結果上。例如:還是上面的程式,我們想收集Map型別,其中key是字串,value 是 Set<String> 型別。

import java.util.stream.Collectors.*

...

Map<String, Set<String>> countryToLanguages = locales.collect(
groupingBy(Locale::getDisplayCountry,
mapping(Locale::getDisplayLanguage,
toSet())));

約簡操作

reduce 方法,支援自定義約簡函數。

List<Integer> values = . . .;

// 計算流中元素的和
Optional<Integer> sum = values.stream().reduce(Integer::sum)

上述程式碼,如果流中的元素用V表示,具體在流中就會執行 V0 + V1 + V2 + ... Vi 個元素。如果流為空,就會返回一個 Optional 裡面為空的物件。

在實踐中,建議通過 toMap(),轉為數位流,並使用其自帶的求和、最大值、最小值等方法更容易。

基本型別流

在流庫中,有針對基本資料型別使用的流型別。IntStream、LongStream、DoubleStream,用來直接儲存基本型別值,而無需使用包裝器,如果想要儲存 short、char、byte、boolean,可以使用 IntStream ,而對於float ,可以使用 DoubleStream。

轉為物件流

使用 boxed 方法

// 生成0~100範圍內的基本型別流,並轉為包裝物件流
Stream<Integer> integers = IntStream.range(0, 100).boxed();

並行流

並行流就是將一個流的內容分成多個資料塊,並用不同的執行緒分別處理每個不同資料塊的流。

預設情況下,從有序集合(陣列和列表)、範圍、生成器和迭代產生的流,或者通過呼叫Stream.sorted 產生的流,都是有序的。它們的結果是按照原來元素的順序累積的,因此是完全可預知的。如果執行相同的操作兩次,將會得到完全相同的結果。

建立並行流

parallel():產生一個與當前流中元素相的並行流

unordered():產生一個與當前流中元素相 的無序流

parallel Stream():用當前集合中的元素產生一個並行流

亂序執行

列印流中的每個元素。由於並行流使用不同執行緒處理不同資料塊,那麼執行緒的執行先後順序也變的不可知,所以列印的數位亂序。

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9);
System.out.println("-------------stream---------------");
numbers.stream().forEach(out::print);
numbers.stream().forEach(out::print);
System.out.println("");
System.out.println("----------parallelStream----------");
numbers.parallelStream().forEach(out::print);
numbers.parallelStream().forEach(out::print);
-------------stream---------------
123456789
123456789
----------parallelStream----------
657893421
643157289

注意:不要將所有的流都轉換為並行流,只有在對已經位於記憶體中的資料執行大量計算操作時,才應該使用並行流。

為了讓並行流正常工作,需要滿足大量的條件:

  • 資料應該在記憶體中 必須等到資料到達是非常低效的。
  • 流應該可以被高效地分成若干個子部分 由陣列或平衡二元樹支撐的流都可以工作得很好,但是 Stream.iterate 返回的結果不行。
  • 流操作的工作量應該具有較大的規模。如果總工作負載並不是很大,那麼搭建平行計算時所付出的代價就沒有什麼意義。
  • 流操作不應該被阻塞。

流的基本概念以及常見應用