阅读 345

Kotlin教程(五)类型

写在开头:本人打算开始写一个Kotlin系列的教程,一是使自己记忆和理解的更加深刻,二是可以分享给同样想学习Kotlin的同学。系列文章的知识点会以《Kotlin实战》这本书中顺序编写,在将书中知识点展示出来同时,我也会添加对应的Java代码用于对比学习和更好的理解。

Kotlin教程(一)基础
Kotlin教程(二)函数
Kotlin教程(三)类、对象和接口
Kotlin教程(四)可空性
Kotlin教程(五)类型
Kotlin教程(六)Lambda编程
Kotlin教程(七)运算符重载及其他约定
Kotlin教程(八)高阶函数
Kotlin教程(九)泛型


基本数据类型

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

val i: Int = 1
val list: List<Int> = listOf(1, 2, 3)
复制代码

这样很方便。此外,你还能对一个数字类型的值调用方法。例如下面使用了标准库的函数coerceIn 来把值限制在特定的范围内:

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

>>> showProgress(146)
We're 100% done!
复制代码

如果基本数据类型和引用类型是一样的,是不是意味着Kotlin使用对象来表示所有的数字是非常低效的?确实低效,所以Kotlin并没有这样做。
在运行时,数字类型会尽可能地使用最高效的方式来表示。大多数情况下Kotlin的Int类型会被编译成Java基本数据类型int。当然,泛型、集合之类的还是会被编译成对应的Java包装类型。
像Int这样的Kotlin类型在底层可以轻易地编译成对应的Java基本数据类型,因为两种类型都不能存储null引用。反过来也一样,当你在Kotlin中使用Java声明时,Java基本数据类型会变成非空类型(而不是平台类型,平台类型详见《Kotlin教程(四)可空性》),因为他们不能持有null值。

可空的基本数据类型

Kotlin中的可空类型(如Int?)不能用Java的基本数据类型表示,因为null只能被存储在Java的引用类型的变量中。这意味着任何时候只要使用了基本数据类型的可空版本,它就会编译成对应的包装类型Int? -> Integer

/* Kotlin */
class Dog(val name: String, val age: Int? = null)

/* Java */
Dog dog = new Dog("julie", 3);
Integer age = dog.getAge();
复制代码

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

数字转换

Kotlin和Java之间一条重要的区别就是处理数字转换的方式。Kotlin不会自动地把数字从一段类型转换成另外一种,即便是转换成范围更大的类型。

val i = 1
val l: Long = i //错误:类型不匹配
复制代码

必须显示进行转换:

val i = 1
val l: Long = i.toLong()
复制代码

每一种基本数据类型(Boolean除外)都定义有转换函数:toByte()toShort()toChar()等。这些函数支持双向转换:即可以把小范围的类型扩展到大范围Int.toLong(),也可以把大范围的类型截取到小范围Long.toInt(),当然于Java中类似首先要确保大范围的类型的值超过小范围上限。
为了避免意外发生,Kotlin要求转换必须是显式的,尤其是在比较装箱值的时候。比较两个装箱值的equals方法不仅会检查他们存储的值,还要比较装箱类型。在Java中new Integer(42).equals(new Long(42)) 会返回false。假设Kotlin支持隐式转换,你可能会这样写:

val x = 1
val list = listOf(1L, 2L, 3L)
x in list  //假设Kotlin支持隐式转换的话返回false
复制代码

但这与我们期望是不同的。因此,x in list 这行代码根本不会编译。Kotlin要求你显式转换类型,这样只有类型相同的值才能比较:

>>> val x = 1
>>> println(x.toLong() in listOf(1L, 2L, 3L))
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

注意,Kotlin 1.1 才开始支持数字字面值中下划线。对字符字面值来说,可以使用和Java几乎一样的语法。把字符写在单引号中,必要时还可以使用转义序列:'1' ,'\t'(制表符), '\u0009'(使用Unicode转义序列表示的制表符)。

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

fun foo(l: Long) = println(l)

>>> val b: Byte = 1 
>>> val l = b + 1L   //Byte + Long -> Long
>>> foo(42)  //42被当做是Long类型
42
复制代码

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

Any 和 Any? :根类型

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

/* Kotlin */
fun a(any: Any) {}

/* 编译成的Java */
public static final void a(@NotNull Object any) {}
复制代码

所有Kotlin类都包含下面三个方法:toString、equals、hashCode。这些方法都继承自Any。Any并不能使用其他Object的方法(如wait和notify),如果你确认想用这些方法,可以通过手动把值转换成Object来调用这些方法。

Unit类型:Kotlin的void

Kotlin中的Unit类型完成了Java中的void一样的功能。当函数没有什么有意思的结果要返回时,它可以用作函数的返回类型:

fun f(): Unit {}
复制代码

在教程(一)中,我们就说到Unit可以直接省略:fun f() {}
大多数情况下,你不会留意到void和Unit之间的区别。如果你的Kotlin函数使用Unit作为返回类型并且没有重写泛型函数,在底层它会被编译成旧的void函数。 那么Kotlin的Unit和Java的void到底有什么不一样呢?Unit是一个完备的类型,可以作为类型参数,而void却不行。只存在一个值是Unit类型,这个值也叫做Unit,并且在函数中会被隐式返回(不需要再显示return null)。当你在重写返回泛型参数的函数时这非常有用,只需要让方法返回Unit类型的值:

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

class NoResultProcessor : Processor<Unit> {
    override fun process() {
        //do something 不需要显式return
    }
}
复制代码

和Java对比一下,Java中为了解决使用“没有值”作为参数类型的任何一种可能解法,都没有Kotlin的解决方案这样漂亮。一种选这是使用分开的接口定义来区别表示需要和不需要返回值的接口。另一种是用特殊的Void类型作为类型参数。即便选择了后者,还是需要加入一个return null; 语句来返回唯一能匹配这个类型的值,因为只要返回类型不是void,你就必须始终有显式饿return语句。
你也许会奇怪为什么Kotlin选择使用一个不一样的名字Unit而不是把它叫做Void。在函数式编程语言中,Unit这个名字习惯上被用来表示“只有一个实例”,这正式Kotlin的Unit和Java的void的区别。Kotlin本可以沿用Void这个名字,但是还有一个Nothing的类型,它有着完全不同的功能。Void和Nothing两种类型的名字含义如此相近,会令人困惑。

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

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

fun fail(message: String): Nothing {
    throw IllegalStateException(message)
}
复制代码

Nothing类型没有任何值,只有被当做函数返回值使用,或者被当做泛型函数返回值的类型参数使用才会有意义。
返回Nothing的函数可以方法Elvis运算符的右边来做先决条件检查:

val address = company.address ?: fail("No address")
println(address)
复制代码

上面这个例子展示了在类型系统中拥有Nothing为什么极其有用。编译器知道这种返回类型的函数从不正常终止,然后在分析调用这个函数的代码时利用这个信息。在这例子中,编译器会把address的类型推断成非空,因为它为null时的分支处理会始终抛出异常。

可空性和集合

对前后一致的类型系统来说知道集合是否可以持有null元素是非常重要的一件事情。而Kotlin就可以非常显眼的表示。

fun readNumbers(reader: BufferedReader): List<Int?> {
    val result = ArrayList<Int?>()
    for (line in reader.lineSequence()) {
        try {
            val number = line.toInt()
            result.add(number)
        } catch (e: NumberFormatException) {
            result.add(null)
        }
    }
    return result
}
复制代码

这个函数从一个文件中读取文本行的列表,并尝试把每一行文本解析成一个数字。List<Int?> 是能持有Int? 类型值得雷柏啊,换句话说可以持有Int或者null。
注意List<Int?>List<Int>? 的区别。前一种表示列表本身始终不为null,但列表中的每个值都可以为null。后一种类型的变量可能包含空引用而不是列表实例,但列表的元素保证是非空的。
处理可空值得集合时,通过要首先要判断是否为null,如果是则不处理,也即过滤掉null值。Kotlin提供了一个标准库函数filterNotNull来完成它:

>>> val list = listOf(1L, null, 3L)
>>> println(list)
[1, null, 3]
>>> println(list.filterNotNull())
[1, 3]
复制代码

这种过滤也影响了集合的类型。过滤前是List<Long?>,过滤后是List<Long>,因为过滤保证了集合不会在包含任何为null的元素。

只读集合与可变集合

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

public interface Collection<out E> : Iterable<E> {
    public val size: Int
    public fun isEmpty(): Boolean
    public operator fun contains(element: @UnsafeVariance E): Boolean
    override fun iterator(): Iterator<E>
    public fun containsAll(elements: Collection<@UnsafeVariance E>): Boolean
}
复制代码

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

public interface MutableCollection<E> : Collection<E>, MutableIterable<E> {
    override fun iterator(): MutableIterator<E>
    public fun add(element: E): Boolean
    public fun remove(element: E): Boolean
    public fun addAll(elements: Collection<E>): Boolean
    public fun removeAll(elements: Collection<E>): Boolean
    public fun retainAll(elements: Collection<E>): Boolean
    public fun clear(): Unit
}
复制代码

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

fun <T> copyElements(source: Collection<T>, target: MutableCollection<T>) {
    for (item in source) {
        target.add(item)
    }
}
复制代码

这个例子中我们读取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);
      }

   }
复制代码

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

*集合创建函数

集合类型 只读 可变
List listOf mutableListOf, arrayListOf
Set setOf mutableSetOf, hashSetOf, linkedSetOf, sortedSetOf
Map mapOf mutableMapOf,hashMapOf,linkedMapOf,sortedMapOf

作为平台类型的集合

上一篇关于可空性的文章,Kotlin中把哪些定义在Java代码中的类型看成平台类型。Kotlin没有任何关于平台类型的可空性信息,所以编译器允许Kotlin代码将其视为可空或者非空。同样,Java中声明的集合类型的变量也被视为平台类型,一个平台类型的集合本质上就是可变性未知的集合。特别是当你在Kotlin中重写或者实现签名中有集合类型的Java方法时,就要考虑到底用哪一种类型来重写:

/* Java */
interface Processor {
    void process(List<String> values);
}

/* Kotlin */
class ProcessorImpl : Processor {
    override fun process(values: MutableList<String?>?) {}
}

class ProcessorImpl2 : Processor {
    override fun process(values: MutableList<String>?) {}
}

class ProcessorImpl3 : Processor {
    override fun process(values: MutableList<String>) {}
}

class ProcessorImpl4 : Processor {
    override fun process(values: List<String>) {}
}
复制代码

这些继承方法的定义都是可以的,你要根据实际情况做出选择:

  • 集合是否可空?
  • 集合中的元素是否可空?
  • 你的方法会不会修改集合?

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

override fun process(values: MutableList<String?>?) {}
复制代码

但是使用的时候就得考虑各种可能为空的情况了。

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

之前的好多例子其实都出现了Kotlin的数组:

Array<String>
复制代码

Kotlin中的一个数组是一个带有类型参数的类,其元素类型被指定为相应的类型参数。可以通过arrayOfarrayOfNulls和Array构造方法来创建数组。

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

>>> val strings = listOf("a", "b", "c")
>>> println("%s/%s/%s".format(*strings.toTypeArray())) //期望varvag参数时使用展开运算符传递数组
a/b/c
复制代码

数组类型的类型参数始终会变成对象类型。如果你声明了一个Array<Int>它将会是一个包含装箱整型的数组Integer[]。如果你需要创建没有装箱的基本数据类型的数组,必须使用一个基本数据类型数组的特殊类。 为了表示基本数据类型的数组。Kotlin提供了若干独立的类,每一种基本数据类型都对应一个,例如Int类型的数组叫做IntArray,还有ByteArray,BooleanArray等等。这些对应Java中的基本数据类型数组:int[]btye[]boolean[]等等。 要穿件一个基本数据类型的数组,你可以通过intArrayOf之类的工厂方法,或者构造方法传入size或者lambda。

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