Java崽学习Kotlin---Kotlin入门基础语法
ZealSinger 发布于 阅读:72 Kotlin
变量定义
Kotlin中定义变量的方式主要有两种:var 和 val
-
var:定义可变变量
标准格式为 var 变量名:数据类型 = 初始化值
当然也可以不写数据类型,因为会自动推导(自动推导和弱类型不一样,JS这种是属于弱类型没有类型限制,而Kotlin是强类型,只不过是能自动推导而已)
var intNumber : Int = 0 var floatNumber : Float = 1.2f //1.2F也是可以的 即大小写f都可以 var doubleNumber : Double = 1.23456 var booleanNumber : Boolean = false var charNumber : Char = 'a' val name = "Kotlin"
-
val:定义不可变变量,使用格式和var一样
数据类型
基础数据类型
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中就不可以)
var intNumber : Int = 0
var floatNumber : Float = 1.2f
var doubleNumber : Double = 1.23456
var booleanNumber : Boolean = false
var charNumber : Char = 'a'
var byteNumber : Byte = 1
var intToString = intNumber.toString()
var intToFloat = intNumber.toFloat()
var byteToInt:Int = byteNumber // 报错 类型不必配
var byteToInt:Int = byteNumber.toInt() // 不报错 正规类型转化方式
复杂/引用数据类型
Any
Any的作用在Kotlin中就是相同与Java中的Object,是所有对象的基类/父类
带有hashCode 和 equal方法
Kotlin中的String的操作相对而言比Java的灵活,可以看到,除了正常的用 .length 直接获取长度之外,还有如下var s:String = ...
s.length // 获取长度
s.first() // 获取第一个字符
s.last() // 获取最后一个字符
s.get(x) / s[x] // 获取索引为x位置上的数据 kotlin的索引也是从0开始算起的
对于字符串的拼接,有 加号+ 和 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)
}
为了搭配三引号使用,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.
nothingnothin这玩意儿稍微有点别扭,可以理解为null,但是又和Java中的null不太一样官方给出的解释如下
可以看到如下,我们直接将b=null的方式进行赋值,可以看到在自动推导的作用下,null其实就是Nothing的数据类型
我们以前的理解就是,null就是没有,常见的就是空指针异常,当我们对一个明确数据类型的数据进行赋值的时候,例如下var b: Int = null这个是会报错的,在kotlin中会存在空指针检测/空安全,不会允许你初始化变量为空的,但是如果你想这么做,可以通过在数据类型后面加问号?进行处理,告知编译器这个变量可能为空var a: Int? = null
当数据类型后面加了?,也就代表可能为空,编译器就会在这个变量使用的时候,通过 变量? 的方式,进行一个判空判断,如下 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()
然后是对于空容量数组(即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)
多维数组采用{}的形式创建或者嵌套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)) // 也是可以的
对于数组的修改和访问,也是通过数据名[索引]的方式进行访问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
可以发现,String的源码底层就是继承的CharSequeue即字符序列,简单理解就是底层也是一个字符数组
集合
kotlin中所有的集合默认是不可变集合,主要分为三种类型List,Set,Map,三者都是接口而非实现类(和Java一样),都继承自Collection<out E>接口,而该接口又继承了Iterable<out T>接口,该接口只会提供size属性 ,isEmpty(),get(),contains()等方法,不可变集合不能进行内容的增删改
因为默认是不可变集合,所以对应的存在可变集合MutableList ; MutableSet ; MutableMap,这些可变集合都提供了对应的增删改的方法
可变集合
上面有说到,可变集合就是加上了mutable的前缀,即mutableList,mutableSet,mutableMap,整体的创建方式和不可变集合差不多
var mutableIntList : MutableList<Int> = mutableListOf(1,2,3,4)
var mutableSet : MutableSet<Long> = mutableSetOf(1L,2L,3L,4L)
var mutableMap : MutableMap<Long,Int> = mutableMapOf(1L to 2,2L to 3)
这里需要注意一下可变集合和val定义不可变变量关键字之间的关系,val不可变值是内存地址不可变,类似于Java中final修饰数组一样,数组头部的地址不可变但是内容可变,val也就是类似final的效果,可变集合所说的也就是内容可变,val代表地址不可变即不能重新复制,所以两者不冲突
val mutableIntList : MutableList<Int> = mutableListOf(1,2,3,4)
mutableIntList.add(10)
//报错 因为val不可变 不可重新赋值
mutableIntList = mutableListOf(10,11,12,13,14,15,16,17,18,19)
var mutableIntList1 : MutableList<Int> = mutableListOf(1,2,3,4)
mutableIntList.add(10)
//不报错 因为var可变
mutableIntList1 = mutableListOf(10,11,12,13,14,15,16,17,18,19)
除此之外,我们在Java中对应的各种集合的不同实现类例如ArrayList ; LinkedList ; HashMap ;
TreeMap 都可以在Kotlin中进行创建,也都属于可变集合
val arrList : ArrayList<Int> = ArrayList()
val set : HashSet<Int> = HashSet()
val queue : Queue<Int> = LinkedList()
val treeMap : TreeMap<Int,Int> = TreeMap()
我们知道,可变集合List和Map都有对应的get方法,list中一般为get(index)获取对应位置上的数据,map一般为get(key)获取对应key上的value,对于这两个get方法,kotlin都提供了类似数组的下标索引获取方式,即中括号的方式
val arrList : ArrayList<Int> = ArrayList()
val map : MutableMap<String, Int> = mutableMapOf("hzw" to 21,"zealSinger" to 22,"kotlin" to 23)
arrList.add(2)
arrList.add(3)
println(arrList[0]) // 等同于 arrList.get(0)
println(arrList[1]) // 等同于 arrList.get(1)
println(map["hzw"]) // 等同于 map.get("hzw")
二元组和三元组(额外)
二元组就是我们上面说到的Pair对象,Kotlin中也存在三元组,也就是传入三个数据作为一个对象,Kotlin中用Triple(first,seconde,third)创建三元组
不可变集合
不可变集合主要通过listOf , setOf , mapOf进行构建创造
var intList : List<Int> = listOf(10,11,12,13,14,15,16,17,18,19,20)
var intSet : Set<Int> = setOf(1,1,2,2,3,3) // size=3 {1,2,3}
var intIntMap : Map<Int,Int> = mapOf(Pair(1,2),Pair(2,3),Pair(2,4))// size=2 {{1,2}.{2,4}}
// pair为kotlin中的一个数据类,用于鵆两个数值,简单的将两个值包装在一起
// to 为kotlin中的中缀函数 返回一个pair对象 1 to 2等效于 Pair(1,2)
var intIntMap : Map<Int,Int> = mapOf(1 to 2, 2 to 3, 2 to 4)
// 创建对应的空集合
var emptyIntList : List<Int> = emptyList()
var emptySet : Set<Int> = emptySet()
var emptyMap : Map<Int,Int> = emptyMap()
以list为例,可以看到,其对应的方法很少,都只有一些get(index),last(),subList等获取元素和截取list的相关方法,没有修改和添加和删除的方法,这也是不可变集合的特点
从上述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
fun main() {
var list:MutableList<Int> = mutableListOf(1,2,3,4,5)
for (i in list) {
println(i)
}
for((index , number) in list.withIndex()){
println("index = $index, number = $number")
}
}
通过下标索引遍历
当然,直接通过下标的方式进行遍历也是可以的,如下,利用区间表达式和for循环和list.get() / l ist[] 的方式,就能很方便的进行下标迭代遍历
fun main() {
var list:MutableList<Int> = mutableListOf(1,2,3,4,5)
for( i in 0..list.size-1 step 1){
println("i = $i, data = " + list[i])
}
}
通过forEache方式遍历
也是类似的,forEache和Java中forEache大差不大,在Java中我们对于forEach遍历集合的时候,每个元素需要取一个变量名,在kotlin中如果不取变量名默认命名为it(即第一个forEach那样),使用 forEachIndexed也可以做到和上面list.withIndex()一样的效果,需要定义两个变量分别接收索引和数值
fun main() {
var charArr:Array<Char> = arrayOf('a','b','c','d','e')
charArr.forEach { println(it) } // 默认变量名为it
charArr.forEachIndexed{ index,it-> println("index:$index,it:$it") } // forEacheIndexed是kotlin自带的一个拓展函数
}
条件控制语句
所谓的条件控制语句,主要的就是我们Java中对应的if ; if-else ; switch 等语句
If语句
在kotlin中,if语句整体使用和Java中类似,但是特别的地方在于,kotlin中的if语句可以有返回值,在if每个判断情况最后面写一个数值就是对应的返回值,if左边用变量接受了就代表需要返回值,反之则不需要,如果需要返回值就需要保证每一个if分支情况都有返回值,有点类似于三元表达式但是更加的强大因为处理更多的情况的返回
自然 因为if 和 where存在返回值,所以没有单独设计三元表达式了,所以我们的三元表达式都需要用if和where实现
val sc = Scanner(System.`in`)
val number = sc.nextInt() // 输入59
if(number >= 60){
println("及格1")
}else{
println("不及格1")
}
val res:Int = if(number>60){
println("及格2")
1
}else{
println("不及格2")
0
}
println(res) // 输出0
when表达式
kotlin中没有switch,但是用了when进行替代,对应的default也被替换为了else
对于when中的每一个情况采用 匹配模式/常量匹配 -> {...}的格式进行编写,每一个{}中不需要写break,kotlin会自动终止
如下
fun main() {
val sc = Scanner(System.`in`)
val number = sc.nextInt()
when(number){
is Int -> { // number的类型为Int则匹配成功
}
100,101,102->{ // 相当于 number=100 || number=101 || number=102
println("满分")
}
in 90..99 -> { // in number1..number 等价于 number1<= number <=number2
println("优秀")
}
else -> {
println("以上都不是")
}
}
}
上述是when(number) 是代表这个when是基于number这个变量进行的,我们也可以直接when{}这样子不会基于某个变量,条件匹配更广,上面这种基于某个确切变量的不能使用布尔表达式和复杂的逻辑表达式&& || 这种进行
从下面案例中也可以看出来,when表达式也可以有返回值,我们这里写的Unit,Unit就是Java中的Void,代表无返回类型
fun main() {
val sc = Scanner(System.`in`)
val number = sc.nextInt()
var res:Unit = when{
number>=100 && number <=100 -> { // 可以用布尔表达式左伟条件匹配
}
number is Int -> {
}
number==0 ->{
}
number.toString() !is Nothing -> { // !is 标识不是
}
else -> {
}
}
}
对于上述when和if
while循环
while循环和Java中的类似,不多说了
fun main() {
var number = 0
while(number<5){
println(number++) //输出 01234
}
}
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
a@ for(i in 1..10){
for(j in 1..10){
count++
if(i==2*j){
println("i = $i , j = $j ")
break@a
}
}
}
标签还有一个作用就是对于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
(1..10).forEach a@{
if(it == 5) return@a
println("it == $it")
}
(1..10).forEach {
if(it == 5) return@forEach
println("it == $it")
}
为了达到跳出forEach循环的效果,还需要借助run{}这个高阶函数,本质上也是一个Lambda表达式,属于kotlin自带的,一般也会自带标签,也可自定义标签,我们利用run的标签实现整个forEach的中断操作
如下代码 最后上下两个循环都只打印了 1 2 3 4 也就是实现了foeEach的中断
fun main() {
run{
(1..10).forEach {
if(it == 5) return@run
println("it == $it")
}
}
println("-----------------------")
run b@{
(1..10).forEach {
if(it == 5) return@b
println("it == $it")
}
}
}
区间表达式
在上面when表达式的时候,我们接触到了一个 1..10 代表 1<= x <=10 即表示一个左闭右闭的区间,这两个点点 .. 其实就是Kotlin的区间表达式的一种
除了左闭右闭,我们还存在别的区间表达式,常用的区间表达式如下
1.。10 左闭右闭 等同于 1<=x<=10
1 until 10 左闭右开 等同于 1<=x<10
在高版本中不建议使用until而是使用 ..< 即:
1 ..< 10
优化了之后 可以允许进行浮点数的范围判断
val number: Float = sc.nextFloat()
println(number in 1.0f ..< 20.56f)
但是同时也失去了step跳步和输出打印的能力 因为浮点数很多
10 downTo 1 相当于..的逆序 从大到小 ..只能从小到大 ..如果从大到小会报错然后推荐修改为downTo
所有的区间表达式都默认以1为单位的step进行的跳跃,可以通过step指定跳跃的步数进行跳跃
fun main() {
val sc = Scanner(System.`in`)
// val number = sc.nextInt()
(1 .. 20 step 3).forEach({
println(it)
}) // 输出1 4 7.....19
println("---------------------------")
(20 until 30 step 2).forEach(::println) // 输出 20....28
println("---------------------------")
(100 downTo 80 step 5).forEach(::println) // 输出100 95 90...80
}
除了整型数据的区间变化之外,也支持字符的区间变化,实质上也就是ASCII码的变化
// 输出 a e i....u y
('a'..'z' step 4).forEach(::println)
函数
正常函数
函数的关键字是 fun 可以参考我们的默认的main函数,完整的标准函数结构如下
fun 函数名(参数列表): 函数返回值数据类型{
}
对于没有返回值的函数,我们可以不写函数返回值类型,因为Kotlin会自动推导,但是同时我们可以和Java一样用void进行表示,只是要记得Kotlin中的void是Unit
fun funcOne(){
}
fun funcTwo():Unit{
}
对于整个函数体只有一行的函数,可以不需要大括号和函数值返回类型,类似于定义变量和赋值的格式,如下sumOne 和 sumTwo两个函数 最终效果是一样的
fun main() {
println(sumOne(1,2))
println(sumOne(1,3))
}
fun sumOne(a:Int, b:Int) : Int{
println("a = $a, b = $b")
return a+b
}
fun sumTwo(a:Int,b: Int) = a+b
infix
这个玩意儿有点意思可以看一下格式来体会,infix的主要功能个人认为是为了简化开发和阅读,infix标识的方法会代表它只能被一个指定类型的对象进行调用,然后参数列表只能接受一个参数(而且是必须接收,不能不接收,没有入参会报错),然后在函数内可以通过this来代指调用这个方法的那个对象
我们将上面的sum求和函数利用infix进行改造一下,我们改造为int类中数据的加法和float类型的加法,如下两个sum函数
fun main() {
println(1.sum(10))
println(1.2f.sum(2.23f))
// 甚至可以简化为
println(1 sum 10)
println(1.2f sum 2.23f)
}
// 可以看到 infix的标准使用格式为
// 完整版: infix fun 调用对象类型.方法名(参数列表):函数返回值类型{函数逻辑}
// 简化版: infix fun 调用对象类型.方法名(参数列表) = 函数逻辑
// 整体可以理解为:限定允许一个Int类型的数据调用该sum方法,传入一个int类型的参数,方法返回值为int类型,方法逻辑为 this即调用方法的这个欸Int类型的对象+入参
infix fun Int.sum(a:Int) : Int{
return this+a
}
infix fun Float.sum(a:Float) = this+a
从上面的过程可以看出来,有点类似给调用对象临时加了一个成员方法,在main函数中我们可以通过 . 的方式调用方法,类似Int对象1调用成员方法sum一样的感觉
而且简化后的 1 sum 10 和我们上面提到的map的初始化得时候传入key-value键值对使用A to B的形式很类似( var map:Map<Int,Int> = mapOf(1 to 2) ),其实我们查看一些to的源码就会发现,实际上就是一个infix函数
// infix函数 指定A对象调用 传入B对象用变量名that代表 函数体逻辑就是创建一个二元组Pair对象
public infix fun <A, B> A.to(that: B): Pair<A, B> = Pair(this, that)
入参相关设计
-
入参可以有默认值,函数定义上就可以给参数加默认值,加了默认值之后可以不传入,不传入则使用默认值,传入则覆盖使用
fun main() {
println(plusOne(1,2)) // 3
println(plusOne(1)) // 使用默认值1
println(plusOne(1,null)) // null
}
fun plusOne(a:Int , b:Int? = 0): Int? { // 这里是因为b可能为null 所以这里用了Int? 但是如果确定b不会为null,可以用下面的格式
return b?.plus(a) // 因为b可能为null 就不能直接用 a+b 需要用plus方法 参数返回类型也变为了 Int?
}
fun plusOne(a:Int , b:Int = 0): Int { // 这里就是纯数据数字计算的时候 可以用来做普通的函数 不需要进行null空判断
return a+b
}
-
调用方法处传入参数的时候,可以通过指定参数名称进行传入,从而不需要按照参数列表定义顺序进行传参
fun main() {
println(plusTwo(c=10,a=1,b=0)) //这里就不需要按照参数列表顺序进行编写 灵活很多
println(plusTwo(1,c=2))
println(plusTwo(a=1,c=2))
println(plusTwo(a=1,1,2))
}
fun plusTwo(a:Int , b:Int=0 , c:Int):Unit{
}
-
入参可以是函数,即Lambda表达式
这一点可以实现将一个方法/函数/逻辑作为参数传入到函数中,然后实现在函数内调用,或者利用入参调用
fun main() {
println(plusOne(0,10,{
var temp = it*it
temp /= 2
temp
})) // {...}中的逻辑 对应下面plusOne中的lambda表达式的逻辑 因为只有一个入参 默认为it {}中的逻辑表示对入参it开平发再除以二得到结果返回 最后打印 plusOne(0,10,c) = c(0+10) = (0+10)*(0+10)/2 = 50
// 同理 这里只是lambda表达式接受了两个参数a和b 函数逻辑为打印ab之和
println(plusTwo(1,20,{ a,b->println(a+b)}))
println(plusThree(2,30,{ println(it)}))
}
// 入参C就是一个lambda表达式,标准入参格式 参数名称:(lambda表达式入参类型列表)->结果返回类型
// 在这里我们就是 c代表入参lambda 他会接收一个Int类型的参数 返回一个Int类型的参数
// 在该方法内部我们调用了C这个lambda表达式 或者说调用了入参函数C,传入参数(a+b) 然后就会按照C函数的内容逻辑进行处理返回
// C函数的逻辑为 入参平方再除以二 所以这里就是对入参a和b的和进行平方再除以二
fun plusOne(a:Int, b:Int , c:(Int)->Int):Int{
return c(a+b)
}
// 这里代表C会接收两个参数但是不会有返回值 将ab作为参数传入逻辑体C中进行处理
fun plusTwo(a:Int , b:Int , c:(Int,Int)->Unit):Unit{
c(a,b)
}
// c接收一个参数但是不会有返回值
// 方法体内将 a+b的结果 作为参数传入到逻辑体C中进行处理
fun plusThree(a:Int, b:Int , c:(Int)->Unit){
c(a+b)
}
并且当参数列表中最后一个参数为如上的lambda的时候,可以将最后一个参数写到()外面 ; 当参数列表中没有别的入参只有一个lamdba表达式入参的时候,甚至可以将()也给省略,这种情况下就有点像我们上面 run{...}那个高阶函数了
// 对上面几个函数进行小改造就能看出来效果
fun main() {
println(plusOne(0, 10) { // 最后一个参数为lambda表达式 {}可以写到方法体后面
var temp = it * it
temp /= 2
temp
})
plusTwo{a, b -> println(a + b) } // 有且只有一个lambda参数 可以()都不需要
}
fun plusOne(a: Int, b: Int, c: (Int) -> Int): Int {
return c(a + b)
}
fun plusTwo(c: (Int, Int) -> Unit) {
}
可以看一下run方法的源码 可以看到其实就是只有一个lambda入参 所以我们上面讲break的时候就能直接加run,他的这个lambda没有入参 返回类型还是泛型R
-
利用 vararg 可以实现多参/变长入参
可变入参也是很需要的,就是有时候需要传入一两个,有时候需要传入三四个,就可以利用到vararg关键字
// 标准格式
fun 函数名(vararg 参数名:参数类型):函数返回值{
}
Eg:
// 如下 代表 sum函数可以接收多个String类型的参数 三个或者五个或者其他个数的可变个数的String类型的参数都可以
fun main() {
sum("1","2","3")
sum("1","2","3","4","5")
}
fun sum(vararg list:String):Unit{
}
fun sum(vararg list:String):Unit{
}
除此之外,我们还可以将一个对应类型的数组也可以作为参数去对应可变长度入参,例如上面,我们就可以传入一个 String类型的Array,但是不能直接传入,需要借助 * 标识展开传入
fun main() {
val stringList:Array<String> = arrayOf("zealSinger","Java")
sum(stringList) // 报错 不能直接传入Array
sum(*stringList) // 借助*可以传入 底层会将Array展开然后传入方法 输出 zealSinger Java
}
fun sum(vararg list:String):Unit{
list.forEach{
println(it)
}
}
这种直接传入Arry的方式 主要在 “当函数除了有vararg表明的可变多参数的情况下,还有别的单个参数的时候” 这种场景下比较有用,可以看如下案例,sum方法中同时具备可变参数list和存在默认数值单个参数test,假设我们现在像test用默认数值,list中"zealSinger"和 "Java"
fun main() {
sum("zealSinger","java") // 不会报错但是不符合题意 编译器会默认第一个 zealSinger属于覆盖test,而后面的Java才是list
sum(list="zealSinger","java") // 报错 使用参数指定的时候 不能存在多个值和逗号分隔 这种写法不对
// 这个时候就可以使用上面 Array作为整体参数传入了
val stringList:Array<String> = arrayOf("zealSinger","Java")
// 下面两种都可以 当array不再是单独入参的时候 即方法不是只存在一个可变参数的时候 这个*可以去除
sum(list = *stringList)
sum(list = stringList) // 推荐这种
}
fun sum(test:String="kotlin" , vararg list:String):Unit{
list.forEach{
println(it)
}
}
类和对象
类的定义
Kotlin中类的定义和创建大致上和Java类似,关键字也是Class(下面案例都是没有写主构造函数的情况下的定义,或者可以理解为只有无参构造没有有参构造的情况)
// 标准格式
class 类名{
成员属性....
成员方法....
}
如果这个类是一个空类,即没有实体,可以省略花括号
class 类名
简单的对象创建和成员调用
对于类的实例化操作,即类的对象的创建,不需要使用new关键字,因为kotlin是面向函数式编程,使用 类型名() 的形式就可以了
如下 是一个比较标准的类的定义 对象创建 和赋值 调用方法的过程
fun main() {
val zs:TestClass = TestClass()
zs.name = "张三"
zs.age = 21
zs.school = "HNUST"
zs.eat()
}
class TestClass{
// 成员属性
var name:String? = null
var age:Int? = null
var school:String? = null
// 成员方法
fun eat(){
println("${this.name} is eating")
}
}
构造函数
在kotlin中,一个类会存在一个主构造方法,可能存在一个或者多个次要构造函数
Kotlin的主构造函数写在类的定义头上,通过constructor关键字 + 函数参数列表声明
class 类名 constructor(构造函数参数列表){....}
当主构造函数没有注解标识或者可见性修饰符的时候,constructor关键字可以省略 自然 上面的展示的类的定义可以理解为构造函数参数列表没有参数,即无参构造,()也可以去掉
class 类名 (构造函数参数列表){....}
可以看到,Kotlin中更关注构造函数作为“函数”的这个特点,完全认为构造函数就是一个名为constructor的函数,当作函数看的话,会发现这个函数少了函数体的部分,目前只提到了函数名constructor和函数参数列表,接下来我们就是要说到函数体的逻辑如何定义,Kotlin中的主构造函数函数体定义在init{}代码块中
fun main() {
val zs = TestClass("张三",21,"HNUST") // 有参构造
println(zs.age)
zs.eat()
}
class TestClass(name:String,age:Int,school:String) {
// 成员属性
var name:String? = null
var age:Int? = null
var school:String? = null
init{ // 主构造函数的函数体逻辑 将入参进行赋值
this.name = name
this.age = age
this.school = school
}
// 成员方法
fun eat(){
println("${this.name} is eating , age is ${this.age}")
}
}
// 当然 构造参数参数列表中的参数不仅仅可以在init中使用,可以直接在成员属性定义的时候进行赋值操作 这样就不需要init块或者说用init块干别的事情
class TestClass(name:String,age:Int,school:String) {
// 成员属性
var name:String? = name
var age:Int? = age
var school:String? = school
// 成员方法
fun eat(){
println("${this.name} is eating , age is ${this.age}")
}
}
这里还有一个kotlin的一个特点,kotlin中类的主构造方法的参数列表中,我们上面的列表内容 (name:String,age:Int,school:String) 和正常的函数中的参数列表十一样的,这样子的情况下代表参数列表中只是一个临时变量,主要是用于init块或者属性初始化的过程中临时使用,不会存在创建一个真实的成员
但是如果参数列表是 (var name:String,var age:Int,var school:String) 即类的主构造方法如下,参数列表中的参数被 var/val定义修饰,那么就会不一样,这个时候的参数列表中的变量name 和 age 和 school都是会被真实创建且作为成员变量加入到该类中,和在 {.....} 类主体中定义的成员方法具有同等地位,能在构造init块和成员方法中使用
// 错误的方式
class TestClass(var name:String,var age:Int,var school:String) {
// 因为上面构造函数中都会加入name age school这三个成员属性 所以这里重复定义了相同名字的成员 下面三行var都会报错
var name:String? = null
var age:Int? = null
var school:String? = null
init{
this.name = name
this.age = age
this.school = school
}
// 成员方法
fun eat(){
println("${this.name} is eating , age is ${this.age}")
}
}
// 正确的定义方式
class TestClass(var name:String,var age:Int,var school:String) {
// 成员方法
fun eat(){
println("${this.name} is eating , age is ${this.age}")
}
}
次要构造函数
Kotlin中次要构造函数也是constructor的关键字,只要不是在类定义头中的constrcutor都是次要构造函数
如果一个类有主构造函数,每一个次级构造函数都需要委托给主构造函数,可以直接委托给主构造函数也可以通过 别的已经委托给了主构造函数的次级构造函数从而实现间接委托
次要构造方法的最大作用就是,实现类的多样化的构造,不在仅仅拘泥于主要构造方法,或者你可以认为是在一个方法中调用了主构造方法进行对象的创建
fun main() {
val tClass0 = TestClass("张三",100,"HNUST") // 调用的主要构造方法
val tClass1 = TestClass(1,2) // 调用的次要构造方法1
val t = TestClass2("zealSinger",200)
val tClass2 = TestClass(t) // 调用的词用构造方法2
val tClass3 = TestClass(2) // 调用的次要构造方法3
/*
下面输出如下
tClass0### name = 张三 , age = 100 , school = 未知
tClass1### name = 未知 , age =3 , school = 未知
tClass2### name = zealSinger , age =200 , school = 未知
tClass3### name = 未知 , age =4 , school = 未知
*/
println("tClass0### name = ${tClass0.name} , age = ${tClass0.age} , school = ${tClass1.school}")
println("tClass1### name = ${tClass1.name} , age =${tClass1.age} , school = ${tClass1.school}")
println("tClass2### name = ${tClass2.name} , age =${tClass2.age} , school = ${tClass2.school}")
println("tClass3### name = ${tClass3.name} , age =${tClass3.age} , school = ${tClass3.school}")
}
class TestClass(name:String,age:Int,school:String) { // 主构造方法
// 成员属性
var name:String? = name
var age:Int? = age
var school:String? = school
constructor(a:Int,b:Int):this("未知",a+b,"未知") // 次要构造方法1 实际上调用的是主要构造方法
constructor(t:TestClass2):this(t.name,t.age,"未知") // 次要构造方法2 实际上调用的是主要构造方法
constructor(a:Int):this(a,a) // 次要构造方法3 实际上是调用的是次要构造方法1 次要构造方法1中调用的主要构造方法
// 成员方法
fun eat(){
println("${this.name} is eating , age is ${this.age}")
}
}
class TestClass2(var name: String, var age: Int) {
}
换做Java的理解就是如下,通过不同的构造方法或者成员函数模拟,次要构造函数3在Java中只能通过成员方法进行模拟
class TestClass{
public String name;
public int age;
public String school;
public TestClass() {
}
public TestClass(String name, int age, String school) { // 主要构造方法
this.name = name;
this.age = age;
this.school = school;
}
public TestClass(int a,int b){ // 等同于次要构造方法1
this.name = "未知";
this.age = a+b;
this.school = "未知";
}
public TestClass(TestClass2 t){ // 等同于次要构造方法2
this.name = t.name;
this.age = t.age;
this.school = "未知";
}
public TestClass constructor2(int a){ // 等同于次要构造方法2
return new TestClass(2,2); // 借助次要构造方法1从而实现对象的构造
}
}
class TestClass2{
public String name;
public int age;
}
当没有主构造函数的时候,次要构造函数可以不需要委托给主构造函数,可以直接定义即可,创建对象也是可以直接利用次要构造方法进行创建
fun main() {
val t = TestClass2(1,2,3)
val t2 = TestClass2(1,2,3)
}
class TestClass2 {
// 成员属性
var name:String? = null
var age:Int? = null
var school:String? = null
constructor(name:String,age:Int,school:String)
constructor(a:Int,b:Int,c:Int){
this.name = ""
this.age = a+b+c
this.name = ""
}
}
get()和set()方法
kotlin中所有的变量默认自带get和set方法,只是没有展示出来,我们可以用它本来的,也可以自己重载
在上面 zs.name 实际上是隐式的调用了getName方法 而且对应成员属性的定义 var因为是可变 所以会有get和set方法 如果是val则不会有set方法,同时,我们可以在每个字段的下面对其进行get和set方法的重载
fun main() {
val zs:TestClass = TestClass()
zs.name = "张三"
zs.age = 21
println(zs.age) // 输出 23 这是因为age的set方法中我们对传入参数+1 所以上面的赋值操作实际上赋值的是22 又因为get方法会让字段+1 也就是对应的22+1=23 如果将get方法中的plus逻辑删除 返回的就是22
zs.school = "HNUST" // 报错 因为 school是val修饰的不可变 不会生成对应的set方法 所以无法进行赋值操作
zs.eat() // 输出 张三 is eating , age is 23
}
class TestClass{
// 成员属性
var name:String? = null
var age:Int? = null
get() = field?.plus(1) // 用filed代表要操作的这个字段
set(value) { // 用value代表入参
field = value?.plus(1)
}
/* 该get和set方法等同于Java中如下代码
public int getAge(){
return this.age+1
}
public void setAge(int value){
this.age = value+1
}
*/
val school:String? = null
// 成员方法
fun eat(){
println("${this.name} is eating , age is ${this.age}")
}
}
权限修饰关键字
Kotlin中类的成员存在四种级别的访问权限
-
private:意味着该成员只能在这个类中的内部(包括其所有成员)可见
-
protected:意味着该成员具有与private一样的可见性,但是也在子类中可见
-
internal:意味着能看到类声明的模块内的任何客户端都可见
-
public:公开的,任何位置的任何类都可见
成员和方法不显示标注访权限关键字的时候默认为public权限
open class Outer { // open关键字标识这个类可以被继承 后面会说
private val a = 1
protected open val b = 2
internal open val c = 3
val d = 4 // 默认 public
protected class Nested {
public val e: Int = 5
}
}
class Subclass : Outer() { // :Outer代表着继承 后面会说
// a 不可见
// b、c、d 可见
// Nested 和 e 可见
override val b = 5 // “b”为 protected
override val c = 7 // 'c' is internal
}
class Unrelated(o: Outer) {
// o.a、o.b 不可见
// o.c 和 o.d 可见(相同模块)
// Outer.Nested 不可见,Nested::e 也不可见
}
继承与重写
Kotlin只能的所有的类默认是【final】标识的也就是不可被继承,所以要使得一个类可以被继承需要关键字open进行标识,所有类的基类Any也是被Open关键字修饰了的
/*
The root of the Kotlin class hierarchy. Every Kotlin class has Any as a superclass.
*/
public open class Any {
public open operator fun equals(other: Any?): Boolean
public open fun hashCode(): Int
public open fun toString(): String
}
如果一个类需要继承另外一个类,需要在类头后面,花括号前面加上 :父类() 作为标识
open class FuTest{
}
class ZiTest : FuTest() {
}
在Java中,我们知道子类在创建之前都会默认的添加一个super从而实现父类的初始化和构造,在kotlin中,不会默认加super,所以要求我们在子类的构造函数中初始化父类,这个根据子类的主构造函数和次构造函数有不同的情况
-
当子类存在主构造函数的时候:在子类的主构造参数中,需要包括基类所需要的参数,在子类声明继承关系 即 :父类 后面加上(参数)调用父类的构造函数进行构造
open class Animal(val name: String){
constructor(a:Int,b:Int):this(a.toString())
}
// 子类有主构造函数,必须在此初始化基类
class Dog(name: String, val breed: String) : Animal(name) { // 依靠父类的主构造函数完成父类的构建
// 次构造函数必须委托给主构造函数(通过this)
constructor(breed: String) : this("Unknown", breed)
}
class Cat(a:Int, val breed: String) : Animal(a,a) { // 这里其实是委托给了父类的次要构造方法
// 次构造函数必须委托给自己的主构造函数(通过this)从而完成对父类的构建
constructor(breed: String) : this(1, breed)
}
在上面中继承关于:父类()案例代码中也可以看到,子类的主构造方法可以是隐式的,而父类的中也没有其他属性且主构造也是隐式的,但也是需要调用父类的构造,所以这个()是必要的
-
如果子类没有主构造函数,那么无法通过主构造函数中借助父类构造函数完成父类的构建,那么在子类的次要构造中,需要显示的调用super方法进行父类的构造,或者通过该类上委托了super的其他次要构造函数达成间接的父类初始化
open class Shape {
// 基类有多个构造函数
constructor(size: Int) { /* ... */ }
constructor(color: String) { /* ... */ }
}
// 子类没有主构造函数
class Circle : Shape {
// 次构造函数1:调用基类的 Int 参数构造函数
constructor(size: Int) : super(size)
// 次构造函数2:调用基类的 String 参数构造函数
constructor(color: String) : super(color)
// 次构造函数3:委托给次构造函数1,最终调用基类的 Int 构造函数
constructor(size: Int, color: String) : this(size)
}
对于父类方法的复写/重写,和继承一样的,所有的父类中的方法默认自带final关键字,也就导致所有的希望能被重写的方法需要被加上open关键字,并且子类对父类的重写方法需要加上override关键字
除此之外,override关键字也表示这个方法可以被子类的子类继承,即子类不需要加open关键字,子类对于重写的方法即加了overried关键字的方法可以加上final防止再次复写
将 open
修饰符添加到 final 类(即没有 open
的类) 的成员上不起作用。
fun main() {
val dog = Doge("布鲁斯",12)
println(dog.name + " " + dog.age)
dog.eat()
}
open class Animals{
open var name:String?=null
open var age:Int?=null
open fun eat(){
println("Animal eat")
}
fun test(){
}
}
open class Doge(name:String,age:Int) : Animals(){
init{
this.name=name
this.age=age
}
final override fun eat(){ // 可以加上final关键字 防止子类的子类重写
println("Doge eat")
}
// 报错 子类中不允许出现和父类一样名称的方法 无论是否重写不重写
fun test(){
}
}
对于成员属性,可以直接继承,也可以通过override关键字继承成员属性,虽然不写overried重写也可以访问到,但是overried显示重写之后,子类中对于属性可以有自定义的get和set方法,不显示继承的情况下会使用父类的get和set逻辑
fun main() {
val dog = Doge("dog",12)
println(dog.name + " " + dog.age)
dog.age = 13
dog.name = "zealSinger" // 调用dog中自己的set方法逻辑了
println(dog.name + " " + dog.age)
dog.eat()
println("-----------------------")
val cat = Cat("cat",12)
println(cat.name + " " + cat.age)
cat.age = 13
cat.name = "kotlin" // 因为没有重写所以用的父类的get set方法
println(cat.name + " " + cat.age)
cat.eat()
}
open class Animals{
open var name:String?=null
open var age:Int?=null
open fun eat(){
println("Animal eat")
}
fun test(){
}
}
class Cat(name:String,age:Int): Animals() {
override fun eat() {
println("Cat eat")
}
}
open class Doge(name:String,age:Int) : Animals(){
override var age:Int?=age
set(value) {
field = value?.plus(10)
}
init {
this.name = name
}
override fun eat(){
println("Doge eat")
}
}
初次之外,继承重写成员属性,可以改变var和val,可以用var覆写val 但是不能用val覆写var,因为var本质上是存在get方法和set方法,val就是只有get方法,那么var覆盖val就是新增一个set方法而已 ;但是反过来是不允许,只有get方法不能覆盖get+set方法
open class Animals{
open var name:String?=null
open val age:Int?=null
}
open class Doge(name:String,age:Int) : Animals(){
override val name:String? = name // 报错 val不能覆盖var
override var age:Int?=age
}
抽象类
kotlin中抽象类的关键字也是abstract,抽象类中的抽象成员可以不具备实现,也不需要被open关键字修饰,默认实可以被继承实现的,并且抽象类是无法创建实例对象的
abstract class Polygon {
abstract fun draw()
}
class Rectangle : Polygon() {
override fun draw() {
// draw the rectangle
}
}
比较特别的是,子类中可以使用抽象方法覆盖父类中的正常方法
open class Polygon {
open fun draw() {
// some default polygon drawing method
}
}
abstract class WildShape : Polygon() {
abstract override fun draw()
}
嵌套类
类可以嵌套到类中,结构上是类似于Java的内部类,但是作用上是相当于静态内部类,在kotlin中称之为嵌套类
如下 是Kotlin中嵌套类
// 错误的写法
fun main() {
Outer().Inner().test() // 报错 因为嵌套类是静态的 脱离外部类 所以不能通过外部类对象调用(静态的要优先于类的创建)
}
class Outer{
var a:Int = 100
class Inner{
fun test(){
// 下面两种调用都报错 inner是静态内部类/嵌套类 不能直接使用外部的成员属性
println(a)
println(Outer.this.a)
}
}
}
// 正确的写法
fun main() {
Outer.Inner().test() // 通过类名.构造方法的方式直接创建嵌套类对象然后再调用test方法 而不是依靠外部类对象
}
class Outer{
var a:Int = 100
class Inner{
// 嵌套类中不能直接调用外部类的普通成员
fun test(){
println("这里是inner")
}
}
}
内部类
这里就是和Java中内部类是一样的,通过关键字 inner标识 内部类可以调用外部类的普通成员属性,因为内部类也是成员的一种且非静态,所以内部类中方法的调用要通过外部类对象创建内部类对象然后再调用 通过 this@外部类名.变量名 的方式可以调用到外部类的普通成员
fun main() {
Outer().Inner().test() // 通过外部类对象调用内部类的构造方法然后通过内部类对象调用内部类中的成员方法
}
class Outer{
var a:Int = 100
inner class Inner{
fun test(){
// 通过 this@外部类名.变量名 的方式可以调用到外部类的普通成员
println("这里是inner , a = ${this@Outer.a}")
}
}
}
接口和实现类
在kotlin中,接口的关键字也是interface
实现类实现接口也是通过 :接口名 的方式进行标识,也就是和继承是一样的,可以理解为类的 :后面就是该类的父类和接口,同Java,kotlin中一个类只能有一个父类但是可以实现多个接口
kotlin中的接口更像是JDK11之后的Java接口,接口中的方法可以有实现也可以没实现,例如方法aTest因为没有实现/方法体,所以实现类D和B必须要实现其接口,实现的关键字也是override
但是对于bTest而言,因为接口中有默认的实现(如下案例中是空的实现而已 但是也是实现了),所以实现类B和D可以不一定要实现该方法
// 对比B 和 D,:后面的接口和父类顺序无关紧要 只是一般我们先写父类再写实现的接口
class D():C(),A{
override fun aTest() {
}
}
class B():A,E,C(){
override fun aTest() {
}
override fun bTest() {
super<A>.btest()
super<E>.bTest() // 可以通过super调用接口中的原本的实现 super.bTest即可 这里是因为同时实现了A和E 通过泛型来指定调用E接口的实现,同理可以用super<A>.bTest来调用A中的逻辑
}
}
open class C{
}
interface A{
fun aTest() // BD必须实现
fun bTest(){ // BD可以不实现 实现的话则是覆盖
}
}
interface E{
fun bTest(){
}
}
接口中也可以定义属性,属性要么是抽象的即没有初始化值,接口中没有初始化就需要在实现类中进行初始化(可以重写变量get方法实现 也可以在主构造函数中初始化);要么就是想要有初始化值但是不能直接=初始化赋值,而是要编写get()方法进行初始化
class B(override val age: Int=21):A{ // 在主构造函数中进程初始化
// 在主构造中就是底层也是实现get方法 但是自定义性比较差
override var year: Int // 对于接口中的抽象成员属性year的初始化 通过实现get方法进行初始化 这里面可实现的操作相对灵活一些
get(){
return 2004
}
set(value) {}
}
interface A{
// 这两个没有被初始化 需要在实现类上进行初始化
var year:Int
val age:Int
// 这两个有初始化 通过get方法进行初始化 val是不可变所以没有set方法 var可变所以要写set逻辑
val name: String
get() = "zealsinger"
var school:String
get() = "HNUST"
set(value) {
// set逻辑
}
}
而且,实现类对于接口中属性的实现,也可以作到var覆盖val,但是需要注意 var覆盖val的话需要在实现属性上进行初始化,不能仅仅只有get和set方法
class B():A{
override var age: Int = 0 // 用 = 进行初始化是必要的
get()=21
set(value){
field=value
}
override var year: Int
get(){
return 2004
}
set(value) {}
}
interface A{
var year:Int
val age:Int
val name: String
get() = "zealsinger"
var school:String
get() = "HNUST"
set(value) {
// set逻辑
}
}
对象表达式
对象表达式形式上是类似于Java的匿名内部类,但是Java中的匿名内部类只能继承一个类或者实现一个接口,而kotlin中对象表达式可以同时继承一个类且实现多个接口,而且在Java的匿名内部类中只能实现和重写方法,对象表达式中可以自定义成员变量,自定义成员方法,实现方法和重写方法,可以理解为对象表达式是匿名内部类的增强版
其主要定义方式为
val/var 对象名 = object:要实现的接口或者继承的类(多个的话用逗号){
成员属性
成员方法
实现方法
重写方法
.......
}
fun main() {
// 可以同时继承类和实现接口
val obj = object : SomeClass(), InterfaceA, InterfaceB ,ClickListener {
val property = "自定义属性"
fun customMethod() = println("额外方法")
override fun onClick() {
println("执行了实现的OnClick方法")
}
override fun sum(a: Int, b: Int):Int{
println("执行了重写的sum方法")
return 2*a+b
}
}
// 直接利用对象表达式进行访问
obj.customMethod()
println(obj.property)
obj.onClick()
println(obj.sum(1, 2))
}
interface ClickListener {
fun onClick()
}
val listener = object : ClickListener {
override fun onClick() {
println("Clicked!")
}
}
interface InterfaceB {
}
interface InterfaceA {
}
open class SomeClass {
open fun sum(a:Int,b:Int):Int{
return a+b
}
}
除此之外,因为我们知道kotlin和Java之间是兼容的,那么kotlin代码中可以对Java的接口或者类也生成对应的对象表达式,而且kotlin对于Java接口的接口表达式和对于koltin自己的接口的接口表达式语法糖稍微不太一样,对于Java的接口的接口表达式允许使用lambda表达式
fun main() {
// JavaInterface和JavaInterfaceTow都是.java的interface
// 对象表达式实现Java中的接口
val javaObj = object : JavaInterface {
override fun javaFunction() {
println("func1")
}
}
// 利用lambda表达式简化
var javaObj2 = JavaInterface { println("func2") }
//如果Java接口中有多个方法就不能用lambda简化
var javaObj3 = object : JavaInterfaceTow{
override fun test1() {
TODO("Not yet implemented")
}
override fun test2() {
TODO("Not yet implemented")
}
}
}
数据类
数据类整体的定义和正常的类是一样的,其有三个特点
-
多了一个data关键字在class关键字前面
data class 类名(主构造参数列表){}
-
数据类要求一定要有主参数列表,即一定需要成员属性(成员方法可有可无)
// Test1是合法的 但是Test2和Test3都是不合法的
data class Test1(val x:Int,val y:Int){
}
data class Test2(x:Int,y:Int){
}
data class Test3(){
}
-
数据类相比较于普通类,会自带equals方法(可以传入任意类型的对象,先比较类型然后比较属性),copy方法(用于拷贝),toString方法,hashCode方法
需要注意 kotlin生成的copy方法是浅拷贝
fun main() {
val t1 = Test(name="张三",age = 21, school = "湖科大")
println(t1.toString()) // 重写了toString方法 Test(name=张三, age=21, school=湖科大)
// 如下copy方法可以基于t1的属性进行copy 传入要改变的参数 其余的就会按照t1的成员属性内容进行拷贝
val t2 = t1.copy(name="李四") // 也可以按照顺寻传入就不需要指定变量名 copy("李四")
println(t2.toString()) //Test(name=李四, age=21, school=湖科大)
val t3 = t1.copy(school = "湘大")
println(t3.toString())// Test(name=张三, age=21, school=湘大)
val t4 = t1.copy()
println(t4.toString()) // Test(name=张三, age=21, school=湖科大)
println(t1==t4) // true 本质就是equal比较
println(t1===t4) // false 说明是完全拷贝出了一个新对象
}
data class Test(val name:String,val age:Int,val school:String){
}
-
数据类不能是抽象、开放、密封或者内部的,即data和open不能共存
数据类的主要用途一般用于定义domain。即数据模型,在JWeb开发中,前后端之间的入参和出参就可以用数据类,因为数据类一般不会存在自己的其余方法,只需要一些equals , toString , copy等方法,类似于Java中一个类+@Data的注解 ,Kotlin中就不需要引入第三方包而是语法糖帮你完成了
伴生类
说了这么久,会发现我们没有提到过静态变量,静态方法(除了嵌套类)等相关的内容,这是因为Kotlin中没有静态类的概念,取而代之的是伴生类 or 伴生对象
伴生类的关键字为 companion object{...}在花括号内就可以定义变量和方法
fun main() {
println(Test.name)
Test.test()
}
class Test{
companion object{
val name:String = "test"
fun test(){
println("func - test")
}
}
}
但是需要注意的是,其本质上生成的不是静态的name属性和test方法,而是生成了一个静态的成员类对象companion
//不等同于如下
public class Test{
public static int name = "test";
public static test(){
println("func - test");
}
}
// 实际上等同于如下
// 实际上是生成了一个静态内部类且静态的成员对象,所以访问的name和test方法都是通过的companion调用的 即A.companion.test这样进行调用的
public class Test{
public static Companion companion = new Companion(null);
public static final class companion{
public int name = "test";
public test(){
println("func - test");
}
}
}
这样相当于成员用了单例模式,如果想要达到Java中的那种效果,可以通过一个 @JvmStatic 注解实现
fun main() {
println(Test.name)
Test.test()
}
class Test{
companion object{
@JvmStatic
val name:String = "test"
fun test(){
println("func - test")
}
}
}
枚举类
枚举类使用enum关键字标识,其标准定义结构为
enum class 类名 {.....}
每个枚举常量都是一个对象,枚举常量以逗号分隔,最后一个枚举类后面用分号;和其他的成员方法成员变量进行区分,也就是每个枚举都是枚举类的实例,所以可以如下进行初始化
fun main() {
println(enumClazz.SUCCESS)
println(enumClazz2.SUCCESS.flag)
}
enum class enumClazz{
SUCCESS(),
FAIL(),
UNKNOWN_ERROR()
}
enum class enumClazz2(val flag:Int){
SUCCESS(1),
FAIL(2),
UNKNOWN_ERROR(0)
}
通过枚举类.entries可以获取所有枚举项的集合,从而实现遍历等需求
同时,kotlin中enum枚举类可以实现接口,枚举类中每个情况枚举项都需要单独的对接口中的所有方法进行实现,也可以在枚举类中对一个接口进行统一的实现
fun main() {
for (entry in enumClazz.entries) { // .entries获得枚举项的集合进行遍历
println(entry.jf(1, 2)) // 分别调用每个枚举项中的jf和cf方法
println(entry.cf(2, 3))
println(entry.test(2, 3)) // SUCCESS调用统一的方法 返回-1;FAIL调用自己实现的test方法逻辑,返回0
}
}
interface A{
fun jf(a:Int, b:Int):Int
}
interface B{
fun cf(a:Int,b:Int):Int
}
interface C{
fun test(a:Int,b:Int):Int
}
enum class enumClazz:A,B,C{ // 实现两个接口 下面每个枚举项中都需要实现这两个方法
SUCCESS{
override fun jf(a: Int, b: Int):Int {
return a+b
}
override fun cf(a: Int, b: Int): Int {
return a*b
}
},
FAIL{
override fun jf(a: Int, b: Int):Int {
println("计算失败,无法加法,返回0")
return 0
}
override fun cf(a: Int, b: Int): Int {
println("计算失败,无法乘法,返回0")
return a*b
}
override fun test(a:Int, b: Int):Int{
println("计算失败,无法减法,返回0")
return 0
}
}; // 分号隔开枚举项和其余成员方法和成员变量
override fun test(a:Int, b: Int):Int{ //作为成员方法统一实现接口C中的方法,从而不需要单个实现,当然,枚举项内也可以同时实现,实现了就用枚举项自己的实现,没实现就用这个统一的实现
return a-b
}
}
每个枚举类中的枚举项,都自带name属性和ordinal属性,前者为我们的枚举项命名,后者从0开始按照枚举类中的枚举项的定义顺序进行递增
fun main() {
for(entry in enumClazz.entries){
println(entry.ordinal)
println(entry.name)
}
}
enum class enumClazz{
SUCCESS(),
FAIL(),
UNKNOWN_ERROR()
}
/* 输出如下
0
SUCCESS
1
FAIL
2
UNKNOWN_ERROR
*/
密封类和密封接口
密封类某种意义哈桑可以理解为是一个增强版的枚举类,主要用来标识受限的类继承结构,密封类的关键字是sealed,密封类是抽象的,可以有子类但是子类可以定义在密封类内部也可以在外部正常维护继承关系
fun main() {
handler(sealedTestClass.test1())
handler(sealedTestClass.test2())
handler(outTestClass())
}
fun handler(clazz:sealedTestClass){
// 重点在这里 密封类可以和when关键字搭配使用
// is 后面接上参数类的类型 底层就是变化成了if-else语句和instanceof语句判断是否为类的实现or继承关系
when(clazz){
is sealedTestClass.test1 ->{
clazz.fun1() // 并且在每个is情况后面 因为is已经指定确认了对应的类型 所以这里只能调用fun1而不能fun2
clazz.allFun()
}
is sealedTestClass.test2 ->{
clazz.fun2()
clazz.allFun()
}
is outTestClass -> {
clazz.allFun()
}
}
}
sealed class sealedTestClass{
// 在内部维护的继承关系
open fun allFun(){
println("allFun")
}
class test1:sealedTestClass(){
fun fun1(){
println("fun1")
}
}
class test2:sealedTestClass(){
fun fun2(){
println("fun2")
}
override fun allFun() {
println("allFun--test2")
}
}
}
// 在外部维护的继承关系
class outTestClass:sealedTestClass(){
override fun allFun(){
println("allFun--outTestClass")
}
}
密封类在安卓架构中用的挺多的,因为密封类可以用于标识猜测用户的意图,从而调用不同的处理方式
密封接口也是类似的,sealed interface的关键字标识,好处就是在于,密封类因为单继承的原因,只能继承一个,但是接口实现的话可以实现多个接口,更加的方便
如下,下面的每个实现类可以分别实现两个密封接口,所以对应的可以在两个方法中针对不同的接口进行when操作,但是如果是密封类的话,只能操作一个,你还需要额外的继承关系
密封接口可以认为是接口+枚举 , 密封类可以认为是抽象类+枚举
单例
单例模式我们知道分为了:饿汉式单例 ; 懒汉式单例 ; 线程安全单例 ; 双重锁检测单例 ;静态内部类单例,这里我们顺便都给讲了kotlin的实现方式
饿汉单例
饿汉单例就是在类中就存在这么个单例,程序一运行就存在了,不管你以后是否会使用
在Java中就是在类中存在一个私有的静态的自己的类对象,对外关闭有参构造和无参构造,而是开放一个getInstance的方法返回内部的这个类对象
而在Kotlin中,因为不存在静态方法和静态成员的概念,所以提供了一个更加简单的方法,直接通过object关键字实现
// Java中实现单例
public class SingletonDemo{
private SingletonDemo instance = new SingletonDemo();
private SingletonDemo(){
}
public static SingletonDemo getInstance(){
return instance;
}
}
// kotlin
object SingletonDemo(){}
懒汉单例
懒汉模式就是在被访问的时候才会被创建,如果为null就会被创建,如果不为null则使用原本创建的对象,如果一直不访问则一直都是null
//Java实现
public class SingletonDemo {
private static SingletonDemo instance;
private SingletonDemo(){}
public static SingletonDemo getInstance(){
if(instance==null){
instance=new SingletonDemo();
}
return instance;
}
}
// kotlin实现
class SingleInstance private constructor() {
companion object {
private var instance:SingleInstance? = null
get(){
if(field==null) field = SingleInstance()
return field
}
fun getInstance(): SingleInstance {
return instance!!
}
}
}
线程安全的懒汉单例模式
线程安全的懒汉单例主要是在懒汉模式的基础上,让getInstance()方法即对外开放的获取实例的方法变得线程安全,最简单的也就是直接对方加锁
//Java实现
public class SingletonDemo {
private static SingletonDemo instance;
private SingletonDemo(){}
public static synchronized SingletonDemo getInstance(){
if(instance==null){
instance=new SingletonDemo();
}
return instance;
}
}
// kotlin实现
class SingleInstance private constructor() {
companion object {
private var instance:SingleInstance? = null
get(){
if(field==null) field = SingleInstance()
return field
}
@Synchronized
fun getInstance(): SingleInstance {
return instance!!
}
}
}
内容和上面懒汉单例基本一致,只是Java中实现锁的方式是添加sychronized关键字而Kotlin中式记住的@Synchronized关键字
双重锁检测
自定义注解
基础用法
Kotlin中自定义注解的关键字为annotation,注解类可以是公开的,内部的,私有的但不能是保护的
如下案例定义了一个Marker注解没有定义任何的成员变量和成员方法,可以通过主构造方法初始化成员属性
annotation class Marker
如下是一个定义了成员属性以及用主构造方法初始化成员属性的自定义注解
annotation class MyAnnotation(val value:String)
如下,Kotlin中的注解默认是没有使用范围限定的,即任何地方都可以使用,可以看到如下1,2,3,4处,分别是对main方法,自定义方法,自定义类,普通变量,普通方法都可以进行修饰注解,初次之外TestClass中也可以看到,注解除了和Java一样
annotation class Marker
annotation class Marker2
annotation class MyAnnotation(val value: String)
@Marker // 1
fun main() {
@Marker // 2
@MyAnnotation(value = "姓名")
val name:String = "zeal"
@MyAnnotation(value = "年龄")
val age:Int = 18
@Marker // 3
@MyAnnotation(value = "方法")
fun testFun(){
}
}
@Marker // 4
@Marker2 class TestClass(@Marker test1: String,@MyAnnotation("成员属性") test2: String) {
}
元注解
我们知道,在Java中,对于自定义注解需要配置一定的元注解,在kotlin中也有类似的要求,在Kotlin中,元注解有四个,但是从上面知道,不是必须的,但是为了实现注解的准确使用和范围限定,我们一般需要元注解来指定一些内容
元注解有如下四个
-
@Target 指定可以使用该注解的元素的可能类型(类,函数,属性,表达式)即注解的目标,Kotlin中注解的目标列表如下
-
@Retention 指定该注解的是否存储在编译后的class文件中,以及他是否在运行中可以通过反射可见(默认为true)
-
@Repeatable 允许在单个元素上多次使用该注解
-
@MustDocumented 指定该注解是公有API的一部分,并且应该包含在生成的API文档中显示的类或者方法的签名中
如下使用,代码第4行声明注解类型 MyAnnotation
,其中使用了三个元注解修饰 MyAnnotaion
注解。代码第1行使用 @MustBeDocumented
指定 MyAnnotaion
注解信息可以被文档生成工具读取。代码第2行使用 @Target(AnnotationTarget.CLASS)
指定 MyAnnotaion
注解用于修饰类 和 接口等类型。代码第3行 @Retention(AnnotationRetention.RUNTIME)
指定 MyAnnotaion
注解信息可以在运行时被读取。代码第4行的 description
是 MyAnnotaion
注解的属性。
@MustBeDocumented // 1
@Target(AnnotationTarget.CLASS) // 2
@Retention(AnnotationRetention.RUNTIME) // 3
annotation class MyAnnotation(val description: String) // 4
除此之外,一个annotation可以在Target中允许修饰多个类型,注解成员参数也可以使用泛型做处理,如下
@MustBeDocumented
@Retention(AnnotationRetention.RUNTIME) // 1
@Target(
AnnotationTarget.FUNCTION,
AnnotationTarget.PROPERTY,
AnnotationTarget.PROPERTY_GETTER,
AnnotationTarget.PROPERTY_SETTER
) // 2
// KClass<*>泛型 类型通配符 标识任一数据类型 默认为Unit::Class类型
annotation class MemberAnnotation(val type: KClass<*> = Unit::class, val description: String) // 3
可以看到,上面的@Target中标识这个注解可以同时作用于方法,属性,属性的get访问器和set访问器,那么该注解修饰一个成员属性的时候,我们同时可以修饰其自己,get/set方法的时候,可以分别在属性,get方法,set方法上分别使用注解,也可以通过::指定修饰的东西(举例 @get::注解 代表修饰的是get访问器),如下
import kotlin.reflect.KClass
@MustBeDocumented // 1
@Target(AnnotationTarget.CLASS) // 2
@Retention(AnnotationRetention.RUNTIME) // 3
annotation class MyAnnotation(val description: String) // 4
@MustBeDocumented
@Retention(AnnotationRetention.RUNTIME) // 1
@Target(
AnnotationTarget.FUNCTION,
AnnotationTarget.PROPERTY,
AnnotationTarget.PROPERTY_GETTER,
AnnotationTarget.PROPERTY_SETTER
) // 2
annotation class MemberAnnotation(val type: KClass<*> = Unit::class, val description: String) // 3
@MyAnnotation(description = "这是一个测试类") // 1
class Student {
@MemberAnnotation(type = String::class, description = "名字") // 2
@get:MemberAnnotation(type = String::class, description = "获得名字") // 3
var name: String? = null
private set
@MemberAnnotation(type = Int::class, description = "年龄") // 4
var age: Int = 0
@MemberAnnotation(type = Int::class, description = "获得年龄") get
@MemberAnnotation(type = Int::class, description = "不允许更新年龄")
private set
@MemberAnnotation(description = "设置姓名和年龄") // 6
fun setNameAndAge(name: String, age: Int) {
this.name = name
this.age = age
}
override fun toString(): String {
return "Person [name=$name, age=$age]"
}
}
在运行时获取注解及其成员属性信息
这里稍微涉及了一点反射相关的知识点,但是问题不大
fun main(args: Array<String>) {
val clz:KClass<Student> = Student::class // 1 获取类对象 KClass对象 类似Java中的class对象
// 读取类注解
val ann = clz.findAnnotation<MyAnnotation>() // 2 findAnnotation<注解类型> 获取该类上对应注解类型的注解对象
println("类${clz.simpleName},注解描述:${ann?.description ?: "无"}") // 3
// 读取成员函数的注解信息
println("---------- 读取成员函数的注解信息 ----------")
clz.declaredFunctions.forEach { // 4 clz.declaredFunctions 获取类中所有的方法集合,对于每一个方法对象使用findAnnotation<注解类型>即可获取每个方法上的对应类型的注解对象
val ann = it.findAnnotation<MemberAnnotation>() // 5
println("函数${it.name},注解描述:${ann?.description ?: "无"}")
}
// 读取属性的注解信息
println("---------- 读取属性的注解信息 ----------")
clz.declaredMemberProperties.forEach { // 6 clz.declaredMemberProperties 获取类中的成员属性集合,对于每个成员调用findAnnotation<>即可获取该成员上的对应类型的注解对象
val ann = it.findAnnotation<MemberAnnotation>() // 7
println("属性${it.name},注解描述:${ann?.description ?: "无"}")
}
}
其余关键字
?:
这个是一个运算逻辑符号,A?:B则代表如果A为null则返回B
const
kotlin中const关键字用于修饰常量且不可变(这个特点类似于val),但是和val的区别在于const修饰的变量必须式在编译的时候就需要能被确认的,从这一点上来看类似于Java中的static,所以const修饰的常量应该被称之为 编译时常量
也是因为这个特性,编译时常量需要如下特点
-
顶层或者object成员或者伴生类中才可以使用
-
没有自定义get方法
-
初始化必须是一个String类似或者基本类型的数值才可以
const val topConstValue = "topConstValue" //顶层
val topValue = "topValue"
class Test {
// const val cc = "cc" 不可在类中声明
companion object {
const val compObjConstValue = "compObjConstValue" // 伴生类中
val compObjValue = "compObjValue"
}
object obj: A() {
const val objConstValue = "objConstValue" // 单例中
val objValue = "objValue"
}
}
apply
可以将一个需要进行配置的变量的一些操作,统一的写在apply{.....}的大括号内
apply返回的是调用者对象,在apply的大括号内部可以使用this来代替调用对象以及通过this调用对应的属性(其实如下第二段代码中setReadable()完整版就是this.setReadable() 只是this被隐藏了)
class Person(var name: String, var age: Int) {
fun eat() {
println("吃柠檬")
}
fun work(hour: Int): Int {
println("work $hour hour,earn ¥${hour * 60}")
return hour * 60
}
}
fun main() {
val person: Person? = Person("hzh", 23)
println("person:$person")
val result = person?.apply {
age = 24
eat()
work(8) // 返回传入的person对象
}
println("result is:$result")
}
let
和apply有点像,也是let{..lamdba..}传入调用者,只不过apply返回调用者,let函数返回的是lambd最后一行,在let函数内部使用it代表调用对象 一般用于做对象的判空处理,属于拓展函数
结合?:运算符可以实现灵活的属性赋值,如下操作,如果不适用let和?: 可能会需要if-else,代码量可想而知的变多
fun main() {
println(testFun(null))
println(testFun("zeal"))
}
fun testFun(name:String?):String{
return name?.let{
"Welcome $it"
} ?: "What is your name?"
}
with
格式 with(对象){.....} 在大括号内使用this代表调用者,返回lambda函数的是最后一行的内容,属于内联函数,一般用于连续调用有个类的多个方法
class Person(var name: String, var age: Int) {
fun eat() {
println("吃柠檬")
}
fun work(hour: Int): Int {
println("work $hour hour,earn ¥${hour * 60}")
return hour * 60
}
}
fun main() {
val person: Person = Person("hzh", 23)
val result = with(person) {
age = 24
eat()
work(8) // 返回480
}
println("result is:$result")
}
run
run行数也和上面两个let和apply差不多,也是传入调用者,但是返回的是lamdba的执行结果也就是最后一行,run可以认为是with和let的结合
*它可以像*with()*函数一样直接在函数块中使用*this指代该对象,也可以像let()*函数一样为对象*做统一的判空处理。
fun main() {
val person: Person? = Person("hzh", 23)
val result = person?.run {
age = 24
eat()
work(8) // 返回480
}
println("result is:$result")
}
also
also函数和apply相似,接受调用者,返回调用者,不同的是also在内部对调用者是使用it进行的指代,而在apply中是使用this
apply,run,with,also,let的区别
takeIf
传入调用者,lambda逻辑执行成功返回true则返回调用者对象 ; 如果执行失败则返回null
.takeIf{.....}
takeUnless
文章标题:Java崽学习Kotlin---Kotlin入门基础语法
文章链接:https://zealsinger.xyz/?post=2
本站所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明来自ZealSinger !
如果觉得文章对您有用,请随意打赏。
您的支持是我们继续创作的动力!

微信扫一扫

支付宝扫一扫