栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

Kotlin 类基础

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

Kotlin 类基础

本文内容

kotlin 类的概念与语法继承接口data class 数据类sealed class 密封类nested class 嵌套类inner class 内部类inline 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.print(name)
        println(name)
    }
    
    override fun print(state: Int) {
        super.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的语法指定父类。遇到多继承时,如果父类或接口有同名方法,如果方法签名一致,子类可以只写一个重写方法。但如果不一致,可以重载多个方法

关于父类与子类的初始化顺序,遵循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().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的继承很少使用,暂且不提

未完待续

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/777818.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号