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与匿名内部类类似,但是更加简洁。

  • lamdba:常规

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 概念

  • 流:无限或有限的数据元素序列。

  • 管道:对流中的元素进行多级计算。

  • 流的源:集合、数组、文件、正则表达式或模式匹配器、伪随机数生成器或其他流。

  • 管道操作:由源流后跟着零个或多个中间操作和一个终止操作。

  • 中间操作:某种转换流的方式,如:元素映射或元素过滤等。

  • 终止操作:执行最终计算,如:流装入容器中或是消费掉。

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。

  • 数组存储的元素在内存中相近,数据定位更快。而上面涉及的数据结构基本都基于数组实现。

  • 流的终止操作会影响并行执行的有效性。而流的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();
}
posted @ 2019-01-31 11:54  月下小魔王  阅读(507)  评论(0编辑  收藏  举报