Java 下一代: Groovy、Scala 和 Clojure 中的共同点,第 2 部分

了解Java 下一代语言如何减少样板代码和降低复杂性

与 Java™ 语言相关的常见抱怨包括:简单的任务涉及到太多的步骤,默认设置有时难以理解。所有 3 种 Java 下一代语言在这些领域都采取了更加明智的方法。这一期 Java 下一代 展示了 Groovy、Scala 和 Clojure 如何消除 Java 语言的瑕疵。

Neal Ford, Director / Software Architect / Meme Wrangler, ThoughtWorks Inc.

Photo of Neal FordNeal Ford 是一家全球性的 IT 咨询公司 ThoughtWorks 的主管、软件架构师和 Meme Wrangler。他还设计并编写了一些应用程序、教材、杂志文章、课件和视频/DVD演示文稿,他是多种技术书籍的作者或编辑,其中包括最近出版的这本 Presentation Patterns。他的工作重点是设计和构建大型企业级应用程序。他还是全球范围开发者大会上的一位国际知名的演讲者。您可以查看 他的网站



2013 年 5 月 20 日

关于本系列

Java 传承的是平台,而不是语言。有超过 200 种语言可以在 JVM 上运行,每种语言带来了 Java 语言所没有的新的有趣功能。本系列探讨 3 种下一代 JVM 语言:Groovy、Scala 和 Clojure,比较和对比新的功能和模式。本系列旨在让 Java 开发人员大致了解他们自己的未来,帮助他们熟练地选择将要花在新语言学习上的时间。

Java 编程语言诞生时所面临的限制与如今的开发人员所面临的条件有所不同。具体来讲,由于上世纪 90 年代中期的硬件的性能和内存限制,Java 语言中存在原语类型。从那时起,Java 语言不断在演化,通过自动装箱(autobox)消除了许多麻烦操作,而下一代语言(Groovy、Scala 和 Clojure)更进一步,消除了每种语言中的不一致性和冲突。

在这一期的文章中,我将展示下一代语言如何消除一些常见的 Java 限制,无论是语法上还是默认行为上。第一个限制是原语数据类型的存在。

原语的消亡

Java 语言最开始有 8 对原语和相应的类型包装器类(最初用于解决性能和内存限制),并通过自动装箱逐步地淡化了它们之间的区别。Java 下一代语言更进一步,让开发人员觉得好像根本不存在差别。

Groovy 完全隐藏了原语类型。例如,int 始终表示 Integer,Groovy 自动处理数字类型的上变换,防止出现数值溢出错误。例如,请查看清单 1 中的 Groovy shell 交互:

清单 1. Groovy 对原语的自动处理
groovy:000> 1.class
===> class java.lang.Integer
groovy:000> 1e12.class
===> class java.math.BigDecimal

清单 1 中,Groovy shell 显示,即使是常量也是通过底层的类来表示的。因为所有数字(和其他伪装的原语)都是真正的类,所以可以使用元编程技术。这些技术包括将方法添加到数字中(这通常用于构建特定领域的语言,即 DSL),支持 3.cm 这样的表达式。在后面介绍可扩展性的那期文章中,我会更全面地介绍此功能。

与 Groovy 中一样,Clojure 自动屏蔽原语与包装器之间的区别,允许对所有类型执行方法调用,自动处理容量的类型转换。Clojure 封装了大量底层优化,这已在语言文档中详细说明(参阅 参考资料)。在许多情况下,可提供类型 hints,使编译器能够生成更快的代码。例如,无需使用 (defn sum[x] ... ) 定义方法,可以添加一个类型提示,比如 (defn sum[^float x] ... ),它会为临界区 (critical section) 生成更高效的代码。

Scala 也屏蔽了原语之间的区别,通常对代码的时效性部件使用底层原语。它还允许在常量上调用方法,就像 2.toString 中一样。借助其混搭原语和包装器的能力,比如 Integer,Scala 比 Java 自动装箱更加透明。例如,Scala 中的 == 运算符可在原语和对象引用上正确运行(比较值,而不是引用),而不同于相同运算符的 Java 版本。Scala 还包含一个 eq 方法(以及一个对称的 ne 方法),它始终比较底层引用类型是否等效。基本而言,Scala 会智能地切换默认行为。在 Java 语言中,== 会对引用数据进行比较,您几乎不需要这么做,可以使用不太直观的 equals() 比较值。在 Scala 中,== 能正确运行(比较值),无论底层实现是什么,它还提供了一个方法来执行不太常见的引用相等性检查 (reference equality check)。

Scala 的这一特性表明,Java 下一代语言的一个重要优势在于:将低级细节卸载到语言和运行时,开发人员能够有更多的时间考虑更高级的问题。


简化默认行为

人们的看法高度一致,大部分 Java 开发人员都认为,在 Java 语言中常见的操作需要太多的语法。例如,属性定义和其他样板代码使类定义变得很杂乱,掩盖了重要的方法。所有 Java 下一代语言都提供了简化创建和访问过程的途径。

Scala 中的类和 case 类

Scala 已简化了类定义,可为您自动创建存取函数、赋值函数和构造函数。例如,请查看清单 2 中的 Java 类:

清单 2. Java 中简单的 Person
class Person {
    private String name;
    private int age;

    Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return name + " is " + age + " years old.";
    }
}

清单 2 中惟一的非样板代码是改写的 toString() 方法。构造函数和所有方法都由 IDE 生成。相比快速生成代码,在以后轻松理解它更为重要。无用的语法增加了您在理解底层含义之前必须使用的代码量。

Scala Person

令人震惊的是,清单 3 中用 Scala 编写的简单 3 行定义就创建了一个等效的类:

清单 3. Scala 中的等效类
class Person(val name: String, var age: Int) {
  override def toString = name + " is " + age + " years old."
}

清单 3 中的 Person 类浓缩成了一个可变的 age 属性、一个不可变的 name 属性,以及一个包含两个参数的构造函数,还有我改写的 toString() 方法。很容易看到这个类的独特之处,因为有趣的部分没有埋藏在语法中。

Scala 的设计强调了以最少的语法创建代码的能力,它使许多语法成为可选语法。清单 4 中的简单类演示了一个将字符串更改为大写字母的 Verbose 类:

清单 4. Verbose 类
class UpperVerbose {
  def upper(strings: String*) : Seq[String] = {
    strings.map((s:String) => s.toUpperCase())
  }
}

清单 4 中的许多代码都是可选的。清单 5 给出了相同的代码,现在使用了一个 object 而不是 class

清单 5. 一个转换为大写的更简单的对象
object Up {
  def upper(strings: String*) = strings.map(_.toUpperCase())
}

对于等效于 Java 静态方法的 Scala 代码,可创建一个 object(与独体实例等效的 Scala 内置实体)而不是一个类。方法的返回类型、用于将单行方法主体分开的括号,以及 清单 4 中无用的 s 参数都从 清单 5 中消失了。Scala 中的这种 “可折叠语法” 有利有弊。使用可折叠语法,能够以非常符合语言习惯的方式编写代码,但这让不熟悉的人难以理解您的代码。

case 类

用作数据持有者的简单类在面向对象的系统中很常见,尤其是必须与不同系统通信的系统。这种类型的类的流行使得 Scala 项目向前推进了一步,创造了 case 类。case 类自动提供了多种便捷的语法:

  • 可根据该类的名称创建一个工厂方法。例如,可以在不使用 new 关键字的情况下构造一个新实例:val bob = Person("Bob", 42)
  • 该类的参数列表中的所有参数都自动 val,也就是说,它们是作为不可变的内部字段来维护的。
  • 编译器为您的类生成合理的默认 equals()hashCode()toString() 方法。
  • 编译器将一个 copy() 方法添加到类中,以便您可返回某个副本来执行变体式更改。

Java 下一代语言不仅修复了语法瑕疵,还促进了对现代软件工作原理的更准确的理解,朝这个方向塑造它们的工具。

Groovy 的自动生成属性

在 Java 下一代语言中,Groovy 与 Java 语法最接近,为常见情形提供了称为 “语法糖 (syntactic-sugar)” 的代码生成方法。参见清单 6 中简单的 Groovy Person 类:

清单 6. Groovy Person
class Person {
  private name
  def age

  def getName() {
    name
  }

  @Override
  String toString() {
    "${name} is ${age} years old."
  }
}

def bob = new Person(name: "Bob", age:42)

println(bob.name)

清单 6 的 Groovy 代码中,定义一个字段 def 会得到一个存取函数和赋值函数。如果仅喜欢其中一个函数,可自行定义它,就像我对 name 属性所做的那样。尽管该方法名为 getName(),但我仍然可以通过更直观的 bob.name 语法访问它。

如果希望 Groovy 自动为您生成 equals()hashCode() 方法对,可以向类中添加 @EqualsAndHashCode 注释。该注释使用 Groovy 的抽象语法树 (Abstract Syntax Tree, AST) 转换 生成基于您的属性的方法(参阅 参考资料)。在默认情况下,此注释仅考虑属性(而不考虑字段);如果添加了 includeFields=true 修饰符,它也会考虑字段。

Clojure 的映射式记录

可在 Clojure 中像其他语言中一样创建相同 Person 类,但这并不符合语言习惯。传统上,Clojure 等语言依靠映射(名称-值对)数据结构来持有这种类型的信息,并使用了一些处理该结构的函数。尽管仍然可以在映射中建模结构化的数据,但目前更常见的情形是使用记录。记录是 Clojure 对具有属性(常常是嵌套的)的类型名的更加正式的封装,每个实例具有相同的语义含义。(Clojure 中的记录就像类 C 语言中的 struct。)

例如,请考虑以下人员定义:

(def mario {:fname "Mario"
            :age "18"})

鉴于此结构,可以通过 (get mario :age) 访问 age。简单的访问是映射上的一个常见操作。借助 Clojure,可以利用使用键充当着映射上的存取函数 的语法糖,以便使用更有效的 (:age mario) 速记法。Clojure 期望对映射进行操作,所以它提供了大量语法糖来简化此操作。

Clojure 还拥有访问嵌套的映射元素的语法糖,如清单 7 所示:

清单 7. Clojure 的速记式访问
(def hal {:fname "hal"
          :age "17"
          :address {:street "Enfield Tennis Academy"
                    :city "Boston"
                    :state "MA"}})

(println (:fname hal))
(println (:city (:address hal)))
(println (-> hal :address :city))

清单 7 中,我定义了一个名为 hal 的嵌套数据结构。对外部元素的访问按预期进行 ((:fname hal))。如 清单 7 中倒数第二行所示,Lisp 语法执行 “内外” 评估。首先,必须从 hal 获取 address 记录,然后访问 city 字段。因为 “内外” 评估是一种常见用法,所以 Clojure 提供了一个特殊运算符(-> thread 运算符)来反转表达式,使它们更加自然、更具可读性:(-> hal :address :city)

可使用记录创建等效的结构,如清单 8 所示:

清单 8. 使用记录创建结构
(defrecord Person [fname lname address])
(defrecord Address [street city state])
(def don (Person. "Don" "Gately" 
           (Address. "Ennet House" "Boston", "MA")))

(println (:fname don))
(println (-> don :address :city))

清单 8 中,我使用 defrecord 创建了相同的结构,得到了一种更加传统的类结构。借助 Clojure,可以通过熟悉的映射操作和方言在记录结构中实现同样便捷的访问。

Clojure 1.2 围绕常见操作的记录定义通过两个工厂函数添加了语法糖:

  • ->类型名称, 接收字段的位置参数
  • ->映射->类型名称, 字段值的关键字映射

使用符合语言习惯的函数,代码由 清单 8 转换成版本 清单 9.

清单 9. Clojure 的漂亮的语法糖
(def don (->Person "Don" "Gately" 
  (->Address "Ennet House" "Boston", "MA")))

在许多情况下,记录比映射和扁平结构更受欢迎。首先,defrecord 创建了一个 Java 类,使它更容易在多方法定义中使用。然后,defrecord 指定更多任务,在您定义记录时启用字段验证和其他细微处理。第三,记录速度快得多,尤其在您拥有一组固定的已知键的时候。

Clojure 结合使用记录和协议来构造代码。未来的一期文章将介绍它们的关系。


结束语

与 Java 语言相比,所有 3 种 Java 下一代语言都提供了更便捷的语法。Groovy 和 Scala 使构建类和常见情形更加轻松,而 Clojure 使映射、记录和类能够无缝地互操作。所有 Java 下一代语言的一个共同主旨是消除不必要的样板代码。在下一期文章中,我将继续探讨这个主题并讨论一些异常。

参考资料

学习

  • Java 下一代:Groovy、Scala 和 Clojure 中的共同点,第 1 部分(Neal Ford,developerWorks,2013 年 3 月):使用 Java 下一代语言(Groovy、Scala 和 Clojure)解决 Java 语言中无法重载运算符的问题。
  • Java 下一代:Java 下一代语言(Neal Ford、developerWorks,2013 年 1 月):在这篇对 Java 下一代语言及其优势进行概述的文章中,探索 3 种下一代 IVM 语言(Groovy、Scala 和 Clojure)中的异同。
  • Scala:Scala 是 JVM 上一种现代的函数式语言。
  • Clojure:Clojure 是一种运行于 JVM 之上的现代的函数式语言。请参阅 Clojure 原语优化和交互 文档。
  • Groovy:Groovy 是一种用于 JVM 的动态语言。 参阅 Groovy AST 转换 文档。
  • 探索 Java 平台的替代语言:跟随此知识路径,浏览有关各种替代性 JVM 语言的 developerWorks 内容。
  • 语言设计者的笔记本:在这个 developerWorks 系列中,Java 语言架构师 Brian Goetz 探讨了为 Java SE 7、Java SE 8 和更改版本中的 Java 语言演化带来挑战的一些语言设计问题。
  • 函数式思维:在 developerWorks 上的 Neal Ford 专栏系列中探索函数式编程。
  • 此作者的更多文章(Neal Ford,developerWorks,2005 年 6 月至今):了解 Groovy、Scala、Clojure、函数式编程、架构、设计、Ruby、Eclipse 和其他 Java 相关技术。
  • developerWorks Java 技术专区:可在这里找到数百篇关于 Java 编程的各个面面的文章。

获得产品和技术

讨论

条评论

developerWorks: 登录

标有星(*)号的字段是必填字段。


需要一个 IBM ID?
忘记 IBM ID?


忘记密码?
更改您的密码

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件

 


在您首次登录 developerWorks 时,会为您创建一份个人概要。您的个人概要中的信息(您的姓名、国家/地区,以及公司名称)是公开显示的,而且会随着您发布的任何内容一起显示,除非您选择隐藏您的公司名称。您可以随时更新您的 IBM 帐户。

所有提交的信息确保安全。

选择您的昵称



当您初次登录到 developerWorks 时,将会为您创建一份概要信息,您需要指定一个昵称。您的昵称将和您在 developerWorks 发布的内容显示在一起。

昵称长度在 3 至 31 个字符之间。 您的昵称在 developerWorks 社区中必须是唯一的,并且出于隐私保护的原因,不能是您的电子邮件地址。

标有星(*)号的字段是必填字段。

(昵称长度在 3 至 31 个字符之间)

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件.

 


所有提交的信息确保安全。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Java technology, Open source
ArticleID=930333
ArticleTitle=Java 下一代: Groovy、Scala 和 Clojure 中的共同点,第 2 部分
publish-date=05202013