此文已由作者申国骏授权网易云社区发布。
欢迎访问网易云社区,了解更多网易技术产品运营经验。
从谨慎地在项目中引入kotlin到全部转为kotlin开发我们用了大概半年的时间。这中间经历了从在一个小功能中尝试使用到完全使用kotlin完成了大版本开发的过程。使用方法也从仅仅地用java风格写kotlin代码,慢慢地变成使用kotlin风格去编写代码。
到目前为止,kotlin的引入至少没有给我们带来不必要的麻烦,在慢慢品尝kotlin语法糖的过程中,我们领略到了能给开发者真正带来好处的一些特性。本文就是对这些我们认为是精髓的一些特性的进行总结,希望能给还在犹豫是否要开始学习kotlin或者刚开始编写kotlin但是不知道该如何利用kotlin的人们先一睹kotlin的优雅风采。
KotlinConf 2018 - Conference Opening Keynote by Andrey Breslav 上讲的Kotlin设计理念: Kotlin拥有强大的IDE厂商Intellij和Google的支持,保证了其务实、简洁、安全和与JAVA互操作的良好设计理念。
其中务实表示了Kotlin并没有独创一些当前没有或大众不太熟悉的设计理念,而是吸收了众多其他语言的精髓,并且提供强大的IDE支持,能真正方便开发者运用到实际项目之中。
简洁主要指的是Kotlin支持隐藏例如getter、setter等Java样板代码,并且有大量的标准库以及灵活的重载和扩展机制,来使代码变得更加直观和简洁。
安全主要是说空值安全的控制以及类型自动检测,帮助减少NullPointerException以及ClassCastException。
与Java互操作以为这可以与Java相互调用、混合调试以及同步重构,同时支持Java到kotlin代码的自动转换。
Kotlin类型分为可空和非可空,赋值null到非可空类型会编译出错
fun main() {
var a: String = "abc"
a = null // compilation error
var b: String? = "abc"
b = null // ok
}
对空的操作有以下这些
使用安全调用运算符 ?:
可以避免Java中大量的空值判断。以下是一个对比的例子:
// 用Java实现
public void sendMessageToClient(
@Nullable Client client,
@Nullable String message,
@NotNull Mailer mailer
) {
if (client == null || message == null) return;
PersonalInfo personalInfo = client.getPersonalInfo();
if (personalInfo == null) return;
String email = personalInfo.getEmail();
if (email == null) return;
mailer.sendMessage(email, message);
}
// 用Kotlin实现
fun sendMessageToClient(
client: Client?,
message: String?,
mailer: Mailer
){
val email = client?.personalInfo?.email
if (email != null && message != null) {
mailer.sendMessage(email, message)
}
}
扩展函数是Kotlin精华特点之一,可以给别人的类添加方法或者属性,使得方法调用更加自然和直观。通过扩展函数的特性,Kotlin内置了大量的辅助扩展方法,非常实用。下面我们通过这个例子看一下
fun main() {
val list = arrayListOf<Int>(1, 5, 3, 7, 9, 0)
println(list.sortedDescending())
println(list.joinToString(
separator = " | ",
prefix = "(",
postfix = ")"
) {
val result = it + 1
result.toString()
})
}
其中sortedDescending
以及joinToString
都是Kotlin内置的扩展方法。 上述的函数会输出
Kotlin内部的实现如下
public fun <T : Comparable<T>> Iterable<T>.sortedDescending(): List<T> {
return sortedWith(reverseOrder())
}
public fun <T> Iterable<T>.joinToString(separator: CharSequence = ", ", prefix: CharSequence = "", postfix: CharSequence = "", limit: Int = -1, truncated: CharSequence = "...", transform: ((T) -> CharSequence)? = null): String {
return joinTo(StringBuilder(), separator, prefix, postfix, limit, truncated, transform).toString()
}
可见sortedDescending
和joinToString
都是对Iterable<T>
类对象的一个扩展方法。
我们也可以自己实现一个自定义的扩展函数如下:
fun Int.largerThen(other: Int): Boolean {
return this > other
}
fun main() {
println(2.largerThen(1))
}
上述代码输出为true
通过扩展函数我们以非常直观的方式,将某个类对象的工具类直接使用该类通过"."
方式调用。
当然扩展函数是一种静态的实现方式,不会对原来类对象的方法进行覆盖,也不会有正常函数的子类方法覆盖父类方法现象。
扩展属性与扩展函数类似,也是可以直接给类对象增加一个属性。例如:
var StringBuilder.lastChar: Char
get() = get(length -1)
set(value: Char) {
this.setCharAt(length -1, value)
}
fun main() {
val sb = StringBuilder("kotlin")
println(sb.lastChar)
sb.lastChar = '!'
println(sb.lastChar)
}
无论是扩展函数还是扩展属性,都是对Java代码中utils方法很好的改变,可以避免多处相似功能的util定义以及使得调用更为直观。
通过扩展的方式,Kotlin对集合类提供了非常丰富且实用的诸多工具,只有你想不到,没有你做不到。下面我们通过 Kotlin Koans 上的一个例子来说明一下:
data class Shop(val name: String, val customers: List<Customer>)
data class Customer(val name: String, val city: City, val orders: List<Order>) {
override fun toString() = "$name from ${city.name}"
}
data class Order(val products: List<Product>, val isDelivered: Boolean)
data class Product(val name: String, val price: Double) {
override fun toString() = "'$name' for $price"
}
data class City(val name: String) {
override fun toString() = name
}
以上是数据结构的定义,我们有一个超市,超市有很多顾客,每个顾客有很多笔订单,订单对应着一定数量的产品。下面我们来通过集合的操作来完成以下任务。
操作符 | 作用 |
---|---|
filter | 将集合里的元素过滤,并返回过滤后的元素 |
map | 将集合里的元素一一对应转换为另一个元素 |
// 返回商店中顾客来自的城市列表
fun Shop.getCitiesCustomersAreFrom(): Set<City> = customers.map { it.city }.toSet()
// 返回住在给定城市的所有顾客
fun Shop.getCustomersFrom(city: City): List<Customer> = customers.filter { it.city == city }
操作符 | 作用 |
---|---|
all | 判断集合中的所有元素是否满足某个条件,都满足返回true |
any | 判断集合中是否有元素满足某个条件,有则返回true |
count | 返回集合中满足某个条件的元素数量 |
find | 查找集合中满足某个条件的一个元素,不存在则返回null |
// 如果超市中所有顾客都来自于给定城市,则返回true
fun Shop.checkAllCustomersAreFrom(city: City): Boolean = customers.all { it.city == city }
// 如果超市中有某个顾客来自于给定城市,则返回true
fun Shop.hasCustomerFrom(city: City): Boolean = customers.any{ it.city == city}
// 返回来自于某个城市的所有顾客数量
fun Shop.countCustomersFrom(city: City): Int = customers.count { it.city == city }
// 返回一个住在给定城市的顾客,若无返回null
fun Shop.findAnyCustomerFrom(city: City): Customer? = customers.find { it.city == city }
操作符 | 作用 |
---|---|
flatMap | 将集合的元素转换为另外的元素(非一一对应) |
// 返回所有该顾客购买过的商品集合
fun Customer.getOrderedProducts(): Set<Product> = orders.flatMap { it.products }.toSet()
// 返回超市中至少有一名顾客购买过的商品列表
fun Shop.getAllOrderedProducts(): Set<Product> = customers.flatMap { it.getOrderedProducts() }.toSet()
操作符 | 作用 |
---|---|
max | 返回集合中以某个条件排序的最大的元素 |
min | 返回集合中以某个条件排序的最小的元素 |
// 返回商店中购买订单次数最多的用户
fun Shop.getCustomerWithMaximumNumberOfOrders(): Customer? = customers.maxBy { it.orders.size }
// 返回顾所购买过的最贵的商品
fun Customer.getMostExpensiveOrderedProduct(): Product? = orders.flatMap { it.products }.maxBy { it.price }
操作符 | 作用 |
---|---|
sort | 根据某个条件对集合元素进行排序 |
sum | 对集合中的元素按照某种规则进行相加 |
groupBy | 对集合中的元素按照某种规则进行组合 |
// 按照购买订单数量升序返回商店的顾客
fun Shop.getCustomersSortedByNumberOfOrders(): List<Customer> = customers.sortedBy { it.orders.size }
// 返回顾客在商店中购买的所有订单价格总和
fun Customer.getTotalOrderPrice(): Double = orders.flatMap { it.products }.sumByDouble { it.price }
// 返回商店中居住城市与顾客的映射
fun Shop.groupCustomersByCity(): Map<City, List<Customer>> = customers.groupBy { it.city }
操作符 | 作用 |
---|---|
partition | 根据某种规则将集合中的元素分为两组 |
fold | 对集合的元素按照某个逻辑进行一一累计 |
// 返回商店中未送到订单比送达订单要多的顾客列表
fun Shop.getCustomersWithMoreUndeliveredOrdersThanDelivered(): Set<Customer> = customers.filter {
val (delivered, undelivered) = it.orders.partition { it.isDelivered }
undelivered.size > delivered.size
}.toSet()
// 对所有顾客购买过的商品取交集,返回所有顾客都购买过的商品列表
fun Shop.getSetOfProductsOrderedByEveryCustomer(): Set<Product> {
val allProduct = customers.flatMap { it.orders }.flatMap { it.products }.toSet()
return customers.fold(allProduct) { orderedByAll, customer ->
orderedByAll.intersect(customer.orders.flatMap { it.products })
}
}
综合使用:
// 返回顾客所有送达商品中最贵的商品
fun Customer.getMostExpensiveDeliveredProduct(): Product? {
return orders.filter { it.isDelivered }.flatMap { it.products }.maxBy { it.price }
}
// 返回商店中某件商品的购买次数
fun Shop.getNumberOfTimesProductWasOrdered(product: Product): Int {
return customers.flatMap { it.orders }.flatMap { it.products }.count{it == product}
}
Kotlin对集合提供了几乎你能想到的所有操作,通过对这些操作的组合减少集合操作的复杂度,提高可读性。以下是Java和Kotln对集合操作的对比
// 用Java实现
public Collection<String> doSomethingStrangeWithCollection(
Collection<String> collection
) {
Map<Integer, List<String>> groupsByLength = Maps.newHashMap();
for (String s : collection) {
List<String> strings = groupsByLength.get(s.length());
if (strings == null) {
strings = Lists.newArrayList();
groupsByLength.put(s.length(), strings);
}
strings.add(s);
}
int maximumSizeOfGroup = 0;
for (List<String> group : groupsByLength.values()) {
if (group.size() > maximumSizeOfGroup) {
maximumSizeOfGroup = group.size();
}
}
for (List<String> group : groupsByLength.values()) {
if (group.size() == maximumSizeOfGroup) {
return group;
}
}
return null;
}
// 用Kotlin实现
fun doSomethingStrangeWithCollection(collection: Collection<String>): Collection<String>? {
val groupsByLength = collection.groupBy { s -> s.length }
val maximumSizeOfGroup = groupsByLength.values.map { group -> group.size }.max()
return groupsByLength.values.firstOrNull { group -> group.size == maximumSizeOfGroup }
}
还是举 Kotlin Koans 上的运算符重载例子。假设我们需要实现以下功能:
enum class TimeInterval { DAY, WEEK, YEAR }
data class MyDate(val year: Int, val month: Int, val dayOfMonth: Int) : Comparable<MyDate> {
override fun compareTo(other: MyDate): Int {
if (year != other.year) return year - other.year
if (month != other.month) return month - other.month
return dayOfMonth - other.dayOfMonth
}
override fun toString(): String {
return "$year/$month/$dayOfMonth"
}
}
fun main() {
val first = MyDate(2018, 10, 30)
val last = MyDate(2018, 11, 1)
for (date in first..last) {
println(date)
}
println()
println(first + DAY)
println()
println(first + DAY * 2 + YEAR * 2)
}
输出为以下:
只要实现以下运算符重载既可:
operator fun MyDate.rangeTo(other: MyDate): DateRange = DateRange(this, other)
operator fun MyDate.plus(timeInterval: TimeInterval): MyDate = this.addTimeIntervals(timeInterval, 1)
operator fun TimeInterval.times(num: Int): RepeatTimeInterval = RepeatTimeInterval(this, num)
operator fun MyDate.plus(repeatTimeInterval: RepeatTimeInterval): MyDate =
this.addTimeIntervals(repeatTimeInterval.timeInterval, repeatTimeInterval.num)
class RepeatTimeInterval(val timeInterval: TimeInterval, val num: Int)
class DateRange(override val start: MyDate, override val endInclusive: MyDate) : ClosedRange<MyDate>, Iterable<MyDate> {
override fun iterator(): Iterator<MyDate> = DateIterator(start, endInclusive)
}
class DateIterator(first: MyDate, private val last: MyDate) : Iterator<MyDate> {
private var current = first
override fun hasNext(): Boolean {
return current <= last
}
override fun next(): MyDate {
val result = current
current = current.nextDay()
return result
}
}
fun MyDate.nextDay(): MyDate = this.addTimeIntervals(DAY, 1)
fun MyDate.addTimeIntervals(timeInterval: TimeInterval, number: Int): MyDate {
val c = Calendar.getInstance()
c.set(year + if (timeInterval == TimeInterval.YEAR) number else 0, month - 1, dayOfMonth)
var timeInMillis = c.timeInMillis
val millisecondsInADay = 24 * 60 * 60 * 1000L
timeInMillis += number * when (timeInterval) {
TimeInterval.DAY -> millisecondsInADay
TimeInterval.WEEK -> 7 * millisecondsInADay
TimeInterval.YEAR -> 0L
}
val result = Calendar.getInstance()
result.timeInMillis = timeInMillis
return MyDate(result.get(Calendar.YEAR), result.get(Calendar.MONTH) + 1, result.get(Calendar.DATE))
}
Kotlin支持使用指定的扩展函数来实现运算符的重载,运算符对应的方法名具体参见官方文档 Operator overloading
标记为infix的方法,可以类似于二元运算符使用,举个例子
infix fun Int.plus(other: Int): Int {
return this + other
}
fun main() {
// 结果会输出7
println(3 plus 4)
}
infix方法执行的优先级低于算数运算符、类型转换type case以及rangTo运算符,但是高于布尔、is、in check等其他运算符。
使用infix的扩展函数可以实现自定义的二元运算标记。
更多网易技术、产品、运营经验分享请点击。