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

反思异常、表达式和空

本文是由三个部分组成的介绍 Clojure、Scala 和 Groovy 的共同点的系列文章的最后一篇,调查了这些语言如何处理异常、表达式和 null —— 这些都是 Java™ 语言容易出问题的地方。每种 Java 下一代语言都通过突出语言特征的独特实现消除了 Java 语言的瑕疵。

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

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



2013 年 6 月 24 日

关于本系列

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

上一期文章 中,我介绍了 Java 下一代语言用来消除 Java 语言中华而不实的东西和复杂性的创新方式。在这一期文章中,我将展示这些语言如何消除 Java 的一些瑕疵:异常、语句与表达式,以及围绕 null 的边缘情况。

表达式

Java 语言从 C 语言那里继承的一项传承是区分编程语言 和编程表达式。Java 语句的示例包括使用 ifwhile 的代码行,以及使用 void 来声明不会返回任何值的方法的代码行。表达式(比如 1 + 2 )用于求取某一个值。

这种区分在最早的编程语言中就已经开始,比如在 Fortran 中,这种区分基于硬件条件以及对编程语言设计的初步了解。在许多语言中,它被保留为操作(语句)与求值(表达式)的指示器。但语言设计人员逐渐意识到,该语言可以完全由表达式组成,在对结果不感兴趣的时候忽略结果。事实上,所有函数式语言完全可以消除这种区分,仅使用表达式。

Groovy 的 if?:

在 Java 下一代语言中,传统的命令式语言(Groovy)和函数式语言(Clojure 和 Scala)之间的分离展示了向表达式的进化。Groovy 仍然包含语句,这些语句基于 Java 语法,但添加了更多的表达式。而 Scala 和 Clojure 则完全使用表达式。

语句和表达式中包含的内容都为语言增添了语法上的笨拙 。可以考虑 Groovy 中的 if 语句,它继承自 Java。它有两个版本,清单 1 对它们进行了比较,这两个版本是用于执行判断的 if 语句,以及神秘的三元运算符 ?:

清单 1. Groovy 的两个 if 语句
def x = 5
def y = 0
if (x % 2 == 0)
  y = x * 2
else
  y = x - 1
println y   // 4

y = x % 2 == 0 ? (x *= 2) : (x -= 1)
println y   // 4

if 语句 清单 1 中,我必须将 x 的值设置为一个副作用 (side effect),因为 if 语句没有返回任何值。要执行判断并同时进行赋值,必须使用三元赋值,如 清单 1 中的第二个赋值语句所示。

Scala 的基于表达式的 if 语句

Scala 消除了对三元运算符的需求,允许 if 表达式对两种情况都进行处理。您可以使用它,就像在 Java 代码中使用 if 语句那样(忽略返回值),或者在赋值语句中使用它,如清单 2 中所示:

清单 2. Scala 的基于表达式的 if 语句
val x = 5
val y = if (x % 2 == 0) 
          x * 2
	else
	  x - 1
println(y)

Scala 和其他两种 Java 下一代语言一样,不要求方法中包含明确的 return 语句。因此,方法的最后一行是返回值,强调了这些语言中的方法的基于表达式的特性。

当您在 Java 和 Groovy 代码中进行操作和设置值时,可以将每个响应封装为一个代码块,如清单 3 中所示,并包含任何所需的副作用:

清单 3. Scala if + 副作用
val z = if (x % 2 == 0) {
              println("divisible by 2")
	      x * 2
	    } else {
              println("not divisible by 2; odd")
	      x - 1
	    }
println(z)

清单 3 中,除了返回新计算得出的值之外,我还为每种情况打印了一条状态消息。代码块中的代码行的顺序非常重要:代码块的最后一行表示符合条件的返回值。因此,当您使用基于表达式的 if 进行混合求值和具有副作用时,必须非常小心。

Clojure 的表达式和副作用

Clojure 也完全由表达式组成,但它更进一步,从求值代码中区分出了副作用代码。前两个示例的 Clojure 版本是用一个 let 代码块表达的,在清单 4 中,这允许定义局部作用域变量:

清单 4. Clojure 的基于表达式的 if 语句
(let [x 5
      y (if (= 0 (rem x 2)) (* x 2) (- x 1))]
  (println y))

清单 4 中,我为 x 分配了一个值 5,然后使用 if 建立了表达式来计算两个条件:(rem x 2) 调用了 remainder 函数,类似于 Java % 操作符,并将结果与零值进行比较,在除以 2 时检查零剩余值(zero remainder)。在 Clojure 的 if 表达式中,第一个参数是 condition,第二个参数是 true 分支,第三个参数是可选的 else 分支。if 表达式的结果被分配给 y,然后被打印出来。

Clojure 也允许对每个条件使用代码块(可以包含副作用),但需要一个包装器,比如 (do ...)。包装器通过使用最后一行作为代码块的返回值,对代码块中的每个表达式进行求值。清单 5 说明了如何对某个条件或副作用进行求值:

清单 5. Clojure 中的显式副作用
(let [x 5
      a (if (= 0 (rem x 2))
          (do
            (println "divisible by 2")
            (* x 2))
          (do
            (println "not divisible by 2; odd")
            (- x 1)))]
  (println a))

清单 5,我为 if 表达式的返回值分配了 a。对于每个条件,都创建了一个 (do ...) 包装器,并允许使用任意数量的语句。代码块的最后一行是 (do...) 代码块的返回值,这类似于 清单 3 中的 Scala 示例。请确保目标返回值是最后进行求值的。以这种方式使用 (do...) 代码块是如此之常见,以致于 Clojure 中的许多结构(比如 (let []))已经包含隐式 (do ...) 代码块,这消除了许多情况下对它们的需求。

Java/Groovy 代码和 Scala/Clojure 代码中的表达式的比较指示了编程语言中的总趋势,即消除不必要的语句/表达式分歧。


异常

对于我而言,Java 编程中 “似乎最不错的特性” 是已检查出的异常以及广播和(实施)异常意识(exception awareness) 的能力。事实上,这带来了一场噩梦,它强迫用户进行断章取义的、不必要的异常处理(和误操作)。

所有 Java 下一代语言都使用了已经内置于 JVM 中的异常机制,以及基于 Java 语法的语法,并对这些语法进行了修改,以获得它们自己的独一无二的语法。这些语言都消除了已检查出的异常,在执行 Java 交互操作期间遇到这些异常时,会将它们转换成为 RuntimeExceptions

Scala 对 Java 异常处理机制的转换表现出了一些兴趣,想在它自己的基于表达式的世界中采用该机制。首先,应考虑到的事实是,异常可能是表达式的返回值,如清单 6 中所示:

清单 6. 异常是返回值
val quarter = 
  if (n % 4 == 0)
    n / 4
  else
    throw new RuntimeException("n must be quarterable")

清单 6 中,我分配了 n 值的 1/4 或一个异常。如果触发了异常,那么返回值将没有任何意义,因为在求取返回值之前,异常已经传播开来。这种墨守成规的赋值看着似乎有些奇怪,可以将 Scala 视为一种类型化的语言。Scala 异常类型不是一种数字类型,开发人员不熟悉这种类型,可将它视为 throw 表达式的返回值。Scala 以独创的方式解决了这个问题,它使用特殊的 Nothing 类型作为 throw 的返回类型。Any 在 Scala 中位于继承层次结构的顶部(类似于 Java 中的 Object),这意味着所有类都可以扩展它。相反,Nothing 位于底部,它是其他所有类的自动子类。因此,编译 清单 6 中的代码是合法的:它要么返回一个数字,要么在设置返回值之前触发异常。编译器没有报告错误,这是因为 NothingInt 的一个子类。

其次,finally 代码块在基于表达式的世界中有一些有趣的行为。Scala 的 finally 代码块的作用类似于其他功能,但有一些与返回值有关的微妙行为。请考虑清单 7 中的代码:

清单 7. Scala 的 finally 返回值
def testReturn(): Int = 
  try {                               
    return 1                          
  } finally {                         
    return -1                         
  }

清单 7 中,总体返回值是 -1finally 代码块的返回值“覆盖”了从 try 语句的主体返回的值。这个令人吃惊的结果仅出现在 finally 代码块包含显式 return 语句时,隐式返回值被忽略,如清单 8 中所示:

清单 8. Scala 的隐式返回值
def testImplicitReturn(): Int = 
  try {
    1 
  } finally {
   -1
  }

清单 8 中,函数的返回值是 1,这说明打算使用 finally 代码块作为清理副作用的地方,而不是将它用作对表达式进行求解的地方。

Clojure 也是完全基于表达式的。(try ...) 的返回值总是以下两者之一:

  • 没有异常的 try 代码块的最后一行
  • 捕获了异常的 catch 代码块的最后一行

清单 9 显示了 Clojure 中用于异常的语法:

清单 9. Clojure 的 (try...catch...finally) 代码块
(try  
  (do-work)
  (do-more-work)
  (catch SomeException e  
    (println "Caught" (.getMessage e)) "exception message")
  (finally  
    (do-clean-up)))

清单 9 中,主路径的返回值是来自 (do-more-work) 的返回值。

Java 下一代语言汲取了 Java 异常机制的长处,摈弃了该机制的缺点。此外,尽管有些实现有所不同,但它们设法将这些异常整合到基于表达式的透视图中。


在 2009 年于 QCon London 召开的报告会议中,Tony Hoare 将他发明的“null”概念称为 ALGOL W(1965 年引入的一种实验性的面向对象的语言),“十亿美元的错误” 是由于编程语言中的 null 引用所导致的所有问题带来的。Java 语言自身也遇到了一些与 null 有关的边缘情况,Java 下一代语言解决了这些问题。

例如,Java 编程中一个习惯用语用于防止在您试图调用方法之前出现 NullPointerException

if (obj != null) {
    obj.someMethod();
}

Groovy 已经将这种模式封装在安全导航 操作符 ?. 中。它自动进行左侧的 null 检查,并尝试仅在返回值为非 null 时进行方法调用;否则返回 null

obj?.someMethod();
def streetName = user?.address?.street

也可以采用嵌套方式调用安全导航操作符。

Groovy 的密切相关的 Elvis 操作符 ?: 缩短了默认值中的 Java 三元运算符。例如,以下这些代码行是等效的:

def name = user.name ? user.name : "Unknown" //traditional ternary operator usage

def name = user.name ?: "Unknown"  // more-compact Elvis operator

当左侧有一个值(通常是默认值)时,Elvis 操作符会保护它,否则设置一个新值。Elvis 操作符是三元运算符的一个较短的、倾向于操作符的版本。

Scala 增强了 null 的概念,并使它成为一个类(scala.Null),并附带一个相关的类 scala.NothingNullNothing 都位于 Scala 类分层结构的底部。Null 是每个引用类的子类,而 Nothing 则是其他每种类型的子类。

Scala 提供了 null 和表达式的替代物,以指示是否缺少值。许多关于收集的 Scala 操作(比如 Map 上的 get)会返回一个 Option 实例,该示例包含以下两个部件之一(但绝不会两个都包括):SomeNone。清单 10 中的 REPL 交互显示了一个示例:

清单 10. Scala 的 Option 返回值
scala> val designers=Map("Java" -> "Gosling", "c" -> "K&R", "Pascal" -> "Wirth")
designers: scala.collection.immutable.Map[java.lang.String,java.lang.String] = 
	   Map(Java -> Gosling, c -> K&R, Pascal -> Wirth)

scala> designers get "c"
res0: Option[java.lang.String] = Some(K&R)

scala> designers get "Scala"
res1: Option[java.lang.String] = None

请注意,在 清单 10 中,来自成功的 get 的返回值是一个 Option[java.lang.String] = Some(value),反之则是 None 中的一个失败的查找结果。从收集值中获得展开值 (unwrapping value) 的一项技术使用了模式匹配,模式匹配本身是一个表达式,它允许在一个简洁的表达式中访问和展开某个值:

println(designers get "Pascal" match { case Some(s) => s; case None => "?"})

Option 允许使用比单独使用 null 更好的空表达式,尤其在为该表达式的使用提供语法支持时。


结束语

在这一期的文章中,我深入探讨了 Java 语言的三个问题领域:表达式、异常和 null。每种 Java 下一代语言都可以消除 Java 的瑕疵,但每种语言都有自己的惯用方式。表达式的出现改变了用于看似不相关的概念(比如异常)的一些习惯用语和选项——进一步阐述了语言特性彼此之间是高度耦合的。

Java 开发人员有时会习惯性地认为继承是扩展行为的惟一方式。在下一期文章中,我将展示 Java 下一代语言如何提供许多更强大的替代。

参考资料

学习

获得产品和技术

  • 下载 IBM 产品评估板并动手使用来自 DB2®、Lotus®、Rational®、Tivoli® 和 WebSphere® 的应用程序开发工具和中间件产品。

讨论

条评论

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=935103
ArticleTitle=Java 下一代: Groovy、Scala 和 Clojure 中的共同点,第 3 部分
publish-date=06242013