Kotlin的类和接口与java的类和接口还是有一点区别的。例如,接口可以包含属性声明。与java不同,Kotlin的声明默认是final和public的。此外嵌套类默认并不是内部类:它们并没有包含对其外部类的隐式引用。
Kotlin的接口和java8中的相似,它们可以包含抽象的方法的定义以及非抽象方法的实现,但是它们不能包含任何状态。我们也同样使用interface关键字来声明一个接口,例如声明一个接口Clickable:
interface Clickable{
fun click()
}
当我们声明了接口Clickable之后,所有实现这个接口的类都需要提供这个方法的实现,例如:
class Button : Clickable{
override fun click() {
//TODO 自己的实现
}
}
在Kotlin当中,在类后面用“ : ”来代替java中的extends和implements关键字。方法前面加 override 来替代java当中的 @override来标注被重写的父类或者接口中的方法和属性。在Kotlin中如果重写 就必须加上 关键字 override,不能省略。我们在接口中可以定义自己的默认实现。如果子类不需要重写这个方法,可以直接使用接口中的方法,子类也可以有自己的实现。
有这一种情况,假如我们在Kotlin中有下面两个接口Clickable和Focusable,都有一个show方法的默认实现:
// Kotlin 中的定义
interface Clickable{
fun show() = {
//clickable的默认实现
}
}
interface Focusable{
fun show()={
//Focusable的默认实现
}
}
//=========java中的定义(必须在java8中才能使用默认实现default)========
public interface Clickable {
default void showoff(){
//默认实现
}
}
public interface Focusable {
default void showoff(){
//默认实现
}
}
如果我们定义一个子类去同时实现这个接口,Kotlin 和java中 子类必须实现自己的showoff方法,否则编译不通过。但是在调用super关键字的时候,有下面的些许的不同。而Kotlin1.0的设计是基于java6设计的,java6还不支持接口中的默认方法。因此在Kotlin中的接口默认方法会编译成一个普通的接口和一个将方法体作为静态函数的类的结合体。
//==========java=========
public class ClickFocusable implements Clickable,Focusable{
@Override
public void showoff() {
Clickable.super.showoff();
Focusable.super.showoff();
}
}
//========kotlin========
class ClickFocusable : Clickable,Focusable{
Override void showoff() {
super.showoff();
super.showoff();
}
}
在java中允许创建任意类的子类并且重写相关的方法,除非你显式的对类使用关键字final 进行标注。在java中如果对于继承和实现没有定义好自己的规则的话,很容易造成风险的,方法被重写或者乱调用啊...。Kotlin默认的将类和方法都默认标记为final类型,不允许轻易的继承和修改。如果允许当前类可以被继承就用open关键字标识它:
open class Button:Clickable{ //这个类是open的 其他类可以继承
fun disable(){ } //这个函数是final的不能被子类重写
open fun show() { } //这个函数是open的可以被子类重写
override fun click() { } //这个函数重写了一个open函数 并且它本身也是open的
}
如果你重写了一个接口或者类中的成员,重写的成员同样默认是open的。如果我们不想让当前的类的重写方法再被子类重写实现,可以显示的在前面加上final。例如我们不想click再被Button的子类重写,就可以这样做 final override fun click(){ };
在kotlin中,和java一样,可以将一个类声明为abstract,这种类不能被实例化,只能被继承。一个抽象类,至少包含一些没有实现并且必须被子类重写的抽象成员,抽象成员始终是open的,因此可以省略掉默认的open 关键字。
修饰符 | 相关成员 | 评注 |
---|---|---|
final | 不能被重写 | 类中成员默认使用 |
open | 可以被重写 | 需要明确的标明 |
abstract | 必须被重写 | 只能在抽象类中使用:抽象成员不能有实现 |
override | 重写父类或者接口中的成员 | 如果没有使用final表明,重写的成员默认是开放的 |
在java中默认没有修饰符,也就是说java中默认的可见性--包私有。但是Kotlin中默认的修饰符(省略了修饰符)是public的。作为替代方案,Kotlin中提供了一种新的修饰符,internal,表示“只在模块内部可见”。一个模块就是一组一起编译的kotlin文件。
internal可见性的优势在于它提供了对模块实现细节的真正封装。使用java的时候,这种封装很容易被破坏(默认包私有),因为外部代码可以将类定义到与你代码相同的包中,从而得到访问你的包私有声明的权限。另外kotlin允许在顶部申明中使用private可见性,包括类,函数和属性。这些声明只会在声明他们的文件中可见。
修饰符 | 类成员 | 顶层声明 |
---|---|---|
public(默认) | 所有地方可见 | 所有地方可见 |
internal | 模块中可见 | 模块中可见 |
protected | 子类中可见 | -- |
private | 当前类中可见 | 文件中可见 |
Kotlin中遵循一个可见性的通用规则:类的基础类型和类型参数裂变中用到的所有类,或者函数的签名都有与这个类或者函数本身相同的可见性。这个规则可以确保在需要调用函数或者继承一个类时候能够施展访问到所有类型。
注意。protected修饰符在java和Kotlin中有不同的行为。在java中,可以从同一个包中访问一个protected成员,但是在Kotlin中不允许,在kotlin中protected成员只在当前类和它的子类中可见,而且kotlin中,类的扩展函数不能访问它的private 和 protected成员。
和java一样,在kotlin中可以在一个类中声明另外一个类。这样做在封装一个辅助类或者把一些代码放到靠近它被使用的地方时非常有用。区别就是Kotlin的嵌套类不能访问外部类的实例。
设想一个java中的例子,定义了一个View,它的状态是可序列化的,我们做了一个辅助类,将View需要序列化的数据放进去,声明State接口去实现Serializable。View的接口声明了可以用来保存视图状态的getCurrentState和restoreState方法。
interface State : Serializable
interface View{
fun getCurrentState() : State
fun restoreState(state:State){
}
}
可以方便的定义一个保存按钮状态的Button类。先看看java代码中是怎么实现的:
/**java 实现***/
public class Button implements View{
@override
public State getCurrentState(){
return new ButtonState();
}
@override
public void restoreState(State state){
//TODO
}
public class ButtonState implements State{
}
}
定义一个实现了State接口的ButtonState类,并且持有Button的特定引用(非静态内部类持有外部类的引用)。在getCurrentState方法创建了一个ButtonState的实例。
在实际使用当中会出现java.io.NotSerializable Exception:Button异常。为什么呢?因为在java中,当你在另外一个类中声明一个类的时候,它会变成内部类,上面的ButtonState就变成内部类,而这个内部类ButtonState会隐式的持有外部Button类的引用。而Button类是不能被序列化的,所以就出现上面的Exception。要解决这个问题,可以将ButtonState声明为static的。这样ButtonSate就不会持有外部类的引用了。
在Kotlin中这一默认行为是相反的。Kotlin中没有显示修饰的嵌套类与java中的static嵌套类是一样的,不会持有外部类的一个隐式引用。如果要把它变成一个内部类来持有外部类的引用的话,可以在前面加inner修饰符。
类A在另一个类B中声明 | 在java中 | 在Kotlin中 |
---|---|---|
嵌套类(不存储外部类的引用) | static class A | class A |
内部类(存储外部类的引用) | class A | inner class A |
在Kotlin中引用外部类实例的语法与java不同。需要使用this.@Outer从Inner类中访问Outer类:
class Outer{
inner class Inner{
fun getOutterReference():Outer = this.@Outer
}
}
Kotlin中通过sealed关键字来修饰一个类为密封类,若要继承密封类,必须将子类定义在同一类文件中,其他文件中的类将无法继承。例如可以用下面2种方式继承,Sealed形容的密封类具有局限性,即它不能被初始化,因为它背后其实是基于一个抽象类实现的。
sealed class Bird {
open fun fly() = "I can Fly"
class Eagle:Bird()
}
class SbuBird : Bird() {
}
在java中一个类可以声明多个构造方法,也可以有自己默认的构造方法。在Kotlin中做出了一点修改,区分了主构造方法和从构造方法
同样也允许初始化语句块中添加额外的初始化逻辑(有点类似java中的静态代码块)。
先定义一个简单的类User
class User(var name : String)
这个类的定义与java类的定义看起来不一样少了一对大括号,多了一对小括号和变量。其实这一段被括号围起来的的语句块叫做"主构造方法"。它主要有2个目的,一个是表明构造方法的参数,二是定义使用这些使用参数初始化的属性(也就是说类中有个属性名字叫name,类似java中的this.name = name)。其实上面的User类也可以用下面的定义来代替
class User constructor(mName : String){ //<---带一个参数的主构造方法
val name : String
init{ //<---初始化语句块
name = mName
}
}
上面使用到两个关键字constructor 和 init:
constructor : 主要用来声明构造方法(主构造方法和从构造方法)的。
init: 引入一个初始化代码语句块的。这个语句块中包含了在类被创建时候执行的代码,并会和主构造函数一起使用。因为主构造方法有语法限制,所以需要初始化语句块来帮主构造函数在某些时机打破这个限制。
在上例中不需要把初始化代码放在初始化语句块中,因为它可以与name属性的声明结合。如果主构造方法没有注解或可见性修饰符,同样可以去掉constructor关键字。如果属性用相应的构造方法来初始化,代码可以把关键字val 加在参数面前来初始化。就可以用下面两种方式了
class User(_name : String){ //带一个参数的主构造方法
val name = _name //用参数来初始化属性
}
//最简洁的形式
class User(var name :String) //val 意味着相应的属性会用构造方法的参数来初始化
还可以给构造方法的参数声明一个默认值,例如
//构造方法 提供一个默认的参数值
class User(val name :String ,val isSubScribed : Boolean = true)
特殊的,我们如果不想类被其他代码实例化,必须把构造方法标记为private
//在实现单例的时候常用
class Secretive private constructor(){ //private的构造函数
}
注意:如果所有的构造方法参数都有默认值,编译器会生成一个额外的不带参数的构造方法来使用所有的默认值。这样可以使Kotlin使用库变得更加简洁。
Kotlin中引入了一种叫做init语句块的语法,类似java中 静态代码块,它属于上述构造方法的一部分,两者在表现形式上市分离的。例如下面的Bird类:
class Bird(var weight:Double,var age:Int ,var color:String) {
init{
println("the weight is $weight")
}
}
在init代码块中我们可以对属性(上面的weight,age..)赋值,或者需要在类初始化的时候做一些其他的处理,都可以在init语句块中执行。
Kotlin中主要使用by lazy和lateinit这2种语法来实现延迟初始化的效果,继续改造上面的Bird类:
class Bird(val weight:Double,val age:Int ,val color:String) {
val sex : String
}
在上面的Bird类中我们假定,我们的鸟类可以根据颜色来确定性别sex,所以定义了一个val sex 属性,但是如果我们将sex用val来修饰的话,我们就必须在实例构造的时候初始化,改造成这样
class Bird(val weight:Double,val age:Int ,val color:String) {
val sex : String
init{
sex = "女"
}
}
其实写成这样是多次一举,还不如直接在后面赋值
val sex : String = if(color=="yellow") "male" else "female"
可是有的时候,我们不需要知道这个属性值,也就没必要去初始化它了,因为我们不用,我们希望的是在用到的时候就去初始化它,那么这个时候延迟初始化就派上用场了:
class Bird(val weight:Double,val age:Int ,val color:String) {
// val sex : String = if(color=="yellow") "male" else "female"
val sex:String by lazy{
if(color=="yellow") "male" else "female"
}
}
系统默认会给lazy属性加上同步锁,也就是说对他的读写是线程安全的,也即是
val sex:String by lazy(LazyThreadSafetyMode.SYNCHRONIZED){
if(color=="yellow") "male" else "female"
}
LazyThreadSafetyMode.SYNCHRONIZED:加上同步锁,保证线程安全的。这也是默认值
LazyThreadSafetyMode.PUBLICATION:在你确定并行没有问题的情况下,可以使用这个参数
LazyThreadSafetyMode.NONO:这个参数不会使用任何线程开销,当然也不保证线程安全
by lazy语法特点如下:
1.该变量必须是引用不可变的,也就是说不能用var来申明。
2.在被首次调用时,才会进行赋值操作。一旦被赋值,后续将不再更改。
lateinit的语法特点如下:
1.lateinit 使用var来声明属性
2.语法使用与by lazy有点不同 lateinit var sex:String 直接在变量前面以关键字的形式修饰就行
3.lateinit主要用于var声明的变量,然后它不能用于基本数据类型,如Int ,Long等,我们需要使用它们对应的包装类Integer等来代替。
通常来讲,使用多个构造方法的类在Kotlin代码中不如在java中常见了,因为大多数java中需要用重载方法的场景都被Kotlin支持默认参数默认值和参数命名的语法涵盖了。
在Kotlin中,接口可以包含抽象属性声明。例如在User接口中提供一个取得nickname值得方式,由于接口本身不包含任何状态,所以事先User的接口需要自己实现对nickname的取值方法。
interface User{
val nickname : String
}
上面说到接口不包含任何状态,User的子类需要自己实现取nickname属性的方法,下面有几种不同实现形式:
class PrivateUser(override val nickname:String) : User //<-- 主构造方法属性,默认生成
//getNickName的getter方法
class SubscribingUser(val email : String) :User{
override val nickname : String
get() = email.subStringBefore('@') //自定义取值的getter方法
}
class FacebookUser(val accountId : int) : User{
override val nickname = getFacebookName(accountId) //属性初始化
}
对于PrivateUser来说,使用了简洁的语法直接在主构造方法中声明了这个属性,所以标记nickname为override.
对于SubscribingUser来说,nickname 是通过一个自定义的getter方法来实现。
对于FacebookUser来说,在初始化时将nickname属性与值关联。
除了抽象属性声明外,接口还可以包含具有getter和setter属性,例如
interface User{
val email : String
val nickname : String
get() = email.substringbefore('@')
}
接口User包含抽象属性email,同时nickname属性有一个自定义的getter。属性email必须在子类重写,而属性nickname可以被继承。
访问器的可见性默认与属性的可见性相同,但是如果需要可以通过get和set关键前放置可见性修饰符的方式来修改它。
class LengthCounter{
var count : Int = 0
private set
fun addWord(word : String){
count += word.length
}
}
可以通过在set方法前面加修饰符private来限制可见性。
java平台定义了一些需要在许多类中呈现的方法,并且通常是以一种很机械的方式实现的,譬如equals,hashCode,toString。比较幸运的是,java的IDE可以将这些方法的生成自动化,不需要我们手动去书写它。
和java类似,所有的Kotlin类也有许多你想 并且可以重写的方法:toString ,equals , hashCode
以下面的Client为例,看看这几个方法的重写:
class Client(val name : String, val postalCode : Int)
字符串表示:toString()
默认打印一个类的实例对象的时候,都会是这样的形式,形如Client@5e9f23b4,但是重写了toString()方法之后就不一样了
class Client (val name :String,val postalCode : Int){
override fun toString() = "client(name=$name,postalCode = $postalCode)"
}
对象相等性:equals()
//上文中
>>> val client1 = Client("Alice",342562)
>>> val client2 = Client("Alice",342562)
>>>println(client1 == client2)
输出结果 : false
在Kotlin中,==检查对象是否相等,而不是比较引用。会编译成equals。
注意 : 在java中,可以使用==运算符来比较基本数据类型和引用类型。如果应用在基本数据类型上,java的== 比较的是值,然而运用在引用类型上 == 比较的是引用。因此,我们一般都是调用equals这样比较明确的方法。
在Kotlin中,== 运算符是比较两个对象的默认方式:本质上说它就是通过调用 equals来比较两个值得。因此如果重写了equals方法,就可以很安全的使用==来进行实例比较。如果想要进行引用比较,可以使用===运算符,这与java中的==比较对象引用是一个效果。
Hash容器:hashCode()方法
其实在java代码中,我们也一直强调,当你重写了对象的equals方法的时候,也必须重写对象的hashCode方法,这其实还是有一些道理的。看看下面的例子
>>>val processed = hashSetOf(Client("Alice",123465)
>>>println(processed.contains(Client("Alice",123465)
false
以上为啥会返回false呢?最主要的原因是出在hashCode方法上面。有一个hashCode契约:如果两个对象相等,他们必须有相同的hashCode值。因为在集合的检查当中,很多都是先检查对象的hashCode值得。想在hashSet中检查set当中是否有这个对象,首先比较当前对象与已经存在的对象的hashCode值是否相等。不相等的话,肯定不存在。相等的话不一定存在,因为我们不同的对象生成的HashCode值可能相等,最后还得比较equals方法。所以为了避免这种困扰,需要重写equals的时候,同时重写hashCode方法
class Client(val name : String , val postalCode : Int){
....
override fun hashCode() : Int = name.hashCode*31 + postalCode
}
在java中需要定义一个数据实体类的时候,我们有时候有那种需要重写toString equals 和 hashCode方法的时候。虽然有javaIDE 可以很方便便捷的帮我们生成他们。但是Kotlin有更简单便捷的方法帮我们生成,我们不必再去重写这些方法了,使用data修饰符
data class Client(val name : String ,val postalCode : Int)
使用data修饰符,就可以得到一个重写了所有标准java方法的类
equals用来比较实例
hashCode用来生成对象的hash值,用于基于HashMap这种集合的容器的键
toString 用来为类生成按声明顺序的所有字段字符串的表达形式
Kotlin中,object关键字在多种情况下出现,但是遵循同样的核心理念:这个关键字定义一个类并同时创建一个实例(换句话说就是一个对象),它有不同的使用场景:
1.对象声明是定义单例的一种方式。
2.伴生对象可以持有工厂方法和其它与这个类相关,但在调用时并不依赖类实例的方法。它们的成员可以通过类名来访问。
3.对象表达式用来替代java的匿名内部类
在java中我们创建单例,都是私有构造方法加上静态get实例的方法相结合。Kotlin中通过对象声明将类的声明与该类的单一实例声明结合在一起,使实现单例变得更加容易。
例如,可以用一个对象声明来表示一个组织的工资单,一般不会有多个工资单,所以可以采用单例的形式实现
object Payroll{
val allEmployees = arrayListOf()
fun calculateSalary(){
for(person in allEmployees){
.....
}
}
}
对象声明通过object关键字引入。一个对象声明可以非常高效的声明一个类和这个类的一个实例。与类一样,一个对象的声明可以包含属性,方法,初始化语句块,唯一不允许的就是构造方法(主构造方法和从构造方法都不允许)。与普通类的声明不同。对象声明,在声明的时候已经创建了一个类的实例,因此就不要再调用构造方法了。
Payroll.allEmployees.add(Person()) //对象声明的调用
Payroll.calculateSalary()
对象的声明通常可以继承类和实现接口。这通常在你使用的框架 要实现一个接口,而且这个实现不包含任何状态的时候非常有用。看一个比较器的例子
object CaseInsensitiveFileCompartor : Comparator{
override fun compara(file1 : File ,file2 : File) : Int{
return file1.path.compareRo(file2.path,ignoreCase = true)
}
}
//使用
>>>println(CaseInsensitiveFileComparator.compare(File("/User"),File("/User")
... 0
//可以在任何使用普通对象的地方使用单例对象
>>> val files = listOf(File("/Z",File("/a"))
>>>println(files.sortedWith(CaseInsensitiveFileComparator))
[/a,/Z]
可以在类中声明一个对象,这样的对象同样只有一个单一实例。它们在每个容器类的实例中并不具有不同的实例。
data class Person(val name : String){
object NameComparator : Comparator{
override fun compare(p1 : Person,p2 :Person) : Int{
return p1.name.compare(p2.name)
}
}
}
注意:在Java中使用Kotlin对象,Kotlin中的对象声明被编译成了通过静态字段来持有它的单一实例的类,这个字段名字始终都是INSTANCE。如果在java中实现单例模式,也许我们会做同样的事情。因此从java中使用Kotlin对象,可以通过访问静态的INSTANCE字段,像下面这样调用上面的CaseInsensitiveFileComprator,例子中的INSTANCE就是CaseInsensitiveFileComprator类型
CaseInsensitiveFileComparator.INSTANCE.compare(file,file2)
在Kotlin中不能拥有静态成员;java的static关键字并不是Kotlin语言的一部分。作为代替,Kotlin依赖包级别函数(在多数情况下能够替代java的静态方法)和对象声明(在其他情况下替代java的静态方法,同时还包括静态字段)。其实在大多数情况下,还是推荐使用顶层函数,但是顶层函数不能访问类的private成员,因此如果需要写一个可以在没有类实例的情况下调用,但是需要访问类内部的函数,可以将其写成那个类中的对象申明的成员。比较好的例子就是工厂方法。
class A {
companion object {
fun bar(){
println("companion object called")
}
}
}
>> A.bar();
输出:companion object called
在一个类中如果用companion包裹一些方法或者属性的话,我们可以直接用类的名称调用这些方法或者属性,不再需要显式的指明一个对象去调用这些方法或者属性了,类似于java中的静态方法。
工厂方法可以作为一个使用伴生对象的很好的实例,还是用上文中的User,FackbookUser,SubscribingUser为例,使用伴生对象来替代从构造方法的用法
//定义多个从构造方法的类
class User{
val nickname : String
constructor (email : String){ //从构造函数
nickname = email.substringBefore('@')
}
constructor(facebookAccountId : Int){
nickname = getFackbookName(facebookAccountId)
}
}
上文中的方法可以实现多个从构造方法实现构造不同的User实例,但是看起来不那么简洁明了,不知道那个构造函数是用来构造哪一种具体子类的方法。但是使用工厂方法的替代从构造方法来创建类的实例,就简洁明了多了,而且只需要一个对象来管理这些工厂方法
//使用工厂方法来替代从构造函数
class User private constructor(val nickname : String){
companion object{
fun newSubscribingUser(email : String)
= User(email.substringBefore('@'))
fun newFacebookUser(accountId : Int)
= User(getFacebookName(accountId))
}
}
作为普通对象使用伴生对象
伴生对象是一个声明在类中的普通对象,它也可以有名字,实现一个接口或者有扩展函数和属性。例如下面的实例
class Person (val name : String){
companion object Loader{
fun fromJson(jsonText :String):Person{
//TODO
}
}
}
>>> var person = Person.Loader.fromJson("{name:'kobe'}")
>>>person.name
kobe
通常情况下,我们不需要指定伴生对象的名字,就像上例中我们可以省略掉Loader。但是如果你需要的话,也可以指定伴生对象的名字。如果省略掉伴生对象的名字,默认的名字将会分配为Companion.如果上例子中我们省略掉Loader这个名字,默认调用fromJson的方法可以为下面2种
Person.Companion.fromJson("") //默认名字为Companion
Person.fromJson("") //省略掉默认的名字
在伴生对象中实现接口
像其他对象声明一样,伴生对象可以实现接口,例如
interface JSONFactory{
fun fromJSON(jsonText : String) : T
}
class Person(val name : String){
companion object : JSONFactory{
override fun fromJSON(jsonText : String) :Person{
//TODO
}
}
}
注意:在Kotlin中使用伴生对象,可以像上面我们调用Person.fromJSON 或者 Person.Companion.fromJSON,亦或者指定名字的Person.Loader.fromJson等方法。但是如果在java代码中调用kotlin代码中的 静态成员(静态方法或者属性都可),我们最好配合上注解@JvmStatic和@JvmField使用,像下面
class Person (val name : String){
companion object{
@JvmStatic
fun fromJson(jsonText :String):Person?{
return null
}
@JvmField
var count:Int =3
}
}
//如果去掉注解@JvmStatic 和 @JvmField 在java中的调用方式
Person.Companion.getCount();
Person.Companion.fromJson("");
//在fromJson上面加上@JvmStatic 和count上面加上 @JvmField 在java中的调用方式
Person.count;
Person.fromJson("");
//如果将count上面的@JvmField换成@JvmStatic 在java中的调用方式为
Person.getCount();
伴生对象扩展
像其他普通对象一样,伴生对象也可以进行扩展,扩展和普通对象一样的方法实现
fun Person.Companion.parseJson(jsonText : String):Person{
}
//调用方法
val p = Person.parseJson("")
object对象表达式:改变写法的匿名内部类
object关键字不仅仅能用来声明单例式的式的对象,还能用来声明匿名对象。匿名对象替代了java中匿名内部类的用法。
window.addMouseListener(
object : MouseAdapter(){ //声明一个继承MouseAdapter的匿名对象
override fun mouseClicked(e:MouseEvent){
//TODO
}
override fun mouseEntered(e:MouseEvent){
//TODO
}
}
)
与java 匿名类一样,在对象表达式中的代码可以访问创建它的函数中的变量。但是与java不同,访问并没有被限制在final变量,还可以在对象表达式中修改变量的值。例如
fun countClicks(window : Window){
var clickCount = 0 //声明局部变量
window.addMouseListener(object : MouseAdapter{
override fun mouseClicked(e:MouseEvent){
clickCount++ //更新变量的值
}
})
}
我们知道在java中是不支持多继承的,Kotlin中也是如此。为什么要这样设计呢?现实中,其实多继承的需求经常会出现,然而类的多继承方式是会导致继承关系上语意的混淆。
如果你了解C++,应该知道C++中的类是支持多重继承机制的。然而C++中存在一个经典的钻石问题—骡子的多继承困惑。我们假设java也支持多继承,然后模仿C++的语法,来看看到底会导致什么问题(下面是伪代码):
abstract class Animal{
abstract public void run();
}
class Horse extends Animal{ //马
@override
public void run(){
//run fast
}
}
class Donkey extends Animal{ //驴
@override
public void run(){
//run slow
}
}
class Mule extends Horse,Donkey{ /骡子
@override
public void run(){
//...
}
}
看着代码其实没啥问题,但是当实现Mule中的run方法的时候,问题就产生了:Mule到底是继承了Horse的run方法,还是继承了Donkey的run方法呢,这个就是经典的钻石问题。
这个其实就是将方法提取成接口,让Mule也去实现就行了。这里不再赘述。
直接上代码,代码一目了然:
open class Horse{
fun runFast(){
print("I can run fast")
}
}
open class Donkey{
fun DoLongTimeThing(){
print("I can do some thing long time")
}
}
class Mule{
fun runFast(){
HorseC().runFast()
}
fun DoLongTimeThing(){
DonkeyC().DoLongTimeThing()
}
private inner class HorseC:Horse()
private inner class DonkeyC:Donkey()
}
通过HorseC,DonkeyC 分别继承Horse和Donkey这两个外部类,我们就可以在Mule中定义它们的实例对象,从而获得Horse和Donkey两者不同的状态和行为。
Kotlin中,只需要通过关键字 by 就可以实现委托的效果,之前的延迟加载by lazy就是这种效果,看看委托实现多继承的需求:
interface CanFly{
fun fly()
}
interface CanEat{
fun eat()
}
open class Flyer:CanFly{
override fun fly() {
print("I can fly")
}
}
open class Animal:CanEat{
override fun eat() {
print("I can eat")
}
}
class Bird(flyer:Flyer,animal:Animal) : CanFly by flyer,CanEat by animal
fun main1(args : Array){
var flyer = Flyer()
var aniaml = Animal()
var b = Bird(flyer,aniaml)
b.fly()
b.eat()
}
有人可能会有疑惑:首先委托方式跟接口实现多继承如此相似,而且比接口实现复杂。其实优势主要在下面2点:
1)接口是无状态的,及时可以提供接口中方法的默认实现,默认实现也只能是简单的逻辑,不能实现复杂的逻辑,也不推荐在接口中实现复杂的方法逻辑。这时候就可以使用委托的方式,让一个具体的类去实现方法逻辑,可以拥有更强大的能力。
2)假设我们需要基础的类是A,委托对象是B、C、我们在具体调用的时候并不是想组合一样A.B.method,而是可以直接调用A.method,这样更能表达A拥有改method的能力,更加直观。