JDK1.8中的新特性——Lambda表达式和Stream
文章目录
一、函数接口(functional interface)
带有单个抽象方法的接口是特殊的,值得特殊对待。这些接口现在被称作函数接口。
二、Lambda表达式(Lambda表达式优于匿名类)
Java允许利用Lambda表达式创建这些函数接口的实例。
Lambda类似于匿名类的函数,但是比它简洁得多。
举例:
List<String> list = new ArrayList<>();
list.add("a");
list.add("ddd");
list.add("cc");
list.add("eeeeee");
list.add("bbbbb");
使用匿名内部类实现排序功能:
Collections.sort(list, new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
return Integer.compare(o1.length(), o2.length());
}
});
使用lambda表达式实现排序功能:
// 使用lambda表示式实现与匿名内部类相同的功能
Collections.sort(list, (s1,s2)->Integer.compare(s1.length(), s2.length()));
使用方法引用:
// 方案一:使用方法的引用
Collections.sort(list, Comparator.comparingInt(String::length));
// 方案二:使用方法的引用
list.sort(Comparator.comparingInt(String::length));
2.1 关于lambda表达式
-
编译器利用一个称作类型推导的过程,根据上下文推断出这些类型;
编译器式从泛型获取到得以执行类型推导的大部分类型信息的。
-
Lambda没有名称和文档;如果一个计算本身不是自描述的,或者超出了几行,那就不要把它放在一个Lambda中;
对于Lambda而言,一行是最理想的,三行诗合理的最大极限。
-
Lambda表达式咸鱼接口函数
-
Lambda无法获得对自身的引用。在Lambda中,关键字this是指外围实例。在匿名类中,关键字this是指匿名类实例。
-
无法通过实现来序列化和反序列化Lambda与匿名类共享的属性。
因此,尽可能不要序列化一个Lambda(或者匿名类实例)。如果想要可序列化的函数对象,如Comparator,就使用私有静态嵌套类的实例。
三、方法引用
只要方法引用能做的事,就没有Lambda不能完成的。
3.1 五种方法引用概述:
方法引用类型 | 范例 | Lambda等式 |
---|---|---|
静态 | Integer::parseInt | str->Integer.parseInt(str) |
有限制 | Instant.now()::isAfter | Instant then = Instant.now(); t->then.isAfter(t) |
无限制 | String::toLowerCase | str->str.toLowerCase() |
类构造器 | TreeMap<K,V>::new | ()->new TreeMap<K,V> |
数组构造器 | int[]::new | len->new int[len] |
只要方法引用更加简洁、清晰,就用方法引用;如果方法引用并不简洁,就坚持使用Lambda。
四、使用Lambda时,坚持使用标准的函数接口
只要标准的函数接口能够满足需求,通常应该优先考虑,而不是专门再构建一个新的函数接口
好处:
- 这样会使API更加容易学习;
- 通过减少它的概念内容,显著提升操作性优势;
标准的函数接口
java.util.function
包中有43个接口。如果能记住其中6个基础接口,必要时就可以推断出其余接口了。
(1)基础接口
基础接口作用于对象引用类型
-
Operator 接口代表其结构与参数类型一致的函数;
-
Predicate 接口代表一个参数并返回一个boolean的函数;
-
Function 接口代表其参数与返回的类型不一致的函数;
-
Supplier 接口代表没有参数并且返回一个值的函数;
-
Consumer 代表的是带有一个参数但不返回任何值的函数,相当于消费掉了其参数;
接口 | 函数签名 | 范例 |
---|---|---|
UnaryOperator | T apply(T t) | String::toLowerCase |
BinaryOperator | T apply(T t1, T t2) | Integer::sum |
Predicate | Boolean test(T t) | Collection::isEmpty |
Function | R apply(T t) | Integer::parseInt |
Supplier | T get() | Instant::now |
Consumer | void accept(T t) | System.out::println |
(2)基础接口的三种变体
这六个基础接口各自还有三种变体,分别可以作用于基本类型:int、long和double。它们的命名方式是在其基础接口名称前面加上基本类型而得。例如:
// IntUnaryOperator
// LongUnaryOperator
// DoubleUnaryOperator
这些变体除了Function
变体外,都不是参数化的:
@FunctionalInterface
public interface DoublePredicate {
boolean test(double value);
...
}
@FunctionalInterface
public interface IntFunction<R> {
R apply(int value);
...
}
LongFunction<int[]>
表示带有一个long类型的参数,并返回一个int[]数组
(3)Function
接口还有9种变体,用于结果类型为基本类型的情况
源类型和结果类型始终不一样。
因为从类型到自身的函数就是Operator
如果源类型和结果类型均为基本类型,就在Function
前面添加格式如SrcToResult
这样有6种变体:
// IntToLongFunction
// IntToDoubleFunction
// LongToIntFunction
// LongToDoubleFunction
// DoubleToIntFunction
// DoubleToLongFunction
如果源类型为对象类型,结果类型是一个基本类型,就在Function
前面添加To<Src>
这样有三种变体:
// ToIntFunction
// ToLongFunction
// ToDoubleFunction
(4)带两个不同参数的版本(都为对象引用,三种)
// BiPredicate<T, U>
// BiFunction<T, U, R>
// BiConsumer<T, U>
(5)Function
带有两个不同参数,并返回基本类型(三种)
// ToIntBiFunction<T, U>
// ToDoubleBiFunction<T, U>
// ToLongBiFunction<T, U>
(6)Consumer
带有一个对象引用和一个基本类型(三种)
// ObjIntConsumer<T>
// ObjLongConsumer<T>
// ObjDoubleConsumer<T>
(7)BooleanSupplier
接口,Supplier
接口的一种变体
// BooleanSupplier
注意事项:
千万不要用带包装类型的基础函数接口来代替基本函数接口
现有的大多数标准接口函数都支持基本类型。千万不要用带包装类型的基础函数接口来代替基本函数接口,使用装箱基本类型进行批量操作处理,最终会导致致命的性能问题。
必须始终用@FunctionalInterface
注解对自己编写的函数接口进行标注。
不要在相同的参数位置,提供不同的函数接口来进行多次重载的方法
五、Stream
JKD1.8
中增加了Stream API,简化了串行或并行的大批量操作。这个API提供了两个关键的抽象:Stream(流)代表数据元素有限或无限的顺序,Stream pipeline(流管道)则代表这些元素的一个多级计算。
5.1 Stream概述
5.1.1 Stream流
Stream中的数据元素可以是对象引用,或者基本类型值。它支持三种基本类型:int、long和double。
5.1.2 Stream pipeline 流管道
一个Stream pipeline中包含一个源Stream,接着是0个或者多个中间操作和一个终止操作。
-
Stream pipeline通常是lazy的
直到调用终止操作时才会开始计算,对于完成终止操作不需要的数据元素,将永远不会被计算。
-
默认情况下,Stream pipeline是按顺序运行的
要使pipeline并发执行,只需在该pipeline的任何Stream上调用parallel方法即可,但通常不建议这么做
(1)中间操作
所有的中间操作都是将一个Stream转换成另一个Stream。
(2)终止操作
终止操作会在最后一个中间操作产生的Stream上执行一个最终的计算。
5.1.3 Stream的使用
(1)最好避免利用Stream来处理char值;
(2)重构现有代码来使用Stream,并且只在必要的时候才在新代码中使用;
5.2 Stream并不只是一个API,它是一种基于函数编程的模型
纯函数是指其结果只取决于输入的函数:它不依赖任何可变的状态,也不更新任何状态。为了做到这一点,传入Stream操作的任何函数对象,无论是中间操作还是终止操作,都应该是无副作用的。
一个错误的实例(下面的代码根本不是Stream代码;只不过是伪装成Stream代码的迭代式代码):
因为这段代码利用一个改变外部状态(频率表)的Lamdba,完成了终止操作的forEach中的所有工作
/**
* 一段伪装成Stream代码的迭代式代码
* 因为这段代码利用一个改变外部状态(频率表)的Lamdba,完成了在终止操作的forEach中的所有工作
* @param file
*/
private static void readFileCountWorld(File file){
Map<String, Long> freq = new HashMap<>();
try(Stream<String> words = new Scanner(file).tokens()){
words.forEach(world->{freq.merge(world.toLowerCase(), 1L, Long::sum);});
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
正确的Stream代码如下:
private static void readFileCountWorldWithStream(File file){
Map<String, Long> freq;
try(Stream<String> words = new Scanner(file).tokens()){
freq = words.collect(groupingBy(String::toLowerCase, counting()));
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
为了获得Stream带来的描述性和速度,有时还有并行性,必须采用范型以及API。
Stream泛型最重要的部分是把计算构造成一系列变型,每一级结果都尽可能靠近上一级结果的纯函数。
5.2.1 forEach操作应该只用于报告Stream计算的结果,而不是执行计算
forEach操作是终止操作中最没有威力的,也是对Stream最不友好的。它是显式迭代,因而不适合并行。
有时候,也可以将forEach用于其他目的,比如将Stream计算的结果添加到之前已经存在的集合中去。
5.2.2 收集器(collector)
Collectors API 有39种方法。
静态导入Collectors的所有成员是惯例也是明智的,因为这样可以提升Stream pipeline的可读性
(1)将Stream的元素集中到一个真正的Collection里去:
toList()
、toSet()
、toCollection(collectionFactory)
-
toList()
返回一个列表 -
toSet()
返回一个集合 -
toCollection(collectionFactory)
返回程序员指定的集合类型
Map<String, Long> freq = new HashMap<>();
...
List<String> topThree = freq.keySet().stream()
.sorted(Comparator.comparing(freq::get).reversed())
.limit(3)
.collect(toList());
(2)Collectors 中的另外36种方法大多数是为了便于将Stream集合到映射中
toMap
- 带两个参数:
toMap(keyMapper,valueMapper)
最简单的映射收集器
这种简单的形式如果出现多个Stream元素映射到同一个键,pipeline就会抛出一个
IllegalStateException异常
将它终止。
public enum Operation {
....
private static final Map<String, Operation> stringToEnum =
Stream.of(values()).collect(Collectors.toMap(Object::toString, e-> e));
...
}
- 带三个参数:
toMap(keyMapper,valueMapper,mergeFunction)
除了提供键和值以外,还提供一个合并函数
合并函数是一个BinaryOperator。合并函数将与键关联的任何其他值与现有值合并起来,因此,假如合并函数是乘法,得到的值就是与该值映射的键关联的所有值的积。
List<WorldObj> worldObjs = new ArrayList<>();
worldObjs.add(new WorldObj("aa", 2));
worldObjs.add(new WorldObj("aa", 3));
worldObjs.add(new WorldObj("bb", 1));
Map<String, WorldObj> topWorldObj = worldObjs.stream()
.collect(toMap(WorldObj::getWorld, worldObj -> worldObj,
BinaryOperator.maxBy(Comparator.comparingInt(WorldObj::getLen))));
- 带有三个参数的toMap形式还有另一种用途,即生成一个收集器,当有冲突时强调“保留最后更新”(last-write-wins)
格式:
toMap(keyMapper, valueMapper, (oldVal, newVal)->newVal)
案例:
List<WorldObj> worldObjs = new ArrayList<>();
worldObjs.add(new WorldObj("aa", 2));
worldObjs.add(new WorldObj("aa", 3));
worldObjs.add(new WorldObj("bb", 1));
worldObjs.add(new WorldObj("aa", 23));
Map<String, WorldObj> map =
worldObjs.stream().collect(toMap(WorldObj::getWorld, worldObj -> worldObj, (oldVal, newVal) -> newVal));
前三种版本还有另外的变换形式,命名为toConcurrentMap
,能有效地并行运行,并生成ConcurrentMap
实例
- 带有四个参数的toMap形式:
toMap(keyMapper, valueMapper, mergeFunction, mapFactory)
带有第四个参数,这是一个映射工厂,在使用时要指定映射实现,如EnumMap
或者TreeMap
groupingBy
-
groupingBy(classifier)
最简单的一个版本,只带一个分类器,并返回一个映射Map<String, List<WorldObj>> map = worldObjs.stream().collect(groupingBy(WorldObj::getWorld));
-
groupingBy(classifier, downstream)
指定一个分类器和一个下游收集器(downstream)Map<String, Integer> map02 = worldObjs.stream().collect(groupingBy(WorldObj::getWorld, summingInt(WorldObj::getLen)));
下游收集器最简单的用法时传入
toSet()
,结果生成一个映射,这个映射值为元素集合而非列表;另一种方法是在下游收集器的位置上传toCollection(collectionFactory)
,允许创建存放各元素类别的集合。 -
groupingBy
使用counting()
作为下游收集器这样会生成一个映射,它将每个类别与该类别中的元素数量关联起来
Map<String, Long> map03 = worldObjs.stream().collect(groupingBy(WorldObj::getWorld, counting()));
-
groupingBy(classifier, mapFactory, downstream)
的第三个版本,除了下游收集器,还可以指定一个映射工厂注意参数mapFactory要在downStream参数之前,而不是在它之后。(这个方法违背了标准的可伸缩参数列表模式)
这个版本可以控制所包围的映射,以及所包含的集合,以及所包围的集合
TreeMap<String, Long> treeMap = worldObjs.stream().collect(groupingBy(WorldObj::getWorld, TreeMap::new, counting()));
-
groupingByConcurrent
方法提供groupingBy
所有三种重载的变体这些变体可以有效地并发运行,生成
ConcurrentHashMap
实例 -
partitioningBy
方法提供groupingBy
两种变体带有一个断言参数:
partitioningBy(predicate)
,返回一个键为Boolean的映射Map<Boolean, List<WorldObj>> map04 = worldObjs.stream().collect(partitioningBy(worldObj -> worldObj.getLen() > 10));
带有一个断言参数和一个下游收集器:
partitioningBy(predicate, downstream)
Map<Boolean, Long> map05 = worldObjs.stream().collect(partitioningBy(worldObj -> worldObj.getLen() > 10, counting()));
5.2.3 类似count
的方法
通过Stream上的count方法,直接就有相同的功能,因此压根没有理由使用collect(counting())
count
summing
、averaging
、summarizing
reducing
、filtering
、mapping
、flatMapping
、collectingAndThen
5.2.4 joining
它只在CharSequence
实例的Stream中操作
joining
只在CharSequence
实例的Stream中操作,例如字符串。
String[] strArr = {"world", "hello"};
String str01 = Stream.of(strArr).collect(joining());
String str02 = Stream.of(strArr).collect(joining("$"));
String str03 = Stream.of(strArr).collect(joining("#", "{", "}"));
5.3 Stream和Iterable,以及flatMap
方法的使用
-
将Stream<E>
变成Iterable<E>
, 用于提供给forEach
遍历/** * 适配器, 将Stream<E> 编程 Iterable<E> * @param stream * @param <E> * @return */ public static <E> Iterable<E> iterableOf(Stream<E> stream){ return stream::iterator; }
-
将
Iterable<E>
变成Stream<E>
, 便于用Stream pipeline
进行处理/** * 适配器,将 Iterable<E> 变成 Stream<E> * @param iterable * @param <E> * @return */ public static <E> Stream<E> streamOf(Iterable<E> iterable){ return StreamSupport.stream(iterable.spliterator(), false); }
-
Collection
接口是Iterable的一个子类型,它有一个stream方法,因此提供了迭代和stream访问对于公共的、返回序列的方法,Collection或者适当的子类型通常是最佳的返回类型
数组也通过
Arrays.asList
和Stream.of
方法提供了简单的迭代和stream访问。 -
千万别在内存中保存巨大的序列,将它作为集合返回即可
5.4 通过AbstractList
定制集合
假设要返回一个指定集合的幂集,例如:{a,b,c} 的幂集是{}、{a}、{b}、{c}、{a,b}、{a,c}、{b,c}、{a,b,c}
如果集合中有n个元素,它的幂集就有 2 n 2^n 2n个。
在二进制数0至 2 n − 1 2^n-1 2n−1和有n位元素的集合的幂集之间,有一个自然映射。
获取指定集合(集合元素个数不能大于 2 n 2^n 2n)的幂集:
public static final <E>Collection<Set<E>> of(Set<E> s){
List<E> src = new ArrayList<>(s);
if (src.size()>30){
throw new IllegalArgumentException("Set too big" +s );
}
return new AbstractList<Set<E>>() {
// 2 to the power srcSize
// 这个方法限制了 序列的长度位 Integer.MAX_VALUE 或者 2^31-1
@Override
public int size() {
return 1<<src.size();
}
@Override
public boolean contains(Object o) {
return o instanceof Set && src.containsAll((Set)o);
}
@Override
public Set<E> get(int index) {
Set<E> result = new HashSet<>();
System.out.println("index : " + index);
for (int i = 0; index != 0; i++, index >>= 1) {
if ((index & 1) == 1) {
System.out.println("i = " + i);
result.add(src.get(i));
}
}
return result;
}
};
}
使用Stream来获取幂集:
Stream.concat
方法将空列表添加到返回到Stream;flatMap
方法生成一个包含了所有前缀的所有后缀的Stream。
flatMap
这个操作将Stream中的每格元素映射到一个Stream中,然后将这些新的Stream全部合并到一个Stream(或将他们扁平化)
通过映射
IntStream.range
和IntStream.rangeClosed
返回的连续int值的Stream,生成了前缀和后缀。
/**
* @Date 2020/2/10
* @Author lifei
*/
public class SubLists {
/**
* 获取元素的幂集
* flatMap 生成一个包含了所有前缀的所有后缀的Stream
* @param list
* @param <E>
* @return
*/
public static <E> Stream<List<E>> of(List<E> list){
return Stream.concat(Stream.of(Collections.emptyList()), prefixes(list)
.flatMap(SubLists::suffixes));
}
/**
* 列表的前缀
* @param list
* @param <E>
* @return
*/
private static <E> Stream<List<E>> prefixes(List<E> list){
return IntStream.rangeClosed(1, list.size()).mapToObj(end->list.subList(0, end));
}
/**
* 列表的后缀
* @param list
* @param <E>
* @return
*/
private static <E> Stream<List<E>> suffixes(List<E> list){
return IntStream.range(0, list.size()).mapToObj(start -> list.subList(start, list.size()));
}
}
第二种Stream方案:
public static <E> Stream<List<E>> of02(List<E> list){
return IntStream.range(0, list.size())
.mapToObj(start -> IntStream.rangeClosed(start + (int)Math.signum(start), list.size()).mapToObj(end -> list.subList(start, end)))
.flatMap(x->x);
}
public static <E> Stream<List<E>> of03(List<E> list){
return Stream.concat(Stream.of(Collections.emptyList()), IntStream.range(0, list.size())
.mapToObj(start-> IntStream.rangeClosed(start + 1, list.size()).mapToObj(end -> list.subList(start, end)))
.flatMap(x->x));
}
5.5 谨慎使用Stream并行
在Java中编写并发程序变得越来越容易,但是要编写出正确又快速的并发程序,则一向没那么简单。
-
千万不要任意地并行
Stream pipeline
, 它造成的性能后果有可能是灾难性的。 -
在Stream上通过并行获得的性能,最好是通过ArrayList、HashMap、HashSet和ConcurrentHashMap实例,数组、int范围和long范围等。
(1)这些数组结构的共性是,都可以被精确、轻松地分成任意大小的子范围,使并行线程中的分工变得更加轻松。
Stream类库用来执行这个任务的抽象是分割迭代器(spliterator),它是由Stream和Iterable中的spliterator方法返回的;
(2)这些数据结构共有的另一项重要特征是,在进行顺序处理时,他们提供了优异的引用局部性。
引用局部性:序列化的元素引用一起保存在内存中。
引用局部性对于并行批处理来说至关重要:没有它,线程就会出现闲置,需要等待数据从内存转移到处理器的缓存中。
具有最佳引用局部性的数据结构是基本类型数组,因为数据本身是相邻地保存在内存中的。
-
Stream pipeline 的终止操作本质上也影响了并发执行的效率;
-
并行Stream不仅可能降低性能,包括活性失败,还可能导致结果出错,以及难以预计的行为;
-
并行Stream是一项严格的性能优化。对于任何优化都必须在改变前后对性能进行测试,以确保值得这么做;
一般来说,程序中所有的并行Stream pipeline都是在一个通用的fork-join池中运行的。只要有一个pipeline运行异常,都会损害到系统中其他不相关部分的性能。