去年读的电子版写的读书笔记兼目录,放出来温习一下😊

第 1 章 数组及其内存管理

1.1 数组初始化 p11

1.1.1 Java 数组是静态的 p11

  • 数组静态初始化、动态初始化的 sample p11

  • Java 数组是静态的,一旦数组初始化完成,数组元素的内存空间分配即结束,程序只能改变数组元素的值,而无法改变数组的长度。

  • 数组变量也是一种引用类型的变量,指向堆内存的数组对象。

  • JS 的数组可动态改变。

1.1.2 数组一定要初始化吗 p14

  • 只要让数组变量指向有效的数组对象即可。

1.1.3 基本类型数组的初始化 p16

  • 引用变量本质上只是一个指针,只要程序通过引用变量访问属性,或者通过引用变量来调用方法,该引用变量就会由它所引用的对象代替。

1.1.4 引用类型数组的初始化 p18

  • sample p18

1.2 使用数组 p21

1.2.1 数组元素就是变量 p21

1.2.2 没有多维数组 p23

第 2 章 对象及其内存管理

  • Java 内存管理分为两个方面:内存分配和内存回收。

  • 内存分配指创建 Java 对象时 JVM 为该对象在堆内存中所分配的内存空间;

  • 内存回收指当该 Java 对象失去引用变成垃圾时,JVM 的 GC 自动清理该对象,并回收该对象所占用的内存。

2.1 实例变量(非静态变量)和类变量(静态变量) p31

2.1.1 实例变量和类变量的属性 p32

  • 在同一个 JVM 内,每个类只对应一个 Class 对象。因此同一个 JVM 内的一个类的类变量只需一块内存空间;但实例变量有几个就需要几块内存空间。

  • sample p33

2.1.2 实例变量的初始化时机 p35

  • 非静态初始化块总是在构造器执行之前获得执行。

  • JDK 提供的 javap 工具,主要用于帮助开发者深入了解 Java 编译器的机制。 用法见 p38

2.1.3 类变量的初始化时机 p39

  • sample p39

2.2 父类构造器 p41

2.2.1 隐式调用和显式调用 p41

  • Java 对象时的初始化过程 sample p41

2.2.2 访问子类对象的实例变量 p43

  • 构造器只是负责对 Java 对象实例变量执行初始化(也就是赋初始值),在执行构造器代码之前,该对象所占的内存已经被分配出来了。

2.2.3 调用被子类重写的方法 p46

  • sample p46

2.3 父子实例的内存控制 p48

2.3.1 继承成员变量和继承方法的区别 p48

  • 对于一个引用类型的变量而言,当通过该变量访问它所引用的对象的实例变量时,该实例变量的值取决于声明该变量时的类型;当通过该变量来调用它所引用的对象的方法时,该方法行为取决于它所实际引用的对象的类型。

2.3.2 内存中子类实例 p51

  • 当程序创建一个子类对象时,系统不仅会为该类中定义的实例变量分配内存,也会为其父类中定义的所有实例变量分配内存,即使子类定义了与父类同名的实例变量。

2.3.3 父、子类的类变量 p56

2.4 final 修饰符 p57

2.4.1 final 修饰的变量 p57

  • 三个地方对 final 实例变量进行初始化 sample p58

  • 对于一个使用 final 修饰的变量而言,如果定义该 final 变量时就指定初始值,而且这个初始值可以在编译时就确定下来,那么这个 final 变量将不再是一个变量,系统会将其当成“宏变量”处理。

2.4.2 执行“宏替换”的变量 p62

  • 对于 final 实例变量而言,只有在定义该变量指定初始值才会有“宏变量”的效果。

2.4.3 final 方法不能被重写 p66

2.4.4 内部类中的局部变量 p68

  • Java 编译器要求被内部类访问的局部变量必须使用 final 修饰符修饰。

第 3 章 常见 Java 集合的实现细节 p72

3.1 Set 和 Map p73

  • 可以说,Map 集合是 Set 集合的扩展。

3.1.1 Set 和 Map 的关系 p73

  • Map 集合的所有 key 都具有 Set 集合的特征。

3.1.2 HashMap 和 HashSet p78

  • Java 集合实际上是多个引用变量所组成的集合,这些引用变量指向实际的 Java 对象。

  • 通常来说,HashMap 的实际容量总比 initialCapacity 大一些,除非指定的 initialCapacity 参数值恰好是 2 的 n 次方。所以在创建 HashMap 时将 initialCapacity 参数值指定为 2 的 n 次方,这样可以减少系统的计算开销。

  • HashMap 在底层将 key-value 对当成一个整体进行处理,这个整体就是一个 Entry 对象。

  • HashSet 只是封装了一个 HashMap 对象来存储所有的集合元素。所有放入 HashSet 中的集合元素实际上由 HashMap 的 key 来保存,而 HashMap 的 value 则存储了一个 PRESENT,它是一个静态的 Object 对象。

  • 测试掌握 HashMap 和 HashSet 集合的功能 sample p86

3.1.3 TreeMap 和 TreeSet p88

  • TreeSet 底层实际使用的存储容器就是 TreeMap。

  • TreeMap 采用一种“红黑树”的排序二叉树来保存 Map 中的每个 Entry——每个 Entry 都被当成红黑树的一个节点来对待。

  • TreeMap 中所有的 key 总是由小到大排列。

3.2 Map 和 List p94

3.2.1 Map 的 values()方法 p94

  • Map 的 values 方法并未返回一个 List 集合。返回的都是内部类的 Values 对象。

3.2.2 Map 和 List 的关系 p100

  • JavaScript 的对象有点类似于 Java 的 Map 的结构,也是由多个 key-value 对组成的。

  • JS 的 sample p101

3.3 ArrayList 和 LinkedList p101

  • List 集合主要有三个实现类:ArrayList、Vector、LinkedList

  • 程序中如果需要栈这种数据结构,推荐使用 Deque 实现类。从 JDK1.6 开始,Java 提供了一个 Deque 接口,并为该接口提供了一个 ArrayDeque 实现类。在无须线程安全的情况下,程序完全可以使用 ArrayDeque 来代替 Stack 类。

  • Deque 接口代表双端队列这种数据结构,双端队列既是队列也是栈。

3.3.1 Vector 和 ArrayList 的区别 p103

  • Vector 其实就是 ArrayList 的线程安全版本。

  • Vector 基本上已经被 ArrayList 所代替。

3.3.2 ArrayList 和 LinkedList 的实现差异 p106

  • LinkedList 本质上就是一个双向链表,但它不仅实现了 List 接口,还实现了 Deque 接口。

  • ArrayList、LinkedList 的一些源码。p106

3.3.3 ArrayList 和 LinkedList 的性能分析及适用场景 p110

3.4 Iterator 迭代器 p110

3.4.1 Iterator 实现类与迭代器模式 p111

  • 对于 Iterator 迭代器而言,它只是一个接口。Java 要求各种集合都提供一个 Iterator()方法,该方法可以返回一个 Iterator 用于遍历该集合中的元素,至于返回的 Iterator 到底是哪种实现类,程序并不关心,这就是典型的“迭代器模式”。

3.4.2 迭代时删除指定元素 p112

第 4 章 Java 的内存回收 p116

4.1 Java 引用的种类 p117

  • Java 内存管理包括内存分配和内存回收两个方面。

4.1.1 对象在内存中的状态 p117

  • 对象的三种状态:可达状态、可恢复状态、不可达状态。

  • 只有当一个对象处于不可达状态时,系统才会真正回收该对象所占有的资源。

4.1.2 强引用 p120

  • 强引用是造成 Java 内存泄漏的主要原因之一。

4.1.3 软引用 p120

  • 通过 SoftReference 类来实现。对于只有软引用的对象而言,当系统内存空间足够时,它不会被系统回收,程序也可使用该对象;当系统内存空间不足时,系统将会回收它。

4.1.4 弱引用 p123

  • 通过 WeakReference 类来实现。对于只有弱引用的对象而言,当系统垃圾回收机制运行时,不管系统内存是否足够,总会回收该对象所占用的内存。

  • 当程序有大量的 Java 对象需要使用弱引用来引用时,可以考虑使用 WeakHashMap 来保存它们。

4.1.5 虚引用 p127

  • 虚引用的主要作用就是跟踪对象被垃圾回收的状态。不能单独使用,必须和引用队列(ReferenceQueue)联合使用。

4.2 Java 的内存泄漏 p128

4.3 垃圾回收机制 p132

4.3.1 垃圾回收的基本算法 p132

  • 对于一个垃圾回收器的设计算法来说,大致有如下可供选择的设计:串行回收和并行回收;并发执行和应用程序停止;压缩/不压缩和复制。

  • 现行的垃圾回收器用分代的方式来采用不同的回收设计。分代的基本思路是根据对象生存时间的长短,把堆内存分成三代:Young,Old,Permanent。

4.3.2 堆内存的分代回收 p134

4.3.3 与垃圾回收相关的附加选项 p136

* 4.3.4 常见的垃圾回收器 p136

  • 1、串行回收器;2、并行回收器;3、并行压缩回收器;4、并发标识-清理回收器(CMS)

4.4 内存管理小技巧 p140

4.4.1 尽量使用直接量 p141

4.4.2 使用 StringBuilder 和 StringBuffer 进行字符串连接 p141

4.4.3 尽早释放无用对象引用 p141

  • 大部分时候程序无须将局部引用变量显式设为 null。

4.4.4 尽量少用静态变量 p142

4.4.5 避免在经常调用的方法、循环中创建 Java 对象 p142

4.4.6 缓存经常使用的对象 p143

  • 开源的缓存实现有 OSCache、Ehcache,它们大都实现了 FIFO、MRU 等常见的缓存算法。

4.4.7 尽量不要使用 finalize 方法 p143

4.4.8 考虑使用 SoftReference p144

第 5 章 表达式中的陷阱 p145

5.1 关于字符串的陷阱 p146

5.1.1 JVM 对字符串的处理 p146

  • 对于 Java 程序中的字符串直接量,JVM 会使用一个字符串池来保存它们。

  • 当程序中需要使用字符串、基本类型包装类实例时,应该尽量使用字符串直接量、基本类型值的直接量,这样能保证较好的性能。

5.1.2 不可变的字符串 p149

  • 当一个 String 对象创建完成后,该 String 类里包含的字符序列就被固定下来,以后永远都不能改变。

  • System 提供的 identifyHashCode()静态方法用于获取某个对象唯一的 hashCode 值,这个方法的返回值与该类是否重写了 hashCode()方法无关。

5.1.3 字符串比较 p151

5.2 表达式类型的陷阱 p153

5.2.1 表达式类型的自动提升 p153

  • 当一个算术表达式包含多个基本类型的值时,整个算术表达式的数据类型将自动提升。

5.2.2 复合赋值运算符的陷阱 p154

  • 复合赋值运算符包含了一个隐式类型转换,a+=5 等价于 a=(a 的类型)(a+5)

  • 复合赋值运算的使用注意点:p156

5.2.3 Java7 新增的二进制整数 p156

5.3 输入法导致的陷阱 p157

5.4 注释字符必须合法 p158

5.5 转义字符的陷阱 p158

5.5.1 慎用字符的 Unicode 转义形式 p158

5.5.2 中止行注释的转义字符 p159

5.6 泛型可能引起的错误 p160

5.6.1 原始类型变量的赋值 p160

  • 把原始类型的变量赋给带泛型类型的变量时的 sample p160

5.6.2 原始类型带来的擦除 p162

  • 将一个 List类型的对象转型为 List,则该 List 对集合元素的类型检查变成了类型变量的上限(即 Object)。

  • 当把一个带泛型信息的 Java 对象赋给不带泛型信息的变量时,Java 程序会发生擦除,这种擦除不仅会擦除使用该 Java 类时传入的类型实参,而且会擦除所有的泛型信息,也就是擦除所有尖括号里的信息。p163

5.6.3 创建泛型数组的陷阱 p164

5.7 正则表达式的陷阱 p166

5.8 多线程的陷阱 p167

5.8.1 不要调用 run 方法 p167

5.8.2 静态的同步方法 p169

  • 任何线程进入同步方法、同步代码块之前,必须先获取同步方法、同步代码块对应的同步监视器。

5.8.3 静态初始化块启动新线程执行初始化 p171

5.8.4 注意多线程执行环境 p176

  • 将线程不安全的类改成线程安全的形式 p179

第 6 章 流程控制的陷阱 p181

6.1 switch 语句陷阱 p182

6.1.1 default 分支永远会执行吗 p182

6.1.2 break 的重要性 p183

  • 输入 javac -X 命令查看支持的全部扩展选项

6.1.3 Java7 增强的 switch 表达式 p185

6.2 标签引起的陷阱 p186

6.3 if 语句的陷阱 p187

6.3.1 else 隐含的条件 p187

  • 使用 if…else 语句有一条基本规则:总是优先把包含范围小的条件放在前面处理。

6.3.2 小心空语句 p190

6.4 循环体的花括号 p191

6.4.1 什么时候可以省略花括号 p191

6.4.2 省略花括号的危险 p192

  • 大部分时候,如果循环体只包含一条语句,那么就可以省略循环体的花括号;但如果循环体只包含一条局部变量定义语句,那依然不可以省略循环体的花括号。

6.5 for 循环的陷阱 p194

6.5.1 分号惹的祸 p194

6.5.2 小心循环计数器的值 p197

6.5.3 浮点数作循环计数器 p197

6.6 foreach 循环的循环计数器 p199

第 7 章 面向对象的陷阱 p202

7.1 instanceof 运算符的陷阱 p203

  • 前一个操作数为引用类型的变量,后一个操作数通常为一个类或接口。

  • 使 null 调用 instanceof 运算符时返回 false 是非常有用的行为。

7.2 构造器的陷阱 p207

7.2.1 构造器之前的 void p207

7.2.2 构造器创建对象吗 p208

  • 构造器并不会创建 Java 对象,构造器只是负责执行初始化。

  • 以下 2 种方式创建 Java 对象无需使用构造器:使用反序列化的方式恢复 Java 对象;使用 clone 方法复制 Java 对象。

  • 为单例类提供 readResolve()方法,保证反序列化时得到已有的 Java 实例。例 p210

7.2.3 无限递归的构造器 p212

7.3 持有当前类的实例 p214

  • 对于一个 Java 类而言,它的一个实例变量持有当前类的另一个实例是被允许的。

7.4 到底调用哪个重载的方法 p215

7.5 方法重写的陷阱 p218

7.5.1 重写 private 方法 p218

7.5.2 重写其他访问权限的方法 p219

7.6 非静态内部类的陷阱 p220

7.6.1 非静态内部类的构造器 p220

  • 非静态内部类必须寄生在外部类的实例中,没有外部类的对象,就不可能产生非静态内部类的对象。因此,非静态内部类不可能有无参数的构造器——即使系统为非静态内部类提供一个默认的构造器,这个默认的构造器也需要一个外部类形参。

  • 系统在编译阶段总会为非静态内部类的构造器增加一个参数,非静态内部类的构造器的第一个形参总是外部类。

7.6.2 非静态内部类不能拥有静态成员 p222

7.6.3 非静态内部类的子类 p223

  • 非静态内部类在外部类的内部派生子类是安全的。

  • 如果条件允许,推荐多使用静态内部类,而不是非静态内部类。对于静态内部类来说,外部类相当于它的一个包。

7.7 static 关键字 p224

7.7.1 静态方法属于类 p224

7.7.2 静态内部类的限制 p226

  • 静态内部类不能访问外部类的非静态成员。

7.8 native 方法的陷阱 p226

  • 对于 native 方法而言,Java 不会为该方法提供实现体。

  • native 方法通常需要借助 C 语言完成,实现步骤:p227

第 8 章 异常处理的陷阱 p229

8.1 正确关闭资源的方式 p230

  • 实例代码 p232

8.1.2 使用 Java7 增强的 try 语句关闭资源 p233

  • 实例代码 p234

8.2 finally 块的陷阱 p235

8.2.1 finally 的执行规则 p235

  • 实例代码:为系统注册了一个关闭钩子,关闭钩子负责在程序退出时回收系统资源。 p236

8.2.2 finally 块和方法返回值 p236

8.3 catch 块的方法 p238

8.3.1 catch 块的顺序 p238

  • 捕捉父类异常的 catch 块都应该排在捕捉子类异常的 catch 块之后(先捕小异常再捕大异常),否则将出现编译错误。

8.3.2 不要用 catch 代替流程控制 p240

8.3.3 只有 catch 可能抛出的异常 p241

8.3.4 做点实际的修复 p244

8.4 继承得到的异常 p246

8.5 Java7 增强的 throw 语句 p247

第 9 章 线性表 p250

9.1 线性表概述 p251

9.1.1 线性表的定义及逻辑结构 p251

9.1.2 线性表的基本操作 p252

9.2 顺序存储结构 p252

  • 简单的顺序结构线性表的源代码。p254

9.3 链式存储结构 p257

  • 链式存储结构的线性表不会按线性的逻辑顺序来保存数据元素,它需要在每一个数据元素里保存一个引用下一个数据元素的引用。

9.3.1 单链表上的基本运算 p258

  • 单链表指的是每个节点只保留一个引用,该引用指向当前节点的下一个节点,没有引用指向头节点,尾节点的 next 引用为 null。

  • 链表和顺序表性能上的差异:顺序表在随机存取时性能很好;链表在插入、删除时性能很好。

9.3.2 循环链表 p264

  • 循环链表是一种首尾相接的链表。循环链表有一个显著特征:从链表的任一节点出发均可找到表中的其他所有节点。

9.3.3 双向链表 p265

  • 双向链表是为每个节点保留两个引用 prev 和 next。

  • 双向链表添加节点、删除节点的指针维护成本更大;在搜索节点、删除指定索引处的节点具有较好的性能。

9.4 线性表的分析 p271

9.4.1 线性表的实现分析 p271

  • 线性表的顺序和链式两种实现的优势 p271

9.4.2 线性表的功能 p272

第 10 章 栈和队列 p274

10.1 栈 p275

  • 栈是一种特殊的线性表,这种线性表只能在固定一端(通常尾端)进行插入、删除操作。

10.1.1 栈的基本定义 p275

  • 栈就是一种后进先出(LIFO)的线性表。

10.1.2 栈的常用操作 p276

  • 栈的标志性方法:入栈、出栈、访问栈顶元素。

  • 栈同样既可采用顺序结构或链式结构存储栈内元素。

10.1.3 栈的顺序存储结构及实现 p276

  • 顺序栈的代码实现 p277

10.1.4 栈的链式存储结构及实现 p281

  • 链栈的代码实现 p282

10.1.5 Java 集合中的栈 p284

  • java.util.Stack:一个普通的顺序栈,底层基于数组实现,线程安全。

  • java.util.LinkedList:一个双端链表。代表栈的链式实现,线程不安全。

10.2 对列 p284

  • 队列使用固定的一端来插入数据元素,另一端只用于删除元素。

10.2.1 队列的基本定义 p284

  • 队列是一种特殊的线性表,只允许在表的前端进行删除,只允许在表的后端进行插入。先进先出(FIFO)的线性表。

10.2.2 队列的常用操作 p285

  • 队列的标志性方法:加入元素、删除元素、访问队列的前端元素。

  • 队列同样既可采用顺序结构或链式结构存储队列内元素。

10.2.3 队列的顺序存储结构及实现 p285

  • 顺序队列的代码实现 p286

10.2.4 循环队列 p289

  • 循环队列是首尾相连的队列:当 front、rear 变量值达到底层数组的 capacity-1 之后,再前进一位就自动变成 0。

  • 循环队列的代码实现 p290

10.2.5 队列的链式存储结构及实现 p293

  • 链队列的代码实现 p294

  • 链队列不会出现队列“满”的情形,因此程序可以不受任何限制地向链队列中添加元素。

10.2.6 Java 集合中的队列 p296

  • 从 JDK1.5 开始,Java 集合框架中提供了一个 Queue 接口,该接口代表了一个队列,实现该接口的类可以当成队列使用。

  • Queue 接口里定义的 6 个方法 p297

  • Dequeue 接口是一个双端队列。

10.3 双端队列 p297

  • 双端队列(Dequeue)可以在两端同时进行插入、删除操作。

  • Dequeue 既可当成队列使用,也可当成栈使用。

  • JDK 为 Dequeue 提供了 ArrayDequeue(顺序存储结构的双端队列)、LinkedList(链式存储结构的双端队列)两个常见的实现类。

  • LinkedList 既可当成线性表、也可当成栈、还可当成队列,但对大部分程序而言,使用 ArrayList、ArrayDequeue 的性能比 LinkedList 更好。

第 11 章 树和二叉树 p299

11.1 树的概述 p300

  • 树是一种非线性结构。

11.1.1 树的定义和基本术语 p300

  • 与树有关的术语 p301

11.1.2 树的基本操作 p301

  • 实现树的数据结构有 2 种选择:1)父节点表示法:每个子节点都记录它的父节点;2)子节点链表示法:每个非叶子节点通过一个链表来记录它所有的子节点。

11.1.3 父节点表示法 p302

  • 采用父节点表示法的代码实现 p303

  • 父节点表示法的特点是:每个节点都可以快速找到它的父节点,但如果要找某个节点的所有子节点就比较麻烦,程序要遍历整个节点数组。

11.1.4 子节点链表示法 p305

  • 子节点链表示法的代码实现 p306

  • 子节点链表示法的特点是:每个节点都可以快速找到它的所有子节点,但如果要找某个节点的父节点则比较麻烦,程序要遍历整个节点数组。

11.2 二叉树 p309

11.2.1 二叉树的定义和基本概念 p309

  • 二叉树指的是每个节点最多只能有两个子树的有序数。

  • 如果一棵二叉树除最后一层外,其余层的所有节点都是满的,并且最后一层或者是满的,或者仅在右边缺少若干连续的节点,则此二叉树就是完全二叉树。

  • 满二叉树是一种特殊的完全二叉树。当完全二叉树最后一层的所有节点都是满的时,这棵完全二叉树就变成了满二叉树。

11.2.2 二叉树的基本操作 p311

  • 要实现二叉树的数据结构,有三种选择:顺序存储;二叉链表存储;三叉链表存储。

11.2.3 二叉树的顺序存储 p312

  • 当使用数组来存储二叉树的所有节点时可能会产生一定的空间浪费,如果该二叉树是完全二叉树,就不会有任何空间浪费了;但如果该二叉树的所有节点都只有右子节点,那么会产生相当大的空间浪费。

  • 顺序存储的二叉树的代码实现 p313

11.2.4 二叉树的二叉链表存储 p315

  • 思想为每个节点增加 left、right 两个指针,分别引用该节点的左右两个子节点。

  • 二叉链表的代码实现 p316

  • 这种二叉链表的存储方式在遍历树节点时效率不高,指定节点访问其父节点时也比较困难。

11.2.5 二叉树的三叉链表存储 p319

  • 三叉链表是在二叉链表上增加了 parent 指针。

  • 三叉链表的代码实现 p319

11.3 遍历二叉树 p322

  • 遍历顺序结构的二叉树直接遍历底层数组即可;遍历链表存储的二叉树有 2 种:深度优先遍历、广度优先遍历。

  • 深度优先遍历算法分为:前序遍历(DLR)、中序遍历(LDR)、后续遍历(LRD)

11.3.1 先序遍历 p323

11.3.2 中序遍历 p323

11.3.3 后序遍历 p324

11.3.4 广度优先(按层)遍历 p325

  • 广度优先遍历的代码实现 p325

11.4 转换方法 p325

11.4.1 森林、数和二叉树的转换 p326

11.4.2 树的链表存储 p327

11.5 哈夫曼数 p327

11.5.1 哈夫曼树的定义和基本概念 p328

  • 带权路径最小的二叉树被称为哈夫曼数或最优二叉树。

11.5.2 创建哈夫曼树 p328

11.5.3 哈夫曼编码 p331

11.6 排序二叉树 p332

  • 排序二叉树的性质:左子树上所有节点的值均小于它的根节点的值;右子数上所有节点的值均大于它的根节点的值;左右子树也分别为排序二叉树。

  • 排序二叉树的代码实现 p335

11.7 红黑树 p340