大家好,我是风筝。大众号「古时的风筝」,专心于后端技能,尤其是 Java 及周边生态。文章会收录在 JavaNewBee 中,更有 Java 后端常识图谱,从小白到大牛要走的路都在里边。

大家好,我是风筝

由于最近做了一个小需求,数据量不大,功用也比较简单,但是核算维度十分多,大部分的核算逻辑其实都能够直接写 SQL 完成,但是那样的话功用就太差了,所以终究采用了在内存中直接核算,这时候 Stream 就有大用处了。

Java Stream 是 JDK 8 开端供给的一种函数式风格的调集操作办法。我之前写过一篇 Java Stream 的文章 – 8000字长文让你彻底了解 Java 8 的 Lambda、函数式接口、Stream 用法和原理,在社区获得了超越 500 个赞,阐明大家仍是很喜欢用 Stream 的。

Java Stream 实用特性:排序、分组和 teeing(上一篇500赞)

上一篇主要介绍了一些基础用法,这一篇主要就介绍三个功用,排序、分组和 teeing,teeing 是 JDK 12 才出现的。

排序

底子数据类型排序

底子数据类型便是字符串、整型、浮点型这些,也便是要排序的列表中的元素都是这些底子类型的,比方 List<Integer>的。

下面就用一个整型列表举例阐明。

正序排序

正序排序,也能够叫做依照天然次序排序,对于整型来说便是从小到大的。

List<Integer> integerList = new ArrayList<>();
for (int i = 0; i < 5; i++) {
  integerList.add(i);
}
List<Integer> collect = integerList.stream()
  .sorted()
  .collect(Collectors.toList());
System.out.println(collect);

输出成果是 [0, 1, 2, 3, 4],这很简单没什么好说的。

倒序排序

List<Integer> integerList = new ArrayList<>();
for (int i = 0; i < 5; i++) {
  integerList.add(i);
}
List<Integer> collect2 = integerList.stream()
  .sorted(Comparator.reverseOrder())
  .collect(Collectors.toList());
System.out.println(collect2);

倒序排便是从大到小排序,也很简单在 sorted()办法中添加 Comparator.reverseOrder() 就能够了。

sorted() 办法接纳的参数是Comparator 函数式接口,在 8000字长文让你彻底了解 Java 8 的 Lambda、函数式接口、Stream 用法和原理 这篇文章清楚的讲了函数式接口和办法引用,能够翻过去看看。

非底子类型实体排序

底子类型的列表排序很简单,但是在实际项目中用到的状况不太多,常常用到的仍是咱们自界说类型的排序,比方项目中有一个用户实体、一个订单实体、一个产品实体等。

首先定一个Product实体类:

import lombok.Data;
/**
 * @author fengzheng
 */
@Data
public class Product {
    /**
     * 仅有标明
     */
    private Integer id;
    /**
     * 所属类别
      */
    private Integer type;
    /**
     * 商品名称
     */
    private String name;
    /**
     * 价格
      */
    private Double price;
}

按某一个字段排序

对应到我上面界说的这个实体,能够是依照 id 排序,或许依照 price排序。

正序排序

假定依照 price从小到大排序,也便是依照价格由低到高排序。

对应到 SQL 上,能够表明成这样的。

select * from product order by price asc

那用 Stream 完成呢?

List<Product> productList = initProductList();
List<Product> collect = productList.stream()
  .sorted(Comparator.comparing(Product::getPrice))
  .collect(Collectors.toList());

等价于

List<Product> collect = productList.stream()
  .sorted((x,y) -> x.getPrice().compareTo(y.getPrice()))
  .collect(Collectors.toList());

等价于

Comparator<Product> comparator = new Comparator<Product>() {
  @Override
  public int compare(Product p1, Product p2) {
    return p1.getPrice().compareTo(p2.getPrice());
  }
};
List<Product> collect = productList.stream()
  .sorted((p1, p2) -> comparator.compare(p1, p2))
  .collect(Collectors.toList());

这里边主要由咱们供给自界说的便是函数式接口 Comparator,凡是完成了 compare () 办法的都能够。

上面咱们自界说的这个 comparator,重载了 compare办法。compare 办法的回来值规矩:

  1. 前者小于后者,回来 -1;
  2. 前者大于后者,回来 1;
  3. 前者等于后者,回来 0;

所以能够理解为,假如 compare 回来的是 1, Stream 就会交换两个实体的方位。所以这样一来,倒序排序就很好整了。

倒序排序

能够这样写,运用 reversed() 办法

List<Product> collect = productList.stream()
  .sorted(Comparator.comparing(Product::getPrice).reversed())
  .collect(Collectors.toList());

或许能够

List<Product> collect = productList.stream()
	.sorted(Comparator.comparing(Product::getPrice,Comparator.reverseOrder()))
  .collect(Collectors.toList());

还能够直接直接运用compare ,倒序排序就简单了,略微改一下就好了。

直接用 Lambda 表达式的写法

List<Product> collect = productList.stream()
  .sorted((x,y) -> y.getPrice().compareTo(x.getPrice()))
  .collect(Collectors.toList());

等价于,抽取出自界说 Comparator的办法

Comparator<Product> comparator = new Comparator<Product>() {
            @Override
            public int compare(Product p1, Product p2) {
                return p2.getPrice().compareTo(p1.getPrice());
            }
};
List<Product> collect = productList.stream()
  .sorted((p1, p2) -> comparator.compare(p1, p2))
  .collect(Collectors.toList());

倒序和正序的差异其实便是将 compare()前后两个元素的方位对调一下。

对于巨细比较的能够直接用 compare()办法,但是有一些状况可能不止这么简单。没有联系,咱们不是能够自界说 Comparator 吗,在 Comparator 重写的 compare 办法中能够参加咱们的排序逻辑,不管多么特殊、多么杂乱,只需回来一个 int 类型的就能够了。

依照多个字段排序

还有一些状况要依照两个甚至多个字段排序,一个主排序,一个次要排序。比方咱们想要先按 type 升序,再按 price 降序。

对应到 SQL 上就像这样

select * from product order by type asc,price desc

那用 Stream 来完成是怎么样的呢?用 thenComparing连接多个要排序的特点。

List<Product> collect = productList.stream().sorted(Comparator.comparing(Product::getType).thenComparing(Product::getPrice, Comparator.reverseOrder())).collect(Collectors.toList());

或许还能够界说两个 Comparator

Comparator<Product> typeComparator = new Comparator<Product>() {
  @Override
  public int compare(Product p1, Product p2) {
    return p1.getType().compareTo(p2.getType());
  }
};
Comparator<Product> priceComparator = new Comparator<Product>() {
  @Override
  public int compare(Product p1, Product p2) {
    return p2.getPrice().compareTo(p1.getPrice());
  }
};
List<Product> collect = productList.stream()
  .sorted(typeComparator.thenComparing(priceComparator))
  .collect(Collectors.toList());

怎么样,一点难度都没有吧。

分组

除了排序,还有一个十分有用并且常常会用的功用便是分组功用。分组功用是 collect()办法供给的功用,回来值是一个字典类型。

依据 type 进行分组

对应到 SQL 中便是下面这样

select * from product group by type

用 Stream 来完成呢,便是下面这姿态

Map<Integer, List<Product>> map = productList.stream()
  .collect(Collectors.groupingBy(Product::getType));

终究生成的目标是一个 Map 类型,key 是用来作为分组依据的字段值,value 是一个列表,也便是同一组的目标调集。在这个比方中,key 便是 product 目标的 type 特点,value 便是 type 相同的 Product 目标的调集。

假如仅仅求出每一个组所包含的目标个数,能够这样完成,不必遍历 Map 这么费事。

Map<Integer, Long> map = productList.stream()
  .collect(Collectors.groupingBy(Product::getType, Collectors.counting()));

依据两个或多个字段分组

有时候咱们可能会依据不止一个字段进行分组,比方想依照类别相同且价格相同进行分组。

Map<String, List<Product>> map = productList.stream()
                .collect(Collectors.groupingBy(p -> p.getType() + "|" + p.getPrice()));

等价于,将分组依据单独抽取出一个办法,这样就能够参加比较杂乱的逻辑了,终究回来的是一个字符串。

Map<String, List<Product>> map = productList.stream()
   .collect(Collectors.groupingBy(p -> buildGroupKey(p)));
private static String buildGroupKey(Product p) {
   return p.getType() + "|" + p.getPrice();
}

为什么两个字段之间要加一个分隔符呢,这是由于有些状况咱们还会用到分组依据中的某一个字段,参加分隔符之后便利拆分字符串。当然了,也能够拿到这个分组下的恣意一个元素获取。

嵌套分组

上面的依据多个字段分组是把多个字段当做同一等级并且的联系处理,还有一些时候呢,咱们想要先按一个字段分组,再分组中再按另一个字段分组,这样就形成了一个嵌套联系,比方先按 type 分组,再按 price 分组,这就适当所以一个二维字典(两个层级)。

Map<Integer, Map<Double, List<Product>>> map = productList.stream()
  .collect(Collectors.groupingBy(Product::getType, Collectors.groupingBy(Product::getPrice)));

经过回来值类型就能够看出来是怎么样的一个层级联系。

teeing()

这是 JDK 12 才出来的办法,所以要用这个办法,比方在 JDK12 以上才行。它的作用是对两个收集器(Collectors)的成果进行处理。上面的比方中,求出最高价格和最低价格的,并输出为一个字符串,将两个价格用 ~符号连接。

String result = productList.stream().collect(Collectors.teeing(
  Collectors.minBy(Comparator.comparing(Product::getPrice)),
  Collectors.maxBy(Comparator.comparing(Product::getPrice)),
  (min, max) -> {
    return min.get().getPrice() + "~" + max.get().getPrice();
  }
));
System.out.println(result);

终究得到的成果是一个字符串,打印如下,测试数据没有做小数位约束。

4.347594572793579~89.43160979811124

终究的回来类型依据teeing() 办法的终究一个参数的回来成果而定。 min 和 max 这两个参数便是前两个收集器 Collectors.minByCollectors.maxBy的回来成果,由于回来类型是 Optional ,所以再取值的时候要加上 get

总结

Stream 供给了很丰厚的 API ,最大的优点是让咱们能够少写许多代码,熟练掌握之后,能够在一些对应的场景快速完成咱们想要的逻辑。

有同学说,不可啊,又是 filter 、又是 collect、又是 Collectors ,底子记不住啊。没联系,记不住也正常,它原本便是一个东西,咱们其实只需知道它能够完成什么功用,具体的用法能够随用随查吗。这不,我的这两篇文章就能够放进收藏夹里,什么时候用,什么时候打开查一下就好了。

下次碰到类似的场景,记得用 Stream 试一下吧。

各位假如觉得有用的话,给个赞吧!还能够重视我的大众号「古时的风筝」,等着你来呦