入行没几年的小码农,近期学习Kotlin,做一份笔记记录,此文依据《Kotlin实战》这本书的流程记录,部分示例内容均摘自《Kotlin实战》,记下自己的理解,本篇记录在Kotlin中我们如何对运算符进行重载,另外还有一些Kotlin中的约定。
Kotlin学习笔记系列
新手上路,Kotlin学习笔记(一)-- Kotlin入门介绍
新手上路,Kotlin学习笔记(四)---Lambda表达式在Kotlin中的使用
新手上路,Kotlin学习笔记(五)---Kotlin中的类型系统
新手上路,Kotlin学习笔记(六)---运算符重载和其它约定
新手上路,Kotlin学习笔记(七)---Lambda作为形参和返回值的使用
一、运算符的重载
有时候我们会感慨,在拼接字符串的时候,直接用加号“+”我们会觉得好方便啊,如果添加集合也这样用就好了,不用去写add方法了。现在,在Kotlin中我们就可以这样做了!而且,不只是集合,其他类型我们也可以用这种简单的运算符替代方法!!那么,接下来我们来学习在Kotlin中怎么对运算符进行重载。
<1>重载二元运算符
二元运算符有 + - * / %五种,这五种符号在重载的时候,我们需要为类定义对应符号的方法,声明方式如下
class Point(var pointX : Int = 0 , var pointY : Int = 0) {
operator fun plus(point : Point) : Point
{
return Point(pointX + point.pointX, pointY + point.pointY)
}
}
fun testPlus()
{
val point = Point(1,1)
val poing2 = Point(2,1)
val point3 = point + poing2 //此时调用Point的plus方法 point3的X和Y分别是3和2
}
看上面的示例,声明运算符的重载方法时,我们需要给方法增加一个operator关键字,然后方法名是加号“+”对应的名称plus,这样我们就声明了一个运算符的重载方法,非常简单,最后就可以直接用加号“+”替代方法了。
五个二元运算符的对应方法名分别是
+ plus
- minus
* times
/ div
% mod
tips:我们也可以将运算符声明为扩展方法,这样我们就可以为一些第三方SDK中的类去重载运算符。
tips:operator方法和普通的方法一样,也可以再次重载不同参数的形式,是被允许的。
<2>复合运算符
简单的二元运算符如何重载我们已经学会了,那么+=这样的复合运算符该怎么办呢?
当我们已经重载二元运算符的时候,对于+=这样的二元运算符,也是直接支持的,效果还是 一样的,先相加,再赋值;
fun testPlus()
{
var point = Point(1,1)
val poing2 = Point(2,1)
val point3 = point + poing2 //此时调用Point的plus方法 point3的X和Y分别是3和2
point += poing2//此时point和上面point3结果相同 相当于调用了point = point + point2
}
同样的,复合运算符我们也可以重新重载,自己书写对应的实现而不是依赖于二元运算符,实现方式和二元运算符一样,方法名只需在二元运算符后面加上Assign即可,示例如下
operator fun minusAssign(point : Point)
{
pointX -= point.pointX
pointY -= point.pointY
}
复合运算符的返回值必须是Unit的!!
当我们同时重载了二元运算符和其对应的复合运算符,在调用的时候就会有两种方式,那么此时编译器会怎么处理呢?
1、如果对象声明是val的,此对象就是不可变的,将不会使用复合运算符(因为复合运算符返回是Unit,对自己改变的,此时不会执行对应的方法)
2、如果对象声明是var的,那么编译器将会报错,所以我们应该只去书写一种实现方式比较好,可以将另一种场景书写为普通的方法调用。
<3>一元运算符和比较运算符
一元运算符的重载方式和二元运算符基本一致,非常简单,其方法名和运算符的对照关系如下:
+a unaryPlus
-a unaryMinus
!a not
++a,a++ inc
--a,a-- dec
对于比较运算符 == , != , >, <, >=, <=这六种,其实只对应了两个方法,equals 和 compareTo,这两个方法我们都很熟悉了,并且equals不需要声明为operator,因为在Any中已经是operator的了,我们只需要override重写即可。而如果用到比较运算符,我们需要让类实现Comparable接口,然后compareTo方法和我们在Java中对集合进行排序的时候,原理是一样的,并且也不需要声明operator,因为接口中已经声明过了,不再过多描述举例,
Any中的equals方法和Comparable中的compareTo方法源码:
/**
* Indicates whether some other object is "equal to" this one. Implementations must fulfil the following
* requirements:
*
* * Reflexive: for any non-null reference value x, x.equals(x) should return true.
* * Symmetric: for any non-null reference values x and y, x.equals(y) should return true if and only if y.equals(x) returns true.
* * Transitive: for any non-null reference values x, y, and z, if x.equals(y) returns true and y.equals(z) returns true, then x.equals(z) should return true
* * Consistent: for any non-null reference values x and y, multiple invocations of x.equals(y) consistently return true or consistently return false, provided no information used in equals comparisons on the objects is modified.
*
* Note that the `==` operator in Kotlin code is translated into a call to [equals] when objects on both sides of the
* operator are not null.
*/
public open operator fun equals(other: Any?): Boolean
/**
* Classes which inherit from this interface have a defined total ordering between their instances.
*/
public interface Comparable<in T> {
/**
* Compares this object with the specified object for order. Returns zero if this object is equal
* to the specified [other] object, a negative number if it's less than [other], or a positive number
* if it's greater than [other].
*/
public operator fun compareTo(other: T): Int
}
二、集合、区间的约定
在Kotlin中,对于集合或者区间,我们知道可以通过下标来直接访问指定的某个元素
val people : ArrayList<Person> = arrayListOf()
people += Person(age = 20)
people[0].age
那么我们先来看一下这个是如何实现的
/**
* The array buffer into which the elements of the ArrayList are stored.
* The capacity of the ArrayList is the length of this array buffer. Any
* empty ArrayList with elementData == EMPTY_ELEMENTDATA will be expanded to
* DEFAULT_CAPACITY when the first element is added.
*
* Package private to allow access from java.util.Collections.
*/
transient Object[] elementData;
/**
* Returns the element at the specified position in this list.
*
* @param index index of the element to return
* @return the element at the specified position in this list
* @throws IndexOutOfBoundsException {@inheritDoc}
*/
public E get(int index) {
if (index >= size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
return (E) elementData[index];
}
/**
* Replaces the element at the specified position in this list with
* the specified element.
*
* @param index index of the element to replace
* @param element element to be stored at the specified position
* @return the element previously at the specified position
* @throws IndexOutOfBoundsException {@inheritDoc}
*/
public E set(int index, E element) {
if (index >= size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
E oldValue = (E) elementData[index];
elementData[index] = element;
return oldValue;
}
在编译期中点击方括号,指向的是ArrayList的get方法,然后我们看到了get方法的实现,就是从内部存储数据的数组取出值,可以看出,方括号也可以理解为一种运算符的重载,方法名是get和set。
接着,我们就可以让自己的类也实现下标表示这种方式,还以刚才的Point为例
operator fun Point.get(index : Int):Int{
return when(index)
{
0 -> pointX
1 -> pointY
else -> throw IndexOutOfBoundsException("error index : $index" )
}
}
var point = Point(1,1)
val pointX = point[0] //此处取到的值为1
operator fun Point.set(index : Int , value : Int)//set方法的最后一个参数为等号右边的值
{
when(index)
{
0 -> pointX = value
1 -> pointY = value
else -> throw IndexOutOfBoundsException("error index : $index")
}
}
tips:get和set的方法,其参数不需要一定是Int类型的,其可以是任何类型的。
in和...
在区间中,我们前面还介绍了in 和 ... 这两种运算符,它们也可以被重载,in 对应的方法名是 contains,而 ... 对应的方法名是rangeTo,并且Comparable的扩展函数实现了rangeTo的方法,所以如果我们的类实现了Comparable接口,可以直接使用...这个运算符。
刚才提到的in是检测某个对象是否在这个区间中,前面我们还看到了in的另一种使用方式,在for循环中,我们可以用in来进行遍历,这又是怎么做的呢?
其实,在for循环中,我们使用的in会转换成list.iterator()方法,然后调用next和hasNext,进行遍历,既然如此,我们也可以为一个类定义名为iterator的扩展方法,然后实现next和hasNext方法,这样in在for循环中的使用,我们也进行了对应的重载。
三、解构声明
当我们将一个类声明为data类型的时候,将允许我们通过下面这种方式将对象展开获取多个变量
var point = Point(1,2)
val (x,y) = point //此处,x的值为1,y的值为2
上面这种展开的方式叫做解构声明,就是将point的两个成员变量,赋值给了x和y,这个是data类自动帮我们实现的。其实际原理是调用了point对象的component1()和component2()方法,和名称一样,component1就是获取第一个参数的值,另外,Kotlin标准库只允许返回前五个值,并不是返回无限多的。
四、委托属性
前面我们有使用by关键字来将类委托,减少我们包装时的代码量,这里我们将继续学习by关键字。
class Book
{
var name : String by BookName(name)
}
class BookName(var value : String) {
operator fun getValue(book : Book, prop : KProperty<*>) = value
operator fun setValue(book : Book, prop : KProperty<*>, newValue : String)
{
value = newValue
}
}
上述示例就是将Book类的成员变量name委托给BookName进行处理,我们不再需要对name书写对应的实现方法,在set、get的时候均会调用BookName的getValue和setValue方法。而需要被委托的类,要求我们实现这两个方法,并且第一个参数为对应的对象,第二个参数为固定的,后面我们学习到再做分析解释。
tips;在Kotlin中,Map已经实现了setValue和getValue方法,所以我们可以直接将变量委托给一个Map。这样也是非常方便的。
惰性初始化:在初始化数据的时候,我们还可以使用by lazy的方式,使用这种方式,该值将在第一次调用的时候通过预定好的方式获取数据,如果有数据,就直接返回,使用方式如下:
data class Client(val username : String , val password : String)
{
val age : Int by lazy { getAgeFirst() } //lazy后面是一个Lambda表达式
fun getAgeFirst():Int
{
return 0
}
}
到此为止,本章的内容就结束了,下一章我们将继续重温Lambda表达式,学习Lambda在Kotlin中更加高大上的使用方式!
// 下一章 新手上路,Kotlin学习笔记(七)---Lambda作为形参和返回值的使用