從Kotlin的類開始說起

依然範特稀西發表於2018-09-04

歡迎來到kotlin的世界,Kotlin 是一個用於現代多平臺應用的靜態程式語言,它可以編譯成Java位元組碼,在JVM平臺上執行,並且可以完全相容Java。它有很多優點,如:如空指標檢查、高階函式、函式擴充套件等等。2017Google IO大會上,指定Kotlin作為Android開發的官方語言。因此,如果你是一個Android開發者,該學習使用kotlin來進行開發了。

如何開始學習Kotlin呢?在物件導向程式設計中,我們說萬物皆物件,任何事物都可以進行抽象和封裝成一個物件來表達它所具有的屬性和特徵。Kotlin作為一種現代物件導向程式語言,因此我們就從類和物件開始來認識它,本篇本章就來講講Kotlin中的所有類。

image

1 . Kotlin中的類

1.1、Java中的類

在認識Kotlin的類之前,我們來先看看我們熟悉的Java類,一段Java程式碼如下:

class Person {
    private String name;
    private int age;

    public Person(String name,int age){
        this.name = name;
        this.age = age;
    }
    
    /**
     * 方法
     */ 
    public void walk(){
        System.out.print("person walk...");
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}

複製程式碼

Java類特徵如下:

  • class關鍵字宣告一個類,形式如:class 類名 {}
  • 類可以有一個或者多個建構函式,如果沒有顯示的建構函式,會預設有一個無參建構函式
  • 類中可以宣告屬性和方法,私有屬性用相應的getXXX()setXXX()方法

一起來看一下,和上面功能功能一樣的kotlin類:

class Person(var name:String,var age:Int){
    /**
     *  函式
     */
    fun walk():Unit{
        println("Person walk...")
    }
}

複製程式碼

1.2 . Kotlin中的類

Kotlin的類的宣告形式為:

  class 類名 [可見性修飾符] [註解] [constructor] (Params){
     ...
  }  
複製程式碼

其中,{}中的 的內容成為類體,類名與類體之間的內容稱為類頭,在kotlin中,類頭和類體是可以省略的,[]中的類容也是可選的。比如一個完整的類宣告如:

class Person private @Inject constructor(name: String) { …… }
複製程式碼

如果沒有可見性修飾符和註解,類頭可以省去,那麼可以簡寫成如下:

class Person(name: String) { …… }
複製程式碼

如果**建構函式(kotlin的建構函式將在下文講解)**沒有引數,可以寫成如下:

class Person { …… }
複製程式碼

如果類體裡面也沒有類容的話,類體也可以省略,如下:

 class Person
複製程式碼

2 . 建構函式

上面提到了建構函式,熟悉Java的同學都知道,Java也有建構函式,Java中的建構函式有如下特點:

  • 一個Java類可以有多個建構函式,建構函式之間是過載的
  • 可以不給Java類顯示宣告建構函式,但是會預設生成一個無參的建構函式
  • 如果顯示的生成了建構函式,則在new物件的時候,不能使用預設的無參建構函式,除非顯示的生成一個無參建構函式。

如:多個過載的建構函式:

public class Person {
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public Person(String name) {
        this.name = name;
    }
}

// 生成物件時
Person person1 = new Person("Paul",30);
Person person2 = new Person("jake");
Person person3 = new Person();//編譯錯誤,沒有無參建構函式
複製程式碼

也可以生明建構函式,但是它會有一個預設的無參建構函式:

public class Person {
    private String name;
    private int age;
}

// 生成物件時
Person person = new Person();//使用預設無參建構函式
複製程式碼

回到Kotlin ,在Kotlin中,一個類可以有一個主建構函式以及一個或多個次建構函式。主建構函式是類頭的一部分:它跟在類名(與可選的型別引數)後。與Java稍有不同,Kotlin有主建構函式和次建構函式之分。

2.1 主建構函式

一個帶有主建構函式的類宣告如下:

class Cat constructor(name: String){
    ...
}
複製程式碼

如果主建構函式沒有任何註解或者可見性修飾符,可以省略這個constructor關鍵字。

class Cat(name: String){
    ...
}
複製程式碼

注意,Kotlin主建構函式是不能包含任何程式碼的,但是有時候我們又需要在建構函式中做一些初始化的操作,這咋辦呢?Kotlin 引入了初始化程式碼塊,用 關鍵字init宣告,需要在主建構函式中初始化的操作可以放到初始化程式碼塊中,如:

class Cat(name: String){
    //初始化程式碼塊
    init {
        // 在這裡面做一些需要在主建構函式中做的初始化操作
        println("第一個初始化程式碼塊,name:$name")
    }
}
// 使用如下:

fun main(args: Array<String>) {
  var  cat: Cat = Cat("喵喵")
}
複製程式碼

執行結果如下:

/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/bin/java
 
第一個初始化程式碼塊,name:喵喵
Process finished with exit code 0
複製程式碼

可以看到,生成一個Cat物件的時候,執行了初始化程式碼塊。有2點值得注意的是:

  • 1,初始化程式碼塊和類體都可以訪問主建構函式的引數,如上面的例子,可以訪問name引數
  • 2,類體中可以有多個初始化程式碼塊,它們的執行順序與在類中宣告的順序一樣

多個初始化程式碼塊例子:

class Cat(name: String){
    //類體中也可以訪問建構函式的引數
    val catName:String = "catName:$name"
    //初始化程式碼塊
    init {
        // 在這裡面做一些需要在主建構函式中做的初始化操作
        println("第一個初始化程式碼塊,name:$name")
    }

    init {
        println("第二個初始化程式碼塊,name:$name")
    }

    init {
        println("第三個初始化程式碼塊,name:$name")
    }
}

// 使用如下:
fun main(args: Array<String>) {
  var  cat: Cat = Cat("喵喵")
  println(cat.catName)//列印屬性
}
複製程式碼

執行結果如下:

/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/bin/java

第一個初始化程式碼塊,name:喵喵
第二個初始化程式碼塊,name:喵喵
第三個初始化程式碼塊,name:喵喵
catName:喵喵

Process finished with exit code 0
複製程式碼

上面的程式碼中,我們在類體中宣告瞭一個屬性catName,在Kotlin中宣告屬性其實有更簡單的方法,那就是在主建構函式中宣告類屬性

方式一:

class Person(var name:String ,var gender: Int)
複製程式碼

方式二:

class Person{
    var name:String = ""
    var gender:Int = 0
    //這是次建構函式
    constructor(name: String, gender: Int) {
        this.name = name
        this.gender = gender
    }
}
複製程式碼

上面2種方式宣告屬性是等價的,宣告瞭2個類屬性namegender,可以看出,通過主建構函式宣告屬性簡潔了很多。

在Kotlin中,函式的引數是可以設定預設值的,如果呼叫的時候不傳對應引數,就使用預設值,主建構函式宣告類屬性也一樣,也可以設定預設值。 程式碼如下:

class Person(var name: String= "" ,var gender: Int= 0)
複製程式碼
2.2 次建構函式

Kotlin 中,類也可以有次建構函式,次建構函式在類體中用關鍵字constructor宣告,程式碼如下:

class Person {
    var name: String = ""
    var gender: Int = 0
    //次建構函式
    constructor(name: String, gender: Int){
        this.name = name
        this.gender = gender
    }
}
複製程式碼

如果類有一個主建構函式,每個次建構函式需要委託給主建構函式, 可以直接委託或者通過別的次建構函式間接委託。委託到同一個類的另一個建構函式用 this 關鍵字即可:

class Person(name: String){
    var name: String = name // 主建構函式引數賦值
    var gender: Int = 0

    constructor(name: String, gender: Int) : this(name) {
        this.name = name
        this.gender = gender
    }
}
複製程式碼

請注意,初始化塊中的程式碼實際上會成為主建構函式的一部分。委託給主建構函式會作為次建構函式的第一條語句,因此所有初始化塊中的程式碼都會在次建構函式體之前執行。即使該類沒有主建構函式,這種委託仍會隱式發生,並且仍會執行初始化塊:

class Person(name: String){
    var name: String = name
    var gender: Int = 0
    
    init {
        println("這是初始化程式碼塊...")
    }

    constructor(name: String, gender: Int) : this(name) {
        println("這是次建構函式...")
        this.name = name
        this.gender = gender
    }
}

// 執行程式
fun main(args: Array<String>) {
    var person = Person("Paul",30)
}
複製程式碼

列印結果如下:

/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/bin/java

這是初始化程式碼塊,name:Paul
這是次建構函式,name:Paul,gender:30

Process finished with exit code 0
複製程式碼

從結果看,先執行初始化塊,再執行次建構函式。

在Java中,如果沒有宣告任何建構函式,會有一個預設的無參建構函式,這有利於通過這個無參建構函式建立類的物件,但是有些情況,比如單例模式,不希望外部構造物件,我們只需私有化一個無參建構函式就行,Java程式碼如下:

 class Singleton{
        // 私有化建構函式,外部不能直接建立物件
        private Singleton(){}
        
        public static Singleton getInstance(){
            return new Singleton();
        }
    }
複製程式碼

在Kotlin中也類似,如果一個非抽象類沒有宣告任何(主或次)建構函式,它會有一個生成的不帶引數的主建構函式。建構函式的可見性是 public。如果你不希望你的類有一個公有建構函式,你需要宣告一個帶有非預設可見性的空的主建構函式:

class DontCreateMe private constructor () { ... }
複製程式碼

kotlin建構函式小結:

1, 可以有一個主建構函式和多個次建構函式 2,可以只有主建構函式或者只有次建構函式 3,主、次建構函式同時存在的時候,次建構函式必須直接或者間接地委託到主建構函式 4,沒有宣告主建構函式或者次建構函式時,會有一個預設的無引數主建構函式,方便建立物件,這與Java一樣 5,如果不希望類有公有建構函式,那麼請私有化一個無引數主建構函式

3 . 抽象類

和Java一樣,在kotlin中,抽象類用關鍵字abstract修飾,抽象類的成員可以在本類中提供實現,也可以不實現而交給子類去實現,不實現的成員必須用關鍵字abstract宣告:

abstract class AbsBase{
    abstract fun  method()
}
複製程式碼

在kotlin中,被繼承的類需要用關鍵字open宣告,表明該類可以被繼承,但是抽象類或者抽象函式是不用 open 標註的,因為這不言而喻。但是如果子類要實現抽象類的非抽象函式,需要在抽象類中將其宣告為open

abstract class AbsBase{
    abstract fun  method()
    // 如果子類要實現需宣告為抽象 
    open fun method1(){
        println("非抽象方法如果要類子類實現,需要宣告為open")
    }
}

class Child : AbsBase() {

    override fun method() {

    }

    override fun method1() {
        super.method1()
        println("子類實現")
    }

}
複製程式碼

另外,抽象成員可以覆蓋一個非抽象成員:

abstract class AbsBase{ 
   open fun method1(){
        println("非抽象方法如果要類子類實現,需要宣告為open")
    }
}

abstract class AbsChild :AbsBase(){
    // 將父類的非抽象方法覆蓋為一個抽象方法
    abstract override fun method1()
}
複製程式碼

抽象類小結: 跟Java 的抽象類幾乎一樣,熟悉Java的同學很容易理解。

4 . 資料類

在Java中,我們會經常建立一些儲存資料的類,xxxModule或者xxxEntry ,主要用在網路請求中,儲存api介面返回的資料,如一個 Java 的User類:

public class User {
    private String name;
    private int gender;
    private String avatar;
    private int age;
    

    public User(String name, int gender, String avatar, int age) {
        this.name = name;
        this.gender = gender;
        this.avatar = avatar;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getGender() {
        return gender;
    }

    public void setGender(int gender) {
        this.gender = gender;
    }

    public String getAvatar() {
        return avatar;
    }

    public void setAvatar(String avatar) {
        this.avatar = avatar;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}
複製程式碼

一個複雜點的實體類,動不動就幾十幾百行程式碼。臃腫而且麻煩。

在Kotlin 中,引入了一種特殊的來來解決這個問題,叫做資料類,用關鍵字data標記,kotlin資料類程式碼如下:

data class User(var  name:String,var age:Int,var gender: Int,var avatar: String)
複製程式碼

Java 中幾十行的資料類,在kotlin中,一行就搞定。為什麼kotlin 能這麼簡單,那是因為編譯器為我們做了許多事,編譯器會自動的從主建構函式中根據所有宣告的屬性提取以下函式:

  • equals()/hashCode() ;
  • toString() 格式是 "User(name=John, age=42)";
  • componentN() 函式 按宣告順序對應於所有屬性;
  • copy() 函式

也就是說編譯器自動為我們的資料類生成了以上個函式,前面幾個熟悉Java的同學知道,Java 中也有,再此不表。componentNcopy 函式使資料類有了2個特性:

  • 1 . 解構
  • 2、資料類複製
4.1 . 解構

解構是什麼意思呢?就是把一個物件拆解成對應的多個變數,比如上面的User類 ,我們可以把它的4 個屬性拆解出來:

fun main(args: Array<String>) {
    // 建立一個User物件
    var user = User("Paul",30,1,"https:qiniu.com/w200/h200.png")
    // 解構
    val(name,age,gender,avatar) = user
    // 列印
    println("name:$name,age:$age,gender:$gender,avatar:$avatar")
}
複製程式碼

列印結果:

name:Paul,age:30,gender:1,avatar:https:qiniu.com/w200/h200.png

Process finished with exit code 0
複製程式碼

為什麼資料類可以解構呢?是因為編譯器自動幫資料類,生成了componentN函式, 其中N代表有N個 component函式,這取決於資料類主建構函式宣告屬性的個數,如上面的例子,有4個屬性,那麼就有4個component函式: component1() component2() component3() component4()

4個函式對應4個component函式,按在主建構函式宣告屬性的順序對應。 上面的解構會被編譯成:

val name = user.component1()
val age = user.component2()
val gender = user.component3()
val avatar = user.component4()
複製程式碼

Q: 普通的類可以解構嗎? A: 可以,為其宣告component函式

4.2 複製copy

在很多情況下,我們需要複製一個物件改變它的一些屬性,但其餘部分保持不變。 copy() 函式就是為此而生成。對於上文的 User 類,其實現會類似下面這樣:

fun copy(name: String = this.name, age: Int = this.age,gender: Int = this.gender,avatar: Int = this.avatar) = User(name, age,gender,avatar)
複製程式碼

比如我們複製了一個User類:

   var user = User("Paul",30,1,"https:qiniu.com/w200/h200.png")

    // 想再建立一個物件只改變年齡
    var user2 = user.copy(age = 31)
    // 改變名字和年齡,其他不變
    var user3 = user.copy(name = "Dw",age = 33)
    
    println(user.toString())
    println(user2.toString())
    println(user3.toString())
複製程式碼

執行結果如下:

1535613834079

注意:資料類也可以在類體中宣告屬性,但是在類體中宣告的屬性不會出現在那些自動生成的函式中,如:

data class User(val name: String) {
    var age: Int = 0
}
複製程式碼

因為主建構函式只有name,因此在 toString()、 equals()、 hashCode() 以及 copy() 的實現中只會用到 name 屬性,只有一個component1函式,對應name。如果你建立2個物件,名字相同,年齡不同,但是會被視為相等。user1 == user2

資料類小結 資料類需滿足以下要求: 1、主建構函式需要至少有一個引數; 2、主建構函式的所有引數需要標記為 val 或 var; 3、資料類不能是抽象、開放、密封或者內部的;

5 . 列舉類

kotlin 中的列舉類與Java 中的列舉類差不多,簡單的說一下:

1、列舉用關鍵字enum宣告,與Java不同,緊跟後面是class (Java宣告列舉沒有class關鍵字)列舉類的宣告形式如下:

  enum class 類名{
    常量1,
    常量2,
    ...
  }
複製程式碼

如:

enum class Direction{
    WEST,
    EAST,
    NORTH,
    SOUTH;
}
複製程式碼

2、列舉類預設有2個屬性ordinalname:

  • ordinal 屬性:列舉常量的順序,從0開始
  • name屬性: 列舉常量的名字 以上面的列舉類Direction為例:
 Direction.WEST.ordinal // 0
 Direction.WEST.name // WEST
複製程式碼

3、列舉類預設又2個方法:values()valueOf()

  • values : 獲取所有列舉常量
  • valueOf() : 獲取對應列舉常量
   // 遍歷
    Direction.values().forEach {
        println("value:${it.ordinal}")
    }
    // 獲取"EAST"對應列舉常量,如果列舉類中沒這個常量會拋異常
    val direction = Direction.valueOf("EAST")
複製程式碼

4、列舉常量可以有建構函式和自有屬性、方法,自定義方法需放在;後,每一個列舉常量都是一個例項,呼叫建構函式初始化。

enum class Season(var enumName: String,var range: String){
    Spring("春季","1-3"),
    Summer("夏季","4-6"),
    Fall("秋季","7-9"),
    Winter("冬季","10-12");
    
    fun printSeason(){
        print("name:$enumName,range:$range")
    }
}
複製程式碼

6. 密閉類

密閉類定義如下:密閉類用來表示受限的類繼承結構:當一個值為有限集中的型別、而不能有任何其他型別時。在某種意義上,他們是列舉類的擴充套件:列舉型別的值集合也是受限的,但每個列舉常量只存在一個例項,而密閉類的一個子類可以有可包含狀態的多個例項。

這麼長一串定義,看得一臉懵逼,沒關係,稍候解釋。先來看一下如何宣告一個密閉類

密閉類用 sealed 修飾符 ,密閉類的字類必須與密閉類在同一檔案中(子類也可以巢狀在密閉類的內部)

sealed class Expr
data class Const(val number: Double) : Expr()
data class Sum(val e1: Expr, val e2: Expr) : Expr()
object NotANumber : Expr()
複製程式碼

子類在內部:

sealed class Expr{
    data class Const(val number: Double) : Expr()
    data class Sum(val e1: Expr, val e2: Expr) : Expr()
    object NotANumber : Expr()
}
複製程式碼

有幾點需要注意:

  • 1、一個密閉類是自身抽象的,它不能直接例項化並可以有抽象(abstract)成員

  • 2、密閉類不允許有非-private 建構函式(其建構函式預設為 private)

  • 3、擴充套件密閉類子類的類(間接繼承者)可以放在任何位置,而無需在同一個檔案中。

密閉類算是列舉類的擴充套件,用法和列舉類相似,經常配合when表示式使用,使用例子如下:

fun eval(expr: Expr): Double = when(expr) {
    is Expr.Const -> expr.number
    is Expr.Sum -> eval(expr.e1) + eval(expr.e2)
    Expr.NotANumber -> Double.NaN
    // 不再需要 `else` 子句,因為我們已經覆蓋了所有的情況
}

fun main(args: Array<String>) {
    val const = eval(Expr.Const(12.0))
    val sum = eval(Expr.Sum(Expr.Const(10.0),Expr.Const(12.0)))

    println("const:$const")
    println("sum:$sum")
}   

// 執行結果:
const:12.0
sum:22.0 
複製程式碼

作為初學者,密閉類是Kotlin 中比較難以理解的一個類,其實看完上面的例子還是很難理解它到底能幹嘛,感覺它做的事兒,列舉類也能做到,前面說它算是對列舉類的擴充套件,那麼他就應該能做到列舉類做不到的事。網上看到一篇部落格用View 顯示和隱藏來舉例,頓時茅舍頓開。如下:

場景: 假如在 Android 中我們有一個 view,我們現在想通過 when 語句設定針對 view 進行兩種操作:顯示和隱藏,那麼就可以這樣做:

sealed class UiOp {
    object Show: UiOp()
    object Hide: UiOp()
} 
//定義了一個操作View的方法
fun viewOperator(view: View, op: UiOp) = when (op) {
    UiOp.Show -> view.visibility = View.VISIBLE 
    UiOp.Hide -> view.visibility = View.GONE
}
複製程式碼

以上功能其實完全可以用列舉實現,但是如果我們現在想加兩個操作:水平平移和縱向平移,並且還要攜帶一些資料,比如平移了多少距離,平移過程的動畫型別等資料,用列舉顯然就不太好辦了,這時密封類的優勢就可以發揮了,現在密閉類中新增2個操作:

sealed class UiOp {
    object Show: UiOp()
    object Hide: UiOp()
    class TranslateX(val px: Float): UiOp() // 水平移動
    class TranslateY(val px: Float): UiOp()//垂直移動
}
複製程式碼

接著在when表示式新增兩個移動的case

fun execute(view: View, op: UiOp) = when (op) {
    UiOp.Show -> view.visibility = View.VISIBLE
    UiOp.Hide -> view.visibility = View.GONE
    is UiOp.TranslateX -> view.translationX = op.px // 這個 when 語句分支不僅告訴 view 要水平移動,還告訴 view 需要移動多少距離,這是列舉等 Java 傳統思想不容易實現的
    is UiOp.TranslateY -> view.translationY = op.px
}
複製程式碼

以上程式碼中,TranslateX 是一個類,它可以攜帶多於一個的資訊,比如除了告訴 view 需要水平平移之外,還可以告訴 view 平移多少畫素,甚至還可以告訴 view 平移的動畫型別等資訊,這大概就是密封類出現的意義吧。

看到這個場景演示後,是不是就覺得撥開雲霧見月明瞭呢?好理解多了吧!

7. 巢狀類

一個類可以巢狀在另一個類裡面

class Outer{
    val attr = 0
    // 巢狀類
    class Nested{
        fun inMethod(){
            // 不能訪問外部類的屬性
            println("內部類")
        }
    }
}

fun main(args: Array<String>) {
    // 呼叫巢狀類方法
    Outer.Nested().inMethod()
}
複製程式碼

巢狀類不能訪問外部類的屬性,它其實就相當於Java 中的靜態內部類,我們把它翻譯成Java 程式碼,Nested其實就是一個靜態內部類,如下:

 public final class Outer {
   private final int attr;
   public static final class Nested {
      public final void inMethod() {
         String var1 = "內部類";
         System.out.println(var1);
      }
   }
}
複製程式碼
7.2 內部類

Kotlin中,內部類用inner關鍵字,宣告,跟Java 一樣,內部類持有一個外部類的物件引用,可以訪問外部類的屬性和方法。

class Outer{
    private val attr = 10
    inner class Inner{
        fun method(){
            println("內部類可以訪問外部類屬性:$attr")
        }
    }
}

fun main(args: Array<String>) {
    // 呼叫內部類方法,看出區別了嗎
    Outer().Inner().method()
}
複製程式碼

注意呼叫方法,巢狀類通過類直接呼叫(Java靜態方法方式),內部類通過物件呼叫。

7.3 匿名內部類

來看一下Java的匿名內部類:

mButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                // onClick
            }
        });
複製程式碼

在kotlin中,匿名內部類用物件表示式建立,給View設定點選事件的kotlin程式碼如下:

 mButton?.setOnClickListener(object: View.OnClickListener{

            override fun onClick(v: View?) {
                //onClick
            }
        })
複製程式碼

8 . 總結

八月份初的時候就在寫著篇文章,前前後後差不多1個月左右,這篇文章終於寫完了,本篇文章看完算是對Kotlin 中的類能有一個完整了解,由於涉及的內容比較多,篇幅太長,關於類的繼承、物件和物件表示式、屬性和方法 這些另開篇幅吧。Kotlin 的的一些中文官方文件翻譯得比較生硬,有的不好理解,本文有些知識點我嘗試通過Java 程式碼對比的方式講解,希望能好理解一點。如果有什麼錯誤的地方,歡迎指出。

參考:

Kotlin 語言官方參考文件

Kotlin 資料類與密封類

更多Android乾貨文章,關注公眾號 【Android技術雜貨鋪】

相關文章