数据类型
对于那些只需要保存数据的类型,我们常常需要为其重写toString
、equals
等函数,针对于这种情况下,Kotlin为我们提供了专门的数据类,数据类不仅能像普通类一样使用,并且自带我们需要的额外成员函数,比如打印到输出、比较实例、复制实例等。
声明一个数据类非常简单:
//在class前面添加data关键字表示为一个数据类
data class User(val name: String, val age: Int)
数据类声明后,编译器会根据主构造函数中声明的所有属性自动为其生成以下函数:
.equals()
/.hashCode()
.toString()
生成的字符串格式类似于"User(name=John, age=42)"
.componentN()
与按声明顺序自动生成用于解构的函数.copy()
用于对对象进行拷贝
我们可以来试试看:
fun main() {
val user1 = User("小明", 18)
val user2 = User("小明", 18)
println(user1) //打印出来就是格式化的字符串 User(name=小明, age=18)
println(user1 == user2) //true,因为自动重写了equals函数
val (name, age) = user1 //自动添加componentN函数,因此支持解构操作
println("名称: $name, 年龄: $age")
}
当然,为了确保生成代码的一致性和有效性,数据类必须满足以下要求:
- 主构造函数中至少有一个参数。
- 主构造函数中的参数必须标记为
val
或var
。 - 数据类不能是抽象的、开放的、密封的或内部的。
此外,数据类的成员属性生成遵循有关成员继承的以下规则:
-
如果数据类主体中
.equals()
.hashCode()
或.toString()
等函数存在显式(手动)实现,或者在父类中有final
实现,则不会自动生成这些函数,并使用现有的实现。data class User(val name: String, val age: Int) { //如果已经存在toString的情况下,不会自动生成 override fun toString(): String = "我是自定义的toString" } fun main() { val user = User("小明", 18) println(user) //结果: 我是自定义的toString }
-
如果超类型具有
open
.componentN()
函数并返回兼容类型,则为数据类生成相应的函数,并覆盖超类型的函数。如果由于一些关键字导致无法重父类对应的函数会导致直接报错。abstract class AbstractUser { //此函数必须是open的,否则无法被数据类继承 open operator fun component1() = "卢本伟牛逼" } data class User(val name: String, val age: Int): AbstractUser() //自动覆盖掉父类的component1函数
-
不允许为
.componentN()
和.copy()
函数提供显式实现。[外链图片转存中…(img-GVONt1pf-1728127119308)]
注意,编译器会且只会根据主构造函数中定义的属性生成对应函数,如果有些时候我们不希望某些属性被添加到自动生成的函数中,我们需要手动将其移出主构造函数:
data class Person(val name: String) {
var age: Int = 0 //age属性不会被处理
}
此时生成的所有函数将不会再考虑age属性:
fun main() {
val person1 = Person("John")
val person2 = Person("John")
person1.age = 10
person2.age = 20
println("person1 == person2: ${person1 == person2}")
// person1 == person2: true
println("person1 with age ${person1.age}: $person1")
// person1 年龄为 10: Person(name=John)
println("person2 with age ${person2.age}: $person2")
// person2 年龄为 20: Person(name=John)
}
数据类自带一个拷贝对象的函数,使用.copy()
函数复制对象,允许您更改其一些属性,而其余的保持不变。此函数对上述User
类的实现如下:
data class User(val name: String, val age: Int)
fun main() {
val user = User("小明", 18)
val copyUser = user.copy() //使用拷贝函数生成一个内容完全一样的新对象
println(user == copyUser)
println(user === copyUser)
}
在copy函数还可以在拷贝过程中手动指定属性的值:
val user = User("小明", 18)
println(user.copy(age = 17)) //结果为 User(name=小明, age=17)
枚举类型
我们知道,在Kotlin中有一个Boolean类型,它只有两种结果,要么为false要么为true,这代表它的两种状态真和假。有些时候,可能两种状态并不能满足我们的需求,比如一个交通信号灯,它具有三种状态:红灯、黄灯和绿灯。
如果我们想要存储和表示自定义的多种状态,使用枚举类型就非常合适:
//在类前面添加enum表示这是一个枚举类型
enum class LightState {
GREEN, YELLOW, RED //直接在枚举类内部写上所有枚举的名称,一般全部用大写字母命名
}
枚举类的值只能是我们在类中定义的那些枚举,不可以存在其他的结果,枚举类型同样也是一个普通的类,只是存在值的限制。
要使用一个枚举类的对象,可以通过类名直接获取定义好的枚举:
fun main() {
val state: LightState = LightState.RED //直接得到红灯
println(state.name) //自带name属性,也就是我们编写的枚举名称,这里是RED
}
同样的,枚举类也可以具有成员:
//同样可以定义成员变量,但是不能命名为name,因为name拿来返回枚举名称了
enum class LightState(val color: String) {
GREEN("绿灯"), YELLOW("黄灯"), RED("红灯"); //枚举在定义时也必须填写参数,如果后面还要编写成员函数之类的其他内容,还需在末尾添加分号结束
fun isGreen() = this == LightState.GREEN //定义一个函数也是没问题的
}
我们可以像普通类那样正常使用枚举类的成员:
val state: LightState = LightState.RED
println("信号灯的颜色是: ${state.color}")
println("信号灯是否可以通行: ${state.isGreen()}")
枚举类型可以用于when
表达式进行判断,因为它的状态是有限的:
val state: LightState = LightState.RED
val message: String = when(state) {
LightState.RED -> "禁止通行"
LightState.YELLOW -> "减速通行"
LightState.GREEN -> "正常通行"
}
println(message) //结果为: 禁止通行
在枚举类中也可以编写抽象函数,抽象函数需要由枚举自行实现:
enum class LightState(val color: String) {
GREEN("绿灯"){
override fun test() = println("我是绿灯,表示可以通过")
}, YELLOW("黄灯") {
override fun test() = println("我是黄灯,是让你减速,不是让你踩油门加速过去")
}, RED("红灯") {
override fun test() = println("我是红灯,禁止通行")
};
abstract fun test() //抽象函数
}
fun main() {
val state: LightState = LightState.RED
state.test() //调用函数: 我是红灯,禁止通行
}
如果枚举类实现了某个接口,同样可以像这样去实现:
interface Message {
fun test()
}
enum class LightState(val color: String) : Message {
GREEN("绿灯"){
override fun test() = println("我是绿灯,表示可以通过")
}, YELLOW("黄灯") {
override fun test() = println("我是黄灯,是让你减速,不是让你踩油门加速过去")
}, RED("红灯") {
override fun test() = println("我是红灯,禁止通行")
};
}
enum class LightState(val color: String) : Message {
GREEN("绿灯"), YELLOW("黄灯"), RED("红灯");
override fun test() = println("")
}
枚举类也为我们准备了很多的函数:
val state: LightState = LightState.valueOf("RED") //通过valueOf函数以字符串名称的形式转换为对应名称的枚举
val state: LightState = enumValueOf<LightState>("RED") //同上
println(state)
println(state.ordinal) //枚举在第几个位置
println(state.name) //枚举名称
val entries: EnumEntries<LightState> = LightState.entries //一键获取全部枚举,得到的结果是EnumEntries类型的,他是List的子接口,因此可以当做List来使用
val values: Array<LightState> = enumValues<LightState>() //或者像这样以Array形式获取到所有的枚举
println(entries)
匿名类、单例类和伴生对象
匿名类
有些时候,可能我们并不需要那种通过class
关键字定义的对象,而是以匿名的形式创建一个临时使用的对象,在使用完之后就不再需要了,这种情况完全没有必要定义一个完整的类型,我们可以使用匿名类的形式去编写。
val obj = object { //使用object关键字声明一个匿名类并创建其对象,可以直接使用变量接收得到的对象
val name: String = ""
override fun toString(): String = "我是一个匿名类" //匿名类默认继承于Any,可以直接重写其toString
}
println(obj)
可以看到,匿名类除了没名字之外,也可以定义成员,只不过这种匿名类不能定义任何构造函数,因为它是直接创建的,这种写法我们也可以叫做对象表达式。
匿名类不仅可以直接定义,也可以作为某个类的子类定义,或是某个接口的实现:
interface Person {
fun chat()
}
fun main() {
val obj: Person = object : Person { //直接实现Person接口
override fun chat() = println("牛逼啊")
}
obj.chat() //当做Person的实现类使用
}
interface Person
open class Human(val name: String)
fun main() {
val obj: Human = object : Human("小明"), Person { //继承类时,同样需要调用其构造函数
override fun toString() = "我叫$name" //因为是子类,直接使用父类的属性也是没问题的
}
println(obj)
}
可以看到,平时我们无法直接实例化的接口或是抽象类,可以通过匿名类的形式得到一个实例化对象。
我们再来看下面这种情况:
interface KRunnable {
fun invoke() //此类型是一个接口且只包含一个函数
}
根据我们上面学习的用法,如果我们想要使用其匿名类,可以像这样编写:
fun main() {
val runnable = object : KRunnable { //使用匿名类的形式编写KRunnable的实现类对象
override fun invoke() {
println("我是函数invoke的实现")
}
}
runnable.invoke()
}
特别的,对于只存在一个抽象函数的接口称为函数式接口或单一抽象方法(SAM)接口,函数式接口可以有N个非抽象成员,但是只能有一个抽象成员。对于函数式接口,可以使用我们前面介绍的Lambda表达式来使代码更简洁:
fun interface KRunnable { //在接口声明前面添加fun关键字
fun invoke()
}
...
fun main() {
val runnable = KRunnable { //支持使用Lambda替换
println("我是函数invoke的实现")
}
runnable.invoke()
}
我们再来看下面这种情况:
fun interface Printer {
fun print()
}
fun test(printer: Printer) { //需要Printer接口实现对象
printer.print()
}
我们在调用test时,也可以写的非常优雅:
fun main() {
test { //直接Lambda一步到位
println("Hello World")
}
}
正是因为有了匿名类,所以有些时候我们通过函数得到的结果,可能并不是某个具体定义的类型,也有可能是直接采用匿名形式创建的匿名类对象:
open class Human(val name: String)
fun test() = object: Human("小明") { //返回的一个匿名类对象
val age: Int = 10
override fun toString() = "我叫$name"
}
fun main() {
println(test().name)
println(test().age) //编译错误,因为返回的类型是Human,由于其匿名特性,只能当做Human使用
}
单例类
object
关键字除了用于声明匿名类型,也可以用于声明单例类。单例类是什么意思呢?就像它的名字一样,在整个程序中只能存在一个对象,也就是单个实例,不可以创建其他的对象,始终使用的只能是那一个对象。
object Singleton { //声明的一个单例类
private var name = "你干嘛"
override fun toString() = "我叫$name"
}
fun main() {
val singleton = Singleton //通过类名直接得到此单例类的对象
//不可以通过构造函数的形式创建对象
println(singleton)
}
object Singleton {
fun test() = println("原神,启动!")
}
fun main() {
Singleton.test() //单例定义的函数直接使用类名即可调用
}
用起来与Java中的静态属性挺像的,只不过性质完全不一样。单例类的这种性质在很多情况下都很方便,比如我们要编写某些工具操作,可以直接使用单例类的形式编写。
伴生对象
现在我们希望一个类既支持单例类那样的直接调用,又支持像一个普通class那样使用,这时该怎么办呢?
我们可以使用伴生对象来完成,实际上就是将一个单例类写到某个类的内部:
class Student(val name: String, val age: Int) {
//使用companion关键字在内部编写一个伴生对象,它同样是单例的
companion object Tools {
//伴生对象定义的函数可以直接通过外部类名调用
fun create(name: String, age: Int) = Student(name, age)
}
}
fun main() {
//现在Student不仅具有对象的函数,还可以通过类名直接调用其伴生对象通过的函数
val student = Student.create("小明", 18)
println(student.toString())
}
伴生对象在Student类加载的时候就自动创建好了,因此我们可以实现直接使用。
委托模式
在有些时候,类的继承在属性的扩展上起到了很大的作用,通过继承我们可以直接获得某个类的全部属性,而不需要再次进行编写,不过,现在有了一个更好的继承替代方案,那就是委托模式(在设计模式中也有提及)名字虽然听着很高级,但是其实很简单,比如我们现在有一个接口:
interface Base {
fun print()
}
正常情况下,我们需要编写一个它的实现类:
class BaseImpl(val x: Int) : Base {
override fun print() = println(x)
}
现在我们换一个思路,我们再来创建一个实现类:
class Derived(val base: Base): Base { //将一个Base的实现类作为属性保存到类中,同样实现Base接口
override fun print() = base.print() //真正去实现这个接口的,实际上并不是当前类,而是被拉进来的那个替身
}
这就是一个非常典型的委托模型,且大量实践已证明委托模式是实现继承的良好替代方案。
Kotlin对于这种模式同样给予了原生支持:
interface Base {
fun print()
}
class BaseImpl(val x: Int) : Base {
override fun print() = println(x)
}
class Derived(val b: Base): Base by b //使用by关键字将所有接口待实现操作委托给指定成员
这样就可以轻松实现委托模式了。
除了类可以委托给其他对象之外,类的成员属性也可以委托给其他对象:
import kotlin.reflect.KProperty
class Example {
var p: String by Delegate() //属性也可以使用by关键字委托给其他对象
}
// 委托的类
class Delegate {
//需要重载属性的获取和设置两个运算
operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
return "$thisRef, 这里委托了 ${property.name} 属性"
}
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
println("$thisRef 的 ${property.name} 属性赋值为 $value")
}
}
fun main() {
println(Example().p)
}
不过,自己去定义一个类来进行委托实在是太麻烦了,Kotlin在标准库中也为我们提供了大量的预设函数:
class Example {
val p: String by lazy { "牛逼啊" } //lazy为我们生成一个委托对象,这样在获取属性值的时候就会执行lazy里面的操作了,看起来效果就像是延迟执行一样,由于只能获取,所以说只支持val变量
}
fun main() {
println(Example().p)
}
也可以设置观察者,实时观察变量的变化:
class Example {
var p: String by Delegates.observable("我是初始值") {
prop, old, new ->
println("检测到$prop 的值发生变化,旧值:$old -> 新值:$new")
}
}
fun main() {
Example().p = "你干嘛"
}
属性也可以直接将自己委托给另一个属性:
class Example(var a: String) {
var p: String by ::a //使用双冒号来委托给其他属性
}
fun main() {
val example = Example("你干嘛")
println(example.p)
}
相信各位应该能猜到,这样委托给其他属性,当前属性的值修改,会直接导致其他属性的值也会修改,相反同样它们已经被相互绑定了。
属性也可以被委托给一个Map来进行存储:
class User(map: MutableMap<String, Any>) {
var name: String by map //直接委托给外部传入的Map集合
var age: Int by map //变量的值从Map中进行存取
override fun toString(): String = "名称: $name, 年龄: $age"
}
fun main() {
val map: MutableMap<String, Any> = mutableMapOf(
"name" to "John Doe",
"age" to 25
)
val user = User(map)
println(user) //名称: John Doe, 年龄: 25
map["age"] = 10 //映射的值修改会直接影响变量的值
println(user) //名称: John Doe, 年龄: 10
}
注意,在使用不可变的Map时,只能给val类型变量进行委托,因为不可修改。
密封类型
有些时候,我们可能会编写一些类给其他人使用,但是我们不希望他们随意继承使用我们提供的类,我们只希望在我们提供的框架内部自己进行使用,这时我们就可以将类或接口设定为密封的。
密封类的所有直接子类在编译时都是已知的。不得在定义密封类的模块和包之外出现其他子类。例如,第三方项目无法在其代码中扩展您的密封类。因此,密封类的每个实例都有一个来自预设好的类型,且该类型在编译该类时是已知的。
package com.test
sealed class A //声明密封类很简单,直接添加sealed关键字即可
sealed class B: A() //密封类同一个模块或包中可以随意继承,并且子类也可以是密封的
当我们在其他包中使用这个密封类,在其他包或模块中无法使用:
class C: A() //编译错误,不在同一个模块
fun main() {
val b = B() //编译错误,不可以实例化
}
密封类将类的使用严格控制在了模块内部,包括密封接口及其实现也是如此:一旦编译了具有密封接口的模块,就不会出现新的实现类。
从某种意义上说,密封类类似于枚举类:枚举类型的值数量也受到限制,由我们自己定义,但每个枚举变量仅作为单个实例存在,而密封类的子类可以有多个实例,每个实例都有自己的状态。密封类本身也是抽象的,它不能直接实例化,并且可以具有abstract
成员:
sealed class A
sealed class B: A() {
abstract fun test()
}
密封类继承后也可以使其不继续密封,让外部可以正常使用:
sealed class A
class B: A()
class C: A()
class D: A() //不添加sealed关键字使其不再密封
但是由于类A是密封的,因此所有继承自A的类只能是我们自己写的,别人无法编写继承A的类,除非我们将某个继承A的类设定为open特性,允许继承。因此,这也进一步证明密封类在一开始就确定了有哪些子类。
由于密封类能够确定,所以在使用when进行类型判断时,也是有限的:
fun main() {
val a: A = C()
when(a) {
is B -> println("是B")
is C -> println("是C")
is D -> println("是D")
}
}
密封类的应用场景其实远不止这些,由于篇幅有限,这里就不展开讲解了。
异常机制
在理想的情况下,我们的程序会按照我们的思路去运行,按理说是不会出现问题的,但是,代码实际编写后并不一定是完美的,可能会有我们没有考虑到的情况,如果这些情况能够正常得到一个错误的结果还好,但是如果直接导致程序运行出现问题了呢?
我们来看下面这段代码:
fun main() {
test(1, 0) //当b为0的时候,还能正常运行吗?
}
private fun test(a: Int, b: Int): Int {
return a / b //没有任何的判断而是直接做计算
}
1怎么可能去除以0呢,数学上有明确规定,0不能做除数,所以这里得到一个异常:
那么这个异常到底是什么样的一种存在呢?当程序运行出现我们没有考虑到的情况时,就有可能出现异常或是错误!它们在默认情况下会强行终止我们的程序。
异常的使用
们在之前其实已经接触过一些异常了,比如数组越界异常,空指针异常,算术异常等,他们其实都是异常类型,我们的每一个异常也是一个类,他们都继承自Throwable
类!异常类型本质依然类的对象,但是异常类型支持在程序运行出现问题时抛出(也就是上面出现的红色报错)也可以提前声明,告知使用者需要处理可能会出现的异常!
每个异常对象都包含一条消息、一个堆栈跟踪和一个可选原因。
我们自己也可以抛出异常,要抛出异常对象,请使用throw
出表达式:
fun main() {
//Exception继承自Throwable类,作为普通的异常类型
throw Exception("牛逼")
}
可以看到,控制台出现了下面的报错:
所以,我们平时看到的那些丰富多彩的异常,其实大部分都是由程序主动抛出的。
我们也可以依葫芦画瓢,自定义我们自己的异常类:
class TestException(message: String) : Exception(message)
fun main() {
throw TestException("自定义异常")
}
是不是感觉很简单,异常的出现就是为了方便我们快速判断程序的错误。我们可以在异常打印出来的栈追踪信息中得到当前程序出现问题的位置:
这里指示的很明确,是我们的Main.kt文件第四行代码出现了异常。
异常的处理
当程序没有按照我们理想的样子运行而出现异常时(JVM平台下,默认会交给JVM来处理,JVM发现任何异常都会立即终止程序运行,并在控制台打印栈追踪信息)现在我们希望能够自己处理出现的问题,让程序继续运行下去,就需要对异常进行捕获,比如:
val array = arrayOf(1, 2, 3)
println(array[3]) //数组长度根本没有4,很明显这里会出现异常
现在我们希望能够手动处理这种情况,即使发生异常也要继续下去,我们可以使用try-catch语句块来完成:
fun main() {
try { //使用try-catch语句进行异常捕获
val array = arrayOf(1, 2, 3)
println(array[3])
} catch (e: ArrayIndexOutOfBoundsException) {
//因为异常本身也是一个对象,catch中实际上就是用一个局部变量去接收异常
}
println("程序继续正常运行!")
}
我们可以将代码编写到try
语句块中,只要是在这个范围内发生的异常,都可以被捕获,使用catch
关键字对指定的异常进行捕获,这里我们捕获的是ArrayIndexOutOfBoundsException数组越界异常:
可以看到,当我们捕获异常之后,程序可以继续正常运行,并不会像之前一样直接结束掉。
注意,catch中捕获的类型只能是Throwable的子类,也就是说要么是抛出的异常,要么是错误,不能是其他的任何类型。
我们可以在catch
语句块中对捕获到的异常进行处理:
fun main() {
try { //使用try-catch语句进行异常捕获
val array = arrayOf(1, 2, 3)
println(array[3])
} catch (e: ArrayIndexOutOfBoundsException) {
e.printStackTrace(); //打印栈追踪信息
println("异常错误信息:"+e.message); //获取异常的错误信息
}
println("程序继续正常运行!")
}
当代码可能出现多种类型的异常时,我们希望能够分不同情况处理不同类型的异常,就可以使用多重异常捕获:
try {
//....
} catch (e: Exception) { //父类型在前,会将子类的也捕获
} catch (e: NullPointerException) { //因为NullPointerException是Exception的子类型,永远都不会进入这里
} catch (e: IndexOutOfBoundsException) { //永远都不会进入这里
}
最后,当我们希望,程序运行时,无论是否出现异常,都会在最后执行任务,可以交给finally
语句块来处理:
try {
//....
} catch (e: Exception) {
} finally {
println("lbwnb") //无论是否出现异常,都会在最后执行
}
注意:try
语句块至少要配合catch
或finally
中的一个。
try
也可以当做一个表达式使用,这意味着它可以有一个返回值:
fun main() {
val input = readln()
val a: Int? = try { input.toInt() } catch (e: NumberFormatException) { null }
println(a)
}
针对于空类型,我们也可以在判断为空时直接抛出异常:
val s = person.name ?: throw IllegalArgumentException("Name required")