Aengus 的技术小筑

Aengus | Blog

Kotlin 学习笔记——概览、类与对象

974
2020-06-30

概览OverView

  • Kotlin的变量或方法声明和Java相比是“反”的,也就是类型声明在后,变量或方法名在前;
  • Kotlin更严格,这点体现在方法声明时就确定变量是否可变,类默认不可继承;
  • Kotlin更安全,变量声明的类都是非空的,如果允许为空,则需要在类后面加?
  • Kotlin用println()函数进行打印,并且每行结束不用加;
  • Kotlin注释格式与Java相同;
  • Kotlin支持字符串模板表达式,使用"${expression}"的格式可以对expression进行运算并将其转为字符串;
  • Kotlin字符串可以直接用==进行比较;

变量与函数

Kotlin变量声明格式如下:

val/var a: Type = someThing

其中val代表不可变,对应Java中的final Typevar代表可变变量,就等同于Java中普通的变量。Type是变量类型,不可空,如果允许为空,则用Type?表示。在声明时便要求声明变量为valvar,是Kotlin安全性上的体现,一般情况下,不可变对象都是线程安全的。

Kotlin一般函数声明如下:

fun funcationName(arg: Type): ReturnType {
    // 函数体
}

可以看到参数的声明也是后置类型,ReturnType是函数的返回类型,也可以不写返回类型代表无返回值。Kotlin函数还有很多用法,我们将在后面再提到。

在变量或函数声明最开始都可以加private等关键字进行修饰。

循环、List和Map

Kotlin只有for循环和Java稍有不同,for循环基本结构为:

for (item: Type in collection) { /* ... */ } // 可以省略类型

若使用数字迭代,则常用以下两种方式:

// 区间,左闭右闭。下面i = 1,2,3
for (i in 1..3) { /* ... */ }
// until函数,左闭右开,符合常用习惯。下面i = 1,2
for (i in 1 until 3) { /* ... */ }
// 可以用 step 和 downTo 控制步长与顺序。下面i = 10,8,6,4,2
for (i in 10 downTo 2 step 2) { /* ... */ }

Kotlin使用listOf()生成List,使用MapOf()生成Map,用法如下:

val list = listOf<String>("a", "b", "c")
print(list[1]) // b
val map = mapOf<String, Int>("key1" to 1, "key2" to 2)
print(map["key1"]) // 1

之后会对集合进行更详细说明。

一切皆对象

Kotlin相对于Java面向对象的程度更高。在Kotlin中,Java的boolean,int,char,double等数据类型的首字母全部变成大写,即Boolean,Int,Char等,这意味着这些数据类型都是对象,可以使用下面这种方法进行操作:

1.toChar()
true.and(false)
1.2.compareTo(1)

类Class

在Java中,所有对象都继承自Object,而在Kotlin中,所有对象均继承自Any(如果可能是null,则继承Any?)。Any类中有三个方法,分别为hashCode()equals()toString()。类声明与创建方法如下:

class Name {
  // 类主体
}
// 如果类中没有body体,可以用这种方式
class Empty
// 创建类,Kotlin中没有 new 关键字
val person: Person = Person()
// Kotlin拥有类型推导机制,可以省略变量类型声明
val person = Person()

构造函数Constructor

每个类都可以有一个主构造函数和次构造函数。

主构造函数

主构造函数如下所示:

class Person constructor(firstName: String) { /* ... */ }
// 构造函数无注解时也可以省略 constructor 关键字
class Person(firstName: String) { /* ... */ }
// 若有注解,则 constructor 关键字不能省略
class Person public @Inject constructor(firstName: String) { /* ... */ }

// 推荐使用
class Person(val firstName: String) { /* ... */ }
// 或者
class Person(private val firstName: String) { /* ... */ }

主构造函数没有函数体,如果想进行初始化工作(就像Java中的构造函数一样),可以放在init块中,init块中的函数会在创建对象时被调用:

class Person(firstName: String) {
    init {
        println("构造函数")
    }
}

Kotlin的主构造函数有点像是语法糖,当你调用构造函数时,Kotlin编译器会自动将参数赋值给主构造函数中的声明的参数,这个过程类似于Java中的:

class Person {
   private String name;
  
    public Person(String name) {
        this.name = name;
    }
}
Person person = new Person("Bob");

所以,Kotlin主构造函数中的参数可以在类body中任意的地方使用。Kotlin中类,字段和函数默认修饰符都是publicprotectedprivate关键字作用和Java中保持一致,但是使用internal表示同一module下可见。

如果没有指定主构造函数,则编译器会为其自动生成一个无参的主构造函数,这点和Java是一致的。

次构造函数

次构造函数是在类body中使用constructor函数进行表示:

class Person {
    constructor(name: String) { /* ... */ }
}

一个类可以有多个次构造函数,但只能有一个主构造函数。如果一个类既有主构造函数又有次构造函数,那么每个次构造函数必须直接或间接调用主构造函数,使用this关键字:

class Person(val name: String) {
      constructor(name: String, age: Int) : this(name) { /* ... */ }
      constructor(name: String, age: Int, sex: String) : this(name, age) { /* ... */ }
}

主构造函数调用在此构造函数之前。注意,**只有主构造函数中的参数可以在类body中的任何地方调用,次构造函数的参数只能在构造函数中使用。**下面是一个较为完整的例子:

class Person(val name: String) {
    init {
        println("name: ${this.name}")
    }
    constructor(name: String, age: Int) : this(name) {
        println("name: $name, age: $age")
    }
    constructor(name: String, age: Int, sex: String) : this(name, age) {
        println("name: $name, age: $age, sex: $sex")
    }
}

fun main() {
  val person = Person("张三")
}
// 输出:
// name: 张三
// name: 张三, age: 18
// name: 张三, age: 18, sex: 男

Kotlin构造函数参数支持默认值,如下:

class Person(val name: String = "李四") {
    init {
        println("name: ${this.name}")
    }
    constructor(name: String = "王五", age: Int = 20) : this(name) {
        println("name: $name, age: $age")
    }
    constructor(name: String = "赵六", age: Int = 21, sex: String = "男") : this(name, age) {
        println("name: $name, age: $age, sex: $sex")
    }
}
fun main() {
    val person = Person()
    val person1 = Person(name="甲")
    val person2 = Person(name="乙", age=10)
    val person3 = Person(age=30)
    val person4 = Person(sex="女")
}
// 第一行代码输出:
// name: 李四
// 第二行代码输出:
// name: 甲
// 第三行代码输出:
// name: 乙
// name: 乙, age: 10
// 第四行代码输出:
// name: 王五
// name: 王五, age: 30
// 第五行代码输出:
// name: 赵六
// name: 赵六, age: 21
// name: 赵六, age: 21, sex: 女

可以看到Kotlin会优先使用参数较少的构造方法,当传入的参数不在此构造方法时,Kotlin才会选择参数更多的构造方法。注意次构造函数中参数不允许使用val/var修饰。

继承Inheritance

上面已经说到,所有的类都直接或间接继承自Any类。在Kotlin中,所有的类默认都是final的,因为类默认是不可继承的,如果允许其被其他类继承,则需要使用open关键字进行修饰

open class Person { /* ... */ }

父类指定了主或次构造函数时:如果子类有主构造函数,则父类必须被在继承时立即初始化

open class Father(val lastName: String) {
    init {
        println("父亲姓 ${lastName}")
    }
}

class Son(val lastNameSon: String) : Father(lastNameSon) {
    init {
        println("儿子姓 $lastNameSon")
    }
}

父类指定了主或次构造函数时:如果子类没有主构造函数,那么它的每个次构造函数都必须使用super初始化父类,或者委派给另外一个有此功能的此构造函数:

open class Father(val lastName: String)

class Son : Father {
    constructor(familyName: String) : super(familyName) {
        println("这里用的是父类的字段lastName=$lastName")
    }
    // 委派给子类的第一个次构造函数
    constructor(familyName: String, age: Int) : this(familyName) {
        println("通过儿子的姓我们知道父亲姓 $lastName,儿子 $age 岁了")
    }
}

fun main() {
    val son = Son("王")
    val son2 = Son("王", 20)
}
// 第一行代码输出:
// 这里用的是父类的字段lastName=王
// 第二行代码输出:
// 这里用的是父类的字段lastName=王
// 通过儿子的姓我们知道父亲姓 王,儿子 20 岁了

父类没有指定主或次构造函数时:如果子类没有指定主构造函数和次构造函数,那么在继承时使用父类默认的构造函数;否则在主构造函数处继承父类构造函数或者次构造函数处使用super实例化父类:

open class Father

class Son : Father()
class Son2 : Father {
    constructor() : super() { /* ... */ }
}

Kotlin与Java一样,仅仅支持单继承,但是可以实现多个接口。

重写override

在Java中重写父类方法使用的是@Override注解,而在Kotlin中则使用override关键字,除此以外函数默认也是final的,如果需要被在子类中重写同样需要使用open关键字进行修饰:

open class Father {
    open fun eat() { /* ... */ }
    fun sleep() { /* ... */ }
}
class Son : Father() {
    override fun eat() { /* .... */ }
}

使用final关键字可以使方法不再次被子类重写(只被自己重写):

class Son : Father() {
    final override fun eat() { /* .... */ }
}

Kotlin也支持属性重写,用法和函数重写类似:

open class Father() {
    open val age: Int = 40
}
class Son1(override val age: Int = 22) : Father() // age不可更改
class Son2 : Father() {
    override var age = 20;	// age此时可以更改
}

抽象类Abstract Class

Kotlin中的抽象类与Java类似,使用abstract关键字进行修饰,需要注意的是当继承抽象类时,需要对父类(抽象类)进行实例化:

abstract class Father
class Son : Father()

抽象类默认也是open的。

接口Interface

Kotlin使用interface声明接口,接口中的方法默认为open,且支持默认方法实现;接口中也可以有属性:

interface Worker {
    val count: Int  // 抽象的属性,不允许赋值
    fun run() {
       println("Worker中的方法")
   }
}

注意,实现类中对接口中的属性进行重写,不可以将var属性变为val属性,但可以将val变为var

当子类同时实现或继承接口与父类时,重写规则如下:

open class Machine {
    open fun run() {
        println("Machine中的方法")
    }
}
class Computer : Machine(), Worker {
    val app = "QQ"
    override fun run() {
        println("Computer中的方法")
    }
    fun runAll() {
        run()
        // 通过这种方式调用父类或接口的方法
        super<Machine>.run()
        super<Worker>.run()
    }
}
fun main() {
    val com = Computer()
    com.runAll()
}
// 输出
// Computer中的方法
// Machine中的方法
// Worker中的方法

内部类Inner Class

嵌套类就是在类中的类:

class Computer {
    val app = "QQ"
    class Desktop {
        fun print() {
          println("嵌套类中的方法")
        }
    }
}

嵌套类无法访问外部类的方法或属性,但是使用inner关键字修饰嵌套类时,嵌套类就变成了内部类,此时就可以访问到外部类的属性或方法了,这是因为此时内部类含有一个外部类的引用(编译器自动生成的):

open class Machine {
    open fun playOuter() {
        println("父类方法")
    }
}
class Computer : Machine() {
    val app = "QQ"
    override fun playOuter() {
        println("外部类的方法")
    }
    inner class Desktop {
        fun print() {
            println(app)
        }
        fun play() {
            playOuter()  // 调用外部类方法
            super@Computer.playOuter() // 调用外部类的父类的方法
        }
    }
}
fun main() {
    val desktop = Computer().Desktop()
    desktop.print()
    desktop.play()
}
// 输出:
// QQ
// 外部类的方法
// 父类方法

数据类Data Class

在Java中写JavaBean时,我们常常需要对其重写equals(),hashCode(),toString()等方法以便于使用,但这些都是一些重复的工作,我们也可以借助Lombok中的@Data注解帮我们完成这些工作,而在Kotlin中,这些工作都可以借助关键字data帮我们实现。数据类的声明如下:

data class User(val name: String, val age: Int)

另外,数据类还给我们提供了函数copy()帮我们实现数据类的复制,copy()函数会针对每个数据类生成其对应的构造方法,以上面的数据类为例,它生成的copy()方法与调用方式如下:

copy(val name: String, val age: Int)
// 如何使用
val oldUser: User = User("李四", 20)
val newUser: User = oldUser.copy(name="王五")

数据类还提供了componentN()函数返回数据类的第N个属性,以上面的数据类为例,componentN()函数使用如下:

val user = User("张三", 20)
val name1 = user.component1() // name1 = "张三"
val age1 = user.component2()	// age1 = 20
// 或者(这里的component1(),component2()被自动调用)
val (name2, age2) = user        // (name2 = "张三", age2 = 20)
// 还可以这么用
val collection = listOf(User("a", 1), User("b", 2), User("3", 3))
for ((name, age) in collection) {
    println("name is $name")
    println("age is $age")
}

数据类也可以有类body,我们同样可以使用自己的方式重写toString()等方法。**如果想让数据类拥有无参的构造方法,可以给所有参数指定默认值。**如果不想让编译器自动将参数添加到构造方法中,可以将其放在body中:

data class User(val name: String) {
    var age: Int = 18
}

数据类目前有以下几种限制:

  • 主构造函数中至少有一个参数;

  • 主构造函数中的参数必须使用val/var修饰;

  • 数据类的父类中的final方法不会被重写;

  • 数据类不能被abstract,open,sealedinner修饰;

  • 若父类有open componentN()并且返回兼容的类型,则会被重写;若返回不兼容类型,则报错;

密封类Sealed Class

密封类只能被和它同文件的类可见,对其他文件的类都不可见(但是它的子类可以被其他文件中的类可见)。密封类使用sealed关键字修饰:

sealed class Example

密封类是抽象的,而且不可以被实例化,但是可以拥有抽象属性。密封类不允许有非private的构造方法。

下面的是常用用途:

sealed class Expr
data class Const(val number: Double) : Expr()
data class Sum(val e1: Expr, val e2: Expr) : Expr()
data class Other(val number: Double) : Expr()
// when相当于Java中的switch,但是有更强大的功能如类型转换,我们将在函数一节中说明
fun eval(expr: Expr): Double {
    return when(expr) {
        is Const -> expr.number
        is Sum -> eval(expr.e1) + eval(expr.e2)
        else -> Double.NaN
    }
}

单例类Object Class

在Java中使用单例类是常用的操作,在这里我们就不给出Java中单例的实现(可以看这篇)。Kotlin中原生支持了单例类的实现:

object Singleton {
    fun doSomeThing() { /* ... */ }
}

这种方式声明的对象是线程安全的,且在第一次访问时才会被实例化,由JVM保证单例。

调用其方法可以通过类似Java中的静态访问的方式进行:

Singleton.doSomeThing()

单例类也可以继承其他父类。单例类可以作为嵌套类在其他类中,但是不可以在内部类中(即不能在inner class):

class Test {
    object Singleton1		  // 可以
    inner class Inner {
        object Singleton2 // 不可以
    }
    class Inner2 {
        object Singleton3 // 可以
    }
}

object关键字还有一个作用是作为匿名内部类的生成,用法如下:

interface Teacher {
    fun speak()
}

fun giveAClass(t: Teacher) {
    t.speak()
}

fun main() {
    giveAClass(object : Teacher {
        override fun speak() {
            println("临时老师开始说话")
        }
    })
    // 还可以这么用
    val point = object {
        var x: Int = 100
        var y: Int = 100
    }
    println(point.x)
}

这种通过匿名内部类间接实现传输函数的方法在Java中比较常见,但是Kotlin中还有更好的方法,我们将在函数一节中继续说明。

除此之外,object还可以作为伴生类的关键字。

伴生类Companion Class

伴生类的声明方法如下:

class MyClass {
    companion object Factory {
        val type = "A"
        fun create(): MyClass {
            return MyClass()
        }
    }
}

伴生类可以让我们用类似Java中静态方法调用的形式掉用某些方法,上面的例子中我们就可以这样调用:

val type = MyClass.type
val instance = MyClass.create()
// 或者
val type = MyClass.Factory.type
val instance = MyClass.Factory.create()

伴生类的类名声明时可以去掉,这时候Kotlin会自动用Companion作为伴生类的名称。注意,**一个类中只能有一个伴生类。**伴生类同样可以继承。

内联类Inline Class

内联类目前还在测试中,可能之后会有变化。内联类表示如下:

inline class Wrapper(val value: Type)

在业务中,有时候因为业务逻辑需要对某些“东西”进行抽象,而单独创建对象表示这种“东西”在运行时可能又比较重,加大了系统负担,内联类便是用来解决这种问题的,内联类可以看作一种包装(Wrapper),它是对需要包装的东西的抽象,在编译时,Kotlin会为其生成一个包装器,在运行时,它要么用这个包装器代表,要么用它主构造函数中的参数类型代表,因此内联类有且只能有一个主构造函数属性,这个属性将可能在运行时代替这个内联类。有个简单的例子:

inline class Password(val vaule: String)

这样便为密码字符串生成了一个内联类,既可以方便开发,又不用增加系统创建对象的开销。关于“代表”也有个例子,比如Kotlin中的Int,它既可以用Java中的原始类型int表示,也可以用Integer表示。

内联类可以继承其他类,但是不能被其他类所继承,内联类中可以定义函数或者其他属性,但是这些调用在运行时都将变成静态调用。

委派Delegation

Kotlin原生支持委派模式,所谓委派模式,是指将一个对象的工作委派给另一个对象,在Kotlin中,委派有类委派与属性委派,通过by关键字实现,通过委派,我们可以将父类的初始化工作交给子类接受的参数:

interface Base {
    val value: Int
    fun printValue()
    fun print()
}
class BaseImpl : Base {
    override val value = 10
    override fun printValue() {
        println("BaseImpl value=${this.value}")
    }
    override fun print() {
        println("BaseImpl print")
    }
}
class Delegation(val base: Base) : Base by base {
    override val value = 20
    override fun print() {
        println("Delegation print")
    }
}
fun main() {
    val baseImpl = BaseImpl()
    val delegation = Delegation(baseImpl)
    delegation.printValue()
    delegation.print()
}
// 输出:
// BaseImpl value=10
// Delegation print

可以看到使用委派对象后,调用方法时会优先调用重写的方法,但是委派对象只能访问自己的接口成员(虽然在Delegation中重写了接口成员,但是委派对象在调用printValue()时只能访问自己的接口成员,并不能访问Delegation中的接口成员)。

Kotlin提供了函数lazy()实现了属性委托,通过by lazy({...})的方式为属性提供委托并实现了懒加载:

val lazyValue: String by lazy {
    println("进行某些操作")
    "Hello"
}

这里lazy使用了lambda表达式,如果你还不清楚lambda表达式,这里你可以简单的理解为当代码运行到这里时,会自动执行块中的代码,并将最后一行的值作为块的返回值。通过lazy函数进行委托,只有在第一次访问lazyValue时才会对其进行初始化并记住其值,之后的访问将会直接返回这个值。

lazy()函数默认是线程安全的,如果需要多个线程同时访问可以传参LazyThreadSafetyMode.PUBLICATION;如果不需要线程同步可以传参LazyThreadSafetyMode.NONE

还有一个常用的委派方法是使用Map进行传参,这个方法常常用于解析JSON,用法如下:

class User(val map: Map<String, Any?>) {
    val name: String by map
    val age: Int by map
}
val user = User(mapOf(
    "name" to "张三",
    "age" to 20
))
println(user.name) // 张三
println(user.age)  // 20