1. 程式人生 > >Java8 新特性(二)- Stream

Java8 新特性(二)- Stream

Stream 用來處理集合資料的,通過 stream 操作可以實現 SQL 的擁有的大部分查詢功能

Java8 API 官方文件

下面藉助例子,演示 stream 操作

Java userList 列表

private List<User> userList = Arrays.asList(
    new User(101, "小明", 10, "男", "青海省", "西寧市"),
    new User(102, "小青", 12, "女", "寧夏回族自治區", "銀川市"),
    new User(103, "小海", 8, "男", "西藏自治區", "拉薩市"),
    new User(108, "阿刁", 18, "女", "西藏自治區", "拉薩市"),
    new User(104, "小陽", 9, "女", "新疆維吾爾自治區", "烏魯木齊市"),
    new User(105, "小強", 14, "男", "陝西省", "西安市"),
    new User(106, "小帥", 15, "男", "河北省", "石家莊市"),
    new User(107, "小云", 15, "女", "河北省", "石家莊市")
);

MySQL user 表資料

DROP TABLE IF EXISTS `user`;
CREATE TABLE `user`  (
  `id` int(11) PRIMARY KEY,
  `name` varchar(20),
  `age` int(2),
  `gender` varchar(10),
  `province` varchar(100),
  `city` varchar(100)
) ;

INSERT INTO `user` VALUES (101, '小明', 10, '男', '青海省', '西寧市');
INSERT INTO `user` VALUES (102, '小青', 12, '女', '寧夏回族自治區', '銀川市');
INSERT INTO `user` VALUES (103, '小海', 8, '男', '西藏自治區', '拉薩市');
INSERT INTO `user` VALUES (104, '小陽', 9, '女', '新疆維吾爾自治區', '烏魯木齊市');
INSERT INTO `user` VALUES (105, '小強', 14, '男', '陝西省', '西安市');
INSERT INTO `user` VALUES (106, '小帥', 15, '男', '河北省', '石家莊市');
INSERT INTO `user` VALUES (107, '小云', 15, '女', '河北省', '石家莊市');

查詢欄位 select - map

// select id from user
userList.stream()
    .map(e -> e.getId())
    .forEach(System.out::println);

至於如何實現 select id, name from user 查詢多欄位在下面 collector 收集器會詳細講解

條件 where - filter

// select * from user where age<10
userList.stream()
        .filter(e-> e.getAge() < 10)
        .forEach(System.out::println);

// select * from user where age<10 and gender='男'
userList.stream()
        .filter(e->e.getAge() < 10)
        .filter(e->e.getGender()=="男")
        .forEach(System.out::println);

最值、總和、數量、均值(max, min, sum, count, average)

// select max(age), min(age), sum(age), count(age), avg(age) from user
// max
Optional<Integer> maxAge = userList.stream()
                                .map(e -> e.getAge())
                                .max(Comparator.comparingInt(x -> x));
// 等同於
// Optional<Integer> maxAge =  userList.stream()
//  .map(e -> e.getAge())
//  .max((x, y) -> x-y);

// min
Optional<Integer> minAge = userList.stream()
                                .map(e -> e.getAge())
                                .min(Comparator.comparingInt(x -> x));
// sum
Optional<Integer> sumAge = userList.stream()
                                .map(e -> e.getAge())
                                .reduce((e, u) -> e + u);
// count
long count = userList.stream()
                .map(e -> e.getAge())
                .count();
// 平均值=總和/數量

排序 order by - sorted

// select * from user order by age
userList.stream()
        .sorted(Comparator.comparingInt(User::getAge))
        .forEach(System.out::println);

分頁 limit - skip、limit

// select * from user limit 5
userList.stream()
        .limit(5)
        .forEach(System.out::println);

// select * from user limit 5, 5
userList.stream()
        .skip(5)
        .limit(5)
        .forEach(System.out::println);

// select * from user order by age limit 1
userList.stream()
        .sorted(Comparator.comparingInt(User::getAge))
        .limit(1)
        .forEach(System.out::println);
// 或者
Optional<User> minAgeUser = userList.stream()
                                .sorted(Comparator.comparingInt(User::getAge))
                                .findFirst();

是否存在 exists - anymatch

// select exists(select * from user where name='小海')
// 有沒有名字叫“小海”的使用者
boolean exists0 = userList.stream()
                        .anyMatch(e -> e.getName().equals("小海"));

// select not exists(select * from user where name='小海')
// 是不是沒有名字叫“小海”的使用者
boolean exists1 = userList.stream()
                        .noneMatch(e -> e.getName().equals("小海"));

// 是不是所有使用者年齡都小於10歲
boolean exists2 = userList.stream()
                        .allMatch(e -> e.getAge() < 10);

收集操作 collect

收集操作就是遍歷 stream 中的元素,並進行累加處理,即歸約 reduction

歸約的定義:

A reduction operation (also called a fold) takes a sequence of input elements and combines them into a single summary result by repeated application of a combining operation, such as finding the sum or maximum of a set of numbers, or accumulating elements into a list.

前面提到的 max() min() count() reduce() 都屬於 reduction operation

collect() 又和前面這幾種歸約操作有所區別,它是 Mutable reduction 動態歸約

動態歸約的定義:

A mutable reduction operation accumulates input elements into a mutable result container, such as a Collection or StringBuilder, as it processes the elements in the stream

區別:動態歸約將結果放進 Collection StringBuilder 這樣的動態容器中,所以稱為動態歸約。

Stream 介面提供了兩個 collect() 方法

<R> R collect(Supplier<R> supplier,
                BiConsumer<R, ? super T> accumulator,
                BiConsumer<R, R> combiner);

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

我們只需理解了第一個方法,第二個方法就手到擒來了

理解第一個 collect 方法,強烈建議閱讀文件 動態歸約的定義,下面只簡單的介紹一下它

三個引數:

  • 供給者 supplier:負責提供動態容器,例如 Collectors、StringBuilder
  • 累加器 accumulator:負責將流中的元素做累加處理
  • 合併者 combiner :負責將兩個容器的元素合併在一起

在序列流中,combiner 根本沒有執行,所以隨便寫點啥滿足引數物件就行。
如果說序列流是單執行緒,那麼並行流就是多執行緒了

舉個例子:


     ArrayList<String> strings = new ArrayList<>();
     for (T element : stream) {
         strings.add(element.toString());
     }
    // 等同於
    ArrayList<String> strings = stream.collect(() -> new ArrayList<>(),
                                                (c, e) -> c.add(e.toString()),
                                                (c1, c2) -> c1.addAll(c2));

與其傳遞三個引數這麼麻煩,還不如直接傳遞一個物件呢!
這就是第二個 collect() 方法的由來,使用收集器 Collector 來替代三個引數

實際上,我們一般不需要自己建立 Collector 物件,Java8 提供了一個 Collectors 類,專門提供收集器 Collector 物件。畢竟我們平時能夠使用到的收集操作也就那幾種:轉為集合物件、分組、統計。

下面以例子演示


在初看 stream 操作的時候,我被什麼建立、中間操作、終止操作、不會改變原物件給弄暈了,我根本不關心這些,我的第一想法是怎麼將操作後的資料匯出來,重新變成集合物件。

toCollection

不使用收集器的情況下:

List<User> subUserList1 = userList.stream()
                .filter(e -> e.getAge() < 10)
                .filter(e -> e.getGender() == "男")
                .collect(() -> new ArrayList<>(),
                        (c, e) -> c.add(e),
                        (c1, c2) -> c1.addAll(c2));

在 collect() 方法第二個引數累加器 accumulator (c, e) -> c.add(e) 這裡,對流中元素進行了遍歷,所以可以把流中元素新增到任意的集合容器中,List、Set、Map 等等

使用 Collectors 工具類提供的收集器:

// toList()
List<User> list = userList.stream()
                .filter(e -> e.getAge() < 10)
                .filter(e -> e.getGender() == "男")
                .collect(Collectors.toList());

// toSet()
Set<User> set = userList.stream()
                .filter(e -> e.getAge() < 10)
                .filter(e -> e.getGender() == "男")
                .collect(Collectors.toSet());

// toCollection(),想要返回什麼容器,就 new 一個
ArrayList<User> collection = userList.stream()
                .filter(e -> e.getAge() < 10)
                .filter(e -> e.getGender() == "男")
                .collect(Collectors.toCollection(
                    () -> new ArrayList<>()
                ));

這裡插播一條新聞:如何將流轉為陣列?

Stream 提供了方法 toArray()

Object[] toArray();
<A> A[] toArray(IntFunction<A[]> generator);

小試牛刀:

Object[] nameArray = userList.stream()
        .map(e -> e.getName())
        .toArray();
Arrays.stream(nameArray)
        .forEach(System.out::println);
// 轉為 User 物件陣列
User[] users = userList.stream()
        .filter(e -> e.getGender() == "女")
        .toArray(User[]::new);
Arrays.stream(users)
        .forEach(System.out::println);

toStringBuilder

不使用收集器的情況下:

StringBuilder joinName = userList.stream()
                .map(e -> e.getName())
                .collect(StringBuilder::new,
                        (s, e) ->  s = s.length() > 0 ? s.append("-" + e) : s.append(e),
                        (s1, s2) -> s1.append(s2)
                );

誰能告訴我在Java中怎麼單獨使用三元運算子?s = s.length() > 0 ? s.append("-" + e) : s.append(e) 我想把 s = 省略掉,但 Java 中不行

使用 Collectors 類提供的收集器:

String joinName1 = userList.stream()
                .map(e -> e.getName())
                .collect(Collectors.joining());

String joinName2 = userList.stream()
    .map(e -> e.getName())
    .collect(Collectors.joining("-"));

String joinName3 = userList.stream()
    .map(e -> e.getName())
    .collect(Collectors.joining("-", "[", "]"));

至於 Collectors.joining() 引數分別代表什麼含義,看一下它們的引數名稱,就明白了

public static Collector<CharSequence, ?, String> joining(CharSequence delimiter,    // 分隔符
                                                             CharSequence prefix,   // 字首
                                                             CharSequence suffix)   // 字尾

toMap

在 Collectors 中一共有3個 toMap(),它們用來處理不同的問題

兩個引數的 toMap

 Collector<T, ?, Map<K,U>> toMap(Function<? super T, ? extends K> keyMapper,
                                    Function<? super T, ? extends U> valueMapper) {
        return toMap(keyMapper, valueMapper, throwingMerger(), HashMap::new);
    }

引數 keyMapper 用來獲取key;valueMapper 用來獲取 value

它的內部呼叫了四個引數的 toMap() 方法

例子

Map<Integer, User> map1 = userList.stream()
    .collect(Collectors.toMap(e -> e.getId(), Function.identity()));
System.out.println(map1);
// Function.identity() 等價於 e -> e

// select id, name, gender from user
Map<Integer, Map<String, Object>> map2 = userList.stream()
    .collect(Collectors.toMap(e -> e.getId(), e -> {
        Map<String, Object> map = new HashMap<>();
        map.put("gender", e.getGender());
        map.put("name", e.getName());
        map.put("id", e.getId());
        return map;
    }));
System.out.println(map2);

你:如果 key 衝突了咋辦?
Java8:你想咋辦就咋辦

三個引數的 toMap

Collector<T, ?, Map<K,U>> toMap(Function<? super T, ? extends K> keyMapper,
                                    Function<? super T, ? extends U> valueMapper,
                                    BinaryOperator<U> mergeFunction) {
        return toMap(keyMapper, valueMapper, mergeFunction, HashMap::new);
    }

第三個引數 mergeFunction 就是用來處理 key 鍵衝突的

內部也是呼叫了四個引數的 toMap() 方法

例子

// 如果 key 衝突,那麼將衝突的 value 值拼接在一起
Map<String, String> map3 = userList.parallelStream()
                .collect(Collectors.toMap(
                    e -> e.getGender(), 
                    e -> e.getName(), 
                    (o1, o2) -> o1 + ", " + o2
                    )
                );
System.out.println(map3);

你:我想自己 new 一個 Map 物件

四個引數的 toMap

Collector<T, ?, M> toMap(Function<? super T, ? extends K> keyMapper,
                            Function<? super T, ? extends U> valueMapper,
                            BinaryOperator<U> mergeFunction,
                            Supplier<M> mapSupplier) 

引數 mapSupplier 用來提供返回容器

例子

LinkedHashMap<String, String> map4 = userList.parallelStream()
                .collect(Collectors.toMap(e -> e.getGender(), e -> e.getName(), (o1, o2) -> o1 + ", " + o2, LinkedHashMap::new));
System.out.println(map4);

reducing

單引數和兩引數的 reducing()

Collector<T, ?, Optional<T>> reducing(BinaryOperator<T> op)
Collector<T, ?, U> reducing(U identity, Function<? super T,? extends U> mapper, BinaryOperator<U> op)

以例子具體解釋這兩個方法

Optional<String> names1 = userList.stream()
                .map(User::getName)
                .collect(Collectors.reducing((e1, e2) -> e1 + "," + e2));
System.out.println(names1.get());

// 等同於
String names2 = userList.stream()
            .collect(Collectors.reducing(
                    "", (e) -> e.getName(), (e1, e2) -> e1 + "," + e2)
                    );
System.out.println(names2);

輸出結果:

小明,小青,小海,阿刁,小陽,小強,小帥,小云
,小明,小青,小海,阿刁,小陽,小強,小帥,小云

引數 identity 表示返回結果的初始值

三引數的 reducing()

reducing(U identity, Function<? super T,? extends U> mapper, BinaryOperator<U> op)

identity 是初始值,mapper 會對元素先進行一次處理,然後 op 對元素進行歸約操作

注意: 返回型別要和引數 identity 的一致。
你也許會納悶,為什麼有的返回一個 Optional<String> 型別資料,而有的就返回了 String
因為含有引數 identity 的 reduing 方法中返回值有初始值,也就是 identity,所以不會出現空的情況

下面Collectors 提供的一些常用歸約收集器

// minBy、maxBy
Optional<User> minAgeUser = userList.stream()
                .collect(Collectors.minBy((o1, o2) -> o1.getAge() - o2.getAge()));

// counting
Long count = userList.stream()
    .collect(Collectors.counting());

// summingInt、summingLong、summingDouble、
Integer sumAge = userList.stream()
    .collect(Collectors.summingInt(User::getAge));

// averagingInt、averagingLong、averagingDouble
// 平均值內部是總值/數量,所以返回值是浮點數 dobule
Double avgAge = userList.stream()
    .collect(Collectors.averagingInt(User::getAge));

你也許覺得每次都要執行一遍 minBy、maxBy、counting、summingXxx、averagingXxx 這些太麻煩了,有沒有一次執行就獲取所有這些方法結果?
有的。這就是 summarizingXxx

Collector<T, ?, IntSummaryStatistics> summarizingInt(ToIntFunction<? super T> mapper)
Collector<T, ?, LongSummaryStatistics> summarizingLong(ToLongFunction<? super T> mapper)
Collector<T, ?, DoubleSummaryStatistics> summarizingDouble(ToDoubleFunction<? super T> mapper)

這裡不演示了,實際上你看一下 XxxSummaryStatistics 這些類就明白了,比如

public class IntSummaryStatistics implements IntConsumer {
    private long count;
    private long sum;
    private int min = Integer.MAX_VALUE;
    private int max = Integer.MIN_VALUE;
    ...
}

group by

最最激動人心的時候到了,我們要使用分組了!!!

Map<String, List<User>> map = userList.stream()
                .collect(Collectors.groupingBy(User::getGender));

SQL 中的 group by 結果集中只能包含分組欄位和聚合函式計算結果,這段程式碼比它更加全面

我們使用如下語句輸出結果

map.keySet().stream()
        .forEach((e) -> {
            System.out.println(e + "=" + map.get(e));
        });

顯示結果:

女=[User{id=102, name='小青', age=12, gender='女', province='寧夏回族自治區', city='銀川市'}, User{id=108, name='阿刁', age=18, gender='女', province='西藏自治區', city='拉薩市'}, User{id=104, name='小陽', age=9, gender='女', province='新疆維吾爾自治區', city='烏魯木齊市'}, User{id=107, name='小云', age=15, gender='女', province='河北省', city='石家莊市'}]
男=[User{id=101, name='小明', age=10, gender='男', province='青海省', city='西寧市'}, User{id=103, name='小海', age=8, gender='男', province='西藏自治區', city='拉薩市'}, User{id=105, name='小強', age=14, gender='男', province='陝西省', city='西安市'}, User{id=106, name='小帥', age=15, gender='男', province='河北省', city='石家莊市'}]

它真的分組了!!這是真正的分組

那怎麼對分組中的元素進行操作呢,像 SQL 那樣??

完全不用擔心,Collectors 提供了三個 groupBy 方法返回分組收集器

Collector<T, ?, Map<K, List<T>>> groupingBy(Function<? super T,? extends K> classifier)
    
Collector<T, ?, Map<K, D>> groupingBy(Function<? super T,? extends K> classifier, 
                                      Collector<? super T,A,D> downstream)
    
Collector<T, ?, M> groupingBy(Function<? super T,? extends K> classifier, 
                              Supplier<M> mapFactory, 
                              Collector<? super T,A,D> downstream)

讓我們放飛想象的翅膀,思考一下這幾個引數分別有什麼用。

downstream ?有 down 就表示有 up。那麼誰是 upstream,很明顯是 userList.stream,那麼 downstream 就是分組集合的流嘍。猜測 downstream 收集器是對分組中的元素進行歸約操作的,就像是分組 SQL 語句欄位中的聚合操作一樣。

// select gender, count(*) from user group by gender
Map<String, Long> map2 = userList.stream()
                .collect(Collectors.groupingBy(User::getGender, Collectors.counting()));
System.out.println(map2);

輸出結果確實不出所料!這就是證明引數 downstream 確實是分組集合元素的收集器。

Supplier<M> mapFactory 這函式式介面方法不會有引數傳入,所以不會操作集合元素;它只是返回一個變數。同志們,注意觀察三個方法返回值,前二者都指定了 Map 作為歸約操作的返回型別,而第三個要我們自己定義,使用 mapFactory 提供返回的資料容器

兩引數的 groupingBy 方法其實是呼叫了三引數的 groupingBy 方法(而單引數 groupingBy 呼叫了兩引數的 groupingBy)

    Collector<T, ?, Map<K, D>> groupingBy(Function<? super T, ? extends K> classifier,
                                          Collector<? super T, A, D> downstream) {
        return groupingBy(classifier, HashMap::new, downstream);
    }

groupingBy 經常使用 Collectors.mapping() 處理分組集合

Map<String, List<Map<String, Object>>> map4 = userList.stream()
    .collect(Collectors.groupingBy(
        User::getGender,
        Collectors.mapping(e -> {
            Map<String, Object> m = new HashMap<>();
            m.put("name", e.getName());
            m.put("id", e.getId());
            return m;
        }, Collectors.toList())
    ));
System.out.println(map4);

輸出結果:

{女=[{name=小青, id=102}, {name=阿刁, id=108}, {name=小陽, id=104}, {name=小云, id=107}], 男=[{name=小明, id=101}, {name=小海, id=103}, {name=小強, id=105}, {name=小帥, id=106}]}

partitionBy

實際上也是分組,只不過 partitionBy 是按照布林值(真假)來分組

Collector<T, ?, Map<Boolean, List<T>>> partitioningBy(Predicate<? super T> predicate) {
    return partitioningBy(predicate, toList());
}

Collector<T, ?, Map<Boolean, D>> partitioningBy(Predicate<? super T> predicate,
                                                    Collector<? super T, A, D> downstream)

例子:大於10歲為一組,小於等於10的為一組

Map<Boolean, List<User>> map1 = userList.stream()
                .collect(Collectors.partitioningBy(e -> e.getAge() > 10));

例子:統計大於10歲的有多少人,小於等於10歲的有多少人

Map<Boolean, Long> map2 = userList.stream()
            .collect(Collectors.partitioningBy(e -> e.getAge() > 10, Collectors.counting()));

第二個引數 downstream 用來處理分組集合

結語

Java8 提供的 stream 幾乎是窮盡了所有集合元素能有的操作,起碼是窮盡了我腦海裡對集合元素操作的所有想象

這篇文章也列舉了 stream 絕大部分的功能,儘量寫得通俗易懂,但讀者理解起來可能還是有模糊的地方,這時建議大家參考 Java8 API 官方文件 ,多做幾個 Demo 加深理解

不要過度使用

stream 是為了方便集合操作,簡化程式碼而推出的,提升程式碼執行效率並不是它的目的。

雖然,並行流會對程式碼的執行效率有較大的提升(尤其是資料量非常大的時候),但也依賴於計算機的CPU配置。

Stream 能實現的功能,for 迴圈都能實現,只是 Stream 程式碼一般比較簡潔,可讀性強。但在某些情況下,使用 for 迴圈要比 Stream 要簡潔程式碼邏輯清晰

舉個例子:

    private List<Order> orderList = Arrays.asList(
            new Order(103),
            new Order(106),
            new Order(107),
            new Order(104),
            new Order(102),
            new Order(103),
            new Order(102),
            new Order(101),
            new Order(104),
            new Order(102),
            new Order(105)
    );
    // 現根據 userId 設定 Order 物件的 name 屬性
    
    // 使用 stream
    List<Order> newOrderList = orderList.stream()
                .map(o -> userList.stream()
                        .filter(u -> u.getId() == o.getUserId())
                        .findFirst()
                        .map(u -> {
                            o.setUserName(u.getName());
                            return o;
                        })
                        .orElse(o))
                .collect(Collectors.toList());
    newOrderList.stream().forEach(System.out::println);

    // 使用 for 迴圈
    for (Order o : orderList) {
        for (User u : userList) {
            if (o.getUserId() == u.getId()) {
                o.setUserName(u.getName());
                break;
            }
        }
    }
    orderList.stream().forEach(System.out::println);

在這個例子中,使用 for 迴圈要比 使用 stream 乾淨利落的多,程式碼邏輯清晰簡明,可讀性也比 stream 好