轉(zhuǎn)眼間使用 Kotlin 已經(jīng)有兩個(gè)月了瓜喇,時(shí)間不長(zhǎng)棒妨,我也算搭上了 Google 宣布 Kotlin 作為官方支持語言的一波末班車。可能大家早已從純 Java 開發(fā) Android 轉(zhuǎn)為了混合使用開發(fā)甚至是 Kotlin 開發(fā),那你轉(zhuǎn)向 Kotlin 的初衷又是什么呢践险?
對(duì)于我童番,很簡(jiǎn)單崩瓤,只是因?yàn)橐痪湓挘骸窯oogle 爸爸都推薦的語言伪货,我們沒理由不用们衙!」
Kotlin 有著諸多的特性钾怔,比如空指針安全、方法擴(kuò)展砍艾、支持函數(shù)式編程蒂教、豐富的語法糖等巍举。這些特性使得 Kotlin 的代碼比 Java 簡(jiǎn)潔優(yōu)雅許多脆荷,提高了代碼的可讀性和可維護(hù)性,節(jié)省了開發(fā)時(shí)間懊悯,提高了開發(fā)效率蜓谋,但同樣作為 Kotlin 使用者的你,我相信你一定也有不少小建議和小技巧炭分,一直想迫不及待地分享給大家桃焕。
**那就給你一個(gè)機(jī)會(huì),愿你把你的黑科技悄悄留言在本文下方捧毛!
我想給大家的一些小建議
這么有趣的活動(dòng)观堂,那我作為一名兩個(gè)月的 Kotlin 開發(fā),自然也應(yīng)該來這個(gè)活動(dòng)湊湊熱鬧呀忧。
1. 避免使用 IDE 自帶的插件轉(zhuǎn)換 Java 代碼
想必 IDE 里面的插件 "Covert Java File To Kotlin File" 早已被大家熟知师痕,要是不知道的小伙伴,趕緊寫個(gè) Java 文件而账,嘗試點(diǎn)擊 Android Studio 工具欄的 Code 下面的 "Convert Java File To Kotlin File"胰坟,看看都有什么小妙用。
這也是南塵最開始喜歡使用的方式泞辐,沒有技術(shù)卻有一顆裝 ? 的內(nèi)心笔横,直接寫成 Java 文件,再直接一鍵轉(zhuǎn)換為 Kotlin咐吼。甚至寶寶想告訴你吹缔,我 GitHub 上 1k Star 的 AiYaGilr 項(xiàng)目的 Kotlin 分支,也是這樣而來锯茄。但真是踩了不少的坑厢塘。
這樣的方式足夠地快,但卻會(huì)出現(xiàn)很多很多的 !!
撇吞,這是由于 Kotlin 的 null safety 特性俗冻。這是 Kotlin 在 Android 開發(fā)中的很牛逼的一大特性,想必不少小伙伴都被此 Android 的 NullPointException
困擾許久牍颈。我們直接轉(zhuǎn)換 Java 文件造成的各種 !!
迄薄,其實(shí)也就意味著你可能存在潛在的未處理的 KotlinNullPointException
。
2. 盡量地使用 val
val
是線程安全的煮岁,并且不需要擔(dān)心 null 的問題讥蔽,我們自然應(yīng)該盡可能地使用它涣易。
比如我們常用的 Android 解析的服務(wù)器數(shù)據(jù),我們應(yīng)該為自己的 data class 設(shè)置為 val
冶伞,因?yàn)樗旧砭筒粦?yīng)該是可寫的新症。
當(dāng)我第一次使用 Kotlin 的時(shí)候,我以為val
和 var
的區(qū)別在于val
代表不可變响禽,而 var
代表是可變的徒爹。但事實(shí)比這更加微妙:val 不代表不可變,val 意味著只讀芋类。隆嗅。這意味著你不允許明確聲明為 val
,它就不能保證它是不可變的侯繁。
對(duì)于普通變量來說胖喳,「不可變」和「只讀」之間并沒什么區(qū)別,因?yàn)槟銢]辦法復(fù)寫一個(gè) val
變量贮竟,所以在此時(shí)卻是是不可變的丽焊。但在 class 的成員變量中,「只讀」和「不可變」的區(qū)別就大了咕别。
在 Kotlin 的類中技健,val 和 var 是用于表示屬性是否有 getter/setter:
- var:同時(shí)有 getter 和 setter。
- val:只有 getter顷级。
這里是可以通過自定義 getter 函數(shù)來返回不同的值:
class Person(val birthDay: DateTime) {
val age: Int
get() = yearsBetween(birthDay, DateTime.now())
}
可以看到凫乖,雖然沒有方法來設(shè)置 age 的值,但會(huì)隨著當(dāng)前日期的變化而變化弓颈。
這種情況下帽芽,我建議不要自定義 val 屬性的 getter 方法。如果一個(gè)只讀的類屬性會(huì)隨著某些條件而變化翔冀,那么應(yīng)當(dāng)用函數(shù)來替代:
class Person(val birthDay: DateTime) {
fun age(): Int = yearsBetween(birthDay, DateTime.now())
}
這也是 Kotlin 代碼約定 中所提到的导街,當(dāng)具有下面列舉的特點(diǎn)時(shí)使用屬性,不然更推薦使用函數(shù):
- 不會(huì)拋出異常纤子。
- 具有 O(1) 的復(fù)雜度搬瑰。
- 計(jì)算時(shí)的消耗很少。
- 同時(shí)多次調(diào)用有相同的返回值控硼。
因此上面提到的泽论,自定義 getter 方法并隨著當(dāng)前時(shí)間的不同而返回不同的值違反了最后一條原則。大家也要盡量的避免這種情況卡乾。
3. 你真的應(yīng)該好好注意一下伴生對(duì)象
伴生對(duì)象通過在類中使用 companion object
來創(chuàng)建翼悴,用來替代靜態(tài)成員,類似于 Java 中的靜態(tài)內(nèi)部類幔妨。所以在伴生對(duì)象中聲明常量是很常見的做法鹦赎,但如果寫法不對(duì)谍椅,可能就會(huì)產(chǎn)生額外開銷。
比如下面的這段代碼:
class CompanionKotlin {
companion object {
val DATA = "CompanionKotlin_DATA"
}
fun getData(): String = DATA
}
挺簡(jiǎn)潔地一段代碼古话。但將這段簡(jiǎn)潔的 Kotlin 代碼轉(zhuǎn)換為等同的 Java 代碼后雏吭,卻顯的晦澀難懂。
public final class CompanionKotlin {
@NotNull
private static final String DATA = "CompanionKotlin_DATA";
public static final CompanionKotlin.Companion Companion = new CompanionKotlin.Companion((DefaultConstructorMarker)null);
@NotNull
public final String getData() {
return DATA;
}
// ...
public static final class Companion {
@NotNull
public final String getDATA() {
return CompanionKotlin.DATA;
}
private Companion() {
}
// $FF: synthetic method
public Companion(DefaultConstructorMarker $constructor_marker) {
this();
}
}
}
與 Java 直接讀取一個(gè)常量不同陪踩,Kotlin 訪問一個(gè)伴生對(duì)象的私有常量字段需要經(jīng)過以下方法:
- 調(diào)用伴生對(duì)象的靜態(tài)方法
- 調(diào)用伴生對(duì)象的實(shí)例方法
- 調(diào)用主類的靜態(tài)方法
- 讀取主類中的靜態(tài)字段
為了訪問一個(gè)常量杖们,而多花費(fèi)調(diào)用4個(gè)方法的開銷,這樣的 Kotlin 代碼無疑是低效的膊毁。
我們可以通過以下解決方法來減少生成的字節(jié)碼:
- 對(duì)于基本類型和字符串胀莹,可以使用
const
關(guān)鍵字將常量聲明為編譯時(shí)常量。 - 對(duì)于公共字段婚温,可以使用
@JvmField
注解。 - 對(duì)于其他類型的常量媳否,最好在它們自己的主類對(duì)象而不是伴生對(duì)象中來存儲(chǔ)公共的全局常量栅螟。
4. @JvmStatic、@JvmFiled 和 object 還有這種故事篱竭?
我們?cè)?Kotlin 中發(fā)現(xiàn)了 object
這個(gè)東西力图,我以前就一直對(duì)這個(gè)東西很好奇,不知道這是個(gè)什么玩意兒掺逼。
object 吃媒?難道又一個(gè)對(duì)象?
之前有人寫過這樣的代碼吕喘,表示很不解赘那,一個(gè)接口類型的成員變量,訪問外部類的成員變量 name氯质。這不是理所應(yīng)當(dāng)?shù)拿矗?/p>
interface Runnable {
fun run()
}
class Test {
private val name: String = "nanchen"
object impl : Runnable {
override fun run() {
// 這里編譯器會(huì)報(bào)紅報(bào)錯(cuò)募舟。對(duì) name
println(name)
}
}
}
即使查看 Kotlin 官方文檔,也有這樣一段描述:
Sometimes we need to create an object of a slight modification of some class, without explicitly declaring a new subclass for it. Java handles this case with anonymous inner classes. Kotlin slightly generalizes this concept with object expressions and object declarations.
核心意思是:Kotlin 使用 object 代替 Java 匿名內(nèi)部類實(shí)現(xiàn)闻察。
很明顯,即便如此辕漂,這里的訪問應(yīng)該也是合情合理的。從匿名內(nèi)部類中訪問成員變量在 Java 語言中是完全允許的钉嘹。
這個(gè)問題很有意思,解答這個(gè)我們需要生成 Java 字節(jié)碼隧期,再反編譯成 Java 看看具體生成的代碼是什么赘娄。
public final class Test {
private final String name = "nanchen";
public static final class impl implements Runnable {
public static final Test.impl INSTANCE;
public void run() {
}
static {
Test.impl var0 = new Test.impl();
INSTANCE = var0;
}
}
}
public interface Runnable {
void run();
}
靜態(tài)內(nèi)部類!確實(shí)遣臼,Java 中靜態(tài)內(nèi)部類是不允許訪問外部類的成員變量的。但揍堰,說好的 object 代替的是 Java 的匿名內(nèi)部類呢?那這里為啥是靜態(tài)內(nèi)部類嗅义。
這里一定要注意屏歹,如果你只是這樣聲明了一個(gè)object,Kotlin認(rèn)為你是需要一個(gè)靜態(tài)內(nèi)部類之碗。而如果你用一個(gè)變量去接收object表達(dá)式蝙眶,Kotlin認(rèn)為你需要一個(gè)匿名內(nèi)部類對(duì)象。
因此褪那,這個(gè)類應(yīng)該這樣改進(jìn):
interface Runnable {
fun run()
}
class Test {
private val name: String = "nanchen"
private val impl = object : Runnable {
override fun run() {
println(name)
}
}
}
為了避免出現(xiàn)這個(gè)問題幽纷,謹(jǐn)記一個(gè)原則:如果 object 只是聲明,它代表一個(gè)靜態(tài)內(nèi)部類博敬。如果用變量接收 object 表達(dá)式友浸,它代表一個(gè)匿名內(nèi)部類對(duì)象。
講到這偏窝,自然也就知道了 Kotlin 對(duì) object 的三個(gè)作用:
- 簡(jiǎn)化生成靜態(tài)內(nèi)部類
- 生成匿名內(nèi)部類對(duì)象
- 生成單例對(duì)象
咳咳收恢,說了那么多,到底和 @JvmStatic 和 @JvmField 有啥關(guān)系呢?
實(shí)際上祭往,目前我們大多數(shù)的 Android 項(xiàng)目都是 Java 和 Kotlin 混編的伦意,包括我們的項(xiàng)目在內(nèi)也是如此。所以我們總是免不了 Java 和 Kotlin 互調(diào)的情況链沼。我們可能經(jīng)常會(huì)在代碼中這樣編寫:
object Test1 {
val NAME = "nanchen"
fun getAge() = 18
}
在 Java 中會(huì)調(diào)用是這樣的:
System.out.println("name:"+Test1.INSTANCE.getNAME()+",age:"+Test1.INSTANCE.getAge());
作為強(qiáng)迫癥重度患者的我默赂,自然是無法接受上面這樣奇怪的代碼。所以我強(qiáng)烈建議大家在 object 和 companion object 中分別為變量和方法增加上 @JvmField 和 @JvmStatic 注解括勺。
object Test1 {
@JvmField
val NAME = "nanchen"
@JvmStatic
fun getAge() = 18
}
這樣外面 Java 調(diào)用起來就好看多了缆八。
5. by lazy 和 lateinit 相愛相殺
在 Android 開發(fā)中,我們經(jīng)常會(huì)有不少的成員變量需要在 onCreate() 中對(duì)其進(jìn)行初始化疾捍,特別是我們?cè)?XML 中使用的各種控件奈辰,而 Kotlin 要求聲明成員變量的時(shí)候默認(rèn)需要為它聲明一個(gè)初始值。這時(shí)候就會(huì)出現(xiàn)不少的下面這樣的代碼乱豆。
private var textView:TextView? = null
迫于壓力奖恰,我們不能不為這些 View 加上 ? 代表它們可以為空,然后為它們賦值為 null。實(shí)際上瑟啃,我們?cè)谑褂弥幸稽c(diǎn)都不希望它們?yōu)榭章鄯骸_@樣造成的后果就是,我們每次要使用它的時(shí)候都必須去先判斷它不為空蛹屿。這樣無用的代碼屁奏,無疑是浪費(fèi)了我們的工作時(shí)間错负。
好在 Kotlin 推出了 lateinit 關(guān)鍵字:延遲加載。這樣我們可以先繞過 kotlin 的強(qiáng)制要求折联,在后面使用的時(shí)候识颊,再也不需要先判斷它是否為空了谊囚。但要注意,訪問未初始化的 lateinit 屬性會(huì)導(dǎo)致UninitializedPropertyAccessException。
并且 lateinit 不支持基礎(chǔ)數(shù)據(jù)類型沙合,比如 Int首懈。對(duì)于基礎(chǔ)數(shù)據(jù)類型,我們可以這樣:
private var mNumber: Int by Delegates.notNull<Int>()
當(dāng)然滤否,我們還可以使用 let 函數(shù)來進(jìn)行上面的這種情況最仑,但無疑都是畫蛇添足的泥彤。
我們前面說了,在一些明知是只讀不可寫不可變的變量菱父,我們盡可能地用 val 去修飾它浙宜。而 lateinit 僅僅能修飾 var 變量,所以 by lazy 懶加載同仆,是時(shí)候表演真正的技術(shù)了亩钟。
對(duì)于很多不可變的變量清酥,比如上個(gè)頁面通過 bundle 傳遞過來的用于該頁面請(qǐng)求網(wǎng)絡(luò)的參數(shù),比如 MVP 架構(gòu)開發(fā)中的 Presenter臭觉,我們都應(yīng)該用 by lazy 關(guān)鍵字去初始化它辱志。
lazy()
委托屬性可以用于只讀屬性的惰性加載,但是在使用 lazy()
時(shí)經(jīng)常被忽視的地方就是有一個(gè)可選的model參數(shù):
- LazyThreadSafetyMode.SYNCHRONIZED:初始化屬性時(shí)會(huì)有雙重鎖檢查什乙,保證該值只在一個(gè)線程中計(jì)算已球,并且所有線程會(huì)得到相同的值。
- LazyThreadSafetyMode.PUBLICATION:多個(gè)線程會(huì)同時(shí)執(zhí)行忆某,初始化屬性的函數(shù)會(huì)被多次調(diào)用弃舒,但是只有第一個(gè)返回的值被當(dāng)做委托屬性的值状原。
- LazyThreadSafetyMode.NONE:沒有雙重鎖檢查遭笋,不應(yīng)該用在多線程下。
lazy()
默認(rèn)情況下會(huì)指定 LazyThreadSafetyMode.SYNCHRONIZED
喂窟,這可能會(huì)造成不必要線程安全的開銷,應(yīng)該根據(jù)實(shí)際情況碗啄,指定合適的model來避免不需要的同步鎖稳摄。
6.注意 Kotlin 中的 for 循環(huán)
Kotlin提供了 downTo
、step
厦酬、until
胆描、reversed
等函數(shù)來幫助開發(fā)者更簡(jiǎn)單的使用 For 循環(huán)昌讲,如果單一的使用這些函數(shù)確實(shí)是方便簡(jiǎn)潔又高效短绸,但要是將其中兩個(gè)結(jié)合呢筹裕?比如下面這樣:
class A {
fun loop() {
for (i in 10 downTo 0 step 3) {
println(i)
}
}
}
上面使用了 downTo 和 step 兩個(gè)關(guān)鍵字,我們看看 Java 是怎樣實(shí)現(xiàn)的证逻。
public final class A {
public final void loop() {
IntProgression var10000 = RangesKt.step(RangesKt.downTo(10, 0), 3);
int i = var10000.getFirst();
int var2 = var10000.getLast();
int var3 = var10000.getStep();
if (var3 > 0) {
if (i > var2) {
return;
}
} else if (i < var2) {
return;
}
while(true) {
System.out.println(i);
if (i == var2) {
return;
}
i += var3;
}
}
}
毫無疑問:IntProgression var10000 = RangesKt.step(RangesKt.downTo(10, 0), 3);
一行代碼就創(chuàng)建了兩個(gè) IntProgression
臨時(shí)對(duì)象瑟曲,增加了額外的開銷。
7. 注意 Kotlin 的可空和不可空
最近鬧了一個(gè)笑話扯罐,在項(xiàng)目中需要寫一個(gè)上傳跳繩數(shù)據(jù)的功能。于是有了下面的代碼掩浙。
public interface ISkipService {
/**
* 上傳用戶跳繩數(shù)據(jù)
*/
@POST("v2/rope/upload_jump_data")
Observable<BaseResponse<Object>> uploadJumpData(@Field("data") List<SkipHistoryBean> data);
}
寫畢上面的接口厨姚,我們?cè)俚?ViewModel 中進(jìn)行網(wǎng)絡(luò)請(qǐng)求键菱。
private List<SkipHistoryBean> list = new ArrayList<>();
public void uploadClick() {
mNavigator.showProgressDialog();
list.add(bean);
RetrofitManager.create(ISkipService.class)
.uploadJumpData(list)
.compose(RetrofitUtil.schedulersAndGetData())
.subscribe(new BaseSubscriber<Object>() {
@Override
protected void onSuccess(Object data) {
mNavigator.hideProgressDialog();
mNavigator.uploadDataSuccess();
// 點(diǎn)擊上傳成功,刪除數(shù)據(jù)庫
deleteDataFromDB();
}
@Override
protected void onFail(ErrorBean errorBean) {
super.onFail(errorBean);
mNavigator.hideProgressDialog();
mNavigator.uploadDataFailed(errorBean.error_description);
}
});
}
運(yùn)行其實(shí)并沒有什么問題部默。但由于某些原因傅蹂,當(dāng)我把上面的 ISkipService 類修改為了 Kotlin 實(shí)現(xiàn)算凿,卻發(fā)生了崩潰,從代碼上暫時(shí)沒看出問題婚夫。
interface ISkipService {
/**
* 上傳用戶跳繩數(shù)據(jù)
*/
@POST("v2/rope/upload_jump_data")
fun uploadJumpData(@Field("data") data: List<SkipHistoryBean>): Observable<BaseResponse<Any>>
}
但確實(shí)就是崩潰了请敦。仔細(xì)一看储玫,發(fā)現(xiàn) Java 編寫這個(gè)接口的時(shí)候,會(huì)被認(rèn)為這個(gè)參數(shù) "data" 對(duì)應(yīng)的 "value" 是可以為 null 的匣椰,而改為 Kotlin 后禽笑,由于 Kotlin 默認(rèn)不為空的機(jī)制蛤奥,所以需要的參數(shù)是一個(gè)不可以為 null 的 List 集合。而我們的 ViewModel 中使用的 Java 代碼蟀伸,由于 Java 認(rèn)為我們的 List 是可以為 null 的啊掏,所以導(dǎo)致了類型不匹配的崩潰衰猛。
找到了原因,解決方案也就很簡(jiǎn)單娜睛,在 Kotlin 接口中允許參數(shù) data 為 null 或者直接在調(diào)用點(diǎn)加上 @NotNull 注解即可微姊。
寫在最后
真是想繼續(xù)寫呀,但參考了不少的資料薪捍,大家如果覺得有意思可以盡情地參見原文配喳。
參考鏈接:
https://blog.danlew.net/2017/05/30/mutable-vals-in-kotlin/
https://juejin.im/post/5ad18d705188255c5668ddf0
https://tech.meituan.com/Kotlin_code_inspect.html