點擊上方“ 搜雲庫技術團隊 ”關注,選擇“ 設爲星標

回覆“ 面試題 ”領 《96份:3265頁面試題》

java8的流式處理極大了簡化我們對於集合、數組等結構的操作,讓我們可以以函數式的思想去操作,本篇文章將探討java8的流式數據處理的基本使用。

一. 流式處理簡介

在我接觸到java8流式處理的時候,我的第一感覺是流式處理讓集合操作變得簡潔了許多,通常我們需要多行代碼才能完成的操作,藉助於流式處理可以在一行中實現。

比如我們希望對一個包含整數的集合中篩選出所有的偶數,並將其封裝成爲一個新的List返回,那麼在java8之前,我們需要通過如下代碼實現:

List<Integer> evens = new ArrayList<>();
for (final Integer num : nums) {
if (num % 2 == 0) {
evens.add(num);
}
}

通過java8的流式處理,我們可以將代碼簡化爲:

List<Integer> evens = nums.stream().filter(num -> num % 2 == 0).collect(Collectors.toList());

這裏列表遍歷,之所以可以簡化爲lamda表達式,是因爲jdk8以後爲lambda表達式已經內置了豐富的函數式接口,這裏使用的就是jdk自帶的Predicate 函數式接口。

jdk爲lambda表達式已經內置了豐富的函數式接口,如下表所示(僅列出部分):

將以上lambda表達式還原成匿名內部類,如下:

List<Integer> evens = nums.stream().filter(new Predicate<Integer>() {
@Override
public boolean test(Integer num) {
return num % 2 == 0;
}
}).collect(Collectors.toList());

先簡單解釋一下上面這行語句的含義,stream()操作將集合轉換成一個流,filter()執行我們自定義的篩選處理,這裏是通過lambda表達式篩選出所有偶數,最後我們通過collect()對結果進行封裝處理,並通過 Collectors.toList() 指定其封裝成爲一個List集合返回。

由上面的例子可以看出,java8的流式處理極大的簡化了對於集合的操作,實際上不光是集合,包括數組、文件等,只要是可以轉換成流,我們都可以藉助流式處理,類似於我們寫SQL語句一樣對其進行操作。java8通過內部迭代來實現對流的處理,一個流式處理可以分爲三個部分:轉換成流、中間操作、終端操作。

如下圖:

以集合爲例,

  • 一個流式處理的操作我們首先需要調用stream()函數將其轉換成流,

  • 然後再調用相應的中間操作達到我們需要對集合進行的操作,比如篩選、轉換等,

  • 最後通過終端操作對前面的結果進行封裝,返回我們需要的形式。

二. 中間操作

我們定義一個簡單的學生實體類,用於後面的例子演示:

public class Student {

/** 學號 */
private long id;

private String name;

private int age;

/** 年級 */
private int grade;

/** 專業 */
private String major;

/** 學校 */
private String school;

// 省略getter和setter
}
// 初始化
List<Student> students = new ArrayList<Student>() {
{
add(new Student(20160001, "孔明", 20, 1, "土木工程", "武漢大學"));
add(new Student(20160002, "伯約", 21, 2, "信息安全", "武漢大學"));
add(new Student(20160003, "玄德", 22, 3, "經濟管理", "武漢大學"));
add(new Student(20160004, "雲長", 21, 2, "信息安全", "武漢大學"));
add(new Student(20161001, "翼德", 21, 2, "機械與自動化", "華中科技大學"));
add(new Student(20161002, "元直", 23, 4, "土木工程", "華中科技大學"));
add(new Student(20161003, "奉孝", 23, 4, "計算機科學", "華中科技大學"));
add(new Student(20162001, "仲謀", 22, 3, "土木工程", "浙江大學"));
add(new Student(20162002, "魯肅", 23, 4, "計算機科學", "浙江大學"));
add(new Student(20163001, "丁奉", 24, 5, "土木工程", "南京大學"));
}
};

2.1 過濾

過濾,顧名思義就是按照給定的要求對集合進行篩選滿足條件的元素,java8提供的篩選操作包括:filter、distinct、limit、skip。

filter

在前面的例子中我們已經演示瞭如何使用filter,其定義爲: Stream<T> filter(Predicate<? super T> predicate)

  • filter接受一個謂詞Predicate,我們可以通過這個謂詞定義篩選條件,

  • 在介紹lambda表達式時我們介紹過Predicate是一個函數式接口,其包含一個test(T t)方法,該方法返回boolean。

現在我們希望從集合students中篩選出所有武漢大學的學生,那麼我們可以通過filter來實現,並將篩選操作作爲參數傳遞給filter:

List<Student> whuStudents = students.stream()
.filter(student -> "武漢大學".equals(student.getSchool()))
.collect(Collectors.toList());

distinct

distinct操作類似於我們在寫SQL語句時,添加的DISTINCT關鍵字,用於去重處理,distinct基於 Object.equals(Object) 實現,回到最開始的例子,假設我們希望篩選出所有不重複的偶數,那麼可以添加distinct操作:

List<Integer> evens = nums.stream()
.filter(num -> num % 2 == 0).distinct()
.collect(Collectors.toList());

limit

limit操作也類似於SQL語句中的LIMIT關鍵字,不過相對功能較弱,limit返回包含前n個元素的流,當集合大小小於n時,則返回實際長度,比如下面的例子返回前兩個專業爲土木工程專業的學生:

List<Student> civilStudents = students.stream()
.filter(student -> "土木工程".equals(student.getMajor())).limit(2)
.collect(Collectors.toList());

sorted

說到limit,不得不提及一下另外一個流操作:sorted。該操作用於對流中元素進行排序,sorted要求待比較的元素必須實現Comparable接口,如果沒有實現也不要緊,我們可以將比較器作爲參數傳遞給 sorted(Comparator<? super T> comparator) ,比如我們希望篩選出專業爲土木工程的學生,並按年齡從小到大排序,篩選出年齡最小的兩個學生,那麼可以實現爲:

List<Student> sortedCivilStudents = students.stream()
.filter(student -> "土木工程".equals(student.getMajor())).sorted((s1, s2) -> s1.getAge() - s2.getAge())
.limit(2)
.collect(Collectors.toList());

skip

skip操作與limit操作相反,如同其字面意思一樣,是跳過前n個元素,比如我們希望找出排序在2之後的土木工程專業的學生,那麼可以實現爲:

List<Student> civilStudents = students.stream()
.filter(student -> "土木工程".equals(student.getMajor()))
.skip(2)
.collect(Collectors.toList());

通過skip,就會跳過前面兩個元素,返回由後面所有元素構造的流,如果n大於滿足條件的集合的長度,則會返回一個空的集合。Java知音公衆號內回覆“面試題聚合”,送你一份面試題寶典

2.2 映射

在SQL中,藉助SELECT關鍵字後面添加需要的字段名稱,可以僅輸出我們需要的字段數據,而流式處理的映射操作也是實現這一目的,在java8的流式處理中,主要包含兩類映射操作:map和flatMap。

map

舉例說明,假設我們希望篩選出所有專業爲計算機科學的學生姓名,那麼我們可以在filter篩選的基礎之上,通過map將學生實體映射成爲學生姓名字符串,具體實現如下:

List<String> names = students.stream()
.filter(student -> "計算機科學".equals(student.getMajor()))
.map(Student::getName).collect(Collectors.toList());

除了上面這類基礎的map,java8還提供了 mapToDouble(ToDoubleFunction<? super T> mapper)mapToInt(ToIntFunction<? super T> mapper)mapToLong(ToLongFunction<? super T> mapper) ,這些映射分別返回對應類型的流,java8爲這些流設定了一些特殊的操作,比如我們希望計算專業爲計算機科學學生的年齡之和,那麼我們可以實現如下:

int totalAge = students.stream()
.filter(student -> "計算機科學".equals(student.getMajor()))
.mapToInt(Student::getAge).sum();

通過將Student按照年齡直接映射爲IntStream,我們可以直接調用提供的sum()方法來達到目的,此外使用這些數值流的好處還在於可以避免jvm裝箱操作所帶來的性能消耗。

flatMap

flatMap與map的區別在於 flatMap是將一個流中的每個值都轉成一個個流,然後再將這些流扁平化成爲一個流 。舉例說明,假設我們有一個字符串數組 String[] strs = {"java8", "is", "easy", "to", "use"}; ,我們希望輸出構成這一數組的所有非重複字符,那麼我們可能首先會想到如下實現:

List<String[]> distinctStrs = Arrays.stream(strs)
.map(str -> str.split("")) // 映射成爲Stream<String[]>
.distinct()
.collect(Collectors.toList());

在執行map操作以後,我們得到是一個包含多個字符串(構成一個字符串的字符數組)的流,此時執行distinct操作是基於在這些字符串數組之間的對比,所以達不到我們希望的目的,此時的輸出爲:

[j, a, v, a, 8]
[i, s]
[e, a, s, y]
[t, o]
[u, s, e]

distinct只有對於一個包含多個字符的流進行操作才能達到我們的目的,即對Stream 進行操作。此時flatMap就可以達到我們的目的:

List<String> distinctStrs = Arrays.stream(strs)
.map(str -> str.split("")) // 映射成爲Stream<String[]>
.flatMap(Arrays::stream) // 扁平化爲Stream<String>
.distinct()
.collect(Collectors.toList());

Arrays::stream 由lamada表達式array -> Arrays.stream(array)簡化而來

array -> Arrays.stream(array) 由如下匿名內部類的簡化而來

.flatMap(new Function<String[], Stream<? extends String>>() {
@Override
public Stream<? extends String> apply(String[] array) {
return Arrays.stream(array);
}
})

flatMap將由map映射得到的 Stream<String[]> ,轉換成由各個字符串數組映射成的流 Stream<String> ,再將這些小的流扁平化成爲一個由所有字符串構成的大流 Steam<String> ,從而能夠達到我們的目的。

與map類似,flatMap也提供了針對特定類型的映射操作:

flatMapToDouble(Function<? super T,? extends DoubleStream> mapper),

flatMapToInt(Function<? super T,? extends IntStream> mapper),

flatMapToLong(Function<? super T,? extends LongStream> mapper)。

三. 終端操作

終端操作是流式處理的最後一步,我們可以在終端操作中實現對流查找、歸約等操作。

3.1 查找

allMatch

allMatch用於檢測是否全部都滿足指定的參數行爲,如果全部滿足則返回true,例如我們希望檢測是否所有的學生都已滿18週歲,那麼可以實現爲:

boolean isAdult = students.stream().allMatch(student -> student.getAge() >= 18);

anyMatch

anyMatch則是檢測是否存在一個或多個滿足指定的參數行爲,如果滿足則返回true,例如我們希望檢測是否有來自武漢大學的學生,那麼可以實現爲:

boolean hasWhu = students.stream().anyMatch(student -> "武漢大學".equals(student.getSchool()));

noneMathch

noneMatch用於檢測是否不存在滿足指定行爲的元素,如果不存在則返回true,例如我們希望檢測是否不存在專業爲計算機科學的學生,可以實現如下:

boolean noneCs = students.stream().noneMatch(student -> "計算機科學".equals(student.getMajor()));

findFirst

findFirst用於返回滿足條件的第一個元素,比如我們希望選出專業爲土木工程的排在第一個學生,那麼可以實現如下:

Optional<Student> optStu = students.stream().filter(student -> "土木工程".equals(student.getMajor())).findFirst();

findFirst不攜帶參數,具體的查找條件可以通過filter設置,此外我們可以發現findFirst返回的是一個Optional類型,關於該類型的具體講解可以參考:

https://blog.csdn.net/fly910905/article/details/87533628。

findAny

findAny相對於findFirst的區別在於,findAny不一定返回第一個,而是返回任意一個,比如我們希望返回任意一個專業爲土木工程的學生,可以實現如下:

Optional<Student> optStu = students.stream().filter(student -> "土木工程".equals(student.getMajor())).findAny();

實際上對於順序流式處理而言,findFirst和findAny返回的結果是一樣的,至於爲什麼會這樣設計,是因爲在下一篇我們介紹的並行流式處理,當我們啓用並行流式處理的時候,查找第一個元素往往會有很多限制,如果不是特別需求,在並行流式處理中使用findAny的性能要比findFirst好。

3.2 歸約

前面的例子中我們大部分都是通過 collect(Collectors.toList()) 對數據封裝返回,如我的目標不是返回一個新的集合,而是希望對經過參數化操作後的集合進行進一步的運算,那麼我們可用對集合實施歸約操作。java8的流式處理提供了reduce方法來達到這一目的。

前面我們通過mapToInt將 Stream<Student> 映射成爲IntStream,並通過IntStream的sum方法求得所有學生的年齡之和,實際上我們通過歸約操作,也可以達到這一目的,實現如下:

// 前面例子中的方法
int totalAge = students.stream()
.filter(student -> "計算機科學".equals(student.getMajor()))
.mapToInt(Student::getAge).sum();
// 歸約操作
int totalAge = students.stream()
.filter(student -> "計算機科學".equals(student.getMajor()))
.map(Student::getAge)
.reduce(0, (a, b) -> a + b);

// 進一步簡化
int totalAge2 = students.stream()
.filter(student -> "計算機科學".equals(student.getMajor()))
.map(Student::getAge)
.reduce(0, Integer::sum);

// 採用無初始值的重載版本,需要注意返回Optional
Optional<Integer> totalAge = students.stream()
.filter(student -> "計算機科學".equals(student.getMajor()))
.map(Student::getAge)
.reduce(Integer::sum); // 去掉初始值

3.3 收集

前面利用 collect(Collectors.toList()) 是一個簡單的收集操作,是對處理結果的封裝,對應的還有toSet、toMap,以滿足我們對於結果組織的需求。這些方法均來自於 java.util.stream.Collectors ,我們可以稱之爲收集器。

3.3.1 歸約

收集器也提供了相應的歸約操作,但是與reduce在內部實現上是有區別的,收集器更加適用於可變容器上的歸約操作,這些收集器廣義上均基於Collectors.reducing()實現。

例1:求學生的總人數

long count = students.stream().collect(Collectors.counting());

// 進一步簡化
long count = students.stream().count();

例2:求年齡的最大值和最小值

// 求最大年齡
Optional<Student> olderStudent = students.stream().collect(Collectors.maxBy((s1, s2) -> s1.getAge() - s2.getAge()));

// 進一步簡化
Optional<Student> olderStudent2 = students.stream().collect(Collectors.maxBy(Comparator.comparing(Student::getAge)));

// 求最小年齡
Optional<Student> olderStudent3 = students.stream().collect(Collectors.minBy(Comparator.comparing(Student::getAge)));

例3:求年齡總和

int totalAge4 = students.stream().collect(Collectors.summingInt(Student::getAge));

對應的還有summingLong、summingDouble。

例4:求年齡的平均值

double avgAge = students.stream().collect(Collectors.averagingInt(Student::getAge));

對應的還有averagingLong、averagingDouble。

例5:一次性得到元素個數、總和、均值、最大值、最小值

IntSummaryStatistics statistics = students.stream().collect(Collectors.summarizingInt(Student::getAge));

輸出:

IntSummaryStatistics{count=10, sum=220, min=20, average=22.000000, max=24}

對應的還有summarizingLong、summarizingDouble。

例6:字符串拼接

String names = students.stream().map(Student::getName).collect(Collectors.joining());
// 輸出:孔明伯約玄德雲長翼德元直奉孝仲謀魯肅丁奉
String names = students.stream().map(Student::getName).collect(Collectors.joining(", "));
// 輸出:孔明, 伯約, 玄德, 雲長, 翼德, 元直, 奉孝, 仲謀, 魯肅, 丁奉

3.3.2 分組

在數據庫操作中,我們可以通過GROUP BY關鍵字對查詢到的數據進行分組,java8的流式處理也爲我們提供了這樣的功能 Collectors.groupingBy 來操作集合。比如我們可以按學校對上面的學生進行分組:

Map<String, List<Student>> groups = students.stream().collect(Collectors.groupingBy(Student::getSchool));

groupingBy接收一個分類器 Function<? super T, ? extends K> classifier ,我們可以自定義分類器來實現需要的分類效果。

上面演示的是一級分組,我們還可以定義多個分類器實現 多級分組,比如我們希望在按學校分組的基礎之上再按照專業進行分組,實現如下:

Map<String, Map<String, List<Student>>> groups2 = students.stream().collect(
Collectors.groupingBy(Student::getSchool, // 一級分組,按學校
Collectors.groupingBy(Student::getMajor))); // 二級分組,按專業

實際上在groupingBy的第二個參數不是隻能傳遞groupingBy,還可以傳遞任意Collector類型,比如我們可以傳遞一個 Collector.counting ,用以統計每個組的個數:

Map<String, Long> groups = students.stream().collect(Collectors.groupingBy(Student::getSchool, Collectors.counting()));

如果我們不添加第二個參數,則編譯器會默認幫我們添加一個 Collectors.toList()

3.3.3 分區

分區可以看做是分組的一種特殊情況,在分區中key只有兩種情況:true或false,目的是將待分區集合按照條件一分爲二,java8的流式處理利用collectors.partitioningBy()方法實現分區,該方法接收一個謂詞,例如我們希望將學生分爲武大學生和非武大學生,那麼可以實現如下:

Map<Boolean, List<Student>> partition = students.stream().collect(Collectors.partitioningBy(student -> "武漢大學".equals(student.getSchool())));

分區相對分組的優勢在於,我們可以同時得到兩類結果,在一些應用場景下可以一步得到我們需要的所有結果,比如將數組分爲奇數和偶數。

以上介紹的所有收集器均實現自接口 java.util.stream.Collector ,該接口的定義如下:

public interface Collector<T, A, R> {
/**
* A function that creates and returns a new mutable result container.
*
* @return a function which returns a new, mutable result container
*/

Supplier<A> supplier();

/**
* A function that folds a value into a mutable result container.
*
* @return a function which folds a value into a mutable result container
*/

BiConsumer<A, T> accumulator();

/**
* A function that accepts two partial results and merges them. The
* combiner function may fold state from one argument into the other and
* return that, or may return a new result container.
*
* @return a function which combines two partial results into a combined
* result
*/

BinaryOperator<A> combiner();

/**
* Perform the final transformation from the intermediate accumulation type
* {@code A} to the final result type {@code R}.
*
* <p>If the characteristic {@code IDENTITY_TRANSFORM} is
* set, this function may be presumed to be an identity transform with an
* unchecked cast from {@code A} to {@code R}.
*
* @return a function which transforms the intermediate result to the final
* result
*/

Function<A, R> finisher();

/**
* Returns a {@code Set} of {@code Collector.Characteristics} indicating
* the characteristics of this Collector. This set should be immutable.
*
* @return an immutable set of collector characteristics
*/

Set<Characteristics> characteristics();

}

我們也可以實現該接口來定義自己的收集器,此處不再展開。

四. 並行流式數據處理

流式處理中的很多都適合採用 分而治之 的思想,從而在處理集合較大時,極大的提高代碼的性能,java8的設計者也看到了這一點,所以提供了 並行流式處理。上面的例子中我們都是調用 stream() 方法來啓動流式處理,java8還提供了 parallelStream() 來啓動並行流式處理, parallelStream() 本質上基於java7的Fork-Join框架實現,其默認的線程數爲宿主機的內核數。

啓動並行流式處理雖然簡單,只需要將 stream() 替換成 parallelStream() 即可,但既然是並行,就會涉及到多線程安全問題,所以在啓用之前要先確認並行是否值得(並行的效率不一定高於順序執行),另外就是要保證線程安全。此兩項無法保證,那麼並行毫無意義,畢竟結果比速度更加重要,以後有時間再來詳細分析一下並行流式數據處理的具體實現和最佳實踐。

原文:http://www.codeceo.com/article/streaming-data-processing-of-java-8.html

《第2版:互聯網大廠面試題》

最近又趕上跳槽的高峯期,好多粉絲,都問我要有沒有最新面試題,索性,我就把我看過的和我面試中的真題,及答案都整理好, 整理了 《第2版:互聯網大廠面試題》 分類  92  PDF 累計 3625頁! 我會持續更新中,馬上就出第三版,涵蓋大廠算法會更多!

第2版:題庫非常全面

包括 Java 集合、JVM、多線程、併發編程、設計模式、Spring全家桶、Java、MyBatis、ZooKeeper、Dubbo、Elasticsearch、Memcached、MongoDB、Redis、MySQL、RabbitMQ、Kafka、Linux、Netty、Tomcat、Python、HTML、CSS、Vue、React、JavaScript、Android 大數據、阿里巴巴等大廠面試題等、等技術棧!

第2版:面試題,怎麼領取?

掃碼添加, 我的個人微信 ,備註: 面試題

(一定要備註: 面試題 )否則不給通過

→  程序員 工作內推羣

 程序員技術交流羣

 3625頁PDF面試題

沒錯,加我好友,給你安排到位

相關文章