Java8据说是Java诞生以来最大的一次演进,说实话,对我个人来说没有什么特别大的感受,因为我学Java也就最近一两年的事,Java8在2014年3月18日发布,新增的特性确实非常惊艳,在语言特性层面上新增了lambda,Optional,默认方法,Stream API等,在虚拟机层面上新增了G1收集器(不过在Java9之后才改为默认的垃圾收集器)......
我个人认为Java8和语言相关的几个最重要的特性是如下几个:
本系列文章的后面几篇文章会围绕这几个主题来展开,今天就先上个开胃菜,lambda表达式!
lambda表达式也叫做匿名函数,其基于著名的λ演算得名,关于λ演算,推荐大家去找找关于“丘奇数”相关的资料。Java一直被人诟病的一点就是“啰嗦”,通常为了实现一个小功能,就不得不编写大量的代码,而用其他的语言例如Python等,也许寥寥几行代码就解决了,但支持lambda表达式之后,这一情况得到了大大的改善,现在只要使用得当,可以大大缩减代码里,使代码的目的更加清晰,易读,纯粹。
在Java中,很多时候在使用一些API的时候,必须要给出一些接口的实现,但因为该实现其实也就用一次,专门去创建一个新的实现类并不划算,所以一般大多数人采取的措施应该是创建一个匿名实现类,比较典型就是Collections.sort(List list, Comparator<? super T> c)方法,该方法接受一个Comparator类型的参数,Comparator是一个接口,表示“比较器”,如果要使用该方法对集合元素进行排序,就必须提供一个Comparator接口的实现,否则无法通过编译。如下所示:
Collections.sort(numbers, new Comparator<Integer>() { @Override public int compare(Integer o1, Integer o2) { return o1.compareTo(o2); } }); 复制代码
其实这个实现类的核心只有一行,即return o1.compareTo(o2);但我们却不得不编写其他“啰嗦”的代码,如果使用lambda表达式,会是怎么个样子呢?
Collections.sort(numbers, (n1, n2) -> n1.compareTo(n2)); 复制代码
没错,就是那么简单粗暴,就是一行核心代码。其他的比如方法签名啥的统统可以省略了,不仅简洁,而且语义也更加清晰,读起来就好像是说:“sort方法,帮我吧numbers这个序列排个序,排序规则就按照n1.compareTo(n2)的返回值来决定”。现在,是不是感觉,写代码就像在和计算机对话一样简单?但(n1, n2) -> n1.compareTo(n2)这玩意是个什么鬼?还带个箭头?不用着急,下面马上介绍lambda表达式的语法。
如果函数主体仅仅包含一行代码,可以省略花括号{}和return关键字(如果有的话)。对于我们的例子,可以改写成这样:
Collections.sort(numbers, (n1, n2) -> {return n1.compareTo(n2);}); 复制代码
注意分号!因为此时return n1.compareTo(n2);就是一条普通的Java语句了,必须遵守Java的语法规则。好了,尽管我们现在明白了lambda语句的语法规则,但还有一个关键的问题,就是为什么要这样写,换句话说,为什么要有俩参数,这return又是几个意思?还有到底哪里才可以使用lambda表达式?说到这,就不得不说一下和lambda息息相关的东西了: 函数式接口 。
函数式接口是这样的:只有一个抽象方法的接口就是函数式接口。为什么要特别强调抽象方法呢?Java接口里声明的方法不都是抽象方法吗?在Java8之前,这么说确实没有任何问题,但Java8新增了接口的默认方法,可以在接口里给出方法的具体实现,这里先不多说,后面的文章会详细讨论这个东西。
lambda表达式仅可以用在函数式接口上,我们在上面遇到的Comparator就是一个函数式接口,他只有一个抽象方法:compare(),其方法签名是这样的:
int compare(T o1, T o2); 复制代码
现在来看看 (n1, n2) -> n1.compareTo(n2)这个表达式,是不是发现了什么?没错,其实lambda表达式的参数列表就是对应的函数式接口的抽象方法的参数列表,并且类型可以省略(编译器自动推断),然后n1.compareTo(n2)的返回值是int类型,也符合compare()的方法描述。这样就算是把lambda表达式和接口的抽象方法签名匹配成功了,不会出现编译错误。
除此之外,Runnable也是一个函数式接口,它只有一个抽象方法,即run(),run()方法的方法签名如下所示:
public abstract void run(); 复制代码
不接受任何参数,也没有返回值。那如果要编写对应的lambda表达式,该如何做呢?其实非常简单,下面是一个示例:
Runnable r = () -> { System.out.println(Thread.currentThread().getName()); //do something }; 复制代码
如果观察仔细的话,会发现,示例代码中把这个lambda表达式赋值给了Runnable类型的变量r!经过上面的讨论,我们知道,其实lambda就是一个方法实现(其实叫做函数会更加合适),这条赋值语句看起来就好像是再说:“把方法(函数)赋值给变量!”。如果没有接触过函数式编程,会觉得这样很奇怪,怎么能把方法赋值给变量呢?计算机就是这样有意思,总是有各种各样奇奇怪怪的东西冲击我们的思维!那这有什么用呢?咱先不说什么高阶函数,科里化啥的(这些是函数式编程里的概念),就说一点: 意味着我们可以把方法(函数)当做变量来使用!即现在方法就是Java世界里的“一等公民”了!既可以将其作为参数传递给其他方法(函数),还可以将其作为其他方法(函数)的返回值(以后会讲到具体的案例)
策略模式是著名的23种设计模式中的一种,关于它的描述,我这里就不多说了。直接来看个例子吧。
例子是这样的,现在有一个代表汽车的Car类以及一个Car列表,现在我们想要筛选列表中符合要求的汽车,为了应对多变的筛选方法,我们打算用策略模式来实现功能。
下面是Car类的代码:
public class Car { //品牌 private String brand; //颜色 private Color color; //车龄 private Integer age; //三个参数的构造函数以及setter和getter //颜色的枚举 public enum Color { RED,WHITE,PINK,BLACK,BLUE; } } //包含Car对象的列表 List<Car> cars = Arrays.asList( new Car("BWM",Car.Color.BLACK, 2), new Car("Tesla", Car.Color.WHITE, 1), new Car("BENZ", Car.Color.RED, 3), new Car("Maserati", Car.Color.BLACK,1), new Car("Audi", Car.Color.PINK, 5)); 复制代码
我们希望用一个方法来封装筛选的逻辑,其方法签名伪代码如下所示:
cars carFilter(cars, filterStrategy); 复制代码
接下来实现策略模式,下面是相关的代码:
public interface CarFilterStrategy { boolean filter(Car car); } public class BWMCarFilterStrategy implements CarFilterStrategy { @Override public boolean filter(Car car) { return "BWM".equals(car.getBrand()); } } public class RedColorCarFilterStrategy implements CarFilterStrategy { @Override public boolean filter(Car car) { return Car.Color.RED.equals(car.getColor()); } } 复制代码
为了简单,仅仅实现了两种筛选策略,第一种是删选出品牌是“BWM”的汽车,第二种是删选出颜色为红色的汽车。最后来实现carFilter方法,如下所示:
private static List<Car> carFilter(List<Car> cars, CarFilterStrategy strategy) { List<Car> filteredCars = new ArrayList<>(); for (Car car : cars) { if (strategy.filter(car)) { filteredCars.add(car); } } return filteredCars; } 复制代码
最后的最后是测试代码:
public static void main(String[] args) { System.out.println(carFilter(cars, new BWMCarFilterStrategy())); System.out.println("----------------------------------------"); System.out.println(carFilter(cars, new RedColorCarFilterStrategy())); } 复制代码
分别实例化两个策略,将其作为参数传递给carFilter()方法,最终的输出如下所示:
[Car{brand='BWM', color=BLACK, age=2}] ---------------------------------------- [Car{brand='BENZ', color=RED, age=3}] 复制代码
确实符合预期。是不是就到此为止了呢?当然不!我们发现,其实BWMCarFilterStrategy以及RedColorCarFilterStrategy的实现代码都非常简单,仅仅寥寥几行代码,而且CarFilterStrategy接口仅仅有一个filter抽象方法,显然是一个函数式接口,那我们能不能用lambda表达式来简化呢?答案是:完全可以!而且更加推荐用lambda表达式来简化这种情况。
只要略微做一些修改就行了:
System.out.println(carFilter(cars, car -> "BWM".equals(car.getBrand()))); System.out.println("----------------------------------------"); System.out.println(carFilter(cars, car -> Car.Color.RED.equals(car.getColor()))); 复制代码
这里不再使用BWMCarFilterStrategy以及RedColorCarFilterStrategy两个类了,直接用lambda表达式就行了!最后把这俩实现删除掉!是不是顿时感觉整个项目的代码清爽了许多?
其实本小节的例子有些过于特殊了,如果你项目中的策略模式的实现非常复杂,其策略不是简简单单的几行代码就能解决的,此时要么进一步封装代码,要么就最好不要用lambda表达式了,因为如果逻辑复杂的话,强行使用lambda不仅仅不能简化代码,反而会使得代码更加晦涩。
最后简单讲一下方法引用吧,方法引用其实是lambda表达式的一种特殊情况的表示,语法规则是:
<class name or instance name>:<method name> 复制代码
如果lambda表达式的主体逻辑仅仅是一个调用方法的语句的话,那么就可以将其转换为方法引用,如下所示:
//普通的lambda表达式 numbers.forEach(n -> System.out.println(n)); //转换成方法引用 numbers.forEach(System.out::println); 复制代码
他俩效果是完全一样的,但显然方法引用更加简洁,语义也更加明确了,这一语法糖“真香!”。具体的我就不多说了,建议看看《Java8 实战》一书,里面有非常非常详细的介绍。
本文简单介绍了lambda表达式的语法以及使用。lambda表达式确实能大大简化原本复杂啰嗦的Java代码,而且更加灵活,语义也更加清晰明了,写代码的时候就好像用自然语言和计算机对话一样!但也不是哪里都能使用的,一个最基本的要求就是:其放置的位置要对应着一个函数式接口。函数式接口即只有一个抽象方法的接口,例如Comparator,Runnable等。除此之外,使用lambda表达式的时候,其主体逻辑最好不要超过10行,否则最好还是换一种方式来实现,这里10行并不是那么严格,具体情况还要具体分析。方法引用是一种特殊情况下的lambda表达式的表示方法,可以理解为是lambda的一个语法糖,其语义更加明确,语法也更加简洁,用起来还是非常舒服的!
最后,作为一个补充,来简单看看JDK内置的一些通用性比较强的函数式接口,这些接口都在java.util.function包下,我没数过,咋一看估计得有40多个吧。常用的有Function,Predicate,Consumer,Supplier等。Function的抽象方法的方法签名如下所示:
R apply(T t); //T,R是泛型 复制代码
简单从语义上来看,就是传入一个T类型的值,然后apply函数将其转换成R类型的值,即一对一映射。其他的接口就不做介绍了。