我们正在取得良好的进展。我们对安卓用户界面选项和 Kotlin 的基础知识都有全面的了解。在前面的几章中,我们开始将这两个领域结合在一起,并使用 Kotlin 代码操纵用户界面,包括一些新的小部件。然而,在构建“自我笔记”应用时,我们在知识中发现了一些空白。在本章中,我们将填写这些空白中的第一个,然后,在下一章中,我们将使用这些新信息来推进应用。我们目前没有办法管理大量的相关数据。除了声明、初始化和管理几十个、几百个甚至几千个属性或实例之外,我们将如何让应用的用户拥有不止一个注释?我们还将快速转向学习随机数。
我们将在本章中讨论以下主题:
- 随机数
- 数组
- 一个简单的数组小应用
- 动态数组小应用
- 范围
- 数组列表
- hashmap
首先,我们来了解一下Random
课。
有时,我们会希望在我们的应用中有一个随机数,在这些情况下,Kotlin 为我们提供了类。这门课有很多可能的用途,比如我们的应用想要展示一个随机的每日提示,或者一个必须在场景之间进行选择的游戏,或者一个提出随机问题的测验。
Random
类是安卓应用编程接口的一部分,在我们的安卓应用中完全兼容。
让我们来看看如何创建随机数。所有的辛苦都是Random
班给我们做的。首先,我们需要创建一个Random
对象,如下所示:
val randGenerator = Random()
然后,我们使用新对象的nextInt
函数生成一个在一定范围内的随机数。下面一行代码使用我们的randGenerator
对象生成随机数,并将结果存储在ourRandomNumber
变量中:
var ourRandomNumber = randGenerator.nextInt(10)
我们输入的数字范围从零开始。因此,前一行将生成一个介于 0 和 9 之间的随机数。如果我们想要一个介于 1 和 10 之间的随机数,我们只需在同一行代码的末尾添加 increment 运算符:
ourRandomNumber ++
我们也可以使用Random
对象,使用nextLong
、nextFloat
和nextDouble
获取其他类型的随机数。
你可能想知道当我们有一个有很多变量要跟踪的应用时会发生什么。我们的 Note to self 应用有 100 个笔记,或者在一个游戏中有 100 个最高分的高分表呢?我们可以如下声明和初始化 100 个独立的变量:
var note1 = Note()
var note2 = Note()
var note3 = Note()
// 96 more lines like the above
var note100 = Note()
或者,通过使用高分示例,我们可以使用如下代码:
var topScore1: Int
var topScore2: Int
// 96 more lines like the above
var topScore100: Int
很快,这段代码看起来很笨拙,但是当有人获得新的最高分时,或者如果我们想让我们的用户对他们笔记的显示顺序进行排序,该怎么办?使用高分方案,我们必须将每个变量的分数下移一位。这是噩梦的开始,如下代码所示:
topScore100 = topScore99;
topScore99 = topScore98;
topScore98 = topScore97;
// 96 more lines like the above
topScore1 = score;
一定有更好的方法。当我们有一个完整的变量数组时,我们需要的是一个 Kotlin数组。数组是一个对象,它包含预定的、固定的最大数量的元素。每个元素都是一个类型一致的变量。
下面的代码声明了一个可以保存Int
类型变量的数组;例如高分表或一系列考试成绩:
var myIntArray: IntArray
我们还可以声明其他类型的数组,如下所示:
var myFloatArray: FloatArray
var myBooleanArray: BooleanArray
在使用之前,这些阵列中的每一个都需要有固定的最大分配存储空间量。就像我们对其他对象所做的一样,我们必须在使用数组之前初始化它们,我们可以这样做:
myIntArray = IntArray(100)
myFloatArray = FloatArray(100)
myBooleanArray = BooleanArray(100)
前面的代码最多分配适当类型的100
个存储空间。想象一下,我们的可变仓库中有一条由 100 个连续存储空间组成的长通道。空格可能会被标记为myIntArray[0]
、myIntArray[1]
和myIntArray[2]
,每个空格代表一个单独的Int
值。这里有点令人惊讶的是,存储空间从零开始,而不是 1。因此,在 100- 宽的阵列中,存储空间将从 0 到 99。
我们可以如下初始化一些存储空间:
myIntArray [0] = 5
myIntArray [1] = 6
myIntArray [2] = 7
但是,请注意,我们只能将预声明的类型放入数组中,并且数组保存的类型永远不会改变,如以下代码所示:
myIntArray [3] = "John Carmack"
// Won't compile String not Int
那么,当我们有一个Int
类型的数组时,这些Int
变量分别被称为什么,我们如何访问存储在其中的值?数组符号语法替换了变量的名称。此外,我们可以用数组中的一个变量做任何事情,就像我们可以用一个有名字的常规变量做任何事情一样;如下所示:
myIntArray [3] = 123
前面的代码将值 123 赋给数组中的第 4 个位置。
下面是另一个使用像普通变量一样的数组的例子:
myIntArray [10] = myIntArray [9] - myIntArray [4]
前面的代码从存储在数组第 10 个位置的值中减去存储在数组第 5 个位置的值,并将答案赋给数组第 11 个位置。
我们还可以将数组中的值赋给相同类型的常规变量,如下所示:
Val myNamedInt = myIntArray[3]
但是请注意,myNamedInt
是一个独立的变量,对它的任何更改都不会影响存储在IntArray
引用中的值。它在仓库中有自己的空间,否则与数组无关。
在前面的例子中,我们没有检查任何字符串或对象。事实上,字符串是对象,当我们想要制作对象数组时,我们对它们的处理略有不同;看看下面的代码:
var someStrings = Array<String>(5) { "" }
// You can remove the String keyword because it can be inferred like
// this
var someMoreStrings = Array(5) { "" }
someStrings[0]= "Hello "
someStrings[1]= "from "
someStrings[2]= "inside "
someStrings[3]= "the "
someStrings[4]= "array "
someStrings[5]= "Oh dear "
// ArrayIndexOutOfBoundsException
前面的代码声明了一个最多可以容纳五个对象的字符串对象数组。请记住,数组从 0 开始,因此有效位置包括 0 到 4。如果您试图使用无效位置,您将得到一个ArrayIndexOutOfBoundsException错误。如果编译器注意到错误,那么代码就不会编译;但是,如果编译器无法发现错误,并且错误是在应用执行时发生的,那么应用将崩溃。
我们避免这个问题的唯一方法是知道规则——数组从 0 开始,直到长度减 1。所以someArray[9]
是数组中的第十个位置。我们也可以使用清晰可读的代码,这样可以很容易地评估我们所做的工作,并更容易地发现问题。
您也可以在声明数组的同时初始化数组的内容,如以下代码所示:
var evenMoreStrings: Array<String> =
arrayOf("Houston", "we", "have", "an", "array")
前面的代码使用arrayOf
内置的 Kotlin 函数来初始化数组。
在 Kotlin 中,声明和初始化数组的方式非常灵活。我们还没有接近涵盖我们可以使用数组的所有方式,即使在本书的结尾,我们也不会涵盖所有的方式。不过,让我们再深入一点。
将一个数组变量视为一组给定类型变量的地址。例如,用仓库类比,someArray
可以是过道号。所以,someArray[0]
和someArray[1]
是过道号,后面是过道中的位置号。
因为数组也是对象,它们有我们可以使用的函数和属性,如下例所示:
val howBig = someArray.size
在前面的例子中,我们将someArray
的长度(即大小)分配给了名为howBig
的Int
变量。
我们甚至可以声明数组的数组。这是一个阵列,其中另一个阵列潜伏在其每个位置;这如下所示:
val cities = arrayOf("London", "New York", "Yaren")
val countries = arrayOf("UK", "USA", "Nauru")
val countriesAndCities = arrayOf(countries, cities)
Log.d("The capital of " +
countriesAndCities[0][0],
" is " +
countriesAndCities[1][0])
在Log
代码之前的将向日志窗口输出以下文本:
The capital of UK: is London
让我们在一个真实的应用中使用一些数组来尝试并了解如何在真实代码中使用它们以及它们可能用于什么。
让我们做一个简单的工作数组例子。您可以在可下载的代码包中获得这个项目的完整代码。可以在Chapter15/Simple Array Example/MainActivity.kt
文件中找到。
使用空活动项目模板创建一个项目,并将其称为Simple Array Example
。
首先,我们声明我们的数组,分配五个空格,并初始化每个元素的值。然后,我们将每个值输出到 logcat 窗口。
这与我们之前看到的例子略有不同,因为我们在声明数组本身的同时声明了大小。
在调用setContentView
之后,在onCreate
函数中添加以下代码:
// Declaring an array
// Allocate memory for a maximum size of 5 elements
val ourArray = IntArray(5)
// Initialize ourArray with values
// The values are arbitrary, but they must be Int
// The indexes are not arbitrary. Use 0 through 4 or crash!
ourArray[0] = 25
ourArray[1] = 50
ourArray[2] = 125
ourArray[3] = 68
ourArray[4] = 47
//Output all the stored values
Log.i("info", "Here is ourArray:")
Log.i("info", "[0] = " + ourArray[0])
Log.i("info", "[1] = " + ourArray[1])
Log.i("info", "[2] = " + ourArray[2])
Log.i("info", "[3] = " + ourArray[3])
Log.i("info", "[4] = " + ourArray[4])
接下来,我们将数组的每个元素加在一起,就像我们对普通的Int
类型变量所做的那样。请注意,当我们将数组元素加在一起时,为了清晰起见,我们是在多条线上进行的。将我们刚才讨论的代码添加到MainActivity.kt
中,如下所示:
/*
We can do any calculation with an array element
provided it is appropriate to the contained type
Like this:
*/
val answer = ourArray[0] +
ourArray[1] +
ourArray[2] +
ourArray[3] +
ourArray[4]
Log.i("info", "Answer = $answer")
运行该示例,并在 logcat 窗口中记录输出。请记住,模拟器显示屏上不会发生任何事情,因为所有输出都将发送到 Android Studio 中的 logcat 窗口;以下是输出:
info﹕ Here is ourArray:
info﹕ [0] = 25
info﹕ [1] = 50
info﹕ [2] = 125
info﹕ [3] = 68
info﹕ [4] = 47
info﹕ Answer = 315
我们声明一个名为ourArray
的数组来保存Int
值,然后为该类型的最多五个值分配空间。
接下来,我们为ourArray
中的五个空格分别赋值。记住第一个空格是ourArray[0]
,最后一个空格是ourArray[4]
。
接下来,我们简单地将每个数组位置中的值打印到 logcat 窗口,从输出中,我们可以看到它们保存着我们在上一步中初始化它们的值。然后,我们将ourArray
中的每个元素加在一起,并将它们的值初始化为answer
变量。然后我们将answer
打印到 logcat 窗口,我们可以看到,所有的值都被加在一起,就好像它们是普通的Int
类型(它们是),只是以不同的方式存储。
正如我们在本节开始时所讨论的,如果我们需要单独声明和初始化数组的每个元素,使用数组而不是常规变量并没有太大的好处。让我们看一个动态声明和初始化数组的例子。
您可以在下载包中获取该示例的工作项目。可以在Chapter15/Dynamic Array Example/MainActivity.kt
文件中找到。
用空活动模板创建一个项目,并将其称为Dynamic Array Example
。
在调用onCreate
函数中的setContentView
后,键入以下代码。在我们讨论和分析代码之前,看看您是否能计算出输出结果:
// Declaring and allocating in one step
val ourArray = IntArray(1000)
// Let's initialize ourArray using a for loop
// Because more than a few variables is allot of typing!
for (i in 0..999) {
// Put the value into ourArray
// At the position decided by i.
ourArray[i] = i * 5
//Output what is going on
Log.i("info", "i = $i")
Log.i("info", "ourArray[i] = ${ ourArray[i]}")
}
运行示例应用。请记住,屏幕上不会发生任何事情,因为所有输出都将发送到我们在 AndroidStudio 的 logcat 窗口;以下是输出:
info﹕ i = 0
info﹕ ourArray[i] = 0
info﹕ i = 1
info﹕ ourArray[i] = 5
info﹕ i = 2
info﹕ ourArray[i] = 10
为了简洁起见,已经删除了循环的 994 次迭代:
info﹕ ourArray[i] = 4985
info﹕ i = 998
info﹕ ourArray[i] = 4990
info﹕ i = 999
info﹕ ourArray[i] = 4995
首先,我们声明并分配了一个名为ourArray
的数组来保存多达 1000 个Int
值。请注意,这次,我们在一行代码中执行了两个步骤:
val ourArray = IntArray(1000)
然后,我们使用了一个for
循环,该循环被设置为循环 1000 次:
for (i in 0..999) {
我们初始化了数组中的空格,从 0 到 999,值i
乘以 5,如下所示:
ourArray[i] = i * 5
然后,为了演示i
的值和数组中每个位置保存的值,我们输出i
的值,后面是数组中相应位置保存的值,如下所示:
//Output what is going on
Log.i("info", "i = $i")
Log.i("info", "ourArray[i] = ${ ourArray[i]}")
这一切发生了 1000 次,产生了我们看到的输出。当然,我们还没有在现实世界的应用中使用这种技术,但是我们很快就会使用它来让我们的笔记应用保存几乎无限多的笔记。
一个ArrayList
物体就像一个正常的阵列,但是在类固醇上。它克服了阵列的一些不足,例如必须预先确定其大小。它增加了几个有用的功能,使其数据易于管理,并被安卓应用编程接口中的许多类使用。最后这一点意味着如果我们想使用 API 的某些部分,我们需要使用ArrayList
。在第十六章、适配器和回收机中,我们将把ArrayList
真正投入工作。首先是理论。
我们来看一些使用ArrayList
的代码:
// Declare a new ArrayList called myList
// to hold Int variables
val myList: ArrayList<Int>
// Initialize myList ready for use
myList = ArrayList()
在前面的代码中,我们声明并初始化了一个名为myList
的新ArrayList
对象。我们也可以一步完成,如下面的代码所示:
val myList: ArrayList<Int> = ArrayList()
到目前为止,这并不是特别有趣,所以让我们看看我们实际上可以用ArrayList
做什么。这次我们用一个String ArrayList
对象:
// declare and initialize a new ArrayList
val myList = ArrayList<String>()
// Add a new String to myList in
// the next available location
myList.add("Donald Knuth")
// And another
myList.add("Rasmus Lerdorf")
// We can also choose 'where' to add an entry
myList.add(1,"Richard Stallman")
// Is there anything in our ArrayList?
if (myList.isEmpty()) {
// Nothing to see here
} else {
// Do something with the data
}
// How many items in our ArrayList?
val numItems = myList.size
// Now where did I put Richard?
val position = myList.indexOf("Richard Stallman")
在之前的代码中,我们看到可以在我们的ArrayList
对象上使用一些有用的ArrayList
类的函数;这些功能如下:
- 我们可以添加一个项目(
myList.add
) - 我们可以在特定位置(
myList.add(x, value)
)添加条目 - 我们可以检查
ArrayList
实例是否为空(myList.isEmpty ()
) - 我们可以看到
ArrayList
实例有多大(myList.size
) - 我们可以得到给定项目的当前位置(
myList.indexOf...
)
ArrayList
类的功能甚至更多,但我们目前看到的已经足够完成这本书了。
有了所有这些功能,我们现在只需要一种动态处理ArrayList
实例的方法。这就是增强的for
循环的情况:
for (String s : myList)
前面的示例将一次迭代(遍历)myList
中的所有项目。在每一步,s
将保存当前的String
条目。
因此,这段代码将把上一节ArrayList
代码示例中的所有优秀程序员打印到 logcat 窗口中,如下所示:
for (s in myList) {
Log.i("Programmer: ", "$s")
}
其工作方式是for
循环遍历ArrayList
中的每个String
,并将当前String
条目分配给s
。然后,依次对每一个,在Log…
函数调用中使用s
。前一个循环将为 logcat 窗口创建以下输出:
Programmer:: Donald Knuth
Programmer:: Richard Stallman
Programmer:: Rasmus Lerdorf
for
循环已经输出了所有的名称。理查德·斯托尔曼在唐纳德·克努特和拉斯穆斯·勒多夫之间的原因是因为我们将他插入了一个特定的位置(1),这是ArrayList
中的第二个位置。insert
函数调用不会删除任何现有条目,而是改变它们的位置。
有新消息传来!
我们已经知道我们可以把对象放入数组和ArrayList
对象中。然而,成为多态意味着它们可以处理多个不同类型的对象,只要它们有一个共同的父类型——所有这些都在同一个数组或ArrayList
中。
在第 10 章,面向对象编程,中,我们了解到多态性意味着许多形式。但是在数组和ArrayList
的上下文中,这对我们意味着什么呢?
在其最简单的形式中,它意味着任何子类都可以用作使用超类的代码的部分。
例如,如果我们有一个Animals
数组,我们可以将属于Animal
子类的任何对象放在Animals
数组中,例如Cat
和Dog
。
这意味着我们可以编写更简单、更容易理解、更容易更改的代码:
// This code assumes we have an Animal class
// And we have a Cat and Dog class that
// inherits from Animal
val myAnimal = Animal()
val myDog = Dog()
val myCat = Cat()
val myAnimals = arrayOfNulls<Animal>(10)
myAnimals[0] = myAnimal // As expected
myAnimals[1] = myDog // This is OK too
myAnimals[2] = myCat // And this is fine as well
此外,我们可以为超类编写代码,并依赖于这样一个事实,即无论它被子类化多少次,在某些参数内,代码仍然会工作。让我们继续前面的例子,如下所示:
// 6 months later we need elephants
// with its own unique aspects
// If it extends Animal we can still do this
val myElephant = Elephant()
myAnimals[3] = myElephant // And this is fine as well
我们刚才讨论的一切对ArrayLists
也是正确的。
KotlinHashMap
s 有意思;他们是ArrayList
的表亲。它们封装了有用的数据存储技术,否则这些技术对于我们自己成功编码来说是非常技术性的。回到“给自己的笔记”应用之前,值得看看HashMap
。
假设我们想要存储角色扮演游戏中许多角色的数据,并且每个不同的角色都由一个Character
类型的对象来表示。
我们可以使用一些我们已经知道的 Kotlin 工具,比如数组或 T0。然而,通过HashMap
,我们可以给每个Character
对象一个唯一的密钥或标识符,并使用相同的密钥或标识符访问任何这样的对象。
术语“散列”来自将我们选择的密钥或标识符转换成HashMap
类内部使用的东西的过程。这个过程叫做散列。
然后可以使用我们选择的密钥或标识符访问我们的任何Character
实例。在Character
类场景中,关键字或标识符的一个很好的候选项是角色的名字。
每个键或标识符都有对应的对象;在这种情况下,Character
实例。这就是所谓的键值对。
我们简单的给HashMap
一个键,它就给了我们对应的对象。不需要担心我们存储角色的索引,比如杰洛特、希里或崔西;只需将名称传递给HashMap
,它就会为我们完成工作。
让我们看一些例子。您不需要键入这些代码;简单地熟悉它是如何工作的。
我们可以声明一个新的HashMap
实例来保存键和Character
实例,如下所示:
val characterMap: Map<String, Character>
前面的代码假设我们已经编码了一个名为Character
的类。然后我们可以如下初始化HashMap
实例:
characterMap = HashMap()
然后,我们可以添加一个新的密钥及其关联的对象,如下所示:
characterMap.put("Geralt", Character())
characterMap.put("Ciri", Character())
characterMap.put("Triss", Character())
所有示例代码都假设我们可以以某种方式赋予Character
实例它们的唯一属性,以反映它们在其他地方的内部差异。
然后我们可以从HashMap
实例中检索一个条目,如下所示:
val ciri = characterMap.get("Ciri")
或者,我们可以直接使用Character
类的函数:
characterMap.get("Geralt").drawSilverSword()
// Or maybe call some other hypothetical function
characterMap.get("Triss").openFastTravelPortal("Kaer Morhen")
前面的代码在存储在HashMap
实例中的Character
类实例上调用假设的drawSilverSword
和openFastTravelPortal
函数。
有了这个新的数组工具包,ArrayList
、HashMap
,以及它们是多态的这一事实,我们可以继续学习更多的安卓类,我们将很快使用它们来增强我们的笔记到自我应用。
尽管我们已经了解了所有情况,但我们还没有准备好将解决方案应用到笔记型电脑应用中。我们可以更新我们的代码,在一个ArrayList
中存储大量Note
实例,但是在此之前,我们还需要一种方法在用户界面中显示我们的ArrayList
的内容。把整件事放在TextView
的例子里看起来不太好。
解决方案是适配器,以及一个名为RecyclerView
的特殊 UI 布局。我们将在下一章讨论它们。
问:一台只能进行真实计算的计算机怎么可能产生真正的随机数?
a)在现实中,计算机无法创建真正随机的数字,但是Random
类使用种子来生成一个在严密的统计审查下真正随机的数字。想了解更多关于种子和生成随机数的信息,请看下面这篇文章:https://en.wikipedia.org/wiki/Random_number_generation。
在这一章中,我们研究了如何使用简单的 Kotlin 数组来存储大量的数据,前提是它是相同类型的。我们还使用了ArrayList
,它就像一个有很多额外特性的数组。此外,我们发现数组和ArrayList
都是多态的,这意味着一个数组(或ArrayList
)可以容纳多个不同的对象,只要它们都来自同一个父类。
我们还了解了HashMap
类,它也是一个数据存储解决方案,但是允许以不同的方式访问。
在下一章中,我们将学习Adapter
和RecyclerView
来将理论付诸实践,并增强我们的 Note to self app。