«

Java崽学习Kotlin---Kotlin入门基础语法

ZealSinger 发布于 阅读:72 Kotlin


基础语法

变量定义

Kotlin中定义变量的方式主要有两种:var 和 val

 

数据类型

基础数据类型

Kotlin中所有的基础数据类型都是对象,这个和Java是不一样的,Java中的基础数据类新,例如int,float,double等等,八大基础类型都只是一个类型而不是对象,对应的只有其包装类才是对象

而Kotlin中就没有包装类和非包装类的区别,因为基础类型也本身就是对象,或者说没有非包装类了

基础数据类型还是一样的,Int , Float ,Double ,Short ,Byte ,Boolean ,Char

所有的数组类型即Int Folat Double Short Byte 都实现了Numbger的接口,而Number的接口带有相互转换的接口,也就是说每个数字类型天生具备了转化为其他数字类型的方法能力,但是也同时代表了每个类型之间都是单独的对象,不存在继承关系,也就不能直接强转(Java中byte可以转int,在kotlin中就不可以)

image-20250426112308677

 

复杂/引用数据类型

Any

Any的作用在Kotlin中就是相同与Java中的Object,是所有对象的基类/父类

带有hashCode 和 equal方法

QQ_1745653314336

String可以发现,String的源码底层就是继承的CharSequeue即字符序列,简单理解就是底层也是一个字符数组image-20250426143048848Kotlin中的String的操作相对而言比Java的灵活,可以看到,除了正常的用 .length 直接获取长度之外,还有如下var s:String = ...
s.length   // 获取长度
s.first()  // 获取第一个字符
s.last()   // 获取最后一个字符
s.get(x) / s[x]  // 获取索引为x位置上的数据 kotlin的索引也是从0开始算起的image-20250426143911120对于字符串的拼接,有 加号+ 和 plus()方法两种方式    val myName : String = "ZealSinger"
   val mySmallName : String = "CC"
   val myAllName : String = myName + mySmallName // 直接用+
   println("myAllName=$myAllName")
   val myAllName2 = myName.plus(mySmallName)  // 使用plus方法
   println("myAllName2="+myAllName2)除此之外,在输出语句的时候想要凭借,可以和Java一样用 “+ 变量名”的方式进行拼接,也可以使用String串联模板println("myAllName2="+myAllName2)  // 类似Java的输出方式

//串联模板 采用$变量名 或者 ${变量名} 的方式拼接 {}主要是为了和后面的字符内容连接不容易进行区分导致变量名无法识别,所以最好是${}的方法(第一个$myName 和 + 之间空了一个空格所以能正常区分)
println("myName:$myName + mySmallName:${mySmallName}=$myAllName ")  对于多行字符串,支持三引号保留格式,使用三引号可以包留字符串的原本的格式,包括缩进和换行,如下案例,直接将整个三引号范围内的直接输出了fun main() {
   val txt = """
       abc
        abc
       abc
   """
   println(txt)
}QQ_1745653009275为了搭配三引号使用,String类型有一个trimIndent()它能自动去除每行共同的最小前导缩进,使得字符串内容对齐,以下是详细说明和示例:val text = """
   Hello,
   Kotlin!
   This is a multi-line string.
""".trimIndent()

println(text)每行共同最小前导缩进为4个空格,所以输出之后就会消除前面的4个空格// 输出
Hello,
Kotlin!
This is a multi-line string.QQ_1745653157736nothingnothin这玩意儿稍微有点别扭,可以理解为null,但是又和Java中的null不太一样官方给出的解释如下image-20250427111148337可以看到如下,我们直接将b=null的方式进行赋值,可以看到在自动推导的作用下,null其实就是Nothing的数据类型image-20250427110923531我们以前的理解就是,null就是没有,常见的就是空指针异常,当我们对一个明确数据类型的数据进行赋值的时候,例如下var b: Int = null这个是会报错的,在kotlin中会存在空指针检测/空安全,不会允许你初始化变量为空的,但是如果你想这么做,可以通过在数据类型后面加问号?进行处理,告知编译器这个变量可能为空var a: Int? = nullimage-20250427111511350当数据类型后面加了?,也就代表可能为空,编译器就会在这个变量使用的时候,通过 变量? 的方式,进行一个判空判断,如下 a?.plus()就会去判断a是否为null。如果为null则不会执行后面的调用,如果不为null才会执行,这样就能比较有效的避免空指针问题var a: Int? =null
var b: Int? =10
println(a?.plus(3))  // 输出null
println(b?.plus(3)) // 输出10这里需要注意一点,null或者说nothing,也可以使用toString的方法,kotlin中的toString是在Any对象下的,哪怕是nothing也可以toString转化为一个字符串var a: Int? =null
println(a.toString())  // 输出null如果说我们不想进行空判断,就是我们知道一个变量初始值为null,但是在调用的地方一定不为null,那么可以使用双感叹号进行强制使用 如果强制使用但是最后a确实为null的话,就会出现空指针异常的错误,并且因为只要变量在前面用过一次双感叹号,也就是代表你上面已经默认了不会为null,所以下面的调用逻辑就不会再次检测var a: Int? =null
println(a.plus(3))  // 爆红
println(a!!.plus(3))  // 不暴红
println(a.plus(3))  // 不爆红 因为上面使用过双感叹号了数组Arraykotlin官方本身不太建议使用数组,大部分情况下推荐使用集合标准的创建数组的方式语法为val/var  数组名:Array<数据类型泛型1> = arrayOf<数据类型泛型二>(初始化元素列表)
数据类型泛型1和泛型2有一个写了即可

Eg:
// 创建且初始化 因为kotlin对于基础类型就有包装和加强 所以这里创建的其实是相当于Integer[]
val arrayInt : Array<Int> = arrayOf<Int>(1,2,3,4,5)  
val arrayString : Array<String> = arrayOf("abc","efg")
除此之外,koltin还提供了很多快速创建指定类型的数组的方法这种封装的方法其实就可以理解为Java中对应的基本数据类型的数组int[] double[] float[]val arrayIntTwo : IntArray = intArrayOf(2,3,4,5) // 创建且初始化
val array: IntArray = IntArray(4) // 创建 容量为4 但是不初始化 全部为默认值
val arrayDouble : DoubleArray = doubleArrayOf()
val arrayFloat : FloatArray = floatArrayOf()
val arrayLong : LongArray = longArrayOf()QQ_1745654247831然后是对于空容量数组(即size为0的数组)的创建// 返回空数组
val emptyIndArray: Array<Int> = emptyArray()
val emptyDoubleArray : DoubleArray = doubleArrayOf()
val emptyFloatArray : FloatArray = emptyArray()  // 报错 因为 emptyArray()返回的是泛型的Array而不是封装好的指定类型的Array,类型冲突 这里很好说明 左边这种封装好的对应的是float[]类型 而emptyArray返回的一定是Any[] 或者说是 Object[] float这种基础数据类型是无法使用null标记的 自然就无法匹配上空数组的创建(内部元素都是null)// 该方法的本之是填充null 所以只有Object[]才能为null 所以左边只能是Array<>带泛型的形式 而不能是 IntArry DoubleArray这种基本数据类型的形式
var array3: Array<Int?> = arrayOfNulls(10) QQ_1745656187356多维数组采用{}的形式创建或者嵌套arrayOf()的方式进行创建val twoDArray = Array(2) { Array<Int>(2) { 0 } } // 快速创建一个2*2的数组且全部初始化为0
var twoDArray2 = arrayOf(arrayOf(1,2), arrayOf(3,4)) // 创建一个2*2每个元素单独初始化的数组
var twoDArray2 = arrayOf(arrayOf(1,2), arrayOf(3)) // 也是可以的QQ_1745656668463对于数组的修改和访问,也是通过数据名[索引]的方式进行访问fun main() {
   val simpleArray = arrayOf(1, 2, 3)
   val twoDArray = Array(2) { Array<Int>(2) { 0 } }

   // Accesses the element and modifies it
   simpleArray[0] = 10
   twoDArray[0][0] = 2

   // Prints the modified element
   println(simpleArray[0].toString()) // 10
   println(twoDArray[0][0].toString()) // 2
}不同的是,kotlin中提供了对数组进行比较的方法contentEquals() 和 contentDeepEqueals()contentEquals是一层纬度的比较,只会比较一层;contentDeepEquals会比较多层,前者效率更高但是适用范围小一些,后者效率低一些但是适用范围更加广,两者都有两种使用格式    val simpleArray = arrayOf(1, 2, 3)
   val anotherArray = arrayOf(1, 2, 3)

   // Compares contents of arrays
   println(simpleArray.contentEquals(anotherArray))// true
   println(simpleArray.contentDeepEquals(anotherArray))// true
   

   simpleArray[0] = 10  // 修改索引位置0上的数据为10
// 这个contentEquals使用方式和上面的不一样,可以看出来contentEquals有两种使用格式;同理 contentDeepEquals也一样可以
   println(simpleArray contentEquals anotherArray) // false val matrix1 = arrayOf(
       arrayOf(1, 2),
       arrayOf(3, 4)
  )
val matrix2 = arrayOf(
       arrayOf(1, 2),
       arrayOf(3, 4)
  )
val matrix3 = arrayOf(
       arrayOf(1, 2),
       arrayOf(3, 5) // 最后一元素不同
  )

// 都返回false(只会比较外层 即二维数组内的每个一位数组的引用是否相同 上面每个二维数组的子一维数组都是新创建的对象,自然都不一样)
println(matrix1.contentEquals(matrix2))
println(matrix1.contentEquals(matrix3))

// 第一个返回true 第二个返回false
// contentDeepEquals()深层比较 对于多维数组会层级比较数值是否相同
println(matrix1.contentDeepEquals(matrix2))
println(matrix1.contentDeepEquals(matrix3))除此之外,还有一些方法val arr = arrayOf(1,6,3,4,7)
// 求数组值之和
arr.sum()

// 随机打乱数组的顺序 无返回值
arr.shuffle()
println(arr.joinToString()) //4 3 1 6 7
arr.shuffle()
println(arr.joinToString()) //7 6 3 4 1


// 反转数组
arr.reverse()

//获取数组元素 除了常规的arr[x]的方式 还可以利用componentX()方法 且这种方式最多获取前5个元素,即只存在component1() component2() .... component5()

// 需要注意的是componentX的X是从1开始算起的 和索引下标从0开始 是不一样的
println(arr.component1())  //获取第一个元素 打印1
println(arr.component2())  //获取第二个元素 打印6


集合

kotlin中所有的集合默认是不可变集合,主要分为三种类型List,Set,Map,三者都是接口而非实现类(和Java一样),都继承自Collection<out E>接口,而该接口又继承了Iterable<out T>接口,该接口只会提供size属性 ,isEmpty(),get(),contains()等方法,不可变集合不能进行内容的增删改

因为默认是不可变集合,所以对应的存在可变集合MutableList ; MutableSet ; MutableMap,这些可变集合都提供了对应的增删改的方法

image-20250426223623679

可变集合

上面有说到,可变集合就是加上了mutable的前缀,即mutableList,mutableSet,mutableMap,整体的创建方式和不可变集合差不多

 

这里需要注意一下可变集合和val定义不可变变量关键字之间的关系,val不可变值是内存地址不可变,类似于Java中final修饰数组一样,数组头部的地址不可变但是内容可变,val也就是类似final的效果,可变集合所说的也就是内容可变,val代表地址不可变即不能重新复制,所以两者不冲突

 

除此之外,我们在Java中对应的各种集合的不同实现类例如ArrayList ; LinkedList ; HashMap ;

TreeMap 都可以在Kotlin中进行创建,也都属于可变集合

 

我们知道,可变集合List和Map都有对应的get方法,list中一般为get(index)获取对应位置上的数据,map一般为get(key)获取对应key上的value,对于这两个get方法,kotlin都提供了类似数组的下标索引获取方式,即中括号的方式

 

image-20250427105423618

二元组和三元组(额外)

二元组就是我们上面说到的Pair对象,Kotlin中也存在三元组,也就是传入三个数据作为一个对象,Kotlin中用Triple(first,seconde,third)创建三元组

不可变集合

不可变集合主要通过listOf , setOf , mapOf进行构建创造

 

以list为例,可以看到,其对应的方法很少,都只有一些get(index),last(),subList等获取元素和截取list的相关方法,没有修改和添加和删除的方法,这也是不可变集合的特点

image-20250426212924051

从上述map的初始化创建中也可以看到,Java中的Map的底层是Entry数组,Kotlin中的Map的底层也是Entry对象,但是提供了一个Pair对象进行方便的键值对操作

不可变集合主要的利用是在一些特定的场景中,可以有效地避免增删改操作从而提高效率

集合的遍历

在Kotlin中集合的遍历是建议使用迭代器进行遍历的

通过迭代器遍历

直接使用 for(i in list) 即代表循环遍历list ,底层即依靠的迭代器对象iterator的next()和hashNext()进行实现的,可以类比Java中的增强for,Java中的增强for的底层也是iterator

当然,直接使用iterator无法方便的获取索引下标,kotlin中提供了一个withIndex()的方法,能让我们在遍历的时候同时获取到下标和数值,类似于Java中使用forEache遍历map

 

 

通过下标索引遍历

当然,直接通过下标的方式进行遍历也是可以的,如下,利用区间表达式和for循环和list.get() / l ist[] 的方式,就能很方便的进行下标迭代遍历

 
通过forEache方式遍历

也是类似的,forEache和Java中forEache大差不大,在Java中我们对于forEach遍历集合的时候,每个元素需要取一个变量名,在kotlin中如果不取变量名默认命名为it(即第一个forEach那样),使用 forEachIndexed也可以做到和上面list.withIndex()一样的效果,需要定义两个变量分别接收索引和数值

 

 

条件控制语句

所谓的条件控制语句,主要的就是我们Java中对应的if ; if-else ; switch 等语句

If语句

在kotlin中,if语句整体使用和Java中类似,但是特别的地方在于,kotlin中的if语句可以有返回值,在if每个判断情况最后面写一个数值就是对应的返回值,if左边用变量接受了就代表需要返回值,反之则不需要,如果需要返回值就需要保证每一个if分支情况都有返回值,有点类似于三元表达式但是更加的强大因为处理更多的情况的返回

自然 因为if 和 where存在返回值,所以没有单独设计三元表达式了,所以我们的三元表达式都需要用if和where实现

 

when表达式

kotlin中没有switch,但是用了when进行替代,对应的default也被替换为了else

对于when中的每一个情况采用 匹配模式/常量匹配 -> {...}的格式进行编写,每一个{}中不需要写break,kotlin会自动终止

如下

 

上述是when(number) 是代表这个when是基于number这个变量进行的,我们也可以直接when{}这样子不会基于某个变量,条件匹配更广,上面这种基于某个确切变量的不能使用布尔表达式和复杂的逻辑表达式&& || 这种进行

从下面案例中也可以看出来,when表达式也可以有返回值,我们这里写的Unit,Unit就是Java中的Void,代表无返回类型

 

对于上述when和if

QQ_1745744118125

while循环

while循环和Java中的类似,不多说了

 

break,continue,return和标签

整体使用和Java类似,但是因为标签的存在,使得比Java中更加灵活一点,标签可以理解为c++中的goto,能方便地进行嵌套for循环之间的跳出

对于如下代码,假设我需要找到第一个i=2*j的数据即可,那么在Java中,break只是将里面那一层进行break,依旧会进行i=2,3,4...的循环,但是我要找到第一个其实就可以直接终止了,那么如果要跳出外层循环就需要单独定义一个flag 然后在外层对flag进行判断

但是在kotlin中 借助标签 可以在外层for循环中打一个标签然后a (@表示打标签 a是标签名),然后在break后面直接加上a@表示直接break到外层for

同理 continue也是一样的,也可以通过 continue@a进行跳转式的continue

 

标签还有一个作用就是对于forEache的循环跳出也是比较方便的,因为forEach本身是一个函数式接口,底层是一个函数,所以在Java和kotlin中都不能在forEach中使用break和continue这种关键字,也就导致了forEach的循环必须全部遍历完成才能结束

在Kotlin中,因为标签的加入,可以让forEach提前跳出

如下,因为我们说到了forEache中无法使用break因为本质上是一个函数,所以我们用return+标签的形式进行返回试一下(上下两种效果是一样的 上面的是我们自定义了一个标签a,下面是kotlin自带的forEach标签,会寻找到最近的一个forEach),两者最后的输出也是一样的,只是在it=5的这样行进行了跳过(即输出1 2 3 4 6 7 8 9 10)这是因为整个forEache是一个过程,return+标签定位后回到的还是forEach里面,也就是相当于一层for循环里面使用了continue

 

为了达到跳出forEach循环的效果,还需要借助run{}这个高阶函数,本质上也是一个Lambda表达式,属于kotlin自带的,一般也会自带标签,也可自定义标签,我们利用run的标签实现整个forEach的中断操作

如下代码 最后上下两个循环都只打印了 1 2 3 4 也就是实现了foeEach的中断

 

 

区间表达式

在上面when表达式的时候,我们接触到了一个 1..10 代表 1<= x <=10 即表示一个左闭右闭的区间,这两个点点 .. 其实就是Kotlin的区间表达式的一种

除了左闭右闭,我们还存在别的区间表达式,常用的区间表达式如下

 

所有的区间表达式都默认以1为单位的step进行的跳跃,可以通过step指定跳跃的步数进行跳跃

 

除了整型数据的区间变化之外,也支持字符的区间变化,实质上也就是ASCII码的变化

 

函数

正常函数

函数的关键字是 fun 可以参考我们的默认的main函数,完整的标准函数结构如下

 

对于没有返回值的函数,我们可以不写函数返回值类型,因为Kotlin会自动推导,但是同时我们可以和Java一样用void进行表示,只是要记得Kotlin中的void是Unit

 

对于整个函数体只有一行的函数,可以不需要大括号和函数值返回类型,类似于定义变量和赋值的格式,如下sumOne 和 sumTwo两个函数 最终效果是一样的

 

infix

这个玩意儿有点意思可以看一下格式来体会,infix的主要功能个人认为是为了简化开发和阅读,infix标识的方法会代表它只能被一个指定类型的对象进行调用,然后参数列表只能接受一个参数(而且是必须接收,不能不接收,没有入参会报错),然后在函数内可以通过this来代指调用这个方法的那个对象

我们将上面的sum求和函数利用infix进行改造一下,我们改造为int类中数据的加法和float类型的加法,如下两个sum函数

 

从上面的过程可以看出来,有点类似给调用对象临时加了一个成员方法,在main函数中我们可以通过 . 的方式调用方法,类似Int对象1调用成员方法sum一样的感觉

而且简化后的 1 sum 10 和我们上面提到的map的初始化得时候传入key-value键值对使用A to B的形式很类似( var map:Map<Int,Int> = mapOf(1 to 2) ),其实我们查看一些to的源码就会发现,实际上就是一个infix函数

 

入参相关设计

类和对象

类的定义

Kotlin中类的定义和创建大致上和Java类似,关键字也是Class(下面案例都是没有写主构造函数的情况下的定义,或者可以理解为只有无参构造没有有参构造的情况)

 

如果这个类是一个空类,即没有实体,可以省略花括号

 

 

简单的对象创建和成员调用

对于类的实例化操作,即类的对象的创建,不需要使用new关键字,因为kotlin是面向函数式编程,使用 类型名() 的形式就可以了

如下 是一个比较标准的类的定义 对象创建 和赋值 调用方法的过程

 

构造函数

在kotlin中,一个类会存在一个主构造方法,可能存在一个或者多个次要构造函数

Kotlin的主构造函数写在类的定义头上,通过constructor关键字 + 函数参数列表声明

 

当主构造函数没有注解标识或者可见性修饰符的时候,constructor关键字可以省略 自然 上面的展示的类的定义可以理解为构造函数参数列表没有参数,即无参构造,()也可以去掉

 

可以看到,Kotlin中更关注构造函数作为“函数”的这个特点,完全认为构造函数就是一个名为constructor的函数,当作函数看的话,会发现这个函数少了函数体的部分,目前只提到了函数名constructor和函数参数列表,接下来我们就是要说到函数体的逻辑如何定义,Kotlin中的主构造函数函数体定义在init{}代码块中

 

这里还有一个kotlin的一个特点,kotlin中类的主构造方法的参数列表中,我们上面的列表内容 (name:String,age:Int,school:String) 和正常的函数中的参数列表十一样的,这样子的情况下代表参数列表中只是一个临时变量,主要是用于init块或者属性初始化的过程中临时使用,不会存在创建一个真实的成员

但是如果参数列表是 (var name:String,var age:Int,var school:String) 即类的主构造方法如下,参数列表中的参数被 var/val定义修饰,那么就会不一样,这个时候的参数列表中的变量name 和 age 和 school都是会被真实创建且作为成员变量加入到该类中,和在 {.....} 类主体中定义的成员方法具有同等地位,能在构造init块和成员方法中使用

 

次要构造函数

Kotlin中次要构造函数也是constructor的关键字,只要不是在类定义头中的constrcutor都是次要构造函数

如果一个类有主构造函数,每一个次级构造函数都需要委托给主构造函数,可以直接委托给主构造函数也可以通过 别的已经委托给了主构造函数的次级构造函数从而实现间接委托

次要构造方法的最大作用就是,实现类的多样化的构造,不在仅仅拘泥于主要构造方法,或者你可以认为是在一个方法中调用了主构造方法进行对象的创建

 

换做Java的理解就是如下,通过不同的构造方法或者成员函数模拟,次要构造函数3在Java中只能通过成员方法进行模拟

 

当没有主构造函数的时候,次要构造函数可以不需要委托给主构造函数,可以直接定义即可,创建对象也是可以直接利用次要构造方法进行创建

 

 

get()和set()方法

kotlin中所有的变量默认自带get和set方法,只是没有展示出来,我们可以用它本来的,也可以自己重载

在上面 zs.name 实际上是隐式的调用了getName方法 而且对应成员属性的定义 var因为是可变 所以会有get和set方法 如果是val则不会有set方法,同时,我们可以在每个字段的下面对其进行get和set方法的重载

 

权限修饰关键字

Kotlin中类的成员存在四种级别的访问权限

成员和方法不显示标注访权限关键字的时候默认为public权限

 

继承与重写

Kotlin只能的所有的类默认是【final】标识的也就是不可被继承,所以要使得一个类可以被继承需要关键字open进行标识,所有类的基类Any也是被Open关键字修饰了的

 

如果一个类需要继承另外一个类,需要在类头后面,花括号前面加上 :父类() 作为标识

 

在Java中,我们知道子类在创建之前都会默认的添加一个super从而实现父类的初始化和构造,在kotlin中,不会默认加super,所以要求我们在子类的构造函数中初始化父类,这个根据子类的主构造函数和次构造函数有不同的情况

对于父类方法的复写/重写,和继承一样的,所有的父类中的方法默认自带final关键字,也就导致所有的希望能被重写的方法需要被加上open关键字,并且子类对父类的重写方法需要加上override关键字

除此之外,override关键字也表示这个方法可以被子类的子类继承,即子类不需要加open关键字,子类对于重写的方法即加了overried关键字的方法可以加上final防止再次复写

open 修饰符添加到 final 类(即没有 open 的类) 的成员上不起作用。

 

对于成员属性,可以直接继承,也可以通过override关键字继承成员属性,虽然不写overried重写也可以访问到,但是overried显示重写之后,子类中对于属性可以有自定义的get和set方法,不显示继承的情况下会使用父类的get和set逻辑

 

初次之外,继承重写成员属性,可以改变var和val,可以用var覆写val 但是不能用val覆写var,因为var本质上是存在get方法和set方法,val就是只有get方法,那么var覆盖val就是新增一个set方法而已 ;但是反过来是不允许,只有get方法不能覆盖get+set方法

 

抽象类

kotlin中抽象类的关键字也是abstract,抽象类中的抽象成员可以不具备实现,也不需要被open关键字修饰,默认实可以被继承实现的,并且抽象类是无法创建实例对象的

 

比较特别的是,子类中可以使用抽象方法覆盖父类中的正常方法

 

嵌套类

类可以嵌套到类中,结构上是类似于Java的内部类,但是作用上是相当于静态内部类,在kotlin中称之为嵌套类

如下 是Kotlin中嵌套类

 

内部类

这里就是和Java中内部类是一样的,通过关键字 inner标识 内部类可以调用外部类的普通成员属性,因为内部类也是成员的一种且非静态,所以内部类中方法的调用要通过外部类对象创建内部类对象然后再调用 通过 this@外部类名.变量名 的方式可以调用到外部类的普通成员

 

 

接口和实现类

在kotlin中,接口的关键字也是interface

实现类实现接口也是通过 :接口名 的方式进行标识,也就是和继承是一样的,可以理解为类的 :后面就是该类的父类和接口,同Java,kotlin中一个类只能有一个父类但是可以实现多个接口

kotlin中的接口更像是JDK11之后的Java接口,接口中的方法可以有实现也可以没实现,例如方法aTest因为没有实现/方法体,所以实现类D和B必须要实现其接口,实现的关键字也是override

但是对于bTest而言,因为接口中有默认的实现(如下案例中是空的实现而已 但是也是实现了),所以实现类B和D可以不一定要实现该方法

 

接口中也可以定义属性,属性要么是抽象的即没有初始化值,接口中没有初始化就需要在实现类中进行初始化(可以重写变量get方法实现 也可以在主构造函数中初始化);要么就是想要有初始化值但是不能直接=初始化赋值,而是要编写get()方法进行初始化

 

而且,实现类对于接口中属性的实现,也可以作到var覆盖val,但是需要注意 var覆盖val的话需要在实现属性上进行初始化,不能仅仅只有get和set方法

 

 

对象表达式

对象表达式形式上是类似于Java的匿名内部类,但是Java中的匿名内部类只能继承一个类或者实现一个接口,而kotlin中对象表达式可以同时继承一个类且实现多个接口,而且在Java的匿名内部类中只能实现和重写方法,对象表达式中可以自定义成员变量,自定义成员方法,实现方法和重写方法,可以理解为对象表达式是匿名内部类的增强版

其主要定义方式为

 

image-20250501153208337

 

除此之外,因为我们知道kotlin和Java之间是兼容的,那么kotlin代码中可以对Java的接口或者类也生成对应的对象表达式,而且kotlin对于Java接口的接口表达式和对于koltin自己的接口的接口表达式语法糖稍微不太一样,对于Java的接口的接口表达式允许使用lambda表达式

 

image-20250501165028574

 

 

 

 

数据类

数据类整体的定义和正常的类是一样的,其有三个特点

数据类的主要用途一般用于定义domain。即数据模型,在JWeb开发中,前后端之间的入参和出参就可以用数据类,因为数据类一般不会存在自己的其余方法,只需要一些equals , toString , copy等方法,类似于Java中一个类+@Data的注解 ,Kotlin中就不需要引入第三方包而是语法糖帮你完成了

伴生类

说了这么久,会发现我们没有提到过静态变量,静态方法(除了嵌套类)等相关的内容,这是因为Kotlin中没有静态类的概念,取而代之的是伴生类 or 伴生对象

伴生类的关键字为 companion object{...}在花括号内就可以定义变量和方法

 

但是需要注意的是,其本质上生成的不是静态的name属性和test方法,而是生成了一个静态的成员类对象companion

 

这样相当于成员用了单例模式,如果想要达到Java中的那种效果,可以通过一个 @JvmStatic 注解实现

 

枚举类

枚举类使用enum关键字标识,其标准定义结构为

 

每个枚举常量都是一个对象,枚举常量以逗号分隔,最后一个枚举类后面用分号;和其他的成员方法成员变量进行区分,也就是每个枚举都是枚举类的实例,所以可以如下进行初始化

 

通过枚举类.entries可以获取所有枚举项的集合,从而实现遍历等需求

同时,kotlin中enum枚举类可以实现接口,枚举类中每个情况枚举项都需要单独的对接口中的所有方法进行实现,也可以在枚举类中对一个接口进行统一的实现

 

每个枚举类中的枚举项,都自带name属性和ordinal属性,前者为我们的枚举项命名,后者从0开始按照枚举类中的枚举项的定义顺序进行递增

 

密封类和密封接口

密封类某种意义哈桑可以理解为是一个增强版的枚举类,主要用来标识受限的类继承结构,密封类的关键字是sealed,密封类是抽象的,可以有子类但是子类可以定义在密封类内部也可以在外部正常维护继承关系

 

密封类在安卓架构中用的挺多的,因为密封类可以用于标识猜测用户的意图,从而调用不同的处理方式

image-20250503102532868

密封接口也是类似的,sealed interface的关键字标识,好处就是在于,密封类因为单继承的原因,只能继承一个,但是接口实现的话可以实现多个接口,更加的方便

如下,下面的每个实现类可以分别实现两个密封接口,所以对应的可以在两个方法中针对不同的接口进行when操作,但是如果是密封类的话,只能操作一个,你还需要额外的继承关系

密封接口可以认为是接口+枚举 , 密封类可以认为是抽象类+枚举

image-20250503102920416

 

单例

单例模式我们知道分为了:饿汉式单例 ; 懒汉式单例 ; 线程安全单例 ; 双重锁检测单例 ;静态内部类单例,这里我们顺便都给讲了kotlin的实现方式

饿汉单例

饿汉单例就是在类中就存在这么个单例,程序一运行就存在了,不管你以后是否会使用

在Java中就是在类中存在一个私有的静态的自己的类对象,对外关闭有参构造和无参构造,而是开放一个getInstance的方法返回内部的这个类对象

而在Kotlin中,因为不存在静态方法和静态成员的概念,所以提供了一个更加简单的方法,直接通过object关键字实现

 

懒汉单例

懒汉模式就是在被访问的时候才会被创建,如果为null就会被创建,如果不为null则使用原本创建的对象,如果一直不访问则一直都是null

 

线程安全的懒汉单例模式

线程安全的懒汉单例主要是在懒汉模式的基础上,让getInstance()方法即对外开放的获取实例的方法变得线程安全,最简单的也就是直接对方加锁

 

内容和上面懒汉单例基本一致,只是Java中实现锁的方式是添加sychronized关键字而Kotlin中式记住的@Synchronized关键字

双重锁检测

 

 

自定义注解

基础用法

Kotlin中自定义注解的关键字为annotation,注解类可以是公开的,内部的,私有的但不能是保护的

如下案例定义了一个Marker注解没有定义任何的成员变量和成员方法,可以通过主构造方法初始化成员属性

 

如下是一个定义了成员属性以及用主构造方法初始化成员属性的自定义注解

 

如下,Kotlin中的注解默认是没有使用范围限定的,即任何地方都可以使用,可以看到如下1,2,3,4处,分别是对main方法,自定义方法,自定义类,普通变量,普通方法都可以进行修饰注解,初次之外TestClass中也可以看到,注解除了和Java一样

 

元注解

我们知道,在Java中,对于自定义注解需要配置一定的元注解,在kotlin中也有类似的要求,在Kotlin中,元注解有四个,但是从上面知道,不是必须的,但是为了实现注解的准确使用和范围限定,我们一般需要元注解来指定一些内容

元注解有如下四个

如下使用,代码第4行声明注解类型 MyAnnotation,其中使用了三个元注解修饰 MyAnnotaion 注解。代码第1行使用 @MustBeDocumented 指定 MyAnnotaion 注解信息可以被文档生成工具读取。代码第2行使用 @Target(AnnotationTarget.CLASS) 指定 MyAnnotaion 注解用于修饰类 和 接口等类型。代码第3行 @Retention(AnnotationRetention.RUNTIME) 指定 MyAnnotaion 注解信息可以在运行时被读取。代码第4行的 descriptionMyAnnotaion 注解的属性。

 

除此之外,一个annotation可以在Target中允许修饰多个类型,注解成员参数也可以使用泛型做处理,如下

 

可以看到,上面的@Target中标识这个注解可以同时作用于方法,属性,属性的get访问器和set访问器,那么该注解修饰一个成员属性的时候,我们同时可以修饰其自己,get/set方法的时候,可以分别在属性,get方法,set方法上分别使用注解,也可以通过::指定修饰的东西(举例 @get::注解 代表修饰的是get访问器),如下

 

在运行时获取注解及其成员属性信息

这里稍微涉及了一点反射相关的知识点,但是问题不大

 

 

其余关键字

?:

这个是一个运算逻辑符号,A?:B则代表如果A为null则返回B

const

kotlin中const关键字用于修饰常量且不可变(这个特点类似于val),但是和val的区别在于const修饰的变量必须式在编译的时候就需要能被确认的,从这一点上来看类似于Java中的static,所以const修饰的常量应该被称之为 编译时常量

也是因为这个特性,编译时常量需要如下特点

 

apply

可以将一个需要进行配置的变量的一些操作,统一的写在apply{.....}的大括号内

apply返回的是调用者对象,在apply的大括号内部可以使用this来代替调用对象以及通过this调用对应的属性(其实如下第二段代码中setReadable()完整版就是this.setReadable() 只是this被隐藏了)

 

image-20250504103454824

let

和apply有点像,也是let{..lamdba..}传入调用者,只不过apply返回调用者,let函数返回的是lambd最后一行,在let函数内部使用it代表调用对象 一般用于做对象的判空处理,属于拓展函数

image-20250504103811997

结合?:运算符可以实现灵活的属性赋值,如下操作,如果不适用let和?: 可能会需要if-else,代码量可想而知的变多

 

with

格式 with(对象){.....} 在大括号内使用this代表调用者,返回lambda函数的是最后一行的内容,属于内联函数,一般用于连续调用有个类的多个方法

 

 

run

run行数也和上面两个let和apply差不多,也是传入调用者,但是返回的是lamdba的执行结果也就是最后一行,run可以认为是with和let的结合

*它可以像*with()*函数一样直接在函数块中使用*this指代该对象,也可以像let()*函数一样为对象*做统一的判空处理

 

also

also函数和apply相似,接受调用者,返回调用者,不同的是also在内部对调用者是使用it进行的指代,而在apply中是使用this

 

apply,run,with,also,let的区别

image-20250504110807783

takeIf

传入调用者,lambda逻辑执行成功返回true则返回调用者对象 ; 如果执行失败则返回null

 

image-20250504111346013

takeUnless

和takeIf刚好相反,如果lambda中的返回为false或者说执行失败,则会返回调用者对象,反之如果执行成果或者说返回true则返回null

Kotlin 编程