Kotlin Koans是Kotlin官方推出的一系列Kotlin語法練習(xí)。一共42個(gè)任務(wù)喘鸟,分為6個(gè)模塊橡卤。每一個(gè)任務(wù)都有一系列單元測試,需要完成的任務(wù)就是編碼通過單元測試榜跌。本文是在學(xué)習(xí)Kotlin Koans過程中將相關(guān)語法點(diǎn)做一個(gè)簡單的記錄闪唆。
寫在前面,不少童鞋在實(shí)際使用中出現(xiàn)了如下錯(cuò)誤:
Process finished with exit code 1
Class not found: "i_introduction._0_Hello_World._00_Start"Empty test suite.
我本人也復(fù)現(xiàn)了這一個(gè)錯(cuò)誤钓葫,最終按照kotlin-koans 官方的文檔重新導(dǎo)入就可以:
How to build and run tests:
- 1悄蕾、Working with the project using Intellij IDEA or Android Studio:
Import the project as Gradle project. - 2、To build the project and run tests use test
task on Gradle panel.
怎么導(dǎo)入為 gralde 工程參考下圖
有網(wǎng)友問關(guān)于單元測試操作的問題础浮,我使用的是 Android Studio帆调。單元測試的操作我貼兩張截圖說一下吧
0.HelloWorld
和所有其他語言一樣,Kotlin Koans的第一個(gè)任務(wù)名稱就是Hello World豆同,這個(gè)任務(wù)比較簡單番刊,提示也說的很清楚,就是要求task0
函數(shù)返回一個(gè)字符串OK
:
fun task0(): String {
return "OK"
}
這一個(gè)任務(wù)主要涉及kotlin的函數(shù)定義影锈。在kotlin中函數(shù)通過關(guān)鍵字fun
聲明芹务,和Java中函數(shù)的返回類型寫在函數(shù)名稱前不一樣,Kotlin中函數(shù)的返回類型在函數(shù)名稱的后面鸭廷,中間以:
分開枣抱。Kotlin中的函數(shù)總是返回一個(gè)值,如果不指定返回值的類型辆床,默認(rèn)返回Uint
(類似Java中的Void
)佳晶。如果函數(shù)體就是一個(gè)簡單的語句,可以去掉大括弧讼载,用等號(hào)表示:
fun task0(): String = "OK"
1.Java to Kotlin Convert
這個(gè)任務(wù)的要求就是將一段Java代碼轉(zhuǎn)換成Kotlin代碼轿秧,提示可以直接將Java代碼復(fù)制粘貼中跌,然后使用Intellij提供的Convert Java File to Kotlin File
功能(僅僅是這個(gè)任務(wù)允許這樣做),非常便捷淤刃。
//Java
public String task1(Collection<Integer> collection) {
StringBuilder sb = new StringBuilder();
sb.append("{");
Iterator<Integer> iterator = collection.iterator();
while (iterator.hasNext()) {
Integer element = iterator.next();
sb.append(element);
if (iterator.hasNext()) {
sb.append(", ");
}
}
sb.append("}");
return sb.toString();
}
//Kotlin
fun todoTask1(collection: Collection<Int>): String
{
val sb = StringBuilder()
sb.append("{")
val iterator = collection.iterator()
while (iterator.hasNext()) {
val element = iterator.next()
sb.append(element)
if (iterator.hasNext()) {
sb.append(", ")
}
}
sb.append("}")
return sb.toString()
}
這一段代碼兩者之間沒有明顯的差別晒他,但是在下一個(gè)任務(wù)中可以看到Kotlin中這一段代碼可以精簡成一行代碼。
2.Named Arguments (命名參數(shù))
任務(wù)的要求是使用Kotlin提供的方法joinToString()
重新完成任務(wù)1,只指定joinToString
的參數(shù)中的兩個(gè)參數(shù)逸贾。
這里涉及到Kotlin函數(shù)的默認(rèn)參數(shù)(Default Arguments)和命名參數(shù)(Named Arguments)兩個(gè)語法陨仅。
Kotlin中函數(shù)參數(shù)可以有默認(rèn)值,當(dāng)函數(shù)被調(diào)用時(shí)铝侵,如果沒有傳遞對(duì)應(yīng)的參數(shù)灼伤,那么就使用默認(rèn)值。和其他語言相比咪鲜,這以功能可以大大的減少重載函數(shù)的數(shù)目狐赡。參數(shù)的默認(rèn)值在參數(shù)的類型后面通過=
賦值。重寫函數(shù)(overriding method)使用和被重寫函數(shù)相同的默認(rèn)參數(shù)疟丙。也就是說當(dāng)我們重寫一個(gè)有默認(rèn)參數(shù)的函數(shù)時(shí)颖侄,我們不允許重新指定默認(rèn)參數(shù)的值。
當(dāng)我們?cè)谡{(diào)用函數(shù)時(shí)享郊,可以為傳遞的參數(shù)命名览祖,這在當(dāng)一個(gè)函數(shù)的參數(shù)很多或者函數(shù)參數(shù)具有默認(rèn)值的時(shí)候非常方便。
讓我們回到任務(wù)本身炊琉,該任務(wù)要求使用joinToString
函數(shù)重新完成任務(wù)1展蒂,并且只能指定兩個(gè)參數(shù)。
我們來看一下joinToString
函數(shù)的定義:
/**
* Creates a string from all the elements separated using [separator] and using the given [prefix] and [postfix] if supplied.
*
* If the collection could be huge, you can specify a non-negative value of [limit], in which case only the first [limit]
* elements will be appended, followed by the [truncated] string (which defaults to "...").
*/
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()
}
該函數(shù)對(duì)分隔符苔咪,前綴锰悼,后綴等其他參數(shù)都指定了默認(rèn)值,我們?cè)賲⒖既蝿?wù)1中的描述团赏,我們只需要重新指定前綴箕般、后綴兩個(gè)參數(shù)。命名參數(shù)通過在參數(shù)值的前面指定參數(shù)名稱就可以舔清,中間需要一個(gè)=
:
fun task2(collection: Collection<Int>): String {
return collection.joinToString(prefix = "{", postfix = "}")
}
3.Default Arguments(默認(rèn)參數(shù))
默認(rèn)參數(shù)的語法在前面已經(jīng)做了介紹隘世,直接來看任務(wù)。任務(wù)要求是將JavaCode3中所有的函數(shù)重載用一個(gè)函數(shù)替換鸠踪。
public class JavaCode3 extends JavaCode {
private int defaultNumber = 42;
public String foo(String name, int number, boolean toUpperCase) {
return (toUpperCase ? name.toUpperCase() : name) + number;
}
public String foo(String name, int number) {
return foo(name, number, false);
}
public String foo(String name, boolean toUpperCase) {
return foo(name, defaultNumber, toUpperCase);
}
public String foo(String name) {
return foo(name, defaultNumber);
}
}
所有的重載都是解決一個(gè)問題,字符串和數(shù)字的拼接复斥,并且需要說明字母是否轉(zhuǎn)換為大寫营密,默認(rèn)是不轉(zhuǎn)換。Kotlin的實(shí)現(xiàn):
fun foo(name: String, number: Int = 42, toUpperCase: Boolean = false): String {
val upCaseName = if (toUpperCase) name.toUpperCase() else name
return upCaseName+number.toString()
}
精簡一下:
fun foo(name: String, number: Int = 42, toUpperCase: Boolean = false): String
= (if (toUpperCase) name.toUpperCase() else name)+number
4.Lambdas
這個(gè)任務(wù)的要求是用Kotlin重寫JavaCode4.task4()
函數(shù)目锭,不允許使用Iterables
類评汰,可以通過Intellij IDEA的代碼補(bǔ)全來選擇合適的方法纷捞。
Java版本的代碼:
public class JavaCode4 extends JavaCode {
public boolean task4(Collection<Integer> collection) {
return Iterables.any(collection, new Predicate<Integer>() {
@Override
public boolean apply(Integer element) {
return element % 42 == 0;
}
});
}
}
就是判斷列表中是否包含42整數(shù)倍的元素,先實(shí)現(xiàn)功能:
fun task4(collection: Collection<Int>): Boolean {
val devide42: (Int) -> Boolean = { x -> x % 42 == 0 }
return collection.filter(devide42).isEmpty().not()
}
這里使用了Collection的filter函數(shù)被去。這個(gè)任務(wù)主要學(xué)習(xí)Kotlin中Lambda表達(dá)式的知識(shí)主儡,簡單來說:
- lambda表達(dá)式總是用大括弧包起來
- 它的參數(shù)(如果有的話)在
->
符號(hào)前面聲明(參數(shù)類型可以省略) - 函數(shù)體寫在
->
符號(hào)后面
在Kotlin中,如果一個(gè)函數(shù)的最后一個(gè)參數(shù)是一個(gè)函數(shù)惨缆,并且你在調(diào)用該函數(shù)時(shí)最后一個(gè)參數(shù)傳遞的是一個(gè)lambda表達(dá)式糜值,那么可以將這個(gè)lambda表達(dá)式寫在括弧外面:
fun task4(collection: Collection<Int>): Boolean {
return collection.filter(){ x -> x % 42 == 0 }.isEmpty().not()
}
如果只有l(wèi)ambda表達(dá)式這一個(gè)參數(shù),那么括弧也可以省略:
fun task4(collection: Collection<Int>): Boolean {
return collection.filter{ x -> x % 42 == 0 }.isEmpty().not()
}
如果lambda表達(dá)式也只有一個(gè)參數(shù)坯墨,那么這個(gè)參數(shù)連同->
符號(hào)也可以省略寂汇,直接將它命名為it
:
fun task4(collection: Collection<Int>): Boolean =
collection.filter { it%42 ==0 }.isNotEmpty()
5.String Templates
任務(wù)要求生成一個(gè)正則表達(dá)式,可以匹配'13 JUN 1992'這樣格式的字符串捣染。
主要是學(xué)習(xí)Kotlin的各種字符串模板骄瓣,Kotlin中字符串有兩種,通過 一對(duì)"
包起來自字符串耍攘,這里可以支持轉(zhuǎn)義字符榕栏。如:
val s = "Hello, world!\n"
或者通過一對(duì)"""
包起來的字符串,如:
val text = """
for (c in "foo")
print(c)
"""
字符串還可以包含模板表達(dá)式(template expressions)蕾各,如:
val i = 10
val s = "i = $i" // evaluates to "i = 10"
val s1 = "abc"
val str = "$s1.length is ${s1.length}" // evaluates to "abc.length is 3"
val price = """
${'$'}9.99
"""
回到我們的任務(wù)本身扒磁,任務(wù)里面已經(jīng)給了足夠的提示,完成起來也比較容易:
val month = "(JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC)"
fun task5(): String = """\d{2} ${month} \d{4}"""
6.Data Class
任務(wù)要求將JavaCode6.Person類轉(zhuǎn)換成Kotlin示损。先來看看Java源碼:
public class JavaCode6 extends JavaCode {
public static class Person {
private final String name;
private final int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
}
}
Kotlin實(shí)現(xiàn):
data class Person(val name: String, val age: Int)
Kotlin中Data class對(duì)應(yīng)Java中的實(shí)體類渗磅,需要在定義類時(shí)標(biāo)明為data
,編譯器會(huì)自動(dòng)根據(jù)主構(gòu)造函數(shù)中定義的屬性生成下面這些成員函數(shù):
equals()
函數(shù)和hashCode()
函數(shù)toString()
函數(shù)检访,返回的形式為"Person(name=TangSir, age=28)"
-
componentN()
函數(shù)始鱼,componentN()具體返回的值根據(jù)類定義時(shí)屬性定義的屬性決定。如:val name = person.component1() val age = person.component2()
copy()
函數(shù)
但是如果上面列出來的函數(shù)已經(jīng)在類的實(shí)現(xiàn)中顯式定義或者繼承了父類相應(yīng)的函數(shù)脆贵,編譯器則不會(huì)生成相應(yīng)的函數(shù)医清。
為了保持一致性以及編譯器所生成代碼具有意義,data class必須滿足以下這些條件:
- 主構(gòu)造函數(shù)至少有一個(gè)參數(shù)
- 主構(gòu)造函數(shù)中的所有參數(shù)都必須定義為
val
或者var
- Data class不能是abstract, open, sealed or inner
7.Nullable Type
任務(wù)要求將ava版本sendMessageToClient用Kotlin實(shí)現(xiàn)卖氨,只允許使用一個(gè)if
語句:
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);
}
這是我們常見的防御式編程会烙,處處都要考慮變量是否為null的情況。
Kotlin對(duì)null的保護(hù)總結(jié)為以下幾點(diǎn):
如果一個(gè)變量可能為null筒捺,那么在定義的時(shí)候在類型后面加上
?
val a: Int? = null
-
對(duì)于一個(gè)可能為null的變量柏腻,如果必須在其值不為null時(shí)才進(jìn)行后續(xù)操作,那么可以使用
?.
操作符來保護(hù)系吭,下面的兩種表示方式是等效的五嫂,即a
為null時(shí)什么都不做:val a: Int? = null ... //if statment if (a != null){ a.toLong() } //?.operator a?.toLong()
當(dāng)一個(gè)變量為null時(shí),如果我們想為它提供一個(gè)替代值,那么可以使用
?:
val myLong = a?.toLong() ?: 0L
上面的語句的意思就是如果a
確實(shí)為null沃缘,那么將myLong
賦值為0
-
最后一條躯枢,就是對(duì)于如果一個(gè)可能是null,如果我們可以確保它已經(jīng)不是null槐臀,那么可以使用
!!
操作符锄蹂。但是不推薦這么使用。!!是壞代碼的味道水慨。val a: Int? = null ... a!!.toLong()
回到我們的任務(wù)得糜,由于只允許使用一個(gè)if語句,官方的參考答案是這樣的:
fun sendMessageToClient(
client: Client?, message: String?, mailer: Mailer
) {
val email = client?.personalInfo?.email
if (email != null && message != null) {
mailer.sendMessage(email, message)
}
}
8.Smart Casts
任務(wù)要求使用Kotlin的Smart Cast和When表達(dá)式重新實(shí)現(xiàn)JavaCode8.eval()
函數(shù):
public class JavaCode8 extends JavaCode {
public int eval(Expr expr) {
if (expr instanceof Num) {
return ((Num) expr).getValue();
}
if (expr instanceof Sum) {
Sum sum = (Sum) expr;
return eval(sum.getLeft()) + eval(sum.getRight());
}
throw new IllegalArgumentException("Unknown expression");
}
}
也是我們常見的強(qiáng)制類型轉(zhuǎn)換讥巡。
在Kotlin中掀亩,多數(shù)情況下我么不需要顯式的使用強(qiáng)制轉(zhuǎn)換,因?yàn)榫幾g器會(huì)為不可變的變量帶入is
檢查欢顷,并且在必要的時(shí)候自動(dòng)插入(安全的)強(qiáng)制轉(zhuǎn)換槽棍。is
通常和when
表達(dá)式一起搭配使用:
when (x) {
is Int -> print(x + 1)
is String -> print(x.length + 1)
is IntArray -> print(x.sum())
}
when
和Java中的switch/case
語句相似,但是要強(qiáng)大的多抬驴。主要區(qū)別在于when語句的參數(shù)可以是任何類型炼七,其所有分支的判斷條件也可以是任何類型的。
when (x) {
1 -> print("x == 1")
2 -> print("x == 2")
else -> { // Note the block
print("x is neither 1 nor 2")
}
}
when可以作為一個(gè)語句也可以作為一個(gè)表達(dá)式布持,如果作為一個(gè)表達(dá)式使用豌拙,它可以有返回值,所以必須覆蓋所有可能的條件或者實(shí)現(xiàn)else
分支题暖,否則編譯器會(huì)報(bào)錯(cuò)按傅。
val result = when (x){
0, 1 -> "binary"
else -> "error"
}
除此之外,when的條件語句還有很多其他的表達(dá)方式胧卤,如判斷范圍等唯绍,可以參考官方文檔。
回到任務(wù)的解決:
fun eval(e: Expr): Int =
when (e) {
is Num -> e.value
is Sum -> eval(e.left) + eval(e.right)
else -> throw IllegalArgumentException("Unknown expression")
}
9.Extension Functions
Kotlin中函數(shù)擴(kuò)展就是不修改一個(gè)類的源碼(通常是我們沒有源碼枝誊,無法修改)的情況下况芒,通過擴(kuò)展函數(shù)為一個(gè)類添加一個(gè)新的功能。擴(kuò)展函數(shù)在行為好像它屬于被擴(kuò)展的類叶撒,所以在擴(kuò)展函數(shù)中我們可以使用this
以及所有被擴(kuò)展類的公有方法绝骚。
任務(wù)要求為Int
和Pair<Int, Int>
分別實(shí)現(xiàn)一個(gè)擴(kuò)展函數(shù)r()
。r()
函數(shù)的功能就是創(chuàng)建一個(gè)有理數(shù)祠够。
data class RationalNumber(val numerator: Int, val denominator: Int)
fun Int.r(): RationalNumber = RationalNumber(this, 1)
fun Pair<Int, Int>.r(): RationalNumber = RationalNumber(this.first,this.second)
Pair的擴(kuò)展函數(shù)r中this可以省略:
fun Pair<Int, Int>.r(): RationalNumber = RationalNumber(first,second)
10.Object Expression
Kotlin中當(dāng)我們需要?jiǎng)?chuàng)建某一個(gè)類的對(duì)象并且只需要對(duì)該對(duì)象做一點(diǎn)小的修改压汪,而不需要重新創(chuàng)建一個(gè)子類時(shí)可以使用object expression。和Java中的匿名內(nèi)部類很相似古瓤。
任務(wù)的要求是創(chuàng)建一個(gè)比較器(comparator)蛾魄,提供給Collection類對(duì)list按照降序排序。先來實(shí)現(xiàn)功能:
<pre>
fun task10(): List<Int> {
val arrayList = arrayListOf(1, 5, 2)
Collections.sort(arrayList,object: Comparator<Int>{
override fun compare(x: Int, y: Int): Int {
return y-x
}
})
return arrayList
}
</pre>
加粗部分就是所謂的object expression,修改為lambda表達(dá)式(哦哦滴须,這好像是Task11的任務(wù)要求)
<pre>
fun task10(): List<Int> {
val arrayList = arrayListOf(1, 5, 2)
Collections.sort(arrayList) { x, y -> y - x }
return arrayList
}
</pre>
這個(gè)任務(wù)還展示了Kotlin和Java之間的交互,因?yàn)?code>task10()函數(shù)中Collections
是一個(gè)Java類叽奥。
11.SAM Conversions
所謂SAM conversions就是如果一個(gè)object實(shí)現(xiàn)了一個(gè)SAM(Single Abstract Method)接口時(shí)扔水,可以直接傳遞一個(gè)lambda表達(dá)式,代碼實(shí)現(xiàn)在上面朝氓。
12.Extensions On Collections
在Kotlin的標(biāo)準(zhǔn)庫提供了許多擴(kuò)展函數(shù)使得集合的使用非常方便魔市。由于Kotlin可以很容易的和Java代碼混合使用,所有Kotlin直接是使用標(biāo)準(zhǔn)的Java集合類(做了細(xì)小的改進(jìn))赵哲。
本任務(wù)的要求就是使用擴(kuò)展函數(shù)sortedDescending
重寫上一個(gè)任務(wù)中的代碼:
fun task12(): List<Int> {
return arrayListOf(1, 5, 2).sortedDescending()
}
好了以上就是Kotlin Koans第一部分全部13個(gè)任務(wù)待德。