Kotlin知识归纳(九) —— 约定

2,296 阅读10分钟

前序

      Java在标准库中,有一些与特定的类相关联的语言特性。比如,实现 java.lang.Iterable 接口的对象可以在forEach循环中使用。Kotlin也提供很多类似原理的特性,但是是通过调用特定的函数,来实现特定的语言特性,这种技术称之为约定。(例如,实现名为plus特殊方法的类,可以在该类的对象上使用 + 运算符)

      因为类实现的接口集是固定的,Kotlin不能为了实现某个语言特性,而修改现有的Java类。但也可以通过把任意约定的方法定义为Java类的扩展方法,使其具备Kotlin约定的能力。

      Kotlin不允许开发者自定义自己的运算符,因为Kotlin限制了你能重载的运算符,以及运算符对应的函数名称。

算术运算符重载

      在Java中,只有基本数据类型才可以使用算术运算符,String类型也仅局限于使用 + 运算符,对于其他类不能使用算术运算符。

      Kotlin中使用约定最直接的例子就是算术运算符,意味着只要实现约定对应的方法,就可以对任意类型使用算数运算符。约定对应的方法都需要使用operator关键字修饰的,表示你将该方法作为相应的约定的实现。

二元算术运算符

运算符 函数名 表达式 转换
*(乘法运算符) times a * b a.times(b)
/(除法运算符) div a / b a.div(b)
%(取模运算符) rem a % b a.rem(b)
+(加法运算符) plus a + b a.plus(b)
-(减法运算符) minus a - b a.minus(b)

对于自定义类型的算术运算符,与基本数据类型的算术运算符具有相同的优先级。

      operator函数不要求两边运算数类型相同。但不可将两边运算数进行交换运算,因为Kotlin不自动支持交换性。想要支持交换性,需要在两边的运算类型中定义相应的算术运算符的函数。

      Kotlin不要求返回值类型必须和运算数类型相同。也允许对约定的函数进行重载,即定义多个参数类型不同operator函数。

data class Point(var x:Int,var y:Int)

operator fun Point.plus(point: Point):Point{
    return Point(x + point.x,y + point.y)
}

//定义另类的operator函数
operator fun Point.plus(value: Int){
    println("x = ${x + value} y = ${y + value}")
}

fun main(args:Array<String>){
    val point1 = Point(3,4)
    val point2 = Point(3,4)
    println(point1 + point2)
    println(point1 + 1)
}

运算符函数与Java

      Java中调用Kotlin的运算符非常简单,只需要像普通函数一样调用运算符对应的函数。但由于Java中没有operator关键字,所以Java中定义约定的具体函数时,唯一的约束是需要参数的 类型 和 数量 匹配。

在Java中定义两个加法运算符的plus方法:

#daqi.java
public class Point {
    public int x;
    public int y;

    public Point(int x ,int y){
        this.x = x;
        this.y = y;
    }

    public Point plus(Point p){
        return  new Point(x + p.x, y + p.y);
    }

    public Point plus(int p){
        return  new Point(x + p, y + p);
    }

    @Override
    public String toString() {
        return "x = " + x + " , y = " + y;
    }
}

在Kotlin中为Java类声明约定的扩展函数,并使用加法运算符:

#daqiKotlin.kt

//将约定的函数声明为Java类的扩展函数
operator fun Point.plus(longNum:Long):Point{
    return Point(this.x + longNum.toInt(), this.y + longNum.toInt())
}

fun main(args:Array<String>){
    var point1 = Point(3,4)
    var point2 = Point(4,5)
    //使用Java定义的运算符函数
    println(point1 + point2)
    println(point1 + 1)
    println(point2 + 1L)
}


      扩展函数可以很好的对现有的Java类添加Kotlin运算符的能力,但还是要遵从扩展函数不能访问privateprotected修饰的属性或方法的特性。

复合辅助运算符

      Kotlin除了支持简单的算术运算符重载,还支持复合赋值运算符重载,即 += 、-=等复合赋值运算符。

运算符 函数名 表达式 转换
*= timesAssign a *= b a.timesAssign(b)
/= divAssign a /= b a.divAssign(b)
%= remAssign a %= b a.remAssign(b)
+= plusAssign a += b a.plusAssign(b)
-= minusAssign a -= b a.minusAssign(b)

      当在某类型中定义了返回该类型的基本算术运算符的operator函数,且右侧运算数的类型符合该operator函数的参数的情况下,可以使用复合辅助运算符。例如,定义不同参数类型的plus函数:

operator fun Point.plus(point: Point):Point{
    x += point.x
    y += point.y
    return this
}

operator fun Point.plus(value: Int):Point{
    x += value
    y += value
    return this
}

借助plus函数使用 复合赋值运算符+= :

fun main(args: Array<String>) {
    var point1 = Point(3,4)
    var point2 = Point(4,5)
    point2 += point1
    point2 += 1
}

      这意味着,使用复合辅助运算符时,基本算术运算符的方法和复合赋值运算符的方法都可能被调用。当存在符合两侧运算数类型的基本算术运算符的operator方法和复合赋值运算符的operator方法时,编译器会报错。解决办法是:

  • 将运算符转换为对应的operator方法,直接调用方法。
  • 用val替代var,使编译器调用复合赋值运算符的该operator方法(例如:plusAssign)

运算符与集合

      Kotlin标准库中支持集合的使用 + 、- 、+= 和 -= 来对元素进行增减。+ 和 - 运算符总是返回一个新的集合,+= 和 -= 运算符始终就地修改集合

一元运算符和位运算符

运算符 函数名 表达式 转换
+ unaryPlus +a a.unaryPlus()
- unaryMinus -a a.unaryMinus()
! not !a a.not()
++ inc a++、++a a.inc()
-- dec a--、--a a.dec()

      当定义incdec函数来重载自增和自减运算符时,编译器会自动支持与普通数字类型的前缀和后缀自增运算符相同的语义。例如,调用前缀形式 ++a,其步骤是:

  • 把 a.inc() 结果赋值给 a
  • 把 a 的新值作为表达式结果返回。

比较运算符

      与算术运算符一样,Kotlin允许对任意类型重载比较运算符(==、!=、>、<等)。可以直接使用运算符进行比较,不用像Java调用equalscompareTo函数。

等号运算符

      如果在Kotlin中使用 == 运算符,它将被转换成equals方法的调用。!=运算符也会被转换为equals方法的调用,但结果会取反。

      与其他运算符不同,== 和 != 可以用于可空运算数,因为这些运算符会检查运算数是否为null。null == null 总是为 true。

表达式 转换
a == b a?.equals(b) ?: (b === null)
a != b !(a?.equals(b) ?: (b === null))

      当自定义重载equals函数时,可以参考data类自动生成的equals函数:

public boolean equals(@Nullable Object var1) {
  if (this != var1) {
     if (var1 instanceof Point) {
        Point var2 = (Point)var1;
        if (this.x == var2.x && this.y == var2.y) {
           return true;
        }
     }
     return false;
  } else {
     return true;
  }
}
  • 当比较自身对象时,直接返回true。
  • 类型不同,则直接返回false。
  • 依据关键字段进行判断,条件符合就返回true。

      Kotlin提供恒等运算符(===)来检查两个参数是否是同一个对象的引用,与Java的==运算符相同。但===!==(同一性检查)不可重载,因此不存在对他们的约定。

      == 运算符和 != 运算符只使用函数 equals(other: Any?): Boolean,可以覆盖它来提供自定义的相等性检测实现。不会调用任何其他同名函数(如 equals(other: Point))或 扩展函数,因为继承自Any类的实现始终优先于扩展函数和其他同名函数

排序运算符

      在Java中,类可以实现Comparable接口,并在compareTo方法中判断一个对象是否大于另一个对象。但只有基本数据类型可以使用 <>来比较,所有其他类型没有简明的语法调用compareTo方法,需要显式调用。

      Kotlin支持相同的Comparable接口(无论是Java的还是Kotlin的Comparable接口),比较运算符将会被转换为compareTo方法。所有在Java中实现Comparable接口的类,都可以在Kotlin中使用比较运算符。

表达式 转换
a > b a.compareTo(b) > 0
a < b a.compareTo(b) < 0
a >= b a.compareTo(b) >= 0
a <= b a.compareTo(b) <= 0

      Kotlin标准库中提供compareValuesBy函数来简洁地实现compareTo方法。该方法接收两个进行比较的对象,和用于比较的数值的方法引用:

data class Point(var x:Int,var y:Int):Comparable<Point>{
    override fun compareTo(other: Point): Int {
        return compareValuesBy(this,other,Point::x,Point::y)
    }
}

fun main(args: Array<String>) {
    val point1 = Point(3,4)
    var point2 = Point(4,5)

    println("result = ${point1 < point2}")
}

equals方法和compareTo方法,在父类中已经添加operator,重载时无需添加。

集合与区间的约定

      处理结合最常见的是通过下标获取和设置元素,以及检查元素是否属于当前集合。而这些操作在Kotlin中都提供相应的运算符语法支持:

  • 使用下标运算符a[b],获取或设置元素。
  • 使用in运算符,检查元素是否在集合或区间内,也可以用于迭代。

下标运算符

      使用下标运算符读取元素会被转换成get运算符方法的调用。当写入元素时,将调用set

表达式 转换
a[i] a.get(i)
a[i_1, ……, i_n] a.get(i_1, ……, i_n)
a[i] = b a.set(i, b)
a[i_1, ……, i_n] = b a.set(i_1, ……, i_n, b)

      Map也可以使用下标运算符,将键作为下标传入到下标运算符中获取对应的value。对于可变的map,同样可以使用下标运算符修改对应键的value值。

注:get的参数可以是任意类型,所以当对map使用下标运算符时,参数类型时键的类型。

in运算符

      in运算符用于检查某个对象是否属于集合。它是一种约定,相应的函数为contains

表达式 转换
a in c c.contains(a)

rangTo 约定

      当需要创建区间时,都是使用..运算符。..运算符是调用rangeTo函数的一种约定。

表达式 转换
start..end start.rangeTo(end)

可以为任何类定义rangeTo函数。但是,如果该类实现了Comparable接口,那么可以直接使用Kotlin标准库为Comparable接口提供的rangeTo函数来创建一个区间。

public operator fun <T : Comparable<T>> T.rangeTo(that: T): ClosedRange<T> = ComparableRange(this, that)

使用Java8的LocalDate来构建一个日期的区间:

fun main(args: Array<String>) {
    val now = LocalDate.now()
    val vacation = now .. now.plusDays(10)
    println(now.plusWeeks(1) in vacation)
}

..运算符注意点:

  • ..运算符的优先级低于算术运算符,但最好还是把参数括起来以避免混淆:
0 .. (n + 1)
  • 区间表达式调用函数式Api时,必须先将区间表达式括起来,否则编译将不通过:
(0..10).filter { 
    it % 2 == 0
}.map { 
    it * it
}.forEach { 
    println(it)
}

iterator 约定

      for循环中可以使用in运算符来表示执行迭代。这意味着Kotlin的for循环将被转换成list.iterator()的调用,然后反复调用hasNextnext 方法。

iterator方法也是Kotlin中的一种约定,这意味iterator()可以被定义为扩展函数。例如:Kotlin标准库中为Java的CharSequence定义了一个扩展函数iterator,使我们能遍历一个常规的Java字符串。

for(s in "daqi"){
    
}

解构声明

      Kotlin提供解构声明,允许你展开单个复合值,并使用它来初始化多个单独的变量。

fun main(args: Array<String>) {
    val point = Point(3,4)
    val(x,y) = point
}   

      解构声明看起来像普通的变量声明,但他的括号中存在多个变量。但其实解构声明也是使用了约定的原理,要在解构声明中初始化每个变量,需要提供对应的componentN函数(其中N是声明中变量的位置)。

val point = Point(3,4)
val x = point.component1()
val y = point.component2()

数据类

      Kotlin中提供一种很方便生成数据容器的方法,那就是将类声明为数据类,也就是data类。

编译器自动从数据类的主构造函数中声明的所有属性生成以下方法:

  • equals()/hashCode()
  • toString()
  • componentN() 按声明顺序对应于所有属性
  • copy()

同时数据类必须满足以下要求:

  • 主构造函数需要至少有一个参数(可以使用默认参数来实现无参主构造函数)
  • 主构造函数的所有参数需要标记为 val 或 var
  • 数据类不能是抽象、开放、密封或者内部的

      equals方法会检查主构造函数中声明的所有属性是否相等;hashCode()会根据主构造函数中声明的所有属性生成一个哈希值;componentN()会按照主构造函数中声明的所有属性的顺序生成;toString()会按照以下格式"Point(x=3, y=4)"生成字符串。

      数据类体中有显式实现 equals()hashCode() 或者 toString(),或者这些函数在父类中有 final 实现,会使用现有函数;数据类不允许为 componentN() 以及 copy() 函数提供显式实现。

      如果该类不是数据类,要想该类的对象也可以应用于解构声明,需要手动声明对应的operator修饰的componentN()函数(成员函数和扩展函数都可以):

fun main() {
    val(x,y) = Piont(1,2)

}

class Piont(val x:Int,val y:Int){
    operator fun component1():Int{
        return x
    }

    operator fun component2():Int{
        return y
    }
}

使用场景

  • 遍历map

      使用解构声明快速获取mapentry 的键和值,快速遍历。

for ((key, value) in map) {
   // 直接使用该 key、value
   
}
  • 从函数中返回多个变量

      创建请求存储返回信息的数据类,在调用方法获取返回信息时,使用解构声明将其分成不同的值:

data class Result(val resultCode: Int, val status: Int,val body:String)
fun getHttpResult(……): Result {
    // 各种计算

    return Result(resultCode, status,josnBody)
}

------------------------------------------------------------------
//获取返回值
val(resultCode, status,josnBody) = getHttpResult()

      注意:我们也可以使用标准库中的 Pair 类作为返回值,来实现返回两个变量。

  • 在 lambda 表达式中解构

      和map遍历相似,就是将lambda中的Map.Entry参数进行解构声明:

val map = mapOf(1 to 1)
map.mapValues { (key, value) -> 
    "key = $key ,value = $value "
}

注意

      由于数据类中componentN()是按照主构造函数中声明的所有属性的顺序对应生成的。也就是说component1()返回的是主构造函数中声明的第一个值,component2()返回的是主构造函数中声明的第二个值,以此类推。

对于解构声明中不需要的变量,可以用下划线取代其名称,Kotlin将不会调用相应的 componentN()

fun main(args: Array<String>) {
    val point = Point(3,4)
    val(_,y) = point
    println(y)
}   

      否则,你想要的值在主构造函数中声明在第二个位置,而你不是使用下划线取代其名称取代第一个变量的位置时,解构声明将使用component1()对值进行赋值,你将得不到你想要的值。

fun main(args: Array<String>) {
    val point = Point(3,4)
    //y轴坐标应该是第二个位置,但由于没有使用_占位,将使用component1()对其进行赋值,也就是使用x轴坐标对y坐标进行赋值。
    val(y) = point
    println(y)
}   

中辍调用

      在提到解构声明的地方,往往伴随着中辍调用的出现。但中辍调用并不是什么约定,是让含有infix 关键字修饰的方法,可以像基本算术运算符一样被调用。即忽略该调用函数的点与圆括号,将函数名放在目标对象和参数之间

//中辍调用
1 to "one"

//普通调用
1.to("one")

中缀函数必须满足以下要求:

  • 成员函数或扩展函数
  • 只有一个参数
  • 参数不得接受可变参数且不能有默认值

使用场景

  • 区间

使用..运算符创建的区间是一个闭区间,当我们需要创建倒序区间或者半闭区间,甚至是设置区间步长时,所使用到的downTountilstep 其实都不是关键字,而是一个个使用infix 关键字修饰的方法,只是使用中辍调用来进行呈现。

  • map

在创建map时,对key和vlaue使用中辍调用来添加元素,提高可读性。

val map = mapOf("one" to 1,"two" to 2)

中辍调用优先级

      中缀函数调用的优先级低于算术操作符、类型转换以及 rangeTo 操作符。所以0 until n * 20 until (n * 2)等价。

      但中缀函数调用的优先级高于布尔操作符&& 与 ||、is 与 in 检测以及其他一些操作符。所以7 in 0 until 107 in (0 until 10)等价。

参考资料:

android Kotlin系列:

Kotlin知识归纳(一) —— 基础语法

Kotlin知识归纳(二) —— 让函数更好调用

Kotlin知识归纳(三) —— 顶层成员与扩展

Kotlin知识归纳(四) —— 接口和类

Kotlin知识归纳(五) —— Lambda

Kotlin知识归纳(六) —— 类型系统

Kotlin知识归纳(七) —— 集合

Kotlin知识归纳(八) —— 序列

Kotlin知识归纳(九) —— 约定

Kotlin知识归纳(十) —— 委托

Kotlin知识归纳(十一) —— 高阶函数

Kotlin知识归纳(十二) —— 泛型

Kotlin知识归纳(十三) —— 注解

Kotlin知识归纳(十四) —— 反射