Kotlin Koans学习笔记(1)

Kotlin Koans是Kotlin官方推出的一系列Kotlin语法练习。一共42个任务,分为6个模块。每一个任务都有一系列单元测试,需要完成的任务就是编码通过单元测试。本文是在学习Kotlin Koans过程中将相关语法点做一个简单的记录。

写在前面,不少童鞋在实际使用中出现了如下错误:

Process finished with exit code 1
Class not found: "i_introduction._0_Hello_World._00_Start"Empty test suite.

我本人也复现了这一个错误,最终按照kotlin-koans 官方的文档重新导入就可以:

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.

怎么导入为 gralde 工程参考下图


选择第4个菜单导入

有网友问关于单元测试操作的问题,我使用的是 Android Studio。单元测试的操作我贴两张截图说一下吧


运行单元测试方法1

运行单元测试方法2

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以及所有被扩展类的公有方法。

任务要求为IntPair<Int, Int>分别实现一个扩展函数r()r()函数的功能就是创建一个有理数。

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按照降序排序。先来实现功能:

<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表达式(哦哦,这好像是Task11的任务要求)

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

这个任务还展示了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()
}

好了以上就是Kotlin Koans第一部分全部13个任务。

下一篇:Kotlin Koans学习笔记(2)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 156,265评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,274评论 1 288
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,087评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,479评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,782评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,218评论 1 207
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,594评论 2 309
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,316评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,955评论 1 237
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,274评论 2 240
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,803评论 1 255
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,177评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,732评论 3 229
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,953评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,687评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,263评论 2 267
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,189评论 2 258

推荐阅读更多精彩内容