PeterHao0 2013-10-11
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 结合使用记录和协议来构造代码。未来的一期文章将介绍它们的关系。
结束语