最详细的Kotlin教程,入门级都能看懂

变量声明 var 与val

在其他语言中,我们定义变量的时候一般会先指明变量的类型,如int a表示这是一个整型的变量。但在Kotlin中却不是这样。
在Kotlin的产生思想之中,安全一直是主导Kotlin语言诞生的一个重要因素。因此,在Kotlin的变量定义时,必须指明这是可变的变量还是不可变的变量,其次再指明其变量类型。如下所示:

var a : Int = 20
val b : Boolean = true

第一条语句定义了一个可变的变量a,这个变量的类型是Int型,值为20;第二条语句定义了一个不可变的变量,变量名为b,变量类型是Boolean,值为true。

  • 关键字var:定义可变的变量,其值可以被重新赋值
  • 关键字val:定义不可变的变量,其一经赋值就不允许被修改
Java基本数据类型Kotlin对象数据类型数据类型说明
intInt整型
longLong长整型
shortShort短整型
floatFloat单精度浮点型
doubleDouble双精度浮点型
booleanBoolean布尔型
charChar字符型
byteByte字节型

val 和 var 的使用规则

如果说 var 代表了 varible(变量),那么 val 可看成 value(值)的缩写。但也有人觉得这样并不直观或准确,而是把 val 解释成 varible + final,即通过 val 声明的变量具有 Java 中的 final 关键字的效果,也就是引用不可变

我们可以在 IntelliJ IDEA 或 Android Studio 中查看 val 语法反编译后转化的 Java 代码,从中可以很清楚地发现它是用 final 实现这一特性的

val 的含义:引用不可变 (变量一旦初始化就不能更改指向,但是可以更改变量指向的内容里的值) 因此,val 声明的变量是只读变量,它的引用不可更改,但并不代表其引用对象也不可变。事实上,我们依然可以修改引用对象的可变成员。这跟 Java 中的 final 对象概念上是一样的。

class Book(var name: String) { // 用 var 声明的参数 name 引用可被改变 

    fun printName() {
        println(this.name)
            }
    }

fun main() {
    val book = Book("Thinking in Java") // 用 val 声明的 book 对象的引用不可变
 book.name = "Diving into Kotlin"
    book.printName() // Diving into Kotlin
}

重要原则:优先使用 val 来声明变量更好的理解可以是:尽可能采用 val、不****可变对象及纯函数来设计程序。关于纯函数的概念,其实就是没有副****作用的函数,具备引用透明性。

因为引用不可变,val 声明的变量只能被赋值一次,且在声明时不能省略变量类型。
在 Kotlin 编程中,我们推荐优先使用 val 来声明⼀个本身不可变的变量,这在大部分情况下更具有优势:

  • 这是⼀种防御性的编码思维模式,更加安全和可靠,因为变量的值永远不会在其他地方被修改(⼀些框架采用反射技术的情况除外);在Java中进行多线程开发时,由于Java的变量默认都是****可变的,状态共享使得开发工作很容易出错不可变性则可以在很大****程度上避免这⼀点
  • 不可变的变量意味着更加容易推理,越是复杂的业务逻辑,它的优势就越大。

既然有了 val, 为什么还要使用 var?

  • 因为 var 有更好的性能,占用内存更少。所以,尤其是可能在业务中需要存储大量的数据的数据结构,如集合遍历中使用 var 来计算结果等,显然采用 var 是其更加适合的实现方案。

在函数式开发中,我们优先推荐使用 val 和不可变对象来减少代码中的副作用,提升程序的可靠性和可组合性。在⼀些个别情况下,尤其是强调性能的代码中,用 var 定义局部变量会更加适合。

boolean

 var x: Int = 10
    var y: Boolean = (x in 0..100) //x是否在0到100之间
    println(y)

    y = (x !in 0..100)
    println(y)

运行结果:
true
false

编译时常量

val 声明的变量只能算是不可变的变量,但并不是一个常量,要定义一个真正意义上的常量,必须使用 constconst 只能修饰基本类型,且必须初始化

// 编译时常量

const val b = 3

这样定义了一个编译时常量,等价于 java 的 static final int

字符串的定义和操作

str.length // 12
str.substring(0, 5) // hello
str + " hello Kotlin!" // hello world! hello Kotlin! 
str.replace("world", "Kotlin") // hello Kotlin!

和 Java 的字符串没什么不同,但是由于 String 是⼀个字符序列,所以我们可以直接对其进行遍历

val str = "hello world!"
for (i in str.toUpperCase()) { 
    print(i)  // HELLO WORLD!
} 

还可以访问这个字符序列的成员:

str[0] // h
str.first() // h 

str.last() //!

str[str.length - 1] //!

此外,Kotlin 的字符串还有各种丰富的 API,如:

// 判断是否为空字符串

"".isEmpty() // true 
" ".isEmpty() // false 
"".isBlank() // true
"abcdefg".filter { c -> c in 'a'..'d' } // abcd 
拼接与java 可以直接用加号
 var a = "100"
    var yy = "结果"
    println(a + yy)

在这里插入图片描述

更多字符串类方法可以查阅Kotlin API⽂档:String - Kotlin Programming Language

定义原生字符串

fun main(args: Array<String>) {
    println("Hello World!")
    val rawString = """
    \n In Kotlin is awesome. 
    \n In Kotlin is a better Java. """
    println(rawString)
}
运行结果

在这里插入图片描述

简而言之,用这种3个引号定义的字符串,最终的打印格式与在代码中所呈现的格式⼀致,而不会解释转化转义字符(正如上述例子中的**\n**),以及Unicode的转义字符(如**\uXXXX**)。比如,我们用字符串来描述一段HTML代码,用普通字符串定义时必须是这样子:

val html = "<html>\n" +
    "    <body>\n" + 
    "        <p>Hello World.</p>\n" + 
    "    </body>\n" + 
    "</html>\n"

采用原生字符串的格式,会非常方便。如下:

val html = """<html>
    <body>
        <p>Hello World.</p> 
    </body>
</html>"""

运行结果:
在这里插入图片描述

String 字符串模版 $

模版拼接符号: $, 在双引号里面使用,可以配合{} 大括号来拼接复杂的串, 与JS的模版拼接类似。
案例:

var a = "100"
    var text = "$a 这是拼接的值"
    println(text)
    text = " 拼接结果: ${text + ",更多拼接"}"
    println(text)

运行结果:
在这里插入图片描述

字符串判等

Kotlin中的判等性主要有两种类型:

  • 结构相等。通过操作符 == 来判断两个对象的内容是否相等。
  • 引用相等。通过操作符 === 来判断两个对象的引用是否一样,与之 相反的判断操作符是 ! == **。**如果比较的是在运行时的原始类型,比如 Int,那么 === 判断的效果也等价于 ==。
var  a = "Java"
    var b = "Java"
    var c = "Kotlin"
    var d = "Kot"
    var e ="lin"
    var f = d + e
    println(a == b)
    println(a === b)
    println(c == f)

运行结果
在这里插入图片描述

类型判断 is ! is

Kotlin中使用is!is操作符判断对象是否符合类型,用法如下:

if (obj is String) {
    print(obj.length)
}

if (obj !is String) { // 与 !(obj is String) 相同
    print("Not a String")
} else {
    print(obj.length)
}
“不安全的”转换操作符

当需要类型转换时,Kotlin也提供了中缀操作符as供我们使用。但是as操作符是不安全的转换操作符,如果两个类型之间不可转换,其就会抛出一个异常,如下:

val x: String = y as String
1

请注意,null 不能转换为 String 因该类型不是可空的。 如果 y 为空,上面的代码会抛出一个异常。 为了让这样的代码用于可空值,请在类型转换的右侧使用可空类型:

val x: String? = y as String?
1
“安全的”转换操作符

为了避免异常,可以使用安全转换操作符 as?,它可以在失败时返回 null:

val x: String? = y as? String
1

请注意,尽管事实上 as? 的右边是一个非空类型的 String,但是其转换的结果是可空的。

变量的 get set

声明一个变量的完全公式如下
在这里插入图片描述
getter,setter可以自己重写

案例

import java.util.*
/*
* field: 变量本身,跟在变量声明后面,get,赋值进行更复杂操作
* */
var appName: String = "我的测试APP"
  //  field 当appName本身,
  get() = field + 10 + '\u2713'
var versionNum: Int = 200
  //  field 当appName本身,
  get() = field + 5
/*
* fild , set  field版本变量本身
* */
var color: Int = 333
  //  field 当appName本身,当color重新被赋值时,会乘以2, 第一次初始化不会
  set(value) {
    println("color被赋值了:${color}")
    field = value * 2
  }
fun main(args: Array<String>) {
  println("appName=${appName}")
  println("versionNum=${versionNum}")
  color = 900
  println("color=${color}")
  val ok = '\u2713' //\u2713
  println(ok)
}

运行结果
在这里插入图片描述

函数 fun

函数与java有一个最大区别就是,参数默认val 常量,不能直接修改 , 关键字是:fun , function的缩写。

我们先来看在Kotlin中的函数写法

fun methodName(param1: Int, param2: Int):Int{
	return 0
}

我们来解释一下上面这个函数。我们定义了一个函数名为methodName的函数,这个函数有两个参数,第一个参数名为param1,参数类型为Int,第二个参数名为param2,参数类型为Int。该函数返回一个Int类型的数据。因此,在Kotlin中函数一般这么定义:

fun 函数名(参数名1:参数类型, 参数名2:参数类型...) :返回值{

}

注意:
函数不需要任何参数,则括号里为空就行。
若函数不需要返回任何数据,则返回值部分就可以不写。
这是Kotlin中最标准的函数定义方式。

接下来,我们就来接近Kotlin的第二个思想,简洁
变量部分,我们说Kotlin的其中一个思想是安全,于是乎出现了var与val,那么这部分我们来了解Kotlin的第二个思想——简洁。
换言之,可以理解为能不写的就不写。

/*
  *  ft.variableController()
    ft.variableController(b=100)
    * 通过指名给哪个人参数传递值,实现java方法重载
    * 但是:a, b在这里默认是常量,函数体内无法修改
  * */
  fun variableController( a: Int = 9, b: Int = 10): Int {
    // a=200 //报错
    println(a + b)
    return a + b
  }


  fun variableController2(a: Int = 9, b: Int = 10): Int = a + b
  
  variableController()
variableController(b=100)
println( variableController2(b=200))

在这里插入图片描述

函数套娃,fun里可以写fun

  fun variableController3(a: Int = 9, b: Int = 10): Int {
    // a=200 //报错
    fun innerFun() {
      println("我是内部fun ${a}, ${b}")
    }
    innerFun()
    return a + b
  }
 

println(variableController3(b=300))

在这里插入图片描述

一行代码的情况

当你的函数体中只有一行代码时,Kotlin允许我们不必编写函数体可以直接将唯一的一行代码写在函数定义的尾部,中间用等号连接即可。来看下面的例子:

fun largeNumber(num1: Int, num2:Int) :Int{
	return max(num1, num2)
} 

这里我们编写了一个函数用来找到两个数中的最大的一个,这里的max()函数是Kotlin的内置函数,用来返回两个数中的最大的那一个。我们可以将这行代码简化如下

fun largeNumber(num1: Int, num2:Int) :Int = max(num1, num2)

这就是经过简化的函数形式,但这不是最简化的。Kotlin有着出色的类型推导机制,也就是能自动推导数据类型,因此,在一些地方,我们就不必写上数据类型,这里就是这种情况,我们可以省略掉返回值的类型,Kotlin能自动帮我们推导出来,代码进一步简化如下:

fun largeNumber(num1: Int, num2:Int) = max(num1, num2)

Kotlin的简化思想在其Lambda表达式中将会表现的淋漓尽致。

函数赋值函数 ::

赋值前提是,参数相同,返回值相同,使用 :: 2个冒号来作为标识。

 /*
  * 将函数赋值给函数
  * */
  fun main() {
    var func: (String) -> String
    func = ::getSummary
   println(func(""))
  }

  fun getSummary(msg: String): String {
    return "这是一个汇总信息"
  }

运行
在这里插入图片描述

嵌套递归

新增关键字:tailrec , 提示编译器尾递归自动进行优化,通过循环来实现功能,这样就不会再出现stackoverflow error

fun main(args: Array<String>) {
//嵌套循环
  var a = testSum(15)
  println(a)

  var b=fib(6)
  println(b)
}

/*
* tailrec 关键字,以结果做了递归,提高递归性能
* */
tailrec fun testSum(n: Int, sum: Int = 0): Int {
  if (n <= 0) return sum
  return testSum(n - 1, sum + n)
}

//斐波那契数列
tailrec fun fib(n: Int, prev: Int = 0, next: Int = 1) {
  println(next + prev)
  if (n == 0) prev else fib(n - 1, next, next + prev)
}

运行
在这里插入图片描述

运算符

Kotlin中的算术运算符基本与其他语言的使用方式相同,这里就不做过多赘述。这里只讲与其他语言有区别 的运算符。

位运算符

运算符功能
shl有符号左移
shr有符号右移
ushr无符号右移
and位与
or位或
xor位异或
inv()位非
案例
//    位移运算
    a = 3
    a = a shl 2 // 3* 2*2
    println(a) //12

运行结果: 12

a=12
a = a shr 1 
println(a) //12 /2 =6

运行结果:6

 //无符号右移
    // Int类型: 32位, 负数最高位是1- -1 = 11111111 11111111 11111111 11111111
    //   - 右移: 01111111 11111111 11111111 11111111(无符号右移使用0填充高位)
    a = -1
    a = a ushr 1
    println(a) // 2147483647

运行结果: 2147483647

程序控制语句 if when

学过编程的人都知道,在编程中有三大程序结构,分别为顺序结构,循环结构以及选择结构

选择结构 if

选择结构一般需要条件语句来支撑。在Kotlin中,条件语句的实现方式主要有两种:ifwhen

  • if语句
    Kotlin中的if语句和C/C++/Java中的if语句基本没太大差别,基本使用方法这三者都是相通的,你在其他语言中怎么用,在Kotlin中基本也能这么用。但也有一些区别,这里我们主要将其特殊之处。
 var b: Boolean = true
    var a = 10
    b = (a > 10)
    if (b) {
      println("这是个true")
    } else {
      println("这是个false")
    }

    if (a >= 90) {
      println("优秀")
    } else if (a >= 70) {
      println("良好")
    } else if (a >= 60) {
      println("及格")
    } else {
      println("不及格")
    }

在这里插入图片描述

if语句在Kotlin中是有返回值的,它的返回值就是if语句每一个条件中最后一行代码的返回值。 我们来看如下的代码:

/*
  * 省略return
  * */
  fun largeNum(a: Int, b: Int): Int {
    return if (a >= b) a else b
  }
  
var re=largeNum(100,200)
println("取大值: ${re}")

在这里插入图片描述

这就是经过简化后的代码,整个函数的函数体只有一条return语句。因此又可以通过我们上面所说的:函数体只有一条语句是可以简化的,我们再次简化如下:


fun largerNumber(num1: Int, num2: Int) = if (num1 > num2)  num1 else  num2

 //最后一条语句是if的返回值
    var a = 70
    var result = if (a >= 60) "及格" else "不及格"
    println("result=${result}")

在这里插入图片描述

  • when语句
    Kotlin中的when语句有点类似于Java中的switch语句,但它又远比switch语句强大得多。
    同时:最后一句语句也是做为when return
    我们先来看一个例子:
fun getScore(name: String) = if (name == "Tom") {
	86
} else if (name == "Jim") {
	77
} else if (name == "Jack") {
	95
} else if (name == "Lily") {
	100
} else {
	0
} 

这是我们利用if语句写出来的成绩查询逻辑,我们可以用when语句再写一次相同逻辑:

fun getScore(name: String) = when (name) {
	"Tom" -> 86
	"Jim" -> 77
	"Jack" -> 95
	"Lily" -> 100
	else -> 0
} 

我们从上面的例子可以看出,when和switch的结构很相似,不同的是switch需要冒号和break,而when语句仅仅需要一个->符号就可实现相同功能。when语句允许传入任何类型的参数。同样的,when语句和if语句一样,也是有返回值的。不仅如此, when不仅能实现值匹配,还能实现类型匹配,我们来看如下代码:

fun checkNumber(num: Number) {
	when (num) {
		is Int -> println("number is Int")
		is Double -> println("number is Double")
		else -> println("number not support")
	}
} 

上述代码中,is关键字就是类型匹配的核心,它相当于Java中的instanceof关键字。由于checkNumber()函数接收一个Number类型的参数,这是Kotlin内置的一个抽象类,像Int、Long、Float、Double等与数字相关的类都是它的子类,所以这里就可以使用类型匹配来判断传入的参数到底属于什么类型,如果是Int型或Double型,就将该类型打印出来,否则就打印不支持该参数的类型。

when语句的基本用法就是这些,但其实when语句还有一种不带参数的用法,虽然这种用法可能不太常用,但有的时候却能发挥很强的扩展性。拿刚才的getScore()函数举例,如果我们不在when语句中传入参数的话,还可以这么写:

fun getScore(name: String) = when {
	name == "Tom" -> 86
	name == "Jim" -> 77
	name == "Jack" -> 95
	name == "Lily" -> 100
	else -> 0
} 

可以看到,这种用法是将判断的表达式完整地写在when的结构体当中。注意,Kotlin中判断字符串或对象是否相等可以直接使用==关键字,而不用像Java那样调用equals()方法。可能你会觉得这种无参数的when语句写起来比较冗余,但有些场景必须使用这种写法才能实现。举个例子,假设所有名字以Tom开头的人,他的分数都是86分,这种场景如果用带参数的when语句来写就无法实现,而使用不带参数的when语句就可以这样写:

fun getScore(name: String) = when {
	name.startsWith("Tom") -> 86
	name == "Jim" -> 77
	name == "Jack" -> 95
	name == "Lily" -> 100
	else -> 0
}

现在不管你传入的名字是Tom还是Tommy,只要是以Tom开头的名字,他的分数就是86分。

循环结构

循环结构我们需要使用循环语句来实现。Kotlin中的循环语句有while循环与for循环。其中,while循环的使用方法与Java/C/C++的while循环的使用方法基本相同。我们主要来学习一下for循环。
Kotlin在for循环方面做了很大幅度的修改,Java中最常用的for-i循环在Kotlin中直接被舍弃了,而Java中另一种for-each循环则被Kotlin进行了大幅度的加强,变成了for-in循环,所以我们只需要学习for-in循环的用法就可以了。
在开始学习for-in循环之前,还得先向你普及一个区间的概念,因为这也是Java中没有的东西。

区间 … until downTo

区间kotlin新东西,我们可以使用如下Kotlin代码来表示一个区间:

//    全闭合,区间表示方法一: ..
    var range1 = 1..10
    println(range1)
//   左闭合 区间表示方法二:
    var range2 = 0 until 9
    println(range2)

    // 降序区间: downto
    var range3 = 8 downTo 0
    println(range3)

运行结果:
在这里插入图片描述

第一条语句表示创建了一个0到9的区间,并且两端都是闭区间,这意味着0到10这两个端点都是包含在区间中的。 代表:[0, 10]。

其中,…是创建两端闭区间的关键字

第二条语句表示创建了一个0到9的左闭右开区间,它的数学表达方式是[0, 10)。

总结:打印出来像是个字符串,区间主要作用是用于遍历。

有了区间之后,我们就可以通过for-in循环来遍历这个区间,如下所示

for (i in 0..10 step 2) {
	println(i)
}

for (i in 0 until 10) {
	println(i)
}

运行
在这里插入图片描述

第一个for语句表示在[0,10]的区间内遍历,每次循环递增2,即i以0,2,4…的方式遍历。step 2表示步长,当需要以1的方式递增时可以省略。第二个for语句则表示在[0,10)的区间内以步长为1的方式遍历。

如果你想创建一个降序的区间,可以使用downTo关键字,用法如下:

fun main() {
	for (i in 10 downTo 1) {
		println(i)
	}
}

这里我们创建了一个[10, 1]的降序区间。

数组

  • 数组是存储一组相同数据类型的容器
基本数据类型数组
kotlin中基本数据类型数组的种类
名称含义
BtyeArray存储8bit整型的数组
ShortArray存储16bit整型的数组
IntArray存储32bit整型的数组
LongArray存储64bit整型的数组
UByteArray存储8bit无符号整型的数组
UShortArray存储16bit无符号整型的数组
UIntArray存储32bit无符号整型的数组
ULongArray存储64bit无符号整型的数组
CharArray存储字符类型的数组
FloatArray存储单精度浮点型的数组
DoubleArray存储双精度浮点型的数组
BooleanArray存储布尔型的数组
kotlin内置创建基本数据类型数组的方法
方法名含义
byteArrayOf()创建底层存储byte类型数据的数组
shortArrayOf()创建底层存储short类型数据的数组
intArrayOf()创建底层存储int类型数据的数组
longArrayOf()创建底层存储long类型数据的数组
ubyteArrayOf()创建底层存储无符号byte类型数据的数组
ushortArrayOf()创建底层存储无符号short类型数据的数组
uintArrayOf()创建底层存储无符号int类型数据的数组
ulongArrayOf()创建底层存储无符号long类型数据的数组
charArrayOf()创建底层存储char类型数据的数组
floatArrayOf()创建底层存储float类型数据的数组
doubleArrayOf()创建底层存储double类型数据的数组
booleanArrayOf()创建底层存储boolran类型数据的数组
  • 创建基本数据类型数组的三种方式

    1. 使用构造方法创建指定长度的空数组

      //语法格式
      数组类型(数组长度);
      
      
    2. 使用构造方法创建指定长度的数组,并使用Lambda表达式初始化数组中的元素

      //语法格式
      数组类型(数组长度){
          Lambda表达式代码块
      } 
      
    3. 使用内置方法创建包含指定元素的数组

      //语法格式,以Int类型的数组为例
      intArrayOf(元素一,元素二,元素三);
        
      
  • 案例

        import java.util.*

fun main(args: Array<String>) {
    /*初始化一个数组,不指定长度域intArrayOf
    * */
    val arr = intArrayOf(1, 2, 3)
    println("打印数组列表: ${Arrays.toString(arr)}")
    println("打印数组地址与长度: $arr ${arr.size}")

    /*确定长度整数数组,指定长度,默认item初始值:0
    * */
    val intArr = IntArray(4)
    println(Arrays.toString(intArr))

    /*
    *    隐世参数it :表示当前遍历的元素下标,
    * 添加it 变更,会遍历的次数为数组的长度,并赋值对应位置索引值给item,
    * */
    val intArr2 = IntArray(5, { it; })
    println("数组默认索引来初始值:${intArr2.contentToString()}")
    //contentToString(),调试是原生java :Arrays.toString()

    /*
    * 所有项:默认初始值:2*/
    val intArr3 = IntArray(6, {
        it;
        println("当前隐式参数it值:${it}");
        2;
    })
    println(intArr3.contentToString())
}

在这里插入图片描述

IntArray和intArrayOf的区别

可以这样初始化数组,并指定赋值

val arr = intArrayOf(1, 2 ,3)

数组内的元素就是[1, 2, 3]
如果要初始化长度为3的数组,但不知道里面填什么内容,用 intArrayOf 实现不了,需要使用IntArray:

val arr = IntArray(4 ) 
//默认初始值为0  [0,0,0,0]
引用类型数据数组
  • 引用数组中的元素都是引用类型,int等基本数据类型存储在引用数组中,实现存储的是对应java中的Integer包装类

  • 创建引用型数组的三种方式

    1. 创建指定长度的空数组

      //语法格式
      arrayOfNull<数据类型>(数组长度);
      
      
    2. 创建数组时指定元素

      //语法格式
      arrayOf<数据类型>(元素一,元素二,元素三);
      //因为kotlin中的类型推断机制,使用指定元素进行创建数组时,可以省略数据
      arrayOf(元素一,元素二,元素三);
      
      
    3. 使用Array类的构造方法配合Lambda表达式创建指定长度的数组

      //语法格式
      Array<数据类型>(数组长度){
          Lambda表达式代码块
      }
      
      
    • 案例
            println("kotlin中的引用数据类型数组");
    
            //1.创建指定长度的空数组
            val array1 = arrayOfNulls<String>(5);
            //创建指定长度的空数组:[null, null, null, null, null]
            println("创建指定长度的空数组:${array1.contentToString()}");
    
            //2.创建数组时指定元素
    //        val array2 = arrayOf<String>("a", "b", "c", "d", "e");
            //简写
            val array2 = arrayOf("a", "b", "c", "d", "e");
            //创建数组时指定元素:[a, b, c, d, e]
            println("创建数组时指定元素:${array2.contentToString()}");
    
            //3.使用Array类的构造器创建数组
            val array3 = Array(5){
                //Lambda表达式中可以使用隐式参数it
                println("当前隐式参数it的值为:$it");
                'a'+it;
            }
            //使用Array类的构造器创建数组:[a, b, c, d, e]
            println("使用Array类的构造器创建数组:${array3.contentToString()}");
    
            //4.当引用类型数组存储的元素数据类型不一致时,这时元素的数组就是存储Any类型的引用型数组,元素也是Any类型的,就不再是原本的数据类型了
            val array4 = arrayOf(1, "a", 3.14);
            println(array4.javaClass);
    
    

运行结果

任意类型的数组 Array<Any?>

任意类型的数组也还是引用类型

    /*
    * 不确定类型的数组, > =中间必须手动留一个空格,否则按错
    * */
//    var anyArr1: Array<Any?>= arrayOf("a", "b", "c", true, 34) //等号之前无空格,报错
    var anyArr: Array<Any?> =arrayOf("a", "b", "c", true, 34)
    println(anyArr.contentToString())

运行结果
在这里插入图片描述

初始值为null : arrayOfNulls<类型>(长度)
  /*
  * 创建一个元素为null 空数组:arrayOfNulls
  * 必须指定元素类型,
  * 必须指定长度
  * */
fun nullArrayTest(){
 var nullArr= arrayOfNulls<Int>(3)
    println(nullArr.contentToString())
 var nullArr2= arrayOfNulls<String>(13)
    println(nullArr2.contentToString())

//    修改元素值
    nullArr[1]=90
    println(nullArr.contentToString())
}

在这里插入图片描述

数组的常用操作
  1. 获取数组的长度,通过size属性
  2. 通过索引获取元素,"[]"索引操作符的方式或get()方法的方式
  3. 修改索引位置的元素,"[]"索引操作符的方式或set()方法的方式
  4. 遍历数组,"for"循环的方式,通过数组属性indeces的方式,通过withIndex()方法同时遍历索引和值的方式
    在这里插入图片描述
    在这里插入图片描述
  • 案例
    //数组的常用操作
    val array = arrayOf(1, 2, 3, 4, 5);
    println("创建的数组为:${array.contentToString()}");

    //1.数组的长度
    val length = array.size;
    //创建的数组为:[1, 2, 3, 4, 5]
    println("数组的长度为:$length");

    //2.通过索引获取数组的元素
    var element = array[0];
//    var element = array.get(0);
    //通过索引获取数组的元素为:1
    println("通过索引获取数组的元素为:$element");

    //3.通过索引修改元素
    array[0] = 10;
//    array.set(0,10);
    //修改后的数组为:[10, 2, 3, 4, 5]
    println("修改后的数组为:${array.contentToString()}");

    //4.遍历数组
    //4.1直接遍历数组的元素
    for (v in array){
        println("当前元素为:$v");
    }
    //4.2通过索引遍历数组的元素
    for (i in  array.indices){
        println("当前索引为:${array.indices},当前元素为:${array[i]}");
    }

    //4.3同时遍历索引和值

    for ((index,value) in array.withIndex()){
        println("当前索引为:$index,当前元素为:$value");
    }

    for (indexvalue in array.withIndex()){
        println("当前索引为:${indexvalue.index},当前元素为:${indexvalue.value}");
    }
 

运行结果

数组打印

/*
* 数组打印
* */
var anyArr2: Array<Any?> = arrayOf("a", "b", "c", true, 34, null, { "key" to 12 })
println(anyArr2.contentToString())
println(anyArr2.joinToString())

在这里插入图片描述

多维数组
  • 数组中存储数组便是多维数组,也就是数组可以套娃,数组套娃一层就是二维数组,两层就是三维数组,以此类推

集合

集合类型在Kotlin中可以是只读的或可变的,因此Java的集合映射如下(此表中的所有Kotlin类型都定义在kotlin.collections包中)

Java类型Kotlin只读类型Kotlin可变类型转换平台类型
Iterator<T>Iterator<T>MutableIterator<T>(Mutable)Iterator<T>!
Iterable<T>Iterable<T>MutableIterable<T>(Mutable)Iterable<T>!
Collection<T>Collection<T>MutableCollection<T>(Mutable)Collection<T>!
Set<T>Set<T>MutableSet<T>(Mutable)Set<T>!
List<T>List<T>MutableList<T>(Mutable)List<T>!
ListIterator<T>ListIterator<T>MutableListIterator<T>(Mutable)ListIterator<T>!
Map<K, V>Map<K, V>MutableMap<K, V>(Mutable)Map<K, V>!
Map.Entry<K, V>Map.Entry<K, V>MutableMap.MutableEntry<K,V>(Mutable)Map.(Mutable)Entry<K, V>!
  /*
  * set 元组,不可变长,set集合
  * */
  var l1 = listOf("a", "c", "c", "d", "e")
  var l2 = listOf("a", "f", "c", "d", "e")
  val set: Set<String> = l1 union l2 // 并集, [a, c, d, e, f]
  println(set)
  
  /*
  * 可变成元组*/
  var mls: MutableSet<String> = mutableSetOf("name" ,"to", "Lani", "Lani")
  println(mls)
  var set2= mutableSetOf("KK","william", "Lani")
  println(set2 intersect  mls) //
  println( set2 subtract mls) //差值

运行结果

[a, c, d, e, f]
[name, to, Lani]
[Lani]
[KK, william]

类和对象

我们先看kotlin中类的实现,如下所示:

class Person {
	var name = ""
	var age = 0
	fun eat() {
		println(name + " is eating. He is " + age + " years old.")
	}
} 

Person类已经定义好了,如下代码是对这个类的实例化:

val p = Person()  

Kotlin中实例化一个类的方式和Java是基本类似的,只是去掉了new关键字。

继承与构造

继承

现在Person类是无法被继承的,我们得让它可以被继承才行,方法也很简单,在Person类的前面加上open关键字就可以了,如下所示:

open class Person {
...
} 

加上open关键字之后,我们就是在主动告诉Kotlin编译器,Person这个类是专门为继承而设计的,这样Person类就允许被继承了。
第二件事,要让Student类继承Person类。在Java中继承的关键字是extends,而在Kotlin中变成了一个冒号,写法如下:

class Student : Person() {
	var sno = ""
	var grade = 0
} 
构造函数
class Student constructor(name: String) {
  /*
  * 1. 类主构造函数:constructor 直接写在声明后 :
  * class Student constructor(){}
  * 2.创建同时指定为类属性,使用var val关键字
  * class Teacher2(var name: String)
  * class Teacher2(var name: String="LANI")
  * */
}

类属性

// 仅是定义了类的构造函数,name还不是类的属性
class Teacher(name: String)

// 仅是定义了类的构造函数,增加了var 或者val, name才是类的属性
class Teacher2(var name: String)

class School(var name: String, var description: String) {
  /** 只有主构造能声明类的属性,次的变量只能是变量,不能声明为属性
   *次要构造函数, 需要使用this() 委托到构造函数,
   * this()调用主构造函数结构必须与主构造函数一致。
   * */

  var age: Int = 0
  constructor(name: String, count:Int) : this(name, "本科")
  
  // 这里count不通声明为属性
  constructor(name: String) : this(name, "本科")
 
}

主构造函数相比次要(辅助)构造函数:

  1. 主构造函数:可以直接在主构造函数中定义类属性,使用更方便,但是主构造函数只能存在一个,并且无法编写函数体,只有为类属性做初始化赋值的效果。
  2. 辅助(次要)构造函数:可以存在多个,并且可以自定义函数体,但是无法像主构造函数那样定义类属性,并且当具有主构造函数时,所有次要构造函数必须直接或间接地调用主构造函数。

Kotlin语言本身比较灵活,类中并不是一定需要主构造函数,全部写辅助构造函数也是可以的,但是再怎么都得有构造函数。

init 关键字

class School(var name: String, var description: String) {
  /**通过init关键字,实现主构造的函数体
   * 多个init 会按顺序合并为一个执行 
  * */
  init{
    println("构造了一个学校 START")
  }  
  init{
    println("构造了一个学校${name}")
  }
   init{
    println("构造了一个学校 END")
  }
  }
  
 fun main(){  var school=School("华科")}
 

运行结果:
在这里插入图片描述

lateinit 关键字

/* *
*类的属性创建方式2,创建时必须给初始值
* 或者使用lateinit
* */
class Teacher3(var name: String) {
  var age: Int = 0
  lateinit var gender: String
}

operator 关键字

operator 实现了对象进行类似基本类型的运算重载
可以重载一元运算符
在这里插入图片描述
可重载二元运算符
在这里插入图片描述

案例1

class Student constructor(var name: String) {

  /*
  * 1. 类主构造函数:constructor 直接写在声明后 :
  * class Student constructor(){}
  * 2.创建同时指定为类属性,使用var val关键字
//  * class Teacher2(var name: String)
  * class Teacher2(var name: String="LANI")
  * */
  operator fun plus(other: Student): String {
    return other.name + " married  "+this.name +" "
  }
}

fun main(args: Array<String>) { 
  var student=Student("Lani")
  var student2=Student("LEE")
  println(student +student2)
}

运行结果
在这里插入图片描述

案例2

class OhterObj<T>() {
  /*
  * T 代表这个数组的元素是可以任意类型,在初始化实例的时候才确定
  * 类里面所有的 T ,都代表初始化时类型,必须匹配
  * 
  * <T>指一个代码块内部,所有地方在用初始时指定类型替换,操作规则按指定类型
  *  <T>一个占坑类型符
  *
  *
  * */
  var array: Array<T>? = null

  constructor(array: Array<T>) : this() {
    this.array = array
  }

  //  返回值是:T,任意,与创建ArrayObj时指定的类型相等
  //  重载数组取值: []
  public operator fun get(index: Int): T? {
    return array?.get(index)
  }
//重载数组赋值值: []=运算符
  public operator fun set(index: Int, value: T): Unit {
    this.array?.set(index, value)
  }
}

fun main(){
  var list=OhterObj<String>() //实例化一个OhterObj实例,指定内部需要使用到T的地方,都是String
  list.array= arrayOf("LANI", "LEE")
  println(list[0])
  list[1]="LODON"
  println(list[1])
  }

运行:
在这里插入图片描述

接口

接口是用于实现多态编程的重要组成部分。我们都知道,Java是单继承结构的语言,任何一个类最多只能继承一个父类,但是却可以实现任意多个接口,Kotlin也是如此。我们写一个接口类,如下所示:

interface Study {
	fun readBooks()
	fun doHomework()
}
1234

接下来就可以让Student类去实现Study接口了,代码如下:

class Student(name: String, age: Int) : Person(name, age), Study {
	override fun readBooks() {
	println(name + " is reading.")
	}
	override fun doHomework() {
		println(name + " is doing homework.")
	}
} 

熟悉Java的人一定知道,Java中继承使用的关键字是extends,实现接口使用的关键字是implements,而Kotlin中统一使用冒号,中间用逗号进行分隔。上述代码就表示Student类继承了Person类,同时还实现了Study接口。另外接口的后面不用加上括号,因为它没有构造函数可以去调用。

override 和 open

/*重写父属性造成的空指针
* */
open class Student4 {
  open var name: String = "LANI"

  init {
    println("我是学生,${this.name}
  }

  open fun sayHello() {
    println("Hello,我是student4,${this.name}}")
  }
}

/*重写父属性造成的空指针
* */
class ComputerStudent : Student4() {
  override var name: String = "LANI"

  init {
    println("我是计算机的学生,${this.name}")
  }

  override fun sayHello() {
    println("Hello,我是student4,${this.name}}")
  }

  fun program() {
    println("我是一个可编程的学生")
  }
}

fun main(args: Array<String>) {
 var cs=ComputerStudent()

//  多态,类型是Student4, 实例化是ComputerStudent
  var student:Student4=ComputerStudent()
  student.sayHello() //调用子类的sayHello
//  error,无法调用子类的方法
  student.program()
}

运行结果
在这里插入图片描述

把 student.program(),删除之后的运行
在这里插入图片描述

数据类和单例类
数据类

在Kotlin中,有一种特别的类,他们以data关键字开头,这就是数据类。在一个规范的系统架构中,数据类通常占据着非常重要的角色,它们用于将服务器端或数据库中的数据映射到内存中,为编程逻辑提供数据模型的支持。数据类通常需要重写equals()hashCode()、**toString()**这几个方法。其中,**equals()**方法用于判断两个数据类是否相等。hashCode()方法作为equals()的配套方法,也需要一起重写,否则会导致HashMapHashSet等hash相关的系统类无法正常工作。**toString()**方法用于提供更清晰的输入日志,否则一个数据类默认打印出来的就是一行内存地址。
这里实现一个手机数据类,只有品牌和价格两个字段,代码如下:

data class Cellphone(val brand: String, val price: Double)
1

当在一个类前面声明了data关键字时,就表明你希望这个类是一个数据类,Kotlin会根据主构造函数中的参数帮你将equals()hashCode()toString() 等固定且无实际逻辑意义的方法自动生成,从而大大减少了开发的工作量。

单例类

单例类是Kotlin中特有的功能,他是单例模式的实现。单例模式是最常用、最基础的设计模式之一,它可以用于避免创建重
复的对象。比如我们希望某个类在全局最多只能拥有一个实例,这时就可以使用单例模式。如下代码是

public class Singleton {
	private static Singleton instance;
	private Singleton() {}
	public synchronized static Singleton getInstance() {
		if (instance == null) {
			instance = new Singleton();
		}
		return instance;
}
	public void singletonTest() {
		System.out.println("singletonTest is called.");
	}
}
12345678910111213

这段代码其实很好理解,首先为了禁止外部创建Singleton的实例,我们需要用private关键字将Singleton的构造函数私有化,然后给外部提供了一个getInstance()静态方法用于获取Singleton的实例。在getInstance()方法中,我们判断如果当前缓存的Singleton实例为null,就创建一个新的实例,否则直接返回缓存的实例即可,这就是单例模式的工作机制。
而如果我们想调用单例类中的方法,也很简单,比如想调用上述的singletonTest()方法,就可以这样写:

Singleton singleton = Singleton.getInstance();
singleton.singletonTest();
12

在Kotlin中创建一个单例类的方式极其简单,只需要将class关键字改成object关键字即可。如下所示:

object Singleton {
	fun singletonTest() {
		println("singletonTest is called.")
	}
}
12345

可以看到,在Kotlin中我们不需要私有化构造函数,也不需要提供getInstance()这样的静态方法,只需要把class关键字改成object关键字,一个单例类就创建完成了。而调用单例类中的函数也很简单,比较类似于Java中静态方法的调用方式:

Singleton.singletonTest()
1

这种写法虽然看上去像是静态方法的调用,但其实Kotlin在背后自动帮我们创建了一个Singleton类的实例,并且保证全局只会存在一个Singleton实例。

Lambda表达式

Lambda表达式是Kotlin的基础,涉及到Kotlin编程的全过程,因此在Android开发——Kotlin语法之Lambda表达式中讲解。

空指针问题

写代码时的空指针异常

我们在编写程序时,很容易遇见因为传入或者程序自动产生空指针导致程序崩溃的问题,而在Kotlin中,很好的处理了这个问题。这同时也是Kotlin语言安全的一大体现。我们来看一段代码:

fun doStudy(study: Study) {
	study.readBooks()
	study.doHomework()
}

这段代码在Kotlin语言中时没有空指针风险的。
因为Kotlin默认所有的参数和变量都不可为空,当你在外部调用此函数时传入空指针,编译器将在编译的时候就会报错。如果我们确实需要传入的参数为空,Kotlin也提供这样的写法,就是在类名的后面加上一个问号 ?
比如,Int表示不可为空的整型,而**Int?就表示可为空的整型;
String表示不可为空的字符串,而
String?**就表示可为空的字符串。
换句话说,对于一个整型来说,它其实分为可为空的整型与不可为空的整型两个类型。
到此为止,其实Kotlin已经解决了我们写代码时传入参数为空的问题,那么还剩下运行时空指针异常的问题,Kotlin也有一套自己的解决方法。

案例:

fun main(args: Array<String>) {
  /*
  * 允许为空变量: ?
  * 判空:?.  空的话,点号后面调用不执行
  * 判空 : !!, 取到空值程序抛出异常并终止
  * 判空: ?.  加上 ?: , 不为空执行冒号左边,空的话,执行?:右边的语句,
  *
  * */
  var student1: Student? = null
  println(student1?.name )
  println(student1?.name ?: "学生对象为空")
  println(student1!!.name)
//  var student2:Student = null

}

运行结果
!!双感叹强制执行语句,出现空会引发空指针: java.lang.NullPointerException
在这里插入图片描述

程序运行时的空指针异常 java.lang.NullPointerException

大部分程序运行起来是程序员无法把控的事情,很多时候会产生我们没有在代码上写出空指针,但程序运行起来时会产生空指针,这种动态的空指针异常最简单的解决办法就是对于所有的涉及到的对象都进行判空操作,但无疑这是一项很大的工程,Kotlin对此使用了简化操作。
以下介绍,Kotlin 判空辅助系统代替我们平常的判空语句进行辅助判空。

  • ?= 变量值允许为空
  • ?. 判空中止执行
  • ?: 判空执行后面语句

案例

/*重写父属性造成的空指针
* */
open class Student4{
 open var name:String="LANI"
  init {
    println("我是学生,${this.name},${this.name.length}")
  }
}
/*重写父属性造成的空指针
* */
class ComputerStudent:Student4(){
 override var name:String="LANI"
  init {
    println("我是计算机的学生,${this.name}")
  }
}

fun main(args: Array<String>) {
 var cs=ComputerStudent(
}

运行结果
在这里插入图片描述

?.操作符

这个操作符

的作用非常好理解,就是当对象不为空时正常调用相
应的方法,当对象为空时则什么都不做。比如以下的判空处理代码:

if (a != null) {
	a.doSomething()
}
123

使用判空操作符进行优化后如下:

a?.doSomething() 

?:操作符

这个操作符的左右两边都接收一个表达式,如果左边表达式的结果不为空就返回左边表达式的结果,否则就返回右边表达式的结果,如下代码:

val c = a ?: b
1

当a不为空时就返回a;当a为空时返回b。Kotlin中 的?:有点类似与C/C++中的三目运算符,只不过Kotlin中是判空,而C/C++中是判真假。

let函数

let函数提供了函数式API的编程接口,并将原始调用对象作为参数传递到Lambda表达式中。(关于这部分,在Lambda表达式中有解释)示例代码如下:

obj.let { obj2 ->
// 编写具体的业务逻辑
}

可以看到,这里调用了obj对象的let函数,然后Lambda表达式中的代码就会立即执行,并且这个obj对象本身还会作为参数传递到Lambda表达式中。不过,为了防止变量重名,这里我将参数名改成了obj2,但实际上它们是同一个对象,这就是let函数的作用。
let函数的特性配合 ?. 操作符可以在空指针检查的时候起到很大的作用。

我们先来看一段代码:

fun doStudy(study: Study?) {
	study?.readBooks()
	study?.doHomework()
}

这段代码在每次调用study对象时都会判空,但如果我们使用if语句时只需要对study对象判一次空即可。这时我们可以使用let函数?. 操作符配合进行优化:

fun doStudy(study: Study?) {
	study?.let { stu ->
		stu.readBooks()
		stu.doHomework()
	}
}

?.操作符表示对象为空时什么都不做,对象不为空时就调用let
函数,而let函数会将study对象本身作为参数传递到Lambda表达式中,此时的study对象肯定不为空了,我们就能放心地调用它的任意方法了。
以上代码经过Lambda表达式的优化步骤还可以优化如下:

fun doStudy(study: Study?) {
	study?.let {
		it.readBooks()
		it.doHomework()
	}
}

Tips:
let函数是可以处理全局变量的判空问题的,而if判断语句则无法做到这一点。比如我们将doStudy()函数中的参数变成一个全局变量,使用let函数仍然可以正常工作,但使用if判断语句则会提示错误,如下所示:

var study:Study? = null
fun doStudy(){
	if(study != null){
		study.readBooks()  //报错
		study.doHomework() //报错
	}
}

之所以这里会报错,是因为全局变量的值随时都有可能被其他线程所修改,即使做了判空处理,仍然无法保证if语句中的study变量没有空指针风险。从这一点上也能体现出let函数的优势。

字符串内嵌表达式

首先来看一下Kotlin中字符串内嵌表达式的语法规则:

"hello, ${obj.name}. nice to meet you!"

可以看到,Kotlin允许我们在字符串里嵌入${}这种语法结构的表达式,并在运行时使用表达式执行的结果替代这一部分内容。另外,当表达式中仅有一个变量的时候,还可以将两边的大括号省略,如下所示:
“hello, $name. nice to meet you!”

函数默认参数

我们可以在定义函数的时候给任意参数设定一个默认值这样当调用此函数时就不
会强制要求调用方为此参数传值,在没有传值的情况下会自动使用参数的默认值。
给参数设定默认值的方式也很简单,代码如下:

fun printParams(num: Int, str: String = "hello") {
	println("num is $num , str is $str")
}
123

可以看到,这里我们给printParams()函数的第二个参数设定了一个默认值,这样当调用
printParams()函数时,可以选择给第二个参数传值,也可以选择不传,在不传的情况下就会自动使用默认值。代码如下:

fun main() {
	printParams(123)
}

我们将代码改成给第一个参数设定默认值,如下所示:

fun printParams(num: Int = 100, str: String) {
	println("num is $num , str is $str")
}

此时,我们必须采用键值对的方法来进行传参调用,如下所示:

printParams(str = "world")

此时哪个参数在前哪个参数在后都无所谓,Kotlin可以准确地将参数匹配上。

  • 23
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值