java编程思想第四版前三章读书笔记

前言

最近一段时间重读了java编程思想,把一些东西重新理解记录一遍

目的

整理知识点,方便以后回顾

正文

第一章 对象导论

1.抽象过程:

万物皆对象;
程序是对象的集合,它们通过发送消息来告知彼此所要做的;
每个对象都有自己的由其它对象构成的存储。
每个对象都有其类型,每个对象都是某个类的一个实例(instance);
某一特定类型的所有对象都可以接受同样的消息。

2.每个对象都有一个接口

每个对象都只能满足某些请求,这些请求是由对象的接口(interface)所定义的,决定接口的便是类型。

3.每个对象都提供服务

将对象看做服务者可以提高对象的内聚性。高内聚性是软件设计的基本质量要求之一,可以将一个拥有很多功能的对象拆分成多个各司其职的对象。

4.被隐藏的具体实现

为什么要进行访问控制?
(1)让客户端程序员无法触及他们不该触及的部分——这些部分对于内部操作是必须的,但不是用户解决特定问题所需要的接口的一部分。减少客户端程序员需要考虑的东西,忽略不重要的东西。
(2)允许库设计者可以改变内部的工作方式而不用担心会影响到客户端程序员

5.复用具体实现

复用对象:
(1)直接使用该类的对象;
(2)创建一个成员对象;
使用现有的类合成新的类,称为“组合”,如果组合是动态发生的,则称为“聚合”。
在创建新类时,优先考虑组合,再考虑继承。

上面的关系可以解读如下:

1
2
3
4
5
6
(关联)Association:A类有B类有逻辑上的连接
(聚合)Aggregation : A类有一个B类
(组合)Composition : A类拥有一个B类
(依赖)Dependency : A类使用了B类
(继承)Inheritance : B类是一个A类 (或者B类扩展A类)
(实现)Realization : B类实现了接口A

6.继承

导出类和基类具有相同的类型即一个圆也是一个几何形
有两种方法可以使基类和导出类产生差异

1
2
直接在导出类中添加新方法,这些新方法并不是基类接口的一部分,应该考虑的是基类是否应该具备这些方法
另一种更重要的使基类和导出类之间产生差异的方法是改变现有基类的方法,称为覆盖

“是一个”和“像是一个”的关系
如果继承只覆盖基类的方法,意味着导出类和基类是完全相同的类型,他们具有完全相同的接口,这被称为纯粹替代。某种意义上,是继承的理想方式,为is-a关系
如果继承在导出类必须添加新的接口元素,扩展了接口,这种替代并不完美,为is-like-a(像是一个)关系

7.伴随多态的可交互对象

将导出类看做它的基类的过程称为向上转型。
方法可以在不知道对象的实际类型时,做出正确的行为。
后期绑定

8.单根继承结构

除了C以外的所有OOP语言,所有类最终都继承自单一的基类。这个终极基类即Object;
单根继承使所有对象都具有统一的接口,给编程带来了更大的灵活性。
垃圾回收器的实现变的容易许多。
C
如果这样,优点:额外的灵活性; 缺点:需要构建自己的继承体系,不兼容

9.容器

不同的容器提供了不同类型的接口和外部行;
不同的容器对于某些操作具有不同的效率
为了避免向下转型为错误的类型,因此有了参数化类型机制,参数化类型就是一个编译器可以自动定制用于特定类型上的类。

10.对象的创建和生命周期

对象的创建
C++认为效率控制是最重要的议题,在堆栈或者静态存储区域创建对象。
Java在创建对象的时候可以不用知道对象的确切数量,生命周期和类型。
Java完全采用了动态内存分配的方式。它认为对象变得复杂后,查找和释放存储空间的开销就不那么那么重要了。提高灵活性,牺牲了时间。
对象的生命周期
Java提供了“垃圾回收器”机制,可以自动发现对象何时不再被使用,继而销毁它。
垃圾回收器可以避免内存溢出的问题。

11.异常处理:处理错误

异常是一种对象,它从出错地点被“抛出”,并被专门设计用来处理特定类型错误的相应的异常处理器“捕获”。
异常不能忽略,所以它保证一定会在某处得到处理。它提供了一种从错误状况进行可靠恢复的路径。异常处理不是面向对象的特征。

12.并发编程

使用线程,但是可能遇到一个隐患,就是共享资源的问题。
某个任务锁定某项资源,完成其任务,然后释放资源锁,使其它任务可以使用这项资源。

13.Java与Internet

客户/服务器系统的核心思想是:系统具有一个中央信息存储池,用来存储某种数据,它通常位于数据库中,你可以根据需要将它分发给某些人员或机器集群。
信息存储池、用于分发信息的软件以及消息与软件所驻留的机器或机群被称为服务器;

第二章 一切都是对象

1.用引用操作对象

字符串可以用带引号的文本初始化

2.必须由你创建所有对象

(1) 寄存器。这是最快的保存区域,因为它位于和其他所有保存方式不同的 地方:处理器内部。然而,寄存器的数量十分有限,所以寄存器是根据需要由编 译器分配。我们对此没有直接的控制权,也不可能在自己的程序里找到寄存器存 在的任何踪迹。
(2) 堆栈。驻留于常规 RAM(随机访问存储器)区域,但可通过它的“堆栈 指针”获得处理的直接支持。堆栈指针若向下移,会创建新的内存;若向上移, 则会释放那些内存。这是一种特别快、特别有效的数据保存方式,仅次于寄存器。 创建程序时,Java 编译器必须准确地知道堆栈内保存的所有数据的“长度”以 及“存在时间”。这是由于它必须生成相应的代码,以便向上和向下移动指针。 这一限制无疑影响了程序的灵活性,所以尽管有些 Java 数据要保存在堆栈里— —特别是对象句柄,但 Java 对象并不放到其中。
(3) 堆。一种常规用途的内存池(也在 RAM 区域),其中保存了 Java 对象。 和堆栈不同,“内存堆”或“堆”(Heap)最吸引人的地方在于编译器不必知道 要从堆里分配多少存储空间,也不必知道存储的数据要在堆里停留多长的时间。 因此,用堆保存数据时会得到更大的灵活性。要求创建一个对象时,只需用 new 命令编制相关的代码即可。执行这些代码时,会在堆里自动进行数据的保存。当 然,为达到这种灵活性,必然会付出一定的代价:在堆里分配存储空间时会花掉 更长的时间!
(4) 静态存储。这儿的“静态”(Static)是指“位于固定位置”(尽管也在 RAM 里)。程序运行期间,静态存储的数据将随时等候调用。可用 static 关键字 指出一个对象的特定元素是静态的。但 Java 对象本身永远都不会置入静态存储 空间。
(5) 常数存储。常数值通常直接置于程序代码内部。这样做是安全的,因为 它们永远都不会改变。有的常数需要严格地保护,所以可考虑将它们置入只读存 储器(ROM)。
(6) 非 RAM 存储。若数据完全独立于一个程序之外,则程序不运行时仍可 存在,并在程序的控制范围之外。其中两个最主要的例子便是“流式对象”和“固 定对象”。对于流式对象,对象会变成字节流,通常会发给另一台机器。而对于 固定对象,对象保存在磁盘中。即使程序中止运行,它们仍可保持自己的状态不 变。对于这些类型的数据存储,一个特别有用的技巧就是它们能存在于其他媒体 中。一旦需要,甚至能将它们恢复成普通的、基于 RAM 的对象。Java 1.1 提供 了对 Lightweight persistence 的支持。未来的版本甚至可能提供更完整的方案

特殊情况:主要类型 有一系列类需特别对待;可将它们想象成“基本”、“主要”或者“主” (Primitive)类型,进行程序设计时要频繁用到它们。之所以要特别对待,是由
于用 new 创建对象(特别是小的、简单的变量)并不是非常有效,因为 new 将 对象置于“堆”里。所以对于这些主要类型,Java 采纳了与 C 和 C++相同的方法。也就 是说,不是用 new 创建变量,而是创建一个并非句柄的“自动”变量。这个变 量容纳了具体的值,并置于堆栈中,能够更高效地存取。 Java 决定了每种主要类型的大小。就象在大多数语言里那样,这些大小并 不随着机器结构的变化而变化。这种大小的不可更改正是 Java 程序具有很强移 植能力的原因之一。

3.特例:基本类型

主类型 大小 最小值 最大值 封装器类型
boolean 1 位 - - Boolean
char 16 位 Unicode 0 Unicode 2的16次方-1 Character
byte 8 位 -128 +127 Byte
short 16 位 -2的15次方 +2的 15 次方-1 Short
int 32 位 -2的31次方 +2的 31 次方-1 Integer
long 64 位 -2的63次方 +2的 63 次方-1 Long
float 32 位 IEEE754 IEEE754 Float
double 64 位 IEEE754 IEEE754 Double
float有一个符号位+8个指数位+23个尾数位 阶码的范围是-126~127 -126-23 即最小值为2的-149次方 最大值(2-2的-23次方)*2的127次方
Float.MIN_VALUE = 1.4e-45f
Float.MAX_VALUE = 3.4028235e+38f
double一样 1+11+52 解码的范围 -1022-1023 -1022-52 即最小值为2的-1074次方 最大值(2-2的-52)*2的1023次方

为什么java中对于float和double定义的最小值都是正数,而不是-Float.MAX_VALUE了?
因为他们不是连续的,它们有精度,无法表示整个实数,最小值也是趋近于零,float无法表示-Float.MIN_VALUE和Float.MIN_VALUE之间的值,如果最小值用-Float.MAX_VALUE是不严谨的

高精度数字
Java 1.1 增加了两个类,用于进行高精度的计算:BigInteger 和 BigDecimal。 尽管它们大致可以划分为“封装器”类型,但两者都没有对应的“主类型”。 这两个类都有自己特殊的“方法”,对应于我们针对主类型执行的操作。也 就是说,能对 int 或 float 做的事情,对 BigInteger 和 BigDecimal 一样可以做。 只是必须使用方法调用,不能使用运算符。此外,由于牵涉更多,所以运算速度 会慢一些。我们牺牲了速度,但换来了精度。
BigInteger 支持任意精度的整数。也就是说,我们可精确表示任意大小的整 数值,同时在运算过程中不会丢失任何信息。
BigDecimal 支持任意精度的定点数字。例如,可用它进行精确的币值计算。
至于调用这两个类时可选用的构建器和方法,请自行参考联机帮助文档

4.Java 的数组

在 C++里,应尽量不要使用数组,换用标准模板库(Standard TemplateLibrary)里更安全的容器。
而一个 Java 可以保证被初始化,而且不可在它的范 围之外访问。由于系统自动进行范围检查,所以必然要付出一些代价:针对每个 数组,以及在运行期间对索引的校验,都会造成少量的内存开销。但由此换回的 是更高的安全性,以及更高的工作效率。为此付出少许代价是值得的。
创建对象数组时,实际创建的是一个句柄数组。而且每个句柄都会自动初始 化成一个特殊值,并带有自己的关键字:null(空)。一旦 Java 看到 null,就知 道该句柄并未指向一个对象。正式使用前,必须为每个句柄都分配一个对象。若 试图使用依然为 null 的一个句柄,就会在运行期报告问题。因此,典型的数组错 误在 Java 里就得到了避免。 也可以创建主类型数组。同样地,编译器能够担保对它的初始化,因为会将 那个数组的内存划分成零。

5.作用域

对于在作用域里 定义的名字,作用域同时决定了它的“可见性”以及“存在时间”。在 C,C++ 和 Java 里,作用域是由花括号的位置决定的

1
2
3
4
5
6
7
8
9
10
 {
int x = 12;
/* only x available */
{
int q = 96;
/* both x & q available */
}
/* only x available */
/* q “out of scope” */
}

注意尽管在 C 和 C++里是合法的,但在 Java 里不能象下面这样书写代码:

1
2
3
4
5
6
7
  {
int x = 12;
{
int x = 96; /* illegal */
}

}

编译器会认为变量 x 已被定义。所以 C 和 C++能将一个变量“隐藏”在一 个更大的作用域里。但这种做法在 Java 里是不允许的,因为 Java 的设计者认 为这样做使程序产生了混淆。
对象的作用域
Java 对象不具备与主类型一样的存在时间。用 new 关键字创建一个 Java 对象的时候,它会超出作用域的范围之外

1
2
3
{
String s = new String("a string");
} /* 作用域的终点 */

那么句柄 s 会在作用域的终点处消失。然而,s 指向的 String 对象依然占据 着内存空间。在上面这段代码里,我们没有办法访问对象,因为指向它的唯一一 个句柄已超出了作用域的边界。在后面的章节里,大家还会继续学习如何在程序 运行期间传递和复制对象句柄。

6.新建数据类型:类

定义一个类时(我们在 Java 里的全部工作就是定义类、制作那些类的对象 以及将消息发给那些对象),可在自己的类里设置两种类型的元素:数据成员(有 时也叫“字段”)以及成员函数(通常叫“方法”)。其中,数据成员是一种对象 (通过它的句柄与其通信),可以为任何类型。它也可以是主类型(并不是句柄) 之一

7.主成员的默认值

若某个主数据类型属于一个类成员,那么即使不明确(显式)进行初始化, 也可以保证它们获得一个默认值。
主类型 默认值
Boolean false
Char ‘\u0000’(null)
byte (byte)0
short (short)0
int 0
long 0L
float 0.0f
double 0.0d
一旦将变量作为类成员使用,就要特别注意由 Java 分配的默认值。这样做 可保证主类型的成员变量肯定得到了初始化(C不具备这一功能),可有效遏 止多种相关的编程错误。 然而,这种保证却并不适用于“局部”变量——那些变量并非一个类的字段。
所以,假若在一个函数定义中写入下述代码:
int x;
那么 x 会得到一些随机值(这与 C 和 C
是一样的),不会自动初始化成零。 我们责任是在正式使用 x 前分配一个适当的值。如果忘记,就会得到一条编译期 错误,告诉我们变量可能尚未初始化。这种处理正是 Java 优于 C的表现之一。 许多 C编译器会对变量未初始化发出警告,但在 Java 里却是错误。

8: 方法、自变量和返回值

迄今为止,我们一直用“函数”(Function)这个词指代一个已命名的子例程。 但在 Java 里,更常用的一个词却是“方法”(Method),代表“完成某事的途径”。 尽管它们表达的实际是同一个意思,但从现在开始,本书将一直使用“方法”, 而不是“函数”。
Java 的“方法”决定了一个对象能够接收的消息。
Java 的方法只能作为类的一部分创建。只能针对某个对象调用一个方法(正如马上就要学到的那样,“静态”方法可针对类调用,毋需一个对象)
int x = a.f();
象这样调用一个方法的行动通常叫作“向对象发送一条消息”。在上面的例 子中,消息是 f(),而对象是 a。面向对象的程序设计通常简单地归纳为“向对象 发送消息”。
正如在 Java 其他地方处理对象时一样,我们实际传递的是“句柄”(注释④)。
④:对于前面提及的“特殊”数据类型 boolean,char,byte,short,int, long,,float 以及 double 来说是一个例外。但在传递对象时,通常都是指传递指 向对象的句柄。

9:使用其他组件

用 import 关键字准确告诉 Java 编译器我们希望的类是什么。import 的作用是 指示编译器导入一个“包”——或者说一个“类库”(在其他语言里,可将“库” 想象成一系列函数、数据以及类的集合。但请记住,Java 的所有代码都必须写 入一个类中)。

1
import java.util.Vector;

它的作用是告诉编译器我们想使用 Java 的 Vector 类。然而,util 包含了数 量众多的类,我们有时希望使用其中的几个,同时不想全部明确地声明它们。为 达到这个目的,可使用“*”通配符。如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import java.util.*;
```
需导入一系列类时,采用的通常是这个办法。应尽量避免一个一个地导入类

#### 10:static 关键字
通常,我们创建类时会指出那个类的对象的外观与行为。除非用 new 创建那 个类的一个对象,否则实际上并未得到任何东西。只有执行了 new 后,才会正 式生成数据存储空间,并可使用相应的方法。
但在两种特殊的情形下,上述方法并不堪用。一种情形是只想用一个存储区 域来保存一个特定的数据——无论要创建多少个对象,甚至根本不创建对象。另 一种情形是我们需要一个特殊的方法,它没有与这个类的任何对象关联。也就是 说,即使没有创建对象,也需要一个能调用的方法。为满足这两方面的要求,可 使用 static(静态)关键字。一旦将什么东西设为 static,数据或方法就不会同 那个类的任何对象实例联系到一起
当然,在正式使用前,由于static 方法不需要创建任何对象,所以它们不可简单地调用其他那些成员,同时不引用 一个已命名的对象,从而直接访问非 static 成员或方法(因为非 static 成员和方 法必须同一个特定的对象关联到一起)。
<font color="#eb4d4b">尽管是“静态”的,但只要应用于一个数据成员,就会明确改变数据的创建 方式(一个类一个成员,以及每个对象一个非静态成员)。若应用于一个方法, 就没有那么戏剧化了。对方法来说,static 一项重要的用途就是帮助我们在不必 创建对象的前提下调用那个方法。正如以后会看到的那样,这一点是至关重要的 ——特别是在定义程序运行入口方法 main()的时候。 和其他任何方法一样,static 方法也能创建自己类型的命名对象。所以经常 把 static 方法作为一个“领头羊”使用,用它生成一系列自己类型的“实例”。</font>

## 第三章 操作符
#### 1.使用Java操作符
几乎所有Java操作符都只能操作“基本类型”,例外的是“=”,“==”和“!=”
String类型支持“+”和“+=”

#### 2.赋值
对象的赋值其实是将“引用”赋值到另一个地方。
如 c = d;
那么c和d都指向原本只有d指向的对象。
下面这个小例子挺好的
t2赋给t1后,并非是互相独立桥归桥路归路。而是绑定在一起共同操作同一个对象。这种特殊的现象称为“别名现象”。

#### 3.算术操作符
整数除法后直接去掉小数位,而非四舍五入的结果。
Random类对象

#### 4.关系操作符
==和!= 比较的是对象的引用。
```java
public class Equivalence {
public static void main(String[] args) {
Integer n1 = new Integer(47);
Integer n2 = new Integer(47);
System.out.println(n1 == n2);
System.out.println(n1 != n2);
}
} /* Output:
false
true

需要比较对象的实际内容使用equals(),此方法不适合基本类型。
然而,对于自定义类,需要比较对象的内容时,还需要覆盖equals()方法

5.逻辑操作符

与&&,或||,非!
在使用逻辑操作符时,会遇到短路现象,即
boolean a =(2>1)&& (3<1) && (5>2) && (9>3);
在计算到(3>1)为false时,后续两个式子就不再计算,结果a = false不会再改变

5.直接常量

直接常量后缀表明了它的类型,大写(或小写)L,表示long,大写(或小写)F,表示float,大写D,表示double
如:
long n3 = 200;
float f4 = 1r-43f;
十六进制 0x(0X) + 0~f
八进制 0+0~7
将变量初始化的超出表示范围,编译器会报错。
通过使用Integer和Long的toBinaryString()方法,可以轻松又随意的以二进制形式表示。
Java的指数计数法中,e表示10的幂次而非自然数2,.71那个e:
float = 1.39e-43f; 
表示1.39*10^(-43)

6.直接常量

与&,或|,非~,异或^
&=,!=,^=。然而并没有=,因为是一元操作符。
对于布尔值,按位操作符具有与逻辑操作符相同效果,但是它们不会中途短路。

7.移位操作符

左移位操作符(<<),低位补0。
“有符号”右移位操作符(>>),高位补0,符号保留。
“无符号”右移位操作符(>>>),无论正负,高位补0。
char,byte,short移位前会被转换为int类型,得到结果也是int类型,只有右端低5位有效。(int类型只有32位)。
因此无符号右移时,它们会被先转成Int型,然后右移操作,然后截断,赋值给原来的类型,在这种情况下可能导致-1的结果。

8.类型转换操作符

可以对变量或者数值进行类型转换(cast)。
如果执行窄化转换,数据可能会丢失,编译器会觉得是不是我们搞错了没注意到,此时需要显式的进行类型转换,强调一下。执行扩展转换,则不必显式的进行类型转换。
float和double转化为整型值时,总会对该数字执行截尾。
对于基本数据类型进行算术运算或按位运算,只要类型比int小,在运算前这些书会自动转换成int,即会自动执行数据类型提升。
表达式中出现最大的数据类型决定了表达式最终的数据类型。

参考资料

https://my.oschina.net/jackieyeah/blog/224265 UML类图中的六大关系:关联、聚合、组合、依赖、继承、实现 JackieYeah
https://www.cnblogs.com/yanquan/p/7248933.html 《THINKING IN JAVA》–第二章一切都是对象 延泉
https://blog.csdn.net/severusyue/article/details/48576589 Java编程思想第四版读书笔记——第三章 操作符 severusyue

Cream Bing wechat
subscribe to my blog by scanning my public wechat account