JDK8新特性詳解(二)

我是七月呀發表於2020-12-23

Stream流的使用

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

同時stream不是一種資料結構,它只是某種資料來源的一個檢視,資料來源可以是一個陣列,Java容器或I/O channel等。在Stream中的操作每一次都會產生新的流,內部不會像普通集合操作一樣立刻獲取值,而是惰性 取值,只有等到使用者真正需要結果的時候才會執行。並且對於現在呼叫的方法,本身都是一種高層次構件,與執行緒模型無關。因此在並行使用中,開發者們無需再去操 心執行緒和鎖了。Stream內部都已經做好了

如果剛接觸流操作的話,可能會感覺不太舒服。其實理解流操作的話可以對比資料庫操作。把流的操作理解為對資料庫中 資料的查詢操作 
	集合 = 資料表
    元素 = 表中的每條資料 
    屬性 = 每條資料的列
    流API = sql查詢 

流操作詳解

Stream流介面中定義了許多對於集合的操作方法,總的來說可以分為兩大類:中間操作和終端操作。

  • 中間操作:會返回一個流,通過這種方式可以將多箇中間操作連線起來,形成一個呼叫鏈,從而轉換為另外 一個流。除非呼叫鏈後存在一個終端操作,否則中間操作對流不會進行任何結果處理。

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

1、篩選

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

1.1基於filter()實現資料過

該方法會接收一個返回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、切片

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,則獲取已知集合大小長度-跳過的長度。最終進行資料切割。

3、對映

在對集合進行操作的時候,我們經常會從某些物件中選擇性的提取某些元素的值,就像編寫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方法進行實現,接收一個物件,返回另外一個物件,並把這個內容存入當前 流中,後返回

4、匹配

在日常開發中,有時還需要判斷集合中某些元素是否匹配對應的條件,如果有的話,在進行後續的操作。在 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

5、查詢

對於集合操作,有時需要從集合中查詢中符合條件的元素,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類似,只是它無論序列流還是並行流都會返回第一個元素,這裡不做詳解

6、歸約

到現在截止,對於流的終端操作,我們返回的有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]

7、收集器

通過使用收集器,可以讓程式碼更加方便的進行簡化與重用。其內部主要核心是通過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();
        
    }

}

8、分組

在資料庫操作中,經常會通過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}}}

相關文章