面向 Java 开发人员的 Scala 指南: 类操作
上一篇 / 下一篇 2008-05-19 21:35:11 / 个人分类:Scala
Scala 的函数编程特性非常引人注目,但这并非 Java 开发人员应该对这门语言感兴趣的惟一原因。实际上,Scala 融合了函数概念和面向对象概念。为了让 Java 和 Scala 程序员感到得心应手,可以了解一下 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类的默认的、首选的构造函数:n和d是构造函数的参数。
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初始化为n和d的最大公分母,然后用它依次设置n和d。
回顾一下清单 1就会发现,我创建了一个覆盖的toString方法来返回Rational的值,在RunRational驱动程序代码中使用toString时,这样做非常有用。
然而,请注意toString的语法:定义前面的override关键字是必需的,这样 Scala 才能确认基类中存在相应的定义。这有助于预防因意外的输入错误导致难于觉察的 bug(Java 5 中创建@Override注释的动机也在于此)。还应注意,这里没有指定返回类型 —— 从方法体的定义很容易看出 —— 返回值没有用return关键字显式地标注,而在 Java 中则必须这样做。相反,函数中的最后一个值将被隐式地当作返回值(但是,如果您更喜欢 Java 语法,也可以使用return关键字)。
|
接下来分别是numer和denom的定义。这里涉及的语法可能让 Java 程序员认为numer和denom是公共的Int字段,它们分别被初始化为n-over-g和d-over-g;但这种想法是不对的。
在形式上,Scala 调用无参数的numer和denom方法,这种方法用于创建快捷的语法以定义 accessor。Rational类仍然有 3 个私有字段:n、d和g,但是,其中的n和d被默认定义为私有访问,而g则被显式地定义为私有访问,它们对于外部都是隐藏的。
此时,Java 程序员可能会问:“n和d各自的 ‘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 将被编译为 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。
一种著名的观点认为,优秀的程序员编写代码,伟大的程序员编写测试;到目前为止,我还没有对我的 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 标识符时才需要用这类方法。
|
我学习 C++ 的时候,Bjarne Stroustrup 建议,学习 C++ 的一种方法是将它看作 “更好的 C 语言”(参见参考资料)。在某些方面,如今的 Java 开发人员也可以将 Scala 看作是 “更好的 Java”,因为它提供了一种编写传统 Java POJO 的更简洁的方式。考虑清单 11 中显示的传统PersonPOJO:
清单 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()返回一个String,lastName()返回一个String,age()返回一个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 的函数方面。
导入论坛 引用链接 收藏 分享给好友 推荐到圈子 管理 举报
TAG:

