赞
踩
本文内容
- 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博客
继承是实现多态的一种方式,虽然不是最好的方式,但一般是性价比最高的方式
由于各种“组合优于继承”的说法,可能误导我们不能使用继承,但事实上继承在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()
- }
接口本质上也是类,是特殊的类。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 }
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背后都干了些什么。所有声明在主构造函数中的属性都会自动生成如下方法
其中第一点需要强调,因为一般意义上我们可以用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的继承很少使用,暂且不提
kotlin新增关键字sealed,修饰类名变成密封类。某种意义上,它可以被认为是对枚举类的增强。因为它有以下特点
其中第三点关于密封类及子类的位置,一般是把子类作为嵌套类放在密封类内部,也可以把它们拆分成多个文件放在同一个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
- }
-
- }
关于密封类的其他细节,参见官方文档
kotlin并没有关键字nested,嵌套类形如下例,可以视为外部类的一个成员,通过点操作符调用
android开发中常见的例子是adapter里嵌套viewholder的声明
- interface OuterInterface {
- class InnerClass
- interface InnerInterface
- }
-
- class OuterClass {
- class InnerClass
- interface InnerInterface
- }
需要注意的是,嵌套类并不持有外部类的引用,把它们嵌套纯粹是符合人类逻辑上的收敛
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使用
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")
- }
在kotlin 1.5之前,内联类使用inline 修饰类名,和内联函数共用一个修饰符。但1.5之后内联类改用value修饰符。之所以有这个改动,需要理解为什么要有内联类。
简单来说,jvm对kotlin中的基本类型,如String等做了很多优化,比如将其内存分配从堆上分配改为栈上分配,这些优化能大幅提高代码性能。但是我们开发者有时候会对基本类型做一些封装(装饰者模式),装饰后的类就无法享受jvm的优化了。鱼与熊掌不可兼得
作为成熟的开发者,我们当然选择全部都要。使用如下的内联类语法即可
value class Password(private val s: String)
kotlin为了实现这一功能,对内联类做了很多限制,主要的几点如下
在某种意义上,内联类和类型别名有些相似,它们之间的核心区别在于内联类声明了一个新的类型,不能与基本类型互相赋值,而类型别名可以
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类
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。