阅读 1434

9012年,铁汁你为什么还不上手Kotlin?

What is Kotlin?

科特林岛(Котлин)是一座俄罗斯的岛屿,位于圣彼得堡以西约30公里处,形状狭长,东西长度约14公里,南北宽度约2公里,面积有16平方公里,扼守俄国进入芬兰湾的水道。科特林岛上建有喀琅施塔得市,为圣彼得堡下辖的城市。

Kotlin是一门以kotlin岛名命名的现代化编程语言。它是一种针对Java平台的静态类型的新编程语言。专注于与Java代码的互操作性,几乎可以应用于现今使用Java的任何地方:服务端开发、Android应用等等。Kotlin可以很好的和所有现存的java库和框架一起工作,且性能与Java旗鼓相当。

谷歌开发者社区做过一个问卷调查,大概有40%的Android开发者已使用过Kotlin。


Kotlin简史

  • 2011年7月,JetBrains推出Kotlin项目。
  • 2012年2月,JetBrains以Apache 2许可证开源此项目。
  • 2016年2月15日,Kotlin v1.0(第一个官方稳定版本)发布。
  • 2017 Google I/O 大会,Kotlin “转正”。
  • 现在 Kotlin 已发布至 V1.3。 kotlinlang.org/

1. Kotlin的主要特征

  • 目标平台:服务端、Android及任何Java运行的地方。
  • 静态类型。
  • 函数式编程与面向对象
  • 免费且开源

1.1 静态类型

Kotlin与Java一样是一门静态类型编程语言,在编译期就已经确定所有表达式的类型。但是与Java不同的一点是Kotlin不需要在源码中显示的每个声明变量的类型,其所拥有的 类型推导 特性使编译器通过上下文推断变量的类型。 如下

var xOld: Int = 1 
var x = 1
var yOld: String = "string"
var y = "string"
复制代码

1.2 函数式编程与面向对象

在讨论Kotlin中的函数式编程前,我们先了解一下函数式编程的核心概念:

  1. 头等函数:把函数当作值来使用和传递,可以使用变量保存它,也可以将其作为其他函数的参数来进行传递,或者将其作为函数的返回值。
  2. 不可变性:使用不可变对象,保证该对象的状态在创建之后不会再变化。
  3. 无副作用:使用纯函数,此类函数在输入相同参数时会输出同样的结果,且不会改变其他变量的状态,也不会与外界有任何交互。

由于Kotlin的首要目标是提供一种更简介、高效、安全的替代java的语言,所以其拥有java的面向对象特性。同时其丰富的特性集也让其支持函数式编程的风格。主要特性如下:

  • 函数类型:允许函数接受其他函数作为参数,或者返回其他函数。
  • lambad表达式,使用最少的样板代码传递代码块,同时节省性能开销。
  • 数据类,提供了创建不可变值对象的简明语法。
  • 标准库提供了丰富的API集合,使你可以用函数式编程风格操作对象和集合。

示例代码:

fun main(args: Array<String>) {
    var hello: () -> Unit = { print("Hello world") }
    test(hello)
}

fun test(f: () -> Unit) {
    f.invoke()
}

>>>
Hello world
复制代码
fun main(args: Array<String>) {
    var square: (Int) -> Int = { it * it }
    test(10, square)
}

fun test(i: Int, f: (Int) -> Int) {
    print(f(i))
}

>>>
100
复制代码
//数据类
data class User(val name:String,val age:Int)
复制代码

1.3 免费且开源

Kotlin完全开源,可以自由使用,采用Apache2许可证;开发过程完全公开在Github上。

推荐的开发工具:IntelliJ IDEA、Android Studio、Eclipse。

2. Kotlin的设计哲学

务实

Kotlin 不是一门哲学性、研究性语言而是一门实用语言,它的的诞生是为了解决当前编程世界的许多问题,它的特性也是依据与许多开发者遇到的场景而选择的。Kotlin开发团队对于Kotlin能帮助解决实际项目问题的特性很有自信。

Kotlin也没有强制使用某种特定的编程风格和范式。由于其设计者是JetBrains,Kotlin的开发工具IntelliJ IDEA的插件和编译器乃是同步开发,且在设计语言特性时就考虑到了对工具的支持,所以毋庸置疑,Kotlin的开发工具对于开发者来说是及其有好的。

在我们编写Kotlin过程中,良好的IDE会发现那些可以用更简洁的结构来替换的通用代码模式,我们同时也可以通过研究IDE使用的语言特性,将其应用到自己的代码中。

简洁

Java中常见的代码在gettersetter以及将构造函数的参数赋值给变量的操作都被设置为隐式的。

var name:String = ""
var name: String
    get() {
        return name
    }
    set(value) {
        name = value
    }
复制代码
class Person(name: String){
    init {
        print(name)
    }
}

class Student{
    constructor(name: String){
        print(name)
    }
}
复制代码

Kotlin丰富的标准库也可以代替很多不必要的冗长的代码。

例:如下所示的list,需求为:输出其中第一个包含字母 b 的item,我们来看看通过Java和Kotlin的分别是如何实现的。

List<String> list = new ArrayList<>();
        list.add("a");
        ...
        list.add("a");
        list.add("ab");
复制代码

Java

 for (String s : list) {
            if (s.contains("b")) {
                System.out.print(s);
                break;
            }
        }
复制代码

Kotlin

 print(list.find { it.contains("b")})
复制代码

仅需一行代码就轻松搞定了。

还有一个细节需要提醒大家,与很多现代语言一样,Kotlin中 没有;号。

越简洁的代码写起来花的时间越短,更重要的是,读起来耗费的时间更短,便于提高你的生产力, 使你更快的达到目标。

安全

通常,我们声称一门语言是安全的,安全的含义是它的设计可以防止程序出现某些类型的错误。Kotlin借鉴于Java,提供了一些设计,使其使用起来更加安全。

  • 在Kotlin中,不必要去指定所有类型声明,编译器会自动推断出来,也降低了程序运行时的出错几率。
  • Kotlin中定义了可空符号?,将其用来定义变量是否为 null
   val s: String= ""  //不可为空
   val s1: String? = null  //可为空
复制代码
  • 有效避免ClassCastException。在Java中,我们把一个对象转换成某一个类型时,需要先对其进行类型检查,检查成功后再将其转换成该类型。一旦忘了检查操作,就有可能会发生上述异常。在Kotlin中,检查和转换被融合成了一个操作,当你检查符合标准时,不再需要额外的转换,就可以调用属于该类型的成员。
    if (value is String){
          print(value.toUpperCase())
    }
复制代码

互操作性

在Java项目中添加Kotlin代码时,不会影响我们的任何Java代码。同时,我们可以在Kotlin中使用Java的方法、库。可以继承Java的类,实现Java的接口,在Kotlin上使用Java的注解。

另外,Kotlin的互操作性使Java代码像调用其他Java类和方法一样轻松的调用Kotlin代码,在项目任何地方都可以混用Java和Kotlin。

Kotlin最大程度的使用Java库,使其互操作性更强。比如,Kotlin没有自己的集合库,它完全依赖的Java标准库,并使用额外的函数来扩展集合功能,使它们在Kotlin中可以更加便捷的使用。

Kotlin的工具也对互操作性提供了全面支持。可以编译任意混合的Java和Kotlin源码。两种源文件自由切换,重构某一种语言时,在另外一种语言中也会得到正确的更新。

两种语言可以随意拼接,就像呼吸一样自然。

3. Kotlin基础

3.1 变量

在Java中声明变量时:

int a = 10
复制代码

在Kotlin中声明变量时,参数的名称和类型使用 分隔,类型在之后,稍后我们会看到函数声明也是如此。

val a: Int = 10 //显示指定变量类型
val a = 10 //类型声明省略
复制代码

在Kotlin中所有的变量必须有初始值,如果没有也必须通过延迟初始化或者懒加载方式,在调用变量之前完成赋值操作。

lateinit var name: String
复制代码
val name by lazy { "liuxi" }
复制代码

如果你细心的话,会发现在上述的例子中出现了两种声明变量的关键字:

  • var ——(来自variable)可变引用,对应Java普通变量(非final),值可以任意改变,类型不可再变。
  • val —— (来自value)不可变引用,对应Java中final变量,val声明的变量在初始化之后不能再次赋值。 Kotlin推荐我们在默认情况下使用 val 声明变量,在需要的时候再使用 var 声明变量

3.1.1 便捷的字符串模版

Kotlin为我们提供了更便捷的字符串拼接方式供我们使用

val name = "liuxi"
fun age() = 12
val log = "name: $name age: ${age()}"

>>>
name: liuxi age: 12
复制代码

3.2 函数的定义和调用

我们先来看一下Kotlin中的Hello world。

fun main(args: Array<String>){ //main 是函数名,()内部是参数列表, {}内部是函数体
    print("Hello world")
}
复制代码

Kotlin使用fun关键字来定义函数。

接下来我们在看一下带返回类型的函数:

fun max(a: Int,b: Int): Int{
    retun if (a > b) a else b 
}
复制代码

函数的返回类型被定义在 之后

Kotlin中 if 是有结果值的表达式,不是语句,同时被用来替换掉Java中三元运算符 (boolean值) ? (表达式1) :(表达式2)。

表达式和语句的区别是表达式有值,且可以作为另一个表达式的一部分来使用。

3.2.1 表达式函数体

如果一个函数的函数体是由单个表达式构成的,那么这个函数可以去掉外层的括号{}return,在函数声明和表达式中间用=连接。

fun max(a: Int,b: Int) = if(a > b) a else b
//=后面的内容可以被称作表达式体
复制代码

借助于Kotlin优秀的类型推导特性,在上述范例中,我们也省略掉了对函数返回类型的声明。


从Kotlin到Java,很多概念都是类似的。Kotlin 为了让函数更简洁易读,借鉴Java并做出了很多改,基本如下:

  • 定义了命名参数、默认参数值以及中缀调用的语法。
  • 通过扩展函数和属性去适配Java库。
  • 使用了顶层函数,局部函数和属性架构代码。

我们先来看看在Kotlin 中命名参数,默认参数值的使用

3.2.2 命名参数、默认参数值

命名参数指的是在调用Kotlin定义的函数是,可以显示的表明一些参数的名称。当然如果一个函数由多个参数组成,为了避免混淆,在你给其中一个参数之名名称之后,其之后的所有参数都需要标明名称。

fun createAccount(account: String,password: String,desc: String){
    ...
}

create("liuxi","123456","cool")//常规使用

create("liuxi",password = "123456", desc = "cool") //使用命名参数的方式调用

复制代码

默认参数值,顾名思义就是一个参数支持声明默认值,用于避免重载的函数。

比如上述的createAccount()函数中的desc是一个非必要参数,这时我们就可以给其设置默认值。

fun createAccount(account: String,password: String,desc: String = ""){
    ...
}

createAccount("liuxi","123456","cool")

createAccount("liuxi","123456")//当用户不想输入这个参数时,我们可以直接最后一个参数省略掉。
复制代码

当然,默认参数值的写法在Java中是不被支持的,调用此方法时仍需显式的指定所有参数。为了Java也可以体会到此方法的便捷性,我们可以使用@JvmOverloads注解此方法。这样在Java中会自动生成此方法的重载方法。

3.2.3 顶层函数和属性

顶层函数和属性的诞生是为了消除Java中静态工具类的写法。即Java中各式各样的Util工具类。

在Kotlin中,我们可以将一些函数和属性(它们的功能类型使得它们很难归属到某一个具体的类中)直接写在代码文件(.kt)的顶层,不属于任何的类。这些函数和属性依旧是包内的成员。如果从包外想访问它,则需要Import。 我们来创建一个Log.kt文件

package com.liuxi.fun

val KEY = "123456"

fun log(msg:String){  //不需要放置在类体中
    Log.d("liuxi_test", msg) 
}
复制代码

在Kotlin中使用此方法就比较便捷:

log("测试顶层函数")
复制代码

而在Java中,这个文件会被编译成一个以文件名命名的类,顶层函数会被编译成这个类的静态函数。我们也可以利用@JvmName注解去自定义类的名称。

/*Java*/
package com.liuxi.fun

public class LogKt{
    
    public static final String KEY = "123456"
    
    public static void log(String msg){
        Log.d("liuxi_test", msg) 
    }
}
复制代码

3.2.4 扩展函数和属性

Kotlin的一个特色就是可以平滑的与现有代码集成。当我们在项目中遇到Java和Kotlin混合使用的时候,为了便捷的使用一些基于Java的功能,我们可以利用扩展函数来对其进行优化。

理论上来说,扩展函数就是类的成员函数,只不过它定义在类的外面。我们来看一个TextView的扩展函数:结合SpannableStringTextView赋值包含一个Image的文本:

/*文件名SpanHelper.kt*/

/**
 * 将目标String的替换为Image
 *
 * @param mainBody
 * @param targetBody
 * @param drawableRes
 * @return
 */
fun TextView.setTextWithImageSpan(mainBody: String?, targetBody: String?, drawableRes: Int) {
    if (mainBody.isNullOrEmpty() || targetBody.isNullOrEmpty() || drawableRes <= 0) {
        return
    }
    val spannableStr = SpannableString(mainBody)
    val start = mainBody.indexOf(targetBody)
    val end = start + targetBody.length
    spannableStr.setSpan(ImageSpan(context, drawableRes), start, end, Spanned.SPAN_EXCLUSIVE_INCLUSIVE)
    text = spannableStr
}
复制代码

此扩展方法的调用示例:

textView.setTextWithImageSpan("酷酷的", "酷酷", R.drawable.icon_diamond_small)
复制代码

简短的一行代码,我们就实现了给TextView设置一段包含图片的文本。这个功能真的很酷~~~

接下来,我们来看一下在Java中如何调用扩展函数。

SpanHelperKt.setTextWithImageSpan(textView, "酷酷的", "酷酷", R.drawable.icon_diamond_small)
复制代码

因为这个函数被声明为顶层函数,所以它被编译成为静态函数。textView被当做静态函数的第一个参数来使用。

扩展函数不存在重写,Kotlin会把其当作静态函数来看待。

接下来我们来看一下扩展属性:

//文件名IntUtil.Kt
val Int.square: Int
    get() = toInt() * toInt()

fun main(args:Array<String>){
    print(5.square)
}

>>> 25
复制代码

与扩展函数一样,扩展属性也像是接收者的一个普通成员属性一样。但是必须定义 getter 函数。因为没有支持的字段,所以没有默认的getter 。同理因为没有存储的地方,所以也无法初始化。个人感觉扩展属性和扩展函数类似,但是使用的便捷性比扩展函数略逊一筹。

//扩展属性在Java中的调用
IntUtilKt.getSquare(5)
>>> 25
复制代码

3.2.5 局部函数

一种支持在函数内部再定义一个函数的语法:

fun createAccount(name:String,password:String){

    fun lengthNotEnough(string:String) = string.length > 10
    
    if(lengthNotEnough(name) return
    if(lengthNotEnough(password) return
}
复制代码

这个功能的意义在于解决代码重复问题,尽可能的保持清晰的代码结构,提高可读性。

小知识点:局部函数可以访问外层函数的参数。

3.3 对集合的支持:可变参数,中缀调用,库的支持

3.3.1 可变参数

如下是Collections.kt中的一个方法

public fun <T> listOf(vararg elements: T): List<T> = if (elements.size > 0) elements.asList() else emptyList()
复制代码

vararg修饰符在Kotlin中被用来声明可以接受任意数量的参数。

3.3.2 中缀调用

一个很有趣的语法糖。 一下是我随手写的一个例子

infix fun TextView.copy(view: TextView) {
    this.text = view.text
} 
复制代码

我们使用infix 添加在只有一个参数的函数前面。上述例子是在一个扩展函数前面添加上了infix。 使用范例如下:

 val textView1 = ...
 val textView2 = ...
 textView1 copy textView2  //中缀调用式写法,这样textView2的内容就被复制到了textView1 中
 textView1.copy(textView2)//常规写法,与中缀调用式写法等价。
复制代码

中缀调用是一种特殊的函数调用,在使用过程中没有添加额外的分隔符,函数名称(copy)直接放置在目标对象和参数之间。

3.3.3 集合库的支持

Kotlin中的集合与Java的类相同,但是对扩展函数、内联函数等对API做了大量的扩展,此处就不再一一阐述了。

4. 类

Kotlin中和Java中一样,使用关键字 class 声明类

class Person{
    
}
复制代码

若一个类没有类体,可上述花括号可以省略

class Person
复制代码

另一个需要注意的地方是:Kotlin中类声明中没有了public。在Kotlin中,类默认为public和final的

4.1 构造方法

如你所知,在Java中,一个类可以声明一个或者多个构造函数,在Kotlin中同样如此。但是Kotlin对构造函数做了一些修改,引入的主构造方法(在类体外声明,为了简洁的初始化类的操作而被创造出来)和从构造方法(与Java构造方法类似,在类体中声明)并为此新增了两个关键字 initconstructor

关键字constructor用来声明 构造方法

下面是一个用Java式思想和kotlin定义的新语法写出来的Person类

class Person{
    val name: String 
    constructor(name: String){
        this.name = name
    }
}
复制代码

接下来我们对其引入主构造函数的概念和 init关键字:

class Person constructor(name: String){ //带一个参数的主构造方法
    val name: String
    init {   //初始化语句块
        this.name = name
    }
}
复制代码

如上代码所示,init用来标示初始化语句块,在类被创建的时候被执行,也可搭配主构造h函数使用。一个类中也可以声明多个初始化语句块,它们按照出现在类体中的顺序执行。

接下来,我们在对Person类进行简化:

class Person constructor(name: String){
    val name: String = name
}
复制代码

主构造函数的参数一样可以使用 varval 声明(从构造函数不可以),我们可以使用如下方式继续简化我们类的属性定义:

//val 表示相应的属性会用构造方法的参数来初始化
class Person constructor(val name: String)
复制代码

如果主构造函数没有注解或者可见性修饰符,则我们可以将constructor省略。

class Person (val name: String)
复制代码

在 Kotlin 中的一个类可以有一个主构造函数以及一个或多个从构造函数。当一个类有主构造函数时,其类体中声明的从构造函数需要通过直接或者间接的方式委托给主构造函数,此时就需要使用到this关键字。

class Person(val name: String = "liuxi"){
    constructor(name: String,age: Int): this(name)
    constructor(name: String,age: Int,gender:String): this(name, age)
}
复制代码

如果你想你的类不被其他代码实例化,则可以给构造函数添加可见型修饰符private

class Person private constructor()
复制代码
  • 创建的实例
val p = Person("liuxi")
复制代码

注:Kotlin中声明对象不需要使用 new 关键字

4.2 类的继承结构

我们先来认识几个基础概念:

接口 : 使用 inerface 定义

interface OnClickListener{
    fun onClick()
    fun test(){ //带有默认方法体的方法
        print("Hello")
    }
}
复制代码

抽象类: 使用abstract修饰

abstract class Game{
  abstract fun play()
  
  open fun record(){ //关键字open用来表示这个方法可以被重写,open也可以被用来修饰类。
      print("score")
  }
  
  fun test(){
      print("test")
  }
}
复制代码

接下来我们来展示一下如何继承一个抽象类

class A: Game(){
    override fun play() = print("Go play")
    
    override fun record(){
        super.record() //super 关键字用来标示对父类或者接口的默认方法体的引用,类似于Java
    }
}
复制代码

接下来我们在让类A实现接口OnClickListener

class A: Game(), OnClickListener{
    override fun play() = print("Go play")
    
    override fun record(){
        super.record() //super 关键字用来标示对父类或者接口的默认方法体的引用,类似于Java
    }

   override fun onClick()
   
   override fun test(){
       super.test()
   }
}

复制代码

如上所示,无论是继承还是实现接口,我们都统一通过 : 来实现,没有使用Java中的 extendsimplements

其中还需要注意下类的继承,和接口的实现有一个区别,被继承的类名后带了(),它值得是类的构造方法。如果你的类声明了主构造方法且含有参数,则子类也被强制要求实现主构造方法。此定义类似于Java的子类必须实现父类构造方法,且将父类构造所需要的参数传递给父类。

open class A(name:String) //一个类如果非抽象类,则默认是final,不可以被继承,但是用open修饰后就可以了。

class B(name: String): A(name)
复制代码

openfinal 是两个互斥定义的关键字。

还有一个细节也需要我们注意,当你override父类或者接口的方法后,它们默认是open的,如果你不希望你的子类再去修改它,需要用final修饰

class A: Game(), OnClickListener{
    final override fun play() = print("Go play")
   ...
}

复制代码

4.3 数据类、内部类、嵌套类、密封类

数据类

data class Person(val name)
复制代码

内部类和嵌套类

Java:

class A{
        public String name = "liuxi";

       static class B{
            void test(){
                System.out.print(name);//此操作被拒绝
            }
        }

        class C{
            void test(){
               System.out.print(name); 
            }
        }
    }
复制代码

Kotlin:

class A{
    var name = "liuxi"
    
    class B{
        fun test(){ //为嵌套类
            //print(name),此操作被拒绝,不持有外部类的实例,相当于Java 中的 static class B
        }
    }
    
    inner class C{
        fun test(){ //为嵌套类
            print(name) //持有外部类的实例 
        }
    }
}

复制代码

密封类 : 定义受限的类继承结构。这是为了解决when结构必须要实现一个else分支(类似于Java switch case中的default)而提出的解决方。给父类添加 sealed 修饰符且所有的直接子类必须嵌套在父类中。

sealed class Children{
    class Boy: Children()
    class Girl:Children()
}
复制代码

4.4 object关键字

object 关键字在Kotlin中有主要有三个使用场景,其核心理念是:object定义一个类同时创建一个实例。 接下来,看一下三个场景各是什么:

  1. 在Kotlin中使用对象声明去去创建单例:引入object 关键字,通过一句话去定义一个类和一个该类的变量。在编译过程中,该变量的名字始终都为INSTANCE
object Utils{     //由于类在定义的时候就立即创建了该类的变量,所以该类不被允许拥有构造函数,除此之外,其他地方都和普通类一样。
    fun test(){
        ...
    }
    val TEST = "test"
}
复制代码

没有在Java中的各种样式书写的单例模式,Kotlin仅需一行代码,轻松搞定。

kotlin中调用如下:

Utils.test()
Utils.TEST
复制代码

在Java中调用如下:

Utils.INSTANCE.test()
Utils.INSTANCE.TEST
复制代码

需要通过.INSTANCE引用对象声明中的方法,类似于我们在Java单例模式中习惯去创建的mInstance

  1. 伴生对象在Kotlin中的使用

Kotlin没有保留Java中的static关键字,为了补足静态函数和属性的功能,Kotlin提出了伴生对象,顶层函数和属性(也叫包级函数和属性),我们先来讲解伴生对象,后者在之后会被提到。

伴生对象是一个声明在类中的普通对象,可以拥有名字,也可以实现接口或者有扩展函数或属性。

class Person(val name: String){
    
    companion object Loader{
        fun test(){
           ... 
        }
        
        val type:String = "simple"
    }
}
复制代码

如上范例所示:companion关键字被用来标记伴生对象,伴生对象的名字可以被省略。简易写法如下:

class Person{
    companion object{
        ...
    }
}
复制代码

我们可以把伴生对象当作Java中的静态对象来看待,被定义为伴生对象后,我们就可以通过容器类的名称来获取这个对象的调用了。示例如下:

Person.companion.test()
复制代码
  1. object 来实现匿名内部类

object 在类体内部,可以被用来声明匿名对象 (替代了Java中的匿名内部类用法)。

常见使用场景有:将一个接口或者抽象类声明为匿名对象。

如下是将我们常见的 OnClikListener 声明为匿名对象:

view.setOnClickListener(object: OnClickListener{
    override onClick(v:View){
        
    }
})
复制代码

5. 一些很有特色的语法

5.1 when

Java中使用swich语句来完成对一个值可能出现的不同情况的作出对应处理。在Kotlin中我们使用when来处理。它比switch拥有更多的使用场景,功能更加强大,使用的也更加频繁,且还可以和智能转换 完美的结合使用。

when和if一样,都是表达式,可以拥有返回值。

举个例子来说:当下有一个需求,我们需要根据不同vip等级返回不同的drawable,供给ImageView使用。

    val imageView: ImageView = ...
    val level: Int = ...
    var imageView = imageView.setImageResource(
    when (level) {
        1 -> R.drawable.icon_level_1
        2 -> R.drawable.icon_level_2
        3 -> R.drawable.icon_level_3
        4 -> R.drawable.icon_level_4
        5 -> R.drawable.icon_level_5
        else -> {
            R.drawable.icon_level_default
        }
    })
复制代码

when语句对于level的不同情况,when返回不同分支上的值交给setImageResource()去使用。

值得一提的是,when 支持在一个分支上合并多个选项,比如我们对上述例子做出修改,将1、2定义为上,3定义为中,4、5定义为下,我们使用 when写出的代码如下:

   ...
    when (level) {
        1,2 -> R.drawable.icon_level_top
        3 -> R.drawable.icon_level_middle
        4,5 -> R.drawable.icon_level_bottom
        else -> {
            R.drawable.icon_level_default
        }
    }
复制代码

whenswitch强大的地方在于: 前者允许使用任何对象作为分支条件,甚至包括无参。但是后者必须使用枚举常量、字符串或者数字字面值。

open class BaseActivity{
  ...  
}

class MainActivity: BaseActivity(){
    ...
    fun mainPrint(){
        print("main")
    }
}

class SettingActivity: BaseActivity(){
    ...
}

复制代码
fun getTitle(activity: BaseActivity):String = when(activity){
    is MainActivity -> {
        activity.mainPrint()
        "main"        //if 和 when 都可以使用代码块作为分支体,在示例情况下,代码块中最后一个表达式就是返回的结果
    }
    is SettingActivity -> "Setting"
     
     else -> {
         ""
     }
}
复制代码

is :用来检查判断一个变量是否是某种类型,检查过后编译器会自动帮你把该变量转换成检查的类型,你可以直接调用该类型下的方法和变量,此功能可以被称作 智能转换 。它替我们省略掉了Java 中的 instanceOf 检查后的类型强制转换这一步,类型判断和类型转换二合一的操作也使我们的代码更加安全。当然,如果你仍然需要显式的类型转换,Kotlin提供了 as 来表示到特定类型的显示转换。

var a = activity as Maintivity
复制代码

接下来我们从一个例子来认识一下 when 的无参写法。

我们需要根据两人团中成员的性别分别给出类型定义,若是都为男性则称作男团、都为女性则成为女团、男女混合则称作二人转

/*常量*/
const val BOY = 0
const val GIRL = 1
const val BOY_GROUP = "男团"
const val GIRL_GROUP = "女团"
const val MIX_GROUP = "二人转"
复制代码

我们先给出 if 语句的写法:

fun getGroupName(gender1: Int, gender2: Int) =
        if (gender1 != gender2) {
            MIX_GROUP
        } else if (gender1 == BOY) {
            BOY_GROUP
        } else {
            GIRL_GROUP
        }
复制代码

我们先给出 when 语句的写法:

fun getGroupName2(gender1: Int, gender2: Int) =
        when {
            gender1 != gender2 -> MIX_GROUP

            gender1 == BOY -> BOY_GROUP

            else -> GIRL_GROUP
        }
复制代码

when语句的功能特别丰富,类似于Java中的switch但是功能更加强大。

5.2 Kotlin的可空性

可空性 是Kotlin类型系统中帮你避免NullPointerException错误的特性。

在Java中我们遇到过太多太多的 java.lang.NullPointerException,Kotlin解决这类问题的方法是将运行时的异常转换为编译时的异常。通过支持作为类型学系统的一部分的可控性,编译器就能在编译期间发现很多潜在的错误,从而减少运行时抛出空指针异常的可能性。

如下是一个获取字符串长度的方法

fun getLength(str: String?){  //我们需要在那些可能为null的实参后面加上问号来标记它
    if(str == null){
        return 0
    }
    return str.length
}
复制代码

问号 ? 可以加在任何类型的后面,来表示这个类型的变量可以存储null引用。相反没有被 ?

标记的变量不能存储null引用。如果你对一个不接收null引用的参数赋值为null,程序会直接抛出异常 Null can not be a value of a non-null type

可空类型和非空类型在Kotlin中有着很严格的区分。一旦你的变量被定义为可空类型,对它的操作也将收到限制,你将无法调用它的方法,除非你能证明它不为空;也不能将它赋值给非空类型的变量。也不能将可空类型的参数传给拥有非空类型参数的函数。

那么我们可以对可空类型参数做什么呢?我们对于可空类型参数,最重要的就是和null进行比较。一旦你进行了比较操作,编译器就会记住。并且在此次比较发生的作用域内把这个值当作非空来对待(我们就可以调用非空类型下的参数和函数了),如上述例子。

可空的和非空的对象在运行时没有什么区别。可空类型并不是非空类型的包装。所有的检查都发生在编译器。即使用Kotlin的可空类型在运行时不会带来额外的开销。

5.2.1 安全调用运算符:?.

安全调用运算符?.允许把一次null检查和一次方法调用合并成一个操作。

str?.toUpperCase()

if(str != null) str.toUpperCase() else null     //等效于str?.toUpperCase()
复制代码

安全运算调符只会调用非空值的方法。 安全调用符的结果类型也是可空的。在上述例子中返回类型为String?

5.2.2 Elvis 运算符 ?:

在引入可空性概念后,就代表了Kotlin中会出现很多默认值为null的可空类型。为了避免默认值weinull带来的麻烦,Kotlin提供了 Elvis运算符 ?: (null合并运算符)

下面展示了它是怎么使用的:

fun toStr(s: String?){
    val str =  s ?: "" // 如果s为null,则赋值为""
    
    var length = s?.length ?: 0  //常见使用场景,和安全调用运算符搭配使用
}
复制代码

5.2.3 非空断言!!

一个很暴力的语法!非空断言是最简单直率的处理可空类型的工具。它使用!!双叹号表示。

var length = s!!.length 
复制代码

当一个使用非空断言的变量为null时,会显式的抛出空指针异常。除非你对你的逻辑很有自信,不要轻易使用这个工具。 常见使用场景是你在一个函数使用一个值时对其进行了非空判断,而在另一个函数中使用这个值时,不想重复进行非空判断,这时你就可以使用非空断言。

5.2.3 let函数

let 函数让可空表达式的处理变得更加容易。它会把调用它的对象变成一个lambda表达式。

和安全调用运算符一起使用时,lambda中的it为该对象的非空值,如果该对象为null则什么都不会发生,不为空时会执行lambda表达式

str?.let{ print(it.length)}

/* 以下是为了增加可读性,简单格式化后的写法*/
str?.let{ it-> 
    print(it.length)
}

/*常规写法的等效语句*/
if(str != null){
     print(it.length)
}
复制代码

let生成的lambda语句中的it代表着调用对象的非空值

6. 总结

本文到此也基本告一段落了,写下这边文章的主要目的是想让更多的对Kotlin有兴趣的开发人员、在Kotlin和Java中徘徊纠结的人们还有那些对Kotlin抱有各种观念的人简单了解一下Kotlin,激发起大家学习Kotlin的兴趣,也抛开对Kotlin的偏见。看完此篇文章你应该也可以简单上手Kotlin了,快快写起来吧~

文章的定义是入门讲解,因此很多地方讲解的不够完善,也不够详细。希望那些对Kotlin有兴趣的人在学习Kotlin过程中最好还是能够系统的学习一边Kotlin的基础知识以及进阶语法。文章绝大部分知识点参照于《Kotlin实战》,推荐入门学者翻阅。

一些想写但没加入到文章中的知识点(有些部分是个人掌握不熟练,怕描述不准确,还有一点是基于篇幅考虑,怕写的太长吓跑新人)

  1. Kotlin中的lambda表达式,这部分的内容十分丰富,还有很多简单便捷的语法糖。
  2. Kotlin 基本数据类型,有点懒就没写,希望可以自行翻阅。
  3. Kotlin中的集合和数组 以及迭代相关知识(for 、区间、数列、map迭代、in关键字)。
  4. Kotlin的高阶函数和泛型。
  5. 协程(coroutines)是在Kotlin1.3版本中正式发布,一定要学习~

欢迎大家多多讨论,讲的晦涩或描述的有问题的地方也恳请大家指正,一起进步~~~~///(^v^)\~~~

关注下面的标签,发现更多相似文章
评论