赞
踩
面向过程和面向对象都是对软件分析、设计、开发的一种思想。
面向过程(Procedure-oriented
)面向对象(Object-oriented
)
procedure [prəˈsiːdʒər] 步骤 oriented [ˈɔːrientɪd] 以……为方向的;重视……的
早期先有面向过程思想,随着软件规模的扩大,问题复杂性的提高,面向过程的弊端越来越明显的显示出来,出现了面向对象思想并成为目前主流的方式。两者都贯穿于软件分析、设计和开发各个阶段,对应面向对象就分别称为面向对象分析(OOA Object-Oriented Analysis
)、面向对象设计(OOD Object-Oriented Design
)和面向对象编程(OOP Object Oriented Programming
)。C
语言是一种典型的面向过程语言,Java
是一种典型的面向对象语言,Kotlin
也是面向对象的。
oriented [ˈɔːrientɪd] 以……为方向的 analysis [əˈnæləsɪs] 分析 programming [ˈproʊɡræmɪŋ] 设计,规划;编制程序,[计] 程序编制
面向过程以“过程”为中心,把要实现的目的分为几个步骤,用函数实现,再按顺序调用。
举个例子,下五子棋,面向过程的设计思路是首先分析解决这个问题的步骤:
(1)开始游戏(2)黑子先走(3)绘制画面(4)判断输赢(5)轮到[白子(6)绘制画面(7)判断输赢(8)返回步骤(2)(9)输出最后结果。
用函数实现上面一个一个的步骤,然后在下五子棋的主函数里依次调用上面的函数:
下五子棋 {
开始游戏();
黑子先走();
绘制画面();
判断输赢();
轮到白子();
绘制画面();
判断输赢();
返回到 黑子先走();
输出最后结果;
}
可见,面向过程始终关注的是怎么一步一步地判断棋局输赢的,通过控制代码,从而实现函数的顺序执行。
在日常生活或编程中,简单的问题可以用面向过程的思路来解决,直接有效,但是当问题变得复杂时,面向过程的思想是远远不够的,所以就出现了面向对象的编程思想。
不同于面向过程的语言,面向对象的语言是可以创建类,类就是对事物的一种封装。 比如说人、汽车、房 屋、书等任何事物,都可以将它封装一个类,类名通常是名词。而类中又可以拥有自己的字段和函数,字段表示该类所拥有的属性, 比如说人可以有姓名和年龄,汽车可以有品牌和价格,这些就属于类中的字段,字段名通常也是名词。而函数则表示该类可以有哪些行为, 比如说人可以吃饭和睡觉,汽车可以驾驶和保养等,函数名通常是动词。
通过这种类的封装,就可以在适当的时候创建该类的对象,然后调用对象中的字段和函数来满足实际编程的需求,这就是面向对象编程最基本的思想。当然,面向对象编程还有很多其他特性,如继承、多态等,但是这些特性都是建立在基本的思想之上的。
面向对象是以“对象”为中心,把要解决的问题分发给各个对象,每个对象都有自己的属性和行为。
在下五子棋的例子中,用面向对象的方法来解决的话,首先将整个五子棋游戏分为三个对象:
(1)黑白双方,这两方的行为是一样的。
(2)棋盘系统,负责绘制画面
(3)规则系统,负责判定犯规、输赢等。
然后赋予每个对象一些属性和行为:第一类对象(黑白双方)负责接受用户输入,并告知第二类对象(棋盘系统)棋子布局的变化,棋盘系统接收到了棋子的变化,并负责在屏幕上面显示出这种变化,同时利用第三类对象(规则系统)来对棋局进行判定。
可以看出,面向对象是以功能来划分问题,而不是以步骤解决。比如绘制画面这个行为,在面向过程中是分散在了多个步骤中的,可能会出现不同的绘制版本,所以要考虑到实际情况进行各种各样的简化。而面向对象的设计中,绘图只可能在棋盘系统这个对象中出现,从而保证了绘图的统一。
面向过程性能比面向对象高。因为创建对象的开销比较大,消耗资源。 所以当性能是最重要的考量因素的时候,比如单片机、嵌入式开发、Linux/Unix
等,一般采用面向过程开发。 但是,面向过程没有面向对象易维护、易复用、易扩展。
但是,面向对象的封装性强,易扩展,易维护,可复用,还可以降低代码的耦合度。
那为什么面向过程性能比面向对象高呢?面向过程也需要分配内存,计算内存偏移量,Java
性能差的主要原因并不是因为它是面向对象语言,而 是因为Java
是半编译语言,最终的执行代码并不是可以直接被CPU
执行的二进制机器码。而面向过程语言大多都是直接编译成机器码在电脑上执行,并且其它一些面向过程的脚本语言性能也并不一定比Java
好。
解释下Java
的编译与解释并存的现象。当.class
字节码文件通过JVM
转为机器可以执行的二进制机器码时,JVM
类加载器首先加载字节码文件,然后通过解释器逐行进行解释执行,这种方式的执行速度相对比较慢。而且有些方法和代码块是反复被调用的(也就是所谓的热点代码),所以后面引进了JIT
编译器,而JIT
属于运行时编译。当JIT
编译器完成一次编译后,会将字节码对应的机器码保存下来,下次可以直接调用。这也解释了我们为什么经常会说Java
是编译与解释共存的语言。
Java
和Kotlin
中都使用class
关键字来声明一个类:
class Bird { }
可以在这个类中加入字段和函数来丰富它的功能:
class Bird {
val weight: Double = 500.0
var color: String = "blue"
val age: Int = 1
fun fly() { } // 全局可见
}
将以上代码反编译成Java
代码(Tools -> Kotlin -> Show Kotlin Bytecode -> Decompile
),如下所示:
public final class Bird { private final double weight = 500.0D; @NotNull private String color = "blue"; private final int age = 1; public final double getWeight() { return this.weight; } @NotNull public final String getColor() { return this.color; } public final void setColor(@NotNull String var1) { Intrinsics.checkNotNullParameter(var1, "<set-?>"); this.color = var1; } public final int getAge() { return this.age; } public final void fly() { } }
可以看出,虽然Kotlin
中类声明的语法非常近似Java
,但也存在很多不同:
Kotlin
支持用val
在类中声明引用不可变的属性成员,这是利用Java
中的final
修饰符来实现的,使用var
声明的属性则反之引用可变;Java
的属性都有默认值,比如int
类型的默认值为0
,引用类型的默认值为null
,所以在声明属性的时候不需要指定默认值。而在Kotlin
中,除非显式地声明延迟初始化,不然就需要指定属性的默认值;Kotlin
类中的成员默认是全局可见,而Java
的默认可见域是包作用域,因此在Java
版本中,我们必须采用public
修饰才能达相同的效果;Bird
类已经定义好了,接下来对这个类进行实例化,代码如下所示:
val b = Bird()
Kotlin
中实例化一个类的方式和Java
是基本类似的,只是去掉了new
关键字。 之所以这么设计,是因为当调用了某个类的构造函数时,只可能是对这个类进行实例化。Kotlin
本着最简化的设计原则,将诸如new
、行尾分号这种不必要的语法结构都取消了。
上述代码将实例化后的类赋值到了b
这个变量上面,b
就可以称为Bird
类的一个实例,也可以称为一个对象。下面对b
进行一些操作:
val b = Bird()
b.weight = 100.0 // 报错:Val cannot be reassigned
b.color = "yellow"
b.fly()
简单概括一下,就是要先将事物封装成具体的类,然后将事物所拥有的属性和能力分别定义成类中的字段和函数,接下来对类进行实例化,再根据具体的编程需求调用类中的字段和方法即可。
当前并没有给Bird
类传入任何参数。现实中,可能因为需要传入不同的参数组合,而在类中创建多个构造方法,在Java
中这是利用构造方法重载来实现的:
public class Bird { private double weight; private int age; private String color; public Bird(double weight) { this.weight = weight; } public Bird(double weight, int age) { this.weight = weight; this.age = age; } public Bird(double weight, int age, String color) { this.weight = weight; this.age = age; this.color = color; } ... }
Java
中的这种方式存在两个缺点:
age
和color
进行了相同的赋值操作;Kotlin
通过引入新的构造语法来解决这些问题。
在Kotlin
中可以给构造方法的参数指定默认值,从而避免不必要的方法重载:
class Bird(val weight: Double = 0.00, val color: String = "blue", val age: Int = 1) {
...
}
val bird1 = Bird(color = "black")
val bird2 = Bird(weight = 1000.00, color = "black")
需要注意的是,由于参数默认值的存在,在创建一个类对象时,最好指定参数的名称,否则必须按照实际参数的顺序进行赋值。
在Bird
类中可以用val
或者var
来声明构造方法的参数。这一方面代表了参数的引用可变性,另一方面它也使得我们在构造类的语法上得到了简化。事实上,构造方法的参数名前可以没有val
和var
,带上它们之后就等价于在Bird
类内部声明了一个同名的属性,可以用this
来进行调用。 比如前面定义的Bird
类就类似于以下的实现:
class Bird(weight: Double = 500.0, color: String = "blue", age: Int = 1) {
val weight: Double
val age: Int
val color: String
init {
this.weight = weight
this.age = age
this.color = color
}
...
}
init
语句块Kotlin
引入了一种叫作init
语句块的语法,它属于构造方法的一部分,两者在表现形式上却是分离的,但是,所有主构造函数中的逻辑都可以写在里面。Bird
类的构造方法在类的外部,它只能对参数进行赋值。如果需要在初始化时进行其他的额外操作,那么就可以使用init
语句块来执行。比如:
class Bird(weight: Double, color: String, age: Int) {
init {
println("do some other things")
println("the weight is $weight")
}
}
因此,构造函数的参数在没有val
或var
修饰的时候,可以在init
语句块被直接调用。另外,构造函数的参数还可以用于初始化类内部的属性成员的情况。如:
class Bird(weight: Double = 0.00, color: String = "blue", age: Int = 0) {
val weight: Double = weight
val color: String = color
val age: Int = age
}
除此之外,不能在其他地方使用。以下是一个错误的用法:
class Bird(weight: Double = 0.00, color: String = "blue", age: Int = 0) {
fun printWeight(){
println(weight) // unresolved reference: weight
}
}
事实上,构造方法还可以拥有多个init
,它们会在对象被创建时按照从上到下的顺序先后执行。看看以下代码的执行结果:
class Bird(weight: Double = 0.00, color: String = "blue", age: Int = 0) { val weight: Double val age: Int val color: String init { this.weight = weight println("The bird's weight is ${this.weight}") this.age = age println("The bird's age is ${this.age}") } init { this.color = color println("The bird's color is ${this.color}") } } fun main() { val bird = Bird(1000.0, "blue", 2) } // The bird's weight is 1000.0 // The bird's age is 2 // The bird's color is blue
可以发现,多个init
语句块有利于进一步对初始化的操作进行职能分离,这在复杂的业务开发中显得特别有用。
再来思考一种场景,现实中我们在创建一个类对象时,很可能不需要对所有属性都进行传值。其中存在一些特殊的属性,比如鸟的性别,我们可以根据它的颜色来进行区分,所以它并不需要出现在构造方法的参数列表中。
有了init
语句块的语法支持,我们很容易实现这一点。假设黄色的鸟儿都是雌性,剩余的都是雄鸟,我们就可以如此设计:
class Bird(val weight: Double = 0.00, val color: String = "blue", val age: Int = 0) {
val sex: String
init {
this.sex = if (this.color == "yellow") "male" else "female"
}
}
我们再来修改下需求。这一次我们并不想在init
语句块中对sex
直接赋值,而是调用一 个专门的printSex
方法来进行,如:
class Bird(val weight: Double, val color: String, val age: Int) {
val sex: String // Property must be initialized or be abstract
fun printSex() {
this.sex = if (this.color == "yellow") "male" else "female" // Val cannot be reassigned
println(this.sex)
}
}
结果报错了,主要由以下两个原因导致:
Kotlin
规定类中的所有非抽象属性成员都必须在对象创建时被初始化值;sex
必须被初始化值,上述的printSex
方法中,sex
会被视为二次赋值,这对val
声明的变量来说也是不允许的;第2
个问题比较容易解决,我们把sex
变成用var
声明,它就可以被重复修改。关于第1
个问题,最直观的方法是指定sex
的默认值,但这可能是一种错误的性别含义;另一种办法是引入可空类型,即把sex
声明为String?
类型,则它的默认值为null
。这可以让程序正确运行,然而实际上也许我们又不想让sex
具有可空性,而 只是想稍后再进行赋值,所以这种方案也有局限性。
by lazy
和lateinit
更好的做法是让sex
能够延迟初始化,即它可以不用在类对象初始化的时候就必须有值。在Kotlin
中,主要使用lateinit
和by lazy
这两种语法来实现延迟初始化的效果。
如果这是一个用val
声明的变量,可以用by lazy
来修饰:
class Bird(val weight: Double, val color: String, val age: Int) {
val sex: String by lazy {
if (color == "yellow") "male" else "female"
}
}
by lazy
语法的特点如下:
var
来声明;lazy
的背后是接受一个lambda
并返回一个Lazy<T>
实例的函数,第一次访问该属性时,会执行lazy
对应的Lambda
表达式并记录结果,后续访问该属性时只是返回记录的结果。
另外,系统会给lazy
属性默认加上同步锁,也就是LazyThreadSafetyMode.SYNCHRONIZED
,它在同一时刻只允许一个线程对lazy
属性进行初始化,所以它是线程安全的。若能确认该属性可以并行执行,没有线程安全问题,那么可以给lazy
传递 LazyThreadSafetyMode.PUBLICATION
参数。还可以给lazy
传递LazyThreadSafetyMode.NONE
参数,这将不会有任何线程方面的开销,也不会有任何线程安全的保证。 比如:
val sex: String by lazy(LazyThreadSafetyMode.PUBLICATION) { // 并行模式
if (color == "yellow") "male" else "female"
}
val sex: String by lazy(LazyThreadSafetyMode.NONE) { // 不做任何线程保证也不会有任何线程开销
if (color == "yellow") "male" else "female"
}
与lazy
不同,lateinit
主要用于var
声明的变量,然而它不能用于基本数据类型,如Int
、Long
等,需要用Integer
这种包装类作为替代。 利用lateinit
解决之前的问题:
class Bird(val weight: Double, val color: String, val age: Int) {
lateinit var sex: String // 可以延迟初始化
fun printSex() {
this.sex = if (this.color == "yellow") "male" else "female"
println(this.sex)
}
}
fun main() {
val bird = Bird(1000.0, "blue", 2)
bird.printSex() // female
}
总而言之,Kotlin
并不主张用Java
中的构造方法重载,来解决多个构造参数组合调用的问题。取而代之的方案是利用构造参数默认值及用val
、var
来声明构造参数的语法,以更简洁地构造一个类对象。
任何一个面向对象的编程语言都会有构造函数的概念,Kotlin
中也有,但是Kotlin
将构造函数分成了两种:主构造函数和次构造函数。
主构造函数是最常用的构造函数,每个类默认都会有一个不带参数的主构造函数,当然也可以显式地给它指明参数。主构造函数的特点是没有函数体,直接定义在类名的后面即可。 比如下面这种写法:
class Bird(val weight: Double = 0.00, val color: String = "blue", val age: Int = 1) {
}
其实几乎是用不到次构造函数的,Kotlin
提供了一个给函数设定参数默认值的功能,基本上可以替代次构造函数的作用。
任何一个类只能有一个主构造函数,但是可以有多个次构造函数。次构造函数也可以用于实例化一个类,这一点和主构造函数没有什么不同,只不过它是有函数体的。
Kotlin
规定,当一个类既有主构造函数又有次构造函数时,所有的次构造函数都必须调用主构造函数(包括间接调用)。 比如,可以把从构造方法A
委托给从构造方法B
,再将从构造方法B
委托给主构造方法。举个例子:
class Bird(age: Int) {
val age: Int
init {
this.age = age
}
constructor(timestamp: Long) : this(DateTime(timestamp)) // 构造函数A
constructor(birth: DateTime) : this(getAgeByBirth(birth)) // 构造函数B
}
fun getAgeByBirth(birth: DateTime): Int {
return Years.yearsBetween(birth, DateTime.now()).years
}
次构造函数是通过constructor
关键字来定义的。
从构造方法的设计除了解决以上的场景之外,还有一个很大的作用就是可以对某些Java
的类库进行更好地扩展,因为我们经常要基于第三方Java
库中的类,扩展自定义的构造方法。典型的例子就是定制业务中特殊的View
类。比如以下的代码:
class KotlinView : View {
constructor(context: Context) : this(context, null)
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {}
}
可以看出,利用从构造方法,我们就能使用不同参数来初始化第三方类库中的类了。
来看看这个新的构造方法是如何运作的:
constructor
方法定义了一个新的构造方法,被称为从构造方法。相应地,在类外部定义的构造方法被称为主构造方法。每个类可最多存在一个主构造方法和多个从构造方法,如果主构造方法存在注解或可见性修饰符,也必须像从构造方法一样加 上constructor
关键字,如:internal public Bird @inject constructor(age: Int) { ... }
在Java
中,如果不希望一个类被其他的类继承或修改,就用final
来修饰它。同时,还可以用public
、private
、protected
等修饰符来描述一个类、方法或属性的可见性。对于Java
的这些修饰符,在Kotlin
中与其也是大同小异。最大的不同是,Kotlin
在默认修饰符的设计上采用了与Java
不同的思路。
当想要指定一个类、方法或属性的修改或者重写权限时,就需要用到限制修饰符。继承是面向对象的基本特征之一,继承虽然灵活,但如果被滥用就会引起一些问题。比如说,Penguin
(企鹅)也是一种鸟类:
open class Bird {
open fun fly() {
println("I can fly.")
}
}
class Penguin : Bird() {
override fun fly() {
println("I can't fly actually.")
}
}
在Kotlin
中,有两个和Java
不一样的语法特性:
Kotlin
中没有采用Java
中的extends
和implements
关键词,而是使用:
来代替类的继承和接口实现;Kotlin
中类和方法默认是不可被继承或重写的,所以必须加上open
修饰符;另外,Penguin
类重写了父类中的fly
方法,这其实是一种比较危险的做法,比如我们修改了Bird
类的fly
方法,增加了一个代表每天能够飞行的英里数的参数:
open class Bird {
open fun fly(miles: Int) {
println("I can fly $miles miles daily.")
}
}
现在如果再次调用Penguin
的fly
方法,那么就会出错,错误信息提示fly
重写了一个不存在的方法:
事实上,这是日常开发中错误设计继承的典型案例。子类应该尽量避免重写父类的非抽象方法,因为一旦父类变更方法,子类的方法调用很可能会出错,而且重写父类非抽象方法违背了面向对象设计原则中的“里氏替换原则”。
对里氏替换原则通俗的理解是:子类可以扩展父类的功能,但不能改变父类原有的功能。 它包含以下4
个设计原则:
然而,实际业务开发中我们常常很容易违背里氏替换原则,导致设计中出问题的概率大大增加。其根本原因,就是我们一开始并没有仔细思考一个类的继承关系。所以 《Effective Java》也提出了一个原则:要么为继承做好设计并且提供文档,否则就禁止这样做。
final
Kotlin
认为类默认开放继承并不是一个好的选择。所以在Kotlin
中的类或方法默认是不允许被继承或重写的。 以Bird
类为例:
class Bird {
val weight: Double = 500.0
val color: String = "blue"
val age: Int = 1
fun fly() {}
}
这是一个简单的类。把它编译后转换为Java
代码:
public final class Bird { private final double weight = 500.0D; @NotNull private final String color = "blue"; private final int age = 1; public final double getWeight() { return this.weight; } @NotNull public final String getColor() { return this.color; } public final int getAge() { return this.age; } public final void fly() { } }
转换后的Java
代码中的类,方法及属性前面多了一个final
修饰符,由它修饰的内容将不允许被继承或修改。我们经常使用的String
类就是用final
修饰的,它不可以被继承。在Java
中,类默认是可以被继承的,除非主动加上final
修饰符。而在Kotlin
中恰好相反,默认是不可被继承的,除非主动加上可以继承的修饰符——open
。
现在,给Bird
类加上open
修饰符:
open class Bird {
val weight: Double = 500.0
val color: String = "blue"
val age: Int = 1
fun fly() {}
}
编译成Java
代码:
public class Bird {
...
}
此外,如果想让一个方法可以被重写,那么也必须在方法前面加上open
修饰符。
final
的意义不少人诟病默认final
的设计会给实际开发带来不便。具体表现在:
Spring
会利用注解私自对类进行增强,由于Kotlin
中 类默认不能被继承,这可能导致框架的某些原始功能出现问题。Kotlin
库进行扩展。就统计层面讨论,Kotlin
类库肯定会比Java
类库更倾向于不开放一个类的继承,因为人总是偷懒的,Kotlin
默认final
可能会阻挠我们对这些类库的类进行继承,然后扩展功能。Kotlin
论坛甚至举行了一个关于类默认final
的喜好投票,略超半数的人更倾向于把open
当作默认情况。
以上的反对观点很有道理。下面再基于Kotlin
的自身定位和语言特性重新反思一 下这些观点:
Kotlin
当前是一门以Android
平台为主的开发语言。在工程开发时,我们很少会频繁地继承一个类,默认final
会让它变得更加安全。如果一个类默认open
而在必要的时候忘记了标记final
,可能会带来麻烦。反之,如果一个默认final
的类,在我们需要扩展它的时候,即使没有标记open
,编译器也会提醒我们,这个就不存在问题。此外,Android
也不存在类似Spring
因框架本身而产生的冲突。Kotlin
非常类似于Java
,然而它对一个类库扩展的手段要更加丰富。典型的案例就是Android
的Kotlin
扩展库android-ktx
。Google
官方主要通过Kotlin
中的扩展语法对Android
标准库进行了扩展,而不是通过继承原始类的手段。这也揭示了一点,以往在Java
中因为没有类似的扩展语法,往往采用继承去对扩展一个类库,某些场景不一定合 理。相较而言,在Kotlin
中由于这种增强的多态性支持,类默认为final
也许可以督促我们思考更正确的扩展手段。除了扩展这种新特性之外,Kotlin
中的其他新特性,比如Smart Casts
结合class
的final
属性也可以发挥更大的作用。
Kotlin
除了可以利用final
来限制一个类的继承以外,还可以通过密封类的语法来限制一个类的继承。 比如可以这么做:
sealed class Bird {
fun fly() = "I can fly"
class Eagle : Bird()
}
Kotlin
通过sealed
关键字来修饰一个类为密封类,若要继承则需要将子类定义在同一个文件中,其他文件中的类将无法继承它。但这种方式有它的局限性,即它不能被初始化,因为它背后是基于一个抽象类实现的。 将它转换成Java
代码:
public abstract class Bird { @NotNull public final String fly() { return "I can fly"; } private Bird() { } // $FF: synthetic method public Bird(DefaultConstructorMarker $constructor_marker) { this(); } public static final class Eagle extends Bird { public Eagle() { super((DefaultConstructorMarker)null); } } }
密封类的使用场景有限,它其实可以看成一种功能更强大的枚举,所以它在模式匹配中可以起到很大的作用。
总的来说,我们需要辩证地看待Kotlin
中类默认final
的原则,它让我们的程序变得更加安全,但也会在其他场合带来一定的不便。最后,关于限制修饰符,还有一个abstract
,用它修饰类说明这个类是抽象类,修饰在方法前面说明这个方法是一个抽象方法。Kotlin
中的abstract
和Java
中的完全一样。
Kotlin
与Java
的限制修饰符比较如表所示:
除了限制类修饰符之外,还有一种修饰符就是可见性修饰符。如果想要指定类、方法及属性的可见性,就需要可见性修饰符。Kotlin
中的可见性修饰符也与Java
中的很类似。但也有不一样的地方,主要有以下几点:
Kotlin
与Java
的默认修饰符不同,Kotlin
中是public
,而Java
中是default
;Kotlin
中有一个独特的修饰符internal
;Kotlin
可以在一个文件内单独声明方法及常量,同样支持可见性修饰符;Java
中除了内部类可以用private
修饰以外,其他类都不允许private
修饰,而Kotlin
可以;Kotlin
和Java
中的protected
的访问范围不同,Java
中是包、类及子类可访问,而Kotlin
只允许类及子类;在Java
中创建一个类时,编辑器会自动帮我们加上public
,这是因为大多数类都可能需要在全局访问。而如果我们在定义类、变量或者方法的省略它的修饰符,会使用Java
中的默认修饰符是default
,它只允许包内访问。
Kotlin
中有一个独特的修饰符internal
,和default
有点像但也有所区别。internal
在Kotlin
中的作用域可以被称作“模块内访问”。 那么到底什么算是模块呢?以下几种情况可以算作 一个模块:
Eclipse
项目Intellij IDEA
项目Maven
项目Grandle
项目Ant
任务执行编译的代码总的来说,一个模块可以看作一起编译的Kotlin
文件组成的集合。 那么,Kotlin
中为什么要诞生这么一种新的修饰符呢?Java
的包内访问不好吗?
Java
的包内访问中确实存在一些问题。举个例子,假如你在Java
项目中定义了一个 类,使用了默认修饰符,那么现在这个类是包私有,其他地方将无法访问它。然后,你把它打包成一个类库,并提供给其他项目使用,这时候如果有个开发者想使用这个类,除了copy
源代码以外,还有一个方式就是在程序中创建一个与该类相同名字的包,那么这个包下面的其他类就可以直接使用我们前面的定义的类。伪代码如下:
package com.example.kotlintest
// 定义的第三方类库代码
class TestDefault {
...
}
该类默认只允许com.example.kotlintest
的包内可见,但是我们在项目中可以这么做:
package com.example.kotlintest
// 自身工程创建com.example.kotlintest
class Test {
TestDefault td = new TestDefault();
...
}
这样我们就可以直接访问该类了。
而Kotlin
默认并没有采用这种包内可见的作用域,而是使用了模块内可见,模块内可见指的是该类只对一起编译的其他Kotlin
文件可见。开发工程与第三方类库不属于同一个模块,这时如果还想使用该类的话只有复制源码一种方式了。这便是Kotlin
中internal
修饰符的一个作用体现。
在Java
程序中,很少见到用private
修饰的类,因为Java
中的类或方法没有单独属于某个文件的概念。比如,我们创建了Rectangle.java
这个文件,那么它里面的类要么是public
,要么是包私有,而没有只属于这个文件的概念。在Java
中,若要用private
修饰,那么这个只能是其他类的内部类。而Kotlin
中则可以用private
给单独的类修饰,它的作用域就是当前这个Kotlin
文件。 比如:
package com.example.kotlintest
class BMWCar(val name: String) {
private val bMWEngine = Engine("BMW")
fun getEngine(): String {
return bMWEngine.engineType() // error:Cannot access'enging Type': it is Protected in Engine
}
}
private class Engine(val type: String) {
fun engineType(): String {
return "the engine type is $type"
}
}
除了private
修饰符的差别,Kotlin
中的protected
修饰符也与Java
有所不同。Java
中的protected
修饰的内容作用域是包内、类及子类可访问,而在Kotlin
中,由于没有包作用域的概念,所以protected
修饰符在Kotlin
中的作用域只有类及子类。 我们对上面的代码稍加修改:
class BMWCar(val name: String) { private val bMWEngine = Engine("BMW") fun getEngine(): String { return bMWEngine.engineType() } } private open class Engine(val type: String) { protected open fun engineType(): String { return "the engine type is $type" } } private class BZEngine(type: String) : Engine(type) { override fun engineType(): String { return super.engineType() } }
我们可以发现同一包下的其他类不能访问protected
修饰的内容了,而在子类中可以:
总结一下,可见性修饰符在Kotlin
与Java
中大致相似,但也有自己的很多特殊之处。这些可见性修饰符比较如表所示:
继承是基于现实场景总结出来的一个概念。比如现在要定义一个Student
类,每个学生都有自己的学号和年级,因此可以在Student
类中加入sno
和grade
字段。但同时学生也是人 ,有姓名和年龄,需要吃饭,如果我们在Student
类中重复定义name
、age
字段和eat()
函数的话就显得太过冗余了。这个时候就可以让Student
类去继承Person
类,这样Student
就自动拥有了Person
中的字段和函数,另外还可以定义自己独有的字段和函数。
想要让Student
类继承Person
类,得做两件事才行。
第一件事,使Person
类可以被继承。在Java
中,一 个类本身就是可以被继承的。但是,在Kotlin
中任何一个非抽象类默认都是不可以被继承的(从2.1
节中可知,相当于Java
中给类声明了final
关键字),之所以这么设计,和val
关键字的原因差不多的,因为类和变量一样,最好都是不可变的,而一个类允许被继承的话,它无法预知子类会如何实现,因此可能就会存在一些未知的风险。Effective Java
这本书中明确提到,如果一个类不是专门为继承而设计的,那么就应该主动将它加上final
声明,禁止它可以被继承。
在Kotlin
在设计的时候遵循了这条编程规范,默认所有非抽象类都是不可以被继承的。之所以这里一直在说非抽象类,是因为抽象类本身是无法创建实例的,一定要由子类去继承它才能创建实例,因此抽象类必须可以被继承才行,要不然就没有意义了。(Kotlin
中的抽象类和 Java
中并无区别)
如果想让Person
类变得可继承,在Person
类的前面加上open
关键字就可以了,如下所示:
open class Person { }
加上open
关键字之后,就是在主动告诉Kotlin
编译器,Person
这个类是专门为继承而设计的,这样Person
类就允许被继承了。
第二件事,要让Student
类继承Person
类。在Java
中继承的关键字是extends
,而在Kotlin
中变成了一个冒号,写法如下:
class Student : Person() {
}
为什么Person
类的后面要加上一对括号呢?这就涉及了Java
继承特性中的一个规定,子类中的构造函数必须调用父类中的构造函数,这个规定在Kotlin
中也要遵守。Kotlin
采用了简单但是可能不太好理解的设计方式:括号。 子类的主构造函数调用父类中的哪个构造函数,在继承的时候通过括号来指定。
在这里,Person
类后面的一对空括号表示Student
类的主构造函数在初始化的时候会调用Person
类的无参数构造函数,即使在无参数的情况下,这对括号也不能省略。
如果Student
的主构造函数有参数:
class Student(val sno: String, val grade: Int) : Person() { }
如果将Person
改造一下,将姓名和年龄都放到主构造函数当中:
open class Person(val name: String, val age: Int) { }
此时Student
类一定会报错:
这里出现错误的原因也很明显,Person
类后面的空括号表示要去调用Person
类中无参的构造函数,但是Person
类现在已经没有无参的构造函数了,所以就提示了上述错误。
如果想解决这个错误的话,就必须给Person
类的构造函数传入name
和age
字段,但是,Student
类的主构造函数中没有name
和age
,所以,需要给Student
类的主构造函数中也要加上name
和age
这两个参数,再将这两个参数传给Person
类的构造函数,代码如下所示:
class Student(val sno: String, val grade: Int, name: String, age: Int) : Person(name, age) { }
需要注意的是,在Student
类的主构造函数中增加name
和age
这两个字段时,不能再将它们声明成val
,因为在主构造函数中声明成val
或者var
的参数将自动成为该类的字段,这就会导致和父类中同名的name
和age
字段造成冲突。因此,这里的name
和age
参数前面我们加任何关键字,让它的作用域仅限定在主构造函数当中即可。
接口是用于实现多态编程的重要组成部分。Java
是单继承结构的语言,任何一个类最多只能继承一个父类,但是却可以实现任意多个接口,Kotlin
也是如此。
接口中可以含有变量和方法。接口中的变量会被隐式地指定为public static final
变量,而方法会被隐式地指定为public abstract
或public
方法(Java 8
支持接口方法默认实现)。 一般情况下不在接口中定义变量。
Java 8
引入了一个新特性——接口方法支持默认实现。这使得我们在向接口中新增方法时候,之前继承过该接口的类则可以不需要实现这个新方法。 以下是Java 8
版本的接口:
public interface Flyer {
public String kind();
default public void fly() {
System.out.println("I can fly");
}
}
接下来再来看看在Kotlin
中如何声明一个接口:
interface Flyer {
val speed: Int
fun kind(): String?
fun fly() {
println("I can fly")
}
}
Kotlin
允许对接口中定义的函数进行默认实现。 同时,它还支持抽象属性(如上面的speed
)。然而,Kotlin
是基于Java 6
的,那么它是如何支持这种行为的呢?将Kotlin
声明的接口转换为Java
代码,提取其中关键的代码:
public interface Flyer {
int getSpeed();
@Nullable
String kind();
void fly();
public static final class DefaultImpls {
public static void fly(@NotNull Flyer $this) {
String var1 = "I can fly";
boolean var2 = false;
System.out.println(var1);
}
}
}
Kotlin
编译器是通过定义了一个静态内部类DefaultImpls
来提供fly
方法的默认实现的。同时,虽然Kotlin
接口支持属性声明,然而它在Java
源码中是通过一个get
方法来实现的。在接口中的属性并不能像Java
接口那样,被直接赋值一个常量。 以下这样做是错误的:
interface Flyer {
var height = 1000 // error Property initializers are not allowed in interfaces
}
Kotlin
提供了另外一种方式来实现这种效果:
interface Flyer {
var height: Int
get() = 1000
set(value) = TODO()
}
Kotlin
接口中的属性背后其实是用方法来实现的,所以说如果要为变量赋值常量,那么就需要编译器原生就支持方法默认实现。但Kotlin
是基于Java 6
的,当时并不支持这种特性,所以不能像Java
那样给一个接口的属性直接赋值一个常量。
以下是在Kotlin
接口中如何定义一个普通属性:
interface Flyer {
val speed: Int
}
它同方法一样,若没有指定默认行为,则在实现该接口的类中必须对该属性进行初始化。 总的来说,Kotlin
的类与接口的声明和Java
很相似,但它的语法整体上要显得更加简洁。
我们在接口中定义一系列的抽象行为,然后由具体的类去实现。下面还是通过具体的代码来学习一下:
class Bird(override val speed: Int) : Flyer {
override fun kind(): String? { // 必须实现
TODO("Not yet implemented")
}
override fun fly() { // 非必须实现
super.fly()
}
}
Java
中继承使用的关键字是extends
,实现接口使用的关键字是implements
,而Kotlin
中统一使用冒号,中间用逗号进行分隔。另外,接口的后面不用加上括号,因为它没有构造函数可以去调用。
当一个类去实现Flyer
接口时,只会强制要求实现kind
函数,而fly
函数则可以自由选择实现或者不实现,不实现时就会自动使用默认的实现逻辑。
Kotlin
中使用override
关键字来重写父类或者实现接口中的函数。
val b = Bird(200)
b.fly() // I can fly
Java
是不支持类的多继承的,Kotlin
亦是如此。 为什么它们要这样设计呢?现实中,其实多继承的需求经常会出现,然而类的多继承方式会导致继承关系上语义的混淆。
C++
中的类是支持多重继承机制的。然而,C++
中存在一个经典的钻石问题——骡子的多继承困惑。我们假设Kotlin
的类也支持多继承,然后模仿C++
中类似的语法,来看看它到底会导致什么问题:
abstract class Animal { abstract fun run() } open class Horse : Animal() { override fun run() { println("I am run very fast") } } open class Donkey : Animal() { override fun run() { println("I am run very slow") } } class Mule : Horse(), Donkey() { }
这是一段伪代码,我们来分析下这段代码具体的含义:
Animal
类,并实现了Animal
中的run
抽象方法;Mule
利用多继承同时继承了Horse
和Donkey
;目前看起来没有问题,然而当我们打算在Mule
中实现run
方法的时候,问题就产生了:Mule
到底是继承Horse
的run
方法,还是Donkey
的run
方法?这个就是经典的钻石问题。可以通过继承关系图来更好地认识这个问题,如图所示:
所以钻石问题也被称为菱形继承问题。可以发现,类的多重继承如果使用不当,就会在继承关系上产生歧义。而且,多重继承还会给代码维护带来很多的困扰:一来代码的耦合度会很高,二来各种类之间的关系令人眼花缭乱。
于是,Kotlin
跟Java
一样只支持类的单继承。那么,面对多重继承的需求,我们在Kotlin
中该如何解决这个问题呢?
Kotlin
中的接口与Java
很相似,但它除了可以定义带默认实现的方法之外,还可以声明抽象的属性。来看看如何用Kotlin
中的接口来实现多继承:
interface Flyer { fun kind() = "flying animals" fun fly() { println("I can fly") } } 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() = super<Flyer>.kind() } fun main() { val bird = Bird("sparrow") println(bird.kind()) // I can fly }
Bird
类同时实现了Flyer
和Animal
两个接口,但由于它们都拥有默认的kind
方法,同样会引起上面所说的钻石问题。而Kotlin
提供了对应的方式来解决这个问题,那就是super
关键字,我们可以利用它来指定继承哪个父接口的方法, 比如上面代码中的 super<Flyer>.kind()
。当然我们也可以主动实现方法,覆盖父接口的方法。如:
override fun kind() = "a fly ${this.name}"
// a fly sparrow
通过这个例子,我们再来分析下实现接口的相关语法:
Kotlin
中实现一个接口时,需要实现接口中没有默认实现的方法及未初始化的属性,若同时实现多个接口,而接口间又有相同方法名的默认实现时,则需要主动指定使用哪个接口的方法或者重写方法;super<T>
这种方式调用它,其中T
为拥有该方法的接口名;override
关键字,不能省略;除此之外,我们通过主构造方法参数的方式来实现Animal
接口中的name
属性。通过val
声明的构造方法参数,其实是在类内部定义了一个同名的属性,所以我们当然还可以把name
的定义放在Bird
类内部:
class Bird(name: String) : Flyer, Animal {
override val name: String
init {
this.name = name
}
...
}
name
的赋值方式其实无关紧要。比如我们还可以用一个getter
对它进行赋值:
class Bird(n: String) : Flyer, Animal {
override val name: String
get() {
TODO()
}
...
}
getter
和setter
在Java
中通过这种方式来对一个类的私有字段进行取值和赋值的操作,通常用IDE
来帮我们自动生成这些方法。而Kotlin
类不存在字段,只有属性,它同样需要为每个属性生成getter
和setter
方法。但在Kotlin
的中,当声明一个类的属性时,Kotlin
编译器会帮忙生成getter
和setter
方法。 当然我们也可以主动声明这两个方法来实现一些特殊的逻辑。 还有以下两点需要注意:
val
声明的属性将只有getter
方法,因为它不可修改;而用var
修饰的属性将同时拥有getter
和setter
方法;private
修饰的属性编译器将会省略getter
和setter
方法,因为在类外部已经无法访问它了,这两个方法的存在也就没有意义了;总的来说,用接口模拟实现多继承是我们最常用的方式。但它有时在语义上依旧并不是很明确。下面我们就来看一种更灵活的方式,它能更完整地解决多继承问题。
我们知道,在Java
中可以将一个类的定义放在另一个类的定义内部,这就是内部类。由于内部类可以继承一个与外部类无关的类,所以这保证了内部类的独立性,我们可以用它的这个特性来尝试解决多继承的问题。Java
中的内部类定义非常直观,我们只要在一个类内部再定义一个类,那么这个类就是内部 类了,如:
public class OuterJava {
private String name = "This is Java's inner class syntax.";
class InnerJava { // 内部类
public void printName() {
System.out.println(name);
}
}
}
现在尝试用类似的Kotlin
代码来改写这段代码,看看有没有类似的效果:
class OuterKotlin {
val name = "This is not Kotlin's inner syntax"
class ErrorInnerKotlin { // 嵌套类
fun printName() {
println("the name is $name")
}
}
}
编译器报错:
在这里,我们声明的并不是Kotlin
中的内部类,而是嵌套类的语法。如果要在Kotlin
中声明一个内部类,我们必须在这个类前面加一个inner
关键字:
class OuterKotlin {
val name = "This is not Kotlin's inner syntax"
inner class ErrorInnerKotlin {
fun printName() {
println("the name is $name")
}
}
}
内部类vs嵌套类
在Java
中,我们通过在内部类的语法上增加一个static
关键词,把它变成一个嵌套类。然而,Kotlin
则是相反的思路,默认是一个嵌套类,必须加上inner
关键字才是一个内部类,就是说可以把静态的内部类看成嵌套类。
内部类和嵌套类的差别体现在:内部类包含着对其外部类实例的引用,在内部类中我们可以使用外部类中的属性,比如上面例子中的name
属性;而嵌套类不包含对其外部类实例的引用,所以它无法调用其外部类的属性。
我们就回到之前的骡子的例子,然后用内部类来改写它:
class Mule {
private inner class HorseC : Horse()
private inner class DonkeyC : Donkey()
fun runFast() {
HorseC().runFast()
}
fun doLongTimeThing() {
DonkeyC().doLongTimeThing()
}
}
通过这个修改后的例子可以发现:
HorseC
、DonkeyC
分别继承Horse
和Donkey
这两个外部类,我们就可以在Mule
类中定义它们的实例对象,从而获得了Horse
和Donkey
两者不同的状态和行为;private
修饰内部类,使得其他类都不能访问内部类,具有非常良好 的封装性;因此,可以说在某些场合下,内部类确实是一种解决多继承非常好的思路。
在Kotlin
中新引入了一种语法——委托,通过它我们也可以代替多继承来解决类似的问题。简单来说,委托是一种特殊的类型,用于方法事件委托,比如我们调用A
类的methodA
方法,其实背后是B
类的methodA
去执行。
在Kotlin
中,只需通过by
关键字就可以实现委托的效果。 比如之前提过的by lazy
语 法,其实就是利用委托实现的延迟初始化语法:
val laziness: String by lazy {
// 用by lazy实现延迟初始化效果
println("I will have a value")
"I am a lazy-initialized string"
}
委托除了延迟属性这种内置行为外,还提供了一种可观察属性的行为,这与我们平常所说的观察者模式很类似。接下来,我们来看看如何通过委托来代替多继承实现需求。请看下面的例子:
interface CanFlyer { fun fly() } interface CanEat { fun eat() } open class Flyer : CanFlyer { override fun fly() { println("I can fly") } } open class Animal : CanEat { override fun eat() { println("I can eat") } } class Bird(flyer: CanFlyer, animal: Animal) : CanFlyer by flyer, CanEat by animal {} fun main() { val flyer = Flyer() val animal = Animal() val b = Bird(flyer, animal) b.fly() b.eat() } // I can fly // I can eat
有人可能会有疑问:首先,委托方式怎么跟接口实现多继承如此相似,而且好像也并没有简单多少;其次,这种方式好像跟组合也很像,那么它到底有什么优势呢?主要有以下两点:
A
,委托对象是B
、C
、我们在具体调用的时候并不是像组合一样A.B.method
,而是可以直接调用A.method
,这更能表达A
拥有该method
的能力, 更加直观,虽然背后也是通过委托对象来执行具体的方法逻辑的;有时候,我们并不想要那么强大的类,也许我们只是想要单纯地使用类来封装数据,类似于Java
中的DTO(Data Transfer Object)
的概念。在Java
中声明一个JavaBean
,定义一堆getter
和setter
。虽然IDE
能帮我们自动生成这些代码,但是代码也十分冗长。下面就来看看Kotlin
是如何改进这个问题的。
JavaBean
在Java
中,定义一个数据模型类需要为其中的每一个属性定义getter
、setter
方法。如果要支持对象值的比较,还要重写 hashcode
、equals
等方法。比如下面的例子:
public class Bird { private double weight; private int age; private String color; public void fly() { } public Bird(double weight, int age, String color) { this.weight = weight; this.age = age; this.color = color; } public double getWeight() { return weight; } public void setWeight(double weight) { this.weight = weight; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } public String getColor() { return color; } public void setColor(String color) { this.color = color; } }
这是一个只有3
个属性的JavaBean
,但代码量却很大,如果想要更多的属性,一个JavaBean
将会有更多的代码。为了解决这个问题,Kotlin
引入了data class
的语法来改善这一情况。
data class
创建数据类data class
就是数据类。 为了搞明白数据类是什么,先把上面那段Java
代码用Kotlin
的data class
来表示:
data class Bird(var weight: Double, var age: Int, var color: String)
当一个类中没有任何代码时,还可以将尾部的大括号省略。 在Kotlin
中只添加了一个data
关键字。在这个关键字后面,Kotlin
编译器帮我们做了很多事情。将这个类反编译后的Java
代码:
public final class Bird { private double weight; private int age; @NotNull private String color; public final double getWeight() { return this.weight; } public final void setWeight(double var1) { this.weight = var1; } public final int getAge() { return this.age; } public final void setAge(int var1) { this.age = var1; } @NotNull public final String getColor() { return this.color; } public final void setColor(@NotNull String var1) { Intrinsics.checkNotNullParameter(var1, "<set-?>"); this.color = var1; } public Bird(double weight, int age, @NotNull String color) { Intrinsics.checkNotNullParameter(color, "color"); super(); this.weight = weight; this.age = age; this.color = color; } public final double component1() { return this.weight; } public final int component2() { return this.age; } @NotNull public final String component3() { return this.color; } @NotNull public final Bird copy(double weight, int age, @NotNull String color) { Intrinsics.checkNotNullParameter(color, "color"); return new Bird(weight, age, color); } // $FF: synthetic method public static Bird copy$default(Bird var0, double var1, int var3, String var4, int var5, Object var6) { if ((var5 & 1) != 0) { var1 = var0.weight; } if ((var5 & 2) != 0) { var3 = var0.age; } if ((var5 & 4) != 0) { var4 = var0.color; } return var0.copy(var1, var3, var4); } @NotNull public String toString() { return "Bird(weight=" + this.weight + ", age=" + this.age + ", color=" + this.color + ")"; } public int hashCode() { int var10000 = (Double.hashCode(this.weight) * 31 + Integer.hashCode(this.age)) * 31; String var10001 = this.color; return var10000 + (var10001 != null ? var10001.hashCode() : 0); } public boolean equals(@Nullable Object var1) { if (this != var1) { if (var1 instanceof Bird) { Bird var2 = (Bird)var1; if (Double.compare(this.weight, var2.weight) == 0 && this.age == var2.age && Intrinsics.areEqual(this.color, var2.color)) { return true; } } return false; } else { return true; } } }
这段代码和JavaBean
的代码很相似,同样有getter/setter
、equals
、hashcode
、构造函数等方法,其中的equals
和hashcode
使得一个数据类对象可以像普通类型的实例一样进行判等,我们可以像基本数据类型一样用==
来判断两个对象相等,如下:
val b1 = Bird(weight = 1000.0, age = 1, color = "blue")
val b2 = Bird(weight = 1000.0, age = 1, color = "blue")
println(b1.equals(b2)) // true
println(b1 == b2) // true
copy
、componentN
与解构在上面反编译的代码中有:
@NotNull public final Bird copy(double weight, int age, @NotNull String color) { Intrinsics.checkNotNullParameter(color, "color"); return new Bird(weight, age, color); } // $FF: synthetic method public static Bird copy$default(Bird var0, double var1, int var3, String var4, int var5, Object var6) { if ((var5 & 1) != 0) { var1 = var0.weight; } if ((var5 & 2) != 0) { var3 = var0.age; } if ((var5 & 4) != 0) { var4 = var0.color; } return var0.copy(var1, var3, var4); }
这段代码中的copy
方法的主要作用就是帮我们从已有的数据类对象中拷贝一个新的数据类对象。可以传入相应参数来生成不同的对象。但同时我们发现,在copy
的执行过程中,若未指定具体属性的值,那么新生成的对象的属性值将使用被copy
对象的属性值,这就是浅拷贝。来看下面这个例子:
val b1 = Bird(weight = 1000.0, age = 1, color = "blue")
val b2 = b1
b2.color = "red"
println(b2.color) // red
这种方式会带来一个问题,明明是对一个新的对象b2
做了修改,为什么还会影响老的对象b1
呢?实际上,除了基本数据类型的属性,其他属性还是引用同一个对象,这便是浅拷贝的特点。
实际上copy
更像是一种语法糖,假如我们的类是不可变的,属性不可以修改,那么我们只能通过copy
来帮我们基于原有对象生成一个新的对象。 比如下面的两个例子:
val b1 = Bird(weight = 1000.0, age = 1, color = "blue")
val b2 = b1
b2.color = "red" // error: Val cannot be reassigned
// 声明的Bird属性不可变
data class Bird(val weight: Double, val age: Int, val color: String)
val b1 = Bird(weight = 1000.0, age = 1, color = "blue")
val b2 = b1.copy(color = "red")
println(b2) // Bird(weight=1000.0, age=1, color=red)
copy
更像提供了一种简洁的方式帮我们复制一个对象,但它是一种浅拷贝的方式。所以在使用copy
的时候要注意使用场景,因为数据类的属性可以被修饰为var
,这便不能保证不会出现引用修改问题。
接下来我们来看看componentN
方法。简单来说,componentN
可以理解为类属性的值,其中N
代表属性的顺序,比如component1
代表第1
个属性的值,component3
代表第3
个属性的值。 那么,这样设计到底有什么用呢?我们来思考一个问题,我们或多或少地知道怎么将属性绑定到类上,但是对如何将类的属性绑定到相应变量上却不是很熟悉。比如:
// 普通方式
val b1 = Bird(weight = 1000.0, age = 1, color = "blue")
val weight = b1.weight
val age = b1.age
val color = b1.color
println("weight = $weight, age = $age, color = $color") // weight = 1000.0, age = 1, color = blue
// Kotlin
val b1 = Bird(weight = 1000.0, age = 1, color = "blue")
val (weight, age, color) = b1
println("weight = $weight, age = $age, color = $color") // weight = 1000.0, age = 1, color = blue
再比如以下的代码:
String birdInfo = "1000.0,1,blue";
String[] temps = birdInfo.split(",");
double weight = Double.parseDouble(temps[0]);
int age = Integer.parseInt(temps[1]);
String color = temps[2];
这样代码有时真的很烦琐,明明知道值的情况,却要分好几步来给变量赋值。Kotlin
提供了更优雅的做法:
val birdInfo = "1000.0,1,blue"
val (weight, age, color) = birdInfo.split(",")
println("weight = $weight, age = $age, color = $color") // weight = 1000.0, age = 1, color = blue
这个语法就是解构,通过编译器的约定实现解构。Kotlin
对于数组的解构也有一定限制,在数组中它默认最多允许赋值5
个变量,因为若是变量过多,效果反而会适得其反,到后期可能都搞不清楚哪个值要赋给哪个变量了。所以一定要合理使用这一特性。
在数据类中,除了可以利用编译器自动生成componentN
方法以外,甚至还可以自己实现对应属性的componentN
方法。比如:
data class Bird(var weight: Double, var age: Int, var color: String) { var sex = 1 operator fun component4(): Int { // operator关键字 return this.sex } constructor(weight: Double, age: Int, color: String, sex: Int) : this(weight, age, color) { this.sex = sex } } fun main() { val b1 = Bird(1000.0, 1, "blue") val (weight, age, color, sex) = b1 println("weight = $weight, age = $age, color = $color, sex = $sex") // weight = 1000.0, age = 1, color = blue, sex = 1 }
除了数组支持解构外,Kotlin
也提供了其他常用的数据类,让使用者不必主动声明这些数据类,它们分别是Pair
和Triple
。其中Pair
是二元组,可以理解为这个数据类中有2
个属性;Triple
是三元组,对应的则是3
个属性。 我们先来看一下它们的源码:
public data class Pair<out A, out B>( public val first: A, public val second: B ) : Serializable { public override fun toString(): String = "($first, $second)" } public data class Triple<out A, out B, out C>( public val first: A, public val second: B, public val third: C ) : Serializable { public override fun toString(): String = "($first, $second, $third)" }
Pair
和Triple
都是数据类,它们的属性可以是任意类型,我们可以按照属性的顺序来获取对应属性的值。 因此,我们可以这么使用它们:
val pair = Pair(20.1, 1)
val triple = Triple(30.0, 2, "blue")
// 利用属性顺序获取值
println("pair.first = ${pair.first}, pair.second = ${pair.second}") // pair.first = 20.1, pair.second = 1
println("triple.first = ${triple.first}, triple.second = ${triple.second}, triple.third = ${triple.third}") // triple.first = 30.0, triple.second = 2, triple.third = blue
// 利用解构
val (weightP, ageP) = Pair(20.0, 1)
val (weightT, ageT, colorT) = Triple(30.0, 2, "blue")
println("weightP = $weightP, ageP = $ageP") // weightP = 20.0, ageP = 1
println("weightT = $weightT, ageT = $ageT, colorT = $colorT") // weightT = 30.0, ageT = 2, colorT = blue
注意:数据类中的解构基于componentN
函数,如果自己不声明componentN
函数,那么就会默认根据主构造函数参数来生成具体个数的componentN
函数,与从构造函数中的参数无关。
如果要在Kotlin
声明一个数据类,必须满足以下几点条件:
var
或者val
进行声明;data class
之前不能用abstract
、open
、sealed
或者inner
进行修饰;Kotlin 1.1
版本前数据类只允许实现接口,之后的版本既可以实现接口也可以继承类;数据类在语法上很简洁,以至于它可以像Map
一样,作为数据结构被广泛运用到业务中。然而,数据类显然更灵活,因为它像一个普通类一样,可以把不同类型的值封装在一处。把数据类和when
表达式结合在一起,就可以提供更强大的业务组织和表达能力。
数据类的另一个典型的应用就是代替我们在Java
中的建造者模式。建造者模式主要化解Java
中书写一大串参数的构造方法来初始化对象的场景。然而由于Kotlin
中的类构造方法可以指定默认值,依靠数据类的简洁语法,我们就可以更方便地解决这个问题。
static
到object
在Java
中,static
是非常重要的特性,可以用来修饰类、方法或属性。虽然,static
修饰的内容是属于类的(而不是某个具体对象),但在定义时却与普通的变量和方法混杂在一起,显得格格不入。
在Kotlin
中,告别了static
这种语法,引入了全新的关键字object
,可以代替使用static
的所有场景。 当然除了代替使用static
的场景之外,它还能实现更多的功能,比如单例对象及简化匿名表达式等。
先来看一个可比较的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_REDPACK = 0; static int TYPE_COUPON = 1; static boolean isRedpack(Prize prize) { return prize.type == TYPE_REDPACK; } public static void main(String[] args) { Prize prize = new Prize("红包", 10, Prize.TYPE_REDPACK); System.out.println(Prize.isRedpack(prize)); } }
这是很常见的Java
代码,如果仔细思考,会发现这种语法其实并不是非常好。因为在一个类中既有静态变量、静态方法,也有普通变量、普通方法。静态变量和静态方法是属于一个类的,普通变量、普通方法是属于一个具体的对象的。虽然有static
作为区分,然而在代码结构上并不是区分得很清晰。
那么,有没有一种方式能将这两部分代码清晰地分开,但又不失语义化呢?Kotlin
中引入了伴生对象的概念,简单来说,这是一种利用companion object
两个关键字创造的语法。
companion [kəmˈpænjən] 陪伴
“伴生”是相较于一个类而言的,意为伴随某个类的对象,它属于这个类所有,因此伴生对象跟Java
中static
修饰效果性质一样,全局只有一个实例(单例)。它需要声明在类的内部,在类被装载时会被初始化。
现在来实现一个伴生对象的版本:
class Prize(val name: String, val count: Int, val type: Int) {
companion object {
val TYPE_REDPACK = 0
val TYPE_COUPON = 1
fun isRedpack(prize: Prize): Boolean {
return prize.type == TYPE_REDPACK
}
}
}
fun main() {
val prize = Prize("红包", 10, Prize.TYPE_REDPACK)
println(Prize.isRedpack(prize)) //true
}
可以发现,该版本在语义上更清晰了。companion object
用花括号包裹了所有静态属性和方法,使得它可以与Prize
类的普通方法和属性清晰地区分开来。最后,我们可以使用点号来对一个类的静态的成员进行调用。
伴生对象的另一个作用是可以实现工厂方法模式,然而这种方式存在以下缺点:
你会发现,伴生对象也是实现工厂方法模式的另一种思路,可以改进以上的两个问题:
class Prize(val name: String, val count: Int, val type: Int) { companion object { val TYPE_COMMON = 1 val TYPE_REDPACK = 2 val TYPE_COUPON = 3 val defaultCommonPrize = Prize("普通奖品", 10, Prizes.TYPE_COMMON) fun newRedpackPrizs(name: String, count: Int) = Prize(name, count, Prizes.TYPE_REDPACK) fun newCouponPrize(name: String, count: Int) = Prize(name, count, Prizes.TYPE_COUPON) fun defaultCommonPrize() = defaultCommonPrize // 无须构造对象 } } fun main() { val redpackPrize = Prize.newRedpackPrizes("红包", 10) val couponPrize = Prize.newCouponPrizes("十元代金券", 10) val commonPrize = Prize.defaultCommonPrizes() }
总的来说,伴生对象是Kotlin
中用来代替static
关键字的一种方式,任何在Java
类内部用static
定义的内容都可以用Kotlin
中的伴生对象来实现。然而,它们是类似的,一个类的伴生对象跟一个静态类一样,全局只能有一个。 这让我们联想到了单例对象,下面会介绍如何用object
更优雅地实现Java
中的单例模式。
object
单例模式最大的一个特点就是在系统中只能存在一个实例对象,所以在Java
中必须通过设置构造方法私有化,以及提供静态方法创建实例的方式来创建单例对象。 比如,现在要创建一个数据库配置的单例对象:
public class Singleton {
private static Singleton instance;
private Singleton() {
}
public synchronized static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
这是用Java
实现的一个最基本单例模式的精简例子(省略了多线程以及多种参数创建单例对象的方法)。它依赖static
关键字,同时还必须将构造方法私有化。这段代码其实很好理解,首先为了禁止外部创建Singleton
的实例,需要用private
关键字将Singleton
的构造函数私有化,然后给外部提供了一个getInstance()
静态方法用于获取Singleton
的实例。在getInstance()
方法中,判断如果当前缓存的Singleton
实例 为null
,就创建一个新的实例,否则直接返回缓存的实例即可,这就是单例模式的工作机制。
如果想调用单例类中的方法,可以这样写:
Singleton singleton = Singleton.getInstance();
虽然Java
中的单例实现并不复杂,但是,在Kotlin
中,由于object
的存在,可以直接用它来实现单例,它将一些固定的、重复的逻辑实现隐藏了起来,只暴露给我们最简单方便的用法。 如下所示:
object Singleton {
}
现在Singleton
就已经是一个单例类了。由于用object
声明的对象全局只有一个,所以它并不用语法上的初始化,甚至都不需要构造方法。因此,可以说,object
创造的是天生的单例, 我们并不需要在Kotlin
中去构建一个类似Java
的单例模式。
可以看到,在Kotlin
中不需要私有化构造函数,也不需要提供getInstance()
这样的静态方法,只需要把class
关键字改成object
关键字,一个单例类就创建完成了。 这种写法虽然看上去像是静态方法的调用,但其实Kotlin
在背后自动帮我们创建了一个 Singleton
类的实例,并且保证全局只会存在一个Singleton
实例,以下是反编译后的代码:
public final class Singleton {
@NotNull
public static final Singleton INSTANCE;
private Singleton() {
}
static {
Singleton var0 = new Singleton();
INSTANCE = var0;
}
}
由于单例也可以和普通的类一样实现接口和继承类,所以可以将它看成一种不需要我们主动初始化的类,它也可以拥有扩展方法。单例对象会在系统加载的时候初始化,当然全局就只有一个。
那么,object
声明除了表现在单例对象及上面的说的伴生对象之外,还有其他的作用吗?它还有一个作用就是替代Java
中的匿名内部类。下面我们就来看看它是如何做的。
object
表达式Java
中的匿名类很繁琐,有时候明明只有一个方法,却要用一个匿名内部类去实现。比如我们要对一个字符串列表排序:
List<String> list = Arrays.asList("redpack", "source", "card");
Collections.sort(list, new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
if (o1 == null) return -1;
if (o2 == null) return 1;
return o1.compareTo(o2);
}
});
并不是说匿名内部类这个方式不好,只不过方法内掺杂类声明不仅让方法看起来复杂,也不易阅读理解。而在Kotlin
中,可以利用object
表达式对它进行改善:
val list = Arrays.asList("redpack", "source", "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
表达式跟Java
的匿名内部类很相似,object
表达式可以赋值给一个变量,这在我们重复使用的时候将会减少很多代码。另外,object
可以继承类和实现接口,匿名内部类只能继承一个类及实现一个接口,而object
表达式却没有这个限制。
用于代替匿名内部类的object
表达式在运行中不像我们在单例模式中说的那样,全局只存在一个对象,而是在每次运行时都会生成一个新的对象。
其实,匿名内部类与object
表达式并不是对任何场景都适合的,Java 8
引入的Lambda
表达式对有些场景实现起来更加适合,比如接口中只有单个方法的实现。而Kotlin
天然就支持Lambda
表达式,现在我们可以将上面的代码用Lambda
表达式的方式重新改造一下:
val list = Arrays.asList("redpack", "source", "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)
使用Lambda
表达式后代码变得简洁很多。
对象表达式与Lambda
表达式哪个更适合代替匿名内部类?当你的匿名内部类使用的类接口只需要实现一个方法时,使用Lambda
表达式更适合;当匿名内部类内有多个方法实现的时候,使用object
表达式更加合适。
在了解抽象类之前,先来了解一下抽象方法。抽象方法是一种特殊的方法:它只有声明,而没有具体的实现。 抽象方法的声明格式为:
abstract fun method()
抽象方法必须用abstract
关键字进行修饰。如果一个类含有抽象方法,则称这个类为抽象类,抽象类必须在类前用abstract
关键字修饰。因为抽象类中含有无具体实现的方法,所以不能用抽象类创建对象。
在《JAVA编程思想》一书中,将抽象类定义为“包含抽象方法的类”,但是后面发现如果一个类不包含抽象方法,只是用abstract
修饰的话也是抽象类。也就是说抽象类不一定必须含有抽象方法。
如果有抽象方法的类不是抽象类:
定义一个抽象类:
abstract class Bird {
abstract fun method()
fun method1() {
}
}
可以看出,抽象类就是为了继承而存在的,如果定义了一个抽象类,却不去继承它,那么等于白白创建了这个类,因为不能用它来做任何事情。 对于一个父类,如果它的某个方法在父类中实现出来没有任何意义,必须根据子类的实际需求来进行不同的实现,那么就可以将这个方法声明为abstract
方法,此时这个类也就成为abstract
类了。
包含抽象方法的类称为抽象类,但并不意味着抽象类中只能有抽象方法,它和普通类一样,同样可以拥有成员变量和普通的成员方法。
注意,抽象类和普通类的主要有三点区别:
public
或者protected
,如果为private
,则不能被子类继承,缺省情况下默认为public
;abstract
类。public static final
类型的;举例来说:门都有open
和close
两个动作,可以定义通过抽象类和接口来定义这个抽象概念:
abstract class Door {
abstract fun open()
abstract fun close()
}
或者:
interface Door {
fun open()
fun close()
}
但是现在如果我们需要门具有报警alarm
的功能,该如何实现?下面提供两种思路:
open
和close
,也许这个类根本就不具备open
和close
这两个功能,比如火灾报警器。从这里可以看出, Door
的open
、close
和alarm
根本就属于两个不同范畴内的行为,open
和close
属于门本身固有的行为特性,而alarm
属于延伸的附加行为。因此最好的解决办法是单独将报警设计为一个接口,包含alarm
行为,Door
设计为单独的一个抽象类,包含open
和close
两种行为。再设计一个报警门继承Door
类和实现Alarm
接口。
abstract class Door { abstract fun open() abstract fun close() } interface Alarm { fun alarm() } class AlarmDoor:Door(),Alarm{ override fun open() { TODO("Not yet implemented") } override fun close() { TODO("Not yet implemented") } override fun alarm() { TODO("Not yet implemented") } }
封装是把数据和操作数据的方法绑定起来,只能通过已定义的接口对数据进行访问。 面向对象的本质就是将现实世界描绘成一系列完全自治,封闭的对象。可以说,封装就是隐藏一切可隐藏的东西,只向外界提供最简单的编程接口。
优势:隐藏类的实现细节,只能通过规定的方法访问数据;
步骤:
private
getter/setter
方法,用于属性的读写。通过这两种方法对数据进行获取和设定,对象通过调用这两种发方法实现对数据的读写;getter/setter
方法中加入属性控制语句,对属性值的合法性进行判断抽象是将一类对象的共同特征总结出来构造类的过程,包括数据抽象和行为抽象两方面。抽象只关注对象有哪些属性和行为,并不关注这些行为的细节是什么。
继承是从已有类得到继承信息创建新类的过程。提供继承信息的类被称为父类(超类、基 类);到继承信息的类被称为子类(派生类)。
多态:多态就是同一个接口,使用不同的实例而执行不同操作。
多态存在的三个必要条件:继承,重写,父类引用指向子类对象(Parent p = new Child();
)
当使用多态方式调用方法时,首先检查父类中是否有该方法,如果没有,则编译错误;如果有,再去调用子类的同名方法。
多态的好处:可以使程序有良好的扩展,并可以对所有类的对象进行通用处理。 以下是一个多态实例:
abstract class Animal { abstract fun eat() } class Cat : Animal() { override fun eat() { println("eat fish") } fun work() { println("catch mouse") } } class Dog : Animal() { override fun eat() { println("eat bone") } fun work() { println("look after the house") } } fun show(a: Animal) { a.eat() if (a is Cat) { a.work() } if (a is Dog) { a.work() } } fun main() { show(Cat()) // eat fish catch mouse show(Dog()) // eat bone look after the house val a = Cat() a.eat() // eat fish a.work() // catch mouse }
https://zhuanlan.zhihu.com/p/142226680
https://zhuanlan.zhihu.com/p/28427324
https://blog.csdn.net/qq_42429369/article/details/84929377
https://blog.csdn.net/weixin_43444439/article/details/84501621
https://www.runoob.com/java/java-polymorphism.html
https://www.cnblogs.com/dolphin0520/p/3811437.html
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。