Kotlin 的类和接口在概念上跟 Java 是同样的,可是用法存在一些差异,好比继承的写法、构造函数和可见性修饰符的不一样等,此外还有一些 Java 中没有的概念,如数据类、密封类、委托和 object 关键字等。下面从类和接口的定义开始,感觉一下 Kotlin 的非凡之处吧!程序员
跟 Java 同样,Kotlin 使用 class 关键字来定义一个类。express
class Animal {
fun eat() {
...
}
fun move() {
...
}
}
复制代码
在 Java 中,一个类除了被手动加上 final 关键字,它都能被任意一个类继承并重写它的非 final 方法,这就可能会致使某些子类出现不符合其父类的设计初衷,特别是在多人协做的开发环境下。编程
这类问题被 Kotlin 语言设计者注意到了并切引发了他们的重视,所以,在 Kotlin 中的类和方法默认都是 final 的,若是要继承或者重写一个类和方法,必须将他们显式地声明为 open。安全
open class Animal {
fun eat() {
...
}
open fun move() {
...
}
}
复制代码
继承该类的时候,须要在类名后面加上冒号后再写被继承的类名,在 Kotlin 中使用冒号代替了 Java 中的 extend
关键字。编程语言
class Dog : Animal {
override fun move() {
...
}
}
复制代码
一样,Kotlin 中可使用 abstract 关键字将一个类声明称抽象类,但它不能被实例化。抽象方法也能够覆盖父类的 open 方法,抽象方法始终是 open 的且必须被子类实现。ide
abstract class Bird : Animal {
abstract fun song()
override abstract fun move()
}
复制代码
Kotlin 中的接口一样使用 interface 关键字来定义,能够在方法中加入默认的方法体。函数
interface Machine {
fun component()
fun control() {
...
}
}
复制代码
与类的继承相似,实现/继承接口方法是在类名/接口名后面加上冒号再写被实现/继承的接口名。ui
实现接口:this
class Electric : Machine {
override fun component() {
...
}
override fun control() {
...
}
}
复制代码
继承接口:spa
interface Computer : Machine {
fun IODevice()
}
复制代码
可见性修饰符用于声明一个类或者接口的可见范围,相似于 Java,Kotlin 中使用 public、private 和 protected 关键字做为可见性修饰符。跟 Java 不一样的是,Kotlin 默认的可见性是 public 的,而且没有 “包私有” 的概念,同时新增 internal 关键字用来表示 “模块内部可见“。
public 是 kotlin 默认的可见性修饰符,表示任何地方可见。
使用 internal 修饰符表示只在模块内部可见。
一个模块就是一组一块儿编译的 Kotlin 文件。这有多是一个 Intellij IDEA 模块、一个 Eclipse 项目、一个 Maven 或 Gradle 项目或者一组使用调用 Ant 任务进行编译的文件。
Kotlin 将构造方法分为了主构造方法和次构造方法,主构造方法做为类头在类体外部声明,次构造方法在类体内部声明。
主构造方法做为类头的一部分存在,它跟在类名(与可选的类型参数)后。
class Animal constructor(name: String) {
...
}
复制代码
若是主构造方法没有可见性修饰符或者注解,则能够省略 constructor 关键字。
class Animal(name: String) {
...
}
复制代码
主构造方法中不能包含其它任何代码,所以,若是须要初始化代码,则须要把这一部分代码放在以 init 关键字做为前缀的**初始化块(initializer blocks)**中。
class Animal(name: String) {
init {
val outPutName = "It's the Animal call: $name"
}
}
复制代码
若是类中的某个属性使用主构造方法的参数来进行初始化,能够经过使用 val 关键字对主构造函数的参数进行修饰,以简化代码。
class Animal(val name: String) {
init {
val outPutName = "It's the Animal call: $name"
}
fun showName() {
println(name)
}
}
复制代码
次构造方法在类体内使用 constructor 关键字进行定义,若是类有一个主构造方法,每一个次构造方法须要委托给主构造方法, 能够直接委托或者经过其它次构造方法间接委托。委托到同一个类的另外一个构造方法用 this 关键字便可。
class Animal(val name: String) {
init {
val outPutName = "It's the Animal call: $name"
}
// 直接委托给主构造方法
constructor(name: String, age: Int): this(name) {
...
}
// 经过上面的构造方法间接委托给主构造方法
constructor(name: String, age: Int, type: Int): this(name, age) {
...
}
}
复制代码
初始化块中的代码实际上会成为主构造函数的一部分。委托给主构造函数会做为次构造函数的第一条语句,所以全部初始化块中的代码都会在次构造函数体以前执行。即便该类没有主构造函数,这种委托仍会隐式发生,而且仍会执行初始化块。
在一个类内部定义的另一个类默认为嵌套类。
class OuterClz {
var num = 1
class NestedClz {
fun show() {
// 编译报错
println(num)
}
}
}
复制代码
嵌套类不持有它所在外部类的引用。
在 class 关键字前面加上 inner 关键字则定义了一个内部类。
class OuterClz {
var num = 1
inner class InnerClz {
fun show() {
// 编译经过
println(num)
}
}
}
复制代码
内部类持有了它所在外部类的引用,所以能够访问外部类的成员。
在 Java 中静态内部类是不会持有外部类引用的,至关于 Kotlin 的嵌套类;而非静态内部类则持有外部类的引用,至关于 Kotlin 的内部类。
有时候为了类型安全,须要将某个属性全部可能的值枚举出来,开发者只能使用该枚举类中定义的枚举常量。
定义一个枚举类须要用到 enum 和 class 关键字。
enum class Color {
RED, GREEN, BLUE
}
复制代码
与 Java 相同,枚举类中能够声明属性和方法。
enum class Color(val r: Int, val g: Int, val b: Int) {
RED(255, 0, 0),
GREEN(0, 255, 0),
BLUE(0, 0, 255);
fun rgb () = Integer.toHexString((r * 256 + g) * 256 + b)
}
复制代码
当声明枚举常量的时候,须要提供该常量所需的属性值,而且须要在声明完成后加上分号。
Kotlin 中的 when 可使用任何对象,所以,可使用 when 表达式来判断枚举类型。
var myColor = Color.RED
when (myColor) {
Color.RED -> println("It's a red color")
Color.GREEN -> println("It's a green color")
Color.BLUE -> println("It's a blue color")
}
复制代码
须要注意的是,若是在 when 表达式中没有 case 到全部的枚举常量,编译器并不会报错。
var myColor = Color.RED
when (myColor) {
Color.RED -> println("It's a red color")
Color.GREEN -> println("It's a green color")
}
复制代码
可是会建议你处理全部可能的状况(添加 “BLUE” 分支或者 “else” 分支)。
'when' expression on enum is recommended to be exhaustive, add 'BLUE' branch or 'else' branch instead
若是一个父类 Animal 只有两个分别是 Bird 和 Dog 的子类,在 when 表达式中处理全部状况的时候若是代码写成以下:
open class Animal {
fun doSomething() {
}
}
class Bird(val name: String) : Animal()
class Dog(val name: String) : Animal()
fun main() {
fun showName(animal: Animal) =
when (animal) {
is Dog -> println("It's the dog name ${animal.name}")
is Bird -> println("It's the bird name ${animal.name}")
}
}
复制代码
这时编译器会提示错误:必须加上 else
分组。
这时候,若是想写出简洁的代码,密封类就派上用场了。
密封类用来表示受限的类继承结构:当一个值为有限集中的类型、而不能有任何其余类型时。在某种意义上,他们是枚举类的扩展:枚举类型的值集合也是受限的,但每一个枚举常量只存在一个实例,而密封类的一个子类能够有可包含状态的多个实例。
密封类的定义须要在类名前面加上 sealed 关键字。
sealed class Animal
复制代码
使用密封类的时候须要注意几点:
使父类变成密封类以后,意味着对可能建立的子类作出了限制,when 表达式中处理了全部 Animal 类的子类的状况,所以不须要 “else” 分支。
sealed class Animal {
fun doSomething() {
}
}
class Bird(val name: String) : Animal()
class Dog(val name: String) : Animal()
fun main() {
fun showName(animal: Animal) =
when (animal) {
is Dog -> println("It's the dog name ${animal.name}")
is Bird -> println("It's the bird name ${animal.name}")
}
}
复制代码
当你为 Animal 类添加一个新的子类且没有修改 when 表达式内容的时候的时候,IDE 会编译报错提醒你没有覆盖全部状况。
使用 Java 的时候,难免会须要一些 Entity 类来承载数据,有时候还须要重写 toString、equals 或者 hashCode 方法,而这些方法的写法千遍一概,有些 IDE 还可以自动生成这些方法。可是 Kotlin 中的数据类可以很好地避免这些状况,使代码看起来更加简洁。
数据类的定义须要在类名前面加上 data 关键字。
data class User(val name: String, val gender: Int, val age: Int)
复制代码
数据类的定义必须保证如下条件:
数据类定义完成以后,编译器自动从主构造函数中声明的全部属性导出如下成员:
equals() 和 hashCode() 方法:
一般用于对象实例之间的比较。
toString() 方法:
fun main() {
var user = User("guanpj", 1, 18)
println(user.toString())
}
复制代码
输出结果为:User(name=guanpj, gender=1, age=18)
componentN 函数:
componentN 称为解构函数,简单来讲就是把一个对象解构成多个变量以便使用。在 data 类中若是有 N 个变量,则编译器会生成 N 个解构函数(component一、component2 ... componentN)按顺序对应这 N 个变量。使用方法以下:
fun main() {
var user = User("guanpj", 1, 18)
var (name, gender, age) = user
println("My name is $name and I'm $age years old.")
}
复制代码
输出结果:My name is guanpj and I'm 18 years old.
须要注意的是,data 类中的这些 componentN 函数不容许提供显式实现。
copy() 函数:
使用 copy() 函数可以生成一个与该对象具备相同属性的对象,而且能够修改部分属性。
fun main() {
var user = User("guanpj", 1, 18)
var newUser = user.copy("gpj")
println("My name is ${newUser.name} and I'm ${newUser.age} years old.")
}
复制代码
输出结果为:My name is gpj and I'm 18 years old.
一样,copy() 函数也不容许提供显式实现。
虽然 ”委托“ 这个设计思想在各个编程语言中都或多或少地有所体现,可是 Kotlin 直接在语法上对 委托模式作了支持,Kotlin 支持类层面的委托和属性的委托,下面分别从这个两个方面讲解委托模式在 Kotlin 中的使用。
前面已经提到过,Kotlin 在设计之初就考虑到因继承带来的 “脆弱的基类” 问题,所以把类默认视做 final 类型的,当须要扩展某些类的时候,手动将它们标记成 open 而且在扩展的过程当中注意兼容性。
假设你有一个需求,须要统计一个集合添加元素的次数,你使用一个 CountingSet 来实现 MutableCollection 接口,并扩展了 add() 和 addAll() 方法,其余方法直接交给成员变量 innerSet 来处理。
class CountingSet<T>() : MutableCollection<T> {
val innerSet: MutableCollection<T> = HashSet<T>()
var objectsAdded = 0
override fun add(element: T) : Boolean {
objectsAdded++
return innerSet.add(element)
}
override fun addAll(c: Collection<T>): Boolean {
objectsAdded += c.size
return innerSet.addAll(c)
}
override val size: Int
get() = innerSet.size
override fun contains(element: T): Boolean = innerSet.contains(element)
override fun containsAll(elements: Collection<T>): Boolean = innerSet.containsAll(elements)
override fun isEmpty(): Boolean = innerSet.isEmpty()
override fun clear() = innerSet.clear()
override fun iterator(): MutableIterator<T> = innerSet.iterator()
override fun remove(element: T): Boolean = innerSet.remove(element)
override fun removeAll(elements: Collection<T>): Boolean = innerSet.removeAll(elements)
override fun retainAll(elements: Collection<T>): Boolean = innerSet.retainAll(elements)
}
复制代码
能够看到,除了 add() 和 addAll() 方法,其余全部的方法都须要重写并交给 innerSet 去实现,这样势必会产生比较多的模板代码,使用类委托即可以很好地解决这个问题。
class CountingSet<T>(val innerSet: MutableCollection<T> = HashSet<T>())
: MutableCollection<T> by innerSet {
var objectsAdded = 0
override fun add(element: T) : Boolean {
objectsAdded++
return innerSet.add(element)
}
override fun addAll(c: Collection<T>): Boolean {
objectsAdded += c.size
return innerSet.addAll(c)
}
}
复制代码
经过在超类型列表中使用 by 关键字进行委托,编译器将会生成转发给 innerSet 的全部 MutableCollection 中的方法,若是有覆盖方法,编译器将使用覆盖的方法而不是委托对象中的方法。
一样,一个属性也能够将它的访问器逻辑委托给一个辅助对象,属性委托的写法为:
val/var <属性名>: <类型> by <表达式>
代码表示以下:
class MyClz {
var p: String by Delegate("abc")
}
复制代码
对于 val 和 var 类型的属性来讲,它的 get()(和 set())方法将会被委托给 Delegate 类的 getter()(和 setter)方法。
class Delegate<T>(default: T) {
private var value = default
operator fun getValue(thisRef: Any?, property: KProperty<*>): T {
return value
}
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
this.value = value
}
}
复制代码
使用属性委托须要注意:
KProperty<*>
或其超类型。另外,Kotlin 提供了 ReadOnlyProperty 和 ReadWriteProperty 接口以方便实现 val 和 var 属性的委托,只需实现这两个接口并重写它的方法便可。
class Delegate<T>(default: T) : ReadWriteProperty<Any?, T> {
private var value = default
override fun getValue(thisRef: Any?, property: KProperty<*>): T {
return value
}
override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
this.value = value
}
}
复制代码
Kotlin 中的 object 被视为对象,可是跟面向对象中的 ”对象“ 却不太同样,它的功能很是强大,它能够定义一个单例、实现相似 Java 中的静态方法的功能以及建立匿名内部类,Kotlin 中的 object 是拥有某个具体状态的实例
,出事化后不会再改变。
应该不少人跟我同样,刚从 Java 转过 Kotlin 的时候都会遇到一个疑惑:Kotlin 中是怎样定义单例的?事实上,经过对象声明,在 Kotlin 中定义单例简直易如反掌,经过 object 关键字,能够定义一个对象声明。
object DataManager {
val data: Set<Person> = setOf()
fun doSomething() {
for (person in data) {
...
}
}
}
复制代码
与变量同样,对象声明容许你使用对象名加.字符的方式来调用方法和访问属性:
fun main() {
DataManager.data
DataManager.doSomething()
}
复制代码
对象声明具备如下特色:
基于以上几点,对象声明彻底符合单例模式的要求。
一样的,从其余语言转到 Kotlin 的程序员可能会遇到另一个问题 —— Kotlin 中怎样在一个类中定义一个静态方法?在 Kotlin 中,若是想要直接经过容器类名称来访问这个对象的方法和属性的能力,再也不须要显式地指明对象的名称,就须要使用到伴生对象的概念了,伴生对象的定义以下:
class MyClz {
companion object {
var myVariable = "My variable"
fun doSomething() {
...
}
}
}
复制代码
如今就能够像 Java 中调用静态变量和静态方法的方式同样调用 myVariable 变量和 doSomething() 方法了!
fun main() {
MyClz.myVariable
MyClz.doSomething()
}
复制代码
与对象声明 同样,伴生对象也能够实现接口。
interface MyInterface {
fun doSomething()
}
class MyClz {
companion object : MyInterface {
override fun doSomething() {
...
}
}
}
fun main() {
// MyClz 类的名字能够被看成 MyInterface 实例
var myInstant: MyInterface = MyClz
myInstant.doSomething()
}
复制代码
object 关键字不只仅能用来声明单例模式的对象,还能用来声明匿名对象,与 Java 匿名内部类只能扩展一个类或实现一个接口不一样 , Kotlin 的匿名对象能够实现多个接口或者不实现接口,咱们称之为对象表达式。
interface MyInterface {
fun doSomething()
fun doOtherthing()
}
val myInstant = object : MyInterface {
override fun doSomething() {
...
}
override fun doOtherthing() {
...
}
}
复制代码
另一个跟 Java 不一样的点就是,对象表达式中能够直接访问建立它的函数中的非 final 变量。
class MyClz {
var mVariable = "This is my variable."
val myInstant = object : MyInterface {
override fun doSomething() {
println(mVariable)
}
override fun doOtherthing() {
}
}
}
复制代码
使用对象声明能够定义一个单例类;
使用伴生对象能够实现相似 Java 中调用静态变量和静态方法的功能;
对象声明和伴生对象均可以实现接口;
对象表达式能够做为 Java 中匿名内部类的替代品,而且使用起来更加方便。
对象声明是在第一次被访问到时延迟初始化的,以后访问不会被初始化、对象表达式是在每次使用到的时候当即初始化并执行的,每次都会建立一个新的对象、伴生对象是在相应的类被加载(解析)的时候初始化的。