Kotlin学习笔记(不包含协程)

文章目录

    • 前言
    • kotlin推进时间表
    • 零碎知识点
      • lateinit关键字
      • for循环
      • 嵌套类与内部类
      • 空安全设计
      • 延迟初始化
      • 类型推断
      • 声明变量除了使用var还可以使用val
      • 可见性
      • 函数
      • 基本类型
      • 强转(is和as关键字使用)
      • 构造器 constructor
      • init的使用,初始化代码块
      • object关键字
      • 顶层声明
      • 常量 关键字const
      • 数组
      • 集合
      • kotlin中创建一个map
      • 可见修饰符
      • 构造器
      • 函数简化
      • 参数默认值
      • 命名参数
      • 嵌套函数
      • 字符
      • 字符串
      • 原生字符串 """
      • range 区间
      • 数组和集合的操作符
      • Sequence
    • 条件语句
        • if/else
        • when
        • for
      • 返回和跳转
        • return、break、continue
          • 补充1:Nothing对象
          • 结合标签使用
      • 空安全判断
          • Elvis 操作符“?:”
          • ?使用
          • 非空断言运算符(!!)
      • 解构声明
      • 位运算
      • 比较
      • 数组
        • 原生类型数组
      • 类与继承
      • 属性
      • 接口
      • 可见性修饰符
        • 补充知识:模块
      • 扩展
          • 扩展函数
          • 扩展属性
          • 声明的扩展函数的可用范围
      • 数据类
          • copy函数
      • 密封类
      • 泛型
        • Java中的泛型
          • ? extends
          • ? super
          • 补充1:区分子类和子类型的概念
          • 补充2:PECS 法则:「Producer-Extends, Consumer-Super」
          • 补充3:[30分钟学会UML图](https://zhuanlan.zhihu.com/p/109655171)
        • 小结
        • kotlin中的泛型
          • kotlin中泛型
          • 型变注解
          • 声明处型变
          • 类型投影(使用处型变)
          • 补充1:java中的单个‘?’和kotlin中的星投影(‘*’)
          • 指定多个边界(where使用)
          • reified关键字
      • 嵌套类与内部类
        • 嵌套
        • 内部类(inner关键字)
        • 匿名内部类
      • 枚举类
      • 对象
      • 类型别名
        • 补充1:import as
      • 委托
        • 委托类
        • 委托属性
        • 自定义委托属性
        • 补充1:Kotlin 标准库为几种有用的委托提供了工厂方法
        • 补充2:从 Kotlin 1.4 开始,一个属性可以把它的 getter 与 setter 委托给另一个属性
        • 补充3:将属性储存在映射中
      • 函数
        • 内联函数
        • 高阶函数
      • 集合
        • Collection
        • Map
        • 空集合
        • 创建快照
        • 集合的各种操作创建集合
        • 迭代器
        • 区间和数列
        • 序列
          • 序列处理示例对比
      • 集合操作
        • 转换
        • 过滤
        • 加减操作符(+ -)
        • 分组
        • 取集合的一部分
        • 取单个元素
        • 集合排序
          • 自定义排序
          • 倒序
          • 随机顺序
        • 聚合操作
        • 集合写操作
        • List相关操作
        • Set相关操作
        • Map相关操作
    • 后话

前言

有句话说的好:种一棵树的最好时间第一是十年前,其次是现在。我第一次听说kotlin,大概是18年12月的时候。真正开始系统学习是在20年上半年,现在stdlib版本都更到1.4.31了貌似,我看好kotlin,写篇笔记方便查阅。


kotlin推进时间表

Kotlin学习笔记(不包含协程)_第1张图片

零碎知识点

  • 定义一个普通变量
//定义一个普通变量
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 = arrayOf("a", "b", "c")
val anys: Array = strs //会报错compile-error: Type mismatch

集合

Kotlin 和 Java 一样有三种集合类型:List、Set 和 Map。
Kotlin 中创建一个 List 特别的简单,有点像创建数组的代码。而且 Kotlin 中的 List 多了一个特性:支持 covariant(协变)。也就是说,可以把子类的 List 赋值给父类的 List 变量:

val strs: List = listOf("a", "b", "c")
val anys: List = strs // success

在java中,会报错:不兼容

List strList = new ArrayList<>();
List 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 = sequence
        .map { i ->
            println("Map $i")
            i * 2
        }
        .filter { i ->
            println("Filter $i")
            i % 3  == 0 //过滤掉不满足条件的,满足条件则生成新的Sequence对象
        }
    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 = 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 = listOf(1, 2, null, 4)
val intList: List = 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 初始化为 ["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.foo()
        super.foo()
    }

    override fun bar() {
        super.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 textViews = new ArrayList(); // 本身
List textViews = new ArrayList

发现问题:++使用了上界通配符后,add方法不好使了,只能拿到数据,却添加不了。++

由于 add 的这个限制,使用了 ? extends 泛型通配符的 List,只能够向外提供数据被消费,从这个角度来讲,向外提供数据的一方称为「生产者 Producer」。对应的还有一个概念叫「消费者 Consumer」,对应 Java 里面另一个泛型通配符 ? super。

? super

以? super T为例

  • 指定泛型T为类时,范围包括此类的直接和间接父类,也包括这个类本身。
  • 指定泛型T为接口时,范围包括此接口以及他的父类接口。

示例

List buttons = new ArrayList