Kotlin——面向对象
Kotlin面向对象
类和对象
Kotlin提供了定义类、属性、方法等最基本的功能。类可被认为是一种自定义的数据类型,可以使用类来定义变量,所有使用类定义的变量都是引用变量,它们将会引用类的对象。也就是说 所有类都是引用类型
定义类
类被称为某一批对象的抽象,而对象才是一个具体存在的实体
定义类的简单语法:
[修饰符]class 类名[constructor 主构造器]{
零个到多个次构造器定义..
零个到多个属性..
零个到多个方法..
}
- 修饰符可以是public|internal|private(只能出现一个)、final|open|obstract(只能出现一个),或者完全省略修饰符
- kotlin的类定义由类名、类头和用花括号包围的类体构成。类头和类体都是可选的。
- 对于一个类定义而言,可以包含三种最常见的成员:构造器、属性和方法,这三种成员都可以定义零个或多个,如果三种成员都只定义了零个,就是定义了一个空类。空类没有类体,可以省略花括号。
class Empty
- 构造器是一个类创建对象的根本途径,如果一个类没有构造器,这个类通常无法创建对象。
- 一个Kotlin类可以有0~ 1个主构造器和0~N个次构造器,主构造器属于类头的一部分,跟在类名(和泛型声明)后
class User constructor(firstName:String){
}
上述代码就是类定义主构造器,主构造器就是在类头使用Construstor关键字定义一个无执行体的构造器,虽然主构造器不能定义执行体,但可以定义多个形参,这些形参可以在属性声明、初始化块中使用
- 如果主构造器没有任何注解或修饰符,可省略Constructor关键字
class User(firstName:)
Kotlin中,如果没有为非抽象类定义任何主或次构造器,系统会自动提供一个无参数的主构造器,该构造器默认使用public修饰,如果为类提供了构造器,系统将不再为该类提供默认构造器
定义属性的语法格式如下
[修饰符]var|val 属性名:类型=[默认值]
- 修饰符:修饰符可以省略,也可以是 public|protected|internal|private、final|open|abstuct
- var|val:使用var声明读写属性,val声明只读属性
- 属性名 遵从合法标识即可
- 类型:类型可以是Kotlin允许的任何数据类型。如果可以从初始值或getter方法的返回值推断出属性类型,可省略属性类型
- 默认值:可以在定义时指定初始值,或者在初始化块或构造器中指定初始值
- getter、setter:用于为该属性编写自定义的getter、setter方法。如果不指定,Kotlin会为读写属性提供默认的getter、setter方法,为只读属性提供默认的getter方法
定义构造器的语法格式如下:、
[修饰符]constructor (形参列表){
由零条到多条可执行语句组成的构造器执行体
}
- 修饰符:修饰符可以省略,也可以是public、protected、internal、private其中之一
- 形参列表:其格式和定义方法的形参列表的格式完全相同
class Person {
//定义两个属性
var name:String = ""
var age:Int = 0
//定义方法
fun say(content: String) {
println(content)
}
}
对象的产生和使用
调用某个类的构造器即可创建这个类的对象,无须使用new关键字
var p:Person = Person()
如果访问权限允许,那么在类中定义的方法和属性都可以通过对象来调用。
对象.属性|方法(参数)。
p.name = "张三"
p.age = 22
p.say(p.name+p.age)
对象的this引用
kotlin提供this关键字,this关键字总是指向调用该方法的对象。根据this出现位置的不同,this作为对象的默认引用有两种情形
- 在构造器中:引用该构造器正在初始化的对象
- 在方法中:引用调用该方法的对象
this关键字最大的作用就是让类中的一个方法访问该类的另一个方法或属性。
class ThisInConstructor{
var foo: Int
constructor(){
val foo = 0;
this.foo = 22
}
}
与普通方法类似,大部分时候,在构造器中访问其他属性和方法时都可以省略this前缀,但如果构造器中有一个与属性同名的局部变量,又必须在构造器中访问这个被覆盖的属性,则必须使用this前缀
当this作为对象的默认引用使用时,程序可以像访问普通变量一样来访问这个this引用,甚至可以把this当作普通方法的返回值
方法详解
方法是类或对象的行为特征的抽象。
方法与函数的关系
Kotlin的方法与函数是统一的,定义函数和方法的语法相同,而且定义在类中的方法可独立出来。
class Dog{
fun run(){
println("run方法")
}
fun eat(food: String) {
println("正在吃${food}")
}
}
fun main() {
//定义一个函数类型变量
var rn:(Dog)->Unit = Dog::run
val d = Dog()
rn(d)
var et = Dog::eat
et(d,"骨头")
中缀表示法
Kotlin的方法还可以使用Infix修饰,这样该方法就可通过中缀表示法调用。就像这些方法是双目运算符一样。infix方法只能有一个参数。
class Apple(weight: Double){
var weight = weight
infix fun add(other: Apple):Apple {
return Apple(weight+other.weight)
}
infix fun drop(other: Apple): Apple {
return Apple(weight-other.weight)
}
}
fun main() {
var apple1 = Apple(3.0)
var apple2 = Apple(2.1)
//不使用infix修饰调用
println(apple1.add(apple2).weight)
println(apple1.drop(apple2).weight)
//使用infix修饰调用
println((apple1 add apple2).weight)
println((apple1 drop apple2).weight)
componentN方法与解构
Kotlin允许将一个对象的N个属性 解构给多个变量
var(name.pass)=user
上面这行代码相当于将user对象的两个属性分别赋值给name、pass两个变量,这两个变量的类型会根据user对象的属性类型来判断
实际上kotlin会将上面的赋值代码转换为:
var name = user.componet1()
var pass = user.component2()
如果希望将对象解构给多个变量,那么必须为该对象的类定义componentN()方法。程序希望将对象解构给几个变量,就需要为该类定义几个componentN()方法,并且该方法需要使用operator修饰
class User(name: String,pass:String,age:Int){
var name = name
var pass = pass
var age = age
operator fun component1():String{
return this.name
}
operator fun component2():String{
return this.pass
}
operator fun component3():Int{
return this.age
}
}
fun main() {
val user= User("a","b",1)
//将user对象解构给2个变量
val (name,pass:String) = user
println(name)
println(pass)
//将user对象解构给3个变量
val (name2,pass2,age2) = user
println(name2)
println(pass2)
println(age2)
在某些时候,程序希望解构对象后面几个componentN()方法的返回值、忽略前面几个componentN()方法的返回值,此时可通过下划线(_)来占位
var(_,pass3,age3) = user
println(pass3)
println(age3)
数据类和返回多个值的函数
Kotlin本身并不支持定义返回多个值的函数或方法,但我们可以让Kotlin函数返回多个值——让Kotlin返回一个支持解构的对象
Kotlin提供了一种特殊的类:数据类,数据类专门用于封装数据
数据类需要使用data修饰,还需要满足:
- 主构造器至少需要有一个参数
- 主构造器的所有参数需要用val、var声明为属性
- 数据类不能用abstract、open、sealed修饰,也不能定义成内部类
定义数据类后,系统会自动为数据类生成如下内容: - 生成equals()/hashCode()方法
- 自动重写toString()方法
- 为每个属性自动生成operator修饰的componentN()方法
- 生成copy()方法,用于完成对象复制
fun factorial(n: Int): Result {
if (n==1) {
return Result(1,"成功")
}else if(n>1){
return Result(2,"大于1")
}else{
return Result(-1,"小于1")
}
}
fun main() {
//通过解构获取函数的两个返回值
var(rt,status) = factorial(6)
println(rt)
println(status)
Kotlin标准库中提供了Pair和Triple两个数据类,Pair数据类可包含两个任意类型的属性,Triple可包含三个任意类型的属性,方便直接使用
在Lambda表达式中解构
Kotlin允许对Lambda表达式使用解构,如果Lambda表达式的参数是支持解构的类型(Pair或Map.Entry等),它们都具有operator修饰的componentN()方法,那么即可通过将它们放在括号中引入多个新参数来代替单个参数
{a -> ...} 一个参数
{a,b->...}两个参数
{(a,b) ->...}一个解构对
{(a,b),c->....}一个解构对和第三个参数
属性和字段
Kotlin的属性相当于Java的成员变量。
读写属性和只读属性
Kotlin使用val定义只读属性,使用var定义读写属性。系统会为val属性生成getter方法,会为var属性生成getter和setter方法
在定义普通属性时,要么在定义时指定初始值,要么在构造器中指定初始值
自定义getter和setter
自定义getter和setter方法中可以假如自己的控制逻辑,其中getter是一个形如get(){}:无参数、带一个返回值的方法,setter是一个形如set(){}:带一个参数,无返回值的方法
class User(first: String, last: String){
var first:String = first
var last:String = last
val fullName:String
//自定义get方法
get() {
return "${first}${last}"
}
}
如果是一个读写属性,那么程序既可以重写它的getter方法,也可以重写它的setter方法
幕后字段
在Kotlin中定义一个普通属性时,Kotlin会为该属性生成一个field、getter、setter方法。Kotlin为该属性所生成的field就被称为幕后字段
- 如果Kotlin类的属性有幕后字段,则Kotlin要求为该属性显式指定初始值
- 如果Kotlin类的属性没有幕后字段,则Kotlin不允许为该属性指定初始值
那么Kotlin何时会为属性生成幕后字段呢?
- 该属性使用Kotlin自动生成的getter和setter方法或其中之一,对于只读属性,必须重写getter方法,对于读写属性,必须重写getter、setter方法,否则总会为该属性生成幕后字段
- 重写getter、setter方法时,使用field关键字显式引用了幕后字段
Kotlin允许开发者在getter或setter方法中通过field关键字引用系统自动生成的字段。
class Person(name: String, age: Int) {
var name = name
set(newName) {
if (newName.length > 6 || newName.length < 2) {
print("输入长度不合法")
}else{
field =newName
}
}
var age = age
set(newAge) {
if (newAge>200||newAge<0) {
println("输入的年龄不合法")
}else{
field = newAge
}
}
}
fun main() {
var p = Person("wx",22)
//属性赋值
p.age = 2000
p.age = 100
println(p.age)
注意:
当程序重写getter或setter方法时,不能通过点语法来对name、age赋值,假如在name的setter方法中使用点语法对name赋值,由于点语法本质时调用setter方法,这样就会形成无限递归。所以在getter、setter方法中需要通过field关键字引用幕后字段进行赋值
幕后属性
在个别情况下,开发者希望自己定义field,并为该field提供setter、getter方法。
幕后属性就是用Private修饰的属性
- Kotlin不会为幕后属性生成任何getter、setter方法。因此不能直接访问幕后属性,必须由开发者为幕后属性提供getter、setter方法
class BackingProperty(name: String){
private var _name = name
var name
get() = _name
set(newName) {
if (newName.length > 6 || newName.length < 2) {
print("输入长度不合法")
}else{
_name =newName
}
}
}
延迟初始化属性
Kotlin提供了lateinit修饰符来实现属性的延迟初始化,使用lateinit修饰的属性,可以在定义该属性时和在构造器中都不指定初始值
对lateinit修饰符的使用有以下限制
- lateinit 只能修饰在类体中声明的可变属性(使用val声明的属性不行,在主构造器中声明的属性也不行)
- lateinit修饰的属性不能有自定义getter和setter方法
- lateinit修饰的属性必须是非空类型
- lateinit修饰的属性不能是原生类型(即java的8种基本类型对应的类型)
class lateUser{
lateinit var name:String
lateinit var age:Date
}
fun main() {
var l = lateUser()
l.name = "asda"
l.age = Date()
内联属性
inline修饰符可修饰没有幕后字段的属性的getter或setter方法,既可单独修饰属性的getter或setter方法,也可修饰属性本身
对于使用inline修饰的getter、setter方法,就像前面介绍的内联函数一样,程序在调用getter和setter方法时也会执行内联化
使用inline修饰必须是没有幕后字段的属性
隐藏和封装
封装:面向对象的三大特征之一,指的是将对象的状态信息隐藏在对象的内部,不允许外部程序直接访问对象内部的信息,而是通过该类所提供的方法来实现对内部信息的操作和访问
包和导包
Kotlin的包和Java的包相同,既是逻辑上的一个程序单元,也是一个命名空间。如果希望把函数、类放在指定的包下,需要在源程序的第一个非注释行放置如下代码
package packagename
为了使用其他包中的函数和类,Kotlin同样使用import执行导入
Kotlin的import语法和Java类似,同样支持精确导入和通配符导入
- 精确导入
import foo.Bar
- 通配符导入
import foo.*
Kotlin的import语句支持as关键字,可以为导入的类指定别名
Kotlin的默认导入
Kotin默认会导入如下包
- kotlin.*
- kotlin.annotaiton.*
- kotlin.collections.*
- kotlin.comparisons.*
- kotlin.io.*
- kotlin.ranges.*
- kotlin.sequences.*
- kotlin.text.*
对于jvm平台 还会自动导入 - java.lang.*
- kotlin.jvm.*
对于javaScript平台,则额外导入 - kotlin.js.*
访问控制修饰符
Kotlin提供了4个访问控制修饰符:private、internal、protected和public
- private:与java的private类似,只能在该类的文件内部访问
- internal:internal成员可以在该类的内部或文件的内部或者同一个模块内被访问
- protected:protected成员可以在该类的内部或文件的内部或其子类中被访问
- public:public成员可以在任意地方被访问
Kotlin没有显式指定修饰符的话,默认访问控制修饰符是public
Kotlin不同作用域中的成员可支持的访问控制符
- 位于包内的顶层成员
位于包内的顶层成员包括:顶层的类、接口、函数、属性只能使用private、internal和public其中之一,不能使用Protected修饰符
- private修饰:只能在当前文件中访问
- internal修饰:只能在当前文件或当前模块中被访问
- 默认public修饰:可以在任意地方被访问
- 位于类、接口之内的成员
对于位于类、接口之内的成员(顶层类、接口、函数、属性)能使用Private、internal、protected和public之一
- private修饰:只能在当前类中访问
- internal修饰:只能在该类或当前模块中访问
- protected:在该类或该类的子类中被访问
- 默认的public修饰:可以在任意地方被访问
- 类的主构造器
类的主构造器可以使用Privae、internal、protected和public其中之一,默认为Public
深入构造器
主构造器和初始化块
Kotlin的初始化块可以接收主构造器传入的参数
初始化块的语法:
init{
//初始化块中的可执行代码,可以使用主构造器定义的参数
...
}
初始化块中的代码可以使用主构造器定义的参数,也可以包含任何可执行语句,包括定义局部变量、调用其他对象的方法,以及使用分支、循环语句等
- Person.kt
class Person2(name: String) {
//定义初始化块
init {
var a = 6;
if (a > 4) {
println("Person2初始化块,a的数值为${a}")
}
println("Person2的初始化块,主构造器name的值为${name}")
}
}
fun main(args: Array<String>) {
Person2("孙行者")
}
输出:
Person2初始化块,a的数值为6
Person2的初始化块,主构造器name的值为孙行者
从上面的程序可以看出,当程序通过主构造器创建对象时,系统其实就是调用该类里定义的初始化块。主构造器的主要作用就是为初始化块定义参数。也可以说初始化块就是主构造器的执行体。
如果希望为对象的属性显式指定初始值,也可以通过初始化块来指定
如果程序员没有为Kotlin类提供任何构造器,则系统会为这个类提供一个无参数的主构造器,这个构造器的执行体为空。也就是说无论如何,Kotlin类至少包含一个构造器
通过初始化块对类进行自定义的初始化操作
class ConstructorTest(name:String,count:Int){
//定义类的属性
var name:String
var count:Int
//初始化块
init {
this.name = name
this.count = count
}
}
fun main(args: Array<String>) {
var con = ConstructorTest("测试",123)
//输出属性
println(con.count)
println(con.name)
}
一旦程序员提供了自定义的构造器,系统就不再提供默认的构造器
如果希望该类保留无参数的构造器或者希望有多个初始化过程,则可以为该类提供多个构造器
次构造器和构造器重载
Kotlin允许使用constructor关键字定义N个次构造器,次构造器类似于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
}
}
fun main(args: Array<String>) {
//通过无参数的构造器创建对象
var oc1 = ConstructorOverload()
//有参数的构造器创建对象2
var oc2 = ConstructorOverload("a",100)
//访问oc1的属性
println(oc1.name + oc1.count)
//访问oc2的属性
println(oc2.name + oc2.count)
}
主构造器声明属性
Kotlin允许在主构造器上声明属性,直接在参数之前使用var或val即可声明属性,var读写属性、val只读属性。当程序调用这种方式声明的主构造器创建对象时,传给该构造器的参数将会赋值给对象的属性
class Item(val code:String, var price:Double){}
fun main(args: Array<String>) {
var item = Item("cccc", 22.02)
println(item.code + item.price)
}
主构造器的所有参数为默认值,程序能以构造参数的默认值来调用该构造器,不需要为构造参数传入值
class Item(val code:String = "dasd", var price:Double = 11.00){}
fun main(args: Array<String>) {
var it = Item()
println(it.code + it.price)
}