【推荐】【Java编程思想】【笔记】

2,551 阅读34分钟

工欲善其事必先利其器!

再快不能快基础,再烂不能烂语言!

第一章 对象导论

"我们之所以将自然界分解,组织成各种概念,并按其含义分类,主要是因为我们是整个口语交流社会共同遵守的协定的参与者,这个协定以语言的形式固定下来......除非赞成这个协定中规定的有关语言信息的组织和分类,否则我们根本无法交谈。———Benjamin Lee Whorf( 1897~1941 )"

1.1 抽象过程

  • 面向对象的特性:
    1. 万物皆对象。 将对象视为奇特的变量,它可以存储数据,除此之外,你还可以要求它 在自身上执行操作。理论上讲,你可以抽取待求解问题的任何概念化构建(狗,建筑 物,服务等),将其表示为程序中的对象。
    2. 程序是对象的集合,他们通过发送消息来告知彼此所要做的。 要想请求一个对象,就 必须对该对象发送一条消息。更具体地说,可以把消息想象为对某个特定对象的方法的 调用请求。
    3. 每个对象都有自己的由其他对象所构成的存储。 换句话说,可以通过创建包含现有对 象的包的方式来创建新类型的对象。因此,可以在程序中构建复杂的体系,同时将其复 杂性影藏在对象的简单性背后。
    4. 每个对象都拥有其类型。 按照通用的说法,“每个对象都是某个类(class)的一个实 例(instance)”,这里“类”就是“类型”的同义词。每个类最重要的区别于其他类的特性 就是“可以发送什么样的消息给它”。
    5. 某一特定类型的所有对象都可以接收同样的消息。 这是一句意味深长的表述,你在稍 后便会看到。因为“圆形”类型的对象同样也是“几何形”类型的对象,所以一个“圆形”对象 必定能够接受发送给“几何形”对象的消息。这意味着可以编写与“几何形”交互并自动处理 所有与几何形性质相关的事务的代码。这种可替代性(substitutability)是OOP中最强 有力的概念之一。
  • 每一个对象在内存中都有一个唯一的地址(每一个对象都可以唯一地与其他对象区分开来)。

1.2 每个对象都有一个接口

  • 创建抽象数据类型(类)是面向对象程序设计的基本概念之一。

    抽象数据的运行方式与内置(built-in)类型几乎完全一致:你可以创建某一类型的变量(按 照面向对象的说法,称其为对象或者实例),然后操作这些变量(称其为发送消息或者请 求;发送消息,对象就知道要做什么)。

  • 每一个对象都属于定义了特性和行为的某个特定的类。

  • 因为类描述了具有相同特性(数据元素)和行为(功能)的对象集合,所以一个类实际上就是一个数据类型。

  • 接口确定了对某一特定对象所能发出的请求。

1.3 每个对象都提供服务

  • 高内聚低耦合

    内聚: 是从功能角度来度量模块内的联系,一个好的内聚模块应当恰好做一件事。它描述 的是模块内的功能联系;

    耦合: 是软件结构中各模块之间相互连接的一种度量,耦合强弱取决于模块间接口的复杂 程度、进入或访问一个模块的点以及通过接口的数据。

    高内聚低耦合,是软件工程中的概念,是判断设计好坏的标准,主要是面向对象的设 计,主要是看类的内聚性是否高,耦合度是否低。

    耦合性: 也称块间联系。指软件系统结构中各模块间相互联系紧密程度的一种度量。模 块之间联系越紧密,其耦合性就越强,模块的独立性则越差。模块间耦合高低取决于模 块间接口的复杂性、调用的方式及传递的信息

    内聚性: 又称块内联系。指模块的功能强度的度量,即一个模块内部各个元素彼此结合 的紧密程度的度量。若一个模块内各元素(语名之间、程序段之间)联系的越紧密,则 它的内聚性就越高。

    所谓高内聚是指一个软件模块是由相关性很强的代码组成,只负责一项任务,也就是常 说的单一责任原则。

    耦合: 一个软件结构内不同模块之间互连程度的度量。

    对于低耦合,粗浅的理解是:一个完整的系统,模块与模块之间,尽可能的使其独立存 在。 也就是说,让每个模块,尽可能的独立完成某个特定的子功能。模块与模块之间的 接口,尽量的少而简单。如果某两个模块间的关系比较复杂的话,最好首先考虑进一步 的模块划分。这样有利于修改和组合。

  • 将对象看作是服务提供者还有一个附带的好处:它有助于提高对象的内聚性。高内聚是 软件设计的基本质量要求之一:这意味着一个软件构造(例如一个对象,当然它也有可能 是指一个方法或一个对象库)的各个方面“组合”得很好。人们在设计对象时所面临的一个 问题是,将过多的功能都塞在一个对象中。

  • 在良好的面向对象设计中,每个对象都可以很好地完成一项任务,但是它并不试图做更多的事情。

  • 将对象作为服务提供者看待是一件伟大的简化工具,这不仅在设计过程中非常有用,而 且当其他人试图理解你的代码或重用某个对象时,如果他们看出了这个对象所能提供的 服务的价值,它会使调整对象以适应其设计的过程变得简单得多。

1.4 被影藏的具体实现

  • 将程序员开发人员按照角色分为类创建者(那些创建行数据类型的程序员)和客户端程序 员(那些在其应用中使用数据类型的类消费者)是大有裨益的。客户端程序员的目标是收 集各种用来实现快速应用开发的类。类创建者的目标是构建类,这种类只向客户端程序 员暴露必须的部分,而隐藏其他部分。
  • 访问控制存在的原因
    • 第一个存在的原因就是让客户端程序员无法触及他们不应该触及的部分。
    • 第二个存在的原因就是允许库设计者可以改变类内部的工作方式而不用担心会影响到客户端程序员。
  • JAVA边界值
    • public: 表示紧随其后的元素对任何人都是可用的。
    • private: 关键字表示除类型创建者和类型的内部方法以外的任何人都不能访问的元素。 private就像是你与客户端程序员之间的一堵砖墙,如果有人试图访问private成 就会在编译时得到错误信息。
    • protected: protected关键字与private作用相当,差别仅在于继承的类可以访问 protected成员,但是不能访问private成员。
    • 默认访问权限: 当没有使用前面提到的任何访问指定词时,它将发挥作用。这种权限通常被称为包访问权限,因为在这种权限下,类可以访问在同一个包(库构件)中的其他类的成员,但是在包之外,这些成员如同指定了private一样。

1.5 复用具体实现

  • 代码复用是面向对象程序设计语言锁提供的最了不起的优点之一。

  • 新的类型可以由任意数量,任意类型的其他对象以任意可以实现新的类中想要的功能的方式组成。

  • 因为是在使用现有的类合成新的其他对象的类,所以这种概念被称为组合(composition),如果组合是动态发生的,那么它通常被称为聚合(aggregation)。

  • 在建立新类时,应该首先考虑组合,因为它更加简单灵活。 如果采用这种方式,设计会变得更加清晰,一旦有了一些经验之后,便能够看出必须使用继承的场合了。

1.6 继承

  • 当源类(被称为基类,超类或父类)发生变动时,被修改的“副本”(被称为导出类,继承类或者子类)也会反应出变动。

  • 可以创建一个基类来表示系统中某些对象的核心概念,从基类型中导出其他类型,来表示此核心可以被实现的各种不同方式。

  • 当继承现有类型时,也就创建了新的类型。 这个新的类型不仅包括现在类型的所有成员(尽管private成员被隐藏起来,并且不可访问),而且更重要的是它复制了基类的接口。也就是说,所有可以发送给基类对象的消息同时也可以发送到导出类对象。

  • 通过继承而产生的类型等价性是理解面向对象程序设计方法内涵的重要门槛!

  • 有两种方法可以使基类与导出类产生差异

      1. 直接在导出类中添加新方法。这些新方法并不是基类接口的一部分。这意味着基类并不能满足你的所有需求,因此必需添加更多的 方法。
      1. 使导出类和基类之间产生差异的方法是改变现有基类的方法的行为,这被称之为覆盖(overriding)那个方法。
  • 继承应该只覆盖基类的方法(而不添加在基类中没有的新方法),在某种意义上,这是一种处理继承的理想方式。我们经常将这种情况下的基类与导出类之间的关系称为is-a(是一个)关系。

  • 有时必须在导出类型添加新的接口元素,这样也就扩展了接口。这种情况我们可以描述为is-like-a(像是一个)关系。

1.7 伴随多态的可互换对象

  • 通过导出新的子类型而轻松扩展设计的能力是对改动进行封装的基本方式之一。

    这种能力可以极大地改善我们的设计,同时也降低软件维护的代价。

  • 面向对象程序设计的最重要妙诀:编译器不可能产生传统意义上的函数调用。 一个非面向对象编程的编译器产生的函数调用会引起所谓的前期绑定 ,这个术语你可能以前从未听说过,可能从未想过函数调用的其他方式。这么做意味着编译器将产生对一个具体函数名字的调用,而运行时将这个调用解析到将要被执行的代码的绝对地址。然而在OOP(面向对象编程)中,程序直到运行时才能够确定代码的地址,所以当消息发送到一个泛化对象时,必须采用其他的机制。

  • 当向对象发送消息时,被调用的代码直到运行时才能确定。 编译器确保被调用方法的存在,并对调用参数和返回值执行类型检查(无法提供此类保证的语言被称为是弱类型的),但是并不知道将被执行的确切代码。

  • 在java中,动态绑定是默认行为,不需要添加额外的关键字来实现多态。

  • 把将被导出类看做是它的基类的过程称为向上转型。

    转型这个名称的灵感来自于模型铸造的塑模动作;而向上(up)这个词来源于继承图的典型布局方式;通常基类在顶部,而导出类在其下部散开。因此,转型为一个基类就是在继承图中向上移动,即“向上转型”。

  • 正是因为多态才使得事情总是能够被正确处理。编译器和运行系统会处理相关的细节,你需要马上知道的只是事情会发生,更重要的是怎样通过它来设计。当向一个对象发送消息时,即使涉及向上转型,该对象也知道要执行什么样的正确行为。

1.8 单根继承结构

  • 在OOP中,是否所有的类最终都继承自单一的基类?答案是yes,这个终极基类的名字就是Object。 事实证明,单根继承结构带来了很多好处。
  • 在单根继承结构中的所有对象都具有一个共用的接口,所以它们归根到底都是相同的基本类型。
  • 单根继承结构保证所有对象都具备某些功能。
  • 单根继承结构使垃圾回收器的实现变得容易很多,而垃圾回收器正是Java相对C++的重要改进之一。 由于所有对象都保证具有其类型信息,因此不会因无法确定对象的类型而陷入僵局。这对于系统级操作(如异常处理)显得尤其重要,并且给编程带来了更大的灵活性。

1.9 容器(集合)

  • 容器(也称为集合,不过java类库以不同的含义使用“集合”这个术语,所以本书使用“容器”这个词),在任何需要时都可扩充自己以容纳你置于其中的所有东西。因此不需要知道将来会把多少个对象置于容器中,只需要创建一个容器对象,然后让它处理所有细节。

  • Java容器

    • List(用于存储序列)
    • Map(也被称为关联数组,是用来建立对象之间的关联)
    • Set(每种对象类型只持有一个)
    • 诸多队列,树,堆栈等更多的构件。
  • 使用时还是要对容器有所选择,原因如下:

    • 不同容器提供了不同类型的接口和外部行为。堆栈相比于队列就具备不同的接口和行为,也不同于集合列表的接口和行为。它们之中的某些容器提供的解决方案可能要比其他容器灵活得多。
    • 不同的容器对于某些操作具有不同的效率。最好的例子就是两种List的比较:ArrayList和LinkedList。它们都是具有相同接口和外部行为的简单的序列,但是它们对于某些操作所花费的代价却有天壤之别。在ArrayList中,随机访问元素是一个花费固定时间的操作;但是,对于LinkedList来说,随机选取元素需要在列表中移动,这种代价是高昂的,访问越靠近表尾的元素,花费的时间越长。而另一方面,如果想在序列中间插入一个元素,LinkedList的开销却比ArrayList要小。
  • 我们可以在一开始使用LinkedList构建程序,而在优化系统性能时改用ArrayList。接口List所带来的抽象,把在容器之间进行转换时对代码产生的影响降低到最小限度。

  • 参数化类型(范型) 在JavaSE5之前,容器存储的对象都只具有Java中的通用类型:Object。

    单根继承结构意味着所有东西都是Object类型,所以可以存储Object的容器可以存储任何东西。这使得容器很容易被复用。

  • 向上转型是安全的,例如Circle是一种Shape类型;但是不知道某个Object是Circle还是Shape,所以除非确切知道所要处理的对象的类型,否则向下转型几乎是不安全的。

  • 如果向下转型错误的话,就会得到被称为异常的运行时错误。

  • 参数化类型(范型)就是一个编译器可以自动定制用于特定类型上的类。 例如,通过使用参数化类型,编译器可以定制一个只接纳和取出Shape对象的容器。

  • 为了利用泛型的优点,很多标准类库构建都已经进行了修改。就像我们将要看到的那样,范型对本书中的许多代码都产生了重要的影响。

1.10 对象的创建和生命期

  • 在使用对象时,最关键的问题之一便是它们的生成和销毁方式。

    每个对象为了生存都需要资源,尤其是内存。当我们不再需要一个对象时,它必须被清理掉。使其占有的资源可以被释放和重用。

  • 为了追求最大的执行速度,对象的存储空间和生命周期可以在编写程序时确定。

    • 这可以通过将对象置于堆栈(它们有时被称为自动变量)或限域变量或静态存储区域内来实现。

      这种方式将存储空间分配和释放置于优先考虑的位置,某些情况下这样的控制非常有价值。但是,也牺牲了灵活性,因为必须在编写程序时知道对象确切的数量,生命周期和类型。

    • 第二种方式是在被称为堆(heap)的内存池中动态地创建对象。

      在这种方式中,直到运行时才知道需要多少对象,它们的生命周期如何,以及它们的具体类型是什么。

  • 如果需要一个新对象,可以在需要的时刻直接在堆中创建。因为存储空间是在运行时被动态管理的,所以需要大量的时间在堆中分配存储空间,这可能要远远大于在堆栈中创建存储空间的时间。

  • 创建堆栈存储空间的时间依赖于存储机制的设计

  • 动态方式有这样一个一般性的逻辑假设:对象趋向于变得复杂。所以查找和释放存储空间的开销不会对对象的创建造成重大冲击。动态方式所带来的更大的灵活性正是解决一般化编程问题的要点所在。

  • java完全采用动态内存分配方式。每当想要创建新对象时,就要使用new关键字来构建此对象的动态实例。

  • 还有一个议题,就是生命周期。对于允许在堆栈上创建对象的语言,编译器可以确定对象存活的时间,并可以自动销毁它。

  • java提供了被称为“垃圾回收器”的机制,它可以自动发现对象何时不再被使用,并继而销毁它。

    垃圾回收器非常有用,因为它减少了所必须考虑的议题和必须编写的代码。更重要的是,垃圾回收器提供了更高层的保障,可以避免暗藏的内存泄漏问题。

  • java的垃圾回收器被设计用来处理内存释放问题(尽管它不包括清理对象的其他方面)。垃圾回收器“知道”对象何时不再被使用,并自动释放对象占用的内存。

1.11 异常处理:处理错误

  • 异常是一种对象,它从出错地点被“抛出”,并被专门设计用来处理特定类型错误的相应的异常处理器“捕获”。异常处理就像是与程序正常执行路径并行的、在错误发生时执行的另一条路径。因为它是另一条完全分离的执行路径,所以它不会干扰正常的执行代码。
  • 被抛出的异常不像方法返回的错误值和方法设置的用来表示错误条件的标志位那样可以被忽略,异常不能被忽略,所以它保证一定会在某处得到处理。
  • 异常提供了一种从错误状况进行可靠恢复的途径。现在不再是只能退出程序,你可以经常进行校正,并恢复程序的执行,这些都有助于编写出更健壮的程序。

1.12 并发编程

  • 在计算机编程中有一个基本的概念,就是在同一时刻处理多个任务的思想。
  • 在程序中,彼此独立运行的部分称之为线程。
  • 对于大量的问题,把问题切分成多个可独立运行的部分(任务),从而提高程序的响应能力。称之为“并发”。
  • 线程只是一种为单一处理器分配执行时间的手段。
  • 如果有多个并行任务都要访问同一项资源,那么就会出问题。例如,两个进程不能同时向一台打印机发送信息。为了解决这个问题,可以共享的资源,例如打印机,必须在使用期间被锁定。因此,整个过程是:某个任务锁定某项资源,完成其任务,然后释放资源锁,使其他任务可以使用这项资源。

第2章 一切都是对象

"如果我们说另一种不同的语言,那么我们就会发觉一个有些不同的世界。——Luduing Wittgerstein( 1889~1951 )"

2.1 用引用操纵对象

  • java中,一切都被视为对象,因此可采用单一的语法。尽管一切都看作是对象,但操作的标识符实际上是对象的一个“引用”。

    例:遥控器(引用)操作电视机(对象)。

    • java操作的标识符实际上是对象的一个“引用”
    • java操作的标识符实际上是对象的一个“引用”
    • java操作的标识符实际上是对象的一个“引用”

2.2 必须由你创建所有对象

  • new 关键字的意思是“给我一个新对象”。
  • 存储数据的地方:
    • 寄存器: 最快的存储区,但由于寄存器的数量极其有限,所以根据需求进行分配。你不能直接控制,在程序中也感觉不到寄存器存在的任何迹象。

    • 堆栈: 位于通用RAM(随机访问存储器)中。堆栈指针若往下移则分配新内存,若往上移动则释放内存。

      堆栈:就是STACK。实际上是只有一个出入口的队列,即后进先出(First In Last Out),先分配的内存必定后释放。一般由系统自动分配,存放函数的参数值,局部变量等,自动清除。

      堆栈是每个函数进入的时候分一小块,函数返回的时候就释放了

      局部变量放在堆栈中,所以函数返回,局部变量就全没了。

    • 堆: 一种通用的内存池(位于RAM区),用来存放所有的Java对象。

      当需要一个对象时,只需用new写一行简单的代码,当执行这行代码时,会自动在堆里进行存储分配。

      用堆进行存储分配和清理可能比用堆栈进行存储分配需要更多的时间。

    • 常量存储: 常量值通常是直接存放在程序代码内部。

    • 非RAM存储: 如果数据完全存活于程序之外,那么它可以不受程序的任何控制,在程序没有运行也可以存在。例如:流对象和持久化对象。

  • 基本数据类型
    • 基本数据类型不用new来创建对象,而是创建一个并非是引用的“自动”变量。这个变量直接存储“值”,并置于堆栈中,因此更加高效。
    • 所有数据类型都有正负号,没有无符号的数值类型。
    • 基本类型具有的包装器类,使得可以在堆中创建一个非基本对象,用来表示对应的及本类型。
    • java SE5的自动包装功能将自动地将基本数据类型转换成包装器类型。
    • java提供了两个用于高精度计算的类:BigInteger(支持任意精度的整数)和BigDecimal(支持任何精度的定点数)。

2.3 永远不需要销毁对象

  • 作用域
    • 作用域由花括号的位置决定。
    • 作用域里定义的变量只可用于作用域结束之前。
  • 对象的作用域
    • 当new创建一个java对象时,它可以存活于作用域之外。
    • java有一个垃圾回收器,用来监视用new创建的所有对象,并辨别那些不会再被引用的对象。随后,释放这些对象的内存空间,以便提供其他新的对象使用。

2.4 创建新的数据类型:类

  • class这个关键字之后紧跟的是新类型的名称。
  • 在java中你所做的全部工作就是定义类,产生那些类的对象,以及发送消息给这些对象。
  • 字段可以是任何类型,可以通过其引用与其进行通信,也可以是基本类型的一种。
  • 如果字段是对某个对象的引用,那么必须初始化该引用,以便使其与一个实际的对象相关联。

2.5 方法、参数和返回值

  • java的方法决定了一个对象能够接收什么样的消息,组成(名称、参数、返回值、方法)。
  • 方法名和参数列表唯一地标识出某个方法。
  • 调用方法的行为通常被称为发送消息给对象。
  • 面向对象的程序设计通常简单地归纳为“向对象发送消息”。
  • 通常,尽管传递的是对象,而实际上传递的是对象的引用。

2.6 构建一个java程序

  • java通过域名来保证每一个类都是独一无二的。
  • static关键字
    • 执行new来创建对象时,数据存储空间才被分配,其方法才供外界调用。
    • new创建对象满足不了的两种场景:
        1. 只想为某特定域分配单一存储空间,而不去考虑究竟要创建多少对象,甚至根本就不创建任何对象。
        1. 希望某个方法不与包含它的类的任何对象关联在一起。也就是说没有创建对象,也能够调用这个方法。
      • 【解决】当声明一个事物是static时,就意味着这个域或方法不会与包含它的类的任何对象实例关联在一起。
    • 使用static方法(或域)前不需要创建任何对象。
    • 一个static字段对每个类来说只有一份存储空间,而非static字段则是对每个对象都有一个存储空间

第3章 操作符

3.7 关系操作符

  • new出来的两个Integer,尽管对象内容相同,但对象的引用却是不同的,== 和 != 比较的就是对象的引用。
  • 如果想比较两个对象实际内容是否相同,必须使用所有对象都适用的equals()
  • 基本类型直接使用== 和 !=

第4章 控制执行流程

  • java不允许我们将一个数字作为布尔值使用!
  • return关键字有两方面的用途:
    • 一方面指定一个方法返回什么值
    • 另一方面它会导致当前的方法退出,并返回那个值。
  • break用于强行退出循环,不执行循环中剩余的语句。
  • 而continue则停止执行当前的迭代,然后退回循环起止处,开始下一次迭代。

第5章 初始化与清理

5.1 用构造器确保初始化

  • 构造器采用与类相同的名称,在创建对象时,new XXX()将会为对象分配存储空间,并调用相应的构造器。这就确保了在操作对象之前,它已经初始化了。
  • 每一类都会有一个默认构造器,java文档中称为“无参构造器”。
  • 构造器是一种特殊类型的方法,没有返回值。
  • 在java中,“初始化”和“创建”捆绑在一起,两者不能分离!

5.2 方法重载

  • 重载:方法名相同而形式参数不同。
  • 每个重载的方法都必须有一个独一无二的参数类型列表。
  • 不可以根据返回值来区分重载方法。
  • 重载最常见的应用场景就是构造器重载!

5.3 默认构造器

  • 默认构造器(又名“无参”构造器)是没有形式参数的——它的作用是创建一个“默认对象”。
  • 假如类中没有构造器的话,编译器会自动帮你创建一个默认构造器。但假如你已写了一个构造器(有参构造),则会自动覆盖掉默认构造器(也就是说此时类里面没有默认的构造器(无参构造),如果你还需要无参构造这时你要自己手动添加一个无参构造)。

5.4 this关键字

class Banana { void peel(int i) {/*...*/} }

public class BananaPeel{
    public static void main(String[] args) {
        Banana a = new Banana();
        Banana b = new Banana();
        a.peel(1);
        b.peel(2);
    }
}

【问题】peel()如何知道是被a还是被b调用的呢?

【答】:为了能简便面向对象的语法来编写代码,编译器暗自把“操作对象的引用”作为第一个参数传递给peel(),
       a.peel(a,1);这是内部表现形式。
  • 如果在方法内部的时候需要获得当前对象的引用,可以使用this关键字!
  • this关键字只能在方法的内部调用!
  • 通常写 this 的时候,都是指“这个对象”或者“当前对象”,而且它本身表示对当前对象的引用。
  • 可以用this调用一个构造器,但却不能调用两个,而且必须将构造器置于最起始处,否则会报错。
  • 除构造器以外,编译器禁止在其他任何方法中调用构造器!
  • static 就是没有this的方法
  • static方法的内部不能调用非静态方法
  • static可以在没有创建对象的前提下,仅仅通过类本身来调用static方法。

5.5 清理:终结处理与垃圾回收

  • java中的垃圾回收器负责回收无用对象占据的内存资源。

  • 垃圾回收器只知道释放由new分配的内存。

  • finalize():在垃圾回收器准备好释放回收对象占用的存储空间之前调用的方法,可以自定义做一些清理工作。

    【问题】finalize()和析构函数的区别:

      析构函数(C++中销毁对象必须用这个函数):“析构”函数与构造函数相反,当对象结束其生命周期,
      如对象所在的函数已调用完毕时,系统自动执行析构函数。析构函数往往用来做“清理善后”的工作。
    
  • Java对象并非总是被垃圾回收:

    1. 对象可能不被垃圾回收。
    2. 垃圾回收并不等于“析构”。
    3. 垃圾回收只与内存有关。
  • 在java中,只要程序没有濒临存储用完的那一刻,对象占用的空间就总也得不到释放。

  • 使用垃圾回收器的唯一原因是为了回收程序不再使用的内存。

  • finalize()的用途之——释放空间:

    1. 无论对象是如何创建的,垃圾回收器都会负责释放对象占据的所有内存。

    当代码中使用“本地方法”的情况下,比如调用C的malloc()函数系列来分配存储空间时,需要在finalize中调用free()来释放内存空间。

    注:本地方法是一种在java中调用非java代码,一般用“native”关键字修饰。

  • finalize()的用途之——终结条件验证:

    在对象资源被释放之前验证对象的某个状态,将验证内容放在finalize()的方法中,可以避免一些由于代码书写问题导致的缺陷。

    注:【Effective Java】中“避免使用终结方法和清楚方法”一节中提到:终结方法(finalize)通常是不可预测的,也是很危险的,一般情况下是不必要的。使用终结方法会导致行为不稳定,性能降低,以及可移植性问题,根据经验,应该避免使用终结方法。

  • 垃圾回收器如何工作

    在堆上分配对象代价是十分高昂的,因此java中所有对象的分配方式也是非常高昂的。然而垃圾回收器对于提高对象的创建速度,却具有明显的效果。

    • 在某些java虚拟机中,堆的实现就相当与一个传送带,每分配一个对象,它就往前移动一格,这意味着对象存储的分配速度非常快。
    • java的堆并未完全像传送带那样工作。
    • 当它工作时,一面回收空间,一面使堆中的对象紧凑排列,这样“堆指针”就可以很容易移动到更靠近传送带的开始处,也就避免来页面错误。通过垃圾回收器对对象重写排列,实现了一种高速的,有无限空间可供分配的堆模型。
  • 垃圾回收器的实现:

    垃圾回收器依据的思想是:对任何“活”的对象,一定能最终追溯到其存在在堆栈或静态存储区之中的引用。

    • 引用计数器: 是一种简单但速度很慢的垃圾回收技术。

      每个对象都含有一个引用计数器,当有引用连接至对象时,引用计数加1。当引用离开作用域或设置为null的时候,引用计数器减1。垃圾回收器会在含有全部对象的列表上便利,当发现某个对象的引用计数为0时,就释放其占用的空间。但循环引用会导致“对象应该被回收,计数却不为0”。

      引用计数未被应用于任何一种java虚拟机实现中。

    • 停止,复制:

      先暂停程序的运行(不属于后台回收模式),然后将所有存活的对象从当前堆复制到另一个堆,没有被复制的全部都是垃圾。当把对象从一处搬到另一处时,所有指向它的那些引用都必须修正了。位于堆或静态存储的引用可以直接被修改,但还有其他指向的引用,在遍历的过程才能被找到。

      这种方式,效率会很低。一方面需要在两个分离的堆之间来回倒腾;另一方面是程序稳定后,可能只会产生少量垃圾,甚至没有垃圾,但仍然会将所有的内存自一处复制到另一处。

      垃圾回收动作不是在后台进行的,垃圾回收动作发生时,程序会被暂停。

      “停止-复制”要求在释放旧的对象之前,必须把所有存活的对象从旧堆里复制到新堆。这将导致大量内存复制行为。

    • 标记,清扫

      从堆栈和静态存储区出发,遍历所有的引用,进而找出所有存活的对象。每当找到一个存活对象,就会给对象设一个标记,但这时不回收任何对象。只有全部标记工作完成,才开始清理。没有标记的将被释放,有标记才会进行复制动作。必须在程序暂停的情况下才能进行。

    • “自适应的,分代的,停止-复制,标记-清扫”式垃圾回收器

      • 内存分配以较大的“块”为单位,如果对象大,它会占用独立的块。使用块以后,垃圾回收器在回收的时候就可以往废弃的块里拷贝对象了。
      • 每个块都有相应的 代数(generation count) 来记录它是否还存活。
      • 如果块在某处被引用,其代数会增加,垃圾回收器将对上次回收动作之后新分配的块进行整理(对于处理大量短命的临时对象很有益处)。
      • 垃圾回收器会定期进行完整的清理动作——大型对象仍然不会被复制(只是其代数会增加),内含小型对象的那些快则被复制并整理。
      • java虚拟机会进行监视,如果所有对象都很稳定,垃圾回收器效率降低的话,就切换到 “标记-清扫” 方式;
      • java虚拟机会跟踪“标记-清扫”的效果,要是堆空间出现很多碎片,就会切换到 “停止-复制” 方式; 根据不同的情况切换不同的方式,称为“自适应”技术。

5.7 构造器初始化

  • 类中的变量初始化在构造器之前

  • 无论创建多少个对象,静态数据都只占一份存储区域。

  • static关键不能应用于局部变量,只能作用于域。

  • 如果是一个静态的基本类型域,且没有初始化,那么它就会获得基本类型的标准初值。

    如果它是一个对象引用,那么它的默认初始值就是null。

  • 静态存储初始化时间:

    类中的静态成员都会随着类的加载而加载,比如创建对象时,或者是被引用时!

    类中的静态成员都会随着类的加载而加载,比如创建对象时,或者是被引用时!

    类中的静态成员都会随着类的加载而加载,比如创建对象时,或者是被引用时!

  • 初始化的顺序是先静态对象,而后“非静态”对象。

  • 对象的创建过程

    1. 若没有显示地使用static方法,构造器实际上也是static方法,当对象被创建或者类的静态方法/静态域被首次访问时,java解释器要找到类路径,然后定位.class文件。
    2. 载入.class文件,有关静态初始化的所有动作都会执行。因此,静态初始文件只在Class对象首次加载的时候进行一次。
    3. 当使用new()创建的时候,将在堆上为对象分配足够的空间。
    4. 这块存储空间会被清零,将所有基本数据都设置为默认值,引用被设置为null。
    5. 执行所有出现于字段定义处的初始化动作。
    6. 执行构造器。

5.10 总结

  • 构造器能保证正确的初始化和清理(没有正确的构造器调用,编译器就不允许创建对象),所以有了完全的控制,也很安全。
  • 垃圾回收器会自动为对象释放内存,所以在很多场合下,类似的清理方法在java中就不太需要了。
  • java的垃圾回收器可以极大地简化编程工作,而且在处理内存的时候也更安全。

【推荐篇】- 书籍内容整理笔记 链接地址
【推荐】【Java编程思想】【笔记】 juejin.cn/post/684490…
【推荐】【Java核心技术 卷Ⅰ】【笔记】 juejin.cn/post/684490…

后期持续更新中。。。。。。

若有书写错误的地方,欢迎留言,希望我们可以一起进步,一起加油!😜😜