2018-10-23

java学习笔记(四)

简单地讲讲Lambda表达式

Lambda管中窥豹

在前一篇文章中,已经看见了Lambda表达式的效果。那什么是Lambda表达式呢?Lambda基于数学中的λ演算得名,在 Java 中你可以把Lambda表达式理解为一种简洁地表示可传递的匿名函数的方式。它没有名称,但它有参数列表,有函数主体,又返回值,可能还可以抛出一个异常列表。

在Java8 之前的Java 传递代码十分繁琐和冗长,Java8 之后可以使用Lambda 就可以解决这个问题,不必要在为匿名类写一堆笨重的代码。使用Lambda 的结果是你的代码变得清晰、灵活。我们可以看看两个Apple比较重量的例子:

Java8 之前

Conparator byWeith = new Comparator<>() {
    public int compare(Apple a1, Apple a2) {
        return a1.getWeith().compareTo(a2.getWeith());
    }
}

使用Lambda之后

Conparator byWeith = (Apple a1, Apple a2) -> a1.getWeith().compareTo(a2.getWeith());

不得不承认,代码看起来更清晰了。要是你觉得Lambda表达式看起来似懂非懂也没关系,我们慢慢的来了解它。

现在,我们来看看几个Java8中有效的Lambda表达式加深对Lambda表达式的理解:

// 这个表达式具有一个String类型的参数并返回一个int,Lambda并没有return语句,因为已经隐含了return。
(String s) -> s.length() 
// 这个表达式有一个Apple类型的参数并返回一个boolean(苹果重来是否大于150克)
(Apple a) -> a.getWeight() > 150
// 这个表达式具有两个int类型二的参数并且没有返回值。注意Lambda表达式可以包含多行代码。
(int x, int y) -> {
    System.out.println("Result:");
    System.out.println(x + y);
}
// 这个表达式没有参数类型,返回一个int。
() -> 88
// 显式的指定为Apple类型,并对重量进行比较返回int
(Apple a2, Apple a2) -> a1.getWeight.compareTo(a2.getWeight())

Java语言设计者选选择了这样的语法,是因为C#和Scala等语言中的类似功能广受欢迎。Lambda的基本语法是:

(parameters) -> expression

或者(请注意语句花括号):

(parameters) -> {statements;}

是的,Lambda表达式的语法看起来就是那么简单。

在哪里以及如何使用Lambda

现在你可能在想哪里适合使用 Lambda ,以前是在 filter 方法中使用 Lambda :

List greenApples = filter(inventory, (Apple a) -> "green".equals(a.getColor()));

在 filter 方法中有一个 Predicate 函数式接口,我们才能把 Lambda 表达式作为参数传入方法中。下面就简单介绍一下函数式接口。

函数式接口

参数化的 filter 方法需要一个函数接口,而 Predicate 接口就是一个函数式接口。满足函数接口的条件其实很简单,只要接口中有且只有一个抽象方法(可以有其他的默认方法)即可。而 Predicate 就满足这个条件。Java API 中提供了很多其他函数接口:

public interface Comparator {
    int compare(T o1, T o2);
}

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

public interface Collable {
    V call();
}

public interface PrivilegedAction {
    V run();
}

这只是一小部分。Lambda 表达式允许直接以内敛的形式为函数是接口的抽象方法提供实现,并把整个表达式作为函数是接口的实例。这就是 Lambda 表达式与函数式接口的关系。

函数描述符

函数式接口的抽象方法的签名基本上就是 Lambda 表达式的签名。这种抽象方法叫做函数描述符。例如:Comparator 接口可以看作一个接受两个参数返回一个整数的函数签名,因为它有一个 compare 方法,这个方法接受两个参数返回一个整数。为了简单会使用形如 () -> void 的表示接受为空,返回为空的 Lambda 和函数式接口的签名。

把 Lambda 付诸实践:环绕执行

看一个简单的例子,在资源处理时一个常见的模式就是打开一个资源,做一些处理,然后关闭资源。设置和清理阶段总是类似的,并且会围绕执行处理的那些重要代码。这就是环绕执行模式,在下面的 Java7 代码中其实已经有了很大简化。

public String processFile() throws IOExceptoin {
    try (BufferedReader br = new BufferedReader(new FileReader("data.txt"))) {
        return br.readLine();
    }
}

现在代码还是有局限的,只能读取一行。如果改变需求,如返回两行,或返回频繁词汇等等。就需要改变行为了。是不是很熟悉,没错就是行为参数化。

第一步:记得行为参数化

传递行为是 Lambda 的拿手好戏。你需要把 processFile 的行为参数化,然后需要一种方法把行为传递给 processFile 。下面是新的 processFile 方法:

String result = processFile((BufferedReader br) -> br.readLine();)

第二步:使用函数式接口传递行为

Lambda 仅可用于上下文是函数式接口的情况,你需要一个能匹配 BufferedReader -> String ,还能抛出 IOException 异常的接口。

@FunctionalInterface
public interface BufferedReaderProcess {
    String process(BufferedReader br) throws IOException;
}

这样你就可以把这个接口作为 processFile 方法的参数了:

public String processFile(BufferedReaderProcess p) throws IOException{
    ...
}

第三步:执行一个行为

现在我们需要在 processFile 方法里执行 Lambda 所代表的代码。Lambda 表达式允许直接内联,为函数是接口的抽象方法提供实现,并将整个表达式作为一个接口的实例。因此,我们能在 processFile 方法里直接调用 process 方法执行处理:

public String processFile(BufferedReaderProcess p) throws IOException{
    try (BufferedReader br = new BufferedReader(new FileReader("data.txt"))) {
        return br.process(br);
    }
}

第四步:传递 Lambda

现在我们可以通过传递 Lambda 重用 processFile 方法,并以不同的方式处理文件。

处理一行:

String oneLine = processFile((BifferedReader br) -> br.readLine());

处理两行:

String oneLine = processFile((BifferedReader br) -> br.readLine() + br.readLine());

简单的展示了如何利用函数式接口来传递 Lambda ,但是我们还需要自己定义接口。下面我们来了解一下 Java8 新加入的接口。

使用函数式接口

函数是接口定义却只定义了一个抽象方法。抽象方法的签名可以描述 Lambda 表达式的签名,也称为函数描述符。为了应用不同的 Lambda 表达式,我们需要一套能够描述常见函数描述符的函数式接口。Java API 已经有了几个函数式接口,他们在 java.util.function 包中,下面简单介绍几个常见的接口。

Predicate 接口

@FunctionInterface
public interface Predicate {
    boolean test(T t);
}

这个接口有一个 test 的抽象方法,它接受一个泛型 T 对象,返回一个 boolean 。其实这个和前面创建的是一样的,现在可以直接使用这个接口。

使用 Predicate

public List filter(List list, Predicate p) {
    List result = new ArrayList<>();
    for (T t : list) {
        if (p.test(t)) {
            result.add(t);
        }
    }
    return result;
}


List nonEmpty = filter(listOfString, (String s) -> !s.isEmpty())

Consumer 接口

@FunctionInterface
public interface Consumer {
    void accept(T t);
}

这个接口有一个 accept 的抽象方法,它接受一个泛型 T 对象,没有返回(void)。如果你需要访问类型 T 的对象,并对其执行某些操作,就可以使用这个接口。比如,在 forEach 方法中使用它执行某些操作:

public void forEach(List list, Consumer c) {
    for (T t : list) {
        c.accept(t);
    }
}


forEach(Array.asList(1, 2, 3, 4, 5), (Integer i) -> System.out.println(i));

Function 接口

@FunctionInterface
public interface Function {
    R apply(T t);
}

这个接口有一个 apply 的抽象方法,它接受一个泛型 T 对象,返回一个泛型 R 对象。如果你想定义一个 Lambda ,将输入对象的信息映射到输出,就可以使用这个接口。例如下面这个例子:

public List map(List list, Function f) {
    List result = new ArrayList<>();
    for (T t : list) {
        result.add(f.apply(t));
    }
    return result;
}


List list = map(Arrays.asList("lambdas", "in", "action"), (String s) -> s.length());

就简单介绍这三个接口,其他接口你可以去查阅 Java API 的相关说明。

Java 中有两种类型,一个是原始类型,一个是引用类型。前面三个接口都是为引用类型而设计的。原始类型可以通过自动拆装箱和引用类型转换,但是性能方面需付出代价。Java8 为此提供了原始类型的特化接口。就是在函数式接口名称上加上对应的原始类型前缀,比如 DoublePredicate、IntConsumer 等等。

类型检查、类型推断以及限制

第一次提到 Lambda 表达式时,说她可以为函数式接口生成一个实例。然而,Lambda 表达式本身并不包含它在实现哪个函数式接口的信息。为了了解 Lambda 表达式,你应该知道 Lambda 的实际类型。

类型检查

Lambda 的类型是从使用 Lambda 的上下文推断出来的,上下文中的 Lambda 表达式需要的类型指的是目标类型。通过下例代码简单的介绍一下类型检查的过程。

List heavierThan150g = filter(inventory, (Apple a) -> getWeight() > 150)
    

第一:找出 filter 方法的声明

filter(List inventory, Predicate p)

第二:找出要求的目标类型

Predicate 

第三:确定 Predicate 是函数式接口,找出抽象方法

boolean test(Apple a)

第四:确定 test 方法的函数描述符

Apple -> boolean

最后:filter 方法的任何实际类型参数都需要匹配,如果有异常抛出也必须匹配。

同样的 Lambda ,不同的接口

有了目标类型的概念,同一个 ​Lambda 就可以和不同的函数是接口联系起来了,只要抽象方法能兼容。例如:

Callable c = () -> 75;
PrivilegeAction p = () -> 75;

应为他们都是什么都不接受,返回一个泛型 T 。

菱形运算符

熟悉 Java 演变的人会记得,Java7 中引入菱形运算符(< >),利用泛型推断从上下文推断类型的思想。一个实例表达式可以出现在多个不同的上下文中,并会像下面这样推断出合适的类型。

List ListOfString = new ArrayList<>();
List ListOfIntegers = new ArrayList<>();

现在你应该能很好理解什么时候使用 Lambda 表达式了。使用类型检查可以知道 Lambda 是否合适某个特定的上下文。其实,我们可以推断 Lambda 的参数类型。

类型推断

还可以进一步简化所写代码。Java 编译器会从上下文中推断函数的目标类型,这表明可以推断出适合的 Lambda 签名,应为函数描述符可以通过目标类型得到。这样我们可以在 Lambda 语法中省去参数注明。例如:

//参数 a 没有类型
List greenApples = filter(inventory, a -> "green".equals(a.getColor()));

Lambda 表达是由多个参数,代码可读性的好处就更为明显。例如:你可以这样创建一个 Comparator 对象:

//没有类型推断
Comparator c = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
//有类型推断
Comparator c = (a1, a2) -> a1.getWeight().compareTo(a2.getWeight());

有时候显示写出类型更易读,有时候去掉更易读。没有哪种说法更好,由程序员喜好决定。

使用局部变量

Lambda 是允许使用自由变量(不是参数,是外层作用域定义的变量)的,就像匿名类一样。它们被称作捕获 Lambda 。例如:

int portNumber = 6666;
Runner r - () -> System.out.println(portNumber);

但有时候有一点小麻烦,局部变量必须是显示声明为 final 或者事实上是 final 。如果是可变的,就不能通过编译。例如:

int portNumber = 6666;
Runner r - () -> System.out.println(portNumber);
int portNumber = 8888;
//这是不能通过编译的

你可能会想,为什么要有这种限制呢?第一:实例变量和局部变量有一个关键不同,实例变量在堆中储存,局部变量在栈中储存。如果 Lambda 直接使用局部变量,而且是在一个线程中使用的,则使用 Lambda 的线程,可能会在分配那个变量的线程回收该变量后使用。因此,Java 访问局部变量时,实际是访问它的副本。如果局部变量只赋值一次就能没什么区别了。第二:这一限制不鼓励你使用改变外部变量的典型命令式编程模式。

方法引用

方法引用让你可以重复使用现有方法定义,并像 Lambda 一样传递它们。下面我们借助更新的 Java8 API ,用方法引用写一个排序的例子:

先前:

inventory.sort((Apple a1, Apple a2) -> a1.compareTo(a2.getWeigth()));

以后

//第一个方法引用实例
inventory.sort(comparing(Apple::getWeigth()));

管中窥豹

方法引用可以看作调用特定方法的 Lambda 的一种快捷写法。基本思想是,调用一个方法最好是直接用名称调用它,而不是去描述如何调用它。当时需要使用方法引用时,目标引用放在分隔符 :: 前,方法名称放在后面。例如:Apple::getWeigth 就是引用了 Apple 类中定义的方法 getWeigth。

方法引用主要有三类:

  • 指向静态的方法引用(例如 Integer::parseInt)

  • 指向任意类型实例方法的方法引用(例如 String::length )

  • 指向现有对象的实例方法的方法引用

    下面是上述方法引用和 Lambda 表达式之间的等价:

    // Lambda 
    (args) -> ClassName.staticMethod(args)
    //方法引用
    ClassName::staticMethod
    
    // Lambda 
    (args, rest) -> args.instance(args)
    //方法引用
    ClassName::instanceMethod
    
    // Lambda 
    (args) -> expr.instanceMethod(args)
    //方法引用
    expr::instanceMethod
    

    还有针对构造函数、数组构造函数和父类调用等一些特殊形式的方法引用。如果你感兴趣,可以查阅相关文档。

    构造函数引用

    对于现有构造函数,你可以利用它的名称和关键字 new 来创建一个引用:ClassName::new 。它的功能和指向静态方法的引用类似。

    Supplier c = Apple::new;
    Apple a = c.get();
    

    等价于

    Supplier c = () -> new Apple();
    Apple a = c.get();
    

    这里就不一一举例了。

    总结

    • Lambda 可以理解为一种匿名函数:它没有名称,但是有参数列表、函数主体、返回类型,可能还会抛出一个异常。
    • Lambda 表达式让你可以简洁的传递代码。
    • 函数是接口就是仅仅声明了一个抽象方法的接口。
    • 只有在接收函数式接口的地方才可以使用 Lambda 表达式。
    • Lambda 表达式允许你直接内联,为函数式接口的抽象方法提供实现,并将整个表达式作为函数式接口的一个实例。
    • Java8 自带了一些常用的函数式接口,放在 java.util.function 包中,包括 Predicate、Function 、Supplier、Consumer 等等。
    • 为了避免装箱操作,对Predicate 和 Function 等常用接口的原始类型特化:IntPredicate、IntToLongFunction 等。
    • 环绕执行模式可以配合 Lambda 提高灵活性和可用性。
    • Lambda 表达式所需的代表类型成为目标类型
    • 方法引用让你重复使用现有的方法实现并之间传递他们。

你可能感兴趣的:(2018-10-23)