Scala是一门同时具有函数式与面向对象特性的多重范式的语言,除了具有函数式特性外,对OOP也有着完整的支持。
在Scala中构造方法的作用与Java类似,用于在创建类实例的同时对指定的成员进行初始化。
在语法上,Scala中类可以拥有一个主构造器(primary constructor)和任意个辅助构造器(auxiliary constructor)。
this
关键字。var/val
关键字,则该参数将作为类的成员字段存在。val/val
关键字,则默认修饰为val private[this]
,若该参数被类的其它成员方法引用,则同样升格为类成员(但受访问权限限制,val
不会生成setter
,private[this]
不会生成公有getter
),否则仅仅作为临时变量存在。如下代码所示:
//定义主构造器
class Constructor(a: Int, var b: Double = 2.0) { //构造器参数紧跟在类名之后,构造器中的参数可以带有默认值
//在构造器中创建了字段b,a变量没有显式使用var/val关键字,同时也没有被其它方法引用,因而仅仅作为临时变量存在
//定义辅助构造器,使用this关键字
def this() = this(2, 3.0) //辅助构造器的函数体中必须最终调用主构造器,辅助构造器即使没有参数也必须也必须带括号
}
//只有主构造器能够调用父类构造器,调用的父类构造器可以是主构造器,也可以是辅助构造器
class ExtendConstructor(a: Int = 2, c: Double = 4.0) extends Constructor(a, c) {
def this() {
//a = 100 //代码要在构造器调用之后,放在this()之前会报错
this(2, 4.0)
//super(2, 4.0) //在Scala中没有这种用法,父类的构造函数只能由主构造器调用
}
}
Scala中的成员和类、特质的默认访问权限即为公有,因而Scala中没有public
关键字。
Scala中的保护成员使用关键字protected
,私有成员使用关键字private
,作用大体上与Java相同,但Scala在访问权限上支持更细粒度的划分。
protected
权限修饰(与Java不同,Java仅允许内部类使用protected
修饰)。[]
操作符设定更具体的访问区域限制,可以是当前定义的类、当前定义类的外部类(若存在外部类)、包名(某个包内的所有类实例可访问)或是this
关键字(仅当前实例可访问)。如下代码所示:
package TestCode {
private class A //类定义前可以使用访问权限修饰
protected class B //类定义的访问权限可以为protected
case class Num private (num: Int = 200) //权限修饰符可以用在类名与主构造器之间
class Test protected () //即使主构造器参数为空,也不能直接以权限关键字结尾
//或者写成 class Test protected {}
class Access(a: Int = 1, var b: Double = 2.0) {
def showOther1(access: Access) = access.show1 //出错,access非当前类实例,无访问权限
def showOther2(access: Access) = access.show2 //正常访问
def showOther3(access: Access) = access.show3 //正常访问
private[this] def show1 = println(a + " " + b) //限定当前实例可访问
private[Access] def show2 = println(a + " " + b) //类似Java中的private,当前类的任意实例皆可相互访问私有成员
private[TestCode] def show3 = println(a + " " + b) //作用域为包名,此时的访问权限类似Java中的default访问权限,当前包中类的实例皆可访问到该私有成员
}
}
Scala类中的字段不仅仅是定义了一个成员变量,编译器还可能会自动为字段生成与字段同名的getter
和setter
方法。
var
关键字定义的字段编译器会同时为其生成setter
和getter
方法,若对象的的权限为私有/保护,则对应生成的setter
和getter
方法同样为私有/保护权限。val
关键字定义的字段为只读字断,编译器不会为其合成setter
方法。private[this]
,则编译器不会为其合成setter
和getter
方法(protected[this]
正常生成setter``getter
方法)。abstract
关键字,且不能被直接实例化。override
关键字。如下所示:
class Override {
var m = 100 //普通成员字段会自动合成setter/getter方法
/*
def m(): Int = m //错误,提示重复定义
def m_=(m: Int) { this.m = m } //错误,提示重复定义
*/
def m(m: Int) {} //正常,签名未冲突
private[this] var num = 100 //私有this字段不会合成setter/getter方法,但自行手动定义同名的setter/getter方法时有许多限制(getter方法需要空参且写明返回值),且没有实用价值(setter方法使用报错)
def num(): Int = num //正常
def num_=(num: Int) { this.num = num } //正常,虽然定义时不报错,但赋值时报错
/*
def num = this.num //报错
def num: Int = num //报错
def num: Int = this.num //报错
*/
//常用的私有变量自定义setter/getter风格是私有字段名前加上下划线
private[this] var _abc = 0
def abc = _abc
def abc_=(abc: Int): Unit = _abc = abc
/*
也可以写成:
def abc_=(abc: Int) { _abc = abc }
*/
}
在Scala中,字段名称可以与方法名称相同,默认情况下,合成的setter``getter
方法就与字段同名,手动在代码中创建与setter``getter
签名相同的方法会导致编译错误,但在访问权限为private[this]
时编译器不合成默认的getter``setter
方法时可以手动定义setter``getter
方法。
需要注意的是,在实际编码过程中,虽然给private[this]
的字段定义同名的setter``getter
方法不会报错,但实际调用过程中会提示错误(如上例子中给num字段赋值回得到错误reassignment to val,因此不要手动给字段定义同名的setter``getter
方法)。
此外,由于字段名称可以与方法名称相同,因而即使编译器生成了setter``getter
方法,编码者依然可以使用字段名称定义其它签名的重载函数。
在Scala中,默认情况下,子类的并不会重写父类的同名方法,而是需要显式地在方法定义前加上override
关键字才会发生重写行为。
Scala中的重写遵循以下规则:
Scala支持函数重载,并且可以使用操作符作为函数名,使用操作符作为函数名可以达到类似C++中操作符重载的效果。
在Scala中没有static
关键字,也没有静态成员的概念,Scala使用单例对象来达到近似静态成员的作用。
每一个类可以拥有一个同名的伴生对象(单例),伴生对象使用object关键字定义,且一个类和其伴生对象的定义必须写在同一个文件中。
在Scala中,允许使用函数风格进行一些对象操作。
假设有一个类实例a,使用:
a(arg1, arg2, arg3, ...)
此表达式等价于:
a.apply(arg1, arg2, arg3, ...)
同样的,使用:
a(arg1, arg2, arg3, ...) = value
等价于:
a.update(arg1, arg2, arg3, ..., value)
如下代码所示:
object Main extends App {
var a = new Apply(0, 0)
val show = () => println(a.num1 + " " + a.num2)
a(1) //相当于调用 a.apply(1)
show() //输出 1 2
a(100, 200) = Apply(10, 20) //相当于调用 a.update(100, 200, new Apply(10, 20))
show() //输出 90 180
Apply(1000, 2000) = a
show() //输出 1000 2000
}
class Apply(var num1: Int, var num2: Int) {
def apply(num: Int) {
this.num1 = num
this.num2 = num + 1
}
def update(num1: Int, num2: Int, test: Apply) {
this.num1 = num1 - test.num1
this.num2 = num2 - test.num2
}
}
object Apply {
def apply(num1: Int, num2: Int) = new Apply(num1, num2)
def update(num1: Int, num2: Int, test: Apply) {
test.num1 = num1
test.num2 = num2
} //伴生对象同样可以拥有apply()/update()方法
}
输出结果:
1 2
90 180
1000 180
在Scala中,还提供了被称为提取器的unapply()
方法。
unapply()
方法则与apply()
方法相反,可以从对象中提取出需要的数据(在实际使用过程中,可以从任意的目标里提取数据)。
unapply()
方法返回值必须为Option
及其子类,单一返回值使用Option[T]
,多个返回值可以包含在元组中Option[(T1, T2, T3, ...)]
。
unapply()
方法虽然也可以定义在类中,但一般在伴生对象中使用(在类中定义没有合适的语法使用)。
假设有伴生对象名为Unapply
,则:
var Unapply(arg1, arg2, arg3, ...) = value
等价于:
var (arg1, arg2, arg3, ...) = Unapply.unapply(value)
如下代码所示:
object TestUnapply extends App {
var Unapply(num1) = 1 //提取一个值
println(num1)
var Unapply(num2, num3) = Unapply(100, 200) //提取多个值
println(num2 + " " + num3)
}
object Unapply {
def apply(num1: Int, num2: Int) = new Unapply(num1, num2)
def unapply(num: Int) = Option(num)
def unapply(a: Unapply) = Option((a.num1, a.num2))
}
class Unapply(var num1: Int, var num2: Int)
输出结果:
1
100 200
若需要提取任意长度的值的序列,则可以使用unapplySeq()
方法,该方法返回值类型为Option[Seq[T]]
。
不要同时定义unapplySeq()
方法和unapply()
方法,会产生冲突。
如下代码所示:
object TestUnapply extends App {
def showSplit(str: String) = str match {
case Unapply(str1, str2) => println(s"$str1 $str2")
case Unapply(str1, str2, str3) => println(s"$str1 $str2 $str3")
case _ => println("Case Nothing")
}
showSplit("abc")
showSplit("abc.cde")
showSplit("abc.cde.efg")
}
object Unapply {
def unapplySeq(str: String) = Option(str split "\\.") //split()方法接收的是正则表达式,小数点、加减乘除之类的符号需要转义
}
输出结果:
Case Nothing
abc cde
abc cde efg
样例类是一种特殊的类,通常用在模式匹配中。
在类定义前使用case
关键字即可定义一个样例类。
相比普通的类,样例类有以下特性:
val
关键字定义(即默认为公有访问权限,而不是普通类默认的private[this]
)。apply()
方法用于构造对象和unapply()
方法用于模式匹配。toString``equals``hashCode``copy
等方法。如下代码所示:
case class Case(num: Int = 100, str: String)
object Main extends App {
var ca = Case(str = "S13")
println(ca.num + " " + ca.str)
//使用样例类提供的copy()方法可以复制出一个字段值相同的类
var caCopy = ca.copy()
println(caCopy.num + " " + caCopy.str)
//也可以在copy()只复制需要的值甚至不使用原先对象的值
var caCopy1 = ca.copy(200)
var caCopy2 = ca.copy(str = "Abc")
var caCopy3 = ca.copy(50, "ABC")
println(caCopy1.num + " " + caCopy1.str)
println(caCopy2.num + " " + caCopy2.str)
println(caCopy3.num + " " + caCopy3.str)
//样例类的实例之间可以直接比较,只要构造器中的字段值相同便会返回true
println(ca == caCopy)
//样例类经常用于模式匹配中
def show(ca: Case) = ca match {
case Case(num, _) if num > 100 => println("Case.num > 100") //模式匹配中条件可以带有守卫
case Case(100, _) => println("Case.num == 100") //模式匹配可以精确到具体的数值,而对于不需要的值可以忽略(使用"_"符号)
case _ => println("Not Matching")
}
show(ca)
show(caCopy1)
show(caCopy3)
}
输出结果:
100 S13
100 S13
200 S13
100 Abc
50 ABC
true
Case.num == 100
Case.num > 100
Not Matching
Scala中的trait
特质对应Java中的interface
接口,但相比Java中的接口,Scala中的特质除了没有默认构造器、不能被直接实例化之外,拥有绝大部分类的特性。
Scala中的trait
可以拥有构造器(非默认),成员变量以及成员方法,成员方法也可以带有方法的实现,并且trait
中的成员同样可以设置访问权限。
with
关键字混入特质,一个类中混入多个特质时,会将第一个扩展的特质的父类作为自身的父类,同时,后续混入的特质都必须是从该父类派生。extends
关键字的后面,with
只能混入特质,不能混入类。如下所示:
class BaseA
class BaseB
trait TraitA extends BaseA
trait TraitB extends BaseB
/* 编译报错,提示:
* superclass BaseA
* is not a subclass of the superclass BaseB
* of the mixin trait TraitB
*/
class TestExtend extends TraitA with TraitB
/* 编译报错,提示:
* class BaseA needs to be a trait to be mixed in
*/
class ExtendClass extends TraitA with BaseA
TestExtend
类中,特质TraitA
的父类BaseA
并不是特质TraitB
父类BaseB
的父类,而Scala中一个类只能拥有一个父类,因而无法通过编译。
ExtendClass
类中,应该继承BaseA
后混入特质TraitA
,with
关键字之后的必需是特质而不能是类名。
与Java8中相同,混入机制同样需要解决富接口带来的成员冲突问题,当一个类的父类与后续混入的特质中带有相同名称的字段或相同签名的方法时,需要在子类重写这些冲突的内容。
如下所示:
class BaseA {
def get = 123
}
trait TraitA {
def get = 456
}
trait TraitB {
def get = 789
}
class TestExtend extends BaseA with TraitA with TraitB {
override def get = 77 //对于冲突的内容,必需显式重写
}
对于混入的内容,按照以下顺序进行构造:
Scala的混入机制是线性化
的,对于冲突的内容,构造中的后一个实现会顶替前一个。
线性化顺序与构造顺序相反
,对于同名字段的内容,最终保留的是最右端的类或特质的实现。
如下所示:
class BaseA {
def get = 123
}
trait TraitA {
def get = 456
}
trait TraitB {
def get = 789
}
trait TraitC extends TraitA {
override def get = 111
}
class TestExtend extends BaseA with TraitA with TraitC {
override def get = super.get //使用父类的实现时不需要显式指定到底是哪一个,编译器会自动按照线性化顺序选择最后的实现,即TraitC中的实现,即返回111
//override def get = super[BaseA].get //也可以使用继承自其它特质或类的实现
//override def get = super[TraitB].get //错误,必需使用直接混入的类或特质,不能使用继承层级中更远的类或特质
}
Scala与Java类似,类实例赋值仅仅是复制了一个引用,实例所指向的内存区域并未被复制。
若需要真正复制一个对象,需要调用对象的clone()
方法。
clone()
方法定义在Object
类中,但由于是protected
成员,不可直接调用,若需要自行实现类的复制功能,则需要实现Cloneable
接口。
例如:
class Clone extends Cloneable {
var nums = Array(1, 2, 3)
var str = "TestClone"
override def clone = {
val clone = super.clone.asInstanceOf[Clone] //Cloneable接口中clone()返回的是Object型,即Scala中的Any,需要进行强制类型转换
clone.nums = nums.clone //深复制需要对成员中的引用类型调用clone()
clone
}
}
与Java中类似,如果需要实现深复制,则需要对类成员中的AnyRef
及其子类调用clone()
进行复制。
对于AnyVal
子类如Int``Double
等类型,没有提供重载的clone()
方法,但这些类型默认即为值复制,无需额外的操作。
Java中的特例java.lang.String
在Scala中同样有效,对于String
类型,在重写clone()
时也可当作基本类型对待。
在Scala中,还可以直接继承scala.collection.mutable.Cloneable[T]
特质:
import scala.collection.mutable.Cloneable
class Clone extends Cloneable[Clone] {
var nums = Array(1, 2, 3)
var str = "TestClone"
override def clone = {
val clone = super.clone //不必进行强制类型转换,类型已在泛型参数中指定
clone.nums = nums.clone
clone
}
}
在Scala中,创建类实例的同时可以直接对类的成员变量进行初始化。
如下代码所示:
object Init extends App {
var num = new Num {
num = 100
name = "Num"
} //相当于创建了一个匿名类,然后向上转型到类Num上
println(num.name + " " + num.num) //正常输出了初始化的值
}
class Num {
var num: Int = 0
var name: String = ""
}
以上代码用Java改写为:
class Init {
public static void main(String[] args) {
Num num = new Num() {{
num = 100;
name = "Num";
}}; //匿名类的构造函数的函数名为空,因此可以使用大括号直接嵌套的方式
System.out.println(num.name + " " + num.num);
}
}
class Num {
int num = 0;
String name = "";
}
输出结果:
Num 100