基本数据类型
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
是用同一个类型表示基本数据类型和引用类型,但是在运行时,数字类型会尽可能地使用最高效的方式来表示。大多数情况下,对于变量、 属性、 参数和返回类型 —— kotlin
的 Int
类型会被编译成 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
方法不仅会检查它们存储的值,还要比较装箱类型。所以,在 Java
中 new 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.Object
。 Kotlin
把 Java
方法参数和返回类型中用到 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
类都包含下面三个方法: toString
、 equals
、 hashCode
。这些方法都继承自 Any
。 Any
并不能使用其他 Object
的方法(如 wait
和 notify
),可以通过手动把值转换成 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
Kotlin
的 Unit
和 Java
的 void
的区别: 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
时的分支处理会始终抛出异常。
Nothing
和 Unit
的区别:假设函数的返回值为 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
元素。
只读集合与可变集合
Kotlin
将 Java
的集合中访问集合数据的接口和修改集合数据的接口进行了拆分。分离出只读集合 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
}
就像 val
和 var
之间的分离一样,只读集合接口与可变集合接口的分离能让程序中的数据发生的事情更容易理解。如果函数接收 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);
}
}
可以看到都变成了 Java
中 Collection
接口,也即是可变的完整的集合接口。也就是说,即使 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>) {}
}
因此,我们在重写时需要提出三大问:
- 集合是否可空?
- 集合中的元素是否可空?
- 你的方法会不会修改集合?
如果你不确定,可以用最保险的方式:
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
,还有 ByteArray
,BooleanArray
等等。这些对应 Java
中的基本数据类型数组:int[]
、 byte[]
、 boolean[]
等等。
如果你想将一个持有装箱类型的数组或者集合,可以用对应的转换函数把它们转换成基本数据类型的数组,比如 toIntArray
。而且,kotlin
标准库提供了一套跟集合相同的用于数组的扩展函数,之前说的 filter
、 map
函数也同样适用于数组,非常方便。
要创建一个基本数据类型的数组,有如下选择:
用对应类型的构造方法接收数组大小来返回初始化好的数组:
// 元素为 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
类型是所有其他类型的超类型,类似于Java
的Object
,而Unit
类比于void
。kotlin
使用标准Java
集合类,并通过区分只读和可变集合来增强它们。kotlin
基本数据类型的数组使用像IntArray
这样的特殊类来表示。