计算机程序的思维逻辑 (9) - 条件执行的本质

条件执行

前面几节我们介绍了如何定义数据和进行基本运算,为了对数据有透彻的理解,我们介绍了各种类型数据的二进制表示。

现在,让我们回顾程序本身,只进行基本操作是不够的,为了进行有现实意义的操作,我们需要对操作的过程进行流程控制。流程控制中最基本的就是条件执行,也就是说,某些操作只能在某些条件满足的情况下才执行,在一些条件下执行某种操作,在另外一些条件下执行另外某种操作。这与交通控制中的红灯停、绿灯行条件执行是类似的。

Java中表达这种流程控制的基本语法是If语句。

if

If的语法为:

if(条件语句){ 代码块 }

if(条件语句) 代码;

它表达的含义也非常简单,只在条件语句为真的情况下,才执行后面的代码,为假就不做了。具体来说,条件语句必须为布尔值,可以是一个直接的布尔变量,也可以是变量运算后的结果,我们在第3节介绍过,比较运算和逻辑运算的结果都是布尔值,所以可作为条件语句。条件语句为true,则执行括号{}中的代码,如果后面没有括号,则执行后面第一个分号(;)前的代码。

如,只在变量为偶数的情况下输出:

int a=10;
if(a%2==0){
   System.out.println("偶数");

}

int a=10;
if(a%2==0) System.out.println("偶数");

if的陷阱

初学者有时会忘记在if后面的代码块中加括号,有时希望执行多条语句而没有加括号,结果只会执行第一条语句。建议所有if后面都跟括号。

if/else

if实现的是条件满足的时候做什么操作,如果需要根据条件做分支,即满足的时候执行某种逻辑,而不满足的时候执行另一种逻辑,则可以用if/else。

if/else的语法是:

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

if/else也非常简单,判断条件是一个布尔值,为true的时候执行代码块1,为假的时候执行代码块2。

三元运算符

我们之前介绍了各种基本运算,这里介绍一个条件运算,和if/else很像,叫三元运算符,语法为:

判断条件 ? 表达式 1 : 表达式2

三元运算符会得到一个结果,判断条件为真的时候就返回表达式1的值,否则就返回表达式2的值。三元运算符经常用于对某个变量赋值,例如求两个数的最大值:

int max = x > y ? x : y;

三元运算符完全可以用if/else代替,但在某些场景下书写更简洁。

if/else if/else

如果有多个判断条件,而且需要根据这些判断条件的组合执行某些操作,则可以使用if/else if/else。

语法是

if(条件1){ 代码块1 }else if(条件2){ 代码块2 }... else if(条件n){ 代码块n }else{ 代码块n+1 }

if/else if/else也比较简单,但可以表达复杂的条件执行逻辑,它逐个检查条件,条件1满足则执行代码块1,不满足则检查条件2,...,最后如果没有条件满足,且有else语句,则执行else里面的代码。最后的else语句不是必须的,没有就什么都不执行。

if/else if/else陷阱

需要注意的是,在if/else if/else中,判断的顺序是很重要的,后面的判断只有在前面的条件为false的时候才会执行。初学者有时会搞错这个顺序,如下面的代码:

if(score>60){
  return "及格";

}else if(score>80){
  return "良好";

}else{
  return "优秀"

}

看出问题了吧?如果score是90,可能期望返回"优秀",但实际只会返回"及格".

switch

在if/else if/else中,如果判断的条件基于的是同一个变量,只是根据变量值的不同而有不同的分支,如果值比较多,比如根据星期几进行判断,有7种可能性,或者根据英文字母进行判断,有26种可能性,使用if/else if/else显的比较啰嗦,这种情况可以使用switch,switch的语法是:

switch(表达式){
   case 值1:
           代码1; break;
   case 值2: 

           代码2; break;
           ...

    case 值n:
          代码n; break;
    default: 代码n+1

}

switch也比较简单,根据表达式的值执行不同的分支,具体来说,根据表达式的值找匹配的case,找到后,执行后面的代码,碰到break时结束,如果没有找到匹配的值则执行default中的语句。

表达式值的数据类型只能是 byte, short, int, char, 枚举, 和String (Java 1.7以后)。枚举和String我们在后续文章介绍。

switch会简化一些代码的编写,但break和case语法会对初学者造成一些困惑。

容易忽略的break

break是指跳出switch语句,执行switch后面的语句。每条case语句后面都应该跟break语句,否则的话它会继续执行后面case中的代码直到碰到break语句或switch结束,例如:下面的代码会输出所有数字而不只是1.

int a = 1;
switch(a){
case 1:
    System.out.println("1");
case 2:
    System.out.println("2");
default:
    System.out.println("3");
}

case堆叠

case语句后面可以没有要执行的代码,如下所示:

char c = 'x';//某字符

switch(c){
   case 'A':
   case 'B':
   case 'C':
        System.out.println("A-Z");break;
   case 'D':
       ....

}

case 'A'/'B'后都没有紧跟要执行的代码,他们实际会执行第一块碰到的代码,即case 'C'匹配的代码。

条件小结

条件执行总体上是比较简单的,单一条件满足时执行某操作使用if,根据一个条件是否满足执行不同分支使用if/else,表达复杂的条件使用if/else if/elese,条件赋值使用三元运算符,根据某一个表达式的值不同执行不同的分支使用switch。

从逻辑上讲,if/else, if/else if/else,三元运算符,switch都可以只用if代替,但使用不同的语法表达更简洁,在条件比较多的时候,switch从性能上也更高(马上解释为什么)。

条件本质

正如我们探讨数据类型的时候,研究数据的二进制表示一样,我们也来看下这些条件执行具体是怎么实现的。

程序最终都是一条条的指令,CPU有一个指令指示器,指向下一条要执行的指令,CPU根据指示器的指示加载指令并且执行。指令大部分是具体的操作和运算,在执行这些操作时,执行完一个操作后,指令指示器会自动指向挨着的下一个指令。

但有一些特殊的指令,称为跳转指令,这些指令会修改指令指示器的值,让CPU跳到一个指定的地方执行。跳转有两种,一种是条件跳转,另一种是无条件跳转。条件跳转检查某个条件,满足则进行跳转,无条件跳转则是直接进行跳转。

if, else实际上会转换为这些跳转指令,比如说下面的代码:

int a=10;
if(a%2==0)
{
   System.out.println("偶数");

}
//其他代码


转换到的转移指令可能是:

int a=10;
条件跳转: 如果a%2==0,跳转到第4行
无条件跳转:跳转到第7行
{
   System.out.println("偶数");

}
//其他代码

你可能会奇怪其中的无条件跳转指令,没有它不行吗?不行,没有这条指令,不管什么条件,括号中的代码都会执行。

不过,对应的跳转指令也可能是:

int a=10;
条件跳转: 如果a%2!=0,跳转到第6行

{
   System.out.println("偶数");

}
//其他代码

这个就没有无条件跳转指令,具体怎么对应和编译器实现有关。在单一if的情况下可能不用无条件跳转指令,但稍微复杂一些的情况都需要。if, if/else, if/else if/else, 三元运算符都会转换为条件跳转和无条件跳转。但switch不太一样。

switch的转换和具体系统实现有关,如果分支比较少,可能会转换为跳转指令。但如果分支比较多,使用条件跳转会进行很多次的比较运算,效率比较低,可能会使用一种更为高效的方式,叫跳转表。跳转表是一个映射表,存储了可能的值以及要跳转到的地址,形如:

值1

代码块1的地址

值2

代码块2的地址

...

值n

代码块n的地址

跳转表为什么会更为高效呢?因为,其中的值必须为整数,且按大小顺序排序。按大小排序的整数可以使用高效的二分查找,即先与中间的值比,如果小于中间的值则在开始和中间值之间找,否则在中间值和末尾值之间找,每找一次缩小一倍查找范围。如果值是连续的,则跳转表还会进行特殊优化,优化为一个数组,连找都不用找了,值就是数组的下标索引,直接根据值就可以找到跳转的地址。即使值不是连续的,但数字比较密集,差的不多,编译器也可能会优化为一个数组型的跳转表,没有的值指向default分支。

程序源代码中的case值排列不要求是排序的,编译器会自动排序。之前说switch值的类型可以是byte, short, int, char, 枚举和String。其中byte/short/int本来就是整数,在上节我们也说过,char本质上也是整数,而枚举类型也有对应的整数,String用于switch时也会转换为整数(通过hashCode方法,后文介绍)。

总结

条件执行的语法是比较自然和容易理解的,需要注意的是其中的一些语法细节和陷阱。它执行的本质依赖于条件跳转、无条件跳转和跳转表。

条件执行中的跳转只会跳转到跳转语句以后的指令,能不能跳转到之前的指令呢?

原文发布于微信公众号 - 老马说编程(laoma_shuo)

原文发表时间:2016-04-25

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏吴伟祥

Shell脚本学习总结(二) 流程控制 转

Shell case语句为多选择语句。可以用case语句匹配一个值与一个模式,如果匹配成功,执行相匹配的命令。case语句格式如下:

7820
来自专栏java工会

JAVA 同步实现原理

Synchronized是Java中解决并发问题的一种最常用的方法,也是最简单的一种方法。Synchronized的作用主要有三个:

6700
来自专栏积累沉淀

JDK动态代理的底层实现原理

JDK动态代理的底层实现原理      动态代理是许多框架底层实现的基础,比如Spirng的AOP等,其实弄清楚了动态代理的实现原理,它就没那么神奇了,下面就来...

60270
来自专栏mwangblog

二、八、十、十六进制

11420
来自专栏Jackson0714

C#多线程之旅(4)——APM初探

413130
来自专栏程序员互动联盟

【答疑释惑】JavaScript解释器是干什么用的?

问题: ? JavaScript解释器主要是干什么用的?他不是一门语言吗? 解答: JavaScript是一门脚本语言,是需要被别人解释执行的,这个别人就是Ja...

31160
来自专栏软件开发 -- 分享 互助 成长

C++ STL之迭代器注意事项

1、两个迭代器组成的区间是前闭后开的 2、如果迭代器的有效性,如果迭代器所指向的元素已经被删除,那么迭代器会失效 http://blog.csdn.net/hs...

24960
来自专栏Micro_awake web

JSON.parse与JSON.stringify

JSON:JavaScript Object Notation(JavaScript对象表示法);甚至我们就可以大致认为JSON就是Javascript的对象,...

18920
来自专栏吾爱乐享

short s=1;s=s+1; short s=1;s+=1; 有区别么?? 如果有的话区别是什么?

15830
来自专栏北京马哥教育

提升Python程序性能的7个习惯

使用局部变量替换模块名字空间中的变量,例如 ls = os.linesep。一方面可以提高程序性能,局部变量查找速度更快;另一方面可用简短标识符替代冗长的模块变...

9000

扫码关注云+社区

领取腾讯云代金券