zoukankan      html  css  js  c++  java
  • JAVA8之lambda表达式详解,及stream中的lambda使用

    原文:http://blog.csdn.net/jinzhencs/article/details/50748202

    lambda表达式详解


    一.问题

    1.什么是lambda表达式? 
    2.lambda表达式用来干什么的? 
    3.lambda表达式的优缺点? 
    4.lambda表达式的使用场景? 
    5.lambda只是一个语法糖吗?


    二.概念

    lambda表达式是JAVA8中提供的一种新的特性,它支持Java也能进行简单的“函数式编程”。 
    它是一个匿名函数,Lambda表达式基于数学中的λ演算得名,直接对应于其中的lambda抽象(lambda abstraction),是一个匿名函数,即没有函数名的函数。


    三.先看看效果

    先看几个例子: 
    1.使用lambda表达式实现Runnable

    package com.lambda;
    
    /**
     * 使用lambda表达式替换Runnable匿名内部类
     * @author MingChenchen
     *
     */
    public class RunableTest {
        /**
         * 普通的Runnable
         */
        public static void runSomeThing(){
    
            Runnable runnable = new Runnable() {
    
                @Override
                public void run() {
                    System.out.println("I am running");
                }
            };
            new Thread(runnable).start();
        }
    
        /**
         * 使用lambda后的
         */
        public static void runSomeThingByLambda(){
            new Thread(() -> System.out.println("I am running")).start();
        }
    
        public static void main(String[] args) {
            runSomeThing();
    //      runSomeThingByLambda();
        }
    }
    
    上述代码中:
    () -> System.out.println("I am running")就是一个lambda表达式,
    可以看出,它是替代了new Runnable(){}这个匿名内部类。

    2.使用lambda表达式实现Comparator

    package com.lambda;
    
    import java.util.Arrays;
    import java.util.Collections;
    import java.util.Comparator;
    import java.util.List;
    
    public class SortList {
        //给入一个List
        private static List<String> list = 
                Arrays.asList("my","name","is","uber","and","uc");
    
        /**
         * 对一个String的list进行排序 - 使用老方法
         */
        public static void oldSort(){
            //排序
            Collections.sort(list,new Comparator<String>() {
                //使用新的排序规则 根据第二个字符进行逆序排
                @Override
                public int compare(String a,String b){
                    if (a.charAt(1) <= b.charAt(1)) {
                        return 1;
                    }else{
                        return -1;
                    }
                }
            });
        }
    
        /**
         * 新的排序方法 - 使用lambda表达式实现
         */
        public static void newSort(){
            //lambda会自动推断出 a,b 的类型
            Collections.sort(list, (a, b) -> a.charAt(1) < b.charAt(1) ? 1:-1);
        }
    
        public static void main(String[] args) {
    //      oldSort();
            newSort();
        }
    }

    3.使用lambda表达式实现ActionListener

    package com.lambda;
    
    import java.awt.event.ActionEvent;
    import java.awt.event.ActionListener;
    
    import javax.swing.JButton;
    
    public class ActionEventDemo {
        private JButton button = new JButton();
    
    
        public void bindEvent(){
            button.addActionListener(new ActionListener() {
                @Override
                public void actionPerformed(ActionEvent e) {
                    System.out.println("你好!" );
    
                }
            });
        }
    
        /**
         * 使用Lambda表达式 为button添加ActionListener
         */
        public void bindEventByLambda(){
            button.addActionListener(e -> System.out.println("你好!"));
        }
    }

    四.来由

    好了,通过上述的几个例子,大家差不多也能明白了lambda是用来干什么以及好处了。 
    显而易见的,好处就是代码量大大减少了!程序逻辑也很清晰明了。 
    它的用处浅显来说就是替代“内部匿名类”、可以对集合或者数组进行循环操作。

    以前: 
    面向对象式编程就应该纯粹的面向对象,于是经常看到这样的写法: 
    如果你想写一个方法,那么就必须把它放到一个类里面,然后new出来对象,对象调用这个方法。 
    匿名类型最大的问题就在于其冗余的语法。 
    有人戏称匿名类型导致了“高度问题”(height problem): 
    比如大多匿名内部类的多行代码中仅有一行在做实际工作。

    因此JAVA8中就提供了这种“函数式编程”的方法 —— lambda表达式,供我们来更加简明扼要的实现内部匿名类的功能。


    五.什么时候可以使用它?

    先说一个名词的概念

    函数式接口:Functional Interface. 
    定义的一个接口,接口里面必须 有且只有一个抽象方法 ,这样的接口就成为函数式接口。 
    在可以使用lambda表达式的地方,方法声明时必须包含一个函数式的接口。 
    JAVA8的接口可以有多个default方法

    任何函数式接口都可以使用lambda表达式替换。 
    例如:ActionListener、Comparator、Runnable

    lambda表达式只能出现在目标类型为函数式接口的上下文中。

    注意: 
    此处是只能!!! 
    意味着如果我们提供的这个接口包含一个以上的Abstract Method,那么使用lambda表达式则会报错。 
    这点已经验证过了。

    场景: 
    这种场景其实很常见: 
    你在某处就真的只需要一个能做一件事情的函数而已,连它叫什么名字都无关紧要。 
    Lambda 表达式就可以用来做这件事。


    六.写法、规则

    基本语法: 
    (parameters) -> expression 或 (parameters) ->{ statements; } 
    即: 参数 -> 带返回值的表达式/无返回值的陈述

    //1. 接收2个int型整数,返回他们的和
    (int x, int y) -> x + y;
    
    //2. 接受一个 string 对象,并在控制台打印,不返回任何值(看起来像是返回void)
    (String s) -> System.out.print(s);

    七.几个特性

    1. 类型推导 
    编译器负责推导lambda表达式的类型。它利用lambda表达式所在上下文所期待的类型进行推导, 
    这个被期待的类型被称为目标类型。就是说我们传入的参数可以无需写类型了!

    2.变量捕获 
    Java SE 7中,编译器对内部类中引用的外部变量(即捕获的变量)要求非常严格: 
    如果捕获的变量没有被声明为final就会产生一个编译错误。 
    我们现在放宽了这个限制——对于lambda表达式和内部类, 
    我们允许在其中捕获那些符合有效只读(Effectively final)的局部变量。

    简单的说,如果一个局部变量在初始化后从未被修改过,那么它就符合有效只读的要求, 
    换句话说,加上final后也不会导致编译错误的局部变量就是有效只读变量。

    注意:此处和final关键字一样,指的是引用不可改!(感觉没多大意义,还不是用的final)

    3.方法引用 
    如果我们想要调用的方法拥有一个名字,我们就可以通过它的名字直接调用它。 
    Comparator byName = Comparator.comparing(Person::getName); 
    此处无需再传入参数,lambda会自动装配成Person类型进来然后执行getName()方法,而后返回getName()的String

    方法引用有很多种,它们的语法如下:

    静态方法引用:ClassName::methodName 
    实例上的实例方法引用:instanceReference::methodName 
    超类上的实例方法引用:super::methodName 
    类型上的实例方法引用:ClassName::methodName 
    构造方法引用:Class::new 
    数组构造方法引用:TypeName[]::new

    4.JAVA提供给我们的SAM接口 
    Java SE 8中增加了一个新的包:java.util.function,它里面包含了常用的函数式接口,例如:

    Predicate<T>——接收T对象并返回boolean
    Consumer<T>——接收T对象,不返回值
    Function<T, R>——接收T对象,返回R对象
    Supplier<T>——提供T对象(例如工厂),不接收值
    UnaryOperator<T>——接收T对象,返回T对象
    BinaryOperator<T>——接收两个T对象,返回T对象

    那么在参数为这些接口的地方,我们就可以直接使用lambda表达式了!

    八.更多的例子

    1.自定义SAM接口,从而使用lambda表达式

    package com.lambda.myaction;
    
    /**
     * 自定义一个函数式接口
     * @author MingChenchen
     *
     */
    public interface MyActionInterface {
        public void saySomeThing(String str);
        /**
         * Java8引入的新特性 接口中可以定义一个default方法的实现 (不是abstract)
         */
        default void say(){
            System.out.println("default say");
    
    
        }
    }
    package com.lambda.myaction;
    
    /**
     * 在我们自定义的函数式接口的地方使用lambda表达式
     * @author MingChenchen
     *
     */
    public class WantSay {
        /**
         * 执行接口中的saySomeThing方法
         * @param action
         * @param thing
         */
        public static void excuteSay(MyActionInterface action,String thing){
            action.saySomeThing(thing);
        }
    
        public static void main(String[] args) {
            /*
            excuteSay(new MyActionInterface(){
                @Override
                public void saySomeThing(String str) {
                    System.out.println(str);
                }
            },"Hello World");
            */
    
            excuteSay((String s) -> System.out.println(s),"Hello world new");
    
        }
    }

    2.使用方法引用( ClassName::Method,无括号)

    package com.lambda.usebean;
    
    /**
     * 实体类Person
     * @author MingChenchen
     *
     */
    public class Person {
        private String name;      //姓名
        private String location;  //地址
    
        public String getName() {
            return name;
        }
        public void setName(String name) {
            this.name = name;
        }
        public String getLocation() {
            return location;
        }
        public void setLocation(String location) {
            this.location = location;
        }
    
        @Override
        public String toString() {
            // TODO Auto-generated method stub
            return "Person:" + name + "," + location;
        }
    }
    //使用String默认的排序规则,比较的是Person的name字段
    Comparator<Person> byName = Comparator.comparing(p -> p.getName());
    //不用写传入参数,传入的用Person来声明
    Comparator<Person> byName2 = Comparator.comparing(Person::getName);

    3.使用lambda表达式完成for-each循环操作

    //原本的for-each循环写做法
    List list = Arrays.asList(....);
    for (int i = 0; i < list.size(); i++) {
        System.out.println(list.get(i));
    }
    
    //使用lambda表达式后的写法
    list.forEach(str -> System.out.println(str));

    list.forEach()是JAVA8的新方法,支持函数式编程,此处使用的参数就是JAVA提供给我们的函数式接口:Consumer< T>

    interface List<E> extends Collection<E>
    interface Collection<E> extends Iterable<E>
    
    public interface Iterable<T> {  
        default void forEach(Consumer<? super T> action) {
            Objects.requireNonNull(action);
            for (T t : this) {
                action.accept(t);
            }
        }
    }

    4.一个完整的例子

    //普通写法:
    List<Person> people = ...
    Collections.sort(people, new Comparator<Person>() {
      public int compare(Person x, Person y) {
        return x.getLastName().compareTo(y.getLastName());
      }
    })
    
    //使用lambda表达式写法:
    people.sort(comparing(Person::getLastName));

    化简流程:

    第一步:去掉冗余的匿名类
    Collections.sort(people,(Person x, Person y) -> x.getLastName().compareTo(y.getLastName()));
    
    第二步:使用Comparator里的comparing方法
    Collections.sort(people, Comparator.comparing((Person p) -> p.getLastName()));
    
    第三步:类型推导和静态导入
    Collections.sort(people, comparing(p -> p.getLastName()));
    
    第四步:方法引用
    Collections.sort(people, comparing(Person::getLastName));
    
    第五步:使用List本身的sort更优
    people.sort(comparing(Person::getLastName));;

    九.优缺点

    优点: 
    1.极大的简化代码。去除了很多无用的Java代码,使得代码更为简洁明了。 
    2.比匿名内部类更加高效(不确定)。编译器会生成专门的lambda方法,可以使用javap -p查看编译过后的代码

    缺点: 
    1.可读性差。在代码简洁的情况下,另一方面又让大多程序员很难读懂。因为很少程序员接触使用它。 
    (不过这个缺点不是本身缺点,而且源于程序员较少使用)


    十.它是一个语法糖吗?

    答: 
    就我自身的理解来说,lambda表达式不算是一个语法糖。 
    语法糖就是说只是帮助我们程序员轻松的少写一些代码,之后编译器帮我们把那部分代码生成出来。 
    但是从编译过后的结果来说,并不是自动帮我们生成一个内部匿名类,而是生成了一个lambda$X方法。 
    第二个就是lambda其实表达的是目前流行的“函数式编程”这种思维。区别于我们面向对象的思维方法。 
    这点我认为很有意义,即我们要从各种思维来对待事情。而不是说,面向对象的这种方法就是最NB的。

    但是论坛基本都认为这是一个语法糖,也没错。毕竟它提倡的只是一种思想,而且jdk底层为lambda生成了新的高效的代码这个事儿并不确定。


    接下来介绍 lambda的 好哥们:stream. 
    stream的方法里面大多都使用了lambda表达式

    stream概要


    一.什么是stream?

    官方解释:

    A sequence of elements supporting sequential and parallel aggregate operations.

    简单来讲,stream就是JAVA8提供给我们的对于元素集合统一、快速、并行操作的一种方式。 
    它能充分运用多核的优势,以及配合lambda表达式、链式结构对集合等进行许多有用的操作。

    概念: 
    stream:可以支持顺序和并行对元素操作的元素集合。

    作用: 
    提供了一种操作大数据接口,让数据操作更容易和更快 
    使用stream,我们能够对collection的元素进行过滤、映射、排序、去重等许多操作。

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


    二.如何使用stream?

    1.通过Stream接口的静态工厂方法(注意:Java8里接口可以带静态方法); 
    2.通过Collection接口的默认方法(默认方法:Default method,也是Java8中的一个新特性,就是接口中的一个带有实现的方法)–stream(),把一个Collection对象转换成Stream

    一般情况下,我们都使用Collection接口的 .stream()方法得到stream.


    三.常见的几个中间方法

    中间方法即是一些列对元素进行的操作。譬如过滤、去重、截断等。

    1.Filter(过滤)

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

    2.Map(对元素进行操作)

    //把person转成Adult
    Stream map = persons.stream().filter(p -> p.getAge() > 18).map(person -> new Adult(person)); 

    3.limit(截断) 
    对一个Stream进行截断操作,获取其前N个元素,如果原Stream中包含的元素个数小于N,那就获取其所有的元素

    4.distinct(去重) 
    对于Stream中包含的元素进行去重操作(去重逻辑依赖元素的equals方法),新生成的Stream中没有重复的元素


    四.常用的终点方法

    通过中间方法,我们对stream的元素进行了统一的操作,但是中间方法得到还是一个stream。要想把它转换为新的集合、或者是统计等。我们需要使用终点方法。

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

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

    2.Collect(收集流的结果) 
    collect方法也是一个流的终点方法,可收集最终的结果

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

    五.顺序流和并行流

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

    //顺序流:
    List <Person> people = list.getStream.collect(Collectors.toList());
    
    //并行流:
    List <Person> people = list.getStream.parallel().collect(Collectors.toList());
    
    //可以看出,要使用并行流,只需要.parallel()即可

    顾名思义,当使用顺序方式去遍历时,每个item读完后再读下一个item。

    而使用并行去遍历时,数组会被分成多个段,其中每一个都在不同的线程中处理,然后将结果一起输出。

    并行流原理: 
    List originalList = someData; 
    split1 = originalList(0, mid);//将数据分小部分 
    split2 = originalList(mid,end); 
    new Runnable(split1.process());//小部分执行操作 
    new Runnable(split2.process()); 
    List revisedList = split1 + split2;//将结果合并

    性能:如果是多核机器,理论上并行流则会比顺序流快上一倍。

    以下是借用他人的一个测试两者性能的Demo.

    package com.lambda.stream;
    
    import java.util.stream.IntStream;
    
    public class TestPerformance {
        public static void main(String[] args) {
            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);
    
        }
        }

    运行结果:

    serial: 0.07s
    parallel 0.02s

    可以看出,并行流的效率确实提高了3.5倍(我本机是4核,电脑较差.)

    进阶学习: 
    1.Predicate和Consumer接口– Java 8中java.util.function包下的接口: 
    http://ifeve.com/predicate-and-consumer-interface-in-java-util-function-package-in-java-8/

    2.深入理解Java 8 Lambda(语言篇——lambda,方法引用,目标类型和默认方法) 
    http://zh.lucida.me/blog/java-8-lambdas-insideout-language-features/


    参考资料:

    1.Java8初体验(二)Stream语法详解: 
    http://ifeve.com/stream/

    2.Java8初体验(一)lambda表达式语法 
    http://ifeve.com/lambda/

  • 相关阅读:
    [bzoj1468]Tree(点分治)
    [bzoj1087]: [SCOI2005]互不侵犯King(状压dp)
    [hdu5628]Clarke and math(dirichlet卷积)
    [bzoj1036]:[ZJOI2008]树的统计Count(树链剖分)
    [bzoj1026][SCOI2009]windy数(前缀和+数位dp)
    洛谷 P1714 切蛋糕(dp+RMQ)
    [hdu3507] Print Article
    [bzoj1597]: [Usaco2008 Mar]土地购买
    php基础二
    php基础
  • 原文地址:https://www.cnblogs.com/shihaiming/p/6100683.html
Copyright © 2011-2022 走看看