7. 物件導向(重點)
7.1 Scala 物件導向基礎
類
[修飾符] class 類名 {
類體
}
-
scala語法中,類並不宣告為public,所有這些類都具有公有可見性(即預設就是public)
-
一個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 檔案
屬性
屬性是類的一個組成部分,一般是值資料型別,也可是引用型別
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 = _
}
複製程式碼
練習
-
針對 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 } } } 複製程式碼
-
使用過程重寫上面的 Scala 程式碼
def func(x: Int) { for (i <- 0 to x reverse) { print(i + "\t") } } 複製程式碼
-
編寫一個for迴圈,計算字串中所有字母的Unicode程式碼(toLong方法)的乘積。舉例來說,"Hello"中所有字串的乘積為9415087488L
def cal(str:String): Unit ={ var result = 1L for(x <- str){ result*=x.toLong } print(result) } 複製程式碼
-
使用 StringOps 的 foreach 方法重寫上面的程式碼
var r2 = 1L // _ 可以理解為字串的每一個字元 "Hello".foreach(r2 *= _.toLong) print(r2) 複製程式碼
-
使用遞迴解決上面求字串每個字元 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)) } 複製程式碼
-
編寫函式計算 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 型別()
-
如果我們不希望改變物件的引用(即:記憶體地址), 應該宣告為val 性質的,否則宣告為var, scala設計者推薦使用val ,因為一般來說,在程式中,我們只是改變物件屬性的值,而不是改變物件的引用
-
scala在宣告物件變數時,可以根據建立物件的型別自動推斷,所以型別宣告可以省略,但當型別和後面new 物件型別有繼承關係即多型時,就必須寫了
方法
Scala中的方法其實就是函式,只不過一般將物件中的函式稱之為方法
def 方法名(引數列表) [:返回值型別] = {
方法體
}
練習
-
巢狀迴圈列印圖形
def func1(): Unit ={ for (i <- 1 to 4; j <- 1 to 3) { if (j == 3) println("*") else print("*\t") } } 複製程式碼
-
計算矩形的面積
class Test { def area(): Double = { (this.width * this.length).formatted("%.2f").toDouble } var width: Double = _ var length: Double = _ 複製程式碼
構造器
java 的構造器回顧
[修飾符] 方法名(引數列表){
構造方法體
}
-
在Java中一個類可以定義多個不同的構造方法,構造方法過載
-
如果程式設計師沒有定義構造方法,系統會自動給類生成一個預設無參構造方法(也叫預設構造器)
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)
}
複製程式碼
細節
-
Scala構造器作用是完成對新物件的初始化,構造器沒有返回值。
-
主構造器的宣告直接放置於類名之後 [反編譯]
-
主構造器會執行類定義中的所有語句,這裡可以體會到Scala的函數語言程式設計和麵向物件程式設計融合在一起,即:構造器也是方法(函式),傳遞引數和使用方法和前面的函式部分內容沒有區別
-
如果主構造器無引數,小括號可省略,構建物件時呼叫的構造方法的小括號也可以省略
-
輔助構造器名稱為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(){}
複製程式碼
- 輔助構造器的宣告不能和主構造器的宣告一致,會發生錯誤
屬性高階
-
Scala類的主構造器函式的形參未用任何修飾符修飾,那麼這個引數是區域性變數
-
如果引數使用val關鍵字宣告,那麼Scala會將引數作為類的私有的只讀屬性使用
-
如果引數使用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", "黃色")
}
複製程式碼
-
載入類資訊(屬性資訊,方法資訊)
-
在堆中,給物件開闢空間
-
呼叫主構造器對屬性進行初始化
-
使用輔助構造器對屬性進行初始化
-
把物件空間的地址,返回給 bike 引用
7.2 物件導向進階
包(難點)
回顧 Java 的包知識
-
作用
-
區分相同名字的類
-
當類很多時,可以很好的管理類
-
控制訪問範圍
-
-
打包基本語法
package com.cris;
-
打包的本質分析
實際上就是建立不同的資料夾來儲存類檔案
-
示例程式碼
先在不同的包下建立同名的類
如果想要在一個類中同時使用上面的兩個 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()); } 複製程式碼
再來看看我們的原始碼所在路徑和位元組碼檔案所在路徑,都是一一對應的
Java 要求原始碼所在路徑和位元組碼檔案所在路徑必須保持一致,如果我們此時去修改原始碼的打包路徑
-
基本語法
import java.awt.* or import java.util.List
-
注意事項:java中包名和原始碼所在的系統檔案目錄結構要一致,並且編譯後的位元組碼檔案路徑也和包名保持一致
接著看看 Scala 是如何處理的
我們使用 Scala 重寫上面的 Java 包案例
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 的打包路徑
再看看原始碼和位元組碼檔案所在的路徑
Scala 的包
和Java一樣,Scala中管理專案可以使用包,但Scala中的包的功能更加強大,使用也相對複雜些
-
基本語法 package 包名
-
Scala包的三大作用(和Java一樣)
- 區分相同名字的類
- 當類很多時,可以很好的管理類
- 控制訪問範圍
-
Scala中包名和原始碼所在的系統檔案目錄結構要可以不一致,但是編譯後的位元組碼檔案路徑和包名會保持一致(這個工作由編譯器完成)
-
圖示
-
命名規範
只能包含數字、字母、下劃線、小圓點.,但不能用數字開頭, 也不要使用關鍵字
一般是小寫字母+小圓點一般是
com.公司名.專案名.業務模組名
-
Scala 自動 import 的包有:java.lang.*,scala,Predef 包
Scala 打包細節(難點)
-
常用的兩種打包形式
-
原始碼的路徑和位元組碼檔案路徑保持一致
-
原始碼的路徑和位元組碼檔案路徑不一致
-
上面的演示中已經很清楚的展示了 Scala 包的這一特點,我們繼續用下面程式碼演示 Scala 包的巢狀
我們在 Detail 類檔案中寫入以上非常奇怪的程式碼,編譯執行後再檢視原始碼和位元組碼檔案的位置
進一步印證了 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層)
包物件
基本介紹:包可以包含類、物件和特質trait,但不能包含函式或變數的定義。這是Java虛擬機器的侷限。為了彌補這一點不足,scala提供了包物件的概念來解決這個問題
參見如下程式碼
package com.cris {
// 不能直接在 package 中定義函式和變數
// var name = "cris"
/**
* 包物件的名字需要和包名一致
* package object emp 會在 com.cris.emp 包下生成 package.class 和 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=》等價於使用了 package$.class 中的 MODULE$.eat()
println(salary) // 1000.0=> 等價於使用了 package$.class 中的 MODULE$.salary()
}
}
}
}
複製程式碼
使用反編譯工具開啟瞧瞧
具體的執行流程第二章節已經解釋過,這裡不再贅述
注意事項:
- 每個包都可以有一個包物件,但是需要在父包中定義它
- 包物件名稱需要和包名一致,一般用來對包(裡面的類)的功能做補充
包的可見性
在Java中,訪問許可權分為: public,private,protected和預設。在Scala中,你可以通過類似的修飾符達到同樣的效果。但是使用上有區別
-
當屬性訪問許可權為預設時,從底層看屬性是private的,但是因為提供了xxx_$eq()[類似setter]/xxx()[類似getter] 方法,因此從使用效果看是任何地方都可以訪問)
-
當方法訪問許可權為預設時,預設為public訪問許可權
-
private為私有許可權,只在類的內部和伴生物件中可用
示例:
-
protected為受保護許可權,scala中受保護許可權比Java中更嚴格,只能子類訪問,同包無法訪問
-
在scala中沒有public關鍵字,即不能用public顯式的修飾屬性和方法。
包訪問許可權(表示屬性有了限制。同時增加了包的訪問許可權),這點和Java不一樣,體現出Scala包使用的靈活性
包的引入
細節說明
-
在Scala中,import語句可以出現在任何地方,並不僅限於檔案頂部,import語句的作用一直延伸到包含該語句的塊末尾。這種語法的好處是:在需要時在引入包,縮小import 包的作用範圍,提高效率
示例如下:
-
Java中如果想要匯入包中所有的類,可以通過萬用字元*,Scala中採用下 _
-
如果不想要某個包中全部的類,而是其中的幾個類,可以採用選取器(大括號)
-
如果引入的多個包中含有相同的類,那麼可以將不需要的類進行重新命名進行區分,這個就是重新命名
-
或者使用 import java.util.{HashMap => _ } 對衝突的包進行隱藏
練習
-
編寫一個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 } } } 複製程式碼
-
建立一個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 } 複製程式碼
-
編寫一段程式,將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}")
}
}
}
複製程式碼