/**
* 谨献给我最爱的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的对象的,以及一个可运行的应用入口只能是通过一个对象来生成。
本文章到此结束。下一章,我打算详细地解释类,届时会接触到样本类和特质。
敬请期待!^_^
------------------------
本作品采用知识共享署名-非商业性使用-相同方式共享 3.0 未本地化版本许可协议进行许可。
- 本文翻译作者为:dogstar,发表于艾翻译(itran.cc);欢迎转载,但请注明出处,谢谢!