印度朋友手把手教你学Scala(4):对象


/**
 * 谨献给我最爱的YoYo
 * 原文出处:https://madusudanan.com/blog/scala-tutorials-part-4-objects/
 * @author dogstar.huang <chanzonghuang@gmail.com> 2017-03-01
 */

本翻译已征得Madusudanan.B.N同意,并链接在原文教程前面。

Scala的对象

这是关于Scala系列教程的第四章。点击这里可查看整个系列。

在这章,我们将讲解objects

目录

  • 什么是对象?
  • Java单例的概念和Scala对象的比较
  • 静态与面向对象编程
  • 伴生对象
  • 重温Hello World
  • 深入理解main方法

什么是对象?

在Scala里,object是一个关键字,在这个教程系列的第一章我们就知道了。

再来回顾看一下Hello World这段代码。

object Test {  
    def main(args : Array[String]){
      println("Hello world")
    }
  }

这和Java的Hello World示例相似,除了main方法不是在class里而是在object里这一事实。这可能会令人困惑,因为在面向对象世界里一个object意味着一个class的实例。

在Scala,这个单词/关键字object有意义着两件事。

1) 像Java一样的类实例
2) 描述单例object的一个关键字,即实例。但非常不同。

Java单例的概念和Scala对象的比较

大部分人都可能已经听说过/使用过Singleton设计模式。但出于完整性的缘故,这里也给出一个示例。

public class DatabaseConnectionSingleton {

    private DatabaseConnectionSingleton dbInstance;

    private DatabaseConnectionSingleton(){

    }


    public DatabaseConnectionSingleton getInstance(){

        if(dbInstance == null){
            dbInstance = new DatabaseConnectionSingleton();
            return dbInstance;
        }

        else
            return dbInstance;

    }

}

这个理念相当简单,如果这里已经创建了一个实例,那么就返回它,否则就创建一个新的并且返回它。

当在应用里,无论何时,都只想要一个类的一个单一实例时,可以使用单例。然而,上面这种方式不是线程安全的。

有方式也可以让它变成线程安全,但我们不打算去了解它。这个示例只是为了演示单例这一概念,而不是实现的要点。

Scala的object类似,除了单例实例是由这门语言/编译器来照料而不是由程序员明确来做外。

既然不存在唯一实例,也就没有对象创建这一概念,并且这是Scala强制的。以下是一个示例。

这是一个编译错误,你可以在IDE里试一下,当你输入时它会有错误提示。

但它不仅仅是一个单例。它把来自Java的几个概念包含成了一个单一的优雅的抽象。

静态与面向对象编程

上面的示例直接访问了对象里面的方法,而不用new关键字,这和Java里的static做的事情相似。

如果你细想一下,Java的static就像是把两件个东西混合到了单一的概念里,并且不是很合适。

以下是一个很简单的例子,演示了为什么它会这样。

简单地,带有一个static方法的Parent类。

public class Parent {

    public static void main(String[] args) {
        printHelloMsg();
    }

    public static void printHelloMsg() {
        System.out.println("Hello there !!!!");
    }

}

一个继承了Parent类的类。

public class Child extends Parent {

    public static void main(String[] args) {
        Child t = new Child();
        t.printHelloMsg();
    }


    // 重载会抛出一个错误
    @Override
    public static void printHelloMsg(){
        System.out.println("Just a test");
    }

}

重载的话会抛出一个错误。

从上面这个幼稚的示例可以了解到,static和面向对象编程不能很好地在一起玩耍。继承一个父类明显的目的就是继承它全部的方法,并且由实现类来决定是否继续沿用默认的行为或者是修改它。

但是再一次,在这种情况下,仅是为了printHelloMsg提供的功能这一缘故,没必要创建不需要的Parent类的实例。

我们真正需要的是一个单例对象,即准确来说一个实例,但正如我们在前一个主题里看到的,单例不善于并行/多线程方面。

Scala根本没有static关键字作为语言里的一部分,但对于这种使用场景有另外的东西,那就是伴生对象。

伴生对象

在这里我们稍微有点超前了,因为我还没有解释类,但他们和Java同行非常相似。

来看一看下面这个示例。

class Person {

  var name = "noname"

  // 实例方法
  def getPersonName: String = {
      name
  }

}

object Person{

  // 静态方法
  def isNameSet(p:Person) : Boolean = {
    if(p.name == "noname") false else true
  }


}

这有一个叫做Person的class,也有一个叫做Person的object。正如注释里解释的,我们有一个返回人的名字的实例方法,和一个判断Person对象是否设置了名字的静态方法。这可以是类本身的一部分,但出于简单起见,假设它一个静态工具方法。

你可以像下面这样调用这些方法。

object Main extends App {

  val p = new Person()

  // 调用实例方法
  println(p.getPersonName)

  // 调用静态方法
  println(Person.isNameSet(p))


}

等效的Java代码非常相似。

public class Person {

    public String name = "noname";


    public String getPersonName() {
        return name;
    }

    public static boolean isNameSet(Person p){
        if(p.name() == "noname"){
            return false;
        }
        else{
            return true;
        }
    }

}

我们实现了Scala里相同的功能,但主要的区别是代码/设计更为优雅。

请记住,这个类和它的伴生对象应该在同一个文件里,否则会有一个编译错误。事实上,这使得管理类和关联的伴生对象更为简单。

现在我们有了一个紧凑的模型,即单例实例并且它能很好地和面向对象编程融合在一起。如果我们想要某种东西是面向对象的,即基于实例,那么我们可以把它放在原来的类中,或者把它放在一个对象里,这样更有意义。

重温Hello World

在Scala,创建一个应用的入口有两种方式。回顾一个在第一章我们是如何实现第一个Hello World的。

object Test {  
  def main(args : Array[String]){
    println("Hello world")
  }
}

这和Java的main方法很相似,除了它是在一个object中外。

public class Test {  
    public static void main(String[] args) {
        System.out.println("Hello world");
    }
}

一个微妙的区别是在Java的main方法被声明为static方法。如前面所述,所有对象里的所有东西都是静态Scala等效项,所以def main类似Java等效项那样工作。

还有另一种创建一个可执行对象的方式,那就是通过一个trait

Scala的特质类型Java的接口,但又有很多区别。我们会在这个系列的后面探讨特质。

object Test extends App{  
  println("Hello world!!")
}

这样就扩展了一个叫做App的特质。实际上,可以看一下Appr的源代码,从而学习到更多东西。

来自注释里值得注意的一段是“App特质可以用于快速把对象转换成可执行的程序”。

它是通过继承App.scala特质里的main方法来做到这一点的。

深入理解main方法

Java里的传统理解是,每一段程序都有一个方法名字叫做main并且是静态的入口。

既然Scala里没有static关键字,让我们深挖一点以便理解到底是怎么回事。

我们知道在Scala有两种创建一个可运行应用的方式。一种是使用某个对象里的main方法,而另一个则是通过扩展App特质。来看一下这两种方式反编译的版本。

继承App特质的对象

实际代码:

object RunExample extends App{

 println("Hello World !!")

}

反编译:

public final class RunExample {  
  public static void main(java.lang.String[]);
    Code:
       0: getstatic     #16                 // Field RunExample$.MODULE$:LRunExample$;
       3: aload_0
       4: invokevirtual #18                 // Method RunExample$.main:([Ljava/lang/String;)V
       7: return

  public static void delayedInit(scala.Function0<scala.runtime.BoxedUnit>);
    Code:
       0: getstatic     #16                 // Field RunExample$.MODULE$:LRunExample$;
       3: aload_0
       4: invokevirtual #22                 // Method RunExample$.delayedInit:(Lscala/Function0;)V
       7: return

  public static java.lang.String[] args();
    Code:
       0: getstatic     #16                 // Field RunExample$.MODULE$:LRunExample$;
       3: invokevirtual #26                 // Method RunExample$.args:()[Ljava/lang/String;
       6: areturn

  public static void scala$App$_setter_$executionStart_$eq(long);
    Code:
       0: getstatic     #16                 // Field RunExample$.MODULE$:LRunExample$;
       3: lload_0
       4: invokevirtual #30                 // Method RunExample$.scala$App$_setter_$executionStart_$eq:(J)V
       7: return

  public static long executionStart();
    Code:
       0: getstatic     #16                 // Field RunExample$.MODULE$:LRunExample$;
       3: invokevirtual #34                 // Method RunExample$.executionStart:()J
       6: lreturn

  public static void delayedEndpoint$RunExample$1();
    Code:
       0: getstatic     #16                 // Field RunExample$.MODULE$:LRunExample$;
       3: invokevirtual #38                 // Method RunExample$.delayedEndpoint$RunExample$1:()V
       6: return
}

可以看到这里创建了一个public static void main方法。

带main方法的对象

实际代码:

object RunExample {

  def main(args: Array[String]) = {
    println("Hello World !!")

  }

}

反编译:

public final class RunExample {  
  public static void main(java.lang.String[]);
    Code:
       0: getstatic     #16                 // Field RunExample$.MODULE$:LRunExample$;
       3: aload_0
       4: invokevirtual #18                 // Method RunExample$.main:([Ljava/lang/String;)V
       7: return
}

这里也同样生成了一个public static void main,但区别是这里反编译的代码更短了,因为它没有继承App特质。一旦在后面的教程里探索了特质,我们就会看到这背后的原因。

从上面的示例,我们可以得出结论,为什么在Scala里有这两种创建可运行应用的方式。但有一点很重要的需要注意,即这两者需要得是对象。

如前面所述,编译器把对象特殊对待为具有实例的一个单例。所以创建一个可运行应用的概念并不适用于类。

让我们来看一下如果尝试通过类来创建一个可运行的Scala应用,会发生什么。

带main方法的类

实际代码:

class RunExample {

  def main(args: Array[String]) = {
    println("Hello World !!")

  }

}

反编译:

public class RunExample {  
  public void main(java.lang.String[]);
    Code:
       0: getstatic     #16                 // Field scala/Predef$.MODULE$:Lscala/Predef$;
       3: ldc           #18                 // String Hello World !!
       5: invokevirtual #22                 // Method scala/Predef$.println:(Ljava/lang/Object;)V
       8: return

  public RunExample();
    Code:
       0: aload_0
       1: invokespecial #30                 // Method java/lang/Object."<init>":()V
       4: return
}

生成的main方法不是静态的,所以它不能运行。

继承App特质的类

实际代码:

class RunExample extends App{

    println("Hello World !!")

}

反编译:

public class RunExample implements scala.App {  
  public long executionStart();
    Code:
       0: aload_0
       1: getfield      #20                 // Field executionStart:J
       4: lreturn

  public java.lang.String[] scala$App$$_args();
    Code:
       0: aload_0
       1: getfield      #25                 // Field scala$App$$_args:[Ljava/lang/String;
       4: areturn

  public void scala$App$$_args_$eq(java.lang.String[]);
    Code:
       0: aload_0
       1: aload_1
       2: putfield      #25                 // Field scala$App$$_args:[Ljava/lang/String;
       5: return

  public scala.collection.mutable.ListBuffer<scala.Function0<scala.runtime.BoxedUnit>> scala$App$$initCode();
    Code:
       0: aload_0
       1: getfield      #31                 // Field scala$App$$initCode:Lscala/collection/mutable/ListBuffer;
       4: areturn

  public void scala$App$_setter_$executionStart_$eq(long);
    Code:
       0: aload_0
       1: lload_1
       2: putfield      #20                 // Field executionStart:J
       5: return

  public void scala$App$_setter_$scala$App$$initCode_$eq(scala.collection.mutable.ListBuffer);
    Code:
       0: aload_0
       1: aload_1
       2: putfield      #31                 // Field scala$App$$initCode:Lscala/collection/mutable/ListBuffer;
       5: return

  public java.lang.String[] args();
    Code:
       0: aload_0
       1: invokestatic  #41                 // Method scala/App$class.args:(Lscala/App;)[Ljava/lang/String;
       4: areturn

  public void delayedInit(scala.Function0<scala.runtime.BoxedUnit>);
    Code:
       0: aload_0
       1: aload_1
       2: invokestatic  #46                 // Method scala/App$class.delayedInit:(Lscala/App;Lscala/Function0;)V
       5: return

  public void main(java.lang.String[]);
    Code:
       0: aload_0
       1: aload_1
       2: invokestatic  #52                 // Method scala/App$class.main:(Lscala/App;[Ljava/lang/String;)V
       5: return

  public final void delayedEndpoint$RunExample$1();
    Code:
       0: getstatic     #60                 // Field scala/Predef$.MODULE$:Lscala/Predef$;
       3: ldc           #62                 // String Hello World !!
       5: invokevirtual #66                 // Method scala/Predef$.println:(Ljava/lang/Object;)V
       8: return

  public RunExample();
    Code:
       0: aload_0
       1: invokespecial #69                 // Method java/lang/Object."<init>":()V
       4: aload_0
       5: invokestatic  #73                 // Method scala/App$class.$init$:(Lscala/App;)V
       8: aload_0
       9: new           #75                 // class RunExample$delayedInit$body
      12: dup
      13: aload_0
      14: invokespecial #78                 // Method RunExample$delayedInit$body."<init>":(LRunExample;)V
      17: invokevirtual #80                 // Method delayedInit:(Lscala/Function0;)V
      20: return
}

当继承这个特质时,生成的main方法没有static修饰符。同样这也不能用于创建一个可运行的应用。

同样明显的是,对象实例的创建是在编译时控制的。一个在运行时的对象仍然是一个正常的类,这可以从上面反编译的代码看到。在编译时的限制使得在运行时只有一个实例创建,这是一种相当优美的处理方式。

这就解释了静态的概念是怎样关联到Scala的对象的,以及一个可运行的应用入口只能是通过一个对象来生成。

本文章到此结束。下一章,我打算详细地解释类,届时会接触到样本类和特质。

敬请期待!^_^

dogstar

一位喜欢翻译的开发人员,艾翻译创始人之一。

广州