Scala学习笔记(3)

Scala中的OOP

Scala是一门同时具有函数式与面向对象特性的多重范式的语言,除了具有函数式特性外,对OOP也有着完整的支持。

构造器(Constructor)

在Scala中构造方法的作用与Java类似,用于在创建类实例的同时对指定的成员进行初始化。
在语法上,Scala中类可以拥有一个主构造器(primary constructor)和任意个辅助构造器(auxiliary constructor)

  • 主构造器的参数定义紧跟在类名之后,辅助构造器定义在类体中,使用this关键字。
  • 在辅助构造器的代码中必须立即调用主构造器或其它辅助构造器,之后才能执行其它代码。
  • 调用父类的构造器必须在主构造器中,写在父类类名之后。
  • 主构造器的参数中若添加了var/val关键字,则该参数将作为类的成员字段存在。
  • 主构造器的参数中若没有使用val/val关键字,则默认修饰为val private[this],若该参数被类的其它成员方法引用,则同样升格为类成员(但受访问权限限制,val不会生成setterprivate[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在访问权限上支持更细粒度的划分。

  • Scala的类定义、特质定义、单例对象定义前可以使用访问权限修饰(与Java相同)。
  • Scala中的顶层类允许使用protected权限修饰(与Java不同,Java仅允许内部类使用protected修饰)。
  • Scala中的访问权限关键字用于类时,还可以写在类名与主构造器之间(特质、单例对象没有这种用法)。
  • 访问级别关键字之后可以使用[]操作符设定更具体的访问区域限制,可以是当前定义的类、当前定义类的外部类(若存在外部类)、包名(某个包内的所有类实例可访问)或是this关键字(仅当前实例可访问)。
  • 访问权限关键字之后若不写明具体的访问限制区域,则默认限制为当前类可访问(与Java行为基本一致,但Java中的保护成员包内可见)。

如下代码所示:

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类中的字段不仅仅是定义了一个成员变量,编译器还可能会自动为字段生成与字段同名的gettersetter方法。

  • var关键字定义的字段编译器会同时为其生成settergetter方法,若对象的的权限为私有/保护,则对应生成的settergetter方法同样为私有/保护权限。
  • val关键字定义的字段为只读字断,编译器不会为其合成setter方法。
  • 若访问权限为private[this],则编译器不会为其合成settergetter方法(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中的重写遵循以下规则:

  • def只能重写另一个def。
  • var只能重写另一个抽象的var(即只有定义没有实现)。
  • val可以重写另一个val以及不带有参数的def。

重载

Scala支持函数重载,并且可以使用操作符作为函数名,使用操作符作为函数名可以达到类似C++操作符重载的效果。

伴生对象

在Scala中没有static关键字,也没有静态成员的概念,Scala使用单例对象来达到近似静态成员的作用。
每一个类可以拥有一个同名的伴生对象(单例),伴生对象使用object关键字定义,且一个类和其伴生对象的定义必须写在同一个文件中。

apply()/update()方法

在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 Class)与模式匹配(Pattern Matching)

样例类是一种特殊的类,通常用在模式匹配中。
在类定义前使用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

特质(Trait)

Scala中的trait特质对应Java中的interface接口,但相比Java中的接口,Scala中的特质除了没有默认构造器、不能被直接实例化之外,拥有绝大部分类的特性。
Scala中的trait可以拥有构造器(非默认),成员变量以及成员方法,成员方法也可以带有方法的实现,并且trait中的成员同样可以设置访问权限。

混入(Mixin)

  • Scala不支持多重继承,一个类只能拥有一个父类,但可以混入(mixin)多个特质。
  • Scala中采用的混入(mixin)机制相比传统的单根继承,保留了多重继承的大部分优点。
  • 使用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后混入特质TraitAwith关键字之后的必需是特质而不能是类名。

重写冲突的方法与字段

与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

你可能感兴趣的:(Scala)