《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)。声明一个变量时,先指定变量的类型,然后是变量名。注意每个声明都以分号结束。

作为变量名(以及其他名字)的标识符由字母、数字、货币符号以及“标点连接符”组成。需遵循以下规则:

  • 第一个字符不能是数字,@+之类的符号也不能出现在变量名中,空格也不行;
  • 字母区分大小写(如 mainMain是不同的标识符);
  • 标识符的长度基本上没有限制。

与大多数程序设计语言相比,Java 中“字母”和“货币符号”的范围更大:

  • 字母:指一种语言中表示字母的任何 Unicode 字符(例如,德国用户可在变量名中使用字母 ß;讲希腊语的人可使用 π);
  • 数字:包括 0-9和表示一位数字的任何 Unicode 字符;
  • 货币符号:如 $等;
  • 标点连接符:包括下画线 _、“波浪线”~及其他一些符号(实际开发中,多数程序员常用 A-Za-z0-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
enum Size { SMALL, MEDIUM, LARGE, EXTRA_LARGE };

现在,可以声明这种类型的变量:

1
Size s = Size.MEDIUM;

Size类型的变量只能存储这个类型声明中所列的某个值,或者特殊值null,null表示这个变量没有设置任何值。

运算符

在 Java 中,使用通常的算术运算符 +、-、*、/ 分别表示加、减、乘、除运算。当参与 / 运算的两个操作数都是整数时,/ 表示整数除法;否则,这表示浮点除法。整数的求余操作有时称为取模 (modulus))用 % 表示。例如,15/2 等于 7,15%2 等于 1,15.0/2 等于 7.5。

需要注意,整数被 0 除将产生一个异常,而浮点数被 0 除将会得到一个无穷大或 NaN 结果。

Math 类中包含你可能会用到的各种数学函数,这取决于你要编写什么类型的程序。

要想计算一个数的平方根,可以使用 sqrt 方法:

1
2
3
double x = 4;
double y = Math.sqrt(x);
System.out.println(y); // prints 2.0

注释:println 方法和 sqrt 方法有一个微小的差异。println 方法处理 System.out 对象,而 Math 类中的 sqrt 方法并不处理任何对象,这样的方法被称为静态方法(static method)。

在 Java 中,没有完成幂运算的运算符,因此必须使用 Math 类的 pow 方法。以下语句:

1
double y = Math.pow(x, a);

将 y 的值设置为 x 的 a 次幂(xᵃ)。pow 方法有两个 double 类型的参数,其返回结果也为 double 类型。

floorMod 方法是为了解决一个长期存在的有关整数余数的问题。

提示:不必在数学方法名和常量名前添加前缀 “Math”,只要在源文件最上面加上下面这行代码就可以了。

1
import static java.lang.Math.*;

数据类型转换

IMG20251026163247

在图3-1中有6个实线箭头,表示无信息丢失的转换;另外有3个虚线箭头,表示可能有精度损失的转换。

当用一个二元运算符连接两个值时(例如 n + f,n 是整数,f 是浮点数),先要将两个操作数转换为同一种类型,然后再进行计算。

  • 如果两个操作数中有一个是 double 类型,另一个操作数就会转换为 double 类型。
  • 否则,如果其中一个操作数是 float 类型,另一个操作数将会转换为 float 类型。
  • 否则,如果其中一个操作数是 long 类型,另一个操作数将会转换为 long 类型。
  • 否则,两个操作数都将被转换为 int 类型。

强制类型转换

在必要的时候,int 类型的值将会自动地转换为 double 类型。但另一方面,有时也需要将 double 类型转换成 int 类型。在 Java 中,允许进行这种数值转换,不过当然可能会丢失一些信息。这种可能损失信息的转换要通过强制类型转换(cast)来完成。强制类型转换的语法格式是在圆括号中指定想要转换的目标类型,后面紧跟待转换的变量名。例如:

1
2
double x = 9.997;
int nx = (int) x;

这样,变量 nx 的值为9,因为强制类型转换通过截断小数部分将浮点值转换为整型。

如果想舍入(round)(四舍五入) 一个浮点数来得到最接近的整数(大多数情况下,这种操作更有用),可以使用 Math.round 方法:

1
2
double x = 9.997;
int nx = (int) Math.round(x);

现在,变量 nx 的值为10。调用 round 时,仍然需要使用强制类型转换(int)。原因是round方法的返回值是 long 类型,由于存在信息丢失的可能性,所以只有使用显式的强制类型转换才能够将一个 long 值赋给 int类 型的变量。

警告:如果试图将一个数从一种类型强制转换为另一种类型,而又超出了目标类型的表示范围,结果就会截断成一个完全不同的值。例如,(byte)300实际上会得到44。

赋值

可以在赋值中使用二元运算符,为此有一种很方便的简写形式。例如,

1
x += 4;

等价于:

1
x = x + 4;

(一般来说,要把运算符放在 = 号左边,如 *= 或 %= )。

警告:如果运算符得到一个值,其类型与左侧操作数的类型不同,就会发生强制类型转换。例如,如果 x 是一个 int,则以下语句

1
x += 3.5;

是合法的,将把 x 设置为 (int)(x+3.5)。

需要说明,在 Java 中 ,赋值是一个表达式(expression)。也就是说,它有一个值,具体来讲就是所赋的那个值。可以使用这个值完成一些操作,例如,可以把它赋给另一个变量。考虑以下语句:

1
2
int x = 1;
int y = x += 4;

x+=4的值是5,因为这是赋给x的值。然后将这个值赋给y。

在 Java 中,借鉴了 C 和 C++ 中的做法,也提供了自增、自减运算符: n++ 将变量 n 的当前值加1,n– 则将 n 的值减1。由于这些运算符会改变变量的值,所以不能对数值本身应用这些运算符。还有一种“前缀”形式: ++n。后缀和前缀形式都会使变量值加1或减1。但用在表达式中时,二者就有区别了。前缀形式会先完成加1;而后缀形式会使用变量原来的值。在 Java 中,很少在表达式中使用 ++。

关系运算符

Java 包含丰富的关系运算符。要检测相等性,可以使用两个等号==。

1
3 == 7

的值为false。

另外可以使用!=检测不相等。例如,

1
3 != 7

的值为true。

最后,还有经常使用的<(小于)、>(大于)、<=(小于等于)和>=(大于等于)运算符。

Java沿用了C++的做法,使用&&表示逻辑“与”运算符,使用||表示逻辑“或”运算符。从!=运算符可以想到,感叹号!就是逻辑非运算符。&&和||运算符是按照“短路”方式来求值的:如果第一个操作数已经能够确定表达式的值,第二个操作数就不必计算了。而且已经计算得到第一个表达式的真值为false,那么结果就不可能为true。因此,第二个表达式就不必计算了。

Java提供了conditional ?: 运算符,可以根据一个布尔表达式选择一个值。如果条件(condition)为true,表达式

1
condition? expression1: expression2

就计算为第一个表达式的值,否则为第二个表达式的值。

需要在两个以上的值中做出选择时,可以使用switch表达式(这是Java 14中引入的)

1
2
3
4
5
6
String seasonName = switch(seasonCode)
{case 0 -> "Spring";
case 1 -> "Summer";
case 2 -> "Fall";
case 3 -> "Winter";
default -> "?";};

case 标签还可以是字符串或枚举类型常量。

注释:与所有表达式类似,switch表达式也有一个值。注意各个分支中箭头->放在值前面。

注释:在Java 14中,switch有4种形式。

可以为各个case提供多个标签,用逗号分隔:

1
2
3
4
5
6
7
8
int numLetters = switch (seasonName)
{
case "Spring","Summer","Winter" -> 6;

case "Fall" -> 4;

default -> -1;
};

switch表达式中使用枚举常量时,不需要为各个标签提供枚举名,这可以从switch值推导得出。

例如:enum Size { SMALL,MEDIUM,LARGE,EXTRA_LARGE };

1
2
3
4
5
6
7
8
9
10
11
12
enum Size { SMALL,MEDIUM, LARGE,EXTRA_LARGE };
Size itemSize = . . . ;
String label = switch (itemSize)
{
case SMALL -> "S";// no need to use Size.SMALL

case MEDIUM -> "M";

case LARGE -> "L";

case EXTRA_LARGE -> "XL";
};

在这个例子中,完全可以省略default,因为每一个可能的值都有相应的一个case。

警告:使用整数或String操作数的switch表达式必须有一个default,因为不论操作数值是什么,这个表达式都必须生成一个值。

警告:如果操作数为null,会抛出一个NullPointerException。

处理整型类型时,还有一些运算符可以直接处理组成整数的各个位。这意味着可以使用掩码技术得到一个数中的各个位。位运算符包括:

& (“and”) | (“or”) ^ (“xor”) ~ (“not”)

这些运算符按位模式操作。

例如,如果n是一个整数变量,而且n的二进制表示中从右边数第4位为1,则

1
int fourthBitFromRight = (n & 0b1000) / 0b1000;

会返回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
2
String e ="";// an empty string
String greeting = "Hello";

String 类的 substring方法可以从一个较大的字符串提取出一个子串。例如:

1
2
String greeting = "Hello";
String s = greeting.substring(0,3);

会创建一个由字符 “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
2
3
String expletive = "Expletive";
String PG13 = "deleted";
String message = expletive + PG13;

上述代码将字符串”Expletivedeleted”赋给变量message(注意,单词之间没有空格,+号完全按照给定的次序将两个字符串拼接起来)。

当将一个字符串与一个非字符串的值进行拼接时,后者会转换成字符串(在第5章中可以看到,任何一个Java对象都可以转换成字符串)。例如:

1
2
int age = 13;
String rating= “PG” + age;

将把 rating 设置为 “PG13” 。

这个特性通常用在输出语句中。例如:

1
System.out.println("The answer is" + answer);

这是一条合法的语句,会打印出你希望的结果(因为单词 is 后面加了一个空格,输出时也会有这个空格)。

如果需要把多个字符串放在一起,用一个界定符分隔,可以使用静态join方法:

1
String all = String.join("/", "S","M", "L", "XL");// all is the string“S / M / L / XL”

在Java11中,还提供了一个repeat方法:

1
String repeated="Java".repeat(3);// repeated is“JavaJavaJava”

String 类没有提供任何方法来修改字符串中的某个字符。如果希望将 greeting 的内容修改为 “Help!” ,不能直接将greeting 的最后两个位置的字符修改为 ‘p’ 和 ‘!’ 。在 Java 中这很容易实现。可以提取想要保留的子串,再与希望替换的字符拼接:

1
2
3
String greeting = "Hello";

greeting = greeting.substring(0,3) + "p!";

上面这条语句将把 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.equals(t)

如果字符串 s 与字符串 t 相等,则返回 true ;否则,返回 false。需要注意的是,s 与 t 可以是字符串变量,也可以是字符串字面量。例如,以下表达式是合法的:

1
"Hello".equals(greeting)

要想检测两个字符串是否相等,而不区分大小写,可以使用 equalsIgnoreCase 方法。

1
"Hello".equalsIgnoreCase("hello")

不要使用 == 运算符检测两个字符串是否相等!这个运算符只能够确定两个字符串是否存放在同一个位置上。当然,如果字符串在同一个位置上,它们必然相等。但是,完全有可能将多个相等的字符串副本存放在不同的位置上。

1
2
3
4
5
String greeting = "Hello"; // initialize greeting to a string
if (greeting == "Hello") . . .
// probably true
if (greeting.substring(0, 3) == "Hel") . . .
// probably false

如果虚拟机总是共享相等的字符串,则可以使用 == 运算符检测字符串是否相等。但实际上只有字符串字面量会共享,而 + 或 substring 等操作得到的字符串并不共享。因此,千万不要使用 == 运算符测试字符串的相等,否则程序中会出现最糟糕的一种bug,这种bug可能会间歇性地随机出现。

空串 “” 是长度为0的字符串。可以调用以下代码检查一个字符串是否为空:

1
2
3
4
5
if (str.length() == 0)



if (str.equals(""))

空串是一个Java对象,有自己的串长度(0)和内容(空)。不过,String变量还可以存放一个特殊的值,名为null,表示目前没有任何对象与该变量关联(关于null的更多信息请参见第4章)。要检查一个字符串是否为null,可以使用以下代码:

1
if (str == null)

有时要检查一个字符串既不是null也不是空串,这种情况下可以使用:

1
if (str != null && str.length() != 0)

首先要检查str不为null。在第4章会看到,如果在一个null值上调用方法,会出现错误。

Java字符串是一个char值序列。char数据类型是采用UTF-16编码表示Unicode码点的一个代码单元。最常用的Unicode字符可以用一个代码单元表示,而辅助字符需要一对代码单元表示。

length方法将返回采用UTF-16编码表示给定字符串所需要的代码单元个数。例如:

1
2
String greeting="Hello";
int n = greeting.length();//is 5

要想得到实际长度,即码点个数,可以调用:

1
int cpCount = greeting.codePointCount(0, greeting.length());

调用 s.charAt(n) 将返回位置 n 的代码单元,n 介于 0 ~ s.length() - 1 之间。例如:

1
2
char first = greeting.charAt(0);//first is 'H'
char last = greeting.charAt(4);//last is 'o'

要想得到第 i 个码点,可以使用以下语句:

1
2
int index = greeting.offsetByCodePoints(0,i);
int cp = greeting.codePointAt(index);

如果想要遍历一个字符串,并且依次查看每一个码点,可以使用以下语句:

1
2
int cp = sentence.codePointAt(i);
i += Character.charCount(cp);

可以使用以下语句实现反向遍历:

1
2
3
i--;
if (Character.isSurrogate(sentence.charAt(i))) i--;
int cp = sentence.codePointAt(i);

显然,这很麻烦。更容易的办法是使用codePoints方法,它会生成int值的一个“流”,每个int值对应一个码点。(流在卷Ⅱ中介绍。)可以将流转换为一个数组(见3.10节),再完成遍历。

1
int[] codePoints = str.codePoints().toArray();

反之,要把一个码点数组转换为一个字符串,可以使用构造器(我们将在第4章详细讨论构造器和new操作符)。

1
String str = new String(codePoints, 0, codePoints.length);

要把单个码点转换为一个字符串,可以使用Character.toString(int)方法:

1
2
int codePoint = 0x1F37A;
str = Character.toString(codePoint);

注释:虚拟机不一定把字符串实现为代码单元序列。在Java 9中使用了一个更紧凑的表示。只包含单字节代码单元的字符串使用byte数组实现,所有其他字符串使用char数组。

本书中给出的API注释可以帮助你理解Java应用程序编程接口(API)。每一个API注释首先给出类名,如java.lang.String。(java.lang包名的重要性将在第4章给出解释。)类名之后是一个或多个方法的名字、解释和参数描述。API注释不会列出一个特定类的所有方法,而是会以简洁的方式给出最常用的一些方法,完整的方法列表请参见联机文档(请参见3.6.8节)。类名后面的编号是引入这个类的JDK版本号。如果某个方法是之后添加的,那么这个方法后面还会给出一个单独的版本号。

有些时候,需要由较短的字符串构建字符串,例如,按键或文件中的单词。如果采用字符串拼接的方式来达到这个目的,效率会比较低。每次拼接字符串时,都会构建一个新的 String 对象,既耗时,又浪费空间。使用 StringBuilder 类就可以避免这个问题。

如果需要用许多小字符串来构建一个字符串,可以采用以下步骤。首先,构建一个空的字符串构建器:

1
StringBuilder builder = new StringBuilder();

当每次需要添加另外一部分时,就调用 append 方法。

1
2
3
builder.append(ch);//appends a single character

String appendedString = builder.toString();

字符串构建完成时,调用 toString 方法。你会得到一个 String 对象,其中包含了构建器中的字符序列。

1
String completedString = builder.toString();

注释: StringBuffer 类的效率不如 StringBuilde 类,不过它允许采用多线程的方式添加或删除字符。如果所有字符串编辑操作都在单个线程中执行(通常都是这样),则应当使用 StringBuilder 类。这两个类的API是一样的。

利用 Java15 新增的 **文本块(text block)**特性,可以很容易地提供跨多行的字符串字面量。文本块以 “”” 开头(这是开始 “”” ),后面是一个换行符,并以另一个 “”” 结尾(这是结束 “”” ):

1
2
3
4
String greeting = """
Hello
World
""";

文本块比相应的字符串字面量更易于读写:

“Hello\nWorld\n”

这个字符串包含两个\n:一个在Hello后面,另一个在World后面。开始 “”” 后面的换行符不作为字符串字面量的一部分。

如果不想要最后一行后面的换行符,可以让结束 “”” 紧跟在最后一个字符后面:

1
2
3
String prompt = """
Hello, my name is Hal.
Please enter your name: """;

文本块特别适合包含用其他语言编写的代码,如 SQL 或 HTML。可以直接将那些代码粘贴到一对三重引号之间:

1
2
3
4
5
String html = """
<div class="Warning">
Beware of those who say "Hello" to the world
</div>
""";

需要说明的是,一般不用对引号转义。只有两种情况下需要对引号转义:

● 文本块以一个引号结尾。

● 文本块中包含三个或更多引号组成的一个序列。

遗憾的是,所有反斜线都需要转义。

常规字符串中的所有转义序列在文本块中也有同样的作用。

有一个转义序列只能在文本块中使用。行尾的\会把这一行与下一行连接起来。例如:

1
2
3
"""
Hello, my name is Hal. \
Please enter your name: "";

等同于:

“Hello, my name is Hal. Please enter your name:”

文本块会对行结束符进行标准化,删除末尾的空白符,并把Windows的行结束符 ( \r\n)改为简单的换行符( \n)。尽管不太可能,不过假如确实需要保留末尾的空格,这种情况下可以把最后一个空格转换为一个 \s 转义序列。

对于前导空白符则更为复杂。考虑一个从左边界缩进的典型的变量声明。文本块也可以缩进:

1
2
3
4
5
String html = """
<div class="Warning">
Beware of those who say "Hello" to the world
</div>
""";

将去除文本块中所有行的公共缩进。实际字符串为:

“<div class=\“Warning\“>\n Beware of those who say \“Hello\“ to the world\n

\n”

注意,第一行和第三行没有缩进。

你的IDE很可能会使用制表符、空格或者制表符以及空格缩进所有文本块。

Java很明智,它没有规定制表符的宽度。空白符前缀必须与文本块中的所有行完全匹配。去除缩进过程中不考虑空行。不过,结束 “”” 前面的空白符很重要。一定要缩进到想要去除的空白符的末尾。

警告:要当心缩进文本块的公共前缀中混用制表符和空格的情况。不小心漏掉一个空格很容易得到一个缩进错误的字符串。

提示:如果一个文本块中包含非Java代码,实际上最好沿左边界放置。这样可以与Java代码区分开,而且可以为长代码行留出更多空间。

输入与输出

前面已经看到,将输出打印到“标准输出流”(即控制台窗口)是一件非常容易的事情,只需要调用 System.out.println 。不过,读取“标准输入流” System.in 就没有那么简单了。要想读取控制台输入,首先需要构造一个与“标准输入流” System.in 关联的 Scanner 对象。

1
Scanner in = new Scanner(System.in);

现在,就可以使用 Scanner 类的各种方法读取输入了。例如,nextLine 方法将读取一行输入。

1
2
System.out.print("What is your name? ");
String name = in.nextLine();

在这里,使用 nextLine 方法是因为输入行中有可能包含空格。要想读取一个单词(以空白符作为分隔符),可以调用

1
String firstName = in.next();

要想读取一个整数,要使用 nextInt 方法。

1
2
System.out.print("How old are you? ");
int age = in.nextInt();

与此类似,nextDouble 方法可以读取下一个浮点数。

在程序清单3-2的程序中,首先询问用户姓名和年龄,然后打印一条如下的消息:

Hello,Cay. Next year,you’ll be 57

最后,在程序的最前面添加一行代码:

1
import java.util.*;

Scanner类在 java.util 包中定义。当使用的类不是定义在基本 java.lang 包中时,需要使用import指令导入相应的包。

程序清单3-2 InputTest/UploadTest.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
1 import java.util.*;
2
3/**
4 * This program demonstrates console input.
5 * @version 1.10 2004-02-10
6 * @author Cay Horstmann
7 */
8 public class InputTest
9 {
10 public static void main(String[] args)
11 {
12 Scanner in = new Scanner(System.in);
13
14 // get first input
15 System.out.print("What is your name? ");
16 String name = in.nextLine();
17
18 // get second input
19 System.out.print("How old are you? ");
20 int age = in.nextInt();
21
22 // display output on console
23 System.out.println("Hello, " + name + ". Next year, you'll be " + (age + 1));
24 }
25 }

注释:因为输入对所有人都可见,所以Scanner类不适用于从控制台读取密码。可以使用Console类来达到这个目的。要想读取一个密码,可以使用以下代码:

1
2
3
4
5
Console cons = System.console();

String username = cons.readLine("User name: ");

char[] passwd = cons.readPassword("Password: ");

为安全起见,将返回的密码存放在一个字符数组中,而不是字符串中。完成对密码的处理之后,应该马上用一个填充值覆盖数组元素。使用Console对象处理输入不如使用Scanner方便。必须一次读取一行输入,而且Console类没有提供方法来读取单个单词或数字。

格式化输出

可以使用 System.out.print(x) 语句将数值 x 输出到控制台。这个命令将以 x 的类型所允许的最大非 0 位数打印 x 。例如:

1
2
double x = 10000.0 / 3.0;
System.out.print(x);

会打印

3333.3333333333335

如果希望显示美元、美分数,这就会有问题。

这个问题可以利用 printf 方法来解决,它沿用了C语言函数库中的古老约定。例如,以下调用

1
System.out.printf("%8.2f", x);

打印 x 时**字段宽度(field width)**为8个字符,精度为2个字符。也就是说,结果包含一个前导的空格和7个字符,如下所示:

3333.33

可以为 printf 提供多个参数,例如:

1
System.out.printf("Hello, %s. Next year, you'll be %d", name, age);

每一个以 % 字符开头的**格式说明符(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
System.out.printf("%,.2f", 10000.0 / 3.0);

会打印

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
String message = String.format("Hello, %s. Next year, you'll be %d", name, age + 1);

注释:在 Java 15 中,可以使用 formatted 方法,这样可以少敲 5 个字符:

1
String message = "Hello, %s. Next year, you'll be %d".formatted(name, age + 1);

现在,我们已经了解了 printf 方法的所有特性。图 3-6 给出了格式说明符的语法图。

image-20251029150720800

图 3-6 格式说明符语法

注释:格式化规则是特定于本地化环境的。例如,在德国,分组分隔符是点号而不是逗号。

3.7.3 文件输入与输出

要想读取一个文件,需要构造一个 Scanner 对象,如下所示:

1
Scanner in = new Scanner(Path.of("myfile.txt"), StandardCharsets.UTF_8);

如果文件名中包含反斜线符号,记住要在每个反斜线之前再加一个额外的反斜线转义:

1
"c:\\mydirectory\\myfile.txt"

现在就可以使用之前见过的任何 Scanner 方法读取这个文件了。

注释:这里指定了 UTF-8 字符编码,这对于互联网上的文件很常见(不过并不是普通通用)。读取一个文本文件时,要知道它的字符编码。如果省略字符编码,则会使用运行这个 Java 程序的机器的”默认编码”。这不是一个好主意,如果在不同的机器上运行这个程序,可能会有不同的表现。

警告:可以提供一个字符串参数来构造一个 Scanner,但这个 Scanner 会把字符串解释为数据,而不是文件名。例如,如果调用:

1
Scanner in = new Scanner("myfile.txt"); // ERROR?

这个 Scanner 会将参数看作是包含 10 个字符(’m’, ‘y’, ‘f’ 等)的数据。这可能是我们的原意。

要想写入文件,需要构造一个 PrintWriter 对象。在构造器(constructor)中,需要提供文件名和字符编码:

1
PrintWriter out = new PrintWriter("myfile.txt", StandardCharsets.UTF_8);

如果文件不存在,则创建该文件。可以像输出到 System.out 一样使用 printprintln 以及 printf 命令。

注释:当指定一个相对文件名时,例如,”myfile.txt”、”mydirectory/myfile.txt” 或 “../myfile.txt”,文件将相对于启动 Java 虚拟机的那个目录放置。如果从一个命令 shell 执行以下命令启动程序:

1
java MyProg

启动目录就是命令 shell 的当前目录。不过,如果使用集成开发环境,那么启动目录将由 IDE 控制。可以使用下面的调用找到这个目录的位置:

1
String dir = System.getProperty("user.dir");

如果觉得文件定位太麻烦,可以考虑使用绝对路径名,例如:”c:\mydirectory\myfile.txt” 或者 “/home/me/mydirectory/myfile.txt”。

要记住一点:如果用一个不存在的文件构造一个 Scanner,或者用一个无法创建的文件名构造一个 PrintWriter,就会产生异常。Java 编译器认为这些异常比”被零除”异常更严重。为此,要用一个 throws 子句标记 main 方法,如下所示:

1
2
3
4
public static void main(String[] args) throws IOException
{
Scanner in = new Scanner(Path.of("myfile.txt"), StandardCharsets.UTF_8);
}

注释:从命令 shell 启动一个程序时,可以利用 shell 的重定向语法将任意文件关联到 System.inSystem.out

1
java MyProg < myfile.txt > output.txt

这样,就不必担心处理 IOException 异常了。

3.8 控制流程

与任何程序设计语言一样,Java 支持使用条件语句和循环结构来确定控制流程。这里首先讨论条件语句,然后介绍循环语句,最后介绍 switch 语句,它可以用来检测一个表达式的多个值。

注释:Java 中没有 goto 语句,但 break 语句可以带标签,可以利用它从嵌套循环中跳出。

3.8.1 块作用域

在学习控制结构之前,需要了解块(block)的概念。

块(即复合语句)由若干条 Java 语句组成,并用一对大括号括起来。块确定了变量的作用域。一个块可以嵌套在另一个块中。下面就是嵌套在 main 方法块中的一个块。

1
2
3
4
5
6
7
8
public static void main(String[] args)
{
int n;
...
{
int k;
} // k is only defined up to here
}

但是,不能在嵌套的两个块中声明同名的变量。例如,下面的代码就有错误,而无法通过编译:

1
2
3
4
5
6
7
8
9
10
public static void main(String[] args)
{
int n;
...
{
int k;
int n; // ERROR - can't redeclare n in inner block
...
}
}

3.8.2 条件语句

在 Java 中,条件语句的形式为:

1
if (condition) statement

这里的条件必须用小括号括起来。

在 Java 中,常常希望在某个条件为真时执行多条语句。在这种情况下,就可以使用块语句(block statement),形式如下:

1
2
3
4
5
{
statement1
statement2
. . .
}

例如:

1
2
3
4
5
if (yourSales >= target)
{
performance = "Satisfactory";
bonus = 100;
}

yourSales 大于或等于 target 时,将执行大括号中的所有语句(请参见图 3-7)。

注释:使用块(有时称为复合语句)可以在 Java 程序结构中原本只能放置一条(简单)语句的地方放置多条语句。

在 Java 中,更一般的条件语句如下所示(请参见图 3-8):

1
if (condition) statement1 else statement2

例如:

1
2
3
4
5
6
7
8
9
10
if (yourSales >= target)
{
performance = "Satisfactory";
bonus = 100 + 0.01 * (yourSales - target);
}
else
{
performance = "Unsatisfactory";
bonus = 0;
}

其中 else 部分总是可选的。else 子句与最邻近的 if 构成一组。因此,在以下语句中:

1
if (x <= 0) if (x == 0) sign = 0; else sign = -1;

else 与第 2 个 if 配对。当然,使用大括号可以让这段代码更加清晰:

1
if (x <= 0) { if (x == 0) sign = 0; else sign = -1; }

image-20251029152221204 image-20251029152244833

图 3-7 if 语句的流程图 图 3-8 if/else 语句的流程图

反复使用 if...else if... 很常见(如图 3-9 所示)。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
if (yourSales >= 2 * target)
{
performance = "Excellent";
bonus = 1000;
}
else if (yourSales >= 1.5 * target)
{
performance = "Fine";
bonus = 500;
}
else if (yourSales >= target)
{
performance = "Satisfactory";
bonus = 100;
}
else
{
System.out.println("You're fired");
}

3.8.3 循环

while 循环会在条件为 true 时执行一个语句(也可以是一个块语句)。一般形式如下:

1
while (condition) statement

image-20251029152937051

图3-9 if/else if(多分支)的流程图

如果开始时循环条件就为 false,那么 while 循环一次也不执行(请参见图3-10)。

程序清单3-3中的程序将计算需要多长时间才能够存下一定数量的退休金,假定每年存入相同金额,而且利率是固定的。在这个示例中,我们会让一个计数器递增,并在循环体中更新当前的累积金额,直到总金额超过目标金额为止。

1
2
3
4
5
6
7
8
while (balance < goal)
{
balance += payment;
double interest = balance * interestRate / 100;
balance += interest;
years++;
}
System.out.println(years + " years.");

while 循环语句在最前面检测循环条件。因此,循环体中的代码有可能一次都不执行。如果希望循环体至少执行一次,需要使用 do/while 循环将检测放在最后。它的语法如下:

1
do statement while (condition);

这种循环先执行语句(通常是一个语句块),然后再检查循环条件。如果条件为 true,就重复执行语句,然后再依次检测循环条件,依此类推。首先计算退休账户中新的余额,然后再询问是否打算退休:

1
2
3
4
5
6
7
8
9
10
11
12
do
{
balance += payment;
double interest = balance * interestRate / 100;
balance += interest;
years++;
// print current balance
...
// ask if ready to retire and get input
...
}
while (input.equals("N"));

只要用户回答 “N”,循环就会重复执行(见图 3-11)。这是一个很好的例子,它展示了一个至少需要执行一次的循环,因为用户必须先看到余额才能决定是否满足退休所用。

image-20251029153213520 image-20251029153231944

图 3-10 while 语句的流程图 图 3-11 do/while 语句的流程图

3.8.4 确定性循环

for 循环语句是支持迭代的一种通用结构,它由一个计数器或类似的变量控制迭代次数,每次迭代后这个变量将会更新。如图 3-12 所示,下面的循环将在屏幕上显示出打印数字 1~10:

1
2
for (int i = 1; i <= 10; i++)
System.out.println(i);

for 语句的第 1 部分通常是对计数器初始化;第 2 部分给出每次新一轮循环执行前要检测的循环条件;第 3 部分指定如何更新计数器。

与 C++ 类似,尽管 Java 允许在 for 循环的各个部分放置任何表达式,但有一条不成立的规则:for 语句的 3 个部分应该对同一个计数器变量进行初始化、检测和更新。

下面这个倒计数的循环:

1
2
3
for (int i = 10; i > 0; i--)
System.out.println("Counting down . . . " + i);
System.out.println("Blastoff!");

警告:在循环中,检测两个浮点数是否相等需要格外小心。下面的 for 循环:

1
for (double x = 0; x != 10; x += 0.1) ...

可能永远不会结束。由于存在舍入误差,可能永远达不到精确的最终值。在这个例子中,因为 0.1 无法精确地用二进制表示,所以,x 将从 9.9999999999998 跳到 10.099999999999998。

在 for 语句的第 1 部分中声明一个变量之后,这个变量的作用域会扩展到这个 for 循环体的末尾。

1
2
3
4
5
for (int i = 1; i <= 10; i++)
{
...
}
// i no longer defined here

特别指出,如果在 for 语句内部定义一个变量,这个变量就不能在循环体之外使用。因此,如果希望在 for 循环体之外使用循环计数器的最终值,就要确保在循环体之外声明这个变量。

1
2
3
4
5
6
int i;
for (i = 1; i <= 10; i++)
{
...
}
// i is still defined here

另一方面,可以在不同的 for 循环中定义同名的变量:

1
2
3
4
5
6
7
8
for (int i = 1; i <= 10; i++)
{
...
}
for (int i = 11; i <= 20; i++) // OK to define another variable named i
{
...
}

image-20251029153845994

图 3-12 for 语句的流程图

for 循环语句只是 while 循环的一种简化形式。例如,

1
2
for (int i = 10; i > 0; i--)
System.out.println("Counting down . . . " + i);

可以重写为:

1
2
3
4
5
6
int i = 10;
while (i > 0)
{
System.out.println("Counting down . . . " + i);
i--;
}

注释:3.10.3 节将会介绍”泛型 for 循环”(又称为 for each 循环),利用这个循环可以很方便地访问一个数组或集合中的所有元素。

3.8.5 多重选择:switch 语句

在处理同一个表达式的多个选项时,使用 if/else 结构会显得有些笨拙。switch 语句会让这个工作变得容易,特别是采用 Java 14 引入的形式时会更简单。

例如,如果建立一个如图 3-13 所示的菜单系统,其中包含 4 个选项,可以使用以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
Scanner in = new Scanner(System.in);
System.out.print("Select an option (1, 2, 3, 4) ");
int choice = in.nextInt();

switch (choice)
{
case 1 -> ...
case 2 -> ...
case 3 -> ...
case 4 -> ...
default -> System.out.println("Bad input");
}

case 标签可以是:

  • 类型为 char、byte、short 或 int 的常量表达式
  • 枚举常量
  • 字符串字面量
  • 多个字符串,用逗号分隔

例如:

1
2
3
4
5
6
7
String input = ...;
switch (input.toLowerCase())
{
case "yes", "y" -> ...
case "no", "n" -> ...
default -> ...
}

switch 语句的”经典”形式可以追溯到 C 语言,从 Java 1.0 开始就支持这种形式。具体形式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int choice = ...;
switch (choice)
{
case 1:
...
break;
case 2:
...
break;
case 3:
...
break;
case 4:
...
break;
default:
// bad input
...
break;

image-20251029155524670

图 3-13 Switch 语句的流程图

switch 语句从与选项值相匹配的 case 标签开始执行,直到遇到下一个 break 语句,或者执行到 switch 语句结束。如果没有匹配的 case 标签,则执行 default 子句(如果有 default 子句)。

警告有可能触发多个分支。如果忘记在一个分支末尾增加 break 语句,就会接着执行下一个分支!

为了检测这种问题,编译代码时可以加上 -Xlint:fallthrough 选项,如下所示:

1
javac -Xlint:fallthrough Test.java

这样一来,如果某个分支最后缺少一个 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
2
3
4
case "Spring" -> {
System.out.println("spring time!");
yield 6;
}

switch 表达式的每个分支必须生成一个值。最常见的做法是,各个值跟在一个箭头 -> 后面:

1
case "Summer", "Winter" -> 6;

如果无法做到,则使用 yield 语句。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
// 表 3-7  4 种 switch 形式

// 表达式形式 - 无直通行为
int numLetters = switch (seasonName) {
case "Spring" -> {
System.out.println("spring time!");
yield 6;
}
case "Summer", "Winter" -> 6;
case "Fall" -> 4;
default -> -1;
};

// 语句形式 - 无直通行为
switch (seasonName) {
case "Spring" -> {
System.out.println("spring time!");
numLetters = 6;
}
case "Summer", "Winter" -> numLetters = 6;
case "Fall" -> numLetters = 4;
default -> numLetters = -1;
}

// 表达式形式 - 有直通行为
int numLetters = switch (seasonName) {
case "Spring":
System.out.println("spring time!");
case "Summer", "Winter":
yield 6;
case "Fall":
yield 4;
default:
yield -1;
};

// 语句形式 - 有直通行为
switch (seasonName) {
case "Spring":
System.out.println("spring time!");
case "Summer", "Winter":
numLetters = 6;
break;
case "Fall":
numLetters = 4;
break;
default:
numLetters = -1;
}

注释:完全可以在 switch 表达式的一个分支中抛出异常。例如:

1
default -> throw new IllegalArgumentException("Not a valid season");

警告:switch 表达式的关键是生成一个值(或者产生一个异常而失败)。不允许”跳出”switch 表达式。

1
default -> { return -1; } // ERROR

具体来说,不能在 switch 表达式中使用 returnbreakcontinue 语句。

switch 表达式优于语句。如果每个分支会为一个变量赋值或方法调用计算值,则用一个表达式生成值,然后使用这个值。例如:

1
2
3
4
5
numLetters = switch (seasonName) {
case "Spring", "Summer", "Winter" -> 6;
case "Fall" -> 4;
default -> -1;
};

要优于

1
2
3
4
5
switch (seasonName) {
case "Spring", "Summer", "Winter" -> numLetters = 6;
case "Fall" -> numLetters = 4;
default -> numLetters = -1;
}

只有在确实需要直通行为时,或者必须为一个 switch 表达式增加语句时,才需要使用 break 或 yield 。

3.8.6 中断控制流程的语句

尽管 Java 的设计者将 goto 仍作为一个保留字,但实际上并不打算在语言中包含 goto

下面首先来看不带标签的 break 语句。与用于退出 switch 语句的 break 语句一样,它也可以用于退出循环。例如,

1
2
3
4
5
6
7
8
while (years <= 100)
{
balance += payment;
double interest = balance * interestRate / 100;
balance += interest;
if (balance >= goal) break;
years++;
}

循环开始时,如果 years > 100,或者如果循环中间 balance >= goal,则退出循环。当然,也可以在不使用 break 的情况下计算 years 的值,如下所示:

1
2
3
4
5
6
7
8
while (years <= 100 && balance < goal)
{
balance += payment;
double interest = balance * interestRate / 100;
balance += interest;
if (balance < goal)
years++;
}

但是需要注意,在这个版本中,检测了两次 balance < goal。为了避免重复检测,有些程序员更偏爱使用 break 语句。

与 C++ 不同,Java 还提供了一种带标签的 break 语句,允许跳出多重嵌套的循环。有时候,在嵌套很深的循环语句中会发生一些不可预料的事情。此时你可能希望完全跳出所有嵌套循环。如果只是为各层循环检测添加一些额外的条件,这会很不方便。

下面的例子展示了 break 语句如何工作。请注意,标签必须放在你想跳出的最外层循环之前,并且必须紧跟一个冒号。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
Scanner in = new Scanner(System.in);
int n;
read_data:
while (...) // this loop statement is tagged with the label
{
// ...
for (...) // this inner loop is not labeled
{
System.out.print("Enter a number >= 0: ");
n = in.nextInt();
if (n < 0) // should never happen-can't go on
break read_data;
// break out of read_data loop
...
}
}
// this statement is executed immediately after the labeled break
if (n < 0) // check for bad situation
{
// deal with bad situation
}
else
{
// carry out normal processing
}

如果输入有误,执行带标签的 break 会跳转到带标签的语句块末尾。与任何使用 break 语句的代码一样,接下来需要检测循环是正常退出,还是由于 break 提前退出。

注释: 有意思的是,可以将标签应用到任何语句,甚至可以应用到 if 语句或者块语句,如下所示:

1
2
3
4
5
6
7
label:
{
....
if (condition) break label; // exits block
...
}
// jumps here when the break statement executes

因此,如果确实希望使用 goto 语句,而且一个代码块恰好在你想要跳转到的位置之前结束,就可以使用 break 语句!当然,我并不提倡使用这种方法。另外需要注意,只能跳出语句块,而不能跳入语句块。

最后,还有一个 continue 语句。与 break 语句一样,它将中断正常的控制流程。continue 语句将控制转移到最内层外围循环的首部。例如:

1
2
3
4
5
6
7
8
Scanner in = new Scanner(System.in);
while (sum < goal)
{
System.out.print("Enter a number: ");
n = in.nextInt();
if (n < 0) continue;
sum += n; // not executed if n < 0
}

如果 n < 0,则 continue 语句会越过当前循环体的剩余部分,直接跳到循环首部。

如果在 for 循环中使用 continue 语句,会跳转到 for 循环的”更新”部分。例如:

1
2
3
4
5
6
7
for (count = 1; count <= 100; count++)
{
System.out.print("Enter a number, -1 to quit: ");
n = in.nextInt();
if (n < 0) continue;
sum += n; // not executed if n < 0
}

如果 n < 0,则 continue 语句将跳转到 count++ 语句。

还有一种带标签的 continue 语句,将跳转到有匹配标签的循环的首部。

提示: 许多程序员发现 breakcontinue 语句很容易混淆。这些语句完全是可选的,即不使用这些语句也能表达同样的逻辑。在本书中,所有程序都不会使用 breakcontinue

3.9 大数

如果基本的整数和浮点数精度不足以满足需求,那么可以使用 java.math 包中两个很有用的类:BigIntegerBigDecimal。这两个类可以处理包含任意长度数字序列的数值。BigInteger 类实现任意精度的整数运算,BigDecimal 实现任意精度的浮点数运算。

使用静态的 valueOf 方法可以将一个普通的数转换为大数:

1
BigInteger a = BigInteger.valueOf(100);

对于更长的数,可以使用一个带字符串参数的构造器:

1
BigInteger realV = BigInteger("222232244629420445529739893461909967206666939096499764999979600");

另外还有一些常量:BigInteger.ZEROBigInteger.ONEBigInteger.TEN,Java 9 之后还增加了 BigInteger.TWO

警告: 对于 BigDecimal 类,总是应当使用带一个字符串参数的构造器。还有一个 BigDecimal(double) 构造器,不过这个构造器本质上很容易产生舍入误差,例如,new BigDecimal(0.1) 会得到以下数位:

1
0.10000000000000055511151231257827021181583404541015625

不能使用人们熟悉的算术运算符(如:+*)来组合大数,而需要使用大数类中的 addmultiply 方法。

1
2
BigInteger c = a.add(b); // c = a + b
BigInteger d = c.multiply(b.add(BigInteger.valueOf(2))); // d = c * (b + 2)

3.10 数组

数组存储相同类型值的序列。

3.10.1 声明数组

数组是一种数据结构,用来存储同一类型值的集合。通过一个整型索引(index,或称下标)可以访问数组中的每一个值。例如,如果 a 是一个整型数组,a[i] 就是数组中索引为 i 的整数。

在声明数组变量时,需要指出数组类型(元素类型后面紧跟 [])和数组变量名。例如,下面声明了整型数组 a

1
int[] a;

不过,这条语句只声明了变量 a,并没有将 a 初始化为一个真正的数组。应该使用 new 操作符创建数组。

1
int[] a = new int[100]; // or var a = new int[100];

这条语句声明并初始化了一个可以存储 100 个整数的数组。

数组长度不要求是常量:new int[n] 会创建一个长度为 n 的数组。

一旦创建了数组,就不能再改变它的长度(不过,当然可以改变单个数组元素)。如果程序运行中需要经常扩展数组的大小,就应该使用另一种数据结构——数组列表(array list)。

注释: 可以使用下面两种形式定义一个数组变量:

1
int[] a;

1
int a[];

第一种风格,它可以将类型 int[](整型数组)与变量名清晰地分开。

在 Java 中,提供了一种创建数组对象并同时提供初始值的简写形式。下面是一个例子:

1
int[] smallPrimes = { 2, 3, 5, 7, 11, 13 };

请注意,这个语法中不需要使用 new,甚至不用指定长度。

最后一个值后面允许有逗号,如果你要不断为数组增加值,这样会很方便:

1
2
3
4
5
6
7
String[] authors =
{
"James Gosling",
"Bill Joy",
"Guy Steele",
// add more names here and put a comma after each name
};

还可以声明一个匿名数组(anonymous array):

1
new int[] { 17, 19, 23, 29, 31, 37 };

这个表达式会分配一个新数组并填入大括号中提供的值。它会统计初始值个数,并相应地设置数组大小。可以使用这种语法重新初始化一个数组而无须创建新变量。例如:

1
smallPrimes = new int[] { 17, 19, 23, 29, 31, 37 };

这是以下语句的简写形式:

1
2
int[] anonymous = { 17, 19, 23, 29, 31, 37 };
smallPrimes = anonymous;

注释: 在 Java 中,允许有长度为 0 的数组。编写一个结果为数组的方法时,如果碰巧结果为空,这样一个长度为 0 的数组就很有用。可以如下构造一个长度为 0 的数组:

1
new elementType[0]

1
new elementType[] {}

注意,长度为 0 的数组与 null 并不相同。

3.10.2 访问数组元素

数组元素从 0 开始编号。最后一个合法的索引为数组长度减 1。在下面的例子中,索引值为 0~99。一旦创建了数组,就可以在数组中填入元素。例如,可以使用一个循环:

1
2
3
int[] a = new int[100];
for (int i = 0; i < 100; i++)
a[i] = i; // fills the array with numbers 0 to 99

创建一个数字数组时,所有元素都初始化为 0。boolean 数组的元素会初始化为 false。对象数组的元素则初始化为一个特殊值 null,表示这些元素(还)未存放任何对象。初学者对此可能有些不解。例如,

1
String[] names = new String[10];

会创建一个包含 10 个字符串的数组,(所有字符串都为 null)。如果希望这个数组包含空串,则必须为元素提供空串:

1
for (int i = 0; i < 10; i++) names[i] = "";

警告: 如果创建了一个包含 100 个元素的数组,然后试图访问元素 a[100](或在 0~99 之外的任何其他索引),就会出现”array index out of bounds”(数组索引越界)异常。

要想获得数组中的元素个数,可以使用 array.length。例如,

1
2
for (int i = 0; i < a.length; i++)
System.out.println(a[i]);

3.10.3 for each 循环

Java 有一种功能很强的循环结构,可以用来依次处理数组(或者任何其他元素集合)中的每个元素,而不必考虑指定索引值。

这种增强的 for 循环形式如下:

1
for (variable : collection) statement

它将给定变量(variable)设置为集合中的每一个元素,然后执行语句(statement)(当然,也可以是语句块)。collection 表达式必须是一个数组或者是一个实现了 Iterable 接口的类对象(例如 ArrayList)。

例如,

1
2
for (int element : a)
System.out.println(element);

会打印数组 a 的每一个元素,一个元素占一行。

这个循环应该读作”循环 a 中的每一个元素”(for each element in a)。Java 语言的设计者也曾考虑过使用诸如 foreachin 这样的关键字,但这种循环并不是最初就包含在 Java 语言中,而是后来添加的,没有人希望破坏已经包含同名方法或变量(例如 System.in)的老代码。

当然,使用传统的 for 循环也可以获得同样的效果:

1
2
for (int i = 0; i < a.length; i++)
System.out.println(a[i]);

但是,”for each”循环更加简洁、更不易出错,因为你不必为起始和终止索引值而操心。

注释: “for each”循环的循环变量将会遍历数组中的每个元素,而不是索引值。

如果需要处理一个集合中的所有元素,相比传统循环,”for each”循环是个让人欣喜的改进。不过,很多情况下还是需要使用传统的 for 循环。例如,可能不希望遍历整个集合,或者可能需要在循环内部使用索引值。

提示: 有一个更容易的方法可以打印数组中的所有值,即利用 Arrays 类的 toString 方法。调用 Arrays.toString(a) 会返回一个包含数组元素的字符串,这些元素包围在中括号内,并用逗号分隔,例如,"[2,3,5,7,11,13]"。要想打印数组,只需要调用

1
System.out.println(Arrays.toString(a));

3.10.4 数组拷贝

在 Java 中,允许将一个数组变量拷贝到另一个数组变量。这时,两个变量将引用同一个数组:

1
2
int[] luckyNumbers = smallPrimes;
luckyNumbers[5] = 12; // now smallPrimes[5] is also 12

图 3-14 显示了拷贝的结果。

image-20251029231328013

图 3-14 拷贝一个数组变量

如果确实希望将一个数组的所有值拷贝到一个一个新的数组中,就要使用Arrays类的copyOf方法:

1
int[] copiedLuckyNumbers = Arrays.copyOf(luckyNumbers, luckyNumbers.length);

第2个参数是新数组的长度。这个方法通常用来增加数组的大小:

1
luckyNumbers = Arrays.copyOf(luckyNumbers, 2 * luckyNumbers.length);

如果数组元素是数值型,那么新增的元素将其入0;如果数组元素是布尔型,则填入false。相反,如果长度小于原数组的长度,则只拷贝前面的值。

C++注释: Java数组与堆栈上的C++数组有很大不同,但基本上与在堆(heap)上分配的数组指针一样。也就是说,

1
int[] a = new int[100]; // Java

不同于

1
int a[100]; // C++

而等同于

1
int* a = new int[100]; // C++

Java 中的 [ ] 运算符预定义为会完成越界检查(bounds checking)。另外,没有指针运算,就意味着不能通过a加1得到数组中的下一个元素。

3.10.5 命令行参数

前面已经看到一个例子,其中一个Java数组重复出现过很多次。每一个Java程序都有一个带String arg[]参数的main方法。这个参数表明main方法将接收一个字符串数组,也就是命令行上指定的参数。

例如,来看下面这个程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Message
{
public static void main(String[] args)
{
if (args.length == 0 || args[0].equals("-h"))
System.out.print("Hello,");
else if (args[0].equals("-g"))
System.out.print("Goodbye,");
// print the other command-line arguments
for (int i = 1; i < args.length; i++)
System.out.print(" " + args[i]);
System.out.println("!");
}
}

如果如下调用这个程序:
java Message -g cruel world
args 数组将包含以下内容:

1
2
3
args[0]: "-g"
args[1]: "cruel"
args[2]: "world"

这个程序会显示下面这个消息:

1
Goodbye, cruel world!

C++ 注释: 在 Java 程序的 main 方法中,程序名并不存储在 args 数组中。例如,从命令行如下运行一个程序时

1
java Message -h world

args[0]"-h",而不是 "Message""java"

3.10.6 数组排序

要想对数值型数组进行排序,可以使用 Arrays 类中的 sort 方法:

1
2
3
int[] a = new int[10000];
. . .
Arrays.sort(a);

这个方法使用了优化的快速排序(QuickSort)算法。快速排序算法对于大多数数据集都很高效。

程序清单 3-7 中的程序具体使用了数组,它会为一个抽彩游戏生成一个随机的数字组合。例如,假如从 49 个数字中抽取 6 个数,那么程序可能的输出结果为:

1
2
3
4
5
6
7
Bet the following combination. It'll make you rich!
4
7
8
19
38
44

要想选择这样一组随机的数字,首先将值 1,2,…,n 填入数组 numbers 中:

1
2
3
int[] numbers = new int[n];
for (int i = 0; i < numbers.length; i++)
numbers[i] = i + 1;

第二个数组存放抽取出来的数:

1
int[] result = new int[k];

现在可以开始抽取 k 个数了。Math.random 方法将返回一个 0 到 1 之间(包含 0、不包含 1)的随机浮点数。用 n 乘以这个浮点数,可以得到从 0 到 n-1 之间的一个随机数。

1
int r = (int) (Math.random() * n);

下面将 result 的第 i 个元素设置为该索引对应的数(numbers[r]),最初是 r+1,但正如所看到的,numbers 数组的内容在每一次抽取之后都会发生变化。

1
result[i] = numbers[r];

现在,必须确保不会再次抽到那个数,因为所有抽形数字必须各不相同。因此,这里用数组中的最后一个数覆盖 number[r],并将 n 减 1。

1
2
numbers[r] = numbers[n - 1];
n--;

关键在于每次抽取的都是索引,而不是实际的值。这个索引指向一个数组,其中包含尚未抽取过的值。在抽取了 k 个数之后,可以对 result 数组进行排序,来得到更美观的输出:

1
2
3
Arrays.sort(result);
for (int r : result)
System.out.println(r);

3.10.7 多维数组

多维数组使用多个索引访问数组元素,它适用于表格或其他更复杂的排列形式。

假设需要建立一个数值表格,用来显示在不同利率下投资 10 000 美元有多少收益,利息每年兑现并复投(见表 3-8)。

表 3-8 不同利率下的投资收益情况

1
2
3
4
5
6
7
8
9
10
11
     10%    11%    12%    13%    14%    15%
10 000.00 10 000.00 10 000.00 10 000.00 10 000.00 10 000.00
11 000.00 11 100.00 11 200.00 11 300.00 11 400.00 11 500.00
12 100.00 12 321.00 12 544.00 12 769.00 12 996.00 13 225.00
13 310.00 13 676.31 14 049.28 14 428.97 14 815.44 15 208.75
14 641.00 15 180.70 15 735.19 16 304.74 16 889.60 17 490.06
16 105.10 16 850.58 17 623.42 18 424.35 19 254.15 20 113.57
17 715.61 18 704.15 19 738.23 20 819.52 21 949.73 23 130.61
19 487.17 20 761.60 22 106.81 23 526.05 25 022.69 26 600.20
21 435.89 23 045.38 24 759.63 26 584.44 28 525.86 30 590.23
23 579.48 25 580.37 27 730.79 30 040.42 32 519.49 35 178.76

可以使用一个二维数组(也称为矩阵)来存储这些信息,名为 balances。在 Java 中,声明一个二维数组相当简单。例如:

1
double[][] balances;

数组在进行初始化之前是不能使用的。在这里可以如下初始化:

1
balances = new double[NYEARS][NRATES];

其他情况下,如果知道数组元素,可以不调用 new,而直接使用一种简写形式对多维数组进行初始化。例如:

1
2
3
4
5
6
7
int[][] magicSquare =
{
{16, 3, 2, 13},
{5, 10, 11, 8},
{9, 6, 7, 12},
{4, 15, 14, 1}
};

一旦初始化数组,就可以利用两对中括号访问单个元素,例如,balances[i][j]

在示例程序中用到了一个存储利率的一维数组 interest 和一个存储账户余额的二维数组 balances(对应每个年度和利率分别有一个余额),使用初始余额来初始化这个数组的第一行:

1
2
for (int j = 0; j < balances[0].length; j++)
balances[0][j] = 10000;

然后计算其他行,如下所示:

1
2
3
4
5
6
7
8
9
for (int i = 1; i < balances.length; i++)
{
for (int j = 0; j < balances[i].length; j++)
{
double oldBalance = balances[i - 1][j];
double interest = . . .;
balances[i][j] = oldBalance + interest;
}
}

注释: “for each”循环语句不会自动循环处理二维数组的所有元素。它会循环处理行,而这些行本身就是一维数组。要想访问二维数组 a 的所有元素,需要使用两个嵌套的循环,如下所示:

1
2
3
for (double[] row : a)
for (double value : row)
do something with value

提示: 要想快速地打印一个二维数组的元素列表,可以调用:

1
System.out.println(Arrays.deepToString(a));

输出格式为:

1
[[16, 3, 2, 13], [5, 10, 11, 8], [9, 6, 7, 12], [4, 15, 14, 1]]

3.10.8 不规则数组

到目前为止,我们看到的数组与其他程序设计语言中的数组没有多大区别。但在底层实际存在着一些细微的差异,有时你可以充分利用这一点:Java 实际上没有多维数组,只有一维数组。多维数组被解释为”数组的数组”。

例如,在前面的示例中,balances 数组实际上是一个包含 10 个元素的数组,而每个元素又是一个由 6 个浮点数组成的数组(请参见图 3-15)。

IMG20251030102018

图 3-15 一个二维数组

表达式 balances[i] 指示第 i 个子数组,也就是表格的第 i 行。它本身也是一个数组,balances[i][j] 指示这个数组的第 j 个元素。

由于可以单独地访问数组的某一行,所以可以让两行交换。

1
2
3
double[] temp = balances[i];
balances[i] = balances[i + 1];
balances[i + 1] = temp;

还可以很容易地构造一个”不规则”数组,即数组的每一行有不同的长度。下面是一个标准的示例。我们将创建一个数组,第 i 行第 j 列的元素将存放”从 i 个数中抽取 j 个数”可能的结果数。

1
2
3
4
5
6
7
1
1 1
1 2 1
1 3 3 1
1 4 6 4 1
1 5 10 10 5 1
1 6 15 20 15 6 1

由于 j 不可能大于 i,所以矩阵是三角形的。第 i 行有 i + 1 个元素(允许抽取 0 个元素,这种选择只有一种可能)。要想创建这样一个不规则的数组,首先需要分配一个数组包含这些行:

1
2
final int NMAX = 10;
int[][] odds = new int[NMAX + 1][];

接下来,分配这些行:

1
2
for (int n = 0; n <= NMAX; n++)
odds[n] = new int[n + 1];

分配了数组之后,可以采用通常的方式访问其中的元素(前提是没有超出边界):

1
2
3
4
5
6
7
for (int n = 0; n < odds.length; n++)
for (int k = 0; k < odds[n].length; k++)
{
// compute lotteryOdds
...
odds[n][k] = lotteryOdds;
}

程序清单 3-9 给出了完整的程序。

C++ 注释: 在 C++ 中(Java 声明)

1
double[][] balances = new double[10][6]; // Java

不同于

1
double balances[10][6]; // C++

也不同于

1
double (*balances)[6] = new double[10][6]; // C++

而是分配了一个包含 10 个指针的数组:

1
double** balances = new double*[10]; // C++

然后,这个指针数组的每一个元素会填充一个包含 6 个数字的数组:

1
2
for (i = 0; i < 10; i++)
balances[i] = new double[6];

庆幸的是,当调用 new double[10][6] 时,这个循环是自动的。需要不规则的数组时,只能单独地分配行数组。