java 万物皆对象
如果我们说另外一种不同的语言,我们会发觉一个不同的世界!
— Ludwig Wittgenstein (1889-1951)
相比 C++ ,Java 是一种更纯粹的面向对象编程语言。虽然它们都是混合语言,但
在 Java 中,设计者们认为混合的作用并非像在 C++ 中那般重要。混合语言允许多种编程风格,这也是 C++ 支持向后兼容 C 的原因。正因为 C++ 是 C 语言的超集,所以它也同时包含了许多 C 语言不具备的特性,这使得 C++ 在某些方面过于复杂。
Java 语言假设你只进行面向对象编程。开始学习之前,我们需要将思维置于面向
对象的世界。本章你将了解到 Java 程序的基本组成,学习在 Java 中万物(几乎)皆对象的思想。
对象操纵
“名字代表什么?玫瑰即使不叫玫瑰,也依旧芬芳”。(引用自莎士比亚,《罗密欧与
朱丽叶》)。
所有的编程语言都会操纵内存中的元素。有时程序员必须要有意识地直接或间接
地操纵它们。在 C/C++ 中,对象的操纵是通过指针来完成的。
Java 利用万物皆对象的思想和单一一致的语法方式来简化问题。虽万物皆可为对
象,但我们所操纵的标识符实际上只是对对象的 “引用”。举例:我们可以用遥控器
(引用)去操纵电视(对象)。只要拥有对象的 “引用”,就可以操纵该 “对象”。换句话说,我们无需直接接触电视,就可通过遥控器(引用)自由地控制电视(对象)的频道和音量。此外,没有电视,遥控器也可以单独存在。就是说,你仅仅有一个 “引用” 并不意味着你必然有一个与之关联的 “对象”。
1 这里可能有争议。有人说这是一个指针,但这假定了一个潜在的实现。此外,Java 引用的语法更类似于 C++ 引用而非指针。在《Thinking in Java》的第 1 版中,我发明了一个新术语叫 “句柄”(handle),因为 C++ 引用和 Java 引用有一些重要的区别。作为一个从 C++ 的过来人,我不想混淆 Java 可能的最大受众 —— C++ 程序员。在《Thinking in Java》的第 2 版中,我认为 “引用”(reference)是更常用的术语,从 C++ 转过来的人除了引用的术语之外,还有很多东西需要处理,所以他们不妨双脚都跳进去。但是,也有些人甚至不同意 “引用”。在某书中我读到一个观点:Java 支持引用传递的说法是完全错误的,因为 Java 对象标识符(根据该作者)实际上是 “对象引用”(object references),并且一切都是值传递。所以你不是通过引用传递,而是 “通过值传递对象引用。人们可以质疑我的这种解释的准确性,但我认为我的方法简化了对概念的理解而又没对语言造成伤害(嗯,语言专家可能会说我骗你,但我会说我只是对此进行了适当的抽象。)
下面来创建一个 String 引用,用于保存单词或语句。代码示例:
String s;
这里我们只是创建了一个 String 对象的引用,而非对象。直接拿来使用会出现错
误:因为此时你并没有给变量 s 赋值–指向任何对象。通常更安全的做法是:创建一个
引用的同时进行初始化。代码示例:
String s = "asdf";
Java 语法允许我们使用带双引号的文本内容来初始化字符串。同样,其他类型的
对象也有相应的初始化方式。
对象创建
“引用” 用来关联 “对象”。在 Java 中,通常我们使用 new 操作符来创建一个新对
象。new 关键字代表:创建一个新的对象实例。所以,我们也可以这样来表示前面的代码示例:
String s = new String("asdf");
以上展示了字符串对象的创建过程,以及如何初始化生成字符串。除了 String 类
型以外,Java 本身自带了许多现成的数据类型。除此之外,我们还可以创建自己的数据类型。事实上,这是 Java 程序设计中的一项基本行为。在本书后面的学习中将会接触到。
数据存储
那么,程序在运行时是如何存储的呢?尤其是内存是怎么分配的。有 5 个不同的地
方可以存储数据:
- 寄存器(Registers)最快的存储区域,位于 CPU 内部。然而,寄存器的数量十
分有限,所以寄存器根据需求进行分配。我们对其没有直接的控制权,也无法在
自己的程序里找到寄存器存在的踪迹(另一方面,C/C++ 允许开发者向编译器
建议寄存器的分配)。 - 栈内存(Stack)存在于常规内存 RAM(随机访问存储器,Random Access Memory)
区域中,可通过栈指针获得处理器的直接支持。栈指针下移分配内存,上移释放
内存。这是一种仅次于寄存器的非常快速有效的分配存储方式。创建程序时,Java
系统必须知道栈内保存的所有项的生命周期。这种约束限制了程序的灵活性。因
此,虽然在栈内存上存在一些 Java 数据(如对象引用),但 Java 对象本身的数据
却是保存在堆内存的。 - 堆内存(Heap)这是一种通用的内存池(也在 RAM 区域),所有 Java 对象都存
在于其中。与栈内存不同,编译器不需要知道对象必须在堆内存上停留多长时间。
因此,用堆内存保存数据更具灵活性。创建一个对象时,只需用 new 命令实例化
对象即可,当执行代码时,会自动在堆中进行内存分配。这种灵活性是有代价的:
分配和清理堆内存要比栈内存需要更多的时间(如果可以用 Java 在栈内存上创
建对象,就像在 C++ 中那样的话)。随着时间的推移,Java 的堆内存分配机制现
在已经非常快,因此这不是一个值得关心的问题了。 - 常量存储(Constant storage)常量值通常直接放在程序代码中,因为它们永远不
会改变。如需严格保护,可考虑将它们置于只读存储器 ROM(只读存储器,Read
Only Memory)中 - 非 RAM 存储(Non-RAM storage)数据完全存在于程序之外,在程序未运行以
及脱离程序控制后依然存在。两个主要的例子:(1)序列化对象:对象被转换为字
节流,通常被发送到另一台机器;(2)持久化对象:对象被放置在磁盘上,即使程
序终止,数据依然存在。这些存储的方式都是将对象转存于另一个介质中,并在
需要时恢复成常规的、基于 RAM 的对象。Java 为轻量级持久化提供了支持。而
诸如 JDBC 和 Hibernate 这些类库为使用数据库存储和检索对象信息提供了更复
杂的支持。
基本类型的存储
有一组类型在 Java 中使用频率很高,它们需要特殊对待,这就是 Java 的基本类
型。之所以这么说,是因为它们的创建并不是通过 new 关键字来产生。通常 new 出来的对象都是保存在堆内存中的,以此方式创建小而简单的变量往往是不划算的。所以对于这些基本类型的创建方法,Java 使用了和 C/C++ 一样的策略。也就是说,不是使用 new 创建变量,而是使用一个 “自动” 变量。这个变量直接存储 “值”,并置于栈内存中,因此更加高效。
3 一个例子是字符串常量池。所有文字字符串和字符串值常量表达式都会自动放入特殊的静态存储中。
Java 确定了每种基本类型的内存占用大小。这些大小不会像其他一些语言那样随
着机器环境的变化而变化。这种不变性也是 Java 更具可移植性的一个原因。
基本类型 | 大小 | 最小值 | 最大值 | 包装类型 |
---|---|---|---|---|
boolean | — | — | — | boolean |
char | 16 bits | Unicode 0 | Unicode 216 -1 | Character |
byte | 8 bits | -128 | +127 | Byte |
short | 16 bits | - 215 | + 215 -1 | Short |
int | 32 bits | - 231 | + 231 -1 | Integer |
long | 64 bits | - 263 | + 263 -1 | Long |
float | 32 bits | IEEE754 | IEEE754 | Float |
double | 64 bits | IEEE754 | IEEE754 | Double |
void | — | — | — | Void |
所有的数值类型都是有正/负符号的。布尔(boolean)类型的大小没有明确的规定,通常定义为取字面值 “true” 或 “false” 。基本类型有自己对应的包装类型,如果你希望在堆内存里表示基本类型的数据,就需要用到它们的包装类。代码示例:
char c = 'x';
Character ch = new Character(c);
或者你也可以使用下面的形式:
Character ch = new Character('x');
基本类型自动转换成包装类型(自动装箱)
Character ch = 'x';
相对的,包装类型转化为基本类型(自动拆箱):
char c = ch;
个中原因将在以后的章节里解释。
高精度数值
在 Java 中有两种类型的数据可用于高精度的计算。它们是 BigInteger 和
BigDecimal。尽管它们大致可以划归为 “包装类型”,但是它们并没有对应的基本类型。
这两个类包含的方法提供的操作,与对基本类型执行的操作相似。也就是说,能对int 或 float 做的运算,在 BigInteger 和 BigDecimal 这里也同样可以,只不过必须要通过调用它们的方法来实现而非运算符。此外,由于涉及到的计算量更多,所以运算速度会慢一些。诚然,我们牺牲了速度,但换来了精度。
BigInteger 支持任意精度的整数。可用于精确表示任意大小的整数值,同时在运算过程中不会丢失精度。BigDecimal 支持任意精度的定点数字。例如,可用它进行精确的货币计算。
关于这两个类的详细信息,请参考 JDK 官方文档。
数组的存储
许多编程语言都支持数组类型。在 C 和 C++ 中使用数组是危险的,因为那些数
组只是内存块。如果程序访问了内存块之外的数组或在初始化之前使用该段内存(常见编程错误),则结果是不可预测的。
Java 的设计主要目标之一是安全性,因此许多困扰 C 和 C++ 程序员的问题不会
在 Java 中再现。在 Java 中,数组使用前需要被初始化,并且不能访问数组长度以外的数据。这种范围检查,是以每个数组上少量的内存开销及运行时检查下标的额外时间为代价的,但由此换来的安全性和效率的提高是值得的。(并且 Java 经常可以优化这些操作)。
当我们创建对象数组时,实际上是创建了一个引用数组,并且每个引用的初始值都为 null 。在使用该数组之前,我们必须为每个引用指定一个对象。如果我们尝试使用为 null 的引用,则会在运行时报错。因此,在 Java 中就防止了数组操作的常规错误。
我们还可创建基本类型的数组。编译器通过将该数组的内存全部置零来保证初始
化。本书稍后将详细介绍数组,特别是在数组章节中。