lambda表达式与函数式编程

lambda表达式

前言

在学习lambda表达式的过程中出现了以下问题:

  • java8为什么把lambda表达式设计成单接口的形式?
  • lambda和匿名类型的关系是什么?
  • java8如何对lambda进行类型推导?
  • java8为什么引入默认方法?

基于这些问题,对lambda进行深入的了解。

背景

Java是一门面向对象编程语言。面向对象编程语言和函数式编程语言中的基本元素都可以动态封装程序行为:面向对象编程语言使用带有方法的对象封装行为,函数式编程语言使用函数封装行为。但这个相同点并不明显,因为Java的对象往往比较“重量级”:实例化一个类型往往会涉及不同的类,并需要初始化类里的字段和方法。
过有些Java对象只是对单个函数的封装。例如下面这个典型用例:Java API中定义了一个接口(一般被称为回调接口),用户通过提供这个接口的实例来传入指定行为,例如:

public interface ActionListener {
  void actionPerformed(ActionEvent e);
}

这里并不需要专门定义一个类来实现 ActionListener,因为它只会在调用处被使用一次。用户一般会使用匿名类型把行为内联:

button.addActionListener(new ActionListener() {
  public void actionPerformed(ActionEvent e) {
    ui.dazzle(e.getModifiers());
  }
});

很多库都依赖于上面的模式。对于并行 API 更是如此,因为我们需要把待执行的代码提供给并行 API,并行编程是一个非常值得研究的领域,因为在这里摩尔定律得到了重生:尽管我们没有更快的 CPU 核心,但是我们有更多的 CPU 核心。而串行 API 就只能使用有限的计算能力。
随着回调模式和函数式编程风格的日益流行,我们需要在Java中提供一种尽可能轻量级的将代码封装为数据(Model code as data)的方法。匿名内部类并不是一个好的选择,因为:

  1. 语法过于冗余
  2. 匿名类中的this和变量名容易使人产生误解
  3. 类型载入和实例创建语义不够灵活
  4. 无法捕获非final的局部变量(局部变量必须定义为final,否则会报错)
  5. 无法对控制流进行抽象

上面的多数问题均在Java SE 8中得以解决:

  • 通过提供更简洁的语法和局部作用域规则,Java SE 8彻底解决了问题1和问题2
  • 通过提供更加灵活而且便于优化的表达式语义,Java SE 8绕开了问题3
  • 通过允许编译器推断变量的“常量性”(finality),Java SE 8减轻了问题4带来的困扰

不过,Java SE 8的目标并非解决所有上述问题。因此捕获可变变量(问题4)和非局部控制流(问题5)并不在Java SE 8的范畴之内。

函数式接口

尽管匿名内部类有着种种限制和问题,但是它有一个良好的特性,它和Java类型系统结合的十分紧密:每一个函数对象都对应一个接口类型。之所以说这个特性是良好的,是因为:

  • 接口是 Java 类型系统的一部分
  • 接口天然就拥有其运行时表示
  • 接口可以通过 Javadoc 注释来表达一些非正式的协定,例如,通过注释说明该操作应可交换

上面提到的 ActionListener 接口只有一个方法,大多数回调接口都拥有这个特征:比如 Runnable 接口和 Comparator 接口。我们把这些只拥有一个方法的接口称为 函数式接口。(之前它们被称为 SAM类型,即 单抽象方法类型)

我们并不需要额外的工作来声明一个接口是函数式接口:编译器会根据接口的结构自行判断(判断过程并非简单的对接口方法计数:一个接口可能冗余的定义了一个 Object 已经提供的方法,比如 toString(),或者定义了静态方法或默认方法,这些都不属于函数式接口方法的范畴)。不过API作者们可以通过 @FunctionalInterface 注解来显式指定一个接口是函数式接口(以避免无意声明了一个符合函数式标准的接口),加上这个注解之后,编译器就会验证该接口是否满足函数式接口的要求。

实现函数式类型的另一种方式是引入一个全新的结构化函数类型,我们也称其为“箭头”类型。例如,一个接收 String 和 Object 并返回 int 的函数类型可以被表示为 (String, Object) -> int。但出于下面的原因,最终将其否定:

  • 它会为Java类型系统引入额外的复杂度,并带来结构类型和 指名类型的混用。(Java 几乎全部使用指名类型)
  • 它会导致类库风格的分歧——一些类库会继续使用回调接口,而另一些类库会使用结构化函数类型
  • 它的语法会变得十分笨拙,尤其在包含受检异常之后
    每个函数类型很难拥有其运行时表示,这意味着开发者会受到 类型擦除的困扰和局限。比如说,我们无法对方法 m(T->U) 和 m(X->Y) 进行重载

所以我们选择了“使用已知类型”这条路——因为现有的类库大量使用了函数式接口,通过沿用这种模式,我们使得现有类库能够直接使用 lambda 表达式。例如下面是 Java SE 7 中已经存在的函数式接口:

java.lang.Runnable
java.util.concurrent.Callable
java.security.PrivilegedAction
java.util.Comparator
java.io.FileFilter
java.beans.PropertyChangeListener
除此之外,Java SE 8中增加了一个新的包:java.util.function,它里面包含了常用的函数式接口,例如:

Predicate——接收 T 并返回 boolean
Consumer——接收 T,不返回值
Function<T, R>——接收 T,返回 R
Supplier——提供 T 对象(例如工厂),不接收值
UnaryOperator——接收 T 对象,返回 T
BinaryOperator——接收两个 T,返回 T
除了上面的这些基本的函数式接口,我们还提供了一些针对原始类型的特化函数式接口,例如 IntSupplier 和 LongBinaryOperator。(我们只为 int、long 和 double 提供了特化函数式接口,如果需要使用其它原始类型则需要进行类型转换)同样的我们也提供了一些针对多个参数的函数式接口,例如 BiFunction<T, U, R>,它接收 T 对象和 U 对象,返回 R 对象。
Consumer:它包含有一个有输入而无输出的accept接口方法,除accept方法,它还包含有andThen这个方法;

default Consumer<T> andThen(Consumer<? super T> after) {
    Objects.requireNonNull(after);
    return (T t) -> { accept(t); after.accept(t); };
}

这个方法就是指定在调用当前Consumer后是否还要调用其它的Consumer;

public static void consumerTest() {
    Consumer f = System.out::println;
    Consumer f2 = n -> System.out.println(n + "-F2");

    //执行完F后再执行F2的Accept方法
    f.andThen(f2).accept("test");

    //连续执行F的Accept方法
    f.andThen(f).andThen(f).andThen(f).accept("test1");
}

Function
它代表的含义是“函数”,而函数经常是有输入输出的,因此它含有一个apply方法,包含一个输入与一个输出…
除apply方法外,它还有compose与andThen及indentity三个方法,其使用见下述示例;

/**
 * Function测试
 */
public static void functionTest() {
    Function<Integer, Integer> f = s -> s++;
    Function<Integer, Integer> g = s -> s * 2;

    /**
     * 下面表示在执行F时,先执行G,并且执行F时使用G的输出当作输入。
     * 相当于以下代码:
     * Integer a = g.apply(1);
     * System.out.println(f.apply(a));
     */
    System.out.println(f.compose(g).apply(1));

    /**
     * 表示执行F的Apply后使用其返回的值当作输入再执行G的Apply;
     * 相当于以下代码
     * Integer a = f.apply(1);
     * System.out.println(g.apply(a));
     */
    System.out.println(f.andThen(g).apply(1));

    /**
     * identity方法会返回一个不进行任何处理的Function,即输出与输入值相等; 
     */
    System.out.println(Function.identity().apply("a"));
}

Predicate:判断某个东西是否满足某种条件

/**
 * Predicate测试
 */
private static void predicateTest() {
    Predicate<String> p = o -> o.equals("test");
    Predicate<String> g = o -> o.startsWith("t");

    /**
     * negate: 用于对原来的Predicate做取反处理;
     * 如当调用p.test("test")为True时,调用p.negate().test("test")就会是False;
     */
    Assert.assertFalse(p.negate().test("test"));

    /**
     * and: 针对同一输入值,多个Predicate均返回True时返回True,否则返回False;
     */
    Assert.assertTrue(p.and(g).test("test"));

    /**
     * or: 针对同一输入值,多个Predicate只要有一个返回True则返回True,否则返回False
     */
    Assert.assertTrue(p.or(g).test("ta"));
}

lambda表达式

1.lambda表达式的一般语法
以下是lambda表达式的重要特征:

可选类型声明:不需要声明参数类型,编译器可以统一识别参数值。
可选的参数圆括号:一个参数无需定义圆括号,但多个参数需要定义圆括号。
可选的大括号:如果主体包含了一个语句,就不需要使用大括号。
可选的返回关键字:如果主体只有一个表达式返回值则编译器会自动返回值,大括号需要指定明表达式返回了一个数值。

(Type1 param1, Type2 param2, ..., TypeN paramN) -> {
  statment1;
  statment2;
  //.............
  return statmentM;
}

这是lambda表达式的完全式语法,后面几种语法是对它的简化。

2.单参数语法

param1 -> {
  statment1;
  statment2;
  //.............
  return statmentM;
}

当lambda表达式的参数个数只有一个,可以省略小括号

例如:将列表中的字符串转换为全小写

List<String> proNames = Arrays.asList(new String[]{"Ni","Hao","Lambda"});
List<String> lowercaseNames1 = proNames.stream().map(name -> {return name.toLowerCase();}).collect(Collectors.toList());

3.单语句写法

param1 -> statment

当lambda表达式只包含一条语句时,可以省略大括号、return和语句结尾的分号

例如:将列表中的字符串转换为全小写

List<String> proNames = Arrays.asList(new String[]{"Ni","Hao","Lambda"});

List<String> lowercaseNames2 = proNames.stream().map(name -> name.toLowerCase()).collect(Collectors.toList());

4.方法引用写法

Class or instance :: method

例如:将列表中的字符串转换为全小写

List<String> proNames = Arrays.asList(new String[]{"Ni","Hao","Lambda"});

List<String> lowercaseNames3 = proNames.stream().map(String::toLowerCase).collect(Collectors.toList());

目标类型

需要注意的是,函数式接口的名称并不是lambda表达式的一部分。那么问题来了,对于给定的lambda表达式,它的类型是什么?答案是:它的类型是由其上下文推导而来。例如,下面代码中的lambda表达式类型是ActionListener:

ActionListener l = (ActionEvent e) -> ui.dazzle(e.getModifiers());

这就意味着同样的lambda表达式在不同上下文里可以拥有不同的类型:

Callable<String> c = () -> "done";

PrivilegedAction<String> a = () -> "done";

第一个lambda表达式() -> "done"是Callable的实例,而第二个lambda表达式则是PrivilegedAction的实例。

编译器负责推导lambda表达式的类型。它利用lambda表达式所在上下文所期待的类型进行推导,这个被期待的类型被称为目标类型。lambda表达式只能出现在目标类型为函数式接口的上下文中。

当然,lambda表达式对目标类型也是有要求的。编译器会检查lambda表达式的类型和目标类型的方法签名是否一致。当且仅当下面所有条件均满足时,lambda表达式才可以被赋给目标类型T:

  • T是一个函数式接口
  • lambda表达式的参数和T的方法参数在数量和类型上一一对应
  • lambda表达式的返回值和T的方法返回值相兼容
  • lambda表达式内所抛出的异常和T的方法throws类型相兼容

由于目标类型(函数式接口)已经“知道”lambda表达式的形式参数类型,所以我们没有必要把已知类型再重复一遍。也就是说,lambda表达式的参数类型可以从目标类型中得出:

Comparator<String> c = (s1, s2) -> s1.compareToIgnoreCase(s2);

在上面的例子里,编译器可以推导出s1和s2的类型是String。此外,当lambda的参数只有一个而且它的类型可以被推导得知时,该参数列表外面的括号可以被省略:

FileFilter java = f -> f.getName().endsWith(".java");

button.addActionListener(e -> ui.dazzle(e.getModifiers()));

这些改进进一步展示了我们的设计目标:“不要把高度问题转化成宽度问题。”我们希望语法元素能够尽可能的少,以便代码的读者能够直达lambda表达式的核心部分。

lambda表达式并不是第一个拥有上下文相关类型的Java表达式:泛型方法调用和“菱形”构造器调用也通过目标类型来进行类型推导:

List<String> ls = Collections.emptyList();
List<Integer> li = Collections.emptyList();

Map<String, Integer> m1 = new HashMap<>();
Map<Integer, String> m2 = new HashMap<>();

目标类型的上下文

之前我们提到lambda表达式只能出现在拥有目标类型的上下文中。下面给出了这些带有目标类型的上下文:

  • 变量声明
  • 赋值
  • 返回语句
  • 数组初始化器
  • 方法和构造方法的参数
  • lambda表达式函数体
  • 条件表达式(? :)
  • 转型(Cast)表达式
    在前三个上下文(变量声明、赋值和返回语句)里,目标类型即是被赋值或被返回的类型:
Comparator<String> c;
c = (String s1, String s2) -> s1.compareToIgnoreCase(s2);

public Runnable toDoLater() {
  return () -> {
    System.out.println("later");
  }
}

数组初始化器和赋值类似,只是这里的“变量”变成了数组元素,而类型是从数组类型中推导得知:\

filterFiles(new FileFilter[] {
              f -> f.exists(), f -> f.canRead(), f -> f.getName().startsWith("q")
            });

方法参数的类型推导要相对复杂些:目标类型的确认会涉及到其它两个语言特性:重载解析和参数类型推导。

重载解析会为一个给定的方法调用寻找最合适的方法声明。由于不同的声明具有不同的签名,当lambda表达式作为方法参数时,重载解析就会影响到lambda表达式的目标类型。编译器会通过它所得之的信息来做出决定。如果lambda表达式具有显式类型(参数类型被显式指定),编译器就可以直接 使用lambda表达式的返回类型;如果lambda表达式具有隐式类型(参数类型被推导而知),重载解析则会忽略lambda表达式函数体而只依赖lambda表达式参数的数量。

如果在解析方法声明时存在二义性,我们就需要利用转型(cast)或显式lambda表达式来提供更多的类型信息。如果lambda表达式的返回类型依赖于其参数的类型,那么lambda表达式函数体有可能可以给编译器提供额外的信息,以便其推导参数类型。

List<Person> ps = ...
Stream<String> names = ps.stream().map(p -> p.getName());

在上面的代码中,ps的类型是List,所以ps.stream()的返回类型是Stream。map()方法接收一个类型为Function<T, R>的函数式接口,这里T的类型即是Stream元素的类型,也就是Person,而R的类型未知。由于在重载解析之后lambda表达式的目标类型仍然未知,我们就需要推导R的类型:通过对lambda表达式函数体进行类型检查,我们发现函数体返回String,因此R的类型是String,因而map()返回Stream。绝大多数情况下编译器都能解析出正确的类型,但如果碰到无法解析的情况,我们则需要:

  • 使用显式lambda表达式(为参数p提供显式类型)以提供额外的类型信息
  • 把lambda表达式转型为Function<Person, String>
  • 为泛型参数R提供一个实际类型。(.map(p -> p.getName()))

lambda表达式本身也可以为它自己的函数体提供目标类型,也就是说lambda表达式可以通过外部目标类型推导出其内部的返回类型,这意味着我们可以方便的编写一个返回函数的函数:

Supplier<Runnable> c = () -> () -> { System.out.println("hi"); };

类似的,条件表达式可以把目标类型“分发”给其子表达式:

Callable<Integer> c = flag ? (() -> 23) : (() -> 42);

最后,转型表达式(Cast expression)可以显式提供lambda表达式的类型,这个特性在无法确认目标类型时非常有用:

// Object o = () -> { System.out.println("hi"); }; 这段代码是非法的
Object o = (Runnable) () -> { System.out.println("hi"); };

除此之外,当重载的方法都拥有函数式接口时,转型可以帮助解决重载解析时出现的二义性。

目标类型这个概念不仅仅适用于lambda表达式,泛型方法调用和“菱形”构造方法调用也可以从目标类型中受益,下面的代码在Java SE 7是非法的,但在Java SE 8中是合法的:

List<String> ls = Collections.checkedList(new ArrayList<>(), String.class);

Set<Integer> si = flag ? Collections.singleton(23) : Collections.emptySet();

词法作用域

在内部类中使用变量名(以及this)非常容易出错。内部类中通过继承得到的成员(包括来自Object的方法)可能会把外部类的成员掩盖,此外未限定的this引用会指向内部类自己而非外部类。

在lambda中,this不是指向lambda表达式产生的那个SAM对象,而是声明它的外部对象。

例如:

public class WhatThis {

     public void whatThis(){
           //转全小写
           List<String> proStrs = Arrays.asList(new String[]{"Ni","Hao","Lambda"});
           List<String> execStrs = proStrs.stream().map(str -> {
                 System.out.println(this.getClass().getName());
                 return str.toLowerCase();
           }).collect(Collectors.toList());
           execStrs.forEach(System.out::println);
     }

     public static void main(String[] args) {
           WhatThis wt = new WhatThis();
           wt.whatThis();
     }
}

输出:

test.WhatThis
test.WhatThis
test.WhatThis
ni
hao
lambda

变量捕获

在Java SE 7中,编译器对内部类中引用的外部变量(即捕获的变量)要求非常严格:如果捕获的变量没有被声明为final就会产生一个编译错误。我们现在放宽了这个限制——对于lambda表达式和内部类,我们允许在其中捕获那些符合有效只读的局部变量。

简单的说,如果一个局部变量在初始化后从未被修改过,那么它就符合有效只读的要求,换句话说,加上final后也不会导致编译错误的局部变量就是有效只读变量。

Callable<String> helloCallable(String name) {
  String hello = "Hello";
  return () -> (hello + ", " + name);
}

对this的引用,以及通过this对未限定字段的引用和未限定方法的调用在本质上都属于使用final局部变量。包含此类引用的lambda表达式相当于捕获了this实例。在其它情况下,lambda对象不会保留任何对this的引用。

这个特性对内存管理是一件好事:内部类实例会一直保留一个对其外部类实例的强引用,而那些没有捕获外部类成员的lambda表达式则不会保留对外部类实例的引用。要知道内部类的这个特性往往会造成内存泄露。

尽管我们放宽了对捕获变量的语法限制,但试图修改捕获变量的行为仍然会被禁止,比如下面这个例子就是非法的:

int sum = 0;
list.forEach(e -> { sum += e.size(); });

为什么要禁止这种行为呢?因为这样的lambda表达式很容易引起race condition。除非我们能够强制这样的函数不能离开其当前线程(最好是在编译时),但如果这么做了可能会导致更多的问题。简而言之,lambda表达式对值封闭,对变量开放。

方法引用

lambda表达式允许我们定义一个匿名方法,并允许我们以函数式接口的方式使用它。我们也希望能够在已有的方法上实现同样的特性。

方法引用和lambda表达式拥有相同的特性(例如,它们都需要一个目标类型,并需要被转化为函数式接口的实例),不过我们并不需要为方法引用提供方法体,我们可以直接通过方法名称引用已有方法。

以下面的代码为例,假设我们要按照name或age为Person数组进行排序:

class Person {
  private final String name;
  private final int age;

  public int getAge() { return age; }
  public String getName() {return name; }
  ...
}

Person[] people = ...
Comparator<Person> byName = Comparator.comparing(p -> p.getName());
Arrays.sort(people, byName);

在这里我们可以用方法引用代替lambda表达式:

Comparator<Person> byName = Comparator.comparing(Person::getName);

这里的Person::getName可以被看作为lambda表达式的简写形式。尽管方法引用不一定(比如在这个例子里)会把语法变的更紧凑,但它拥有更明确的语义——如果我们想要调用的方法拥有一个名字,我们就可以通过它的名字直接调用它。

因为函数式接口的方法参数对应于隐式方法调用时的参数,所以被引用方法签名可以通过放宽类型,装箱以及组织到参数数组中的方式对其参数进行操作,就像在调用实际方法一样:`

Consumer<Integer> b1 = System::exit;    // void exit(int status)
Consumer<String[]> b2 = Arrays:sort;    // void sort(Object[] a)
Consumer<String> b3 = MyProgram::main;  // void main(String... args)
Runnable r = Myprogram::mapToInt        // void main(String... args)
objectName::instanceMethod

ClassName::staticMethod

ClassName::instanceMethod

前两种方式类似,等同于把lambda表达式的参数直接当成instanceMethod|staticMethod的参数来调用。比如System.out::println等同于x->System.out.println(x);Math::max等同于(x, y)->Math.max(x,y)。

最后一种方式,等同于把lambda表达式的第一个参数当成instanceMethod的目标对象,其他剩余参数当成该方法的参数。比如String::toLowerCase等同于x->x.toLowerCase()。

可以这么理解,前两种是将传入对象当参数执行方法,后一种是调用传入对象的方法。

默认方法和静态接口方法

lambda表达式和方法引用大大提升了Java的表达能力,不过为了使把代码即数据变的更加容易,我们需要把这些特性融入到已有的库之中,以便开发者使用。

Java SE 7时代为一个已有的类库增加功能是非常困难的。具体的说,接口在发布之后就已经被定型,除非我们能够一次性更新所有该接口的实现,否则向接口添加方法就会破坏现有的接口实现。默认方法(之前被称为虚拟扩展方法或守护方法)的目标即是解决这个问题,使得接口在发布之后仍能被逐步演化。

这里给出一个例子,我们需要在标准集合API中增加针对lambda的方法。例如removeAll方法应该被泛化为接收一个函数式接口Predicate,但这个新的方法应该被放在哪里呢?我们无法直接在Collection接口上新增方法——不然就会破坏现有的Collection实现。我们倒是可以在Collections工具类中增加对应的静态方法,但这样就会把这个方法置于“二等公民”的境地。

默认方法利用面向对象的方式向接口增加新的行为。它是一种新的方法:接口方法可以是抽象的或是默认的。默认方法拥有其默认实现,实现接口的类型通过继承得到该默认实现(如果类型没有覆盖该默认实现)。此外,默认方法不是抽象方法,所以我们可以放心的向函数式接口里增加默认方法,而不用担心函数式接口的单抽象方法限制。

下面的例子展示了如何向Iterator接口增加默认方法skip:

interface Iterator<E> {
  boolean hasNext();
  E next();
  void remove();

  default void skip(int i) {
    for ( ; i > 0 && hasNext(); i -= 1) next();
  }
}

根据上面的Iterator定义,所有实现Iterator的类型都会自动继承skip方法。在使用者的眼里,skip不过是接口新增的一个虚拟方法。在没有覆盖skip方法的Iterator子类实例上调用skip会执行skip的默认实现:调用hasNext和next若干次。子类可以通过覆盖skip来提供更好的实现——比如直接移动游标,或是提供为操作提供原子性等。

当接口继承其它接口时,我们既可以为它所继承而来的抽象方法提供一个默认实现,也可以为它继承而来的默认方法提供一个新的实现,还可以把它继承而来的默认方法重新抽象化。

除了默认方法,Java SE 8还在允许在接口中定义静态方法。这使得我们可以从接口直接调用和它相关的辅助方法,而不是从其它的类中调用(之前这样的类往往以对应接口的复数命名,例如Collections)。比如,我们一般需要使用静态辅助方法生成实现Comparator的比较器,在Java SE 8中我们可以直接把该静态方法定义在Comparator接口中:

public static <T, U extends Comparable<? super U>>
    Comparator<T> comparing(Function<T, U> keyExtractor) {
  return (c1, c2) -> keyExtractor.apply(c1).compareTo(keyExtractor.apply(c2));
}

继承默认方法

和其它方法一样,默认方法也可以被继承,大多数情况下这种继承行为和我们所期待的一致。不过,当类型或者接口的超类拥有多个具有相同签名的方法时,我们就需要一套规则来解决这个冲突:

  • 类的方法声明优先于接口默认方法。无论该方法是具体的还是抽象的。
  • 被其它类型所覆盖的方法会被忽略。这条规则适用于超类型共享一个公共祖先的情况。
    为了演示第二条规则,我们假设Collection和List接口均提供了removeAll的默认实现,然后Queue继承并覆盖了Collection中的默认方法。在下面的implement从句中,List中的方法声明会优先于Queue中的方法声明:
class LinkedList<E> implements List<E>, Queue<E> { ... }

当两个独立的默认方法相冲突或是默认方法和抽象方法相冲突时会产生编译错误。这时程序员需要显式覆盖超类方法。一般来说我们会定义一个默认方法,然后在其中显式选择超类方法:

interface Robot implements Artist, Gun {
default void draw() { Artist.super.draw(); }
}

super前面的类型必须是有定义或继承默认方法的类型。这种方法调用并不只限于消除命名冲突——我们也可以在其它场景中使用它。

最后,接口在extends从句中的声明顺序和它们被实现的顺序无关。

实例

String[] players = {"Rafael Nadal", "Novak Djokovic",   
    "Stanislas Wawrinka", "David Ferrer",  
    "Roger Federer", "Andy Murray",  
    "Tomas Berdych", "Juan Martin Del Potro",  
    "Richard Gasquet", "John Isner"};  
   
// 1.1 使用匿名内部类根据 name 排序 players  
Arrays.sort(players, new Comparator<String>() {  
    @Override  
    public int compare(String s1, String s2) {  
        return (s1.compareTo(s2));  
    }  
}); 


// 1.2 使用 lambda expression 排序 players  
Comparator<String> sortByName = (String s1, String s2) -> (s1.compareTo(s2));  
Arrays.sort(players, sortByName);  
  
// 1.3 也可以采用如下形式:  
Arrays.sort(players, (String s1, String s2) -> (s1.compareTo(s2)));

通过对比,可以看出在逻辑不是很复杂的情况下,lambda的书写更加简单优雅。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值