练习 55:记录数组
记录很棒,数组更好,但是当你把记录放入数组时,这个生活中几乎没有你不能编码的东西。
1 class Student 2 { 3 String name; 4 int credits; 5 double gpa; 6 } 7 8 public class StudentDatabase 9 { 10 public static void main( String[] args ) 11 { 12 Student[] db; 13 db = new Student[3]; 14 15 db[0] = new Student(); 16 db[0].name = "Esteban"; 17 db[0].credits = 43; 18 db[0].gpa = 2.9; 19 20 db[1] = new Student(); 21 db[1].name = "Dave"; 22 db[1].credits = 15; 23 db[1].gpa = 4.0; 24 25 db[2] = new Student(); 26 db[2].name = "Michelle"; 27 db[2].credits = 132; 28 db[2].gpa = 3.72; 29 30 for ( int i=0; i<db.length; i++ ) 31 { 32 System.out.println("Name: " + db[i].name); 33 System.out.println("\tCredit hours: " + db[i].credits); 34 System.out.println("\tGPA: " + db[i].gpa + "\n"); 35 } 36 37 int max = 0; 38 for ( int i=1; i<db.length; i++ ) 39 if ( db[i].gpa > db[max].gpa ) 40 max = i; 41 42 System.out.println(db[max].name + " has the highest GPA."); 43 } 44 }
你应该看到什么
Name: Esteban Credit hours: 43 GPA: 2.9 Name: Dave Credit hours: 15 GPA: 4.0 Name: Michelle Credit hours: 132 GPA: 3.72 Dave has the highest GPA.
当你看到变量定义中某物的右侧有方括号时,那就是“某物的数组”。实际上,由于这本书快要结束了,也许我应该解释一下public static
void main
的业务。至少部分地。
public static void main( String[] args )
这一行声明了一个名为 main 的函数。该函数需要一个参数:名为 args 的字符串数组(缩写为“arguments”)。该函数不返回任何值;它是void
。
无论如何。
第 12 行声明了db作为一个可以容纳“学生数组”的变量。还没有数组,只是一个可能容纳数组的变量。就像我们说…
int n;
…还没有整数。变量n可能容纳一个整数,但它里面还没有数字。n被声明但未定义。同样,一旦第 12 行执行完毕,db是一个可能指向学生数组的变量,但仍未定义。
幸运的是,我们不必等太久;第 13 行通过创建一个实际的具有三个槽的学生数组来初始化 db。此时,db 被定义,db.length
为3
,db 有三个合法索引:0
,1
和2
。
好吧,在这一点上,db是一个学生记录的数组。除了它不是。db是一个学生变量的数组,每个变量都可能容纳一个学生记录,但没有一个变量是这样的。数组中的所有三个槽都未定义。
(从技术上讲,它们包含值null
,这是 Java 中引用变量在其中没有对象时具有的特殊值。)
因此,在第 15 行,重要的是创建一个学生对象并将其存储到数组的第一个槽(索引0
)中。然后在第 16 行,我们可以将一个值存储到数组 db 中索引0
的学生记录的名字字段中。
让我们从外到内追踪它:
表达式 | 类型 | 描述 |
db |
students[] |
一组学生记录 |
db[0] |
students |
一个单独的学生记录(第一个) |
db[0].name |
String |
数组中第一个学生的name字段 |
db.name |
错误 | 整个数组没有一个名字字段 |
因此,第 16 行将一个值存储到数组中第一个记录的name字段中。第 17 和 18 行将值存储到该记录中的其余字段中。第 20 到 28 行创建并填充数组中的其他两个记录
尽管在第 30 到 34 行,我们使用循环在屏幕上显示所有的值。
然后,第 37 到 42 行找到了 GPA 最高的学生。这值得更详细解释。在第 37 行,定义了一个名为 max 的int
。但 max 不会保存最高 GPA 的值;它只会保存它的索引。
所以当我把0
放入 max 时,我的意思是“在代码的这一点上,就我所知,最高分的学生
在槽0
中。”这可能不是真的,但由于我们还没有查看数据库中的任何值,这是一个很好的起点。
然后在第 38 行,我们设置循环来查看数组的每个槽。然而,请注意,循环从索引1
(第二个槽)开始。为什么?
因为 max 已经是0
。所以如果 i 也从0
开始,那么if
语句将进行以下比较:
if ( db[0].gpa > db[0].gpa )
…这是浪费。因此,通过从1
开始,第一次循环时,if
语句将进行以下比较:
if ( db[1].gpa > db[0].gpa )
“如果戴夫的 GPA 大于埃斯特万的 GPA,则将 max 从0
更改为 i(1
)的当前值。”
因此,当循环结束时,max包含具有最高 GPA 的记录的索引。这正是我们在第 42 行显示的内容。
学习演练
- 将数组的容量更改为
4
而不是 3。不改变任何其他内容,编译并运行程序。你明白为什么程序会崩溃吗? - 现在添加一些代码,将值放入新学生的字段中。给这个新学生一个比“Dave”更高的 GPA,并确认代码正确地将他们标记为具有最高的 GPA。
- 更改代码,使其查找具有最少学分的人,而不是具有最高 GPA 的人。
练习 56:从文件中读取记录的数组(温度重访)
这个练习从互联网上的一个文件中填充了一个记录数组。到目前为止,您应该知道您是否需要下载此文件的副本,还是您的计算机可以直接从互联网上打开它。
与本书中迄今为止使用的所有其他文件不同,这个数据文件正是我从戴顿大学的平均日温度档案中下载的。这意味着三件事:
- 文件的第一行没有数字告诉我们有多少记录。
- 除了温度之外,每个记录还包括样本的月份、日期和年份。
- 文件中有错误数据。特别是,“当数据不可用时,我们使用‘-99’作为无数据标志。”
因此,有些天的温度是-99。我们将不得不在代码中处理这个问题。
1 import java.util.Scanner; 2 3 class TemperatureSample 4 { 5 int month, day, year; 6 double temperature; 7 } 8 9 public class TemperaturesByDate 10 { 11 public static void main(String[] args) throws Exception 12 { 13 String url = "http://learnjavathehardway.org/txt/avgdailytempswithdatesatx.txt"; 14 Scanner inFile = new Scanner((new java.net.URL(url)).openStream()); 15 16 TemperatureSample[] tempDB = new TemperatureSample[10000]; 17 int numRecords, i = 0; 18 19 while ( inFile.hasNextInt() && i < tempDB.length ) 20 { 21 TemperatureSample e = new TemperatureSample(); 22 e.month = inFile.nextInt(); 23 e.day = inFile.nextInt(); 24 e.year = inFile.nextInt(); 25 e.temperature = inFile.nextDouble(); 26 if ( e.temperature == 99 ) 27 continue; 28 tempDB[i] = e; 29 i++; 30 } 31 inFile.close(); 32 numRecords = i; 33 34 System.out.println(numRecords + " daily temperatures loaded."); 35 36 double total = 0, avg; 37 int count = 0; 38 for ( i=0; i<numRecords; i++ ) 39 { 40 if ( tempDB[i].month == 11 ) 41 { 42 total += tempDB[i].temperature; 43 count++; 44 } 45 } 46 47 avg = total / count; 48 avg = roundToOneDecimal(avg); 49 System.out.println("Average daily temperature over " + count + " days in November: " + avg); 50 } 51 52 public static double roundToOneDecimal( double d ) 53 { 54 return Math.round(d*10)/10.0; 55 } 56 }
你应该看到什么
6717 daily temperatures loaded. Average daily temperature over 540 days in November: 59.7
第 3 到 7 行声明了我们的记录,它将存储单个平均日温度值(一个
double
),还有月份、日期和年份的字段。
第 16 行定义了一个记录数组。但是我们有一个问题。我们无法在不提供容量的情况下定义数组,而在看到文件中有多少记录之前,我们不知道需要多大的容量。这个问题有三种可能的解决方案:
- 不要使用数组。使用其他东西,比如一个可以在添加条目时自动增长的数组。这实际上可能是正确的解决方案,但是“其他东西”超出了本书的范围。
- 读取文件两次。首先只计算记录的数量,然后使用完美大小创建数组。然后再次读取文件将所有值读入数组。这样做很慢,但有效。
- 不要担心使数组的大小合适。只需使其“足够大”。然后在读取它们时计算实际拥有的记录数量,并在任何循环中使用该计数,而不是数组的容量。这并不完美,但它有效且简单。编写软件有时需要妥协,这就是其中之一。
因此,第 16 行声明了数组并定义为有一万个槽位:“足够大”。
在第 19 行,我们开始一个循环,读取文件中的所有值。我们使用索引变量i来跟踪数组中下一个需要填充的槽位。因此,只要文件中还有更多整数,并且我们的数组容量还没有用完,我们的循环就会继续。
仅仅因为我们通过使数组“足够大”来节省了一些步骤,并不意味着我们会对此感到愚蠢。如果文件最终比我们的数组容量大,我们希望尽早停止读取文件,而不是因为 ArrayIndexOutOfBounds 异常而使程序崩溃。
21 行定义了一个名为e的 TemperatureSample 记录。22 到 25 行将文件中的下几个值加载到该记录的适当字段中。
但是!请记住,我们的文件中有“缺失”的值。有些天的温度读数是
-99
,所以我们在第 26 行放置了一个if
语句来检测它,然后将它们放入我们的数据库中。
然后在第 27 行有一些新东西:Java 关键字continue
。continue
只能在循环体内合法。它的意思是“跳过循环体中剩余的代码行,然后返回顶部进行下一次迭代。”
这实际上丢弃了当前(无效)记录,因为它跳过了第 28 和 29 行,这两行将当前记录存储在数组中的下一个可用槽位中,然后增加索引。
有些人不喜欢使用continue
,他们会这样写:
if ( e.temperature != 99 ) { tempDB[i] = e; i++; }
这也完全没问题。只有当温度不是-99
时,才将此条目放入数组中。我更喜欢使用continue
,因为这样的代码对我来说更清晰,但是理智的人可能会有不同意见。选择对你来说最有意义的方式。
一旦在第 31 行完成循环,我们确保关闭文件,然后将最终索引存储到 numRecords 中,以便我们可以在任何循环中使用它,而不是tempDB.length
。毕竟,我们使数组比我们需要的大,最后的 3283 个槽(在这个例子中)是空的。仅循环到 numRecords 会更有效一些,我们可以通过这种方式避免检查任何无效的记录。
在第 34 行,我们在屏幕上显示记录的数量,这可以帮助您查看是否在读取时出现了任何问题。
第 36 至 45 行循环遍历所有我们的记录。任何月份字段为11
(11 月)的记录都会被添加到一个运行总数中,我们也在此过程中计算匹配记录的总数。
然后,当循环结束时,我们可以通过将总和除以计数来获得数据库中所有 11 月份每日温度的平均值。
现在,我的程序的第一个版本的整体平均温度是59.662962962963
。这不仅看起来不好,而且不正确:所有输入温度只精确到十分之一度。因此,显示具有十几个有效数字的结果看起来比实际更准确。
因此,在第 52 至 55 行,您将找到一个小小的函数,用于将数字四舍五入到小数点后一位。据我所知,Java 没有内置的此功能,但它确实有一个内置的将数字四舍五入到最接近的整数的函数:Math.round()
。所以我将数字乘以十,四舍五入,然后再除以十。也许有更好的方法,但我喜欢这样做。
第 48 行将平均温度作为参数传递给我的函数,然后取舍返回值并将其存储为avg的新值。
学习演练
- 访问戴顿大学的温度档案,并下载一个附近城市的温度数据文件!让你的代码从该文件中读取数据。
- 更改代码以查找其他内容,比如二月份的最高温度或其他你感兴趣的内容。
- 尝试在屏幕上打印整个 TemperatureSample 记录。类似于这样:
TemperatureSample ts = tempDB[0]; System.out.println( ts );
请注意,它不会打印像 ts.year 这样的整数或像ts.temperature
这样的双精度;它试图在屏幕上显示整个记录。编译并运行文件。屏幕上显示了什么?
尝试更改索引以从数组中提取不同的值,并查看它如何改变打印出来的内容。
笨办法学 Java(四)(2)https://developer.aliyun.com/article/1481923