敬请期待该系列的后续内容。
表达能力是函数式编程的优势之一,但这对您的代码意味着什么?在本文中,我们将比较命令式和函数式代码的示例,判断这两者的表达能力和简洁性的品质。您将了解这些品质如何帮助确保可读性,还会考虑一个反面示例:对简洁性的过度追求导致代码无用。最后,我将介绍 Java 8 对于函数组合中的垂直对齐点的约定。尽管一些 Java 开发人员可能不熟悉此约定,但可以用一个简单示例来证明其价值。
在 Java 8 发布一年左右,我在自己的网站上宣布进行一次简短且不受约束的调查,并邀请开发人员参与该调查。每个参与者都会看到一段命令式或函数式代码,然后需要确定代码的行为。我计算了每位访问者提供回复所花的时间,并比较来自两个不同代码示例的结果。该调查开放了 48 小时,在此期间有 1,100 多人参与调查。调查结果令人感到有些惊讶。
大部分开发人员,包括作者本人,都有丰富的命令式编程经验。尽管函数式编程已存在很长时间,但大部分 Java 程序员都对它并不熟悉。了解到这一点后,82% 的收到命令式代码的调查回复者能确定其正确行为并不令人感到惊讶。与此同时,收到函数式代码的回复者中只有 75% 回答正确。
但是,令我惊讶的是回复者理解两个代码示例所用的时间。理解命令式代码所用的平均时间比理解函数式代码所用的平均时间长 30 秒。
函数式代码比命令式代码更富于表达且更简洁 —前提是需要精心编写。一个简单示例可以证明这一点。在查看下面的代码示例之前,请准备一个计时器。像我的调查回复者一样,您的任务是理解代码的细节。您要计算每个示例所需的时间。
准备好了吗?启动计时器并阅读下面的代码,然后写下您预计它会产生的行为。
- List<String> names = Arrays.asList("Jack", "Jill", "Nate", "Kara", "Kim", "Jullie", "Paul", "Peter");
- List<String> subList = new ArrayList<>();
- for(String name : names) {
- if(name.length() == 4)
- subList.add(name);
- }
- StringBuilder namesOfLength4 = new StringBuilder();
- for(int i = 0; i < subList.size() - 1; i++) {
- namesOfLength4.append(subList.get(i));
- namesOfLength4.append(", ");
- }
- if(subList.size() > 1)
- namesOfLength4.append(subList.get(subList.size() - 1));
- System.out.println(namesOfLength4);
您理解此代码需要多少时间?如果时间比您预计的要长,不要惊讶。与其说这个时间反映了您的能力,不如说它反映了该代码的糟糕品质。
现在考虑一个使用 Java 8 支持的函数样式编写的等效示例:
- List<String> names = Arrays.asList("Jack", "Jill", "Nate", "Kara", "Kim", "Jullie", "Paul", "Peter");
- System.out.println(
- names.stream()
- .filter(name -> name.length() == 4)
- .collect(Collectors.joining(", ")));
您理解此代码会花用多少时间?显然,您已经确定了清单 1 的用途,所以这不是一次真正的实验。如果想真正比较这些示例,可以要求一些同事来理解一个或另一个代码示例,然后比较他们的回复时间。
如果您熟悉 Java 8,可能会顺利理解清单 2 中的代码。即使不熟悉 Java 8,得益于描述性的方法名称,您可能也能理解它。您还能快速理解此代码,因为它比清单 1 简洁得多。
基本上讲,该代码的含义是:给定一个名称集合,仅选择长度为 4 的名称,然后通过逗号将它们连接起来。
这个示例是人为的,但它确实证明了简洁性和表达能力在编码中的价值。我们在函数式代码中看到的这些品质比命令式代码中要多得多。
函数式代码富于表达且简洁,这使程序不但更短,而且更容易阅读。考虑另一个示例:
- int result = 0;
- for (int e: numbers) {
- if (e > 3 && e % 2 == 0 && e < 8) {
- result += e * 2;
- }
- }
- System.out.println(result);
给定名为
的列表,此代码将计算大于 3 且小于 8 的偶数并将该数字乘以 2,然后输出结果。该代码包含 7 行,我们可能可以减少一两行。
- numbers
现在考虑 Java 8 中使用函数样式编写的相同代码:
- System.out.println(
- numbers.stream()
- .filter(e -> e > 3)
- .filter(e -> e % 2 == 0)
- .filter(e -> e < 8)
- .mapToInt(e -> e * 2)
- .sum());
清单 4 也为 7 行,但在这种情况下,进一步减少代码没有帮助。
函数式代码并不总是比命令式代码短。更重要的是它富于表达。简洁但难读的代码毫无帮助。
函数式代码的设计目标是比命令式代码更简洁,但这不能保证它更可读。考虑下面这个示例:
- System.out.println(names.stream().filter(name -> name.startsWith("J")).filter(name -> name.length() > 3)
- .map(name -> name.toUpperCase()).collect(Collectors.joining(", ")));
在清单 5 中,
、
- filter
和其他函数式元素增加了代码的表达能力。但您可能注意到,此代码让人感觉更加生硬,而不是更简洁。
- map
尽管它只有两行,但此代码仍需要花大量精力来阅读和理解。您的眼睛需要努力寻找函数调用在何处结束,下一个调用从何处开始。该代码非常简短,但它编写得非常生硬。编写这种毫无帮助的代码只有一个理由:开发人员肯定憎恨与他共事的每个人。
生硬的代码可能非常简短,但仍然很难读懂。简洁的代码也简短,但读起来让人感觉愉悦且容易理解。
在编程过程中,我们很容易忽略表达能力和可读性的价值。Java 8 通过约定来鼓励这些品质,建议我们对齐函数组合的垂直方向上的各点。
不幸的是,我观察到不熟悉 Java 8 的程序员常常忽略此约定,甚至在多次提醒后才想起它。经验更丰富的程序员应在代码评审期间执行此约定。优秀的 Java 8 IDE 也可以提供帮助,提供使用这类约定的快捷方式。
清单 6 展示了我们使用对齐约定重写清单 5 的代码的结果:
- System.out.println(
- names.stream()
- .filter(name -> name.startsWith("J"))
- .filter(name -> name.length() > 3)
- .map(name -> name.toUpperCase())
- .collect(Collectors.joining(", ")));
在这里,我们看到了来自清单 5 的同一段生硬的代码,但各个点已在垂直方向上对齐,而且我们抵抗住了将多个条件组合到一个参数中的诱惑。结果,每行都具有凝聚力:范围狭小而专注,仅含一个明确的任务。
尽管可能看似可有可无,但遵循 Java 8 的对齐约定肯定非常有益。
保持每行代码都简短紧凑是一种不错的做法,但是走极端可能导致代码变得生硬难读。要提高表达能力,可以问自己代码是否容易理解。要提高可读性,可采用 Java 8 垂直对齐各点的约定。使用这些简单技巧,就能创建出简洁、富于表达且可读的函数式代码。
来源: http://www.ibm.com/developerworks/cn/middleware/library/mw-1703-maurip3/index.html