第 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
是后期绑定的语言。后期绑定发生在运行时,并且确定对象的类型。正是这种方法调用机制的存在,尽管编译器实际上并不知道确切的类型,依然可以正确调用方法。而在有了多态这个机制后,我们只需要编写与基类沟通的方法,就可以与其子类进行沟通,不必再去判断对象类型,没有冗余代码,实现代码复用。
继承与成员变量#
继承后如果子类定义了与父类同名的成员变量,则如何展现取决于引用。例如;
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
。
构造器内部的多态方法行为#
准则:用尽可能少的操作使对象进入正常状态,若非必要,尽量不要再构造器中调用任何其他方法。
使用多态的例子,假如父类的构造器使用了一个方法,这个方法同时还被子类重写,这种情况下就会很危险:== 调用了此时还没有初始化的子类对象的方法!==
协变返回类型#
子类重写方法的返回值可以是基类方法返回值的子类型。
使用继承的准则#
使用继承表达行为上的差异,使用字段表达状态上的变化。