0%

第3章:面向对象

类的构造方法

Kotlin 中的类及接口

Kotlin中的类与Java的很像:

1
2
3
4
5
class Bird {
val weight: Double = 500.0
val color: String = "blue"
fun fly()
}

反编译成Java的版本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public final class Bird {
private final double weight = 500.0D;
@NotNull
private final String color = "blue";

public final double getWeight() {
return this.weight;
}

@NotNull
public final String getColor() {
return this.color;
}

public final void fly() {

}
}

由此可以看出,虽然声明方式很像,但是也存在很多不同:

  • 属性默认值。Kotlin中,除非显式声明延迟初始化,不然就需要指定默认值。
  • 不同的可访问修饰符。Kotlin类中的成员默认是全局可见的(public),而Java默认可见域是包作用域。
  • 方法默认是final修饰的。意味着不能覆写(这条是自己添加的)。

可带有属性和默认方法的接口

我们知道,Java 8 之后,接口支持默认实现,如下所示:

1
2
3
4
5
6
public interface Flayer {
public String kind();
default public void fly() {
System.out.println("I can fly");
}
}

接下来看下Kotlin的接口实现:

1
2
3
4
5
6
7
interface Flyer {
val speed: Int
fun kind()
fun fly() {
println("I can fly");
}
}

同样,我们可以用Kotlin 定义一个带有方法实现的接口,同时,它还支持抽象属性(如例子中的speed属性),然而,Kotlin是基于Java6实现的,那它是如何支持的呢?转换为Java代码看下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public interface Flyer {
int getSpeed();

void kind();

void fly();

public static final class DefaultImpls {
public static void fly(Flyer $this) {
String var1 = "I can fly";
System.out.println(var1);
}
}
}

由此我们发现,Kotlin编译器通过定义一个静态内部类 DefaultImpls 来提供fly方法的默认实现。同时,抽象属性是通过一个get方法来实现的!所以呢,我们不能像Java一样,为属性直接赋值,如下这样是错误的:

1
2
3
interface Flyer {
val height = 1000;//error Property initializers are not allowed in interfaces
}

但是Kotlin 提供了另外一种方式来实现这种效果:

1
2
3
4
interface Flyer {
val height
get() = 1000
}

更简洁地构造类的对象

如果要在Java中实现参数个数不同的构造方法,那我们就要重载很多个构造方法,这种方式主要存在2个缺点:

  • 如果要支持任意参数组合来创建对象,那么需要实现的构造方法非常多
  • 每个构造方法中的代码都会冗余,如在构造方法中可能都需要对 age 和color 进行相同的赋值操作。

Kotlin 通过引入新的构造语法来解决这些问题。比如我们可以用一行代码来表示复杂的构造方式:

1
class Bird(val weight: Double = 0.00, val age: Int = 0, val color: String = "blue")

如果用Java实现这种参数任意组合的效果,那是非常复杂的。但是如果不写入全部的参数,而只用其中某些参数的时候,需要写参数名,否则会报错:

1
2
3
4
5
//错误的
val bird1 = Bird(1000.00)

//正确示例
val bird2 = Bird(weight = 1000.00, color = "black")

init方法:事实上,我们的构造方法可以拥有多个 init,他们会在对象创建时按照类中从上到下的顺序先后执行

延迟初始化: by lazy 和 lateinit

在Kotlin中,主要使用lateinit 和 by lazy 这两种语法来实现延迟初始化的效果。如果这是一个用 val 声明的变量,我们用 by lazy 来修饰:

1
2
3
4
5
class Bird(val weight: Double, val age: Int, val color: String) {
val sex: String by lazy {
if(color == "yellow") "male" else "female"
}
}

总结 by lazy 语法的特点如下:

  • 该变量必须是引用不可变的,而不能通过var声明
  • 被首次调用时,才会进行赋值操作,一旦赋值,后续将不能更改。

需要注意的是,系统会给 lazy属性默认加上同步锁,也就是 LazyThreadSafetyMode.SYNCHRONIZED ,它在同一时刻只允许一个线程对lazy属性初始化,所以,lazy是线程安全的。当然,你可以自己给lazy指定参数,如: val sex: String by lazy(LazyThreadSafetyMode.NONE)

与lazy 不同,lateinit 主要用于 var 声明的变量,然而它不能用于基本数据类型,如 Int、Long 等,我们需要使用Integet这种包装类作为替代。lateinit 的用法如下:

1
2
3
4
5
6
7
8
class Bird(val weight: Double, val age: Int, val color: String) {
lateinit var sex: String

fun printSex() {
sex = if(color == "yellow") "male" else "female"
println(sex)
}
}

Kotlin只用一个构造方法实现了Java中需要重载才能实现的功能,那么,Kotlin中是否真的只需要一个构造方法呢?

主从构造方法

前面似乎遗漏了一些情况,简化前面的Bird类:

1
2
3
4
5
6
7
class Bird(age: Int) {
val age: Int

init {
this.age = age
}
}

假设当前我们知道鸟的生日,希望可以通过生日来得到鸟的年龄,然后创建一个Bird对象,如何实现?有一种方案就是在别处定义一个工厂方法:

1
fun Bird(birth: DateTme) = Bird(getAgeByBirth(birth))

在哪声明这个工厂方法呢?这种方式的缺点在于,Bird 方法与Bird类在代码层面的分离不够直观。其实我们可以像Java那样新增一个构造方法来解决,Kotlin 也支持多构造方法,与Java的区别是,Kotlin中多个构造方法之间存在主从关系

1
2
3
4
5
6
7
8
9
10
11
class Bird(age: Int) {
val age: Int

init {
this.age = age
}

constructor(birth: DateTime) : this(getAgeByBirth(birth)){

}
}

以上代码的运作方式是:

  • 通过constructor方法定义一个新的构造方法,称为从构造方法。相应地,我们熟悉的构造方法叫做主构造方法,每个类最多存在一个主构造方法,但是可以存在多个从构造方法
  • 如果一个类存在主构造方法,那么每个从构造方法都要直接或间接地委托给它。

不同的访问控制原则

构造完对象,就要考虑访问控制了。

限制修饰符

我们知道,Kotlin中的类和方法默认实现反编译成 Java的时候,会被final修饰,所以,类默认是不能被继承的,方法默认也不能被覆写的,如果要实现继承,类之前需要用open修饰: open class Bird {} ,方法也需要使用open 修饰: open fun fly()

类默认final 真的好吗?

网上有很多人认为默认final有很多缺点,那为什么Kotlin要设计成默认final呢?主要有2个原因:

  • Kotlin 当前是一门以Android为平台的开发语言,在开发中,我们很少会频繁继承一个类,默认final会更加安全。
  • Kotlin的扩展手段更加丰富。不像Java,Kotlin 可以通过多种方式去扩展,而不是通过原始类的手段,典型的莫过于 Android 的Kotlin 扩展库 android-ktx,Google就是通过Kotlin的扩展语法而不是继承来实现。

此外,Kotlin还可以利用密封类来限制一个类的继承,如下所示:

1
2
3
4
5
sealed class Bird {
open fun fly() = "I can fly"

class Eagle: Bird()
}

Kotlin通过 sealed 关键字来修饰一个类为密封类,若要继承则需要将子类定义在同一个文件中,其他文件中的类将无法继承它。但是这种方式有它的局限性,即它不能被初始化,为什么呢?这是因为它是基于抽象类实现的,我们看反编译后的Java代码就知道了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public abstract class Bird {
private Bird() {

}

public Bird(DefaultConstructorMarker $constrctor_maker) {
this();
}

public static final class Eagle extends Bird {
public Eagle() {
super((DefaultConstructorMarker)null)
}
}
}

密封类的使用场景优先,它其实可以看成一种功能更强大的枚举,所以它在模式匹配中可以起到很大的作用

可见性修饰符

除了限制类修饰符外,还有一种可见性修饰符。Kotlin与Java的不同在于:

  • 默认修饰符不同,Kotlin是public,而Java是default
  • Kotlin中有一个独特的 internal
  • Java类只有内部类可以用private修饰,其他类不允许;而Kotlin可以
  • protected访问范围不同。Java中是包、类及子类可访问,而Kotlin只有类和子类可以访问

说下Kotlin 独特的 internal 修饰符,它的作用域被称作模块内访问,那到底什么是模块?以下几种情况可以算作一个模块:

  • 一个Eclipse项目
  • 一个 Intellij IDEA项目
  • 一个Maven项目
  • 一个Gradle项目
  • 一组由一次Ant任务执行编译的代码

那为什么要这种修饰符呢?Java的包内访问不好吗?Java包内访问确实是有问题的,举个例子,你再Java项目中定义了一个类,默认修饰符,那就是包私有的,其他地方将无法访问。然后你id啊宝诚一个类库,供三方使用。但如果有个开发者想使用这个类,除了copy源码以外,还有一个方式就是在程序中创建一个与该类相同名字的包,那么这个包下面的其他类就能直接使用我们前面定义的类了!

而Kotlin这种,模块内可见指的是该类只对一起编译的其他Kotlin文件可见,开发工程与第三方类库不属于同一模块,这时候如果还想用,就只能复制源码了。

Java中我们很少见到private修饰的类,因为Java中的类或者方法没有单独属于某个文件的概念。若要用provate修饰,那么这个只能是其他类的内部类,而Kotlin中则可以用private给单独的类修饰,它的作用域就是当前这个Kotlin文件:

解决多继承问题

Java和Kotlin都不支持多继承。为什么这样呢?是因为多继承会导致继承关系语义上的混淆。

骡子的多继承困惑

C++支持多继承,然而C++中存在一个经典的钻石问题。假如我们有个抽象的 Animal 类,它有个 run() 方法,Horse (马) 和 Donkey(驴) 都继承了Animal,假如支持多继承,Mule(驴)继承了 Horse 和 Donkey ,那么,在 Mule 中的 run() 到底是继承了谁的呢?这就是典型的钻石问题,因为继承关系像个钻石图,如下:

钻石问题

接口实现多继承

在Java中我们经常提及使用接口来实现多继承,其实,如果多个接口中都存在同样的方法,比如上述的 run() ,同样也会导致钻石问题。不过,Kotlin 通过提供 super 关键字来指定继承那个父接口的方法,从而解决了这个问题,如下:

1
2
3
4
5
6
7
8
9
10
11
interface Flyer {
fun kind() = "flying animals"
}

interface Animal {
fun kind() = "flying animals"
}

class Bird(): Flyer, Animal {
override fun kind() = super<Flyer>.kind()
}

通过 super<Flyer>.kind()来指定继承哪个父接口的方法!当然,子类也可以自己实现这个方法,不用父类的,完全没问题。

内部类解决多继承问题

Kotlin的内部类的定义方式和Java 还不一样,如果我们按照Java的习惯来定义 Kotlin中的内部类:

1
2
3
4
5
6
7
8
9
class OuterKotlin {
val name = "not kotlin inner class"

class ErrorInnerKotlin {
fun printName() {
print("thie name is $name")//报错,不能访问name
}
}
}

报错了,和Java 还真不一样。其实,我们这样声明的是Kotlin 的 嵌套类,并非内部类。如果要在Kotlin中声明一个内部类,必须在这个类前面加一个 inner 关键字,即这样子:

1
2
3
4
5
6
7
8
9
class OuterKotlin {
val name = "kotlin inner class"

inner class InnerKotlin {
fun printName() {
print("thie name is $name")
}
}
}

我们知道,Java中在内部类的语法上增加一个 static 关键字,就可以变成 嵌套类;Kotlin则是相反的思路,默认是嵌套类,必须加上 inner 关键字才是一个内部类。

了解内部类之后,可以通过内部类实现上述的骡子类:

1
2
3
4
5
6
7
8
9
10
11
12
class Mule {
fun runFast() {
HorseC().runFast()
}

fun runSlow() {
DonkeyC().runSlow()
}

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

使用委托代替多继承

Kotlin中的委托只需要通过 by 关键字就可以实现,比如之前学习的 by lazy 语法,其实就是利用了委托实现了延迟初始化。我们看下如何通过委托代替多继承需求:

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
interface CanFly {
fun fly()
}

interface Caneat {
fun eat()
}

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

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

//关键
class Bird(flyer: Flyer, animal: Animal): CanFly by flyer, CanEat by animal {}

fun main(args: Array<String>) {
val fyler = Flyer()
val animal = Animal()
val b = Bird(flyer, animal)
b.fly()
b.eat()
}

真正的数据类

繁琐的JavaBean

JavaBean中需要各种setter和getter,如果要支持对象值的比较,还得重写hashCode 和 equals 等方法。

用data class创建数据类

data class 顾名思义就是数据类,这不是Kotlin首创,在Scala等语言中也有。一般我们只需要如下定义即可:

1
data class Bird(var weight: Double, var age: Int, var color: String)

这么一行代码,编译器为我们做了很多事情,来看看反编译后的代码:

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
public final class Bird {
private double weight;
private int age;
@NotNull
private String color;

public final double getWeight() {
return this.weight;
}

public final void setWeight(double var1) {
this.weight = var1;
}

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

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

@NotNull
public final String getColor() {
return this.color;
}

public final void setColor(@NotNull String var1) {
Intrinsics.checkNotNullParameter(var1, "<set-?>");
this.color = var1;
}

public Bird(double weight, int age, @NotNull String color) {
Intrinsics.checkNotNullParameter(color, "color");
super();
this.weight = weight;
this.age = age;
this.color = color;
}

public final double component1() {//Java中没有的
return this.weight;
}

public final int component2() {//Java中没有的
return this.age;
}

@NotNull
public final String component3() {//Java中没有的
return this.color;
}

@NotNull
public final Bird copy(double weight, int age, @NotNull String color) {//Java中没有的
Intrinsics.checkNotNullParameter(color, "color");
return new Bird(weight, age, color);
}

// $FF: synthetic method
public static Bird copy$default(Bird var0, double var1, int var3, String var4, int var5, Object var6) {
if ((var5 & 1) != 0) {
var1 = var0.weight;//copy时,若未指定具体属性的值,则使用被copy对象的属性值,这是浅拷贝
}

if ((var5 & 2) != 0) {
var3 = var0.age;
}

if ((var5 & 4) != 0) {
var4 = var0.color;
}

return var0.copy(var1, var3, var4);
}

@NotNull
public String toString() {
return "Bird(weight=" + this.weight + ", age=" + this.age + ", color=" + this.color + ")";
}

public int hashCode() {
int var10000 = (Double.hashCode(this.weight) * 31 + Integer.hashCode(this.age)) * 31;
String var10001 = this.color;
return var10000 + (var10001 != null ? var10001.hashCode() : 0);
}

public boolean equals(@Nullable Object var1) {
if (this != var1) {
if (var1 instanceof Bird) {
Bird var2 = (Bird)var1;
if (Double.compare(this.weight, var2.weight) == 0 && this.age == var2.age && Intrinsics.areEqual(this.color, var2.color)) {
return true;
}
}

return false;
} else {
return true;
}
}
}

这就和JavaBean很相似了,同时还有 equals 和 hashCode 的实现。同时,我们发现里面有几个JavaBean中没有的方法,比如 copy、component1、component2、component3。下一节来介绍它们。

copy 、componentN 与 结构

上上述代码可以看到,两个copy方法,可以传入响应的参数来生成不同的对象;同时,如果你未指定具体属性的值,那么新生成的对象的属性值将使用被copy对象的属性值,这就是我们常说的浅拷贝。看个例子:

1
2
3
4
5
6
7
8
//如果Bird的属性是var,即可变的
val b1 = Bird(20.0,1, "blue")
val b2 = b1
b2.age = 2

//如果Bird的属性是val,不可变的,那么更改属性只能通过copy
val b1 = Bird(20.0,1, "blue")
val b2 = b1.copy(age = 2)

注意,Kotlin提供的上述copy方法是浅拷贝的,所以我们要注意使用场景。因为数据类的属性可以被修饰为var,所以不能保证不会出现引用被修改的情况。

接下来看 componentN (其中N为1,2,3…,根据参数个数来定),这个设计到底有什么用?我们或多或少直到怎么将属性绑定到类上,但是对于如何将类的属性绑定到响应变量上却不是很熟悉,比如:

1
2
3
4
5
6
7
8
9
val b1 = Bird(20.0,1, "blue")

//通常方法,也符合Java的思维逻辑
val weight = b1.weight
val age = b1.age
val color = b1.color

//Kotlin 进阶方法
val (weight, age, color) = b1

看到进阶方法的时候,一定感到兴奋了吧,普通方法确实很繁琐。还有一种情形,看Java的代码:

1
2
3
4
5
6
String birdInfo = "20.0,1, blue";
//如果要把值取出来,就得split
String[] temps = birdInfo(",");
double weight = Double.valueOf(temps[0]);
int age = Integer.valueOf(temps[1]);
String color = temps[2];

在我们明明直到值得情况下,还需要这样分割,很繁琐,好在Kotlin提供了更优雅的做法:

1
val (weight, age, color) = birdInfo.split(",")

这个语法也很简洁和直观,其原理也很简单,就是 解构,通过编译器的约定实现解构。当然,Kotlin对于解构也有限制,在数组中它默认最多允许赋值5个变量,因为如果变量过多,效果反而会适得其反,因为到后期你都搞不清哪个值要赋给哪个变量了。除了利用编译器自动生成的 componentN之外,你还可以实现自己的 componentN,比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
data class Bird(var weight: Double, var age: Int, var color: String) {
var sex = 1

operator fun component4: Int {//注意 operator 关键字
return this.sex
}

constructor(weight: Double, age: Int, color: String, sex: Int) : this(weight, age, color) {
this.sex = sex
}

//使用
fun main(args: Array<String>) {
val b1 = Bird(20.0, 1, "blue", 0)
val (weight, age, color, sex) = b1
...
}
}

除了数组支持解构外,Kotlin也提供了其他常用数据类型,分别是 Pair 和 Triple 前者是二元组,后者是 三元组,,我们可以

用类似以下方法来使用它们:

1
2
3
4
5
6
7
val pair = Pair(20.0,1)
//利用属性顺序获取
val weightP = pair.first
val ageP = pair.second

//使用解构
val (weightP, ageP) = Pair(20.0,1)

从static 到 object

Kotlin中告别了static,因为有了 object 关键字,除了替代static外,它还有更多的功能实现,比如单例对象以及简化匿名表达式等。

伴生对象

看一段常见的Java代码:

1
2
3
4
5
6
7
8
9
public class Prize {
private int type;

static int TYPE_REDPACK = 0;

static boolean isRedpack(Prize prize) {
return prize.type == TYPE_REDPACK;
}
}

这个类中既有静态变量、静态方法,也有普通变量、普通方法,然而,静态变量和静态方法是属于类的,普通变量和普通方法是属于具体对象的,所以在代码解构上职能并不清晰。Kotlin中利用 companion object 两个关键字引入伴生对象来清晰区分。

顾名思义,“伴生”即伴随某个类的对象,它属于这个类所有,全局只有一个单例,因此伴生对象跟Java中static修饰的效果一样,在类装载的时候被初始化

companion object 用花括号包裹了所有静态属性和方法,使得将普通方法和属性清晰区分开来。此外,伴生对象很适合作为工厂,这里就不展开。

天生的单例: object

单例模式最大的一个特点就是在系统中只能存在一个实例对象,所以在java中我们必须通过设置构造方法私有化,以及提供静态方法创建实例的方式来创建单例。在Kotlin中,由于object的存在,我们可以直接用它来实现单例,如下所示:

1
2
3
4
5
6
object DatabaseConfig {
var host: String = "127.0.0.1"
var port: Int = 3306
var userName: String = "root"
var password: String = ""
}

由于object全局声明的对象只有一个,所以它并不用语法上的初始化,甚至都不需要构造方法,因此,我们可以说object创造的是天生的单例。此外,由于 DatabaseConfig 的属性是 var 声明的属性,我们还能修改它们:

1
2
DatabaseConfig.host = "localhost"
DatabaseConfig.port = 3307

由于单例也可以和普通类一样实现接口和继承类,所以可以将其看成一种不需要主动初始化的类,它也可以拥有扩展方法单例对象会在系统加载的时候初始化

object 表达式

主要说的是,利用object来完善匿名内部类,这里不展开说。

谢谢你的鼓励