感动,我终于学会了Java对数组求和

简介:

感动,我终于学会了Java对数组求和

看到题目是不是有点疑问:你确定你没搞错?!数组求和???遍历一遍累加起来不就可以了吗???

是的,你说的都对,都听你的,但是我说的就是数组求和,并且我也确实是刚刚学会。╮(╯▽╰)╭

继续看下去吧,或许你的疑问会解开↓

注:记录于学习完《Java 8 实战》数据并行处理与性能,如果有错误,欢迎大佬指正

0|1传统方式

求和方法
我相信你和我一样,提到数组求和,肯定最想想到的就是将数组迭代一遍,累加迭代元素。这是最简单的一种方式,代码实现如下:

public static long traditionSum(long[] arr){
//和
long sum = 0;
//遍历数组中的每个元素
for (long l : arr) {
//累加
sum += l;
}
return sum;
}
性能测试方法
为了便于我们测试性能,我们写一个比较通用的测试函数,用来记录对每种方式的运行时间,直接看代码吧!

public static long test(Function function, long[] arr){
//记录最快的时间
long fasttime = Long.MAX_VALUE;
//对函数调用10次
for (int i = 0; i < 10; i++) {
//记录开始的系统时间
long start = System.nanoTime();
//执行函数
long result = function.apply(arr);
//获取运行时间转换为ms
long time = (System.nanoTime() - start) / 1_000_000;
//打印本次的就和结果
System.out.println("结果为:" + result);
//更新最快的时间
if (time < fasttime) {
fasttime = time;
}
}
return fasttime;
}
性能测试代码解释
传入参数Function function: 我们需要测试的函数,稍后我们会把每种求和方式都传入到这个参数里面。如果你对java 8的新特性(Lambda表达式、行为参数化、方法引用等)不熟悉,那么你可以理解为Function是一个匿名类,我们传入的求和方法会放到function.apply()的方法中,我们调用apply()方法,实际上就是调用我们传入的求和方法。
Function的泛型: 第一个为我们求和方法需要传入的参数的类型(传入一个long类型的数组作为待求和数组),第二个为我们的求和方法返回值的类型(返回数组的和为long)
long[] arr:待求和数组
关于为什么会调用10次:任何的Java代码都需要多执行几次才会被JIT编译器优化,多执行几次是为了保证我们测量性能的准确性。
数据准备
方法有了,我们当然要准备好我们的测试数据了,为了简便起见,我们直接顺序生成1到100,000,000(1亿)来最为待求和的数组:

long[] longs = LongStream.rangeClosed(1, 100_000_000).toArray();
测试性能
数据有了,我们可以测试一下传统方式的性能了(所在类TestArraysSum)

public static void main(String[] args) {
long[] longs = LongStream.rangeClosed(1, 100_000_000).toArray();
//执行测试函数
long time = test(TestArraysSum::traditionSum, longs);

System.out.println("时间为: " + time + "ms");

}
结果:

结果为:5000000050000000
结果为:5000000050000000
结果为:5000000050000000
结果为:5000000050000000
结果为:5000000050000000
结果为:5000000050000000
结果为:5000000050000000
结果为:5000000050000000
结果为:5000000050000000
结果为:5000000050000000
时间为: 62ms
继续看其他方式

0|1Stream流的顺序执行方式

求和方法
java 8的流可谓是非常的强大,配合lambda表达式和方法引用,极大的简化了对数据处理方面,下面是使用流对数组进行顺序求和

public static long sequentialSum(long[] arr){
return Arrays.stream(arr)
.reduce(0L, Long::sum);
}
代码解释
Arrays.stream(arr)将我们传入的数组变为一个流(此处没有Java包装类与原始类型的装箱和拆箱,装箱和拆箱会极大影响性能,应该尽量避免)
.reduce(0L, Long::sum):0L是初始值,Long::sum通过方法引用的方式使用Long提供的求和函数,对数组的每一个元素都进行求和
性能测试
Java 8让我们的代码极大的简化了,那么性能如何呢?
我们将main方法内执行求和方法部分换为调用这个方法看看

long time = test(TestArraysSum::sequentialSum, longs);
结果为:5000000050000000
结果为:5000000050000000
结果为:5000000050000000
结果为:5000000050000000
结果为:5000000050000000
结果为:5000000050000000
结果为:5000000050000000
结果为:5000000050000000
结果为:5000000050000000
结果为:5000000050000000
时间为: 62ms
emmmm 好像差不多,Ծ‸Ծ,先不急,Java 8的流给我们带来的另一大好处还没用上呢,下面我们就来看看吧

0|1Stream流的并行执行

求和方法
Java 8 的Stream流可以让我们非常简单的去使用多线程解决问题,而我们的求和需求好像完美适合多线程问题去解决

public static long parallelSum(long[] arr){
return Arrays.stream(arr)
.parallel()
.reduce(0L, Long::sum);
}
代码解释
.parallel():与顺序流实现相比,仅仅是多调用了一个parallel()方法,他的作用就是将顺序流转化为并行流(其实就是改变了一下boolean标志),如何并行执行呢,不用我们实现,无脑调用就好了
性能测试
long time = test(TestArraysSum::parallelSum, longs);
结果

结果为:5000000050000000
结果为:5000000050000000
结果为:5000000050000000
结果为:5000000050000000
结果为:5000000050000000
结果为:5000000050000000
结果为:5000000050000000
结果为:5000000050000000
结果为:5000000050000000
结果为:5000000050000000
时间为: 52ms
哦吼~这就很舒服了,是不是瞬间就快了

注:并行流内部默认使用ForkJoinPool的线程池,线程数量默认为计算机处理器的数量,使用Runtime.getRuntime().availableProcessors()可以获取处理器核心数
(我的测试环境是8个),可是设置这个值,但是只能全局设置,所以最好还是不要更改

是不是疑问我们除了调用parallel()方法以外什么都没干,究竟是怎么实现多线程的呢,其实并行流底层使用的是Java 7的分支/合并框架,下面我们就看一下使用分支/合并框架实现多线程求和吧!

0|1分支合并框架的实现方式

分支合并框架的目的是以递归的方式将可以并行的任务拆分成更小的子任务,然后将每个子任务的结果进行合并生成整体结果。

求和方法
我们可以继承RecursiveTask实现其compute()方法
分支合并实现的类ForkJoinSumCalculator

package java_8.sum;

import java.util.concurrent.RecursiveTask;

public class ForkJoinSumCalculator extends RecursiveTask {

//任务处理的数组
private final long[] arr;
//当前任务处理的开始和结束索引
private final int start;
private final int end;

//划分到处理数组的长度10_000_000变不来划分,进而合并
public static final long THRESHOLD = 10_000_000;

//公共的构造函数,用来创建主任务
public ForkJoinSumCalculator(long[] arr){
this(arr,0,arr.length);
}

//私有的构造函数,用来创建子任务
private ForkJoinSumCalculator(long[] arr, int start, int end){
this.arr = arr;
this.start = start;
this.end = end;
}

//实现的方法
@Override
protected Long compute() {
//当时子任务处理长度
int length = end - start;
//当数组处理长度足够小时
if (length <= THRESHOLD){
//进行合并
return computeSequentially();
}

//创建第1个子任务对前面一半数组进行求和
ForkJoinSumCalculator leftTask = new ForkJoinSumCalculator(arr, start, start + length / 2);
//使用线程池中的另一个线程求和前一半
leftTask.fork();
//创建第2个子任务对后一半数组进行求和
ForkJoinSumCalculator rightTask = new ForkJoinSumCalculator(arr, start + length / 2, end);
//直接使用当前线程进行求和 获取求和结果
Long rightResult = rightTask.compute();
//获取前一半的求和结果
Long leftTesult = leftTask.join();
//合并
return leftTesult + rightResult;

}

//合并是的调用方法 迭代求和
private long computeSequentially(){
long sum = 0;
for (int i = start; i < end; i++) {
sum += arr[i];
}
return sum;
}
}

public static final long THRESHOLD = 10_000_000;
划分的界线使我随便设定的当前值的情况下会划分为10个线程

然后我们就可以编写我们的求和方法了

public static long forkJoinSum(long[] arr){
ForkJoinSumCalculator calculator = new ForkJoinSumCalculator(arr);
return new ForkJoinPool().invoke(calculator);
}
性能测试
long time = test(TestArraysSum::forkJoinSum, longs);
结果:

结果为:5000000050000000
结果为:5000000050000000
结果为:5000000050000000
结果为:5000000050000000
结果为:5000000050000000
结果为:5000000050000000
结果为:5000000050000000
结果为:5000000050000000
结果为:5000000050000000
结果为:5000000050000000
时间为: 53ms
还不错,跟并行流的性能差不多

由于分支合并时的递归调用也消耗性能,因此我们更改public static final long THRESHOLD = 10_000_000;的大小时,运行时间会差距很大。
具体更改多少效率最高,这个真的不好说

0|1总结

使用了4种方式完成数组求和
使用传统方式(遍历)效率其实也不低,因为实现方式比较接近底层
使用流极大简化了数组处理
并行流在适合的场景下可以大展身手
并行流使用分支合并框架实现

EOF

作  者:erkye
出  处:https://www.cnblogs.com/erkye/p/12686223.html

相关文章
|
7月前
|
Java
Java 数组学习笔记
本文整理Java数组常用操作:遍历、求和、查找、最值及二维数组行求和等典型练习,涵盖静态初始化、元素翻倍、去极值求平均等实例,帮助掌握数组基础与应用。
|
8月前
|
存储 缓存 Java
Java数组全解析:一维、多维与内存模型
本文深入解析Java数组的内存布局与操作技巧,涵盖一维及多维数组的声明、初始化、内存模型,以及数组常见陷阱和性能优化。通过图文结合的方式帮助开发者彻底理解数组本质,并提供Arrays工具类的实用方法与面试高频问题解析,助你掌握数组核心知识,避免常见错误。
|
9月前
|
存储 Java 索引
java 数组
在 Java 中,数组是一种数据结构,用于存储多个相同类型的数据元素。数组的大小一旦创建后就不能改变,因此它是固定长度的。Java 数组是一种 对象,即使它存储的值是基本类型(如 int、double 等),它也是一个对象引用。
217 0
|
11月前
|
存储 人工智能 Java
打乱数组内容引发的问题( Java)
本文介绍了两种实现数组随机打乱的方法,并深入探讨了Java中原始数据类型与对象类型的差异。方法一通过自定义随机数交换数组元素位置,方法二借助`Collections.shuffle()`函数完成数组打乱。同时,文章详细解析了`int`和`Integer`的区别,包括声明方式、内存占用、初始化以及对象特性等,并讲解了自动装箱与拆箱的功能,帮助读者更好地理解Java的基础知识。
177 0
|
存储 Java 数据挖掘
Java 中数组的多种定义方式
本文深入解析了Java中数组的多种定义方式,涵盖基础的`new`关键字创建、直接初始化、动态初始化,到多维数组、`Arrays.fill()`方法以及集合类转换为数组等高级用法。通过理论与实践结合的方式,探讨了每种定义方法的适用场景、优缺点及其背后的原理,帮助开发者掌握高效、灵活的数组操作技巧,从而编写更优质的Java代码。
631 0
|
人工智能 Java
Java 中数组Array和列表List的转换
本文介绍了数组与列表之间的相互转换方法,主要包括三部分:1)使用`Collections.addAll()`方法将数组转为列表,适用于引用类型,效率较高;2)通过`new ArrayList&lt;&gt;()`构造器结合`Arrays.asList()`实现类似功能;3)利用JDK8的`Stream`流式计算,支持基本数据类型数组的转换。此外,还详细讲解了列表转数组的方法,如借助`Stream`实现不同类型数组间的转换,并附带代码示例与执行结果,帮助读者深入理解两种数据结构的互转技巧。
935 1
Java 中数组Array和列表List的转换
|
存储 Java 索引
Java 复制数组
本文介绍了Java中数组的基础知识与常用操作,包括数组的概念、创建、访问元素、遍历、复制、排序和搜索等方法。同时详细讲解了数组的五种赋值方式,并通过代码示例演示了求总和平均值、最大最小值、升序降序排序及Arrays类的常用方法。内容深入浅出,适合初学者学习掌握Java数组的核心功能与应用场景。
295 2
|
存储 监控 Java
《从头开始学java,一天一个知识点》之:数组入门:一维数组的定义与遍历
**你是否也经历过这些崩溃瞬间?** - 看了三天教程,连`i++`和`++i`的区别都说不清 - 面试时被追问&quot;`a==b`和`equals()`的区别&quot;,大脑突然空白 - 写出的代码总是莫名报NPE,却不知道问题出在哪个运算符 这个系列就是为你打造的Java「速效救心丸」!我们承诺:每天1分钟,地铁通勤、午休间隙即可完成学习;直击痛点,只讲高频考点和实际开发中的「坑位」;拒绝臃肿,没有冗长概念堆砌,每篇都有可运行的代码标本。明日预告:《多维数组与常见操作》。 通过实例讲解数组的核心认知、趣味场景应用、企业级开发规范及优化技巧,帮助你快速掌握Java数组的精髓。
436 23
|
存储 Java C++
Java数组:静态初始化与动态初始化详解
本文介绍了Java中数组的定义、特点及初始化方式。
1156 12
下一篇
开通oss服务