点击量:557
本文主要包括以下内容:
Lambda 表达式
方法引用
接口的默认方法和静态方法
Stream API
Lambda 表达式(Lambda Expression)
Java8最大的变化莫过于引入函数式编程的概念,也就是Lambda表达式,它因数学中的λ演算得名。1958年LISP语言第一个支持了Lambda,发展至今,越来越多的编程语言也逐渐开始支持这一特征,比如C#,C++,PHP等。而Oracle终于在2014年正式在Java8中支持Lambda表达式,这个广大Java程序员期待已久的特性终于来临。
什么是Lambda 表达式?
看起来很高大上的名字,其实说白了就是一个匿名函数,它没有函数名,没有访问修饰符,只是用一个表达式来代表一个函数。在Java8中你可以把一个函数(Lambda表达式)作为一个方法的参数传递进去(很奇怪?下面会说)。
语法:
1 2 3 |
(parameters) -> expression 或 (parameters) ->{ statements; } |
圆括号里代表的是参数,花括号里是函数体(返回),->在数学上的解释是由A推导可以得到B。Lambda表达式的语法很简单,具体细则就不说了,参考以个几个例子就可以很好的理解了:
1 2 3 4 5 6 |
//参数没有指定类型,函数体不需要返回修饰符 (a, b) -> a + b //指定参数类型,函数体是语句 (int a, int b) -> { return a + b; } //没有参数 () -> System.out.println("Hello World"); |
这几段极其简洁的代码只是为了说明Lambda表达式的语法规则,而更实际地,在日常的编程工作中Lambda表达式被广泛应用在内部匿名类的简化上,接下来举两个最常见的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
//开启一个新的线程 //旧方法: new Thread(new Runnable() { @Override public void run() { System.out.println("New Thread Starts to Run"); } }).start(); //新方法: new Thread( () -> System.out.println("New Thread Starts to Run") ).start(); //排序 //旧方法 Collections.sort(personList,new Comparator(){ @Override public int compare(Object o1, Object o2) { Person p1 = (Person)o1; Person p2 = (Person)o2; return p2.getAge() - p1.getAge(); } }); //新方法 personList.sort( ( e1, e2 ) -> p2.getAge() - p1.getAge() ); |
通过新老写法的对比可以很容易地发现Lambda表达式极大的简化了代码,很优雅的替掉匿名内部类的写法。大致知道了Lambda表达式的用处之后,我们再进一步深入地思考两个问题:
1、Lambda表达式是如何被执行的?
2、什么情况下可以使用Lambda表达式?为什么Thread类和Collections.sort类可以接受Lambda表达式?
要解决这两个问题就要引入另一个概念:函数式接口(Functional Interface),也被称为:Single Abstract Method Interface。
函数式接口的定义如下:顾名思义,只有一个抽象方法和多个默认或者静态方法的接口被称为函数式接口,重点是只有一个抽象方法。它可以显式地使用注解@FunctionalInterface来声明,当然也可以不加注解。但是建议还是要加上的,因为如果不加注解,别人可能不认为这是个函数式接口,一旦添加新的方法,就会导致编译报错接口失效。
函数式接口的提出主要是为了使现有的方法更好地支持Lambda表达式,因为在现有的Java语法规则下,一个方法的参数是不能接受函数的,如果新加一种类型的参数,那么很多以前的方法都要重写!所以提出了函数式接口这么个概念,只要是函数式接口作为参数的,都会被认为是Lambda表达式,也就是说函数式接口可以被隐式地转换为Lambda表达式。讲起来比较抽象,我们来看个例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
public class Test { public static void main(String[] args) { // TODO Auto-generated method stub Test test = new Test(); int res = test.calculate(1, 2, (a, b) -> a + b); System.out.print(res); } @FunctionalInterface interface SimpleMath{ int doMath(int a, int b); } private int calculate(int a, int b, SimpleMath sm){ return sm.doMath(a, b); } } |
看到这里可以发现Lambda表达式被隐式地转换成了接口去执行,真正调用的地方是sm.doMath(),编译器则会把这个方法的执行转为对Lambda表达式的执行。对方法calculate而言这和接受一个普通的接口参数没任何区别,这样的话Java8之前的大多数代码几乎不用修改就可以支持Lambda表达式,这一点真的非常巧妙!所以只要一个方法接受函数式接口作为参数,则它就能接受Lambda表达式。这样我们自己就可以写一个方法来使用Lambda表达式了,但是那你可能会想,如果这样的方法很多,岂不是每一个Lambda表达式都要写一个函数式接口?其实如果你仔细思考下就会发现,Lambda表达式只关注参数的一致性以及返回类型,所以一般情况下肯定有很多函数式接口在功能上是重复的,因此Java8新增了一个函数接口:java.util.function用来更方便地支持函数式编程,这个接口下面包括了很多常用的接口,比如:
接口 | 说明 |
---|---|
Function | 接受一个输入参数,返回一个结果 |
Predicate | 接受一个输入参数,返回一个布尔值结果。 |
Supplier | 无参数,返回一个结果 |
Consumer | 接受一个输入参数并且无返回的操作 |
更细分地包括了对具体数据类型的支持,比如IntSupplier,无参数,返回int类型,这里就不再一一举例了。下面举个例子看一下function接口的使用,以Predicate为例。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
public static void main(String[] args) { // TODO Auto-generated method stub Java8Test jt = new Java8Test(); jt.filter(Arrays.asList(1,2,3,4,5,6,7), n -> n > 3); } private void filter( List<Integer> list, Predicate<Integer> p ){ list.forEach( e -> { //测试是否通过,隐藏了具体判断逻辑,由外部的Lambda表达式确定 if(p.test(e)){ System.out.println(e); } }); } |
Lambda表达式的变量作用域
Lambda 表达式只能引用 final 或 final 局部变量,这就是说不能在 Lambda 内部修改定义在域外的变量,否则会编译错误。很多文章介绍到了这一点,但并没有解释为什么。其实很简单,因为Lambda表达式本质上是匿名内部函数,所以问题就等价于为什么Java的匿名内部类只能引用final和final局部变量?这源于Java的资源管理方式,当你创建一个内部类的实例时,它所使用的变量都会被拷贝一份。如果这个变量来自外部并且可以被更改,那么就需要同步这两个变量来避免数据不一致的情况,所以Java作了这样的限制。
方法引用(Method Reference)
方法引用使用一对冒号(::)通过方法的名字来指向一个方法,与Lambda表达式结合起来使用可以使语言的构造更紧凑简洁,减少冗余代码。它包括以下几种情况:
1、构造器引用:它的语法是Class::new,或者更一般的Class< T >::new,比如:
ArrayList::new等价于() -> new ArrayList()
2、静态方法引用:它的语法是Class::static_method,实例如下:
Integer::parseInt等价于(x) -> Ingeter.parseInt(x)
3、特定类的任意对象的方法引用:它的语法是Class::method,这个其实还是这个类的实例对象调用了该方法
4、特定对象的方法引用:它的语法是instance::method,这个是实例调用方法
方法引用其实是Lambda表达式的另一种形式,如果我们要写的Lambda表达式是一个已经存在的方法,那么可以直接通过方法引用来实现。
接口的默认方法和静态方法
我们都知道以前Java里面的接口是不能有方法的具体实现的,只能提供方法名,并且实现类必须全部实现接口中的方法。但是现在,Java对这一限制作了一定的开放:允许在接口里面添加具体的方法,包括:默认方法(default method)和静态方法(static method)。默认方法的语法规则是在方法名前加上default关键字,默认方法和抽象方法不同的是:它不要求实现类一定要实现这个方法。在一个接口里面可以有多个默认方法或者静态方法。如果继承了多个接口,默认方法冲突了该怎么办呢?可以重写一个自己的默认方法,还可以通过super关键字来指定调用哪个接口的默认方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
public interface A{ default void print(){ System.out.println("print A"); } //多个默认方法 default void print2(){ System.out.println("print2 A"); } static void print3(){ System.out.println("print3 A"); } //多个静态方法 static void print4(){ System.out.println("print4 A"); } } public interface B{ default void print(){ System.out.println("print B"); } } public class C implements A, B{ public void print(){ //冲突后,使用A.super指定使用A的默认方法 A.super.print(); //或者自己实现 //System.out.println("print C"); } } |
那么为什么要引入这个功能呢?
试想一下在这之前如果要对某一个接口进行修改,那么它所有的实现类都要被修改。特别是在Java8引入了Lambda表达式之后,很多集合类库都需要扩展,这样重构的代价太大。所以为了更方便地对接口进行扩展,尽量不破坏实现类的代码,Java8引入了这样的功能。举个例子,比如Java8在Iterable接口中新添加了foreach方法:
1 2 3 4 5 6 |
default void forEach(Consumer<? super T> action) { Objects.requireNonNull(action); for (T t : this) { action.accept(t); } } |
有了这个功能之后,我们就可以在Collection类中使用forEach方法了,以ArrayList为例:
1 |
Arrays.asList( "a", "b", "d" ).forEach( e -> System.out.println( e ) ); |
Stream API
可以毫不夸张的说,之前介绍的Lambda表达式的作用只是花拳绣腿,只有在结合Stream之后才能爆发出他强大的威力,能极大地提高我们的生产力。Stream API主要关注于集合类数据的处理,而我们日常编程中对集合类的使用实在是太频繁了,因此Stream的引入对Java程序员的影响很大,它可以帮助我们写出简洁,干净,高效的代码。下面我们来简单认识下Stream,其实要说清楚Stream这个概念并不容易,这里引用一下写的比较好的解释:
Stream 不是集合元素,它不是数据结构并不保存数据,它是有关算法和计算的,它更像一个高级版本的 Iterator。原始版本的 Iterator,用户只能显式地一个一个遍历元素并对其执行某些操作;高级版本的 Stream,用户只要给出需要对其包含的元素执行什么操作,比如 “过滤掉长度大于 10 的字符串”、“获取每个字符串的首字母”等,Stream 会隐式地在内部进行遍历,做出相应的数据转换。Stream 就如同一个迭代器(Iterator),单向,不可往复,数据只能遍历一次,遍历过一次后即用尽了,就好比流水从面前流过,一去不复返。而和迭代器又不同的是,Stream 可以并行化操作,迭代器只能命令式地、串行化操作。顾名思义,当使用串行方式去遍历时,每个 item 读完后再读下一个 item。而使用并行去遍历时,数据会被分成多个段,其中每一个都在不同的线程中处理,然后将结果一起输出。
个人理解下来这非常类似于linux中的管道操作,在处理数据时当前的操作结果可以作为下一步的输入,程序员不需要额外保存数据再进行下一步处理。举个例子:
1 2 3 4 5 6 |
Arrays.asList(1,2,3,4,5,6,7) .stream() .filter( e -> e > 3) .sorted( (e1, e2) -> e2 - e1) .map(e -> "this is:"+e) .forEach(System.out::println); |
通过这段代码我们来了解下Stream的几个部分,一个Stream一般分为三个部分:源头(source),中间操作(Intermediate Operation)和终结操作(Terminal Operation)。过程如下:
获取一个数据源(source)→ 数据转换(中间操作) → 执行操作(终结操作)获取想要的结果,每次中间操作转换原有 Stream 对象不改变,返回一个新的 Stream 对象(可以有多次转换),这就允许对其操作可以像链条一样排列,变成一个管道。
Stream接受多种形式的输入作为source,最常见的就是Collection和数组,比如上例代码中就使用ArrayList作为Stream的源头。
中间操作包括:map、 filter、 distinct、 sorted、 peek、 limit等
终结操作包括:forEach、 toArray、 reduce、 collect、 max、 count等。具体的使用示例不再赘述,参考官方API即可。
内部迭代和外部迭代
外部迭代一般指的是通过for,while或者Iterator来对集合元素进行遍历的行为,比如:
1 2 3 4 |
List subjects = asList(new Subject("Physics"), new Subject("Chemistry"), new Subject("Math")); for (Subject s : subjects) { s.setCatalogy("Science"); } |
这种方式是按照固定的顺序进行遍历的,但是我们知道这段代码的执行结果跟遍历的顺序没有关系,那么在如今多核当道的时代,我们能不能并行遍历,分给多个线程去执行以提高效率呢?很明显,在内部迭代出现之前只能自己改代码,不仅麻烦,风险也高。因此我们可以借助Lambda表达式和forEach方法来实现内部迭代:
1 |
subjects.forEach( e -> e.setCatalogy("Science")); |
内部迭代用户只需要关心做什么事情即可,而具体怎么做则交给了JVM。这样一来jvm对迭代就有了优化的可能,可能是通过乱序,并发等方式来进行的。
顺序流与并行流
顾名思义,顺序流就是顺序遍历,而并行流则是并发遍历,通过stream.parallel来实现,值得注意的是并行流不能保证输出的顺序,使用时要小心。
1 2 3 4 5 6 |
Arrays.asList(1,2,3,4,5,6,7) .stream().parallel(). filter( e -> e > 3) .sorted( (e1, e2) -> e2 - e1) .map(e -> "this is:"+e) .forEach(System.out::println); |
输出:
this is:6
this is:4
this is:7
this is:5
这里稍微提及一下并行流的实现原理,其实就是一个mapReduce模型,先把任务分割给不同的线程,计算完毕之后再整合起来。