第 6 章 数据类型

第 6 章 数据类型

数据类型在计算机语言中是非常重要的,在前面介绍变量或常量时已经用到一些数据类型,例如Int、Double和String等。本章主要介绍Kotlin的基本数据类型和可空类型。

6.1 回顾Java数据类型

Kotlin作为依赖于Java虚拟机运行的语言,它的数据类型最终被编译成为Java数据类型,所以本节先回顾一下Java数据类型的基础知识。

Java语言的数据类型分为:基本类型和引用类型。基本类型变量在计算机中保存的是数值,当赋值或作为参数传递给函数时基本类型数据会创建一个副本,把副本赋值或传递给函数,这副本被改变不会影响原始数据。引用类型在计算机中保存的是指向数据的内存地址,即引用,当赋值或作为参数传递给函数时引用类型数据会把引用赋值或传递给函数,事实上引用有多少个副本,都是指向相同的数据,通过任何一个引用修改数据,都会导致数据的变化。

基本类型表示简单的数据,基本类型分为4大类,共8种数据类型。

  • 整数类型:byte、short、int和long,int是默认类型。
  • 浮点类型:float和double,double是默认类型。
  • 字符类型:char。
  • 布尔类型:boolean。

基本数据类型如图6-1所示,其中整数类型、浮点类型和字符类型都属于数值类型,它们之间可以互相转换。

图6-1 Java基本数据类型

图6-1所示的8种基本数据类型不属于类,不具备“对象”的特征,没有成员变量和成员函数,不方便进行面向对象的操作。为此,Java提供包装类(Wrapper Class)来将基本数据类型包装成类,每个Java基本数据类型在java.lang包中都有一个相应的包装类,每个包装类对象封装一个基本数据类型数值。对应关系如表6-1所示,除int和char类型外,其他的类型对应规则就是第一个字母大写。

表 6-1 Java基本数据类型与包装类对应关系

基本数据类型

包装类

boolean

java.lang.Boolean

byte

java.lang.Byte

char

java.lang.Character

short

java.lang.Short

int

java.lang.Integer

long

java.lang.Long

float

java.lang.Float

double

java.lang.Double

6.2 Kotlin基本数据类型

与Java基本类型相对应,Kotlin也有8中基本数据类型。

  • 整数类型:Byte、Short、Int和Long,Int是默认类型。
  • 浮点类型:Float和Double,Double是默认类型。
  • 字符类型:Char。
  • 布尔类型:Boolean。

Kotlin基本数据类型如图6-2所示,其中整数类型和浮点类型都是属于数值类型,而字符类型不再属于数值类型。

图6-2 Kotlin基本数据类型

Kotlin的8个基本数据类型没有对应的包装类,Kotlin编译器会根据不同的场景将其编译成为Java中的基本类型数据还是包装类对象。例如,Kotlin的Int用来声明变量、常量、属性、函数参数类型和函数返回类型等情况时,被编译为Java的int类型;当作为集合泛型类似参数时,则被编译为Java的java.lang.Integer,这是因为Java集合中只能保存对象,不能是基本数据类型。Kotlin编译器如此设计是因为基本类型数据能占用更少的内存,运行时效率更高。

6.2.1 整型类型

从图6-2可见Kotlin中整数类型包括:Byte、Short、Int和Long,它们之间的区别仅仅是宽度和范围的不同。

Kotlin的数据类型与Java一样都是跨平台的(与平台无关),也就是计算机是32位的还是64位的,Byte类型整数都是一个字节(8位)。这些整数类型的宽度和范围如表6-2所示。

表 6-2 整数类型

整数类型

宽度

取值范围

Byte

1个字节(8位)

-128~127

Short

2个字节(16位)

-215~215-1

Int

4个字节(32位)

-231~231-1

Long

8个字节(64位)

-263~263-1

Kotlin语言中整型类型默认是Int类型,例如16表示为Int类型常量,而不是Short或Byte,更不是Long,Long类型需要在数值后面加L,示例代码如下:

//代码文件:chapter6/src/com/a51work6/ch6.2.1.kt
package com.a51work6

fun main(args: Array<String>) {
    // 声明整数变量
    // 输出一个默认整数常量
    println("默认整数常量 =  " + 16)     ①
    val a: Byte = 16         ②
    val b: Short = 16    ③
    val c = 16           ④
    val d = 16L          ⑤

    println("Byte整数     =  " + a)
    println("Short整数    =  " + b)
    println("Int整数      =  " + c)
    println("Long整数     =  " + d)
}

上述代码多次用到了16整数,但它们是有所区别的。其中代码①行和第④行的16是默认整数类型,即Int类型常量。代码②行的16是Byte整数类型。代码③行的16是Short类型。代码第⑤行的16后加了L,这是说明Long类型整数。

Java程序员注意 在Java中表示long类型整数时可以在数字后面加小写英文字母l,但由于可读性不好,容易被误认为是阿拉伯数字1,所以在Kotlin中部不允许这样表示。

在Java和Swift等语言中为了增强可读性,可以在较大的数字常量中添加下划线分割数字,Kotlin在1.1之后的版本增加了这一功能。示例代码如下:

//数字常量添加下划线,增强可读性
val e = 160_000_000L       //表示160000000数字
println("数字常量添加下划线   =  " + e)

分割的位置一般是按照统计习惯3位数字分割一下,但也不受这个限制。另外,下划线分割数字也适用于浮点数。

在使用整数变量赋值,还可以使用二进制和十六进制表示,但不支持八进制,它们的表示方式分别如下:

  • 二进制数:以 0b 或0B为前缀,注意0是阿拉伯数字,不要误认为是英文字母o。
  • 十六进制数:以 0x 或0X为前缀,注意0是阿拉伯数字。

例如下面几条语句都是表示int整数28。

val decimalInt = 28         //十进制表示
val binaryInt1 = 0b11100    //二进制表示
val binaryInt2 = 0B11100    //二进制表示
val hexadecimalInt1 = 0x1C    //十六进制表示
val hexadecimalInt2 = 0X1C    //十六进制表示

6.2.2 浮点类型

浮点类型主要用来储存小数数值,也可以用来储存范围较大的整数。它分为浮点数(Float)和双精度浮点数(Double)两种,双精度浮点数所使用的内存空间比浮点数多,可表示的数值范围与精确度也比较大。浮点类型说明如表6-3所示。

表 6-3 浮点类型

浮点类型

宽度

Float

4个字节(32位)

Double

8个字节(64位)

Kotlin语言的浮点类型默认是Double类型,例如0.0表示Double类型常量,而不是Float类型。如果想要表示Float类型,则需要在数值后面加f或F,示例代码如下:

//代码文件:chapter6/src/com/a51work6/ch6.2.2.kt
package com.a51work6

fun main(args: Array<String>) {
    // 声明浮点数
    // 输出一个默认浮点常量
    println("默认浮点常量    =  " + 360.66)   ①
    val myMoney = 360.66f  ②
    val yourMoney = 360.66 ③
    val pi = 3.14159F      ④

    println("Float浮点数  =  " + myMoney)
    println("Double浮点数 =  " + yourMoney)
    println("pi       =  " + pi)
}

上述代码①行的360.66是默认浮点类型Double。代码②行和第④行的360.66f是Float浮点类型,float浮点类型常量表示时,数值后面需要加f或F。代码第③行的360.66表示是Double浮点类型。

Java程序员注意 在Java中表示double浮点数时候还可以在数字后面加英文字母d或D,而在Kotlin中部不能这样表示。

进行数学计算时往往会用到指数表示的浮点数,表示指数需要使用大写或小写的e表示幂,e2表示102。示例如下:

// 指数表示方式
val ourMoney = 3.36e2            //指数表示336.0
val interestRate = 1.56E-2       //指数表示0.0156

其中3.36e2表示的是3.36×102,1.56e-2表示的是1.56×10-2

6.2.3 字符类型

字符类型表示单个字符,Kotlin中Char声明字符类型,Kotlin中的字符常量必须用单引号括起来的单个字符,如下所示:

val c: Char = 'A'

Kotlin字符采用双字节Unicode编码,占两个字节(16位),因而可用十六进制(无符号的)编码形式表示,它们的表现形式是\un,其中n为16位十六进制数,所以'A'字符也可以用Unicode编码'\u0041'表示,如果对字符编码感兴趣可以到维基百科(https://zh.wikipedia.org/wiki/Unicode字符列表)查询。

示例代码如下:

//代码文件:chapter6/src/com/a51work6/ch6.2.3.kt
package com.a51work6

fun main(args: Array<String>) {
    val c1 = 'A'
    val c2 = '\u0041'
    val c3: Char = '花'

    println(c1)
    println(c2)
    println(c3)
}

上述代码变量c1和c2都是保存的'A',所以输出结果如下:

A
A
花

在Kotlin中,为了表示一些特殊字符,前面要加上反斜杠(\),这称为字符转义。常见的转义符的含义参见表6-4。

表 6-4 转义符

字符表示

Unicode编码

说明

\t

\u0009

水平制表符tab

\n

\u000a

换行

\r

\u000d

回车

\"

\u0022

双引号

\'

\u0027

单引号

\\

\u005c

反斜线

\$

\u0024

美元符

\b

\u0008

退格

示例如下:

//转义符
//在Hello和World插入制表符
val specialCharTab1 = "Hello\tWorld."
//在Hello和World插入制表符,制表符采用Unicode编码\u0009表示
val specialCharTab2 = "Hello\u0009World."
//在Hello和World插入换行符
val specialCharNewLine = "Hello\nWorld."
//在Hello和World插入双引号
val specialCharQuotationMark = "Hello\"World\"."
//在Hello和World插入单引号
val specialCharApostrophe = "Hello\'World\'."
//在Hello和World插入反斜杠
val specialCharReverseSolidus = "Hello\\World."
//使用退格符
val specialCharReverseBack = "Hello\bWorld."
//在Hello和World插入美元符
val specialCharReverseUSD = "Hello\$World."

println("水平制表符tab1: " + specialCharTab1)
println("水平制表符tab2: " + specialCharTab2)
println("换行: " + specialCharNewLine)
println("双引号: " + specialCharQuotationMark)
println("单引号: " + specialCharApostrophe)
println("反斜杠: " + specialCharReverseSolidus)
println("退格符: " + specialCharReverseBack)
println("美元符: " + specialCharReverseUSD)

输出结果如下:

水平制表符tab1: Hello World.
水平制表符tab2: Hello World.
换行: Hello
World.
双引号: Hello"World".
单引号: Hello'World'.
反斜杠: Hello\World.
退格符: HellWorld.
美元符: Hello$World.

6.2.4 布尔类型

在Kotlin语言中声明布尔类型的关键字是Boolean,它只有两个值:true和false。

提示 在C语言中布尔类型是数值类型,它有两个取值:1和0。而在Kotlin和Java中的布尔类型取值不能用1和0替代,也不属于数值类型,不能与数值类型之间进行数学计算或类型转化。

示例代码如下:

val isMan = true
val isWoman = false

如果试图给它们赋值true和false之外的常量,代码如下所示:

val isMan1: Boolean = 1
val isWoman1: Boolean = 'A'

则发生类型不匹配编译错误。

6.3 数值类型之间的转换

学习了前面的数据类型后,大家会思考一个问题,数据类型之间是否可以转换呢?数据类型的转换情况比较复杂。在基本数据类型中数值类型之间可以互相转换,字符类型和布尔类型不能与它们之间进行转换。

本节讨论数值类型之间互相转换,数值在进行赋值时采用的是显示转换,而在数学计算时采用的是隐式转换。

6.3.1 赋值与显式转换

Kotlin是一种安全的语言,对于类型的检查非常严格,不同类型数值进行赋值是禁止的,示例代码如下:

val byteNum: Byte = 16
val shortNum: Short = byteNum //编译错误

上述代码试图将Byte数值16赋值给Short类型常量shortNum,Kotlin语言会发生编译错误,而在C、Objective-C和Java等其他语言中是可以编译成功的,这些语言中从小范围数到大范围数转换是隐式的(自动的)。

Kotlin中要想实现这种赋值转换,需要使用转换函数显式转换。Kotlin的6种数值类型(Byte、Short、Int、Long、Float和Double),以及Char类型都有如下7个转换函数:

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

通过上述7个转换函数可以实现7种类型(Byte、Short、Int、Long、Float、Double和Char)之间的任意转换。

注意 转换函数虽然可以实现任意转换,但是需要注意当大宽度数值转换为小宽度数值时,大宽度数值的高位被截掉,这可能会导致数据精度丢失。除非大宽度数值的高位没有数据,就是这个数值比较小的情况。

示例代码如下:

//代码文件:chapter6/src/com/a51work6/ch6.3.1.kt
package com.a51work6

fun main(args: Array<String>) {

    // 声明整数常量
    val byteNum: Byte = 16
    //val shortNum: Short = byteNum //编译错误
    val shortNum: Short = byteNum.toShort()// Byte类型转换为Short类型
    var intNum = 16

    val longNum: Long = intNum.toLong()// Int类型转换为Long类型   ①
    intNum = longNum.toInt()    // Long类型转换为Int类型     ②

    val doubleNum = 10.8
    println("doubleNum.toInt : " + doubleNum.toInt())// Double类型转换为Int类型,结果是10    ③
    // 声明Char常量
    val charNum = 'A'
    println("charNum.toInt : " + charNum.toInt())// Char类型转换为Int类型,结果是65    ④

    //精度丢失问题
    val llongNum = 6666666666L    ⑤
    println("llongNum : " + llongNum)

    println("llongNum.toInt : " + llongNum.toInt())//结果是-1923267926,精度丢失  ⑥

}

转换函数可以实现双向转换,上述代码第①行是将Int类型转换为Long类型,代码第②行是将Long类型转换为Int类型。代码第③行是将浮点数转换为整数,这种转换是将小数部分截掉。代码第④行是将Char类型转换为Int类型,Char类型在计算机中存放的Unicode编码,所以转换的结果是Unicode编码数值,65是A字符的Unicode编码数值。代码第⑤行的Long数值比较大,在代码第⑥行转换为Int类型发生了精度丢失。

6.3.2 数学计算与隐式转换

多个数值类型数据可以数学计算,由于参与进行数学计算的数值类型可能不同,编译器会根据上下文环境进行隐式转换。计算过程中隐式转换类型转换规则如表6-5所示。

表 6-5 计算过程中隐式转换类型转换规则

操作数1类型

操作数2类型

转换后的类型

Byte

Byte

Int

Byte

Short

Int

Byte、Short

Int

Int

Byte、Short、Int

Long

Long

Byte、Short、Int、Long

Float

Float

Byte、Short、Int、Long、Float

Double

Double

示例如下:

//代码文件:chapter6/src/com/a51work6/ch6.3.2.kt
package com.a51work6

fun main(args: Array<String>) {

    // 声明整数常量
    val b: Byte = 16
    val s: Short = 16
    val i = 16
    val l = 16L

    // 声明浮点变量
    val f = 10.8f
    val d = 10.8

    val result1 = b + b        //结果是Int类型
    val result2 = b + s        //结果是Int类型
    val result3 = b + s - i     //结果是Int类型
    val result4 = b + s - i + l  //结果是Long类型

    val result5 = b * s + i + f / l         //结果是Float类型
    val result6 = b * s + i + f / l + d     //结果是Double类型

}

从上述代码表达式的运算结果类型,可知表6-5所示的类型转换规则,这里不再赘述。

6.4 可空类型

Kotlin语言与Swift语言类似,默认情况下所有的数据类型都是非空类型(Non-Null),声明的变量都是不能接收空值(null)的。这一点与Java和Objective-C等语言有很大的不同。

6.4.1 可空类型概念

Kotlin的非空类型设计能够有些防止空指针异常(NullPointerException),空指针异常引起的原因是试图调用一个空对象的函数或属性,则抛出空指针异常。在Kotlin中可以将一个对象的声明为非空类型,那么它就永远不会接收空值,否则会发生编译错误。示例代码如下:

var n: Int = 10
n = null      //发生编译错误

上述代码n = null会发生编译错误,因为Int是非空类型,它所声明的变量n不能接收空值。但有些场景确实没有数据,例如查询数据库记录时,没有查询出符合条件的数据是很正常的事情。为此,Kotlin为每一种非空类型提供对应的可空类型(Nullable),就是在非空类型后面加上问号(?)表示可空类型。修改上面示例代码:

var n: Int? = 10
n = null        //可以接收空值(null)

Int?是可空类型,它所声明的变量n可以接收空值。可空类型在具体使用时会有一些限制:

  • 不能直接调用可空类型对象的函数或属性。
  • 不能把可空类型数据赋值给非空类型变量。
  • 不能把可空类型数据传递给非空类型参数的函数。

为了“突破”这些限制,Kotlin提供了如下运算符:

  • 安全调用运算符:?.
  • 安全转换运算符:as?
  • Elvis运算符:?:
  • 非空断言:!!

此外,还一个let函数帮助处理可空类型数据。本章重点介绍安全调用运算符(?.)、Elvis运算符(?:)和非空断言(!!)。

6.4.2 使用安全调用运算符(?.)

可空类型变量使用安全调用运算符(?.)可以调用非空类型的函数或属性。安全调用运算符(?.)会判断可空类型变量是否为空,如果是则不会调用函数或属性,直接返回空值;否则返回调用结果。

示例代码如下:

//代码文件:chapter6/src/com/a51work6/ch6.4.2.kt
//安全调用运算符(?.)
package com.a51work6

//声明除法运算函数
fun divide(n1: Int, n2: Int): Double? {

    if (n2 == 0) {//判断分母是否为0
        return null
    }
    return n1.toDouble() / n2
}

fun main(args: Array<String>) {

    val divNumber1 = divide(100, 0)    ①
    val result1 = divNumber1?.plus(100)//divNumber1+100,结果null ②
    println(result1)

    val divNumber2 = divide(100, 10)   ③
    val result2 = divNumber2?.plus(100)//divNumber2+100,结果110.0    ④
    println(result2)
}

上述代码自定义了divide函数进行除法运算,当参数n2为0的情况下,函数返回空值,所以函数返回类型必须是Double的可空类型,即Double?。

代码第①行和第③行都调用divide函数,返回值divNumber1和divNumber2都是可空类型,不能直接调用plus函数,需要使用“?.”调用plus函数。事实上由于divNumber1为空值,代码第②行并没有调用plus函数,而直接返回空值。而代码第④行是调用了plus函数进行计算返回结果。

提示 plus函数是一种加法运算函数,它将当数值与参数相加,与“+”运算符作用一样。事实上这是因为“+”通过调用plus函数进行运算符重载,实现加法运算。与plus类似的函数还有很多,这里不再赘述。

6.4.3 非空断言运算符(!!)

可空类型变量可以使用非空断言运算符(!!)调用非空类型的函数或属性。非空断言运算符(!!)顾名思义就是断言可空类型变量不会为空,调用过程是存在风险的,如果可空类型变量真的为空,则会抛出空指针异常;如果非则可以正常调用函数或属性。

修改6.4.2节代码如下:

//代码文件:chapter6/src/com/a51work6/ch6.4.3.kt
// 非空断言运算符(!!)
package com.a51work6

fun main(args: Array<String>) {

    val divNumber1 = divide(100, 10)
    val result1 = divNumber1!!.plus(100)//divNumber1+100,结果110.0           ①
    println(result1)

    val divNumber2 = divide(100, 0)
    val result2 = divNumber2!!.plus(100)//divNumber2+100,结果抛出异常   ②
    println(result2)
}

运行结果

110.0
Exception in thread "main" kotlin.KotlinNullPointerException
       at com.a51work6.Ch6_4_4Kt.main(ch6.4.4.kt:12)

上述代码第①行和第②行都调用plus函数,代码第①行可以正常调用,而代码第②行,由于divNumber2是空值,非空断言调用会发生异常。

6.4.4 使用Elvis运算符(?:)

有的时候在可空类型表达式中,当表达式为空值时,并不希望返回默认的空值,而是其他数值。此时可以使用Elvis运算符(?:),也称为空值合并运算符,Elvis运算符有两个操作数,假设有表达式:A ?: B,如果A不为空值则结果为A;否则结果为B。

Elvis运算符经常与安全调用运算符结合使用,重写上一节示例代码如下:

//代码文件:chapter6/src/com/a51work6/ch6.4.4.kt
//使用Elvis运算符(?:)
package com.a51work6

fun main(args: Array<String>) {

    val divNumber1 = divide(100, 0)
    val result1 = divNumber1?.plus(100) ?: 0//divNumber1+100,结果0           ①
    println(result1)

    val divNumber2 = divide(100, 10)
    val result2 = divNumber2?.plus(100) ?: 0//divNumber2+100,结果110.0 ②
    println(result2)
}

代码第①行和第②行都是用了Elvis运算符,divNumber1?.plus(100)表达式为空值,则返回0。divNumber2?.plus(100)表达式不为空值,则返回110.0。

Elvis运算符由来 Elvis一词是指美国摇滚歌手埃尔维斯·普雷斯利(Elvis Presley),绰号“猫王”。由于他的头型和眼睛很有特点,不用过多解释,从图6-3可见为什么?:叫做Elvis了。

{%}

图6-3 Elvis运算符由来

本章小结

本章主要介绍了Kotlin中的数据类型,重点介绍基本数据类型,其中数值类型如何互相转换是学习的难点。最后介绍了可空类型,可空类型是Kotlin语言的特色,读者需要理解并掌握它的几个运算符的使用。

目录

  • 目录
  • 简介
  • 前言
  • 第 1 章 开篇综述
  • 第 2 章 开发环境搭建
  • 第 3 章 第一个Kotlin程序
  • 第 4 章 Kotlin语法基础
  • 第 5 章 Kotlin编码规范
  • 第 6 章 数据类型
  • 第 7 章 字符串
  • 第 8 章 运算符
  • 第 9 章 程序流程控制
  • 第 10 章 函数
  • 第 11 章 面向对象编程
  • 第 12 章 继承与多态
  • 第 13 章 抽象类与接口
  • 第 14 章 函数式编程基石——高阶函数和Lambda表达式
  • 第 15 章 泛型
  • 第 16 章 数据容器——数组和集合
  • 第 17 章 Kotlin中函数式编程API
  • 第 18 章 异常处理
  • 第 19 章 线程
  • 第 20 章 协程
  • 第 21 章 Kotlin与Java混合编程
  • 第 22 章 Kotlin I/O与文件管理
  • 第 23 章 网络编程
  • 第 24 章 Kotlin与Java Swing图形用户界面编程
  • 第 25 章 轻量级SQL框架——Exposed
  • 第 26 章 反射
  • 第 27 章 注解
  • 第 28 章 项目实战1:开发PetStore宠物商店项目
  • 第 29 章 项目实战2:开发Kotlin版QQ2006聊天工具