使用stream在内存中处理数据
stream编程产生背景:
随着互联网行业的兴起,数据来源不再是单一的数据库。可能是缓存,数据库,消息中间件,es等。在对数据进行处理时,需要进行整合数据,原有的编写sql关联查询等方式不再适用复杂数据的处理。在此应用场景下,stream编程应运而生。
Stream介绍
stream是java Se8 除lambda外的又一重要属性,stream基于lambda构建和使用。stream是集合框架的伙伴框架,同样在内存中处理数据,stream’有着高效的处理过程。stream编程不仅使你的代码效率高,并且阅读性更好。
List<String> strings = List.of("one","two","three","four");
var map = strings.stream()
.collect(groupingBy(String::length, counting()));
map.forEach((key, value) -> System.out.println(key + " :: " + value));
这段代码分以下步骤完成:
通过 groupingBy(String::length) 对strings进行分组
通过counting()统计每个长度字符的数量
然后创建Map<Integer, Long> 去存放结果
3 :: 2
4 :: 1
5 :: 1
映射-过滤-整合 算法的介绍
这个算法是处理数据的经典算法。下面举一个例子来说明:
假设你有一个 Sale集合,Sale里面包含了 date,product引用,金额amount,简单起见,假设金额为Integer。
public class Sale {
private String product;
private LocalDate date;
private int amount;
// constructors, getters, setters
// equals, hashCode, toString
}
假设你要计算三月份的总金额,你会写出如下的算法:
List<Sale> sales = ...; // this is the list of all the sales
int amountSoldInMarch = 0;
for (Sale sale: sales) {
if (sale.getDate().getMonth() == Month.MARCH) {
amountSoldInMarch += sale.getAmount();
}
}
System.out.println("Amount sold in March: " + amountSoldInMarch);
第一个步骤是过滤,过滤那些不是三月份的订单。
第二步是获取过滤后的金额。
第三步是对金额求和。
sql中也可以进行如下的处理:
select sum(amount)
from Sales
where extract(month from date) = 3;
定制化结果而不是算法
通过之前所写的sql,我们可以看到我们只负责三月份金额的求和,具体的性能优化交给数据库服务器。
计算这个金额的代码片段是关于金额求和的每一步的描述。它精确的规定了每一步的执行,而没有留给java虚拟机优化的空间。
Stream Api的两个目标是使代码更具表现力和可读性,并给java运行时提供优化计算的调整空间。
将对象映射为其他的值
映射-过滤-整合算法的第一步是映射,映射是对对象或值进行转换。映射是一个接一个的,如果你10个对象进行转换,那么将得到10个转换后值。
映射改变的是对象的类型,不改变对象的顺序。
映射由function接口建模,接受一个类型参数,返回另一个类型的数据,特殊化的 function接口可以映射为基本类型或其他的。
过滤对象
在另一方面,过滤不会接触到处理的对象,只会选择保留对象或移除该对象。
过滤不改变数据的类型,只改变数据的数量。
过滤由predicate接口建模,predicate接口只接受一个参数,并返回一个boolean类型。
数据整合
整合比看起来的要复杂。它与sql中的聚合类似。如COUNT, SUM, MIN, MAX, AVERAGE。stream Api支持所有的聚合。
整合数据允许你构建复杂的结构数据,如list,set,map,和其他自己定义的类型。
优化映射-过滤-整合 算法
看一个例子:假设你有一个城市列表,现在要统计人口超过10万的城市的总人口数
List<City> cities = ...;
int sum = 0;
for (City city: cities) {
int population = city.getPopulation();
if (population > 100_000) {
sum += population;
}
}
System.out.println("Sum = " + sum);
你可以在城市列表使用另一种 映射-过滤-整合处理数据。
现在假设没有stream api,在集合上定义map(),filter(),sum()方法,
int sum = cities.map(city -> city.getPopulation())
.filter(population -> population > 100_000)
.sum();
进一步的拆解开来:
Collection<Integer> populations = cities.map(city -> city.getPopulation());
Collection<Integer> filteredPopulations = populations.filter(population -> population > 100_000);
int sum = filteredPopulations.sum();
分析一下这段代码:
第一步是映射步骤。您看到,如果必须处理1,000个城市,那么这个映射步骤将生成1,000个整数,并将它们放入一个集合中。
第二步是过滤步骤。它遍历所有元素,并根据给定的标准删除其中一些元素。这是另外1000个要测试的元素和另一个要创建的集合,可能更小。
因为此代码返回一个集合,所以它映射所有城市,然后过滤得到的整数集合。这与您最初编写的for循环的工作方式非常不同。存储这个中间的整数集合可能会导致很大的开销,特别是在有很多城市要处理的情况下。for循环没有这种开销:它直接对结果中的整数求和,而不将它们存储在中间结构中。
这种开销很糟糕,在某些情况下甚至会更糟。假设您需要知道是否有人口超过10万的城市。也许集合的第一个城市就是这样一个城市。在这种情况下,你几乎不需要付出任何努力就能得到结果。首先,构建来自你的城市的所有人口的集合,然后过滤它,并检查结果是否为空,这将是荒谬的。
出于明显的性能原因,创建在Collection接口上返回Collection的map()方法并不是正确的方法。最终,您将创建不必要的中间结构,并在内存和CPU上造成很高的开销。
这就是为什么没有将map()和filter()方法添加到Collection接口的原因。相反,它们是在Stream接口上创建的。
正确的处理方式如下:
Stream<City> streamOfCities = cities.stream();
Stream<Integer> populations = streamOfCities.map(city -> city.getPopulation());
Stream<Integer> filteredPopulations = populations.filter(population -> population > 100_000);
int sum = filteredPopulations.sum(); // in fact this code does not compile; we'll fix it later
这引导出了stream的一个重要的属性
A stream is an object that does not store any data.
stream 不存储任何数据。
使用中间操作创建管道
中间操作是返回另一个流的操作。调用这样的操作将在现有的操作管道上增加一个操作,而不处理任何数据。它由一个返回流的方法建模。
用最终操作计算结果
终端操作是不返回流的操作。调用这样的操作将触发对流源元素的消费。然后,这些元素由中间操作流水线处理,每次处理一个元素。
终端操作由一个方法建模,该方法返回除流之外的任何内容,包括void。
您不能在一个流上调用多个中间方法或终端方法。如果你这样做,你会得到一个IllegalStateException和以下消息:“流已经被操作或关闭”。
使用特殊的数字流来避免包装
stream api给了四个接口:
第一个是Stream,你可以使用它定义任何类型的管道。
接着有三个特殊化的数字流: IntStream, LongStream and DoubleStream.
这三个数字流使用基本数字类型而不是包装类型来处理数据。
它们有stream流中的大多数方法,只有少许不同。因为它们是处理数字的,所有它们有一些整合方法,在stream中是没有的。如:
sum():计算总和
min (), max():计算流的最小或最大数目
average():计算数值的平均值
summaryStatistics():这个调用产生一个特殊的对象,该对象携带多个统计信息,所有这些统计信息都是在对数据的一次传递中计算出来的。这些统计数据是该流处理的元素数量、最小值、最大值、总和和平均值。
跟随着良好的练习
如您所见,只允许调用流上的一个方法,即使该方法是中间方法。因此,将流存储在字段或局部变量中是无用的,有时甚至是危险的。编写将流作为参数的方法也可能是危险的,因为您不能确定接收到的流是否已经被操作过。流应该在现场创建和使用。
流是一个连接到源的对象。它从这个源中提取它处理的元素。流本身不应该修改这个源。这样做将导致未指定的结果。在某些情况下,这个源是不可变的或只读的,所以您不能这样做,但在某些情况下您可以这样做。
我们对Markdown编辑器进行了一些功能拓展与语法支持,除了标准的Markdown编辑器功能,我们增加了如下几点新功能,帮助你用它写博客:
- 全新的界面设计 ,将会带来全新的写作体验;
- 在创作中心设置你喜爱的代码高亮样式,Markdown 将代码片显示选择的高亮样式 进行展示;
- 增加了 图片拖拽 功能,你可以将本地的图片直接拖拽到编辑区域直接展示;
- 全新的 KaTeX数学公式 语法;
- 增加了支持甘特图的mermaid语法1 功能;
- 增加了 多屏幕编辑 Markdown文章功能;
- 增加了 焦点写作模式、预览模式、简洁写作模式、左右区域同步滚轮设置 等功能,功能按钮位于编辑区域与预览区域中间;
- 增加了 检查列表 功能。
功能快捷键
撤销:Ctrl/Command + Z
重做:Ctrl/Command + Y
加粗:Ctrl/Command + B
斜体:Ctrl/Command + I
标题:Ctrl/Command + Shift + H
无序列表:Ctrl/Command + Shift + U
有序列表:Ctrl/Command + Shift + O
检查列表:Ctrl/Command + Shift + C
插入代码:Ctrl/Command + Shift + K
插入链接:Ctrl/Command + Shift + L
插入图片:Ctrl/Command + Shift + G
查找:Ctrl/Command + F
替换:Ctrl/Command + G
合理的创建标题,有助于目录的生成
直接输入1次#,并按下space后,将生成1级标题。
输入2次#,并按下space后,将生成2级标题。
以此类推,我们支持6级标题。有助于使用TOC
语法后生成一个完美的目录。
如何改变文本的样式
强调文本 强调文本
加粗文本 加粗文本
标记文本
删除文本
引用文本
H2O is是液体。
210 运算结果是 1024.
插入链接与图片
链接: link.
图片:
带尺寸的图片:
居中的图片:
居中并且带尺寸的图片:
当然,我们为了让用户更加便捷,我们增加了图片拖拽功能。
如何插入一段漂亮的代码片
去博客设置页面,选择一款你喜欢的代码片高亮样式,下面展示同样高亮的 代码片
.
// An highlighted block
var foo = 'bar';
生成一个适合你的列表
- 项目
- 项目
- 项目
- 项目
- 项目1
- 项目2
- 项目3
- 计划任务
- 完成任务
创建一个表格
一个简单的表格是这么创建的:
项目 | Value |
---|---|
电脑 | $1600 |
手机 | $12 |
导管 | $1 |
设定内容居中、居左、居右
使用:---------:
居中
使用:----------
居左
使用----------:
居右
第一列 | 第二列 | 第三列 |
---|---|---|
第一列文本居中 | 第二列文本居右 | 第三列文本居左 |
SmartyPants
SmartyPants将ASCII标点字符转换为“智能”印刷标点HTML实体。例如:
TYPE | ASCII | HTML |
---|---|---|
Single backticks | 'Isn't this fun?' | ‘Isn’t this fun?’ |
Quotes | "Isn't this fun?" | “Isn’t this fun?” |
Dashes | -- is en-dash, --- is em-dash | – is en-dash, — is em-dash |
创建一个自定义列表
-
Markdown
- Text-to- HTML conversion tool Authors
- John
- Luke
如何创建一个注脚
一个具有注脚的文本。2
注释也是必不可少的
Markdown将文本转换为 HTML。
KaTeX数学公式
您可以使用渲染LaTeX数学表达式 KaTeX:
Gamma公式展示 Γ ( n ) = ( n − 1 ) ! ∀ n ∈ N \Gamma(n) = (n-1)!\quad\forall n\in\mathbb N Γ(n)=(n−1)!∀n∈N 是通过欧拉积分
Γ ( z ) = ∫ 0 ∞ t z − 1 e − t d t . \Gamma(z) = \int_0^\infty t^{z-1}e^{-t}dt\,. Γ(z)=∫0∞tz−1e−tdt.
你可以找到更多关于的信息 LaTeX 数学表达式here.
新的甘特图功能,丰富你的文章
- 关于 甘特图 语法,参考 这儿,
UML 图表
可以使用UML图表进行渲染。 Mermaid. 例如下面产生的一个序列图:
这将产生一个流程图。:
- 关于 Mermaid 语法,参考 这儿,
FLowchart流程图
我们依旧会支持flowchart的流程图:
- 关于 Flowchart流程图 语法,参考 这儿.
导出与导入
导出
如果你想尝试使用此编辑器, 你可以在此篇文章任意编辑。当你完成了一篇文章的写作, 在上方工具栏找到 文章导出 ,生成一个.md文件或者.html文件进行本地保存。
导入
如果你想加载一篇你写过的.md文件,在上方工具栏可以选择导入功能进行对应扩展名的文件导入,
继续你的创作。
注脚的解释 ↩︎