学习 Kotlin 02

本文中的所有示例仅为了演示, 没有实际意义, 更不代表生产环境的代码应该这样写.

基本数据类型

Kotlin 中, 一切都是对象 (Everything is an object), 包括 Java 中的基本数据类型 (primitive types), 这样我们就可以直接调用它们的方法或者访问它们的属性. 因此在 Java 中属于基本数据类型的字面量在 Kotlin 中也能够直接调用其方法:

// kotlin
println(1.toString())   // 1
println(1.equals(2))    // false
数值型 (Number)

Kotlin 数值数据类型包括以下 6 种: Double, Float, Long, Int, Short, Byte, 它们的长度分别为 64, 32, 64, 32, 16, 8.

  • Char 类型不属于数值类型

    注意到在 KotlinChar 类型不属于数值类型. 因此像这样的 Java 代码在 Kotlin 中将不会编译通过:

// java
char a = 65;
System.out.println(a);  // a
// kotlin
val a: Char = 65    // 编译失败, 因为赋值符号两边类型不一致

Kotlin 为所有数值类型都提供了 toChar 方法, 加上 “原始数据类型可以直接调用方法” 我们同样可以方便地达到最初的目的.

// kotlin
val a = 65.toChar()
println(65.toChar())    // A
println(66L.toChar())   // B
println(66.4.toChar())  // B
println(66.6.toChar())  // B
  • 没有隐式转换

    除了 Char 类型不属于数值类型之外, kotlin 还不会将低精度的数值类型自动提升成高精度的数值类型. 像这样常见的 java 代码在 kotlin 中同样会编译失败.

// java
int i = 1;
// ...
long l = i;
// kotlin
var i: Int = 1
// ...
var l: Long = i     // 失败, 因为 Int 类型不会被自动提升为 Long 类型

我们需要显式地告诉编译器需要把 i 作为 Long 类型. 为了方便进行数据类型转换, Kotlin 中所有数值类型 (Number 类的子类) 都实现了 Number 类的转换方法:

public abstract class Number {
    public abstract fun toDouble(): Double
    public abstract fun toFloat(): Float
    public abstract fun toLong(): Long
    public abstract fun toInt(): Int
    public abstract fun toChar(): Char
    public abstract fun toShort(): Short
    public abstract fun toByte(): Byte
}

即使 Char 类型不是 Number 类型的子类, 它也提供了和以上方法签名一样的系列转换函数.

  • 字面数值表示方法

    Java 中, 表示数值字面量的方式有 3 种, 即 十六进制, 十进制, 和 八进制, 直到 Java7 , 才可以使用 二进制 表示一个数值字面量.

    int eight = 010;              // 八进制, 以 0 为前缀
    System.out.println(eight);      // 8
    int sixteen = 0x10;               // 十六进制, 以 0x 为前缀
    System.out.println(sixteen);    // 16
    int ten = 10;                 // 十进制, nothing special
    System.out.println(ten);        // 10
    int two = 0b10;                   // 二进制, 以 0b 为前缀. (since 1.7)
    System.out.println(two);      // 2

    Kotlin 则去掉了八进制表示方法, 只留下其他三种表示方法, 其表示方法与 Java 一致, 不再赘述.

字符型 (Character)

字符型不能直接当做整型进行运算和操作

布尔型 (Boolean)

Nothing special

字符串 (String)

Kotlin 中的字符串与 Java 中的字符串一样都是不可变的, 不过由于 Kotlin 的其他特性, Kotlin 中的字符串的操作比 Java 中对字符串的操作要方便和丰富很多.

  • 下标操作

    由于 Kotlin 敞开了对操作符重载的大门, 现在可以通过 str[n] 获取字符串 str 的第 n 个字符了, 但因为字符串确实是不可变的, 所以无论如何也无法使用 str[n] = 'a' 改变字符串 str 的第 n 个字符:

    val str = "abc"
    println(str[1])
    str[1] = 'd'  // 编译失败, 不能改变字符串

    与字符串关系密切的还有 StringBuilderStringBuffer, 但它们是可变的, 因此使用下标改变它们是可行的:

    val sb = StringBuilder("abcd")
    sb[0] = 'd'
    println(sb)       // dbcd
  • 字符串字面量 (literals)

    Kotlin 中字符串的字面量表示方法除了从 Java 继承来最基本的表示方法之外, 还学 (chao) 习了 python 的表示方法. 像 val helloWorld = "Hello \n world" 这样的表示方法与 Java 别无二致, 但想在 Java 中构建像下面这样的 JSON 字符串用于测试 JSON 解析工具或想在字符串中存放代码时, 可能就有点蛋疼了.

    private String getFansLoginInfo() {
    String returnBody = "{\n" +
      "\"snsUid\":\"fansj\",\n" +
      "\"snsPlatform\":\"tw\",\n" +
      "\"nickname\":\"Groupy\",\n" +
      "\"avatar\":\"aa\",\n" +
      "\"sign\":\"aa\",\n" +
      "\"introduce\":\"\"\n" +
      "}";
    
    return returnBody;
    }
    public void printMain() {
    System.out.println("\t\tpublic static void main (String[]args){\n" +
      "\t\t\tPersonUtil.persisted(new Person());\n" +
      "\t\t}");
    }

    Kotlin 可以使用原始字符串 (raw string) 来表示复杂的字符串字面量, 使用 3 个双引号包围:

    fun mainMethodInString(): String {
    return """
      public static void main(String[] args) {
        PersonUtil.persisted(new Person());
    }
    """
    }

    需要注意的是在 raw string 中, 没有转义字符的概念, 因此诸如 \n,\t 等都会原样输出.

  • 字符串模板 (String Template)

    Kotlin 字符串模板语法与 Swift 非常类似, 区别是后者使用 \() 在字符串中添加表达式, 而 Kotlin 使用 ${} , 当大括号中的表达式为简单变量名时, 大括号可以省略:

    fun hello(name: String) : String {
    return "Hello $name"
    }

    字符串模板也可以在 raw string 中使用, 但 raw string 不支持 \ 转义, 因此如果想输出 $ 本身, 请使用 ${'$'}.

数组 (Array)
  • kotlin 使用 Array 表示数组, 要声明一个 Int 数组 a, 可以使用

    var a: Array<Int>

    也可以使用

    var a: IntArray

    避免自动装箱拆箱的代价. 要快速声明和初始化一个 Int 数组, 可以使用便捷方法 arrayOf(), 同样为了避免装箱拆箱的代价, kotlin 还提供了原始数据类型的便捷操作, var array = intArrayOf(1, 2, 3) . 对于 6 种数据类型, kotlin 都提供了相应的方法 xxxArrayOf .

控制流

if 表达式
  • kotlinif 语句是一个表达式, 也就是说 if 语句拥有返回值. 因为 if 语句已经提供了三目运算符的功能, kotlin 没有三目运算符号:

    // java
    int absA = a >= 0 ? a : -a;
    // kotlin
    val absA = if (a >= 0) a else -a

    if 分支还可以是代码块, 如果不嫌麻烦的话上面的代码也可以写成:

    // kotlin
    var absA: Int
    if (a >= 0) {
    absA = a
    } else {
    absA = -a
    }

    如果 if 分支是一个代码块, 那么其最后一个表达式就是该分支的返回值.

    // kotlin
    var absA = if (a >= 0) {
    println("a is positive")
    a
    } else {
    println("a is negative")
    -a
    }

    Note : 当把 if 作为表达式 (即在 if 中返回一个值并赋值给某个变量) 时, 必须提供一个 else 分支.

when 表达式

kotlin 中使用 when 语句代替 java 中的 switch 语句, 与 if 类似, when 也是一个表达式. kotlin 中的 when 表达式比 java (甚至其他任何语言) 的 switch 语句功能都要强大. when 表达式的语法表示如下:

when (used by atomicExpression)
  : "when" ("(" expression ")")? "{"
        whenEntry*
    "}"
  ;
whenEntry (used by when)
  : whenCondition{","} "->" controlStructureBody SEMI
  : "else" "->" controlStructureBody SEMI
  ;
whenCondition (used by whenEntry)
  : expression
  : ("in" | "!in") expression
  : ("is" | "!is") type
  ;
  • 不像 javaswitch 语句要求所有 case 都必须是常量表达式, when 表达式中的 case 项可以是任何与 when 参数类型一致的常量, 变量, 表达式, 函数调用或类型判断 (is,!is) 和包含关系判断 (in,!in):

    // kotlin
    open class Person(open val age: Int = 0)
    class Employee(override val age: Int = 0, val leader: Person?) : Person(age) {
    fun work() {
      println("working ")
    }
    }
    
    val person = Person(22)
    val lisi = Employee(21, person)
    val elites = arrayOf(lisi)
    val superElites = arrayOf(lisi)
    val zhangsan = Employee(18, lisi)
    
    val description = when (lisi) {
    is Employee -> {
      lisi.work()
      "一个普通的员工"
    }
    in elites, in superElites -> "一个精英"
    zhangsan.leader -> "张三的领导"
    else -> "默默无闻"
    }
    println(description)  // 一个普通的员工

    尽管没有显式的 break 语句, 但是只要 when 表达式中按顺序从上而下有满足条件的分支成立, when 表达式就会返回.

  • when 作为语句时, 其分支代码块中的返回值相当于被忽略 (与 if 类似):

    // kotlin
    when {
    person === lisi -> println("此人正是李四")
    person === zhangsan.leader -> println("此人是张三的领导")
    zhangsan in elites -> println("张三是个人才啊")
    }

    kotlin== 相当于调用 equals 方法, === 相当于 java 中的 == , 即比较引用

for 循环

for 循环能够迭代任何提供了 iterator() 函数的对象. 其语法结构定义如下:

for (used by loop)
  : "for" "(" annotations (multipleVariableDeclarations | variableDeclarationEntry) "in" expression ")" controlStructureBody
  ;
  • 其中迭代变量可以使用注解, 也可以有多个迭代变量(构成元组), 这在遍历提供了 componentn 函数的对象集合上可以很方便的 destructure 对象, 典型的示例是在遍历 Map 和带下标遍历数组时:

    // kotlin
    for (@NotNull elite in elites) {
    println(elite)
    }
    
    val map = mapOf(1 to "one", 2 to "two", 3 to "three")
    for ((key, value) in map){
    println("$key 对应英文是 $value")
    }
    
    for ((index, value) in array.withIndex()) {
      println("the element at $index is $value")
    }
while 循环

Nothing special

函数

kotlin 中声明函数使用 fun 关键字, 大有一语双关之意. 声明函数的语法规则如下:

function (used by memberDeclaration, declaration, topLevelObject)
  : modifiers "fun"
      typeParameters?
      (type ".")?
      SimpleName
      typeParameters? valueParameters (":" type)?
      typeConstraints
      functionBody?
  ;
functionBody (used by getter, setter, function)
  : block
  : "=" expression
  ;
  • 首先是 modifiers 修饰符, 与 java 一样, kotlin 函数也有 public , protected , private 等访问权限修饰符和 abstract , final 等修饰符

  • 接着是关键字 fun

  • 然后是泛型参数, 这在声明泛型方法时可以用上:

    fun <T : Comparable<T>> max(n1: T, n2: T): T {
    return if (n1 > n2) n1 else n2
    }
  • 接着是方法名, 方法名前面是可选的 type. , 在使用 kotlin 给已有的类添加方法时, 需要指定该方法是添加在哪个类上的, 比如我们想给 String 类添加一个本地化的方法 localized , 我们需要指定 type 说明该方法是添加在 String 上的:

    fun String.localized(): String {
    return if (this == "hello") "你好" else "不懂"
    }
  • 接着是函数参数, kotlin 的参数声明使用 pascal 的写法, 即 name: type , 使用逗号分隔多个参数.

  • 接着是类型约束, 在声明泛型函数时有额外约束可以使用 where 语句指定

  • 最终是函数体, 函数体可以是代码块, 当函数体只有一行时, 直接用 = :

    fun <T:Comparable<T>> max(a: T, b: T ): T = if (a > b) a else b

中缀函数 (infix)

顾名思义, 中缀函数是指在调用函数时, 可以使用形如 a plus b 的中缀表达式形式调用的函数. 由于 kotlin 中缀函数的存在, 使得很多看似平常的函数调用看起来像魔法一样. 要能够使用中缀形式调用的函数需要满足以下几个条件:

  1. 必须是成员函数或者 extension 函数
  2. 有且只有一个参数
  3. 使用 infix 修饰符修饰

kotlin 类库中找几个中缀函数作为参考:

public infix fun shl(bitCount: Int): Int  // 左移位运算, 1 shl 2 = 4
public infix fun shr(bitCount: Int): Int  // 右移位运算, 4 shr 2 = 1
public infix fun ushr(bitCount: Int): Int // 无符号右移, -4 ushr 2 = 1073741823
public infix fun <A, B> A.to(that: B): Pair<A, B> = Pair(this, that)

最后一个函数是快速创建一个二元组的中缀函数, 其调用形式为 1 to "one", 由于这种方便的创建方式, kotlin 提供的快速创建一个 map 的函数也非常方便和简洁:

class ModelAndView(viewName: String, model: Map<String, Any?>)

fun index(): ModelAndView {
   // ...
   return ModelAndView("index", mapOf("name" to "kid", "age" to 23))
}
参数默认值/命名参数

kotlin 函数中参数可以有默认值, 在参数声明后使用 = <default> 为该参数指定默认值为 <default>:

fun sayHello(name: String = "world", times: Int = 1) {
    repeat(times) {
        println("Hello $name")
    }
}

sayHello()  // Hello world

在调用函数时, 对于已经指定了默认值的参数, 可以不传递参数. 不一定要按照函数参数的声明顺序进行传递参数, 但此时必须指定要传递的参数名称(参数的命名就是该函数的形参名), 有了该特性可以减少很多的重载函数, 如上函数 sayHello 可以按照如下调用:

sayHello(times = 2, name = "kid")
sayHello(times = 2)
sayHello(name = "kid")

我们将该特性用在 JavaBean 的构造函数中:

open class Person(var id: Int? = null, var name: String? = null, var age: Int? = null)

val p1 = Person(id = 1)
val p2 = Person(name = "kid", age = 23)
val p3 = Person(id = 2, age = 23)

若使用 java 实现相同的构造函数, 对于一个有三个字段的类, 要提供 (3 + 6 + 6)=15 个构造函数(如果参数类型不冲突的话).

Unit 返回值

kotlin 中, 当声明方法的返回值为 Unit 时, 表明该方法返回 Unit (不返回任何东西). Unit 类似于 java 中的 void . 对于返回 Unit 的函数, 其返回类型可以不指定, 其函数体也不需要显式的返回 Unit, 因此以下几个函数是等价的:

// kotlin

// 显式指定
fun a(): Unit {
    return Unit
}

// 不指定返回类型
fun b() {
    return Unit
}

// 不显式返回
fun c(): Unit {
}

高阶函数和 lambda

高阶函数是指接受函数作为参数或者返回函数作为返回值的函数.

官网上提供了一个比较复杂的例子详细说明了高阶函数 戳这里, 这里借用学习 swift 时经常被举例的另一个简单的例子介绍 kotlin 的高阶函数, 以供对比.

要实现一个函数 adder 接收一个数字参数 a, 然后返回一个新的函数, 这个新的函数接收一个数字参数, 返回该参数加上 a 之后的值. 比如说:

val addTen = adder(10)  // addTen 是一个函数
println(addTen(10))     // 20
val addTwo = adder(2)
println(addTwo(10))     // 12
函数作为返回值

kotlin 中, 将函数作为参数或者返回值时, 其类型的写法与在声明函数时写法有以下几点不同:

  1. 不需要 fun 关键字和函数名
  2. 参数列表和返回值之间的 : 使用 -> 代替

比如直接声明一个 addTwo 函数时是这样的:

fun addTwo(n: Int): Int { return n + 2 }

如果要将与这个函数签名一样的函数作为参数或者返回值, 则使用:

((n: Int) -> Int)

所以以上构建加法器的函数可以这样实现:

// kotlin
fun adder(n: Int): ((a: Int) -> Int) {  // 返回值是这种函数类型: ((a: Int) -> Int) 
  return fun (a: Int): Int {        // 将函数返回, 这里的函数写法与普通写法一样
    return a + n
  }
}
函数作为参数

如果上面的例子中, 我们传入的参数 a 也希望是通过传入一个函数计算出来的, 那么原来的参数 a 就需要使用一个函数作为参数:

// kotlin
fun adder(aFun: () -> Int): ((a: Int) -> Int) {
  return fun (a: Int): Int {
    return a + aFun()
  }
}

在调用的时候就需要传入一个函数作为参数:

// kotlin
val tenFun = fun(): Int { return 10 }
val addTen = adder(tenFun)
println(addTen(10))
lambda

以上调用含有函数作为参数的函数时略显麻烦, 因为要先创建一个函数, 赋值给 tenFun , 然后再把这个变量传递给函数 adder, 即使要传递的参数函数仅简单地返回 10. kotlin 中, 当在传递函数作为参数时, 可以传递一个 lambda 表达式, lambda 表达式有以下特点:

  1. 总是使用 {} 包围
  2. 如果 lambda 有参数, 则参数写在 -> 前面
  3. lambda 的内容写在 -> 后面
  4. lambda 的参数类型可以省略 (可以被推断出来)

将上面的例子中改用 lambda 进行传递函数:

// kotlin
val addTen = adder({ -> 10 })

因为该 lambda 没有参数, 因此 -> 可以被省略:

如果 lambda 表达式只有一个参数, 那么该参数也可以被省略, 在 lambda 内部可以使用 it 引用到该参数.

// kotlin
val addTen = adder({ 10 })

当该 lambda 表达式是函数的最后一个参数时, 该 lambda 表达式可以被提到括号外面:

// kotlin
val addTen = adder() { 10 }

如果该 lambda 表达式还是函数的唯一一个参数, 函数调用的 () 也可以省略掉:

// kotlin
val addTen = adder { 10 }

kotlin 的类库中有大量利用 lambda 表达式这种简洁表示方法的函数, 提供了很多类似语言本身提供的功能, 但实际是语法糖的 “特效”

kotlinjava 一样使用关键字 class 声明一个类. 但 kotlin 中的类的结构与 java 的不太一样, kotlin 声明一个类的语法规则如下:

class (used by memberDeclaration, declaration, topLevelObject)
  : modifiers ("class" | "interface") SimpleName
      typeParameters?
      primaryConstructor?
      (":" annotations delegationSpecifier{","})?
      typeConstraints
      (classBody? | enumClassBody)
  ;
primaryConstructor (used by class, object)
  : (modifiers "constructor")? ("(" functionParameter{","} ")")
  ;
classBody (used by objectLiteral, enumEntry, class, companionObject, object)
  : ("{" members "}")?
  ;

其中 class | interface 类名<泛型类型>java 类似, 后面有可选的 primaryConstructor , 注解 annotations 和类型约束 typeConstraints. 这些在 kotlin 中都称为类头部 class header , 紧接着才是大括号和类内容. 如果类中没有内容, 大括号也可以被省略.

// kotlin
class Test
  • 构造函数 - primary constructor

    上面说过, 在 class header 中可以有 primaryConstructor , 但一个类最多只能有一个 primaryConstructor , primaryConstructor 中可以接收参数, 用于构造类对象时的需要, 由于 primaryConstructorclass header 的一部分, 因此 primaryConstructor 没有包含代码的能力, 如果需要在构造对象时执行初始化代码, 使用初始化代码块, 与 java 中的初始化代码块不一样, kotlin 的初始化代码块多了 init 关键字修饰, 更加直白. primaryConstructor 同样可以有访问修饰符和注解:

    // kotlin
    class UserBean public @Inject constructor(name: String, age: Int) {
    var name: String? = null
    var age: Int? = null
    init {
      this.name = name
      this.age = age
    }
    }
    1. 其中 public 表示该构造函数的访问权限修饰符, 由于 primaryConstructor 的默认修饰符是 public , 因此可以省略;
    2. primaryConstructor 前面没有修饰符和注解 (即 @Inject ) 的时候, 关键字 constructor 也可以省略;
    3. 虽然 primaryConstructor 不能包含代码块, 但在其内部声明的参数却可以在 class body 中声明属性时作为初始化值使用;
    // kotlin
    // 简化后的代码
    class UserBean (name: String?, age: Int?) {
    var name: String? = name
    var age: Int? = age
    }

    为了在 primaryConstructor 中声明并且初始化属性, kotlin 还提供了更简便的方法:

    // kotlin
    class UserBean (var name: String?, var age: Int?) {
    // ...
    }
  • 构造函数 - secondary constructor

    由于 primary constructor 最多只能有一个, 当需要声明多个构造函数时, 就需要使用 secondary constructor . 为 UserBean 添加重载的构造函数, 当 age 未知时赋为 null :

    // kotlin
    class UserBean (var name: String?, var age: Int?) {
    constructor(name: String?) : this(name, null)
    }
    1. 所有 secondary constructor 都必须直接或间接的调用 primary constructor , 调用方法是在方法体前使用 : this() ;
    2. 当构造函数的函数体为空的时候, {} 也可以省略;

NOTE: 前面说过函数的参数可以有默认值, 考虑到构造函数也是函数, 因此构造函数也可以有默认值, 并且也可以通过命名参数进行调用, 以上代码可以进一步简化:

// kotlin
class UserBean (var name: String? = null, var age: Int? = null)

// 使用:
val u1 = UserBean() // name 和 age 都是 null
val u2 = UserBean("kid", 23)
val u3 = UserBean(age = 23) // name = null, age = 23
val u4 = UserBean(name = "kid") // name = "kid", age = null

kotlin 中创建对象时不需要 new 关键字

  • 继承

    正如所有的 java 类都继承自 Object 类, 所有的 kotlin 类都继承自 Any 类. 当声明一个类的时候, 默认是 final 修饰的, 如果要允许该类被继承, 需要使用 open 修饰:

    // kotlin
    open class UserBean

    Kotlin 中使用 : 代替 java 中的 extends 关键字表示继承

    1. 如果子类拥有 primary constructor , 那么父类必须在子类 primary constructor 后进行初始化 (调用其构造函数):

      // kotlin
      class StudentBean: UserBean() 
    2. 如果子类没有 primary constructor , 那么其每一个 secondary constructor 都必须使用 super 关键字初始化父类:

      class StudentBean : UserBean {
      constructor(name: String?) : super(name)
      }

      子类的 secondary constructor 默认会尝试调用父类的无参构造函数, 如果父类有无参构造函数, 那么子类的 secondary constructor 可以不用显式调用 super()

      // kotlin
      class StudentBean : UserBean {
      constructor()
      }
  • 抽象类

    使用 abstract 关键字修饰的类即抽象类, 抽象类默认就是 open 的, 因此不需要 open 关键字修饰.

  • 伴随对象 (companion objects)

    不像 java , kotlin 中没有静态方法和静态字段, kotlin 推荐在大多数情况下都可以使用包级别的函数替代静态方法:

    package cn.groupy.share.kotlin.utils
    fun toCamel(string: String): String {
    var result = string
    // ...
    return result
    }

    这样就能直接调用 toCamel("abc_def") 而不用类名.

    如果需要访问类的内部信息, 或该方法与一个类确实相关联, 需要使用类名来调用一个静态方法 (比如工厂方法) , 此时可以将该方法作为该类的伴随对象的成员:

    // kotlin
    class Session
    class SessionFactory {
    companion object {
        fun openSession(): Session {
            return Session()
        }
    }
    }
    
    // 使用
    val session = SessionFactory.openSession()

    更确切的说, 在伴随对象内部的所有字段, 方法都可以被外部类像在 java 中调用静态字段和静态方法一样调用.

语法糖

函数/属性扩展 - Extensions

kotlin 提供了不用继承一个类就能够给它添加方法 (和属性) 的能力, 这种能力叫做 Extension .

  • 扩展函数

    为了把一个函数添加到一个已有的类上, 需要在声明函数的时候在函数名前面加上接收对象的类型.

    假如我们想给字符串 String 类添加一个将首字母变大写的函数 capitalize , 在声明 capitalize 的时候需要在前面加上该函数的接收对象 (即要扩展的类), 然后在函数内部就能够使用 this 引用到调用该函数时的接收对象了:

    // kotlin
    fun String.capitalize(): String {
    // 此时可以使用 this 引用到该方法被调用时的字符串
    if (this.isEmpty()) return this
    if (this[0].isLowerCase()) return substring(0, 1).toUpperCase() + substring(1)
    return this
    }

    以上只是一个示例, kotlin 标准类库中已经提供了该方法了

  • 扩展属性

    为一个类添加扩展属性与为其添加扩展函数类似, 都是在属性前加接收类型.

    假如我们想为字符串添加一个属性 upperCaseCount 表示该字符串中大写字母的个数, 在声明完属性时同时为该属性指定一个 getter 函数:

    // kotlin
    val String.size: Int
    get() = this.filter { it.isUpperCase() }.count()
  • 扩展函数的坑

扩展函数实际并不会改变类, 仅仅是使得该方法能够被该类的实例使用 . 来调用而已. 扩展方法是 静态解析 的, 也就是说静态方法无法享受到面向对象语言的 多态 特性了, 考虑以下代码的输出:

// kotlin
open class Animal
open class Cat: Animal()
class Tiger : Cat ()

fun Animal.whoAmI() {
    println("Animal")
}

fun Cat.whoAmI() {
    println("Cat")
}

fun Tiger.whoAmI() {
    println("Tiger")
}

fun test() {
  var animal: Animal = Animal()
  animal.whoAmI()   // (1)
  animal = Cat()
  animal.whoAmI()   // (2)
  animal = Tiger()
  animal.whoAmI()   // (3)

  val animals: Array<Animal> = arrayOf(Animal(), Cat(), Tiger())
  animals.forEach { it.whoAmI() } // (4)
}

以上代码将会输出:

Animal
Cat
Tiger
Animal
Animal
Animal

对于第 1 到 3 的输出, 看起来像是扩展函数还具有多态的特性, 但是第 4 到 6 个为什么就不行了呢? 原因是编译器在调用代码 (1) ~ (3) 的时候很智能地检测了一下 animal 的类型, 发现 animal 有更具体的类型, 并且该类型都有 whoAmI 方法, 因此调用了更加具体类型的 whoAmI 方法, 而在执行代码 (4) 时, 由于泛型擦除, 编译器首先要检测 animals 中取出来的对象是否是 Animal 类型的, 就不会再检测一遍该对象是否有更具体的类型了.

通过查看编译后的字节码可以看到在调用代码 (2) , (3) 中的 whoAmI 之前确实有对 animal 变量做更详细类型的检测:

Code:
0: new           #9                  // class cn/groupy/share/kotlin/Animal
3: dup
4: invokespecial #12                 // Method cn/groupy/share/kotlin/Animal."<init>":()V
7: astore_1
8: aload_1
9: invokestatic  #18                 // Method cn/groupy/share/kotlin/ExtensionKt.whoAmI:(Lcn/groupy/share/kotlin/Animal;)V
12: new           #20                 // class cn/groupy/share/kotlin/Cat
15: dup
16: invokespecial #21                 // Method cn/groupy/share/kotlin/Cat."<init>":()V
19: checkcast     #9                  // class cn/groupy/share/kotlin/Animal
22: astore_1
23: aload_1
24: checkcast     #20                 // class cn/groupy/share/kotlin/Cat <调用 (2)前>
27: invokestatic  #24                 // Method cn/groupy/share/kotlin/ExtensionKt.whoAmI:(Lcn/groupy/share/kotlin/Cat;)V
30: new           #26                 // class cn/groupy/share/kotlin/Tiger
33: dup
34: invokespecial #27                 // Method cn/groupy/share/kotlin/Tiger."<init>":()V
37: checkcast     #9                  // class cn/groupy/share/kotlin/Animal
40: astore_1
41: aload_1
42: checkcast     #26                 // class cn/groupy/share/kotlin/Tiger <调用 (3) 前>
45: invokestatic  #30                 // Method cn/groupy/share/kotlin/ExtensionKt.whoAmI:(Lcn/groupy/share/kotlin/Tiger;)V
48: iconst_4
49: anewarray     #9                  // class cn/groupy/share/kotlin/Animal
52: dup
53: iconst_0
54: new           #9                  // class cn/groupy/share/kotlin/Animal
57: dup
58: invokespecial #12                 // Method cn/groupy/share/kotlin/Animal."<init>":()V
61: aastore
62: dup
63: iconst_1
64: new           #20                 // class cn/groupy/share/kotlin/Cat
67: dup
68: invokespecial #21                 // Method cn/groupy/share/kotlin/Cat."<init>":()V
71: checkcast     #9                  // class cn/groupy/share/kotlin/Animal
74: aastore
75: dup
76: iconst_2
77: new           #26                 // class cn/groupy/share/kotlin/Tiger
80: dup
81: invokespecial #27                 // Method cn/groupy/share/kotlin/Tiger."<init>":()V
84: checkcast     #9                  // class cn/groupy/share/kotlin/Animal
87: aastore
88: dup
89: iconst_3
90: ldc           #32                 // String a
92: checkcast     #9                  // class cn/groupy/share/kotlin/Animal
95: aastore
96: checkcast     #34                 // class "[Ljava/lang/Object;"
99: astore_3
100: aload_3
101: checkcast     #36                 // class "[Lcn/groupy/share/kotlin/Animal;"
104: astore_2
105: aload_2
106: checkcast     #34                 // class "[Ljava/lang/Object;"
109: astore_3
110: iconst_0
111: istore        4
113: iload         4
115: aload_3
116: arraylength
117: if_icmpge     144
120: aload_3
121: iload         4
123: aaload
124: astore        5
126: aload         5
128: checkcast     #9                  // class cn/groupy/share/kotlin/Animal
131: astore        6
133: aload         6
135: invokestatic  #18                 // Method cn/groupy/share/kotlin/ExtensionKt.whoAmI:(Lcn/groupy/share/kotlin/Animal;)V
138: iinc          4, 1
141: goto          113
144: return

如果在调用代码 (3) 之前, 我们在把 Tiger() 赋给 animal 的时候把其类型显式转为 Animal , 在调用 whoAmI 的时候就不会在做详细类型的检测了:

// kotlin
fun test() {
  var animal: Animal = Animal()
  animal.whoAmI()   // (1)
  animal = Cat() as Animal      // <-------
  animal.whoAmI()   // (2)
  animal = Tiger() as Animal    // <------------
  animal.whoAmI()   // (3)

  val animals: Array<Animal> = arrayOf(Animal(), Cat(), Tiger())
  animals.forEach { it.whoAmI() } // (4)
}

此时将会输出:

Animal
Animal
Animal
Animal
Animal
Animal

查看字节码也发现更详细的类型检测没有了.

kotlin 这种 “自作聪明” 的检测有时候会给初学者带来困惑

java 中, 我们经常使用像 xxxUtils 这样的类来给 xxx 类提供一些静态工具函数, 虽然 java 类库中使用 XXXs 类名而不是 xxxUtils , 但也是使用静态函数提供一系列工具方法, java.util.Collections , java.util.Arraysjava 1.7java.lang.Objects 都是这样的例子. kotlin 中扩展函数的作用就是取代这些工具类, 给已有的类提供更加方便的工具函数, 使得在调用这些工具函数的时候代码更佳简洁. 之所以称扩展函数是 kotlin 的语法糖, 就是因为 kotlin 实际上就是把扩展函数编译成我们以前在 java 中的静态函数.

集合框架
  • kotlin 提供了很多便利的方法能够快速创建各种集合, 前面讲到的 arrayOf 就是其中一个. 对于 array, list, map , kotlint 都提供了快速创建它们的不可变长度对象 (其存放的元素个数不可变) 的函数: arrayOf, listOf, mapOf 和可变长度对象 (除了 array, 因为其本身就是不可变长的) 的函数: mutableListOf, mutableMapOf.

  • 所有集合类型, 得益于 Extension , 以前在 java (java 8) 中要使用 stream() 函数才能够进行的流式操作都能直接在集合类上进行了, 并且功能更多; 得益于 lambda , 流操作写出来的代码将更加简洁.

    // kotlin
    // 遍历一个列表
    val products = arrayOf("newsjet", "groupy", "newsline")
    products.forEach { println(it) }
    
    // 求一个区间的所有偶数的和
    (1..100).filter { it % 2 == 0 }.sum()
    
    val names = listOf("tracy", "roy", "kid", "ken", "benny", "paddy", "neo", "blues")
    println(names.sortedBy { it[1] })   // 根据第二个字母排序 [paddy, ken, benny, neo, kid, blues, roy, tracy]
    println(names.groupBy { it[0] })    // 根据首字母进行分组 {t=[tracy], r=[roy], k=[kid, ken], b=[benny, blues], p=[paddy], n=[neo]}
    println(names.groupBy { it[0] }.map { Pair(it.key, it.value.count()) }) // 统计各个首字母的名字数 [(t, 1), (r, 1), (k, 2), (b, 2), (p, 1), (n, 1)]
    println(names.groupBy { it[0] }.map { Pair(it.key, it.value.count()) }.sortedByDescending { it.second }) // 统计完再根据次数降序
    
    class Person(val age: Int = 0, val name: String = "", val weight: Int = 50)
    val people = arrayOf(Person(24, "kid"), Person(age = 50, name = "paddy"), Person(name = "benny", weight = 200), Person(name = "tracy", weight = 53))
    // 求平均年龄
    people.sumBy { it.age } / people.size
    // 求最重的
    people.maxBy { it.weight }
try-with-resources

java 6 开始, 可以使用下面的代码使用一个 autoclosable 的资源, 不需要手动关闭该资源:

try (FileReader reader = new FileReader("/tmp/a")) {
} catch (Exception e) {
}

kotlin 中可以使用 use 函数:

FileReader("/tmp/a").use {
  // 使用 it 引用
}

use 确实是一个函数, 该函数的唯一参数是一个 lambda 表达式, 因此函数调用的括号 () 被省略, 并且在 lambda 内部可以使用 it 引用到 lambda 的参数. 说 use 函数是一个语法糖就是因为该函数内部只是替我们把本应该手动写的代码写好了而已:

public inline fun <T : Closeable?, R> T.use(block: (T) -> R): R {
    var closed = false
    try {
        return block(this)
    } catch (e: Exception) {
        closed = true
        try {
            this?.close()
        } catch (closeException: Exception) {
        }
        throw e
    } finally {
        if (!closed) {
            this?.close()
        }
    }
}

更多精彩的语法糖, 请 戳这里 参考官方文档

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值