第 2 章 变量和运算符

第 2 章 变量和运算符

本章将介绍如何用变量和运算符来编写语句。变量用于存储数字、单词等值,而运算符是执行计算的符号。另外,本章还将介绍三种编程错误,并提供其他的调试建议。

2.1 声明变量

编程语言最强大的功能之一是能够定义和操作变量(variable)。变量是存储(value)的命名位置,其中的值可以是数字、文本、图像、声音和其他类型的数据。要存储值,得先声明变量。

String message;

这条语句是一个声明(declaration),因为它声明变量 message 的类型为 String。每个变量都有类型(type),决定了它可以存储什么样的值。例如,类型为 int 的变量可存储整数,而类型为 char 的变量可存储字符。

有些类型名的首字母大写,有些类型名的首字母小写。这种差别的含义将在后文中介绍,就目前而言,你只需要确保首字母大小写正确即可,因为没有类型 Int,也没有类型 string

要声明整型变量,可用如下语法:

int x;

其中的 x 是一个随便指定的变量名。一般而言,使用的名称应指出变量的含义。例如,看到下面的声明,你可能就能猜出各个变量将存储什么值:

String firstName;
String lastName;
int hour, minute;

这个示例声明了四个变量,其中两个的类型为 String,另外两个的类型为 int。根据约定,对于包含多个单词的变量名,如 firstName,应将每个单词的首字母大写,但第一个单词除外。变量名是区分大小写的,因此,firstNamefirstnameFirstName 指的是不同的变量。

这个示例还演示了在一行中声明多个同类变量的语法:hourminute 都是 int 变量。请注意,每条声明语句都以分号结尾。

你可以随便给变量命名,但大约有 50 个被称为关键词(keyword)的保留词不能用作变量名。这些关键词包括 publicclassstaticvoidint,被编译器用来分析程序的结构。

有关完整的关键词清单,请参阅 http://docs.oracle.com/javase/tutorial/java/nutsandbolts/_keywords.html,但不必记住它们。大多数编程编辑器都提供了“语法突出”的功能,即用不同的颜色显示程序的不同部分。

2.2 赋值

声明变量后,即可用它们来存储值。为此,可用赋值(assignment)语句:

message = "Hello!"; // 给变量message指定值"Hello!"
hour = 11;          // 将值11赋给变量hour
minute = 59;        // 将变量minute的值设置为59

这个示例包含三条赋值语句,其中的注释指出了三种解读赋值语句的方式。这里使用的术语可能令人迷惑,但涉及的概念简单易懂。

  • 当声明变量时,便创建了一个命名的存储位置。

  • 当给变量赋值时,便修改了它的值。

一般而言,变量和赋给它的值必须是相同的类型。例如,你不能将字符串存储到变量 mintue 中,也不能将整数存储到变量 message 中。在本书的后文中,你将看到一些违反这条规则的示例。

有些字符串看起来像是整数,但其实不是整数,这常常令人迷惑。例如,变量 message 可包含字符串 "123",这个字符串由字符 '1''2''3' 组成,与整数 123 不是同一码事。

message = "123"; // 合法
message = 123;   // 非法

使用变量前,必须对其进行初始化(initialize,首次赋值)。你可以像前一个示例那样,先声明变量,再赋值;也可以在声明变量的同时给它赋值:

String message = "Hello!";
int hour = 11;
int minute = 59;

2.3 状态图

鉴于 Java 用符号“=”来赋值,你可能会认为语句 a=b 是一个相等声明,但事实并非如此!

相等具有交换性,但赋值并非如此。例如,在数学中,如果 a=7,则 7=a;而在 Java 中,a=7; 是一条合法的赋值语句,但 7=a; 则不是,因为赋值语句的左边必须是变量名(存储位置)。

另外,在数学中,相等声明在任何情况下都成立。如果当前 a=b,那么在任何情况下 ab 都相等;而在 Java 中,赋值语句可能导致两个变量相等,但它们并不一定始终如此。

int a = 5;
int b = a;     // 现在a和b相等
a = 3;         // a和b不再相等

第三行代码修改了 a 的值,但没有修改 b 的值,因此它们不再相等。

程序中的所有变量及其当前值一同组成了程序的状态(state)。图 2-1 显示了程序在上述三条语句运行后的状态。

{%}

图 2-1:变量 a 和 b 的状态图

显示程序状态的图被称为状态图(state diagram)。每个变量都用一个方框表示,方框内是变量的值,方框外是变量名。状态随程序的运行而变化,因此,应将状态图视为程序执行过程中特定时点的快照。

2.4 显示变量

可用 printprintln 显示变量的值。下面的语句声明了一个名为 firstLine 的变量,将值 "Hello, again!" 赋给它,并显示这个值:

String firstLine = "Hello, again!";
System.out.println(firstLine);

在说显示变量时,我们通常指的是显示变量的。要显示变量的名称,必须将其用引号括起。

System.out.print("The value of firstLine is ");
System.out.println(firstLine);

这个示例的输出如下:

The value of firstLine is Hello, again!

不管变量的类型如何,显示其值的语法都相同。例如:

int hour = 11;
int minute = 59;
System.out.print("The current time is ");
System.out.print(hour);
System.out.print(":");
System.out.print(minute);
System.out.println(".");

这个程序的输出如下:

The current time is 11:59.

要在同一行输出多个值,通常使用多条 print 语句,并在最后使用一条 println 语句。千万不要忘了 println 语句!很多计算机都将来自 print 的输出存储起来,等遇到 println 后才将整行输出一次性显示出来。如果省略了 println,程序可能在意想不到的时候显示存储的输出,甚至直到结束也不显示任何输出。

2.5 算术运算符

算符(operator)是表示简单计算的符号,例如,加法运算符为 +,减法运算符为 -,乘法运算符为 *,而除法运算符为 /

下面的程序将时间转换为分钟数:

int hour = 11;
int minute = 59;
System.out.print("Number of minutes since midnight: ");
System.out.println(hour * 60 + minute);

在这个程序中,hour * 60 + minute 是一个表达式(expression),表示计算将得到的单个值。这个程序运行时,每个变量都被替换为当前值,再执行运算符指定的计算。运算符使用的值称为操作数(operand)。

前述示例的结果如下:

Number of minutes since midnight: 719

表达式通常由数字、变量和运算符组成。程序编译并执行时,表达式将变成单个值。

例如,表达式 1 + 1 的值为 2。对于表达式 hour - 1,Java 将变量 hour 替换为其当前值,因此这个表达式变成 11 - 1,结果为 10。对于表达式 hour * 60 + minute,其中的两个变量都被替换了,整个表达式变为 11 * 60 + 59。先执行乘法运算,因此这个表达式变为 660 + 59;再执行加法运算,结果为 719

加法、减法、乘法运算都与你的预期一样,但除法运算可能会让你感到意外。例如,下面的代码片段试图将分钟数转换为小时数:

System.out.print("Fraction of the hour that has passed: ");
System.out.println(minute / 60);

其输出如下:

Fraction of the hour that has passed: 0

这样的结果令人感到迷惑。变量 minute 的值为 59,59 除以 60 的结果应为 0.98333,而不是 0。问题在于 Java 在两个操作数都为整数时执行“整数除法”,而根据设计,整数除法总是向下圆整,即便这里的结果更接近下一个整数时也是如此。

一种替代方式是,计算百分比而不是小数:

System.out.print("Percent of the hour that has passed: ");
System.out.println(minute * 100 / 60);

上述代码的输出如下:

Percent of the hour that has passed: 98

同样,结果也被向下圆整了,但至少离正确的答案更近了。

2.6 浮点数

一种更通用的解决方案是使用浮点(floating-point)数,它可用于表示小数,也可用于表示整数。在 Java 中,默认的浮点类型为 double,它指的是双精度浮点数。double 变量的声明和赋值语法与其他类型的变量相同:

double pi;
pi = 3.14159;

只要有一个操作数为 double 值,Java 就执行“浮点除法”,因此,我们可以用如下方式来解决 2.5 节中的问题:

double minute = 59.0;
System.out.print("Fraction of the hour that has passed: ");
System.out.println(minute / 60.0);

输出如下:

Fraction of the hour that has passed: 0.9833333333333333

虽然浮点数很有用,但也可能让人感到迷惑。例如,Java 区分整数 1 和浮点数 1.0,即使它们看起来是同一个数字。它们属于不同的数据类型,而严格来说,你不能将一种类型的值赋给另一种类型的变量。

下面的语句是非法的,因为左边是一个 int 变量,而右边是一个 double 值:

int x = 1.1; // 编译错误

这种规则很容易忘记,因为 Java 在很多情况下会自动转换类型:

double y = 1; // 合法,但这是一种糟糕的做法

这个示例原本应该是非法的,但由于 Java 自动将 int1 转换为 double1.0,使得这个示例变得合法了。这样的宽容是十分便利的,但常会给初学者带来问题,例如:

double y = 1 / 3; // 常见的错误

你可能会认为变量 y 的值为 0.333333——一个合法的浮点值,但实际上其值为 0。右边的表达式将两个整数相除,因此 Java 执行整数除法,结果为 int0。这个结果被转换为 double0.0,再赋给变量 y

对于这种问题,其中一种解决方案是将右边的表达式变成浮点表达式,如下所示,这样变量 y 将像预期的那样被设置为 0.333333

double y = 1.0 / 3.0; // 正确

作为一种编程风格,在任何情况下都应将浮点值赋给浮点变量。编译器并没有要求必须这样做,但如果不这样做的话,不知什么时候一个简单的错误就可能阴魂不散,给你带来麻烦。

2.7 舍入误差

大多数浮点数只能大致正确地表示。有些数字,如果不是特别大的整数,可以准确地表示。但循环小数(如 1/3)和无理数(如 π)不能准确地表示。为表示这些数字,计算机必须将其舍入到最接近的浮点数。

所需数字和实际得到的浮点数之间的差称为舍入误差(rounding error)。例如,以下两条语句应该是等价的:

System.out.println(0.1 * 10);
System.out.println(0.1 + 0.1 + 0.1 + 0.1 + 0.1
                 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1);

但在很多计算机上,它们的输出如下:

1.0
0.9999999999999999

原因是 0.1 在十进制中为有限小数,但在二进制中为循环小数,因此其浮点数表示只是近似的。将近似值相加会逐渐累积舍入误差。

在很多应用领域,如计算机图形学、加密、统计分析和多媒体渲染,使用浮点数算术利大于弊。但如果要求绝对精确,应使用整数。例如,查看余额为 123.45 美元的银行账户:

double balance = 123.45; // 可能存在舍入误差

在这个示例中,随着不断地对变量执行算术运算(如存款和取款),它存储的值将不再准确。这可能会激怒顾客,甚至引发诉讼。为避免这种问题,可以用整数来表示余额:

int balance = 12345; // 美分数

只要美分数不超过变量 int 可表示的最大值(约 20 亿),就可以使用这种解决方案。

2.8 字符串运算符

一般而言,不能对字符串执行数学运算,即便对那些看起来像数字的字符串亦是如此。下面的表达式是非法的:

"Hello" - 1     "World" / 123    "Hello" * "World"

运算符 + 可用于字符串,但其所作所为可能出乎意料。用于字符串时,运算符 + 执行串接(concatenation),即首尾相连,因此 "Hello, " + "World!" 的结果为字符串 "Hello, World!"

再举一个例子。如果你声明了类型为 String 的变量 name,则表达式 "Hello, " + name 会将变量 name 的值附加在字符串 hello 的后面,从而生成个性化的问候。

鉴于对数字和字符串都定义了加法运算,因此 Java 可能执行意料之外的自动转换:

System.out.println(1 + 2 + "Hello");
// 输出为3Hello

System.out.println("Hello" + 1 + 2);
// 输出为Hello12

Java 按从左到右的顺序执行这些运算。在第 1 行中,1 + 2 等于 3,而 3 + "Hello" 的结果为 "3Hello";在第 2 行中,"Hello" + 1 的结果为 "Hello1",而 "Hello1" + 2 的结果为 "Hello12"

表达式包含多个运算符时,将根据运算顺序(order of operation)计算表达式。一般而言,Java 按从左到右的顺序执行运算(如 2.7 节所示),但对于数值运算符,Java 遵循如下的数学规则。

  • 乘除运算的优先级高于加减运算,这意味着先乘除后加减。因此 1 + 2 * 3 的结果为 7,而不是 9,而 2 + 4 / 2 的结果为 4,而不是 3

  • 运算符的优先级相同时,按从左到右的顺序执行。因此,表达式 minute * 100 / 60 先执行乘法运算;如果 minute 的值为 59,这个表达式将变为 5900 / 60,结果为 98。如果按从右到左的顺序执行这些运算,将得到错误的结果 59 * 1

  • 要想改变默认的运算优先级或对默认的运算优先级不太确定时,可使用括号。首先计算括号内的表达式,因此 (1 + 2) * 3 的结果为 9。还可用括号让表达式更容易理解,如 (minute * 100) / 60,虽然就这个表达式而言,用不用括号对结果并没有影响。

别费劲地去记运算符的优先级,尤其是除算术运算符外的其他运算符。如果表达式的含义不那么明显,可用括号让它清晰起来。

2.9 组合

前面分别介绍了编程语言的一些元素——变量、表达式和语句,但没有讨论如何结合使用它们。

编程语言最有用的功能之一是能够组合(compose)小型构件。例如,在知道如何将数字相乘以及如何显示值后,我们可以将这些操作放在一条语句中:

System.out.println(17 * 3);

任何算术表达式都可用于打印语句中,我们见过这样的例子:

System.out.println(hour * 60 + minute);

还可将表达式放在赋值语句的右边:

int percentage;
percentage = (minute * 100) / 60;

赋值语句的左边必须是变量名,不能是表达式,这是因为赋值语句的左边要指定将结果放在什么地方,而表达式表示的并非存储位置。

hour = minute + 1;  // 正确
minute + 1 = hour;  // 导致编译错误

就目前而言,能够将操作组合起来好像没什么大不了的,但在本书的后文中你将了解到,这让我们能够编写简洁的代码以执行复杂的计算。不过,也别忘乎所以,冗长而复杂的表达式可能会难以理解和调试。

2.10 错误类型

程序中可能出现的错误有三种:编译时错误、运行时错误和逻辑错误。区分这些错误可以更快地找出错误。

编译时错误(compile-time error)指的是因违反 Java 语法(syntax)规则而导致的错误。例如,括号和大括号必须成对出现,所以 (1 + 2) 是合法的,而 8) 是非法的。8) 导致程序无法编译,而编译器将显示一条错误消息。

编译器显示的错误消息通常会指出错误出现在程序的什么地方,有时还可以准确地指出错误。我们来重温一下第 1 章中的 Hello World 程序。

public class Hello {

    public static void main(String[] args) {
        // 生成一些简单的输出
        System.out.println("Hello, World!");
    }
}

如果遗漏了打印语句末尾的分号,将出现类似于以下的错误消息:

File: Hello.java  [line: 5]
Error: ';' expected

真是太好了:这条错误消息准确地指出了错误的位置,还指出了是什么样的错误。

然而,并非所有的错误消息都是容易理解的。有时编译器报告的错误位置不准确;有时对错误的描述模棱两可,几乎没什么帮助。

例如,如果遗漏了方法 main 末尾(第 6 行)的右大括号,可能出现类似于以下的错误消息:

File: Hello.java  [line: 7]
Error: reached end of file while parsing

这里有两个问题。首先,这条错误消息是从编译器的角度而不是你的角度生成的。分析(parsing)指的是在转换前读取程序的过程;如果编译器到达文件末尾后分析还在进行的话,那么就意味着程序遗漏了什么东西,但编译器不知道遗漏了什么,也不知道在何处遗漏的。它认为错误发生在程序末尾(第 7 行),但遗漏的大括号应该在前一行。

错误消息提供了很有用的信息,你应尽力阅读并理解它们,但也不能将它们奉为圭臬。

刚从事编程的几周内,你可能会为找出编译错误花费大量的时间,但随着经验越来越丰富,你犯的错误将越来越少,找出错误的速度也将越来越快。

第二种错误是运行时错误(run-time error),因其要等到程序运行后才会出现而得名。在 Java 中,这种错误发生在解释器执行字节码期间,也被称为异常,因为它们通常表明出现了异常而糟糕的情况。

本书前几章的简单程序中很少出现运行时错误,因此可能需要过段时间才能见到它们。运行时错误发生时,解释器将显示一条错误消息,指出在什么地方出现了什么问题。

例如,如果你不小心将零用作了除数,将出现类似于以下的错误消息:

Exception in thread "main" java.lang.ArithmeticException: / by zero at Hello.main(Hello.java:5)

上述的输出对调试很有帮助。第 1 行指出了异常的名称 ——java.lang.ArithmeticException,还具体地指出了发生的情况——/ by zero(除以零)。接下来的一行指出了问题所在的方法;Hello.main 指的是 Hello 类的方法 main;还指出了这个方法是在哪个文件(Hello.java)中定义的以及问题出现在第几行(5)。

有些错误消息还包含无意义的信息,因此你面临的挑战之一是确定有用的部分,而不被多余的信息搞得不知所措。另外别忘了,导致程序崩溃的代码行可能并不是需要修改的代码行。

第三种错误是逻辑错误(logic error)。存在逻辑错误的程序能够通过编译,且运行时不会出现错误消息,但不会做正确的事。相反,你让它怎么做,它就怎么做。例如,下面这个版本的 Hello World 程序存在一个逻辑错误:

public class Hello {

    public static void main(String[] args) {
        System.out.println("Hello, ");
        System.out.println("World!");
    }
}

这个程序能够通过编译并运行,但输出如下:

Hello,
World!

如果我们要在一行中显示全部输出,那么上述输出就不正确。问题出在第 1 行,它用的是 println,而我们原本想用的是 print(参见 1.5 节中的 goodbye world 示例)。

有时很难找出逻辑错误,因为你必须进行反向推导:根据输出结果推断程序行为不正确的原因,并确定如何让它的行为正确无误。编译器和解释器在这方面帮不了你,因为它们并不知道正确的行为是什么样的。

了解这三种错误后,你应该阅读一下附录 C,其中搜集了一些我们最喜欢的调试建议。因为这些建议涉及了一些还未讨论的语言功能,所以你可能需要时不时地再次阅读这个附录。

2.11 术语表

  • 变量

    命名的存储位置。所有变量都有类型,这是在创建变量时声明的。

  • 数字、字符串或其他可存储在变量中的数据。每个值都属于特定的类型,如 intString

  • 声明

    创建变量并指定其类型的语句。

  • 类型

    从数学角度来说,类型是一个值集。变量的类型决定了它可存储哪些值。

  • 关键词

    编译器用来分析程序的保留词。关键词(如 publicclassvoid)不能用作变量名。

  • 赋值

    给变量指定值的语句。

  • 初始化

    首次给变量赋值。

  • 状态

    程序中的变量及其当前值。

  • 状态图

    程序在特定时点的状态的图形表示。

  • 运算符

    表示计算(如加、乘和字符串串接)的符号。

  • 操作数

    运算符操作的值。在 Java 中,大多数运算符需要两个操作数。

  • 表达式

    表示单个值的变量、运算符和值的组合。表达式也有类型,这是由表达式包含的运算符和操作数决定的。

  • 浮点

    一种数据类型,表示包含整数部分和小数部分的数字。在 Java 中,默认的浮点类型为 double

  • 舍入误差

    要表示的数字和与之最接近的浮点数之间的差。

  • 拼接

    将两个值(通常是字符串)首尾相连。

  • 运算顺序

    决定运算顺序执行的规则。

  • 组合

    将简单的表达式和语句合并为复合的表达式和语句。

  • 语法

    程序的结构,即程序包含的单词和符号的排列方式。

  • 编译时错误

    导致源代码无法编译的错误,也叫“语法错误”。

  • 分析

    分析程序的结构,这是编译器做的第一项工作。

  • 运行时错误

    导致程序无法完成运行的错误,也叫“异常”。

  • 逻辑错误

    导致程序的行为不符合程序员预期的错误。

2.12 练习

本章的示例代码位于仓库 ThinkJavaCode 的目录 ch02 中,有关如何下载这个仓库,请参阅前言中的“使用示例代码”一节。做以下的练习前,建议你先编译并运行本章的示例。

如果你还没有阅读 A.2 节,那么现在正是阅读的好时机。该节介绍了 DrJava 的 Interactions 窗格,它提供了极佳的途径,让你无需编写完整的类定义就能开发并测试简短的代码片段。

练习2-1

如果你将本书用作教材的话,可能会很喜欢这个练习。找个同伴一起来玩 Stump the Chump 的游戏吧。

先编写一个能够通过编译并正确运行的程序。一个人在程序中添加一个错误,另一个人不能偷看,然后尝试找出并修复这个错误。在不编译程序的情况下找出错误得两分,求助于编译器找出错误得 1 分,找不出错误对手得 1 分。

练习2-2

这个练习旨在:用字符串拼接显示不同类型(intString)的值;以每次添加几条语句的方式循序渐进地练习程序开发。

(1) 新建一个程序,将其命名为 Date.java。输入或复制类似于程序 Hello World 中的代码,并确保程序能够通过编译并运行。

(2) 仿照 2.4 节中的示例,编写一个创建变量 daydatemonthyear 的程序。变量 day 用于存储星期几(如星期五),date 用于存储日期(如 13 号)。这些变量应声明为何种类型呢?将表示当前日期的值赋给这些变量。

(3) 显示(打印)每个变量的值,且每个变量要独占一行。这是一个中间步骤,有助于确认到目前为止一切正确。编译并运行这个程序,然后再接着往下做。

(4) 修改程序,使其以美国标准格式显示日期,如 Thursday, July 16, 2015

(5) 修改程序,使其以欧洲格式显示日期。最终的输出应类似于以下这样:

American format:
Thursday, July 16, 2015
European format:
Thursday 16 July 2015

练习2-3

这个练习旨在:使用一些算术运算符;考虑用多个值表示复合实体(如时间)。

(1) 新建一个程序,将其命名为 Time.java。从现在开始,我们将不再提醒你先编写一个可运行的小程序,但你应该这样做。

(2) 仿照 2.4 节中的示例,创建变量 hourminutesecond,并将大致表示当前时间的值赋给这些变量。请使用 24 小时制,即如果当前时间是下午两点,就将变量 hour 的值设置为 14。

(3) 让程序计算并显示从午夜开始过去了多少秒。

(4) 计算并显示当天还余下多少秒。

(5) 计算并显示当天已逝去时间的百分比。如果用整数计算百分比,可能会出现问题,因此请考虑使用浮点数。

(6) 根据当前时间修改变量 hourminutesecond 的值,再编写代码来计算从你开始做这个练习算起,已过去了多少时间。

提示:你可能想在计算期间用额外的变量来存储值。只用于计算而不显示的变量被称为“中间变量”或“临时变量”。

目录