【Kotlin】基础变量、集合和安全操作符

在Kotlin中,所有东西都是对象,在这个意义上讲我们可以在任何变量上调用成员函数和属性。
一些java中的基本数据类型可以有特殊的内部表示——例如,数字(int)、字符和布尔值可以在运行时表示为原生类型值

数字

Kotlin处理数字在某种程度上接近Java,但是并不完全相同。例如,对于数字没有隐式拓宽转换(如Java中int可以隐式转换为long), 另外有些情况的字面值略有不同。

Kotlin提供了如下的内置类型来表示:

Byte(字节):1字节
Short(短整型):2字节
Int(整型):4字节
Long(长整型):8字节
Float(单精度浮点型):4字节
Double(双精度浮点型):8字节
Boolean(布尔型):1字节
Char(字符型):2字节

注意:在Kotlin中字符不是数字,字符用Char类型表示。它们不能直接当作数字(可以用char.code表示出对应编码)

  val char = 'c'
    print(char.code)
    //99

字面常量

可以使用下划线使数字常量更易读,并且不影响赋值

val int = 1_0000_0000
print(int / 10000)
//10000

还可以:

val LongNumber = 999_99_9999L
val hexBytes = 0xFF_EC_DE_5E
val bytes = 0b11010010_01101001_10010100_10010010

显式转换

由于不同的表示方式,较小类型并不是较大类型的子类型。这代表他们不能进行直接的隐式转换:

在这里插入图片描述

我们可以显式转换来拓宽数字

fun main(){
    val int = 1_0000_0000
    val long: Long = int.toLong()
}

数值类型转换背后

var x = 5 // 这行代码创建了一个Int类型的变量x以及一个Int类型值为5的对象。x保存了该对象的引用
var z : Long = x.toLong() // 这行代码创建了一个新的Long变量z。x对象的toLong()函数被调用并且创建了一个值为5的Long对象,该Long对象的引用被存储在z中

如果该数值超出了新对象所能存储的范围该怎么办?
就像一大杯水装进一个小杯,有些会溢出

如果Long的值超出了Int能容纳的范围,那么编译器将会舍弃超出的部分,会得到一个奇怪(仍可计算)的数值。例如:

var x = 1234567890123
var y: Int = x.toInt()
println(y) // 1912276171

(C语言初学者应该很熟悉的问题)

位运算符

(只用于Int和Long。Kotlin的特性:可以重写操作符函数的逻辑):

shl(bits) – 有符号左移 (Java 的 <<)
shr(bits) – 有符号右移 (Java 的 >>)
ushr(bits) – 无符号右移 (Java 的 >>>)
and(bits) – 位与
or(bits) – 位或
xor(bits) – 位异或
inv() – 位非
区间实例以及区间检测:a..b、 x in a..b、 x !in a..b

字符串

字符串的元素——字符(Char)可以使用索引运算符访问:str[i]。可以用for循环迭代字符串:

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

当你使用双引号(“)表示字符串时,它只能包含单行文本。
而当你使用三个双引号(”“”)表示字符串时,它可以包含多行文本
转义采用传统的反斜杠方式。
原生字符串 使用三个引号"""分界符括起来,内部没有转义并且可以包含换行和任何其他字符:

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

字符串模板

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

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

字符串判等
Kotlin中的判等性主要有两种类型:

  • 结构相等。通过操作符==来判断两个对象的内容是否相等。
  • 引用相等。引用相等由双等号以及其否定形式!= = =操作判断。a = = = b当且仅当a和b指向同一个对象时求值为true。如果比较的是运行时的原始类型,比如Int,那么’= = =判断的效果也等价于’=='。
var a = "hello"
var b = "hello"
var c = "Kotlin"
var d = "Kot"
var e = "lin"
var f = d + e
a == b // true
a === b // true
c == f // true
c === f // false

结构相等由= =以及其否定形式!= =操作判断。
那么a = = b这样的表达式会被翻译为 a?.equals(b) ?: (b === null) 也就是说如果a不是null则调用equals(Any?)函数,否则即a是null检查b是否与null引用相等。
(精妙!)
好多等号一直显示高亮

修饰符

注意,在kotlin中默认的修饰符是public

  • private 所以如果我们给一个类声明为private, 我们就不能在定义这个类之外的文件中使用它。 另一方面,如果我们在一个类里面使用了private修饰符,那访问权限就被限制在这个类里面了。甚至是继承这个类的子类也不能使用它。
  • protected 在Java中是包、类及子类可访问,而在Kotlin中只允许类及子类。
  • internal 它与Java的default有点像但也有所区别。如果是一个定义为internal的包成员的话,对所在的整个module可见。如果它是一个其它领域的成员,它就需要依赖那个领域的可见性了。 比如如果写了一个private类,那么它的internal修饰的函数的可见性就会限制与它所在的这个类的可见性。
  • public 这是最没有限制的修饰符。这是默认的修饰符,成员在任何地方被修饰为public,很明显它只限制于它的领域。

数组

和java不同的是,新建一个数组用类Array实现,并且还有一个size属性及get和set,万物皆对象,由于kotlin使用[]重载了get和set方法,所以我们可以通过下标很方便的获取或者 设置数组对应位置的值。 Kotlin标准库提供了arrayOf()创建数组和xxArrayOf创建特定类型数组

val myArray = arrayOf(1, 2, 3)

还有:

val nums = arrayOf(1, 2, 3)
val numbers = intArrayOf(10, 20, 30)
//intArrayOf 新建int类型数组
val array1 = Array(10, { k -> k * k })
//可用表达式
val emptyArray = emptyArray<String>()
//括号内可以填参数,表示新建数组的大小

和Java不一样的,Kotlin的数组是容器类,提供了ByteArray,CharArray,ShortArray,IntArray,LongArray,BooleanArray,FloatArrayDoubleArray
那么以下两个有什么区别呢?

val arrayA = arrayOf(1, 2, 3)
val arrayB = intArrayOf(1, 2, 3)

当我们显式指明数据类型后:

val arrayA: Array<Int> = arrayOf(1, 2, 3)
val arrayB: IntArray = intArrayOf(1, 2, 3)

区别注意一下,arrayOf() 是Kotlin自带的,很便携,可以进行自判断类型生成对应的Array并填写泛型类。
但是Array是会使用装箱技术。它使用对象引用来存储每个整数(或其他基本数据类型),因此它的内存占用比较大。而IntArray由于不需要装箱和拆箱的过程,IntArray拥有更好的性能和更高的效率。但是也有一定的缺点,他比Array的api要少…

集合(Kotlin自带)

Kotlin有三个主要的集合类型(List、Set和Map),也可以分为可变集合和不可变集合//反正用得不多
简单的List、Set和Map是不可变的,这意味着集合被初始化后不能再添加或移除元素。如果想要添加或移除元素,Kotlin提供了可变的子类型作为替代方案:MutableList、MutableSet和MutableMap。因此,如果想要利用List的所有优势,并希望能够更新其内容,请使用MutableList。
Kotlin的List<out T>类型是一个提供只读操作如size、get等的接口。和Java类似,它继承自Collection<T>进而继承自Iterable<T>。 改变list的方法是由MutableList<T>加入的,且适用于SetMap

可变集合,顾名思义,就是可以改变的集合。可变集合都会有一个修饰前缀“Mutable”,比如MutableList。这里的改变是指改变集合中的元素,比如以下可变集合:

val list = mutableListOf(1, 2, 3, 4, 5)
list[0] = 0 // 变成[0, 2, 3, 4, 5]

Kotlin没有专门的语法结构创建list或set。要用标准库的方法如listOf()、mutableListOf()、setOf()、mutableSetOf()。 创建map可以用mapOf(a to b, c to d)。
最主要可以用 map[key] = value 我很喜欢这样用

var list = listOf(“a”, “b”, “c”)
for(c in list) {
println©
}

fun main() {
    var map = TreeMap<String, String>()
    map["0"] = "0 haha"
    map["1"] = "1 haha"
    map["2"] = "2 haha"
    println(map["1"])
}
val numbers: MutableList<Int> = mutableListOf(1, 2, 3)
val readOnlyView: List<Int> = numbers
println(numbers)        // "[1, 2, 3]"
numbers.add(4)
println(readOnlyView)   // "[1, 2, 3, 4]"
readOnlyView.clear()    // -> 不能编译
val strings = hashSetOf("a", "b", "c", "c")

Kotlin中提供了很多操作结合的函数,例如:

val newList = list.map{it * 2} // 对集合遍历,在遍历过程中,给每个元素都乘以2,得到一个新的集合
val mStudents = students.filter{it.sex == "m"} // 筛选出性别为男的学生
val scoreTotal = students.sumBy{it.score} // 拥挤和中的sumby实现求和

通过序列提高效率

因为filter方法和map方法都会返回一个新的集合,也就是说会产生两个临时集合(跟当初java:String一样),因为list会先调用filter方法,然后产生的集合会再次调用map方法。如果list中的元素非常多,这将会是一笔不小的开销。为了解决这一问题,序列(Sequence)就出现了。(Builder?)

list.asSequence().filter {it > 2}.map {it * 2}.toList()
  • 首先通过asSequence方法将一个列表转换为序列,然后在这个序列上进行相应的操作,最后通过toList方法将序列转为列表。将list转换为序列,在很大程度上就提高了上面操作集合的效率。

因为在使用序列的时候filter方法和map方法的操作都没有创建额外的集合,这样当集合中的元素数量巨大的时候,就减少了大部分开销。

  • 在Kotlin中,序列中元素的求值是惰性的,这就意味着在利用序列进行链式求值的时候,不需要像操作普通集合那样,每进行一次求值操作,就产生一个新的集合保存中间数据。

惰性求值

在编程语言理论中,惰性求值表示一种在需要时才进行求值的计算方式。 在使用惰性求值的时候,表达式不在它被绑定到变量之后就立即求值,而是在该值被取用时才去求值。 通过这种方式,不仅能得到性能上的提升,还有一个重要的好处就是它可以构造出一个无限的数据类型。

序列的操作方式

list.asSequence().filter {it > 2}.map {it * 2}.toList()

序列总共执行了两类操作分别是:

  • filter{it > 2}.map{it * 2}:filter和map的操作返回的都是序列,我们将这类操作称为中间操作。
  • toList():这一类操作将序列转换为List,我们将这类操作称为末端操作。

Kotlin中序列的操作就分为:

中间操作
  • 中间操作都是采用惰性求值的,例如:
list.asSequence().filter {
    println("filter($it)")
}.map {
    println("map($it)")
}

上面操作中的println方法没有执行,这说明filter和map方法的执行被延迟了,正所谓惰性求值。惰性求值仅仅在该值被需要的时候才会真正去求值。 那么怎么去触发这个”被需要“的状态?即——末端操作

末端操作
  • 末端操作就是一个返回结果的操作,它的返回值不能是序列,必须是一个明确的结果, 比如列表、数字、对象等表意明确的结果。
  • 末端操作一般都放在链式操作的末尾, 在执行末端操作的时候,会去触发中间操作的延迟计算,也就是将”被需要“这个状态被激活了
list.asSequence().filter {
    println("filter($it)")
    it > 2
}.map {
    println("map($it)")
    it * 2
}.toList()
// 输出↓
filter(1)
filter(2)
filter(3)
map(3)
filter(4)
map(4)
filter(5)
map(5)
[6, 8, 10]

可以看到,所有的中间操作都被执行了。从上面执行打印的结果我们发现,它的执行顺序与我们预想的不一样。

  • 普通集合在进行链式操作的时候会先在list上调用filter,然后产生一个结果列表,接下来map就在这个结果列表上进行操作。
  • 而序列则不一样,序列在执行链式操作的时候,会将所有的操作都应用在一个元素,也就是说,第一个元素执行完所有的操作之后,第二个元素再去执行所有的操作,以此类推。

可null类型

因为在Kotlin中一切都是对象,一切都是可null的。这就会防止出现NPE异常。当某个变量的值可以为null的时候,必须在声明处的类型后添加?来标识该引用可为空。 Kotlin通过?将是否允许为空分割开来,甚至Any也分可空和不可空。

var value1: String
value1 = null        // 编译错误 Null can not be a value of a non-null type String
var value2 : String?
value2 = null       // 编译通过

在对可空变量进行操作时,如果变量是可能为空的,那么将不能直接调用,否则将会报错。

安全调用操作符 ?.

当你对一个可空类型的变量进行操作时,如果直接调用方法或访问属性,可能会导致空指针异常,那么此时可以使用 ?.

val length = name?.length

如果name不是null,length将返回name的长度;如果name是null,length将直接返回null,而不会抛出异常。

操作符 ?:

该操作符允许我们为可能为null的表达式提供一个默认值。

val length = name?.length ?: 0

在这个例子中,如果name不为null,length将返回其长度;如果name为null,length将返回0。

非空断言操作符 !!

当你确定一个变量绝对不为空时,你可以使用!!操作符,这将告诉编译器说,我已经排除了null值的情况。但是,如果该变量为null,还是会抛出空指针异常。

val length = name!!.length

请谨慎使用!!操作符,因为它会忽略Kotlin中对null安全性的所有保护措施。

使用类型检测及自动类型转换

is运算符检测一个表达式是否某类型的一个实例,如同Java的instanceof。 如果一个不可变的局部变量或属性已经判断出为某类型,那么检测后的分支中可以直接当作该类型使用,在大多数情况下,is操作符会进行智能转换。表示编译器将变量当作与其声明的类型不同的类型,而智能转换是说编译器替你自动地进行转换。 无需显式转换:

fun getStringLength(obj: Any): Int? {
    if (obj !is String) return null
    // obj 在这一分支自动转换为 String,这是因为Kotlin的编译器帮我们做了转换
    // 这称为Kotlin中的智能转换(Smart Casts)。官方文档中这样介绍: 当且仅当Kotlin的编译器
    // 确定在类型检查后该变量不会再改变,才会产生Smart Casts。
    return obj.length
}

只要编译器能够保证在介于判断对象类型和被使用之间不能修改变量,is操作符就会进行智能转换。

例如,在上面的代码中,编译器知道在介于调用is操作符和调用String的某个方法之间,item变量不能被赋予另一类型的引用。但是在一些特殊情况下,智能转换不会生效。例如,is操作符不会对类中的var属性进行智能转换,那是因为编译器无法保证该属性是否还会被更改。

安全的类型转换 as?

安全类型转换操作符as?尝试进行类型转换,如果失败了不会抛出异常,而是返回null。

val a: Int? = "123".toIntOrNull()//将字符串"123"转换为整数类型,由于转换成功,所以intValue的值为123。
val b: Int? = "abc" as? Int //将字符串"abc"尝试转换为整数类型,由于转换失败,所以intValueOrNull的值为null。
//a = 123  b = null

返回和跳转

Kotlin有三种结构化跳转表达式,和java差不太多:

  • return:默认从最直接包围它的函数或者匿名函数返回。
  • break:终止最直接包围它的循环。
  • continue:继续下一次最直接包围它的循环。
    在Kotlin中任何表达式都可以用标签label来标记。标签的格式为标识符后跟@符号,例如:test@
    要为一个表达式加标签,我们只要在其前加标签即可。
loop@ for (i in 1..100) {
    for (j in 1..100) {
        if (i + j == 100) break@loop//退出指定的循环  C语言的goto?
    }
}

本图文内容来源于网友网络收集整理提供,作为学习参考使用,版权属于原作者。
THE END
分享
二维码
< <上一篇
下一篇>>