Lamdba 和 Stream
1.Lamdba优于匿名内部类
lamdba 类似于匿名类的函数,但是要比匿名类更加简洁
// 匿名类
Collections.sort(words, new Comparator<String>() {
public int compare(String s1, String s2) {
return Integer.compare(s1.length(), s2.length());
}
});
//lamdba 更简洁
// 编译器利用一个类似于类型推倒的过程根据上下文推断出 s1,s2的数据类型。很少情况下需要指定类型。
Collections.sort(words,(s1, s2) -> Integer.compare(s1.length(), s2.length()));
lamdba
- 使之前不能使用函数对象的地方现在也能使用了。
- lambda中的不要超过三行,一行最理想。
- lambda中无法访问枚举的实例成员。
- lambda无法创建抽象类的实例,但匿名内部类可以。
- lambda无法获取到对自身的引用。
- 尽可能的不要序列化一个Lamdba
如果需要反序列化一个函数接口,如:Comparator,我们需要使用私有静态内部类。
2.方法引用优先于Lamdba
如果方法引用更加简洁和清晰,请使用方法引用,反之使用Lambda表达式。
map.merge(key, 1, (count, incr) -> count + incr); = map.merge(key, 1, Integer::sum);
//Static
Integer::parseInt == str -> Integer.parseInt(str)
//Bound
Instant.now()::isAfter == Instant then = Instant.now();then.isAfter(t)
//Unbound
String::toLowerCase = str.toLowerCase()
//Class Constructor
TreeMap::new = () -> new TreeMap
//Array Constructor
int[]::new = len -> new int[len]
3.坚持使用标准的函数接口
(1)模板方法模式
- 只要标准的函数接口能够满足需求,通常应该优先考虑,而不是专门在构造一个新的函数接口。
- 不要用带包装类型的基础函数接口来代替基本函数接口
- 用@FunctionalInterface注解
在模版方法模式中通过子类覆盖基本类型方法实现限定其超类的行为已经过时了,可以使用一个接受函数对象的静态工厂或者构造器来实现。
一般来说,我们将编写更多以函数对象作为参数的构造函数和方法。
abstract class TestA {
public void print() {
System.out.println("TestA");
doSubThing();
}
abstract void doSubThing();
}
class TestB extends TestA {
@Override
void doSubThing() {
System.out.println("TestB");
}
}
// lambda
class TestA {
private Supplier<String> supplier;
public testA(Supplier<String> supplier) {
this.supplier = supplier;
}
public void print() {
System.out.println("A");
System.out.println(supplier.get());
}
}
public static void main(String[] args) {
TestA a = new TestA(() -> "TestB");
a.print();
}
6个基础函数接口
接口 | 函数签名 | 例子 |
---|---|---|
UnaryOperator | T apply(T t) | String::toLowerCase |
BinaryOperator | T apply(T t1, T t2) | BigInteger::add |
Predicate | boolean test(T t) | Collection::isEmpty |
Function | R apply(T t) | Arrays::asList |
Supplier | T get() | Instant::now |
Consumer | void accept(T t) | System.out::println |
装箱基本类型进行批量处理,可能会导致致命的性能问题
@F
4.明智地使用Stream
Stream:数据元素无限或有限的序列。
Stream pipeline:对流中的元素进行多级计算。
流的来源:集合、数组、文件、正则表达式模式匹配器、伪随机数生成器或其他流。
管道操作:源Stream后跟着零个或多个中间操作和一个终止操作。
中间操作:转换流的方式,如:元素映射或元素过滤等。
终止操作:执行最终计算,如:流装入容器中或是消费掉。
流管道只包含中间操作时是惰性的:只有调用终止操作时才会开始计算,使无限Stream成为可能。
Stream API是流式的,所有包含Pipeline的调用可以链接成一个表达式。多个pipeline 也可以链接在一起,成为一个表达式。
流管道的API被设计成链式编码风格。
不要滥用流
// 普通方式
// 读取文件中的单词,检查单词的字母,相同字母的单词收集在一起
public class Anagrams {
public static void main(String[] args) throws IOException {
File dictionary = new File(args[0]);
int minGroupSize = Integer.parseInt(args[1]);
Map<String, Set<String>> groups = new HashMap<>();
try (Scanner s = new Scanner(dictionary)) {
while (s.hasNext()) {
String word = s.next();
groups.computeIfAbsent(alphabetize(word), (unused) -> new TreeSet<>()).add(word);
}
}
for (Set<String> group : groups.values())
if (group.size() >= minGroupSize)
System.out.println(group.size() + ": " + group);
}
private static String alphabetize(String s) {
char[] a = s.toCharArray();
Arrays.sort(a);
return new String(a);
}
}
// 过度使用流:虽然很简洁,但是对流不了解的开发人员可能无法理解。
// 打个比方,有些动漫是只有死宅才看的:永生之酒。
public static void main(String[] args) throws IOException {
Path dictionary = Paths.get(args[0]);
int minGroupSize = Integer.parseInt(args[1]);
try (Stream<String> words = Files.lines(dictionary)) {
words.collect(
groupingBy(word -> word.chars().sorted()
.collect(StringBuilder::new,
(sb, c) -> sb.append((char) c),
StringBuilder::append).toString())
)
.values().stream()
.filter(group -> group.size() >= minGroupSize)
.map(group -> group.size() + ": " + group)
.forEach(System.out::println);
}
}
// 合适使用流方式
// 有的动漫是大家都看的:龙珠。对动漫不需要太了解也能够接收。
public static void main(String[] args) throws IOException {
Path dictionary = Paths.get(args[0]);
int minGroupSize = Integer.parseInt(args[1]);
try (Stream<String> words = Files.lines(dictionary)) {
words.collect(groupingBy(word -> alphabetize(word)))
.values().stream()
.filter(group -> group.size() >= minGroupSize)
.forEach(group -> System.out.println(group.size() + ": " + group));
}
}
lambda中参数的命名尤为重要,好的命名能够提升可读性。
使用lambda来消灭循环,但实际是不可取的(元素少时lambda存在性能问题)。
Stream的缺点
- 代码块能够读取或修改范围内的局部变量,lambda只能操作final变量和当前范围的局部变量。
- 代码块中能够return、抛出异常、跳出循环或是跳过循环,lambda中都无法做到。
Stream的优势
- map:统一转换元素类型
- filter:过滤序列
- min、compute:计算最小值、合并序列等
- reduce:累计序列
- grouping:分组
流无法做到同时在多级阶段访问相应的元素
//通过操作反转来获取上一个流元素
public static void main(String[] args) {
primes().map(p -> TWO.pow(p.intValueExact()).subtract(ONE))
// (1-1/50)=98%代表isProbablePrime只有当98%几率为素数才返回true。
.filter(mersenne -> mersenne.isProbablePrime(50))
.limit(20)
// mp.bitLength等于p值,反向运算来获取上一个流的值。
.forEach(mp -> System.out.println(mp.bitLength() + ": " + mp));
}
static Stream<BigInteger> primes() {
return Stream.iterate(TWO, BigInteger::nextProbablePrime);
}
(4)笛卡尔积
private static List<Card> newDeck() {
List<Card> result = new ArrayList<>();
for (Suit suit : Suit.values())
for (Rank rank : Rank.values())
result.add(new Card(suit, rank));
return result;
}
// flatMap 用于展平一个序列,如:List<String> -> String.
private static List<Card> newDeck() {
return Stream.of(Suit.values())
.flatMap(suit ->
Stream.of(Rank.values())
.map(rank -> new Card(suit, rank)))
.collect(toList());
}
5.优先选择Stream中无副作用的功能
为了得到stream的表现力、速度和并行度,我们必须遵守范式和使用API。
stream范式最重要的部分:计算 -> 转换 ,每个转换(中间或终止操作)都是纯函数。
纯函数应该都是无副作用的(不依赖任何可变状态,不更新任何状态)。
// 不遵守范式,forEach应该只用于呈现流执行的计算结果
Map<String, Long> freq = new HashMap<>();
try (Stream<String> words = new Scanner(file).tokens()) {
words.forEach(word -> freq.merge(word.toLowerCase(), 1L, Long::sum));
}
// 使用流初始化频率表
Map<String, Long> freq;
try (Stream<String> words = new Scanner(file).tokens()) {
freq = words.collect(groupingBy(String::toLowerCase, counting()));
}
// 按照频次获取前十个元素
List<String> topTen = freq.keySet().stream()
.sorted(comparing(freq::get).reversed())
.limit(10)
.collect(toList());
// groupingByConcurrent返回并发Map
ConcurrentHashMap<String, Long> freq;
try (Stream<String> words = new Scanner(file).tokens()) {
freq = words.collect(groupingByConcurrent(String::toLowerCase, counting()));
}
Collector<T, ?, Map<Boolean, List>> partitioningBy(Predicate<? super T> predicate):true和false分成两组。
Collector<T, ?, Map<K, List>> groupingBy(Function<? super T, ? extends K> classifier):按照key值分组。
List<String> words = new ArrayList<>();
words.add("1");
words.add("1");
words.add("2");
words.add("3");
Map<Boolean, List<String>> map = words.stream().collect(
partitioningBy(s -> s.equals("1"))
);
// {false=[2, 3], true=[1, 1]}
System.out.println(map);
Map<String, List<String>> map = words.stream().collect(
groupingBy(String::toLowerCase)
);
// {1=[1, 1], 2=[2], 3=[3]}
System.out.println(map);
// List转Map的正确实现
Map<Integer, Data> collect = words.stream().collect(toMap(Data::getId, e -> e));
// key值重复时,获取销量最大的Album
Map<Artist, Album> topHits = albums.collect(
toMap(Album::artist, a->a,maxBy(comparing(Album::sales)))
);
// 后访问的覆盖先访问的
Map<Artist, Album> topHits = albums.collect(
toMap(Album::artist, a->a,(v1, v2) -> v2)
);
// 指定返回Map的类型
HashMap<Artist, Album> topHits = albums.collect(
toMap(Album::artist, a->a,(v1, v2) -> v2,HashMap::new)
);
// joining
List<String> words = new ArrayList<>();
words.add("2");
words.add("1");
words.add("1");
words.add("3");
String join1 = words.stream().collect(joining());
String join2 = words.stream().collect(joining(","));
String join3 = words.stream().collect(joining(",","[","]"));
// 2113
System.out.println(join1);
// 2,1,1,3
System.out.println(join2);
//[2,1,1,3]
System.out.println(join3);
// mapping和map类似
List<String> words = new ArrayList<>();
words.add("2");
words.add("1");
words.add("1");
words.add("3");
List<Integer> list1 = words.stream().collect(mapping(e -> Integer.valueOf(e), toList()));
List<Integer> list2 = words.stream().map(e -> Integer.valueOf(e)).collect(toList());
// [2, 1, 1, 3]
System.out.println(list1);
// [2, 1, 1, 3]
System.out.println(list2);
(7)计算
List<String> words = new ArrayList<>();
words.add("2");
words.add("1");
words.add("3");
// 求和
Integer sum1 = words.stream().collect(summingInt(value -> Integer.valueOf(value)));
Integer sum2 = words.stream().mapToInt(value -> Integer.valueOf(value)).sum();
// 平均值
Double avg = words.stream().collect(averagingInt(value -> Integer.valueOf(value)));
// 最大值
String max1 = words.stream().max(comparing(Integer::valueOf)).get();
String max2 = words.stream().collect(maxBy(comparing(Integer::valueOf))).get();
// 总结值
IntSummaryStatistics summary = words.stream().collect(summarizingInt(Integer::valueOf));
System.out.println(summary.getAverage());
System.out.println(summary.getSum());
System.out.println(summary.getCount());
System.out.println(summary.getMax());
System.out.println(summary.getMin());
Stream 要优先用Collection作为返回值
//stream的iterator
// need to cast
for (ProcessHandle ph : (Iterable<ProcessHandle>)ProcessHandle.allProcesses()::iterator){
...
}
// Adapter from Stream<E> to Iterable<E>
public static <E> Iterable<E> iterableOf(Stream<E> stream) {
return stream::iterator;
}
for (ProcessHandle p : iterableOf(ProcessHandle.allProcesses())) {
// Process the process
}
//spliterator
// spliterator用于并行迭代
public static <E> Stream<E> streamOf(Iterable<E> iterable) {
return StreamSupport.stream(iterable.spliterator(), false);
}
Collection是Iterable的子类型,具有stream方法,因此提供迭代和流访问,所以Collection或适当的子类型通常是返回方法的最佳返回类型。
如果返回的序列小到足够放到内存中,则最好返回一个标准集合实现。
前缀子集、后缀子集
public class SubLists {
public static <E> Stream<List<E>> of(List<E> list) {
return Stream.concat(Stream.of(Collections.emptyList()),
prefixes(list).flatMap(SubLists::suffixes));
}
// (a,b,c) => ((a),(a,b),(a,b,c))
private static <E> Stream<List<E>> prefixes(List<E> list) {
return IntStream.rangeClosed(1, list.size())
.mapToObj(end -> list.subList(0, end));
}
// (a,b,c) => ((a,b,c),(b,c),(c))
private static <E> Stream<List<E>> suffixes(List<E> list) {
return IntStream.range(0, list.size())
.mapToObj(start -> list.subList(start, list.size()));
}
}
//所有子列表
// [1,3,2] => [[1], [1, 3], [1, 3, 2], [3], [3, 2], [2]]
public static <E> Stream<List<E>> of(List<E> list) {
return IntStream.range(0, list.size())
.mapToObj(start -> IntStream.rangeClosed(start + 1, list.size())
.mapToObj(end -> list.subList(start, end)))// subList使用闭区间
.flatMap(x -> x);
}
6. 谨慎使用并行流
ArrayList、HashMap、HsahSet、CouncurrentHashMap、数组、int范围流和long范围流的并行性性能效益最佳。
它们的范围可以确定,而执行任务的抽象为spliterator。
数组存储的元素在内存中相近,数据定位更快。而上面涉及的数据结构基本都基于数组实现。
流的终止操作会影响并行执行的有效性。而流的reduce操作或预先打包(min、max、count和sum)是并行流的最佳实践。
流的中间操作(anyMatch、allMatch和noneMatch)也适合并行操作。
流的collect操作则不适合。
自己实现Stream、Iterable或Collection且希望有良好的并行性能,则需要覆盖spliterator方法。
并行流是基于fork-join池实现的。
当无法写出正确的并行流,将导致异常或者错误的数据。
注:程序的安全性、正确性比性能更重要。
(2)DEMO
// 串行,10^8需要30秒
static long pi(long n) {
return LongStream.rangeClosed(2, n)
.mapToObj(BigInteger::valueOf)
.filter(i -> i.isProbablePrime(50))
.count();
}
// 并行,10^8需要9秒
static long pi(long n) {
return LongStream.rangeClosed(2, n)
.parallel()
.mapToObj(BigInteger::valueOf)
.filter(i -> i.isProbablePrime(50))
.count();
}