Kotlin使用心得
一、Kotlin出生背景
2017年,甲骨文对谷歌所谓的安卓侵权使用Java提起诉讼,要求后者赔偿高达90亿美元。于是谷歌在后面的I/O大会上宣布了新决定:Kotlin语言正式成为安卓开发的一级编程语言。资料显示,Kotlin由JetBrains公司开发,于2010年首次推出,次年开源。它与Java 100%互通,并具备诸多Java尚不支持的新特性。Kotlin作为一门高度与Java兼容、并且简洁开发语言,对于我这种后端开发者也有很大的吸引力。
二、Kotlin具有哪些优势
- 因为Kotlin是基于JVM开发的,所以它同时具备了Android 开发、Web浏览器开发、原生Native开发的能力。在Web开发方面,Kotlin可以结合Spring框架使用(这为我们当前业务项目使用Kotlin语言开发提供了条件),也可以编译生成JavaScript模块,方便在一些JavaScript的虚拟机上编译运行。
- Kotlin能够和Java达到100%互通,也就是说,使用Kotlin依旧可以调用 Java已有的代码或库,也可以同时使用Java和Kotlin来混合编写代码。同时,为了方便项目的过渡,JetBrains提供的开发工具可以很简单的实现Java代码到Kotlin的转换。
- 在使用Java编程的过程中,大家聊得最多的话题莫过于如何避免空指针异常(NullPointerException)。针对空指针问题,Kotlin有专门的语法来避免空指针问题。
- Kotlin语法简洁直观,看上去非常像Scala,但更简单易学。同时,Kotlin使用了大量的语法糖,使得代码更加简洁。Kotlin并不遵循特定的编程规范,它借鉴了函数式风格和面向对象风格的诸多优点。
- 使用Kotlin编程,开发人员不必为每个变量明确指定类型,编译器可以在编译的时候推导出某个参数的数据类型,从而使得代码更为简洁。
- 作为JetBrains旗下的产品,JetBrains旗下众多的IDE可以为Kotlin开发提供无缝支持,并相互协作,协同发展。
三、Kotlin与Java的异同
- main方法的差异:猜测很多java程序员在入门的时候写的第一个程序是打印“Hello World”,其写法如下:
如上图,java的main方法只能写在Java类之中。但是换成Kotlin,却可以这样写:
可以发现Kotlin的main方法可以写在类里面,也可以写在类外面。当写在类里面的时候,需要嵌套一层companion object { 静态代码块 },companion object相当于Java中的static。
- Kotlin参数的格式。通过上面的main方法可以看出来,Kotlin的入参的写法是field: Field,而传统的Java写法是Field field。相对于入参,出参的变化更大一些,如下:
fun getSumVal(a: Int, b: Int): Int {
return a + b
}
fun printSumVal1(a: Int, b: Int): Unit {
println(a + b)
}
fun printSumVal2(a: Int, b: Int) {
println(a + b)
}
出参的位置是在入参的右边,格式为:(入参):出参类型。如果没有出参该怎么表示呢。Kotlin中摒除了void关键字,取而代之的是Unit,并且Unit可以省略。
- 新增不可变关键字val和可变关键字var。
fun testVal() {
val a: Int = 1 val b = 2 println("a = $a, b = $b")
var c: Int = 6 c = 8 println("c = $c")
}
如果一个变量被val修饰,那么该变量再被初始化后不能再被修改。如果希望被修改,那么将val关键字改为var即可。
上面的代码中使用到了 Kotlin的字符串模板的功能:字符串字面值可以包含模板表达式 ,即一些小段代码,会求值并把结果合并到字符串中。 模板表达式以美元符($
)开头,由一个简单的名字构成。
val s = "abc"
println("$s.length is ${s.length}") // 输出“abc.length is 3”
- 空值与null检测。
fun testNull() {
var a: Int = 1 var b: Int? = null var c: Int? = getNextVal()
a.toLong()
b?.toLong()
c?.toLong()
}
fun getNextVal(): Int? {
return null
}
a可以确定是非空的,可以直接转成Long类型。但是b和c都是null,编译器可以检测到这两个变量有可能是非空的,于是这两个参数类型后面必须要加问号(?)。当将b和c转为Long类型时,需要也需要在变量名后面加个?,这样便可以避免空指针异常。
- 数组、集合、Map等使用更方便。
对于java,如果要实例化一个数组,可以这么写:
String[] stringArray = new String[]{"red", "white", "blue"};
而Kotlin的写法是:
val array = arrayOf("red", "white", "blue")
可以看出来Kotlin的写法更简洁。
Kotlin的集合和Map的写法更简单。如下:
val list = listOf("red", "white", "blue")
val map = mapOf("color1" to "red", "color2" to "white", "color3" to "blue")
同样遍历集合和Map的方式也很方便。
遍历list:
for (item in list) {
println(item)
}
遍历Map:
for (item in map) {
println(item.key + item.value)
}
- Kotlin的when语句取代Java的Switch语句,如下:
val color = "blue"when (color) {
"red" -> print("color == red")
"white" -> print("color == white")
else -> {
print("color is neither red nor white")
}
}
- Kotlin不支持三元表达式,不过支持If not null and else 缩写,效果和三元表达式差不多,如下:
val array = arrayOf("red", "white", null)
val e = array[2]
println(e?.length ?: 0)
打印出已知数组中第三个元素的长度,比三元表达式还简洁。
- Kotlin类的构造器和实例化
Kotlin 中使用关键字 class 声明类,如:class Invoice { /*……*/ }。类声明由类名、类头(指定其类型参数、主构造函数等)以及由花括号包围的类体构成。类头与类体都是可选的; 如果一个类没有类体,可以省略花括号,如:class Empty。
在 Kotlin 中的一个类可以有一个主构造函数以及一个或多个次构造函数。主构造函数是类头的一部分:它跟在类名(与可选的类型参数)后。如果主构造函数没有任何注解或者可见性修饰符,可以省略这个 constructor 关键字,如下:class Person(firstName: String) { /*……*/ }。主构造函数不能包含任何的代码。初始化的代码可以放到以 init 关键字作为前缀的初始化块(initializer blocks)中。
类也可以声明前缀有 constructor的次构造函数。如果类有一个主构造函数,每个次构造函数需要委托给主构造函数, 可以直接委托或者通过别的次构造函数间接委托。委托到同一个类的另一个构造函数用 this 关键字即可:
class Person(val name: String) {
var children: MutableList<Person> = mutableListOf()
constructor(name: String, parent: Person) : this(name) {
parent.children.add(this)
}
}
如果一个非抽象类没有声明任何(主或次)构造函数,它会有一个生成的不带参数的主构造函数。构造函数的可见性是 public。如果你不希望你的类有一个公有构造函数,你需要声明一个带有非默认可见性的空的主构造函数:
class DontCreateMe private constructor () { /*……*/ }
注意 Kotlin 并没有 new 关键字。要创建一个类的实例,我们就像普通函数一样调用构造函数:
val invoice = Invoice()
val customer = Customer("Joe Smith")
- Kotlin 与 Java之间的 互操作性。
可以在 Kotlin 中自然地调用现存的 Java 代码,并且在 Java 代码中也可以很顺利地调用 Kotlin 代码。比如Java中有一些原生类型:byte, short, int, long, char, float, double, boolean。Kotlin 特殊处理一部分 Java 类型。这样的类型不是“按原样”从 Java 加载,而是 映射 到相应的 Kotlin 类型。 映射只发生在编译期间,运行时表示保持不变。 Java 的原生类型映射到相应的 Kotlin 类型(请记住平台类型):
Java 类型 | Kotlin 类型 |
byte |
kotlin.Byte |
short |
kotlin.Short |
int |
kotlin.Int |
long |
kotlin.Long |
char |
kotlin.Char |
float |
kotlin.Float |
double |
kotlin.Double |
boolean |
kotlin.Boolean |
一些非原生的内置类型也会作映射:
Java 类型 | Kotlin 类型 |
java.lang.Object |
kotlin.Any! |
java.lang.Cloneable |
kotlin.Cloneable! |
java.lang.Comparable |
kotlin.Comparable! |
java.lang.Enum |
kotlin.Enum! |
java.lang.Annotation |
kotlin.Annotation! |
java.lang.CharSequence |
kotlin.CharSequence! |
java.lang.String |
kotlin.String! |
java.lang.Number |
kotlin.Number! |
java.lang.Throwable |
kotlin.Throwable! |
Java 的装箱原始类型映射到可空的 Kotlin 类型:
Java type | Kotlin type |
java.lang.Byte |
kotlin.Byte? |
java.lang.Short |
kotlin.Short? |
java.lang.Integer |
kotlin.Int? |
java.lang.Long |
kotlin.Long? |
java.lang.Character |
kotlin.Char? |
java.lang.Float |
kotlin.Float? |
java.lang.Double |
kotlin.Double? |
java.lang.Boolean |
kotlin.Boolean? |
请注意,用作类型参数的装箱原始类型映射到平台类型: 例如,List<java.lang.Integer>
在 Kotlin 中会成为 List<Int!>
。
集合类型在 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>! |
Java 的数组按下文所述映射:
Java 类型 | Kotlin 类型 |
int[] |
kotlin.IntArray! |
String[] |
kotlin.Array<(out) String>! |
注意:这些 Java 类型的静态成员不能在相应 Kotlin 类型的伴生对象中直接访问。要调用它们,请使用 Java 类型的完整限定名,例如 java.lang.Integer.toHexString(foo)
。
- Kotlin中的类型检测与类型转换:“is”与“as”
我们可以在运行时通过使用 is
操作符或其否定形式 !is
来检测对象是否符合给定类型。在许多情况下,不需要在 Kotlin 中使用显式转换操作符,因为编译器跟踪不可变值的 is
-检测以及显式转换,并在需要时自动插入(安全的)转换:
fun demo(x: Any) {
if (x is String) {
print(x.length) // x 自动转换为字符串
}
}
“is”是安全的转换操作符,那么“as”就是非安全的转换操作符。通常,如果转换是不可能的,转换操作符会抛出一个异常。因此,我们称之为不安全的。 Kotlin 中的不安全转换由中缀操作符 as完成:
val x: String = y as String
请注意,null 不能转换为 String
因该类型不是可空的, 即如果 y
为空,上面的代码会抛出一个异常。 为了让这样的代码用于可空值,请在类型转换的右侧使用可空类型:
val x: String? = y as String?
为了避免抛出异常,可以使用安全转换操作符 as?,它可以在失败时返回 null:
val x: String? = y as? String
请注意,尽管事实上 as? 的右边是一个非空类型的 String
,但是其转换的结果是可空的。
- 相等性差异
在java中,如果我们想比较两个对象的结构是否相同,会采用equal方法,如果想知道两个对象是否是同一个对象,那就需要用==来比较两个对象的引用。
这一点Kotlin则不大相同。Kotlin会使用==来比较两个对象的结构是否相同,比如像 a == b
这样的表达式会翻译成:a?.equals(b) ?: (b === null),也就是说如果 a
不是 null
则调用 equals(Any?)
函数,否则(即 a
是 null
)检测 b 是否与 null
引用相等。Kotlin中引用相等由 ===
(以及其否定形式 !==
)操作判断。a === b
当且仅当 a
与 b
指向同一个对象时求值为 true。对于运行时表示为原生类型的值 (例如 Int
),===
相等检测等价于 ==
检测。
四、Kotlin异步编程框架协程
- 什么是协程,协程的开发人员 Roman Elizarov 是这样描述协程的:协程就像非常轻量级的线程。线程是由系统调度的,线程切换或线程阻塞的开销都比较大。而协程依赖于线程,但是协程挂起时不需要阻塞线程,几乎是无代价的,协程是由开发者控制的。所以协程也像用户态的线程,非常轻量级,一个线程中可以创建任意个协程。从上面的描述可以看出来,其最大优点是省去了传统 Thread 多线程并发机制中切换线程时带来的线程上下文切换、线程状态切换、Thread 初始化上的性能损耗,能大幅度唐提高并发性能。缺点是本质是个单线程,不能利用到单个CPU的多个核。
- 线程和协程的对比:
线程拥有独立的栈、局部变量,基于进程的共享内存,因此数据共享比较容易,但是多线程时需要加锁来进行访问控制,不加锁就容易导致数据错误,但加锁过多又容易出现死锁。线程之间的调度由内核控制(时间片竞争机制),程序员无法介入控制(即便我们拥有sleep、yield这样的API,这些API只是看起来像,但本质还是交给内核去控制,我们最多就是加上几个条件控制罢了
),线程之间的切换需要深入到内核级别,因此线程的切换代价比较大,表现在:
* 线程对象的创建和初始化
* 线程上下文切换
* 线程状态的切换由系统内核完成
* 对变量的操作需要加锁
协程是跑在线程上的优化产物,被称为轻量级 Thread,拥有自己的栈内存和局部变量,共享成员变量。传统 Thread 执行的核心是一个while(true) 的函数,本质就是一个耗时函数,Coroutine 可以用来直接标记方法,由程序员自己实现切换,调度,不再采用传统的时间段竞争机制。在一个线程上可以同时跑多个协程,同一时间只有一个协程被执行,在单线程上模拟多线程并发,协程何时运行,何时暂停,都是有程序员自己决定的,使用: yield/resume
API,优势如下:
- 因为在同一个线程里,协程之间的切换不涉及线程上下文的切换和线程状态的改变,不存在资源、数据并发,所以不用加锁,只需要判断状态就OK,所以执行效率比多线程高很多
- 协程是非阻塞式的(也有阻塞API),一个协程在进入阻塞后不会阻塞当前线程,当前线程会去执行其他协程任务
程序员能够控制协程的切换,是通过yield
API 让协程在空闲时(比如等待io,网络数据未到达)放弃执行权,然后在合适的时机再通过resume
API 唤醒协程继续运行。协程一旦开始运行就不会结束,直到遇到yield
交出执行权。Yield
、resume
这一对 API 可以非常便捷的实现异步
,这可是目前所有高级语法孜孜不倦追求的。
- 协程的三种启动方式
第一种启动方式:runBlocking:T
runBlocking 方法用于启动一个协程任务,通常只用于启动最外层的协程,例如线程环境切换到协程环境。runBlocking启动的协程任务会阻断当前线程,直到该协程执行结束。如:
fun main() = runBlocking<Unit> { // 开始执行主协程
GlobalScope.launch { // 在后台启动一个新的协程并继续
delay(1000L)
println("World!")
}
println("Hello,") // 主协程在这里会立即执行
delay(2000L) // 延迟 2 秒来保证 JVM 存活
}
第二种启动方式:launch:Job
我们最常用的用于启动协程的方式,它最终返回一个Job类型的对象,这个Job类型的对象实际上是一个接口,它包涵了许多我们常用的方法。例如join()启动一个协程、cancel() 取消一个协程。该方式启动的协程任务是不会阻塞线程的。
val job = GlobalScope.launch { // 启动一个新协程并保持对这个作业的引用
delay(1000L)
println("World!")
}
println("Hello,")
job.join() // 等待直到子协程执行结束
第三种启动方式:async/await:Deferred
1.async和await是两个函数,这两个函数在我们使用过程中一般都是成对出现的。
2.async用于启动一个异步的协程任务,await用于去得到协程任务结束时返回的结果,结果是通过一个Deferred对象返回的。
以上就是kotlin中协程的简单介绍,当然还有更多的特性,等待我们去深挖。
五、Kotlin结合Springboot项目实践
目前Springboot在线生成项目的功能已经支持了Kotlin语言版本,如下:
我们可以快速生成整合springboot+kotlin的项目。查看pom文件,多了2个Kotlin相关依赖:
启动类如下:
和普通的springboot+java项目一样傻瓜化,然后我们就可以开发具体的业务功能了。
上一篇: 人要靠自己