4.类、对象和接口

Kotlin的类和接口与Java的类和接口还是有一点区别的。例如,接口可以包含属性声明。与Java不同,Kotlin的生命默认是public和final的。此外,嵌套的类默认并不是内部类:它们没有包含对其外部类的隐式引用。

对于构造方法来说,简短的主构造方法语法在大多数情况下都工作得很好,但是依然有完整的语法可以让你声明带有重要初始化逻辑的构造方法。对于属性来说也是一样的:简洁的语法非常好用,但是你还是可以方便地定义你自己的访问器实现。

Kotlin的接口和Java 8中的相似:他们可以包含抽象方法的定义以及非抽象方法的实现(与Java 8中的默认方法类似),但他们不能包含任何状态。

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

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

Java允许你创建任意类的子类并重写任意方法,除非显式地使用了final关键字进行标注。但是,对基类进行修改会导致子类不正确的行为,这就是所谓的脆弱的基类问题,因为基类代码的修改不再符合在其子类中的假设。如果类没有提供子类应该怎么实现的明确规则(哪些方法需要被重写及如何重写),当事人可能会有按基类作者预期之外的方式来重写方法的风险。因为不能分析所有的子类,这种情况下基类是如此“脆弱”,任何修改都有可能导致子类出现预期之外的行为改变。为了防止这种问题,Effective Java建议“要么为继承做好设计并记录文档,要么禁止这么做”。这意味着所有没有特别需要在子类中被重写的类和方法应该被显式地标注为final。Kotlin采用了同样的哲学思想。Java的类和方法默认是open的,而Kotlin中默认都是final的。如果你想允许创建一个类的子类,需要使用open修饰符来标示这个类。此外,需要给每一个可以被重写的属性或方法添加open修饰符。

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

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

Kotlin中可见修饰符与Java中的类似,同样可以使用public,protectedprivate修饰符。但是默认的可见性是不一样的:如果省略了修饰符,声明就是public的。Java中的默认可见性——包私有,在Kotlin中并没有使用。Kotlin只把包作为在命名空间里组织代码的一种方式使用,并没有将其用作可见性控制。作为替代方案,Kotlin提供了一个新的修饰符internal,表示“只在模块内部可见”。internal可见性的优势在于它提供了对模块实现细节的真正封装。使用Java时,这种封装很容易被破坏,因为外部代码可以将类定义到与你代码相同的包中,从而得到访问你的包私有声明的权限。

一个通用的规则是:类的基础类型和类型参数列表中用到的所有类,或者函数的签名都有雨这个类或者函数本身相同的可见性。

注意,protected修饰符在Java和Kotlin中有不同的行为。在Java中,可以从同一个包中访问一个protected的成员,但是Kotlin不允许这样做。在Kotlin中可见性规则非常简单,protected成员只能在类和它的子类中可见。同样还要注意的是类的扩展函数不能访问它的privateprotected成员。

Kotlin中publicprotectedprivate修饰符在编译成Java字节码时会被保留。你从Java代码使用这些Kotlin声明时就如同他们在Java中声明了同样的可见性。唯一的例外是private类:在这种情况下它会被编译成包私有声明(在Java中你不能把类声明为private)。对于internal修饰符,Java中并没有直接与之类似的东西。包私有可见性是一个完全不同的东西:一个模块通常会有多个包组成,并且不同模块可能会包含来自同一个包的声明。因此internal修饰符在字节码中会变成public

这些Kotlin声明和它们的Java翻版(或者说它们的字节码实现)的对应关系解释了为什么有时你能从Java代码中访问一些你不能从Kotlin中访问的东西。例如,可以从另一个模块的Java代码中访问internal类或顶层声明,抑或从同一个包的Java代码中访问一个protected的成员(和在Java中一样)。

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

Kotlin中没有显式修饰符的嵌套类与Java中的static嵌套类是一样的。要把它变成一个内部类来持有一个外部类的引用的话需要使用inner修饰符。

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

为父类添加一个sealed修饰符,对可能创建的子类做出严格的限制。所有的直接子类必须嵌套在父类中。在Kotlin1.0中,sealed功能时相当严格的。例如,所有的子类必须是嵌套的,并且子类不能创建为data类。Kotlin1.1解除了这些限制并允许在同一文件的任何位置定义sealed类的子类。

在Java中一个类可以声明一个或多个构造方法。Kotlin也是类似的,只是做出了一点修改:区分了主构造方法(通常是主要而简洁的初始化类的方法并且在类体外部声明)和从构造方法(在类体内部声明)。同样也允许在初始化语句块中添加额外的初始化逻辑。

可以像函数参数一样为构造方法参数声明一个默认值。如果所有的构造方法参数都有默认值,编译器会生成一个额外的不带参数的构造方法来使用所有的默认值。这可以让Kotlin使用库时变得更简单,因为可以通过无参构造方法来实例化类。

如果你的类具有一个父类,主构造方法同样需要初始化父类。可以通过在基类列表的父类引用中提供父类构造方法参数的方式来做到这一点:

open class User(val nickname: String) {...}

class TwitterUser(nickname: String) : User(nickname) {...}

open class Button

class RadioButton: Button()

如果想要确保你的类不被其他代码实例化,必须把构造方法标记为private

class Secretive private constructor() {}

多个构造方法

open class View {
    constructor(ctx: Context) {
        // ...
    }
    constructor(ctx: Context, attr: AttributeSet) {
        // ...
    }
}

class MyButton: View {
    constructor(ctx: Context) : super(ctx) {
        // ...
    }
    constructor(ctx: Context, attrs: AttributeSet) : super(ctx, attrs) {
        // ...
    }
}

可以像Java中一样,使用this()关键字,从一个构造方法中调用另一个构造方法。

class MyButton: View {
    constructor(ctx: Context) : this(ctx, MY_STYLE) {
        // ...
    }
    constructor(ctx: Context, attrs: AttributeSet) : super(ctx, attrs) {
        // ...
    }
}

如果类没有主构造方法,那么每个从构造方法必须初始化基类或者委托给另一个这样做了的构造方法。

在Kotlin中,接口可以包含抽象属性声明。

interface User {
    val nickname: String
}

class PrivateUser(override val nickname : String) : User

除了抽象属性声明外,接口还可以包含具有getter和setter的属性,只要他们没有引用一个支持字段(支持字段需要在接口中存储状态,这是不允许的)

interface User {
    val email : String
    val nickname : String
        get() = email.substringBefore('@') //属性没有支持字段,结果值在每次访问时通过计算得到
}

通过getter和setter访问支持字段

class TTTUser(val name: String) {
    var address: String = "unspecified"
        set(value: String) {
            println("""Address was changed for $name:"$field" -> "$value".""".trimIndent())
            field = value
        }
        get() = field + " hahaha"
}

修改访问器的可见性

class LengthCounter {
    var counter: Int = 0
        private set // 不能在类外部修改这个属性

    fun addWord(word: String) {
        counter += word.length
    }
}

==表示相等性,在Java中,可以使用==运算符来比较基本数据类型和引用类型。如果应用在基本数据类型上,Java的==比较的是值,然而在引用类型上==比较的是引用。在Kotlin中,==运算符是比较两个对象的默认方式:本质上说它就是通过调用equals来比较两个值的。因此,如果equals在你的类中被重写了,你能够很安全地使用==来比较实例。要想进行引用比较,可以使用===运算符,这与Java中的==比较对象引用的效果一模一样。

如果想要你的类是一个方便的数据容器,需要重写这些方法:toStringequalshashCode。在Kotlin中不必再去生成这些方法了。如果为你的类添加data修饰符,必要的方法将会自动生成好。为了让使用不可变对象的数据类变得更容易,Kotlin编译器为他们多生成了一个方法:一个允许copy类的实例的方法,并在copy的同时修改某些属性的值。创建副本通常是修改实例的好选择:副本有着单独的生命周期而且不会影响代码中引用原始实例的位置。

class Client(val name:String, val postalCode: Int) {
    ...
    fun copy(name: String = this.name, postalCode : Int = this.postalCode) = Client(name, postalCode)
}
val bob = Client("Bob", 1234)
println(bob.copy(postalCode = 5678))

装饰器模式的本质就是创建一个新类,实现与原始类一样的接口并将原来的类的实例作为一个字段保存。与原始类拥有同样行为的方法不用被修改,只需要直接转发到原始类的实例。这种方式的缺点是需要相当多的样板代码。

class DelegatingCollection<T>(innerList: Collection<T> = ArrayList<T>()) : Collection<T> by innerList {}
class CountingSet<T>(val innerSet: MutableCollection<T> = HashSet<T>()) : MutableCollection<T> by innerSet {
    var objectsAdded = 0
    override fun add(element: T): Boolean {
        println("add")
        objectsAdded ++
        return innerSet.add(element)
    }

    override fun addAll(elements: Collection<T>): Boolean {
        println("addall")
        objectsAdded+=elements.size
        return innerSet.addAll(elements)
    }
}

对象声明是定义单例的一种方式,伴生对象可以持有工厂方法和其他与这个类有关,但在调用时并不依赖类实例的方法。它们的成员可以通过类名来访问。对象表达式用来替代Java的匿名内部类。

在面向对象系统设计中一个相当常见的情形就是只需要一个实例的类。在Java中,这通常通过单例模式来实现:定义一个使用private构造方法并且用静态字段来持有这个类仅有的实例。Kotlin通过使用对象声明功能为这一切提供了最高级的语言支持。对象声明将类声明与该类的单一实例声明结合到了一起。对象声明通过object关键字引入。一个对象声明可以非常高效地以一句话来定义一个类和一个该类的变量。与类一样,一个对象声明也可以包含属性、方法、初始化语句块等的声明。唯一不允许的就是构造方法(包括主构造方法和从构造方法)。与普通类的实例不同,对象声明在定义的时候就立即创建了,不需要在代码的其他地方调用构造方法。

Kotlin中的对象声明被编译成了通过静态字段来持有它的单一实例的类,这个字段名字始终都是INSTANCE。因此,要从Java代码使用Kotlin对象,可以通过访问静态的INSTANCE字段。

Kotlin中的类不能拥有静态成员:Java的static关键字并不是Kotlin语言的一部分。作为替代,Kotlin依赖包级别函数(在大多数情形下能够替代Java的静态方法)和对象声明(在其他情况下替代Java的静态方法,同事还包括静态字段)。在大多数情况下,还是推荐使用顶层函数。但是顶层函数不能访问类的private成员。因此如果需要写一个可以在没有类实例的情况下调用但是需要访问类内部的函数,可以将其写成那个类中的对象声明的成员。

在类中定义的对象之一可以使用一个特殊的关键字来标记:companion,如果这样做,就获得了直接通过容器类名称来访问这个这个对象的方法和属性的能力,不需要显示的指明对象的名称。最终的语法看起来非常像Java中的静态方法调用。

class KKUser private constructor(val nickname: String) {
    companion object {
        fun newSubscirbeingUser(email: String) = KKUser(email.substringBefore("@"))
        fun newFacebookUser(id : Int) = KKUser("id:$id")
    }
}

val subscribingUser = KKUser.newSubscirbeingUser("a@b.com")
println(subscribingUser.nickname)
val facebookUser = KKUser.newFacebookUser(1)
println(facebookUser.nickname)

伴生对象是一个声明在类中的普通对象。它可以有名字,实现一个接口或者有扩展函数或属性。在大多数情况下,通过包含伴生对象的类的名字来引用伴生对象,所以不必关心它的名字。但是如果需要你也可以指明。如果你省略了伴生对象的名字,默认的名字将会分配为Companion

类的伴生对象会同样被编译成常规对象:类中的一个引用了它的实例的静态字段。如果伴生对象没有命名,在Java代码中它可以通过Companion引用来访问,如果伴生对象有名字,那就用这个名字代替Companion。但是你也许要和这样的Java代码一起工作,它需要勒种的成员是静态的。可以在对应的成员上使用@JvmStatic注解来达到这个目的。如果你想声明一个static字段,可以再在一个顶层属性或者声明在object中的属性上使用@JvmField注解。这些功能专门为互操作性而存在。

如果类C有一个伴生对象,并且在C.Companion上定义了一个扩展方法func,可以通过C.func()来调用它。为了能够为你的类定义扩展,必须在其中声明一个伴生对象,即使是空的。

object关键字不仅仅能用来声明单例式的对象,还能用来声明匿名对象。匿名对象替代了Java中匿名内部类的用法。

val listener = object : MouseAdapter() {
    override fun mouseClicked(e : MouseEvent) { ... }
}

与Java匿名内部类只能扩展一个类或实现一个接口不同,Kotlin的匿名对象可以实现多个接口或者实现接口。与对象声明不同,匿名对象不是单例的。每次对象表达式被执行都会创建一个新的对象实例。与Java中的匿名类一样,在对象表达式中的代码可以访问创建它的函数中的变量。但是与Java不同,访问并没有被限制在final变量,还可以在对象表达式中修改变量的值。

对象表达式在需要匿名对象中重写多个方法时是最有用的。如果只需要实现一个单接口的方法,可以将你的实现写作lambda并依靠SAM转换。

results matching ""

    No results matching ""