文章目录
一、类和构造方法
1.1、Kotlin 中的类和接口
Kotlin 中的类
class Bird {
val weight: Double = 500.0
val color: String = "blue"
val age: Int = 1
fun fly() {}
}
- 不可变属性成员:声明不可变的属性成员用 val,是利用在 Java 中的 final 修饰符实现的,声明可变的属性成员用 var。
- 属性默认值:在 Java 中属性都有默认值,比如 int 类型的默认值为 0,引用类型的默认值为 null,所以在声明属性的时候不需要指定默认值。而在 Kotlin 中,除非显式的声明延迟初始化,不然就需要指定属性的默认值。
- 不同的可访问修饰符:Kotlin 类中的成员默认全局可见,而 Java 的默认值可见域是包作用域,要想全局可见,必须采用 public 修饰符。
可带有属性和默认方法的接口
首先看看 Kotlin 和 Java 中接口的差异,首先看一个 Java 8 版本的接口。Java 8 的接口方法支持默认实现,这使得我们向接口中新增方法时候, 继承过该接口的类可选择不实现这个新方法。
public interface Flyer {
public String kind();
default public void fly() {
System.out.println("I can fly");
}
}
在 Kotlin 中接口也可以有默认实现,同时它还支持抽象属性。下面来看一个例子,还有转换为 Java 后的例子,这里只提取其中的关键代码。
interface Flyer {
val speed: Int
fun kind()
fun fly() {
println("I can fly")
}
}
public interface Flyer {
int getSpeed();
void kind();
void fly();
public static final class DefaultImpls {
public static void fly(Flyer $this) {
String var1 = "I can fly";
System.out.println(var1);
}
}
}
可以发现,Kotlin 编译器是通过定义了一个静态内部类 DefaultImpls 来提供 fly 方法的默认实现的。同时,虽然 Kotlin 接口支持属性声明,然而它在 Java 源码中是通过一个 get 方法来实现的,所以在接口中的属性并不能像 Java 接口那样,被直接赋值一个常量。
interface Flyer {
val height = 1000 //错误的方法
//正确的方法
val height
get() = 1000
}
1.2、更简洁的构造类的对象
构造方法默认参数
class Bird(val weight: Double = 0.00, val age: Int = 0, val color: String = "blue") {
}
val bird1 = Bird(1000.00)
val bird2 = Bird(1000.00, 1, "black")
val bird3 = Bird(1000.00, "black")
我们在 Bird 类中可以用 val 或 var 来声明构造方法的参数,避免了像 Java 中要支持任意参数组合来创建对象时,需要实现的构造方法将会非常多。
构造方法的参数名前可以没有 val 和 var,如果带上它们之后就等价于在 Bird 类内部声明了一个同名的属性,就可以用 this 来进行调用。比如上面的代码如果不带 val 就类似于下面的实现。
class Bird(weight: Double = 0.00, age: Int = 0, color: String = "blue") {
val weight: Double
val age: Int
val color: String
init {
this.weight = weight //构造方法参数可以在 init 语句块被调用
this.age = age
this.color = color
}
}
init 语句块
Kotlin 引入了一种叫做 init 语句块的语法,它属于上述构造方法的一部分,两者在表现形式上却是分离的。Bird 类的构造方法在类的外部,它只能对参数进行赋值,如果需要在初始化时进行其他额外操作,就可以用 init 语句块来执行。比如:
class Bird(weight: Double = 0.00, age: Int = 0, color: String = "blue") {
init {
println("do some other things")
println("the weight is ${weight}") //当没有 val 或 var 时,构造方法的参数可以在 init 语句块中直接调用
}
}
事实上,我们的构造方法还可以拥有多个 init,它们会在对象被创建时按照类中从上到下的顺序先后执行。多个 init 语句块有利于我们进一步对初始化的操作进行职能分离。
延迟初始化:by lazy 和 lateinit
如果这是一个用 val 声明的变量,我们可以用 by lazy 来修饰:
class Bird(weight: Double = 0.00, age: Int = 0, color: String = "blue") {
val sex: String by lazy {
if (color == "yellow") "male" else "female"
}
}
lazy 的背后接受一个 lambda 并返回一个 Lazy<T> 实例的函数,第一次访问该属性时,会执行 lazy 对应的 Lambda 表达式并记录结果,后续访问该属性时只是返回记录的结果。
另外系统会给 lazy 属性默认加上同步锁,也就是 LazyThreadSafetyMode.SYNCHRONIZED,它在同一时刻只允许一个线程对 lazy 属性进行初始化,所以它是线程安全的。也可以指定线程模式,
class Bird(weight: Double = 0.00, age: Int = 0, color: String = "blue") {
val sex: String by lazy(LazyThreadSafetyMode.PUBLICATION) {
//并行模式
if (color == "yellow") "male" else "female"
}
}
class Bird(weight: Double = 0.00, age: Int = 0, color: String = "blue") {
val sex: String by lazy(LazyThreadSafetyMode.NONE) {
//指定 NONE 不会有任何线程开销但不能保证线程安全
if (color == "yellow") "male" else "female"
}
}
lateinit 主要用于 var 声明的变量,然而它不能用于基本数据类型,如 Int、Long 等,需要用 Integer 这种包装类作为替代。
class Bird(val weight: Double = 0.00, val age: Int = 0, val color: String = "blue") {
lateinit var sex: String
fun printSex() {
this.sex = if (this.color == "yellow") "male" else "female"
println(this.sex)
}
}
如果想要让 var 声明的基本数据类型变量也具有延迟初始化的效果,一种可参考的解决方案是通过 Delegates.notNull<T>,这是利用 Kotlin 中的委托来实现的,这里暂不介绍。例:var test by Delegates.notNull<Int>
1.3、主从构造方法
通过 constructor 定义一个构造方法,称为从构造方法。相应的,前面所介绍的构造方法称为主构造方法。每个类可最多存在一个主构造方法,多个从构造方法,如果主构造方法存在注解或可见性修饰符,也必须像从构造方法一样加上 constructor 关键字,如:
internal public Bird @inject constructor(age: Int) { … }
每个从构造方法由两部分组成,一部分是对其他构造方法的委托,另一部分是由花括号包裹的代码块。执行顺序上会先执行委托的方法,然后执行自身代码块的逻辑。比如常见的自定义 View 的构造方法。
class KotlinView : View {
constructor(context: Context) : this(context, null) //构造方法 A,委托给构造方法 B
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0) //构造方法 B,委托给构造方法 C
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super( //构造方法 C,委托给主构造方法
context,
attrs,
defStyleAttr
)
}
二、不同的访问控制原则
2.1、限制修饰符
对比一下 Kotlin 和 Java 不一样的语法特性:
- Kotlin 中没有采用 Java 中的 extends 和 implements 关键词,而是使用 “:” 来代替类的继承和接口实现。
- 由于 Kotlin 中类和方法默认是不可被继承或重写的,所以必须加上 open 修饰符。比如定义一个鸟类 Bird,定义一个企鹅类 Penguin 继承鸟类。
open class Bird {
open fun fly() {
println("I can fly.")
}
}
class Penguin : Bird() {
override fun fly() {
println("I can't fly actually.")
}
}
Kotlin 除了可以利用 final 来限制一个类的继承以外,还可以通过密封类的语法来限制一个类的继承。比如:
sealed class Bird {
open fun fly() {
println("I can fly.")
}
}
Kotlin 通过 sealed 关键字来修饰一个类为密封类,若要继承则需要将子类定义在同一个文件中,其他文件中的类将无法继承它。但这种方式有它的局限性,即它不能被初始化,因为它背后是基于一个抽象类实现的。
密封类的使用场景有限,它其实可以看成一种功能更强大的枚举,所以模式匹配中可以起到很大的作用。
Kotlin 1.0 时,密封类的子类只能定义在父类结构体中
修饰符 | 含义 | 与 Java 比较 |
---|---|---|
open | 允许被继承或重写 | 相当于 Java 类与方法的默认情况 |
abstract | 抽象类或抽象方法 | 与 Java 一致 |
final | 不允许被继承或重写(默认情况) | 与 Java 主动指定 final 的效果一致 |
2.2、可见性修饰符
- Kotlin 与 Java 的默认修饰符不同,Kotlin 中是 public,而 Java 中是 default。
- Kotlin 中有一个独特的修饰符 internal。
- Kotlin 可以在一个文件内单独声明方法及常量,同样支持可见性修饰符。
- Java 中除了内部类可以用 private 修饰以外,其他类都不允许 private 修饰,而 Kotlin 可以。
- Kotlin 和 Java 中的 protected 的访问范围不同,Java 中是包、类及子类可访问,而 Kotlin 只允许类及子类。
修饰符 | 含义 | 与 Java 比较 |
---|---|---|
public | Kotlin 中默认修饰符,全局可见 | 与 Java 中 public 效果相同 |
protected | 受保护修饰符,类及子类可见 | 含义一致,但作用域除了子类及子类外,包内也可见 |
private | 私有修饰符,类内修饰只有本类可见,类外修饰文件内可见 | 私有修饰符,只有类内可见 |
internal | 模块内可见 | 无 |
三、解决多继承问题
首先定义了两个接口 Flyer 和 Animal,它们都拥有默认实现的 kind 方法,定义了一个 Bird 类同时实现了 Flyer 和 Animal,此时如果需要接口中 kind 方法的默认实现,就需要做出选择,可以在实现类中通过 super<T> 这种方式来选择,还需要带上 override 关键字。如下代码。
interface Flyer {
fun fly()
fun kind()="flying animals"
}
interface Animal {
val name:String
fun eat()
fun kind()="flying animals"
}
class Bird(override val name: String) : Flyer, Animal {
override fun eat() {
println("I can eat")
}
override fun fly() {
println("I can fly")
}
override fun kind(): String {
return super<Flyer>.kind()
}
}
getter 和 setter
在 Kotlin 中,默认会为每个属性生成 getter 和 setter 方法,但是需要注意。
1、用 val 声明的属性将只有 getter 方法,因为它不可修改;用 var 修饰的属性将同时拥有 getter 和 setter 方法。
2、用 private 修饰的属性编译器将会省略 getter 和 setter 方法,因为在类外无法访问,没有存在的意义。
3.1、内部类解决多继承问题的方案
跟 Java 语法有所不同,Kotlin 中声明内部类需要在前面加上 inner 关键字。
class OuterKotlin {
val name = "This is not Kotlin's inner class syntax"
inner class ErrorInnerKotlin {
fun printName() {
print("the name is $name")
}
}
}
如果没有 inner 关键字,就不是内部类,而是嵌套类,就像这样。
class OuterKotlin {
val name = "This is not Kotlin's inner class syntax"
class ErrorInnerKotlin { //嵌套类
fun printName() {
print("the name is $name") //这里不能引用 name,会报错
}
}
}
内部类和嵌套类有明显的区别:内部类包含着对其外部类实例的引用,在内部类中我们可以使用外部类中的属性,而嵌套类不包含对其外部类实例的引用,所以它无法调用用其外部类的属性。
看下面的例子。
open class Horse { //马
fun runFast() {
println("I can run fast")
}
}
open class Donkey { //驴
fun doLongTimeThing() {
println("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 两者不同的状态和行为。
- 我们可以利用 private 修饰内部类,使得其他类都不能访问内部类,具有非常良好的封装性。
3.2、使用委托代替多继承
interface CanRunFast {
fun runFast()
}
interface CanDoLongTimeThing {
fun doLongTimeThing()
}
open class Horse : CanRunFast { //马
override fun runFast() {
println("I can run fast")
}
}
open class Donkey : CanDoLongTimeThing { //驴
override fun doLongTimeThing() {
println("I can do some thing long time")
}
}
class Mule(horse: Horse, donkey: Donkey) : CanRunFast by horse, CanDoLongTimeThing by donkey { //骡子
}
//调用
val horse = Horse()
val donkey = Donkey()
val mule = Mule(horse, donkey)
mule.runFast()
mule.doLongTimeThing()
使用委托方式的优势:
- 前面说到接口是无状态的,所以即使它提供了默认方法实现也是简单的,不能实现复杂的逻辑,也不推荐在接口中实现复杂的方法逻辑。可以利用上面委托的这种方式,虽然也是接口委托,但它是用一个具体的类去实现方法逻辑,可以拥有更强大的逻辑。
- 假设我们需要继承的类是 A,委托对象是 B、C,我们在具体调用时就不用再在类 A 中再调用一次类 B,类 C 的方法了,而是可以直接用类 A 来调用相应的方法,这更能表达类 A 拥有该方法的能力,更加直观。
四、真正的数据类
跟 Java 不同,Kotlin 中只需要添加一个 data 关键字,默认就会实现类中属性的 getter/setter、equals、hashcode、构造方法等。其中 equals 和 hashcode 使得一个数据类对象可以像普通的实例一样进行判等,甚至可以像基本数据类型一样用 == 来判断两个对象相等。
data class Bird(var weight:Double,var age:Int,var color:String)
4.1、copy、componentN 与解构
Kotlin 的数据类中还另外提供了 copy 与 componentN 两个方法。其中 copy 方法的主要作用是帮我们从已有的数据类对象中拷贝一个新的数据类对象,可以传入相应参数来生成不同的对象,这是一种浅拷贝的方式。
声明的 Bird 属性可变
data class Bird(var weight:Double,var age:Int,var color:String)
val b1 = Bird(20.0, 1, "blue")
val b2 = b1
b2.age = 2
声明的 Bird 属性不可变
data class Bird(val weight: Double, val age: Int, val color: String)
val b1 = Bird(20.0, 1, "blue")
val b2 = b1.copy(age = 2) //只能通过 copy
对于 componentN,可以理解为类属性的值,其中 N 代表属性的顺序,比如 component1 代表第一个属性的值,component3 代表第 3 个属性的值。
//通常方式
val b1 = Bird(20.0, 1, "blue")
val weight = b1.weight
val age = b1.age
val color = b1.color
//Kotlin 进阶
val b1 = Bird(20.0, 1, "blue")
val (weight, age, color) = b1
//Kotlin 进阶
val b1 = Bird(20.0, 1, "blue")
val birdInfo = "20.0,1,blue"
val (weight, age, color) = birdInfo.split(",")
这个语法的原理就是解构,通过编译器的约定实现解构。不过 Kotlin 对于数组的解构有一定的限制,在数组中默认最多允许赋值 5 个变量。
4.2、数据类的约定与使用
Kotlin 声明一个数据类,必须满足以下几点:
- 数据类必须拥有一个构造方法,该方法至少包含一个参数,一个没有数据的数据类是没有意义的。
- 与普通的类不同,数据类构造方法的参数强制使用 val 或 var 进行声明。
- data class 之前不能用 abstract、open 或者 inner 进行修饰。
- Kotlin 1.1 版本前数据类只允许实现接口,之后的版本既可以实现接口也可以继承类。
五、从 static 到 object
在 Kotlin 中,不再有 static 关键字,而是使用了 object 来代替。并且除了代替使用 static 外,还可以实现更多的功能,比如单例对象及简化匿名表达式等。
5.1、什么是伴生对象
首先看一个 Java 例子。
/**
* 奖品类
*/
public class Prize {
private String name;
private int count;
private int type;
public Prize(String name, int count, int type) {
this.name = name;
this.count = count;
this.type = type;
}
static int TYPE_RED_PACKET = 0; //红包
static int TYPE_COUPON = 1; //优惠券
static boolean isRedPacket(Prize prize) {
return prize.type == TYPE_RED_PACKET;
}
}
//调用
Prize prize = new Prize("红包", 10, Prize.TYPE_RED_PACKET);
System.out.println(Prize.isRedPacket(prize));
很常见的 Java 代码,仔细分析可以发现这种写法的弊端。因为在一个类中既有静态变量、静态方法,也有普通变量、普通方法的声明。然而静态变量和静态方法是属于一个类的,普通变量、普通方法是是属于一个具体对象的。虽然有 static 作为区分,但是在代码结构上职能并不是区分的很清晰。
对此 Kotlin 中引入了伴生对象的概念,利用 companion object 两个关键字来创建的语法。
“伴生” 是相较于一个类而言的,意为伴随某个类的对象,它属于这个类所有,因此伴生对象跟 Java 中 static 修饰效果性质一样,全局只有一个单例。它需要声明在类的内部,在类被装载时会被初始化。
将上面的 Java 代码改写成一个伴生对象的版本。
class Prize(val name: String, val count: Int, val type: Int) {
companion object {
val TYPE_RED_PACKET = 0
val TYPE_COUPON = 1
fun isRedpack(prize: Prize): Boolean {
return prize.type == TYPE_RED_PACKET
}
}
}
//调用
val prize = Prize("红包", 10, Prize.TYPE_RED_PACKET)
println(Prize.isRedpack(prize))
可以发现,该版本在语义上更清晰了。而且,companion object 用花括号包裹了所有静态属性和方法,使得它可以与 Prize 类的普通方法和属性区分开来。此时就可以使用点号来对一个类的静态成员进行调用。
伴生对象的另一个作用是可以实现工厂方法模式。两点优势:
- 无需创建多个构造方法
- 不会每次获取对象时都重新创建
class Prize(val name: String, val count: Int, val type: Int) {
companion object {
val TYPE_RED_PACKET = 0
val TYPE_COUPON = 1
val TYPE_COMMON = 2
val defaultCommonPrize = Prize("普通奖品", 10, Prize.TYPE_COMMON)
fun newRedPacket(name: String, count: Int) = Prize(name, count, Prize.TYPE_RED_PACKET)
fun newCoupon(name: String, count: Int) = Prize(name, count, Prize.TYPE_COUPON)
fun defaultCommonPrize() = defaultCommonPrize //无需构造新对象
}
}
//调用
val redPacketPrize = Prize.newRedPacket("红包", 10)
val couponPrize = Prize.newCoupon("代金券", 10)
val commonPrize = Prize.defaultCommonPrize()
5.2、天生的单例:object
单例模式最大的特点就是在系统中只能存在一个实例对象,所以在 Java 中我们必须通过设置构造方法私有化,以及提供静态方法创建实例的方式来创建单例对象。在 Kotlin 中我们用一个 object 关键字就可以实现单例,如下代码。
object Prize {
val TYPE_RED_PACKET = 0
val TYPE_COUPON = 1
val TYPE_COMMON = 2
var name: String = "普通奖品" //name 是用 var 声明的,
var count: Int = 10
var type: Int = TYPE_COMMON
fun isRedpack(prize: Prize): Boolean {
return prize.type == TYPE_RED_PACKET
}
}
//调用
Prize.name = "红包" //用 var 声明的属性还可以直接赋值修改
Prize.type = Prize.TYPE_RED_PACKET
Prize.isRedpack(Prize)
5.3、object 表达式
在 Java 中会经常使用到匿名内部类,有时候尽管只有一个方法,也需要用一个匿名内部类来实现。比如对一个字符串排序。
List<String> list = Arrays.asList("redPacket", "score", "card");
Collections.sort(list, new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
if (o1 == null) {
return -1;
} else if (o2 == null) {
return 1;
}
return o1.compareTo(o2);
}
});
并不是说匿名内部类这个方式不好,只不过方法内掺杂类声明不仅让方法看起来复杂,也不易于阅读理解。而在 Kotlin 中,可以利用 object 表达式进行改善。
val list = Arrays.asList("redPacket", "score", "card")
val comparator = object : Comparator<String> {
override fun compare(o1: String?, o2: String?): Int {
if (o1 == null) {
return -1
} else if (o2 == null) {
return 1
}
return o1.compareTo(o2)
}
}
Collections.sort(list, comparator)
可以发现,object 表达式可以赋值给一个变量,这在我们重复使用时将会减少很多代码。另外,我们说过 object 可以继承类和实现接口,匿名内部类只能继承一个类及实现一个接口,而 object 表达式却没有这个限制。
用于代替匿名内部类的 object 表达式在运行中不像在单例模式中那样,全局只存在一个对象,而是在每次运行时都会生成一个新的对象。
然而,匿名内部类与 object 表达式并不是对任何场景都适合,有时使用 Lambda 表达式会更好,比如接口中只有单个方法的实现时。下面用 Lambda 表达式改造下。
val list = Arrays.asList("redPacket", "score", "card")
val comparator = Comparator<String> { o1, o2 ->
if (o1 == null) {
return@Comparator -1
} else if (o2 == null) {
return@Comparator 1
}
o1.compareTo(o2)
}
Collections.sort(list, comparator)
object 表达式与 Lambda 表达式那个更适合代替匿名内部类:
当匿名内部类使用的接口只需要实现一个方法是,使用 Lambda 表达式更合适,否则使用 object 表达式更合适。