Kotlin(五) 类 接口
LanyuanXiaoyao's Blog ヽ(✿゚▽゚)ノ

Kotlin(五) 类 接口

2017-11-04

概述

在Kotlin里面大概最难以转换思维的也就是类写法和继承写法了,但这也是最重要的,在Kotlin里,对于类的定义产生了很多新的概念

简单定义一个类

// Primary constructor
class A constructor(a: Int) {
    val b: Int = a
    var d: Int = 0

    init {
        println(a)
    }

    // Secondary constructor
    constructor(a: Int, c: Int) : this(a) {
        d = c
    }
}

这里简单定义了一个类,和Java一样,Kotlin的类也是使用class关键字进行声明的,但是非常不同的是,Kotlin的类可以在类名后面直接跟一个参数声明,这是怎么回事呢?
其实在Kotlin里面,一个类的构造函数分为两种,一种叫做Primary constructor,另一种叫Secondary constructor,写在类名后面的就是Primary constructor,在类里面使用constructor关键字声明的构造方法就是Secondary constructor
它们之间的关系是怎样的呢?

  1. Primary constructor可以写也可以不写,写了Primary constructor的情况下,Secondary constructor就必须间接实现Primary constructor,如同上面代码中的this(a),如果不写Primary constructor,则这个this(a)就可以省略掉
  2. Primary constructor只能存在一个,也就是写在类名后面,而Secondary constructor可以存在多个
  3. Primary constructor中传进来的参数只能在变量初始化和init代码块里的时候才能使用(参考上面代码)

当然Primary constructorconstructor关键字可以被省略,所以我们的类也可以写成

// Primary constructor
class A(a: Int) {
    ......
}

实际上和Java的类定义类比一下的话,如果没有写Primary constructor,那么看起来就和Java的类写法差不多

class A {
    var a: Int = 0
    
	// 无参构造方法
	constructor()

	// 有参构造方法
    constructor(a: Int) {
        this.a = a
    }
}

构造函数的参数

我们可以像Java一样在类中定义一个变量来接收来自构造函数的参数

class A(a: Int) {
    val b: Int = a
}

当然有更简单的方式

class A(val a: Int)

上面的两个代码是等价的,在第二种方式中实际上也是声明了一个新的变量引用了参数的值

类的继承

先来看被继承的B类的写法,可以发现在class关键字前面多了一个关键字open,这是Kotlin的语法之一,Kotlin默认所有类都不能被继承,只有被open关键字修饰的类才能被继承,类中的方法也是同样,只有被open关键字修饰的方法才能被继承和重写

open class B {
    var a: Int = 0

    // 无参构造方法
    constructor()

    // 有参构造方法
    constructor(a: Int) {
        this.a = a
    }

    // 加上了open关键字的方法才能被继承重写
    open fun sayHello() {
        println("hello")
    }

    // 没有open关键字的方法和Java里面用final修饰了一样
    fun sayHi() {
        println("hi")
    }
}

接下来是子类的写法,在类名之后使用:冒号进行继承的操作,相当于Java的extends关键字

class C : B() {
    // 重写open修饰的方法
    override fun sayHello() {
        super.sayHello()
    }
}

这里的B()相当于实现了B类的构造方法,如果父类存在Primary constructor,那么这里实现的就是父类的Primary constructor,如果父类没有Primary constructor,那么这里实现的就是父类的Secondary constructor,看下面的代码

// 父类有Primary constructor
open class I(a: Int) {

}

class J(a: Int) : I(a) {

}

// 父类没有Primary constructor,有Secondary constructor
open class K {
    constructor(a: Int) {

    }
}

class L(a:Int) : K(a) {

}

抽象类

抽象类也使用和Java相同的abstract,在Kotlin的语法里面,抽象类不仅有抽象方法也有抽象属性,抽象属性和抽象方法也要使用abstract关键字修饰,其中抽象属性不能有初始值,抽象方法不能有方法体

abstract class F {
    // 普通的属性
    val a: Int = 0
    // 抽象的属性,不能有初始值
    abstract val b: Int

    // 抽象方法
    abstract fun sayHello()

    // 普通方法
    fun sayHi() {
        println("hi")
    }
}

继承了抽象类的子类必须实现抽象方法和初始化抽象属性,这里必须使用关键字override来修饰重写的方法和属性,不再是Java里面的一个可有可无的注解了,清晰地标注出了哪些属性哪些方法是从父类重写而来的

class G : F() {
    // 重写抽象属性
    override val b: Int = 0

    override fun sayHello() {
        println("hello")
    }
}

当然我们都知道抽象的东西在子类里面必须实现,所以父类的抽象属性可以在子类的构造方法里面作为一个传进来的参数来实现

class H( override val a: Int) : F() {
    override fun sayHello() {
        println("hello")
    }
}

嵌套类和内部类

Kotlin引入了这么一个新的词,但是这两个东西其实并不是什么新事物,嵌套类和内部类分别对应Java里的静态内部类和普通内部类
其实很多用Java的人都不太明白静态内部类和非静态内部类在使用上有什么蹊跷,大多数人应该都是把静态内部类当成一个临时的类来使用,但是实际上就是这种临时的使用,引申出静态内部类的作用
举个常见的例子,我在写Java Web的时候经常会定义一些“临时”的Bean类来作Gson的解析类来转换前端通过Ajax传来的一些数据,比如:

@RestController
@RequestMapping("/")
public indexController {

    @RequestMapping("query")
    public somethingGet(@RequestBody String json){
        QueryRequestBody temp = GsonUtil.getInstance.from(json, QueryRequestBody.class)
        ......
    }
    
    class QueryRequestBody implement Serializable{
        int id;
        String name;
    }

}

在这里的这个QueryRequestBody就是临时的一个内部Bean类,乍一看好像没有什么问题,但是如果我们要使用这个临时的内部类作序列化操作的时候就会出现问题了,我们可以看到这个临时的QueryRequestBody实现了Serializable接口,但是外部的IndexController类却没有,在Java里面如果在这种情况下直接对QueryRequestBody进行序列化的话,就会报错,错误原因就是IndexController没有实现Serializable接口,这是因为QueryRequestBody作为普通内部类持有了外部类的引用,所以序列化内部类也就必须要序列化这个隐藏的外部类的引用,但是外部类不能被序列化,于是报错,解决的方法也很也能简单,只要把内部类设为静态内部类即可
在Kotlin里面这件事情就被反过来了,先看用Kotlin改写上面的代码

@RestController
@RequestMapping("/")
class IndexController {

    @RequestMapping("query")
    fun somethingGet(@RequestBody json) {
        QueryRequestBody temp = Gson.from (json, QueryRequestBody::java.class)
    }

    class QueryRequestBody(id: Int, name: String) : Serializable

}

在Kotlin里面直接写的内部类默认编译为Java的静态内部类,这样即使实现了Serializable接口也不会因为隐性持有外部类的引用而出现错误了
如果要定义一个普通内部类,在Kotlin里就要使用一个新的关键字inner,被inner关键字修饰过的内部类才会被编译为普通内部类

class Outer {

    class Nested

    inner class Inner {
        // 如果要在内部类获得外部类的引用,使用this@Outer即可
        fun getOuterReference(): Outer = this@Outer
    }

}

密封类

这个概念的用法我还没有找到实际的应用场景,只能做一些简单的描述和讲一些自己的见解
我们都知道在Java里如果一个类如果已经写得非常完整或者不希望被第三方随意调用,那么我们可以使用final关键字修饰这个类,那么这个类将不能被继承,但是这面临一个情况就是,就是我们确实需要在代码里面继承这个类做一些开发,但我又不希望被第三方开发者自己写的类继承,简单说就是我们只想要我们指定的子类继承这个类
在Java里我们知道这是做不到的,因为没有加final关键字的类注定可以被随意继承,但是这件事在Kotlin里面提供了解决方案,于是我们又有了一个新的关键字sealed
sealed关键字修饰的类意味着只能有指定的子类,而其他类不能再继承这个类

sealed class Father {
    abstract fun say()

    class Son1 : Father() {
        override fun say() = println("son1")
    }

    class Son2 : Father() {
        override fun say() = println("son2")
    }
}

// 这里将会报错
class Son3 : Father() {
    override fun say() {
        println("son3")
    }
}

可以看到所有的Father的子类都必须要被嵌套在Father类中,类外的继承都会被禁止
当然!由于这是Kotlin编译器定义的功能,所以不能保证密封类在Java中不被别的类实现

接口

简单定义一个接口

既然是和Java完全兼容的语言,那么在概念上肯定是和Java相同的,所以Kotlin也有接口,使用的关键字是和Java相同的interface

  1. 接口不需要open关键字,默认所有方法都是open的,和Java里接口方法都是public的同理
  2. 另外和Java8里的接口相同,这里接口也可以有非抽象方法
  3. 接口可以有属性,但这个属性是抽象的,不能有普通属性
interface D {
    val a: Int

    fun sayHello()
    fun sayHi() {
        println("hi")
    }
}

实现

实现一个接口,方式和继承一个类相同,使用:冒号来实现接口

class E : D {
    // 实现接口的抽象属性
    override val a: Int = 1

    // 实现接口的方法
    override fun sayHello() {
        println("hello")
    }
}

实现多个接口使用,逗号进行分隔

class P(override val a: Int) : M, N {
    override fun sayHello() {
        println("hello")
    }
}

冲突

多个接口存在相同的抽象属性和抽象方法

子类中会自动合并各个接口里面相同的方法和属性,不存在冲突问题

interface M {
    val a: Int
    fun sayHello()
}

interface O {
    val a: Int
    fun sayHello()
}

class Q : M, O {
    override val a: Int = 1

    override fun sayHello() {

    }
}

多个接口存在相同的普通方法

我们都知道接口里面可以写普通方法,那么多个接口里面有相同的普通方法肿么办呢?
答案是编译器会强制我们重写相同的方法,但是我们可以使用super关键字来决定到底调用哪个接口里面的同名普通方法

interface R {
    val a: Int

    fun sayHi() {
        println("Hi,S")
    }
}

interface S {
    val a: Int

    fun sayHi() {
        println("Hi,R")
    }
}

class U : R, S {
    override val a: Int = 1

    override fun sayHi() {
        super<R>.sayHi()
		super<S>.sayHi()
    }
}

继承的类和实现的接口有相同的普通方法

很遗憾,还是要被强制重写这个方法,决定调用哪个接口或类里的普通方法和上一例相同,使用super关键字

interface R {
    val a: Int

    fun sayHi() {
        println("Hi,S")
    }
}

interface S {
    val a: Int

    fun sayHi() {
        println("Hi,R")
    }
}

open class T {
    open val a: Int = 1

    open fun sayHi() {
        println("Hi,T")
    }
}

class U : R, S, T() {
    override val a: Int = 1

    override fun sayHi() {
        super<T>.sayHi()
        super<R>.sayHi()
    }
}

相似文章

评论