此文已由作者申國駿授權網易雲社區發布。

歡迎訪問網易雲社區,了解更多網易技術產品運營經驗。

前言

從謹慎地在項目中引入kotlin到全部轉為kotlin開發我們用了大概半年的時間。這中間經歷了從在一個小功能中嘗試使用到完全使用kotlin完成了大版本開發的過程。使用方法也從僅僅地用java風格寫kotlin代碼,慢慢地變成使用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()
}

可見sortedDescendingjoinToString都是對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
}

以上是數據結構的定義,我們有一個超市,超市有很多顧客,每個顧客有很多筆訂單,訂單對應著一定數量的產品。下面我們來通過集合的操作來完成以下任務。

// 返回商店中顧客來自的城市列表
fun Shop.getCitiesCustomersAreFrom(): Set<City> = customers.map { it.city }.toSet()

// 返回住在給定城市的所有顧客
fun Shop.getCustomersFrom(city: City): List<Customer> = customers.filter { it.city == city }

// 如果超市中所有顧客都來自於給定城市,則返回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 }

// 返回所有該顧客購買過的商品集合
fun Customer.getOrderedProducts(): Set<Product> = orders.flatMap { it.products }.toSet()

// 返回超市中至少有一名顧客購買過的商品列表
fun Shop.getAllOrderedProducts(): Set<Product> = customers.flatMap { it.getOrderedProducts() }.toSet()

// 返回商店中購買訂單次數最多的用戶
fun Shop.getCustomerWithMaximumNumberOfOrders(): Customer? = customers.maxBy { it.orders.size }

// 返回顧所購買過的最貴的商品
fun Customer.getMostExpensiveOrderedProduct(): Product? = orders.flatMap { it.products }.maxBy { it.price }

// 按照購買訂單數量升序返回商店的顧客
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 }

// 返回商店中未送到訂單比送達訂單要多的顧客列表
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的方法,可以類似於二元運算符使用,舉個例子

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的擴展函數可以實現自定義的二元運算標記。

相關文章:Kotlin精髓 (2)

免費體驗雲安全(易盾)內容安全、驗證碼等服務

更多網易技術、產品、運營經驗分享請點擊


推薦閱讀:
相关文章