java streams
在最近的一篇文章中 ,我提到了2020年新年的解决方案:Java中不再循环。 在那篇文章中,我选择了一种常见的(和简化的)森林管理计算方法-根据法律定义,通过计算树冠遮荫的地面比例来确定某个区域是否有森林。
从数据收集的角度来看,这需要对区域进行采样,然后从该样本中估计树木冠层所占的比例。 传统上,首先通过查看航空照片或卫星图像中的区域并将该区域划分为看起来具有大致均匀的植被特征的单位来进行采样。 这些单位称为地层 (复数层)。 然后,在每个层中生成随机定位的点的集合。 在每个点上都放置了一个样本 ,通常是一个特定尺寸的圆形或矩形,并且每个样本中的所有树木都在现场进行测量。 然后,回到办公室,对样本值求和,计算地层平均值,然后将这些平均值加权为该区域的总平均值。
在我的上一篇文章中,我解释了如何使用Java Streams用一系列map映射和reduce函数调用来替换每个循环。 Java接口java.util.stream定义了两种不同的reduce函数(在我的示例计算中,它们采用累加器的形式):
- reduce() ,在消耗流中的每个项目时产生不可变的部分累积
- collect() ,在消耗流中的每个项目时产生可变的部分累积
使用collect()的好处是开销更少:不会生成新的不可变的部分结果,然后在累加的每个步骤中将其丢弃; 相反,现有的部分结果将新的数据元素累积到其中。
在进行样本计算时,我发现自己以一种普遍且不令人满意的方式学习了collect() :我可以找到的所有示例和教程都是基于玩具问题,每次累积一个数据项; 而且,所有这些都被构造为使用现有预定义功能的小配方,这似乎仅在“一次累积一个数据项”这一有限情况下才有用。 在继续进行编程时,我不断深入,直到不确定我是否足够了解整个Java Streams框架以真正能够使用它为止。
因此,我决定重新审视我的代码,试图详细了解“幕后”所发生的事情,并以更加一致和一致的方式公开更多涉及的机制。 继续阅读以获取我所做修订的摘要。
收集复杂事物的地图
以前,我使用了一次collect()调用,将第一列中包含层数和第二列中的层区域的输入行转换为Map <Integer,Double> :
final Map
<
Integer ,Double
> stratumDefinitionTable
= inputLineStream
.
skip
(
1
)
// skip the column headings
.
map
( l
-> l.
split
(
" \\ |"
)
)
// split the line into String[] fields
.
collect
(
Collectors.
toMap
(
a
->
Integer .
parseInt
( a
[
0
]
) ,
// (1)
a
->
Double .
parseDouble
( a
[
1
]
)
// (2)
)
)
;
上面的代码注释(1)标记键的定义(整数层数),注释(2)标记值的定义(双层区域)。
更详细地讲,(静态)便捷方法java.util.stream.Collectors.toMap()创建一个Collector ,该Collector初始化地图并在处理输入数据时使用地图条目填充地图。 严格来说,这不是积累……但无论如何。
但是,如果要收集的信息不仅仅是层区域,该怎么办? 例如,如果我想在输出中使用文本标签以及要使用的区域,该怎么办?
为了解决这个问题,我可能首先定义一个这样的类,该类将保存有关该层的所有信息:
class StratumDefinition
{
private
int number
;
private
double ha
;
private
String label
;
public StratumDefinition
(
int number,
double ha,
String label
)
{
this .
number
= number
;
this .
ha
= ha
;
this .
label
= label
;
}
public
int getNumber
(
)
{
return
this .
number
;
}
public
double getHa
(
)
{
return
this .
ha
;
}
public
String getLabel
(
)
{
return
this .
label
;
}
}
然后,在声明StratumDefinition之后 ,我可以使用类似于以下代码的代码来进行“累加”(以绿色文本突出显示的更改):
final Map
<
Integer ,StratumDefinition
> stratumDefinitionTable
= inputLineStream
.
skip
(
1
)
// skip the column headings
.
map
( l
-> l.
split
(
" \\ |"
)
)
// split the line into String[] fields
.
collect
(
Collectors.
toMap
(
a
->
Integer .
parseInt
( a
[
0
]
) ,
a
->
new StratumDefinition
(
Integer .
parseInt
( a
[
0
]
) ,
Double .
parseDouble
( a
[
1
]
) , a
[
2
]
)
)
)
;
现在,代码更加通用,因为我可以更改层定义文件中的列以及StratumDefinition类中的字段和方法以匹配,而无需更改Streams处理逻辑。
请注意,我可能不需要同时将层号既作为键又保留在每个映射条目中存储的值中; 但是,这样一来,如果以后我决定将地图条目的值作为流进行处理,则可以免费获得层号,而无需进行任何体操操作即可获取密钥。
按组和子组收集几个数据项的小计
以前,我使用了一次collect()来将每个树冠区域累积到每个层中每个样本所覆盖的总比例中,即maps Map <Integer,Map <Integer,Double >>的地图 :
final Map
<
Integer ,Map
<
Integer ,Double
>> sampleValues
= inputLineStream
.
skip
(
1
)
.
map
( l
-> l.
split
(
" \\ |"
)
)
.
collect
(
Collectors.
groupingBy
( a
->
Integer .
parseInt
( a
[
0
]
) ,
// (1)
Collectors.
groupingBy
( b
->
Integer .
parseInt
( b
[
1
]
) ,
// (2)
Collectors.
summingDouble
(
// (3)
c
->
{
double rm
=
(
Double .
parseDouble
( c
[
5
]
)
+
Double .
parseDouble
( c
[
6
]
)
)
/ 4d
;
return rm
* rm
*
Math .
PI
/ 500d
;
// (4)
}
)
)
)
)
;
上面的代码注释(1)标记了定义顶层密钥(层号)的位置。 注释(2)标记了第二级键(样本编号)的定义,注释(3)累积了在(4)中计算的双精度值流。
更详细地讲,(静态)便捷方法java.util.stream.Collectors.groupingBy()创建一个Collector ,该Collector根据第一个参数返回的值将流子集化,并应用给定的Collector作为第二个参数。 在上面的示例中,有两个分组级别,一个是按层,另一个是按样本(在层内)。 内部的groupingBy()使用java.util.stream.Collectors.summingDouble()创建一个收集器 ,该收集器初始化和并累积每棵树对样本中总覆盖率的比例贡献。
请注意,在上面,如果您只想汇总一个数字, summingDouble()是一个方便的快捷方式。 但是,请记住,我已经记录了所测量的每棵树的种类,树干直径,树冠直径和高度,如果我想累积与所有这些测量值相关的数字该怎么办?
为了解决这个问题,我需要定义一对类,一个类来包装测量信息,看起来可能像这样:
class Measurement
{
private
int stratum, sample, tree
;
private
String species
;
private
double ha, basalDiameter, crownArea, height
;
public Measurement
(
int stratum,
int sample,
double ha,
int tree,
String species,
double basalDiameter,
double crownDiameter1,
double crownDiameter2,
double height
)
{
...
}
public
int getStratum
(
)
{
return
this .
stratum
;
}
public
int getSample
(
)
{
return
this .
sample
;
}
public
double getHa
(
)
{
return
this .
ha
;
}
public
int getTree
(
)
{
return
this .
tree
;
}
public
String getSpecies
(
)
{
return
this .
species
;
}
public
double getBasalDiameter
(
)
{
return
this .
basalDiameter
;
}
public
double getCrownArea
(
)
{
return
this .
crownArea
;
}
public
double getHeight
(
)
{
return
this .
height
;
}
}
然后将信息累积到样本总数中,看起来可能像这样:
class SampleAccumulator
implements Consumer
< Measurement
>
{
private
double ...
;
public SampleAccumulator
(
)
{
...
}
public
void accept
( Measurement m
)
{
...
}
public
void combine
( SampleAccumulator other
)
{
...
}
...
}
请注意, SampleAccumulator实现接口java.util.function.Consumer <T> 。 这不是严格必要的; 只要最终提供与构建Collector所需功能类似的功能,我就可以设计“手绘”类,这将在下面显示。
然后,我可以使用与原始代码类似的代码将其累加到SampleAccumulator的实例中(更改以绿色文本突出显示):
final Map
<
Integer ,Map
<
Integer ,SampleAccumulator
>> sampleAccumulatorTable
= inputLineStream
.
skip
(
1
)
.
map
( l
-> l.
split
(
" \\ |"
)
)
.
map
( a
->
new Measurement
(
Integer .
parseInt
( a
[
0
]
) ,
Integer .
parseInt
( a
[
1
]
) ,
Double .
parseDouble
( a
[
2
]
) ,
Integer .
parseInt
( a
[
3
]
) , a
[
4
] ,
Double .
parseDouble
( a
[
5
]
) ,
Double .
parseDouble
( a
[
6
]
) ,
Double .
parseDouble
( a
[
7
]
) ,
Double .
parseDouble
( a
[
8
]
)
)
)
.
collect
(
Collectors.
groupingBy
( Measurement
:: getStratum,
Collectors.
groupingBy
( Measurement
:: getSample,
Collector.
of
(
SampleAccumulator
::
new ,
( smpAcc, msrmt
)
-> smpAcc.
accept
( msrmt
) ,
( smpAcc1, smpAcc2
)
->
{
smpAcc1.
combine
( smpAcc2
)
;
return smpAcc1
;
} ,
Collector.
Characteristics .
UNORDERED
)
)
)
)
;
请注意使用上面的两个新类创建的两个重大更改:
- 它使用lambda插入对java.util.stream.map()的第二次调用,以使用从数据字段的String数组中解析出的值来创建Measurement的新实例。
- 它使用java.util.stream.Collector.of()代替了使用java.util.stream.Collectors.summingDouble()创建的“ doubles收集器” (一次仅累加一个数字)来创建“ SampleAccumulators收集器”,一次可累积任意数量的数字。
再次,生成的代码具有更多通用性:我可以更改样本数据文件以及Measurement和SampleAccumulator类中的字段,以管理不同的输入数据项,而不必弄乱流处理代码。
也许我很慢,但是花了我一些时间才能了解of()方法的参数类型与实际的lambda参数之间的对应关系。 例如, of()的第三个参数定义“ combiner”函数,其类型为BinaryOperator <A> 。 尽管类型的名称具有暗示性,但重要的是实际查找定义以了解它接受两个A类型的参数并返回一个A类型的值(即参数的组合)。 顺便说一句,我要强调的是,这与java.util.function.Consumer <T>的“ combine”方法不同,后者采用类型T的一个参数并将其与实例组合。
一旦弄清楚了这一点,我便意识到我已经定义了一个Collector.of()版本,该版本以Consumer作为参数。这太糟糕了,以致于它没有内置在java.util.stream.Collector接口中。 (现在)对我来说似乎是一个明显的遗漏。
其余代码
上一个示例中的其余代码使用collect()的版本,该版本带有三个参数:供应商,累加器和组合器。 StratumAccumulator和TotalAccumulator类都实现接口java.util.function.Consumer <T> ,因此提供了这三个函数。
对于StratumAccumulator ,我看到:
.
collect
(
(
)
->
new StratumAccumulator
( stratumDefinitionTable.
get
( e.
getKey
(
)
) .
getHa
(
)
) ,
StratumAccumulator
:: accept,
StratumAccumulator
:: combine
)
对于TotalAccumulator :
.
collect
(
TotalAccumulator
::
new ,
TotalAccumulator
:: accept,
TotalAccumulator
:: combine
)
对于这两种方法,唯一需要做的工作是进一步完善StratumAccumulator和TotalAccumulator类,以合并其他字段和累加步骤。
但是,为了对称起见,也可以重写这些代码,以使用Collector.of()作为collect()调用的参数(对于那些愿意在可能的情况下应用通用方法的人)。
然后,对于StratumAccumulator ,我看到:
.
collect
(
Collector.
of
(
(
)
->
new StratumAccumulator
( stratumDefinitionTable.
get
( e.
getKey
(
)
) .
getHa
(
)
) ,
( strAcc, smpAcc
)
-> strAcc.
accept
( smpAcc
) ,
( strAcc1, strAcc2
)
->
{
strAcc1.
combine
( strAcc2
)
;
return strAcc1
;
} ,
Collector.
Characteristics .
UNORDERED
)
)
对于TotalAccumulator :
.
collect
(
Collector.
of
(
TotalAccumulator
::
new ,
( totAcc, strAcc
)
-> totAcc.
accept
( strAcc
) ,
( totAcc1, totAcc2
)
->
{
totAcc1.
combine
( totAcc2
)
;
return totAcc1
;
} ,
Collector.
Characteristics .
UNORDERED
)
)
这是否更好? 好吧,也许是因为它对每个collect()调用都使用了相同的模式,但是它也更加复杂。 你是法官。 也许我应该硬着头皮实现java.util.stream.Collector而不是java.util.function.Consumer 。
结论
当我将单一用途的应用程序转换为可以处理所有可用数据的通用应用程序时,我发现自己学习了更多关于collect()和Collectors的知识 。 特别是,在处理输入流时需要累积多个值,这意味着我不得不扔掉java.util.stream.Collectors中定义的那些方便且诱人的专用收集器 ,并学习如何构建自己的收集器 。 最后,我想这并不难,但是从使用java.util.stream.Collectors.summingDouble(例如)跳跃()积累双重价值流,以我自己的滚动集电极与Collector.of( )至少对于我而言,为了积累元组流是一次真正的跳跃。
我认为至少有两件事可以使Java Streams用户的生活更加轻松:
- java.util.stream.Collectors.groupingBy()的版本,它接受“分类器”和三个参数,分别对应于由java.util.function.Consumer <定义的“供应商”,“消费者”和“合并者”。 T> (和collect()一样 )
- java.util.stream.Collector.of()的版本,它接受三个参数,分别对应于由java.util.function.Consumer <T>定义的“供应商”,“消费者”和“合并者”。 collect() ),尽管最好使用与of()不同的名称来实现 。
也许有一天,当我对所有这些有了更深入的了解时,我会清楚地知道为什么我们真正需要一个服务于消费者和收集者的类似目的。
也许我接下来的学习工作将是用功能完善的Collector <T,A,R>代替我对Consumer <T>的使用。
无论如何,我希望通过详细说明我的学习途径,可以帮助其他人前往同一目的地。
您对Java Streams有什么经验? 您是否发现自己正在努力从玩具示例过渡到更复杂的实际应用程序?
java streams
1084

被折叠的 条评论
为什么被折叠?



