Java8:Lambdas(二)学习怎样去使用lambda表达式

原文链接  作者:Ted Neward   译者:赵峰

Java SE 8的发布很快就到了。伴随着它来的不仅仅是新的语言lambda表达式(同样被称为闭包或匿名方法)——伴随着一些语言特性支持——更重要的是API和library的增强将会使传统的Java核心libraries变的更易于使用。其中大多数的增强和补充是在Collections API中,因为Collections API在整个应用中随处可见,这篇文章大部分是在讨论它。

然而 ,很有可能大多数的Java开发者将不会熟悉隐藏在lambdas背后的概念,和在设计中体现出的lambda形式与行为 。所以,在使用它们之前,最好先弄清楚它们为什么这样设计和怎么工作。因此,我们将在之前和之后看一些方法,看它们在lambda之前和lambda之后是怎么去处理一个问题的。

注意:这篇文章是根据b92(2013/3/30)构建的Java SE8,当你读到这篇文章或Java SE8发布的时候,它的APIs、语法和语义可能已经改变了。当然,Oracle工程师所采取的APIs背后的概念,和方法,应该是非常接近现在展示的。

Algorithms,一个与集合交互更函数化的方法(自从它们初次发布以来就是Collections API的一部分),尽管它们很有用,但得到的关注很少。

自从JDK1.2时Collections API就伴随着我们,但并不是其中所有的部分都得到开发者社区的关注。Alogrithms,一个与集合交互更函数化的方法(自从它们初次发布以来就是Collections API的一部分),尽管它们很有用,但得到的关注很少。例如,Collections类提供了十几个方法,这些方法使用集合当参数,并且在collection或它的内容上执行一些操作。

考虑一下,例如,在Listing2中有十几个Listing 1中的Person对象被放到List中。

Listing 1

public class Person {
  public Person(String fn, String ln, int a) {
    this.firstName = fn; this.lastName = ln; this.age = a;
  }

  public String getFirstName() { return firstName; }
  public String getLastName() { return lastName; }
        public int getAge() { return age; }
}


Listing 2

List<Person> people = Arrays.asList(
      new Person("Ted", "Neward", 42),
      new Person("Charlotte", "Neward", 39),
      new Person("Michael", "Neward", 19),
      new Person("Matthew", "Neward", 13),
      new Person("Neal", "Ford", 45),
      new Person("Candy", "Ford", 39),
      new Person("Jeff", "Brown", 43),
      new Person("Betsy", "Brown", 39)
    );
}

现在,假设我们想要使用last name然后用age来检测或排序这个list,通常的做法是写一个for循环(换句话说,是每次需要排序时实现一个排序)。当然,这个的问题违反了DRY(the Don’t Repeat Yourself principle不要重复自己的原则)原则,并且,更糟的是for循环不具有可复用性,所以我们必须每次用到时都重新实现一次。

Collections API有一个很好的方法:Collections类中有一个sort方法可以实现List的排序,如果要使用这个方法,Person类需要实现Comparable方法(这被称做自然排序,为所有Person类型提供了一个默认排序),或者你可以传一个Comparator实例来决定Person应该怎么排序。

所以,如果你想先使用last name再使用age排序(如果last name相同时),代码将像Listing 3展示的那样。但是,这会有很多简单的像先按last name 排序然后按age排序类似的工作要做。这里新的闭包特性(closures feature)将帮助你,更简单的去写Comparator(参照Listing 4)。

Listing 3

 Collections.sort(people, new Comparator<Person>() {
      public int compare(Person lhs, Person rhs) {
        if (lhs.getLastName().equals(rhs.getLastName())) {
          return lhs.getAge() - rhs.getAge();
        }
        else
          return lhs.getLastName().compareTo(rhs.getLastName());
      }
    });

Listing 4

Collections.sort(people, (lhs, rhs) -> {
      if (lhs.getLastName().equals(rhs.getLastName()))
        return lhs.getAge() - rhs.getAge();
      else
        return lhs.getLastName().compareTo(rhs.getLastName());
    });

Comparator是语言中需要使用lambdas的一个初级的例子:这是使用一次性匿名方法的多情况中的一个例子。(记在心里,这可能是使用lamdbas的好处中最简单和最脆弱的。我们根本上已经把一种语法转换另一种,当然这样让语法更简洁。但是,即使你现在把这篇文章丢下,然后离开,大量的代码依然会以这样简洁的方法保存。)
如果我们使用这个特殊的比较一段时间,我们会把lambda当做Comparator实例,因为这是这个方法的实质。既然lamdba适合这样—”int compare(Person,Person)”,并且直接存放在Person类中,这样使lambda的实现更简单(参考Listing 5),和更具可读性(参考Listing 6)。

Listing 5

public class Person {
  // . . .

  public static final Comparator<Person> BY_LAST_AND_AGE =
    (lhs, rhs) -> {
      if (lhs.lastName.equals(rhs.lastName))
        return lhs.age - rhs.age;
      else
        return lhs.lastName.compareTo(rhs.lastName);
    };
}

Listing 6

 Collections.sort(people, Person.BY_LAST_AND_AGE);

虽然,在Person类中存储一个Comparator<Person>实例看起来很怪。更好的是用一个方法去做比较,而不是使用Comparator实例。幸运的是,Java将会允许任何满足Comparator中方法签名的方法实现类似的功能。所以,同样可以写BY_LAST_AND_AGE Comparator做一个标准实例,或者在Person中用静态方法(如Listing 7),并用它来代替使用(如Listing 8)。

Listing 7

  public static int compareLastAndAge(Person lhs, Person rhs) {
    if (lhs.lastName.equals(rhs.lastName))
      return lhs.age - rhs.age;
    else
      return lhs.lastName.compareTo(rhs.lastName);
  }

Listing 8

Collections.sort(people, Person::compareLastAndAge);

因此,既使Collections API没有什么改变,lambdas已经很有帮助了。再一次,如果你现在放下文章离开,事情也已经很好了。但是他们会变的更好。

Collections API的改变
为Collection类加了一些API,各种各样的新的和功能更强的方法和技术被使用,其中很多是从函数式编程借鉴来的。幸运的是你不需要有函数式编程的知识,你可以把函数当做有操作和重用价值的类和对象就可以。
Comparisons。以前Comparator方法的一个缺点是隐藏在Comparator实现之中。实际上代码做了两次比较,第一次是“主元素”的比较,就是lastname先比较。然后如果lastname相同的话再比较age。如果应用需要先按age排序,再次last names排序,就必须要写一个新的Comparator——compareLastAndAge没有可以复用的部分。
在这里函数式方法能发挥它的作用。如果你把这个比较当作一个完全分开的Comparator实例,我们可以把它合并起来创建我们需要的比较方法(参考:Listing 9)。

Listing 9

public static final Comparator<Person> BY_FIRST =
    (lhs, rhs) -> lhs.firstName.compareTo(rhs.firstName);
  public static final Comparator<Person> BY_LAST =
    (lhs, rhs) -> lhs.lastName.compareTo(rhs.lastName);
  public static final Comparator<Person> BY_AGE =
    (lhs, rhs) -> lhs.age – rhs.age;

从经历来看,手动实现每一个合并不是非常的高效,因为从时间上来看你手写合并的时间跟实现多级比较是一样。事实上,像这样“我要通过X中的方法比较两个X的值,并返回结果”是非常常用的,平台创造性的给了我们这样的功能。通过Comparator类,一个比较方法通过一个函数从对象中提取一个比较关键字,然后返回一个基于此关键字的Comparator。这表明Listing 9可以重写成像Listing10一样简单。

Listing 10

 public static final Comparator<Person> BY_FIRST =
    Comparators.comparing(Person::getFirstName);
  public static final Comparator<Person> BY_LAST =
    Comparators.comparing(Person::getLastName);
  public static final Comparator<Person> BY_AGE =
    Comparators.comparing(Person::getAge);

被简化:这样做会错过Java新API的一个更强大的功能,这是做一个减法——把一个集合的值通过自定义操作合并成一个值。

思考一会我们是在做什么:Person不再只是排序,它现在只需要提取出需要排序的关键字就可以。这是件好事——Person不需要考虑怎么样排序;Person只需要关注怎么样做一个Person就可以。特别是我们要比较两个或两个以上参数的情况正在变好。
Composition。基于Java 8,Comparator接口拥有几种方法,并通过以不同的方式串起来的方法,组合Comparator实例。例如,comparator.thenComparing()方法是Comparator比较完第一个参数后比较另一个参数后使用的。所以,重新创建“先比较last name,然后比较age”方法,现在可以使用两个Comparator实例(LAST和AGE),像Listing11展示的那样。或者,你更倾向使用方法而不是Comparator实例,参考Listing12。
Listing 11

 Collections.sort(people, Person.BY_LAST.
                                   .thenComparing(Person.BY_AGE));


Listing 12

Collections.sort(people,
      Comparators.comparing(Person::getLastName)
                 .thenComparing(Person::getAge));

顺便说一句,对于那些不是使用Collections.sort()长的大人,现在在List中有了一个新的sort()方法。这是接口默认方法介绍中其中一种简洁的事情:我们把这种基于非继承(noninheritance-based)的可重用行为放在static方法中,现在可以被放到接口中。(参考 previous article in this series ,可以了解更多)
同样的,如果代码想把Person集合通过先通过last name,然后first name排序,不需要新写Comparator,因为 比较可以通过两个特殊的原子比较组合实现,像Listing13展示的那样。
Listing 13

    Collections.sort(people,
      Comparators.comparing(Person::getLastName)
      .thenComparing(Person::getFirstName));

这种组合“连接”的方法,就是函数式组合(functional composition),这种在函数式编程中非常常见,并且是函数式编程功能非常强大的原因。
更重要的是需要明白,真正的好处并不仅仅是API允许我们去做比较,而是有能力去传输小块可执行代码,从而创造机会复用和设计。Comparator只是其中一个小的应用。很多事情可以做的更灵活和强大,特别是结合和组合它们的时候。
Iteration。另一个lambdas和函数式方法改变编码实现的例子,参考其中一个在集合中的基本操作:迭代元素。Java 8会通过把forEach默认方法放到Iterator和Iterable接口来改变集合。通过它来打印集合中的项目,例如,在Iterator中的forEach方法中实现lambda,像Listing 14实现的那样。

Listing 14

people.forEach((it) -> System.out.println("Person: " + it));

官方的定义,lambda类型是被当做一个Consumer实例传入的,在java.util.function包中定义的。然而,并不像传统的Java接口,Consumer是一种新的函数式接口,这意味着将不会发生直接实现——反而,要以新的思维去接受它,因为它只有一个实现,重要的方法——accept,这个方法是lambda提供的。剩下的(例如,compose和andThen)都是被定义为重要方法(important method)的功能方法,它们被设计为支持重要方法(important method)。
例如,andThen()是把两个Consumer实例连接起来,所以第一个被称为一,第二个被称为立即跟进的单独Consumer(immediately after into a single Consumer)。这提供了有用的组合技术,超出了这篇文章的范畴。

做一个收集者:它丑陋到必须要修改它。实际上如果我们使用内置的Collector接口,和它专门做mutable-reduction操作的伙伴Collectors,代码将更容易写。

很多用例都是在集合中寻找一个符合特殊条件的子项——例如,确定集合中的Person对象哪个到了可以饮酒的年龄,因为自动化代码系统需要给集合中的每个人发一瓶啤酒。这种“在一群东西中选中一个”远比操作一个集合有更广泛的用途。设想下在一个文件中操作每一行,在一个结果集中操作每一行,每一个由随机数生成器生成的值,等等。Java SE 8更进一步深化了这个概念,除了应用在集合中,并给它自己加入了自己的接口:Stream。
Stream。像JDK中的其它几个接口,Stream接口是需要运用到多种场景的基本接口,包括Collections API。它代表了一个流对象,它就类似于Iterator通过一个集合让我们一次访问一个对象。
然而,并不同于集合的是,Stream不能保证集合对象是有限的。因此,这个可以用来从文件中读字符串,或者其它请求式操作,特别是因为它被设计出来并不仅仅允许函数组合,同样的允许“在底层的”并行。
考虑到前面的需求:代码需要过滤掉任何不满21周岁的Person对象。当把一个Collection转化为Stream时(通过Collection接口中定义的stream()方法),filter方法可以只把过滤出来的对象生成一个新Stream(参考Listing 15)。

Listing 15

people
      .stream()
      .filter(it -> it.getAge() >= 21)

filter的参数是一个Predicate,一个被定义为使用一个通用参数,并返回Boolean值的接口。使用Predicate的意图是决定参数对象需不需要放到返回对象集中。
filter()的返回对象是另一个Stream,这意味着过滤出的Stream同样可以做更进一步的操作,比如,使用forEach()读Stream中的每个元素,在下面的例子中展示结果(参考Listing 16)。

Listing 16

 people.stream()
      .filter((it) -> it.getAge() >= 21)
      .forEach((it) -> 
        System.out.println("Have a beer, " + it.getFirstName()));

这个巧妙的展示了流的可组合性——我们可以使用流,并通过各种各样的原子操作去使用它,每一个操作只能做一件事。此外,filter()是延时操作——当需要它的时候它才会执行,而不是提前遍历整个Person集合(像我们之前使用Collections API做的那样)。

Predicates。第一次使用带一个Predicate参数的filter()方法可能会感到奇怪。毕竟,如果目标是查找年龄大于21岁、last name是Neward的Person对象时,filter()应该使用一对Predicate实例。当然,这如同打开了一个可能性的潘多拉盒子。假如,目标是查找所有Person对象中满足年龄大于21小于65,并且first name至少有4个字母的Person对象?无限的可能性被打开了,filter()API需要以某种方法去实现。
除非,当然一种机制可能以某种方式把所有的可能合并到一个单独的Predicate中。幸运的是,很容易看出所有的Predicate实例组合可以自己为一个单独的Predicate。换句话说,如果一个过滤器在对象使用过滤流之前,需要条件A是true和条件B是true,像这样Predicate(A and B)。我们可以通过写一个Predicate包括任意两个Predicate实例,并且返回true当A和B同时为true时。
这样 “and” Predicate完全通用并且可以事先写好——事实上它只知道两个需要被调用的Predicate实例(并且这两个没有传入参数)。
如果Predicate是写在Predicate引用里(像之前Person之前使用Comparator引用一样),它们可以用and()方法捆绑在一起使用,像Listing 17展示的那样。

Listing 17

 Predicate<Person> drinkingAge = (it) -> it.getAge() >= 21;
    Predicate<Person> brown = (it) -> it.getLastName().equals("Brown");
    people.stream()
      .filter(drinkingAge.and(brown))
      .forEach((it) ->
                System.out.println("Have a beer, " +
                                   it.getFirstName()));

正如所料,and()、or()和xor()所有都可用。可以查看Javadoc去了解所有的介绍。
map() and reduce()。其它常用Stream操作包括map(),通过使用一个函数把每个元素放到Stream中,然后从每个元素中输出结果。所以,我们可以把集合中每个Person的age包括进来,然后执行一个简单函数把每个Person的age检索出来,像Listing 18展示的那样。

Listing 18

  IntStream ages =
      people.stream()
            .mapToInt((it) -> it.getAge());

实际上,IntStream(它的同类LongStream和DoubleStream)对于这些基本类型是一个特殊的Stream<T>接口(表示它会创建该接口的定制版本)。
然后这样就在Person集合中创建出一个Stream。这同样在有些时候被称作转化操作,因为代码把Person转化或重构成int。
同样的,reduce()需要输入一系列的值,然后执行某种操作,最后把它们减为一个值。Reduction对开发人员来说是非常熟悉的操作,既然他们并没有注意到:SQL中的COUNT()操作就是其中之一(把一个行的集合减为一个数),同样还有SUM(),MAX(),和MIN()操作。对流中的每个值,都通过输入一系列的值,然后执行一些操作(例如,增加一个计数器,将值添加到正在运行的总和中,查找最高,或者低的值),最后输出一个单独的值(一个integer)。
所以,例如,你可以在除以流中元素的个数之前先得到它们的和,然后得到平均年龄。给了新的API,这是最容易使用的内置方法,像Listing 19展示的那样。

Listing 19

int sum = people.stream()
                .mapToInt(Person::getAge)
                .sum();

但是,这样做会使你错过探索Java新API的一个强大特点,这是做一个减法——通过一个特定的操作把一个聚合的值合并成一个单独的值。所以,让我们使用reduce()重写求和的部分:

.reduce(0, (l, r) -> l + r);

这个减法,同样是在功能圈(functional circles)中做为fold成名的,开始于一个种子值(在这里是0),然后为种子申请闭包(closure)和流中的第一个值,得到结果并把它当做累积值保存,然后把它当做下一次操作的种子值。
换句庆说,在一系列的integers中像1,2,3,4,5这样,先是0加上1,得到1做为累积值,然后1就在下一次操作中被当做种子值,执行(1+2)。依次类推,得到最后的值15 。像在Listing 20中展示的那样。

Listing 20

List<Integer> values = Arrays.asList(1, 2, 3, 4, 5);
    int sum = values.stream().reduce(0, (l,r) -> l+r);
    System.out.println(sum);

注意闭包把reduce的二个参数当作IntBinaryOperator ,被定义为使用两个integer得到一个int结果。IntBinaryOperator和IntBiFunction是专门功能接口的例子,其中还包括Double和Long类型的专门版本,它们都会需要两个参数,最后返回一个int。这些特殊版本的建立是用于缓解使用常见基本类型的工作。
IntStream同样有几个辅助方法,包括average()、min()和max()方法,去做一些基本integer操作。此外,二进制的操作(像两个数相加)同样经常被定义这样的基本包装类(Integer::sum,Long::max等等)。
More maps and reduction。Maps和reduction在各种各样的状况下不仅仅是被当作一个简单的数学方法使用。毕竟,它们能在任何情况下把一个对象集合转化成另一种对象,然后生成一个单独的值、map和reduction(then collected into a single value, map and reduction operations work)。
例如,map操作可以被用来取出或projection一个对象,并且提取其中的一部分。比如,从Person对象中提取出last name:

Stream lastNames = people.stream().map(Person::getLastName); 

一旦last name从Person流中取出,reduction能把strings连接到一起。例如,把last name转化为XML表示的数据。参考Listing 21。

Listing 21

String xml =
      "<people data='lastname'>" +
      people.stream()
            .map(it -> "<person>" + it.getLastName() + "</person>")
            .reduce("", String::concat)
      + "</people>";
    System.out.println(xml);

自然,如果需要不同的XML格式,不同格式的内容用不同的操作,要么使用特别提供的操作,像Listing 21,或者使用其它类定义的方法,例如,像Listing 22中展示的Person类。要么像Listing 23展示的,使用map()的一部分操作把Person对象流转化为JSON串。

Listing 22

public class Person {
  // . . .
  public static String toJSON(Person p) {
    return
      "{" +
        "firstName: /"" + p.firstName + "/", " +
        "lastName: /"" + p.lastName + "/", " +
        "age: " + p.age + " " +
      "}";
  }
}


Listing 23

String json =
      people.stream()
        .map(Person::toJSON)
        .reduce("[", (l, r) -> l + (l.equals("[") ? "" : ",") + r)
        + "]";
    System.out.println(json);

准备:Java SE 8的发布日期很快就到了,伴随着它来的不仅仅是lambda语法表达式(同样被称为闭包和匿名方法)——伴随着一些语法特性支持——更重要的是API和library的增强将会使传统的Java核心libraries变的更易于使用。

在reduce操作中间的三目操作是避免在把Person转化为JSON时在第一个Person前面加上逗号。有一些JSON解析器能识别,但不规范,并且看起来很丑。

实事上它丑到必须要去修改它。代码实际上可以用Collector接口内置方法和Collectors能把它变的更简单,特别是做这种mutable-reduction操作(参考Listing 24)。这个比我们之前用的reduce和String::concat运行的更快,所以它是一个更好的选择。

Listing 24

  String joined = people.stream()
                          .map(Person::toJSON)
                          .collect(Collectors.joining(", "));System.out.println("[" + joined + "]");

哦,不要忘了我们的老朋友Comparator,注意Stream同样有排序stream的操作,所以排序Person的JSON串例子可以像Listing 25展示的那样写。

Listing 25

String json = people.stream()
                        .sorted(Person.BY_LAST)
                        .collect(Collectors.joining(", " "[", "]"));
    System.out.println(json);

这是个很强大的东西。

Parallelization。更强大的是这些操作在逻辑上是完全独立的,需要将每个对象通过stream操作每一个。这意味着传统的for循环将会被弃用,当试图把集合分成几段使用iterate、map、或者reduce操作一个大的集合时,每段可以用单独的线程处理。

了解更多

Java8:Lambdas(二)学习怎样去使用lambda表达式Lambda表示式

 

然而,Stream API已经覆盖了,与前面使用过的XML或者JSON map()和reduce()操作有区别的操作——parallelStream(),而不是调用stream()从集合中获得一个流。像Listing 26中展示的那样。

Listing 26

  people.parallelStream()
      .filter((it) -> it.getAge() >= 21)
      .forEach((it) ->
                System.out.println("Have a beer " + it.getFirstName() +
                  Thread.currentThread()));

至少在我的笔记本电脑上一个包括十二个子项目的集合,两个线程用于处理集合:Java中调用main()的主线程,和另一个不是我们创建的线程ForkJoinPool.commonPool worker-1

很显然,对于一个包括十二子项目的集合,这是没有必要的。但是对于有几百或更多个的情况,运行的“足够好”跟“需要加快”就有区别了。如果没有这些新的方法,你就需要关注这些重要代码和学习算法。使用它们,你可以通过为之前的顺序处理增加八个键(如果Shift键需要使用流就是9个)去写并行代码。(译者:不明白这句话是什么意思英文原文为:With them, you can write parallelized code literally by adding eight keystrokes (nine if you count the Shift key required to capitalize the s in stream) to the previously sequential processing.)

并且在必要的时候,一个并行的流可以退回到之前的顺序流,通过调用sequential()

重要的是,不管并行或顺序谁更好用,它们都可以运用在同一个流接口上。我们更关注于业务需求,只有当需要的时候才实现,这样顺序或并行的实现就成为了一个实现细节。我们并不想关注线程池中启动和同步线程的低层实现细节。

总结

Lambdas能给Java带来很多改变,包括怎么样写和设计Java代码。有一些改变已经在Java SE 包中,并且它们将会改变其它的包(包括Java平台的包和其它的开源包),开发者使用lambdas会越来越方便。

当Java SE 8发布的时候将会出现更多的改变。如果你能理解lambdas在集合中做工作,你将会在自己设计和编码时使用lambdas更加顺手。并且,在今后几年你会创造更好的解耦代码。

原创文章,作者:ItWorker,如若转载,请注明出处:https://blog.ytso.com/tech/pnotes/140506.html

(0)
上一篇 2021年9月5日 14:13
下一篇 2021年9月5日 14:13

相关推荐

发表回复

登录后才能评论