46、使用无副作用的Stream
本章节主要举例了Stream的几种用法。
案例一:
java
// Uses the streams API but not the paradigm--Don't do this!
Map<String, Long> freq = new HashMap<>();
try (Stream<String> words = new Scanner(file).tokens()) {
words.forEach(word -> {
freq.merge(word.toLowerCase(), 1L, Long::sum);
});
}
案例一使用了forEach,代码看上去像是stream,但是并不是。为了操作HashMap不得不使用循环,导致代码更长。
java
// Proper use of streams to initialize a frequency table
Map<String, Long> freq;
try (Stream<String> words = new Scanner(file).tokens()) {
freq = words.collect(groupingBy(String::toLowerCase, counting()));
}
代码二使用了Stream语法做了和代码一相同的事情,但是代码更短,语义更明确。ForEach应该只负责结果的输出,而不是用来做计算。
案例二:
java
// Pipeline to get a top-ten list of words from a frequency table
List<String> topTen = freq.keySet().stream()
.sorted(comparing(freq::get).reversed())
.limit(10)
.collect(toList());
案例二给freq做排序,输出做多10个元素到List。**Collectors包含很多常用的静态方法,所以直接静态引用Collectors是非常明智的。**例如案例中的comparing方法。
案例三:
java
// Using a toMap collector to make a map from string to enum
private static final Map<String, Operation> stringToEnum
= Stream.of(values()).collect(toMap(Object::toString, e -> e));
案例三将valus的值输出为以String为key, value为Operation的Map。
案例四:
java
// Collector to generate a map from key to chosen element for key
Map<Artist, Album> topHits
= albums.collect(toMap(Album::artist, a->a, maxBy(comparing(Album::sales))));
案例四将albums输出为,Album::artist为key,Album为value,并按照Album::sales排序。
案例五:
java
Map<String, Long> freq = words.collect(groupingBy(String::toLowerCase, counting()));
案例五将words的key变为小写,并出输出。
47、返回值优先使用Collection,而不是Stream
如果一个方法需要按顺序返回多个数据,推荐的返回值类型为Collection。常用的Collection有List,Map。大多数情况下都需要遍历数据,Stream虽然也可以,但是遍历不如Collection方便,并且Collection可以方便的转成Stream。但是不要为了返回Collection而保存大量的元素。
例如:Set有a,b,c三个元素,返回所有的元素组合。 结果:{a},{ab},{abc},{ac},{b},{bc},{c},{}。
结果的个数是当前元素数量的2的n次方,如果Set里面包含更多的子元素,把所有的结果保存下来返回Collection就会占用非常大的内容空间。
java
// Returns a stream of all the sublists of its input list
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));
}
private static <E> Stream<List<E>> prefixes(List<E> list) {
return IntStream.rangeClosed(1, list.size())
.mapToObj(end -> list.subList(0, end));
}
private static <E> Stream<List<E>> suffixes(List<E> list) {
return IntStream.range(0, list.size())
.mapToObj(start -> list.subList(start, list.size()));
}
}
Stream的写法类似使用了for-loop:
java
for (int start = 0; start < src.size(); start++)
for (int end = start + 1; end <= src.size(); end++)
System.out.println(src.subList(start, end));
48、谨慎的使用Stream并发
Stream提供了parallel()函数用于多线程操作,目标是提高运行效率,但是实际上可能并不会这样。
java
// Stream-based program to generate the first 20 Mersenne primes
public static void main(String[] args) {
primes().map(p -> TWO.pow(p.intValueExact()).subtract(ONE))
.filter(mersenne -> mersenne.isProbablePrime(50))
.limit(20)
.forEach(System.out::println);
}
static Stream<BigInteger> primes() {
return Stream.iterate(TWO, BigInteger::nextProbablePrime);
}
上面的代码正常运行时间为12.5s,使用了parallel()函数后,代码的速度并没与提升,且cpu提高到90%一直未执行结束,作者在半小时后强制关闭了程序。
如果资源来自Stream.iterate或者limit这种有中间操作,让管道并行不太可能提升提升效率。所以不能随意的使用parallel()。**如果Stream的数据来自于ArrayList , HashMap , HashSet , ConcurrentHashMap instances,arrays,int ranges,long ranges,使用parallel会让运行效率更高。**让Stream并行除了导致运行效率降低,还有可能出现错误的结果以及不可以预料的情况,所以在使用paralle()一定要经过测试验证,保证自己编写的代码运行正确。
在合适的环境下,Stream在多核机器下使用paralle()会得到接近线性的加速,例如如下代码:
java
// Prime-counting stream pipeline - benefits from parallelization
static long pi(long n) {
return LongStream.rangeClosed(2, n)
.mapToObj(BigInteger::valueOf)
.filter(i -> i.isProbablePrime(50))
.count();
}
// Prime-counting stream pipeline - parallel version
static long pi(long n) {
return LongStream.rangeClosed(2, n)
// 此处使用了并行
.parallel()
.mapToObj(BigInteger::valueOf)
.filter(i -> i.isProbablePrime(50))
.count();
}
在作者的机器上第一个代码运行时间耗时31s,使用parallel()之后耗时降到9.2s。
49、检查参数的合法性
在大多数的方法和构造函数中都需要传递必要的参数,对每一个参数的合法性验证是非常重要的。在public和protected方法中,需要在JavaDoc说明参数的含义和有效范围,如果参数不合法是否抛出异常:
java
/**
* Returns a BigInteger whose value is (this mod m). This method
* differs from the remainder method in that it always returns a
* non-negative BigInteger.
*
* @param m the modulus, which must be positive
* @return this mod m
* @throws ArithmeticException if m is less than or equal to 0
*/
public BigInteger mod(BigInteger m) {
if (m.signum() <= 0)
throw new ArithmeticException("Modulus <= 0: " + m);
... // Do the computation
}
常见的NullPointerException,可以使用@Nullable注解标注参数不可为null,在Java 7中,提供了Objects.requireNonNull方法帮助检查对象是否为空,为空则会抛出NullPointerException。在Java 9,java.util.Objects还提供了检查索引越界的方法:checkFromIndexSize , checkFromToIndex , checkIndex。还可以使用assert:
java
// Private helper function for a recursive sort
private static void sort(long a[], int offset, int length) {
assert a != null;
assert offset >= 0 && offset <= a.length;assert length >= 0 && length <= a.length - offset;
... // Do the computation
}
如果断言不成立,将会抛出AssertionError。
总之检查参数合法性是非常必要的,它可以防止运行非法的参数造成程序的错误,每一个程序员都应该养成良好的编码习惯。
50、做必要的防御性Copy
为了防止保存的变量被其他人破坏,需要做一些防御性的对象拷贝。例如以下代码:
java
// Broken "immutable" time period class
public final class Period {
private final Date start;
private final Date end;
/**
* @param start the beginning of the period
* @param end the end of the period; must not precede start
* @throws IllegalArgumentException if start is after end
* @throws NullPointerException if start or end is null
*/
public Period(Date start, Date end) {
if (start.compareTo(end) > 0)
throw new IllegalArgumentException(
start + " after " + end);
this.start = start;
this.end = end;
}
public Date start() {
return start;
}
public Date end() {
return end;
}
... // Remainder omitted
}
Period的构造函数保存了start和end,但是这种做法不安全的,因为外部可以改变start和end的变量,所以不能保证此类运算结果不变:
java
// Attack the internals of a Period instance
Date start = new Date();
Date end = new Date();
Period p = new Period(start, end);
end.setYear(78); // Modifies internals of p!
为了解决此问题,应该保存start和end的副本,而不是直接保存start和end:
java
// Repaired constructor - makes defensive copies of parameters
public Period(Date start, Date end) {
this.start = new Date(start.getTime());
this.end = new Date(end.getTime());
if (this.start.compareTo(this.end) > 0)
throw new IllegalArgumentException(this.start + " after " + this.end);
}
除了参数以外,返回值也需要考虑返回副本,尤其是List、Map、Array,要防止直接返回原始数据,导致外部增删改查影响了原始数据。