Effective Java 讀書筆記(五):Lambda和Stream

1 Lamdba優於匿名內部類

(1)DEMO1

  • 匿名內部類:過期
Collections.sort(words, new Comparator<String>() {
    public int compare(String s1, String s2) {
     return Integer.compare(s1.length(), s2.length());
    }
});
  • 上述使用了策略模式,Comparator接口爲排序的抽象策略,匿名內部類爲具體實現策略,可是匿名內部類的實現過於冗長。
  • 在java8中,若是一個接口只有一個方法,那麼這個接口能夠看做一個函數接口,功能接口的實現類能夠經過lambda來實現,lambda與匿名內部類相似,可是更加簡潔。html

  • lamdba:常規java

Collections.sort(words,(s1, s2) -> Integer.compare(s1.length(), s2.length()));
  • 參數爲String類型,返回值爲int類型,編譯器是如何知道的呢?
  • 編譯器使用稱爲類型推斷的過程從上下文中推導出這些類型,可是編譯器不是萬能的,有時候仍然須要顯式設定。正則表達式

  • lamdba:方法引用數組

Collections.sort(words, comparingInt(String::length));
words.sort(comparingInt(String::length));

(2)DEMO2

  • 常量類:enum with function object
public enum Operation {
    PLUS("+") {
        public double apply(double x, double y) { return x + y; }
    },
    MINUS("-") {
        public double apply(double x, double y) { return x - y; }
    },
    TIMES("*") {
        public double apply(double x, double y) { return x * y; }
    },
    DIVIDE("/") {
        public double apply(double x, double y) { return x / y; }
    };
    
    private final String symbol;
    
    Operation(String symbol) { this.symbol = symbol; }
    
    @Override public String toString() { return symbol; }
    
    public abstract double apply(double x, double y);
}
  • lambda:enum with function object
public enum Operation {
    PLUS ("+", (x, y) -> x + y),
    MINUS ("-", (x, y) -> x - y),
    TIMES ("*", (x, y) -> x * y),
    DIVIDE("/", (x, y) -> x / y);
    private final String symbol;
    private final DoubleBinaryOperator op;
    
    Operation(String symbol, DoubleBinaryOperator op) {
     this.symbol = symbol;
     this.op = op;
    }
    @Override public String toString() { return symbol; }
    public double apply(double x, double y) {
     return op.applyAsDouble(x, y);
    }
}
  • lambda中的不要超過三行。
  • lambda中沒法訪問枚舉的實例成員。
  • lambda沒法建立抽象類的實例,但匿名內部類能夠。
  • lambda沒法獲取到對自身的引用。
  • 若是須要反序列化一個函數接口,如:Comparator,咱們須要使用私有靜態內部類。

2 lambda中優先使用方法引用

(1)DEMO

// lambda代碼塊
map.merge(key, 1, (count, incr) -> count + incr);

// 方法引用
map.merge(key, 1, Integer::sum);

(2)類型

方法引用類型 例子 Lambda等效方案
Static Integer::parseInt str -> Integer.parseInt(str)
Bound Instant.now()::isAfter Instant then = Instant.now();
t -> then.isAfter(t)
Unbound String::toLowerCase str -> str.toLowerCase()
Class Constructor TreeMap::new () -> new TreeMap
Array Constructor int[]::new len -> new int[len]
  • 若是方法引用更加簡潔和清晰,請使用方法引用,反之使用Lambda表達式。

3 優先使用標準功能接口

(1)模板方法模式

  • 因爲Lambda的存在,經過子類重寫基本方法以專門化超類的行爲的方式有點過期。
  • 替代方案:提供一個靜態工廠或者構造器,它們接收一個函數對象來實現相同的效果。
  • 通常來講,咱們將編寫更多以函數對象做爲參數的構造函數和方法。
// 模板方法
abstract class A {
    public void print() {
        System.out.println("A");
        doSubThing();
    }

    abstract void doSubThing();
}

class B extends A {
    @Override
    void doSubThing() {
        System.out.println("B");
    }
}

// lambda
class A {
    private Supplier<String> supplier;

    public A(Supplier<String> supplier) {
        this.supplier = supplier;
    }

    public void print() {
        System.out.println("A");
        System.out.println(supplier.get());
    }
}

public static void main(String[] args) {
    A a = new A(() -> "B");
    a.print();
}

(2)標準函數接口

接口 函數簽名 例子
UnaryOperator<T> T apply(T t) String::toLowerCase
BinaryOperator<T> T apply(T t1, T t2) BigInteger::add
Predicate<T> boolean test(T t) Collection::isEmpty
Function<T> R apply(T t) Arrays::asList
Supplier<T> T get() Instant::now
Consumer<T> void accept(T t) System.out::println
  • 優先使用標準函數接口,這可以縮小概念表面積,從而下降學習成本。
  • 可是若是全部標準函數接口都不能很好表示時,請
  • 上述的六種接口擁有許多變種,如:int、long和double,甚至是int->long等等。
  • 其實大多數變種都使用了基礎類型,而不是包裝類型,基礎類型的運算更快,節省內存空間,不要使用包裝類去替換它們。
  • 其實咱們並不會去記憶全部變種,變種隨着JDK升級可能會增長或減小,只須要在使用時去翻翻java.util.function包是否有須要的接口便可。

具體請參考:JAVA8的java.util.function包安全

(3)Comparator接口

  • Comparator接口與ToIntBiFunction接口的結構相同,可是仍然不要用ToIntBiFunction去替代Comparator
  • 由於Comparator的名稱含義十分清晰,它在jdk中已經普遍使用了,並且Comparator提供了許多有用默認方法。

(4)@FunctionalInterface

  • @FunctionalInterface註解可以幫助開發者檢查這個接口是否只有一個抽象方法,若是不僅一個將沒法編譯。
  • @FunctionalInterface目的:將某個接口標誌爲函數接口且提供編譯時檢查。

4 明智地使用Stream

4.1 概念

  • 流:無限或有限的數據元素序列。
  • 管道:對流中的元素進行多級計算。數據結構

  • 流的源:集合、數組、文件、正則表達式或模式匹配器、僞隨機數生成器或其餘流。併發

  • 管道操做:由源流後跟着零個或多箇中間操做和一個終止操做。
  • 中間操做:某種轉換流的方式,如:元素映射或元素過濾等。
  • 終止操做:執行最終計算,如:流裝入容器中或是消費掉。app

4.2 補充

  • 流管道只包含中間操做時是惰性的:當一個流沒有最終操做時,流管道是什麼都不作的。
  • 流管道的API被設計成鏈式編碼風格。

4.3 流的使用時機

(1)不要濫用流
// 普通方式
// 讀取文件中的單詞,檢查單詞的字母,相同字母的單詞收集在一塊兒
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存在性能問題)。
(2)代碼塊與lambda
  • Stream的缺點
    • 代碼塊可以讀取或修改範圍內的局部變量,lambda只能操做final變量和當前範圍的局部變量。
    • 代碼塊中可以return、拋出異常、跳出循環或是跳過循環,lambda中都沒法作到。
  • Stream的優點
    • map:統一轉換元素類型
    • filter:過濾序列
    • min、compute:計算最小值、合併序列等
    • reduce:累計序列
    • grouping:分組
(3)流沒法作到同時在多級階段訪問相應的元素
  • 經過操做反轉來獲取上一個流元素
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 優先選擇流中無反作用的功能

(1)概述

  • 爲了獲得stream的表現力、速度和並行度,咱們必須遵照範式和使用API。
  • stream範式最重要的部分:計算 -> 轉換 ,每一個轉換(中間或終止操做)都是純函數。
  • 純函數應該都是無反作用的(不依賴任何可變狀態,不更新任何狀態)。

(2)Collectors的基本方法

名稱 做用
toCollection toList toSet 流轉換爲集合
toMap 流轉換爲Map
partitioningBy groupingBy groupingByConcurrent 分組
minBy maxBy 最值
counting 計數
summingInt averagingInt 和 平均值
joining mapping 合併 映射

(3)示例

// 不遵照範式,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()));
}

(4)分組

  • 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"))
);
System.out.println(map); // {false=[2, 3], true=[1, 1]}


Map<String, List<String>> map = words.stream().collect(
    groupingBy(String::toLowerCase)
);
System.out.println(map); // {1=[1, 1], 2=[2], 3=[3]}

(5)List轉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)
);

(6)List->字符串

// 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(",","[","]"));
System.out.println(join1); // 2113
System.out.println(join2); // 2,1,1,3
System.out.println(join3); //[2,1,1,3]

// 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());
System.out.println(list1); // [2, 1, 1, 3]
System.out.println(list2); // [2, 1, 1, 3]

(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());

6 優先選擇集合做爲返回值

(1)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
}

(2)spliterator

// spliterator用於並行迭代
public static <E> Stream<E> streamOf(Iterable<E> iterable) {
    return StreamSupport.stream(iterable.spliterator(), false);
}

// 例子:並行計算1+2+...+10000
public static void main(String[] args) throws InterruptedException {
    List<String> words = new ArrayList<>();
    for (int i = 1; i <= 10000; i++) {
        words.add(i + "");
    }
    final AtomicInteger atomicInteger = new AtomicInteger(0);
    int count = 10;
    CountDownLatch latch = new CountDownLatch(count);

    final List<Spliterator<String>> splitList = split(words, count);
    for (int i = 0; i < count; i++) {
        int finalI = i;
        new Thread(() -> {
            try {
                splitList.get(finalI)
                    .forEachRemaining(s -> atomicInteger.getAndAdd(Integer.valueOf(s)));
            } finally {
                latch.countDown();
            }
        }, "Thread:" + i).start();
    }
    latch.await();
    System.out.println(atomicInteger.get());
}

public static <T> List<Spliterator<T>> split(List<T> list, int size) {
    List<Spliterator<T>> returnList = new ArrayList<>();
    returnList.add(list.spliterator());
    if (size > 1) spliterator(returnList, 2, size);
    return returnList;
}

private static <T> void spliterator(List<Spliterator<T>> returnList, int i, int size) {
    int j = i / 2 - 1;
    returnList.add(returnList.get(j).trySplit());
    if (size == i) return;
    spliterator(returnList, i + 1, size);
}

(3)原則

  • Collection是Iterable的子類型,具備stream方法,所以提供迭代和流訪問,所以Collection或適當的子類型一般是返回方法的最佳返回類型。
  • 若是返回的序列小到足夠放到內存中,則最好返回一個標準集合實現。

(4)DEMO

  • 冪集
// {a,b,c}的冪集爲{{},{a},{b},{c},{a,b},{a,c},{b,c},{a,b ,c}}
public class PowerSet {
    public static final <E> Collection<Set<E>> of(Set<E> s) {
        List<E> src = new ArrayList<>(s);
        if (src.size() > 30)
            throw new IllegalArgumentException("Set too big " + s);
        return new AbstractList<Set<E>>() {
            @Override public int size() {
                // 若是size > 31將致使溢出int的範圍
                return 1 << src.size(); // 2^size
            }
            @Override public boolean contains(Object o) {
                return o instanceof Set && src.containsAll((Set)o);
            }
            @Override public Set<E> get(int index) {
                Set<E> result = new HashSet<>();
                for (int i = 0; index != 0; i++, index >>= 1)
                    if ((index & 1) == 1) result.add(src.get(i));
                return result;
            }
        };
    }
}
  • 前綴子集、後綴子集
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);
}

7 謹慎使用並行流

(1)原則

  • ArrayList、HashMap、HsahSet、CouncurrentHashMap、數組、int範圍流和long範圍流的並行性性能效益最佳。
  • 它們的範圍能夠肯定,而執行任務的抽象爲spliterator。
  • 數組存儲的元素在內存中相近,數據定位更快。而上面涉及的數據結構基本都基於數組實現。ide

  • 流的終止操做會影響並行執行的有效性。而流的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();
}
相關文章
相關標籤/搜索