JAVA8新特性一:lambda表达式

大家好,最近入职了新的公司,所以很久都没有写新博客了(其实就是懒)
新公司大量的使用了一些JAVA8的新特性,所以也恶补了一下JAVA8的知识(好吧我知道JAVA14都出来了我还在写JAVA8的特性确实有点落伍了)但最近看了一下实战系列的JAVA8实战,感觉很受启发,作者的高度带给了我不一样的角度来看这些特性。

Lambda表达式

Lambda表达式概述

首先让我们来康康JAVA8最大的一个变动,支持了Lambda表达式。其实这个变动也是JAVA不得已而为之,因为他的竞争对手都支持了这种方便的写法,不思进取的话只能落得和cobol前辈一样的下场。
用最简单的话说,Lambda其实就是把一段代码作为了一个变量,在JAVA8之前变量只能是那几大基础类型+引用类型,而把一段代码作为变量有什么好处呢?好处是可以很简洁的描述匿名内部类,让我们来看一个《JAVA8实战》中的例子:

假设你在设计一个农场库存程序,你必须实现一个从列表中筛选绿苹果的功能。

好,是不是很简单?让我们来实现它

第一版:单纯的绿苹果筛选
public static List<Apple> filterGreenApples(List<Apple> inventory) { List<Apple> result = new ArrayList<Apple>();
for(Apple apple: inventory){
  if( "green".equals(apple.getColor() ) { result.add(apple);
} }
    return result;
}

OK,非常简单,遍历List逐个比较颜色,把符合条件的放进新List。
现在农民伯伯提出需求:
他还想要筛选红苹果。 你该怎么做呢?

第二版:抽象颜色
public static List<Apple> filterApplesByColor(List<Apple> inventory, String color) {
    List<Apple> result = new ArrayList<Apple>();
    for (Apple apple: inventory){
if ( apple.getColor().equals(color) ) { result.add(apple);
} }
    return result;
}

我们多加了个参数叫color,用以抽象颜色,这样农民伯伯无论要什么颜色的苹果,都可以筛选。

但是,农民伯伯又来了:“要是能区分 轻的苹果和重的苹果就太好了。重的苹果一般是重量大于150克。”你握紧了拳头,对农民伯伯抱拳以示尊敬。马上坐下来又开始编写:

第三版:单纯的重量筛选

public static List<Apple> filterApplesByWeight(List<Apple> inventory, int weight) {
        List<Apple> result = new ArrayList<Apple>();
        For (Apple apple: inventory){
if ( apple.getWeight() > weight ){ result.add(apple);
} }
        return result;
    }

OK,还是很简单。但你已经开始注意到事情没对了,水温逐渐升高,作为一只老练的青蛙你发现你写了一个很类似颜色筛选的方法,单纯只是从比较颜色变成了比较重量。

第四版:重量或颜色筛选(通过flag来控制)
public static List<Apple> filterApples(List<Apple> inventory, String color, int weight, boolean flag) {
     List<Apple> result = new ArrayList<Apple>();
    for (Apple apple: inventory){
if ( (flag && apple.getColor().equals(color)) || (!flag && apple.getWeight() > weight) ){ result.add(apple);
} }
    return result;
}

你通过一个flag来表示是要比较颜色还是比较重量,这样这个方法可以筛选颜色或重量,but。。。你觉不觉得那个flag很丑?再说,如果农民伯伯又跑过来说我还需要筛选形状和大小,难道你需要加更多的flag来控制到底筛选哪个吗?

第五版:策略模式
//重量筛选器
public class AppleHeavyWeightPredicate implements ApplePredicate{ 
	public boolean test(Apple apple){
	return apple.getWeight() > 150; 
	}
}
//颜色筛选器
public class AppleGreenColorPredicate implements ApplePredicate{
	public boolean test(Apple apple){
	return "green".equals(apple.getColor());
 	 } 
}
//我们要比什么,就放什么筛选器进去
public static List<Apple> filterApples(List<Apple> inventory,
                                           ApplePredicate p){
        List<Apple> result = new ArrayList<>();
        for(Apple apple: inventory){
			if(p.test(apple)){ result.add(apple);
			} 
		}
    return result;
}

好了,到这一步策略模式也用上了,感觉越来越高级了哈。之后如果要再筛选什么,我们直接写一些筛选器就好了,看起来非常好。
但能不能再改进呢?好像也不是不行,如果硬要挑骨头的话,我们会觉得每次要新建好多筛选器类啊,如果有的筛选器类就用得到一次,确实在我们工程里很不妥,删又删不得,但又没啥其他的作用。
其实我们不用特地声明筛选器啊,在要筛选的时候,用一个匿名类不就好了吗?

第六版:匿名类
List<Apple> redApples = filterApples(inventory, new ApplePredicate() { 
public boolean test(Apple apple){
	return "red".equals(apple.getColor());
	}
});

Good,这样我们就不用新建很多筛选器类了,但尴尬的是代码好像没有减少啊。。。代码只是从一个单独的类移到了方法入参。。。。

这时候就该。。。。。Lambda救场!

第七版:Lambda
List<Apple> result =
filterApples(inventory, (Apple apple) -> "red".equals(apple.getColor()));

仔细看,Lambda用(Apple apple) -> “red”.equals(apple.getColor())取代了匿名类的声明,这就是它的简洁之处。

当然,现在可能你还不知道这行带箭头的代码是什么意思,但没事,我们已经一步一步引出了Lambda的必要性与优越性。

Lambda语法初探

我们以一个例子来讲解Lambda
在这里插入图片描述
Lambda分三部分:

参数列表——这里它采用了Comparator中compare方法的参数,两个Apple。
箭头——箭头->把参数列表与Lambda主体分隔开。
Lambda主体——比较两个Apple的重量。表达式就是Lambda的返回值了。

大概知道了这三部分后,我将引用一篇知乎的文章来讲解Lambda,当时我看了如醍醐灌顶,知乎链接在这里,也可以直接去看:
Lambda 表达式有何用处?如何使用? - Mingqi的回答 - 知乎
知乎地址

我们知道,对于一个Java变量,我们可以赋给其一个“值”。如果你想把“一块代码”赋给一个Java变量,应该怎么做呢?比如,我想把右边那块代码,赋给一个叫做aBlockOfCode的Java变量:在Java 8之前,这个是做不到的。但是Java 8问世之后,利用Lambda特性,就可以做到了。当然,这个并不是一个很简洁的写法。所以,为了使这个赋值操作更加elegant, 我们可以移除一些没用的声明。这样,我们就成功的非常优雅的把“一块代码”赋给了一个变量。而“这块代码”,或者说“这个被赋给一个变量的函数”,就是一个Lambda表达式。但是这里仍然有一个问题,就是变量aBlockOfCode的类型应该是什么?在Java 8里面,所有的Lambda的类型都是一个接口,而Lambda表达式本身,也就是”那段代码“,需要是这个接口的实现。这是我认为理解Lambda的一个关键所在,简而言之就是,Lambda表达式本身就是一个接口的实现。直接这样说可能还是有点让人困扰,我们继续看看例子。我们给上面的aBlockOfCode加上一个类型:这种只有一个接口函数需要被实现的接口类型,我们叫它”函数式接口“。为了避免后来的人在这个接口中增加接口函数导致其有多个接口函数需要被实现,变成"非函数接口”,我们可以在这个上面加上一个声明@FunctionalInterface, 这样别人就无法在里面添加新的接口函数了。这样,我们就得到了一个完整的Lambda表达式声明:

---------------引用完毕
所以lambda里代表的方法,就是函数式接口里的那个唯一的方法。
当然,你可能会想:“为什么只有在需要函数式接口的时候才可以传递Lambda呢?”语言的设计者 也考虑过其他办法,例如给Java添加函数类型(有点儿像我们介绍的描述Lambda表达式签名的特 殊表示法)。但是他们选择了现在这种方式,因为这种方式自然且能避免语言变得更复杂。

JAVA自带的函数式接口

Lambda必须要有函数式接口才能用,JAVA设计师也设计了几个内嵌的函数式接口供我们使用:
Predicate
java.util.function.Predicate接口定义了一个名叫test的抽象方法,它接受泛型 T对象,并返回一个boolean。

consumer
java.util.function.Consumer定义了一个名叫accept的抽象方法,它接受泛型T 的对象,没有返回(void)。你如果需要访问类型T的对象,并对其执行某些操作,就可以使用 这个接口。

Function
java.util.function.Function<T, R>接口定义了一个叫作apply的方法,它接受一个 泛型T的对象,并返回一个泛型R的对象。如果你需要定义一个Lambda,将输入对象的信息映射 到输出,就可以使用这个接口(比如提取苹果的重量,或把字符串映射为它的长度)。

局部变量的限制

当我们在Lambda里使用局部变量时,一定要注意必须加上final修饰符的局部变量才可以在Lambda中使用(当然大多数情况其实也不会使用到局部变量)

原因如下:
第一,实例变量和局部变量背后的实现有一 个关键不同。实例变量都存储在堆中,而局部变量则保存在栈上。如果Lambda可以直接访问局 部变量,而且Lambda是在一个线程中使用的,则使用Lambda的线程,可能会在分配该变量的线 程将这个变量收回之后,去访问该变量。因此,Java在访问自由局部变量时,实际上是在访问它 的副本,而不是访问原始变量。如果局部变量仅仅赋值一次那就没有什么区别了——因此就有了 这个限制。

第二,这一限制不鼓励你使用改变外部变量的典型命令式编程模式

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章