聊聊Java 8 Lambda 表达式

 

    早在2014年oracle发布了jdk 8,在里面增长了lambda模块。因而java程序员们又多了一种新的编程方式:函数式编程,也就是lambda表达式。我本身用lambda表达式也差很少快4年了,但在工做中却鲜有看到同事使用这种编程方式,即便有些使用了,但感受好像对其特性也不是很了解。我看了一上网上的资料也很多,本身整理了一下顺便写下一些本身的见解,但愿个人分享能带给别人一些帮助。php

 

    函数式编程基本概念入门html

  •    什么是函数式编程 

        函数式编程(英语:functional programming)或称函数程序设计,又称泛函编程,是一种编程典范,它将电脑运算视为数学上的函数计算,而且避免使用程序状态以及易变对象。函数编程语言最重要的基础是λ演算(lambda calculus)。并且λ演算的函数能够接受函数看成输入(引数)和输出(传出值)。比起指令式编程,函数式编程更增强调程序执行的结果而非执行的过程,倡导利用若干简单的执行单元让计算结果不断渐进,逐层推导复杂的运算,而不是设计一个复杂的执行过程。这是维基百科给出的定义。从这个咱们知道函数式编程是相对于指令式编程的一种编程典范,而且对而言具备一些优势。java

  •   函数式编程的特性与优缺点

      特性程序员

      一、函数是"第一等公民" 算法

         什么是"第一等公民"?所谓"第一等公民"(first class),指的是函数与其余数据类型同样,处于平等地位,它不只拥有一切传统函数的使用方式(声明和调用),能够赋值给其余变量(赋值),也能够做为参数,传入另外一个函数(传参),或者做为别的函数的返回值(返回)。函数能够做为参数进行传递,意味咱们能够把行为"参数化",处理逻辑能够从外部传入,这样程序就能够设计得更灵活。express

     二、没有"反作用"编程

     所谓"反作用"(side effect),指的是函数内部与外部互动(最典型的状况,就是修改全局变量的值),产生运算之外的其余结果。函数式编程强调没有"反作用",意味着函数要保持独立,全部功能就是返回一个新的值,没有其余行为,尤为是不得修改外部变量的值。api

    三、引用透明数组

    引用透明(Referential transparency),指的是函数的运行不依赖于外部变量或"状态",只依赖于输入的参数,任什么时候候只要参数相同,引用函数所获得的返回值老是相同的。这里强调了一点"输入"不变则"输出"也不变,就像数学函数里面的f(x),只要输入的x同样那获得的结果也确定定是同样的。数据结构

      优势:

    一、代码简洁,开发快速。

     函数式编程大量使用函数,减小了代码的重复,所以程序比较短,开发速度较快。Paul Graham在《黑客与画家》一书中写道:一样功能的程序,极端状况下,Lisp代码的长度多是C代码的二十分之一。若是程序员天天所写的代码行数基本相同,这就意味着,"C语言须要一年时间完成开发某个功能,Lisp语言只须要不到三星期。反过来讲,若是某个新功能,Lisp语言完成开发须要三个月,C语言须要写五年。"固然,这样的对比故意夸大了差别,可是"在一个高度竞争的市场中,即便开发速度只相差两三倍,也足以使得你永远处在落后的位置。" 

    2. 接近天然语言,易于理解

       函数式编程的自由度很高,能够写出很接近天然语言的代码。以java为例把学生以性别分组:

       没用labmda表达式:       

Map<String,List<Student>> studentsMap = new HashMap<>();
        for(Student student : students){
            List<Student> studentList = studentsMap.getOrDefault(student.getSex(), new ArrayList<>());
            studentList.add(student);
            studentsMap.put(student.getSex(),studentList);
        }

  用了lambda表达式:

        Map<String,List<Student>> studentsMap = students.stream().collect(Collectors.groupingBy(Student::getSex));

       这基本就是天然语言的表达了,你们应该一眼就能明白它的意思吧。 

      3. 更方便的代码管理

      函数式编程不依赖、也不会改变外界的状态,只要给定输入参数,返回的结果一定相同。所以,每个函数均可以被看作独立单元,颇有利于进行单元测试(unit testing)和除错(debugging),以及模块化组合。 

      4. 易于"并发编程"
      函数式编程不须要考虑"死锁"(deadlock),由于它不修改变量,因此根本不存在"锁"线程的问题。没必要担忧一个线程的数据,被另外一个线程修改,因此能够很放心地把工做分摊到多个线程,部署"并发编程"(concurrency)。
     请看下面的代码:
     var s1 = Op1();
     var s2 = Op2();
     var s3 = concat(s1, s2);
     因为s1和s2互不干扰,不会修改变量,谁先执行是无所谓的,因此能够放心地增长线程,把它们分配在两个线程上完成。其余类型的语言就作不到这一点,由于s1可能会修改系统状态,而s2可能会用到这些状态,因此必须保证s2在s1以后运行,天然也就不能部署到其余线程上了。多核CPU是未来的潮流,因此函数式编程的这个特性很是重要。

      5. 代码的热升级

      函数式编程没有反作用,只要保证接口不变,内部实现是外部无关的。因此,能够在运行状态下直接升级代码,不须要重启,也不须要停机。Erlang语言早就证实了这一点,它是瑞典爱立信公司为了管理电话系统而开发的,电话系统的升级固然是不能停机的。

     缺点:

     一、函数式编程常被认为严重耗费在CPU和存储器资源。主因有二:

  • 早期的函数式编程语言实现时并没有考虑过效率问题。
  • 有些非函数式编程语言为求提高速度,不提供自动边界检查或自动垃圾回收等功能。
    惰性求值亦为语言如 Haskell增长了额外的管理工做。
    
    二、语言学习曲线陡峭,难度高

    函数式语言对开发者的要求比较高,学习曲线比较陡,并且很容易由于其灵活的语法控制很差程序的结构。

 

     介绍完函数式编程的概念和优缺点以后,下面让咱们来进入java8 lambda的编程世界~

 

      Lambda表达式的组成

       java 8 中Lambda 表达式由三个部分组成:第一部分为一个括号内用逗号分隔的形式参数,参数是函数式接口里面方法的参数;第二部分为一个箭头符号:->;第三部分为方法体,能够是表达式和代码块。语法以下

       一、方法体为表达式,该表达式的值做为返回值返回。

(parameters) -> expression
(int a,int b) -> return a + b; //求和

       二、方法体为代码块,必须用 {} 来包裹起来,且须要一个 return 返回值,但若函数式接口里面方法返回值是 void,则无需返回值。

(parameters) -> { statements; }
(int a) -> {System.out.println("a = " + a);} //打印,无返回值
(int a) -> {return a * a;} //求平方

     

      Lambda表达式的底层实现

      java 8 内部Lambda 表达式的实现方式在本质是以匿名内部类的形式的实现的,看下面代码。代码中咱们定义了一个叫binaryOperator的Lambda表达式,看返回值它是一个IntBinaryOperator实例。  

IntBinaryOperator binaryOperator = (int a, int b) -> {
    return a + b;
};
int result = binaryOperator.applyAsInt(1, 2);
System.out.println("result = " + result); //3

    咱们再看一下IntBinaryOperator的定义 

@FunctionalInterface
public interface IntBinaryOperator {
    /**
     * Applies this operator to the given operands.
     * @param left the first operand
     * @param right the second operand
     * @return the operator result
     */
    int applyAsInt(int left, int right);
}

       

     咱们得知IntBinaryOperator是一个接口而且上面有一个@FunctionalInterface的注解,@FunctionalInterface标注了这是一个函数式接口,因此咱们知道了(int a, int b) -> {return a + b;}返回的一个IntBinaryOperator的匿名实现类。

 

     Lambda表达式的函数式接口 

     上面提到了函数式接口,那这是一个什么样的概念呢?

      函数式接口(Functional Interface)是Java 8对一类特殊类型的接口的称呼。这类接口只定义了惟一的抽象方法的接口(除了隐含的Object对象的公共方法,所以最开始也就作SAM类型的接口(Single Abstract Method)。定义函数式接口的缘由是在Java Lambda的实现中,开发组不想再为Lambda表达式单独定义一种特殊的Structural函数类型,称之为箭头类型(arrow type,依然想采用Java既有的类型(class, interface, method等).缘由是增长一个结构化的函数类型会增长函数类型的复杂性,破坏既有的Java类型,并对成千上万的Java类库形成严重的影响。权衡利弊,所以最终仍是利用SAM 接口做为 Lambda表达式的目标类型.另外对于函数式接口来讲@FunctionalInterface并非必须的,只要接口中只定义了惟一的抽象方法的接口那它就是一个实质上的函数式接口,就能够用来实现Lambda表达式。

       在java 8中已经为咱们定义了不少经常使用的函数式接口它们都放在java.util.function包下面,通常有如下经常使用的四大核心接口:       

函数式接口 参数类型 返回类型 用途
Consumer<T>(消费型接口) T void 对类型为T的对象应用操做。void accept(T t)
Supplier<T>(供给型接口) T 返回类型为T的对象。 T get();
Function<T, R>(函数型接口) T R 对类型为T的对象应用操做并返回R类型的对象。R apply(T t);
Predicate<T>(断言型接口) T boolean 肯定类型为T的对象是否知足约束。boolean test(T t);

 

    Lambda表达式的应用场景

     一、使用() -> {} 替代匿名类

 Thread t1 = new Thread(new Runnable() {
              @Override
              public void run() {
                  System.out.println("no use lambda");
              }
          });
         
Thread t2 = new Thread(() -> System.out.println("use lambda"));

  咱们看到相对而言Lambda表达式要比匿名类要优雅简洁不少~。

     二、以流水线的方式处理数据

        List<Integer> integers = Arrays.asList(4, 5, 6,1, 2, 3,7, 8,8,9,10);

        List<Integer> evens = integers.stream().filter(i -> i % 2 == 0)
                .collect(Collectors.toList()); //过滤出偶数列表 [4,6,8,8,10]
List<Integer> sortIntegers = integers.stream().sorted() .limit(5).collect(Collectors.toList());//排序而且提取出前5个元素 [1,2,3,4,5] List<Integer> squareList = integers.stream().map(i -> i * i).collect(Collectors.toList());//转成平方列表 int sum = integers.stream().mapToInt(Integer::intValue).sum();//求和 Set<Integer> integersSet = integers.stream().collect(Collectors.toSet());//转成其它数据结构好比set Map<Boolean, List<Integer>> listMap = integers.stream().collect(Collectors.groupingBy(i -> i % 2 == 0)); //根据奇偶性分组 List<Integer> list = integers.stream().filter(i -> i % 2 == 0).map(i -> i * i).distinct().collect(Collectors.toList());//复合操做

  借助stream api和Lambda表达式,以住须要定义多个变量,编写数十行甚至数百行的代码的集合操做,如今都基本简化成了能够在一行以内完成~

      三、更简单的数据并行处理

        List<Integer> squareList = integers.stream().parallel().map(i -> i * i).collect(Collectors.toList());//转成平方列表

    数据并行处理,只须要在原来的基础上加一个parallel()就能够开启~。顺便提一下这里parallel()开启的底层并行框架是fork/join,默认的并行数是Ncpu个。

      四、用内部迭代取代外部迭代

       外部迭代:描述怎么干,代码里嵌套2个以上的for循环的都比较难读懂;只能顺序处理List中的元素;

       内部迭代:描述要干什么,而不是怎么干;不必定须要顺序处理List中的元素

List features = Arrays.asList("Lambdas", "Default Method", "Stream API", "Date and Time API");
for (String feature : features) {
    System.out.println(feature); //外部迭代
}

List features = Arrays.asList("Lambdas", "Default Method", "Stream API",
 "Date and Time API");
features.stream.forEach(n -> System.out.println(n)); //内部迭代

       五、重构现有臃肿代码,更高的开发效率

      在Lambda表达式出现以前,咱们的处理逻辑只能是以命令式编程的方式来实现,须要大量的代码去编写程序的每一步操做,定义很是多的变量,代码量和工做量都相对的巨大。若是用Lambda表达式咱们看到以往数十行甚至上百行的代码均可以浓缩成几行甚至一行代码。这样处理逻辑就会相对简单,开发效率能够获得明显提升,维护工做也相对容易。

 

       Lambda表达式中的Stream

        在java 8 中 Stream 不是集合元素,它不保存数据,它是有关算法和计算的,它更像一个高级版本的 Iterator。原始版本的 Iterator,用户只能显式地一个一个遍历元素并对其执行某些操做;高级版本的 Stream,用户只要给出须要对其包含的元素执行什么操做,好比 “过滤掉长度大于 10 的字符串”、“获取每一个字符串的首字母”等,Stream 会隐式地在内部进行遍历,作出相应的数据转换。

        Stream 就如同一个迭代器(Iterator),单向,不可往复,数据只能遍历一次,遍历过一次后即用尽了,就比如流水从面前流过,一去不复返。而和迭代器又不一样的是,Stream 能够并行化操做,迭代器只能命令式地、串行化操做。顾名思义,当使用串行方式去遍历时,每一个 item 读完后再读下一个 item。而使用并行去遍历时,数据会被分红多个段,其中每个都在不一样的线程中处理,而后将结果一块儿输出。Stream 的并行操做依赖于 Java7 中引入的 Fork/Join 框架(JSR166y)来拆分任务和加速处理过程。

        Stream能够有限的也能够是无限的,流的构造方式有不少能够从经常使用的Collection(List,Array,Set and so on...),文件,甚至函数....

      由值建立流:

               Stream<String> stream = Stream.of("Java 8 ", "Lambdas ", "In ", "Action");

       由数组建立流: 

               int[] numbers = {2, 3, 5, 7, 11, 13}; int sum = Arrays.stream(numbers).sum();       

       由文件建立流

               Stream<String> lines =Files.lines(Paths.get("data.txt"), Charset.defaultCharset())

       上面的这些Stream都是有限的,咱们能够用函数来建立一个无限Stream

               Stream.iterate(0, n -> n + 2).forEach(System.out::println);

        Stream也很懒惰,它只会在你真正须要数据的时候才会把数据给传给你,在你不须要时它一个数据都不会产生。

 

       Lambda表达式的Best Practice

        一、保持Lambda表达式简短和一目了然

values.stream()
  .mapToInt(e -> {     
    int sum = 0;
    for(int i = 1; i <= e; i++) {
      if(e % i == 0) {
        sum += i;
      }
    }   
    return sum;
  })
  .sum());  //代码复杂难懂 
values.stream()
  .mapToInt(e -> sumOfFactors(e))
  .sum() //代码简洁一目了然

  长长的Lambda表达式一般是危险的,由于代码越长越难以读懂,意图看起来也不明,而且代码也难以复用,测试难度也大。

      二、使用@FunctionalInterface 注解

         若是你肯定了某个interface是用于Lambda表达式,请必定要加上@FunctionalInterface,代表你的意图否则未来说不定某个不知情的家伙好比你旁边的好基友,在这个interface上面加了另一个抽像方法时,你的代码就悲剧了。

      三、优先使用java.util.function包下面的函数式接口

         java.util.function 这个包下面提供了大量的功能性接口,能够知足大多数开发人员为lambda表达式和方法引用提供目标类型的需求。每一个接口都是通用的和抽象的,使它们易于适应几乎任何lambda表达式。开发人员应该在建立新的功能接口以前研究这个包,避免重复定义接口。另一点就是,里面的接口不会被别人修改~。

      四、不要在Lambda表达中执行有"反作用"的操做

        "反作用"是严重违背函数式编程的设计原则,在工做中我常常看到有人在forEach操做里面操做外面的某个List或者设置某个Map这实际上是不对的。

      五、不要把Lambda表达式和匿名内部类同等对待

         虽然咱们能够用匿名内部类来实现Lambda表达式,也能够用Lambda表达式来替换内部类,但并不表明这二者是等价的。这二者在某一个重要概念是不一样的:this指代的上下文是不同的。当您使用内部类时,它将建立一个新的范围。经过实例化具备相同名称的新局部变量,能够从封闭范围覆盖局部变量。您还能够在内部类中使用这个关键字做为它实例的引用。可是,lambda表达式可使用封闭范围。您不能在lambda的主体内覆盖范围内的变量

private String value = "Enclosing scope value";

public String scopeExperiment() {
    Foo fooIC = new Foo() {
        String value = "Inner class value";
 
        @Override
        public String method(String string) {
            return this.value;
        }
    };
    String resultIC = fooIC.method("");
 
    Foo fooLambda = parameter -> {
        String value = "Lambda value";
        return this.value;
    };
    String resultLambda = fooLambda.method("");
 
    return "Results: resultIC = " + resultIC + 
      ", resultLambda = " + resultLambda;
}

   运行上面这段代码咱们将到 resultIC = "Inner class value",resultLambda = "Enclosing scope value"。也就是说在匿名内部类中this指的是自身的引用,在Lambda表达式中this指的是外部。

       六、多使用方法引用

       在Lambda表达式中 a -> a.toLowerCase()和String::toLowerCase都能起到相同的做用,但二者相比,后者一般可读性更高而且代码会简短。

       七、尽可能避免在Lambda的方法体中使用{}代码块

       优先使用

Foo foo = parameter -> buildString(parameter);
private String buildString(String parameter) {
    String result = "Something " + parameter;
    //many lines of code
    return result;
}

     而不是

Foo foo = parameter -> { String result = "Something " + parameter; 
    //many lines of code 
    return result; 
};

  八、不要盲目的开启并行流

       Lambda的并行流虽好,但也要注意使用场景。若是日常的业务处理好比过滤,提取数据,没有涉及特别大的数据和耗时操做,则真的不须要开启并行流。我在工做中看到有些人一个只有几十个元素的列表的过滤操做也开启了并行流,其实这样作会更慢。由于多行线程的开启和同步这些花费的时间每每比你真实的处理时间要多不少。但一些耗时的操做好比I/O访问,DB查询,远程调用,这些若是能够并行的话,则开启并行流是可提高很大性能的。由于并行流的底层原理是fork/join,若是你的数据分块不是很好切分,也不建议开启并行流。举个例子ArrayList的Stream能够开启并行流,而LinkedList则不建议,由于LinkedList每次作数据切分要遍历整个链表,这自己就已经很浪费性能,而ArrayList则不会。

相关文章
相关标签/搜索