Kotlin-Android世界的一股清流-Class类

源码地址:https://github.com/cn-ljb/KotlinBlogs

类的定义

Kotlin中的类也是使用class关键字定义

但整个类结构与Java有所不同:Kotlin中类的定义主要由三部分组成:类名、类头、类主体。

//        类名        类头
class Person(var name: String) {

    //...类主体

}

一、类名

同Java紧跟class关键字之后,空格隔开

二、类头

两部分组成:

  • 成员属性
  • 主构造函数

代码示例:

class Person(var name: String) {

    init {
        println("属性已被初始化:$name")
        println("主构造额外操作")
    }

}

name是Person类的一个成员属性,小括号()是Person类的主构造方法。也就是说Kotlin中在创建Person类对象时,必须传入name属性值,并且会自动为其初始化,帮我们做了类似Java里this.name = name的操作。

如果我们的主构造除了初始化属性之外,还有别的代码操作,需要把这些操作放到init()函数中,Kotlin的主构造函数在初始化属性之后,会调用该函数。

调用:

Person("Jack")

输出:

属性已被初始化:Jack
主构造额外操作

三、类主体

类主体部分类似于Java(大括号里的东西),但也有所不同,前面的主构造额外函数init()也属于类主体区域内。

类主体可以包含:

  • 构造函数和初始代码块
  • 成员属性
  • 成员函数
  • 内部类
  • 伴生对象
  • 对象声明

1、构造函数

除之前的主构造函数和init()函数外,Kotlin也支持多构造函数,其它的构造函数称为二级构造函数

 constructor (name: String, age: Int) : this(name) {
    this.age = age
}

这里,Kotlin定义二级构造函数与Java不同:

  • 仅需constructor关键字修饰,不需要函数名
  • 所有的二级构造必须先调用(代理)主构造,主构造执行完后,会执行二级构造中的代码

所以我们仅在二级构造中初始化了age属性,name属性已由之前的主构造初始化。

如果我们没有定义构造函数(主\二级),JVM虚拟机依旧会为我们提供一个无参构造函数,这和Java一样;但是,如果我们的主构造参数设置了默认值,Jvm虚拟机也会为我们提供一个无参构造。

//主构造存在默认参数,JVM依旧会提供一个无参构造
class Person(var name: String = "无名") {
    //...
}

所以我们可以使用默认值直接创建Person对象

val wuMing = Person()

(注:虽然我们已经创建过很多次对象了,但还是提下Kotlin创建对象是不需要new关键字的

2、成员属性

首先概念上是同Java的成员变量一样的,我们还是单独来说说它的区别和特点

Kotlin支持可变属性和只读属性,什么意思呢?类似于Java中常量和变量的概念

  • val关键字:只读属性,初始化后,只能访问,不能赋值

  • var关键字:可变属性,可读可写

getter()\setter()

讲到属性,不能少了JavaBean里的getter()\setter()方法,虽然Java的IDE基本都提供了自动生成代码的快捷选项,但Kotlin将其再一步简化

Kotlin中如果属性是var可变属性,那么默认就提供getter()\setter()函数,val则提供getter()函数

如果我们需要覆写这些getter()\setter()方法只需遵循以下格式:

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

我们尝试修改age属性的getter()方法,不同年龄段,返回不同的数字:

var age: Int = 0
    get() {
        if (field < 16) {
            return 0
        } else if (field in 16..30) {
            return 1
        } else return 2
    }

调用:

val per = Person("Jack", 10)
println(per.age) //输出0

等等,getter()里的field是什么?备用字段

为什么我们不直接比较age而是用field关键字呢?

因为在Kotlin中规定,如果直接使用属性名,那么就相当于调用它的getter()函数,所以如果我们在getter函数里直接使用age属性,那么这将是一个无限的递归,JVM自然会抛出 栈溢出

为了解决这个问题Kotlin在getter()和setter()函数中为我们提供了这个备用字段field

3、成员函数

在函数章节提到过,无论是定义,使用,作用域都同Java,暂一笔带过。

4、内部类

Kotlin中对于内部类的定义有所不同,在Kotlin中内部类除了必须定义在类的内部外,还必须通过inner关键字修饰,否则它只是个嵌套类

探讨内部类和嵌套类各自的特点:

A是外部类,B是A的嵌套类,C是A的内部类,我们尝试互相访问对方的属性以及函数。

外部类访问嵌套类以及内部类中的资源,我们在A类中添加tell()函数测试:

以上代码中可以得出结论:外部类是无法直接访问嵌套类或者内部类中的任何资源

那么嵌套类能访问外部类资源吗?我们给嵌套类也加上tell()函数

很显然嵌套类是不能直接调用外部类资源以及内部类资源

再来看看内部类:

Kotlin中的内部类跟Java一样是可以直接访问外部类资源,但不能访问嵌套类资源

最后,嵌套类、外部类是如何创建对象的:

//嵌套类创建对象
val b = A.B()

//内部类创建对象
val c =A().C()

嵌套类创建对象,需要指定外部类前缀;而内部类创建对象则需要通过外部类对象来创建

细心的朋友可能已经发现这两种书写格式在Java中很眼熟,内部类的创建格式与Java相同就不说了,这个嵌套类与Java的(static)静态内部类创建语法是一样的。

如果真如我们猜想的那样,Java中的静态内部类,可以访问直接外部类中static修饰的全局变量;Kotlin中的嵌套类我们不妨测试一下。(Kotlin中的全局变量通过伴生对象定义,这个之后会讲,先知道它就是全局变量即可)

我们发现嵌套类可以调用该变量

所以可以得出:Kotlin中的嵌套类的使用场景是类似与Java中的静态内部类的使用场景。

5、伴生对象

前面也提到了Kotlin中是没有static关键字的,但是总得有东西来替代它,那么它就是伴生对象。

从名字来看它貌似是随着类一起出生的,它和Java中static的定义也很相似(当类加载时static修饰的属性和方法就直接加载到内存中)。

使用起来也和Java中是一样的,可以直接通过类名调用,还有伴生对象只能使用伴生对象中的属性及方法,就如同static修饰的方法只能调用static修饰的属性和方法一样。

6、对象声明与表达式

单例设计模式大家一定再熟悉不过,如果在Java中,肯定首要是创建一个类,然后私有化构造方法等等,Kotlin中提供了更简便的实现方案。你不是仅需一个对象吗,那么干嘛要创建类,直接创建一个对象不是更好。

Kotlin中支持直接创建对象,称为:对象声明 ,使用object关键字修饰。

我们在之前的Person类中声明一个obj对象

那么这个对象对于Person来说就像一个全局的只读变量一样

当然你完全可以不用写在类的内部,只是要知道类的主体里可以写对象声明的

对象表达式又是什么?

Java中的匿名内部类,想必做android开发的同学再熟悉不过,那么Kotlin中的匿名内部类就叫做对象表达式。

既然类似Java中的匿名内部类,那么使用场景肯定也是类似的了。

这段代码想必大家都很熟悉,为Button设置点击事件的回调,用Java代码实现最直接的方法便是创建个匿名内部类对象,那么我们来看看Kotlin的对象表达式

我只能说实在是不能再像了(与Java匿名内部类相比)。

唯一的区别就在于,对象表达式可以直接访问局部作用域里的变量,不需要像java那样final修饰才行。

四、数据类

最后再讲讲一种特殊的类:数据类

前面属性时,提到了JavaBean的概念,提供getter()和setter()函数,除了这两个函数之外equals()、toString()等也是几个常用函数,那么Kotlin中为了开发方便,直接提供data关键字来修饰这些数据类。

使用data修饰的类,除了之前var\val提供的setter()\getter()函数之外,还默认实现了

equals()/hashCode 函数
toString 格式是 "User(name=john, age=42)"
[compontN()] 
copy() 函数    

如果在类中明确声明或从基类继承了这些方法,编译器不会自动生成。

为确保这些生成代码的一致性,并实现有意义的行为,数据类要满足下面的要求:

  • 主构造函数应该至少有一个参数;
  • 主构造函数的所有参数必须标注为 val 或者 var ;
  • 数据类不能是 abstract,open,sealed,或者 inner ;
  • 在 JVM 中如果构造函数是无参的,则所有的属性必须有默认的值。

例如:

data class Data(var data1: String, var data2: String)