Effective Java(第三版) 学习笔记 - 第七章 Lambda和Stream Rule42~Rule48

目录

Rule42 Lambda优先于匿名类

Rule43 方法引用优先于Lambda

Rule44 坚持使用标准的函数接口

Rule45 谨慎使用Stream

Rule46 优先选择Stream中无副作用的函数

Rule47 Stream要优先用Collection作为返回类型

Rule48 谨慎使用Stream并行


Rule42 Lambda优先于匿名类

匿名类不太了解的请先看 Java中四种嵌套类

// Sorting with function objects (Pages 193-4)
public class SortFourWays {
    public static void main(String[] args) {
        List<String> words = Arrays.asList(args);
//        List<String> words = Arrays.asList("51", "12345", "2345", "711");

        // Anonymous class instance as a function object - obsolete! (Page 193)
        // 1:匿名内部类方式
        // idea提示:Anonymous new Comparator<String>() can be replaced with lambda
        Collections.sort(words, new Comparator<String>() {
            public int compare(String s1, String s2) {
                return Integer.compare(s1.length(), s2.length());
            }
        });
        System.out.println(words);
        Collections.shuffle(words);

        // Lambda expression as function object (replaces anonymous class) (Page 194)
        // 2:Lambda带入参方式
        // idea提示:Can be replaced with Comparator.comparingInt
        Collections.sort(words,
                (s1, s2) -> Integer.compare(s1.length(), s2.length()));
        System.out.println(words);
        Collections.shuffle(words);

        // Comparator construction method (with method reference) in place of lambda (Page 194)
        // 3:Lambda 使用Comparator.comparingInt方式
        Collections.sort(words, comparingInt(String::length));
        System.out.println(words);
        Collections.shuffle(words);

        // Default method List.sort in conjunction with comparator construction method (Page 194)
        // 4:直接使用List对象的sort方法
        words.sort(comparingInt(String::length));
        System.out.println(words);
    }
}

如这段代码示例,从原先的匿名内部类5行代码最终直接缩减为1行,同时在不影响理解的基础上,当行代码量也在不断的缩减。需要注意的是,就算是按2的Lambda方式来实现,也不用指定参数类型,因为编译器利用类型推导的过程,根据上下文可以推导出当前类型(java10引入了var关键字、但是有一定的限制条件),同时泛型的作用就体现出来了,只有在极少数情况下需要指定Lambda的参数类型。

Lambda可以大幅缩减代码量,但是如果运用Lambda的方式实现时,如果一个功能不是自描述的、或者代码量超出了几行,那就可以考虑换一种方式来完成,因为Lambda没有名称和文档,短短几行内,大篇幅的Lambda代码可能反而会对阅读性带来一些负面影响。

同时,书中指出,尽可能不要序列化一个Lambda(或者匿名内实例),如果对此不明白的,可以去参照java嵌套类那篇里面的介绍。

// Enum with function object fields & constant-specific behavior (Page 195)
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);
    }

    // Main method from Item 34 (Page 163)
    public static void main(String[] args) {
        double x = Double.parseDouble("1");
        double y = Double.parseDouble("2");
        for (Operation op : Operation.values())
            System.out.printf("%f %s %f = %f%n",
                    x, op, y, op.apply(x, y));
    }
}

Rule34用枚举代替int常量 中的代码示例可以改造成上面。其中需要注意的是java.util.function.DoubleBinaryOperator,在Java8中,java.util.function这个包下面提供了很多Function接口,仔细观察就会发现,这些接口都只有一个待实现方法,里面提供的其他方法都是带有默认实现的default方法。这与Java8,“带有单个抽象方法的接口是特殊的,值得特殊对待”观念呼应。

Rule43 方法引用优先于Lambda

首先一个问题什么是方法引用?

用Rule42中的例子来说,words.sort(comparingInt(String::length));,其中String::length就是方法引用,期初我看这种写法时,都会当成是Lambda表达式。看了这部分才知道,这种写法虽然一般都是搭配Lambda表达式来使用,但其本身是有专门的称呼的,就是方法引用(虽然常常会被叫做Lambda写法)。

方法引用与Lambda表达式
方法引用类型范例Lambda等式
静态Integer::parseIntstr -> Integer.parseInt(str)
有限制Instant.now()::isAfter

Instant then = Instant.now();

t -> then.isAfter(t)

无限制String::toUpperCasestr -> str.toUpperCase()
类构造器TreeMap<K, V>::new() -> new TreeMap<K, V>
数组构造器int[]::newlen -> new int[len]

相比之下,方法引用常常会比Lambda表达式显得更加简练。但是有时候也有例外的情况,比如自定义了一个类GoshThisClassNameIsHumongous

  • service.execute(GoshThisClassNameIsHumongous::action);
  • service.execute(() -> action());

相比之下,Lambda表达式会显得更加简练,所以我们日常开发只要遵循一个原则就好,如果方法引用会比较简练,那我们就用方法引用,否则我们就使用Lambda表达式。

Rule44 坚持使用标准的函数接口

  • 函数接口:即这个接口主要目的是为了提供给函数式编程所用,即只包含一个抽象方法的接口。可以用注解@FunctionalInterface来进行标明。
  • 标准函数接口:java.util.function下面所提供的已经定义好了的函数接口。如果标准函数接口能够满足需求,那么我们就不应该自己再定义一套。

@FunctionalInterface有什么作用

  1. 告诉大家这个接口是针对Lambda设计的。
  2. 这个接口只允许有一个抽象方法,不然无法编译。
  3. 不要提供重载方法,有可能会引起客户端的歧义。

目前java8的java.util.function下面,接口有几十个,但是我们只要记住6个基础的接口,其他的接口可以通过这6个基础的进行类推。本质上就是执行那唯一一个抽象方法的实现,只不过由于参数类型、返回类型,无参、一个参数、两个参数,有无返回值的区别,做了基础的区分而已。

标准函数接口 - 基础6种类型
接口函数签名范例备注

一元运算符

UnaryOperator<T>

T apply(T t)String::toUpperCase

Unary代表一个入参、Operator代表返回结果与参数一致的函数

※继承自Function<T, R>接口,统一了泛型类型

二元运算符

BinaryOperator<T>

T apply(T t1, T t2)BigInteger::add

Binary代表两个入参、Operator代表返回结果与参数一致的函数

※继承自BiFunction<T, U, R>接口,统一了泛型类型

谓词

Predicate<T>

boolean test(T t)Collection::isEmptyPredicate代表有一个入参,并返回布尔值的函数

函数

Function<T, R>

R apply(T t)Arrays::asListFunction代表返回类型与入参类型不一致的函数

提供者

Supplier<T>

T get()Instant::nowSupplier代表无参数,返回一个值的函数

消费者

Consumer<T>

void accept(T t)System.out::printlnConsumer代表无返回值,有一个参数的函数

Rule45 谨慎使用Stream

关于Stream流的基础介绍,请看这篇《Java8 流相关分享

流的写法虽然可以大大简化代码量,但是运用的不好的话反而会产生负面效应。

// Overuse of streams - don't do this! (page 205)
public class StreamAnagrams {
    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);
        }
    }
}

就比如这段代码,很难快速理解它的意图是什么。适当的做一些方法抽出,反而会提高代码的阅读性。

// Tasteful use of streams enhances clarity and conciseness (Page 205)
public class HybridAnagrams {
    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(g -> System.out.println(g.size() + ": " + g));
        }
    }

    private static String alphabetize(String s) {
        char[] a = s.toCharArray();
        Arrays.sort(a);
        return new String(a);
    }
}

有一些工作适合用Lambda表达式来完成,但是Lambda表达式并不是万能的,比如下面的一些情况:

  • 代码块中,可以读取和修改范围内,任意局部变量;Lambda只能读取final变量,而且无法改值
  • 代码块中,可以return、break、continue,或者抛出异常;Lambda都不可以

反之,下面的情况适合运用Lambda完成:

  • 统一转换元素的序列
  • 过滤元素的序列
  • 利用单个操作(如添加、连接或者计算其最小值)合并元素顺序
  • 将元素的序列存放到一个集合中,根据某些公共属性进行分组
  • 搜索满足某些条件的元素的序列

Rule46 优先选择Stream中无副作用的函数

Stream副作用在另一篇Stream中间也有过介绍。理想中的Stream侧重的是把一系列的操作尽可能的成为纯函数,不依赖任何可变状态、也不更新任何状态。函数的结果只取决于输入的函数,尽可能的选用无状态的、无副作用的方法。

// Frequency table examples showing improper and proper use of stream (Page 210-11)
public class Freq {
    public static void main(String[] args) throws FileNotFoundException {
        File file = new File(args[0]);

        // Uses the streams API but not the paradigm--Don't do this!
        Map<String, Long> freq1 = new HashMap<>();
        try (Stream<String> words = new Scanner(file).tokens()) {
            words.forEach(word -> {
                freq1.merge(word.toLowerCase(), 1L, Long::sum);
            });
        }

        // Proper use of streams to initialize a frequency table (
        Map<String, Long> freq2;
        try (Stream<String> words = new Scanner(file).tokens()) {
            freq2 = words
                    .collect(groupingBy(String::toLowerCase, counting()));
        }
    }
}

这两种实现方式,freq1只是看起来是Stream操作,但是其实本质还是迭代器操作,而freq2才是纯Stream操作。forEach操作应该只用于报告Stream操作的结果,而不是执行计算

Rule47 Stream要优先用Collection作为返回类型

三大类、Collection集合、数组、迭代器。一般仅关注循环,推荐用迭代器;如果返回基本类型、同时对性能有要求,推荐用数组;其他情况一般都推荐用集合返回。

※遗憾的是,关于书中这部分4页的篇幅,我没太理解到核心想要表达什么内容。

Rule48 谨慎使用Stream并行

Java5引入了JUC、Java7引入了Fork-Join(处理并行分解的高性能框架)、Java8引入了Stream(只需要parallel就可以实现并行处理、内部实现是默认的ForkJoinPool)。编写并发编程越来越容易、但是还是需要关注并发编程的安全性活性失败问题。

※活性失败:A修改了共享变量,此时线程B感知不到此共享变量的改变,就叫活性失败。常见的对策就是两个线程对此贡献变量拥有happens-before关系,最常见的就是volatile 或 加锁。(关于并发编程后面会陆续完善整理)

// Parallel stream-based program to generate the first 20 Mersenne primes - HANGS!!! (Page 222)
public class ParallelMersennePrimes {
    public static void main(String[] args) {
        primes().map(p -> TWO.pow(p.intValueExact()).subtract(ONE))
                .parallel()
                .filter(mersenne -> mersenne.isProbablePrime(50))
                .limit(20)
                .forEach(System.out::println);
    }

    static Stream<BigInteger> primes() {
        return Stream.iterate(TWO, BigInteger::nextProbablePrime);
    }
}

这段代码用了parallel()做并行,但是由于源头是Stream.iterate、或者用了limit()有状态中间操作,最终的效果与预期会完全相反,甚至引起活性失败,导致一致没有进度。什么场合适合用parallel(),数据源可以被切分成精准的、轻松地分成任意大小的子范围。例如:ArrayList、HashMap、ConcurrentHashMap、HashSet、数组、int范围、long范围等。

这些数据结构还拥有另一个特性,优异的引用局部性。序列化元素的引用一起被保存在内存中。被那些引用访问到的对象在内存中可能并不是一个挨着一个的,这种不连续性会降低引用局部性。在并行执行的时候,线程需要等待将对象从内存获取到处理器的缓存中,就容易出现线程等待。引用局部性最好的数据结构是基本类型数组,因为他们本身就是相邻的保存在内存之中。

如果大量的操作逻辑都是放在终端方法中的,而且还是固有顺序的执行,也会降低并行时的效率。并行操作最理想的终端操作是做减法,用一个reduce方法,将所有pipeline结果合并起来。或者利用短路(short-circuiting)方法也可以并行。

由Stream的collect执行的操作,并不是最好的并行方法,因为本身集合的合并就成本比较高。

如果想要使用并行操作,请先确保需要这样做,同时最好在真实环境中进行性能测试。

本文技术菜鸟个人学习使用,如有不正欢迎指出修正。xuweijsnj

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值