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
修饰的,之后会提到class
和object
的区别)
一般来说,类内部的属性使用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 Long |
0 |
Float Double |
0.0 |
String 和 AnyRef |
Null |
Boolean |
false |
如果赋值了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.")
}
辅助构造器
用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的辅助构造器。
在Scala中定义异常
对Java的异常做个简单回顾:Java的异常分为两类,受检异常(RuntimeException)和非受检异常(CheckedException)。
受检异常,指代JVM在运行期间由于程序的逻辑问题可能引发的意外:如NullPointerException, ArrayIndexOutOfBoundsException等。
非受检异常,指代因外部因素可能引发的程序问题:如ClassNotFoundException(试图反射的类实际上在工程中并不存在)。
然而在Scala的异常机制沿用了Java的try-catch体系,但是写法有些区分,并且从形式上被简化了:在Scala中,只有运行时异常。下面给出一个实例:
try {
val a: Int = 10 / 0
println(a)
} catch {
case ex: ArithmeticException => ex.printStackTrace()
case ex: Exception => ex.printStackTrace()
}finally {
println("try-catch执行完毕")
}
在Scala中,只需要写一个catch:它会自动对捕获到的异常进行模式匹配,并执行=>
后面的代码块。(注意不要把=>
写为->
,和Java的lambda语法区分开来)。
Java对catch的顺序有一定的要求:小范围的异常要写在前面。范围大的异常写在后面。而Scala并没有对此做严格限制,但是我们仍然应当遵守这样的编程习惯。
Scala可以在def
函数上面加上一个@throws
注解的方式来表示可能抛出的异常:
@throws(classOf[NumberFormatException])
def mis(stringInt:String): Int= {
stringInt.toInt
}
这篇博客记录了有关Java两种异常的梗概。