显微镜下的 i 与 i

低并发编程,周一很颓废,周四很硬核

注意,以下讨论的语言是 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_1istore_1iload_1iinc 1 1istore_1return

void ppi() --> i = ++i;

iconst_1
istore_1
iinc 1 1
iload_1
istore_1
return

这回看到不一样了,但字节码指令都相同,只是顺序不同

i = i++ 就是 先 iload_1iinc 1 1

i = ++i 就是 先 iinc 1 1iload_1

所以也很简单,i++ 和 ++i 只有在最终赋值给某变量时(实际上是因为参与了运算,因为直接赋值也是一种无运算符号的运算),字节码指令是不同的,而且也只是顺序上的不同。

那顺序的不同会导致结果怎样呢?下面我们通过观察 虚拟机栈 来细化整个过程

观察虚拟机栈中的变化

但你得先知道虚拟机栈是什么,也就得知道 JVM 的内存划分,这块就不帮你复习了哈,直接上 ipp() 方法入虚拟机栈后,这个方法栈帧里的初始构造

看 i = i++

下面一步步执行 ipp() 方法的字节码

iconst_1istore_1iload_1iinc 1 1istore_1return
注意局部变量表 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 的位置,这时才叫赋值。
总之,这类文章还是少看为好
今天的闪客,很愤怒,请见谅~


低并发编程,专注底层原理,重视讲述过程

(0)

相关推荐

  • JVM 字节码指令

    本文部分摘自<深入理解 Java 虚拟机> 简介 Java 虚拟机的指令由操作码 + 操作数组成,其中操作码是代表某种特定操作含义的数字,长度为一个字节,而操作数就是此操作所需的一个或多个 ...

  • Dji Mimo APP逆向.3

    对jar8的分析 先看结构 内部有对版本号码的判断,且打印log Android x应该是对实现的包 和注解 什么是注解(Annotation)?注解是放在Java源码的类.方法.字段.参数前的一种特 ...

  • JVM架构 |栈式指令集与寄存器指令集有什么区别?

    文章目录 一.两种指令集的区别 二.代码直观演示两种指令集架构 三.基于栈的解释器执行过程 四.总结 一.两种指令集的区别 指令集的架构模型分为基于栈的指令集架构与基于寄存器的指令集架构两种,HotS ...

  • 一、JVM与Java体系结构

    文章目录 1. 虚拟机与Java虚拟机 2. JVM的位置 3. Java的体系结构 4. JVM整体结构 5. Java代码执行流程 6. JVM的架构模型 7. JVM生命周期 8. JVM发展历 ...

  • Java学习笔记--来自Java核心卷和尚学堂视频

    Java常见点解析 起步之注意点 Java对大小写敏感,关键字public被称为访问修饰符 关键字class 的意思是类.Java是面向对象的语言,所有代码必须位于类里面. 一个源文件中至多只能声明一 ...

  • C#深入浅出之操作符和控制流程

    操作符 操作符简单举例就是生活中的+-*/等等运算符号,下面会详细讨论运算符内容. 一元正负操作符 有时候需要改变数值的正负号.一元操作符(-)可以使得数字的正负号改变. 例如: int a = -1 ...

  • 【显微镜下】鳃隐鞭虫

    鳃隐鞭虫隶属原生动物门.鞭毛纲.动鞭亚纲.动基体目.波豆亚目.波豆科.隐鞭虫属. ▲寄生于黄颡鱼鳃部的鳃隐鞭虫(杜辉 摄) 鳃隐鞭虫.虫体狭长形,轮廓像一片柳叶.毛基体在身体的前端.前鞭毛和后鞭毛大致 ...

  • 心头好| 显微镜下的大明 ——带你领略大明百姓的真实生活

    一.  作者简介 马伯庸,原名马力.人民文学奖.朱自清散文奖得主.马伯庸擅长以推理对真实史料进行解构和猜想,重组为兼具想像力与真实感的"历史可能性"小说,被评为沿袭"'五 ...

  • 古瓷在显微镜下呈现的图形,原来是这样的

    鉴定古瓷器真伪,科学的方法是微观定新老,宏观断年代,已成为藏家的共识, 用100-500倍显微镜能分辨出用肉眼分不清的真伪特征, 抓住瓷器造假无法达到的特征,就是抓住了瓷器真伪的绝对特征. 在网上交流 ...

  • 男子将手放在显微镜下,放大500倍细看,结果把自己给恶心到了

    手是我们身体的一部分,在生活中手的作用非常重要,无论是吃饭,工作还是玩游戏我们都需要用到手,可以说除了睡觉的时候,我们的手无时无刻不在为我们工作,但是有这么句话说的好:"病从手入" ...

  • 将秦陵兵马俑放显微镜下,发现白色“花朵”,专家:我们犯了大错

    众所周知,秦始皇生前有着丰功伟绩,他造长城,统一度量单位.给世人留下深刻的印象,他离世后,秦始皇陵墓仍然是人们关注的一个焦点,其中发现的兵马俑,已经成为第一批中国遗产,更是"世界第八大奇迹& ...

  • 兵马俑褪色真相被揭开!显微镜下发现神秘白色“花朵”,至今无解

    在考古工作上,确实我们也曾经犯下了许多的错误,但是这些都是考古经验和技术的不足,虽然不能怪罪于我们的专家,但是看到那么多稀世珍宝毁灭了,的确是非常令人痛心的事情啊! 相信不少人都去陕西看过兵马俑吧,其 ...

  • 洗澡搓下来的黑泥究竟是什么?黑泥在1000倍显微镜下是什么样子?

    对于一个北方人而言,如果冬季没有去澡堂子搓次澡,那这个冬天是不完整的.其实不只是北方人,在现如今许多地方,都保留着洗澡时搓澡的习惯. 表面上看,人们洗澡的频率足够高,按理说没有什么灰尘能够附着在皮肤上 ...

  • 【每天美玉】——显微镜下的玉钺 揭秘解玉砂加工工艺

    这件"出身高贵"的玉钺,通身没有纹饰,却有着远古石器的特征,最难得的是,在它的身上,还保存着非常清晰的解玉砂的加工痕迹. 请您跟随每天美玉一起,研究探讨出土古玉. 这件玉钺乍看之下 ...

  • 显微镜下的病毒竟如此美丽

    最近迷上微观世界,在显微镜下有着无比美妙奇幻的另一个宇宙. 今天来看看漂亮得不可思议的病毒们吧! 只要有生命的地方,就有病毒存在:病毒很可能在第一个细胞进化出来时就存在了.病毒起源于何时尚不清楚,因为 ...

  • 显微镜下的神奇世界——动物篇

    此文中图片素材将入库,真爱粉稍后可以索要提取码. A butterfly proboscis. 蝴蝶的象鼻 Thorax, head and eye section of Chrysochroa fu ...