Kotlin面向对象 | 青训营

115 阅读7分钟

这是我参与「第四届青训营 」笔记创作活动的第9天

继承

  • kotlin中的类默认都是封闭的,如果要让某个类开放继承,必须要用open关键字修饰它

  • 样例:

    open class Father{...}
    class son:Father(){...}
    

类型判断

  • 当需要对类进行具体类型判断是时候,可以用kotlin提供的语法

  • 关键字:is

  • 样例:

    open class Father{...}
    class son:Father(){...}
    fun main() {
        var s: son = son()
        println(s is son)
        println(s is Father)
    }
    // 运行结果:
    true
    true
    

类型转换

  • 当父类的引用指向了子类的对象,则当其要调用子类特有的方法时,就需要进行类型转换,将其转为响应的子类类型,才能调用对应的方法

  • 关键字:as

  • 样例:

    open class Father{...}
    
    class Son:Father(){
        fun myPrint(){
            println("you are calling a son object...")
        }
    }
    
    fun main() {
        var s: Father = Son()
        (s as Son).myPrint()
    }
    
    // 运行结果:
    you are calling a son object...
    

智能类型转换

  • 不同于Java的类型转换,在需要用的时候都需要进行类型转换,Kotlin提供了更加智能的语法,在需要类型转换时,只需要写一次类型转换,之后需要类型转换的地方,Kotlin就会自动地进行类型转换,相对方便

  • 样例:

    open class Father{...}
    
    class Son:Father(){
        fun myPrint(){
            println("you are calling a son object...")
        }
        fun myPrintAgain(){
            println("you are calling a son object again...")
        }
    }
    
    fun main() {
        var s: Father = Son()
        (s as Son).myPrint()
        s.myPrintAgain()	// 进行了智能类型转换了
    }
    

Any超类

  • 与Java类似,Java中所有的类都有一个公共的父类Object,同理,在Kotlin中,所有类的公共父类是Any

  • 简单验证:

    open class MyClass{}
    
    fun main() {
        var s: MyClass = MyClass()
        println(s is Any)
    }
    
    // 运行结果:
    true
    

    从上面的测试可以看出,任何类型都是Any的子类

对象

对象声明

  • 关键字:object

  • 对象声明相当于创建了一个静态类,具有单例的特性,因为Kotlin没有静态类的概念,所以才有了object的产生,这样做的好处是,有利于我们组织代码和管理一些类似于全局的状态,使得在整个应用的生命周期内保持状态的一致性更加便利

  • 用法:

    object MyClass{
        init {
            println("My SingletonInstance Class is loading...")
        }
    
        fun mainFunc(){
            println(this)
        }
    }
    
    fun main(){
        MyClass.mainFunc()
        println(MyClass)
    }
    
    // 运行结果
    My SingleInstance Class is loading...
    MyClass@5b480cf9
    MyClass@5b480cf9
    
    • 注:从上面的测试代码中可以看出,用object关键字修饰的类,与Java的静态类的用法基本一致,在调用时,不用先创建其对象,因为其本身就是一个对象,可以直接通过对象名进行访问操作。值得注意的是,其天然的就具有单例的特点

对象表达式

  • 当某个类只需要被实例化一次,并且你不需要指定它的具体名字时,就可以用到该语法,其作用类似于Java的OnClickListener的使用

  • 用法:

    open class MyClass{
        init {
            println("My SingleInstance Class is loading...")
        }
    
        open fun mainFunc(){
            println(this)
        }
    }
    
    fun main(){
        object : MyClass(){
            override fun mainFunc(){
                println("override function created!")
            }
        }
    }
    
    // 运行结果:
    My SingleInstance Class is loading...
    

伴生对象

  • 前面的对象声明,只是让对象达到静态的效果,粒度相对于函数及其属性,还是太大了。

  • 在有些时候,我们并不需要整个类都是静态的效果,而是希望在类对应的多个对象中,能有一份共同的静态函数或者变量以供使用,这时候就需要用到kotlin为此设计的伴生对象了。(Kotlin没有static关键字)

  • 关键字:companion object

  • 用法:

    class MyClass{
        companion object{
            private const val str = "Shared..."
            fun getStr(){
                println(str)
            }
        }
    } 
    
    fun main(){
        MyClass.getStr()
    }
    
    // 运行结果:
    Shared...
    
    • 注:
      • 一个类只能有一个伴生对象,也很好理解,一个伴生对象,就可以放入我们需要的所有希望成为静态的函数和属性了,就不用多此一举,创建多个伴生对象了
      • 不同于Java的static关键字修饰的静态变量与静态函数,Kotlin的伴生对象,在不需要调用的时候,是不会载入内存的,只有在需要被调用的时候,才会自动载入内存,这点也节省了kotlin的内存开销

嵌套类

  • 含义:顾名思义,就是在一个类里面声明另一个类,也即Java中的内部类的概念

  • 使用时机:当一个类只对另一个类有用,则将其嵌入到该类中并使这两个类保持在一起是合乎逻辑的操作的时候

  • 注意:在Java中的内部类,调用的时候,除了静态内部类,都需要先实例化其外部的类,再通过外部的类调用其内部类,而Kotlin中的嵌套类,不需要先实例化其外部类,可以直接通过外部类的类名访问其内部类

  • 用法:

    class MyClass{
        class InMyClass{
            fun showSomething() = println("InMyClass is calling...")
        }
    }
    
    fun main(){
        MyClass.InMyClass().showSomething()
    }
    
    // 运行结果:
    InMyClass is calling...
    

数据类

  • 关键词:data
  • 更多:参考Kotlin常用语法章节中的Create DTOs说明

枚举类

  • 含义:用于定义常量的类,是一种特殊的类

  • 细节:枚举类里的每一个常量,其实都可以看做是该枚举类的一个实例,也就是说,其常量的类型就是该枚举类类型

  • 使用:

    • 最简单的使用:

      enum class MyEnum{
          One,
          Two,
          Three,
          Four
      }
      
      fun main(){
          println(MyEnum.Four)
      }
      
      // 运行结果:
      Four
      
    • 枚举类定义函数:

      enum class MyEnum{
          One,
          Two,
          Three,
          Four;
      
          fun getCardinality(){
              println(this.name)
          }
      }
      
      fun main(){
          MyEnum.Four.getCardinality()
      }
      
      // 运行结果:
      Four
      

密封类

  • 关键词:sealed class

  • 密封类也提供了一系列的类型,类似于枚举类,但是相对于枚举类,密封类不止可以定义枚举常量,还可以对每个枚举常量进行更加灵活地控制,比如为其添加不同的属性

  • 特点:密封类可以有若干个子类,但都要继承自密封类,而且这些子类都必须和其父类(密封类)定义在同一个文件里

  • 用法:

    sealed class LicenseStatus{
        object UnQualified : LicenseStatus()
        object Learning : LicenseStatus()
        class Qualified(val licenseId : String) : LicenseStatus()
    }
    
    class Driver(var status: LicenseStatus){
        fun checkLicense() : String{
            return when (status){
                is LicenseStatus.UnQualified -> "A"
                is LicenseStatus.Learning -> "B"
                is LicenseStatus.Qualified -> "C:${(status as LicenseStatus.Qualified).licenseId}"
            }
        }
    }
    
    fun main() {
        println(Driver(LicenseStatus.Qualified("88745223")).checkLicense())
    }
    
    // 运行结果:
    C:88745223
    

    从上面测试代码可以看出,密封类,比枚举类更加灵活,且可以对每个子类进行一定的定制,更加灵活可控

抽象类

  • 含义:抽象类与接口不同的地方在于,抽象类中的函数,除了具体的实现,还可以是一个抽象的定义,即没有函数体,且两者都可以声明属性

  • 关键字:abstract

  • 用法:

    abstract class Human(var age: Int){
        abstract var name: String
        fun getMyInfo() = "myName: $name, I am $age years old!"
        abstract fun setMyName(myName: String): Unit
    }
    
    class Student(override var name: String,var myAge: Int): Human(age = myAge){
        override fun setMyName(myName: String) {
            name = myName;
        }
    }
    
    fun main() {
        var student: Student = Student("wgw",18)
        println(student.getMyInfo())
    }
    
    // 运行结果:
    myName: wgw, I am 18 years old!
    

    从上面的测试代码可以看出,抽象类中,可以声明抽象方法和抽象属性,并且抽象方法是能有默认实现的

  • 注:kotlin与Java一样,只能支持单继承,否则就会造成经典的骡子问题,因此要实现多继承,就只能借助接口,来达到多继承的效果

接口

  • 定义:Kotlin规定所有的接口属性和函数实现都要使用override关键字(该处与Java略有不同),接口中定义的函数并需要open关键字修饰,因为他们默认就是open

  • 关键词:interface

  • 开发建议:

    • 一般不要在接口里面定义属性,因为,接口的作用是定义功能
    • 如果一定要定义属性,也一般是定义为抽象类,方便后面功能的实现
  • 细节:

    • Kotlin接口中定义的方法,是可以带默认实现的,这点与Java不同
    • Kotlin接口也能声明抽象的属性(不推荐)
  • 用法:

    interface Transportation{
        var speed: Int
        fun ignition(name: String): String
    }
    
    class Car : Transportation{
        override var speed: Int
            get() = (50..300).shuffled().first()
            set(value) {speed = value}
    
        override fun ignition(name: String): String {
            return "The speed of $name is $speed"
        }
    }
    
    fun main() {
        var car: Car = Car();
        var res: String = car.ignition("bench")
        println(res)
    }
    
    // 运行结果:
    The speed of bench is 199
    

    由上面的测试代码可以看出,即使是接口中的属性,在其实现类中,也要用override关键字进行重载,这点与Java的写法略有不同

泛型

  • 泛型,即 "参数化类型",将类型参数化,可以用在类,接口,方法上。

泛型类

  • 用法:

    class Human<T>(t: T){
        var value = t
    }
    
    fun main() {
        var man: Human<String> = Human("I am a human!")
        println(man.value)
    }
    
    // 运行结果:
    I am a human!
    

泛型函数

  • 用法:

    class Human<T>(t: T){
        var value = t
    }
    
    fun<T> GenericsFunc(parameter: T) = Human(parameter)
    
    fun main() {
        println(GenericsFunc("test output").value)
    }
    
    // 运行结果:
    test output
    

泛型约束

  • 场景:在某些场合下,我们需要对泛型传入的实际类型进行范围约束,使其是在有限的几个类型中的其中之一

  • 类比:Java的superextends

  • 用法:

    class Human<T: String>(t: T){
        var value = t
    }
    
    fun main() {
        var man1: Human<String> = Human("I am String")
        println(man1.value)
    }
    
    / 运行结果:
    I am String
    

型变

可以将kotlin的协变和逆变,类比Java中的泛型上下界来比较理解

协变

  • 含义:如果泛型类只将泛型类型作为函数的返回类型,就可以用协变,可以将之称为生产类/接口,因为它主要是用来生产(produce)指定的泛型对象的

  • 作用:让泛型子类可以直接给其泛型父类使用

  • 关键字:out

  • 例子:

    // 定义一个支持协变的类
    class Runoob<out A>(val a: A) {
        fun foo(): A {
            return a
        }
    }
    
    fun main(args: Array<String>) {
        var strCo: Runoob<String> = Runoob("a")
        var anyCo: Runoob<Any> = Runoob<Any>("b")
        anyCo = strCo		  // 将泛型子类赋值给泛型父类
        println(anyCo.foo())   // 输出 a
    }
    

逆变

  • 含义:如果泛型类只将泛型类型作为函数的入参类型,就可以用逆变,可以将之称为消费者类/接口,因为它主要是用来消费(consume)指定的泛型对象的

  • 作用:让泛型父类可以直接给其泛型子类使用

  • 关键字:in

  • 例子:

    // 定义一个支持逆变的类
    class Runoob<in A>(a: A) {
        fun foo(a: A) {
        }
    }
    
    fun main(args: Array<String>) {
        var strDCo = Runoob("a")
        var anyDCo = Runoob<Any>("b")
        strDCo = anyDCo		// 将泛型父类赋值给泛型子类
    }
    

不变

  • 含义:即将泛型类型作为函数参数,又将泛型类型作为函数的输出,那么就既不用out,也不用in