天天看點

開始使用流

  • Java 8 中的 Stream 俗稱為流,它與 java.io 包裡的 InputStream 和 OutputStream 是完全不同的概念
  • Stream 用于對集合對象進行各種非常便利、高效的聚合操作,或者大批量資料操作
  • Stream API 借助于 Lambda 表達式,極大的提高程式設計效率和程式可讀性
  • 同時它提供串行和并行兩種模式進行彙聚操作,并發模式能夠充分利用多核處理器的優勢
  • 通過下面的例子我們可以初步體會到使用 Stream 處理集合的便利性

初探Stream

  • 有如下一個 List 集合,現要從中篩選出以​

    ​J​

    ​ 開頭的元素,然後轉換為大寫,最後輸出結果
  • Java 8之前我們是這樣做的:
/**
 * @author BNTang
 **/
public class Demo {
    public static void main(String[] args) {
        List<String> list = Arrays.asList("Java", "JavaScript", "python", "PHP", "C#", "Golang", "Swift");
        List<String> filterList = new ArrayList<>();
        for (String str : list) {
            if (str.startsWith("J")) {
                filterList.add(str.toUpperCase());
            }
        }
        for (String str : filterList) {
            System.out.println(str);
        }
    }
}      
  • 為了篩選集合我們進行了兩次外部疊代,并且還建立了一個用來臨時存放篩選元素的集合對象
  • 借助Java 8中的Stream我們可以極大的簡化這個處理過程:
/**
 * @author BNTang
 **/
public class Demo {
    public static void main(String[] args) {
        List<String> list = Arrays.asList("Java", "JavaScript", "python", "PHP", "C#", "Golang", "Swift");
        list.stream()
                .filter(s -> s.startsWith("J"))
                .map(String::toUpperCase)
                .forEach(System.out::println);
    }
}      

是不是很友善?上面的例子中,集合使用 ​

​stream​

​​ 方法建立了一個流,然後使用 ​

​filter​

​​ 和 ​

​map​

​ 方法來處理這個集合,它們統稱為 中間操作。中間操作都會傳回另一個流,以便于将各種對集合的操作連接配接起來形成一條流水線。最後我們使用了 ​

​forEach​

​ 方法疊代篩選結果,這種位于流的末端,對流進行處理并且生成結果的方法稱為 終端操作。

總而言之,流的使用一般包括三件事情:

  • 一個資料源(如集合)來執行一個查詢
  • 一個中間操作鍊,形成一條流的流水線
  • 一個終端操作,執行流水線,并能生成結果

下表列出了流中常見的中間操作和終端操作:

操作 類型 傳回類型 使用的類型 / 函數式接口 函數描述符
filter 中間 Stream<T> Predicate<T> T -> boolean
distinct 中間 Stream<T>
skip 中間 Stream<T> long
limit 中間 Stream<T> long
map 中間 Stream<R> Function<T, R> T -> R
flatMap 中間 Stream<R> Function<T, Stream<R>> T -> Stream<R>
sorted 中間 Stream<T> Comparator<T> (T, T) -> int
anyMatch 終端 boolean Predicate<T> T -> boolean
noneMatch 終端 boolean Predicate<T> T -> boolean
allMatch 終端 boolean Predicate<T> T -> boolean
findAny 終端 Optional<T>
findFirst 終端 Optional<T>
forEach 終端 void Consumer<T> T -> void
collect 終端 R Collector<T, A, R>
reduce 終端 Optional<T> BinaryOperator<T> (T, T) -> T
count 終端 long
  • 下面詳細介紹這些操作的使用
  • 除了特殊說明,使用下面這個集合作為示範:
List<String> list = Arrays.asList("Java", "JavaScript", "python", "PHP", "C#", "Golang", "Swift", "C++", "Ruby");      

中間操作

filter

  • Streams 接口支援​

    ​filter​

    ​​ 方法,該方法接收一個​

    ​Predicate<T>​

    ​​,函數描述符為​

    ​T -> boolean​

    ​,用于對集合進行篩選,傳回所有滿足的元素:
/**
 * @author BNTang
 **/
public class Demo {
    public static void main(String[] args) {
        List<String> list = Arrays.asList("Java", "JavaScript", "python", "PHP", "C#", "Golang", "Swift", "C++", "Ruby");

        list.stream()
                .filter(s -> s.contains("#"))
                .forEach(System.out::println);
    }
}      
  • 結果輸出​

    ​C#​

distinct

  • ​distinct​

    ​​ 方法用于​

    ​排除​

    ​ 流中重複的元素,類似于 SQL 中的 distinct 操作
  • 比如篩選中集合中所有的偶數,并排除重複的結果:
/**
 * @author BNTang
 **/
public class Demo {
    public static void main(String[] args) {
        List<Integer> numbers = Arrays.asList(1, 2, 1, 3, 3, 2, 4);
        numbers.stream()
                .filter(i -> i % 2 == 0)
                .distinct()
                .forEach(System.out::println);
    }
}      
  • 結果輸出​

    ​2 4​

skip

  • ​skip(n)​

    ​​ 方法用于跳過流中的​

    ​前n個元素​

    ​,如果集合元素小于n,則傳回空流
  • 比如篩選出以​

    ​J​

    ​ 開頭的元素,并排除第一個:
/**
 * @author BNTang
 **/
public class Demo {
    public static void main(String[] args) {
        List<String> list = Arrays.asList("Java", "JavaScript", "python", "PHP", "C#", "Golang", "Swift", "C++", "Ruby");
        list.stream()
                .filter(s -> s.startsWith("J"))
                .skip(1)
                .forEach(System.out::println);
    }
}      
  • 結果輸出​

    ​JavaScript​

limit

  • ​limit(n)​

    ​​ 方法傳回一個長度不超過​

    ​n​

    ​​ 的流,比如下面的例子将輸出​

    ​Java JavaScript python​

    ​:
  • 例如你輸入的 3,傳回的就是3,不會超過3
/**
 * @author BNTang
 **/
public class Demo {
    public static void main(String[] args) {
        List<String> list = Arrays.asList("Java", "JavaScript", "python", "PHP", "C#", "Golang", "Swift", "C++", "Ruby");
        list.stream()
                .limit(3)
                .forEach(System.out::println);
    }
}      

map

  • ​map​

    ​ 方法接收一個函數作為參數
  • 這個函數會被應用到每個元素上,并将其映射成一個新的元素
  • 如:
/**
 * @author BNTang
 **/
public class Demo {
    public static void main(String[] args) {
        List<String> list = Arrays.asList("Java", "JavaScript", "python", "PHP", "C#", "Golang", "Swift", "C++", "Ruby");
        list.stream()
                .map(String::length)
                .forEach(System.out::println);
    }
}      
  • 結果輸出​

    ​4 10 6 3 2 6 5 3 4​

  • ​map​

    ​​ 還支援将流特化為指定的原始類型的流,如通過​

    ​mapToInt​

    ​​,​

    ​mapToDouble​

    ​​ 和​

    ​mapToLong​

    ​​ 方法,可以将流轉換為​

    ​IntStream​

    ​​,​

    ​DoubleStream​

    ​​ 和​

    ​LongStream​

  • 特化後的流支援​

    ​sum​

    ​​,​

    ​min​

    ​​ 和​

    ​max​

    ​ 方法來對流中的元素進行計算
  • 比如:
/**
 * @author BNTang
 **/
public class Demo {
    public static void main(String[] args) {
        List<Integer> numbers = Arrays.asList(1, 2, 1, 3, 3, 2, 4);
        IntStream intStream = numbers.stream().mapToInt(a -> a);
        System.out.println(intStream.sum());
    }
}      
  • 也可以通過下面的方法,将​

    ​IntStream​

    ​​ 轉換為​

    ​Stream​

    ​:
/**
 * @author BNTang
 **/
public class Demo {
    public static void main(String[] args) {
        List<Integer> numbers = Arrays.asList(1, 2, 1, 3, 3, 2, 4);
        IntStream intStream = numbers.stream().mapToInt(a -> a);
        Stream<Integer> s = intStream.boxed();
    }
}      

flatMap

  • ​flatMap​

    ​ 用于将多個流合并成一個流,俗稱流的扁平化
  • 這麼說有點抽象,舉個例子,比如現在需要将 list 中的各個元素拆分為一個個字母,并過濾掉重複的結果,你可能會這樣做:
/**
 * @author BNTang
 **/
public class Demo {
    public static void main(String[] args) {
        List<String> list = Arrays.asList("Java", "JavaScript", "python", "PHP", "C#", "Golang", "Swift", "C++", "Ruby");
        list.stream()
                .map(s -> s.split(""))
                .distinct()
                .forEach(System.out::println);
    }
}      
  • 輸出結果如下:
[Ljava.lang.String;@58372a00
[Ljava.lang.String;@4dd8dc3
[Ljava.lang.String;@6d03e736
[Ljava.lang.String;@568db2f2
[Ljava.lang.String;@378bf509
[Ljava.lang.String;@5fd0d5ae
[Ljava.lang.String;@2d98a335
[Ljava.lang.String;@16b98e56
[Ljava.lang.String;@7ef20235      
  • 這明顯不符合我們的預期
  • 實際上在​

    ​map(s -> s.split(""))​

    ​​ 操作後,傳回了一個​

    ​Stream<String[]>​

    ​​ 類型的流,是以輸出結果為每個數組對象的句柄,而我們真正想要的結果是​

    ​Stream<String>​

    ​!
  • 在 Stream 中,可以使用​

    ​Arrays.stream()​

    ​ 方法來将數組轉換為流,改造上面的方法:
/**
 * @author BNTang
 **/
public class Demo {
    public static void main(String[] args) {
        List<String> list = Arrays.asList("Java", "JavaScript", "python", "PHP", "C#", "Golang", "Swift", "C++", "Ruby");
        list.stream()
                .map(s -> s.split(""))
                .map(Arrays::stream)
                .distinct()
                .forEach(System.out::println);
    }
}      
  • 輸出結果如下:
java.util.stream.ReferencePipeline$Head@568db2f2
java.util.stream.ReferencePipeline$Head@378bf509
java.util.stream.ReferencePipeline$Head@5fd0d5ae
java.util.stream.ReferencePipeline$Head@2d98a335
java.util.stream.ReferencePipeline$Head@16b98e56
java.util.stream.ReferencePipeline$Head@7ef20235
java.util.stream.ReferencePipeline$Head@27d6c5e0
java.util.stream.ReferencePipeline$Head@4f3f5b24
java.util.stream.ReferencePipeline$Head@15aeb7ab      
  • 因為上面的流經過​

    ​map(Arrays::stream)​

    ​​ 處理後,将每個數組變成了一個新的流,傳回結果為流的數組​

    ​Stream<String>[]​

    ​,是以輸出是各個流的句柄
  • 我們還需将這些新的流連接配接成一個流,使用​

    ​flatMap​

    ​ 來改寫上面的例子:
/**
 * @author BNTang
 **/
public class Demo {
    public static void main(String[] args) {
        List<String> list = Arrays.asList("Java", "JavaScript", "python", "PHP", "C#", "Golang", "Swift", "C++", "Ruby");
        list.stream()
                .map(s -> s.split(""))
                .flatMap(Arrays::stream)
                .distinct()
                .forEach(s -> System.out.print(s + " "));
    }
}      
  • 輸出結果如下:
J a v S c r i p t y h o n P H C # G l g w f + R u b      
  • 和​

    ​map​

    ​​ 類似,​

    ​flatMap​

    ​​ 方法也有相應的原始類型特化方法,如​

    ​flatMapToInt​

    ​ 等

終端操作

anyMatch

  • ​anyMatch​

    ​​ 方法用于判斷流中是否有符合判斷條件的元素,傳回值為​

    ​boolean類型​

  • 比如判斷 list 中是否含有​

    ​SQL​

    ​ 元素:
/**
 * @author BNTang
 **/
public class Demo {
    public static void main(String[] args) {
        List<String> list = Arrays.asList("Java", "JavaScript", "python", "PHP", "C#", "Golang", "Swift", "C++", "Ruby");
        System.out.println(list.stream()
                .anyMatch(s -> "SQL".equals(s)));
    }
}      

allMatch

  • ​allMatch​

    ​​ 方法用于判斷流中是否所有元素都滿足給定的判斷條件,傳回值為​

    ​boolean類型​

  • 比如判斷 list 中是否所有元素長度都不大于10:
/**
 * @author BNTang
 **/
public class Demo {
    public static void main(String[] args) {
        List<String> list = Arrays.asList("Java", "JavaScript", "python", "PHP", "C#", "Golang", "Swift", "C++", "Ruby");
        System.out.println(list.stream()
                .allMatch(s -> s.length() <= 10));
    }
}      

noneMatch

  • ​noneMatch​

    ​​ 方法用于判斷流中是否所有元素都不滿足給定的判斷條件,傳回值為​

    ​boolean類型​

  • 比如判斷 list 中不存在長度大于10的元素:
/**
 * @author BNTang
 **/
public class Demo {
    public static void main(String[] args) {
        List<String> list = Arrays.asList("Java", "JavaScript", "python", "PHP", "C#", "Golang", "Swift", "C++", "Ruby");
        System.out.println(list.stream()
                .noneMatch(s -> s.length() > 10));
    }
}      

findAny

  • ​findAny​

    ​ 方法用于傳回流中的任意元素的 Optional 類型
  • 例如篩選出 list 中任意一個以​

    ​J​

    ​ 開頭的元素,如果存在,則輸出它:
/**
 * @author BNTang
 **/
public class Demo {
    public static void main(String[] args) {
        List<String> list = Arrays.asList("Java", "JavaScript", "python", "PHP", "C#", "Golang", "Swift", "C++", "Ruby");
        list.stream()
                .filter(s -> s.startsWith("J"))
                .findAny()
                .ifPresent(System.out::println);
    }
}      

findFirst

  • ​findFirst​

    ​ 方法用于傳回流中的第一個元素的 Optional 類型
  • 例如篩選出 list 中長度大于 5 的元素,如果存在,則輸出第一個:
/**
 * @author BNTang
 **/
public class Demo {
    public static void main(String[] args) {
        List<String> list = Arrays.asList("Java", "JavaScript", "python", "PHP", "C#", "Golang", "Swift", "C++", "Ruby");
        list.stream()
                .filter(s -> s.length() > 5)
                .findFirst()
                .ifPresent(System.out::println);
    }
}      

reduce

  • ​reduce​

    ​函數從字面上來看就是壓縮,縮減的意思,它可以用于數字類型的流的求和,求最大值和最小值。如對numbers中的元素求和:
/**
 * @author BNTang
 **/
public class Demo {
    public static void main(String[] args) {
        List<Integer> numbers = Arrays.asList(1, 2, 1, 3, 3, 2, 4);
        System.out.println(numbers.stream().reduce(0, Integer::sum));
    }
}      
  • ​reduce​

    ​​ 函數也可以不指定初始值,但這時候将傳回一個​

    ​Optional​

    ​ 對象,比如求最大值和最小值:
/**
 * @author BNTang
 **/
public class Demo {
    public static void main(String[] args) {
        List<Integer> numbers = Arrays.asList(1, 2, 1, 3, 3, 2, 4);
        numbers.stream()
                .reduce(Integer::max)
                .ifPresent(System.out::println);

        numbers.stream()
                .reduce(Integer::min)
                .ifPresent(System.out::println);
    }
}      

forEach

  • ​forEach​

    ​ 用于疊代流中的每個元素,最為常見的就是疊代輸出,如:
/**
 * @author BNTang
 **/
public class Demo {
    public static void main(String[] args) {
        List<String> list = Arrays.asList("Java", "JavaScript", "python", "PHP", "C#", "Golang", "Swift", "C++", "Ruby");
        list.stream().forEach(System.out::println);
    }
}      

count

  • ​count​

    ​ 方法用于統計流中的元素的個數,比如:
/**
 * @author BNTang
 **/
public class Demo {
    public static void main(String[] args) {
        List<String> list = Arrays.asList("Java", "JavaScript", "python", "PHP", "C#", "Golang", "Swift", "C++", "Ruby");
        System.out.println(list.stream().count());
    }
}      

collect

  • ​collect​

    ​​ 方法用于收集流中的元素,并放到不同類型的結果中,比如​

    ​List​

    ​​、​

    ​Set​

    ​​ 或者​

    ​Map​

  • 舉個例子:
/**
 * @author BNTang
 **/
public class Demo {
    public static void main(String[] args) {
        List<String> list = Arrays.asList("Java", "JavaScript", "python", "PHP", "C#", "Golang", "Swift", "C++", "Ruby");
        List<String> filterList = list.stream().filter(s -> s.startsWith("J")).collect(Collectors.toList());
        System.out.println(filterList);
    }
}      
  • 如果需要以​

    ​Set​

    ​​ 來替代​

    ​List​

    ​​,隻需要使用​

    ​Collectors.toSet()​

    ​ 就好了

流的建構

  • 除了使用集合對象的​

    ​stream​

    ​ 方法建構流之外,我們可以手動建構一些流

數值範圍建構

  • ​IntStream​

    ​​ 和​

    ​LongStream​

    ​​ 對象支援​

    ​range​

    ​​ 和​

    ​rangeClosed​

    ​ 方法來建構數值流
  • 這兩個方法都是第一個參數接受起始值,第二個參數接受結束值
  • 但​

    ​range​

    ​​ 是不包含結束值的,而​

    ​rangeClosed​

    ​ 則包含結束值
  • 比如對 1 到 100 的整數求和:
/**
 * @author BNTang
 **/
public class Demo {
    public static void main(String[] args) {
        System.out.println(IntStream.rangeClosed(1, 100).sum());
    }
}      

由值建構

  • 靜态方法​

    ​Stream.of​

    ​ 可以顯式值建立一個流
  • 它可以接受任意數量的參數
  • 例如,以下代碼直接使用​

    ​Stream.of​

    ​ 建立了一個字元串流:
/**
 * @author BNTang
 **/
public class Demo {
    public static void main(String[] args) {
        Stream<String> s = Stream.of("Java", "JavaScript", "C++", "Ruby");
    }
}      
  • 也可以使用​

    ​Stream.empty()​

    ​ 建構一個空流:
Stream<Object> emptyStream = Stream.empty();      

由數組建構

  • 靜态方法​

    ​Arrays.stream​

    ​ 可以通過數組建立一個流
  • 它接受一個數組作為參數
  • 例如:
/**
 * @author BNTang
 **/
public class Demo {
    public static void main(String[] args) {
        int[] arr = {1, 2, 3, 4, 5};
        IntStream intStream = Arrays.stream(arr);
    }
}      

由檔案生成流

  • ​java.nio.file.Files​

    ​ 中的很多靜态方法都會傳回一個流
  • 例如​

    ​Files.lines​

    ​ 方法會傳回一個由指定檔案中的各行構成的字元串流
  • 比如統計一個檔案中共有多少個字:
/**
 * @author BNTang
 **/
public class Demo {
    public static void main(String[] args) {
        long wordCout = 0L;
        try (Stream<String> lines = Files.lines(Paths.get("file.txt"), Charset.defaultCharset())) {
            wordCout = lines.map(l -> l.split(""))
                    .flatMap(Arrays::stream)
                    .count();
        } catch (Exception ignore) {
        }
        System.out.println(wordCout);
    }
}      

由函數構造

  • Stream API 提供了兩個靜态方法來從函數生成流:​

    ​Stream.iterate​

    ​​ 和​

    ​Stream.generate​

  • 這兩個操作可以建立所謂的無限流
  • 比如下面的例子建構了 10 個偶數:
/**
 * @author BNTang
 **/
public class Demo {
    public static void main(String[] args) {
        Stream.iterate(0, n -> n + 2)
                .limit(10).forEach(System.out::println);
    }
}      
  • ​iterate​

    ​ 方法接受一個初始值(在這裡是0)還有一個依次應用在每個産生的新值上的 Lambda(UnaryOperator類型)
  • 這裡,我們使用 Lambda​

    ​n -> n + 2​

    ​,傳回的是前一個元素加上 2
  • 是以,​

    ​iterate​

    ​ 方法生成了一個所有正偶數的流:流的第一個元素是初始值0
  • 然後加上 2 來生成新的值 2,再加上 2 來得到新的值 4,以此類推
  • 與​

    ​iterate​

    ​​ 方法類似,​

    ​generate​

    ​ 方法也可讓你按需生成一個無限流
  • 但​

    ​generate​

    ​ 不是依次對每個新生成的值應用函數,比如下面的例子生成了 5 個 0 到 1 之間的随機雙精度數:
/**
 * @author BNTang
 **/
public class Demo {
    public static void main(String[] args) {
        Stream.generate(Math::random)
                .limit(5)
                .forEach(System.out::println);
    }
}      
  • 輸出結果如下:
0.4477477019693912
0.8866972547736678
0.6893219838296453
0.3768607796229386
0.9647978867306028