X-Spirit的陋室铭

生活有的时候面临许多选择,这些选择让人困惑。 生活有的时候没有任何选择,这个时候让人压抑。 但无论生活有没有给你选择,你只能做一件事情: 排除困难,勇往直前!

2007年10月2日星期二

Unicode编码规范

Unicode On Wiki: http://www.wiki.cn/wiki/Unicode

前几天朋友在做用PHP做图片的水印功能,说是一定要用到汉字的Unicode的编码,才能显出汉字,和他说了半天,仔细想想,自己也没有清楚 unicode是什么一回事,所以就找一点资料学习一下,没有学过操作系统,和其它一些基本的计算机的东西,看来自己要恶补一下了!

Unicode是一种字符编码规范 。

先从ASCII说起。ASCII是用来表示英文字符的一种编码规范,每个ASCII字符占用1个字节(8bits)

因此,ASCII编码可以表示的最大字符数是256,其实英文字符并没有那么多,一般只用前128个(最高位为0),其中包括了控制字符、数字、
大小写字母和其他一些符号 。

而最高位为1的另128个字符被成为“扩展ASCII”,一般用来存放英文的制表符、部分音标字符等等的一些其他符号

这种字符编码规范显然用来处理英文没有什么问题 。(实际上也可以用来处理法文、德文等一些其他的西欧字符,但是不能和英文通用),
但是面对中文、阿拉伯文之类复杂的文字,255个字符显然不够用

于是,各个国家纷纷制定了自己的文字编码规范,其中中文的文字编码规范叫做“GB2312-80”,它是和ASCII兼容的一种编码规范,其实就是
利用扩展ASCII没有真正标准化这一点,把一个中文字符用两个扩展ASCII字符来表示。

但是这个方法有问题,最大的问题就是,中文文字没有真正属于自己的编码,因为扩展ASCII码虽然没有真正的标准化,但是PC里的ASCII码还
是有一个事实标准的(存放着英文制表符),所以很多软件利用这些符号来画表格。这样的软件用到中文系统中,这些表格符就会被误认作中
文字,破坏版面。而且,统计中英文混合字符串中的字数,也是比较复杂的,我们必须判断一个ASCII码是否扩展,以及它的下一个ASCII是否
扩展,然后才“猜”那可能是一个中文字 。

总之当时处理中文是很痛苦的。而更痛苦的是GB2312是国家标准,台湾当时有一个Big5编码标准,很多编码和GB是相同的,所以……,嘿嘿。

这时候,我们就知道,要真正解决中文问题,不能从扩展ASCII的角度入手,也不能仅靠中国一家来解决。而必须有一个全新的编码系统,这
个系统要可以将中文、英文、法文、德文……等等所有的文字统一起来考虑,为每个文字都分配一个单独的编码,这样才不会有上面那种现象
出现。

于是,Unicode诞生了。

Unicode有两套标准,一套叫UCS-2(Unicode-16),用2个字节为字符编码,另一套叫UCS-4(Unicode-32),用4个字节为字符编码。

以目前常用的UCS-2为例,它可以表示的字符数为2^16=65535,基本上可以容纳所有的欧美字符和绝大部分的亚洲字符 。

UTF-8的问题后面会提到 。

在Unicode里,所有的字符被一视同仁。汉字不再使用“两个扩展ASCII”,而是使用“1个Unicode”,注意,现在的汉字是“一个字符”了,
于是,拆字、统计字数这些问题也就自然而然的解决了 。

但是,这个世界不是理想的,不可能在一夜之间所有的系统都使用Unicode来处理字符,所以Unicode在诞生之日,就必须考虑一个严峻的问题
:和ASCII字符集之间的不兼容问题。

我们知道,ASCII字符是单个字节的,比如“A”的ASCII是65。而Unicode是双字节的,比如“A”的Unicode是0065,这就造成了一个非常大的
问题:以前处理ASCII的那套机制不能被用来处理Unicode了 。

另一个更加严重的问题是,C语言使用’\0′作为字符串结尾,而Unicode里恰恰有很多字符都有一个字节为0,这样一来,C语言的字符串函数将
无法正常处理Unicode,除非把世界上所有用C写的程序以及他们所用的函数库全部换掉 。

于是,比Unicode更伟大的东东诞生了,之所以说它更伟大是因为它让Unicode不再存在于纸上,而是真实的存在于我们大家的电脑中。那就是
:UTF 。

UTF= UCS Transformation Format UCS转换格式

它是将Unicode编码规则和计算机的实际编码对应起来的一个规则。现在流行的UTF有2种:UTF-8和UTF-16 。

其中UTF-16和上面提到的Unicode本身的编码规范是一致的,这里不多说了。而UTF-8不同,它定义了一种“区间规则”,这种规则可以和ASCII
编码保持最大程度的兼容 。

UTF-8有点类似于Haffman编码,它将Unicode编码为00000000-0000007F的字符,用单个字节来表示;

00000080-000007FF的字符用两个字节表示

00000800-0000FFFF的字符用3字节表示

因为目前为止Unicode-16规范没有指定FFFF以上的字符,所以UTF-8最多是使用3个字节来表示一个字符。但理论上来说,UTF-8最多需要用6字
节表示一个字符。

在UTF-8里,英文字符仍然跟ASCII编码一样,因此原先的函数库可以继续使用。而中文的编码范围是在0080-07FF之间,因此是2个字节表示
(但这两个字节和GB编码的两个字节是不同的),用专门的Unicode处理类可以对UTF编码进行处理。

下面说说中文的问题。

由于历史的原因,在Unicode之前,一共存在过3套中文编码标准。

GB2312-80,是中国大陆使用的国家标准,其中一共编码了6763个常用简体汉字。Big5,是台湾使用的编码标准,编码了台湾使用的繁体汉字,
大概有8千多个。HKSCS,是中国香港使用的编码标准,字体也是繁体,但跟Big5有所不同。

这3套编码标准都采用了两个扩展ASCII的方法,因此,几套编码互不兼容,而且编码区间也各有不同

因为其不兼容性,在同一个系统中同时显示GB和Big5基本上是不可能的。当时的南极星、RichWin等等软件,在自动识别中文编码、自动显示正
确编码方面都做了很多努力 。

他们用了怎样的技术我就不得而知了,我知道好像南极星曾经以同屏显示繁简中文为卖点。

后来,由于各方面的原因,国际上又制定了针对中文的统一字符集GBK和GB18030,其中GBK已经在Windows、Linux等多种操作系统中被实现。

GBK兼容GB2312,并增加了大量不常用汉字,还加入了几乎所有的Big5中的繁体汉字。但是GBK中的繁体汉字和Big5中的几乎不兼容。

GB18030相当于是GBK的超集,比GBK包含的字符更多。据我所知目前还没有操作系统直接支持GB18030。

谈谈Unicode编码,简要解释UCS、UTF、BMP、BOM等名词
这是一篇程序员写给程序员的趣味读物。所谓趣味是指可以比较轻松地了解一些原来不清楚的概念,增进知识,类似于打RPG游戏的升级。整理
这篇文章的动机是两个问题:

问题一:
使用Windows记事本的“另存为”,可以在GBK、Unicode、Unicode big endian和UTF-8这几种编码方式间相互转换。同样是txt文件,Windows
是怎样识别编码方式的呢?

我很早前就发现Unicode、Unicode big endian和UTF-8编码的txt文件的开头会多出几个字节,分别是FF、FE(Unicode),FE、FF
(Unicode big endian),EF、BB、BF(UTF-8)。但这些标记是基于什么标准呢?

问题二:
最近在网上看到一个ConvertUTF.c,实现了UTF-32、UTF-16和UTF-8这三种编码方式的相互转换。对于Unicode(UCS2)、GBK、UTF-8这些编码方
式,我原来就了解。但这个程序让我有些糊涂,想不起来UTF-16和UCS2有什么关系。
查了查相关资料,总算将这些问题弄清楚了,顺带也了解了一些Unicode的细节。写成一篇文章,送给有过类似疑问的朋友。本文在写作时尽量
做到通俗易懂,但要求读者知道什么是字节,什么是十六进制。

0、big endian和little endian
big endian和little endian是CPU处理多字节数的不同方式。例如“汉”字的Unicode编码是6C49。那么写到文件里时,究竟是将6C写在前面,
还是将49写在前面?如果将6C写在前面,就是big endian。还是将49写在前面,就是little endian。

“endian”这个词出自《格列佛游记》。小人国的内战就源于吃鸡蛋时是究竟从大头(Big-Endian)敲开还是从小头(Little-Endian)敲开,由
此曾发生过六次叛乱,其中一个皇帝送了命,另一个丢了王位。

我们一般将endian翻译成“字节序”,将big endian和little endian称作“大尾”和“小尾”。

1、字符编码、内码,顺带介绍汉字编码
字符必须编码后才能被计算机处理。计算机使用的缺省编码方式就是计算机的内码。早期的计算机使用7位的ASCII编码,为了处理汉字,程序
员设计了用于简体中文的GB2312和用于繁体中文的big5。

GB2312(1980年)一共收录了7445个字符,包括6763个汉字和682个其它符号。汉字区的内码范围高字节从B0-F7,低字节从A1-FE,占用的码位
是72*94=6768。其中有5个空位是D7FA-D7FE。

GB2312支持的汉字太少。1995年的汉字扩展规范GBK1.0收录了21886个符号,它分为汉字区和图形符号区。汉字区包括21003个字符。2000年的
GB18030是取代GBK1.0的正式国家标准。该标准收录了27484个汉字,同时还收录了藏文、蒙文、维吾尔文等主要的少数民族文字。现在的PC平
台必须支持GB18030,对嵌入式产品暂不作要求。所以手机、MP3一般只支持GB2312。

从ASCII、GB2312、GBK到GB18030,这些编码方法是向下兼容的,即同一个字符在这些方案中总是有相同的编码,后面的标准支持更多的字符。
在这些编码中,英文和中文可以统一地处理。区分中文编码的方法是高字节的最高位不为0。按照程序员的称呼,GB2312、GBK到GB18030都属
于双字节字符集 (DBCS)。

有的中文Windows的缺省内码还是GBK,可以通过GB18030升级包升级到GB18030。不过GB18030相对GBK增加的字符,普通人是很难用到的,通常
我们还是用GBK指代中文Windows内码。

这里还有一些细节:

GB2312的原文还是区位码,从区位码到内码,需要在高字节和低字节上分别加上A0。

在DBCS中,GB内码的存储格式始终是big endian,即高位在前。

GB2312的两个字节的最高位都是1。但符合这个条件的码位只有128*128=16384个。所以GBK和GB18030的低字节最高位都可能不是1。不过这不
影响DBCS字符流的解析:在读取DBCS字符流时,只要遇到高位为1的字节,就可以将下两个字节作为一个双字节编码,而不用管低字节的高位
是什么。

2、Unicode、UCS和UTF
前面提到从ASCII、GB2312、GBK到GB18030的编码方法是向下兼容的。而Unicode只与ASCII兼容(更准确地说,是与ISO-8859-1兼容),
与GB码不兼容。例如“汉”字的Unicode编码是6C49,而GB码是BABA。

Unicode也是一种字符编码方法,不过它是由国际组织设计,可以容纳全世界所有语言文字的编码方案。Unicode的学名是”Universal Multiple

-Octet Coded Character Set”,简称为UCS。UCS可以看作是”Unicode Character Set”的缩写。

根据维基百科全书(http://zh.wikipedia.org/wiki/)的记载:历史上存在两个试图独立设计Unicode的组织,即国际标准化组织(ISO)和一
个软件制造商的协会(unicode.org)。ISO开发了ISO 10646项目,Unicode协会开发了Unicode项目。

在1991年前后,双方都认识到世界不需要两个不兼容的字符集。于是它们开始合并双方的工作成果,并为创立一个单一编码表而协同工作。
从Unicode2.0开始,Unicode项目采用了与ISO 10646-1相同的字库和字码。

目前两个项目仍都存在,并独立地公布各自的标准。Unicode协会现在的最新版本是2005年的Unicode 4.1.0。ISO的最新标准是10646-3:2003。

UCS规定了怎么用多个字节表示各种文字。怎样传输这些编码,是由UTF(UCS Transformation Format)规范规定的,常见的UTF规范包括UTF-8、
UTF-7、UTF-16。

IETF的RFC2781和RFC3629以RFC的一贯风格,清晰、明快又不失严谨地描述了UTF-16和UTF-8的编码方法。我总是记不得IETF是Internet Engine
ering Task Force的缩写。但IETF负责维护的RFC是Internet上一切规范的基础。

3、UCS-2、UCS-4、BMP

UCS有两种格式:UCS-2和UCS-4。顾名思义,UCS-2就是用两个字节编码,UCS-4就是用4个字节(实际上只用了31位,最高位必须为0)编码。
下面让我们做一些简单的数学游戏:

UCS-2有2^16=65536个码位,UCS-4有2^31=2147483648个码位。

UCS-4根据最高位为0的最高字节分成2^7=128个group。每个group再根据次高字节分为256个plane。每个plane根据第3个字节分为256行
(rows),每行包含256个cells。当然同一行的cells只是最后一个字节不同,其余都相同。

group 0的plane 0被称作Basic Multilingual Plane, 即BMP。或者说UCS-4中,高两个字节为0的码位被称作BMP。

将UCS-4的BMP去掉前面的两个零字节就得到了UCS-2。在UCS-2的两个字节前加上两个零字节,就得到了UCS-4的BMP。而目前的UCS-4规范中还
没有任何字符被分配在BMP之外。

4、UTF编码

UTF-8就是以8位为单元对UCS进行编码。从UCS-2到UTF-8的编码方式如下:

UCS-2编码(16进制) UTF-8 字节流(二进制)
0000 - 007F 0xxxxxxx
0080 - 07FF 110xxxxx 10xxxxxx
0800 - FFFF 1110xxxx 10xxxxxx 10xxxxxx

例如“汉”字的Unicode编码是6C49。6C49在0800-FFFF之间,所以肯定要用3字节模板了:1110xxxx 10xxxxxx 10xxxxxx。将6C49写成二进制
是:0110 110001 001001, 用这个比特流依次代替模板中的x,得到:11100110 10110001 10001001,即E6 B1 89。

读者可以用记事本测试一下我们的编码是否正确。

UTF-16以16位为单元对UCS进行编码。对于小于0×10000的UCS码,UTF-16编码就等于UCS码对应的16位无符号整数。对于不小于0×10000的UCS码
,定义了一个算法。不过由于实际使用的UCS2,或者UCS4的BMP必然小于0×10000,所以就目前而言,可以认为UTF-16和UCS-2基本相同。
但UCS-2只是一个编码方案,UTF-16却要用于实际的传输,所以就不得不考虑字节序的问题。

5、UTF的字节序和BOM
UTF-8以字节为编码单元,没有字节序的问题。UTF-16以两个字节为编码单元,在解释一个UTF-16文本前,首先要弄清楚每个编码单元的字节序
。例如收到一个“奎”的Unicode编码是594E,“乙”的Unicode编码是4E59。如果我们收到UTF-16字节流“594E”,那么这是“奎”还是“乙
”?

Unicode规范中推荐的标记字节顺序的方法是BOM。BOM不是“Bill Of Material”的BOM表,而是Byte Order Mark。BOM是一个有点小聪明的想
法:

在UCS编码中有一个叫做”ZERO WIDTH NO-BREAK SPACE”的字符,它的编码是FEFF。而FFFE在UCS中是不存在的字符,所以不应该出现在实际传
输中。UCS规范建议我们在传输字节流前,先传输字符”ZERO WIDTH NO-BREAK SPACE”。

这样如果接收者收到FEFF,就表明这个字节流是Big-Endian的;如果收到FFFE,就表明这个字节流是Little-Endian的。因此字符”ZERO WIDTH
NO-BREAK SPACE”又被称作BOM。

UTF-8不需要BOM来表明字节顺序,但可以用BOM来表明编码方式。字符”ZERO WIDTH NO-BREAK SPACE”的UTF-8编码是EF BB BF(读者可以用
我们前面介绍的编码方法验证一下)。所以如果接收者收到以EF BB BF开头的字节流,就知道这是UTF-8编码了

2007年9月30日星期日

3.8 Control Flow 控制流

Control Flow 控制流

Java, like any programming language, supports both conditional statements and loops to determine control flow. We start with the conditional statements and then move on to loops. We end with the somewhat cumbersome switch statement that you can use when you have to test for many values of a single expression.

本节介绍条件语句和循环语句,最后介绍用于检测单一表达式的多个值的开关语句。

C++ NOTE


The Java control flow constructs are identical to those in C and C++, with a few exceptions. There is no goto, but there is a "labeled" version of break that you can use to break out of a nested loop (where you perhaps would have used a goto in C). Finally. JDK 5.0 adds a variant of the for loop that has no analog in C or C++. It is similar to the foreach loop in C#.

Java控制流结构和C以及C++是相同的,有些例外。没有goto语句 ,但是有加标签的break语句,你可以跳出嵌套循环(在C中你可能使用goto语句)。最后,JDK5.0增加了一个for循环的变体,该变体在C或C++中没有类似。它和C#中的foreach循环相似。


Block Scope 块作用域

Before we get into the actual control structures, you need to know more about blocks.

A block or compound statement is any number of simple Java statements that are surrounded by a pair of braces. Blocks define the scope of your variables. Blocks can be nested inside another block. Here is a block that is nested inside the block of the main method.

一个块或者复合语句是由一对花括号括起来的许多简单Java语句。块定义了你变量的作用范围。块可以内嵌在另一个块中。这里有一个内嵌在main方法块中的块。

public static void main(String[] args)

{

int n;

. . .

{

int k;

. . .

} // k is only defined up to here

}


However, you may not declare identically named variables in two nested blocks. For example, the following is an error and will not compile:

但是不要再嵌套的两个块中定义同名的变量,这样会导致编译出错。

public static void main(String[] args)

{

int n;

. . .

{

int k;

int n; // error--can't redefine n in inner block

. . .

}

}


C++ NOTE


In C++, it is possible to redefine a variable inside a nested block. The inner definition then shadows the outer one. This can be a source of programming errors; hence, Java does not allow it.

在C++中,在嵌套块中定义同名变量是可能的。内部定义可能覆盖掉外部定义。这可能导致程序错误,因此Java不允许该情况。


Conditional Statements 条件语句

The conditional statement in Java has the form

Java中的条件语句有如下形式:if (条件) 语句


if (condition) statement

The condition must be surrounded by parentheses.

条件必须被圆括号括起来。

In Java, as in most programming languages, you will often want to execute multiple statements when a single condition is true. In this case, you use a block statement that takes the form

在Java中,和在多数编程语言中一样,你经常需要在一个条件为真时执行多条语句。此时,你需要使用一个块语句来使之清晰明了


{
statement
1
statement
2
. . .
}

For example:例如

if (yourSales >= target)

{

performance = "Satisfactory";

bonus = 100;

}


In this code all the statements surrounded by the braces will be executed when yourSales is greater than or equal to target. (See Figure 3-8.)

在这段代码中,当yourSales大于或等于target时,括号中的语句将被执行。

Figure 3-8. Flowchart for the if statement


NOTE


A block (sometimes called a compound statement) allows you to have more than one (simple) statement in any Java programming structure that might otherwise have a single (simple) statement.

一个块有时候也可允许你在其中放置一条语句,或者也可放置一条以上的语句。


The more general conditional in Java looks like this (see Figure 3-9):
Java
中更常见的条件判断如下
if (condition) statement1 else statement2

Figure 3-9. Flowchart for the if/else statement


For example:例如

if (yourSales >= target)

{

performance = "Satisfactory";

bonus = 100 + 0.01 * (yourSales - target);

}

else

{

performance = "Unsatisfactory";

bonus = 0;

}


The else part is always optional. An else groups with the closest if. Thus, in the statement
else
部分总是可选的。一个else部分总是紧跟最近的if。因此下面的语句中

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


the else belongs to the second if.
else
部分属于第二个if。

Repeated if . . . else if . . . alternatives are common (see Figure 3-10). For example:
重复的if…else if …交替是很常见的(参见图3-10)。例如:

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");

}


Figure 3-10. Flowchart for the if/else if (multiple branches)


Loops 循环

The while loop executes a statement (which may be a block statement) while a condition is TRue. The general form is
当条件判断为真时,while循环执行循环体内的语句。通式如下:
while (condition) statement

The while loop will never execute if the condition is false at the outset (see Figure 3-11).
如果条件判断在最开始时就为false,则while循环永远不会执行。(图3-11)

Figure 3-11. Flowchart for the while statement


The program in Example 3-3 determines how long it will take to save a specific amount of money for your well-earned retirement, assuming that you deposit the same amount of money per year and that the money earns a specified interest rate.
程序Example3-3将求出为你正式退休存定量的钱要花多长时间,假设你每年定量存款,并且这些钱有一个指定的利率。

In the example, we are incrementing a counter and updating the amount currently accumulated in the body of the loop until the total exceeds the targeted amount.

在这个例子中,我们使一个计数器自增并更新当前循环体内的积累的总和,直到总量超过目标量。

while (balance < goal)

{

balance += payment;

double interest = balance * interestRate / 100;

balance += interest;

years++;

}

System.out.println(years + " years.");


(Don't rely on this program to plan for your retirement. We left out a few niceties such as inflation and your life expectancy.)
(不要依靠这个程序来为你的退休生活做预算。我们忽略了一些细节,例如通货膨胀和你的预期寿命。)

A while loop tests at the top. Therefore, the code in the block may never be executed. If you want to make sure a block is executed at least once, you will need to move the test to the bottom. You do that with the do/while loop. Its syntax looks like this:
while
循环在开始时做判断。因此,循环体内的代码可能永远不会执行。如果你希望保证块内的代码至少执行一次,你需要将检测移动到底部。你可以用do/while循环来做,语句如下:


do statement while (condition);

This loop executes the statement (which is typically a block) and only then tests the condition. It then repeats the statement and retests the condition, and so on. The code in Example 3-4 computes the new balance in your retirement account and then asks if you are ready to retire:
这个循环首先执行语句(这些语句是一个典型的块结构),仅当执行完后才进行条件判断。之后再重复语句和执行判断,如此循环往复。例子3-4中的代码计算了你退休帐户中的新余额,并问你是否准备退休。

do

{

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"));


As long as the user answers "N", the loop is repeated (see Figure 3-12). This program is a good example of a loop that needs to be entered at least once, because the user needs to see the balance before deciding whether it is sufficient for retirement.
只要用户回答”N”,循环就重复(看图3-12)。这个程序是至少需要进入一次的循环的一个好例子,因为用户需要在判断帐户余额是否足够退休前就看到余额情况。

Example 3-3. Retirement.java

1. import java.util.*;

2.

3. public class Retirement

4. {

5. public static void main(String[] args)

6. {

7. // read inputs

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

9.

10. System.out.print("How much money do you need to retire? ");

11. double goal = in.nextDouble();

12.

13. System.out.print("How much money will you contribute every year? ");

14. double payment = in.nextDouble();

15.

16. System.out.print("Interest rate in %: ");

17. double interestRate = in.nextDouble();

18.

19. double balance = 0;

20. int years = 0;

21.

22. // update account balance while goal isn't reached

23. while (balance < goal)

24. {

25. // add this year's payment and interest

26. balance += payment;

27. double interest = balance * interestRate / 100;

28. balance += interest;

29. years++;

30. }

31.

32. System.out.println("You can retire in " + years + " years.");

33. }

34. }


Example 3-4. Retirement2.java

1. import java.util.*;

2.

3. public class Retirement2

4. {

5. public static void main(String[] args)

6. {

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

8.

9. System.out.print("How much money will you contribute every year? ");

10. double payment = in.nextDouble();

11.

12. System.out.print("Interest rate in %: ");

13. double interestRate = in.nextDouble();

14.

15. double balance = 0;

16. int year = 0;

17.

18. String input;

19.

20. // update account balance while user isn't ready to retire

21. do

22. {

23. // add this year's payment and interest

24. balance += payment;

25. double interest = balance * interestRate / 100;

26. balance += interest;

27.

28. year++;

29.

30. // print current balance

31. System.out.printf("After year %d, your balance is %,.2f%n", year, balance);

32.

33. // ask if ready to retire and get input

34. System.out.print("Ready to retire? (Y/N) ");

35. input = in.next();

36. }

37. while (input.equals("N"));

38. }

39.}


Figure 3-12. Flowchart for the do/while statement


Determinate Loops 确定性循环

The for loop is a general construct to support iteration that is controlled by a counter or similar variable that is updated after every iteration. As Figure 3-13 shows, the following loop prints the numbers from 1 to 10 on the screen.
for
循环是一个常见的结构,该结构支持由计数器或者简单变量控制的迭代,这些计数器和变量在每次迭代完成后都会更新。如图3-13所示,下面的循环在屏幕上输出1至10。

for (int i = 1; i <= 10; i++)

System.out.println(i);


Figure 3-13. Flowchart for the for statement


The first slot of the for statement usually holds the counter initialization. The second slot gives the condition that will be tested before each new pass through the loop, and the third slot explains how to update the counter.
for
语句的第一个空隙通常写入计数器初始化。第二个空隙给出每轮循环之前都要进行的条件判断,第三个空隙说明了如何更新计数器的值。

Although Java, like C++, allows almost any expression in the various slots of a for loop, it is an unwritten rule of good taste that the three slots of a for statement should only initialize, test, and update the same counter variable. One can write very obscure loops by disregarding this rule.
虽然Java和C++一样,允许for循环中的各个空隙中的任何表达式,但是for循环的三个空隙应当是初始化、判断、更新相同变量,这已经成了一条不成文的规定。忽视这条规则将导致循环语句晦涩难懂。

Even within the bounds of good taste, much is possible. For example, you can have loops that count down:
可以写出倒数的循环


for (int i = 10; i > 0; i--)

System.out.println("Counting down . . . " + i);

System.out.println("Blastoff!");


CAUTION


Be careful about testing for equality of floating-point numbers in loops. A for loop that looks like this

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


may never end. Because of roundoff errors, the final value may not be reached exactly. For example, in the loop above, x jumps from 9.99999999999998 to 10.09999999999998 because there is no exact binary representation for 0.1.

由于没有精确的0.1的值,使用浮点数字作为循环变量可能导致死循环。


When you declare a variable in the first slot of the for statement, the scope of that variable extends until the end of the body of the for loop.
循环变量的作用范围是整个循环体

for (int i = 1; i <= 10; i++)

{

. . .

}

// i no longer defined here


In particular, if you define a variable inside a for statement, you cannot use the value of that variable outside the loop. Therefore, if you wish to use the final value of a loop counter outside the for loop, be sure to declare it outside the loop header!
如果你希望循环变量的值能够在循环体外继续使用,请在for循环之前定义该变量:

int i;

for (i = 1; i <= 10; i++)

{

. . .

}

// i still defined here


On the other hand, you can define variables with the same name in separate for loops:
在两个独立的for循环中可以定义同名循环变量:

for (int i = 1; i <= 10; i++)

{

. . .

}

. . .

for (int i = 11; i <= 20; i++) // ok to define another variable named i

{

. . .

}


A for loop is merely a convenient shortcut for a while loop. For example,
for
循环不过仅是while循环的简便形式,例如

for (int i = 10; i > 0; i--)

System.out.println("Counting down . . . " + i);


can be rewritten as可以写成

int i = 10;

while (i > 0)

{

System.out.println("Counting down . . . " + i);

i--;

}


Example 3-5 shows a typical example of a for loop.
例子3-5展示了一个for循环的典型例子

The program computes the odds on winning a lottery. For example, if you must pick 6 numbers from the numbers 1 to 50 to win, then there are (50 x 49 x 48 x 47 x 46 x 45)/(1 x 2 x 3 x 4 x 5 x 6) possible outcomes, so your chance is 1 in 15,890,700. Good luck!
这个程序计算了赢得彩票的几率。例如,你从1至50中选出6个数字,就有(50 x 49 x 48 x 47 x 46 x 45)/(1 x 2 x 3 x 4 x 5 x 6)中可能的选法,所以你的机会是15,890,700分之1。祝你好运。。。

In general, if you pick k numbers out of n, there are
通常,如果你从n中选出k个数字,就有


possible outcomes. The following for loop computes this value:
种可能的选法。下面的for循环计算了如下的值:

int lotteryOdds = 1;

for (int i = 1; i <= k; i++)

lotteryOdds = lotteryOdds * (n - i + 1) / i;


NOTE


See page 82 for a description of the "generalized for loop" (also called "for each" loop) that was added to the Java language in JDK 5.0.
在数组一节中还将介绍JDK 5中新增的for each循环


Example 3-5. LotteryOdds.java

1. import java.util.*;

2.

3. public class LotteryOdds

4. {

5. public static void main(String[] args)

6. {

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

8.

9. System.out.print("How many numbers do you need to draw? ");

10. int k = in.nextInt();

11.

12. System.out.print("What is the highest number you can draw? ");

13. int n = in.nextInt();

14.

15. /*

16. compute binomial coefficient

17. n * (n - 1) * (n - 2) * . . . * (n - k + 1)

18. -------------------------------------------

19. 1 * 2 * 3 * . . . * k

20. */

21.

22. int lotteryOdds = 1;

23. for (int i = 1; i <= k; i++)

24. lotteryOdds = lotteryOdds * (n - i + 1) / i;

25.

26. System.out.println("Your odds are 1 in " + lotteryOdds + ". Good luck!");

27. }

28. }


Multiple Selections—The switch Statement

The if/else construct can be cumbersome when you have to deal with multiple selections with many alternatives. Java has a switch statement that is exactly like the switch statement in C and C++, warts and all.
事实上,Java也有C和C++中的switch语句 。

For example, if you set up a menuing system with four alternatives like that in Figure 3-14, you could use code that looks like this:

Scanner in = new Scanner(System.in);

System.out.print("Select an option (1, 2, 3, 4) ");

int choice = in.nextInt();

switch (choice)

{

case 1:

. . .

break;

case 2:

. . .

break;

case 3:

. . .

break;

case 4:

. . .

break;

default:

// bad input

. . .

break;

}


Figure 3-14. Flowchart for the switch statement


Execution starts at the case label that matches the value on which the selection is performed and continues until the next break or the end of the switch. If none of the case labels match, then the default clause is executed, if it is present.

如果不加入break语句,则会跳到下一个分支。但是default语句块是都不匹配时执行的。

Note that the case labels must be integers or enumerated constants. You cannot test strings. For example, the following is an error:
case
后面必须是整数或者枚举类型。不可以是字符串

String input = . . .;

switch (input) // ERROR

{

case "A": // ERROR

. . .

break;

. . .

}


PITFALL缺陷


It is possible for multiple alternatives to be triggered. If you forget to add a break at the end of an alternative, then execution falls through to the next alternative! This behavior is plainly dangerous and a common cause for errors. For that reason, we never use the switch statement in our programs.
由于忘记加入break语句可能导致执行下一个分支,所以不建议在程序中使用switch语句。


Statements That Break Control Flow

Although the designers of Java kept the goto as a reserved word, they decided not to include it in the language. In general, goto statements are considered poor style. Some programmers feel the anti-goto forces have gone too far (see, for example, the famous article of Donald Knuth called "Structured Programming with goto statements"). They argue that unrestricted use of goto is error prone but that an occasional jump out of a loop is beneficial. The Java designers agreed and even added a new statement, the labeled break, to support this programming style.

尽管goto语句可能破坏程序结构,但是我们还是偶尔需要跳出循环,因此Java的设计师们加入了标签式Break。

Let us first look at the unlabeled break statement. The same break statement that you use to exit a switch can also be used to break out of a loop. For example,以下是不加标签的break语句。用于退出switch的break语句也可以退出一个循环。

while (years <= 100)

{

balance += payment;

double interest = balance * interestRate / 100;

balance += interest;

if (balance >= goal) break;

years++;

}


Now the loop is exited if either years > 100 occurs at the top of the loop or balance >= goal occurs in the middle of the loop. Of course, you could have computed the same value for years without a break, like this:
以上程序段也可写成如下这个样子

while (years <= 100 && balance < goal)

{

balance += payment;

double interest = balance * interestRate / 100;

balance += interest;

if (balance < goal)

years++;

}


But note that the test balance < goal is repeated twice in this version. To avoid this repeated test, some programmers prefer the break statement.
但是请注意balance<goal这个判断在这个版本中重复了两次。要避免重复判断,很多程序员更倾向于使用break语句。

Unlike C++, Java also offers a labeled break statement that lets you break out of multiple nested loops. Occasionally something weird happens inside a deeply nested loop. In that case, you may want to break completely out of all the nested loops. It is inconvenient to program that simply by adding extra conditions to the various loop tests.
与C++不同,Java也包含标签式break语句可以使你跳出多层嵌套循环。

Here's an example that shows the break statement at work. Notice that the label must precede the outermost loop out of which you want to break. It also must be followed by a colon.
下面是一个使用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

{

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

}


If there was a bad input, the labeled break moves past the end of the labeled block. As with any use of the break statement, you then need to test whether the loop exited normally or as a result of a break.
如果输入有误,标签式的break语句跳转到标签的块之后执行。由于使用了break语句,你需要检测循环是否正常退出或者是否达到break的目的。

NOTE


Curiously, you can apply a label to any statement, even an if statement or a block statement, like this:
有趣的是,你可以对任何语句应用标签,即使是if语句或是语句体,如下:


label
:
{
. . .
if (condition) break label; // exits block
. . .
}
// jumps here when the break statement executes

Thus, if you are lusting after a goto and if you can place a block that ends just before the place to which you want to jump, you can use a break statement! Naturally, we don't recommend this approach. Note, however, that you can only jump out of a block, never into a block.
因此,如果你希望使用goto跳转,或者你希望在一个将一个语句体放置在你想跳转的点之前,你可以使用break语句。当然,我们不推荐此方法。注意,你只能跳出一个块,而不能跳出一个块。


Finally, there is a continue statement that, like the break statement, breaks the regular flow of control. The continue statement transfers control to the header of the innermost enclosing loop. Here is an example:
最后,有一种continue语句,和break语句一样,可以打破规则的控制流。continue语句将控制转向最内层的封闭循环的起始处。下面是个例子:

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

}


If n < 0, then the continue statement jumps immediately to the loop header, skipping the remainder of the current iteration.
如果n<0,则continue语句立刻跳过当前迭代的剩余部分,转向循环的顶部。

If the continue statement is used in a for loop, it jumps to the "update" part of the for loop. For example, consider this loop.
如果continue语句用于for循环,则其跳转到for循环的“更新”部分,例如:

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

}


If n < 0, then the continue statement jumps to the count++ statement.
如果n<0,则continue语句跳转到count++语句。

There is also a labeled form of the continue statement that jumps to the header of the loop with the matching label.
也有加标签的continue语句可以跳转到指定标签所指向的循环的头部。


TIP


Many programmers find the break and continue statements confusing. These statements are entirely optional—you can always express the same logic without them. In this book, we never use break or continue.
如果你对break和continue语句感到困惑,你可以不使用他们。本书中,我们不会使用它们的。