Kotlin 开发实践 面向对象 类特性

时间:2022-07-31 20:06:24

转载请注明出处:http://blog.csdn.net/smartbetter/article/details/73656551

大家好,上一篇主要从 Kotlin 的数据类型出发,完整的学习了 Kotlin 的数据类型。之后从本篇开始,会真正讲述 Kotlin 语言的神奇之处。与 Java 相同,Kotlin 声明类、抽象类与接口的关键字分别是 class、abstract 与 interface 。类声明由类名、类头和类体构成。其中类头和类体都是可选的; 如果一个类没有类体,那么花括号也是可以省略的。

1.类的构造函数

1.主构造函数

Kotlin 的主构造函数可以写在类头中,跟在类名后面,如果有注解还需要加上关键字 constructor。下面我们为 Person 创建带一个 String 类型参数的主构造函数:

class Person(private var name: String) {
init {
name = "zhangsan"
}

internal fun printName() {
println("name $name")
}
}

在主构造函数中不能有任何代码实现,额外的代码需要放到 init 代码块中执行。

2.次级构造函数

一个类可以有多个构造函数,但是只有主构造函数可以写在类头中,其他的次级构造函数就需要写在类体中了。

class Person(private var name: String) {
private var description: String? = null
init {
name = "zhangsan"
}

// 让次级构造函数调用了主构造函数,完成 name 的赋值
constructor(name: String, description: String) : this(name) {
this.description = description
}

internal fun printName() {
println("name $name")
}

}

由于次级构造函数不能直接将参数转换为字段,所以需要手动声明一个 description 字段,并为 description 字段赋值。

3.修饰符

修饰符 含义
public 访问权限修饰符,公共的
internal 访问权限修饰符,模块内可见(IDEA 中一个 module 就是一个模块)
protected 访问权限修饰符,受到保护的,子类可见
private 访问权限修饰符,私有的
open 确定这个类会被继承时添加本修饰符(在 Kotlin 中默认每个类都是不可被继承的)

2.继承与实现

类只能单继承,接口可以多实现。

abstract class Person(open val age: Int){
abstract fun printAge()
}
class Student(age: Int): Person(age){ // 继承类时实际上调用了父类构造方法
override val age: Int // 覆写参数
get() = 0
override fun printAge() { // 覆写父类(接口)成员需要 override 关键字
println(age)
}
}
fun main(args: Array<String>) {
val person: Person = Student(18)
person.printAge()
println(person.age)
}

输出结果为:

0
0

在这里我们看到了一个关键字 open,open 关键字可以用来允许一个类被继承,而且同样函数默认也是final的,不能被 override,要想重写父类函数,父类函数必须使用 open 定义。接口、接口方法、抽象类默认为 open 无须 open 定义。不仅如此,在Kotlin中,函数参数默认也都是final的。

3.抽象代理与属性代理

1.接口代理

// 普通讲师
class Teacher: Reader, Writer {
override fun read() {
}
override fun write() {
}
}

// 资深讲师
class SeniorTeacher(val reader: Reader, val writer: Writer): Reader by reader, Writer by writer // 接口代理

class BookReader: Reader {
override fun read() {
println("读书")
}
}
class BookWriter: Writer {
override fun write() {
println("写书")
}
}

interface Reader{
fun read()
}
interface Writer{
fun write()
}

fun main(args: Array<String>) {
val reader = BookReader()
val writer = BookWriter()
val seniorManager = SeniorTeacher(reader, writer)
seniorManager.read()
seniorManager.write()
}

输出结果为:

读书
写书

在这里我们看到了一个关键字 by,by 关键字可以用来表示接口代理,除了接口代理,还有属性代理。

3.属性代理

class Delegates{
val hello by lazy { // 属性代理,只有第一次访问到 hello 的时候才会被初始化为 "hello"
"Hello"
}
val hello2 by X()
var hello3 by X()
}

class X{ // 自己新建一个属性代理
private var value: String? = null

// 代理者需要实现相应的 setValue/getValue 方法
operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
println("getValue: $thisRef -> ${property.name}")
return value?: ""
}
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String){
println("setValue, $thisRef -> ${property.name} = $value")
this.value = value
}
}

fun main(args: Array<String>) {
val delegates = Delegates()
println(delegates.hello)
println(delegates.hello2)
println(delegates.hello3)
delegates.hello3 = "value of hello3"
println(delegates.hello3)
}

输出结果为:

Hello
getValue: net.smartbetter.kotlindemo.Delegates@3d075dc0 -> hello2

getValue: net.smartbetter.kotlindemo.Delegates@3d075dc0 -> hello3

setValue, net.smartbetter.kotlindemo.Delegates@3d075dc0 -> hello3 = value of hello3
getValue: net.smartbetter.kotlindemo.Delegates@3d075dc0 -> hello3
value of hello3

2.接口方法冲突

下面再来看一种情况:

abstract class A{
open fun x(): Int = 3
}
interface B{
fun x(): Int = 2
}
interface C{
fun x(): Int = 1
}

class D(var y: Int = 0): A(), B, C{
override fun x(): Int { // 子类必须覆写冲突方法
if(y > 100){
return y
}else if(y > 10){
return super<A>.x()
}else if(y > 1){
return super<B>.x()
}else{
return super<C>.x()
}
}
}

fun main(args: Array<String>) {
println(D(1000).x())
println(D(100).x())
println(D(10).x())
println(D(1).x())
}

输出结果为:

1000
3
2
1

这样我们就解决掉两个签名一致且返回值类型相同的冲突(返回值不属于方法签名)。

4.方法重载与默认参数

此处需要先清楚方法覆写和重载的区别,方法覆写是重写了父类已经存在的方法,而重载是写了一个与父类方法方法名相同,参数不同的方法而已。

class Overloads{
fun a(): Int{ // 返回值不属于方法签名,方法重载和返回值没有关系
return 0
}
fun a(int: Int): Int{
return int
}
}

为了避免定义关系不大的重载,也可以添加默认参数:

class Overloads{
@JvmOverloads // 加上这个注解后 Java 中调用 Kotlin 也可传空参数
fun a(int: Int = 0): Int{ // 当不传参数时候直接默认0
return int
}
}

扩展:Jvm 函数签名的概念:函数名、参数列表。

5.一些特殊的类

1.内部类

我们先来看一下在 Kotlin 中静态内部类与非静态内部类的使用(与Java 有一定的区别):

class Outter{
val a: Int = 0;
class Inner{ // 静态内部类
}
inner class Inner2{ // 非静态内部类
val a: Int = 5;
fun hello(){
print(this.a) // 外部a用this@Outter.a,外部a只能非静态内部类访问到
}
}
}

fun main(args: Array<String>) {
val inner = Outter.Inner()
}

我们再来看一个匿名内部类(实际上有类名,类名编译时生成,类似 Outter$1.class)的例子:

interface OnClickListener{
fun onClick()
}

class View{
var onClickListener: OnClickListener? = null
}

fun main(args: Array<String>) {
val view = View()
view.onClickListener = object : OnClickListener{ // 可继承父类、实现多个接口,与 Java 注意区别
override fun onClick() {
}
}
}

2.枚举类

在 Kotlin 中,每个枚举常量都是一个对象。

enum class LogLevel(){
VERBOSE, DEBUG, INFO, WARN, ERROR, ASSERT
}


fun main(args: Array<String>) {
println(LogLevel.INFO.ordinal)
println(LogLevel.INFO.name)
}

如果关注的是代码质量,可以考虑使用枚举,如果你的程序是跑在嵌入式上的,那么更建议用整型来表示,比较省内存。

3.sealed 密封类

sealed 修饰的类称为密封类,用来表示受限的类层次结构。注意和枚举的区分,状态更适合用枚举,而指令则适合用密封类,密封类可以有效的保护我们的指令集,不被继承。

// 定义一个播放器的密封类,子类可数
sealed class Player {
// 播放
class Play(val url: String, val position: Long = 0): Player()
// 停止
object Stop: Player()
}

enum class PlayerState{
IDLE, PLAYING // 空闲状态, 播放状态
}

Kotlin 版本小于 V1.1,子类必须定义为密封类的内部类,V1.1 及以上子类只需要与密封类在同一个文件中即可。

4.data 数据类

我们在 Java 中经常会用到 JavaBean,在 Kotlin 中也有语言级别的支持
——数据类,默认实现 copy、toString 等方法,还会自动将所有成员用 operator 声明(即为这些成员生成 getter/setter 方法):

data class Country(val id: Int, val name: String)

fun main(args: Array<String>) {
val china = Country(0, "中国")
println(china)

println(china.component1())
println(china.component2())

val(id, name) = china
println(id)
println(name)
}

输出结果为:

Country(id=0, name=中国)
0
中国
0
中国

data class 是存在许多坑的,data class 默认是 final 的,并且默认不存在无参构造函数,如果直接使用 data class 替代 JavaBean,会出现很多莫名其妙的问题,为此官方出了两个插件 allOpen、noArg,解决这些问题,使用步骤如下:

1.打开 build.gradle 文件,编辑文件如下:

group 'net.smartbetter'
version '1.0-SNAPSHOT'

buildscript {
ext.kotlin_version = '1.1.2-5'

repositories {
mavenCentral()
}
dependencies {
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
// allOpen、noArg 插件
classpath "org.jetbrains.kotlin:kotlin-noarg:$kotlin_version"
classpath "org.jetbrains.kotlin:kotlin-allopen:$kotlin_version"
}
}

apply plugin: 'java'
apply plugin: 'kotlin'
// allOpen、noArg 插件
apply plugin: 'kotlin-noarg'
apply plugin: 'kotlin-allopen'

// allOpen、noArg 插件配置
// 意思是如果类被PoKo标注了,那么在编译的时候会把final去掉,同时也会生成默认的无参构造方法
noArg{
annotation("net.smartbetter.kotlindemo.annotations.PoKo") // 名字无所谓
}
allOpen{
annotation("net.smartbetter.kotlindemo.annotations.PoKo")
}

sourceCompatibility = 1.5

repositories {
mavenCentral()
}

dependencies {
compile "org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlin_version"
testCompile group: 'junit', name: 'junit', version: '4.11'
}

PoKo.kt 文件如下:

annotation class PoKo

2.为 data class 添加 @PoKo 注解:

@PoKo
data class Country(val id: Int, val name: String)

此时问题就解决了。

6.类的扩展

在 Java 开发的时候,经常会写一大堆的 Utils 类。如果每个类在想要用这些工具类的时候,他们自己就已经具备了这些工具方法多好,Kotlin的类扩展方法就是这个作用。

1.扩展方法

Kotlin 中使用 args.isEmpty() 判断 args 是否为空,而 isEmpty 方法也并不是定义在 Arrays.kt 中,而是定义在了 _Arrays.kt 中,isEmpty 其实就是一个扩展方法,扩展方法的定义和普通方法的定义非常相似:

public inline fun <T> Array<out T>.isEmpty(): Boolean {
return size == 0 // this.size,this 指的就是 Array<out T>
}

下面我们自己定义一个扩展方法:

// 定义 String类 的扩展方法
operator fun String.times(int: Int): String{ // operator:运算符重载
val stringBuilder = StringBuilder()
for(i in 0 until int){
stringBuilder.append(this)
}
return stringBuilder.toString()
}

// 定义 String类 的扩展成员属性
var String.a: Int
set(value) {
}
get() = 5

fun main(args: Array<String>) {
println("abc" * 3) // "abc".times(3)
println("abc".a)
}

输出结果为:

abcabcabc
5

需要注意的是扩展方法是静态解析的,而并不是真正给类添加了这个方法。

8.伴生对象与静态成员

Kotlin 中也实现类似 Java 中的静态方法,可以使用包级函数、伴生对象、扩展函数和对象声明,需要大家根据不同的情况进行选择。官方推荐是包级函数,也有人说用伴生对象。

包级函数可以在包里面直接声明函数。伴生对象从语义上来讲,伴生函数与 Java 中静态方法最为相近,所以用伴生对象完全可以实现 Java 中静态类的所有内容。

// 伴生对象
class Latitude private constructor(val value: Double) {
// 只有一个实例,静态方法
companion object {
@JvmStatic // 加上这个注解后,在 Java 类中就可以直接 Latitude.ofDouble 调用了
fun ofDouble(double: Double): Latitude{
return Latitude(double)
}
@JvmStatic
fun ofLatitude(latitude: Latitude): Latitude{
return Latitude(latitude.value)
}
}
@JvmField
val TAG: String = "Latitude" // 静态常量
}

fun main(args: Array<String>) {
val a = minOf(args[0].toInt(), args[1].toInt()) // minOf 就是一个包级函数
val latitude = Latitude.ofDouble(3.0)
}

每个类可以对应一个伴生对象,伴生对象的成员全局唯一,伴生对象的成员类似 Java 的静态成员。

静态成员考虑用包级函数、常量替代。

8.单例类设计

Kotlin 中使用 object 关键字声明一个单例对象,单例类不能自定义构造方法,本质上就是单例模式最基本的实现。

class Driver

interface OnPlayerListener{
fun onMount(driver: Driver)
fun onUnmount(driver: Driver)
}

abstract class Player

object MusicPlayer: Player(), OnPlayerListener{
override fun onMount(driver: Driver) {
}
override fun onUnmount(driver: Driver) {
}

val state : Int = 0

fun play(url : String){
}
fun stop(){
}
}

9.动态代理与伪多继承

1.动态代理

Kotlin 原生支持动态代理,而 Java 的动态代理需要反射,而且需要额外多写很多的代码方法。在动态代理上,Kotlin 比 Java 爽太多。

interface Person{
fun printName()
}

class Student :Person {
override fun printName() {
println("zhangsan")
}
}

class Teacher(person: Person) : Person by person {
}

fun main(args: Array<String>) {
Teacher(Student()).printName()
}

输出结果为:

zhangsan

这样,我们就让 Teacher 的 printName() 用 Student 去代理掉了。

2.伪多继承

Kotlin 的动态代理更多的是用在一种需要多继承的场景。使用一个代理类实现所有需要获取信息的接口方法。然后让不同的子类去实现所需的接口,请求统一交给代理类完成。这样不仅维护了请求信息方便,而且每个类不会有额外多的方法,防止新人接触项目时调错请求方法。

interface A{
fun printA()
}

interface B{
fun printB()
}

class Person : A, B {
override fun printA() {
println("printA")
}

override fun printB() {
println("printB")
}
}

class Teacher(a: A, b: B) : A by a, B by b {
}

fun main(args: Array<String>) {
val person: Person = Person()
Teacher(person, person).printA() // 输出 printA
}

Kotlin 的面向对象与类特性就介绍这么多。喜欢本文的记得顶一下。