Effective Java 案例分享(九)

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,要防止直接返回原始数据,导致外部增删改查影响了原始数据。

相关推荐
爱码小白10 分钟前
Python 异常处理 完整学习笔记
开发语言·python
c++之路25 分钟前
C++20概述
java·开发语言·c++20
Championship.23.2429 分钟前
Linux Top 命令族深度解析与实战指南
java·linux·服务器·top·linux调试
芝士就是力量啊 ೄ೨39 分钟前
Python如何编写一个简单的类
开发语言·python
橘子海全栈攻城狮44 分钟前
【最新源码】养老院系统管理A013
java·spring boot·后端·web安全·微信小程序
胖虎喜欢静香1 小时前
从零到一快速实现 Mini DeepResearch
人工智能·python·开源
逻辑驱动的ken1 小时前
Java高频面试考点18
java·开发语言·数据库·算法·面试·职场和发展·哈希算法
MoonBit月兔1 小时前
「Why MoonBit 」第一期——Singularity Note AI 学习助手
开发语言·人工智能·moonbit
qq_392690661 小时前
Redis怎样应对Redis集群整体宕机带来的雪崩
jvm·数据库·python
木木_王1 小时前
嵌入式Linux学习 | 数据结构 (Day05) 栈与队列详解(原理 + C 语言实现 + 实战实验 + 易错点剖析)
linux·c语言·开发语言·数据结构·笔记·学习