Cris 的 Scala 筆記整理(七):物件導向

Cris就是我發表於2018-12-18

7. 物件導向(重點)

7.1 Scala 物件導向基礎

[修飾符] class 類名 {

類體

}

  1. scala語法中,類並不宣告為public,所有這些類都具有公有可見性(即預設就是public)

  2. 一個Scala原始檔可以包含多個類

定義一個最簡單的類

object Demo {
  def main(args: Array[String]): Unit = {
    var man = new Man
    man.name = "cris"
    man.age = 12
    println(man.name + "----" + man.age) // cris----12
  }

}

class Man {
  var name = ""
  var age = 0
}
複製程式碼

反編譯對應的 class 檔案

Cris 的 Scala 筆記整理(七):物件導向

屬性

屬性是類的一個組成部分,一般是值資料型別,也可是引用型別

Cris 的 Scala 筆記整理(七):物件導向

  def main(args: Array[String]): Unit = {
    val man = new Man()
    val pc = new PC
    man.pc = pc
    man.pc.brand = "惠普"
    // man.pc().brand()
    println(man.pc.brand) // 惠普
  }

class Man {
  var name = "" // 手動設定初始值,此時可以省略成員屬性的資料型別宣告
  var age = 0
  var pc: PC = _ // _ 表示讓 Scala 自動賦預設值,此時宣告帶上成員屬性的資料型別,否則編譯器無法確定預設值
}

class PC {
  var brand: String = _
}
複製程式碼

Cris 的 Scala 筆記整理(七):物件導向

練習

  1. 針對 for(int i = 10;i>0;i--){System.out.println(i)} 翻譯成 Scala 程式碼

    object Practice {
      def main(args: Array[String]): Unit = {
        for (i <- 0.to(10).reverse) {
          print(i + "\t") // 10  9  8  7  6  5  4  3  2  1  0  
        }
      }
    }
    複製程式碼
  2. 使用過程重寫上面的 Scala 程式碼

    def func(x: Int) {
      for (i <- 0 to x reverse) {
        print(i + "\t")
      }
    }
    複製程式碼
  3. 編寫一個for迴圈,計算字串中所有字母的Unicode程式碼(toLong方法)的乘積。舉例來說,"Hello"中所有字串的乘積為9415087488L

    def cal(str:String): Unit ={
      var result = 1L
      for(x <- str){
        result*=x.toLong
      }
      print(result)
    }
    複製程式碼
  4. 使用 StringOps 的 foreach 方法重寫上面的程式碼

    var r2 = 1L
    // _ 可以理解為字串的每一個字元
    "Hello".foreach(r2 *= _.toLong)
    print(r2)
    複製程式碼
  5. 使用遞迴解決上面求字串每個字元 Unicode 編碼乘積的問題

    def recursive(str: String): Long = {
      if (str.length == 1) str.charAt(0).toLong
      /*drop(n)從索引為 1 開始切片到結尾*/
      else str.take(1).charAt(0).toLong * recursive(str.drop(1))
    }
    複製程式碼
  6. 編寫函式計算 x^n,其中 n 是整數(負數,0,正數),請使用遞迴解決

    def pow(x: Int, n: Int): Double = {
      if (n == 0) 1
      else if (n < 0) {
        1.0 / x * pow(x, n + 1)
      } else {
        x * pow(x, n - 1)
      }
    }
    複製程式碼

物件

val | var 物件名 [:型別] = new 型別()

  1. 如果我們不希望改變物件的引用(即:記憶體地址), 應該宣告為val 性質的,否則宣告為var, scala設計者推薦使用val ,因為一般來說,在程式中,我們只是改變物件屬性的值,而不是改變物件的引用

  2. scala在宣告物件變數時,可以根據建立物件的型別自動推斷,所以型別宣告可以省略,但當型別和後面new 物件型別有繼承關係即多型時,就必須寫

方法

Scala中的方法其實就是函式,只不過一般將物件中的函式稱之為方法

def 方法名(引數列表) [:返回值型別] = {

​ 方法體

}

練習

  1. 巢狀迴圈列印圖形

    def func1(): Unit ={
        for (i <- 1 to 4; j <- 1 to 3) {
            if (j == 3) println("*")
            else print("*\t")
        }
    }
    複製程式碼

    Cris 的 Scala 筆記整理(七):物件導向

  2. 計算矩形的面積

    class Test {
      def area(): Double = {
        (this.width * this.length).formatted("%.2f").toDouble
      }
    
    
      var width: Double = _
      var length: Double = _
    複製程式碼

構造器

java 的構造器回顧

[修飾符] 方法名(引數列表){

構造方法體

}

  1. 在Java中一個類可以定義多個不同的構造方法,構造方法過載

  2. 如果程式設計師沒有定義構造方法,系統會自動給類生成一個預設無參構造方法(也叫預設構造器)

3)一旦定義了自己的構造方法,預設的構造方法就覆蓋了,就不能再使用預設的無參構造方法,除非顯示的定義一下,即: Person(){}

Scala 構造器

和Java一樣,Scala構造物件也需要呼叫構造方法,並且可以有任意多個構造方法。

Scala類的構造器包括: 主構造器 和 輔助構造器

基礎語法

class 類名(形參列表) { // 主構造器

// 類體

def this(形參列表) { // 輔助構造器

}

def this(形參列表) { //輔助構造器可以有多個...

}

}

簡單示例

abstract class Dog {
  var name = ""
  var age = 0
  val color: String

  def this(name: String, age: Int) {
    this()
    this.name = name
    this.age = age
  }

  def eat(): Unit = {
    println("吃狗糧")
  }

  def run()
}
複製程式碼
class Cat(var name: String, val color: String) {
  println("constructor is processing")

  def describe: String = name + "--" + color
}
  def main(args: Array[String]): Unit = {
	var cat = new Cat("tom", "gray")
    println(cat.describe)
    var cat2 = new Cat("jack", "red")
    println(cat2.describe)
  }
複製程式碼

細節

  1. Scala構造器作用是完成對新物件的初始化,構造器沒有返回值。

  2. 主構造器的宣告直接放置於類名之後 [反編譯]

  3. 主構造器會執行類定義中的所有語句,這裡可以體會到Scala的函數語言程式設計和麵向物件程式設計融合在一起,即:構造器也是方法(函式),傳遞引數和使用方法和前面的函式部分內容沒有區別

  4. 如果主構造器無引數,小括號可省略,構建物件時呼叫的構造方法的小括號也可以省略

  5. 輔助構造器名稱為this(這個和Java是不一樣的),多個輔助構造器通過不同引數列表進行區分, 在底層就是java的構造器過載,輔助構造器第一行函式體必須為 this.主構造器

abstract class Dog {
    var name = ""
    var age = 0
    val color: String

    def this(name: String, age: Int) {
        this()
        this.name = name
        this.age = age
    }

    def eat(): Unit = {
        println("吃狗糧")
    }

    def run()
}
複製程式碼

6)) 如果想讓主構造器變成私有的,可以在()之前加上private,這樣使用者只能通過輔助構造器來構造物件了,說明:因為Person3的主構造器是私有,因此就需要使用輔助構造器來建立物件

class Car private(){}
複製程式碼
  1. 輔助構造器的宣告不能和主構造器的宣告一致,會發生錯誤

屬性高階

  1. Scala類的主構造器函式的形參未用任何修飾符修飾,那麼這個引數是區域性變數

  2. 如果引數使用val關鍵字宣告,那麼Scala會將引數作為類的私有的只讀屬性使用

  3. 如果引數使用var關鍵字宣告,那麼那麼Scala會將引數作為類的成員屬性使用,並會提供屬性對應的xxx()[類似getter]/xxx_$eq()[類似setter]方法,即這時的成員屬性是私有的,但是可讀寫

class Counter {

  /*1. 有公開的 getter 和 setter 方法*/
  var count = 0
  /*2. 私有化 getter 和 setter,可以手動提供 setter 和 getter*/
  private var number = 1
  /*3. 只能被訪問getter,無法修改setter,final 修飾的 age 屬性*/
  val age = 12
  /*4. 物件級別的私有*/
  private[this] var length = 12

  def compare(other: Counter): Boolean = other.number > number

  //  def compareLength(other: Counter): Boolean = length > other.length

  def increase(): Unit = {
    number += 1
  }

  /*無參方法可以省略(),{}也可以省略*/
  def current: Int = number
}

def main(args: Array[String]): Unit = {
    var c = new Counter()
    c.count = 3
    println(c.count) // 3

    c.increase()
    println(c.current) // 2

    println(c.age) // 12
}
複製程式碼

如果在主構造器中為屬性設定了預設值,那麼就不必在函式體內再去宣告屬性以及賦值了,大大簡化程式碼的書寫

def main(args: Array[String]): Unit = {
	val dog = new Dog()
    println(dog.name) // cris
    println(dog.age)  // 10
  }
}

class Dog(var name :String= "cris",var age:Int = 10){
    
}
複製程式碼

JavaBean 註解

JavaBeans規範定義了Java的屬性是像getXxx()和setXxx()的方法。許多Java工具(框架)都依賴這個命名習慣。為了Java的互操作性。將Scala欄位加@BeanProperty時,這樣會自動生成規範的 setXxx/getXxx 方法。這時可以使用 物件.setXxx() 和 物件.getXxx() 來呼叫屬性

給某個屬性加入@BeanPropetry註解後,會生成getXXX和setXXX的方法

並且對原來底層自動生成類似xxx(),xxx_$eq()方法,沒有衝突,二者可以共存

物件建立流程分析

請針對以下程式碼簡述物件建立流程

class Bike {
  var brand = ""
  var color = ""

  def this(brand: String, color: String) {
    this
    this.brand = brand
    this.color = color
  }
}

def main(args: Array[String]): Unit = {
   var bike = new Bike("ofo", "黃色")   
}
複製程式碼
  1. 載入類資訊(屬性資訊,方法資訊)

  2. 在堆中,給物件開闢空間

  3. 呼叫主構造器對屬性進行初始化

  4. 使用輔助構造器對屬性進行初始化

  5. 把物件空間的地址,返回給 bike 引用

7.2 物件導向進階

包(難點)

回顧 Java 的包知識

  1. 作用

    1. 區分相同名字的類

    2. 當類很多時,可以很好的管理

    3. 控制訪問範圍

  2. 打包基本語法

    package com.cris;

  3. 打包的本質分析

    實際上就是建立不同的資料夾儲存類檔案

  4. 示例程式碼

    先在不同的包下建立同名的類

    Cris 的 Scala 筆記整理(七):物件導向

    如果想要在一個類中同時使用上面的兩個 Pig,Java 的解決方式如下:

        public static void main(String[] args) {
            Pig pig1 = new Pig();
            cris.package2.Pig pig2 = new cris.package2.Pig();
    //        pig1.getClass() = class cris.package1.Pig
            System.out.println("pig1.getClass() = " + pig1.getClass());
    //        pig2.getClass() = class cris.package2.Pig
            System.out.println("pig2.getClass() = " + pig2.getClass());
        }
    複製程式碼

    再來看看我們的原始碼所在路徑和位元組碼檔案所在路徑,都是一一對應的

    Cris 的 Scala 筆記整理(七):物件導向

    Cris 的 Scala 筆記整理(七):物件導向

    Java 要求原始碼所在路徑和位元組碼檔案所在路徑必須保持一致,如果我們此時去修改原始碼的打包路徑

    Cris 的 Scala 筆記整理(七):物件導向

  5. 基本語法

    import java.awt.* or import java.util.List

  6. 注意事項:java中包名和原始碼所在的系統檔案目錄結構要一致,並且編譯後的位元組碼檔案路徑也和包名保持一致

接著看看 Scala 是如何處理的

我們使用 Scala 重寫上面的 Java 包案例

Cris 的 Scala 筆記整理(七):物件導向

def main(args: Array[String]): Unit = {
  var b1 = new cris.package1.Bird1
  var b2 = new cris.package2.Bird2
  //    class cris.package1.Bird1
  println(b1.getClass)
  //    class cris.package2.Bird2
  println(b2.getClass)
}
複製程式碼

此時我們如果修改了 Bird1 的打包路徑

Cris 的 Scala 筆記整理(七):物件導向

再看看原始碼和位元組碼檔案所在的路徑

Cris 的 Scala 筆記整理(七):物件導向

Cris 的 Scala 筆記整理(七):物件導向

Scala 的包

和Java一樣,Scala中管理專案可以使用包,但Scala中的包的功能更加強大,使用也相對複雜些

  1. 基本語法 package 包名

  2. Scala包的三大作用(和Java一樣)

    1. 區分相同名字的類
    2. 當類很多時,可以很好的管理類
    3. 控制訪問範圍
  3. Scala中包名和原始碼所在的系統檔案目錄結構要可以不一致,但是編譯後的位元組碼檔案路徑包名會保持一致(這個工作由編譯器完成)

  4. 圖示

    Cris 的 Scala 筆記整理(七):物件導向

  5. 命名規範

    只能包含數字、字母、下劃線、小圓點.,但不能用數字開頭, 也不要使用關鍵字

    一般是小寫字母+小圓點一般是 com.公司名.專案名.業務模組名

  6. Scala 自動 import 的包有:java.lang.*,scala,Predef 包

Scala 打包細節(難點)

  • 常用的兩種打包形式

    • 原始碼的路徑和位元組碼檔案路徑保持一致

    • 原始碼的路徑和位元組碼檔案路徑不一致

    • 上面的演示中已經很清楚的展示了 Scala 包的這一特點,我們繼續用下面程式碼演示 Scala 包的巢狀

      Cris 的 Scala 筆記整理(七):物件導向

      我們在 Detail 類檔案中寫入以上非常奇怪的程式碼,編譯執行後再檢視原始碼和位元組碼檔案的位置

      Cris 的 Scala 筆記整理(七):物件導向

      Cris 的 Scala 筆記整理(七):物件導向

      Cris 的 Scala 筆記整理(七):物件導向

      進一步印證了 Scala 中原始檔和位元組碼檔案路徑可以不一致

  • 包也可以像巢狀類那樣巢狀使用(包中有包), 見上面圖示。好處是:程式設計師可以在同一個檔案中,將類(class / object)、trait 建立在不同的包中,非常靈活

  • 作用域原則:可以直接向上訪問。即: Scala中子包中直接訪問父包中的內容, 大括號體現作用域。(提示:Java中子包使用父包的類,需要import)。在子包和父包 類重名時,預設採用就近原則,如果希望指定使用某個類,則帶上包名即可

    示例程式碼

    package com.cris {
    
      class Apple {
    
      }
      package scala {
        
        class Apple {
    
        }
    
        object Boy {
          def main(args: Array[String]): Unit = {
            /*1. Scala 中子包可以直接訪問父包的內容;2. 子包和父包的類重名,預設採取就近原則;3. 可以帶上類的路徑名指定使用該類*/
            val apple = new Apple
            val apple2 = new com.cris.Apple
            //        class com.cris.scala.Apple
            println(apple.getClass)
            //        class com.cris.Apple
            println(apple2.getClass)
          }
        }
      }
    }
    複製程式碼
  • 父包要訪問子包的內容時,需要import對應的類

    package com.cris {
    	
      import com.cris.scala.Apple
    
      object Apple{
        def main(args: Array[String]): Unit = {
            // 推薦只在使用的時候再引用,控制作用域
    	  import com.cris.scala.Apple
          val apple = new Apple()
    //      class com.cris.scala.Apple
          println(apple.getClass)
        }
      }
    
      package scala {
    
        class Apple {
    
        }
      }
    }-
    複製程式碼
  • 可以在同一個.scala檔案中,宣告多個並列的package(建議巢狀的pakage不要超過3層)

    Cris 的 Scala 筆記整理(七):物件導向

包物件

基本介紹:包可以包含類、物件和特質trait,但不能包含函式或變數的定義。這是Java虛擬機器的侷限。為了彌補這一點不足,scala提供了包物件的概念來解決這個問

參見如下程式碼

package com.cris {

  // 不能直接在 package 中定義函式和變數
  //  var name = "cris"

  /**
    * 包物件的名字需要和包名一致
    * package object emp 會在 com.cris.emp 包下生成&emsp;package.class 和&emsp;package$.class
    */
  package object emp {
    def eat(): Unit = {
      println("eat")
    }

    val salary = 1000.0
  }

  package emp {

    object test {
      def main(args: Array[String]): Unit = {
        eat() // eat=》等價於使用了&emsp;package$.class 中的&emsp;MODULE$.eat()
        println(salary) // 1000.0=>&emsp;等價於使用了&emsp;package$.class 中的 MODULE$.salary()
      }
    }
  }
}
複製程式碼

Cris 的 Scala 筆記整理(七):物件導向

使用反編譯工具開啟瞧瞧

Cris 的 Scala 筆記整理(七):物件導向

具體的執行流程第二章節已經解釋過,這裡不再贅述

注意事項:

  1. 每個包都可以有一個包物件,但是需要在父包中定義它
  2. 包物件名稱需要和包名一致,一般用來對包(裡面的類)的功能做補充

包的可見性

在Java中,訪問許可權分為: public,private,protected和預設。在Scala中,你可以通過類似的修飾符達到同樣的效果。但是使用上有區別

  1. 當屬性訪問許可權為預設時,從底層看屬性是private的,但是因為提供了xxx_$eq()[類似setter]/xxx()[類似getter] 方法,因此從使用效果看是任何地方都可以訪問)

  2. 當方法訪問許可權為預設時,預設為public訪問許可權

  3. private為私有許可權,只在類的內部和伴生物件中可用

示例:

Cris 的 Scala 筆記整理(七):物件導向

Cris 的 Scala 筆記整理(七):物件導向

  1. protected為受保護許可權,scala中受保護許可權比Java中更嚴格,只能子類訪問,同包無法訪問

  2. 在scala中沒有public關鍵字,即不能用public顯式的修飾屬性和方法。

包訪問許可權(表示屬性有了限制。同時增加了包的訪問許可權),這點和Java不一樣,體現出Scala包使用的靈活性

Cris 的 Scala 筆記整理(七):物件導向

包的引入

細節說明

  1. 在Scala中,import語句可以出現在任何地方,並不僅限於檔案頂部,import語句的作用一直延伸到包含該語句的塊末尾。這種語法的好處是:在需要時在引入包,縮小import 包的作用範圍,提高效率

    示例如下:

    Cris 的 Scala 筆記整理(七):物件導向

  2. Java中如果想要匯入包中所有的類,可以通過萬用字元*,Scala中採用下 _

  3. 如果不想要某個包中全部的類,而是其中的幾個類,可以採用選取器(大括號)

    Cris 的 Scala 筆記整理(七):物件導向

  4. 如果引入的多個包中含有相同的類,那麼可以將不需要的類進行重新命名進行區分,這個就是重新命名

    Cris 的 Scala 筆記整理(七):物件導向

  5. 或者使用 import java.util.{HashMap => _ } 對衝突的包進行隱藏

練習

  1. 編寫一個Time類,加入只讀屬性hours和minutes,和一個檢查某一時刻是否早於另一時刻的方法before(other:Time):Boolean。Time物件應該以new Time(hrs,min)方式構建

    object Practice {
      def main(args: Array[String]): Unit = {
        val time1 = new Time(4, 12)
        val result = time1.before(new Time(4, 14))
        println(result)
      }
    }
    
    class Time(val hour: Int, val minute: Int) {
      
      def before(other: Time) = {
        if (this.hour < other.hour) true
        else if (this.hour > other.hour) false
        else if (this.hour == other.hour) {
          if (this.minute < other.minute) true
          else if (this.minute > other.minute) false
          else false
        }
      }
    }
    複製程式碼
  2. 建立一個Student類,加入可讀寫的JavaBeans屬性name(型別為String)和id(型別為Long)。有哪些方法被生產?(用javap檢視。)你可以在Scala中呼叫JavaBeans的getter和setter方法嗎?

    object Practice {
      def main(args: Array[String]): Unit = {
        var s = new Student
        println(s.getName)
        println(s.age)
      }
    }
    
    class Student {
      @BeanProperty var name = "好學生"
      @BeanProperty var age = 0
    
    }
    複製程式碼

    Cris 的 Scala 筆記整理(七):物件導向

  3. 編寫一段程式,將Java雜湊對映中的所有元素拷貝到Scala雜湊對映。用引入語句重新命名這兩個類

    object Ex extends App {
    
      import java.util.{HashMap => JavaHashMap}
      import scala.collection.mutable.{HashMap => ScalaHashMap}
    
      var map1 = new JavaHashMap[Int, String]()
      map1.put(1, "cris")
      map1.put(2, "james")
      map1.put(3, "simida")
    
    
      var map2 = new ScalaHashMap[Int, String]()
      for (key <- map1.keySet().toArray()) { // key 的資料型別是 AnyRef
        // asInstanceOf 強制資料型別轉換
        map2 += (key.asInstanceOf[Int] -> map1.get(key))
      }
      println(map2.mkString("||")) // 2 -> james||1 -> cris||3 -> simida
    
    }
    複製程式碼

抽象

我們在前面去定義一個類時候,實際上就是把一類事物的共有的屬性和行為提取出來,形成一個物理模型(模板)。這種研究問題的方法稱為抽象

示例程式碼

object Demo extends App {

  var account = new Account("招行:888888", 200, "123456")
  account.query("123456")

  account.save("123456", 100)
  account.query("123456")

  account.withdraw("123456", 250)
  account.query("123456")

}

class Account(val no: String, var balance: Double, var pwd: String) {
  def query(pwd: String): Unit = {
    if (pwd != this.pwd) {
      println("密碼錯誤!")
    } else {
      println(s"卡號:${this.no},餘額還有:${this.balance}")
    }
  }

  def save(pwd: String, money: Double): Unit = {
    if (pwd != this.pwd) {
      println("密碼錯誤")
    } else {
      this.balance += money
      println(s"卡號:${this.no},存入:${money},餘額為:${this.balance}")
    }
  }

  def withdraw(pwd: String, money: Double): Unit = {
    if (pwd != this.pwd) {
      println("密碼錯誤")
    } else if (money > this.balance) {
      println("餘額不足")
    } else {
      this.balance -= money
      println(s"卡號:${this.no},取出:${money},餘額為:${this.balance}")
    }
  }
}
複製程式碼

Cris 的 Scala 筆記整理(七):物件導向

相關文章