public cl ass Constants〗
public static final double CM_PER_INCH = 2.54;
public static void main(Stringn args)
double paperWidth = 8.5;
double paperHeight = 11;
System.out.println("Paper size in centimeters: "
+ paperWidth * CMJERJNCH + by " + paperHeight * CM_PER_INCH) ;
需要注意, 类常量的定义位于 main方法的外部。因此,在同一个类的其他方法中也可以使用这个常量。而且,如果一个常量被声明为 public,那么其他类的方法也可以使用这个常量。 在这个示例中,Constants2.CM_PER-INCH 就是这样一个常量。
C++ 注释:const 是 Java 保留的关键字,但目前并没有使用。在 Java 中, 必须使用 final 定义常量。
3.5 运算符
在 Java 中,使用算术运算符 + 、-、 * 、/ 表示加、减、 乘、除运算。 当参与 / 运算的两个 操作数都是整数时, 表示整数除法;否则, 表示浮点除法。 整数的求余操作(有时称为取模) 用 % 表示。例如,15/2 等于7 ,15%2 等于 1 , 15.0/2 等于 7.5。
需要注意, 整数被 0 除将会产生一个异常, 而浮点数被 0 除将会得到无穷大或 NaN 结果。
注释: 可移植性是 Java 语言的设计目标之一 , 无论在哪个虚拟机上运行, 同一运算应该 得到同样的结果3 对于浮点数的算术运算, 实现这样的可移植性是相当困难的。double 类型使用 64 位存储一个数值, 而有些处理器使用 80 位浮点寄存器这些寄存器增加了中间过程的计算精度. 例如, 以下运算:
double w = x * y / z;
很多 Intel 处理器计算 x * y,并且将结果存储在 80 位的寄存器中, 再除以 z 并将结果截断为 64 位„ 这样可以得到一个更加精确的计算结果,并且还能够避免产生指数溢出。但是, 这个结果可能与始终在 64 位机器上计算的结果不一样。 因此,Java 虚拟机的最初规范规定所有的中间计算都必须进行截断。这种行为遭到了数值计算团体的反对。 截断计算不仅可能导致溢出, 而且由于截断操作需要消耗时间, 所以在计算速度上实际上要比精确计算慢。 为此,Java 程序设计语言承认了最优性能与理想结果之间存在的冲突,并给予了改进。在默认情况下, 虚拟机设计者允许对中间计算结果采用扩展的精度。 但是, 对于使用 strictfp 关键字标记的方法必须使用严格的浮点计算来生成可再生的结果。例如,可以把 main 方法标记为
public static strictfp void main(String[] args)
于是,在 main 方法中的所有指令都将使用严格的浮点计算。如果将一个类标记为 strictfp, 这个类中的所有方法都要使用严格的浮点计算。
实际的计算方式将取决于 Intel 处理器的行为。在默认情况下,中间结果允许使用扩展的指数, 但不允许使用扩展的尾数(Intel 芯片在截断尾数时并不损失性能)。因此,这两种方式的区别仅仅在于采用默认的方式不会产生溢出, 而采用严格的计算有可能产生溢出。
如果没有仔细阅读这个注释, 也没有什么关系。 对大多数程序来说, 浮点溢出不属于大问题。在本书中, 将不使用 strictfp 关键字。
3.5.1 数学函数与常量
在 Math类中,包含了各种各样的数学函数。在编写不同类别的程序时,可能需要的函 数也不同。
要想计算一个数值的平方根, 可以使用 sqrt 方法:
double x = 4;
double y = Math.sqrt(x);
System.out.println(y); // prints 2.0
注释: println 方法和 sqrt 方法存在微小的差异。println 方法处理 System.out 对象。但是, Math 类中的 sqrt 方法处理的不是对象,这样的方法被称为静态方法。有关静态方法的详细内容请参看第 4 章。
在 Java中,没有幂运算, 因此需要借助于 Math 类的 pow 方法。语句:
double y = Math.pow(x, a);
将 y 的值设置为 x 的 a 次幂( xa)。pow 方法有两个 double 类型的参数, 其返回结果也为 double 类型。
floorMod 方法的目的是解决一个长期存在的有关整数余数的问题。考虑表达式 n % 2。 所有人都知道, 如果 n 是偶数, 这个表达式为 0 ; 如果 n 是奇数, 表达式则为 1。当然, 除 非 n 是负数 如果 n 为负,这个表达式则为 -1。为什么呢? 设计最早的计算机时,必须有人制定规则,明确整数除法和求余对负数操作数该如何处理。数学家们几百年来都知道这样一 个最优(或“ 欧几里德”)规则:余数总是要>=0。不过, 最早制定规则的人并没有翻开数学书好好研究,而是提出了一些看似合理但实际上很不方便的规则。
下面考虑这样一个问题: 计算一个时钟时针的位置。这里要做一个时间调整, 而且要归 一化为一个 0 ~ 11 之间的数。 这很简单: position + adjustment) % 12。不过, 如果这个调整为负会怎么样呢? 你可能会得到一个负数。所以要引入一个分支, 或者使用 (position + adjustment) % 12 + 12) % 12。不管怎样, 总之都很麻烦。
floorMod 方法就让这个问题变得容易了:floorMod(position + adjustment, 12) 总会得到一个 0 ~ 11 之间的数。(遗憾的是,对于负除数,floorMod 会得到负数结果,不过这种情况在实际中很少出现。)
Math 类提供了一些常用的三角函数:
Math,sin
Math.cos
Math.tan
Math.atan
Math.atan2
还有指数函数以及它的反函数--自然对数以及以 10 为底的对数:
Math.exp
Math.log
Math.logl0
最后,Java 还提供了两个用于表示 π 和 e 常量的近似值:
Math.PI
Math.E
提示:不必在数学方法名和常量名前添加前缀“ Math”, 只要在源文件的顶部加上下面 这行代码就可以了。
import static java.1ang.Math.*;
System.out.println("The square root of \u03C0 is " + sqrt(PI));
在第 4 章中将讨论静态导入。
注释: 在 Math 类中, 为了达到最快的性能, 所有的方法都使用计算机浮点单元中的例程.. 如果得到一个完全可预测的结果比运行速度更重要的话, 那么就应该使用 StrictMath 类,, 它使用“ 自由发布的 Math 库”(fdlibm) 实现算法, 以确保在所有平台上得到相同的结果。 有关这些算法的源代码请参看 www.netlib.org/fdlibm ( 当 fdlibm 为一个函数提供了 多个定义时, StrictMath 类就会遵循 IEEE 754 版本,它的名字将以“ e” 开头)
3.5.2 数值类型之间的转换
经常需要将一种数值类型转换为另一种数值类型。图 3-1 给出了数值类型之间的合法 转换。
在图 3-1 中有 6 个实心箭头,表示无信息丢失的转换;有 3 个虚箭头, 表示可能有精度 损失的转换。 例如,123 456 789 是一个大整数, 它所包含的位数比 float 类型所能够表达的位数多。 当将这个整型数值转换为 float 类型时, 将会得到同样大小的结果,但却失去了一定 的精度。
int n = 123456789;
float f = n; // f is 1.23456792E8
当使用上面两个数值进行二元操作时(例如 n + f,n 是整数, f 是浮点数,) 先要将两个操作数转换为同一种类型,然后再进行计算。
•如果两个操作数中有一个是 double 类型, 另一个操作数就会转换为 double 类型。
•否则,如果其中一个操作数是 float 类型,另一个操作数将会转换为 float 类型。
•否则, 如果其中一个操作数是 long 类型, 另一个操作数将会转换为 long 类型。
•否则, 两个操作数都将被转换为 int 类型。
3.5.3 强制类型转换
在上一小节中看到, 在必要的时候, int 类型的值将会自动地转换为 double 类型。但另 一方面,有时也需要将 double 转换成 int。 在 Java 中, 允许进行这种数值之间的类型转换。 当然, 有可能会丢失一些信息。在这种情况下,需要通过强制类型转换( cast) 实现这个操 作。强制类型转换的语法格式是在圆括号中给出想要转换的目标类型,后面紧跟待转换的变量名。例如:
double x * 9.997;
int nx = (int) x;
这样, 变量 nx 的值为 9。强制类型转换通过截断小数部分将浮点值转换为整型。
如果想对浮点数进行舍入运算, 以便得到最接近的整数(在很多情况下, 这种操作更有用,) 那就需要使用 Math_ round 方法:
double x z 9.997;
int nx = (int) Math.round(x);
现在, 变量 nx 的值为 10。 当调用 round 的时候, 仍然需要使用强制类型转换( int。) 其原因 是 round 方法返回的结果为 long 类型,由于存在信息丢失的可能性,所以只有使用显式的强 制类型转换才能够将 long 类型转换成 int 类型。
警告: 如果试图将一个数值从一种类型强制转换为另一种类型, 而又超出了目标类型的 表示范围,结果就会截断成一个完全不同的值。例如,(byte ) 300 的实际值为 44。
C++ 注释:不要在 boolean 类型与任何数值类型之间进行强制类型转换, 这样可以防止发生错误。只有极少数的情况才需要将布尔类型转换为数值类型,这时可以使用条件表 达式 b ? 1:0。
3.5.4 结合赋值和运算符
可以在赋值中使用二元运算符,这是一种很方便的简写形式。例如,x+=4等价于 x=x+4;(一般地, 要把运算符放在 = 号左边,如 *= 或 %=)。
注释: 如果运算符得到一个值, 其类型与左侧操作数的类型不同, 就会发生强制类型转换。 例如,如果 X 是一个 int, 则以下语句 x += 3.5; 是合法的, 将把 X 设置为(int)(x + 3.5)。
3.5.5 自增与自减运算符
当然, 程序员都知道加 1、 减 1 是数值变量最常见的操作。在 Java 中, 借鉴了 C 和 C++ 的做法,也提供了自增、 自减运算符: n++ 将变量 n 的当前值加 1, n-- 则将 n 的值减 1。例 如, 以下代码:
int n = 12;
n++;
将 n 的值改为 13。由于这些运算符会改变变量的值,所以它们的操作数不能是数值。例如, 4++ 就不是一个合法的语句。
实际上, 这些运算符有两种形式;上面介绍的是运算符放在操作数后面的“ 后缀” 形式。 还有一种“ 前缀” 形式:++n。后缀和前缀形式都会使变量值加 1 或减 1。但用在表达式中时, 二者就有区别了。前缀形式会先完成加 1; 而后缀形式会使用变量原来的值。
int m = 7;
int n = 7;
int a = 2 * ++m; // now a is 16, m is 8
int b = 2 * n++; // now b is 14, n is 8
建议不要在表达式中使用 ++, 因为这样的代码很容易让人闲惑,而且会带来烦人的 bug。
3.5.6 关系和boolean运算符
Java 包含丰富的关系运算符:要检测相等性,可以使用两个等号 = 。例如,3==7的值为false。另外可以使用!= 检测不相等。例如,3!=7的值为true。
最后, 还有经常使用的 < (小于、) > (大于) 、<=(小于等于)和 >= (大于等于)运算符。
Java 沿用了 C++ 的做法,使用 && 表示逻辑“ 与” 运算符,使用丨| 表示逻辑“ 或” 运算符。从 != 运算符可以想到,感叹号!就是逻辑非运算符。&& 和丨| 运算符是按照“ 短路” 方 式来求值的: 如果第一个操作数已经能够确定表达式的值,第二个操作数就不必计算了。如 果用 && 运算符合并两个表达式,
expression1 && expression2
而且已经计算得到第一个表达式的真值为 false, 那么结果就不可能为 true。因此, 第二个表达式就不必计算了。可以利用这一点来避免错误。例如, 在下面的表达式中:
x != 0&&1 / x > x + y // no division by 0
如果 x 等于 0, 那么第二部分就不会计算。因此,如果 x 为 0, 也就不会计算 1 / x , 除 以 0 的错误就不会出现。
类似地, 如果第一个表达式为 true, expression1 || expression2的值就自动为 true, 而无需 计算第二个表达式。
最后一点,Java 支持三元操作符?:,这个操作符有时很有用。如果条件为 true, 下面的 表达式
condition ? expression1: expression2
就为第一个表达式的值,否则计算为第二个表达式的值。例如,x < y ? x : y会返回 x 和 y 中较小的一个。
3.5.7 位运算符
处理整型类型时,可以直接对组成整型数值的各个位完成操作。这意味着可以使用掩码技术得到整数中的各个位。位运算符包括:
& ("and") | ("or") A ("XOr") ~ ("not")
这些运算符按位模式处理。例如, 如果 n 是一个整数变量,而且用二进制表示的 n 从右边数第 4 位为 1,则
int fourthBitFromRight = (n & OblOOO) / OblOOO;
会返回 1,否则返回 0。利用 & 并结合使用适当的 2 的幂, 可以把其他位掩掉, 而只保留其 中的某一位。
注释:应用在布尔值上时, & 和丨运算符也会得到一个布尔值。这些运算符与 && 和 ||运 算符很类似,不过 & 和丨运算符不采用“ 短路” 方式来求值, 也就是说,得到计算结果之前两个操作数都需要计算。
另外,还有>>和 <<运算符将位模式左移或右移。需要建立位模式来完成位掩码时, 这 两个运算符会很方便:
int fourthBitFromRight = (n & (1« 3)) » 3;
最后,>>> 运算符会用 0 填充高位,这与>>不同,它会用符号位填充高位。不存在<<<运算符。
警告: 移位运算符的右操作数要完成模 32 的运算(除非左操作数是 long 类型, 在这种情 况下需要对右操作數模 64 )。 例如, 1<<35 的值等同于 1 <<3 或 8。
C++ 注释: 在 C/C++ 中,不能保证>> 是完成算术移位(扩展符号位)还是逻辑移位(填 充 0。) 实现者可以选择其中更高效的任何一种做法。 这意味着 C/C++ >>运算符对于负数生成的结果可能会依赖于具体的实现。Java 则消除了这种不确定性。
3.5.8 括号与运算符级别
表 3-4 给出了运算符的优先级。 如果不使用圆括号, 就按照给出的运算符优先级次序进行计算。同一个级别的运算符按照从左到右的次序进行计算(除了表中给出的右结合运算符外。)例如,由于 && 的优先级比 || 的优先级高, 所以表达式
a && b | c
等价于
(a M b) 11 c
有因为+=是右结合运算符,所以表达式
a += b += c
等价于
a += (b += c)
也就是将 b += c 的结果(加上 c 之后的 b) 加到 a 上。
C++ 注释:与 C 或 C++ 不同,Java 不使用逗号运算符。不过, 可以在 for语 句 的 第 1 和 第 3 部分中使用逗号分隔表达式列表。
3.5.9 枚举类型
有时候,变量的取值只在一个有限的集合内。例如: 销售的服装或比萨饼只有小、中、 大和超大这四种尺寸。当然, 可以将这些尺寸分别编码为 1、2、3、4 或 S、 M、 L、X。但 这样存在着一定的隐患。在变量中很可能保存的是一个错误的值(如 0 或 m)。
针对这种情况, 可以自定义枚举类型。枚举类型包括有限个命名的值。 例如,
enum Size { SMALL, MEDIUM, LARGE, EXTRA.LARCE };
现在,可以声明这种类型的变量:
Size s = Size.MEDIUM;
Size 类型的变量只能存储这个类型声明中给定的某个枚举值,或者 null 值,null 表示这 个变量没有设置任何值。
有关枚举类型的详细内容将在第 5 章介绍。
3.6 字符串
从概念上讲, Java 字符串就是 Unicode 字符序列。 例如, 串“ Java\u2122” 由 5 个 Unicode 字符 J、a、 v、a 和™。Java 没有内置的字符串类型, 而是在标准 Java 类库中提供了 一个预定义类,很自然地叫做 String。每个用双引号括起来的字符串都是 String类的一个实例:
String e = ""; // an empty string
String greeting = "Hello";
3.6.1 子串
String 类的 substring 方法可以从一个较大的字符串提取出一个子串。例如:
String greeting = "Hello";
String s = greeting.substring(0, 3);
创建了一个由字符“ Hel” 组成的字符串。
substring 方法的第二个参数是不想复制的第一个位置。这里要复制位置为 0、 1 和 2 (从 0 到 2, 包括 0 和 2 ) 的字符。在 substring 中从 0 开始计数,直到 3 为止, 但不包含 3。 substring 的工作方式有一个优点:容易计算子串的长度。字符串 s.substring(a, b) 的长度 为 b-a。例如, 子串“ Hel ” 的长度为 3-0=3。
3.6.2 拼接
与绝大多数的程序设计语言一样,Java语言允许使用 + 号连接(拼接)两个字符串。
String expletive = "Expletive";
String PC13 = "deleted";
String message = expletive + PC13;
上述代码将“ Expletivedeleted” 赋给变量 message (注意, 单词之间没有空格, + 号按照 给定的次序将两个字符串拼接起来)。
当将一个字符串与一个非字符串的值进行拼接时,后者被转换成字符串(在第 5 章中可以看到,任何一个 Java 对象都可以转换成字符串)。例如:
int age = 13;
String rating = "PC" + age;
rating 设置为“ PG13”。 这种特性通常用在输出语句中。例如:
System.out.println("The answer is " + answer);
这是一条合法的语句, 并且将会打印出所希望的结果(因为单词 is 后面加了一个空格, 输出 时也会加上这个空格)。
如果需要把多个字符串放在一起, 用一个定界符分隔,可以使用静态 join 方法:
String all = String.join(" / ", "S", "M","L", "XL");
// all is the string "S / H / L / XL"
3.6.3 不可变字符串
String 类没有提供用于修改字符串的方法。如果希望将 greeting 的内容修改为“ Help!”, 不能直接地将 greeting 的最后两个位置的字符修改为‘ p ’ 和‘ ‘!’。 这对于 C 程序员来说, 将会感到无从下手。如何修改这个字符串呢? 在 Java中实现这项操作非常容易。首先提取需要的字符, 然后再拼接上替换的字符串:
greeting = greeting.substring(0, 3) + "p!";
上面这条语句将 greeting 当前值修改为“ Help ! ”。
由于不能修改 Java 字符串中的字符, 所以在 Java 文档中将 String 类对象称为不可变字符串, 如同数字 3 永远是数字 3 —样,字符串“ Hello” 永远包含字符 H、 e、1、 1 和 o 的代 码单元序列, 而不能修改其中的任何一个字符。当然, 可以修改字符串变量 greeting, 让它 引用另外一个字符串, 这就如同可以将存放 3 的数值变量改成存放 4 一样。
这样做是否会降低运行效率呢? 看起来好像修改一个代码单元要比创建一个新字符串更 加简洁。答案是:也对,也不对。的确, 通过拼接“ Hel ” 和“ p! ” 来创建一个新字符串的 效率确实不高。但是,不可变字符串却有一个优点:编译器可以让字符串共享。
为了弄清具体的工作方式,可以想象将各种字符串存放在公共的存储池中。字符串变量 指向存储池中相应的位置。如果复制一个字符串变量, 原始字符串与复制的字符串共享相同的字符。
总而言之,Java 的设计者认为共享带来的高效率远远胜过于提取、 拼接字符串所带来的低效率。查看一下程序会发现:很少需要修改字符串, 而是往往需要对字符串进行比较(有 一种例外情况,将来自于文件或键盘的单个字符或较短的字符串汇集成字符串。为此, Java 提供了一个独立的类,在 3.6.9 节中将详细介绍)。
C++ 注释: 在 C 程序员第一次接触 Java 字符串的时候, 常常会感到迷惑, 因为他们总将 字符串认为是字符型数组:
char greeting[] = "Hello";
这种认识是错误的, Java 字符串大致类似于 char* 指针,
char* greeting = "Hello";
当采用另一个字符串替换 greeting 的时候, Java 代码大致进行下列操作:
char* temp = malloc(6);
stmcpy(temp, greeting, 3);
strncpy(temp + 3, "p! " , 3);
greeting = temp;
的确, 现在 greeting 指向字符串“ Help!”。 即使一名最顽固的 C 程序员也得承认 Java 语法要比一连串的 stmcpy 调用舒适得多。然而,如果将 greeting 斌予另一个值又会怎样呢?
greeting = "Howdy";
这样做会不会产生内存遗漏呢? 毕竞, 原始字符串放置在堆中。十分幸运,Java 将 自动地进行垃圾回收。 如果一块内存不再使用了, 系统最终会将其回收。
对于一名使用 ANSI C++ 定义的 string 类的 C++ 程序员, 会感觉使用 Java 的 String 类型更为舒适。C++ string 对象也自动地进行内存的分配与回收。内存管理是通过构造器、 赋值操作和析构器显式执行的。然而,C++ 字符串是可修改的, 也就是说,可以修改字符串中的单个字符。
3.6.4 检测字符串是否相等
可以使用 equals 方法检测两个字符串是否相等。对于表达式:
s.equals(t)
如果字符串 s 与字符串 t 相等, 则返回 true ; 否则, 返回 false。需要注意,s与 t 可以是字符串变量, 也可以是字符串字面量。 例如, 下列表达式是合法的:
"Hello".equals(greeting)
要想检测两个字符串是否相等,而不区分大小写, 可以使用 equalsIgnoreCase 方法。
"Hello".equalsIgnoreCase("hel1o")
一定不要使用 = 运算符检测两个字符串是否相等! 这个运算符只能够确定两个字串是否放置在同一个位置上。当然, 如果字符串放置在同一个位置上, 它们必然相等。但是, 完全有可能将内容相同的多个字符串的拷贝放置在不同的位置上。
String greeting = "Hello"; //initialize greeting to a string
if (greeting == "Hello") .
// probably true
if (greeting.substring(0, 3) == "Hel") . . .
// probably false
如果虚拟机始终将相同的字符串共享, 就可以使用=运算符检测是否相等。但实际上 只有字符串常量是共享的,而 + 或 substring 等操作产生的结果并不是共享的。因此,千万不 要使甩== 运算符测试字符串的相等性, 以免在程序中出现糟糕的 bug。从表面上看, 这种 bug 很像随机产生的间歇性错误。
C++ 注释: 对于习惯使用 C++ 的 string 类的人来说, 在进行相等性检测的时候一定要特别小心。C++ 的 string 类重载了 == 运算符以便检测字符串内容的相等性。 可惜 Java 没 有采用这种方式, 它的字符串“ 看起来、 感觉起来” 与数值一样, 但进行相等性测试时, 其操作方式又类似于指针。语言的设计者本应该像对 + 那样也进行特殊处理, 即重定义 =运算符。 当然,每一种语言都会存在一些不太一致的地方。
C 程序员从不使用 =对字符串进行比较, 而使用 strcmp 函数。Java 的 compareTo 方法与 strcmp 完全类似, 因此,可以这样使用:
if (greeting.compareTo("Hel1oH) == 0} . . .
不过, 使用 equals 看起来更为清晰。
3.6.5 空串与Null串
空串 "" 是长度为 0 的字符串。可以调用以下代码检查一个字符串是否为空:
if (str.length() = 0)
if (str.equals(""))
空串是一个 Java 对象, 有自己的串长度( 0 ) 和内容(空)。不过, String 变量还可以存放一个特殊的值, 名为 null, 这表示目前没有任何对象与该变量关联(关于 null 的更多信息 请参见第 4 章)。要检查一个字符串是否为 null, 要使用以下条件:
if (str == null)
有时要检查一个字符串既不是 null 也不为空串,这种情况下就需要使用以下条件:
if (str != null && str.lengthO != 0)
首先要检查 str 不为 null。在第 4 章会看到, 如果在一个 mill 值上调用方法, 会出现错误。
3.6.6 码点与代码单元
Java 字符串由 char 值序列组成。从 3.3.3 节“ char 类型” 已经看到, char 数据类型是一个采用 UTF-16 编码表示 Unicode 码点的代码单元。大多数的常用 Unicode 字符使用一个代码单元就可以表示,而辅助字符需要一对代码单元表示。
length 方法将返回采用 UTF-16 编码表示的给定字符串所需要的代码单元数量。例如:
String greeting = "Hello";
int n = greeting.length。; // is 5 .
要想得到实际的长度,即码点数量,可以调用:
int cpCount = greeting.codePointCount(0, greeting.lengthQ);
调用 s.charAt(n) 将返回位置 n 的代码单元,n 介于 0 ~ s.length()-l 之间。例如:
char first = greeting.charAt(0); // first is 'H'
char last = greeting.charAt(4); // last is ’o’
要想得到第 i 个码点,应该使用下列语句
int index = greeting.offsetByCodePoints(0, i);
int cp = greeting.codePointAt(index);
注释: 类似于 C 和 C++, Java 对字符串中的代码单元和码点从 0 开始计数。 为什么会对代码单元如此大惊小怪? 请考虑下列语句:
is the set of octonions 使用 UTF-16 编码表示字符
(U+1D546) 需要两个代码单元。调用 char ch = sentence.charAt(1) 返回的不是一个空格,而是
的第二个代码单元。为了避免这个问题, 不要使用 char 类型。 这太底层了。
如果想要遍历一个字符串,并且依次査看每一个码点, 可以使用下列语句:
int cp = sentence.codePointAt(i);
if (Character.isSupplementaryCodePoint(cp)) i+= 2;
else i++;
可以使用下列语句实现回退操作:
if (CharacterssSurrogate(sentence.charAt(i))) i ;
int cp = sentence.codePointAt(i);
显然, 这很麻烦。更容易的办法是使用 codePoints 方法, 它会生成一个 int 值的“ 流”, 每个 int 值对应一个码点。(流将在卷 II 的第 2 章中讨论〉 可以将它转换为一个数组(见 3.10 节,) 再完成遍历。
int[] codePoints = str.codePointsO.toArrayO;
反之,要把一个码点数组转换为一个字符串, 可以使用构造函数(我们将在第 4 章详细 讨论构造函数和 new 操作符 )。
String str = new String(codePoints, 0, codePoints.length);
3.6.7 String API
Java 中的 String类包含了 50 多个方法。令人惊讶的是绝大多数都很有用, 可以设想使用的频繁非常高。下面的 API 注释汇总了一部分最常用的方法。
注释: 可以发现,本书中给出的 API 注释会有助于理解 Java 应用程序编程接口( API )。 每一个 API 的注释都以形如 java.lang.String 的类名开始。(java.lang 包的重要性将在第 4 章给出解释。) 类名之后是一个或多个方法的名字、 解释和参数描述。
在这里, 一般不列出某个类的所有方法, 而是选择一些最常用的方法, 并以简洁的方式给予描述。 完整的方法列表请参看联机文档(请参看 3.6.8 节)。
这里还列出了所给类的版本号。如果某个方法是在这个版本之后添加的, 就会给出 一个单独的版本号。
API java.lang.string 1.0
• char charAt (int index) 返回给定位置的代码单元。除非对底层的代码单元感兴趣, 否则不需要调用这个方法。
• int codePointAt(int Index) 5.0 返回从给定位置开始的码点。
• int offsetByCodePoints(int startlndex, int cpCount) 5.0 返回从 startlndex 代码点开始,位移 cpCount 后的码点索引。
• int compareTo(String other) 按照字典顺序,如果字符串位于 other 之前, 返回一个负数;如果字符串位于 other 之 后,返回一个正数;如果两个字符串相等,返回 0。
• IntStream codePoints() 8 将这个字符串的码点作为一个流返回。调用 toArray 将它们放在一个数组中。
• new String(int[] codePoints, int offset, int count) 5.0 用数组中从 offset 开始的 count 个码点构造一个字符串。
• boolean equals(0bject other) 如果字符串与 other 相等, 返回 true。
•boolean equalsIgnoreCase(String other ) 如果字符串与 other 相等 ( 忽略大小写,) 返回 tme。
•boolean startsWith(String prefix )
•boolean endsWith(String suffix )
如果字符串以 suffix 开头或结尾, 则返回 true。
•int indexOf(String str)
•int indexOf(String str, int fromlndex )
•int indexOf(int cp) •int indexOf(int cp, int fromlndex )
返回与字符串 str 或代码点 cp 匹配的第一个子串的开始位置。这个位置从索引 0 或 fromlndex 开始计算。 如果在原始串中不存在 str, 返回 -1。 •int lastIndexOf(String str)
•Int lastIndexOf(String str, int fromlndex )
•int lastindexOf(int cp)
•int lastindexOf(int cp, int fromlndex )
返回与字符串 str 或代码点 cp 匹配的最后一个子串的开始位置。 这个位置从原始串尾端或 fromlndex 开始计算。
•int length( ) 返回字符串的长度。
•int codePointCount(int startlndex , int endlndex ) 5.0 返回 startlndex 和 endludex-1之间的代码点数量。没有配成对的代用字符将计入代码点。 參
•String replace( CharSequence oldString,CharSequence newString) 返回一个新字符串。这个字符串用 newString 代替原始字符串中所有的 oldString。可 以用 String 或 StringBuilder 对象作为 CharSequence 参数。
• String substring(int beginlndex )
• String substring(int beginlndex, int endlndex )
返回一个新字符串。这个字符串包含原始字符串中从 beginlndex 到串尾或 endlndex-1的所有代码单元。
• String toLowerCase( )
• String toUpperCase( )
返回一个新字符串。 这个字符串将原始字符串中的大写字母改为小写,或者将原始字 符串中的所有小写字母改成了大写字母。
• String trim( ) 返回一个新字符串。这个字符串将删除了原始字符串头部和尾部的空格。
• String join(CharSequence delimiter, CharSequence ... elements ) 8 返回一个新字符串, 用给定的定界符连接所有元素。
注释:在 API 注释中, 有一些 CharSequence 类型的参数这是一种接口类型, 所有字符串都属于这个接口。第 6 章将介绍更多有关接口类型的内容。现在只需要知道只要看到 一个 CharSequence 形参, 完全可以传入 String 类型的实参。
3.6.8 阅读联机 API文档
正如前面所看到的, String 类包含许多方法。 而且, 在标准库中有几苄个类, 方法数tt 更加惊人。要想记住所有的类和方法是一件不太不可能的事情。 因此,学会使用在线 API 文档十分重要,从中可以查阅到标准类库中的所有类和方法。API文档是 JDK 的一部分, 它是 HTML 格式的。 让浏览器指向安装 roK 的 docs/api/index.html 子目录, 就可以看到所示的屏幕。(图略)
3.6.9 构建字符串
有些时候, 需要由较短的字符串构建字符串, 例如, 按键或来自文件中的单词。采用字符串连接的方式达到此目的效率比较低。每次连接字符串, 都会构建一个新的 String 对象, 既耗时, 又浪费空间。使用 StringBuilder类就可以避免这个问题的发生。
如果需要用许多小段的字符串构建一个字符串, 那么应该按照下列步骤进行。 首先, 构 建一个空的字符串构建器:
StringBuilder builder = new StringBuilderO;
当每次需要添加一部分内容时, 就调用 append 方法。
builder.append(ch); // appends a single character
bui1der.append(str); // appends a string
在需要构建字符串时就凋用 toString 方法, 将可以得到一个 String 对象, 其中包含了构建器 中的字符序列。
String completedString = builder.toStringO;
注释: 在 JDK5.0 中引入 StringBuilder 类。 这个类的前身是 StringBuffer, 其效率稍有些低, 但允许采用多线程的方式执行添加或删除字符的操作。如果所有字符串在一个单线 程中编辑 (通常都是这样) , 则应该用 StringBuilder 替代它。 这两个类的 API是相同的。
下面的 API 注释包含了 StringBuilder 类中的重要方法。
API java.lang.StringBuilder 5.0
• StringBuilder() 构造一个空的字符串构建器。
• int length() 返回构建器或缓冲器中的代码单元数量。
• StringBuilder appencl(String str) 追加一个字符串并返回 this。
• StringBuilder append(char c) 追加一个代码单元并返回 this。
• StringBuilder appendCodePoint(int cp) 追加一个代码点,并将其转换为一个或两个代码单元并返回 this。
• void setCharAt(int i ,char c) 将第 i 个代码单元设置为 c。
• StringBuilder insert(int offset,String str) 在 offset 位置插入一个字符串并返回 this。
•StringBuilder insert(int offset,Char c) 在 offset 位置插入一个代码单元并返回 this。
• StringBuilder delete(1 nt startindex,int endlndex) 删除偏移量从 startindex 到 -endlndex-1 的代码单元并返回 this
• String toString() 返回一个与构建器或缓冲器内容相同的字符串.。
3.7 输入与输出
为了增加后面示例程序的趣味性,需要程序能够接收输入,并以适当的格式输出。当 然, 现代的程序都使用 GUI 收集用户的输人, 然而,编写这种界面的程序需要使用较多的工具与技术,目前还不具备这些条件。主要原因是需要熟悉 Java 程序设计语言,因此只要有简单的用于输入输出的控制台就可以了。第 10 章 ~ 第 12 章将详细地介绍 GUI 程序设计。
3.7.1 读取输入
前面已经看到,打印输出到“ 标准输出流”(即控制台窗口)是一件非常容易的事情,只要 调用 System.out.println 即可。然而,读取“ 标准输入流” System.in 就没有那么简单了。要想通过控制台进行输入,首先需要构造一个 Scanner 对象,并与“ 标准输入流” System.in 关联。
Scanner in = new Scanner(System.in);
现在,就可以使用 Scanner 类的各种方法实现输入操作了。例如, nextLine 方法将输入 一行。
System.out.print("How old are you? ");
int age = in.nextInt();
与此类似,要想读取下一个浮点数, 就调用 nextDouble 方法。
在程序清单 3-2 的程序中,询问用户姓名和年龄, 然后打印一条如下格式的消息:
Hello, Cay. Next year, you'll be 57
最后,在程序的最开始添加上一行:
import java.util.*;
Scanner 类定义在java.util 包中。 当使用的类不是定义在基本java.lang 包中时,一定要使用 import 指示字将相应的包加载进来。有关包与 import 指示字的详细描述请参看第 4 章。
import java.util.*;
public class InputTest
public static void main(String口 args)
Scanner in = new Scanner(System.in);
// get first input
System,out.print("What is your name? ");
String name = in.nextLine();
// get second input
System,out.print("How old are you? ");
int age = in.nextlntO;
//display output on console
System.out.println("Hello, " + name + Next year, you'll be " + (age + 1));
注释: 因为输入是可见的, 所以 Scanner 类不适用于从控制台读取密码。Java SE 6 特别 引入了 Console 类实现这个目的。要想读取一个密码, 可以采用下列代码:
Console cons = System.console();
String username = cons.readLine("User name: ");
char [] passwd = cons.readPassword("Password:");
为了安全起见, 返回的密码存放在一维字符数组中, 而不是字符串中。在对密码进行处理之后,应该马上用一个填充值覆盖数组元素(数组处理将在 3.10 节介绍)。
采用 Console 对象处理输入不如采用 Scanner 方便。每次只能读取一行输入, 而没有能够读取一个单词或一个数值的方法。
API java.util.Scanner 5.0
• Scanner (InputStream in) 用给定的输入流创建一个 Scanner 对象。
• String nextLine( ) 读取输入的下一行内容。
• String next( ) 读取输入的下一个单词(以空格作为分隔符。)
• int nextlnt( )
• double nextDouble( )
读取并转换下一个表示整数或浮点数的字符序列。
• boolean hasNext( ) 检测输入中是否还有其他单词。
• boolean hasNextInt( )
• boolean hasNextDouble( )
检测是否还有表示整数或浮点数的下一个字符序列。
API java.Iang.System 1.0
• static Console console( ) 6
如果有可能进行交互操作, 就通过控制台窗口为交互的用户返回一个 Console 对象, 否则返回 null。对于任何一个通过控制台窗口启动的程序, 都可使用 Console 对象。 否则, 其可用性将与所使用的系统有关。
API java.io.Console 6
• static char[] readPassword(String prompt, Object...args)
• static String readLine(String prompt, Object...args)
显示字符串 prompt 并且读取用户输入,直到输入行结束。args 参数可以用来提供输人 格式。有关这部分内容将在下一节中介绍。
3.7.2 格式化输出
可以使用 SyStem.out.print(x) 将数值 x 输出到控制台上。这条命令将以 x 对应的数据类型所允许的最大非 0 数字位数打印输出 X。 例如:
double x = 10000.0 / 3.0;
System.out.print(x);
打印 3333.333333333335
如果希望显示美元、美分等符号, 则有可能会出现问题。
在早期的 Java 版本中,格式化数值曾引起过一些争议。庆幸的是,Java SE 5.0 沿用了 C 语言库函数中的 printf方法。例如,调用
System.out.printf("%8.2f", x);
可以用 8 个字符的宽度和小数点后两个字符的精度打印 x。也就是说,打印输出一个空格和 7 个字符, 如下所示:
3333.33
在 printf中,可以使用多个参数, 例如:
System.out.printf("Hello, %s. Next year, you'll be %d", name, age);
每一个以 % 字符开始的格式说明符都用相应的参数替换。 格式说明符尾部的转换符将指示被格式化的数值类型:f 表示浮点数,s 表示字符串,d 表示十进制整数。表 3-5列出了所有转换符。
另外,还可以给出控制格式化输出的各种标志。表 3-6 列出了所有的标志。例如,逗号 标志增加了分组的分隔符。 即
Systen.out.printf("%,.2f", 10000.0 / 3.0);
可以使用多个标志,例如,“ %,( .2f” 使用分组的分隔符并将负数括在括号内。
注释:可以使用 s 转换符格式化任意的对象。 对于任意实现了 Formattable 接口的对象都将调用 formatTo 方法;否则将调用 toString 方法, 它可以将对象转换为字符串。在第 5 章中将讨论 toString 方法, 在第 6 章中将讨论接口。
可以使用静态的 String.format 方法创建一个格式化的字符串, 而不打印输出:
String message = String.format("Hello, %s. Next year, you'll be %d", name , age);
基于完整性的考虑, 下面简略地介绍 printf方法中日期与时间的格式化选项。在新代码中, 应当使用卷 II 第 6 章中介绍的 java.time 包的方法。 不过你可能会在遗留代码中看到 Date 类和相关的格式化选项。格式包括两个字母, 以 t 开始, 以表 3-7 中的任意字母结束。 例如,
System.out.printf("%tc", new Date());
这条语句将用下面的格式打印当前的日期和时间:
Mon Feb 09 18:05:19 PST 2015
从表 3-7 可以看到, 某些格式只给出了指定日期的部分信息 。例如, 只有 日期 或 月份 如果需要多次对口期操作才能实现对每一部分进行格式化的目的就太笨拙了。为此, 可以采用一 个格式化的字符串指出要被格式化的参数索引。索引必须紧跟在 % 后面, 并以 $ 终止。 例如
System.out.printf("%1$s %2$tB %2$te, %2$tY", "Due date:", new Date());
打印 Due date: February 9, 2015
还可以选择使用 < 标志。它指示前面格式说明中的参数将被再次使用。也就是说, 下列语句将产生与前面语句同样的输出结果:
System.out .printf("%s %tB %<te, %<tY", "Due date:", new DateO);
提示: 参 教 索 引 值 从 1 开 始, 而 不 是 从 0 开 始, 对 第 1个 参 数 格 式 化 这 就 避 免 了 与 0 标 志 混 淆。
现在,已经了解了 printf 方法的所有特性。图 3-6 给出了格式说明符的语法图。
注释:许多格式化规则是本地环境特有的。例如,在德国,组分隔符是句号而不是逗号, Monday 被格式化为 Montag,, 在 卷 II 第 5 章中将介绍如何控制应用的国际化行为。
3.7.3 文件输入与输出
要想对文件进行读取,就需要一个用 File 对象构造一个 Scanner 对象,如下所示:
Scanner in = new Scanner(Paths.get("niyflle.txt"), "UTF-8");
如果文件名中包含反斜杠符号,就要记住在每个反斜杠之前再加一个额外的反斜杠:“ c:\\mydirectory\\myfile.txt ” 。
注释: 在这里指定了 UTF-8 字符编码, 这对于互联网上的文件很常见(不过并不是普遍适用)。读取一个文本文件时,要知道它的字符编码---更多信息参见卷 II 第 2 章。如果省略字符编码, 则会使用运行这个 Java 程序的机器的“ 默认编码”。 这不是一个好主意, 如果在不同的机器上运行这个程序, 可能会有不同的表现。
现在,就可以利用前面介绍的任何一个 Scanner 方法对文件进行读取。
要想写入文件, 就需要构造一个 PrintWriter 对象。在构造器中,只需要提供文件名:
PrintWriter out = new PrintWriter('myfile.txt", "UTF-8");
如果文件不存在,创建该文件。 可以像输出到 System.out —样使用 print、 println 以及 printf 命令。
警告: 可以构造一个带有字符串参数的 Scanner, 但 这 个 Scanner 将字符串解释为数据, 而不是文件名。例如, 如果调用:
Scanner in = new Scanner("myfile.txt"); // ERROR?
这个 scanner 会将参数作为包含 10 个字符的数据:‘ m ’,‘ y ’,‘ f’ 等。在这个示例中所显示的并不是人们所期望的效果。
注释: 当指定一个相对文件名时, 例如,“ myfile.txt”,“ mydirectory/myfile.txt” 或“ ../myfile.txt",文件位于 Java 虚拟机启动路径的相对位置 , 如果在命令行方式下用下列命令启动程序:
java MyProg
启动路径就是命令解释器的当前路径。 然而,如果使用集成开发环境, 那么启动路径将由 IDE 控制。 可以使用下面的调用方式找到路径的位置:
String dir = System.getProperty("user.dir"):
如果觉得定位文件比较烦恼, 则可以考虑使用绝对路径, 例如:“ c:\\mydirectory\\ myfile.txt ” 或者“/home/me/mydirectory/myfile.txt” 。
正如读者所看到的,访问文件与使用 System.in 和 System.out —样容易。要记住一点:如果 用一个不存在的文件构造一个 Scanner, 或者用一个不能被创建的文件名构造一个 PrintWriter, 那么就会发生异常。Java 编译器认为这些异常比“ 被零除” 异常更严重。在第 7 章中,将会 学习各种处理异常的方式。现在,应该告知编译器: 已经知道有可能出现“ 输入 / 输出” 异 常。这需要在 main 方法中用 throws 子句标记,如下所示:
public static void main(String[] args) throws IOException
Scanner in = new Scanner(Paths.get("myfi1e.txt"), "UTF-8");
现在读者已经学习了如何读写包含文本数据的文件。对于更加高级的技术,例如,处理不同的字符编码、 处理二进制数据、 读取目录以及写压缩文件,请参看卷 II 第 2 章。
注释:当采用命令行方式启动一个程序时, 可以利用 Shell 的重定向语法将任意文件关联 到 System.in 和 System.out:
java MyProg < myfile.txt > output.txt
这样,就不必担心处理 IOException 异常了。
API java.util.Scanner 5.0
•Scanner(File f) 构造一个从给定文件读取数据的 Scanner。
•Scanner(String data) 构造一个从给定字符串读取数据的 Scanner。
API java.io.PrintWriter 1.1
• PrintWriter(String fileName) 构造一个将数据写入文件的 PrintWriter。 文件名由参数指定。
API Java.nio.file.Paths 7
• static Path get(String pathname) 根据给定的路径名构造一个 Path。
3.8 控制流程
与任何程序设计语言一样, Java 使用条件语句和循环结构确定控制流程。本节先讨论条件语句, 然后讨论循环语句,最后介绍看似有些笨重的 switch 语句,当需要对某个表达式的多个值进行检测时, 可以使用 switch 语句。
C++ 注释:Java 的控制流程结构与 C 和 C++ 的控制流程结构一样, 只有很少的例外情 况。没有 goto 语句,但 break 语句可以带标签, 可以利用它实现从内层循环跳出的目的 (这种情况 C 语言采用 goto 语句实现。) 另外,还有一种变形的 for 循环, 在 C 或 C++ 中 没有这类循环。它有点类似于 C# 中的 foreach 循环。
3.8.1 块作用域
在深入学习控制结构之前, 需要了解块(block) 的概念。
块(即复合语句)是指由一对大括号括起来的若干条简单的 Java 语句。块确定了变量的作用域。一个块可以嵌套在另一个块中。下面就是在 main方法块中嵌套另一个语句块的示例。
public static void main(String口 args)
int n;
int k;
} // k is only defined up to here
但是,不能在嵌套的两个块中声明同名的变量。例如,下面的代码就有错误,而无法通过编译:
public static void main(String口 args)
int n;
int k;
int n ;//Error can't redefine n in inner block
C++ 注释:在 C++ 中, 可以在嵌套的块中重定义一个变量。在内层定义的变量会覆盖在外层定义的变量。这样,有可能会导致程序设计错误, 因此在 Java 中不允许这样做。
3.8.2 条件语句
在 Java 中,条件语句的格式为:if (condition) statement 这里的条件必须用括号括起来。
与绝大多数程序设计语言一样, Java 常常希望在某个条件为真时执行多条语句。在这种情况下, 应该使用块语句 (block statement), 形 式 为
statement1
statement2
...
if (yourSales >= target)
performance = "Satisfactory";
bonus = 100;
当 yourSales 大于或等于 target 时, 将执行括号中的所有语句(请参看图 3-7 ) 。
注释: 使用块 ( 有时称为复合语句)可以在 Java 程序结构中原本只能放置一条 ( 简单)语句的地方放置多条语句。
在 Java 中, 更一般的条件语句格式如下所示 (请参看图 3-8 ):
if (condition) statement1 else statement2
其中 else 部分是可选的。else 子句与最邻近的 if 构成一组。因此,在语句
if (x <= 0) if (x == 0) sign = 0; else sign = -1;
中 else 与第 2 个 if 配对。当然, 用一对括号将会使这段代码更加清晰:
if (x <= 0) { if (x == 0) sign = 0; else sign = -1; }
重复地交替出现 if...else if... 是一种很常见的情况(请参看图 3-9 )。例如:
程序清单 3-3 中的程序将计算需要多长时间才能够存储一定数量的退休金,假定每年存 人相同数量的金额,而且利率是固定的。(此处略过)
while 循环语句首先检测循环条件。因此, 循环体中的代码有可能不被执行。如果希望 循环体至少执行一次, 则应该将检测条件放在最后。 使用 do/while 循环语句可以实现这种操作方式。它的语法格式为:
do statement while (condition);
这种循环语句先执行语句 (通常是一个语句块,) 再检测循环条件;然后重复语句,再检测循环条件, 以此类推: 在程序清单 3-4中, 首先计算退休账户中的余额,然后再询问是否打算退休:
balance += payment;
double interest = balance * interestRate / 100;
balance += interest;
year++;
// print current balance
// ask if ready to retire and get input
while (input. equals ("N"));
3.8.4 确定循环
for 循环语句是支持迭代的一种通用结构, 利用每次迭代之后更新的计数器或类似的变量 来控制迭代次数。 如图 3-12 所示, 下面的程序将数字 1 ~ 10 输出到屏幕上。
for (int i = 1; i <= 10; i++)
System.out.println(i);
for 语句的第 1 部分通常用于对计数器初始化;第 2 部分给出每次新一轮循环执行前要检 测的循环条件;第 3 部分指示如何更新计数器。
与 C++ —样, 尽管 Java 允许在 for 循环的各个部分放置任何表达式,但有一条不成文的 规则:for 语句的 3 个部分应该对同一个计数器变量进行初始化、 检测和更新。若不遵守这一 规则,编写的循环常常晦涩难懂。
即使遵守了这条规则,也还有可能出现很多问题。例如,下面这个倒计数的循环:
for (int i = 10; i > 0; i )
System.out.println("Counting down . . . " + i);
System.out.println("B1astoff!");
警告: 在循环中,检测两个浮点数是否相等需要格外小心。下面的 for 循环
for (double x = 0; x != 10; x += 0.1) . . .
可能永远不会结束。 由于舍入的误差, 最终可能得不到精确值。 例如, 在上面的 循环中, 因为 0.1 无法精确地用二进制表示, 所以,x 将从 9.999 999 999 999 98 跳到10.099 999 999 999 98。
当在 for 语句的第 1 部分中声明了一个变量之后,这个变量的作用域就为 for 循环的整个 循环体。
for (int i = 1; i <= 10; i++)
}
// i no longer defined here
特别指出,如果在 for 语句内部定义一个变量, 这个变量就不能在循环体之外使用。因 此, 如果希望在 for 循环体之外使用循环计数器的最终值,就要确保这个变量在循环语句的 前面且在外部声明!
int i ;
for (i = 1; i <= 10; i++)
// i is still defined here
//另一方面,可以在各自独立的不同 for 循环中定义同名的变量:
for (int i = 1; i <= 10; i++)
for (int i = 11; i <= 20; i ++) // OK to define another variable named i
for 循环语句只不过是 while 循环的一种简化形式例如,
for (int i = 10; i > 0; i-- )
System.out.println("Counting down . . . " + i);
可以重写为:
int i = 10;
while (i > 0)
System.out.println("Counting down . . . " + i);
i--;
注释:3.10.1 节将会介绍“ 通用 for 循环”( 又称为 for each 循环 ), 这是 Java SE 5.0 新增 加的一种循环结构。
3.8.5 多重选择:switch语句
在处理多个选项时, 使用 if/else 结构显得有些笨拙。 Java 有一个与 C/C++ 完全一样的 switch 语句。
例如, 如果建立一个如图 3-13 所示的包含 4 个选项的菜单系统, 可以使用下列代码:
Scanner in = new Scanner(System.in);
System.out.print("Select an option (1, 2, 3, 4)");
int choice = in.nextlnt();
switch (choice)
case 1:
...
break;
case 2:
...
break;
case 3:
...
break;
case 4:
...
break;
default:
// bad input
...
break;
switch语句将从与选项值相匹配的 case 标签处开始执行直到遇到 break 语句,或者执行到 switch 语句的结束处为止。如果没有相匹配的 case 标签, 而有 default 子句, 就执行这个子句。
警告: 有可能触发多个 case 分支。 如果在 case 分支语句的末尾没有 break 语句, 那么就会接着执行下一个 case 分支语句。这种情况相当危险, 常常会引发错误。 为此,我们在 程序中从不使用 switch 语句。
如果你比我们更喜欢 switch 语句, 编译代码时可以考虑加上 -Xlint:fallthrough 选项, 如下所示:
javac -Xlint:fallthrough Test.java
这样一来, 如果某个分支最后缺少一个 break 语句, 编译器就会给出一个警告消息。
如果你确实正是想使用这种“ 直通式”(fallthrough) 行为, 可以为其外围方法加一个 标注@SuppressWamings("fallthrough")。 这样就不会对这个方法生成警告了 。 (标注是为编译器或处理 Java 源文件或类文件的工具提供信息的一种机制。我们将在卷 II 的 第 8 章 详细讨论标注。)
3.8.6 中断控制流程语句
尽管 Java 的设计者将 goto 作为保留字,但实际上并没有打算在语言中使用它。通常, 使用 goto 语句被认为是一种拙劣的程序设计风格。 当然,也有一些程序员认为反对 goto 的 呼声似乎有些过分(例如,Donald Knuth 就曾编著过一篇名为《 Structured Programming with goto statements》的著名文章。) 这篇文章说:无限制地使用 goto 语句确实是导致错误的根源, 但在有些情况下,偶尔使用 goto 跳出循环还是有益处的。Java 设计者同意这种看法,甚至在 Java语言中增加了一条带标签的 break, 以此来支持这种程序设计风格。
下面首先看一下不带标签的 break 语句。与用于退出 switch 语句的 break 语句一样,它 也可以用于退出循环语句。 例如,
while (years <= 100)
balance += payment;
double interest = balance * interestRate / 100;
balance += interest;
if (balance >= goal ) break;
years++ ;
在循环开始时, 如果 years > 100, 或者在循环体中 balance 多 goal , 则退出循环语句。 当然,也可以在不使用 break 的情况下计算 years 的值,如下所示:
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 语句的工作状态。请注意,标签必须放在希望跳出的最外层循环之前, 并且必须紧跟一个冒号。
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
Systen.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 readjata loop
...
// this statement is executed immediately after the labeled break
if (n < 0) // check for bad situation
// deal with bad situation
// carry out normal processing
如果输入有误,通过执行带标签的 break 跳转到带标签的语句块末尾。对于任何使用 break语句的代码都需要检测循环是正常结束, 还是由 break 跳出。
注释: 事实上,可以将标签应用到任何语句中, 甚至可以应用到 if语句或者块语句中, 如下所示:
label:
if (condition) break label; // exits block
...
// jumps here when the break statement executes
因此,如果希望使用一条 goto 语句, 并将一个标签放在想要跳到的语句块之前, 就 可以使用 break 语句! 当然,并不提倡使用这种方式。另外需要注意, 只能跳出语句块, 而不能跳入语句块。
最后,还有一个 continue 语句。与 break 语句一样, 它将中断正常的控制流程。continue 语句将控制转移到最内层循环的首部。例如:
Scanner in = new Scanner(System.in);
while (sum < goal )
System.out.print("Enter a number: ");
n = in.nextlntO;
if (n < 0) continue;
sum += n; // not executed if n < 0
如果 n<0, 则 continue语句越过了当前循环体的剩余部分, 立刻跳到循环首部。
如果将 continue 语句用于 for 循环中, 就可以跳到 for 循环的“ 更新” 部分。例如, 下面 这个循环:
for (count = 1; count <= 100; count++)
System.out.print("Enter a number, -1 to quit: ");
n = in.nextlntO;
if (n < 0) continue;
sum += n; // not executed if n < 0
如果 n<0, 则 continue 语句跳到 count++ 语句。还有一种带标签的 continue 语句,将跳到与标签匹配的循环首部。
3.9 大数值
如果基本的整数和浮点数精度不能够满足需求, 那么可以使用java.math 包中的两个 很有用的类:Biglnteger 和 BigDecimal 这两个类可以处理包含任意长度数字序列的数值。 Biglnteger 类实现了任意精度的整数运算, BigDecimal 实现了任意精度的浮点数运算。
使用静态的 valueOf方法可以将普通的数值转换为大数值:
Biglnteger a = Biglnteger.valueOf(100);
遗憾的是,不能使用人们熟悉的算术运算符(如:+ 和 *) 处理大数值。 而需要使用大数值类中的 add 和 multiply 方法。
Biglnteger c = a.add(b); // c = a + b
Biglnteger d = c.multiply(b.add(Biglnteger.valueOf(2))); // d = c * (b + 2)
C++ 注释: 与 C++ 不同, Java 没有提供运算符重载功能。 程序员无法重定义 + 和 * 运算 符, 使其应用于 BigInteger 类的 add 和 multiply 运算。Java 语言的设计者确实为字符串 的连接重载了 + 运算符,但没有重载其他的运算符,也没有给 Java 程序员在自己的类中重载运算符的机会 ,
lotteryOdds = lotteryOdds *(n - i + 1) / i;
如果使用大数值, 则相应的语句为:
lotteryOdds = lotteryOdds.multiply(BigInteger.valueOf(n - i + 1)).divide(Biglnteger.valueOf(i));
API java.math.Biglnteger 1.1
• Biglnteger add(Biglnteger other)
• Biglnteger subtract(Biglnteger other)
• Biglnteger multiply(Biginteger other)
• Biglnteger divide(Biglnteger other)
• Biglnteger mod(Biglnteger other)
返冋这个大整数和另一个大整数 other的和、 差、 积、 商以及余数。
• int compareTo(Biglnteger other)
如果这个大整数与另一个大整数 other 相等, 返回 0; 如果这个大整数小于另一个大整 数 other, 返回负数; 否则, 返回正数。
• static Biglnteger valueOf(long x)
返回值等于 x 的大整数。
API java.math.Biglnteger 1.1
• BigDecimal add(BigDecimal other)
• BigDecimal subtract(BigDecimal other)
• BigDecimal multipiy(BigDecimal other)
• BigDecimal divide(BigDecimal other RoundingMode mode) 5.0
返回这个大实数与另一个大实数 other 的和、 差、 积、 商。要想计算商, 必须给出舍 入方式 ( rounding mode。) RoundingMode.HALF UP 是在学校中学习的四舍五入方式 ( BP , 数值 0 到 4 舍去, 数值 5 到 9 进位)。它适用于常规的计算。有关其他的舍入方 式请参看 Apr文档。
• int compareTo(BigDecimal other) 如果这个大实数与另一个大实数相等, 返回 0 ; 如果这个大实数小于另一个大实数, 返回负数;否则,返回正数。
• static BigDecimal valueOf(1 ong x)
• static BigDecimal valueOf(1 ong x ,int scale)
返回值为 X 或 x / 10的scale方 的一个大实数。
3.10 数组
数组是一种数据结构, 用来存储同一类型值的集合。通过一个整型下标可以访问数组中的每一个值。例如, 如果 a 是一个整型数组, a[i] 就是数组中下标为 i 的整数。
在声明数组变量时, 需要指出数组类型 ( 数据元素类型紧跟 []) 和数组变量的名字。下 面声明了整型数组 a:int[] a;
不过, 这条语句只声明了变量 a, 并没有将 a 初始化为一个真正的数组。应该使用 new 运算 符创建数组。
int[] a = new int[100];
这条语句创建了一个可以存储 100 个整数的数组。数组长度不要求是常量: newint[n] 会创建 一个长度为 n 的数组。
注释:可以使用下面两种形式声明数组
int[] a; 或 int a[];
大多数 Java 应用程序员喜欢使用第一种风格, 因为它将类型 int[] ( 整型数组)与变量名分开了。
这个数组的下标从 0 ~ 99 (不是 1 ~ 100 )。一旦创建了数组,就可以给数组元素赋值。 例如,使用一个循环:
intp 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, 这表示这些元素(还)未存放任何对象。初学者 对此可能有些不解。例如,
String[] names = new String[10];
会创建一个包含 10 个字符串的数组, 所有字符串都为 null。 如果希望这个数组包含空 串, 可以为元素指定空串:
for (int i = 0; i < 10; i++) names[i]="";
警告: 如果创建了一个 100 个元素的数组, 并且试图访问元素 a[100] (或任何在 0 ~ 99 之外的下标,) 程序就会引发“ array index out of bounds ” 异常而终止执行。
要想获得数组中的元素个数,可以使用 array.length。例如,
for (int i = 0; i < a.length; i ++)
System.out.println(a[i]);
一旦创建了数组, 就不能再改变它的大小(尽管可以改变每一个数组元素)。如果经常需要在运行过程中扩展数组的大小, 就应该使用另一种数据结构—数组列表( array list ) 有关数组列表的详细内容请参看第 5 章。
3.10.1 for each循环
Java 有一种功能很强的循环结构, 可以用来依次处理数组中的每个元素(其他类型的元素集合亦可)而不必为指定下标值而分心。 这种增强的 for 循环的语句格式为: for (variable : collection) statement
定义一个变量用于暂存集合中的每一个元素, 并执行相应的语句(当然,也可以是语句块)。 collection 这一集合表达式必须是一个数组或者是一个实现了 Iterable 接口的类对象(例如 ArrayList)。有关数组列表的内容将在第 5 章中讨论, 有关 Iterable 接口的内容将在第 9 章中 讨论。
for (int element : a)
System.out.println(element):
打印数组 a 的每一个元素,一个元素占一行。
这个循环应该读作“ 循环 a 中的每一个元素”(for each element in a )。Java 语言的设计者认为应该使用诸如 foreach、 in 这样的关键字,但这种循环语句并不是最初就包含在 Java 语 言中的,而是后来添加进去的, 并且没有人打算废除已经包含同名(例如 System.ii ) 方法或 变量的旧代码。
当然,使用传统的 for 循环也可以获得同样的效果:
for (int i = 0; i < a.length; i++)
System,out.println(a[i]);
但是,for each 循环语句显得更加简洁、 更不易出错(不必为下标的起始值和终止值而操心。
注释:foreach 循环语句的循环变量将会遍历数组中的每个元素, 而不需要使用下标值。 如果需要处理一个集合中的所有元素, for each 循环语句对传统循环语句所进行的改进 更是叫人称赞不已。然而, 在很多场合下, 还是需要使用传统的 for 循环。例如,如果不希望遍历集合中的每个元素, 或者在循环内部需要使用下标值等。
提示:有个更加简单的方式打印数组中的所有值, 即利用 Arrays 类的 toString 方法。 调 用 Arrays.toString(a), 返回一个包含数组元素的字符串,这些元素被放置在括号内, 并 用逗号分隔, 例如,“ [2,3,5,7,11,13] ” 、 要想打印数组,可以调用
System.out.println(Arrays.toString(a));
3.10.2 数组初始化以及匿名数组
在 Java中, 提供了一种创建数组对象并同时赋予初始值的简化书写形式。下面是一 例子:
int[] smallPrimes = { 2, 3, 5, 7, 11, 13 };
请注意, 在使用这种语句时,不需要调用 new。
甚至还可以初始化一个匿名的数组:
new int[] { 17, 19, 23, 29, 31, 37 }
这种表示法将创建一个新数组并利用括号中提供的值进行初始化,数组的大小就是初始值的 个数。 使用这种语法形式可以在不创建新变量的情况下重新初始化一个数组。例如:
smallPrimes = new int[] { 17, 19, 23, 29, 31, 37 };
这是下列语句的简写形式:
int[] anonymous = { 17, 19, 23, 29, 31, 37 };
smallPrimes = anonymous;
注释: 在 Java 中, 允许数组长度为 0。在编写一个结果为数组的方法时, 如果碰巧结果为空, 则这种语法形式就显得非常有用。此时可以创建一个长度为 0 的数组:
new elementType[0]
注意, 数组长度为 0 与 null 不同。
3.10.3 数组拷贝
在 Java 中,允许将一个数组变量拷贝给另一个数组变量。这时, 两个变量将引用同 一个数组:
int[] luckyNumbers = smallPrimes;
1uckyNumbers[S] = 12; // now smallPrimes[S] is also 12
如果希望将 一个数组的所有值拷贝到一个新的数组中去, 就要使用 Arrays 类的 copyOf方法:
int[] copiedLuckyNumbers = Arrays.copyOf(luckyNumbers , luckyNumbers.length);
第 2 个参数是新数组的长度。这个方法通常用来增加数组的大小:
luckyNumbers = Arrays.copyOf(luckyNumbers , 2 * luckyNumbers.length);
如果数组元素是数值型,那么多余的元素将被赋值为 0 ; 如果数组元素是布尔型,则将赋值 为 false。相反,如果长度小于原始数组的长度,则只拷贝最前面的数据元素。
C++ 注释:Java 数组与 C++ 数组在堆栈上有很大不同, 但基本上与分配在堆(heap) 上 的数组指针一样。也就是说,
int[] a = new int[100]; // Java
int a[100]; // C++
int* a = new int[100]; // C++
Java 中的 [ ] 运算符被预定义为检查数组边界,而且没有指针运算, 即不能通过 a 加 1 得到数组的下一个元素。
3.10.4 命令行参数
前面已经看到多个使用 Java 数组的示例。 每一个 Java 应用程序都有一个带 String arg[]参数的 main 方法。这个参数表明 main 方法将接收一个字符串数组, 也就是命令行参数。
例如, 看一看下面这个程序:
public class Message
public static void main(String[] args)
if (args.length = 0 11 args[0].equals("_h"))
System.out.print("Hello,");
else if (args[0].equa1s("-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 数组将包含下列内容:
args[0]:"-g"
args[1]:"cruel"
args[2]:"world"
这个程序将显示下列信息:
Goodbye, cruel world!
C++ 注释: 在 Java 应用程序的 main 方法中, 程序名并没有存储在 args 数组中。 例如, 当使用下列命令运行程序时
java Message -h world
args[0] 是“ -h”, 而不是“ Message” 或“ java”。
3.10.5 数组排序
要想对数值型数组进行排序, 可以使用 Arrays 类中的 sort 方法:
int[] a = new int[10000];
Arrays.sort(a)
这个方法使用了优化的快速排序算法。快速排序算法对于大多数数据集合来说都是效率比较高的。Arrays 类还提供了几个使用很便捷的方法, 在稍后的 API 注释中将介绍它们。
程序清单 3-7中的程序用到了数组,它产生一个抽彩游戏中的随机数值组合。 假如抽彩 是从 49 个数值中抽取 6 个,那么程序可能的输出结果为:
Bet the following combination. It'll make you rich!
要想选择这样一个随机的数值集合,就要首先将数值 1, 2, …,n 存入数组 numbers 中:
int[] numbers = new int[n];
for(int i = 0;i < numbers.length;i++)
numbers[i] = i+ 1;
而用第二个数组存放抽取出来的数值:
int[] result = new int[k];
现在,就可以开始抽取 k 个数值了。Math.random 方法将返回一个 0 到 1 之间(包含 0、 不包含 1 ) 的随机浮点数。用 乘以这个浮点数, 就可以得到从 0 到 n-l 之间的一个随机数。
int r = (int) (Math.random() * n);
下面将 result 的第 i 个元素设置为 munbers[r] 存放的数值, 最初是 r+1。但正如所看到 的,numbers 数组的内容在每一次抽取之后都会发生变化。
result[i] = numbers[r];
现在,必须确保不会再次抽取到那个数值,因为所有抽彩的数值必须不相同。因此,这里用数组中的最后一个数值改写 mimber[r],并将 n 减 1。
numbers[r] = numbers[n - 1];
n--;
关键在于每次抽取的都是下标, 而不是实际的值。下标指向包含尚未抽取过的数组元素。 在抽取了 k 个数值之后, 就可以对 result 数组进行排序了, 这样可以让输出效果更加清晰:
Arrays.sort(result);
for (int r : result)
System.out.println(r);
API Java.util.Arrays 1.2
•static String toString(type[] a) 5 . 0
返回包含 a 中数据元素的字符串, 这些数据元素被放在括号内, 并用逗号分隔。 参数: a 类型为 int、long、short、 char、 byte、boolean、float 或 double 的数组。
• static type copyOf(type[]a, int length)6
• static type copyOfRange(type[]a , int start, int end)6
返回与 a 类型相同的一个数组, 其长度为 length 或者 end-start, 数组元素为 a 的值。
参数:a 类型为 int、 long、 short、 char、 byte、boolean、 float 或 double 的数组。
start 起始下标(包含这个值)0
end 终止下标(不包含这个值)。这个值可能大于 a.length。 在这种情况 下,结果为 0 或 false。
length 拷贝的数据元素长度。 如果 length 值大于 a.length, 结果为 0 或 false ; 否则, 数组中只有前面 length 个数据元素的拷贝 值。 參 • static void sort(type [ ] a)
采用优化的快速排序算法对数组进行排序。 参数:a 类型为 int、long、short、char、byte、boolean、float 或 double 的数组。
•static int binarySearch(type[]a ,type v)
• static int binarySearch(type[]a, int start, int end, type v) 6
采用二分搜索算法查找值 v。如果查找成功, 则返回相应的下标值; 否则, 返回一个 负数值 r。 -r-1 是为保持 a 有序 v 应插入的位置。
参数:a 类型为 int、 long、 short、 char、 byte、 boolean 、 float 或 double 的有 序数组。
start 起始下标(包含这个值)。
end 终止下标(不包含这个值)。
v 同 a 的数据元素类型相同的值。
• static void fi11(type[]a , type v) 将数组的所有数据元素值设置为 V。
参数:a 类型为 int、 long、short、 char、byte、boolean、float 或 double 的数组。
v 与 a 数据元素类型相同的一个值。
• static boolean equals(type[]a, type[]b)
如果两个数组大小相同, 并且下标相同的元素都对应相等, 返回 true。
参数:a、 b 类型为 int、long、short、char、byte、boolean、float 或 double 的两个数组。
3.10.6 多维数组
多维数组将使用多个下标访问数组元素, 它适用于表示表格或更加复杂的排列形式。这 一节的内容可以先跳过, 等到需要使用这种存储机制时再返四来学习。
假设需要建立一个数值表, 用来显示在不同利率下投资 $10,000 会增长多少,利息每年 兑现, 而且又被用于投资(见表 3-8) 。
可以使用一个二维数组(也称为矩阵) 存储这些信息。这个数组被命名为 balances。
在 Java 中, 声明一个二维数组相当简单。例如:double[][] balances;
与一维数组一样, 在调用 new 对多维数组进行初始化之前不能使用它。 在这里可以这样 初始化:
balances = new double[NYEARS] [NRATES]:
另外, 如果知道数组元素, 就可以不调用 new, 而直接使用简化的书写形式对多维数组 进行初始化。例如:
int[][] magicSquare =
{16, 3, 2, 13},
{5, 10, 11, 8},
(9, 6, 7, 12},
{4, 15, 14, 1}
一旦数组被初始化, 就可以利用两个方括号访问每个元素, 例如, balances[i][j]。
在示例程序中用到了一个存储利率的一维数组 interest 与一个存储余额的二维数组 balances。一维用于表示年, 另一维用于表示利率, 最初使用初始余额来初始化这个数组的 第一行:
for (int j = 0; j < balances[0].length; j++)
balances [0][j] = 10000;
然后, 按照下列方式计算其他行:
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;
程序清单 3-8 给出了完整的程序。
注释: for each 循环语句不能自动处理二维数组的每一个元素。它是按照行, 也就是一维数组处理的要想访问二维教组 a 的所有元素, 需要使用两个嵌套的循环, 如下所示:
for (double[] row : a)
for (double value : row)
do something with value
提示: 要想快速地打印一个二维数组的数据元素列表, 可以调用:
System.out.println(Arrays.deepToString(a));
输出格式为:
[[16, B, 2, 13], [5, 10, 11, 8], [9, 6, 7, 12], [4, 15, 14, 1]]
3.10.7 不规则数组
到目前为止,读者所看到的数组与其他程序设计语言中提供的数组没有多大区别。但实际存在着一些细微的差异, 而这正是 Java 的优势所在:Java 实际上没有多维数组,只有一维数组。多维数组被解释为“ 数组的数组。”
例如, 在前面的示例中, balances 数组实际上是一个包含 10 个元素的数组,而每个元素 又是一个由 6 个浮点数组成的数组(请参看图 3-15 )。
表达式 balances[i]引用第 i 个子数组, 也就是二维表的第 i 行。它本身也是一个数组, balances[i][j] 引用这个数组的第 j 项。
由于可以单独地存取数组的某一行, 所以可以让两行交换。
double[] temp = balances[i]:
balances[i] = balances[i + 1];
balances[i + 1] = temp;
还可以方便地构造一个“ 不规则” 数组, 即数组的每一行有不同的长度。下面是一个典型的示例。在这个示例中,创建一个数组, 第 i 行第 j 列将存放“ 从 i 个数值中抽取 j 个数值” 产生的结果,
1 2 1
1 3 3 1
1 4 6 4 1
1 5 1 0 1 0 5 1
1 6 1 5 2 0 1 5 6 1
由于 j 不可能大于 i, 所以矩阵是三角形的。第 i 行有 i + 1 个元素(允许抽取 0 个元素, 也是一种选择) 。要想创建一个不规则的数组, 首先需要分配一个具有所含行数的数组。
int[][] odds = new int[NMAX + 1] [] ;
接下来, 分配这些行。
for (int n = 0; n <= NMAX ; n++)
odds [n] = new int[n + 1];
在分配了数组之后, 假定没有超出边界, 就可以采用通常的方式访问其中的元素了。
for (int n = 0; n < odds.length; n++)
for (int k = 0; k < odds [n] .length; k++)
// compute lotteryOdds
...
odds [n] [k] = lotteryOdds;
现在, 已经看到了 Java 语言的基本程序结构, 下一章将介绍 Java 中的面向对象的程序设计。
恭喜,本章完!