欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Scala学习笔记(4)-面向对象编程下篇

程序员文章站 2022-06-14 22:12:07
...

本文是对Scala语言面向对象编程的学习总结(下篇),共包括如下章节:

  • 类的继承
  • 抽象类
  • 内部类
  • 匿名类
  • 特质(trait)
  • 泛型类

参考资料:
1、如果要了解scala面向对象编程的基础内容,可参考《Scala学习笔记(3)-面向对象编程上篇》

2、如果要了解scala语言的面向函数的编程知识,可参考《Scala学习笔记(5)-函数式编程》

一、类的继承

(一)基本概念

继承是面向对象编程中的重要特性之一,很多面向对象的编程语言(如c++,java,python等)都支持继承的特性。但是面向对象编程中有一个重要的原则是“高内聚、低耦合”,继承带来很多好处,但也会增加子类和父类之间的耦合程度,任何一方的改变很容易影响另一方,所以在面向对象的编程实践中,推荐尽量少用继承,可以多用组合(即一个类的对象是另一个类的成员)来代替继承,甚至有的编程语言(如go语言)直接不支持继承这个特性。

下面来简单介绍下scala语言的继承特性。scala语言同Java一样,通过关键字extends来实现类的继承,同样scala只支持单重继承。

下面看一个简单例子,代码如下:

class Person{
  val name="jack"
  var age:Int = 2
}

class Child extends Person

object Hello {
  def main(args: Array[String]){
    val child = new Child
    println(child.name)
  }
}

上面例子代码,Child类继承了Person类,这时Person类的成员也被Child类继承了。

(二)父类有构造函数

我们来看下,当父类有构造函数时,继承会有什么变化?假设父类的定义如下:

class Person(var name:String,var age:Int){
    def show{
      println(name+"="+age)
    }
}

Person类中主构造函数声明了两个成员变量,但成员变量没有设置缺省值。这时我们定义Person类的子类的定义和使用方式如下:

class Child( name:String, age:Int) extends Person(name,age)

object Hello {
  def main(args: Array[String]){
    val child = new Child("jack",12)
    child.show
  }
}

因为父类有构造函数,定义子类时需要向父类传递参数,语法格式如上面。注意,上面定义中的变量名name和age不一定和父类中的一样。

注意,如果父类的成员变量有默认值,则子类不需要向父类传递参数,如下面代码是正确的:

class Person(var name:String="jack",var age:Int=12){
    def show{
      println(name+"="+age)
    }
}

class Child extends Person

object Hello {
  def main(args: Array[String]){
    val child = new Child
    child.show
  }
}

如果父类有辅助构造函数,子类的定义中也可以按照与辅助构造函数参数一致的方式来定义构造函数。

(三)继承中的多态

同java一样,在scala中,子类可以重写(override)父类的方法,这是面向对象编程中的多态特性的一种表现。多态是面向对象编程的重要特点之一,是面向对象编程的灵魂,可以说没有多态,面向对象编程就失去了活力。

多态(Polymorphic)也称为动态绑定(Dynamic Binding)或延迟绑定(Late Binding),指在执行期而非编译期确定所引用对象的实际类型,根据其实际类型调用其相应的方法。也就是说子类的引用可以赋值给父类的变量,程序在运行期根据实际类型调用具体的方法。

如下面例子:

class Person{
    def show{
      println("i am person")
    }
}

class Child extends Person{
  override def show{
    println("i am child")
  }
}

object Hello {
  def main(args: Array[String]){
    var per:Person = null
    per = new Person
    per.show
    per = new Child
    per.show
  }
}

上面例子中,Child类重写了父类Person类的show方法,注意 在scala中,重写方法一定要显示的加上override关键字,否则编译无法通过。

运行上面程序,我们可以到执行时动态绑定的效果。

二、抽象类

抽象类是指不能被实例化(创建对象)的类,java和c++都有抽象类的概念,scala也有抽象类的概念。对于scala的抽象类,有如java,c++中抽象类的一些共性的特点:

  • 抽象类不能被实例化

  • 同java一样,scala使用abstract关键字来定义抽象类

  • 抽象方法是指只有声明,没有实现部分的方法。

  • 如果一个类中有抽象方法,则该类必须定义为抽象类(即必须加上abstract关键字)。

  • 一个抽象类中,可以没有抽象方法,而只有普通方法(这里指有实现的方法)。

  • 子类继承抽象类时,需要重写并实现父类中的抽象方法,否则子类也必须声明为抽象类,这时override关键字可忽略。

下面看一个例子代码:

abstract class Animal{
  def show
}

class Dog extends Animal{
  def show{
    println("i am dog")
  }
}

object Hello {
  def main(args: Array[String]){
    var animal:Animal = new Dog
    animal.show
  }
}

上面代码中的Animal类中定义了一个抽象方法,所以Animal类必需被定义为抽象类。Dog类继承了Animal类,并重写实现了父类的抽象方法(没加override关键字)。

与Java,c++不同的是,scala抽象类中不仅可以有抽象方法,也可以有抽象成员变量。根据前面的介绍我们知道,在scala中,类中的成员变量必须要初始化,否则该类必须要定义成抽象类。

如下面例子:

abstract class Animal{
  var name:String
}

class Dog extends Animal{
  var name:String = "tom"
}

object Hello {
  def main(args: Array[String]){
    var animal:Animal = new Dog
    println(animal.name)
  }
}

上面例子中,Animal中定义了name变量,但没有初始化,所以name变量为抽象成员变量,这时类必须定义为抽象类(加上abstract关键字修饰)。Dog类继承了Animal类,重写并初始化了父类的name变量(可以省去override关键字)。

三、内部类

对于scala,可以在一个类中定义一个类或者单例对象。如下面例子:

class Demo{
  class Person{
    var name:String="tom"
  }
  object User{
    var age:Int=12
  }
}

object Hello {
  def main(args: Array[String]){
      val demo = new Demo
      var per = new demo.Person //demo必须定义为val,不能为var
      println(per.name)
      println(demo.User.age)
  }
}

上面代码中,Demo类中定义了一个内部类Person和内部单例对象User。使用内部类和单例对象,和使用普通的成员变量一样,需要通过外部类的引用(即Demo类的对象变量)去使用。需要注意的是,如果要创建内部类的对象(如上面的Person类的对象),需要的外部类引用必须是val型的,不能是var型的。

同样,在单例对象中也能定义内部类和内部单例对象,如下面例子代码:

object Demo{
  class Person{
    var name:String="tom"
  }
  object User{
    var age:Int=12
  }
}

object Hello {
  def main(args: Array[String]){
      var per = new Demo.Person
      println(per.name)
      println(Demo.User.age)
  }
}

上面代码是在单例对象中定义了内部类和单例对象,使用时就如同普通的成员一样,直接通过单例对象名去使用。

四、匿名类

匿名类,顾明思议就是没有名字的类,使用匿名类,通常是继承某个类(如抽象类)时使用,下面看一个具体例子。

abstract  class Animal{
  def show
}

object Hello {
  def main(args: Array[String]){
      var animal = new Animal{
        def show{
          println("i am a dog")
        }
      }
    animal.show
  }
}

上面代码,先定义了一个抽象类Animal。在main方法中,我们直接通过new操作创建了一个对象。前面介绍抽象类时我们知道,抽象类不能直接被实例化。这里的new操作不是实例化一个抽象类,实际实例化的是Animal的一个子类,也就是说new关键字后的代码其实是定义了一个匿名类(该类是Animal的子类),该匿名类实现了抽象方法show方法。

使用匿名类,在很多场合下可以简化代码编写,让代码看起来更加简洁和清晰。

五、特质(trait)

(一)基本概念

scala没有java中的接口功能,而是通过关键字trait提供了一个与Java接口类似的但又不太一样的机制。

我们可以通过关键字trait来定义一个特质,一个类可以混入一个或多个特质,就如java中一个类可以实现一个或多个接口一样。

下面我们先看一个定义特质的例子:

trait Visible{
  def show
}

上面代码定义了一个特质Visible,该特质中有一个抽象方法show。如同java的接口一样,特质也可以继承。如下面例子:

trait Visible{
  def show
}

trait Runnable{
  def run
}

trait Demo1 extends Visible
trait Demo2 extends Visible with Runnable

当一个特质只继承单个特质时,使用extends关键字;如果继承多个特质,继承的第一个特质使用extends关键字,其它的特质使用with关键字。

如果一个类引入特质,需要实现特质中定义的抽象成员,否则该类要定义为抽象类。如下面例子:

trait Visible{
  def show
}

trait Runnable{
  def run
}

abstract class Demo1 extends Visible

class Demo2 extends Visible with Runnable{
  def show = {println("show")}
  def run = {println("run")}
}

object Hello {
  def main(args: Array[String]){
    var demo2 = new Demo2
    demo2.show
    demo2.run
  }
}

上面代码中,类Demo1引入了特质Visible,但没有实现其抽象方法show,所以Deml1类必须定义为抽象类。而类Demo2引入了特质Visible和Runnable,且实现了这两个特质中定义的方法,所以可以不定义为抽象类,且可以被实例化使用。

当一个类引入单个特质时,使用extends关键字;当引入多个特质时,第一个特质使用extends关键,其它的使用with关键字。如果一个类同时继承了一个类,且引入了特质,则使用extends关键字引入继承,使用with引入特质。

(二)trait中的成员

scala特质中的成员,不仅可以有抽象方法,也可以有抽象变量、具体变量和具体方法。所以,从功能上看,特质更像抽象类。只不过类只能被单重继承。

前面我们介绍类时,知道可以直接在类中添加可执行语句,这样在创建对象时,这些语句会被执行,相当于主构造函数的代码。同样在特质中,也可以加入可执行代码,创建对象时会被自动执行,可理解成特质的构造函数代码。

在创建一个对象时,构造函数(或类中可执行代码)执行的顺序是:

1、如果有超类,则先调用超类的构造函数

2、如果混入的特质有父特质,会按照继承层次先调用父特质的构造函数(即特质中的可执行代码)

3、如果有多个父特质,则按照从左到右顺序执行

4、所有父类构造函数和父特质被构造完成后,才会执行本类的构造函数。

需要注意的是,特质无法像类一样通过主构造函数来定义成员变量。

(三)trait中的多态

我们可以定义一个trait类型的变量,去引用一个对象,在运行时,会实际执行绑定的对象。如下面例子:

trait Animal{
  def show={
    println("i am a animal")
  }
}

class Dog extends Animal{
  override def show={
    println("i am a dog")
  }
}

class Cat extends Animal{
  override def show={
    println("i am a cat")
  }
}

object Hello {
  def main(args: Array[String]){
    var animal:Animal = new Dog
    animal.show
    animal = new Cat
    animal.show
  }
}

上面代码中,定义了一个特质Animal,然后定义了两个具体的类实现了该特质。在main方法中,可以看到Animal的变量可以指向具体的对象。运行会发现,实际执行的是绑定的对象实例。

六、泛型类

同java,c++一样,scala也支持泛型类,泛型类是将类型作为参数的类,这样一个泛型类可以支持不同的数据类型。

下面我们先看一个简单的例子:

class A[T](var value:T){
  def show={
    println(value)
  }
}

object Hello {
  def main(args: Array[String]){
    var a:A[String] = new A[String]("tom")
    a.show

    var b:A[Int] = new A[Int](10)
    b.show
  }
}

上面代码定义了一个泛型类A,可以传入的数据类型是不定的。在使用时,可以传入各种数据类型。

一般情况下,我们很少自己设计泛型类。无论是在java中,还是scala中,泛型使用最多的场景是集合类。关于scala集合的操作,本文中不作介绍。

七、小结

本文对scala面向对象编程的继承、抽象类、多态、特质、泛型类等概念进行了介绍。