笨办法学 Java(三)(2)https://developer.aliyun.com/article/1481915
练习 46:嵌套 for 循环
在编程中,“嵌套”一词通常意味着将某物放在同一物体内。“嵌套循环”将是两个循环,一个在另一个内部。如果你做对了,那么内部循环将在外部循环执行一次迭代时重复所有迭代。
1 public class NestingLoops 2 { 3 public static void main( String[] args ) 4 { 5 // this is #1 I'll call it "CN" 6 for ( char c='A'; c <= 'E'; c++ ) 7 { 8 for ( int n=1; n <= 3; n++ ) 9 { 10 System.out.println( c + " " + n ); 11 } 12 } 13 14 System.out.println("\n"); 15 16 // this is #2 I'll call it "AB" 17 for ( int a=1; a <= 3; a++ ) 18 { 19 for ( int b=1; b <= 3; b++ ) 20 { 21 System.out.print( "(" + a + "," + b + ") " ); 22 } 23 // * You will add a line of code here. 24 } 25 26 System.out.println("\n"); 27 28 } 29 }
你应该看到什么
A 1 A 2 A 3 B 1 B 2 B 3 C 1 C 2 C 3 D 1 D 2 D 3 E 1 E 2 E 3 (1,1) (1,2) (1,3) (2,1) (2,2) (2,3) (3,1) (3,2) (3,3)
学习演练
- 看看嵌套循环的第一组(“CN”)。哪个变量变化更快?是变量
由外部循环(c)控制还是由内部循环(n)控制的变量? - 更改循环的顺序,使“c”循环在内部,“n”循环在外部。输出如何改变?
- 看看第二组嵌套循环(“AB”)。将
print()
语句更改为println()
。输出如何改变?(然后将其改回print()
。) - 在内部循环(“b”循环)的关闭大括号后添加一个
System.out.println()
语句,但仍在外部循环内。输出如何改变?
练习 47:生成和过滤值
嵌套的for
循环有时很方便,因为它们非常紧凑,可以使一些变量通过许多不同的值组合进行更改。
多年前,一名学生向我提出了以下数学问题:
“布朗农场主想要花费 100.00 美元,并且想要购买确切的 100 只动物。如果每只羊的成本为 10 美元,每只山羊的成本为 3.50 美元,每只鸡的成本为 0.50 美元,那么他应该购买多少只动物?”
他离开后,我想了几秒钟,然后写了以下程序。
1 public class FarmerBrown 2 { 3 public static void main( String[] args ) 4 { 5 for ( int s = 1 ; s <= 100 ; s++ ) 6 { 7 for ( int g = 1 ; g <= 100 ; g++ ) 8 { 9 for ( int c = 1 ; c <= 100 ; c++ ) 10 { 11 if ( s+g+c == 100 && 10.00*s + 3.50*g + 0.50*c == 100.00 ) 12 { 13 System.out.print( s + " sheep, " ); 14 System.out.print( g + " goats, and " ); 15 System.out.println( c + " chickens." ); 16 } 17 } 18 } 19 } 20 } 21 }
你应该看到什么
4 sheep, 4 goats, and 92 chickens.
这个程序很整洁,因为它非常简短。但是,坐在最内部循环内(在第 11 行的if
语句前面)的观察者将看到一百万种不同的 s、g 和 c 组合。尝试的第一个组合将是 1 只羊,1 只山羊,1 只鸡。这将被插入到if
语句中的数学方程式中。它们不会是真的,也不会打印任何东西。
然后下一个组合将是 1 只羊,1 只山羊和 2 只鸡。这也会失败。然后是 1 只羊,1 只山羊,3 只鸡。等等,直到 1 只羊,1 只山羊和 100 只鸡,当内部循环运行最后一次迭代时。
然后,第 7 行的g ++
将执行,第 7 行的条件将检查 g 是否仍然小于或等于 100(是的),并且中间for
循环的主体将再次执行。
这将导致最内层循环的初始化表达式再次运行,将 c 重置为 1。因此,在if
语句中将测试的下一个变量组合是 1 只羊,2 只山羊和 1 只鸡。然后是 1 只羊,2 只山羊,2 只鸡,然后是 1 只羊,2 只山羊,3 只鸡。依此类推。
到最后,所有 100 * 100 * 100 种组合都经过了测试,其中 999,999 种失败了。但是因为计算机非常快,答案立即出现。
由于在if
的主体中只有一行代码时,Java 中的大括号是可选的
1 public class FarmerBrownCompact 2 { 3 public static void main( String[] args ) 4 { 5 for ( int s = 1 ; s <= 100 ; s++ ) 6 for ( int g = 1 ; g <= 100 ; g++ ) 7 for ( int c = 1 ; c <= 100 ; c++ ) 8 if ( s+g+c == 100 && 10.00*s + 3.50*g + 0.50*c == 100.00 ) 9 System.out.println( s + " sheep, " + g + " goats, and " + c + " chickens." ); 10 } 11 }
在if
的主体或for
循环的主体中,我可以使代码更加紧凑:
这是完全合法的,并且与以前的版本行为完全相同。将其与我们使用while
循环而不是for
循环解决此程序时需要编写多少代码进行比较:
1 public class FarmerBrownWhile 2 { 3 public static void main( String[] args ) 4 { 5 int s = 1; 6 while ( s <= 100 ) 7 { 8 int g = 1; 9 while ( g <= 100 ) 10 { 11 int c = 1; 12 while ( c <= 100 ) 13 { 14 if ( s+g+c == 100 && 10.00*s + 3.50*g + 0.50*c == 100.00 ) 15 { 16 System.out.print( s + " sheep, " ); 17 System.out.print( g + " goats, and " ); 18 System.out.println( c + " chickens." ); 19 } 20 c++; 21 } 22 g++; 23 } 24 s++; 25 } 26 } 27 }
while
循环版本也更加脆弱,因为很容易忘记将变量重置为1
或在循环体的末尾递增它。使用while
循环可能更容易编译,但更有可能出现细微的逻辑错误,编译后却不能按预期工作。
学习演练
1.我们的代码可以运行,但不够高效。(例如,没有理由让“sheep”循环尝试 11 只或 12 只或更多的羊,因为我们买不起。看看你是否可以改变循环边界,使组合更少浪费。
练习 48:数组-单个变量中的多个值
在这个练习中,你将学到两件新事物。第一件事非常重要,第二件事只是有点有趣。
在 Java 中,“数组”是一种类型的变量,它有一个名称(“标识符”),但包含多个变量。在我看来,只有当你能够处理数组时,你才能成为一个真正的程序员。所以,这是个好消息。你快要成功了!
1 public class ArrayIntro 2 { 3 public static void main( String[] args ) 4 { 5 String[] planets = { "Mercury", "Venus", "Earth", "Mars", "Jupiter", "Saturn", "Uranus", "Neptune" }; 6 7 for ( String p : planets ) 8 { 9 System.out.println( p + "\t" + p.toUpperCase() ); 10 } 11 } 12 }
你应该看到什么
Mercury MERCURY Venus VENUS Earth EARTH Mars MARS Jupiter JUPITER Saturn SATURN Uranus URANUS Neptune NEPTUNE
在第 5 行,我们声明并定义了一个名为planets的变量。它不仅仅是一个字符串:注意方括号。这个变量是一个字符串数组。这意味着这个变量包含了所有八个字符串,并且它们被分成不同的槽,所以我们可以逐个访问它们。
这一行上的花括号用于不同于通常的目的。所有这些值都在引号中,因为它们是字符串。每个值之间有逗号,然后整个初始化列表在花括号中。最后有一个分号。
这个练习中的第二个新东西是一种新的for
循环。(有时被称为foreach
循环,因为它有点像另一种编程语言中的循环,那里的关键字实际上是foreach
而不是for
。)
在第 7 行,你将看到这个 foreach 循环在运行。你可以这样大声朗读:“对于数组‘planets’中的每个字符串‘p’……”
因此,在这个 foreach 循环的循环体内,字符串变量 p 将获得字符串数组 planets 中每个值的副本。也就是说,第一次循环时,p 将包含数组中的第一个值("Mercury"
)的副本。然后第二次循环时,p 将包含数组中的第二个值("Venus"
)的副本。依此类推,直到数组中的所有值都被看到。然后循环将自动停止。
在循环体内(第 9 行),我们只是打印出p的当前值和p的大写版本。可能只是为了好玩。
这种新的for
循环只适用于像这样的复合变量:只有一个名称的变量。
但包含多个值。数组不是 Java 中唯一的复合变量,但我们在本书中不会研究其他任何复合变量。
数组很重要,所以这就够了。在给你增加更多内容之前,我想要绝对确定你理解了这个任务中发生的事情。
练习 49:在数组中查找东西
更多关于数组的内容!在这个练习中,我们将研究如何找到特定的值。我们在这里使用的技术有时被称为“线性搜索”,因为它从数组的第一个槽开始查找,然后移动到第二个槽,然后是第三个,依此类推。
1 import java.util.Scanner; 2 3 public class ArrayLinearSearch 4 { 5 public static void main( String[] args ) 6 { 7 Scanner keyboard = new Scanner(System.in); 8 9 int[] orderNumbers = { 12345, 54321, 78753, 101010, 8675309, 31415, 271828 }; 10 int toFind; 11 12 System.out.println("There are " + orderNumbers.length + " orders in the database."); 13 14 System.out.print("Orders: "); 15 for ( int num : orderNumbers ) 16 { 17 System.out.print( num + " " ); 18 } 19 System.out.println(); 20 21 System.out.print( "Which order to find? " ); 22 toFind = keyboard.nextInt(); 23 24 for ( int num : orderNumbers ) 25 { 26 if ( num == toFind ) 27 { 28 System.out.println( num + " found."); 29 } 30 } 31 } 32 }
你应该看到什么
There are 7 orders in the database. Orders: 12345 54321 78753 101010 8675309 31415 271828 Which order to find? 78753 78753 found.
这次数组的名称是 orderNumbers,它是一个整数数组。它有七个槽。12345
是第一个槽,271828
在数组的最后一个槽。这七个槽中的每一个都可以容纳一个整数。
当我们创建一个数组时,Java 会给我们一个内置变量,告诉我们数组的容量。这个变量是只读的(你可以检索它的值,但不能改变它),被称为.length
。在这种情况下,由于数组 orderNumbers 有七个槽,变量orderNumbers.length
等于7
。这在第 12 行中使用。
在第 15 行,我们有一个 foreach 循环,以在屏幕上显示所有订单号。 “对于数组orderNumbers
中的每个整数‘num’…”。因此,在此循环的主体中,num将逐个接受数组中的每个值,并将它们全部显示出来。
在第 22 行,我们让人类输入订单号。然后我们使用循环让num逐个接受每个
订单号并将它们与toFind逐个比较。当我们找到匹配时,我们会这样说。
(你必须想象我们的数据库中有数百或数千个订单,而不仅仅是七个,当我们找到匹配时,我们会打印出更多内容。我们很快就会到那里。)
学习演练
- 我们在两个 foreach 循环中都创建了一个名为 num 的
int
。我们是否可以只在第 10 行声明变量一次,然后从两个循环中删除int
?试一试看看。 - 尝试更改代码,以便如果未找到订单号,则打印出一条单一消息。这很棘手。即使您没有成功,也要努力尝试,然后再进行下一个练习。
练习 50:说数组中没有某个东西
在生活中,某些类型的陈述之间存在一般缺乏对称性。
存在一只白色的乌鸦。
这个陈述很容易证明。开始观察乌鸦。一旦找到一只白色的,就停下。完成。
不存在白色的乌鸦。
这个陈述要难得多,因为要证明它,我们必须收集世界上所有符合乌鸦资格的东西。如果我们已经全部看过它们,却没有找到任何白色的乌鸦,那么我们才能安全地说没有存在。
希望您尝试了昨天练习中的学习演练。
1 import java.util.Scanner; 2 3 public class ItemNotFound 4 { 5 public static void main( String[] args ) 6 { 7 Scanner keyboard = new Scanner(System.in); 8 9 String[] heroes = { 10 "Abderus", "Achilles", "Aeneas", "Ajax", "Amphitryon", 11 "Bellerophon", "Castor", "Chrysippus", "Daedalus", "Diomedes", 12 "Eleusis", "Eunostus", "Ganymede", "Hector", "Iolaus", "Jason", 13 "Meleager", "Odysseus", "Orpheus", "Perseus", "Theseus" }; 14 String guess; 15 boolean found; 16 17 System.out.print( "Pop Quiz! Name any mortal hero from Greek mythology: " ); 18 guess = keyboard.next(); 19 20 found = false; 21 for ( String hero : heroes ) 22 { 23 if ( guess.equals(hero) ) 24 { 25 System.out.println( "That's correct!" ); 26 found = true; 27 } 28 } 29 30 if ( found == false ) 31 { 32 System.out.println( "No, " + guess + " wasn't a Greek mortal hero." ); 33 } 34 } 35 }
你应该看到什么
Pop Quiz! Name any mortal hero from Greek mythology: Hercules No, Hercules wasn't a Greek mortal hero.
大多数学生希望通过在循环内部放置另一个if
语句(或else
)来解决这个问题,以表明“未找到”。但这是行不通的。如果我想知道是否找到了某物,那么一旦我找到它,就可以这样说。但是,如果我想知道某物从未被找到,您必须等到循环结束才能确定。
所以在这种情况下,我使用了一种称为“标志”的技术。标志是一个以一个值开始的变量。如果发生了某事,该值将被更改。然后在程序的后面,您可以使用标志的值来查看是否发生了该事件。
我的标志变量是一个名为 found 的布尔变量,在第 20 行设置为false
。如果找到匹配,我们会这样做,并在第 26 行将标志更改为true
。请注意,在循环内部没有可以将标志更改为false
的代码,因此一旦它被翻转为true
,它将保持不变。
然后在第 30 行,在循环结束后,您可以检查标志。如果它仍然是false
,那么我们知道
循环内的if
语句从未为真,因此我们从未找到我们要找的东西。
练习 51:没有 foreach 循环的数组
正如您现在可能已经注意到的那样,数组和 foreach 循环被设计为很好地配合使用。但也有一些情况下,我们一直在做的事情不起作用。
- foreach 循环无法向后迭代数组;它只能向前。
- foreach 循环不能用来更改数组槽中的值。foreach 循环变量是数组中的一个只读副本,更改它不会改变数组。
此外,我们一直在使用初始化列表(花括号的东西)将值放入数组中,这有其自身的局限性:
- 初始化列表只在声明数组时有效;你不能在代码的其他地方使用它。
- 初始化列表最适合相对较小的数组,如果数组中有 1000 个值,初始化列表就不好玩了。
- 如果我们希望数组中的值来自文件或者我们在输入代码时没有的其他地方,初始化列表就帮不上忙了。
所以还有另一种方法可以存储数组中的值并访问它们。事实上,这种方法比你一直在做的更常见。使用方括号和槽号,我们可以单独访问数组的槽。
1 public class ArraySlotAccess 2 { 3 public static void main( String[] args ) 4 { 5 int[] arr = new int[3]; 6 int i; 7 8 arr[0] = 0; 9 arr[1] = 0; 10 arr[2] = 0; 11 12 System.out.println("Array contains: " + arr[0] + " " + arr[1] + " " + arr[2] ); 13 14 // Fill each slot of this array with a random number 1100 15 arr[0] = 1 + (int)(Math.random()*100); 16 arr[1] = 1 + (int)(Math.random()*100); 17 arr[2] = 1 + (int)(Math.random()*100); 18 19 // Display them again. 20 System.out.println("Array contains: " + arr[0] + " " + arr[1] + " " + arr[2] ); 21 22 // This is a bit silly, but try to understand it. 23 i = 0; 24 arr[i] = 1 + (int)(Math.random()*100); 25 i = 1; 26 arr[i] = 1 + (int)(Math.random()*100); 27 i = 2; 28 arr[i] = 1 + (int)(Math.random()*100); 29 30 // Display them again. 31 System.out.print("Array contains: "); 32 i = 0; 33 System.out.print(arr[i] + " "); 34 i = 1; 35 System.out.print(arr[i] + " "); 36 i = 2; 37 System.out.print(arr[i] + " "); 38 System.out.println(); 39 40 // This is even more silly but it works. Can you guess where this is headed? 41 i = 0; 42 arr[i] = 1 + (int)(Math.random()*100); 43 i++; 44 arr[i] = 1 + (int)(Math.random()*100); 45 i++; 46 arr[i] = 1 + (int)(Math.random()*100); 47 i++; 48 49 // Display them again. 50 System.out.print("Array contains: "); 51 i = 0; 52 System.out.print(arr[i] + " "); 53 i++; 54 System.out.print(arr[i] + " "); 55 i++; 56 System.out.print(arr[i] + " "); 57 i++; 58 System.out.println(); 59 60 // Ah! Let's just use a regular 'for' loop! 61 for ( i=0 ; i < arr.length ; i++ ) 62 { 63 arr[i] = 1 + (int)(Math.random()*100); 64 } 65 66 // Display them again. 67 System.out.print("Array contains: "); 68 for ( i=0 ; i < arr.length ; i++ ) 69 { 70 System.out.print(arr[i] + " "); 71 } 72 System.out.println(); 73 } 74 }
你应该看到什么
Array contains: 0 0 0 Array contains: 98 49 18 Array contains: 83 77 1 Array contains: 62 74 32 Array contains: 40 17 54
在第 5 行,我们创建了一个整数数组,没有使用初始化列表。[3]
表示数组的容量为 3。由于我们没有提供值,数组中的每个槽最初都存有值0
。一旦数组被创建,它的容量就不能改变。
在第 8 到 10 行有一个惊喜。数组有 3 个槽,但槽号是基于 0 的。(指代数组槽的数字称为“索引”。总体上应该称为“索引”(INN-duh-SEEZ),但大多数人只说“索引”)。
所以数组中的第一个槽是索引0
。这个数组可以容纳三个值,所以最后一个索引是2
。除了习惯它,你无法做任何事情。所以arr.length
是3
,但没有一个槽的索引是3
。这可能会在一开始给你带来 bug,但最终你会学会的。
无论如何,第 8 到 10 行将值0
存储到数组的所有三个槽中。(这个值已经在其中了,所以这段代码没有任何用处。)
在第 12 行,我们打印出数组中所有三个当前值,这样你就可以看到它们都是零。
在第 15 到 17 行,我们将随机数放入数组的每个槽中。然后在第 20 行再次打印出来。
从第 22 行开始,我做了一些傻事。在练习结束之前,请不要下判断。
不管你为什么要这样做,你看到第 24 行基本上与第 15 行相同吗?第 24 行将一个随机数存储到数组的一个位置。哪个位置?索引取决于 i 的当前值。而 i 当前是0
。所以我们将随机数存储到索引为0
的槽中。明白了吗?
所以在第 25 行,我们将 i 的值从0
改为1
。然后在第 26 行,我们将一个随机值存储在由 i 的值索引的槽中,所以索引是1
。明白了吗?奇怪,但合法。
我在第 31 到 38 行使用了类似的花招来再次在屏幕上显示所有的值。现在,这显然比我在第 20 行做的要糟糕。我的意思是,我用了 8 行代码来做我之前用一行代码做的事情。(跟着我。)
在第 40 到 47 行,我们做的事情甚至可能比第 22 到 28 行更糟糕。第 41 和 42 行是一样的,但在第 43 行,我没有直接将1
放入 i,而是说“增加 i 的值 1”。所以 i 原来是0
;在第 43 行之后它变成了1
。
这种方法的唯一优势几乎是复制和粘贴更容易。第 42 和 43 行完全相同于第 44 和 45 行。第 46 和 47 行也是如此。我的意思是,完全一样。
我们以类似的愚蠢方式在第 50 到 58 行显示它们。
但也许你会想到。“为什么我要连续三次输入完全相同的行,而不是……”你知道一种允许你重复一段代码的东西,同时使一个变量每次增加一个的东西,对吧?
没错:for
循环就是这样的。我一点都不傻,对吧?
第 61 到 64 行与第 41 到 47 行相同,只是我们让for
循环处理重复和索引的变化。for
循环的初始化表达式(第一部分)将 i 设置为0
,这恰好是数组的最小合法索引。条件说“只要 i 小于arr.length
(即3
)就重复这个操作。”请注意,这是小于,而不是小于或等于,那样就太多了。更新表达式(第三部分)每次只是将 i 加 1。
第 67 到 72 行显示了屏幕上的值。
事实上,这种代码在 61 到 72 行之间可能看起来有点复杂,但在 Java 中使用数组时,你会一直写这样的代码。我甚至无法告诉你我有多少次为了处理数组而写了一个像那样的for
循环。
实际上,如果你的问题是“我怎么才能一个数组?”(在空白处填入你喜欢的任何任务。)答案是“用for
循环。”几乎可以肯定。
学习演练
- 在代码的顶部,将数组的容量改为 1000 而不是 3。不要改变任何其他代码,然后重新编译和运行。猜猜看?底部的那些
for
循环可能会更难写和理解,但一旦写好,它们对于 1000 个值和 3 个值一样有效。这很酷。
练习 52:最低温度
在我们离开数组之前,这个练习将整合函数、循环、数组和从文件中读取数据,做一些(希望)有趣的事情!
我已经创建了一个文本文件,其中包含了 1995 年 1 月 1 日至 2013 年 6 月 23 日德克萨斯州奥斯汀市的平均日温度。文件中有一些数据点丢失,所以文件中共有 6717 个温度。你可以在这里看到这些数字:
这些值是以华氏度为单位。这个练习将把文件中的所有值(甚至直接从互联网上)读入一个double
数组,然后使用循环来找到整个 17 年半范围内的最低温度。听起来有趣吗?让我们开始吧。
1 import java.net.URL; 2 import java.util.Scanner; 3 4 public class LowestTemperature 5 { 6 public static void main(String[] args) throws Exception 7 { 8 double[] temps = arrayFromUrl("http://learnjavathehardway.org/txt/avgdailytempsatx.txt"); 9 10 System.out.println( temps.length + " temperatures in database."); 11 12 double lowest = 9999.99; 13 14 for ( int i=0; i<temps.length; i++ ) 15 { 16 if ( temps[i] < lowest ) 17 { 18 lowest = temps[i]; 19 } 20 } 21 22 System.out.print( "The lowest average daily temperature was " ); 23 System.out.println( lowest + "F (" + fToC(lowest) + "C)" ); 24 } 25 26 public static double[] arrayFromUrl( String url ) throws Exception 27 { 28 Scanner fin = new Scanner((new URL(url)).openStream()); 29 int count = fin.nextInt(); 30 31 double[] dubs = new double[count]; 32 33 for ( int i=0; i<dubs.length; i++ ) 34 dubs[i] = fin.nextDouble(); 35 fin.close(); 36 37 return dubs; 38 } 39 40 public static double fToC( double f ) 41 { 42 return (f32)*5/9; 43 } 44 45 }
你应该看到什么
6717 temperatures in database. The lowest average daily temperature was 22.1F (5.499999999999999C)
(如果你必须在没有互联网访问权限的机器上运行这个程序,那么这段代码就行不通了。因为你已经知道如何从文本文件中读取数据,你可以自己修改它,让它从一个本地文件中读取(一个与你的代码在同一个文件夹中的文件,而不是在互联网上)。但如果你懒得动手,我在下面列出了一个备用版本。)
嗯,一开始我就扔了一个曲线球。在第 8 行,我们声明了一个名为double
的数组
温度,但是不是像这样简单地设置它的容量:
double[] temps = new double[6717];
……我用一个函数的返回值初始化了数组!所以在继续之前,让我们看看函数。
在第 26 行,函数定义开始。这个函数叫做arrayFromUrl()
,它有一个参数:一个字符串。它返回什么?它返回的不是double
,而是double[]
(一个double
数组)。
在第 28 行,我们创建了一个 Scanner 对象来从文件中读取数据,但是我们并没有从文件中获取数据,而是从一个 URL 中获取信息。Java 的一个好处就是这只是一个微小的改变。
现在,我用了一个我多年前学会的文本文件技巧。在我写这一章的时候,我的文件包含了 6717 个温度。但也许你是在一年后读到这篇文章,我想更新文件以添加更多的温度。所以文件的第一行只是一个数字:6717
。然后在那之后,我有 6717 行温度,每行一个。
在这段代码的第 29 行,我从文件中的第一行读取count。我使用该计数来决定第 31 行上我的数组应该有多大。所以,六个月后,如果我决定向文件中添加更多温度,我只需要更改文件的第一行,这段代码仍然可以工作。是不是一个不错的技巧?
在第 31 行,我们定义了一个具有count槽的双精度数组。(目前为 6717。)
在第 33 行,有一个for
循环,它遍历数组中的每个槽,并且在第 34 行,我们每次从文件中读取一个double
(fin.nextDouble()
)并将其存储到数组中的下一个索引槽中。
然后当循环结束时,我close()
了文件。然后在第 37 行,数组从函数中返回,这个数组就是存储在main()
的第 8 行的数组 temps 中的。
在第 10 行,我们打印出数组的当前长度,以确保读取没有出错。
在第 12 行,我们创建一个变量,最终将保存整个数组中的最低温度。起初,我们在那里放了一个非常大的值。
第 14 行是另一个for
循环,将给出数组中的所有合法索引。在这种情况下,由于数组中有 6717 个值,索引将从0
到6716
。
第 16 行比较了我们当前在数组中查看的值(取决于当前值i)。如果该值小于lowest中的任何值,那么我们就有了一个新的记录!在第 18 行,我们用这个新的更小的值替换了以前在lowest中的值。
循环会一直持续,直到数组中的所有值都被比较。当循环结束时,变量lowest现在实际上包含了最小的值。
在第 40 到 43 行有一个小函数,用于将华氏度转换为摄氏度的温度。所以在第 23 行,我们显示了来自文件的最低温度,也转换为摄氏度。
你可能会认为 22.1 华氏度(-5.5 摄氏度)不是非常寒冷的温度。好吧,这就是德克萨斯。还要记住,这些温度不是一天中最低的温度,它们是每天 24 小时温度样本的平均值。
学习演练
- 将代码更改为显示最低平均日温度和最高平均日温度。
- 尝试在网上找到另一个离你更近的城市的温度文件,并将你的代码更改为从该文件中读取!
(我上面提到过,但这是修改后的代码,用于从本地文件中读取温度数据,以防你无法在具有互联网访问权限的计算机上运行 Java 程序。)
1 import java.io.File; 2 import java.util.Scanner; 3 4 public class LowestTemperatureLocal 5 { 6 public static void main(String[] args) throws Exception 7 { 8 double[] temps; 9 double lowest = 9999.99; 10 11 // Read values from file 12 Scanner fin = new Scanner(new File("avgdailytempsatx.txt")); 13 temps = new double[fin.nextInt()]; 14 15 System.out.println( temps.length + " temperatures in database."); 16 17 for ( int i=0; i<temps.length; i++ ) 18 temps[i] = fin.nextDouble(); 19 fin.close(); 20 21 for ( int i=0; i<temps.length; i++ ) 22 if ( temps[i] < lowest ) 23 lowest = temps[i]; 24 25 System.out.print( "The lowest average daily temperature was " ); 26 System.out.println( lowest + "F (" + fToC(lowest) + "C)" ); 27 } 28 29 public static double fToC( double f ) { return (f32)*5/9; } 30 }
练习 53:邮寄地址(记录)
今天的练习是关于我所谓的“记录”。在 C 和 C++编程语言中,它们被称为“结构”。数组是一个变量中的许多不同值,其中值都是相同类型的,并且它们由索引(槽号)区分。记录是一个变量中的几个不同值,但值可以是不同类型的,并且它们由名称(通常称为“字段”)区分。
将以下代码输入到一个名为MailingAddresses.java
的单个文件中。(第一行说
class Address
是正确的,但你不能把你的文件命名为Address.java
,否则它就不会工作。
1 class Address 2 { 3 String street; 4 String city; 5 String state; 6 int zip; 7 } 8 9 public class MailingAddresses 10 { 11 public static void main(String[] args) 12 { 13 Address uno, dos, tres; 14 15 uno = new Address(); 16 uno.street = "191 Marigold Lane"; 17 uno.city = "Miami"; 18 uno.state = "FL"; 19 uno.zip = 33179; 20 21 dos = new Address(); 22 dos.street = "3029 Losh Lane"; 23 dos.city = "Crafton"; 24 dos.state = "PA"; 25 dos.zip = 15205; 26 27 tres = new Address(); 28 tres.street = "2693 Hannah Street"; 29 tres.city = "Hickory"; 30 tres.state = "NC"; 31 tres.zip = 28601; 32 33 System.out.println(uno.street + "\n" + uno.city + ", " + uno.state + " " + uno.zip + "\n"); 34 System.out.println(dos.street + "\n" + dos.city + ", " + dos.state + " " + dos.zip + "\n"); 35 System.out.println(tres.street + "\n" + tres.city + ", " + tres.state + " " + tres.zip + "\n"); 36 } 37 }
你应该看到什么
191 Marigold Lane
- 这一切只有一个问题:Java 实际上并没有记录。事实证明,如果你创建一个没有方法,只有公共变量的嵌套类,它就像一个结构一样工作,即使它不是 Java 的方式。
我不在乎这是否是 Java 的方式。我已经教了很多学生,我坚信如果你不先理解记录,就很难理解面向对象的编程。
如果你不先理解记录,就很难理解面向对象的编程。所以我要以一种完全正常的方式伪造它们,这在许多不同的编程语言中都是非常好的代码。
一些顽固的面向对象的 Java 爱好者会偶然发现这个练习,并给我发送一封恶毒的电子邮件,说我做错了,为什么我要用谎言来填充这些可怜的孩子的头脑?哦,好吧。
Miami, FL 33179 3029 Losh Lane Crafton, PA 15205 2693 Hannah Street Hickory, NC 28601
因此,在第 1 到 7 行,我们定义了一个名为Address
的记录。
(我知道它说class
,而不是record
。如果我能做点什么,我发誓我会。无论如何,您应该将其称为record
,或者如果您真的想要的话,称为“struct”。如果您将其称为class
,它将使任何热爱面向对象编程的 Java 程序员感到困惑,如果您将其称为“struct”,至少 C 和 C++程序员会理解您。)
我们的记录有四个字段。 第一个字段是名为street的字符串。 第二个字段是称为
城市。等等。
然后在第 9 行开始我们的“真正”类。
在第 13 行,我们声明了三个名为 uno,dos 和 tres 的变量。 这些变量不是整数或字符串; 它们是记录。 类型为Address
。 每个记录中都有四个字段。
在第 15 行,我们必须将一个 Address 对象存储在变量中,因为请记住,我们只声明了变量,它们还没有被初始化。
一旦处理好这些,您将看到我们可以将字符串"191 Marigold Lane"
存储到
Address 记录名为 uno 的 street 字段,这正是我们在第 16 行所做的。 第 17 行将字符串"Miami"
存储到记录 uno 的 city 字段中。
我不打算解释程序的其余部分发生了什么,因为我认为这是
非常清楚。 我想值得一提的是,尽管记录中的三个字段都是字符串,但zip字段是整数。 记录的字段可以是您想要的任何类型。
学习演练
- 在第 13 行创建第四个 Address 变量,并更改代码以将您的邮寄地址放入其中。不要忘记在底部打印出来。
常见问题
- 你从哪里得到这些地址的?
我编造了它们。 我相当肯定这些街道在这些城市中并不存在。 如果我奇迹般地编造了一个真实地址,请告诉我,我会更改它。
练习 54:从文件中读取记录
这个练习将向您展示如何从文本文件中读取记录的值。 还有一个示例,演示了一个循环,该循环会读取整个文件,无论文件有多长。
如果你在一个没有连接到互联网的机器上运行这个程序,这段代码将无法正常工作,尽管更改非常小。 该代码访问此文件,如果需要,您可以下载该文件。
将以下代码键入名为ActorList.java
的单个文件中。(说class
Actor
的行是正确的,但您不能将文件命名为Actor.java
,否则它将无法工作。)
1 import java.util.Scanner; 2 3 class Actor 4 { 5 String name; 6 String role; 7 String birthdate; 8 } 9 10 public class ActorList 11 { 12 public static void main(String[] args) throws Exception 13 { 14 String url = "http://learnjavathehardway.org/txt/s01e01cast.txt"; 15 // Scanner inFile = new Scanner(new java.io.File("s01e01cast.txt")); 16 Scanner inFile = new Scanner((new java.net.URL(url)).openStream()); 17 18 while ( inFile.hasNext() ) 19 { 20 Actor a = getActor(inFile); 21 System.out.print(a.name + " was born on " + a.birthdate); 22 System.out.println(" and played the role of " + a.role); 23 } 24 inFile.close(); 25 } 26 27 public static Actor getActor( Scanner input ) 28 { 29 Actor a = new Actor(); 30 a.name = input.nextLine(); 31 a.role = input.nextLine(); 32 a.birthdate = input.nextLine(); 33 34 return a; 35 } 36 }
你应该看到什么
Sean Bean was born on 19590417 and played the role of Eddard 'Ned' Stark Mark Addy was born on 19640114 and played the role of Robert Baratheon Nikolaj CosterWaldau was born on 19700727 and played the role of Jaime Lannister Michelle Fairley was born on 1964 and played the role of Catelyn Stark
这次我们的记录称为Actor
,有三个字段,所有字段都是字符串。
在第 16 行,我们创建了一个与输入文本文件的互联网地址连接的 Scanner 对象。 您注意到我在顶部没有导入java.net.URL
吗? 只有在您想要能够输入类名的简短版本时,才需要导入类。
在这种情况下,如果我在代码顶部导入了java.net.URL
,我可以直接写:
Scanner inFile = new Scanner((new URL(url)).openStream()); // instead of Scanner inFile = new Scanner((new java.net.URL(url)).openStream());
有时,如果我只打算使用一个类一次,我宁愿在我的代码中使用完整的名称,而不是导入它。 我在第 15 行也使用了同样的技巧; 而不是导入java.io.File
,我只是在这里使用了完整的类名。
(如果您的机器没有互联网访问权限,请删除第 15 行开头的两个斜杠,这样它就不再是注释,然后在第 16 行开头添加两个斜杠使它成为注释。
然后程序将在本地读取文件,而不是通过互联网读取。)
无论您是从互联网还是从您自己的计算机打开文件,在第 17 行之后,我们都有一个名为inFile的 Scanner 对象,它连接到一个文本文件。
当我们从文本文件中读取数据时,很多时候我们事先不知道它的长度。在最低温度练习中,我向你展示了一个处理这个问题的技巧:将项目数量存储为文件的第一行。但更常见的技术是我在这里使用的:只需使用一个循环,直到我们到达文件的末尾。
Scanner 对象的.hasNext()
方法将在尚未读取的数据时返回true
。如果没有更多数据,则返回false
。因此,在第 18 行,我们创建一个while
循环,只要.hasNext()
继续返回true
,就会重复。
在我们查看第 20 行之前,让我们跳到第 27 到 35 行,我在那里创建了一个函数,该函数将从文件中读取单个演员记录的所有数据。
该函数名为 getActor。它有一个参数:一个 Scanner 对象!没错,你将一个已经打开的 Scanner 对象传递给函数,它会从中读取。getActor 函数返回一个Actor
。它返回整个记录。
如果我们要从函数中返回一个Actor
对象,我们需要一个Actor
类型的变量来返回,因此我们在第 29 行定义了一个。我只是称它为 a,因为在函数内部我们对这个变量的目的一无所知。您应该为变量提供良好的名称,但在这种情况下,像 a 这样的简短、无意义的名称是完全可以的。
第 30 到 32 行读取文本文件中的三行并将它们存储到记录的三个字段中。然后函数完成了它的工作,我们将记录返回到main()
中的第 20 行。
为什么我们在main()
中和函数中都要创建一个名为 a 的Actor
变量?因为变量作用域。变量只在其所在的块中(也就是“可见”)。
声明。不管变量是否从函数中“返回”,因为请记住,返回的不是变量本身,而是变量的值的副本。
在main()
的第 20 行声明(和定义)了一个名为 a 的Actor
变量,但当第 23 行的闭合大括号出现时,该变量就超出了作用域。在getActor()
函数的第 29 行声明(和定义)了另一个名为 a 的Actor
变量,但当第 35 行的闭合大括号出现时,该变量也超出了作用域。
好的,回到第 20 行。变量 a 的值来自函数getActor()
的返回值。我们将打开的 Scanner 对象 inFile 作为函数的参数传递给它,它会返回一个填充了所有字段的Actor
对象。
(为什么参数称为inFile,而参数称为input?因为它们不是同一个变量。参数input在第 27 行声明,并从参数inFile获取值的副本。它们是两个具有相同值的不同变量。)
经过所有这些,第 21 和 22 行非常无聊:它们只是显示记录的所有字段的值。在第 23 行,循环会再次重复检查条件:现在我们从文件中读取了另一条记录,文件是否仍然有更多?如果是,继续循环。如果不是,跳到第 24 行,关闭文件。
请注意,在函数和main()
中的while
循环中,变量 a 一次只保存一个记录。我们从文件中读取所有记录并将它们全部打印在屏幕上,但当程序最后一次通过循环时,变量 a 只保存最近的记录。所有其他记录仍然在文件中,并且已经显示在屏幕上,但它们的值目前没有保存在任何变量中。
我们可以修复这个问题,但要等到下一个练习。