技术博客
显微镜下的 i++ 与 ++i
admin2022-09-30 05:31
247人已围观
简介显微镜下的 i++ 与 ++i
本文来自微信公众号:低并发编程 (ID:dibingfa),作者:闪客
注意,以下讨论的语言是 Java
这个问题被网上的好多文章写烂了,但基本重复度很高,我看过后的感觉是,大部分都是错误的、误导读者的。
随便百度一下,我们打开第一条。
上来先说个结论
i++ 先赋值在运算,例如 a=i++,先赋值 a=i,后运算 i=i+1,所以结果是 a==1
++i 先运算在赋值,例如 a=++i,先运算 i=i+1,后赋值 a=i,所以结果是 a==2
然后给了成吨的例子来说明
public class Test3 { public static void main(String[] args) { int y=0; //注意 "=" 是赋值,"==" 才是相等 //这里的 y=++y 是先运算在赋值 y=++y;// y==0,++y==y+1; 结果 y=++y == y+1 == 0+1 ==1 y=++y;// y==1,++y==y+1; 结果 y=++y == y+1 == 1+1 ==2 y=++y;// y==2,++y==y+1; 结果 y=++y == y+1 == 2+1 ==3 y=++y;// y==3,++y==y+1; 结果 y=++y == y+1 == 3+1 ==4 y=++y;// y==4,++y==y+1; 结果 y=++y == y+1 == 4+1 ==5 System.out.println("y="+y);//5 int i =0; // i==0,i++==0; 结果 i=i++ == (记住先赋值后运算) i=i++; i=i++; i=i++; i=i++; i=i++; System.out.println("i="+i);//0 System.out.println("================");//1 } }
首先这个例子没有任何代表性;
其次得出的结论也是极其误导人的;
但最关键的是,这无法帮助你真正理解 i++ 和 ++i 的本质是什么
所以 i++ 和 ++i 的区别请听我说
先忘掉什么“先赋值、后运算”
别着急,慢慢来,忍住看到最后
i++ 和 ++i 字节码
查看字节码用 javap 命令,或者直接用 idea 的插件,这里不做过多介绍
在某方法里写上这样一段代码
public void ipp() { int i = 1; i++; }
查看其字节码
iconst_1 istore_1 iinc 1 1 return
然后我们在写上这样一段代码
public void ipp() { int i = 1; ++i; }
查看其字节码
iconst_1 istore_1 iinc 1 1 return
发现没,完全一样。也就是说,在没有赋值操作时,i++ 和 ++i 编译成字节码后,都是
iinc 1 1
完全一样
有多少人之前的理解是 i++ 和 ++i 本身孤零零地放在那是有区别的呢?
看 iinc 字节码的定义
找到 JVM 官方手册
https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html#jvms-6.5.iinc
看到 iinc 字节码指令的格式为
iinc index const
index 表示局部变量表的索引,const 表示将其值加上多少
所以上面的
iinc 1 1
就表示
将局部变量表索引为 1 位置的值,加上 1
局部变量表索引 0 位置处是 this,索引 1 位置处的值,是 int i = 1 这段代码设置的,也就是 1。把这个值加 1,就变成了了 2
再来回顾下上面的代码
public void ipp() { int i = 1; i++; System.out.println(i); } public void ppi() { int i = 1; ++i; System.out.println(i); }
如果打印 i 的值,很容易知道,两个都是 2
所以很简单,i++ 和 ++i 本身在字节码指令中的体现都是 iinc,就是单纯把 i 所在的局部变量表那个位置的值,+1
稍稍复杂一点
我们把上面的代码稍稍复杂一点,++ 操作后,再重新赋值给 i
public void ipp() { int i = 1; i = i++; } public void ppi() { int i = 1; i = ++i; }
你猜 i 的值分别是多少
别急,再次查看字节码
void ipp() --> i = i++;
iconst_1 istore_1 iload_1 iinc 1 1 istore_1 return
void ppi() --> i = ++i;
iconst_1 istore_1 iinc 1 1 iload_1 istore_1 return
这回看到不一样了,但字节码指令都相同,只是顺序不同
i = i++ 就是 先 iload_1 再 iinc 1 1
i = ++i 就是 先 iinc 1 1 再 iload_1
所以也很简单,i++ 和 ++i 只有在最终赋值给某变量时(实际上是因为参与了运算,因为直接赋值也是一种无运算符号的运算),字节码指令是不同的,而且也只是顺序上的不同。
那顺序的不同会导致结果怎样呢?下面我们通过观察 虚拟机栈 来细化整个过程
观察虚拟机栈中的变化
但你得先知道虚拟机栈是什么,也就得知道 JVM 的内存划分,这块就不帮你复习了哈,直接上 ipp () 方法入虚拟机栈后,这个方法栈帧里的初始构造。
看 i = i++
下面一步步执行 ipp () 方法的字节码
iconst_1 istore_1 iload_1 iinc 1 1 istore_1 return
注意局部变量表 0 处表示的是 this,这里为了简化没有写出,然后栈帧的“帧”字写错啦,我就任性一下不改了哈
iconst_1:将立即数 1 压栈
istore_1:操作数栈顶 -> 局部变量表 1 位置
iload_1:局部变量表 1 位置 -> 操作数栈顶
iinc 1 1:局部变量表 1 位置的值 +1
istore_1:操作数栈顶 -> 局部变量表 1 位置
所以,最后 i 的值,也就是局部变量表中 1 位置处的值,就是 1
我们用动画再演示一遍
你可以感受到,i = i++ 这种写法,iinc 1 1 这一步是完全没有用的,因为最后局部变量表 1 位置处的值,在最后一步赋值操作时,会被操作数栈顶处的值覆盖,所以之前的 +1 完全没用
所以 idea 也会提示你,这里的 i++ 没用
the value changed at 'i++' is never used
再看 i = ++i
相信这个你自己也可以推到出来了
iconst_1 istore_1 iinc 1 1 iload_1 istore_1 return
iconst_1:将立即数 1 压栈
istore_1:操作栈顶 -> 局部变量表 1 位置
iinc 1 1:局部变量表 1 位置的值 +1
iload_1:局部变量表 1 位置 -> 操作栈顶
istore_1:操作栈顶 -> 局部变量表 1 位置
所以,最后 i 的值,也就是局部变量表中 1 位置处的值,就是 2
我们直接用动画演示一遍
本质区别
所以看出本质区别是什么了么?
区别就是
是 "先把局部变量表中的值 +1,再放到操作数栈中"
还是 "先放到操作数栈中,再把局部变量表中的值 +1"
仅此而已
所以网上普遍的说法,i++ 表示 先 赋值 再 运算
赋值就是 压入操作数栈顶
运算就是 局部变量表 +1 操作
反正这俩词我是对应不上...
还有的说法是,i++ 是先把 i 拿出来使用,然后再 + 1;
还有的说法是,i++ 先赋值在自增
还有的 ... ...
哥哥诶,咱别用自己造的词误导读者了好不?
所以最后用我的话总结一个没有任何歧义的
i++:先将局部变量表中的 i 放入操作数栈中,再将局部变量表中的 i 值 +1
++i:先局部变量表中的 i 值 +1,再将 i 放入操作数栈中
来点难的
当你从这个角度理解了之后,再做类似的复杂一点的题,也不在话下,大不了在脑子里从头推导一遍即可
看题
int i = 2; int y = i++ + ++i; y = ? int a = 2; a = a++ + ++a; a = ? int b = 2; b = b++ + (++b + ++b) + (b += 2); b = ?
思
考
一
分
钟
答案
y = 6
a = 6
b = 18
你做对了么?
我把最难的那个题的字节码展示出来
按照黄色的字可以到操作数栈的变化(从左到右就是操作数栈从栈底到栈顶),自己脑补一下动画吧,不想做了有点懒哈哈哈~
int b = 2; b = b++ + (++b + ++b) + (b += 2); iconst_2 ; 操作数栈 2 istore_1 ; 局部变量表 b=2 iload_1 ; 操作数栈 2 iinc 1 by 1 ; 局部变量表 b=3 iinc 1 by 1 ; 局部变量表 b=4 iload_1 ; 操作数栈 2 4 iinc 1 by 1 ; 局部变量表 b=5 iload_1 ; 操作数栈 2 4 5 iadd ; 操作数栈 2 9(=4+5) iadd ; 操作数栈 11(=2+9) iinc 1 by 2 ; 局部变量表 b=7 iload_1 ; 操作数栈 11 7 iadd ; 操作数栈 18(=11+7) istore_1 ; 局部变量表 b=18
再难的,我觉得就有些无聊了,大家自己给自己出题吧~
如果你对这里的入栈顺序有困惑,比如你感觉加了()数学上不是先进行运算么?怎么不是先入栈参与运算呢?
那其实这和 i++ 与 ++i 的知识就不相关了,你需要了解的是 前缀、中缀、后缀表达式,这里只举个例子不展开讲解。
简单说就是如何将数学表达式,转换成一种格式,按照这个顺序可以方便通过栈来实现计算。
比如
b++ + (++b + ++b) + (b += 2)
在转成后缀表达式过程中 ++ 操作根本不受影响,先简化成
b + (b + b) + b
转换成后缀表达式后就是
b b b + + b +
照着这个顺序压栈,就是字节码中指令的顺序啦,比如 b 压栈就是 iload_1,运算符(+)压栈就是 iadd,你再回过去证明一下哦~
而这里的每一个 b 的值,就是压栈那一时刻的 b 的值
最后愤怒地再说两句
所以,网上关于 ++ 的题目,其实是两个知识点
i++ 与 ++i 参与运算时的字节码指令
将数学表达式转换为栈操作的后缀表达式
而网上的讲解,大部分都不是从最直接的字节码指令说,还将两个知识点混为一谈,我觉得是不负责任的。
回过头来再看开头说的那篇文章的结论
i++ 先赋值在运算,例如 a=i++,先赋值 a=i,后运算 i=i+1,所以结果是 a==1
++i 先运算在赋值,例如 a=++i,先运算 i=i+1,后赋值 a=i,所以结果是 a==2
先不说它没有用字节码来说明问题,你有没有发现这说的本身就是错的,有很大的误导性。
先赋值 a=i,后运算 i=i+1
其实根本没有先赋值吧,只是把 i 丢到操作数栈中等待被运算而已,然后局部变量表 i=i+1,最后操作数栈中的 i 出栈并写入局部变量表中 a 的位置,这时才叫赋值。
总之,这类文章还是少看为好
本文来自微信公众号:低并发编程 (ID:dibingfa),作者:闪客

微信公众号
很赞哦!(0)
相关文章
文章评论
评论0
站点信息
- 微信公众号:扫描二维码,关注我们

点击排行

标签云
-
php
网页设计
个人博客
JS
个人博客
Html
春节必看: 2020新春红包大战 全攻略
新增详细玩法攻略!
支付宝集五福5亿集分宝招商银行抽现金券抖
抖音 2020 发财中国年 攻略
支付宝集五福5亿集分宝招商银行抽现金券抖
最近购买威尔胜WTB0900复刻版和WT
mysql慢查询和php-fpm慢日志
PSR-2
基础代码规范
Thinkphp
响应式
公司
整站
源码
网络科技网站模板
1024
节日
百度收录
论坛
社区
2020
豆瓣
评分最高
电影
debugger
调试
Python
语法
高德
百度地图
MySQL
追寻
webpack
vue
oracle
服务器搭建
有趣
动物
人体
历史
天文
生活
名人
体育
地理
文化
科学
心理
植物
饮食
自然
图片
JVM
IDEA
Loader
Git
UNIAPP
股票
A股
同花顺
海尔
海天味业
半年报
股市总结
歌尔股份
乐普医疗
涪陵榨菜
餐饮
财报分析
酒店
年报分析
美锦能源
山煤国际
贵州茅台
张坤
腾讯
华鲁恒升
淮北矿业
药明康德
早盘关注
国电电力
北方华创
宝丰能源
TCL中环
兔宝宝
天润乳业
启明星辰
阳光电源
山西汾酒
迈瑞医疗
人福医药
比亚迪
宁德时代
汤臣倍健
伊利股份
通威股份
东鹏饮料
隆基股份
紫金矿业
五粮液
康龙化成
赣锋锂业
爱尔眼科
片仔癀
VR
永新股份
爱美客
美的集团
格力电器
科沃斯
云南白药
同仁堂
洋河股份
白云山
三体
狂飙 原著