简介
java8
为我提供的简单快捷的数值流计算API
,本文就基于几个常见的场景介绍一下数值流API
的使用。
基础示例
我们以一个食物热量计算的功能展开演示,如下所示,可以看到Dish
类它记录了每一个食物的名称、热量、类型等信息:
public class Dish {
/**
* 名称
*/
private final String name;
/**
* 是否是素食
*/
private final boolean vegetarian;
/**
* 卡路里
*/
private final int calories;
/**
* 类型
*/
private final Type type;
//类型枚举 分别是是:肉类 鱼类 其他
public enum Type {MEAT, FISH, OTHER}
public Dish(String name, boolean vegetarian, int calories, Type type) {
this.name = name;
this.vegetarian = vegetarian;
this.calories = calories;
this.type = type;
}
//...... get set
}
基于这个食物类,我们给出一个食物类的集合作为模拟数据:
public static final List<Dish> menuList =
Arrays.asList(
new Dish("pork", false, 800, Dish.Type.MEAT),
new Dish("beef", false, 700, Dish.Type.MEAT),
new Dish("chicken", false, 400, Dish.Type.MEAT),
new Dish("french fries", true, 530, Dish.Type.OTHER),
new Dish("rice", true, 350, Dish.Type.OTHER),
new Dish("season fruit", true, 120, Dish.Type.OTHER),
new Dish("pizza", true, 550, Dish.Type.OTHER),
new Dish("prawns", false, 400, Dish.Type.FISH),
new Dish("salmon", false, 450, Dish.Type.FISH)
);
我们希望计算出这个菜肴集合的总热量,我们可能会这样写:
public static void main(String[] args) {
int total = menuList.stream()
//获取每个食物的卡路里
.map(Dish::getCalories)
//调用reduce,从0开始累加每个食物的热量
.reduce(0, Integer::sum);
System.out.println(total);
}
输出结果如下:
4300
尽管它尽可能的简洁并计算出了总热量,但是它存在许多隐患,首先时map时,它会将基本类型的calories
装箱成Integer
,这一点我们查看map
的返回值即可知晓。
Stream<Integer> integerStream = menuList.stream()
//获取每个食物的卡路里
.map(Dish::getCalories);
因为拿到的是包装类的流,调用reduce进行数值计算时,有需要对其进行拆箱,拆箱时就会调用到Integer
的intValue
方法:
public int intValue() {
return value;
}
所以若在大量数值计算的情况下,频繁的拆箱和装箱势必导致程序的执行效率低下。
特值流
那么有没有什么办法可以保证在数据收集的时候避免频繁装箱和拆箱呢?答案是特化流,就以本案例来说,我们在数值收集的时候直接调用mapToInt
方法,通过该方法即可得到每一个数值的特值流IntStream
,随后我们直接调用特值流计算方法sum
即可完成热量统计:
对应的代码示例如下:
public static void main(String[] args) {
int total = menuList.stream()
//将每一个卡路里转换为特值流IntStream
.mapToInt(Dish::getCalories)
//将所有数值累加
.sum();
System.out.println(total);
}
最终输出结果也是4300:
4300
相较于reduce
方法,特值流提供了更多更方便的计算API
:
average
:计算所有数值的平均数。count
:获取数值总数。max
:获取收集数据中的最大值。min
:获取收集数据中的最小值。
特化流还原会原始流
有时候我们希望这些特化流转为原始流即包装类的流,那么我们可直接调用boxed
方法完成对特值流的装箱:
public static void main(String[] args) {
Stream<Integer> integerStream = menuList.stream()
//拿到所有数值的特值流
.mapToInt(Dish::getCalories)
//将所有特值流装箱
.boxed();
//输出特值流对象的数值
integerStream.forEach(i -> System.out.println(i));
}
特化流空数值问题
我们都知道特化流可以直接获取收集到数值的最大值或者最小值,我们假设这样一个场景,食物类对象的卡路里字段为Integer
:
private final Integer calories;
并且我们食物类的集合为空:
public static final List<Dish> menuList = new ArrayList<>();
面对可能存在的空结果问题,要如何解决呢?
实际上java8
已经考虑到这个问题了,当我们调用max
等计算API
获取结果时,它实际返回的对象是OptionalInt
,该对象提供了各种API用于判断数值是否为空,当我们最大值为空,就直接返回1时,我们可以直接使用orElse
方法:
public static void main(String[] args) {
OptionalInt max = menuList.stream()
.mapToInt(Dish::getCalories)
.max();
//不存在最大值时,直接返回1
System.out.println(max.orElse(0));
}
亦或者我们需要判断是否存在最大值时,可以直接调用isPresent
方法:
public static void main(String[] args) {
OptionalInt max = menuList.stream()
.mapToInt(Dish::getCalories)
.max();
//若存在最大值直接返回true
System.out.println(max.isPresent());
}
数值流的范围操作
我们希望统计1-100之间的偶数数量,在java8
之前,你可能会这样做:
for
循环1-100。- 判断是否是偶数。
- 如果是偶数,则临时变量
count
自增一下。
而java8
的步骤则精简许多:
- 基于特值流生成1-100全闭区间数据。
- 过滤出偶数。
- 调用
count
进行统计。
public static void main(String[] args) {
//生成1-100全闭区间数据
long count = IntStream.rangeClosed(1, 100)
//过滤出偶数
.filter(i -> i % 2 == 0)
//计算统计结果
.count();
System.out.println(count);
}
输出结果:
50
当然,如果你要生成左闭右开即1-99,则可以调用range
方法生成:
IntStream.range(1, 100)
数值流的应用——勾股数
现在我们来写一个获取1-100以内前3个勾股数的小功能。由公式:
a^2 + b^2=c^2
可知,要想得到勾股数,我们只需判断a^2 + b^2
的和再开根号是否可以被整除,即:
Math.sqrt(a * a + b * b) % 1 == 0
所以我们可以按照下面这样的步骤执行:
- 创建1-100全闭区间作为第一条边a。
- 为避免计算的勾股数重复,出现[
3,4,5]
,[4,3,5]
这种情况,我们的第二条边b范围为a-100。 - 拿着a和b,计算这两个数值的平方和再开根号看看是否为整数。
- 将开根号结果为整数的结果生成数组。
- 获取前3个这样的数组。
所以我们写出下面这段代码,需要注意的是笔者在生成b的时候用到了flatMap,原因很简单,因为生成a时boxed
返回的对象是Stream<Integer>
,假如把这个流直接用map
和b进行映射操作的话,最终结果只能是[Stream<Integer>,Integer,Integer]
,所以我们需要使用flatMap
将a进行扁平化从而得到一个Integer
:
public static void main(String[] args) {
//生成a
Stream<int[]> result = IntStream.rangeClosed(1, 100).boxed()
//基于a的范围生成 a-100范围的b,并过滤出平方再开方后可以整除的b,构成数组
.flatMap(a -> IntStream.rangeClosed(a, 100).filter(b -> Math.sqrt(a * a + b * b) % 1 == 0).boxed().map(b -> new int[]{a, b, (int) Math.sqrt(a * a + b * b)}))
//取前3个
.limit(3);
//打印输出
result.forEach(r -> System.out.println(r[0] + " " + r[1] + " " + r[2]));
}
最终输出结果如下:
3 4 5
5 12 13
6 8 10
但是这种写法不够好,可以看到我们得到合适a和b时,还需要手动调用boxed
将其还原为原始流,再用map
映射为数组,这样实在太麻烦了。
还记得我们特化流还原为原始流的一个方法mapToxxx
方法吗?如果我们希望将其转为数组,我们在得到a和b之后,直接调用mapToObj
,代码一步到位:
public static void main(String[] args) {
//生成a
Stream<int[]> result = IntStream.rangeClosed(1, 100).boxed()
//基于a的范围生成 a-100范围的b,并过滤出平方再开方后可以整除的b,构成数组
.flatMap(a -> IntStream.rangeClosed(a, 100).filter(b -> Math.sqrt(a * a + b * b) % 1 == 0).mapToObj(b -> new int[]{a, b, (int) Math.sqrt(a * a + b * b)}))
//取前3个
.limit(3);
//打印输出
result.forEach(r -> System.out.println(r[0] + " " + r[1] + " " + r[2]));
}
参考资料
Java 8 in Action:https://book.douban.com/subject/25912747/