Kotlin 的类型

基本数据类型

Java 把基本数据类型和引用类型做了区分。一个基本数据类型(如 int )的变量直接存储了它的值,而一个引用类型(如 String )的变量存储的是指向包含该对象的内存地址的引用。基本数据类型的值能够更高效地存储和传递,但是你不能对这些值调用方法,或是把他们存放在集合中。 Java 提供了特殊的包装类型(如 Integer )在你需要对象的时候对基本数据类型进行封装。因此,你不能用 Collection<int> 来定义一个整数的集合,而必须用 Collection<Integer> 来定义。

注意: Kotlin 并不区分基本数据类型和包装类型,你使用的永远是同一个类型(如 Int ),这样很方便,此外,你还能对一个数字类型的值调用方法。例如下面使用了标准库的函数 coerceIn 来把值限制在特定的范围内:

fun showProgress(progress: Int) {
    val percent = progress.coerceIn(0, 100)
    println("$percent% done")
}

showProgress(-34)   //0% done
showProgress(300)   //100% done

虽然 kotlin 是用同一个类型表示基本数据类型和引用类型,但是在运行时,数字类型会尽可能地使用最高效的方式来表示。大多数情况下,对于变量、 属性、 参数和返回类型 —— kotlinInt 类型会被编译成 Java 基本数据类型 int 。唯一例外是泛型类,会被编译成对应的 Java 包装类。

fun testBaseDataType(params : Int): Int {
    val i: Int = 2
    val list: List<Int> = listOf(1, 2, 3)
    return  i
}

上述 Kotlin 写的方法编译成 Java 代码是这样的:

// 参数和返回类型变成 int 
public static final int testBaseDataType(int params) {    
   int i = 2;    // 变量变成 int 
    //集合变成 Integer
   List list = CollectionsKt.listOf(new Integer[]{1, 2, 3});    
   return i;
}

Int 这样的 Kotlin 类型在底层可以轻易地编译成对应的 Java 基本数据类型,因为两种类型都不能存储 null 引用。反过来也一样,当你在 Kotlin 中使用 Java 声明时,Java 基本数据类型会变成非空类型 ,因为他们不能持有 null 值。

可空的基本数据类型

Kotlin 中的可空类型(如 Int? ) 不能用 Java 的基本数据类型表示 ,因为 Null 只能存储在 Java 的引用类型中。这意味着,只要使用了基本数据类型的可空版本,它就能编译成对应的包装类型 Int? -> Integer 。此外,虽然你用非空的基本数据类型作为泛型类的类型参数,但是 kotlin 还是会使用该类型的包装形式,这是由 Java 虚拟机实现泛型的方式决定的。

// kotLin
class Man(val age: Int? = null) {

    fun isOlderThan(other: Man): Boolean? {
        // 先校验
        if (age == null || other.age == null) {
            return null
        }
        // 当做非空类型来处理
        return age > other.age
    }
}

// Java
public final class Man {
   @Nullable
   private final Integer age;

   @Nullable
   public final Integer getAge() {
      return this.age;
   }

   public Man(@Nullable Integer age) {
      this.age = age;
   }

   @Nullable
   public final Boolean isOlderThan(@NotNull Man other) {
      return this.age != null && other.age != null ? Intrinsics.compare(this.age, other.age) > 0 : null;
   }
}

可以看到 val age: Int?Java 中被编译成了 Integer 。因此,在 Java 中使用的时候需要注意可能为 null 的情况。当然在 Kotlin 中也需要使用 ?.!! 等安全调用方式。

数字转换

Kotlin 不会自动地把数字从一种类型转换成另外一种,即便是转换成范围更大的类型,例如:

val i: Int = 2
val l: Long = i // 不能直接这样赋值,不会自动转换,会报错
val l: Long = i.toLong() // 需要显式转换

为了避免意外情况, kotlin 要求转换必须是显式的,每一种基本数据类型( Boolean 除外)都定义有转换函数:toByte()toShort()toChar()等。这些函数支持双向转换:即可以把小范围的类型扩展到大范围 Int.toLong() ,也可以把大范围的类型截取到小范围 Long.toInt()

比较两个装箱值的 equals 方法不仅会检查它们存储的值,还要比较装箱类型。所以,在 Javanew Integer(42).equals(new Long(42)) 会返回 false 。而在 Kotlin 中,只有类型是相同的值才能比较,不支持隐式转换,所以下面这个示例会报错:

val longList = listOf(1L, 2L, 3L)
println(i in longList)    //假设支持隐式转换,结果会返回 false 

需要显式转换,如下:

val longList = listOf(1L, 2L, 3L)
println(i.toLong() in longList)     // 返回 true

基本数据类型字面值

Kotlin 除了支持简单的十进制数字之外,还支持下面这些在代码中书写数字字面值的方式:

  • 使用后缀 L 表示 Long 类型的字面值: 123L 。
  • 使用标准浮点数表示 Double 字面值: 0.12, 2.0, 1.2e10, 1.2e-10 。
  • 使用后缀 F 表示 Float 字面值: 123.4f, .456F,1e3f 。
  • 使用前缀 0x 或者 0X 表示十六进制字面值: 0xCAFEBABE, 0xbcdL 。
  • 使用前缀 0b 或者 0B 表示二进制字面值: 0b000000101 。

当你书写数字字面值的时候一般不需要使用转换函数。算数运算符也被重载了,它们可以接收所有适当的数字类型:

fun testFoo() {
    val i = 100
    val b: Byte = 1
    val l = b + 1L  // //Byte + Long -> Long,运算符被重载,会自动处理

    foo(42)     // 通过数字字面值,不用显式转换
    foo(i.toLong())     // 不是通过数字字面值,需要显式转换
}

Kotlin 标准库提供了一套相似的扩展方法,用来把字符串转换成基本数据类型: "42".toInt() 。每个这样的函数都会尝试把字符串的内容解析成对应的类型,如果解析失败则抛出 NumberFormatException

“Any” 和 “Any?” :根类型

Object 作为 Java 类层级结构的根差不多,Any 类型是 Kotlin 所有非空类型的超类型,如果可能持有 null 值,则是 Any? 类型。在底层,Any 类型对应 java.lang.ObjectKotlinJava 方法参数和返回类型中用到 Object 类型看做 Any(更切确地说是平台类型,因为其可空性未知)。当 Kotlin 函数使用 Any 时,它会被编译成 Java 字节码的 Object

// Kotlin
fun method(params: Any): Any {
    return  params
}

// Java
@NotNull
public static final Object method(@NotNull Object params) {
  return params;
}

所有 Kotlin 类都包含下面三个方法: toStringequalshashCode 。这些方法都继承自 AnyAny 并不能使用其他 Object 的方法(如 waitnotify ),可以通过手动把值转换成 Object 来调用这些方法。

我们看一下 Any 类的声明:

public open class Any public constructor() {
    public open operator fun equals(other: kotlin.Any?): kotlin.Boolean { /* compiled code */ }

    public open fun hashCode(): kotlin.Int { /* compiled code */ }

    public open fun toString(): kotlin.String { /* compiled code */ }
}

“ Unit 类型” :Kotlin 的 “void”

Kotlin 中的 Unit 类型跟 Java 中的 void 功能相似,当函数不需要返回结果时,可以用作函数的返回类型。但是,在 Java 中,void 是必须加上的,而 Unit 是可以省略的,比如:

fun testUnit():Unit {}
fun testUnit() {}   // 省略 Unit

KotlinUnitJavavoid 的区别: Unit 是一个完备的类型,可以作为类型参数,而 void 却不行。只存在一个值是 Unit 类型,这个值也叫做 Unit ,并且在函数中会被隐式返回(不需要再显式 return null )。即,Unit 是有值的,在方法体函数中,没有显式调用 return 的时候,会被隐式调用返回,返回的值也是 Unit ,不需要像在 Java 中要显式返回 null ,这点非常方便。

当你在重写返回泛型参数的函数时这非常有用,只需要让方法返回 Unit 类型的值:

interface Processor<T>{
    fun process(): T
}

class NoResultProcessor : Processor<Unit> {
    override fun process() {
        //do something
        // 不需要显式调用 return 
    }
}

接口签名要求返回一个值,当实现类确实没有可返回的值时,可以用 Unit 类型,而且 Unit 是有值的,编译器会隐式加上 return Unit 。在 Java 中,可以用 Void (不是 void)类型作为类型参数,但是需要显式调用 return null

Nothing 类型:这个函数永不返回

对某些 Kotlin 函数来说,返回类型的概念没有任何意义,因为他们从来不会成功地结束,例如,许多测试库中都有一个叫做 fail 的函数,它通过抛出带有特定消息的异常来让当前测试失败。一个包含无限循环的函数也永远不会成功地结束。当分析调用这样函数的代码时,知道函数永远不会正常终止时很有帮助的。Kotlin 使用一种特殊的返回类型 Nothing 来表示:

fun fail(message: String): Nothing {
    throw IllegalStateException(message)
}

上述函数省略掉返回类型 Nothing 也是可以的,只是如果知道函数不会成功结束,可以显式返回 Nothing 。比如:

val address = company.address ?: fail("No address")
println(address)

在这例子中,编译器会把 address 的类型推断成非空,因为它为 null 时的分支处理会始终抛出异常。

NothingUnit 的区别:假设函数的返回值为 Nothing ,因为永远无法获取一个 Nothing 的实例,所以用户就知道这个函数永远不会返回。这与返回 Unit 的函数不同,它的确会返回,只不过返回值没有意义。

可空性与集合

Kotlin 完全支持类型参数的可空性,直接通过 ? 就能知道参数是否支持 Null 类型,例如:

fun readNumbers(reader: BufferedReader): List<Int?> {
    val result = ArrayList<Int?>()  //创建包含可空 int 值的列表
    for (line in reader.lineSequence()) {
        try {
            val number = line.toInt()
            result.add(number)  // 添加非空值
        } catch (e: NumberFormatException) {
            result.add(null)    // 添加 null 值
        }
    }
    return result
}

List<Int?> 是能够持有 Int 值或 null 值的列表。注意 List<Int?>List<Int>? 的区别:前者表示列表本身是非空的,但列表中的每个值都可以为 null 。后者正好相反,列表本身实例可以为 null ,但列表中的每个值都不能为 null

当你需要声明一个变量持有可空的列表,并且包含可空的元素,可以这样声明: List<Int?>?

注意: 在处理可空值的集合的元素时,要先检查是否为 null ,如果不想做检查处理,可以过滤掉集合中的 null 值,kotlin 标准库中提供了 filterNotNull 来完成它。

val list = listOf(1, null, 4, "fanda")
println(list)   //[1, null, 4, fanda]

val result = list.filterNotNull()   //过滤操作,变成非空类型的了
println(result)   //[1, 4, fanda]

调用 filterNotNull 过滤集合之后,元素类型会变成非空的,因为过滤保证了集合不会再包含 null 元素。

只读集合与可变集合

KotlinJava 的集合中访问集合数据的接口和修改集合数据的接口进行了拆分。分离出只读集合 kotlin.collections.Collection ,使用这个接口可以遍历集合中的元素,获取集合大小、判断集合中是否包含某个元素,以及执行其他从该集合中读取数据的操作,但这个接口没有任何添加或移除元素的方法。

public interface Collection<out E> : kotlin.collections.Iterable<E> {
    public abstract val size: kotlin.Int

    public abstract operator fun contains(element: E): kotlin.Boolean

    public abstract fun containsAll(elements: kotlin.collections.Collection<E>): kotlin.Boolean

    public abstract fun isEmpty(): kotlin.Boolean

    public abstract operator fun iterator(): kotlin.collections.Iterator<E>
}

另一个则是 kotlin.collections.MutableCollection 接口,使用以它可以修改集合中的数据。它继承kotlin.collections.Collection ,提供了方法来添加和移除元素,清空集合等:

public interface MutableCollection<E> : kotlin.collections.Collection<E>, kotlin.collections.MutableIterable<E> {
    public abstract fun add(element: E): kotlin.Boolean

    public abstract fun addAll(elements: kotlin.collections.Collection<E>): kotlin.Boolean

    public abstract fun clear(): kotlin.Unit

    public abstract operator fun iterator(): kotlin.collections.MutableIterator<E>

    public abstract fun remove(element: E): kotlin.Boolean

    public abstract fun removeAll(elements: kotlin.collections.Collection<E>): kotlin.Boolean

    public abstract fun retainAll(elements: kotlin.collections.Collection<E>): kotlin.Boolean
}

就像 valvar 之间的分离一样,只读集合接口与可变集合接口的分离能让程序中的数据发生的事情更容易理解。如果函数接收 Collection 而不是 MutableCollection 作为参数,你就知道它不会修改集合,而只是读取集合中的数据。如果函数要求你传递给他 MutableCollection 作为参数,可以认为它将会修改数据。

fun <T> copyElements(source: Collection<T>, target: MutableCollection<T>) {
    for (item in source) {  // 读取 source 的元素
        target.add(item)    // 将元素添加到可变集合 target
    }
}

这个例子中我们读取 source 中的元素添加到 target 中,因此声明函数的时候可以很好的区分:一个只读,一个可变。

注意:只读集合并不总是线程安全的,如果你在多线程环境下处理数据,你需要保证代码正确地同步了对数据的访问,或者使用支持并发访问的数据结构。

这种分离只在 Kotlin 的代码中有效,上面这个例子转换成 Java 代码后:

public static final void copyElements(@NotNull Collection source, @NotNull Collection target) {
   Iterator var3 = source.iterator();
   while(var3.hasNext()) {
      Object item = var3.next();
      target.add(item);
   }

}

可以看到都变成了 JavaCollection 接口,也即是可变的完整的集合接口。也就是说,即使 Kotlin 中把集合声明成只读的,Java 代码也能够修改这个集合。 kotlin 的可变和不可变的集合,都能够正常地被 java 的方法使用。 Kotlin 编译器不能完全分析 Java 代码到底对集合做了什么,因此 Kotlin 无法拒绝向可以修改集合的 Java 代码传递只读 Collection 。如果你将定义的函数中会将只读集合传递给 Java ,你有责任将参数声明成正确的参数类型,取决于 Java 代码是否会修改集合。这个注意事项也同样适用于 Kotlin 定义的非空元素集合传递给 Java 时,可能会存入 null 值。

总结一下: 可变跟不可变集合只在 kotlin 中生效,Java 代码可以正常对 Kotlin 的集合做修改,即使声明成不可变。集合的非空声明也只在 Kotlin 中生效,Java 代码可以正常对 Kotlin 的集合传入 null 值。

注意:当你在 Kotlin 中写的代码是可以传递给 Java 使用时,需要采取特别的预防措施,来确保 kotlin 类型正确地反映出集合上所有可能的修改。

集合创建函数

只读:

listOf 、 setOf 、 mapOf

可变:

List:  mutableListOf() 、 arrayListOf()

Set:   mutableSetOf() 、 hashSetOf() 、 linkedSetOf() 、 sortedSetOf()

Map:   mutableMapOf() 、 hashMapOf() 、 linkedMapOf() 、 sortedMapOf()

作为平台类型的集合

Java 中声明的集合类型的变量也被视为平台类型,一个平台类型的集合本质上就是可变性未知的集合。特别是当你在 Kotlin 中重写或者实现签名中有集合类型的 Java 方法时,就要考虑到底用哪一种类型来重写,比如:

有个 Java 接口如下:

public interface TestInterface {
    void test(List<String> values);
}

Kotlin 中可以有如下的实现方式:

class TestInterfaceImpl1 : TestInterface {
    // 集合是否可空
    override fun test(values: MutableList<String>?) {}
    override fun test(values: MutableList<String>) {}

    // 元素是否可空
    override fun test(values: MutableList<String>) {}
    override fun test(values: MutableList<String?>) {}

    // 方法会不会修改集合
    override fun test(values: MutableList<String>) {}
    override fun test(values: List<String>) {}
}    

因此,我们在重写时需要提出三大问:

  1. 集合是否可空?
  2. 集合中的元素是否可空?
  3. 你的方法会不会修改集合?

如果你不确定,可以用最保险的方式:

override fun test(values: MutableList<String?>?) {}

使用的时候就要考虑各种可能为空的情况。

对象和基本数据类型的数组

默认情况下,应该优先使用集合而不是数组。Kotlin 的数组是一个带有类型参数的类,其元素类型被指定为相应的类型参数。

fun testArray() {
    // 因为有初始化值,通过类型推导可知,则可以省略 <String>
    val testStringArray = Array<String>(3) { "init"} // 带初始化数量的数组,且给定初始化值

    println(testStringArray.size)   //5

    for (i in testStringArray.indices) {    // 遍历数组的下标
        println("index is $i and value is ${testStringArray[i]}")
    }
}

系统提供了很多创建数组的函数,比如 :

arrayOf()    // 创建包含任意类型实参的数组
intArrayOf()    // 创建 Int 数组
floatArrayOf()    // 创建 Float 数组
booleanArrayOf()    // 创建 Boolean 数组

创建有固定大小的且元素是非空的数组,可以用 Array 构造方法,该方法接收数组的大小和一个 lambda 表达式(用来初始化每一个元素):

// 有3个元素,每个元素的值都是 init ,元素都是非空的
val testStringArray = Array<String>(3) { "init"}

// it 代表每个数组元素的下标,lambda 会生成对应的下标对应的值
val result = Array(26){ ('a' + it).toString()}
// ABCDEFGHIJKLMNOPQRSTUVWXYZ
println(result.joinToString(""){it.toUpperCase()})

也可以创建元素是可空的数组,通过 arrayOfNulls 函数:

// 没有初始化值,不能通过类型推导知道,要加上类型参数
val nullableArray = arrayOfNulls<String>(3)

上述方法创建的数组,有 3 个值 ,都是 null

Kotlin 代码中最常见的创建数组的情况之一是需要调用参数为数组的 Java 方法时,或是调用带有 vararg 参数的 Kotlin 函数。在这些情况下,通常已经将数据存储在集合中,只需要将其转换为数组即可。可以使用 toTypeArray 方法的来执行:

val content = listOf("a", "b", "c")
// toTypedArray 用来给集合转成对应数组
println("%s/%s/%s".format(*content.toTypedArray()))    //  a/b/c

数组类型的类型参数始终会变成对象类型。如果你声明了一个 Array<Int> 它将会是一个包含装箱整型的数组 Integer[] 。如果你需要创建没有装箱的基本数据类型的数组,必须使用一个基本数据类型数组的特殊类。为了表示基本数据类型的数组。 Kotlin 提供了若干独立的类,每一种基本数据类型都对应一个,例如 Int 类型的数组叫做 IntArray ,还有 ByteArrayBooleanArray 等等。这些对应 Java 中的基本数据类型数组:int[]byte[]boolean[] 等等。

如果你想将一个持有装箱类型的数组或者集合,可以用对应的转换函数把它们转换成基本数据类型的数组,比如 toIntArray 。而且,kotlin 标准库提供了一套跟集合相同的用于数组的扩展函数,之前说的 filtermap 函数也同样适用于数组,非常方便。

要创建一个基本数据类型的数组,有如下选择:

用对应类型的构造方法接收数组大小来返回初始化好的数组:

// 元素为 0 0 0
val a = IntArray(3)

用工厂函数,接收可变参数的值,并创建存储这些值的数组:

// 元素为 0 0 0
val b = intArrayOf(0, 0, 0)

用对应类型的带 lambda 的构造方法来创建:

// 元素为 0 1 4
val c = IntArray(3) { it * it }

小结

  • kotlin 对可空类型的支持,可以帮助我们在编译期,检测出潜在的空指针错误。
  • kotlin 提供了像安全调用 ( ?. ) 、 Elvis 运算符 ( ?: )、 非空断言 ( !! ) 及 let 函数这样的工具来简洁地处理可空类型。
  • as? 运算符提供了一种简单的方式来把值转换成一个类型,以及处理当它拥有不同类型的情况。
  • Java 中的类型在 kotlin 中被解释成平台类型,允许开发者把它们当作可空或非空对待。
  • 可空的基本数据类型对应着 Java 中的装箱基本类型。
  • Any 类型是所有其他类型的超类型,类似于 JavaObject ,而 Unit 类比于 void
  • kotlin 使用标准 Java 集合类,并通过区分只读和可变集合来增强它们。
  • kotlin 基本数据类型的数组使用像 IntArray 这样的特殊类来表示。