Kotlin学习笔记3---面向对象

103 阅读7分钟

本课程为极客时间上朱涛老师的[《Kotlin编程第一课》](time.geekbang.o# 面向对象

1.如何写出Kotlin风格的代码

由于Kotlin和Java都是基于JVM,而且二者的原理极其接近,这就导致了很多人会用Java的思维来写Kotlin的代码,那么Java和Kotlin之间的代码具备什么样的区别呢?

  • 如何写出Kotlin的类

下面就是一个Kotlin的类

class Person(val name: String, var age: Int)

一行代码搞定,言简意赅;如果把上面的代码翻译成对应的Java代码

public class Person {
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    // 属性 name 没有 setter
    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

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

对比一目了然;而且Kotlin中使用了val关键字修饰参数name,也就是说;这个类中name只有getter,没有setter;而age使用的是var,也就是说age同时拥有getter和setter

2.自定义属性getter

例如:这里提出一个需求,当前Person的年龄是否大于18;

从一个Java Coder的角度,肯定能写出下面的代码:

class Person(val name: String, var age: Int) {
    fun isAdult(): Boolean {
        return age >= 18 
    }
}

当然,还会很聪明觉得可以写得更简洁

class Person(val name: String, var age: Int) {
    fun isAdult() = age >= 18 
}

但其实,这里还有个更便捷的写法,就是用getter

class Person(val name: String, var age: Int) {
    val isAdult
        get() = age >= 18
//        ↑
//    这就是isAdult属性的getter方法
}

当然,若是判断逻辑非常复杂,也可以用{} 修饰

class Person(val name: String, var age: Int) {
    val isAdult: Boolean
        get() {
            // do something else
            return age >= 18
        }
}

但是这样的,类型推断就会失效,必须在参数值后加返回的参数类型(否者就是返回Void,这是后话不提)

  • 答疑时刻:为何这里需要引入一个isAdult的属性了,属性在Java中会常占一些内存,会不会本末倒置了?

    1.首先,从语法的角度上来说,是否为成年人,本来就是属于人身上的一种属性。我们在代码当中将其定义为属性,更符合直觉。而如果我们要给 Person 增加一个行为,比如 walk,那么这种情况下定义一个新的方法就是非常合适的。

    2.其次,从实现层面来看,我们确实定义了一个新的属性 isAdult,但是 Kotlin 编译器能够分析出,我们这个属性实际是根据 age 来做逻辑判断的。在这种情况下,Kotlin 编译器可以在 JVM 层面,将其优化为一个方法。而且转义成Java,这种属性也是定义成方法的

    综上所述就是:我们用语法上的一个属性,实现了一个方法;这样可读性好,而且资源消耗低

  • 错误的写法(下面的写法会导致什么问题?)

class Person(val name: String, var age: Int) {
    val isAdult = age >= 18
}

分析:它的问题就在于一次初始化后,age的参数被使用一次后就无法修改,这就违背了代码初衷(曾几何时写过这样的bug代码)

3.自定义属性setter

问题:如果在设置age的时候,我想要添加一些log日志记录,这又该怎么做?

回答:Java中肯定就直接在setAge的代码块中直接添加;但是Kotlin有更简洁的方法

class Person(val name: String) {
    var age: Int = 0
//  这就是age属性的setter
//       ↓
        set(value: Int) {
            log(value)
            field = value
        }
    // 省略
}

上面就是setter,用来对属性赋值;其中关键字就是field = value甚至,如果要限制外部访问,还可以加private限制

class Person(val name: String) {
    var age: Int = 0
        private set(value: Int) {
            log(value)
            field = value
        }
    // 省略
}

4.抽象类与继承

  • 抽象

关键字:abstract

abstract class Person(val name: String) {
    abstract fun walk()
    // 省略
}
  • 继承

Kotlin的继承与Java有很大的区别;Kotlin 的继承和接口实现语法是一样的

下面是接口的实现实例:

interface Behavior {
    // 接口内的可以有属性
    val canWalk: Boolean

    // 接口方法的默认实现
    fun walk() {
        if (canWalk) {
            // do something
        }
    }
}

class Person(val name: String): Behavior {
    // 重写接口的属性
    override val canWalk: Boolean
        get() = true
}

而且Kotlin还提供了接口默认实现方法,这个在Java一直到Java8才引入;

Kotlin 的类,默认是不允许继承的,除非这个类明确被 open 关键字修饰了。另外,对于被 open 修饰的普通类,它内部的方法和属性,默认也是不允许重写的,除非它们也被 open 修饰了:

ps:这个好处就在于符合设计模式的开闭原则----对修改关闭,对拓展开放

open class Person() {
    val canWalk: Boolean = false
    fun walk()
}

class Boy: Person() {
    // 报错
    override val canWalk: Boolean = true
    // 报错
    override fun walk() {
    }
}

在继承的行为上面,Kotlin 和 Java 完全相反;Java 的继承是默认开放的,Kotlin 的继承是默认封闭的;这个在很多Java的技术类书籍类都抨击,因为Java的继承滥用会导致更多问题;而Kotlin就很好避开了这一点;

5.嵌套

Java中嵌套有两种:非静态内部类、静态内部类

  • 静态内部类
class A {
    val name: String = ""
    fun foo() = 1


    class B {
        val a = name   // 报错
        val b = foo()  // 报错
    }
}

​ 特点就是:内外部无法相互访问,国中国的关系

  • 非静态内部类(关键字:inner)
class A {
    val name: String = ""
    fun foo() = 1
// 增加了一个关键字
//    ↓
    inner class B {
        val a = name   // 通过
        val b = foo()  // 通过
    }
}

内部类可以访问外部类的方法,属性;

追问:为何要这样设计?

因为在Java中若是不加stattic关键字,内部类默认是非静态内部类;这样内部类会持有外部类的对象;这样就很容易导致外部类内存无法释放;导致内存泄漏

然而,Kotlin这样的设计,从设计角度就将默认犯错的风险完全抹掉了

6.Kotlin中的特殊类

6.1 数据类

数据类(Data Class),顾名思义,就是用于存放数据的类;它的格式如下:

    // 数据类当中,最少要有一个属性data class Person(val name: String, val age: Int)

数据类的好处就在于会自动生成一些有用的方法。分别如下

  • equals()
  • hashCode()
  • toString()
  • componentN() 函数
  • copy()

实例:

val tom = Person("Tom", 18)
val jack = Person("Jack", 19)

println(tom.equals(jack)) // 输出:false
println(tom.hashCode())   // 输出:对应的hash code
println(tom.toString())   // 输出:Person(name=Tom, age=18)

val (name, age) = tom     // name=Tom, age=18
println("name is $name, age is $age .")

val mike = tom.copy(name = "Mike")
println(mike)             // 输出:Person(name=Mike, age=18)

val (name, age) = tom”这行代码,其实是使用了数据类的解构声明;

copy方法实现了拷贝的同时,还能修改某个属性。(而且这个只是浅拷贝,深拷贝另有他法)

6.2密封类

密封类,是更强大的枚举类

  • 枚举
enum class Human {
    MAN, WOMAN
}

fun isMan(data: Human) = when(data) {
    Human.MAN -> true
    Human.WOMAN -> false
    // 这里不需要else分支,编译器自动推导出逻辑已完备
}

所谓枚举,就是一组有限的数量的值;配合when一起,甚至可以推导出逻辑是否完备;但是它有一定局限性

下面的例子(有点难理解)

//== 这里是对比结构是否相等
println(Human.MAN == Human.MAN)
//=== 可以理解为指针对比,相当于对比内存地址
println(Human.MAN === Human.MAN)

输出
true
true

这里就出现了枚举的结构和引用都是相等的;如果出现以下场景,它就局限了:

我们要枚举的值来自于不同的对象引用呢?

回答:kotlin引入了新的关键字sealed,中文直译就是"密封",下面就是一个密封类的例子:

sealed class Result<out R> {
    data class Success<out T>(val data: T, val message: String = "") : Result<T>()

    data class Error(val exception: Exception) : Result<Nothing>()

    data class Loading(val time: Long = System.currentTimeMillis()) : Result<Nothing>()
}

这种代码在业务代码中回来处理Result回调会常用到;而且在处理回调时就可以用以下代码来写:

fun display(data: Result) = when(data) {
    is Result.Success -> displaySuccessUI(data)
    is Result.Error -> showErrorMsg(data)
    is Result.Loading -> showLoading()
}