返回介绍

6.3 一步一步走入函数式编程

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

在了解了Java 8的一些新特性后,就可以正式开始进入函数式编程了。为了能让大家更快地理解函数式编程,我们先从简单的例子开始。

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

public static void main(String[] args) {
  for(int i:arr){
    System.out.println(i);
  }
 }

上述代码循环遍历了数组内的元素,并且进行了数值的打印,这也是传统的做法。如果使用Java 8中的流,那么可以写成这样:

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

public static void main(String[] args) {
  Arrays.stream(arr).forEach(new IntConsumer() {
    @Override
    public void accept(int value) {
      System.out.println(value);
    }
  });
}

注意:Arrays.stream()方法返回了一个流对象。类似于集合或者数组,流对象也是一个对象的集合,它将给予我们遍历处理流内元素的功能。

这里值得注意的是这个流对象的forEach()方法,它接收一个IntConsumer接口的实现,用于对每个流内的对象进行处理。之所以是IntConsumer接口,因为当前流是IntStream,也就是装有Integer元素的流,因此,它自然需要一个处理Integer元素的接口。函数forEach()会挨个将流内的元素送入IntConsumer进行处理,循环过程被封装在forEach()内部,也就是JDK框架内。

除了IntStream流外,Arrays.stream()还支持DoubleStream、LongStream和普通的对象流Stream,这完全取决于它所接受的参数,如图6.3所示。

图6.3 Stream流的几种类型

但这样的写法可能还不能让人满意,代码量似乎比原先更多,而且除了引入了不必要的接口和匿名类等复杂性外,似乎也看不出来有什么太大的好处。但是,我们的脚步并未就此打住。试想,既然forEach()函数的参数是可以从上下文中推导出来的,那为什么还要不厌其烦地写出来呢?这些机械的推导工作,就交给编译器去做吧!于是:

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

public static void main(String[] args) {
  Arrays.stream(arr).forEach((final int x)-> {
       System.out.println(x);
  });
}

从上述代码中可以看到,IntStream接口名称被省略了,这里只使用了参数名和一个实现体,看起来简洁很多了。但是还不够,因为参数的类型也是可以推导的。既然是IntConsumer接口,参数自然是int了,于是:

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

public static void main(String[] args) {
  Arrays.stream(arr).forEach((x)-> {
       System.out.println(x);
  });
}

好了,现在连参数类型也省略了,但是这两个花括号特别碍眼。虽然它们对程序没有什么影响,但是为了简单的一句执行语句要加上一对花括号也实属没有必要,那干脆也去掉吧!去掉花括号后,为了清晰起见,把参数申明和接口实现就放在一行吧!

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

public static void main(String[] args) {
  Arrays.stream(arr).forEach((x)->System.out.println(x));
}

这样看起来就好多了。此时,forEach()函数的参数依然是IntConsumer,但是它却以一种新的形式被定义,这就是lambda表达式。表达式由“->”分割,左半部分表示参数,右半部分表示实现体。因此,我们也可以简单地理解lambda表达式只是匿名对象实现的一种新的方式。实际上,也是这样的。

有兴趣的读者可以使用虚拟机参数-Djdk.internal.lambda.dumpProxyClasses启动带有lambda表达式的Java小程序,该参数会将lambda表达式相关的中间类型进行输出,方便调试和学习。在本例中,输出了HelloFunction6$$Lambda$1.class类,使用以下命令进行并发汇编操作:

javap -p -v HelloFunction6$$Lambda$1.class

在输出结果中,可以清楚地看到:

final class geym.java8.func.ch3.HelloFunction6$$Lambda$1 implements
java.util.function.IntConsumer
省略部分输出
  public void accept(int);
  descriptor: (I)V
  flags: ACC_PUBLIC
  Code:
    stack=1, locals=2, args_size=2
    0: iload_1
    1: invokestatic  #17 // Method geym/java8/func/ch3/HelloFunction6.lambda$0:(I)V
    4: return

限于篇幅有限,这里只给出了我们关心的内容。首先,这个中间类型确实实现了IntConsumer接口。其次,在实现accept()方法时,它内部委托给了一个名为HelloFunction6.lambda$0()的方法。可以推测,这个方法也是编译时自动生成的。

使用以下命令查看HelloFunction6的编译结果:

javap -p -v HelloFunction6

我们很惊喜地找到了期待已久的lambda$0()方法,其实现如下:

private static void lambda$0(int); descriptor: (I)V flags: ACC_PRIVATE, ACC_STATIC, ACC_SYNTHETIC Code: stack=2, locals=1, args_size=1 0: getstatic #41 // Field java/lang/System.out:Ljava/io/PrintStream; 3: iload_0 4: invokevirtual #47 // Method java/io/PrintStream.println:(I)V 7: return

它被实现为一个私有的静态方法,实现内容就是简单地进行了System.out.println()的调用,也正是我们代码中lambda表达式的内容。

由此,可以看到,Java 8中对lambda表达式的处理几乎等同于匿名类的实现,但是在写法上和编程范式上有了明显的区别。

不过,简化代码的流程并没有结束,在上一节中已经提到,Java 8还支持了方法引用,通过方法引用的推导,你甚至连参数申明和传递都可以省略。

static int[] arr={1,3,4,5,6,7,8,9,10}; public static void main(String[] args) { Arrays.stream(arr).forEach(System.out::println); }

至此,欢迎大家正式进入Java 8函数式编程的殿堂,那些看似玄妙的lambda表达式的解析和工作原理已经介绍完毕。

使用lambda表达式不仅可以简化匿名类的编写,与接口的默认方法相结合,还可以使用更顺畅的流式API对各种组件进行更自由的装配。

下面这个例子对集合中所有元素进行两次输出,一次输出到标准错误,一次输出到标准输出中。

static int[] arr={1,3,4,5,6,7,8,9,10}; public static void main(String[] args) { IntConsumer outprintln=System.out::println; IntConsumer errprintln=System.err::println; Arrays.stream(arr).forEach(outprintln.andThen(errprintln)); }

这里首先使用函数引用,直接定义了两个IntConsumer接口实例,一个指向标准输出,另一个指向标准错误。使用接口默认函数IntConsumer.addThen(),将两个IntConsumer进行组合,得到一个新的IntConsumer,这个新的IntConsumer会依次调用outprintln和errprintln,完成对数组中元素的处理。

其中IntConsumer.addThen()的实现如下,仅供大家参考:

default IntConsumer andThen(IntConsumer after) {
  Objects.requireNonNull(after);
  return (int t) -> { accept(t); after.accept(t); };
}

可以看到,addThen()方法返回一个新的IntConsumer,这个新的IntConsumer会先调用第1个IntConsumer进行处理,接着调用第2个IntConsumer处理,从而实现多个处理器的整合。这种操作手法在Java 8的函数式编程中极其常见,请大家留意。

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

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

发布评论

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