java:lambda表达式

 

1.为什么引入lambda表达式?

lambda表达式是一个可传递的代码块,可以在以后执行一次或者多次。其实在我们日常代码的编写中,已经不止一次的使用过这种代码块。

列如:考虑如何用一个定制比较器完成排序。如果我们想要按照长度而不是字典序对字符串进行排序,可以像sort方法传入一个Comparator对象:

	class LengthComparator implements Comparator<String>{

		@Override
		public int compare(String o1, String o2) {
			// TODO 自动生成的方法存根
			return o1.length()-o2.length();
		}
	}
	
	...Arrays.sory(strings,new LengthComparator());

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

这个例子是将一个代码块传递到某个对象,这个代码块会在将来的某个时间调用。

目前看来,在java中传递一个代码段并不容易,不能直接传递代码段。作为一种面向对象的程序设计语言,我们必须构造一个对象,这个对象的类需要有一个方法能包含所需的代码块。

2.lambda表达式的语法

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

o1.length()-o2.length();

其中o1和o2都是字符串,java是一种强类型语言,所以我们还要指定他们的类型:

(String o1,String o2)->o1.length()-o2.length()

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

lambda名字的具体由来这里就不再细讲了(应该不重要吧?),我们已经见过java中的一种lambda表达式形式:

参数,箭头(->)以及一个表达式

如果代码要完成的计算无法放在一个表达式中,就可以像写方法一样,把这些代码放在{}中,并包含显示的return语句。列如:

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

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

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

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

Comparator<String> comp=(o1,o2)->(o1.length()-o2.length());

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

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

Actionlistener listener=event->System.out.println("The time is "+new Date());

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

(String o1,String o2)->o1.length()-o2.length();

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

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

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

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

class Solution {
	public static void main(String[] args) {
		String[] planets=new String[] {"Mecury","Venus","Earth","Mars",
				"Jupiter","Saturn","Uranus","Meptune"};
		System.out.println(Arrays.deepToString(planets));
		System.out.println("Sorted in dictionary order:");
		Arrays.parallelSort(planets);
		System.out.println(Arrays.toString(planets));
		System.out.println("Sorted by length");
		Arrays.parallelSort(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();
		
		JOptionPane.showMessageDialog(null, "Quit program?");
		System.exit(0);
	}
}

输出为:

[Mecury, Venus, Earth, Mars, Jupiter, Saturn, Uranus, Meptune]
Sorted in dictionary order:
[Earth, Jupiter, Mars, Mecury, Meptune, Saturn, Uranus, Venus]
Sorted by length
[Mars, Earth, Venus, Mecury, Saturn, Uranus, Jupiter, Meptune]
The time is Mon Nov 18 19:01:22 CST 2019
The time is Mon Nov 18 19:01:23 CST 2019
The time is Mon Nov 18 19:01:24 CST 2019
The time is Mon Nov 18 19:01:25 CST 2019
The time is Mon Nov 18 19:01:26 CST 2019
The time is Mon Nov 18 19:01:27 CST 2019
The time is Mon Nov 18 19:01:28 CST 2019
The time is Mon Nov 18 19:01:29 CST 2019

3.函数式接口

前面已经讨论过,java中已经有很多封装代码块的接口,例如:ActionListener或者Comparator。lambda表达式与这些接口是兼容的。对于只有一个抽象方法的接口,需要这种接口的对象时,就可以提供一个lambda表达式。这种接口称为函数式接口。

注释:按照常理来说,应该接口中所有方法都是抽象的才对,实际上,接口完全有可能重新声明Object类的方法,如toString或者Clone,这些声明有可能会让方法不再是抽象的。

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

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

在底层,Arrays.sort方法会接收实现了Comparator<String>的某个类的对象。在这个对象上调用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不是一个函数式接口!

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

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

不过,这对于排序并没有帮助,没有那个Arrays。sort方法想要接受一个BiFunction。

如果我们想要用lambda表达式做某些处理,需要谨记表达式的用途,为它建立一个特定的函数式接口。java.util.function包中有一个尤其有用的接口Predicate:

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

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

list.removeIf(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是一个方法引用,它等价于lambda表达式->System.out.println(x)。

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

Arrays.sort(strings,String::compareToIgnoreCase);

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

object::instanceMethod
Class::staticMethod
Class::instanceMethod

在前两种情况中,方法引用等价于提供方法参数的lambda表达式。对于第三种情况,第一个参数会成为方法的目标。列如,String::compareToIgnoreCase等同于(x,y)->x.compareToIgnoreCase(y)

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

可以在方法引用中使用this参数,列如,this::equals等同于x->this.equals(x)。使用super也是合法的。使用this作为目标,会调用给定方法的超类版本。我们看下面一个例子:

class Greeter{
	public void greet() {
		System.out.println("Hello World!");
	}
}
class TimedGreeter extends Greeter{
	public void greet() {
		Timer t=new Timer(1000,super::greet);
		t.start();
	}
}

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

5.构造器引用

构造器引用与方法引用很类似,只不过方法名为new,例如,Person::new是Person构造器的一个引用。具体是哪一个构造器呢?这取决于上下文。

可以用数组类型建立构造器引用。例如,int[]::new是一个构造器引用,它有一个参数:即数组的长度。这其实等价于lambda表达式x->new int[x];

java有一个限制,无法构造泛型类型T的数组。数组构造器引用对于客服这个限制很有用。表达式new T[n]会产生错误,因为这会改为new Object[n]。例如,假设我们需要一个Person对象数组。Stream接口有一个toArray方法可以返回Object数组。

Object[] people=stream.toArray();

不过, 这并不让人满意。用户希望得到一个Person引用数组,而不是Object引用数组。流库利用构造器引用解决了这个问题。可以把Person[]::new传入toArray方法:

Person[] people=stream.toArray(Person[]::new);

toArray方法调用这个构造器来得到一个正确类型的数组,然后填充这个数组并返回。

6.变量作用域

通常,我们希望能够在lambda表达式中访问外围方法或类中的变量。考虑下面这个例子:

public static void repeatMessage(String text,int delay) {
	ActionListener listener = event->
	{
		System.out.println(text);
		Toolkit.getDefaultToolkit().beep();
	};
	new Timer(delay,listener).start();
}

来看这样一个调用:

repeatMessage("Hello",1000);

该lambda表达式中的变量text并没有在表达式中定义,事实上它是repeatMessage方法的一个参数变量。仔细想想看,这里好像会有问题,尽管不那么明显。lambda表达式的代码可能会在repeatMessage调用很久以后才运行,而那时这个参数变量已经不存在了。如何保留text变量呢?

要了解到底会发生什么,下面来巩固我们对lambda表达式的理解。lambda表达式有3个部分:

1)一个代码块         2)参数         3)自由变量的值,这是指非参数而且不在代码中定义的变量

在上边的例子中,这个lambda表达式有1个自由变量text。表示lambda表达式的数据结构必须存储自由变量的值,在这里就是字符串“Hello”。我们称它被lambda表达式捕获。

扩展:在java中,lambda表达式就是闭包

我们可以看到,lambda表达式可以捕获外围作用域中变量的值。但是有一条明文规定:lambda表达式中捕获的变量必须实际上是最终变量,实际上的最终变量是指,这个变量初始化之后就不会再为它赋新值。上个例子中,text总是指示同一个String对象,所以捕获这个变量是合法的。

lambda表达式的体与嵌套块有相同的作用域,这里同样适用命名冲突和遮蔽的有关规则。在lambda表达式中声明与一个局部变量同名的参数或局部变量是不合法的。

class Solution {
    public static void main(String[] args) {
    	Path first=Paths.get("/usr/bin");
        Comparator<String> comp=(first,second)->first.length()-second.length();
    }
}

在方法中,不能有两个同名的局部变量,因此,lambda表达式中同样也不能有同名的局部变量。

在一个lambda表达式中使用this关键字时,是指创建这个lambda表达式的方法的this参数,列如:

public class Application() {
    public void init() {
    	ActionListener listener=event->{
           System.out.println(this.toString());
    	}
    }
}

表达式this.toString()会调用Application对象的toString方法,而不是ActionListener实例的方法。在lambda表达式中,this的使用并没有任何特殊之处,lambda表达式的作用域嵌套在init方法中,与出现在这个方法中的其他位置一样,lambda表达式中this的含义并没有变化。

7.处理lambda表达式

使用lambda表达式的重点是延迟执行。毕竟,如果想要立即执行代码,完全可以直接执行,而无需把它包装在一个lambda表达式中。之所以希望以后再执行代码,这里有很多原因,如:

1)在一个单独的线程中运行代码         

2)多次运行代码

3)在算法的适当位置运行代码(例如:排序中的比较操作)

4)发生某种情况时执行代码(例如:点击了一个按钮,数据到达,等等)

5)只在必要时才运行代码

8.再谈Comparator

Comparator接口包含很多方便的静态方法来创建比较器,这些方法可以用于lambda表达式或方法引用。

静态comparing方法取一个“键提取器”函数,它将类型T映射为一个可比较的类型(如String)。对要比较的对象应用这个函数,然后对返回的键完成比较,例如,假设有一个Person对象数组,可以如下按名字对这些对象排序:

Arrays.sort(people,Comparator.comparing(Person::getName));

也可以把比较器与thenComparing方法串起来,例如:

Arrays.sort(people,Comparator.comparing(Person::getName).thenComparing(Person::getFirstName));

如果两个人的姓相同,就会使用第二个比较器。

这些方法有很多变体形式。可以为comparing和thenComparing方法提取的键指定一个比较器。例如,可以如下根据人名长度完成排序:

Arrays.sort(people,Comparator.comparing(Person::getName,(s,t)->Integer.compare(s.length(),t.length())));

另外,comparing和thenComparing方法都有变体形式,可以避免int、long或double值的装箱。要完成前一个操作,还有一种更容易的做法:

Arrays.sort(people,Comparator.comparingInt(p->p.length()));

如果键函数可以返回nunll,可能就要用到nullsFirst和nullLast适配器。这些静态方法会修改现有的比较器,从而在遇到null值时不会抛出异常,而是将这个值标记为小于或者大于正常值。列如,假设一个人没有中名时getMiddleName会返回一个null,就可以使用

Comparator.comparing(Person::getMiddleName(),Comparator.nullsFirst(...))

nullsFirst方法所需要一个比较器,在这里就是比较两个字符串的比较器。naturalOrder方法可以为任何实现了Comparable的类建立一个比较器。在这里,Comparator.<String>naturalOrder()正是我们需要的。下面是一个完整的调用,可以按可能为null的中名进行排序。

Arrays.sort(people,comparing(Person::getMiddleName,nullsFirst(naturalOrder())));

静态reverseOrder方法会提供自然顺序的逆序。要让比较器逆序比较,可以使用reversed实例方法,例如naturalOrder().reversed

等同于reverseOrder()。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值