java 8——Lambda表达式

来源:互联网 发布:深圳美生创谷淘宝地址 编辑:程序博客网 时间:2024/06/05 20:50

整理自http://www.cnblogs.com/WJ5888/p/4667086.html


二、Lambda表达式

Lambda表达式的目的是:

Java 8中每一个Lambda表达式必须有一个函数式接口与之对应。

利用函数式的写法对parameters执行expression中的操作,其整体是一个函数式接口的对象,parameters为函数式接口唯一抽象方法的形参,且类型可以省略,编译器自动识别类型,expression为函数式接口唯一抽象方法的内部实现。也常用来代替匿名内部类的冗杂语法。

1.1 引言

课本上说编程有两种模式,面向过程的编程以及面向对象的编程,其实在面向对象编程之前还出现了面向函数的编程(函数式编程,以前一直被忽略、不被重视,现在从学术界已经走向了商业界,对函数编程语言的支持目前有ScalaErlangF#PythonPhpJavaJavascript等,有人说他将会是编程语言中的下一个主流...

1.2 Lambda表达式

为什么需要Lambda表达式?

1.使用Lambda表达式可以使代码变的更加紧凑,例如在Java中实现一个线程,只输出一个字符串Hello World!,我们的代码如下所示:

复制代码
public static void main(String[] args) throws Exception {    new Thread(new Runnable() {        @Override        public void run() {            System.out.println("Hello World!");        }    }).start();    TimeUnit.SECONDS.sleep(1000);}
复制代码

使用Lambda表达式之后代码变成如下形式:

public static void main(String[] args) throws Exception {    new Thread(() -> System.out.println("Hello World!")).start();    TimeUnit.SECONDS.sleep(1000);}

是不是代码变的更紧凑了~,其他的例如各种监听器,以及事件处理器等都可以用这种方式进行简化。

2.修改方法的能力,其实说白了,就是函数中可以接受以函数为单元的参数,在C/C++中就是函数指针,在Java中就是Lambda表达式,例如在Java中使用集合类对一个字符串按字典序列进行排序,代码如下所示:

public static void main(String[] args) {    String []datas = new String[] {"peng","zhao","li"};    Arrays.sort(datas);    Stream.of(datas).forEach(param ->     System.out.println(param));}

在上面代码中用了Arrays里的sort方法,现在我们不需要按字典排序,而是按字符串的长度进行排序,代码如下所示:

public static void main(String[] args) {    String []datas = new String[] {"peng","zhao","li"};    Arrays.sort(datas,(v1 , v2) -> Integer.compare(v1.length(), v2.length()));    Stream.of(datas).forEach(param -> System.out.println(param));}

是不是很方便,我们不需要实现Comparable接口,使用一个Lambda表达式就可以改变一个函数的形为~

1.3 Syntax

1.Lambda表达式的形式化表示如下所示

Parameters -> an expression 

2.如果Lambda表达式中要执行多个语句块,需要将多个语句块以{}进行包装,如果有返回值,需要显示指定return语句,如下所示:

Parameters -> {expressions;};

3.如果Lambda表达式不需要参数,可以使用一个空括号表示,如下示例所示

() -> {for (int i = 0; i < 1000; i++) doSomething();};

4.Java是一个强类型的语言,因此参数必须要有类型,如果编译器能够推测出Lambda表达式的参数类型,则不需要我们显示的进行指定,如下所示,在Java中推测Lambda表达式的参数类型与推测泛型类型的方法基本类似,至于Java是如何处理泛型的,此处略去

String []datas = new String[] {"peng","zhao","li"};Arrays.sort(datas,(String v1, String v2) -> Integer.compare(v1.length(), v2.length()));

上述代码中 显示指定了参数类型Stirng,其实不指定,如下代码所示,也是可以的,因为编译器会根据Lambda表达式对应的函数式接口Comparator<String>进行自动推断

String []datas = new String[] {"peng","zhao","li"};;Arrays.sort(datas,(v1, v2) -> Integer.compare(v1.length(), v2.length()));

5.如果Lambda表达式只有一个参数,并且参数的类型是可以由编译器推断出来的,则可以如下所示使用Lambda表达式,即可以省略参数的类型及括号

Stream.of(datas).forEach(param -> {System.out.println(param.length());});

6.Lambda表达式的返回类型,无需指定,编译器会自行推断,说是自行推断

7.Lambda表达式的参数可以使用修饰符及注解,如final@NonNull等 

1.4函数式接口

函数式接口是Java 8为支持Lambda表达式新发明的,在上面讲述的Lambda Syntax时提到的sort排序方法就是一个样例,在这个排序方法中就使用了一个函数式接口,函数的原型声明如下所示

public static <T> void sort(T[] a, Comparator<? super T> c)

上面代码中Comparator<? Super T>就是一个函数式接口,? Super T or ? entends TJava 5支持泛型时开始引入,得理解清楚,在此忽略讲述

什么是函数式接口

1.函数式接口具有两个主要特征,是一个接口,这个接口具有唯一的一个抽像方法,我们将满足这两个特性的接口称为函数式接口,说到这,就不得不说一下接口中是有具体实现这个问题啦~

2.Lambda表达式不能脱离目标类型存在,这个目录类型就是函数式接口,所下所示是一个样例

String []datas = new String[] {"peng","zhao","li"};Comparator<String> comp = (v1,v2) -> Integer.compare(v1.length(), v2.length());Arrays.sort(datas,comp);Stream.of(datas).forEach(param -> {System.out.println(param);}); 

Lambda表达式被赋值给了comp函数接口变量

3.函数式接口可以使用@FunctionalInterface进行标注,使用这个标注后,主要有两个优势,编译器知道这是一个函数式接口,符合函数式的要求,另一个就是生成Java Doc时会进行显式标注

4.异常,如果Lambda表达式会抛出非运行时异常,则函数式接口也需要抛出异常,说白了,还是一句话,函数式接口是Lambda表达式的目标类型

5.函数式接口中可以定义public static方法,想想在Java中我们提供了Collection接口,同时还提供了一个Collections工具类等等,在Java中将这种Collections的实现转移到了接口里面,但是为了保证向后兼容性,以前的这种Collection/Collections等逻辑均未改变

6.函数式接口可以提供多个抽像方法,纳尼!上面不是说只能有一个嘛?是的,在函数式接口中可以提供多个抽像方法,但这些抽像方法限制了范围,只能是Object类型里的已有方法,为什么要这样做呢?此处忽略,大家可以自已研究

7.函数式接口里面可以定义方法的默认实现,如下所示是Predicate类的代码,不仅可以提供一个default实现,而且可以提供多个default实现呢,Java 8以前可以嘛?我和我的小伙伴们都惊呆了,这也就导致出现了多继承下的问题,想知道Java 8是如何对其进行处理的嘛,其实很Easy,后面我会再讲~

8.为什么要提供default接口的实现?如下就是一个默认实现

default Predicate<T> or(Predicate<? super T> other) {     Objects.requireNonNull(other);     return (t) -> test(t) || other.test(t);}

Java 8中在接口中增加了默认实现这种函数,其实在很大程序上违背了接口具有抽象这种特征的,增加default实现主要原因是因为考虑兼容及代码的更改成本,例如,在Java 8中向iterator这种接口增加一个方法,那么实现这个接口的所有类都要需实现一遍这个方法,那么Java 8需要更改的类就太多的,因此在Iterator接口里增加一个default实现,那么实现这个接口的所有类就都具有了这种实现,说白了,就是一个模板设计模式吧

为了方便使用Lambda表达式,Lambda表达式通常也配合Stream API使用,Stream接口定义的方法中的形参大量使用了函数接口,如Function,Consumer,Supplier,Predicate等等,每个函数接口的抽象方法只有返回值或者形参不同。collcet方法中的Collector接口不是函数接口。


1.5方法引用 

有时,我们需要执行的代码在某些类中已经存在,这时我们没必要再去写Lambda表达式,可以直接使用该方法,这种情况我们称之为方法引用,如下所示,未采用方法引用前的代码

如下所示

Stream.of(datas).forEach(param -> {System.out.println(param);});

使用方法引用后的代码如下所示

Stream.of(datas).forEach(System.out::println);

以上示例使用的是out对象,下面示例使用的是类的静态方法引用对字符串数组里的元素忽略大小写进行排序

String []datas = new String[] {"peng","Zhao","li"};Arrays.sort(datas,String::compareToIgnoreCase);Stream.of(datas).forEach(System.out::println);

上面就是方法引用的一些典型示例

方法引用的具体分类

Object:instanceMethodClass:staticMethodClass:instanceMethod

上面分类中前两种在Lambda表达式的意义上等同,都是将参数传递给方法,如上示例

System.out::println == x -> System.out.println(x)

最后一种分类,第一个参数是方法执行的目标,如下示例 

String::compareToIgnoreCase == (x,y) ->     x.compareToIgnoreCase(y)

还有类似于super::instanceMethod这种方法引用本质上与Object::instanceMethod类似

1.6构造方法引用

构造方法引用与方法引用类似,除了一点,就是构造方法引用的方法是new!以下是两个示例

示例一:

String str = "test";Stream.of(str).map(String::new).peek(System.out::println).findFirst();

示例二:

String []copyDatas = Stream.of(datas).toArray(String[]::new);Stream.of(copyDatas).forEach(x -> System.out.println(x));

总结一下,构造方法引用有两种形式

Class::newClass[]::new

1.7 Lambda表达式作用域

总体来说,Lambda表达式的变量作用域与内部类非常相似,只是条件相对来说,放宽了些以前内部类要想引用外部类的变量,必须像下面这样

复制代码
final String[] datas = new String[] { "peng", "Zhao", "li" };new Thread(new Runnable() {    @Override    public void run() {        System.out.println(datas);    }}).start();
复制代码

将变量声明为final类型的,现在在Java 8中可以这样写代码

复制代码
String []datas = new String[] {"peng","Zhao","li"};new Thread(new Runnable() {    @Override    public void run() {        System.out.println(datas);    }}).start();
复制代码

也可以这样写

new Thread(() -> System.out.println(datas)).start();

总之你爱怎么写,就怎么写吧,I don’t Care it!

看了上面的两段代码,能够发现一个显著的不同,就是Java 8中内部类或者Lambda表达式对外部类变量的引用条件放松了,不要求强制的加上final关键字了,但是Java 8中要求这个变量是effectively final

What is effectively final?

Effectively final就是有效只读变量,意思是这个变量可以不加final关键字,但是这个变量必须是只读变量,即一旦定义后,在后面就不能再随意修改,如下代码会编译出错

String []datas = new String[] {"peng","Zhao","li"};datas = null;new Thread(() -> System.out.println(datas)).start();

Java中内部类以及Lambda表达式中也不允许修改外部类中的变量,这是为了避免多线程情况下的race condition

Lambda中变量以及this关键字

Lambda中定义的变量与外部类中的变量作用域相同,即外部类中定义了,Lambda就不能再重复定义了,同时在Lambda表达式使用的this关键字,指向的是外部类,大家可以自行实践下,此处略


Lambda实现原理分析

为了支持函数式编程,Java 8引入了Lambda表达式,那么在Java 8中到底是如何实现Lambda表达式的呢? Lambda表达式经过编译之后,到底会生成什么东西呢在没有深入分析前,让我们先想一想,Java 8中每一个Lambda表达式必须有一个函数式接口与之对应,函数式接口与普通接口的区别,可以参考前面的内容,那么你或许在想Lambda表达式是不是转化成与之对应的函数式接口的一个实现类呢,然后通过多态的方式调用子类的实现呢,如下面代码是一个Lambda表达式的样例

复制代码
@FunctionalInterfaceinterface Print<T> {    public void print(T x);}public class Lambda {       public static void PrintString(String s, Print<String> print) {        print.print(s);    }    public static void main(String[] args) {        PrintString("test", (x) -> System.out.println(x));    }}
复制代码

按照上面的分析,理论上经过编译器处理后,最终生成的代码应该如下面所示:

复制代码
@FunctionalInterfaceinterface Print<T> {    public void print(T x);}class Lambda$$0 implements Print<String> {    @Override    public void print(String x) {        System.out.println(x);    }}public class Lambda {       public static void PrintString(String s,             Print<String> print) {        print.print(s);    }    public static void main(String[] args) {        PrintString("test", new Lambda$$0());    }}
复制代码

再或者是一个内部类实现,代码如下所示:

复制代码
@FunctionalInterfaceinterface Print<T> {    public void print(T x);}public class Lambda {       final class Lambda$$0 implements Print<String> {        @Override        public void print(String x) {            System.out.println(x);        }    }      public static void PrintString(String s,             Print<String> print) {        print.print(s);    }     public static void main(String[] args) {        PrintString("test", new Lambda().new Lambda$$0());    }}
复制代码

异或是这种匿名内部类实现,代码如下所示:

复制代码
@FunctionalInterfaceinterface Print<T> {    public void print(T x);}public class Lambda {       public static void PrintString(String s,             Print<String> print) {        print.print(s);    }    public static void main(String[] args) {        PrintString("test", new Print<String>() {            @Override            public void print(String x) {                System.out.println(x);            }        });    }}
复制代码

上面的代码,除了在代码长度上长了点外,与用Lambda表达式实现的代码运行结果是一样的,那么Java 8到底是用什么方式实现的呢是不是上面三种实现方式中的一种呢,你也许觉的自已想的是对的,其实本来也就是对的,在Java 8中采用的是内部类来实现Lambda表达式。

补充

1.1lambda表达式语法

1.1.1lambda表达式的一般语法

(Type1 param1, Type2 param2, ..., TypeN paramN) -> {  statment1;  statment2;  //.............  return statmentM;}

这是lambda表达式的完全式语法,后面几种语法是对它的简化。

1.1.2单参数语法

param1 -> {  statment1;  statment2;  //.............  return statmentM;}

当lambda表达式的参数个数只有一个,可以省略小括号

例如:将列表中的字符串转换为全小写

List<String> proNames = Arrays.asList(new String[]{"Ni","Hao","Lambda"});
List<String> lowercaseNames1 = proNames.stream().map(name -> {return name.toLowerCase();}).collect(Collectors.toList());

1.1.3单语句写法

param1 -> statment

当lambda表达式只包含一条语句时,可以省略大括号、return和语句结尾的分号

例如:将列表中的字符串转换为全小写

List<String> proNames = Arrays.asList(new String[]{"Ni","Hao","Lambda"});

List<String> lowercaseNames2 = proNames.stream().map(name -> name.toLowerCase()).collect(Collectors.toList());

1.1.4方法引用写法

(方法引用和lambda一样是Java8新语言特性,后面会讲到)

Class or instance :: method

例如:将列表中的字符串转换为全小写

List<String> proNames = Arrays.asList(new String[]{"Ni","Hao","Lambda"});

List<String> lowercaseNames3 = proNames.stream().map(String::toLowerCase).collect(Collectors.toList());

1.2lambda表达式可使用的变量

先举例:

//将为列表中的字符串添加前缀字符串
String waibu = "lambda :";
List<String> proStrs = Arrays.asList(new String[]{"Ni","Hao","Lambda"});
List<String>execStrs = proStrs.stream().map(chuandi -> {
Long zidingyi = System.currentTimeMillis();
return waibu + chuandi + " -----:" + zidingyi;
}).collect(Collectors.toList());
execStrs.forEach(System.out::println);

输出:

lambda :Ni -----:1474622341604
lambda :Hao -----:1474622341604
lambda :Lambda -----:1474622341604

 

变量waibu :外部变量

变量chuandi :传递变量

变量zidingyi :内部自定义变量

 

lambda表达式可以访问给它传递的变量,访问自己内部定义的变量,同时也能访问它外部的变量。

不过lambda表达式访问外部变量有一个非常重要的限制:变量不可变(只是引用不可变,而不是真正的不可变)。

当在表达式内部修改waibu = waibu + " ";时,IDE就会提示你:

Local variable waibu defined in an enclosing scope must be final or effectively final

编译时会报错。因为变量waibu被lambda表达式引用,所以编译器会隐式的把其当成final来处理。

以前Java的匿名内部类在访问外部变量的时候,外部变量必须用final修饰。现在java8对这个限制做了优化,可以不用显示使用final修饰,但是编译器隐式当成final来处理。

1.3lambda表达式中的this概念

在lambda中,this不是指向lambda表达式产生的那个SAM对象,而是声明它的外部对象。

例如:

public class WhatThis {

     public void whatThis(){
           //转全小写
           List<String> proStrs = Arrays.asList(new String[]{"Ni","Hao","Lambda"});
           List<String> execStrs = proStrs.stream().map(str -> {
                 System.out.println(this.getClass().getName());
                 return str.toLowerCase();
           }).collect(Collectors.toList());
           execStrs.forEach(System.out::println);
     }

     public static void main(String[] args) {
           WhatThis wt = new WhatThis();
           wt.whatThis();
     }
}

输出:

com.wzg.test.WhatThis
com.wzg.test.WhatThis
com.wzg.test.WhatThis
ni
hao
lambda



原创粉丝点击