《Java核心技术卷一》第三章
Java 区分大小写
类是所有 Java 应用的构建模块,Java 程序中的所有内容都必须放在类中。
类名必须以字母开头,不能使用 Java 保留字作为类名。
标准命名(骆驼命名):类名是以大写字母开头的名词。如果名字由多个单词组成,每个单词的第一个字母都应该大写。
源代码的文件名必须与公共类的类名相同。
访问修饰符 关键字 public
main 方法必须声明为 public 。运行一个已编译的程序时,Java 虚拟机总是从指定类中 main 方法的代码开始执行。
Java 中的所有函数都是某个类的方法,Java 中的 main 方法必须有一个外壳类。
Java 中的 main 方法总是静态的,关键字 void 表示这个方法不返回值。
main 方法不会为操作系统返回一个“退出码”,如果 main 方法正常退出,那么 Java 程序的退出码为0。
在 Java 中,每个语句必须用分号结束,回车不是语句的结束标志。
点号(.)用于调用方法。
Java 中的方法可以没有参数,也可以有一个或多个参数(实参),即使一个方法没有参数,也需要使用空括号。
从//开始到本行结尾都是注释。
/* */表示一段比较长的注释。
注意:/* */ 注释不能嵌套。
Java 是一种强类型语言,必须为每一个变量声明一个类型。
Java 中有一个能够表示任意精度的算术包,所谓的“大数”是 Java 对象,而不是一个基本 Java 类型。
整型
整型用于表示没有小数部分的数,可以是负数。
| 整型 | 存储需求 | 取值范围 |
|---|---|---|
| int | 4字节 | - $2^31$ ~ $2^31$ - 1 |
| short | 2字节 | - $2^15$ ~ $2^15$ - 1 |
| long | 8字节 | - $2^63$ ~ $2^63$ - 1 |
| byte | 1字节 | - $2^7$ ~ $2^7$ -1 |
整型的范围与运行 Java 代码的机器无关,所以各种数据类型的取值范围是固定的。
长整型数值有一个后缀L和l 。十六进制数值有一个前缀0x或0X 。八进制有一个前缀 0 。二进制前缀 0b 或 0B 。
Java 没有无符号形式的 int、long、short、byte。
浮点类型
浮点类型用于表示有小数部分的数值。
double 表示这种类型的数值精度是 float 类型的两倍
float 类型的数值有一个后缀 F 和 f
没有后缀 F 的浮点数值总是默认为 double 类型(后缀 d 或 D )
可以使用十六进制表示浮点数字面量,使用 p 表示指数,尾数采用十六进制,指数采用十进制,指数基数是2 (0.125 写为 0x1.0p-3)
有3个特殊的浮点数值表示溢出和出错情况:正无穷大 、负无穷大、NaN(不是一个数)(所有 NaN 的数值都认为是不同的)
浮点数值不适用于无法接受舍入误差的金融计算
char 类型
char 类型的字面量值要用单引号括起来
char 类型的值可以表示为十六进制值,其范围从 \u0000~\uFFFF。
转义序列 \u 还可以在加引号字符常量和字符串之外使用(而其他所有转义序列不可以。)
可以在加引号的字符字面量或字符串中使用这些转义序列。
Unicode 转义序列会在解析代码之前处理。
一定要当心注释中的 \u。
特殊字符的转义序列
| 转义序列 | 名称 | Unicode值 |
|---|---|---|
| \b | 退格 | \u0008 |
| \t | 制表 | \u0009 |
| \n | 换行 | \u000a |
| \r | 回车 | \u000d |
| \f | 换页 | \u000c |
| \“ | 双引号 | \u0022 |
| \‘ | 单引号 | \u0027 |
| \\ | 反斜线 | \u005c |
| \s | 空格。在文本块中用来保留末尾空白符 | \u0020 |
| \newline | 只在文本块中使用:连接这一行和下一行 |
码点(code point)是指与一个编码表中的某个字符对应的代码值。在 Unicode 标准中,码点采用十六进制书写,并加上前缀 U+ 。Unicode 的码点可以分成17个代码平面(code plane)。第一个代码平面称为基本多语言平面(basic multilingual plane),包括码点从 U+0000 到 U+FFFF 的“经典” Unicode 代码;其余的16个平面的码点从 U+10000 到 U+10FFFF , 包含各种辅助字符(supplementary character)。
UTF-16 编码采用不同长度的代码表示所有 Unicode 码点。在基本多语言平面中,每个字符用16位表示,通常称为代码单元(code unit);而辅助字符编码为一对连续的代码单元。采用这种编码对表示的每个值都属于基本多语言平面中未用的2048个值范围,通常称为替代区域(surrogate area)(U+D800U+DBFF用于第一个代码单元,U+DC00U+DFFF用于第二个代码单元)。
在 Java 中,char 类型描述了采用 UTF-16 编码的一个代码单元。
boolean类型
boolean(布尔)类型有两个值:false 和 true,用来判定逻辑条件。整型值和布尔值之间不能进行相互转换。
变量与常量
Java 使用变量来存储值。常量就是值不变的变量。
在 Java 中,每个变量都有一个类型(type)。声明一个变量时,先指定变量的类型,然后是变量名。注意每个声明都以分号结束。
作为变量名(以及其他名字)的标识符由字母、数字、货币符号以及“标点连接符”组成。需遵循以下规则:
- 第一个字符不能是数字,
@、+之类的符号也不能出现在变量名中,空格也不行; - 字母区分大小写(如
main和Main是不同的标识符); - 标识符的长度基本上没有限制。
与大多数程序设计语言相比,Java 中“字母”和“货币符号”的范围更大:
- 字母:指一种语言中表示字母的任何 Unicode 字符(例如,德国用户可在变量名中使用字母
ß;讲希腊语的人可使用π); - 数字:包括
0-9和表示一位数字的任何 Unicode 字符; - 货币符号:如
$、€、₹等; - 标点连接符:包括下画线
_、“波浪线”~及其他一些符号(实际开发中,多数程序员常用A-Z、a-z、0-9和下画线)。
尽管$是一个合法的标识符字符,但不要在你自己的代码中使用这个字符(它只用于 Java 编译器或其他工具生成的名字)
不能使用 Java 关键字作为变量名
在Java 9 中,单下画线 _是保留字。
可以在一行中声明多个变量。
初始化变量
声明一个变量之后,必须用赋值语句显式地初始化变量,千万不要使用未初始化的变量的值。
要想对一个已声明的变量进行赋值,需要将变量名放在等号(=)左侧,再把一个有适当值的 Java 表达式放在等号的右侧。也可以将变量的声明和初始化放在同一行中。最后,Java 中可以将声明放在代码中的任何地方。
在 Java 中,并不区分变量的声明和定义。
注释:从 Java 10 开始,对于局部变量,如果可以从变量的初始值推断出它的类型,就不再需要声明类型。只需要使用关键字 var 而无须指定类型。
常量
在 Java 中,可以用关键字 final 指示常量。
关键字 final 表示这个变量只能被赋值一次。一旦赋值,就不能再更改了。习惯上,常量名使用全大写。
在 Java 中,可能经常需要创建一个常量以便在一个类的多个方法中使用,通常将这些常量称为类常量(class constant)。可以使用关键字 static final 设置一个类常量。
需要注意的是,类常量的定义位于 main 方法之外。这样一来,同一个类的其他方法也可以使用这个常量。另外,如果一个常量被声明为 public,那么其他类的方法也可以使用这个常量。
const 是 Java 保留的关键字。在 Java 中,必须使用 final 声明常量。
枚举类型
自定义枚举类型(enumerated type)。枚举类型包括有限个命名值。
1 | |
现在,可以声明这种类型的变量:
1 | |
Size类型的变量只能存储这个类型声明中所列的某个值,或者特殊值null,null表示这个变量没有设置任何值。
运算符
在 Java 中,使用通常的算术运算符 +、-、*、/ 分别表示加、减、乘、除运算。当参与 / 运算的两个操作数都是整数时,/ 表示整数除法;否则,这表示浮点除法。整数的求余操作有时称为取模 (modulus))用 % 表示。例如,15/2 等于 7,15%2 等于 1,15.0/2 等于 7.5。
需要注意,整数被 0 除将产生一个异常,而浮点数被 0 除将会得到一个无穷大或 NaN 结果。
Math 类中包含你可能会用到的各种数学函数,这取决于你要编写什么类型的程序。
要想计算一个数的平方根,可以使用 sqrt 方法:
1 | |
注释:println 方法和 sqrt 方法有一个微小的差异。println 方法处理 System.out 对象,而 Math 类中的 sqrt 方法并不处理任何对象,这样的方法被称为静态方法(static method)。
在 Java 中,没有完成幂运算的运算符,因此必须使用 Math 类的 pow 方法。以下语句:
1 | |
将 y 的值设置为 x 的 a 次幂(xᵃ)。pow 方法有两个 double 类型的参数,其返回结果也为 double 类型。
floorMod 方法是为了解决一个长期存在的有关整数余数的问题。
提示:不必在数学方法名和常量名前添加前缀 “Math”,只要在源文件最上面加上下面这行代码就可以了。
1 | |
数据类型转换

在图3-1中有6个实线箭头,表示无信息丢失的转换;另外有3个虚线箭头,表示可能有精度损失的转换。
当用一个二元运算符连接两个值时(例如 n + f,n 是整数,f 是浮点数),先要将两个操作数转换为同一种类型,然后再进行计算。
- 如果两个操作数中有一个是 double 类型,另一个操作数就会转换为 double 类型。
- 否则,如果其中一个操作数是 float 类型,另一个操作数将会转换为 float 类型。
- 否则,如果其中一个操作数是 long 类型,另一个操作数将会转换为 long 类型。
- 否则,两个操作数都将被转换为 int 类型。
强制类型转换
在必要的时候,int 类型的值将会自动地转换为 double 类型。但另一方面,有时也需要将 double 类型转换成 int 类型。在 Java 中,允许进行这种数值转换,不过当然可能会丢失一些信息。这种可能损失信息的转换要通过强制类型转换(cast)来完成。强制类型转换的语法格式是在圆括号中指定想要转换的目标类型,后面紧跟待转换的变量名。例如:
1 | |
这样,变量 nx 的值为9,因为强制类型转换通过截断小数部分将浮点值转换为整型。
如果想舍入(round)(四舍五入) 一个浮点数来得到最接近的整数(大多数情况下,这种操作更有用),可以使用 Math.round 方法:
1 | |
现在,变量 nx 的值为10。调用 round 时,仍然需要使用强制类型转换(int)。原因是round方法的返回值是 long 类型,由于存在信息丢失的可能性,所以只有使用显式的强制类型转换才能够将一个 long 值赋给 int类 型的变量。
警告:如果试图将一个数从一种类型强制转换为另一种类型,而又超出了目标类型的表示范围,结果就会截断成一个完全不同的值。例如,(byte)300实际上会得到44。
赋值
可以在赋值中使用二元运算符,为此有一种很方便的简写形式。例如,
1 | |
等价于:
1 | |
(一般来说,要把运算符放在 = 号左边,如 *= 或 %= )。
警告:如果运算符得到一个值,其类型与左侧操作数的类型不同,就会发生强制类型转换。例如,如果 x 是一个 int,则以下语句
1 | |
是合法的,将把 x 设置为 (int)(x+3.5)。
需要说明,在 Java 中 ,赋值是一个表达式(expression)。也就是说,它有一个值,具体来讲就是所赋的那个值。可以使用这个值完成一些操作,例如,可以把它赋给另一个变量。考虑以下语句:
1 | |
x+=4的值是5,因为这是赋给x的值。然后将这个值赋给y。
在 Java 中,借鉴了 C 和 C++ 中的做法,也提供了自增、自减运算符: n++ 将变量 n 的当前值加1,n– 则将 n 的值减1。由于这些运算符会改变变量的值,所以不能对数值本身应用这些运算符。还有一种“前缀”形式: ++n。后缀和前缀形式都会使变量值加1或减1。但用在表达式中时,二者就有区别了。前缀形式会先完成加1;而后缀形式会使用变量原来的值。在 Java 中,很少在表达式中使用 ++。
关系运算符
Java 包含丰富的关系运算符。要检测相等性,可以使用两个等号==。
1 | |
的值为false。
另外可以使用!=检测不相等。例如,
1 | |
的值为true。
最后,还有经常使用的<(小于)、>(大于)、<=(小于等于)和>=(大于等于)运算符。
Java沿用了C++的做法,使用&&表示逻辑“与”运算符,使用||表示逻辑“或”运算符。从!=运算符可以想到,感叹号!就是逻辑非运算符。&&和||运算符是按照“短路”方式来求值的:如果第一个操作数已经能够确定表达式的值,第二个操作数就不必计算了。而且已经计算得到第一个表达式的真值为false,那么结果就不可能为true。因此,第二个表达式就不必计算了。
Java提供了conditional ?: 运算符,可以根据一个布尔表达式选择一个值。如果条件(condition)为true,表达式
1 | |
就计算为第一个表达式的值,否则为第二个表达式的值。
需要在两个以上的值中做出选择时,可以使用switch表达式(这是Java 14中引入的)。
1 | |
case 标签还可以是字符串或枚举类型常量。
注释:与所有表达式类似,switch表达式也有一个值。注意各个分支中箭头->放在值前面。
注释:在Java 14中,switch有4种形式。
可以为各个case提供多个标签,用逗号分隔:
1 | |
switch表达式中使用枚举常量时,不需要为各个标签提供枚举名,这可以从switch值推导得出。
例如:enum Size { SMALL,MEDIUM,LARGE,EXTRA_LARGE };
1 | |
在这个例子中,完全可以省略default,因为每一个可能的值都有相应的一个case。
警告:使用整数或String操作数的switch表达式必须有一个default,因为不论操作数值是什么,这个表达式都必须生成一个值。
警告:如果操作数为null,会抛出一个NullPointerException。
处理整型类型时,还有一些运算符可以直接处理组成整数的各个位。这意味着可以使用掩码技术得到一个数中的各个位。位运算符包括:
& (“and”) | (“or”) ^ (“xor”) ~ (“not”)
这些运算符按位模式操作。
例如,如果n是一个整数变量,而且n的二进制表示中从右边数第4位为1,则
1 | |
会返回1,否则返回0。利用&并结合适当的2的幂,可以屏蔽其他位,而只留下其中的某一位。
注释:应用在布尔值上时,& 和 | 运算符也会得到一个布尔值。这些运算符与 && 和 || 运算符很类似,不过 & 和 | 运算符不采用“短路”方式来求值,也就是说,计算结果之前,两个操作数都需要计算。
另外,还有<<和>>运算符可以将位模式左移或右移。需要建立位模式来完成位掩码时,这两个运算符会很方便。
最后>>>运算符会用0填充高位,这与>>不同,>>会用符号位填充高位,不存在<<<运算符。
警告:移位运算符的右操作数要完成模32运算(除非左操作数是long类型,在这种情况下需要对右操作数完成模64运算)。例如,1<<35的值等同于1<<3或8。
表3-4给出了运算符的优先级。如果不使用圆括号,就按照这里给出的运算符优先级次序进行计算。同一个级别的运算符按照从左到右的次序进行计算(但右结合运算符除外,如表中所示)。例如,由于 && 的优先级比 || 的优先级高,所以表达式 a && b || c 等价于 (a && b) ||c
因为+= 是右结合运算符,所以表达式 a += b += c 等价于 a += (b += c) 也就是将b=c的结果(完成加法后b的值)加到a上。
C++注释:与C或C++不同,Java不使用逗号运算符。不过,可以在for语句的第1和第3部分中使用逗号分隔表达式列表。
| 优先级 | 运算符 | 结合顺序 | 运算符类别 |
|---|---|---|---|
| 1 | [ ] . ( ) (方法调用) | 从左向右 | 括号运算符 |
| 2 | ! 、~ 、++ 、– 、+(一元运算)、-(一元运算)、( )(强制类型转换)、new | 从右向左 | 逻辑、正负、自增减运算符 |
| 3 | * 、/ 、% | 从左向右 | 算术运算符 |
| 4 | +(加)、-(减) | 从左向右 | 算术运算符 |
| 5 | << 、>> 、>>> | 从左向右 | 移位运算符 |
| 6 | < 、<= 、> 、>= 、instanceof | 从左向右 | 关系运算符 |
| 7 | == 、!= | 从左向右 | 关系运算符 |
| 8 | & | 从左向右 | 位逻辑运算符 |
| 9 | ^ | 从左向右 | 位逻辑运算符 |
| 10 | | | 从左向右 | 位逻辑运算符 |
| 11 | && | 从左向右 | 逻辑运算符 |
| 12 | || | 从左向右 | 逻辑运算符 |
| 13 | ?: | 从右向左 | 条件运算符 |
| 14 | =、+=、-=、*=、/=、%=、&=、|=、^=、<<=、>>=、>>>= | 从右向左 | 赋值运算符 |
字符串
从概念上讲,Java 字符串就是 Unicode 字符序列。例如,字符串 “Java\u212B” 由 5 个 Unicode 字符 J、a、v、a 和 TM组成。Java 没有内置的字符串类型,而是标准 Java 类库中提供了一个预定义类,很自然地叫作 String。每个用双引号括起来的字符串都是 String 类的一个实例:
1 | |
String 类的 substring方法可以从一个较大的字符串提取出一个子串。例如:
1 | |
会创建一个由字符 “Hel” 组成的字符串。
注释:类似于 C 和 C++,Java 字符串中的代码单元和码点从 0 开始计数。
substring 方法的第二个参数是你不想复制的第一个位置。这里要复制位置为 0、1 和 2(从 0 到 2,包括 0 和 2)的字符。substring 会计数,这说明会从 0 开始,直到 3 为止,但不包含 3。
substring 的工作方式有一个优点:很容易计算子串的长度。字符串 s.substring(a, b) 的长度为 b-a。例如,子串 “Hel” 的长度为 3-0=3。
与绝大多数程序设计语言一样,Java 语言允许使用 + 号连接(拼接)两个字符串。
1 | |
上述代码将字符串”Expletivedeleted”赋给变量message(注意,单词之间没有空格,+号完全按照给定的次序将两个字符串拼接起来)。
当将一个字符串与一个非字符串的值进行拼接时,后者会转换成字符串(在第5章中可以看到,任何一个Java对象都可以转换成字符串)。例如:
1 | |
将把 rating 设置为 “PG13” 。
这个特性通常用在输出语句中。例如:
1 | |
这是一条合法的语句,会打印出你希望的结果(因为单词 is 后面加了一个空格,输出时也会有这个空格)。
如果需要把多个字符串放在一起,用一个界定符分隔,可以使用静态join方法:
1 | |
在Java11中,还提供了一个repeat方法:
1 | |
String 类没有提供任何方法来修改字符串中的某个字符。如果希望将 greeting 的内容修改为 “Help!” ,不能直接将greeting 的最后两个位置的字符修改为 ‘p’ 和 ‘!’ 。在 Java 中这很容易实现。可以提取想要保留的子串,再与希望替换的字符拼接:
1 | |
上面这条语句将把 greeting 变量的当前值修改为 “Help!” 。
由于不能修改 Java 字符串中的单个字符,所以在 Java 文档中将String类对象称为是不可变的(immutable),如同数字3永远是数字3一样,字符串 “Hello” 永远包含字符H、e、l、l和o的代码单元序列。你不能修改这些值,不过,我们已经看到,可以修改字符串变量 greeting 的内容,让它指向另外一个字符串,这就如同可以让原本存放3的数值变量改成存放4一样。
通过拼接“Hello”和“p!”来生成一个新字符串的效率确实不高。但是,不可变字符串有一个很大的优点:编译器可以让字符串共享。
为了弄清具体如何工作,可以想象各个字符串存放一个在公共存储池中。字符串变量指向存储池中相应的位置。如果复制一个字符串变量,原始字符串和复制的字符串共享相同的字符。
总而言之,Java的设计者认为共享带来的高效率远远超过编辑字符串(提取子串和拼接字符串)所带来的低效率。可以看看你自己的程序,你会发现,大多数情况下都不会修改字符串,而只是需要对字符串进行比较。(有一种例外情况,将来自文件或键盘的单个字符或较短字符串组装成更大的字符串。为此,Java提供了一个单独的类,在3.6.9节中将详细介绍)。
可以使用 equals 方法检测两个字符串是否相等。对于表达式:
1 | |
如果字符串 s 与字符串 t 相等,则返回 true ;否则,返回 false。需要注意的是,s 与 t 可以是字符串变量,也可以是字符串字面量。例如,以下表达式是合法的:
1 | |
要想检测两个字符串是否相等,而不区分大小写,可以使用 equalsIgnoreCase 方法。
1 | |
不要使用 == 运算符检测两个字符串是否相等!这个运算符只能够确定两个字符串是否存放在同一个位置上。当然,如果字符串在同一个位置上,它们必然相等。但是,完全有可能将多个相等的字符串副本存放在不同的位置上。
1 | |
如果虚拟机总是共享相等的字符串,则可以使用 == 运算符检测字符串是否相等。但实际上只有字符串字面量会共享,而 + 或 substring 等操作得到的字符串并不共享。因此,千万不要使用 == 运算符测试字符串的相等,否则程序中会出现最糟糕的一种bug,这种bug可能会间歇性地随机出现。
空串 “” 是长度为0的字符串。可以调用以下代码检查一个字符串是否为空:
1 | |
空串是一个Java对象,有自己的串长度(0)和内容(空)。不过,String变量还可以存放一个特殊的值,名为null,表示目前没有任何对象与该变量关联(关于null的更多信息请参见第4章)。要检查一个字符串是否为null,可以使用以下代码:
1 | |
有时要检查一个字符串既不是null也不是空串,这种情况下可以使用:
1 | |
首先要检查str不为null。在第4章会看到,如果在一个null值上调用方法,会出现错误。
Java字符串是一个char值序列。char数据类型是采用UTF-16编码表示Unicode码点的一个代码单元。最常用的Unicode字符可以用一个代码单元表示,而辅助字符需要一对代码单元表示。
length方法将返回采用UTF-16编码表示给定字符串所需要的代码单元个数。例如:
1 | |
要想得到实际长度,即码点个数,可以调用:
1 | |
调用 s.charAt(n) 将返回位置 n 的代码单元,n 介于 0 ~ s.length() - 1 之间。例如:
1 | |
要想得到第 i 个码点,可以使用以下语句:
1 | |
如果想要遍历一个字符串,并且依次查看每一个码点,可以使用以下语句:
1 | |
可以使用以下语句实现反向遍历:
1 | |
显然,这很麻烦。更容易的办法是使用codePoints方法,它会生成int值的一个“流”,每个int值对应一个码点。(流在卷Ⅱ中介绍。)可以将流转换为一个数组(见3.10节),再完成遍历。
1 | |
反之,要把一个码点数组转换为一个字符串,可以使用构造器(我们将在第4章详细讨论构造器和new操作符)。
1 | |
要把单个码点转换为一个字符串,可以使用Character.toString(int)方法:
1 | |
注释:虚拟机不一定把字符串实现为代码单元序列。在Java 9中使用了一个更紧凑的表示。只包含单字节代码单元的字符串使用byte数组实现,所有其他字符串使用char数组。
本书中给出的API注释可以帮助你理解Java应用程序编程接口(API)。每一个API注释首先给出类名,如java.lang.String。(java.lang包名的重要性将在第4章给出解释。)类名之后是一个或多个方法的名字、解释和参数描述。API注释不会列出一个特定类的所有方法,而是会以简洁的方式给出最常用的一些方法,完整的方法列表请参见联机文档(请参见3.6.8节)。类名后面的编号是引入这个类的JDK版本号。如果某个方法是之后添加的,那么这个方法后面还会给出一个单独的版本号。
有些时候,需要由较短的字符串构建字符串,例如,按键或文件中的单词。如果采用字符串拼接的方式来达到这个目的,效率会比较低。每次拼接字符串时,都会构建一个新的 String 对象,既耗时,又浪费空间。使用 StringBuilder 类就可以避免这个问题。
如果需要用许多小字符串来构建一个字符串,可以采用以下步骤。首先,构建一个空的字符串构建器:
1 | |
当每次需要添加另外一部分时,就调用 append 方法。
1 | |
字符串构建完成时,调用 toString 方法。你会得到一个 String 对象,其中包含了构建器中的字符序列。
1 | |
注释: StringBuffer 类的效率不如 StringBuilde 类,不过它允许采用多线程的方式添加或删除字符。如果所有字符串编辑操作都在单个线程中执行(通常都是这样),则应当使用 StringBuilder 类。这两个类的API是一样的。
利用 Java15 新增的 **文本块(text block)**特性,可以很容易地提供跨多行的字符串字面量。文本块以 “”” 开头(这是开始 “”” ),后面是一个换行符,并以另一个 “”” 结尾(这是结束 “”” ):
1 | |
文本块比相应的字符串字面量更易于读写:
“Hello\nWorld\n”
这个字符串包含两个\n:一个在Hello后面,另一个在World后面。开始 “”” 后面的换行符不作为字符串字面量的一部分。
如果不想要最后一行后面的换行符,可以让结束 “”” 紧跟在最后一个字符后面:
1 | |
文本块特别适合包含用其他语言编写的代码,如 SQL 或 HTML。可以直接将那些代码粘贴到一对三重引号之间:
1 | |
需要说明的是,一般不用对引号转义。只有两种情况下需要对引号转义:
● 文本块以一个引号结尾。
● 文本块中包含三个或更多引号组成的一个序列。
遗憾的是,所有反斜线都需要转义。
常规字符串中的所有转义序列在文本块中也有同样的作用。
有一个转义序列只能在文本块中使用。行尾的\会把这一行与下一行连接起来。例如:
1 | |
等同于:
“Hello, my name is Hal. Please enter your name:”
文本块会对行结束符进行标准化,删除末尾的空白符,并把Windows的行结束符 ( \r\n)改为简单的换行符( \n)。尽管不太可能,不过假如确实需要保留末尾的空格,这种情况下可以把最后一个空格转换为一个 \s 转义序列。
对于前导空白符则更为复杂。考虑一个从左边界缩进的典型的变量声明。文本块也可以缩进:
1 | |
将去除文本块中所有行的公共缩进。实际字符串为:
“<div class=\“Warning\“>\n Beware of those who say \“Hello\“ to the world\n
注意,第一行和第三行没有缩进。
你的IDE很可能会使用制表符、空格或者制表符以及空格缩进所有文本块。
Java很明智,它没有规定制表符的宽度。空白符前缀必须与文本块中的所有行完全匹配。去除缩进过程中不考虑空行。不过,结束 “”” 前面的空白符很重要。一定要缩进到想要去除的空白符的末尾。
警告:要当心缩进文本块的公共前缀中混用制表符和空格的情况。不小心漏掉一个空格很容易得到一个缩进错误的字符串。
提示:如果一个文本块中包含非Java代码,实际上最好沿左边界放置。这样可以与Java代码区分开,而且可以为长代码行留出更多空间。
输入与输出
前面已经看到,将输出打印到“标准输出流”(即控制台窗口)是一件非常容易的事情,只需要调用 System.out.println 。不过,读取“标准输入流” System.in 就没有那么简单了。要想读取控制台输入,首先需要构造一个与“标准输入流” System.in 关联的 Scanner 对象。
1 | |
现在,就可以使用 Scanner 类的各种方法读取输入了。例如,nextLine 方法将读取一行输入。
1 | |
在这里,使用 nextLine 方法是因为输入行中有可能包含空格。要想读取一个单词(以空白符作为分隔符),可以调用
1 | |
要想读取一个整数,要使用 nextInt 方法。
1 | |
与此类似,nextDouble 方法可以读取下一个浮点数。
在程序清单3-2的程序中,首先询问用户姓名和年龄,然后打印一条如下的消息:
Hello,Cay. Next year,you’ll be 57
最后,在程序的最前面添加一行代码:
1 | |
Scanner类在 java.util 包中定义。当使用的类不是定义在基本 java.lang 包中时,需要使用import指令导入相应的包。
程序清单3-2 InputTest/UploadTest.java
1 | |
注释:因为输入对所有人都可见,所以Scanner类不适用于从控制台读取密码。可以使用Console类来达到这个目的。要想读取一个密码,可以使用以下代码:
1 | |
为安全起见,将返回的密码存放在一个字符数组中,而不是字符串中。完成对密码的处理之后,应该马上用一个填充值覆盖数组元素。使用Console对象处理输入不如使用Scanner方便。必须一次读取一行输入,而且Console类没有提供方法来读取单个单词或数字。
格式化输出
可以使用 System.out.print(x) 语句将数值 x 输出到控制台。这个命令将以 x 的类型所允许的最大非 0 位数打印 x 。例如:
1 | |
会打印
3333.3333333333335
如果希望显示美元、美分数,这就会有问题。
这个问题可以利用 printf 方法来解决,它沿用了C语言函数库中的古老约定。例如,以下调用
1 | |
打印 x 时**字段宽度(field width)**为8个字符,精度为2个字符。也就是说,结果包含一个前导的空格和7个字符,如下所示:
3333.33
可以为 printf 提供多个参数,例如:
1 | |
每一个以 % 字符开头的**格式说明符(format specifiers)**都替换为相应的参数。格式说明符末尾的转换字符(conversion character)指示要格式化的数值的类型: f 表示浮点数,,s 表示字符串,d 表示十进制整数。表3-5列出了用于printf的转换字符。
大写形式会生成大写字母。例如,“%8.2E” 将 3333.33 格式化为 3.33E+03,这里有一个大写的 E。
表 3-5:用于 printf 的转换字符
| 转换字符 | 类型 | 示例 | 转换字符 | 类型 | 示例 |
|---|---|---|---|---|---|
| d | 十进制整数 | 159 | s 或 S | 字符串 | Hello |
| x 或 X | 十六进制整数。要想对十六进制格式化有更多控制,可以使用 HexFormat 类 | 9f | c 或 C | 字符 | H |
| o | 八进制整数 | 237 | b 或 B | 布尔 | true |
| f 或 F | 定点浮点数 | 15.9 | h 或 H | 散列码 | 4262802 |
| e 或 E | 指数浮点数 | 1.59e+01 | tx 或 Tx | 遗留的日期时间格式化。应当改为使用 java.time 类,参见第 6 章 | —— |
| g 或 G | 通用浮点数(e 和 f 中较短的一个) | —— | % | 百分号 | % |
| a 或 A | 十六进制浮点数 | 0x1.fccdp3 | n | 与平台有关的行为符 | —— |
注释:可以使用 s 转换字符格式化任意的对象。如果一个任意对象实现了 Formattable 接口,会调用这个对象的 formatTo 方法。否则,会调用 toString 方法将这个对象转换为一个字符串。
另外,还可以指定控制格式化输出外观的各种标志( flag )。表 3-6 列出了用于 printf 的标志。例如,逗号标志会增加分组分隔符。即
1 | |
会打印
3,333.33
可以使用多个标志,例如,“ %,( .2f “ 会使用分组分隔符,并将负数包围在括号内。
表 3-6:用于 printf 的标志
| 标志 | 作用 | 示例 |
|---|---|---|
| + | 打印正数和负数的符号 | +3333.33 |
| 空格 | 在正数前面增加一个空格 | | 3333.33| |
| 0 | 增加前导 0 | 003333.33 |
| - | 字段左对齐 | |3333.33 | |
| ( | 将负数包围在括号内 | (3333.33) |
| , | 增加分组分隔符 | 3,333.33 |
| # (对于 f 格式) | 总是包含一个小数点 | 3,333. |
| # (对于 x 或 o 格式) | 添加前缀 0x 或 0 | 0xcafe |
| $ | 指定要格式化的参数索引。例如,%1$d %1$x 将以十进制和十六进制格式打印第 1 个参数 |
159 9F |
| < | 格式化前面指定的同一个值。例如,%d %<x 将以十进制和十六进制打印同一个数 |
159 9F |
可以使用静态的 String.format 方法创建一个格式化的字符串,而不打印输出:
1 | |
注释:在 Java 15 中,可以使用 formatted 方法,这样可以少敲 5 个字符:
1 | |
现在,我们已经了解了 printf 方法的所有特性。图 3-6 给出了格式说明符的语法图。

图 3-6 格式说明符语法
注释:格式化规则是特定于本地化环境的。例如,在德国,分组分隔符是点号而不是逗号。
3.7.3 文件输入与输出
要想读取一个文件,需要构造一个 Scanner 对象,如下所示:
1 | |
如果文件名中包含反斜线符号,记住要在每个反斜线之前再加一个额外的反斜线转义:
1 | |
现在就可以使用之前见过的任何 Scanner 方法读取这个文件了。
注释:这里指定了 UTF-8 字符编码,这对于互联网上的文件很常见(不过并不是普通通用)。读取一个文本文件时,要知道它的字符编码。如果省略字符编码,则会使用运行这个 Java 程序的机器的”默认编码”。这不是一个好主意,如果在不同的机器上运行这个程序,可能会有不同的表现。
警告:可以提供一个字符串参数来构造一个 Scanner,但这个 Scanner 会把字符串解释为数据,而不是文件名。例如,如果调用:
1 | |
这个 Scanner 会将参数看作是包含 10 个字符(’m’, ‘y’, ‘f’ 等)的数据。这可能是我们的原意。
要想写入文件,需要构造一个 PrintWriter 对象。在构造器(constructor)中,需要提供文件名和字符编码:
1 | |
如果文件不存在,则创建该文件。可以像输出到 System.out 一样使用 print、println 以及 printf 命令。
注释:当指定一个相对文件名时,例如,”myfile.txt”、”mydirectory/myfile.txt” 或 “../myfile.txt”,文件将相对于启动 Java 虚拟机的那个目录放置。如果从一个命令 shell 执行以下命令启动程序:
1 | |
启动目录就是命令 shell 的当前目录。不过,如果使用集成开发环境,那么启动目录将由 IDE 控制。可以使用下面的调用找到这个目录的位置:
1 | |
如果觉得文件定位太麻烦,可以考虑使用绝对路径名,例如:”c:\mydirectory\myfile.txt” 或者 “/home/me/mydirectory/myfile.txt”。
要记住一点:如果用一个不存在的文件构造一个 Scanner,或者用一个无法创建的文件名构造一个 PrintWriter,就会产生异常。Java 编译器认为这些异常比”被零除”异常更严重。为此,要用一个 throws 子句标记 main 方法,如下所示:
1 | |
注释:从命令 shell 启动一个程序时,可以利用 shell 的重定向语法将任意文件关联到 System.in 和 System.out:
1 | |
这样,就不必担心处理 IOException 异常了。
3.8 控制流程
与任何程序设计语言一样,Java 支持使用条件语句和循环结构来确定控制流程。这里首先讨论条件语句,然后介绍循环语句,最后介绍 switch 语句,它可以用来检测一个表达式的多个值。
注释:Java 中没有 goto 语句,但 break 语句可以带标签,可以利用它从嵌套循环中跳出。
3.8.1 块作用域
在学习控制结构之前,需要了解块(block)的概念。
块(即复合语句)由若干条 Java 语句组成,并用一对大括号括起来。块确定了变量的作用域。一个块可以嵌套在另一个块中。下面就是嵌套在 main 方法块中的一个块。
1 | |
但是,不能在嵌套的两个块中声明同名的变量。例如,下面的代码就有错误,而无法通过编译:
1 | |
3.8.2 条件语句
在 Java 中,条件语句的形式为:
1 | |
这里的条件必须用小括号括起来。
在 Java 中,常常希望在某个条件为真时执行多条语句。在这种情况下,就可以使用块语句(block statement),形式如下:
1 | |
例如:
1 | |
当 yourSales 大于或等于 target 时,将执行大括号中的所有语句(请参见图 3-7)。
注释:使用块(有时称为复合语句)可以在 Java 程序结构中原本只能放置一条(简单)语句的地方放置多条语句。
在 Java 中,更一般的条件语句如下所示(请参见图 3-8):
1 | |
例如:
1 | |
其中 else 部分总是可选的。else 子句与最邻近的 if 构成一组。因此,在以下语句中:
1 | |
else 与第 2 个 if 配对。当然,使用大括号可以让这段代码更加清晰:
1 | |
图 3-7 if 语句的流程图 图 3-8 if/else 语句的流程图
反复使用 if...else if... 很常见(如图 3-9 所示)。例如:
1 | |
3.8.3 循环
while 循环会在条件为 true 时执行一个语句(也可以是一个块语句)。一般形式如下:
1 | |
图3-9 if/else if(多分支)的流程图
如果开始时循环条件就为 false,那么 while 循环一次也不执行(请参见图3-10)。
程序清单3-3中的程序将计算需要多长时间才能够存下一定数量的退休金,假定每年存入相同金额,而且利率是固定的。在这个示例中,我们会让一个计数器递增,并在循环体中更新当前的累积金额,直到总金额超过目标金额为止。
1 | |
while 循环语句在最前面检测循环条件。因此,循环体中的代码有可能一次都不执行。如果希望循环体至少执行一次,需要使用 do/while 循环将检测放在最后。它的语法如下:
1 | |
这种循环先执行语句(通常是一个语句块),然后再检查循环条件。如果条件为 true,就重复执行语句,然后再依次检测循环条件,依此类推。首先计算退休账户中新的余额,然后再询问是否打算退休:
1 | |
只要用户回答 “N”,循环就会重复执行(见图 3-11)。这是一个很好的例子,它展示了一个至少需要执行一次的循环,因为用户必须先看到余额才能决定是否满足退休所用。

图 3-10 while 语句的流程图 图 3-11 do/while 语句的流程图
3.8.4 确定性循环
for 循环语句是支持迭代的一种通用结构,它由一个计数器或类似的变量控制迭代次数,每次迭代后这个变量将会更新。如图 3-12 所示,下面的循环将在屏幕上显示出打印数字 1~10:
1 | |
for 语句的第 1 部分通常是对计数器初始化;第 2 部分给出每次新一轮循环执行前要检测的循环条件;第 3 部分指定如何更新计数器。
与 C++ 类似,尽管 Java 允许在 for 循环的各个部分放置任何表达式,但有一条不成立的规则:for 语句的 3 个部分应该对同一个计数器变量进行初始化、检测和更新。
下面这个倒计数的循环:
1 | |
警告:在循环中,检测两个浮点数是否相等需要格外小心。下面的 for 循环:
1 | |
可能永远不会结束。由于存在舍入误差,可能永远达不到精确的最终值。在这个例子中,因为 0.1 无法精确地用二进制表示,所以,x 将从 9.9999999999998 跳到 10.099999999999998。
在 for 语句的第 1 部分中声明一个变量之后,这个变量的作用域会扩展到这个 for 循环体的末尾。
1 | |
特别指出,如果在 for 语句内部定义一个变量,这个变量就不能在循环体之外使用。因此,如果希望在 for 循环体之外使用循环计数器的最终值,就要确保在循环体之外声明这个变量。
1 | |
另一方面,可以在不同的 for 循环中定义同名的变量:
1 | |

图 3-12 for 语句的流程图
for 循环语句只是 while 循环的一种简化形式。例如,
1 | |
可以重写为:
1 | |
注释:3.10.3 节将会介绍”泛型 for 循环”(又称为 for each 循环),利用这个循环可以很方便地访问一个数组或集合中的所有元素。
3.8.5 多重选择:switch 语句
在处理同一个表达式的多个选项时,使用 if/else 结构会显得有些笨拙。switch 语句会让这个工作变得容易,特别是采用 Java 14 引入的形式时会更简单。
例如,如果建立一个如图 3-13 所示的菜单系统,其中包含 4 个选项,可以使用以下代码:
1 | |
case 标签可以是:
- 类型为 char、byte、short 或 int 的常量表达式
- 枚举常量
- 字符串字面量
- 多个字符串,用逗号分隔
例如:
1 | |
switch 语句的”经典”形式可以追溯到 C 语言,从 Java 1.0 开始就支持这种形式。具体形式如下:
1 | |

图 3-13 Switch 语句的流程图
switch 语句从与选项值相匹配的 case 标签开始执行,直到遇到下一个 break 语句,或者执行到 switch 语句结束。如果没有匹配的 case 标签,则执行 default 子句(如果有 default 子句)。
警告:有可能触发多个分支。如果忘记在一个分支末尾增加 break 语句,就会接着执行下一个分支!
为了检测这种问题,编译代码时可以加上 -Xlint:fallthrough 选项,如下所示:
1 | |
这样一来,如果某个分支最后缺少一个 break 语句,编译器就会给出一个警告。如果你确实是想使用这种”直通式”(fallthrough)行为,可以为其外围方法加一个注解 @SuppressWarnings("fallthrough")。这样就不会对这个方法生成警告了。(注解是为编译器或处理 Java 源文件或类文件的工具提供信息的一种机制。)
这两种 switch 形式都是语句。在 3.5.9 节中,我们已经见过一个 switch 表达式,它会生成一个值。switch 表达式没有”直通式”行为。
为了对称,Java 14 还引入了一个有直通行为的 switch 表达式,所以总共有 4 种不同形式的 switch。表 3-7 给出了这 4 种形式。
**在有直通行为的形式中,每个 case 以一个冒号结束。如果 case 以箭头 -> 结束,则没有直通行为。**不能在一个 switch 语句中混合使用冒号和箭头。
注意 switch 表达式的 yield 关键字。与 break 类似,yield 会终止执行。但与 break 不同的是,yield 还会生成一个值,这就是表达式的值。
要在 switch 表达式的一个分支中使用语句而不想有直通行为,就必须使用大括号和 yield,如表中的示例所示,这个例子将为一个分支增加日志语句:
1 | |
switch 表达式的每个分支必须生成一个值。最常见的做法是,各个值跟在一个箭头 -> 后面:
1 | |
如果无法做到,则使用 yield 语句。
1 | |
注释:完全可以在 switch 表达式的一个分支中抛出异常。例如:
1 | |
警告:switch 表达式的关键是生成一个值(或者产生一个异常而失败)。不允许”跳出”switch 表达式。
1 | |
具体来说,不能在 switch 表达式中使用 return、break 或 continue 语句。
switch 表达式优于语句。如果每个分支会为一个变量赋值或方法调用计算值,则用一个表达式生成值,然后使用这个值。例如:
1 | |
要优于
1 | |
只有在确实需要直通行为时,或者必须为一个 switch 表达式增加语句时,才需要使用 break 或 yield 。
3.8.6 中断控制流程的语句
尽管 Java 的设计者将 goto 仍作为一个保留字,但实际上并不打算在语言中包含 goto。
下面首先来看不带标签的 break 语句。与用于退出 switch 语句的 break 语句一样,它也可以用于退出循环。例如,
1 | |
循环开始时,如果 years > 100,或者如果循环中间 balance >= goal,则退出循环。当然,也可以在不使用 break 的情况下计算 years 的值,如下所示:
1 | |
但是需要注意,在这个版本中,检测了两次 balance < goal。为了避免重复检测,有些程序员更偏爱使用 break 语句。
与 C++ 不同,Java 还提供了一种带标签的 break 语句,允许跳出多重嵌套的循环。有时候,在嵌套很深的循环语句中会发生一些不可预料的事情。此时你可能希望完全跳出所有嵌套循环。如果只是为各层循环检测添加一些额外的条件,这会很不方便。
下面的例子展示了 break 语句如何工作。请注意,标签必须放在你想跳出的最外层循环之前,并且必须紧跟一个冒号。
1 | |
如果输入有误,执行带标签的 break 会跳转到带标签的语句块末尾。与任何使用 break 语句的代码一样,接下来需要检测循环是正常退出,还是由于 break 提前退出。
注释: 有意思的是,可以将标签应用到任何语句,甚至可以应用到 if 语句或者块语句,如下所示:
1 | |
因此,如果确实希望使用 goto 语句,而且一个代码块恰好在你想要跳转到的位置之前结束,就可以使用 break 语句!当然,我并不提倡使用这种方法。另外需要注意,只能跳出语句块,而不能跳入语句块。
最后,还有一个 continue 语句。与 break 语句一样,它将中断正常的控制流程。continue 语句将控制转移到最内层外围循环的首部。例如:
1 | |
如果 n < 0,则 continue 语句会越过当前循环体的剩余部分,直接跳到循环首部。
如果在 for 循环中使用 continue 语句,会跳转到 for 循环的”更新”部分。例如:
1 | |
如果 n < 0,则 continue 语句将跳转到 count++ 语句。
还有一种带标签的 continue 语句,将跳转到有匹配标签的循环的首部。
提示: 许多程序员发现 break 和 continue 语句很容易混淆。这些语句完全是可选的,即不使用这些语句也能表达同样的逻辑。在本书中,所有程序都不会使用 break 和 continue。
3.9 大数
如果基本的整数和浮点数精度不足以满足需求,那么可以使用 java.math 包中两个很有用的类:BigInteger 和 BigDecimal。这两个类可以处理包含任意长度数字序列的数值。BigInteger 类实现任意精度的整数运算,BigDecimal 实现任意精度的浮点数运算。
使用静态的 valueOf 方法可以将一个普通的数转换为大数:
1 | |
对于更长的数,可以使用一个带字符串参数的构造器:
1 | |
另外还有一些常量:BigInteger.ZERO、BigInteger.ONE 和 BigInteger.TEN,Java 9 之后还增加了 BigInteger.TWO。
警告: 对于 BigDecimal 类,总是应当使用带一个字符串参数的构造器。还有一个 BigDecimal(double) 构造器,不过这个构造器本质上很容易产生舍入误差,例如,new BigDecimal(0.1) 会得到以下数位:
1 | |
不能使用人们熟悉的算术运算符(如:+ 和 *)来组合大数,而需要使用大数类中的 add 和 multiply 方法。
1 | |
3.10 数组
数组存储相同类型值的序列。
3.10.1 声明数组
数组是一种数据结构,用来存储同一类型值的集合。通过一个整型索引(index,或称下标)可以访问数组中的每一个值。例如,如果 a 是一个整型数组,a[i] 就是数组中索引为 i 的整数。
在声明数组变量时,需要指出数组类型(元素类型后面紧跟 [])和数组变量名。例如,下面声明了整型数组 a:
1 | |
不过,这条语句只声明了变量 a,并没有将 a 初始化为一个真正的数组。应该使用 new 操作符创建数组。
1 | |
这条语句声明并初始化了一个可以存储 100 个整数的数组。
数组长度不要求是常量:new int[n] 会创建一个长度为 n 的数组。
一旦创建了数组,就不能再改变它的长度(不过,当然可以改变单个数组元素)。如果程序运行中需要经常扩展数组的大小,就应该使用另一种数据结构——数组列表(array list)。
注释: 可以使用下面两种形式定义一个数组变量:
1 | |
或
1 | |
第一种风格,它可以将类型 int[](整型数组)与变量名清晰地分开。
在 Java 中,提供了一种创建数组对象并同时提供初始值的简写形式。下面是一个例子:
1 | |
请注意,这个语法中不需要使用 new,甚至不用指定长度。
最后一个值后面允许有逗号,如果你要不断为数组增加值,这样会很方便:
1 | |
还可以声明一个匿名数组(anonymous array):
1 | |
这个表达式会分配一个新数组并填入大括号中提供的值。它会统计初始值个数,并相应地设置数组大小。可以使用这种语法重新初始化一个数组而无须创建新变量。例如:
1 | |
这是以下语句的简写形式:
1 | |
注释: 在 Java 中,允许有长度为 0 的数组。编写一个结果为数组的方法时,如果碰巧结果为空,这样一个长度为 0 的数组就很有用。可以如下构造一个长度为 0 的数组:
1 | |
或
1 | |
注意,长度为 0 的数组与 null 并不相同。
3.10.2 访问数组元素
数组元素从 0 开始编号。最后一个合法的索引为数组长度减 1。在下面的例子中,索引值为 0~99。一旦创建了数组,就可以在数组中填入元素。例如,可以使用一个循环:
1 | |
创建一个数字数组时,所有元素都初始化为 0。boolean 数组的元素会初始化为 false。对象数组的元素则初始化为一个特殊值 null,表示这些元素(还)未存放任何对象。初学者对此可能有些不解。例如,
1 | |
会创建一个包含 10 个字符串的数组,(所有字符串都为 null)。如果希望这个数组包含空串,则必须为元素提供空串:
1 | |
警告: 如果创建了一个包含 100 个元素的数组,然后试图访问元素 a[100](或在 0~99 之外的任何其他索引),就会出现”array index out of bounds”(数组索引越界)异常。
要想获得数组中的元素个数,可以使用 array.length。例如,
1 | |
3.10.3 for each 循环
Java 有一种功能很强的循环结构,可以用来依次处理数组(或者任何其他元素集合)中的每个元素,而不必考虑指定索引值。
这种增强的 for 循环形式如下:
1 | |
它将给定变量(variable)设置为集合中的每一个元素,然后执行语句(statement)(当然,也可以是语句块)。collection 表达式必须是一个数组或者是一个实现了 Iterable 接口的类对象(例如 ArrayList)。
例如,
1 | |
会打印数组 a 的每一个元素,一个元素占一行。
这个循环应该读作”循环 a 中的每一个元素”(for each element in a)。Java 语言的设计者也曾考虑过使用诸如 foreach 和 in 这样的关键字,但这种循环并不是最初就包含在 Java 语言中,而是后来添加的,没有人希望破坏已经包含同名方法或变量(例如 System.in)的老代码。
当然,使用传统的 for 循环也可以获得同样的效果:
1 | |
但是,”for each”循环更加简洁、更不易出错,因为你不必为起始和终止索引值而操心。
注释: “for each”循环的循环变量将会遍历数组中的每个元素,而不是索引值。
如果需要处理一个集合中的所有元素,相比传统循环,”for each”循环是个让人欣喜的改进。不过,很多情况下还是需要使用传统的 for 循环。例如,可能不希望遍历整个集合,或者可能需要在循环内部使用索引值。
提示: 有一个更容易的方法可以打印数组中的所有值,即利用 Arrays 类的 toString 方法。调用 Arrays.toString(a) 会返回一个包含数组元素的字符串,这些元素包围在中括号内,并用逗号分隔,例如,"[2,3,5,7,11,13]"。要想打印数组,只需要调用
1 | |
3.10.4 数组拷贝
在 Java 中,允许将一个数组变量拷贝到另一个数组变量。这时,两个变量将引用同一个数组:
1 | |
图 3-14 显示了拷贝的结果。

图 3-14 拷贝一个数组变量
如果确实希望将一个数组的所有值拷贝到一个一个新的数组中,就要使用Arrays类的copyOf方法:
1 | |
第2个参数是新数组的长度。这个方法通常用来增加数组的大小:
1 | |
如果数组元素是数值型,那么新增的元素将其入0;如果数组元素是布尔型,则填入false。相反,如果长度小于原数组的长度,则只拷贝前面的值。
C++注释: Java数组与堆栈上的C++数组有很大不同,但基本上与在堆(heap)上分配的数组指针一样。也就是说,
1 | |
不同于
1 | |
而等同于
1 | |
Java 中的 [ ] 运算符预定义为会完成越界检查(bounds checking)。另外,没有指针运算,就意味着不能通过a加1得到数组中的下一个元素。
3.10.5 命令行参数
前面已经看到一个例子,其中一个Java数组重复出现过很多次。每一个Java程序都有一个带String arg[]参数的main方法。这个参数表明main方法将接收一个字符串数组,也就是命令行上指定的参数。
例如,来看下面这个程序:
1 | |
如果如下调用这个程序:java Message -g cruel world
args 数组将包含以下内容:
1 | |
这个程序会显示下面这个消息:
1 | |
C++ 注释: 在 Java 程序的 main 方法中,程序名并不存储在 args 数组中。例如,从命令行如下运行一个程序时
1 | |
args[0] 是 "-h",而不是 "Message" 或 "java"。
3.10.6 数组排序
要想对数值型数组进行排序,可以使用 Arrays 类中的 sort 方法:
1 | |
这个方法使用了优化的快速排序(QuickSort)算法。快速排序算法对于大多数数据集都很高效。
程序清单 3-7 中的程序具体使用了数组,它会为一个抽彩游戏生成一个随机的数字组合。例如,假如从 49 个数字中抽取 6 个数,那么程序可能的输出结果为:
1 | |
要想选择这样一组随机的数字,首先将值 1,2,…,n 填入数组 numbers 中:
1 | |
第二个数组存放抽取出来的数:
1 | |
现在可以开始抽取 k 个数了。Math.random 方法将返回一个 0 到 1 之间(包含 0、不包含 1)的随机浮点数。用 n 乘以这个浮点数,可以得到从 0 到 n-1 之间的一个随机数。
1 | |
下面将 result 的第 i 个元素设置为该索引对应的数(numbers[r]),最初是 r+1,但正如所看到的,numbers 数组的内容在每一次抽取之后都会发生变化。
1 | |
现在,必须确保不会再次抽到那个数,因为所有抽形数字必须各不相同。因此,这里用数组中的最后一个数覆盖 number[r],并将 n 减 1。
1 | |
关键在于每次抽取的都是索引,而不是实际的值。这个索引指向一个数组,其中包含尚未抽取过的值。在抽取了 k 个数之后,可以对 result 数组进行排序,来得到更美观的输出:
1 | |
3.10.7 多维数组
多维数组使用多个索引访问数组元素,它适用于表格或其他更复杂的排列形式。
假设需要建立一个数值表格,用来显示在不同利率下投资 10 000 美元有多少收益,利息每年兑现并复投(见表 3-8)。
表 3-8 不同利率下的投资收益情况
1 | |
可以使用一个二维数组(也称为矩阵)来存储这些信息,名为 balances。在 Java 中,声明一个二维数组相当简单。例如:
1 | |
数组在进行初始化之前是不能使用的。在这里可以如下初始化:
1 | |
其他情况下,如果知道数组元素,可以不调用 new,而直接使用一种简写形式对多维数组进行初始化。例如:
1 | |
一旦初始化数组,就可以利用两对中括号访问单个元素,例如,balances[i][j]。
在示例程序中用到了一个存储利率的一维数组 interest 和一个存储账户余额的二维数组 balances(对应每个年度和利率分别有一个余额),使用初始余额来初始化这个数组的第一行:
1 | |
然后计算其他行,如下所示:
1 | |
注释: “for each”循环语句不会自动循环处理二维数组的所有元素。它会循环处理行,而这些行本身就是一维数组。要想访问二维数组 a 的所有元素,需要使用两个嵌套的循环,如下所示:
1 | |
提示: 要想快速地打印一个二维数组的元素列表,可以调用:
1 | |
输出格式为:
1 | |
3.10.8 不规则数组
到目前为止,我们看到的数组与其他程序设计语言中的数组没有多大区别。但在底层实际存在着一些细微的差异,有时你可以充分利用这一点:Java 实际上没有多维数组,只有一维数组。多维数组被解释为”数组的数组”。
例如,在前面的示例中,balances 数组实际上是一个包含 10 个元素的数组,而每个元素又是一个由 6 个浮点数组成的数组(请参见图 3-15)。

图 3-15 一个二维数组
表达式 balances[i] 指示第 i 个子数组,也就是表格的第 i 行。它本身也是一个数组,balances[i][j] 指示这个数组的第 j 个元素。
由于可以单独地访问数组的某一行,所以可以让两行交换。
1 | |
还可以很容易地构造一个”不规则”数组,即数组的每一行有不同的长度。下面是一个标准的示例。我们将创建一个数组,第 i 行第 j 列的元素将存放”从 i 个数中抽取 j 个数”可能的结果数。
1 | |
由于 j 不可能大于 i,所以矩阵是三角形的。第 i 行有 i + 1 个元素(允许抽取 0 个元素,这种选择只有一种可能)。要想创建这样一个不规则的数组,首先需要分配一个数组包含这些行:
1 | |
接下来,分配这些行:
1 | |
分配了数组之后,可以采用通常的方式访问其中的元素(前提是没有超出边界):
1 | |
程序清单 3-9 给出了完整的程序。
C++ 注释: 在 C++ 中(Java 声明)
1 | |
不同于
1 | |
也不同于
1 | |
而是分配了一个包含 10 个指针的数组:
1 | |
然后,这个指针数组的每一个元素会填充一个包含 6 个数字的数组:
1 | |
庆幸的是,当调用 new double[10][6] 时,这个循环是自动的。需要不规则的数组时,只能单独地分配行数组。

