当前位置:   article > 正文

Kotlin 类基础

kotlin 类

本文内容

  • kotlin 类的概念与语法
  • 继承
  • 接口
  • data class 数据类
  • sealed class 密封类
  • nested class 嵌套类
  • inner class 内部类
  • enum class 枚举类
  • value class 内联类
  • object expression 对象表达式

kotlin是OOP模式的语言,其中的类是对事物的特征、逻辑的综合抽象,用关键词class声明一个类。包含class name, class header, class body。

  1. class A {}
  2. class Empty

类的基本结构:构造函数,属性,方法

构造函数

构造函数是实例化一个类时调用的api,它可以接收参数来控制对象的特征与逻辑

构造函数可以用关键词constructor声明

kotlin的类有两种构造函数,主构造函数、次构造函数

主构造函数是class header的一部分,且一般也是最主要的部分。如果主构造函数没有用注解或修饰符修饰,此时的关键词constructor可以省略,如下是最通常的写法

class Person(name: String?) {}

不可以省略的情况例如

class Customer public @Inject constructor(name: String?) {}

主构造函数不能包含代码块,它只能作为一个纯粹的参数列表声明,如果我们需要初始化逻辑,用关键词init可以声明一个局部作用域,它会在实例化时被调用。事实上所有init block都会被编译为主构造函数的一部分,然后按照声明顺序执行

次构造函数时class body的一部分,且一般用不到,声明它必须显式使用constructor关键词

如果我们显式地声明了一个主构造函数,则次构造函数需要借助委托模式,直接或间接地调用主构造函数来被调用,写法如下,通过关键字this来实现,如果该类没有主构造函数,则通过super调用父类的主构造函数。某种意义上,这里的次构造函数可以理解为java中对主构造函数的重载

  1. class Person(val name: String, val age: Int, val country: String) {
  2. constructor(name: String, age: Int) : this(name = "", age = 0, country = "China") {
  3. #直接委托给主构造函数
  4. }
  5. constructor(name: String) : this(name = "", age = 0) {
  6. #先委托给上一个次构造函数,再间接委托给主构造函数
  7. }
  8. }

由于init block本质上是主构造函数的一部分,而次构造函数需要委托主构造函数,所以所有的init block要优先于次构造函数执行

属性

kotlin中类的属性通过基本关键词val, var来声明,可以像java一样直接声明中类体中,也可以通过语法糖直接写在主构造函数中。如果属性声明了默认值,根据类型推导规则可以省略类型声明

kotlin中类的属性必须被初始化,或者声明为abstract。初始化有两种方式,一种是添加默认值,一种是延迟初始化,使用后者需要用lateinit修饰属性,表示我希望该属性在运行时动态加载,并且我信任自己的代码不会在它没有初始化之前就使用它(如果这么干,空指针crash)

  1. class Person(
  2. val age: Int
  3. val address: String = "Asia"
  4. ) {
  5. val name: String? = ""
  6. var country = "China"
  7. }

kotlin的属性提供了getter/setter语法。一般情况下不需要手动重写get/set方法,下面例子是两种常见的重写case。这里的field关键字是字面量的含义,可以粗略理解为它是当前变量在内存中的指针

  1. class A {
  2. var aa = 1
  3. get() = field
  4. set(value) {
  5. #提供特殊的过滤逻辑
  6. field = if (value < 10) value else 10
  7. }
  8. var _bb = "bb"
  9. var bb
  10. #对外仅仅暴露get方法,这里只是演示,真实情况_bb一般用val声明
  11. get() = _bb
  12. set(value) {
  13. _bb = value
  14. }
  15. }

kotlin的类还存在编译时常量的概念,用const修饰,和java的const概念基本一致

方法

声明类成员方法和声明一个顶层方法几乎没有区别,方法的具体规则参考

kotlin函数基础 上_ljjliujunjie123的博客-CSDN博客

​​​​​​kotlin 函数基础 下_ljjliujunjie123的博客-CSDN博客

类的继承: Inheritance

继承是实现多态的一种方式,虽然不是最好的方式,但一般是性价比最高的方式

由于各种“组合优于继承”的说法,可能误导我们不能使用继承,但事实上继承在kotlin中随处可见。kotlin所有原生类都隐式继承自同一个父类Any

  1. public open class Any {
  2. public open operator fun equals(other: Any?): Boolean
  3. public open fun hashCode(): Int
  4. public open fun toString(): String
  5. }

继承需要涉及到一些关键词,罗列如下 

open修饰类名、类成员名,允许子类进行重写
final修饰类名、类成员名,禁止子类进行重写
override声明该类成员是对父类的重写
public任何地方可见,类成员默认是public的
protected

该类及子类可见

private仅该类可见
internal      仅该模块可见,一般指app本身
abstract修饰类名、类成员名,强制子类进行重写
super调用父类的成员

下面给出一个简单的继承例子

  1. open class Person(open val name: String) {
  2. open fun print(name: String) {
  3. println(name)
  4. }
  5. }
  6. interface Action {
  7. val state: String
  8. fun print(state: Int) {
  9. println(state)
  10. }
  11. }
  12. class Coder(
  13. override val name: String = "",
  14. override var state: String = "coding"
  15. ) : Person(name), Action {
  16. override fun print(name: String) {
  17. super<Person>.print(name)
  18. println(name)
  19. }
  20. override fun print(state: Int) {
  21. super<Action>.print(state)
  22. println(state)
  23. }
  24. }

如上例,声明子类需要则class header中添加父类列表

注意这里父类列表中一个是类,一个是接口。kotlin不支持直接的多继承,比如这里的Action改成class就编译失败,原因是为了避免方法冲突等问题。但是kotlin的接口比java的接口更宽松,允许定义成员、或者直接实现方法,所以可以通过接口来实现“多继承” 

kotlin的一般类默认是final修饰的,所以可以被继承的类必须声明open。对应的类成员如果需要被重写,也要声明open。子类重写父类成员时,声明override,如果在override前再加上final,则意味着该子类的子类不允许继续重写该成员

重写成员时,可以在子类中用var修饰的变量替换父类中用val修饰的变量,但反过来不行。原因是var变量有get/set方法,而val变量只有get方法,重写时可以增多方法,但不能减少

重写方法时,可以用super调用父类的方法,但是注意这一些特殊case时,需要使用super@classname或者super<classname>的语法指定父类。遇到多继承时,如果父类或接口有同名方法,如果方法签名一致,子类可以只写一个重写方法。但如果不一致,可以重载多个方法

关于父类与子类的初始化顺序,遵循jvm类实例化过程,先加载子类,发现父类没被加载,就中断去加载父类,并完成父类的实例化,之后再进行子类的实例化

最后是一种特殊类,abstract抽象类,它修饰的类方法不能有方法体,且子类必须重写,可以理解为open的加强版。如果我们希望禁用基类的某个方法,可以在子类中重写并用它修饰,那么该子类的子类就必须重新实现该方法,并且不会调用基类的方法

  1. open class Polygon {
  2. open fun draw() {
  3. // some default polygon drawing method
  4. }
  5. }
  6. abstract class WildShape : Polygon() {
  7. abstract override fun draw()
  8. }

接口: interface

接口本质上也是类,是特殊的类。kotlin的接口在java接口基础上扩充了能力,允许直接实现方法体,也允许声明属性。但是需要注意的是接口中的属性要么是abstract的,要么提供了get方法,但是接口的属性不存在backing fields,无法用field关键字获取真实值

接口可以继承多个接口,多个接口也可以被一个类继承。继承规则和上面的类继承基本一致

继承多个接口遇到同名方法时,依照官方文档解决冲突Interfaces | Kotlin

但是事实上,几乎不会有人把两个接口的方法命名相同

kotlin还支持一种特殊的语法糖,当接口中有且只有一个abstract方法时,可以进行如下简写

  1. fun interface IntPredicate {
  2. fun accept(i: Int): Boolean
  3. }
  4. //传统写法
  5. val isEven = object : IntPredicate {
  6. override fun accept(i: Int): Boolean {
  7. return i % 2 == 0
  8. }
  9. }
  10. //语法糖
  11. val isEven = IntPredicate { it % 2 == 0 }

数据类 Data class

kotlin新增的关键词data,修饰类名变成数据类。在java开发中经常需要解析一个json文件到内存中,这时需要写一个java bean类,定义好对应的属性和get/set方法,然后用诸如GSON的解析库解析。这里的java bean作为数据的容器。kotlin的数据类就可以替代这一功能。例如

有一个json文件表示一个人的帐号信息,那么我们可以用如下的数据类作为解析它的容器

  1. //帐号信息
  2. {
  3. "username": "somebody",
  4. "id": "18239048190234891032",
  5. "basic_info": {
  6. "age": 10,
  7. "level": 2
  8. }
  9. }
  10. //data class
  11. data class User(
  12. val username: String = "unknown",
  13. val id: String = "unknown",
  14. val basicInfo: BasicInfo = BasicInfo()
  15. )
  16. data class BasicInfo(
  17. val age: Int = 0,
  18. var level: Int = 0
  19. )

形如上述例子,数据类基本语法规则有如下几条:

1. 主构造函数至少要有一个参数

2. 主构造函数中的所有参数必须声明val/var,也就是把它们作为属性而声明

3. data class不能用abstract, open, sealed, inner来修饰

如何理解这三条约束,需要考虑data class背后都干了些什么。所有声明在主构造函数中的属性都会自动生成如下方法

  • equals()  hashCode()  用来判断两个对象是否相等
  • toString()  形如"User(param1 = value1, param2 = value2)"
  • componentN()  用于解构的语法糖
  • copy()  “拷贝构造函数"

其中第一点需要强调,因为一般意义上我们可以用hashCode来区分两个对象(虽然这并不保险),但data class的这一特性使得下例中的风险很容易发生

因为data class类体中声明的属性不参与hashCode的计算,所以只要主构造函数的参数列表一致,两个对象的hashCode就相等,虽然它们在内存中是独立的两个对象

  1. data class Person(val name: String) {
  2. var age: Int = 0
  3. }
  4. fun main() {
  5. val person1 = Person("John")
  6. val person2 = Person("John")
  7. person1.age = 10
  8. person2.age = 20
  9. println("person1 == person2: ${person1 == person2}")
  10. println("person1 with age ${person1.age}: ${person1}")
  11. println("person2 with age ${person2.age}: ${person2}")
  12. }
  13. //result
  14. person1 == person2: true
  15. person1 with age 10: Person(name=John)
  16. person2 with age 20: Person(name=John)

关于第三点所说的解构语法,则是一种语法糖,在很多语言中都存在,最常见的例子如下

这里的(key,value)就是解构语法糖

  1. val numbersMap = mutableMapOf<String, String>().apply {
  2. this["one"] = "1"
  3. this["two"] = "2"
  4. }
  5. for ((key, value) in numbersMap) {
  6. println(key + ' ' + value)
  7. }

而data class会自动声明componetN方法,也就意味着我们可以对它的对象使用这种语法糖

  1. data class User(val age: Int = 0, val name: String = "someone")
  2. val (age, name) = User(10, "Alice")

关于第四点的拷贝函数,一个简单的例子是,假设某个人的帐号level信息改变了,其他都不变,那么你可以这么写

  1. val someOne = User("Alice", "123345", BasicInfo(10, 2))
  2. val copyOne = someOne.copy(
  3. basicInfo = someOne.basicInfo.copy(
  4. level = 3
  5. )
  6. )

关于数据类最后一点是,kotlin标准库中的Pair和Triple都是data class,所以它们才能使用解构语法

另外,kotlin 1.1后, data class是可以继承自普通类或者接口的,但事实上data class的继承很少使用,暂且不提

密封类 Sealed Class

kotlin新增关键字sealed,修饰类名变成密封类。某种意义上,它可以被认为是对枚举类的增强。因为它有以下特点

  • 密封类的所有直接子类在编译期就被唯一确定
  • 密封类的子类可以拥有多个实例
  • 密封类和它的直接子类必须声明在同一个package下
  • 密封类本身是abstract的,必须通过子类来实例化
  • 密封类的构造器只能是protect或者private

其中第三点关于密封类及子类的位置,一般是把子类作为嵌套类放在密封类内部,也可以把它们拆分成多个文件放在同一个package下。但需要注意,必须是严格的相同package,不能有如下情况

  1. packageA {
  2. sealed class Parent
  3. packageB {
  4. class child: Parent
  5. }
  6. }
  7. //子类放在密封类所在package的子package中也是不合法的

关于密封类的其他4点,其实共同做了一件事情:“保证密封类只有有限的几种已知子类”。这样和枚举类型就非常相似,枚举类型的实例只能是一些基本类型,作为flag使用。而密封类的子类可以包含属性、方法,同时也能作为flag,是枚举类型的增强。

考虑如下的例子。假设我希望根据屏幕的亮度来自适应调整软件主题,可以设计这样一个Theme的工具类,这里的Dark, Normal两个子类就是对主题类型的枚举,同时内部也包含一定逻辑

  1. fun main() {
  2. println(Theme.getThemeByBrightNess(234).toString())
  3. //Theme$Dark@7a07c5b4
  4. }
  5. sealed class Theme {
  6. companion object {
  7. const val NORMAL_MAX_BRIGHTNESS = 1000f
  8. fun getThemeByBrightNess(brightness: Int): Theme = when {
  9. Dark.isThisTheme(brightness) -> Dark
  10. Normal.isThisTheme(brightness) -> Normal
  11. else -> Normal
  12. }
  13. }
  14. abstract fun isThisTheme(brightness: Int): Boolean
  15. object Dark : Theme() {
  16. private val darkBrightRange = (0.1 * NORMAL_MAX_BRIGHTNESS).toInt() .. (0.3 * NORMAL_MAX_BRIGHTNESS).toInt()
  17. override fun isThisTheme(brightness: Int): Boolean = brightness in darkBrightRange
  18. }
  19. object Normal : Theme() {
  20. private val normalBrightRange = (0.3 * NORMAL_MAX_BRIGHTNESS).toInt() .. NORMAL_MAX_BRIGHTNESS.toInt()
  21. override fun isThisTheme(brightness: Int): Boolean = brightness in normalBrightRange
  22. }
  23. }

关于密封类的其他细节,参见官方文档

嵌套类 Nested class

kotlin并没有关键字nested,嵌套类形如下例,可以视为外部类的一个成员,通过点操作符调用

android开发中常见的例子是adapter里嵌套viewholder的声明

  1. interface OuterInterface {
  2. class InnerClass
  3. interface InnerInterface
  4. }
  5. class OuterClass {
  6. class InnerClass
  7. interface InnerInterface
  8. }

需要注意的是,嵌套类并不持有外部类的引用,把它们嵌套纯粹是符合人类逻辑上的收敛

内部类 inner class

kotlin用关键字inner修饰一个嵌套类,被称为内部类。二者唯一的变化就是内部类持有了外部类的引用,可以访问外部类的成员

  1. class Outer {
  2. private val bar: Int = 1
  3. inner class Inner {
  4. fun foo() = bar
  5. }
  6. }
  7. val demo = Outer().Inner().foo() // == 1

 关于内部类的一个通用例子如下。很多时候,回调函数需要访问调用者的一些参数,如果把回调的实现放在其他类或文件中,我们就需要以参数的形式将该参数传递。但如果使用内部类,则可以方便地解决这个问题

  1. class A {
  2. private val listener = Listener()
  3. fun doA() {
  4. println("jdiasocdads")
  5. }
  6. inner class Listener {
  7. fun doSomething() {
  8. doA()
  9. }
  10. }
  11. }

很显然的,内部类有两个潜在问题,第一是this指针,如果遇到同名方法或属性,需要使用this@receiver的语法指定当前this指向哪一个作用域,具体细节请参考This expressions | Kotlin

 第二个问题是内部类天然存在循环引用问题,可能会导致内存泄漏,可以参考LeakCanary使用详细教程(附Demo)_小火你好的博客-CSDN博客_leakcanary使用

 枚举类 enum class

kotlin用enum修饰类成为枚举类,最常用的两种case如下。都是作为flag使用,只不过带不带参数

  1. enum class Direction {
  2. NORTH, SOUTH, WEST, EAST
  3. }
  4. enum class Color(val rgb: Int) {
  5. RED(0xFF0000),
  6. GREEN(0x00FF00),
  7. BLUE(0x0000FF)
  8. }

但事实上,enum class可以实现接口,自定义方法,来实现很多逻辑,例如

  1. enum class ItemType {
  2. A,
  3. B,
  4. c;
  5. fun getTypeForMob(): String {
  6. return when (this) {
  7. A -> "aa"
  8. B -> "bb"
  9. C -> "cc"
  10. }
  11. }
  12. }

kotlin官方库还有一些关于枚举类型的工具函数,用来罗列或查询枚举类型的成员,例如

  1. fun main(args: Array<String>) {
  2. //罗列
  3. var directions = Direction.values()
  4. for (d in directions){
  5. println(d)
  6. }
  7. for (direction in enumValues<Direction>()) {
  8. println(direction)
  9. }
  10. //查找
  11. println(Direction.valueOf("WEST")) //创建枚举类对象用这样的方式
  12. val west = enumValueOf<Direction>("WEST")
  13. }

内联类 value class

在kotlin 1.5之前,内联类使用inline 修饰类名,和内联函数共用一个修饰符。但1.5之后内联类改用value修饰符。之所以有这个改动,需要理解为什么要有内联类。

简单来说,jvm对kotlin中的基本类型,如String等做了很多优化,比如将其内存分配从堆上分配改为栈上分配,这些优化能大幅提高代码性能。但是我们开发者有时候会对基本类型做一些封装(装饰者模式),装饰后的类就无法享受jvm的优化了。鱼与熊掌不可兼得

作为成熟的开发者,我们当然选择全部都要。使用如下的内联类语法即可

value class Password(private val s: String)

kotlin为了实现这一功能,对内联类做了很多限制,主要的几点如下

  • 有且仅有一个包含单个基本类型参数的构造器
  • 内联类可以有成员和方法,但没有字面量(也就是在堆中无法分配内存),只能对构造器中的参数做一些简单处理
  • 内联类可以实现接口,但不能继承其他类,也不能被其他类继承

在某种意义上,内联类和类型别名有些相似,它们之间的核心区别在于内联类声明了一个新的类型,不能与基本类型互相赋值,而类型别名可以

对象表达式 object expression

kotlin用object关键字声明一个对象表达式,这个说法可能有些奇怪,但如果改成匿名内部类就觉得非常熟悉了。object就是对匿名内部类的优化,结合kotlin的lambda语法糖,可以让代码写得极度简洁

最通常的写法如下

  1. window.addMouseListener(object : MouseAdapter() {
  2. override fun mouseClicked(e: MouseEvent) { /*...*/ }
  3. override fun mouseEntered(e: MouseEvent) { /*...*/ }
  4. })

关于它的一些其他细节

  • 对象表达式可以实现多个接口
  • 对象表达式持有外部类的引用,可以访问外部作用域的成员(比如当前函数作用域)

注意,object关键字除了声明对象表达式,还可以声明单例和伴生对象,参考Kotlin——object(单例,伴生对象,内部类)_散人1024的博客-CSDN博客_kotlin object类

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/不正经/article/detail/277419
推荐阅读
相关标签
  

闽ICP备14008679号