美文网首页
Kotlin-面向对象-基础

Kotlin-面向对象-基础

作者: AilurusFulgens | 来源:发表于2020-11-05 11:54 被阅读0次

    方法

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

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

    fun main() {
        // 以下两种函数类型等价
        val a1: (Int) -> Int = ::test
        val a2: Function1<Int, Int> = ::test
        // 以下三种函数类型等价
        val b1: (Dog, String) -> String = Dog::eat
        val b2: Dog.(String) -> String = Dog::eat
        val b3: Function2<Dog, String, String> = Dog::eat
        // 注:FunctionN<A, B, ...>: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<A, B, ...>
    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<MandatoryOverride>.test() //class MandatoryOverride test
            super<IMandatoryOverride>.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-面向对象-基础

          本文链接:https://www.haomeiwen.com/subject/ohpqvktx.html