《Java 8 in Action》【03】----Lambda表达式(一)

1.Lambda介绍

 前面章节说到利用行为参数化来传递代码有助于应对不断变化的需求,Java8之前为表现不同行为需要定义一个对应实体类,这种方式繁琐之处在于需要显式声明类。虽然匿名类解决了这个问题,但代码存在大量重复模板。使用Lambda,可以很简洁地表示一个行为,同匿名类一样,可以将它作为参数传递给一个方法。Lambda一词来自于学术界开发出来的一套用来描述计算的λ演算法,可以将lambda表达式理解为可传递的匿名函数的简洁表示:它没有名称,但它有参数,函数主题,返回类型。可能还有一个可以抛出的异常列表。

  • 匿名——不像普通方法样有一个明确的名称。
  • 函数——Lambda函数不像其他方法那样属于某个特定类,但和方法一样,Lambda有参数列表,函数主题,返回类型,还可能有抛出的异常列表。
  • 传递——Lambda表达式可以作为参数传递给方法或者存储在变量中。
  • 简洁——无需像匿名类那样写很多模板代码。

 下面通过一个例子展示了Lambda表达式和匿名类相比,是如何很简明地传递代码。使用匿名类和Lambda定义一个Comparator对象代码分别如下:

//java8之前使用匿名类
Comparator<Apple> byWeight = new Comparator<Apple>() {
   public int compare(Apple a1, Apple a2){
        return a1.getWeight().compareTo(a2.getWeight());
    }
};
//使用Lambda表达式定义
Comparator<Apple> byWeight = 
(Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());

上面展示的Lambda表达式有三个部分。

  • 参数列表——两个Apple。
  • 箭头——箭头->把参数列表与Lambda主体分隔开。
  • Lambda主体——比较两个Apple的重量。表达式就是Lambda的返回值了。
    在这里插入图片描述
//String类型参数,并返回一个int。Lambda没有return语句,因为隐含了return
(String s) -> s.length()
//Apple类型参数并返回一个boolean(苹果重量是否超过150g)
(Apple a) -> a.getWeight() > 150
//有两个int类型参数而没有返回值(void返回),Lambda可以包含多行语句,这里有两行
(int x, int y) -> {
	System.out.println("Result:");
	System.out.println(x+y);
}
//没有参数,返回一个int
() -> 42
//两个Apple类型的参数,返回一个int:比较两个Apple的重量
(Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight())

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

(parameters) -> expression

或者(注意语句的花括号)

(parameters) -> { statements; }

下表提供了一些Lambda的例子和使用案例:
在这里插入图片描述

2.Lambda使用及函数式接口

 前面对Lambda做了简单介绍,现在问题是如何使用Lambda表达式,在此之前需要先理解下函数式接口的概念。函数式接口就是只定义了一个抽象方法的接口,实际上Java API中存在一些函数式接口。例如:

public interface Comparator<T> {
	//只有一个抽象方法
	int compare(T o1, T o2);
}
public interface Runnable{
	//只有一个抽象方法
	void run();
}

需要注意的一点是Java8中接口可以拥有默认方法(default标识),但即使接口中存在很多默认方法,只要接口只定义了一个抽象方法,它依然是一个函数式接口。
  Lambda表达式允许以内联的形式为函数式接口的抽象方法提供了实现,并把整个表达式作为函数式接口的一个实例,也就是说Lambda表达式是函数式接口的一个具体实现的实例,函数式接口的抽象方法的签名基本上就是Lambda表达式的签名,可以将这种抽象方法的签名叫做函数描述符。例如将Runnable接口可以看作是什么参数不接受什么也不返回(void)的函数的签名,因为它仅包含一个run抽象方法,这个方法什么参数也不接受,什么也不返回(void)。本章使用了一种特殊的表示法来描述Lambda和函数式接口的签名,例如()->void表达式的参数列表为空,且返回void的函数,而这正是Runnable接口所代表的。另外一个例子是(Apple,Apple)->int表示接受两个Apple作为参数且返回int的函数。Java中常见的是函数描述符可见下面Java 8中的常用函数式接口一表。
  那么编译器是如何检查Lambda表达式在给定上下文中是否有效的呢?下面章节将会细谈这个问题。现在只需了解Lambda可以被赋给一个变量,或者传递给一个接受函数式接口作为参数的方法就行了,而且这个Lambda表达式的签名要和函数式接口的抽象方法一样的。正如前面所说函数式接口有且只有一个抽象方法,此抽象方法的签名可以描述Lambda表达式的签名。为了应该不同的Lambda表达式,需要一套能够描述常见函数描述符的函数式接口,实际上Java8在java.util.function包中引入几个新的函数式接口。 见下面Java 8中的常用函数式接口一表。下面对常见的函数式接口PredicateConsumerFunction做一个简单介绍。

2.1 Predicate

java.util.function.Predicate<T>接口定义了一个叫test的抽象方法,test方法接受泛型T对象,并返回一个boolean

@FunctionalInterface
public interface Predicate<T>{
	boolean test(T t);
}

 当需要表示一个涉及类型T的布尔表达式时,就可以使用这个接口。比如可以定义一个接受String对象的Lambda表达式,代码如下:

public static <T> List<T> filter(List<T> list, Predicate<T> p) {
   List<T> results = new ArrayList<>();
    for(T s: list){
        if(p.test(s)){
            results.add(s);
        }
    }
    return results;
}
//传入跟Predicate函数描述符一致的Lambda表达式
Predicate<String> nonEmptyStringPredicate = (String s) -> !s.isEmpty();
List<String> nonEmpty = filter(listOfStrings, nonEmptyStringPredicate);
2.2 Consumer

java.util.function.Consumer<T>定义了一个叫accept的抽象方法,accept方法接受泛型T的对象,没有返回(void)。

@FunctionalInterface
public interface Consumer<T>{
	void accept(T t);
}

 当需要访问类型T的对象,并对其执行某些操作,就可以使用这个接口。例如可以用它来创建一个forEach方法,接受一个Integers的列表,并对其中每个元素执行操作,例如打印列表中的所有元素。

public static <T> void forEach(List<T> list, Consumer<T> c){
    for(T i: list){
         c.accept(i);
     }
 }
 //传入跟Consumer函数描述符一致的Lambda表达式
 forEach(Arrays.asList(1,2,3,4,5), (Integer i) -> System.out.println(i));
2.3 Function

java.util.function.Function<T, R>接口定义了一个叫作apply的方法,它接收一个泛型T的对象,并返回一个泛型R的对象。

@FunctionalInterface
public interface Function<T, R>{
	R apply(T t);
}

 如果需要定义一个Lambda表达式,将输入对象的信息映射到输出,就可以使用这个接口(比如提取苹果的重量,或把字符串映射为它的长度)。例如下面代码中,利用它来创建一个map方法,以将一个String列表映射到包含每个String长度的Integer列表。

 public static <T, R> List<R> map(List<T> list,Function<T, R> f) {
    List<R> result = new ArrayList<>();
     for(T s: list){
         result.add(f.apply(s));
     }
     return result;
 }
 // Lambda是Function接口的apply方法的实现
 List<Integer> l = map(Arrays.asList("lambdas","in","action"),
     (String s) -> s.length()
 );
2.4 原始类型特化

 前面介绍了三个常见泛型函数式接口:Predicate<T>,Consumer<T>Function<T, R>。还有些函数接口专门为某些类型而设计。Java中类型要么是引用类型(如ByteIntegerObjectList),要么是原始类型(比如byteintdoublechar)。但是泛型函数式接口(如Consumer<T>中的是T)只能绑定到引用类型,这是由泛型内部实现方式造成的。在Java里有一个将原始类型转换为对应的引用类型的机制,这个机制叫作装箱。相反将引用类型转换为对应的原始类型,叫作拆箱。Java还有一个自动装箱机制:装箱和拆箱操作是自动完成的,因此下面代码是有效的(int被装箱为Integer)。

List<Integer> list = new ArrayList<>();
	for (int i = 300; i < 400; i++){
		list.add(i);
}

 但这种方式是有性能的损失,装箱的本质是将原始类型包裹起来,并保存在堆里。因此装箱后需要更多的内存,并需要额外的内存搜索来获取被包裹的原始值。Java8中为一些函数式接口提供了专门的版本。以便在输入和输出都是原始类型时避免自动装箱的操作。例如下面代码中,IntPredicate就避免了对值1000进行装箱操作,但是要使用Predicate<Integer>就会将参数1000装箱到一个Integer对象中。

public interface IntPredicate{
	boolean test(int t);
}
//IntPredicate,返回true无装箱。
IntPredicate evenNumbers = (int i) -> i % 2 == 0;
evenNumbers.test(1000);
//Predicate,返回false会装箱
Predicate<Integer> oddNumbers = (Integer i) -> i % 2 == 1;
oddNumbers.test(1000);

 一般来说,针对专门的输入参数类型的函数式接口的名称都要加上对应的原始类型前缀,比如 DoublePredicateIntConsumerLongBinaryOperatorIntFunction 等。 Function接口还有针对输出参数类型的变种: ToIntFunction<T>IntToDoubleFunction 等。下表Java 8中的常用函数式接口提供了最常用的函数式接口及其函数描述符以及原始类型特化。其中函数描述符,箭头左侧代表了输入参数类型,箭头右侧表示返回类型。例如 (T,U) -> R 表达式代表一个函数,它具有两个参数,分别为泛型TU ,返回类型为 R

在这里插入图片描述
在这里插入图片描述
下表是一些使用Lambda和函数式接口的一些例子。
在这里插入图片描述

3.类型检查、类型推断及限制
3.1类型检查

可以将Lambda表达式看作是函数式接口的一个实例,但Lambda表达式本身并不包含它在实现哪个函数式接口的信息,它的类型实际是从使用Lambda的上下文推断出来的。上下文(如接受它传递的方法的参数,或者接受它的值的局部变量)中Lambdda表达式需要的类型称为目标类型。可以通过下面这个例子了解下类型检查过程。
在这里插入图片描述
类型检查过程可以分解为如下步骤:

  1. 首先,要出filter方法的声明。
  2. 第二,要求它是Predicate<Apple>(目标类型)对象的第二个正式参数。
  3. 第三,Predicate<Apple>是一个函数式接口,定义了一个叫作test的抽象方法。
  4. 第四,test方法描述了一个函数描述符,它可以接受一个Apple,并返回一个boolean。
  5. 最后,filter的任何实际参数都必须匹配这个要求。

 这段代码是有效的,因为传递filter方法的Lambda表达式同样接收Apple为参数,并返回一个boolean。需要注意的是,如果Lambda表达式抛出一个异常,那么抽象方法声明的throws语句也必须与之匹配。

3.2 同样的Lambda,不同的函数式接口

有了目标类型的概念,同一个Lambda表达式就可以与不同的函数式接口联系起来,只要它们的抽象方法签名能够兼容,比如 CallablePrivilegedAction接口根据它的抽象方法可知,它们都代表着什么也不接受且返回一个泛型T的函数,因此下面赋值是有效的。

//第一个赋值的目标类型是 Callable<Integer>
Callable<Integer> c = () -> 42;
//第二个赋值的目标类型是PrivilegedAction<Integer> 
PrivilegedAction<Integer> p = () -> 42;

如下面例子中同一个Lambda表达式可用于不同的函数式接口

Comparator<Apple> c1 = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
ToIntBiFunction<Apple, Apple> c2 = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
BiFunction<Apple, Apple, Integer> c3 = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());

【 特殊的void兼容规则】
如果一个Lambda主题是一个语句表达式,它就和一个返回void的函数描述符兼容(当然需要参数列表也兼容)、例如西面两行代码都是合法的,尽管List的add方法返回了一个boolean,而不是Consumer上下文(T->void )锁要求的void。
// Predicate返回了一个boolean
Predicate p = s -> list.add(s);
// Consumer返回了一个void
Consumer b = s -> list.add(s);

3.4 类型推断

 代码还可以进一步的简化,Java编译器会从上下文(目标类型)推断出用什么函数式接口来配合Lambda表达式,这意味着它也可以推断出适合Lambda的签名,因为函数描述符可以通过目标类型来得到。这样做的好处在于,编译器可以了解Lambda表达式的参数类型,就可以将Lambda语法中的省去标注参数类型,如下面的代码:

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

需要注意的是有时候显式写出类型更易读,有时候去掉它们更易读,没有具体规则。

3.5 使用局部变量

 迄今为止,前面介绍的Lambda表达式都只用到了其主体里面的参数,但Lambda表达式也允许使用自由变量(不是参数,而是外层作用域中定义的变量),就像是匿名类一样。他们被称作是捕获Lambda。如下面Lambda捕获了portNumber变量:

int portNumber = 1337;
Runnable r = () -> System.out.println(portNumber);

 Lambda表达式可以没有限制地捕获实例变量和静态变量。但是局部变量必须显式声明为final或者事实上为final。换句话说Lambda表达式只能捕获指派给它们的局部变量一次。(注:捕获实例变量可以被看作捕获最终局部变量 this) 例如下面的代码无法编译,因为 portNumber变量被赋值两次:

//编译有问题,Lambda表达式引用的局部变量必须是最终的(final)或事实上最终的
int portNumber = 1337;
Runnable r = () -> System.out.println(portNumber);
portNumber = 31337;

 局部变量存在限制的原因:
第一,实例变量和局部变量背后的实现有一个关键的不同。实例变量都存储在堆中,而局部变量则保存在栈上。如果Lambda可以直接访问局部变量,而且Lambda是在一个线程中使用的,则使用Lambda的线程,可能会在分该变量的线程将这个变量收回之后,去访问该变量。因此Java在访问自由局部变量时,实际上是在访问它的副本,而不是访问原始的变量。如果局部变量,仅仅赋值一次,就没有这个区别了。
第二,由于局部变量存在限制,因此不建议使用改变外部变量的典型命令式编程模式,这种模式会阻碍做到并行处理。

4.总结

1.Lambda表达式可以理解为一种匿名函数:它没有名称,但有参数列表,函数主题,返回类型,可能还有一个可以抛出的异常列表。
2.函数式接口就是仅仅声明了一个抽象方法的接口。只有在接收函数式接口的地方才可以使用Lambda表达式。
3.Lambda 表达式允许你直接内联方式,为函数式接口的抽象方法提供实现,并且将整个表达式作为函数式接口的一个实例。
4.Java8中 java.util.function包下有有一些常用的函数式接口,包括 Predicate<T>Function<T,R>Supplier<T>Consumer<T>BinaryOperator<T> 。为避免装箱操作,对 Predicate<T>Function<T, R> 等通用函数式接口的原始类型特化: IntPredicateIntToLongFunction 等。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值