前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >酷炫的 Stream API 最佳指南

酷炫的 Stream API 最佳指南

作者头像
码哥字节
发布2020-03-24 15:18:07
1.8K0
发布2020-03-24 15:18:07
举报
文章被收录于专栏:Java 技术栈

Java 8 带来一大新特性 Lambda 表达式流(Stream),当流与 Lambda 表达式结合使用,代码将变得相当骚气与简洁。

超级大招,释放代码

假如有一个需求,需要对数据库查询的发票信息进行处理:

  1. 取出金额小于 10000 的发票。
  2. 对筛选出来的数据排序。
  3. 获取排序后的发票销方名称。

发票 Model

代码语言:javascript
复制
@Builder
@Data
public class Invoice implements Serializable {
    /**
     * 销方名称
     */
    private String saleName;
    /**
     * 是否作废
     */
    private Boolean cancelFlag;
    /**
     * 开票金额
     */
    private BigDecimal amount;
    /**
     * 发票类型
     */
    private Integer type;
    /**
     * 明细条数
     */
    private Integer detailSize;
}

我们使用传统的方式实现,在之前我们初始化测试数据

代码语言:javascript
复制
public class StreamTest {

    private List<Invoice> invoiceList;

    @Before
    public void initData() {
        Invoice invoice = Invoice.builder().amount(BigDecimal.valueOf(100.02)).cancelFlag(false).detailSize(10)
                .saleName("广西制药").type(1).build();
        Invoice invoice2 = Invoice.builder().amount(BigDecimal.valueOf(89032478.9)).cancelFlag(false).detailSize(2)
                .saleName("深圳电子科技").type(1).build();
        Invoice invoice3 = Invoice.builder().amount(BigDecimal.valueOf(2077777889)).cancelFlag(true).detailSize(6)
                .saleName("宇宙心空").type(1).build();
        Invoice invoice4 = Invoice.builder().amount(BigDecimal.valueOf(356.8)).cancelFlag(false).detailSize(10)
                .saleName("孟达餐厅").type(2).build();
        Invoice invoice5 = Invoice.builder().amount(BigDecimal.valueOf(998.88)).cancelFlag(false).detailSize(0)
                .saleName("网红餐厅").type(2).build();
        Invoice invoice6 = Invoice.builder().amount(BigDecimal.valueOf(9009884.09)).cancelFlag(false).detailSize(1)
                .saleName("机动车").type(3).build();
        invoiceList = Stream.of(invoice, invoice2, invoice3, invoice4, invoice5, invoice6).collect(Collectors.toList());
        System.out.println("原始数据:" + invoiceList.toString());
    }

Java8 之前的实现方式

代码语言:javascript
复制
/**
     * 筛选出金额小于 10000 的发票,根据金额排序,获取排序后的销方名称列表
     */
    @Test
    public void testJava7() {
        ArrayList<Invoice> lowInvoiceList = new ArrayList<>();
        //筛选出 金额小于 10000 的发票
        for (Invoice invoice: invoiceList) {
            if (invoice.getAmount().compareTo(BigDecimal.valueOf(10000)) < 0) {
                lowInvoiceList.add(invoice);
            }
        }
        // 对筛选出的发票排序
        lowInvoiceList.sort(new Comparator<Invoice>() {
            @Override
            public int compare(Invoice o1, Invoice o2) {
                return o1.getAmount().compareTo(o2.getAmount());
            }
        });
        // 获取排序后的销方名字
        ArrayList<String> nameList = new ArrayList<>();
        for (Invoice invoice : lowInvoiceList) {
            nameList.add(invoice.getSaleName());
        }

    }

Java8 之后的骚气操作,一气呵成。再也不用加班写又臭又长的代码了

代码语言:javascript
复制
@Test
public void testJava8() {
  List<String> nameList = invoiceList.stream()
    .filter(item -> item.getAmount().compareTo(BigDecimal.valueOf(10000)) < 0)// 过滤数据
    .sorted(Comparator.comparing(Invoice::getAmount))// 对金额升序排序
    .map(Invoice::getSaleName)//提取名称
    .collect(Collectors.toList());//转换成list

}

一套龙服务的感觉,一气呵成送你上青天。大大减少了代码量。

现在又来一个需求

对查询出来的发票数据进行分类,返回一个 Map<Integer, List> 的数据。

回顾下 Java7 的写法,有没有一种我擦,这也太麻烦了。还能不能早点下班回去抱女朋友。

代码语言:javascript
复制
@Test
public void testGroupByTypeJava7() {
  HashMap<Integer, List<Invoice>> groupMap = new HashMap<>();
  for (Invoice invoice : invoiceList) {
    //存在则追加
    if (groupMap.containsKey(invoice.getType())) {
      groupMap.get(invoice.getType()).add(invoice);
    } else {
      // 不存在则初始化添加
      ArrayList<Invoice> invoices = new ArrayList<>();
      invoices.add(invoice);
      groupMap.put(invoice.getType(), invoices);
    }
  }
  System.out.println(groupMap.toString());
}

接着就是我们利用 stream 的骚操作代码实现上面的需求

groupingBy 分组

代码语言:javascript
复制
@Test
public void testGroupByTypeJava8() {
  Map<Integer, List<Invoice>> groupByTypeMap = invoiceList.stream().collect(Collectors.groupingBy(Invoice::getType));
}

就是这么简单粗暴,一行代码直捣黄龙。

什么是 Stream?

Stream(流)是一个来自数据源的元素队列并支持聚合操作,它不是数据结构并不保存数据,主要目的是在于计算。

元素是特定类型的对象,形成一个队列。Java中的Stream并不会存储元素,而是按需计算。数据源流的来源。可以是集合,数组,I/O channel, 产生器 generator 等。聚合操作类似SQL语句一样的操作,比如filter, map, reduce, find, match, sorted等。和以前的Collection操作不同,Stream操作还有两个基础的特征:

  • Pipelining:中间操作都会返回流对象本身。这样多个操作可以串联成一个管道,如同流式风格(fluent style)。这样做可以对操作进行优化,比如延迟执行(laziness)和短路( short-circuiting)。
  • 内部迭代:以前对集合遍历都是通过Iterator或者For-Each的方式, 显式的在集合外部进行迭代,这叫做外部迭代。Stream提供了内部迭代的方式,通过访问者模式(Visitor)实现。

如何生成流

主要有五种方式

1. 通过集合生成

代码语言:javascript
复制
Collection<String> collection = Arrays.asList("a", "b", "c");
Stream<String> streamOfCollection = collection.stream();

2.通过数组生成

代码语言:javascript
复制
int[] intArr = new int[]{1, 2, 3, 4, 5};
IntStream stream = Arrays.stream(intArr);

通过Arrays.stream方法生成流,并且该方法生成的流是数值流【即IntStream】而不是Stream<Integer>。补充一点使用数值流可以避免计算过程中拆箱装箱,提高性能。

Stream API提供了mapToInt、mapToDouble、mapToLong三种方式将对象流【即Stream】转换成对应的数值流,同时提供了boxed方法将数值流转换为对象流

3. 通过值生成

代码语言:javascript
复制
Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5);

通过Stream的of方法生成流,通过Stream的empty方法可以生成一个空流

4. 通过文件生成

代码语言:javascript
复制
Stream<String> lines = Files.lines(Paths.get("data.txt"), Charset.defaultCharset());

通过Files.line方法得到一个流,并且得到的每个流是给定文件中的一行

5. 通过函数生成,iterate和generate两个静态方法从函数中生成流

iterator: iterate方法接受两个参数,第一个为初始化值,第二个为进行的函数操作,因为iterator生成的流为无限流,通过limit方法对流进行了截断,只生成5个偶数

代码语言:javascript
复制
Stream<Integer> stream = Stream.iterate(0, n -> n + 2).limit(5);

generator: 接受一个参数,方法参数类型为Supplier,由它为流提供值。generate生成的流也是无限流,因此通过limit对流进行了截断

代码语言:javascript
复制
Stream<Double> stream = Stream.generate(Math::random).limit(5);

流的操作类型

主要分为两种类型

1. 中间操作

一个流可以后面跟随零个或多个中间操作。其目的主要是打开流,做出某种程度的数据映射/过滤,然后返回一个新的流,交给下一个操作使用。

这类操作都是惰性化的,仅仅调用到这类方法,并没有真正开始流的遍历,真正的遍历需等到终端操作时,常见的中间操作有下面即将介绍的filter、map等

2. 终端操作

一个流有且只能有一个终端操作,当这个操作执行后,流就被关闭了,无法再被操作,因此一个流只能被遍历一次,若想在遍历需要通过源数据在生成流。终端操作的执行,才会真正开始流的遍历。如下面即将介绍的 count、collect 等。

中间操作 API

filter筛选

代码语言:javascript
复制
Stream<Invoice> invoiceStream = invoiceList.stream().filter(invoice -> invoice.getDetailSize() < 10);

distinct去除重复元素

代码语言:javascript
复制
List<Integer> integerList = Arrays.asList(1, 1, 2, 3, 4, 5);
Stream<Integer> stream = integerList.stream().distinct();

limit返回指定流个数

代码语言:javascript
复制
Stream<Invoice> invoiceStream = invoiceList.stream().limit(3);

通过limit方法指定返回流的个数,limit的参数值必须>=0,否则将会抛出异常

skip跳过流中的元素

代码语言:javascript
复制
 List<Integer> integerList = Arrays.asList(1, 1, 2, 3, 4, 5);
 Stream<Integer> stream = integerList.stream().skip(2);

通过skip方法跳过流中的元素,上述例子跳过前两个元素,所以打印结果为2,3,4,5,skip的参数值必须>=0,否则将会抛出异常。

map流映射

所谓流映射就是将接受的元素映射成另外一个元素

代码语言:javascript
复制
List<String> stringList = Arrays.asList("Java 8", "Lambdas",  "In", "Action");
Stream<Integer> stream = stringList.stream().map(String::length);

通过 map 方法可以完成映射,该例子完成中String -> Integer的映射,之前上面的例子通过 map 方法完成了 Invoice -> String 的映射

flatMap流转换

将一个流中的每个值都转换为另一个流

代码语言:javascript
复制
List<String> wordList = Arrays.asList("Hello", "World");
        List<String> strList = wordList.stream()
                .map(w -> w.split(""))// 将元素根据 空格分隔字符的Stream<String[]>
                .flatMap(Arrays::stream)// 将Stream<String[]> 转换成 Stream<String>
                .distinct() //去重
                .collect(Collectors.toList());
        System.out.println(strList.toString());

map(w -> w.split(" "))的返回值为Stream<String[]>,我们想获取Stream<String>,可以通过flatMap方法完成Stream ->Stream的转换。所以最后打印的结果是 [H, e, l, o, W, r, d]

元素匹配

  1. allMatch匹配所有
代码语言:javascript
复制
if (invoiceList.stream().allMatch(Invoice::getCancelFlag)) {
  System.out.println("发票全是作废");
}
  1. anyMatch匹配其中一个

存在作废发票则打印

代码语言:javascript
复制
if (invoiceList.stream().anyMatch(Invoice::getCancelFlag)) {
  System.out.println("存在作废发票");
}

等同于

代码语言:javascript
复制
for (Invoice invoice : invoiceList) {
  if (invoice.getCancelFlag()) {
    System.out.println("存在作废发票");
    break;
  }
}
  1. noneMatch全部不匹配
代码语言:javascript
复制
List<Integer> integerList = Arrays.asList(1, 2, 3, 4, 5);
if (integerList.stream().noneMatch(i -> i > 3)) {
    System.out.println("值都小于3");
}

终端操作

统计流中元素个数

  1. 使用 count
代码语言:javascript
复制
long count = invoiceList.stream()
  .filter(item -> item.getAmount().compareTo(BigDecimal.valueOf(10000)) < 0)
  .count();
  1. 使用 counting
代码语言:javascript
复制
long count = invoiceList.stream()
  .filter(item -> item.getAmount().compareTo(BigDecimal.valueOf(10000)) < 0)
  .collect(Collectors.counting());

最后一种统计元素个数的方法在与collect联合使用的时候特别有用

查找

  1. findFirst查找第一个
代码语言:javascript
复制
Optional<Invoice> first = invoiceList.stream()
  .filter(item -> item.getAmount().compareTo(BigDecimal.valueOf(10000)) < 0)
  .findFirst();

通过 findFirst 找到金额小于 10000 的第一个元素

  1. findAny随机查找一个
代码语言:javascript
复制
Optional<Invoice> any = invoiceList.stream()
  .filter(item -> item.getAmount().compareTo(BigDecimal.valueOf(10000)) < 0)
  .findAny();

通过findAny方法查找到其中一个小于 10000 的元素并打印,因为内部进行优化的原因,当找到第一个满足大于三的元素时就结束,该方法结果和findFirst方法结果一样。提供findAny方法是为了更好的利用并行流,findFirst方法在并行上限制更多【本篇文章将不介绍并行流】

reduce将流中的元素组合起来

假设我们对一个集合中的值进行求和

jdk8 之前

代码语言:javascript
复制
int sum = 0;
for (int i : integerList) {
	sum += i;
}

jdk8之后通过reduce进行处理

代码语言:javascript
复制
int sum = integerList.stream().reduce(0, (a, b) -> (a + b));
//还可以用方法引用写
int sum = integerList.stream().reduce(0, Integer::sum);

比如统计发票金额求和

代码语言:javascript
复制
BigDecimal reduce = invoiceList.stream().map(Invoice::getAmount).reduce(BigDecimal.ZERO, (a, b) -> (a.add(b)));

继续使用方法引用简化

代码语言:javascript
复制
BigDecimal reduce = invoiceList.stream().map(Invoice::getAmount).reduce(BigDecimal.ZERO, BigDecimal::add);

reduce 接受两个参数,一个初始值这里是0,一个BinaryOperator<T> accumulator来将两个元素结合起来产生一个新值,

另外reduce方法还有一个没有初始化值的重载方法

获取流中最小最大值

通过min/max获取最小最大值

代码语言:javascript
复制
Optional<BigDecimal> min = invoiceList.stream().map(Invoice::getAmount).min(BigDecimal::compareTo);
Optional<BigDecimal> max = invoiceList.stream().map(Invoice::getAmount).max(BigDecimal::compareTo);

也可以写成

代码语言:javascript
复制
OptionalInt min1 = invoiceList.stream().mapToInt(Invoice::getDetailSize).min();
OptionalInt max1 = invoiceList.stream().mapToInt(Invoice::getDetailSize).max();

min获取流中最小值,max获取流中最大值,方法参数为Comparator<? super T> comparator

通过minBy/maxBy获取最小最大值

代码语言:javascript
复制
invoiceList.stream().map(Invoice::getAmount).collect(Collectors.minBy(BigDecimal::compareTo)).get();

通过reduce获取最小最大值

代码语言:javascript
复制
Optional<BigDecimal> max = invoiceList.stream().map(Invoice::getAmount).reduce(BigDecimal::max);

求和

通过summingInt

代码语言:javascript
复制
Integer sum = invoiceList.stream().collect(Collectors.summingInt(Invoice::getDetailSize));

如果数据类型为double、long,则通过summingDouble、summingLong方法进行求和

通过reduce

代码语言:javascript
复制
Integer sum = invoiceList.stream().map(Invoice::getDetailSize).reduce(0, Integer::sum);

通过sum,最佳写法

代码语言:javascript
复制
//推荐写成
Integer sum = invoiceList.stream().mapToInt(Invoice::getDetailSize).sum();

在上面求和、求最大值、最小值的时候,对于相同操作有不同的方法可以选择执行。可以选择collect、reduce、min/max/sum方法,推荐使用min、max、sum方法。因为它最简洁易读,同时通过mapToInt将对象流转换为数值流,避免了装箱和拆箱操作

通过averagingInt求平均值

代码语言:javascript
复制
Double avg = invoiceList.stream().collect(Collectors.averagingInt(Invoice::getDetailSize));

如果数据类型为double、long,则通过averagingDouble、averagingLong方法进行求平均

对于BigDecimal 则需要先求和再除以总条数

代码语言:javascript
复制
List<BigDecimal> sumList = invoiceList.stream().map(Invoice::getAmount).collect(Collectors.toList());
        BigDecimal average = average(sumList, RoundingMode.HALF_UP);
// 求平均值
public BigDecimal average(List<BigDecimal> bigDecimals, RoundingMode roundingMode) {
  BigDecimal sum = bigDecimals.stream()
    .map(Objects::requireNonNull)
    .reduce(BigDecimal.ZERO, BigDecimal::add);
  return sum.divide(new BigDecimal(bigDecimals.size()), roundingMode);
}

通过summarizingInt同时求总和、平均值、最大值、最小值

代码语言:javascript
复制
IntSummaryStatistics statistics = invoiceList.stream().collect(Collectors.summarizingInt(Invoice::getDetailSize));
double average1 = statistics.getAverage();
int max1 = statistics.getMax();
int min1 = statistics.getMin();
long sum = statistics.getSum();

通过foreach进行元素遍历

代码语言:javascript
复制
invoiceList.forEach(item -> {
  System.out.println(item.getAmount());
});

通过joining拼接流中的元素

代码语言:javascript
复制
String result = invoiceList.stream().map(Invoice::getSaleName).collect(Collectors.joining(", "));

通过groupingBy进行分组

代码语言:javascript
复制
 Map<Integer, List<Invoice>> groupByTypeMap = invoiceList.stream().collect(Collectors.groupingBy(Invoice::getType));

在collect方法中传入groupingBy进行分组,其中groupingBy的方法参数为分类函数。还可以通过嵌套使用groupingBy进行多级分类

代码语言:javascript
复制
Map<String, Map<String, List<RzInvoice>>> = invoiceList.stream().collect(Collectors.groupingBy(Invoice::getType, Collectors.groupingBy(invoice -> {
    if (invoice.getAmount().compareTo(BigDecimal.valueOf(10000)) <= 0) {
        return "low";
    } else if (invoice.getAmount().compareTo(BigDecimal.valueOf(80000)) <= 0) {
        return "mi";
    } else {
        return "high";
    }
})));

首先根据 发票类型分组,再根据开票金额大小分组,返回的数据类型是 Map<String, Map<String, List>>

进阶通过partitioningBy进行分区

特殊的分组,它分类依据是true和false,所以返回的结果最多可以分为两组

代码语言:javascript
复制
Map<Boolean, List<Dish>> = invoiceList.stream().collect(Collectors.partitioningBy(RzInvoice::getCancelFlag));

等同于

代码语言:javascript
复制
Map<Boolean, List<Dish>> = invoiceList.stream().collect(Collectors.groupingBy(RzInvoice::getCancelFlag));

这个例子可能并不能看出分区和分类的区别,甚至觉得分区根本没有必要,换个明显一点的例子:

代码语言:javascript
复制
List<Integer> integerList = Arrays.asList(1, 2, 3, 4, 5);
Map<Boolean, List<Integer>> result = integerList.stream().collect(partitioningBy(i -> i < 3));

返回值的键仍然是布尔类型,但是它的分类是根据范围进行分类的,分区比较适合处理根据范围进行分类

来一个本人在工作中遇到的样例

代码语言:javascript
复制
// 过滤T-1至T-12 近12月数据,根据省份分组求和开票金额,使用金额进行倒序,产生LinkedHashMap
        LinkedHashMap<String, BigDecimal> areaSortByAmountMaps =
                invoiceStatisticsList.stream().filter(FilterSaleInvoiceUtil.filterSaleInvoiceWithRange(1, 12, analysisDate)) //根据时间过滤数据
                        .collect(Collectors.groupingBy(FkSalesInvoiceStatisticsDO::getBuyerAdministrativeAreaCode
                                , Collectors.reducing(BigDecimal.ZERO, FkSalesInvoiceStatisticsDO::getInvoiceAmount, BigDecimal::add)))// 根据开票地区分组,并同时将每个分组数据的开票金额求和
                        .entrySet().stream().sorted(Map.Entry.<String, BigDecimal>comparingByValue().reversed()) // 根据金额大小倒序
                        .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (e1, e2) -> e1, LinkedHashMap::new)); //收集数据生成LinkedHashMap

总结

通过使用Stream API可以简化代码,同时提高了代码可读性,赶紧在项目里用起来。讲道理在没学Stream API之前,谁要是给我在应用里写很多Lambda,Stream API,飞起就想给他一脚。

我想,我现在可能爱上他了【嘻嘻】。同时使用的时候注意不要将声明式和命令式编程混合使用。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2019-09-29,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 码哥字节 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 超级大招,释放代码
  • 什么是 Stream?
  • 如何生成流
    • 1. 通过集合生成
      • 2.通过数组生成
        • 3. 通过值生成
          • 4. 通过文件生成
            • 5. 通过函数生成,iterate和generate两个静态方法从函数中生成流
            • 流的操作类型
              • 1. 中间操作
                • 2. 终端操作
                • 中间操作 API
                  • 终端操作
                  • 总结
                  领券
                  问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档