找回密码
 骑士注册

QQ登录

微博登录

搜索
❏ 站外平台:

查看: 1223|回复: 0
收起左侧

Java 下一代: 使用 Java 8 作为 Java 下一代语言

[复制链接]
夜域诡士 发表于 2014-07-09 21:41:37 | 显示全部楼层 |阅读模式

对三种较新的 JVM 语言进行比较,帮助您评估其中哪一种语言是 Java 语言最有可能的继任者。但与此同时,Java 语言经历了它自从增加范型以来最为重要的一次变革。现在,Java 本身展示出了众多 Groovy、Scala 与 Clojure 语言中最令人满意的特性。在本文中,我将 Java 8 视为 Java 下一代语言,并给出了一些示例来说明该语言的编程范型已经被有效增强到了何种程度。

最后的高阶函数

高阶函数使用其他函数作为参数,并返回其他函数作为结果。Java 最终以 lambda 表达式 的形式拥有了高阶函数,它或许是流行语言中最后一个拥有高阶函数的语言。Java 8 工程师不仅将高阶函数收入语言中,而且还聪明地支持老式接口,以便利用函数功能。

在 "函数式编码风格" 一文中,我演示了如何使用 Java 下一代语言实现来解决一个通常要求强制解决的问题。该问题假设要在一个输入的姓名清单中删除单字符的项,并返回一个以逗号隔开的清单,并将其中的每个姓名都被转换为大写。清单 1 中给出了这个强制性的 Java 解决方案。

清单 1. 强制性的姓名转换
public String cleanNames(List<String> listOfNames) {
    StringBuilder result = new StringBuilder();
    for(int i = 0; i < listOfNames.size(); i++) {
        if (listOfNames.get(i).length() > 1) {
            result.append(capitalizeString(listOfNames.get(i))).append(",");
        }
    }
    return result.substring(0, result.length() - 1).toString();
}

public String capitalizeString(String s) {
    return s.substring(0, 1).toUpperCase() + s.substring(1, s.length());
}

在以前的 Java 版本中,迭代属于规范,但在 Java 8 中,这项任务将由流(stream) 更好地完成 — 流是一种抽象概念,就像是在集合与 UNIX® 管道之间搭起了一座桥梁。清单 2 使用了流。

清单 2. Java 8 中的姓名转换
public String cleanNames(List<String> names) {
    return names
            .stream()
            .filter(name -> name.length() > 1)
            .map(name -> capitalize(name))
            .collect(Collectors.joining(","));
}

private String capitalize(String e) {
    return e.substring(0, 1).toUpperCase() + e.substring(1, e.length());
}

清单 1 中的迭代版本必须将过滤器、转换与连接合并到一个 for 循环中,因为针对每个任务在集合上进行循环的效率非常低。借助 Java 8 中的流,您可以连接函数并将它们组合在一起,直到调用生成输出的函数(称为终端操作)为止,比如 collect() 或 forEach()。

我使用了一些语法糖来创建 清单 2 中的 lambda 表达式。.filter(name -> name.length() > 1) 是filter((name) -> name.length() > 1 的简写形式。对于单个参数而言,那一对括号是多余的。

清单 2 中的 filter() 方法与函数式语言中常见的 filter 方法是一样的(参见 "Java 下一代:克服同义词干扰")。filter() 方法接受一个返回 Boolean 值的高阶函数,该值被用作一个过滤标准:true 表示包含在已过滤的集合中,而 false 表示在已过滤的集合中找不到它。

filter() 方法接受一个 Predicate<T> 类型,这是一个返回 Boolean 值的方法。如果愿意的话,您可以显式地创建谓词实例,如清单 3 中所示。

清单 3. 手动创建一个谓词
Predicate<String> p = (name) -> name.startsWith("Mr");
List<String> l = List.of("Mr Rogers", "Ms Robinson", "Mr Ed");
l.stream().filter(p).forEach(i -> System.out.println(i));

在 清单 3 中,通过将过滤的 lambda 表达式指定给它,我创建了一个谓词。当我在第三行调用 filter() 方法时,我将谓词作为期望的参数进行传递。

在 清单 2 中,map() 方法按照预期将 capitalize() 方法应用于集合中的每个元素。最后,我调用了 collect() 方法,这是一个终端操作 — 一个从流中生成值的方法。collect() 方法执行类似的 reduce 操作:组合元素,以便生成一个(通常)更小的结果,有时候只生成了一个值(例如,一次 sum 操作)。Java 8 有一个 reduce() 方法,但在这个例子中,collect() 方法更适用,因为它对可变容器(比如StringBuilder)的处理更加高效。

通过向现有的类和集合添加函数式结构,比如映射(map) 和缩减(reduce) 操作,Java 需要面对高效更新集合的问题。例如,如果您不能在典型的 Java 集合(如 ArrayList)上使用缩减 操作,它的作用就会要小很多。Scala 与 Clojure 中的很多集合库在默认情况下都是不可变的,这让运行时能够产生高效的操作。Java 8 不能强迫开发人员修改集合,而且 Java 中很多现有的集合类都是可变的。因此,Java 8 中包含对诸如 ArrayList 与 StringBuilder 之类集合执行可变缩减(mutable reduction)操作的方法,这些方法将会更新现有元素,而不是更新每次替换结果。尽管 reduce() 也能用在 清单 2 中,但 collect() 对于这个实例中返回的集合更加高效。

我在 "对比并发性" 一文中谈到的函数式语言的优势之一就是,通常只要添加一个修饰符就能轻松地并行处理集合。Java 8 也提供同样的优势,如清单 4 中所示。

清单 4. Java 8 中的并行处理
public String cleanNamesP(List<String> names) {
    return names 
            .parallelStream() 
            .filter(n -> n.length() > 1) 
            .map(e -> capitalize(e)) 
            .collect(Collectors.joining(","));
}

和在 Scala 中一样,只需添加一个 parallelStream() 修饰符,便可在清单 4 中并行完成流操作。函数式编程将实现细节交给运行时来完成,这样它就可以在更高的抽象层上工作。可将线程轻松地应用于集合就是这种优势的一种证明。

Java 8 中的 reducer 类之间存在差异,这使得向现有的语言构件添加深度范型变得非常困难。Java 8 团队做了大量的工作,才能以基本无缝的方式添加函数式结构。这种集成的一个良好示例是添加函数式接口。

函数式接口

一种常见的 Java 模式是附带一个方法的接口,该接口也被称为 SAM(单一抽象方法)接口,比如 Runnable 或 Callable。在多数情况下,SAM 主要被用作可移植代码的一种传送机制。Java 8 使用 lambdas 表达式这种方式实现了可移植代码。一种叫做函数式接口 的聪明机制支持 lambdas 与 SAM 以有用的方式进行交互。每个函数式接口都包含一个抽象方法(并可以包含几个默认方法)。函数式接口增强了现有的 SAM 接口,支持使用 lambda 表达式代替传统的匿名内部类。例如,Runnable 接口现在可以使用 @FunctionalInterface 注释进行标记。这种可选的注释用于告诉编译器检查 Runnable 是一个接口(不是类或枚举),而且被注释的类型满足函数式接口的要求。

作为 lambda 表达式可替代性的一个例子,我通过传递一个 lambda 表达式来代替 Runnable 匿名内部类,在 Java 8 中创建了一个新线程:

new Thread(() -> System.out.println("Inside thread")).start();

在很多有用的地方,函数式接口可以与 lambda 表达式无缝集成。函数式接口是一项重大创新,因为它与已有的 Java 模式能够完美融合。

默认方法

借助 Java 8,您还可以在接口上声明默认方法。默认方法就是公共的非抽象非静态方法(有方法体),在接口类型中进行声明,并使用default 关键字进行标记。每个默认方法都自动被添加给实现接口的类 — 一种使用默认功能修饰类的便捷方法。例如,Comparator 接口现在包含的默认方法超过一打。如果使用 lambda 表达式创建一个比较器,我可以轻松创建反转比较器,如清单 5 中所示。

清单 5. Comparator 的默认方法
List<Integer> n = List.of(1, 4, 45, 12, 5, 6, 9, 101);
Comparator<Integer> c1 = (x, y) -> x - y;
Comparator<Integer> c2 = c1.reversed();
System.out.println("Smallest = " + n.stream().min(c1).get());
System.out.println("Largest = " + n.stream().min(c2).get());

在清单 5 中,我创建了一个 Comparator 实例,并使用 lambda 表达式来封装它。然后,我可以通过调用 reversed() 默认方法来创建一个反转比较器。将默认方法附加给接口的能力模仿了 mixins 的通常用法(参见 "混入和特征" 一文),这是对 Java 语言的一种有益补充。

可选

请注意,在 清单 5 中的终端调用中,最后调用了 get()。对 min() 这种内置方法的调用返回一个 Optional 而非值。这种行为模仿了 Java 下一代的 option 特性,正如 "Groovy、Scala 与 Clojure 的共性,第 3 部分" 一文的 Scala 中一样。Optional 可防止方法返回将 null 作为错误与null 作为合法值进行合并。例如,只有合法结果存在时,Java 8 才可以使用 ifPresent() 方法来执行代码块。例如,这段代码只会在有值存在时才打印结果:

n.stream()
    .min((x, y) -> x - y)
    .ifPresent(z -> System.out.println("smallest is " + z));

如果希望执行其他的操作,那么还可以使用 orElse() 方法。浏览 Java 8 中的 Comparator 接口对您了解默认方法有多么强大有很好的启发作用。

Java 8 中的流接口及其相关功能是一套经过深思熟虑的扩展集合,可以将 Java 语言带上一个新的层面。

 

Java 8 中的 抽象概念让很多高级的功能特性变为可能。流在很多方面与集合很像,但也存在关键区别:

  • 流不存储值,更像是从输入源通过终端操作到达目的地的一条管道。
  • 流被设计为是函数式的,而不是状态式的。例如,filter() 操作返回经过过滤的值的流,同时不会修改底层集合。
  • 流操作正在尝试尽量变懒(参见 "Java 下一代:内存化和函数式协同" 和 "函数式思维:惰性计算,第 1 部分")。惰性集合仅在必须检索值的时候才会执行操作。
  • 流可以是无限制的。例如,您可以构造一个流来返回所有数字,然后使用诸如 limit() 和 findFirst() 之类的方法来收集子集。
  • 与 Iterator 类似,流在使用时才被充满,在后续再次使用之前,必须重新生成它。

流操作要么是中间操作,要么是终端 操作。中间操作返回一个新流,并始终是懒惰的。例如,在流上使用 filter() 操作并不会真正过滤流,而是创建一个只在终端 操作进行遍历时返回过滤后值的流。终端操作会遍历流,产生值或副作用(如果您编写的函数存在副作用,这会让人气馁)。

流已经包含很多有用的终端操作。例如,我在函数式思维 系列文章中举的数字分类器例子(这个例子也出现在前面的两篇 Java 下一代 文章中)。清单 6 使用 Java 8 实现了该分类器。

清单 6. Java 8 中的数字分类器
public class NumberClassifier {

    public static IntStream factorsOf(int number) {
        return range(1, number + 1)
                .filter(potential -> number % potential == 0);
    }

    public static boolean isPerfect(int number) {
        return factorsOf(number).sum() == number * 2;
    }

    public static boolean isAbundant(int number) {
        return factorsOf(number).sum() > number * 2;
    }

    public static boolean isDeficient(int number) {
        return factorsOf(number).sum() < number * 2;
    }

}

如果熟悉我使用其他语言实现的数字分类器版本(参见 "函数式思维:从功能的角度思考"),您就会注意到清单 6 缺少一个 sum() 方法声明。在这段代码使用其他语言的所有实现中,我不得不自己编写 sum() 方法。Java 8 中提供了一个作为终端操作的 sum() 方法,这样我就不必再编写它了。通过隐藏可变部分,函数式编程降低了开发人员犯错的可能性。如果我不需要实现 sum(),也就不会在实现过程中犯错。Java 8 中的流接口及其相关功能是经过深思熟虑的扩展集合,将 Java 语言提升到了一个新的层面。

在数字分类器的其他版本中,我显示了 factors() 方法的一个优化版本,它只会遍历寻找等于平方根的可能因子,并生成对称的因子。Java 8factors() 方法的优化版本如清单 7 中所示。

清单 7. Java 8 中经过优化的分类器
    public static List fastFactorsOf(int number) {
        List<Integer> factors = range(1, (int) (sqrt(number) + 1))
                .filter(potential -> number % potential == 0)
                .boxed()
                .collect(Collectors.toList());
        List factorsAboveSqrt = factors
                .stream()
                .map(e -> number / e).collect(toList());
        factors.addAll(factorsAboveSqrt);
        return factors.stream().distinct().collect(toList());
    }

即便支持对流进行合并 ,清单 7 中的 factorsOf() 方法无法将两个流合并为一个结果。然而,一旦某个流被遍历,被耗尽资源(这一点与Iterator 相同),在再次使用之前,必须重新生成该流。在 清单 7 中,我使用流创建了两个集合并将结果连接在一起,还添加了对distinct() 的调用,以便处理由整数平方根引起的边缘情况。Java 8 中流的强大功能令人印象深刻,包括构建流的能力。

结束语

在这篇文章中,我经过研究将 Java 8 称作 Java 下一代语言,而它确实当之无愧。精心设计的流库和聪明的扩展机制(比如默认方法)让大量现有的 Java 代码通过极小的代价便能从新功能中获益。

---------------------------------------------------为方便阅读本文有改动,敬请原著见谅-----------------------------------------------------------

原文http://www.ibm.com/developerworks/cn/java/j-jn15/index.html

您需要登录后才可以回帖 登录

本版积分规则

快速回复 返回顶部 返回列表

分享到微信

打开微信,点击顶部的“╋”,
使用“扫一扫”将网页分享至微信。