面向 Java 开发人员的 Scala 指南: 类操作

理解 Scala 的类语法和语义

Java™ 开发人员可以将对象作为理解 Scala 的出发点。本文是面向 Java 开发人员的 Scala 指南系列 的第二期,作者 Ted Neward 遵循对一种语言进行评价的基本前提:一种语言的威力可以直接通过它集成新功能的能力衡量,在本文中就是指对复数的支持。跟随本文,您将了解在 Scala 中与类的定义和使用有关的一些有趣特性。

Ted Neward, 主管, Neward & Associates

Ted Neward photoTed Neward 是 Neward & Associates 的主管,负责有关 Java、.NET、XML 服务和其他平台的咨询、指导、培训和推介。他现在居住在华盛顿州西雅图附近。



2008 年 3 月 18 日

在上一期 文章 中,您只是稍微了解了一些 Scala 语法,这些是运行 Scala 程序和了解其简单特性的最基本要求。通过上一篇文章中的 Hello World 和 Timer 示例程序,您了解了 Scala 的 Application 类、方法定义和匿名函数的语法,还稍微了解了 Array[] 和一些类型推断方面的知识。Scala 还提供了很多其他特性,本文将研究 Scala 编程中的一些较复杂方面。

Scala 的函数编程特性非常引人注目,但这并非 Java 开发人员应该对这门语言感兴趣的惟一原因。实际上,Scala 融合了函数概念和面向对象概念。为了让 Java 和 Scala 程序员感到得心应手,可以了解一下 Scala 的对象特性,看看它们是如何在语言方面与 Java 对应的。记住,其中的一些特性并不是直接对应,或者说,在某些情况下,“对应” 更像是一种类比,而不是直接的对应。不过,遇到重要区别时,我会指出来。

Scala 和 Java 一样使用类

我们不对 Scala 支持的类特性作冗长而抽象的讨论,而是着眼于一个类的定义,这个类可用于为 Scala 平台引入对有理数的支持(主要借鉴自 “Scala By Example”,参见 参考资料):

清单 1. rational.scala
class Rational(n:Int, d:Int)
{
  private def gcd(x:Int, y:Int): Int =
  {
    if (x==0) y
    else if (x<0) gcd(-x, y)
    else if (y<0) -gcd(x, -y)
    else gcd(y%x, x)
  }
  private val g = gcd(n,d)
  
  val numer:Int = n/g
  val denom:Int = d/g
  
  def +(that:Rational) =
    new Rational(numer*that.denom + that.numer*denom, denom * that.denom)
  def -(that:Rational) =
    new Rational(numer * that.denom - that.numer * denom, denom * that.denom)
  def *(that:Rational) =
    new Rational(numer * that.numer, denom * that.denom)
  def /(that:Rational) =
    new Rational(numer * that.denom, denom * that.numer)

  override def toString() =
    "Rational: [" + numer + " / " + denom + "]"
}

从词汇上看,清单 1 的整体结构与 Java 代码类似,但是,这里显然还有一些新的元素。在详细讨论这个定义之前,先看一段使用这个新 Rational 类的代码:

清单 2. RunRational
class Rational(n:Int, d:Int)
{
  // ... as before
}

object RunRational extends Application
{
  val r1 = new Rational(1, 3)
  val r2 = new Rational(2, 5)
  val r3 = r1 - r2
  val r4 = r1 + r2
  Console.println("r1 = " + r1)
  Console.println("r2 = " + r2)
  Console.println("r3 = r1 - r2 = " + r3)
  Console.println("r4 = r1 + r2 = " + r4)
}

清单 2 中的内容平淡无奇:先创建两个有理数,然后再创建两个 Rational,作为前面两个有理数的和与差,最后将这几个数回传到控制台上(注意, Console.println() 来自 Scala 核心库,位于 scala.* 中,它被隐式地导入每个 Scala 程序中,就像 Java 编程中的 java.lang 一样)。


用多少种方法构造类?

现在,回顾一下 Rational 类定义中的第一行:

清单 3. Scala 的默认构造函数
class Rational(n:Int, d:Int)
{
  // ...

您也许会认为清单 3 中使用了某种类似于泛型的语法,这其实是 Rational 类的默认的、首选的构造函数:nd 是构造函数的参数。

Scala 优先使用单个构造函数,这具有一定的意义 —— 大多数类只有一个构造函数,或者通过一个构造函数将一组构造函数 “链接” 起来。如果需要,可以在一个 Rational 上定义更多的构造函数,例如:

清单 4. 构造函数链
class Rational(n:Int, d:Int)
{
  def this(d:Int) = { this(0, d) }

注意,Scala 的构造函数链通过调用首选构造函数(Int,Int 版本)实现 Java 构造函数链的功能。

实现细节

在处理有理数时,采取一点数值技巧将会有所帮助:也就是说,找到公分母,使某些操作变得更容易。如果要将 1/2 与 2/4 相加,那么 Rational 类应该足够聪明,能够认识到 2/4 和 1/2 是相等的,并在将这两个数相加之前进行相应的转换。

嵌套的私有 gcd() 函数和 Rational 类中的 g 值可以实现这样的功能。在 Scala 中调用构造函数时,将对整个类进行计算,这意味着将 g 初始化为 nd 的最大公分母,然后用它依次设置 nd

回顾一下 清单 1 就会发现,我创建了一个覆盖的 toString 方法来返回 Rational 的值,在 RunRational 驱动程序代码中使用 toString 时,这样做非常有用。

然而,请注意 toString 的语法:定义前面的 override 关键字是必需的,这样 Scala 才能确认基类中存在相应的定义。这有助于预防因意外的输入错误导致难于觉察的 bug(Java 5 中创建 @Override 注释的动机也在于此)。还应注意,这里没有指定返回类型 —— 从方法体的定义很容易看出 —— 返回值没有用 return 关键字显式地标注,而在 Java 中则必须这样做。相反,函数中的最后一个值将被隐式地当作返回值(但是,如果您更喜欢 Java 语法,也可以使用 return 关键字)。


一些重要值

接下来分别是 numerdenom 的定义。这里涉及的语法可能让 Java 程序员认为 numerdenom 是公共的 Int 字段,它们分别被初始化为 n-over-gd-over-g;但这种想法是不对的。

在形式上,Scala 调用无参数的 numerdenom方法,这种方法用于创建快捷的语法以定义 accessor。Rational 类仍然有 3 个私有字段:ndg,但是,其中的 nd 被默认定义为私有访问,而 g 则被显式地定义为私有访问,它们对于外部都是隐藏的。

此时,Java 程序员可能会问:“nd 各自的 ‘setter’ 在哪里?” Scala 中不存在这样的 setter。Scala 的一个强大之处就在于,它鼓励开发人员以默认方式创建不可改变的对象。但是,也可使用语法创建修改 Rational 内部结构的方法,但是这样做会破坏该类固有的线程安全性。因此,至少对于这个例子而言,我将保持 Rational 不变。

当然还有一个问题,如何操纵 Rational 呢?与 java.lang.String 一样,不能直接修改现有的 Rational 的值,所以惟一的办法是根据现有类的值创建一个新的 Rational,或者从头创建。这涉及到 4 个名称比较古怪的方法:+-*/

与其外表相反,这并非操作符重载。


操作符

记住,在 Scala 中一切都是对象。在上一篇 文章 中, 您看到了函数本身也是对象这一原则的应用,这使 Scala 程序员可以将函数赋予变量,将函数作为对象参数传递等等。另一个同样重要的原则是,一切都是函数;也就是说,在此处,命名为 add 的函数与命名为 + 的函数没有区别。在 Scala 中,所有操作符都是类的函数。只不过它们的名称比较古怪罢了。

Rational 类中,为有理数定义了 4 种操作。它们是规范的数学操作:加、减、乘、除。每种操作以它的数学符号命名:+-*/

但是请注意,这些操作符每次操作时都构造一个新的 Rational 对象。同样,这与 java.lang.String 非常相似,这是默认的实现,因为这样可以产生线程安全的代码(如果线程没有修改共享状态 —— 默认情况下,跨线程共享的对象的内部状态也属于共享状态 —— 则不会影响对那个状态的并发访问)。

有什么变化?

一切都是函数,这一规则产生两个重要影响:

首先,您已经看到,函数可以作为对象进行操纵和存储。这使函数具有强大的可重用性,本系列 第一篇文章 对此作了探讨。

第二个影响是,Scala 语言设计者提供的操作符与 Scala 程序员认为应该 提供的操作符之间没有特别的差异。例如,假设提供一个 “求倒数” 操作符,这个操作符会将分子和分母调换,返回一个新的 Rational (即对于 Rational(2,5) 将返回 Rational(5,2))。如果您认为 ~ 符号最适合表示这个概念,那么可以使用此符号作为名称定义一个新方法,该方法将和 Java 代码中任何其他操作符一样,如清单 5 所示:

清单 5. 求倒数
  val r6 = ~r1
  Console.println(r6) // should print [3 / 1], since r1 = [1 / 3]

在 Scala 中定义这种一元 “操作符” 需要一点技巧,但这只是语法上的问题而已:

清单 6. 如何求倒数
class Rational(n:Int, d:Int)
{
  // ... as before ...

  def unary_~ : Rational =
    new Rational(denom, numer)
}

当然,需要注意的地方是,必须在名称 ~ 之前加上前缀 “unary_”,告诉 Scala 编译器它属于一元操作符。因此,该语法将颠覆大多数对象语言中常见的传统 reference-then-method 语法。

这条规则与 “一切都是对象” 规则结合起来,可以实现功能强大(但很简单)的代码:

清单 7. 求和
  1 + 2 + 3 // same as 1.+(2.+(3))
  r1 + r2 + r3 // same as r1.+(r2.+(r3))

当然,对于简单的整数加法,Scala 编译器也会 “得到正确的结果”,它们在语法上是完全一样的。这意味着您可以开发与 Scala 语言 “内置” 的类型完全相同的类型。

Scala 编译器甚至会尝试推断具有某种预定含义的 “操作符” 的其他含义,例如 += 操作符。注意,虽然 Rational 类并没有显式地定义 +=,下面的代码仍然会正常运行:

清单 8. Scala 推断
  var r5 = new Rational(3,4)
  r5 += r1
  Console.println(r5)

打印结果时,r5 的值为 [13 / 12],结果是正确的。


Scala 内幕

记住,Scala 将被编译为 Java 字节码,这意味着它在 JVM 上运行。如果您需要证据,那么只需注意编译器生成以 0xCAFEBABE 开头的 .class 文件,就像 javac 一样。另外请注意,如果启动 JDK 自带的 Java 字节码反编译器(javap),并将它指向生成的 Rational 类,将会出现什么情况,如清单 9 所示:

清单 9. 从 rational.scala 编译的类
C:\Projects\scala-classes\code>javap -private -classpath classes Rational
Compiled from "rational.scala"
public class Rational extends java.lang.Object implements scala.ScalaObject{
    private int denom;
    private int numer;
    private int g;
    public Rational(int, int);
    public Rational unary_$tilde();
    public java.lang.String toString();
    public Rational $div(Rational);
    public Rational $times(Rational);
    public Rational $minus(Rational);
    public Rational $plus(Rational);
    public int denom();
    public int numer();
    private int g();
    private int gcd(int, int);
    public Rational(int);
    public int $tag();
}


C:\Projects\scala-classes\code>

Scala 类中定义的 “操作符” 被转换成传统 Java 编程中的方法调用,不过它们仍使用看上去有些古怪的名称。类中定义了两个构造函数:一个构造函数带有一个 int 参数,另一个带有两个 int 参数。您可能会注意到,大写的 Int 类型与 java.lang.Integer 有点相似,Scala 编译器非常聪明,会在类定义中将它们转换成常规的 Java 原语 int

测试 Rational 类

一种著名的观点认为,优秀的程序员编写代码,伟大的程序员编写测试;到目前为止,我还没有对我的 Scala 代码严格地实践这一规则,那么现在看看将这个 Rational 类放入一个传统的 JUnit 测试套件中会怎样,如清单 10 所示:

清单 10. RationalTest.java
import org.junit.*;
import static org.junit.Assert.*;

public class RationalTest
{
    @Test public void test2ArgRationalConstructor()
    {
        Rational r = new Rational(2, 5);

        assertTrue(r.numer() == 2);
        assertTrue(r.denom() == 5);
    }
    
    @Test public void test1ArgRationalConstructor()
    {
        Rational r = new Rational(5);

        assertTrue(r.numer() == 0);
        assertTrue(r.denom() == 1);
            // 1 because of gcd() invocation during construction;
            // 0-over-5 is the same as 0-over-1
    }    
    
    @Test public void testAddRationals()
    {
        Rational r1 = new Rational(2, 5);
        Rational r2 = new Rational(1, 3);

        Rational r3 = (Rational) reflectInvoke(r1, "$plus", r2); //r1.$plus(r2);

        assertTrue(r3.numer() == 11);
        assertTrue(r3.denom() == 15);
    }
    
    // ... some details omitted
}

除了确认 Rational 类运行正常之外,上面的测试套件还证明可以从 Java 代码中调用 Scala 代码(尽管在操作符方面有点不匹配)。当然,令人高兴的是,您可以将 Java 类迁移至 Scala 类,同时不必更改支持这些类的测试,然后慢慢尝试 Scala。

您惟一可能觉得古怪的地方是操作符调用,在本例中就是 Rational 类中的 + 方法。回顾一下 javap 的输出,Scala 显然已经将 + 函数转换为 JVM 方法 $plus,但是 Java 语言规范并不允许标识符中出现 $ 字符(这正是它被用于嵌套和匿名嵌套类名称中的原因)。

为了调用那些方法,需要用 Groovy 或 JRuby(或者其他对 $ 字符没有限制的语言)编写测试,或者编写 Reflection 代码来调用它。我采用后一种方法,从 Scala 的角度看这不是那么有趣,但是如果您有兴趣的话,可以看看本文的代码中包含的结果(参见 下载)。

注意,只有当函数名称不是合法的 Java 标识符时才需要用这类方法。


“更好的” Java

我学习 C++ 的时候,Bjarne Stroustrup 建议,学习 C++ 的一种方法是将它看作 “更好的 C 语言”(参见 参考资料)。在某些方面,如今的 Java 开发人员也可以将 Scala 看作是 “更好的 Java”,因为它提供了一种编写传统 Java POJO 的更简洁的方式。考虑清单 11 中显示的传统 Person POJO:

清单 11. JavaPerson.java(原始 POJO)
public class JavaPerson
{
    public JavaPerson(String firstName, String lastName, int age)
    {
        this.firstName = firstName;
        this.lastName = lastName;
        this.age = age;
    }
    
    public String getFirstName()
    {
        return this.firstName;
    }
    public void setFirstName(String value)
    {
        this.firstName = value;
    }
    
    public String getLastName()
    {
        return this.lastName;
    }
    public void setLastName(String value)
    {
        this.lastName = value;
    }
    
    public int getAge()
    {
        return this.age;
    }
    public void setAge(int value)
    {
        this.age = value;
    }
    
    public String toString()
    {
        return "[Person: firstName" + firstName + " lastName:" + lastName +
            " age:" + age + " ]";
    }
    
    private String firstName;
    private String lastName;
    private int age;
}

现在考虑用 Scala 编写的对等物:

清单 12. person.scala(线程安全的 POJO)
class Person(firstName:String, lastName:String, age:Int)
{
    def getFirstName = firstName
    def getLastName = lastName
    def getAge = age

    override def toString =
        "[Person firstName:" + firstName + " lastName:" + lastName +
            " age:" + age + " ]"
}

这不是一个完全匹配的替换,因为原始的 Person 包含一些可变的 setter。但是,由于原始的 Person 没有与这些可变 setter 相关的同步代码,所以 Scala 版本使用起来更安全。而且,如果目标是减少 Person 中的代码行数,那么可以删除整个 getFoo 属性方法,因为 Scala 将为每个构造函数参数生成 accessor 方法 —— firstName() 返回一个 StringlastName() 返回一个 Stringage() 返回一个 int

即使必须包含这些可变的 setter 方法,Scala 版本仍然更加简单,如清单 13 所示:

清单 13. person.scala(完整的 POJO)
class Person(var firstName:String, var lastName:String, var age:Int)
{
    def getFirstName = firstName
    def getLastName = lastName
    def getAge = age
    
    def setFirstName(value:String):Unit = firstName = value
    def setLastName(value:String) = lastName = value
    def setAge(value:Int) = age = value

    override def toString =
        "[Person firstName:" + firstName + " lastName:" + lastName +
            " age:" + age + " ]"
}

注意,构造函数参数引入了 var 关键字。简单来说, var 告诉编译器这个值是可变的。因此,Scala 同时生成 accessor( String firstName(void))和 mutator(void firstName_$eq(String))方法。然后,就可以方便地创建 setFoo 属性 mutator 方法,它在幕后使用生成的 mutator 方法。


结束语

Scala 将函数概念与简洁性相融合,同时又未失去对象的丰富特性。从本系列中您可能已经看到,Scala 还修正了 Java 语言中的一些语法问题(后见之明)。

本文是面向 Java 开发人员的 Scala 指南 系列中的第二篇文章,本文主要讨论了 Scala 的对象特性,使您可以开始使用 Scala,而不必深入探究函数方面。应用目前学到的知识,您现在可以使用 Scala 减轻编程负担。而且,可以使用 Scala 生成其他编程环境(例如 Spring 或 Hibernate )所需的 POJO。

但是,请继续关注本系列,下期文章将开始讨论 Scala 的函数方面。


下载

描述名字大小
本文的示例 Scala 代码j-scala02198-code.zip2.6MB

参考资料

学习

获得产品和技术

  • Scala:下载 Scala 并使用本系列学习它!
  • SUnit:这是标准 Scala 发行版的一部分, 位于 scala.testing 包中。

讨论

条评论

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
ArticleID=295568
ArticleTitle=面向 Java 开发人员的 Scala 指南: 类操作
publish-date=03182008