Kotlin教程(五)类型

写在开头:本人打算开始写一个Kotlin系列的教程,一是使本身记忆和理解的更加深入,二是能够分享给一样想学习Kotlin的同窗。系列文章的知识点会以《Kotlin实战》这本书中顺序编写,在将书中知识点展现出来同时,我也会添加对应的Java代码用于对比学习和更好的理解。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值。bash

可空的基本数据类型

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中也须要使用?.!! 等安全调用方式。ide

数字转换

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支持隐式转换,你可能会这样写:post

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。

相关文章
相关标签/搜索