【Kotlin回顾】1.面向对象

191 阅读9分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第1天,点击查看活动详情

写在前面:Kotlin问世很久了,用了也蛮久的,但是自己也一直没总结过,知识完整学习过程是有输入还有输出,这样才理解的更清楚,前段时间有空正好就总结了一下,算是一个回顾吧,有不足的地方还请大家指正。

1.类

  • 类的定义

Kotlin中类的定义一般这么写

class Person(){
}

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

上面的代码一个是无参一个有参,这个有参的就类似与Java中的有参构造函数,Kotlin中的构造函数默认这么写

多个构造函数的情况要怎么办? 可以这么写,要把主构造函数中的参数添加到次构造函数中

class Person constructor(val name: String) {
    constructor(name: String, age: Int) : this(name)

    constructor(name: String, gender: String) : this(name)
}

再回头看第一个代码案例,构造函数中的参数定义一个用了val,一个用了var,这是什么意思呢?Kotlin有一个特性是不可变特性,val定义的参数是不能被修改的因此代码段1在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;
    }
}

val定义的name只有setter因为其不可被修改只能读取,var定义的agegettersetter这就意味着它可以被随意修改和读取。上面的代码也表明了另一个信息Kotlin中定义的类默认是Public的, 编译器会帮我们生成构造函数,类中的属性也会根据声明类型生成gettersetter方法。

  • 自定义getter

现在有一个新的需求,我要从Person类中获取到创建的这个对象是否已成年,第一个想法是再创建一个是否成年的参数就好了,但是这样做的意义其实不大,因为我们可以根据年龄就可以判断是否成年,那要怎么做呢,请看代码:

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

//根据Kotlin的特性优化一下
class Person(val name: String, var age: Int) {
    fun isAdult() = age >= 18
}

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


//Java中的代码是这样的
public final class Person {
    private final String name;
    private int age;

    public final boolean isAdult() {
        return this.age >= 18;
    }

    public final String getName() {
        return this.name;
    }

    public final int getAge() {
        return this.age;
    }

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

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

上面的代码中 val isAdult get() = age >= 18利用了Kotlin的单一表达式的属性省略了Boolean类型的声明。自定义的get方法就是getter方法。如果get方法中的逻辑比较复杂就不能省去类型声明了,就要这么写

class Person(val name: String, var age: Int) {
    val isAdult: Boolean
        get() {
            // 其他逻辑
            return age >= 18
        }
}

这里还要解决一个疑问:上面的isAdult原本定义的是一个属性,但是转换为Java代码后怎么就转变成了一个方法呢?这主要是因为后面的age >= 18这行代码决定,Kotlin编译器认为这个isAdult属性的值时根据age来决定的,因此Kotlin编译器在JVM层面将其优化为一个方法了,这样做的好处就是节省了内存开销。

  • 自定义setter

现在又有一个新的需求,Person类中的age是可以被修改的,那么此时我想要记录这个age被修改的记录要怎么做呢?在Java中可以在setAge方法中处理,但是Kotlin没有setAge方法要怎么办?Kotlin提供了自定义set方法,代码如下

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

上面讲了Kotlin中定义的类默认是Public的那么它的属性的被JVM编译后的getset方法也是默认Public,因此如果有些属性不想被修改就要使用private进行修饰。

2.抽象类与继承

Kotlin中的抽象类的定义与Java类似的没有太大区别,但是在使用时是有区别的

abstract class Person(){
    abstract fun walk()
}
  • 区别1:继承,Java的继承关键字是extends,Kotlin只需要用【:】修饰即可,方法的重写也有一些小区别,Java是@OverrideKotlin是override
abstract class Person(){
    abstract fun walk()
}

class children(): Person(){
    override fun walk() {
        
    }
}
  • 普通类继承,普通类没有abstract在Kotlin中是不能被继承的,这就跟Java中被final修饰的类一样不能被继承,那要怎么处理呢,用open关键字修饰即可,代码如下
open class Person() {
    open fun walk() { }
}

class children() : Person() {
    
    override fun walk() {
        super.walk()
    }
}

这里有个细节,如果只是对类进行了open修饰那就只能继承这个类不能重写这个方法,想要在children类中重写walk方法就必须将walk方法也进行open修饰,同时Person类中的walk方法还必须有方法体。

这里我们将Java和Kotlin做一个对比:

Java:一个普通类如果没有被 final 明确修饰的话,它默认就是可以被继承的。

Kotlin:一个普通类如果没有被 open 明确修饰的话,它默认不可以被继承。

由此可以得出一个结论:Java的继承是默认开放的,Kotlin的继承是默认封闭的

3.接口和实现

Kotlin中的接口定义与Java是一样,同时这里还有个细节就是在Kotlin中接口实现和抽象类的继承都是用【:】声明的,如果有多个集成或者接口实现的话后面跟【,】即可

interface Behavior {
    fun walk()
}

class Person(val name: String) : Behavior {
    override fun walk() {
        
    }
}

但是Kotlin接口定义比Java的接口定义更强大,Kotlin的接口可以定义属性和默认实现方法,有了默认实现的方法后Kotlin的类在实现接口的时候就不用必须去重写这个方法。

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

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

        }
    }
}

class Person(val name: String): Behavior {
    override val canWalk: Boolean
        get() = true
}

4.嵌套

嵌套类分为两种,【非静态内部类】和【静态内部类】,Kotlin和Java单从在定义上来讲没有太大区别

class A {
    class B {

    }
}


//Java代码
public class A {
    public class B {

    }
}

但是Kotlin被编译成Java代码区别就显示出来了

//Kotlin的普通嵌套类被编译后的Java代码
public final class A {
   
   public static final class B {
       
   }
}

由上面的代码可以得出一个结论:Kotlin的嵌套类默认是静态的,什么意思呢?我们跟Java做个对比来讲

假如说A类有一个属性和一个方法,如果B类没有用static修饰是可以访问的,反之则不可访问

//没有static修饰
public class A {
    private int a = 0;

    private int test() {
        return 1;
    }

    public class B {
        int b = a;
        int test = test();
    }
}

//有static修饰
public class A {
    private int a = 0;

    private int test() {
        return 1;
    }

    public static class B {
        int b = a;				//报错
        int test = test();		//报错
    }
}

//报错的原因是:静态类不能访问非静态字段

由Java代码可以得知静态类不能引用非静态字段,Kotlin的嵌套类默认是静态类,那要想在Kotlin中访问A类中的属性要怎么办?加一个inner字段即可成为普通类

class A {
    val a = 1
    fun test() = 1
    
    class B {
        val b = a				//报错
        val c = test()			//报错
    }
}


//inner修饰
class A {
    val a = 1
    fun test() = 1

    inner class B {
        val b = a
        val c = test()
    }
}

Kotlin为什么要这么做?因为在Java中嵌套类默认情况下内部类会持有外部类的引用,这样就会造成内存泄漏,而只有加入static关键字之后这个问题才能避免,但是Java开发中这个static字段是很容易被遗漏的,kotlin的做法就是为了杜绝这个问题,默认就是无法持有外部类的引用,想调用外部类的属性就要加上inner关键字。

5.Kotlin中的特殊类

  • 数据类

数据类型就是普通类的前面加上data关键字,同时数据类中还必须要至少要有一个属性

data class Person(val name: String)

密封类还会自动生成几个方法:

    • equals()
    • hashCode()
    • toString()
    • copy()

具体用法如下:

val san = Person("张三", 19)
val four = Person("李四", 22)

//equals()
// 输出:false,这类的equals和 == 是一样的,Android Studio会提示用 == 替换equals
println(san.equals(four)) 

//hashCode()
//输出:对应的hash code
println(san.hashCode())  

//toString()
//输出:Person(name=张三, age=19)
println(san.toString())

//这种类的实现方式使用了数据类型的结构声明,这种方式的好处是
//可以快速的通过数据类来创建变量
//输出:name is 张三, age is 19 .
val (name, age) = san
println("name is $name, age is $age .")


//copy()
//这种方式就是拷贝一份新的数据并修改某项属性值
//输出:Person(name=王五, age=19)
 val five = san.copy(name = "王五")
 println(five)
  • 密封类

密封类是Kotlin中特有的类,用sealed修饰,用来表示某种受到限制的继承结构:当一个值为有限的几种类型而不能有任何其他类型时。也可以理解枚举类的扩展,枚举类型的值集合也是受限的,但每个枚举常量只存在一个实例、而密封类的一个子类可以有可包含状态的多个实例。

前面讲过声明一个密封类只需要用sealed修饰即可,但还有一点要注意:虽然密封类也可以有子类但是所有子类都必须在与密封类自身相同的文件中声明。

//密封类定义
sealed class Result {
    data class Success(val message: String = "") : Result()

    data class Error(val exception: Exception) : Result()

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


//密封类使用
fun main() {
   fun display(result: Result) {
        when (result) {
            is Result.Success -> "成功"
            is Result.Error -> "失败"
            is Result.Loading -> "进行中"
        }
    }
}

密封类的定义还要注意几点:

    • 密封类的定义不允许有private修饰,这样就无法被when引用;

    • 扩展密封类子类的类(间接继承者)可以放在任何位置,无需在同一个文件中;

    • 密封类在使用时如果when表达式中能覆盖所有情况那么就不需要else分支了;

    • 密封类不仅可以定义类也可以定义接口

6.思考题

接口的“成员属性”,是 Kotlin 独有的。请问它的局限性在哪?

Kotlin接口当中的属性在它被真正实现之前,它并不是一个真正的属性,因此Kotlin接口当中的属性它既不能存储任何状态也不能被赋予人和网初始值,因为它本质上还是一个接口方法。