Kotlin 核心编程(五):面向对象

0x0001 Kotlin 中的类

Kotlin 中类与 Java 中的几点不同:

  • 在 Kotlin 中除非显示声明延时初始化,那么属性需要显式的指定默认值。
  • val 为不可变属性
  • 修饰符的访问权限不同,Kotlin 默认全局可见, Java 默认包可见。
  • 在接口中,不可为属性初始化值( Kotlin 接口中抽象属性,背后通过方法实现)

0x0002 Koltlin 中的接口

在 Kotlin 中,接口可以拥有属性和默认方法:

1
2
3
4
5
6
7
interface IShow{
val value:Int
fun show()
fun test(){
println("test")
}
}

反编译后的代码为:

1
2
3
4
5
6
7
8
9
10
11
12
public interface IShow {
int getValue();
void show();
void test();
public static final class DefaultImpls {
public static void test(IShow $this) {
String var1 = "test";
boolean var2 = false;
System.out.println(var1);
}
}
}

Kotlin 接口中的属性其实是通过方法来实现的,而方式是基于静态内部类实现的,由于 Kotlin 是基于 Java6 的,而在 Java 6 中接口是不支持默认方法的,所以在 Kotlin 中接口不支持对属性进行赋值以及默认方法的实现,满满的语法糖既视感。

0x0003 类的构造方法

1. 主从构造方法

在类外部定义的构造方法称为 主构造方法,在类内部通过 constructor 定义的方法称为 从构造方法。如果主构造方法存在注解或者可见修饰符,那么需要添加 constructor 关键字。

从构造器由两部分组成:

  1. 对其他构造方法的委托(必须要有的)。
  2. {} 内部包裹的代码块。

$如果一个类存在主构造方法,那么所有的从构造方法需要直接或间接的委托给它,执行顺序是为先执行委托的方法,然后执行自身代码块的逻辑。$

1
2
3
4
5
6
7
8
9
10
11
class Dog(val country: String) {
var telNum: Int? = 0
constructor(tel: Int, country: String) : this(country) {
telNum = tel
}
}

fun main() {
val dog = Dog(123,"China")
println("dog`s tel num is ${dog.telNum}, country is ${dog.country}")
}

2. 为构造器参数指定默认值

在 Kotlin 中可以给构造函数的参数指定默认值:

1
2
3
class Bird(val color:String = "white", val age : Int = 1){
...
}

在类初始化时可以指定其参数名,或者按照参数的顺序赋值:

1
2
3
val  bird = Bird(age = 3,color =  "red")
或者
val bird = Bird( "red",3)

3. 为构造器参数添加修饰符

使用 val 或者 var 声明构造方法的参数,有两个作用:

  • 代表参数的引用可变性,var 可变,val 不可变。
  • 简化构造类中语法。

至于怎样简化,示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
class Bird(val color:String = "white", val age : Int = 1){
...
}
// 与下面的写法等效,很明显上面写法简洁
class Bird( color:String = "white", age : Int = 1){
private val color: String
private val age: Int
init {
this.age = age
this.color = color
}
}

4. init 代码块

当构造方法的参数没有 val 或者 var 时,构造方法的参数可以在 init 语句中使用,除了 init 代码块,此时构造方法中的参数不可以在其他位置使用

Kotlin 中规定类中的所有非抽象属性成员必须在对象创建时被初始化,但是可以在声明val 属性时不赋值,但是在 init 代码块中进行初始化:

1
2
3
4
5
6
7
8
class Bird( color:String = "white",  age : Int = 1){
private val color: String
private val age: Int
init {
this.age = age
this.color = color
}
}

一个类可以拥有多个 init 代码块,执行顺序:自上而下,多个 init 代码块可以将初始化的操作进行智能分离,在复杂业务时使逻辑更加清晰。

0x0004 属性的延迟初始化

延迟初始化的属性,可以在对象初始化时不必有值。

1. by lazy

可以使用 by lazy 修饰 val 声明的变量:

1
2
3
4
5
class Dog{
val name by lazy {
"Ni"
}
}

在首次调用时,才会进行赋值操作,一旦赋值,后续将不能更改。

1
2
3
4
public actual fun <T> lazy(initializer: () -> T): Lazy<T> = SynchronizedLazyImpl(initializer)


public inline operator fun <T> Lazy<T>.getValue(thisRef: Any?, property: KProperty<*>): T = value

lazy 的背后是接受一个 Lambda 表达式,并且返回一个 Lazy 实例的函数,第一次调用该属性时,会执行 lazy 对应 Lambda 表达式并记录,后续访问该属性会返回记录的结果。

默认系统会为 lazy 属性加上同步锁 - LazyThreadSafetyMode.SYNCHRONIZED,同一时刻只有一个线程可以对 lazy 的属性进行初始化,为线程安全的。同时 lazy 可以指定其他线程参数,以此来满足需求。如果能够确保可以并行执行,可以给 lazy 传递其他线程参数。

2. lateinit

lateinit 主要用于声明 var 变量,不能用于基本数据类型,如 Int 等,如果需要的话,使用 Interger 等包装类替代。

1
2
3
4
5
6
7
class Dog {
lateinit var color: String

fun printColor(newColor: String) {
color = newColor
}
}

0x0005 类的访问修饰符权限

  • Kotlin 中类和方法默认修饰符是 final,默认是不被继承或重写的,如果有这样的需求,必须添加 open 修饰符。

  • Kotlin 的默认修饰符为 public,Java 为 default。

  • Kotlin 独特的修饰符:internal。

  • Kotlin 和 Java 的 protected 的访问权限不同,Java 中是包、子类可访问,而 Kotlin 中只允许子类访问。

internal 修饰符的作用域是模块内可见,那么在 Kotlin 中模块是什么?
一个模块可以看成是一起编译的 Kotlin 文件组成的集合。

修饰符 Kotlin 中含义 Java 中含义
public Kotlin 的默认修饰符,全局可见 与Java 中public 效果相同
protected 受保护的修饰符,类及子类可见 含义一致,但是访问权限为类、子类、包
private 私有修饰符,类内修饰只有本类可见,类外修饰本文件可见 私有修饰符,本类可见
internal 模块可见

0x0006 密闭类

Kotlin 中除了使用 final 来限制类的继承外,还可以使用密闭类来限制一个类的继承。

Kotlin 通过 sealed 关键字来修饰一个类为密闭类,若要继承密闭类,则子类只能定义在同一个文件中(文件内或者本类内),其他文件无法继承它。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
sealed class Animal{
abstract fun eat()

fun breath(){
println("I can breath.")
}
class Cat:Animal() {
override fun eat() {
println("I eat mouse")
}
}
}

class Fish: Animal(){
override fun eat() {
println("I drink water")
}
}

密闭类可以看成一个功能强大的枚举类。

0x0007 Kotlin 中的多继承问题

1. 多继承中存在的钻石问题

Java 中的类时不支持多继承的,但是可以通过接口实现多继承,但是此方式存在一个缺陷:当多个类中存在同一个方法时,那么无法完成实现的目的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public interface Animal {
void eat();
default void kind(){
System.out.println("i am animal");
}
}

public interface Flyer {
void fly();
default void kind(){
System.out.println("i am flyer");
}
}

// 由于Flyer,Animal存在相同的默认方法,所以 Bird 无法定义
public class Bird implements Flyer,Animal {
@Override
public void eat() {
}
@Override
public void fly() {
}
}

但是在 Kotlin 中通过一些写法支持这样的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
interface Flyer {
fun fly()
fun kind() {
println("i am flyer")
}
}

interface Animal {
fun eat()
fun kind() {
println("i can animal")
}
}

class Bird : Flyer, Animal {
override fun fly() {
}

override fun eat() {
}

// 可以通过 super 关键字指定继承哪个父类的方法
override fun kind() = super<Flyer>.kind()
//同时也可以主动重写
// override fun kind() {
// println("i am bird")
// }
}

2. 内部类解决多继承的问题

嵌套类和内部类:嵌套类包括静态内部类和非静态内部类,而非静态内部类被称为内部类,内部类持有外部类的引用。

在 Kotlin 中使用 inner 标记一个类为 内部类

1
2
3
4
5
6
7
8
9
10
11
12
class Outer {
private val bar: Int = 1
class Nested {
fun foo() = 2
}
}
class Outer2 {
private val bar: Int = 1
inner class Inner {
fun foo() = bar
}
}

反编译代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

public final class Outer {
private final int bar = 1;
// 没有使用 inner 修饰的类,会被编译成静态内部类
public static final class Nested {
public final int foo() {
return 2;
}
}
}

public final class Outer2 {
private final int bar = 1;
// 使用 inner 修饰的类,会被编译成内部类
public final class Inner {
public final int foo() {
return Outer2.this.bar;
}
}
}

同时可以使用 private 修饰内部类,这样其他类就不能访问该内部类,具有很好的封装性。

使用内部类实现多继承:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
open class Horse{
fun runFast(){
println("run fast")
}

fun eat(){
println("horse eat")
}
}

open class Donkey{
fun doLongTimeThing(){
println("do longtime thing")
}

fun eat(){
println("donkey eat")
}
}

class Mule{
fun runFast(){
HorseC().runFast()
}de

fun doLongTimeThing(){
DonkeyC().doLongTimeThing()
}

private inner class HorseC:Horse()
private inner class DonkeyC:Donkey()
}

在 Mule 中定义两个内部类分别继承 Horse 和 Donkey,那么 Mule 可以通过内部类访问 Horse 和 Donkey 的特性,在一定程度上能够达到继承的效果。

3. 使用委托代替多继承

什么是委托?委托是一种特殊的类型,用于方法事件de委托,比如你调用 A 类的 methodA 方法,其实背后是 B 类的 methodB 方法,在 Java 中有静态委托和动态委托。

在 Kotlin 中可以使用关键字 by 来实现委托的效果,比如 by lazy 为通过委托实现延时初始化。

使用 Kotlin 中的类委托实现多继承的需求:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
interface CanFly{
fun fly()
}
interface CanEat{
fun eat()
}

open class Flyer:CanFly{
override fun fly() {
println("can fly")
}
}

open class Animal:CanEat{
override fun eat() {
println("can eat")
}
}

class Bird(flyer: Flyer,animal: Animal):CanEat by animal,CanFly by flyer{
fun main(args:Array<String>){
val flyer = Flyer()
val animal = Animal()
val bird = Bird(flyer,animal)
bird.fly()
bird.eat()
}
}

通过 类委托,则委托类(Bird)拥有了和被委托类(Flyer、Animal)一样的状态和属性,在一定程度上实现了多继承的效果。

0x0008 数据类

通过 data 关键字实现数据类:

1
data class Student(val name:String,var age:Int)

反编译

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public final class Student {
@NotNull
private final String name;
private int age;

@NotNull
public final String getName() {
return this.name;
}

public final int getAge() {
return this.age;
}

public final void setAge(int var1) {
this.age = var1;
}

public Student(@NotNull String name, int age) {
Intrinsics.checkParameterIsNotNull(name, "name");
super();
this.name = name;
this.age = age;
}

@NotNull
public final String component1() {
return this.name;
}

public final int component2() {
return this.age;
}

@NotNull
public final Student copy(@NotNull String name, int age) {
Intrinsics.checkParameterIsNotNull(name, "name");
return new Student(name, age);
}
...
}

定义数据类的规则:

  • 数据类必须有一个构造函数,并且构造函数中至少包括一个参数。
  • 数据类构造函数的参数必须使用 var 或者 val 修饰。
  • data class 不能用 abstract、open、sealed、inner 修饰。
  • 数据类可以实现接口,也可以继承类。

可以发现最终还是像 JavaBean 中一样实现 getter/setter 方法,重写 hashCode、equal 等方法,但是其中的存在的 copy、componentN 是我们从来没见过的。

1.copy、componentN 与解构

通过反编译的代码我们可以知道 copy 方法主要是帮我们从一个已有对象拷贝一个新的数据类对象,其中需要重点关注的是这里的拷贝是 浅拷贝,注意其使用场景。

copy 的使用示例:

1
2
3
4
5
6
7
8
9
fun testMethod(){
val student = Student("小明",3)
//因为 val 为不可变量,所以不能通过以下方式进行浅拷贝,而 copy 提供了一种简洁的方式进行浅拷贝,
//但是通过反编译的代码我们可以其实现原理,所以 copy 是一种语法糖,但在 Kotlin 中有哪些特性不是语法糖呢?
// val student3 = student
// student3.name = "小明"
val student2 = student.copy(name = "小刚",age = 3)
val student4 = student.copy(name = "小刚")
}

下面来看一下 componentN 方法,componentN 可以理解为类属性的值,而 N 代表属性的声明顺序,比如上例中 方法,component1 代表 name 的属性值,而 component2 代表 age 的属性值。

示例:

1
2
3
4
5
6
7
8
9
10
fun testTwo(){
val student = Student("小明",3)
val name = student.name
val age = student.age
// 使用 Kotlin
val(name2,age2)= student
// 更可以这样使用,一次性实现多个属性赋值
val str= "nike,2,10086"
val(tag,num,phone) = str.split(",")
}

以上能够实现原因就是 解构,通过编译器的约定实现解构。在数据类中,可以直接使用编译器自动生成的 componentN 方法,也可以自己实现对应属性的 componentN 方法,为上例中的 Student 数据类添加自定义 componentN 方法,该方法需要使用 operator 修饰。

1
2
3
4
5
6
7
8
9
10
11
12
13
data class Student(val name: String, var age: Int) {
var phoneNum = 110
operator fun component3() = phoneNum

constructor(name: String, age: Int, phoneNum: Int) : this(name, age) {
this.phoneNum = phoneNum
}
}

fun testThree() {
val student = Student("mike", 2, 10087)
var (name, age, phoneNum) = student
}

除了数据类外,数组也支持解构,不过其默认最多允许赋值 5 个变量,若变量过多,反而适得其反,但是可以通过扩展实现多余 5 的变量的赋值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 为 Array<T> 扩展解构函数
operator fun <T> Array<T>.component6(): T {
return this[5]
}

fun main(args: Array<String>) {
val array = arrayOf("one", "two", "three")
val (a, b, c) = array
println("$a $b $c")
// 通过扩展方法扩大解构个数
val list = arrayOf("one", "two", "three", "four", "five", "six")
val (one, two, three, four, five, six) = list
println("$one $two $three $four $five $six")
}

2. 系统定义的其他支持解构的数据类:Pair、Triple

  • Pair

    Pair 为二元组,代表这个数据类有两个属性。

  • Triple

Triple为三元组,代表这个数据类有三个属性。

两者的源码:

1
2
3
4
5
6
7
8
9
10
public data class Pair<out A, out B>(
public val first: A,
public val second: B
)

public data class Triple<out A, out B, out C>(
public val first: A,
public val second: B,
public val third: C
)

使用示例:

1
2
3
4
5
6
7
fun testFive(){
val pair = Pair("Name",3)
val triple = Triple("one","two","three")
val name = pair.first
val age = pair.second
val(one,two,three) = triple
}

0x0009 Object 关键字

在 Kotlin 中没有 static 关键字,使用全新的 object 关键字,可以替代 static 所有的场景,object 除了替代 static 的关键字之外,还有其他很多功能,比如单例对象、简化匿名表达式。

1. 伴生对象

针对 Java 中的 static,Kotin 中引入了 companion object 关键字。companion object 意为 伴生对象,所谓 伴生 意为伴随着某个类,伴生对象即为伴随着某个类产生的对象,所以伴生对象属于类,和 Java 中的 static 关键字是一样的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Cat{
companion object{
val MEAL = 1
val FEMEAL = 2
fun play(){
println("i am play")
}
}
}

fun main(args: Array<String>) {
println(Cat.FEMEAL)
Cat.play()
}

2. 天生的单例:object

1
2
3
4
object Config{
var host:String = "localhost"
var port:Int = 8080
}

单例类可以像普通类一样实现接口和继承类,还可以进行扩展函数。

3. object 表达式

在 Java 中的匿名内部类可以使用 object 表达式进行替换,更易理解。

1
2
3
4
5
6
7
8
9
10
val comparator = object :Comparator<String>{
override fun compare(s1: String?, s2: String?): Int {
if ( s1 == null){
return -1
}else if(s2 == null)
return 1
return s1.compareTo(s2)
}
}
Collections.sort(list, comparator)

object 表达式在运行中不像在单例模式中所说的那样,全局只有一个对象,而是每次运行时都会生成一个对象。

但是在一些场合下,匿名内部类和 object 表达式并不适合,而 Lambda 更适合,那么 Lambda 和 object 表达式哪一个更适合替代匿名内部类?

当匿名内部类使用的接口只需要实现一个方法时,使用 Lambda 表达式更适合,当匿名内部类内有多个方法时,使用 object 表示式更适合。

所以以上 object 表示式可以使用 Lambda 替代:

1
2
3
4
5
6
7
8
val comparator = Comparator<String> { s1, s2 ->
if (s1 == null) {
return@Comparator -1
} else if (s2 == null)
return@Comparator 1
s1.compareTo(s2)
}
Collections.sort(list, comparator)