带你快速看完9.8分神作《Effective Java》—— Lambda 和 Stream篇(小王工作里用的很多)

简介: 42 Lambda优先于匿名类43 方法引用优先于Lambda44 优先使用标准的函数式接口45 谨慎使用Stream46 优先选择Stream中无副作用的函数47 Stream要优先用Collection作为返回类型48 谨慎使用Stream并行

42 Lambda优先于匿名类


自从JDK 1.1于1997年发布以来,创建函数对象的主要手段就是匿名类。下面代码是按照字符串⻓度顺序对列表进行排序,使用匿名类创建排序的比较方法:


Collections.sort(words, new Comparator<String>() {
  public int compare(String s1, String s2) {
    return Integer.compare(s1.length(), s2.length());
  }
});


匿名类适用于需要函数对象的经典面向对象设计模式,特别是策略模式。Comparator接口代表一种排序的抽象策略;上面的匿名类是排序字符串的具体策略。


在Java 8中,“带有单个抽象方法的接口”是特殊的,他们被称作函数式接口,Java允许利用Lambda表达式创建这些接口的实例。


Collections.sort(words, (s1, s2) -> Integer.compare(s1.length(), s2.length()));


Lambda的类型是Comparator <String>,其参数s1,s2的类型是String,返回值类型int全都没有出现在代码里


编译器使用一个叫类型推断的过程从上下文中推断出这些类型。


如果编译器产生错误消息,无法推断出Lambda参数的类型,那就手动指定它。



如果用Lambda表达式代替Comparator的构造方法,代码会更加简练:


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

如果用Java 8中List接口里的sort方法,代码还可以更简洁:


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

Java中增加了Lambda之后,使得之前不能使用函数对象的地方现在也能用了。例如,以34条里的Operation枚举类型为例:

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改造的话,只要给每个枚举常量的构造器传递一个实现其行为的Lambda即可:


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存储在实例属性中,apply 方法将调用转发给Lambda,代码量更少,逻辑也更清晰。


使用Lambda需要注意的地方是:Lambda没有名称和文档;如果计算比较复杂,或者代码量超过几行,就不要用Lambda了。


Lambda表达式一行是最好的,三行是极限!不能再多了


传递给枚举构造方法的参数是在静态环境中计算的。因此,枚举构造方法中的Lambda表达式不能访问枚举的实例成员。如果枚举类型具有难以理解的特殊方法,使用原先的实现方式仍是首选。



匿名类在Lambda时代并未过时,可以依据以下几点来进行选择:

1. 如果想创建抽象类的实例,可以用匿名类来完成


2. 如果一个接口有多个抽象方法,创建实例要用匿名类


3. Lambda无法获得对自身的引用

在Lambda中,关键字this指向外围实例,这通常是我们想要的



不要序列化一个 Lambda 或 匿名类实例,如果想要可序列化的函数对象,如Comparator,就使用私有静态嵌套类的实例。


43 方法引用优先于Lambda


如果方法引用看起来更简短更清晰,就用方法引用;否则还是用Lambda



Java提供了一种生成函数对象的方法,比lambda还要简洁:方法引用(method references),就是常说的::运算符



下面代码是用来保持从任意键到Integer的映射:


map.merge(key, 1, (count, incr) -> count + incr);

代码使用了Java 8中的 Map 接口中的merge方法,如果没有给定key的映射,就插入默认值(上面代码里的1);如果映射已经存在,则将函数应用于当前值和指定值,并用结果覆盖当前值。这里的函数是将现有值count递增incr



Integer 类(和所有其他包装数字基本类型)提供了一个静态方法sum,只传入这个方法的引用也行:

map.merge(key, 1, Integer::sum);


使用了方法引用的代码更简洁


但有时候Lambda会比方法引用更简洁,大多数情况是方法与lambda相同的类中,例如下面的代码发生在GoshThisClassNameIsHumongous类里:

- 方法引用


service.execute(GoshThisClassNameIsHumongous::action);

- Lambda

service.execute(() -> action());


类似的还有 Function 接口,它用一个静态工厂方法返回 id 函数 Function.identity()。如果使用等效的lambda内联代码:

x -> x

这样会更简洁



无限制的引用经常用在流管道(Stream pipeline)中作为映射和过滤函数;构造器引用是充当工厂对象


44 优先使用标准的函数式接口


如果标准函数接口能满足要求,应该优先使用它,而不是专⻔自己创建新的函数接口。



以LinkedHashMap为例,可以通过重写其protected removeEldestEntry方法将此类用作缓存,每次将新的key值加入到map时都会调用该方法。以下代码重写允许map最多保存100个条目,然后在每次添加新key值时删除最老的条目:


protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
  return size() > 100;
}


用Lambda可以做得更好,自己定义一个函数接口如下:

@FunctionalInterface
interface EldestEntryRemovalFunction<K,V>{
  boolean remove(Map<K,V> map, Map.Entry<K,V> eldest);
}

这个接口可以正常工作,但是没必要,因为java.util.function包提供了大量标准函数式接口以供使用。


许多标准函数式接口都提供了有用的默认方法。如Predicate接口提供了组合判断的方法。标准的BiPredicate<Map<K,V>, Map.Entry<K,V>> 接口应优先于自定义的EldestEntryRemovalFunction接口的使用。



EldestEntryRemovalFunction 接口使用@FunctionalInterface注解进行标注的。这个注解类型本质上与@Override一样,有三个目的:


告诉读者这个接口是针对Lambda设计的

这个接口不会进行编译,除非他只有一个抽象方法

避免后续维护人员不小心给该接口添加抽象方法

始终使用@FunctionalInterface1注解标注自己写的函数式接口

这六个基础接口各自还有3种变体(int、long、double),例如predicate的变体IntPredicate

Function 接口还有9种变体,LongToIntFunction、DoubleToObjFunction等

这三种基础函数接口还有带两个参数的版本,BiPredicate <T,U>、BiFunction <T,U,R>、BiConsumer <T,U>

还有BiFunction变体用于返回三个相关的基本类型:ToIntBiFunction<T,U>,ToLongBiFunction<T,U> 和ToDoubleBiFunction <T,U>

Consumer接口也有带两个参数的变体版本,带一个对象和一个基本类型:ObjDoubleConsumer <T>,ObjIntConsumer <T>和 ObjLongConsumer <T>

还有一个 BooleanSupplier 接口,它是 Supplier 的一个变体,返回boolean


注意:不要用带包装类型的基础函数接口来代替基本函数接口。使用装箱基本类型进行批量操作处理,后果可能是致命的。



什么时候应该自己编写接口呢?


答案是:如果没有一个标准的函数接口能够满足需求时



以Comparator <T>为例,它的结构与ToIntBiFunction <T, T>接口相同。Comparator有自己的接口有以下几个原因:


每当在API中使用时,其名称提供了良好的文档信息

Comparator接口对于如何构成一个有效的实例,有着严格的条件限制

这个接口配置了大量好用的default方法,可以对Comparator进行转换和合并


如果所需要的函数接口与Comparator一样具有以下特征,就需要自己编写专用的函数接口了:


通用,并且将受益于描述性的名称

具有与其关联的严格的契约

将受益于定制的缺省方法


45 谨慎使用Stream


在Java 8中添加了Stream API,以简化串(并)行执行批量操作的任务。


Stream表示有限或无限的数据元素序列,Stream pipeline,表示对这些元素的多级计算。Stream中的元素可以来自集合、数组、文件、正则表达式模式匹配器、伪随机数生成器和其他Stream。数据可以是对象引用或基本类型(int、long、double)。


一个Stream pipeline包含一个 源Stream,几个中间操作,1个终止操作。每个中间操作都以某种方式转换Stream,比如过滤操作。终止操作会对Stream执行一个最终计算,比如返回一个List,打印所有元素等。


Stream pipeline是lazy的:直到调用终止操作时才会开始计算

没有终止操作的的Stream pipeline是静默的,所以终止操作千万不能忘


Stream API是fluent的:所有包含pipeline的调用可以链接成一个表达式

介绍完Stream之后,肯定就会有小伙伴们开始思考了,我们应该在什么时候用呢?


其实并没有任何硬性的规定,但可以从以下例子中得到启发:


例一:

读取字典中的单词,打印出单词出现次数大于某值的所有“换位词”


换位词:包含相同字母,但顺序不同的单词

如果换位词一样,这里就认为是同一个单词


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


将每个单词插入到map中中使用了computeIfAbsent方法,computeIfAbsent 方法对 hashMap 中指定 key 的值进行重新计算,如果不存在这个 key,则添加到 hashMap 中。

语法为:


hashmap.computeIfAbsent(K key, Function remappingFunction)


参数说明:

  • key - 键
  • remappingFunction - 重新映射函数,用于重新计算value


例二:

这个例子大量使用了Stream


public class Anagrams {
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);
    }
  }
}


如果你发现这段代码难以阅读,别担心,我也难看懂吗,在工作里面也是不提倡的,所以滥用Stream会使得程序代码难以读懂和维护


例三:

下面的代码和例二的逻辑相同,它没有过度使用Stream,代码可读性很强:


public class Anagrams {
    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));
        }
    }
    // alphabetize method is the same as in original version
    private static String alphabetize(String s) {
        char[] a = s.toCharArray();
        Arrays.sort(a);
        return new String(a);
    }
}


它在一个try-with-resources块中打开文件,获得一个由文件中的所有代码的Stream。Stream中的pipeline没有中间操作,终止操作是将所有单词集合到一个映射中,按照它们的换位词对单词进行分组


values().stream()


打开了一个新的Stream<List<String>>,这个Stream里的元素都是换位词,filter进行了过滤,忽略大小小于minGroupSize的所有组,最后由终结操作forEach打印剩下的同位词组。



提高Stream代码的可读性有两个要求:


在没有显式类型的情况下,认真命名Lambda参数

使用辅助方法(上面的alphabetize),因为pipeline缺少显式类型信息和命名临时变量

需要提醒一点,使用Stream处理char类型的数据有风险:


例四:

"Hello world!".chars().forEach(System.out::print);


发现它打印 721011081081113211911111410810033。这是因为“Hello world!”.chars()返回的Stream的元素不是char值,而是int,修改方法是加一个强制类型转换:

"Hello world!".chars().forEach(x -> System.out.print((char) x));

所以应该避免使用Stream来处理char值


综上所述,Stream适合完成下面这些工作:


统一转换元素序列

过滤元素序列

使用单个操作组合元素序列(例如添加、连接或计算最小值)

将元素序列累积到一个集合中,可能通过一些公共属性将它们分组

在元素序列中搜索满足某些条件的元素

假设Card是一个不变值类,用于封装Rank和Suit,下面代码求他们的笛卡尔积:

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;
}

基于Stream实现的代码如下:


private static List<Card> newDeck() {
  return Stream.of(Suit.values())
    .flatMap(suit ->
      Stream.of(Rank.values())
        .map(rank -> new Card(suit, rank)))
    .collect(toList());
}

其中用到了 flatMap 方法:这个操作将一个Stream中的每个元素都映射到一个Stream中,然后将这些新的Stream全部合并到一个Stream(或展平它们)。

newDeck的两个版本中到底哪一个更好?这就是仁者见仁智者见智的问题了,取决于你的个人喜好 : )


相关文章
|
27天前
|
存储 Java 数据挖掘
Java 8 新特性之 Stream API:函数式编程风格的数据处理范式
Java 8 引入的 Stream API 提供了一种新的数据处理方式,支持函数式编程风格,能够高效、简洁地处理集合数据,实现过滤、映射、聚合等操作。
44 6
|
27天前
|
Java API 开发者
Java中的Lambda表达式与Stream API的协同作用
在本文中,我们将探讨Java 8引入的Lambda表达式和Stream API如何改变我们处理集合和数组的方式。Lambda表达式提供了一种简洁的方法来表达代码块,而Stream API则允许我们对数据流进行高级操作,如过滤、映射和归约。通过结合使用这两种技术,我们可以以声明式的方式编写更简洁、更易于理解和维护的代码。本文将介绍Lambda表达式和Stream API的基本概念,并通过示例展示它们在实际项目中的应用。
|
29天前
|
Java API 开发者
Java中的Lambda表达式:简洁代码的利器####
本文探讨了Java中Lambda表达式的概念、用途及其在简化代码和提高开发效率方面的显著作用。通过具体实例,展示了Lambda表达式如何在Java 8及更高版本中替代传统的匿名内部类,使代码更加简洁易读。文章还简要介绍了Lambda表达式的语法和常见用法,帮助开发者更好地理解和应用这一强大的工具。 ####
|
1月前
|
并行计算 Java 编译器
深入理解Java中的Lambda表达式
在Java 8中引入的Lambda表达式,不仅简化了代码编写,还提升了代码可读性。本文将带你探索Lambda表达式背后的逻辑与原理,通过实例展示如何高效利用这一特性优化你的程序。
|
1月前
|
搜索推荐 Java API
探索Java中的Lambda表达式
本文将深入探讨Java 8引入的Lambda表达式,这一特性极大地简化了代码编写,提高了程序的可读性。通过实例分析,我们将了解Lambda表达式的基本概念、使用场景以及如何优雅地重构传统代码。文章不仅适合初学者,也能帮助有经验的开发者加深对Lambda表达式的理解。
|
1月前
|
Java
探索Java中的Lambda表达式
【10月更文挑战第37天】本文将带你深入理解Java的Lambda表达式,从基础语法到高级特性,通过实例讲解其在函数式编程中的应用。我们还将探讨Lambda表达式如何简化代码、提高开发效率,并讨论其在实际项目中的应用。
|
18天前
|
Rust 安全 Java
Java Stream 使用指南
本文介绍了Java中Stream流的使用方法,包括如何创建Stream流、中间操作(如map、filter、sorted等)和终结操作(如collect、forEach等)。此外,还讲解了并行流的概念及其可能带来的线程安全问题,并给出了示例代码。
|
1月前
|
Java API
Java中的Lambda表达式与函数式编程####
【10月更文挑战第29天】 本文将深入探讨Java中Lambda表达式的实现及其在函数式编程中的应用。通过对比传统方法,我们将揭示Lambda如何简化代码、提高可读性和维护性。文章还将展示一些实际案例,帮助读者更好地理解和应用Lambda表达式。 ####
|
1月前
|
Java API 开发者
Java中的Lambda表达式与函数式编程####
在Java的演变过程中,Lambda表达式和函数式编程的引入无疑是一次重大的飞跃。本文将深入探讨Lambda表达式的定义、用法及优势,并结合实例说明如何在Java中利用Lambda表达式进行函数式编程。通过对比传统编程方式,揭示Lambda表达式如何简化代码、提高开发效率和可维护性。 ####
|
26天前
|
安全 Java API
Java中的Lambda表达式:简化代码的现代魔法
在Java 8的发布中,Lambda表达式的引入无疑是一场编程范式的革命。它不仅让代码变得更加简洁,还使得函数式编程在Java中成为可能。本文将深入探讨Lambda表达式如何改变我们编写和维护Java代码的方式,以及它是如何提升我们编码效率的。