- 1. 目录
- 2. 第1章 面向对象导论
- 2.1. 面向对象程序设计(Object-oriend Programming,OOP)。
- 2.2. 第1章-1 抽象过程
- 2.3. 第1章-2 每个对象都有一个接口
- 2.4. 第1章-3 每个对象都提供服务
- 2.5. 第1章-4 被隐藏的具体实现
- 2.6. 第1章-5 复用具体实现
- 2.7. 第1章-6 继承
- 2.8. 第1章-7 伴随多态的可互换对象
- 2.9. 第1章-8 单根继承结构
- 2.10. 第1章-9 容器
- 2.11. 第1章-10 对象的创建和生命周期
- 2.12. 第1章-11 异常处理:错误处理
- 2.13. 第1章-12 并发编程
- 2.14. 第1章-13 Java与Internet
- 2.15. 第1章-14 总结
- 3. 第2章 一切都是对象
- 3.1. 第2章-1 用引用操纵对象
- 3.2. 第2章-2 必须由你创建所有对象【底层存储】
- 4. 第3章 操作符
- 4.1. 第3章-1 更简单的打印语句
- 4.2. 第3章-2 使用Java操作符
- 4.3. 第3章-3 优先级
- 4.4. 第3章-4 赋值
- 4.5. 第3章-5 算数操作符
- 4.6. 第3章-6 自动递增和递减
- 4.7. 第3章-7 关系操作符
- 4.8. 第3章-8 逻辑操作符
- 4.9. 第3章-9 直接常量
- 4.10. 第3章-10 按位操作符
- 4.11. 第3章-11 移位操作符
- 4.12. 第3章-12 三元操作符if-else
- 4.13. 第3章-13 字符串操作符+和=
- 4.14. 第3章-14 使用操作符时常犯的错误
- 4.15. 第3章-15 类型转换操作符
- 4.16. 第3章-16 Java没有sizeof
- 4.17. 第3章-17 操作符小结
- 4.18. 第3章-18 总结
- 5. 第4章 控制执行流程
- 6. 第5章 初始化与清理
- 7. 第6章 访问权限控制
- 8. 第7章 复用类
- 9. 第8章 多态
- 10. 第9章 接口
- 10.1. 第9章-1 抽象类和抽象方法
- 10.2. 第9章-2 接口
- 10.3. 第9章-3 完全解耦
- 10.4. 第9章-4 Java中的多重继承
- 10.5. 第9章-5 通过继承来拓展接口
- 10.6. 第9章-6 适配接口
- 10.7. 第9章-7 接口中的域
- 10.8. 第9章-8 嵌套接口
- 10.9. 第9章-9 接口与工厂
- 10.10. 第9章-10 总结
- 16.1. 第15章-1 与C++比较
- 16.2. 第15章-2 简单泛型
- 16.3. 第15章-3 泛型接口
- 16.4. 第15章-4 泛型方法
- 16.5. 第15章-5 匿名内部类
- 16.6. 第15章-6 构建复杂模型
- 16.7. 第15章-7 擦除的神秘之处
- 16.8. 第15章-8 擦除的补偿
- 16.9. 第15章-9 边界
- 16.10. 第15章-10 通配符
- 16.11. 第15章-11 问题
- 16.12. 第15章-12 自限定的类型
- 16.13. 第15章-13 动态类型安全
- 16.14. 第15章-14 异常
- 16.15. 第15章-15 混型
- 16.16. 第15章-16 潜在类型机制
- 16.17. 第15章-17 对缺乏潜在类型机制的补偿
- 16.18. 第15章-18 将函数对象用作策略
- 16.19. 第15章-19 总结:转型真的如此之糟吗?
- 19.1. 第18章-1 File类
- 19.2. 第18章-2 输入(Input)和输出(Output)
- 19.3. 第18章-3 添加属性和有用的接口
- 19.4. 第18章-4 Reader和Writer
- 19.5. 第18章-5 自我独立的类:RandomAccessFile
- 19.6. 第18章-6 IO流的典型使用方式
- 19.7. 第18章-7 文件读写的实用工具
- 19.8. 第18章-8 标准IO
- 19.9. 第18章-9 进程控制
- 19.10. 第18章-10 新IO
- 19.11. 第18章-11 压缩
- 19.12. 第18章-12 对象序列化
- 19.13. 第18章-13 XML
- 19.14. 第18章-14 Preferences
- 19.15. 第18章-15 总结
《Thinking in Java》(4th) 的译本《Java编程思想》(第四版) 的整理笔记。
该手册中还包括 本人添加的 一些知识更新和拓展。
顺便吐槽一下,译本的翻译比较生硬,很多地方使用容易出现歧义的长句
目录
第1章 面向对象导论
面向对象程序设计(Object-oriend Programming,OOP)。
本章将介绍包括开发方法概述在内的 OOP 的基本概念。
基础概念
面向对象程序设计(OOP)是一种具有对象概念的程序编程规范,同时也是一种程序开发的抽象方针。
- 它可以包含数据、属性、代码 与 方法。
- 在面向对象程序设计(OOP)中,计算机程序会被设计成彼此相关的 对象。
- 这种在程序中包含各种独立而又相互调用的对象的思想,与传统编程思想正好相反:传统的程序设计主张把程序看成一系列函数的集合,或者直接对计算机下达的指令。
- OOP 中的每个对象都应该能接受和处理数据,并且能将数据传达给其他对象。
- OOP = 对象 + 类 + 继承 + 多态 + 消息,其中的核心概念是类和对象。
- 其中,对象 指的是 类的实例。
- 对象是程序的基本单元,将程序的数据封装在其中,以提高软件的重用性、灵活性和拓展性。对象里的程序可以访问和修改该对象相关联的数据。
主要特征:封装性、继承性、多态性
封装性:封装是指将计算机程序的数据,以及此数据相关的一切操作语言(即描述对象的属性和行为的代码)组装到一起,一并封装到一个有机实体(也就是“类”)中。
- 封装的最基本单位是对象。
- 封装增强了软件结构的模块性,是软件在结构上实现“高内聚,低耦合”的基础。
- 封装的原则:隐藏对象的属性和实现细节,仅对外提供公共访问方式。
- 封装的好处:
- (1)高内聚:将变化隔离,提高安全性;
- (2)低耦合:便于使用,提高重用性。
继承性:继承是一种多种类之间的联系和区别关系。在面向对象中,继承是指一类对象针对另一类对象的某些特点和能力进行复制或者延续。
- 父类又称为基类、超类;子类又称为派生类。子类可以直接访问父类中的非私有的属性和行为。关键字为
extends
。 - 按照继承源进行划分,继承可以分为单继承和多继承。
- 按照继承中包含的内容进行划分,继承可以分为4类,分别为取代继承、包含继承、受限继承、特化继承。
- 继承的好处:
- 父类又称为基类、超类;子类又称为派生类。子类可以直接访问父类中的非私有的属性和行为。关键字为
多态性:在面向对象技术中——
- 从宏观角度来讲,多态是指当不同的对象同时接收到同一个完全相同的消息时,所表现出来的动作是各不相同的,具有多种形态。
- 从微观角度来讲,多态是指在一个类中,调用同一个函数名,使用不同的参数(注:参数列表,包括参数数量和参数类型),得到不同的执行效果。
- 多态实现的前提条件:
- (1)有继承关系;
- (2)有方法重写;
- (3)有父类引用指向子类对象。
- 多态有三种体现形式:
- (1)类多态;
- (2)抽象类多态;
- (3)接口多态。
- 多态的优点:提高软件的拓展性和可维护性。
- 多态的缺点(?):父类引用不能使用子类特有的功能。
- 多态在类型转换中的体现:
- (1)基本类型:隐式转换(小到大),强制转换(大到小);
- (2)引用类型:向上转型(小到大),向下转型(大到小)。
设计优点
面向对象出现以前,结构化程序设计是程序设计的主流,结构化程序设计又称为面向过程的程序设计。在面向过程程序设计中,问题被看作一系列需要完成的任务,函数(在此泛指例程、函数、过程)用于完成这些任务,解决问题的焦点集中于函数。其中函数是面向过程的,即它关注如何根据规定的条件完成指定的任务。
比较面向对象程序设计和面向过程程序设计,还可以得到面向对象程序设计的其他优点:
- 数据抽象的概念可以在保持外部接口不变的情况下改变内部实现,从而减少甚至避免对外界的干扰;
- 通过继承大幅减少冗余的代码,并可以方便地扩展现有代码,提高编码效率,也减低了出错概率,降低软件维护的难度;
- 结合面向对象分析、面向对象设计,允许将问题域中的对象直接映射到程序中,减少软件开发过程中中间环节的转换过程;
- 通过对对象的辨别、划分可以将软件系统分割为若干相对为独立的部分,在一定程度上更便于控制软件复杂度;
- 以对象为中心的设计可以帮助开发人员从静态(属性)和动态(方法)两个方面把握问题,从而更好地实现系统;
- 通过对象的聚合、联合可以在保证封装与抽象的原则下实现对象在内在结构以及外在功能上的扩充,从而实现对象由低到高的升级。
设计缺陷
运行效率较低。
类的大量加载会牺牲系统性能,降低运行速度。虽然CPU速度在提高,内存容量在增加,但这一问题仍会随着系统规模变大而逐渐显示出来,变得越发严重。
类库庞大。
由于类库都过于庞大,程序员对它们的掌握需要一段时间,从普及、推广的角度来看,类库应在保证其功能完备的基础上进行相应的缩减。
类库可靠性。
越庞大的系统必会存在我们无法预知的问题隐患,程序员无法完全保证类库中的每个类在各种环境中百分之百的正确,当使用的类发生了问题,就会影响后续工作,程序员也有可能推翻原来的全部工作。
名词解释
面向对象程序设计中的概念主要包括:对象、类、数据抽象、继承、动态绑定、数据封装、多态性、消息传递。通过这些概念面向对象的思想得到了具体的体现。
(1)对象(Object):
可以对其做事情的一些东西。对象有3种属性:状态、行为、标识。
(2)类(Class):
一个共享相同结构和行为的对象的集合。类(Class)定义了一件事物的抽象特点。通常来说,类定义了事物的属性和它可以做到的(它的行为)。举例来说,“狗”这个类会包含狗的一切基础特征,例如它的孕育、毛皮颜色和吠叫的能力。类可以为程序提供模版和结构。一个类的方法和属性被称为“成员”。
(3)封装(Encapsulation):
第一层意思:将数据和操作捆绑在一起,创造出一个新的类型的过程。第二层意思:将接口与实现分离的过程。
(4)继承:
类之间的关系,在这种关系中,一个类共享了一个或多个其他类定义的结构和行为。继承描述了类之间的“是一种”关系。子类可以对基类的行为进行扩展、覆盖、重定义。
(5)组合:
既是类之间的关系也是对象之间的关系。在这种关系中一个对象或者类包含了其他的对象和类。
(6)多态:
类型理论中的一个概念,一个名称可以表示很多不同类的对象,这些类和一个共同超类有关。因此,这个名称表示的任何对象可以以不同的方式响应一些共同的操作集合。
(7)动态绑定:
也称动态类型,指的是一个对象或者表达式的类型直到运行时才确定。通常由编译器插入特殊代码来实现。与之对立的是静态类型。
(8)静态绑定:
也称静态类型,指的是一个对象或者表达式的类型在编译时确定。
(9)消息传递:
指的是一个对象调用了另一个对象的方法(或者称为成员函数)。
(10)方法:
也称为成员函数,是指对象上的操作,作为类声明的一部分来定义。方法定义了可以对一个对象执行那些操作。
第1章-1 抽象过程
所有编程语言都提供抽象机制。可以认为,人们所能够解决的问题的复杂性直接取决于抽象的类型和质量。所谓的“类型”指的是“所抽象的是什么”。
汇编语言是对底层机器的轻微抽象。接着出现的许多所谓“命令式”语言(如FORTRAN、BASIC、C等)都是对汇编语言的抽象。……
另一种对机器建模的方式就是只针对待解决问题建模。……
面向对象方式通过向程序员提供表示问题空间中的元素的工具而更近了一步。……
Alan Kay 曾经总结了第一个[成功的面向对象语言、同时也是 Java 所基于的语言之一的 Smalltalk 的]五个基本特性,这些特性表现了一种纯粹的面向对象程序设计方式:
- (1) 万物皆为对象。将对象视为奇特的变量,它可以存储数据,除此之外,你还可以要求它在自身身上执行操作。理论上讲,你可以抽取待求解问题的任何概念化构件(示例:狗、建筑物、服务等),将其表示为程序中的对象。
- (2) 程序是对象的集合,它们通过发送消息来告知彼此所要做的。……
- (3) 每个对象都有自己的由其他对象所构成的存储。……
- (4) 每个对象都拥有其类型。……
- (5) 某一特定类型的所有对象都可以接收同样的消息。……
Booch 对 对象 提供了一个更加简洁的描述:对象具有状态、行为和标识。
这意味着每一个对象都可以拥有内部数据(它们给出了该 对象的状态)和方法(它们产生 对象的行为),并且每一个对象都可以唯一地与其他对象区分开来(标识),具体说来,就是每一个对象在其内存中都有一个唯一的地址。
第1章-2 每个对象都有一个接口
亚里士多德是第一个深入研究类型(type)的哲学家,他曾提出过鱼类和鸟类这样的概念。所有的对象都是唯一的,但同时也是具有相同的特性和行为的对象所属的类的一部分。这种思想被直接应用于第一个面向对象语言 Simula-67,它在程序中使用基本关键字class
来引入新的类型。
Simula,就像其名字一样,是为了开发诸如经典的“银行出纳员问题”(bank teller problem)这样的仿真程序而创建的。……
所以,尽管我们在面向对象程序设计中实际上进行的是创建新的数据类型,但事实上所有的面向对象程序设计语言都使用class
这个关键词来表示数据类型。……
因为类(class)描述了相同特性(数据元素)和行为(功能)的对象集合,所以一个类(class)实际上就是一个数据类型,例如所有的浮点型数字都具有相同的特性和行为集合。……
……
UML(Unified Modelling Language,统一建模语言)形式的图,……
第1章-3 每个对象都提供服务
……
第1章-4 被隐藏的具体实现
将程序开发人员按照角色分为类创建者(创建新数据类型的程序员)和客户端程序员(在应用程序中使用数据类型的类消费者)是大有裨益的。客户端程序员的目标,是收集各种用来实现快速应用开发的类。类创建者的目标是构建类,这种类只向客户端程序员暴露必需的部分,同时隐藏其他部分。构建类的只向客户端程序员暴露必需部分而隐藏其他部分的设计,能保证类的稳定安全,避免人为攻击,减少程序 Bug。
明确边界:在任何相互关系中,具有关系所涉及的各方都遵守的边界是十分重要的事情。……
因此,访问控制的存在原因:
- (1)让客户端程序员无法触及他们不应该接触的部分 —— 这部分对数据类型的内部操作是必需的,但是对于解决问题所需的接口的一部分。
- (2)允许库设计者可以改变 类内部 的工作方式,同时不用担心会影响到客户端程序员。
Java 用3个关键字在类的内部设定边界:public,private,protected。这些访问指定词(access specifier)决定了紧跟其后被定义的东西可以被谁使用。……
Java 还有一种默认的访问权限,当没有使用前面提到的任何访问指定词时,它将发挥作用。这种权限通常被称为 包访问权限,在这种权限下,类可以访问在同一个包中的其他类的成员。
第1章-5 复用具体实现
一旦类被设计创建并被测试完,那么它就应该(在理想情况下)代表一个有用的代码单元。……。代码复用是面向对象程序设计语言提供的最了不起的优点之一。
最简单地复用某个类的方式就是直接使用该类的一个对象,此外也可以将那个类的一个对象置于某个新的类中。我们称其为“创建一个成员对象”。新的类可以由任意数量、任意类型的其他对象,以任意可以实现新的类中想要的功能的方式所组成。……
……
第1章-6 继承
对象这种概念,本身就是十分方便的工具,方便你通过概念将数据和功能封装到一起,因此可以对问题空间的观念给出恰当的表示,而不用受制于 必须使用底层机器语言。这些概念用关键字class
来表示,它们形成了编程语言中的基本单位。
当继承现有类型时,也就创造了新的类型。这个新的类型不仅包括 现有类型的所有成员(尽管private
成员被隐藏了起来,并且不可被访问),而且更重要的是它复制了 基类的接口。也就是说,所有可以发送给基类对象的消息同时也能发送给派生类对象。由于通过发送给类的消息类型可以判断类的类型,所以派生类与基类具有相同的类型。
……
第1章-6-1 “是一个”与“像是一个”的关系
对于继承可能会引发某些争论:继承是否应该只覆盖基类的方法,而并不添加在基类中没有的新方法?
略(原文此处论述太过智障)。
第1章-7 伴随多态的可互换对象
在处理类型的层次结构时,经常想把一个对象不当作的它所属的特定类型来看待,而是将其当作其基类的对象来处理。这使得程序员可以编写出不依赖于特定类型的代码。
泛化(generic),涉及到向上转型。
泛化处理的负面作用是导致编译器无法明确实际执行的代码,而代码的执行只能在编译完成后运行时才能确定。
因为面向对象程序设计语言使用了 后期绑定 的概念——当向对象发送消息时,被调用的代码直到运行时才能确定:编译器确保被调用的方法的存在,并对调用参数和返回值执行类型检查(无法提供此类语言保证的语言被称为 弱类型语言),但是不能确定将被执行的确切代码。
为了执行 后期绑定,Java 使用一小段特殊的代码来替代绝对地址调用。这段代码使用在对象中存储的信息来计算方法体的地址(这个过程将在第八章中体现)。这样,根据这一小段代码的内容,每一个对象都可以具有不同的行为表现。当向一个对象发送消息时,该对象就能够知道通过这条消息应该做什么。
在某些语言中,必须明确地声明某个方法具备后期绑定属性所带来的灵活性(C++是使用virtual关键字来实现的)。在这些语言中,方法在默认情况下不是动态绑定的。而在 Java 中,动态绑定是默认行为,不需要添加额外的关键字来实现多态。
示例:多态的表现 - 示例
第1章-8 单根继承结构
在 OOP(面向对象编程)中,自 C++ 面世以来就令人关注的一个问题:是否所有的类最终都继承自同一个基类。
在 Java 中(事实上还包括 C++ 以外的所有 OOP 语言),答案是 yes,这个终极基类是Object
。
事实证明,单根继承结构 带来了很多好处。
在 单根继承结构 中,所有对象都具有一个共用接口,所以它们归根结底都是相同的基本类型。
单根继承结构 保证所有对象都具备某些功能,可以在每个对象上执行基本操作。所有对象都很容易地在[ 堆(Heap)]上创建,同时 参数的传递 也被极大地简化。
单根继承结构 使得垃圾回收器的实现变得容易很多,而垃圾回收器正是 Java 相对 C++ 的重要改进之一。由于所有对象都保证具有其自身的类型信息,因此不会因为无法确定对象的类型而陷入僵局;这对于系统级操作(如异常处理)显得尤其重要,并且给编程带来了更大的灵活性。
在另一种(C++所提供的)非单根继承结构中,无法确保所有的对象都属于同一个基本类型,从向后兼容的角度来看,这么做能够更好地适应C模型,且受限较少;除此以外不值得……
第1章-9 容器
第1章-9-1 参数化类型
第1章-10 对象的创建和生命周期
第1章-11 异常处理:错误处理
第1章-12 并发编程
第1章-13 Java与Internet
第1章-13-1 Web是什么
第1章-13-2 客户端编程
第1章-13-3 服务器端编程
第1章-14 总结
第2章 一切都是对象
第2章-1 用引用操纵对象
每种编程语言都会有自己的操纵内存中元素的方式。很多时候,程序员必须注意将要处理的数据是什么类型。是选择直接操纵元素,还是用某种基于特殊语法的间接表示()来操纵对象?
所有这一切在 Java 里得到了简化。一切都被视为对象,因此可采用单一固定的语法。尽管一切都看做对象,但操纵的标识符实际上是对象的一个“引用”(
reference
)
第2章-2 必须由你创建所有对象【底层存储】
一旦创建了一个引用,我们通常希望它能够与一个新的对象关联。
通常使用new
操作符来实现这一目的。new
关键字的意思是“分配一个新对象”。
1 String s = new String(“abcdef”);上述代码不仅表示“分配一个新的字符串”,还通过给构造方法提供的初始字符串,确定了如何构建这个
String
对象的信息。这是 Java 程序设计中的一项基本行为。
第2章-2-1 存储到什么地方【对象的存储】
程序运行时,对象是怎么进行存放安排的呢?特别是内存是怎样分配的呢?
对这些方面的了解对程序员会有很大的帮助。
有五个不同的地方可以存储数据:
寄存器。
位于 处理器内部。
这是 最快的存储区。因为它位于不同于其他存储区的地方 —— 处理器内部。
但是 寄存器的数量 极其有限,所以寄存器 根据需求进行分配,且 Java 不允许程序员直接或间接地控制寄存器,甚至屏蔽寄存器的存在概念。
⤴ (另一方面,C 和 C++ 允许程序员向编译器建议寄存器的分配方式)
堆栈(Heap)。
位于通用 RAM(随机访问存储器) 中。
通过 堆栈指针 可以从 处理器 那里获得 直接支持。
- 堆栈指针,若向下移动,则分配新的内存;若向上移动,则释放已分配的内存。
堆栈(Heap)是一个快速存储区域,存取效率仅次于 寄存器。
堆栈(Heap)中存放的数据 必须明确其 数据大小 和 生命周期 ,导致 堆栈(Heap)区 的 存储分配 和 清理释放 操作不灵活。
在 Java 中,堆栈(Heap)用来存放 基本类型数据 和 对象的引用(对象句柄)。
堆(Stack)。
位于 RAM 中;是一种 通用内存池。
- 堆(Stack)中的数据不需要明确 数据大小 和 生命周期,相比于 堆栈(Heap) 具有很好的灵活性。
- 但相应的代价是:堆(Stack)的 存储分配 和 清理释放 操作,相比于 堆栈(Heap),速度慢很多。
- 在 Java 中,堆(Stack)用于存放 所有的 Java 对象。
常量存储(常量池)。
通常位于 程序代码内部,随着 JDK 的迭代而存在不同的设计。(另外在嵌入式系统中,常量会和其他部分隔离,此时可以选择 ROM 作为常量存储区)
常量存储(常量池)位于 堆(Heap) 中。
JDK 7 之前的版本运行时常量池 是 方法区 的一部分。Class文件中除了有 类的版本、字段、方法、接口 等描述信息外,还有 常量池信息(用于存放编译期生成的各种字面量和符号引用)
既然 运行时常量池 是 方法区 的一部分,自然受到 方法区内存 的限制,当 常量池 无法再申请到内存时会抛出
OutOfMemoryError
异常。JDK 7 及之后的版本JDK 7 及之后的版本中,JVM 已经将 运行时常量池 从 方法区 中移了出来,并在 堆(Heap) 中开辟了一块区域存放 运行时常量池。
常量存储(常量池) 用于存储 常量。
⤷ 因为常量是永远不会被改变的,所以 Java 中将 常量池 设置在 程序内部 的设计是安全的。
非RAM存储。
在 非 RAM 存储 中,存储的数据的生命周期不受程序本身的生命周期影响。
⤷ 其中两个基本的例子是 流对象 和 持久化对象。在 流对象 中,对象被转化为字节流(Bit Stream);通常被发送给另一台机器。
在 持久化对象 中,对象被存放于磁盘上,对象的存储形式与其存储媒介有关。
⤷ 在需要时,可以将 持久化对象 从 具体的存储形式 恢复成 常规的、基于 RAM 的对象。
Java 提供了对 轻量级持久化 的支持……Java 提供了 对 轻量级持久化 的支持。诸如 JDBC 和 Hibernate 这样的机制,提供了 更加复杂的、对数据库中的对象信息的 存取支持。
第2章-2-2 特例:基本类型
基本类型对象 直接存储“值”,而不是引用;基本类型对象直接存储于 堆栈(Heap) 中,所以其所占空间大小是确定的。
Java 要确定每种 基本类型 所占空间大小。它们的大小不会随着机器硬件架构的变化而变化,这种 所占存储空间大小的不变性 是 Java 可移植性 好的原因之一。
基本类型 | 中文名称 | 数据大小(单位:位) | 最小值 | 最大值 | 对应包装器类型 |
---|---|---|---|---|---|
boolean | 布尔型 | —— | —— | —— | Boolean |
char | 字符型 | 16 bit | Unicode 0 | Unicode 2^16-1 | Character |
byte | 字节型 | 8 bit | -2^7(-128) | +2^7-1(127) | Byte |
short | 短整型 | 16 bit | -2^15 | +2^15-1 | Character |
int | 整型 | 32 bit | -2^31 | +2^31-1 | Integer |
long | 长整型 | 64 bit | -2^63 | +2^63-1 | Long |
float | 浮点型 | 32 bit | IEEE754 | IEEE754 | Character |
double | 双精度浮点型 | 64 bit | IEEE754 | IEEE754 | Double |
void | 字符型 | —— | —— | —— | Void |
boolean 类型所占空间大小没有明确地指定,仅定义为能够取字面值
true
或false
。
所有的 数值类型 都有 符号,所以不要去寻找无符号的 数值类型。
第2章-2-3 Java中的数组
第2章-3 永远不需要销毁对象
第2章-4 创建新的数据类型
第2章-5 方法、参数和返回值
第2章-6 构建一个Java程序
第2章-7 你的第一个Java程序
第2章-8 注释和嵌入式文档
第2章-9 编码风格
第2章-10 总结
第2章-11 练习
第3章 操作符
在最底层,Java 中的数据是通过使用 操作符 来操作的。
第3章-1 更简单的打印语句
第3章-2 使用Java操作符
操作符 接受一个或多个 参数,并生成一个新值。
参数的形式 与普通的 方法调用 不同,但效果是相同的。
加号和一元的正号
+
、减号和一元的负号-
、乘号*
、除号/
以及赋值号=
的用法与其他编程语言类似。操作符 用于操作 数,生成一个新值。
另外,有些操作符可能会改变 操作数自身的值,这被称为“副作用”。
那些能改变其操作数的操作符,最普遍的用途就是用来产生副作用;但要记住,使用此类操作符生成的值,与使用无副作用的操作符生成的值,没有什么区别。
几乎所有的 操作符 都只能操作“基本类型”。
- 例外的操作符是
=
、==
和!=
:这些操作符能操作所有的对象(这也是 对象 易令人糊涂的地方)。 - 除此之外,
String
类支持+
和+=
:操作String
类的+
和+=
意味着字符串拼接,并且如果必要(被操作的对象不属于String
类),编译器会先尝试将非String
类型的对象转换为String
类型对象,再进行字符串拼接的操作。注意:操作
String
类型对象的+
和+=
操作符,必须要小心使用(…),显式执行(?),杜绝在 循环体 中使用+=
操作String
造成内存的不可控使用(最坏结果:内存溢出)。
- 例外的操作符是
第3章-3 优先级
- 当一个 表达式 中存在多个 操作符 时,操作符的优先级 就决定了各部分的计算顺序。
Java 对 计算顺序 做了特别的规定。
第3章-4 赋值
第3章-4.1 方法调用中的别名问题
第3章-5 算数操作符
第3章-5-1 一元加减操作符
第3章-6 自动递增和递减
第3章-7 关系操作符
关系操作符生成的是一个
boolean
(布尔)类型结果,计算的是 操作数的值之间的关系。如果关系是真实的,则关系表达式会生成true
(真),否则生成false
(假)。关系操作符包括:小于
<
、大于>
、小于或等于<=
、大于或等于>=
、等于==
、不等于!=
。其中等于
==
、不等于!=
适用于所有的 基本数据类型,而其他比较符适用于 除boolean
类型以外的 基本数据类型。因为
boolean
值只能为true
或false
,对于 大于 和 小于 的逻辑来说没有实际意义。
第3章-7-1 测试对象的等价性
关系操作符==
和!=
适用于所有对象。
注意:关系运算符 比较的是 对象的值。
若要比较 对象的引用,可以使用 对象的equals
方法(来自Object
)实现。
第3章-8 逻辑操作符
逻辑操作符:与
&&
、或||
、非!
。逻辑操作符 能根据 参数的逻辑关系,生成一个 布尔值
boolean
(true
或false
)。在 Java 中,逻辑操作符(与
&&
、或||
、非!
)只可应用于 布尔值boolean
。而在 C 和 C++ 中,不可将一个 布尔值 当做 非布尔值 在 逻辑表达式 中使用。
注意:如果在使用
String
值的地方使用布尔值,该布尔值会自动转换为String
形式。
第3章-8-1 短路
当使用逻辑运算符时,我们会遇到一种“短路”现象:一旦能够明确无误地确定 整个表达式的值,就不再计算表达式的余下部分。
示例:
1 | /** |
【应用】我们可以借助“短路”这种设计,节省不必要的代码,使业务逻辑的实现(在编码层面)更精简方便。
第3章-9 直接常量
第3章-9-1 指数记数法
第3章-10 按位操作符
第3章-11 移位操作符
第3章-12 三元操作符if-else
第3章-13 字符串操作符+和=
第3章-14 使用操作符时常犯的错误
第3章-15 类型转换操作符
第3章-15-1 截尾和舍入
第3章-15-2 提升
第3章-16 Java没有sizeof
第3章-17 操作符小结
第3章-18 总结
第4章 控制执行流程
第5章 初始化与清理
随着计算机革命的发展,“不安全”的编程方式已逐渐成为编程代价高昂的主因之一。
初始化 和 清理(cleanup) 正是涉及安全的两个问题。
许多 C 程序的错误都源于程序员忘记 初始化变量。特别是在使用程序库,且用户不知道如何正确地初始化库的构件(或者是必须初始化的其他东西)时,更是如此。
清理 也是一个特殊问题,当使用完一个元素时,它对你也就不会有什么影响了,所以很容易把它忘记,导致这个元素占用的资源一直得不到释放,最终结果是资源(尤其是内存)用尽。
C++ 引入了 构造器(constructor) 的概念,这是一个 在创建对象时 被自动调用的特殊方法。Java 中也采用了 构造器,并额外提供了“垃圾回收器”。对于不再使用的内存资源,垃圾回收器能自动将其释放。
本章就讨论 初始化 和 清理 的相关问题,以及 Java 对它们的支持。
第5章-1 用构造器确保初始化
可以假想为编写的每个类都定义一个
initialize()
方法,该方法的名称提醒你在使用其对象之前,应首先调用initialize()
。然而,这同时意味着用户必须记得自己去调用此方法。
在 Java 中,通过提供构造器,类的设计者可以确保每个对象都会得到初始化。创建对象时,如果其类具有构造器,Java 就会在用户能够操作对象之前自动调用相应的构造器,从而保证了初始化的顺利进行。
接下来的问题就是如何命名这个方法,2个考虑点:
① 构造器按照方法命名规范所取的任何名字,都可能与类的某个成员名称冲突;
② 调用构造器是编译器的责任,所以必须让编译器知道构造器对应哪一个方法。
C++ 中采用了的解决方案看起来是最简单且更符合逻辑的,所以 Java 中也采用了这种方案:构造器采用与类相同的名称(符合类的命名规范,但不符合方法的命名规范)。
注意,由于构造器名称必须与类名完全一致,所以“每个方法首字母小写”的编码风格并不适用于构造器。
以下是一个带有构造器的简单类:
……
现在,在创建对象时:
1 | new Rock() |
将会为对象分配存储空间,并调用相应的构造器。这就保证了在你能操作对象之前,它已经被恰当地初始化了。
默认构造器(无参构造器):不接收形式参数的构造器。
有参构造器。如果
Tree(int)
是Tree
类中唯一的构造器,那么编译器将不会允许你以其他任何方式创建Tree
对象。构造器 有助于减少错误,并使代码更易阅读。从概念上讲,“初始化”与“创建”是彼此独立的,然而在上面的代码中,你却找不到对
initialize()
方法的明确调用。在 Java 中,“初始化”和“创建”是捆绑在一起,不可分离的。构造器 是一种特殊类型的方法,因为它没有返回值。
这与 返回值为空(void) 明显不同。对于空返回值,尽管方法本身不会自动返回什么,但仍可选择让它返回别的东西;构造器 则不会返回任何东西,你别无选择(new 表达式确实返回了对新建对象的引用,但构造器本身并没有返回任何值)。假如构造器具有返回值,并且允许程序员自行选择返回类型,那么势必得让编译器知道该如何处理此返回值。
第5章-2 方法重载
任何程序设计语言都具备一项重要特性,就是对名字的运用。
- 当创建一个对象时,也就是给这个对象分配到的存储空间取了一个名字。
- 所谓方法就是给某个动作取的名字。
通过使用名字,你可以引用所有的对象和方法。
名字起的好可以更易于理解和修改。
第5章-3 默认构造器
默认构造器(又称 无参构造器)是没有形式参数的,它的作用是创建一个默认对象。
如果一个类中没有构造器,则编译器会自动创建一个该类的默认构造器;而如果类中已经定义了构造器(无论是否有参数),则编译器不会再为该类创建默认构造器。
第5章-4 this关键字
方法调用时,往往需要指定 对象(static
方法 还可以直接指定 类class
)。
……
同一个类型的对象,a 和 b,想要让它们都能调用同一个方法peel()
,该如何实现?
在 Java 中,使用了简便且面向对象的语法来编写代码 —— 即“发送消息给对象”,编译器做了一些幕后处理 —— 它将“所操作对象的引用”作为第一个 参数 传递给 方法。
所以,示例中的方法调用代码可以如下解释:
1
2
3
4 a.peel();
a.peel(1);
b.peel();
b.peel(1);↓ 以下是内部的表现形式(注意:不符合编码规范,编译会报错)
1
2
3
4
5
6 //=== (不符合编码规范,编译会报错) ===//
Banana.peel(a);
Banana.peel(a,1);
Banana.peel(b);
Banana.peel(b,1);
//======//
this 关键字为此而生,它表示对“调用方法的那个对象”的引用,且只能在 方法内部 使用(指代发起方法调用时,该方法对应的对象)。
this 的用法与其他对象引用并无不同。
注意,如果在方法内部调用同一个类的另一个方法,不必使用 this,直接调用即可。只有当需要明确指出对当前对象的引用时,才需要使用 this 关键字。例如,当需要返回对当前对象的引用时,就可以这样写:
……this 引用对于将当前对象传递给其他方法也很有用。
……
第5章-4-1 在构造器中调用构造器
调用形式:this(
参数类型列表)
……
在 构造器 中,最多只能调用一次 构造器。(否则编译报错)
在 构造器 中,[ 调用构造器 的代码 ] 必须在 最开始的地方。(否则编译报错)
这个例子中也展示了 this 的另一种用法 —— 避免歧义。
由于参数
s
的名称和数据成员s
的名称相同,同时使用会产生歧义;使用this.s
来代表数据成员就能解决这个问题。
第5章-4-2 static的含义
了解this
关键字以后,就能更好地理解static
(静态)方法的含义。
static
方法 中不能使用this
。使用
static
方法时,由于不存在this
,所以它不是通过“向对象发送消息”来完成的。static
方法 中不能(直接)调用 非静态方法 和 非静态对象(涉及到底层加载顺序);而 非静态方法 中可以调用 静态方法 和 静态对象。static
方法 的 加载 是依附于 类class
而不是 类的对象Object
:可以在没有创建任何对象的前提下,通过 类本身 来调用static
方法,这正是static
方法 的主要用途。static
方法很像 全局方法;Java 中禁止使用全局方法,但你在static
方法中就能访问其他static
方法 和static
域。
第5章-5 清理:终结处理和垃圾回收
在 Java 中,有 垃圾回收器 负责回收无用对象占据的内存资源。但也有特殊情况,由于 垃圾回收器 只能释放那些经由new
分配的内存,所以那些不使用new
获得的特殊内存将不会被垃圾回收器正常处理释放。
为了应对这种情况,Java 允许在类中定义一个名为finalize()
的方法。
……
Java 中的 垃圾回收:
- 对象 可能不被 垃圾回收。
- 垃圾回收 不等于“析构”。
- 垃圾回收 只与 内存 有关。
……
第5章-5-1 finalize()的用途何在
此时,已经明确了不该将finalize()
作为通用的清理方法,那么finalize()
真正的用途是什么呢?
垃圾回收只与内存有关
……
第5章-5-2 你必须实施清理
……
第5章-5-3 终结条件
……
第5章-5-4 垃圾回收器如何工作
……
第5章-6 成员初始化
Java 尽力保证:所有变量在使用前都能得到恰当的 初始化。Java 不允许使用 方法的局部变量。
……
1 | int a; |
方法中定义的变量 没有初值,但是 类的数据成员(即字段)会有 默认值。
……
在 类 中定义一个 对象引用 时,如果不将其 初始化,此引用会获得一个特殊值null
。
类中定义的 基本数据类型,会有 对应类型的默认值。
第5章-6-1 指定初始化
第5章-7 构造器初始化
可以用构造器来进行初始化。
在运行时刻,可以调用方法或者执行某些动作来确定初值,这为编程带来了极大的便利性。
因此假如使用如下代码:
1 | public class Test { |
那么类Test
的数据成员(字段)i
会首先被置为0
(默认值),然后被 构造器 置为7
。
运行结果:
1 | 0 |
对于所有 基本类型 和 对象引用,包括 在定义时 直接指定初值的变量,这种情况都是成立的。
第5章-7-1 初始化顺序
在[ 类的内部 ],变量定义的顺序 决定了 初始化的顺序;并且,变量的初始化(默认值) 会在 任何方法(包括构造器)被调用之前。
……
第5章-7-2 静态数据的初始化
无论创建多少个对象,静态数据 都只占用一份存储区域。
static
关键字不能应用于 局部变量,因此它只能作用于 域。
如果一个 域 是 静态的基本类型域,且没有被 初始化,那么它就会被自动赋予 默认值(基本类型 → 对应类型的默认值,引用类型 →
null
)。如果想在 定义处 进行初始化,采取的方法和 非静态数据 没什么不同。
……
……
由输出可见,静态初始化 只有在必要时刻才会进行。……
初始化的顺序 是先 静态对象(如果它们尚未因排序在前的对象创建而被初始化),而后是 非静态对象。……
总结一下对象创建的过程(书中的总结不够好,详见习题)
首次创建 类型为
Dog
的 对象 时,或者Dog
类的 ,静态方法 / 静态域 首次被访问时,Java 解释器 将会查找类的路径,定位Dog.class
文件。载入
Dog.class
(后面章节会提及,这将 创建一个对象),有关 静态初始化 的所有动作都会执行。- 因此,静态初始化 只在
Class
对象 首次加载时 进行一次。
- 因此,静态初始化 只在
当用
new Dog()
创建对象的时候:- 首先,将 在堆上 为
Dog
对象 分配 足够的存储空间。
🡇 - 这块存储空间会被清零,这就自动地将
Dog
对象中的 所有基本类型数据 都设置成了 默认值。
🡇 - 然后,执行所有出现于 字段定义处 的 初始化动作。
🡇 - 最后,执行 构造器。正如第7章所看到的,这可能会涉及到很多动作,尤其是涉及到 继承 的时候。
- 首先,将 在堆上 为
第5章-7-3 显式的静态初始化
Java 允许将多个 静态初始化动作 组织成一个特殊的“静态子句”(有时也叫做“静态块”)。
语法形式:
1 | public class Test { |
尽管上面的代码看起来像个方法,但它实际上只是一段跟在static
关键字后面的代码;与其他初始化动作一样,这段代码仅会执行一次。
……
第5章-7-4 非静态实例初始化
Java 中也有被称为 实例初始化 的类似语法,用来初始化每一个对象的 非静态变量。
1 | class Mug { |
输出:
1 | Inside main |
⤷你可以看到 实例初始化子句:
1 | { |
⤷它看起来与静态初始化子句一样,只不过少了static
关键字。这种语法对于支持“匿名内部类”(参见第10章)是必须的,但是它也使得无论你调用哪个显式构造器,某些操作都会发生。
从输出中可以看到,实例初始化子句 是在两个 构造器 之前执行的。
第5章-8 数组初始化
数组→概念
数组 是 用一个 标识符名称 封装到一起的、相同类型的 [ 一个 对象序列 或 基本类型数据序列 ]。
数组→定义和使用
数组 是 通过方括号下标操作符[]
来 定义和使用的。
要定义一个数组,只需在 类型名 后面加上一对方括号[]
即可(或许更合理):⤵
1 | int[] a1; |
也可以将方括号[]
置于标识符后面(符合C和C++程序员的习惯):⤵
1 | int a1[]; |
数组→特点
编译器不允许指定 数组的大小。这是因为数组是一组 对象的引用(Java 中 对象引用本身 是不变的,但是 所引用的对象 是可变的),所以 声明时 无法指定数组的成员数量,但是 创建时 不受限制。
所有数组都有一个 固有成员
length
……
数组初始化的3种方式
(只能在 创建数组的地方 使用)花括号
{}
+ 数组成员。
此时存储空间的分配(等价于使用new
)将由编译器负责:1
int[] a1 = { 1, 2, 3, 4, 5 };
new
+ 类型说明 + 成员数量说明:1
2// i 为【不小于0的 int 类型参数】
int[] a1 = new int[i];演示:
👆1
2
3
4
5
6public static void main(String[] args) {
// int[] a1 = new int[-1]; // Error -- java.lang.NegativeArraySizeException
int[] a1 = new int[0];
System.out.prinln(a1);
}输出⤵
1
2
3[I@7adf9f5f
Process finished with exit code 0示例:
👆……
初始化进程说明 → 此时,即便使用
new
创建数组之后:⤵1
Integer[] a = new Integer[rand.nextInt(20)]
⤷ 它还只是一个 引用数组,直到通过创建新的
Integer
对象(本例中通过 自动包装机制 创建),并把 对象 赋值给 引用,初始化进程 才算结束。⤵1
a[i] = rand.nextInt(500);
⤷ 如果忘记了创建对象,并且试图使用数组中的空引用,就会出现运行时异常。
new
+ 类型说明 + 花括号{}
+ 数组成员:⤵演示:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19public static void main() {
Integer[] a;
a = new Integer[]{
new Integer(1),
new Integer(2),
// Autoboxing
3,
};
Integer[] b = new Integer[]{
new Integer(1),
new Integer(2),
// Autoboxing
3,
};
System.out.println(Arrays.toString(a));
System.out.println(Arrays.toString(b));
}输出:⤵
1
2
3
4[1, 2, 3]
[1, 2, 3]
Process finished with exit code 0
第5章-8-1 可变参数列表
“
new
+ 类型说明 + 花括号{}
+ 数组成员”形式的 数组初始化方法,可以用于Object
数组实现与 C 的 可变参数列表 一样的效果。⤵⤷ 示例:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19class A {}
public class Test {
static void printArray(Object[] args) {
for (Object obj : args) {
System.out.println(obj + " ");
}
}
public static void main(String[] args) {
printArray(new Object[]{
new Integer(47), new Float(3.14), new Double(11.11),
});
printArray(new Object[]{"one", "two", "three"};
printArray(new Object[]{new A(), new A(), new A()};
}
}⤷ 输出:⤵
1
2
3
4
5
6
7
8
9
10
1147
3.14
11.11
one
two
three
com.suite.A@7adf9f5f
com.suite.A@85ede7b
com.suite.A@5674cd4d
Process finished with exit code 0在 Java SE 5 之前,通常使用上例来实现;而从 Java SE 5 开始,提供了对 可变参数 的语法支持。有了可变参数,就再也不用显式地编写数组语法了,当你指定参数时,编译器实际上将会自动填充数组。⤵
⤷ 示例:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21class A {}
public class Test {
static void printArray(Object... args) {
for (Object obj : args) {
System.out.println(obj + " ");
}
}
public static void main(String[] args) {
printArray(new Integer(47), new Float(3.14), new Double(11.11));
printArray(47, 3.14F, 11.11);
printArray("one", "two", "three");
printArray(new A(), new A(), new A());
printArray((Object[]) new Integer[]{1, 2, 3, 4});
// 【Tips】Empty list is ok
printArray();
}
}输出:⤵
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
1847
3.14
11.11
47
3.14
11.11
one
two
three
com.suite.A@7adf9f5f
com.suite.A@85ede7b
com.suite.A@5674cd4d
1
2
3
4
Process finished with exit code 0在 可变参数列表 中,可以使用 任何类型的参数,包括 基本类型(涉及到 自动包装机制)。
如果 可变参数列表 中 没有任何参数(包括为空),则 参数传递时 转变为 数据尺寸为0的 指定类型数组。同样,可变参数列表 也能直接接受 对应类型的数组(此时编译期间将不会再做不必要的数据转换)。
⤷ 示例:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21public class Test {
static void f(Character... args) {
System.out.println(args.getClass());
System.out.println(" length " + args.length);
}
static void g(int... args) {
System.out.println(args.getClass());
System.out.println(" length " + args.length);
}
public static void main(String[] args) {
f('a');
f();
g(1);
g();
System.out.println("int[]:" + new int[0].getClass());
}
}输出:⤵
1
2
3
4
5
6
7
8
9
10
11class [Ljava.lang.Character;
length 1
class [Ljava.lang.Character;
length 0
class [I
length 1
class [I
length 0
int[]:class [I
Process finished with exit code 0可变参数列表 不依赖于 自动包装机制 🡄 实际上使用的是 基本类型
⤷ 见示例最后一行→创建并打印出来的int
数组(其中的I
表示 基本数据类型)。然而,可变参数列表 与 自动包装机制 可以和谐共处。
⤷ 示例:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16public class Test {
public static void f(Integer... args) {
for (Integer i : args) {
System.out.println(i + " ");
}
System.out.println();
}
public static void main(String[] args) {
f(new Integer(1), new Integer(2));
f(4, 5, 6, 7, 8, 9);
f(10, new Integer(11), 12);
}
}输出:⤵
1
2
3
4
51 2
4 5 6 7 8 9
10 11 12
Process finished with exit code 0注意:你可以在 单一类型的参数列表 中 将类型混合在一起,而 自动包装机制 将有选择地将
int
参数提升为Integer
。可变参数列表 使得 重载过程 变得复杂了,尽管乍一看似乎足够安全。
⤷ 示例:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32public class Test {
static void f(Character... args) {
System.out.println("first");
for (Character c : args) {
System.out.println(" " + c);
}
System.out.println();
}
static void f(Integer... args) {
System.out.println("second");
for (Integer c : args) {
System.out.println(" " + c);
}
System.out.println();
}
static void f(Long... args) {
System.out.println("third");
}
public static void main(String[] args) {
f('a', 'b', 'c');
f(1);
f(2, 1);
f(0);
f(0L);
//! f(); // Won't compile -- ambiguous
}
}输出:⤵
1
2
3
4
5
6first a b c
second 1
second 2 1
second 0
third
Process finished with exit code 0⤷ 在每一种情况中,编译器都会使用 自动包装机制 来匹配 重载方法。
⤷ 但是在上例中,不传递参数,调用f()
时,编译器就无法确定该调用哪个方法(编译不通过)。可以考虑添加 非可变参数 来解决 重载方法之间 可变参数列表混淆 的问题。
【设计原则】
- 如果是 有必要在 重载方法 中使用 可变参数列表,请保证 至多在重载方法的一个版本中 使用 可变参数列表,或者不使用它。
- 某些情况下,可以考虑使用 恰当类型的数组 来替代 可变参数列表。
第5章-9 枚举类型
在 Java SE 5 中添加了一个看似很小的新特性 —— enum
关键字,它使得我们需要群组并使用 枚举类型集 时,可以很方便地处理。
C / C++ 都有 枚举类型,现在 Java 也拥有 枚举类型 且比它们的要完备得多。
额外参考资料
第5章-9-1 枚举类(enum)——基本概念
枚举类:一种 特殊的类,其 实例对象 是 有限且固定的。
枚举类(enum)的特点枚举类 和普通的类(
class
)一样,有自己的 成员变量、成员方法、构造器(因为 设计时 限定使用private
访问修饰符,故无法 从外部 调用 枚举类的构造器,只能 在构造枚举值时 调用 对应枚举类的构造器)。与普通类(
class
)一样,一个 Java 源文件中最多只能有一个public
类型的 枚举类,且 该 Java 源文件的名称 必须与 该枚举类的名称 相同。枚举类(
enum
)默认继承了java.lang.Enum
类,并实现了java.lang.Seriablizable
和java.lang.Comparable
两个接口。⤷ 枚举类 是非抽象的,且不能再派生出子类。
尽管 枚举类 不能被继承,但是可以有 抽象方法 🡆 枚举类的抽象方法
必须必需被其每一个 枚举值 实现。所有的 枚举值 都是 限定
public static final
修饰的。
在 Java 中,使用
enum
关键字来定义 枚举类;其地位与class
和interface
相同。枚举类的 所有实例,必须在 枚举类的第一行 显式地列出;否则这个 枚举类 将 不能生成实例。
⤷ 同时,编译器会自动地为 枚举值 添加public static final
修饰,无需程序员显式添加。
第5章-9-2 枚举类的使用
……
- 参看项目 [ SuiteLHY/DingDing - githun.com ] 代码
第5章-10 总结
- 父类的 静态成员变量 和 静态代码块(按 声明先后顺序 执行);
🡇 - 子类的 静态成员变量 和 静态代码块(按 声明先后顺序 执行);
🡇 - 父类的 非静态成员变量 和 非静态代码块(按 声明先后顺序 执行);
🡇 - 父类的 构造方法;
🡇
- 父类的 非静态成员变量 和 非静态代码块(按 声明先后顺序 执行);
- 子类的 非静态成员变量 和 非静态代码块(按 声明先后顺序 执行);
🡇 - 子类的 构造方法。
- 子类的 非静态成员变量 和 非静态代码块(按 声明先后顺序 执行);
第6章 访问权限控制
访问控制(或隐藏具体实现)与“最初的实现并不恰当”有关。
……
为了解决这一问题,Java 提供了 访问权限修饰词,以供类库开发人员向客户端程序员指明哪些是可用的,哪些是不可用的。
访问权限的控制等级,从最大权限到最小权限依次为:public 🡆 protected 🡆 [ 包访问权限 ](没有关键词) 🡆 private。
……
对于这一点,Java 用关键字package
加以控制;而 访问权限修饰词 会因为 类class
是否在同一个包下 而受到影响。
……
第6章-1 包:库单元
包package
内包含有一组 [ 类class
/ 接口interface
/ 枚举enum
],它们 在单一的名字空间下 被组织在了一起。
……
第6章-1-1 代码组织
当编译一个
.java
文件时,在.java
文件中的每个类都会有一个输出文件;该输出文件的名称与.java
文件中的每个类的名称相同,且后缀名为.class
。因此,在编译少量的.java
文件之后,会得到大量的.class
文件。如果用编译型语言编写过程序,就会清楚:在传统的编译型语言中,编译器产生一个中间文件(通常是一个
obj
文件),然后再与通过链接器(用于创建一个可执行文件)或类库产生器(librarian
,用以创建一个类库)产生的其他同类文件捆绑在一起。然而 Java 的可运行程序,是一组可以打包并压缩为一个 Java 文档文件(JAR,使用 Java 的 jar 文档生成器)的
.class
文件。
⤷ Java 解释器 负责这些文件的 查找、装载 和 解释。
类库 实际上是一组类文件.class
。其中每个文件都有一个public
类,以及 任意数量的 非public
类;因此每个文件都有一个构件。如果希望这些构件(每一个都有它们自己独立的.java
和.class
文件)从属于同一个群组,可以使用关键字package
。
如果使用
package
语句,它必须是 文件中 除注释以外的 第一行程序代码。
在 文件起始处 写:⤵
1 package access/* 包的名称 */;⤷ (请注意,Java 包的命名规则是 全部使用小写字母,包括中间的字母也是如此)
注意:关键字
import
与通配符*
搭配,不能定位到命名空间下的包(不能保证没有冲突)。
第6章-1-2 创建独一无二的包名
既然一个包 从未真正地将 被打包的东西 包装成 单一的文件,并且一个包可以由许多
.class
文件构成;那么仅仅是这样,就可能出现名称完全相同的.class
文件混淆在一起。
⤷ 为了避免这种情况,Java 将每个包的所有.class
文件都置于独一无二的目录下。
……
冲突
……
注意:import
关键字 和 通配符*
,不能定位指定命名空间下的包(package
),因为没有分配资源去进行 唯一性的校验,无法避免 包名冲突。
第6章-1-3 定制工具库
……
第6章-1-4 用import改变行为
……
第6章-1-5 对使用包的忠告
务必记住,无论何时创建 包(package
),都已经在给定 包的名称 的时候隐式地指定了目录结构。
第6章-2 Java访问权限修饰词
……
第6章-3 接口和实现
访问权限的控制 常被称为是 具体实现的隐藏。
封装:把 数据 和 方法 包装进 类(class
) 中,隐藏 类(class
)的具体实现。其结果是 生成一个 带有特征和行为的 数据类型。
使用封装的原因(好处)
在结构中建立内部机制,将公开的接口和具体实现分离。
⤴ 隐藏实现细节,提供公共的访问方式。⤵
提高代码的可维护性。
提高代码的可重用性。
提高数据的安全性。
第6章-4 类的访问权限
在 Java 中,访问权限修饰词 也可以用于 确定库中的哪些类对于该库的使用者是可用的。
如果希望某个类可以为客户端程序员所用,就可以通过把关键字
public
作用于整个类的定义来实现。
这样做甚至可以控制客户端程序员是否能创建一个该类的对象。
……
- 访问权限修饰词 还有一些额外的限制:
每个编译单元(文件)都只能有一个
public
类。
⤷ 这表示,每个编译单元 都有一个 唯一的公共接口,通过public
实现。public
类的名称必须完全与含有该编译单元的文件名一致(包括大小写)。虽然不是很常用,但编译单元内完全不带
public
类也是可以的。
⤷ 此时文件的命名不受限制。
第6章-5 总结
……
第7章 复用类
复用代码 是 Java 众多引人注目的功能之一。但要想成为极具革命性的语言,仅仅能够复制代码并对之加以改变是不够的,它必须能够做更多的事情。
在 Java 中,所有问题的解决都是围绕着 类(class
) 展开的。可以通过创建新类来复用代码。此方法的窍门在于使用 类 而不破坏现有程序代码。
第1种实现方法非常直观:只需在新的类中创建现有类的对象。该方法只是复用了现有程序代码的功能,而非它的形式。由于新的类是由现有类的对象所组成,所以这种方法称为 组合。
第2种方法则更细致一些,它按照现有类的类型来创建新类,无需改变现有类的形式,采用现有类的形式,并在其基础上添加新的代码,这种方法被称为 继承。编译器完成其中绝大部分的工作。继承 是面向对象的基础之一。
第7章-1 组合语法
使用组合技术,只需要将对象引用置于新的类中即可。对于 基本类型数据,可以直接定义;而对于 非基本类型的对象,必须将其 引用 置于新的类中。
编译器并不会为每一个引用都创建默认对象,为的是避免不必要的负担(现在Java之父后悔了!声称:当初是因为实现简单才这样设计的;后来的使用中造成了至少10亿美元的损失)。
- 定义对象的地方。这意味着它们总是能够在被构造器调用之前被初始化。
- 类的构造器中。
- 在使用这些对象的代码之前,这种方式被称为惰性初始化。
在声明的引用不必每次都生成对象的情况下,可以避免不必要的负担。 - 使用实例初始化。
……
第7章-2 继承语法
继承是所有 OOP 语言不可缺少的组成部分。
当创建一个类时,总是在 继承;除非明确地指出要从其他类中继承,否则就是在隐式地从 Java 的标准根类Object
进行继承。
继承的语法:……关键字extends
第7章-2-1 初始化基类
继承 涉及到 基类(被继承的类,又称 父类、超类)和 派生类(继承的类,又称 子类、导出类)2个类。
从外部看,派生类的对象 就像是一个 与基类具有相同接口的新类,或许还会有一些额外的 方法 和 域。
但 继承 并不只是单纯地复制基类的接口。
Java会自动在派生类的构造器中插入对基类构造器的调用。
当通过继承创建了一个派生类的对象时,该对象包含了一个基类(超类)的子对象。
⤷ 其中,对基类的子对象的正确初始化也是至关重要的,而且有且仅有一种方法来保证这一点:在 派生类构造器 中调用 基类构造器 来执行初始化,且 必须 在派生类构造器中 最开始执行。
在设计上,基类的构造器 具有 执行基类初始化所需要的 所有知识和能力,包括 超类对象的 构造器调用所必需的能力。
……
带参数的构造器
上例中各个类均含有默认的构造器,即不带参数的构造器。
如果没有 默认的 基类构造器,或者想要 调用一个 带参数的 基类构造器,就必须 使用关键字
super
显式地编写 调用基类构造器的语句,同时配以适当的 参数列表。需要格外注意的是:在 子类构造器 中,对 超类构造器 的(显式)调用动作 必须在最开始定义。
……
【拓展】第7章-2-2 重写
重写(Override) 是 子类对 [ 父类中 允许子类访问的方法 ] 的实现过程 进行重新编写(方法名、返回值、形参列表 都保持一致,访问权限 和 抛出异常 符合重写规则)。
在面向对象原则里,重写(Override)意味着可以重写任何现有的方法。
- 子类可以根据自己的需要,定义特定于自己的行为(即子类能够根据需要实现父类的方法)。
重写(Override)规则
参数列表 必须 与 被重写的方法 完全相同(参数个数、参数类型 及其 排列方式)。
访问权限 对比 父类中被重写的方法 更高 或 相等。
⤷ 例如:如果一个 父类的方法 被声明为public
,那么 子类中对应的重写方法 就不能声明为protected
。父类中的方法 只能 被它的子类 重写。
声明为
final
的方法 不能被重写。声明为
static
的方法 不能被重写,但是能够 被再次声明。static
方法 仅与 类(class
) 绑定,而不与 类的具体对象 绑定;- 继承 是描述 对象之间的关系 的概念。
⤷ 所以
static
方法 与 重写(Override)的概念 搭不上边。构造方法 不能被重写。
如果不能 继承 一个方法,则不能 重写 这个方法。
如果 子类和父类 在同一个包中,那么 子类 可以重写 [ 父类中 除了声明为
private
或final
的 其他所有方法 ]。如果 子类和父类 在不同的包中,那么 子类 只能够重写 [ 父类中 声明为
public
或protected
的、非final
修饰的 所有方法 ]。重写的方法 能够抛出 任何非强制异常,无论 被重写的方法 是否抛出异常;
⤷ 不能抛出 新的强制性异常,或者 [ 比 被重写的方法 声明得更广泛的 强制性异常 ]。
Super关键字的使用
当需要 在子类中 调用 父类的被重写方法 时,要使用
super
关键字(类似this
关键字;代替 父类指针)。
重写(Override)与重载(Overload)之间的区别
区别点 | 重载(Overload) | 重写(Override) |
---|---|---|
参数列表 | 必须修改 | 必须一致 |
返回类型 | 可以修改 | 必须一致 |
访问权限 | 可以修改 | 可以 保持一致 或 降低限制,不能做更严格的限制(访问权限 不能提高) |
抛出异常 | 可以修改 | 可以 减少 或 删除,不能抛出 新的 或者 范围更广泛的 强制性异常 |
重写(Override)与重载(Overload)之间的联系
重写(Override) 是 父类与子类之间 多态性的一种表现;
重载(Overload) 可以理解成多态的具体表现形式。
第7章-3 代理
第7章-4 结合使用组合和继承
同时使用组合和继承是很常见的事。……
第7章-4-1 确保正确清理
Java 中没有 C++ 中 析构函数 的概念。
析构函数 是一种 在对象被销毁时 可以被自动调用的函数。
⤷ 其原因可能是在 Java 的设计中,销毁对象的操作 由 垃圾回收器 进行控制执行。
⤷ 通常这样做是好事,但有时 类(class
) 可能要 在生命周期内 执行一些必需的 清理活动。
……
第7章-4-2 名称屏蔽
如果 Java 的基类 拥有某个 已被多次重载(Overload)的方法名称,那么 在派生类中 重新定义该方法名称时,并不会 屏蔽 其 在基类中的 任何版本。
⤷ 因此,无论在该层还是在它的基类中进行定义,重载(Overload)机制都可以正常工作。
……(C++ 中设计 重写(Override)时 屏蔽 基类的重载机制 的原因之一 —— 防止程序员犯错误)
Java SE 5 新增加了@Override
注解,它并不是关键字,但是可以被当做关键字使用。
@Override
注解的作用:在编译时进行 重写(Override)检验的 编译检查
⤷ 防止意外地 重载(Override)。
第7章-5 在组合与继承之间选择
组合(Composition) 和 继承(extends
) 都允许 在新的类中 放置 子对象。
组合(Composition) 是 显式地 执行;继承(extends
) 是 隐式地 执行。
要想使用好 组合 和 继承,我们需要明确:二者之间的区别何在?怎样在二者之间做出选择?
组合和继承的区别
在底层实现层面上,继承(
extends
) 实际上是 在 组合(Composition)的基础上 封装实现的抽象逻辑。在抽象逻辑层面上:
- 组合(Composition) 是“has-a”(有一个)关系的表达;
- 继承(
extends
) 是“is-a”(是一个)关系的表达。在 Java 中,继承(
extends
) 支持 从派生类向基类进行 向上转型。
在组合和继承之间的选择
组合(Composition) 通常用于 在新的类中 使用 现有类的功能 🡄 而非现有类的接口。
继承(
extends
) 往往用于 在新的类中 实现 现有类的接口。
第7章-6 protected关键字
理解了 继承,关键字
protected
才具有意义。
protected
关键字的作用:限制 对 类(class
)的成员 的访问,仅允许 [ 该类的派生类 和 同一个包下的 ] 任何类 访问。
第7章-7 向上转型
“为新的类提供方法”并不是 继承 技术中最重要的方面,其最重要的方面是 表现新的类和基类之间的所属关系,这种关系可以概括为“新的类是现有类的一种类型”。
第7章-7-1 为什么称为向上转型
该术语的使用有其历史原因,并且是以传统的 类继承图 的绘制方法为基础:将根置于页面的顶端,然后逐渐向下。
由 派生类 生成 基类,在 继承图 上是向上移动的,因此称为 向上转型。
第7章-7-2 再论组合与继承
在 OOP 中,生成和使用程序代码最有可能采用的方法是 组合 🡆 直接将数据和方法包装进一个类中,并使用该类的对象。
判断是否应该使用 继承,最清晰的标准是 是否需要 从派生类向基类进行 向上转型。
第7章-8 final关键字
根据 上下文环境,Java 的关键字final
的含义存在细微的差别,通常它指的是“这是无法改变的”。
⤷ 不想改变可能出于2种理由:设计 或 效率(Java SE 5 之后不需要考虑,交由 JVM 来优化)。
⤷ 由于这2个原因相差很远,因此关键字
final
有可能被误用。
下面讨论可能使用到final
的3种情况:数据、方法、类。
第7章-8-1 final数据
许多编程语言都有某种方法,来向编译器告知一块数据是 恒定不变的。
对于 数据的恒定不变,有2种情况:
- 一个 永不改变的 编译时常量:以
final
关键字修饰,且 值 为 基本数据类型的数据(总之就是 编译期 能够确定值的对象)。- 对于 编译常量 这种情况,编译器可以将该 常量值 代入任何可能用到它的计算式中。即 在编译时 执行计算式,减轻运行时负担。
- 在 Java 中,这类 常量 必须是 基本数据类型,并且以
final
关键字修饰。
- 一个 运行时常量 —— 在运行时 被初始化的值,且不希望它被改变。
- 在 Java 中,当
final
关键字修饰 对象引用 时,该 对象引用 会恒定不变(一旦该引用被初始化指向一个对象,就不会再被更改为其他对象);然而,该引用所指向的对象,其自身是可以被修改的。
- 在 Java 中,当
- 一个 永不改变的 编译时常量:以
Java 并未提供 使任何对象恒定不变 的途径(可以自己编写 类 以实现 使对象恒定不变的效果)。
一个既是
static
又是final
的域,只占据一段不能改变的存储空间(……)。示例:下面的示例示范了
final
域的情况。
注意,根据惯例,既是static
又是final
的域(……)将用 大写字母 表示,并使用下划线_
分隔各个单词。……
空白final
Java 允许生成“空白final
”,所谓空白final
是指:被声明为final
,但又未给定初值的域。无论什么情况,编译器都确保空白final
在使用前被初始化。
空白final
在final
关键字的使用中提供了更大的灵活性 —— 一个类中的final
域可以根据对象而有所不同,却还能保持其恒定不变的特性。
……
final参数
Java 允许 在参数列表中 以声明的方式将 参数 指明为final
。这意味着你无法在方法中更改参数引用 —— 在方法中 可以读参数,但不能修改参数。
这一特性主要用来向 匿名内部类 传递数据。
第7章-8-2 final方法
使用final
修饰方法的原因有两个:
把方法锁定,以防任何继承类修改它的含义。
⤷ 这是出于设计的考虑:想要确保该方法在继承中保持其行为不变,并且不会被覆盖。不能重写的前提是派生类能够访问到基类中的方法。
⤷ 注意:如果一个方法同时被final
和private
修饰 —— 派生类没有指定方法的访问权限,final
对方法的禁止重写的限制将会失效;派生类中可以定义与基类几乎一致(符合重写规则)的方法,作为派生类定义的新方法。在过去,建议使用
final
方法的第2个原因是 效率;在 Java 的早期实现中,如果将一个方法指明为
final
,则编译器将针对该方法的所有调用都转为 内嵌调用。……
在被final
修饰的方法不大的情况下,这样做能减少方法调用的开销;否则,程序的代码就会膨胀(……),内嵌直接带来的性能提高会因为在方法内花费过多的时间量而被缩减,实际上达不到性能提高的效果。⤷ 在最近的版本中(Java SE 5 以后),虚拟机(特别是 hotspot 技术)可以探测到这些情况,并优化去掉这些效率反而降低的 内嵌调用,因此不再需要使用
final
方法来优化了。同时,为了效率而使用
final
方法的做法逐渐地受到劝阻 —— (Java SE 5 以后的环境下)应该让编译器和 JVM 去处理效率的问题,⤷ 只有在想要明确地禁止覆盖时,才将方法设置为
final
。
final和private关键字
概述:final
+ private
= 无法覆盖 + 无法调用 = 无法调用
对于 基类中的 无法覆盖的方法,子类中 可以定义 [ 与这些基类方法 在形式上符合重写规则(方法名、参数列表、返回值、访问权限、抛出异常)的 ] 方法,但是这些子类方法只是在子类中新定义的方法,与父类无关。
第7章-8-3 final类
当 类(class
) 被修饰为final
时,该类无法被 继承。
设计目的:
该类不需要做任何改动;
出于安全性的考虑,不希望该类有子类。
final
类禁止继承,因此final
类中的 方法 都 隐式地 指定为final
。
第7章-8-4 有关final的忠告
在设计类时,将方法指定为final
会禁止方法被继承,这需要对 该类如何被复用 有明确的预见和定义。
……
第7章-9 初始化类及类的加载
在许多传统语言中,程序是作为启动过程的一部分立刻被加载的,然后是初始化,紧接着程序开始运行。
这些语言的 初始化过程 必须小心地控制,以确保定义为
static
的东西,其 初始化顺序 不会出现问题。
⤷ 例如 C++ 中,如果某个static
期望另一个static
在被初始化之前 就能有效地使用它,那么就会出现问题。
Java 采用了一种不同的加载方式避免了该问题。……
class
)的代码在初次使用时才加载一般来说,“类的代码在初次使用时才加载”,这通常指的是 加载 发生在 类(class
)的第一个对象创建之时,但是当访问static
域或static
方法时,也会发生加载。
static
的初次使用之处 也是其 初始化发生之处所有的static
对象和static
代码段,都会 在加载时,依据 程序中的顺序(即 书写顺序)依次初始化。
当然,定义为static
的东西只会被初始化一次。
……
第7章-9-1 继承与初始化
了解包括继承在内的初始化全过程,对所发生的一切有个全局把控,是很有益的。
……
第7章-10 总结
继承 和 组合 都能 从现有类型 生产新的类型。
⤷ 组合 一般是 将现有类型作为新类型底层实现的一部分 来加以复用;而 继承 复用的是接口。
在使用 继承 时,由于 派生类具有基类的接口,因此它可以 向上转型 至 基类。这对 多态 来讲至关重要。
……
第8章 多态
面向对象的程序设计语言(OOP)的基本特性:封装(数据抽象)、继承、多态。
封装:通过合并特征和行为 创建新的数据类型。
“实现隐藏”
继承:……
继承 允许 将对象(
Object
)视为 [ 它本身的类型 或者 其基类型 ] 来处理。多态(也称作 动态绑定、后期绑定 或 运行时绑定):……
多态的作用是 消除类型之间的耦合关系。
第8章-1 再论向上转型
对象 既可以 作为它本身的类型使用,也可以 作为基类型使用。而这种 [ 把 对象的引用 视为 对其基类型的引用 ] 的做法,被称作 向上转型。
但是,这样做也有一个问题,具体看下面这个例子:
……
第8章-1-1 忘记对象类型
⤷ 上例中的Music.java
看起来似乎有些奇怪。为什么所有人都故意忘记对象的类型呢?在进行向上转型时,就会产生这种情况;并且如果让tune()
方法接受一个Wind
引用作为自己的参数,似乎会更为直观。
⤷ 但这样引发的一个重要问题是:如果那样做,就需要为系统内Instrument
的每种类型都编写一个新的tune()
方法。
……
这样做行得通,但有一个主要的缺点:必须为每个新的Instrument类编写特定类型的方法。这意味着在开始时就需要更多的编程,……
此外,如果我们忘记重载某个方法,编译器不会返回任何错误信息,这样对于整个类型的处理过程就会变得难以操纵。
也许我们可以写一个简单地方法,它仅接收基类作为参数,而不接收派生类。
⤷ 这是多态所允许的,但这往往不符合大多数的业务设计场景。
第8章-2 转机
⤷ 运行上例所设计的程序后,……
那么在这种情况下,编译器如何知道这个Instrument
指向的是Wind
对象呢?实际上,编译器仅凭这些无法确定,需要使用 绑定 来解决。
第8章-2-1 方法调用绑定
前期绑定:在程序执行前 进行绑定。
前期绑定 是面向过程语言的默认绑定方式。
⤷ 例如,C 语言只有 前期绑定 这一种方法调用。后期绑定(也叫 动态绑定、运行时绑定):在程序运行时,根据 对象的类型 进行绑定。
……
后期绑定 随语言不同而有所不同,但是试想一下就知道,不管怎样都必须在对象中放置某种“类型信息”。在 Java 中,除了
static
和final
限定的方法 之外,其他所有的方法 都是 后期绑定。
第8章-2-2 产生正确的行为
在确认Java中所有方法都是通过动态绑定实现多态这个事实后,我们就可以编写只与基类打交道的程序代码了,这些代码对所有的派生类都可以正常运行;或者换一种说法,发送消息给某个对象,让该对象去判断应该执行什么动作。
在面向对象程序设计中,有一个经典的例子就是“几何形状”(shape)。
……向上转型可以像下面这条语句这么简单:1
Shape s = new Ciecle();
这条语句创建了一个
Circle
对象,并把得到的引用立即赋值给Shape
。因为通过 继承,Circle
就是一种Shape
,所以编译器认可这条语句,不会产生错误信息。假如要调用
s.draw()
方法(在Circle
中已被覆盖),由于 后期绑定(多态),最终正确调用了Circle.draw()
方法。
第8章-2-3 可拓展性
由于多态机制,我们可以从通用的基类继承出新的数据类型,这样就可以添加新功能;同时那些操作基类接口的方法不需要改动就能够应用于新类。
第8章-2-4 缺陷:“覆盖”私有方法
……
由于private
方法被自动认为是final
方法,而且对派生类是屏蔽的,所以 派生类中定义 与 基类中符合重写规则形式的方法,算不上 重写,只能算是派生类中新定义的方法;同时派生类中的方法 也不能 对基类中的private
方法进行 重载。
此时编译器不会报错,但是程序也不会按照所期望的(覆盖基类的方法)来执行。确切地说,在派生类中,对于基类的private
方法,最好采用不同的方法名。
第8章-2-5 缺陷:域与静态方法
在 Java 中,只有 普通的方法调用(非static
| final
的方法调用)可以是 多态的。
……
……
因为 静态方法 是与 类,而非与 单个的对象 相关联的。
……
第8章-3 构造器和多态
通常,构造器 不同于 其他种类的方法,涉及到 多态 时也是如此。
构造器 不具有 多态性(它们是 隐式声明的
static
方法),但还是很有必要理解 构造器怎么通过 多态 在复杂的层次结构中 运作,这将有利于 避免一些不必要的困扰。
第8章-3-1 构造器的调用顺序
构造器的调用顺序已经在第5章简要说明和第7章再次提及,但那些都是在多态引入之前讨论的。
基类的构造器 总是 在导出类的构造过程中 被调用,并且 按照继承层次 逐渐向上链接,使得 每个基类的构造器 都能得到调用。
1 | class Meal { |
输出:
1 | Meal() |
⤷ 由上例可见,构造器调用顺序:
- 调用 基类的构造器。
🡇 - 按照声明顺序 调用 成员的初始化方法。
🡇 - 调用 导出类的构造器。
第8章-3-2 继承与清理
通过 组合 和 继承 的方法 来创建新类时,一般不用担心对象的情况问题(子对象留给 垃圾回收器(GC) 进行处理)。
如果确实需要做 自定义的清理动作,需要注意 对象销毁的顺序 和 其 初始化的顺序 相反。
……
第8章-3-3 构造器内部的多态方法的行为
构造器调用的层次结构 带来了一个有趣的两难问题:
🡆 如果在 构造器的内部,调用 当前构造对象的 动态绑定方法,该如何处理?
在 一般方法的内部,动态绑定的方法 是 在运行时 才决定的;
⤷ 所以,此时使用的是 [ 对象存储空间 在初始化时的 默认值 ]。
……
⤷ 因此,编写构造器时 有一条 有效的准则:“用尽可能简单地方法 使对象进入正常状态;尽量避免 调用其他方法”。
⤷ 在构造器中 唯一能够安全调用的方法 是 基类中的final
方法。
第8章-4 协变返回类型
Java SE 5 中添加了 协变返回类型。
协变返回类型:在导出类中的、被覆盖的 方法,可以返回 基类方法返回类型的 某种导出类型。
⤷ 简单来说,协变返回类型 允许 返回 更具体的返回类型。
1 |
|
输出:
1 | Grain |
Java SE 5 以前的版本 强制 process()
的覆盖版本 必须返回Grain
类型,而不能 返回 Grain
类型的派生类型Wheat
;
⤷ 从 Java SE 5 开始,协变返回类型 允许返回 更具体的Wheat
类型。
第8章-5 用继承进行设计
……
第8章-5-1 纯继承与拓展
采用“纯粹”的方式 来创建继承层次结构 似乎是 最好的方式。
……
第8章-5-2 向下转型与运行时类型识别
由于向上转型会丢失 具体类型信息,……
⤷ 要解决这个问题,必须 有某个方法 来确保 向下转型的正确性,……
在某些程序语言(例如 C++)中,我们必须 执行特殊的操作 来获得 安全的向下转型;
⤷ 但是在 Java 中,所有转型 都会得到检查。
……
运行时类型识别(RTTI)
运行时类型识别(RTTI):在运行期间 对 类型 进行检查,如果 类型转换的返回结果 不是 程序定义时所期望的类型,就会抛出一个ClassCastException
(类转换异常)。
1 |
|
输出:
1 |
|
第8章-6 总结
多态意味着“不同的形式”。……
……
第9章 接口
接口(Interface) 和 内部类(Inner class) 为我们提供了一种 将接口与实现分离的 更加结构化的方法。
⤷ 这种机制在编程语言中并不通用。例如,C++ 对这些概念只有间接的支持。
⤷ 而在 Java 中,有 关键字 对其进行支持。
首先,我们将学习 抽象类。
抽象类是 普通的类 与 接口 之间的一种中庸之道。
这某些场景下,抽象类 可能比 接口 更合适。
关于接口
接口(Interface)默认是
abstract
的。接口中定义的 变量,默认是
public abstract final
类型的。
⤷ 所以,接口中定义的变量必须在定义时初始化,在实现类中也不能被重新定义或者修改其值。接口中定义的 方法,默认是
public abstract
类型的。
第9章-1 抽象类和抽象方法
在第8章的“乐器”例子中,基类
Instrument
中的 方法 往往是“哑”(dummy)方法;Instrument
类的设计目的是 为它的所有导出 创建一个通用接口,使得 不同的子类可以用不同的方式 实现这些通用接口。若要调用这些“哑”(dummy)方法,就会出现错误。此例中的
Instrument
类被称为 抽象基类,又称 抽象类。
因为 抽象类本身 只是定义了 接口(
interface
),没有对 接口(interface
) 进行 具体地实现,所以 创建抽象类的对象 是不必要且必须被禁止的。
为了 抽象类 概念的实现,Java 提供了 抽象方法:一种 由abstract
关键字修饰,且 只有声明、没有方法体的 特殊方法。
注意:构造器 不能被
abstract
修饰,即 构造器不能被抽象。
抽象类具体定义
一种被
abstract
关键字修饰的 类,其中 可以定义 抽象方法(也可以没有);且 定义了抽象方法的类 必须被abstract
修饰,即 只有 抽象类 才可以定义 抽象方法。抽象类 不能被实例化,但是 可以定义构造器(不能被
abstract
修饰,即不能被抽象)。由于 抽象类里会存在一些 属性,那么 抽象类 一定会有 构造方法,其存在目的是 为了属性能够正确地初始化。
抽象类,全称 抽象基类,是 在继承关系的基础上 实现的;要想使用抽象类,只能通过 其 非抽象的派生类 实现其全部抽象方法。
具体来说,抽象类的子类 可以是抽象类,此时 这些抽象的子类 不需要实现 父类的全部抽象方法;而如果 非抽象类 继承了 抽象类,则 其 必须实现 该抽象类中的全部抽象方法。
因为 抽象类 只能通过 继承关系 来使用,所以 抽象类 不能被final
关键字修饰(final
类无法被继承)。外部抽象类 不允许使用
static
修饰,而 内部抽象类 可以使用static
修饰;因为 外部类 是不能被static
修饰为 静态的,只有 内部类 可以被static
修饰(此时作为 成员,可以被static
修饰)。
使用static
修饰的 内部类,可以不需要 实例化其所在的外部类 就可以 作为静态成员使用,使用形式“外部类.内部类”。内部抽象类使用示例:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
abstract class A {
static abstract class B {
public abstract void print();
}
}
class C extends A.B {
public void print() {
System.out.println("Class C: Hello World!");
}
}
public class Test {
public static void main(String[] args) {
// 向上转型
A.B ab = new C();
ab.print();
}
}输出:
1
2
3Class C: Hello World!
Process finished with exit code 0
抽象类的使用
有时候,在抽象类中 只需要一个特定的系统子类操作,所以可以忽略掉 外部子类;这样的设计 在系统类库中 会比较常见,目的是 对用户 隐藏 不需要知道的子类。
1 |
|
输出:
1 | Hello World! |
抽象方法具体定义
抽象方法 必须被
abstract
关键字修饰,且 没有方法体(Java 8 开始 可以有方法体)。构造器 和
static
方法 不能被abstract
修饰,即 它们 不能被抽象。抽象方法 在其所在抽象类的非抽象子类中,必须被实现。
第9章-2 接口
第9章-3 完全解耦
第9章-4 Java中的多重继承
第9章-5 通过继承来拓展接口
第9章-6 适配接口
第9章-7 接口中的域
第9章-8 嵌套接口
第9章-9 接口与工厂
第9章-10 总结
第10章 内部类
第10章-1 创建内部类
第10章-2 链接到外部类
第10章-3 使用.this与.new
第10章-4 内部类与向上转型
第10章-5 在方法和作用域内的内部类
第10章-6 匿名内部类
第10章-7 嵌套类
第10章-8 为什么需要内部类
第10章-9 内部类的继承
第10章-10 内部类可以被覆盖吗
第10章-11 局部内部类
第10章-12 内部类标识符
第10章-13 总结
第11章 持有对象
第11章-1 泛型和类型安全的容器
第11章-2 基本概念
第11章-3 添加一组元素
第11章-4 容器的打印
第11章-5 List(原理 & 简介)
第11章-6 迭代器
第11章-7 LinkedList
第11章-8 Stack
第11章-9 Set
第11章-10 Map
第11章-11 Queue
第11章-12 Collection和Iterator
第11章-13 Foreach与迭代器
第11章-13-1 适配器方法惯用法→示例源码说明
第11章-14 总结
第12章 通过异常处理错误
第12章-1 概念
第12章-2 基本异常
第12章-2-1 捕获异常参数
第12章-3 捕获异常
第12章-3-1 try块
第12章-3-2 异常处理程序
第12章-4 创建自定义异常
第12章-4-1 异常与记录日志
第12章-5 异常说明
第12章-6 捕获所有异常
第12章-6-1 栈轨迹
第12章-6-2 重新抛出异常
第12章-6-3 异常链
第12章-7 Java标准异常
第12章-7-1 特例:RuntimeException
第12章-8 使用finally进行清理
第12章-8-1 finally用来做什么
第12章-8-2 在return中使用finally
第12章-8-3 缺憾:异常缺失
第12章-9 异常的限制
第12章-10 构造器
第12章-11 异常匹配
第12章-12 其他可选方式
第12章-12-1 历史
第12章-12-2 观点
第12章-12-3 把异常传递给控制台
第12章-12-4 把“被检查的异常”转换为“不检查的异常”
第12章-13 异常使用指南
第12章-14 总结
第13章 字符串
可以证明,字符串操作 是计算机程序设计中 最常见的行为。
尤其是在 Java 大展拳脚的 Web 系统中 更是如此。在本章中,我们将深入学习 在 Java 语言中 应用最广泛的 String 类,并研究 与之相关的类与工具。
第13章-1 不可变String
String 对象 是 不可变的。
查看 JDK 文档 你就会发现,String 类中 每一个看起来会修改 String 值的方法,实际上都是 创建了一个全新的 String 对象,以包含修改后的字符串内容;而最初的 String 对象则丝毫未动。
1 | package com.suite; |
运行结果:
1 | abc |
来看upperCase()
的定义,传入其中的引用 有了名字s
,只有在upperCase()
运行的时候,局部引用s
才存在。一旦upperCase()
运行结束,s
就消失了。当然了,upperCase()
的返回值,其实只是 最终结果的引用。
这足以说明,upperCase()
返回的引用 已经指向了一个 新的对象,而 原本的对象 还在原地。
String 的这种行为方式 其实正是我们想要的,难道你真的希望upperCase()
改变其 实际参数 吗?对于一个方法而言,参数 是为该方法 提供信息的,而不是 想让该方法改变自己的。
这一点很重要,正是有了这种保障,才使得代码易于编写与阅读。
第13章-2 重载“+”与StringBuilder
String 对象 是不可变的,你可以给一个 String 对象 加任意多的别名。
因为 String 对象 具有 只读特性,所以 指向它的任何引用 都不可能 改变它的值,因此,也就不会对其他引用有什么影响。
不可变性 会带来一定的 效率问题。
为 String 重载的“
+
”操作符 就是一个例子。拓展 👆
- 用于 String 的“
+
”和“+=
”是 Java 仅有的两个重载过的操作符,而 Java 不允许程序员重载任何操作符。
1 | package com.suite; |
运行结果: ⤵
使用 JDK 自带的 javap 反编译工具 查看: ⤵
如果你有汇编语言的经验,以上代码一定看着眼熟,其中的dup
和invokevirtural
语句 相当于 Java 虚拟机上的汇编语句。即使你完全不了解汇编语言 也无需担心,需要注意的重点是:编译器自动引入了java.lang.StringBuilder
类(因为它更高效)。
在这个例子中,
- 编译器 创建了一个
StringBuilder
对象,用以构造最终的String
; - 编译器 为每个字符串 调用一次
StringBuilder
的append()
方法; - 最后,编译器 调用
toString()
生成结果,并存为ss
(对应astore_2
)。
String的拼接问题
现在,也许你会觉得可以随意使用 String 对象,反正 编译器会为你 自动地优化性能。可是在这之前,让我们更深入地看看 编译器能为我们 优化到什么程度。
下面的程序 采用两种方式 生产一个 String
;方法一 使用了多个 String
对象;方法二 使用了 StringBuilder
:
==============================
(待 继续搬运并完善 个人整理笔记的内容)
==============================
第13章-3 无意识的递归
问题背景:Java 中的每个类 从根本上 都是继承自
Object
,标准容器类 自然也不例外。因此 所有类都有toString()
方法,并且复写了该方法,使得toString()
生成的结果能够表达容器自身,以及容器所包含的对象。例如ArrayList.toString()
,它会遍历ArrayList
中包含的所有对象,调用每个对象的toString()
方法。
如果你希望toString()
方法 打印出 对象的内存地址,也许 你会考虑使用this
关键字:
==============================
(待 继续搬运并完善 个人整理笔记的内容)
==============================
上例中 InfiniteRecusion
对象 自动转换为字符串时,会调用InfiniteRecusion
对象的toString()
方法,从而 陷入无限递归的死循环中。
解决方案是 换用Object
对象的toString()
方法,即 使用super.toString()
。
第13章-4 String上的操作
String 对象具备的一些基本方法:
(……)
从中可以看出,当需要 改变字符串的内容 时,String 类的方法 都会返回一个新的 String 对象。同时,如果 内容没有改变,则会 返回指向原对象的引用,这可以 节约存储空间 并 避免额外的开销。
本章稍后 还将介绍 正则表达式在 String 方法中的应用。
第13章-5 格式化输出
额外参考整理:Java格式化说明符 - 简书
Java SE 5 推出了 C
语言中printf()
风格的格式化输出
这一功能。这不仅 使得控制输出的代码更加简单,同时 也给予 Java 开发者 对于输出格式
与排列
更强大的控制能力。
第13章-5-1 printf()
C 语言中的printf()
并不能像 Java 那样 连接字符串,它使用一个简单的格式化字符串
,加上要插入其中的值,然后 将其格式化输出。
printf()
并不使用重载的“+
”操作符(C 没有重载) 来连接引号内的字符串或字符串变量,而是使用特殊的占位符来表示数据将来的位置;而且它还将插入格式化字符串的参数,以逗号分隔,排成一行。
1 | printf(“Row 1: [%d %f]\n”, x, y); |
- 过程分析:这一行代码运行的时候,
- 首先将x的值插入到%d的位置,
- 然后将y的值插入到%f的位置;其中的%d和%f这些占位符,称作格式修饰符,它们不但说明了插入数据的位置,同时还说明了将插入什么类型的变量,以及如何对其格式化。
第13章-5-4 格式化说明符
第13章-6 正则表达式
第13章-7 扫描输入
第13章-8 StringTokenizer(已废弃)
第13章-9 总结
第14章 类型信息(RTTI)
第14章-1 为什么需要RTTI
第14章-2 Class对象
第14章-2-1 类字面量
第14章-2-2 泛化的Class引用
第14章-2-3 类的转换(原:新的转型语法)
第14章-3 类型转换前先做检查
第14章-4 注册工厂
第14章-5 instanceof与Class的等价性
第14章-6 反射:运行时的类信息
第14章-6-1 类方法提取器
第14章-7 动态代理
第14章-8 空对象
第14章-8-1 模拟对象与桩
第14章-9 接口与类型信息
第14章-10 总结
第15章 泛型
第15章-1 与C++比较
第15章-2 简单泛型
第15章-2-1 一个元祖类库
第15章-2-2 一个堆栈类
第15章-2-3 RandomList
第15章-3 泛型接口
第15章-4 泛型方法
第15章-4-1 杠杆利用类型参数判断
第15章-4-2 可变参数与泛型方法
第15章-4-3 用于Generator的泛型方法
第15章-4-4 一个通用的Generator
第15章-4-5 简化元祖的使用
第15章-4-6 一个Set实用工具
第15章-5 匿名内部类
第15章-6 构建复杂模型
第15章-7 擦除的神秘之处
第15章-7-1 C++的方式
第15章-7-2 迁移兼容性
第15章-7-3 擦除的问题
第15章-7-4 边界处的动作
第15章-8 擦除的补偿
第15章-8-1 创建类型实例
第15章-8-2 泛型数组
第15章-9 边界
第15章-10 通配符
第15章-10-1 编译器有多聪明
第15章-10-2 逆变
第15章-10-3 无界通配符
第15章-10-4 捕获转换(通配符捕获)
第15章-11 问题
第15章-11-1 任何基本类型都不能作为类型
第15章-11-2 实现参数化接口
第15章-11-3 转型和警告
第15章-11-4 重载
第15章-11-5 基类劫持了接口
第15章-12 自限定的类型
第15章-12-1 古怪的循环泛型
第15章-12-2 自限定
第15章-12-3 参数协变
第15章-13 动态类型安全
第15章-14 异常
第15章-15 混型
第15章-15-1 C++中的混型
第15章-15-2 与接口混合
第15章-15-3 使用装饰器模式
第15章-15-4 与动态代理混合
第15章-16 潜在类型机制
第15章-17 对缺乏潜在类型机制的补偿
第15章-17-1 反射
第15章-17-2 将一个方法应用于序列
第15章-17-3 当你并为碰巧拥有正确的接口时
第15章-17-4 用适配器仿真潜在类型机制
第15章-18 将函数对象用作策略
第15章-19 总结:转型真的如此之糟吗?
第15章-19-1 进阶读物
第16章 数组
第16章-1 数组为什么特殊
第16章-2 数组是第一级对象
第16章-3 返回一个数组
第16章-4 多维数组
第16章-5 数组与泛型
第16章-6 创建测试数据
第16章-6-1 Arrays.fill()
第16章-6-2 数据生成器
第16章-6-3 从Generator中创建数组
第16章-7 Arrays实用功能
第16章-7-1 复制数组
第16章-7-2 数组的比较
第16章-7-3 数组元素的比较
第16章-7-4 数组排序
第16章-7-5 在已排序的数组中查找
第16章-8 总结
第17章 容器深入研究
第17章-1 完整容器分类法
第17章-2 填充容器
第17章-2-1 一种Generator解决方案
第17章-2-2 Map生成器
第17章-2-3 使用Abstract类
第17章-3 Collection的功能方法
第17章-4 可选操作
第17章-4-1 未获支持的操作
第17章-5 List的功能方法
第17章-6 Set和存储排序
第17章-6-1 SortedSet
第17章-7 队列
第17章-7-1 优先级队列
第17章-7-2 双向队列
第17章-8 理解Map
第17章-8-1 性能
第17章-8-2 SortedMap
第17章-8-3 LinkedHashMap
第17章-9 散列与散列码
第17章-9-1 理解hashCode()
第17章-9-2 为速度而散列
第17章-9-3 覆盖hashCode()
第17章-10 选择接口的不同实现
第17章-10-1 性能测试框架
第17章-10-2 对List的选择
第17章-10-3 微基准测试的危险
第17章-10-4 对Set的选择
第17章-10-5 对Map的选择
第17章-11 实用方法
第17章-11.1 List的排序和查询
第17章-11.2 设定Collection或Map为不可修改
第17章-11.3 Collection或Map的同步控制
第17章-12 持有引用
第17章-12.1 WeakHashMap
第17章-13 Java 1.0-1.1的容器
第17章-13.1 Vector和Enumeration
第17章-13.2 Hashtable
第17章-13.3 Stack
第17章-13.4 BitSet
第17章-14 总结
第18章 Java IO系统
第18章-1 File类
第18章-1-1 目录列表器
第18章-1-2 目录实用工具
第18章-1-3 目录的检查及创建
第18章-2 输入(Input)和输出(Output)
第18章-2-1 InputStream类型
第18章-2-2 OutputStream类型
第18章-3 添加属性和有用的接口
第18章-3-1 通过FilterInputStream从InputStream读取数据
第18章-3-2 通过FilterOutputStream从OutputStream写入
第18章-4 Reader和Writer
第18章-4-1 数据的来源和去处(字节流和字符流类库的关联)
第18章-4-2 更改流的行为
第18章-4-3 未发生变化的类
第18章-5 自我独立的类:RandomAccessFile
第18章-6 IO流的典型使用方式
第18章-6-1 缓冲输入文件
第18章-6-2 从内存输入
第18章-6-3 格式化的内存输入
第18章-6-4 基本的文件输出
第18章-6-5 存储和恢复数据
第18章-6-6 随机读写访问文件
第18章-6-7 管道流
第18章-7 文件读写的实用工具
第18章-7-1 读取二进制文件
第18章-8 标准IO
第18章-8-1 从标准输入中读取
第18章-8-2 将System.out转换成PrintWriter
第18章-8-3 标准IO重定向
第18章-9 进程控制
第18章-10 新IO
第18章-10-1 转换数据
第18章-10-2 获取基本类型
第18章-10-3 视图缓冲器
第18章-10-4 用缓冲器操纵数据
第18章-10-5 缓冲器的细节
第18章-10-6 内存映射文件
第18章-10-7 文件加锁
第18章-11 压缩
第18章-11-1 用GZIP进行简单压缩
第18章-11-2 用Zip进行多文件保存
第18章-11-3 Java档案文件
第18章-12 对象序列化
第18章-12-1 寻找类
第18章-12-2 序列化的控制
第18章-12-3 使用“持久性”
第18章-13 XML
第18章-14 Preferences
第18章-15 总结
第19章 枚举类型
第19章-1 基本enum特性
第19章-1-1 将静态导入用于enum
第19章-2 向enum中添加新方法
第19章-2-1 覆盖enum的方法
第19章-3 switch语句中的enum
第19章-4 values()的神秘之处
第19章-5 实现而非继承
第19章-6 随机选取
第19章-7 使用接口组织枚举
第19章-8 使用EnumSet替代标志
第19章-9 使用EnumMap
第19章-10 常量相关的方法(枚举类的抽象方法)
第19章-10-1 使用enum的职责链
第19章-10-2 使用enum的状态机
第19章-11 多路分发
第19章-11-1 使用enum分发
第19章-11-2 使用常量相关的方法
第19章-11-3 使用EnumMap分发
第19章-11-4 使用二维数组
第19章-12 总结
第20章 注解
第20章-1 基本语法
第20章-1-1 定义注解
第20章-1-2 元注解
第20章-2 编写注解处理器
第20章-2-1 注解元素
第20章-2-2 默认值限制
第20章-2-3 生成外部文件
第20章-2-4 注解不支持继承
第20章-2-5 实现处理器
第20章-3 使用apt处理注解
第20章-4 将观察者模式用于apt
第20章-5 基于注解的单元测试
第20章-5-1 将@Unit用于泛型
第20章-5-2 不需要任何“套件”
第20章-5-3 实现@Unit
第20章-5-4 移除测试代码
第20章-6 总结
第21章 并发
第21章-1 并发的多面性
第21章-2 基本的线程机制
第21章-3 共享受限资源
第21章-4 终结任务
第21章-5 线程之间的协作
第21章-6 死锁
第21章-7 新类库中的构件
第21章-8 仿真
第21章-9 性能调优
第21章-10 活动对象
第21章-11 总结
第22章 图形化用户界面
📖参看
- calc() - CSS(层叠样式表) | MDN
- fit-content() - CSS(层叠样式表) | MDN
- 【★】【GFM】GitHub Flavored Markdown Spec - github.github.com
- HTML Color Picker
- 「本站_标准颜色」 ⤵
- 『 红 』
#CC0000
- 『 粉 』
#FF6699
- 『 橙 』
#FCC000
- 『 绿 』
#6AA84F
- 『 蓝 』
#4343FF
- 『 紫 』
#9900FF
- 『 灰 』
#808080
- 『 红 』
- HTML中      等6种空白空格的区别_电脑小技巧_上网技巧_QQ地带
- Markdown 教程 | 菜鸟教程
- 👆 - 白色指向反手指数 表情符号: U+1F446 - Unicode 字符百科
- 📖 - 打开书 表情符号: U+1F4D6 - Unicode 字符百科
- 🔗 - 链接符号 表情符号: U+1F517 - Unicode 字符百科
- 🖇 - 链接回形针 表情符号: U+1F587 - Unicode 字符百科
- 🗎 - 文献: U+1F5CE - Unicode 字符百科
- ※ - 参考标志: U+203B - Unicode 字符百科
- ☌ - 关联: U+260C - Unicode 字符百科
- 🡅 - 向上重箭头: U+1F845 - Unicode 字符百科
- 🡆 - 向右重箭头: U+1F846 - Unicode 字符百科
- 🡇 - 向下重箭头: U+1F847 - Unicode 字符百科
- 🡄 - 向左重箭: U+1F844 - Unicode 字符百科
- ⤴ - 指向右侧然后向上弯曲的箭头 表情符号: U+2934 - Unicode 字符百科
- ⤵ - 指向右侧然后向下弯曲的箭头 表情符号: U+2935 cudarrr - Unicode 字符百科
- ⤶ - 指向下侧然后向左弯曲的箭头: U+2936 ldca - Unicode 字符百科
- ⤷ - 指向下侧然后向右弯曲的箭头: U+2937 rdca - Unicode 字符百科
- 🎵 - 快乐的音符 表情符号: U+1F3B5 - Unicode 字符百科
- ⇔ - 左右双箭头: U+21D4 hArr - Unicode 字符百科
- ⇒ - 向右双箭头: U+21D2 rArr - Unicode 字符百科
- — - Em 长划: U+2014 mdash - Unicode 字符百科
- 标点符号 - 维基百科,自由的百科全书
- 连接号 - 维基百科,自由的百科全书
※参考和引用
🔗外部链接
- Wikipedia's external link ltr-icon
- Java 重写(Override)与重载(Overload) | 菜鸟教程
- Java 中的枚举 (enum) - 简书
- Java 枚举(enum) 详解7种常见的用法_请叫我大师兄-CSDN博客_枚举
- SuiteLHY/DingDing: Instant Messaging System, Microservice Architecture, DDD (Domain-driven design); Spring, Spring MVC, Spring Data JPA, Hibernate, Spring Cloud, Spring Cloud Alibaba
- 枚举类 - 廖雪峰的官方网站