Kotlin第四讲-Kotlin的类、对象和接口

类是逻辑组织的基本单元,类含有以下成分:依赖包,类名,构造方法,属性,成员方法,伴生对象,接口,父类等

类的构造方法的完整逻辑过程

先考虑主构造函数,当主构造函数不够用时,再引入从构造函数。最初的主构造函数是这样的:步骤1通过主构造函数的参数传入数据,步骤2在类里定义所需的属性,步骤3在init代码块里对属性做初始化/赋值操作,这三个步骤分工明确,前后关联,共同完成了一个类的构造。

为了简化,Kotlin支持步骤2和步骤1合并操作,在主构造函数参数前加var/val;

支持步骤3和步骤2合并,对定义的属性直接赋值。支持步骤3和步骤2和步骤1合并操作,使用主构造函数的默认参数表达。

当然这些合并带来简便的同时,降低了构造的能力。

最初的状态(步骤1,步骤2,步骤3都存在)

1
2
3
4
5
6
7
8
9
class User(_sickName: String){

val sickName: String

init {
println("init")
sickName = _sickName
}
}

步骤2和步骤1的合并,在主构造函数里传参和定义类的属性

1
2
3
4
5
6
class User(val sickName: String){

init {
println("init")
}
}

步骤3,步骤2和步骤1的合并

1
2
3
4
5
6
open class User private constructor(val sickName: String = "Kotlin", var age: Int){

init {
println("init")
}
}

注意:Java中需要重载构造方法的场景大多数都被Kotlin参数默认值和参数命名的语法特性涵盖了

子类的构造方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
 open class AirBook(name: String, val year: Int, val size: Int) {

constructor(name: String, year: Int) : this(name, year, 22){
println("constructor")
}
}

class MacBookPro: AirBook{

public constructor(name: String, year: Int) : super(name, year) {
}

public constructor(name: String, year: Int, size: Int) : super(name, year, size){
}
}

子类在创建的时候,必须调用父类的构造函数,如果其父类还有父类,仍然要调用父类的父类的构造函数,直至顶层的基类。

1
2
3
4
5
6
7
8
open class AirBook(name: String, val year: Int, val size: Int){

init {
}

constructor(name: String, year: Int) : this(name, year, 22){
}
}

构造函数可以调用当前自身的构造函数,因为自身的构造函数必然有至少一个调用了父类的构造函数。

属性访问器

在kotlin里,一个属性 = 字段 + 属性访问器;这里的字段是Java里的成员变量

属性访问器分为读访问器getter,和写访问器setter

访问器里存在field字段,用来连接写访问器和读访问器,作为两者的通信桥梁

val修饰的属性只有读访问器,var修饰的属性既有有读也有写访问器

Kotlin引入属性访问器后,凡事对属性进行赋值操作,就会调用属性的写访问器setter;读取值的操作对应调用的是属性的读访问器

那么如果我们要自定义一个类似Java里的setter方法,要怎么做呢?

通过var变量的私有setter来实行

代码如下:

1
2
3
4
5
6
7
8
9
10
class TableLamp(val name: String, lightness: Int){

var lightness: Int = 1
private set

fun setupLightness(lightness: Int){
this.lightness = lightness
println("setupLightness lightness = $lightness")
}
}

这样对属性lightness的修改,只能通过setupLightness方法。

修饰词final, open, abstract相互影响和使用

Kotlin的函数,类默认是final的,如果想重写函数,类有子类,则需使用open修饰。abstract修饰的函数和类,意味着有open的特性。这和Java的用法是一致的。

Kotlin自带的特殊类

数据类,嵌套类,内部类,密封类的基本写法这里就略去不谈。

数据类data

data修饰的类,必须有主构造函数,且主构造每个入参必须都val或var修饰

data修饰补充的方法里的所用的变量取决于主构造函数的入参

嵌套类和内部类

定义在某个类内部并用inner修饰的类称为内部类

嵌套类可以类比Java的静态内部类,Kotlin的内部类类比Java的内部类,他们含有外部类的引用。与Java内部类不同的是,Kotlin的内部类能对外部类的变量进行写操作。

举个例子

1
2
3
4
5
6
7
8
9
class TextView{
var counter: Int= 0

inner class Operator{
fun calculate(){
counter ++
}
}
}

内部类Operator的成员函数可以写外部类的counter属性,这在Java里是做不到的。

密封类

sealed关键词修饰的类,表达有限个子类。

1
2
3
4
5
6
7
8
9
sealed class Color constructor(val name: String)

class Red : Color("Red")

class Green : Color("Green")

class Blue : Color("Blue")

class Gray : Color("Gray")

上述写法,编译器就会知道,Color的子类的数量,此例Color一共只有四个子类,分别是:Red,Green, Blue, Gray。如果增加或删除Color的子类,编译器是能感知到的。密封类的子类可以作为嵌套类,也可以写在密封类所在的文件里,但是不能写在其他文件里。

举个和When搭配的应用例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
fun testSealed(color: Color): Int =
when (color) {
is Red -> {
println("Red")
1
}
is Green -> {
println("Green")
2
}
is Blue -> {
println("Blue")
3
}
is GRAY ->{
println("GRAY")
4
}
}

密封类的意义:
如果不用密封类,使用when总是不得不添加一个默认分支。更重要的是,如果你添加了一个新的子类,编译器并不能发现有地方改变了。如果你忘记了添加一个分支,就会选择默认的选项,这可能导致潜在的bug。

使用密封类就能解决上述的问题。

Object的使用

声明一个类并创建一个对应的实例。与类一样,对象可以包含属性,方法,初始化语句块等的声明。对象声明可以继承类和实现接口,尤其不包含状态的时候很适用,也可以有扩展函数。

对象声明

创建单一的实例,对象声明在定义的时候就创建了,不需要在代码的其他地方调用构造方法。

因此,对象声明不允许有构造函数。

1
2
3
4
5
6
object NameComparator : Comparator<String>{

override fun compare(o1: String, o2: String): Int {
return o1.compareTo(o2, true)
}
}

Java调用上述代码,则需要通过INSTANCE来调用,如下

1
CaseInsensitiveFileComparator.INSTANCE.compare("abc", "abe");

伴生对象

创建单一的实例,可以实现Java里访问类的私有成员的静态方法

在对象声明的基础上,使用companion关键字来标记,这样做就获得了直接通过容器类名称来访问这个对象的方法和属性的能力。

举个例子,实现CarFactory工厂

通过对象声明的实现

1
2
3
4
5
6
7
8
9
10
11
class CarFactory{
object Instance{
fun newCar(name: String): Car{
return Car(name)
}

fun newRedCar(name: String): Car{
return Car(name, Red())
}
}
}

调用

CarFactory.Instance.newCar("Mini Cooper")

通过伴生对象的实现

1
2
3
4
5
6
7
8
9
10
11
class CarFactory{
companion object {
fun newCar(name: String): Car{
return Car(name)
}

fun newRedCar(name: String): Car{
return Car(name, Red())
}
}
}

调用

CarFactory.newCar("Mini Cooper")

对象表达式

既然是表达式,就意味着有返回值

与Java的匿名内部类只能扩展一个类或者实现一个接口不同,Kotlin的匿名对象可以实现多个接口或者不实现接口。

与对象声明不通,匿名对象不是单例的。每次对象表达式被执行都会创建一个新的对象实例。在对象表达式里不经可以访问创建它的函数中的变量,还可以修改变量的值

举个例子

1
2
3
4
5
6
7
8
9
10
fun countClick(view: View){
var clickCount = 0

view.addClick(object : IClick{

override fun onClick() {
clickCount ++
}
})
}

小结

object对象在Kotlin中的意义:

  1. 实现Java里静态的功能,等效实现静态调用
  2. 代替Java匿名内部类书写

接口

1
2
3
4
5
6
7
8
9
interface IFocus {
val focusName: String

fun showOff(){
println("IFocus foucusName length = ${focusName.length}")
}

fun onFocus()
}

IFocus接口声明函数,让子类实现,默认方法showOff,和抽象方法onFucus

1
2
3
4
5
6
7
8
9
class View : IFocus{

override val focusName: String
get() = "View"

override fun onFocus() {
showOff()
}
}

View实现IFocus接口,focusName由View来确定,获取focusName逻辑写在IFocus的默认方法里。调用如下

val view = View()
view.onFocus()

调用返回内容

IFocus foucusName length = 4

其他

访问权限修饰词

protected在Kotlin和Java的区别
kotlin中protected只能是其子类和自身才能访问;Java中则是同包下所有文件和不同包的子类能访问

Kotiln访问权限由小到大排列依次是:
private, protected, internal, public

Kotlin函数和属性默认是public的,Java的默认是包级访问范围,即同一个包下的类能访问。
Kotlin缺少包级别访问控制,而多了一个模块访问范围internal。internal表示同一个项目模块下的类都能访问

参考资料