zoukankan      html  css  js  c++  java
  • [转]Java 8新特性探究(lambda)

    原文地址:http://my.oschina.net/benhaile/blog/175012

    函数式接口

    函数式接口(functional interface 也叫功能性接口,其实是同一个东西)。简单来说,函数式接口是只包含一个方法的接口。比如Java标准库中的java.lang.Runnable和 java.util.Comparator都是典型的函数式接口。java 8提供 @FunctionalInterface作为注解,这个注解是非必须的,只要接口符合函数式接口的标准(即只包含一个方法的接口),虚拟机会自动判断, 但 最好在接口上使用注解@FunctionalInterface进行声明,以免团队的其他人员错误地往接口中添加新的方法。
    Java中的lambda无法单独出现,它需要一个函数式接口来盛放,lambda表达式方法体其实就是函数接口的实现,下面讲到语法会讲到

    Lambda语法

    包含三个部分

    1. 一个括号内用逗号分隔的形式参数,参数是函数式接口里面方法的参数

    2. 一个箭头符号:->

    3. 方法体,可以是表达式和代码块,方法体函数式接口里面方法的实现,如果是代码块,则必须用{}来包裹起来,且需要一个return 返回值,但有个例外,若函数式接口里面方法返回值是void,则无需{}

    总体看起来像这样

    1
    (parameters) -> expression 或者 (parameters) -> { statements; }

    看一个完整的例子,方便理解

    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
    /**
     * 测试lambda表达式
     *
     * @author benhail
     */
    public class TestLambda {
     
        public static void runThreadUseLambda() {
            //Runnable是一个函数接口,只包含了有个无参数的,返回void的run方法;
            //所以lambda表达式左边没有参数,右边也没有return,只是单纯的打印一句话
            new Thread(() ->System.out.println("lambda实现的线程")).start(); 
        }
     
        public static void runThreadUseInnerClass() {
            //这种方式就不多讲了,以前旧版本比较常见的做法
            new Thread(new Runnable() {
                @Override
                public void run() {
                    System.out.println("内部类实现的线程");
                }
            }).start();
        }
     
        public static void main(String[] args) {
            TestLambda.runThreadUseLambda();
            TestLambda.runThreadUseInnerClass();
        }
    }

    可以看出,使用lambda表达式设计的代码会更加简洁,而且还可读。

    方法引用

    其实是lambda表达式的一个简化写法,所引用的方法其实是lambda表达式的方法体实现,语法也很简单,左边是容器(可以是类名,实例名),中间是"::",右边是相应的方法名。如下所示:

    1
    ObjectReference::methodName

    一般方法的引用格式是

    1. 如果是静态方法,则是ClassName::methodName。如 Object ::equals

    2. 如果是实例方法,则是Instance::methodName。如Object obj=new Object();obj::equals;

    3. 构造函数.则是ClassName::new

    再来看一个完整的例子,方便理解

    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
    34
    35
    36
    37
    38
    39
    import java.awt.FlowLayout;
    import java.awt.event.ActionEvent;
    import javax.swing.JButton;
    import javax.swing.JFrame;
     
    /**
     *
     * @author benhail
     */
    public class TestMethodReference {
     
        public static void main(String[] args) {
     
            JFrame frame = new JFrame();
            frame.setLayout(new FlowLayout());
            frame.setVisible(true);
             
            JButton button1 = new JButton("点我!");
            JButton button2 = new JButton("也点我!");
             
            frame.getContentPane().add(button1);
            frame.getContentPane().add(button2);
            //这里addActionListener方法的参数是ActionListener,是一个函数式接口
            //使用lambda表达式方式
            button1.addActionListener(e -> { System.out.println("这里是Lambda实现方式"); });
            //使用方法引用方式
            button2.addActionListener(TestMethodReference::doSomething);
             
        }
        /**
         * 这里是函数式接口ActionListener的实现方法
         * @param e 
         */
        public static void doSomething(ActionEvent e) {
             
            System.out.println("这里是方法引用实现方式");
             
        }
    }

    可以看出,doSomething方法就是lambda表达式的实现,这样的好处就是,如果你觉得lambda的方法体会很长,影响代码可读性,方法引用就是个解决办法

    总结

    以上就是lambda表达式语法的全部内容了,相信大家对lambda表达式都 有一定的理解了,但只是代码简洁了这个好处的话,并不能打动很多观众,java 8也不会这么令人期待,其实java 8引入lambda迫切需求是因为lambda 表达式能简化集合上数据的多线程或者多核的处理,提供更快的集合处理速度 ,这个后续会讲到,关于JEP126的这一特性,将分3部分,之所以分开,是因为这一特性可写的东西太多了,这部分让读者熟悉lambda表达式以及方法 引用的语法和概念,第二部分则是虚拟扩展方法(default method)的内容,最后一部分则是大数据集合的处理,解开lambda表达式的最强作用的神秘面纱。敬请期待。。。。

    上篇讲了 lambda表达式的语法,但只是 JEP126 特性的一部分,另一部分就是默认方法(也称为虚拟扩展方法或防护方法)

    什么是默认方法,为什么要有默认方法

    简单说,就是接口可以有实现方法,而且不需要实现类去实现其方法。只需在方法名前面加个default关键字即可。

    为什么要有这个特性?首先,之前的接口是个双刃剑,好处是面向抽象而不是面向具体编程,缺陷是,当需要修改接口时候,需要修改全部实现该接口的类,目前的 java 8之前的集合框架没有foreach方法,通常能想到的解决办法是在JDK里给相关的接口添加新的方法及实现。然而,对于已经发布的版本,是没法在给接口 添加新方法的同时不影响已有的实现。所以引进的默认方法。他们的目的是为了解决接口的修改与现有的实现不兼容的问题。

    简单的例子

    一个接口A,Clazz类实现了接口A。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    public interface A {
        default void foo(){
           System.out.println("Calling A.foo()");
        }
    }
       
    public class Clazz implements A {
        public static void main(String[] args){
           Clazz clazz = new Clazz();
           clazz.foo();//调用A.foo()
        }
    }

    代码是可以编译的,即使Clazz类并没有实现foo()方法。在接口A中提供了foo()方法的默认实现。

    java 8抽象类与接口对比

    这一个功能特性出来后,很多同学都反应了,java 8的接口都有实现方法了,跟抽象类还有什么区别?其实还是有的,请看下表对比。。

    相同点 不同点

    1.都是抽象类型;

    2.都可以有实现方法(以前接口不行);

    3.都可以不需要实现类或者继承者去实现所有方法,(以前不行,现在接口中默认方法不需要实现者实现)

    1.抽象类不可以多重继承,接口可以(无论是多重类型继承还是多重行为继承);

    2.抽象类和接口所反映出的设计理念不同。其实抽象类表示的是"is-a"关系,接口表示的是"like-a"关系;

    3.接口中定义的变量默认是public static final 型,且必须给其初值,所以实现类中不能改变其值;抽象类中的变量默认是 friendly 型,其值可以在子类中重新定义,也可以重新赋值。 

    多重继承的冲突说明

    由于同一个方法可以从不同接口引入,自然而然的会有冲突的现象,默认方法判断冲突的规则如下:

    1.一个声明在类里面的方法优先于任何默认方法(classes always win)

    2.否则,则会优先选取最具体的实现,比如下面的例子 B重写了A的hello方法。

    输出结果是:Hello World from B

    如果想调用A的默认函数,则用到新语法X.super.m(...),下面修改C类,实现A接口,重写一个hello方法,如下所示:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    public class implements A{
        
        @Override
        public void hello(){
            A.super.hello();
        }
         
        public static void main(String[] args){
            new C().hello();
        }
    }

    输出结果是:Hello World from A

    总结

    默认方法给予我们修改接口而不破坏原来的实现类的结构提供了便利,目前java 8的集合框架已经大量使用了默认方法来改进了,当我们最终开始使用Java 8的lambdas表达式时,提供给我们一个平滑的过渡体验。也许将来我们会在API设计中看到更多的默认方法的应用。

    我们期待了很久lambda为java带来闭包的概 念,但是如果我们不在集合中使用它的话,就损失了很大价值。现有接口迁移成为lambda风格的问题已经通过default methods解决了,在这篇文章将深入解析Java集合里面的批量数据操作(bulk operation),解开lambda最强作用的神秘面纱。

    1.关于JSR335

    JSR是Java Specification Requests的缩写,意思是Java 规范请求,Java 8 版本的主要改进是 Lambda 项目(JSR 335),其目的是使 Java 更易于为多核处理器编写代码。JSR 335=lambda表达式+接口改进(默认方法)+批量数据操作。加上前面两篇,我们已是完整的学习了JSR335的相关内容了。

    2.外部VS内部迭代

    以前Java集合是不能够表达内部迭代的,而只提供了一种外部迭代的方式,也就是for或者while循环。

    1
    2
    3
    4
    List persons = asList(new Person("Joe"), new Person("Jim"), new Person("John"));
    for (Person p :  persons) {
       p.setLastName("Doe");
    }

    上面的例子是我们以前的做法,也就是所谓的外部迭代,循环是固定的顺序循环。在现在多核的时代,如果我们想并行循环,不得不修改以上代码。效率能有多大提升还说定,且会带来一定的风险(线程安全问题等等)。
    要描述内部迭代,我们需要用到Lambda这样的类库,下面利用lambda和Collection.forEach重写上面的循环

    1
    persons.forEach(p->p.setLastName("Doe"));

    现在是由jdk 库来控制循环了,我们不需要关心last name是怎么被设置到每一个person对象里面去的,库可以根据运行环境来决定怎么做,并行,乱序或者懒加载方式。这就是内部迭代,客户端将行为p.setLastName当做数据传入api里面。

    内部迭代其实和集合的批量操作并没有密切的联系,借助它我们感受到语法表达上的变化。真正有意思的和批量操作相关的是新的流(stream)API。新的java.util.stream包已经添加进JDK 8了。

    3.Stream API

    流(Stream)仅仅代表着数据流,并没有数据结构,所以他遍历完一次之后便再也无法遍历(这点在编程时候需要注意,不像Collection,遍历多少次里面都还有数据),它的来源可以是Collection、array、io等等。

    3.1中间与终点方法

    流作用是提供了一种操作大数据接口,让数据操作更容易和更快。它具有过滤、映射以及减少遍历数等方法,这些方法分两种:中间方法和终端方法,“流”抽象天 生就该是持续的,中间方法永远返回的是Stream,因此如果我们要获取最终结果的话,必须使用终点操作才能收集流产生的最终结果。区分这两个方法是看他 的返回值,如果是Stream则是中间方法,否则是终点方法。具体请参照Stream的api

    简单介绍下几个中间方法(filter、map)以及终点方法(collect、sum)

    3.1.1Filter

    在数据流中实现过滤功能是首先我们可以想到的最自然的操作了。Stream接口暴露了一个filter方法,它可以接受表示操作的Predicate实现来使用定义了过滤条件的lambda表达式。

    1
    2
    List persons = …
    Stream personsOver18 = persons.stream().filter(p -> p.getAge() > 18);//过滤18岁以上的人

    3.1.2Map

    假使我们现在过滤了一些数据,比如转换对象的时候。Map操作允许我们执行一个Function的实现(Function<T,R>的泛型T,R分别表示执行输入和执行结果),它接受入参并返回。首先,让我们来看看怎样以匿名内部类的方式来描述它:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    Stream adult= persons
                  .stream()
                  .filter(p -> p.getAge() > 18)
                  .map(new Function() {
                      @Override
                      public Adult apply(Person person) {
                         return new Adult(person);//将大于18岁的人转为成年人
                      }
                  });

    现在,把上述例子转换成使用lambda表达式的写法:

    1
    2
    3
    Stream map = persons.stream()
                        .filter(p -> p.getAge() > 18)
                        .map(person -> new Adult(person));

    3.1.3Count

    count方法是一个流的终点方法,可使流的结果最终统计,返回int,比如我们计算一下满足18岁的总人数

    1
    2
    3
    4
    int countOfAdult=persons.stream()
                           .filter(p -> p.getAge() > 18)
                           .map(person -> new Adult(person))
                           .count();

    3.1.4Collect

    collect方法也是一个流的终点方法,可收集最终的结果

    1
    2
    3
    4
    List adultList= persons.stream()
                           .filter(p -> p.getAge() > 18)
                           .map(person -> new Adult(person))
                           .collect(Collectors.toList());

    或者,如果我们想使用特定的实现类来收集结果:

    1
    2
    3
    4
    5
    List adultList = persons
                     .stream()
                     .filter(p -> p.getAge() > 18)
                     .map(person -> new Adult(person))
                     .collect(Collectors.toCollection(ArrayList::new));


    篇幅有限,其他的中间方法和终点方法就不一一介绍了,看了上面几个例子,大家明白这两种方法的区别即可,后面可根据需求来决定使用。

    3.2顺序流与并行流

    每个Stream都有两种模式:顺序执行和并行执行。
    顺序流:

    1
    List <Person> people = list.getStream.collect(Collectors.toList());

    并行流:

    1
    List <Person> people = list.getStream.parallel().collect(Collectors.toList());

    顾名思义,当使用顺序方式去遍历时,每个item读完后再读下一个item。而使用并行去遍历时,数组会被分成多个段,其中每一个都在不同的线程中处理,然后将结果一起输出。

    3.2.1并行流原理:

    1
    2
    3
    4
    5
    6
    List originalList = someData;
    split1 = originalList(0, mid);//将数据分小部分
    split2 = originalList(mid,end);
    new Runnable(split1.process());//小部分执行操作
    new Runnable(split2.process());
    List revisedList = split1 + split2;//将结果合并

    大家对hadoop有稍微了解就知道,里面的 MapReduce  本身就是用于并行处理大数据集的软件框架,其 处理大数据的核心思想就是大而化小,分配到不同机器去运行map,最终通过reduce将所有机器的结果结合起来得到一个最终结果,与MapReduce 不同,Stream则是利用多核技术可将大数据通过多核并行处理,而MapReduce则可以分布式的。

    3.2.2顺序与并行性能测试对比

    如果是多核机器,理论上并行流则会比顺序流快上一倍,下面是测试代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    long t0 = System.nanoTime();
     
    //初始化一个范围100万整数流,求能被2整除的数字,toArray()是终点方法
     
    int a[]=IntStream.range(0, 1_000_000).filter(p -> p % 2==0).toArray();
     
    long t1 = System.nanoTime();
     
    //和上面功能一样,这里是用并行流来计算
     
    int b[]=IntStream.range(0, 1_000_000).parallel().filter(p -> p % 2==0).toArray();
     
    long t2 = System.nanoTime();
     
    //我本机的结果是serial: 0.06s, parallel 0.02s,证明并行流确实比顺序流快
     
    System.out.printf("serial: %.2fs, parallel %.2fs%n", (t1 - t0) * 1e-9, (t2 - t1) * 1e-9);

    3.3关于Folk/Join框架

    应用硬件的并行性在java 7就有了,那就是 java.util.concurrent 包的新增功能之一是一个 fork-join 风格的并行分解框架,同样也很强大高效,有兴趣的同学去研究,这里不详谈了,相比Stream.parallel()这种方式,我更倾向于后者。

    4.总结

    如果没有lambda,Stream用起来相当别扭,他会产生大量的匿名内部类,比如上面的3.1.2map例子,如果没有default method,集合框架更改势必会引起大量的改动,所以lambda+default method使得jdk库更加强大,以及灵活,Stream以及集合框架的改进便是最好的证明。

  • 相关阅读:
    Python并发编程—自定义线程类
    Python并发编程—线程对象属性
    syfomny 好教材....
    drupal_get_css -- drupal
    common.inc drupal
    date iso 8610
    js很好的教材
    user_load_by_name
    eck add form
    把一个表导入到另一个地方...
  • 原文地址:https://www.cnblogs.com/tianlanliao/p/4294858.html
Copyright © 2011-2022 走看看