返回介绍

6.1 Java 8 的函数式编程简介

发布于 2024-08-21 22:20:21 字数 3272 浏览 0 评论 0 收藏 0

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

6.1.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的值。

函数可以作为另外一个函数的返回值,也是函数式编程的重要特点。

6.1.2 无副作用

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

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

然而,完全的无副作用实际上做不到的,因为系统总是需要获取或者修改外部信息的,同时,模块之间的交互也极有可能是通过共享变量进行的。如果完全禁止副作用的出现,也是一件让人很不愉快的事情。因此,大部分函数式编程语言,如Clojure等,都允许副作用的存在。但是与面向对象相比,这种函数调用的副作用,在函数式编程里,需要进行有效的限制。

6.1.3 申明式的(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()函数似乎在这里也没有指定任何参数,在此,我们只是简单地申明了我们的用意。有关循环以及判断循环是否结束等操作都被简单地封装在程序库中。

6.1.4 不变的对象

在函数式编程中,几乎所有传递的对象都不会被轻易修改。

请看以下代码:

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数组所有的成员值时,你还是会发现,数组成员并没有变化!在使用函数式编程时,这种状态是一种常态,几乎所有的对象都拒绝被修改。这非常类似于不变模式。

6.1.5 易于并行

由于对象都处于不变的状态,因此函数式编程更加易于并行。实际上,你甚至完全不用担心线程安全的问题。我们之所以要关注线程安全,一个很重要的原因是当多个线程对同一个对象进行写操作时,容易将这个对象“写坏”。但是,由于对象是不变的,因此,在多线程环境下,也就没有必要进行任何同步操作。这样不仅有利于并行化,同时,在并行化后,由于没有同步和锁机制,其性能也会比较好。

6.1.6 更少的代码

通常情况下,函数式编程更加简明扼要,Clojure语言(一种运行于JVM的函数式语言)的爱好者就宣称,使用Clojure可以将Java代码行数减少到原有的十分之一。一般说来,精简的代码更易于维护。引入函数式编程范式后,我们可以使用Java用更少的代码完成更多的工作。

请看下面这个例子,对于数组中每一个成员,首先判断是否是奇数,如果是奇数,则执行加1,并最终打印数组内所有成员。

数组定义:

static int[] arr={1,3,4,5,6,7,8,9,10};

传统的处理方式:

for(int i=0;i<arr.length;i++){
  if(arr[i]%2!=0){
    arr[i]++;
  }
  System.out.println(arr[i]);
}

使用函数式方式:

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

可以看到,函数式范式更加紧凑而且简洁。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文