JAVA8--Stream流

Stream流

在JAVA8中添加Stream,這個真的是一個好東西。學習使用Stream會有效提高開發效率。究竟流有什麼用呢?簡單來說就是從數據源當中處理操作並輸元素序列的一個東西,好像說得不太好。來段代碼看看吧!我們有一個列表,裏面篩選出女同學放到另外一個列表當中,以前我們是這樣做的。

先給出一個實體Student類(這個實體類會在例子當中使用)

public class Student {

    public static final int GENDER_FEMALE = 0;
    public static final int GENDER_MALE = 1;

    private String name;

    private int age;

    private int gender;

    private String className;

    public Student() {
    }

    public Student(String name, int age, int gender, String className) {
        this.name = name;
        this.age = age;
        this.gender = gender;
        this.className = className;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public int getGender() {
        return gender;
    }

    public void setGender(int gender) {
        this.gender = gender;
    }

    public String getClassName() {
        return className;
    }

    public void setClassName(String className) {
        this.className = className;
    }
}

按照原定計劃,通過舊的代碼篩選出女同學。不過在輸出我也偷懶了一下用了stream來打印輸出,不過具體怎麼寫我後面會說到。

    public static void main(String[] args) {
        List<Student> studentList = new ArrayList<>();
        studentList.add(new Student("Jason", 18, Student.GENDER_MALE, "ClassA"));
        studentList.add(new Student("May", 17, Student.GENDER_FEMALE, "ClassB"));
        studentList.add(new Student("Angelia", 18, Student.GENDER_FEMALE, "ClassB"));
        studentList.add(new Student("Alisa", 16, Student.GENDER_FEMALE, "ClassA"));
        studentList.add(new Student("Aimee", 16, Student.GENDER_FEMALE, "ClassA"));
        studentList.add(new Student("Ryan", 18, Student.GENDER_MALE, "ClassB"));

        List<Student> list = new ArrayList<>();
        for (Student stu : studentList) {
            if (stu.getGender() == Student.GENDER_FEMALE) {
                list.add(stu);
            }
        }

        //打印結果(後面會學到,這個就是用stream實現的)
        list.stream().map(Student::getName).forEach(System.out::println);
    }

如果換成Stream的寫法將會非常簡單,看看以下代碼:

public static void main(String[] args) {
        List<Student> studentList = new ArrayList<>();
        studentList.add(new Student("Jason", 18, Student.GENDER_MALE, "ClassA"));
        studentList.add(new Student("May", 17, Student.GENDER_FEMALE, "ClassB"));
        studentList.add(new Student("Angelia", 18, Student.GENDER_FEMALE, "ClassB"));
        studentList.add(new Student("Alisa", 16, Student.GENDER_FEMALE, "ClassA"));
        studentList.add(new Student("Aimee", 16, Student.GENDER_FEMALE, "ClassA"));
        studentList.add(new Student("Ryan", 18, Student.GENDER_MALE, "ClassB"));

        List<Student> list = studentList.stream() //獲得一個stream流對象
                //篩選出女性同學(通過lambda表達式定義)
                .filter((student)->student.getGender() == Student.GENDER_FEMALE)
                //篩選後獲得一個列表
                .collect(Collectors.toList());

        //打印結果(後面會學到,這個就是用stream實現的)
        list.stream().map(Student::getName).forEach(System.out::println);
    }

是不是簡單很多呢?段段幾行代碼就完成這個操作了,接下來我們好好學習一下Stream流是如何去使用的。

一、瞭解Stream

 

Stream最簡單的操作,可以拆解成三步

  • 源:stream會獲得一個數據源,這個數據源可以是文件、List、數組等等。
  • 數據處理:流的數據處理操作非常多種多樣,非常類似與數據庫操作。如上面我們見到的filter和map都是輸入數據操作的一部分,除此之外還包括find、match、sort、reduce等,而且流的操作可以並行執行和順序執行充分利用CPU的多核特性。後面我們會一一講解如何使用。
  • 元素序列:通過一系列的處理操作後,我們可以獲得處理完成的元素序列,例如ArrayList等。除了獲得List之外當然可以獲得更多的東西了例如獲得一個統計結果也是沒有問題的。後面我們會認識到這些用法。

Stream的特性

  • 流水線操作:所謂的流水線操作,就是鏈式編寫。數據處理絕大部分都是返回一個Stream對象,所以可以你可以根據的自身的業務需求一直寫下去。個人覺得,其實這也不算是什麼特性。
  • 內部迭代:這個算是一個特性,因爲我們所謂的迭代是通過JAVA語法糖做到的迭代,而Stream是真正在內部實現的迭代。
  • 遍歷特性:流對象和迭代器是一樣的,只能迭代一次。例如上面的例子當中我們用到了 forEach去迭代輸出Student的name,如果我們繼續使用這個stream對象再去調用一次forEac你將會得到一個exception。再去迭代的話,還是老老實實重新生成一個stream吧。每次迭代完成等於是消費了一個stream了。

 

二、Stream的操作

Stream的操作主要兩種,中間操作、終端操作。中間操作就是數據處理、終端操作就是如獲得列表、統計數量等等這些。反正竄起來就是一個數據源--- 多箇中間操作--- 一個終端操作:

 public static void main(String[] args) {
        List<Student> studentList = new ArrayList<>();
        studentList.add(new Student("Jason", 18, Student.GENDER_MALE, "ClassA"));
        studentList.add(new Student("May", 17, Student.GENDER_FEMALE, "ClassB"));
        studentList.add(new Student("Angelia", 18, Student.GENDER_FEMALE, "ClassB"));
        studentList.add(new Student("Alisa", 16, Student.GENDER_FEMALE, "ClassA"));
        studentList.add(new Student("Aimee", 16, Student.GENDER_FEMALE, "ClassA"));
        studentList.add(new Student("Melinda", 15, Student.GENDER_FEMALE, "ClassA"));
        studentList.add(new Student("Ryan", 18, Student.GENDER_MALE, "ClassB"));

        studentList.stream()    //數據源 獲得一個stream對象
                .filter((std)->std.getGender()==Student.GENDER_FEMALE)  //中間操作 獲得所有女生
                .filter((std)->std.getClassName().equals("ClassA"))     //中間操作 獲得classA班的學生
                .sorted((stdA,stdB)->Integer.compare(stdA.getAge(),stdB.getAge()))  //中間操作 按照按年齡排序
                .limit(2)       //中間操作 獲得年輕的兩個
                .map((std)->std.getName()+"["+std.getAge()+"]") //中間操作 將結合整合成一個字符串 姓名加年齡
                .forEach(System.out::println);      //終端操作 將結果遍歷打印
    }

打印結果

Melinda[15]
Alisa[16]

Process finished with exit code 0

1、中間操作

其實中間操作又非常多我們說一些平常有可能用到的吧,其他自己查API好不好。

filter、sorted、limit、map都用過了,下面來看看distinct,提供數組生成stream然後剔除一樣的元素。

public static void main(String[] args) {
        String[] word = {"AAA","BBB","CCC","BBB","AAA","DDD"};
        Arrays.stream(word).distinct().forEach(System.out::println);
    }

輸出:

AAA
BBB
CCC
DDD

flatMap 可以將多個數組換成統一的流,然後會把多個流融合成一個流。下面數組當中有多行英文數據,我們將其每句英文轉換成String[] 數組的單詞數組,然後通過flatMap統一換成Stream<String>最後多個Stream<String>融合成一個Stream<String>然後排除相同的英文單詞。

public static void main(String[] args) {
    String[] word = {"Age has reached the end of the beginning of a word"
                , "May be guilty in his seems to passing a lot of different life became the appearance of the same day"
                , "May be back in the past, to oneself the paranoid weird belief disillusionment"};

        Arrays.stream(word).map((line) -> line.split(" "))
                .flatMap((wordArray) -> Arrays.stream(wordArray))
                .map(String::toLowerCase)
                .distinct()
                .sorted()
                .forEach((String str) -> System.out.print(str + " "));
}

輸出

a age appearance back be became beginning belief day different disillusionment end guilty 
has his in life lot may of oneself paranoid passing past, reached same seems the to weird word 

skip跳過某個元素

studentList.stream().skip(3).forEach(System.out::println);

 

2、查找與匹配

Stream API 還提供了allMatch,anyMatch,noneMatch,findFirst,findAny 的操作,這裏操作在平時我們也是用得相當多,從字面上我們已經基本能夠明白其中的意義,下面我們一個個來實踐一下

allMatch,我們嘗試使用allMatch。看看在student列表當中,是否全部都滿足少於20歲這個要求

public static void main(String[] args) {
        List<Student> studentList = new ArrayList<>();
        studentList.add(new Student("Jason", 18, Student.GENDER_MALE, "ClassA"));
        studentList.add(new Student("May", 17, Student.GENDER_FEMALE, "ClassB"));
        studentList.add(new Student("Angelia", 18, Student.GENDER_FEMALE, "ClassB"));
        studentList.add(new Student("Alisa", 16, Student.GENDER_FEMALE, "ClassA"));
        studentList.add(new Student("Aimee", 16, Student.GENDER_FEMALE, "ClassA"));
        studentList.add(new Student("Melinda", 15, Student.GENDER_FEMALE, "ClassA"));
        studentList.add(new Student("Ryan", 18, Student.GENDER_MALE, "ClassB"));

        if(studentList.stream().allMatch((std)->std.getAge()<20)){
            System.out.println("全部學習少於20歲");
        }
    }

anyMatch 是否有任意一個同學是15歲

if(studentList.stream().anyMatch((std)->std.getAge()==15)){
    System.out.println("有同學等於15歲");
}

noneMatch 是否沒有同學在classC班

if(studentList.stream().noneMatch((std)->std.getClassName().equals("ClassC"))){
    System.out.println("沒有同學在classC班");
}

findFirst獲得年紀最小的同學

    Optional<String> optional = studentList.stream()
                .sorted(Comparator.comparing(Student::getAge))
                .map(Student::getName)
                .findFirst();
    System.out.println("name:" + optional.get());

findAny 獲得任意一個年紀是18的同學

    Optional<String> optional = studentList.stream()
                .filter((std) -> std.getAge() == 18)
                .map(Student::getName)
                .findAny();
    System.out.println("name:" + optional.get());

說完來看看爲什麼返回一個optional的對象,optional是以防拋出nullpointexception的操作對象,是JAVA8最新提供的。我們來看看他的類方法有那些就基本知道它是做什麼的,下表來源於網絡(沒錯我懶得做)

Optional類方法

序號 方法 & 描述
1 static <T> Optional<T> empty()

返回空的 Optional 實例。

2 boolean equals(Object obj)

判斷其他對象是否等於 Optional。

3 Optional<T> filter(Predicate<? super <T> predicate)

如果值存在,並且這個值匹配給定的 predicate,返回一個Optional用以描述這個值,否則返回一個空的Optional。

4 <U> Optional<U> flatMap(Function<? super T,Optional<U>> mapper)

如果值存在,返回基於Optional包含的映射方法的值,否則返回一個空的Optional

5 T get()

如果在這個Optional中包含這個值,返回值,否則拋出異常:NoSuchElementException

6 int hashCode()

返回存在值的哈希碼,如果值不存在 返回 0。

7 void ifPresent(Consumer<? super T> consumer)

如果值存在則使用該值調用 consumer , 否則不做任何事情。

8 boolean isPresent()

如果值存在則方法會返回true,否則返回 false。

9 <U>Optional<U> map(Function<? super T,? extends U> mapper)

如果存在該值,提供的映射方法,如果返回非null,返回一個Optional描述結果。

10 static <T> Optional<T> of(T value)

返回一個指定非null值的Optional。

11 static <T> Optional<T> ofNullable(T value)

如果爲非空,返回 Optional 描述的指定值,否則返回空的 Optional。

12 T orElse(T other)

如果存在該值,返回值, 否則返回 other。

13 T orElseGet(Supplier<? extends T> other)

如果存在該值,返回值, 否則觸發 other,並返回 other 調用的結果。

14 <X extends Throwable> T orElseThrow(Supplier<? extends X> exceptionSupplier)

 

如果存在該值,返回包含的值,否則拋出由 Supplier 繼承的異常

15 String toString()

返回一個Optional的非空字符串,用來調試

基本上明白是幹什麼用的吧 其中orElse讓數據爲空的時候提供一個默認值這個其實是非常實用的一個功能,防止你部署發現“哎喲NullPointException了~”

3、規約

規約這個其實就可以根據目前已經完成處理的數據進行一個統計獲得一個統一的值,這個是相當有用的一個功能,也是我們平時用for循環幹得最多的事情,下面來看看stream這麼完成這項操作。

reduce方法,reduce方法是一個較爲通用的一個規約方法。首先我們來舉個例子看看如何使用,將所有同學的歲數進行相加:

    public static void main(String[] args) {
        List<Student> studentList = new ArrayList<>();
        studentList.add(new Student("Jason", 18, Student.GENDER_MALE, "ClassA"));
        studentList.add(new Student("May", 17, Student.GENDER_FEMALE, "ClassB"));
        studentList.add(new Student("Angelia", 18, Student.GENDER_FEMALE, "ClassB"));
        studentList.add(new Student("Alisa", 16, Student.GENDER_FEMALE, "ClassA"));
        studentList.add(new Student("Aimee", 16, Student.GENDER_FEMALE, "ClassA"));
        studentList.add(new Student("Melinda", 15, Student.GENDER_FEMALE, "ClassA"));
        studentList.add(new Student("Ryan", 18, Student.GENDER_MALE, "ClassB"));

        int ageSum = studentList.stream().map(Student::getAge).reduce(0, (sum, studentAge) -> sum + studentAge);
        System.out.println(ageSum);
    }

reduce第一個參數是初始化的值,然後裏面接受一個BiFunction函數式接口,裏面會將上一條的計算結果通過第一個參數傳入,第二個參數再傳入當前循環的學生歲數,然後再相加返回結果。

其實可以再簡單一點,通過函數引用的方法去做:

//獲得總和
int ageSum = studentList.stream().map(Student::getAge).reduce(0, Integer::sum);
int ageSum2 = studentList.stream().map(Student::getAge).reduce(0, (sum, age) -> sum + age);
//獲得最大值
int ageMax = studentList.stream().map(Student::getAge).reduce(0, Integer::max);
int ageMax2 = studentList.stream().map(Student::getAge).reduce(0, (max, age) -> max > age ? max : age);
//獲得最小值
int ageMin = studentList.stream().map(Student::getAge).reduce( Integer::min).get();
int ageMin2 = studentList.stream().map(Student::getAge).reduce((min, age) -> min < age ? min : age).get();

最小值爲什麼不用默認值,因爲很簡單,給一個0的默認值,最小值永遠是0。然而沒有默認值的reduce方法返回的是一個optional對象所以要調用一下get

count 方法獲得結果的總條目數,這個嘛就不用怎麼解釋了吧

Long femaleStudentCount = studentList.stream().filter((std)->std.getGender()==Student.GENDER_FEMALE).count();

4、收集數據

其實我們剛剛也用到了收集器了,collect方法toList用很多了,現在說說其他的。先給一下toList的例子回憶一下

    public static void main(String[] args) throws IOException {
        List<Student> studentList = new ArrayList<>();
        studentList.add(new Student("Jason", 18, Student.GENDER_MALE, "ClassA"));
        studentList.add(new Student("May", 17, Student.GENDER_FEMALE, "ClassB"));
        studentList.add(new Student("Angelia", 18, Student.GENDER_FEMALE, "ClassB"));
        studentList.add(new Student("Alisa", 16, Student.GENDER_FEMALE, "ClassA"));
        studentList.add(new Student("Aimee", 16, Student.GENDER_FEMALE, "ClassA"));
        studentList.add(new Student("Melinda", 15, Student.GENDER_FEMALE, "ClassA"));
        studentList.add(new Student("Ryan", 18, Student.GENDER_MALE, "ClassB"));
        
        studentList.stream().filter((stu)->stu.getAge() == 18).collect(Collectors.toList());
    }

我們也可以使用Collections.groupingBy方法分別分開那男女同學(Map的KEY未性別,VALUE爲student對象列表):

public static void main(String[] args) throws IOException {
        List<Student> studentList = new ArrayList<>();
        studentList.add(new Student("Jason", 18, Student.GENDER_MALE, "ClassA"));
        studentList.add(new Student("May", 17, Student.GENDER_FEMALE, "ClassB"));
        studentList.add(new Student("Angelia", 18, Student.GENDER_FEMALE, "ClassB"));
        studentList.add(new Student("Alisa", 16, Student.GENDER_FEMALE, "ClassA"));
        studentList.add(new Student("Aimee", 16, Student.GENDER_FEMALE, "ClassA"));
        studentList.add(new Student("Melinda", 15, Student.GENDER_FEMALE, "ClassA"));
        studentList.add(new Student("Ryan", 18, Student.GENDER_MALE, "ClassB"));

        Map<Integer,List<Student>> result = studentList.stream()
            .collect(Collectors.groupingBy(Student::getGender));
    }

我們可以通過joinning方法將名字合拼並用“,”號隔開

    public static void main(String[] args) throws IOException {
        List<Student> studentList = new ArrayList<>();
        studentList.add(new Student("Jason", 18, Student.GENDER_MALE, "ClassA"));
        studentList.add(new Student("May", 17, Student.GENDER_FEMALE, "ClassB"));
        studentList.add(new Student("Angelia", 18, Student.GENDER_FEMALE, "ClassB"));
        studentList.add(new Student("Alisa", 16, Student.GENDER_FEMALE, "ClassA"));
        studentList.add(new Student("Aimee", 16, Student.GENDER_FEMALE, "ClassA"));
        studentList.add(new Student("Melinda", 15, Student.GENDER_FEMALE, "ClassA"));
        studentList.add(new Student("Ryan", 18, Student.GENDER_MALE, "ClassB"));

        String names = studentList.stream().map(Student::getName).collect(Collectors.joining(","));
        System.err.println(names);

    }

輸出爲:

Jason,May,Angelia,Alisa,Aimee,Melinda,Ryan

 

三、原始類型流

什麼是原始類型的流,就是諸如IntStream、DoubleStream、LongStream這種的Stream特殊類。首先這種類減少了的拆箱和裝箱是肯定的,除此之外還提供如sum、max、min、average等方法 提供最大值、最小值、平均值等等這些操作,大大減少我們自己去寫reduce的時間。

如何去獲得Stream流,通過mapToInt等stream的方法就可以獲得相應的原始類流,原始類流也可以通過boxed獲得Stream的對象。上代碼看看

//獲得IntStream 特定流對象
IntStream intStream = studentList.stream().mapToInt(Student::getAge);
int ageSum = intStream.sum();
//換回爲標準的stream流對象
Stream<Integer> stream = intStream.boxed();

同樣我們可以使用IntStream獲得班上同學的平均年齡

OptionalDouble avgAgeOpt =studentList.stream().mapToInt(Student::getAge).average();
avgAgeOpt.ifPresent(System.out::println);

IntStream還可以直接定義其迭代範圍,貌似有點python range的味道。

IntStream.range(0,100).forEach(System.out::println);    //打印0-99
IntStream.rangeClosed(0,100).forEach(System.out::println);  //打印0-100

 

四、創建流的方法

創建流的方法有很多種,在我們剛剛用得最多的就是通過一個List獲得一個stream對象,但是除了這種方法還可以通過文件IO獲得stream對象,還可以通過Stream.of 靜態方法獲得Stream對象,我們就來一個個試試

Stream.of

Stream<String> stream = Stream.of("A","B","C");
stream.forEach(System.out::println);

通過NIO獲得文件的stream

Stream<String> stream =Files.lines(Paths.get("resource/test.txt").toAbsolutePath(), Charset.defaultCharset());
stream.forEach(System.out::println);

通過一定規則的迭代

//從2開始 一直累加2 無限迭代 limit限制10
Stream.iterate(2, (a) -> a + 2).limit(10).forEach(System.out::println);

五、總結

Stream流裏面太多知識點了,例如自定義collectors等等。還有通過parallelStream獲得一個支持併發多核操作的stream。知識點也有待發掘。總體來說是我寫得有點累該截稿了。

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章