《On Java》阅读笔记-02

第 6 章 初始化和清理

‘不安全’的编程是导致编程成本高昂的罪魁祸首之一。

而 == 初始化 == 和 == 清理 == 正是导致‘不安全’编程的两个因数。

本章着重说明了 Java 使用构造器初始化对象以及垃圾收集器的一些说明。图灵导读指南说明本章有重点和难点，因此尽量详细阐述自己的心得。

用构造器保证初始化

构造器（构造函数），是 Java 保证对象能够正确初始化的重要手段。构造器是与类名完全相同的没有返回类型的方法。类名完全相同意味着构造器并不适用于方法名采用 lowerCamelCase 的编程规范；没有返回类型并非返回类型为void，而是没有返回类型。与 C++ 对比，Java 的构造器与初始化是一体的。

这里会有一个经典的陷阱：我们在编写代码时，总是使用new关键字，调用构造器创建对象，所以很容易认为构造器的作用是创建对象，然而事实却并非如此。对象的创建是由JVM负责的，JVM负责调用构造器，而构造器的作用仅为为对象的初始化。

方法重载

方法重载的规则：每个重载方法必须有独一无二的参数类型列表，即：

形参数量不同。

形参数量相同，类型不同。

形参数量相同，类型相同，顺序不同。

返回值类型不同，不构成方法重载。

形参标识符不同，不构成方法重载。

修饰符不同，不构成方法重载。

当然，当传入参数的类型不在重载方法的参数类型列表中时，较小的类型会被提升；char类型会被提升为int类型；类型过大则会报错。当方法调用可以匹配多个方法时，一般选择数据类型 “近” 的，自动类型转换 “次数最少” 的。

无参构造器

无参构造器的常规部分还蛮好理解的：没有参数的构造器。

不过需要注意的是：只要你已经定义了一个构造器，无论其是否带参数，编译器都 == 不会 == 帮你自动创建一个无参构造器。

形象比喻：当你没有定义构造器时，编译器会认为：“哦～你一定需要一个构造器，让我来帮你添加一个无参构造器吧”；但当你已经提供有构造器，编译器认为：“你已经有构造器了，你知道自己在做什么；如果你自己没有提供无参构造器的话，你一定有自己的理由，或许是你不想要它，那我也不会帮你添加。”
它真的，我哭死

this关键字

this参数是每个成员方法的隐式传参，它是当前对象的引用，这可以通过向终端输出该引用确认。不要滥用this，这会影响代码的可读性，它应该只出现在必要的地方。

在构造器中，this如果后面带参数列表，表示调用匹配参数列表的构造器。注意，不能同时调用两个，并且构造器调用必须出现在方法的最开始部分，也就是方法体的第一行。

static静态方法没有this，这是其静态的本质：静态方法没法隐式传参this，因为静态方法与对象无关。

清理与垃圾收集

资源清理

我们常常使用new关键字创建对象，JVM也会通过垃圾收集器，也就是 GC，来进行对象的生命周期管理。Java程序员 “可以” 一股脑的创建对象，而不必关心该对象何时需要销毁。

使用 finalize () 方法管理内存空间释放，这个方法的调用时机是由 GC 控制的。可 GC 何时启动，这是不确定的。因此不建议使用 finalize () 方法。实际上，自从JDK 9开始，finalize () 方法已被注解为@Deprecated，这个方法已经被弃用了R.I.P, finalize()

目前推荐的方式是显式定义 “善后” 方法，例如Java中各种流的close()方法。

垃圾收集

垃圾收集器GC，是在空闲时期不定时回收没有引用的对象的内存空间的一种机制。它让我们不必像C++那样不使用对象后必须显式执行析构函数。这减轻了程序员的工作，缓解了内存泄漏的问题。

为何存储的释放会影响存储的分配

C等一众语言在堆上分配空间的前例，可能会让你觉得Java在堆上分配所有内容（基本类型除外）的代价很高昂。但实际上你也可以将Java的分配方式想象成传送带：分配空间后，“堆指针” 向前移动到尚未分配的区域，这就和栈分配的方式很类似了，尽管也会有一些额外开销。但这样不是没有节制的，长期以往，这样会导致操作系统开始执行分页调度—— 通过移入移出磁盘，可以执行比物理内存大得多的程序，高效利用物理内存空间，但也会显著影响性能。而这就到GC出场的时刻了，垃圾收集器会清理所有无用数据，压缩对象，回收空间，将 “堆指针” 重新移到靠近起始位置的地方，从而尽量避免产生缺页错误—— 当需要的页不在内存中时，操作系统会产生中断，从磁盘中调入需要的页再重新执行中断产生处的代码。

引用计数 —— 实际没有采用的方案

我们可能或多或少听过有关垃圾收集器实现原理的介绍：每个对象都持有一个引用计数器，每当对象被引用时，计数器都会增加；而每当引用离开作用域或者被设为 null 时，计数器都会减少。垃圾收集器会检查这些计数器是否为 0，为 0 表示没有引用指向它，开始回收它的空间。引用计数通常用于解释垃圾收集器的工作方式，但并没有哪款JVM实际上采用了这种方案。
而垃圾收集器的回收算法，大体上可以分为以下几种：

停止 - 复制：

标记 - 清除：

分代：

自适应：

所有的方案都遵循一个思路：所有堆上存活的对象，最后都能追溯到栈上或静态存储中的引用。

停止 - 复制方案

这种方案的处理简单粗暴：直接将程序停止，然后将所有存活的对象（也就是可以通过引用访问的对象）从一个堆复制到另外一个堆。没有被复制的对象将视为垃圾被回收掉。
该方案会有两个明显的缺点：

需要两倍的内存来实现复制。当然这个问题也有解决方案：有些JVM实现可以再将堆分成块，复制发生在块之间。

若程序运行稳定，垃圾产生很少。但仍旧会触发 “停止 - 复制”，这是一种资源浪费。

标记 - 清除方案

为了防止 “停止 - 复制方案” 造成的资源浪费，当检测没有垃圾产生时，JVM会切换到 “标记 - 清除” 方案：从栈和静态存储开始，遍历查找所有引用指向的对象，对查找到的对象设置标志，此时并未发生回收动作。当遍历完成后，再执行回收。
该方案在通常情况下效率比 “停止 - 复制” 方案低，但当程序稳定，垃圾没有那么多时，该方案的速度就很快了。

自适应

JVM会根据情况，自动切换适合的方案执行。

唉，确实是绕，可能我的 Java 境界还没到能研究 GC 的地步。这里做个 TO-DO ，后期再来补更详细的细节。

对象创建的过程

当第一次创建对象，或第一次访问类的静态成员或者方法时，Java 解释器会根据路径搜索对应的.class文件。

当.class文件被加载后，按照文件定义顺序开始其所有的静态初始化。静态初始化仅会在首次加载时发生一次。

在堆上创建对应的对象，分配足够的存储空间。

将其对象的所有基本类型设为默认值，引用设置为null。

按照文件定义顺序初始化成员变量。

执行构造器。

可变参数列表

function(Object... args)使用省略号，编译器会自动为你填充，args 是一个长度可变的数组。可变参数列表必须放在参数列表的末尾。

JDK 11局部变量类型推断

在JDK 11中，可以通过关键字var声明变量，编译器会自动进行类型推断。

第 7 章 实现隐藏

本章主要介绍的核心思想为：将变化的事物与保持不变的事物分离。通过访问权限修饰符允许库开发者说明哪些是对客户程序员可用的，哪些是不可用的。个人觉得重点主要是在搞懂访问权限修饰符上。

public：任意类均能访问，实际就是没有限制访问权限。

protected：同包中的其他类都可以访问，不同包下必须是子类才能够访问。

（缺省的）什么关键字都不写，表示同包中的其他类都可以访问。

private：仅对自身类中的其他成员可见。

第 8 章 复用

本章主要介绍了Java语言的两种代码复用的方式：组合与继承。组合是指在新类中创建现有类的对象，我们复用的是代码的功能，而非形式。继承则是直接复制了现有类的形式，并添加了新代码，使其成为不污染现有代码的新类。图灵导读中还详细解读了类加载的时机与顺序，与继承体系中，子类与基类初始化的正确顺序。

重载与重写

方法重载：在同一个类中，或者基类与子类中，多个方法的方法名可以相同，这被叫做方法重载。方法重载的条件为：

形参数量不同时。

形参数量相同，数据类型不同时。

形参数量相同，数据类型相同，顺序不同时。

不过第 3 种最好别实际运用。

方法重写：重写是子类对父类的允许访问的方法的实现过程进行重新编写，返回值和形参都不能改变。即外壳不变，核心重写！重点是方法签名是一样的，不能更改！

慎重考虑继承

尽管继承思想在各类书籍和教材中都受到重视，但这并不意味着我们要尽可能的使用继承。在有相关需求时，最好是优先使用组合，再谨慎考虑使用继承。书中提供了一种清晰分辨是选择组合还是考虑继承的问题：“我需要使用向上转型吗？”。

final

final数据

对于基本数据类型，final关键字使其值恒定不变；对于引用数据类型，final使其引用恒定不变。也就是说，一旦一个被final修饰的引用被指向（初始化）了一个对象，那么这个引用将无法指向另外一个对象。

空白final

原话摘抄：对final执行赋值的操作只能发生在两个地方：

字段定义处使用表达式赋值。

每个构造器中。

理由：这是为了保证final字段在使用前总是被初始化。

private方法就是隐式的final方法

方法中两个修饰符添加在一起没有额外的意义。private方法无法访问，也无法重写。虽然测试下来好像可以重写，但其实两者没有任何关系。private方法已经不是类的接口了，它只是类中隐含的代码。所以两个类中的相同方法名只是重名了，当你用注解@Override时就能检查出这个错误。

初始化及类的加载

类的初始化时机一般为：

而类的初始化顺序为：

第 9 章 多态

本章详细介绍了多态 —— 面向对象思想的又一大基本特征，它是程序员 “将变化的事物与不变的事物分离” 的一项重要技术。回忆大学的 Java学习中，多态常常与封装，继承并称面向对象思想的三大特征。可是就目前的众多反馈，包括《On Java》书中也提到，若非必要，优先考虑组合，谨慎考虑继承。读过本章后，我的总结是：继承的优点就在可以运用多态上。多态是这么的美好，以至于应该尽可能运用多态；但继承体系导致了后续若有稍微不适用于继承体系的新类出现后，后续的重构修改会很头疼。而接口可以有效解决这个头疼的问题。

绑定

使用多态时，常常会有这样的问题：明明我传入的是基类的引用，调用的是基类的方法，为什么把子类引用传入，也能调用子类的方法呢？换句话说，编译器是怎么知道我这个引用的确切类型的呢？实际上编译器是不知道的，但Java是后期绑定的语言。后期绑定发生在运行时，并且确定对象的类型。正是这种方法调用机制的存在，尽管编译器实际上并不知道确切的类型，依然可以正确调用方法。而在有了多态这个机制后，我们只需要编写与基类沟通的方法，就可以与其子类进行沟通，不必再去判断对象类型，没有冗余代码，实现代码复用。

继承与成员变量

继承后如果子类定义了与父类同名的成员变量，则如何展现取决于引用。例如；

Copy
class Person {
String name = "Person";

public String getName() {
return name;
}
}

class Student extends Person {
String name = "Student";

@Override
public String getName() {
return name;
}
}

public class Demo {

public static void main(String[] args) {
Student student = new Student();
Person person = new Student();
System.out.println(student.name);
System.out.println(person.name);
System.out.println(student.getName());
System.out.println(person.getName());
}
}

输出结果：

这说明成员变量没有多态，成员方法才有多态可言。实际上子类此时会有两个 name成员变量：Person.name和Student.name被分配了不同的存储空间。要想访问父类的同名字段，必须显示使用super.name。所以为了避免这种令人困惑的情况，一般情况下都是将成员变量设为private。

构造器内部的多态方法行为

准则：用尽可能少的操作使对象进入正常状态，若非必要，尽量不要再构造器中调用任何其他方法。

使用多态的例子，假如父类的构造器使用了一个方法，这个方法同时还被子类重写，这种情况下就会很危险：== 调用了此时还没有初始化的子类对象的方法！==

协变返回类型

子类重写方法的返回值可以是基类方法返回值的子类型。

使用继承的准则

使用继承表达行为上的差异，使用字段表达状态上的变化。