参考:
Java中有8种基本类型byte,short,int,long,float,double,boolean,char,还提供了支持字符串的java.lang.String类,最后提供了上述的数组类型。Kotlin中的数据类型与Java基本一致,不过可以将基本数据类型也看做对象。
数字
类型 |位宽度
--------|---------
Double |64
Float |32
Long |64
Int |32
Short |16
Byte |8
注意,Kotlin中字符不是数字。
在Java平台中,数字的以JVM中的基本数据类型保存于物理存储,除非我们想要一个可以为空(nullable)的数字或者使用了泛型,那么这时数字会装箱(boxed)为对应的类型。
数字装箱后不保留同一性(identity),但保留相等性(equality),这与Java中是一样的,两个数字值相等的对象是不同的两个对象,但它们的值是相等的。
与Java中不同的是,同一性比较使用三个等号符号:===
或者!==
,相等性比较使用:==
或者!=
。
对于数字的隐式转换和自动拆装箱,对比Java中和Kotlin中有何区别:
- Java中,表达式中的数字类型会自动拆箱为对应的基础类型,然后向上转型为表达式中较大的基础类型。自动装箱只能发生在对应的基础类型上。
Integer a = 11;
Short b = 12;
Long c = 13L;
int i = 1;
short j = 2;
long k = 3;
k = a;
k = a + b;
k = i + j;
k += i;
a = a + b;
a = i + j;
c += i;
c = 14; // error, right expression is 'int'
c = i + j; // error, right expression is 'int'
c = a + b; // error, right expression is 'int'
- Kotlin中,与类型系统是相关的,因为所有数字类型都认为是对象,可以认为没有拆箱的说法,并且也没有隐式的类型提升,运算符事实上都是重载的操作符。
var a: Int? = 11
var b: Short? = 12
var c: Long? = 13L
var i: Int = 1
var j: Short = 2
var k: Long = 3
k = a // error, no unboxing and explicitly conversion
k = a + b // error, no unboxing and explicitly conversion
k = i + j // error, assignment has no explicitly conversion
k += i // ok
a = a + b // error, no unboxing and explicitly conversion
a = i + j // ok
c += i // error, not allow
c = 14 // ok
c = i + j // error, assignment has no explicitly conversion
c = a + b // error, no unboxing and explicitly conversion
正确通过编译的版本:
var a: Int? = 11
var b: Short? = 12
var c: Long? = 13L
var i: Int = 1
var j: Short = 2
var k: Long = 3
k = a!!.toLong()
k = (i + j).toLong()
k = (a!! + b!!).toLong()
k += i
a = a!! + b!!
a = i + j
c += i // error, not allow
c = 14
c = (i + j).toLong()
c = (a!! + b!!).toLong()
字符
字符使用Char
表示,它们不能被当做数字使用。
布尔值
布尔值使用Boolean
表示。
字符串
字符串使用String
表示。字符串是不可变的的,字符串的元素是字符,可以使用序号操作符[]
来访问。
有两种类型字符字面值,一种是脱字符字符串(escaped string),可以包含脱字符。另一种是原始字符串(raw string),没有脱字符,可以包含换行符和其它任意字符。
字符串模板
字符串可以包含模板表达式,也就是一段代码运行的结果转换为字符串并插入到字符串中。一个模板表达式以一个美元符号($
)开始,并且由一个简单名字或花括弧中任意表达式构成:
val i = 10
val s = "i = $i" // evaluates to "i = 10"
val s = "abc"
val str = "$s.length is ${s.length}" // evaluates to "abc.length is 3"
val price = """
${'$'}9.99
"""
在原始字符串中,如果要表示美元符号字面值,可以使用单引号,如上所示。
数组
数字由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()
,arrayOfNulls()
,以及使用工厂函数,如下所示,它接收一个数组大小,和返回每个序号对应值的方法:
// Creates an Array<String> with values ["0", "1", "4", "9", "16"]
val asc = Array(5, { i -> (i * i).toString() })
和Java中不一样,Kotlin中的数组是不变量,这意味着,不能将
Array<String>
赋值给一个Array<Any>
,这样避免了一个可能的运行时错误,但可以使用Array<out Any>
,见类型投影。
Kotlin还有指定的类用于表示基本数据类型数组,可以免去数据装箱的开销,有ByteArray, ShortArray, IntArray
等等,这些类与Array
类没有继承关系。它们有自己的工厂函数:
val x: IntArray = intArrayOf(1, 2, 3)
x[0] = x[1] + x[2]
相等性
Kotlin中有两种形式的相等性:
- 引用相等性(即同一性,两个引用指向同一个对象),使用
===
和!==
操作符。 - 结构相等性(equals()检查),使用
==
和!=
操作符。
通常结构相等性操作符a == b
会被翻译为:
a?.equals(b) ?: (b === null)
即如果a不为null,它盗用equals(Any?)函数,否则,a为null,它检查b是否引用相等于null。
注意没有必要直接与null进行同一性比较,a == null
会自动转换为a === null
。
空引用安全性(Null safety)
Kotlin的类型系统的目的之一是消灭空引用,即传说中的百万美元bug。
仅有几种情况会引起空指针异常(NullPointerException
):
- 代码中直接抛出异常
throw NullPointerException()
- 使用操作符
!!
- 外部Java代码导致的异常
- 初始化相关的数据不一致问题(一个构造器中未初始化的this使用在了其它地方)
在条件语句中检查null
第一,你可以显式地检查b是否为空,并且分别处理两种情况:
val l = if (b != null) b.length else -1
注意只有当b不可变(检查和使用间没有被修改的局部变量或者一个有支撑域的不可重写的val成员),因为其它情况下b在使用前检查后可能值被修改了。
安全调用
使用安全调用操作符(?.
):
b?.length // 如果b为null则返回null,否则返回b.length
// 链式调用,如果有一个为null,就返回null
bob?.department?.head?.name
// 只在非空的情况下执行某一操作
val listWithNulls: List<String?> = listOf("A", null)
for (item in listWithNulls) {
item?.let { println(it) } // prints A and ignores null
}
Elvis操作符
当我们有一个可以为空的引用r,我们可以说“如果r不为null,那么就使用它,否则就使用某个非空的值x”:
val l: Int = if (b != null) b.length else -1
使用Elvis操作符?:
表示为:
val l = b?.length ?: -1
由于throw和return也是表达式,它们也可以用于该操作符的右边,例如:
fun foo(node: Node): String? {
val parent = node.getParent() ?: return null
val name = node.getName() ?: throw IllegalArgumentException("name expected")
// ...
}
!!
操作符
对于NPE爱好者还有一个选择,可以使用b!!
,如果b不为空那么返回它,否则抛出一个NPE:
val l = b!!.length
安全转型
常规的类型转换可能导致ClassCastException
,另一种选择是使用在尝试转型失败后返回null
的安全转换:
val aInt: Int? = a as? Int
可以为空类型的集合
如果一个集合的元素可以为空,并且你想过滤出非空的元素,可以使用filterNotNull
函数:
val nullableList: List<Int?> = listOf(1, 2, null, 4)
val intList: List<Int> = nullableList.filterNotNull()
类型检查与转换:“is"和"as”
is
和!is
操作符
使用is
和!is
操作符可以在运行时检查一个对象是否符合一个给定的类型:
if (obj is String) {
print(obj.length)
}
if (obj !is String) { // same as !(obj is String)
print("Not a String")
}
else {
print(obj.length)
}
智能转型
很多情形下,不用显式地进行类型转换,因为编译器会跟踪is
检查,在需要的时候为不可变值进行显式地转型并自动插入(安全的)类型转换:
fun demo(x: Any) {
if (x is String) {
print(x.length) // x is automatically cast to String
}
}
注意如果编译器不能保证检查和使用之间变量不能改变,那么智能转型就没法进行。更具体来说,智能转型在以下规则下是适用的:
val
局部变量——总是可行的。val
属性——如果属性是private
的或者internal
的或者检查在属性声明的同一模块下进行。对于open
的属性或自定义了getter的属性不适用。var
局部变量——如果变量在检查和使用间没有被改变,并且没有在一个修改它的lambda中被捕获。var
属性——总是不可行的(因为变量可以在任何时间被其它代码修改)。
“不安全的(Unsafe)”转型操作符
通常,如果转型失败会抛出一个异常。因此,我们称之为_不安全的(unsafe)_。不安全的转型使用中缀操作符as
来进行:
val x: String = y as String
注意null
不能被转型为String
,因为String
类型不是可为空的,也就是说,如果y
是null
,那么上面的代码会抛出一个异常。为了符合java的转型语义我们在转型操作符的右边有可以为空的类型,例如:
val x: String? = y as String?
“安全的(safe)“转型操作
为了避免抛出一个异常,可以使用_安全的(safe)_转型操作符as?
,如果转型失败会返回null
:
val x: String? = y as? String
注意,尽管操作符as?
的右边是一个不为null
的类型String
,转型的结果是可以为空的。