在学习这个函数的用法之前,我们要先知道这个函数参数的意义
先举一个简单的例子:
算法题:Words
题目描述
每个句子由多个单词组成,句子中的每个单词的长度都可能不一样,我们假设每个单词的长度Ni为该单词的重量,你需要做的就是给出整个句子的平均重量V。
解答要求
时间限制:1000ms, 内存限制:100MB
输入
输入只有一行,包含一个字符串S(长度不会超过100),代表整个句子,句子中只包含大小写的英文字母,每个单词之间有一个空格。
输出
输出句子S的平均重量V(四舍五入保留两位小数)。
Who Love Solo
输出样例
3.67
这道题的意思是求一句话中每个单词的平均长度,我们求得总长度然后除以单词数量即可,刚好能用到reduce()这个方法。
public class Demo {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
String[] s = sc.nextLine().split(" ");
double res = Arrays.stream(s).mapToDouble(a ->a.length()).reduce(0,(a,b)->a+b);
System.out.println(String.format("%.2f",res/s.length));
}
}
在代码中,.reduce(0,(a,b)->a+b);
这一块就是我们经典的使用案例,我们要先明白其中a,b的含义,然后再学习如何使用
关键概念:初始值的定义(Identity),累加器(Accumulator),组合器(Combiner)
也就是说0
就是我们的初始值,(a,b)->a+b
就是我们的累加器,其中a
就是上一次的计算结果,b
就是Stream流中当前元素,而后面的a+b
则是计算规则,比如如果我们改成a*b
,那就是计算乘积了,当然我们也可以用方法引用来代替 lambda 表达式。
double res = Arrays.stream(s).mapToDouble(a ->a.length()).reduce(0,Double::sum);
这就是最基本的使用了,不知道小伙伴们有没有学会呢?
当然,我们可以用reduce 方法处理其他类型的 stream,例如,可以操作一个 String 类型的数组,把数组的字符串进行拼接。
List<String> letters = Arrays.asList("a", "b", "c", "d", "e");
String result = letters
.stream()
.reduce("", (partialString, element) -> partialString + element);
assertThat(result).isEqualTo("abcde");
同样也可以用方法引用来简化代码
String result = letters.stream().reduce("", String::concat);
assertThat(result).isEqualTo("abcde");
我们再把上面的拼接字符串的例子改下需求,先把字符串转变成大写然后再拼接
String result = letters
.stream()
.reduce(
"", (partialString, element) -> partialString.toUpperCase() + element.toUpperCase());
assertThat(result).isEqualTo("ABCDE");
另外,我们可以并行地归并元素(并行归并,下面会详细讲解),如下并行归并一个数字数组来求和
List<Integer> ages = Arrays.asList(25, 30, 45, 28, 32);
int computedAges = ages.parallelStream().reduce(0, a, b -> a + b, Integer::sum);
当对一个流进行并行操作时,在运行时会把流分割多个子流来并行操作。在上面例子中,我们需要一个函数来组合各个子流返回的结果,这个函数就是前面提到的Combiner
(组合器)。
有一个注意点,下面的代码无法通过编译
List<User> users = Arrays.asList(new User("John", 30), new User("Julie", 35));
int computedAges =
users.stream().reduce(0, (partialAgeResult, user) -> partialAgeResult + user.getAge());
上代码无法编译的原因是,流中包含的是User 对象,但是累加函数的参数分别是数字和user 对象,而累加器的实现是求和,所以编译器无法推断参数 user 的类型。可以把代码改为如下可以通过编译
int result = users.stream()
.reduce(0, (partialAgeResult, user) -> partialAgeResult + user.getAge(), Integer::sum);
assertThat(result).isEqualTo(65);
当顺序读流或者累加器的参数和它的实现的类型匹配时,我们不需要使用组合器。
如上文提到的,我们可以并行的使用 reduce() 方法。并行使用时,要注意一下几点:
在以上的例子中,reduce 方法都没抛出异常,如果出现异常我们该如何优雅的处理异常呢?看下面例子:
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);
int divider = 2;
int result = numbers.stream().reduce(0, a / divider + b / divider);
如果 divider =0 , 会抛出 ArithmeticException
,遇到这种情况,一般的处理方法使用 try/catch 捕获异常
public static int divideListElements(List<Integer> values, int divider) {
return values.stream()
.reduce(0, (a, b) -> {
try {
return a / divider + b / divider;
} catch (ArithmeticException e) {
LOGGER.log(Level.INFO, "Arithmetic Exception: Division by Zero");
}
return 0;
});
}
如果直接使用 try/catch 会影响代码的可读性,我们可以把 divide 的操作封装一个单独的方法,并在里面捕获异常,如下:
rivate static int divide(int value, int factor) {
int result = 0;
try {
result = value / factor;
} catch (ArithmeticException e) {
LOGGER.log(Level.INFO, "Arithmetic Exception: Division by Zero");
}
return result
}
divideListElements 调用 divide 方法
public static int divideListElements(List<Integer> values, int divider) {
return values.stream().reduce(0, (a, b) -> divide(a, divider) + divide(b, divider));
}
我们可以使用 reduce 方法处理复杂的对象,reduce 需要接受和复杂对象相对应的 identity、accumulator、combiner。
假设一个场景:计算一个网站用户的评分,该评分是所有用户所有评论的平均值。
有个类 Review 定义如下:
public class Review {
private int points;
private String review;
// constructor, getters and setters
}
类 Rating 引用 Review 计算用户的评分
public class Rating {
double points;
List<Review> reviews = new ArrayList<>();
public void add(Review review) {
reviews.add(review);
computeRating();
}
private double computeRating() {
double totalPoints =
reviews.stream().map(Review::getPoints).reduce(0, Integer::sum);
this.points = totalPoints / reviews.size();
return this.points;
}
public static Rating average(Rating r1, Rating r2) {
Rating combined = new Rating();
combined.reviews = new ArrayList<>(r1.reviews);
combined.reviews.addAll(r2.reviews);
combined.computeRating();
return combined;
}
}
先组装一些用户和用户的评论
User john = new User("John", 30);
john.getRating().add(new Review(5, ""));
john.getRating().add(new Review(3, "not bad"));
User julie = new User("Julie", 35);
john.getRating().add(new Review(4, "great!"));
john.getRating().add(new Review(2, "terrible experience"));
john.getRating().add(new Review(4, ""));
List<User> users = Arrays.asList(john, julie);
调用 reduce 方法处理评分
Rating averageRating = users.stream()
.reduce(new Rating(),
(rating, user) -> Rating.average(rating, user.getRating()),
Rating::average);
不知道大家学会了吗?
别忘了给个赞b( ̄▽ ̄)d