Kotlin进阶-类型构造

310 阅读7分钟

阅读本文解决什么问题?

解决很多人写kotlin代码的时候 主要依赖ide提示来完成代码的问题,这是一个不太好的习惯,虽然可以编译成功,甚至功能看起来正常,但是容易埋坑,一定要知道为什么ide 提示你这样做。

另外kotlin目前还是和java在一起混合使用的比较多。本文会介绍kotlin的部分注解,让java可以顺利按照java的想法调用kotlin的代码。

类的构造器


class Person constructor(var age: Int, var name: String)

//很kotln的写法  如果参数加上var 或者val 那就是属性 ,不加的话 就仅仅是个参数了
//例如这里params 就是个参数 但是age 和name就是属性

class Person2(var age: Int, var name: String, params: Int)

//params 在init 这个主构造器里面是可见的
class Person3(var age: Int, var name: String, params: Int) {
    var pvalue: Int

    init {
        pvalue = params
    }
}


//这里面构造器123 3个加起来就是这个person4 初始化的时候构造函数里面的调用了
//init块 是可以多个的 反正最后都会合并
class Person4(var age: Int, name: String) {
    var name: String

    //构造器1
    init {
        this.name = name

    }
    //这个也算 构造器2
    val firstName = name.split(" ")[0]
    //构造器3
    init {

    }
}

当然你可以选择不定义主构造器,但是这么做非常不好,这会导致你的类初始化路径出现多条。 这是非常不推荐的设计模式,想想你平常java代码里面有没有这么写。如果有 记得改正。

可以看看kotlin中不推荐的写法,但是可以编译通过。

同样的kotlin的这种能力还可以提供给java

在java中调用

当你如果把注解取消掉的时候,那么java代码就无法调用Person10(int age) 这个构造函数了。 这个地方要细细体会一下。

接口代理

这里需要你对代理模式有一定了解。 这里就不再说了,大家可以看一下最后一种写法 可以省略多少代码。

interface Api {
    fun a()
    fun b()
    fun c()
}

class ApiImpl : Api {
    override fun a() {
        TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
    }

    override fun b() {
        TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
    }

    override fun c() {
        TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
    }

}

class ApiWrapper(private val api: Api) : Api {
    override fun a() {
        api.a()
    }

    override fun b() {
        api.b()
    }

    override fun c() {
        println("ccccc")
        api.c()
    }

}

//这种写法 就相对于上面的写法 要简单许多
class ApiWrapper2(private val api: Api) : Api by api {
    override fun c() {
        println("ccccc")
        api.c()
    }
}

整体来说 接口代理用的不是特别多,大家知道有这么个语法糖就好

属性代理

属性代理用的就比较多了。

接口Lazy的实例代理了对象Person的实例的属性 firstName的getter

再比如说 可以很方的用observable来监听某个属性的变化。真是超级好用。。

class StateManager {
    var state: Int by Delegates.observable(0) { property, oldValue, newValue ->
        println("state changed $oldValue   ->$newValue")
    }
}

延迟初始化

lateinit 其实不建议用,因为lateinit就会导致你kotlin的空安全失效了。 这就是lateInit最恶心的地方。 虽然大部分人看到的kotlin android代码里面 activity 里面的那些 控件 都是用lateinit来做 声明。

虽然kotlin 1.2 中提供了一个api来处理 是否初始化的逻辑,但是也不建议使用。

这里推荐使用lazy 来做初始化

private val nameTextView by lazy{
    findViewById<TextView>(R.id.textview)
}

而且 声明和初始化放在一起 是最好的写法。

kotlin单例与java 混用

kotlin中的单例可能是kotlin中最好的一个语法糖了,使用起来比java 那堆东西简单多了。但是往往我们在写的时候还要给java调用一下。

毕竟我们现在主要的工程都是kotlin与java 混合在一起的。

object Singleton {
    var x: Int = 2
    fun y() {

    }
}

看下在java中如何调用

在这里其实可以看出来,在kotlin中实际上是没有静态成员这个概念的,因为kotlin的野心极大, 他想做跨平台的语言,其他语言往往是没有静态成员这个概念的

我们甚至还可以把get和set去掉

object Singleton {
    @JvmField
    var x: Int = 2

    @JvmStatic
    fun y() {

    }
}

这里再说一个伴生对象的概念。

比如说我们有时候java代码会这么写:

class Foo{
    public static void y(){}
}

在kotlin中 就可以这么写

class Foo{
    companion object{
        @JvmStatic fun y(){}
    }
}

这个就叫做伴生对象了。

另外就是

kotlin中的object 你就把他当做是一个java中的饿汉单例,仅此而已

内部类

这个地方一定搞清楚差别,因为普通的内部类 会持有外部类的引用,容易引发内存泄漏。

java的写法

public class Outer{
    class Inner{}
    static class StaticInner{}
}

再看看kotlin中的写法

class Outer{
    inner class Inner //这个是非静态的内部类
    class StaticInner //这个是静态的内部类
}

kotlin使用内部类和在java中也是一样的。

kotlin中枚举类的使用

枚举类在kotlin中的使用 与java是基本一致的。只有少数部分不一样。 例如 kotlin中是可以为枚举定义扩展的

enum class State:Runnable{
    Idle,Busy,Error;
    override fun run() {
        println("every state run")
    }

}

//取下一个枚举的值
fun State.next():State{
    return State.values().let {
        val nextOrdinal=(ordinal+1)%it.size
        it[nextOrdinal]
    }
}

 println(State.Idle.next())
    println(State.Busy.next())
    println(State.Error.next())

应该就可以理解了。当然严格意义上来说 这是属于kotlin中 扩展函数的能力了。

kotlin中 枚举多数是和when表达式在一起使用的。

val state=State.Error
    println(when(state){
        State.Idle->{"get idle"}
        State.Busy ->{"get busy"}
        State.Error->{"get error"}
    })

既然是枚举,所以肯定是可以穷尽的对吧,所以当他和when表达式在一起的时候,他是可以省略else分支的。

枚举的区间

比方说扑克牌。

enum class Poker{
    J,Q,K,A
}

可以直接使用区间来判断他们

 val pokerRange=Poker.J..Poker.K
    println(Poker.J in pokerRange)
    println(Poker.Q in pokerRange)
    println(Poker.K in pokerRange)

密封类

他有3个特点; 1.密封类是一个抽象类 2.密封类的子类必须与密封类定义在同一个文件中。 3.密封类的子类个数是有限的。

这里的第三点很多人无法理解,其实很好理解,因为你密封类的子类必须和密封类在一个文件中,所以实际上密封类的子类是不可以无限扩展的,那你当然子类个数就是有限的了、

为什么外部无法继承密封类?

来看个例子:

sealed class PlayerState
  constructor(val state:Int,val message:String)

object Idle:PlayerState(2,"idle")

class Error(var errorNo:Int):PlayerState(1,"error")

然后我们反编译看一下:

构造器都是私有的,密封类的子类还都是final类的,那当然外部无法继承了

密封类 与when在一起使用

  val state:PlayerState=Idle
    println(when(state){
        Idle->{
            "idle"
        }
        is Error->{ "error"}
        is Playing->{"playing"}
    })

可以观察一下,密封类其实主要还是类型可数的,而枚举只能是值可数 这是两者最大的区别。 换句话说 密封类是用来标定类型差异的,而枚举是用来标定值差异的。

数据类

这个地方要着重提一下,很多人都觉得 kotlin中的data class 有啥好说的?不就是类似于java中的javabean么?

一定要切记:kotlin中的data class 与java bean 并不相等!

编译器在编译data class的时候 会自动生成equals hashcode toString 以及 copy 方法。

这里注意copy 方法是一个浅拷贝方法。

此外,数据类 编译以后 在java中要用component1 的形式调用。

data class Book2(val id: Long, val name: String, val author: String)

反编译看下 就知道为何要用com的形式调用了。

数据类建议大家 就写成上述最简单的写法,不要做任何扩展

最后dataclass 相关的还有2个插件,在这里介绍一下,感兴趣的同学 可以执行搜索一下。

noArg 插件 可以 让kotlin生成的data class 拥有 无参的构造器,但是因为是编译期生成的,所以你想要调用他 必须在代码中利用反射来调用。

allOpen插件。可以改变final的特点。