Scala入门:对象

大数据学习路线图

【版权声明】博客内容由厦门大学数据库实验室拥有版权,未经允许,请勿转载!
[返回Spark教程首页]

Scala并没有提供Java那样的静态方法或静态字段,但是,可以采用object关键字实现单例对象,具备和Java静态方法同样的功能。

单例对象

下面是单例对象的定义:

object Person {
    private var lastId = 0  //一个人的身份编号
    def newPersonId() = {
        lastId +=1
        lastId
    }
}

从上面的定义可以看出,单例对象的定义和类的定义很相似,明显的区分是,用object关键字,而不是用class关键字。
假设有一个班级人员管理系统,每当新来一个班级成员,就给分配一个身份编号。当第一个人加入班级时,你就可以调用Person.newPersonId()获得身份编号。
现在,我们把上述代码放入到一个test.scala文件中测试运行一个,看看效果。请登录Linux系统,进入shell命令提示符状态,然后,输入以下命令进入“/usr/local/scala/mycode”目录,打开vim编辑器:

cd /usr/local/scala/mycode
vim test.scala

在test.scala文件中输入以下内容:

object Person {
    private var lastId = 0  //一个人的身份编号
    def newPersonId() = {
        lastId +=1
        lastId
    }
}
printf("The first person id is %d.\n",Person.newPersonId())
printf("The second person id is %d.\n",Person.newPersonId())
printf("The third person id is %d.\n",Person.newPersonId())

保存文件并退出vim编辑器,然后,在Shell命令提示符下输入scala命令运行上面代码:

scala test.scala //不能使用scalac编译,直接运行即可

执行后,屏幕上会显示以下结果:

The first person id is 1.
The second person id is 2.
The third person id is 3.

注意,对于一个Scala应用程序而言,必须包含main方法,由于上面代码中没有包含main方法,因此,不能使用scalac命令进行编译,而是直接使用scala命令运行代码,就可以得到结果;如果使用scalac命令去编译test.scala文件,就会报错。

伴生对象

在Java中,我们经常需要用到同时包含实例方法和静态方法的类,在Scala中可以通过伴生对象来实现。当单例对象与某个类具有相同的名称时,它被称为这个类的“伴生对象”。类和它的伴生对象必须存在于同一个文件中,而且可以相互访问私有成员(字段和方法)。

下面通过一个实例演示一下伴生对象的使用方法。可以使用下面命令,首先删除刚才创建的test.scala文件:

cd /usr/local/scala/mycode
rm test.scala

然后,在“/usr/local/scala/mycode”目录下,用vim编辑器重新创建一个test.scala,在该文件中输入如下代码:

class Person {
    private val id = Person.newPersonId() //调用了伴生对象中的方法
    private var name = ""
    def this(name: String) {
        this()
        this.name = name
    }
    def info() { printf("The id of %s is %d.\n",name,id)}
}
object Person {
    private var lastId = 0  //一个人的身份编号
    private def newPersonId() = {
        lastId +=1
        lastId
    }
    def main(args: Array[String]){
        val person1 = new Person("Ziyu")
        val person2 = new Person("Minxing")
        person1.info()
        person2.info()      
    }
}

然后,在Shell命令提示符状态下,输入以下命令编译并执行:

scalac test.scala
scala Person //这里用Person,就是上面代码中object关键字后面的Person

运行结果如下:

The id of Ziyu is 1.
The id of Minxing is 2.

从上面结果可以看出,伴生对象中定义的newPersonId()实际上就实现了Java中静态(static)方法的功能,所以,实例化对象person1调用newPersonId()返回的值是1,实例化对象person2调用newPersonId()返回的值是2。我们说过,Scala源代码编译后都会变成JVM字节码,实际上,在编译上面的源代码文件以后,在Scala里面的class和object在Java层面都会被合二为一,class里面的成员成了实例成员,object成员成了static成员。为了验证这一点,我们可以一起测试一下。还是使用上面的代码文件,这里做一点小小修改,那就是把object Person中的newPersonId()方法前面的private去掉(后面会说明原因):
现在请在“/usr/local/scala/mycode”目录下,用vim编辑器重新创建一个test.scala,在该文件中输入如下代码:

class Person {
    private val id = Person.newPersonId() //调用了伴生对象中的方法
    private var name = ""
    def this(name: String) {
        this()
        this.name = name
    }
    def info() { printf("The id of %s is %d.\n",name,id)}
}
object Person {
    private var lastId = 0  //一个人的身份编号
    def newPersonId() = { //注意,这里已经没有private
        lastId +=1
        lastId
    }
    def main(args: Array[String]){
        val person1 = new Person("Ziyu")
        val person2 = new Person("Minxing")
        person1.info()
        person2.info()      
    }
}

然后,在Shell命令提示符状态下,输入以下命令编译并执行:

scalac test.scala
ls //显示当前目录下生成的编译结果文件

这时,你会在目录下看到两个编译后得到的文件,即Person.class和Person$.class。经过编译后,伴生类和伴生对象在JVM中都被合并到了一起。我们忽略Person$.class,这里看一下Person.class。请使用下面命令进行“反编译”:

javap Person

执行结果如下:

Compiled from "test.scala"
public class Person {
  public static void main(java.lang.String[]);
  public static int newPersonId();
  public void info();
  public Person();
  public Person(java.lang.String);
}

从上面的结果可以看出,经过编译后,伴生类Person中的成员和伴生对象Person中的成员都被合并到一起,并且,伴生对象中的方法newPersonId(),成为静态方法。现在补充说明一下,为什么上面要把伴生对象Person的定义中newPersonId()的private修饰符去掉,因为,如果不去掉,作为伴生对象的私有方法,在javap反编译后,在执行结果中是看不到这个方法newPersonId()的。

应用程序对象

每个Scala应用程序都必须从一个对象的main方法开始,我们之前介绍的HelloWorld程序就是一个非常典型的例子。
我们可以在“/usr/local/scala/mycode”目录下,用vim编辑器重新创建一个test.scala,在该文件中输入如下代码:

object HelloWorld {
    def main(args: Array[String]){
        println("Hello, World!")
    }
}

为了运行上述代码,我们现在可以使用两种不同的方法。
第一种方法:直接使用scala命令运行得到结果。
因为这段代码中没有定义类,就是一个单例对象,因此,可以不用编译,直接使用scala命令运行得到结果,命令如下:

cd /usr/local/scala/mycode
scala test.scala

第二种方法:先编译再执行
可以首先使用scalac编译命令对test.scala进行编译,然后,使用scala命令运行,具体如下:

cd /usr/local/scala/mycode
scalac test.scala
scala -classpath . HelloWorld //这里的HelloWorld是在test.scala文件中object关键字后面的HelloWorld

apply方法和update方法

我们经常会用到对象的apply方法和update方法,虽然我们表面上并没有察觉,但是,实际上,在Scala中,apply方法和update方法都会遵循相关的约定被调用,约定如下:用括号传递给变量(对象)一个或多个参数时,Scala 会把它转换成对apply方法的调用;与此相似的,当对带有括号并包括一到若干参数的对象进行赋值时,编译器将调用对象的update方法,在调用时,是把括号里的参数和等号右边的对象一起作为update方法的输入参数来执行调用(在本博客的末尾有一个update实例,有助于对这句话的理解)。

下面我们测试一下apply方法是否被调用。可以在Linux系统的“/usr/local/scala/mycode/test.scala”文件中输入以下代码:

class TestApplyClass {

    def apply(param: String): String = {

        println("apply method called, parameter is: " + param)

        "Hello World!"
    }
}
val myObject = new TestApplyClass
println(myObject("param1"))

然后,在Linux系统的Shell命令提示符下运行scala命令:

scala test.scala

运行后会得到以下结果:

apply method is called, parameter is:param1
Hello World!

如果把println(myObject("param1"))这个语句给注释掉,就不会出现上面结果。可以看出,apply方法确实被调用了,而且是在执行myObject("param1")时被调用的,调用后,会把“Hello World!”作为返回值,因此,执行println(myObject("param1"))语句后,会打印出“Hello World”。

上面是类中定义了apply方法,下面看一个在单例对象中定义apply方法的例子:

object TestApplySingleObject {
    def apply(param1: String, param2: String): String = {
        println("apply method called")
        param1 + " and " + param2
    }
}

val group = TestApplySingleObject("Zhangfei", "Liubei")
println(group)

把上面代码放入到test.scala文件中测试执行后,可以得到如下结果:

apply method called
Zhangfei and Liubei

可以看出,在执行TestApplySingleObject("Zhangfei", "Liubei")时调用了apply方法,并且把“Zhangfei and Liubei”作为返回值,赋值给group变量,因此,println(group)语句会打印出“Zhangfei and Liubei”。

下面我们测试一个伴生类和伴生对象中的apply方法实例。请在Linux系统的“/usr/local/scala/mycode/test.scala”文件中输入以下代码:

class TestApplyClassAndObject {
}
class ApplyTest{
    def apply() = println("apply method in class is called!")
    def greetingOfClass: Unit ={
        println("Greeting method in class is called.")
    }
}
object ApplyTest{
     def apply() = {
          println("apply method in object is called")
        new ApplyTest()
     }
}
object  TestApplyClassAndObject{
     def main (args: Array[String]) {       
        val a = ApplyTest() //这里会调用伴生对象中的apply方法       
        a.greetingOfClass
        a() // 这里会调用伴生类中的apply方法         
    }
}

首先使用scalac编译命令对test.scala进行编译,然后,使用scala命令运行,具体如下:

cd /usr/local/scala/mycode
scalac test.scala
scala  -classpath . TestApplyClassAndObject //这里的TestApplyClassAndObject是在test.scala文件中object关键字后面的TestApplyClassAndObject

上述代码执行后得到以下结果:

apply method in object is called
Greeting method in class is called.
apply method in class is called!

从上面代码可以看出,当我们执行val a = ApplyTest()时,会导致apply方法的调用并返回该方法调用的值,也就是ApplyTest的实例化对象。当执行a()时,又会导致调用伴生类的apply方法,如果我们愿意,就可以在伴生类的apply方法中写入一些处理逻辑,这样就可以把传入的参数赋值给实例化对象的变量。
下面看一个apply方法的例子。由于Scala中的Array对象定义了apply方法,因此,我们就可以采用如下方式初始化一个数组:

val myStrArr = Array("BigData","Hadoop","Spark")

也就是说,不需要new关键字,不用构造器,直接给对象传递3个参数,Scala就会转换成对apply方法的调用,也就是调用Array类的伴生对象Array的apply方法,完成数组的初始化。

讲到这里,我们已经理解了apply方法,这样,我们现在解释Scala中伴生对象的一个重要用途,你就不会存在理解障碍了。在Scala中,伴生对象有一个重要用途,那就是,我们通常将伴生对象作为工厂使用,这样就不需要使用关键字new来创建一个实例化对象了,具体实例如下:

class Car(name: String){
    def info() {println("Car name is "+ name)}
}
object Car {
  def apply(name: String) = new Car(name) //apply方法会调用伴生类Car的构造方法创建一个Car类的实例化对象
}
object MyTest{
     def main (args: Array[String]) {       
        val mycar = Car("BMW") //这里会调用伴生对象中的apply方法,apply方法会创建一个Car类的实例化对象
                mycar.info()
    }
}

首先使用scalac编译命令对test.scala进行编译,然后,使用scala命令运行,具体如下:

cd /usr/local/scala/mycode
scalac test.scala
scala  -classpath . MyTest //这里的MyTest是在test.scala文件中object关键字后面的MyTest

上述代码执行后得到以下结果:

Car name is BMW

前面介绍了apply方法,实际上,update方法也是类似的(感兴趣的读者,可以查找网络资料学习update方法如何测试,这里不再赘述),比如:

val myStrArr = new Array[String](3) //声明一个长度为3的字符串数组,每个数组元素初始化为null
myStrArr(0) = "BigData" //实际上,调用了伴生类Array中的update方法,执行myStrArr.update(0,"BigData")
myStrArr(1) = "Hadoop" //实际上,调用了伴生类Array中的update方法,执行myStrArr.update(1,"Hadoop")
myStrArr(2) = "Spark" //实际上,调用了伴生类Array中的update方法,执行myStrArr.update(2,"Spark")

从上面可以看出,在进行元组赋值的时候,之所以没有采用Java中的方括号myStrArr[0],而是采用圆括号的形式,myStrArr(0),是因为存在上述的update方法的机制。

子雨大数据之Spark入门
扫一扫访问本博客