`

Kotlin使用心得

 
阅读更多

一、Kotlin出生背景

2017年,甲骨文对谷歌所谓的安卓侵权使用Java提起诉讼,要求后者赔偿高达90亿美元。于是谷歌在后面的I/O大会上宣布了新决定:Kotlin语言正式成为安卓开发的一级编程语言。资料显示,Kotlin由JetBrains公司开发,于2010年首次推出,次年开源。它与Java 100%互通,并具备诸多Java尚不支持的新特性。Kotlin作为一门高度与Java兼容、并且简洁开发语言,对于我这种后端开发者也有很大的吸引力。

二、Kotlin具有哪些优势

  1. 因为Kotlin是基于JVM开发的,所以它同时具备了Android 开发、Web浏览器开发、原生Native开发的能力。在Web开发方面,Kotlin可以结合Spring框架使用(这为我们当前业务项目使用Kotlin语言开发提供了条件),也可以编译生成JavaScript模块,方便在一些JavaScript的虚拟机上编译运行。
  2. Kotlin能够和Java达到100%互通,也就是说,使用Kotlin依旧可以调用 Java已有的代码或库,也可以同时使用Java和Kotlin来混合编写代码。同时,为了方便项目的过渡,JetBrains提供的开发工具可以很简单的实现Java代码到Kotlin的转换。
  3. 在使用Java编程的过程中,大家聊得最多的话题莫过于如何避免空指针异常(NullPointerException)。针对空指针问题,Kotlin有专门的语法来避免空指针问题。
  4. Kotlin语法简洁直观,看上去非常像Scala,但更简单易学。同时,Kotlin使用了大量的语法糖,使得代码更加简洁。Kotlin并不遵循特定的编程规范,它借鉴了函数式风格和面向对象风格的诸多优点。
  5. 使用Kotlin编程,开发人员不必为每个变量明确指定类型,编译器可以在编译的时候推导出某个参数的数据类型,从而使得代码更为简洁。
  6. 作为JetBrains旗下的产品,JetBrains旗下众多的IDE可以为Kotlin开发提供无缝支持,并相互协作,协同发展。

三、Kotlin与Java的异同

  1. main方法的差异:猜测很多java程序员在入门的时候写的第一个程序是打印“Hello World”,其写法如下:

image

如上图,java的main方法只能写在Java类之中。但是换成Kotlin,却可以这样写:

image

可以发现Kotlin的main方法可以写在类里面,也可以写在类外面。当写在类里面的时候,需要嵌套一层companion object { 静态代码块 },companion object相当于Java中的static。

 

  1. 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可以省略。

 

  1. 新增不可变关键字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”

 

  1. 空值与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类型时,需要也需要在变量名后面加个?,这样便可以避免空指针异常。

 

  1. 数组、集合、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)

}

 

  1. 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")

    }

}

 

  1. Kotlin不支持三元表达式,不过支持If not null and else 缩写,效果和三元表达式差不多,如下:

val array = arrayOf("red", "white", null)

val e = array[2]

println(e?.length ?: 0)

打印出已知数组中第三个元素的长度,比三元表达式还简洁。

 

  1. 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")

 

  1. 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)

 

  1. 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,但是其转换的结果是可空的。

 

  1. 相等性差异

在java中,如果我们想比较两个对象的结构是否相同,会采用equal方法,如果想知道两个对象是否是同一个对象,那就需要用==来比较两个对象的引用。

这一点Kotlin则不大相同。Kotlin会使用==来比较两个对象的结构是否相同,比如像 a == b 这样的表达式会翻译成:a?.equals(b) ?: (b === null),也就是说如果 a 不是 null 则调用 equals(Any?) 函数,否则(即 anull)检测 b 是否与 null 引用相等。Kotlin中引用相等由 ===(以及其否定形式 !==)操作判断。a === b 当且仅当 a 与 b 指向同一个对象时求值为 true。对于运行时表示为原生类型的值 (例如 Int),=== 相等检测等价于 == 检测。

 

四、Kotlin异步编程框架协程

  1. 什么是协程,协程的开发人员 Roman Elizarov 是这样描述协程的:协程就像非常轻量级的线程。线程是由系统调度的,线程切换或线程阻塞的开销都比较大。而协程依赖于线程,但是协程挂起时不需要阻塞线程,几乎是无代价的,协程是由开发者控制的。所以协程也像用户态的线程,非常轻量级,一个线程中可以创建任意个协程。从上面的描述可以看出来,其最大优点是省去了传统 Thread 多线程并发机制中切换线程时带来的线程上下文切换、线程状态切换、Thread 初始化上的性能损耗,能大幅度唐提高并发性能。缺点是本质是个单线程,不能利用到单个CPU的多个核。
  2. 线程和协程的对比:

线程拥有独立的栈、局部变量,基于进程的共享内存,因此数据共享比较容易,但是多线程时需要加锁来进行访问控制,不加锁就容易导致数据错误,但加锁过多又容易出现死锁。线程之间的调度由内核控制(时间片竞争机制),程序员无法介入控制(即便我们拥有sleep、yield这样的API,这些API只是看起来像,但本质还是交给内核去控制,我们最多就是加上几个条件控制罢了),线程之间的切换需要深入到内核级别,因此线程的切换代价比较大,表现在:

* 线程对象的创建和初始化

* 线程上下文切换

* 线程状态的切换由系统内核完成

* 对变量的操作需要加锁

 

image.png

协程是跑在线程上的优化产物,被称为轻量级 Thread,拥有自己的栈内存和局部变量,共享成员变量。传统 Thread 执行的核心是一个while(true) 的函数,本质就是一个耗时函数,Coroutine 可以用来直接标记方法,由程序员自己实现切换,调度,不再采用传统的时间段竞争机制。在一个线程上可以同时跑多个协程,同一时间只有一个协程被执行,在单线程上模拟多线程并发,协程何时运行,何时暂停,都是有程序员自己决定的,使用: yield/resume API,优势如下:

  • 因为在同一个线程里,协程之间的切换不涉及线程上下文的切换和线程状态的改变,不存在资源、数据并发,所以不用加锁,只需要判断状态就OK,所以执行效率比多线程高很多
  • 协程是非阻塞式的(也有阻塞API),一个协程在进入阻塞后不会阻塞当前线程,当前线程会去执行其他协程任务

image.png

程序员能够控制协程的切换,是通过yield API 让协程在空闲时(比如等待io,网络数据未到达)放弃执行权,然后在合适的时机再通过resume API 唤醒协程继续运行。协程一旦开始运行就不会结束,直到遇到yield交出执行权。Yieldresume 这一对 API 可以非常便捷的实现异步,这可是目前所有高级语法孜孜不倦追求的。

 

  1. 协程的三种启动方式

第一种启动方式: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语言版本,如下:

image.png

我们可以快速生成整合springboot+kotlin的项目。查看pom文件,多了2个Kotlin相关依赖:

image.png

启动类如下:

image.png

和普通的springboot+java项目一样傻瓜化,然后我们就可以开发具体的业务功能了。

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics