在 Kotlin 中使用列表

1. 准备工作

在日常生活中,我们常常针对各种情况制定列表,例如待办事项列表、活动来宾列表、心愿清单或购物清单。在编程中,列表同样非常有用。例如,应用中可能会提供新闻文章列表、歌曲列表、日历活动列表或社交媒体帖子列表。

列表是一项重要的编程概念,学习如何创建和使用列表将有助于您创建更复杂的应用。

在此 Codelab 中,您将通过 Kotlin 园地熟悉 Kotlin 中的列表,并创建一个用于订购不同种类面汤的程序。您饿了吗?

前提条件

  • 熟悉如何使用 Kotlin 园地创建和修改 Kotlin 程序。
  • 熟悉《使用 Kotlin 进行 Android 开发的基础知识》课程第 1 单元中介绍的基本 Kotlin 编程概念:main() 函数、函数参数和返回值、变量、数据类型和操作,以及控制流语句。
  • 能够定义 Kotlin 类、通过该类创建对象实例以及访问其属性和方法。
  • 能够创建子类并了解它们之间如何互相继承。

学习内容

  • 如何在 Kotlin 中创建和使用列表?
  • ListMutableList 之间的区别,以及各自适用的使用场景
  • 如何遍历列表中的所有项,并对每个项执行操作。

构建内容

  • 您将在 Kotlin 园地中试用列表和列表操作。
  • 您将在 Kotlin 园地中创建一个使用列表的订餐程序。
  • 您的程序将能够创建订单、在订单中添加面条和蔬菜,然后计算订单的总金额。

所需条件

  • 一台能够连接互联网以访问 Kotlin 园地的计算机。

2. 列表简介

在之前的 Codelab 中,您已经了解了 Kotlin 中的基本数据类型(例如 IntDoubleBooleanString)。利用它们可以在变量中存储某种类型的值。但是,如果您想存储多个值,该怎么办?这便是 List 数据类型的用武之地。

列表是指按特定顺序排列的项的集合。Kotlin 中有两种类型的列表:

  • 只读列表:List 一经创建便无法再修改。
  • 可变列表:MutableList 创建之后可以进行修改,这意味着您可以添加、移除或更新其元素。

使用 ListMutableList 时,您必须指定它可以包含的元素类型。例如,List<Int> 可存储整数列表,而 List<String> 则可存储字符串列表。如果您在程序中定义了 Car 类,则可以创建 List<Car> 用于存储 Car 对象实例列表。

了解列表的最佳方式就是尝试使用它们。

创建列表

  1. 打开 Kotlin 园地并删除已提供的现有代码。
  2. 添加一个空的 main() 函数。接下来的所有代码步骤都将包含在此 main() 函数中。
fun main() {

}
  1. main() 中,创建一个名为 numbers、类型为 List<Int> 的变量,因为此变量将包含一个只读整数列表。使用 Kotlin 标准库函数 listOf() 创建一个新的 List,并将列表中的元素作为实参(用英文逗号分隔)传入。listOf(1, 2, 3, 4, 5, 6) 会返回一个包含整数 1 到 6 的只读列表。
val numbers: List<Int> = listOf(1, 2, 3, 4, 5, 6)
  1. 如果可以根据赋值运算符 (=) 右侧的值猜测(或推断)出变量的类型,则可以不指定变量的数据类型。因此,可以将此行代码缩短为下面这样:
val numbers = listOf(1, 2, 3, 4, 5, 6)
  1. 使用 println() 输出 numbers 列表。
println("List: $numbers")

请注意,在字符串中添加 $,表示后面的内容是一个表达式,该表达式将被求值,然后再将结果添加到此字符串中(请参阅字符串模板)。这行代码还可以编写为 println("List: " + numbers).

  1. 使用 numbers.size 属性检索列表的大小,并输出结果。
println("Size: ${numbers.size}")
  1. 运行程序。输出结果是一个包含列表中所有元素的列表以及该列表的大小。请注意,方括号 [] 表明这是一个 List。方括号中是 numbers 元素(以英文逗号分隔)。另请注意,元素的顺序与您创建时的顺序相同。
List: [1, 2, 3, 4, 5, 6]
Size: 6

访问列表元素

列表特有的功能是,您可以根据索引(一个表示元素位置的整数)访问列表中的每个元素。下面是我们创建的 numbers 列表的示意图,其中显示了每个元素及其相应的索引。

cb6924554804458d.png

索引实际上是相对于首个元素的偏移量。比如,当您输入 list[2] 时,您不是在请求列表中的第二个元素,而是在请求相对于首个元素偏移 2 个位置的元素。因此,list[0] 表示首个元素(零偏移),list[1] 表示第二个元素(偏移量为 1),list[2] 表示第三个元素(偏移量为 2),以此类推。

将以下代码添加到 main() 函数中现有代码之后。执行完每个步骤之后都运行一下代码,以便验证输出内容是否符合您的预期。

  1. 输出列表中位于索引 0 处的首个元素。您可以使用所需的索引调用 get() 函数(如 numbers.get(0));或者,您也可以使用简写语法,用方括号将索引括起来(如 numbers[0])。
println("First element: ${numbers[0]}")
  1. 接下来,输出列表中位于索引 1 处的第二个元素。
println("Second element: ${numbers[1]}")

列表的有效索引值(“索引”)从 0 开始,到最后一个索引(即列表的大小减 1)结束。这就表示,您的 numbers 列表的索引为 0 到 5。

  1. 输出列表的最后一个元素,可以通过 numbers.size - 1 来计算其索引,得出的结果应为 5。访问位于第 5 个索引处的元素应返回 6 作为输出。
println("Last index: ${numbers.size - 1}")
println("Last element: ${numbers[numbers.size - 1]}")
  1. Kotlin 还支持对列表执行 first()last() 操作。尝试调用 numbers.first()numbers.last(),并查看输出结果。
println("First: ${numbers.first()}")
println("Last: ${numbers.last()}")

您会看到,numbers.first() 返回列表的首个元素,而 numbers.last() 返回列表的最后一个元素。

  1. 另一个实用的列表操作是 contains() 方法,它可以查明指定的元素是否在列表中。例如,如果您有一份公司员工姓名列表,您可以使用 contains() 方法来查明指定的姓名是否在列表中。

numbers 列表中,使用列表中的某个整数调用 contains() 方法。numbers.contains(4) 会返回值 true。然后,使用不在列表中的某个整数调用 contains() 方法。numbers.contains(7) 会返回 false

println("Contains 4? ${numbers.contains(4)}")
println("Contains 7? ${numbers.contains(7)}")
  1. 完成后的代码应如下所示。注释为可选。
fun main() {
    val numbers = listOf(1, 2, 3, 4, 5, 6)
    println("List: $numbers")
    println("Size: ${numbers.size}")

    // Access elements of the list
    println("First element: ${numbers[0]}")
    println("Second element: ${numbers[1]}")
    println("Last index: ${numbers.size - 1}")
    println("Last element: ${numbers[numbers.size - 1]}")
    println("First: ${numbers.first()}")
    println("Last: ${numbers.last()}")

    // Use the contains() method
    println("Contains 4? ${numbers.contains(4)}")
    println("Contains 7? ${numbers.contains(7)}")
}
  1. 运行您的代码。输出如下所示。
List: [1, 2, 3, 4, 5, 6]
Size: 6
First element: 1
Second element: 2
Last index: 5
Last element: 6
First: 1
Last: 6
Contains 4? true
Contains 7? false

列表是只读的

  1. 删除 Kotlin 园地中的代码,并替换为以下代码。colors 列表被初始化为以 Strings 形式表示的 3 种颜色的列表。
fun main() {
    val colors = listOf("green", "orange", "blue")
}
  1. 请记住,您不能在只读 List 中添加或更改元素。我们来看看,如果您尝试在列表中添加项,或者通过将列表中的某个元素设置为新值来修改该元素,会发生什么。
colors.add("purple")
colors[0] = "yellow"
  1. 运行代码,您会收到多条错误消息。实质上,这些错误的意思就是 add() 方法不适用于 List,您不能更改元素的值。

dd21aaccdf3528c6.png

  1. 移除错误的代码。

您已经亲眼看到,无法更改只读列表。不过,有一些列表操作虽然不会更改列表,但会返回新的列表。reversed()sorted() 就是其中的两个操作。reversed() 函数会返回元素按照倒序排列的新列表;而 sorted() 函数则会返回元素按照升序排列的新列表。

  1. 添加代码,以使 colors 列表按照倒序排列。输出结果。这是一个新列表,其中包含按照倒序排列的 colors 元素。
  2. 添加第二行代码,以输出原始的 list 列表,这样您就可以看到原始列表并未更改。
println("Reversed list: ${colors.reversed()}")
println("List: $colors")
  1. 以下便是输出的两个列表。
Reversed list: [blue, orange, green]
List: [green, orange, blue]
  1. 添加代码,以使用 sorted() 函数返回按照升序排列的 List
println("Sorted list: ${colors.sorted()}")

输出结果为按照字母顺序排列的新颜色列表。棒极了!

Sorted list: [blue, green, orange]
  1. 您还可以尝试对未排序的数字列表使用 sorted() 函数。
val oddNumbers = listOf(5, 3, 7, 1)
println("List: $oddNumbers")
println("Sorted list: ${oddNumbers.sorted()}")
List: [5, 3, 7, 1]
Sorted list: [1, 3, 5, 7]

现在,您已经明白了能够创建列表的好处。不过,如果在创建列表后可以对其进行修改就更棒了,因此,接下来我们来了解可变列表。

3. 可变列表简介

可变列表是指在创建后可以修改的列表。您可以添加、移除或更改其中的项。同时还可以执行可对只读列表执行的所有操作。可变列表的类型为 MutableList,您可以通过调用 mutableListOf() 来创建此类列表。

创建 MutableList

  1. 删除 main() 中的现有代码。
  2. main() 函数中,创建一个空的可变列表,并将其赋值给名为 entreesval 变量。
val entrees = mutableListOf()

如果您尝试运行代码,会出现以下错误。

Not enough information to infer type variable T

如前所述,当您创建 MutableListList 时,Kotlin 会尝试根据传递的实参来推断列表包含的元素的类型。例如,如果您编写 listOf("noodles"),Kotlin 会推断出您想要创建一个 String 列表。在初始化没有元素的空列表时,Kotlin 无法推断元素类型,因此您必须明确指定类型。为此,您可以在 mutableListOflistOf 后的尖括号内添加相应类型。(在文档中,您可能会看到这显示为 <T>,其中 T 代表类型参数)。

  1. 更正变量声明,指明您想要创建一个 String 类型的可变列表。
val entrees = mutableListOf<String>()

另一种更正此错误的方法是,预先指定变量的数据类型。

val entrees: MutableList<String> = mutableListOf()
  1. 输出列表。
println("Entrees: $entrees")
  1. 输出会显示 [],表示列表为空。
Entrees: []

向列表中添加元素

当您添加、移除和更新元素时,可变列表会变得很有趣。

  1. 通过 entrees.add("noodles")."noodles" 添加到列表中。如果成功将该元素添加到列表中,add() 函数会返回 true;否则,返回 false
  2. 输出列表,以确认已成功添加 "noodles"
println("Add noodles: ${entrees.add("noodles")}")
println("Entrees: $entrees")

输出结果如下:

Add noodles: true
Entrees: [noodles]
  1. 将另一个项 "spaghetti" 添加到列表中。
println("Add spaghetti: ${entrees.add("spaghetti")}")
println("Entrees: $entrees")

生成的 entrees 列表现在包含两个项。

Add spaghetti: true
Entrees: [noodles, spaghetti]

您可以不使用 add() 逐个添加元素,而使用 addAll() 一次添加多个元素,并将其传递到列表中。

  1. 创建一个 moreItems 列表。您不需要更改此列表,因此可将其设置为 val 且不可变。
val moreItems = listOf("ravioli", "lasagna", "fettuccine")
  1. 使用 addAll(),将新列表中的所有项添加到 entrees 中。输出生成的列表。
println("Add list: ${entrees.addAll(moreItems)}")
println("Entrees: $entrees")

输出结果显示列表添加成功。entrees 列表现在共包含 5 个项。

Add list: true
Entrees: [noodles, spaghetti, ravioli, lasagna, fettuccine]
  1. 现在,尝试向此列表中添加一个数字。
entrees.add(10)

此操作失败,并显示以下错误:

The integer literal does not conform to the expected type String

这是因为 entrees 列表需要的是 String 类型的元素,而您试图添加 Int 类型的元素。请记住,只能向列表中添加正确数据类型的元素,否则会出现编译错误。这是 Kotlin 通过类型安全确保代码更加安全的一种方法。

  1. 移除错误的代码行,以便代码能够正常编译。

从列表中移除元素

  1. 调用 remove() 以便从列表中移除 "spaghetti"。再次输出列表。
println("Remove spaghetti: ${entrees.remove("spaghetti")}")
println("Entrees: $entrees")
  1. 移除 "spaghetti" 会返回 true,因为该元素存在于列表中,可以成功移除。该列表现在只剩下 4 个项。
Remove spaghetti: true
Entrees: [noodles, ravioli, lasagna, fettuccine]
  1. 如果尝试移除列表中不存在的项,会发生什么?尝试使用 entrees.remove("rice") 从列表中移除 "rice"
println("Remove item that doesn't exist: ${entrees.remove("rice")}")
println("Entrees: $entrees")

remove() 方法会返回 false,因为该元素不存在,因此无法移除。列表保持不变,仍然只有 4 个项。输出结果如下:

Remove item that doesn't exist: false
Entrees: [noodles, ravioli, lasagna, fettuccine]
  1. 您还可以指定要移除的元素的索引。使用 removeAt() 移除位于索引 0 处的项。
println("Remove first element: ${entrees.removeAt(0)}")
println("Entrees: $entrees")

removeAt(0) 的返回值为已从列表中移除的首个元素 ("noodles")。entrees 列表现在剩下 3 个项。

Remove first element: noodles
Entrees: [ravioli, lasagna, fettuccine]
  1. 如果要清除整个列表,您可以调用 clear()
entrees.clear()
println("Entrees: $entrees")

输出结果现在显示了一个空列表。

Entrees: []
  1. Kotlin 提供了 isEmpty() 函数来检查列表是否为空。请尝试输出 entrees.isEmpty().
println("Empty? ${entrees.isEmpty()}")

输出结果应为 true,因为列表当前为空,没有任何元素。

Empty? true

如果您想要对列表执行某项操作或访问某个元素,但需要先确认列表不为空,isEmpty() 方法就会非常有用。

以下是您针对可变列表编写的所有代码。注释为可选。

fun main() {
    val entrees = mutableListOf<String>()
    println("Entrees: $entrees")

    // Add individual items using add()
    println("Add noodles: ${entrees.add("noodles")}")
    println("Entrees: $entrees")
    println("Add spaghetti: ${entrees.add("spaghetti")}")
    println("Entrees: $entrees")

    // Add a list of items using addAll()
    val moreItems = listOf("ravioli", "lasagna", "fettuccine")
    println("Add list: ${entrees.addAll(moreItems)}")
    println("Entrees: $entrees")

    // Remove an item using remove()
    println("Remove spaghetti: ${entrees.remove("spaghetti")}")
    println("Entrees: $entrees")
    println("Remove item that doesn't exist: ${entrees.remove("rice")}")
    println("Entrees: $entrees")

    // Remove an item using removeAt() with an index
    println("Remove first element: ${entrees.removeAt(0)}")
    println("Entrees: $entrees")

    // Clear out the list
    entrees.clear()
    println("Entrees: $entrees")

    // Check if the list is empty
    println("Empty? ${entrees.isEmpty()}")
}

4. 循环遍历列表

如需对列表中的每个项执行操作,您可以循环遍历该列表(也称为“遍历列表”)。循环可用于 ListsMutableLists

While 循环

有一种类型的循环是 while 循环。在 Kotlin 中,while 循环以 while 关键字开头。这类循环包含一个代码块(位于大括号内),只要括号中的表达式为 true,该代码块就会反复不停地执行下去。为了防止代码永久执行下去(即无限循环),该代码块必须包含用于更改表达式的值的逻辑,以便表达式最终变为 false,进而停止执行循环。这时,您就可以退出 while 循环,并继续执行该循环之后的代码。

while (expression) {
    // While the expression is true, execute this code block
}

使用 while 循环来遍历列表。创建一个变量来跟踪您当前在列表中查看的 index。您每查看一个元素,此 index 变量都会增加 1,直到达到列表的最后一个索引,然后您便会退出循环。

  1. 删除 Kotlin 园地中的现有代码,然后您就会得到一个空的 main() 函数。
  2. 假设您正在组织一场派对。创建一个列表,其中每个元素代表一个家庭回复的宾客人数。第一个家庭表示,他们家会有 2 人参加。第二个家庭表示,他们家会有 4 人参加;等等。
val guestsPerFamily = listOf(2, 4, 1, 3)
  1. 计算总共会有多少人参加派对。请编写一个循环来找到答案。针对宾客总数创建一个 var,并将其初始化为 0
var totalGuests = 0
  1. 如前所述,初始化 index 变量的 var
var index = 0
  1. 编写一个 while 循环,以遍历列表。条件是只要 index 值小于列表大小,便继续执行代码块。
while (index < guestsPerFamily.size) {

}
  1. 在该循环中,会获取列表中位于当前 index 处的元素,并将其加到宾客总数变量中。请注意,totalGuests += guestsPerFamily[index]totalGuests = totalGuests + guestsPerFamily[index]. 的作用相同。

请注意,循环的最后一行使用 index++ 使 index 变量按 1 递增,以便循环的下一次迭代查看列表中的下一个家庭。

while (index < guestsPerFamily.size) {
    totalGuests += guestsPerFamily[index]
    index++
}
  1. while 循环之后,您可以输出结果。
while ... {
    ...
}
println("Total Guest Count: $totalGuests")
  1. 运行程序,输出结果如下所示。您可以通过手动合计列表中的数字来验证输出的结果是否正确。
Total Guest Count: 10

以下是完整的代码段:

val guestsPerFamily = listOf(2, 4, 1, 3)
var totalGuests = 0
var index = 0
while (index < guestsPerFamily.size) {
    totalGuests += guestsPerFamily[index]
    index++
}
println("Total Guest Count: $totalGuests")

使用 while 循环时,您必须编写代码来创建用于跟踪索引的变量、获取列表中位于相应索引处的元素,并更新该索引变量。遍历列表还有一种更加快速、简洁的方式,那就是使用 for 循环!

For 循环

for 循环是另一种类型的循环。它可以更轻松地遍历列表。在 Kotlin 中,这种循环以 for 关键字开头,其代码块包含在大括号中。执行代码块的条件在圆括号中指定。

for (number in numberList) {
   // For each element in the list, execute this code block
}

在此示例中,变量 number 被设置为等于 numberList 的首个元素,并开始执行代码块。然后,number 变量自动更新为 numberList 的下一个元素,并再次执行代码块。这种操作会针对列表中的每个元素重复执行,直到达到 numberList 的末尾。

  1. 删除 Kotlin 园地中的现有代码,并替换为以下代码:
fun main() {
    val names = listOf("Jessica", "Henry", "Alicia", "Jose")
}
  1. 添加一个 for 循环,以输出 names 列表中的所有项。
for (name in names) {
    println(name)
}

相对于必须用 while 循环编写,这种方法要简单得多!

  1. 输出结果如下:
Jessica
Henry
Alicia
Jose

一种常见的列表操作是对列表中的每个元素执行某种操作。

  1. 修改循环,以同时输出人员姓名中包含的字符数。提示:您可以使用 Stringlength 属性来确定该 String 中的字符数。
val names = listOf("Jessica", "Henry", "Alicia", "Jose")
for (name in names) {
    println("$name - Number of characters: ${name.length}")
}

输出结果如下:

Jessica - Number of characters: 7
Henry - Number of characters: 5
Alicia - Number of characters: 6
Jose - Number of characters: 4

循环中的代码并未更改原始 List,而只是影响了输出结果。

您可以编写指令来指明应对 1 个列表项执行的操作,然后该代码会对每个列表项执行,这种方法非常简洁!使用循环可避免输入大量重复的代码。

现在,您已经尝试了创建和使用列表及可变列表,并且了解了循环,是时候在示例用例中运用这些知识了!

5. 综合应用

在本地餐厅订餐时,客户的一个订单中通常会包含多项菜品。使用列表是存储订单信息的理想选择。此外,您还将利用您所掌握的关于类和继承的知识来创建一个更加强大可伸缩的 Kotlin 程序,而不是将所有代码都放到 main() 函数中。

对于接下来的一系列任务,您需要创建一个允许订购不同菜品组合的 Kotlin 程序。

首先,我们来看一下最终代码的以下示例输出。您能否开动脑筋想一想,需要创建哪些类型的类来帮助整理所有这些数据?

Order #1
Noodles: $10
Total: $10

Order #2
Noodles: $10
Vegetables Chef's Choice: $5
Total: $15

在输出中您会看到:

  • 其中有一个订单列表
  • 每个订单都有一个编号
  • 每个订单都可以包含一个菜品列表,例如面条和蔬菜
  • 每种菜品都有一个价格
  • 每个订单都有一个总价,即各项菜品的价格总和

您可以创建一个类来表示 Order,再创建另一个类来表示每个菜品,例如 NoodlesVegetables。您可能会进一步观察到 NoodlesVegetables 具有一些相似之处,因为它们都是菜品并且都有价格。您可以考虑创建一个 Item 类,并为其指定 Noodle 类和 Vegetable 类都可以继承的共享属性。这样一来,您便无需在 Noodle 类和 Vegetable 类中重复相同的逻辑。

  1. 您将获得以下起始代码。专业开发者经常需要阅读其他人的代码,例如,当他们加入新项目或需要在别人创建的功能上添加代码时。能够阅读和理解代码是一项需要掌握的重要技能。

请花点时间看一看此代码,想想它会执行什么操作。复制此代码并将其粘贴到 Kotlin 园地中,然后运行。请务必先删除 Kotlin 园地中的所有现有代码,然后再粘贴这段新代码。查看输出内容,看看这是否能帮助您更好地理解代码。

open class Item(val name: String, val price: Int)

class Noodles : Item("Noodles", 10)

class Vegetables : Item("Vegetables", 5)

fun main() {
    val noodles = Noodles()
    val vegetables = Vegetables()
    println(noodles)
    println(vegetables)
}
  1. 您应该会看到类似如下的输出内容:
Noodles@5451c3a8
Vegetables@76ed5528

现在我们对代码进行更详细的说明。首先,代码中有一个名为 Item 的类,其构造函数接受 2 个形参:菜品的 name(字符串类型)和 price(整数类型)。这两个属性在传入后均不会更改,因此将它们标记为 val。由于 Item 是一个父类,会从其扩展子类,因此使用 open 关键字标记该类。

Noodles 类构造函数未接受任何形参,而是从 Item 进行扩展,通过传入 "Noodles"(作为 name)和 10(作为 price)来调用父类构造函数。Vegetables 类与此类似,只不过是通过传入 "Vegetables" 和 5(作为 price)来调用父类构造函数。

main() 函数会初始化 NoodlesVegetables 类的新对象实例,并将其输出。

替换 toString() 方法

当您输出某个对象实例时,会调用该对象的 toString() 方法。在 Kotlin 中,每个类都会自动继承 toString() 方法。此方法的默认实现仅返回对象类型以及相应实例的内存地址。您应该替换 toString(),以返回比 Noodles@5451c3a8Vegetables@76ed5528 对用户更有意义和更易于理解的信息。

  1. Noodles 类中,替换 toString() 方法,让其返回 name。请记住,Noodles 会从其父类 Item 继承 name 属性。
class Noodles : Item("Noodles", 10) {
   override fun toString(): String {
       return name
   }
}
  1. 针对 Vegetables 类重复上述步骤。
class Vegetables() : Item("Vegetables", 5) {
   override fun toString(): String {
       return name
   }
}
  1. 运行您的代码。输出内容现在看起来比之前好理解了:
Noodles
Vegetables

接下来,您将更改 Vegetables 类构造函数以接受一些参数,并更新 toString() 方法以反映额外的信息。

自定义订单中的蔬菜

为使面汤更吸引人,您可以在订单中加入不同的蔬菜。

  1. main() 函数中,初始化 Vegetables 实例时,并非采用任何输入实参,而是传入客户想要的特定类型的蔬菜。
fun main() {
    ...
    val vegetables = Vegetables("Cabbage", "Sprouts", "Onion")
    ...
}

如果您现在尝试编译代码,将会显示以下错误消息:

Too many arguments for public constructor Vegetables() defined in Vegetables

现在,您要向 Vegetables 类构造函数传入 3 个字符串类型的形参,因此需要修改 Vegetables 类。

  1. 更新 Vegetables 类标头,以接受 3 个字符串参数,如以下代码所示:
class Vegetables(val topping1: String,
                 val topping2: String,
                 val topping3: String) : Item ("Vegetables", 5) {
  1. 现在,再次编译您的代码。但是,只有当客户总是点 3 种蔬菜时,此解决方案才有用。当客户想要点 1 种或 5 种蔬菜时,就行不通了。
  2. 为了解决此问题,可以不为每种蔬菜使用一个属性,而是在 Vegetables 类的构造函数中接受一个蔬菜列表(可以是任意长度)。该 List 应仅包含 Strings,因此输入参数的类型为 List<String>
class Vegetables(val toppings: List<String>) : Item("Vegetables", 5) {

这并非最佳解决方案,因为在 main() 中,您需要先更改代码以创建浇头列表,然后再将其传入 Vegetables 构造函数。

Vegetables(listOf("Cabbage", "Sprouts", "Onion"))

还有一种更好的方法来解决此问题。

  1. 在 Kotlin 中,您可以利用 vararg 修饰符将可变数量的同类型实参传递到函数或构造函数中。通过这种方式,您便能够以单独的字符串(而非列表)的形式提供不同的蔬菜。

更改 Vegetables 的类定义,以接受 String 类型的 vararg toppings

class Vegetables(vararg val toppings: String) : Item("Vegetables", 5) {
  1. main() 函数中的这段代码现在就可以正常运行了。您可以通过传入任意数量的浇头字符串来创建 Vegetables 实例。
fun main() {
    ...
    val vegetables = Vegetables("Cabbage", "Sprouts", "Onion")
    ...
}
  1. 现在,修改 Vegetables 类的 toString() 方法,使其返回同样按以下格式显示浇头的 StringVegetables Cabbage, Sprouts, Onion

以菜品的名称 (Vegetables) 开头。然后使用 joinToString() 方法将所有浇头添加到单个字符串中。使用 + 运算符将这个两部分结合起来,并在中间添加一个空格。

class Vegetables(vararg val toppings: String) : Item("Vegetables", 5) {
    override fun toString(): String {
        return name + " " + toppings.joinToString()
    }
}
  1. 运行您的程序,输出结果应如下所示:
Noodles
Vegetables Cabbage, Sprouts, Onion
  1. 编写程序时,您需要考虑所有可能的输入。当 Vegetables 构造函数没有输入参数时,应以更加便于用户理解的方式处理 toString() 方法。

由于客户想要蔬菜,但没有指明要哪种蔬菜,一种解决方案是默认为他们提供厨师选择的蔬菜。

更新 toString() 方法,以在没有传入浇头时返回 Vegetables Chef's Choice。注意利用前面介绍的 isEmpty() 方法。

override fun toString(): String {
    if (toppings.isEmpty()) {
        return "$name Chef's Choice"
    } else {
        return name + " " + toppings.joinToString()
    }
}
  1. 更新 main() 函数,以测试创建不带任何构造函数实参和带多个实参的 Vegetables 实例的可能性。
fun main() {
    val noodles = Noodles()
    val vegetables = Vegetables("Cabbage", "Sprouts", "Onion")
    val vegetables2 = Vegetables()
    println(noodles)
    println(vegetables)
    println(vegetables2)
}
  1. 验证输出内容是否符合预期。
Noodles
Vegetables Cabbage, Sprouts, Onion
Vegetables Chef's Choice

创建一个订单

现在,您已经有一些菜品,可以创建订单了。将订单逻辑封装在程序的 Order 类中。

  1. 想一想,哪些属性和方法适合 Order 类。同样,以下也是最终代码的一些示例输出,希望对您有帮助。
Order #1
Noodles: $10
Total: $10

Order #2
Noodles: $10
Vegetables Chef's Choice: $5
Total: $15

Order #3
Noodles: $10
Vegetables Carrots, Beans, Celery: $5
Total: $15

Order #4
Noodles: $10
Vegetables Cabbage, Onion: $5
Total: $15

Order #5
Noodles: $10
Noodles: $10
Vegetables Spinach: $5
Total: $25
  1. 您可能已经想到了以下几种:

Order 类

属性:订单编号、菜品列表

方法:添加菜品、添加多个菜品、输出订单摘要(包括价格)

  1. 首先我们来关注属性,每个属性的数据类型应该是什么?这些属性应该公开还是为类所私有?它们应该以参数形式传入,还是应该在类中定义?
  2. 实现这一点的方法有多种,其中一种为:创建一个带整数 orderNumber 构造函数参数的 class Order
class Order(val orderNumber: Int)
  1. 由于您事先可能并不知道订单中的所有菜品,因此不应将菜品列表以实参形式传入,而是可以将其声明为顶级类变量,并将其初始化为能够存储 Item 类型元素的空 MutableList。将该变量标记为 private,以便只有此类能够直接修改该菜品列表。这样做可以防止列表被除此类之外的代码以不希望的方式修改。
class Order(val orderNumber: Int) {
    private val itemList = mutableListOf<Item>()
}
  1. 接下来,将方法也添加到类定义中。您可以随意为每种方法选择合适的名称,也可以暂时将每种方法内的实现逻辑保留为空。此外,您还要确定需要使用哪些函数参数和返回哪些值。
class Order(val orderNumber: Int) {
   private val itemList = mutableListOf<Item>()

   fun addItem(newItem: Item) {
   }

   fun addAll(newItems: List<Item>) {
   }

   fun print() {
   }
}
  1. addItem() 方法看上去最简单,因此首先实现该函数。该方法应接受一个新的 Item,然后应将其添加到 itemList 中。
fun addItem(newItem: Item) {
    itemList.add(newItem)
}
  1. 接下来实现 addAll() 方法。该方法接受一个只读的菜品列表。将其中的所有菜品添加到内部菜品列表中。
fun addAll(newItems: List<Item>) {
    itemList.addAll(newItems)
}
  1. 然后再实现 print() 方法,用于输出所有菜品及其价格的摘要,以及订单总价。

首先输出订单编号。然后,使用循环来遍历订单列表中的所有菜品。输出每个菜品及其价格。此外,还要计算订单到目前为止的总价,并在遍历列表的过程中继续计算新的总价。最后输出最终的总价。尝试自行实现此逻辑。如果需要帮助,请查看以下解决方案。

您可能需要添加货币符号,以使输出更易于理解。下面提供了一种实现该解决方案的方法。这段代码使用了 $ 货币符号,但您完全可以将其修改为您本地的货币符号。

fun print() {
    println("Order #${orderNumber}")
    var total = 0
    for (item in itemList) {
        println("${item}: $${item.price}")
        total += item.price
    }
    println("Total: $${total}")
}

对于 itemList 中的每个 item,输出 item(这会触发对 item 调用 toString()),然后再输出菜品的 price。此外,在循环之前,需要将一个 total 整数变量初始化为 0。然后,通过将当前菜品的价格加入 total,不断计算新的总价。

创建多个订单

  1. 通过在 main() 函数中创建多个 Order 实例来测试您的代码。请先删除 main() 函数中现有的内容。
  2. 您可以使用以下示例订单,也可以创建自己的订单。在订单中试验不同的菜品组合,以确保测试完代码中的所有代码路径。例如,在 Order 类中测试 addItem()addAll() 方法,创建不带实参和带实参的 Vegetables 实例,等等。
fun main() {
    val order1 = Order(1)
    order1.addItem(Noodles())
    order1.print()

    println()

    val order2 = Order(2)
    order2.addItem(Noodles())
    order2.addItem(Vegetables())
    order2.print()

    println()

    val order3 = Order(3)
    val items = listOf(Noodles(), Vegetables("Carrots", "Beans", "Celery"))
    order3.addAll(items)
    order3.print()
}
  1. 上述代码的输出应如下所示。验证总价是否计算正确。
Order #1
Noodles: $10
Total: $10

Order #2
Noodles: $10
Vegetables Chef's Choice: $5
Total: $15

Order #3
Noodles: $10
Vegetables Carrots, Beans, Celery: $5
Total: $15

太棒了!现在看起来就像是菜品订单了!

6. 改进代码

维护订单列表

如果您构建的是现实中将用于面馆的程序,那么就需要维护一个所有客户订单的列表。

  1. 创建一个用于存储所有订单的列表。该列表应该是一个只读列表还是可变列表?
  2. 将以下代码添加到 main() 函数中。首先,将列表初始化为空列表。然后,每创建一个订单,就将相应订单添加到列表中。
fun main() {
    val ordersList = mutableListOf<Order>()

    val order1 = Order(1)
    order1.addItem(Noodles())
    ordersList.add(order1)

    val order2 = Order(2)
    order2.addItem(Noodles())
    order2.addItem(Vegetables())
    ordersList.add(order2)

    val order3 = Order(3)
    val items = listOf(Noodles(), Vegetables("Carrots", "Beans", "Celery"))
    order3.addAll(items)
    ordersList.add(order3)
}

随着时间的推移会不断有订单添加到列表中,因此该列表应为 Order 类型的 MutableList。然后,对 MutableList 使用 add() 方法,以添加每个订单。

  1. 有了订单列表后,您就可以使用循环来输出每个订单。在订单之间输出一个空白行,以使输出内容更易于阅读。
fun main() {
    val ordersList = mutableListOf<Order>()

    ...

    for (order in ordersList) {
        order.print()
        println()
    }
}

这里移除了 main() 函数中的重复代码,使代码更易于阅读!输出内容应当与先前一样。

针对订单实现构建器模式

为了使您的 Kotlin 代码更加简洁,您可以使用构建器模式来创建订单。构建器模式是编程中的一种设计模式,您可以利用这种模式一步步地构建复杂的对象。

  1. Order 类中的 addItem()addAll() 方法返回更改后的 Order,而不是返回 Unit(或者不返回任何内容)。Kotlin 提供了关键字 this 来引用当前的对象实例。在 addItem()addAll() 方法中,通过返回 this 即可返回当前的 Order
fun addItem(newItem: Item): Order {
    itemList.add(newItem)
    return this
}

fun addAll(newItems: List<Item>): Order {
    itemList.addAll(newItems)
    return this
}
  1. 现在,您可以在 main() 函数中将调用链接起来,如以下代码所示。此代码利用构建器模式来创建新的 Order
val order4 = Order(4).addItem(Noodles()).addItem(Vegetables("Cabbage", "Onion"))
ordersList.add(order4)

Order(4) 会返回一个 Order 实例,然后您可以对其调用 addItem(Noodles())addItem() 方法会返回同一 Order 实例(具有新状态),您可以使用 vegetables 再次对该实例调用 addItem()。返回的 Order 结果可以存储在 order4 变量中。

现有用于创建 Orders 的代码仍然可行,因此可以保持不变。尽管并不强制要求将这些调用链接起来,但利用函数的返回值是一种常见且推荐的做法。

  1. 此时,您其实根本不需要将订单存储在变量中。在 main() 函数中(在用于输出订单的最后一个循环之前),直接创建一个 Order 并将其添加到 orderList。如果使每个方法调用各占一行,代码也会更易于阅读。
ordersList.add(
    Order(5)
        .addItem(Noodles())
        .addItem(Noodles())
        .addItem(Vegetables("Spinach")))
  1. 运行您的代码,预期输出如下:
Order #1
Noodles: $10
Total: $10

Order #2
Noodles: $10
Vegetables Chef's Choice: $5
Total: $15

Order #3
Noodles: $10
Vegetables Carrots, Beans, Celery: $5
Total: $15

Order #4
Noodles: $10
Vegetables Cabbage, Onion: $5
Total: $15

Order #5
Noodles: $10
Noodles: $10
Vegetables Spinach: $5
Total: $25

恭喜您完成此 Codelab!

现在,您已经了解到将数据存储在列表中、使列表可变以及循环遍历列表是多么有用。在下一个 Codelab 中,您将在 Android 应用中运用这些知识实现在屏幕上显示数据列表!

7. 解决方案代码

以下是 ItemNoodlesVegetablesOrder 类的解决方案代码。main() 函数还展示了如何使用这些类。有多种方法可以实现此程序,因此您的代码有可能略有不同。

open class Item(val name: String, val price: Int)

class Noodles : Item("Noodles", 10) {
    override fun toString(): String {
        return name
    }
}

class Vegetables(vararg val toppings: String) : Item("Vegetables", 5) {
    override fun toString(): String {
        if (toppings.isEmpty()) {
            return "$name Chef's Choice"
        } else {
            return name + " " + toppings.joinToString()
        }
    }
}

class Order(val orderNumber: Int) {
    private val itemList = mutableListOf<Item>()

    fun addItem(newItem: Item): Order {
        itemList.add(newItem)
        return this
    }

    fun addAll(newItems: List<Item>): Order {
        itemList.addAll(newItems)
        return this
    }

    fun print() {
        println("Order #${orderNumber}")
        var total = 0
        for (item in itemList) {
            println("${item}: $${item.price}")
            total += item.price
        }
        println("Total: $${total}")
    }
}

fun main() {
    val ordersList = mutableListOf<Order>()

    // Add an item to an order
    val order1 = Order(1)
    order1.addItem(Noodles())
    ordersList.add(order1)

    // Add multiple items individually
    val order2 = Order(2)
    order2.addItem(Noodles())
    order2.addItem(Vegetables())
    ordersList.add(order2)

    // Add a list of items at one time
    val order3 = Order(3)
    val items = listOf(Noodles(), Vegetables("Carrots", "Beans", "Celery"))
    order3.addAll(items)
    ordersList.add(order3)

    // Use builder pattern
    val order4 = Order(4)
        .addItem(Noodles())
        .addItem(Vegetables("Cabbage", "Onion"))
    ordersList.add(order4)

    // Create and add order directly
    ordersList.add(
        Order(5)
            .addItem(Noodles())
            .addItem(Noodles())
            .addItem(Vegetables("Spinach"))
    )

    // Print out each order
    for (order in ordersList) {
        order.print()
        println()
    }
}

8. 总结

Kotlin 提供了相关功能来帮助您通过 Kotlin 标准库更轻松地管理和操控数据集合。集合就是指具有相同数据类型的多个对象。Kotlin 中有几种不同的基本集合类型:列表、集和映射。本 Codelab 主要介绍了列表,在之后的 Codelab 中,您还将详细了解集和映射。

  • 列表是特定类型元素的有序集合(例如 Strings. 列表)。
  • 索引是反映元素位置的整数位置(例如 myList[2])。
  • 在列表中,首个元素位于索引 0 处(例如 myList[0]),最后一个元素位于索引 myList.size-1 处(例如 myList[myList.size-1]myList.last())。
  • 有两种类型的列表:ListMutableList.
  • List 是只读的,一但初始化就无法再修改。不过,您可以应用 sorted()reversed() 等操作,这些操作可在不更改原始列表的情况下返回新的列表。
  • MutableList 在创建后可以修改,例如添加、移除或修改元素。
  • 您可以使用 addAll() 向可变列表添加一个项列表。
  • 使用 while 循环可反复执行代码块,直到表达式的结果为 false 并退出循环。

while (expression) {

// While the expression is true, execute this code block

}

  • 使用 for 循环可遍历列表中的所有项:

for (item in myList) {

// Execute this code block for each element of the list

}

  • 利用 vararg 修饰符可向函数或构造函数传递可变数量的参数。

9. 了解详情