《疯狂Java程序员的基本修养》读书笔记

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

第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