同很多高级编程语言一样,Java语言的运算符系统当中也有自增(++)和自减(--)这两个运算符。很多小伙伴对这两个运算符都深感头疼,并且很多公司在面试的时候也经常会问到与之相关的问题,今天我们就通过一篇文章来深度解析一下这两个运算符,相信在看过这篇文章之后,你再也不会被自增(++)和自减(--)运算符难住。由于自增和自减运算符的原理完全相同,所以我们在讲解的时候仅以自增运算符举例。(预警:本文举例较多,篇幅较长,请耐心看完)
一、基本运算规则介绍
自增(++)和自减(--)运算符是对变量在原始值的基础上进行加1或减1的操作。它们都有前缀和后缀两种形式。前缀形式的运算规则可以概括为:”先自增(减),后引用”,而后缀形式的运算规则可以概括为:”先引用,后自增(减)”。这里所说的”引用”,指的是使用变量的值。另外,我们还要强调一个细节: 无论是前缀形式还是后缀形式,自增自减运算符的优先级要高于赋值运算符 。大家要记清楚这个细节,后文还会针对这个细节进行论述。下面我们就分为几种情况来研究++和--在不同场合下的运算效果。
二、语句中仅有++或--
请看代码:
我们可以看到,这段代码中总共有3条语句,其中第2条语句中仅有一个后缀形式++操作,程序的输出结果是3。那么我们再来看另外一段代码:
这段代码与之前的那段代码基本一样,只是第2条语句中,后缀形式的++操作被换成了前缀形式,程序的输出结果还是3。这说明: 当一条语句中仅有++或--操作时,前缀形式与后缀形式的运算符没有任何区别 。请注意:这句话的前半句是一个很重要的前提,那就是” 一条语句中仅有++或--操作”,如果脱离了这个前提,后半句所说的结论并不成立。
三、++或--运算结果赋值给其他变量
可能有些小伙伴没看明白这个标题是什么意思,我们来看一段代码:
这段代码的第2条语句对变量a进行了自增操作,并且把这个操作结果赋值给另一个变量b。语句中的变量b就是标题中所说的”其他变量”,是指没有进行自增自减操作的其他变量。为什么要强调”赋值给其他变量”这个前提呢?就是因为如果把运算结果赋值给变量a自身,又会产生不同的效果,我们后面再去讲解赋值给自身的情况。现在先来分析程序,重点看第2条语句:变量a所进行的是前缀形式的自增操作,那么按照”先自增后引用”的运算规则,a的值首先变成3,然后赋值b。因此,给变量b赋值的是3,那么输出结果当然就是3和3。
如果代码变成下面的样子:
这一次,代码的第2行发生了变化,a的自增操作变成了后缀形式。此时的程序输出结果是3和2。为什么会是这样的运行结果呢?网上有很多资料对此的解释是:因为表达式中出现的是后缀形式的自增操作,因此,运算的规则就变成了”先引用后自增”。计算机会先使用a的值给b赋值,a的值是2,所以b被赋值为2,a在完成给b赋值的操作之后,才会完成自增变为3,所以程序的输出结果为3和2。这种解释看似非常合理,但 其实是错误的 !
按照这种解释,后缀形式的自增是在赋值之后才完成的,由此可以推出后缀形式的自增自减运算的优先级比赋值运算的优先级更低。而我们之前已经特意强调过:无论是前缀形式还是后缀形式,自增自减运算符的优先级都比赋值运算符要高。接下来问题来了:既然++和--的运算优先级高于赋值运算符,那么为什么赋值之前a的值没有自增为3呢?
其实这是个错觉!a在赋值给变量b之前,就已经完成了自增!为了讲解清楚真实情况,我们必须科普一个小常识,那就是:当程序中,如果变量参与了算术运算、或者以变量的值进行赋值,或者打印了某个变量的值,总之只要程序中用到这个变量的值,都会先把这个变量存入一个临时的空间,专业上把这个临时的空间称之为”操作数栈”。我们之前所说的”先引用后自增”中所说的这个”引用”操作,其实就是指”把变量的值存入操作数栈”这个动作。当程序中需要用到变量的值,计算机是从”操作数栈”中取出值进行运算,并不是我们想象的直接从变量所在的内存单元中取出数值。但是,如果语句中仅有++或--,并不会把变量的值存入操作数栈,而是直接对变量进行自增或自减的操作,这也是为什么我们把语句中仅有++或--单独作为一种情况讲解的原因。
科普完这个小常识之后,我们来解释刚才的代码为什么会输出3和2。代码中出现用a的值给变量b赋值的语句,并且a的后面出现了++,说明要对a进行后缀形式的自增操作。按照我们刚才科普的小常识,a参与了赋值运算,那么就会把a的值存入操作数栈。因为a的自增是后缀形式的,所以要遵循”先引用后自增”的运算规则,因此,计算机会首先取出a的值2存入操作数栈,然后再把a的值增加到3。 做完自增操作之后 ,接下来会对变量b进行赋值操作。那么,是用哪个值给变量b赋值呢?就是用刚才存到操作数栈中的那个2对变量b进行赋值,所以b最终得到的值是2,因此输出结果是3和2。
在这里,请大家注意一个细节,那就是:代码中的自增操作虽然是后缀形式的,但这个自增动作却是在赋值之前完成的,这也解释了后缀形式的自增运算优先级高于赋值运算,而网上很多资料中所说的”先完成赋值再去做自增操作”是完全错误的。
那么,之前例题中第2条语句是”b=++a;”,会不会也把a的值存入操作数栈呢?答案是肯定的,只要是变量参了算术运算、赋值、被打印这些操作,都会取出变量的值存入操作数栈。因为语句中出现的是前缀形式的自增,所以在把值存入操作数栈之前就已经完成了自增操作。
好,现在我们来加大一点难度,请看代码:
这段代码的第2条语句,出现了2次++a,那么运算结果会是多少呢?”=”右边的表达式为”++a + ++a”,按照运算优先级,计算机首先对a进行自增操作,经过这一步操作之后,变量a的值变成了3,紧接着,计算机会把这个3存入操作数栈中,为了方便讲述,我们把这个操作数栈叫做”栈1”,接下来,表达式中又出现了++a,此时计算机再次对a进行自增,a的值变成了4,为了完成加法操作,计算机又把这个4存入到另一个操作数栈中。我们把这个操作数栈称为”栈2”。按照运算优先级,接下来要做的事情就是完成加法运算,计算机把”栈1”中的3和”栈2”中的4分别取出并且进行相加,得到的结果是7,最后把这个7赋值给”=”左边的变量b。所以上面代码运行所输出的结果分别是4和7。
下面我们再来研究一个关于运算顺序问题,请看下面的代码:
我们还是重点来分析第2行代码。”=”右边首先是++a,所以对先a进行自增操作,此时a变成了3,紧接着把自增之后的值存入”栈1”,接下来遇到了a++,因为是后缀形式的自增操作,遵循”先引用后自增”的运算规则,计算机先取出a的值3,并且存入”栈2”,然后对a进行自增的操作,a的值变成了4。现在,”栈1”和”栈2”中都已经存入了值,那么,此时计算机是先把”栈1”和”栈2”中的值做个相加操作呢,还是先去做变量a的第3次自增操作呢?很多同学肯定都会认为计算机肯定是先去完成a的第3次自增操作,理由是自增自减运算优先级高于加法运算。其实不然,当运算进行到这一步的时候,计算机会先把”栈1”和”栈2”中的两个值相加,然后才去完成a的第3次自增操作。很多人都不理解为什么会这样,难道不是++a的优先级更高吗?
这里需要澄清一个大家对”优先级”概念的误解。当计算机有AB两个操作可以做的时候,如果选择先完成A得到的结果是X,而选择先完成B得到结果是Y,此时计算机必须按照运算的优先级做出选择。而如果先完成A和先完成B对运算结果没有影响,那么计算机就会先完成左边(先出现的)的操作。此时,如果把”栈1”和”栈2”中的值相加,并不会影响到最终的运算结果,并且这个操作是先于”a的第3次自增”出现的,所以计算机会先把”栈1”和”栈2”中的值加起来,然后才完成”a的第3次自增”。
很多小伙伴可能不理解,那么自增操作优先级高于加法操作,是如何体现出来的呢?大家仔细看,在代码的第2条语句中,a的自增出现了3次,加法运算出现了2次。”a的第2次自增”后于”第1次加法运算”出现,但却先于”第1次加法运算”执行,同理,”a的第3次自增”后于”第2次加法运算”出现,却先于”第2次加法运算”执行。这些都体现出自增运算的优先级是高于加法运算的。
讲清楚”优先级”这个问题之后,我们再回到例题本身。第1次加法运算是3和3相加,结果是6,计算机会把这个6存入”栈3”。紧接着计算机看到表达式中第2次出现了++a,于是再次对a进行自增操作,a的值变成了5,并且存入”栈4”。之后就是完成”栈3”和”栈4”中数值的相加操作,也就是把6和5相加,最终得到的结果是11。因此程序最终的输出结果是5和11。
四、++或--运算结果赋值给自身
标题中所说的”自身”,就是指进行了自增或自减运算操作的变量。我们来看下面的代码:
以上代码的第2条语句,对a进行了前缀形式的自增,然后又赋值给a自身,那么a的值是多少呢?因为a进行的是前缀形式的自增,所以运算规则是”先自增后引用”,自增之后a的值变成了3,把3存入操作数栈,之后以3赋值给a,所以a的值还是3。
但是,如果我们把代码变成如下形式:
这一次,我们把第2条语句中的++a改成了a++。这种情况下,程序输出a的值为竟然为2,而不是3。说的直白一点,a并没有按我们的想象实现自增。这是为什么呢?我们来分析一下整个运算的过程:计算机看到”=”右边是后缀形式的自增,因此以”先引用后自增”的规则进行运算,先把a的值存入操作数栈,紧接着对a进行自增操作,a的值变成了3,最后又用操作数栈中的那个2对a进行赋值,a的值又变成了2。这样给我们造成了一种”a没有进行自增”的错觉。之前说过,网上很多资料都误传“后缀形式自增操作优先级低于赋值运算”。如果按照这种错误说法,无法解释以上代码最终的输出结果为2的原因。这也是为什么本文一开始就强调” 无论是前缀形式还是后缀形式,自增自减运算符的优先级要高于赋值运算符 ”的原因,就是因为这个细节是解决此类问题的关键点。类似的情况还可以衍生出很多版本,例如以下这段代码:
这段代码中,第2条语句对a进行了两次自增操作,最终输出a的值是6。而如果按照”后缀形式的自增自减优先级低于赋值运算”的错误说法,则会认为最终输出a的值是7,理由是”=”右边的运算结果是6,赋值给”=”左边的a之后,又进行一次自增,最终的结果是7,当然,事实可以证明这种理解是错误的。
接下来,我们再来研究一种更特殊的情况,请看以下代码:
这一次,语句中出现了复合赋值运算符。如果程序运行,输出a的值会是多少呢?我们首先可以推导出”+=”右边的运算结果是7(具体推导过程不再赘述)。我们还知道,复合赋值运算符在完成运算的时候,要把右边当作整体。那么现在关键的问题就只剩一个了,那就是:”+=”左边的a到底是多少?很多人认为”+=”左边a的值应该是4,原因是++的运算优先级高于+=,所以要先完成2次自增,完成了2次自增以后,a的值已经变成了4,由此推得”+=”左边a的值应该是4,而最终的运算结果是11(4+7的和)。但实际运行程序的话,可以看到输出a的值为9而非11。这是为什么呢?就是因为+=的优先级虽然低于++,但是计算机在实际完成+=运算的时候会分为好几个步骤进行。我们可以大致把+=运算分解为四大步骤:
A、把+=左边的变量值存入操作数栈1
B、计算+=右边的表达式,并把计算结果存入操作数栈2(此步骤其实是由多个具体步骤组成的)
C、把操作数栈1和操作数栈2中的数值相加得到运算结果
D、把运算结果存入变量a当中
现在最关键的问题是步骤A和B哪一个先被执行。如果先执行步骤A,那么存入操作数栈1的是变量a自增之前的值,也就是2;反之,如果先执行B,那么存入操作数栈1的是变量a自增之后的值,也就是4。真实的情况是先执行步骤A,也就是把变量a自增之前的值存入操作数栈1。这是一个普遍适用的规律,所以大家一定要记住: 当语句中以复合赋值运算符给变量赋值的时候,计算机会先把复合赋值运算符左边变量的值存入操作数栈 。因此,这段程序运行的结果是9。
五、总结与说明
1、以上,我们把自增自减运算符的题目总结为3大类,无论是企业的面试题,还是学校的考试题,关于自增自减运算符的题目基本都可以归结到这3大类中。所以,仔细研究这3大类题目的规律,基本可以保证相关题目不会难住你。
2、笔者为确保对文中所提到的代码都能做到正确解释,对文中所有代码均用javap命令查看了编译后的字节码。
3、文中例题中的代码如果放到C语言环境下,执行结果会有不同,原因是C语言对源码的编译和解析规则与Java语言略有不同,因此,千万不要把本文的结论套用到C语言中。
4、虽然文章详细分析了自增自减运算的各种情况,但笔者本身非常反对代码中出现类似于” a += ++a + ++a;”这样的代码,因为这样的代码可读性非常差,很容易造成误解,所以,各位小伙伴在真实编程的时候,一定别怕麻烦,哪怕多写几行代码,也要把代码写的可读性强一些,尽量不要让人产生误解和歧义。
5、为方便初学者理解,文中并没有出现编译器、解析器之类的名词。而文中所提到的”栈1”、 ”栈2”、 ”栈3”、 ”栈4”这些操作数栈的名称,也只是为了方便读者理解而起的名称,并非对应系统真实对各个操作数栈的命名。
希望本文能够帮助初学者深入理解Java语言自增自减运算符。