欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Kotlin使用心得  

程序员文章站 2022-07-03 09:39:02
...

一、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”,其写法如下:

Kotlin使用心得
            
    
    
         

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

Kotlin使用心得
            
    
    
         

可以发现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只是看起来像,但本质还是交给内核去控制,我们最多就是加上几个条件控制罢了),线程之间的切换需要深入到内核级别,因此线程的切换代价比较大,表现在:

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

* 线程上下文切换

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

* 对变量的操作需要加锁

 

Kotlin使用心得
            
    
    
         

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

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

Kotlin使用心得
            
    
    
         

程序员能够控制协程的切换,是通过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语言版本,如下:

Kotlin使用心得
            
    
    
         

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

Kotlin使用心得
            
    
    
         

启动类如下:

Kotlin使用心得
            
    
    
         

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