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
以及所有被扩展类的公有方法。
任务要求为Int
和Pair<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)
第二部分一共12个任务,都是关于Kotlin集合操作。
13.Introduction
Kotlin提供了一系列的to
方法将一个集合类型转换成另外一个集合类型。
这一部分的第一个任务很简单,根据提示就可以完成,关于任务就不必多说。
先说明一下第二部分所有任务的数据模型。这一部分所有的任务都是围绕一个商店(Shop)展开,商店有一个客户(Customer)列表。
客户具有姓名、城市和订单(Order)列表三个属性。
订单具有商品(Product)列表和是否已经发货两个属性。
商品具有名称和价格两个属性。
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
}
第二部分所有的任务都是使用扩展函数的形式完成。
14.Filter Map
这个任务主要练习使用filter
和 map
这两个方法。
filter
filter方法返回一个包含所有满足指定条件元素的列表。与之对应的还有filterNot,顾名思义就是返回一个包含所有不满足指定条件的元素列表。还有一个filterNotNull,返回所有不为null的元素列表。
回到我们的任务要求:返回指定城市所有客户的列表。使用filter方法就可以完成:
fun Shop.getCustomersFrom(city: City): List<Customer> {
// Return a list of the customers who live in the given city
return customers.filter{it.city == city}
}
再精简一下:
// Return a list of the customers who live in the given city
fun Shop.getCustomersFrom(city: City) = customers.filter{it.city == city}
map
map就是将指定的转换函数运用到原始集合的每一个元素,并返回一个转换后的集合。
任务要求返回所有客户所在城市的Set。这里我们需要使用map 和toSet两个方法:
// Return the set of cities the customers are from
fun Shop.getCitiesCustomersAreFrom() = customers.map { it.city }.toSet()
15.All Any and others Predicates
这个任务主要练习all
,any
和count
等几个方法。
all
第一个小任务是判断是否所有的客户都来自指定的城市。这需要使用Kotlin库提供的all
方法。如果所有的元素都满足指定的条件那么all
方法就返回true:
// Return true if all customers are from the given city
fun Shop.checkAllCustomersAreFrom(city: City): Boolean {
//return customers.filter { it.city != city }.isEmpty()
//return customers.filter{!it.isFrom(city)}.isEmpty()
return customers.all { it.isFrom(city) }
}
当然也可以不使用all来完成,不过效率可能没有all高,因为all方法在遍历的过程中遇到第一个不满足条件的元素就返回结果(false):
// Return true if all customers are from the given city
fun Shop.checkAllCustomersAreFrom(city: City) = customers.filter{!it.isFrom(city)}.isEmpty()
any
第二个小任务就是查询是否至少存在一个用户来自指定的城市。需要使用any
方法,如果至少有一个元素满足指定的条件any
就返回ture:
// Return true if there is at least one customer from the given city
fun Shop.hasCustomerFrom(city: City) = customers.any{it.city==city}
count
第三个小任务计算来自指定城市的客户数量。需要使用count
方法,count
方法返回满足指定条件的元素数量。
fun Shop.countCustomersFrom(city: City): Int {
// Return the number of customers from the given city
return customers.count{ it.city == city}
}
firstOrNull
最后一个小任务,返回一个来自指定城市的客户,如果没有就返回null。需要使用firstOrNull
方法,该方法返回第一个满足指定条件的元素,如果没有就返回null。和它相似的还有first
,不过first是返回第一个满足指定条件的元素,如果没有元素满足指定条件则抛出异常NoSuchElementException
。
// Return a customer who lives in the given city, or null if there is none
fun Shop.findAnyCustomerFrom(city: City) = customers.firstOrNull { it.city == city }
16.FlatMap
这个任务的两个小项都是练习使用flatmap
方法。flatmap
方法就是针对列表中的每一项根据指定的方法生成一个列表,最后将所有的列表拼接成一个列表返回。
第一个小项是要求返回一个客户所有已订购的产品,需要使用flatmap
方法,遍历该用户所有的订单,然后将所有订单的产品拼接起来:
val Customer.orderedProducts: Set<Product> get() {
// Return all products ordered by customer
return orders.flatMap { it.products }.toSet()
}
第二个小项是要求返回所有至少被一个客户订购过的商品集合。这个在第一个小任务的基础上再flatmap一次:
val Shop.allOrderedProducts: Set<Product> get() {
// Return all products that were ordered by at least one customer
return customers.flatMap { it.orderedProducts }.toSet()
}
17.Max Min
第一个任务是返回商店中订单数目最多的一个客户。使用Kotlin库提供的max
方法很好实现。max
方法返回最大的一个元素,如果没有元素则返回null。对于自定义的对象,我们可以通过maxBy
方法提供最大的评判标准,maxBy
方法返回第一个满足指定评判标准的最大值。
fun Shop.getCustomerWithMaximumNumberOfOrders(): Customer? {
// Return a customer whose order count is the highest among all customers
return customers.maxBy { it.orders.size }
}
第二个任务是要求返回一个客户所订购商品中价格最高的一个商品,使用flatmap
和 maxBy
组合:
fun Customer.getMostExpensiveOrderedProduct(): Product? {
// Return the most expensive product which has been ordered
return orders.flatMap { it.products }.maxBy { it.price }
}
当然和max
和maxBy
对应的还有min
和minBy
,只不过返回的是最小值。
18.Sort
Kotlin库提供了为元素排序的方法sorted
。sorted
方法会返回一个升序排序的列表,同样可以通过sortedBy
指定排序的标准,按照指定的标准排序。
任务的要求返回一个客户列表,客户的顺序是根据订单的数量由低到高排列:
fun Shop.getCustomersSortedByNumberOfOrders(): List<Customer> {
// Return a list of customers, sorted by the ascending number of orders they made
return customers.sortedBy { it.orders.size }
}
对于排序操作同样可以要求按照降序排序,两个方法分别是:sortedDescending
和sortedByDescending
。
还有另外一个操作方法就是反转reverse
。
19.Sum
任务要求计算一个客户所有已订购商品的价格总和。使用Kotlin的sumBy
方法就可以完成,sumBy
将集合中所有元素按照指定的函数变换以后的结果累加。当然先要将所有的订单flatmap:
fun Customer.getTotalOrderPrice(): Double {
// Return the sum of prices of all products that a customer has ordered.
// Note: a customer may order the same product for several times.
return orders.flatMap { it.products }.sumByDouble { it.price }
}
20.GroupBy
groupBy
方法返回一个根据指定条件分组好的map。任务要求是返回来自每一个城市的客户的map:
fun Shop.groupCustomersByCity(): Map<City, List<Customer>> {
// Return a map of the customers living in each city
return customers.groupBy { it.city }
}
21.Parition
任务要求返回所有未发货订单数目多于已发货订单的用户。
任务的范例中展示了怎么使用partition
方法。partition
方法会将原始的集合分成一对集合,这一对集合中第一个是满足指定条件的元素集合,第二个是不满足指定条件的集合。
这里我们先给Customer定义一个函数,判断该用户是否属于未发货订单大于已发货订单,处理方法就是使用partition
方法将所有的订单分割,分割的条件就是该订单已经发货:
fun Customer.isMoreUndeliveredOrdersThanDelivered(): Boolean{
val(delivered, undelivered) = orders.partition { it.isDelivered }
return delivered.size < undelivered.size
}
然后再对所有的客户进行筛选:
fun Shop.getCustomersWithMoreUndeliveredOrdersThanDelivered(): Set<Customer> {
// Return customers who have more undelivered orders than delivered
return customers.filter { it.isMoreUndeliveredOrdersThanDelivered() }.toSet()
}
将这两个函数写到一起:
fun Shop.getCustomersWithMoreUndeliveredOrdersThanDelivered() =
customers.filter {
val(delivered, undelivered) = it.orders.partition { it.isDelivered }
undelivered.size > delivered.size
}.toSet()
22.Fold
任务要求返回每一个顾客都购买过的商品集合。
先来看一下fold
方法,fold
方法就是给定一个初始值,然后通过迭代对集合中的每一个元素执行指定的操作并将操作的结果累加。注意操作函数的两个参数分别是累加结果和集合的元素。
直接看fold函数的定义吧:
public inline fun <T, R> Iterable<T>.fold(initial: R, operation: (R, T) -> R): R {
var accumulator = initial
for (element in this) accumulator = operation(accumulator, element)
return accumulator
}
回到我们的任务,使用fold函数,由于任务要求返回所有客户都已经订购的商品,所以初始值设置为所有已经订购的商品,然后用这个初始值去和每一个客户已订购的商品求交集,最终的结果就是所有用户都已经购买过的商品:
fun Shop.getSetOfProductsOrderedByEveryCustomer(): Set<Product> {
// Return the set of products ordered by every customer
return customers.fold(allOrderedProducts, {
orderedByAll, customer -> orderedByAll.intersect(customer.orderedProducts)
})
}
这里使用了Kotlin提供的intersect
方法。
23.CompoundTasks
终于快结束这一部分的任务了。这一部分包括几个小任务,完成任务需要用到前面所练习的各种方法的组合。
来看第一个小任务:返回所有购买了指定商品的客户列表。首先给Customer扩展一个方法,判断他是否已经订购指定的商品,使用any
方法:
fun Customer.hasOrderedProduct(product: Product) = orders.any{it.products.contains(product)}
然后根据他是否已经订购指定商品来做过滤:
fun Shop.getCustomersWhoOrderedProduct(product: Product): Set<Customer> {
// Return the set of customers who ordered the specified product
return customers.filter { it.hasOrderedProduct(product) }.toSet()
}
第二个小任务:查找某个用户所有已发货的商品中最昂贵的商品。首先过滤出已发货的订单,然后flatmap
,再求最大值:
fun Customer.getMostExpensiveDeliveredProduct(): Product? {
// Return the most expensive product among all delivered products
// (use the Order.isDelivered flag)
return orders.filter { it.isDelivered }.flatMap { it.products }.maxBy { it.price }
}
第三个小任务:查找指定商品被购买的次数。首先获取到客户所有已订购的商品列表,使用flatmap
:
fun Customer.getOrderedProducts() = orders.flatMap { it.products }
然后继续flatmap
,将所有客户已经订购的商品组成一个列表,最后再count
:
fun Shop.getNumberOfTimesProductWasOrdered(product: Product): Int {
// Return the number of times the given product was ordered.
// Note: a customer may order the same product for several times.
return customers.flatMap { it.getOrderedProducts() }.count{it == product}
}
将两个函数组合到一起:
// Return the number of times the given product was ordered.
// Note: a customer may order the same product for several times.
fun Shop.getNumberOfTimesProductWasOrdered(product: Product)
= customers.flatMap { it.orders.flatMap { it.products } }.count{it == product}
24.Extensions On Collections
最后一个任务,就是实现 _24_JavaCode.doSomethingStrangeWithCollection
函数的功能。所以先读懂_24_JavaCode.doSomethingStrangeWithCollection
的意图:
public class _24_JavaCode extends JavaCode {
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;
}
}
- 将一个字符串集合按照长度分组,放入一个map中
- 求出map中所有元素(String List)的最大长度
- 根据步骤2的结果,返回map中字符串数目最多的那一组
Kotlin的实现,首先根据长度分组,然后求最大值:
fun doSomethingStrangeWithCollection(collection: Collection<String>): Collection<String>? {
val groupsByLength = collection.groupBy { it.length }
return groupsByLength.values.maxBy { it.size }
}
精简一下:
fun doSomethingStrangeWithCollection(collection: Collection<String>) =
collection.groupBy { it.length }.values.maxBy { it.size }
好了,第二部分全部12个任务都在这里了。
Kotlin Koans学习笔记(3)
这一部分一共7个任务,所有的任务都是围绕日期展开,日期对象具有年、月、日三个属性:
data class MyDate(val year: Int, val month: Int, val dayOfMonth: Int)
这一部分的任务主要是练习Kotlin对象运算符的重载。
Kotlin有固定数目的符号运算符,我们可以很简单的将这些运算符应用到任何一个类。具体的实现方法就是:对需要实现的运算符以Kotlin保留的函数名创建一个函数,这个运算符就会自动映射到相应的函数。为了提醒编译器我们重载了一个运算符,我们需要将重载函数标记为operator
。 在后面的任务中我们可以看到这一特性将大大的提高代码的简洁性和可读性。
这里先列出全部的运算符和它对应的函数。某一个类如果要使用某一个运算符,就需要实现相应的函数。
单目运算符
表达式 | 对应的函数 |
---|---|
+a | a.unaryPlus() |
-a | a.unaryMinus() |
!a | a.not() |
a++ | a.inc() |
a-- | a.dec() |
双目运算符
表达式 | 对应的函数 |
---|---|
a+b | a.plus(b) |
a-b | a.minus(b) |
a*b | a.times(b) |
a/b | a.div(b) |
a%b | a.mod(b) |
a..b | a.rangeTo(b) |
a in b | b.contains(a) |
a !in b | !b.contains(a) |
a+=b | a.plusAssign(b) |
a-=b | a.minusAssign(b) |
a*=b | a.timesAssign(b) |
a/=b | a.divAssign(b) |
a%=b | a.modAssign(b) |
类数组(Array-like)运算符
表达式 | 对应函数 |
---|---|
a[i] | a.get(i) |
a[i,j] | a.get(i,j) |
a[i_1, ..., i_n] | a.get(i_1, ..., i_n) |
a[i]=b | a.set(i,b) |
a[i,j]=b | a.set(i,j,b) |
a[i_1, ..., i_n]=b | a.set(i_1, ..., i_n, b) |
相等运算符
表达式 | 对应的函数 |
---|---|
a==b | a?.equals(b) ?:b === null |
a==b | !(a?.equals(b) ?:b === null) |
函数调用
表达式 | 对应的函数 |
---|---|
a(i) | a.invoke(i) |
a(i,j) | a.invoke(i,j) |
a(i_1, ..., i_n) | a.invoke(i_1, ..., i_n) |
这一部分的任务主要就是实现上面的这些运算符函数。
25.Comparsion
第一个任务要求实现日期对象大小的比较,如下代码能够返回比较结果:
fun task25(date1: MyDate, date2: MyDate): Boolean {
//todoTask25()
return date1 < date2
}
代码不做任何修改是编译不过的,编译器会提示MyDate
类没有实现相关的函数。任务的提示也很清楚,MyDate
类实现Comparable
接口即可:
data class MyDate(val year: Int, val month: Int, val dayOfMonth: Int): Comparable<MyDate> {
override fun compareTo(other: MyDate) =
when{
other.year != year -> year - other.year
other.month != month -> month - other.month
else -> dayOfMonth - other.dayOfMonth
}
}
通过实现Comparable接口,就可以在代码中直接使用大小比较运算符。
26.InRange
这个任务的要求编码实现检查指定的日期是不是在某一个日期范围内:
fun checkInRange(date: MyDate, first: MyDate, last: MyDate): Boolean {
//todoTask26_()
return date in DateRange(first, last)
}
根据前面的表格知道:a in b
翻译以后就是b.contains(a)
,所以这里的任务就转换成实现DateRange
类的contains(d: MyDate):Boolean
函数。DateRange类的定义已经给出来了,它包含一个起始日期(start
)和一个截止日期(endInclusive
)。由于上一个任务已经实现了日期大小的比较,所以这个任务也就很好完成:
operator fun contains(d: MyDate) = (d>=start && d<=endInclusive)
注意函数定义前面的operator
修饰符。
27.RangeTo
这一个任务是要求实现MyDate
类的..
运算符。..
运算符最终会翻译成rangeTo()
函数,所以本任务就是实现MyDate.rangeTo()
,由于在上一个任务中DateRange
已经实现了operator fun contains(d: MyDate)
。所以rangeTo()
函数返回一个DateRange
对象即可:
operator fun MyDate.rangeTo(other: MyDate): DateRange = DateRange(this, other)
fun checkInRange2(date: MyDate, first: MyDate, last: MyDate): Boolean {
//todoTask27()
return date in first..last
}
28.ForLoop
任务要求可以对DateRange内的MyDate执行for循环,也就是要求DataRange
实现Iterable<MyDate>
接口。任务描述中也给出了足够的提示。日期范围的迭代以天
为迭代间隔,所以需要使用已经定义好的nextDay()
方法,一次递增一天:
fun MyDate.nextDay() = addTimeIntervals(DAY, 1)
Iterator<MyDate>
接口有两个方法next()
和hasNext()
:
lass DateRange(val start: MyDate, val endInclusive: MyDate) : Iterable<MyDate>{
...
override fun iterator(): Iterator<MyDate> = DateIterator(this)
}
class DateIterator(val dateRange:DateRange) : Iterator<MyDate> {
var current: MyDate = dateRange.start
override fun next(): MyDate {
val result = current
current = current.nextDay()
return result
}
override fun hasNext(): Boolean = current <= dateRange.endInclusive
}
29.OperatorOverloading
这一个任务包含两个任务,主要练习重载运算符。第一个小任务是重载MyDate
的+
运算符:
fun task29_1(today: MyDate): MyDate {
// todoTask29()
return today + YEAR + WEEK
MyDate
可以和一个时间间隔相加,所以需要实现MyDate.plus()
函数,以时间间隔为参数,有了上一个任务计算下一天的基础,实现和时间间隔相加就比较容易了:
operator fun MyDate.plus(timeInterval: TimeInterval) = addTimeIntervals(timeInterval, 1)
第二个小任务在第一个小任务的基础上更进一步,不再是加一个单一的间隔,要求相加多个时间间隔,如加3年,加7个星期。。。所以需要先实现时间间隔的*
运算符,将TimeInterval
的乘法结果定义成RepeatedTimeInterval
:
class RepeatedTimeInterval(val timeInterval: TimeInterval, val number: Int)
operator fun TimeInterval.times(number: Int) = RepeatedTimeInterval(this, number)
由于这里*
运算返回的是一个RepeatedTimeInterval
对象,所以还需要实现MyDate
和RepeatedTimeInterval
相加:
operator fun MyDate.plus(timeIntervals: RepeatedTimeInterval) = addTimeIntervals(timeIntervals.timeInterval, timeIntervals.number)
仍然是使用已有的addTimeIntervals
方法, 将RepeatedTimeInterval
对象的两个属性作为该方法的两个参数。这样,就可以实现以下这样的表达式运算:
fun task29_2(today: MyDate): MyDate {
//todoTask29()
return today + YEAR * 2 + WEEK * 3 + DAY * 5
}
30.Destructuring Declaration
Kotlin可以将一个对象的所有属性一次赋值给一堆变量:
val (name, age) = person
这段代码会被编译成:
val name = person.component1()
val age = person.component2()
这就是为什么DataClass
会自动生成componentN()
函数。
任务的要求是判断一个日期是否是闰年,第一步需要将时间的属性赋值给年、月、日。这就需要使用destructuring declaration
。
<pre>
data class MyDate(val year: Int, val month: Int, val dayOfMonth: Int)
fun isLeapDay(date: MyDate): Boolean {
//todoTask30()
val (year, month, dayOfMonth) = date
// 29 February of a leap year
return year % 4 == 0 && month == 2 && dayOfMonth == 29
}
</pre>
注意如果需要使用Destructuring Declaration,类必须声明为data
类型。
31.Invoke
终于到了这一部分的最后一个任务,invoke。Invoke是什么呢?如果一个类实现了invoke()
这个函数,那么该类的实体对象在调用这个函数时可以省略函数名,当然invoke
函数必须要有operator
修饰符。先看一下栗子吧:
class customClass{
operator fun invoke(param: Int){
println("print in invoke method, param is :"+param)
}
operator fun invoke(param: Int, msg: String): customClass{
println("print in invoke method, param is :"+param+",message is:" + msg)
return this
}
}
那么就可以这样使用:
val printer = customClass()
printer(2)
printer.invoke(4)
printer(12, "first")(23, "second")(46)
执行结果就是:
print in invoke method, param is :2
print in invoke method, param is :4
print in invoke method, param is :12,message is:first
print in invoke method, param is :23,message is:second
print in invoke method, param is :46
回到我们的任务,要求如下代码返回的结果为4:
fun task31(invokable: Invokable): Int {
//todoTask31()
return invokable()()()().getNumberOfInvocations()
}
所以需要实现Invokable
的invoke
方法,该方法每调用一次,内部计数器就需要加1,最后返回计数器的值:
class Invokable {
var numberOfInvocations: Int = 0
operator fun invoke(): Invokable {
numberOfInvocations++
return this
}
}