Java8的Stream最佳实践

从这一篇文章开始,我们会由浅入深,全面的学习stream API的最佳实践(结合我的使用经验),本想一篇写完,但写着写着发现需要写的内容太多了,所以分成一个系列慢慢来说。给大家分享我的经验的同时,也促使我复习每一个细节,大家共同进步。

Stream是什么

Java 8新增了一个API叫做Stream ,Stream的英文可以理解为流动的液体,可能很多人一听脑子里的第一印象就是流式计算,不自觉地就心生畏惧,感觉非常的高深莫测。其实这就是一个辅助处理集合数据的工具类,工具的更新必然带来的是生产力的提升,这里的生产力代表的就是整洁优雅的代码,更高的灵活度,更好的性能。相信各类的技术文章(包括博客和书籍)已经写过无数遍了。比如下面摘录《Java 8实战》关于流的描述:

流是Java API的新成员,它允许你以声明性方式处理数据集合(通过查询语句来表达,而不是临时编写一个实现)。就现在来说,你可以把它们看成遍历数据集的高级迭代器。此外,流还可以透明地并行处理,你无需写任何多线程代码了!

这段话的表述个人感觉类似于抓手、赋能、心智之类的PPT黑话,看着挺高级的,也能懂一些,但也不是很懂,反正如果对于不知道Stream的人,并不能建立直接的理解。

所以流到底是什么呢?是一个接口。让我们看看它的声明:

public interface Stream<T> extends BaseStream<T, Stream<T>> {

  Stream<T> filter(Predicate<? super T> predicate);

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

  void forEach(Consumer<? super T> action);
  ...
}

就是个接口,然后这个借口有一些抽象方法:filter,map,forEach等等。我们可以看到有些方法返回了新的Stream,有些直接是void。这个接口用来干什么用呢?处理集合数据。为什么这么说?看下面一个Collection接口的方法:

public interface Collection<E> extends Iterable<E> {
  ...
  default Stream<E> stream() {
        return StreamSupport.stream(spliterator(), false);
    }
}

那么所有继承了Collection的接口都可以直接创建Stream,然后再执行Stream里面的操作。所以这么看下来,首先得承认书中的表述是高度抽象且精炼的,这是书籍该做的事情。但从易于理解的角度,我觉得可以说是简洁高效安全的处理集合数据的工具类。如下图所示,Stream是一个中间过程。
在这里插入图片描述

需要注意的点

  • 首先Stream不是一个数据结构,它不存储任何数据,它是一种数据处理工具,代表了一种能力。
  • Stream不会对处理的数据本身做任何修改,永远都是返回新的Stream或者最终的处理结果。
  • Stream可以有多个中间操作,但只能有一个终端操作,因为终端操作就求值了。
  • 一个Stream只能用一次,不能多次复用。(因为它不存储数据,只是一个转换能力)。

能力范围

Stream随着Java 8的发布已经8年多了,在我有限的职业生涯里,碰到的一些职场新人依然有些人觉得使用for或者iterator来遍历集合更易读易懂。但如果他真正了解Stream所蕴含的能力后,应该会转变想法。下面简单介绍一下Stream都提供了什么样的能力。

1. 生成流

  • java.util.stream.Stream#of(T… values) 。首先stream接口本身提供了一个静态默认方法,可以直接创建,这里的可变参数会被解析成一个数组。
  • java.util.Collection#stream()
  • java.util.Arrays#stream(T[] array)
  • java.nio.file.Files#list(Path dir)
  • java.nio.file.Files#lines(Path path)

可以看到,可以操作stream的对象基本为List或者Array.

2. 筛选和切片

这可能是用的最多的功能。对应的方法为:

  • filter:接受一个Predicate断言函数,用来遍历元素是否符合断言条件。可以简单的理解为一个过滤器。
  • distinct:无参数,将所有元素去重,和数据库的distinct关键词能力一样。
  • limit:接受一个int型长度字段,表示要保留多少个元素,需要注意的时候limit并不排序。
  • skip:和limit相对应,接受一个int型长度字段表示跳过多少个元素,也不排序。
    下面举个例子:
Stream.of("d2", "a2", "b1", "a3", "c1", "a4", "a2", "a1")
        .filter(x -> x.startsWith("a"))
        .distinct()
        .skip(1)
        .limit(3)
        .forEach(System.out::println);
  }
// output: 
a3
a4
a1

3. 映射/转换

这里主要是map,map代表了一种对应关系,即地图坐标与实际地点的对应关系,我们有了经纬度就可以准确的找到地址,这个例子可以很形象的解释map命名的由来和功能。

  • map:接受一个Function作为参数,即输入一个值,返回另一个值,满足转换的语义。
  • flatmap:同样接受一个Function作为参数,不同的是这个Function中有一个参数是一个stream,返回的也是一个stream,意为将多个stream连成一个stream。

同样,举个简单的例子:

Stream.of("d2", "a2", "b1", "a3", "c1", "a4", "a2", "a1")
        .filter(x -> x.startsWith("a"))
        .map(String::toUpperCase)
        .forEach(System.out::println);
//output
A2
A3
A4
A2
A1

List<String> list = Stream.of("Hello", "world!")
        .map(s -> s.split(""))
        .flatMap(Arrays::stream)
        .collect(Collectors.toList());
System.out.println(list);
//output
[H, e, l, l, o, w, o, r, l, d, !]

4. 查找和匹配

这里的能力可以认为是一个加强版的contains方法,具备多种查找匹配能力。

  • allMatch:返回boolean,接受一个Predicate断言,确认全部元素均满足这个条件则返回true,否则返回false
  • anyMatch:与allMatch类似,但从语义上可以区分只要任意元素满足条件即可
  • noneMatch:同样,要求没有任何元素满足条件
  • findFirst:返回一个Optional,里面是满足条件的第一个元素
  • findAny:返回Optional,里面是满足条件的任一元素

这里需要解惑的是findAny与findFirst的区别,因为这两个都是找到满足条件的元素就返回,但findFirst会在限制并行流的计算,会严格按照集合中元素的顺序来依次查找。findAny就不会有这个限制。如果非并行计算场景,这二者并无区别。

下面依旧举简单的例子说明:

boolean b1 = Stream.of("d2", "a2", "b1", "a3", "c1", "a4", "a2", "a1")
        .anyMatch(x -> x.startsWith("a"));
    System.out.println(b1);
//output: true

String s2 = Stream.of("d2", "a2", "b1", "a3", "c1", "a4", "a2", "a1")
        .filter(x -> x.startsWith("a"))
        .findFirst()
        .get();
    System.out.println(s2);
//output: a2

String s3 = Stream.of("d2", "a2", "b1", "a3", "c1", "a4", "a2", "a1")
        .filter(x -> x.startsWith("a"))
        .findAny()
        .get();
    System.out.println(s3);
//output: a2

//换成并行流
String s4 = Stream.of("d2", "a2", "b1", "a3", "c1", "a4", "a2", "a1")
        .parallel()
        .filter(x -> x.startsWith("a"))
        .findFirst()
        .get();
    System.out.println(s4);
//output: a2


String s5 = Stream.of("d2", "a2", "b1", "a3", "c1", "a4", "a2", "a1")
        .parallel()
        .filter(x -> x.startsWith("a"))
        .findAny()
        .get();
    System.out.println(s5);
//output: a4

5. 归约

归约是一个比较复杂的数学理论,通常是用于将一个未知的问题转换成另一些已知问题,同时这些已知的问题和未知的问题存在某种关联。这里不做详细探讨。在Stream API有一些方法就是用的类似的归约的思想,将大的集合计算分解成小的函数计算并最终合成结果。

  • reduce
  • collect
    这两个方法都很重要,且都是终端操作,执行完即返回流的计算结果。我们逐个来说,先看reduce。reduce的英文含义为减少、归纳,在stream接口中的定义如下:
T reduce(T identity, BinaryOperator<T> accumulator);

Optional<T> reduce(BinaryOperator<T> accumulator);

<U> U reduce(U identity, BiFunction<U, ? super T, U> accumulator, BinaryOperator<U> combiner);

这样的方法签名如同天书,先看一个简单的例子:

Integer i = Stream.of(1, 4, 6, 7, 9).reduce(1, (sum, i) -> sum + i);
System.out.println(i);

其中reduce我传了2个参数:

  • 1表示初始值,可以不给,不给的话默认从流的第一个元素开始计算,但返回是就是Optional
  • (sum, i) -> sum + i表示计算函数,每次计算的结果都会暂存在sum中,i则是下一个元素
    所以总的来说,这是一个迭代归纳的过程,将多个元素的流按照自己制定的计算规则变成一个元素。不仅仅可以做述职运算,也可以实现复杂对象的转换,先看例子(此例来源于与廖雪峰老师的网站并稍做修改,具体链接:https://www.liaoxuefeng.com/wiki/1252599548343744/1322402971648033):
List<String> props = Lists.newArrayList("profile=native", "debug=true", "logging=warn", "interval=500");
    Map<String, String> map = props.stream()
        .map(kv -> {
          String[] ss = kv.split("=", 2);
          Map<String, String> m = Maps.newHashMap();
          m.put(ss[0], ss[1]);
          return m;
        })
        .reduce(new HashMap<>(), (m, kv) -> {
          m.putAll(kv);
          return m;
        });
    map.forEach((k, v) -> System.out.println(k + " = " + v));

//output:
logging = warn
interval = 500
debug = true
profile = native

第一个map执行完之后返回了多个小map这里使用reduce进行一个map的累加:

  • new HashMap<>()是初始值,一个空map
  • (m, kv) ->中,m是暂存累加结果,kv表示下一个元素map
    以上看来,reduce的使用场景应该会很广泛,尤其是多个集合合成一个大集合的场景。

小结

本文介绍了stream是什么、创建stream的方法、stream的一些基本API的能力和reduce方法的使用。作为stream最佳实践的开篇,先从stream的基础开始写,后续会逐步深入并总结我个人使用下来的最佳实践,希望大家持续关注,共同学习。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/338671.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

hadoop必记知识点(1)

1.Hadoop是什么&#xff0c;解决什么问题&#xff1f; Hadoop是一个由Apache基金会所开发的分布式系统基础架构。它可以让使用者在普通的硬件上搭建起一个强大的计算集群。Hadoop的特点包括&#xff1a;高可靠性、高扩展性、高容错性、支持大数据和高并发等。Hadoop核心组件包…

python写完程序怎么运行

python有两种运行方式&#xff0c;一种是在python交互式命令行下运行; 另一种是使用文本编辑器直接在命令行上运行。 注&#xff1a;以上两种运行方式均由CPython解释器编译运行。 当然&#xff0c;也可以将python代码写入eclipse中&#xff0c;用JPython解释器运行&#xff0c…

推荐系统|2.4 矩阵分解的目的和效果

文章目录 矩阵分解矩阵分解的必要性和方法隐向量 矩阵分解 矩阵分解的必要性和方法 比如原本是一个 m n m\times n mn规模大小的矩阵,经过分解后可得到两个矩阵一个是 m k m\times k mk&#xff0c;另外一个是 k n k\times n kn,于是总占用空间为 ( m n ) k (mn)\times k…

腾讯云.com域名报价

腾讯云com域名首年价格&#xff0c;企业新用户注册com域名首年1元&#xff0c;个人新用户注册com域名33元首年&#xff0c;非新用户注册com域名首年元85元一年&#xff0c;优惠价75元一年&#xff0c;com域名续费85元一年。腾讯云百科txybk.com分享腾讯云com域名注册优惠价格&a…

【C语言编程之旅 7】刷题篇-函数

第1题 解析 A&#xff1a;错误&#xff0c;一个函数只能返回一个结果 B&#xff1a;正确&#xff0c;将形参存在数组中&#xff0c;修改数组中内容&#xff0c;可以通过数组将修改结果带出去 C&#xff1a;正确&#xff0c;形参如果用指针&#xff0c;最终指向的是外部的实参…

Unity3D学习之UI系统——GUI

文章目录 1. 前言2. 工作原理和主要作用3. 基础控件3.1 重要参数及文本和按钮3.1.1 GUI 共同点3.1.2 文本控件3.1.3 按钮控件 3.2 多选框和单选框3.2.1 多选框3.2.2 单选框3.2.3 输入框3.2.4 拖动条 3.3 图片绘制和框3.3.1 图片3.3.2 框绘制 4 工具栏和选择网格4.1 工具栏4.2 选…

Docker(十一)Swarm mode

作者主页&#xff1a; 正函数的个人主页 文章收录专栏&#xff1a; Docker 欢迎大家点赞 &#x1f44d; 收藏 ⭐ 加关注哦&#xff01; Swarm mode Docker 1.12 Swarm mode 已经内嵌入 Docker 引擎&#xff0c;成为了 docker 子命令 docker swarm。请注意与旧的 Docker Swarm …

Liunx系统和Window系统有什么区别

在信息技术世界里&#xff0c;操作系统扮演着至关重要的角色&#xff0c;它负责管理和控制计算机硬件与软件资源。Linux和Windows是市面上两个最流行的操作系统。接下来&#xff0c;我们将深入研究这两种操作系统的主要差异。 核心体系结构及源代码访问&#xff1a; 首先&#…

node介绍

1.node是什么 Node是一个基于Chrome V8引擎的JS运行环境。 Node不是一个独立的语言、node不是JS框架。 Node是一个除了浏览器之外的、可以让JS运行的环境 Node.js是一个让JS运行在服务端的开发平台&#xff0c;是使用事件驱动&#xff0c;异步非阻塞I/O&#xff0c;单线程&…

团灭 LeetCode 股票买卖问题

这几道题目是有共性的&#xff0c;我们只需要抽出来力扣第 188 题「188. 买卖股票的最佳时机 IV - 力扣&#xff08;LeetCode&#xff09;」进行研究&#xff0c;因为这道题是最泛化的形式&#xff0c;其他的问题都是这个形式的简化&#xff0c;看下题目&#xff1a; 第一题是只…

RHCE上课笔记(前半部分)

第一部分 网络服务 第一章 例行性工作 1.单一执行的例行性工作 单一执行的例行性工作&#xff08;就像某一个时间点 的闹钟&#xff09;&#xff1a;仅处理执行一次 1.1 at命令&#xff1a;定时任务信息 [rhellocalhost ~]$ rpm -qa |grep -w at at-spi2-core-2.40.3-1.el9.x…

一条sql是如何运行的

在我们平时使用sql的时候&#xff0c;基本是基于黑盒的使用方式&#xff0c;在客户端输入一条sql语句&#xff0c;然后回显想要的数据&#xff0c;对于mysql server端内部如何运行的以及与存储引擎如何交互的不得而知。 通过下面一幅图&#xff0c;大致描述客户端和服务端交互…

重定位(一)段的概念引入

1.2440结构图 对于2440来说&#xff0c;cpu可以直接发指令给SRAM、网卡、SDRAM、NOR FLASH&#xff0c;但无法直接控制NAND FLASH,必须由NAND FLASH控制器来操作NAND FLASH&#xff0c;但为什么我们的裸机程序烧入NAND FLASH还可以运行呢&#xff1f; 这就引入了重定位机制&…

matlab appdesigner系列-常用12-日期选择器

日期选择器&#xff0c;目的就是显示时间&#xff0c;时间格式目前常用的 正序2024/1/19 也有倒序 19/1/2024 或者写成年-月-日格式的&#xff0c; 此示例&#xff0c;为当用户要更改日期时&#xff0c;弹出对话框提示&#xff1a;把日期从XXX改到XXX&#xff1f;确认日期…

热血江湖服务端服务器架设教程

热血江湖服务端服务器架设教程 大家好&#xff0c;我是艾西今天简单的说下热血江湖架设需要哪些东西然后怎么操作&#xff0c;不管你是自己玩还是对外开放&#xff0c;这对于有兴趣的小伙伴总的都是一件好事。技多不压身就是这么个道理&#xff0c;当你需要用上时还希望能记起…

【二叉树练习2】

文章目录 判断是否是完全二叉树找出p和q的最近的公共祖先非递归实现前序遍历非递归实现中序遍历非递归实现后序遍历 判断是否是完全二叉树 boolean isCompleteTree(TreeNode root){if (root null){return true;}//创建队列Queue<TreeNode> queue new LinkedList<>…

Midjourney在线绘画及提示词精选库

网址:https://chat.xutongbao.top/ 一碗面粉&#xff1a; Self-Rising Flour in a 50s colourful bowl. professional photograph --ar 720:1170 --v 6 烟花古建筑&#xff1a; At night, with the snow-covered scenery of the Beijing Forbidden City as the backdrop, brill…

linux内核源码编译2.6失败

centos7环境 iso选择 https://mirrors.tuna.tsinghua.edu.cn/centos/7/isos/x86_64/CentOS-7-x86_64-DVD-2009.iso 自带qemu&#xff0c;未实测是否可用 选择编译版本2.6 下载地址 遇到的编译错误解决 yum list | grep curses yum install ncurses-devel.x86_64 -y yum i…

算法专题[递归-搜索-回溯-2-DFS]

算法专题[递归-搜索-回溯-2-DFS] 一.计算布尔二叉树的值&#xff1a;1.思路一&#xff1a;2.GIF题目解析 二.求根节点到叶子节点的数字之和1.思路一&#xff1a;2.GIF题目解析 三.二叉树剪枝1.思路一&#xff1a;2.GIF题目解析 四.验证二叉搜索树1.思路一&#xff1a;2.GIF题目…

触摸屏监控双速电动机-硬件设计1

主电路设计 主电路如图所示。三相总电源从前门配电箱的-X1-1接线端子排引出&#xff0c;给混料泵电动机供三相电&#xff0c;给PLC供单相电。混料泵电动机用KM3主触点接通低速&#xff0c;用KM4的主触点和辅助触点接通高速。注意&#xff0c;高低速切换时&#xff0c;双速电动…