【CS61B】4. References, Recursion, and Lists

数组在 Java 中具有固定大小,永远无法更改

比特 Bits

计算机中的所有信息都以 1 和 0 的序列存储在内存中

一个例子

  • 72 通常存储为 01001000
  • 字母 H 通常存储为 01001000(与 72 相同)

变量类型

这二者的存储值相同, 那 Java 的代码如何知道如何解释 01001000 呢?
答案是通过变量类型

Java 中的 8 种原始类型:
byte, short, int, long, float, double, boolean, char

声明变量

  • 当声明某种类型的变量时, Java 会找到内存中一个连续的块, 其恰好足以容纳该类型的数据
  • 留出内存之外, Java 会在内部表中创建一个条目, 将每个变量名映射到每个块中的第一位
    • 感觉跟指针很像
  • Java 中无法知道内存块的具体位置, 和 C 不同
  • Java 中没有默认值

等号的黄金法则(GRoE)

在 Java 里,所有参数传递都是按值传递(pass-by-value)
区别在于:

  • 基本类型:传递的是数值本身
  • 对象类型:传递的是“对象引用”的值(即对象的地址)

引用类型 Reference Type

Java 中的 8 种原始类型:
byte, short, int, long, float, double, boolean, char

上面提到了原始类型, 任何原始类型之外的其他内容都不是原始类型, 被称为引用类型 reference type

对象实例化

当使用new实例化一个对象时, Java 为类的每个实例变量分配一定的空间, 并用默认值填充
随后, 构造函数用输入值填充对应的空间

例如:

1
2
3
4
5
6
7
8
9
public static class Walrus {
    public int weight;
    public double tuskSize;

    public Walrus(int w, double ts) {
          weight = w;
          tuskSize = ts;
    }
}

其中 int 占 32 bits, double 占 64bits, 一共 96bits
但是在 Java 实现中, 任意对象都会有额外开销, 因此占用空间会略多于 96bits

引用变量声明

当声明任何引用类型的变量时, Java 都会分配 64bits 的空间

但这里的 64bits 不是用来存储变量的数据, 而是用于存储变量的地址

海象之谜

其实是最开始提的一个小问题: 对 b 的更改会影响 a 吗?

1
2
3
4
5
6
Walrus a = new Walrus(1000, 8.3);
Walrus b;
b = a;
b.weight = 5;
System.out.println(a);
System.out.println(b);

../../source/CS61B4. References, Recursion, and Lists\_walrus.png

  • b 被声明为一个 Walrus 类型的变量
  • b = a; 的意思是:把 a 里面存的那个“引用地址”复制一份交给 b
  • 所以现在 ab 指向同一个 Walrus 对象。堆里还是只有一只“海象”
  • 所以更改 b 中的变量值, a 也会跟着改动

参数传递

当把参数传递给一个函数时, 只是在复制 bits, 被称为按值传递

  • Java 永远是 值传递

  • 基本类型:复制数值 → 方法里改不影响外部

  • 对象类型:复制的是引用地址 → 可以改对象内容,但不能改掉外部引用

数组的实例化

存储数组的变量与其他变量一样是引用变量

1
2
int[] x;
Planet[] planets;

这里的两个声明创造了两个 64bits 的空间:

  • x 只能保存 int 数组的地址
  • planets 只能保存 Planet 数组的地址
1
x = new int[]{0, 1, 2, 95, 4};

new 关键字创建 5 个框,每个框 32 位,并返回整个对象的地址以分配给 x

如果丢失了对象地址的那位, 可能导致对象丢失:
若某一个实例的地址存在 x 中, 在 x=null 后会丢失这个实例

整数列表 IntList

1
2
3
4
5
6
7
8
9
public class IntList {
    public int first;
    public IntList rest;        

    public IntList(int f, IntList r) {
        first = f;
        rest = r;
    }
}

然而, 使用这种链表时, 代码会很丑陋:

1
2
3
IntList L = new IntList(5, null);
L.rest = new IntList(10, null);
L.rest.rest = new IntList(15, null);

这是在构造一个[ 5, 10, 15 ] 这样的链表, 每次通过调用 rest 方法并赋值实现延长

1
2
3
IntList L = new IntList(15, null);
L = new IntList(10, L);
L = new IntList(5, L);

这是优化后的构造方法, 但仍然比较麻烦

size 和 iterativeSize

递归方法

我们希望实现一个方法, 用来得到列表 L 中的元素数量

1
2
3
4
5
6
7
/** Return the size of the list using... recursion! */
public int size() {
    if (rest == null) {
        return 1;
    }
    return 1 + this.rest.size();
}

以下是使用递归实现的一种查询 size 大小的方法, 其实跟 cs61a 中的很像 hhh

关于递归: 我的想法

借着讲到递归, 梳理一下我对递归的一些认识吧, 主要是在 CS61A 的学习过程中建立的:

  1. 确定递归间前后状态的关系: 二者相加, 相乘, 多者相加…..
  2. 确定递归终止状态, 比如上面的 null 时, 并给出终止态时的返回值
  3. 根据 1, 设计返回值的具体迭代计算方法

其实似乎也没说啥

迭代方法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
/** Return the size of the list using no recursion! */
public int iterativeSize() {
    IntList p = this;
    int totalSize = 0;
    while (p != null) {
        totalSize += 1;
        p = p.rest;
    }
    return totalSize;
}

和递归最大的不同是, 迭代是通过 while 来实现的, 虽然判断条件相同
但是递归就是通过嵌套, 调用函数实现的
这里需要先把 this 赋值给 p, 然后后续通过让 p 自己赋值为p.rest 实现不断迭代

get

这里我们想再实现一个功能: 根据 index , 获得列表中对应的元素

1
2
3
4
5
6
public int get(int i){
	if(i==0){
		return first;
	}
	return rest.get(i - 1);
}

这里用递归的方法实现

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计