CS61B学习笔记(四)--Reading2.1-列表-海象之谜
什么是海象之谜?
尝试预测当我们运行下面的代码时会发生什么。更改为 b 会影响 a 吗,更改为 x 会影响 y 吗?提示:如果你来自 Python,Java 也有相同的行为。
1 | public class PollQuestions { |
答案:
位
计算机中的所有信息都以 1 和 0 的序列形式存储在内存中。一些例子:
- 72 通常存储为 01001000
- 205.75 通常存储为 01000011 01001101 11000000 00000000
- 字母 H 通常存储为 01001000(与 72 相同)
- 真实值通常存储为00000001
一个有趣的观察结果是,72 和 H 都存储为 01001000。这就提出了一个问题:一段 Java 代码如何知道如何解释 01001000?
答案是通过类型
声明变量(简体)
您可以将计算机视为包含大量用于存储信息的内存位,每个内存位都有一个唯一的地址。现代计算机可以使用数十亿个这样的比特。
当你声明某种类型的变量时,Java 会找到一个连续的块,该块的位恰好足够容纳该类型的事物。例如,如果声明一个 int,则会得到一个 32 位的块。如果声明一个字节,则会得到一个 8 位的块。Java 中的每种数据类型都包含不同数量的位。在这个类中,确切的数字对我们来说并不是很重要。
为了方便的比喻,我们将其中一个块称为比特的“盒子”。
除了留出内存之外,Java 解释器还在内部表中创建一个条目,该条目将每个变量名称映射到框中第一个位的位置。
例如,如果声明 int x
了 和 double y
,则 Java 可能会决定使用计算机内存的 352 到 384 位来存储 x,并使用 20800 到 20864 位来存储 y。然后,解释器将记录 int x 从位 352 开始,y 从位 20800 开始。例如,在执行代码后:
1 | int x; |
我们最终会得到尺寸为 32 和 64 的盒子,如下图所示:
声明变量时,Java 不会将任何内容写入保留框中。换句话说,没有默认值。因此,Java 编译器会阻止您使用变量,直到使用 =
运算符将位填充到框中。出于这个原因,我避免在上图的框中显示任何位。
当您为内存盒赋值时,它将填充您指定的位。例如,如果我们执行以下行:
1 | x = -1431195969; |
简化的框表示法
虽然我们在上一节中使用的框表示法对于理解引擎盖下发生的事情非常有用,但它对实际目的没有用,因为我们不知道如何解释二进制位。
因此,我们将用人类可读的符号来编写它们,而不是用二进制文件编写内存盒内容。我们将在课程的其余部分这样做。例如,在执行以下操作后:
1 | int x; |
我们可以使用我称之为简化框表示法来表示程序环境,如下所示:
黄金平等法则 (GRoE)
现在有了简化的盒子符号,我们终于可以开始解开海象之谜了。
事实证明,我们的 Mystery 有一个简单的解决方案:当你编写 y = x
时,你告诉 Java 解释器将 x 中的位复制到 y 中。在理解我们的海象之谜时,这条平等的黄金法则 (GRoE) 是所有真理的根源。
1 | int x = 5; |
因此,Java的复制操作是复制位
引用类型
上面,我们说了 8 种原始类型:byte、short、int、long、float、double、boolean、char。其他所有内容,包括数组,都不是原始类型,而是 reference type
.
当我们使用 new
(例如 Dog、Walrus、Planet)实例化一个 Object 时,Java 首先为类的每个实例变量分配一个框,并用默认值填充它们。然后,构造函数通常(但并非总是)用其他值填充每个框。
引用类型可能都需要new,这可能意味着引用变量声明的其实是类似于cpp中的指针?
引用变量声明
当我们声明任何引用类型(Walrus、Dog、Planet、数组等)的变量时,无论什么类型的对象,Java 都会分配一个 64 位的盒子。
乍一看,这似乎导致了海象悖论。上一节中的 Walrus 需要超过 64 位来存储。此外,无论对象的类型如何,我们只能获得 64 位来存储它,这似乎很奇怪。
但是,通过以下信息可以轻松解决此问题:64 位框不包含有关海象的数据,而是内存中海象的地址。
类似cpp中的指针,指针变量存储的是指针指向的地址,通常只有4个字节;而指针所指向的那个地址不一定存储了4个字节。
框和指针表示法
和以前一样,很难解释引用变量中的一堆位,因此我们将为引用变量创建一个简化的框表示法,如下所示:
- 如果一个地址全为零,我们将用 null 表示它。
- 非零地址将由指向对象实例化的箭头表示。
这有时也称为“框和指针”表示法。
对于上一节中的示例,我们将有:
参数传递
当您将参数传递给函数时,您也只是在复制位。换言之,GRoE 也适用于参数传递。复制位通常称为“按值传递”。在 Java 中,我们总是按值传递。
例如,请考虑以下函数:
1 | public static double average(double a, double b) { |
假设我们调用这个函数,如下所示:
1 | public static void main(String[] args) { |
实际上我们是将x,y的值拷贝给了a, b。
列表的实例化
如上所述,存储数组的变量是引用变量,就像任何其他变量一样。例如,请考虑以下声明:
1 | int[] x; |
这两个声明都创建了 64 位的内存盒。 x
只能保存数组的地址,并且 planets
只能保存 Planet
int
数组的地址。
实例化数组与实例化对象非常相似。例如,如果我们创建一个大小为 5 的整数数组,如下所示:
1 | x = new int[]{0, 1, 2, 95, 4}; |
然后,关键字 new
创建 5 个框,每个框 32 位,并返回整个对象的地址以分配给 x。
破碎的被褥法则
我感觉这个很有意思:
1 | 在大学里,我和我的室友从一些朋友那里买了一个二手被褥。他们住在一楼;我们住在四楼。他们很好心,他们帮我们抬上楼梯。 |
一时半解虽然这在短期内可能很好,但从长远来看,在没有完全理解的情况下做问题可能会注定你以后会失败。有一篇关于这个所谓的破碎被褥法则的博客文章,你可能会觉得很有趣。
IntLists
事实证明,一个非常基本的列表实现起来是微不足道的,如下所示:
实现起来其实类似于”链表”
1 | public class IntList { |