1. 程式人生 > >Scala面向物件程式設計

Scala面向物件程式設計

Scala面向物件程式設計

目錄:

1 類與物件初步
2 引用與值型別
3 價值類
4 父類
5 Scala的構造器
6 類的欄位
    6.1 統一訪問原則
    6.2 一元方法
7 驗證輸入
8 呼叫父類構造器與良好的面向物件設計
9 巢狀型別

 

Scala 是一個函數語言程式設計語言,也是一個面向物件的程式語言,與Java、Python、Ruby、Smalltalk 等其他語言一樣。在此強調兩點,首先,函數語言程式設計已經成為解決現代程式設計問題的一項基本技能,這個技能對你而言可能是全新的。開始使用Scala 時,人們很容易把它作為一個“更好的Java”語言來使用,而忽略了它“函式式的一面”。其次,Scala 在架構層面上提倡的方法是:小處用函數語言程式設計,大處用面向物件程式設計。用函式式實現演算法、操作資料,以及規範地管理狀態,是減少bug、壓縮程式碼行數和降低專案延期風險的最好方法。另一方面,Scala 的OO 模型提供很多工具,可用來設計可組合、可複用的模組。這對於較大的應用程式是必不可少的。因此,Scala 將兩者完美地結合在了一起。

1 類與物件初步

類用關鍵字class 宣告,而單例物件用關鍵字object 宣告;(單例模式和Java的實現方式不同)

在類宣告之前加上關鍵字final,可以避免從一個類中派生出其他類;

abstract 關鍵字可以阻止類的例項化;

一個例項可以使用this 關鍵字指代它本身。(。儘管在Java 程式碼中經常看到this 的這種用
法,Scala 程式碼中卻很少看到。原因之一是,Scala 中沒有樣板建構函式。)

Java建立一個Person類:

package cn.com.tengen.test;

public class Person {
    private String name;
    private int age;
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
    public void setName(String name) { this.name = name; }
    public String getName() { return this.name; }
    public void setAge(int age) { this.age = age; }
    public int getAge() { return this.age; }
}

但是如果用Scala實現就比較簡單了

package cn.com.tengen.test

class Person(var name: String, var age: Int){

}

使用scala測試一下

package cn.com.tengen.test.obj

class Person(var name: String,var age: Int){

}

object Person extends App {

    val p = new Person("Lucky",18)
  p.age = 20
    println(p.name+"---"+p.age)

}

輸出結果:

Lucky---20

在構造引數前加上var,使得該引數成為類的一個可變欄位,這在其他的OO 語言中也稱為例項變數或屬性。

在構造引數前加上val,使得該引數成為類的一個不可變欄位,

用case 關鍵字可以推斷出val,同時自動增加一些方法,如下所示:

package cn.com.tengen.test.obj

case class ImmutablePerson(name: String, age: Int) {

}

object ImmutablePerson extends App{
  val p = new ImmutablePerson("Lucky",18)
  println(p.name+"---"+p.age)
}

case做了那些事:

1.重寫了toString
2.預設實現了equals 和hashCode
3.預設是可以序列化的,也就是實現了Serializable
4.自動從scala.Product中繼承一些函式
5.case class建構函式的引數是public級別的,我們可以直接訪問
6.支援模式匹配
7.實現了copy方法

method(方法)指與例項繫結在一起的函式。換句話說,它的引數列表中有一個“隱含”的this 引數。方法用關鍵字def 定義。當其他函式或方法需要一個函式作為引數時,Scala 會自動將可用的方法“提升”為函式,作為前者的函式引數。

如同大多數靜態型別語言一樣,Scala 允許方法過載。只要各方法的完整簽名是唯一的,兩個或更多方法就可以具有相同的名稱。方法的簽名包括返回型別,方法名稱和引數型別的列表(引數的名稱不重要)。因此,在JVM 中只憑不同的返回型別不足以區分不同的方法。

  def f1(s : String): Unit ={
    println(s)
  }

  def f1(s : String,i : Int): Unit ={
    println(s)
  }

型別名稱必須唯一。

成員是類的欄位、方法或型別的統稱。與Java 不同,如果方法有引數列表,該方法可以與類的欄位同名:

2 引用與值型別

Java 語法為JVM 實現資料的方式提供了模型。首先,它提供了一組原生型別:short、int、long、float、double、boolean、char、byte 和關鍵字void。它們被儲存在堆疊中,或為了獲得更好的效能,被儲存於CPU 暫存器。

Scala 固然必須符合JVM 的規則,但Scala 做了改進,使得原生型別和引用型別的區別更明顯。

所有引用型別都是AnyRef 的子型別。AnyRef 是Any 的子型別,而Any 是Scala 型別層次的根型別。所有值型別均為AnyVal 的子型別,AnyVal 也是Any 的子型別。Any 僅有這兩個直接的子型別。需要注意,Java 的根型別Object(http://docs.oracle.com/javase/8/docs/api/java/lang/Object.html)實際上更接近Scala 的AnyRef,而不是Any。
引用型別仍用new 關鍵字來建立。類似其他不帶引數的方法一樣,如果構造器不帶引數(在有的語言中稱為“預設構造器”),我們在使用構造器時也可以去掉後面的括號。


Scala 沿用了Java 中數字和字串的字面量語法。例如: 在Scala 中,val name ="Programming Scala" 與val name = new String("Programming Scala") 等價。不過,Scala還為元組增加了字面量語法,(1,2,3) 就等價於new Tuple3(1,2,3)。我們已經接觸過Scala的一些語言特性,可以實現編譯器原本不支援的字面量寫法,如:用1 :: 2 :: 3 :: Nil表示Map("one" ->, "two" -> 2)。
用帶apply 方法的物件建立引用型別的例項是很常見的做法,apply 方法起到工廠的作用(這種方法必須在內部呼叫new 或對應的字面量語法)。由於case 類會自動生成伴隨物件及其apply 方法,因此case 類的例項通常就是用這種方法創建出來的。
Short、Int、Long、Float、Double、Boolean、Char、Byte 和Unit 型別稱為值型別,分別對應JVM 的原型short、int、long、float、double、boolean、char、byte 和void 關鍵字。在Scala 的物件模型中,所有的值型別均為AnyVal 的子型別,AnyVal 是Any 的兩個子型別之一。
值型別的“例項”不是在堆上建立的。相反,Scala 用JVM 原生型別來表示值型別,它們的值都存放在暫存器或棧上。值型別的“例項”總是用字面量來建立,如1,3.14,true。Unit 對應的字面量是(),不過我們很少顯式地使用。
事實上,值型別沒有構造器,所以像val I = new Int(1) 這樣的表示式將無法通過編譯。

3 價值類

package cn.com.tengen.test.obj

class Dollar(val value: Float) extends AnyVal {
  override def toString = "$%.2f".format(value)
}
object Dollar extends App{
  val hello = new Dollar(100)
  println(hello)
}


//輸出結果:$100.00

要成為一個有效的價值類,必須遵守以下的規則。

(1) 價值類有且只有一個公開的val 引數(對於Scala 2.11,該引數也可以是不公開的)。
(2) 引數的型別不能是價值類本身。
(3) 價值類被引數化時,不能使用@specialized(http://www.scala-lang.org/api/current/scala/specialized.html)標記。
(4) 價值類沒有定義其他構造器。
(5) 價值類只定義了一些方法,沒有定義其他的val 和var 變數。
(6) 然而,價值類不能過載equals 和hashCode 方法。
(7) 價值類定義沒有巢狀的特徵、類或物件。
(8) 價值類不能被繼承。
(9) 價值類只能繼承自通用特徵。
(10) 價值類必須是物件可引用的一個頂級型別或物件的一個成員。

通常,被包裝的型別是AnyVal 的子型別之一,但並不是必須如此。如果換成引用型別,我們仍然可以受益於記憶體不在堆上分配的優勢。例如,下例中,隱含了對電話號碼字串的包裝:

package cn.com.tengen.test.obj

class USPhoneNumber(val s: String) extends AnyVal {
  override def toString = {
    val digs = digits(s)
    val areaCode = digs.substring(0,3)
    val exchange = digs.substring(3,6)
    val subnumber = digs.substring(6,10) // “客戶編號”
    s"($areaCode) $exchange-$subnumber"
  }
  private def digits(str: String): String = str.replaceAll("""\D""", "")
}

object USPhoneNumber extends App{
  val number = new USPhoneNumber("987-654-3210")
  println(number)
}

//輸出結果:(987) 654-3210

一個通用特徵具有以下特性:

(1) 它可以從Any 派生(而不能從其他通用特徵派生)。
(2) 它只定義方法。
(3) 它沒有對自身做初始化。

下面給出了一個改進版的USPhoneNumber,這裡混用了兩個通用特徵:

package cn.com.tengen.test.obj

/**
  * Digitizer 是一個通用特徵,定義了我們之前的digits 方法。
  */
trait Digitizer extends Any {
  def digits(s: String): String = s.replaceAll("""\D""", "")
}

/**
  * Formatter 特徵按我們想要的格式對電話號碼進行格式化。
  */
trait Formatter extends Any {
  def format(areaCode: String, exchange: String, subnumber: String): String =
    s"($areaCode) $exchange-$subnumber"
}


class USPhoneNumber(val s: String) extends AnyVal
  with Digitizer with Formatter {
  override def toString = {
    val digs = digits(s)
    val areaCode = digs.substring(0,3)
    val exchange = digs.substring(3,6)
    val subnumber = digs.substring(6,10)
    //呼叫Formatter.format。
    format(areaCode, exchange, subnumber)
  }
}
object USPhoneNumber extends App{
  val number = new USPhoneNumber("987-654-3210")
  println(number)
}
//輸出結果:(987) 654-3210

Formatter 實際上解決了一個設計上的問題。我們可能要給USPhoneNumber 指定另一個引數作為格式字串,或需要一些機制去配置toString 的格式,因為流行的格式可能有很多。但是,我們只允許傳遞一個引數給USPhoneNumber。針對這種情況,我們可以在通用特徵中去配置我們想要的格式!然而,由於JVM 的限制,通用特徵有時會觸發例項化(即例項的記憶體分配於堆中)。這裡將需要例項化的情況總結如下。

(1) 當價值類的例項被傳遞給函式作引數,而該函式預期引數為通用特徵且需要被例項實現。不過,如果函式的預期引數是價值類本身,則不需要例項化。
(2) 價值類的例項被賦值給陣列。
(3) 價值類的型別被用作型別引數。

4 父類

子類是從父類或基類中派生的派生類,是大部分面嚮物件語言的核心特徵。這種機制用來重用、封裝和實現多型行為(具體行為取決於例項在型別層次結構中的實際型別)。像Java 一樣,Scala 只支援單一繼承,而不是多重繼承。子類(或派生類)可以有且只有一個父類(即基類)。唯一的例外是,Scala 的型別結構中的根類Any 沒有父類。

例項:

abstract class BulkReader {
  type In
  val source: In
  def read: String // Read source and return a String
}
class StringBulkReader(val source: String) extends BulkReader {
  type In = String
  def read: String = source
}
class FileBulkReader(val source: java.io.File) extends BulkReader {
  type In = java.io.File
  def read: String = {...}
}

如在Java 中一樣,關鍵字extend 表示後面是父類,因此本例中的父類為BulkReader。
在Scala 中,當類繼承trait 時,也用extend 表示(甚至當該類用with 關鍵字混入了其他trait時也是如此)。
此外,當trait 是其他trait 或類的子trait 時,也用extend。是的,trait 可以繼承類。
如果我們不指定父類,預設父類為AnyRef。
 

5 Scala的構造器

Scala 將主構造器與零個或多個輔助構造器區分開,輔助構造器也被稱為次級構造器。在Scala 中,主構造器是整個類體。構造器所需的所有引數都被羅列在類名稱後面。

package cn.com.tengen.test.obj

case class Address(street: String, city: String, state: String, zip: String) {
  def this(zip: String) = this("[unknown]", Address.zipToCity(zip), Address.zipToState(zip), zip)
}
object Address {
  def zipToCity(zip: String) = "Anytown"
  def zipToState(zip: String) = "CA"
}
case class Person(name: String, age: Option[Int], address: Option[Address]) {
  def this(name: String) = this(name, None, None)
  def this(name: String, age: Int) = this(name, Some(age), None)
  def this(name: String, age: Int, address: Address) = this(name, Some(age), Some(address))
  def this(name: String, address: Address) = this(name, None, Some(address))
}

構造器呼叫

object Main extends App{
  var address = new Address("Lucky");
  var p = Person("lucky",Some(20),Some(address));
  println(p.name+"---"+p.age.get+"---"+p.address.get)
}

在我們的實現中,使用者使用 new 來建立例項,使用主構造器建立例項時new可以省略

6 類的欄位

如果在主建構函式引數的前面加上val 或var 關鍵字,該引數就成為例項的一個欄位。對於case 類,val 是預設的。這樣可以大大減少冗餘的程式碼。是Scala 會自動做Java 程式碼中明顯做的事情。類會建立一個私有的欄位,並生產對應的getter 和setter 訪問方法。

class Name (var value: String)

改程式碼等價於

class Name(s: String){
  private var _value: String = s //不可見的欄位,在本例中宣告為可變變數。
  def value: String = _value //getter,即讀方法。
  def value_= (newValue: String): Unit = _value = newValue //setter,即寫方法。
}

注意value_= 這個方法名的一般規範。當編譯器看到這樣的一個方法名時,它會允許客戶端程式碼去掉下劃線_,轉而使用中綴表示式,這就好像我們是在設定物件的一個裸欄位一樣:

object Main extends App{
  val name = new Name("Lucky")
  println(name.value)
  name.value_=("Helen")
  println(name.value)
  name.value="Jon"
  println(name.value)
  name.value=("Lucy")
  println(name.value)
}

執行結果:
Lucky
Helen
Jon
Lucy

6.1 統一訪問原則

Scala 沒有遵循JavaBeans 的約定規範,沒有把value 欄位的讀方法和寫方法分別命名為getValue 和setValue,但是Scala 遵循統一訪問的原則。正如我們在Name 這個例子中看到的,客戶端似乎可以不經過方法就對“裸”欄位值進行讀和寫的操作,但實際上它們呼叫了方法。

請注意,使用者的“體驗”是一致的。使用者程式碼不瞭解實現,這使我們可以在需要的時候,自由地將直接操作裸欄位改為通過訪問方法來操作。例如:我們要在寫操作中新增某些驗證工作,或者為了提高效率,在讀取某個結果時採用惰性求值。這些情況下,我們可以通過訪問方法來操作裸欄位。相反地,我們也可以用公開可見性的欄位代替訪問方法,以消除該方法呼叫的開銷(儘管JVM 可能會消除這種開銷)。因此,統一訪問原則的重要益處在於,它能最大程度地減少客戶端程式碼對類的內部實現所必須瞭解的知識。儘管重新編譯仍然是必需的,我們可以改變具體實現,而不必強制客戶端程式碼跟著做出改變。

Scala 實現統一訪問原則的同時,沒有犧牲訪問控制功能,並且滿足了在簡單的讀寫基礎上增加其他邏輯的需求。

6.2 一元方法

package cn.com.tengen.test.obj

case class Complex(real: Double, imag: Double) {
  //方法名為unary_X,這裡X 就是我們想要使用的操作符。在本例中,X 就是-。注意-和: 之間的空格,空格在這裡是必須的,它可以告訴編譯器方法名以- 而不是: 結尾!為了比較,我們也實現了常見的減號操作符。
  def unary_- : Complex = Complex(-real, imag)
  def -(other: Complex) = Complex(real - other.real, imag - other.imag)
}

object Main extends App{
  val c1 = Complex(88.8, 88.8)
  val c2 = -c1
  val c3 = c1.unary_-
  val c4 = c1 - Complex(22.2, 22.2)
  println(c1)
  println(c2)
  println(c3)
  println(c4)
}

輸出結果:
Complex(88.8,88.8)
Complex(-88.8,88.8)
Complex(-88.8,88.8)
Complex(66.6,66.6)

方法名為unary_X,這裡X 就是我們想要使用的操作符。在本例中,X 就是-。

注意-和: 之間的空格,空格在這裡是必須的,它可以告訴編譯器方法名以- 而不是: 結尾!

我們一旦定義了一元操作符,就可以將操作符放在例項之前,就像我們在定義c2 時所做的那樣。也可以像定義c3 時那樣,將一元操作符當做其他方法一般進行呼叫。

7 驗證輸入

保證建立例項引數處於有效的狀態,示例如下:

package cn.com.tengen.test.obj

case class ZipCode(zip: Int, extension: Option[Int] = None) {
  // 使用require 方法驗證輸入。
  require(valid(zip, extension),  s"Invalid Zip+4 specified: $toString")
  protected def valid(z: Int, e: Option[Int]): Boolean = {
    if (0 < z && z <= 99999) e match {
      case None => validUSPS(z, 0)
      case Some(e) => 0 < e && e <= 9999 && validUSPS(z, e)
    }
    else false
  }

  /**
    * 真正的方法實現應該查詢USPS 認可的資料庫來驗證郵政編碼是否確實存在。
    */
  protected def validUSPS(i: Int, e: Int): Boolean = true

  /**
    * 覆寫toString 方法,返回符合人們預期的郵政編碼格式,對結尾可能的四位數字進行覆寫toString 方法,
    * @return 返回符合人們預期的郵政編碼格式,對結尾可能的四位數字進行
    */
  override def toString =  if (extension != None) s"$zip-${extension.get}" else zip.toString
}
object ZipCode {
  def apply (zip: Int, extension: Int): ZipCode =
    new ZipCode(zip, Some(extension))

  def main(args: Array[String]): Unit = {
    var z1 = ZipCode(12345)
    println(z1) //12345
    var z2 = ZipCode(12345, Some(6789))
    println(z2) //12345-6789
    var z3 = ZipCode(123456)
    println(z3) //異常 java.lang.IllegalArgumentException: requirement failed: Invalid Zip+4 specified: 123456
  }
}

定義ZipCode 這種領域專用的類的充分理由是:這種類可以在構造時對值的有效性做一次 檢驗,然後類ZipCode 的使用者就不再需要再次檢驗了。

雖然我們在構造器的背景下討論輸入的驗證,但實際上我們也可以在任何方法中呼叫這些斷言方法。然而,價值類的類體是一個例外,它不能使用斷言驗證,否則就需要呼叫分配堆。不過,由於ZipCode 帶有兩個構造器引數,它無論如何不會是價值類。

8 呼叫父類構造器與良好的面向物件設計

派生類的主構造器必須呼叫父類的構造器,可以是父類的主構造器或次級構造器。

package cn.com.tengen.test.obj

case class Address(street: String, city: String, state: String, zip: String) {
  def this(zip: String) = this("[unknown]", Address.zipToCity(zip), Address.zipToState(zip), zip)
}

object Address {
  def zipToCity(zip: String) = "Jiaxing"
  def zipToState(zip: String) = "CA"
}

case class Person( name: String, age: Option[Int] = None, address: Option[Address] = None)

class Employee( name: String, age: Option[Int] = None,  address: Option[Address] = None, val title: String = "[unknown]",  val manager: Option[Employee] = None) 
  extends Person(name, age, address) {
  override def toString = s"Employee($name, $age, $address, $title, $manager)"
}

object Employee extends App {
  val a1 = new Address("1 Scala Lane", "Anytown", "CA", "98765")
  val a2 = new Address("98765")
  val ceo1 = new Employee("Joe CEO", title = "CEO")
  println(ceo1) //Employee(Joe CEO, None, None, CEO, None)
  val ceo2 = new Employee("Buck Trends1");
  println(ceo2) //Employee(Buck Trends1, None, None, [unknown], None)
}

在Java 中,我們會定義構造方法,並用super 呼叫父類的初始化邏輯。而Scala 中,我們用ChildClass(…) extends ParentClass(…) 的語法隱式地呼叫父類的構造器。

當使用繼承時,建議遵循以下規則。

(1) 一個抽象的基類或trait,只被下一層的具體的類繼承,包括case 類。
(2) 具體類的子類永遠不會再次被繼承,除了兩種情況:
	a. 類中混入了定義於trait中的其他行為。理想情況下, 這些行為應該是正交的, 即不重疊的。
	b. 只用於支援自動化單元測試的類。
(3) 當使用子類繼承似乎是正確的做法時,考慮將行為分離到trait 中,然後在類裡混入這些trait。
(4) 切勿將邏輯狀態跨越父類和子類。

換一種實現方式:

Employee 不再是Person 的一個子類,但它是PersonState 的一個子類,因為它混入了該trait。

package cn.com.tengen.test.obj

case class Address(street: String, city: String, state: String, zip: String)

object Address {
  def apply(zip: String) = new Address("[unknown]", Address.zipToCity(zip), Address.zipToState(zip), zip)
  def zipToCity(zip: String) = "Anytown"
  def zipToState(zip: String) = "CA"
}

trait PersonState {
  val name: String
  val age: Option[Int]
  val address: Option[Address]
}

case class Person(name: String,age: Option[Int] = None,address: Option[Address] = None)
  extends PersonState

trait EmployeeState {
  val title: String
  val manager: Option[Employee]
}
case class Employee(name: String,age: Option[Int] = None, address: Option[Address] = None, title: String = "[unknown]", manager: Option[Employee] = None)
  extends PersonState with EmployeeState

object Person extends App{
  val ceoAddress = Address("1 Scala Lane", "Anytown", "CA", "98765")
  println(ceoAddress)
  val buckAddress = Address("98765")
  println(buckAddress)
  val ceo = Employee( name = "Joe CEO", title = "CEO", age = Some(50), address = Some(ceoAddress), manager = None)
  println(ceo)
  val ceoSpouse = Person("Jane Smith", address = Some(ceoAddress))
  println(ceoSpouse)
  val buck = Employee(  name = "Buck Trends", title = "Zombie Dev", age = Some(20), address = Some(buckAddress), manager = Some(ceo))
  println(buck)
  val buckSpouse = Person("Ann Collins", address = Some(buckAddress))
  println(buckSpouse)
}


輸出結果:
Address(1 Scala Lane,Anytown,CA,98765)
Address([unknown],Anytown,CA,98765)
Employee(Joe CEO,Some(50),Some(Address(1 Scala Lane,Anytown,CA,98765)),CEO,None)
Person(Jane Smith,None,Some(Address(1 Scala Lane,Anytown,CA,98765)))
Employee(Buck Trends,Some(20),Some(Address([unknown],Anytown,CA,98765)),Zombie Dev,Some(Employee(Joe CEO,Some(50),Some(Address(1 Scala Lane,Anytown,CA,98765)),CEO,None)))
Person(Ann Collins,None,Some(Address([unknown],Anytown,CA,98765)))

9 巢狀型別

Scala 允許我們巢狀型別的成名和定義。例如:在物件中定義型別轉義的異常和其他有用的型別,就是很常見的做法。以下是一個數據庫層可能的骨架結構:

package cn.com.tengen.test.obj

object Database {
  case class ResultSet(/*...*/)
  case class Connection(/*...*/)
  case class DatabaseException(message: String, cause: Throwable) extends RuntimeException(message, cause)

  /**
    * 使用sealed 的繼承結構表示狀態;所有允許的值都在這裡定義。當例項實際上不攜帶狀態資訊時,使用case 物件。這些物件表現得像“標誌位”,用來表示狀態。
    */
  sealed trait Status
  case object Disconnected extends Status
  case class Connected(connection: Connection) extends Status
  case class QuerySucceeded(results: ResultSet) extends Status
  case class QueryFailed(e: DatabaseException) extends Status
}
class Database {
  import Database._

  /**
    * ??? 是定義在Predef 中的真實方法。它會丟擲一個異常,用來表示某一方法尚未實現的情況。該方法是最近才引入Scala 庫的。
    */
  def connect(server: String): Status = ???
  def disconnect(): Status = ???
  def query(/*...*/): Status = ???
}

當case 類沒有用任何欄位表示狀態資訊時,考慮使用case 物件。

當方法還正處在開發階段時,??? 方法作為佔位符十分有用。因為這樣可以使程式碼通過編譯。問題是你沒法呼叫那個方法!

為case 物件生成的hashCode 方法僅僅將物件的名稱做了雜湊。

 def main(args: Array[String]): Unit = {
    println(Disconnected.hashCode())
    println("Disconnected".hashCode())
  }

輸出結果:
-1217068453
-1217068453