Kotlin-面向对象-基础

方法

Kotlin 中方法和函数其实是统一的,但是我们这么理解区别:
函数:直接定义在文件中的 fun
方法:定义在 class 中的 fun

方法和函数一样,也是可以赋值给其他对象,也使用双冒号::运算符。
函数::函数名
方法类名::方法名

fun main() {
    // 以下两种函数类型等价
    val a1: (Int) -> Int = ::test
    val a2: Function1 = ::test
    // 以下三种函数类型等价
    val b1: (Dog, String) -> String = Dog::eat
    val b2: Dog.(String) -> String = Dog::eat
    val b3: Function2 = Dog::eat
    // 注:FunctionN:N为参数加返回值个数,<>中依次写入 类名,参数类型(0~n)个,返回值类型(0~1)个
    
    println(a1.invoke(2)) //4
    println(a2(3)) //9
    println(b1.invoke(Dog(), "bone1")) //Dog is eating bone1
    println(b2(Dog(), "bone2")) //Dog is eating bone2
    println(b3.invoke(Dog(), "bone3")) //Dog is eating bone3
}

// 定义函数
fun test(a: Int) = a * a

// 定义类
class Dog {
    // 定义方法
    fun eat(food: String) = "Dog is eating $food"
}

注:函数类型均可省略,因为编译器可以自动推断出来。

FunctionN
N:参数加返回值个数
<>:依次写入 类名,参数类型(0 ~ n)个,返回值类型(0 ~ 1)个


中缀表示法

Koltin 中的方法也可以使用 infix 来修饰,写法就和双目运算符一样。
需要注意的是:infix 方法只能有一个参数,因为双目运算符的后面只能带一个参数。


componentN 方法与解构

Kotlin 允许将一个对象的 N 个属性 “解构” 给多个变量,写法如下:
var (name, age) = user
Kotlin 实际会执行的代码:
var name = user.component1()
var age = user.component2()
如果希望将对象解构成多个变量的话,那就需要定义多个 componentN() 方法,且该方法需要使用 operator 修饰。

class User(private var name: String, private var age: Int) {

    operator fun component1() = this.name

    operator fun component2() = this.age
}

fun main() {
    // 解构 User
    val (name, age) = User("张三", 28)
    println("$name 今年的年龄是 $age 岁") //张三 今年的年龄是 28 岁
    // 只解构 User 的部分属性,如果想忽略前面属性,可使用 _ 来代替
    val (name2) = User("李四", 29)
    println("用户的名字是 $name2") //用户的名字是 李四
    val (_, age2) = User("王五", 30)
    println("用户的年龄是 $age2 岁") //用户的年龄是 30 岁
}

如果只想解构部分属性,那忽略前面属性可以使用 _ 来占位。


数据类 data class

数据类专门用来封装数据,由于简化 Java 中某些只定义fieldgettersetter方法的类。
数据类型支持解构,从而可以实现返回多个值的函数。

数据类需要满足如下要求:

  1. 主构造器中至少需要一个参数;
  2. 主构造器的所有参数需要使用 valvar 声明为属性;
  3. 不能使用 abstractopensealed 修饰,也不能定义成内部类;
  4. 可以实现接口,也可继承其他类。

数据类会自动生成如下内容:

  1. 生成 equals()/hashCode() 方法;
  2. 自动重写 toString() 方法;
  3. 为每个属性自动生成 operator 修饰的 componentN() 方法;
  4. 生成 copy() 方法,用于完成对象复制。

数据类定义

data class DataClass(
    val result: Int,
    val status: Int
)

数据类使用

fun main() {
    val (result, status) = DataClass(1, 2)
    println("result: $result, status: $status") //result: 1, status: 2
    val d1 = DataClass(1, 1)
    val d2 = d1.copy()
    println(d1) //DataClass(result=1, status=1)
    println(d2) //DataClass(result=1, status=1)
    println(d1 === d2) //false,说明 copy() 出来的不是同一个的对象
}

Kotlin 标准库中提供了 Pair(支持两个任意类型的属性) 和 Triple(支持三个任意类型的属性) 两个数据类。


Lambda 表达式中解构

如果 Lambda 表达式的参数是支持解构的类型,那么就可以在括号中引入多个新参数来替代单个参数。
map.mapValues { entry -> "${entry.value}" }
可以写成
map.mapValues { (_, value) -> "$value" }

Lambda 表达式参数与解构的区别:
{ a -> ... }:一个参数
{ a, b -> ... }:两个参数
{ (a, b) -> ... }:一个参数,并解构成了两个变量
{ (a, b), c -> ... }:两个参数,第一次参数解构成了两个变量

注意:Lambda 表达式中的参数是不需要圆括号的,如果出现圆括号,那就是使用解构


属性和字段

属性是 Kotlin 中的一个重要特色,相当于 Java 中的field加上gettersetter方法(只读属性没有setter方法),且开发者不需要自己实现gettersetter
在定义 Kotlin 的普通属性时,必须显示指定初始值,要么在定义时指定,要么在构造器中指定。

// 属性a需要在构造器中初始化,属性b,c直接在定义时初始化
// a,b是只读属性,只有getter方法
// c是读写属性,有getter和setter方法
class Item(val a: String) {
    val b = "bbb"
    var c = "ccc"
}

在 Kotlin 类中定义属性后,Kotlin 中只能使用点语法来访问属性,Java 中只能使用 getter 和 setter 方法来访问属性。


自定义 getter 和 setter

虽然定义了属性之后,系统会自动生成gettersetter方法,但是你还可以自定义这两个方法。无须使用 fun 关键字。
getterget() {}(可使用单表达式),应该是一个无参,有返回值的方法;
setterset(value) {}(可使用单表达式),应该带一个参数,无返回值。

class UserInfo(var first: String, var last: String) {
    // 自定义getter和setter
    var fullName: String
        // 由于fullName是通过first和last计算出来的,所以不需要生成field,所以就不能设置初始值
        get() = "$first.$last"
        set(value) {
            if ("." !in value && value.indexOf(".") != value.lastIndexOf(".")) {
                throw IllegalArgumentException("您输入的名称不合法")
            } else {
                val names = value.split(".")
                first = names[0]
                last = names[1]
            }
        }
    // 可直接在getter和setter方法名前修改可见性和添加注解,并不修改默认实现
    var school = "清华大学"
        private set
        @Inject get
}

val user1 = UserInfo("张", "三")
println("first: ${user1.first}, last: ${user1.last}, full: ${user1.fullName}") //first: 张, last: 三, full: 张.三
val user2 = UserInfo("张", "三")
user2.fullName = "李.四"
println("first: ${user2.first}, last: ${user2.last}, full: ${user2.fullName}") //first: 李, last: 四, full: 李.四

幕后字段

当定义完属性后,系统自动为属性生成的field字段就成为幕后字段(backing field)。
只要满足以下条件,系统就会为属性生成幕后字段:

  1. 该属性使用系统自动生成的gettersetter
  2. 重写gettersetter时,使用field显式引用了幕后字段。
class BackingField(name: String, age: Int) {
    var name = name
        set(value) {
            if (value.length < 2 || value.length > 6) {
                println("您输入的姓名不合法!")
            } else {
                field = value
            }
        }
    var age = age
        set(value) {
            if (value < 0 || value > 100) {
                println("您输入的年龄不合法!")
            } else {
                field = value
            }
        }
}

val backingField = BackingField("张三", 29)
backingField.name = "张三三三三三三"
println(backingField.name) //张三
backingField.name = "李四"
println(backingField.name) //李四

gettersetter方法中,需要通过field关键字来引用幕后字段。


幕后属性

如果需要自定义field,并自定义getter和setter,就可以使用幕后属性(backing property)。
幕后属性就是用private修饰的属性,Kotlin不会为幕后属性提供getter和setter方法。

class BackingProperty(name: String) {
    // 定义幕后属性 _name
    private var _name: String = name
    // name赋值取值都是通过幕后属性 _name 进行的
    var name
        get() = _name
        set(value) {
            _name = value
        }
}

val backingProperty = BackingProperty("Kotlin")
println(backingProperty.name) //Kotlin
backingProperty.name = "Java"
println(backingProperty.name) //Java

延迟初始化属性

设置延迟初始化后,就可以在定义时和构造方法里不设置初始值了。使用lateinit关键字修饰。
lateinit修饰符有以下限制:

  1. 只能修饰在类体中声明的可变属性,即lateinit var是固定搭配;
  2. 修饰的属性不能有自定义的gettersetter方法;
  3. 修饰的属性必须是非空类型;
  4. 修饰的属性不能是原生类型(即Java的8种类型对应的类型)。

注意:使用lateinit修饰后,Kotlin不会为属性执行默认初始化,如果在赋值之前调用,则会引发lateinit property name has not been initialized异常。

class LateInit {
    lateinit var a: String
    lateinit var b: String
}

val lateInit = LateInit()
lateInit.a = "aaa"
lateInit.b = "bbb"
println("${lateInit.a}, ${lateInit.b}") //aaa, bbb

内联属性

可以使用inline修饰符修饰没有幕后字段的属性的gettersetter方法,也可以修饰属性本身,这就相当于同时修饰该属性的gettersetter
被修饰的gettersetter方法在调用时会执行内联化。

class InlineProp {

    // 定义普通属性,由于有幕后字段,故不能被inline修饰
    var name: String = ""

    // inline get,不能有幕后字段field
    val firstName: String
        inline get() = name.split(".")[0]

    // inline set,不能有幕后字段field
    var lastName: String
        inline set(value) {
            name = "${name.split(".")[0]}.$value"
        }
        get() = name.split(".")[1]

    // inline prop <=> inline get & set,不能有幕后字段field
    inline var userName: String
        get() = name
        set(value) {
            if ("." !in value && value.indexOf(".") != value.lastIndexOf(".")) {
                println("您输入的名称不合法")
                return
            }
            name = value
        }
}

深入构造器

Kotlin 类可以定义0 ~ 1个主构造器和0 ~ N个次构造器。
如果主构造器没有任何注解或可见性修饰符,则可以省略constructor关键字

主构造器和初始化块

主构造器的作用:
初始化块可以使用主构造器定义的形参;
在声明属性时可以使用主构造器定义的形参;

class ConstructorTest(name: String) {
    // 初始化块中可以直接调用主构造器中定义的参数
    init {
        println(name)
    }
}

// 定义一个private的主构造器,不可省略constructor关键字
class ConstructorTest private constructor(name: String) {
    // 初始化块中可以直接调用主构造器中定义的参数
    init {
        println(name)
    }
}

次构造器和构造器重载

初始化块必定在每个构造器之前调用,因为次构造器是委托的主构造器,即委托调用初始化块。
: this()

// 定义一个无参的主构造器
class ConstructorOverload() {
    var a: String = ""
    var b: String = ""
    init {
        println("这是初始化块")
    }

    // 构造器重载,定义有一个参数的次构造器,委托主构造器,即委托调用初始化块,使用 : this()
    constructor(a: String): this() {
        println("有一个参数的构造器:$a")
        this.a = a
    }

    // 构造器重载,定义有两个参数的次构造器,委托主构造器,即委托调用初始化块,使用 : this()
    constructor(a: String, b: String): this() {
        println("有两个参数的构造器:a = $a,b = $b")
        this.a = a
        this.b = b
    }
}

val constructorOverload = ConstructorOverload()
//这是初始化块
val constructorOverload1 = ConstructorOverload("param1")
//这是初始化块
//有一个参数的构造器:param1
val constructorOverload2 = ConstructorOverload("param1", "param2")
//这是初始化块
//有两个参数的构造器:a = param1,b = param2

主构造器声明属性

在主构造器参数上直接加上 var 或 val 即可声明属性,也可为参数设上默认值

// 在主构造器参数上直接加上 var 或 val 即可声明属性,也可为参数设上默认值
class ConstructorParam(var p1: String = "p1", var p2: String = "p2") {}

继承

修饰符 class SubClass: SuperClass { ... }
Kotlin 的类默认是final的,不能派生子类,所以如果需要让一个类能派生子类,需要使用open修饰该类。

open class SuperClass(name: String) {
    constructor(): this("nnnn")

    init {
        println(name)
    }
}

// 定义一个无参子类,必须立即调用父类构造器
class SubClass1: SuperClass("foo") {
}

// 定义一个参数的子类,必须立即调用父类构造器
class SubClass2(name: String): SuperClass(name) {
}

class SubClass3: SuperClass {
    // 定义一个次构造器,隐式委托调用父类无参的构造器
    constructor()

    // 定义一个参数的次构造器,显式委托父类带参数的构造器
    constructor(name: String): super(name)

    // 定义两个参数的次构造器,显式委托本类带参数的构造器
    constructor(name: String, name1: String): this(name)
}

重写父类的方法

方法也需要使用open来修饰,才可以被子类重写。
子类重写父类的方法必须使用override来修饰;

open class Bird(name: String) {
    init {
        println(name)
    }

    // 该方法可被子类重写,故需要使用 open 修饰
    open fun fly() {
        println("我能飞")
    }
}

// 麻雀
class Sparrow : Bird("麻雀")

// 鸵鸟
class Ostrich : Bird("鸵鸟") {
    // 重写父类的 fly 方法
    override fun fly() {
        println("我不能飞")
    }
}

val sparrow = Sparrow() //麻雀
sparrow.fly() //我能飞
val ostrich = Ostrich() //鸵鸟
ostrich.fly() //我不能飞

重写父类的属性

父类中需要被重写的属性,也需要用open修饰;
子类重写属性必须用override修饰;
属性重写有两个限制:

  1. 类型要兼容;
  2. 访问权限要更大或相等;只读属性val可被读写属性var重写;读写属性var不可被只读属性val重写;
open class Book {
    internal open var price: Double = 10.9
    open var publisher: String = ""
    open val name: String = ""
}

class KotlinBook: Book() {
    // 重写父类price属性,可扩大访问权限
    public override var price: Double = 20.9
    // 重写父类publisher属性,不能将var改成val,读写属性不能被只读属性重写
    override var publisher: String = "机械工业出版社"
    // 重写父类name属性,能将val改成var,只读属性能被读写属性重写
    override val name: String = "Kotlin编程实践"
}

val kotlinBook = KotlinBook()
println("《${kotlinBook.name}》是由“${kotlinBook.publisher}”出版,售价${kotlinBook.price}元") //《Kotlin编程实践》是由“机械工业出版社”出版,售价20.9元

super

访问被子类重写的属性或调用被子类重写的方法时,默认会访问子类中的属性或执行子类中的方法,若仍想访问父类中被重写的属性或调用父类中被重写的方法,需要使用super

open class Book {
    internal open var price: Double = 10.9
    open var publisher: String = "出版社"
    open val name: String = "书名"

    open fun test1() {
        println("Book test1")
    }

    open fun test2() {
        println("Book test2")
    }
}

class KotlinBook: Book() {
    // 重写父类price属性,可扩大访问权限
    public override var price: Double = 20.9
    // 重写父类publisher属性,不能将var改成val,读写属性不能被只读属性重写
    override var publisher: String = "机械工业出版社"
    // 重写父类name属性,能将val改成var,只读属性能被读写属性重写
    override val name: String = "Kotlin编程实践"

    // 访问被重写的属性,默认只会访问当前子类中定义的属性
    fun getSelfName() = name
    // 如果想访问父类中的属性,使用super
    fun getParentName() = super.name

    override fun test2() {
        println("KotlinBook test2")
    }

    fun test3() {
        // test2 被当前子类重写,默认子类的test2
        test2() //KotlinBook test2
        // 使用super来调用父类的test2
        super.test2() //Book test2
        // 由于当前子类未重写test1,故会调用父类的test1
        test1() //Book test1
    }
}

强制重写

如果子类从多个直接超类型(接口或类)继承了同名成员,那么Kotlin要求子类必须重写该成员;
如果要访问超类中的成员,可使用super<超类型名>来进行引用。

open class MandatoryOverride {
    open fun test() {
        println("class MandatoryOverride test")
    }
}

interface IMandatoryOverride {
    fun test() {
        println("interface IMandatoryOverride test")
    }
}

class Mandatory: MandatoryOverride(), IMandatoryOverride {
    // 由于MandatoryOverride和IMandatoryOverride中都有test方法,所以子类必须强制重写该方法
    override fun test() {
        // 可使用super<超类型名>来引用指定超类的test方法
        super.test() //class MandatoryOverride test
        super.test() //interface IMandatoryOverride test
    }
}

多态

把一个子类对象赋值给父类变量,这就是多态。
变量在编译阶段只能调用其编译时类型所具有的方法,但在运行时则执行其运行时所具有的方法。

open class BaseClass {
    open var book = 6

    fun base() {
        println("父类中的普通方法")
    }

    open fun test() {
        println("父类中可以被覆盖的方法")
    }
}

class SubClass: BaseClass() {
    override var book = 60

    fun sub() {
        println("子类中的普通方法")
    }

    override fun test() {
        println("子类的覆盖父类的方法")
    }
}

//把一个子类对象赋值给父类变量,这就是多态
val baseClass: BaseClass = SubClass()
println(baseClass.book) //60
baseClass.base() //父类中的普通方法
baseClass.test() //子类的覆盖父类的方法
// baseClass.sub() 由于声明的是BaseClass,没有sub方法,故此处使用会编译错误

使用is检查类型

Kotlin提供了类型检查运算符is!is,来保证类型转换不会出错。

is运算符前面操作数的编译时类型要么与后面的类相同,要么与后面的类具有父子继承关系,否则编译时程序就会报错。

val hello: Any = "Hello"
//由于Any是所有类的基类,所以可以使用 is String,is Date
println("hello 是String:${hello is String}") //hello 是String:true
println("hello 是Date:${hello is Date}") //hello 是Date:false
val str = "Hello"
// println(str is Date) 编译错误,因为String与Date没有继承关系

Kotlin的is!is是非常智能的,只要你对其进行了判断,变量就会自动转换为目标类型。

val hello: Any = "Hello"
if (hello is String) {
    println(hello.length) //hello自动转换为String,可直接调用String相关的方法。
}

when分支也可进行智能转换。

fun isTest(x: Any) {
    when (x) {
        is String -> println(x.length)
        is Int -> println(x.toDouble())
    }
}

isTest(3) //3.0
isTest("abcd") //4

使用as运算符转型

除了使用is进行类型检查,还可使用asas?进行强制转型。
as:不安全的强制转型运算符,若转换失败,会引发ClassCastException异常;
as?:安全的强制转型运算符,若转换失败,不会引发异常,而是返回null

val obj: Any = "Hello"
//使用as强制转型,成功
val objStr = obj as String
println(objStr)
// val objInt = obj as Int 转型失败,会引发ClassCastException异常
// val num: Number = objStr as Number 由于objStr是String,String和Number没有继承关系,所以编译器会提示转换不可能成功

val objStrNullable = obj as? String
println(objStrNullable?.length) //5
val objIntNullable = obj as? Int
println(objIntNullable?.toDouble()) //null

你可能感兴趣的:(Kotlin-面向对象-基础)