1. 程式人生 > >Kotlin在Android上的運用(三)

Kotlin在Android上的運用(三)

0.HelloWorld

和所有其他語言一樣,Kotlin Koans的第一個任務名稱就是Hello World,這個任務比較簡單,提示也說的很清楚,就是要求task0函式返回一個字串OK:

fun task0(): String {
    return "OK"
}

這一個任務主要涉及kotlin的函式定義。在kotlin中函式通過關鍵字fun宣告,和Java中函式的返回型別寫在函式名稱前不一樣,Kotlin中函式的返回型別在函式名稱的後面,中間以:分開。Kotlin中的函式總是返回一個值,如果不指定返回值的型別,預設返回Uint(類似Java中的Void)。如果函式體就是一個簡單的語句,可以去掉大括弧,用等號表示:

fun task0(): String = "OK"

1.Java to Kotlin Convert

這個任務的要求就是將一段Java程式碼轉換成Kotlin程式碼,提示可以直接將Java程式碼複製貼上,然後使用Intellij提供的Convert Java File to Kotlin File功能(僅僅是這個任務允許這樣做),非常便捷。

//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()
        }

這一段程式碼兩者之間沒有明顯的差別,但是在下一個任務中可以看到Kotlin中這一段程式碼可以精簡成一行程式碼。

2.Named Arguments (命名引數)

任務的要求是使用Kotlin提供的方法joinToString()重新完成任務1,只指定joinToString的引數中的兩個引數。

這裡涉及到Kotlin函式的預設引數(Default Arguments)和命名引數(Named Arguments)兩個語法。

Kotlin中函式引數可以有預設值,當函式被呼叫時,如果沒有傳遞對應的引數,那麼就使用預設值。和其他語言相比,這以功能可以大大的減少過載函式的數目。引數的預設值在引數的型別後面通過=賦值。重寫函式(overriding method)使用和被重寫函式相同的預設引數。也就是說當我們重寫一個有預設引數的函式時,我們不允許重新指定預設引數的值。

當我們在呼叫函式時,可以為傳遞的引數命名,這在當一個函式的引數很多或者函式引數具有預設值的時候非常方便。

讓我們回到任務本身,該任務要求使用joinToString函式重新完成任務1,並且只能指定兩個引數。

我們來看一下joinToString函式的定義:

/**
 * 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()
}

該函式對分隔符,字首,字尾等其他引數都指定了預設值,我們再參考任務1中的描述,我們只需要重新指定字首、字尾兩個引數。命名引數通過在引數值的前面指定引數名稱就可以,中間需要一個=:

fun task2(collection: Collection<Int>): String {
    return collection.joinToString(prefix = "{", postfix = "}")
}

3.Default Arguments(預設引數)

預設引數的語法在前面已經做了介紹,直接來看任務。任務要求是將JavaCode3中所有的函式過載用一個函式替換。

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);
    }
}

所有的過載都是解決一個問題,字串和數字的拼接,並且需要說明字母是否轉換為大寫,預設是不轉換。Kotlin的實現:

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

這個任務的要求是用Kotlin重寫JavaCode4.task4()函式,不允許使用Iterables類,可以通過Intellij IDEA的程式碼補全來選擇合適的方法。

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整數倍的元素,先實現功能:

fun task4(collection: Collection<Int>): Boolean  {
    val devide42: (Int) -> Boolean = { x -> x % 42 == 0 }
    return collection.filter(devide42).isEmpty().not()
}

這裡使用了Collection的filter函式。這個任務主要學習Kotlin中Lambda表示式的知識,簡單來說:

lambda表示式總是用大括弧包起來
它的引數(如果有的話)在->符號前面宣告(引數型別可以省略)
函式體寫在->符號後面
在Kotlin中,如果一個函式的最後一個引數是一個函式,並且你在呼叫該函式時最後一個引數傳遞的是一個lambda表示式,那麼可以將這個lambda表示式寫在括弧外面:

fun task4(collection: Collection<Int>): Boolean  {
    return collection.filter(){ x -> x % 42 == 0 }.isEmpty().not()
}

如果只有lambda表示式這一個引數,那麼括弧也可以省略:

fun task4(collection: Collection<Int>): Boolean  {
    return collection.filter{ x -> x % 42 == 0 }.isEmpty().not()
}

如果lambda表示式也只有一個引數,那麼這個引數連同->符號也可以省略,直接將它命名為it:

fun task4(collection: Collection<Int>): Boolean = 
    collection.filter { it%42 ==0 }.isNotEmpty()

5.String Templates

任務要求生成一個正則表示式,可以匹配’13 JUN 1992’這樣格式的字串。
主要是學習Kotlin的各種字串模板,Kotlin中字串有兩種,通過 一對”包起來自字串,這裡可以支援轉義字元。如:

val s = "Hello, world!\n"

或者通過一對”“”包起來的字串,如:

val text = """
    for (c in "foo")
        print(c)
"""

字串還可以包含模板表示式(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
"""

回到我們的任務本身,任務裡面已經給了足夠的提示,完成起來也比較容易:

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

任務要求將JavaCode6.Person類轉換成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實現:

data class Person(val name: String, val age: Int)

Kotlin中Data class對應Java中的實體類,需要在定義類時標明為data,編譯器會自動根據主建構函式中定義的屬性生成下面這些成員函式:

equals()函式和hashCode()函式
toString()函式,返回的形式為”Person(name=TangSir, age=28)”
componentN()函式,componentN()具體返回的值根據類定義時屬性定義的屬性決定。如:

 val name = person.component1()
 val age = person.component2()

copy()函式

但是如果上面列出來的函式已經在類的實現中顯式定義或者繼承了父類相應的函式,編譯器則不會生成相應的函式。

為了保持一致性以及編譯器所生成程式碼具有意義,data class必須滿足以下這些條件:

主建構函式至少有一個引數
主建構函式中的所有引數都必須定義為val或者var
Data class不能是abstract, open, sealed or inner
7.Nullable Type

任務要求將ava版本sendMessageToClient用Kotlin實現,只允許使用一個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對null的保護總結為以下幾點:

如果一個變數可能為null,那麼在定義的時候在型別後面加上?
val a: Int? = null
對於一個可能為null的變數,如果必須在其值不為null時才進行後續操作,那麼可以使用?.操作符來保護,下面的兩種表示方式是等效的,即a為null時什麼都不做:

 val a: Int? = null
  ...
  //if statment
  if (a != null){
      a.toLong()
  }

  //?.operator
  a?.toLong()

當一個變數為null時,如果我們想為它提供一個替代值,那麼可以使用?:

val myLong = a?.toLong() ?: 0L

上面的語句的意思就是如果a確實為null,那麼將myLong賦值為0

最後一條,就是對於如果一個可能是null,如果我們可以確保它已經不是null,那麼可以使用!!操作符。但是不推薦這麼使用。!!是壞程式碼的味道。

 val a: Int? = null
  ...
  a!!.toLong()

回到我們的任務,由於只允許使用一個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

任務要求使用Kotlin的Smart Cast和When表示式重新實現JavaCode8.eval()函式:

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");
    }
}

也是我們常見的強制型別轉換。

在Kotlin中,多數情況下我麼不需要顯式的使用強制轉換,因為編譯器會為不可變的變數帶入is檢查,並且在必要的時候自動插入(安全的)強制轉換。is通常和when表示式一起搭配使用:

when (x) {
    is Int -> print(x + 1)
    is String -> print(x.length + 1)
    is IntArray -> print(x.sum())
}

when和Java中的switch/case語句相似,但是要強大的多。主要區別在於when語句的引數可以是任何型別,其所有分支的判斷條件也可以是任何型別的。

when (x) {
    1 -> print("x == 1")
    2 -> print("x == 2")
    else -> { // Note the block
        print("x is neither 1 nor 2")
    }
}

when可以作為一個語句也可以作為一個表示式,如果作為一個表示式使用,它可以有返回值,所以必須覆蓋所有可能的條件或者實現else分支,否則編譯器會報錯。

val result = when (x){
    0, 1 -> "binary"
    else -> "error"
}

除此之外,when的條件語句還有很多其他的表達方式,如判斷範圍等,可以參考官方文件。

回到任務的解決:

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中函式擴充套件就是不修改一個類的原始碼(通常是我們沒有原始碼,無法修改)的情況下,通過擴充套件函式為一個類新增一個新的功能。擴充套件函式在行為好像它屬於被擴充套件的類,所以在擴充套件函式中我們可以使用this以及所有被擴充套件類的公有方法。

任務要求為Int和Pair

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的擴充套件函式r中this可以省略:

fun Pair<Int, Int>.r(): RationalNumber = RationalNumber(first,second)

10.Object Expression

Kotlin中當我們需要建立某一個類的物件並且只需要對該物件做一點小的修改,而不需要重新建立一個子類時可以使用object expression。和Java中的匿名內部類很相似。

任務的要求是建立一個比較器(comparator),提供給Collection類對list按照降序排序。先來實現功能:

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
}

加粗部分就是所謂的object expression,修改為lambda表示式(哦哦,這好像是Task11的任務要求)

fun task10(): List<Int> {
    val arrayList = arrayListOf(1, 5, 2)
    Collections.sort(arrayList) { x, y -> y - x }
    return arrayList
}

這個任務還展示了Kotlin和Java之間的互動,因為task10()函式中Collections是一個Java類。

11.SAM Conversions

所謂SAM conversions就是如果一個object實現了一個SAM(Single Abstract Method)介面時,可以直接傳遞一個lambda表示式,程式碼實現在上面。

12.Extensions On Collections

在Kotlin的標準庫提供了許多擴充套件函式使得集合的使用非常方便。由於Kotlin可以很容易的和Java程式碼混合使用,所有Kotlin直接是使用標準的Java集合類(做了細小的改進)。

本任務的要求就是使用擴充套件函式sortedDescending重寫上一個任務中的程式碼:

fun task12(): List<Int> {
    return arrayListOf(1, 5, 2).sortedDescending()
}