Skip to content

面向对象编程之高阶

1. 单例对象

所谓的单例对象,就是在程序运行过程中,指定类的对象只能创建一个,而不能创建多个。这样的对象可以由特殊的设计方式获得,也可以由语言本身设计得到,比如object伴生对象。
Scala语言是完全面向对象的语言,所以并没有静态的操作(即在Scala中没有静态的概念)。但是为了能够和Java语言交互(因为Java中有静态概念),就产生了一种特殊的对象来模拟类对象,该对象为单例对象。若单例对象名与类名一致,则称该单例对象这个类的伴生对象,这个类的所有"静态"内容都可以放置在它的伴生对象中声明, 然后通过伴生对象名称直接调用。
如果类名和伴生对象名称保持一致,那么这个类称之为伴生类。Scala编译器可以通过伴生对象的apply方法创建伴生类对象。apply方法可以重载,并传递参数,且可由Scala编译器自动识别。所以在使用时,其实是可以省略的。

scala
object Baby {
    def apply(): Baby = new Baby()

    def main(args: Array[String]): Unit = {
        // 通过构造方法完成对象的初始化
        val baby1 = new Baby()
        println(s"--------baby1: ${baby1}--------------")
        // 直接访问类名,访问的不是类,而是类的伴生对象
        val baby2: Baby.type = Baby
        println(s"--------baby2: ${baby2}--------------")
        // apply主要应用于构建对象
        val baby3: Baby = Baby.apply()
        println(s"--------baby3: ${baby3}--------------")
        // apply方法由于应用场景比较多,所以编译器可以动态识别,调用时可以省略apply
        val baby4: Baby = Baby()
        println(s"--------baby4: ${baby4}--------------")
    }
}

class Baby {
    println("构造函数初始化。。。。")
}

运行结果:
Alt text 值得一提的是:
Alt text

2. apply方法

  1. 通过伴生对象的apply方法,实现不使用new方法创建对象。
  2. 如果想让主构造器变成私有的,可以在()之前加上private。
  3. apply方法可以重载。
  4. Scala中obj(arg)的语句实际是在调用该对象的apply方法,即obj.apply(arg)。用以统一面向对象编程和函数式编程的风格。
  5. 当使用new关键字构建对象时,调用的其实是类的构造方法,当直接使用类名构建对象时,调用的其实时伴生对象的apply方法。

2. 特质(Trait)

Scala语言中,采用特质trait(特征)来代替接口的概念,也就是说多个类具有相同的特质(特征)时,就可以将这个特质(特征)独立出来,采用关键字trait声明。
Scala中的trait中即可以有抽象属性和方法,也可以有具体的属性和方法,一个类可以混入(mixin)多个特质。这种感觉类似于Java中的抽象类。Scala引入trait特征,第一可以替代Java的接口,第二个也是对单继承机制的一种补充。

2.1 特质声明

基本语法:

scala
trait 特质名 {
    trait 主体
}

2.2 特质基本语法

一个类具有某种特质(特征),就意味着这个类满足了这个特质(特征)的所有要素,要使用extends关键字,如果有多个特质或存在父类,那么需要采用with关键字连接。
基本语法:
没有父类: class 类名 extends 特质 1 with 特质 2 with 特质 3 …
有父类: class 类名 extends 父类 with 特质 1 with 特质 2 with 特质 3…

:::

scala
object TraitDemo {
    def main(args: Array[String]): Unit = {
        val cat = new Cat()
        val woman = new Woman()
    }
}
trait Run{   // 特质
    println("trait")
    def run:Unit
}

class Cat extends Run{
    override def run(): Unit = {
        println("cat")
    }
}

class Human{}

class Woman extends Human with Run{
    override def run(): Unit = {
        println("woman run ")
    }
}

运行结果:
Alt text

说明

  1. 类和特质的关系:使用继承的关系。
  2. 当一个类去继承特质时,第一个连接词是extends,后面是with。
  3. 如果一个类在同时继承特质和父类时,应当把父类写在extends后。

反编译字节码文件,可以看到特质就是Java中的interface:
Alt text

scala
object TraitDemo {
    def main(args: Array[String]): Unit = {
        val cat = new Cat()
    }
}
// trait可以继承类
trait Run extends Exception{
    println("trait")
    def run:Unit
}
class Cat extends Run{
    override def run(): Unit = {
        println("cat")
    }
}

trait可以继承类, 将trait理解为interface/abstract的结合体更好一些。
反编译字节码文件,可以看出scala编译器将代码中继承和实现顺序调整了:
Alt text

2.3 动态混入

动态混入可以灵活的扩展类的功能,无需更改现有的类的功能代码,实现开闭原则的编程思想。

scala
object TraitDemo2 {
    def main(args: Array[String]): Unit = {
        // 使用with关键字, 不需要更改Cat2的代码
        val cat = new Cat2() with Run2 {
            // 如果特质中方法没有实现需要手动实现
            override def run(): Unit = {
                println("run....")
            }
        }
        cat.eat()
        cat.run()
        // cat支持play功能
        cat.play()
    }
}
trait Run2 {
    println("trait")
    def run: Unit
    def play():Unit={
        println("play....")
    }
}
class Cat2 {
    def eat(): Unit = {
        println("eat....")
    }
}

运行结果:
Alt text

2.4 特质叠加

由于一个类可以混入(mixin)多个trait,且trait中可以有具体的属性和方法,若混入的特质中具有相同的方法(方法名,参数列表,返回值均相同),必然会出现继承冲突问题。冲突分为以下两种:

  1. 一个类(Sub)混入的两个 trait(TraitA,TraitB)中具有相同的具体方法,且两个trait之间没有任何关系,解决这类冲突问题,直接在类(Sub)中重写冲突方法。
    Alt text
  2. 一个类(Sub)混入的两个trait(TraitA,TraitB)中具有相同的具体方法,且两个trait继承自相同的 trait(TraitC),及所谓的"钻石问题",解决这类冲突问题,Scala采用了特质叠加的策略。所谓的特质叠加,就是将混入的多个trait中的冲突方法叠加起来。
    Alt text
scala
object TraitDemo4 {
    def main(args: Array[String]): Unit = {
        val spark = new Spark()
        spark.operate()
    }
}

trait Operate {
    def operate():Unit={
        println("处理数据")
    }
}

trait Database extends Operate {
    override def operate():Unit={
        print("向数据库中")
        super.operate()
    }
}

trait Log extends Operate {
    override def operate(): Unit = {
        print("向日志文件中")
        super.operate()
    }
}

class Spark extends Database with Log {
    
}

运行结果:
Alt text 特质的执行顺序就是初始化顺序的反向操作:
Alt text 如果只需要执行其中一个特质,比如不执行Database,代码可以这么调整:

scala
trait Log extends Operate {
    override def operate(): Unit = {
        print("向日志文件中")
        // 这里的super不是父特质而是指的上一级特质
        super.operate() 
        super[Operate].operate() 
    }
}

java中super的作用?

super关键字只在编译时关联父类,运行时无效。如下图反编译的字节码文件:
Alt text 如果使用画图表述,下图第二种才正确:
Alt text

2.5 特质初始化执行顺序

  1. 将trait作为抽象类,那么执行顺序: trait初始化 > 当前类
  2. 如果当前类同时有父类和trait, 那么执行顺序: parent类初始化 > trait初始化 > 当前类
  3. 如果当前类有多个特质,那么执行顺序是从左往右依次执行: left trait > right trait
  4. 如果当前类有父类,并且父类和子类都有trait, 那么执行顺序: Parent Trait> Parent class > current trait > current class

需要注意的是初始化都是只执行一次。

scala
object TraitDemo3 {
    def main(args: Array[String]): Unit = {
        val cat = new Cat3()
    }
}
trait Run3 {
    println("Run3 trait 初始化")
}
trait Drink {
    println("Drink trait 初始化")
}
trait Eat {
    println("Eat trait 初始化")
}
class Animal extends Eat {
    println("Animal 初始化")
}
class Cat3 extends Animal with Run3 with Drink {
    println("Cat3 初始化")
}

运行结果:
Alt text

总结

特质和抽象类的区别?

  1. 优先使用特质。一个类扩展多个特质是很方便的,但却只能扩展一个抽象类。
  2. 如果你需要构造函数参数,使用抽象类。因为抽象类可以定义带参数的构造函数,而特质不行(有无参构造)。

3. 类型检查和转换

  1. obj.isInstanceOf[T]:判断obj是不是T类型。
  2. obj.asInstanceOf[T]:将obj强转成T类型。
  3. classOf: 获取对象的类名。
scala
object ExtendDemo {
    def main(args: Array[String]): Unit = {
    val user1 = new User()
    user1.id = 123
    val user2 = new User()
    user2.id = 123
    // 默认调用equals方法,默认的equals方法执行hashcode比较
    println(user1 == user2)
    // java中 Class clazz = String.class
    // scala中获取类型信息
    val clazz: Class[User] = classOf[User]
    println(clazz.getMethods.head)
}

class User {
    var id: Int = _

    // 重写默认的equals方法
    override def equals(other: Any): Boolean = {
        if (other.isInstanceOf[User]) {   // 类型判断
            val that = other.asInstanceOf[User] // 强制转换
            id == that.id
        }else{
            false
        }
    }
}

运行结果:
Alt text

4. 枚举类和应用类

枚举类:需要继承Enumeration
应用类:需要继承App

scala
// 枚举类
object Color extends Enumeration {
    val RED = Value(1, "red")
    val YELLOW = Value(2, "yellow")
    val BLUE = Value(3, "blue")
}
// 应用类, 直接可以执行,简单化了main函数
object Test20 extends App {
    println(Color.RED)
    println(Color.RED.id)
}

5. Type定义新类型

使用type关键字可以定义新的数据数据类型名称,本质上就是类型的一个别名。

scala
object Test20 extends App {
    // 区分scala中的hashmap, 取别名
    type JavaHashMap = _root_.java.util.HashMap[String, String]
    println(new JavaHashMap())
}