4.5.4 使用数组

数组最常用的用法就是访问数组元素,包括对数组元素进行赋值和取出数组元素的值。访问数组元素都是通过在数组引用变量后紧跟一个方括号([]),方括号里是数组元素的索引值,这样就可以访问数组元素了。访问到数组元素后,就可以把一个数组元素当成一个普通变量使用了,包括为该变量赋值和取出该变量的值,这个变量的类型就是定义数组时使用的类型。

Java语言的数组索引是从0开始的,也就是说,第一个数组元素的索引值为0,最后一个数组元素的索引值为数组长度减1。下面代码示范了输出数组元素的值,以及为指定数组元素赋值:

1
2
3
4
// 输出objArr数组的第二个元素,将输出字符串"李刚"
System.out.println(objArr[1]);
// 为objArr2的第一个数组元素赋值
objArr2[0] = "Spring";

数组越界

如果访问数组元素时指定的索引值小于0,或者大于等于数组的长度,编译程序不会出现任何错误但运行时出现异常:java.lang.ArrayIndexOutofBoundsException:N(数组索引越界异常),异常信息后的N就是程序员试图访问的数组索引。
下面代码试图访问的数组元素索引值等于数组长度,将引发数组索引越界异常:

1
2
// 访问数组元素指定的索引等于数组长度,所以下面代码将在运行时出现异常
System.out.println(objArr2[2]);

length属性

所有的数组都提供了一个length属性,通过这个属性可以访问到数组的长度,一旦获得了数组的长度,就可以通过循环来遍历该数组的每个数组元素。

下面代码示范了输出prices数组(动态初始化的int数组)的每个数组元素的值

1
2
3
4
// 使用循环输出prices数组的每个数组元素的值
for (int i = 0; i < prices.length; i++) {
System.out.println(prices[i]);
}

执行上面代码将输出5个0,因为prices数组执行的是默认初始化,数组元素是int类型,系统为int类型的数组元素赋值为0
下面代码示范了为动态初始化的数组元素进行赋值,并通过循环方式输出每个数组元素:

1
2
3
4
5
6
7
// 对动态初始化后的数组元素进行赋值
books[0] = "Hello";
books[1] = "World";
// 使用循环输出books数组的每个数组元素的值
for (int i = 0; i < books.length; i++) {
System.out.println(books[i]);
}

上面代码将先输出字符串”Hello”和”World”,然后输出两个null。因为books使用了动态初始化,系统为所有数组元素都分配一个null作为初始值,后来程序又为前两个元素赋值,所以看到了这样的程序输出结果。
从上面代码中不难看出,初始化一个数组后,相当于同时初始化了多个相同类型的变量,通过数组元素的索引就可以自由访问这些数组元素。使用数组元素与使用普通变量并没有什么不同,一样可以对数组元素进行赋值,或者取出数组元素的值。

4.5.3 数组的初始化

什么是数组初始化

Java语言中数组必须先初始化,然后才可以使用。所谓初始化,就是为数组的数组元素分配内存空间,并为每个数组元素赋初始值。

数组元素一旦分配了空间就有了初始值

一旦为数组的每个数组元素分配了内存空间,每个内存空间里存储的内容就是该数组元素的值,即使这个内存空间存储的内容是空,这个空也是一个值(null)。不管以哪种方式来初始化数组,只要为数组元素分配了内存空间,数组元素就具有了初始值。初始值的获得有两种形式:一种由系统自动分配另一种由程序员指定。

静态初始化 动态初始化

数组的初始化有如下两种方式:

  • 静态初始化:初始化时由程序员显式指定每个数组元素的初始值,由系统决定数组长度
  • 动态初始化:初始化时程序员只指定数组长度,由系统为数组元素分配初始值

1. 静态初始化

静态初始化的语法格式如下:

1
arrayName = new type[]{element1, element2,element3,...};

在上面的语法格式中,前面的type就是数组元素的数据类型,此处的type必须与定义数组变量时所使用的type相同,也可以是定义数组时所指定的type的子类,并使用花括号把所有的数组元素括起来,多个数组元素之间以英文逗号(,)隔开,定义初始化值的花括号紧跟在之后。
值得指出的是,执行静态初始化时,显式指定的数组元素值的类型必须与new关键字后的type类型相同,或者是其子类的实例

程序示例 数组静态初始化

下面代码定义了使用这三种形式来进行静态初始化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 定义一个int数组类型的变量,变量名为intArr.
int[] intArr;
// 使用静态初始化,初始化数组时只指定数组元素的初始值,不指定数组长度。
intArr = new int[] { 5, 6, 8, 20 };

// 定义一个Object数组类型的变量,变量名为objArr.
Object[] objArr;
// 使用静态初始化,初始化数组时数组元素的类型是
// 定义数组时所指定的数组元素类型的子类
objArr = new String[] { "Hello", "World" };

Object[] objArr2;
// 使用静态初始化
objArr2 = new Object[] { "Hello", "World" };

因为Java语言是面向对象的编程语言,能很好地支持子类和父类的继承关系:子类实例是一种特殊的父类实例。在上面程序中,String类型是Object类型的子类,即字符串是一种特殊的Object实例。

静态初始化简写

除此之外,静态初始化还有如下简化的语法格式:

1
type[] arrayName = {element1, element2, element3, ... };

在这种语法格式中,直接使用花括号来定义一个数组,花括号把所有的数组元素括起来形成一个数组。只有在定义数组的同时执行数组初始化才支持使用简化的静态初始化
简写形式比规范的动态初始化等号右边少了new type[]

使用简写形式比较多

在实际开发过程中,可能更习惯将数组定义和数组初始化同时完成,代码如下

1
2
// 数组的定义和初始化同时完成,使用简化的静态初始化写法
int[] a = { 5, 6, 7, 9 };

2. 动态初始化

动态初始化只指定数组的长度,由系统为每个数组元素指定初始值。动态初始化的语法格式如下:

1
arrayName = new type[length];

在上面语法中,需要指定一个int类型的length参数,这个参数指定了数组的长度,也就是可以容纳数组元素的个数。

与静态初始化相似的是,此处的type必须与定义数组时使用的type类型相同,或者是定义数组时使用的type类型的子类。

程序示例 动态初始化

下面代码示范了如何进行动态初始化:

1
2
3
4
// 数组的定义和初始化同时完成,使用动态初始化语法
int[] prices = new int[5];
// 数组的定义和初始化同时完成,初始化数组时元素的类型是定义数组时元素类型的子类
Object[] books = new String[4];

动态初始化只需要指定数组长度

执行动态初始化时,程序员只需指定数组的长度,即为每个数组元素指定所需的内存空间,系统将负责为这些数组元素分配初始值。

数组动态初始化 默认值

指定初始值时,系统按如下规则分配初始值:

  • 数组元素的类型是基本类型中的整数类型(byteshortintlong),则数组元素的值是0。
  • 数组元素的类型是基本类型中的浮点类型(foatdouble),则数组元素的值是0.0
  • 数组元素的类型是基本类型中的字符类型(char),则数组元素的值是\u00000
  • 数组元素的类型是基本类型中的布尔类型(boolean),则数组元素的值是false
  • 数组元素的类型是引用类型(类、接口和数组),则数组元素的值是null

静态初始化和动态初始化无法同时使用

不要同时使用静态初始化和动态初始化,也就是说,不要在进行数组初始化时,既指定数组的长度,也为每个数组元素分配初始值
数组初始化完成后,就可以使用数组了,包括为数组元素赋值、访问数组元素值和获得数组长度等。

4.5.2 定义数组

Java语言支持两种语法格式来定义数组:

1
2
type[] arrayName;
type arrayName[];

对这两种语法格式而言,通常推荐使用第一种格式。因为第一种格式不仅具有更好的语意,而且具有更好的可读性。
对于type[] ArrayName;方式,很容易理解这是定义一个变量,其中变量名是arrayName而变量类型是type[]
前面已经指出:type[]确实是一种新类型,与type类型完全不同(例如int类型是基本类型,但int[]是引用类型)。
因此,这种方式既容易理解,也符合定义变量的语法。
但第二种格式type ArrayName[]的可读性就差了,看起来好像定义了一个类型为type的变量,而变量名是arrayName这与真实的含义相去甚远.

类似语言Java的C#就不再支持type arrayName[]这种语法,它只支持第一种定义数组的语法。越来越多的语言不再支持type arrayName[]这种数组定义语法。

定义数组时不能指定长度

数组是一种引用类型的变量,因此使用它定义一个变量时,仅仅表示定义了一个引用变量(也就是定义了一个指针),这个引用变量还未指向任何有效的内存,因此定义数组时不能指定数组的长度。而且由于定义数组只是定义了一个引用变量,并未指向任何有效的内存空间,所以还没有内存空间来存储数组元素,因此这个数组也不能使用,只有对数组进行初始化后才可以使用。

4.5 数组类型

数组是编程语言中最常见的一种数据结构,可用于存储多个数据,每个数组元素存放一个数据,通常可通过数组元素的索引来访问数组元素,包括为数组元素赋值和取出数组元素的值。Java语言的数组则具有其特有的特征,下面将详细介绍Java语言的数组。

4.5.1 理解数组:数组也是一种类型

数组中只能存储一种数据类型的数据

Java的数组要求所有的数组元素具有相同的数据类型。因此,在一个数组中,数组元素的类型是唯一的,即一个数组里只能存储一种数据类型的数据,而不能存储多种数据类型的数据

因为Java语言是面向对象的语言,而类与类之间可以支持继承关系,这样可能产生一个数组里可以存放多种数据类型的假象。例如有一个水果数组,要求每个数组元素都是水果,实际上数组元素既可以是苹果,也可以是香蕉(苹果、香蕉都继承了水果,都是一种特殊的水果),但这个数组的数组元素的类型还是唯一的,只能是水果类型。
一旦数组的初始化完成,数组在内存中所占的空间将被固定下来,因此数组的长度将不可改变。即使把某个数组元素的数据清空,但它所占的空间依然被保留,依然属于该数组,数组的长度依然不变。

Java的数组既可以存储基本类型的数据,也可以存储引用类型的数据,只要所有的数组元素具有相同的类型即可.

数组是引用类型

值得指出的是,数组也是一种数据类型,它本身是一种引用类型。例如int是一个基本类型,但int[]就是一种引用类型了。

4.4 控制循环结构

Java语言没有提供goto语句来控制程序的跳转,这种做法提高了程序流程控制的可读性,但降低了程序流程控制的灵活性。为了弥补这种不足,Jaⅵa提供了continuebreak来控制循环结构。除此之外,retur可以结束整个方法,当然也就结束了一次循环。

4.4.1 使用break结束循环

某些时候需要在某种条件岀现时强行终止循环,而不是等到循环条件为false时才退出循环。此时,可以使用break来完成这个功能。**break用于完全结束一个循环,跳出循环体。不管是哪种循环,一旦在循环体中遇到break,系统将完全结束该循环,开始执行循环之后的代码**。
例如如下程序

1
2
3
4
5
6
7
8
9
10
11
12
public class BreakTest {
public static void main(String[] args) {
// 一个简单的for循环
for (int i = 0; i < 10; i++) {
System.out.println("i的值是" + i);
if (i == 2) {
// 执行该语句时将结束循环
break;
}
}
}
}

运行上面程序,当i等于2时,循环体内遇到break语句,程序跳出该循环。

break外层循环

break语句不仅可以结束其所在的循环,还可以直接结束其外层循环。此时需要在break后紧跟个标签,这个标签用于标识一个外层循环。

标签

Java中的标签就是一个紧跟着英文冒号(:)的标识符:

1
标签:

与其他语言不同的是,Java中的标签只有放在循环语句之前才有作用
例如下面代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class BreakTest2 {
public static void main(String[] args) {
// 外层循环,outer作为标识符
outer:
for (int i = 0; i < 5; i++) {
// 内层循环
for (int j = 0; j < 3; j++) {
System.out.println("i的值为:" + i + " j的值为:" + j);
if (j == 1) {
// 跳出outer标签所标识的循环。
break outer;
}
}
}
}
}

运行上面程序,看到如下运行结果:

1
2
i的值为:0  j的值为:0
i的值为:0 j的值为:1

程序从外层循环进入内层循环后,当j等于1时,程序遇到一个break outer;语句,这行代码将会结束outer标签指定的循环,也就是结束外层循环。所以看到上面的运行结果。

标签应该放在break所在的循环的外层循环定义

值得指出的是,break后的标签必须是一个有效的标签,即这个标签必须在break语句所在的循环之前定义,或者在其所在循环的外层循环之前定义。
如果把这个标签放在break语句所在的循环之前定义,也就失去了标签的意义,因为break默认就是结束其所在的循环。

紧跟break之后的标签,必须在break所在循环的外层循环之前定义才有意义

4.4.2 使用continue忽略本次循环剩下语句

continue的功能和break有点类似,区别是**continue只是忽略本次循环剩下语句,接着开始下一次循环,并不会终止循环**;而break则是完全终止循环本身。

如下程序示范了continue的用法。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class ContinueTest {
public static void main(String[] args) {
// 一个简单的for循环
for (int i = 0; i < 3; i++) {
System.out.println("i的值是" + i);
if (i == 1) {
// 忽略本次循环的剩下语句
continue;
}
System.out.println("continue后的输出语句");
}
}
}

运行上面程序,看到如下运行结果:

1
2
3
4
5
i的值是0
continue后的输出语句
i的值是1
i的值是2
continue后的输出语句

从上面运行结果来看,当i等于1时,程序没有输出“continue后的输出语句”字符串,因为程序执行到continue时,忽略了当次循环中continue语句后的代码。
从这个意义上来看,如果把一个continue语句放在单次循环的最后一行,这个continue语句是没有任何意义的,因为它仅仅忽略了一片空白,没有忽略任何程序语句。

continue后接标签

break类似的是,continue后也可以紧跟一个标签,用于直接跳过标签所标识循环的当次循环的剩下语句,重新开始下一次循环。
例如下面代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class ContinueTest2 {
public static void main(String[] args) {
// 外层循环
outer:
for (int i = 0; i < 5; i++) {
// 内层循环
for (int j = 0; j < 3; j++) {
System.out.println("i的值为:" + i + " j的值为:" + j);
if (j == 1) {
// 忽略outer标签所指定的循环中本次循环所剩下语句。
continue outer;
}
}
}
}
}

运行上面程序可以看到,循环变量j的值将无法超过1,因为每当j等于1时,continue outer;语句就结束了外层循环的当次循环,直接开始下一次循环,内层循环没有机会执行完成。
break类似的是,continue后的标签也必须是一个有效标签,即这个标签通常应该放在continue所在循环的外层循环之前定义。

4.4.3 使用return结束方法

return关键字并不是专门用于结束循环的,return的功能是结束一个方法。当一个方法执行到一个return语句时,这个方法将被结束。
Java程序中大部分循环都被放在方法中执行,例如前面介绍的所有循环示范程序。一旦在循环体内执行到一个return语句,return语句就会结束该方法,循环自然也随之结束。例如下面程序。

1
2
3
4
5
6
7
8
9
10
11
12
public class ReturnTest {
public static void main(String[] args) {
// 一个简单的for循环
for (int i = 0; i < 3; i++) {
System.out.println("i的值是" + i);
if (i == 1) {
return;
}
System.out.println("return后的输出语句");
}
}
}

运行上面程序,循环只能执行到i等于1时,当i等于1时程序将完全结束(当main方法结束时,也就是Java程序结束时)。从这个运行结果来看,虽然return并不是专门用于循环结构控制的关键字,但通过return语句确实可以结束一个循环。与continuebreak不同的是,return直接结束整个方法,不管这个return处于多少层循环之内。

4.3.4 嵌套循环

如果把一个循环放在另一个循环体内,那么就可以形成嵌套循环,嵌套循环既可以是for循环嵌套while循环,也可以是while循环嵌套do while循环…,即各种类型的循环都可以作为外层循环,也可以作为内层循环
当程序遇到嵌套循环时,如果外层循环的循环条件允许,则开始执行外层循环的循环体,而内层循环将被当做外层循环的循环体来执行。当内层循环执行结束,且外层循环的循环体执行结束时,则再次计算外层循环的循环条件,决定是否再次开始执行外层循环的循环体

总循环次数

根据上面分析,**假设外层循环的循环次数为n次,内层循环的循环次数为m次,那么内层循环的循环体实际上需要执行n乘以m次**。
嵌套循环的执行流程如图4.1所示。
这里有一张图片

内层循环当成外层循环的循环体

**嵌套循环就是把内层循环当成外层循环的循环体**。当只有内层循环的循环条件为false时,才会完全跳出内层循环,才可以结束外层循环的当次循环,开始下一次循环。

程序示例 嵌套循环

下面是一个嵌套循环的示例代码

1
2
3
4
5
6
7
8
9
10
11
public class NestedLoopTest {
public static void main(String[] args) {
// 外层循环
for (int i = 0; i < 5; i++) {
// 内层循环
for (int j = 0; j < 3; j++) {
System.out.println("i的值为:" + i + " j的值为:" + j);
}
}
}
}

运行上面程序,看到如下运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
i的值为:0  j的值为:0
i的值为:0 j的值为:1
i的值为:0 j的值为:2
i的值为:1 j的值为:0
i的值为:1 j的值为:1
i的值为:1 j的值为:2
i的值为:2 j的值为:0
i的值为:2 j的值为:1
i的值为:2 j的值为:2
i的值为:3 j的值为:0
i的值为:3 j的值为:1
i的值为:3 j的值为:2
i的值为:4 j的值为:0
i的值为:4 j的值为:1
i的值为:4 j的值为:2

从上面运行结果可以看出,进入嵌套循环时,循环变量i开始为0,这时即进入了外层循环。进入外层循环后,内层循环把i当成一个普通变量,其值为0。在外层循环的当次循环里,内层循环就是一个普通循环。
实际上,嵌套循环不仅可以是两层嵌套,而且可以是三层嵌套、四层嵌套…不论循环如何嵌套,总可以把内层循环当成外层循环的循环体来对待,区别只是这个循环体里包含了需要反复执行的代码。

4.3.3 for循环

for循环是更加简洁的循环语句,大部分情况下,for循环可以代替while循环、do while循环

for循环的语法格式

for循环的基本语法格式如下:

1
2
3
for ([初始化语句]; [循环条件]; [迭代语句]){
循环体
}

for循环执行流程

程序执行for循环时,先执行循环的初始化语句,初始化语句只在循环开始前执行次。每次执行循环体之前,先计算循环条件的值,如果循环条件返回true,则执行循环体,循环体执行结束后,执行循环迭代语句

因此,对于for循环而言,循环条件总比循环体要多执行一次,因为最后一次执行循环条件返回false,将不再执行循环体。

for循环的continue无法结束 循环迭代语句

值得指出的是,for循环的循环迭代语句并没有与循环体放在一起,因此即使在执行循环体时遇到continue语句结束本次循环,循环迭代语句依旧会执行

while循环do while循环的continue 会结束 循环迭代语句

for循环和whiledo while循环不一样:由于whiledo while循环的循环迭代语句紧跟着循环体,因此如果循环体不能完全执行,如使用continue语句来结束本次循环,则循环迭代语句不会被执行。

for循环的循环迭代语句并没有与循环体放在一起,因此不管是否使用continue语句来结束本次循环,for选好的循环迭代语句一样会获得执行
与前面循环类似的是,如果循环体只有一行语句,那么循环体的花括号可以省略。

程序示例

下面使用for循环代替前面的while循环,代码如下。

1
2
3
4
5
6
7
8
9
public class ForTest {
public static void main(String[] args) {
// 循环的初始化条件,循环条件,循环迭代语句都在下面一行
for (int count = 0; count < 10; count++) {
System.out.println(count);
}
System.out.println("循环结束!");
}
}

for循环可以多个初始化语句 使用复杂的循环条件

在上面的循环语句中,for循环的初始化语句只有一个,循环条件也只是一个简单的boolean表达式。实际上,for循环允许同时指定多个初始化语句,循环条件也可以是一个包含逻辑运算符的表达式。例如如下程序:

1
2
3
4
5
6
7
8
9
public class ForTest2 {
public static void main(String[] args) {
// 同时定义了三个初始化变量,使用&&来组合多个boolean表达式
for (int b = 0, s = 0, p = 0; b < 10 && s < 4 && p < 10; p++) {
System.out.println(b++);
System.out.println(++s + p);
}
}
}

上面代码中初始化变量有三个,但是只能有一个声明语句,因此如果需要在初始化表达式中声明多个变量,那么这些变量应该具有相同的数据类型

建议不要在循环体内修改循环变量

初学者使用for循环时也容易犯一个错误,他们以为只要在for后的圆括号内控制了循环迭代语句就万无一失,但实际情况则不是这样的。例如下面的程序

1
2
3
4
5
6
7
8
9
10
11
public class ForErrorTest {
public static void main(String[] args) {
// 循环的初始化条件,循环条件,循环迭代语句都在下面一行
for (int count = 0; count < 10; count++) {
System.out.println(count);
// 再次修改了循环变量
count *= 0.1;
}
System.out.println("循环结束!");
}
}

在上面的for循环中,表面上看起来控制了count变量的自加,表达式count<10有变成false的时候。但实际上程序中在循环体内修改了count变量的值,并且把这个变量的值乘以了0.1。这会导致count的值永远都不能超过10,因此上面程序也是一个死循环。

建议不要在循环体内修改循环变量(也叫循环计数器)的值,否则会増加程序出错的可能性。万一程序真的需要访问、修改循环变量的值,建议重新定义一个临时变量,先将循环变量的值赋给临时变量,然后对临时变量的值进行修改。

for循环的初始化语句 循环条件 迭代语句都可以省略

for循环的圆括号中只有两个分号是必需的,初始化语句、循环条件、迭代语句部分都是可以省略的
如果省略了for循环的循环条件,则这个循环条件默认为true,将会产生一个死循环。例如下面程序。

1
2
3
4
5
6
7
8
public class DeadForTest {
public static void main(String[] args) {
// 省略了for循环三个部分,循环条件将一直为true
for (;;) {
System.out.println("xxxxx");
}
}
}

while循环转for循环

使用for循环时,还可以把初始化条件定义在循环体之外,把循环迭代语句放在循环体内,这种做法就非常类似于前面的while循环了。下面的程序再次使用for循环来代替前面的whie循环

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class ForInsteadWhile {
public static void main(String[] args) {
// 把for循环的初始化条件提出来独立定义
int count = 0;
// for循环里只放循环条件
for (; count < 10;) {
System.out.println(count);
// 把循环迭代部分放在循环体之后定义
count++;
}
System.out.println("循环结束!");
// 此处将还可以访问count变量
}
}

上面程序的执行过程和前面的WhileTest.java程序的执行过程完全相同。因为把for循环的循环迭代部分放在循环体之后,则会岀现与while循环类似的情形,如果循环体部分使用continue语句来结束本次循环,将会导致循环迭代语句得不到执行

如何在for循环外访问循环变量的值

扩大循环变量的作用域

for循环的初始化语句放在循环之前定义还有一个作用:可以扩大初始化语句中所定义变量的作用域。在for循环里定义的变量,其作用域仅在该循环内有效,for循环终止以后,这些变量将不可被访问。如果需要在for循环以外的地方使用这些变量的值,就可以采用上面的做法。

定义额外的变量来保存循环变量的值

除此之外,还有种做法也可以满足这种要求:额外定义一个变量来保存这个循环变量的值。例如下面代码片段:

1
2
3
4
5
6
7
8
9
int tmp =0;
//′循环的初始化条件、循环条件、循环迭代语句都在下面一行
for (int i =0 ;i < 10 ; 1++){
System.out.println(i);
tmp =i;
}
//使用tmp来保存循环变量i的值
System.out.println("循环结束!");
//此处还可通过tmp变量来访问i变量的值

相比前面的代码,通常更愿意选择这种解决方案。使用一个变量tmp来保存循环变量i的值,使得程序更加清晰,变量i和变量tmp的责任更加清晰。反之,如果采用前一种方法,则变量i的作用域被扩大了,功能也被扩大了。
作用域扩大的后果是:如果该方法还有另一个循环也需要定义循环变量,则不能再次使用i作为循环变量

建议使用i,j,k作为循环变量

选择循环变量时,习惯选择i、j、k来作为循环变量。

4.3 循环结构

循环语句可以在满足循环条件的情况下,反复执行某一段代码,这段被重复执行的代码被称为循环体。当反复执行这个循环体时,需要在合适的时候把循环条件改为假,从而结束循环,否则循环将一直执行下去,形成死循环。循环语句可能包含如下4个部分:

  • 初始化语句(init Statement):一条或多条语句,这些语句用于完成一些初始化工作,初始化语句在循环开始之前执行
  • 循环条件(test Expression):这是一个boolean表达式,这个表达式能决定是否执行循环体。
  • 循环体(body Statement):这个部分是循环的主体,如果循环条件允许,这个代码块将被重复执行。如果这个代码块只有一行语句,则这个代码块的花括号是可以省略的。
  • 迭代语句(iteration Statement):这个部分在一次循环体执行结束后,对循环条件求值之前执行,通常用于控制循环条件中的变量,使得循环在合适的时候结束。

上面4个部分只是一般性的分类,并不是每个循环中都非常清晰地分出了这4个部分。

4.3.1 While循环语句

while循环的语法格式

1
2
3
4
5
[初始化语句]
while(循环条件){
循环体
[迭代语句]
}

while循环每次执行循环体之前,先对循环条件求值,如果循环条件true,则运行循环体部分。从上面的语法格式来看,迭代语句总是位于循环体的最后,因此只有当循环体能成功执行完成时,while循环才会执行迭代语句.
从这个意义上来看,while循环也可被当成条件语句:如果循环条件一开始就为false,则循环体部分将永远不会获得执行。

程序示例 简单的while循环

下面程序示范了一个简单的while循环。

1
2
3
4
5
6
7
8
9
10
// 循环的初始化条件
int count = 0;
// 当count小于10时,执行循环体
while (count < 10)
{
System.out.println(count);
// 迭代语句
count++;
}
System.out.println("循环结束!");

如果while循环的循环体部分和迭代语句合并在一起,且只有一行代码,则可以省略while循环后的花括号。但这种省略花括号的做法,可能降低程序的可读性
如果省略了循环体的花括号,那么while循环条件仅控制到紧跟该循环条件的第一个分号处。

死循环

使用while循环时,一定要保证循环条件有变成false的时候,否则这个循环将成为一个死循环,永远无法结束这个循环。
例如如下代码:

1
2
3
4
5
6
7
8
// 下面是一个死循环
int count = 0;
while (count < 10)
{
System.out.println("不停执行的死循环 " + count);
count--;
}
System.out.println("永远无法跳出的循环体");

在上面代码中,count的值越来越小,这将导致count值永远小于10,使得循环条件count<10一直为true,从而导致这个循环永远无法结束。

循环条件后面不要加分号

除此之外,对于许多初学者而言,使用while循环时还有一个陷阱:while循环的循环条件后紧跟个分号。比如有如下程序片段:

1
2
3
4
5
6
7
8
int count = 0;
// while后紧跟一个分号,表明循环体是一个分号(空语句)
while (count < 10);
// 下面的代码块与while循环已经没有任何关系
{
System.out.println("------" + count);
count++;
}

乍一看,这段代码片段没有任何问题,但仔细看一下这个程序,不难发现while循环的循环条件表达式后紧跟了一个分号。在Java程序中,一个单独的分号表示一个空语句,不做任何事情的空语句,这意味着这个while循环的循环体是空语句。空语句作为循环体也不是最大的问题,问题是当Java反复执行这个循环体时,循环条件的返回值没有任何改变,这就成了一个死循环。分号后面的代码块则与while循环没有任何关系。

4.3.2 do while循环语句

do while循环和while循环的区别

do while循环与while循环的区别在于:
while循环是先判断循环条件,如果条件为真则执行循环体;
do while循环则先执行循环体,然后才判断循环条件,如果循环条件为真,则执行下一次循环,否则中止循环。

do while循环的语法格式

do while循环的语法格式如下:

1
2
3
4
5
[初始化语句]
do{
循环体
[迭代语句]
}while (循环条件);

while循环不同的是,do while循环的循环条件后必须有一个分号,这个分号表明循环结束

程序示例 do while循环

下面程序示范了do while循环的用法:

1
2
3
4
5
6
7
8
9
10
// 定义变量count
int count = 1;
// 执行do while循环
do {
System.out.println(count);
// 循环迭代语句
count++;
// 循环条件紧跟while关键字
} while (count < 10);
System.out.println("循环结束!");

do while循环至少执行一次

即使循环条件的值开始就是假,do while循环也会执行循环体。因此,do while循环的循环体至少执行一次。下面的代码片段验证了这个结论:

1
2
3
4
5
6
7
8
// 定义变量count2
int count2 = 20;
// 执行do while循环
do
// 这行代码把循环体和迭代部分合并成了一行代码
System.out.println(count2++);
while (count2 < 10);
System.out.println("循环结束!");

从上面程序来看,虽然开始count2的值就是20,count2<10表达式返回false,但do while循环还是会把循环体执行一次。

4.2.2 Java7增强后的switch分支语句

switch语句控制表达式的数据类型

switch语句由一个控制表达式和多个case标签组成,和if语句不同的是,**switch语句后面的控制表达式的数据类型只能是

  • byteshortcharint四种整数类型,
  • 枚举类型
  • java.lang.String类型(从Java7才允许),不能是boolean类型.

switch语法格式

switch语句往往需要在case标签后紧跟一个代码块,case标签作为这个代码块的标识。switch语句的语法格式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
switch (expression){
case condition1:{
statement(s);
break;
}
case condition2:{
statement(s);
break;
}
case conditionN:{
statement(s);
break;
}
default:{
statement(s);
}
}

这种分支语句的执行是先对expression求值,然后依次匹配condition1condition2、…、conditionN等值,遇到匹配的值即执行对应的执行体;如果所有case标签后的值都不与expression表达式的值相等,则执行default标签后的代码块。

case后面的花括号可以省略

if语句不同的是,switch语句中各case标签后代码块的开始点和结束点非常清晰,因此完全可以省略case后代码块的花括号。与if语句中的else类似,switch语句中的default标签看似没有条件,其实是有条件的,条件就是expression表达式的值不能与前面任何一个case标签后的值相等。

程序示例

下面程序示范了switch语句的用法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class SwitchTest {
public static void main(String[] args) {
// 声明变量score,并为其赋值为'C'
char score = 'C';
// 执行swicth分支语句
switch (score) {
case 'A':
System.out.println("优秀");
break;
case 'B':
System.out.println("良好");
break;
case 'C':
System.out.println("中");
break;
case 'D':
System.out.println("及格");
break;
case 'F':
System.out.println("不及格");
break;
default:
System.out.println("成绩输入错误");
}
}
}

运行上面程序,看到输出“中”,这个结果完全正常,字符表达式score的值为C”,对应结果为“中”

case穿透

case标签后的每个代码块后都有一条break;语句,这个break;语句有极其重要的意义,Javaswitch语句允许case后代码块没有break;语句,但这种做法可能引入一个陷阱。如果把上面程序中的break;语句都注释掉,将看到如下运行结果:

1
2
3
4

及格
不及格
成绩输入错误

switch执行流程

这个运行结果看起来比较奇怪,但这正是由switch语句的运行流程决定的:switch语句会先求出expression表达式的值,然后拿这个表达式和case标签后的值进行比较,一旦遇到相等的值,程序就开始执行这个case标签后的代码,不再判断与后面casedefault标签的条件是否匹配,除非遇到break;才会结束

Java7后switch的控制表达式可以是String

Java 7增强了switch语句的功能,允许switch语句的控制表达式是java.lang.String类型的变量或表达式.但只能是java.lang.String类型,不能是StringBufferStringBuilder这两种字符串类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class StringSwitchTest {
public static void main(String[] args) {
// 声明变量season
String season = "夏天";
// 执行swicth分支语句
switch (season) {
case "春天":
System.out.println("春暖花开.");
break;
case "夏天":
System.out.println("夏日炎炎.");
break;
case "秋天":
System.out.println("秋高气爽.");
break;
case "冬天":
System.out.println("冬雪皑皑.");
break;
default:
System.out.println("季节输入错误");
}
}
}

总结

使用switch语句时,有两个值得注意的地方:

  • 第一个地方是switch语句后的expression表达式的数据类型只能是byteshortcharint四种整数类型,枚举类型,以及Java7后可以是String类型;
  • 第二个地方是如果省略了case后代码块的break;时会发生case穿透。

4.2 分支结构

Java提供了两种常见的分支控制结构:if语句和switch语句,其中

  • if语句使用**布尔表达式布尔值**作为分支条件来进行分支控制;
  • switch语句则用于对多个整型值进行匹配,从而实现分支控制。

4.2.1 if条件语句

if语句的三种形式

if语句使用布尔表达式或布尔值作为分支条件来进行分支控制。if语句有如下三种形式。
第一种形式:

1
2
3
4
if(logic expression)
{
statement
}

第二种形式:

1
2
3
4
5
if (logic expression){
statement
} else {
statement
}

第三种形式:

1
2
3
4
5
6
7
8
9
10
11
12
if (logic expression){
statement
}
else if(logic expression){
statement
}
//可以有零个或多个else if语句
...
//最后的else语句也可以省略
else{
statement
}

在上面if语句的三种形式中,放在if之后括号里的只能是一个逻辑表达式,即这个表达式的返回值只能是truefalse。第二种形式和第三种形式是相通的,如果第三种形式中else if块不出现,就变成了第二种形式。

条件执行体

在上面的条件语句中,if(logic expression)else if(logic expression)else花括号括起来的多行代码被称为代码块,一个代码块通常被当成一个整体来执行(除非运行过程中遇到returnbreakcontinue等关键字,或者遇到了异常),因此这个代码块也被称为条件执行体。例如如下程序。

1
2
3
4
5
6
7
8
int age = 30;
if (age > 20)
// 只有当age > 20时,下面花括号括起来的语句块才会执行
// 花括号括起来的语句是一个整体,要么一起执行,要么一起不会执行
{
System.out.println("年龄已经大于20岁了");
System.out.println("20岁以上的人应该学会承担责任...");
}

条件执行体只有一条语句的话可省略花括号

如果if(logic expression)else if(logic expression)else后的代码块只有一行语句时,则可以省略花括号,因为单行语句本身就是一个整体,无须用花括号来把它们定义成一个整体。下面代码完全可以正常执行:

1
2
3
4
5
6
7
8
9
// 定义变量a ,并为其赋值
int a = 5;
if (a > 4)
// 如果a>4,执行下面的执行体,只有一行代码作为代码块
System.out.println("a大于4");
else
// 否则,执行下面的执行体,只有一行代码作为代码块
System.out.println("a不大于4");

建议不要省略条件执行体的花括号

通常建议不要省略ifelseelse if后执行体的花括号,即使条件执行体只有一行代码,也保留花括号会有更好的可读性,而且保留花括号会减少发生错误的可能。例如如下代码,则不能正常执行:

1
2
3
4
5
6
7
8
9
10
11
// 定义变量b ,并为其赋值
int b = 5;
if (b > 4)
// 如果b>4,执行下面的执行体,只有一行代码作为代码块
System.out.println("b大于4");
else
// 否则,执行下面的执行体,只有一行代码作为代码块
b--;
// 对于下面代码而言,它已经不再是条件执行体的一部分,因此总会执行。
System.out.println("b不大于4");

上面的

1
System.out.println("b不大于4");

这行代码总会执行,因为这行代码不属于else后的条件执行体,else后的条件执行体就是b--;这行代码。
ifelseelse if后的条件执行体要么是一个花括号括起来的代码块,则这个代码块整体作为条件执行体;要么是以分号为结束符的一行语句,甚至可能是一个空语句(空语句是一个分号),那么就只是这条语句作为条件执行体。**如果省略了if条件后条件执行体的花括号,那么if条件只控制到紧跟该条件语句的第一个分号处**。
如果if后有多条语句作为条件执行体,若省略了这个条件执行体的花括号,则会引起编译错误看下面代码

1
2
3
4
5
6
7
8
9
10
11
12
// 定义变量c ,并为其赋值
int c = 5;
if (c > 4)
// 如果b>4,执行下面的执行体,将只有c--;一行代码为条件体
c--;
// 下面是一行普通代码,不属于条件体
System.out.println("c大于4");
// 此处的else将没有if语句,因此编译出错。
else
// 否则,执行下面的执行体,只有一行代码作为代码块
System.out.println("c不大于4");

在上面代码中,因为if后的条件执行体省略了花括号,则系统只把c-;行代码作为条件执行体当c-;语句结束后,if语句也就结束了。后面的:

1
System.out.println("c大于4");

这行代码已经是一行普通代码了,不再属于条件执行体,从而导致**else语句没有匹配的if语句**,从而引起编译错误。

优先把包含范围小的条件放在前面处理

对于if语句,还有一个很容易出现的逻辑错误,这个逻辑错误并不属于语法问题,但引起错误的可能性更大。看下面程序。

1
2
3
4
5
6
7
8
9
10
11
12
public class IfErrorTest {
public static void main(String[] args) {
int age = 45;
if (age > 20) {
System.out.println("青年人");
} else if (age > 40) {
System.out.println("中年人");
} else if (age > 60) {
System.out.println("老年人");
}
}
}

表面上看起来,上面的程序没有任何问题:人的年龄大于20岁是青年人,年龄大于40岁是中年人,年龄大于60岁是老年人。但运行上面程序,发现打印结果是:青年人,而正确的情况是:45岁是中年人;

else的隐含条件是对前面条件取反

对于任何的if else语句,表面上看起来else后没有任何条件,或者else if后只有一个条件,但这不是真相:
因为else的含义是“否则”,所以else本身就是一个条件!else的隐含条件是对前面条件取反。因此,上面代码实际上可改写为如下形式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class IfErrorTest2 {
public static void main(String[] args) {
int age = 45;
if (age > 20) {
System.out.println("青年人");
}
// 在原本的if条件中增加了else的隐含条件
if (age > 40 && !(age > 20)) {
System.out.println("中年人");
}
// 在原本的if条件中增加了else的隐含条件
if (age > 60 && !(age > 20) && !(age > 40 && !(age > 20))) {
System.out.println("老年人");
}
}
}

省略…

正确的写法如下:

1
2
3
4
5
6
7
8
9
10
11
12
public class IfCorrectTest {
public static void main(String[] args) {
int age = 45;
if (age > 60) {
System.out.println("老年人");
} else if (age > 40) {
System.out.println("中年人");
} else if (age > 20) {
System.out.println("青年人");
}
}
}

如果每次都去计算if条件和else条件的交集也是一件非常烦琐的事情,为了避免出现上面的错误,在使用if...else语句时有一条基本规则:总是优先把包含范围小的条件放在前面处理,例如:age>60age>20两个条件,明显age>60的范围更小,所以应该先处理age>60的情况。
注意:使用if...else语句时,一定要先处理包含范围更小的情况