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

相关推荐
m0_5719575837 分钟前
Java | Leetcode Java题解之第543题二叉树的直径
java·leetcode·题解
一点媛艺2 小时前
Kotlin函数由易到难
开发语言·python·kotlin
姑苏风2 小时前
《Kotlin实战》-附录
android·开发语言·kotlin
奋斗的小花生3 小时前
c++ 多态性
开发语言·c++
魔道不误砍柴功3 小时前
Java 中如何巧妙应用 Function 让方法复用性更强
java·开发语言·python
NiNg_1_2343 小时前
SpringBoot整合SpringSecurity实现密码加密解密、登录认证退出功能
java·spring boot·后端
闲晨3 小时前
C++ 继承:代码传承的魔法棒,开启奇幻编程之旅
java·c语言·开发语言·c++·经验分享
_.Switch3 小时前
高级Python自动化运维:容器安全与网络策略的深度解析
运维·网络·python·安全·自动化·devops
老猿讲编程3 小时前
一个例子来说明Ada语言的实时性支持
开发语言·ada
Chrikk4 小时前
Go-性能调优实战案例
开发语言·后端·golang