美文网首页我爱编程Kotlin
Kotlin 类、对象和接口(一)——定义类继承结构

Kotlin 类、对象和接口(一)——定义类继承结构

作者: Lin野人 | 来源:发表于2018-05-09 22:34 被阅读0次

    Kotlin 的类和接口跟 Java 中对应的有点不同,比如:Kotlin 中接口可以包含属性声明,Kotlin 的声明默认是 finalpublic 的,嵌套类并不是默认在内部的,他们不包含外部类的隐式引用。

    定义类层级

    Kotlin 中有跟 Java 类似但是有些不同的默认可见性和访问修饰符,以及 sealed 修饰符,它将限制创建子类的可能性。

    1 Kotlin 中的接口:带有默认实现的方法

    Kotlin 接口跟 Java 8 中的相似:它们可以包含抽象方法的定义和非抽象方法的实现(类似 Java 8 的默认方法),但是它们不能包含任何状态。
    使用 interface 关键字而不是 class 来声明一个 Kotlin 的接口。

    // 代码清单 1.1     声明一个简单的接口
    interface Clickable {
      fun click()
    }
    

    这声明了一个拥有名为 click 的但抽象方法的几口。所有实现这个接口的非抽象类都需要提供这个方法的一个实现。

    // 代码清单 1.2     实现一个简单接口
    class Button : Clickable {
      override fun click() = println("I was clicked")
    }
    >>> Button().click()
    I was clicked
    

    Kotlin 在类名后面使用冒号来代替了 Java 中的 extends 和 implements 关键字。和 Java 一样,一个类可以实现任意多个接口,但是只能继承一个类。
    与 Java 中 @Override 注解类似,override 修饰符用来标注被重写的父类或接口的方法和属性。与 Java 不同的是,在 Kotlin 中使用 override 修饰符是强制要求的。这会避免先写出实现方法再添加抽象方法造成的意外重写;代码将不能编译,除非显式地将方法标注为 override 或者重命名。
    接口方法可以有一个默认实现。与 Java 8 不同的是,Java 8 中需要在这样的实现上标注 default 关键字,对于这样的方法,Kotlin 没有特殊的注解:只需要提供一个方法体。

    // 代码清单 1.3     在接口中定义一个带方法体的方法
    interface Clickable {
        // 普通的方法声明
        fun click()
        // 带默认实现的方法
        fun showOff() = println("I'm clickable!")
    }
    

    如果实现了这个接口,就需要为 click 提供一个实现。可以重新定义 showOff 方法,也可以直接使用默认方法。
    假如存在一个定义了 showOff 方法并且有如下实现的另一个接口:

    // 代码清单1.4      定义另一个实现了同样方法的接口
    interface Focusable {
        fun setFocus(b: Boolean) =
                println("I ${if (b) "got" else "lost"} focus.")
    
        fun showOff() = println("I'm focusable!")
    }
    

    如果类实现这两个接口,就必须显式实现 showOff,否则会得到如下的编译错误:

    The Class 'Button' must 
    override public open fun showOff() because it inherits
    many implementions of it
    

    Kotlin 编译器强制要求提供自己的实现。

    // 代码清单 1.5     调用继承自接口方法的实现
    class Button : Clickable, Focusable {
        override fun click() = println("I was clicked")
        // 如果同样的继承成员有不止一个实现,必须提供一个显式实现
        override fun showOff() {
            // 使用尖括号加上父类型名字的 "super" 表明了想要调用哪一个父类的方法
            super<Clickable>.showOff()
            super<Focusable>.showOff()
        }
    }
    

    Button 通过调用实现的两个父类型中的实现来实现 showOff()。要调用一个继承的实现,可以使用与 Java 相同的关键字:super。但是选择一个特定实现的语法是不同的。在 Java 中可以把基类的名字放在 super 关键字前面,就像 Clickable.super.showOff() 这样,在 Kotlin 中需要把基类的名字放在尖括号中:super<Clickable>.showOff()

    在 Java 中实现包含方法体的接口
    Kotlin 1.0 是以 Java 6 为目标设计的,其并不支持接口中的默认方法,因此它会把每个默认方法的接口编译成一个普通接口和一个将方法体作为静态函数的类的结合体。接口中只包含声明,类中包含了以静态方法存在的所有实现。因此,如果需要在 Java 类中实现这样一个接口,必须为所有的方法,包括在 Kotlin 中有方法体的方法定义自己的实现。


    2 open、final 和 abstract 修饰符:默认为 final

    Java 的类和方法默认是 open 的,而 Kotlin 中默认都是 final 的。如果想允许创建一个类的子类,需要使用 open 修饰符来标示这个类。此外,需要给每一个可以被重写的属性或方法添加 open 修饰符

    // 代码清单 1.6     声明一个带一个 open 方法的 open 类
    // 这个类是 open 的:其他类可以继承它
    open class RickButton : Clickable {
        // 这个函数是 final 的:不能在子类中重写它
        fun disable() {}
        // 这个函数是 open 的:可以在子类中重写它
        open fun animate() {}
        // 这个函数重写了一个 open 函数并且它本身同样是 open 的
        override fun click() {}
    }
    

    注意,如果重写一个基类或者接口的成员,重写了的成员同样默认是 open 的。如果想改变这一行为,阻止类的子类重新类的实现,可以显式地将重写的成员标注为 final。

    // 代码清单 1.7     禁止重写
    open class RickButton : Clickable {
        // 在这里 "final" 并没有被删减是因为没有 "final" 的 "override" 意味着是 open 的
        final override fun click() {}
    }
    

    open 类和智能转化
    类默认为 final 带来了一个重要的好处就是这使得在大量场景中的智能转化成为可能。智能转换只能在进行类型检查后没有改变过的变量上起作用,这意味着智能转换只能在 val 类型并且没有自定义访问器的类属性上使用。这个前提意味着属性必须是 final 的,否则如果一个子类可以重写属性并定义一个自定义的访问器将会打破智能转换的关键前提。

    在 Kotlin 中,同 Java 一样,可以将一个类声明为 abstract 的,这种类不能被实例化。一个抽象类通常包含一些没有实现并且必须在子类重写的抽象成员。抽象成员始终是 open 的,所以不需要显式地使用 open 修饰符。

    // 代码清单 1.8     声明一个抽象类
    // 这个类是抽象的:不能创建它的实例
    abstract class Animated {
        abstract fun animate()
        
        // 抽象类中的非抽象方法并不是默认 open 的,
        // 但可以标注为 open 的
        open fun stopAnimating() {}
        
        fun animateTwice() {}
    }
    

    表 1.1 列出了 Kotlin 中的访问修饰符。表中的评注适用于类中的修饰符;在接口中,不能使用 final、open 或者是 abstract。接口中的成员始终是 open 的,不能将其声明为 final 的。如果没有函数体就是 abstract 的,但是这个关键字不是必须的。

    表 1.1 类中访问修饰符的意义

    修饰符 相关成员 评注
    final 不能被重写 类中成员默认使用
    open 可以被重写 需要明确地表明
    abstract 必须被重写 只能在抽象类中使用;抽象成员不能有实现
    override 重写父类或接口中的成员 如果没有使用 final 表明,重写的成员默认是开放的

    3 可见性修饰符:默认为 public

    总的来说,Kotlin 的可见性修饰符与 Java 中的类似,同样可以使用 public、protected 和 private 修饰符。但是默认的可见性是不一样的:如果省略了修饰符,声明就是 public 的。
    Java 中的默认可见性——包私有,在 Kotlin 中并没有使用。Kotlin 只把包作为命名空间里组织代码的一种方式使用,并没有将其用作可见性控制。
    作为替代方案,Kotlin 提供了一个新的修饰符 internal,表示“只在模块内部可见”。一个模块就是一组一起编译的 Kotlin 文件。这有可能是一个 IntelliJ IDEA 模块、一个 Eclipse 项目、一个 Maven 或 Gradle 项目或者一组使用调用 Ant 任务进行编译的文件。
    internal 可见性的优势在于它提供了对模块实现细节的真正封装。使用 Java 时,这种封装很容易被破坏,因为外部代码可以将类定义到与代码相同的包中,从而得到访问包私有声明的权限。
    另一个区别是 Kotlin 允许在顶层声明中使用 private 可见性,包括类、函数和属性,这些声明就会只在声明他们的文件中可见。这就是另外一种隐藏子系统实现细节的非常有用的方式。

    表 1.2 Kotlin 的可见性修饰符

    修饰符 类成员 顶层声明
    public(默认) 所有地方可见 所有地方可见
    internal 模块中可见 模块中可见
    protected 子类中可见 ——
    private 类中可见 文件中可见

    Kotlin 禁止从 public 函数去引用低可见的类型。一个通用的规则是:类的基础类型和类型参数列表中用到的所有类,或者函数的签名都有与这个类或函数本身相同的可见性。这个规则可以确保在需要调用函数或者继承一个类时能够始终访问到所有的类型。
    protected 修饰符在 Java 和 Kotlin 中具有不同的行为。在 Java 中,可以从同一个包中访问一个 protected 的成员,但是 Kotlin 不允许这样做。在 Kotlin 中,protected 成员只在类和它的子类中可见。类的扩展函数不能访问它的 private 和 protected 成员

    Kotlin 的可见性修饰符和 Java
    Kotlin 中的 public、protected 和 private 修饰符在编译成 Java 字节码时会被保留。从 Java 代码使用这些 Kotlin 声明时就如同他们在 Java 中声明了同样的可见性。唯一的例外是 private 类:在这种情况下它会被编译成包私有声明(Java 中不能把类声明为 private)
    Java 中并没有直接与 internal 类似的东西。包私有可见性是一个完全不同的东西:一个模块通常会由多个包组成,并且不同模块可能会包含来自同一个包的声明。因此 internal 修饰符在字节码中会变成 public。
    这些 Kotlin 声明和它们的翻版的对应关系解释了为什么有时能从 Java 代码中访问一些不能从 Kotlin 中访问的东西。例如:可以从另一个模块的 Java 代码中访问 internal 类或顶层声明,抑或从同一个包的 Java 代码中访问一个 protected 的成员

    另一个 Kotlin 和 Java 之间可见性规则的区别就是在 Kotlin 中一个外部类不能看到其内部(或者嵌套)类中的 private 成员。


    4 内部类和嵌套类:默认是嵌套类

    像 Java 一样,在 Kotlin 中可以在另一个类中声明一个类。区别是 Kotlin 的嵌套不能访问外部类的实例,除非特别地做出了需求。

    // 代码清单 1.9     声明一个包含可序列化状态的视图
    interface State : Serializable
    
    interface View {
        fun getCurrentState(): State
        fun restoreState(state: State) {}
    }
    

    可以方便地定义一个保存按钮状态的 Button 类。在 Java 中实现如下:

    // 代码清单 1.10        用带内部类的 Java 代码来实现 View
    public class Button implements View {
        @Override
        public State getCurrentState() {
            return new ButtonState();
        }
    
        @Override
        public void restoreState(State state) {
        }
    
        public class ButtonState implements State {
        }
    }
    

    定义了实现 State 接口的 ButtonState 类,并且持有 Button 的特定信息。在 getCurrentState 方法中,创建了这个类的一个新的实例。在真实情况下,需要使用所有需要的数据来初始化 ButtonState。如果视图序列化声明的按钮的状态,就会得到一个 java.io.NotSerializable Exception: Button 异常,因为 Java 中在另一个类中声明一个类时,它会默认变成内部类。这个例子中的 ButtonState 类隐式的存储了它的外部 Button 类的引用。这就解释了为什么 ButtonState 不能被序列化:Button 不是可序列化的,并且它的引用破坏了 ButtonState 的序列化。要修复这个问题,需要声明 ButtonState 类是 static 的。将一个嵌套类声明为 static 会从这个类中删除保卫它的类的隐式引用
    在 Kotlin 中,内部类的默认行为与 Java 的是相反的:

    // 代码清单 1.11     在 Kotlin 中使用嵌套类来实现 View
    class Button : View {
        override fun getCurrentState(): State = ButtonState()
    
        override fun restoreState(state: State) {
        }
    
        // 这个类与 Java 中的静态嵌套类类似
        class ButtonState : State {
        }
    }
    

    Kotlin 中没有显式修饰符的嵌套类与 Java 中的 static 嵌套类一样的。要把它变成一个内部类来持有一个外部类的引用的话需要使用 inner 修饰符。表 1.3 描述了 Java 和 Kotlin 在这个行为上的不同之处:
    表 1.3 嵌套类和内部类在 Java 与 Kotlin 中的对应关系

    类 A 在另一个类 B 中声明 在 Java 中 在 Kotlin 中
    嵌套类(不存储外部类的引用) static class A class A
    内部类(存储外部类的引用) class A inner class A

    在 Kotlin 中引用外部类实例的语法也与 Java 不同。需要使用 this@Outer 从 Inner 类中去访问 Outer 类:

    class Outer {
        inner class Inner {
            fun getOuterReference(): Outer = this@Outer
        }
    }
    

    5 密封类:定义受限的类继承结构

    在 when 表达式中处理所有可能的子类后,必须提供一个 else 分支来处理没有任何分支能匹配的情况

    // 代码清单 1.12        作为接口实现的表达式
    interface Expr
    
    class Num(val value: Int) : Expr
    class Sum(val left: Expr, val right: Expr) : Expr
    
    fun eval(e: Expr): Int =
            when (e) {
                is Num -> e.value
                is Sum -> eval(e.left) + eval(e.right)
                else ->
                    // 必须检查 "else" 分支
                    throw IllegalArgumentException("Unknown expression")
            }
    

    当使用 when 结构来执行表达式的时候,Kotlin 编译器会强制检查默认选项。在上面的例子中,else 分支不能返回一个有意义的值,所以直接抛出一个异常。但是每次都需要添加一个默认分支非常不方便,而且如果添加一个新的子类,编译器就不能发现有地方改变了,如果忘记添加一个新的分支,就会选择默认的选项,这有可能导致潜在的 bug。
    Kotlin 为这个问题提供了一个解决方案:sealed 类。为父类添加一个 sealed 修饰符,对可能创建的子类做出严格的限制。所有的直接子类必需嵌套在父类中。

    // 代码清单 1.13        作为密封类的表达式
    // 将基类标记为密封的
    sealed class Expr {
        // 将所有可能的类作为嵌套类列出
        class Num(val value: Int) : Expr()
        class Sum(val left: Expr, val right: Expr) : Expr()
    }
    
    fun eval(e: Expr) : Int =
            // "when" 表达式涵盖了所有可能的情况,所以不再需要 "else" 分支
            when(e) {
                is Expr.Num -> e.value
                is Expr.Sum -> eval(e.left) + eval(e.right)
            }
    

    如果在 when 表达式中处理所有 sealed 类的子类,就不再需要提供默认分支。sealed 修饰符隐含的这个类是一个 open 类,不在需要显示添加 open 修饰符。密封类不能在类外部拥有子类。
    当在 when 中使用 sealed 类并且添加一个新的子类的时候,有返回值的 when 表达式会导致编译失败,就可以清楚的知道哪里的代码必需要修改。
    在这种情况下,Expr 类有一个只能在内部调用的 private 构造方法。同时,不能声明一个 sealed 接口,因为 Kotlin 编译器不能保证任何人都不能在 Java 中实现这个接口。

    在 Kotlin 1.0 中,sealed 功能是相当严格的。例如,所有的子类必需是嵌套的,并且子类不能创建为 data 类。Kotlin 1.1 解除了这些限制并允许在同一文件的任何位置定义 sealed 类的子类。

    相关文章

      网友评论

        本文标题:Kotlin 类、对象和接口(一)——定义类继承结构

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