Kotlin学习笔记(三):类和对象

本文是Kotlin学习笔记第三篇,还没看过前两篇文章的朋友可以考虑看看。

在学习的过程中,我会不断的比对Kotlin和Java间的差异点,相信你可以从中看到Kotlin相比Java的一些亮点,看完本篇文章,你可以知晓以下这些问题。

类声明

和Java一样,Kotlin中使用关键字class来声明一个类。如下即是声明一个最简单的没有任何属性和方法的类

1
2
// 没有任何属性、方法的Coder类
class Coder {}

一个完整的类声明包含类名类头(指定构造参数、构造方法等),类体(用大括号包裹的部分)。类头和类体这两个部分并非必要的,像上面没有任何属性、方法的Coder类,我们可以省略掉类头和类体写成下面这样。

1
2
// 没有类头、类体的Coder类
class Coder

属性和赋值

在声明一个最简单的空壳类之后,我们来为它增加一些类属性。Kotlin中类的属性可以用var或者val关键字进行声明,其中var为可变属性,val为只读属性(相当于Java的final)。

1
2
3
4
class Student {
var name = "D_clock爱吃葱花" //名字属性可变,用var
val birthday = "1994-10-24" //生日属性不可变,用val
}

像上面这样就简单的为Student类声明了name和birthday两个属性,且在声明属性时进行了初始化,按照Kotlin的类型推断特点,name和birthday就是属于String类型(不知道类型推断的同学可以翻阅前面的写的文章)。现在我想为Student类添加一个age属性,但是我并不想在声明时进行初始化,用Java写起来非常简单即可实现

1
2
3
4
5
public class JavaStudent {
private String name = "D_clock爱吃葱花";
private String birthday = "1994-10-24";
private int age;//Java版的实现
}

按照Java的实现套路直接套入Kotlin你会发现IDE直接报错并提示property must be initialized or be abstract

按照提示我们必须把类和字段都声明为abstract才可以通过编译。

1
2
3
4
5
abstract class Student {
var name = "D_clock爱吃葱花" //名字属性可变,用var
val birthday = "1994-10-24" //生日属性不可变,用val
abstract var age: Int
}

这样未免太过麻烦,而且理解起来也非常奇怪。Kotlin提供了延迟初始化的方式来解决初始化的问题,使用关键字lateinit即可,这样就无需声明abstract了。

可惜使用lateinit延迟初始化age之后,IDE依旧报错,这次提示的内容是lateinit modifier is not allowed on primitive type properties。Kotlin并不支持对原生类型进行lateinit,为什么呢?因为Kotlin会使用null来对每一个用lateinit修饰的属性做初始化,而基础类型是没有null类型,所以无法使用lateinit。这点很好理解,就像可以把int型变量赋值为0,却无法赋值为null一样。所以这里为了通过IDE的编译,我们可以采用两种方案,要么还是直接在age声明时进行初始化,要么不用基础类型来定义age。

1
2
3
4
5
6
class Student {
var name = "D_clock爱吃葱花" //名字属性可变,用var
val birthday = "1994-10-24" //生日属性不可变,用val
var age = 0 //直接使用0初始化age,age为Int型
lateinit var ageStr: String //String不是基础类型,可以使用lateinit初始化
}

创建对象和访问属性

在定义好类并加上属性后,我们可以来创建对象给属性赋值,并打印输出对象的信息

1
2
3
4
5
6
7
fun main(args: Array<String>) {
var student = Student()
student.age = 18 //年龄赋值
println("student birthday: ${student.birthday}")
println("student name: ${student.name}")
println("student age: ${student.age}")
}

这里对属性的访问要比Java简单很多,从Java的角度来看,似乎是直接在对一个类的public属性进行操作,但实际上我们是调用了Student类属性对应的get/set方法在进行操作,从编译后的class文件再进行逆向就可以还原真实现场了。

这里需要强调一点,Kotlin的类会对每个声明的属性自动生成对应的get/set方法,只读类型的val只有get方法,可变类型的var有get/set方法。相比Java的get/set方法,Kotlin显得要简洁得多。

get/set方法

Kotlin类的Kotlin中类属性的get/set方法声明语法如下:

1
2
3
var <propertyName>[: <PropertyType>] [= <property_initializer>]
[<getter>]
[<setter>]

PropertyType、property_initializer、getter、setter均是可选的元素,这里再强调下,val类型变量为可读变量,所以只拥有get方法,而var类型则有get/set方法。这里直接自定义Student类的birthday的get/set方法

1
2
3
4
5
6
7
8
9
class Student {
...
var age: Int?//自定义get/set方法
get() = age
set(value) {//value是自定义的变量名,名字可以随便起
age = value
}
...
}

编译没有问题,但是运行之后报了StackOverflowError错误。

为什么会出现调用栈爆掉的错误呢?前面提到我们student.age这种方式实际上是调用了默认生成的get/set方法,而这里在自定义的get/set方法中使用age属性本质上也是一样,导致形成没有终止条件的递归调用了。直接看Student类的class文件逆向得到的代码你就明白其中原因了。

为了以上问题,这里需要用到Kotlin提供的备用字段(关键字field),将以上代码修改成如下即可正常运行

1
2
3
4
5
6
7
8
9
class Student {
...
var age: Int? = 0
get() = field //使用备用字段自定义get/set方法
set(value) {
field = value
}
...
}

查看class文件也发现代码正常了

备用字段field的使用范围仅限于属性的get/set方法。

主构造函数

前面我们创建对象时都是使用类的默认构造函数,如果有些属性需要在对象创建时就进行初始化,则需要用到自定义构造函数了。Kotlin中的类可以拥有一个主构造函数以及多个二级构造函数,主构造是类头的一部分,使用关键字constructor声明并且跟在类名之后。下面直接自定义构造函数创建了一个Coder类,且类中包含id和name两个属性。

1
2
3
4
5
// 为Coder添加id、name两个属性
class Coder constructor(id: Int, name: String) {
var id = id
var name = name
}

以上的代码相信很好理解,构造函数中的id和name是形参,类体中的id和name是类的成员变量。当构造函数没有注解或者可见性声明时,constructor关键字可以省略不写。但如果构造函数带有可见性声明和注解时,constructor关键字则不可省略,且可见性声明应该在constructor关键字之前。

1
2
//带可见性声明和注解的构造函数
class Coder public @inject constructor(id: Int, name: String) {}

Kotlin类的主构造函数不能包含任何代码,所以,如果你想做一些初始化操作,可以使用init初始化代码块来完成。像下面这样在代码块中做一些初始化操作即可。

1
2
3
4
5
6
7
8
class Coder constructor(id: Int, name: String) {
var id = id
var name = name
init {
println("do sth...")
}
}

Kotlin中,类属性既可以在声明出直接进行初始化,也可以在init代码块中进行初始化操作。个人觉得,直接在类体中把形参赋值给成员变量,会更直观一些。

二级构造函数

二级构造函数,也称为次级构造函数。关于二级构造函数,主要有以下几点:

  • 次级构造函数不能省略constructor关键字
  • 当类拥有主构造函数时,任何一个二级构造函数都需要直接或间接通过另一个二级构造函数代理主构造函数
  • 类中的一个构造函数代理另一个构造函数,需要使用关键字this

第一点容易理解,二三点看起来好像有点饶,但写完代码意思就明朗了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Person constructor(id: Int) {//(构造函数No.0)主构造函数
var id = id//主构造函数初始化id
var name = ""
var age = 0
//(构造函数No.1)直接代理主构造函数
constructor(name: String, id: Int) : this(id) {
this.name = name
}
//(构造函数No.2)代理了构造函数No.1,间接代理主构造函数
constructor(name: String, age: Int, id: Int) : this(name, id) {
this.age = age
}
}

这种构造函数代理初始化的方式在Java代码中也是很常见的,只不过比较少提及代理这个词而已。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class User {
private int id;
private String name;
private int age;
public User(int id) {
this(id, "");
}
public User(int id, String name) {
this(id, name, 0);
}
public User(int id, String name, int age) {
this.id = id;
this.name = name;
this.age = age;
}
}

还有一点,Kotlin和Java一样,如果一个非抽象类没有声明任何构造函数(主构造函数和二级构造函数),那么它会产生一个默认的构造函数,并且该函数的可见性是public。如果一个类不想拥有默认的public构造函数,只需要把默认构造函数可见性显示声明为private。

继承

和所有的Java类都有一个共同的父类Object一样且不支持同时继承多个父类。Kotlin中所有的类都拥有一个共同的父类Any(但Any不是Object,不要搞错)。Any相比Object其内部结构要简单很多,仅有equals()、hashCode()、toString()三个抽象方法。

Object内部的方法

Kotlin中如果一个类能够被子类继承,需要在class前加上open关键字声明,否则是无法被继承的。

1
2
3
open class Animal(name: String) {//声明open才能被子类继承
var name = name
}

如果父类有主构造函数,子类需要在类体之后加上冒号(:)代理父类的主构造函数。

1
2
3
class Cat(name: String, food: String) : Animal(name) {//代理父类的主构造函数
var food = food
}

如果父类没有主构造函数而拥有二级构造函数,则需要通过super关键字来代理实现父类的二级构造函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
open class Animal {
var name = "unknown"
constructor(name: String) {
this.name = name
}
}
class Cat : Animal {
var food = "unknown"
constructor(name: String, food: String) : super(name) {//代理实现父类的二级构造函数
this.food = food
}
}

讲到继承这里你可以发现,Kotlin上所有类默认都是无法继承的,这点和Java恰好相反,从编译生成的字节码文件也可以看出来。

总结

以上就是本篇文章的全部内容,外发前已经做了几次校正,如果还是存在错误疏漏,欢迎读者们指正。结合读者留言过的一些问题,大致总结了一下:

  • 语法点很多记不住怎么办?代码写多了就会记住了,记不住就是写得太少了。
  • 写过Java又写Kotlin,有时候遇到一些问题一时绕不过弯来怎么办?我遇到这种情况,都是拿class文件反编译来看,往往问题就迎刃而解。
  • 连载更新太慢了?确实太慢了,这点我最近参考了一些优秀的公众号,他们的做法,是每次围绕小知识点写一篇小短文,我也打算这么做,说不定以后还要写Python笔记、React Native笔记呢…

大致就是以上这些了,对内容有问题,欢迎大家留言反馈。

本文为技术视界原创作品,转载请注明原文出处:http://blog.coderclock.com/2017/07/30/kotlin/kotlin-notes-2 ,欢迎关注我的微信公众号:技术视界

推荐文章