Java核心技术卷阅读随笔--第3章【Java 的基本程序设计结构】

3.1 一个简单的Java应用程序
  下面看一个最简单的 Java 应用程序,它只发送一条消息到控制台窗口中:

复制代码
public class FirstSample
{
  public static void main(String[] args)
  {
    System.out.println(“We will not use 'Hello, World!”’);
  }
}
复制代码
  这个程序虽然很简单, 但所有的 Java 应用程序都具有这种结构, 还是值得花一些时间来研究。首先,Java 区分大小写。 如果出现了大小写拼写错误(例如, 将 main 拼写成 Main), 程序将无法运行。

下面逐行地查看一下这段源代码。关键字 public 称为访问修饰符(access modifier), 这 些修饰符用于控制程序的其他部分对这段代码的访问级別。在第 5 章中将会更加详细地介绍访问修饰符的具体内容。关键字 class 表明 Java 程序中的全部内容都包含在类中。这里, 只需要将类作为一个加载程序逻辑的容器,程序逻辑定义了应用程序的行为。在第 4 章中将会用大量的篇幅介绍 Java 类。正如第 1 章所述, 类是构建所有 Java 应用程序和 applet 的构建块。Java 应用程序中的全部内容都必须放置在类中。

关键字 class 后面紧跟类名。 Java 中定义类名的规则很宽松。名字必须以字母开头,后面可以跟字母和数字的任意组合。长度基本上没有限制。但是不能使用 Java 保留字(例如, public 或 class) 作为类名(保留字列表请参看附录 A)。

标准的命名规范为(类名 FirstSample 就遵循了这个规范):类名是以大写字母开头的名 词。如果名字由多个单词组成,每个单词的第一个字母都应该大写(这种在一个单词中间使用大写字母的方式称为骆驼命名法。以其自身为例, 应该写成 CamelCase)。

源代码的文件名必须与公共类的名字相同,并用 .java 作为扩展名。因此,存储这段源代 码的文件名必须为 FirstSample.java (再次提醒大家注意,大小写是非常重要的, 千万不能写成 firstsample.java)。

如果已经正确地命名了这个文件, 并且源代码中没有任何录入错误,在编译这段源代码之后就会得到一个包含这个类字节码的文件。Java 编译器将字节码文件自动地命名为 FirstSample. class, 并与源文件存储在同一个目录下。最后, 使用下面这行命令运行这个程序:

java FirstSample

(请记住,不要添加 .class 扩展名。)程序执行之后,控制台上将会显示“ We will not use ‘ Hello,World,!”。

当使用

java ClassName

运行已编译的程序时,Java 虚拟机将从指定类中的 main 方法开始执行(这里的“ 方法” 就是 Java 中所说的“ 函数”),因此为了代码能够执行,在类的源文件中必须包含一个 main 方法。当然,也可以将用户自定义的方法添加到类中,并且在 main 方法中调用它们(第 4 章 将讲述如何自定义方法)。

注释:根据 Java 语言规范, main 方法必须声明为 public ( Java 语言规范是描述 Java 语言 的官方文档。可以从网站 http://docs.oracle.com/javase/specs 上阅读或下载)。

不过, 当 main 方法不是 public 时, 有些版本的 Java 解释器也可以执行 Java 应用程序。 有个程序员报告了这个 bug 如果感兴趣的话, 可以在网站 http://bugsjava.com/ bugdatabase/ indexjsp 上输入 bug 号码 4252539 查看。这个 bug 被标明“ 关闭, 不予修 复。” Sun 公司的工程师解释说: Java 虚拟机规范(在 http://docs.orade.com/javase/specs/ jvms/se8/html) 并没有要求 main 方法一定是 public, 并且“ 修复这个 bug 有可能带来其 他的隐患”。好在,这个问题最终得到了解决。在 Java SE 1.4 及以后的版本中强制 main 方法是 public 的。

从上面这段话可以发现一个问题的两个方面。 一方面让质量保证工程师判断在 bug报告中是否存在问题是一件很头痛的事情, 这是因为其工作量很大,并且工程师对 Java 的所有细节也未必了解得很清楚。 另一方面, Sun 公司在 Java 开源很久以前就把 bug 报告及其解决方案放到网站上让所有人监督检查, 这是一种非常了不起的举动某些情况 下, Sun 甚至允许程序员为他们最厌恶的 bug 投票, 并用投票结果来决定发布的下一个 JDK 版本将修复哪些 bug。

需要注意源代码中的括号{ }。在 Java 中,像在 C/C++ 中一样,用大括号划分程序的各个部分(通常称为块)。Java 中任何方法的代码都用“ {” 开始,用“}”结束。

大括号的使用风格曾经引发过许多无意义的争论。我们的习惯是把匹配的大括号上下对齐。不过,由于空白符会被 Java 编译器忽略,所以可以选用自己喜欢的大括号风格。在下面讲述各种循环语句时, 我们还会详细地介绍大括号的使用。

我们暂且不去理睬关键字 static void, 而仅把它们当作编译 Java 应用程序必要的部分就行了。在学习完第 4 章后,这些内容的作用就会揭晓。现在需要记住:每个 Java 应用程序都 必须有一个 main 方法,其声明格式如下所示:

复制代码
public class ClassName
{
public static void main(String[] args)
{
program statements
}
}

复制代码
  C++ 注释: 作为一名 C++ 程序员, 一定知道类的概念。Java 的类与 C++ 的类很相似, 但还是有些差异会使人感到困惑。 例如, Java 中的所有函数都属于某个类的方法(标准术语将其称为方法, 而不是成员函数)。因此,Java 中的 main 方法必须有一个外壳类。 读者有可能对 C++ 中的静态成员函数( static member functions) 十分熟悉。这些成员函数定义在类的内部, 并且不对对象进行操作。Java 中的 main 方法必须是静态的。 最后, 与 C/C++ —样, 关键字 void 表示这个方法没有返回值, 所不同的是 main 方法没有为操作系统返回“ 退出代码” 。如果 main 方法正常退出, 那么 Java 应用程序的退出代码为 0, 表示成功地运行了程序。如果希望在终止程序时返回其他的代码, 那就需要调用 System.exit 方法。

接下来,研究一下这段代码:

{
Systein.out.println(“We will not use ‘Hello, World!’”);
}
  一对大括号表示方法体的开始与结束, 在这个方法中只包含一条语句。与大多数程序设计语言一样, 可以将 Java 语句看成是这种语言的句子。在 Java 中,每个句子必须用分号结束。 特别需要说明,回车不是语句的结束标志,因此,如果需要可以将一条语句写在多行上。

在上面这个 main 方法体中只包含了一条语句,其功能是:将一个文本行输出到控制台上。

在这里,使用了 System.out 对象并调用了它的 println 方法。注意, 点号( • )用于调用方法。Java 使用的通用语法是

object,method(parameters)

这等价于函数调用。

在这个示例中,调用了 println 方法并传递给它一个字符串参数。 这个方法将传递给它的字符串参数显示在控制台上。然后,终止这个输出行,使得每次调用 println 都会在新的一行 上显示输出。需要注意一点,Java 与 C/C++ —样,都采用双引号分隔字符串。(本章稍后将 会详细地讲解有关字符串的知识)。

与其他程序设计语言中的函数一样,在 Java 的方法中,可以没有参数, 也可以有一个或 多个参数(有的程序员把参数叫做实参。) 对于一个方法, 即使没有参数也需要使用空括号。 例如, 不带参数的 println 方法只打印一个空行。使用下面的语句来调用:

System,out.printlnO;

注释: System.out 还有一个 print 方法, 它在输出之后不换行。例如, System.out.print ( “ Hello”)打印“ Hello” 之后不换行, 后面的输出紧跟在字母“o”之后。

3.2 注释
  与大多数程序设计语言一样,Java 中的注释也不会出现在可执行程序中。因此, 可以在源程序中根据需要添加任意多的注释,而不必担心可执行代码会膨胀。在 Java 中,有 3 种标记注释的方式。最常用的方式是使用 // 其注释内容从 // 开始到本行结尾。

System.out.println(“We will not use 'Hello, World!’”);// is this too cute?
  当需要长篇的注释时, 既可以在每行的注释前面标记 // 也可以使用 /* 和 */ 将一段比较长的注释括起来。

最后,第 3 种注释可以用来自动地生成文档。这种注释以 / ** 开始, 以 */ 结束。请参见 程序清单 3-1。 有关这种注释的详细内容和自动生成文档的具体方法请参见第 4 章。

警告: 在 Java 中,/* / 注释不能嵌套 „ 也就是说, 不能简单地把代码用 / 和 */ 括起来 作为注释, 因为这段代码本身可能也包含一个 */。

3.3 数据类型
  Java 是 一种强类型语言。这就意味着必须为每一个变量声明一种类型: 在 Java 中, -共有 8 种基本类型( primitive type ), 其中有 4 种整型、2 种浮点类型、 1 种用于表示 Unicode 编码的字符单元的字符类型 char (请参见论述 char 类型的章节) 和 1 种用于表示真值的 boolean 类型。

注释: Java 有一个能够表示任意精度的算术包, 通常称为“ 大数值”( bignumber。) 虽然 被称为大数值,但它并不是一种新的 Java 类型,而是一个 Java 对象。 本章稍后将会详细地介绍它的用法

3.3.1 整型
    整型用于表示没有小数部分的数值, 它允许是负数。Java 提供了 4 种整型,具体内容如 表 3-1 所示。

在通常情况下,int 类型最常用。但如果表示星球上的居住人数, 就需要使用 long 类型 了。byte 和 short 类型主要用于特定的应用场合,例如,底层的文件处理或者需要控制占用存储空间量的大数组。

在 Java 中, 整型的范围与运行 Java 代码的机器无关。这就解决了软件从一个平台移植到 另一个平台,或者在同一个平台中的不同操作系统之间进行移植给程序员带来的诸多问题。与此相反,C 和 C++ 程序需要针对不同的处理器选择最为高效的整型, 这样就有可能造成一个在 32 位处理器上运行很好的 C 程序在 16 位系统上运行却发生整数溢出。由于 Java 程序必须保证 在所有机器 L都能够得到相同的运行结果, 所以各种数据类型的取值范围必须固定。

长整型数值有一个后缀 L 或 1 ( 如 4000000000L。) 十六进制数值有一个前缀 Ox 或 0X (如 OxCAFEL 八进制有一个前缀 0 , 例如, 010 对应八进制中的 8。 很显然, 八进制表示法比较容易混淆, 所以建议最好不要使用八进制常数。

从 Java 7 开始, 加上前缀 0b 或 0B 就可以写二进制数。例如,0b1001就是 9。 另外,同样是 从 Java 7 开始,还可以为数字字面量加下划线,如用 1_000_000(或0b1111_0100_0010_0100_0000 ) 表示一百万。这些下划线只是为丫让人更易读。Java 编译器会去除这些下划线。

C++ 注释: 在 C 和 C++ 中, int 和 long 等类型的大小与目标平台相关。在 8086 这样的 16 位处理器上整型数值占 2 字节;不过, 在 32 位处理器(比如 Pentium 或 SPARC) 上, 整型数值则为 4 字节。 类似地, 在 32 位处理器上 long 值为 4 字节, 在 64 位处理器上则 为 8 字节。由于存在这些差别, 这对编写跨平台程序带来了很大难度。 在 Java 中, 所有 的数值类型所占据的字节数量与平台无关。

注意, Java 没有任何无符号(unsigned) 形式的 int、 long、short 或 byte 类型。

3.3.2 浮点类型
    浮点类型用于表示有小数部分的数值。在 Java 中有两种浮点类型,具体内容如表 3-2 所示。

double 表示这种类型的数值精度是 float 类型的两倍(有人称之为双精度数值)。绝大部分应用程序都采用 double 类型。在很多情况下,float 类型的精度很难满足需求。实际上,只 有很少的情况适合使用 float 类型,例如,需要单精度数据的库, 或者需要存储大量数据。

float 类型的数值有一个后缀 F 或 f (例如,3.14F) 。没有后缀 F 的浮点数值(如 3.14 ) 默认为 double 类型。当然,也可以在浮点数值后面添加后缀 D 或 d (例如,3.14D) 。

注释:可以使用十六进制表示浮点数值。例如,0.125=2的负3次方 可以表示成 0xl.0p-3。在十六 进制表示法中, 使用 p 表示指数, 而不是 e。 注意, 尾数采用十六进制,指数采用十进制。指数的基数是 2,而不是 10。

所有的浮点数值计算都遵循 IEEE 754 规范。具体来说,下面是用于表示溢出和出错情况 的三个特殊的浮点数值:

•正无穷大

•负无穷大

•NaN (不是一个数字)

例如, 一 正整数除以 0 的结果为正无穷大。计算 0/0 或者负数的平方根结果为 NaN。

注释: 常量 Double_POSITIVE_INFINITY、 Double.NEGATIVEJNFINITY 和 Double.NaN ( 以及相应的 Float 类型的常量) 分别表示这三个特殊的值, 但在实际应用中很少遇到。 特别要说明的是, 不能这样检测一个特定值是否等于 Double.NaN:

if (x = Double.NaN)// is never true
    所有“ 非数值” 的值都认为是不相同的。然而,可以使用 Double.isNaN 方法:

if (Double.isNaN(x)) // check whether x is “not a number”
    警告: 浮点数值不适用于无法接受舍入误差的金融计算中。 例如,命令 System.out.println ( 2.0-1.1 ) 将打印出 0.8999999999999999, 而不是人们想象的 0.9。这种舍入误差的主要原因是浮点数值采用二进制系统表示, 而在二进制系统中无法精确地表示分数 1/10。这就好像十进制无法精确地表示分数 1/3 —样。如果在数值计算中不允许有任何舍入误差, 就应该使用 BigDecimal类, 本章稍后将介绍这个类。

3.3.3 char类型
    char 类型原本用于表示单个字符。不过,现在情况已经有所变化。 如今,有些 Unicode 字符可以用一个 char值描述,另外一些 Unicode 字符则需要两个 char 值。有关的详细信息请 阅读下一节。

char 类型的字面量值要用单引号括起来。例如:‘A’是编码值为 65 所对应的字符常量。 它与 “A” 不同,“A” 是包含一个字符 A 的字符串, char 类型的值可以表示为十六进制值,其 范围从 \u0000 到 \Uffff。例如:W2122 表示注册符号 ( ), \u03C0 表示希腊字母 it。

除了转义序列 \u 之外, 还有一些用于表示特殊字符的转义序列, 请参看表 3-3。所有这些转义序列都可以出现在加引号的字符字面量或字符串中。例如,’ \u2122’ 或 "Hello\n”。转义序列 \u还可以出现在加引号的字符常量或字符串之外(而其他所有转义序列不可以)。例 如:

public static void main(String\u005B\ u00SD args)
    就完全符合语法规则, \u005B 和 \u005D 是 [ 和 ] 的编码。

警告: Unicode 转义序列会在解析代码之前得到处理。 例如,"\u0022+\u0022 ” 并不是一 个由引号(U+0022) 包围加号构成的字符串。 实际上, \u0022 会在解析之前转换为 “, 这 会得到 “”+”",也就是一个空串。

更隐秘地, 一定要当心注释中的 \u。注释

// \u00A0 is a newline

会产生一个语法错误, 因为读程序时 \u00A0 会替换为一个换行符类似地,下面这个注释

// Look inside c:\users

也会产生一个语法错误, 因为 \u 后面并未跟着 4 个十六进制数, ,

3.3.4 Unicode和char类型
    要想弄清 char 类型, 就必须了解 Unicode 编码机制。Unicode 打破了传统字符编码机制的限制。 在 Unicode 出现之前, 已经有许多种不同的标准:美国的 ASCII、 西欧语言中的 ISO 8859-1 、俄罗斯的 KOI-8、 中国的 GB 18030 和 BIG-5 等。这样就产生了下面两个问题: 一个是对于任意给定的代码值,在不同的编码方案下有可能对应不同的字母;二是采用大字符集的语言其编码长度有可能不同。例如,有些常用的字符采用单字节编码, 而另一些字符则需要两个或更多个字节。

设计 Unicode 编码的目的就是要解决这些问题。在 20 世纪 80 年代开始启动设计工作时, 人们认为两个字节的代码宽度足以对世界上各种语言的所有字符进行编码, 并有足够的空间留给未来的扩展。在 1991 年发布了 Unicode 1.0, 当时仅占用 65 536 个代码值中不到一半的 部分。在设计 Java 时决定采用 16 位的 Unicode 字符集,这样会比使用 8 位字符集的程序设计语言有很大的改进。

十分遗憾, 经过一段时间, 不可避免的事情发生了。Unicode 字符超过了 65 536 个,其主要原因是增加了大量的汉语、 日语和韩语中的表意文字。现在,16 位的 char 类型已经不能满足描述所有 Unicode 字符的需要了。

下面利用一些专用术语解释一下 Java 语言解决这个问题的基本方法。从 Java SE 5.0 开 始。码点( code point) 是指与一个编码表中的某个字符对应的代码值。在 Unicode 标准中, 码点采用十六进制书写,并加上前缀 U+, 例如 U+0041 就是拉丁字母 A 的码点。Unicode 的 码点可以分成 17 个代码级别( codeplane)。第一个代码级别称为基本的多语言级别( 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+D800 ~ U+DBFF 用于第一个代码单元,U+DC00 ~ U+DFFF 用 于第二个代码单元 ]。这样设计十分巧妙,我们可以从中迅速地知道一个代码单元是一个字符的编码,还是一个辅助字符的第一或第二部分。例如,是八元数集(http://math.ucr.edu/ home/baez/octonions) 的一个数学符号,码点为 U+1D546, 编码为两个代码单元 U+D835 和 U+DD46。(关于编码算法的具体描述见 http://en.wikipedia.org/wiki/UTF-l6 ) 。

在 Java 中,char 类型描述了 UTF-16 编码中的一个代码单元。

我们强烈建议不要在程序中使用 char 类型,除非确实需要处理 UTF-16 代码单元。最好将字符串作为抽象数据类型处理(有关这方面的内容将在 3.6 节讨论)。

3.3.4 boolean类型
    boolean (布尔)类型有两个值:false 和 true, 用来判定逻辑条件整型值和布尔值之间不能进行相互转换。

C++ 注释: 在 C++ 中, 数值甚至指针可以代替 boolean 值。值 0 相当于布尔值 false, 非 0 值相当于布尔值 true, 在 Java 中则不是这样,, 因此, Java 程序员不会遇到下述麻烦:

if (x = 0) // oops… meant x = 0

在 C++ 中这个测试可以编译运行, 其结果总是 false: 而在 Java 中, 这个测试将不 能通过编译, 其原因是整数表达式 x = 0 不能转换为布尔值。

3.4 变量
  在 Java 中,每个变量都有一个类型( type)。在声明变量时,变量的类型位于变量名之前。这里列举一些声明变量的示例:

double salary;
int vacationDays;
long earthPopulation;
boolean done;
  可以看到,每个声明以分号结束。由于声明是一条完整的 Java语句,所以必须以分号结束。

变量名必须是一个以字母开头并由字母或数字构成的序列。需要注意,与大多数程序设计语言相比,Java 中“ 字母” 和“ 数字” 的范围更大。字母包括 ’ A’ ~ ’ Z’、 ’ a 1 ~ ’ z 1 、 或在某种语言中表示字母的任何 Unicode 字符。例如, 德国的用户可以在变量名中使用字母 ;希腊人可以用π。同样, 数字包括 ‘0’ ~ '9 ’ 和在某种语言中表示数字的任何 Unicode 字 符。但 ‘+’ 和 '©’ 这样的符号不能出现在变量名中,空格也不行。变量名中所有的字符都是有意义的,并且大小写敏感。变量名的长度基本上没有限制。

提示:如果想要知道哪些 Unicode 字符属于 Java 中的“ 字母”, 可以使用 Character 类的 isJavaldentifierStart 和 isJavaldentifierPart 方法来检查。

提示:尽管 $ 是一个合法的 Java 字符, 但不要在你自己的代码中使用这个字符。它只用 在 Java 编译器或其他工具生成的名字中。

另外, 不能使用 Java 保留字作为变量名(请参看附录 A 中的保留字列表。)

可以在一行中声明多个变量:

int i , j; // both are integers
  不过,不提倡使用这种风格。逐一声明每一个变量可以提高程序的可读性。

注释:如前所述, 变量名对大小写敏感, 例如,hireday 和 hireDay 是两个不同的变量名。在对两个不同的变量进行命名时, 最好不要只存在大小写上的差异。 不过,在有些时候, 确实很难给变量取一个好的名字。于是,许多程序员将变量名命名为类型名, 例如:

Box box; // “Box” is the type and “box” is the variable name
  还有一些程序员更加喜欢在变量名前加上前缀“ a”:

Box aBox;
  3.4.1 变量初始化
    声明一个变量之后,必须用赋值语句对变量进行显式初始化, 千万不要使用未初始化的 变量。例如, Java 编译器认为下面的语句序列是错误的:

int vacationDays;
System.out.println(vacationDays): // ERROR variable not initialized
    要想对一个已经声明过的变量进行赋值, 就需要将变量名放在等号(=) 左侧, 相应取值 的 Java 表达式放在等号的右侧。

int vacationDays;
vacationDays = 12;
    也可以将变量的声明和初始化放在同一行中。例如:

int vacationDays = 12;
    最后,在 Java 中可以将声明放在代码中的任何地方。例如,下列代码的书写形式在 Java 中是完全合法的:

double salary = 65000.0;
System,out.println(salary);
int vacationDays = 12; // OK to declare a variable here
    在 Java 中, 变量的声明尽可能地靠近变量第一次使用的地方, 这是一种良好的程序编写风格。

C++ 注释:C 和 C++ 区分变量的声明与定义。例如:

int i = 10;

是一个定义, 而

extern int i;

是一个声明。在 Java 中, 不区分变量的声明与定义。

3.4.2 常量
    在 Java 中, 利用关键字 final 指示常量。例如:

复制代码
public class Constants
{
public static void main(String[] args)
{
  final double CM_PER_INCH = 2.54;
    double paperWidth = 8.5;
    double paperHeight = 11;
    System,out.println("Paper size in centimeters: "
      + paperWidth * CM PER INCH + " by " + paperHeight * CM.PER.INCH);
  }
}
复制代码
    关键字 final 表示这个变量只能被赋值一次。一旦被赋值之后,就不能够再更改了。习惯上, 常量名使用全大写。

在 Java 中,经常希望某个常量可以在一个类中的多个方法中使用,通常将这些常量称为 类常量。可以使用关键字 static final设置一个类常量。 下面是使用类常量的示例:

复制代码
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++;
可以使用下列语句实现回退操作:
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 s %2 stB %2 t e , te, %2 te,tY", “Due date:”, new Date());
    打印 Due date: February 9, 2015

还可以选择使用 < 标志。它指示前面格式说明中的参数将被再次使用。也就是说, 下列语句将产生与前面语句同样的输出结果:

System.out .printf("%s %tB %     提示: 参 教 索 引 值 从 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

复制代码
例如:
if (yourSales >= target)
{
  performance = “Satisfactory”;
  bonus = 100 + 0.01 *(yourSales - target);
}
else
{
  performance = “Unsatisfactory”;
  bonus = 0;
}
复制代码
    其中 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 )。例如:

复制代码
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”);
}

USB Microphone https://www.soft-voice.com/
Wooden Speakers https://www.zeshuiplatform.com/
亚马逊测评 www.yisuping.cn
深圳网站建设www.sz886.com

你可能感兴趣的:(Java核心技术卷阅读随笔--第3章【Java 的基本程序设计结构】)