全面了解Kotlin

768 阅读20分钟

全面了解Kotlin

缘起

2019年谷歌在I/O大会上宣布,Kotlin 编程语言现在是 Android 应用程序开发人员的首选语言。(Kotlin-first)

Kotlin 由 JetBrains 公司开发,与 Java 100% 互通,并具备诸多 Java 不支持的特性。两年前的 I/O 2017 年上,谷歌才宣布在其 Android Studio IDE 中支持 Kotlin,Kotlin 与 Java 并存,开始成为 Android 开发的一级语言,当时业界并不太看好,因为 Java 在安卓的发展过程中做出了巨大的贡献,其作为 Android 开发语言不二之选的观念已经根深蒂固,而且它也一直在不断完善。

然而仅仅几年时间,Kotlin 因其相比 Java 更安全与简洁等优越性,很快占领市场,谷歌 Android 首席布道师 Chet Haase 表示:“在过去的两年中,Kotlin 受欢迎程度一直在提高,超过 50% 的专业 Android 开发人员现在使用 Kotlin 开发他们的应用”。从今年 Stack Overflow 年度开发者调查的数据中也能看到这一点,如下图所示,报告中指出,在开发者最喜爱的编程语言中,Kotlin 得到了 72.6% 正面反馈。

谷歌为什么选择Kotlin

谷歌选择kotlin的最直接原因,是因为甲骨文碰瓷

2010年,甲骨文以 74 亿美元收购了 Sun(Sun 于 1995 年开发了 Java),然后在不到8个月的时间里便起诉谷歌侵犯了关于 Java 语言的版权,索赔 88 亿美元。

回顾一下甲骨文和谷歌的这场 Java 版权案拉锯战:

  • 2010年,甲骨文起诉谷歌,称谷歌 Android 操作系统未经授权使用了 Java API
  • 2012年,谷歌成功让法庭认可了 API 不在著作权保护范畴内的观点,地方法院裁定 API 不受法律保护,并驳回案件
  • 2012年,甲骨文不满裁决,并上诉至美国联盟上诉法院
  • 2014年,上诉法院三名法官意见一致地将地方法院对该案件的判决驳回,并宣布 API 受著作权保护
  • 2014年,谷歌不服判决便发起上诉,并上诉到了联邦最高法院
  • 2015年,联邦最高法院驳回谷歌的上诉,并将本案发回地方法院重审
  • 2016年3月,甲骨文将索赔金额提至 93 亿美元
  • 2016年4月,谷歌 CEO 与甲骨文 CEO 和解会议失败
  • 2016年5月,旧金山地区法庭进行二次审理,认同谷歌对 Java API 的使用受“fair use”原则保护
  • 2016年10月,甲骨文在联邦巡回上诉法院提起上诉
  • 2017年,联邦巡回上诉法院审理了甲骨文的上诉
  • 2018年3月,联邦巡回上诉法院认定 Android 侵权,判决甲骨文胜诉
  • 随后谷歌再次发起上诉,但2018年8月被驳回
  • 2019年1月,谷歌要求美国最高法院对它与甲骨文之间的 Java API 版权诉讼案做出最终裁决

谷歌选择kotlin只是为了摆脱专利诉讼而已。当然Kotlin自己也争气。确实表现优异。

一、语法一瞥

1.Hello World

    @JvmStatic
    fun main(args: Array<String>) {
       print("Hello World!")
    }

2.变量

首先我们先用var定义一个num变量,用val定义一个str变量。

var num : Int = 1
val str : String = "test"

变量定义.jpg 根据上图,我们需要注意两点:

2.1.val等于final

我们可以看到var定义的num可以被重新赋值,str却不可以。上图的val实际上就等于Java中的final String,也就是val定义的变量默认添加了final关键字。

2.2.可空?以及空匹配

第二点就是num变量在定义为Int的时候是不能赋值为Null的,如果需要我们需要这么定义

var num : Int?

2.3.类型推断

val str = ""

kotlin具有类型推断功能,上面的语句等于Java

final String str = ""

自定义getter&setter

 var msg: String
    get() = field
    set(value) {
        field = value
    }

3.函数

3.1.定义

3.1.1普通函数

定义一个名为test函数,返回值为String?,可能返回为空

  fun test(): String? {
        return null
    }

调用:

调用和Java类似,由于test返回的是可空的字符串,添加?:表示当前面为空取冒号后面的值。

val result = test() ?: "x"

也可以

fun isEmpty(str: String) = str.isEmpty()

其中isEmpty函数的返回值为后面isEmpty()的返回值。

3.1.2默认参数

kotlin支持带默认参数的函数,默认参数不传则为默认值。

data class EnvConfig(val baseUrl: String, val isDebug: Boolean = false)

//构造1 等于 EnvConfig("https://xx.com",false)
val env1 = EnvConfig("https://xx.com")
  
3.1.3命名参数

kotlin方法调用可以指明参数名称,以避免混淆。更加直观。

EnvConfig(
      baseUrl = "https://xx2.com",
      isDebug = true
   )

3.2顶层函数和属性

kotlin可以定义全局可以调用的工具函数,它会编译成该文件的静态方法以供调用。

TopFunc.kt

fun toString(obj:Any) = obj.toString()

翻译成Java类

public final class TopFuncKt {
   @NotNull
   public static final String toString(@NotNull Object obj) {
      Intrinsics.checkParameterIsNotNull(obj, "obj");
      return obj.toString();
   }
}

同理顶层属性

var count = 0

Java

    public static final int getCount() {
      return count;
   }

   public static final void setCount(int var0) {
      count = var0;
   }

3.3 给别人的类添加方法: 拓展函数和属性

3.3.1拓展函数

拓展函数非常简单,它就是一个类的成员函数。

TopFunc.kt

//定义一个成员函数 方法内的this会指向被拓展的对象。
//即这里的this是这个字符串
fun String.print() = println(this)

//使用
"string extension".print()

//输出
string extension

拓展函数也是顶层函数,所以它在Java中也是静态函数,调用如下:

TopFuncKt.print("extension in Java");

拓展函数仅仅是为了缩短语法而存在。并非真正意义上的"拓展",也做不到真正的拓展,所以拓展函数无法进行重写,或者在Java中当作成员函数来调用。

3.3.2拓展属性

类似拓展函数,拓展属性提供了一种方法,用来拓展类的API,可以用来访问属性,用的是属性语法而不是函数的语法。**尽管它们被称为属性,但他们可以没有任何状态,也没有合适的地方来存储它们,**不能给现有的Java对象的实例添加额外的字段。

var StringBuilder.lastChar: Char
    get() = get(length - 1)
    set(value: Char) {
        setCharAt(length - 1, value)
    }

**上面的代码只是利用拓展属性提供了一种快捷访问该类成员方法的途径,但是并没有给StringBuilder这个类添加lastChar这个属性。**尽管如此,拓展属性依旧十分实用,比如Android中常用的Float转换为dp:

val Float.dp
    get() =  get() = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, this, Resources.getSystem().displayMetrics)

3.4 可变参数和中缀调用

vararg:可变参数。只需要注意一点

fun test(vararg string: String){
    test2(*string)//这里传递可变参数需要添加*
}

fun test2(vararg string: String){
    
}

中缀调用:

//允许使用中缀符号调用函数,函数会返回一个Pair对象
infix fun Any.with(other:Any) = Pair(this,other)
val c = "3" with 4 // c为pair对象
val (f, s) = "3" with 4 //val (f,s)称为解构声明,将Pair的first给f,second给s.
println("f:$f,s:$s")

//输出结果
f:3,s:4

4.程序逻辑控制

4.1带返回值的if

kotlin没有三目运算符,取而代之的是带返回值的if语句。而且是把每一个条件中的最后一行代码作为返回值

//Kotlin 中把每一个条件中的最后一行代码作为返回值
fun largeNumber(number1: Int,number2: Int) : Int{
    return if(number1 > number2){
      	number1
    }else {
      	number2
    }
}

//根据上面学习的语法糖和 Kotlin 类型推导机制,我们还可以简写 largeNumber 函数,最终变成了这样
fun largeNumber(number1: Int,number2: Int) = if(number1 > number2) number1 else number 2

4.2带返回值的when

类比 Java 中的 Switch 语句学习,Java 中的 Switch 并不怎么好用:

①Switch 语句只能支持一些特定的类型,如整型,短于整型,字符串,枚举类型。如果我们使用的并非这几种类型,Switch 并不可用

②Switch 语句的 case 条件都要在最后加上一个 break

这些问题在 Kotlin 中都得到了解决,而且 Kotlin 还加入了许多强大的新特性:

when 条件语句也是有返回值的,和 if 条件语句类似,条件中的最后一行代码作为返回值

when 条件语句允许传入任意类型的参数

when 条件体中条件格式:匹配值 -> { 执行逻辑 }

⑥when 条件语句和 if 条件语句一样,当条件体里面只有一行代码的时候,条件体的 {} 可省略

//when 中有参数的情况
fun getScore(name: String) = when (name) {
    "tom" -> 99
    "jim" -> 80
    "lucy" -> 70
     else -> 0
}

//when 中无参数的情况,Kotin 中判断字符串或者对象是否相等,直接使用 == 操作符即可
fun getScore(name: String) = when {
    name == "tom" -> 99
    name == "jim" -> 80
    name =="lucy" -> 70
    else -> 0
}

4.3循环语句

kotlin中主要有两种循环语句:while和for-in。其中while和Java中使用一致。

而kotlin中的for-in则比Java中更为方便易用。

for-in:
//使用 .. 表示创建两端都是闭区间的升序区间[0,10]
    for (i in 0..10){
        print("$i ")
    }
//打印结果
0 1 2 3 4 5 6 7 8 9 10

for-until:
//使用 until 表示创建左端是闭区间右端是开区间的升序区间[0,10)
    for (i in 0 until 10){
        print("$i ")
    }
//打印结果
0 1 2 3 4 5 6 7 8 9
for-downTo:
//使用 downTo 表示创建两端都是闭区间的降序区间[10,0]
    for (i in 10 downTo 0){
        print("$i ")
    }
//打印结果
10 9 8 7 6 5 4 3 2 1 0
步进:
//使用 downTo 表示创建两端都是闭区间的降序区间,每次在跳过3个元素
    for (i in 10 downTo 0 step 3){
        print("$i ")
    }
迭代list
//使用withIndex迭代list
 val list = arrayListOf("10","11","100")
 for ((index,element) in list.withIndex()){//解构申明
        println("$index:$element")
 }

//打印结果
0:10
1:11
2:100
迭代map
 val map = mapOf(1 to "Java", 2 to "Kotlin", 3 to "Flutter")//中缀调用
 for ((key, value) in map) { //解构
     println("$key:$value")
 }

//打印结果
1:Java
2:Kotlin
3:Flutter

5.类

和Java一样,类的定义如下

 class Base {
     var num = 1
    }

但是意义却不太一样。

5.1可见性

**Kotlin中,默认类的可见性为public以及final的。**内部类默认为static的,用inner标记非静态内部类。

①Kotlin 中可见性
  • private :私有,本类内部可见
  • protected :子类可见
  • internal :模块内可见
  • public :默认,公有
②对比 Java
  • private :私有,本类内部可见
  • protected :子类可见
  • default :默认,包内可见
  • public :公有

单个构造函数&私有构造

class Response private constructor(val code: Int, val msg: String){
         override fun toString(): String {
            return "code:$code,msg:$msg"
        }
    }

多个构造函数

//非open不可被继承 
class Response {
        val code: Int
        val msg: String

        constructor(code: Int) {
            this.code = code
            msg = ""
        }

        constructor(code: Int, msg: String) {
            this.code = code
            this.msg = msg
        }

        override fun toString(): String {
            return "code:$code,msg:$msg"
        }
    }

其中code和msg的getter会自动生成。

kotlin中也是单继承多实现,共同存在时,继承类写到第一位,后面追加逗号跟上接口接口。

public class String : Comparable<String>, CharSequence{
    ...
}

密封类(private构造,默认open)

sealed class Expr {

    class Num(val value: Int) : Expr()
    class Sum(val left: Expr, val right: Expr) : Expr()

}

5.2Object关键字

①类单例
object SingleTon {

    @JvmStatic
    fun isEmpty(str: String) = str.isEmpty()
}

反编译成Java

public final class SingleTon {
   public static final SingleTon INSTANCE;

   @JvmStatic
   public static final boolean isEmpty(@NotNull String str){
      Intrinsics.checkParameterIsNotNull(str, "str");
      CharSequence var1 = (CharSequence)str;
      boolean var2 = false;
      return var1.length() == 0;
   }

   private SingleTon() {
   }

   static {
      SingleTon var0 = new SingleTon();
      INSTANCE = var0;
   }
}

根据面可以看出,object单独修饰一个类,表示为静态代码块实现的单例

@JvmStatic修饰的方法,kotlin会在其方法上添加static关键字,而static关键字在kotlin中是不存在的。

②匿名类
test(object : Listener {

            })
interface Listener{

    }

kotlin中没有new关键字,所以匿名类用object实现。

③伴生对象

kotlin没有static,那么如何实现静态变量以及常量等的定义和使用呢?

答案是伴生对象

class UserManager {

    companion object{
        val USER_TYPE = 0x01
    }
}

上面的companion object会生成一个内部类Companion,并添加返回USER_TYPE的静态getter,如下

public final class UserManager {
   private static final int USER_TYPE = 1;
   public static final UserManager.Companion Companion = new UserManager.Companion((DefaultConstructorMarker)null);

   public static final class Companion {
      public final int getUSER_TYPE() {
         return UserManager.USER_TYPE;
      }
...
   }
}

PS:const关键字

const关键字只能用在静态类中, 只能与val连用,即const val,而且只能修饰基本类型。意义为编译期常量,在用到的地方替换为该常量的值。如下:

object SingleTon {
    const val str = "const"
}


fun test(): String? {
    return SingleTon.str
}

其中test反编译Java如下:

 public final String test() {
     return "const";
}

可以看到kotlin对const常量做了内联

5.3类委托

equals

在Java中,可以使用==来比较基本数据类型和引用类型,基本数据类型比较的是值,引用类型上比较的是引用。在kotlin中==就等于调用Java中的equals。如果需要比较引用则需要用===。

by关键字

装饰器模式的代码通常就较为模板,kotlin中可以利用by关键字来实现类的委托。比如:

class MyList<T> : List<T> by ArrayList() {
    //这里面默认利用ArrayList实现了List的所有接口
}

转换成Java:

public final class MyList implements List, KMappedMarker {
   // $FF: synthetic field
   private final ArrayList $$delegate_0 = new ArrayList();
    ...
   public Object get(int index) {
      return this.$$delegate_0.get(index);
   }
    ...
}

当然,我们也可以通过重写来实现自己的逻辑。

by也可以用来实现延迟加载:

private val arr by lazy { MyList<String>() }

它的实现是double-check的懒加载方式,如下:

private class SynchronizedLazyImpl<out T>(initializer: () -> T, lock: Any? = null) : Lazy<T>, Serializable {
    private var initializer: (() -> T)? = initializer
    @Volatile private var _value: Any? = UNINITIALIZED_VALUE
    // final field is required to enable safe publication of constructed instance
    private val lock = lock ?: this

    override val value: T
        get() {
            val _v1 = _value
            if (_v1 !== UNINITIALIZED_VALUE) {
                @Suppress("UNCHECKED_CAST")
                return _v1 as T
            }

            return synchronized(lock) {
                val _v2 = _value
                if (_v2 !== UNINITIALIZED_VALUE) {
                    @Suppress("UNCHECKED_CAST") (_v2 as T)
                } else {
                    val typedValue = initializer!!()
                    _value = typedValue
                    initializer = null
                    typedValue
                }
            }
        }

    override fun isInitialized(): Boolean = _value !== UNINITIALIZED_VALUE

    override fun toString(): String = if (isInitialized()) value.toString() else "Lazy value not initialized yet."

    private fun writeReplace(): Any = InitializedLazyImpl(value)
}

6.lambda

6.1语法

lambda表达式,本质上就是可以传递给其他函数的一小段代码。

kotlin中简单lambda:

button.setOnclickListener{
    
}

kotlin中lambda始终被花括号{}包围。可以把lambda表达式存储在变量中:

val sum = { x:Int,y:Int -> x + y }
println(sum(1,2))
 
3

kotlin中,lambda作为最后一个参数可以把它放到()后面如下1;如果只有lambda作为参数,可以省略(),如下2。

list.maxBy({it.length})
list.maxBy(){it.length}//1
list.maxBy{it.length}//2

6.2集合函数API

比如list过滤:

val list = arrayListOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
list.filter { it % 2 == 0 }
    .forEach { print(it) }

//打印结果
246810

类似的还有:

all:全部匹配返回true

any:存在一个匹配返回true

count:返回匹配的数量

firstOrNull:第一个匹配,没有返回null

first:第一个匹配,没有抛出异常

find:找到第一个匹配 等于 firstOrNull

还有map,flatmap,groupby...

基本涵盖了RxJava的常用操作符。

apply,let,also...

apply改变this指向调用者。方便各种操作,返回调用者

 val str = "123456"
 val rec = str.apply {
     println("lastChar:${get(lastIndex)}")
  }
  println("rec:$rec")


//打印结果
lastChar:6
rec:123456

with改变this指向参数,返回lambda最后一行结果

let创建局部变量,返回最后一行结果。

   val str :String ? = "123456"
   val res = str?.let {
       println("it:$it")
                "return"
    }
    println("res:$res")

//打印结果
it:123456
res:return

also:创建it,返回调用者

run:改变this指向调用者,返回最后一行

类似的语法糖takeIf,repeat等等,都在Standard.kt中有定义。

二、深入理解

1.kotlin类型系统

1.1可空性

kotlin中类型定义如果没有添加为可空,当它接受到一个null时,kotlin会在运行时抛出ERROR:Type mismatch的错误。

当一个类规定为可空,可以使用安全调用?.,后面可以跟上Elvis运算符?:。标识在前面?.调用者为null时执行。

val str :String ? = "123456"
str?.get(str.lastIndex) ?: toast("str is null") //toast会在str为null时执行

安全转换:as?

is:检查类型,可以自动转型

 val obj: Any = "str"
if (obj is String) {
   println("obj:$obj")//obg自动转型为string
}

//打印结果
obj:str

as:类型转换,as?安全类型转换

val obj: Any = "str"
(obj as? String)?.print()//在obj为String时才会执行后面的语句,print为本地定义的拓展函数

//打印结果
str

!!非空断言

让kotlin在对象为空的时候抛出异常。

   val obj: Any? = "str"
   obj!!.toString()
   obj.hashCode()//不需要再加!!,kotlin编译器自动检查

2.运算符重载

data class Point(val x: Int, val y: Int) {
    operator fun plus(other: Point): Point {
        return Point(x + other.x, y + other.y)
    }
}

  val p1 = Point(1,2)
  val p2 = Point(5,6)
  val p = p1 + p2
  println("p:$p")//自动调用toString

//打印结果
p:Point(x=6, y=8)

可用于重载的运算符:

表达式函数名
a * btimes
a / bdiv
a % bmod
a + bplus
a - bminus

PS:位运算也有自己的符号

运算符操作
shl带符号左移
shr带符号右移
ushr无符号右移
and按位与
or按位或
xor按位异或
inv按位取反

3.lambda作为形参

无其他参数:

 fun exec(func:()->Unit){
     func.invoke()
 }
 exec { 
   println("hello world")
 }

带其他参数:

 fun exec(msg: String,func:(msg:String)->Unit){
      func.invoke(msg)
 }
 exec("hello world") {msg->
    println(msg)
 }

以上的lambda是作为不能为空的形参。如果为空,需要将其定义用()?包裹。如下:

 fun exec(msg: String,func:((msg:String)->Unit)?){
     func?.invoke(msg)
 }

lambda作为参数传递虽然好,但是其实现传递的还是对象(匿名类),在每一次调用都会创建一个对象,如何避免这部分开销提升性能?

答案是内联函数。

4.内联函数系列

inline

当一个函数声明为inline时,它的函数体是内联的--换句话说,函数体会被直接替换到函数被调用的地方,而不是被正常调用。

inline fun inlineTest(inlineFunc:()->Unit){
    println("before invoke")
    inlineFunc()
    println("after invoke")
}

  inlineTest {
        println("hello world")
    }

//打印结果
before invoke
hello world
after invoke

to Java:

String var2 = "before invoke";
System.out.println(var2);
String var5 = "hello world";
System.out.println(var5);
var2 = "after invoke";
System.out.println(var2);
内联函数的限制

当我们在内联的lambda中添加return,代码就不正确运行了,它在中途就return掉了。

  inlineTest {
        println("hello world")
        return
    }
 println("code works as well")

//打印结果
before invoke
hello world

针对这种情况,我们需要添加return的范围

  inlineTest {
        println("hello world")
        return@inlineTest
    }
   println("code works as well")

//打印结果
before invoke
hello world
after invoke
code works as well

当然也可以用其他方法,比如noinline

noinline

noinline 修饰的是 inline 方法中的 lambda 参数。noinline 用于我们不想让 inline 特性作用到 inline 方法的某些 lambda 参数上的场景。

    // Kotlin
fun main(args: Array<String>) {
        val methodName = "main"
        multiplyByTwo(5) {
            result: Int -> println("call method $methodName, Result is: $result")
        }
    }

    inline fun multiplyByTwo(
            num: Int,
            noinline lambda: (result: Int) -> Unit): Int {
        val result = num * 2
        lambda.invoke(result)
        return result
    }

反编译的结果是:

 public final void main(@NotNull String[] args) {
      Intrinsics.checkParameterIsNotNull(args, "args");
      final String methodName = "main";
      byte num$iv = 5;
      Function1 lambda$iv = (Function1)(new Function1() {
         // $FF: synthetic method
         // $FF: bridge method
         public Object invoke(Object var1) {
            this.invoke(((Number)var1).intValue());
            return Unit.INSTANCE;
         }

         public final void invoke(int result) {
            String var2 = "call method " + methodName + ", Result is: " + result;
            boolean var3 = false;
            System.out.println(var2);
         }
      });
      int $i$f$multiplyByTwo = false;
      int result$iv = num$iv * 2;
      lambda$iv.invoke(result$iv);
   }

   public final int multiplyByTwo(int num, @NotNull Function1 lambda) {
      int $i$f$multiplyByTwo = 0;
      Intrinsics.checkParameterIsNotNull(lambda, "lambda");
      int result = num * 2;
      lambda.invoke(result);
      return result;
   }

可以看到, 因为使用了 noinline 修饰了 lambda,所以,编译器使用了匿名内部类的方式来处理这个 lambda,生成了一个 Function1 对象。

crossinline

是不是有了 inlinenoinline,对于我们开发人员来讲就够了呢?就满足了呢?显然不是的。考虑一种情况,我们既想让 lambda 也被 inline,但是又不想让 lambda 对调用方的控制流程产生影响。这个产生影响,可以是有意识的主动控制,但是大多数情况下是开发人员的不小心导致的。我们知道 java 语言是一个编译型语言,如果能在编译期间对这种 inline lambda 对调用方产生控制流程影响的地方进行提示甚至报错,就万无一失了。

crossinline 就是为了处理这种情况而产生的。crossinline 保留了 inline 特性,但是如果想在传入的 lambda 里面 return 的话,就会报错。return 只能 return 当前的这个 lambda

    // Kotlin
    fun main(args: Array<String>) {
        val methodName = "main"
        multiplyByTwo(5) {
            result: Int -> println("call method $methodName, Result is: $result")
            return@multiplyByTwo
        }
    }

如面代码所示,必须 return@multiplyByTwo,而不能直接写 return

5.泛型

5.1泛型的基本用法

1)、首先我们解释下什么是泛型,泛型就是参数化类型,它允许我们在不指定具体类型的情况下进行编程。我们在定义一个类,方法,或者接口的时候,给他们加上一个类型参数,就是为这个类,方法,或者接口添加了一个泛型

//1、定义一个泛型类,在类名后面使用 <T> 这种语法结构就是为这个类定义一个泛型
class MyClass<T>{
  	fun method(params: T) {
      
    }
}
//泛型调用
val myClass = MyClass<Int>()
myClass.method(12)

//2、定义一个泛型方法,在方法名的前面加上 <T> 这种语法结构就是为这个方法定义一个泛型
class MyClass{
    fun <T> method(params: T){

    }
}
//泛型调用
val myClass = MyClass()
myClass.method<Int>(12)
//根据 Kotlin 类型推导机制,我们可以把泛型给省略
myClass.method(12)

//3、定义一个泛型接口,在接口名后面加上 <T> 这种语法结构就是为这个接口定义一个泛型
interface MyInterface<T>{
    fun interfaceMethod(params: T)
}

2)、为泛型指定上界,我们可以使用 <T : Class> 这种语法结构,如果不指定泛型的上界,默认为 Any? 类型

class MyClass{
  	//我们指定了泛型的上界为 Number, 那么我们就只能传入数字类型的参数了
  	fun <T : Number> method(params: T) {
   
    }
}

实化泛型

注意JVM上的泛型一般是通过泛型擦除实现的,所以kotlin上的泛型也是如此。但是kotlin可以通过内联函数来实化泛型(reified)

inline fun <reified T> Iterable<*>.filterIsInstance(): List<T> { //reified声明后不再进行擦除
    val des = mutableListOf<T>()
    forEach {
        if (it is T) des.add(it)
    }
    return des
}

fun main(args: Array<String>) {
    val items = arrayListOf("one", 2, "three")
    println(items.filterIsInstance<String>())
}

//打印结果
[one, three]

kotlin为什么可以避免泛型?因为kotlin编译器把实现内联函数的字节码插入每一次调用的地方的时候,带上了实际作为参数的类型。

5.2协变和逆变

在Java中,在 Source 类型的变量中存储 Source 实例的引用是极为安全的——没有消费者-方法可以调用。但是 Java 并不知道这一点,并且仍然禁止这样操作:

void demo(Source<String> strs) {
  Source<Object> objects = strs; // !!!在 Java 中不允许
  // ……
}

为了修正这一点,我们必须声明对象的类型为 Source<? extends Object>,这是毫无意义的,因为我们可以像以前一样在该对象上调用所有相同的方法,所以更复杂的类型并没有带来价值。但编译器并不知道。

所以在Kotlin中,有一种方法向编译器解释这种情况。这称为声明处型变:我们可以标注Source 的参数类型T 来确保它仅从Source 成员中返回(只读取,相当于Java中? extends T)。为此,kotlin提供out 修饰符。

//kotlin
interface Source<out T> {
    fun nextT(): T
}
fun demo(strs: Source<String>) {
    val objects: Source<Any> = strs // 这个没问题,因为 T 是一个 out-参数
    // ……
}

简单来说:消费者用 in,生产者用 out

一般原则是:当一个类C 的类型参数T 被声明为out 时,它就只能出现在C 的成员的输出位置,但回报是C 可以安全的作为C 的超类。 另外除了 out,Kotlin 又补充了一个型变注释:in。它使得一个类型参数逆变:只可以被写入而不可以被读取(相当于Java中 ? super T)。逆变类型的一个很好的例子是 Comparable:

interface Comparable<in T> {
    operator fun compareTo(other: T): Int
}

fun demo(x: Comparable<Number>) {
    x.compareTo(1.0) // 1.0 拥有类型 Double,它是 Number 的子类型
    // 因此,我们可以将 x 赋给类型为 Comparable <Double> 的变量
    val y: Comparable<Double> = x // OK!
}

6.DSL

6.1DSL 介绍

DSL英文全称:domain specific language,中文翻译即领域特定语言,例如:HTML,XML等 DSL 语言

特点

  • 解决特定领域的专有问题
  • 它与系统编程语言走的是两个极端,系统编程语言是希望解决所有的问题,比如 Java 语言希望能做 Android 开发,又希望能做后台开发,它具有横向扩展的特性。而 DSL 具有纵向深入解决特定领域专有问题的特性。

总的来说,DSL 的核心思想就是:“求专不求全,解决特定领域的问题”。

6.2Kotin DSL

首先介绍一下Gradle:Gradle 是一个开源的自动化构建工具,是一种基于 Groovy 或 Kotin 的 DSL。我们的 Android 应用就是使用 Gradle 构建的,因此后续写脚本,写插件,我们可以使用 Kotlin 去编写,而且 AndroidStudio 对 Kotlin 的支持很友好,各种提示,写起来很爽。

比如:

例1-请求回调:

open class RequestCallback<T> : Callback<T> {

    private val builder: Builder<T>
    
	...

    private fun onSuccess(data: T) {
        builder.onSuccess?.invoke(data)
    }

    private fun onError(code: Int, msg: String?) {
        builder.onError?.invoke(code, msg) ?: toast(msg)
    }

    private fun onFinished() {
        decrement()
        builder.onFinished?.invoke()
    }

    class Builder<T> {
        internal var onSuccess: ((data: T) -> Unit)? = null
        internal var onError: ((code: Int, msg: String?) -> Unit)? = null
        internal var onFinished: (() -> Unit)? = null

        fun onSuccess(func: ((data: T) -> Unit)?) {
            onSuccess = func
        }

        fun onError(func: ((code: Int, msg: String?) -> Unit)?) {
            onError = func
        }

        fun onFinished(func: (() -> Unit)?) {
            onFinished = func
        }
    }
}

   fun <T> getCallback(refresh:Boolean = false,dsl: RequestCallback.Builder<T>.() -> Unit): RequestCallback<T> {
     ...
        return RequestCallback(b)
    }

//使用
   api.fetchQrCode(orderNo, faceSuccess).enqueue(getCallback {
            onSuccess {
                ...
            }
            onError { _, msg ->
               ...
            }
        })

例2-拓展系统Animator监听:

class AnimationCallbackBuilder {
    internal var onRepeat: ((animation: Animator?) -> Unit)? = null
    internal var onCancel: ((animation: Animator?) -> Unit)? = null
    internal var onStart: ((animation: Animator?) -> Unit)? = null
    internal var onEnd: ((animation: Animator?) -> Unit)? = null

    fun onRepeat(function: ((animation: Animator?) -> Unit)?) {
        this.onRepeat = function
    }

    fun onCancel(function: ((animation: Animator?) -> Unit)?) {
        this.onCancel = function
    }

    fun onStart(function: ((animation: Animator?) -> Unit)?) {
        this.onStart = function
    }

    fun onEnd(function: ((animation: Animator?) -> Unit)?) {
        this.onEnd = function
    }
}

fun AnimatorSet.registerCallback(listener: AnimationCallbackBuilder.() -> Unit) {
    val mListener = AnimationCallbackBuilder().also(listener)
    addListener(object : Animator.AnimatorListener {
        override fun onAnimationRepeat(animation: Animator?) {
            mListener.onRepeat?.invoke(animation)
        }

        override fun onAnimationEnd(animation: Animator?) {
            mListener.onEnd?.invoke(animation)
        }

        override fun onAnimationCancel(animation: Animator?) {
            mListener.onCancel?.invoke(animation)
        }

        override fun onAnimationStart(animation: Animator?) {
            mListener.onStart?.invoke(animation)
        }
    })
}

//使用

    set.registerCallback {
        onEnd {
          
        }
    }


在kotlin中,规定一个方法返回一个可空类型,那么在每次调用的时候都需要打个?判断是否为空是挺麻烦的。比如:

//当然也可以更加定义自己的apply
inline fun <T> T.exist(block: T.() -> Unit) = block()
inline fun curActivity(block: Activity.() -> Unit) = ActivityCollector.currentActivity()?.let(block)

//使用
item?.exist{
    this.toString()//这里改变了this的指向,同时this不会为空
}

curActivity{
    startActivity(intent)//同上,this为ActivityCollector.currentActivity()获取到的不为空的activity,
    //**当然为空的时候,则不执行,需要警惕**
}

其他

带默认实现的接口,比如截图权限配置:

interface ICaptureView {

    fun canCapture() = false
}

不能截图的类只需要实现接口,能够截图的实现重写即可。

Ktorm

Ktorm 是什么?

Ktorm 是直接基于纯 JDBC 编写的高效简洁的轻量级 Kotlin ORM 框架,它提供了强类型而且灵活的 SQL DSL 和方便的序列 API,以减少我们操作数据库的重复劳动。当然,所有的 SQL 都是自动生成的。Ktorm 基于 Apache 2.0 协议开放源代码,源码托管在 GitHub,如果对你有帮助的话,请留下你的 star:kotlin-orm/ktormGitHub Stars

特性

  • 没有配置文件、没有 xml、没有注解、甚至没有任何第三方依赖、轻量级、简洁易用
  • 强类型 SQL DSL,将低级 bug 暴露在编译期
  • 灵活的查询,随心所欲地精确控制所生成的 SQL
  • 实体序列 API,使用 filtermapsortedBy 等序列函数进行查询,就像使用 Kotlin 中的原生集合一样方便
  • 易扩展的设计,可以灵活编写扩展,支持更多运算符、数据类型、 SQL 函数、数据库方言等

ktorm-example.png

总结

kotlin相比Java有一些不少优势:

①Kotlin 语法更加简洁,使用 Kotlin 开发的代码量可能会比 Java 开发的减少 50% 甚至更多

②Kotlin 的语法更加高级,相比于 Java 老旧的语法,Kotlin 增加了很多现代高级语言的语法特性(语法糖),大大提升了我们的开发效率

③Kotlin 和 Java 是 100% 兼容的,Kotlin 可以直接调用 Java 编写的代码,也可以无缝使用 Java 第三方开源库,这使得 Kotlin 在加入了诸多新特性的同时,还继承了 Java 的全部财富

...

当然也有一些劣势:

①Kotlin在编译后会生成比Java更多的类,这可能会影响安装包的体积。

②Kotlin虽然和Java完全兼容,但是部分功能在Java下使用并不太方便美观,所以库作者如果对代码(美观)有严格要求,最好在外层包裹一层Java调用层。

HttpManager.INSTANCE.create(MineApi.class).getCashRecordList(map).enqueue(new RequestCallback<>(baseResponseBuilder -> {//dsl会变成如下,然后lambda变成Function,Function1,Function2等(根据参数位数)
            baseResponseBuilder.onSuccess(new Function1<BaseResponse<ArrayList<CashRecordListBean>>, Unit>({
                @Override
                public Unit invoke(BaseResponse<ArrayList<CashRecordListBean>> arrayListBaseResponse) 
                    ...
                    return null;
                }
            });
            return null;
        }));

③Kotlin的KAPT需要生成APT可以解析的stub(Java代码),然后再利用APT生成代码,这影响了KAPT的性能,从而拖慢Kotlin项目整体编译速度。

对应kotlin也发布的alpha版原生处理的KSP,号称带来了两倍的速度提升[Announcing Kotlin Symbol Processing (KSP) Alpha]

KSP offers a powerful and yet simple API for parsing Kotlin code directly, dramatically reducing the build speed tax imposed by KAPT’s stub generation. Indeed, initial benchmarks with the Room library show that KSP is approximately 2x faster than KAPT.

总的来说,Kotlin带来的优点是大于缺点的,简洁易用的语法,代码量大大减少。实用的类型系统,避免了绝大部分的空指针异常。而且与Java完全兼容,谷歌宣布Kotlin First后,Android开发迟早也是要迁移到Kotlin的,再加上随着KSP的发布,kotlin编译缓慢的问题得到解决,生态会越来越好。

参考资料

《kotlin实战》 [俄]Dmitry Jemerov , Svetlana Isakova著

Kotlin inline, noinline and crossinline_

"Kotlin"系列: 一、Kotlin入门

由Kotlin 中关键字out和in和Java中的泛型的比较学习

[Announcing Kotlin Symbol Processing (KSP) Alpha]

OneMoreThing

Kotlin发布了Coroutines库,声称是基于线程的协程。下一期预览:

Kotlin真的实现了基于Jvm的协程了?

Kotlin协程里面的suspend是什么意思?

如何优化多重Callback嵌套?

Retrofit内部如何实现兼容协程?

如何利用协程完成完善的页面的生命周期管理(请求、后台任务)?