多态定义
多态是指允许不同类的对象对同一消息做出相应,即对同一消息可以根据发送对象的不同而采用不同的行为方式。(发送消息就是函数调用)
open class Father{
open fun bar(){
println("father.bar")
}
}
open class Son:Father(){
override fun bar() {
println("son.bar")
}
}
class GrandSon:Son(){
override fun bar() {
println("grandSon.bar")
}
}
class Play{
fun play(father: Father){
father.bar()
}
}
fun main() {
val play = Play()
play.play(Father())
play.play(Son())
play.play(GrandSon())
}
father.bar
son.bar
grandSon.bar
重写
面向接口编程就是多态的一种技术实现方式,多态的一个很显著的特定就是运行期绑定,也叫动态绑定。所谓的动态绑定就是指在程序中定义的变量,其指向的真实类似和通过该变量发出的方法调用,在静态编译期并不能确定,而是要等到运行期才能确定。
总体而言,在java和kotlin中,通常使用的最多的动态绑定机制是通过如下三种编程技巧来实现的:
- 纯粹的类继承,如上所示
- 子类继承虚类,子类实现虚类中的抽象方法
- 面向接口编程
这三种编程技巧都能在运行期进行动态绑定,在编译期,开发者也不知道类变量最终会指向哪一个方向。
重载
重载发生在同一个类中,与父类,子类和继承毫无关系,重载是指在同一个类中定义了多个拥有相同名字的方法,但是这些同名方法的参数列表不同。这些函数之间其实各不相同,只是可能功能类似,所以才采用相同的命名,增加刻度性,仅此而已。
class MyPrint{
fun print(str:String){
println("strValue = $str")
}
fun print(intValue:Int){
println("intValue = $intValue")
}
fun print(longValue:Long){
println("longValue = $longValue")
}
fun print(charValue:Char){
println("charValue = $charValue")
}
}
fun main() {
val p = MyPrint()
p.print("我来自大中国")
p.print(22)
p.print(12L)
p.print('A')
}
strValue = 我来自大中国
intValue = 22
longValue = 12
charValue = A
扩展
扩展方法使你能够向现有类 “添加”方法,而无需穿件新的派生类,重新编译或以其他方式修改原始类。
在kotlin中为核心类 编写扩展方法,下面例子为String 添加一个扩展方法:
fun String.toMyStr():String{
return "$this ===> add My String"
}
fun String.toFloat(digits:Int):Float{
return try {
this.substring(0,digits).toFloat()
}catch (e:Exception){
0f
}
}
fun main() {
val str = "12.2563"
println(str.toMyStr())
println(str.toFloat(5))
}
12.2563 ===> add My String
12.25
也可以给自定义类添加扩展函数:
class MyClass(val name: String) {
fun foo() {
println("$name print foo")
}
}
//无返回的扩展函数
fun MyClass.bar() {
//扩展函数 可以获取到 所在类的字段属性
println("$name bar fun run ... ")
}
fun main() {
val myCla = MyClass("王麻子")
myCla.foo()
myCla.bar()
}
王麻子 print foo
王麻子 bar fun run ...
扩展与重载
看起来似乎函数扩展无所不能,但其实也有一个内在的限制——所扩展的函数不能与类内部已有的函数签名相同,或者套用多态里面的术语来说,扩展函数不能是对类已有函数的 “重写”,虽然在语法上允许这么做,但是在运行期它会失效(保证安全,核心类库的方法不能被篡改),但是函数扩展可以对类已有函数进行重载。
class MyClass(val name: String) {
fun foo() {
println("$name print foo")
}
}
//无返回的扩展函数
fun MyClass.foo() {
//扩展函数 可以获取到 所在类的字段属性
println("$name extends foo fun run ... ")
}
fun MyClass.foo(int: Int){
println("extends foo print int = $int")
}
fun main() {
val myCla = MyClass("王麻子")
myCla.foo()
myCla.foo(22)
}
王麻子 print foo
extends foo print int = 22
函数扩展的多态性
在对类方法进行 “重载”式扩展时,如果重载函数与类函数的入参类型不同,但是参数类型之间有继承关系,结果会怎样呢?(不会执行扩展函数)
open class Animal(val name: String) {
}
class Dog(name: String):Animal(name){
}
class MyClass{
fun foo(animal: Animal){
println("animal : ${animal.name}")
}
}
fun MyClass.foo(dog: Dog){
println("dog : ${dog.name}")
}
fun main() {
val animal = Animal("小飞鼠")
val dog = Animal("雪地三傻 之 拆家二哈")
val cls = MyClass()
cls.foo(animal)
cls.foo(dog)
}
animal : 小飞鼠
animal : 雪地三傻 之 拆家二哈
kotlin这么设计的原因还是因为安全,如果扩展函数可以代替核心类库的自身方法,那么核心类库将变的不安全。
- 函数扩展的多态性研究
open class Animal(val name: String) {
}
class Dog(name: String) : Animal(name) {
}
/**为 父类 和 子类 同时 扩展同一个函数*/
fun Animal.eat() {
println("animal $name eat food")
}
fun Dog.eat() {
println("dog $name eat food")
}
class MyClass {
fun foo(animal: Animal) {
animal.eat()
}
}
fun main() {
val animal = Animal("小飞鼠")
val dog = Dog("大金毛")
val cls = MyClass()
cls.foo(animal)
cls.foo(dog)
}
animal 小飞鼠 eat food
animal 大金毛 eat food
为什么会出现这样的情况呢?是因为kotlin在对扩展函数进行绑定时,使用的是“静态绑定”机制,即在编译期绑定,而非运行时,由于是静态绑定,编译期根本不知道实际运行期的基类变量指针究竟会指向哪一个子类,因此便索性不做任何猜想,编译器调用的是哪一个类的扩展函数,运行期便也调用这个类的扩展函数,即使这个类实际不指向其所对应的实例对象。由此可以进一步得出,函数扩展与多态之间存在本质的区别:
- 函数扩展一定是静态绑定
- 多态的重写是动态绑定
将上面的例子改成多态就可以符合我们的预期了:
open class Animal(val name: String) {
open fun eat(){
println("animal $name eat food")
}
}
class Dog(name: String) : Animal(name) {
override fun eat() {
println("dog $name eat food")
}
}
class MyClass {
fun foo(animal: Animal) {
animal.eat()
}
}
fun main() {
val animal = Animal("小飞鼠")
val dog = Dog("大金毛")
val cls = MyClass()
cls.foo(animal)
cls.foo(dog)
}
animal 小飞鼠 eat food
dog 大金毛 eat food
函数扩展原理
函数扩展并没有向类中间插入一个方法,也没有改变类的源码,函数扩展其实是编译器所使用的一种 “障眼法”——当程序被编译后,其实这种扩展函数就变成了一个我们平常写的工具方法。换言之,扩展函数的这种声明方式:
fun String.str2Int():Int
在编译后会变成下面这种传统的声明方式:
fun str2Int(str:Int):Int
属性扩展
在扩展类属性的时候,需要遵循如下3个规定:
- 扩展类属性时,只能使用val关键字,而不能使用var关键字
- 扩展类属性时,必须明确声明属性的类型
- 扩展类属性时,在get()访问器中不能使用field关键字段
class Animal {}
//扩展属性,不能使用默认赋值的方式
val Animal.name: String
get() = "大熊猫"
fun main() {
val animal = Animal()
println(animal.name)
}
操作符重载
相比于java,kotlin还提供了一种强大的能力——操作符重载,通过操作符重载,可以自定义两个变量执行相加,相乘,相除等运算行为。
在kotlin中重载操作符很简单,只需要在函数前面添加operator关键字,就表示该函数是对操作符的重载。
open class Arith(var value:Int){
/**
* 重载乘法运算
*/
operator fun times(arith: Arith):Arith{
this.value = this.value * arith.value
return this
}
/**
* 重载加法运算
*/
operator fun plus(int: Int):Arith{
this.value = this.value + int
return this
}
override fun toString(): String {
return value.toString()
}
}
fun main() {
//本来数学的四则运算,只能用于类型都是数字的型的操作数,而通过操作符重载,我们就可以突破这一约束
val result = Arith(11) * Arith(9)
println(result)
println(Arith(6).times(Arith(12)))
println(Arith(7) + 12)
}
99
72
19
通过扩展函数重载操作符
在上面的例子中,对Arith 类型重载了 plus 加法操作符,因此可以执行 arith + 2 操作,但是如果写成 2+arith就会报错。 解决办法就是给Int类添加扩展函数:
operator fun Int.plus(arith: Arith):Int{
return this + arith.value
}
fun main() {
val result = 2 + Arith(12);
}
操作符重载原理
总体而言,操作符重载使用了下面两种手段:
- 如果在类的内部定义了操作符重载函数,则kotlin会将使用该类进行相应运算的地方都替换成对重载函数的调用
- 如果通过扩展的方式在外部为类重载了操作符,则kotlin会将其变成普通的方法(对应java中的不可变静态方法),并将使用该类进行相应运算的地方都替换成对该顶级方法的调用
如上面的Arith 示例,在其类内部重载了 times(),则在main 函数中,通过如下方式使用:
println(Arith(6) * Arith(12))
经过编译了,kotlin会将这里的乘法关键字进行替换,替换成如下形式:
println(Arith(6).times(Arith(12)))
操作符重载限制
当重载操作符运算函数时,不仅仅要在函数签名的最前面天机 “operator” 关键字,还需要确定正确的函数名——运算符重载的函数名称是受限的,并不是想怎么命名就 怎么命名的,在kotlin中,对每一种数学运算都规定了唯一的与之对应的重载函数名。必须乘法就只能是 times 并有一个入参 和 返回值。
中缀符
除了可以直接重载运算符外,kotlin 还提供了一种逆天的功能——中缀符。所谓中缀符,就是连个操作数中间的运算符,通过中缀符,可以将如下表达式:
a + b
改成下面这种形式:
a plus b
定义一个中缀符函数,必须包含关键字 infix ,除此之外,其他的看起来就像是一个扩展函数,下面的例子,为Int 类型的加法运算定义中缀符函数:
//定义一个加法中缀符
infix fun Int.add(x:Int):Int{
return this + x
}
//定义一个除法中缀符
infix fun Int.divX(y:Int):Int{
return this / y
}
fun main() {
val result = 2 add 22
println(result)
println(66 divX 22)
}
指针与传递
kotlin作为一种面向对象的编程语言,与java一样,将“指针”的语法彻底隐藏起来,不暴露给开发者。但是指针并没有真正的消失。而是披了个马甲,被使了障眼法。kotlin和java都通过对象引用符号(也即类型变量)持有对一块连续内存地址的引用,这种引用其实就是指针,开发者可以通过相关的文法规则,基于这块内存地址访问其中处于特定偏移位置的数据和方法。而在JVM虚拟机层面,这种引用被转换为真正的指针,物理CPU可以识别并基于此进行内存寻址。
在java和kotlin中,不仅可以安全的使用指针,而且还玩出了不少花样,例如:在类内部,使用 this关键字就可以访问类本身。再如,一个接口指针可以指向其实现的子类对象实例。在调用函数时,对于类型对象也通过指针进行传递。尤其在传递函数参数的过程中,需要区分变量的类型——值类型还是引用类型,这对函数传参特别重要——必须知道参数被传递进参数之后,其值会不会被修改。
- java中的函数参数传递,只是复制了一个值进去,没有引用,所有函数内参数如何改变也不影响外面的参数。
private static void swap(Integer x, Integer y){
Integer temp = x;
x = y;
y = temp;
System.out.println("swap ---> x = " + x + ", y = " + y);
}
public static void main(String[] args) {
Integer x = 22;
Integer y = 33;
swap(x,y);
System.out.printf(" x = %s , y = %s",x,y);
}
swap ---> x = 33, y = 22
x = 22 , y = 33
- 按值/引用传递的终结者
由于java的 “按值传递”和“按引用传递”的概念曾经坑过太多的人,因此kotlin干脆从语法层面杜绝了这种情况的发生,看如下示例:
fun swap(x:Int,y:Int){
var temp = x;
x = y
y = temp
}
这个函数在kotlin中会报错,它会提示你不能对入参进行修改,这是因为在kotlin中,函数入参的类型一律变成了val,也即常量类型,而常量类型在kotlin中是只可读的,而不能被修改的。kotlin通过这种机制,避免开发者在开发的时候,被所谓的 “按值传递”和“按引用传递”所害。如果需要做到按引用传递的效果,就需要包装,包装一个类,传递引用进去,java和kotlin一样。
class Pair(var x:Int,var y:Int){
override fun toString(): String {
return "x = $x , y = $y"
}
}
fun swap(pair: Pair){
val temp = pair.x
pair.x = pair.y
pair.y = temp
}
fun main() {
val pair = Pair(22,66)
swap(pair)
println(pair)
}
x = 66 , y = 22
- this指针
在java和kotlin中,虽然没有了指针的概念,但是变量名称其实就是一个指针(非基本类型)。在JVM虚拟机内部,变量名会被处理成指向某个地址的值。
1.在构造函数中使用this指针 (在类中,this表示类型实例对象本身)
class Language{
var name:String = ""
//除了构造函数,其他普通函数也可以使用this 指向类型实例对象本身
constructor(name: String){
this.name = name
}
}
2.在内部类中使用this指针
open class Outer{
override fun toString(): String {
return "outer class... "
}
inner class Inner{
override fun toString(): String {
return "inner class"
}
fun foo(){
println(this)//访问内部类
println(this@Outer)//访问外部类
}
}
}
fun main() {
val inn = Outer().Inner();
inn.foo()
}
inner class
outer class...
3.在扩展函数中使用this指针
当一个函数处于内部类中,同时该方法本身又是一个扩展函数时,情况就变得复杂了——扩展函数本身是可以使用this关键字的,而内部类的函数也是可以使用this关键字的,那么在这种情况下,应该如何正确的使用this指针呢?kotlin给出的解决方案依然是通过标签:
- 在扩展函数内部直接使用this,则this指代被扩展的类型实例对象
- 如果扩展函数位于内部类,则通过this加标签的方式分别指代内部类和内部类的宿主外部类的实例对象
open class Outer(val value:Int){
override fun toString(): String {
return "outer class... "
}
inner class Inner(val value:Int){
override fun toString(): String {
return "inner class"
}
/**
* 为Int 扩展函数
*/
fun Int.foo(){
println(this)//访问 int 实例
println(this@Outer)//访问外部类
println(this@Inner)//访问内部类
println([email protected])//访问外部类属性
println([email protected])//访问内部类属性
}
fun foo(){
val a = 3
a.foo()
}
}
}
fun main() {
val inn = Outer(12).Inner(55)
inn.foo()
}
3
outer class...
inner class
12
55
- 函数调用机制与this
在类内部的函数中可以访问类的属性,可以可以调用该类的其他函数,通过this调用。那么在运行期间是如何处理this的,JVM虚拟机怎样将其与类实例对象进行关联呢?
open class MyClass(val value:Int){
override fun toString(): String {
return this.value.toString()
}
fun print(){
println(this.toString())
}
}
fun main() {
val cls = MyClass(44)
cls.print()
}
在上面这个例子中,MyClass.toString() 方法内部通过this访问了该类的value属性,同时该类的print()方法内部通过this调用了该类重写的toString()方法。
为了处理this与实际所实例化处理的MyClass对象之间的绑定关系,编译器就偷偷的做了手脚,首先编译器会修改你所定义的每一个类方法,为这些方法增加一个入参,该入参类型就是方法所属的类型,并且编译器会将该入参插入到方法的第一个参数位置,该方法原有的参数都会往后顺延。以这个类为例子编译后如下:
override fun toString(MyClass cls): String {
return cls.value.toString()
}
fun print(MyClass cls){
println(cls.toString())
}
接着这些方法的调用也会被修改成正确的传参方式。例如本例中在main()函数中调用了类方法,则编译器会对其进行修改,修改后的main()函数变成:
fun main(){
val cls = MyClass(44)
print(cls)
}
kotlin 正是通过这种“腾挪”第一个入参的机制,完成类方法内部this指针与类实例对象之间的绑定的。