类(class)详解
在任何一门面向对象编程的语言里,类(class
)是非常基础、但也是非常重要的一项组成,通俗的说就是万般皆对象,而所说的对象就是我们生成的类。Kotlin
也是如此,下面详细为大家介绍Kotlin
中的类的组成结构、函数、声明等。
一、类的声明
1、关键字
声明类的关键字为
class
2、声明格式
class Test{
// 属性...
...
// 构造函数
...
// 函数
...
// 内部类
...
...
}
其中:
当类没有结构体的时候,大括号可以省略。即:
class Test
二、类的构造函数
- 在
Kotlin
中,允许有一个主构造函数和多个二级构造函数(辅助构造函数)。其中主构造函数是类头的一部分。- 关键字或者构造函数名:
constructor(参数)
1、主构造函数
- 主构造函数是类头的一部分,类名的后面跟上构造函数的关键字以及类型参数。
1.1、举例说明:
class Test constructor(num : Int){
...
}
等价于
/*
因为是默认的可见性修饰符且不存在任何的注释符
故而主构造函数constructor关键字可以省略
*/
class Test(num: Int){
...
}
1.2、构造函数中的初始化代码块
- 构造函数中不能出现其他的代码,只能包含初始化代码。包含在初始化代码块中。
- 关键字:
init{...}
- 值得注意的是,
init{...}
中能使用构造函数中的参数
例:
fun main(args: Array<String>) {
// 类的实例化,会在下面讲解到,这里只是作为例子讲解打印结果
var test = Test(1)
}
class Test constructor(var num : Int){
init {
num = 5
println("num = $num")
}
}
输出结果为:
num = 5
其中,上面的constructor
关键字是可以省略的。
1.3、声明属性的简便方法
- 即在主构造函数中声明。
例:
class Test(val num1 : Int, var num2 : Long, val str : String){
...
}
则:相当于声明了3个属性。
其中,var
表示变量(可读写),val
表示常量(只读)。
1.4、什么时候constructor可以省略
- 在构造函数不具有注释符或者默认的可见性修饰符时,
constructor
关键字可以省略。- 默认的可见性修饰符时
public
。可以省略不写。
例:
// 类似下面两种情况的,都必须存在constructor关键字,并且在修饰符或者注释符后面。
class Test private constructor(num: Int){
}
class Test @Inject constructor(num: Int){
}
2、辅助(二级)构造函数
Kotlin
中支持二级构造函数。它们以constructor
关键字作为前缀。
2.1、声明
例:
class Test{
constructor(参数列表){
}
}
2.2、同时存在主构造函数和二级构造函数时的情况
- 如果类具有主构造函数,则每个辅助构造函数需要通过另一个辅助构造函数直接或间接地委派给主构造函数。 使用
this
关键字对同一类的另一个构造函数进行委派:
例:
fun main(args: Array<String>) {
var test1 = Test(1)
var test2 = Test(1,2)
}
// 这里是为了代码清晰,故而没有隐藏constructor关键字
class Test constructor(num: Int){
init {
println("num = $num")
}
constructor(num : Int, num2: Int) : this(num) {
println(num + num2)
}
}
说明:二级构造函数中的参数1(num
),是委托了主构造函数的参数num
。
可以看出,当实例化类的时候只传1个参数的时候,只会执行init
代码块中的代码。当传2个参数的时候,除了执行了init
代码块中代码外,还执行了二级构造函数中的代码。
输出结果为:
num = 1
num = 1
3
2.3、当类的主构造函数都存在默认值时的情况
- 在
JVM
上,如果类主构造函数的所有参数都具有默认值,编译器将生成一个额外的无参数构造函数,它将使用默认值。 这使得更容易使用Kotlin
与诸如Jackson
或JPA
的库,通过无参数构造函数创建类实例。- 同理可看出,当类存在主构造函数并且有默认值时,二级构造函数也适用
例:
fun main(args: Array<String>) {
var test = Test()
var test1 = Test(1,2)
var test2 = Test(4,5,6)
}
class Test constructor(num1: Int = 10 , num2: Int = 20){
init {
println("num1 = $num1\t num2 = $num2")
}
constructor(num1 : Int = 1, num2 : Int = 2, num3 : Int = 3) : this(num1 , num2){
println("num1 = $num1\t num2 = $num2 \t num3 = $num3")
}
}
输出结果为:
num1 = 10 num2 = 20
num1 = 1 num2 = 2
num1 = 4 num2 = 5
num1 = 4 num2 = 5 num3 = 6
说明: 当实例化无参的构造函数时。使用了参数的默认值。
三、类的实例化
- 创建一个类的实例,需要调用类的构造函数,就像它是一个常规函数一样:
例:
var test = Test()
var test1 = Test(1,2)
其实在上面的例子中就实例化类的运用。
注意:这里和Java
不同的点是,没有new
这个关键字
四、类的组成
- 类的构成由
构造函数和初始化代码块、属性(字段)、函数(方法)、内部类(嵌套类)、对象声明
五部分组成
1、构造函数和初始化代码块
在此篇文章已经讲解,不清楚请再次熟悉下此篇文章中的构造函数模块。
2、属性(字段)
见下文: 属性与字段详解。
3、函数(方法)
见下文:函数基础总结。
4、内部类(嵌套类)
5、对象声明
见下文:对象声明。
五、类的类别
Kotlin
中的类可详细的分为:密封类、内部类(嵌套类)、抽象类、枚举类、接口类、数据类
1、密封类
2、内部类(嵌套类)
内部类
、嵌套类
。
3、抽象类
4、枚举类
5、接口类
6、数据类
六、类的继承
七、总结
关于类的介绍就到这里,不清楚的可以多看看文章。不过也没太大关系,此篇文章大多数都是理论性东西。自己用Kotlin
多写写代码就理解、掌握了。
数据类(data)、密封类(sealed)详解
在Koltin
中,除了接口类、枚举类
之外,还有抽象类、内部类、数据类
以及密封类
。在今天的章节中,为大家详细讲解数据类
和密封类
。
一、数据类
- 在
Java
中,或者在我们平时的Android
开发中,为了解析后台人员给我们提供的接口返回的Json
字符串,我们会根据这个字符串去创建一个类
或者实例对象
,在这个类中,只包含了一些我们需要的数据,以及为了处理这些数据而所编写的方法。这样的类,在Kotlin
中就被称为数据类
。
1、关键字
声明数据类的关键字为:
data
1.1、声明格式
data class 类名(var param1 :数据类型,...){}
或者
data class 类名 可见性修饰符 constructor(var param1 : 数据类型 = 默认值,...)
说明:
data
为声明数据类
的关键字,必须书写在class
关键字之前。- 在没有结构体的时候,大括号
{}
可省略。- 构造函数中必须存在至少一个参数,并且必须使用
val
或var
修饰。这一点在下面数据类特性中
会详细讲解。- 参数的默认值可有可无。(若要实例一个无参数的数据类,则就要用到默认值)
例:
// 定义一个名为Person的数据类
data class Preson(var name : String,val sex : Int, var age : Int)
1.2、约定俗成的规定
- 数据类也有其约定俗成的一些规定,这只是为增加代码的阅读性。
即,当构造函数中的参过多时,为了代码的阅读性,一个参数的定义占据一行。
例:
data class Person(var param1: String = "param1",
var param2: String = "param2",
var param3 : String,
var param4 : Long,
var param5 : Int = 2,
var param6 : String,
var param7 : Float = 3.14f,
var param8 : Int,
var param9 : String){
// exp
.
.
.
}
1.3、编辑器为我们做的事情
当我们声明一个数据类
时,编辑器自动为这个类做了一些事情,不然它怎么又比Java
简洁呢。它会根据主构造函数中所定义的所有属性自动生成下列方法:
- 生成
equals()
函数与hasCode()
函数- 生成
toString()
函数,由类名(参数1 = 值1,参数2 = 值2,....)
构成- 由所定义的属性自动生成
component1()、component2()、...、componentN()
函数,其对应于属性的声明顺序。- copy()函数。在下面会实例讲解它的作用。
其中,当这些函数中的任何一个在类体中显式定义或继承自其基类型,则不会生成该函数
2、数据类的特性
数据类
有着和Kotlin
其他类不一样的特性。除了含有其他类的一些特性外,还有着其独特的特点。并且也是数据类
必须满足的条件:
- 主构造函数需要至少有一个参数
- 主构造函数的所有参数需要标记为 val 或 var;
- 数据类不能是抽象、开放、密封或者内部的;
- 数据类是可以实现接口的,如(序列化接口),同时也是可以继承其他类的,如继承自一个密封类。
3、用实例说明其比Java
的简洁性
3.1、数据类的对比
Kotlin版:
data class User(val name : String, val pwd : String)
Java版:
public class User {
private String name;
private String pwd;
public User(){}
public User(String name, String pwd) {
this.name = name;
this.pwd = pwd;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPwd() {
return pwd;
}
public void setPwd(String pwd) {
this.pwd = pwd;
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", pwd='" + pwd + '\'' +
'}';
}
}
分析:实现同一个功能,从代码量来说,Koltin
比Java
少了很多行代码,比起更简洁。
3.2、修改数据类属性
例:修改User
类的name
属性
Kotlin版:
Koltin
要修改数据类的属性,则使用其独有的copy()
函数。其作用就是:修改部分属性,但是保持其他不变
val mUser = User("kotlin","123456")
println(mUser)
val mNewUser = mUser.copy(name = "new Kotlin")
println(mNewUser)
输出结果为:
User(name=kotlin, pwd=123456)
User(name=new Kotlin, pwd=123456)
Java版:
User mUser = new User("Java","123456");
System.out.println(mUser);
mUser.setName("new Java");
System.out.println(mUser);
输出结果为:
User{name='Java', pwd='123456'}
User{name='new Java', pwd='123456'}
分析:从上面对两种方式的实现中可以看出,Kotlin
是使用其独有的copy()
函数去修改属性值,而Java
是使用setXXX()
去修改
4、解构声明
- 在前面讲到,
Kotlin
中定义一个数据类,则系统会默认自动根据参数的个数生成component1() ... componentN()
函数。其...,componentN()
函数就是用于解构声明的
val mUser = User("kotlin","123456")
val (name,pwd) = mUser
println("name = $name\tpwd = $pwd")
输出结果为:
name = kotlin pwd = 123456
5、系统标准库中的标准数据类
- 标准库提供了 Pair 和 Triple。尽管在很多情况下命名数据类是更好的设计选择, 因为它们通过为属性提供有意义的名称使代码更具可读性。
- 其实这两个类的源码部分不多,故而贴出这个类的源代码来分析分析
5.1、源码分析
@file:kotlin.jvm.JvmName("TuplesKt")
package kotlin
// 这里去掉了源码中的注释
public data class Pair<out A, out B>(
public val first: A,
public val second: B) : Serializable {
// toString()方法
public override fun toString(): String = "($first, $second)"
}
// 转换
public infix fun <A, B> A.to(that: B): Pair<A, B> = Pair(this, that)
// 转换成List集合
public fun <T> Pair<T, T>.toList(): List<T> = listOf(first, second)
// 这里去掉了源码中的注释
public data class Triple<out A, out B, out C>(
public val first: A,
public val second: B,
public val third: C ) : Serializable {
// toString()方法
public override fun toString(): String = "($first, $second, $third)"
}
// 转换成List集合
public fun <T> Triple<T, T, T>.toList(): List<T> = listOf(first, second, third)
分析:从上面的源码可以看出,标准库中提供了两个标准的数据类,Pair类
以及Triple类
.其中:
- 两个类中都实现了
toList()
方法以及toString()
方法。to()
方法乃Pair类
特有,起作用是参数转换Pair类
需要传递两个参数,Triple类
需要传递三个参数。
5.2、用法
val pair = Pair(1,2) // 实例
val triple = Triple(1,2,3) // 实例
println("$pair \t $triple") // 打印:即调用了各自的toString()方法
println(pair.toList()) // 转换成List集合
println(triple.toList()) // 转换成List集合
println(pair.to(3)) // Pair类特有: 其作用是把参数Pair类中的第二个参数替换
输出结果为:
(1, 2) (1, 2, 3)
[1, 2]
[1, 2, 3]
((1, 2), 3)
二、密封类
密封类是用来表示受限的
类继承结构
。
1、什么是受限的类继承结构
- 所谓受限的类继承结构,即当类中的一个值只能是有限的几种类型,而不能是其他的任何类型。
- 这种受限的类继承结构从某种意义上讲,它相当于是枚举类的扩展。但是,我们知道
Kotlin
的枚举类中的枚举常量是受限的,因为每一个枚举常量只能存在一个实例。- 但是其和枚举类不同的地方在于,密封类的一个子类可以有可包含状态的多个实例。
- 也可以说成,密封类是包含了一组受限的类集合,因为里面的类都是继承自这个密封类的。但是其和其他继承类(
open
)的区别在,密封类可以不被此文件外被继承,有效保护代码。但是,其密封类的子类的扩展是是可以在程序中任何位置的,即可以不再统一文件下。
上面的几点内容是密封类的特点,请详细的看下去,小生会对这几点内容进行详细的分析。
2、关键字
定义密封类的关键字:
sealed
2.1、声明格式
sealed class SealedExpr()
注意:密封类是不能被实例化的
即
val mSealedExpr = SealedExpr() // 这段代码是错误的,编译器直接会报错不能编译通过。
既然密封类
是不能实例化,那么我们要怎么使用,或者说它的作用是什么呢?请继续往下看
3、密封类的作用及其详细用法。
3.1、作用
用来表示受限的类继承结构。
例:
sealed class SealedExpr{
data class Person(val num1 : Int, val num2 : Int) : SealedExpr()
object Add : SealedExpr() // 单例模式
object Minus : SealedExpr() // 单例模式
}
// 其子类可以定在密封类外部,但是必须在同一文件中 v1.1之前只能定义在密封类内部
object NotANumber : SealedExpr()
分析:即所定义的子类都必须继承于密封类,表示一组受限的类
3.2、和普通继承类的区别
- 我们知道普通的继承类使用
open
关键字定义,在项目中的类都可集成至该类。- 而密封类的子类必须是在密封类的内部或必须存在于密封类的同一文件。这一点就是上面提到的有效的代码保护。
3.3、和枚举类的区别
- 枚举类的中的每一个枚举常量都只能存在一个实例。而密封类的子类可以存在多个实例。
例:
val mPerson1 = SealedExpr.Person("name1",22)
println(mPerson1)
val mPerson2 = SealedExpr.Person("name2",23)
println(mPerson2)
println(mPerson1.hashCode())
println(mPerson2.hashCode())
输出结果为:
Person(name=name1, age=22)
Person(name=name2, age=23)
-1052833328
-1052833296
3.4、其子类的类扩展实例
- 在
Kotlin
支持扩展功能,其和C#
、Go
语言类似。这一点是Java
没有的。
为了演示密封类的子类的扩展是可以在项目中的任何位置这个功能,大家可以下载源码。源码链接在文章末尾会为大家奉上。
例:
// 其存在于SealedClassDemo.kt文件中
sealed class SealedExpr{
data class Person(val name : String, val age : Int) : SealedExpr()
object Add : SealedExpr()
companion object Minus : SealedExpr()
}
object NotANumber : SealedExpr()
其存在TestSealedDemo.kt文件中
fun <T>SealedExpr.Add.add(num1 : T, num2 : T) : Int{
return 100
}
fun main(args: Array<String>) {
println(SealedExpr.Add.add(1,2))
}
输出结果为:
100
说明:上面的扩展功能没有任何的意义,只是为了给大家展示密封类子类的扩展不局限与密封类同文件这一个功能而已。
3.5、使用密封类的好处
- 有效的保护代码(上面已说明原因)
- 在使用
when
表达式 的时候,如果能够验证语句覆盖了所有情况,就不需要为该语句再添加一个else
子句了。
例:
sealed class SealedExpr{
data class Person(val name : String, val age : Int) : SealedExpr()
object Add : SealedExpr()
companion object Minus : SealedExpr()
}
object NotANumber : SealedExpr()
fun eval(expr: SealedExpr) = when(expr){
is SealedExpr.Add -> println("is Add")
is SealedExpr.Minus -> println("is Minus")
is SealedExpr.Person -> println(SealedExpr.Person("Koltin",22))
NotANumber -> Double.NaN
}
输出结果为:
is Minus
三、总结
在实际的项目开发当中,数据类(data
)类的用处是很多的,因为在开发APP
时,往往会根据后台开发者所提供的接口返回的json
而生成一个实体类,现在我们学习了数据类后,就不用再像Java
一样写那么多代码了,即使是用编辑器提供的方法去自动生成。但是代码量上就能节省我们很多时间,并且也更加简洁。何乐而不为呢!密封类的情况在实际开发中不是很常见的。只有当时特殊的需求会用到的时候,才会使用密封类。当然我们还是要学习的。
抽象类(abstract)、内部类(嵌套类)详解
在这个章节中会对Koltin
的抽象类
和内部类
作出一个详细的讲解。
一、抽象类
- 我们知道,在实际的开发程序的时候,一般都会写一个基类,封装常用方法、以及处理一些共有的逻辑,但是程序逻辑是根据每个类不同的功能实现不同的代码。而这个所谓的基类,一般都是一个抽象类。不管是
Java
还是Kotlin
,实现其抽象类的作用就在于此。那么什么是抽象类呢,它是怎么定义的,它又要怎么使用呢?
1、抽象类的定义
抽象类,可以理解为类定义了一个模板。所有的子类都是根据这个模板是填充自己的代码。
1.1、关键字
- 声明一个抽象(类或函数)的关键字为:
abstract
其中值得注意的是:抽象可以分为抽象类、抽象函数、抽象属性。而一个抽象类和普通类的区别在于抽象类除了可以有其自己的属性、构造函数、方法等组成部分,还包含了抽象函数以及抽象属性。
例:
abstract class Lanauage{
val TAG = this.javaClass.simpleName // 自身的属性
// 自身的函数
fun test() : Unit{
// exp
}
abstract var name : String // 抽象属性
abstract fun init() // 抽象方法
}
/**
* 抽象类Lanauage的实现类TestAbstarctA
*/
class TestAbstarctA : Lanauage(){
override var name: String
get() = "Kotlin"
set(value) {}
override fun init() {
println("我是$name")
}
}
/**
* 抽象类Lanauage的实现类TestAbstarctB
*/
class TestAbstarctB : Lanauage(){
override var name: String
get() = "Java"
set(value) {}
override fun init() {
println("我是$name")
}
}
fun main(args: Array<String>) {
// val lanauage = Lanauage() 是错误的,因为抽象类不能直接被实例化
val mTestAbstarctA = TestAbstarctA()
val mTestAbstarctB = TestAbstarctB()
println(mTestAbstarctA.name)
mTestAbstarctA.init()
println(mTestAbstarctB.name)
mTestAbstarctB.init()
}
输出结果为:
Kotlin
我是Kotlin
Java
我是Java
1.2、小结
- 抽象类本身具有普通类特性,以及组成部分。不过值得注意的是,抽象类不能直接被实例化
- 其抽象了类的子类必须全部重写带
abstract
修饰的属性和方法。- 抽象成员只有定义,没有实现。都有
abstract
修饰符修饰。- 抽象类是为其子类定义了一个模板。不同是类实现不同的功能
2、抽象类的规则
- 在
Kotlin
中的抽象类在顶层定义的时候只能使用public
可见性修饰符修饰。- 抽象类中可以定义内部抽象类。
- 只能继承一个抽象类。
- 若要实现抽象类的实例化,需要依靠子类采用向上转型的方式处理。
- 抽象类可以继承自一个继承类,即抽象类可以作为子类。不过,抽象类建议不用
open
修饰符修饰,因为可以覆写抽象类的父类的函数。
例:
open class Base{
open fun init(){}
}
abstract class Lanauage : Base(){
val TAG = this.javaClass.simpleName // 自身的属性
// 自身的函数
fun test() : Unit{
// exp
}
abstract var name : String // 抽象属性
abstract override fun init() // 覆写父类的方法
abstract class Name(){} // 嵌套抽象类,可查看第二节中的嵌套类使用
}
fun main(args: Array<String>) {
// 若要实现抽象类的实例化,需要依靠子类采用向上转型的方式处理。
val mLanauage : Lanauage = TestAbstarctB()
}
3、抽象类的实际应用
- 在
Java
的设计模式中,有一种设计模式叫模板设计模式
,其定义为:
- 定义一个操作中算法的骨架,而将一些步骤延迟到子类中,模板方法使得子类可以不改变算法的结构即可重定义该算法的某些特定步骤。
- 通俗点的理解就是 :完成一件事情,有固定的数个步骤,但是每个步骤根据对象的不同,而实现细节不同;就可以在父类中定义一个完成该事情的总方法,按照完成事件需要的步骤去调用其每个步骤的实现方法。每个步骤的具体实现,由子类完成。
Kotlin
和Java
是互通的,说明Kotlin
也是支持这种设计模式的。
二、内部类(嵌套类)
在实际开发中,用到内部类的地方是很多的。比如说:
- 对于
Android
开发来说,列表适配器(adapter
)中的ViewHolder
类,就是一个内部类。- 根据后台开发人员提供的
json
字符串生成的对象中,也包含另外一个对象,这也是一个内部类。
1、嵌套类
上面提到的两种情况,是在开发中最常见的。当然,说到内部类,就必须世道另一个概念嵌套类,所谓的嵌套类:即指一个类可以嵌套在其他类中。
例:
class Other{ // 外部类
val numOuther = 1
class Nested { // 嵌套类
fun init(){
println("执行了init方法")
}
}
}
fun main(args: Array<String>) {
Other.Nested().init() // 调用格式为:外部类.嵌套类().嵌套类方法/属性
}
输出结果为:
执行了init方法
注意:
- 调用嵌套类的属性或方法的格式为:
外部类.嵌套类().嵌套类方法/属性。在调用的时候嵌套类是需要实例化的
。- 嵌套类不能使用外部类的属性和成员
2、内部类
在上面的例子中讲解了嵌套类的使用,而内部类和嵌套类还是有一定的区别的,而且内部类是有特定的关键字去声明的。
2.1、关键字
声明一个内部类使用
inner
关键字。
声明格式:inner class 类名(参数){}
例:
class Other{ // 外部类
val numOther = 1
inner class InnerClass{ // 嵌套内部类
val name = "InnerClass"
fun init(){
println("我是内部类")
}
}
}
fun main(args: Array<String>) {
Other().InnerClass().init() // 调用格式为:外部类().内部类().内部类方法/属性
}
注意:
- 调用内部类的属性或方法的格式为:
外部类().内部类().内部类方法/属性。在调用的时候嵌套类是需要实例化的
。- 内部类不能使用外部类的属性和成员
2.2、匿名内部类
作为一名
Android
开发者,对匿名内部类都不陌生,因为在开发中,匿名内部类随处可见。比如说Button
的OnClickListener
,ListView
的单击、长按事件等都用到了匿名内部类。
一般的使用方式为定义一个接口,在接口中定义一个方法。
例:
class Other{
lateinit private var listener : OnClickListener
fun setOnClickListener(listener: OnClickListener){
this.listener = listener
}
fun testListener(){
listener.onItemClick("我是匿名内部类的测试方法")
}
}
interface OnClickListener{
fun onItemClick(str : String)
}
fun main(args: Array<String>){
// 测试匿名内部类
val other = Other()
other.setOnClickListener(object : OnClickListener{
override fun onItemClick(str: String) {
// todo
println(str)
}
})
other.testListener()
}
输出结果为:
我是匿名内部类的测试方法
遗留的问题
在上面实现的匿名内部类是很常规的用法以及写法。在我们的实际开发当中也是大家熟知的写法。但是在我们实际开发当中,会引入
lambda
语法糖,让我们的项目支持lambda
语法,简化代码量。
3、局部类
所谓局部类,这一点和
Java
是一致的。即定义在方法(函数)中的类。
例:
class Other{ // 外部类
val numOther = 1
fun partMethod(){
var name : String = "partMethod"
class Part{
var numPart : Int = 2
fun test(){
name = "test"
numPart = 5
println("我是局部类中的方法")
}
}
val part = Part()
println("name = $name \t numPart = " + part.numPart + "\t numOther = numOther")
part.test()
println("name = $name \t numPart = " + part.numPart + "\t numOther = numOther")
}
}
fun main(args: Array<String>) {
// 测试局部类
Other().partMethod()
}
输出结果为:
name = partMethod numPart = 2 numOther = 1
我是局部类中的方法
name = test numPart = 5 numOther = 1
通过上面的实例:我们可以看出:
- 局部类只能在定义该局部类的方法中使用。
- 定义在实例方法中的局部类可以访问外部类的所有变量和方法。但不能修改
- 局部类中的可以定义属性、方法。并且可以修改局部方法中的变量。
4、静态类
熟悉
Java
的朋友都知道Java
的静态类,或者说用static
修饰符修饰的类。但是在Kotlin
中,是不存在static
关键字的。那么我们怎样去实现一个静态类呢?
关于静态类的使用,以及静态类的语法。以及Koltin
的单例模式实现。由于篇幅原因我在这里就不展示了。有兴趣的朋友请参见kotlin中的object更像是语法糖。这篇文章是别的大牛诠释静态类以及单例实现很好的文章。后面我会出一篇详细的文章为大家讲解。
三、总结
在学完本篇博文中,你应该掌握抽象类
的作用,掌握其和普通类
、接口类
、继承类
的区别所在,了解实现抽象类
的意义,或者说在项目中为什么要用抽象类
去编写一个基类
等。
对于嵌套类
和内部类
而言,知道这两者的区别所在,和熟知他们在项目中用在什么地方就够了。对于静态类
来说,常用的实现都是用其去实现一个单例模式。在Koltin
的不像Java
一样实现很多的工具类,因为Kotlin
中的扩展功能很强大。可以用扩展去替换掉大部分的工具类。
枚举类(Enum)、接口类(Interface)详解
这篇文章就详细说一说Kotlin
中的枚举类(Enum
)、接口类(Interface
)的使用。
一、枚举类
1.1、声明方式及枚举常量
- 关键字:
enum
- 枚举常量:即枚举类下的对象,每个枚举类包含0个到多个枚举常量。
1.1.1、声明
enum
关键字在类头中的class
关键字前面
声明格式:
enum class 类名{
...
}
1.1.2、枚举常量
枚举类中的每一个枚举常量都是一个对象,并且他们之间用逗号分隔。
例:
/**
* 例:关于一个网络请求结果的枚举类
*/
enum class State{
/*
NORMAL : 正常
NO_DATA : 数据为空
NO_INTERNET : 网络未连接
ERROR : 错误
OTHER : 其他
*/
NORMAL,NO_DATA,NO_INTERNET,ERROR,OTHER
}
1.1.3、访问枚举常量
- 不需要实例化枚举类就可以访问枚举常量
使用方式为:
枚举类名.枚举常量.属性
通过上面例子来实例讲解:
// 使用中缀符号访问枚举常量
State.NORMAL.name
State.NO_DATA.name
State.NO_INTERNET.name
State.ERROR.name
State.OTHER.name
这里只是让大家明白怎样去访问一个枚举常量。没有讲解到枚举常量的使用。枚举常量的使用请大家耐心的看下去。在下面会详细介绍怎样去使用它。
1.2 、枚举常量的初始化
- 因为每一个枚举都是枚举类的实例,所以他们可以是初始化过的。
例:
enum class Color(var argb : Int){
RED(0xFF0000),
WHITE(0xFFFFFF),
BLACK(0x000000),
GREEN(0x00FF00)
}
1.3、枚举常量的匿名类
- 要实现枚举常量的匿名类,则必须提供一个抽象方法(必须重写的方法)。且该方法定义在枚举类内部。而且必须在枚举变量的后面。
- 枚举变量之间使用逗号(
,
)分割开。但是最后一个枚举变量必须使用分号结束。不然定义不了抽象方法。- 在上面已经说过,每一个枚举常量就是一个对象。
例:
fun main(args: Array<String>) {
ConsoleColor.BLACK.print()
}
enum class ConsoleColor(var argb : Int){
RED(0xFF0000){
override fun print() {
println("我是枚举常量 RED ")
}
},
WHITE(0xFFFFFF){
override fun print() {
println("我是枚举常量 WHITE ")
}
},
BLACK(0x000000){
override fun print() {
println("我是枚举常量 BLACK ")
}
},
GREEN(0x00FF00){
override fun print() {
println("我是枚举常量 GREEN ")
}
};
abstract fun print()
}
输出结果为:
我是枚举常量 BLACK
1.4、枚举类的使用
- 每个枚举常量都包含两个属性:
name(枚举常量名)
和ordinal(枚举常量位置)
- 提供了
values()
和valueOf()
方法来检测指定的名称与枚举类中定义的任何枚举常量是否匹配。- 自
Kotlin 1.1
起,可以使用enumValues<T>()
和enumValueOf<T>()
函数以泛型的方式访问枚举类中的常量。
1.4.1、访问枚举变量属性
例:
fun main(args: Array<String>) {
println("name = " + Color.RED.name + "\tordinal = " + Color.RED.ordinal)
println("name = " + Color.WHITE.name + "\tordinal = " + Color.WHITE.ordinal)
println("name = " + Color.BLACK.name + "\tordinal = " + Color.BLACK.ordinal)
println("name = " + Color.GREEN.name + "\tordinal = " + Color.GREEN.ordinal)
}
enum class Color(var argb : Int){
RED(0xFF0000),
WHITE(0xFFFFFF),
BLACK(0x000000),
GREEN(0x00FF00)
}
输出结果为:
name = RED ordinal = 0
name = WHITE ordinal = 1
name = BLACK ordinal = 2
name = GREEN ordinal = 3
1.4.2、使用enumValues<T>()
和 enumValueOf<T>()
访问
例: 枚举类还是上面例子中的Color
类
println(enumValues<Color>().joinToString { it.name })
println(enumValueOf<Color>("RED"))
输出结果为:
RED, WHITE, BLACK, GREEN
RED
1.4.3、使用valueOf()
和values()
检测
例:
println(Color.valueOf("RED"))
println(Color.values()[0])
println(Color.values()[1])
println(Color.values()[2])
println(Color.values()[3])
输出结果为:
RED
RED
WHITE
BLACK
GREEN
其中,若使用Color.valueOf("不存在的枚举常量")
,则会抛出IllegalArgumentException
异常,即枚举变量不存在。若使用Color.values()[大于枚举常量位置]
,则会抛出下标越界异常。
1.5、枚举类的源码分析
即
Enum.kt
这个源文件。
在这里我大致的说明一下这个源文件的方法、属性等。有兴趣的可以去看看这个源文件。其实里面也没几个方法。
1.5.1、默认实现了companion object {}
这也是我们访问枚举常量无需实例化枚举类的原因。
1.5.2、仅提供了两个属性
- 即我们上面用到的枚举常量名称(
name
)和枚举常量位置(ordinal
)
贴上这两个属性的源码:
/**
* Returns the name of this enum constant, exactly as declared in its enum declaration.
*/
public final val name: String
/**
* Returns the ordinal of this enumeration constant (its position in its enum declaration, where the initial constant
* is assigned an ordinal of zero).
*/
public final val ordinal: Int
1.5.3、实现了Comparable
接口
- 这也是我们能获取枚举常量位置的原因。
这是Enum.kt
源文件。让大家看看它实现了Comparable
接口
public abstract class Enum<E : Enum<E>>(name: String, ordinal: Int): Comparable<E>{
...
}
再来看看Comparable.kt
里面做了些什么。其实里面就提供了一个方法罢了...
public interface Comparable<in T> {
/**
* Compares this object with the specified object for order. Returns zero if this object is equal
* to the specified [other] object, a negative number if it's less than [other], or a positive number
* if it's greater than [other].
*/
public operator fun compareTo(other: T): Int
}
关于枚举类的讲解就写到这里了。不清楚的可以多看看文章,或者看看源码、官方文档等等。当然,自己按照我的例子去敲一遍代码也是非常不错的。
二、接口类
2.1、接口的基础使用
2.1.1、声明
关键字:
interface
定义格式:
interface 接口名{
...
}
2.1.2、用法
- 关键字:冒号(
:
),这一点是和Java
不同的。Java
中使用接口使用的是implements
关键字- 在
Kotlin
中冒号(:
)使用的地方很多:
- 用于变量的定义
- 用于继承
- 用于接口
- 方法的返回类型声明
使用格式:
class 类名 : 接口名{
// 重写的接口函数、属性等
...
}
2.1.3、举例说明
fun main(args: Array<String>) {
// 类的初始化
var demo = Demo1()
demo.fun1()
}
/**
* 我定义的接口
*/
interface Demo1Interface{
// 定义的方法
fun fun1()
}
/**
* 接口的实现类
*/
class Demo1 : Demo1Interface{
override fun fun1() {
println("我是接口中的fun1方法")
}
}
输出结果为:
我是接口中的fun1方法
2.2、接口中的方法使用
- 不带结构体的函数可以省略大括号,且不用强制重写带结构体的函数就可以直接调用。不太明白也没关系,下面的代码中都有注释。
例:
fun main(args: Array<String>) {
var demo = Demo2()
demo.fun1()
demo.fun2(5)
println(demo.fun3(10))
println(demo.fun4())
//可以不重写该方法直接调用
demo.fun5()
}
interface Demo2Interface{
/**
* 定义一个无参数无返回值的方法
*/
fun fun1()
/**
* 定义一个有参数的方法
*/
fun fun2(num: Int)
/**
* 定义一个有参数有返回值的方法
*/
fun fun3(num: Int) : Int
// 下面的两个方法是有结构体, 故可以不重写
/**
* 定义一个无参数有返回值的方法
*/
fun fun4() : String{
return "fun4"
}
/**
* 定义一个无结构体函数,大括号是可以省略的
*/
fun fun5(){
// 如果函数中不存在表达式,大括号可以省略。
// 如fun1一样
}
}
class Demo2 : Demo2Interface{
override fun fun1() {
println("我是fun1()方法")
}
override fun fun2(num: Int) {
println("我是fun2()方法,我的参数是$num")
}
override fun fun3(num: Int): Int {
println("我是fun3()方法,我的参数是$num,并且返回一个Int类型的值")
return num + 3
}
override fun fun4(): String {
println("我是fun4()方法,并且返回一个String类型的值")
/*
接口中的fun4()方法默认返回”fun4“字符串.
可以用super.fun4()返回默认值
也可以不用super关键字,自己返回一个字符串
*/
return super.fun4()
}
/*
接口中的fun5()带有结构体,故而可以不用重写,
fun4()同样
*/
// override fun fun5() {
// super.fun5()
// }
}
输出结果为:
我是fun1()方法
我是fun2()方法,我的参数是5
我是fun3()方法,我的参数是10,并且返回一个Int类型的值
13
我是fun4()方法,并且返回一个String类型的值
fun4
2.3、接口中的属性使用
- 在接口中申明属性。接口中的属性要么是抽象的,要么提供访问器的实现。接口属性不可以有后备字段。而且访问器不可以引用它们。
2.3.1、作为抽象
- 即重写属性的时候是在实现类的类参数中。这也是用代码提示去重写的实现方法
例:
fun main(args: Array<String>) {
var demo = Demo3(1,2)
println(demo.sum())
}
interface Demo3Interface{
val num1: Int
val num2 : Int
}
class Demo3(override val num1: Int, override val num2: Int) : Demo3Interface{
fun sum() : Int{
return num1 + num2
}
}
输出结果为:
3
2.3.2、作为访问器
即手动方式去实现重写,并提供get()方法
例:
fun main(args: Array<String>) {
println(demo.result())
// 在这里也可以改变接口属性的值
demo.num4 = 10
println(demo.result())
}
interface Demo3Interface{
// 声明比那俩和提供默认值
// 注意: val num3: Int = 3 这种方式不提供,为直接报错的
val num3: Int
get() = 3
val num4: Int
}
class Demo3(override val num1: Int, override val num2: Int) : Demo3Interface{
// 提供访问器实现
override val num3: Int
get() = super.num3
// 手动赋值
override var num4: Int = 4
fun result() : Int{
return num3 + num4
}
}
输出结果为:
7
13
2.4、接口的冲突问题解决
- 该问题是指当我们在父类中声明了许多类型,有可能出现一个方法的多种实现。
例:
fun main(args: Array<String>) {
// 类的初始化
val demo = Demo4()
demo.fun1()
demo.fun2()
}
interface Demo4InterfaceOne{
fun fun1(){
println("我是Demo4InterfaceOne中的fun1()")
}
fun fun2(){
println("我是Demo4InterfaceOne中的fun2()")
}
}
interface Demo4InterfaceTwo{
fun fun1(){
println("我是Demo4InterfaceTwo中的fun1()")
}
fun fun2(){
println("我是Demo4InterfaceTwo中的fun2()")
}
}
class Demo4 : Demo4InterfaceOne,Demo4InterfaceTwo{
override fun fun1() {
super<Demo4InterfaceOne>.fun1()
super<Demo4InterfaceTwo>.fun1()
}
override fun fun2() {
super<Demo4InterfaceOne>.fun2()
super<Demo4InterfaceTwo>.fun2()
}
}
说明:Demo4
实现了Demo4InterfaceOne
和Demo4InterfaceTwo
两个接口,而两个接口中都存在两个相同方法名的方法。因此编译器不知道应该选哪个,故而我们用super<接口名>.方法名
来区分。
三、 总结
对于接口类来说,它在一个项目中是重中之重的,对于项目中代码的耦合性、便利性都能用接口类去实现一个良好的项目架构,对项目后期的维护或者说重构来说,都能有良好的体现。