Hello! 欢迎来到小浪云!


第4章 类与面向对象编程第4章 类与面向对象编程


第4章 类与面向对象编程

在前面的章节中,我们学习了kotlin的语言基础知识、类型系统等相关的知识。在本章节以及下一章中,我们将一起来学习kotlin面向对象编程以及函数式编程的支持。

本章我们介绍kotlin的面向对象编程。

4.1 面向对象编程简史

50年代后期,在用FORTRAN语言编写大型程序时,由于没有封装机制,那个时候的变量都是“全局变量”,那么就会不可避免的经常出现变量名冲突问题。在ALGOL60中采用了以 Begin – End 为标识的程序块,使块内变量名是局部的,以避免它们与程序中块外的同名变量相冲突。在编程语言中首次提供了封装(保护)的机制。此后,程序块结构广泛用于Pascal 、Ada、C等高级语言之中。

60年代中后期,Simula语言在ALGOL基础上研制开发,它将ALGOL的块结构概念向前发展一步,提出了对象的概念,并使用了类,也支持类继承。其后的发展简史如下图所示:

第4章 类与面向对象编程第4章 类与面向对象编程

面向对象发展简史

阿伦·凯(Alan Kay)是Smalltalk面向对象编程语言的发明人之一,也是面向对象编程思想的创始人之一,同时,他还是笔记本电脑最早的构想者和现代Windows GUI的建筑师。最早提出PC概念和互联网的也是阿伦·凯,所以人们都尊称他为“预言大师”。他是当今IT界屈指可数的技术天才级人物。

面向对象编程思想主要是复用性和灵活性(弹性)。复用性是面向对象编程的一个主要机制。灵活性主要是应对变化的特性,因为客户的需求是不断改变的,怎样适应客户需求的变化,这是软件设计灵活性或者说是弹性的问题。

Java是一种面向对象编程语言,它基于Smalltalk语言,作为OOP语言,它具有以下五个基本特性:

1.万物皆对象,每一个对象都会存储数据,并且可以对自身执行操作。因此,每一个对象包含两部分:成员变量和成员方法。在成员方法中可以改变成员变量的值。

2.程序是对象的集合,他们通过发送消息来告知彼此所要做的事情,也就是调用相应的成员函数

3.每一个对象都有自己的由其他对象所构成的存储,也就是说在创建新对象的时候可以在成员变量中使用已存在的对象。

4.每个对象都拥有其类型,每个对象都是某个类的一个实例,每一个类区别于其它类的特性就是可以向它发送什么类型的消息,也就是它定义了哪些成员函数

5.某一个特定类型的所有对象都可以接受同样的消息。另一种对对象的描述为:对象具有状态(数据,成员变量)、行为(操作,成员方法)和标识(成员名,内存地址)。

面向对象语言其实是对现实生活中的实物的抽象。

每个对象能够接受的请求(消息)由对象的接口所定义,而在程序中必须由满足这些请求的代码,这段代码称之为这个接口的实现。当向某个对象发送消息(请求)时,这个对象便知道该消息的目的(该方法的实现已定义),然后执行相应的代码。

我们经常说一些代码片段是优雅的或美观的,实际上意味着它们更容易被人类有限的思维所处理。

对于程序的复合而言,好的代码是它的表面积要比体积增长的慢。

代码块的“表面积”是是我们复合代码块时所需要的信息(接口API协议定义)。代码块的“体积”就是接口内部的实现逻辑(API背后的实现代码)。

在面向对象编程中,一个理想的对象应该是只暴露它的抽象接口(纯表面, 无体积),其方法则扮演箭头的角色。如果为了理解一个对象如何与其他对象进行复合,当你发现不得不深入挖掘对象的实现之时,此时你所用的编程范式的原本优势就荡然无存了。

面向对象编程是一种编程思想,相比于早期的结构化程序设计,抽象层次更高,思考解决问题的方式上也更加贴近人类的思维方式。现代编程语言基本都支持面向对象编程范式。

计算机领域中的所有问题,都可以通过向上一层进行抽象封装来解决.这里的封装的本质概念,其实就是“映射”。从面向过程到面向对象,再到设计模式,架构设计,面向服务,sass/Pass/Iass等等的思想,各种软件理论思想五花八门,但万变不离其宗——

你要解决一个怎样的问题?你的问题领域是怎样的?你的模型(数据结构)是什么?你的算法是什么?你对这个世界的本质认知是怎样的?你的业务领域的逻辑问题,流程是什么? 等等。

面向对象编程的以现实世界中的事物(对象)为中心来思考, 认识问题, 并根据这些事物的本质特征, 把它们抽象表示为系统中的类。其核心思想可以用下图简要说明:

第4章 类与面向对象编程第4章 类与面向对象编程

面向对象编程

面向对象编程基于类编程,更加贴近人类解决问题的习惯方法。让软件世界更像现实世界。面向对象编程通过抽象出关键的问题域来分解系统。对象不仅能表示具体的事物,还能表示抽象的规则、计划或事件。关于面向对象编程的核心的概念如下图所示

第4章 类与面向对象编程第4章 类与面向对象编程

面向对象编程的核心的概念

4.2 声明类

本节介绍Kotlin中类和构造函数的声明。

4.2.1 空类

使用class关键字声明类。我们可以声明一个什么都不干的类

代码语言:Javascript代码运行次数:0运行复制

class AnEmptyClassfun main(args: Array<string>) {    val anEmptyClass = AnEmptyClass() // Kotlin中不需要使用new    println(anEmptyClass)    println(anEmptyClass is AnEmptyClass) // 对象实例是AnEmptyClass类型    println(anEmptyClass::class)}</string>

输出

代码语言:JavaScript代码运行次数:0运行复制

com.easy.kotlin.AnEmptyClass@2626b418trueclass com.easy.kotlin.AnEmptyClass (Kotlin reflection is not available)

4.2.2 声明类和构造函数

在Kotlin中, 我们可以在声明类的时候同时声明构造函数,语法格式是在类的后面使用括号包含构造函数的参数列表

代码语言:javascript代码运行次数:0运行复制

class Person(var name: String, var age: Int, var sex: String) { // 声明类和构造函数    override fun toString(): String { // override关键字,重写toString()        return "Person(name='$name', age=$age, sex='$sex')"    }}

使用这样的简洁语法,可以通过主构造器来定义属性并初始化属性值(这里的属性值可以是var或val)。

在代码中这样使用Person类

代码语言:javascript代码运行次数:0运行复制

val person = Person("Jack", 29, "M")println("person = ${person}")

输出

代码语言:javascript代码运行次数:0运行复制

person = Person(name='Jack', age=29, sex='M')

另外,我们也可以先声明属性,等到构造实例对象的时候再去初始化属性值,那么我们的Person类可以声明如下

代码语言:javascript代码运行次数:0运行复制

class Person1 {    lateinit var name: String // lateinit 关键字表示该属性延迟初始化    var age: Int = 0  // lateinit 关键字不能修饰 primitive 类型    lateinit var sex: String    override fun toString(): String {        return "Person1(name='$name', age=$age, sex='$sex')"    }}

我们可以在代码中这样创建Person1的实例对象

代码语言:javascript代码运行次数:0运行复制

    val person1 = Person1()    person1.name = "Jack"    person1.age = 29    person1.sex = "M"    println("person1 = ${person1}")

输出

代码语言:javascript代码运行次数:0运行复制

person1 = Person1(name='Jack', age=29, sex='M')

如果我们想声明一个具有多种构造方式的类,可以使用 constructor 关键字声明构造函数,示例代码如下

代码语言:javascript代码运行次数:0运行复制

class Person2() { // 无参的主构造函数    lateinit var name: String    var age: Int = 0    lateinit var sex: String    constructor(name: String) : this() { // this 关键字指向当前类对象实例        this.name = name    }    constructor(name: String, age: Int) : this(name) {        this.name = name        this.age = age    }    constructor(name: String, age: Int, sex: String) : this(name, age) {        this.name = name        this.age = age        this.sex = sex    }    override fun toString(): String {        return "Person1(name='$name', age=$age, sex='$sex')"    }}

上面的写法,总体来看也有些样板代码,其实在idea中,我们写上面的代码,只需要写下面的3行,剩下的就交给IDEA自动生成了

代码语言:javascript代码运行次数:0运行复制

class Person2 {    lateinit var name: String    var age: Int = 0    lateinit var sex: String}

自动生成构造函数的操作示意图

1.在当前类中“右击”鼠标操作,选择Generate (在Mac上的快捷键是 Command + N)

第4章 类与面向对象编程第4章 类与面向对象编程

右击鼠标操作

点击之后,跳出对话框:生成次级构造函数

第4章 类与面向对象编程第4章 类与面向对象编程

选择Generate

选择构造函数的参数

第4章 类与面向对象编程第4章 类与面向对象编程

生成次级构造函数

选中相应的属性,点击OK,即可生成。

一个属性都不选,生成

代码语言:javascript代码运行次数:0运行复制

constructor()

选择一个 name 属性,生成

代码语言:javascript代码运行次数:0运行复制

    constructor(name: String) {        this.name = name    }

选择name,age属性生成

代码语言:javascript代码运行次数:0运行复制

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

3个属性都选择,生成

代码语言:javascript代码运行次数:0运行复制

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

最后,我们可以在代码中这样创建Person2的实例对象

代码语言:javascript代码运行次数:0运行复制

    val person21 = Person2()    person21.name = "Jack"    person21.age = 29    person21.sex = "M"    println("person21 = ${person21}")    val person22 = Person2("Jack", 29)    person22.sex = "M"    println("person22 = ${person22}")    val person23 = Person2("Jack", 29, "M")    println("person23 = ${person23}")

实际上,我们在编程实践中用到最多的构造函数,还是这个

代码语言:javascript代码运行次数:0运行复制

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

而当确实需要通过比较复杂的逻辑来构建一个对象的时候,可采用构建者(Builder)模式来实现。

4.3 抽象类与接口

抽象类表示“is-a”的关系,而接口所代表的是“has-a”的关系。

抽象类用来表征问题领域的抽象概念。所有编程语言都提供抽象机制。机器语言是对机器的模仿抽象,汇编语言是对机器语言的高层次抽象,高级语言(Fortran,C,Basic等)是对汇编的高层次抽象。而我们这里所说的面向对象编程语言是对过程函数的高层次封装。这个过程如下图所示

第4章 类与面向对象编程第4章 类与面向对象编程

编程语言的抽象机制

抽象类和接口是Kotlin语言中两种不同的抽象概念,他们的存在对多态提供了非常好的支持。这个机制跟Java相同。

4.3.1 抽象类与抽象成员

抽象是相对于具象而言。例如设计一个图形编辑软件,问题领域中存在着长方形(Rectangle)、圆形(Circle)、三角形(Triangle)等这样一些具体概念,它们是具象。但是它们又都属于形状(Shape)这样一个抽象的概念。它们的关系如下图所示

第4章 类与面向对象编程第4章 类与面向对象编程

形状Shape的抽象继承关系

对应的Kotlin代码如下

代码语言:javascript代码运行次数:0运行复制

package com.easy.kotlinabstract class Shapeclass Rectangle : Shape() // 继承类的语法是使用冒号 : , 父类需要在这里使用构造函数初始化class Circle : Shape()class Triangle : Shape()

因为抽象的概念在问题领域中没有对应的具体概念,所以抽象类是不能够实例化的。下面的代码编译器会报错

代码语言:javascript代码运行次数:0运行复制

val s = Shape() // 编译不通过!不能实例化抽象类

我们只能实例化它的继承子类。代码示例如下

代码语言:javascript代码运行次数:0运行复制

val r = Rectangle()println(r is Shape) // true

现在我们有了抽象类,但是没有成员。通常一个类的成员有属性和函数。抽象类的成员也必须是抽象的,需要使用abstract 关键字修饰。下面我们声明一个抽象类Shape,并带有width ,heigth,radius属性和 area() 函数, 代码如下

代码语言:javascript代码运行次数:0运行复制

abstract class Shape {    abstract var width: Double    abstract var heigth: Double    abstract var radius: Double    abstract fun area(): Double}

这个时候,继承抽象类Shape的方法如下

代码语言:javascript代码运行次数:0运行复制

class Rectangle(override var width: Double, override var heigth: Double, override var radius: Double) : Shape() { // 声明类的同时也声明了构造函数    override fun area(): Double {        return heigth * width    }}class Circle(override var width: Double, override var heigth: Double, override var radius: Double) : Shape() {    override fun area(): Double {        return 3.14 * radius * radius    }}

其中,override 是覆盖写父类属性和函数的关键字。

在代码中这样调用具体实现的类的函数

代码语言:javascript代码运行次数:0运行复制

fun main(args: Array<string>) {    val r = Rectangle(3.0, 4.0, 0.0)    println(r.area()) // 12.0    val c = Circle(0.0, 0.0, 4.0)    println(c.area()) // 50.24}</string>

抽象类中可以有带实现的函数,例如我们在抽象类Shape中添加一个函数onClick()

代码语言:javascript代码运行次数:0运行复制

abstract class Shape {    ...    fun onClick() { // 默认是final的,不可被覆盖重写        println("I am Clicked!")    }}

那么,我们在所有的子类中都可以直接调用这个onClick()函数

代码语言:javascript代码运行次数:0运行复制

    val r = Rectangle(3.0, 4.0, 0.0)    r.onClick() // I am Clicked!    val c = Circle(0.0, 0.0, 4.0)    c.onClick() // I am Clicked!

父类Shape中的onClick()函数默认是final的,不可被覆盖重写。如果想要开放给子类重新实现这个函数,我们可以在前面加上open 关键字

代码语言:javascript代码运行次数:0运行复制

abstract class Shape {    ...    open fun onClick() {        println("I am Clicked!")    }}

在子类中这样覆盖重写

代码语言:javascript代码运行次数:0运行复制

class Rectangle(override var width: Double, override var heigth: Double, override var radius: Double) : Shape() {    override fun area(): Double {        return heigth * width    }    override fun onClick(){        println("${this::class.simpleName} is Clicked!")    }}fun main(args: Array<string>) {    val r = Rectangle(3.0, 4.0, 0.0)    println(r.area())    r.onClick()}</string>

其中,this::class.simpleName 是Kotlin中的反射的API,在gradle工程的build.gradle中需要添加依赖 compile “org.jetbrains.kotlin:kotlin-reflect:$kotlin_version” ,我们将在后面的章节中详细介绍。

上面的代码运行输出

代码语言:javascript代码运行次数:0运行复制

12.0Rectangle is Clicked!

当子类继承了某个类之后,便可以使用父类中的成员变量,但是并不是完全继承父类的所有成员变量。具体的原则如下:

1.能够继承父类的publicprotected成员变量;不能够继承父类的private成员变量;

2.对于父类的包访问权限成员变量,如果子类和父类在同一个包下,则子类能够继承;否则,子类不能够继承;

3.对于子类可以继承的父类成员变量,如果在子类中出现了同名称的成员变量,则会发生隐藏现象,即子类的成员变量会屏蔽掉父类的同名成员变量。如果要在子类中访问父类中同名成员变量,需要使用super关键字来进行引用。

4.3.2 接口

接口是一种比抽象类更加抽象的“类”。接口本身代表的是一种“类型”的概念。但在语法层面,接口本身不是类,不能实例化接口,我们只能实例化它的实现类。

接口是用来建立类与类之间的协议。实现该接口的实现类必须要实现该接口的所有方法。在Java 8 和Kotlin中,接口可以实现一些通用的方法。

接口是抽象类的延伸,Kotlin跟Java一样,不支持同时继承多个父类,也就是说继承只能存在一个父类(单继承)。但是接口不同,一个类可以同时实现多个接口(多组合),不管这些接口之间有没有关系。这样可以实现多重继承

和Java类似,Kotlin使用interface作为接口的关键词:

代码语言:javascript代码运行次数:0运行复制

Interface ProjectService

Kotlin 的接口与 Java 8 的接口类似。与抽象类相比,他们都可以包含抽象的方法以及方法的实现:

代码语言:javascript代码运行次数:0运行复制

interface ProjectService {    val name: String    val owner: String    fun save(project: Project)    fun print() {        println("I am project")    }}

接口是没有构造函数的。我们使用冒号: 语法来实现一个接口,如果有多个用,逗号隔开:

代码语言:javascript代码运行次数:0运行复制

class ProjectServiceImpl : ProjectService // 跟继承抽象类语法一样,使用冒号class ProjectMilestoneServiceImpl : ProjectService, MilestoneService // 实现多个接口使用逗号( ,) 隔开

在重写print()函数时,因为我们实现的ProjectService、MilestoneService都有一个print()函数,当我们直接使用super.print()时,编译器是无法知道我们想要调用的是那个里面的print函数的,这个我们叫做覆盖冲突,如下图所示

第4章 类与面向对象编程第4章 类与面向对象编程

覆盖冲突

这个时候,我们可以使用下面的语法来调用:

代码语言:javascript代码运行次数:0运行复制

super<projectservice>.print()super<milestoneservice>.print()</milestoneservice></projectservice>

4.4 Object对象

单例模式很常用。它是一种常用的软件设计模式。例如,spring中的Bean默认就是单例。通过单例模式可以保证系统中一个类只有一个实例。即一个类只有一个对象实例。

Kotlin中没有 静态属性和方法,但是可以使用关键字 object 声明一个object 单例对象:

代码语言:javascript代码运行次数:0运行复制

package com.easy.kotlinobject User {    val username: String = "admin"    val password: String = "admin"    fun hello() {        println("Hello, object !")    }}fun main(args: Array<string>) {    println(User.username) // 跟Java的静态类一样的调用形式    println(User.password)    User.hello()}</string>

Kotlin中还提供了 伴生对象 ,用companion object关键字声明:

代码语言:javascript代码运行次数:0运行复制

class DataProcessor {    companion object DataProcessor {        fun process() {            println("I am processing data ...")        }    }}fun main(args: Array<string>) {    DataProcessor.process() // I am processing data ...}</string>

一个类只能有1个伴生对象。

4.5 数据类

顾名思义,数据类就是只存储数据,不包含操作行为的类。Kotlin的数据类可以为我们节省大量样板代码(Java 中强制我们要去写一getter、setter,而实际上这些方法都是“不言自明”的),这样最终代码更易于理解和便于维护。

使用关键字为 data class 创建一个只包含数据的类:

代码语言:javascript代码运行次数:0运行复制

data class LoginUser(val username: String, val password: String)

在IDEA中提供了方便的Kotlin工具箱,我们可以把上面的代码反编译成等价的Java代码。步骤如下

1.菜单栏选择:Tools -> Kotlin -> Show Kotlin Bytecode

第4章 类与面向对象编程第4章 类与面向对象编程

菜单栏选择:Tools -> Kotlin -> Show Kotlin Bytecode

点击Decompile

第4章 类与面向对象编程第4章 类与面向对象编程

点击Decompile

反编译之后的Java代码

第4章 类与面向对象编程第4章 类与面向对象编程

反编译之后的Java代码

上面这段反编译之后的完整的Java代码是

代码语言:javascript代码运行次数:0运行复制

public final class LoginUser {   @NotNull   private final String username;   @NotNull   private final String password;   @NotNull   public final String getUsername() {      return this.username;   }   @NotNull   public final String getPassword() {      return this.password;   }   public LoginUser(@NotNull String username, @NotNull String password) {      Intrinsics.checkParameterIsNotNull(username, "username");      Intrinsics.checkParameterIsNotNull(password, "password");      super();      this.username = username;      this.password = password;   }   @NotNull   public final String component1() {      return this.username;   }   @NotNull   public final String component2() {      return this.password;   }   @NotNull   public final LoginUser copy(@NotNull String username, @NotNull String password) {      Intrinsics.checkParameterIsNotNull(username, "username");      Intrinsics.checkParameterIsNotNull(password, "password");      return new LoginUser(username, password);   }   // $FF: synthetic method   // $FF: bridge method   @NotNull   public static LoginUser copy$default(LoginUser var0, String var1, String var2, int var3, Object var4) {      if ((var3 &amp; 1) != 0) {         var1 = var0.username;      }      if ((var3 &amp; 2) != 0) {         var2 = var0.password;      }      return var0.copy(var1, var2);   }   public String toString() {      return "LoginUser(username=" + this.username + ", password=" + this.password + ")";   }   public int hashCode() {      return (this.username != null ? this.username.hashCode() : 0) * 31 + (this.password != null ? this.password.hashCode() : 0);   }   public boolean equals(Object var1) {      if (this != var1) {         if (var1 instanceof LoginUser) {            LoginUser var2 = (LoginUser)var1;            if (Intrinsics.areEqual(this.username, var2.username) &amp;&amp; Intrinsics.areEqual(this.password, var2.password)) {               return true;            }         }         return false;      } else {         return true;      }   }}

编译器会从主构造函数中声明的属性,自动创建以下函数:

equals() / hashCode() 函数toString() 格式为”LoginUser(username=” + this.username + “, password=” + this.password + “)”component1(),component2() 函数返回对应下标的属性值,按声明顺序排列copy() 函数: 根据旧对象属性重新 new LoginUser(username, password) 一个对象出来

如果这些函数在类中已经被明确定义了,或者从超类中继承而来,编译器就不再生成。

数据类有如下限制:

主构造函数至少包含一个参数参数必须标识为val 或者 var不能为 abstract, open, sealed 或者 inner不能继承其它类 (但可以实现接口)

另外,数据类可以在解构声明中使用:

代码语言:javascript代码运行次数:0运行复制

package com.easy.kotlindata class LoginUser(val username: String, val password: String)fun main(args: Array<string>) {    val loginUser = LoginUser("admin", "admin")    val (username, password) = loginUser    println("username = ${username}, password = ${password}") // username = admin, password = admin}</string>

Kotlin 标准库提供了 Pair 和 Triple数据类 。

4.6 注解

注解是将元数据附加到代码中。元数据信息由注解 kotlin.Metadata定义。

代码语言:javascript代码运行次数:0运行复制

@Retention(AnnotationRetention.RUNTIME)@Target(AnnotationTarget.CLASS)internal annotation class Metadata

这个@Metadata信息存在于由 Kotlin 编译器生成的所有类文件中, 并由编译器和反射读取。例如,我们使用Kotlin声明一个注解

代码语言:javascript代码运行次数:0运行复制

annotation class Suspendable // Java中使用的是@interface Suspendable

那么,编译器会生成对应的元数据信息

代码语言:javascript代码运行次数:0运行复制

@Retention(RetentionPolicy.RUNTIME)@Metadata(   mv = {1, 1, 7},   bv = {1, 0, 2},   k = 1,   d1 = {"u0000nnu0002u0018u0002nu0002u0010u001bnu0000bu0086u0002u0018u00002u00020u0001Bu0000¨u0006u0002"},   d2 = {"Lcom/easy/kotlin/Suspendable;", "", "production sources for module kotlin_tutorials_main"})public @interface Suspendable {}

Kotlin 的注解完全兼容 Java 的注解。例如,我们在Kotlin中使用Spring Data Jpa

代码语言:javascript代码运行次数:0运行复制

interface ImageRepository : PagingAndSortingRepository<image long> {       @Query("SELECT a from #{#entityName} a where a.isDeleted=0 and a.isFavorite=1 and a.category like %:searchText% order by a.gmtModified desc")    fun searchFavorite(@Param("searchText") searchText: String, pageable: Pageable): Page<image>    @Throws(Exception::class)    @Modifying    @Transactional    @Query("update #{#entityName} a set a.isFavorite=1,a.gmtModified=now() where a.id=?1")    fun addFavorite(id: Long)}</image></image>

用起来跟Java的注解基本一样。再举个Kotlin使用Spring mvc注解的代码实例

代码语言:javascript代码运行次数:0运行复制

@Controllerclass MeituController {    @Autowired    lateinit var imageRepository: ImageRepository    @RequestMapping(value = *arrayOf("/", "meituView"), method = arrayOf(RequestMethod.GET))    fun meituView(model: Model, request: HttpServletRequest): ModelAndView {        model.addAttribute("requestURI", request.requestURI)        return ModelAndView("meituView")    }}

从上面的例子,我们可以看出Kotlin使用java框架非常简单方便。

4.7 枚举

Kotlin中使用 enum class 关键字来声明一个枚举类。例如

代码语言:javascript代码运行次数:0运行复制

enum class Direction {    NORTH, SOUTH, WEST, EAST // 每个枚举常量都是一个对象, 用逗号分隔}

相比于字符串常量,使用枚举能够实现类型安全。枚举类有两个内置的属性:

代码语言:javascript代码运行次数:0运行复制

    public final val name: String    public final val ordinal: Int

分别表示的是枚举对象的值跟下标位置。例如上面的Direction枚举类,它的枚举对象的信息如下

代码语言:javascript代码运行次数:0运行复制

&gt;&gt;&gt; val north = Direction.NORTH&gt;&gt;&gt; north.nameNORTH&gt;&gt;&gt; north.ordinal0&gt;&gt;&gt; north is Directiontrue

每一个枚举都是枚举类的实例,它们可以被初始化:

代码语言:javascript代码运行次数:0运行复制

enum class Color(val rgb: Int) {    RED(0xFF0000),    GREEN(0x00FF00),    BLUE(0x0000FF)}

枚举Color的枚举对象的信息如下

代码语言:javascript代码运行次数:0运行复制

&gt;&gt;&gt; val c = Color.GREEN&gt;&gt;&gt; cGREEN&gt;&gt;&gt; c.rgb65280&gt;&gt;&gt; c.ordinal1&gt;&gt;&gt; c.nameGREEN

4.8 内部类4.8.1 普通嵌套类

Kotlin中,类可以嵌套。一个类可以嵌套在其他类中,而且可以嵌套多层。

代码语言:javascript代码运行次数:0运行复制

class NestedClassesDemo {    class Outer {        private val zero: Int = 0        val one: Int = 1        class Nested {            fun getTwo() = 2            class Nested1 {                val three = 3                fun getFour() = 4            }        }    }}

测试代码:

代码语言:javascript代码运行次数:0运行复制

    val one = NestedClassesDemo.Outer().one    val two = NestedClassesDemo.Outer.Nested().getTwo()    val three = NestedClassesDemo.Outer.Nested.Nested1().three    val four = NestedClassesDemo.Outer.Nested.Nested1().getFour()

我们可以看出,代码中 NestedClassesDemo.Outer.Nested().getTwo() 访问嵌套类的方式是直接使用 类名.来访问, 有多少层嵌套,就用多少层类名来访问。

普通的嵌套类,没有持有外部类的引用,所以是无法访问外部类的变量的:

代码语言:javascript代码运行次数:0运行复制

class NestedClassesDemo {class Outer {        private val zero: Int = 0        val one: Int = 1        class Nested {            fun getTwo() = 2            fun accessOuter() = {                println(zero) // error, cannot access outer class                println(one)  // error, cannot access outer class            }        }}}

4.8.2 嵌套内部类

如果一个类Inner想要访问外部类Outer的成员,可以在这个类前面添加修饰符 inner。内部类会带有一个对外部类的对象的引用:

代码语言:javascript代码运行次数:0运行复制

package com.easy.kotlinclass NestedClassesDemo {    class Outer {        private val zero: Int = 0        val one: Int = 1        inner class Inner {            fun accessOuter() = {                println(zero) // works                println(one) // works            }        }    }}fun main(args: Array<string>) {    val innerClass = NestedClassesDemo.Outer().Inner().accessOuter()}</string>

我们可以看到,当访问inner class Inner的时候,我们使用的是Outer().Inner(), 这是持有了Outer的对象引用。跟普通嵌套类直接使用类名访问的方式区分。

4.8.3 匿名内部类

匿名内部类,就是没有名字的内部类。既然是内部类,那么它自然也是可以访问外部类的变量的。

我们使用对象表达式创建一个匿名内部类实例:

代码语言:javascript代码运行次数:0运行复制

class NestedClassesDemo {    class AnonymousInnerClassDemo {        var isRunning = false        fun doRun() {            Thread(object : Runnable { // 匿名内部类                override fun run() {                    isRunning = true                    println("doRun : i am running, isRunning = $isRunning")                }            }).start()        }    }}

如果对象是函数式 Java 接口,即具有单个抽象方法的 Java 接口的实例,例如上面的例子中的Runnable接口:

代码语言:javascript代码运行次数:0运行复制

@functionalInterfacepublic interface Runnable {    public abstract void run();}

我们可以使用Lambda表达式创建它,下面的几种写法都是可以的:

代码语言:javascript代码运行次数:0运行复制

            fun doStop() {                var isRunning = true                Thread({                    isRunning = false                    println("doStop: i am not running, isRunning = $isRunning")                }).start()            }            fun doWait() {                var isRunning = true                val wait = Runnable {                    isRunning = false                    println("doWait: i am waiting, isRunning = $isRunning")                }                Thread(wait).start()            }            fun doNotify() {                var isRunning = true                val wait = {                    isRunning = false                    println("doNotify: i notify, isRunning = $isRunning")                }                Thread(wait).start()            }

更多关于Lambda表达式以及函数式编程相关内容,我们将在下一章节中介绍。

本章小结

本章我们介绍了Kotlin面向对象编程的特性: 类与构造函数、抽象类与接口、继承与组合等知识,同时介绍了Kotlin中的注解类、枚举类、数据类、嵌套类、内部类、匿名内部类、单例object对象等特性类。

总的来说,在面向对象编程范式的支持上,Kotlin相比于Java增加不少有趣的功能与特性支持,这使得我们代码写起来更加方便快捷了。

我们知道,在Java 8 中,引进了对函数式编程的支持:Lambda表达式、Function接口、stream API等,而在Kotlin中,对函数式编程的支持更加全面丰富,代码写起来也更加简洁优雅。下一章中,我们来一起学习Kotlin的函数式编程。

相关阅读