CS61B 课程笔记(Lecture 03)
CS61B 课程笔记(Lecture 03)
Java 内存与变量
比特与基本类型
- 比特 (Bits): 内存中的信息存储为一和零的序列。
- Java 中的 8 种基本数据类型:
byte
,short
,int
,long
,float
,double
,boolean
,char
。- 基本类型 (Primitives): 这些是数据的简单表示形式,每种类型在内存中都有固定的大小。
声明变量(简化版)
- 当声明一个变量时,Java 会:
- 分配足够的比特来存储该类型的数据。
- 在内部表中将变量名映射到内存位置。
- 不会自动为变量赋值或初始化内存中的内容。
- 关键规则:如果访问未初始化的变量,会导致错误,这是为了安全。
Java 内存抽象
- 隐藏内存地址:Java
隐藏了具体的内存地址,防止手动管理内存。
- 优点:减少编程错误(例如内存泄漏、段错误)。
- 缺点:降低了对底层内存优化的控制能力。
- 过早优化:引用 Donald Knuth 的话,过早关注小的效率优化往往是不明智的。类似于不能手动控制心跳,避免人为操作带来的风险。
等号的黄金法则 (Golden Rule of Equals, GRoE)
- 赋值
(
=
):将一个变量的所有比特从源复制到目标。 - 对于基本类型:这意味着直接复制值。
等号的黄金法则 (Golden Rule of Equals, GRoE)详解
在 Java 中,等号 (
=
) 的作用是将一个变量的值从源复制到目标,而这一过程的背后实质是比特的复制。我们可以通过理解“等号的黄金法则”更深入地理解赋值操作的原理。基本类型 (Primitive Types)
基本类型包括以下几种数据类型:
byte
: 8 比特 (bit)short
: 16 比特int
: 32 比特long
: 64 比特float
: 32 比特double
: 64 比特boolean
: 虽然表示为true
或false
,但通常用 1 比特表示。char
: 16 比特每种基本类型在内存中都占据了固定大小的比特空间。当我们对这些基本类型进行赋值时,实质上是将这些比特从一个内存位置复制到另一个内存位置。
赋值操作的背后机制
声明变量时,计算机会根据变量的数据类型在内存中分配足够的空间。比如,声明
int x;
,系统会在内存中为x
分配 32 比特的空间。赋值操作时,如
x = 42;
,系统会将表示数字 42 的比特模式写入x
对应的内存空间。赋值=
的作用是把源值的比特逐一复制到目标变量的内存空间中。例如:
1
2 int x = 42;
int y = x; // y 获得 x 中的 32 比特值在这个例子中,
y = x
的操作就是将x
所对应的 32 比特从x
的内存地址复制到y
的内存地址。这是一个完全独立的复制,因此x
和y
各自拥有自己的内存空间,修改y
的值不会影响x
,反之亦然。比如:
1
2 y = 100;
System.out.println(x); // 输出仍然是 42等号的黄金法则与基本类型
“等号的黄金法则”表明,在赋值时,只是比特的复制,这意味着赋值不会影响源变量本身。即使目标变量改变,源变量的比特值保持不变。这一点对于基本类型来说尤为重要,因为这些类型在赋值时总是复制实际的值,而不是引用。
复制比特的影响
内存独立性
由于基本类型的赋值是将比特复制到一个独立的内存区域,因此在进行赋值操作后,两个变量各自独立。修改一个变量不会影响另一个变量。例如:
1
2
3
4 int a = 10;
int b = a;
b = 20;
System.out.println(a); // 输出 10这里,
a
和b
是两个独立的变量,尽管b
是从a
中复制的值。但赋值后,它们之间没有任何内存上的联系。性能与效率
对于基本类型的赋值操作,由于这些类型的数据量通常较小(如
int
占用 32 比特),所以比特的复制是非常高效的。现代计算机硬件可以快速进行这些固定长度比特的复制操作。常见的误解
初学者常常会将基本类型的赋值操作与引用类型混淆,以为修改了一个变量的值,另一个变量也会随之改变。实际上,基本类型的赋值是值的复制,与引用类型的赋值机制不同。
Java 中的引用类型
引用类型基础
引用类型存储的是指向内存中对象的64 位地址,而不是对象本身的数据。
声明示例:
1
2Walrus someWalrus; // 声明了对 Walrus 对象的引用。
someWalrus = new Walrus(1000, 8.3); // `new` 操作符创建对象并返回其内存地址。关键点:变量
someWalrus
保存的是引用(地址),而不是实际的 Walrus 数据。
未定义 vs. 空值
- 引用类型的变量可以是未定义的(即已声明但未初始化)。
- 空值 (null):明确表示该引用指向的是“空”,即不指向任何对象。
参数传递(值传递)
- 在 Java
中,参数传递是值传递:变量的比特(无论是基本类型还是引用类型)都会被复制到参数变量中。
- 即使是对象,也是传递对象的地址,而不是实际对象本身。
数组与实例化
声明数组
- 声明数组(例如
int[] x;
)会创建一个指向数组的64 位引用。 new
操作符为数组分配内存,并返回其地址。- 数组大小:在创建时固定,无法更改。
对象丢失
- 如果没有任何引用指向一个对象,该对象就会被视为丢失,可能会被垃圾回收器回收。
- 当对象不再需要时,可以安全地丢弃引用。
链表数据结构:IntList
IntList 定义
IntList
是一个自定义的整数链表。每个
IntList
节点包含两部分:first
: 保存一个整数值。rest
: 指向下一个节点(另一个IntList
)。
1
2
3
4
5
6
7
8
9public class IntList {
public int first;
public IntList rest;
public IntList(int f, IntList r) {
first = f;
rest = r;
}
}
创建链表
创建一个包含数字(如 5、10、15)的链表:
1
2
3IntList L = new IntList(5, null);
L.rest = new IntList(10, null);
L.rest.rest = new IntList(15, null);另一种方法:反向构建链表,代码更简洁但不易理解:
1
2
3IntList L = new IntList(15, null);
L = new IntList(10, L);
L = new IntList(5, L);
IntList
方法
递归的 size()
方法
递归:使用基准情况(
rest == null
)来终止递归。1
2
3
4
5
6public int size() {
if (rest == null) {
return 1;
}
return 1 + this.rest.size();
}
迭代的 iterativeSize()
方法
迭代:使用指针变量 (
p
) 遍历链表。1
2
3
4
5
6
7
8
9public int iterativeSize() {
IntList p = this;
int totalSize = 0;
while (p != null) {
totalSize += 1;
p = p.rest;
}
return totalSize;
}注意:使用
p
作为指针可以避免重写this
,这是在 Java 中不可行的。
最后说明
破旧沙发法则 (The Law of the Broken Futon)
- 这一概念强调了理解 Java 中内存与变量处理的核心机制的重要性。
- 深刻理解:如果没有真正掌握内存和引用的细微差别,表面的知识可能会导致后续出现错误,例如没有充分利用面向对象的特性,或者误解引用传递的工作原理。
破旧沙发法则 (The Law of the Broken Futon) 详解
破旧沙发法则是一种用于解释学生在学习编程语言时,尤其是像 Java 这样抽象程度较高的语言,可能会遇到的理解困境的比喻。
比喻背景
这个法则把学生的编程理解比作一张“破旧的沙发”。当一个人看到一张破旧的沙发时,可能会觉得它还能用,并且在短期内确实能够坐在上面。但实际上,沙发已经有了问题,如果不彻底修理或者更换,它很可能会随着时间的推移变得越来越不稳定,最终崩塌。
编程中的破旧沙发
在编程的学习过程中,许多学生往往会因为浅层的理解而觉得自己掌握了某些概念,但其实这种理解是不完整的。这种情况下,虽然他们能够写出表面上可行的代码,但背后涉及的机制或者原理并没有真正掌握。
例如,在 Java 中处理引用类型和内存时,初学者可能只是知道赋值语句
=
将数据从一个变量复制到另一个变量,但没有意识到这是复制引用(地址),而不是复制实际的数据。这样虽然代码能跑,但在遇到复杂场景时,他们会无法理解为什么程序会出现内存泄漏或对象丢失等问题。这种“半吊子”的理解就像是一张已经松动的沙发,表面上还能坐,但在关键时刻(例如遇到更复杂的编程任务或 Bug 时),整个认知体系就可能会崩溃。
短期和长期影响
- 短期影响:学生可能会觉得自己的代码能正常工作,没有任何问题,似乎已经掌握了这个知识点。这种表面上的成功使得他们在短期内可能通过考试或完成任务。
- 长期影响:当学生需要处理更复杂的问题时(如调试多线程程序、优化内存使用或实现自定义数据结构),他们将会发现自己对底层机制的理解不足,导致解决问题时困难重重。这种不全面的理解就像沙发最终会坍塌一样,在长远的编程道路上会阻碍他们的发展。
为什么要避免“破旧沙发”
编程不仅仅是写出能运行的代码,还需要真正理解代码背后的逻辑和机制。深入理解内存管理、对象引用、数据结构等核心知识,能够帮助程序员避免在更复杂的环境下出错。破旧沙发法则的核心警示是:不要仅仅满足于表面的成功,要确保自己真正掌握了背后的原理。
通过扎实的学习基础,学生能够写出更稳定、高效、可靠的代码,并能够应对更具挑战性的编程任务。
实际案例:引用类型误解
问题场景:学生可能会误解引用类型的赋值,以为两个变量之间的赋值是复制了对象的实际数据。
例如:
1
2
3 Walrus a = new Walrus(1000, 8.3);
Walrus b = a;
b.weight = 2000;学生可能以为
a
和b
是两个独立的对象,但实际上它们是指向同一个Walrus
对象,修改b
的值也会影响a
,这就导致了潜在的逻辑错误。理解不深的后果:在大型项目中,如果开发者不理解引用和内存的细节,他们可能会无意中修改同一个对象的多个引用,导致难以跟踪的 Bug 和错误。
破旧沙发法则提醒我们不要满足于能“跑起来”的代码,要深刻理解程序的运作方式,特别是涉及到复杂内存管理、引用和对象时。只有这样,才能在编程的长远道路上避免“沙发坍塌”的灾难。