在这篇文章中,我们会尝试建立一个关于变量的抽象模型,足够你可以解释所有在 AP 中会遇到的现象,以及很长一段时间内你编写 Java 代码时会遇到的现象。
如果有更好的表达方式,我会更新在文章中。
一个变量就是一个储存数据的储物柜。
我们在使用这个储物柜之前,先要说清楚 (声明)这个储物柜的
无论什么类型的变量,都用这个抽象模型来理解。
如果再精确一些,其实这些储物柜就是计算机内存中对应某一个内存地址的空间。一个内存地址其实就是一个储物柜的号码。不过内存里的储物柜非常非常多,一个储物柜的编号,也就是内存地址,有可能长这个样子: 0x9A219FEA
。所以声明变量的很重要的意义就是让我可以用变量名来代替这个内存地址。
在声明完一个变量之后,我就可以用简短的有意义的变量名来代替这个又长又难记的内存地址。 我可以说把 38
放进 total
这个柜子,而不需要说把 38
放进 0xEFA8923A
这个柜子。
原始类型比如 int
, boolean
都是有固定的范围的,它们只会占掉很小的一部分固定空间。比如 boolean
只需要一个 bit, int
是 32 个 bit。所以对于原始类型的数据,我们可以放心地把数据直接放在变量这个储物柜中。
但是对象类型就比较麻烦了。比如说,一个 int[]
的变量,我们要储存的数据有可能是10个元素,有可能是1000个元素。再比如说我们自己定义的一个类叫 Teacher
里面有可能有各种各样的属性,每一个属性又相应的要占用掉一些空间。总而言之,对象类型的数据有可能会需要非常非常大的储存空间,这个时候我们的小储物柜肯定是放不下的。
所以我要储存的数据有可能变得很大,塞不下我这个小房间,但是我依然想用这些储物柜来“储存”我的数据。所以,计算机大佬们想出了一个很聪明的办法,在计算机的内存里,除了这些标准储物柜,还有一个很大很大的仓库可以让我可以任意分配空间。我在用这个仓库的时候,我可以随便划出一个我想要的隔间,这个隔间想要多大都可以。
当我需要储存对象类型的数据的时候,我就去大仓库里划出一个隔间,这个隔间也是有编号的。我把数据保存在这个隔间里,然后把隔间的编号储存在原来的储物柜里。 `
发现没有?对象类型和原始类型的最大区别,就是在于变量这个储物柜里面储存的到底是什么:
所以我们说,原始类型储存的是 value
(值),对象类型储存的是 reference
(引用)
==
这个运算符如果遇到了变量,那么它会去检查变量(储物柜)中的内容,无论这个内容的代表的是 value
还是 reference
。比如说 a==b
的意思就是说看看 a
和 b
这两个储物柜里面的内容是否一样。至于这两个储物柜里面是地址,还是真实的数据,对它来说都无所谓,反正本质上都是二机制的数,一个一个 bit 比较一下就完了。
equals
就比较有意思了。如果继承有好好学的话,你会知道 equals
来自于超级父类 Object
。Object
这个类是所有类的爸爸,它的 equals
实现其实跟 ==
是一样的,会直接比较两个储物柜里面的内容。不过有些类就不爽了,比如 String
,它觉得应该去看真实的数据是否一致,所以它就 override
了 equals
方法,不只是比较储物柜里面的内容,而是根据储物柜里面的内存地址,找到真正储存数据的大隔间,然后再去比较大隔间里面的内容是否相等。 这也是为什么强调字符串要用 equals
来比较相等的原因。
xxxxxxxxxx
int a =1;
Teacher dex = new Teacher();
如果=
右边的是原始类型的数据,那么把数据直接塞进储物柜;如果是对象类型的数据,则会把数据的内存地址塞进储物柜
xxxxxxxxxx
int b = a;
Teacher dexter = dex;
如果=
右边的是变量,也就是另一个储物柜,那么不管那个储物柜里面装的东西代表什么,一股脑复制过来就对了。所以对于原始类型来讲,复制的是数据本身;而对于对象类型来说,复制的是数据的引用。
首先我们要区分一下两个概念,实际参数(Actual Parameter)和形式参数(Formal Parameter)。比如:
xxxxxxxxxx
public static void dummy(int a, int[] arr) {...}
public static void main(String[] args) {
int[] data = {1,3,2,4};
dummy(3+2*4, data);
}
3+2*4
和 data
是我们 调用 dummy
时的实际参数;而 a
和 arr
是 定义 dummy
时所规定好的形式参数。
当你调用一个方法的时候,Java 首先会对你的实际参数进行求值(evaluation),然后把结果赋值给形式参数。所以上面的例子相当于
xxxxxxxxxx
a = 11;
arr = data;
你会发现,依然是赋值,所以当调用方法时,原始类型传递的是 value,而对象类型传递的是 reference。这也解释了为什么在方法内部,如果你修改原始类型的参数,方法外部看不到变化;而如果你修改的是对象类型的参数的内部状态,这个变化会生效。比如以下例子
xxxxxxxxxx
public static void change(int a, int[] arr) {
a = 2; // 原始类型的参数,修改了也不会影响这个静态方法以外的地方
arr[0] = 4; // 对象类型的参数,修改了这个对象的内部状态,会生效
}
public static void main(String[] args) {
int num = 1;
int[] data = {1};
change(num, data);
System.out.println(num); //依然是1
System.out.println(data[0]); // 变成了4
}
如果你是用 new
来创建字符串,那它的推理模型跟其他的对象类型是一致的
xxxxxxxxxx
String a = new String("hello"); // 创建了一个新对象
String b = new String("hello"); // 又创建了一个新对象
System.out.println(a==b); // false. == 比较对象类型的时候,在储物柜里面看到的是这两个对象的内存地址(引用)。这两个是不同的对象,内存地址当然不一样了
System.out.println(a.equlas(b)); // true. 不过它们的内容是一样的
不过如果你是用我们平常习惯的 ””
(String literal) 来创建字符串,那就稍微有点不一样了
xxxxxxxxxx
String a = “hello”;
String b = “hello”;
System.out.println(a==b); // true
System.out.println(a.equals(b)); // true
为什么 b
和 a
是同一个对象呢?因为字符串是一个非常非常常用的数据类型,设计者认为同样内容的字符串很有可能会多次出现。所以当你以 String Literal (也就是双引号) 的方式去创建字符串时,Java 会先去看看内存里有没有内容一样的字符串对象,如果有的话就不给你创建新的,直接用旧的,以此来提高性能。
不过这个行为并不在语言标准之内,是由不同的编译器实现决定的,所以不要依赖这个行为来作为判断依据。
这一节讲了这么多,目的是让你知道为什么不应该用 ==
去比较 String
的相等性
理解了这个模型,你在很长一段时间内都应该够用,能够自己去解释很多不同的现象了。
但是要记得
所有的模型都是对世界的简化
意思是,从某个角度说,任何模型都不是完全正确的。当你需要理解和利用更深层次的计算机原理时(比如你要去设计编译器),这个模型就不够用了,那时候你会需要用一个更准确的模型来描述变量和数据的关系。
请把你对上述任何概念和内容的新理解,或者对例子和解释的疑问,写在下面的评论区中。