Cris 的 Scala 笔记整理(八):面向对象中级-继承和多态

继承和多态
1. Java 继承回顾

class 子类名 extends 父类名 { 类体 }

子类会继承父类所有的属性和方法

2. 继承简述

继承可以解决代码复用,让我们的编程更加靠近人类思维。当多个类存在相同的属性(变量)和方法时,可以从这些类中抽象出父类,在父类中定义这些相同的属性和方法,所有的子类不需要重新定义这些属性和方法,只需要通过 extends 语句来声明继承父类即可

Java 一样,Scala 也支持类的单继承

3. Scala 继承案例
object ExtendDemo extends App {

  val stu = new Stu
  stu.name = "cris"
  stu.age = 23
  stu.study() // cris is studying!!!
  stu.info() // Student(cris,23)

}

class Person {
  var name: String = ""
  var age: Int = 18

  def info(): Unit = {
    println(toString)
  }

  override def toString = s"Person($name, $age)"
}

class Stu extends Person {
  def study(): Unit = {
    println(this.name + " is studying!!!")
  }

  override def toString = s"Student($name,$age)"
}
复制代码

子类继承了所有的属性,只是私有的属性不能直接访问,需要通过公共的方法去访问

验证代码如下

class Father {
  var a = 100
  protected var b = 200
  private var c = 300

  def func1() {}

  protected def func2() {}

  private def func3() {}
}

class Son extends Father {
  def func(): Unit = {
    println(this.a + this.b)
    func1()
    func2()
  }
}
复制代码

观察反编译后的字节码文件

public class Father
{
  public int a()
  {
    return this.a;
  }
  
  public void a_$eq(int x$1)
  {
    this.a = x$1;
  }
  
  private int a = 100;
  
  public int b()
  {
    return this.b;
  }
  
  public void b_$eq(int x$1)
  {
    this.b = x$1;
  }
  
  private int b = 200;
  
  private int c()
  {
    return this.c;
  }
  
  private void c_$eq(int x$1)
  {
    this.c = x$1;
  }
  
  private int c = 300;
  
  public void func1() {}
  
  public void func2() {}
  
  private void func3() {}
}
复制代码

实际上,Scala 中只有两种访问修饰符,一种 public,一种 private,protected 编译后就是 public

4. 方法的重写

Scala 明确规定,重写一个非抽象方法需要用 override 修饰符,调用超类的方法需要使用 super 关键字

5. Scala 中的类型检查和转换(多态)
  1. 要测试某个对象是否属于某个给定的类,可以用 isInstanceOf 方法;用 asInstanceOf 方法将引用转换为子类的引用;classOf 获取对象的类名

  2. classOf[String] 就如同 JavaString.class

  3. obj.isInstanceOf[T] 就如同 Javaobj instanceof T 判断 obj 是不是 T 类型

  4. obj.asInstanceOf[T] 就如同 Java(T)objobj 强转成 T 类型

示例代码

  def main(args: Array[String]): Unit = {

    println(classOf[String]) // class java.lang.String
    // 上行代码使用反射实现
    val string = "cris"
    println(string.getClass.getName) // java.lang.String

    // 类型判断
    println(string.isInstanceOf[String]) // true

    // 类型转换(向上转型)
    val any: AnyRef = string
    // 类型转换(向下转型)
    println(any.asInstanceOf[String].charAt(0)) // c
  }
}
复制代码

向上转型的目的:为了实现方法参数的统一;向下转型的目的:为了使用特定类的特定方法

类型转换最佳示例

object TypeConverse {
  def main(args: Array[String]): Unit = {
    val dog = new Dog02
    val fish = new Fish02
    func(dog) // dog is eating bone
    func(fish) // fish is swimming
  }

  def func(p: Pet02): Unit = {
    if (p.isInstanceOf[Dog02]) p.asInstanceOf[Dog02].eatBone()
    else if (p.isInstanceOf[Fish02]) p.asInstanceOf[Fish02].swimming()
    else println("类型错误!")
  }

}

class Pet02 {

}

class Dog02 extends Pet02 {
  var name = "dog"

  def eatBone(): Unit = {
    println(s"$name is eating bone")
  }
}

class Fish02 extends Pet02 {
  var name = "fish"

  def swimming(): Unit = {
    println(s"$name is swimming")
  }
}
复制代码

向下转型的前提是:该对象本身就是要转型的子类数据类型

6. 超类构造

回顾 Java 的超类构造

Java 中,创建子类对象时,子类的构造器总是去调用一个父类的构造器(显式或者隐式调用)

看看 Scala 的超类构造

示例代码

object SuperDemo {
  def main(args: Array[String]): Unit = {
    var b = new B("cris")
  }
}

class A {

  var name = "A"
  println(s"A's name is $name")

}

class B extends A {

  println(s"B's name is $name")

  def this(name: String) {
    this()
    this.name = name
    println(s"finally, B's name is $name")
  }
}
复制代码

执行结果如下:

总结一下执行顺序:

  1. 调用 B 的辅助构造函数时,先要调用 B 的主构造(this()
  2. 调用 B 的主构造之前,调用父类 A 的主构造
  3. 最后才是调用 B 的辅助构造

注意点:

Scala 的构造器中,你不能使用 super 来调用父类的构造器

练习:写一个能体现 Scala 构造器继承特点的案例

object SuperDemo2 {
  def main(args: Array[String]): Unit = {
    val worker = new Worker("cris")
    //    name = cris
    //    age = 20
    worker.info()
  }

}

class People(pName: String) {
  var name: String = this.pName

  def info(): Unit = println(s"name = $name")
}

class Worker(name: String) extends People(name) {
  var age = 20

  override def info(): Unit = {
    super.info()
    println(s"age = $age")
  }

}
复制代码

结合输出,想想上面代码的执行顺序

总结

  1. 子类构造一定会调用父类的构造(可以是主构造,也可以是辅助构造)
  2. 父类的所有辅助构造,最终都会调用父类的主构造
7. 属性覆写

回想:Java 中父类的属性可以被覆写吗?

示例代码

public class Demo {
    public static void main(String[] args) {
        Sub s = new Sub();
        // james
        System.out.println(s.name);

        Super s2 = new Sub();
        // cris
        System.out.println(s2.name);

    }
}

class Super {
    String name = "cris";
}

class Sub extends Super {
    String name = "james";
}
复制代码

答案是:不会!

Java 给出的解释是:隐藏字段代替了重写

官网解释如下:

​ Within a class, a field that has the same name as a field in the superclass hides the superclass’s field, even if their types are different. Within the subclass, the field in the superclass cannot be referenced by its simple name. Instead, the field must be accessed through super. Generally speaking, we don’t recommend hiding fields as it makes code difficult to read.

从上面这段解释中,我们可以看出成员变量不能像方法一样被重写。当一个子类定义了一个跟父类相同名字的字段,子类就是定义了一个新的字段。这个字段在父类中被隐藏的,是不可重写的

如果想要访问父类的隐藏字段

  • 采用父类的引用类型,这样隐藏的字段就能被访问了,像上面所给出的例子一样
  • 将子类强制类型转化为父类类型,也能访问到隐藏的字段

小结

​ 父类和子类定义了一个同名的字段,不会报错。但对于同一个对象,用父类的引用去取值(字段),会取到父类的字段的值,用子类的引用去取值(字段),则取到子类字段的值。在实际的开发中,要尽量避免子类和父类使用相同的字段名,否则很容易引入一些不容易发现的bug

回顾 Java 的动态绑定

示例代码

public class Demo {
    public static void main(String[] args) {
        Super s = new Sub();
        System.out.println("s.getI() = " + s.getI());
        System.out.println("s.sum() = " + s.sum());
        System.out.println("s.sum1() = " + s.sum1());
    }
}

class Super {
    public int i = 10;

    public int sum() {
        return getI() + 10;
    }

    public int sum1() {
        return i + 10;
    }

    public int getI() {
        return i;
    }
}

class Sub extends Super {
    public int i = 20;

    @Override
    public int sum() {
        return i + 20;
    }

    @Override
    public int getI() {
        return i;
    }

    @Override
    public int sum1() {
        return i + 10;
    }
}
复制代码

结果如下

如果我们将子类的 getI()sum1() 方法注释掉,再执行,结果如下:

总结 Java 的动态绑定机制

  1. 当调用对象方法的时候,该方法会和该对象的内存地址绑定
  2. 当调用对象属性时,没有动态绑定机制,哪里声明,那里使用

Scala 的属性覆写

示例代码

object OverrideDemo {
  def main(args: Array[String]): Unit = {
    val a: AA = new BB
    val b: BB = new BB
    println(a.i)	// 实质调用的是 BB 的 i()方法
    println(b.i)	// 实质调用的是 BB 的 i()方法
  }

}

class AA {
  // AA 编译后的文件会生成一个 i() 方法用于读取该属性
  val i = 10
}

class BB extends AA {
  // BB 编译后的文件会覆写 AA 中的 i() 方法
  override val i = 20
}
复制代码

输出

看看编译后的源代码

覆写字段的注意事项和细节

  1. val 属性只能重写另一个 val 属性或重写不带参数的同名方法

    示例代码

    object OverrideDemo {
      def main(args: Array[String]): Unit = {
        val a: AA = new BB
        val b: BB = new BB
    
    
        println(a.func())	// 实质都是调用的 BB 中的 func()
        println(b.func)	// 实质都是调用的 BB 中的 func()
      }
    
    }
    
    class AA {
      // AA 编译后的文件会生成一个 i() 方法用于读取该属性
      val i = 10
    
      def func(): Int = i
    }
    
    class BB extends AA {
      // BB 编译后的文件会覆写 AA 中的 i() 方法
      override val i = 20
    
      override val func: Int = i
    }
    复制代码

    输出

    查看编译后的字节码

  2. var 只能重写另一个抽象的 var 属性

    示例代码

    先看看什么是抽象属性:未初始化的变量就是抽象的属性,抽象属性需要在抽象类中

    然后再看看编译后的字节码

    值得一提的是,Scala 语法中,BBB 中的 override 关键字可以省略

重写抽象的 var 属性小结

  • 一个 var 属性没有初始化,那么这个 var 属性就是抽象属性
  • 抽象的 var 属性在编译成字节码文件时,属性并不会声明,但是会自动生成抽象方法,所以类必须声明为抽象类
  • 如果是覆写一个父类的抽象 var 属性,那么 override 关键字可省略
  • 如果是 var 属性覆写非抽象的 var 属性,运行时会报错,参考 StackOverflow
8. 抽象类

Scala 中,通过abstract关键字标记不能被实例化的类方法不用标记abstract,只要省掉方法体即可。抽象类可以拥有抽象字段,抽象字段就是没有初始值的字段

示例代码

如果在抽象方法前面加了 abstract ,运行时将会报以下错误,非常奇葩~

抽象类的价值

抽象类的价值更多在于设计,是设计者设计好之后,让子类去继承并实现(表示一种规范)

Scala 抽象类的细节

  1. 抽象类不能被实例化

  2. 抽象类不一定要包含 abstract 方法

  3. 一旦类包含了抽象方法或者抽象属性,则这个类必须声明为 abstract

  4. 抽象方法不能有主体,不允许使用 abstract 修饰

  5. 如果一个类继承了抽象类,则它必须实现抽象类的所有抽象方法和抽象属性,除非它自己也声明为 abstract

  6. 抽象方法和抽象属性不能使用 private、final 来修饰,因为这些关键字都是和重写/实现相违背的

  7. 子类重写抽象方法不需要 override ,写上也不会错

9. 匿名子类

回顾 Java 的匿名子类

示例代码

public class Demo {
    public static void main(String[] args) {
        Man man = new Man() {
            @Override
            void work() {
                System.out.println("厨师炒菜挣钱");
            }
        };
//        厨师炒菜挣钱
        man.work();
    }
}

abstract class Man {
    /**
     * 挣钱的方法
     */
    abstract void work();
}
复制代码

Scala 的匿名子类

object SubDemo2 {
  def main(args: Array[String]): Unit = {
    val monkey = new Monkey {
      override var name: String = "金丝猴"

      override def eat(): Unit = {
        println("吃桃子")
      }
    }
    monkey.eat()
    println(monkey.name)
  }
}

abstract class Monkey {
  var name: String
  def eat()
}
复制代码
10. 继承层级

请参考《第三章:变量》

11. 练习

定义员工类,包含姓名和月工资,以及计算年薪的方法。普通员工和经理继承了员工,经理类多了奖金属性和管理方法,普通员工多了工作方法,并且普通员工和经理均要重写计算年薪的方法

测试类中添加一个方法,实现获取任何员工年薪的需求

测试类中添加一个方法,实现如果是普通员工,调用工作方法;如果是经理,调用管理方法的需求

object Practice {
  def main(args: Array[String]): Unit = {
    val worker = new Worker2
    val manager = new Manager

    showEmployeeAnnual(worker)
    showEmployeeAnnual(manager)
    testEmployee(worker)
    testEmployee(manager)

  }

  def showEmployeeAnnual(e: Employee): Unit = {
    println(e.getAnnual)
  }

  def testEmployee(e: Employee): Unit = {
    if (e.isInstanceOf[Worker2]) e.asInstanceOf[Worker2].work()
    else if (e.isInstanceOf[Manager]) e.asInstanceOf[Manager].manage()
  }
}

abstract class Employee {
  // 定义抽象属性
  var name: String
  var salary: Double

  // 定义抽象方法
  def getAnnual: Double
}

class Worker2 extends Employee {
  override var name: String = "工人"
  override var salary: Double = 2000

  override def getAnnual: Double = {
    this.salary * 12
  }

  def work(): Unit = {
    println("工人工作~")
  }
}

class Manager extends Employee {
  override var name: String = "经理"
  override var salary: Double = 20000.0
  var bonus = 60000

  override def getAnnual: Double = {
    this.salary * 12 + this.bonus
  }

  def manage(): Unit = {
    println("经理在管理~")
  }
}
复制代码
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值