转载

Kotlin旅途之类与接口

本文基于 Kotlin 1.2.x版本

“函数”和“方法”概念不做特殊区分

插播一下,下期预告《对象与扩展》,敬请关注

第一篇文章 《 Kotlin 由浅入深开车啦》 为 Kotlin 的画了一个概要地图。通过它,我们大概知道了这是一个什么地方(语言),有什么景点(特性),怎么快速玩起来。上车的各位小伙伴,现在我们来到第一站 —— 类与对象 ,接下来看看有什么好玩的东西吧!

对于从 Java 转过来的小伙伴,这块相对来说要熟悉一些,但同时也有一些未竟之地

等你探索,开始吧

类和接口

这部分主要介绍 Kotlin 中的类的构造和使用,以及 Kotlin 中的特有类以及属性的概念。毕竟作为多范式语言,对于面向对象的支持可不能含糊。

另外这部分也会对其在 JVM 上背后的实现作必要解释,看看编译器的魔法糖是怎么实现的~

接口类似于 Java 8,可以提供不记录状态(没有幕后字段)的属性以及默认方法,其余基本相似,就不展开说了,来看看类:

构造函数与初始化

Java 中声明一个类:

public class User { 

    User(String _name) {
        name = _name;
    }

    private String name;

    public String getName() {
        return name;
    };

    public void setName(String _name) {
        name = _name;
    };
}

Kotlin 中声明一个类:

open class User (var name: String)

以上两者是等价的。因为 Kotlin 中的类默认是 final 的,所以如果想要能被继承,需要加一个 open 。 另外注意到声明了一个 属性 name ,不同于 Java 中的字段,属性默认是 public 的,具体的区别下边会讲,让我们回到类的构造上来。

对于 Kotlin 上边的类声明,完整版的应该是这样的:

open class User constructor(var name: String) {

    var age = 18
    lateinit var message: String

    constructor() : this(name = "yy")

    init {
        message = "Hello $name"
    }
}

一点一点来看一看遇到的新东西。首先 主构造函数 关键字 constructor 是可以省略的,我们在类内部多声明了两个 属性 agemessageage 的类型被编译器根据赋值自动推断为 Int ,后者被修饰为 lateinit ,表示告诉编译器这个不能为null,但是我现在还没办法初始化它,稍后我在使用之前一定会初始化,你就别操心给我报错啦。所以顺利成章,我们在一个 init 块中初始化了 message 。这里同时用到了 Kotlin 的字符串拼接,这个不言自明这儿就不提了。

一口气看了这么多,就剩下一个 constructor() 了,相信机智的你早猜到了,有主构造函数,就有 次构造函数 。这里正式声明了一个次构造函数,它本身无参,并且委托给了主构造函数(如果一个类存在主构造函数,必须委托给它),把 name 属性初始化为 “yy”。

一般来说 init 块主要用于需要在构造类的同时运行一段初始化代码,比如在 Android 中初始化 view:

Kotlin旅途之类与接口

lateinit 这个关键字的使用场景主要是一些具有生命周期或者需要依赖注入初始化的地方,不建议滥用,因为你的工程代码规模变大之后,很有可能你并不能控制所有的调用处都能在正确初始化之后,这时就会抛出一个 UninitializedPropertyAccessException 异常。当然如果你也可以在使用处保护一下,原理是运行时反射拿到记录在 Kproperty 中的一个布尔值:

if (::message.isLateinit) {
    println(message)}

有了以上了解,我们就可以简单的创建一个类了,那么对于大多时候,我们需要设计框架复用代码,就需要用到继承,其实这个概念面向对象的语言都差不多,你对 Java 中的关于继承大部分的知识很容易迁移到 Kotlin ,接下来看看 Kotlin 中是怎么做的。

继承

对于 Android 最常见的一个现实的例子就是各种 View 的继承了,来看一个例子:

Kotlin旅途之类与接口
  • MyView:把次构造函数委托给父类的构造函数
  • MyView2:把主构造函数委托给父类的构造函数
  • MyView3: @JvmOverloads 注解修饰构造函数表示生成 Java 中的多个重载构造函数,即我们所熟悉的形式。结合上边所讲,相信你大部分都搞明白了。

这儿值得说的一点是 具名函数默认参数 ,可以看到 MyView3 只声明一个主构造函数,就相当于 Java 中的三个 重载方法 。但是要想真正生成我们熟悉的 JVM 上的重载方法,需要用 @JvmOverloads 修饰主构造函数。这些生成的重载方法会按照参数的默认顺序依次生成。看到这你也大概猜到默认参数的意义了,在 Kotlin 中一个多参函数如果有默认参数,那么我们就可以像在 Java 中做的那样调用它,不同的是,当我们忽略这些默认参数的时候,它们就会使用你定义的默认值。另外一个相关的概念是具名函数,也就是命名参数,即在调用时通过指定参数名字传值的方式,这是 Java 所不支持的。

继承父类就有可能需要实现它的抽象方法或者覆盖某些方法的默认实现,不同于 Java 使用注解 @Override 这种弱约束的方式,在 Kotlin 中属性和方法的覆盖需要显式 override ,同时 var 可以覆盖 val 的属性(这个具体说属性的时候会解释为什么),反之不然。

有一种情况是,从直接超类继承相同成员的多个实现:

Kotlin旅途之类与接口

这个时候就需要覆盖并提供自己的实现。

属性

关于属性,上边已经提到多次,现在我们就来详细剖析一下。

声明一个属性的完整语法

var <preopertyName>[: PropertyType] [= <property_initializer>]
    [<getter>]
    [<setter>]

val <preopertyName>[: PropertyType] [= <property_initializer>]
    [<getter>]
  • var声明可变属性,提供默认的getter和setter
  • val声明只读属性,提供默认的getter,相当于 Javafinal

属性类型如果能推断出来的话,那么 PropertyType 是可以省略的,初始化器也不是必须的,要看具体情况而定,另外编译器为我们提供了 gettersetter 的默认实现,也可以自己覆盖实现,需要注意能用 val 就不用 var ,养成良好的编程风格,有利于设计健壮的系统~

关于自定义属性初始化器,引用官方文档的一个例子:

val isEmpty get() = this.size == 0

相信这句代码是自表达的,不再解释,详细可以参考官方文档。

const 属性

  • 编译期全局常量用 const 标记,不能是引用类型 Kotlin旅途之类与接口 它们有什么不同呢?看看字节码对应成 Java 代码的实现便一目了然: Kotlin旅途之类与接口

Kotlin 没有静态属性和方法,一般通过包级函数实现,或者采用 伴生对象 (这个哥们下一篇文章会说)

特性类

Kotlin 为我们提供了很多便利,减少了很多模版代码,关于类,做了什么?

数据类

做过 Java 开发的都知道大名鼎鼎的 POJO 类、 Java Bean 之流,如果大家统计过代码行数,会发现你的工程中它们可是占据了不菲之地,其中的 getter setter 方法即使有诸如 IDEA 的加持,也显得有些繁琐,更不用说还需要小心翼翼的重写他们的 hasCodeequals 方法等。你可能要问了,兄台说这么多,到底 Kotlin 能为我们做什么呢?且看:

Kotlin旅途之类与接口

看看编译器为我们做了什么:

  • 生成了两个 Java 字段,及其对应的 getter setter 方法

    Kotlin旅途之类与接口
  • 生成对应的 componentN 函数,用来支持解构(后边文章也会讲):

    val (name, age) = User()

    Kotlin旅途之类与接口

  • copy 函数,用来拷贝一个数据类的实例对象,并允许你修改其中的一些值

    Kotlin旅途之类与接口
  • 提供了 equals/hashCode 对以及 toString 方法的实现(如果显示提供这些函数的实现,那么将不会自动生成,并使用现有函数)

    Kotlin旅途之类与接口

这就是隐藏在编译器背后的秘密。关于更加详细的数据类使用方法可以自己摸索,有些坑还是有必要提醒一下,其实也不算是 Kotlin 的坑。

其中之一便是即使你数据类的属性都声明为了 不可空 版本,在使用一些反序列化框架如 gson ,运行时也可能会是空的,最本质的原因还是在运行时被反射修改:

  • 数据类并没有默认的无参构造方法,而是带默认参数的方法,反序列化框架不一定能找到匹配的构造方法初始化,这时候就便“八仙过海各显神通”了,有的会及时打住,返回 null ; 有的会直接抛出异常;更有甚者(主要是 gson )不顾一切,采用 Java 底层 API 直接分类对象内存,而不做任何初始化操作,这个时候就会出问题了。因为与业务相关,对应的解决方法就不说了,兵来将挡,水来土掩,比如使用好默认参数的特性,找好应对之策应该不成问题。
  • 这就预示着接口返回的数据转换的时候也是需要特别注意的。

另外说到这个,提一嘴官方提供了一个 all-open gradle 插件,可以在需要的类编译字节码中织入一个无参构造方法,以保证在找不到合适的构造函数时可以安全的创建对象。

密闭类

sealed 关键修饰的类,类如其名,表示受限的类继承结构,类似于枚举类,不同的是密封类的子类可以存在多个不同状态的实例,这就意味着它有着比枚举更灵活的使用场景。如果同是受限的类继承结构,预计子类的状态不变,实例只可能有一个,那就用枚举,否则密闭类就派上用场咯。

需要注意的是,密闭类自身抽象不能被实例化,子类是一个有限集合。当然在 when 表达式中枚举是完备的,可以不需要 else 分支。

嵌套类与内部类

如果类嵌套,默认是嵌套类,内部类的话需要用inner修饰。区别是前者相当于Java的静态内部类,而后者持有外部类的引用。上一张官方图,一看便知:

Kotlin旅途之类与接口

好了,今天旅程就到这里吧,希望大家逛的不是那么累~

题外话

这一系列文章是有相互渗透的,尤其是对于初学者有些东西可能现在模模糊糊,但是为了保证清晰的结构,有些东西提前提到了,会在后边详细解释,到时候回头一看便悟得^_^。

下一篇文章主要是对象及扩展:

  • 对象声明
  • 对象表达式
  • 伴生对象
  • 扩展
  • 委托

敬请关注

另外原文有任何错误改进之处,欢迎联系我修正改进,任何疑问也可以联系我交流。欢迎订阅点赞哦,不定期更新~

声明:转载请著名作者和出处

原文  https://xiaozhuanlan.com/topic/1827634950
正文到此结束
Loading...