三篇文章带你快速入门 Kotlin(上)
Kotlin的发展历程
2011年,JetBrains发布了Kotlin的第一个版本,并在2012年将其开源。
2016年Kotlin发布了1.0正式版,代表着Kotlin语言已经足够成熟和稳定了,并且JetBrains也在自家的旗舰IDE开发工具IntelliJ IDEA中加入了Kotlin的支持。
2017年Google宣布Kotlin正式成为Android开发一级语言,并且Android Studio也加入了对Kotlin的支持。
2019年Google正式宣布了Kotlin First,未来提供的官方API也将会以Kotlin版本为主。
Kotlin相比于Java的优势
语法更加简洁,对于同样的功能,使用Kotlin开发的代码量可能会比使用Java开发的减少50%甚至更多。
语法更加高级,相比于Java比较老旧的语法,Kotlin增加了很多现代高级语言的语法特性,使得开发效率大大提升。
语言更加安全,Kotlin几乎杜绝了空指针这个全球崩溃率最高的异常。
和Java是100%兼容的,Kotlin可以直接调用使用Java编写的代码,也可以无缝使用Java第三方的开源库。这使得Kotlin在加入了诸多新特性的同时,还继承了Java的全部财富。
Kotlin的工作原理
Kotlin可以做到和Java 100%兼容,这主要是得益于Java虚拟机的工作机制。
其实Java虚拟机并不会直接和你编写的Java代码打交道,而是和编译之后生成的class文件打交道。
而Kotlin也有一个自己的编译器,它可以将Kotlin代码也编译成同样规格的class文件。
Java虚拟机不会关心class文件是从Java编译来的,还是从Kotlin编译来的,只要是符合规格的class文件,它都能识别。
也正是这个原因,JetBrains才能以一个第三方公司的身份设计出一门用来开发Android应用程序的编程语言。
函数和变量
变量
Kotlin中定义一个变量,只允许在变量前声明两种关键字:val和var。
val(value的简写的简写)用来声明一个不可变的变量,这种变量在初始赋值之后就再也不能重新赋值,对应Java中的final变量。
-
var(variable的简写的简写)用来声明一个可变的变量,这种变量在初始赋值之后仍然可以再被重新赋值复制,对应Java中的非final变量。
fun main() { val a = 10 var b = 5 b = b + 3 println("a = " + a) println("b = " + b) }
内嵌表达式
在Kotlin中,我们可以直接将表达式写在字符串里面,即使是构建非常复杂的字符串,也会变得轻而易举。
Kotlin中字符串内嵌表达式的语法规则如下:
"hello, ${obj.name}. nice to meet you!"
当表达式中仅有一个变量的时候,还可以将两边的大括号省略:
"hello, $name. nice to meet you!"
函数
定义一个函数的语法规则如下:
fun methodName(param1: Int, param2: Int): Int {
return 0
}
当一个函数的函数体中只有一行代码时,可以使用单行代码函数的语法糖:
fun methodName(param1: Int, param2: Int) = 0
使用这种写法,可以直接将唯一的一行代码写在函数定义的尾部,中间用等号连接即可
return关键字也可以省略,等号足以表达返回值的意思。
Kotlin还拥有出色的类型推导机制,可以自动推导出返回值的类型。
逻辑控制
if条件语句
fun largerNumber4(num1: Int, num2: Int): Int {
val value = if (num1 > num2) {
num1
} else {
num2
}
return value
}
Kotlin中的if语句相比于Java有一个额外的功能:它是可以有返回值的,返回值就是if语句每一个条件中最后一行代码的返回值。
fun largerNumber(num1: Int, num2: Int): Int {
val value = if (num1 > num2) {
num1
} else {
num2
}
return value
}
仔细观察上述代码,你会发现value其实是一个多余的变量,我们可以直接将if语句返回,这样代码将会变得更加精简,如下所示:
fun largerNumber(num1: Int, num2: Int): Int {
return if (num1 > num2) {
num1
} else {
num2
}
}
当一个函数只有一行代码时,可以省略函数体部分,直接将这一行代码使用等号串连在函数定义的尾部。虽然largerNumber()函数不止只有一行代码,但是它和只有一行代码的作用是相同的,只是return了一下if语句的返回值而已,符合该语法糖的使用条件。那么我们就可以将代码进一步精简:
fun largerNumber(num1: Int, num2: Int) = if (num1 > num2) {
num1
} else {
num2
}
最后,还可以将上述代码再精简一下,直接压缩成一行代码:
fun largerNumber(num1: Int, num2: Int) = if (num1 > num2) num1 else num2
when条件语句
当需要判断的条件非常多的时候,可以考虑使用when语句来替代if语句。
类似与switch语句又远比switch语句强大的多
分支可以时表达式,也可以时具体数值。
fun getScore1(name: String) = when (name) {
"Tom" -> 86
"Jack" -> 77
"Jim" -> 33
"Lily" -> 44
else -> 0
}
高级用法:when不带参数写法,不常用,但是扩展性很强
fun getScore2(name: String) = when {
name == "Tom" -> 86
name.equals("Jack") -> 77
name.endsWith("Jim") -> 33
name.startsWith("Lily") -> 44
else -> 0
}
when不带参数,多参数
fun getScore3(name: String, name1: String) = when {
name == "Tom" -> 86
name.equals("Jack") -> 77
name1.endsWith("Jim") -> 33
name1.startsWith("Lily") -> 44
else -> 0
}
for-in循环语句
我们可以使用如下Kotlin代码来表示一个区间:
val range = 0..10
上述代码表示创建了一个0到10的区间,并且两端都是闭区间,这意味着0到10这两个端点都是包含在区间中的,用数学的方式表达出来就是[0, 10]。
也可以使用until关键字来创建一个左闭右开的区间:
val range = 0 until 10
如果你想跳过其中的一些元素,可以使用step关键字:
fun main() {
for (i in 0 until 10 step 2) {
println(i)
}
}
如果你想创建一个降序的区间,可以使用downTo关键字:
fun main() {
for (i in 10 downTo 1) {
println(i)
}
}
控制循环语句Break
退出当前循环
@ 标签可以指定退出外层循环
fun for(){
out@ for (i in 0..5) {
println("i$i")
for (j in 0..3) {
println("j$j")
if (j == 1) {
break@out
}
}
}
}
控制循环语句Continue
退出本次循环
fun for(){
out@ for (i in 0..5) {
println("i$i")
for (j in 0..3) {
println("j$j")
if (j == 1) {
continue@out
}
}
}
}
面向对象编程
类与对象
可以使用如下代码定义一个类,以及声明它所拥有的字段和函数:
class Person {
var name = ""
var age = 0
fun eat() {
println("name:" + name + " age:" + age)
}
}
然后使用如下代码创建对象,并对对象进行操作:
fun main() {
val p = Person()
p.name = "yx"
p.age = 66
p.eat()
}
继承
Kotlin中一个类默认是不可以被继承的,如果想要让一个类可以被继承,需要主动声明open关键字:
open class Person {
…
}
要让另一个类去继承Person类,则需要使用冒号关键字:
class Student : Person() {
var sno = ""
var grade = 0
}
现在Student类中就自动拥有了Person类中的字段和函数,还可以定义自己独有的字段和函数。
构造器
- 主构造器
constructor 关键字如果没有任何注解或修饰可以省略
如果没有为非抽象类定义任何主次构造器,系统会提供一个无参数的主构造器。默认修饰为public
一旦程序员为一个类提供了构造器,系统将不再为该类提供构造器
- 次构造器
任何一个类只能有一个主构造函数,但可以有多个次构造函数
如果定义了主构造器,那么次构造器必须委托主构造器
如果没有定义柱构造器则不用委托主构造器
class Student(val sno: String, val grade: Int) : Person() {
init {
println("sno:$sno")
println("grade:$grade")
}
}
init 关键字为初始化时回调的逻辑块
接口
Kotlin中定义接口的关键字和Java中是相同的,都是使用的interface:
interface Study {
fun readBooks()
fun doHomework()
}
而Kotlin中实现接口的关键字变量了冒号,和继承使用的是同样的关键字:
class Artist(var name: String) : Study {
override fun readBook() {
println("name:$name readBook")
}
override fun doHomework() {
println("name:$name doHomework")
}
}
数据类
Kotlin中使用data关键字可以定义一个数据类:
data class Cellphone(val brand: String, val price: Double)
Kotlin会根据数据类的主构造函数中的参数将equals()、hashCode()、toString()等固定且无实际逻辑意义的方法自动生成,从而大大简少了开发的工作量。
data class CellPhone(val brand: String, val price: Double)
class Cellphone1(val brand: String, val price: Double)
fun main() {
val str = "{'userCode':'demo','userName':'Demo Group'}"
val a = EncryptUtils.decryptBase64AES(
str.toByteArray(),
"PHA".toByteArray(),
"CBC",
"".toByteArray()
)
println(a)
val cellPhone1 = CellPhone("apple", 1000.0)
val cellPhone2 = CellPhone("apple", 1000.0)
println(cellPhone1)
println(cellPhone1 == cellPhone2)
val cellPhone3 = Cellphone1("apple", 1000.0)
val cellPhone4 = Cellphone1("apple", 1000.0)
println(cellPhone3)
println(cellPhone3 == cellPhone4)
}
单例类
Kotlin中使用object关键字可以定义一个单例类:
object Singleton {
fun singletonTest() {
println("singletonTest is called.")
}
}
而调用单例类中的函数比较类似于Java中静态方法的调用方式:
Singleton.singletonTest()
这种写法虽然看上去像是静态方法的调用,但其实Kotlin在背后自动帮我们创建了一个Singleton类的实例,并且保证全局只会存在一个Singleton实例。
集合
集合的创建
使用如下代码可以初始化一个List集合:
val list = listOf("Apple", "Banana", "Orange", "Pear", "Grape")
使用如下代码可以初始化一个Set集合:
val set = setOf("Apple", "Banana", "Orange", "Pear", "Grape")
使用如下代码可以初始化一个Map集合
val map = mapOf("Apple" to 1, "Banana" to 2, "Orange" to 3, "Pear" to 4, "Grape" to 5)
集合的取值
fun mutableList() {
val list = mutableListOf("apple", "banana", "orange", "pear", "grape")
list.add("other")
for (fruit in list) {
println(fruit)
}
}
SetOf集合,去重的集合,底层是map 所以可以去重
fun setList() {
val list = setOf("apple", "banana", "orange", "pear", "grape")
for (fruit in list) {
println(fruit)
}
}
map 集合 java写法:键值对一对一
fun mapList() {
val map = HashMap()
map.put("apple", 1)
map.put("banana", 2)
}
kotlin 可以用[]
fun mapListKotlin() {
val map = HashMap()
map["apple"] = 2
map["banana"] = 1
val number = map["apple"]
}
最简洁的写法 mapOf不可变,mutableMapOf可变
fun mapListKotlin1() {
val map = mapOf("apple" to 1, "banana" to 2, "orange" to 3)
val map1 = mutableMapOf("apple" to 1, "banana" to 2, "orange" to 3)
map1["watermelon"] = 6
for ((fruit, number) in map1) {
println("mutable" + fruit + number)
}
}
Lambda编程
Lambda就是一小段可以作为参数传递的代码。正常情况下,我们向某个函数传参时只能传入变量,而借助Lambda却允许传入一小段代码。
我们来看一下Lambda表达式的语法结构:
{参数名1: 参数类型, 参数名2: 参数类型 -> 函数体}
首先最外层是一对大括号,如果有参数传入到Lambda表达式中的话,我们还需要声明参数列表,参数列表的结尾使用一个->符号,表示参数列表的结束以及函数体的开始,函数体中可以编写任意行代码,并且最后一行代码会自动作为Lambda表达式的返回值。
集合的函数式API
集合中的map函数是最常用的一种函数式API,它用于将集合中的每个元素都映射成一个另外的值,映射的规则在Lambda表达式中指定,最终生成一个新的集合。比如,这里我们希望让所有的水果名都变成大写模式,就可以这样写:
fun main() {
val list = listOf("Apple", "Banana", "Orange", "Pear", "Grape", "Watermelon")
val newList = list.map({ fruit: String -> fruit.toUpperCase() })
for (fruit in newList) {
println(fruit)
}
}
- 当Lambda参数是函数的最后一个参数时,可以将Lambda表达式移到函数括号的外面。
- 如果Lambda参数是函数的唯一一个参数的话,还可以将函数的括号省略。
- 由于Kotlin拥有出色的类型推导机制,Lambda表达式中的参数列表其实在大多数情况下也不必声明参数类型。
- 当Lambda表达式的参数列表中只有一个参数时,也不必声明参数名,而是可以使用it关键字来代替。
因此,Lambda表达式的写法可以进一步简化成如下方式:
val newList = list.map { it.toUpperCase() }
推导过程
fun lambda1() {
val list = listOf("apple", "banana", "orange", "pear", "grape", "other")
val max = list.maxByOrNull { it.length }
println(max)
}
maxByOrNul实际上它一个普通函数,只不过它接受的是一个Lambda类型参数,每次集合遍历时把遍历的值作为参数传递给lambda表达式,工作原理就是找到最长值并返回
fun lambda2() {
val list = listOf("apple", "banana", "orange", "pear", "grape", "other")
val lambda = { fruit: String -> fruit.length }
val max = list.maxByOrNull(lambda)
println(max)
}
可直接将lambda表达式传入maxByOrNull函数中
fun lambda3() {
val list = listOf("apple", "banana", "orange", "pear", "grape", "other")
val max = list.maxByOrNull({ fruit: String -> fruit.length })
println(max)
}
Kotlin规定,当lambda参数是函数的最后一个参数时,可以将Lambda表达式移到函数括号外边
fun lambda4() {
val list = listOf("apple", "banana", "orange", "pear", "grape", "other")
val max = list.maxByOrNull() { fruit: String -> fruit.length }
println(max)
}
Kotlin规定,如果Lambda参数是函数的唯一一个参数的话,还可以讲函数的括号省略
fun lambda5() {
val list = listOf("apple", "banana", "orange", "pear", "grape", "other")
val max = list.maxByOrNull { fruit: String -> fruit.length }
println(max)
}
Kotlin有出色的类推导机制,大多数的情况下不必声明参数的类型
fun lambda6() {
val list = listOf("apple", "banana", "orange", "pear", "grape", "other")
val max = list.maxByOrNull { fruit -> fruit.length }
println(max)
}
Kotlin规定,当lambda表达式的参数列表中只有一个参数时,也不必声明参数名,而是可以使用it关键字来代替
fun lambda7() {
val list = listOf("apple", "banana", "orange", "pear", "grape", "other")
val max = list.maxByOrNull { it.length }
println(max)
}
其他集合函数API:
使用map函数转换为大写
fun lamdabMap() {
val list = listOf("apple", "banana", "orange", "pear", "grape", "other")
var newList = list.map { it.toUpperCase() }
}
filter函数过滤集合中的元素,这里注意调用顺序 先map 在filter 效率会低
fun lamdabMapFilter() {
val list = listOf("apple", "banana", "orange", "pear", "grape", "other")
var newList = list.filter { it.length <= 5 }.map { it.toUpperCase() }
}
any函数用于判断集合中至少存在一个元素满足条件
all函数判断集合中所有元素满足条件
这样是不是比每次循环方便多了
fun lamdabAnyAll() {
val list = listOf("apple", "banana", "orange", "pear", "grape", "other")
val any = list.any { it.length <= 5 }
val all = list.all { it.length <= 5 }
}
Java函数式API
如果我们在Kotlin代码中调用了一个Java方法,并且该方法接收一个Java单抽象方法接口参数,就可以使用函数式API。Java单抽象方法接口指的是接口中只有一个待实现方法,如果接口中有多个待实现方法,则无法使用函数式API。
举个例子,Android中有一个极为常用的点击事件接口OnClickListener,其定义如下:
public interface OnClickListener {
void onClick(View v);
}
可以看到,这是一个单抽象方法接口。假设现在我们拥有一个按钮button的实例,就可以使用函数式API的写法来注册这个按钮的点击事件:
button.setOnClickListener { v ->
}
推导过程
fun lmClass() {
Thread(object : Runnable {
override fun run() {
println("Thead is run")
}
}).start()
}
Runnable只有一个待实现方法,这里没有显示地重写run方法kotlin自动明白runnable后面的lambda表达式就是在run中的实现内容
fun lmClass1() {
Thread(Runnable {
println("Thead is run")
}).start()
}
java方法的参数列表不存在一个以上java单抽象方法接口参数,还可以将接口名省略
fun lmClass2() {
Thread({
println("Thead is run")
}).start()
}
当lambda表达式是方法的最后一个参数时,可以将lambda表达式移到方法括号外面
fun lmClass3() {
Thread() {
println("Thead is run")
}.start()
}
同时lambda表大会还是方法的唯一一个参数,还可以方法的括号省略
fun lmClass4() {
Thread {
println("Thead is run")
}.start()
}
空指针检查
空指针是一种不受编程语言检查的运行时异常,只能由程序员主动通过逻辑判断来避免,但即使是最出色的程序员,也不可能将所有潜在的空指针异常全部考虑到。
public void doStudy(Study study) {
study.readBooks();
study.doHomework();
}
这段Java代码安全吗?不一定,因为这要取决于调用方传入的参数是什么,如果我们向doStudy()方法传入了一个null参数,那么毫无疑问这里就会发生空指针异常。因此,更加稳妥的做法是在调用参数的方法之前先进行一个判空处理,如下所示:
public void doStudy(Study study) {
if (study != null) {
study.readBooks();
study.doHomework();
}
}
可空类型系统
Kotlin中引入了一个可空类型系统的概念,它利用编译时判空检查的机制几乎杜绝了空指针异常。
fun doStudy(study: Study) {
study.readBooks()
study.doHomework()
}
这段代码看上去和刚才的Java版本并没有什么区别,但实际上它是没有空指针风险的,因为Kotlin默认所有的参数和变量都不可为空,所以这里传入的Study参数也一定不会为空,可以放心地调用它的任何函数。
Kotlin提供了另外一套可为空的类型系统,就是在类名的后面加上一个问号。比如,Int表示不可为空的整型,而Int?就表示可为空的整型;String表示不可为空的字符串,而String?就表示可为空的字符串。
使用可为空的类型系统时,需要在编译时期就把所有的空指针异常都处理掉才行。
判空辅助工具
Kotlin提供了一系列的辅助工具,使开发者能够更轻松地进行判空处理。
?. 操作符表示当对象不为空时正常调用相应的方法,当对象为空时则什么都不做。比如:
if (a != null) {
a.doSomething()
}
这段代码使用?.操作符就可以简化成:
a?.doSomething()
?: 操作符表示如果左边表达式的结果不为空就返回左边表达式的结果,否则就返回右边表达式的结果。比如:
val c = if (a ! = null) {
a
} else {
b
}
这段代码的逻辑使用?:操作符就可以简化成:
val c = a ?: b
kotlin并非总是那么智能如下
我们发现一个可空的全局变量,在contextNull已经判断为空却报错kotlin识别不了
!!:我们使用强制判断不为空!!非空断言工具
var content: String? = "hello"
fun contextNull() {
if (content != null) {
upCase()
}
}
fun upCase() {
val upcase = content!!.toUpperCase()
println(upcase)
}
结合使用?.操作符和let函数也可以对多次重复调用的某个变量统一进行判空处理
fun doStudy(study: Study?) {
study?.let {
it.readBooks()
it.doHomework()
}
}
取文本长度的函数
fun getTextLength(text: String?): Int {
if (text != null) {
return text.length
}
return 0
}
简写为:由于text可为空?.判断返回null借助?:返回0
fun getTextLength1(text: String?) = text?.length ?: 0
函数默认值
Kotlin允许在定义函数的时候给任意参数设定一个默认值,这样当调用此函数时就不会强制要求调用方为此参数传值,在没有传值的情况下会自动使用参数的默认值。语法格式如下:
fun printParams(num: Int, str: String = "hello") {
println("num is $num , str is $str")
}
这里给printParams()函数的第二个参数设定了一个默认值,这样当调用printParams()函数时,可以选择给第二个参数传值,也可以选择不传,在不传的情况下就会自动使用默认值。
示例:
fun params(num: Int, str: String = "Hello") {
println("num $num str $str")
}
fun params1(num: Int = 100, str: String) {
println("num $num str $str")
}
fun params2(num: Int = 100, str: String = "Hello") {
println("num $num str $str")
}
给构造函数传入默认入参,可以默认不传入参数
class Student(val sno: String = "", val grade: Int = 0, name: String = "", age: Int = 0) :
Person3(name, age) {
init {
println("sno:$sno")
println("grade:$grade")
}
}
fun main() {
params(123)
//params1(123)
params1(str = "wordla")
params2(str = "11", num = 55)
val s = Student()
}