Kotlin中的面向对象编程

Kotlin和Java一样是面向对象的。面向对象简单的说就是把现实中的事物都抽象为“对象”。每个对象是唯一的,且都可以拥有它的属性与行为。我们就可以通过调用这些对象的方法、属性去解决问题。

一、类和对象

class Person {
    var name=""
    var age=0
    fun eat(){
        println(name+"is eating. He is "+age+"years old.")
    }


}

fun main() {
    val person = Person()
    person.name="Jack"
    person.age=19
    person.eat()
}

Kotlin实例化一个类的方式和Java基本相似,只是去掉了new关键字而已。
Kotlin中的面向对象编程_第1张图片

二、继承与构造函数

新建Student类

class Student {

    var sno="";
    var grade=0;
}

这个时候如果想让Student类继承Person类,需要做两件事
第一件事:让Person类可继承,在Kotlin中任何一个非抽象类默认都是不可以被继承的,相当于java中给类声明了final关键字。之所以这么设计,其实还和val关键字差不多,因为类和变量一样,最好都是不可变的,一个类如果允许被继承的话,它无法预估子类会如何实现,因此可能存在一些未知的风险。Effective Java这本书就明确规定了,如果一个类不是专门为继承而设计的,就应该加上final声明,禁止它可以被继承。
既然现在Person类是无法被继承的,我们只需在Person类的前面加个open关键字就可以了

open class Person { 
}

这样Person类就允许被继承了
第二件事:让Student类继承Person类。在java中继承的关键字是extends,而在Kotlin中变成一个冒号,写法如下:

class Student :Person(){//继承Person类

    var sno="";
    var grade=0;
}

继承的写法只是替换一下关键字的话,倒也是挺简单的,但是为什么Person类的后面要加上一对括号呢?
这个时候涉及到了Kotlin的主构造函数和次构造函数。
主构造函数:
每个类默认都会有一个不带参数的主构造函数,当然你也可以显式地给它指明参数。主构造函数的特点是没有函数体,直接定义在类名后面即可。
比如下面这种写法:

class Student(sno:String,grade:Int) : Person() {//继承Person类
    var sno="";
    var grade=0;
}

这里我们将学号和年纪这两个字段都放到了主构造函数当中,这就表明在对Student类进行实例化的时候,必须传入构造函数中要求的参数。

 val student = Student("a123",5)

这样我们就创建了一个Student的对象,同时指定该学生的学号是a123,年级是5。
这个时候由于主构造函数没有函数体,如果我想在主构造函数中编写一些逻辑,可以使用init结构体,所有主构造函数中的逻辑都可以写在里面。

class Student(sno:String,grade:Int) : Person() {//继承Person类
    var sno="";
    var grade=0;
    init {
        println("sno is "+sno)
        println("grade is "+grade)
    }
}

这个时候如果你去创建一个Student类的实例,一定会将构造函数中传入的值打印出来。
根据继承特性规定:子类的构造函数必须调用父类的构造函数,可是主构造函数没有函数体,我们怎么去调用父类的构造函数呢?可能会去init结构体中调用,当在绝大多数场景中,我们不需要编写init结构体的。
Kotlin中子类的主构造函数调用父类中的哪个构造函数,在继承的时候通过括号来指定。例如:

class Student(sno:String,grade:Int) : Person() {
}

在这里Person类后面的一对空括号表示Student类的主构造函数在初始化的时候会调用Person类的无参构造函数,即使 在无参数的情况下,这对括号也不能省略。
我们将Person改造一下,将姓名和年龄都放到主构造函数当中,如下:

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

此时由于Person类后面的空括号表示要求调用Person类中无参的构造函数,但是Person类现在已经没有无参的构造函数了,所以就提示了上述错误。
在这里插入图片描述
我们可以在Person类的主构造函数中加入name和age这两个参数,再将这两个参数传给Person类的构造函数,代码如下:

class Student(sno:String,grade:Int,name:String,age:Int) : Person(sno,grade) {
}

通过如下代码创建一个Student类的实例

  val student = Student("Lisa",5,"Mike",19)

主构造函数基本上就这些,接下来次构造函数
次构造函数:
任何一个类只能有主构造函数,但是可以有多个次构造函数。次构造函数也可以用于实例化一个类,这一点和主构造函数没有什么不同,只不过它是有函数体的。
Kotlin规定,当一个类既有主构造函数又有次构造函数时,所有的次构造函数都必须调用主构造函数(包括间接调用)。例如 :

class Student( sno:String, grade:Int, name:String,age:Int) : Person( name,age) {

    constructor(name: String,age: Int):this("",0,name,age){
}
    constructor():this("",0){
        
    }
}

次构造函数是通过constructor关键字来定义的,这里我们定义了两个次构造函数:第一个次构造函数接受name和age参数,然后它又通过this关键字调用了主构造函数,并将sno和grade这两个参数赋值成初始值;第二个次构造函数不接收任何参数,它通过this关键字调用了我们刚才定义的第一个次构造函数,并将name和age参数也赋值成初始值,由于第二个次构造函数间接调用了主构造函数,因此这仍然是合法的。
那么现在就拥有三种方式来对Student类进行实例化,分别是通过不带参数的构造函数,两个参数的构造函数和四个参数的构造函数,对应代码如下所示:

    val student1=Student()
    val student2=Student("Jack",18)
    val student3 = Student("Lisa",5,"Mike",19)

接下来看一下一种特殊情况:类中只有次构造函数,没有主构造函数。当一个类没有显式地定义主构造函数且定义了次构造函数时,它就是没有主构造函数的。

class Student : Person {
    constructor(name: String,age: Int):super(name, age){

}
}

首先Student类的后面没有显式地定义主构造函数, 同时又因为定义了次构造函数,所以现在Student类是没有主构造函数的。所以继承Person类的时候也就不需要再加上括号了。所以这也就是为什么继承的时候有时候加上括号,有时候不加上括号。
由于没有主构造函数,次构造函数只能直接调用父类的构造函数,所以上述代码也是将this关键字换成了super关键字。

三、函数头和函数参数

Kotlin中的面向对象编程_第2张图片
函数参数的声明格式是:参数名:参数类型

fun  main(){
println(doSomething(5,true))
}
private  fun doSomething(age:Int ,flag:Boolean):String{
        return  "result"
}

Kotlin中的面向对象编程_第3张图片

fun main() {
    var a = 37
    var b = 40
    println("larger number is "+largeNumber(a,b))

}
fun largeNumber(number1:Int,number2:Int):Int{
    return max(number1,number2);

}

Kotlin中的面向对象编程_第4张图片
还可以对该函数进行简化,当函数只有一行代码的时候,Kotlin允许我们不必编写函数体,可以直接将唯一一行代码写在函数定义的尾部,中间用符号连接即可。

import kotlin.math.max

fun main() {
    var a = 37
    var b = 40
    println("larger number is "+largerNumber(a,b))
}
fun largerNumber(num1:Int,num2:Int):Int= max(num1,num2)

由与Kotlin的类型推导机制,max函数返回的是一个Int类型的值,而我们在largeNumber()尾部又使用等号连接了 max()函数,所以Kotlin可以推导出largerNumber()函数返回的必定是一个Int值。也可以不用显式地声明返回值类型

fun largerNumber(num1:Int,num2:Int)= max(num1,num2)
fun unitExample() {
    println("test,Unit")
}


fun main() {
    val helloUnit = unitExample()
    println(helloUnit)
    //println(helloUnit is kotlin.Unit)
}

Kotlin中的面向对象编程_第5张图片

有时候用print打印输出的时候,控制台会打印出一串“kotlin.Unit”
研究了一下和print中打印的内容有关,如果打印的是一个有返回值的方法,则输出返回值,如果打印的是一个没有返回值的方法,就会打印出一串“kotlin.Unit”,而不是什么都不打印,为什么呢?因为print调用Unit的toString方法, Unit的toString方法内容:

public object Unit {
    override fun toString() = "kotlin.Unit"
}

Kotlin中的面向对象编程_第6张图片
函数的参数默认值
我们可以在定义函数的时候给任意参数设定一个默认值,这样当调用此函数时就不会强制要求调用方为此参数传值,在没有传值的情况下会自动使用参数的默认值。

fun  main(){
    fix("woshi ")
}

//age采用默认值参
fun fix(name:String,age:Int=2 ){
    println(name+age)
}

Kotlin中的面向对象编程_第7张图片
这个例子比较理想化,因为正好是给最后一个参数设定了默认值,现在我们将代码改成给第一个参数设定默认值,如下所示:

fun fix(age:Int=2 ,name:String){
    println(name+age)
}

这个时候如果按照之前的写法是行不通的,因为编译器会认为我们想把字符串赋值给第一个num参数,从而报类型不匹配的错误。
Kotlin中的面向对象编程_第8张图片
不过不用担心,Kotlin提供了另外一种神奇的机制,就是通过具名函数参数方式来传参,从而不必按照参数定义的顺序来传参。


fun main() {
    //使用命名值参数可以不管值参的顺序
    fix(age=4,name = "jack")

}

fun fix(name:String,age:Int){
    println("name-->$name age-->$age")
}

Kotlin中的面向对象编程_第9张图片
在主构造函数中,我们也可以给参数设定默认值的方式来实现,代码如下:

class Student(val sno:String="",val grade:Int=0,name:String="",age:Int=0):Person(name,age)

在给主构造函数的每个参数都设定默认值之后,我们就可以使用任何传参组合的方式来对Student进行实例化。

四、接口

接口是用于实现多态编程的重要组成部分。我们也知道,java是单继承,一个类最多只能继承一个父类,但是却可以实现多个接口,Kotlin也是如此。
我们可以在接口中定义一系列的抽象行为,然后由具体的类去实现。

//新建Study接口
interface Study {
    fun readBooks()
    fun doHomework()
}

接下来就可以让Student类去实现Study接口了,这里将Student类原有的代码调整,以突出继承父类和实现接口的区别:

class Student(name:String,age:Int):Person(name, age),Study{
    //实现Study接口的两个方法
    override fun readBooks() {
        println(name+"is reading")
    }

    override fun doHomework() {
        println(name+"is doing homework")
    }
}

java中继承使用的关键字是extends,实现接口使用的关键字是implements,而Kotlin中统一使用冒号,中间用逗号进行分隔。上述代码就表示Student类继承了Person类,同时还实现了Study接口。另外接口的后面不用加上括号,因为它没有构造函数可以去调用。

class Student(name:String,age:Int):Person(name, age),Study{
    override fun readBooks() {
        println(name+" is reading")
    }
    override fun doHomework() {
        println(name+" is doing homework")
    }
}
fun doStudy(study: Study){
    study.doHomework()
    study.readBooks()
}

fun main(){
    val student = Student("lisa", 20)
    doStudy(student)
}

首先创建一个Student类的实例,将它传入了doStudy()函数中。doStudy()函数接收一个Study类型的参数,由于Student类实现了Study接口,因此Student类的实例是可以传递给doStudy()函数的,接下来我们调用了Study接口的readBooks()和doHomework()函数,这种就叫做面向接口编程,也可以称为多态。

运行代码,如下图所示:
Kotlin中的面向对象编程_第10张图片
Kotlin增加了一个额外的功能:允许对接口中定义的函数进行默认实现。
修改Study接口中的代码

interface Study {
    fun readBooks()
    fun doHomework(){
        println("do homework default implementation.")
    }
}

可以看到我们给doHomeWork()函数加上了函数体,并且在里面打印了一行日志。如果接口中的一个函数拥有函数体,这个函数体中的内容就是它的默认实现现在当一个类去实现Study接口时,只会强制去实现readBooks()函数,而doHomework()函数则可以自由选择实现或者不实现,不实现时就会自动使用默认的实现逻辑。
此时在Student类中,你会发现如果我们删除了doHomeWork()函数,代码时不会提示错误的,而删除readBooks()函数则不行。当删除doHomeWork()函数之后,重新运行main()函数,结果为
Kotlin中的面向对象编程_第11张图片

五、可见性修饰符

Java和Kotlin函数可见性修饰符对照表

修饰符 Java Kotlin
public 所有类可见 所有类可见(默认)
private 当前类可见 当前类可见
protected 当前类、子类、同一包路径下的类可见 当前类,子类可见
default 同一包路径下的类可见 (默认)
internal 同一模块中的类可见

六、数据类与单例类

数据类通常需要重写equals()、hashCode()、toString()这几个方法。其中equals()方法用于判断两个数据类是否相等。hashCode()方法作为equals()的配套方法,也需要一起重写,否则会导致HashMap、HashSet等hash相关的系统类无法正常工作。toString()方法用于提供更清晰的输入日志,否则一个数据类默认打印出来的就是一行内存地址。
新建一个手机数据类,只有品牌和价格这两个字段。如果使用java来实现这样一个数据类,代码写为:

public class Cellphone {
    String brand;
    double price;

    public Cellphone(String brand, double price) {
        this.brand = brand;
        this.price = price;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Cellphone)) return false;
        Cellphone that = (Cellphone) o;
        return Double.compare(that.price, price) == 0 &&
                Objects.equals(brand, that.brand);
    }

    @Override
    public int hashCode() {
        return brand.hashCode()+(int)price;
    }

    @Override
    public String toString() {
        return "Cellphone{" +
                "brand='" + brand + '\'' +
                ", price=" + price +
                '}';
    }
}

而在Kotlin中,如果你希望这个类是一个数据类,可以在类前面声明data关键字,就表明你希望这个类是一个数据类,Kotlin会根据主构造函数中的参数帮你将equals()、hashCode()、toString()等固定且逻辑意义的方法自动生成,从而减少开发工作。

data class Cellphone(val brand:String,val price:Double)

fun main(){
    val cellphone1= Cellphone("Samsung",1399.99)
    val cellphone2= Cellphone("Samsung",1399.99)
    println(cellphone1)
    println("cellphone1 equals cellphone2 "+(cellphone1==cellphone2));
}

Kotlin中的面向对象编程_第12张图片
单例模式可以用于避免创建重复对象,比如我们希望某个类在全局最多只能拥有一个实例,这时可以使用单例模式。单例模式有很多种写法,这里使用饿汉式写法。
java中实现为

public class SingletonDemo {

    private static SingletonDemo instance =  new SingletonDemo();	
    private SingletonDemo() {}
 
    public static SingletonDemo getInstance() {
        return instance;
    }
       public void show() { println("我是show函数...")
    }

如果想调用单例类中的方法,比如调用show方法可以这样写

SingletonDemo singletonDemo=SingletonDemo.getInstance();
singletonDemo.show();

而在Kotlin中创建一个单例类的方式更为简单,只需要将class关键字改为object关键字即可,一个单例类就完成了。

object SingletonDemo {
    fun show() { println("我是show函数...")
    }
}

调用单例类的函数也和java中静态方法的调用方式类似

SingletonDemo.show()

这种写法虽然看上去像是静态方法的调用,但其实Kotlin背后自动帮我们创建了一个SingletonDemo类的实例,并且保证全局只会存在一个SingletonDemo实例。

你可能感兴趣的:(Kotlin,kotlin,android)