文章目录
前言
有句话说的好:种一棵树的最好时间第一是十年前,其次是现在。我第一次听说kotlin,大概是18年12月的时候。真正开始系统学习是在20年上半年,现在stdlib版本都更到1.4.31了貌似,我看好kotlin,写篇笔记方便查阅。
kotlin推进时间表
零碎知识点
- 定义一个普通变量
//定义一个普通变量
public final var MAX_MONTH:Int=12
- kotlin实现java的静态方法
kotlin没有static关键字,但是有伴生对象,可以实现Java中静态方法调用和静态变量修改,静态方法使用@JvmStatic关键字实现
companion object{
/**点击选择接口回调*/
interface ResultHandler{
abstract fun handle(time: String)
}
var MAX_MONTH:Int=12
}
lateinit关键字
延迟加载:Kotlin会使用null来对每一个用lateinit修饰的属性做初始化,
而基础类型是没有null类型,所以这个关键字只适用非基础类型
for循环
private var star: Int = 0
private var end: Int = 100
//正遍历
for (index in star..end){}
//倒序遍历
for (index in end downto start){}
//可以设置遍历步长(step使用)
for (index in star..end step 2){}
//不包含末尾 (util使用)
for (index in star util end){}
//遍历一个数组/列表,想同时取出下标和元素(withIndex使用)
val array = arrayOf("a", "b", "c")
for ((index,e) in array.withIndex()){
println("下标=$index----元素=$e")
}
//遍历一个数组/列表,只取出下标(indices使用)
val array = arrayOf("a", "b", "c")
for (index in array.indices){
println("index=$index")//输出0,1,2
}
//遍历取元素(element)
val array = arrayOf("a", "b", "c")
for (element in array){
println("element=$element")//输出a,b,c
}
嵌套类与内部类
如果类嵌套,默认是嵌套类。内部类需要使用innner修饰。前者相当于Java的静态内部类,而后者持有外部类的引用。
空安全设计
在 Kotlin 里面,所有的变量默认都是不允许为空的。
这种类型之后加 ? 的写法,在 Kotlin 里叫可空类型
你可以在类型右边加一个 ? 号,解除它的非空限制,比如
var name: String? = null
「可能为空」的变量,Kotlin 不允许用。就算你加上if语句判断也会报错。
意思是即使你检查了非空也不能保证下面调用的时候就是非空,因为在多线程情况下,其他线程可能把它再改成空的。
kotlin的[safe call]非空确认之后再调用方法
var view: View? = null
view?.setBackgroundColor(Color.RED)
加双感叹号就是「non-null asserted call」,意思是肯定不为空,出问题我负责,这样就和java没什么区别了,但也就享受不到 Kotlin 的空安全设计带来的好处了。(在编译时做检查,而不是运行时抛异常)
延迟初始化
告诉编译器我没法第一时间就初始化,但我肯定会在使用它之前完成初始化的。延迟初始化对变量的赋值次数没有限制。
类型推断
有些类型(比如String)直接赋值就完事了,有类型推断。
声明变量除了使用var还可以使用val
val 是 Kotlin 在 Java 的「变量」类型之外,又增加的一种变量类型:只读变量。它只能赋值一次,不能修改。
可见性
在 Kotlin 里变量默认就是 public 的,而对于其他可见性修饰符,之后再说。
函数
Java 的方法(method)在 Kotlin里叫函数(function),其实没啥区别,或者说其中的区别我们可以忽略。对任何编程语言来讲,变量就是用来存储数据,而函数就是用来处理数据。
对比java没有返回值,返回void的情况,Kotlin 里是返回 Unit,并且可以省略。
fun main(): Unit {}
基本类型
在 Kotlin 中,所有东西都是对象。
var number: Int = 1 // 👈还有 Double Float Long Short Byte 都类似
var c: Char = 'c'
var b: Boolean = true
var array: IntArray = intArrayOf(1, 2) // 👈类似的还有 FloatArray DoubleArray CharArray 等,intArrayOf 是 Kotlin 的 built-in 函数
var str: String = "string"
原先在 Java 里的基本类型,类比到 Kotlin 里面,条件满足如下之一就不装箱:
不可空类型。
使用 IntArray、FloatArray 等。
java中的implement和extends在kotlin中都可以用“:”实现。
- open关键字的使用。
Kotlin 里的类默认是 final 的,而 Java 里只有加了 final 关键字的类才是 final 的。
所以,当你定义了一个类,没有加open的话,他是不能被继承,而open关键字就是用来解除这个限制的。
强转(is和as关键字使用)
在 Java 里,需要先使用 instanceof 关键字判断类型,再通过强转来调用。
Kotlin 里同样有类似解决方案,使用 is关键字进行「类型判断」,并且因为编译器能够进行类型推断,可以帮助我们省略强转的写法:
fun main() {
var activity: Activity = NewActivity()
if (activity is NewActivity) {
//强转由于类型推断被省略了
activity.action()
}
}
fun main() {
var activity: Activity = NewActivity()
// 👇'(activity as? NewActivity)' 之后是一个可空类型的对象,所以,需要使用 '?.' 来调用
(activity as? NewActivity)?.action()
}
如果强转成功就执行之后的调用,如果强转不成功就不执行。
构造器 constructor
熟悉java构造方法的我们知道,通常是public,那在kotlin中构造方法是如何实现的呢?
class User {
val id: Int
val name: String
constructor(id: Int, name: String) {
// 没有 public
this.id = id
this.name = name
}
}
init的使用,初始化代码块
在java中,我们知道想要在构造方法前做些事情,只要两个大括号就行(static关键字可加可不加)
在kotlin中
class User {
init {
// 初始化代码块,先于下面的构造器执行
//如果是主构造器,因为主构造器没有代码体,所以init紧随其后执行。
}
constructor() {
}
}
object关键字
java中的是大写开头的Object,在 Kotlin 中变成了 Any,和 Object 作用一样:作为所有类的基类。
而object关键字意思很直接:创建一个类,并且创建一个这个类的对象。
单例实现使用kotlin变得简单,用object修饰实现的单例,是一个饿汉式的单例,实现了线程安全。
与java相比,不需要维护实例变量xxinstance,也不需要getInstance方法。
- 匿名类如何写?使用object关键字
val listener = object: ViewPager.SimpleOnPageChangeListener() {
override fun onPageSelected(position: Int) {
// override
}
}
和 Java 创建匿名类的方式很相似,只不过把 new 换成了 object::
Java 中 new 用来创建一个匿名类的对象;
Kotlin 中 object: 也可以用来创建匿名类的对象;
这里的 new 和 object: 修饰的都是接口或者抽象类。
- Java 静态变量和方法的等价写法:companion object 变量和函数
可以省略对象名,比如下面代码,其实B写不写都无所谓,都是用A.c直接拿到变量
class A {
companion object B {
var c: Int = 0
}
}
顶层声明
top-level property / function 声明
除了静态函数这种简便的调用方式,Kotlin 还有更方便的东西:「top-level declaration 顶层声明」。其实就是把属性和函数的声明不写在 class 里面,这个在 Kotlin 里是允许的。
调用也简单,直接通过 import包名.函数名,就能用了。还能用包名区分相同名字的方法,真是方便。
常量 关键字const
Java中不说了,public static final。
Kotlin 的常量必须声明在对象(包括伴生对象)或者「top-level 顶层」中。
Kotlin 新增了修饰常量的 const 关键字。
class Sample {
companion object {
const val CONST_NUMBER = 1
}
}
const val CONST_SECOND_NUMBER = 2
Kotlin 中只有基本类型和 String 类型可以声明成常量。java中一些static final的类也可以作为常量,但在kotlin中却不行。因为可以通过set方法,修改,可以理解成「伪常量」
原因是 Kotlin 中的常量指的是 「compile-time constant 编译时常量」。
数组
Kotlin 中的数组是一个拥有泛型的类,创建函数也是泛型函数,和集合数据类型一样。
kotlin中数组不支持协变。
子类数组对象不能赋值给父类的数组变量。java中却可以。
//在java中可以
String[] strs = {"a", "b", "c"};
Object[] objs = strs; // success
val strs: Array<String> = arrayOf("a", "b", "c")
val anys: Array<Any> = strs //会报错compile-error: Type mismatch
集合
Kotlin 和 Java 一样有三种集合类型:List、Set 和 Map。
Kotlin 中创建一个 List 特别的简单,有点像创建数组的代码。而且 Kotlin 中的 List 多了一个特性:支持 covariant(协变)。也就是说,可以把子类的 List 赋值给父类的 List 变量:
val strs: List<String> = listOf("a", "b", "c")
val anys: List<Any> = strs // success
在java中,会报错:不兼容
List<String> strList = new ArrayList<>();
List<Object> objList = strList; // compile error: incompatible types
kotlin中创建一个map
//创建一个List
val strList = listOf("a", "b", "c")
//创建一个Set
val strSet = setOf("a", "b", "c")
//创建一个Map(to 表示将「键」和「值」关联,这个叫做「中缀表达式」)
val map = mapOf("key1" to 1, "key2" to 2, "key3" to 3, "key4" to 3)
可变集合和不可变集合
listOf() 创建不可变的 List,mutableListOf() 创建可变的 List。
setOf() 创建不可变的 Set,mutableSetOf() 创建可变的 Set。
mapOf() 创建不可变的 Map,mutableMapOf() 创建可变的 Map。
可以通过toMutable*()方法转换成可变集合。"*"替换成上述任意集合类型
可见修饰符
Kotlin 中有四种可见性修饰符:
public:公开,可见性最大,哪里都可以引用。
private:私有,可见性最小,根据声明位置不同可分为类中可见和文件中可见。
protected:保护,相当于 private + 子类可见。
internal:内部,仅对 module 内可见。
相比 Java 少了一个 default 「包内可见」修饰符,多了一个 internal「module 内可见」修饰符。
private 修饰内部类的变量时,在 Java 和 Kotlin 中的区别
在 Java 中,外部类可以访问内部类的 private 变量
在 Kotlin 中,外部类不可以访问内部类的 private 变量
class Outter {
fun method() {
val inner = Inner()
val result = inner.number * 2 // compile-error: Cannot access 'number': it is private in 'Inner'
}
class Inner {
private val number = 1
}
}
构造器
主构造器只能有一个,次构造器能有无数个,但是必须调用主构造器
函数简化
使用=号,再运用kotlin的类型推断特征,下面放原版和简化版
//原版
fun area(width: Int, height: Int): Int {
return width * height
}
//简化版本1
fun area(width: Int, height: Int): Int = width * height
//简化版本2
fun area(width: Int, height: Int)= width * height
参数默认值
在函数入参,可以指定默认值,如果没有传,就使用默认值。
fun sayHi(name: String = "world") = println("Hi " + name)
命名参数
前面讲到参数默认值,如果有2个参数,1个又默认值,1个没有,那么怎么写呢?这时候就用到了命名参数。
fun sayHi(name: String = "world", age: Int) {
...
}
//用法
sayHi(age = 21)
还有种情况,就是参数特别多,按照java的习惯,必须按照顺利传,而kotlin的命名参数可以让你不按照顺序来。
fun sayHi(name: String = "world", age: Int, isStudent: Boolean = true, isFat: Boolean = true, isTall: Boolean = true) {
...
}
//例子
sayHi(name = "wo", age = 21, isStudent = false, isFat = true, isTall = false)
嵌套函数
嵌套函数中可以访问在它外部的所有变量或常量,但是外部函数之外的地方是无法访问这个嵌套函数的。
字符
字符用 Char 类型表示。
字符字面值用单引号括起来: ‘1’。 特殊字符可以用反斜杠转义。 支持这几个转义序列:\t、 \b、\n、\r、’、"、\ 与 $。 编码其他字符要用 Unicode 转义序列语法:’\uFF00’。
字符有各种转换的方法,比如toInt
字符串
java中,通常是直接通过符号"+"拼接,kotlin也可以这样,但是当变量比较多的时候,可读性就出现问题了。
java的解决方案是String.format方法。
System.out.print(String.format("Hi %s", name));
在kotlin中则更加简洁,使用 ‘$’ 符号加参数的方式
var yui= "习惯"
println("name is $yui")
甚至可以通过"{}"拿变量的属性
var yui= "习惯"
println("name is ${yui.length}")
原生字符串 “”"
转义字符不会被转义
//trimMargin作用是去掉行头的空格,$字符也能都生效了
val text = """
👇
|Hi world!
|My name is kotlin.
""".trimMargin()
println(text)
range 区间
java中没有这个概念
// [0,1000]
val range: IntRange = 0..1000
//[0, 1000)
//val range: IntRange = 0 until 1000
//[4,1] downTo
for (i in 4 downTo 1) {
print("$i, ")
}
数组和集合的操作符
val intArray = intArrayOf(1, 2, 3)
val strList = listOf("a", "b", "c")
forEach:遍历每一个元素
intArray.forEach { i ->
print(i + " ")
}
filter:对每个元素进行过滤操作,最终会生成新的集合。
// [1, 2, 3]
↓
// {2, 3}
val newList: List = intArray.filter { i ->
i != 1 // 过滤掉数组中等于 1 的元素
}
map:遍历每个元素并执行给定表达式,最终形成新的集合。
// [1, 2, 3]
↓
// {2, 3, 4}
val newList: List = intArray.map { i ->
i + 1 // 👈 每个元素加 1
}
flatMap:遍历每个元素,并为每个元素创建新的集合,最后合并到一个集合中
// [1, 2, 3]
↓
// {"2", "a" , "3", "a", "4", "a"}
var intArray = intArrayOf(1, 2, 3)
var newArray = intArray.flatMap { i ->
listOf("${i + 1}", "a") // 生成新集合
}
Sequence
「惰性集合操作」Sequence是个接口
Sequence 这种类似懒加载的实现有下面这些优点:
一旦满足遍历退出的条件,就可以省略后续不必要的遍历过程。
像 List 这种实现 Iterable 接口的集合类,每调用一次函数就会生成一个新的Iterable,下一个函数再基于新的 Iterable 执行,每次函数调用产生的临时 Iterable >会导致额外的内存消耗,而 Sequence 在整个流程中只有一个。
因此,Sequence 这种数据类型可以在数据量比较大或者数据量未知的时候,作为流式处理的解决方案。
val sequence = sequenceOf(1, 2, 3, 4)
val result: Sequence<Int> = sequence
.map { i ->
println("Map $i")
i * 2
}
.filter { i ->
println("Filter $i")
i % 3 == 0 //过滤掉不满足条件的,满足条件则生成新的Sequence<Int>对象
}
println("--------------")
println(result.toList())//集合里应该只有一个元素6,而且上面的加载过程,i=4并没有打印,到3就结束了
println("--------------")
println(result.first()) // 只取集合的第一个元素
再看List例子,声明之后立即执行,并且是map模块执行完后再执行filter模块,与Sequence的例子不太一样。
val list = listOf(1, 2, 3, 4)
val result: List<Int> = list
.map { i ->
println("Map $i")
i * 2
}
.filter { i ->
println("Filter $i")
i % 3 == 0
}
println(result.first())
条件语句
if/else
kotlin中if是一个表达式,他会返回一个值,用他就能实现java中3元运算符功能(条件 ? 然后 : 否则)。
示例
var max=if(a>b){
prinln("a big")
a
}else{
prinln("b big")
b
}
when
Java中使用switch,kotlin用when
when (x) {
1 -> { println("1") }
2 -> { println("2") }
//else相当于default
else -> { println("else") }
}
此外可以检测一个值在(in)或者不在(!in)一个区间、检测一个值是(is)或者不是(!is)一个特定类型的值。
for
可以对任何提供迭代器(iterator)的对象进行遍历
简单写一个-遍历数组
val array = intArrayOf(1, 2, 3, 4)
for (item in array) {
...
}
for 循环可以对任何提供迭代器(iterator)的对象进行遍历,这相当于像 C# 这样的语言中的 foreach 循环。
for(item in collection) print(item)
//结合库函数withIndex使用
for ((index, value) in array.withIndex()) {
println("the element at $index is $value")
}
返回和跳转
return、break、continue
基本和java一样。
补充1:Nothing对象
throw 表达式的类型是特殊类型Nothing。该类型没有值,而是用于标记永远不能达到的代码位置。
val s = person.name ?: return
结合标签使用
在kotlin中任何表达式都可以用标签来标记。标签的格式为标识符后跟@符号,例如:abc@、fooBar@都是有效的标签。
loop@ for (i in 1..100) {
for (j in 1..100) {
if (……) break@loop
}
}
//为什么不用continue?因为break和continue只能用在一个循环里,forEach这里是方法,然后kotlin里貌似没有foreach循环,只有for循环
fun foo() {
listOf(1, 2, 3, 4, 5).forEach lit@{
if (it == 3) return@lit // 局部返回到该 lambda 表达式的调用者,即 forEach 循环
print(it)
}
print(" done with explicit label")
}
空安全判断
Elvis 操作符“?:”
val str: String? = "Hello"
//大概意思:如果左侧表达式 str?.length 结果为空,则返回右侧的值 -1
val length: Int = str?.length ?: -1
因为 throw 和 return 在 Kotlin 中都是表达式,所以它们也可以用在 elvis 操作符右侧。
//null就返回
fun validate(user: User) {
val id = user.id ?: return // 👈 验证 user.id 是否为空,为空时 return
}
// 等同于
fun validate(user: User) {
if (user.id == null) {
return
}
val id = user.id
}
?使用
示例1
//只会打印null,不会报空指针
val b: String? = null
println(b?.length)
//链式调用不用频繁的判空
bob?.department?.head?.name
// 如果 `person` 或者 `person.department` 其中之一为空,都不会调用函数getManager
person?.department?.head = managersPool.getManager()
链式调用中很有用,任意一个属性(环节)为空,这个链式调用就会返回 null。
示例2 安全的类型转换
//如果转换不成功则返回null
val aInt: Int? = a as? Int
示例3 使用filterNotNull过滤可空类型元素的集合中的非空元素
val nullableList: List<Int?> = listOf(1, 2, null, 4)
val intList: List<Int> = nullableList.filterNotNull()
非空断言运算符(!!)
将任何值转换为非空类型,若该值为空则抛出异常。
解构声明
把一个对象解构成很多变量会很方便。这种语法称为解构声明。一个解构声明可以同时创建多个变量,同时使用他们。
val (name, age) = person
一个解构声明会被编译成以下代码:
val name = person.component1()
val age = person.component2()
关于componentN函数,数据类默认会实现,当然也可以重写,需要用 operator 关键字标记,以允许在解构声明中使用它们。
位运算
对于位运算,没有特殊字符来表示,而只可用中缀方式调用具名函数,示例
val x = (1 shl 2) and 0x000FF000
完整的位运算列表(只用于 Int 与 Long):
shl(bits) – 有符号左移
shr(bits) – 有符号右移
ushr(bits) – 无符号右移
and(bits) – 位与
or(bits) – 位或
xor(bits) – 位异或
inv() – 位非
比较
普通的相等和比较就不说了,重点讲一下
- 区间实例和区间监测:a…b、 x in a…b、 x !in a…b
- == 和 === :== 相当于java中的equals,对基本数据类型以及 String 等类型进行内容比较。=== 相当于java中的==,对内存地址进行比较。
数组
数组在Kotlin中用Array表示
示例
// 创建一个 Array<String> 初始化为 ["0", "1", "4", "9", "16"]
val asc = Array(5) { i -> (i * i).toString() }
asc.forEach { println(it) }
原生类型数组
Kotlin也有无装箱开销的类来表示原生类型数组,比如ByteArray、 ShortArray、IntArray 等。
示例
val arr = IntArray(5)
//使用 lambda 表达式初始化数组中的值
var arr = IntArray(5)->{it*1}
类与继承
- Any是所有类的超类。有三个方法:equals()、 hashCode() 与 toString()。
- 一个类有一个主构造函数,可以有一个或者多个次构造函数。
- 主构造函数没有注解或者可见性修饰符,可以省略constructor关键字。
- 主构造函数不能包含任何代码,初始化代码可以放在init关键字作为前缀的初始化模块。
- 一般来说,每个次级构造函数都会直接货间接委托给主构造函数,像下面这样
class Parent(name: String) {
var age = 0;
var sex = "man"
constructor(name: String, age: Int) : this("Main name 1") {
this.age = age;
println("constructor 1 $name , $age , $sex")
}
constructor(nickName: String, sex: String) : this("Main name 2") {
this.sex = sex;
println("constructor 2 $nickName , $age , $sex")
}
}
- 还有一种情况是,没有声明主构造函数,我们知道主构造函数是有的,会自动生成一个不带参数的,调用次级构造函数时隐式调用,但是代码中可以省略
lass Parent {
init {
// 初始化代码块本质就是主构造函数的方法体
// 因为主构造函数在类名头部声明不能带有方法体
println("Main constructor")
}
constructor(sex: String) {
println("constructor , $sex")
}
}
- 默认情况下Kotlin类是final的,不允许继承,除非使用open关键字标记它。
- 派生类初始化顺序
open class Base(val name: String) {
init { println("Initializing Base") }
open val size: Int =
name.length.also { println("Initializing size in Base: $it") }
}
class Derived(
name: String,
val lastName: String,
) : Base(name.capitalize().also { println("Argument for Base: $it") }) {
init { println("Initializing Derived") }
override val size: Int =
(super.size + lastName.length).also { println("Initializing size in Derived: $it") }
}
fun main() {
println("Constructing Derived(\"hello\", \"world\")")
val d = Derived("hello", "world")
}
打印结果为
Constructing Derived("hello", "world")
Argument for Base: Hello
Initializing Base
Initializing size in Base: 5
Initializing Derived
Initializing size in Derived: 10
属性
- 声明的属性
要使用一个属性,只要用名称引用它即可。(kotlin没有new字段)
- 幕后字段和幕后属性
当一个属性需要一个幕后字段时,Kotlin 会自动提供。这个幕后字段可以使用field标识符在访问器中引用。
接口
- 一个接口可以从其他接口派生,从而既提供基类型成员的实现也声明新的函数与属性。很自然地,实现这样接口的类只需定义所缺少的实现。
interface Person : Named {
val firstName: String
val lastName: String
override val name: String get() = "$firstName $lastName"
}
data class Employee(
// 不必实现“name”
override val firstName: String,
override val lastName: String,
) : Person
fun main() {
val staff = Employee(lastName="Li",firstName="Lei")
//打印Lei Li
println(staff.name)
}
- 解决覆盖冲突
实现多个接口时,可能会遇到同一方法继承多个实现的问题。
interface A {
fun foo() { print("A") }
fun bar()
}
interface B {
fun foo() { print("B") }
fun bar() { print("bar") }
}
class C : A {
override fun bar() { print("bar") }
}
class D : A, B {
override fun foo() {
super<A>.foo()
super<B>.foo()
}
override fun bar() {
super<B>.bar()
}
}
可见性修饰符
- public:默认的,声明随处可见
- protected:不适用于顶层声明,子类可见,其他和private一样
- private:只在这个类内部(包含其所有成员)可见
- internal:本模块内可见
补充知识:模块
一个模块是编译在一起的一套 Kotlin 文件:
- 一个 IntelliJ IDEA 模块;
- 一个 Maven 项目;
- 一个 Gradle 源集(例外是 test 源集可以访问 main 的 internal 声明);
- 一次 Ant 任务执行所编译的一套文件。
扩展
扩展乃是kotlin厉害的地方之一。Kotlin 能够扩展一个类的新功能而无需继承该类或者使用像装饰者这样的设计模式。
扩展函数
某个类的扩展函数一般新建一个以类名+"Ext"命名的文件,然后按照类名.方法名模板新增方法,之后就可以直接通过类名调用这个方法了,好像新增的函数就像那个原始类本来就有的函数一样,示例:
//字符串是否是邮箱
fun String?.isEmail(): Boolean {
return this?.let {
Pattern.matches(this, "^\\w+([-+.]\\w+)*@\\w+([-.]\\w+)*\\.\\w+([-.]\\w+)*\$")
}?:let {
false
}
}
扩展属性
和扩展函数很像,也是类名.参数名定义属性。
注意:由于扩展没有实际的将成员插入类中,因此对扩展属性来说幕后字段是无效的。这就是为什么扩展属性不能有初始化器。他们的行为只能由显式提供的 getters/setters 定义。示例:
data class Employee(
val firstName: String,
val lastName: String,
)
//扩展属性 布尔型 是否姓吴
val Employee.isWu:Boolean
get()=firstName.equals("Wu")
fun main() {
val staff = Employee(lastName="Li",firstName="Lei")
println(staff.isWu)
}
声明的扩展函数的可用范围
示例:给Host类添加一个扩展函数,但是这个函数只能在Connection类内部使用
class Host(val hostname: String) {
fun printHostname() { print(hostname) }
}
class Connection(val host: Host, val port: Int) {
fun printPort() { print(port) }
fun Host.printConnectionString() {
printHostname() // 调用 Host.printHostname()
print(":")
printPort() // 调用 Connection.printPort()
}
fun connect() {
/*……*/
host.printConnectionString() // 调用扩展函数
}
}
fun main() {
Connection(Host("kotl.in"), 443).connect()
//Host("kotl.in").printConnectionString(443) // 错误,该扩展函数在 Connection 外不可用
}
数据类
标准库提供了 Pair 与 Triple。平时创建的只保存数据的类叫数据类,用data关键字标记。
copy函数
在很多情况下,我们需要复制一个对象改变它的一些属性,但其余部分保持不变。
示例
val jack = User(name = "Jack", age = 1)
//copy了jack并且只修改年龄
val olderJack = jack.copy(age = 2)
解构声明示例
data class User(val name:String?,val age:Int?)
fun main() {
val jane=User(name = "Jane",age = 25)
//变量名字可以随意,但是顺序还是那个构造函数的顺序
val (p,q)=jane
println("$p, $q years of age")
}
密封类
用sealed关键字修饰。密封类用来表示受限的类继承结构:当一个值为有限几种的类型、而不能有任何其他类型时。在某种意义上,他们是枚举类的扩展:枚举类型的值集合也是受限的,但每个枚举常量只存在一个实例,而密封类的一个子类可以有可包含状态的多个实例。
特点
- 可以有伴生对象
- 二所有子类都必须在与密封类自身相同的文件中声明
- 密封类是自抽象的,不能实例化,可以有抽象成员。
某些时候,密封类比枚举类更好用。配合when使用示例如下:
sealed class Expr
data class Const(val number: Double) : Expr()
data class Sum(val e1: Expr, val e2: Expr) : Expr()
object NotANumber : Expr()
fun eval(expr: Expr): Double = when(expr) {
is Const -> expr.number
is Sum -> eval(expr.e1) + eval(expr.e2)
NotANumber -> Double.NaN
// 不再需要 `else` 子句,因为我们已经覆盖了所有的情况
}
fun main() {
println(eval(Const(1.2)))
}
泛型
Java中的泛型
先回顾一下,Java 提供了「泛型通配符」? extends 和 ? super。前者叫「上界通配符」,后者叫「下界通配符」。Java 的泛型本身具有「不可变性 Invariance」,在编译时发生类型擦除。
? extends
以? extends T为例
- 指定泛型T为类时,范围包括此类的直接和间接子类,也包括这个类本身。
- 指定泛型T为接口时,范围包括此接口以及他的子接口。(关于这里我为什么要把接口单独拎出来说,因为接口是一种规范,我认为他不是类,但是广义来说又可以认为他是一种特殊的抽象类)
示例
List<? extends TextView> textViews = new ArrayList<TextView>(); // 本身
List<? extends TextView> textViews = new ArrayList<Button>(); // 直接子类
List<? extends TextView> textViews = new ArrayList<RadioButton>(); // 间接子类
发现问题:++使用了上界通配符后,add方法不好使了,只能拿到数据,却添加不了。++
由于 add 的这个限制,使用了 ? extends 泛型通配符的 List,只能够向外提供数据被消费,从这个角度来讲,向外提供数据的一方称为「生产者 Producer」。对应的还有一个概念叫「消费者 Consumer」,对应 Java 里面另一个泛型通配符 ? super。
? super
以? super T为例
- 指定泛型T为类时,范围包括此类的直接和间接父类,也包括这个类本身。
- 指定泛型T为接口时,范围包括此接口以及他的父类接口。
示例
List<? super Button> buttons = new ArrayList<Button>(); // 本身
List<? super Button> buttons = new ArrayList<TextView>(); // 直接父类
List<? super Button> buttons = new ArrayList<Object>(); // 间接父类
发现问题:++使用了下界通配符后,add方法好使了,get方法拿到的Object需要强制转换才能用。++
补充1:区分子类和子类型的概念
以橘子和水果为例,见下图:
Crate 是 Crate<? extends Fruit> 的子类型,但是Crate 并不是 Crate<? extends Fruit> 的子类。
补充2:PECS 法则:「Producer-Extends, Consumer-Super」
补充3:30分钟学会UML图
帮助理解上界和下界的概念。
小结
- 型变包括:不型变、协变、逆变。指我们是否允许对参数类型进行子类型转换。解释如下:
Type variance refers to the techniques by which we can allow, or not allow, subtyping in our parameterized types.
- java的泛型本身不支持协变和逆变。
Java 中的泛型是不型变的,这意味着 List 并不是 List 的子类型。 为什么这样? 如果 List 不是不型变的,它就没比 Java 的数组好用到哪去。
- 使用泛型通配符 ? extends 来使泛型支持协变。修改功能受到了一定限制,比如add方法不好使了。
- 使用泛型通配符 ? super 来使泛型支持逆变。读取功能受到了一定限制,不能按照泛型类型读取了,需要强转,但是强转会不会成功却不一定。
- 通配符保证的是类型安全,不可变性完全是另外一回事。比如你使用一个生产者对象,如 List<? extends Foo>,在该对象上不允许调用 add() 或 set(),但是clear()不需要参数,依然可以调用。
kotlin中的泛型
kotlin中泛型
类型参数可以推断出来,例如从构造函数的参数或者从其他途径,允许省略类型参数。
型变注解
- out
类比java中的 ? extends - in
类比java中的 ? super
var textViews: List<out TextView>
var textViews: List<in TextView>
声明处型变
在java中,因为泛型是不型变的,有些代码看上去没问题,却通过不了编译。
// Java
interface Source<T> {
T nextT();
}
// Java
void demo(Source<String> strs) {
Source<Object> objects = strs; // !!!在 Java 中不允许
// ……
}
为解决上述问题,必须声明对象的类型为 Source<? extends Object>,但是这毫无意义,赋值和取值也显得非常麻烦,并没有让工作变轻松。
上述情况在kotlin中能很好的解决:
interface Source<out T> {
fun nextT(): T
}
fun demo(strs: Source<String>) {
val objects: Source<Any> = strs // 这个没问题,因为 T 是一个 out-参数
// ……
}
类型参数声明处提供,所以我们称之为声明处型变
class Producer<out T> {
fun produce(): T {
...
}
}
val producer: Producer<TextView> = Producer<Button>() // 这里不写 out 也不会报错
//val producer: Producer<out TextView> = Producer<Button>() // out 可以但没必要,但是如果声明处没有加out关键字,那么这里就得使用out
类型投影(使用处型变)
与Java类似,只是将“? extends T”换成了“out T”,代表协变;“? super T”换成了“in T”,代表逆变。
以Array为例:
class Array<T>(val size: Int) {
fun get(index: Int): T { ///* …… */ }
fun set(index: Int, value: T) { ///* …… */ }
}
该类在 T 上既不是协变的也不是逆变的。这造成了⼀些不灵活性。
考虑下列函数:
fun copy(from: Array<Any>, to: Array<Any>) {
assert(from.size == to.size)
for (i in from.indices)
to[i] = from[i]
}
错误代码:
val ints: Array<Int> = arrayOf(1, 2, 3)
val any: Array<Any> = Array<Any>(3) { "" }
copy(ints, any) // 错误
Array 在 T 上是不型变的,因此 Array 和 Array 之间没有关系。copy方法看上去没问题,但是调用起来编译器通不过
只需要修改一下copy方法,加一个out关键字:from: Array。这里发生的事情称为类型投影:我们说from不仅仅是一个数组,而是一个受限制的(投影的)数组:我们只可以调用返回类型为类型参数 T 的方法,如上,这意味着我们只能调用 get()。这就是我们的使用处型变的用法,并且是对应于 Java 的 Array<? extends Object>、 但使用更简单些的方式。
补充1:java中的单个‘?’和kotlin中的星投影(‘*’)
在java中,单个 ? 号也能作为泛型通配符使用,相当于 ? extends Object。
在kotlin中也有等价写法,就是星投影,作用是当T未知时,可以安全的读或者写。
对于 Foo ,其中 T 是一个具有上界 TUpper 的不型变类型参数,Foo<*> 对于读取值时等价于 Foo 而对于写值时等价于 Foo。
注意:如果你的类型定义里已经有了out或者in,那这个限制在变量声明时也依然在,不会被*号覆盖。
指定多个边界(where使用)
java中设置继承多个边界使用‘&’,示例:
class Monster<T extends Animal & Food>{
}
kotlin中使用where,示例:
//T既要满足是字符又要是实现了Comparable接口的对象
fun <T> copyWhenGreater(list: List<T>, threshold: T): List<String>
where T : CharSequence,
T : Comparable<T> {
return list.filter { it > threshold }.map { it.toString() }
}
reified关键字
Java 中的泛型存在类型擦除的情况,任何在运行时需要知道泛型确切类型信息的操作都没法用了。java的解决方法是额外传递一个Class类型的参数,然后通过Class#isInstance方法来检查:
<T> void check(Object item, Class<T> type) {
if (type.isInstance(item)) {
System.out.println(item);
}
}
kotlin中的解决方法是使用关键字reified配合inline来解决:
inline fun <reified T> printIfTypeMatch(item: Any) {
if (item is T) { // 里就不会在提示错误了
println(item)
}
}
再来个例子方便理解
inline fun <reified T> T?.notNull(notNullAction: (T) -> Unit, nullAction: () -> Unit) {
if (this != null) {
//invoke即调用,Method类中的native方法
notNullAction.invoke(this)
} else {
nullAction.invoke()
}
}
嵌套类与内部类
嵌套
类可以嵌套在其他类中,此外接口和类的所有组合都是可能的,比如接口中嵌套类,类中嵌套接口。
class Outer{
private val bar: Int = 1
class Nested {
fun foo() = 2
}
}
val demo = Outer.Nested().foo() // == 2
内部类(inner关键字)
标记为 inner 的嵌套类能够访问其外部类的成员。内部类会带有一个对外部类的对象的引用。
class Outer{
private val bar: Int = 1
inner class Nested {
fun foo() = bar
}
}
val demo = Outer.Nested().foo() // == 1
匿名内部类
很常见,来个例子
window.addMouseListener(object : MouseAdapter() {
override fun mouseClicked(e: MouseEvent) { /*……*/ }
override fun mouseEntered(e: MouseEvent) { /*……*/ }
})
枚举类
经常配合when使用,每个枚举常量都可以在声明中获取name和位置ordinal。
enum class RGB { RED, GREEN, BLUE }
inline fun <reified T : Enum<T>> printAllValues() {
print(enumValues<T>().joinToString { it.name })
}
fun main() {
printAllValues<RGB>()
}
对象
如果我们只需要“一个对象而已”,并不需要特殊超类型,可以这么写
val adHoc = object {
var x: Int = 0
var y: Int = 0
}
匿名类前文提过了。
伴生对象,用companion关键字标记。用的地方很多,比如使用类名作为限定符来调用,定义一些常量。
在 JVM 平台,如果使用 @JvmStatic 注解,你可以将伴生对象的成员生成为真正的静态方法和字段。
类型别名
作用是用来简化函数声明的。
比如创建一个类型别名
typealias Restaurant = Organization<(Currency, Coupon?) -> Sustenance>
然后使用它
interface RestaurantPatron {
fun makeReservation(restaurant: Restaurant)
fun visit(restaurant: Restaurant)
fun complainAbout(restaurant: Restaurant)
}
如果我们需要去查看别名背后隐藏细节是什么,只需要Common+Click单击就可以了。
注意事项
- 类型别名只能定义在代码顶层位置
- 把2个对象设置成同一种类型别名,那么调用方法的时候编译器将发现不了你传参顺序出现的问题
typealias UserId = UniqueIdentifier
typealias ProductId = UniqueIdentifier
interface Store {
fun purchase(user: UserId, product: ProductId): Receipt
}
//productId和userId哪怕传参顺序错了,但是因为被类型解释成同一种东西,所以不会报错
val receipt = store.purchase(productId, userId)
补充1:import as
你是否遇到过需要导入两个同名的类的情况,使用的时候非常难受,总有一个前面带着一串包名对不?但是现在可以解决了,好消息!import a’s’它允许你给一个类型、函数或者属性一个新的命名,然后你可以把它导入到一个文件中。
常用于
- 布局文件控件id
- 某个引用的类
委托
委托类
通过关键字 by 可以很方便的实现语法级别的委托模式。
示例:
interface DB{
fun save()
}
class SqlDB() : DB {
override fun save() { println("save to sql") }
}
class GreenDaoDB() : DB {
override fun save() { println("save to GreenDao") }
}
// 参数 通过 by 将接口实现委托给 db
// ↓ ↓
class UniversalDB(db: DB) : DB by db
fun main() {
UniversalDB(SqlDB()).save()
UniversalDB(GreenDaoDB()).save()
}
这种委托模式在我们实际编程中十分常见,UniversalDB 相当于一个壳,它提供数据库存储功能,但并不关心它怎么实现。具体是用 Sql 还是 GreenDao,传不同的委托对象进去就行。
然后有人就会说,我用java也可以实现啊,这个by有啥用啊
class UniversalDB implements DB {
DB db;
public UniversalDB(DB db) { this.db = db; }
// 手动重写接口,将 save 委托给 db.save()
@Override// ↓
public void save() { db.save(); }
}
no no no~上述接口只有一个方法,当我们想委托的接口方法很多的时候,这个by能极大的减少我们的代码量。比如我们给MutableList添加一个方法,kotln只需要几行,而java继承List接口后会增加很多代码。
class LogList(val log: () -> Unit, val list: MutableList<String>) : MutableList<String> by list{
fun getAndLog(index: Int): String {
log()
return get(index)
}
}
委托属性
委托属性,委托出去的是属性的 getter,setter。
val text: String = by lazy{}
// 它的原理其实跟下面是一样的
// 语法层面上是等价的哈,实际我们不能这么写
val text: String
get() { lazy{} }
自定义委托属性
thisRef字段使用
class Owner {
//委托给StringDelegate
var text: String by StringDelegate()
}
class StringDelegate(private var s: String = "Hello") {
//提示:这里的Owner可以改成Any,这样任意类里面的 String 属性,我们都可以用这种方式去委托了
operator fun getValue(thisRef: Owner, property: KProperty<*>): String {
return s
}
operator fun setValue(thisRef: Owner, property: KProperty<*>, value: String?) {
if (value is String) {
s = value
}
}
}
补充1:Kotlin 标准库为几种有用的委托提供了工厂方法
- 延迟属性Lazy
- 可观察属性Observable
import kotlin.properties.Delegates
class User {
var name: String by Delegates.observable("<no name>") {
prop, old, new ->
println("$old -> $new")
}
}
fun main() {
val user = User()
user.name = "first"
user.name = "second"
}
打印结果如下:
<no name> -> first
first -> second
补充2:从 Kotlin 1.4 开始,一个属性可以把它的 getter 与 setter 委托给另一个属性
这种委托对于顶层和类的属性(成员和扩展)都可用。
这是很有用的,例如,当想要以一种向后兼容的方式重命名一个属性的时候:引入一个新的属性、 使用 @Deprecated 注解来注解旧的属性、并委托其实现。
class MyClass {
var newName: Int = 0
//旧属性直接委托给新属性,并添加过时标签,注意这里的“::”,一般就是这么用的
@Deprecated("Use 'newName' instead", ReplaceWith("newName"))
var oldName: Int by this::newName
}
fun main() {
val myClass = MyClass()
myClass.oldName = 42
//这里就实现了向后兼容
println(myClass.newName)
}
补充3:将属性储存在映射中
class User(val map: Map<String, Any?>) {
val name: String by map
val age: Int by map
}
val user = User(mapOf(
"name" to "John Doe",
"age" to 25
))
println(user.name) // Prints "John Doe"
println(user.age) // Prints 25
函数
- 函数参数可以有默认值,当省略相应的参数时使用默认值。与其他语言相比,这可以减少重载数量。来个例子:
fun read(
b: Array<Byte>,
off: Int = 0,
len: Int = b.size,
) { /*……*/ }
- 如果默认参数之后的最后一个参数是lambda表达式,那么它既可以作为具体参数在括号内传入,也可以在括号外传入
fun foo (
bar: Int=0,
baz: Int=0,
qux:->Unit,
){/*.....*/ }
foo(qux = { println("hello") }) // 使用两个默认值 bar = 0 与 baz = 1
foo { println("hello") } // 使用两个默认值 bar = 0 与 baz = 1
- 在函数调用时使用命名参数时候,可以自由更改他们的列出顺序。然后有默认值的可以不输入。
- 如果一个函数不返回任何有用的值,它的返回类型是 Unit,并且它可以省略。
- 函数返回单个表达式时,可以省略花括号,直接使用“=”,比如
fun double(x: Int): Int = x * 2
- 可变数量的参数
回顾java,使用"obj… args"的形式,kotlin中使用</font color=“ff0000”>vararg
fun <T> asList(vararg ts: T): List<T> {
val result = ArrayList<T>()
for (t in ts) // ts is an Array
result.add(t)
return result
}
补充1:伸展操作符 *
val a = arrayOf(1, 2, 3)
//list的长度为6
val list = asList(-1, 0, *a, 4)
补充2:中缀表示法
使用infix关键字。必须满足一定条件。
- 必须是成员函数或者扩展函数
- 必须只有一个参数
- 参数不能接受可变数量的参数而且不能有默认值
//以Int的扩展函数shl为例
infix fun Int.shl(x: Int): Int { …… }
//中缀表示法这样写
1 shl 2
内联函数
inline fun <T> lock(lock: Lock, body: () -> T): T { …… }
- 内联函数内部可以调其他函数,但是他的变量不能作为参数传入外部函数中,会报错,需要修改成noinline才行。
- 内联可能导致生成的代码增加;不过如果我们使用得当(即避免内联过大函数),性能上会有所提升。
- 禁用内联使用noinline关键字。配合inline使用的,也就是说在内联函数内,允许参数判空啊,使用参数啊等等。
高阶函数
高阶函数是将函数用作参数或返回值的函数。
示例,自定义String类的扩展函数
/**
* if [String.isNullOrEmpty], invoke f()
* otherwise invoke t()
*/
fun <T> String?.notNull(f: () -> T, t: () -> T): T {
return if (isNullOrEmpty()) f() else t()
}
使用也简单
fun checkStringEnable(st:String):Boolean{
return st.notNull({true},{false})
}
集合
Kotlin 标准库提供了基本集合类型的实现: set、list 以及 map。
注意:
- 一个 只读 接口,提供访问集合元素的操作。
- 一个 可变 接口,通过写操作扩展相应的只读接口:添加、删除和更新其元素。
Kotlin 集合接口的图表
结论1:只读集合是型变的,可变集合是不型变的。
//List接口提供访问集合元素操作,所以printList能正常编译
fun printList(list: List<Any>) {
//注意:这里函数形参类型是List<Any>,函数内部是不知道外部传入是List<Int>还是List<String>,全部当做List<Any>处理
list.forEach {
println(it)
}
}
fun main() {
val stringList: List<String> = listOf("a", "b", "c", "d")
val intList: List<Int> = listOf(1, 2, 3, 4)
printList(stringList)//向函数传递一个List<String>函数实参,也就是这里List<String>是可以替换List<Any>
printList(intList)//向函数传递一个List<Int>函数实参,也就是这里List<Int>是可以替换List<Any>
}
看一个可变类型的fun printList(list: MutableList<Any>) {
//MutableList接口提供了add等方法,List则没有
list.add(3.0f)
list.forEach {
println(it)
}
}
fun main() {
val stringList: MutableList<String> = mutableListOf("a", "b", "c", "d")
val intList: MutableList<Int> = mutableListOf
//编译报错:Type mismatch
printList(stringList)
//编译报错:Type mismatch
printList(intList)
}
Collection
集合层次结构的根。此接口表示一个只读集合的共同行为:检索大小、检测是否为成员等等。Collection继承自 Iterable接口,它定义了迭代元素的操作。可以使用Collection作为适用于不同集合类型的函数的参数。
Map
Map<K, V> 不是 Collection 接口的继承者;但是它也是 Kotlin 的一种集合类型。
无论顺序如何,包含相同键值对的map是相等的。
常用示例
//List kotlin中默认实现是ArrayList
val numbers = listOf("one", "two", "three", "four")
println("Number of elements: ${numbers.size}")
println("Third element: ${numbers.get(2)}")
println("Fourth element: ${numbers[3]}")
println("Index of element \"two\" ${numbers.indexOf("two")}")
//MutableList
val numbers = mutableListOf(1, 2, 3, 4)
numbers.add(5)
numbers.removeAt(1)
numbers[0] = 0
numbers.shuffle()
println(numbers)
//set 默认实现是LinkedHashSet,另一种实现HashSet则不声明元素顺序
val numbers = setOf(1, 2Map, 3, 4)
println(numbers.first())
//map 默认实现是LinkedHashMap,保留插入顺序,HashMap则不声明元素顺序
val numbersMap = mapOf("key1" to 1, "key2" to 2, "key3" to 3, "key4" to 1)
//to 符号创建了一个短时存活的 Pair 对象,因此建议仅在性能不重要时才使用它。 为避免过多的内存使用,请使用其他方法。
val numbersMap = mutableMapOf<String, String>().apply { this["one"] = "1"; this["two"] = "2" }
空集合
还有用于创建没有任何元素的集合的函数:emptyList()、emptySet() 与 emptyMap()
var empty=emptyList<String>()
创建快照
创建了一个具有相同元素的新集合。
val sourceList = mutableListOf(1, 2, 3)
val copyList = sourceList.toMutableList()
val readOnlyCopyList = sourceList.toList()
sourceList.add(4)
println("Copy size: ${copyList.size}")
集合的各种操作创建集合
过滤示例
val numbers = listOf("one", "two", "three", "four")
val longerThan3 = numbers.filter { it.length > 3 }
映射示例
val numbers = setOf(1, 2, 3)
//[3,6,9]
println(numbers.map { it * 3 })
//[0,2,6]
println(numbers.mapIndexed { idx, value -> value * idx })
关联示例
val numbers = listOf("one", "two", "three", "four")
//{one=3, two=3, three=5, four=4}
println(numbers.associateWith { it.length })
迭代器
我们知道List和Set是可以获取迭代器的。
示例
val numbers = listOf("one", "two", "three", "four")
val numbersIterator = numbers.iterator()
while (numbersIterator.hasNext()) {
println(numbersIterator.next())
- ListIterator 它支持列表双向迭代:正向与反向。
- MutableListIterator可以在迭代列表时插入和替换元素。
区间和数列
使用…;常见配合使用的操作符有in !in ;相关的函数**downTo、step、util
常用示例
//if中使用
if(i in 1..4){//等价于1 <= i && i <= 4
print(i)
}
//for循环中使用
for(i in 1..4) print(i)
//downTo使用
for (i in 4 downTo 1) print(i)
//设置步长
for(i in 1..8 step 2)print(i)
//util使用排除最后一个数据
for(i in 1 util 10)print(i)
//使用集合函数
var range=1..10
println(range.filter({it %2==0}))
补充1:数列实现 Iterable,其中 N 分别是 Int、Long 或 Char,因此可以在各种集合函数(如 map、filter 与其他)中使用它们。
序列
是一种容器类型,提供了与Iterable相同的函数,但是与Iterable的处理方式不同。有延迟性。
- Iterable处理包含多个步骤时,会优先处理每个步骤完成并返回结果,有中间集合。序列则在可能的情况下会延迟执行,挡请求整个处理链的结果时才进行实际计算。
示例
//创建一个序列
val numbersSequence = sequenceOf("four", "three", "two", "one")
//由一个Iterable对象(比如List或者Set)
var numbers=listof("one","two","three","four")
var numbersSequence = numbers.asSequence()
//由函数构建一个无限序列,并取前5个数据
var oddNumbers=generateSequence(1){it +2}
println(oddNumbers.take(5).toList())
//创建一个有限序列,通常用到null
val oddNumbersLessThan10 = generateSequence(1) { if (it + 2 < 10) it + 2 else null }
println(oddNumbersLessThan10.count())
//逐个或按任意大小的组块生成序列元素,sequence函数使用
val oddNumbers = sequence {
yield(1)
yieldAll(listOf(3, 5))
yieldAll(generateSequence(7) { it + 2 })
}
//打印结果:[1, 3, 5, 7, 9]
println(oddNumbers.take(5).toList())
序列处理示例对比
一个是Iterable,一个是Sequence,对于同一件事步骤是不同的。
假定有一个单词列表。下面的代码过滤长于三个字符的单词,并打印前四个单词的长度。
//使用List,需要23个步骤
val words = "The quick brown fox jumps over the lazy dog".split(" ")
val lengthsList = words.filter { println("filter: $it"); it.length > 3 }
.map { println("length: ${it.length}"); it.length }
.take(4)
println("Lengths of first 4 words longer than 3 chars:")
println(lengthsList)
使用序列,需要18个步骤
val words = "The quick brown fox jumps over the lazy dog".split(" ")
// 将列表转换为序列
val wordsSequence = words.asSequence()
val lengthsSequence = wordsSequence.filter { println("filter: $it"); it.length > 3 }
.map { println("length: ${it.length}"); it.length }
.take(4)
println("Lengths of first 4 words longer than 3 chars")
// 末端操作:以列表形式获取结果。
println(lengthsSequence.toList())
集合操作
转换
- 映射
//基本的映射函数是map,如果需要元素索引可以使用mapIndexed
val numbers = setOf(1, 2, 3)
println(numbers.map { it * 3 })
println(numbers.mapIndexed { idx, value -> value * idx })
//不想产生null值使用mapNotNull和mapIndexedNotNull
println(numbers.mapNotNull{if(it==2} null else it*3)
println(number.mapIndexedNotNull{idx,value->if(idx==0) null else value*idx})
//关于映射转换 mapKeys和mapValues方法
val numbersMap=mapOf("key1" to 1,"key2" to 2,"key3" to 3,"key11" to 11)
println(numbersMap.mapKeys { it.key.toUpperCase() })
println(numbersMap.mapValues { it.value + it.key.length })
- 合拢
在一个集合(或数组)上以另一个集合(或数组)作为参数调用时,zip() 返回 Pair 对象的列表(List)。
val colors = listOf("red", "brown", "grey")
val twoAnimals = listOf("fox", "bear")
println(colors.zip(twoAnimals))
//打印结果:[(red, fox), (brown, bear)]
unzip反向转换,从键值对中构建两个列表
val numberPairs = listOf("one" to 1, "two" to 2, "three" to 3, "four" to 4)
println(numberPairs.unzip())
//打印结果:([one, two, three, four], [1, 2, 3, 4])
-
关联
associate() 会生成临时的 Pair 对象,这可能会影响性能。 -
打平
常用于操作嵌套的集合
// flatten返回嵌套集合中所有元素的一个List
val numberSets = listOf(setOf(1, 2, 3), setOf(4, 5, 6), setOf(1, 2))
println(numberSets.flatten())
//flatMap使用,常用于集合合并
val list = listOf(
1..20,
2..15,
100..166)
val flatList = list.flatMap{
it.map{
"No.$it"
}
}
- 转换成字符串
//joinToString使用
val numbers = listOf("one", "two", "three", "four")
println(numbers)
println(numbers.joinToString()) //one, two, three, four
//joinTo则是加到后面
val listString = StringBuffer("The list of numbers: ")
numbers.joinTo(listString)
println(listString)
//加头尾和分隔符
val numbers = listOf("one", "two", "three", "four")
//打印结果:start: one | two | three | four: end
println(numbers.joinToString(separator = " | ", prefix = "start: ", postfix = ": end"))
//指定长度限制
val numbers = (1..100).toList()
println(numbers.joinToString(limit = 10, truncated = "<...>"))
过滤
集合的过滤,基本函数是filter。
//过滤时想使用索引使用filterIndexed
val numbers = listOf("one", "two", "three", "four")
val filteredIdx = numbers.filterIndexed { index, s -> (index != 0) && (s.length < 5) }
//使用filterNot
val filteredNot = numbers.filterNot { it.length <= 3 }
//返回给定类型的集合元素,不符合的会无视
val numbers = listOf(null, 1, "two", 3.0, "four")
println("All String elements in upper case:")
numbers.filterIsInstance<String>().forEach {
println(it.toUpperCase())
}
//返回非null
val numbers = listOf(null, "one", "two", null)
numbers.filterNotNull().forEach {
println(it.length) // 对可空的 String 来说长度不可用
}
另一个过滤函数是partition,意思是划分
val numbers = listOf("one", "two", "three", "four")
//这里划分成2个了
val (match, rest) = numbers.partition { it.length > 3 }
println(match)
println(rest)
检验谓词
any表示至少有一个元素匹配,none表示没有元素匹配,all表示所有元素匹配给定词
val numbers = listOf("one", "two", "three", "four")
val empty = emptyList<String>()
println(numbers.any { it.endsWith("e") }) //true
println(numbers.none { it.endsWith("a") }) //true
println(numbers.all { it.endsWith("e") }) //false
println(emptyList<Int>().all { it > 5 }) //true
//空集合调用any和none用于判断是否有数据
println(empty.any())//false
println(empty.none())//ture
加减操作符(+ -)
集合和集合是可以直接通过加减操作符来操作的。
val numbers = listOf("one", "two", "three", "four")
val plusList = numbers + "five"
val minusList = numbers - listOf("three", "four")
println(plusList)
println(minusList)
分组
基本函数 groupBy() 使用一个 lambda 函数并返回一个 Map。
//按照首字母分组
var numbers=listOf("one", "two", "three", "four", "five")
println(numbers.groupBy{ it.first() })
//打印结果 {o=[ONE], t=[TWO, THREE], f=[FOUR, FIVE]}
println(numbers.groupBy(keySelector = { it.first() }, valueTransform = { it.toUpperCase() }))
取集合的一部分
常用的slice、take、takeLast、drop、dropLast
//slice 返回具有给定索引的集合元素列表
val numbers = listOf("one", "two", "three", "four", "five", "six")
println(numbers.slice(1..3)) //[two, three, four]
println(numbers.slice(0..4 step 2)) //[one,three,five]
println(numbers.slice(setOf(3, 5, 0))) //[four,six,one]
//take 从头开始获取指定数量的元素 takeLast从尾获取指定数量元素
//drop 从头去除指定数量的元素 dropLast从尾去除指定数量的元素
val numbers = listOf("one", "two", "three", "four", "five", "six")
println(numbers.take(3))
println(numbers.takeLast(3))
println(numbers.drop(1))
println(numbers.dropLast(5))
//chunked 将集合分解为给定大小的“块”
val numbers = (0..13).toList()
println(numbers.chunked(3){ it.sum() }) //14个数,分解成元素个数为3的块,并累加 [3, 12, 21, 30, 25]
//windowned 返回可能区间,与chunked有点不同
val numbers = listOf("one", "two", "three", "four", "five")
println(numbers.windowed(3)) //[[one, two, three], [two, three, four], [three, four, five]]
取单个元素
集合中通用方法elementAt,但是对于List建议使用get和[]
//elementAt方法检索特定位置的元素 对于List而言可以使用索引访问操作符
val numbers = linkedSetOf("one", "two", "three", "four", "five")
println(numbers.elementAt(3))
var list = listOf(1,2,3,4)
println(list.get(0))
//关于sortedSetOf方法 Set里数据源必须是有继承Comparable接口的,比如String
val numbersSortedSet = sortedSetOf("7", "2", "3", "4")
println(numbersSortedSet.elementAt(0)) //2 意思元素以升序存储
val numbersSortedSet = sortedSetOf("one", "two", "three", "four")
println(numbersSortedSet.elementAt(0)) //four
//first和last函数前面讲过了,略
//first和last 按条件取
val numbers = listOf("one", "two", "three", "four", "five", "six")
println(numbers.first { it.length > 3 })
println(numbers.last { it.startsWith("f") })
//random函数 随机取
val numbers = listOf(1, 2, 3, 4)
println(numbers.random())
//contains和containsAll检查是否存在
val numbers = listOf("one", "two", "three", "four", "five", "six")
println(numbers.contains("four")) //true
println(numbers.containsAll(listOf("four", "two"))) //true
集合排序
首先要排序,数据必须要实现Comparable接口,重写compareTo方法,不然谁知道怎么比对不。
大多数内置类型是可比较的:
- 数值类型使用传统的数值顺序:1 大于 0; -3.4f 大于 -5f,以此类推。
- Char 和 String 使用字典顺序: b 大于 a; world 大于 hello。
//关于compareTo 正值表示我比较大 0表示相等
class Version(val major: Int, val minor: Int): Comparable<Version> {
override fun compareTo(other: Version): Int {
if (this.major != other.major) {
return this.major - other.major
} else if (this.minor != other.minor) {
return this.minor - other.minor
} else return 0
}
}
fun main() {
println(Version(1, 2) > Version(1, 3))
println(Version(2, 0) > Version(1, 5))
}
自定义排序
对不可比较对象排序,可以使用函数 sortedBy和sortedByDescending。
val numbers = listOf("one", "two", "three", "four")
val sortedNumbers = numbers.sortedBy { it.length }
println("Sorted by length ascending: $sortedNumbers")
倒序
//reversed 使用
val numbers = listOf("one", "two", "three", "four")
println(numbers.reversed())
随机顺序
//shuffled 使用
val numbers = listOf("one", "two", "three", "four")
println(numbers.shuffled())
聚合操作
Kotlin 集合包含用于常用的 聚合操作 (基于集合内容返回单个值的操作)的函数 。常见的比如max、min、average、sum、count。
val numbers = listOf(6, 42, 10, 4)
println("Count: ${numbers.count()}")
println("Max: ${numbers.max()}")
println("Min: ${numbers.min()}")
println("Average: ${numbers.average()}")
println("Sum: ${numbers.sum()}")
如果你想通过条件筛选可以使用xxxBy函数,示例如下
val numbers = listOf(5, 42, 10, 4)
val min3Remainder = numbers.minBy { it % 3 }
println(min3Remainder)
reduce和fold函数,fold可以设置初始值,而reduce则是把第一个和第二个元素作为第一步的操作参数
val numbers = listOf(5, 2, 10, 4)
val sum = numbers.reduce { sum, element -> sum + element }
println(sum) //21
val sumDoubled = numbers.fold(0) { sum, element -> sum + element * 2 }
println(sumDoubled) //42
集合写操作
注意是可变集合,比如mutableListOf的对象就能使用add,而listOf生成的就不能add。
val numbers = mutableListOf(1, 2, 3, 4, 3)
//添加
numbers.add(5)
//删除
numbers.remove(3)
//更新在后面讲
List相关操作
按索引取出元素
var numbers=listOf(1,2,3,4)
//通过下标和索引
println(numbers.get(0))
println(numbers[0])
//获取不到就返回默认值
println(numbers.getOrElse(5){99})
println(numbers.getOrNull(5))
取出一部分
val numbers = (0..13).toList()
println(numbers.subList(3, 6))
写操作
add 和addAll
val numbers = mutableListOf("one", "five", "six")
numbers.add(1,"two") //在1位置插入,位置后面的都往后移
numbers.add(2,"three")
println(numbers) //[one, two, three, four, five, six]
更新操作
使用get或者其操作符形式[]
val numbers = mutableListOf("one", "five", "three")
numbers[1]="two"
删除操作
使用removeAt,删除之后的元素索引都会减少1
val numbers = mutableListOf(1, 2, 3, 4, 3)
numbers.removeAt(1)
排序操作
使用sort、shuffle、reverse之类的。
val numbers = mutableListOf("one", "two", "three", "four")
numbers.sort() //[four, one, three, two] 为啥呢,因为字符有asc码
Set相关操作
常用的比如并集和交集还有差集合 (union、intersect、subtract)
val numbers = setOf("one", "two", "three")
println(numbers union setOf("four", "five")) //有顺序的啊,这个是[one, two, three, four, five]
println(numbers intersect setOf("two", "one")) //一样有顺序,这个是[one, two]
println(numbers subtract setOf("three", "four")) //[one, two]
Map相关操作
- 取键与值
keys和values
val numbersMap = mapOf("one" to 1, "two" to 2, "three" to 3)
println(numbersMap.keys) //[one, two, three]
println(numbersMap.values) //[1,2,3]
getOrDefault和getOrElse以及get(key: K)
val numbersMap = mapOf("one" to 1, "two" to 2, "three" to 3)
println(numbersMap.get("one")) //1
println(numbersMap.getOrDefault("four", 10)) //10
- 过滤
- plus 与 minus
- map写操作
put和putAll使用
//写一条
val numbersMap = mutableMapOf("one" to 1, "two" to 2)
numbersMap.put("three", 3)
println(numbersMap)
//写多条
numbersMap.putAll(setOf("four" to 4, "five" to 5))
- map删除
remove的使用(可以remove())
val numbersMap = mutableMapOf("one" to 1, "two" to 2, "three" to 3)
numbersMap.remove("one")
println(numbersMap)
//通过键或值从可变 Map 中删除条目
val numbersMap = mutableMapOf("one" to 1, "two" to 2, "three" to 3, "threeAgain" to 3)
numbersMap.keys.remove("one")
println(numbersMap)
numbersMap.values.remove(3)
println(numbersMap)
//通过-=
val numbersMap = mutableMapOf("one" to 1, "two" to 2, "three" to 3)
numbersMap -= "two"
println(numbersMap)
后话
kotlin语言中文站
码上开学
朱涛的自习室
kotlin项目GitHub地址
Android KTX官方文档
Kotlin DSL script代替Groovy DSL script