Kotlin作为一种JVM语言,与传统的Java相比,增加了很多有意思的语法糖,可以显著降低开发难度,提升开发效率,增加代码健壮性。

基础篇

变量和类型

Kotlin中定义变量使用如下结构定义常量/变量

val 常量名: 类型 = 常量值
var 变量名: 类型 = 初始值

空安全

Java中最常见的一个问题就是NullPointException,在Kotlin中为了减少此问题推出了optional语法。

  1. 在定义一个变量时,若在常规类型后加一个?,表示此变量可以为null。

    /** str1不能为null */
    var str1: String = ""
    /** str2可以为null */
    var str2: String? = null
  2. 在调用可空对象的方法时,需要加一个?以进行安全的调用。

    /** str1一定不为null,因此直接访问它的属性就是安全的 */
    val len1 = str1.length
    /** str2可能为null,必须加?以安全访问它的属性,当str2为null时,len2也为null */
    val len2 = str2?.length
  3. 如果一个对象定义为可空,但在使用时确信一定不为null,则可以使用!!进行强转,但强烈建议不要这么做

    // 当str2为null时会抛NPE
    val len2 = str2!!.length
  4. 结合?:操作符,可以省掉很多null判断(A ?: B的含义为,只有当A为null时才会执行B)

    • 例一

      fun joinRoom(roomId: Long?, password: String?): Boolean {
       roomId ?: return false
       password ?: return false
       ……
      }
    • 例二

      public String recordingFaceUrl() {
       if (mediaBean != null) {
           if (mediaBean.recording != null) {
               if (mediaBean.recording.face_cover_url != null) {
                   return mediaBean.recording.face_cover_url;
               } else {
                   return mediaBean.recording.cover_image;
               }
           } else {
               return "";
           }
       } else {
           return "";
       }
      }
      fun recordingFaceUrl(): String {
       return mediaBean?.recording?.face_cover_url
               ?: mediaBean?.recording?.cover_image
               ?: ""
      }

类型推导

  1. 当定义变量时初始值类型明确时,可以省略类型声明,Kotlin会自动推导出来。

    // 自动推导出str1的类型为String
    var str1 = "abcde"
    // 会自动推导出len的类型为Int
    var len = "ABDEF".length
  2. 当已经经过了类型检测时,可以直接当作检测后的类型使用,而无需强转。

    void forbar(Object obj) {
        if (obj instanceof String) {
            ((String) obj).chatAt(0);
        }
    }
    fun forbar(any: Any) {
        if (any is String) {
            // obj已经被自动转成了String类型
            obj.chatAt(0)
        }
    }
    fun forbar(any: Any) {
        if (any !is String) return
        // obj已经被自动转成了String类型
        obj.chatAt(0)
    }
  3. 在泛型相关调用中,多数泛型参数也能够被自动推导填充(注:目前Java8也支持大部分泛型的类型推导)
  4. 与Java进行互调时,自动推导出的类型不是空安全的,并且会失去空安全检测。

    // 自动推导出str1类型是 String! ,即不是 String 也不是 String?
    val str1 = context.getString(R.string.title)
    // 编译不会报错并可运行,但有NPE风险
    var len1 = str1.length
    
    // 编译不会报错并可运行,但有NPE风险
    val str2: String = context.getString(R.string.title)

    解决办法:在Java的方法中使用@Nullable@NotNull注解明确返回值是否可空

特殊类型

  1. Kotlin中数字类型处理方式与Java很不同,表面上全部都是封装的类类型,但实际可能根据上下文章自动装箱或拆箱。\
    例外情况是可空类型一定对应封装类型,如Kotlin的Int?一定对应Java的Integer
  2. Kotlin中不存在数字的隐式转换,所有不同的类型必须显式调用相应的转换方法。
  3. 对于Int和Long类型的的位操作,在Kotlin中必须使用位操作函数或中缀表达式,不能使用操作符。
Java操作Kotlin函数形式Kotlin中缀形式
a << ba.shl(b)a shl b
a >> ba.shr(b)a shr b
a >>> ba.ushr(b)a ushr b
a & ba.and(b)a and b
`ab`a.or(b)a or b
~aa.inv()
  1. 另外在Kotlin中定义数组的方式也和在Java中不同,尤其基本类型有其自己的数组定义方式。
Java类型Kotlin类型
int[]IntArray
Integer[]Array&lt;Int&gt;Array&lt;Int?&gt;
String[]Array&lt;String&gt;Array&lt;String?&gt;

方法和属性

方法

常规方法

Kotlin中使用如下格式定义方法:

fun 方法名(参数列表) = 一句话表达式
fun 方法名(参数列表): 返回类型 {
    方法体
}
  1. 当返回类型是Unit时(相当于Java中的void),可以省略返回类型
  2. 当方法体只有一句话时,可以使用简约格式

    fun square(x: Int) = x * x

包级方法

在Kotlin文件中可以像C++一样直接定义方法,这样的方法在其它Kotlin中看起来是属于包的,不属于任何一个具体的类,在Java中各种Utils类的静态方法都可以使用包级方法实现。例如在IO.kt中有如下方法时,在Java中可以用IOKt.closeQuietly(is)调用。

fun closeQuietly(closable: Closable) {}

扩展方法

在Kotlin中可以按照如下格式给类增加方法,新增加的方法和类原本的方法调用形式完全相同。

fun 类型.方法名(参数列表): 返回类型 {
    方法体
}
  1. 如果定义的扩展方法是包级方法,则任何使用该类型的地方均可调用。
  2. 也可以在某个类型内部定义扩展方法,此时扩展方法只能在外部类型内使用,并且可以访问外部类型的成员变量。

    abstract class BaseFragment : Fragment() {
        private val composite = CompositeDisposable()
        // 只有在BaseFragment及其子类中的Disposable才可调用autoDispose方法
        fun Disposable.autoDispose() {
            /*
             * 在扩展方法内部的this指代的是被扩展的对象,此例中指Disposable对象
             * 若要访问外部对象则需要用@明确标识
             */
            this@BaseFragment.composite.add(this)
        }
    }

    对应于Java中如下代码:

    class BaseFragment extends Fragment {
        CompositeDisposable composite = CompositeDisposable();
        public void autoDispose(Disposable disposable) {
            this.composite.add(disposable);
        }
    }

特殊参数

  1. 默认参数

    定义方法时可以给参数设置默认值,在调用时可以省略最右侧的参数

    fun foobar(arg1: String, arg2: String = "", arg3: String? = null) {
        // Do something
    }
    
    /* 以下三个调用完全相同 */
    foobar("8023")
    foobar("8023", "")
    foobar("8023", "", null)

    Kotlin实现此功能时,实际是将被省略的参数自动补全,若要在Java中调用也能省略参数,则需要给方法增加@JvmOverloads注解。

  2. 命名参数

    在调用方法时,可以指定参数名称,这样就只给指定名称的参数赋值。

    fun foobar(arg1: String = "", arg2: String = "", arg3: String? = null) {
        // Do something
    }
    
    /* 以下三个调用完全相同 */
    foobar(arg2 = "8023")
    foobar("", "8023")
    foobar("", "8023", null)
  3. 变长参数

    • 和Java中使用...不同,Kotlin中使用varargs标记变长参数。

      void foobar(String... args) {}
      fun foobar(vararg args: String) {}
    • 若需要将变长参数作为其它变长参数的参数,则需要用*进行展开。

      fun foo(vararg args: Any) {}
      
      fun bar1(vararg args: Any) = foo(args)
      fun bar2(vararg args: Any) = foo(*args)
      
      // 在foo中args[0][0]为“abc”
      bar1("abc")
      // 在foo中args[0]为“abc”
      bar2("abc")

构造方法

  1. Kotlin中任意类的构造方法名称都是constructor
  2. 与类定义写在一起的构造方法是默认构造方法,其它构造方法都必须调用它。
  3. 如果默认构造没有其它特殊要求(如设置显隐性等),constructor关键字可以省略
  4. 如果默认构造方法的参数有val/var关键字修饰,则对应的参数自动变成成员常量/成员变量
  5. 构造函数参数列表可以有默认值,如果加上@JvmOverloads注解则会自动生成多个构造方法以供Java调用
  6. init{}代码块默认构造函数代码块

    /**
     * 由于有JvmOverloads注解,会自动生成RatioLayout(context)和RatioLayout(context, attrs)两个构造方法
     * 由于context有var标记,则context自动变为成员变量
     * 另:Kotlin中定义class默认是final的,只有标为open才能被继承
     */
    open class RatioLayout @JvmOverloads constructor(var context: Context, attrs: AttributeSet? = null)
         : FrameLayout(context, attrs, defStyle) {
     /** 非默认构造,必须调用默认构造,即this(context, attrs) */
     protected constructor(context: Context, attrs: AttributeSet?, defStyle: Int): this(context, attrs)
     /** 初始化代码块,相当于默认构造方法的方法体 */
     init {
         /** 在初始化代码块中,可以访问默认构造的所有参数,即使没有标记val/var */
         Log.e("RatioLayout", attrs.length)
     }
    }

属性

在传统Java开发中经常会使用到JavaBean。但JavaBean通常需要手写很多余的getter和setter方法。而在Kotlin中,所有非private变量默认都会自动生成bean方法。

  1. 如果不需要自动生成bean方法,可以使用@JvmField注解
  2. 如果需要在getter或setter中进行额外操作,可以覆盖get()和/或set(value)方法,其中get的返回类型和set的参数类型必须与属性兼容
  3. 属性有字段属性和计算属性两类,带有初始值的为字段属性,字段属性的方法中中可以用field访问原始的字段值
  4. val标识的属性为只读属性,只读属性只有get()方法
  5. 在kotlin中访问属性时与在java访问public成员变量形式完全相同

    public boolean isFavored() {
     return this.recordings != null 
             && this.recordings.recording != null
             && this.recordings.recording.has_favored;
    }
    
    public void setFavored(boolean favored) {
     if (recordings != null && recordings.recording != null) {
         recordings.recording.has_favored = favored;
     }
    }
    /** 计算属性 */
    var isFavored: Boolean
     get() = mediaBean?.recording?.has_favored ?: false
     set(value) { mediaBean?.recording?.has_favored = value }
    /** 字段属性 */
    var favoredCount: Int = 0
     private set(value) {
         field = Math.max(0, value)
     }

伴生对象

Kotlin没有静态变量、静态方法的概念,如果要完成相应的功能可以使用伴生对象。\
如果需要生成纯static变量或方法,可以增加@JvmStatic注解。(注意此时变量不能使用const关键字)

class SomeFragment: Fragment {
    companion object {
        private const val EXTRA_NAME = "extra_name"
        
        @JvmStatic
        fun newInstance(name: String): SomeFragment {
            val args = Bundle()
            args.put(EXTRA_NAME, name)
            val fragment = SomeFragment()
            fragment.arguments = args
            return fragment
        }
    }
}

Lambda函数

  1. 当一个方法(包括构造方法)的参数为有且仅有一个方法的Java接口类型时,可以使用更为简约的Lambda形式。

    Thread {
        // 异步线程
    }.start()
    new Thread(new Runnable {
        @Override
        public void run() {
            // 异步线程
        }
    }).start();
  2. 当接口有一个参数时,可以不用显示标明,会自动将参数命名为it,另外一般默认会将最后一个语句作为返回值

    // 按钮只能点击一次,点击一次后禁用掉
    button.setOnClickListener {
        it.enabled = false // it指onClick(view)的参数view,即button
    }
  3. 当方法的最后一个参数为Java单方法接口时,也可以使用Lambda表达式,此时可以将Lambda写在圆括号之外
  4. 当接口有多个参数时,必须明确标示出来,没有用到的参数可以用下划线_占位

    dialog.setPositiveButton("确定") { it, _ ->
        it.dismiss()
    }
  5. 在Kotlin中定义的接口不能使用Lambda表达式简写
  6. 在Kotlin中可以直接定义lambda函数,并且lambda可以作为其它函数的参数使用。

    fun setCallback(callback: (Int, Int) -> Int): Int {
        return callback(1, 2)
    }
    // result的值为3,类型为Int
    val result = setCallback { arg1, arg2 ->
        return arg1 + arg2
    }
  7. 当lambda函数可以为null,定义时需要使用optional语法,调用时需要使用invoke操作符。

    fun setCallback(callback: ((Int, Int) -> Int)?): Int {
        return callback?.invoke(1, 2) ?: 0
    }
    // result的值为3
    val result = setCallback { arg1, arg2 ->
        return arg1 + arg2
    }
    // result的值为0
    val result = setCallback(null)
  8. lambda函数在定义时可以为扩展方法,也可以和泛型结合以达到更高级的用法,此时在lambda函数体内this指代的是被扩展的对象。

    fun <T, R> with(receiver: T, block: T.() -> R) {
        return receiver.block()
    }
    // result的结果是“这是”
    val result = with("这是测试代码") {
        // 此时this指代的是“这是测试代码”这个字符串对象
        return this.substring(0, 2)
    }

数组和集合

  1. 集合分为可修改集合和只读集合,带有Mutable的集合为可修改集合,只读集合没有add/set/remove等方法
  2. 若数组/集合的初始值已经确定,则可以使用arrayOflistOfsetOfmapOf等扩展方法

    val a = intArrayOf(1, 2, 3, 4)      // a的类型为IntArray
    val b = arrayOf(1, 2, 3, 4)         // b的类型为Array<Int>
    val c = listOf(1, 2, 3, 4)          // c的表面类型为List<Int>,实际承载类型是ArrayList,但不能调用add等方法
    val d = mutableListOf(1, 2, 3, 4)   // d的表面类型为MutableList<Int>,实际承载类型是ArrayList
    val e = mapOf("name" to "Qi",       // e的表面类型为Map<String, Any>,实际承载类型是HashMap
                  "sex" to true)        // key to value 语法表示一个k-v键值对,参见infix表达式
  3. 在Kotlin中集合也可以完全按照数组的样式访问,并且对于Map类型的“下标”可以是非数字形式。(另:参见操作符重载之get/set)

    val list = listOf("Android", "iOS", "Web")
    val it1 = list[0]           // it1类型为String,值为Android
    val it2 = list[4]           // it2类型为String,但会抛出IndexOutOfBoundsException
    val it3 = list.getOrNull(4) // it3类型为String?,值为null,不抛异常
    val it4 = list.getOrElse(4, "RD") // it4类型为String,值为RD,不抛异常
    
    val map = HashMap()
    map["user_id"] = UserManager.INSTANCE.getCurrentUserID()
    map["song_id"] = "1234567890"
  4. Kotlin为数组和集合提供了很多扩展方法,可以很方便的进行过滤、排序、循环、映射等诸多操作。\
    每一个filter、map、forEach等都会产生一个循环调用,一般不应该出现多次连续调用的情况。\
    (附注:部分文档中说Kotlin在编译时会有优化,无需太过于考虑性能浪费问题,未能验证)

    // 打印所有身高超过165的18岁女性用户姓名
    // 糟糕
    users.filter { it.height > 165 }
            .filter { it.age == 18 }
            .filter { it.sex = Sex.FEMALE }
            .map { it.name }
            .forEach { println(it) }
    // 较好
    users.filter { it.height > 165 && it.age == 18 && it.sex = Sex.FEMALE }
            .forEach { println(it.name) }
  5. 数组和集合操作实际就是for循环,但在for循环中可以通过continue跳过本次循环,但在数组和集合操作中则需要使用return@语法,因为每一次调用都相当于调用了一次函数。

    fun main(args: Array<String>) {
        val list = listOf(1, 2, 3, 4, 5)
        list.map {
            // 若存在此语句,则不会有任何输出,因为map是一个inline函数,实际编译时会被展开
            // 此处的return返回的是main方法,map尚未执行完时main就已经结束了,后续不会执行
            // if (it % 2 == 0) return
    
            // map函数必须有返回值,并且有多少个传入参数就必须有多少个返回值
            if (it % 2 == 0) return@map 0
            it * it
            // map全部执行完成后的临时集合为[1, 0, 9, 0, 25]
        }.forEach {
            // forEach不要求有返回值,此时就相当于continue功能了
            if (it % 3 == 0) return@forEach
            print(it)
        }
        // 最终输出125
    }

区间

在Java中使用for循环时一般使用for (int i = A; i < B; i += C)格式,其中限制条件A、B、C在Kotlin中被封装成XXXRange的区间形式。(参见infix和操作符重载)

Java 代码Kotlin 代码
for (int i = A; i <= B; i++)for (i in A .. B)
for (int i = A; i <= B; i += C)for (i in A .. B step C)
for (int i = A; i <= B; i += C)for (i in A rangeTo B step C)
for (int i = A; i < B; i += C)for (i in A until B step C)
for (int i = A; i >= B; i--)for (i in A downTo B)
for (int i = A; i >= B; i -= C)for (i in A downTo B step C)

异常

与Java相同,Kotlin中所有异常均是Throwable的子类,try……catch……finally也与Java基本相同。\
但在Java中强制捕获的异常在Kotlin中并不强制!

进阶篇

字符串

  1. 字符串模板

    fun info(User: User) {
     print("姓名:${user.name},性别:${user.sex},年龄:${user.age}")
    }
    void info(User user) {
     StringBuilder sb = new StringBuilder();
     sb.append("姓名:");
     sb.append(user.name);
     sb.append(",性别:");
     sb.append(user.sex);
     sb.append(",年龄:");
     sb.append(user.age);
     System.out.print(sb.toString());
    }
  2. 原生字符串
    与Python、PHP等类似,可以用3个双引号来包裹字符串,这样的字符串内容没有转义,也可以直接使用双引号。另外,使用原生字符串时,为了保持页面的缩进样式可能在每行行首会产生多余缩进,此时可以使用trinIndent进行处理。

    val text0 = """
     for(c in str) {
          println(c)
     }
     """
    val text1 = """
     for(c in str) {
          println(c)
     }
     """.trimIndent()
    val text2 = """for(c in str) {
      println(c)
    }"""
    text0 == text1 // false
    text1 == text2 // true

多分支

与Java中的switch……case相似,Kotlin中也提供了多分支操作when,但远比switch强大的多,另外在when中按照从上到下的顺序来进行匹配,当某个分支的条件满足时整个when就退出(==因此else不能作为第一个条件==),每个分支不需要用break强制中断。

when(any) {
    1, "ONE"  -> {/*当any的值是数字1或者字符串ONE时执行此处操作*/}
    -1        -> {/*当any的值为-1时执行此处操作*/}
    "TWO"     -> {/*当any的值为"TWO"时执行此处操作*/}
    is String -> {/*当any是字符串类型时执行此处操作,不能和上一条件调换顺序*/}
    else      -> {/*以上条件都不满足时执行此处操作*/}
}

也可以

when {
    any == 1 || any == "ONE" -> {/*当any的值是数字1或者字符串ONE时执行此处操作*/}
    any == -1                -> {/*当any的值为-1时执行此处操作*/}
    any == "TWO"             -> {/*当any的值为"TWO"时执行此处操作*/}
    any is String            -> {/*当any是字符串类型时执行此处操作,不能和上一条件调换顺序*/}
    else                     -> {/*以上条件都不满足时执行此处操作*/}
}

泛型

在Kotlin中,泛型用法与Java大致相同,但Kotlin的泛型更严格一点,每一个泛型类都必须明确指定泛型类型,或可以正确推导出泛型类型。

val list1 = listOf<Any>()   // 正确,list1的类型是List<Any>,由明确指定得到
val list2 = listOf("", 1)   // 正确,list2的类型是List<Any>,由自动推导得到
val list3 = listOf()        // 错误,无法确定list3的泛型类型

不变

不变即泛型参数是一个十分明确的类型,而这个泛型类型即可以作为输入类型也能作为输类型。例如常见的ArrayList<String>就是不变泛型。

逆变(in)

逆变泛型使用in关键字,表示传入的参数至少是指定类型,因此逆变泛型只能作为输入类型,与Java中Foo<? super T>语法相似。

class Foo<in T> {
    fun set(item: T) { } // 合法,逆变可以作为参数
    fun get(): T { }     // 非法,逆变不能作为返回值
}

class Bar
class Child: Bar

val foo = Foo<Bar>
foo.set(Child())    // 合法,Child对象也至少是一个Bar类型的对象

协变(out)

协变泛型使用out关键字,表示传出的参数最多是指定类型,因此逆变泛型只能作为输入类型,与Java中Foo<? extends T>语法相似。

class Foo<out T> {
    fun set(item: T) { } // 非法,协变不能作为参数
    fun get(): T { }     // 合法,协变可以作为返回值
}

星投影(*)

星投影有点类似在Java中的省略泛型参数,但在Java中省略泛型参数后泛型类型为Object,但Kotlin中星投影只能表示泛型类型传递的不确定性,不能当作Any泛型,因此不能创建星投影泛型对象。

val list1 = ArrayList<Any>()    // 合法,泛型类型为Any
val list2 = ArrayList<*>()      // 非法,不能创建星投影泛型对象

class MainAdapter: Adapter {
    fun setItems(items: List<*>) {
        ……
        notifyDataSetChanged()
    }
}
val adapter = MainAdapter()

class items1 = ArrayList<Int>()
adapter.setItems(items1) // 合法,可以接受任意泛型类型

class items2 = ArrayList<String>()
adapter.setItems(items1) // 合法,可以接受任意泛型类型

具化(reified)

JVM虚拟机下的泛型全部是假泛型,在实现编译时会抹去所有泛型信息,若要想知道具体的泛型信息则需要通过一些复杂的操作才能实现,但在Kotlin中还可能通过具化关键字reified来简化泛型操作。\
具体实现上来说,实际是在编译时生成相应的方法重载来达到具化目的的,因此具化只能用于泛型函数不能用于泛型类,并且这个泛型函数必须是inline的。

inline fun <reified T> foobar(value: T): String {
    return "“${value}”的类型是${T::class.simpleName}"
}
// 会自动生成foobar(value: String)重载
val value1 = foobar("字符")
// 如果有下面的调用会再生成foobar(value: Int)重载
// val value2 = foobar(123)

数据类

如果是一个纯Bean类,可以使用data class,data class其实就是JavaBean的简化语法糖,因此它需要至少一个字段,即默认构造需要至少一个val/var参数。data class默认实现了equals、hashCode、toString等JavaBean必备的方法,除这些常规方法外,它还实现了一系列componentN解构方法。

data class BlockEvent(@JvmField var userId: String, val isBlocked: Boolean)
val event = BlockEvent("951753", false)

// 语句段1
val (userId, isBlocked) = event
// 语句段2
val userId = event.component1()
val isBlocked = event.component2()

// 语句段3
val (_, isBlocked) = event
// 语句段4
val isBlocked = event.component2()

// 以上四个语句段,1和2完全等同,3和4完全等同

单例模式

object Singleton
public class Singleton {
    public static final volatile Singleton INSTANCE;
    static {
        synchronized(Singleton.class) {
            INSTANCE = new Singleton();
        }
    }
    private Singleton() {}
}

多数文档中说明Kotlin的单例是线程安全并且懒加载的,但从反编译的代码上来看使用的是线程不安全的饿汉模式,暂时还没有搞明白原因。

操作符

常规操作符

Java中的一些操作符如instanceof等在Kotlin中也有与之对应的形式,如下表所示:

Java 代码Kotlin 代码备注
a instanceof Aa is A
!(a instanceof A)a !is A
B b = (B)aval b = a as B?
B b = (a instanceof B) ? (B)a : nullval b = a as? B
a.equlas(b)a == ba?.equals(b) ?: (b === null)
a == ba === b

==注意:a as? Ba as B?含义是完全不一样的!==

操作符重载

在Java中只有数字类型支持++--+=-=*=/=%=等操作,只有数组可以进行arr[0]形式的下标访问,只有函数可以进行foobar()形式的调用,但在Kotlin中也可以让我们自己的类型支持这些操作符,此时就要用到操作符重载。\
(特别注意:滥用操作符重载会严重影响代码可读性,因此重载操作符一定要慎重!)

class Demo {
    // +this
    operator fun unaryPlus(): Demo { }
    // ++this、this++
    operator fun inc(): Demo { } // 注意不要直接修改this值
    // !this
    operator fun not(): Demo { }
    // this + other
    operator fun plus(other: Demo): Demo { }
    // this += other
    operator fun plusAssign(other: Demo) { }
    // this .. other
    operator fun rangeTo(other: Demo): ClosedRange<Demo> { }
    // other in this
    operator fun contains(other: Demo): Boolean { }
    // this(other)
    operator fun invoke(other: Any): Any { }
    // this[index]
    operator fun get(index: Int): Any { }
    // this[index] = value
    operator fun set(index: Int, value: Any) { }
    // this[index1, index2]
    operator fun get(index1: Any, index2: Any): Any { }
    // this[index1, index2] = value
    operator fun set(index1: Any, index2: Any, value: Any) { }
    // this == other、this != other
    operator fun equals(other: Any?): Boolean { }
    // this > other、this >= other、this < other、this <= other
    operator fun compareTo(other: Demo): Int { }
}

别名

在一些场景下可以给类型指定另外的名称,以解决名字冲突或缩减代码。

import android.twitter.util.Constants as ConstT
import com.facebook.util.Constants as ConstF

ConstT.SIZE_PB // 相当于调用android.twitter.util.Constants.SIZE_PB
ConstF.SIZE_PB // 相当于调用com.facebook.util.Constants.SIZE_PB

typealias StringList = ArrayList<String>
typealias StringMap<T> = HashMap<String, T>
typealias LongClick = (View) -> Boolean

// val list = ArrayList<String>(10)
val list = StringList(10)
// val map = HashMap<String, Double>()
val map = StringMap<Double>

val lc: LongClick = { it -> false }
view.setOnLongClickListener(lc)

反射*

类引用

最基本的反射功能是获取 Kotlin 类的运行时引用。

val kclazz: KClass<UserModel> = UserModel::class
val jclazz: Class<UserModel> = kclazz.java

// 声明Intent需要用到java class
val intent = Intent(context, MainActivity::class.java)

方法引用

在Kotlin中可以将方法赋值给一个变量,然后就可以把这个变量当作该方法调用。\
实现上Kotlin是将这个引用封装成了一系列叫作Function的inline函数

  1. 类方法引用

    val foobar = String::substring
    val sub1: String = foobar("这是字符串", 0, 1) // sub1的结果为“这”
    val sub2: String = foobar("这是字符串", 1, 2) // sub2的结果为“是”

    约等于以下Java代码,但一般情况下foobar会被当作inline函数展开。

    String foobar(String ref, int start, int end) {
        return ref.substring(start, end);
    }
    String sub1 = foobar("这是字符串", 0, 1);
    String sub2 = foobar("这是字符串", 1, 2);

    由于双冒号::右侧的方法不带有参数,当这个方法存在重载时,需要显式声明我们希望使用的是哪一个

    // foo会引用substring(int)
    val foo: Function2<String, Int, String> = String::substring
    // bar会引用substring(int, int)
    val bar: Function3<String, Int, Int, String> = String::substring

    ==注意:如果要显式声明,则必须依赖kotlin-reflect库,Function后的数字表示参数个数,泛型最后一个是返回值类型==

  2. 实例方法引用

    val foobar = "这是字符串"::substring
    val sub1: String = foobar(0, 1) // sub1的结果为“这”
    val sub2: String = foobar(1, 2) // sub2的结果为“是”

    约等于以下Java代码,但一般情况下foobar会被当作inline函数展开。

    String ref = "这是字符串";
    String foobar(int start, int end) {
        return ref.substring(start, end);
    }
    String sub1 = foobar(0, 1);
    String sub2 = foobar(1, 2);
  3. 方法引用的作用
    方法引用可在一些场景下简化方法的调用。

    RxTextView.textChanges(EditText(context!!))
        .map(CharSequence::toString)
        .subscribe { }

    属性引用

    与方法引用相似,我们也可以用::来引用一个属性。
    (实现上Kotlin会假想将val属性封装成KProperty对象,将var封装成KMutableProperty对象,实际编译后并不存在这样的对象)

  4. 类属性引用

    val user = UserModel()
    val id = UserModel::id
    id.set(user, 123456)

    约等于以下Java代码,但实际比这要复杂些,并且一般不会展开。

    void id(UserModel user, int id) {
        user.id = id;
    }
    UserModel user = new UserModel();
    id(user, 123456);
  5. 实例属性引用

    val user = UserModel()
    val id = user::id
    id.set(123456)
  6. 属性引用的作用
    通过属性引用可以检查属性的当前状态信息。

    id.isLateinit
    id.isInitialized

新语法

中缀

中缀即用infix修饰有且仅有一个参数的函数(包括扩展函数),后续调用此函数时可以使用中缀表达式的形式。这个语法糖主要是在某些场景下可以使函数更美观一些,或者更符合一般常规的上的习惯。若有多个中缀表达式连续调用,则一律采取从左往右的执行顺序。

class Foobar {
    infix fun go(step: Int)……
}
val foobar = Foobar()
// 与foobar.go(1)功能含义完全相同
foobar go 1

例如DBFlow的Kotlin扩展,使得在Kotlin中的DBFlow操作拥有于SQL语句90%的相似度。

select(name).from(Android.class).where(name.is("Nexus 5x")).and(version.is(6.0))
select name from Android::class where (name eq "Nexus 5x") and (version eq 6.0)

委托

委托模式是软件设计模式中的一项基本技巧。在委托模式中,有两个以上的对象参与处理同一个请求,接受请求的对象将请求委托给另一个对象来处理。

类委托

类的委托即一个类中定义的方法实际是调用另一个类的对象的方法来实现的。

interface IButtons {
    fun clickLeft()
    fun clickRight()
}
interface IWheel {
    fun clickWheel()
    fun rollUp()
    fun rollDown()
}
class Mouse(val buttons: IButtons, val wheel: IWheel)
        : IButtons by buttons, IWheel by wheel {
    // 正常的委托需要有以下方法,但在类委托下此方法自动实现,其它4个方法类似
    fun clickLeft() {
        buttons.clickLeft()
    }
}

属性委托

属性委托指的是一个类的某个属性值不是在类中直接进行定义,而是将其托付给一个代理类,从而实现对该类的属性统一管理。
格式为val/var <属性名>: <类型> by <表达式>by关键字之后的表达式就是委托, 属性的 getter (以及setter)将被委托给这个对象的 getValue() 和 setValue() 方法(因此被委托的属性不能使用@JvmField注解)。
属性委托不必实现任何接口(因此实际是通过编译时替换实现), 但必须提供 getValue() 函数(对于 var属性,还需要 setValue()函数)。

class Foobar {
    var prop: String by Delegate()
}

/**
 * 具体处理委托操作的类
 * 1. 委托的getValue和setValue都属于操作符,因此必须有operator关键字
 * 2. 两个方法的thisRef表示属性所在的对象,此例中即Foobar对象
 * 3. property是对被委托属性的反射引用,参见反射→属性引用部分
 */
class Delegate {
    // val/var属性均必须有getValue方法
    operator fun getValue(thisRef: Any, property: KProperty<*>): String {
        return "$thisRef, 这里委托了 ${property.name} 属性"
    }
    // 只有var属性才需要setValue方法
    operator fun setValue(thisRef: Any, property: KProperty<*>, value: String) {
        println("$thisRef 的 ${property.name} 属性赋值为 $value")
    }
}

常用委托

  1. 懒加载

    // 只有在首次调用text的时候才会执行lazy的lambda体初始化,由于lazy只初始化一次并且中途不能进行修改,所以必须是val类型
    val text by lazy { File("/sdcard/log.txt").readText() }
  2. 映射器

    class User(val map: MutableMap<String, Any>) {
        var name: String by map
        val age: Int by map
    }
    val user = User(mapOf("name" to "张珊", age to 18))
  3. 观察器

    var name: String by Delegates.observable("初始值") { property, old, new ->
        println("旧值:$old -> 新值:$new")
    }
  4. 拦截器

    var name: String by Delegates.vetoable("初始值") { property, old, new ->
        println("旧值:$old -> 新值:$new")
        new.length > 0 // 当返回false时,不允许修改属性的值,相当于拦截了本次修改
    }
  5. 预占位

    var name: String by Delegates.notNull<String>()

    此委托适用于那些无法在初始化阶段就确定属性值的场合。但与延迟lateinit功能基本相同,一般不建议使用。

延迟

Kotlin一般要求在定义变量时有初始值(类的成员属性可以在默认构造时赋值),但典型如Fragment传递的参数,明确知道参数是不可null的,但又无法在构造函数时获取,常规方式下只能定义成可空类型然后再在onCreate时获取,但将实际不可空的属性定义成可空类型会增加后续操作的复杂度。
延迟加载就恰恰适用于这些无法在初始化阶段就确定属性值的场合,由于延迟加载需要稍后修改属性,因此必须定义为var可修改类型。

class SomeFragment: Fragment {
    lateinit var name: String

    override fun onCreate(state: Bundle?) {
        super.onCreate(state)
        name = arguments.get(EXTRA_NAME)
    }
    
    companion object {
        private const val EXTRA_NAME = "extra_name"
        
        @JvmStatic
        fun newInstance(name: String): SomeFragment {
            val args = Bundle()
            args.put(EXTRA_NAME, name)
            val fragment = SomeFragment()
            fragment.arguments = args
            return fragment
        }
    }
}

==注意:在首次赋值之前访问lateinit属性会抛出UninitializedPropertyAccessException异常==

内联

  1. inline

    众所周知函数的调用是需要有栈操作的,一些简单又频繁调用的功能如果用函数实现,可能会有一些性能上的损失,此时可以使用内联函数实现。与C/C++中的内联函数相似,在编译期间内联函数会被展开,即实际的字节码中并不存在这个函数。

    inline fun mix(a: Int, b: Int) = if (a < b) a else b
  2. noinline

    默认情况下所有内联函数的lambda参数也都是内联的,如果不希望内联则需要使用noinline关键字。

    inline fun foo(value: Int, noinline callback: (Int) -> String): String {
        return callback(value)
    }
    foo(2) { return "${it * it}" }
    Sting foo(int value, Function1<Integer, String> callback) {
        return callback.invoke(value);
    }
    class Inner0 extends Function1<Integer, String> {
        public String invoke(Integer it) {
            return String.valueOf(it * it);
        }
    }
    foo(2, new Inner0());
  3. crossinline

    inline fun inner(lambda: () -> Int) { lambda() }
    fun outer() { 
        inner {
            // (1) 由于inline和它的lambda参数都将被展开,处的return实际最终将是outer的return
            return
            // (2) 有了@inner标记才是inner的return,此处必须按照inner的lambda要求一个Int
            return@inner 0
        }
        println("结束") // 若(1)处代码存在,则此行不会执行
    }

    要解决以上问题,可以在定义inner时使用crossinline关键字。

    // 此时(1)处代码编译时报错
    inline fun inner(crossinline lambda: () -> Int) { lambda() }

辅助函数

  1. let

    inline fun <T, R> T.let(block: (T) -> R): R
    
    val result: Int = user.let { it ->
        // it指代user对象,并默认以最后一个语句作为返回值
        it.age
    }
  2. run

    inline fun <T, R> T.run(block: T.() -> R): R
    
    val result: Int = user.run {
        // this指代user对象,并默认以最后一个语句作为返回值
        this.age
    }
  3. also

    inline fun <T> T.also(block: (T) -> Unit): T
    
    val result = user.also { it ->
         // it指代user对象,此lambda无返回值,also会返回对象本身
         it.age = 16
    }
  4. apply

    inline fun <T> T.apply(block: T.() -> Unit): T
    
    val result = user.apply {
         // this指代user对象,此lambda无返回值,apply会返回对象本身
         this.age = 16
    }
  5. takeIf

    inline fun <T> T.takeIf(predicate: (T) -> Boolean): T?
    
    // 当user的年龄大于16时,result = user,否则result = null
    val result = user.takeIf { it ->
         // it指代user对象,并默认以最后一个语句作为返回值
         // 若返回值为true,则takeIf整体返回user对象本身
         // 若返回值为false,则takeIf整体返回null
         it.age > 16
    }
  6. takeUnless

    inline fun <T> T.takeUnless(predicate: (T) -> Boolean): T?
    
    // 当user的年龄不大于16时,result = user,否则result = null
    val result = user.takeUnless { it ->
         // it指代user对象,并默认以最后一个语句作为返回值
         // 若返回值为false,则takeIf整体返回user对象本身
         // 若返回值为true,则takeIf整体返回null
         it.age > 16
    }
  7. with

    inline fun <T, R> with(receiver: T, block: T.() -> R): R
    
    val result: Int = with(user) {
        // this指代user对象,并默认以最后一个语句作为返回值
        this.age
    }
  8. repeat

    inline fun repeat(times: Int, action: (Int) -> Unit)
    
    // 无返回值,以下语句输出10次带换行的你好
    repeat(10) { println("你好") }

补漏篇

Kotlin调Java

  1. Getter 和 Setter

    遵循 Java 约定的 getter 和 setter 的方法(名称以 get 开头的无参数方法和以 set 开头的单参数方法)在 Kotlin 中表示为属性。 Boolean 访问器方法(其中 getter 的名称以 is 开头而 setter 的名称以 set 开头)会表示为与 getter 方法具有相同名称的属性。

  2. void类型处理

    如果一个 Java 方法返回 void,那么从 Kotlin 调用时中返回 Unit。 万一有人使用其返回值,它将由 Kotlin 编译器在调用处赋值, 因为该值本身是预先知道的(是 Unit)。

  3. 特殊关键字

    一些 Kotlin 关键字在 Java 中是有效标识符:in、 object、 is 等等。 如果一个 Java 库使用了 Kotlin 关键字作为方法,你仍然可以通过反引号(`)字符转义它来调用该方法

    foo.`is`(bar)
  4. 平台类型空安全

    Java 中的任何引用都可能是 null,这使得 Kotlin 对来自 Java 的对象要求严格空安全是不现实的。 Java 声明的类型在 Kotlin 中会被特别对待并称为平台类型。对这种类型的空检查会放宽, 因此它们的安全保证与在 Java 中相同。

    • T! 表示“T 或者 T?”
    • (Mutable)Collection! 表示“可以可变或不可变、可空或不可空的 T 的 Java 集合”
    • Array<(out) T>! 表示“可空或者不可空的 T(或 T 的子类型)的 Java 数组”

    强烈建议在Java使用@NotNull@Nullable注解,以使Kotlin能明确知道空安全性。Kotlin支持多种框架下空安全性注解。

    • JetBrains(org.jetbrains.annotations 包中的 @Nullable@NotNull
    • Eclipse(org.eclipse.jdt.annotation
    • Android(com.android.annotationsandroid.support.annotations
    • FindBugs(edu.umd.cs.findbugs.annotations
    • Lombok(lombok.NonNull

Java调Kotlin

  1. Getter 和 Setter

    Kotlin属性的getter方法会通过名称加get前缀得到,setter方法会通过名称加set前缀得到。一个例外是如果属性的名称以is开头,则getter的名称与属性相同,setter的名称通过将is替换成get得到,并且不仅限于Boolean类型

  2. 包级函数

    在org.foo.bar包内的Example.kt文件中声明的所有的函数和属性,包括扩展函数,都编译成一个名为org.foo.bar.ExampleKt的Java类的静态方法。
    可以使用@file:JvmName注解修改生成的Java类的类名,若期望将多个kt文件合并生成一个Java类,可以在相关文件中使用@JvmMultifileClass注解。

  3. 实例字段

    如果需要在Java中将Kotlin属性作为字段暴露,那就需要使用@JvmField注解对其标注,该字段将具有与底层属性相同的可见性。

  4. 静态字段

    在命名对象或伴生对象中声明的Kotlin属性,会在该命名对象或包含伴生对象的类中具有静态幕后字段。通常这些字段是私有的,但可以通过@JvmField注解、lateinit修饰符和const修饰符将它暴露出来。

  5. 静态方法

    在命名对象或伴生对象中声明的Kotlin方法,会在相应的对象中生成实例方法。如果使用@JvmStatic注解,编译器既会在相应对象的类中生成静态方法,也会在对象自身中生成实例方法。

  6. 可见性

    Kotlin的可见性以下列方式映射到Java:

    • private成员编译成private成员
    • private的顶层声明编译成包级局部声明
    • protected保持protected(注意 Java 允许访问同一个包中其他类的受保护成员, 而Kotlin不能,所以 Java 类会访问更广泛的代码
    • internal声明会成为Java中的publicinternal类的成员会通过名字修饰,使其更难以在Java中意外使用到,并且根据Kotlin规则使其允许重载相同签名的成员而互不可见
    • public保持public
  7. 重载

    通常有默认参数值的Kotlin函数在Java中只会有一个所有参数都存在的完整参数签名的方法可见,如果希望向Java调用者暴露多个重载,可以使用@JvmOverloads注解。

  8. 受检异常

    Kotlin没有受检异常,如果我们想要在Java中捕捉这个异常,要在Kotlin中使用@Throws注解。

标签: kotlin, java

添加新评论