mirror of
https://github.com/LingCoder/OnJava8.git
synced 2026-08-19 12:33:29 +08:00
排版格式调整
This commit is contained in:
@@ -5,11 +5,11 @@
|
||||
# 第一章 对象的概念
|
||||
|
||||
|
||||
> “我们没有意识到惯用语言的结构有多大的力量。可以毫不夸张地说,它通过语义反应机制奴役我们,语言表现出来并在无意识中给我们留下深刻印象的结构会自动投射到我们周围的世界。” -- Alfred Korzybski (1930)
|
||||
> “我们没有意识到惯用语言的结构有多大的力量。可以毫不夸张地说,它通过语义反应机制奴役我们。语言表现出来并在无意识中给我们留下深刻印象的结构会自动投射到我们周围的世界。” -- Alfred Korzybski (1930)
|
||||
|
||||
计算机革命的起源来自机器。编程语言就像是那台机器,它不仅是我们思维的放大工具和另一种表达媒介,更像我们思想的一部分。语言的灵感来自其他形式的表达,如写作,绘画,雕塑,动画和电影制作。编程语言就是创建应用程序的思想结构。
|
||||
计算机革命的起源来自机器。编程语言就像是那台机器。它不仅是我们思维放大的工具与另一种表达媒介,更像我们思想的一部分。语言的灵感来自其他形式的表达,如写作,绘画,雕塑,动画和电影制作。编程语言就是创建应用程序的思想结构。
|
||||
|
||||
面向对象编程(*Object-oriented programming OOP*)是一种编程思维方式和编码架构。本章讲述 OOP 的基本概述。如果读者对不太理解,可先行跳过本章。等你具备一定编程基础后,请务必再回头看。只有这样你才能深刻理解面向对象编程的重要性及设计方式。
|
||||
面向对象编程(Object-Oriented Programming OOP)是一种编程思维方式和编码架构。本章讲述 OOP 的基本概述。如果读者对不太理解,可先行跳过本章。等你具备一定编程基础后,请务必再回头看。只有这样你才能深刻理解面向对象编程的重要性及设计方式。
|
||||
|
||||
## 抽象
|
||||
|
||||
@@ -21,8 +21,7 @@
|
||||
问题都归纳为决策链。对于这些语言,我们认为它们一部分是“基于约束”的编程,另一部分则是专为
|
||||
处理图形符号设计的(后者被证明限制性太强)。每种方法都有自己特殊的用途,适合解决某一类的问题。只要超出了它们力所能及的范围,就会显得非常笨拙。
|
||||
|
||||
面向对象的程序设计在此基础上跨出了一大步,程序员可利用一些工具表达“问题空间”内的元素。由于这种
|
||||
表达非常具有普遍性,所以不必受限于特定类型的问题。我们将问题空间中的元素以及它们在解决方案空间的表示物称作“对象”(**Object**)。当然,还有一些在问题空间没有对应的对象体。通过添加新的对象类型,程序可进行灵活的调整,以便与特定的问题配合。所以在阅读方案的描述代码时,会读到对问题进行表达的话语。与我们以前见过的相比,这无疑是一种更加灵活、更加强大的语言抽象方法。总之,OOP 允许我们根据问题来描述问题,而不是根据方案。然而,仍有一个联系途径回到计算机。每个对象都类似一台小计算机;它们有自己的状态,而且可要求它们进行特定的操作。与现实世界的“对象”或者“物体”相比,编程“对象”与它们也存在共通的地方:它们都有自己的特征和行为。
|
||||
面向对象的程序设计在此基础上跨出了一大步,程序员可利用一些工具表达“问题空间”内的元素。由于这种表达非常具有普遍性,所以不必受限于特定类型的问题。我们将问题空间中的元素以及它们在解决方案空间的表示物称作“对象”(**Object**)。当然,还有一些在问题空间没有对应的对象体。通过添加新的对象类型,程序可进行灵活的调整,以便与特定的问题配合。所以在阅读方案的描述代码时,会读到对问题进行表达的话语。与我们以前见过的相比,这无疑是一种更加灵活、更加强大的语言抽象方法。总之,OOP 允许我们根据问题来描述问题,而不是根据方案。然而,仍有一个联系途径回到计算机。每个对象都类似一台小计算机;它们有自己的状态,而且可要求它们进行特定的操作。与现实世界的“对象”或者“物体”相比,编程“对象”与它们也存在共通的地方:它们都有自己的特征和行为。
|
||||
|
||||
Smalltalk 作为第一种成功的面向对象程序设计语言和 Java 的基础语言,*Alan Kay* 总结了其五大基本特征。通过这些特征,我们可理解“纯粹”的面向对象程序设计方法是什么样的:
|
||||
|
||||
@@ -32,28 +31,20 @@ Smalltalk 作为第一种成功的面向对象程序设计语言和 Java 的基
|
||||
> 4. **每个对象都有一种类型**。根据语法,每个对象都是某个“类”的一个“实例”。其中,“类”(Class)是“类型”(Type)的同义词。一个类最重要的特征就是“能将什么信息发给它?”。
|
||||
> 5. **同一类所有对象都能接收相同的信息**。这实际是别有含义的一种说法,大家不久便能理解。由于类型为“圆”(Circle)的一个对象也属于类型为“形状”(Shape)的一个对象,所以一个圆完全能接收形状信息。这意味着可让程序代码统一指挥“形状”,令其自动控制所有符合“形状”描述的对象,其中自然包括“圆”。这一特性称为对象的“可替换性”,是OOP最重要的概念之一。
|
||||
|
||||
Grady Booch 提供了对对象更简洁的描述:一个对象具有自己的状态,行为和身份。这意味着对象有自己的内部数据(由状态提供)、方法 (由特性提供),并彼此区分(每个对象在内存中都有唯一的地址)。
|
||||
*Grady Booch* 提供了对对象更简洁的描述:一个对象具有自己的状态,行为和身份。这意味着对象有自己的内部数据(由状态提供)、方法 (由特性提供),并彼此区分(每个对象在内存中都有唯一的地址)。
|
||||
|
||||
|
||||
## 接口
|
||||
|
||||
亚里士多德(*Aristotle*)大概是第一个认真研究“类型”的哲学家,他曾提出过“鱼和鸟类”这样的概念。所有对象都是唯一的,但同时也是具有相同的特性和型位的对象所归属的类的一部分。这种思想被首次应用于第一个面向对象编程语言 Simula-67,它在程序中使用基本关键字 **class** 来引入新的类型(class 和 type 通常可互换使用,有些人对它们进行了进一步区分,他们强调 type 决定了接口,而 class 是那个接口的一种特殊实现方式)。
|
||||
|
||||
Simula 是一个很好的例子。正如这个名字所暗示的,它的作用是“模拟”(*Simulate*)类似“银行出纳员”这
|
||||
样的经典问题。在这个例子里,我们有一系列出纳员、客户、帐号以及交易等。每类成员(元素)都具有一
|
||||
些通用的特征:每个帐号都有一定的余额;每名出纳都能接收客户的存款;等等。与此同时,每个成员都有
|
||||
自己的状态;每个帐号都有不同的余额;每名出纳都有一个名字。所以在计算机程序中,能用独一无二的实
|
||||
体分别表示出纳员、客户、帐号以及交易。这个实体便是“对象”,而且每个对象都隶属一个特定的“类”,那个类具有自己的通用特征与行为。
|
||||
Simula 是一个很好的例子。正如这个名字所暗示的,它的作用是“模拟”(Simulate)类似“银行出纳员”这样的经典问题。在这个例子里,我们有一系列出纳员、客户、帐号以及交易等。每类成员(元素)都具有一些通用的特征:每个帐号都有一定的余额;每名出纳都能接收客户的存款;等等。与此同时,每个成员都有自己的状态;每个帐号都有不同的余额;每名出纳都有一个名字。所以在计算机程序中,能用独一无二的实体分别表示出纳员、客户、帐号以及交易。这个实体便是“对象”,而且每个对象都隶属一个特定的“类”,那个类具有自己的通用特征与行为。
|
||||
|
||||
因此,在面向对象的程序设计中,尽管我们真正要做的是新建各种各样的数据“类型”(*Type*),但几乎所
|
||||
有面向对象的程序设计语言都采用了 `class` 关键字。当您看到“type”这个字的时候,请同时想到 `class`;反之亦然。
|
||||
因此,在面向对象的程序设计中,尽管我们真正要做的是新建各种各样的数据“类型”(Type),但几乎所有面向对象的程序设计语言都采用了 `class` 关键字。当您看到“type”这个字的时候,请同时想到 `class`;反之亦然。
|
||||
|
||||
创建好一个类后,可根据情况生成许多对象。随后,可将那些对象作为要解决问题中存在的元素进行处理。事实上,当我们进行面向对象的程序设计时,面临的最大一项挑战性就是:如何在“问题空间”(问题实际存
|
||||
在的地方)的元素与“方案空间”(对实际问题进行建模的地方,如计算机)的元素之间建立理想的“一对
|
||||
一”的映射关系。
|
||||
创建好一个类后,可根据情况生成许多对象。随后,可将那些对象作为要解决问题中存在的元素进行处理。事实上,当我们进行面向对象的程序设计时,面临的最大一项挑战性就是:如何在“问题空间”(问题实际存在的地方)的元素与“方案空间”(对实际问题进行建模的地方,如计算机)的元素之间建立理想的“一对一”的映射关系。
|
||||
|
||||
那么如何利用对象完成真正有用的工作呢?必须有一种办法能向对象发出请求,令其解决一些实际的问题,比如完成一次交易、在屏幕上画一些东西或者打开一个开关等等。每个对象仅能接受特定的请求。我们向对象发出的请求是通过它的“接口”(*Interface*)定义的,对象的“类型”或“类”则规定了它的接口形式。“类
|
||||
型”与“接口”的对应关系是面向对象程序设计的基础。
|
||||
那么如何利用对象完成真正有用的工作呢?必须有一种办法能向对象发出请求,令其解决一些实际的问题,比如完成一次交易、在屏幕上画一些东西或者打开一个开关等等。每个对象仅能接受特定的请求。我们向对象发出的请求是通过它的“接口”(Interface)定义的,对象的“类型”或“类”则规定了它的接口形式。“类型”与“接口”的对应关系是面向对象程序设计的基础。
|
||||
|
||||
下面让我们以电灯泡为例:
|
||||
|
||||
@@ -64,11 +55,11 @@ Light lt = new Light();
|
||||
lt.on();
|
||||
```
|
||||
|
||||
在这个例子中,类型/类的名称是 **Light**,可向 **Light** 对象发出的请求包括包括打开(on)、关闭(*off*)、变得更明亮(*brighten*)或者变得更暗淡(*dim*)。通过简单地声明一个名字(lt),我们为 **Light** 对象创建了一个“句柄”。然后用new关键字新建类型为 **Light** 的一个对象。再用等号将其赋给句柄。
|
||||
在这个例子中,类型/类的名称是 **Light**,可向 **Light** 对象发出的请求包括包括打开(on)、关闭(off)、变得更明亮(brighten)或者变得更暗淡(dim)。通过简单地声明一个名字(lt),我们为 **Light** 对象创建了一个“句柄”。然后用new关键字新建类型为 **Light** 的一个对象。再用等号将其赋给句柄。
|
||||
|
||||
为了向对象发送一条信息,我们列出句柄名(*lt*),再用一个句点符号(`.`)把它同信息名称(*on*)连接起来。从中可以看出,使用一些预先定义好的类时,我们在程序里采用的代码是非常简单和直观的。
|
||||
为了向对象发送一条信息,我们列出句柄名(lt),再用一个句点符号(`.`)把它同信息名称(on)连接起来。从中可以看出,使用一些预先定义好的类时,我们在程序里采用的代码是非常简单和直观的。
|
||||
|
||||
上图遵循 **UML**(*Unified Modeling Language*,统一建模语言)的格式。每个类由一个框表示,框的顶部有类型名称,框中间部分要描述的任何数据成员,以及方法(属于此对象的方法,它们接收任何发送到该对象的信息)在框的底部。通常,只有类的名称和公共方法在 **UML** 设计图中显示,因此中间部分未显示,如本例所示。如果您只对类名感兴趣,则也不需要显示方法信息。
|
||||
上图遵循 **UML**(Unified Modeling Language,统一建模语言)的格式。每个类由一个框表示,框的顶部有类型名称,框中间部分要描述的任何数据成员,以及方法(属于此对象的方法,它们接收任何发送到该对象的信息)在框的底部。通常,只有类的名称和公共方法在 **UML** 设计图中显示,因此中间部分未显示,如本例所示。如果您只对类名感兴趣,则也不需要显示方法信息。
|
||||
|
||||
|
||||
## 服务提供
|
||||
@@ -107,9 +98,9 @@ Java 有三个显式关键字来设置类中的访问权限:`public`(公开
|
||||
|
||||
代码和设计方案的复用性是面向对象的程序设计的优点之一。我们可以通过重复使用某个 `class` 来达到这种复用性。同时,我们也可以将这个 `class` 作为另一个 `class` 的成员变量来使用。新的对象可以是由任意数量、类型的其他对象构成。这里涉及到“组合”和“聚合”的概念:
|
||||
|
||||
* **组合**(*Composition*) 经常用来表示“拥有”关系(*Has-a Relationship*)。例如,“汽车拥有了主机”。
|
||||
* **组合**(Composition) 经常用来表示“拥有”关系(Has-a Relationship)。例如,“汽车拥有了主机”。
|
||||
|
||||
* **聚合**(*Aggregation*) 动态的 **组合**。
|
||||
* **聚合**(Aggregation) 动态的 **组合**。
|
||||
|
||||

|
||||
|
||||
@@ -162,7 +153,7 @@ Java 有三个显式关键字来设置类中的访问权限:`public`(公开
|
||||
|
||||
通过继承,程序直到运行时才能确定代码的地址,因此当信息被发送到对象时,还需要其他一些方案。为了解决这个问题,面向对象语言使用后期绑定的概念。当向对象发送信息时,调用的代码直到运行时才确定。编译器确保方法存在,并对参数和返回值执行类型检查,但是它不知道要执行的确切代码。
|
||||
|
||||
为了执行后期绑定,Java 使用一个特殊的代码位来代替绝对调用。这段代码使用对象中存储的信息来计算方法主体的地址(此过程在多态性章节中有详细介绍)。因此,每个对象的行为根据特定代码位的内容而不同。当您向对象发送信息时,该对象实际上确定如何处理该信息。在某些语言中,必须显式地授予方法后期绑定属性的灵活性。例如,C++使用虚拟关键字。在这些语言中,默认情况下方法没有动态绑定。在 Java 中,动态绑定是默认行为,不需要额外的关键字来生成多态性。
|
||||
为了执行后期绑定,Java 使用一个特殊的代码位来代替绝对调用。这段代码使用对象中存储的信息来计算方法主体的地址(此过程在多态性章节中有详细介绍)。因此,每个对象的行为根据特定代码位的内容而不同。当您向对象发送信息时,该对象实际上确定如何处理该信息。在某些语言中,必须显式地授予方法后期绑定属性的灵活性。例如,C++ 使用虚拟关键字。在这些语言中,默认情况下方法没有动态绑定。在 Java 中,动态绑定是默认行为,不需要额外的关键字来生成多态性。
|
||||
|
||||
为了演示多态性,我们编写了一段代码,它忽略了类型的特定细节,只与基类对话。该代码与特定于类型的信息分离,因此更易于编写和更容易理解。而且,如果通过继承添加了一个新类型(例如,一个六边形),那么代码对于新类型的 Shape 就像对现有类型一样有效。因此,该程序是可扩展的。
|
||||
|
||||
@@ -176,7 +167,7 @@ void doSomething(Shape shape) {
|
||||
}
|
||||
```
|
||||
|
||||
此方法与任何 Shape 都相关,因此它独立于所绘制和擦除的对象的特定类型。此时程序的其他部分使用**doSomething()** 方法:
|
||||
此方法与任何 Shape 都相关,因此它独立于所绘制和擦除的对象的特定类型。此时程序的其他部分使用 `doSomething()` 方法:
|
||||
|
||||
```java
|
||||
Circle circle = new Circle();
|
||||
@@ -198,9 +189,9 @@ void doSomething(Shape shape) {
|
||||
doSomething(circle);
|
||||
```
|
||||
这里将 **Circle**(圆)句柄传递给一个本来期待 **Shape**(形状)句柄的方法。由于圆也是一种几何形状,所
|
||||
以 **doSomething(circle)** 能正确地执行。也就是说,**doSomething()** 能接受任意 **Shape** 的信息。这是完全安全和合乎逻辑的事情。
|
||||
以 `doSomething(circle)` 能正确地执行。也就是说,`doSomething()` 能接受任意 **Shape** 的信息。这是完全安全和合乎逻辑的事情。
|
||||
|
||||
这种把子类当成其基类来处理的过程叫做“向上转型”(**upcasting**)。在面向对象的编程里,经常利用这种方法来给程序解耦。再看下面的 **doSomething()** 代码示例:
|
||||
这种把子类当成其基类来处理的过程叫做“向上转型”(**Upcasting**)。在面向对象的编程里,经常利用这种方法来给程序解耦。再看下面的 `doSomething()` 代码示例:
|
||||
|
||||
```java
|
||||
shape.erase();
|
||||
@@ -238,17 +229,17 @@ Java 的单继承结构有很多好处。由于所有对象都有继承自一个
|
||||
|
||||
“集合”这种类型的对象可以存储任意类型、数量的其他对象。它能根据需要自动扩容,我们不用关心过程是如何实现的。
|
||||
|
||||
还好,一般优秀的 OOP 语言都会将“集合”作为其基础包。在 C++ 中,“集合”是其标准库的一部分。通常被称为 STL(标准模板库,*the Standard Template Library*)。SmallTalk 有一套非常完整的集合库。同样,Java 的标准库中也提供许多现成的集合类。在一些库中,一两个集合泛型就能满足我们所有的需求了。在 Java 中不同的需求对应不同种类的集合
|
||||
还好,一般优秀的 OOP 语言都会将“集合”作为其基础包。在 C++ 中,“集合”是其标准库的一部分。通常被称为 STL(Standard Template Library,标准模板库)。SmallTalk 有一套非常完整的集合库。同样,Java 的标准库中也提供许多现成的集合类。在一些库中,一两个集合泛型就能满足我们所有的需求了。在 Java 中不同的需求对应不同种类的集合
|
||||
|
||||
在一些库中,一个或两个泛型集合被认为是对所有需求都足够好的,而在其他(Java)中,不同类型的集合对应不同的需求:常见的有 List,常用于保存序列;Map,也称为关联数组,常用于将对象与其他对象关联);Set,只能保存非重复的值;其他还包括如队列(*Queue*)、树(*Tree*)、堆(*Stack*)等等。从设计的角度来看,我们真正想要的是一个能够解决某个问题的集合。如果一种集合就满足所有需求,那么我们就不需要剩下的了。之所以选择集合有以下两个原因:
|
||||
在一些库中,一个或两个泛型集合被认为是对所有需求都足够好的,而在其他(Java)中,不同类型的集合对应不同的需求:常见的有 List,常用于保存序列;Map,也称为关联数组,常用于将对象与其他对象关联);Set,只能保存非重复的值;其他还包括如队列(Queue)、树(Tree)、堆(Stack)等等。从设计的角度来看,我们真正想要的是一个能够解决某个问题的集合。如果一种集合就满足所有需求,那么我们就不需要剩下的了。之所以选择集合有以下两个原因:
|
||||
|
||||
1. 集合可以提供不同类型的接口和外部行为。堆栈、队列的应用场景和集合、列表不同,为我们解决问题提供了灵活的方案。
|
||||
|
||||
2. 不同的集合种类对应着不同的用途。例如,List 的两种基本类型:ArrayList 和 LinkedList。虽然两者具有相同接口和外部行为,但是在某些操作中它们的效率差别很大。在 ArrayList 中随机查找元素是很高效的,而 LinkedList 随机查找效率低下。反之,在 LinkedList 中插入元素的效率要比在 ArrayList 中高。由于底层数据结构的不同,每种集合类型在执行相同的操作时会表现出效率上的差异。
|
||||
|
||||
通过对 List 接口的抽象,我们可以很容易的将 LinkedList 改为 ArrayList。在 Java 5 泛型出来之前,集合中保存的是通用类型 `Object`。Java 单继承的结构意味着所有元素都基于 `Object` 类,所以在集合中可以保存任何类型的数据。这也使得集合易于重用。要使用这样的集合时,我们先要往集合添加元素。由于 Java 5 版本前的集合只保存 `Object`,当我们往集合中添加元素时,元素便向上转型成了 `Object`,从而丢失自己原有的类型特性。这时我们再从对象中取出该元素时,元素的类型变成了 `Object`。那么我们该怎么将其转回原先具体的类型的?这里,我们使用了强制类型转换将其转为更具体的类型。这个过程称之为对象的“向下转型”。通过“向上转型”,我们知道“圆形”也是一种“形状”,这个过程是安全的。可是我们不能从“Object”看出其就是“圆圈”或“形状”,所以除非我们能确定元素的具体类型信息,否则“向下转型”就是不安全的。也不能说这样的错误就时完全危险的,因为一旦我们转化了错误的类型,程序就会运行出错,抛出“运行时异常”(*RuntimeException*)。(后面的章节会提到) 无论如何,我们要寻找一种在取出集合元素时确定其具体类型的方法。另外,每次取出元素都要做额外的“向下转型”对程序和程序员都是一种开销。以某种方式创建集合,以确认保存元素的具体类型,减少集合元素“向下转型”中的开销和可能出现的错误难道不好吗?这种解决方案就是:参数化类型机制(*Parameterized Type Mechanism*)。
|
||||
通过对 List 接口的抽象,我们可以很容易的将 LinkedList 改为 ArrayList。在 Java 5 泛型出来之前,集合中保存的是通用类型 `Object`。Java 单继承的结构意味着所有元素都基于 `Object` 类,所以在集合中可以保存任何类型的数据。这也使得集合易于重用。要使用这样的集合时,我们先要往集合添加元素。由于 Java 5 版本前的集合只保存 `Object`,当我们往集合中添加元素时,元素便向上转型成了 `Object`,从而丢失自己原有的类型特性。这时我们再从对象中取出该元素时,元素的类型变成了 `Object`。那么我们该怎么将其转回原先具体的类型的?这里,我们使用了强制类型转换将其转为更具体的类型。这个过程称之为对象的“向下转型”。通过“向上转型”,我们知道“圆形”也是一种“形状”,这个过程是安全的。可是我们不能从“Object”看出其就是“圆圈”或“形状”,所以除非我们能确定元素的具体类型信息,否则“向下转型”就是不安全的。也不能说这样的错误就时完全危险的,因为一旦我们转化了错误的类型,程序就会运行出错,抛出“运行时异常”(RuntimeException)。(后面的章节会提到) 无论如何,我们要寻找一种在取出集合元素时确定其具体类型的方法。另外,每次取出元素都要做额外的“向下转型”对程序和程序员都是一种开销。以某种方式创建集合,以确认保存元素的具体类型,减少集合元素“向下转型”中的开销和可能出现的错误难道不好吗?这种解决方案就是:参数化类型机制(Parameterized Type Mechanism)。
|
||||
|
||||
参数化类型机制可以使得编译器能够自动识别某个 `class` 的具体类型并正确地执行. 举个例子,对集合的参数化类型机制可以让其仅接受“形状”这种类型的元素,并以“形状”类型取出元素。Java 5 版本支持了参数化类型机制,称之为“泛型”(*Generic*)。泛型是 Java 5 的主要特性之一。举个例子,你可以按以下方式向 ArrayList 种添加 Shape(形状):
|
||||
参数化类型机制可以使得编译器能够自动识别某个 `class` 的具体类型并正确地执行. 举个例子,对集合的参数化类型机制可以让其仅接受“形状”这种类型的元素,并以“形状”类型取出元素。Java 5 版本支持了参数化类型机制,称之为“泛型”(Generic)。泛型是 Java 5 的主要特性之一。举个例子,你可以按以下方式向 ArrayList 种添加 Shape(形状):
|
||||
|
||||
```java
|
||||
ArrayList<Shape> shapes = new ArrayList<>();
|
||||
@@ -267,11 +258,11 @@ Java 的单继承结构有很多好处。由于所有对象都有继承自一个
|
||||
|
||||
现在问题开始棘手了:我们怎么知道何时该清理这些对象呢?当某一个系统处理完成,而其他系统可能还没有处理完成。这样的问题在其他的场景下也可能发生。在 C++ 程序设计中,当使用完一个对象后,必须明确将其删除,这就让问题变复杂了。
|
||||
|
||||
这个对象的数据在哪?它的生命周期是怎么被控制的? 在 C++ 设计中采用的观点是效率第一,因此它将选择权交给了程序员。为了获得最大的运行时速度,程序员可以在编写程序时,通过将对象放在堆栈(*Stack*,有时称为自动变量或作用域变量)或静态存储区域(*static storage area*)中来确定内存占用和生存时间。这些区域的对象会被优先分配内存和释放。这种控制在某些情况下非常有用。
|
||||
这个对象的数据在哪?它的生命周期是怎么被控制的? 在 C++ 设计中采用的观点是效率第一,因此它将选择权交给了程序员。为了获得最大的运行时速度,程序员可以在编写程序时,通过将对象放在堆栈(Stack,有时称为自动变量或作用域变量)或静态存储区域(static storage area)中来确定内存占用和生存时间。这些区域的对象会被优先分配内存和释放。这种控制在某些情况下非常有用。
|
||||
|
||||
然而相对的,我们也牺牲了程序的灵活性。因为在编写代码时,我们必须要弄清楚对象的数量、生存时间还有类型。如果我们要用它来解决一个相当普遍的问题时(如计算机辅助设计、仓库管理或空中交通管制等),限制就太大了。
|
||||
|
||||
第二种方法是在堆内存(*Heap*)中动态地创建对象。在这种方式下,直到程序运行我们才能确定需要创建的对象数量、生存时间和类型。什么时候需要,什么时候在堆内存中创建。 因为内存的占用是动态管理的,所以在运行时,在堆内存上开辟空间所需的时间可能比在栈内存上要长(但也不一定)。在栈内存开辟空间通常是一条将栈指针向下移动,另一条将其向后移动的汇编指令。开辟堆内存空间的时间取决于内存机制的设计。
|
||||
第二种方法是在堆内存(Heap)中动态地创建对象。在这种方式下,直到程序运行我们才能确定需要创建的对象数量、生存时间和类型。什么时候需要,什么时候在堆内存中创建。 因为内存的占用是动态管理的,所以在运行时,在堆内存上开辟空间所需的时间可能比在栈内存上要长(但也不一定)。在栈内存开辟空间通常是一条将栈指针向下移动,另一条将其向后移动的汇编指令。开辟堆内存空间的时间取决于内存机制的设计。
|
||||
|
||||
动态方法使一般的逻辑假设对象趋于复杂,因此额外的内存查找和释放的开销对对象的创建影响不大。(原文:*The dynamic approach makes the generally logical assumption that objects tend to be complicated, so the extra overhead of finding storage and releasing that storage will not have an important impact on the creation of an object.*)此外,更好的灵活性对于问题的解决至关重要。
|
||||
|
||||
@@ -284,7 +275,7 @@ Java 的垃圾收集器被设计用来解决内存释放的问题(虽然这不
|
||||
|
||||
自编程语言被发明以来,程序的错误处理一直都是个难题,因为很难设计出一个好的错误处理方案。许多编程语言都忽略了这个问题,把这个问题丢给了程序类库的设计者。他们提出了在许多情况下都可以工作但很容易被规避的半途而废的措施,通常只需忽略错误。多数错误处理方案的主要问题是:它们依赖程序员之间的约定俗成而不是语言层面的限制。换句话说,如果程序员赶时间或没想起来,这些方案就很容易被忘记。
|
||||
|
||||
异常处理机制将程序错误直接交给编程语言甚至是操作系统。“异常”(*Exception*)是一个从出错点“抛出”(*thrown*)后能被特定类型的异常处理程序捕获(*catch*)的一个对象。它不会干扰程序的正常运行,仅当程序出错的时候才被执行。这让我们的编码更简单:不用再反复检查错误了。另外,如果 **throw** 的异常类型和 **catch** 的不符,则不会触发该条件下的异常处理程序。异常的发生是不会被忽略的,它终究会在某一时刻被处理。
|
||||
异常处理机制将程序错误直接交给编程语言甚至是操作系统。“异常”(Exception)是一个从出错点“抛出”(thrown)后能被特定类型的异常处理程序捕获(catch)的一个对象。它不会干扰程序的正常运行,仅当程序出错的时候才被执行。这让我们的编码更简单:不用再反复检查错误了。另外,如果 **throw** 的异常类型和 **catch** 的不符,则不会触发该条件下的异常处理程序。异常的发生是不会被忽略的,它终究会在某一时刻被处理。
|
||||
|
||||
最后,“异常机制”提供了一种可靠地从意外情况中恢复的方法,使得我们可以编写出更健壮的程序。有时你只要处理好抛出的异常情况并恢复程序的运行即可,无需退出。
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
|
||||
尽管和现在的图形用户界面相比,Shell 操作方式很原始。但是同时 shell 也为我们提供了许多有用的功特性。在学习本书的过程中,我们会经常使用到 Shell,包括现在这部分的安装,还有运行 Java 程序。
|
||||
|
||||
Mac:单击聚光灯(屏幕右上角的放大镜图标),然后键入“terminal”。单击看起来像小电视屏幕的应用程序(您也可以单击“return”)。这就启动了你的用户下的 shell 窗口。
|
||||
Mac:单击聚光灯(屏幕右上角的放大镜图标),然后键入 `terminal`。单击看起来像小电视屏幕的应用程序(您也可以单击“return”)。这就启动了你的用户下的 shell 窗口。
|
||||
|
||||
windows:首先,通过目录打开 windows 资源管理器:
|
||||
Windows 7: 单击屏幕左下角的“开始”图标,输入“explorer”后按回车键。
|
||||
@@ -37,9 +37,11 @@ Mac:单击聚光灯(屏幕右上角的放大镜图标),然后键入“te
|
||||
Fedora: 按 Alt+F2,在弹出的对话框中输入“gnome-terminal”
|
||||
|
||||
**目录**
|
||||
目录是 Shell 的基础元素之一。目录用来保存文件和其他目录。目录就好比树的分支。如果书籍是您系统上的一个目录,并且它有两个其他目录作为分支,例如数学和艺术,那么我们就可以说你有一个书籍目录里,它包含数学和艺术两个子目录。注意:Windows 使用“\”而不是“/”来分隔路径。
|
||||
|
||||
目录是 Shell 的基础元素之一。目录用来保存文件和其他目录。目录就好比树的分支。如果书籍是您系统上的一个目录,并且它有两个其他目录作为分支,例如数学和艺术,那么我们就可以说你有一个书籍目录里,它包含数学和艺术两个子目录。注意:Windows 使用 `\` 而不是 `/` 来分隔路径。
|
||||
|
||||
**Shell基本操作**
|
||||
|
||||
我在这展示的 Shell 操作和系统中大体相同。出于本书的原因,下面列举一些在 Shell 中的基本操作:
|
||||
|
||||
```bash
|
||||
|
||||
@@ -46,15 +46,15 @@ Java 语法允许我们使用带双引号的文本内容来初始化字符串。
|
||||
|
||||
那么, 程序在运行时是如何存储的呢?尤其是内存。下面我们就来形象地描述下, Java 中数据存储的5个不同的地方:
|
||||
|
||||
1. **寄存器** (*Registers*) 最快的保存区域,位于CPU内部 [^2]。然而,寄存器的数量十分有限,所以寄存器是根据需要由编译器分配。我们对其没有直接的控制权,也无法在自己的程序里找到寄存器存在的踪迹(另一方面,C/C++ 允许开发者向编译器建议寄存器的分配)。
|
||||
1. **寄存器** (Registers) 最快的保存区域,位于CPU内部 [^2]。然而,寄存器的数量十分有限,所以寄存器是根据需要由编译器分配。我们对其没有直接的控制权,也无法在自己的程序里找到寄存器存在的踪迹(另一方面,C/C++ 允许开发者向编译器建议寄存器的分配)。
|
||||
|
||||
2. **栈内存**(*Stack*) 存在于常规内存(RAM)区域中,可通过栈指针获得处理器的直接支持。栈指针下移创建新内存,上移释放该内存,顺序后进先出,速度仅次于寄存器。创建程序时,Java 编译器必须准确地知道栈内保存的所有数据的“长度”以及生命周期。栈内存的这种约束限制了程序的灵活性。因此,虽然在栈内存上存在一些 Java 数据,特别是对象引用,但 Java 对象本身却是保存在堆内存的。
|
||||
2. **栈内存**(Stack) 存在于常规内存(RAM)区域中,可通过栈指针获得处理器的直接支持。栈指针下移创建新内存,上移释放该内存,顺序后进先出,速度仅次于寄存器。创建程序时,Java 编译器必须准确地知道栈内保存的所有数据的“长度”以及生命周期。栈内存的这种约束限制了程序的灵活性。因此,虽然在栈内存上存在一些 Java 数据,特别是对象引用,但 Java 对象本身却是保存在堆内存的。
|
||||
|
||||
3. **堆内存**(*Heap*) 这是一种常规用途的内存池(也在 RAM区域),所有 Java 对象都存在于其中。与栈内存不同,编译器不需要知道对象必须在堆内存上停留多长时间。因此,用堆内存保存数据更具灵活性。创建一个对象时,只需用 `new` 命令实例化代码即可。执行这些代码时,数据会在堆内存里自动进行保存。这种灵活性是有代价的:分配和清理堆内存要比栈内存需要更多的时间(如果你甚至可以用 Java 在栈内存上创建对象,就像在C++ 中那样)。随着时间的推移,Java 的堆内存分配机制现已非常快,因此这不是一个值得关心的问题了。
|
||||
3. **堆内存**(Heap) 这是一种常规用途的内存池(也在 RAM区域),所有 Java 对象都存在于其中。与栈内存不同,编译器不需要知道对象必须在堆内存上停留多长时间。因此,用堆内存保存数据更具灵活性。创建一个对象时,只需用 `new` 命令实例化代码即可。执行这些代码时,数据会在堆内存里自动进行保存。这种灵活性是有代价的:分配和清理堆内存要比栈内存需要更多的时间(如果你甚至可以用 Java 在栈内存上创建对象,就像在C++ 中那样)。随着时间的推移,Java 的堆内存分配机制现已非常快,因此这不是一个值得关心的问题了。
|
||||
|
||||
4. **常量存储** (*Constant storage*) 常量值通常直接放在程序代码中,因为它们永远不会改变。如需严格保护,可考虑将它们置于只读存储器(ROM)中 [^3]。
|
||||
4. **常量存储** (Constant storage) 常量值通常直接放在程序代码中,因为它们永远不会改变。如需严格保护,可考虑将它们置于只读存储器(ROM)中 [^3]。
|
||||
|
||||
5. **非 RAM 存储** (*Non-RAM storage*) 数据完全存在于程序之外,在程序未运行以及脱离程序控制后依然存在。两个主要的例子:(1)序列化对象:对象被转换为字节流,通常被发送到另一台机器;(2)持久化对象:对象被放置在磁盘上,即使程序终止,数据依然存在。这些存储的方式都是将对象转存于另一个介质中,并在需要时恢复到常规内存中。Java 为轻量级持久性提供支持。诸如 JDBC 和 Hibernate 之类的库为使用数据库存储和检索对象信息提供了更复杂的支持。
|
||||
5. **非 RAM 存储** (Non-RAM storage) 数据完全存在于程序之外,在程序未运行以及脱离程序控制后依然存在。两个主要的例子:(1)序列化对象:对象被转换为字节流,通常被发送到另一台机器;(2)持久化对象:对象被放置在磁盘上,即使程序终止,数据依然存在。这些存储的方式都是将对象转存于另一个介质中,并在需要时恢复到常规内存中。Java 为轻量级持久性提供支持。诸如 JDBC 和 Hibernate 之类的库为使用数据库存储和检索对象信息提供了更复杂的支持。
|
||||
|
||||
|
||||
<!-- Special Case: Primitive Types -->
|
||||
@@ -76,7 +76,7 @@ Java 预设了每种基本类型的初始内存占用大小。 这些大小标
|
||||
| double | 64 bits |IEEE754 | IEEE754 | Double |
|
||||
| void | — | — | — | Void |
|
||||
|
||||
所有的数值类型都是有正/负符号的。布尔(*boolean*)类型的大小没有明确的规定,通常定义为采用文字 “true” 和 “false”。基本类型有自己对应的包装类型,如果你希望在堆内存里表示基本类型的数据,就需要用到它们的包装类。代码示例:
|
||||
所有的数值类型都是有正/负符号的。布尔(boolean)类型的大小没有明确的规定,通常定义为采用文字 “true” 和 “false”。基本类型有自己对应的包装类型,如果你希望在堆内存里表示基本类型的数据,就需要用到它们的包装类。代码示例:
|
||||
|
||||
```java
|
||||
char c = 'x';
|
||||
@@ -223,7 +223,7 @@ ATypeName a = new ATypeName();
|
||||
<!-- Fields -->
|
||||
### 属性
|
||||
|
||||
当我们创建好了一个类之后,我们可以往类里存放两种类型的元素。方法(*method*)和属性(*field*)。类的属性可以是基本类型。如果类的属性是对象的话,那么必须要初始化该引用将其关联到一个实际的对象上(通过之前介绍的创建对象的方法)。每个对象都会为其属性保留独立的存储空间。通常,属性不再对象之间做共享。下面是一个包含部分属性的类的代码示例:
|
||||
当我们创建好了一个类之后,我们可以往类里存放两种类型的元素。方法(method)和属性(field)。类的属性可以是基本类型。如果类的属性是对象的话,那么必须要初始化该引用将其关联到一个实际的对象上(通过之前介绍的创建对象的方法)。每个对象都会为其属性保留独立的存储空间。通常,属性不再对象之间做共享。下面是一个包含部分属性的类的代码示例:
|
||||
|
||||
```java
|
||||
class DataOnly {
|
||||
@@ -286,7 +286,7 @@ class DataOnly {
|
||||
<!-- Methods, Arguments,and Return Values -->
|
||||
### 方法使用
|
||||
|
||||
在许多语言(如 C 和 C++)中,术语函数(*function*)用于描述命名子程序。在 Java 中,我们使用术语方法(*method*)来表示“做某事的方式”。
|
||||
在许多语言(如 C 和 C++)中,术语函数(function)用于描述命名子程序。在 Java 中,我们使用术语方法(method)来表示“做某事的方式”。
|
||||
|
||||
在 Java 中,方法决定着对象能接收哪些信息。方法的基础部分包含名称、参数、返回类型、方法体。格式如:
|
||||
|
||||
@@ -298,7 +298,7 @@ class DataOnly {
|
||||
|
||||
#### 返回类型
|
||||
|
||||
方法的返回类型表明了当你调用它时会返回的结果类型。参数列表则显示了可被传递到方法内部的参数类型及名称。方法的名称和参数列表被统称为**方法签名**(*signature of the method*)。签名作为方法的唯一性标识。
|
||||
方法的返回类型表明了当你调用它时会返回的结果类型。参数列表则显示了可被传递到方法内部的参数类型及名称。方法的名称和参数列表被统称为**方法签名**(signature of the method)。签名作为方法的唯一性标识。
|
||||
|
||||
Java 中的方法只能作为类的一部分创建。它只能被对象所调用 [^4],并且该对象必须有权限来执行调用。若对象调用错误的方法,则程序将在编译时报错。
|
||||
|
||||
@@ -411,7 +411,7 @@ import java.util.*;
|
||||
|
||||
**static** 关键字(从 C++ 采用)就符合我们的要求。当我们说某些东西是静态的时,它意味着该字段或方法不依赖于任何特定的对象实例 。 即使我们从未创建过该类的对象,也可以调用其静态方法或访问静态字段。相反,对于普通的非静态字段和方法,我们必须要先创建一个对象并使用该对象来访问该字段或方法,因为非静态字段和方法必须与替对象关联 [^6] 。
|
||||
|
||||
一些面向对象的语言使用类数据(*class data*)和类方法(*class method*)这样的术语来表述静态。静态的数据意味着该数据和方法仅存在于类中,而非类的任何实例对象中。有时 Java 文献也使用这些术语。我们可以通过在类的属性或方法前添加 `static` 修饰来表示这是一个静态属性或静态方法。 代码示例:
|
||||
一些面向对象的语言使用类数据(class data)和类方法(class method)这样的术语来表述静态。静态的数据意味着该数据和方法仅存在于类中,而非类的任何实例对象中。有时 Java 文献也使用这些术语。我们可以通过在类的属性或方法前添加 `static` 修饰来表示这是一个静态属性或静态方法。 代码示例:
|
||||
|
||||
```java
|
||||
class StaticTest {
|
||||
@@ -580,7 +580,7 @@ java HelloDate
|
||||
## 编码风格
|
||||
|
||||
|
||||
Java 的代码约定规范(*Code Conventions for the Java Programming Language*)[^10] 要求类名的首字母大写。 如果类名是由多个单词构成的,则每个单词的首字母都应大写(不采用下划线来分隔)例如:
|
||||
Java 的代码约定规范(Code Conventions for the Java Programming Language)[^10] 要求类名的首字母大写。 如果类名是由多个单词构成的,则每个单词的首字母都应大写(不采用下划线来分隔)例如:
|
||||
|
||||
```java
|
||||
class AllTheColorsOfTheRainbow {
|
||||
@@ -610,7 +610,7 @@ class AllTheColorsOfTheRainbow {
|
||||
本章向您展示了简单的 Java 程序编写以及该语言相关的基本概念。到目前为止,我们的示例都只是些简单的顺序执行。在接下来的两章里,我们将会接触到 Java 的一些基本操作符,以及如何去控制程序执行的流程。
|
||||
|
||||
|
||||
[^1]: 这里可能有争议。有人说这是一个指针,但这假定了一个潜在的实现。此外,Java 引用的语法更类似于 C++ 引用而非指针。在 *Thinking in Java* 的第 1 版中,我发明了一个新术语叫“句柄”(*handle*),因为 C++ 引用和Java 引用有一些重要的区别。作为一个从 C++ 的过来人,我不想混淆 Java 可能的最大受众 —— C++ 程序员。在*Thinking in Java* 的第 2 版中,我认为“引用”(*reference*)是更常用的术语,从 C++ 转过来的人除了引用的术语之外,还有很多东西需要处理,所以他们不妨双脚都跳进去。但是,也有些人甚至不同意“引用”。在某书中我读到一个观点:Java 支持引用传递的说法是完全错误的,因为 Java 对象标识符(根据该作者)实际上是“对象引用”(*object references*),并且一切都是值传递。所以你不是通过引用传递,而是“通过值传递对象引用。人们可以质疑我的这种解释的准确性,但我认为我的方法简化了对概念的理解而又没对语言造成伤害(嗯,语言专家可能会说我骗你,但我会说我只是对此进行了适当的抽象。)
|
||||
[^1]: 这里可能有争议。有人说这是一个指针,但这假定了一个潜在的实现。此外,Java 引用的语法更类似于 C++ 引用而非指针。在 《Thinking in Java》 的第 1 版中,我发明了一个新术语叫“句柄”(handle),因为 C++ 引用和Java 引用有一些重要的区别。作为一个从 C++ 的过来人,我不想混淆 Java 可能的最大受众 —— C++ 程序员。在《Thinking in Java》的第 2 版中,我认为“引用”(reference)是更常用的术语,从 C++ 转过来的人除了引用的术语之外,还有很多东西需要处理,所以他们不妨双脚都跳进去。但是,也有些人甚至不同意“引用”。在某书中我读到一个观点:Java 支持引用传递的说法是完全错误的,因为 Java 对象标识符(根据该作者)实际上是“对象引用”(object references),并且一切都是值传递。所以你不是通过引用传递,而是“通过值传递对象引用。人们可以质疑我的这种解释的准确性,但我认为我的方法简化了对概念的理解而又没对语言造成伤害(嗯,语言专家可能会说我骗你,但我会说我只是对此进行了适当的抽象。)
|
||||
|
||||
[^2]: 大多数微处理器芯片都有额外的高速缓冲存储器,但这是按照传统存储器而不是寄存器。
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
Java 是从 C++ 的基础上做了一些改进和简化发展而成的。对于 C/C++ 程序员来说,Java 的运算符并不陌生。如果你已了解 C 或 C++,大可以跳过本章和下一章,直接阅读 Java 与 C/C++ 不同的地方。
|
||||
|
||||
如果理解这两章的内容对你来说还有点困难,那么我推荐你先了解下 *Thinking in C* 再继续后面的学习。 这本书现在可以在 [www.OnJava8.com](http://www.OnJava8.com]) 上免费下载。它的内容包含音频讲座、幻灯片、练习和解决方案,专门用于帮助你快速掌握学习 Java 所需的基础知识。
|
||||
如果理解这两章的内容对你来说还有点困难,那么我推荐你先了解下 《Thinking in C》 再继续后面的学习。 这本书现在可以在 [www.OnJava8.com](http://www.OnJava8.com]) 上免费下载。它的内容包含音频讲座、幻灯片、练习和解决方案,专门用于帮助你快速掌握学习 Java 所需的基础知识。
|
||||
|
||||
|
||||
<!-- Using-Java-Operators -->
|
||||
@@ -17,7 +17,7 @@ Java 是从 C++ 的基础上做了一些改进和简化发展而成的。对于
|
||||
|
||||
运算符接受一个或多个参数并生成新值。这个参数与普通方法调用的形式是不同的,但效果是相同的。加法 `+`,减法 `-`,乘法 `*`,除法 `/` 以及赋值 `=` 在任何编程语言中的工作方式都是类似的。所有运算符都能根据自己的运算对象生成一个值。除此以外,一个运算符可改变运算对象的值,这叫作“副作用”(**Side Effect**)。运算符最常见的用途就是修改自己的运算对象,从而产生副作用。但要注意生成的值亦可由没有副作用的运算符生成。
|
||||
|
||||
几乎所有运算符都只能操作基本类型(*Primitives*)。唯一的例外是 `=`、`==` 和 `!=`,它们能操作所有对象(这也是令人混淆的一个地方)。除此以外,**String** 类支持 `+` 和 `+=`。
|
||||
几乎所有运算符都只能操作基本类型(Primitives)。唯一的例外是 `=`、`==` 和 `!=`,它们能操作所有对象(这也是令人混淆的一个地方)。除此以外,**String** 类支持 `+` 和 `+=`。
|
||||
|
||||
|
||||
<!-- Precedence -->
|
||||
@@ -98,7 +98,7 @@ public class Assignment {
|
||||
|
||||
这是个简单的 `Tank` 类,通过 main 方法 `new` 出了 2 个实例对象。 两个对象的 `level` 属性分别被赋予了不同的值。 然后,t2 的值被赋予给 t1。在许多编程语言里,预期的结果是 t1 和 t2 的值会一直相对独立。但是,在 Java 中,由于赋予的只是对象的引用,改变 t1 也就改变了 t2。 这是因为 t1 和 t2 此时指向的是堆中同一个对象。(t1 原始对象的引用在 t2 赋值给其时被丢失,它就将会在垃圾回收时被清理)。
|
||||
|
||||
这种现象通常称为别名(*aliasing*),这是 Java 处理对象的一种基本方式。但是假若你不想这里出现这样的混淆的话,你可以这么做。代码示例:
|
||||
这种现象通常称为别名(aliasing),这是 Java 处理对象的一种基本方式。但是假若你不想这里出现这样的混淆的话,你可以这么做。代码示例:
|
||||
|
||||
```java
|
||||
t1.level = t2.level;
|
||||
@@ -140,13 +140,13 @@ public class PassObject {
|
||||
2: x.c: z
|
||||
```
|
||||
|
||||
在许多编程语言中,方法 **f()** 似乎在内部复制其参数 **Letter y**。但是一旦传递了一个引用,那么实际上 `y.c ='z';` 是在方法 **f()** 之外改变对象。别名现象以及其解决方案是个复杂的问题,在附录中有包含:[对象传递和返回](./Appendix-Passing-and-Returning-Objects.md)。意识到这一点,我们可以警惕类似的陷阱。
|
||||
在许多编程语言中,方法 `f()` 似乎在内部复制其参数 **Letter y**。但是一旦传递了一个引用,那么实际上 `y.c ='z';` 是在方法 `f()` 之外改变对象。别名现象以及其解决方案是个复杂的问题,在附录中有包含:[对象传递和返回](./Appendix-Passing-and-Returning-Objects.md)。意识到这一点,我们可以警惕类似的陷阱。
|
||||
|
||||
|
||||
<!-- Mathematical Operators -->
|
||||
## 算术运算符
|
||||
|
||||
Java 的基本算术运算符与其他大多编程语言是相同的。其中包括加号 `+`、减号 `-`、除号 `/`、乘号 `*` 以及模数`%`(从整数除法中获得余数)。整数除法会直接砍掉小数,而不是进位。
|
||||
Java 的基本算术运算符与其他大多编程语言是相同的。其中包括加号 `+`、减号 `-`、除号 `/`、乘号 `*` 以及模数 `%`(从整数除法中获得余数)。整数除法会直接砍掉小数,而不是进位。
|
||||
|
||||
Java 也用一种简写形式同时进行运算和赋值操作,由运算符后跟等号表示,并且与语言中的所有运算符一致(只要有意义)。为了将 4 的值赋予给变量 x 同时将结果赋予给 x , 可用 x += 4 来表示。下面带来代码更多代码示例:
|
||||
|
||||
@@ -230,7 +230,7 @@ u *= v : 5.2778773
|
||||
u /= v : 9.940527
|
||||
```
|
||||
|
||||
为了生成随机数字,程序首先创建一个 **Random** 对象。不带参数的 **Random** 对象会利用当前的时间用作随机数生成器的“种子”(*seed*),从而为程序的每次执行生成不同的输出。在本书的示例中,重要的是每个示例末尾的输出尽可能一致,以便可以使用外部工具进行验证。所以我们通过在创建 **Random** 对象时提供种子(随机数生成器的初始化值,其始终为特定种子值产生相同的序列),让程序每次执行都生成相同的随机数,如此以来输出结果就是可验证的 [^1]。 若需要生成随机值,可删除代码示例中的种子参数。该对象通过调用方法 **nextInt()** 和 **nextFloat()**(还可以调用 **nextLong()** 或 **nextDouble()**),使用 **Random** 对象生成许多不同类型的随机数。**nextInt()** 的参数设置生成的数字的上限,下限为零,为了避免零除的可能性,结果偏移1。
|
||||
为了生成随机数字,程序首先创建一个 **Random** 对象。不带参数的 **Random** 对象会利用当前的时间用作随机数生成器的“种子”(seed),从而为程序的每次执行生成不同的输出。在本书的示例中,重要的是每个示例末尾的输出尽可能一致,以便可以使用外部工具进行验证。所以我们通过在创建 **Random** 对象时提供种子(随机数生成器的初始化值,其始终为特定种子值产生相同的序列),让程序每次执行都生成相同的随机数,如此以来输出结果就是可验证的 [^1]。 若需要生成随机值,可删除代码示例中的种子参数。该对象通过调用方法 `nextInt()` 和 `nextFloat()`(还可以调用 `nextLong()` 或 `nextDouble()`),使用 **Random** 对象生成许多不同类型的随机数。`nextInt()` 的参数设置生成的数字的上限,下限为零,为了避免零除的可能性,结果偏移1。
|
||||
|
||||
|
||||
<!-- Unary Minus and Plus Operators -->
|
||||
@@ -296,7 +296,7 @@ i: 1
|
||||
|
||||
对于前缀形式,我们将在执行递增/减操作后获取值;使用后缀形式,我们将在执行递增/减操作之前获取值。它们是唯一具有“副作用”的运算符(除那些涉及赋值的以外) —— 它们既改变操作数又改变值。
|
||||
|
||||
C++ 名称来自于递增运算符,同时也代表着“比 C 更进一步”。在早期的 Java 演讲中,*Bill Joy*( Java 创建者之一)说“**Java = C ++ --**”(C++ 减减)。这意味着 Java 是在 C++ 的基础上减少了许多不必要的东西,因此语言更简单。随着进一步地学习,我们会发现 Java 的确有许多地方相对 C++ 来说更简便,但是在其他方面,难度并不会比 C++ 小多少。
|
||||
C++ 名称来自于递增运算符,同时也代表着“比 C 更进一步”。在早期的 Java 演讲中,*Bill Joy*(Java 作者之一)说“**Java = C ++ --**”(C++ 减减)。这意味着 Java 是在 C++ 的基础上减少了许多不必要的东西,因此语言更简单。随着进一步地学习,我们会发现 Java 的确有许多地方相对 C++ 来说更简便,但是在其他方面,难度并不会比 C++ 小多少。
|
||||
|
||||
|
||||
<!-- Relational-Operators -->
|
||||
@@ -354,9 +354,9 @@ public class EqualsMethod2 {
|
||||
false
|
||||
```
|
||||
|
||||
上例的结果再次令人困惑:结果是错误的。原因: **equals()** 的默认行为是比较对象的引用而非具体内容。因此,除非你在新类中重写 **equals()** 方法,否则我们将获取不到想要的结果。不幸的是,在学习 [复用](./08-Reuse.md)(**Reuse**) 章节后我们才能接触到“覆盖”(**override**),并且直到 [附录:集合主题](./Appendix-Collection-Topics.md),才能知道定义 **equals()** 方法的正确方式,但是现在明白 **equals()** 行为方式也可能为你节省一些时间。
|
||||
上例的结果再次令人困惑:结果是错误的。原因: `equals()` 的默认行为是比较对象的引用而非具体内容。因此,除非你在新类中重写 `equals()` 方法,否则我们将获取不到想要的结果。不幸的是,在学习 [复用](./08-Reuse.md)(**Reuse**) 章节后我们才能接触到“覆盖”(**Override**),并且直到 [附录:集合主题](./Appendix-Collection-Topics.md),才能知道定义 **equals()** 方法的正确方式,但是现在明白 `equals()` 行为方式也可能为你节省一些时间。
|
||||
|
||||
大多数 Java 库类通过覆写 **equals()** 方法比较对象的内容而不是其引用。
|
||||
大多数 Java 库类通过覆写 `equals()` 方法比较对象的内容而不是其引用。
|
||||
|
||||
<!-- Logical-Operators -->
|
||||
## 逻辑运算符
|
||||
@@ -408,7 +408,7 @@ i != j is true
|
||||
```
|
||||
|
||||
在 Java 逻辑运算中,我们不能像 C/C++ 那样使用非布尔值, 而仅能使用 **AND**、**OR**、**NOT**。
|
||||
下面是一次错误的尝试: ~~a || -~~。 但是,后续表达式使用关系比较生成布尔值,然后对结果使用逻辑运算结果。请注意,如果在预期为 **String** 类型的位置使用 **boolean**类型的值,则结果会自动转为适当的文本格式。
|
||||
下面是一次错误的尝试: ~~a || -~~。 但是,后续表达式使用关系比较生成布尔值,然后对结果使用逻辑运算结果。请注意,如果在预期为 **String** 类型的位置使用 **boolean** 类型的值,则结果会自动转为适当的文本格式。
|
||||
|
||||
我们可以将前一个程序中 **int** 的定义替换为除 **boolean** 之外的任何其他基本数据类型。
|
||||
但请注意,**float** 类型的数值比较非常严格。只要最小位部分的数字不同则两个数值之间的比较仍然是“非等”的;只要数字最小位是大于 0 的,那么它就不等于 0。
|
||||
@@ -417,7 +417,7 @@ i != j is true
|
||||
<!-- Short-Circuiting -->
|
||||
### 短路
|
||||
|
||||
逻辑运算符支持一种称为“短路”(*short-circuiting*)的现象。整个表达式会在运算到可以明确结果时就停止并返回结果,这意味着该逻辑表达式的后半部分不会被执行到。代码示例:
|
||||
逻辑运算符支持一种称为“短路”(short-circuiting)的现象。整个表达式会在运算到可以明确结果时就停止并返回结果,这意味着该逻辑表达式的后半部分不会被执行到。代码示例:
|
||||
|
||||
```java
|
||||
// operators / ShortCircuit.java
|
||||
@@ -549,7 +549,7 @@ bll: 101111101011111010111110101111
|
||||
|
||||
Java 7 引入了二进制的字面值常量,由前导 `0B` 或 `0B` 表示,它可以初始化所有的整数类型。
|
||||
|
||||
使用整型数值类型时,显示其二进制形式会很有用。在 Long 型和 Integer 型中这很容易实现,调用其静态的 **toBinaryString()** 方法即可。 但是请注意,若将较小的类型传递给 **Integer.tobinarystring()** 时,类型将自动转换为 **int**。
|
||||
使用整型数值类型时,显示其二进制形式会很有用。在 Long 型和 Integer 型中这很容易实现,调用其静态的 `toBinaryString()` 方法即可。 但是请注意,若将较小的类型传递给 **Integer.**`tobinarystring()` 时,类型将自动转换为 **int**。
|
||||
|
||||
<!-- Underscores in Literals -->
|
||||
### 下划线
|
||||
@@ -653,14 +653,14 @@ float f4 = 1e-47f; //10 的幂数
|
||||
|
||||
按位运算符和逻辑运算符都使用了同样的字符,只不过数量不同。“按位”短“移位”长。按位运算符可与等号 `=` 联合使用以接收结果及赋值:`&=`,`|=` 和 `^=` 都是合法的(由于 `~` 是一元运算符,所以不可与 `=` 联合使用)。
|
||||
|
||||
我们将 **Boolean** 类型被视为“单位值”(*one-bit value*)对待,所以它多少有些独特的地方。我们可以执行按位“与”、“或”、“异或”,但不能执行按位“非”(大概是为了避免与逻辑“非”混淆)。对于布尔值,按位运算符具有与逻辑运算符相同的效果,只是它们不会中途“短路”。此外,针对布尔值进行的按位运算为我们新增了一个“异或”逻辑运算符,它并未包括在“逻辑”运算符的列表中。在移位表达式中,禁止使用布尔运算,原因将在下面解释。
|
||||
我们将 **Boolean** 类型被视为“单位值”(one-bit value)对待,所以它多少有些独特的地方。我们可以执行按位“与”、“或”、“异或”,但不能执行按位“非”(大概是为了避免与逻辑“非”混淆)。对于布尔值,按位运算符具有与逻辑运算符相同的效果,只是它们不会中途“短路”。此外,针对布尔值进行的按位运算为我们新增了一个“异或”逻辑运算符,它并未包括在“逻辑”运算符的列表中。在移位表达式中,禁止使用布尔运算,原因将在下面解释。
|
||||
|
||||
|
||||
<!-- Shift Operators -->
|
||||
## 移位运算符
|
||||
|
||||
|
||||
移位运算符面向的运算对象也是二进制的“位”。可单独用它们处理整数类型(基本类型的一种)。左移位运算符 `<<` 能将其左边的运算对象向左移动右侧指定的位数(在低位补 0)。右移位运算符 `>>` 则相反。位移运算符有“正”、“负”值”:若值为正,则在高位插入 0;若值为负,则在高位插入 1。Java 也添加了一种“不分正负”的右移位运算符(>>>),它使用了“零扩展”(*zero extension*):无论正负,都在高位插入 0。这一运算符是 C/C++ 没有的。
|
||||
移位运算符面向的运算对象也是二进制的“位”。可单独用它们处理整数类型(基本类型的一种)。左移位运算符 `<<` 能将其左边的运算对象向左移动右侧指定的位数(在低位补 0)。右移位运算符 `>>` 则相反。位移运算符有“正”、“负”值”:若值为正,则在高位插入 0;若值为负,则在高位插入 1。Java 也添加了一种“不分正负”的右移位运算符(>>>),它使用了“零扩展”(zero extension):无论正负,都在高位插入 0。这一运算符是 C/C++ 没有的。
|
||||
|
||||
如果移动 **char**、**byte** 或 **short**,则会在移动发生之前将其提升为 **int**,结果为 **int**。仅使用右侧的 5 个低阶位。这可以防止我们移动超过 **int** 范围的位数。若对一个 **long** 值进行处理,最后得到的结果也是 **long**。
|
||||
|
||||
@@ -816,7 +816,7 @@ i >>> 5, int: 97591828, binary:
|
||||
```
|
||||
|
||||
|
||||
结尾的两个方法 **printBinaryInt()** 和 **printBinaryLong()** 它们分别操作一个 **int** 和 **long** 值,并转换为二进制格式输出,同时附有简要的文字说明。除了演示 **int** 和 **long** 的所有位运算符的效果之外,本示例还显示 **int** 和 **long** 的最小值、最大值、+1 和 -1 值,以便我们了解它们的形式。注意高位代表符号:0 表示正,1 表示负。上面显示了 **int** 部分的输出。数字的二进制表示称为有符号的两个补数。
|
||||
结尾的两个方法 `printBinaryInt()` 和 `printBinaryLong()` 它们分别操作一个 **int** 和 **long** 值,并转换为二进制格式输出,同时附有简要的文字说明。除了演示 **int** 和 **long** 的所有位运算符的效果之外,本示例还显示 **int** 和 **long** 的最小值、最大值、+1 和 -1 值,以便我们了解它们的形式。注意高位代表符号:0 表示正,1 表示负。上面显示了 **int** 部分的输出。数字的二进制表示称为有符号的两个补数。
|
||||
|
||||
|
||||
<!-- Ternary-if-else-Operator -->
|
||||
@@ -864,7 +864,7 @@ static int standardIfElse(int i) {
|
||||
100
|
||||
```
|
||||
|
||||
可以看出,**ternary()** 中的代码更简短。然而,**standardIfElse()** 中的代码更易理解且不要求更多的录入。所以我们在挑选三元运算符时,请务必权衡一下利弊。
|
||||
可以看出,`ternary()` 中的代码更简短。然而,**standardIfElse()** 中的代码更易理解且不要求更多的录入。所以我们在挑选三元运算符时,请务必权衡一下利弊。
|
||||
|
||||
|
||||
<!-- String-Operator-+-and-+= -->
|
||||
@@ -1483,7 +1483,7 @@ bigger = -4
|
||||
<!-- Summary -->
|
||||
## 本章小结
|
||||
|
||||
如果你已接触过一门 C 语法风格编程语言,那么你在学习 Java 的运算符时实际上没有任何曲线。如果你觉得有难度,那么我推荐你要先去 www.OnJava8.com 观看 *Thinking in C* 的视频教程来补充一些前置知识储备。
|
||||
如果你已接触过一门 C 语法风格编程语言,那么你在学习 Java 的运算符时实际上没有任何曲线。如果你觉得有难度,那么我推荐你要先去 www.OnJava8.com 观看 《Thinking in C》 的视频教程来补充一些前置知识储备。
|
||||
|
||||
[^1]: 我在 *Pomona College* 大学读过两年本科,在那里 47 被称之为“魔法数字”(*magic number*),详见 [维基百科](https://en.wikipedia.org/wiki/47_(number)) 。
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ else
|
||||
“statement”
|
||||
```
|
||||
|
||||
布尔表达式(*Boolean-expression*)必须生成 **boolean** 类型的结果,执行语句 `statement` 既可以是只包含单个分号 `;` 的简单语句,也可以是包含大括号 `{}` 的复合语句 —— 封闭在大括号内的一组简单语句。 凡本书中提及的“statement”一词,皆表示类似的执行语句。
|
||||
布尔表达式(Boolean-expression)必须生成 **boolean** 类型的结果,执行语句 `statement` 既可以是只包含单个分号 `;` 的简单语句,也可以是包含大括号 `{}` 的复合语句 —— 封闭在大括号内的一组简单语句。 凡本书中提及的“statement”一词,皆表示类似的执行语句。
|
||||
|
||||
下面是一个有关 **if-else** 语句的例子。`test()` 方法可以告知你两个数值之间的大小关系。代码示例:
|
||||
|
||||
@@ -168,7 +168,7 @@ for(initialization; Boolean-expression; step)
|
||||
statement
|
||||
```
|
||||
|
||||
初始化 (*initialization*) 表达式、布尔表达式 (*Boolean-expression*) ,或者步进 (*step*) 运算,都可以为空。每次迭代之前都会判断布尔表达式的结果是否成立。一旦计算结果为 `false`,则跳出 **for** 循环体并继续执行后面代码。 每次循环结束时,都会执行一次步进。
|
||||
初始化 (initialization) 表达式、布尔表达式 (Boolean-expression) ,或者步进 (step) 运算,都可以为空。每次迭代之前都会判断布尔表达式的结果是否成立。一旦计算结果为 `false`,则跳出 **for** 循环体并继续执行后面代码。 每次循环结束时,都会执行一次步进。
|
||||
|
||||
**for** 循环通常用于“计数”任务。代码示例:
|
||||
|
||||
@@ -204,7 +204,7 @@ value: 106 character: j
|
||||
|
||||
**注意**:变量 **c** 是在 **for** 循环执行时才被定义的,并不是在 **main()** 的开头。**c** 的作用域范围仅在 **for** 循环体内。
|
||||
|
||||
传统的面向过程语言如 C 需要先在代码块(*block*)前定义好所有变量才能够使用。这样编译器才能在创建块时,为这些变量分配内存空间。在 Java 和 C++ 中,我们可以在整个块使用变量声明,并且可以在需要时才定义变量。 这种自然的编码风格使我们的代码更容易被人理解 [^1]。
|
||||
传统的面向过程语言如 C 需要先在代码块(block)前定义好所有变量才能够使用。这样编译器才能在创建块时,为这些变量分配内存空间。在 Java 和 C++ 中,我们可以在整个块使用变量声明,并且可以在需要时才定义变量。 这种自然的编码风格使我们的代码更容易被人理解 [^1]。
|
||||
|
||||
上例使用了 **java.lang.Character** 包装类,该类不仅包含了基本类型 `char` 的值,还封装了一些有用的方法。例如这里就用到了静态方法 `isLowerCase()` 来判断字符否为小写。
|
||||
|
||||
@@ -239,7 +239,7 @@ i = 4 j = 8
|
||||
|
||||
## for-in语法
|
||||
|
||||
Java 5 引入了更为简洁的“增强版 **for** 循环”语法来操纵数组和集合。(更多细节,可参考 [数组](./21-Arrays.md) 和 [集合](./12-Collections.md) 章节内容)。,大部分文档也称其为 **for-each** 语法,但因为了不与 Java 8 新添的 **forEach()** 产生混淆,因此我称之为 **for-in** 循环。 (Python 已有类似的先例,如:**for x in sequence**)。**注意**:你可能会在其他地方看到不同叫法。
|
||||
Java 5 引入了更为简洁的“增强版 **for** 循环”语法来操纵数组和集合。(更多细节,可参考 [数组](./21-Arrays.md) 和 [集合](./12-Collections.md) 章节内容)。,大部分文档也称其为 **for-each** 语法,但因为了不与 Java 8 新添的 `forEach()` 产生混淆,因此我称之为 **for-in** 循环。 (Python 已有类似的先例,如:**for x in sequence**)。**注意**:你可能会在其他地方看到不同叫法。
|
||||
|
||||
**for-in** 无需你去创建 **int** 变量和步进来控制循环计数。 下面我们来遍历获取 **float** 数组中的元素。代码示例:
|
||||
|
||||
@@ -313,7 +313,7 @@ for(int i = 0; i < 100; i++)
|
||||
|
||||
正因如此,除非先创建一个 **int** 数组,否则我们无法使用 **for-in** 循环来操作。为简化测试过程,我已在 `onjava` 包中封装了 **Range** 类,利用其 `range()` 方法可自动生成恰当的数组。
|
||||
|
||||
在 [封装](./07-Implementation-Hiding.md)(*Implementation Hiding*)这一章里我们介绍了静态导入(*static import*),无需了解细节就可以直接使用。 有关静态导入的语法,可以在 **import** 语句中看到:
|
||||
在 [封装](./07-Implementation-Hiding.md)(Implementation Hiding)这一章里我们介绍了静态导入(static import),无需了解细节就可以直接使用。 有关静态导入的语法,可以在 **import** 语句中看到:
|
||||
|
||||
```java
|
||||
// control/ForInInt.java
|
||||
@@ -451,7 +451,7 @@ public class BreakAndContinue {
|
||||
<!--The Infamous “Goto”-->
|
||||
## 臭名昭著的goto
|
||||
|
||||
[**goto** 关键字](https://en.wikipedia.org/wiki/Goto) 很早就在程序设计语言中出现。事实上,**goto** 起源于[汇编](https://en.wikipedia.org/wiki/Assembly_language)(*assembly language*)语言中的程序控制:“若条件 A 成立,则跳到这里;否则跳到那里”。如果你读过由由编译器编译后的代码,你会发现在其程序控制中充斥了大量的跳转。较之汇编产生的代码直接运行在硬件 CPU 中,Java 也会产生自己的“汇编代码”(字节码),只不过它是运行在 Java 虚拟机里的(*Java Virtual Machine*)。
|
||||
[**goto** 关键字](https://en.wikipedia.org/wiki/Goto) 很早就在程序设计语言中出现。事实上,**goto** 起源于[汇编](https://en.wikipedia.org/wiki/Assembly_language)(assembly language)语言中的程序控制:“若条件 A 成立,则跳到这里;否则跳到那里”。如果你读过由由编译器编译后的代码,你会发现在其程序控制中充斥了大量的跳转。较之汇编产生的代码直接运行在硬件 CPU 中,Java 也会产生自己的“汇编代码”(字节码),只不过它是运行在 Java 虚拟机里的(Java Virtual Machine)。
|
||||
|
||||
一个源码级别跳转的 **goto**,为何招致名誉扫地呢?若程序总是从一处跳转到另一处,还有什么办法能识别代码的控制流程呢?随着 *Edsger Dijkstra*发表著名的 “Goto 有害” 论(*Goto considered harmful*)以后,**goto** 便从此失宠。甚至有人建议将它从关键字中剔除。
|
||||
|
||||
@@ -822,7 +822,7 @@ java RandomBounds upper
|
||||
|
||||
## 本章小结
|
||||
|
||||
本章总结了我们对大多数编程语言中出现的基本特性的探索:计算,运算符优先级,类型转换,选择和迭代。 现在让我们准备好,开始步入面向对象和函数式编程的世界吧。 下一章的内容涵盖了 Java 编程中的重要问题:对象的[初始化和清理](./06-Housekeeping.md)。紧接着,还会介绍[封装](./07-Implementation-Hiding.md)(*implementation hiding*)的核心概念。
|
||||
本章总结了我们对大多数编程语言中出现的基本特性的探索:计算,运算符优先级,类型转换,选择和迭代。 现在让我们准备好,开始步入面向对象和函数式编程的世界吧。 下一章的内容涵盖了 Java 编程中的重要问题:对象的[初始化和清理](./06-Housekeeping.md)。紧接着,还会介绍[封装](./07-Implementation-Hiding.md)(implementation hiding)的核心概念。
|
||||
|
||||
<!--下面是脚注-->
|
||||
[^1]: 在早期的语言中,许多决策都是基于让编译器设计者的体验更好。 但在现代语言设计中,许多决策都是为了提高语言使用者的体验,尽管有时会有妥协 —— 这通常会让语言设计者后悔。<sup>
|
||||
|
||||
@@ -6,11 +6,11 @@
|
||||
|
||||
> Java 新I/O 库是在 1.4 版本引入到 `Java .nio.* package` 中的,旨在更快速。
|
||||
|
||||
实际上,新 I/O 使用 **NIO**(同步非阻塞)的方式重写了老的 I/O 了,因此它获得了 **NIO** 的种种优点。即使我们不显式地使用 **NIO** 方式来编写代码,也能带来性能和速度的提高。这种提升不仅仅体现在文件读写(*File I/O*),同时也体现在网络读写(*Network I/O*)中。例如,网络编程。
|
||||
实际上,新 I/O 使用 **NIO**(同步非阻塞)的方式重写了老的 I/O 了,因此它获得了 **NIO** 的种种优点。即使我们不显式地使用 **NIO** 方式来编写代码,也能带来性能和速度的提高。这种提升不仅仅体现在文件读写(File I/O),同时也体现在网络读写(Network I/O)中。例如,网络编程。
|
||||
|
||||
速度的提升来自于使用了更接近操作系统 I/O 执行方式的结构:**Channel**(通道) 和 **Buffer**(缓冲区)。我们可以想象一个煤矿:通道就是连接矿层(数据)的矿井,缓冲区是运送煤矿的小车。通过小车装煤,再从车里取矿。换句话说,我们不能直接和 **Channel** 交互; 我们需要与 **Buffer** 交互并将 **Buffer** 中的数据发送到**Channel** 中;**Channel** 需要从 **Buffer** 中提取或放入数据。
|
||||
速度的提升来自于使用了更接近操作系统 I/O 执行方式的结构:**Channel**(通道) 和 **Buffer**(缓冲区)。我们可以想象一个煤矿:通道就是连接矿层(数据)的矿井,缓冲区是运送煤矿的小车。通过小车装煤,再从车里取矿。换句话说,我们不能直接和 **Channel** 交互; 我们需要与 **Buffer** 交互并将 **Buffer** 中的数据发送到 **Channel** 中;**Channel** 需要从 **Buffer** 中提取或放入数据。
|
||||
|
||||
本篇我们将深入探讨 `nio` 包。虽然 像 I/O 流这样的高级库使用了 **NIO**,但多数时候,我们考虑这个层次的问题。使用Java 7 和 8 版本,理想情况下我们甚至不必费心去处理 I/O 流。当然,一些特殊情况除外。在[文件](./17-Files.md)(*File*)一章中基本涵盖了我们日常使用的相关内容。只有在遇到性能瓶颈(例如内存映射文件)或创建自己的 I/O 库时,我们才需要去理解 **NIO**。
|
||||
本篇我们将深入探讨 `nio` 包。虽然 像 I/O 流这样的高级库使用了 **NIO**,但多数时候,我们考虑这个层次的问题。使用Java 7 和 8 版本,理想情况下我们甚至不必费心去处理 I/O 流。当然,一些特殊情况除外。在[文件](./17-Files.md)(**File**)一章中基本涵盖了我们日常使用的相关内容。只有在遇到性能瓶颈(例如内存映射文件)或创建自己的 I/O 库时,我们才需要去理解 **NIO**。
|
||||
|
||||
|
||||
<!-- ByteBufferS -->
|
||||
@@ -344,7 +344,7 @@ gb2312-1980
|
||||
|
||||
|
||||
|
||||
回到 **BufferToText.java** 中,如果你 `rewind()` 缓冲区(回到数据的开头),使用该平台的默认字符集 `decode()` 数据,那么生成的 **CharBuffer** 数据将在控制台上正常显示。可以通过 **System.getProperty(“file.encoding”)** 方法来查看平台默认字符集名称。传递该名称参数到 **Charset.forName()** 方法可以生成对应的 `Charset` 对象用于解码字符串。
|
||||
回到 **BufferToText.java** 中,如果你 `rewind()` 缓冲区(回到数据的开头),使用该平台的默认字符集 `decode()` 数据,那么生成的 **CharBuffer** 数据将在控制台上正常显示。可以通过 `System.getProperty(“file.encoding”)` 方法来查看平台默认字符集名称。传递该名称参数到 `Charset.forName()` 方法可以生成对应的 `Charset` 对象用于解码字符串。
|
||||
|
||||
另一种方法是使用字符集 `encode()` 方法,该字符集在读取文件时生成可打印的内容,如你在 **BufferToText.java** 的第三部分中所看到的。上例中,**UTF-16BE** 被用于将文本写入文件,当文本被读取时,你所要做的就是将其转换为 **CharBuffer**,并生成预期的文本。
|
||||
|
||||
@@ -421,7 +421,7 @@ H o w d y !
|
||||
|
||||
在分配 **ByteBuffer** 之后,我们检查并确认它的 1,024 元素被初始化为 0。(截至到达 `limit()` 结果的位置)。
|
||||
|
||||
将基本类型数据插入 **ByteBuffer** 的最简单方法就是使用 `asCharBuffer()`、`asShortBuffer()` 等方法获取该缓冲区适当的“视图”(*View*),然后调用该“视图”的 `put()` 方法。
|
||||
将基本类型数据插入 **ByteBuffer** 的最简单方法就是使用 `asCharBuffer()`、`asShortBuffer()` 等方法获取该缓冲区适当的“视图”(View),然后调用该“视图”的 `put()` 方法。
|
||||
|
||||
这是针对每种基本数据类型执行的。其中唯一有点奇怪的是 **ShortBuffer** 的 `put()`,它需要类型强制转换。其他视图缓冲区不需要在其 `put()` 方法中进行转换。
|
||||
|
||||
@@ -430,7 +430,7 @@ H o w d y !
|
||||
## 视图缓冲区
|
||||
|
||||
|
||||
“视图缓冲区”(*view buffer*)是通过特定的基本类型的窗口来查看底层 **ByteBuffer**。**ByteBuffer** 仍然是“支持”视图的实际存储,因此对视图所做的任何更改都反映在对 **ByteBuffer** 中的数据的修改中。
|
||||
“视图缓冲区”(view buffer)是通过特定的基本类型的窗口来查看底层 **ByteBuffer**。**ByteBuffer** 仍然是“支持”视图的实际存储,因此对视图所做的任何更改都反映在对 **ByteBuffer** 中的数据的修改中。
|
||||
|
||||
如前面的示例所示,这方便地将基本类型插入 **ByteBuffer**。视图缓冲区还可以从 **ByteBuffer** 读取基本类型数据,每次单个(**ByteBuffer** 规定),或者批量读取到数组。下面是一个通过 **IntBuffer** 在 **ByteBuffer** 中操作 **int** 的例子:
|
||||
|
||||
@@ -563,7 +563,7 @@ Double Buffer 0 -> 4.8E-322,
|
||||
<!-- Endians -->
|
||||
### 字节存储次序
|
||||
|
||||
不同的机器可以使用不同的字节存储顺序(*Endians*)来存储数据。“高位优先”(*Big Endian*):将最重要的字节放在最低内存地址中,而“低位优先”(*Little Endian*):将最重要的字节放在最高内存地址中。
|
||||
不同的机器可以使用不同的字节存储顺序(Endians)来存储数据。“高位优先”(Big Endian):将最重要的字节放在最低内存地址中,而“低位优先”(Little Endian):将最重要的字节放在最高内存地址中。
|
||||
|
||||
当存储大于单字节的数据时,如 **int**、**float** 等,我们可能需要考虑字节排序问题。**ByteBuffer** 以“高位优先”形式存储数据;通过网络发送的数据总是使用“高位优先”形式。我们可以 使用 **ByteOrder** 的 `order()` 方法和参数 **ByteOrder.BIG_ENDIAN** 或 **ByteOrder.LITTLE_ENDIAN** 来改变它的字节存储次序。
|
||||
|
||||
@@ -572,7 +572,7 @@ Double Buffer 0 -> 4.8E-322,
|
||||

|
||||
|
||||
|
||||
将数据作为 **short** 型来读取(**ByteBuffer.asshortbuffer()**)),生成数字 97 (00000000 01100001)。更改为“低位优先”后 将生成数字 24832 (01100001 00000000)。
|
||||
将数据作为 **short** 型来读取(`ByteBuffer.asshortbuffer()`)),生成数字 97 (00000000 01100001)。更改为“低位优先”后 将生成数字 24832 (01100001 00000000)。
|
||||
|
||||
这显示了字节顺序的变化取决于字节存储次序设置:
|
||||
|
||||
@@ -628,20 +628,20 @@ public class Endians {
|
||||
|
||||
### 缓冲区细节
|
||||
|
||||
缓冲区由数据和四个索引组成,以有效地访问和操作该数据:*mark*、*position*、*limit* 和 *capacity*(标记、位置、限制和容量)。伴随着的还有一组方法可以设置和重置这些索引,并可查询它们的值。
|
||||
缓冲区由数据和四个索引组成,以有效地访问和操作该数据:mark、position、limit 和 capacity(标记、位置、限制和容量)。伴随着的还有一组方法可以设置和重置这些索引,并可查询它们的值。
|
||||
|
||||
| | |
|
||||
| :----- | :----- |
|
||||
| **capacity()** | 返回缓冲区的 *capacity* |
|
||||
|**clear()** |清除缓冲区,将 *position* 设置为零并 设 *limit* 为 *capacity*;可调用此方法来覆盖现有缓冲区|
|
||||
|**flip()** | 将 *limit* 设置为 *position*,并将 *position* 设置为 0;此方法用于准备缓冲区,以便在数据写入缓冲区后进行读取|
|
||||
|**limit()** |返回 *limit* 的值|
|
||||
|**limit(int limit)**| 重设 *limit*|
|
||||
|**mark()** |设置 *mark* 为当前的 *position* |
|
||||
|**position()** |返回 *position* |
|
||||
|**position(int pos)**| 设置 *position*|
|
||||
|**remaining()** |返回 *limit* 到 *position* |
|
||||
|**hasRemaining()**| 如果在 *position* 与 *limit* 中间有元素,返回 `true`|
|
||||
| **capacity()** | 返回缓冲区的 capacity |
|
||||
|**clear()** |清除缓冲区,将 position 设置为零并 设 limit 为 capacity;可调用此方法来覆盖现有缓冲区|
|
||||
|**flip()** | 将 limit 设置为 position,并将 position 设置为 0;此方法用于准备缓冲区,以便在数据写入缓冲区后进行读取|
|
||||
|**limit()** |返回 limit 的值|
|
||||
|**limit(int limit)**| 重设 limit|
|
||||
|**mark()** |设置 mark 为当前的 position |
|
||||
|**position()** |返回 position |
|
||||
|**position(int pos)**| 设置 position|
|
||||
|**remaining()** |返回 limit 到 position |
|
||||
|**hasRemaining()**| 如果在 position 与 limit 中间有元素,返回 `true`|
|
||||
|
||||
从缓冲区插入和提取数据的方法通过更新索引来反映所做的更改。下例使用一种非常简单的算法(交换相邻字符)来对 **CharBuffer** 中的字符进行加扰和解扰。代码示例:
|
||||
|
||||
@@ -689,21 +689,21 @@ UsingBuffers
|
||||
|
||||
虽然可以通过使用 **char** 数组调用 `wrap()` 直接生成 **CharBuffer**,但是底层的 **ByteBuffer** 将被分配,而 **CharBuffer** 将作为 **ByteBuffer** 上的视图生成。这强调了目标始终是操作 **ByteBuffer**,因为它与通道交互。
|
||||
|
||||
下面是程序在 **symmetricgrab()** 方法入口时缓冲区的样子:
|
||||
下面是程序在 `symmetricgrab()` 方法入口时缓冲区的样子:
|
||||
|
||||

|
||||
|
||||
*position* 指向缓冲区中的第一个元素,*capacity* 和 *limit* 紧接在最后一个元素之后。在`symmetricgrab()` 中,**while** 循环迭代到 *position* 等于 *limit*。当在缓冲区上调用相对位置的 `get()` 或 `put()` 函数时,缓冲区的位置会发生变化。你可以调用绝对位置的 `get()` 和 `put()` 方法,它们包含索引参数:`get()` 或 `put()` 发生的位置。这些方法不修改缓冲区 *position* 的值。
|
||||
position 指向缓冲区中的第一个元素,capacity 和 limit 紧接在最后一个元素之后。在`symmetricgrab()` 中,**while** 循环迭代到 position 等于 limit。当在缓冲区上调用相对位置的 `get()` 或 `put()` 函数时,缓冲区的位置会发生变化。你可以调用绝对位置的 `get()` 和 `put()` 方法,它们包含索引参数:`get()` 或 `put()` 发生的位置。这些方法不修改缓冲区 position 的值。
|
||||
|
||||
当控件进入 **while** 循环时,使用 `mark()` 设置 *mark* 的值。缓冲区的状态为:
|
||||
当控件进入 **while** 循环时,使用 `mark()` 设置 mark 的值。缓冲区的状态为:
|
||||
|
||||

|
||||
|
||||
两个相对 *get()* 调用将前两个字符的值保存在变量 `c1` 和 `c2` 中。在这两个调用之后,缓冲区看起来是这样的:
|
||||
两个相对 `get()` 调用将前两个字符的值保存在变量 `c1` 和 `c2` 中。在这两个调用之后,缓冲区看起来是这样的:
|
||||
|
||||

|
||||
|
||||
为了执行交换,我们在位置 0 处编写 `c2`,在位置 1 处编写 `c1`。我们可以使用绝对 `put()` 方法来实现这一点,或者用 `reset()` 方法,将 *position* 的值设置为 *mark*:
|
||||
为了执行交换,我们在位置 0 处编写 `c2`,在位置 1 处编写 `c1`。我们可以使用绝对 `put()` 方法来实现这一点,或者用 `reset()` 方法,将 position 的值设置为 mark:
|
||||
|
||||

|
||||
|
||||
@@ -711,11 +711,11 @@ UsingBuffers
|
||||
|
||||

|
||||
|
||||
在下一次循环中,将 *mark* 设置为 *position* 的当前值:
|
||||
在下一次循环中,将 mark 设置为 position 的当前值:
|
||||
|
||||

|
||||
|
||||
该过程将继续,直到遍历整个缓冲区为止。在 **while** 循环的末尾,*position* 位于缓冲区的末尾。如果显示缓冲区,则只显示位置和限制之间的字符。因此,要显示缓冲区的全部内容,必须使用 `rewind()` 将 *position* 设置为缓冲区的开始位置。这是 `rewind()` 调用后缓冲区的状态(*mark* 的值变成未定义):
|
||||
该过程将继续,直到遍历整个缓冲区为止。在 **while** 循环的末尾,position 位于缓冲区的末尾。如果显示缓冲区,则只显示位置和限制之间的字符。因此,要显示缓冲区的全部内容,必须使用 `rewind()` 将 position 设置为缓冲区的开始位置。这是 `rewind()` 调用后缓冲区的状态(mark 的值变成未定义):
|
||||
|
||||

|
||||
|
||||
@@ -927,7 +927,7 @@ Stream Read/Write: 4.069
|
||||
Mapped Read/Write: 0.013
|
||||
```
|
||||
|
||||
**Tester** 使用了模板方法(*Template Method*)模式,它为匿名内部子类中定义的 `test()` 的各种实现创建一个测试框架。每个子类都执行一种测试,因此 `test()` 方法还提供了执行各种I/O 活动的原型。
|
||||
**Tester** 使用了模板方法(Template Method)模式,它为匿名内部子类中定义的 `test()` 的各种实现创建一个测试框架。每个子类都执行一种测试,因此 `test()` 方法还提供了执行各种I/O 活动的原型。
|
||||
|
||||
虽然映射的写似乎使用 **FileOutputStream**,但是文件映射中的所有输出必须使用 **RandomAccessFile**,就像前面代码中的读/写一样。
|
||||
|
||||
@@ -990,7 +990,7 @@ Released Lock
|
||||
|
||||
锁定文件的一部分,锁住 **size-position** 区域。第三个参数指定是否共享此锁。
|
||||
|
||||
虽然零参数锁定方法适应文件大小的变化,但是如果文件大小发生变化,具有固定大小的锁不会发生变化。如果从一个位置到另一个位置获得一个锁,并且文件的增长超过了 *position* + *size* ,那么超出 *position* + *size* 的部分没有被锁定。零参数锁定方法锁定整个文件,即使它在增长。
|
||||
虽然零参数锁定方法适应文件大小的变化,但是如果文件大小发生变化,具有固定大小的锁不会发生变化。如果从一个位置到另一个位置获得一个锁,并且文件的增长超过了 position + size ,那么超出 position + size 的部分没有被锁定。零参数锁定方法锁定整个文件,即使它在增长。
|
||||
|
||||
底层操作系统必须提供对独占锁或共享锁的支持。如果操作系统不支持共享锁并且对一个操作系统发出请求,则使用独占锁。可以使用 **FileLock.**`isShared()` 查询锁的类型(共享或独占)。
|
||||
|
||||
|
||||
Reference in New Issue
Block a user