Java学习day041 lambda表达式(为什么引入lambda表达式、lambda表达式的语法、函数式接口、方法引用)

使用的教材是java核心技术卷1,我将跟着这本书的章节同时配合视频资源来进行学习基础java知识。

day041   lambda表达式(为什么引入lambda表达式、lambda表达式的语法、函数式接口、方法引用)


1.为什么引入lambda表达式

lambda表达式是一个可传递的代码块,可以在以后执行一次或多次。先观察一下我们在Java中的哪些地方用过这种代码块。

在之前我们已经了解了如何按指定时间间隔完成工作。将这个工作放在一个ActionListener的actionPerformed方法中:

class Worker implements ActionListener
{
    public void actionPerformed(ActionEvent event)
    {
        //do some work
    }
}

想要反复执行这个代码时,可以构造Worker类的一个实例。然后把这个实例提交到一个Timer对象。这里的重点是actionPerformed方法包含希望以后执行的代码。

或者可以考虑如何用一个定制比较器完成排序。如果想按长度而不是默认的字典顺序对字符串排序,可以向sort方法传人一个Comparator对象:

class LengthComparator implements Comparator
{
    public int compare(String first,String second)
    {
        return first.length()-second.length();
    }
}
...
Arrays.sort(strings,new LengthComparator());

compare方法不是立即调用。实际上,在数组完成排序之前,sort方法会一直调用compare方法,只要元素的顺序不正确就会重新排列元素。将比较元素所需的代码段放在sort方法中,这个代码将与其余的排序逻辑集成(你可能并不打算重新实现其余的这部分逻辑)。

这两个例子有一些共同点,都是将一个代码块传递到某个对象(一个定时器,或者一个sort方法)。这个代码块会在将来某个时间调用。到目前为止,在Java中传递一个代码段并不容易,不能直接传递代码段。Java是一种面向对象语言,所以必须构造一个对象,这个对象的类需要有一个方法能包含所需的代码。

在其他语言中,可以直接处理代码块。Java设计者很长时间以来一直拒绝增加这个特性。毕竟,Java的强大之处就在于其简单性和一致性。如果只要一个特性能够让代码稍简洁一些,就把这个特性增加到语言中,这个语言很快就会变得一团糟,无法管理。不过,在另外那些语言中,并不只是创建线程或注册按钮点击事件处理器更容易;它们的大部分API都更简单、更一致而且更强大。在Java中,也可以编写类似的AP丨利用类对象实现特定的功能,不过这种API使用可能很不方便。

就现在来说,问题已经不是是否增强Java来支持函数式编程,而是要如何做到这一点。设计者们做了多年的尝试,终于找到一种适合Java的设计。


2.lambda表达式的语法

再来考虑上面讨论的排序例子。我们传人代码来检查一个字符串是否比另一个字符串短。这里要计算:

first.length()-second.length()

first和second是什么?它们都是字符串。Java是一种强类型语言,所以我们还要指定它们的类型:

(String first,String second)->first.length()-second.length()

这就是你看到的第一个表达式。lambda表达式就是一个代码块,以及必须传人代码的变量规范。

为什么起这个名字呢?很多年前,那时还没有计算机,逻辑学家Alonzo Church想要形式化地表示能有效计算的数学函数。(奇怪的是,有些函数已经知道是存在的,但是没有人知道该如何计算这些函数的值。)他使用了希腊字母lambda(λ)来标记参数如果他知道JavaAPI,可能就会写为

λfirst.λsecond.first.length()-second.length()

前面已经见过Java中的一种lambda表达式形式:参数,箭头(->)以及一个表达式。如果代码要完成的计算无法放在一个表达式中,就可以像写方法一样,把这些代码放在{}中,并包含显式的return语句。例如:

(String first,String second)->
    {
        if(first.length() < second.length()) return -1 ;
        else if (first.length() > second.length()) return 1;
        else return 0;
    }

即使lambda表达式没有参数,仍然要提供空括号,就像无参数方法一样:

0->{ for(int i=100;i>=0;i--) System.out.println(i);}

如果可以推导出一个lambda表达式的参数类型,则可以忽略其类型。例如:

Comparator<String> comp
    =(first,second)//Same as (String first,String second)
    ->first.length()-second.length();

在这里,编译器可以推导出first和second必然是字符串,因为这个lambda表达式将赋给一个字符串比较器。

如果方法只有一参数,而且这个参数的类型可以推导得出,那么甚至还可以省略小括号:

ActionListener listener = event->
    System.out.println("The time is"+new Date()");
    //Instead of(event)->...or(ActionEvent event)->...

无需指定lambda表达式的返回类型。lambda表达式的返回类型总是会由上下文推导得出。例如,下面的表达式

(String first,String second)->first.length()-second.length()

可以在需要im类型结果的上下文中使用。

如果一个lambda表达式只在某些分支返回一个值,而在另外一些分支不返回值,这是不合法的。例如,

(int x)->{if(x>=0) return 1;}

就不合法。

下面的程序显示了如何在一个比较器和一个动作监听器中使用lambda表达式。

/**
 *@author  zzehao
 */
import java.util.*;
import javax.swing.*;
import javax.swing.Timer;

public class LambdaTest
{
	public static void main(String[] args) 
	{
		String[] planets = new String[] { "Mercury", "Venus", "Earth","Mars", "Jupiter", "Saturn", "Uranus", "Neptune" }; 
		System.out.println(Arrays.toString(planets)); 
		System.out.println("Sorted in dictionary order:"); 
		Arrays.sort(planets); 
		System.out.println(Arrays.toString(planets)); 
		System.out.println("Sorted by length:");
		Arrays.sort(planets, (first, second) -> first.length() - second.length());
		System.out.println(Arrays.toString(planets)); 
		
		Timer t = new Timer(1000, event -> System.out.println("The time is "+ new Date()));
		t.start(); 

		//keep program running until user selects "0k" 
		JOptionPane.showMessageDialog(null, "Quit program?");
		System.exit(0);
	}
}

运行的结果是:


3.函数式接口

前面已经讨论过,Java中已经有很多封装代码块的接口,如ActionListener或Comparator。lambda表达式与这些接口是兼容的。

对于只有一个抽象方法的接口,需要这种接口的对象时,就可以提供一个lambda表达式。这种接口称为函数式接口(functional interface)。

为了展示如何转换为函数式接口,下面考虑Arrays.sort方法。它的第二个参数需要一个Comparator实例,Comparator就是只有一个方法的接口,所以可以提供一个lambda表达式:

Arrays.sort(words,(first,second)->first.length()-second.length());

在底层,Arrays.sort方法会接收实现了Comparator的某个类的对象。在这个对象上调用compare方法会执行这个lambda表达式的体。这些对象和类的管理完全取决于具体实现,与使用传统的内联类相比,这样可能要高效得多。最好把lambda表达式看作是一个函数,而不是一个对象,另外要接受lambda表达式可以传递到函数式接口。

lambda表达式可以转换为接口,这一点让lambda表达式很有吸引力。具体的语法很简短。下面再来看一个例子:

Timer t = new Timer(1000,event->
    {
        System.out.println("At the tone,the time is"+new Date());
        Toolkit.getDefaultToolkit().beep();
    });

与使用实现了ActionListener接口的类相比,这个代码可读性要好得多。

实际上,在Java中,对lambda表达式所能做的也只是能转换为函数式接口。在其他支持函数字面量的程序设计语言中,可以声明函数类型(如(String,String)->int)、声明这些类型的变量,还可以使用变量保存函数表达式。不过,Java设计者还是决定保持我们熟悉的接口概念,没有为Java语言增加函数类型。

甚至不能把lambda表达式赋给类型为Object的变量,Object不是一个函数式接口。

JavaAPI在java.util.fimction包中定义了很多非常通用的函数式接口。其中一个接口BiFunction<T,U,R>描述了参数类型为T和U而且返回类型为R的函数。可以把我们的字符串比较lambda表达式保存在这个类型的变量中:

BiFunction<String,String,Integer>comp
    =(first,second)->first.length()-second.length();

不过,这对于排序并没有帮助。没有哪个Arrays.sort方法想要接收一个BiFunction。如果你之前用过某种函数式程序设计语言,可能会发现这很奇怪。不过,对于Java程序员而言,这非常自然。类似Comparator的接口往往有一个特定的用途,而不只是提供一个有指定参数和返回类型的方法。Java SE 8沿袭了这种思路。想要用lambda表达式做某些处理,还是要谨记表达式的用途,为它建立一个特定的函数式接口。

java.util.function包中有一个尤其有用的接口Predicate:

public interface Predicate<T>
{
    boolean test(T t);
    //Additional default and static methods
}

ArrayList类有一个removelf方法,它的参数就是一个Predicate。这个接口专门用来传递lambda表达式。例如,下面的语句将从一个数组列表删除所有null值:

list.removelf(e->e==null);

 


4.方法引用

有时,可能已经有现成的方法可以完成你想要传递到其他代码的某个动作。例如,假设你希望只要出现一个定时器事件就打印这个事件对象。当然,为此也可以调用:

Timer t = new Timer(1000,event->System.out.println(event));

但是,如果直接把println方法传递到Timer构造器就更好了。具体做法如下:

Timer t = new Timer(1000,System.out::println);

表达式System.out::println是一个方法引用(method reference),它等价于lambda表达式x一>System.out.println(x)。

再来看一个例子,假设你想对字符串排序,而不考虑字母的大小写。可以传递以下方法表达式:

Arrays.sort(strings,String::compareToIgnoreCase)

从这些例子可以看出,要用::操作符分隔方法名与对象或类名。主要有3种情况:

•object::instanceMethod

•Class::staticMethod

•Class::instanceMethod

在前2种情况中,方法引用等价于提供方法参数的lambda表达式。前面已经提到,System.out::println等价于x->System.out.println(x)。类似地,Math::pow等价于(x,y)->Math.pow(x,y)。

对于第3种情况,第1个参数会成为方法的目标。例如,String::compareToIgnoreCase等同于(x,y)->x.compareToIgnoreCase(y)。

如果有多个同名的重栽方法,编译器就会尝试从上下文中找出你指的那一个方法。例如,Math.max方法有两个版本,一个用于整数,另一个用于double值。选择哪一个版本取决于Math::max转换为哪个函数式接口的方法参数。类似于lambda表达式,方法引用不能独立存在,总是会转换为函数式接口的实例。

可以在方法引用中使用this参数。例如,this::equals等同于x->this.equals(x)。使用super也是合法的。下面的方法表达式

super::instanceMethod

使用this作为目标,会调用给定方法的超类版本。

为了展示这一点,下面给出一个假想的例子:
 

class Greeter
{
    public void greet()
    {
        System.out.println("Hello,world!");
    }
}

class TimedCreeter extends Greeter
{
    public void greet()
    {
        Timer t = new Timer(1000,super::greet);
        t.start();
    }
}

TimedGreeter.greet方法开始执行时,会构造一个Timer,它会在每次定时器滴答时执行super::greet方法。这个方法会调用超类的greet方法。


 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值