Kotlin 基本类型

在 Kotlin 中,所有的东西都是对象。在这个意义上讲,我们可以在任何变量上调用成员函数与属性。在 Kotlin 中的基本类型,都是以对象的形式存在的。我们常用的类型基本有以下几类:数字,字符,布尔值,数组,字符串。

数字

Kotlin 中的数字类型类似于Java,内置了一组表示数字的内置类型。主要有以下一些:

类型 长度
Double 64
Float 32
Long 64
Int 32
Short 16
Byte 8

当我们声明数字变量时,如果不指定变量的类型,Kotlin 就会根据当前变量的初始化的长度来推断其类型。整数的类型推断时会以 Int 为的最大值为准。未超出 Int 最大值的整型值初始化的变量都会推断为 Int 类型,如果初始值超过了Int 最大值,那么就会推断为 Long 类型。

对于浮点数,类型推断时会默认推断为 Double 类型;除非在声明时,在宿主后面添加 f 或者 F 后缀。

字面常量

在 Kotlin 中支持以下的数值类型

  • 十进制:比如123,一般的,Long 类型需要在数字后缀加大写的 L
  • 十六进制:比如 0x012;0xabc
  • 二进制:比如:0b00110011
  • 不支持八进制
  • 浮点类型

    • 默认 Double 123.4,123.5e10
    • Float 后缀需要加 f 或者 F ,否者会默认使用 Double

      在 Kotlin 1.1 起,数字常量支持下滑下 _ 按位分隔,使数字更易读。比如:

  • 按英文方式断位: 1_0001_000_000

  • 按中文方式断位:1_000010000_0000
  1. val oneThousand = 1_000
  2. val oneMillion = 1_000_000
  3. val oneBillion = 1_000_000_000
  4. // 按中文表示 一万
  5. val wan = 1_0000
  6. // 按中文表示 一亿
  7. val yi = 10000_0000

表示方式

在 Java 平台数字是物理存储为 JVM 的原生类型。比如下面的代码:

  1. val a = 100
  2. val b = 100.0
  3. val c = 100.0F
  4. val d = 100L

将上面的代码转换成 Java 代码:

  1. private static final int a = 100;
  2. private static final double b = 100.0D;
  3. private static final float c = 100.0F;
  4. private static final long d = 100L;

有一个疑问?在 Java 平台上,什么时候才会触发 Java 数据类型的装箱操作呢?

在 Kotlin 中,当我们的变量声明为可空类型时,即使用 ? 操作符修饰时。这个时候就会触发装箱的操作。比如下面的代码:

  1. val e: Int? = 100
  2. val f: Float? = 100.0F
  3. val g: Double? = 100.0
  4. val h: Long? = 100L

将上面的代码转换成 Java 代码:

  1. @Nullable
  2. private static final Integer e = 100;
  3. @Nullable
  4. private static final Float f = 100.0F;
  5. @Nullable
  6. private static final Double g = 100.0D;
  7. @Nullable
  8. private static final Long h = 100L;

涉及到装箱时,我们需要注意,变量只保留的相等性(使用 == 判断,即数字的值相等),却不能保证同一性(使用 === 判断,即引用相等,两个变量执行同一个内存地址)。

  1. val i = 1000
  2. val boxedJ : Int ? = i
  3. println(i == boxedJ) // true
  4. println(i === boxedJ) // false

我们将上面的代码转换成 Java 代码,结果就一目了然了:

  1. int i = 1000;
  2. // 将 i 装箱,生成一个 boxedJ 的引用,这个引用会在内存中开辟一块新的内存空间
  3. Integer boxedJ = Integer.valueOf(i);
  4. // 拆箱,将包装类转换为基本的数据类型进行比较两个变量的字面变量的值
  5. boolean var2 = i == boxedJ;
  6. System.out.println(var2);
  7. // 当对比 两个引用时,会再次装箱,将变量 i 装箱,这是又会重新生成一个引用,然后在内存中重新分配一个新的内存空间
  8. var2 = Integer.valueOf(i) == boxedJ;
  9. System.out.println(var2);

对比一下 kotlin 和 Java 代码,不难看出,涉及到拆箱和装箱时一定要注意同一性的问题。

数据类型转换

Kotlin 不支持数字的隐式拓宽转换,我们不能直接将 Int 类型的值直接赋给 Long 类型变量。在 Kotlin 中所有的东西都是对象,如果我们将 Int 类型的值直接赋给 Long 类型变量,就是将一个 Int 对象 强制转换之后 赋值给 Long 对象。在编译时就会报错。

  1. val x: Int? = 100 // 将 x 装箱成 Java Integer
  2. val y: Long? = x // 将 y 装箱成 Java Long ,然后 Long y = (Long)x;

上面代码编译就会出现错误:Error:(42, 20) Kotlin: Type mismatch: inferred type is Int? but Long? was expected

除了上面的错误,当做算术运算时也需要注意:

  1. val x: Int? = 100
  2. val y: Long? = 100L
  3. if (x != null && y != null) {
  4. val z = x + y // Long + Int ==> Long
  5. }
  6. val a = 1L + 2 // Long + Int ==> Long
  1. Kotlin 算术运算会有重载做适当的转换,所以上面的代码都是可以运行的。

在 Kotlin 中不能隐式的转换,只能显式转换。

在 Kotlin 中每个数字类型都提供了显式转换的方法:

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

运算

Kotlin 支持数字的标准运算。运算被定义为相应的类成员。像 + - × ÷ 等这些运算符都有对应的预定义的函数。预定义运算符只能作用在数字类型的对象上。如果要在其它的对象中使用这些运算符,就需要重载这些操作符的预定义函数。

一元前缀操作符
操作符/表达式 预定义函数
+a a.unaryPlus()
-a a.unaryMinus()
!a a.not()

+a 为例,介绍上面的操作符的执行步骤:

  1. 根据 a 的值推断 a 的类型,比如 Int ;
  2. 然后在 Int 类中查找带有 operator 修饰符的无参函数 unaryPlus(),即成员函数或者扩展函数;
  3. 如果函数没有找到,则会导致编译错误;
  4. 如果函数存在,则会继续确定其返回类型,如果返回类型为 Int,那么 +a 的值就是 Int 类型。

递增与递减
操作符/表达式 预定义函数
a++/++a a.inc()
a--/--a a.dec()

对于 inc()dec() 这两个函数,它们用于赋值给使用 ++ 或者 -- 操作的变量,所以这两个函数必须有返回值。

a++ 为例,介绍编译器执行解析 后缀形式表达式 的步骤:

  1. 根据 a 的值推断 a 的类型,比如 Int ;
  2. 然后在 Int 类中查找带有 operator 修饰符的无参函数 inc(),即成员函数或者扩展函数;
  3. 如果函数存在,则会继续确定其返回类型,如果返回类型为 Int,那么 a++ 的值就是 Int 类型。

其计算表达式的步骤:

  1. a 的初始值存储到临时变量a0 中;
  2. a.inc() 的结果赋值给 a
  3. a0 作为表达式的结果返回。

对于 a-- 其步骤都是完全相似的。

对于前缀形式的 ++a--a 以相同方式解析,但是其不会在创建临时变量。再次以 ++a 为例,其计算步骤如下:

  1. a.inc() 的结果直接赋值给 a
  2. 然后把 a 的新值作为表达式的结果返回。

二元操作

算术运算符
操作符/表达式 翻译成的表达式
a + b a.plus(b)
a - b a.minus(b)
a * b a.times(b)
a / b a.div(b)
a % b a.rem(b)a.mod(b) (已弃用)
a..b a.rangeTo(b)

对于上面这张表的操作,编译器只是翻译成列中搞得表达式。

比如上面的 a+b ;编译器就会翻译成 a.plus(b)

In 操作符
操作符/表达式 翻译成的表达式
a in b b.contains(a)
a !in b !b.contains(a)

对于 in!in,过程是相同的,但是参数的顺序是相反的。

索引访问操作符
操作符/表达式 翻译成的表达式
a[i] a.get(i)
a[i, j] a.get(i, j)
a[i_1, ……, i_n] a.get(i_1, ……, i_n)
a[i] = b a.set(i, b)
a[i, j] = b a.set(i, j, b)
a[i_1, ……, i_n] = b a.set(i_1, ……, i_n, b)

方括号转换为调用带有适当数量参数的 getset 。不过在 数组 或者 List 中一般只会使用一个参数的 set 、get 。

调用操作符
表达式 翻译成的表达式
a() a.invoke()
a(i) a.invoke(i)
a(i, j) a.invoke(i, j)
a(i_1, ……, i_n) a.invoke(i_1, ……, i_n)

圆括号转换为调用带有适当数量参数的 invoke

广义赋值
表达式 翻译为
a += b a.plusAssign(b)
a -= b a.minusAssign(b)
a *= b a.timesAssign(b)
a /= b a.divAssign(b)
a %= b a.remAssign(b), a.modAssign(b)(已弃用)

在 Kotlin 中赋值不是表达式。其没有返回值。

对于赋值操作,例如 a+b ,编译器质性以下步骤:

  1. 如果右列的函数可用
  2. 如果相应的二元函数(即plusAssign()对应于plus())也可用 ,那么报告错误(模糊),因为编译器不知道使用 plusAssign() 还是使用 plus()
  3. 确保其返回值是 Unit ,否知会报错;
  4. 生成 a.plusAssign(b) 的代码。
  5. 如果右列的函数不可用,则会试着生成 a = a + b 的代码(这里包含类型检查:a + b 的类型必须是 a 的子类型)

相等与不等操作符
表达式 翻译成的表达式
a == b a?.equals(b) ?: (b === null)
a != b !(a?.equals(b) ?: (b === null))

这些操作符只使用函数 equals(other: Any?): Boolean,可以覆盖它来提供自定义的相等性检测实现。不会调用任何其他同名函数(如 equals(other: Foo))。

注意===!==(同一性检查)不可重载,因此不存在对他们的约定。

这个 == 操作符有些特殊:它被翻译成一个复杂的表达式,用于筛选 null 值。 null == null 总是 true,对于非空的 xx == null 总是 false 而不会调用 x.equals()

比较操作符
表达式 翻译为
a > b a.compareTo(b) > 0
a < b a.compareTo(b) < 0
a >= b a.compareTo(b) >= 0
a <= b a.compareTo(b) <= 0

所有的比较都转换为对 compareTo 的调用,这个函数需要返回 Int 值。

位运算

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

位运算函数 含义
shl(bits) 有符号左移 ( Java 的 << )
shr(bits) 有符号右移 ( java 的 >> )
ushr(bits) 无符号右移 ( Java 的 >>> )
and(bits) 位与 ( & )
or(bits) 位或 ( | )
xor(bits) 位异或 ( ^ )
inv() 位非 ( ! )

浮点数比较
  • 相等性检测:a == ba != b
  • 比较操作符:a < ba > ba <= ba >= b
  • 区间实例以及区间检测:a..bx in a..bx !in a..b

字符

在 Kotlin 中 字符使用 Char 类型表示。它们不能直接当做数字,但是可以通过 toInt() 等函数转换为数字。

字符的字面值使用单引号表示: 'a' 。 特殊字符需要使用转义字符 \ 进行转义。支持这几个转义序列:\t\b\n\r\'\"\\\$。 编码其他字符要用 Unicode 转义序列语法:'\uFF00'

当需要可空引用(声明为可空变量)时,像数字、字符会被装箱。装箱操作不会保留同一性。比如:

  1. var char: Char? = 'c'

转换成 Java:

  1. Character var21 = 'c';

布尔

布尔值采用 Boolen 类型表示,它只有两个值 truefalse

它内置了如下的布尔运算:

  • || ==> 短路逻辑 或
  • && ==> 短路逻辑 与
  • ! ==> 逻辑 非

若需要可空引用(声明为可空变量)则布尔值会被装箱。比如:

  1. var boolean: Boolean? = false

转换成 Java:

  1. Boolean var23 = false;

数组

在 kotlin 中数组使用 Array 类来表示,它定义了 getset 函数以及 size 属性,以及一些其他的有用的成员函数。

我们可以使用库函数 arryOf() 创建一个数组并传递元素值给它。或者使用库函数 arryOfNulls 创建一个指定大小的、所有元素都为空的数组。

  1. // 创建一个数组,数组的元素为 1,2,3,4,5
  2. var arrayOf = arrayOf(1,2,3,4,5)
  3. // 创建一个长度为5的数组,数组的每个元素都是 Int 类型
  4. var arrayOfNulls = arrayOfNulls<Int>(5)

将上面的代码转换成 Java 代码很容易就可以看出,这种数组的创建方式涉及到了Int的装箱操作:

  1. Integer[] arrayOf = new Integer[]{1, 2, 3, 4, 5};
  2. Integer[] arrayOfNulls = new Integer[5];

除了上面的两种方式,我们还可以通过 Array 的构造函数来创建数组。用接受数组大小以及一个函数参数的 Array 构造函数,用作参数的函数能够返回给定索引的每个元素初始值:

  1. // 创建一个 Array<String> 初始化为 ["0","1","2","3","4"]
  2. var asc = Array(5){
  3. (it).toString()
  4. }

[] 运算符代表调用成员函数 get()set()

  1. var array = arrayOf(1,2,3,4)
  2. // 将数组的第0个元素的值设置为100;相当于调用array.set(0,100)
  3. array[0] = 100
  4. // 获取数组的第0个元素,并打印;相当于 array.get(0)
  5. println(array[0])

原生类型数组

Kotlin 为了避免装箱开箱,专门提供了原生类型的数组: ByteArrayShortArrayIntArray 等等。这些类与 Array 并没有继承关系,但是它们有同样的方法属性集。它们都有相同的工厂方法,以 IntArray 为例:

  1. val x: IntArray = intArrayOf(1, 2, 3)
  2. x[0] = x[1] + x[2]
  1. // Array of int of size 5 with values [0, 0, 0, 0, 0]
  2. val arr = IntArray(5)
  3. // e.g. initialise the values in the array with a constant
  4. // Array of int of size 5 with values [42, 42, 42, 42, 42]
  5. val arr = IntArray(5, { 42 })
  6. // e.g. initialise the values in the array using a lambda
  7. // Array of int of size 5 with values [0, 1, 2, 3, 4] (values initialised to their index value)
  8. var arr = IntArray(5, { it * 1 })

字符串

在 Kotlin 中用 String 类型表示字符串,字符串是不可以变的。字符串的元素——字符可以使用索引运算符访问,也可以使用 for 循环迭代字符串:

  1. val string: String = "Hello , World"
  2. // 获取字符串的第0个字符
  3. println(string[0])
  4. // 使用 for 循环迭代字符串
  5. for (c in string) {
  6. println(c)
  7. }

字符串可以使用 + 操作符进行连接。这也适用于连接字符串与其他类型的值, 只要表达式中的第一个元素是字符串:

  1. val s = "abc" + 1
  2. println(s + "def")

虽然 Kotlin 提供了使用 + 操作符进行连接字符串的方式,但是并不提倡使用,大多数情况下还是优先使用字符串模板。

字符串字面值

Kotlin 有两种类型的字符串字面值:

  1. 可以包含转义字符的字符串,使用双引号 " 字符串 " 包括起来;
  2. 可以包含换行以及任意文本的原始字符串,使用三个引号 """ 字符串 """ 包括起来。
    转义采用传统的反斜杠方式
    以下是转义字符串的一个示例:
  1. val s = "Hello, world!\n"

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

  1. val text = """
  2. for (c in "foo")
  3. print(c)
  4. """

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

  1. val text = """
  2. |Tell me and I forget.
  3. |Teach me and I remember.
  4. |Involve me and I learn.
  5. |(Benjamin Franklin)
  6. """.trimMargin()

trimMargin()函数会去除 | 前缀,这里的 | 前缀是为了更好地可读性。

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

字符串模板

Kotlin 支持在字符串中引用变量。可以通过 $ 在字符串中引用某个常量或者变量。如果引用的是一个变量的函数、属性或者一个表达式,可以 ${} 来引用。如果需要在字符串中使用 $ 就需要对其的进行转义 ${'$'},这样就可以在字符串中引用 $

  1. val name = "Ricky"
  2. val num = 10
  3. val rise = 10000
  4. val welcome = """
  5. welcome
  6. No. $num
  7. name: $name ,
  8. name length:${name.length} ,
  9. social status:${'$'}$rise """
  10. println(welcome)

上面的代码输出结果为:

  1. welcome
  2. No. 10
  3. name: Ricky ,
  4. name length5 ,
  5. social status$10000

在 字符串中使用 表达式:

  1. val range = 1..5
  2. println("3 in range is ${3 in range}")

总结:

  1. 在 Kotlin 中,所有的东西都是对象,基本类型也是对象;
  2. 基本类型包含:Double、Float、Long、Int、Short、Byte、Char、Boolean,每个类型都是一个类;
  3. 声明变量时,如果不指定变量的类型,Kotlin 会根据当前变量值推断类型;
  4. 声明整形数字时,默认为 Int ;声明浮点型数字时,默认为 Double;
  5. 在 Kotlin 中,变量声明为可空类型时,即使用 ? 操作符修饰时,就会触发装箱的操作;
  6. 每个基本类型都是一个类,各个基本类型不能进行隐式转换;
  7. 每个基本都是提供了方法显式转换成其它的各个基本类型;
  8. Kotlin支持数字运算的标准集
  9. Kotlin支持各种位运算,但是没有特定的符号,需要使用中缀方式调用命名函数;
  10. 字符 Char 类型 不能直接当作数字;
  11. 特殊字符需要使用转义字符 \ 进行转义,支持常见的转义字符;
  12. 数组使用 Array 类表示,它定义了 get/set 函数以及 size 属性;
  13. 以使用库函数 arryOf() 创建一个数组并传递元素值给它。或者使用库函数 arryOfNulls 创建一个指定大小的、所有元素都为空的数组;
  14. 使用库函数 arryOf()/arryOfNulls() 会涉及到拆箱装箱;
  15. 使用原生类型数组可以避免拆箱装箱;
  16. 字符串用 String 类型表示。字符串是不可变的。可以使用 [] 通过下标访问字符串中的某个字符;
  17. 可以使用字符串模板进行字符串连接;也可以使用 + 连接字符串,但不推荐;
  18. 原始字符串使用三个引号(""")分界符括起来,内部没有转义并且可以包含换行以及任何其他字符;并且可以通过 trimMargin()函数去除前导空格;