2、变量和数据类型

变量和常量

变量

可变变量定义:var

var <标识符> : <类型> = <初始化值>
fun main(args: Array<String>) {
    var name = "John"
    println(name)
    name = "Tom"
    println(name)
}

常量

不可变常量定义:val,只能赋值一次的变量(类似Java中final修饰的变量)

val <标识符> : <类型> = <初始化值>
fun main(args: Array<String>) {
    val name = "John"
    println(name)
}

注意:常量与变量都可以没有初始化值,但是在引用前必须初始化

编译器支持自动类型判断,即声明时可以不指定类型,由编译器判断。

fun main(args: Array<String>) {
    var name: String // 如果不在声明时初始化则必须提供变量类型
    var age = 10 // 系统自动推断变量类型为Int
    name = "Jane"
    println(name)
    println(age)
}

基本数据类型

Kotlin 的基本数值类型包括 Byte、Short、Int、Long、Float、Double 等。

不同于 Java 的是,字符不属于数值类型,是一个独立的数据类型。

数字类型

整数类型

类型 大小(比特数) 最小值 最大值
Byte 8 -128 127
Short 16 -32768 32767
Int 32 -2,147,483,648 (-2^31) 2,147,483,647 (2^31 - 1)
Long 64 -9,223,372,036,854,775,808 (-2^63) 9,223,372,036,854,775,807 (2^63 - 1)

当初始化一个没有显式指定类型的变量时,编译器会自动推断为自 Int 起足以表示该值的最小类型。 如果不超过 Int 的表示范围,那么类型是 Int。 如果超过了,那么类型是 Long。 如需显式指定 Long 值,请给该值追加后缀 L。 显式指定类型会触发编译器检测该值是否超出指定类型的表示范围。

val one = 1 // Int
val threeBillion = 3000000000 // Long
val oneLong = 1L // Long
val oneByte: Byte = 1

无符号整数

Type Size (bits) Min value Max value
UByte 8 0 255
UShort 16 0 65,535
UInt 32 0 4,294,967,295 (2^32 - 1)
ULong 64 0 18,446,744,073,709,551,615 (2^64 - 1)

为使无符号整型更易于使用,Kotlin 提供了用后缀标记u整型字面值来表示指定无符号类型

val b: UByte = 1u  // UByte,已提供预期类型
val s: UShort = 1u // UShort,已提供预期类型
val l: ULong = 1u  // ULong,已提供预期类型

val a1 = 42u // UInt:未提供预期类型,常量适于 UInt
val a2 = 0xFFFF_FFFF_FFFFu // ULong:未提供预期类型,常量不适于 UInt

val a = 1UL // ULong,即使未提供预期类型并且常量适于 UInt

浮点数类型

类型 大小(比特数) 有效数字比特数 指数比特数 十进制位数
Float 32 24 8 6-7
Double 64 53 11 15-16

可以使用带小数部分的数字初始化 DoubleFloat 变量。 小数部分与整数部分之间用句点(.)分隔 对于以小数初始化的变量,编译器会推断为 Double 类型

val pi = 3.14 // Double
// val one: Double = 1 // 错误:类型不匹配
val oneDouble = 1.0 // Double

如需将一个值显式指定为 Float 类型,请添加 fF 后缀。 如果这样的值包含多于 6~7 位十进制数,那么会将其舍入:

val e = 2.7182818284 // Double
val eFloat = 2.7182818284f // Float,实际值为 2.7182817

数字字面常量

数值常量字面值有以下几种:

Kotlin 不支持八进制。

比较数字

数值比较

fun main(args: Array<String>) {
    var a: Int = 10
    var b: Double = 2.5
    println(a < b)
    println(a <= b)
    println(a > b)
    println(a >= b)
    println(a.toDouble() == b)
}

Kotlin 中 == 运算符用于结构相等性比较,即值的比较,而 === 运算符用于引用相等性比较,即对象是否是同一个实例。在比较基本数据类型时,通常使用 == 运算符。

默认情况下在 JVM 平台数字存储的是原生类型 intdouble 等。

但是当创建可空数字引用如 Int? 或者使用泛型时,数字会装箱为 Java 类 IntegerDouble 等。

由于 JVM 对 -128127 的整数(Integer)应用了内存优化,因此,a 的所有可空引用实际上都是同一对象。但是没有对 b 应用内存优化,所以它们是不同对象。

fun main(args: Array<String>) {
    val a: Int = 100
    val boxedA: Int? = a
    val anotherBoxedA: Int? = a

    val b: Int = 10000
    val boxedB: Int? = b
    val anotherBoxedB: Int? = b

    println(boxedA === anotherBoxedA) // true
    println(boxedB === anotherBoxedB) // false
}

显式数字类型转换

所有数字类型都支持转换为其他类型:

很多情况都不需要显式类型转换,因为类型会从上下文推断出来, 而算术运算会有重载做适当转换,例如:

val l = 1L + 3 // Long + Int => Long

数字运算

Kotlin支持数字运算的标准集:+-*/%。它们已定义为相应的类成员:

fun main() {
    println(1 + 2)
    println(2_500_000_000L - 1L)
    println(3.14 * 2.71)
    println(10.0 / 3)
}

其中除法、取余的逻辑和 Java 一样

同样,Kotlin 也有和 Java 一样的++以及--操作,放在变量前与变量后的逻辑也是一样的。

位运算

Kotlin提供了一组整数的位运算操作,可以直接在二进制层面上与数字表示的位进行操作,不过只适用于IntLong类型的数据:

字符类型

fun main(args: Array<String>) {
    var c : Char = 'a'
    println(c)
}

特殊字符可以以转义反斜杠 \ 开始。 支持这几个转义序列:

如果字符变量的值是数字,那么可以使用 digitToInt()函数将其显式转换为 Int 数字。

字符串类型

遍历字符

和 Java 一样,String 是不可变的。方括号[]语法可以很方便的获取字符串中的某个字符,也可以通过 for 循环来遍历:

fun main(args: Array<String>) {
    var str = "Hello World"
    // 使用 for 遍历
    for (s in str){
        println(s)
    }
    // 使用方括号取字符
    println(str[0])
}

多行字符串

Kotlin 支持三个引号 """ 扩起来的字符串

fun main(args: Array<String>) {
    var str = """
    你好
    Kotlin
    """
    println(str) // 输出有一些前置空格
}

可以通过trimIndent()方法来删除多余的空白。

fun main(args: Array<String>) {
    var str = """
    你好
    Kotlin
    """.trimIndent()
    println(str)
}

字符串模板

字符串可以包含模板表达式 ,即一些小段代码,会求值并把结果合并到字符串中。 模板表达式以美元符$开头

直接引用变量或常量

fun main(args: Array<String>) {
    var age = 10
    var msg = "my age is $age"
    println(msg)
}

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

fun main(args: Array<String>) {
    var str = "Hello"
    println("the str is $str,length is ${str.length}")
}

字符串拼接

拼接使用+,字符串除了和字符串拼接之外,也可以和其他类型进行拼接,但是我们需要注意字符串拼接的顺序,只能由字符串拼接其他类型,如果是其他类型拼接字符串,可能会出现问题。

fun main(args: Array<String>) {
    var a = "Hello"
    var b = 123;
    println(a + b)
    println(b + a) // 这样是错误的
    println("$b$a")
}

布尔类型

布尔值的内置运算有:

fun main(args: Array<String>) {
    var b = true
    println(b)
}

数组

数组类型放在后面另起一章

变量作用域

前面的变量都是声明在函数中的,此时也就是局部变量。可以将变量的作用域进行提升,将其直接变成一个顶级定义。此时,这个变量可以被所有的函数使用。

var name = "Tom"

fun main() {
    println("Hello, $name")
}

变量的作用域被提升到顶层,它可以具有更多的一些特性

getter和setter

我们在使用这种全局变量时,对于变量的获取和设定,本质上都是通过其getter和setter函数来完成的,只不过默认情况下不需要我们去编写

var <propertyName>[: <PropertyType>] [= <property_initializer>]
    [<getter>]
    [<setter>]
var name = "Tom"

fun main() {
    println(name) // 编译后,相当于 println(getName())
}

重写getter和setter

在默认情况下,变量的获取就是直接返回,设置就是直接修改,不过有些时候我们可能希望修改这些变量获取或修改时执行的操作,我们可以手动编写

注意:重写getter和setter 时候,使用变量field代表当前变量

注意,对于val类型的变量,没有set函数,因为不可变

var name = "Tom"
    get() = "$field$field"

fun main() {
    println(name) //TomTom
}
var name = "Tom"
    get() {
        println("get $field")
        return field
    }
    set(value) {
        println("set $value")
        field = value
    }

fun main() {
    name =  "Jerry" // set Jerry
    println(name)
    // get Jerry
    // Jerry
}
// 对于常量,有的时候也会这么写
val name get() = "Tom"
// 但是不常用,直接声明就可以了
val name = "Tom"

类型别名

如果有一个在代码库中多次用到的函数类型或者带有类型参数的类型,那么最好为它定义一个类型别名,使用typealias定义

typealias MouseClickHandler = (Any, MouseEvent) -> Unit
typealias PersonIndex = Map<String, Person>

空值和空类型

所有的变量除了引用一个具体的值之外,还有一种特殊的值可以使用,那就是null,它代表空值,也就是不引用任何对象。

在其他语言中,比如Java中null是一个非常常见的值,因为在某些情况下,引用类型的变量默认值就是null,这就经常会导致程序中出现一些空指针导致的异常,在Kotlin中,对空值处理是非常严格的,正常情况下,我们的变量是不能直接赋值为null的,否则会报错,无法编译通过。

这是因为所有的类型默认都是非空类型,非空类型的变量是不允许被赋值为null的,这直接在编译阶段就避免了其他语言中经常存在的空指针问题。

那么,如果我们希望某个变量在初始情况下使用null而不去引用某一个具体对象,该怎么做呢,此时我们需要将变量的类型修改为可空类型,只需在类型名称的后面添加一个?即可。

fun main() {
    var a: Int = null // 错误,不能赋值null给Int类型变量
    var b: Int? = null // 正确,可以赋值null给Int?类型变量
}

不过在有些情况下,我们可能已经非常清楚,这里的str一定不为null,即使它是一个可空类型变量,我们可以像这样做,来告诉编译器,我们这里一定是安全的,只管执行就好

fun main() {
    var str: String? = null
  	//使用非空断言操作符!!.来明确不会出现null问题
    println(str!!.length)
}

虽然通过?我们可以将一个变量初始化为 null,但是也带来了安全问题,如果在引用这个变量的时候仍未 null 呢,这个时候就需要对变量进行判断,看看其是否为null然后才能去做一些正常情况下该做的事情

fun main() {
    var str: String? = null
  	//这里直接通过if语句判断str变量是否为null,如果不是才执行
    if (str != null) {
        println(str.length)  //现在就可以编译通过了
    }
}

Kotlin为我们提供了一种更安全的空类型操作,要安全地访问可能包含null值的对象的属性,请使用安全调用运算符?.,如果对象的属性为null则安全调用运算符返回null

fun main() {
    var str: String? = null
    println(str?.length)
}

这里的调用结果存在两种情况:

不过在有些时候,可能我们希望如果变量为null,在使用安全调用运算符时,返回一个我们自定义的结果,我们可以使用Elvis运算符,这里我们使用了Elvis运算符来判断左侧是否为null,如果左侧为null,那么这里直接得到右侧的自定义值

fun main() {
    var str: String? = null
    println(str?.length ?: 0)
}

类型检测与类型转换

is 与 !is 操作符

使用 is 操作符或其否定形式 !is 在运行时检测对象是否符合给定类型:

if (obj is String) {
    print(obj.length)
}

if (obj !is String) { // 与 !(obj is String) 相同
    print("Not a String")
} else {
    print(obj.length)
}

智能转换

大多数场景都不需要在 Kotlin 中使用显式转换操作符,因为编译器跟踪不可变值的is检测以及显式转换,并在必要时自动插入(安全的)转换:

fun demo(x: Any) {
    if (x is String) {
        print(x.length) // x 自动转换为字符串
    }
}

不安全的转换操作符

通常,如果转换是不可能的,转换操作符会抛出一个异常。因此,称为不安全的。 Kotlin 中的不安全转换使用中缀操作符 as

val x: String = y as String

请注意,null 不能转换为 String 因该类型不是可空的。 如果 y 为空,上面的代码会抛出一个异常。 为了让这样的代码用于可空值,请在类型转换的右侧使用可空类型:

val x: String? = y as String?

安全的(可空)转换操作符

为了避免异常,可以使用安全转换操作符 as?,它可以在失败时返回 null

val x: String? = y as? String

请注意,尽管事实上 as? 的右边是一个非空类型的 String,但是其转换的结果是可空的。