5.面向对象

/ 0评 / 0

1.类

申明一个类:

class Person{}

如果这个类没有方法,可以省略:

class Person

;都不用写,语法很简洁

2.构造函数

如果类中有一些属性,我们也可以这样申明一个类

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

这里的属性申明时,可以是var也可以是val,这里的(var name: String,var age:Int),叫做主构造函数,主构造其实就是为了我们像这样申明一个类,我们例子中省略了constructor,那如果我们要对这些参数做初始化呢?主构造里面是不支持任何代码逻辑的,只能通过初始代码块来完成初始化的操作:

class Person (var name: String,var age:Int){
    var nameAge:String? = null

    init {
        nameAge = name+age;
    }
}

当我们不想用这种主构造的时候,或者我们有多个构造函数时,就得使用到次构造函数,使用次构造时,必须要把参数传递给主构造

class Person (var name: String,var age:Int){
    var nameAge:String? = null

    init {
        nameAge = name+age;
    }

    constructor(name: String, age: Int,from:String):this(name,age){
        nameAge = nameAge()
    }
    fun nameAge() = "nameAge"
}

次构造里面可以直接调用其他函数,那主构造,初始化代码块和次构造执行的顺序是怎么样的?我们反编译下就明白了:

public final class Person {
   @Nullable
   private String nameAge;
   @NotNull
   private String name;
   private int age;

   public Person(@NotNull String name, int age) {
      Intrinsics.checkNotNullParameter(name, "name");
      super();
      this.name = name;
      this.age = age;
      this.nameAge = this.name + this.age;
   }

   public Person(@NotNull String name, int age, @NotNull String from) {
      Intrinsics.checkNotNullParameter(name, "name");
      Intrinsics.checkNotNullParameter(from, "from");
      this(name, age);
      this.nameAge = this.nameAge();
   }
}

撇开get,set我省略掉了,最终发编出来的代码,初始化代码块是包含在构造函数当中的,所以,当我们调用一个次构造函数,最终执行顺序是主构造->初始化代码块->次构造函数.

3.创建对象

kotlin里面省去了new关键字,创建对象,简单粗暴var person = Person("Pantheon",20)

4.继承

在第一章的时候简单介绍过一点kotlin的继承,kotlin里面默认创建出来的类都是不可变的,也就是都是由final修饰的,所以一个类如果需要被继承,需要加上open关键字,而接口则天生需要被实现,所以不需要如此:

interface My{
    fun doSth()
}

open class MyClass(val sth: String){
    fun doSth():String = "do${sth}"
}

class MyImpl:My{
    override fun doSth() {
        println("doSth")
    }
}

class MyClassImpl(val mySth: String): MyClass(mySth) {

}

class MyClassConstruct:MyClass{
    constructor():super("other thing")
}

而继承的语法在kotlin中也发生了改变,继承类或者接口都是用:来表示继承关系,对于构造函数的处理的思想则和java类似,只不过可以在主构造中传递参数.

在上述例子中,也演示了如何实现[覆盖]一个方法.也就是对于实现[覆盖]的方法,需要加上override关键字.

5.属性

kotlin因为其简洁的语法特性,导致原本很多比较简单的概念变的复杂起来,比如我们在构造方法中,提到的,主构造和次构造,属性这一个本来不复杂的内容,也因为它简洁的语法而变得复杂.

我们可以直接把属性写在构造方法中,可以是主构造也可以是次构造,最终编译器都会帮助我们生成属性字段,并且生成set,get方法:

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

我们在使用时,可以像访问public属性一样访问这些字段

val person = Person("xxxx", 10)
println(person.name)

如果我们需要在body体内自己定义属性:

class Person{
    var name:String? = null;
    var age:Int = 18;

}

如果创建的时候就知道值是多少,可以用不可空变量申明,如果不知道值是多少,那就用可空变量来什么.

kotlin的set,get方法比较奇葩,语法类似c#

var <propertyName>[: <PropertyType>] [= <property_initializer>]
    [<getter>]
    [<setter>]

举个例子:

class Person{

    private var name:String? = null
    get() = name
    set(value) {
        field = parse(value)
    }
    var age:Int = 18;

}

这个field叫做back field,其实就是指代当前的这个属性,如果用下面这种方式赋值,会陷入无止境的递归

class Person{

    private var name:String? 
    get() = name
    set(value) {
        name = parse(value)
    }
    var age:Int = 18;

}
fun main(vararg args:String) {
    val person = Person()
    person.name = "pppp"
    println(person.name)
}

反编译下:

public final class Person {
   private int age = 18;

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

   public final void setName(@Nullable String value) {
      this.setName(value);
   }

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

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

可以看到最终get方法生成的方法是去掉自己,而set也是掉自己,这个时候我们如果想给属性赋值,只能用back filed,这个和c#的思想是一致的.

还有一个隐藏属性的backing property感觉用处不大,不介绍了.

我们再刚刚介绍说,如果一个字段是可空的,需要用可空类型来申明,并且赋上一个默认null值,那我们想一下这个常见,在IOC中,我们注入的某一个属性,在申明上,它没有默认值,是可空类型,但是使用上,我们知道,一般不会空的,如果我们用可空类型来申明,那得写一堆?,非常不美观,这里就出现了lateinit 关键字:

class Person{
    lateinit var name:String
}

lateinit申明的属性,可以是非空类型,且没有默认值.但是我们想一下,如果我创建完了,不复制,那不一样会报空指针?

fun main(vararg args:String) {
    val person = Person()
    println(person.name)
}

报错如下:

Exception in thread "main" kotlin.UninitializedPropertyAccessException: lateinit property name has not been initialized

我们可以通过如下方式,判断字段是否初始化

val person = Person()
if (person::name.isInitialized1){
        println("-----")
}

lateinit其实在生成字节码时,只在get方法上加了个是不是空值的判断,是空,抛出异常,不是则返回值.

6.代理模式

设计模式中有一个很重要的设计模式,叫做代理设计模式,代理类和实际类都实现同一个接口,访问代理对象的方法时,再去访问实际对象的方法,在kotlin中,用一个by关键字实现了这个逻辑:

interface DoSth{
    fun doSth();
}

class DoSthImpl:DoSth  {
    override fun doSth() {
        println("DoSthImpl")
    }

}

class DoSthProxy(sth: DoSth):DoSth by sth ;

fun main() {
  var proxy = DoSthProxy(DoSthImpl())
    proxy.doSth()
}

代理对象持有实际对象的引用,通过主构造函数传入,就可以很快速的出代理模式,代理类可以重写代理方法,但是没有办法获取到实际对象的引用(虽然传入了)

class DoSthProxy(sth: DoSth):DoSth by sth {
    override fun doSth() {
        println("proxy")
    }
}

代理类感觉用处不算大,实际使用中,以改变字节码的代理技术使用居多,而且代理对象还不能调用实际对象的方法,要么就重写,要么就只是做代理,个人感觉功能很弱.

7.代理属性

代理属性说的是这么一个功能,假如类里面有个属性,我们再访问它或者改变它的值时,可以通过代理来做一些事情,比如lazy赋值,当我们首次访问时,通过lazy后面的表达式来赋值

class Lazy{
    val name:String by lazy {
        println("computed!")
        calName()
    }
    fun calName():String = "name"
}

lazy属性需要申明为不可变类型val,上面这段代码name属性的初始值则由calName来提供,接近于java的init方法,但是比init方法要复杂.by后面也可以跟个对象,代表该属性的初始值由该对象的某个方法来提供,看下这段代码:

class DelegateDemo{

    var delegate1:MyDelegate by Delegate()

    var delegate2:MyDelegate by Delegate()

}

class MyDelegate
class Delegate{
    operator fun getValue(delegateDemo: DelegateDemo, property: KProperty<*>): MyDelegate {

        println("Delegate getValue:${property.name}")
            return MyDelegate()
    }

    operator fun setValue(delegateDemo: DelegateDemo, property: KProperty<*>, myDelegate: MyDelegate) {
        println("Delegate setValue:${property.name}")
    }

}

fun main() {
    var demo = DelegateDemo();
    demo.delegate1
    demo.delegate2
    demo.delegate2 = MyDelegate()

}

运行的结果如下:

Delegate getValue:delegate1
Delegate getValue:delegate2
Delegate setValue:delegate2

也就是说,代理属性如果由某个对象提供,其实就是由这个对象提供了该属性的get,set方法,也就是该类需要定义两个方法,getValue和setValue.

代理属性也可以将自己的值代理给别的属性,也就是自己的值,由别的属性来提供:

var topLevelInt: Int = 10
class ClassWithDelegate(val anotherClassInt: Int)

class MyClass(var memberInt: Int, val anotherClassInstance: ClassWithDelegate) {
    var delegatedToMember: Int by this::memberInt
    var delegatedToTopLevel: Int by ::topLevelInt

    val delegatedToAnotherClass: Int by anotherClassInstance::anotherClassInt
}
var MyClass.extDelegated: Int by ::topLevelInt

因为by后面跟的是表达式,所以这里是获取的函数引用,那哪些属性可以被代理呢:

还有一个功能是观察属性,比如属性中的值发生了变化,可以获取到值得变化:

class Lazy{
    var name:String by Delegates.observable("name"){
        property, oldValue, newValue ->
        println("${property.name} has changed,before:${oldValue},now:${newValue}")
    }
}
fun main() {
    val lazy = Lazy();
    lazy.name = "Pan"
}

Delegates.observable有两个参数,第一个是初始值,第二个是个lambda表达式,执行的结果:

name has changed,before:name,now:Pan

代理属性的功能还远远不止这么些,还有map代理属性,本地代理属性等等功能,似乎这些功能在实际编码中用的不多,不做过多介绍了,有兴趣可以看看官网的介绍.

8.SAM

SAM这个东西有点类似于java的lambada定义的functional interface,定义如下:

fun interface Even{
    fun invoke(num:Int):Boolean
}
fun main() {
    var even:Even = Even { it % 2 == 0 }
    even.invoke(10)

}

其实功能和函数类型有点冲突,而且还得特地定义出一个接口出来,感觉用处不是很大.

9.Data class

data class类似于java14中的record class,专门用于承载数据,和普通的class相比有以下的几个区别:

申明一个data class,并创建实例

data class Person(val name: String) {
    var age: Int = 0
}
fun main() {
    val person = Person("Pantheon").apply { age = 10 }
    val copy = person.copy(name = "Lucy")
    println(copy)

}

data class的属性可以是在主构造中也可以是在body体内,输出结果

Person(name=Lucy)

10.sealed class

密封类的特性是这样,被sealed修饰的类或者接口,在最早的版本中,不能在该类申明的包以外被继承和实现,比如我引用了一个第三方类库的包中包含sealedclass,我想继承它,是不行的,所以才有了密封类的说法,它除了不让其他人实现外,还有一个比较实用的功能就是类似于java中enum的使用,但是比enum功能强大:

sealed class Week(val day: String) {

    class Monday : Week("monday");

    object Tuesday : Week("tuesday");

    object Thursday : Week("thursday") {
        override fun today() {
            println("hah...today is $day")
        }
    }

    class Friday : Week("friday") {
        fun friday() {
            println("saturday is coming.....")
        }
    }

    open fun today() {
        println("today is $day")
    }
}

fun main() {
    Week.Monday().today()
    Week.Tuesday.today()
    Week.Thursday.today()
    Week.Friday().friday()
}
-------------------
today is monday
today is tuesday
hah...today is thursday
saturday is coming.....

从上面代码可以看到,密封类里的对象和enum区别在于,enum对象永远只有一个,而密封类里面可以是class,也可以是object,是可以创建出多个对象,而继承密封类的实现,也可以自己定义的方法,通过 Week.Friday()方法调用返回的类型是Friday类型,而不是Week,所以可以调用friday()这个方法,这和enum是最大的区别,因为enum获取的实例就是enum定义出来的类型.

密封类还可以和whenis配合使用,接近于java中对enumSwitch case判断:

fun whatDay(week: Week) = when(week){
    is Week.Monday -> println("Monday")
    is Week.Friday -> println("Friday")
    Week.Thursday -> println("Thursday")
    Week.Tuesday -> println("Tuesday")
}

fun main() {
    whatDay(Week.Thursday)
}

11.泛型(in,out,where)

要说kotlin的泛型,不得不先说java的泛型,java泛型因为在运行的时候会丢失泛型,所以为了防止发生类型安全的问题,java的泛型是不允许协变的

List<String> lists = new ArrayList<>();
//编译错误 泛型不能协变
List<Object> objects = lists;

假如我们就是有需要这样的赋值的操作呢?这个时候就要用到两个关键字,一个extends,一个super

  List<? super Object> lists = new ArrayList<>();
  //编译通过
  List<Object> objects = lists;

java里面把对泛型变量的操作划分成了两块,一个是读操作,一个是写操作,使用extends的泛型,只能读,不能写,而super关键字只能写,不能读

 List<? extends Object> objects = new ArrayList<>();
 //编译报错,extend只能用在读的场景
 objects.add("1111");
 //编译正确
 Object o = objects.get(0);
List<? super Fruit> objects = new ArrayList<>();
objects.add(new Apple());
//error,类型只能是object类型
Fruit object = objects.get(0);

PECS原则

如果要从集合中读取类型T的数据,并且不能写入,可以使用 ? extends 通配符;(Producer Extends)

如果要从集合中写入类型T的数据,并且不需要读取,可以使用 ? super 通配符;(Consumer Super)

这两个关键字就刚刚对应kotlin里面的in,out关键字,in代表添加数据,也就是对应的是super关键字,而out则代表获取数据,对应的是extend关键字

open class Fruit;

class Apple:Fruit();

var list:ArrayList<in Fruit> = ArrayList()
//error
val fruit:Fruit = list.get(0)
//ok
list.add(Apple())
var list:ArrayList<out Fruit> = ArrayList()
//ok
val fruit:Fruit = list.get(0)
//error
list.add(Apple())

当泛型类型有多个时,需要有到where关键字

fun <T> copyWhenGreater(list: List<T>, threshold: T): List<String>
    where T : CharSequence,
          T : Comparable<T> {
    return list.filter { it > threshold }.map { it.toString() }
}

12.object 表达式

kotlin中,如果对于一些只需要创建一次的对象,或者对于一些不需要复用的实现,可以用object关键字来申明和创建该对象,比如:

fun main() {

    var pantheon = object {
        var name:String = "Pantheon";

        fun sayHello(){
            println("hello,$name")
        }
    }

    pantheon.sayHello()

}

这个例子可以让我们看到object的背后运作机制,它不仅仅是创建了一个匿名类,而且还用这个匿名类直接给我们创建出来了一个对象.

object也可以去实现某些特定的接口:

 var pantheon = object:ObjectInterface {
        var name:String = "Pantheon";

        fun sayHello(){
            println("hello,$name")
        }

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

    pantheon.sayHello()
    pantheon.doSth()

13. companion object

kotlin中没有static关键字,class的static 方法和static属性由companion object来提供,比如Java中

public class StaticClass {

    public static final String name = "Pantheon";

    public static void sayHello(){
        System.out.println("Hello "+name);
    }
}

它的写法在kotlin中就等价于:

class StaticClass{
    companion object{
      val name = "Pantheon"
        fun sayHello(){
            println("Hello,$name")
        }
    }
}

之前在拓展函数中也讲过,类的静态拓展方法是需要加在companion object,在receiver申请处需要声明成class.companion.

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注