关于变量你所需要知道的真相

在这篇文章中,我们会尝试建立一个关于变量的抽象模型,足够你可以解释所有在 AP 中会遇到的现象,以及很长一段时间内你编写 Java 代码时会遇到的现象。

如果有更好的表达方式,我会更新在文章中。

什么是变量

一个变量就是一个储存数据的储物柜

我们在使用这个储物柜之前,先要说清楚 (声明)这个储物柜的

  1. 名字 (变量名)
  2. 储存什么类型的数据 (变量类型)

无论什么类型的变量,都用这个抽象模型来理解。

如果再精确一些,其实这些储物柜就是计算机内存中对应某一个内存地址的空间。一个内存地址其实就是一个储物柜的号码。不过内存里的储物柜非常非常多,一个储物柜的编号,也就是内存地址,有可能长这个样子: 0x9A219FEA。所以声明变量的很重要的意义就是让我可以用变量名来代替这个内存地址。

在声明完一个变量之后,我就可以用简短的有意义的变量名来代替这个又长又难记的内存地址。 我可以说把 38 放进 total 这个柜子,而不需要说把 38 放进 0xEFA8923A这个柜子。

原始类型和对象类型的区别

原始类型比如 int, boolean 都是有固定的范围的,它们只会占掉很小的一部分固定空间。比如 boolean 只需要一个 bit, int 是 32 个 bit。所以对于原始类型的数据,我们可以放心地把数据直接放在变量这个储物柜中。

但是对象类型就比较麻烦了。比如说,一个 int[] 的变量,我们要储存的数据有可能是10个元素,有可能是1000个元素。再比如说我们自己定义的一个类叫 Teacher 里面有可能有各种各样的属性,每一个属性又相应的要占用掉一些空间。总而言之,对象类型的数据有可能会需要非常非常大的储存空间,这个时候我们的小储物柜肯定是放不下的。

动机和解决方案

所以我要储存的数据有可能变得很大,塞不下我这个小房间,但是我依然想用这些储物柜来“储存”我的数据。所以,计算机大佬们想出了一个很聪明的办法,在计算机的内存里,除了这些标准储物柜,还有一个很大很大的仓库可以让我可以任意分配空间。我在用这个仓库的时候,我可以随便划出一个我想要的隔间,这个隔间想要多大都可以。

当我需要储存对象类型的数据的时候,我就去大仓库里划出一个隔间,这个隔间也是有编号的。我数据保存在这个隔间里,然后把隔间的编号储存在原来的储物柜里。 `

发现没有?对象类型和原始类型的最大区别,就是在于变量这个储物柜里面储存的到底是什么:

  1. 对于原始类型来说,变量中储存的是真正的数据
  2. 对于对象类型来说,变量中储存的是个编号(内存地址),而这个编号对应的隔间里储存的才是真正的数据

所以我们说,原始类型储存的是 value (值),对象类型储存的是 reference (引用)

== vs. equals

== 这个运算符如果遇到了变量,那么它会去检查变量(储物柜)中的内容,无论这个内容的代表的是 value 还是 reference。比如说 a==b 的意思就是说看看 ab 这两个储物柜里面的内容是否一样。至于这两个储物柜里面是地址,还是真实的数据,对它来说都无所谓,反正本质上都是二机制的数,一个一个 bit 比较一下就完了。

equals 就比较有意思了。如果继承有好好学的话,你会知道 equals 来自于超级父类 ObjectObject这个类是所有类的爸爸,它的 equals 实现其实跟 ==是一样的,会直接比较两个储物柜里面的内容。不过有些类就不爽了,比如 String,它觉得应该去看真实的数据是否一致,所以它就 overrideequals 方法,不只是比较储物柜里面的内容,而是根据储物柜里面的内存地址,找到真正储存数据的大隔间,然后再去比较大隔间里面的内容是否相等。 这也是为什么强调字符串要用 equals 来比较相等的原因。

赋值语句到底赋的是个啥

如果=右边的是原始类型的数据,那么把数据直接塞进储物柜;如果是对象类型的数据,则会把数据的内存地址塞进储物柜

如果= 右边的是变量,也就是另一个储物柜,那么不管那个储物柜里面装的东西代表什么,一股脑复制过来就对了。所以对于原始类型来讲,复制的是数据本身;而对于对象类型来说,复制的是数据的引用。

调用静态/非静态方法的时候参数到底是如何传递的

首先我们要区分一下两个概念,实际参数(Actual Parameter)和形式参数(Formal Parameter)。比如:

3+2*4data 是我们 调用 dummy 时的实际参数;而 aarr定义 dummy 时所规定好的形式参数。

当你调用一个方法的时候,Java 首先会对你的实际参数进行求值(evaluation),然后把结果赋值给形式参数。所以上面的例子相当于

你会发现,依然是赋值,所以当调用方法时,原始类型传递的是 value,而对象类型传递的是 reference。这也解释了为什么在方法内部,如果你修改原始类型的参数,方法外部看不到变化;而如果你修改的是对象类型的参数的内部状态,这个变化会生效。比如以下例子

为什么 String 是特别的,以及怎么个特别法

如果你是用 new 来创建字符串,那它的推理模型跟其他的对象类型是一致的

不过如果你是用我们平常习惯的 ”” (String literal) 来创建字符串,那就稍微有点不一样了

为什么 ba 是同一个对象呢?因为字符串是一个非常非常常用的数据类型,设计者认为同样内容的字符串很有可能会多次出现。所以当你以 String Literal (也就是双引号) 的方式去创建字符串时,Java 会先去看看内存里有没有内容一样的字符串对象,如果有的话就不给你创建新的,直接用旧的,以此来提高性能。

不过这个行为并不在语言标准之内,是由不同的编译器实现决定的,所以不要依赖这个行为来作为判断依据。

这一节讲了这么多,目的是让你知道为什么不应该用 == 去比较 String 的相等性

最后

理解了这个模型,你在很长一段时间内都应该够用,能够自己去解释很多不同的现象了。

但是要记得

所有的模型都是对世界的简化

意思是,从某个角度说,任何模型都不是完全正确的。当你需要理解和利用更深层次的计算机原理时(比如你要去设计编译器),这个模型就不够用了,那时候你会需要用一个更准确的模型来描述变量和数据的关系。

请把你对上述任何概念和内容的新理解,或者对例子和解释的疑问,写在下面的评论区中。