入行没几年的小码农,近期学习Kotlin,做一份笔记记录,此文依据《Kotlin实战》这本书的流程记录,部分示例内容均摘自《Kotlin实战》,记下自己的理解,本篇记录Kotlin中对类,对象,接口的使用。
Kotlin学习笔记系列
新手上路,Kotlin学习笔记(一)-- Kotlin入门介绍
新手上路,Kotlin学习笔记(四)---Lambda表达式在Kotlin中的使用
新手上路,Kotlin学习笔记(五)---Kotlin中的类型系统
新手上路,Kotlin学习笔记(六)---运算符重载和其它约定
新手上路,Kotlin学习笔记(七)---Lambda作为形参和返回值的使用
Lambda表达式是非常简洁的书写方式,在Java1.8中,我们已经可以使用Lambda了,下面我们来看在Kotlin中如何使用Lambda表达式。
由于之前没有用过Java1.8,对于Lambda表达式不是很理解,先简单记录一下最近的所学吧。
一、Lambda在集合中的使用
在Kotlin的集合库中,有很多封装好的方法供我们使用,其中一些就使用了Lambda的方式,如下面的例子,获取集合中的最大值
/**
* Returns the first element yielding the largest value of the given function or `null` if there are no elements.
*/
public inline fun <T, R : Comparable<R>> Iterable<T>.maxBy(selector: (T) -> R): T? {
val iterator = iterator()
if (!iterator.hasNext()) return null
var maxElem = iterator.next()
var maxValue = selector(maxElem)
while (iterator.hasNext()) {
val e = iterator.next()
val v = selector(e)
if (maxValue < v) {
maxElem = e
maxValue = v
}
}
return maxElem
}
上述源码中可以看到,我们的传参是selector,这个参数是(T) -> R的,T就是我们集合所包含的对象,而R在前面可以看到要求是R : Comparable<R> ,就是要求R实现Comparable接口,因为这样才能进行比较。
所以我们在调用该方法的时候,可以这样使用
open class Person(var age : Int = 0, var name : String = "", var addr : String = "") //open 修饰之后可以被子类继承
{
open fun showName() = println(name) //open修饰之后的可以被子类重写
fun showAddr() = println(addr) //默认为final的,不能被子类重写
var user : User? = null
}
interface User
{
val nickName : String
}
上面是我们之前定义的Person类,现在新加了一个User接口,Person中有变量User
当我们对于一个Person对象的集合进行排序时,调用maxBy方法
fun findOldestPerson(people : ArrayList<Person>)
{
people.maxBy ({person: Person -> person.age })
}
这种使用方式符合源码中selector : (T) -> R的格式,T就是person,而R是person.age,因为age是Int类型的,默认已经实现了Comparable接口,所以完全可以正常使用。而如果我们对User进行排序的话,由于User没有实现Comparable接口,所以编辑会报错
Type parameter bound for R in
inline fun <T, R : Comparable<R>> Iterable<T>.maxBy(selector: (T) → R) : T?
is not satisfied: inferred type User? is not a subtype of Comparable<User?>接下来我们继续回到Lambda中,刚才的maxBy的使用看起来还是有点不舒服,我们可以继续改进,毕竟Kotlin的宗旨就是更加美观简介。在Kotlin中,当Lambda表达式是最后一个实参的时候,可以放置在括号的外面,同时,如果只有Lambda这个实参的时候,还可以去掉空的括号对。前面我们也提到过,参数的类型已知的情况下,是可以省略参数类型的,最后,代码就变成了这样
people.maxBy { person -> person.age }
现在看起来美观了不少,不过,我们还可以继续简化。当是只有一个参数Lambda并且这个参数的类型可以被推导出来时,此时可以用it代替这个参数,如刚才的maxBy方法,people是泛型为Person的集合类,所以可以推导出来,参数中的Lambda一定是Person类型的,所以我们可以用It代替之前的person,这样就简化成了
people.maxBy { it.age }
对于it的使用,在大量使用It的时候,我们的代码看起来简洁了,但不一定易于理解,所以还是建议使用之前的方式,显式的写出参数的名称,便于后期会代码的阅读,不要盲目的使用it。
前面介绍的都是将代码块当做参数使用的,接下来我们看怎么讲属性或者方法当做参数使用。我们可以将属性或者方法转换成一个值,然后我们就可以传递方法或者属性了,使用方式如下,用类名 + ::(双冒号) + 属性或方法名表示,如
people.maxBy(Person::age)
此处就是将Person的age属性直接使用
val action = {person : Person, message : String -> sendEmail(person,message)}
val nextAction = :: sendEmail //MainActivity :: sendEmail 当前类为MainActivity,可以省略
fun sendEmail(person: Person,message:String){}
这里是将方法转换为一个值的方式,action和nextAction是同样的效果,nextAction看起来更加简洁。
二、.集合相关的API
在Lambda中,有很多函数式的API,熟练掌握这些API,对我们的开发过程会有明显的提升,下面我们一起学习一下。
(1)基础的filter、map、all、any、count、find
就和方法名一样,filter的作用就是过滤,将符合过滤条件的数据返回。而map是转换的作用,将集合中的每个元素按照一定的规则进行转换,然后返回。
/**
* Returns a list containing only elements matching the given [predicate].
*/
public inline fun <T> Iterable<T>.filter(predicate: (T) -> Boolean): List<T> {
return filterTo(ArrayList<T>(), predicate)
}
/**
* Appends all elements matching the given [predicate] to the given [destination].
*/
public inline fun <T, C : MutableCollection<in T>> Iterable<T>.filterTo(destination: C, predicate: (T) -> Boolean): C {
for (element in this) if (predicate(element)) destination.add(element)
return destination
}
通过源码,我们可以看到filter的实现方式,就是遍历集合,将符合要求的元素组成新的集合,值得注意的是,该方法的参数是一个Lambda表达式,结合之前我们学习的Lambda表达式,对于集合过滤的处理,就可以用短短一两行代码完成了。
/**
* Returns a list containing the results of applying the given [transform] function
* to each element in the original collection.
*/
public inline fun <T, R> Iterable<T>.map(transform: (T) -> R): List<R> {
return mapTo(ArrayList<R>(collectionSizeOrDefault(10)), transform)
}
/**
* Applies the given [transform] function to each element of the original collection
* and appends the results to the given [destination].
*/
public inline fun <T, R, C : MutableCollection<in R>> Iterable<T>.mapTo(destination: C, transform: (T) -> R): C {
for (item in this)
destination.add(transform(item))
return destination
}
map的实现同filter差不多,也是进行某些操作后,将操作后的数据,组成新的集合返回。
/**
* Returns `true` if all elements match the given [predicate].
*/
public inline fun <T> Iterable<T>.all(predicate: (T) -> Boolean): Boolean {
if (this is Collection && isEmpty()) return true
for (element in this) if (!predicate(element)) return false
return true
}
/**
* Returns `true` if collection has at least one element.
*/
public fun <T> Iterable<T>.any(): Boolean {
if (this is Collection) return !isEmpty()
return iterator().hasNext()
}
/**
* Returns `true` if at least one element matches the given [predicate].
*/
public inline fun <T> Iterable<T>.any(predicate: (T) -> Boolean): Boolean {
if (this is Collection && isEmpty()) return false
for (element in this) if (predicate(element)) return true
return false
}
通过源码,我们也可以看到all和any的作用,all是判断是否所有元素都满足我们传入的Lambda表达式。any则是判断是否有元素满足我们传入的Lambda表达式,两者是互相对应的关系。
/**
* Returns the number of elements matching the given [predicate].
*/
public inline fun <T> Iterable<T>.count(predicate: (T) -> Boolean): Int {
if (this is Collection && isEmpty()) return 0
var count = 0
for (element in this) if (predicate(element)) count++
return count
}
count则是查询满足Lambda表达式的所有元素的个数。
/**
* Returns the first element matching the given [predicate], or `null` if no such element was found.
*/
@kotlin.internal.InlineOnly
public inline fun <T> Iterable<T>.find(predicate: (T) -> Boolean): T? {
return firstOrNull(predicate)
}
/**
* Returns the first element matching the given [predicate], or `null` if element was not found.
*/
public inline fun <T> Iterable<T>.firstOrNull(predicate: (T) -> Boolean): T? {
for (element in this) if (predicate(element)) return element
return null
}
find方法的效果是,找到满足Lambda表达式的第一个元素,如果没有满足要求的元素,则返回null。
上面这些是一些基本的集合操作,在Kotlin源码中还有很多相关的函数供我们使用,比如groupBy可以让我们对集合重组,根据规则分成多个子集合,这要比我们自己来书写这部分业务逻辑简单地多,具体可以参考Collections.kt文件。
(2)序列
上面的集合操作很便捷,每次都会返回新的集合对象,然后我们可以继续调用新的操作,这样就形成了一个链式的写法,看起来很清楚,但是通过源码我们可以看到,在操作的时候,每次都创建了新的集合,代价就是创建了非常多的中间对象,这些对象我们实际是不需要的,那么可不可以不创建这些临时的对象呢?答案是可以的,接下来让我们了解一下序列。
序列拥有集合这些链式操作的API,集合调用asSequence()方法即可转换成序列,序列变换后使用toList()等方法可以再转换回集合。让我们以filter为例,看一下序列是如何实现的
/**
* Creates a [Sequence] instance that wraps the original collection returning its elements when being iterated.
*
* @sample samples.collections.Sequences.Building.sequenceFromCollection
*/
public fun <T> Iterable<T>.asSequence(): Sequence<T> {
return Sequence { this.iterator() }
}
可以看到,asSequence方法返回的是一个Sequence对象,然后调用Sequence的filter方法
/**
* Returns a sequence containing only elements matching the given [predicate].
*
* The operation is _intermediate_ and _stateless_.
*/
public fun <T> Sequence<T>.filter(predicate: (T) -> Boolean): Sequence<T> {
return FilteringSequence(this, true, predicate)
}
/**
* A sequence that returns the values from the underlying [sequence] that either match or do not match
* the specified [predicate].
*
* @param sendWhen If `true`, values for which the predicate returns `true` are returned. Otherwise,
* values for which the predicate returns `false` are returned
*/
internal class FilteringSequence<T>(private val sequence: Sequence<T>,
private val sendWhen: Boolean = true,
private val predicate: (T) -> Boolean
) : Sequence<T> {
override fun iterator(): Iterator<T> = object : Iterator<T> {
val iterator = sequence.iterator()
var nextState: Int = -1 // -1 for unknown, 0 for done, 1 for continue
var nextItem: T? = null
private fun calcNext() {
while (iterator.hasNext()) {
val item = iterator.next()
if (predicate(item) == sendWhen) {
nextItem = item
nextState = 1
return
}
}
nextState = 0
}
override fun next(): T {
if (nextState == -1)
calcNext()
if (nextState == 0)
throw NoSuchElementException()
val result = nextItem
nextItem = null
nextState = -1
@Suppress("UNCHECKED_CAST")
return result as T
}
override fun hasNext(): Boolean {
if (nextState == -1)
calcNext()
return nextState == 1
}
}
}
通过源码可以看出,我们在调用filter方法的时候并没有去创建新的集合接收,而是更改了Iterator的方法,在下一个查询的时候,过滤掉了我们不需要的元素,只在最后toList的时候,才去创建新的集合,这样就省略了很多中间不需要的变量,对我们来说是一个好事。
tips:通过上面分析,我们得知序列调用的链式方法,只有在最后转换成集合的时候才会生效,所以如果我们没有toList并进行接收,其实是没有做任何操作的,比如people.asSequence().maxBy { it.age }是没有意义的,
另外,还可以通过generateSequence(seed: T?, nextFunction: (T) -> T?)这个方法创建序列,第一个参数是序列的第一个元素,后一个参数是序列的下一个元素的计算方式。
三、Lambda中更简洁的使用方式: with 和 apply
让我们先看一个示例,当我们想要拼接a123456789b这样的字符串时,用StringBuilder实现的代码可能是这样的
fun getString():String{
val stringBuilder = StringBuilder()
stringBuilder.append("a")
for(i in 1..9)
{
stringBuilder.append(i)
}
stringBuilder.append("b")
return stringBuilder.toString()
}
这个实现是我们通常的写法,也是没有什么问题的,但是我们在里面不停的书写stringBuilder.append,其实也比较麻烦,代码编写的终极目标是就消除所有的重复代码,那么我们可不可以连stringBuilder也省略掉呢?答案当然是可以的,with就可以帮我们完成这件事,看下面示例
fun getStringByWith():String{
val stringBuilder = StringBuilder()
return with(stringBuilder)
{
append("a")
for(i in 1..9)
{
append(i)
}
append("b")
stringBuilder.toString()
}
}
看上面代码,我们将stringBuilder对象放在了with后面的括号中,然后在代码块中调用append方法的时候就不用再次书写stringBuilder对象了,同if那样,在最后一个表达式stringBuilder的结果就是with方法的返回值。
接下来我们看另一种实现方式,用apply实现这个逻辑
fun getStringByApply() = StringBuilder().apply{
append("a")
for(i in 1..9)
{
append(i)
}
append("b")
}.toString()
apply和with的使用方式差不多,调用apply的那个对象,就是代码块中的默认对象,可以省略那个对象名,直接书写调用的方法即可,和with的区别在于,apply方法是在原有的对象上进行更改,不需要返回值,所以我们修改完成之后,直接调用toString()方法即可。
今天的文档到这里就结束了,下一章我们将记录学习Kotlin中的类型,对于null怎么办,还有数据类型之间的转换,特殊的数据类型等。
//下一章 新手上路,Kotlin学习笔记(五)---Kotlin中的类型系统