第三章 Kotlin 类型系统

类型系统是在计算机科学中,类型系统用于定义如何将编程语言中的数值和表达式归类为许多不同的类型,如何操作这些类型,这些类型如何互相作用。类型可以确认一个值或者一组值具有特定的意义和目的(虽然某些类型,如抽象类型和函数类型,在程序运行中,可能不表示为值)。类型系统在各种语言之间有非常大的不同,也许,最主要的差异存在于编译时期的语法,以及运行时期的操作实现方式。(百度百科)

本章我们主要简单介绍Kotlin的类型相关的知识。

对象是带有属性和方法的特殊数据类型。笔者记得,在大学时候,学习C语言的结构体struct的时候,里面介绍过ADT(Abstract Data Type, 抽象数据类型)。其实,这就是对象。

Kotlin 是一门完全面向对象(Object Oriented)的语言。在Kotlin中一切皆是对象。所有对象皆继承自Any(类似Java中的所有对象的祖先类Object)。

在 Kotlin 中,函数是对象,基本类型也是对象,所有东西都是对象:数字、字符、布尔和数组。同时,Kotlin提供多个内建对象(buildin object): Number,Char,Boolean,String,Array等。

这个跟JavaScript挺像的。JavaScript 中的所有事物都是对象:字符串、数值、数组、函数等等。此外,JavaScript 提供多个内建对象,比如 String、Date、Array 等等。

Kotlin 的类型表现起来有着高度的一致性。

基本数据类型

在Java 中,有基本类型。这使得Java的类型系统有点不伦不类。所以,在Kotlin与Java互操作时,遇到Java基本类型的时候,要小心点。这地方Kotlin的编译器,会把Java的基本类型自动装箱成对应的封装类。

Kotlin的基本数据类型有:

Number: 包含整型与浮点型等
Char: 字符类型(Character)
Boolean: 布尔类型
String: 字符串类型
Array: 数组类型

在kotlin源码工程中如下图所示:


《Kotlin 程序设计》第三章 Kotlin 类型系统_数组



1.数字Number类型

Kotlin的基本数值类型包括Byte、Short、Int、Long、Float、Double等,这些类型都是内置类型。不同于Java的是,字符不属于数值类型。 Kotlin 处理数字在某种程度上接近 Java,但是并不完全相同。例如,对于数字没有隐式拓宽转换( Java 中 ​​int​​​ 可以隐式转换为​​long​​),另外有些情况的字面值略有不同。

类型

宽度

取值范围

Byte

8

-128~127

Short

16

-32768~32767

Int

32

-2147483648~2147483647

Long

64

-9223372036854775807L - 1L ~ 9223372036854775807L

Float

32

32位单精度浮点数 10^-38~10^38和-10^-38~-10^38

Double

64

64位双精度浮点数 10^-308~10^308和-10^-308~-10^308

在Kotlin内置的类型,有点像Java中的包装类。当然它们与Java多少有些不同。
这些数字类型在kotlin-runtime.jar里面的kotlin包下面的Primitives.kt。在kotlin源码工程中的core/buildins/native/kotlin/Primitives.kt。他们都继承自kotlin.Number, kotlin.Comparable。在Number类里面定义了数字类型之间显式转换的函数:

package kotlin

/**
* Superclass for all platform classes representing numeric values.
*/
public abstract class Number {
/**
* Returns the value of this number as a [Double], which may involve rounding.
*/
public abstract fun toDouble(): Double

/**
* Returns the value of this number as a [Float], which may involve rounding.
*/
public abstract fun toFloat(): Float

/**
* Returns the value of this number as a [Long], which may involve rounding or truncation.
*/
public abstract fun toLong(): Long

/**
* Returns the value of this number as an [Int], which may involve rounding or truncation.
*/
public abstract fun toInt(): Int

/**
* Returns the [Char] with the numeric value equal to this number, truncated to 16 bits if appropriate.
*/
public abstract fun toChar(): Char

/**
* Returns the value of this number as a [Short], which may involve rounding or truncation.
*/
public abstract fun toShort(): Short

/**
* Returns the value of this number as a [Byte], which may involve rounding or truncation.
*/
public abstract fun toByte(): Byte
}

1.1自动向上转型

Kotlin中没有自动向上转型,

val b: Byte = 1 // OK, 字面值是静态检测的
val i: Int = b // 错误

如果你想向上转型,可以通过显式地调用函数来实现:

var i: Int = 5;
var l: Long = i.toLong();
val i: Int = b.toInt() // OK: 显式拓宽

这样,类型之间关系更加清晰干净。

1.2自动装箱

在Kotlin中数值通过装箱存储。

val a: Int = 10000
print (a === a ) //打印 'true'

val boxedA: Int? =a
val anotherBoxedA: Int? = a

print (boxedA === anotherBoxedA ) // false
println (boxedA == anotherBoxedA )// true

从运行结果中可以看出,数值是相等的。但是引用地址不同。

1.3字面常量

在Kotlin的数字常量可以用十进制、十六进制、指数形式小数、二进制但是没有八进制。数值常量字面值有以下几种:

十进制: 123
Long 类型用大写 L 标记: 123L
十六进制: 0x0F
二进制: 0b00001011

注意: Kotlin 不支持八进制,有点奇怪。

Kotlin 同样支持浮点数的常规表示方法:

默认 double:123.5、123.5e10
Float 用 f 或者 F 标记: 123.5f

代码示例:

val d = 123.4
println(d)
println(d::class.java)//double

数字字面值中的下划线(自 1.1 起):

我们可以使用下划线使数字常量更易读:

val oneMillion = 1_000_000
val creditCardNumber = 1234_5678_9012_3456L
val socialSecurityNumber = 999_99_9999L
val hexBytes = 0xFF_EC_DE_5E
val bytes = 0b11010010_01101001_10010100_10010010

输出:

1000000
1234567890123456
999999999
4293713502
3530134674

每个数字类型支持如下的转换:

  • ​toByte(): Byte​
  • ​toShort(): Short​
  • ​toInt(): Int​
  • ​toLong(): Long​
  • ​toFloat(): Float​
  • ​toDouble(): Double​
  • ​toChar(): Char​

缺乏隐式类型转换并不显著,因为类型会从上下文推断出来,而算术运算会有重载做适当转换,例如:

val l = 3L + 1
println(l)
println(l::class.java) // long, Long + Int => Long

1.4运算

Kotlin支持数字运算的标准集,运算被定义为相应的类成员(但编译器会将函数调用优化为相应的指令)。

对于位运算,没有特殊字符来表示,而只可用中缀方式调用命名函数,例如:

val x = (1 shl 2) and 0x000FF000

这是完整的位运算列表(只用于 ​​Int​​​ 和 ​​Long​​):

  • ​shl(bits)​​​ – 有符号左移 (Java 的​​<<​​)
  • ​shr(bits)​​​ – 有符号右移 (Java 的​​>>​​)
  • ​ushr(bits)​​​ – 无符号右移 (Java 的​​>>>​​)
  • ​and(bits)​​ – 位与
  • ​or(bits)​​ – 位或
  • ​xor(bits)​​ – 位异或
  • ​inv()​​ – 位非

这些运算函数定义在Primitives.kt中。

2.字符类型Char

字符类型在Kotlin中用​​Char​​来表示。

val ch: Char = 'a'
println(ch::class.java)//char

val s = "abc"
println(s::class.java)//class java.lang.String

我们在JavaScript,Groovy可以使用单引号​​''​​​来标识字符串,但是Kotlin还是跟C,Java一样,char类型只能是用单引号​​''​​引用单个字符。当然,从编译器的角度,完全可以通过字符数的多少来判断是char类型还是String类型。

同时,Kotlin与Java不同的是,它不能直接去应用一个Number类型。例如

var c: Int = 'c'// 错误:类型不兼容

fun check(c: Char) {
if (c == 1) { // 错误:类型不兼容
// ……
}
}

是错误的。报错信息:

Error:(29, 19) Kotlin: The character literal does not conform to the expected type Int

必须要显式的调用函数进行转换:

val c : Int = 'c'.toInt()
println(c)

fun check(c: Char) {
if (c.toInt() == 1) { // 显式的调用函数进行转换
// ……
}
}

fun decimalDigitValue(c: Char): Int {
if (c !in '0'..'9')
throw IllegalArgumentException("Out of range")
return c.toInt() - '0'.toInt() // 显式转换为数字
}

从这几个细节,我们可以看出来自俄罗斯民族的Kotlin的节制的自由

同样Char也支持转意字符\n、\b、\r等跟Java中的差不多:

  • 字符字面值用单引号括起来:​​'1'​​。
  • 特殊字符可以用反斜杠转义。
  • 支持这几个转义序列:​​\t​​、 ​​\b​​、​​\n​​、​​\r​​、​​\'​​、​​\"​​、​​\​​ 和 ​​\$​​等。
  • 编码其他字符要用 Unicode 转义序列语法:​​'\uFF00'​​。

当需要可空引用时,像数字、字符会被装箱。装箱操作不会保留同一性。

3.布尔类型Boolean

布尔用 ​​Boolean​​ 类型表示,它有两个值:true{: .keyword } 和 false{: .keyword }。

若需要可空引用布尔会被装箱。

内置的布尔运算有:

  • ​||​​ – 短路逻辑或
  • ​&&​​ – 短路逻辑与
  • ​!​​ - 逻辑非

4.数组类型Array<T>

package kotlin

/**
* Represents an array (specifically, a Java array when targeting the JVM platform).
* Array instances can be created using the [arrayOf], [arrayOfNulls] and [emptyArray]
* standard library functions.
* See [Kotlin language documentation](http://kotlinlang.org/docs/reference/basic-types.html#arrays)
* for more information on arrays.
*/
public class Array<T> {
/**
* Creates a new array with the specified [size], where each element is calculated by calling the specified
* [init] function. The [init] function returns an array element given its index.
*/
public inline constructor(size: Int, init: (Int) -> T)

/**
* Returns the array element at the specified [index]. This method can be called using the
* index operator:
* value = arr[index]
*/
public operator fun get(index: Int): T

/**
* Sets the array element at the specified [index] to the specified [value]. This method can
* be called using the index operator:
* arr[index] = value
*/
public operator fun set(index: Int, value: T): Unit

/**
* Returns the number of elements in the array.
*/
public val size: Int

/**
* Creates an iterator for iterating over the elements of the array.
*/
public operator fun iterator(): Iterator<T>
}

数组在 Kotlin 中使用 ​​Array​​​ 类来表示,它定义了 ​​get​​​ 和 ​​set​​​ 函数(按照运算符重载约定这会转变为 ​​[]​​​)和 ​​size​​ 属性,以及一些其他有用的成员函数:

class Array<T> private constructor() {
val size: Int
operator fun get(index: Int): T
operator fun set(index: Int, value: T): Unit

operator fun iterator(): Iterator<T>
// ……
}

我们可以使用

  • 库函数​​arrayOf()​​ 来创建一个数组并传递元素值给它,这样 ​​arrayOf(1, 2, 3)​​ 创建了 array [1, 2, 3]。
  • 库函数​​arrayOfNulls()​​ (初始化值为null),​​emptyArray​​(其实现是(arrayOfNulls<T>(0) as Array<T>)) 可以用于创建一个指定大小、元素都为空的数组。

另一个选项是用接受数组大小和一个函数参数的工厂函数,用作参数的函数能够返回。

给定索引的每个元素初始值:

// 创建一个 Array<String> 初始化为 ["0", "1", "4", "9", "16"]
val asc = Array(5, { i -> (i * i).toString() })

如上所述,​​[]​​​ 运算符代表调用成员函数 ​​get()​​​ 和 ​​set()​​。

我们知道,Java的数组是​​协变的​​​。与 Java 不同的是,Kotlin 中数组是​​非协变的​​(invariant)。

这意味着 Kotlin 不让我们把 ​​Array<String>​​​赋值给 ​​Array<Any>​​,以防止可能的运行时失败(但是你可以使用 ​​Array<out Any>​​。

Kotlin 也有无装箱开销的专门的类来表示原生类型数组,这些原生类型数组如下:

  • BooleanArray — Boolean类型数组
  • ByteArray — Byte类型数组
  • ShortArray — Short类型数组
  • IntArray — Int类型数组
  • LongArray — Long类型数组
  • CharArray — Char类型数组
  • FloatArray — Float类型数组
  • DoubleArray — Double类型数组

这些类和 ​​Array​​ 并没有继承关系,但是它们有同样的方法属性集。它们也都有相应的工厂方法。这些方法定义在Library.kt中:

package kotlin

import kotlin.internal.PureReifiable

/**
* Returns a string representation of the object. Can be called with a null receiver, in which case
* it returns the string "null".
*/
public fun Any?.toString(): String

/**
* Concatenates this string with the string representation of the given [other] object. If either the receiver
* or the [other] object are null, they are represented as the string "null".
*/
public operator fun String?.plus(other: Any?): String

/**
* Returns an array of objects of the given type with the given [size], initialized with null values.
*/
public fun <reified @PureReifiable T> arrayOfNulls(size: Int): Array<T?>

/**
* Returns an array containing the specified elements.
*/
public inline fun <reified @PureReifiable T> arrayOf(vararg elements: T): Array<T>

/**
* Returns an array containing the specified [Double] numbers.
*/
public fun doubleArrayOf(vararg elements: Double): DoubleArray

/**
* Returns an array containing the specified [Float] numbers.
*/
public fun floatArrayOf(vararg elements: Float): FloatArray

/**
* Returns an array containing the specified [Long] numbers.
*/
public fun longArrayOf(vararg elements: Long): LongArray

/**
* Returns an array containing the specified [Int] numbers.
*/
public fun intArrayOf(vararg elements: Int): IntArray

/**
* Returns an array containing the specified characters.
*/
public fun charArrayOf(vararg elements: Char): CharArray

/**
* Returns an array containing the specified [Short] numbers.
*/
public fun shortArrayOf(vararg elements: Short): ShortArray

/**
* Returns an array containing the specified [Byte] numbers.
*/
public fun byteArrayOf(vararg elements: Byte): ByteArray

/**
* Returns an array containing the specified boolean values.
*/
public fun booleanArrayOf(vararg elements: Boolean): BooleanArray

/**
* Returns an array containing enum T entries.
*/
@SinceKotlin("1.1")
public inline fun <reified T : Enum<T>> enumValues(): Array<T>

/**
* Returns an enum entry with specified name.
*/
@SinceKotlin("1.1")
public inline fun <reified T : Enum<T>> enumValueOf(name: String): T

我们可以这样使用原生类型数组:

val x: IntArray = intArrayOf(1, 2, 3)
x[0] = x[1] + x[2]

5.字符串类型String

字符串用 ​​String​​ 类型表示。字符串是不可变的。

代码示例:

val c1 = 'c'
val c2 = "cc"
println(c1::class.java)//char
println(c2::class.java)//

访问字符串的元素:可以使用索引运算符访问: ​​s[i]​​。

可以用 foreach 循环迭代字符串:

for (c in str) {
println(c)
}

字符串字面值

Kotlin 有两种类型的字符串字面值: 转义字符串可以有转义字符,以及原生字符串可以包含换行和任意文本。转义字符串很像 Java 字符串:

val s = "Hello, world!\n"

转义采用传统的反斜杠方式。参见上面的 ​​字符​​ 查看支持的转义序列。

原生字符串 使用三个引号(​​"""​​)分界符括起来,内部没有转义并且可以包含换行和任何其他字符:

val text = """
for (c in "foo")
print(c)
"""

你可以通过 ​​trimMargin()​​ 函数去除前导空格:

val text = """
for (c in "foo")
print(c)
"""
println(text)

val text2 = """
for (c in "foo")
print(c)

""".trim()

println(text2)

val text3 = """
|About.
|Kotlin.
|Easy Learning.
|(Jason Chen)
""".trimMargin("|")

println(text3)

输出:

for (c in "foo")
print(c)

for (c in "foo")
print(c)
About.
Kotlin.
Easy Learning.
(Jason Chen)

默认 ​​|​​​ 用作边界前缀,但你可以选择其他字符并作为参数传入,比如 ​​trimMargin(">")​​。

字符串模板

字符串可以包含模板表达式 ,即一些小段代码,会求值并把结果合并到字符串中。
模板表达式以美元符(​​​$​​)开头,由一个简单的名字构成:

val i = 10
val s = "i = $i" // 求值结果为 "i = 10"

或者用花括号扩起来的任意表达式:

val s = "abc"
val str = "$s.length is ${s.length}" // 求值结果为 "abc.length is 3"

原生字符串和转义字符串内部都支持模板。
如果你需要在原生字符串中表示字面值 ​​​$​​ 字符(它不支持反斜杠转义),你可以用下列语法:

val price = """
${'$'}9.99
"""

Kotlin类型系统

Kotlin有一个统一的类型系统,它有一个根类型kotlin.Any?。并且每个其他类型是此根类型的子类型。

Kotlin中的根对象Any

Kotlin中所有对象皆继承自Any。这个Any类源码如下:

/*
* Copyright 2010-2015 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package kotlin

/**
* The root of the Kotlin class hierarchy. Every Kotlin class has [Any] as a superclass.
*/
public open class Any {
/**
* Indicates whether some other object is "equal to" this one. Implementations must fulfil the following
* requirements:
*
* * Reflexive: for any non-null reference value x, x.equals(x) should return true.
* * Symmetric: for any non-null reference values x and y, x.equals(y) should return true if and only if y.equals(x) returns true.
* * Transitive: for any non-null reference values x, y, and z, if x.equals(y) returns true and y.equals(z) returns true, then x.equals(z) should return true
* * Consistent: for any non-null reference values x and y, multiple invocations of x.equals(y) consistently return true or consistently return false, provided no information used in equals comparisons on the objects is modified.
*
* Note that the `==` operator in Kotlin code is translated into a call to [equals] when objects on both sides of the
* operator are not null.
*/
public open operator fun equals(other: Any?): Boolean

/**
* Returns a hash code value for the object. The general contract of hashCode is:
*
* * Whenever it is invoked on the same object more than once, the hashCode method must consistently return the same integer, provided no information used in equals comparisons on the object is modified.
* * If two objects are equal according to the equals() method, then calling the hashCode method on each of the two objects must produce the same integer result.
*/
public open fun hashCode(): Int

/**
* Returns a string representation of the object.
*/
public open fun toString(): String
}

关于学习一门语言的最好的方式,就是阅读其源码。通过这个Any类的源码以及注释,我们可以看出

判断两个对象x,y是否相等,必须满足以下条件:

自反性:对于任何非空引用值x,x.equals(x)应返回true。

对称性:对于任何非空引用值x和y,x.equals(y)应返回true当且仅当y.equals(x)返回true。

传递性:对于任何非空引用值x,y,z,如果x.equals(y)返回true,y.equals(Z)返回true,那么x.equals(Z)应返回true

一致性:对于任何非空引用值x和y,多次调用x.equals(y)始终返回true或者始终返回false,没有提供任何信息进行相等比较的对象被修改。

另外,请注意,​​==​​​算子在Kotlin中,等价于调用​​equals​​​函数。要比较引用是否相同,使用​​===​​算子。

Kotlin泛型与类型安全

跟Java一样,Kotlin也支持泛型类:

package com.easy.kotlin

/**
* Created by jack on 2017/5/30.
*/

class Box<T>(t:T){
var value = t
}

在具体使用的时候,需要传入具体的类型:

val box1 = Box<String>("easy kotlin")
println(box1.value)

Kotlin也可以通过值可以推断出类型(type projections),所以,我们也可以省略类型参数:

// 1 的类型是 Int,  编译器可以推断出泛型T的类型是Int
val box2 = Box(1)
println(box2.value)

在Kotlin泛型中,数组类型Array是非协变的(Non covariant)。意思是,泛型类型参数是不可变的。例如​​Array<Int>​​​与​​Array<Any>​​​不是子父类关系,故无法将​​Array<Int>​​​的实例当做​​Array<Any>​​使用。这么做,是为了类型安全。

在Java中的使用通配符类型的场景中,Kotlin通过使用关键字out,in来支持特殊场景下的协变(covariant)。

Java类型系统最复杂的特性之一,就是通配符。但是Kotlin中一个都没有,取而代之的是两种其他实现::

  • declaration-site协变
  • 类型预测(type projections)

首先,让我们想一下,为什么Java需要如此难以理解的通配符。这个问题在《Effective Java》(Joshua Bloch)的第28节有解释:

利用有限制的通配符来提高API的灵活性。

首先,Java中泛型的参数化类型是非协变的(invariant),这意味着List<String>并不是List<Object>的子类型。下面这段代码将会带来编译异常以及运行时异常:

// Java
List<String> strs = new ArrayList<String>();
List<Object> objs = strs; // !!! 这就是即将引入的问题的原因。Java禁止这样!
objs.add(1); // 我们向一个包含`String`的列表添加了一个`Integer`
String s = strs.get(0); // !!! ClassCastException: Cannot cast Integer to String

所以,Java禁止这样做其实是为了保证运行时安全。

我们来看一下Collection接口的addAll()方法,这个方法的签名是什么呢?直观来看觉得可能会是这样的:

// Java
public interface Collection<E> ... {
... addAll(Collection<E> items);
}

但是,addAll()的实际的签名函数是下面这个样子:

public interface Collection<E> ... {
// Java
... addAll(Collection<? extends E> c);
}

通配符唯一保证的事情就是类型安全(type safety)。

通配符类型参数(wildcard type argument) ​​? extends T​​ 表明这个方法接受 T子类型的对象集合,并非T本身。这意味着,可以从列表中安全的读取T(集合中所有的元素都是T的一个子类),但是我们无法写入因为我们并不知道哪些类是T的子类。因为有了这种限制,我们渴望这种行为: Collection<String>是 Collection<? extends Object>的子类。从表面意义来看,通过extends-bound(向上限制)修饰的通配符使得类型可协变。

Java中的另一个通配符 ​​List<? super String>​​ 是List<Object>的超类。这被成为 逆变(contravariance)。

你只能使用String作为参数在​​List<? super String>​​上调用方法( 你可以调用add(String)或者 set(int, String))。然而,如果当你调用List<T>的一些函数来返回T的话,你将会得到一个Object,而不是String。

Joshua Bloch称:

这些对象你只能从生产者(Producers)中读取,只能在消费者(Consumers)中写入。为了最大程度的灵活性,在输入参数时使用通配符类型来表示生产者或者消费者。

关于Kotlin的泛型,更多可以参考[1]

类型别名typealias

类型别名为现有类型提供替代名称。这个跟Linux的shell中的命令行alias类似。

如果类型名称太长,你可以另外引入较短的名称,并使用新的名称替代原类型名。

它有助于缩短较长的泛型类型。这样可以简化我们的代码:

typealias NodeSet = Set<Network.Node>

typealias FileTable<K> = MutableMap<K, MutableList<File>>

为函数类型提供另外的别名:

typealias MyHandler = (Int, String, Any) -> Unit

typealias Predicate<T> = (T) -> Boolean

为内部类和嵌套类创建新名称:

class A {
inner class Inner
}
class B {
inner class Inner
}

typealias AInner = A.Inner
typealias BInner = B.Inner

类型别名不会引入新类型。

当我们在代码中添加 ​​typealias Predicate<T>​

typealias Predicate<T> = (T) -> Boolean

并使用 ​​Predicate<Int>​​​ 时,Kotlin 编译器总是把它扩展为 ​​(Int) -> Boolean​​。

因此,当我们需要泛型函数类型时,可以传递该类型的变量,反之亦然:

// 类型别名
typealias Predicate<T> = (T) -> Boolean

fun foo1(p: Predicate<Int>) = p(1)
fun foo2(p: Predicate<Int>) = p(-1)

fun main(args: Array<String>) {

val f: (Int) -> Boolean = { it > 0 }
println(foo1(f)) // 输出 "true"
println(foo2(f)) // 输出 "false"

val fn : (Int) -> Boolean = {it < 0}
println(foo1(fn)) // 输出 "false"
println(foo2(fn)) // 输出 "true"

val p: Predicate<Int> = { it > 0 }
println(listOf(1, -2).filter(p)) // 输出 "[1]"
}

参考资料

1.​​http://kotlinlang.org/docs/reference/generics.html​