java8 streams_使用Java 8 Streams进行编程对算法性能的影响

java8 streams

多年来,使用Java进行多范式编程已经成为可能,它支持面向服务,面向对象和面向方面的编程的混合。 带有lambda和java.util.stream.Stream类的Java 8是个好消息,因为它使我们可以将功能性编程范例添加到混合中。 确实,lambda周围有很多炒作。 但是,改变我们的习惯和编写代码的方式是明智的选择,而无需先了解可能隐患的危险吗?

Java 8的Stream类很简洁,因为它使您可以收集数据并将该数据上的多个功能调用链接在一起,从而使代码整洁。 映射/归约算法是一个很好的例子,您可以通过首先从复杂域中选择或修改数据并对其进行简化(“映射”部分),然后将其缩减为一个有用的值来收集数据并将其聚合。

以以下数据类为例(用Groovy编写,这样我就可以免费生成构造函数,访问器,哈希/等于和toString方法的代码!):

//Groovy
@Immutable
class City {String nameList<Temperature> temperatures
}
@Immutable
class Temperature {Date dateBigDecimal reading
}

我可以使用这些类在City对象列表中构造一些随机天气数据,例如:

private static final long ONE_DAY_MS = 1000*60*60*24;
private static final Random RANDOM = new Random();public static List<City> prepareData(int numCities, int numTemps) {List<City> cities = new ArrayList<>();IntStream.range(0, numCities).forEach( i ->cities.add(new City(generateName(), generateTemperatures(numTemps))));return cities;
}private static List<Temperature> generateTemperatures(int numTemps) {List<Temperature> temps = new ArrayList<>();for(int i = 0; i < numTemps; i++){long when = System.currentTimeMillis();when += ONE_DAY_MS*RANDOM.nextInt(365);Date d = new Date(when);Temperature t = new Temperature(d, new BigDecimal(RANDOM.nextDouble()));temps.add(t);}return temps;
}private static String generateName() {char[] chars = new char[RANDOM.nextInt(5)+5];for(int i = 0; i < chars.length; i++){chars[i] = (char)(RANDOM.nextInt(26) + 65);}return new String(chars);
}

第7行使用同样来自Java 8的IntStream类来构造第8-13行进行迭代的范围,从而将新的城市添加到第6行构建的列表中。第22-30行在随机的日期生成随机温度。

如果要计算所有城市在八月记录的平均温度,则可以编写以下函数算法:

Instant start = Instant.now();
Double averageTemperature = cities.stream().flatMap(c ->c.getTemperatures().stream()
).filter(t -> {LocalDate ld = LocalDateTime.ofEpochSecond(t.getDate().getTime(), 0, ZoneOffset.UTC).toLocalDate();return ld.getMonth() == Month.AUGUST;
}).map(t ->t.getReading()
).collect(Collectors.averagingDouble(TestFilterMapReducePerformance::toDouble)
);Instant end = Instant.now();
System.out.println("functional calculated in " + Duration.between(start, end) + ": " + averageTemperature);

第1行用于启动时钟。 然后,代码在第2行从城市列表中创建一个流。然后,我使用flatMap方法(也在第2行)通过创建所有温度的单个长列表来flatMap ,并在第3行传递一个lambda,该lambda返回每个以流的形式列出温度, flatMap方法可以将其附加在一起。 完成此操作后,我将在第4行使用filter方法丢弃所有非8月份以来的数据。 然后,我在第11行调用map方法,将每个Temperature对象转换为一个
BigDecimal以及生成的流,我在第13行使用了collect方法以及一个计算平均值的收集器。 第15行需要一个辅助函数来将BigDecimal实例转换为double ,因为第14行使用double而不是
BigDecimal

/** method to convert to double */
public static Double toDouble(BigDecimal a) {return a.doubleValue();
}

上面清单中的数字运算部分可以用命令式编写,如下所示:

BigDecimal total = BigDecimal.ZERO;
int count = 0;
for(City c : cities){for(Temperature t : c.getTemperatures()){LocalDate ld = LocalDateTime.ofEpochSecond(t.getDate().getTime(), 0, ZoneOffset.UTC).toLocalDate();if(ld.getMonth() == Month.AUGUST){total = total.add(t.getReading());count++;}}
}
double averageTemperature = total.doubleValue() / count;

在命令式的命令式版本中,我以不同的顺序进行映射,过滤和归约,但是结果是相同的。 您认为哪种风格(功能性或命令性)更快,并且提高了多少?

为了更准确地读取性能数据,我需要多次运行算法,以便热点编译器有时间进行预热。 以伪随机顺序多次运行算法,我能够测量出以功能样式编写的代码平均大约需要0.93秒(使用一千个城市,每个城市的温度为一千;使用英特尔笔记本电脑进行计算i5 2.40GHz 64位处理器(4核)。 以命令式风格编写的代码花费了0.70秒,速度提高了25%。

所以我问自己,命令式代码是否总是比功能代码更快。 让我们尝试简单地计算8月记录的温度数。 功能代码如下所示:

long count = cities.stream().flatMap(c ->c.getTemperatures().stream()
).filter(t -> {LocalDate ld = LocalDateTime.ofEpochSecond(t.getDate().getTime(), 0, ZoneOffset.UTC).toLocalDate();return ld.getMonth() == Month.AUGUST;
}).count();

功能代码涉及过滤,然后调用count方法。 另外,等效的命令性代码可能如下所示:

long count = 0;
for(City c : cities){for(Temperature t : c.getTemperatures()){LocalDate ld = LocalDateTime.ofEpochSecond(t.getDate().getTime(), 0, ZoneOffset.UTC).toLocalDate();if(ld.getMonth() == Month.AUGUST){count++;}}
}

在此示例中,运行的数据集与用于计算平均8月温度的数据集不同,命令性代码的平均时间为1.80秒,而功能代码的平均时间略短。 因此,我们无法推断出功能性代码比命令性代码更快或更慢。 这实际上取决于用例。 有趣的是,我们可以使用parallelStream()方法而不是stream()方法来使计算并行运行。 在计算平均温度的情况下,使用并行流意味着计算平均时间为0.46秒而不是0.93秒。 并行计算温度需要0.90秒,而不是连续1.80秒。 尝试编写命令式代码,该命令将数据分割,在内核之间分布计算并将结果汇​​总为一个平均温度,这将需要大量工作! 正是这是想要向Java 8中添加函数式编程的主要原因之一。它如何工作? 拆分器和完成器用于在默认的ForkJoinPool中分发工作,默认情况下,该ForkJoinPool已优化为使用与内核一样多的线程。 从理论上讲,只使用与内核一样多的线程就意味着不会浪费任何时间进行上下文切换,但这取决于所完成的工作是否包含任何阻塞的I / O –这就是我在有关Scala的书中所讨论的。

在使用Java EE应用程序服务器时,生成线程是一个有趣的主题,因为严格来说,不允许您生成线程。 但是由于创建并行流不会产生任何线程,因此无需担心! 在Java EE环境中,使用并行流完全合法!

您也可以使用地图/减少算法来计算8月的温度总数:

int count = cities.stream().map(c ->c.getTemperatures().size()
).reduce(Integer::sum
).get();

第1行从列表中创建流,并使用第2行上的lambda将城市映射(转换)为城市的温度数。第3行通过使用总和将“温度数”流减少为单个值第4行上的Integer类的method。由于流可能不包含任何元素, reduce方法返回Optional ,我们调用get方法获取总数。 我们可以安全地这样做,因为我们知道城市中包含数据。 如果您正在使用可能为空的数据,则可以调用orElse(T)方法,该方法允许您指定默认值(如果没有可用结果时使用)。

就编写功能代码而言,还有另一种编写此算法的方法:

long count = cities.stream().map(c ->c.getTemperatures().stream().count()
).reduce(Long::sum
).get();

使用上述方法,第2行上的lambda通过将温度列表转换为蒸汽并调用count方法来count温度列表的大小。 就性能而言, 这是获取列表大小的一种不好的方法。 在第一个算法中,每个城市有1000个城市,温度有1000个,总计数在160毫秒内计算。 第二种算法将时间增加到280ms! 原因是ArrayList知道其大小,因为它在添加或删除元素时对其进行跟踪。 另一方面,流首先通过将每个元素映射到值1L ,然后使用Long::sum方法减少1L的流来计算大小。 在较长的数据列表上,与仅从列表中的属性查找大小相比,这是相当大的开销。

将功能代码所需的时间与以下命令代码所需的时间进行比较,可以看出该功能代码的运行速度慢了一倍–命令代码计算的平均温度总数仅为80ms。

long count = 0;
for(City c : cities){count += c.getTemperatures().size();
}

通过使用并行流而不是顺序流,再次通过简单地在上面三个清单中的第1行上调用parallelStream()方法而不是stream()方法,结果是该算法平均需要90毫秒,即比命令性代码略长。

计算温度的第三种方法是使用收集器 。 在这里,我使用了一百万个城市,每个城市只有两个温度。 该算法是:

int count = cities.stream().collect(Collectors.summingInt(c -> c.getTemperatures().size())
);

等效的命令性代码为:

long count = 0;
for(City c : cities){count += c.getTemperatures().size();
}

平均而言,功能性列表花费了100毫秒,这与命令性列表花费的时间相同。 另一方面,使用并行流将计算时间减少了一半,仅为50ms。

我问自己的下一个问题是,是否有可能确定需要处理多少数据,因此使用并行流值得吗? 拆分数据,将其提交给ForkJoinPool类的ExecutorService并在计算后将结果收集在一起并不是免费的-它会降低性能。 当可以并行处理数据时,肯定可以计算出来,通常的答案是,这取决于用例。

在此实验中,我计算了一个数字列表的平均值。 我NUM_RUNS地重复工作( NUM_RUNS次),以获得可测量的值,因为计算三个数字的平均值太快了,无法可靠地进行测量。 我将列表的大小从3个数字更改为3百万个,以确定列表需要多大才能使用并行流计算平均值才能得到回报。

使用的算法是:

double avg = -1.0;
for(int i = 0; i < NUM_RUNS; i++){avg = numbers.stream().collect(Collectors.averagingInt(n->n));
}

只是为了好玩,这是另一种计算方法:

double avg = -1.0;
for(int i = 0; i < NUM_RUNS; i++){avg = numbers.stream().mapToInt(n->n).average().getAsDouble();
}

结果如下。 仅使用列表中的三个数字,我就运行了100,000次计算。 多次运行测试表明,平均而言,串行计算花费了20ms,而并行计算则花费了370ms。 因此,在这种情况下,使用少量数据样本,不值得使用并行流。

另一方面,列表中有300万个数字,串行计算花费了1.58秒,而并行计算仅花费了0.93秒。 因此,在这种情况下,对于大量数据样本,值得使用并行流。 请注意,随着数据集大小的增加,运行次数减少了,因此我不必等待很长的时间(我不喝咖啡!)。

列表中的#个数字 平均 时间序列 平均 时间平行 NUM_RUNS
3 0.02秒 0.37秒 100,000
30 0.02秒 0.46秒 100,000
300 0.07秒 0.53秒 100,000
3,000 1.98秒 2.76秒 100,000
30,000 0.67秒 1.90秒 10,000
30万 1.71秒 1.98秒 1,000
3,000,000 1.58秒 0.93秒 100

这是否意味着并行流仅对大型数据集有用? 没有! 这完全取决于手头的计算强度。 以下无效的算法只是加热CPU,但演示了复杂的计算。

private void doIntensiveWork() {double a = Math.PI;for(int i = 0; i < 100; i++){for(int j = 0; j < 1000; j++){for(int k = 0; k < 100; k++){a = Math.sqrt(a+1);a *= a;}}}System.out.println(a);
}

我们可以使用以下清单生成两个可运行对象的列表,它们将完成这项繁重的工作:

private List<Runnable> generateRunnables() {Runnable r = () -> {doIntensiveWork();};return Arrays.asList(r, r);
}

最后,我们可以测量运行两个可运行对象的时间,例如,并行运行(请参见第3行对parallelStream()方法的调用):

List<Runnable> runnables = generateRunnables();
Instant start = Instant.now();
runnables.parallelStream().forEach(r -> r.run());
Instant end = Instant.now();
System.out.println("functional parallel calculated in " + Duration.between(start, end));

使用并行流平均要花费260毫秒来完成两次密集的工作。 使用串行流,平均花费460毫秒,即几乎翻倍。

从所有这些实验中我们可以得出什么结论? 好吧,不可能最终说出功能代码比命令性代码慢,也不能说使用并行流比使用串行流快。 我们可以得出的结论是,程序员在编写对性能至关重要的代码时,需要试验不同的解决方案并测量编码风格对性能的影响。 但是说实话,这不是什么新鲜事! 对我而言,阅读本文后您应该带走的是,总是有很多方法可以编写算法,并且选择正确的方法很重要。 知道哪种方法是对的,这是经验的结合,但更重要的是,尝试使用代码并尝试不同的解决方案。 最后,尽管如此,还是不​​要过早地进行优化!

翻译自: https://www.javacodegeeks.com/2014/05/the-effects-of-programming-with-java-8-streams-on-algorithm-performance.html

java8 streams

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

光纤交换机光纤通道协议介绍

光纤通道&#xff08;FC&#xff0c;Fibre Channel&#xff09;协议是美国国家标准所指定的一种串行高速、低延时、低误码的标准协议能够为存储设备、IP 数据网、音频流等应用提供高速数据传输的骨干网络技术。随着光纤通道在不同领域的应用&#xff0c;在光纤通道协议的基础上…

解决E: 仓库 “http://ppa.launchpad.net/fcitx-team/nightly/ubuntu bionic Release” 没有 Release 文件。

转载声明&#xff1a;本文为博主原创文章&#xff0c;遵循 CC 4.0 BY-SA 版权协议&#xff0c;转载请附上原文出处链接和本声明。 原文链接&#xff1a;在NS-3中安装可视化工具pyviz的一些问题的解决_寻同学的博客-CSDN博客 今天&#xff0c;在更新软件时&#xff0c;使用以下…

POI创建的文档具有不同条件的灵活样式

介绍 这篇文章解释了基于各种标准将样式应用于文档的困难并提供了解决方案。 Java编程中的常见任务之一是根据存储在数据库中的数据创建Excel报告。 在这些情况下&#xff0c;Java程序员使用POI :-)。 这些报告通常具有严格的样式和数据格式规则。 数据通常是SQL查询执行的结果…

在NS-3中安装可视化工具pyviz的一些问题的解决

转载声明&#xff1a;本文为寻同学原创文章&#xff0c;遵循 CC 4.0 BY-SA 版权协议&#xff0c;转载请附上原文出处链接和本声明。 原文链接&#xff1a;https://blog.csdn.net/qq_31676673/article/details/88107454 在NS-3中安装可视化工具pyviz的一些问题的解决 PyViz是一…

什么是光纤交换机?

光纤交换机是一种高速的网络传输中继设备&#xff0c;又叫做光纤通道交换机、SAN交换机&#xff0c;它较普通交换机而言&#xff0c;采用了光纤电缆作为传输介质。光纤传输的优点是速度快、抗干扰能力强。光纤交换机主要有两种&#xff0c;一是用来连接存储的FC交换机。另一种是…

java 调用 js性能_太快了,太变态了:什么会影响Java中的方法调用性能?

java 调用 js性能那么这是怎么回事&#xff1f; 让我们从一个简短的故事开始。 几周前&#xff0c;我提议对Java核心libs邮件列表进行更改 &#xff0c;以覆盖当前final一些方法。 这刺激了一些讨论主题-其中之一是其中一个性能回归通过采取这是一个方法被引入的程度final免遭停…

VMware虚拟机扩展Ubuntu系统磁盘空间

转载&#xff1a;https://www.cnblogs.com/dongry/p/10620894.html 1 首先给虚拟机扩容 虚拟机->设置->硬盘->实用程序->扩展磁盘容量 2 启动Ubuntu系统 2.1 打开终端安装gparted&#xff0c;sudo apt-get install gparted&#xff1b;输入sudo gparted打开gparte…

光纤以太网交换机产品性能介绍

光纤以太网交换机是一款高性能的管理型的二层光纤以太网接入交换机。那么&#xff0c;光纤以太网交换机产品性能有哪些呢&#xff1f;接下来&#xff0c;飞畅科技的小编就来为大家详细介绍下光纤以太网交换机的产品性能&#xff0c;感兴趣的朋友就一起来看看吧&#xff01; 光…

操作方法:具有多个Mongo存储库和Kotlin的Spring Boot 2 Web应用程序

首先&#xff0c;免责声明&#xff1a;如果您正在编写微服务 &#xff08;每个人现在都对吗&#xff1f;&#xff09;并希望它是惯用的 &#xff0c;那么通常不会在其中使用几个不同的数据源。 图片取自Pixabay© https: //pixabay.com/illustrations/software-binary-sys…

2020-09-03解决pip install安装非常慢[Errno 101] 网络不可达问题

转载链接&#xff1a;http://huanyouchen.github.io/2019/11/21/pip-install-package-very-slow/ 问题描述 使用pip安装seaborn提示&#xff1a; sudo python3.5 -m pip install seabornCollecting seabornWARNING: Retrying (Retry(total4, connectNone, readNone, redirect…

环网工业交换机ERPS技术解析

ERPS(Ethernet Ring Protection Switching&#xff0c;以太环网保护切换协议)是ITU开发的一种环网保护协议&#xff0c;也称G.8032。它是一个专门应用于以太环网的链路层协议。它在以太环网完整时能够防止数据环路引起的广播风暴&#xff0c;而当以太环网上一条链路断开时能迅速…

交换机和路由器的区别是什么?

在我们日常生活中&#xff0c;上网很多时候都要用到交换机和路由器&#xff0c;很多人只知道二者都是用于连接上网的设备&#xff0c;但很少知道它们之间有什么区别&#xff0c;以及各自的功能。 交换机和路由器的区别&#xff1a; 路由器可以给你的局域网自动分配IP&#xf…

import _ssl # if we can‘t import it, let the error propagate

转载链接&#xff1a; https://blog.csdn.net/u013398960/article/details/107524068 实测有用

apache flume_Flume:使用Apache Flume收集客户产品搜索点击数据

apache flume这篇文章涵盖了使用Apache flume收集客户产品搜索点击并使用hadoop和elasticsearch接收器存储信息。 数据可能包含不同的产品搜索事件&#xff0c;例如基于不同方面的过滤&#xff0c;分类信息&#xff0c;分页信息&#xff0c;以及进一步查看的产品以及某些被客户…

如何选配合适的百兆或者千兆工业交换机

工业上常常会用到工业交换机&#xff0c;工程商在做小型网络监控方案的时候&#xff0c;经常需要选配合适的百兆工业交换机或千兆工业交换机&#xff0c;但是&#xff0c;到底是配百兆工业交换机还是千兆工业交换机呢&#xff1f;如何计算百兆工业交换机和千兆工业交换机的码率…

2020年9月14日运行代码总结

由于研究ns3gym的使用&#xff0c;官网案例不足&#xff0c;需要运行别人的代码&#xff0c;在本机上运行成功&#xff0c;但在仿真机过程中出现一类列问题。一开始以为环境配置问题&#xff0c;升级环境与python3.6&#xff0c;但在python3.6.12的安装与python默认版本的更改中…

使用Selenium自动化测试处理多个浏览器选项卡

使用Selenium进行自动化测试一直是将萌芽的自动化测试人员培养为专业人员的生命线。 硒是开源的&#xff0c;在全球范围内被广泛采用。 结果&#xff0c;您会得到社区的大力支持。 有多种用于不同语言的框架&#xff0c;这些框架提供与Selenium的绑定。 因此&#xff0c;您已经…

三层交换机工作原理介绍

每个网络主机、工作站或者服务器都有自己的IP地址和子网掩码。当主机与服务器进行通信的时候&#xff0c;根据自身的IP地址和子网掩码、以及服务器的IP地址&#xff0c;来确定服务器是否和自己处于相同的网段&#xff1a; 1、如果判定在相同网段内&#xff0c;则直接通过地址解…

TCP流中各种队列:

TCP流中各种队列&#xff1a; RED队列的介绍 [https://blog.csdn.net/sinat_20184565/article/details/107521549]

三层交换机有什么优势?

三层交换机的技术日益成熟&#xff0c;应用日益广泛&#xff0c;在一定范围内&#xff0c;它比路由器更具优势&#xff0c;但是三层交换机与路由器还是有很大的区别&#xff0c;在局域网中&#xff0c;三层交换机有着明显的优势。 1、子网间的传输带宽可以任意分配。 在传统路…