本章要点
- 顺序结构
- if分支语句
- switch语句
- while循环
- do while循环
- for循环
- 嵌套循环
- 控制循环结构
- 理解数组
- 数组的定义和初始化
- 使用数组元素
- 数组作为引用类型的运行机制
- 多维数组的实质
- 操作数组的工具类
- 数组的实际应用场景
不论哪一种编程语言,都会提供两种基本的流程控制结构:分支结构和循环结构。其中分支结构用于实现根据条件来选择性地执行某段代码,循环结构则用于根据循环条件重复执行某段代码。java同样提供了这两种流程控制结构的语法,java提供了if和switch两种分支语句,并提供了while,do while和for三种循环语句,除此之外,JDK1.5还提供了一种新的循环:foreach循环,能以更简单的方式来遍历集合,数组的元素。除此之外,java还提供了break和continue来控制程序的循环结构。
数组也是大部分编程语言都支持的数据结构,java也不例外。java的数组类型是一种引用类型的变量,java程序通过数组引用变量来操作数组,包括获得数组的长度,访问数组元素的值等。
4.1 顺序结构
任何编程语言中最常见的程序结构就是顺序结构。顺序结构就是程序从上到下一行一行地执行,中间没有任何判断和跳转。
如果main方法多行代码之间没有任何流程控制,则程序总是从上向下依次执行,排在前面的代码先执行,排在后面的代码后执行。这意味着:如果没有流程控制,java方法里的语句是一个顺序执行流,从上向下依次执行每条语句。
4.2 分支结构
java提供了两种常见的分支控制结构:if语句和switch语句,其中if语句使用布尔表达式或布尔值作为分支条件来进行分支控制;而switch语句则用于对多个整型值进行匹配,从而实现分支控制。
4.2.1 if条件语句
if语句使用布尔表达式或布尔值作为分支条件来进行分支控制,其中if语句有如下三种形式:
if(){}
if(){}else{}
if(){}else if(){}...else{}
在上面if语言的三种形式中,放在if之后的括号里的只能是一个逻辑表达式,即这个表达式的返回值只能是true或false。第二种情形和第三种情形是想通的,如果第三种形式中else if块不出现,则变成了第二种形式。
上面的条件语句中,花括号括起来多行代码被称为代码块,一个代码块通常被当成一个整体来执行(除非运行过程中遇到return,break,continue等关键字,或者遇到了异常),因此这个代码块也被称为条件执行体。
如果if(),else if()和else后的语句块只有一行语句时,则可以省略花括号,因为单行语句本身就是一个整体,无须花括号来把它们定义成一个整体。
使用if else语句时,一定要先处理包括范围更小的情况。
4.2.2 switch分支语句
switch语句由一个控制表达式和多个case标签组成,和if语句不同的是,switch语句后面的控制表达式的数据类型只能是整型(long除外),不能是boolean型。case标签后紧跟一个代码块,case标签作为这个代码块的标识。
switch语句中控制表达式的类型只能是byte,short,char和int!不能是字符串。
在case标签后的每个代码块后都有一条break语句,这个break语句有极其重要的意义,java的switch语句允许省略case后代码块的break语句,但这个省略可能引入一个陷阱。
注意:使用switch语句时,有两个值得注意的地方:第一个地方是switch语句后的expression表达式的数据类型只能是byte,short,char和int类型;第二个地方是如果省略了case后代码块的break时所引入的陷阱。
4.3 循环结构
循环语句可以在满足循环条件的情况下,反复执行某一段代码,这段被重复执行的代码被称为循环体。当反复执行这段循环体时,需要在合适的时候把循环条件改为假,从而结束循环,否则循环将一直执行下去,形成死循环。循环语句可能包含如下四个部分:
- 初始化语句(init_statements):一条或多条语句,这些代码用于完成一些初始化工作,初始化语句在循环开始之前执行。
- 循环条件(test_expression):这是一个boolean表达式,这个表达式能决定是否执行循环体。
- 循环体(body_statements):这个部分是循环的主题,如果循环条件允许,这个代码块将被重复执行。如果这个代码块只有一行语句,则这个代码块的花括号是可以省略的。
- 迭代语句(iteration_statements):这个部分在一次循环体执行结束后,对循环条件求值之前执行,通常用于控制循环条件中的变量,使得循环在合适时候结束。
上面四个部分只是一般分类,并不是每个循环中都非常清晰地分出了上面四个成分。
4.3.1 while 循环语句
4.3.2 do while 循环语句
do while循环与while循环的区别在于:while循环是先判断循环条件,如果条件为真才执行循环体;而do while循环则先执行循环体,然后判断循环条件,如果循环条件为真,则执行下一次循环,否则中止循环。
4.3.3 for 循环
for循环是更加简洁的循环语句,大部分情况下,for循环可以代替while循环,do while循环。
注意:for循环和while,do while循环不一样:由于while,do while循环的循环迭代语句紧跟着循环体,因此如果循环体不能完全执行,如使用continue来结束本次循环,则循环迭代语句不会被执行。但for循环的循环迭代语句并没有与循环体放在一起,因此不管是否使用continue来结束本次循环,循环迭代语句一样会获得执行。
4.3.4 嵌套循环
如果把一个循环放在另一个循环体内,那么就可以形成嵌套循环,嵌套循环既可以是for循环嵌套while循环,也可以是while循环嵌套do while循环......即各种类型的循环都可以作为外层循环,各种类型的循环也可以作为内层循环。
4.4 控制循环结构
java语言没有提供goto语句来控制程序的跳转,这种做法提高了程序流程控制的可读性,但降低了程序流程控制的灵活性。为了弥补这种不足,java提供了continue和break来控制循环结构。除此之外,return可以结束整个方法,当然也就结束了一次循环。
4.4.1 使用break结束循环
break用于完全结束一个循环,跳出循环体。不管是哪种循环,一旦在循环体中遇到break,系统将完全结束该循环开始执行循环之后的代码。
break语句不仅可以结束其所在的循环,还可直接结束其外层循环。此时需要在break后紧跟一个标签,这个标签用于标识一个外层循环。
java中的标签就是一个紧跟着英文冒号(:)的标识符。与其他语言不同的是,java中的标签只有放在循环语句之前才有作用。
注意:通常紧跟break之后的标签,必须在break所在循环的外层循环之前定义才有意义。
4.4.2 使用continue结束本次循环
continue的功能和break有点类似,区别是continue只是中止本次循环,接着开始下一次循环。而break则是完全终止循环。可以理解为continue的作用是略过当次循环中剩下的语句,重新开始新的循环。
4.4.3 使用return结束方法
return关键字并不是专门用于跳出循环的,return的功能是结束一个方法。当一个方法执行到一个return语句时(return关键字后还可以跟变量,常量和表达式),这个方法将被结束。
java程序中大部分循环都被放在方法中执行。一旦在循环体内执行到一个return语句,return语句将会结束该方法,循环自然也随之结束。
4.5 数组类型
数组是编程语言中最常见的一种数据结构,它可用于存储多个数据,一个数据被称为数组元素,通常可通过数组元素的索引来访问数组元素,包括为数组元素赋值和取出数组元素的数据。
4.5.1 理解数组:数组也是一种类型
java的数组要求所有数组元素具有相同的数据类型。因此,在一个数组中,数组元素的类型是唯一的,即一个数组里只能存储一种数据类型的数据,而不能存储多种数据类型的数据。
注意:因为java语言是面向对象的语言,能很好地支持类与类之间的继承关系,这样可能产生一个数组里可以存放多种数据类型的假象。
一旦数组的初始化完成,数组在内存中所占的空间将被固定下来,因此数组的长度将不可改变。即使把某个数组元素的数据清空,但它所占的空间仍然被保留,依然属于该数组,数组的长度依然不变。
java的数组既可以存储基本类型的数据,也可以存储引用数据类型的数据。只要所有数组元素具有相同类型即可。
值得指出的是:数组也是一种数据类型,它本身是一种引用类型。例如int是一个基本类型,但int[]就是一种引用类型了。
4.5.2 定义数组
java语言支持两种语法格式来定义数组:
type[] arrayName;
type arrayName[];
定义数组时不能指定数组的长度。
4.5.3 数组的初始化
java语言中数组必须先初始化,然后才可以使用。所谓初始化,就是为数组的数组元素分配内存空间,并未每个数组元素赋初始值。
注意:一旦为数组的每个数组元素分配了内存空间,每个内存空间里存储的内容就是该数组元素的值,即使这个内存空间存储的内容是空,这个空也是一个值(null)。不管以哪种方式来初始化数组,只要为数组元素分配了内存空间,数组元素就具有了初始值,初始值的获得由两种形式:一种由系统自动分配,一种由程序员指定。
4.5.5 JDK1.5提供了foreach循环
注意:使用foreach循环迭代数组元素时,并不能改变数组元素的值,因此不要对foreach的循环变量进行赋值。
4.6 深入数组
数组是一种引用数据类型,数据引用变量只是一个引用,数组元素和数组变量在内存里是分开存放的。
4.6.1 内存中的数组
数组引用变量只是一个引用,这个引用变量可以指向任何有效的内存,只有当该引用指向有效内存后,才可通过该数组变量来访问数组元素。
与所有引用变量相同的是,引用变量时访问真实对象的根本方式。也就是说,如果我们希望在程序中访问数组,则只能通过这个数组的引用变量来访问它。
实际的数组元素被存储在堆(heap)内存中;数组引用变量时一个引用类型的变量,被存储在栈(stack)内存中。
为什么会有栈内存和堆内存之分?
当一个方法执行时,每个方法都会建立自己的内存栈,在这个方法内定义的变量将会逐个放入这块栈内存里,随着方法的执行结束,这个方法的内存栈也将自然销毁了。因此,所有在方法中定义的变量都是放在栈内存中的;当我们在程序中创建一个对象时,这个对象将被保存到运行时数据区中,以便反复利用(因为对象的创建成本通常较大),这个运行时数据区就是堆内存。堆内存中的对象不会随方法的结束而销毁,即使方法结束后,这个对象还可能被另一个引用变量所引用(方法的参数传递时很常见),则这个对象依然不会被销毁。只有当一个对象没有任何引用变量引用它时系统的垃圾回收机制才会在合适的时候回收它。
如果堆内存中数组不再有任何引用变量指向自己,则这个数组将成为垃圾,该数组所占的内存将会被系统的垃圾回收机制回收。因此,为了让垃圾回收机制回收一个数组所占的内存空间,则可以将该数组变量赋为null,也就切断了数组引用变量和实际数组之间的引用关系,实际数组也就成了垃圾。
只要类型相互兼容,可以让一个数组变量指向另一个实际的数组,这种操作会产生数组的长度可变的错觉。
程序员进行程序开发时,不要仅仅停留在代码表面,而要深入底层的运行机制,才可以对程序的运行机制有更准确的把握。当我们看一个数组时,一定要把数组看成两个部分,一个是数组引用,也就是在代码中定义的数组引用变量;还有一个是实际数组本身,这个部分是运行在系统内存里的,通常无法直接访问它,只能通过数组引用变量来访问它。
4.6.2 基本类型数组的初始化
对于基本类型数组而言,数组元素的值直接存储在对应的数组元素中,因此,初始化数组时,先为该数组分配内存空间,然后直接将数组元素的值存入对应数组元素中。
4.6.3 引用类型数组的初始化
引用类型数组的数组元素是引用,因此情况变得更加复杂;每个数组元素里存储的还是引用,它指向另一块内存,这块内存里存储了有效数据。
4.6.4 没有多维数组
java语言里提供了支持多维数组的语法。但笔者还是想说,没有多维数组--如果从数组底层的运行机制上来看。
java语言里的数组类型是引用类型,因此,数组变量其实是一个引用,这个引用指向真实的数组内存。数组元素的类型也可以是引用,如果数组元素的引用再次指向真实的数组内存,这种情形看上去很像多维数组。
通过上面讲解,我们可以得到一个结论:二维数组是一堆数组,其数组元素是一维数组;三维数组也是一堆数组,其数组元素是二维数组;思维数组还是一堆数组,其数组元素是三维数组......从这个角度来看,java语言里没有多维数组。
4.6.5 操作数组的工具类
java提供的Arrays类里包含了一些static修饰方法可以直接操作数组,这个arrays类里包含了如下几个static修饰的方法(static修饰的方法可以直接通过类名调用):
- int binarySearch(type[] a,type key):使用二分法查询key元素值在a数组中出现的索引;如果a数组不包含key元素值,则返回负数。调用该方法时要求数组中元素已经按升序排列,这样才能得到正确结果。
- int binarySearch(type[] a,int fromIndex,int toIndex,type key):这个方法与前一个方法类似,但它只搜索a数组中fromIndex到toIndex索引的元素。调用该方法时要求数组中元素已经按升序排列,这样才能得到正确结果。
- type[] copyOf(type[] original,int newLength):这个方法将会把original数组复制成一个新数组,其中length是新数组的长度。如果length小于original数组的长度,则新数组就是原数组的前面length个元素;如果length大于original数组的长度,则新数组的前面元素就是原数组的所有元素,后面补充0(数值型),false(布尔型)或者null(引用型)。
- type[] copyOfRange(type[] original,int fromint to):这个方法与前面方法相似,但这个方法只复制original数组的from索引到to索引的元素。
- boolean equals(type[] a,type[] a2):如果a数组和a2数组的长度相等,而且a数组和a2数组的数组元素也一一相同,该方法将返回true。
- void fill(type[] a,type val):该方法将会把a数组所有元素值都赋值为val。
- void fill(type[] a.nt fromIndex,int toIndex,type val):该方法与前一个方法的作用相同,区别只是该方法仅仅将a数组的fromIndex到toIndex索引的数组元素赋值为val。
- void sort(type[] a):该方法对a数组的数组元素进行排序。
- void sort(type[] a,int fromIndex,int toIndex):该方法与前一个方法相似,区别是该方法仅仅对fromIndex到toIndex索引的元素进行排序
- String toString(type[] a):该方法将一个数组转换成一个字符串。该方法按顺序把多个数组元素连缀在一起,多个数组元素使用英文逗号(,)和空格隔开。
4.6.6 数组的应用举例
数组的用途是很广泛的,如果程序中有多个类型相同的变量,而且他们具有逻辑的整体性,则可以把它们定义成一个数组。
例如,在实际开发中的一个常用工具函数:需要将一个浮点数转换成人民币读法字符串,这个程序就需要使用数组。当然这个程序还需要大量使用循环,分支的知识。
实现这个函数的思路是,先把这个浮点数分成整数部分和小数部分;提取整数部分很容易,直接将这个浮点数强制类型转换成一个整数即可,这个整数就是浮点数的整数部分;再使用浮点数减去整数将可以得到这个浮点数的小数部分。
然后分开处理整数部分和小数部分,其中小数部分的处理比较简单,直接截断到保留2位数字,转换成几角几分的字符串。整数部分的处理则稍微复杂一点,但只要我们认真分析不难发现,中国的数字习惯是4位一节的,一个4位的数字可被转成几千几百几十几,至于后面添加什么单位则不确定,如果这节4位数字出现在14位,则后面添加单位元,如果这节4位数字出现在58位,则后面添加单位万,如果这节4位数字出现在9~12位,则后面添加单位亿,多于12位就暂不考虑了。
4.7 本章小结
本章主要介绍了java的两种程序流程结构:分支结构和循环结构。