Java中赋值语句的返回值(通过反编译来分析)
引言
今天在看书的时候,看到了一句:
return (count = ++ temp;)
看到这行代码的时候,有点莫名其妙,因为以前并没有见过类似的用法。
仔细想想,这里的返回的应该是count = ++ temp
这一语句的返回值,而这是一个赋值语句,那么在Java里的赋值语句的是否有返回值呢?接下来,博主就准备自己动手试一下。
Java中赋值语句的返回值
首先,准备了一个简单的例子来实验:
// AssignReturnTest.java
public class AssignReturnTest {
public static void main(String[] args) {
int count = 8;
int res = (count = 21);
}
}
简单解释一下,这个例子中,在int res = (count = 21);
中有两层赋值语句,内层的是count = 21
,然后将这个语句的返回值赋给了外层的res
。
首先编译一遍,没有报错,点击运行也可以正常跑通,说明对于变量res
的赋值语句int res = (count = 21);
是没有问题的。
接下来,在这一句设定一个断点:
然后进行调试,运行到这一赋值语句的下一行(即下图的第7行花括号处):
这时,可以看到各个变量的值:
这里要注意有两个点:
count
已经被赋值为21,说明语句后半部分的赋值语句被正常执行res
也被赋值为21,说明等号右端的21不止赋给了变量count
,也作为返回值返回到前一个等号,并赋给了res
由这个例子,我们发现,一个赋值语句的返回值就是赋值语句中等号右端的值,并且,将赋值语句作为返回值的时候,这个赋值语句也是被正常执行的。
如果不想继续深究的话,基本上,记住这个规则就可以了。但是如果想要细细研究一下的话,我们可以再详细地看一看这个语句是怎样被执行的。
通过反编译得到的字节码分析
这里,我们可以通过对于.class文件进行反汇编,得到这个代码的字节码,来仔细看看这条语句是怎样被执行的。
字节码文件是可以被直接打开阅读的,但多少还是有些不便,但是,Oracle已经为我们准备好了一个专门用于分析Class文件的字节码的工具:javap,接下来我们可以看一下具体怎么使用这个工具。
首先,我们打开cmd,并进入存放代码编译得到的.class文件的路径(小技巧:如何快速地打开指定文件夹的cmd),比如我用的IDE是IDEA,就在程序设置的out文件夹(默认就在项目文件夹下,与src平级),里面的各个文件的路径与项目中是一样的。或者,没有在IDE中编写或者想要自己编译的话,可以找到所写的文件AssignReturnTest.java的路径,并在cmd中用javac
命令得到.class文件。
在cmd中进入.class文件路径之后,我们键入’javap -verbose AssignReturnTest.class`命令,即可在cmd中打印出文本化的可阅读的字节码内容:
Last modified 2019-5-13; size 497 bytes
MD5 checksum cabfd26a2477fdf6540a362b22d5e98c
Compiled from "AssignReturnTest.java"
public class learning_java.GrammarTest.AssignReturnTest
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #3.#20 // java/lang/Object."<init>":()V
#2 = Class #21 // learning_java/GrammarTest/AssignReturnTest
#3 = Class #22 // java/lang/Object
#4 = Utf8 <init>
#5 = Utf8 ()V
#6 = Utf8 Code
#7 = Utf8 LineNumberTable
#8 = Utf8 LocalVariableTable
#9 = Utf8 this
#10 = Utf8 Llearning_java/GrammarTest/AssignReturnTest;
#11 = Utf8 main
#12 = Utf8 ([Ljava/lang/String;)V
#13 = Utf8 args
#14 = Utf8 [Ljava/lang/String;
#15 = Utf8 count
#16 = Utf8 I
#17 = Utf8 res
#18 = Utf8 SourceFile
#19 = Utf8 AssignReturnTest.java
#20 = NameAndType #4:#5 // "<init>":()V
#21 = Utf8 learning_java/GrammarTest/AssignReturnTest
#22 = Utf8 java/lang/Object
{
public learning_java.GrammarTest.AssignReturnTest();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 3: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Llearning_java/GrammarTest/AssignReturnTest;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=1
0: bipush 8
2: istore_1
3: bipush 21
5: dup
6: istore_1
7: istore_2
8: return
LineNumberTable:
line 5: 0
line 6: 3
line 7: 8
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 args [Ljava/lang/String;
3 6 1 count I
8 1 2 res I
}
SourceFile: "AssignReturnTest.java"
其中,我们关注的是这一部分:
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=1
0: bipush 8
2: istore_1
3: bipush 21
5: dup
6: istore_1
7: istore_2
8: return
LineNumberTable:
line 5: 0
line 6: 3
line 7: 8
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 args [Ljava/lang/String;
3 6 1 count I
8 1 2 res I
这一部分,即为程序中main()
方法部分的内容。
我们现在详细看一看其中的Code部分中的字节码指令,下面是对于指令我的理解,如有不足还请读者指出:
0: bipush 8 // 将常量8推送至栈顶
2: istore_1 // 将栈顶元素存入标号为1的局部变量(即count),并出栈
3: bipush 21 // 将常量21推送至栈顶
5: dup // 将栈顶元素21复制一份,并压入栈顶
6: istore_1 // 将栈顶元素21存入标号为1的局部变量(即count),并出栈
7: istore_2 // 将栈顶元素21存入标号为2的局部变量(即res),并出栈
8: return // 方法结束
代码行int res = (count = 21);
所对应的即为上面标号3 - 7的指令,即将21入栈,并再复制一份,也压入栈顶,然后按顺序将栈顶的两个21陆续出栈并赋给两个局部变量:count、res,而这与之前我们所分析的赋值规则是相吻合的。
写在最后
因为在平时用python用的颇多,所以也在python中试了一下:
return i = 2
结果,报错。。。。
说明在python中并没有对赋值语句的返回值的支持。