46、使用無副作用的Stream
本章節主要舉例了Stream的幾種用法。
案例一:
// 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不得不使用循環,導致代碼更長。
// 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應該只負責結果的輸出,而不是用來做計算。
案例二:
// 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方法。
案例三:
// 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。
案例四:
// 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排序。
案例五:
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就會占用非常大的內容空間。
// 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:
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()函數用于多線程操作,目標是提高運行效率,但是實際上可能并不會這樣。
// 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()會得到接近線性的加速,例如如下代碼:
// 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說明參數的含義和有效范圍,如果參數不合法是否拋出異常:
/**
* 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:
// 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
為了防止保存的變量被其他人破壞,需要做一些防御性的對象拷貝。例如以下代碼:
// 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的變量,所以不能保證此類運算結果不變:
// 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:
// 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,要防止直接返回原始數據,導致外部增刪改查影響了原始數據。