重拾Java基础知识:控制流操作

前言

Java 使用了 C 的所有执行控制语句,因此对于熟悉 C/C++ 编程的人来说,这部分内容轻车熟路。大多数面向过程编程语言都有共通的某种控制语句。在 Java 中,涉及的关键字包括 if-else,while,do-while,for,return,break 和选择语句 switch。 Java 并不支持备受诟病的 goto(尽管它在某些特殊场景中依然是最行之有效的方法)。 尽管如此,在 Java 中我们仍旧可以进行类似的逻辑跳转,但较之典型的 goto 用法限制更多。

if-else

if-else语句是指编程语言(包括c语言、C#、VB、java、汇编语言等)中用来判定所给定的条件是否满足,根据判定的结果(真或假)决定执行给出的两种操作之一。

单独if语句:单分支结构,简单的一个控制语句,只有满足条件才会进入代码块执行

if( 条件 ) {
  条件成立时执行的代码
}

if-else语句:双分支结构。当满足if条件为真时则执行相对应代码否则执行else语句里的代码。

if( 条件 ) {
  条件成立时执行的代码
} else {
  条件失败时执行的代码
}

if-else if语句:多分支结构。会依次判断if语句中的条件是否满足再执行相对应的代码,否则进入else执行代码。可以使用return来结束后面的代码

if ( 条件1 ) {
  代码块1
} else if ( 条件2 ) {
    //在条件 1 不满足的情况下,才会进行条件 2 的判断
  代码块2
} else if ( 条件3 ) {
  代码块3
} else {
    //当前面的条件均不成立时,才会执行 else 块内的代码。
  代码块N
}

if嵌套:当外层 if 的条件成立时,才会判断内层 if 的条件。

if(条件1){
  if(条件2){
    代码块1
  }else{
    代码块2
  }
}else{
  代码块3
}

布尔表达式(Boolean-expression)必须生成 boolean 类型的结果,执行语句 statement 既可以是以分号 ; 结尾的一条简单语句也可以是包含在大括号 {}内的的复合语句

if(true) 
    return “statement”; 

if(true) {
    return "statement";
}

switch

switch 有时也被划归为一种选择语句。根据整数表达式的值,switch 语句可以从一系列代码中选出一段去执行。它的格式如下:

switch ( 常量表达式 ){
    case 常量1 :语句;
    case 常量2 :语句;
        ...
    case 常量n:语句; break;
    default :语句;
}

switch 能够将这个表达式的结果与每个常量相比较。若发现相符的,就执行对应的语句(每个 case 均以一个 break 结尾,但 break 是可选的。若省略 break, 会继续执行后面的 case 语句的代码,直到遇到一个 break 为止。)若没有发现相符的,就执行 default 语句default 语句没有 break,因为执行流程已到了 break 的跳转目的地。即使加上也没有任何实际的作用)。

switch 语句选择因子,并且必须是 intchar 那样的整数值。例如,假若将一个字串或者浮点数作为选择因子使用,那么它们在 switch 语句里是不会工作的。对于非整数类型(Java 7 以上版本中的 String 型除外),则必须使用一系列 if 语句。

    public static void main(String[] args) {
        int[] a = {1, 2, 3, 4, 5};
        for (int i = 0; i < a.length; i++) {
            switch (a[i]) {
                case 1:
                    System.out.println("one");
                    break;
                case 2:
                case 3:
                case 4:
                    System.out.println("other");
                    break;
                default:
                    System.out.println("default");
            }
        }
    }

输出结果:

one
other
other
other
default

switch 字符串

Java 7 增加了在字符串上 switch 的用法(注意:防止字符串为空导致的异常)。 下例展示了从一组 String 中选择可能值的传统方法,以及新式方法:

    public static void main(String[] args) {
        String letter = "c";
            switch (letter) {
                case "a":
                    System.out.println("a");
                    break;
                case "b":
                case "c":
                case "d":
                    System.out.println("other");
                    break;
                default:
                    System.out.println("default");
        }
        /*Output:
		other
		*/
    }

一旦理解了 switch,你会明白这其实就是一个逻辑扩展的语法糖。新的编码方式能使得结果更清晰,更易于理解和维护。

迭代语句

whiledo-whilefor 用来控制循环语句(有时也称迭代语句)。只有控制循环的布尔表达式计算结果为 false,循环语句才会停止。

while

当满足条件时,重复执行语句块。

while(布尔表达式)
{
	语句块;
}

执行语句会在每一次循环前,判断布尔表达式返回值是否为 true。

    public static void main(String[] args) {
        int a = 0;
        while (a<5){
            System.out.println(a);
            a++;
        }
        /*Output:
        0
		1
		2
		3
		4
		*/
    }

do-while

先执行后判断,do-while 的格式如下:

do{
	语句块;
}while (布尔表达式);

whiledo-while 之间的唯一区别是:即使条件表达式返回结果为 falsedo-while 语句也至少会执行一次。 在 while 循环体中,如布尔表达式首次返回的结果就为 false,那么循环体内的语句不会被执行

    public static void main(String[] args) {
        int a = 5;
        do{
            System.out.println(a);
            a++;
        }while (a < 5);
        /*Output:
		5
		*/
    }

for

for 循环可能是最常用的迭代形式。 该循环在第一次迭代之前执行初始化。随后,它会执行布尔表达式,并在每次迭代结束时,for 循环语句都会在一个整型数值序列中步进for 循环的形式是:

for(定义初始化值;循环条件;初始化值++ 或--(步进)){
	语句块;
}

每次迭代之前都会判断布尔表达式的结果是否成立。一旦计算结果为 false,则跳出 for 循环体并继续执行后面代码。 每次循环结束时,都会执行一次步进。
数组是通过下标获取,所以遍历的时候只需要将 i 当作数组a的下标即可遍历出所有值。

    public static void main(String[] args) {
        int[] a = {1,2,3,4,5};
        for (int i = 0; i < a.length; i++) {
            System.out.println(a[i]);
        }
        /*Output:
		1
		2
		3
		4
		5
		*/
    }
逗号操作符

在 Java 中逗号运算符(这里并非指我们平常用于分隔定义和方法参数的逗号分隔符)仅有一种用法:在 for 循环的初始化和步进控制中定义多个变量。我们可以使用逗号分隔多个语句,并按顺序计算这些语句。注意:要求定义的变量类型相同。代码示例:

  public static void main(String[] args) {
    for(int i = 1, j = i + 10; i < 5; i++, j = i * 2) {
      System.out.println("i = " + i + " j = " + j);
    }
  }

输出结果:

i = 1 j = 11
i = 2 j = 4
i = 3 j = 6
i = 4 j = 8

无论在初始化还是在步进部分,语句都是顺序执行的。

for-in 语法

Java 5 引入了更为简洁的“增强版 for 循环”语法来操纵数组和集合。大部分文档也称其为 for-each 语法,但因为了不与 Java 8 新添的 forEach() 产生混淆,因此我称之为 for-in 循环。格式如下:

for(元素类型type 元素变量x : 遍历对象obj)
{
	代码块
}

for-in 无需你去创建 int 变量和步进来控制循环计数。 下面我们来遍历获取int 数组中的元素。代码示例:

    public static void main(String[] args) {
        int[] a = {1,2,3,4,5};
        for (int i:a) {
            System.out.println(i);
        }
		/*Output:
		1
		2
		3
		4
		5
		*/
    }

return

在 Java 中有几个关键字代表无条件分支,这意味无需任何测试即可发生。return 关键字有两方面的作用:

  1. 退出当前循环;
  2. 退出当前方法,并返回方法指定类型值。
    public static void main(String[] args) {
        System.out.println(test());//Output: 2
        System.out.println(test1());//Output: 1
    }
    public static int test(){
        int[] a = {1,2,3,4,5};
        for (int i:a) {
            if(i == 1){
                return i+1;
            }
        }
        return 0;
    }
    public static void test1(){
	    int[] a = {1,2,3,4,5};
	    for (int i:a) {
	        if(i == 2){
	            return ;
	        }
	        System.out.println(i);
	    }
}

返回值类型为 void,在代码执行结束时会有一个隐式的 return。如果你的方法声明的返回值类型为void 类型,那么则必须确保每个代码路径都返回一个值

break 和 continue

在任何迭代语句的主体内,都可以使用 breakcontinue 来控制循环的流程。 其中,break 表示跳出当前循环体。而 continue 表示停止本次循环,开始下一次循环

    public static void main(String[] args) {
        int[] a = {1,2,3,4,5};
        for (int i:a) {
        	//i%2=0结束本次循环,进入下一次循环
            if(i%2==0){
                continue;
            }
            System.out.println(i);
            //i+1=4结束循环
            if(i+1==4){
                break;
            }
        }
        /*Output:
		1
		3
		*/
    }

臭名昭著的 goto

goto 关键字 很早就在程序设计语言中出现。事实上,goto 起源于汇编(assembly language)语言中的程序控制:“若条件 A 成立,则跳到这里;否则跳到那里”。
尽管 goto 仍是 Java 的一个保留字,但其并未被正式启用。可以说, Java 中并不支持 goto。然而,在 breakcontinue 这两个关键字的身上,我们仍能看出一些 goto 的影子。它们并不属于一次跳转,而是中断循环语句的一种方法。之所以把它们纳入 goto 问题中一起讨论,是由于它们使用了相同的机制:标签。

label1:

对 Java 来说,唯一用到标签的地方是在循环语句之前。在循环之前设置标签的唯一理由是:我们希望在其中嵌套另一个循环或者一个开关。这是由于 breakcontinue 关键字通常只中断当前循环,但若搭配标签一起使用,它们就会中断并跳转到标签所在的地方开始执行。下面是 for 循环的一个例子:

    public static void main(String[] args) {
        out:
        for (;true;) {
            for (int i = 0; i < 10; i++) {
                System.out.println(i);
                if(i % 2 ==0 ){
                    continue;
                }
                if (i == 3){
                    break out;
                }
            }
        }
    }

break out语句会跳到循环的顶部,如果没有 break out语句,就没有办法在一个内部循环里找到出外部循环的路径。这是由于 break 本身只能中断最内层的循环(对于 continue 同样如此)。 当然,若想在中断循环的同时退出方法,简单地用一个 return 即可。
大家要记住的重点是:在 Java 里需要使用标签的唯一理由就是因为有循环嵌套存在,而且想从多层嵌套中 breakcontinuebreakcontinue 标签在编码中的使用频率相对较低 (此前的语言中很少使用或没有先例),所以我们很少在代码里看到它们。

知识拓展

if和switch那个效率更高

在编程语言中,不管是那种编程语言,if和switch都是是条件分支的重要组成部分。下面通过网络上的一些答复和自己的理解给大家解释下它们之间的区别。

前面我们讲解了if-else if这种多分支的结构:会依次判断if语句中的条件是否满足再执行相对应的代码,不满足所有条件的时候会进入else

if(argc > 0) {
    printf("argc > 0\n");
} else if(argc < 0) {
    printf("argc < 0\n");
} else {
    printf("argc == 0\n");
}
printf("argc = %d\n", argc);

汇编后的代码

9:        if(argc > 0)
  cmp         dword ptr [ebp+8],0
0040102C   jle         main+2Dh (0040103d);
10:       {
11:           printf("argc > 0\n");
0040102E   push        offset string "argc > 0\n" (00420f9c)
  call        printf (00401090)
  add         esp,4
12:       }else if(argc < 0)
0040103B   jmp         main+4Fh (0040105f)  ;
0040103D   cmp         dword ptr [ebp+8],0
  jge         main+42h (00401052)
13:       {
14:           printf("argc < 0\n");
  push        offset string "argc < 0\n" (0042003c)
  call        printf (00401090)
0040104D   add         esp,4
15:       }else
  jmp         main+4Fh (0040105f)
16:       {
17:           printf("argc == 0\n");
  push        offset string "argc <= 0\n" (0042002c)
  call        printf (00401090)
0040105C   add         esp,4
18:       }
19:       printf("argc = %d\n", argc);
0040105F   mov         eax,dword ptr [ebp+8]

当满足if条件时,会执行里面的代码块,然后利用jmp跳转到else语句块外,当都不满足时会利用jmp跳转到else语句块中。然后依次执行其后的每一句代码。

switch是另一种比较常用的多分支结构,下面将分析switch结构的实现:

switch(argc)
{
     case 1:
    printf("argc = 1\n");
    break;
  	 case 2:
    printf("argc = 2\n");
    break;
   case 6:
    printf("argc = 6\n");
    break;
   default:
    printf("else\n");
    break;
}

汇编后的代码

//对应的汇编代码
0040B798   mov         eax,dword ptr [ebp+8] ;eax = argc
0040B79B   mov         dword ptr [ebp-4],eax
0040B79E   mov         ecx,dword ptr [ebp-4]  ;ecx = eax
0040B7A1   sub         ecx,1
0040B7A4   mov         dword ptr [ebp-4],ecx
0040B7A7   cmp         dword ptr [ebp-4],5 ;
0040B7AB   ja          $L544+0Fh (0040b811) ;
0040B7AD   mov         edx,dword ptr [ebp-4] ;edx = argc
0040B7B0   jmp         dword ptr [edx*4+40B831h]
11:       case 1:
12:           printf("argc = 1\n");
0040B7B7   push        offset string "argc = 1\n" (00420fc0)
0040B7BC   call        printf (00401090)
0040B7C1   add         esp,4
13:           break;
0040B7C4   jmp         $L544+1Ch (0040b81e)
14:       case 2:
15:           printf("argc = 2\n");
0040B7C6   push        offset string "argc = 2\n" (00420fb4)
0040B7CB   call        printf (00401090)
0040B7D0   add         esp,4
16:           break;
0040B7D3   jmp         $L544+1Ch (0040b81e)
.....
29:       default:
30:           printf("else\n");
0040B811   push        offset string "argc = %d\n" (0042001c)
0040B816   call        printf (00401090)
0040B81B   add         esp,4
31:           break;
32:       }
33:
34:       return 0;
0040B81E   xor         eax,eax

汇编后的代码中并没有看到像if那样,对每一个条件都进行比较,jmp dword ptr [edx*4+40B831h] 是取数组中的元素。按照顺序生成了一个跳转表,通过传入的整数在数组中查到到对应的地址,直接通过这个地址跳转到对应的位置减少了比较操作,提升了效率。通过40B831h获取内存值,下图可知 0040b7b7对应case1,依此类推。
在这里插入图片描述
当没有那个值得时候,它对应的就是default。如图
在这里插入图片描述
如果每两个case之间的差距大于6,或者case语句数小于4则不会采取上面的做法,如果再采用这种方式,那么会造成较大的资源消耗
编译器先查找到每个值在数组中对应的元素位置,然后根据这个位置值再在地址表中找到地址进行跳转,这个过程可以用下面的图来表示:
在这里插入图片描述
这样通过一个每个元素占一个字节的表,来表示对应的case在地址表中所对应的位置,从而跳转到对应的地址,这样通过对每个case增加一个字节的内存消耗来达到,减少地址表对应的内存消耗。

switch-case结构有一点以空间换时间的意思,当分支较多的时候明显switch-case结构的实行效率会高很多。if-else if可以应用于更多的场合(如a > 10 && a < 20),显得更为灵活。所以开发的时候得根据不同场合选择不同的语句。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值