java 流循环
在本文中,我将解释如何不再编写循环。
什么? Whaddaya的意思是,不再循环吗?
是的,那是我的2020年决议-Java中不再循环。 明白循环并不是让我失败,也不是它们使我误入歧途(至少,我可以争论这一点)。 确实,我是1997年左右左右开始具有中等能力的Java程序员,我最终必须了解所有这些新的Streams内容,说出我想做的“事情”而不是我想做的“如何”,也许来并行化我的一些计算以及所有其他好的东西。
我猜想那里还有其他Java程序员,他们用Java进行了相当长时间的编程,并且处在同一条船上。 因此,我提供的经验是“如何不再使用Java编写循环”的指南。
找到值得解决的问题
如果您像我一样,那么您遇到的第一个表演停止者是“对的,很酷的东西,但是我要解决什么,如何应用呢?” 我意识到我可以发现我曾经做过的伪装的绝佳机会。
以我为例,它是对特定区域内的土地覆盖进行采样,然后得出整个区域内土地覆盖的估计值和该估计值的置信区间。 具体问题涉及根据特定的法律定义来确定某个区域是否被“造林”:如果树冠覆盖了至少10%的土壤,则该区域被视为造林; 否则,那是另一回事。
这是一个反复出现的问题的绝妙例子。 我会给你的 但是有。 对于习惯于为温带温带或热带森林降温的生态学家和林业者来说,10%听起来可能有点低,但是在干旱的地区,灌木和树木的生长量低,这是一个合理的数字。
因此,基本思想是:使用图像对区域进行分层(即,完全没有树木的区域,彼此隔开的主要小树的区域,彼此隔开更近的主要小树的区域,较大的树的区域),找到一些样本在这些地层中,派出工作人员测量样本,分析结果并计算整个区域被树冠覆盖的土壤比例。 简单吧?
现场数据是什么样的
在当前项目中,样本是20米宽×25米长的矩形区域,因此每个样本为500平方米。 在每个斑块上,野外工作人员都测量了每棵树:树的种类,高度,树冠的最大和最小宽度以及树干高度(距离地面至少30厘米)处的树干直径。 收集了此信息,将其输入电子表格,然后导出到条形分隔值(BSV)文件供我分析。 看起来像这样:
地层# | 样品# | 树# | 种类 | 躯干直径(厘米) | 牙冠直径1(m) | 牙冠直径2(m) | 高度(米) |
1 | 1个 | 1个 | 交流电 | 6 | 3.6 | 4.6 | 2.4 |
1 | 1个 | 2 | 交流电 | 6 | 2.2 | 2.3 | 2.5 |
1 | 1个 | 3 | 交流电 | 16 | 2.5 | 1.7 | 2.4 |
1 | 1个 | 4 | 交流电 | 6 | 1.5 | 2.1 | 1.8 |
1 | 1个 | 5 | 交流电 | 5 | 0.9 | 1.7 | 1.7 |
1 | 1个 | 6 | 交流电 | 6 | 1.7 | 1.3 | 1.6 |
1 | 1个 | 7 | 交流电 | 5 | 1.82 | 1.32 | 1.8 |
1 | 1个 | 1个 | 交流电 | 1个 | 0.3 | 0.25 | 0.9 |
1 | 1个 | 2 | 交流电 | 2 | 1.2 | 1.2 | 1.7 |
第一列是层数(其中1是“主要是距离很远的小树”,2是“主要是距离较近的小树”,而3是“稍大一些的树”;我们没有对区域进行完全采样)没有树木”)。 第二列是样本数(共有73个样本,与每个层的面积成比例地位于三个层中)。 第三列是样本中的树号。 第四个是由两个字母组成的物种代码,第五个是树干直径(在这种情况下,是离地面10厘米或裸露的根部),第六个是横跨树冠的最小距离,第七个是最大距离,第八个是树冠高度。树。
在本练习中,我只关心树冠覆盖的地面总量,而不是树种,树干的高度或直径。
除了上面的测量信息之外,我还具有三个层次的区域,也属于BSV:
地层 | 公顷 |
1 | 114.89 |
2 | 207.72 |
3 | 29.77 |
我想做什么(不是我想怎么做)
为了符合Java Streams的主要设计目标之一,这是我想做的“事情”:
- 读取层区域BSV并将数据另存为查找表。
- 从测量BSV文件中读取测量。
- 累积每个测量值(树)以计算树冠覆盖的样本总面积。
- 累积样本树冠面积值并计数样本数,以估计平均树冠面积覆盖率和每个层的平均值的标准误差。
- 汇总层数。
- 通过层面积(从步骤1中创建的表中查找)权衡层平均值和标准误差,并对其进行累加,以估计平均树冠面积覆盖率和整个区域的平均值的标准误差。
- 总结加权数字。
一般来说,使用Java Streams定义“什么”的方法是通过创建传递数据的函数调用的流处理管道。 因此,是的,实际上还有一些“如何”逐渐蔓延……实际上,有很多“如何”。 但是,与良好的老式循环相比,它需要非常不同的知识库。
我将详细介绍每个步骤。
建立地层面积表
第一步是将地层区域BSV文件转换为查找表:
String fileName
=
"stratum_areas.bsv"
;
Stream
< String
> inputLineStream
= Files.
lines
( Paths.
get
( fileName
)
)
;
// (1)
final Map
<
Integer ,Double
> stratumAreas
=
// (2)
inputLineStream
// (3)
.
skip
(
1
)
// (4)
.
map
( l
-> l.
split
(
" \\ |"
)
)
// (5)
.
collect
(
// (6)
Collectors.
toMap
(
// (7)
a
->
Integer .
parseInt
( a
[
0
]
) ,
// (8)
a
->
Double .
parseDouble
( a
[
1
]
)
// (9)
)
)
;
inputLineStream.
close
(
)
;
// (10)
System .
out .
println
(
"stratumAreas = "
+ stratumAreas
)
;
// (11)
我一次要用一两行,上面注释后面的数字(例如//(3))与下面的数字相对应:
- java.nio.Files.lines()给出与文件中的行相对应的字符串流。
- 目标是创建查找表stratumAreas ,它是Map <Integer,Double> 。 因此,我可以将第2层的双 精度值区域获取为stratumAreas.get(2) 。
- 这是流“管道”的开始。
- 跳过管道中的第一行,因为它是包含列名称的标题行。
- 使用map()将String输入行拆分为String字段数组,第一个字段是阶层#,第二个字段是阶层区域。
- 使用collect() 实现结果 。
- 物化结果将作为一系列Map条目产生。
- 每个映射条目的键是管道中数组的第一个元素- 整数层数。 顺便说一下,这是一个Java Lambda表达式- 一个匿名函数 ,它接受一个参数并将返回的参数转换为int 。
- 每个映射条目的值是管道中数组的第二个元素- 双层区域。
- 不要忘记关闭流(文件)。
stratumAreas = { 1 = 114.89 , 2 = 207.72 , 3 = 29.77 }
stratumAreas = { 1 = 114.89 , 2 = 207.72 , 3 = 29.77 }
建立测量表并将测量值累积到样本总数中
现在我有了层次区域,我可以开始处理数据的主体-测量。 由于我对测量数据本身没有兴趣,因此将构建测量表并将测量结果累积到样本总数这两项任务结合在一起。
fileName
=
"sample_data_for_testing.bsv"
;
inputLineStream
= Files.
lines
( Paths.
get
( fileName
)
)
;
final Map
<
Integer ,Map
<
Integer ,Double
>> sampleValues
=
inputLineStream
.
skip
(
1
)
.
map
( l
-> l.
split
(
" \\ |"
)
)
.
collect
(
// (1)
Collectors.
groupingBy
( a
->
Integer .
parseInt
( a
[
0
]
) ,
// (2)
Collectors.
groupingBy
( b
->
Integer .
parseInt
( b
[
1
]
) ,
// (3)
Collectors.
summingDouble
(
// (4)
c
->
{
// (5)
double rm
=
(
Double .
parseDouble
( c
[
5
]
)
+
Double .
parseDouble
( c
[
6
]
)
)
/ 4d
;
// (6)
return rm
* rm
*
Math .
PI
/ 500d
;
// (7)
}
)
)
)
)
;
inputLineStream.
close
(
)
;
System .
out .
println
(
"sampleValues = "
+ sampleValues
)
;
// (8)
同样,一次或两行左右:
- 前七行在此任务中与前一行相同,除了此查找表的名称为sampleValues之外 ; 它是Map的Map 。
- 测量数据分为样本(按样本号),然后又按层(按层号)分组,因此我在最顶层使用Collectors.groupingBy() 将数据分为层,其中a [0 ]这里是层数。
- 我再次使用Collectors.groupingBy()将数据分成样本,其中b [1]是样本编号。
- 我使用方便的Collectors.summingDouble() 来累积层中样本中每次测量的数据 。
- 同样,一个Java拉姆达或匿名函数其参数c是领域的阵列,其中该拉姆达有几行代码由之前的四周} {和}用一个return语句。
- 计算测量的平均冠部半径。
- 计算测量的冠部面积占总样品面积的比例,并将该值作为lambda的结果返回。
sampleValues = { 1 = { 1 = 0.09083231861452731 , 66 = 0.06088002082602869 , ... 28 = 0.0837823490804228 } , 2 = { 65 = 0.14738326403381743 , 2 = 0.16961183847374103 , ... 63 = 0.25083064794883453 } , 3 = { 64 = 0.3306323635177101 , 32 = 0.25911911184680053 , ... 30 = 0.2642668470291564 } }
sampleValues = { 1 = { 1 = 0.09083231861452731 , 66 = 0.06088002082602869 , ... 28 = 0.0837823490804228 } , 2 = { 65 = 0.14738326403381743 , 2 = 0.16961183847374103 , ... 63 = 0.25083064794883453 } , 3 = { 64 = 0.3306323635177101 , 32 = 0.25911911184680053 , ... 30 = 0.2642668470291564 } }
此输出清楚地显示了Map的Map结构-顶层有三个条目,分别对应于第1、2和3层,每个层次都有对应于树冠覆盖的样本比例区域的子条目。
将样本总数累计到层均值和标准误中
至此,任务变得更加复杂。 我需要计算样本数,对样本值求和以准备计算样本均值,并对样本值的平方求和以准备计算平均值的标准误。 我也可能将阶层区域也合并到此数据分组中,因为我很快需要它来权衡阶层结果。
因此,首先要做的是创建一个StratumAccumulator类,以处理累积量并提供有趣结果的计算。 此类实现java.util.function.DoubleConsumer ,可以将其传递给collect()来处理累积:
class StratumAccumulator
implements DoubleConsumer
{
private
double ha
;
private
int n
;
private
double sum
;
private
double ssq
;
public StratumAccumulator
(
double ha
)
{
// (1)
this .
ha
= ha
;
this .
n
=
0
;
this .
sum
= 0d
;
this .
ssq
= 0d
;
}
public
void accept
(
double d
)
{
// (2)
this .
sum
+= d
;
this .
ssq
+= d
* d
;
this .
n
++;
}
public
void combine
( StratumAccumulator other
)
{
// (3)
this .
sum
+= other.
sum
;
this .
ssq
+= other.
ssq
;
this .
n
+= other.
n
;
}
public
double getHa
(
)
{
// (4)
return
this .
ha
;
}
public
int getN
(
)
{
// (5)
return
this .
n
;
}
public
double getMean
(
)
{
// (6)
return
this .
n
>
0
?
this .
sum
/
this .
n
: 0d
;
}
public
double getStandardError
(
)
{
// (7)
double mean
=
this .
getMean
(
)
;
double variance
=
this .
n
>
1
?
(
this .
ssq
- mean
* mean
* n
)
/
(
this .
n
-
1
)
: 0d
;
return
this .
n
>
0
?
Math .
sqrt
( variance
/
this .
n
)
: 0d
;
}
}
逐行:
- 构造函数StratumAccumulator(double ha)接受一个参数,即以公顷为单位的层面积,这使我可以将层面积查找表合并到此类的实例中。
- accept(double d)方法用于累积double值流,我将其用于: 一个。 计算值的数量。 b。 将值求和以准备计算样本平均值。 C。 求和值的平方,以计算平均值的标准误。
- Combine ()方法用于合并StratumAccumulator的子流(如果我要并行处理的话)。
- 地层面积的吸气剂
- 层中样本数量的吸气剂
- 层中平均样本值的吸气剂
- 层中均值标准误差的吸气剂
一旦有了该累加器,就可以使用它来累加与每个阶层有关的样本值:
final Map
<
Integer ,StratumAccumulator
> stratumValues
=
// (1)
sampleValues.
entrySet
(
) .
stream
(
)
// (2)
.
collect
(
// (3)
Collectors.
toMap
(
// (4)
e
-> e.
getKey
(
) ,
// (5)
e
-> e.
getValue
(
) .
entrySet
(
) .
stream
(
)
// (6)
.
map
(
Map. Entry
:: getValue
)
// (7)
.
collect
(
// (8)
(
)
->
new StratumAccumulator
( stratumAreas.
get
( e.
getKey
(
)
)
) ,
// (9)
StratumAccumulator
:: accept,
// (10)
StratumAccumulator
:: combine
)
// (11)
)
)
;
逐行:
- 这次,我使用管道构建stratumValues ,它是Map <Integer,StratumAccumulator> ,因此stratumValues.get(3)将返回第3层的StratumAccumulator实例。
- 在这里,我使用Map提供的entrySet()。stream()方法来获取(键,值)对的流。 回想一下这些是按层的样本值的Map 。
- 再次,我使用collect()来按层收集管道结果…
- 使用Collectors.toMap()生成Map条目流…
- 其键是传入流的键(即层号)…
- 并且其值是样本值的Map,我再次使用entrySet()。stream()转换为Map条目的流,每个样本一个。
- 使用map()获取示例Map条目的值; 此时,我对密钥不感兴趣。
- 再一次,使用collect()将样本结果累积到StratumAccumulator实例中。
- 告诉collect()如何创建一个新的StratumAccumulator —我需要在此处将层区域传递到构造函数中,所以我不能只使用StratumAccumulator :: new 。
- 告诉collect()使用StratumAccumulator的accept()方法来累积样本值流。
- 告诉collect()使用StratumAccumulator的Combine ()方法合并StratumAccumulator实例。
汇总阶层数字
ew! 毕竟,打印出层次图非常简单:
stratumValues.
entrySet
(
) .
stream
(
)
.
forEach
( e
->
{
StratumAccumulator sa
= e.
getValue
(
)
;
int n
= sa.
getN
(
)
;
double se66
= sa.
getStandardError
(
)
;
double t
=
new TDistribution
( n
-
1
) .
inverseCumulativeProbability
( 0.975d
)
;
System .
out .
printf
(
"stratum %d n %d mean %g se66 %g t %g se95 %g ha %g \n " ,
e.
getKey
(
) , n, sa.
getMean
(
) , se66, t, se66
* t, sa.
getHa
(
)
)
;
}
)
;
在以上内容中,我再次使用entrySet()。stream()将stratumValues Map转换为流,然后将forEach()方法应用于该流。 ForEach()听起来几乎像是一个循环! 但是查找流头,查找下一个元素并检查是否命中末尾的工作全部由Java Streams处理。 因此,我只想说说我要为每条记录做些什么,这基本上是将其打印出来。
我的代码看起来更加复杂,因为我声明了一些局部变量来保存一些不止一次使用的中间结果,即n ,样本数,以及se66 ,均值的标准误差。 我还计算了逆T值, 将平均值的标准误转换为95%的置信区间 。
结果看起来像这样:
stratum
1 n
24 mean
0.0903355 se66
0.0107786 t
2.06866 se95
0.0222973 ha
114.890
stratum
2 n
38 mean
0.154612 se66
0.00880498 t
2.02619 se95
0.0178406 ha
207.720
stratum
3 n
11 mean
0.223634 se66
0.0261662 t
2.22814 se95
0.0583020 ha
29.7700
将层均值和标准误累积到总计中
任务再次变得更加复杂,因此我创建了一个类TotalAccumulator ,以处理累积并提供有趣结果的计算。 此类实现java.util.function.Consumer <T> ,可以将其传递给collect()来处理累积:
class TotalAccumulator
implements Consumer
< StratumAccumulator
>
{
private
double ha
;
private
int n
;
private
double sumWtdMeans
;
private
double ssqWtdStandardErrors
;
public TotalAccumulator
(
)
{
this .
ha
= 0d
;
this .
n
=
0
;
this .
sumWtdMeans
= 0d
;
this .
ssqWtdStandardErrors
= 0d
;
}
public
void accept
( StratumAccumulator sa
)
{
double saha
= sa.
getHa
(
)
;
double sase
= sa.
getStandardError
(
)
;
this .
ha
+= saha
;
this .
n
+= sa.
getN
(
)
;
this .
sumWtdMeans
+= saha
* sa.
getMean
(
)
;
this .
ssqWtdStandardErrors
+= saha
* saha
* sase
* sase
;
}
public
void combine
( TotalAccumulator other
)
{
this .
ha
+= other.
ha
;
this .
n
+= other.
n
;
this .
sumWtdMeans
+= other.
sumWtdMeans
;
this .
ssqWtdStandardErrors
+= other.
ssqWtdStandardErrors
;
}
public
double getHa
(
)
{
return
this .
ha
;
}
public
int getN
(
)
{
return
this .
n
;
}
public
double getMean
(
)
{
return
this .
ha
>
0
?
this .
sumWtdMeans
/
this .
ha
: 0d
;
}
public
double getStandardError
(
)
{
return
this .
ha
>
0
?
Math .
sqrt
(
this .
ssqWtdStandardErrors
)
/
this .
ha
:
0
;
}
}
我不会对此进行详细介绍,因为它在结构上与StratumAccumulator非常相似。 主要兴趣:
- 构造函数不带任何参数,从而简化了其使用。
- accept()方法会累积StratumAccumulator的实例,而不是double值的实例,因此将使用Consumer <T>接口。
- 至于计算,它们正在组合StratumAccumulator实例的加权平均值,因此它们利用了层次区域,对于不习惯分层抽样的人来说,这些公式可能看起来有些奇怪。
至于实际执行工作,这很容易:
final TotalAccumulator totalValues
=
stratumValues.
entrySet
(
) .
stream
(
)
.
map
(
Map. Entry
:: getValue
)
.
collect
( TotalAccumulator
::
new , TotalAccumulator
:: accept, TotalAccumulator
:: combine
)
;
与以前相同的旧资料:
- 使用entrySet()。stream()将stratumValue Map条目转换为流。
- 使用map()将Map条目替换为其值( StratumAccumulator的实例)。
- 使用收集()将TotalAccumulator适用于StratumAccumulator的实例。
汇总总数
从TotalAccumulator实例中获取有趣的内容也非常简单:
int nT
= totalValues.
getN
(
)
;
double se66T
= totalValues.
getStandardError
(
)
;
double tT
=
new TDistribution
( nT
- stratumValues.
size
(
)
) .
inverseCumulativeProbability
( 0.975d
)
;
System .
out .
printf
(
"total n %d mean %g se66 %g t %g se95 %g ha %g \n " ,
nT, totalValues.
getMean
(
) , se66T, tT, se66T
* tT, totalValues.
getHa
(
)
)
;
与StratumAccumulator相似,我只是调用相关的吸气剂以选择样本数量nT和标准误差se66T 。 我计算T值tT (这里有3个层,因此使用“ n – 3”),然后打印结果,如下所示:
total n 73 mean 0.139487 se66 0.00664653 t 1.99444 se95 0.0132561 ha 352.380
total n 73 mean 0.139487 se66 0.00664653 t 1.99444 se95 0.0132561 ha 352.380
结论
哇,看起来有点像马拉松比赛。 感觉也一样。 通常,有很多关于如何使用Java Streams的信息,所有这些都通过玩具示例进行了说明,这是一种帮助,但并非如此。 我发现很难将其与实际(尽管非常简单)的示例一起使用。
因为我最近在Groovy中工作了很长时间,所以我一直发现自己想累积到“ maps of maps”中,而不是创建累加器类,但是除了将样品中的测量值。 因此,我使用的是累加器类而不是映射图,使用的是累加器类的映射而不是映射图的地图。
我现在还不熟悉Java Streams,但是我确实对Collect()非常了解,这非常重要,还有各种将数据结构重新格式化为流以及重新格式化的方法。流元素本身。 是的,还有很多东西要学!
说到collect(),在上面介绍的示例中,我们可以看到从非常简单地使用此基本方法开始-使用Collectors.summingDouble()累积方法-通过定义一个扩展了预定义之一的累加器类而已接口-在这种情况下为DoubleConsumer-用于定义我们自己的成熟累加器,用于累加中间层类。 我很想-进行一些工作-向后工作并为层和样本累加器实现完全自定义的累加器,但是此练习的目的是要了解有关Java Streams的更多信息,而不是成为其中的一个专家。
您对Java Streams有什么经验? 做过又大又复杂的事情了吗? 请在评论中分享。
翻译自: https://opensource.com/article/20/2/java-streams
java 流循环