一、Lambda表达式
lambda可以让代码更加简洁清晰
(参数1,参数2) -> 方法体
匿名—不像普通方法那样有一个明确的名称
函数—因为lambda函数不像方法那样属于某个特定的类,但和方法一样,lambda有参数列表、函数主体、返回类型,还可能有可以抛出的异常列表
传递—lambda表达式可以作为参数传递给方法或存储在变量中
简洁—无需像匿名类那样写很多模板代码
lambda表达式
(List<String> list) -> list.isEmpty()
创建对象
() -> new Apple(10)
消费一个对象
(Apple a) -> {
System.out.println(a.getWeight());
}
从一个对象中选择/抽取
(String s) -> s.length()
组合两个值
(int a, int b) -> a * b
比较两个对象
(Apple a1, Apple a2) ->a1.getWeight().compareTo(a2.getWeight())
lambda表达式可以用在函数式接口上,而什么是函数式接口呢,只定义一个抽象方法的接口称为函数式接口
Reduce接口 定义了一个接受泛型T,返回boolean类型的方法
Consumer接口 定义了一个接受泛型T,没有返回值的方法
Function接口 定义了一个接受泛型T,并返回泛型R的对象
任何函数式接口都不允许抛出受检异常,可以自己定义函数式接口,并声明受检异常,或者把lambda包在try/catch块中
二、方法引用
方法引用可以重复使用现有的方法定义,并像lambda一样传递它们。
语法:类/对象::方法名
方法引用主要有三类:
1)指向静态方法的方法引用
2)指向任意类型实例方法的方法引用
3)指向现有对象的实例方法的方法引用
构造函数引用: ClassName::new
三、函数式数据处理
流是javaAPI的新成员,它允许你以声明性方式处理数据集合。从支持数据处理操作的源生成的元素序列。使用流式进行编程有如下好处:
声明性—更简洁、更易读
可复合—更灵活
可并行—性能更好
流操作的两个重要特点:
1)流水线:很多流操作本身会返回一个流,这样多个操作就可以链接起来,形成一个大的流水线
2)内部迭代:与使用迭代器显式迭代的集合不同,流的迭代操作是在背后进行的
流只能遍历一次,遍历结束就会被关掉,如果需要使用需要重新获取
3.1、筛选和切片
用谓词筛选,filter(),distinct()
截短流 limit(num),返回前num个元素,list返回的是有序的,如果是set结果就无序
跳过元素 skip(n),跳过前n个元素
3.2、映射
对流中的每一个元素应用函数:使用map方法,可以映射出一个新的流
流的扁平化:flatMap(),各个数组并不是分别映射成一个流,而是映射成流的内容。
3.3、查找和匹配
数据集中的某个元素是否匹配一个给定的属性。StreamAPI通过allMatch、anyMatch、noneMatch、findMatch、findAny方法提供了这样的工具
3.3.1 检查谓词是否至少匹配一个元素
anyMatch可以回答“流中是否有一个元素能匹配给定的谓词”,返回一个boolean,因此是一个终端操作
3.3.2 检查谓词是否匹配所有元素
allMatch方法的工作原理和anyMatch类似,但它会看看流中的元素是否都能匹配给定的谓词
noneMatch和allMatch相对,它可以确保流中没有任何元素与给定的谓词匹配
anyMatch、allMatch、noneMatch三个操作都用到了我们所谓的短路,就是java中的&&和||
短路求值
有些操作不需要处理真个流就能得到结果。例如由“&&”连接的长布尔表达式,当找到第一个表达式为false的时候,就可以推断整个表达式为false,不需要计算整个表达式
3.3.3 查找元素
findAny方法将返回当前流中的任意元素,返回的是Optional对象
Optional类是一个容器类,代表一个值存在或不存在
isPresent()将在Optional包含值的时候返回true,否则返回false
ifPresent(Consumer
block) 会在值存在的时候执行给定的代码块 T get() 会在值存在的时候返回值,否则抛出NoSuchElement异常
T orElse(T other) 会在值存在的时候返回值,否则返回一个默认值
3.3.4 查找第一个元素
有些流有一个出现顺序来指定流中的项目出现的逻辑顺序,有一个findFirst方法,它的工作方式类似于findAny
findFirst在并行上限制更多,如果不关心返回的是哪个元素,还是使用findAny()
3.4 归约
3.4.1 元素求和
reduce方法可以对流中的元素按照指定的方式进行归约操作,函数式变成语言的术语叫做折叠(fold)
reduce接受两个参数:
1) 一个初始值
2) 一个BinaryOperator
3.4.2 最大值和最小值
Optional
归约方法的优势与并行化
相比逐步迭代求和,使用reduce的好处在于,迭代被内部迭代抽象掉了,这让内部实现得以选择并行执行reduce操作。
3.5 数值流
3.5.1 原始类型流特化
java8引入了三个原始类型特化流接口:IntStream、DoubleStream、LongStream来避免暗含的装箱成本
1、映射到数值流
可以使用mapToInt、mapToDouble、mapToLong转化为特化版本
2、转换回对象流
使用boxed()方法将特化流转为一般流
3、默认值OptionalInt
对于三种原始流特化,也分别有一个Optional原始类特化版本:OptionalInt、OptionalDouble和OptionalLong
3.5.2 数值范围
java8引入了两个可以用于IntStream和LongStream的静态方法,帮助生成这种范围:range和rangeClosed
3.5.3 数值流应用:勾股数
Stream<double[]> pythagoreanTriples2 =
IntStream.rangeClosed(1, 100).boxed()
.flatMap(a ->
IntStream.rangeClosed(a, 100)
.mapToObj(b -> new double[]{a, b, Math.sqrt(a*a + b*b)})
.filter(t -> t[2] % 1 == 0));
3.6 构建流
3.6.1 由值创建流
可以使用静态方法Stream.of,通过显式值创建一个流。可以接受任意数量的参数
Stream<String> stream = Stream.of("Java 8 ", "Lambdas ", "In ", "Action");
stream.map(String::toUpperCase).forEach(System.out::println);
3.6.2 由数组创建流
可以使用静态方法Arrays.stream从数组创建一个流。它接受一个数组作为参数
int[] numbers = {2, 3, 5, 7, 11, 13};
int sum = Arrays.stream(numbers).sum();
3.6.3 由文件生成流
java中用于处理文件等I/O操作的NIO API已经更新,以便利用StreamAPI。Files中的多个静态方法会返回一个流
3.6.4 由含税生成流: 创建无限流
Stream API提供了两个静态方法来从含税生成流:Steam.iterate和Stream.generate。这两个操作可以创建所谓的无限流:不像从固定集合创建的流那样有固定大小的流。由iterate和generate产生的流会用给定的含税按需创建值,因此可以无穷无尽地计算下去。一般来说应该使用limit(n)来对这种流加以限制,以避免打印无穷多个值。
- 迭代
Stream.iterate(0, n -> n + 2)
.limit(10)
.forEach(System.out::println);
iterate方法接受一个初始值,还有一个依次应用在每个产生的新值上的lambda。这里,我们使用lambda返回的是前一个元素加上2,这样以此类推,因为是无界的,所以应该使用limit进行限制。
- 生成
Stream.generate(Math::random)
.limit(5)
.forEach(System.out::println);
与iterate方法类似,generate方法也可以让你按需生成一个无限流。generate不是依次对每个新生成的值引用函数的。使用generate生成的流,没有初始值,只根据给的条件去生成值。
四、用流收集数据
4.1 收集器
函数式编程相对于指令试编程的一个主题优势:你只需要指出希望的结果-“做什么”,而不用操心执行的步骤—“如何做”
4.1.1 收集器用作高级归约
对流调用collect方法将对流中的元素触发一个归约操作,一般来说,Collector会对元素应用一个转换函数,并将结果累积在一个数据结构中,从而产生这一过程的最终输出。
4.1.2 预定义收集器
Collectors类中提供的工厂方法,主要提供了三大功能:
将流元素归约和汇总为一个值
元素分组
元素分区
4.2 归约和汇总
4.2.1 汇总
Collectors类专门为汇总提供了一个工厂方法:Collectors.summingInt。它可以接受一个把对象映射为求和所需int的函数,并返回一个收集器,还有个Collectors.averagingInt,连同对应的averagingLong和averagingDouble可以计算数值的平均数
4.2.2 连接字符串
joining工厂方法返回的收集器会把对流的每一个对象应用toString()方法得当的所有字符串连接成一个字符串,默认是空格分割,可以指定分割符
String shortMenu = menu.stream().map(Dish::getName).collect(joining(", "));
4.2.3 广义的归约汇总
事实上,我们已经讨论的所有收集器,都是一个可以用reducing公共方法定义的归约过程的特情况而已。
int totalCalories = menu.stream().collect(reducing(
0, Dish::getCalories, (i, j) -> i + j));
它需要三个参数:
第一个参数是归约操作的起始值
第二个参数就是一个函数,将菜肴转换成一个表示其所含热量的int
第三个参数是一个BinaryOperator,将两个项目累积成一个同类型的值
收集和归约
collect和reduce的区别在于使用场景。collect方法特别适合表达可变容器上的归约的原因,更关键的是它适合并行操作
1、 收集框架的灵活性:以不同的方法执行同样的操作
int totalCalories = menu.stream()
.collect(reducing(0,
Dish::getCalories,
Integer::sum));
int totalCalories =
menu.stream().map(Dish::getCalories).reduce(Integer::sum).get();
2、根据情况选择最佳解决方案
4.3 分组
4.3.1 多级分组
要实现多级分组,我们可以使用一个由双参数版本的Collectors.groupingBy工厂方法创建的收集器,
Map<Dish.Type, Map<CaloricLevel, List<Dish>>> dishesByTypeCaloricLevel =
menu.stream()
.collect(groupingBy(Dish::getType,
groupingBy(dish -> {
if (dish.getCalories() <= 400)
return CaloricLevel.DIET;
else if (dish.getCalories() <= 700)
return CaloricLevel.NORMAL;
else
return CaloricLevel.FAT;
})
)
);
4.3.2 按子组收集数据
Map<Dish.Type, Optional<Dish>> mostCaloricByType =
menu.stream()
.collect(groupingBy(Dish::getType,
maxBy(comparingInt(Dish::getCalories))));
Optional在这里没有作用,如何转换收集器的结果中的类型呢
1、把收集器的结果转换为另一种类型
Map<Dish.Type, Dish> mostCaloricByType =
menu.stream()
.collect(groupingBy(Dish::getType,
collectingAndThen(
maxBy(comparingInt(Dish::getCalories)),
Optional::get)));
2、与groupingBy联合使用的其他收集器的例子
Map<Dish.Type, Integer> totalCaloriesByType =
menu.stream()
.collect(groupingBy(Dish::getType,
summingInt(Dish::getCalories)));
Map<Dish.Type, Set<CaloricLevel>> caloricLevelsByType =
menu.stream()
.collect(
groupingBy(Dish::getType, mapping(
dish -> {
if (dish.getCalories() <= 400)
return CaloricLevel.DIET;
else if (dish.getCalories() <= 700)
return CaloricLevel.NORMAL;
else
return CaloricLevel.FAT; },
toSet() )));
4.4 分区
分区是分组的特殊情况:由一个谓词(返回一个布尔值的函数)作为分类函数,它称分区函数。分区函数返回一个布尔值,这意味着得到的分组Map的键类型是Boolean,于是它最多可以分为两组
Map<Boolean, List<Dish>> partitionedMenu =
menu.stream().collect(partitioningBy(Dish::isVegetarian));
4.4.1 分区的优势
分区的好处在于保留了分区函数返回treu或者false的两套流元素列表
4.4.2 将数组按质数和非质数分区
Map<Boolean, List<Integer>> result = IntStream.rangeClosed(2, 100).boxed()
.collect(
partitioningBy(candidate -> isPrime(candidate)));
4.5 收集器接口
public interface Collector<T, A, R> {
Supplier<A> supplier();
BiConsumer<A, T> accumulator();
Function<A, R> finisher();
BinaryOperator<A> combiner();
Set<Characteristics> characteristics();
}
T是流中要收集的项目的泛型
A是累加器的类型,累加器是在收集过程中用于累计部分结果的对象
R是收集操作得到的对象(通常但并不一定是集合)的类型
4.5.1 理解Collector接口声明的方法
1、建立新的结果容器:supplier方法
supplier方法必须返回一个结果为空的Supplier,也就是一个无参函数,在调用时它会创建一个空的累加器实例,供数据收集过程使用。
2、将元素添加到结果容器:accumulator方法
accumulate方法会返回执行归约操作的函数。当遍历到流中第n个元素时,这个函数执行时会有两个参数:保存归约结果的累加器(已收集了流中的前n-1个项目),还有第n个元素本身。该函数将返回void
3、对结果让其应用最终转换:finisher方法
在遍历完流后,finisher方法必须返回在累积过程的最后要调用的一个函数,以便将累加器对象转换为整个集合操作的最终结果。
4、合并两个结果容器:combiner方法
四个方法中的最后一个-combiner方法会返回一个供归约操作使用的函数,它定义了对流的各个子部分进行并行处理时,各个字部分归约所得的累加器要如何合并
原始流会以递归方式拆分为子流,直到定义流是否需要进一步拆分的一个条件为非
现在,所有的子流都可以并行处理。
最后,使用收集器combiner方法返回的函数,将所有的部分结果两两合并。
5、characteristics方法
最后一个方法—characteristics会返回一个不可变的Characteristics集合,它定义了收集器的行为—尤其是关于流是否可以并行归约,以及可以使用哪些优化的提示。
4.5.2 全部融合到一起
import java.util.*;
import java.util.function.*;
import java.util.stream.Collector;
import static java.util.stream.Collector.Characteristics.*;
public class ToListCollector<T> implements Collector<T, List<T>, List<T>> {
@Override
public Supplier<List<T>> supplier() {
return ArrayList::new;
}
@Override
public BiConsumer<List<T>, T> accumulator() {
return List::add;
}
@Override
public Function<List<T>, List<T>> finisher() {
return Function.indentity();
}
@Override
public BinaryOperator<List<T>> combiner() {
return (list1, list2) -> {
list1.addAll(list2);
return list1;
};
}
@Override
public Set<Characteristics> characteristics() {
return Collections.unmodifiableSet(EnumSet.of(
IDENTITY_FINISH, CONCURRENT));
}
}
4.6 开发你自己的收集器以获得更好的性能
4.6.1 仅用质数做除数
public static <A> List<A> takeWhile(List<A> list, Predicate<A> p) {
int i = 0;
for (A item : list) {
if (!p.test(item)) {
return list.subList(0, i);
}
i++;
}
return list;
}
public static boolean isPrime(List<Integer> primes, int candidate){
int candidateRoot = (int) Math.sqrt((double) candidate);
return takeWhile(primes, i -> i <= candidateRoot)
.stream()
.noneMatch(p -> candidate % p == 0);
}
这里isPrime实现是即时的。理想情况下,我们会想要一个延迟求值的takeWhile,这样就可以和noneMatch操作合并,具体实现,
public class PrimeNumbersCollector implements Collector<Integer,
Map<Boolean, List<Integer>>,
Map<Boolean, List<Integer>>> {
//实现归约过程
public Supplier<Map<Boolean, List<Integer>>> supplier() {
return () -> new HashMap<Boolean, List<Integer>>() {{
put(true, new ArrayList<Integer>());
put(false, new ArrayList<Integer>());
}};
}
//实现累加器
public BiConsumer<Map<Boolean, List<Integer>>, Integer> accumulator() {
return (Map<Boolean, List<Integer>> acc, Integer candidate) -> {
//根据是否质数结果,获取质数或非质数列表
acc.get( isPrime(acc.get(true), candidate) )
//加入列表
.add(candidate);
};
}
//组合器,将第二个Map合并到第一个,实际这个收集器不能并行使用,因为该算法本身是顺序的
public BinaryOperator<Map<Boolean, List<Integer>>> combiner() {
return (Map<Boolean, List<Integer>> map1,
Map<Boolean, List<Integer>> map2) -> {
map1.get(true).addAll(map2.get(true));
map1.get(false).addAll(map2.get(false));
return map1;
};
}
//完成器,最后无需转换
public Function<Map<Boolean, List<Integer>>,
Map<Boolean, List<Integer>>> finisher() {
return Function.identity();
}
//这个收集器是 IDENTITY_FINISH ,但既不是 UNORDERED
//也不是 CONCURRENT ,因为质数是按顺序发现的
public Set<Characteristics> characteristics() {
return Collections.unmodifiableSet(EnumSet.of(IDENTITY_FINISH));
}
}
五、 并行数据处理与性能
5.1 并行流
可以通过对收集源调用parallelStream方法来把集合转换为并行流。并行流就是把内容分成多个数据块,并用不同的线程分别处理每个数据库的流。
5.1.1 将顺序流转换为并行流
可以把流转换成并行流,从而让前面的函数归约过程并行运行—对顺序流调用parallel方法,调用parallel方法并不意味着流本身有任何实际的变化。内部是设置了一个boolean标志,表示你想以书面方式运行,调用sequential方法可以变成顺序流,当对流多次调用parallel、sequential时,最后一次的调用决定了流以什么方式执行
并行流内部使用了默认的ForkJoinPool,默认线程数量是处理器数量,由Runtime.getRuntime().availableProcessors()得到,可以通过System.setProperty(“java.util.concurrent.ForkJoinPool.common.parallelism”, “8”);设置,这是一个全局变量,无法对某个并行流设置指定值
5.1.2 正确使用并行流
错误使用并行流而产生错误的首要原因,是因为使用的算法改变了某些共享状态。
5.1.3 高效使用并行流
一般而言,想给出任何关于什么时候该用并行流的定量建议都是不可能也毫无意义的,因为情况不同
如果有疑问,测量。把顺序流转换成并行流很简单,但不一定是好事,需要找到一个测量的方法,来验证是否有必要以及是否正确
留意装箱。自动装箱和拆箱会大大降低性能,如果可以尽量使用元素类型流
在有些操作本身在并行流上的性能就比顺序流差。特别是limit和findFirst等依赖元素顺序的操作
还要考虑流的操作流水线的总计算成本。
对于较小的数据量,选择并行流,基本上不是一个好的决定
要考虑流背后的数据结构是否已于分解
流自身的特点,以及流水线中的中间操作修改流的方式,都可能会改变分解过程的性能。
还要考虑终端操作中合并步骤的代价是大是小。
5.2 Spliterator
spliterator是java8中加入的另一个新接口,叫做“可分迭代器”,和Iterator一样,Spliterator也用于遍历数据源中的元素,但它是为了并行设计的
5.2.1 拆分过程
讲Stream拆分成多个部分的算法是一个递归过程,调用trySplit尝试拆分,直到返回null
6.1 重构
6.1.1 改善代码的可读性
利用java8的新特性,可以减少代码的冗余,提高可读性
用lambda表达式取代匿名类
用方法引用重构lambda表达式
用StreamAPI重构命令式的数据处理
6.1.2 增加代码的灵活性
7 默认方法
接口可以提供方法的具体实现,使用“default”修饰,这样实现类就不需要强制实现接口的所有方法,
当实现多个接口出现方法签名冲突时的规则:
类中的方法优先级最高
子接口优先级更高
显式覆盖和调用期望的方法
8 Optional
为了规避null的检查,Optional类在java8中引入