关于Java8函数式编程你须要了解的几点

函数式编程与面向对象的设计方法在思路和手段上都各有千秋,在这里,我将简要介绍一下函数式编程与面向对象相比的一些特色和差别。java

  1. 函数做为一等公民

在理解函数做为一等公民这句话时,让咱们先来看一下一种很是经常使用的互联网语言JavaScript,相信你们对它都不会陌生。JavaScript并非严格意义上的函数式编程,不过,它也不是属于严格的面向对象。可是,若是你愿意,你既能够把它当作面向对象语言,也能够把它当作函数式语言,所以,称之为多范式语言,可能更加合适。编程

若是你使用jQuery,你可能会常用以下的代码: 数组

$("button").click(function(){  
  $("li").each(function(){  
    alert($(this).text())  
   });  
 });  

注意这里each()函数的参数,这是一个匿名函数,在遍历全部的li节点时,会弹出li节点的文本内容。将函数做为参数传递给另一个函数,这是函数式编程的特性之一。安全

 

再来考察另一个案例:多线程

function f1(){  
    var n=1;  
    function f2(){  
      alert(n);  
    }  
    return f2;  
  }  
var result=f1();  
result(); // 1  

 这也是一段JavaScript代码,在这段代码中,注意函数f1的返回值,它返回了函数f2。在倒数第2行,返回的f2函数并赋值给result,实际上,此时的result就是一个函数,而且指向f2。对result的调用,就会打印n的值。编程语言

函数能够做为另一个函数的返回值,也是函数式编程的重要特色。函数式编程

2.无反作用

函数的反作用指的是函数在调用过程当中,除了给出了返回值外,还修改了函数外部的状态,好比,函数在调用过程当中,修改了某一个全局状态。函数式编程认为,函数的副用做应该被尽可能避免。能够想象,若是一个函数肆意修改全局或者外部状态,当系统出现问题时,咱们可能很难判断到底是哪一个函数引发的问题。这对于程序的调试和跟踪是没有好处的。若是函数都是显式函数,那么函数的执行显然不会受到外部或者全局信息的影响,所以,对于调试和排错是有益的。函数

注意:显式函数指函数与外界交换数据的惟一渠道就是参数和返回值,显式函数不会去读取或者修改函数的外部状态。与之相对的是隐式函数,隐式函数除了参数和返回值外,还会读取外部信息,或者可能修改外部信息。性能

然而,彻底的无反作用实际上作不到的。由于系统老是须要获取或者修改外部信息的。同时,模块之间的交互也极有多是经过共享变量进行的。若是彻底禁止反作用的出现,也是一件让人很不愉快的事情。所以,大部分函数式编程语言,如Clojure等,都容许反作用的存在。可是与面向对象相比,这种函数调用的反作用,在函数式编程里,须要进行有效的限制。优化

申明式的(Declarative)

函数式编程是申明式的编程方式。相对于命令式(imperative)而言,命令式的程序设计喜欢大量使用可变对象和指令。咱们老是习惯于建立对象或者变量,而且修改它们的状态或者值,或者喜欢提供一系列指令,要求程序执行。这种编程习惯在申明式的函数式编程中有所变化。对于申明式的编程范式,你不在须要提供明确的指令操做,全部的细节指令将会更好的被程序库所封装,你要作的只是提出你要的要求,申明你的用意便可。

请看下面一段程序,这一段传统的命令式编程,为了打印数组中的值,咱们须要进行一个循环,而且每次须要判断循环是否结束。在循环体内,咱们要明确地给出须要执行的语句和参数。

 
public static void imperative(){  
         int[]iArr={1,3,4,5,6,9,8,7,4,2};  
         for(int i=0;i<iArr.length;i++){  
                   System.out.println(iArr[i]);  
         }  
}  

与之对应的申明式代码以下: 

public static void declarative(){  
         int[]iArr={1,3,4,5,6,9,8,7,4,2};  
         Arrays.stream(iArr).forEach(System.out::println);  
}  

能够看到,变量数组的循环体竟然消失了!println()函数彷佛在这里也没有指定任何参数,在此,咱们只是简单的申明了咱们的用意。有关循环以及判断循环是否结束等操做都被简单地封装在程序库中。

3.尾递归优化

递归是一种经常使用的编程技巧。使用递归一般能够简化程序编码,大幅减小代码行数。可是递归有一个很大的弊病——它老是使用栈空间。可是,程序的栈空间是很是有限的,与堆空间相比,可能相差几个数量级(栈空间大小一般只有几百K,而堆空间则一般达到几百M甚至上百G)。所以,大规模的递归操做有可能发生栈空间溢出错误,这也限制了递归函数的使用,并给系统带来了必定的风险。

而尾递归优化能够有效地避免这种情况。尾递归指递归操做处于函数的最后一步。在这种状况下,该函数的工做其实已经完成(剩余的工做就是再次调用它本身),此时,只须要简单得将中间结果传递给后继调用的递归函数便可。此时,编译器就能够进行一种优化,使当前的函数调用返回,或者用新函数的帧栈覆盖老函数的帧栈。总之,当递归处于函数操做的最后一步时,咱们老是能够千方百计避免递归操做不断申请栈空间。

大部分函数式编程语言直接或者间接支持尾递归优化。

4.不变模式

若是读者熟悉多线程程序设计,那么必定对不变模式有全部了解。所谓不变,是指对象在建立后,就再也不发生变化。好比,java.lang.String就是不变模式的典型。若是你在Java中建立了一个String实例,不管如何,你都不可能改变整个String的值。好比,当你使用String.replace()函数试图进行字符串替换时,实际上,原有的字符串对象并不会发生变化,函数自己会返回一个新的String对象,做为给定字符替换后的返回值。不变的对象在函数式编程中被大量使用。

请看如下代码:

static int[] arr={1,3,4,5,6,7,8,9,10};  
Arrays.stream(arr).map((x)->x=x+1).forEach(System.out::println);  
System.out.println();  
Arrays.stream(arr).forEach(System.out::println);  
 

代码第2行看似对每个数组成员执行了加1的操做。可是在操做完成后,在最后一行,打印arr数组全部的成员值时,你仍是会发现,数组成员并无变化!在使用函数式编程时,这种状态是一种常态,几乎全部的对象都拒绝被修改。

5.易于并行

因为对象都处于不变的状态,所以函数式编程更加易于并行。实际上,你甚至彻底不用担忧线程安全的问题。咱们之因此要关注线程安全,一个很大的缘由是当多个线程对同一个对象进行写操做时,容易将这个对象“写坏”,更专业的说法是“使得对象状态不一致”。可是,因为不变模式的存在,对象自建立以来,就不可能发生改变,所以,在多线程环境下,也就没有必要进行任何同步操做。这样不只有利于并行化,同时,在并行化后,因为没有同步和锁机制,其性能也会比较好。读者能够关注一下java.lang.String对象。很显然,String对象能够在多线程中很好的工做,可是,它的每个方法都没有进行同步处理。

6.更少的代码

一般状况下,函数式编程更加简明扼要,Clojure语言(一种运行于JVM的函数式语言)的爱好者就宣称,使用Clojure能够将Java代码行数减小到原有的十分之一。通常说来,精简的代码更易于维护。而Java代码的冗余性也是出了名的,大部分对于Java语言的攻击都会直接针对Java繁琐,并且死板的语法(但我认为这也是Java的优势之一,正如本书第一段提到的“保守的设计思想是Java最大的优点”),然而,引入函数式编程范式后,这种状况发生了改变。咱们可让Java用更少的代码完成更多的工做。

请看下面这个例子,对于数组中每个成员,首先判断是不是奇数,若是是奇数,则执行加1,并最终打印数组内全部成员。

数组定义:

  1. static int[] arr={1,3,4,5,6,7,8,9,10};  
  2. 传统的处理方式:  
  3. for(int i=0;i<arr.length;i++){  
  4.          if(arr[i]%2!=0){  
  5.                    arr[i]++;  
  6.          }  
  7.          System.out.println(arr[i]);  
  8. }  

 

使用函数式方式:

Arrays.stream(arr).map(x->(x%2==0?x:x+1)).forEach(System.out::println);

能够看到,函数式范式更加紧凑并且简洁。

 感兴趣的朋友能够看看这本电子书《Java8函数式编程入门

相关文章
相关标签/搜索