转载

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

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.times(point: Point):Point{
    return Point(x + point.x,y + point.y)
}

//定义另类的operator函数
operator fun Point.times(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)
}


复制代码
Kotlin知识归纳(九) —— 约定

扩展函数可以很好的对现有的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
operator

运算符与集合

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知识归纳(九) —— 约定

数据类

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

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

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

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

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

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

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

如果不使用数据类,需要手动声明 componentN() 函数:

class Point(val x :Int,val y: Int){
    operator fun component1() = x
    operator fun component21() = 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()
复制代码
  • 在 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)
}   
复制代码
Kotlin知识归纳(九) —— 约定
否则,你想要的值在主构造函数中声明在第二个位置,而你不是使用下划线取代其名称取代第一个变量的位置时,解构声明将使用 component1()

对值进行赋值,你将得不到你想要的值。

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

中辍调用

在提到 解构声明 的地方,往往伴随着 中辍调用 的出现。但 中辍调用 并不是什么约定,是让含有 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) 等价。

原文  https://juejin.im/post/5d17875e518825351d566957
正文到此结束
Loading...