Scala 之类与构造器

944 阅读7分钟

Scala语言来源于Java,因此它天生就是一个面向对象的语言。在本章中会介绍如何在Scala中利用构造器来构造一个类对象,顺便简要介绍Scala中的异常机制。

使用class关键字定义一个类

我们直接给出一个简单的Cat类模板:

class Cat{
  var name : String = "null"
  var age : Int = _
  var color : String = _
  override def toString = s"Cat($name, $age, $color)"
}

在Scala内定义一个类,仍然使用的是class关键字。(我们目前为止都是使用object修饰的,之后会提到classobject的区别)

一般来说,类内部的属性使用var关键字(因为这可以在外部修改这些变量)。在Scala中,声明变量是一定要赋值的。但是可使用 _ 表示为这个属性赋默认值,比如AnyVal类型的成员默认为0,AnyRef类型的成员默认为Null。

从Scala的层面上来说,Cat内部定义的成员都是public的,尽管没有加任何修饰符修饰。但是如果我们查看编译后的Cat.class文件的话,这些属性实际上是修饰为private,只不过编译器又额外地生成了公开的name(相当于Java的get方法)和name_$eq方法。(相当于Java的set方法)

这些细节在Scala层面是透明的,因此我们可以不受限制地访问一个Scala类的成员,除非这个成员被显示地标注为了private关键字。

在Scala中,类成员的访问权限被简化了:要么就不带任何修饰符来表示公有,要么就带上private或者protected修饰符表示为私有变量或只允许子类访问,也就是说,Scala的类成员只有三类权限

class Cat {
  //公开
  val age : Int = 1
  //允许子类访问
  protected val toy : String = "toy"
  //私有成员
  private val name : String = "kitty"
}

@BeanProperty注解

为了适配Java的使用习惯,Scala提供@BeanProperty注解,来提供某个属性对应的set/get方法。

@BeanProperty
var name : String = _

等价于:

def setName(inName : String): Unit ={
    this.name = inName
}

def getName : String = this.name

注意,如果要使用此注解,此内部成员需要是可变var的。并且不能使用private关键字来修饰。

.scala文件中的一些细节

一个.scala源文件内部可以存在多个公开的类,这一点与java不同。在编译期间,scalac会将一个.scala文件中不同的类编译成分别的.class类文件。

我们在编译过程中还可以发现一个细节:如果声明的是class,则只会编译出一个.class文件。如果声明的是object,则会同时生成两个.class文件。我们会在后续的伴生对象当中详细地叙述它。

赋值"_"与赋值null

注意,这两种赋值方式都要求变量显示声明数据类型

赋值为"_"表示为当前的变量赋默认值。如果该成员没有显示地声明数据类型,则在程序执行的时候会报错:

unbound placeholder parameter,表示没有绑定参数值类型。

对于不同的数据类型,_所默认代表的值参见下表。

类型对应的值
Byte Short Int Long0
Float Double0.0
StringAnyRefNull
Booleanfalse

如果赋值了null代表默认为这个属性指向空引用。如果赋值为null,且又不显示的声明数据类型,则该属性变为Null类型(注意!在Scala中,null单独为一个类型,就和Unit一样)。这个属性除了null之外无法再赋其它值,同样会在使用时带来问题。

Scala在声明对象变量时,可以根据new对象的类型自动推断,所以一般情况下: Type可以省略。但是如果我们希望定义一个上转型,这个时候则需要显示地写上数据类型。

object CreateOBJ {
  def main(args: Array[String]): Unit = {
   	//定义了一个上转型对象。
    val person : Person = new Emp
  }
  class Person {}
  class Emp extends Person{}
}

类方法

Scala的方法与上一章的函数定义方法是一样的。根据笔者的习惯,在类内部声明的函数(Function)特指为方法(Method)。在class修饰的类内部声明的方法为实例方法。

  1. 当Scala开始执行的时候,会在栈区开辟一个main栈,main栈在程序执行完毕后最后销毁。

  2. 当Scala程序执行到一个方法时,总会开辟一个新的栈。

  3. 每个栈都是独立的空间,AnyVal类型的数据类型是独立的,相互不会影响。

  4. 当方法执行完毕之后,该方法开辟的栈会被JVM机回收。

Scala构造方法(构造器)

Scala的构造方法和Java存在着较大的区别。

构造器的作用是对新对象的初始化。

在Java中,一个类可以定义多个不同的构造方法,简称构造方法重载。如果没有显示声明构造器,则Java默认提供无参构造器。如果显示声明了构造器,则Java将不再提供无参构造器。且Java的构造器内第一行省略了super();

Scala的构造器包括了主构造器辅助构造器。编译器通过不同的参数来区分选择何种构造器。而无论是何种构造器,都是不需要返回值的。

主构造器

主构造器直接在class上进行声明,如下面的代码块。类名的后面直接跟进一个参数列表,来为Teacher的内部成员赋值。

class Teacher(inAge: Int = 25, inName: String = "Tom") {
  var age: Int = inAge
  var name: String = inName
}

这等价于Java的这种声明方式:

//上述构造器的Java写法:
public Teacher(int inAge,String inName){
	this.age = inAge;
	this.Name = inName;
}

class内部的代码块实际上就可看作是一个主构造函数的函数体。当主程序使用主构造器创建一个类实例时,会像执行函数一样顺序执行class内部的每一条语句,而函数体内的声明语句则成为了该类的属性成员和内部方法。

//上述构造器的Java写法:
public Teacher(int inAge,String inName){
	this.age = inAge;
	this.Name = inName;
    //我们在Scala的class内的语句相当于都是写入了构造器的函数体内。
	System.out.println("Hire a teacher.")
}

我们甚至有更精简的写法:

class Teacher(var inAge: Int = 25,var inName: String = "Tom")

看到差别了吗?它直接省略了将"外部参数赋值给内部成员"的步骤,相当于直接将内部成员放到参数列表中进行初始化。(这种省略方式仅限于在主构造器当中使用)

另外,在单例模式当中,我们为了隐匿某个类的构造方法,在 Java 中的通常做法是将其构造器声明为私有的。那在 Scala 当中如何做到这一点呢?我们只需直接在参数列表的前边加上 private 关键字修饰就可以了。

class Teacher private (var inAge: Int = 25,var inName: String = "Tom")

这样,我们就不能直接去调用 Teacher 类的构造方法去创建实例了。不过令人费解的是,IntelliJ IDEA 并不会报语法错误,而是在编译之后才会提示没有权限访问此类的构造器。

辅助构造器

this关键字为类内部的方法命名,以表示该方法是一个辅助构造器。注意!辅助构造器内的第一行必须显示或隐式地调用主构造器!(这种感觉就像是在Java类当中,super构造器必须写在子构造器的第一行一样。)

这么做的根本原因是:只有主构造器通过extends关键字建立了与父类的联系。而子类的辅助构造器无法直接地调用父类的任何构造器,因此在调用辅助构造器之前,一定要先调用主构造器。这像是一个装饰者模式:先调用主构造器构造其父类和主要属性,然后再使用辅助构造器对其它的拓展属性进行补充。

对于显示地调用主构造器很好理解,那何为间接地调用主构造器呢?那就是某个辅助构造器调用了另一个辅助构造器,而该辅助构造器调用了主构造器。

我们直接给出以下代码:

class B{
  println("1")
  def this(int: Int){
    //B在调用辅助构造器之前,会先调用B的主构造器
    this
    println("2")
  }
}
//A的主构造器调用了B的辅助构造器
class A extends B(5){
  println("3")
  def this(string: String){
    //在调用A的辅助构造器之前,会首先调用A的主构造器
    this
    println("4")
  }
}

其中,类A的示例在初始化前先调用父类B的带参数构造器。

那么这行语句实际上调用了几个构造器呢?

//使用了A的辅助构造器构造对象。
val a : A = new A("a")

从结果来看,一共是调用了4个构造器。因为程序会在控制台中打印:1 2 3 4。程序首先调用父类B的主构造器,然后再调用B的辅助构造器。当B构建完成时,再调用A的主构造器,然后调用A的辅助构造器。