Java 入坑 Kotlin 必看 —— 类、对象和接口

3,579 阅读10分钟
  • Kotlin 类、对象和接口

    Kotlin 的类和接口在概念上跟 Java 是一样的,但是用法存在一些差别,比如继承的写法、构造函数和可见性修饰符的不同等,此外还有一些 Java 中没有的概念,如数据类、密封类、委托和 object 关键字等。下面从类和接口的定义开始,感受一下 Kotlin 的非凡之处吧!

    类和接口的定义

    类与继承和 open、final 以及 abstract 关键字

    跟 Java 一样,Kotlin 使用 class 关键字来定义一个类。

    class Animal {
        fun eat() {
            ...
        }
        
        fun move() {
            ...
        }
    }
    

    在 Java 中,一个类除了被手动加上 final 关键字,它都能被任意一个类继承并重写它的非 final 方法,这就可能会导致某些子类出现不符合其父类的设计初衷,特别是在多人协作的开发环境下。

    这类问题被 Kotlin 语言设计者注意到了并切引起了他们的重视,因此,在 Kotlin 中的类和方法默认都是 final 的,如果要继承或者重写一个类和方法,必须将他们显式地声明为 open

    open class Animal {
        fun eat() {
            ...
        }
        
        open fun move() {
            ...
        }
    }
    

    继承该类的时候,需要在类名后面加上冒号后再写被继承的类名,在 Kotlin 中使用冒号代替了 Java 中的 extend关键字。

    class Dog : Animal {
        override fun move() {
            ...
        }
    }
    

    同样,Kotlin 中可以使用 abstract 关键字将一个类声明称抽象类,但它不能被实例化。抽象方法也可以覆盖父类的 open 方法,抽象方法始终是 open 的且必须被子类实现。

    abstract class Bird : Animal {
        abstract fun song() 
        
        override abstract fun move()
    }
    

    接口

    Kotlin 中的接口同样使用 interface 关键字来定义,可以在方法中加入默认的方法体。

    interface Machine {
        fun component()
        fun control() {
            ...
        }
    }
    

    与类的继承类似,实现/继承接口方法是在类名/接口名后面加上冒号再写被实现/继承的接口名。

    实现接口:

    class Electric : Machine {
        override fun component() {
            ...
        }
        
        override fun control() {
            ...
        }
    }
    

    继承接口:

    interface Computer : Machine {
        fun IODevice()
    }
    

    可见性修饰符

    可见性修饰符用于声明一个类或者接口的可见范围,类似于 Java,Kotlin 中使用 publicprivateprotected 关键字作为可见性修饰符。跟 Java 不同的是,Kotlin 默认的可见性是 public 的,并且没有 “包私有” 的概念,同时新增 internal 关键字用来表示 “模块内部可见“。

    public

    public 是 kotlin 默认的可见性修饰符,表示任何地方可见。

    private

    • 如果使用 private 声明一个类成员,则表示该类成员只在类中可见;
    • 如果使用 private 对一个类进行声明,则表示这个类只在声明该类中的文件中可见,除此之外,Kotlin 还有顶层函数和顶层属性的概念,如果用 private 声明顶层函数或者顶层属性,同样也只能在声明它的文件中对其可见。

    protected

    • 使用 protected 声明的类成员除了跟 private 一样,还够对子类可见;
    • protected 不适用于顶层声明。

    internal

    使用 internal 修饰符表示只在模块内部可见。

    一个模块就是一组一起编译的 Kotlin 文件。这有可能是一个 Intellij IDEA 模块、一个 Eclipse 项目、一个 Maven 或 Gradle 项目或者一组使用调用 Ant 任务进行编译的文件。

    类的构造方法(函数)

    Kotlin 将构造方法分为了主构造方法和次构造方法,主构造方法作为类头在类体外部声明,次构造方法在类体内部声明。

    主构造方法

    主构造方法作为类头的一部分存在,它跟在类名(与可选的类型参数)后。

    class Animal constructor(name: String) {
        ...
    }
    

    如果主构造方法没有可见性修饰符或者注解,则可以省略 constructor 关键字。

    class Animal(name: String) {
        ...
    }
    

    主构造方法中不能包含其它任何代码,因此,如果需要初始化代码,则需要把这一部分代码放在以 init 关键字作为前缀的**初始化块(initializer blocks)**中。

    class Animal(name: String) {
        init {
             val outPutName = "It's the Animal call: $name"
        }
    }
    

    如果类中的某个属性使用主构造方法的参数来进行初始化,可以通过使用 val 关键字对主构造函数的参数进行修饰,以简化代码。

    class Animal(val name: String) {
        init {
             val outPutName = "It's the Animal call: $name"
        }
        
    
        fun showName() {
            println(name)
        }
    }
    

    次构造方法

    次构造方法在类体内使用 constructor 关键字进行定义,如果类有一个主构造方法,每个次构造方法需要委托给主构造方法, 可以直接委托或者通过其它次构造方法间接委托。委托到同一个类的另一个构造方法用 this 关键字即可。

    class Animal(val name: String) {
        init {
             val outPutName = "It's the Animal call: $name"
        }
        
        // 直接委托给主构造方法
        constructor(name: String, age: Int): this(name) {
            ...
        }
        
        // 通过上面的构造方法间接委托给主构造方法
        constructor(name: String, age: Int, type: Int): this(name, age) {
            ...
        }
    }
    

    初始化块中的代码实际上会成为主构造函数的一部分。委托给主构造函数会作为次构造函数的第一条语句,因此所有初始化块中的代码都会在次构造函数体之前执行。即使该类没有主构造函数,这种委托仍会隐式发生,并且仍会执行初始化块。

    内部类和嵌套类

    嵌套类

    在一个类内部定义的另外一个类默认为嵌套类。

    class OuterClz {
        var num = 1
    
        class NestedClz {
            fun show() {
                // 编译报错
                println(num)
            }
        }
    }
    

    嵌套类不持有它所在外部类的引用。

    内部类

    在 class 关键字前面加上 inner 关键字则定义了一个内部类。

    class OuterClz {
        var num = 1
    
        inner class InnerClz {
            fun show() {
                // 编译通过
                println(num)
            }
        }
    }
    

    内部类持有了它所在外部类的引用,因此可以访问外部类的成员。

    与 Java 对比

    在 Java 中静态内部类是不会持有外部类引用的,相当于 Kotlin 的嵌套类;而非静态内部类则持有外部类的引用,相当于 Kotlin 的内部类。

    枚举类

    有时候为了类型安全,需要将某个属性所有可能的值枚举出来,开发者只能使用该枚举类中定义的枚举常量。

    枚举类的定义

    定义一个枚举类需要用到 enumclass 关键字。

    enum class Color {
    	RED, GREEN, BLUE
    }
    

    与 Java 相同,枚举类中可以声明属性和方法。

    enum class Color(val r: Int, val g: Int, val b: Int) {
        RED(255, 0, 0),
        GREEN(0, 255, 0),
        BLUE(0, 0, 255);
    
        fun rgb () = Integer.toHexString((r * 256 + g) * 256 + b)
    }
    

    当声明枚举常量的时候,需要提供该常量所需的属性值,并且需要在声明完成后加上分号

    枚举类的使用

    Kotlin 中的 when 可以使用任何对象,因此,可以使用 when 表达式来判断枚举类型。

    var myColor = Color.RED
    
    when (myColor) {
        Color.RED -> println("It's a red color")
        Color.GREEN -> println("It's a green color")
        Color.BLUE -> println("It's a blue color")
    }
    

    需要注意的是,如果在 when 表达式中没有 case 到所有的枚举常量,编译器并不会报错。

    var myColor = Color.RED
    
    when (myColor) {
        Color.RED -> println("It's a red color")
        Color.GREEN -> println("It's a green color")
    }
    

    但是会建议你处理所有可能的情况(添加 “BLUE” 分支或者 “else” 分支)。

    'when' expression on enum is recommended to be exhaustive, add 'BLUE' branch or 'else' branch instead

    密封类

    如果一个父类 Animal 只有两个分别是 Bird 和 Dog 的子类,在 when 表达式中处理所有情况的时候如果代码写成如下:

    open class Animal {
        fun doSomething() {
    
        }
    }
    
    class Bird(val name: String) : Animal()
    
    class Dog(val name: String) : Animal()
    
    fun main() {
        fun showName(animal: Animal) =
                when (animal) {
                    is Dog -> println("It's the dog name ${animal.name}")
                    is Bird -> println("It's the bird name ${animal.name}")
                }
    }
    

    这时编译器会提示错误:必须加上 else 分组。

    extends_when_error

    这时候,如果想写出简洁的代码,密封类就派上用场了。

    密封类用来表示受限的类继承结构:当一个值为有限集中的类型、而不能有任何其他类型时。在某种意义上,他们是枚举类的扩展:枚举类型的值集合也是受限的,但每个枚举常量只存在一个实例,而密封类的一个子类可以有可包含状态的多个实例。

    密封类的定义

    密封类的定义需要在类名前面加上 sealed 关键字。

    sealed class Animal
    

    使用密封类的时候需要注意几点:

    • 密封类是具有 open 属性的,子类可以直接继承它;
    • 密封类的直接子类必须在与它自身相同的文件中定义(Kotlin 1.1 之前子类的定义被限制在密封类的内部),但是密封类的间接子类可以放在任何位置;
    • 密封类是抽象的,意味着它不能被实例化并且可以拥有抽象成员;
    • 密封类的构造方法默认为 private 并且不允许拥有非 private 构造方法。

    密封类的使用

    使父类变成密封类之后,意味着对可能创建的子类做出了限制,when 表达式中处理了所有 Animal 类的子类的情况,因此不需要 “else” 分支。

    sealed class Animal {
        fun doSomething() {
    
        }
    }
    
    class Bird(val name: String) : Animal()
    
    class Dog(val name: String) : Animal()
    
    fun main() {
        fun showName(animal: Animal) =
                when (animal) {
                    is Dog -> println("It's the dog name ${animal.name}")
                    is Bird -> println("It's the bird name ${animal.name}")
                }
    }
    

    当你为 Animal 类添加一个新的子类且没有修改 when 表达式内容的时候的时候,IDE 会编译报错提醒你没有覆盖所有情况。

    sealed_class_when_error

    数据类

    使用 Java 的时候,不免会需要一些 Entity 类来承载数据,有时候还需要重写 toString、equals 或者 hashCode 方法,而这些方法的写法千遍一律,有些 IDE 还能够自动生成这些方法。但是 Kotlin 中的数据类能够很好地避免这些情况,使代码看起来更加简洁。

    数据类的定义

    数据类的定义需要在类名前面加上 data 关键字。

    data class User(val name: String, val gender: Int, val age: Int)
    

    数据类的定义必须保证以下条件:

    • 主构造方法至少又一个参数,且参数必须标记为 var 或者 val;
    • 数据类不能是抽象、开放、密封或者内部的。

    数据类的使用

    数据类定义完成之后,编译器自动从主构造函数中声明的所有属性导出以下成员:

    1. equals() 和 hashCode() 方法:

      通常用于对象实例之间的比较。

    2. toString() 方法:

      fun main() {
          var user = User("guanpj", 1, 18)
          println(user.toString())
      }
      

      输出结果为:User(name=guanpj, gender=1, age=18)

    3. componentN 函数:

      componentN 称为解构函数,简单来说就是把一个对象解构成多个变量以便使用。在 data 类中如果有 N 个变量,则编译器会生成 N 个解构函数(component1、component2 ... componentN)按顺序对应这 N 个变量。使用方法如下:

      fun main() {
          var user = User("guanpj", 1, 18)
          var (name, gender, age) = user
          println("My name is $name and I'm $age years old.")
      }
      

      输出结果:My name is guanpj and I'm 18 years old.

      需要注意的是,data 类中的这些 componentN 函数不允许提供显式实现。

    4. copy() 函数:

      使用 copy() 函数能够生成一个与该对象具有相同属性的对象,并且可以修改部分属性。

      fun main() {
          var user = User("guanpj", 1, 18)
          var newUser = user.copy("gpj")
          println("My name is ${newUser.name} and I'm ${newUser.age} years old.")
      }
      

      输出结果为:My name is gpj and I'm 18 years old.

      同样,copy() 函数也不允许提供显式实现。

    委托

    虽然 ”委托“ 这个设计思想在各个编程语言中都或多或少地有所体现,但是 Kotlin 直接在语法上对 委托模式做了支持,Kotlin 支持类层面的委托和属性的委托,下面分别从这个两个方面讲解委托模式在 Kotlin 中的使用。

    类委托

    前面已经提到过,Kotlin 在设计之初就考虑到因继承带来的 “脆弱的基类” 问题,因此把类默认视作 final 类型的,当需要扩展某些类的时候,手动将它们标记成 open 并且在扩展的过程中注意兼容性。

    假设你有一个需求,需要统计一个集合添加元素的次数,你使用一个 CountingSet 来实现 MutableCollection 接口,并扩展了 add() 和 addAll() 方法,其他方法直接交给成员变量 innerSet 来处理。

    class CountingSet<T>() : MutableCollection<T>  {
        val innerSet: MutableCollection<T> = HashSet<T>()
    
        var objectsAdded = 0
    
        override fun add(element: T) : Boolean {
            objectsAdded++
            return innerSet.add(element)
        }
    
        override fun addAll(c: Collection<T>): Boolean {
            objectsAdded += c.size
            return innerSet.addAll(c)
        }
    
        override val size: Int
            get() = innerSet.size
    
        override fun contains(element: T): Boolean  = innerSet.contains(element)
    
        override fun containsAll(elements: Collection<T>): Boolean = 				       innerSet.containsAll(elements)
    
        override fun isEmpty(): Boolean = innerSet.isEmpty()
    
        override fun clear() = innerSet.clear()
    
        override fun iterator(): MutableIterator<T> = innerSet.iterator()
    
        override fun remove(element: T): Boolean = innerSet.remove(element)
    
        override fun removeAll(elements: Collection<T>): Boolean = innerSet.removeAll(elements)
    
        override fun retainAll(elements: Collection<T>): Boolean = innerSet.retainAll(elements)
    }
    

    可以看到,除了 add() 和 addAll() 方法,其他所有的方法都需要重写并交给 innerSet 去实现,这样势必会产生比较多的模板代码,使用类委托便可以很好地解决这个问题。

    class CountingSet<T>(val innerSet: MutableCollection<T> = HashSet<T>()) 
        : MutableCollection<T> by innerSet {
        
        var objectsAdded = 0
    
        override fun add(element: T) : Boolean {
            objectsAdded++
            return innerSet.add(element)
        }
    
        override fun addAll(c: Collection<T>): Boolean {
            objectsAdded += c.size
            return innerSet.addAll(c)
        }
    }
    

    通过在超类型列表中使用 by 关键字进行委托,编译器将会生成转发给 innerSet 的所有 MutableCollection 中的方法,如果有覆盖方法,编译器将使用覆盖的方法而不是委托对象中的方法。

    属性委托

    同样,一个属性也可以将它的访问器逻辑委托给一个辅助对象,属性委托的写法为:

    val/var <属性名>: <类型> by <表达式>

    代码表示如下:

    class MyClz {
        var p: String by Delegate("abc")
    }
    

    对于 val 和 var 类型的属性来说,它的 get()(和 set())方法将会被委托给 Delegate 类的 getter()(和 setter)方法。

    class Delegate<T>(default: T) {
        private var value = default
    
        operator fun getValue(thisRef: Any?, property: KProperty<*>): T {
            return value
        }
    
        operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
            this.value = value
        }
    }
    

    使用属性委托需要注意:

    1. 对于一个 val (只读) 属性,委托类必须提供一个名为 getValue 的方法,并使用 operator 关键字修饰,该方法接收以下参数:
      • thisRef —— 必须与 属性所有者 类型(对于扩展属性——指被扩展的类型)相同或者是它的超类型;
      • property —— 必须是类型 KProperty<*> 或其超类型。
    2. 对于一个 var(可读写) 属性,委托类必须额外提供一个名为 setValue 的方法,并使用 operator 关键字修饰,该方法接收以下参数:
      • thisRef —— 同上;
      • property —— 同上;
      • new value —— 必须与属性同类型或者是它的超类型。

    另外,Kotlin 提供了 ReadOnlyPropertyReadWriteProperty 接口以方便实现 val 和 var 属性的委托,只需实现这两个接口并重写它的方法即可。

    class Delegate<T>(default: T) : ReadWriteProperty<Any?, T> {
        private var value = default
    
        override fun getValue(thisRef: Any?, property: KProperty<*>): T {
            return value
        }
    
        override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
            this.value = value
        }
    }
    

    对象——object

    Kotlin 中的 object 被视为对象,但是跟面向对象中的 ”对象“ 却不太一样,它的功能非常强大,它可以定义一个单例、实现类似 Java 中的静态方法的功能以及创建匿名内部类,Kotlin 中的 object 是拥有某个具体状态的实例,出事化后不会再改变。

    对象声明

    应该很多人跟我一样,刚从 Java 转过 Kotlin 的时候都会遇到一个疑惑:Kotlin 中是怎样定义单例的?事实上,通过对象声明,在 Kotlin 中定义单例简直易如反掌,通过 object 关键字,可以定义一个对象声明。

    object DataManager {
        val data: Set<Person> = setOf()
    
        fun doSomething() {
            for (person in data) {
    			...
            }
        }
    }
    

    与变量一样,对象声明允许你使用对象名加.字符的方式来调用方法和访问属性:

    fun main() {
        DataManager.data
        DataManager.doSomething()
    }
    

    对象声明具有以下特点:

    1. 对象声明的初始化过程是线程安全的;
    2. 个对象声明也可以包含属性、方法、初始化语句块等的声明;
    3. 对象声明在定义的时候立即创建,因此不允许有任何构造方法;
    4. 对象声明同样可以继承自类和接口。

    基于以上几点,对象声明完全符合单例模式的要求。

    伴生对象

    同样的,从其他语言转到 Kotlin 的程序员可能会遇到另外一个问题 —— Kotlin 中怎样在一个类中定义一个静态方法?在 Kotlin 中,如果想要直接通过容器类名称来访问这个对象的方法和属性的能力,不再需要显式地指明对象的名称,就需要使用到伴生对象的概念了,伴生对象的定义如下:

    class MyClz {
        companion object {
            var myVariable = "My variable"
            
            fun doSomething() {
                ...
            }
        }
    }
    

    现在就可以像 Java 中调用静态变量和静态方法的方式一样调用 myVariable 变量和 doSomething() 方法了!

    fun main() {
        MyClz.myVariable
        MyClz.doSomething()
    }
    

    与对象声明 一样,伴生对象也可以实现接口。

    interface MyInterface {
        fun doSomething()
    }
    
    class MyClz {
        companion object : MyInterface {
            override fun doSomething() {
                ...
            }
        }
    }
    
    fun main() {
        // MyClz 类的名字可以被当作 MyInterface 实例
        var myInstant: MyInterface = MyClz
        myInstant.doSomething()
    }
    

    对象表达式

    object 关键字不仅仅能用来声明单例模式的对象,还能用来声明匿名对象,与 Java 匿名内部类只能扩展一个类或实现一个接口不同 , Kotlin 的匿名对象可以实现多个接口或者不实现接口,我们称之为对象表达式

    interface MyInterface {
        fun doSomething()
        fun doOtherthing()
    }
    
    val myInstant = object : MyInterface {
        override fun doSomething() {
            ...
        }
    
        override fun doOtherthing() {
            ...
        }
    }
    

    另外一个跟 Java 不同的点就是,对象表达式中可以直接访问创建它的函数中的非 final 变量。

    class MyClz {
        var mVariable = "This is my variable."
    
        val myInstant = object : MyInterface {
            override fun doSomething() {
                println(mVariable)
            }
    
            override fun doOtherthing() {
            }
        }
    }
    

    小结

    • 使用对象声明可以定义一个单例类;

    • 使用伴生对象可以实现类似 Java 中调用静态变量和静态方法的功能;

    • 对象声明和伴生对象都可以实现接口;

    • 对象表达式可以作为 Java 中匿名内部类的替代品,并且使用起来更加方便。

    • 对象声明是在第一次被访问到时延迟初始化的,之后访问不会被初始化、对象表达式是在每次使用到的时候立即初始化并执行的,每次都会创建一个新的对象、伴生对象是在相应的类被加载(解析)的时候初始化的。