2.1.Java语言特性
\(2.1\) \(Java\)语言特性
1.比特(\(bit\))
在计算机中,所有的信息都以\(01\)串的形式存储于内存中,例如:
- \(72\)被存储为
01001000
- \(H\)字母被存储为
01001000
\(72\)和\(H\)的\(01\)串是相同的,\(Java\)代码通过数据类型(\(tyep\))区分它们: 1
2
3
4char c='H';
int x=c;
System.out.println(c);
System.out.println(x);
1 | H |
在这个例子中,x
和c
变量都有相同的\(01\)串,但解释器会在打印它们的时候对其进行不同的处理。
在\(Java\)中,有\(8\)种基本数据类型:byte
,short
,int
,long
,float
,double
,boolean
,char
。
2.变量的声明(简化)
我们可以将计算机想象成一个一个拥有大量记忆比特(\(memory\;bits\))来存储信息的机器,每个比特都有独立的地址。
在声明某一基本数据类型的变量时,\(Java\)会找到一个区域(\(blocks\)),这个区域有足够的比特来存储该类型的信息。我们可以把这些区域比喻为 一个装着比特的盒子(\(box\))。
除了安置内存,\(Java\)解释器还会创造一个内部表格的入口(\(entry\)),它将每个变量的名字与它们对应盒子的第一个比特相对应(\(map\))。
例如,当我们声明int x
和double y
这两个变量时,\(Java\)可能会决定用内存中的第\(352\)到第\(384\)位比特来存储x
,用第\(20800\)到第\(20864\)位比特来存储y
。解释器会接着记录x
从第\(352\)位比特开始存储,y
从第\(20800\)位比特开始存储。在执行下面的代码后:
1
2int x;
double y;
我们会得到下面两个\(32\)位和\(64\)位的盒子:
\(java\)语言并没有像\(C\)语言一样提供访问盒子地址的途径。这虽然阻止了某些类型优化,但也防止了一大类非常棘手的编程错误。
在变量声明后,\(Java\)并不会在预留的盒子里写入任何默认的东西,也就是说,每个变量并没有默认值。因此,在这个盒子被通过=
运算符装满比特之前,\(Java\)解释器会阻止我们使用这个变量。而当我们对内存盒子(\(memory\;box\))进行赋值后,这个盒子就会装入我们指定的值。
例如,当我们执行下面的程序时: 1
2x = -1431195969;
y = 567213.112;
每个变量的内存盒子会像下面这样被填满。这种标记被叫做盒子标记法(\(box\;notation\)):
为了简化,我们将盒子标记简化为如下格式:
3.等式的黄金规则(\(GRoE\))
当我们写下y=x
,我们在告诉\(Java\)解释器把x
的比特复制给y
。这个规则是理解下面这个谜题的基础:
1
2
3
4
5
6Walrus 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
9public 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
2Walrus someWalrus;
someWalrus = new Walrus(1000, 8.3);
我们假设\(Walrus\)的weight
存储在开始于5051956592385990207
的比特里,而tuskSize
存储在开始于5051956592385990239
的比特里,我们会把5051956592385990207
存储在\(Walrus\)变量中:
我们也可以对引用变量赋予特殊值null
:
这样表示引用变量的比特较为复杂,因此我们引入箭头标记:
- 如果一个地址全部为空,就用
null
来表示。 - 如果一个地址非空,就用箭头指向它所代指的对象实例。
回到下面这个问题: 1
2
3
4
5
6Walrus a = new Walrus(1000, 8.3);
Walrus b;
b = a;
b.weight = 5;
System.out.println(a);
System.out.println(b);
执行完下面的语句后,可以得到下面的标记: 1
2
3Walrus 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
15public 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
9public 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
7IntList 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
。
this
refers to the current object. It's like saying: "I am" or "my name is". You can't change who you are.