第4章 类与面向对象编程java
在前面的章节中,咱们学习了Kotlin的语言基础知识、类型系统等相关的知识。在本章节以及下一章中,咱们将一块儿来学习Kotlin对面向对象编程以及函数式编程的支持。算法
本章咱们介绍Kotlin的面向对象编程。编程
50年代后期,在用FORTRAN语言编写大型程序时,因为没有封装机制,那个时候的变量都是“全局变量”,那么就会不可避免的常常出现变量名冲突问题。在ALGOL60中采用了以 Begin - End 为标识的程序块,使块内变量名是局部的,以免它们与程序中块外的同名变量相冲突。在编程语言中首次提供了封装(保护)的机制。此后,程序块结构普遍用于Pascal 、Ada、C等高级语言之中。设计模式
60年代中后期,Simula语言在ALGOL基础上研制开发,它将ALGOL的块结构概念向前发展一步,提出了对象的概念,并使用了类,也支持类继承。其后的发展简史以下图所示:安全
阿伦·凯(Alan Kay)是Smalltalk面向对象编程语言的发明人之一,也是面向对象编程思想的创始人之一,同时,他仍是笔记本电脑最先的构想者和现代Windows GUI的建筑师。最先提出PC概念和互联网的也是阿伦·凯,因此人们都尊称他为“预言大师”。他是当今IT界屈指可数的技术天才级人物。数据结构
面向对象编程思想主要是复用性和灵活性(弹性)。复用性是面向对象编程的一个主要机制。灵活性主要是应对变化的特性,由于客户的需求是不断改变的,怎样适应客户需求的变化,这是软件设计灵活性或者说是弹性的问题。架构
Java是一种面向对象编程语言,它基于Smalltalk语言,做为OOP语言,它具备如下五个基本特性:app
1.万物皆对象,每个对象都会存储数据,而且能够对自身执行操做。所以,每个对象包含两部分:成员变量和成员方法。在成员方法中能够改变成员变量的值。框架
2.程序是对象的集合,他们经过发送消息来告知彼此所要作的事情,也就是调用相应的成员函数。编程语言
3.每个对象都有本身的由其余对象所构成的存储,也就是说在建立新对象的时候能够在成员变量中使用已存在的对象。
4.每一个对象都拥有其类型,每一个对象都是某个类的一个实例,每个类区别于其它类的特性就是能够向它发送什么类型的消息,也就是它定义了哪些成员函数。
5.某一个特定类型的全部对象均可以接受一样的消息。另外一种对对象的描述为:对象具备状态(数据,成员变量)、行为(操做,成员方法)和标识(成员名,内存地址)。
面向对象语言实际上是对现实生活中的实物的抽象。
每一个对象可以接受的请求(消息)由对象的接口所定义,而在程序中必须由知足这些请求的代码,这段代码称之为这个接口的实现。当向某个对象发送消息(请求)时,这个对象便知道该消息的目的(该方法的实现已定义),而后执行相应的代码。
咱们常常说一些代码片断是优雅的或美观的,实际上意味着它们更容易被人类有限的思惟所处理。
对于程序的复合而言,好的代码是它的表面积要比体积增加的慢。
代码块的“表面积”是是咱们复合代码块时所须要的信息(接口API协议定义)。代码块的“体积”就是接口内部的实现逻辑(API背后的实现代码)。
在面向对象编程中,一个理想的对象应该是只暴露它的抽象接口(纯表面, 无体积),其方法则扮演箭头的角色。若是为了理解一个对象如何与其余对象进行复合,当你发现不得不深刻挖掘对象的实现之时,此时你所用的编程范式的本来优点就荡然无存了。
面向对象编程是一种编程思想,相比于早期的结构化程序设计,抽象层次更高,思考解决问题的方式上也更加贴近人类的思惟方式。现代编程语言基本都支持面向对象编程范式。
计算机领域中的全部问题,均可以经过向上一层进行抽象封装来解决.这里的封装的本质概念,其实就是“映射”。从面向过程到面向对象,再到设计模式,架构设计,面向服务,Sass/Pass/Iass等等的思想,各类软件理论思想五花八门,但万变不离其宗——
我对OO编程的目标历来就不是复用。相反,对我来讲,对象提供了一种处理复杂性的方式。这个问题能够追溯到亚里士多德:您把这个世界视为过程仍是对象?在OO兴起运动以前,编程以过程为中心--例如结构化设计方法。然而,系统已经到达了超越其处理能力的复杂性极点。有了对象,咱们可以经过提高抽象级别来构建更大的、更复杂的系统--我认为,这才是面向对象编程运动的真正胜利。(Grady Booch,统一建模语言UML创始人)
面向对象编程的以现实世界中的事物(对象)为中心来思考, 认识问题, 并根据这些事物的本质特征, 把它们抽象表示为系统中的类。其核心思想能够用下图简要说明:
面向对象编程基于类编程,更加贴近人类解决问题的习惯方法。让软件世界更像现实世界。面向对象编程经过抽象出关键的问题域来分解系统。对象不只能表示具体的事物,还能表示抽象的规则、计划或事件。关于面向对象编程的核心的概念以下图所示
本节介绍Kotlin中类和构造函数的声明。
使用class关键字声明类。咱们能够声明一个什么都不干的类
class AnEmptyClass fun main(args: Array<String>) { val anEmptyClass = AnEmptyClass() // Kotlin中不须要使用new println(anEmptyClass) println(anEmptyClass is AnEmptyClass) // 对象实例是AnEmptyClass类型 println(anEmptyClass::class) }
输出
com.easy.kotlin.AnEmptyClass@2626b418 true class com.easy.kotlin.AnEmptyClass (Kotlin reflection is not available)
在Kotlin中, 咱们能够在声明类的时候同时声明构造函数,语法格式是在类的后面使用括号包含构造函数的参数列表
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类
val person = Person("Jack", 29, "M") println("person = ${person}")
输出
person = Person(name='Jack', age=29, sex='M')
另外,咱们也能够先声明属性,等到构造实例对象的时候再去初始化属性值,那么咱们的Person类能够声明以下
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的实例对象
val person1 = Person1() person1.name = "Jack" person1.age = 29 person1.sex = "M" println("person1 = ${person1}")
输出
person1 = Person1(name='Jack', age=29, sex='M')
若是咱们想声明一个具备多种构造方式的类,可使用 constructor 关键字声明构造函数,示例代码以下
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自动生成了
class Person2 { lateinit var name: String var age: Int = 0 lateinit var sex: String }
自动生成构造函数的操做示意图
1.在当前类中“右击”鼠标操做,选择Generate (在Mac上的快捷键是 Command + N)
选中相应的属性,点击OK,便可生成。
一个属性都不选,生成
constructor()
选择一个 name 属性,生成
constructor(name: String) { this.name = name }
选择name,age属性生成
constructor(name: String, age: Int) : this(name) { this.name = name this.age = age }
3个属性都选择,生成
constructor(name: String, age: Int, sex: String) : this(name, age) { this.name = name this.age = age this.sex = sex }
最后,咱们能够在代码中这样建立Person2的实例对象
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}")
实际上,咱们在编程实践中用到最多的构造函数,仍是这个
class Person(var name: String, var age: Int, var sex: String)
而当确实须要经过比较复杂的逻辑来构建一个对象的时候,可采用构建者(Builder)模式来实现。
抽象类表示“is-a”的关系,而接口所表明的是“has-a”的关系。
抽象类用来表征问题领域的抽象概念。全部编程语言都提供抽象机制。机器语言是对机器的模仿抽象,汇编语言是对机器语言的高层次抽象,高级语言(Fortran,C,Basic等)是对汇编的高层次抽象。而咱们这里所说的面向对象编程语言是对过程函数的高层次封装。这个过程以下图所示
抽象类和接口是Kotlin语言中两种不一样的抽象概念,他们的存在对多态提供了很是好的支持。这个机制跟Java相同。
抽象是相对于具象而言。例如设计一个图形编辑软件,问题领域中存在着长方形(Rectangle)、圆形(Circle)、三角形(Triangle)等这样一些具体概念,它们是具象。可是它们又都属于形状(Shape)这样一个抽象的概念。它们的关系以下图所示
对应的Kotlin代码以下
package com.easy.kotlin abstract class Shape class Rectangle : Shape() // 继承类的语法是使用冒号 : , 父类须要在这里使用构造函数初始化 class Circle : Shape() class Triangle : Shape()
由于抽象的概念在问题领域中没有对应的具体概念,因此抽象类是不可以实例化的。下面的代码编译器会报错
val s = Shape() // 编译不经过!不能实例化抽象类
咱们只能实例化它的继承子类。代码示例以下
val r = Rectangle() println(r is Shape) // true
如今咱们有了抽象类,可是没有成员。一般一个类的成员有属性和函数。抽象类的成员也必须是抽象的,须要使用abstract 关键字修饰。下面咱们声明一个抽象类Shape,并带有width ,heigth,radius属性和 area() 函数, 代码以下
abstract class Shape { abstract var width: Double abstract var heigth: Double abstract var radius: Double abstract fun area(): Double }
这个时候,继承抽象类Shape的方法以下
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 是覆盖写父类属性和函数的关键字。
在代码中这样调用具体实现的类的函数
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 }
抽象类中能够有带实现的函数,例如咱们在抽象类Shape中添加一个函数onClick()
abstract class Shape { ... fun onClick() { // 默认是final的,不可被覆盖重写 println("I am Clicked!") } }
那么,咱们在全部的子类中均可以直接调用这个onClick()函数
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 关键字
abstract class Shape { ... open fun onClick() { println("I am Clicked!") } }
在子类中这样覆盖重写
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() }
其中,this::class.simpleName 是Kotlin中的反射的API,在Gradle工程的build.gradle中须要添加依赖 compile "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" ,咱们将在后面的章节中详细介绍。
上面的代码运行输出
12.0 Rectangle is Clicked!
当子类继承了某个类以后,即可以使用父类中的成员变量,可是并非彻底继承父类的全部成员变量。具体的原则以下:
1.可以继承父类的public和protected成员变量;不可以继承父类的private成员变量;
2.对于父类的包访问权限成员变量,若是子类和父类在同一个包下,则子类可以继承;不然,子类不可以继承;
3.对于子类能够继承的父类成员变量,若是在子类中出现了同名称的成员变量,则会发生隐藏现象,即子类的成员变量会屏蔽掉父类的同名成员变量。若是要在子类中访问父类中同名成员变量,须要使用super关键字来进行引用。
接口是一种比抽象类更加抽象的“类”。接口自己表明的是一种“类型”的概念。但在语法层面,接口自己不是类,不能实例化接口,咱们只能实例化它的实现类。
接口是用来创建类与类之间的协议。实现该接口的实现类必需要实现该接口的全部方法。在Java 8 和Kotlin中,接口能够实现一些通用的方法。
接口是抽象类的延伸,Kotlin跟Java同样,不支持同时继承多个父类,也就是说继承只能存在一个父类(单继承)。可是接口不一样,一个类能够同时实现多个接口(多组合),无论这些接口之间有没有关系。这样能够实现多重继承。
和Java相似,Kotlin使用interface做为接口的关键词:
interface ProjectService
Kotlin 的接口与 Java 8 的接口相似。与抽象类相比,他们均可以包含抽象的方法以及方法的实现:
interface ProjectService { val name: String val owner: String fun save(project: Project) fun print() { println("I am project") } }
接口是没有构造函数的。咱们使用冒号:
语法来实现一个接口,若是有多个用,
逗号隔开:
class ProjectServiceImpl : ProjectService // 跟继承抽象类语法同样,使用冒号 class ProjectMilestoneServiceImpl : ProjectService, MilestoneService // 实现多个接口使用逗号( ,) 隔开
在重写print()
函数时,由于咱们实现的ProjectService、MilestoneService都有一个print()
函数,当咱们直接使用super.print()
时,编译器是没法知道咱们想要调用的是那个里面的print函数的,这个咱们叫作覆盖冲突,以下图所示
这个时候,咱们可使用下面的语法来调用:
super<ProjectService>.print() super<MilestoneService>.print()
单例模式很经常使用。它是一种经常使用的软件设计模式。例如,Spring中的Bean默认就是单例。经过单例模式能够保证系统中一个类只有一个实例。即一个类只有一个对象实例。
Kotlin中没有 静态属性和方法,可是可使用关键字 object
声明一个object 单例对象:
package com.easy.kotlin object 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() }
Kotlin中还提供了 伴生对象 ,用companion object
关键字声明:
class DataProcessor { companion object DataProcessor { fun process() { println("I am processing data ...") } } } fun main(args: Array<String>) { DataProcessor.process() // I am processing data ... }
一个类只能有1个伴生对象。
顾名思义,数据类就是只存储数据,不包含操做行为的类。Kotlin的数据类能够为咱们节省大量样板代码(Java 中强制咱们要去写一堆getter、setter,而实际上这些方法都是“不言自明”的),这样最终代码更易于理解和便于维护。
使用关键字为 data class 建立一个只包含数据的类:
data class LoginUser(val username: String, val password: String)
在IDEA中提供了方便的Kotlin工具箱,咱们能够把上面的代码反编译成等价的Java代码。步骤以下
1.菜单栏选择:Tools -> Kotlin -> Show Kotlin Bytecode
上面这段反编译以后的完整的Java代码是
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 & 1) != 0) { var1 = var0.username; } if ((var3 & 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) && Intrinsics.areEqual(this.password, var2.password)) { return true; } } return false; } else { return true; } } }
编译器会从主构造函数中声明的属性,自动建立如下函数:
若是这些函数在类中已经被明肯定义了,或者从超类中继承而来,编译器就再也不生成。
数据类有以下限制:
另外,数据类能够在解构声明中使用:
package com.easy.kotlin data 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 }
Kotlin 标准库提供了 Pair 和 Triple数据类 。
注解是将元数据附加到代码中。元数据信息由注解 kotlin.Metadata定义。
@Retention(AnnotationRetention.RUNTIME) @Target(AnnotationTarget.CLASS) internal annotation class Metadata
这个@Metadata信息存在于由 Kotlin 编译器生成的全部类文件中, 并由编译器和反射读取。例如,咱们使用Kotlin声明一个注解
annotation class Suspendable // Java中使用的是@interface Suspendable
那么,编译器会生成对应的元数据信息
@Retention(RetentionPolicy.RUNTIME) @Metadata( mv = {1, 1, 7}, bv = {1, 0, 2}, k = 1, d1 = {"\u0000\n\n\u0002\u0018\u0002\n\u0002\u0010\u001b\n\u0000\b\u0086\u0002\u0018\u00002\u00020\u0001B\u0000¨\u0006\u0002"}, d2 = {"Lcom/easy/kotlin/Suspendable;", "", "production sources for module kotlin_tutorials_main"} ) public @interface Suspendable { }
Kotlin 的注解彻底兼容 Java 的注解。例如,咱们在Kotlin中使用Spring Data Jpa
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) }
用起来跟Java的注解基本同样。再举个Kotlin使用Spring MVC注解的代码实例
@Controller class 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框架很是简单方便。
Kotlin中使用 enum class 关键字来声明一个枚举类。例如
enum class Direction { NORTH, SOUTH, WEST, EAST // 每一个枚举常量都是一个对象, 用逗号分隔 }
相比于字符串常量,使用枚举可以实现类型安全。枚举类有两个内置的属性:
public final val name: String public final val ordinal: Int
分别表示的是枚举对象的值跟下标位置。例如上面的Direction枚举类,它的枚举对象的信息以下
>>> val north = Direction.NORTH >>> north.name NORTH >>> north.ordinal 0 >>> north is Direction true
每个枚举都是枚举类的实例,它们能够被初始化:
enum class Color(val rgb: Int) { RED(0xFF0000), GREEN(0x00FF00), BLUE(0x0000FF) }
枚举Color的枚举对象的信息以下
>>> val c = Color.GREEN >>> c GREEN >>> c.rgb 65280 >>> c.ordinal 1 >>> c.name GREEN
Kotlin中,类能够嵌套。一个类能够嵌套在其余类中,并且能够嵌套多层。
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 } } } }
测试代码:
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() 访问嵌套类的方式是直接使用 类名.
来访问, 有多少层嵌套,就用多少层类名来访问。
普通的嵌套类,没有持有外部类的引用,因此是没法访问外部类的变量的:
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 } } } }
若是一个类Inner想要访问外部类Outer的成员,能够在这个类前面添加修饰符 inner。内部类会带有一个对外部类的对象的引用:
package com.easy.kotlin class 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() }
咱们能够看到,当访问inner class Inner
的时候,咱们使用的是Outer().Inner()
, 这是持有了Outer的对象引用。跟普通嵌套类直接使用类名访问的方式区分。
匿名内部类,就是没有名字的内部类。既然是内部类,那么它天然也是能够访问外部类的变量的。
咱们使用对象表达式建立一个匿名内部类实例:
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接口:
@FunctionalInterface public interface Runnable { public abstract void run(); }
咱们可使用lambda表达式建立它,下面的几种写法都是能够的:
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表达式以及函数式编程相关内容,咱们将在下一章节中介绍。