Java8 Stream流

来源:https://www.cnblogs.com/yulinfeng/archive/2020/03/24/12561664.html

第三章 Stream流 關註公眾號( CoderBuff )回覆“ stream ”獲取《Java8 Stream編碼實戰》PDF完整版。 《Java8 Stream編碼實戰》的代碼全部在 "https://github.com/yu linfeng/BlogRepositories/tree/ma ...


第三章 Stream流

關註公眾號(CoderBuff)回覆“stream”獲取《Java8 Stream編碼實戰》PDF完整版。

《Java8 Stream編碼實戰》的代碼全部在https://github.com/yu-linfeng/BlogRepositories/tree/master/repositories/stream-coding,一定要配合源碼閱讀,並且不斷加以實踐,才能更好的掌握Stream。

對於初學者,必須要聲明一點的是,Java8中的Stream儘管被稱作為“流”,但它和文件流、字元流、位元組流完全沒有任何關係。Stream流使程式員得以站在更高的抽象層次上對集合進行操作[1]。也就是說Java8中新引入的Stream流是針對集合的操作。

3.1 迭代

我們在使用集合時,最常用的就是迭代。

public int calcSum(List<Integer> list) {
    int sum = 0;
    for (int i = 0; i < list.size(); i++) {
        sum += list.get(i);
    }
    return sum;
}
com.coderbuff.chapter3_stream.chapter3_1.ForDemo#calcSum

例如,我們可能會對集合中的元素累加並返回結果。這段代碼由於for迴圈的樣板代碼並不能很清晰的傳達程式員的意圖。也就是說,實際上除了方法名叫“計算總和”,程式員必須閱讀整個迴圈體才能理解。你可能覺得一眼就能理解上述代碼的意圖,但如果碰上下麵的代碼,你還能一眼理解嗎?

public Map<Long, List<Student>> useFor(List<Student> students) {
    Map<Long, List<Student>> map = new HashMap<>();
    for (Student student : students) {
        List<Student> list = map.get(student.getStudentNumber());
        if (list == null) {
            list = new ArrayList<>();
            map.put(student.getStudentNumber(), list);
        }
        list.add(student);
    }
    return map;
}

閱讀完這個迴圈體以及包含的if判斷條件,大概可以知道這是想使用“studentNumber”對“Student”對象分組。這段代碼在Stream進行重構後,將會變得非常簡潔和易讀

public Map<Long, List<Student>> useStreamByGroup(List<Student> students) {
    Map<Long, List<Student>> map = students.stream().collect(Collectors.groupingBy(Student::getStudentNumber));
    return map;
}

當第一次看到這樣的寫法時,可能會認為這樣的代碼可讀性不高,不容易測試。我相信,當你在學習掌握Stream後會重新改變對它的看法。

3.2 Stream

3.2.1 創建

要想使用Stream,首先要創建一個流,創建流最常用的方式是直接調用集合的stream方法。

/**
 * 通過集合構造流
 */
private void createByCollection() {
    List<Integer> list = new ArrayList<>();
    Stream<Integer> stream = list.stream();
}
com.coderbuff.chapter3_stream.chapter3_2.StreamCreator#createByCollection

也能通過數組構造一個流。

/**
 * 通過數組構造流
 */
private void createByArrays() {
    Integer[] intArrays = {1, 2, 3};
    Stream<Integer> stream = Stream.of(intArrays);
    Stream<Integer> stream1 = Arrays.stream(intArrays);
}
com.coderbuff.chapter3_stream.chapter3_2.StreamCreator#createByArrays
學習Stream流,掌握集合創建流就足夠了。

3.2.2 使用

對於Stream流操作共分為兩個大類:惰性求值及時求值

所謂惰性求值,指的是操作最終不會產生新的集合。及時求值,指的是操作會產生新的集合。舉以下示例加以說明:

/**
 * 通過for迴圈過濾元素返回新的集合
 * @param list 待過濾的集合
 * @return 過濾後的集合
 */
private List<Integer> filterByFor(List<Integer> list) {
    List<Integer> filterList = new ArrayList<>();

    for (Integer number : list) {
        if (number > 1) {
            filterList.add(number);
        }
    }
    return filterList;
}
com.coderbuff.chapter3_stream.chapter3_3.Example#filterByFor

通過for迴圈過濾元素返回新的集合,這裡的“過濾”表示排除不符合條件的元素。我們使用Stream流過濾並返回新的集合:

/**
 * 通過Stream流過濾元素返回新的集合
 * @param list 待過濾的集合
 * @return 新的集合
 */
private List<Integer> filterByStream(List<Integer> list) {
    return list.stream()
            .filter(number -> number > 1)
            .collect(Collectors.toList());
}
com.coderbuff.chapter3_stream.chapter3_3.Example#filterByStream

Stream操作時,先調用了filter方法傳入了一個Lambda表達式代表過濾規則,後調用了collect方法表示將流轉換為List集合。

按照常理來想,一個方法調用完後,接著又調用了一個方法,看起來好像做了兩次迴圈,把問題搞得更複雜了。但實際上,這裡的filter操作是惰性求值,它並不會返回新的集合,這就是Stream流設計精妙的地方。既能在保證可讀性的同時,也能保證性能不會受太大影響。

所以使用Stream流的理想方式就是,形成一個惰性求值的鏈,最後用一個及早求值的操作返回想要的結果。

我們不需要去記哪些方法是惰性求值,如果方法的返回值是Stream那麼它代表的就是惰性求值。如果返回另外一個值或空,那麼它代表的就是及早求值。

3.2.3 常用的Stream操作

map

map操作不好理解,它很容易讓人以為這是一個轉換為Map數據結構的操作。實際上他是將集合中的元素類型,轉換為另外一種數據類型。

例如,你想將“學生”類型的集合轉換為只有“學號”類型的集合,應該怎麼做?

/**
 * 通過for迴圈提取學生學號集合
 * @param list 學生對象集合
 * @return 學生學號集合
 */
public List<Long> fetchStudentNumbersByFor(List<Student> list) {
    List<Long> numbers = new ArrayList<>();
    for (Student student : list) {
        numbers.add(student.getStudentNumber());
    }
    return numbers;
}
com.coderbuff.chapter3_stream.chapter3_4.StreamMapDemo#fetchStudentNumbersByFor

這是只藉助JDK的“傳統”方式。如果使用Stream則可以直接通過map操作來獲取只包含學生學號的集合。

/**
 * 通過Stream map提取學生學號集合
 * @param list 學生對象集合
 * @return 學生學號集合
 */
public List<Long> fetchStudentNumbersByStreamMap(List<Student> list) {
    return list.stream()
               .map(Student::getStudentNumber)
               .collect(Collectors.toList());
}
com.coderbuff.chapter3_stream.chapter3_4.StreamMapDemo#fetchStudentNumbersByStreamMap

map傳入的是一個方法,同樣可以理解為傳入的是一個“行為”,在這裡我們傳入方法“getStudentNumber”表示將通過這個方法進行轉換分類。

“Student::getStudentNumber”叫方法引用,它是“student -> student.getStudentNumber()”的簡寫。表示直接引用已有Java類或對象的方法或構造器。在這裡我們是需要傳入“getStudentNumber”方法,在有的地方,你可能會看到這樣的代碼“Student::new”,new調用的就是構造方法,表示創建一個對象。方法引用,可以將我們的代碼變得更加緊湊簡潔。

我們再舉一個例子,將小寫的字元串集合轉換為大寫字元串集合。

/**
 * 通過Stream map操作將小寫的字元串集合轉換為大寫
 * @param list 小寫字元串集合
 * @return 大寫字元串集合
 */
public List<String> toUpperByStreamMap(List<String> list) {
    return list.stream()
               .map(String::toUpperCase)
               .collect(Collectors.toList());
}
com.coderbuff.chapter3_stream.chapter3_4.StreamMapDemo#toUpperByStreamMap

filter

filter,過濾。這裡的過濾含義是“排除不符合某個條件的元素”,也就是返回true的時候保留,返回false排除。

我們仍然以“學生”對象為例,要排除掉分數低於60分的學生。

/**
 * 通過for迴圈篩選出分數大於60分的學生集合
 * @param students 待過濾的學生集合
 * @return 分數大於60分的學生集合
 */
public List<Student> fetchPassedStudentsByFor(List<Student> students) {
    List<Student> passedStudents = new ArrayList<>();
    for (Student student : students) {
        if (student.getScore().compareTo(60.0) >= 0) {
            passedStudents.add(student);
        }
    }
    return passedStudents;
}
com.coderbuff.chapter3_stream.chapter3_4.StreamFilterDemo#fetchPassedStudentsByFor

這是我們通常的實現方式,通過for迴圈能解決“一切”問題,如果使用Stream filter一行就搞定。

/**
 * 通過Stream filter篩選出分數大於60分的學生集合
 * @param students 待過濾的學生集合
 * @return 分數大於60分的學生集合
 */
public List<Student> fetchPassedStudentsByStreamFilter(List<Student> students) {
    return students.stream()
            .filter(student -> student.getScore().compareTo(60.0) >= 0)
            .collect(Collectors.toList());
}
com.coderbuff.chapter3_stream.chapter3_4.StreamFilterDemo#fetchPassedStudentsByStreamFilter

sorted

排序,也是日常最常用的操作之一。我們常常會把數據按照修改或者創建時間的倒序、升序排列,這步操作通常會放到SQL語句中。但如果實在是遇到要對集合進行排序時,我們通常也會使用Comparator.sort靜態方法進行排序,如果是複雜的對象排序,還需要實現Comparator介面。

/**
 * 通過Collections.sort靜態方法 + Comparator匿名內部類對學生成績進行排序
 * @param students 待排序學生集合
 * @return 排好序的學生集合
 */
private List<Student> sortedByComparator(List<Student> students) {
    Collections.sort(students, new Comparator<Student>() {
        @Override
        public int compare(Student student1, Student student2) {
            return student1.getScore().compareTo(student2.getScore());
        }
    });
    return students;
}
com.coderbuff.chapter3_stream.chapter3_4.StreamSortedDemo#sortedByComparator

關於Comparator可以查看這篇文章《似懂非懂的Comparable與Comparator》。簡單來講,我們需要實現Compartor介面的compare方法,這個方法有兩個參數用於比較,返回1代表前者大於後者,返回0代表前者等於後者,返回-1代表前者小於後者。

當然我們也可以手動實現冒泡演算法對學生成績進行排序,不過這樣的代碼大多出現在課堂教學中。

/**
 * 使用冒泡排序演算法對學生成績進行排序
 * @param students 待排序學生集合
 * @return 排好序的學生集合
 */
private List<Student> sortedByFor(List<Student> students) {
    for (int i = 0; i < students.size() - 1; i++) {
        for (int j = 0; j < students.size() - 1 - i; j++) {
            if (students.get(j).getScore().compareTo(students.get(j + 1).getScore()) > 0) {
                Student temp = students.get(j);
                students.set(j, students.get(j + 1));
                students.set(j + 1, temp);
            }
        }
    }
    return students;
}
com.coderbuff.chapter3_stream.chapter3_4.StreamSortedDemo#sortedByFor

在使用Stream sorted後,你會發現代碼將變得無比簡潔。

/**
 * 通過Stream sorted對學生成績進行排序
 * @param students 待排序學生集合
 * @return 排好序的學生集合
 */
private List<Student> sortedByStreamSorted(List<Student> students) {
    return students.stream()
                   .sorted(Comparator.comparing(Student::getScore))
                   .collect(Collectors.toList());
}
com.coderbuff.chapter3_stream.chapter3_4.StreamSortedDemo#sortedByStreamSorted

簡潔的後果就是,代碼變得不那麼好讀,其實並不是代碼的可讀性降低了,而只是代碼不是按照你的習慣去寫的。而大部分人恰好只習慣墨守成規,而不願意接受新鮮事物。

上面的排序是按照從小到大排序,如果想要從大到小應該如何修改呢?

Compartor.sort方法和for迴圈調換if參數的位置即可。

return student1.getScore().compareTo(student2.getScore()); 
修改為
return student2.getScore().compareTo(student1.getScore());
if (students.get(j).getScore().compareTo(students.get(j + 1).getScore()) > 0)
修改為
if (students.get(j).getScore().compareTo(students.get(j + 1).getScore()) < 0)

這改動看起來很簡單,但如果這是一段沒有註釋並且不是你本人寫的代碼,你能一眼知道是按降序還是升序排列嗎?你還能說這是可讀性強的代碼嗎?

如果是Stream操作。

return students.stream()
               .sorted(Comparator.comparing(Student::getScore))
               .collect(Collectors.toList());
修改為
return students.stream()
               .sorted(Comparator.comparing(Student::getScore).reversed())
               .collect(Collectors.toList());

這就是聲明式編程,你只管叫它做什麼,而不像命令式編程叫它如何做。

reduce

reduce是將傳入一組值,根據計算模型輸出一個值。例如求一組值的最大值、最小值、和等等。

不過使用和讀懂reduce還是比較晦澀,如果是簡單最大值、最小值、求和計算,Stream已經為我們提供了更簡單的方法。如果是複雜的計算,可能為了代碼的可讀性和維護性還是建議用傳統的方式表達。

我們來看幾個使用reduce進行累加例子。

/**
 * Optional<T> reduce(BinaryOperator<T> accumulator);
 * 使用沒有初始值對集合中的元素進行累加
 * @param numbers 集合元素
 * @return 累加結果
 */
private Integer calcTotal(List<Integer> numbers) {
    return numbers.stream()
            .reduce((total, number) -> total + number).get();
}
com.coderbuff.chapter3_stream.chapter3_4.StreamReduceDemo#calcTotal

reduce有3個重載方法,
第一個例子調用的是Optional<T> reduce(BinaryOperator<T> accumulator);它只有BinaryOperator一個參數,這個介面是一個函數介面,代表它可以接收一個Lambda表達式,它繼承自BiFunction函數介面,在BiFunction介面中,只有一個方法:

@FunctionalInterface
public interface BiFunction<T, U, R> {
    R apply(T t, U u);
}

這個方法有兩個參數。也就是說,傳入reduce的Lambda表達式需要“實現”這個方法。如果不理解這是什麼意思,我們可以拋開Lambda表達式,從純粹傳統的介面角度去理解。

首先,Optional<T> reduce(BinaryOperator<T> accumulator);方法接收BinaryOperator類型的對象,而BinaryOperator是一個介面並且繼承自BiFunction介面,而在BiFunction中只有一個方法定義 R apply(T t, U u),也就是說我們需要實現apply方法。

其次,介面需要被實現,我們不妨傳入一個匿名內部類,並且實現apply方法。

private Integer calcTotal(List<Integer> numbers) {
    return numbers.stream()
            .reduce(new BinaryOperator<Integer>() {
                @Override
                public Integer apply(Integer integer, Integer integer2) {
                    return integer + integer2;
                }
            }).get();
}

最後,我們在將匿名內部類改寫為Lambda風格的代碼,箭頭左邊是參數,右邊是函數主體。

private Integer calcTotal(List<Integer> numbers) {
    return numbers.stream()
            .reduce((total, number) -> total + number).get();
}

至於為什麼兩個參數相加最後就是不斷累加的結果,這就是reduce的內部實現了。

接著看第二個例子:

/**
 * T reduce(T identity, BinaryOperator<T> accumulator);
 * 賦初始值為1,對集合中的元素進行累加
 * @param numbers 集合元素
 * @return 累加結果
 */
private Integer calcTotal2(List<Integer> numbers) {
    return numbers.stream()
            .reduce(1, (total, number) -> total + number);
}
com.coderbuff.chapter3_stream.chapter3_4.StreamReduceDemo#calcTotal2

第二個例子調用的是reduceT reduce(T identity, BinaryOperator<T> accumulator);重載方法,相比於第一個例子,它多了一個參數“identity”,這是進行後續計算的初始值,BinaryOperator和第一個例子一樣。

第三個例子稍微複雜一點,前面兩個例子集合中的元素都是基本類型,而現實情況是,集合中的參數往往是一個對象我們常常需要對對象中的某個欄位做累加計算,比如計算學生對象的總成績。

我們先來看for迴圈怎麼做的:

/**
 * 通過for迴圈對集合中的學生成績欄位進行累加
 * @param students 學生集合
 * @return 分數總和
 */
private Double calcTotalScoreByFor(List<Student> students) {
    double total = 0;
    for (Student student : students) {
        total += student.getScore();
    }
    return total;
}
com.coderbuff.chapter3_stream.chapter3_4.StreamReduceDemo#calcTotalScoreByFor

要按前文的說法,“這樣的代碼充斥了樣板代碼,除了方法名,代碼並不能直觀的反應程式員的意圖,程式員需要讀完整個迴圈體才能理解”,但凡事不是絕對的,如果換做reduce操作:

/**
 * <U> U reduce(U identity,
 *                  BiFunction<U, ? super T, U> accumulator,
 *                  BinaryOperator<U> combiner);
 * 集合中的元素是"學生"對象,對學生的"score"分數欄位進行累加
 * @param students 學生集合
 * @return 分數總和
 */
private Double calcTotalScoreByStreamReduce(List<Student> students) {
    return students.stream()
            .reduce(Double.valueOf(0),
                    (total, student) -> total + student.getScore(),
                    (aDouble, aDouble2) -> aDouble + aDouble2);
}
com.coderbuff.chapter3_stream.chapter3_4.StreamReduceDemo#calcTotalScoreByStreamReduce

這樣的代碼,已經不是樣板代碼的問題了,是大部分程式員即使讀十遍可能也不知道要表達什麼含義。但是為了學習Stream我們還是要硬著頭皮去理解它。

Lambda表達式不好理解,過於簡潔的語法,也代表更少的信息量,我們還是先將Lambda表達式還原成匿名內部類。

private Double calcTotalScoreByStreamReduce(List<Student> students) {
    return students.stream()
            .reduce(Double.valueOf(0), new BiFunction<Double, Student, Double>() {
                @Override
                public Double apply(Double total, Student student) {
                    return total + student.getScore();
                }
            }, new BinaryOperator<Double>() {
                @Override
                public Double apply(Double aDouble, Double aDouble2) {
                    return aDouble + aDouble2;
                }
            });
}

reduce的第三個重載方法<U> U reduce(U identity, BiFunction<U, ? super T, U> accumulator, BinaryOperator<U> combiner);一共有3個參數,與第一、二個重載方法不同的是,第一、第二個重載方法參數和返回類型都是泛型“T”,意思是入參和返回都是同一種數據類型。但在第三個例子中,入參是Student對象,返回卻是Double,顯然不能使用第一、二個重載方法。

第三個重載方法的第一個參數類型是泛型“U”,它的返回類型也是泛型“U”,所以第一個參數類型,代表了返回的數據類型,我們必須將第一個類型定義為Double例子中的入參是Double.valueOf(0)表示了累加的初始值為0,且返回值是Double類型。第二個參數可以簡單理解為“應該如何計算,累加還是累乘”的計算模型。最難理解的是第三個參數,因為前兩個參數類型看起來已經能滿足我們的需求,為什麼還有第三個參數呢?

當我在第三個參數中加上一句輸出時,發現它確實沒有用。

private Double calcTotalScoreByStreamReduce(List<Student> students) {
    return students.stream()
            .reduce(Double.valueOf(0), new BiFunction<Double, Student, Double>() {
                @Override
                public Double apply(Double total, Student student) {
                    return total + student.getScore();
                }
            }, new BinaryOperator<Double>() {
                @Override
                public Double apply(Double aDouble, Double aDouble2) {
                    System.out.println("第三個參數的作用");
                    return aDouble + aDouble2;
                }
            });
}

控制台沒有輸出“第三個參數的作用”,改變它的返回值最終結果也沒有任何改變,這的確表示它真的沒有用

第三個參數在這裡的確沒有用,這是因為我們目前所使用的Stream流是串列操作,它在並行Stream流中發揮的是多路合併的作用,在下一章會繼續介紹並行Stream流,這裡就不再多做介紹。

對於reduce操作,我的個人看法是,不建議在現實中使用。如果你有累加、求最大值、最小值的需求,Stream封裝了更簡單的方法。如果是特殊的計算,不如直接按for迴圈實現,如果一定要使用Stream對學生成績求和也不妨換一個思路。

前面提到map方法可以將集合中的元素類型轉換為另一種類型,那我們就能把學生的集合轉換為分數的集合,再調用reduce的第一個重載方法計算總和:

/**
 * 先使用map將學生集合轉換為分數的集合
 * 再使用reduce調用第一個重載方法計算總和
 * @param students 學生集合
 * @return 分數總和
 */
private Double calcTotalScoreByStreamMapReduce(List<Student> students) {
    return students.stream()
            .map(Student::getScore)
            .reduce((total, score) -> total + score).get();
}
com.coderbuff.chapter3_stream.chapter3_4.StreamReduceDemo#calcTotalScoreByStreamMapReduce

min

min方法能返回集合中的最小值。它接收一個Comparator對象,Java8對Comparator介面提供了新的靜態方法comparing,這個方法返回Comparator對象,以前我們需要手動實現compare比較,現在我們只需要調用Comparator.comparing靜態方法即可。

/**
 * 通過Stream min計算集合中的最小值
 * @param numbers 集合
 * @return 最小值
 */
private Integer minByStreamMin(List<Integer> numbers) {
    return numbers.stream()
                  .min(Comparator.comparingInt(Integer::intValue)).get();
}
com.coderbuff.chapter3_stream.chapter3_4.StreamMinDemo#minByStreamMin

Comparator.comparingInt用於比較int類型數據。因為集合中的元素是Integer類型,所以我們傳入Integer類型的iniValue方法。如果集合中是對象類型,我們直接調用Comparator.comparing即可。

/**
 * 通過Stream min計算學生集合中的最低成績
 * @param students 學生集合
 * @return 最低成績
 */
private Double minScoreByStreamMin(List<Student> students) {
    Student minScoreStudent = students.stream()
            .min(Comparator.comparing(Student::getScore)).get();
    return minScoreStudent.getScore();
}
com.coderbuff.chapter3_stream.chapter3_4.StreamMinDemo#minScoreByStreamMin

max

min的用法相同,含義相反取最大值。這裡不再舉例。

summaryStatistics

求和操作也是常用的操作,利用reduce會讓代碼晦澀難懂,特別是複雜的對象類型。

好在Streaam提供了求和計算的簡便方法——summaryStatistics,這個方法並不是Stream對象提供,而是 IntStream,可以把它當做處理基本類型的流,同理還有LongStreamDoubleStream

summaryStatistics方法也不光是只能求和,它還能求最小值、最大值。

例如我們求學生成績的平均分、總分、最高分、最低分。

/**
 * 學生類型的集合常用計算
 * @param students 學生
 */
private void calc(List<Student> students) {
    DoubleSummaryStatistics summaryStatistics = students.stream()
            .mapToDouble(Student::getScore)
            .summaryStatistics();
    System.out.println("平均分:" + summaryStatistics.getAverage());
    System.out.println("總分:" + summaryStatistics.getSum());
    System.out.println("最高分:" + summaryStatistics.getMax());
    System.out.println("最低分:" + summaryStatistics.getMin());
}
com.coderbuff.chapter3_stream.chapter3_4.StreamSummaryStatisticsDemo#calc

返回的summaryStatistics包含了我們想要的所有結果,不需要我們單獨計算。mapToDouble方法將Stream流按“成績”欄位組合成新的DoubleStream流,summaryStatistics方法返回的DoubleSummaryStatistics對象為我們提供了常用的計算。

靈活運用好summaryStatistics,一定能給你帶來更少的bug和更高效的編碼。

3.3 Collectors

前面的大部分操作都是以collect(Collectors.toList())結尾,看多了自然也大概猜得到它是將流轉換為集合對象。最大的功勞當屬Java8新提供的類——Collectors收集器。

Collectors不但有toList方法能將流轉換為集合,還包括toMap轉換為Map數據類型,還能分組

/**
 * 將學生類型的集合轉換為只包含名字的集合
 * @param students 學生集合
 * @return 學生姓名集合
 */
private List<String> translateNames(List<Student> students) {

    return students.stream()
                   .map(Student::getStudentName)
                   .collect(Collectors.toList());
}
com.coderbuff.chapter3_stream.chapter3_4.StreamCollectorsDemo#translateNames
/**
 * 將學生類型的集合轉換為Map類型,key=學號,value=學生
 * @param students 學生集合
 * @return 學生Map
 */
private Map<Long, Student> translateStudentMap(List<Student> students) {
    return students.stream()
            .collect(Collectors.toMap(Student::getStudentNumber, student -> student));
}
com.coderbuff.chapter3_stream.chapter3_4.StreamCollectorsDemo#translateStudentMap
/**
 * 按學生的學號對學生集合進行分組返回Map,key=學生學號,value=學生集合
 * @param students 學生集合
 * @return 按學號分組的Map
 */
private Map<Long, List<Student>> studentGroupByStudentNumber(List<Student> students) {
    return students.stream()
            .collect(Collectors.groupingBy(Student::getStudentNumber));
}
com.coderbuff.chapter3_stream.chapter3_4.StreamCollectorsDemo#studentGroupByStudentNumber

關註公眾號(CoderBuff)回覆“stream”搶先獲取PDF完整版。

近期教程:

《ElasticSearch6.x實戰教程》

《Redis5.x入門教程》

《Java8 編碼實戰》

這是一個能給程式員加buff的公眾號 (CoderBuff)

  1. 《Java 8函數式編程》 ↩︎


您的分享是我們最大的動力!

更多相關文章
  • 我的LeetCode刷題源碼[GitHub]:https://github.com/izhoujie/Algorithmcii LeetCode 213. 打家劫舍 II 題目 你是一個專業的小偷,計劃偷竊沿街的房屋,每間房內都藏有一定的現金。這個地方所有的房屋都圍成一圈,這意味著第一個房屋和最後一 ...
  • 我的LeetCode刷題源碼[GitHub]:https://github.com/izhoujie/Algorithmcii LeetCode 198. 打家劫舍 題目 你是一個專業的小偷,計劃偷竊沿街的房屋。每間房內都藏有一定的現金,影響你偷竊的唯一制約因素就是相鄰的房屋裝有相互連通的防盜系統, ...
  • 我的LeetCode刷題源碼[GitHub]:https://github.com/izhoujie/Algorithmcii LeetCode 面試題 17.16. 按摩師 題目 一個有名的按摩師會收到源源不斷的預約請求,每個預約都可以選擇接或不接。在每次預約服務之間要有休息時間,因此她不能接受相 ...
  • Android終端(車載,手機)等, 需要考慮進行隨意進行各類按鍵(車載方向盤按鍵,手機硬按鍵)的操作, 測試系統對按鍵事件的響應穩定性,一般測試2小時。 準備階段 一般是用adb shell input keyevent + keyCode 來模擬按鍵事件, 比如adb shell input k ...
  • 我們知道C++中非常重要的:1.全局函數、2.普通成員函數、3.靜態成員函數。 類中的成員函數構成的重載有這幾點: 1. 構造函數的重載。 2.普通成員函數的重載。 3.靜態成員函數的重載。 例子: 1 #include <stdio.h> 2 3 class Test 4 { 5 int i; 6 ...
  • 使用python下載音樂,小白也可以寫爬蟲 **簡介:使用BeautifulSoup和request模塊進行抓取和解析,最後保存音樂(註:音樂質量是普通品質的)在這裡順便給大家推薦一個資源很全的python學習免非解答.裙 :七衣衣九七七巴而五(數字的諧音)轉換下可以找到了,這裡有資深程式員分享以前 ...
  • 結果: ...
  • 1.bytes和str函數 那我接下來就簡述下他文章的意思吧: bytes格式時二進位型的文件,全為010101之類的,而str為字元串型的 bytes函數中的參數為bytes(字元串,encoding=' 括弧裡面經常填utf 8')其中encoding一定要填,str函數則為str()與其是一樣 ...
一周排行
  • 一:背景 1. 講故事 如果你常翻看FCL的源碼,你會發現這裡面有不少方法藉助了C/C++的力量讓C#更快更強悍,如下所示: [DllImport("QCall", CharSet = CharSet.Unicode)] [SecurityCritical] [SuppressUnmanagedCo ...
  • 上一篇(https://www.cnblogs.com/meowv/p/12966092.html)文章使用AutoMapper來處理對象與對象之間的映射關係,本篇主要圍繞定時任務和數據抓取相關的知識點並結合實際應用,在定時任務中迴圈處理爬蟲任務抓取數據。 開始之前可以刪掉之前測試用的幾個Hello ...
  • 首先創建實體類 1 public class MacState 2 { 3 /// <summary> 4 /// 請求狀態 5 /// </summary> 6 public string success { get; set; } 7 /// <summary> 8 /// 錯誤信息 9 /// ...
  • 0. 前言 前幾天FreeSql的作者向我推薦了FreeSql框架,想讓我幫忙寫個文章介紹一下。嗯,想不到我也能帶個貨了。哈哈,開個玩笑~看了下覺得設計的挺有意思的,所以就謝了這篇文章。 簡單介紹一下,FreeSql 是NCC組織的沙盒級項目,是一款功能強大的 ORM 組件,支持 .NET Core ...
  • 0. 前言 這是一個新的系列,名字是《ASP.NET Core 入門到實戰》。這個系列主講ASP.NET Core MVC,輔助一些前端的基礎知識(能用來實現我們需要的即可,並非主講)。同時這個系列也會在後續介紹ASP.NET Core 平臺的其它類型的項目,並帶領大家以各個類型的項目為主要架構開發 ...
  • 我寫了一個Winform測試程式,用的System.Timers.Timer,在事件里,設置label1.Text,然後,居然句柄泄漏、用戶對象泄漏! 百思不得其解,最後換成System.Windows.Forms.Timer,居然不泄漏了! 最近睡眠不足,哪怕一個很小的問題,隨便搞搞,都半夜了! ...
  • leetcode-7. 整數反轉。 給出一個 32 位的有符號整數,你需要將這個整數中每位上的數字進行反轉。 示例 1: 輸入: 123 輸出: 321 示例 2: 輸入: -123 輸出: -321 示例 3: 輸入: 120 輸出: 21 註意: 假設環境只能存儲得下 32 位的有符號整數,則其 ...
  • 1. Java 虛擬機是什麼? 1.1 虛擬機 虛擬機:虛擬的電腦,一個用來執行虛擬電腦指令的軟體。 虛擬機分為系統虛擬機和程式虛擬機。 系統虛擬機:提供一個可運行完整操作系統的軟體平臺,如 Visual Box、VMware。 程式虛擬機:專門執行單個程式的,典型代表 Java 虛擬機。Jav ...
  • 前言 - strlen 概述 無意間掃到 glibc strlen.c 中代碼, 久久不能忘懷. 在一無所知的編程生涯中又記起點點滴滴: 編程可不是兒戲 ❀, 有些難, 也有些不捨. 隨軌跡一同重溫, 曾經最熟悉的 strlen 手感吧 ~ /* Copyright (C) 1991-2020 Fr ...
  • 背景 隊列[Queue]:是一種限定僅在表頭進行刪除操作,僅在表尾進行插入操作的線性表;即先進先出(FIFO-first in first out):最先插入的元素最先出來。 本文通過編碼實現鏈式隊列類,並模擬一個有趣的應用,能夠幫助我們對鏈式隊列有更深度的理解。 基本概念 結點 每個元素,除了存儲 ...