在这篇文章中,我们将重点介绍如何使用Java流处理数据的后处理。我们还将看到非常强大的技术来优化和排序结果。
数字技术收集的海量数据创造了大数据分析的爆炸式增长。它允许公司、政府和其他组织发现模式并预测未来的行为。例如,它可以帮助销售预测,营销活动,解决和预防犯罪等。可能性是不同的和无限的。
同样,人工智能应用程序和神经网络特别使用大数据集。在用神经网络建模的复杂问题上,收集大量格式化和标注的数据是非常重要的。我们将这些数据分成训练和测试数据集,我们使用这些数据来获得模型的权重矩阵的值。
前几点的重点是将处理大量数据的需求带回家,我们用这些应用程序格式化、标记和优化这些数据。通常,在处理数据之前和之后都需要进行这种数据操作。由于所涉及的数据量很大,很难格式化、处理和有效地存储这些数据。
在本文中,我们将重点讨论如何使用Java流处理数据的后处理。我们将看到非常强大的技术来优化和排序结果。重点将放在提供非常有效的机制来处理数据、轻松地并行计算和允许使用功能接口快速交换实现。
优化技术
让我们考虑一个简单的例子,其中我们有一个要处理的事件列表。我们并不关心如何获得此列表,无论是批处理、分析工具,还是为我们选择要优化、排序和分类的候选事件列表的任何其他机制。
我们将要考虑的事件非常简单,并且有一个非常通用的结构。这只是为了演示的目的。我们也可以考虑其他实体,如客户,住房,报告等。接下来的分析将适用于广泛的案例。
我们定义具有以下实现的事件:
public static record Event(String location, int price, String eventType, int eventRankId) {}
注意,我们使用Java记录若要定义事件,请执行以下操作。这个记录是定义类的简明方法。该实体有四个字段:位置、价格、事件类型和事件RankId。EventRankId是唯一定义事件的主键。
首先,让我们解决一个简单的问题:假设我们希望选择一个由10个事件组成的列表,这些事件离给定的位置最近。主要的要求是我们希望这个操作是可并行的和可执行的。同时,我们希望实现此选择的算法是可插拔的,因此当我们想要更改实现时,我们可以无缝地交换它。
作为一个例子,让我们编写下面的代码来获得两个位置之间的距离:
public static int evaluateLocationProximity(String location1, String location2) {
return Math.abs(location1.hashCode() - location2.hashCode());
}
此函数以两个位置作为参数,并返回一个表示它们之间距离的数字。方法签名及其实现是不现实的,在这里只是为了演示目的。在生产环境中,该函数可以找到两个地点之间的最佳路线,并根据旅行时间、物理邻近性或便利性分配一个值。返回值越小,这两个位置就越近,从一般意义上说,我们已经定义了它们。
我们需要将这个算法应用到给定的事件列表中。我们希望根据此函数的返回值对事件进行排序。
让我们使用JavaStreams查看以下实现
public static List<Event> getNearEvents(int number, List<Event> events, String targetLocation) {
Map<Integer, Set<Event>> distanceEventsMap = events.stream().
collect(Collectors.groupingBy(
event -> evaluateLocationProximity(event.location(), targetLocation),
TreeMap::new,
Collectors.toCollection(() -> new TreeSet<>(
Comparator.comparingInt(Event::eventRankId)))));
return distanceEventsMap.values()
.stream()
.flatMap(Set::stream)
.limit(number)
.collect(Collectors.toList());
}
函数参数获取我们希望返回的事件数量、事件的输入列表以及事件必须最接近的目标位置。
在实现中,我们使用静态函数。Collectors::groupingBy(第4行)。它的第一个参数组使用evaluateLocationProximity()作为分类器的功能。在第二个参数中,映射工厂生成一个TreeMap为了保持顺序,映射的键是事件到目标位置之间的距离,值是对应的事件。最后,最后一个参数是下游收集器,我们使用Collectors::toCollection获取一个收集器,该收集器累加具有相同键值并由Event::eventRankId.
最后,从Map<Integer, Set>我们获取这些值,使事件集变平,并对返回的列表设置一个限制。
我们之前发布的问题的解决方案,例如,在给定的地点找到最好的10项活动,如“纽约中央广场”,将是:
List<Event> events = //list of initial input list
List<Event> bestEvents = getNearEvents(10, events, "Grand Central NYC");
我们得到一个子列表,其中包含按距离排序的10个最近的事件,如果两个事件与目标位置的距离相同,则由eventRankId.
使用函数接口
有时,我们使用的算法必须进行修改或调整,以应对市场环境的变化。功能接口是一种简单的技术,它允许我们轻松地更改实现。例如,考虑上一个经过以下实现修改的示例:
public static List<Event> getNearEvents(int number, List<Event> events, String targetLocation,
BiFunction<String, String, Integer> locationProximityEvaluator) {
Map<Integer, Set<Event>> distanceEventsMap = events.stream().
collect(Collectors.groupingBy(
event -> evaluateLocationProximity(locationProximityEvaluator.apply(event.location(), targetLocation),
TreeMap::new,
Collectors.toCollection(() -> new TreeSet<>(
Comparator.comparingInt(Event::eventRankId)))));
return distanceEventsMap.values()
.stream()
.flatMap(Set::stream)
.limit(number)
.collect(Collectors.toList());
}
我们使用Java BiFunction来传递算法的实现。
现在调用此函数非常简单:
getNearEvents(10, events, "New York", (l1, l2) -> Math.abs(l1.hashCode() - l2.hashCode()));
该技术不仅可以将排序与优化算法解耦,而且可以方便地改变算法的实现。这在开发过程中很有用,但在运行时也很有用。
并行实现
使用Java流可以方便地使用并行处理来提高函数的性能。最简单的方法就是在前面的函数(第3行)中调用“并行”。getNearEvents() :
Map<Integer, Set<Event>> distanceEventsMap = events.stream().parallel()
这个简单的修改会导致框架将集合细分为各个部分,并在分离的线程中运行它们。在处理所有部件时,框架将所有这些元素依次组合到前面的TreeMap 给你结果。这是一个非常令人印象深刻的结果,只要向流中添加一个简单的“并行”方法,框架就会利用运行环境中的多核资源来提高应用程序的性能。
但是,我们可以做得更好,并编写一个优化代码的实现。getNearEvents(),使用
Collectors::groupingByConcurrent在第4行而不是Collectors:groupingBy:
Collectors.groupingByConcurrent(
event -> evaluateLocationProximity(event.location(), targetLocation),
ConcurrentSkipListMap::new,
Collectors.toCollection(() -> new TreeSet<>(
Comparator.comparingInt(Event::eventRankId)))));
与以前的实现不同的是,对于第二个参数,我们用TreeMap::new带着
ConcurrentSkipListMap::new. ConcurrentSkipListMap 提供Map的可伸缩并发实现,在本例中,该实现按键的自然排序(两个事件之间的距离)排序。好处是groupingByConcurrent将来自不同线程的元素直接累加到此Map中,而不必按顺序组合。
因此,以下功能展示了如何完成此实现,同时保持事件的顺序:
List<Event> getNearEventsConcurrent(int number, List<Event> events, String targetLocation) {
Map<Integer, Set<Event>> distanceEventsMap = events.stream().parallel().
collect(Collectors.groupingByConcurrent(
event -> evaluateLocationProximity(event.location(), targetLocation),
ConcurrentSkipListMap::new,
Collectors.toCollection(() -> new TreeSet<>(
Comparator.comparingInt(Event::eventRankId)))));
return distanceEventsMap.values()
.stream()
.flatMap(Set::stream)
.limit(number)
.collect(Collectors.toList());
}
总括而言,我们检讨了3宗个案:
- 串行处理(getNearEvents())
- 使用相同的方法,但添加并行流调用
- getNearEventsConcurrent()来执行并行计算。groupingByConcurrent.
运行一个包含1,000万个事件列表的快速测试,我们为每一种情况得到以下结果:
启动进程系列,选定的事件:
事件[位置=mtrddza,价格=10,事件类型=drihps,事件rankId=5255511].
进程持续时间:16022毫秒
启动过程并行,选定的事件:
事件[位置=mtrddza,价格=10,事件类型=drihps,事件rankId=5255511].
进程持续时间并行:12014毫秒
同时启动进程,选择事件:
事件[位置=mtrddza,价格=10,事件类型=drihps,事件rankId=5255511].
进程持续时间并发:10615毫秒
我们看到,并行实现比串行进程具有更高的性能。但最好的结果是groupingByConcurrent()超过50%的速度。
到目前为止,我们已经回顾了实现几种优化技术的方法,以获得与给定目标位置最近的事件。如果两个事件具有相同的值,我们将根据它们的事件级别id对它们进行排序。正如我们将在下一节中看到的,使用Java流,我们可以进行更复杂的选择,在这些选择中,我们可以在无限条件下找到事件。
使用多个选择进行优化
Java流允许我们进行多级缩减。我们将使用这种强大的技术来排序应用多个条件的事件列表。我们在Collectors::goupingBy实现,可以在其第三个参数中指定下游收集器。这允许我们对原始选择进行子分组。
让我们考虑一个例子,在这个例子中,我们希望按位置邻近程度来分类事件,但是对于具有相同值的事件,我们想要用其他方法来排序它们,这就是我们所称的亲和力。对于这个简单的例子,我们将使用以下函数定义关联:
int evaluateProvidersAffinity(String localEventType, int localPrice, String targetEvtType, int targetPrice) {
if (localEventType.equalsIgnoreCase(targetEvtType))
return Math.abs(localPrice - targetPrice);
else
return (100 + Math.abs(localPrice - targetPrice));
}
该实现比较价格和事件类型,以获得与目标值最接近的值;
有了这一点,我们现在可以扩展我们以前实现的getNearEvents()要演示如何对事件位置和关联进行优化和排序:
List<Event> getEventsByProximityAndAffinity(int number, List<Event> events,
String targetLocation, String targetType, int targetPrice) {
Map<Integer, Map<Integer, Set<Event>>> chosenEvents = events.stream()
.collect(groupingBy(event -> evaluateLocationProximity(event.location(),
targetLocation),
TreeMap::new,
groupingBy(event -> evaluateProvidersAffinity(event.eventType(),
event.price(), targetType, targetPrice),
TreeMap::new,
Collectors.toCollection(() ->
new TreeSet<>(Comparator.comparingInt(
Event::eventRankId))))));
return chosenEvents.values()
.stream()
.flatMap(map -> map.values(
.stream()
.flatMap(Set::stream))
.limit(number)
.collect(Collectors.toList());
}
就像我们以前一样,第一次Collectors::groupingBy采取evaluateLocationProximity()作为分类函数,我们生成一个TreeMap 来维持秩序。对于下游收集器,我们使用Collectors::groupingBy使用evaluateProvidersAffinity()作为分类函数,具有与以前相同的顺序。
结果是Map<Integer, Map<Integer, Set>>其中,第一个映射的键是分配给每个事件的“距离”,其值是另一个映射,其中键是与该事件的亲缘关系的值,其值是事件列表。因此,这些事件对应于给定的“距离”和“亲和力”。
最后一部分是返回事件列表,方法是执行两次平面映射,提取按“距离”、“亲和力”和“RandId”排序的事件。
我们可以继续这一技术,使事件具有更多的属性。
结论
我们已经回顾了使用Java流从使用多个算法优化的组中选择有序元素的强大技术。
通过简单的修改,可以将计算并行化,有效地利用计算资源,最大限度地提高实现速度。
在将来的出版物中,我们将深入研究如何横向扩展这个应用程序,以处理大量的数据。
最后
小伙伴们如果觉得我写得不错,可以点赞+收藏!需要以上资料的小伙伴关注博主获取免费资料