第四章(类、对象和接口)

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

Kotlin中的接口

Kotlin的接口与Java 8中的相似:它们可以包含抽象方法的定义以及非抽象方法的实现。与Java 8不同的是,Java 8中需要你在这样的实现上标注default关键字,而Kotlin没有特殊的注解:只需要提供一个方法体。示例:

interface Clickable {
    fun click()
    fun showOff() { //带默认方法的实现
        println("Clickable!")
    }
}

上面是接口的基本定义方式,接下来我们了解下如何实现接口?Kotlin在类名后面使用冒号来代替了Java中的extends和implements关键字。和Java一样,一个类可以实现任意多个接口,但是只能继承一个类。与Java中的 @Override 注解类似,Kotlin中使用 override 修饰符来标注被重写的父类或者接口的方法属性。与Java不同的是,在Kotlin中使用 override 修饰符是强制要求的。 

open class TextView : Clickable {

    override fun click() {
        // 此方法必须实现
    }

//    override fun showOff() { //接口有默认实现,这里可以不用再实现此方法
//        super.showOff()
//    }

}

class Button : TextView() {
    // 需要注意的是,Kotlin中继承与实现接口都是用的冒号,区别在于继承类时,
    //如果父类有主构造方法,则需要在类名后添加括号及主构造函数中的参数,如果没有主构造方法,
    //则必须加上一个默认的空括号来表示。
    //如上面的TextView(),而继承接口只需要直接写接口名称即可。关于主构造函数下面有说
}

那么如果我们还有一个接口Focusable,该接口下有一个和Clickable接口中的showOff一样的方法,当TextView同时继承这两个接口的时候,到底该如何实现这两个方法呢?

open class TextView : Clickable,Focusable {

  /**
  * 如果同样的继承成员有不止一个实现,那么必须最少提供一个显示实现
  * 即super<Clickable>.showOff()
        super<Focusable>.showOff()两行中最少要调用其中一行
  */
    override fun showOff() {
      // 通过下面代码显示的实现两个showOff方法
        super<Clickable>.showOff()
        super<Focusable>.showOff()
    }
    
    override fun click() {
        // 此方法必须实现
    }
}

修饰符

open

Java中允许你创建任意类的子类并重写任意方法,除非显示地使用了 final 关键字进行标注。而在Kotlin中,默认都是 final 的,也就是默认都是不能被继承或者重写的。如果你想允许创建一个类的子类,则需要使用 open 修饰符来标识这个类。此外,需要给每一个可以被重写的属性或方法添加 open 修饰符。

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

open class RichButton:Clickable{ //这个类是open的,其他类可以继承它
    override fun click() {//这个函数重写了一个open函数并且它本身也是oepn的
        
    }
    fun disable(){} //这个函数是final的:不能在子类中重写它
    
    open fun animate(){} //这个函数是open的,可以在子类中重写它
}

abstract

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

abstract class Animated{ //这个类是抽象的:不能创建它的实例
    abstract fun animate()//这个函数是抽象的:他没有实现,必须被子类重写

    open fun stopAnimating(){ //抽象类中的非抽象函数并不是默认open的,但是可以标注为open的

    }

    fun animateTwice(){ // 不是open的,不能被重写

    }
}

类中访问修饰符的意义

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

可见性修饰符(public等)

Kotlin中的可见性修饰符与Java中的类似。同样可以使用public、protected和private修饰符。但是默认的可见性是不一样的:如果省略了修饰符,声明默认就是public的。
Java中的默认可见性——包私有,在Kotlin中并没有使用。Kotlin只把包作为在命名空间里组织代码的一种方式使用,并没有将其用作可见性控制。
作为替代方案,Kotlin提供了一个新的修饰符,internal ,表示“只在模块内部可见”。一个模块就是一组一起编译的Kotlin文件。这有可能是一个Intellij IDEA模块、一个Eclipse项目、一个Maven或Gradle项目或者一组使用调用Ant任务进行编译的文件。

Kotlin的可见性修饰符

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

示例:下面giveSpeech函数的每一行都试图违反可见性规则。在编译时就会发生错误。

 internal open class TalkativeButton:Focusable{
        private fun yell() = println("Hey!")
        protected  fun whisper() = println("talk")
    }
    
 fun TalkativeButton.giveSpeech(){ //错误:"public"成员暴露了其"internal"接收者类型TalkativeButton
      yell() //错误:不能访问"yell()":它在"TalkativeButton"中是"private"的
     whisper() //错误:不能访问"whisper()":它在"TalkativeButton"中是"protected"的
  }

要解决上面例子中的问题,既可以把giveSpeech函数改为internal的,也可以把类改成public的,当然,如果要访问“yell与whisper方法,则需要将这两个方法改为public或者internal的。

内部类和嵌套类

和Java一样,在Kotlin中可以在另一个类中声明一个类。区别是Kotlin的嵌套类不能访问外部类的实例,除非你特别的做出了要求。
在Kotlin中,没有显示修饰符的嵌套类与Java中的static嵌套类时一样的。要把它变成一个内部类来持有一个外部类的引用的话需要使用 inner 修饰符。在Kotlin中引用外部类示例的语法也与Java不同。需要使用this@OuterClass从InnerClass去访问Outerclass类。

嵌套类和内部类在Java与Kotlin中的对应关系

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

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

当使用when结构来执行表达式的时候,Kotlin编译器会强制检查默认选项。例如:假设父类Expr有两个子类:表示数字的Num,以及表示两个两个表达式之和的SUm。在when表达式中处理所有可能的子类固然很方便,但是必须提供一个else分支来处理没有任何其他分支能匹配的情况:

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.right)+ eval(e.left)
    else -> throw IllegalArgumentException("Unknown expression")
}

上面示例中,else条件中因为不能返回一个有意义的值,所以直接抛出了一个异常。如果你添加了一个新的子类,编译器并不能发现有地方改变了。如果你忘记了添加一个新分支,就会选择默认的else分支,这有可能导致潜在的bug。
Kotlin为这个问题提供了一个解决方案:sealed类。为父类添加一个sealed修饰符,对可能创建的子类做出严格的限制。所有的直接子类必须嵌套在父类中。

sealed class Expr{
    class Num(val value:Int):Expr()
    class Sum(val left:Expr,val right:Expr):Expr()
}

fun eval(e:Expr):Int = when(e){
    is Expr.Num -> e.value
    is Expr.Sum -> eval(e.right)+ eval(e.left)
}

如果你在when表达式中处理所有sealed类的子类,你就不再不要提供默认的分支。注意,sealed修饰符隐含的这个类是一个open类,你不再需要显示地添加open修饰符。当你sealed类(Expr)的子类中添加一个新的子类的时候,有返回值的when表达式会编译失败。需要将新的子类添加一个新分支。

声明构造方法

Java中一个类可以声明一个或多个构造方法。Kotlin也是类似的,知识做出了一点修改:区分了主构造方法(通常是主要而简洁的初始化类的方法,并且在类体外部声明)和从构造方法(在类体内部声明)。

主构造方法

主构造方法通常直接在声明类的时候在类名后面加括号,然后在括号内声明相应的参数来进行声明,而被括号围起来的语句块就叫做主构造方法。代码如下:

//以下代码即表示声明了一个带有nickname参数的主构造方法
class User(val nickname: String)

上面代码是声明一个主构造方法的最简单的方式,但是其实上面代码Kotlin最终会帮我们进行处理,我们看下它是怎样工作的:

class User constructor(_nickname: String) {
    val nickname: String

    init {
        nickname = _nickname
    }
}

在上面例子中,出现了 constructor 和 init 两个新的关键字。constructor关键字用来开始一个主构造方法或者从构造方法的声明。init关键字用来引入一个初始化语句块。因为主构造方法有语法限制,不能包含初始化代码,这就是为什么要使用初始化语句块的原因。如果你原因,也可以在一个类中声明多个初始化语句块。

当然,如果像上面代码中初始化语句块中仅仅只是一个赋值操作,那么我们可以不需要把 nickname = _nickname这行赋值操作的代码放在初始化语句块中,因为它可以与nickname属性的声明结合。如果主构造方法没有注解或可见性修饰符,同样可以把constructor关键字省略。最终代码可以简化成如下:

class User constructor(_nickname: String) {
  val nickname = _nickname
}

以上三种主构造方法声明方式都是一样的,只不过第一种使用了最简洁的语法。

主构造方法可以像函数参数一样为构造方法参数声明一个默认值:

class User(val nickname:String = "xiaochao",val isSubscribed:Boolean = true)

fun main(args: Array<String>) {
    val user1 = User("hello")
    val user2 = User(isSubscribed = false)
    val user3 = User("kotlin",false)
}

如果你的类具有一个父类,主构造方法同样需要初始化父类:

open class User(val nickname: String)

class MyUser(nickname:String) :User(nickname){
    //...
}

如果没有给一个类声明任何构造方法,将会生成一个不做任何事的默认构造方法,如果有子类继承了该类,那么子类必须显示地调用该类的默认构造方法,即使它没有任何的参数 :

open class Button

class RadioButton: Button() {
    //...
}

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

class User private constructor(val name:String){
    //...
}

从构造方法

上面我们说了当一个类没有定义任何构造方法的时候,Kotlin会自动为其生成一个默认的主构造方法,所以在子类继承该类时也总是需要显示的调用父类的默认构造方法。而如果当一个类中定义了从构造方法,而没有定义主构造方法的时候,那么Kotlin不会在自动为其生成默认的主构造方法。当子类继承的时候则需要显示的调用父类的从构造方法。代码如下:

open class User{
    constructor(userName:String){
        //...
    }
    constructor(userName: String,age:Int){
        //...
    }
}

class MyUser:User{
    constructor(userName:String):super(userName) // 调用父类的从构造方法
    constructor(userName: String,age:Int):super(userName,age) // 调用父类的从构造方法
}

//当然,这里也可以像之前一样通过在继承的时候就显示调用父类的从构造方法,如下
//class MyUser(userName: String) : User(userName) {
//  
//}

从上面我们看到可以在子类内部通过使用super()关键字调用对应的父类的构造方法。同时,也可以像Java中一样,通过使用this()关键字,从一个构造方法中调用你自己类的另一个构造方法。代码如下:

class MyUser : User {
    //通过this调用自己的两个参数的构造方法
    constructor(userName: String) : this(userName, 0)
    
    constructor(userName: String, age: Int) : super(userName, age)
}

通过getter或setter访问支持字段(field)

class User(val name:String){
    var address: String = "unspecified"
        set(value) {
            println("$field --->${value.trimIndent()}")
            field = value
        }
}

fun main(args: Array<String>) {
    val user = User("xiaochao")
    user.address = "shanghai"
    user.address = "beijing"
}
>>> unspecified --->shanghai
>>> shanghai --->beijing

上面代码实现了一个既可以存储值又可以在值被访问和修改时提供额外逻辑的属性。要支持这种情况,需要能够从属性的访问器中访问它的支持字段(field)。在setter的函数体中,使用了特殊的标识符field来访问支持字段的值。在getter中,只能读取值;而在setter中,既能读取它也能修改它。

有支持字段的属性和没有的有什么区别?
访问属性的方式不依赖于它是否含有支持字段。如果你显示地引用或者使用默认的访问器实现,编译器会为属性生成支持字段。如果你提供了一个自定义的访问器实现并且没有使用 field ,支持字段将不会被呈现出来。

有时候不需要修改访问器的默认实现,但是需要修改它的可见性。则代码示例如下:

class LengthCounter {
    var counter: Int = 0
        private set

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

fun main(args: Array<String>) {
    val lengthCounter = LengthCounter()
    lengthCounter.addWord("HelloWorld!")
    println(lengthCounter.counter)
}

>>> 11

数据类data

数据类用 data 关键字修饰,数据类表示已经被Kotlin重写了equal、hashCode、toString方法的类。示例如下:

class Student(val name:String,val age:Int)

data class DataStudent(val name:String ,val age:Int)

fun main(args: Array<String>) {
    val xiaoming1 = Student("xiaoming",20)
    val xiaoming2 = Student("xiaoming",20)
    val xiaomingSet = hashSetOf(xiaoming1,xiaoming2)
    println(xiaoming1.toString())
    println(xiaoming1==xiaoming2)
    println("size = ${xiaomingSet.size} ${xiaomingSet.contains(Student("xiaoming",20))}")
    println("----------------------------")

    val dataxiaoming1 = DataStudent("xiaoming",20)
    val dataxiaoming2 = DataStudent("xiaoming",20)
    val dataxiaomingSet = hashSetOf(dataxiaoming1,dataxiaoming2)
    println(dataxiaoming1.toString())
    println(dataxiaoming1==dataxiaoming2)
    println("size = ${dataxiaomingSet.size} ${dataxiaomingSet.contains(DataStudent("xiaoming",20))}")
}

>>>chapter04.Student@3fee733d
>>>false
>>>size = 2 false
>>>----------------------------
>>>DataStudent(name=xiaoming, age=20)
>>>true
>>>size = 1 true

数据类和不可变性:copy()方法

虽然数据类的属性并没有要求一定是val — 同样可以使用var — 但还是强烈推荐只使用val属性,让数据类的示例不可变。如果你想使用这样的示例作为HashMap或者类似容器的键,这会是必须的要求,因为如果不这样,被用作键的对象在加入容器后被修改了,容器可能会进入一种无效的状态。不可变对象同样更容易理解,特别是在多线程中:一旦一个对象被创建出来,它会一直保持初始状态,也不用担心在你的代码工作时其他线程修改了对象的值。

为了让使用不可变对象的数据类变得更容易,Kotlin编译器为它们多生成了一个方法:一个允许copy类的实例的方法,并在copy的同时修改了某些属性的值。创建副本通常是修改实例的好选择:副本有着单独的生命周期而且不会影响代码中引用原始实例的位置。下面就是手动实现copy方法后看起来的样子:

class Student(val name:String,val age:Int){
    fun copy(name:String = this.name,age:Int = this.age) = Student(name,age)
}

fun main(args: Array<String>) {
    val student = Student("xiaoming",22)
    println(student.copy(age = 23).age)
}
>>> 23

这就是data修饰符是如何让值对象类的使用更方便的原因。

类委托:by关键字

在开发中,我们可能常常需要向其他类添加一些行为,即使它并没有被设计为可扩展的。一个常用的实现方式以装饰器模式闻名。这种模式的本质就是创建一个新类,实现与原始类一样的接口并将原来的类的实例作为一个字段保存。与原始类拥有同样行为的方法不用被修改,只需要直接转发到原始类的实例。

这种方式的一个缺点是需要相当多的样板代码。例如,下面就是你需要多少代码来实现一个简单得如Collection的接口的装饰起,即使你不需要修改任何的行为:

class DelegatingCollection<T> : Collection<T> {
    private val innerList = arrayListOf<T>()
    override val size: Int
        get() = innerList.size

    override fun contains(element: T): Boolean = innerList.contains(element)

    override fun containsAll(elements: Collection<T>): Boolean = innerList.containsAll(elements)

    override fun isEmpty(): Boolean = innerList.isEmpty()

    override fun iterator(): Iterator<T> = innerList.iterator()
}

Kotlin中将委托作为一个语言级别的功能做了头等支持,无论什么时候实现一个接口,你都可以使用 by 关键字将接口的实现委托到另一个对象。下面通过使用by关键字来实现上面同样功能代码:

class DelegatingCollection<T>(innerList: Collection<T> = ArrayList<T>())
    : Collection<T> by innerList

现在,当你需要修改某些方法的行为时,你可以重写它们,这样你的方法就会被调用而不是使用生成的方法。可以保留感到满意的委托给内部的实例中的默认实现。

class CountingSet<T>(val innerList: MutableCollection<T> = HashSet<T>()) : MutableCollection<T> by innerList {
    var objectsAdd = 0
    override fun add(element: T): Boolean {
        objectsAdd++
        return innerList.add(element)
    }

    override fun addAll(elements: Collection<T>): Boolean {
        objectsAdd += elements.size
        return innerList.addAll(elements)
    }
}

fun main(args: Array<String>) {
    val cset = CountingSet<Int>()
    cset.addAll(listOf(1, 2, 2))
    println("${cset.objectsAdd}     ${cset.size}")
}

>>> 3     2

object:将声明一个类与创建一个实例结合

object 关键字使用主要有三种场景:

  • 定义单例
  • 替代Java的匿名内部类
  • 配合companion创建伴生对象

对象声明:通过object创建单例

object SingleInstance {
    var name: String? = null
    fun initName() {
        name = "hello world"
    }
}

fun main(args: Array<String>) {
    val a = SingleInstance
    val b = SingleInstance
    SingleInstance.initName()
    b.initName()
    println(a === b)
}
>>> true

与类一样,一个对象声明也可以包含属性、方法、初始化语句块等的声明。唯一不允许的就是构造方法(包括主构造方法和从构造方法)。与普通的实例不同,对象声明在定义的时候就立即创建了。对象声明同样可以继承自类和接口。

用object替代Java的匿名内部类

interface Click{
    fun onClick()
    fun onLongClick()
}

fun main(args: Array<String>) {
    val clickClistener = object :Click{
        override fun onClick() {
        }

        override fun onLongClick() {
        }
    }
}

当使用object来表示匿名内部类时,与对象声明不同,匿名对象不是单例的,每次对象表达式被执行都会创建一个新的对象实例。

伴生对象

Kotlin中的类不能拥有静态成员;Java中的static关键字并不是Kotlin语言的一部分。作为替代,Kotlin依赖包级别函数和对象声明,在大多数情况下,还是推荐使用顶层函数,但是顶层函数不能访问类的private成员。这时候就可以考虑通过伴生对象来解决这个问题。在Kotlin中,伴生对象通过 companion 来标记。语法如下:

class A {
    companion object {
        const val name = "Hello world"
        fun bar() {
            println("Companion object called")
        }
    }
}

fun main(args: Array<String>) {
    A.bar()
    A.name
}

伴生对象是一个声明在类中的普通对象,它可以有名字,实现一个接口或者有扩展函数或属性。

interface Click{   
        fun onClick()
    fun onLongClick()
}

class A {
    companion object Inner : Click { // 实现接口
        override fun onClick() {
        }

        override fun onLongClick() {
        }

        const val name = "Hello world"
        fun bar() {
            println("Companion object called")
        }
    }
}

fun A.Inner.kuozhan() { //扩展函数
    println("这是伴生对象的扩展函数")
}

fun main(args: Array<String>) {
    A.bar()
    A.name
    A.Inner.bar()
    A.Inner.name
    A.Inner.kuozhan()
}

总结

  1. Kotlin的接口与Java的相似,但是可以包含默认实现和属性(Java从第8版才开始支持)。
  2. 所有的声明默认都是 final 和 public 的。
  3. 要想使声明不是 final 的,将其标记为 open 。
  4. internal 声明在同一模块中可见。
  5. 嵌套类默认不是内部类。使用 inner 关键字来声明内部类,存储外部类的引用。
  6. sealed 类的子类只能嵌套在自身的声明中(Kotlin1.1允许将子类放置在同一文件的任意地方)。
  7. 初始化语句块和从构造方法为初始化类实例提供了灵活性。
  8. 使用 field 标识符在访问器方法体中引用属性的支持字段。
  9. 数据类提供了编译器生成的equal、hashCode、toString、copy和其他方法。
  10. 类委托帮助避免在代码中出现许多相似的委托方法。
  11. 对象声明是Kotlin中定义单例类的方法。
  12. 伴生对象(与包级别函数合属性一起)替代了Java静态方法和字段定义。
  13. 伴生对象与其他对象一样,可以实现接口,也可以拥有扩展函数和属性。
  14. 对象表达式是Kotlin中针对Java匿名内部类的替代品,并增加了诸如实现多个接口的能力和修改在创建对象的作用域中定义的变量的能力等功能。

猜你喜欢

转载自www.cnblogs.com/xxiaochao/p/11497252.html