Java8新特性---Stream(流)

Stream

流是Java API 的新成員,它允許你以聲明性方式處理數據集合。你可以把他看成遍歷數據集的高級迭代器,他其實是一連串支持連續、並行聚集操作的元素。同樣,流還可以透明的並行處理。

流和簡單實例

public class Dish {

    private final String name;
    private final boolean vegetarian;
    private final int calories;
    private final Type type;

    public Dish(String name, boolean vegetarian, int calories, Type type) {
        super();
        this.name = name;
        this.vegetarian = vegetarian;
        this.calories = calories;
        this.type = type;
    }

    public String getName() {
        return name;
    }

    public boolean isVegetarian() {
        return vegetarian;
    }

    public int getCalories() {
        return calories;
    }

    public Type getType() {
        return type;
    }

    public enum Type {
        MEAT, FISH, OTHER;
    }
}
public static List<Dish> menu = Arrays.asList(
            new Dish("pork", false, 800, Dish.Type.MEAT),
            new Dish("beaf", false, 700, Dish.Type.MEAT), 
            new Dish("chicken", false, 400, Dish.Type.MEAT),
            new Dish("french fries", true, 530, Dish.Type.OTHER), 
            new Dish("rice", true, 350, Dish.Type.OTHER),
            new Dish("season fruit", true, 120, Dish.Type.OTHER), 
            new Dish("pizza", true, 550, Dish.Type.OTHER),
            new Dish("prawns", false, 300, Dish.Type.FISH), 
            new Dish("salmon", false, 450, Dish.Type.FISH));

目標:獲得卡路里大於300的三種菜名

public class Client {

    public static List<Dish> menu = Arrays.asList(
            new Dish("pork", false, 800, Dish.Type.MEAT),
            new Dish("beaf", false, 700, Dish.Type.MEAT), 
            new Dish("chicken", false, 400, Dish.Type.MEAT),
            new Dish("french fries", true, 530, Dish.Type.OTHER), 
            new Dish("rice", true, 350, Dish.Type.OTHER),
            new Dish("season fruit", true, 120, Dish.Type.OTHER), 
            new Dish("pizza", true, 550, Dish.Type.OTHER),
            new Dish("prawns", false, 300, Dish.Type.FISH), 
            new Dish("salmon", false, 450, Dish.Type.FISH));

    public static List<String> filterDishName() {
        List<String> dishNames = 
                menu.stream()                               \\ 獲取流
                .filter(dish -> dish.getCalories() > 300)   \\ 篩選大於300
                .map(Dish::getName)                         \\ 獲取菜名
                .limit(3)                                   \\ 取三位
                .collect(Collectors.toList());              \\ 終端轉換爲列表
        return dishNames;
    }

    public static void main(String[] args){
        filterDishName().forEach(System.out::println);
    }

}
// console輸出
pork
beaf
chicken

通過流的使用,很簡單的就能完成從前需要一大段代碼才能實現的功能。

構建流

值創建流

Stream<String> stream = Stream.of("hello","world");

數組創建流

String[] s = {"hello","world"};
Arrays.stream(s);

文件生成流

// 查看文件有多少不重複的詞
try {
            Files.lines(Paths.get("data.txt"))
                .flatMap(line -> Arrays.stream(line.split(" ")))
                .distinct()
                .count();
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }

函數生成無限流

Stream.iterate(0, i -> i+2)
            .limit(10);
Stream.generate(Math::random)
            .limit(10);

使用流

篩選

menu.stream()
            .filter(dish -> dish.getCalories() > 300)   \\ 按條件篩選
            .distinct()                                 \\ 選出不同的元素
            .skip(2L)                                   \\ 跳過兩個 
            .limit(3L);                                 \\ 只取三個

映射

  • 流映射

    <R> Stream<R> map(Function<? super T, ? extends R> mapper);

    參數爲Function,映射後轉換成另一種類型返回

    menu.stream()
                .map(Dish::getName)
                .limit(3)
                .collect(Collectors.toList());
  • 流的扁平化

    流的扁平化其實就是將轉化出來的多個流扁平到同一個流中進行處理。

    示例:從字符串數組取出所有不重複的字母

    public static void main(String[] args){
        String[] arrays = {"hello", "world"};
        List<String> list = Arrays.stream(arrays)
                .map(word -> word.split(""))        // 轉換成字母數組
                .flatMap(Arrays::stream)            // 將兩個數組流合成一個數組流
                .distinct()                         // 取唯一
                .collect(Collectors.toList());
        list.forEach(e ->  System.out.print(e));
    }

    流轉換過程

查找

  • 任意匹配

    boolean anyMatch(Predicate<? super T> predicate);
  • 所有匹配

    boolean anyMatch(Predicate<? super T> predicate);
  • 沒有匹配

    boolean noneMatch(Predicate<? super T> predicate);
  • 查找任意一個元素

    Optional<T> findAny();
  • 查找第一個

    Optional<T> findFirst();

    findAny 差別在於,如果並行運行不在意順序可以使用 findAny,如果在意順序則是 findFirst

歸約

Lambda反覆結合每個元素,直到流被歸約成一個值

T reduce(T identity, BinaryOperator<T> accumulator);

實例:將卡路里大於300的菜連接在一起組成一個字符串

public static void main(String[] args){
        String dishNames = menu.stream()
            .filter(dish -> dish.getCalories() > 300)
            .map(Dish::getName)
            .reduce("", (a,b) -> {
                if("".equals(a)){
                    return b;
                } else {
                    return a+","+b;
                }
            });
        System.out.println(dishNames);
    }

數值流

由於流中處理數值數據,必須要將數值裝箱成Integer,處理時轉化成int拆箱計算。無疑是對性能的浪費,所以就有了專門三個原始類型特化流接口:IntStream,DoubleStream,LongStream

  • 映射到數值流

    menu.stream()
    .mapToInt(Dish::getCalories);
  • 轉換爲對象流

    menu.stream()
    .mapToInt(Dish::getCalories)
    .boxed();
  • 數值範圍

    IntStream.rangeClosed(1, 100);

用流收集數據

前篇都是如何操作流,這些操作不會消耗流,目的是建立一條流水線。而最後的終端操作目的是消耗流,最終得到一個結果。

<R, A> R collect(Collector<? super T, A, R> collector);

首先,拋開這個方法,我們先看看jdk爲我們提供的幾個常用的彙總收集器:

  • 歸約彙總

    • 數值彙總
    Collectors.summingInt(ToIntFunction<? super T> mapper)
    int sumCalories = 
                    menu.stream()
                    .filter(dish -> dish.getCalories() > 300)
                    .limit(3)
                    .collect(Collectors.summingInt(Dish::getCalories));
    • 連接字符串
    Collector<CharSequence, ?, String> joining(CharSequence delimiter,
                                                                 CharSequence prefix,
                                                                 CharSequence suffix)
    String dishNames = 
                    menu.stream()
                    .filter(dish -> dish.getCalories() > 300)
                    .map(Dish::getName)
                    .limit(3)
                    .collect(Collectors.joining(","));
  • 分組

    分組的概念也很清晰,類似於數據庫中使用groupby根據屬性對流進行分組的做法。

    Map<Type, List<Dish>> maps = 
                menu.stream()
                .collect(Collectors.groupingBy(Dish::getType));

    對菜根據類型分組,得到Map。

    自定義分組

    Map<String, List<Dish>> maps = 
                        menu.stream().collect(Collectors.groupingBy(dish -> {
                            if( dish.getCalories() >500) 
                                return "big";
                            else 
                                return "ok";
                }));

    多層分組:

    Map<Type, Map<String,List<Dish>>> maps = 
                        menu.stream().collect(Collectors.groupingBy(Dish::getType,
                                Collectors.groupingBy(dish -> {
                                    if( dish.getCalories() >500) 
                                        return "big";
                                    else 
                                        return "ok";
                                })
    
                                ));
  • 分區

    和分組概念差別在於使用true,false區分分組差異

    Map<Boolean, List<Dish>> maps = 
                        menu.stream().collect(Collectors.partitioningBy(Dish::isVegetarian));

自定義收集器

​ 雖然jdk爲我們提供了常用的收集器實現,但是如果需要定製特殊功能實現,仍然需要自定義收集器。那麼接下來看看如何實現Collector接口 。

/**
* T代表流中收集的項目的泛型
* A是累加器的類型
* R是收集操作得到的對象
*/
public interface Collector<T, A, R> {

    Supplier<A> supplier();

    BiConsumer<A, T> accumulator();

    BinaryOperator<A> combiner();

    Function<A, R> finisher();

    Set<Characteristics> characteristics();
}

所以接下來實現一個簡單的收集器,將流中數據收集在一起,放在列表中。實現後,也可以跟Collectors.toList()對照,比較實現過程如何。

public class ToListCollector<T> implements Collector<T, List<T>, List<T>> {

    // 創建空的累加器實例,數據收集時使用此實例。
    // 因此在這個收集列表的收集器中,應當創建一個列表
    @Override
    public Supplier<List<T>> supplier() {
        // return () -> new ArrayList<>();
        return ArrayList::new;
    }

    // 執行歸約操作,當遍歷到第n個元素時,參數爲已經累加n-1次的累加器和第n個元素
    // 因此在當前收集器中,使用列表添加即可。
    @Override
    public BiConsumer<List<T>, T> accumulator() {
        // return (list, t) -> list.add(t);
        return List::add;
    }

    // 對兩個並行的流如何歸約
    // 當前只要將一個列表添加到另一個即可。
    @Override
    public BinaryOperator<List<T>> combiner() {
        return (list1, list2) -> {
            list1.addAll(list2);
            return list1;
        };
    }

    // 所有元素歸約後,進行的轉換操作
    // 當前收集器,收集成list即可,無需轉換
    @Override
    public Function<List<T>, List<T>> finisher() {
        // return Function.identity();
        return (list) -> list;
    }

    // 定義收集器的行爲,關於流是否可以歸約等
    // UNORDERED 歸約不受項目遍歷和累積順序影響
    // CONCURRENT 可以並行歸約
    // IDENTITY_FINISH 便是是恆等函數,無需最後的轉換函數。
    @Override
    public Set<java.util.stream.Collector.Characteristics> characteristics() {
        // TODO Auto-generated method stub
        return Collections.unmodifiableSet(
                EnumSet.of(Collector.Characteristics.IDENTITY_FINISH, 
                        Collector.Characteristics.CONCURRENT));
    }

}

測試示例:

public static void main(String[] args){
        List<Dish> dishes = 
        menu.stream()
            .filter(dish -> dish.getCalories() > 300)
            .collect(new ToListCollector<>());
        dishes.forEach(System.out::println);
    }

運行結果也沒有問題,實現成功。

並行流

簡單的轉換

public static long one(long n){
        return LongStream.rangeClosed(1L, n)
                .limit(n)
                .reduce(0l, Long::sum);
    }

    public static long parallel(long n){
        return LongStream.rangeClosed(1L, n)
                .limit(n)
                .parallel()
                .reduce(0l, Long::sum);
    }

使用並行流計算和比單個流計算和的比較。

不過實際使用中,需要考慮流本身實現是否適合並行拆分計算,才能充分發揮並行流並行的優勢。

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