BlackFlame33

BlackFlame33

若无力驾驭,自由便是负担。个人博客 https://blackflame33.cn/

《On Java》阅读笔记-02

第 6 章 初始化和清理#

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

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

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

第六章

用构造器保证初始化#

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

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

方法重载#

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

  1. 形参数量不同。
  2. 形参数量相同,类型不同。
  3. 形参数量相同,类型相同,顺序不同。
  • 返回值类型不同,不构成方法重载。
  • 形参标识符不同,不构成方法重载。
  • 修饰符不同,不构成方法重载。

当然,当传入参数的类型不在重载方法的参数类型列表中时,较小的类型会被提升;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实际上采用了这种方案。
而垃圾收集器的回收算法,大体上可以分为以下几种:

  1. 停止 - 复制
  2. 标记 - 清除
  3. 分代
  4. 自适应

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

停止 - 复制方案#

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

  1. 需要两倍的内存来实现复制。当然这个问题也有解决方案:有些JVM实现可以再将堆分成块,复制发生在块之间。
  2. 若程序运行稳定,垃圾产生很少。但仍旧会触发 “停止 - 复制”,这是一种资源浪费。

标记 - 清除方案#

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

自适应#

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

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

对象创建的过程#

  1. 当第一次创建对象,或第一次访问类的静态成员或者方法时,Java 解释器会根据路径搜索对应的.class文件。
  2. .class文件被加载后,按照文件定义顺序开始其所有的静态初始化。静态初始化仅会在首次加载时发生一次。
  3. 在堆上创建对应的对象,分配足够的存储空间。
  4. 将其对象的所有基本类型设为默认值,引用设置为null
  5. 按照文件定义顺序初始化成员变量。
  6. 执行构造器。

可变参数列表#

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

JDK 11局部变量类型推断#

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

第 7 章 实现隐藏#

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

第 7 章

  1. public:任意类均能访问,实际就是没有限制访问权限。
  2. protected:同包中的其他类都可以访问,不同包下必须是子类才能够访问。
  3. (缺省的)什么关键字都不写,表示同包中的其他类都可以访问。
  4. private:仅对自身类中的其他成员可见。

第 8 章 复用#

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

第 8 章

重载与重写#

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

    1. 形参数量不同时。

    2. 形参数量相同,数据类型不同时。

    3. 形参数量相同,数据类型相同,顺序不同时。

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

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

慎重考虑继承#

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

final#

final数据#

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

空白final#

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

  1. 字段定义处使用表达式赋值。
  2. 每个构造器中。

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

private方法就是隐式的final方法#

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

初始化及类的加载#

类的初始化时机一般为:

8.9 初始化及类的加载 - 8.9 初始化及类的加载 @02-21.07 1672980574647

而类的初始化顺序为:

8.9 初始化及类的加载 - 8.9 初始化及类的加载 @02-21.07 1672980574648

第 9 章 多态#

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

第 9 章

绑定#

使用多态时,常常会有这样的问题:明明我传入的是基类的引用,调用的是基类的方法,为什么把子类引用传入,也能调用子类的方法呢?换句话说,编译器是怎么知道我这个引用的确切类型的呢?实际上编译器是不知道的,但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.nameStudent.name被分配了不同的存储空间。要想访问父类的同名字段,必须显示使用super.name。所以为了避免这种令人困惑的情况,一般情况下都是将成员变量设为private

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

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

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

协变返回类型#

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

使用继承的准则#

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

加载中...
此文章数据所有权由区块链加密技术和智能合约保障仅归创作者所有。