JDK8新特性詳解-Stream流經常使用方法(二)

Stream流的使用

流操做是Java8提供一個重要新特性,它容許開發人員以聲明性方式處理集合,其核心類庫主要改進了對集合類的 API和新增Stream操做。Stream類中每個方法都對應集合上的一種操做。將真正的函數式編程引入到Java中,能 讓代碼更加簡潔,極大地簡化了集合的處理操做,提升了開發的效率和生產力。java

同時stream不是一種數據結構,它只是某種數據源的一個視圖,數據源能夠是一個數組,Java容器或I/O channel等。在Stream中的操做每一次都會產生新的流,內部不會像普通集合操做同樣馬上獲取值,而是惰性 取值,只有等到用戶真正須要結果的時候纔會執行。而且對於如今調用的方法,自己都是一種高層次構件,與線程模型無關。所以在並行使用中,開發者們無需再去操 心線程和鎖了。Stream內部都已經作好了sql

若是剛接觸流操做的話,可能會感受不太舒服。其實理解流操做的話能夠對比數據庫操做。把流的操做理解爲對數據庫中 數據的查詢操做 
	集合 = 數據表
    元素 = 表中的每條數據 
    屬性 = 每條數據的列
    流API = sql查詢 

流操做詳解

Stream流接口中定義了許多對於集合的操做方法,總的來講能夠分爲兩大類:中間操做和終端操做。數據庫

  • 中間操做:會返回一個流,經過這種方式能夠將多箇中間操做鏈接起來,造成一個調用鏈,從而轉換爲另外 一個流。除非調用鏈後存在一個終端操做,不然中間操做對流不會進行任何結果處理。編程

  • 終端操做:會返回一個具體的結果,如boolean、list、integer等。數組

一、篩選數據結構

對於集合的操做,常常性的會涉及到對於集中符合條件的數據篩選,Stream中對於數據篩選兩個常見的API: filter(過濾)、distinct(去重)app

1.1基於filter()實現數據過ide

該方法會接收一個返回boolean的函數做爲參數,終返回一個包括全部符合條件元素的流。函數式編程

案例:獲取全部年齡20歲如下的學生函數

/**
 * @author 我是七月呀
 * @date 2020/12/22
 */
public class FilterDemo {
    public static void main(String[] args) {
        
        //獲取全部年齡20歲如下的學生
        ArrayList<Student> students = new ArrayList<>();
        students.add(new Student(1,19,"張三","M",true));
        students.add(new Student(1,18,"李四","M",false));
        students.add(new Student(1,21,"王五","F",true));
        students.add(new Student(1,20,"趙六","F",false));
        students.stream().filter(student -> student.getAge()<20);
    }
}

源碼解析


此處能夠看到filter方法接收了Predicate函數式接口。

首先判斷predicate是否爲null,若是爲null,則拋出NullPointerException;構建Stream,重寫opWrapsink方法。參數flags:下一個sink的標誌位,供優化使用。參數sink:下一個sink,經過此參數將sink構形成單鏈。此時流已經構建好,可是由於begin()先執行,此時是沒法肯定流中後續會存在多少元素的,因此傳遞-1,表明沒法肯定。最後調用Pridicate中的test,進行條件判斷,將符合條件數據放入流中。

1.2基於distinct實現數據去重

/**
 * @author 我是七月呀
 * @date 2020/12/22
 */
public class DistinctDemo {
    public static void main(String[] args) {
        
        List<Integer> integers = Arrays.asList(1, 2, 3, 4, 4, 5, 5, 6, 7, 8, 2, 2, 2, 2);
        integers.stream().distinct().collect(Collectors.toList());
        
    }
}

源碼解析

根據其源碼,咱們能夠知道在distinct()內部是基於LinkedHashSet對流中數據進行去重,並終返回一個新的流。

二、切片

2.1基於limit()實現數據截取

該方法會返回一個不超過給定長度的流

案例:獲取數組的前五位

/**
 * @author 我是七月呀
 * @date 2020/12/22
 */
public class LimitDemo {

    public static void main(String[] args) {
		//獲取數組的前五位
        List<Integer> integers = Arrays.asList(1, 2, 3, 4, 4, 5, 5, 6, 7, 8, 2, 2, 2, 2);
        integers.stream().limit(5);
       
    }
}

源碼解析:


對於limit方法的實現,它會接收截取的長度,若是該值小於0,則拋出異常,不然會繼續向下調用 SliceOps.makeRef()。該方法中this表明當前流,skip表明須要跳過元素,比方說原本應該有4個元素,當跳過元素 值爲2,會跳過前面兩個元素,獲取後面兩個。maxSize表明要截取的長度

在makeRef方法中的unorderedSkipLimitSpliterator()中接收了四個參數Spliterator,skip(跳過個數)、limit(截取 個數)、sizeIfKnown(已知流大小)。若是跳過個數小於已知流大小,則判斷跳過個數是否大於0,若是大於則取截取 個數或已知流大小-跳過個數的二者小值,不然取已知流大小-跳過個數的結果,做爲跳過個數。
後對集合基於跳過個數和截取個數進行切割。

2.2基於skip()實現數據跳過

案例:從集合第三個開始截取5個數據

/**
 * @author 我是七月呀
 * @date 2020/12/22
 */
public class LimitDemo {

    public static void main(String[] args) {
        //從集合第三個開始截取5個數據
        List<Integer> integers = Arrays.asList(1, 2, 3, 4, 4, 5, 5, 6, 7, 8, 2, 2, 2, 2);
        List<Integer> collect = integers.stream().skip(3).limit(5).collect(Collectors.toList());
        collect.forEach(integer -> System.out.print(integer+" "));

    }
}
結果4 4 5 5 6

案例:先從集合中截取5個元素,而後取後3個

/**
 * @author 我是七月呀
 * @date 2020/12/22
 */
public class LimitDemo {

    public static void main(String[] args) {
        //先從集合中截取5個元素,而後取後3個
        List<Integer> integers = Arrays.asList(1, 2, 3, 4, 4, 5, 5, 6, 7, 8, 2, 2, 2, 2);
        List<Integer> collect = integers.stream().limit(5).skip(2).collect(Collectors.toList());
        collect.forEach(integer -> System.out.print(integer+" "));

    }
}
結果:3 4 4

源碼分析:

在skip方法中接收的n表明的是要跳過的元素個數,若是n小於0,拋出非法參數異常,若是n等於0,則返回當前 流。若是n小於0,纔會調用makeRef()。同時指定limit參數爲-1.

此時能夠發現limit和skip都會進入到該方法中,在肯定limit值時,若是limit<0,則獲取已知集合大小長度-跳過的長度。最終進行數據切割。

三、映射

在對集合進行操做的時候,咱們常常會從某些對象中選擇性的提取某些元素的值,就像編寫sql同樣,指定獲取表 中特定的數據列

 #指定獲取特定列 SELECT name FROM student

在Stream API中也提供了相似的方法,map()。它接收一個函數做爲方法參數,這個函數會被應用到集合中每個 元素上,並終將其映射爲一個新的元素。
案例:獲取全部學生的姓名,並造成一個新的集合

/**
 * @author 我是七月呀
 * @date 2020/12/22
 */
public class MapDemo {
    public static void main(String[] args) {

        //獲取全部學生的姓名,並造成一個新的集合
        ArrayList<Student> students = new ArrayList<>();
        students.add(new Student(1,19,"張三","M",true));
        students.add(new Student(1,18,"李四","M",false));
        students.add(new Student(1,21,"王五","F",true));
        students.add(new Student(1,20,"趙六","F",false));

        List<String> collect = students.stream().map(Student::getName).collect(Collectors.toList());
        collect.forEach(s -> System.out.print(s + " "));
    }
}
結果:張三 李四 王五 趙六

源碼解析:


內部對Function函數式接口中的apply方法進行實現,接收一個對象,返回另一個對象,並把這個內容存入當前 流中,後返回

四、匹配

在平常開發中,有時還須要判斷集合中某些元素是否匹配對應的條件,若是有的話,在進行後續的操做。在 Stream API中也提供了相關方法供咱們進行使用,如anyMatch、allMatch等。他們對應的就是&&和||運算符。

4.1基於anyMatch()判斷條件至少匹配一個元素

anyMatch()主要用於判斷流中是否至少存在一個符合條件的元素,它會返回一個boolean值,而且對於它的操做, 通常叫作短路求值

案例:判斷集合中是否有年齡小於20的學生

/**
 * @author 我是七月呀
 * @date 2020/12/22
 */
public class AnyMatchDemo {

    public static void main(String[] args) {
        //判斷集合中是否有年齡小於20的學生
        ArrayList<Student> students = new ArrayList<>();
        students.add(new Student(1,19,"張三","M",true));
        students.add(new Student(1,18,"李四","M",false));
        students.add(new Student(1,21,"王五","F",true));
        students.add(new Student(1,20,"趙六","F",false));

        if(students.stream().anyMatch(student -> student.getAge() < 20)){
            System.out.println("集合中有年齡小於20的學生");
        }else {
            System.out.println("集合中沒有年齡小於20的學生");
        }
    }
}

根據上述例子能夠看到,當流中只要有一個符合條件的元素,則會馬上停止後續的操做,當即返回一個布爾值,無需遍歷整個流。

源碼解析:

內部實現會調用makeRef(),其接收一個Predicate函數式接口,並接收一個枚舉值,該值表明當前操做執行的是 ANY。

若是test()抽象方法執行返回值==MatchKind中any的stopOnPredicateMatches,則將stop中斷置爲true,value 也爲true。並終進行返回。無需進行後續的流操做。

4.2基於allMatch()判斷條件是否匹配全部元素

allMatch()的工做原理與anyMatch()相似,可是anyMatch執行時,只要流中有一個元素符合條件就會返回true, 而allMatch會判斷流中是否全部條件都符合條件,所有符合纔會返回true

案例:判斷集合全部學生的年齡是否都小於20

/**
 * @author 我是七月呀
 * @date 2020/12/22
 */
public class AllMatchDemo {

    public static void main(String[] args) {
        //判斷集合全部學生的年齡是否都小於20
        ArrayList<Student> students = new ArrayList<>();
        students.add(new Student(1,19,"張三","M",true));
        students.add(new Student(1,18,"李四","M",false));
        students.add(new Student(1,21,"王五","F",true));
        students.add(new Student(1,20,"趙六","F",false));

        if(students.stream().allMatch(student -> student.getAge() < 20)){
            System.out.println("集合全部學生的年齡都小於20");
        }else {
            System.out.println("集合中有年齡大於20的學生");
        }
    }
}

源碼解析:與anyMatch相似,只是其枚舉參數的值爲ALL

五、查找

對於集合操做,有時須要從集合中查找中符合條件的元素,Stream中也提供了相關的API,findAny()和 findFirst(),他倆能夠與其餘流操做組合使用。findAny用於獲取流中隨機的某一個元素,findFirst用於獲取流中的 第一個元素。至於一些特別的定製化需求,則須要自行實現。

5.1基於findAny()查找元素

案例:findAny用於獲取流中隨機的某一個元素,而且利用短路在找到結果時,當即結束

/**
 * @author 我是七月呀
 * @date 2020/12/22
 */
public class FindAnyDemo {
    public static void main(String[] args) {
        //findAny用於獲取流中隨機的某一個元素,而且利用短路在找到結果時,當即結束
        ArrayList<Student> students = new ArrayList<>();
        students.add(new Student(1,19,"張三1","M",true));
        students.add(new Student(1,18,"張三2","M",false));
        students.add(new Student(1,21,"張三3","F",true));
        students.add(new Student(1,20,"張三4","F",false));
        students.add(new Student(1,20,"張三5","F",false));
        students.add(new Student(1,20,"張三6","F",false));
        Optional<Student> student1 = students.stream().filter(student -> student.getSex().equals("F")).findAny();
        System.out.println(student1.toString());
    }

}
結果:Optional[Student{id=1, age=21, name='張三3', sex='F', isPass=true}]

此時咱們將其循環100次

/**
 * @author 我是七月呀
 * @date 2020/12/22
 */
public class FindAnyDemo {
    public static void main(String[] args) {
        //findAny用於獲取流中隨機的某一個元素,而且利用短路在找到結果時,當即結束
        ArrayList<Student> students = new ArrayList<>();
        students.add(new Student(1,19,"張三1","M",true));
        students.add(new Student(1,18,"張三2","M",false));
        students.add(new Student(1,21,"張三3","F",true));
        students.add(new Student(1,20,"張三4","F",false));
        students.add(new Student(1,20,"張三5","F",false));
        students.add(new Student(1,20,"張三6","F",false));
        for (int i = 0; i < 100; i++) {
            Optional<Student> student1 = students.stream().filter(student -> student.getSex().equals("F")).findAny();
            System.out.println(student1.toString());
        }

    }

}

結果:

因爲數量較大,只截取了部分截圖,所有都是同樣的,不行的小夥伴能夠本身測試一下

這時候咱們改成串行流在執行一下

/**
 * @author 我是七月呀
 * @date 2020/12/22
 */
public class FindAnyDemo {
    public static void main(String[] args) {
        //findAny用於獲取流中隨機的某一個元素,而且利用短路在找到結果時,當即結束
        ArrayList<Student> students = new ArrayList<>();
        students.add(new Student(1,19,"張三1","M",true));
        students.add(new Student(1,18,"張三2","M",false));
        students.add(new Student(1,21,"張三3","F",true));
        students.add(new Student(1,20,"張三4","F",false));
        students.add(new Student(1,20,"張三5","F",false));
        students.add(new Student(1,20,"張三6","F",false));
        for (int i = 0; i < 100; i++) {
            Optional<Student> student1 = students.parallelStream().filter(student -> student.getSex().equals("F")).findAny();
            System.out.println(student1.toString());
        }

    }

}

結果:

如今咱們經過源碼解析來分析下這是爲何?

根據這一段源碼介紹,findAny對於同一數據源的屢次操做會返回不一樣的結果。可是,咱們如今的操做是串行的, 因此在數據較少的狀況下,通常會返回第一個結果,可是若是在並行的狀況下,那就不能確保返回的是第一個了。 這種設計主要是爲了獲取更加高效的性能。並行操做後續會作詳細介紹。

傳遞參數,指定沒必要須獲取第一個元素

在該方法中,主要用於判斷對於當前的操做執行並行仍是串行。

在該方法中的wrapAndCopyInto()內部作的會判斷流中是否存在符合條件的元素,若是有的話,則會進行返回。結 果終會封裝到Optional中的IsPresent中。

總結:當爲串行流且數據較少時,獲取的結果通常爲流中第一個元素,可是當爲並流行的時 候,則會隨機獲取。

5.2基於findFirst()查找元素

findFirst使用原理與findAny相似,只是它不管串行流仍是並行流都會返回第一個元素,這裏不作詳解

六、歸約

到如今截止,對於流的終端操做,咱們返回的有boolean、Optional和List。可是在集合操做中,咱們常常會涉及 對元素進行統計計算之類的操做,如求和、求大值、小值等,從而返回不一樣的數據結果。

6.1基於reduce()進行累積求和

案例:對集合中的元素求和

/**
 * @author 我是七月呀
 * @date 2020/12/22
 */
public class ReduceDemo {
    public static void main(String[] args) {
        List<Integer> integers = Arrays.asList(1, 2, 3, 4, 4, 5, 5, 6, 7, 8, 2, 2, 2, 2);
        Integer reduce = integers.stream().reduce(0, (integer1, integer2) -> integer1 + integer2);
        System.out.println(reduce);
    }
}
結果:53

在上述代碼中,在reduce裏的第一個參數聲明爲初始值,第二個參數接收一個lambda表達式,表明當前流中的兩 個元素,它會反覆相加每個元素,直到流被歸約成一個終結果

Integer reduce = integers.stream().reduce(0,Integer::sum);

優化成這樣也是能夠的。固然,reduce還有一個不帶初始值參數的重載方法,可是要對返回結果進行判斷,由於若是流中沒有任何元素的話,可能就沒有結果了。具體方法以下所示

List<Integer> integers = Arrays.asList(1, 2, 3, 4, 4, 5, 5, 6, 7, 8, 2, 2, 2, 2);
        Optional<Integer> reduce = integers.stream().reduce(Integer::sum);
        if(reduce.isPresent()){
            System.out.println(reduce);
        }else {
            System.out.println("數據有誤");
        }

源碼解析:兩個參數的reduce方法

在上述方法中,對於流中元素的操做,當執行第一個元素,會進入begin方法,將初始化的值給到state,state就 是後的返回結果。並執行accept方法,對state和第一個元素根據傳入的操做,對兩個值進行計算。並把終計 算結果賦給state。
當執行到流中第二個元素,直接執行accept方法,對state和第二個元素對兩個值進行計算,並把終計算結果賦 給state。後續依次類推。
能夠按照下述代碼進行理解

T result = identity; 
for (T element : this stream){  
result = accumulator.apply(result, element) 
}
return result;

源碼解析:單個參數的reduce方法

在這部分實現中,對於匿名內部類中的empty至關因而一個開關,state至關於結果。

對於流中第一個元素,首先會執行begin()將empty置爲true,state爲null。接着進入到accept(),判斷empty是否 爲true,若是爲true,則將empty置爲false,同時state置爲當前流中第一個元素,當執行到流中第二個元素時, 直接進入到accpet(),判斷empty是否爲true,此時empty爲false,則會執行apply(),對當前state和第二個元素進 行計算,並將結果賦給state。後續依次類推。
當整個流操做完以後,執行get(), 若是empty爲true,則返回一個空的Optional對象,若是爲false,則將後計算 完的state存入Optional中。

能夠按照下述代碼進行理解:

boolean flag = false; 
T result = null; 
for (T element : this stream) { 
    if (!flag) { 
        flag = true;  
        result = element; 
    }else{  
        result = accumulator.apply(result, element); 
    } 
} 
return flag ? Optional.of(result) : Optional.empty();

6.2獲取流中元素的最大值、最小值

案例:獲取集合中元素的最大值、最小值

/**
 * @author 我是七月呀
 * @date 2020/12/22
 */
public class MaxDemo {
    public static void main(String[] args) {

        List<Integer> integers = Arrays.asList(1, 2, 3, 4, 4, 5, 5, 6, 7, 8, 2, 2, 2, 2);

        /**
         * 獲取集合中的最大值
         */
        //方法一
        Optional<Integer> max1 = integers.stream().reduce(Integer::max);
        if(max1.isPresent()){
            System.out.println(max1);
        }
        //方法二
        Optional<Integer> max2 = integers.stream().max(Integer::compareTo);
        if(max2.isPresent()){
            System.out.println(max2);
        }

        /**
         * 獲取集合中的最小值
         */
        //方法一 
        Optional<Integer> min1 = integers.stream().reduce(Integer::min);
        if(min1.isPresent()){
            System.out.println(min1);
        }

        //方法二
        Optional<Integer> min2 = integers.stream().min(Integer::compareTo);
        if(min2.isPresent()){
            System.out.println(min2);
        }
    }
}

結果:

Optional[8]
Optional[8]
Optional[1]
Optional[1]

七、收集器

經過使用收集器,可讓代碼更加方便的進行簡化與重用。其內部主要核心是經過Collectors完成更加複雜的計算 轉換,從而獲取到終結果。而且Collectors內部提供了很是多的經常使用靜態方法,直接拿來就能夠了。比方說: toList。

/**
 * @author 我是七月呀
 * @date 2020/12/22
 */
public class CollectDemo {
    public static void main(String[] args) {
        ArrayList<Student> students = new ArrayList<>();
        students.add(new Student(1,19,"張三","M",true));
        students.add(new Student(1,18,"李四","M",false));
        students.add(new Student(1,21,"王五","F",true));
        students.add(new Student(1,20,"趙六","F",false));

        //經過counting()統計集合總數  方法一
        Long collect = students.stream().collect(Collectors.counting());
        System.out.println(collect);
        //結果 4

        //經過count()統計集合總數  方法二
        long count = students.stream().count();
        System.out.println(count);
        //結果 4

        //經過maxBy求最大值
        Optional<Student> collect1 = students.stream().collect(Collectors.maxBy(Comparator.comparing(Student::getAge)));
        if(collect1.isPresent()){
            System.out.println(collect1);
        }
        //結果 Optional[Student{id=1, age=21, name='王五', sex='F', isPass=true}]

        //經過max求最大值
        Optional<Student> max = students.stream().max(Comparator.comparing(Student::getAge));
        if(max.isPresent()){
            System.out.println(max);
        }
        //結果  Optional[Student{id=1, age=21, name='王五', sex='F', isPass=true}]

        //經過minBy求最小值
        Optional<Student> collect2 = students.stream().collect(Collectors.minBy(Comparator.comparing(Student::getAge)));
        if(collect2.isPresent()){
            System.out.println(collect2);
        }
        //結果  Optional[Student{id=1, age=18, name='李四', sex='M', isPass=false}]

        //經過min求最小值
        Optional<Student> min = students.stream().min(Comparator.comparing(Student::getAge));
        if(min.isPresent()){
            System.out.println(min);
        }
        //結果  Optional[Student{id=1, age=18, name='李四', sex='M', isPass=false}]

        //經過summingInt()進行數據彙總
        Integer collect3 = students.stream().collect(Collectors.summingInt(Student::getAge));
        System.out.println(collect3);
        //結果 78

        //經過averagingInt()進行平均值獲取
        Double collect4 = students.stream().collect(Collectors.averagingInt(Student::getAge));
        System.out.println(collect4);
        //結果 19.5

        //經過joining()進行數據拼接
        String collect5 = students.stream().map(Student::getName).collect(Collectors.joining());
        System.out.println(collect5);
        //結果 張三李四王五趙六
        
        //複雜結果的返回
        IntSummaryStatistics collect6 = students.stream().collect(Collectors.summarizingInt(Student::getAge));
        double average = collect6.getAverage();
        long sum = collect6.getSum();
        long count1 = collect6.getCount();
        int max1 = collect6.getMax();
        int min1 = collect6.getMin();
        
    }

}

八、分組

在數據庫操做中,常常會經過group by對查詢結果進行分組。同時在平常開發中,也常常會涉及到這一類操做, 如經過性別對學生集合進行分組。若是經過普通編碼的方式須要編寫大量代碼且可讀性很差。

對於這個問題的解決,java8也提供了簡化書寫的方式。經過 Collectors。groupingBy()便可。

//經過性別對學生進行分組
        Map<String, List<Student>> collect = students.stream().collect(Collectors.groupingBy(Student::getSex));
結果  {
    F=[Student{id=1, age=21, name='王五', sex='F', isPass=true}, Student{id=1, age=20, name='趙六', sex='F', isPass=false}],
    M=[Student{id=1, age=19, name='張三', sex='M', isPass=true}, Student{id=1, age=18, name='李四', sex='M', isPass=false}]
}

8.1多級分組

剛纔已經使用groupingBy()完成了分組操做,可是隻是經過單一的sex進行分組,那如今若是需求發生改變,還要 按照是否及格進行分組,可否實現?答案是能夠的。對於groupingBy()它提供了兩個參數的重載方法,用於完成這 種需求。

這個重載方法在接收普通函數以外,還會再接收一個Collector類型的參數,其會在內層分組(第二個參數)結果,傳 遞給外層分組(第一個參數)做爲其繼續分組的依據。

//現根據是否經過考試對學生分組,在根據性別分組     
Map<String, Map<Boolean, List<Student>>> collect1 = students.stream().collect(Collectors.groupingBy(Student::getSex, Collectors.groupingBy(Student::getPass)));
結果: {
    F={
        false=[Student{id=1, age=20, name='趙六', sex='F', isPass=false}],
        true=[Student{id=1, age=21, name='王五', sex='F', isPass=true}]
    }, 
    M={
        false=[Student{id=1, age=18, name='李四', sex='M', isPass=false}], 
        true=[Student{id=1, age=19, name='張三', sex='M', isPass=true}]}
}

8.2多級分組變形

在平常開發中,咱們頗有可能不是須要返回一個數據集合,還有可能對數據進行彙總操做,比方說對於年齡18歲 的經過的有多少人,未及格的有多少人。所以,對於二級分組收集器傳遞給外層分組收集器的能夠任意數據類型, 而不必定是它的數據集合。

//根據年齡進行分組,獲取並彙總人數
        Map<Integer, Long> collect2 = students.stream().collect(Collectors.groupingBy(Student::getAge, Collectors.counting()));
        System.out.println(collect2);
結果:{18=1, 19=1, 20=1, 21=1}
//要根據年齡與是否及格進行分組,並獲取每組中年齡的學生
        Map<Integer, Map<Boolean, Student>> collect3 = students.stream().collect(Collectors.groupingBy(Student::getAge, Collectors.groupingBy(Student::getPass,
                Collectors.collectingAndThen(Collectors.maxBy(Comparator.comparing(Student::getAge)), Optional::get))));
        System.out.println(collect3.toString());
結果:{
    18={false=Student{id=1, age=18, name='李四', sex='M', isPass=false}},
    19={true=Student{id=1, age=19, name='張三', sex='M', isPass=true}},
    20={false=Student{id=1, age=20, name='趙六', sex='F', isPass=false}}, 
    21={true=Student{id=1, age=21, name='王五', sex='F', isPass=true}}}
相關文章
相關標籤/搜索