Koltin - 面向对象(上)

Kotlin支持面向对象的三大特征:继承、多态、封装。提供了private、protected、internal、public四个访问权限修饰符。

1、Kotlin定义类,语法:

[修饰符] class 类名 [constructor 主构造器]{

零到多个次构造器

零到多个属性

零到多个方法

}

修饰符:可以是public|internal|private(只能出现其中之一)、final|open|abstract(也只能出现其中之一)或者完全省略修饰符。

Kotlin类的定义:类名、类头(泛型声明、主构造器等)和用花括号包围的类体构成。类头和类体都是可选的【空类没有类体,可以省略花括号】,比如class EmptyClass。

2、一个Kotlin类有0~1个主构造器、0~N个次构造器。主构造器是类头的一部分,它跟在类名和泛型声明之后:

class User constructor(name:String){}//如果主构造器没有任何注解或修饰符,则可以省略constructor关键字。

主构造器是在类头使用constructor关键字定义一个无执行体的构造器。虽然主构造器不能定义执行体,但是可以定义多个形参,这些形参可以在属性声明、初始化块中使用。

关于默认构造器:如果没有为非抽象类定义任何的主/次构造器,系统会自动提供一个无参数的主构造器,该构造器默认用public修饰。一旦程序员为一个类提供了构造器,系统将不再为该类提供构造器。

3、类的属性

[修饰符] var|val 属性名:类型 [= 默认值]

[<getter>]

[<setter>]

修饰符:可以省略,也可以是private|protected|internal|public、final|open|abstract。

public|protected|private只能出现其中之一,final|open|abstract也只能出现其中之一。

对于属性使用protected|internal|public修饰符和不使用访问控制符,效果是一样的。如果使用private修饰该属性,该属性将作为幕后属性使用。

[<getter>]|[<setter>]:用于为该属性编写自定义的getter、setter方法。如果不指定,kotlin会为读写属性提供默认getter、setter方法,为只读属性提供默认的getter方法。

4、顶层函数与类中方法的区别

主要区别在于修饰符:顶层函数不能使用protected、abstract、final修饰符。但类中的方法可以使用private|protected|internal|public、final|open|abstract这些修饰符。

5、构造器的语法:

[修饰符] constructor (形参列表){

//零到多条可执行语句组成的构造器执行体

}

修饰符:可以省略,也可以是private|protected|internal|public其中之一

形参列表:其格式和定义方法的形参列表的格式完全相同

6、方法与函数的关系

Kotlin中的方法与函数其实是统一的,不仅定义函数和方法的语法相同,而且定义在类中的方法依然可独立出来。也就是说,即使我们将方法定义在类里面,这个方法依然可以转换为函数。

  class Person {

        var age: Int = 0

        constructor() {

            this.age = 10

        }

        fun showAge() {

            println(age)

        }

    }

    fun main(args: Array<String>) {

        var show = Person::showAge//将showAge方法独立成函数,调用showAge方法的调用者将作为第一个参数传入

        var p = Person()

        show(p)

    }

7、解构与componentN方法

Kotlin允许将一个对象的N个属性“解构”给多个变量,写法如下:

var(name,pass) = user

Kotlin是怎么知道把哪两个属性赋值给name、pass变量的?其实Kotlin会将上面的赋值代码转换为如下两行:

var name = user.component1()

var pass = user.component2()

如果希望将对象解构给多个变量,那么必须为该对象的类定义componentN方法,程序希望解构多少个变量就定义多少个componentN方法,且该方法使用operator修饰。   

 class User(name: String, pass: String, age: Int) {

        var name = name

        var age = age

        var pass = pass

        operator fun component1(): String {

            return this.name

        }

        operator fun component2(): String {

            return this.pass

        }

        operator fun component3(): Int {

            return this.age

        }

    }

    fun main(args: Array<String>) {

        val user = User("tom", "123", 20)

        var (name, pass: kotlin.String) = user

    }

如果只希望解构后面两个,可以使用_作为占位符

var (_,pass,age) = user

还记得遍历Map时候的一个语法吗?

for((key,value) in map){

...}

其原理是一样的:

operator fun<k,v> Map.Entry<k,v>.component1() = getKey()

operator fun<k,v> Map.Entry<k,v>.component2() = getValue()

即  var (key,value = Map.Entry对象)

Lambda表达式中解构:如果Lambda表达式的参数是支持解构的类型(比如Map.Entry),即可通过将它们放在括号中引入多个新参数来代替单个参数,例如如下的两种写法是一样的:

map.mapValues(entry->"${entry.value}")

使用解构:

map.mapValues((key,value)->"${value}")

注意:Lambda表达式的多个参数是不需要使用圆括号的,只要看到在Lambda表达式的形参列表中出现圆括号,那就是使用了解构。

8、数据类

数据类专门用于封装数据。

数据类除了使用data修饰外,还要满足:

  • 主构造器至少需要一个参数

  • 主构造器的所有参数需要有val或var声明为属性

  • 不能用abstract、open、sealed修饰,也不能成为内部类

  • 在kotlin1.1之前,数据类只能实现接口,现在数据类也可以继承其他类了

data class person(var name: String, var pass: String, val age: Int)

定义数据类之后,系统会自动为我们生成如下方法:

  • equals/hashcode

  • toString

  • componentN

  • copy方法,完成对象的复制

由于数据类支持解构,因此可以完成函数中一次返回多个值。

9、属性和字段

Kotlin的属性相当于Java字段(field - 幕后字段)再加上getter和setter(只读的属性没有setter方法),而且开发者不需要自己实现getter和setter方法。

Kotlin使用val定义只读属性,使用var定义读写属性,系统会为只读属性生成getter方法,会为读写属性生成getter和setter方法。

注意:在定义Kotlin的普通属性时,需要程序员显式的指定初始值:要么在定义时指定初始值,要么在构造器中指定初始值。

在Kotlin中定义一个属性,就相当于定义了一个Java类private修饰的字段,以及public final 修饰的getter和setter方法。

需要指出的是,虽然Kotlin为属性生成了getter和setter方法,但由于源程序中并未真正定义这些getter和setter方法,因此Kotlin程序不允许直接调用getter和setter方法。但是如果是Java程序,由于各属性对应的field都用了private修饰,因此只能用getter和setter方法访问属性。简单的概括就是:Kotlin访问只能使用点语法访问属性,而Java程序则只能使用getter和setter方法访问属性。

10、自定义getter(get(){})和setter(set(value){})方法

可以在get(){} 、set(value){}中添加自己的逻辑;

11、幕后字段

在Kotlin中定义一个普通属性时,Kotlin就会为该属性生成一个field、getter和setter方法。kotlin为该属性所生成的field就被成为幕后字段。

注意:如果类的属性有幕后字段,则必须进行显式的初始化;如果类的属性没有幕后字段,则Kotlin不允许为该属性指定初始值。

生成幕后字段的条件:

  • 对于只读属性,必须重写setter方法。对于读写属性,必须重写getter和setter方法。否则,就总会生成幕后字段;

  • 重写getter、setter方法时,使用field显式引用了幕后字段,则也会生成幕后字段;

12、幕后属性

用private修饰的属性就是幕后属性,Kotlin不会为幕后属性生成任何的getter和setter方法。因此,程序不能直接访问幕后属性,必须由开发者为幕后属性提供getter和setter方法

class Person(var name: String) {

        private var _name: String = name

        var name1

        get() = _name

        set(value) {

            _name = value

        }

}

_name 属性是幕后属性,没有getter和setter方法,因此程序无法直接访问Person对象的_name属性。

接下来程序定义了一个name1属性,并重写了name1属性的getter和setter方法,重写的getter和setter方法实际上访问的是_name幕后属性。

13、延迟初始化属性

Kotlin要求所有属性必须由程序员显式初始化 - 要么在定义该属性的时候赋初始值,要么在构造器中对该属性进行赋初始值。

Kotlin提供了lateinit修饰符来解决属性的延迟初始化。使用lateinit修饰的属性,可以在定义该属性时和在构造器中不指定初始值。

对lateinit修饰符有以下限制:

  • 只能修饰可变属性(var)

  • lateinit修饰的属性不能自定义getter和setter方法

  • 必须是非空类型

  • 不能是原生类型(即Java的8种基本数据类型)

与Java不同的是,Kotlin不会为属性执行默认初始化。

14、内联属性 - 与内联方法一个意思,不过此处修饰的是属性或属性的getter或setter方法

15、访问控制符

private / public / internal /protected(类或文件内部、子类,可访问)

与Java的区别有如下的几点:

  • kotlin取消了Java默认的访问权限(包访问权限),引入了internal访问控制符(模块访问权限)

  • Kotlin取消了protected的包访问权限

  • Kotlin的默认访问控制符是public

何为模块?模块是编译在一起的一套Kotlin文件,模块的存在形式可以是一个项目,也可以是一次kotlinc执行所编译的一套文件。

不同作用域的成员可支持的访问控制符

==》顶层成员:顶层类、接口、函数、属性

  • public 或不加修饰符,可在任意地方被访问;

  • 使用private修饰,只能在当前文件中被访问;

  • 使用internal修饰,只能在当前文件或当前模块中被访问;

注意:直接定义在包内的顶层函数、属性,会转换成Kotlin所生成的类(类名为文件名 + Kt后缀的类)中的静态方法和静态属性。

package lee;

private fun foo(){}

public var bar:Int = 5

internal val baz:Int = 6

==》位于类、接口之内的成员

能使用:private / public / internal /protected其中之一

不加修饰符,默认为public

注意:类的主构造器比较特殊,如果需要为主构造器指定访问权限修饰符,则一定要使用constructor关键字,并在该关键字前面添加private / public / internal /protected其中之一。如果不添加修饰符,默认为public。

==》局部声明

与Java相同,局部声明(局部变量、局部方法、局部嵌套类)的作用域仅在该方法(或函数)内有效,因此访问控制符是没有任何意义的,因此不能使用private / public / internal /protected修饰符。

16、主构造器和初始化块

Kotlin类可以定义0~1个主构造器和0~N个次构造器。如果主构造器没有任何注解或可见性修饰符,则可以省略constructor关键字。

主构造器可以声明形参,但自己没有执行体,主构造器的形参有什么用呢?

  • 初始化块可以使用主构造器定义的形参;

  • 在声明属性时可以使用主构造器定义的形参;

初始化块:

init{

//初始化块中的可执行代码,可以使用主构造器定义的参数

}

class Person(name:String){

var name:String

init{

this,name = name//属性的初始值也可以通过初始化块指定

println(name)

}}

Person("Tom")结果打印了Tom

==>两者的关系:初始化块是在创建对象时隐式执行的,程序调用主构造器创建对象,实际上就是执行初始化块。由此可见,主构造器的主要作用是为初始化块定义参数,因此主构造器更像是初始化块的一部分。也可以说是,初始化块就是主构造器的执行体。

17、次构造器和构造器重载

Kotlin允许使用constructor关键字定义N个次构造器,次构造器类似传统的Java构造器。注:Java的初始化块其实是假象,其会自动插入到每个构造器的前面执行。

注意:Kotlin要求所有的次构造器必须先调用主构造器,然后才执行次构造器的代码

同一个类中具有多个构造器,多个构造器的形参列表不同,则为构造器重载。如果多个构造器中有相同的代码,则可以把它们放到初始化块中定义。如果这些初始化块需要参数,则可将参数放在主构造器中定义。这样可以提高复用性。

示例:

class ConstructorOverload{

var name:String?

var count:Int

init{

println("初始化块")

}

constructor(){

name = null

count = 0

}

constructor(name:String?,count:Int){

this.name = name

this.count = count

}
}

ConstructorOverload()

ConstructorOverload("tom",5)

==》结果:

初始化块

初始化块

初始化块总会在所有的次构造器之前执行,即次构造器都要委托调用初始化块

上面的类没有定义主构造器,因此次构造器不需要委托主构造器

看如下示例:

class User(name:String){

var name:String?

var age:Int

var info:String?

init{

println("User的初始化块")

this.name = name

this.age = 0。3

}

constructor(name:String,age:Int):this(name){

this.age = age

}

constructor(name:String,age:Int,info:String):this(name,age){

this.info=info

}
}

Kotlin使用“:this(参数)”的语法委托另一个构造器。

18、主构造器声明属性

Kotlin允许在主构造器上声明属性,直接在参数之前使用var 或val即可声明属性。当程序调用这种方式声明的主构造器创建对象时,传给该构造器的参数将会赋值给对象的属性

class Item(val code:String,var price:Double){

}

var im = Item("123",3.7)

println(im.code)//123

println(im.price)//3.7


子类主构造器必须委托调用父类的主构造器;

子类的次构造器必须先调用主构造器,然后才执行次构造器中的代码;

19、类的继承(单继承,不支持多继承)

修饰符 class SubClass :SuperClass{

//类定义部分

}

如果不写SuperClass,则默认继承Any类,Any类是所有类的父类,要么是其直接父类,要么是其间接父类。

注意:

Any类不是Java中的Object类,Any类只有equals、hashcode、toString这三个方法。

Kotlin的类,默认就有final修饰,因此Kotlin的类默认是不能派生子类的。为了让一个类能够派生子类,需要使用open修饰该类。

19.1、Kotlin与Java的设计相同:所有子类构造器必须调用父构造器一次。

  • 子类的主构造器

如果子类定义了主构造器,由于主构造器属于类头部分,为了让主构造器能调用父类构造器,因此主构造器必须在继承父构造器的同时委托调用父类构造器。

open class BaseClass{

var name:String

constructor(name:String){

this.name = name

}}

class SubClass :BaseClass("foo"){}

或

class SubClass(name:String) :BaseClass(name){}
  • 子类的次构造器

次构造器同样需要委托调用父类构造器。

如果子类定义了主构造器,由于子类的次构造器总会委托调用子类的主构造器(直接或间接),而主构造器一定会委托调用父类构造器,因此子类的所有次构造器最终也调用了父类构造器。

如果子类没有定义主构造器,则此时次构造器委托调用父类构造器可以分为三种方式

1)子类构造器显式使用:this(参数)显式调用本类中重载的构造器。调用本类中的另一个构造器最终还是要调用父类构造器;

2)子类构造器显式使用:super(参数)委托调用父类构造器;

3)子类既没有:this也没有:super,系统将会在执行子类构造器之前,隐式调用父类的无参构造器;

open class Base{

constructor(){

}

constructor(name:String){

}}

class Sub : Base{

contructor(){//3

}

contructor(name:String):super(name){//2

}

contructor(name:String):this(name){//1

}}

当调用子类构造器来初始化子类对象时,父类构造器总会在子类构造器之前执行;不仅如此,在执行父类构造器的时候,系统会再次上溯执行其父类构造器.....以此类推,创建任何Kotlin对象,最先执行的总是Any类的构造器。

19.2、重写父类的方法/属性

Kotlin默认为所有的方法添加final修饰符,阻止该方法被重写,添加open关键字用于阻止Kotlin自动添加final修饰符。

open class Bird{

open fun fly(){

}} 

class Ostrich:Bird(){

override fun fly(){

}}

方法重写遵从“两同两小一大”

  • 两同:函数名、参数列表相同;

  • 两小:子类方法的返回值类型应比父类方法的返回值类型更小或相等,子类方法声明抛出的异常类应比父类声明抛出的异常类更小或相等;

  • 一大:子类方法的访问权限要比父类方法的访问权限更大或相等;

重写父类的属性与重写父类的方法大致相似:父类被重写的属性必须使用open修饰,子类重写的属性必须使用override修饰。

重写属性有如下限制:

  • 重写的子类属性的类型要与父类属性的类型要兼容;

  • 访问权限方面:子类属性的访问权限要比父类属性的访问权限要大或相等;

  • 只读属性可以被读写属性重写,但是读写属性不能被只读属性重写;

open class item{

open proteced var price =10.9

open val name:String =""

open var count:Int= 0

}

class SubItem:Item{

override public var price =10.9 // 正确,上述第二条

override var name:String =""//正确l上述第三条

override val count:Int= 0 //错误,上述第三条

}

注意:接口中的方法默认有open修饰符。

20、多态

Kotlin的变量也有两种类型:编译时类型和运行时类型。如果编译时类型和运行时类型不一致就出现了多态。

编译时类型由声明该变量时使用的类型决定,运行时类型由实际赋给该变量的对象决定。

open class BaseClass{

open fun test(){}

}

class SubClass:BaseClass(){

override fun test(){}

fun sub(){}

}

var c:BaseClass = SubClass()

c.test()//正确,访问子类

c.sub()//编译时错误,虽然c引用的对象是SubClass类型,也包含了sub()方法,但是c的编译时类型是Base Class,因此在编译时是无法调用sub()方法的。

21、is类型检查

is:判断前面的变量是否引用了后面的类,或者是其子类、实现类的实例。

is运算符前面的操作数的编译时类型要么与后面的类相同,要么与后面的类具有父子继承关系,否则编译时程序就会报错。

is运算符在进行强制类型转换之前,首先判断前一个变量是否引用后一个类或者其子类的实例,是否可以转换成功。

var hello:Any = "Hello"

println(hello is String)//true,hello变量的实际类型是String

println(hello is Date)//false,Any和Date存在继承关系

var a:String="hello"

println(a is Date)//报错,因此String与Date不存在继承关系

Kotlin的is运算符很智能,只要程序使用了is进行了判断,系统就会自动将变量的类型转换为目标类型。

 var a:Any  ="fkit"

println(a.length)//错误,因为a的编译时是Any类型,没有length属性

if(a is String) println(a.length)//正确

猜你喜欢

转载自blog.csdn.net/json_it/article/details/81840090