2.1.Java语言特性

\(2.1\) \(Java\)语言特性

1.比特(\(bit\))

  在计算机中,所有的信息都\(01\)串的形式存储于内存中,例如:

  • \(72\)被存储为01001000
  • \(H\)字母被存储为01001000

  \(72\)\(H\)\(01\)串是相同的,\(Java\)代码通过数据类型(\(tyep\))区分它们:

1
2
3
4
char c='H';
int x=c;
System.out.println(c);
System.out.println(x);

1
2
H
72

  在这个例子中,xc变量都有相同的\(01\)串,但解释器会在打印它们的时候对其进行不同的处理。

  在\(Java\)中,有\(8\)种基本数据类型:byteshortintlongfloatdoublebooleanchar

2.变量的声明(简化)

  我们可以将计算机想象成一个一个拥有大量记忆比特(\(memory\;bits\))来存储信息的机器,每个比特都有独立的地址

  在声明某一基本数据类型的变量时,\(Java\)找到一个区域(\(blocks\)),这个区域有足够的比特来存储该类型的信息。我们可以把这些区域比喻为 一个装着比特的盒子(\(box\))

  除了安置内存,\(Java\)解释器还会创造一个内部表格的入口(\(entry\)),它将每个变量的名字与它们对应盒子的第一个比特相对应(\(map\))

  例如,当我们声明int xdouble y这两个变量时,\(Java\)可能会决定用内存中的第\(352\)到第\(384\)位比特来存储x,用第\(20800\)到第\(20864\)位比特来存储y。解释器会接着记录x从第\(352\)位比特开始存储,y从第\(20800\)位比特开始存储。在执行下面的代码后:

1
2
int x;
double y;

  我们会得到下面两个\(32\)位和\(64\)位的盒子:

  \(java\)语言并没有像\(C\)语言一样提供访问盒子地址的途径。这虽然阻止了某些类型优化,但也防止了一大类非常棘手的编程错误。

  在变量声明后,\(Java\)并不会在预留的盒子里写入任何默认的东西,也就是说,每个变量并没有默认值。因此,在这个盒子被通过=运算符装满比特之前,\(Java\)解释器会阻止我们使用这个变量。而当我们对内存盒子(\(memory\;box\))进行赋值后,这个盒子就会装入我们指定的值

  例如,当我们执行下面的程序时:

1
2
x = -1431195969;
y = 567213.112;

  每个变量的内存盒子会像下面这样被填满。这种标记被叫做盒子标记法(\(box\;notation\)):

  为了简化,我们将盒子标记简化为如下格式:

3.等式的黄金规则(\(GRoE\))

  当我们写下y=x,我们在告诉\(Java\)解释器把x的比特复制给y。这个规则是理解下面这个谜题的基础:

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);

4.引用类型(\(reference\;type\))

  在之前我们介绍了\(Java\)的基本类型,除了基本类型外的其他类型,如数组,被称为引用类型

  \(a.\)对象实例化

  当我们利用new语句将某个对象实例化(\(instantiate\))后,\(Java\)会先给这个类(\(class\))的每个实例变量分配一个盒子,然后在盒子里面装一个默认值

  例如,当我们的\(Warlus\)类如下:

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;
}
}

  并且我们创造了一个实例new Walrus(1000,8.3),那么我们就会得到下面这两个独立的盒子:

  \(b.\)引用类型变量的声明

  我们上面所举的例子是匿名的实例,它没有被存储到一个变量中。

  当我们声明任何一个引用类型的变量时,\(Java\)会给这个变量分配一个\(64\)比特的盒子,不管这个对象是什么类型的。这是因为:这个\(64\)比特的盒子并不是装这个对象(例如\(Walrus\))的具体数据的,而是装这个对象在内存中的地址

  例如,当我们如下调用变量时:

1
2
Walrus someWalrus;
someWalrus = new Walrus(1000, 8.3);

  我们假设\(Walrus\)weight存储在开始于5051956592385990207的比特里,而tuskSize存储在开始于5051956592385990239的比特里,我们会5051956592385990207存储在\(Walrus\)变量中

  我们也可以对引用变量赋予特殊值null

  这样表示引用变量的比特较为复杂,因此我们引入箭头标记:

  • 如果一个地址全部为空,就用null来表示。
  • 如果一个地址非空,就用箭头指向它所代指的对象实例

  回到下面这个问题:

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);

  执行完下面的语句后,可以得到下面的标记:

1
2
3
Walrus a = new Walrus(1000, 8.3);
Walrus b;
b = a;

5.参数传递

  当我们给一个函数传递参数时,我们也只是将参数的比特复制过去\(GRoE\)原则同样适用于参数的传递。比特复制也被称作传递数值(\(passing\;by\;value\))。在\(Java\)中,我们经常使用这种做法。

  注意到,对于基本类型变量,比特表示的是它们的数值而非地址;而对于引用类型的变量,比特表示的是它们的地址,因此函数对两者的作用并不相同

  考虑下面这个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class PassByValueFigure {
public static void main(String[] args) {
Walrus walrus = new Walrus(3500, 10.5);
int x = 9;

doStuff(walrus, x);
System.out.println(walrus);
System.out.println(x);
}

public static void doStuff(Walrus W, int x) {
W.weight = W.weight - 100;
x = x - 5;
}
}
  该例子对应的标记图如下:

  可以看到,\(doStuff\)只对\(Walrus\)的对象有作用,对x并无作用。

6.数组的实例化

  像先前对引用变量的讲解,用于存储数组的变量也遵循相同的规则。例如下面这个整数数组的创建:

1
x = new int[]{0, 1, 2, 95, 4};

  这里的new关键词会创建\(5\)\(32\)比特的盒子,并返回赋值给x的整个对象的地址。

  如果丢失了和对象的地址相关的比特,对象可能被遗失。例如如果上例的x是右侧数组唯一的备份(\(copy\)),那么x=null的赋值语句会导致原先数组的丢失。这并非坏事,因为有时我们的确需要遗忘一些对象。

7.整型链表(\(IntList\))

  \(a.\)整型链表的构建

  类似\(python\)中的\(Linked\;list\),可以用递归的方式实现初步的整型链表:

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;
}
}

  这种形式的链表使用起来非常不方便、容易出错,之后我们将会通过在类中添加辅助方法(\(helper\;method\))的面向对象编程策略来优化它:

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

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

  \(b.\)\(Size\)函数的递归与迭代实现

  整型链表的\(Size\)函数的递归实现如下:

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();
}

  这里有一个要点:为什么递归的基本条件不写if(this == null)呢?问题出在L == null这种特殊情况,我们不能对一个null对象使用实例方法。

  \(Size\)函数的迭代实现如下:

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;
}

  需要注意,在迭代时我们需要一个指针p,因为在\(java\)中,我们不能重新分配(\(reassign\))this

thisrefers to the current object. It's like saying: "I am" or "my name is". You can't change who you are.