在上个月的文章中 ,您仅了解了Scala的语法,这是运行Scala程序并观察其一些更简单功能所必需的最低限度。 该文章中的Hello World和Timer示例使您可以看到Scala的Application
类,其用于方法定义和匿名函数的语法,对Array[]
的一瞥以及对类型推断的一些了解。 Scala提供了更多的功能,因此本文研究了Scala编码的复杂性。
Scala的函数式编程功能引人注目,但这并不是Java开发人员对该语言感兴趣的唯一原因。 实际上,Scala融合了功能概念和面向对象。 为了让Java-cum-Scala程序员有种宾至如归的感觉,有必要查看Scala的对象功能,并了解它们如何在语言上映射到Java。 请记住,其中某些功能没有直接映射,或者在某些情况下,“映射”更多是模拟而不是直接并行。 但是在区别很重要的地方,我会指出。
Scala也有课程
与其开始对Scala支持的类功能进行冗长而抽象的讨论,不让我们看一看一个类的定义,该类可用于为Scala平台带来合理的数字支持(主要从“ Scala By Example”(Scala通过示例) 相关主题 ):
清单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-over-2(也称为“一半”)添加到2-over-4(也称为“四分之四”),那么Rational
类应该足够聪明以实现2-over -4与1-over-2相同,请先进行相应转换,然后再将两者相加。
这是Rational
类内部嵌套的私有gcd()
函数和g值的目的。 当在Scala中调用构造函数时,将评估该类的整个主体,这意味着g将使用n和d的最大公分母进行初始化,然后依次用于适当地设置n和d 。
回顾清单1 ,还很容易看到我创建了一个重写的toString
方法以返回Rational
的值,当我从RunRational
驱动程序代码开始RunRational
它时,这将非常有用。
请注意toString
周围的语法,但是:定义前面的override
关键字是必需的,以便Scala可以检查以确保基类中存在相应的定义。 这可以帮助防止因意外的键盘滑动而产生的细微错误。 (正是这种动机导致在Java 5中创建@Override
批注。)同样,请注意,未指定返回类型-从方法主体的定义中可以明显看出-并且返回的是值未使用Java要求的return
关键字明确表示。 而是将函数中的最后一个值隐式视为返回值。 (但是,如果您喜欢Java语法,则可以始终使用return
关键字。)
一些核心价值
接下来是定义numer
和denom
,分别。 语法介入,副手,会导致Java程序员认为numer
和denom
是公开的Int
是初始化为分别正过G和d-过克 ,价值领域; 但是这个假设是不正确的。
形式上,斯卡拉称numer
和denom
方法不带参数 ,这是用来创建定义存取一个快速和容易的语法。 在Rational
类仍然有三个私有字段,N,d,和g,但他们来自世界由n和d的情况下,默认的专用通道,并通过G的情况下,明确私接隐患。
此时,您中的Java程序员可能会问:“ n和d的相应“设置程序”在哪里? 没有这样的二传手。 Scala的部分功能是鼓励开发人员默认创建不可变对象。 当然,可以使用语法来创建用于修改Rational
内部的方法,但是这样做会破坏此类的隐式线程安全性。 其结果是,至少在这个例子中,我要离开Rational
,因为它是。
自然地,这就提出了一个问题,即如何操纵一个Rational
。 像java.lang.String
一样,您不能采用现有的Rational
并修改其值,因此唯一的选择是从现有值中创建新的Rational
,或者从头开始创建它。 这使下一组四个方法成为焦点:名为+
, -
, *
和/
方法。
不,与其看起来相反,这不是操作员超载。
接线员,给我打电话
请记住,在Scala中, 一切都是对象 。 在上一篇文章中 ,您了解了该原理如何适用于函数本身就是对象的想法,这使Scala程序员可以将函数分配给变量,将函数作为对象参数传递,等等。 同样重要的原则是一切都是函数 ; 也就是说,在这种特殊情况下,名为add
的函数和名为+
的函数没有区别。 在Scala中,所有运算符都是类上的函数。 他们只是碰巧有个时髦的名字。
然后,在Rational
类中,为有理数定义了四个运算。 这些是规范的数学运算加,减,乘和除。 其中每一个均以其数学符号命名: +
, -
, *
和/
。
但是请注意,每个运算符都通过每次构造一个新的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编译器它打算成为一元运算符; 因此,该语法将与大多数对象语言中常见的传统“引用-然后-方法”语法“相背离”。
请注意,这与“一切都是对象”规则相结合,创建了一些功能强大但易于解释的代码机会:
清单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]
r5
[13 / 12]
,正好是该值。
引擎盖下的Scala
请记住,Scala会编译为Java字节码,这意味着它可以在JVM上运行。 如果需要证明, 0xCAFEBABE
就像编译器正在生成以0xCAFEBABE
开头的.class文件0xCAFEBABE
,就像javac
一样。 还请注意,如果启动JDK( javap
)附带的Java字节码反汇编程序并将其指向生成的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
。类定义。
测试,测试1-2-3 ...
众所周知,优秀的程序员编写代码,优秀的程序员编写测试是一个模因。 到目前为止,我一直在为我的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
有一些可变的二传手,这并不是一个完全的替代品。 但是考虑到原始的Person
也没有围绕这些可变设置器的同步代码,因此Scala版本使用起来更安全。 同样,如果目标是真正减少Person
的代码行数,则可以完全删除getFoo
属性方法,因为Scala会围绕每个构造函数参数生成访问器方法getFoo
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生成访问器( String firstName(void)
)和mutator( void firstName_$eq(String)
)方法。 这样就很容易创建在setFoo
使用生成的mutator方法的setFoo
属性mutator方法。
结论
Scala尝试将功能性概念和简洁性结合在一起,而又不会失去对象范式的丰富性。 正如您可能在本系列文章中开始看到的那样,Scala还纠正了Java语言中发现的一些令人讨厌的(事后看来)语法问题。
繁忙的Java开发人员Scala系列指南中的第二篇文章重点介绍了Scala的对象工具,使您可以开始使用Scala,而不必深入研究功能池。 根据到目前为止的经验,您可以开始使用Scala减少编程工作量。 除其他外,您可以使用Scala生成与其他编程环境(例如Spring或Hibernate)相同的POJO。
但是,请保留您的潜水帽和潜水用具,因为下个月的文章将标志着我们开始进入功能池深层的起点。
翻译自: https://www.ibm.com/developerworks/java/library/j-scala02198/index.html