Scala學習筆記(4)-面向對象編程下篇

本文是對Scala語言面向對象編程的學習總結(下篇),共包括如下章節(jié):

  • 類的繼承
  • 抽象類
  • 內部類
  • 匿名類
  • 特質(trait)
  • 泛型類

參考資料:
1、如果要了解scala面向對象編程的基礎內容,可參考《Scala學習筆記(3)-面向對象編程上篇》。

2、如果要了解scala語言的面向函數的編程知識,可參考《Scala學習筆記(5)-函數式編程》。

一、類的繼承

(一)基本概念

繼承是面向對象編程中的重要特性之一,很多面向對象的編程語言(如c++,java,python等)都支持繼承的特性。但是面向對象編程中有一個重要的原則是“高內聚、低耦合”,繼承帶來很多好處,但也會增加子類和父類之間的耦合程度,任何一方的改變很容易影響另一方,所以在面向對象的編程實踐中,推薦盡量少用繼承,可以多用組合(即一個類的對象是另一個類的成員)來代替繼承,甚至有的編程語言(如go語言)直接不支持繼承這個特性。

下面來簡單介紹下scala語言的繼承特性。scala語言同Java一樣,通過關鍵字extends來實現(xiàn)類的繼承,同樣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
  }
}

如果父類有輔助構造函數,子類的定義中也可以按照與輔助構造函數參數一致的方式來定義構造函數。

(三)繼承中的多態(tài)

同java一樣,在scala中,子類可以重寫(override)父類的方法,這是面向對象編程中的多態(tài)特性的一種表現(xiàn)。多態(tài)是面向對象編程的重要特點之一,是面向對象編程的靈魂,可以說沒有多態(tài),面向對象編程就失去了活力。

多態(tài)(Polymorphic)也稱為動態(tài)綁定(Dynamic Binding)或延遲綁定(Late Binding),指在執(zhí)行期而非編譯期確定所引用對象的實際類型,根據其實際類型調用其相應的方法。也就是說子類的引用可以賦值給父類的變量,程序在運行期根據實際類型調用具體的方法。

如下面例子:

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關鍵字,否則編譯無法通過。

運行上面程序,我們可以到執(zhí)行時動態(tài)綁定的效果。

二、抽象類

抽象類是指不能被實例化(創(chuàng)建對象)的類,java和c++都有抽象類的概念,scala也有抽象類的概念。對于scala的抽象類,有如java,c++中抽象類的一些共性的特點:

  • 抽象類不能被實例化

  • 同java一樣,scala使用abstract關鍵字來定義抽象類

  • 抽象方法是指只有聲明,沒有實現(xiàn)部分的方法。

  • 如果一個類中有抽象方法,則該類必須定義為抽象類(即必須加上abstract關鍵字)。

  • 一個抽象類中,可以沒有抽象方法,而只有普通方法(這里指有實現(xiàn)的方法)。

  • 子類繼承抽象類時,需要重寫并實現(xiàn)父類中的抽象方法,否則子類也必須聲明為抽象類,這時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類,并重寫實現(xiàn)了父類的抽象方法(沒加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類的對象變量)去使用。需要注意的是,如果要創(chuàng)建內部類的對象(如上面的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操作創(chuàng)建了一個對象。前面介紹抽象類時我們知道,抽象類不能直接被實例化。這里的new操作不是實例化一個抽象類,實際實例化的是Animal的一個子類,也就是說new關鍵字后的代碼其實是定義了一個匿名類(該類是Animal的子類),該匿名類實現(xiàn)了抽象方法show方法。

使用匿名類,在很多場合下可以簡化代碼編寫,讓代碼看起來更加簡潔和清晰。

五、特質(trait)

(一)基本概念

scala沒有java中的接口功能,而是通過關鍵字trait提供了一個與Java接口類似的但又不太一樣的機制。

我們可以通過關鍵字trait來定義一個特質,一個類可以混入一個或多個特質,就如java中一個類可以實現(xiàn)一個或多個接口一樣。

下面我們先看一個定義特質的例子:

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關鍵字。

如果一個類引入特質,需要實現(xiàn)特質中定義的抽象成員,否則該類要定義為抽象類。如下面例子:

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,但沒有實現(xiàn)其抽象方法show,所以Deml1類必須定義為抽象類。而類Demo2引入了特質Visible和Runnable,且實現(xiàn)了這兩個特質中定義的方法,所以可以不定義為抽象類,且可以被實例化使用。

當一個類引入單個特質時,使用extends關鍵字;當引入多個特質時,第一個特質使用extends關鍵,其它的使用with關鍵字。如果一個類同時繼承了一個類,且引入了特質,則使用extends關鍵字引入繼承,使用with引入特質。

(二)trait中的成員

scala特質中的成員,不僅可以有抽象方法,也可以有抽象變量、具體變量和具體方法。所以,從功能上看,特質更像抽象類。只不過類只能被單重繼承。

前面我們介紹類時,知道可以直接在類中添加可執(zhí)行語句,這樣在創(chuàng)建對象時,這些語句會被執(zhí)行,相當于主構造函數的代碼。同樣在特質中,也可以加入可執(zhí)行代碼,創(chuàng)建對象時會被自動執(zhí)行,可理解成特質的構造函數代碼。

在創(chuàng)建一個對象時,構造函數(或類中可執(zhí)行代碼)執(zhí)行的順序是:

1、如果有超類,則先調用超類的構造函數

2、如果混入的特質有父特質,會按照繼承層次先調用父特質的構造函數(即特質中的可執(zhí)行代碼)

3、如果有多個父特質,則按照從左到右順序執(zhí)行

4、所有父類構造函數和父特質被構造完成后,才會執(zhí)行本類的構造函數。

需要注意的是,特質無法像類一樣通過主構造函數來定義成員變量。

(三)trait中的多態(tài)

我們可以定義一個trait類型的變量,去引用一個對象,在運行時,會實際執(zhí)行綁定的對象。如下面例子:

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,然后定義了兩個具體的類實現(xiàn)了該特質。在main方法中,可以看到Animal的變量可以指向具體的對象。運行會發(fā)現(xiàn),實際執(zhí)行的是綁定的對象實例。

六、泛型類

同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面向對象編程的繼承、抽象類、多態(tài)、特質、泛型類等概念進行了介紹。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容