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。

class A {}
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中对主构造函数的重载

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

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

属性

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

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

class Person(
    val age: Int
    val address: String = "Asia"
) {
    val name: String? = ""
    var country = "China"
}

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

class A {

    var aa = 1
        get() = field
        set(value) {
            #提供特殊的过滤逻辑
            field = if (value < 10) value else 10
        }
    
    var _bb = "bb"
    
    var bb
        #对外仅仅暴露get方法,这里只是演示,真实情况_bb一般用val声明
    	get() = _bb
    	set(value) {
            _bb = value
        }

}

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

方法

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

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

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

类的继承: Inheritance

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

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

public open class Any {
    
    public open operator fun equals(other: Any?): Boolean

    
    public open fun hashCode(): Int

    
    public open fun toString(): String
}

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

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

该类及子类可见

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

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

open class Person(open val name: String) {
    
    open fun print(name: String) {
        println(name)
    }

}

interface Action {
    val state: String

    fun print(state: Int) {
        println(state)
    }
}

class Coder(
    override val name: String = "",
    override var state: String = "coding"
) : Person(name), Action {
    
    override fun print(name: String) {
        super<Person>.print(name)
        println(name)
    }
    
    override fun print(state: Int) {
        super<Action>.print(state)
        println(state)
    }
    
}

如上例,声明子类需要则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的加强版。如果我们希望禁用基类的某个方法,可以在子类中重写并用它修饰,那么该子类的子类就必须重新实现该方法,并且不会调用基类的方法

open class Polygon {
    open fun draw() {
        // some default polygon drawing method
    }
}

abstract class WildShape : Polygon() {
    abstract override fun draw()
}

接口: interface

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

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

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

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

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

fun interface IntPredicate {
   fun accept(i: Int): Boolean
}

//传统写法
val isEven = object : IntPredicate {
   override fun accept(i: Int): Boolean {
       return i % 2 == 0
   }
}

//语法糖
val isEven = IntPredicate { it % 2 == 0 }

数据类 Data class

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

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

//帐号信息
{
    "username": "somebody",
    "id": "18239048190234891032",
    "basic_info": {
        "age": 10,
        "level": 2
    }
}

//data class
data class User(
    val username: String = "unknown",
    val id: String = "unknown",
    val basicInfo: BasicInfo = BasicInfo()
)

data class BasicInfo(
    val age: Int = 0,
    var level: Int = 0
)

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

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就相等,虽然它们在内存中是独立的两个对象

data class Person(val name: String) {
    var age: Int = 0
}
fun main() {
    val person1 = Person("John")
    val person2 = Person("John")
    person1.age = 10
    person2.age = 20
    println("person1 == person2: ${person1 == person2}")
    println("person1 with age ${person1.age}: ${person1}")
    println("person2 with age ${person2.age}: ${person2}")
}

//result
person1 == person2: true
person1 with age 10: Person(name=John)
person2 with age 20: Person(name=John)

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

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

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

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

data class User(val age: Int = 0, val name: String = "someone")

val (age, name) = User(10, "Alice")

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

val someOne = User("Alice", "123345", BasicInfo(10, 2))

val copyOne = someOne.copy(
    basicInfo = someOne.basicInfo.copy(
        level = 3
    )
)

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

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

密封类 Sealed Class

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

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

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

packageA {
    
    sealed class Parent

    packageB {

        class child: Parent

    }

}

//子类放在密封类所在package的子package中也是不合法的

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

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

fun main() {
   
    println(Theme.getThemeByBrightNess(234).toString())
    //Theme$Dark@7a07c5b4

}

sealed class Theme {
    
    companion object {
        
        const val NORMAL_MAX_BRIGHTNESS = 1000f

        fun getThemeByBrightNess(brightness: Int): Theme = when {
            
            Dark.isThisTheme(brightness) -> Dark

            Normal.isThisTheme(brightness) -> Normal
            
            else -> Normal
           
        }

    }

    abstract fun isThisTheme(brightness: Int): Boolean


    object Dark : Theme() {

        private val darkBrightRange = (0.1 * NORMAL_MAX_BRIGHTNESS).toInt() .. (0.3 * NORMAL_MAX_BRIGHTNESS).toInt()

        override fun isThisTheme(brightness: Int): Boolean = brightness in darkBrightRange

    }

    object Normal : Theme() {
        private val normalBrightRange = (0.3 * NORMAL_MAX_BRIGHTNESS).toInt() .. NORMAL_MAX_BRIGHTNESS.toInt()
        
        override fun isThisTheme(brightness: Int): Boolean = brightness in normalBrightRange
    }

}

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

嵌套类 Nested class

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

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

interface OuterInterface {
    class InnerClass
    interface InnerInterface
}

class OuterClass {
    class InnerClass
    interface InnerInterface
}

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

内部类 inner class

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

class Outer {
    private val bar: Int = 1
    inner class Inner {
        fun foo() = bar
    }
}

val demo = Outer().Inner().foo() // == 1

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

class A {

    private val listener = Listener()
    
    fun doA() {
        println("jdiasocdads")
    }

    inner class Listener {

        fun doSomething() {
            doA()
        }

    }

}

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

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

 枚举类 enum class

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

enum class Direction {
    NORTH, SOUTH, WEST, EAST
}

enum class Color(val rgb: Int) {
    RED(0xFF0000),
    GREEN(0x00FF00),
    BLUE(0x0000FF)
}

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

enum class ItemType {
        A,    
        B,
        c;

        fun getTypeForMob(): String {
            return when (this) {
                A -> "aa"
                B -> "bb"
                C -> "cc"
            }
        }
}

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

fun main(args: Array<String>) {
    //罗列
    var directions = Direction.values()
    for (d in directions){
        println(d)
    }

    for (direction in enumValues<Direction>()) {
        println(direction)
    }
    
    //查找
    println(Direction.valueOf("WEST")) //创建枚举类对象用这样的方式
    
    val west = enumValueOf<Direction>("WEST")
}

内联类 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语法糖,可以让代码写得极度简洁

最通常的写法如下

window.addMouseListener(object : MouseAdapter() {
    override fun mouseClicked(e: MouseEvent) { /*...*/ }

    override fun mouseEntered(e: MouseEvent) { /*...*/ }
})

关于它的一些其他细节

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

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

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值