zoukankan      html  css  js  c++  java
  • 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();
    }
    
  • 相关阅读:
    javascript异步编程学习及实例
    通过调试vue-cli 构建代码学习vue项目构建运行过程
    web技术栈开发原生应用-多端共用一套代码
    vuejs应用开发前后端分离
    web前端跨域解决方案JSONP,CORS,NGINX反向代理
    js script 加载顺序
    js 百度地图
    layui-table 样式
    css 排版 test
    layui table
  • 原文地址:https://www.cnblogs.com/linzhanfly/p/10341371.html
Copyright © 2011-2022 走看看