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 好。

posted @ 2020-01-16 11:13  刘一二  阅读(1022)  评论(0编辑  收藏  举报