Kotlin语法进阶

一、类

image

1.1 类声明

Kotin中使用关键字class声明类,且默认是public。如果一个类没有类体,可以省略花括号

class Invoice { /*……*/ }
class Test

关于类,有几个细节需要注意:

  1. Kotlin中的成员变量必须初始化
  2. 类默认不可以被继承,如需被继承,需要加上open关键字
  3. Kotlin中用:表示继承与实现,即同时表示Java中的extendsimplement
  4. Kotlin类中override变成关键字,且省略了protected关键字,即override函数可见性继承自父类

1.2 构造函数

//Java 格式
public class User {
    int id;
    public User(int id, String name) {
        this.id = id;
    }
}
//kotlin格式
class User {
    val id: Int
    constructor(id: Int) {
        this.id = id
    }
}

Koltin中定义一个构造函数,使用constructor关键字,默认为public。一般不会像前述方式实现,而是通过主构造器实现。

主构造函数

一个类可以有一个主构造函数以及一个或多个次构造函数。

主构造函数是类头的一部分:它跟在类名(与可选的类型参数)后。

class Person constructor(firstName: String) { /*……*/ }
//如果主构造函数没有任何注解或者可见性修饰符,可以省略这个 constructor 关键字,等价于
class Person(firstName: String) { /*……*/ }

//如果构造函数有注解或可见性修饰符,这个 constructor 关键字是必需的,并且这些修饰符在它前面
class Customer public @Inject constructor(name: String) { /*……*/ }

关于主构造函数,主要有两部分内容需要关注,即初始化代码块与属性声明

  • 初始化代码块

    主构造函数不能包含任何代码,所以初始化的代码放到init 关键字作为前缀的初始化块(initializer blocks)

    class Test constructor(num : Int){
        init {
            println("num = $num")
        }
    }
    
    var test = Test(1)    //类实例化
    

    init代码块执行顺序:在次构造函数之前,主构造函数之后执行

    init代码块可以写多个

  • 属性声明

    可以直接在主构造函数中简便声明属性

    class Test(val num1 : Int, var num2 : Long, val str : String){
        ...
    }
    //等价于
    class Test(num1 : Int, num2 : Long, str : String){
        val num1: Int = num1
        var num2: Long = num2
        val str: String = str
        ...
    }
    

次构造函数

Kotlin中支持二级构造函数。以constructor关键字作为前缀

class Person {
    var children: MutableList = mutableListOf();
  constructor(parent: Person) {
        parent.children.add(this)
  }
}

如果类有一个主构造函数,每个次构造函数需要委托给主构造函数,可以直接委托或者通过别的次构造函数间接委托。
委托到同一个类的另一个构造函数用 this 关键字

class User constructor(var name: String) {
    constructor(name: String, id: Int) : this(name) {
    }
    constructor(name: String, id: Int, age: Int) : this(name, id) {
    }
}

如果类主构造函数的所有参数都具有默认值,编译器将生成一个额外的无参数构造函数,它将使用默认值。

fun main(args: Array) {
    var test = Test()         //num1 = 10  num2 = 20
    var test1 = Test(1,2)     //num1 = 1   num2 = 2
    var test2 = Test(4,5,6)       //num1 = 4   num2 = 5   num1 = 4     num2 = 5    num3 = 6
}

class Test constructor(num1: Int = 10 , num2: Int = 20){
    init {
        println("num1 = $num1\t num2 = $num2")
    }

    constructor(num1 : Int = 1, num2 : Int = 2, num3 : Int = 3) : this(num1 , num2){
        println("num1 = $num1\t num2 = $num2 \t num3 = $num3")
    }
}

类实例化

创建一个类的实例,调用类的构造函数即可

val invoice = Invoice()

val customer = Customer("Joe Smith")

注意 Kotlin 并没有 new 关键字

二、属性与字段

2.1 属性声明

类中属性用关键字varval进行声明

class Address {
    var name: String = "Holmes, Sherlock"
    var street: String = "Baker"
    var city: String = "London"
    var state: String? = null
}

属性使用:直接用名称引用即可

 fun copyAddress(address: Address): Address {
      val result = Address() // Kotlin 中没有“new”关键字
      result.name = address.name // 将调用访问器
      result.street = address.street
      // ……
      return result
  }

2.2 属性访问器

在Java中,需要加上getter方法和setter方法才可以调用属性,而在Kotlin中可直接用名称引用,这时因为其隐含默认实现了gettersetter(读访问器与写访问器)

class User {
    var name = "Czh"
    var userName: String
        get() = name 
        set(value) {
            name = value
        }

    //用val只读标识只读
    val sex: String
        get() = "男"
}

这里有几个细节需要重点说明一下:

  1. val修饰的只读属性,不能有写访问器setter()
  2. 关键字getset分别对应gettersetter
  3. gettersetter自定义实现位于声明的变量下方
  4. setter函数的参数为value,且使用时需要设置幕后字段

简单来说,Kotlin中每个成员变量就是一个属性,每个属性中包含了(Field + Getter + Setter)

属性访问器可见性

可以根据实际情况修改属性访问器的可见性

var str1 = "kotlin_1"
      private set       //setter()访问器的私有化,且拥有默认实现

var str3 = "kotlin_3"
      private get       //编译出错,因为不能有getter()的访问器可见性

属性引用

属性与方法一样,可以获取其引用。分为类获取属性引用和实例获取属性引用

  • 类获取属性引用

    // 通过类对象获取该类的属性引用
    val ageRef = Person::age
    // 通过类名属性引用操作该属性时需要一个Receiver
    val person = Person(18, "Kotlin")
    // 通过属性引用调用get方法
    val age = ageRef.get(person)
    
  • 实例获取属性引用

    // 通过实例获取该类的属性引用
    val ageRef = person::age
    // 使用时无需Receiver
    ageRef.set(30)
    

其中获取类中属性引用时,其属性必须对获取方式可见。

2.3 幕后字段

Kotlin 的类不能直接声明域变量,如果属性需要,kotlin会自动提供。

在属性的取值方法或设值方法中, 使用 field 标识符可以引用这个backing field

var counter = 0 // 注意:这个初始器直接为后端域变量
    set(value) {
        if (value >= 0) field = value
    }

field 标识符只能在属性的访问器内使用

更好的理解backing field,需要知道Kotlin中,访问一个属性的实质是什么?

属性的读写,实质是执行了属性的gettersetter访问器

class Person {
    var name:String = "Paul"
      get() = "i am getter,name is Jake"
}

var person = Person()
val name = person.name
println("打印结果:$name")   //打印结果:i am getter,name is Jake
class Person {
    var name:String = "Paul"
        set(value) {
           println("执行了写访问器,参数为:$value") 
        }
}

var person = Person()
person.name = "hi,this is new value"    //执行了写访问器,参数为:hi,this is new value
println("打印结果:${person.name}")      //打印结果:Paul

注意:前述给属性赋值,执行了写访问器setter,但是没有属性赋值,所以还是返回默认值

如果按照Java的方法实现setter,则会直接报错

class Person {
var name = ""
  set(value) {
      this.name = value //报错
  }
}

上述代码会直接内存溢出错误,反编译为Java代码

/**java*/
public final class Person {
    @NotNull
    private String name = "Paul";

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

    public final void setName(@NotNull String value) {
        this.setName(value);    //setName递归调用
    }
}

若无field字段,在set方法中进行属性赋值会出现递归调用的情况。

Kotlin中,如果属性至少一个访问器使用默认实现,则会自动提供幕后字段,用关键字field表示,其只能在gettersetter内访问。

class Person {
    var name:String = ""
    get() = field 
    set(value) {
        field = value
    }
}
class Person {
    var name:String = ""
}

上述两个属性声明是等价的。

通过backing field可以帮助我们在属性访问器做一系列操作

class Person(var gender:Gender){
    var name:String = ""
      set(value) {
            field = when(gender){
              Gender.MALE -> "Jake.$value"
                Gender.FEMALE -> "Rose.$value"
          }
        }
}

enum class Gender{
    MALE,
    FEMALE
}

2.4 幕后属性

有时我们希望一个属性:对外表现为只读,对内表现为可读可写,我们将这种属性称为幕后属性

private var _table: Map? = null
public val table: Map
    get() {
        if (_table == null) {
            _table = HashMap() // 初始化
        }
        return _table ?: throw AssertionError("Set to null by another thread")
    }

_table属性声明为private,因此外部是不能访问的,内部可以访问,外部访问通过table属性,而table属性的值取决于_table,这里_table就是幕后属性。

三、可见性修饰符

  1. public:表示公有,系统默认使用此修饰符

  2. internal:表示模块 ,例如创建一个函数仅开放给module内部使用,不想对library的使用者可见,这是就应该用internal可见性修饰符

  3. protected:表示私有+子类。此修饰符不能用于顶层声明。

  4. private:表示私有

    Java中private表示类中可见,作为内部类时对外部类可见。

    Kotlin中private表示类中或所在文件内可见,作为内部类时对外部类不可见

    class Sample {
        private val propertyInClass = 1 // 仅 Sample 类中可见
    }
    
    private val propertyInFile = "A string." //整个文件可见
    
    class Outter {
        fun method() {
            val inner = Inner()
            
            val result = inner.number * 2 // 报错,对外部类不可见
        }
        class Inner {
            private val number = 1
        }
    }
    

四、继承类

4.1 定义

定义继承类的关键字为:open。其中类、成员,方法都要使用open关键字,不过对于abstract类默认具有open属性。

open class Demo{

    open var num = 3
    open fun foo() = "foo"
    open fun bar() = "bar"
}

class DemoTest : Demo(){
    // Kotlin使用继承是使用`:`符号
}

其中在 Kotlin 中,所有类都有一个共同的超类 Any,这对于没有超类型声明的类是默认超类

class Example // 从 Any 隐式继承

Any 有三个方法:equals()hashCode()toString()

修饰符 相应类的成员 注解
final 不能被覆写 在kotlin中默认所有的方法和类都是final属性
open 可以被覆写 需要被明确指出
abstract 必须要覆写 不能被实例化,默认具有open属性。
override 覆写超类的方法 如果没有被指定为final,则默认具有open属性

4.2 构造函数

主要分为实现类无主构造函数和有主构造函数。

实现类无主构造函数

这种情况,次构造函数必须使用super关键字初始化基类型或委托给另一个构造函数

class MyView : View{

    constructor(context: Context) : super(context)

    constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)

    constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int)
        : super(context, attrs, defStyleAttr)
}

存在主构造函数

这种情况,其基类必须用实现类主构造函数的参数就地初始化

class MyView(context: Context?, attrs: AttributeSet?, defStyleAttr: Int)
    : View(context, attrs, defStyleAttr) {

    constructor(context: Context?) : this(context,null,0)
    
    constructor(context: Context?,attrs: AttributeSet?) : this(context,attrs,0)
}

4.3 重写与重载

重写

当基类中的函数,没有用open修饰符修饰的时候,实现类中不得出现与基类相同函数名的函数

open class Demo{
    fun test(){}
}

class DemoTest : Demo(){
    fun test(){}   //编辑器直接报红报错
    override fun test(){}   //同样报错
}

当一个基类去继承另外一个基类时,第二个基类不想去覆盖掉第一个基类的方法时,第二个基类的该方法使用final修饰符修饰。

open class A{
    open fun foo(){}
}

open class B : A(){
    // 这里使用final修饰符修饰该方法,禁止覆盖掉类A的foo()函数
    final override fun foo(){}
}

重载

Kotlin中函数重载与Java一致

open class Demo{
    open fun foo() = "foo"
}

class DemoTest : Demo(){

    fun foo(str: String) : String{
        return str
    }

    override fun foo(): String {
        return super.foo()
    }
}    

属性重写

在基类中声明的属性,可在其实现类中重写该属性。

  1. 该属性必须以override关键字修饰
  2. 重写前后属性类型保持一致
  3. 仅可重写属性的Getter
  4. 可以用var重写基类的val修饰的属性,反之不行
open class Demo{
    open var num = 3
}

class DemoTest : Demo(){
    override var num: Int = 10
}
open class Demo{
    open val valStr = "我是用val修饰的属性"
}

class DemoTest : Demo(){
    override var valStr: String = "abc"
        set(value){field = value}
}

//重写属性的时候不用get() = super.xxx,因为这样的话,不管你是否重新为该属性赋了新值,还是支持setter(),在使用的时候都调用的是基类中的属性值。
class DemoTest : Demo(){

    override var valStr: String = "abc"、
        get() = super.valStr
        set(value){field = value}
}

fun main(arge: Array>){
    println(DemoTest().valStr)  //我是用val修饰的属性

    val demo = DemoTest()
    demo.valStr = "1212121212"
    println(demo.valStr)    //我是用val修饰的属性
}

重写属性的时候不用get() = super.xxx,因为这样的话,不管你是否重新为该属性赋了新值,还是支持setter(),在使用的时候都调用的是基类中的属性值。

open class Demo{
    open val valStr = "我是用val修饰的属性"
}

class DemoTest : Demo(){
    override var valStr: String = "abc"、
        get() = super.valStr
        set(value){field = value}
}

println(DemoTest().valStr)  //我是用val修饰的属性
val demo = DemoTest()
demo.valStr = "1212121212"
println(demo.valStr)    //我是用val修饰的属性

4.4 初始化顺序

open class Base(val name: String) {

    //基类初始化块
    init { println("Initializing Base") }
    //基类属性初始化
    open val size: Int = 
    name.length.also { println("Initializing size in Base: $it") }
}
class Derived(
    name: String,
    val lastName: String
) : Base(name.capitalize().also { println("Argument for Base: $it") }) {

    //子类初始化块
    init { println("Initializing Derived") }

    //子类属性初始化
    override val size: Int =
    (super.size + lastName.length).also { println("Initializing size in Derived: $it") }
}

fun main() {
    val d = Derived("hello", "world")
}
Argument for Base: Hello        // 基类init代码块
Initializing Base
Initializing size in Base: 5    //基类属性初始化
Initializing Derived            //子类init代码块
Initializing size in Derived: 10    //子类属性初始化

可知继承类的执行顺序是:先基类的init代码块,然后是基类的属性初始化,接下来是子类的init代码块与属性的初始化。

五、接口与枚举

image

5.1 接口定义

使用关键字 interface 来定义接口,且一个类或对象可以实现一个或多个接口

interface MyInterface {
    fun bar()
    fun foo() {
      // 可选的方法体
    }
}

class Child : MyInterface {
    override fun bar() {
    // 方法体
    }
}

5.2 属性与方法

接口属性

接口属性主要有如下特点:

  1. 属性要么是抽象,要么提供访问器的实现
  2. 不可以有幕后字段
interface Demo3Interface{
    val num1: Int   //抽象的
    val num2 : Int  
}

class Demo3(override val num1: Int, override val num2: Int) : Demo3Interface{
    fun sum() : Int{
        return num1 + num2
    }
}

var demo = Demo3(1,2)
println(demo.sum()) // 3
interface Demo3Interface{

    // val num3: Int = 3  这种方式不提供,会直接报错的
    val num3: Int
    get() = 3

    val num4: Int
}

class Demo3(override val num1: Int, override val num2: Int) : Demo3Interface{

    // 提供访问器实现
    override val num3: Int
    get() = super.num3

    // 手动赋值
    override var num4: Int = 4

    fun result() : Int{
        return num3 + num4
    }
}

fun main(args: Array) {
    println(demo.result())      // 7

    // 在这里也可以改变接口属性的值
    demo.num4 = 10
    println(demo.result())  // 13
}

接口方法

接口方法主要有如下特点:

  1. 不带结构体的函数可以省略大括号
  2. 不用强制重写带结构体的函数,可直接调用
interface Demo2Interface{

    fun fun1()  //无参无返回
    fun fun2(num: Int)  //有参无返回
    fun fun3(num: Int) : Int    //有参有返回

    // 有结构体, 可以不重写
    fun fun4() : String{
        return "fun4"
    }

    fun fun5(){}    //等价fun1
}

class Demo2 : Demo2Interface{

    override fun fun1() {
        println("我是fun1()方法")
    }

    override fun fun2(num: Int) {
        println("我是fun2()方法,我的参数是$num")
    }

    override fun fun3(num: Int): Int {
        println("我是fun3()方法,我的参数是$num,并且返回一个Int类型的值")
        return num + 3
    }

    override fun fun4(): String {
        println("我是fun4()方法,并且返回一个String类型的值")
        return super.fun4()
    }
}
fun main(args: Array) {
    var demo = Demo2()

    demo.fun1()
    demo.fun2(5)
    println(demo.fun3(10))
    println(demo.fun4())

    //可以不重写该方法直接调用
    demo.fun5()
}
/************输出结果***************/
我是fun1()方法
我是fun2()方法,我的参数是5
我是fun3()方法,我的参数是10,并且返回一个Int类型的值
13
我是fun4()方法,并且返回一个String类型的值
fun4

5.3 冲突解决

某些场景下,可能出现不同父类存在相同方法名的方法,导致子类实现时冲突,用用super<接口名>.方法名来区分

interface A {
    fun foo() { print("A") }
    fun bar()
}

interface B {
    fun foo() { print("B") }
    fun bar() { print("bar") }
}
class C : A {
    override fun bar() { print("bar") }
}

class D : A, B {
    override fun foo() {
        super.foo()
        super.foo()
    }

    override fun bar() {
        super.bar()
    }
}

5.4 枚举定义

声明格式

enum class 类名{
      ...
}

enum class State{
    NORMAL,NO_DATA,NO_INTERNET,ERROR,OTHER
}

//访问枚举常量
State.NORMAL.name
State.NO_DATA.name

Kotlin中枚举的特点是:

  1. 每一个枚举常量都是一个对象,用逗号分隔
  2. 访问方式为:枚举类名.枚举常量.属性

枚举匿名类

实现枚举常量的匿名类,须提供一个定义在枚举类内部的抽象方法,且必须在枚举变量后面,最后一个枚举变量用分号结束。

enum class ConsoleColor {
    BLACK {
        override fun print() {
            println("我是枚举常量 BLACK ")
        }
    },
    GREEN {
        override fun print() {
            println("我是枚举常量 GREEN ")
        }
    };

    abstract fun print()
}

fun main(args: Array) {
    ConsoleColor.BLACK.print()  //我是枚举常量 BLACK 
}

5.5 枚举类使用

枚举类对象内置两个属性:

  • name:枚举对象名称
  • ordinal:枚举对象索引

枚举类同时提供了values()valueOf()方法来检测指定的名称与枚举类中定义的任何枚举常量是否匹配,且提供enumValues()enumValueOf()函数以泛型的方式访问枚举类中的常量

enum class Color(var argb : Int){
    RED(0xFF0000),
    WHITE(0xFFFFFF),
    BLACK(0x000000),
    GREEN(0x00FF00)
}  
println("name = " + Color.RED.name + "\tordinal = " + Color.RED.ordinal)
println("name = " + Color.WHITE.name + "\tordinal = " + Color.WHITE.ordinal)
println("name = " + Color.BLACK.name + "\tordinal = " + Color.BLACK.ordinal)
println("name = " + Color.GREEN.name + "\tordinal = " + Color.GREEN.ordinal)

/************输出结果*********/
name = RED      ordinal = 0
name = WHITE    ordinal = 1
name = BLACK    ordinal = 2
name = GREEN    ordinal = 3
println(enumValues().joinToString { it.name })
println(enumValueOf("RED"))

/************输出结果*********/
RED, WHITE, BLACK, GREEN
RED
println(Color.valueOf("RED"))
println(Color.values()[0])
println(Color.values()[1])
println(Color.values()[2])
println(Color.values()[3])

 /************输出结果*********/
RED
RED
WHITE
BLACK
GREEN

枚举的一些其他特性

  • 可以定义扩展方法和扩展属性
  • 可以用在条件分支when
  • 有顺序,可以用来比较大小
  • 可以应用在区间
  • 也是类,可以实现接口

六、数据类

Kotlin中提供了关键字data,声明一个类为数据类

data class 类名(var param1 :数据类型,...){}
//或者
data class 类名 可见性修饰符 constructor(var param1 : 数据类型 = 默认值,...)

数据类的主要特点是:

  1. 没有结构体的时候,大括号{}可省略
  2. 主构造函数至少存在一个参数,用varval修饰
  3. 不能是抽象的,开放的或者内部的
  4. 可以实现接口或继承其他类
//kotlin
data class User(val name : String, val pwd : String)

上述代码等价于

//Java
public class User {
    private String name;
    private String pwd;

    public User(String name, String pwd) {
        this.name = name;
        this.pwd = pwd;
    }
    // ......
    
    @Override
    public String toString() {
        return "User{" +
                "name='" + name + '\'' +
                ", pwd='" + pwd + '\'' +
                '}';
    }
}
    

copy

如需复制一个数据对象并改变它的一些属性,其余保持不变,可用其copy函数。

val mUser = User("kotlin","123456")
println(mUser)  // User(name=kotlin, pwd=123456)
val mNewUser = mUser.copy(name = "new Kotlin")
println(mNewUser)   // User(name=new Kotlin, pwd=123456)

解构声明

解构声明即把一个对象 解构成很多变量。

Kotlin中定义一个数据类,则系统会默认自动根据参数的个数生成component1() ... componentN()函数。其...,componentN()函数就是用于解构声明

val mUser = User("kotlin","123456")
val (name,pwd) = mUser
println("name = $name\tpwd = $pwd")

备注:

关于data数据类序列化问题,参考资料:https://cloud.tencent.com/developer/article/1587304

七、密封类

密封类用来表示受限的类继承结构:当一个值为有限几种的类型、而不能有任何其他类型时。某种意义上讲,它相当于是枚举类的扩展

密封类的子类必须是在密封类的内部或必须存在于密封类的同一文件

kotlin1.5放宽了限制,只需要保证 Sealed Classes 和它的子类,在同一个包名和 module 下面即可

声明

定义密封类的关键字:sealed,声明格式为:

sealed class SealedExpr()

sealed class Expr {
    data class Const(val number: Double) : Expr()
    data class Sum(val e1: Expr, val e2: Expr) : Expr()
}
object NotANumber : Expr()  // 子类可以定在密封类外部

密封类不能被实例化,类似抽象类。通过反编译可知,内部的子类编译为静态内部类

使用

密封类主要的作用在于限制继承,起到划分子类的作用,也可保证安全。一般与when合用。

sealed class SealedClass{
    class SonClass1: SealedClass(){}

    class SonClass2: SealedClass(){}
}
fun check(fatherClass: FatherClass): String =
when(fatherClass){
    is SonClass1 -> "1"
    is SonClass2 -> "2"
    else ->
    throw IllegalArgumentException("Unknown Class")
}

fun main(args: Array){
    val son1 = SonClass1();
    val result = check(son1)
    println(result)
}

前述代码,每在SealedClass密封类添加一个子类,when结构就会检查到增加了子类,必须给when结构添加分支,否则编译报错

密封类一般也称为枚举类的扩展sealed不能修饰interfaceabstract class

和抽象类类似,Sealed Class可用于表示层级关系。它的子类可以是任意的类:data class、普通Kotlin对象、普通的类,甚至也可以是另一个密封类

sealed class ResponseResult {
    data class Success(val data:T):ResponseResult()

    data class Failure(val throwable: Throwable?):ResponseResult()

    override fun toString(): String {
        return when(this) {
            is Success<*> -> "Success=[data=$data]"
            is Failure -> "Failure[throwable=$throwable]"
        }
    }
}

inline fun  ResponseResult.doSuccess(success:(T) -> Unit) {
    if (this is ResponseResult.Success) {
        success(data)
    }
}

inline fun  ResponseResult.doFailure(failure:(Throwable?) -> Unit) {
    if (this is ResponseResult.Failure) {
        failure(throwable)
    }
}
fun main() {
    val result = if(true) {
        ResponseResult.Success(xxx)
    } else {
        ResponseResult.Failure(xxx)
    }
    
    result.doSuccess{
        
    }
    
    result.doError{
        
    }
}

八、其它类

8.1 抽象类

定义

用关键字abstract声明抽象类

抽象类和普通类的区别在于:抽象类除了可以有其自己的属性、构造函数、方法等组成部分,还包含了抽象函数以及抽象属性

abstract class Lanauage{
    val TAG = this.javaClass.simpleName  // 自身的属性

    fun test() : Unit{

    }
    abstract var name : String           // 抽象属性
    abstract fun init()                  // 抽象方法
}
class TestAbstarctA : Lanauage(){

    override var name: String
    get() = "Kotlin"
    set(value) {}

    override fun init() {
        println("我是$name")
    }
}

val mTestAbstarctA = TestAbstarctA()
println(mTestAbstarctA.name)    //Kotlin
mTestAbstarctA.init()   //我是Kotlin

抽象类的主要特点为:

  1. 不能被直接实例化
  2. 抽象成员只有定义,没有实现
  3. 只能继承一个抽象类
  4. 可以覆写抽象类父类的函数

8.2 嵌套类

定义

类可以嵌套在其他类中,嵌套类默认是静态的。

class Other{           // 外部类
    val numOuther = 1

    class Nested {      // 嵌套类
        fun init(){
            println("执行了init方法")
        }
    }
}

Other.Nested().init()      // 调用格式为:外部类.嵌套类().嵌套类方法/属性

嵌套类的主要特点为:

  1. 调用嵌套类属性或方法格式:外部类.嵌套类().嵌套类方法/属性
  2. 无法访问外部类的属性和成员

8.3 内部类

声明一个内部类使用inner关键字。

内部类会带有一个对外部类的对象的引用,能够访问外部类的成员,但内部类不能在接口或非内部嵌套类中声明。

class Other{            // 外部类
    val numOther = 1

    inner class InnerClass{     // 嵌套内部类
        val name = "InnerClass"
        fun init(){
            println("我是内部类")
        }
    }
}

Other().InnerClass().init()  // 调用格式为:外部类().内部类().内部类方法/属性

8.4 匿名内部类

通过object来创建匿名内部类

interface OnClickListener{
    fun onItemClick(str : String)
}

class Other{
    
  private lateinit var listener : OnClickListener

    fun setOnClickListener(listener: OnClickListener){
        this.listener = listener
    }
    
    fun testListener(){
        listener.onItemClick("我是匿名内部类的测试方法")
    }
}    
// 测试匿名内部类
val other = Other()
other.setOnClickListener(object : OnClickListener{
    override fun onItemClick(str: String) {
        // todo
        println(str)
    }
})
other.testListener()

8.5 局部类

局部类即定义在方法中的类

    val numOther = 1

    fun partMethod(){
        var name : String = "partMethod"

        class Part{
            var numPart : Int = 2

            fun test(){
                name = "test"
                numPart = 5
                println("我是局部类中的方法")
            }
        }

        val part = Part()
        println("name = $name \t numPart = " + part.numPart + "\t numOther = numOther")
        part.test()
        println("name = $name \t numPart = " + part.numPart + "\t numOther = numOther")
    }
}
Other().partMethod()

/***********输出结果***********/
name = partMethod    numPart = 2    numOther = 1
我是局部类中的方法
name = test      numPart = 5    numOther = 1

局部类的主要特点是:

  1. 局部类只能在定义该局部类的方法中使用。
  2. 定义在实例方法中的局部类可以访问外部类的所有变量和方法。但不能修改
  3. 局部类中的可以定义属性、方法,并且可以修改局部方法中的变量

九、类型别名

类型别名是为现有类型提供替代名称,例如用较短名称替代原有类型,解决代码过于冗余与臃肿的问题,关键字为typelias

类名

// 类型别名,切记声明在顶部
typealias First = TypeAliasDemoTestFirst

class TypeAliasDemoTestFirst{
    fun show(){
        println("name : ${this.javaClass.simpleName}")
    }
}

val first = First()
first.show()
typealias NestA = DemoClassTestNest.A

class DemoClassTestNest{
    class A{
        fun show(){
            println("name : ${this.javaClass.simpleName}")
        }
    }
}

val nestA = NestA()
nestA.show()

函数参数

typealias Predicate = (T) -> Boolean

fun foo(p: Predicate) = p(42)

fun main() {
    val f: (Int) -> Boolean = { it > 0 }
    println(foo(f)) // 输出 "true"

    val p: Predicate = { it > 0 }
    println(listOf(1, -2).filter(p)) // 输出 "[1]"
}

属性名冲突解决

在开发中,处理json字符串转换成实体类的时候,会出现属性名和关键字冲突的问题

对于Java,使用@SerializedName注解解决

public class Test{
    private int id;
    private String name;
    @SerializedName("swicth")
    private int s;
}

对于Kotlin,只需将属性名包括在符号``中即可

data class TestBean(val id : Int,
                val name : String,
              val `package` : String)

十、解构声明

解构声明:将一个对象解构为多个变量的操作。data数据类默认支持解构声明,会生成component()方法。

data class Book(var name: String, var price: Float)

val (name, price) = Book("Kotlin入门", 66.6f) //解构声明
println(name)   //Kotlin入门
println(price)  //66.6

实现原理

数据类之所以可以使用解构声明,因为编译器默认为它声明了类似函数

operator fun component1(): String {
    return name
}

operator fun component2(): Float {
    return price
}

其中的 component1()component2() 函数是在 Kotlin 中广泛使用的 约定原则 的另一个例子,为 name、price变量赋值相当于分别调用了 Book 对象的component1()component2()函数

image

自定义解构声明

对于普通类,可以手动声明类似operator fun component1()的方法,来实现解构声明功能

class Book(var name: String, var price: Float) {
    operator fun component1(): String {
        return name
    }

    operator fun component2(): Float {
        return price
    }
}

fun main(args: Array) {
    val (name, price) = Book("Kotlin入门", 66.6f)
    println(name)   //Kotlin入门
    println(price)  //66.6
}

数组、集合解构声明

Kotlin 中数组,listmap系列集合默认也支持解构声明的功能

fun main(args: Array) {
    val array = arrayOf(1, 2, 3)
    val (a1, a2, a3) = array
    
    val list = listOf(1, 2, 3)
    val (b1, b2, b3) = list
    
    val map = mapOf("key1" to 1, "key2" to 2, "key3" to 3)
    for ((key, value) in map) {
        println("$key-$value")
    }
}

解构声明忽略

如果在解构声明中不需要某个变量,那么可以用下划线_取代其名称,这样也就不会调用相应的componentN()操作符函数:

val (_, price) = Book("Kotlin入门", 66.6f)

十一、对象表达式与对象声明

11.1 对象表达式

对象表达式是在使用他们的地方立即执行

创建一个继承某个(或某些)类型的匿名类的对象,我们一般会这么写:

window.addMouseListener(object : MouseAdapter() {
    override fun mouseClicked(e: MouseEvent) { /*……*/ }

    override fun mouseEntered(e: MouseEvent) { /*……*/ }
})

kotlin和Java在创建匿名类的方式相似,只不过将new换成了object

定义一个接口与抽象类,kotlin可以一个匿名对象搞定,而java需要定义两个匿名对象类

abstract class Person {
    abstract fun play()
}
interface Move {
    fun walk()
}
//对象表达式
val a: Any = object : Person(), Move {
    override fun walk() {
        println("walk")
    }
    override fun play() {
        println("play")
    }
}


fun main(args: Array) {
    //调用
    if (a is Person) {
        a.play()
    }
    if (a is Move) {
        a.walk()
    }
}

越过类定义

通过对象表达式,可以越过类的定义,直接得到一个对象

fun foo() {
    val adHoc = object {
        var x: Int = 0
        var y: Int = 0
    }
    print(adHoc.x + adHoc.y)
}

返回值类型

匿名对象可以用作只在本地和私有作用域中声明的类型。

  1. 作为公有函数的返回类型或公有属性类型,实际类型为匿名对象的超类型
  2. 如果是私有,则为匿名对象类型
class C {
    // 私有函数,所以其返回类型是匿名对象类型
    private fun foo() = object {
        val x: String = "x"
    }

    // 公有函数,所以其返回类型是 Any
    fun publicFoo() = object {
        val x: String = "x"
    }

    fun bar() {
        val x1 = foo().x        // foo私有函数,没问题,
        val x2 = publicFoo().x  // 错误:公用函数返回超类型,未能解析的引用“x”
    }
}

对象表达式中的代码可以访问来自包含它的作用域的变量。

fun countClicks(window: JComponent) {
    var clickCount = 0
    var enterCount = 0

    window.addMouseListener(object : MouseAdapter() {
        override fun mouseClicked(e: MouseEvent) {
            clickCount++
        }

        override fun mouseEntered(e: MouseEvent) {
            enterCount++
        }
    })
    // ……
}

11.2 对象声明

Kotlin 使用 object 关键字来声明一个对象,其中变量与函数都是静态的。

可以通过对象声明来获得一个单例,且是线程安全的饿汉式单例

object DataProviderManager {
    fun registerDataProvider(provider: DataProvider) {
        // ……
    }

    val allDataProviders: Collection
        get() = // ……
}

对象声明在另一个类的内部时,只能通过类名来访问,且该对象不能直接访问到外部类的方法和变量

class Site {
    var name = "Hello World"
    object DeskTop{
        var url = "www.runoob.com"
        fun showName(){
            print{"desk legs $name"} // 错误,不能访问到外部类的方法和变量
        }
    }
}
fun main(args: Array) {
    var site = Site()
    site.DeskTop.url // 错误,不能通过外部类的实例访问到该对象
    Site.DeskTop.url // 正确
}

伴生对象

类内部的对象声明可以用 companion关键字标记,可以直接通过外部类访问到对象的内部元素,类似于Java中的static静态功能

class MyClass {
    companion object Factory {
        fun create(): MyClass = MyClass()
    }
}

val instance = MyClass.create()   // 访问到对象的内部元素

可以省略伴生对象的名称,在这种情况下将使用名称 Companion

class MyClass {
    companion object { }
}

val x = MyClass.Companion

一个类里只能声明一个内部关联对象,即companion只能使用一次

Java中的静态变量和方法,在kotlin中都放在companion object中。因此Java中的静态初始化在kotlin中也放在companion object中,由init和一对大括号表示。

class Sample {
    companion object {
      init {
            ...
        }
    }
}

伴生对象是真实对象的实例成员,而且还可以实现接口

interface Factory {
    fun create(): T
}

class MyClass {
      companion object : Factory {
          override fun create(): MyClass = MyClass()
      }
      fun test() {
          println("test")
      }
  }

  fun main(args: Array) {
      val f: Factory = MyClass
      f.create().test()
  }

语义差异

  • 对象表达式是在使用他们的地方立即执行(及初始化)的;
  • 对象声明是在第一次被访问到时延迟初始化的;
  • 伴生对象的初始化是在相应的类被加载(解析)时,与 Java 静态初始化器的语义相匹配

你可能感兴趣的:(Kotlin语法进阶)