类的构造方法
Kotlin 中的类及接口
Kotlin中的类与Java的很像:
1 | class Bird { |
反编译成Java的版本:
1 | public final class Bird { |
由此可以看出,虽然声明方式很像,但是也存在很多不同:
- 属性默认值。Kotlin中,除非显式声明延迟初始化,不然就需要指定默认值。
- 不同的可访问修饰符。Kotlin类中的成员默认是全局可见的(public),而Java默认可见域是包作用域。
- 方法默认是final修饰的。意味着不能覆写(这条是自己添加的)。
可带有属性和默认方法的接口
我们知道,Java 8 之后,接口支持默认实现,如下所示:
1 | public interface Flayer { |
接下来看下Kotlin的接口实现:
1 | interface Flyer { |
同样,我们可以用Kotlin 定义一个带有方法实现的接口,同时,它还支持抽象属性(如例子中的speed属性),然而,Kotlin是基于Java6实现的,那它是如何支持的呢?转换为Java代码看下:
1 | public interface Flyer { |
由此我们发现,Kotlin编译器通过定义一个静态内部类 DefaultImpls 来提供fly方法的默认实现。同时,抽象属性是通过一个get方法来实现的!所以呢,我们不能像Java一样,为属性直接赋值,如下这样是错误的:
1 | interface Flyer { |
但是Kotlin 提供了另外一种方式来实现这种效果:
1 | interface Flyer { |
更简洁地构造类的对象
如果要在Java中实现参数个数不同的构造方法,那我们就要重载很多个构造方法,这种方式主要存在2个缺点:
- 如果要支持任意参数组合来创建对象,那么需要实现的构造方法非常多
- 每个构造方法中的代码都会冗余,如在构造方法中可能都需要对 age 和color 进行相同的赋值操作。
Kotlin 通过引入新的构造语法来解决这些问题。比如我们可以用一行代码来表示复杂的构造方式:
1 | class Bird(val weight: Double = 0.00, val age: Int = 0, val color: String = "blue") |
如果用Java实现这种参数任意组合的效果,那是非常复杂的。但是如果不写入全部的参数,而只用其中某些参数的时候,需要写参数名,否则会报错:
1 | //错误的 |
init方法:事实上,我们的构造方法可以拥有多个 init,他们会在对象创建时按照类中从上到下的顺序先后执行
延迟初始化: by lazy 和 lateinit
在Kotlin中,主要使用lateinit 和 by lazy 这两种语法来实现延迟初始化的效果。如果这是一个用 val 声明的变量,我们用 by lazy 来修饰:
1 | class Bird(val weight: Double, val age: Int, val color: String) { |
总结 by lazy 语法的特点如下:
- 该变量必须是引用不可变的,而不能通过var声明
- 被首次调用时,才会进行赋值操作,一旦赋值,后续将不能更改。
需要注意的是,系统会给 lazy属性默认加上同步锁,也就是 LazyThreadSafetyMode.SYNCHRONIZED ,它在同一时刻只允许一个线程对lazy属性初始化,所以,lazy是线程安全的。当然,你可以自己给lazy指定参数,如: val sex: String by lazy(LazyThreadSafetyMode.NONE)
。
与lazy 不同,lateinit 主要用于 var 声明的变量,然而它不能用于基本数据类型,如 Int、Long 等,我们需要使用Integet这种包装类作为替代。lateinit 的用法如下:
1 | class Bird(val weight: Double, val age: Int, val color: String) { |
Kotlin只用一个构造方法实现了Java中需要重载才能实现的功能,那么,Kotlin中是否真的只需要一个构造方法呢?
主从构造方法
前面似乎遗漏了一些情况,简化前面的Bird类:
1 | class Bird(age: Int) { |
假设当前我们知道鸟的生日,希望可以通过生日来得到鸟的年龄,然后创建一个Bird对象,如何实现?有一种方案就是在别处定义一个工厂方法:
1 | fun Bird(birth: DateTme) = Bird(getAgeByBirth(birth)) |
在哪声明这个工厂方法呢?这种方式的缺点在于,Bird 方法与Bird类在代码层面的分离不够直观。其实我们可以像Java那样新增一个构造方法来解决,Kotlin 也支持多构造方法,与Java的区别是,Kotlin中多个构造方法之间存在主从关系:
1 | class Bird(age: Int) { |
以上代码的运作方式是:
- 通过constructor方法定义一个新的构造方法,称为从构造方法。相应地,我们熟悉的构造方法叫做主构造方法,每个类最多存在一个主构造方法,但是可以存在多个从构造方法
- 如果一个类存在主构造方法,那么每个从构造方法都要直接或间接地委托给它。
不同的访问控制原则
构造完对象,就要考虑访问控制了。
限制修饰符
我们知道,Kotlin中的类和方法默认实现反编译成 Java的时候,会被final修饰,所以,类默认是不能被继承的,方法默认也不能被覆写的,如果要实现继承,类之前需要用open修饰: open class Bird {}
,方法也需要使用open 修饰: open fun fly()
类默认final 真的好吗?
网上有很多人认为默认final有很多缺点,那为什么Kotlin要设计成默认final呢?主要有2个原因:
- Kotlin 当前是一门以Android为平台的开发语言,在开发中,我们很少会频繁继承一个类,默认final会更加安全。
- Kotlin的扩展手段更加丰富。不像Java,Kotlin 可以通过多种方式去扩展,而不是通过原始类的手段,典型的莫过于 Android 的Kotlin 扩展库 android-ktx,Google就是通过Kotlin的扩展语法而不是继承来实现。
此外,Kotlin还可以利用密封类来限制一个类的继承,如下所示:
1 | sealed class Bird { |
Kotlin通过 sealed 关键字来修饰一个类为密封类,若要继承则需要将子类定义在同一个文件中,其他文件中的类将无法继承它。但是这种方式有它的局限性,即它不能被初始化,为什么呢?这是因为它是基于抽象类实现的,我们看反编译后的Java代码就知道了:
1 | public abstract class Bird { |
密封类的使用场景优先,它其实可以看成一种功能更强大的枚举,所以它在模式匹配中可以起到很大的作用。
可见性修饰符
除了限制类修饰符外,还有一种可见性修饰符。Kotlin与Java的不同在于:
- 默认修饰符不同,Kotlin是public,而Java是default
- Kotlin中有一个独特的 internal
- Java类只有内部类可以用private修饰,其他类不允许;而Kotlin可以
- protected访问范围不同。Java中是包、类及子类可访问,而Kotlin只有类和子类可以访问。
说下Kotlin 独特的 internal 修饰符,它的作用域被称作模块内访问,那到底什么是模块?以下几种情况可以算作一个模块:
- 一个Eclipse项目
- 一个 Intellij IDEA项目
- 一个Maven项目
- 一个Gradle项目
- 一组由一次Ant任务执行编译的代码
那为什么要这种修饰符呢?Java的包内访问不好吗?Java包内访问确实是有问题的,举个例子,你再Java项目中定义了一个类,默认修饰符,那就是包私有的,其他地方将无法访问。然后你id啊宝诚一个类库,供三方使用。但如果有个开发者想使用这个类,除了copy源码以外,还有一个方式就是在程序中创建一个与该类相同名字的包,那么这个包下面的其他类就能直接使用我们前面定义的类了!
而Kotlin这种,模块内可见指的是该类只对一起编译的其他Kotlin文件可见,开发工程与第三方类库不属于同一模块,这时候如果还想用,就只能复制源码了。
Java中我们很少见到private修饰的类,因为Java中的类或者方法没有单独属于某个文件的概念。若要用provate修饰,那么这个只能是其他类的内部类,而Kotlin中则可以用private给单独的类修饰,它的作用域就是当前这个Kotlin文件:
解决多继承问题
Java和Kotlin都不支持多继承。为什么这样呢?是因为多继承会导致继承关系语义上的混淆。
骡子的多继承困惑
C++支持多继承,然而C++中存在一个经典的钻石问题。假如我们有个抽象的 Animal 类,它有个 run() 方法,Horse (马) 和 Donkey(驴) 都继承了Animal,假如支持多继承,Mule(驴)继承了 Horse 和 Donkey ,那么,在 Mule 中的 run() 到底是继承了谁的呢?这就是典型的钻石问题,因为继承关系像个钻石图,如下:
接口实现多继承
在Java中我们经常提及使用接口来实现多继承,其实,如果多个接口中都存在同样的方法,比如上述的 run() ,同样也会导致钻石问题。不过,Kotlin 通过提供 super 关键字来指定继承那个父接口的方法,从而解决了这个问题,如下:
1 | interface Flyer { |
通过 super<Flyer>.kind()
来指定继承哪个父接口的方法!当然,子类也可以自己实现这个方法,不用父类的,完全没问题。
内部类解决多继承问题
Kotlin的内部类的定义方式和Java 还不一样,如果我们按照Java的习惯来定义 Kotlin中的内部类:
1 | class OuterKotlin { |
报错了,和Java 还真不一样。其实,我们这样声明的是Kotlin 的 嵌套类,并非内部类。如果要在Kotlin中声明一个内部类,必须在这个类前面加一个 inner 关键字,即这样子:
1 | class OuterKotlin { |
我们知道,Java中在内部类的语法上增加一个 static 关键字,就可以变成 嵌套类;Kotlin则是相反的思路,默认是嵌套类,必须加上 inner 关键字才是一个内部类。
了解内部类之后,可以通过内部类实现上述的骡子类:
1 | class Mule { |
使用委托代替多继承
Kotlin中的委托只需要通过 by 关键字就可以实现,比如之前学习的 by lazy 语法,其实就是利用了委托实现了延迟初始化。我们看下如何通过委托代替多继承需求:
1 | interface CanFly { |
真正的数据类
繁琐的JavaBean
JavaBean中需要各种setter和getter,如果要支持对象值的比较,还得重写hashCode 和 equals 等方法。
用data class创建数据类
data class 顾名思义就是数据类,这不是Kotlin首创,在Scala等语言中也有。一般我们只需要如下定义即可:
1 | data class Bird(var weight: Double, var age: Int, var color: String) |
这么一行代码,编译器为我们做了很多事情,来看看反编译后的代码:
1 | public final class Bird { |
这就和JavaBean很相似了,同时还有 equals 和 hashCode 的实现。同时,我们发现里面有几个JavaBean中没有的方法,比如 copy、component1、component2、component3。下一节来介绍它们。
copy 、componentN 与 结构
上上述代码可以看到,两个copy方法,可以传入响应的参数来生成不同的对象;同时,如果你未指定具体属性的值,那么新生成的对象的属性值将使用被copy对象的属性值,这就是我们常说的浅拷贝。看个例子:
1 | //如果Bird的属性是var,即可变的 |
注意,Kotlin提供的上述copy方法是浅拷贝的,所以我们要注意使用场景。因为数据类的属性可以被修饰为var,所以不能保证不会出现引用被修改的情况。
接下来看 componentN (其中N为1,2,3…,根据参数个数来定),这个设计到底有什么用?我们或多或少直到怎么将属性绑定到类上,但是对于如何将类的属性绑定到响应变量上却不是很熟悉,比如:
1 | val b1 = Bird(20.0,1, "blue") |
看到进阶方法的时候,一定感到兴奋了吧,普通方法确实很繁琐。还有一种情形,看Java的代码:
1 | String birdInfo = "20.0,1, blue"; |
在我们明明直到值得情况下,还需要这样分割,很繁琐,好在Kotlin提供了更优雅的做法:
1 | val (weight, age, color) = birdInfo.split(",") |
这个语法也很简洁和直观,其原理也很简单,就是 解构,通过编译器的约定实现解构。当然,Kotlin对于解构也有限制,在数组中它默认最多允许赋值5个变量,因为如果变量过多,效果反而会适得其反,因为到后期你都搞不清哪个值要赋给哪个变量了。除了利用编译器自动生成的 componentN之外,你还可以实现自己的 componentN,比如:
1 | data class Bird(var weight: Double, var age: Int, var color: String) { |
除了数组支持解构外,Kotlin也提供了其他常用数据类型,分别是 Pair 和 Triple 前者是二元组,后者是 三元组,,我们可以
用类似以下方法来使用它们:
1 | val pair = Pair(20.0,1) |
从static 到 object
Kotlin中告别了static,因为有了 object 关键字,除了替代static外,它还有更多的功能实现,比如单例对象以及简化匿名表达式等。
伴生对象
看一段常见的Java代码:
1 | public class Prize { |
这个类中既有静态变量、静态方法,也有普通变量、普通方法,然而,静态变量和静态方法是属于类的,普通变量和普通方法是属于具体对象的,所以在代码解构上职能并不清晰。Kotlin中利用 companion object 两个关键字引入伴生对象来清晰区分。
顾名思义,“伴生”即伴随某个类的对象,它属于这个类所有,全局只有一个单例,因此伴生对象跟Java中static修饰的效果一样,在类装载的时候被初始化。
companion object 用花括号包裹了所有静态属性和方法,使得将普通方法和属性清晰区分开来。此外,伴生对象很适合作为工厂,这里就不展开。
天生的单例: object
单例模式最大的一个特点就是在系统中只能存在一个实例对象,所以在java中我们必须通过设置构造方法私有化,以及提供静态方法创建实例的方式来创建单例。在Kotlin中,由于object的存在,我们可以直接用它来实现单例,如下所示:
1 | object DatabaseConfig { |
由于object全局声明的对象只有一个,所以它并不用语法上的初始化,甚至都不需要构造方法,因此,我们可以说object创造的是天生的单例。此外,由于 DatabaseConfig 的属性是 var 声明的属性,我们还能修改它们:
1 | DatabaseConfig.host = "localhost" |
由于单例也可以和普通类一样实现接口和继承类,所以可以将其看成一种不需要主动初始化的类,它也可以拥有扩展方法,单例对象会在系统加载的时候初始化。
object 表达式
主要说的是,利用object来完善匿名内部类,这里不展开说。