Day 3
Kotlin学习之旅-第三天
今天的主题是:类与继承
前言
今日目标
今天空闲的时候 baidu一下,发现简书,掘金上有很多Kotlin学习总结,但是基本上都是把官方文档一字不落地复制粘贴了过来。我不希望自己也是这样子的,因此在Kotlin学习之旅里面:
- 我只会把最常用的归纳总结起来,其他的大家可以去官方文档进行查看
- 除了官方文档有的知识点,也会加上自己的思考,标注一些不太容易理解,或者容易踩坑的地方
- 根据自己经验,或者评论的意见,不断补充和修改
把这一系列的学习经验写成简洁易懂又实用的文章
话不多说, 今天我们的目标就是搞定下面几个知识点:
- 类与对象基本用法
- 数据类
- 嵌套类
- 内部类
- 继承与接口
Tips:
类与对象基本用法
最基本的用法:
class Day3
没有类体,只有类名,连大括号都省了
然后在Day3中加上构造方法
class Day3 constructor(name: String) {...} // 关键字constructor
但是一般我们看别人写的代码都是没有constructor这个关键字的,为什么呢?
官方文档给了答案:
如果主构造函数没有任何注解或者可见性修饰符,可以省略这个 constructor 关键字
也就是说,Day3可以写成
class Day3(name: String) {...}
直接 类名(参数1,参数2){…} 这种格式就可以了
在kotlin里面,只能有一个主构造函数,和多个次构造函数,例如:
class Day3(name: String){ // 1
constructor(name: String, age: Int): this(name){ // 2
println("this is second constructor and the age is " + age)
}
}
fun main(args: Array<String>) {
var day3 = Day3("hello") // 3
}
- Day3(…) 括号里的就是主构造函数,只是省略了constructor关键字
- { } 大括号里的 constructor(…) 就是次构造函数,每个次构造函数都要委托给主构造函数
- 实例化一个Day3对象
运行代码的结果:啥都没有~
因为我们没有在主构造函数里面做任何的操作,那么如果我们要做初始化操作,要怎么写呢?
class Day3(name: String){
init { // 1
println("this is main constructor")
}
constructor(name: String, age: Int): this(name){
println("this is second constructor and the age is " + age)
}
}
fun main(args: Array<String>) {
var day3 = Day3("hello") // 2
var day33 = Day3("hello", 1) // 3
}
- init就是主构造函数的初始化方法
- 使用主构造函数初始化Day3对象
- 使用次构造函数初始化Day3对象
运行代码的结果:
this is main constructor
this is main constructor
this is second constructor and the age is 1
这里输出了两次main和一次second,原因就是上面我们讲到的 每个次构造函数都要委托给主构造函数 ,通过this关键字,在调用次构造函数之前,都会先调用一次主构造函数,因此会有两个main输出~
通过这个例子,应该就能弄懂 类的定义,主/次构造函数,init的用法,如何实例化对象了
数据类
Kotlin中通过 data 关键字来表示数据类:
data class dataClass(val name: String, val age: Int)
在实际开发中,数据类的使用是很常见的,那么Kotlin里的数据类具有哪些特性呢?
编译器自动从主构造函数中声明的所有属性导出以下成员:
equals()
/hashCode()
对;toString()
格式是"User(name=John, age=42)"
;componentN()
函数 按声明顺序对应于所有属性;copy()
函数(见下文)。
为了确保生成的代码的一致性以及有意义的行为,数据类必须满足以下要求:
- 主构造函数需要至少有一个参数;
- 主构造函数的所有参数需要标记为
val
或var
; - 数据类不能是抽象、开放、密封或者内部的;
因此,有两点需要注意:
- 我们不能像普通类一样 class test(name: String, age: Int) 参数必须标记 val 或 var
- 不能写 data class test(),而是必须至少有一个参数
这样创建数据类,编译器才不会报错~
请注意,对于那些自动生成的函数,编译器只使用在主构造函数内部定义的属性。如需在生成的实现中排出一个属性,请将其声明在类体中
这句话是什么意思呢?举个例子:
data class Person(val name: String) {
var age: Int = 0
}
fun main(args: Array<String>) {
val person1 = Person("John")
val person2 = Person("John")
person1.age = 10
person2.age = 20
println("person1 == person2: ${person1 == person2}")
println("person1 with age ${person1.age}: ${person1}")
println("person2 with age ${person2.age}: ${person2}")
}
输出结果:
person1 == person2: true
person1 with age 10: Person(name=John)
person2 with age 20: Person(name=John)
虽然age不一样,但是 == 结果是 true
说明只有在主构造函数内部定义的属性才具有toString()、 equals()、 hashCode() 、copy()这几个方法,由于属性age定义在类体中,因此是没有的。
最后,标准库提供了Pair和Triple这两个标准数据类
我们来看一下Pair的源码:
public data class Pair<out A, out B>(
public val first: A,
public val second: B
) : Serializable {
/**
* Returns string representation of the [Pair] including its [first] and [second] values.
*/
public override fun toString(): String = "($first, $second)"
}
会发现其中他就是key-value格式的数据类,重写了toString()方法,其他的默认属性都是一样的~
在代码中运行一下:
fun main(args: Array<String>) {
var pair = Pair("1","2")
println(pair)
println(pair.toString())
println(pair.toList())
}
输出结果:
(1, 2)
(1, 2)
[1, 2]
Triple也是类似的,这里就不看了。数据类知识点大概就是这么多~
嵌套类与内部类
嵌套在类里面的类,就是嵌套类,这句话说起来比较拗口,直接看代码:
class Outer { // 1
private val bar: Int = 1
class Nested { // 2
fun foo() = 2
}
}
val demo = Outer.Nested().foo() // == 2
- 外部类
- 内部类
通过Outer.Nested().foo()调用内部类的方法
但是这个时候 Nested 类是不能访问Outer类的成员变量的,直接访问的话会报错~
如果需要,要加上inner关键字,让Nested成为内部类
class Outer {
private val bar: Int = 1
inner class Inner {
fun foo() = bar
}
}
val demo = Outer().Inner().foo() // == 1
继承与接口
这部分在实际应用中也是非常重要的,经常都会用到,毕竟抽象封装多态这三大特性,除了类与对象以外基本上就是通过继承和接口实现了
继承
我们都知道,在Java里面是只能实现单继承的,也就是一个子类只能有一个父类,但是通过接口的方式,其实也就相当于实现了多继承,在Kotlin里面也是一样的,我们先说继承
open class Father(name: String) // 1
class Son(name: String) : Father(name) // 2
- open class Father就是父类,主构造函数里面需要传入String类型的参数name
- Son继承自Father,需要用父类的主构造函数参数进行初始化
覆盖方法
继承自然免不了要覆盖父类的方法,Kotlin里面通过关键字override 来标识
open class Base {
open fun v() { ... } // 1
fun nv() { ... }
}
class Derived() : Base() {
override fun v() { ... } // 2
}
- 被覆盖的方法需要用open标识
- 覆盖的方法需要用override标识
那么如果不想让子类继续覆盖要怎么做呢,只要加上final关键字就可以了
open class AnotherDerived() : Base() {
final override fun v() { ... }
}
调用父类实现
在子类中可以通过super关键字来调用父类的属性和方法
open class Foo {
open fun f() { println("Foo.f()") }
open val x: Int get() = 1
}
class Bar : Foo() {
override fun f() {
super.f()
println("Bar.f()")
}
override val x: Int get() = super.x + 1
}
接口
在Kotlin中,我们使用interface关键字来定义接口
interface MyInterface { // 1
fun bar() // 2
fun foo() {
// 可选的方法体
}
}
- interface 表示接口
- fun xxx() 定义方法名,不需要实现
接口中的继承
interface Named {
val name: String
}
interface Person : Named {
val firstName: String // 1
val lastName: String
override val name: String get() = "$firstName $lastName" // 2
}
data class Employee(
// 不必实现“name”
override val firstName: String,
override val lastName: String,
val position: Position
) : Person
可以看到,Person 继承 Named ,并且重写了 name 属性的get()方法,因此在Employee类实现Person接口的时候,只要实现 firstName, lastName 两个属性就可以了,而不用实现 name 属性
一般来说,我们都会使用到继承+接口两种方式,格式是这样的:
class Day3(name: String) : DayFather(), MyInterface{ // 1,2
init {
println("this is main constructor")
}
constructor(name: String, age: Int): this(name){
println("this is second constructor and the age is " + age)
}
override fun test1() { // 3
TODO("not implemented")
}
override fun test2() {
TODO("not implemented")
}
}
interface test{
fun test1()
fun test2()
}
open class DayFather{
}
- 通过 :Father() 来继承父类
- 通过 ,Interface 来实现接口
- 实现接口中定义的方法
总结
- 类与对象基本用法
- 数据类
- 嵌套类
- 内部类
- 继承与接口
这几个知识点我们今天就学习完了,明天我们会继续学习 函数 和 Lambda表达式
Day 3 - Learning Kotlin Trip,Completed.