原文:Learn Java The Hard Way 译者:飞龙 协议:CC BY-NC-SA 4.0
练习 37:从函数返回一个值
有些函数有参数,有些没有。参数是将值传递到函数的唯一方法。也只有一种方法可以从函数中得到一个值:返回值。
这个练习给出了一个具有三个参数(三角形的边长)和一个输出(使用海伦公式计算三角形的面积)的函数的例子。
代码语言:javascript复制 1 public class HeronsFormula
2 {
3 public static void main( String[] args )
4 {
5 double a;
6
7 a = triangleArea(3, 3, 3);
8 System.out.println("A triangle with sides 3,3,3 has an area of " a );
9
10 a = triangleArea(3, 4, 5);
11 System.out.println("A triangle with sides 3,4,5 has an area of " a );
12
13 a = triangleArea(7, 8, 9);
14 System.out.println("A triangle with sides 7,8,9 has an area of " a );
15
16 System.out.println("A triangle with sides 5,12,13 has an area of "
triangleArea(5, 12, 13) );
17 System.out.println("A triangle with sides 10,9,11 has an area of "
triangleArea(10, 9, 11) );
18 System.out.println("A triangle with sides 8,15,17 has an area of "
triangleArea(8, 15, 17) );
19 }
20
21 public static double triangleArea( int a, int b, int c )
22 {
23 // the code in this function computes the area of a triangle whose sides have
lengths a, b, and c
24 double s, A;
25
26 s = (a b c) / 2;
27 A = Math.sqrt( s*(sa)*(sb)*(sc) );
28
29 return A;
30 // ^ after computing the area, "return" it
31 }
32 }
你应该看到什么
代码语言:javascript复制A triangle with sides 3,3,3 has an area of 2.0 A triangle with sides 3,4,5 has an area of 6.0
A triangle with sides 7,8,9 has an area of 26.832815729997478 A triangle with sides 5,12,13 has an area of 30.0
A triangle with sides 10,9,11 has an area of 42.42640687119285 A triangle with sides 8,15,17 has an area of 60.0
你可以看到函数triangleArea
有三个参数。它们都是整数,它们的名字分别是 a、b 和 c。正如你已经知道的,这意味着我们不能在不提供三个整数值作为参数的情况下调用函数。
除此之外,triangleArea
函数返回一个值。请注意,在第 21 行,它没有在public static
和triangleArea
之间说void
。它说double
。这意味着“这个函数返回一个值,它返回的值的类型是double
。”
如果在这个位置上有关键字void
,这意味着“这个函数不返回任何值。” 如果我们想让triangleArea
返回不同类型的值:
public static int triangleArea( int a, int b, int c ) // this would return an
int
public static String triangleArea( int a, int b, int c ) // this would return a
String
public static boolean triangleArea( int a, int b, int c ) // this would return
either true or false
public static void triangleArea( int a, int b, int c ) // this cannot return
any value of any type
有时我的学生会对返回值和不返回值的函数感到困惑。类比是有帮助的。
假设我们坐在我的学校教室里。我们听到雷声,我记得我把车窗开着。我不想让雨水把车里弄湿,所以我让你出去停车场。
“学生,请出去停车场,把我的车窗摇上。” “好的,先生,”你说。
如果你需要我关于我的车是什么样子的信息,那么这些就是参数。如果你已经知道哪辆是我的,你就不需要参数。
最终你返回并说“我完成了任务。”这种类型的函数不返回值。
代码语言:javascript复制rollUpWindows(); // if you don't need parameters
rollUpWindows("Toyota", "Corolla", 2008, "blue"); // if you do need parameters
无论哪种情况,函数都会被执行并完成其任务,但不返回任何值。现在,例子#2:
我们再次在我的教室里。我正在网上更新我的汽车保险,网页要求我输入我的车牌号。我不记得了,所以我让你去停车场帮我拿。
最终你返回并告诉我车牌号。也许你把它写在一张纸上,也许你记住了。当你给我时,我自己抄下来。这种类型的函数返回一个值。
代码语言:javascript复制String plate;
plate = retrieveLicensePlate(); // if you don't need parameters
plate = retrieveLicensePlate("Toyota", "Corolla", 2008, "blue"); // if you do need them
如果我粗鲁,你可以回到我的教室,把值给我,我可以把手指放在耳朵上,这样我就听不到你,或者拒绝自己写下来,这样我很快就会忘记它。如果你调用一个返回值的函数,你可以选择不将返回值存储到一个变量中,而是让这个值消失:
代码语言:javascript复制retrieveLicensePlate("Toyota", "Corolla", 2008, "blue"); // returns a value which is lost
triangleArea(3, 3, 3); // returns the area but we refuse to store it into a variable
这通常是一个坏主意,但也许你有你的理由。
无论如何,在第 10 行我们调用triangleArea
函数。我们传入3
、4
和5
作为三个参数。3
被存储为 a 的值(在第 21 行)。4
被存储为 b,5
被放入
c. 使用这些参数值运行第 23 到 28 行的所有代码。最后,变量 A 中存储了一个值。
在第 29 行,我们返回变量A中的值。这个值返回到第 10 行,存储到变量a中。
为了确保你能明白函数值得麻烦的原因,这里有一个例子,写出了同样的程序,但没有使用函数。
代码语言:javascript复制 1 public class HeronsFormulaNoFunction
2 {
3 public static void main( String[] args )
4 {
5 int a, b, c;
6 double s, A;
7
8 a = 3;
9 b = 3;
10 c = 3;
11 s = (a b c) / 2;
12 A = Math.sqrt( s*(sa)*(sb)*(sc) );
13 System.out.println("A triangle with sides 3,3,3 has an area of "
A );
14
15 a = 3;
16 b = 4;
17 c = 5;
18 s = (a b c) / 2;
19 A = Math.sqrt( s*(sa)*(sb)*(sc) );
20 System.out.println("A triangle with sides 3,4,5 has an area of "
A );
21
22 a = 7;
23 b = 8;
24 c = 9;
25 s = (a b c) / 2;
26 A = Math.sqrt( s*(sa)*(sb)*(sc) );
27 System.out.println("A triangle with sides 7,8,9 has an area of "
A );
28
29 a = 5;
30 b = 12;
31 c = 13;
32 s = (a b c) / 2;
33 A = Math.sqrt( s*(sa)*(sb)*(sc) );
34 System.out.println("A triangle with sides 5,12,13 has an area of " A
);
35
36 a = 10;
37 b = 9;
38 c = 11;
39 s = (a b c) / 2;
40 A = Math.sqrt( s*(sa)*(sb)*(sc) );
41 System.out.println("A triangle with sides 10,9,11 has an area of " A
);
42
43 a = 8;
44 b = 15;
45 c = 17;
46 s = (a b c) / 2;
47 A = Math.sqrt( s*(sa)*(sb)*(sc) );
48 System.out.println("A triangle with sides 8,15,17 has an area of " A
);
49 }
50 }
- (变量 A 本身并没有被返回,只有它的值。事实上,要记住变量的“作用域”仅限于它所定义的代码块内吗?(你在练习 21 中学到了这一点。)变量 a 只在函数 main 内部的作用域内,变量 s、A 和参数变量 a、b 和 c 只在函数 triangleArea 内部的作用域内。)
你应该看到什么
代码语言:javascript复制A triangle with sides 3,3,3 has an area of 2.0 A triangle with sides 3,4,5 has an area of 6.0
A triangle with sides 7,8,9 has an area of 26.832815729997478 A triangle with sides 5,12,13 has an area of 30.0
A triangle with sides 10,9,11 has an area of 42.42640687119285 A triangle with sides 8,15,17 has an area of 60.0
学习演练
- 哪一个更长,有函数的还是没有函数的?
- 这两个文件的公式中有一个错误。当
(a b c)
是奇数时,除以2
会丢失.5
。将其修正为(a b c)/2.0
。在没有使用函数的版本中修复会更难吗? - 再添加一个测试:找到一个边长为 9、9 和 9 的三角形的面积。添加起来难吗?如果在不使用函数的版本中添加测试会更难吗?
在完成了学习演练后,你应该看到的内容
代码语言:javascript复制A triangle with sides 3,3,3 has an area of 3.897114317029974 A triangle with sides 3,4,5 has an area of 6.0
A triangle with sides 7,8,9 has an area of 26.832815729997478 A triangle with sides 5,12,13 has an area of 30.0
A triangle with sides 10,9,11 has an area of 42.42640687119285 A triangle with sides 8,15,17 has an area of 60.0
A triangle with sides 9,9,9 has an area of 35.074028853269766
这更好。
练习 38:形状的面积
今天的练习没有什么新东西。这只是对函数的额外练习。这个程序有三个函数(如果算上main
就有四个),它们都有参数,三个都有返回值。
1 import java.util.Scanner;
2
3 public class ShapeArea
4 {
5 public static void main( String[] args )
6 {
7 Scanner keyboard = new Scanner(System.in);
8
9 int choice;
10 double area = 0;
11
12 System.out.println("Shape Area Calculator version 0.1 (c) 2013
Mitchell Sample Output, Inc.");
13
14 do
15 {
16 System.out.println("n==============n");
17 System.out.println("1) Triangle");
18 System.out.println("2) Circle");
19 System.out.println("3) Rectangle");
20 System.out.println("4) Quit");
21 System.out.print("> ");
22 choice = keyboard.nextInt();
23
24 if ( choice == 1 )
25 {
26 System.out.print("nBase: ");
27 int b = keyboard.nextInt();
28 System.out.print("Height: ");
29 int h = keyboard.nextInt();
30 area = computeTriangleArea(b, h);
31 System.out.println("The area is " area);
32 }
33 else if ( choice == 2 )
34 {
35 System.out.print("nRadius: ");
36 int r = keyboard.nextInt();
37 area = computeCircleArea(r);
38 System.out.println("The area is " area);
39 }
40 else if ( choice == 3 )
41 {
42 System.out.print("nLength: ");
43 int length = keyboard.nextInt();
44 System.out.print("Width: ");
45 int width = keyboard.nextInt();
46 System.out.println("The area is "
computeRectangleArea(length, width) );
47 }
48 else if ( choice != 4 )
49 {
50 System.out.println("ERROR.");
51 }
52
53 } while ( choice != 4 );
54
55 }
56
57 public static double computeTriangleArea( int base, int height )
58 {
59 double A;
60 A = 0.5 * base * height;
61 return A;
62 }
63
64 public static double computeCircleArea( int radius )
65 {
66 double A;
67 A = Math.PI * radius * radius;
68 return A;
69 }
70
71 public static int computeRectangleArea( int length, int width )
72 {
73 return (length * width);
74 }
75 }
你应该看到什么
代码语言:javascript复制Shape Area Calculator version 0.1 (c) 2013 Mitchell Sample Output, Inc.
==============
1) Triangle
2) Circle
3) Rectangle
4) Quit
> 1
Base: 3
Height: 5
The area is 7.5
==============
1) Triangle
2) Circle
3) Rectangle
4) Quit
> 2
Radius: 3
The area is 28.274333882308138
==============
1) Triangle
2) Circle
3) Rectangle
4) Quit
> 4
在第 57 行,我们定义了一个计算三角形面积的函数(这次只使用底边和高)。它需要两个参数,并将返回一个double
值。在第 59 行,我们声明了一个名为 A 的变量。这个变量是函数“局部”的。尽管在第 66 行声明了一个名为 A 的变量,但它们并不是同一个变量。(就像有两个名叫“迈克尔”的朋友。只是因为他们有相同的名字并不意味着他们是同一个人。)
变量b(在第 27 行定义)的值作为函数调用中参数base的初始值传入。b被存储到base中,因为b是首先出现的,而不是因为base以b开头。
计算机对此并不在乎。只有顺序才重要。
在第 61 行,A 的值返回到main
,最终被存储在名为 area 的变量中。在矩形面积函数的定义开始于第 71 行时,我做了三件奇怪的事情。
首先,形式参数与实际参数具有相同的名称。(记住,参数是函数定义中声明的变量,位于第 71 行,参数是函数调用中括号中的变量。)这是一个巧合,但并不意味着什么。这就像一个名叫“史蒂文”的演员扮演一个名叫“史蒂文”的角色。main
版本的 length 的值被存储到computeRectangleArea
的 length 变量中,因为它们都在括号中首先列出,没有其他原因。
其次,我没有费心为函数将要返回的值创建一个变量。我只是返回了表达式length*width
的值。函数会计算出值并立即返回,而不会将其存储到变量中。
第三,矩形面积值在第 46 行返回到main
,但我没有费心将返回值存储到变量中:我直接在屏幕上打印出来。(我在HeronsFormula
中也这样做了,但我没有特别指出。)这是完全可以的,实际上非常常见。我们经常调用函数,几乎总是使用函数的返回值,但我们并不总是需要将返回值存储到自己的变量中。
最后,在我们转到另一个话题之前,我应该提到,在 Java 中,函数只能返回一个值。在其他一些编程语言中,函数可以返回多个值。但在 Java 中,函数可以返回一个值或没有值(如果函数是void
),但绝不会超过一个。
P.S.这些函数有点傻。如果我真的需要一个形状面积计算器,我不确定是否值得为一个只有一行代码的方程创建一个完整的函数。但是这个例子用来解释是很好的。
学习演练
- 添加一个计算正方形面积的函数。也将其添加到菜单中。
练习 39:使用 Javadoc 重新审视三十天
在上一个练习中,我们写了一些可能更好被省略的函数。在今天的练习中,我们将重新做一个之前的练习,使用函数使其更好。
而且,因为我总是不断努力,我在类的上方和每个函数的上方添加了称为“Javadoc 注释”的特殊注释。
代码语言:javascript复制 1 import java.util.Scanner;
2
3 /**
4 * Contains functions that make it easier to work with months.
5 */
6 public class ThirtyDaysFunctions
7 {
8 public static void main( String[] args )
9 {
10 Scanner kb = new Scanner(System.in);
11
12 System.out.print( "Which month? (112) " );
13 int month = kb.nextInt();
14
15 System.out.println( monthDays(month) " days hath "
monthName(month) );
16
17 }
18
19 /**
20 * Returns the name for the given month number (112).
21 *
22 * @author Graham Mitchell
23 * @param month the month number (112)
24 * @return a String containing the English name for the given
month, or "error" if month out of range
25 */
26 public static String monthName( int month )
27 {
28 String monthName = "error";
29
30 if ( month == 1 )
31 monthName = "January";
32 else if ( month == 2 )
33 monthName = "February";
34 else if ( month == 3 )
35 monthName = "March";
36 else if ( month == 4 )
37 monthName = "April";
38 else if ( month == 5 )
39 monthName = "May";
40 else if ( month == 6 )
41 monthName = "June";
42 else if ( month == 7 )
43 monthName = "July";
44 else if ( month == 8 )
45 monthName = "August";
46 else if ( month == 9 )
47 monthName = "September";
48 else if ( month == 10 )
49 monthName = "October";
50 else if ( month == 11 )
51 monthName = "November";
52 else if ( month == 12 )
53 monthName = "December";
54
55 return monthName;
56 }
57
58 /**
59 * Returns the number of days in a given month.
60 *
61 * @author Graham Mitchell
62 * @param month the month number (112)
63 * @return the number of days in a nonleap year for that month, or
31 if month out of range
64 */
65 public static int monthDays( int month )
66 {
67 int days;
68
69 /* Thirty days hath September
70 April, June and November
71 All the rest have thirtyone
72 Except the second month alone.... */
73
74 switch(month)
75 {
76 case 9:
77 case 4:
78 case 6:
79 case 11: days = 30;
80 break;
81 case 2: days = 28;
82 break;
83 default: days = 31;
84 }
85
86 return days;
87 }
88 }
你应该看到什么
代码语言:javascript复制Which month? (112) 9
30 days hath September
如果现在忽略 Javadoc 注释,希望您应该能够看到在这里使用函数实际上改进了代码。main()
非常简短,因为大部分有趣的工作都是在函数中完成的。
与月份名称相关的所有代码和变量都被隔离在monthName()
函数中。查找月份天数的所有代码都包含在monthDays()
函数中。
像这样将变量和代码收集到函数中被称为“过程式编程”,这被认为是比将所有代码放在main()
中更重要的进步。这使得您的代码更容易调试,因为如果您遇到月份名称的问题,您就知道它必须在monthName()
函数中。
好了,现在让我们谈谈 Javadoc 注释。
javadoc
是随同 Java 编译器一起提供的自动生成文档的工具。您可以通过在类、函数或变量上方使用特殊类型的块注释来在代码中编写文档。
注释以/**
开头,以*/
结尾,中间的每一行都以星号(*
)开头
就像您在练习中看到的那样排列。
javadoc 注释的第一行是关于该事物(类或函数)的一句话摘要。然后有标签如@author
或@return
,提供了更多关于谁编写了代码,函数期望的参数或它将返回的值的详细信息。
好了,现在是魔法部分。打开终端窗口,就像您要编译代码一样,然后输入以下命令:
代码语言:javascript复制$ javadoc ThirtyDaysFunctions.java
Loading source file ThirtyDaysFunctions.java... Constructing Javadoc information...
Standard Doclet version 1.7.0_21
Building tree for all the packages and classes... Generating /ThirtyDaysFunctions.html...
Generating /packageframe.html... Generating /packagesummary.html... Generating /packagetree.html...
Generating /constantvalues.html...
Building index for all the packages and classes... Generating /overviewtree.html...
Generating /indexall.html... Generating /deprecatedlist.html... Building index for all classes...
Generating /allclassesframe.html... Generating /allclassesnoframe.html... Generating /index.html...
Generating /helpdoc.html...
$
然后,如果您查看ThirtyDaysFunctions.java
所在的文件夹,您将看到许多新文件。(也许我应该先警告您。)在您选择的 Web 浏览器中打开名为index.html
的文件。
这是 javadoc 文档,其中包含大量的信息。您可以在顶部附近找到您为类放置的注释,函数的注释在名为“Method Summary”的部分中。
有关参数和返回类型的详细信息在名为“Method Detail”的部分下面。
学习演练
- 查看内置 Java 类 java.util.Scanner 的 javadoc 文档。注意它看起来与 javadoc 工具生成的文档有多相似?所有官方的 Java 文档都是使用 javadoc 工具创建的,因此学习如何阅读它将成为成为专业 Java 程序员的重要部分。不过现在不要太担心细节,只是试着感受一下它的外观。
左上角是包含在 Java 中的所有代码包的列表,下面是左侧是您可以导入以避免编写代码的所有类/库的列表。专业的 Java 程序员的工作的一大部分是编写代码来粘合现有的 Java 库。
现在这可能让您感到不知所措。这没关系,因为您刚刚开始。希望没有人期望您现在就了解太多。事实上,大多数程序员只了解 Java 的内置库的一小部分,并且当他们需要做一些新的事情时,他们会在互联网上搜索并阅读文档,就像您一样!
练习 40:导入标准库
在上一个练习中,您看到了 Java 中可用的所有内置模块,这可能让您感到恐慌。今天我们将看一个“简单”的程序,我花了大约半个小时的时间编写,因为我花了很多时间在互联网上搜索和导入东西,尝试了一些不起作用的东西。
这段代码有效。它允许用户输入密码(或任何内容),然后打印出该密码的 SHA-256 消息摘要。
当您在编写此代码时,不要忘记在第 7 行的末尾加上throws Exception
。
1 import java.util.Scanner;
2 import java.security.MessageDigest;
3 import javax.xml.bind.DatatypeConverter;
4
5 public class PasswordDigest
6 {
7 public static void main( String[] args ) throws Exception
8 {
9 Scanner keyboard = new Scanner(System.in);
10
11 String pw, hash;
12
13 MessageDigest digest = MessageDigest.getInstance("SHA256");
14
15 System.out.print("Password: ");
16 pw = keyboard.nextLine();
17
18 digest.update( pw.getBytes("UTF8") );
19 hash = DatatypeConverter.printHexBinary( digest.digest() );
20
21 System.out.println( hash );
22 }
23 }
你应该看到什么
代码语言:javascript复制Password: password
5E884898DA28047151D0E56F8DC6292773603D0D6AABBDD62A11EF721D1542D8
那个 64 个字符长的字符串是字符串password
的 SHA256 摘要。该消息摘要对于该输入始终是相同的。
如果您输入不同的密码,当然会得到不同的摘要:
代码语言:javascript复制Password: This is a really long password and no one will ever guess it.
A113B65D8BA8DB72D631D97B7A3698E82CDB9D1F52456C8957312CB91EC02B10
在编程的早期,当机器开始拥有用户名和密码时,很明显您不希望直接在数据库中存储密码本身。相反,他们会存储密码的某种加密哈希。
加密哈希具有两个有用的属性:
- 它们是一致的。给定的输入将始终产生完全相同的输出。
- 它们是单向的。您可以轻松计算给定输入的输出,但找出给您某个输出的输入是非常困难或不可能的。
SHA256 是一个非常好的加密哈希函数,它始终产生一个给定输入(或“消息”)的“摘要”,长度恰好为 256 位。在这里,我们没有尝试处理位,而是打印出了这些位的 base64 表示,最终是 64 个字符长,其中每个字符都是十六进制数字。
回到 20 世纪 70 年代,要在某台机器上更改密码,您需要输入密码,然后机器会将您的用户名和新密码的哈希存储在文件中。
然后,当您以后想要登录到机器时,它会让您输入用户名和密码。它会在密码数据库文件中找到用户名,并找到您密码的存储哈希值。然后它会找到您刚刚输入的密码的哈希值。如果存储的哈希值和计算的哈希值匹配,那么您必须输入了正确的密码,您将被允许访问该机器。
这是一个聪明的方案。这也比直接在数据库中存储密码要好得多。然而,现在计算机速度太快,存储空间太大,这已经不足以提供足够的安全性。由于机器可以非常快速地计算密码的 SHA256,一个决心的黑客不需要很长时间就能够弄清楚您的密码是什么。
(如果您真的想要在数据库中安全存储密码,您应该使用bcrypt
,它专门用于此类事情。不幸的是,bcrypt 并没有内置到 Java 中,因此您需要下载其他人制作的 bcrypt 库。)
好了,关于安全密码就说这么多,让我们走一遍这段代码。您可能希望打开这两个库的 javadoc 文档。
- java.security.MessageDigest
- javax.xml.bind.DatatypeConverter
在第 2 和 3 行,我们导入了两个库,这两个库将用于执行此练习的难点。
在第 13 行,我们创建了一个MessageDigest
类型的变量(现在存在,因为我们导入了java.security.MessageDigest
)。我们的变量名为 digest,尽管我也可以叫它其他名字。变量的值来自于MessageDigest.getInstance()
方法的返回值。我们将一个字符串作为该方法的参数传递,这是我们想要的摘要。在这种情况下,我们使用了"SHA256"
,但"SHA1"
和"MD5"
也可以工作。您可以在 javadoc 文档中阅读有关此内容的信息。
第 15 和 16 行希望是无聊的。请注意,我使用nextLine()
而不是next()
来读取密码,这允许用户输入多个单词。
在第 18 行,我们调用了 String 类的getBytes()
方法,参数为"UTF8"
。这将把字符串值转换为 UTF8 格式的原始字节列表,然后将其直接作为参数传递给名为 digest 的 MessageDigest 对象的update()
方法。我通过阅读 String 类的 javadoc 文档了解了getBytes()
方法!
- java.lang.String
第 19 行我们调用了名为 digest 的 MessageDigest 对象的digest()
方法。这给了我们一个原始的字节列表,不适合在屏幕上打印,所以我们直接将这个原始的字节列表作为参数传递给 DatatypeConverter 类的printHexBinary()
方法。这将返回一个字符串,我们将其存储到变量 hash 中。
然后我们在屏幕上显示哈希值。哇!
如果这个练习让你有点紧张,别担心。如果你能完成本书的前 39 个练习,那么你也可以学会做这种事情。你必须学会阅读 javadoc 文档,了解其他人已经为你写好了什么样的工具,以及如何将它们连接在一起以获得你想要的东西。这只是需要大量的练习!记住,第一次写这个练习花了我半个多小时,而我从上世纪 80 年代开始编程,1996 年开始编写 Java 代码!
学习演练
- 查看本练习中使用的所有方法的 javadoc 文档:getInstance、getBytes、update、digest 和 printHexBinary。查看它们期望的参数和它们将返回的值的类型。
- 从第 7 行的末尾删除
throws Exception
。尝试编译它。(然后再放回去。)你将在下一个练习中学到一点关于异常。
练习 41:写入文件的程序
我们现在要暂时停下来专注于函数,学习一些简单的东西。我们要创建一个程序,可以将信息放入文本文件,而不仅仅是在屏幕上打印东西。
当你在输入这段代码时,不要错过第 6 行末尾的throws Exception
。(在这个练习中,我会解释这意味着什么。)
1 import java.io.FileWriter;
2 import java.io.PrintWriter;
3
4 public class LetterRevisited
5 {
6 public static void main( String[] args ) throws Exception
7 {
8 PrintWriter fileout;
9
10 fileout = new PrintWriter( new FileWriter("letter.txt") );
11
12 fileout.println( " " );
13 fileout.println( "| #### |" );
14 fileout.println( "| #### |" );
15 fileout.println( "| #### |" );
16 fileout.println( "| |" );
17 fileout.println( "| |" );
18 fileout.println( "| Bill Gates |" );
19 fileout.println( "| 1 Microsoft Way |" );
20 fileout.println( "| Redmond, WA 98104 |" );
21 fileout.println( "| |" );
22 fileout.println( " " );
23 fileout.close();
24 }
25 }
你应该看到什么
没错。当你运行你的程序时,它似乎什么都没做。但如果你写得正确,它应该在与你的代码相同的文件夹中创建一个名为letter.txt
的文件。你可以使用与写代码相同的文本编辑器查看这个文件。
如果由于某种原因你正在使用随 Windows 95 一起提供的版本的记事本,它看起来可能会像这样:
(那个截图是很久以前做的,好吧?记住我做这个已经很长时间了。当我第一次
当我把这个作业给学生时,Windows 95 是最新版本的 Windows……实际上,我猜邮政编码在某个时候改变了。)
在第 1 行和第 2 行有两个新的import
语句,分别是用于这两个 Java 类的。
在第 8 行,我们声明了一个变量。这个变量的类型是PrintWriter
,我选择将它命名为 fileout(尽管变量的名称并不重要)。
fileout(尽管变量的名称并不重要)。
在第 10 行,我们给 PrintWriter 变量赋了一个值:一个新的 PrintWriter 对象的引用。创建 PrintWriter 对象需要一个参数。我们给它的参数是一个新的FileWriter
对象,它本身是用文件名作为参数创建的。
可以只使用FileWriter
对象而不使用任何 PrintWriter 来写入文本文件。然而,PrintWriters 更容易使用,你可以通过查看代码的其余部分来看出来。所以我们不直接使用 FileWriter 对象,而是用 PrintWriter 对象“包装”FileWriter 对象,然后通过 PrintWriter 对象进行操作。
(如果你不理解最后两段,没关系。你不需要理解它们来写文件。)
好消息是,一旦 PrintWriter 对象设置好了,其他的事情就很容易了。因为你从一开始就在秘密地使用 PrintWriters!这是因为System.out
是一个 PrintWriter!
因此,在第 12 行,您可以看到写入文件看起来与在屏幕上打印非常相似。但是字符串(
)不会被打印在屏幕上。它将被存储为文件letter.txt
的第一行!
如果该文件夹中已经存在名为letter.txt
的文件,则其内容将被覆盖而不会有警告。如果文件不存在,则将创建该文件。
练习中的另一个重要行是第 23 行。这实际上保存了文件的内容并关闭了它,因此您的程序无法再对其进行写入。如果删除此行,您的程序很可能会创建一个名为letter.txt
的文件,但该文件将为空。
好的,在结束练习之前,我想简要讨论一下throws Exception
。这在真正的编程中并不常见,并且适当地解释它超出了本书的范围,但我确实想谈一下。
在练习的原始版本中,当您在函数的第一行之后放置throws Exception
时,它的意思是“我已经在这个函数中编写了可能不起作用的代码,如果失败,它将会失败(通过抛出异常)。”
在这种情况下,可能不起作用的是new FileWriter(“letter.txt”)
这一行,因为它试图在当前文件夹中打开一个文件进行写入。如果已经有一个名为“letter.txt”的文件并且该文件是只读的,这可能会失败。或者整个文件夹是只读的。或者有其他原因导致程序无法获得对文件的写入权限。
因此,我们不是简单地使程序崩溃,而是应该检测异常并处理它。就像这样:
try
块的意思是“这段代码可能会抛出异常,但尝试执行它。”如果一切顺利(如果没有抛出异常),那么catch
块将被跳过。如果抛出异常,则会执行catch
块,并将抛出的异常作为参数传递进去。(我已经将异常参数命名为 err,尽管它可以被命名为任何东西。)
在catch
块中,我打印出一个合适的错误消息,然后通过调用内置函数System.exit()
来结束程序。如果向System.exit()
传递参数0
,程序将结束,但零表示“一切正常”。参数1
表示“程序正在结束,因为出了问题”。
所以我不会在这本书中再使用try
和catch
了,但至少现在你知道通过使用throws Exception
来避免什么了。
练习 42:从文件中获取数据
能够将信息放入文件的程序只是故事的一部分。因此,在这个练习中,您将学习如何读取已经存在于文本文件中的信息。
如果你输入这段代码并编译并运行,它会崩溃。这是因为它试图从一个名为name-and-numbers.txt
的文本文件中读取,这个文件必须与你的代码在同一个文件夹中。你可能没有这样的文件!
因此,在你写代码之前,让我们创建一个包含一个字符串和三个整数的文本文件。我的文件看起来像这样:
(这是一个稍微更新的记事本版本。现在开心了吗?)好了,来看代码吧!
代码语言:javascript复制 1 import java.util.Scanner;
2 import java.io.File;
3
4 public class GettingFromFile
5 {
6 public static void main( String[] args ) throws Exception
7 {
8 Scanner fileIn = new Scanner(new File("nameandnumbers.txt"));
9
10 int a, b, c, sum;
11 String name;
12
13 System.out.print("Getting name and three numbers from file...");
14 name = fileIn.nextLine();
15 a = fileIn.nextInt();
16 b = fileIn.nextInt();
17 c = fileIn.nextInt();
18 fileIn.close();
19
20 System.out.println("done.");
21 System.out.println("Your name is " name);
22 sum = a b c;
23 System.out.println( a " " b " " c " = " sum );
24 }
25 }
你应该看到什么
代码语言:javascript复制Getting name and three numbers from file...done. Your name is Samantha Showalter
5 6 7 = 18
你知道 Scanner 对象不一定要从键盘上的人那里获取输入吗?它也可以从文本文件中读取数据!
我们只是稍微不同地创建了 Scanner 对象:不再使用System.in
作为参数,而是使用new File("blah.txt")
。这将以只读方式打开文本文件。我选择称之为 fileIn 的 Scanner 对象将附加到文件上,就像吸管插入果汁盒一样。(果汁盒就是文本文件,Scanner 对象就是吸管。)
第 14 行看起来相当无聊。它“暂停”程序并从 Scanner 对象中读取一个字符串,这个字符串来自文件。这个来自文件的字符串被存储到变量中。
第 15 到 17 行也很简单。除了从文件中读取的内容在放入变量之前被转换为整数。
如果文件中的下一个内容不是整数会怎样?那么你的程序将崩溃。现在你不能再责怪人类了:你创建了这个文件。你的工作是确保你知道里面有什么值,以及顺序是什么。
在第 18 行,文件被关闭,这意味着你的 Scanner 对象不再与它连接。这比你预期的要容易吗?希望是这样。
学习演练
- 打开文本文件并更改名称或数字。保存它。然后再次运行程序(您不必重新编译它;代码没有更改,直到运行程序时它才会打开文件)。
练习 43:保存最高分
现在你知道如何从文件中获取信息以及如何将信息放入文件,我们可以创建一个保存最高分的游戏!
这是之前几个练习中的抛硬币游戏,但现在高分保存在运行之间。
代码语言:javascript复制 1 import java.util.Scanner;
2 import java.io.File;
3 import java.io.FileWriter;
4 import java.io.PrintWriter;
5
6 public class CoinFlipSaved
7 {
8 public static void main( String[] args ) throws Exception
9 {
10 Scanner keyboard = new Scanner(System.in);
11
12 String coin, again, bestName, saveFile = "coinflipscore.txt";
13 int flip, streak = 0, best;
14
15 File in = new File(saveFile);
16 if ( in.createNewFile() )
17 {
18 System.out.println("Save game file doesn't exist. Created.");
19 best = 1;
20 bestName = "";
21 }
22 else
23 {
24 Scanner input = new Scanner(in);
25 bestName = input.next();
26 best = input.nextInt();
27 input.close();
28 System.out.println("High score is " best " flips in a row by "
bestName );
29 }
30
31
32 do
33 {
34 flip = 1 (int)(Math.random()*2);
35
36 if ( flip == 1 )
37 coin = "HEADS";
38 else
39 coin = "TAILS";
40
41 System.out.println( "You flip a coin and it is... " coin );
42
43 if ( flip == 1 )
44 {
45 streak ;
46 System.out.println( "tThat's " streak " in a row...." );
47 System.out.print( "tWould you like to flip again (y/n)? " );
48 again = keyboard.next();
49 }
50 else
51 {
52 streak = 0;
53 again = "n";
54 }
55 } while ( again.equals("y") );
56
57 System.out.println( "Final score: " streak );
58
59 if ( streak > best )
60 {
61 System.out.println("That's a new high score!");
62 System.out.print("Your name: ");
63 bestName = keyboard.next();
64 best = streak;
65 }
66 else if ( streak == best )
67 {
68 System.out.println("That ties the high score. Cool.");
69 }
70 else
71 {
72 System.out.println("You'll have to do better than " streak "
if you want a high score.");
73 }
74
75 // Save this name and high score to the file.
76 PrintWriter out = new PrintWriter( new FileWriter(saveFile) );
77 out.println(bestName);
78 out.println(best);
79 out.close();
80 }
81 }
你应该看到什么
代码语言:javascript复制Save game file doesn't exist. Created. You flip a coin and it is... HEADS
That's 1 in a row....
Would you like to flip again (y/n)? y You flip a coin and it is. HEADS
That's 2 in a row....
Would you like to flip again (y/n)? y You flip a coin and it is. HEADS
That's 3 in a row....
Would you like to flip again (y/n)? n Final score: 3
That's a new high score! Your name: Mitchell
(好吧,我作弊了。我尝试了很多次才连续三次猜对。)
在第 15 行,我们使用文件名coin-flip-score.txt
创建了一个File
对象。即使文件不存在,我们也可以这样做。
在第 16 行有一个if
语句,在条件中我调用了 File 对象的createNewFile()
方法。这将检查文件是否存在。如果是,它将什么也不做并返回布尔值false
。如果文件不存在,它将创建一个空文件并返回值true
。
当if
语句为真时,这意味着保存游戏文件不存在。我们会这样说,并将合适的初始值放入变量 best 和 bestName 中。如果不是,那么已经有一个文件存在,所以我们使用 Scanner 对象从文件中获取现有的名称和最高分。很酷,对吧?
第 32 到 57 行是现有的抛硬币游戏。我一点也没有改变这段代码。
在第 59 行,我们需要弄清楚他们是否打破了最高分。如果是,我们会打印出相应的消息并让他们输入他们的名字。
如果他们打破了最高分,我们会这样说,但他们不会因此而得到任何名声。
并且在第 70 行,如果他们没有打破或并列最高分,else
将运行。所以我们当然会嘲笑他们。
在第 75 到 79 行,我们将当前的最高分以及最高得分者的名字保存到文件中。这可能是一个新分数,也可能是我们在程序开始时读取的先前值。
学习演练
- 更改程序,只有在高分发生变化时才保存到高分文件。
- 通过在文本编辑器中打开高分文件并手动更改它来“黑客”高分文件。用您惊人的幸运连胜给朋友留下深刻印象!
练习 44:使用 for 循环计数
正如您在以前的练习中看到的,while
循环和 do-while 循环可以用来多次执行某些操作。
但是,这两种循环都设计成只要条件为真就继续进行。如果我们事先知道要做某事的次数,Java 有一种专门设计用于改变变量值的循环:for
循环。
1 import java.util.Scanner;
2
3 public class CountingFor
4 {
5 public static void main( String[] args )
6 {
7 Scanner keyboard = new Scanner(System.in);
8
9 int n;
10 String message;
11
12 System.out.println( "Type in a message, and I'll display it five
times." );
13 System.out.print( "Message: " );
14 message = keyboard.nextLine();
15
16 for ( n = 1 ; n <= 5 ; n )
17 {
18 System.out.println( n ". " message );
19 }
20
21 System.out.println( "nNow I'll display it ten times and count by 5s."
);
22 for ( n = 5 ; n <= 50 ; n = 5 )
23 {
24 System.out.println( n ". " message );
25 }
26
27 System.out.println( "nFinally, three times counting backward." );
28 for ( n = 3 ; n > 0 ; n = 1 )
29 {
30 System.out.println( n ". " message );
31 }
32
33 }
34 }
你应该看到什么
代码语言:javascript复制Type in a message, and I'll display it five times.
Message: Howdy, y'all!
1. Howdy, y'all!
2. Howdy, y'all!
3. Howdy, y'all!
4. Howdy, y'all!
5. Howdy, y'all!
Now I'll display it ten times and count by 5s.
5. Howdy, y'all!
10. Howdy, y'all!
15. Howdy, y'all!
20. Howdy, y'all!
25. Howdy, y'all!
30. Howdy, y'all!
35. Howdy, y'all!
40. Howdy, y'all!
45. Howdy, y'all!
50. Howdy, y'all!
Finally, three times counting backward.
3. Howdy, y'all!
2. Howdy, y'all!
1. Howdy, y'all!
第 16 行演示了一个非常基本的for
循环。每个for
循环都有三个部分,它们之间用分号分隔。
第一部分(n=1
)无论循环重复多少次,都只会发生一次。它发生在循环的最开始,并且通常为将要用于控制循环的某个变量设置一个起始值。在这种情况下,我们的“循环控制变量”是 n,它将以1
的值开始。
第二部分(n <= 5
)是一个条件,就像while
或 do-while 循环的条件一样。for
循环是一个前测试循环,就像while
循环一样,这意味着在循环开始之前会测试这个条件。如果条件为真,循环体将执行一次。如果条件为假,循环体将被跳过,循环结束。
第三部分(n
)在每次循环迭代之后运行,就在再次检查条件之前。请记住,
会将变量加一。
因此,如果我们展开这个循环,这些语句将会发生并按顺序执行:
代码语言:javascript复制n = 1;
// check if ( n <= 5 ), which is true System.out.println( 1 "." message ); n ; // so now n is 2
// check if ( n <= 5 ), which is true System.out.println( 2 "." message ); n ; // so now n is 3
// check if ( n <= 5 ), which is true System.out.println( 3 "." message ); n ; // so now n is 4
// check if ( n <= 5 ), which is true System.out.println( 4 "." message ); n ; // so now n is 5
// check if ( n <= 5 ), which is true System.out.println( 5 "." message ); n ; // so now n is 6
// check if ( n <= 6 ), which is false. The loop stops
注意,第一部分只发生了一次,第三部分发生的次数正好与循环体发生的次数一样多。
在第 22 行有另一个for
循环。循环控制变量仍然是 n。(请注意,循环控制变量出现在循环的所有三个部分中。这几乎总是这种情况。)
第一部分(“初始化表达式”)将循环控制变量设置为5
。然后第二部分检查 n 是否小于或等于50
。如果是,循环体将执行一次,然后执行第三部分。第三部分将5
添加到循环控制变量中,然后再次检查条件。如果条件仍然为真,循环将重复。一旦条件为假,循环停止。
在第 28 行有一个最后的for
循环。这次循环控制变量从3
开始,只要 n 大于零,循环就会重复。并且在循环体的每次迭代之后,第三部分(“更新表达式”)会从循环控制变量中减去1
。
那么何时应该使用for
循环而不是while
循环?
当我们事先知道要做某事的次数时,最好使用for
循环。
- 做这件事十次。
- 做这件事五次。
- 选择一个随机数,并执行相应次数。
- 拿这个物品清单,对列表中的每个物品执行一次。
另一方面,
while
和 do-while 循环最适合在条件为真时重复执行: - 只要他们没有猜到,就继续进行。
- 只要你没有得到对子,就继续进行。
- 只要他们继续输入负数,就继续进行。
- 只要他们没有输入零,就继续进行。
学习演练
- 从第三个循环中删除第一部分(“初始化表达式”)。如果您正确删除它,它仍将编译。当您运行它时会发生什么?
练习 45:凯撒密码(遍历字符串)
凯撒密码是一种非常简单的密码学形式,以朱利叶斯·凯撒命名,他用它来保护他的私人信件。在密码中,每个字母都会按照字母表中的某个数量上下移动。例如,如果移位是2
,那么消息中的所有A
都会被替换为C
,B
被替换为D
,依此类推。
1 import java.util.Scanner;
2
3 public class CaesarCipher
4 {
5 /**
6 * Returns the character shifted by the given number of letters.
7 */
8 public static char shiftLetter( char c, int n )
9 {
10 int ch = c;
11
12 if ( ! Character.isLetter(c) )
13 return c;
14
15 ch = ch n;
16 if ( Character.isUpperCase(c) && ch > 'Z' || Character.isLowerCase(c) && ch > 'z' )
17 ch = 26;
18 if ( Character.isUpperCase(c) && ch < 'A' || Character.isLowerCase(c) && ch < 'a' )
19 ch = 26;
20
21 return (char)ch;
22 }
23
24 public static void main( String[] args )
25 {
26 Scanner keyboard = new Scanner(System.in);
27 String plaintext, cipher = "";
28 int shift;
29
30 System.out.print("Message: ");
31 plaintext = keyboard.nextLine();
32 System.out.print("Shift (026): ");
33 shift = keyboard.nextInt();
34
35 for ( int i=0; i<plaintext.length(); i )
36 {
37 cipher = shiftLetter( plaintext.charAt(i), shift );
38 }
39 System.out.println( cipher );
40
41 }
42 }
你应该看到什么
代码语言:javascript复制Message: This is a test. XyZaBcDeF Shift (026): 2
Vjku ku c vguv. ZaBcDeFgH
你知道main()
不一定要是类中的第一个函数吗?好吧,它不是。函数可以以任何顺序出现。
除了int
,double
,String
和boolean
之外,还有一种基本的变量类型我没有提到:char
。char
变量可以像String
一样保存字符,但一次只能保存一个字符。代码中的字符串文字用双引号括起来,如"Axe"
,而代码中的char
文字用单引号括起来,如'A'
。
从第 8 行开始有一个名为shiftLetter()
的函数。它有两个参数:c 是要移动的字符,n 是要移动的空格数。这个函数返回一个char
。所以shiftLetter('A', 2)
将返回字符'C'
。
我们不想尝试移动任何不是字母的东西,所以在第 10 行我们使用内置的 Character 类
告诉我们。
然后我们将对字符进行一些简单的数学运算,所以我们在第 13 行将字符的 Unicode 值存储到一个int
中,以便更容易进行操作。然后在第 15 行,我们将所需的偏移量添加到字符上。
这就是它,除了我们希望偏移“环绕”,所以第 16 到 19 行确保最终值仍然是一个字母。最后在第 21 行,我们取 ch 的值,将其转换为char
,并返回它。
在main()
中,第 27 到 34 行非常无聊。在我解释for
循环之前,我需要解释两个String
类方法:charAt()
和length()
。
如果你有一个String
,你可以使用charAt()
方法从中获取一个单独的char
。就像这样:
String s = "Howdy"; char c = s.charAt(2);
// Now c == 'w'
// s.length() is 5
charAt()
是从零开始的,所以s.charAt(0)
从字符串 s 中获取第一个字符。如果s.length()
告诉你 s 中有多少个字符,那么s.charAt(s.length()1)
获取最后一个字符。
现在我们可以理解第 36 行的for
循环了。初始化表达式声明并设置了一个循环控制变量 i,将其设置为0
。条件是只要 i 小于消息中的字符数。更新表达式将每次将1
添加到 i。
在第 38 行,发生了很多事情。我们使用charAt
方法只提取消息的第 i 个字符。该字符和移位值作为参数传递给shiftLetter()
函数,该函数返回移位后的字母。最后,该移位后的字母被添加到 String cipher 的末尾。
当循环结束时,它已经逐个遍历了消息的每个字母,并从字母的移位版本中构建了一个新的消息。
也许这一次太多了。让我知道。
学习演练
- 制作这个练习的新版本,从文本文件中获取消息,并创建一个“加密”文件,而不仅仅是在屏幕上打印它。
练习 46:嵌套 for 循环
在编程中,“嵌套”一词通常意味着将某物放在同一物体内。“嵌套循环”将是两个循环,一个在另一个内部。如果你做对了,那么内部循环将在外部循环执行一次迭代时重复所有迭代。
代码语言:javascript复制 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 }
你应该看到什么
代码语言:javascript复制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 美元,那么他应该购买多少只动物?”
他离开后,我想了几秒钟,然后写了以下程序。
代码语言:javascript复制 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 }
你应该看到什么
代码语言:javascript复制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 中,“数组”是一种类型的变量,它有一个名称(“标识符”),但包含多个变量。在我看来,只有当你能够处理数组时,你才能成为一个真正的程序员。所以,这是个好消息。你快要成功了!
代码语言:javascript复制 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 }
你应该看到什么
代码语言:javascript复制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:在数组中查找东西
更多关于数组的内容!在这个练习中,我们将研究如何找到特定的值。我们在这里使用的技术有时被称为“线性搜索”,因为它从数组的第一个槽开始查找,然后移动到第二个槽,然后是第三个,依此类推。
代码语言:javascript复制 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 }
你应该看到什么
代码语言:javascript复制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:说数组中没有某个东西
在生活中,某些类型的陈述之间存在一般缺乏对称性。
存在一只白色的乌鸦。
这个陈述很容易证明。开始观察乌鸦。一旦找到一只白色的,就停下。完成。
不存在白色的乌鸦。
这个陈述要难得多,因为要证明它,我们必须收集世界上所有符合乌鸦资格的东西。如果我们已经全部看过它们,却没有找到任何白色的乌鸦,那么我们才能安全地说没有存在。
希望您尝试了昨天练习中的学习演练。
代码语言:javascript复制 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 }
你应该看到什么
代码语言:javascript复制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 个值,初始化列表就不好玩了。
- 如果我们希望数组中的值来自文件或者我们在输入代码时没有的其他地方,初始化列表就帮不上忙了。
所以还有另一种方法可以存储数组中的值并访问它们。事实上,这种方法比你一直在做的更常见。使用方括号和槽号,我们可以单独访问数组的槽。
代码语言:javascript复制 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 }
你应该看到什么
代码语言:javascript复制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 个温度。你可以在这里看到这些数字:
- http://learnjavathehardway.org/txt/avgdailytempsatx.txt
这些值是以华氏度为单位。这个练习将把文件中的所有值(甚至直接从互联网上)读入一个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 }
你应该看到什么
代码语言:javascript复制6717 temperatures in database.
The lowest average daily temperature was 22.1F (5.499999999999999C)
(如果你必须在没有互联网访问权限的机器上运行这个程序,那么这段代码就行不通了。因为你已经知道如何从文本文件中读取数据,你可以自己修改它,让它从一个本地文件中读取(一个与你的代码在同一个文件夹中的文件,而不是在互联网上)。但如果你懒得动手,我在下面列出了一个备用版本。)
嗯,一开始我就扔了一个曲线球。在第 8 行,我们声明了一个名为double
的数组
温度,但是不是像这样简单地设置它的容量:
代码语言:javascript复制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 程序。)
代码语言:javascript复制 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 }
你应该看到什么
代码语言:javascript复制191 Marigold Lane
- 这一切只有一个问题:Java 实际上并没有记录。事实证明,如果你创建一个没有方法,只有公共变量的嵌套类,它就像一个结构一样工作,即使它不是 Java 的方式。
我不在乎这是否是 Java 的方式。我已经教了很多学生,我坚信如果你不先理解记录,就很难理解面向对象的编程。
如果你不先理解记录,就很难理解面向对象的编程。所以我要以一种完全正常的方式伪造它们,这在许多不同的编程语言中都是非常好的代码。
一些顽固的面向对象的 Java 爱好者会偶然发现这个练习,并给我发送一封恶毒的电子邮件,说我做错了,为什么我要用谎言来填充这些可怜的孩子的头脑?哦,好吧。
代码语言:javascript复制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:从文件中读取记录
这个练习将向您展示如何从文本文件中读取记录的值。 还有一个示例,演示了一个循环,该循环会读取整个文件,无论文件有多长。
如果你在一个没有连接到互联网的机器上运行这个程序,这段代码将无法正常工作,尽管更改非常小。 该代码访问此文件,如果需要,您可以下载该文件。
- http://learnjavathehardway.org/txt/s01e01cast.txt
将以下代码键入名为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 }
你应该看到什么
代码语言:javascript复制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 只保存最近的记录。所有其他记录仍然在文件中,并且已经显示在屏幕上,但它们的值目前没有保存在任何变量中。
我们可以修复这个问题,但要等到下一个练习。