0%

编译原理概述

编译程序

编译程序是现代计算机系统的基本组成部分。

  • 功能上:一个编译程序就是一个语言翻译程序。把源语言翻译成目标语言。
  • 目的:让程序员不需要考虑机器的细节。

需要处理的源程序—(预处理器)—源程序—(编译程序)—-目标汇编程序—(汇编程序)—可再装配的机器代码—(装配连接编辑)—绝对机器码

编译过程

编译过程的阶段是一种逻辑上的划分:

  • 划分“前端/后端”。 将与仅依赖于源程序而与目标机器(硬件)无关的阶段组合成前端,将与目标机器(硬件)相关的阶段组合成后端。
    • 前端:语法分析程序、语义分析程序
    • 后端:中间代码生成程序、代码优化程序、目标代码生成程序
  • 划分“遍”。从头到尾扫描一遍输入串称谓遍。每遍可以完成编译的若干阶段的编译任务。

词法分析(扫描)

语法分析器读入组成源程序的字符流,并且将它们组织成为有意义的词素的序列。<token-name,attribute-value>

token-name是一个由语法分析步骤使用的抽象符号;attribute-value指向符号表中关于这个词法单元的条目。符号表条目的信息会被语义分析和代码生成步骤使用。

单词符号:

  • 常数
  • 保留字
  • 标识符
  • 运算符
  • 界符等类型(例如:空格、括号···)

语法分析(解析)

语法分析器使用由词法分析器生成的各个词法单元的第一个分量来创建树形的中间表示。该中间表示给出了词法分析产生的词法单元流的语法结构。

功能:层次分析,依据源程序的语法规则把源程序的单词序列组成语法短语(表示成语法树)

语法树中的每个内部结点表示一个运算,而该结点的子结点表示该运算的分量。

语义分析

语义分析器使用语法树和符号表中的信息来检查源程序是否和语言定义的语义一致。同时收集类型信息,并且将这些信息存放在语法树或符号表中,方便在随后的中间代码生成过程中使用。

类型检查:检查每个运算符是否具有匹配的运算分量(例如:要求的数组是一个整数,但是给定的是一个浮点数,编译器就会报错)

中间代码生成

在源程序的语法分析和语义分析完成后,很多编译器生成一个明确的低级的或类机器语言的中间表示。可以把这个表示看作是某个抽象机器的程序(应具有两个性质:易于生成、易于翻译成目标机器的语言。)

代码优化

机器无关的代码优化步骤试图改进中间代码,以便生成更好的目标代码。

符号表管理

  • 记录源程序中使用的各种符号名称

  • 收集每个符号的各种名称的属性信息

  • 类型、作用域、分配存储信息

  • 符号表管理()

    • 登录:扫描到说明语句就将标识符登记在符号表中
    • 查找:在执行语句查找标识符的属性,判断语义是否正确

错误检查

  • 报告出错信息

  • 排错

  • 恢复编译工作

编译方式和解释方式

采用编译方式的编译程序称为编译型的编译程序,简称编译程序;采用解释方式的编译程序称为解释型的编译程序,简称解释程序。

  编译方式是先翻译后执行,即将整个源程序翻译完毕,再执行目标程序,只需要保存完整的目标程序而无需保存源程序。一次翻译后无需再翻译,可多次执行。

  解释方式是边翻译边执行,即翻译一句就执行一句,翻译完毕也执行完毕,只保存源程序无需保存完整的目标程序。执行一次需要翻译一次。**

解释程序

  • 不产生目标程序文件
  • 不区别翻译阶段和执行阶段
  • 翻译源程序的每条语句后直接执行
  • 程序执行期间一直有解释程序守候
  • 常用于实现虚拟机

存储组织不同

编译程序处理时,在源语言程序被编译阶段,存储区中要为源程序(中间形式)和目标代码开辟空间,要存放编译用的各种各样表格,比如符号表.在目标代码运行阶段,存储区中主要是目标代码和数据,编译所用的任何信息都不再需要.

解释程序一般是把源程序一条语句一条语句的进行语法分析,转换为一种内部表示形式,存放在源程序区,比如BASIC解释程序,将LET和GOTO这样的关键字表示为一个字节的操作码,标识符用其在符号表的入口位置表示.因为解释程序允许在执行用户程序时修改用户程序,这就要求源程序,符号表等内容始终存放在存储区中,并且存放格式要设计的易于使用和修改.

基本概念和分类

ISA(指令系统)定义了软硬件交互的协约。

指令系统设计原则

  • 可编程性
  • 可实现性
  • 兼容性

指令系统设计要素

  • 指令格式:包括了指令长度(定长或者是变长)以及编码方式

  • 操作数存储位置(寄存器、主存、累加器、堆栈)、类型(整型、浮点)、长度(字节、字、双字)和个数(1,2,3,多操作数)

  • 寻址方式

  • 支持的操作类型:加减、比较

指令系统结构分类

根据操作数的存储位置对指令进行分类:

  • 主存型结构:主存
  • 累加器型结构:累加器
  • 堆栈型结构:堆栈
  • 通用寄存器结构:通用寄存器组

指令系统中操作数给出方式:

  • 显式给出:用指令字中的操作数字段给出
  • 隐式给出:隐式给出则是使用实现约定好的单元

指令系统的发展和改进

由性能公式:CPU时间=指令条数IC*CPI *周期时间

复杂指令系统CISC

改进方法:

  • 减少指令条数,使用复杂的指令
  • 对于使用频率高的指令串,用一条新的指令来代替

问题:

  • 设计周期长,准确性难以保证
  • 需要大量的硬件支持
  • 很多复杂指令使用频率低,造成资源浪费
  • 许多指令由于操作复杂,其CPI值比较大,执行速度慢
  • 规整性不好,不利于采用流水线技术来提高性能

精简指令系统RISC

RISC遵循的原则:

  • 指令条数少,功能简单
    【只选取使用频率很高的指令,再补充一些其他最有用的指令】
  • 指令格式简单、规整、并减少寻址方式
  • 指令的执行在单个周期内完成(采用流水线机制)
  • 只有load和store指令才能访问存储器,其他指令的操作都是在寄存器之间进行(load-store结构)
  • 大多数指令都采用硬连逻辑来实现
  • 强调优化编译器的作用,为高级语言程序生成优化的代码
  • 充分利用流水线技术来提高性能

改进方法:

  • 减少CPI,使用大量单周期指令
  • 增加指令条数,复杂的指令使用频率很低,实际程序的指令条数并不太多
  • 减少时钟周期时间

绪论

数据模型

数据模型是数据库系统的核心和基础。它是现实世界的模拟。

三要素:数据结构、数据操作、数据约束

概念模型

信息模型,按用户的观点来对数据和信息建模。

  • 实体:客观存在并可相互区别的事物
  • 属性:实体所具有的某一特性
  • 码:唯一标识实体的属性集
  • 实体型:

逻辑模型和物理模型

  • 逻辑模型主要包括网状模型、层次模型、关系模型、面向对象模型···
  • 物理模型是对数据最底层的抽象,描述数据在系统内部的表示方式和存取方法,在磁盘或者是磁带上的存储方式和存取方法。

客观对象的抽象过程–两步抽象

1、客观对象抽象为概念模型

2、把概念模型转换为某一DBMS支持的数据模型

常用的数据模型

  • 非关系模型
    • 层次模型
    • 网状模型
  • 关系模型
    • 面向对象模型
    • 对象关系模型

层次模型

使用树状结构来表示各类实体以及实体间的联系。

要求:

  1. 有且只有一个结点没有双亲结点,这个结点称为根节点
  2. 根以外的其他结点有且只有一个双亲结点

根据上述的要求,可以确认层次模型其实是父与子之间一对多的联系。每个结点表示的是记录类型,记录类型之间的联系用结点之间的连线表示。

层次模型的特点

  • 结点的双亲是唯一的
  • 只能直接处理一对多的实体联系
    【如果一个结点有多个双亲结点的话,只能通过引入冗余数据或者创建非自然的数据结构来解决】
  • 每个记录类型可以定义一个排序字段(码字段)
    【方便查找,并且查找效率高】
  • 任何记录值只有按其路径查看时,才能显出它的全部意义
    【可以直观的知道其一脉相承的父子关系,结构严密】
  • 没有一个子女记录值能够脱离双亲记录值而独立存在
    【例如:插入操作时,要先找到其父结点值;删除操作时,其子结点会一并被删除】
  • 查询子女结点必须通过双亲结点

多对多联系在层次模型中的表示

基本思路是:将多对多联系分解成一对多联系

冗余结点法:就是将存在多对多联系的结点拆分成一对多联系的模式。会产生多个根节点,同时该多个根结点也表示子结点。

虚拟结点法:就是将存在多对多联系的结点拆分,产生多个根结点。每个根节点连接一个虚拟结点【该虚拟结点就是上述拆开的子节点】

增删查改与完整性约束

  • 无相应的双亲结点值就不能插入子女结点值
  • 如果删除双亲结点值,则相应的子女结点值也会被删除
  • 更新操作时,应更新所有相应记录,以保证数据的一致性

层次数据模型的存储结构

邻接法

按照层次树前序遍历将所有的记录值一次邻接存储,通过物理空间的位置相邻来实现层次顺序

链接法

用指针来反映数据之间的层次联系

网状模型

要求:

  1. 允许一个以上的结点无双亲结点
  2. 一个结点可以有多个双亲结点

层次模型可以看成是网状模型的一个特例

关系模型

用二维表来表示实体及其联系:行、列

用表格表示实体集,用列表示属性,表结构表示实体的型

用表间的特定的冗余信息表示实体间的联系(主键和外键)

行、列是无序的

列不可再分

没有重复行

关系规范化要求:关系的每一个分量必须是一个不可分的数据项 不允许表中还有表

关系模型的存储结构

实体及实体间的联系都用表来表示

表以文件形式存储

数据库系统结构

在数据模型中有“型”和“值”的概念。型是指对某一类数据的结构和属性的说明。值是型的一个具体赋值

数据库系统内部的体系结构:采取三级模式结构

数据库系统外部的体系结构:

  • 单用户结构
  • 主从式结构
  • 分布式结构
  • 客户|服务器
  • 浏览器|应用服务器|数据库服务器多层结构

数据库系统的三级模式结构

模式(逻辑模式)

对数据库中的全部数据的逻辑结构和特征的描述,它仅仅涉及到的是型的描述,不涉及到具体的值。

模式的定义:

  • 数据的逻辑结构(包括了数据项的名字、类型、取值范围···)
  • 数据之间的联系
  • 数据有关的安全性、完整性要求

模式(schema):反映的是数据的结构及其联系

实例(instance):模式的一个具体值,实例会随着数据库中的数据的更新而变动

模式是相对稳定的,而实例是相对变动的

一个数据库只有一个模式,可以将模式视为是数据库数据在逻辑级上的视图。

==模式是数据库系统模式结构的中间层==

  • 与数据的物理存储细节和硬件环境无关
  • 与具体的应用程序、开发工具及高级程序设计语言无关

外模式(子模式、用户模式)

数据库用户的数据视图,是用户使用的局部数据的逻辑结构和特征的描述

==外模式介于模式和应用之间==

  • 模式与外模式的关系:一对多
    • 外模式通常是模式的子集
    • 一个数据库可以拥有多个外模式。
    • 对模式中同一数据,在外模式中的结构、类型、长度、保密级别都不同(这倒是让我想起了在同一个项目下使用不同数据库存储不同的数据)
  • 外模式与应用的关系:一对多
    • 同一外模式可以为某个用户的多个应用系统所使用
    • 但是一个应用程序只能使用一个外模式

外模式的用途

  • 保证数据库安全性
  • 每个用户只能看见和访问所对应的外模式中的数据

内模式(存储模式)

是数据物理结构和存储方式的描述(不同数据库的内部实现方式)

是数据在数据库内部的表示方式

  • 记录的存储方式(顺序存储、按照B树结构存储、按照hash方式存储)
  • 索引的组织方式
  • 数据是否压缩存储
  • 数据是否加密
  • 数据存储记录结构的规定

一个数据库只有一个内模式

数据库的二级映像功能与数据独立性

二级映像在DBMS内部实现这三个抽象层次的联系和转换

  • 外模式|模式映像
  • 模式|内模式映像

外模式|模式映象

定义外模式与模式之间的对应关系。映象定义通常包含在各自外模式的描述中。

模式描述的是数据的全局逻辑结构;外模式描述的是数据的局部逻辑结构。

同一个模式下可以有任意多个外模式。

保证数据的逻辑独立性:应用程序是依据数据的外模式编写的,从而应用程序不必修改,保证了数据与程序的逻辑独立性。

模式|内模式映象

定义了数据全局逻辑结构与存储结构之间的对应关系。通常包含在模式描述中。

数据库中模式|内模式映象是唯一的。

保证了数据的物理独立性:当数据库的存储结构改变了,只要修改模式|内模式映象使得模式保持不变,应用程序就不会受到影响。

数据库模式

  • 全局逻辑结构
  • 设计数据库模式结构时应该首先确定数据库的逻辑模式

数据库的内模式

  • 依赖其全局逻辑结构
  • 独立于数据库的用户视图(外模式)
  • 独立于具体的存储设备
  • 将全局逻辑结构中所定义的数据结构及其联系按照一定的物理存储策略进行组织,提高空间使用率。

数据库的外模式

  • 面向的是具体的应用程序
  • 定义在逻辑模式之上
  • 独立于存储模式和存储设备
  • 当应用需求发生较大的变化时,相对应外模式不能满足其视图要求时,该外模式就得做出相应改动。
  • 设计外模式时应充分考虑到应用的扩展性

特定的应用程序

  • 是在外模式描述的数据结构上编制的
  • 依赖于特定的外模式
  • 与数据库的模式和存储结构独立
  • 不同的应用程序有时可以共用一个外模式

数据库系统的组成

硬件平台及数据库

  1. 足够大的内存

    存放操作系统、数据库管理系统的核心模块、数据缓冲区和应用程序

  2. 足够大的外存

    可以用作数据的备份

  3. 较高的通道能力,提高数据传送率

软件

  1. DBMS

    数据库管理系统是为数据库的建立、使用和维护配置的系统软件

  2. 支持DBMS运行的操作系统

  3. 与数据库接口的高级语言和配套的编译系统

  4. 以DBMS为核心的应用开发工具

  5. 为特定应用环境开发的数据库应用系统

人员

  1. 数据库管理人员DBA

  2. 系统分析员和数据库设计人员

  3. 应用程序员

  4. 用户

一些问题

文件系统和数据库系统都能管理数据,也都支持通过应用程序访问数据,两种方式在数据独立性上有何不同?

数据管理文件系统阶段和数据库系统阶段“数据独立性”有何不同?

在数据管理技术的发展过程中,经历了人工管理阶段、文件系统阶段和数据库系统阶段,其中数据独立性最高的阶段是数据库系统。数据库阶段用数据模型表示复杂的数据,有较高的数据独立性。数据库系统为用户提供了方便的用户接口,用户可使用查询语言或终端命令操作数据库,也可以用程序方式操作数据库。数据库管理系统提供了数据控制功能。

文件系统和数据库系统之间的区别:

(1) 文件系统用文件将数据长期保存在外存上,数据库系统用数据库统一存储数据;

(2) 文件系统中的程序和数据有一定的联系,数据库系统中的程序和数据分离;

(3) 文件系统用操作系统中的存取方法对数据进行管理,数据库系统用DBMS统一管理和控制数据;

(4) 文件系统实现以文件为单位的数据共享,数据库系统实现以记录和字段为单位的数据共享。

文件系统和数据库系统之间的联系:

(1) 均为数据组织的管理技术;

(2) 均由数据管理软件管理数据,程序与数据之间用存取方法进行转换;

(3) 数据库系统是在文件系统的基础上发展而来的。

文件系统是操作系统用于明确存储设备(常见的是磁盘,也有基于NAND Flash的固态硬盘)或分区上的文件的方法和数据结构;即在存储设备上组织文件的方法。操作系统中负责管理和存储文件信息的软件机构称为文件管理系统,简称文件系统。

文件系统由三部分组成:文件系统的接口,对对象操纵和管理的软件集合,对象及属性。从系统角度来看,文件系统是对文件存储设备的空间进行组织和分配,负责文件存储并对存入的文件进行保护和检索的系统。具体地说,它负责为用户建立文件,存入、读出、修改、转储文件,控制文件的存取,当用户不再使用时撤销文件等。

数据库系统在管理数据时采用的分层管理的思想,也就是三级模式两级映像的架构,这么做起到了什么作用?

采用三级模式和二级映像的原因:

1)保证了数据的独立性。将模式与内模式、模式与外模式分开保证了数据的物理独立性和逻辑独立性

2)简化了用户接口,按照外模式编写应用程序或敲入命令,不需了解内部结构

3)有利于数据共享,不同应用可共用一个外模式,减少了数据冗余

4)有利于数据的安全保密,在外模式限定下进行操作,不能对限定数据操作,保证了其他数据的安全

ML概览

  • ML强调的是表达式估计
  • ML更倾向于代数
  • ML表达式特征:类型可有可无、值可有可无、效果可有可无
  • 类型检查十分严格

ML标准类型

  • 基础类型(basic types) unit, int, real, bool, string
  • 表(lists) int list, (int -> int) list
  • 元组(tuples) int * int, int * int * real
  • 函数(functions) int -> int, real -> int * int

所有对象都要有类型,不一定显式说明,但必须能静态推导(在编译时该类型能被编译器根据上下文推算出来)。例如:int y=x+3就默认了x和y均是int型,因为会强制类型转换。

  • 单元(unit) 只包含一个元素,用空的括号表示,类似于C语言中的void类型。 ( ) : unit
  • 整型(int) 负号用“~”表示。
  • 浮点型(real)
  • 布尔型(bool) true, false
  • 字符串型(string) 双引号间的字符序列

  • 包含相同类型的元素序列
  • 表中元素用“,”分隔,整个表用[ ]括起来
  • 空表:[ ]或nil
  • 表的类型表达式取决于表的元素类型,写作:
    <元素类型> list

如: int list, (int -> int) list

  • 表可以嵌套

如: [1,2,3] : int list [“张三”, “李四”]: string list

  • 相同类型元素的有限序列

  • 元素可以重复出现,其顺序是有意义的

  • 表中元素可以为任意类型,但需具有相同类型

  • 表为多态类型

  • 表的基本函数:

    :: (追加元素), @ (连接表), null (空表测试), hd(返回表头元素), tl(返回非空表的表尾), length(返回表长)

    • [1, 3, 2, 1, 21+21] : int list

    • [true, false, true] : bool list

    • [[1],[2, 3]] : (int list) list

    • [ ] : int list, [ ] : bool list, ……

    • 1::[2, 3] = [1, 2, 3]

    • [1, 2]@[3, 4] = [1, 2, 3, 4]

    • nil = [ ]

元组

  • 包含任意类型数据元素的定长序列
  • 类型表达式:每个元素的类型用*间隔并排列在一起。如: int * int, int * int * real
  • 圆括号中用逗号分隔的数据元素,允许嵌套。如: (“张三”, “男”, 19, 1.75)

​ [((“赵”,”子昂”),21, 1.81), ((“张”, “文艺”), 20, 1.69)]对应的是((string * string)* int * real)list

记录

  • 类似C中的结构类型,可以包含不同类型的元素
  • 每个元素有个名字
  • 记录的值和类型的写法都用{ }括起来。如: {First_name=“赵”, Last_name=“子昂”}

元组、表和记录的异同点

  • 符号:() 、 [ ] 、{ }
  • 元素类型:可以不同 、 必须相同 、可以不同
  • 长度:定长 、 变长 、变长

函数

  • 以一定的规则将定义域上的值映射到值域上去
  • 类型由它的定义域类型和值域类型共同描述
  • ->表示定义域到值域的映射

fn: <定义域类型> -> <值域类型>。如: fun add(x, y) = x + y;

ML标准函数

  • 标准布尔函数:not, andalso, orelse。如:not true; true andalso false; true orelse false;

  • 标准算数运算函数:~, +, -, *, div, /。如:6 * 7; 3.0 * 2.0;

    • 运算符重载(operator ): 把同一运算符作用在不同类型上。
    • 重载运算符的两边必须为同一类型。
    • 整数到实数的转换:real
    • 实数到整数的转换:floor(下取整), ceil(上取整), round(四舍五入),trunc(忽略小数)
  • 标准字符串函数:

    • 把两个字符串合并成一个:^
    • 返回字符串的长度:size

  • 每个类型都有一个值的集合
    For each type t there is a set of values

  • 一个类型的表达式求值结果为该类型的一个值(或出错)
    An expression of type t evaluates to a value of type t (or fails to terminate)

函数求值

函数:以一定的规则将定义域上的值映射到值域上

原型: fn:<定义域类型> -> <值域类型>

声明

赋予某个对象一个名字,包括值、类型、签名、结构和函子

  • 函数的声明: fun <函数名> (<形式参数>) : <结果类型> = <函数体>

例:fun divmod(x:int, y:int) : int*int = (x div y, x mod y)

  • 值的声明:val pi = 3.1415;val (q:int, r:int) = divmod(42, 5);

    采用静态绑定方式——重新声明不会损坏系统、库或程序

  • 类型绑定:type float = real

    type count = int and average = real

  • 值绑定:val m : int = 3+2

    val pi : real = 3.14 and e : real = 2.17

  • 组合声明:val m : int = 3+2

    val n : int = m*m

声明的使用

声明函数:
check : int * int -> bool

局部声明:

let D in E end

1
2
3
4
5
6
fun check(x:int, y:int):bool =
let
val (q:int, r:int)= divmod(x, y)
in
(x = q*y + r)
end

全局声明

1
2
3
4
5
6
val pi : real = 3.14;
fun square(r:real) : real = r * r;
fun area(r:real) : real = pi * square(r);

val pi : real = 3.14159;
fun area(r:real) : real = pi * square(r);

声明、类型和值

  • 任意一个类型的表达式都可以进行求值操作
  • 任意一个类型表达式求值的结果为该类型的一个值
  • ML提供重新声明功能
  • 声明将产生名字(变量)和值的绑定(结合)
  • 绑定具有静态作用域

ML =

  • “=”用于类型的等式判断,称为等式类型(“equality types”)
  • 等式类型包括整数、布尔值结合元组、表等构造子生成的类型

模式

  • 只包含变量、构造子(数值、字符、元组、表等)和通配符的表达式

    • 模式中不是构造子的名字,是变量
    • 模式中的变量必须彼此不同
    • 构造子必须和变量区分开来
  • 通配符: _

  • 变量 : x //同一模式中,一个变量不能出现两次

  • 常数 : 42, true, ~3 // 实数和函数没有常数模式

  • 元组 : (p1, …, pk) //p1, …, pk均为模式

  • 表 : nil, p1::p2, [p1, …, pk]

规则说明

  • 部分操作的内建规则:
    • 结合性强于 ->
    • 无结合规则
    • -> 为右结合

替代

  • 给定集合绑定值【 x1:v1,…,xk:vk 】

  • 及表达式e,计为[ x1:v1, …, xk:vk ] e

  • 表达式替换为

  • v1 for x1,…,vk for xk

  • [ x:2 ] (x + x) is (2 + 2);[ x:2 ] (fn y => x + y) is (fn y => 2 + y)

    [ x:2 ] (if x>0 then 1 else f(x-1)) is (if 2>0 then 1 else f(2-1))

代码说明

  • 函数定义前,用注释信息描述函数功能,形如(* comments*) :
    • 函数名字和类型 (类型定义)
    • REQUIRES:参数说明 (明确参数范围)
    • ENSURES:函数在有效参数范围内的执行结果 (函数功能)

范例1:函数eval的说明

fun eval ([ ]:int list) : int = 0
| eval (d::L) = d + 10 * (eval L);

(* eval : int list -> int )
( * REQUIRES: )
( * every integer in L is a decimal digit * ) (
ENSURES: * )
(
eval(L) evaluates to a non-negative integer *)

程序正确性证明

  • 基于等式或者推导的方式进行数学证明
  • 程序结构作为指导

为什么要进行程序正确性证明?

传统程序编写地是否正确性依靠分支测试,根据条件分支输入不同的数据检验是否正确?延伸出来很多自动化测试工具,黑盒测试、白盒测试和覆盖度测试。

函数式编程程序正确性证明是代码完成后用严格的数学推导证明对所有可能输入都产生正确结果。

归纳法

简单归纳法

  • 适用于涉及自然数的递归函数
    • 参数为非负整数
    • f(x)的递归调用形如f(y),且size(y)=size(x)-1

完全归纳法

证明对所有非负整数n,P(n)都成立

将P(k)简化为k个子问题: P(0), P(1), … , P(k-1),且它们均成立时,可以利用{P(0), P(1), … , P(k-1)}推导出P(k)也成立

如:P(0)成立

​ P(1)可由P(0)推导出来

​ P(2)可由P(0), P(1)推导出来

​ P(3)可由P(0), P(1), P(2)推导出来

​ ……

​ P(k)可由P(0), P(1), … , P(k-1)推导出来

  • 适用于涉及自然数的递归函数
    • 参数为非负整数
    • f(x)的递归调用形如f(y),且size(y)<size(x)

结构归纳法

基本情形: P([ ])

归纳步骤:对具有类型t的所有元素y和t list类型的数ys,都有P(ys)成立时, P(y::ys)成立

∀ i < k, P(i)成立的条件下有P(k)

  • 适用于涉及表和树的递归函数

良基归纳法

关系≺是良基的:

不存在无穷降序链:…≺Xn≺…≺X2≺X1,

对所有y’≺y,有P(y),则P(y’)成立

  • 可以处理广泛的可终止计算问题

近似运行时间(近似时间复杂度)

  • 反映基于大批量数据的程序运行性能

    • 假设基本操作为常量执行时间(Assume basic ops take constant time)

    • 用Ο记号表示算法的时间性能(Give big-O classification)

  • 求解步骤:

    • 1.找出算法中的基本语句:算法中执行次数最多的那条语句就是基本语句,通常是最内层循环的循环体
    • 2.计算基本语句的执行次数的数量级:忽略所有低次幂和最高次幂的系数,保证基本语句执行次数的函数中的最高次幂正确
    • 3.用Ο记号表示算法的时间性能:将基本语句执行次数的数量级放入Ο记号中。

递归分析

递归函数的定义给出了程序的递推关系,执行情况用work表示

functional programming

函数式语言的特点

  • 不依赖于冯·诺伊曼体系结构的计算机

  • 函数是”第一等公民”

    所谓“第一等公民”(first class),指的是函数与其他数据类型一样,处于平等地位,可以赋值给其他变量,也可以作为参数,传入另一个函数,或者作为别的函数的返回值。

  • 只用”表达式”,不用”语句”

    “表达式”(expression)是一个单纯的运算过程,总是有返回值;”语句”(statement)是执行某种操作,没有返回值。函数式编程要求,只使用表达式,不使用语句。也就是说,每一步都是单纯的运算,而且都有返回值。

    原因是函数式编程的开发动机,一开始就是为了处理运算(computation),不考虑系统的读写(I/O)。”语句”属于对系统的读写操作,所以就被排斥在外。

    当然,实际应用中,不做I/O是不可能的。因此,编程过程中,函数式编程只要求把I/O限制到最小,不要有不必要的读写行为,保持计算过程的单纯性。

  • 没有”副作用”

    所谓“副作用”(side effect),指的是函数内部与外部互动(最典型的情况,就是修改全局变量的值),产生运算以外的其他结果。

    函数式编程强调没有”副作用”,意味着函数要保持独立,所有功能就是返回一个新的值,没有其他行为,尤其是不得修改外部变量的值。

  • 不修改状态

    上一点已经提到,函数式编程只是返回新的值,不修改系统变量。因此,不修改变量,也是它的一个重要特点。

    在其他类型的语言中,变量往往用来保存”状态”(state)。不修改变量,意味着状态不能保存在变量中。函数式编程使用参数保存状态,最好的例子就是递归

    由于使用了递归,函数式语言的运行速度比较慢。

    【递归的速度慢,一般都是将递归写成尾递归,然后编译器进行优化,将尾递归转化成迭代】

  • 引用透明(referential transparency)

    引用透明(Referential transparency),指的是函数的运行不依赖于外部变量或”状态”,只依赖于输入的参数,任何时候只要参数相同,引用函数所得到的返回值总是相同的。

    有了前面的第三点和第四点,这点是很显然的。其他类型的语言,函数的返回值往往与系统状态有关,不同的状态之下,返回值是不一样的。这就叫”引用不透明”,很不利于观察和理解程序的行为。

  • 确定性(determinism)

    所谓确定性的意思就是像数学那样 f(x) = y ,这个函数无论在什么场景下,都会得到同样的结果,这个我们称之为函数的确定性。而不是像程序中的很多函数那样,同一个参数,却会在不同的场景下计算出不同的结果。所谓不同的场景的意思就是我们的函数会根据一些运行中的状态信息的不同而发生变化。

  • 惰性求值(延迟计算)与并行

    惰性求值:

    这个需要编译器的支持。表达式不在它被绑定到变量之后就立即求值,而是在该值被取用的时候求值。也就是说,当调用函数时,不是盲目的计算所有实参的值后再进入函数体,而是先进入函数体,只有当需要实参值时才计算所需的实参值(按需调用

  • 递归调用及其优化

    就是使用尾递归进行优化

函数式语言的优点

  1. 代码简洁,开发快速

    其实这条是比较好理解的,函数式的编程使用了大量的函数,减少了代码的重复。

  2. 易于理解

  3. 方便代码管理

    函数式编程不依赖、也不会改变外界的状态,只要给定输入参数,返回的结果必定相同。因此,每一个函数都可以被看做独立单元,很有利于进行单元测试(unit testing)和除错(debugging),以及模块化组合。

  4. 易于“并发编程”

    函数式编程不需要考虑”死锁”(deadlock),因为它不修改变量,所以根本不存在”锁”线程的问题。不必担心一个线程的数据,被另一个线程修改,所以可以很放心地把工作分摊到多个线程,部署”并发编程”(concurrency)。

  5. 可以进行代码的热升级

    函数式编程没有副作用,只要保证接口不变,内部实现是外部无关的。所以,可以在运行状态下直接升级代码,不需要重启,也不需要停机。

函数式编程的几个技术

  1. map&reduce

  2. pipeline

  3. recursing递归

  4. currying

  5. higher-order function高阶函数

    所谓高阶函数就是函数当参数,把传入的函数做一个封装,然后返回这个封装函数。现象上就是函数传进传出,就像面向对象对象满天飞一样。

参考文章

参考文章

双指针法

双指针法可以分成两个部分:快慢指针,左右指针。前者解决主要解决链表中的问题,比如典型的判定链表中是否包含环;后者主要解决数组(或者字符串)中的问题,比如二分查找。

(注:这里的指针,并非专指c中指针的概念,而是指索引,游标或指针,可迭代对象等)

快慢指针常见算法

左右指针常见算法

左右指针在数组中实际是指两个索引值,一般初始化为 left = 0, right = nums.length - 1 。

一般我们会对数组进行排序,然后在进行比较时,通过移动指针来找到解。

二分查找

两个指针的算法都是很基础的,也是很简单的,通常只要取一头一尾进行移动比较即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int binarySearch(int[] nums, int target) 
{
int left = 0;
int right = nums.length - 1;
while(left <= right)
{
int mid = (right + left) / 2;
if (nums[mid] == target)
return mid;
else if (nums[mid] < target)
left = mid + 1;
else if (nums[mid] > target)
right = mid - 1;
}
}

两数之和

Leetcode:167——两数之和II-输入有序数组

题目描述:在有序数组中找出两个数,使它们的和为 target。

在进行数组元素相加后比较之类的问题,都可以采取双指针方式进行求解。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Solution {
public int[] twoSum(int[] numbers, int target) {
int right=0;
int left=numbers.length-1;
int []output = new int[2];
while(right<left)
{
if(numbers[right]+numbers[left]==target){
output[0]=right+1;
output[1]=left+1;
break;
}
else if(numbers[right]+numbers[left]<target)
{
right++;
}
else
{
left--;
}
}
return output;
}
}

两数平方和

Leetcode:633——平方数之和

题目描述:给定一个非负整数 c ,你要判断是否存在两个整数 a 和 b,使得 a^2^ + b^2^ = c。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Solution {
public boolean judgeSquareSum(int c) {
int right = 0,left =(int)Math.sqrt(c);
while(right<=left)
{
if(Math.pow(right,2)+Math.pow(left,2)==c)
{
return true;
}
else if(Math.pow(right,2)+Math.pow(left,2)<c)
{
right++;
}
else
{
left--;
}
}
return false;
}
}

Leetcode:18

左右指针:

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
class Solution {
public List<List<Integer>> fourSum(int[] nums, int target) {
List<List<Integer>> result = new ArrayList<>();
if(nums==null||nums.length<4)
{
return result;
}
Arrays.sort(nums);
int length=nums.length;
//一个指向最小的值,一个指向最大的值
int head,tail;
int left,right;
for(head=0;head<length-3;head++)
{
//在有相同的数的前提下,移动到最右边的数
if(head>0&&nums[head]==nums[head-1])
continue;
//如果当前可以取得的最小值比目标值大的话,直接退出。
int min=nums[head]+nums[head+1]+nums[head+2]+nums[head+3];
if(min>target)break;
//如果当前取得的最大值比目标值小的话,将head往后移动
int max = nums[head]+nums[length-1]+nums[length-2]+nums[length-3];
if(max<target)continue;
for(left=head+1;left<length-2;left++)
{
tail = length-1;
right = left+1;
if(left>head+1&&nums[left-1]==nums[left])continue;
min = nums[head]+nums[left]+nums[left+1]+nums[left+2];
if(min>target)continue;
max = nums[head]+nums[left]+nums[tail-1]+nums[tail];
if(max<target)
{
continue;
}
while(right<tail)
{
int all = nums[head]+nums[left]+nums[right]+nums[tail];
if(all==target)
{
result.add(Arrays.asList(nums[head],nums[left],nums[right],nums[tail]));
right++;
while(right>left&&right<tail&&nums[right-1]==nums[right])
{
right++;
}
tail--;
while(tail>left&&nums[tail]==nums[tail+1])
{
tail--;
}
}
else if(all>target)
{
tail--;
}
else if(all<target)
{
right++;
}

}
}
}
return result;
}

}

Java- 反射机制

参考文章:重要

参考文章

什么是反射机制

简单来说,反射可以帮助我们在动态运行的时候,对于任意一个类,可以获得其所有的方法(包括 public protected private 默认状态的),所有的变量 (包括 public protected private 默认状态的)。程序中一般的对象的类型都是在编译期就确定下来的,而 Java 反射机制可以动态地创建对象并调用其属性,这样的对象的类型在编译期是未知的。所以我们可以通过反射机制直接创建对象,即使这个对象的类型在编译期是未知的。

反射的核心是 JVM 在运行时才动态加载类或调用方法/访问属性,它不需要事先(写代码的时候或编译期)知道运行对象是谁。

Java 反射主要提供以下功能:

  • 在运行时判断任意一个对象所属的类;
  • 在运行时构造任意一个类的对象;
  • 在运行时判断任意一个类所具有的成员变量和方法(通过反射甚至可以调用private方法);
  • 在运行时调用任意一个对象的方法

重点:是运行时而不是编译时

反射的主要用途

很多人都认为反射在实际的 Java 开发应用中并不广泛,其实不然。当我们在使用 IDE(如 Eclipse,IDEA)时,当我们输入一个对象或类并想调用它的属性或方法时,一按点号,编译器就会自动列出它的属性或方法,这里就会用到反射。

反射最重要的用途就是开发各种通用框架。很多框架(比如 Spring)都是配置化的(比如通过 XML 文件配置 Bean),为了保证框架的通用性,它们可能需要根据配置文件加载不同的对象或类,调用不同的方法,这个时候就必须用到反射,运行时动态加载需要加载的对象。

  • 静态编译:在编译时确定类型,绑定对象
  • 动态编译:运行时确定类型,绑定对象

  两者的区别在于,动态编译可以最大程度地支持多态,而多态最大的意义在于降低类的耦合性,因此反射的优点就很明显了:解耦以及提高代码的灵活性。

  因此,反射的优势和劣势分别在于:

  • 优势
    • 运行期类型的判断,动态类加载:提高代码灵活度
  • 劣势
    • 性能瓶颈:反射相当于一系列解释操作,通知 JVM 要做的事情,性能比直接的java代码要慢很多

反射的基本运用

1、获得class对象

Class对象

在Java中有两种对象:Class对象和实例对象,实例对象是类的实例,通常是通过 new关键字构建的。Class对象是JVM生成用来保存对象的类的信息的Java程序执行之前需要经过编译、加载、链接和初始化这几个阶段,编译阶段会将源码文件编译为 .class字节码文件,编译器同时会在 .class文件中生成Class对象,加载阶段通过JVM内部的类加载机制,将Class对象加载到内存中。在创建对象实例之前,JVM会先检查Class对象是否在内存中存在,如果不存在,则加载Class对象,然后再创建对象实例,如果存在,则直接根据Class对象创建对象实例。JVM中只有一个Class对象,但可以根据Class对象生成多个对象实例。

Class对象的获得

1、类名.class

当执行 类名.class时,JVM会先检查Class对象是否装入内存,如果没有装入内存,则将Class对象装入内存,然后返回Class对象,如果装入内存,则直接返回Class对象。在加载Class对象后,不会对Class对象进行初始化。

  • Example
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Test {

static {
System.out.println("Run static initialization block.");
}

{
System.out.println("Run nonstatic initialization block.");
}
}

public class ClassTest {

/**
* @param args
*/
public static void main(String[] args) {
Class t = Test.class;
}
}
  • Result
1
// 空
2、 Class.forName()

当执行 Class.forName()时,JVM也会先检查Class对象是否装入内存,如果没有装入内存,则将Class对象装入内存,然后返回Class对象,如果装入内存,则直接返回Class对象。在加载Class对象后,会对类进行初始化,即执行类的静态代码块。forName()方法中的参数是类名字符串,类名字符串 = 包名 + 类名。Class.forName()的一个很常见的用法是在加载数据库驱动的时候。

  • Example
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
package com.tyan.test;

public class Test {

static {
System.out.println("Run static initialization block.");
}

{
System.out.println("Run nonstatic initialization block.");
}
}


package com.tyan.test;

public class ClassTest {

/**
* @param args
*/
public static void main(String[] args) {
try {
Class t = Class.forName("com.tyan.test.Test");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
  • Result
1
Run static initialization block
3、getClass()

getClass()方法的方法是在通过的类的实例调用的,即已经创建了类的实例。

  • Example
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Test {

static {
System.out.println("Run static initialization block.");
}

{
System.out.println("Run nonstatic initialization block.");
}
}

public class ClassTest {

public static void main(String[] args) {
Test t = new Test();
Class test = t.getClass();
}
}
  • Result
1
2
Run static initialization block.
Run nonstatic initialization block.

Class类的常用方法

  • getName()

一个Class对象描述了一个特定类的属性,Class类中最常用的方法getName以String的形式返回此Class对象所表示的实体(类、接口、数组类、基本类型或void名称。

  • newInstance()

Class还有一个有用的方法可以为类创建一个实例,这个方法叫做newInstance()。例如:x.getClass.newInstance(),创建了一个同 x一样类型的新实例。newInstance()方法调用默认构造器(无参数构造器)初始化新建对象。

  • getClassLoader()

返回该类的类加载器。

  • getComponentType()

返回表示数组组件类型的Class。

  • getSuperclass()

返回表示此 Class 所表示的实体(类、接口、基本类型或 void)的超类的Class。

  • isArray()

判定此 Class 对象是否表示一个数组类。

2、判断是否为某个类的实例

一般地,我们用 instanceof 关键字来判断是否为某个类的实例。同时我们也可以借助反射中 Class 对象的 isInstance() 方法来判断是否为某个类的实例,它是一个 native 方法:

1
public native boolean isInstance(Object obj);

3、创建实例

通过反射来生成对象主要有两种方式。

  • 使用Class对象的newInstance()方法来创建Class对象对应类的实例。
1
Class<?> c = String.class;Object str = c.newInstance();
  • 先通过Class对象获取指定的Constructor对象,再调用Constructor对象的newInstance()方法来创建实例。这种方法可以用指定的构造器构造类的实例。
1
2
3
4
5
6
7
//获取String所对应的Class对象
Class<?> c = String.class;
//获取String类带一个String参数的构造器
Constructor constructor = c.getConstructor(String.class);
//根据构造器创建实例
Object obj = constructor.newInstance("23333");
System.out.println(obj);

4、获取方法

获取某个Class对象的方法集合,主要有以下几个方法:

  • getDeclaredMethods 方法返回类或接口声明的所有方法,包括公共、保护、默认(包)访问和私有方法,但不包括继承的方法。
1
public Method[] getDeclaredMethods() throws SecurityException
  • getMethods 方法返回某个类的所有公用(public)方法,包括其继承类的公用方法。
1
public Method[] getMethods() throws SecurityException
  • getMethod 方法返回一个特定的方法,其中第一个参数为方法名称,后面的参数为方法的参数对应Class的对象。
1
public Method getMethod(String name, Class<?>... parameterTypes)

只是这样描述的话可能难以理解,我们用例子来理解这三个方法:

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
32
33
package org.ScZyhSoft.common;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class test1 {
public static void test() throws IllegalAccessException,InstantiationException, NoSuchMethodException, InvocationTargetException
{
Class<?> c = methodClass.class;
Object object = c.newInstance();
Method[] methods = c.getMethods();
Method[] declaredMethods = c.getDeclaredMethods();
//获取methodClass类的add方法
Method method = c.getMethod("add", int.class, int.class); //getMethods()方法获取的所有方法
System.out.println("getMethods获取的方法:");
for(Method m:methods)
System.out.println(m);
//getDeclaredMethods()方法获取的所有方法
System.out.println("getDeclaredMethods获取的方法:");
for(Method m:declaredMethods)
System.out.println(m);
}
}
class methodClass
{
public final int fuck = 3;
public int add(int a,int b)
{
return a+b;
}
public int sub(int a,int b)
{
return a+b;
}
}

程序运行的结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
getMethods获取的方法:
public int org.ScZyhSoft.common.methodClass.add(int,int)
public int org.ScZyhSoft.common.methodClass.sub(int,int)
public final void java.lang.Object.wait() throws java.lang.InterruptedException
public final void java.lang.Object.wait(long,int) throws java.lang.InterruptedException
public final native void java.lang.Object.wait(long) throws java.lang.InterruptedException
public boolean java.lang.Object.equals(java.lang.Object)
public java.lang.String java.lang.Object.toString()
public native int java.lang.Object.hashCode()
public final native java.lang.Class java.lang.Object.getClass()
public final native void java.lang.Object.notify()
public final native void java.lang.Object.notifyAll()
getDeclaredMethods获取的方法:
public int org.ScZyhSoft.common.methodClass.add(int,int)
public int org.ScZyhSoft.common.methodClass.sub(int,int)

可以看到,通过 getMethods() 获取的方法可以获取到父类的方法,比如 java.lang.Object 下定义的各个方法。

5、获取构造器信息

获取类构造器的用法与上述获取方法的用法类似。主要是通过Class类的getConstructor方法得到Constructor类的一个实例,而Constructor类有一个newInstance方法可以创建一个对象实例:

1
public T newInstance(Object ... initargs)

此方法可以根据传入的参数来调用对应的Constructor创建对象实例。

6、获取类的成员变量(字段)信息

主要是这几个方法,在此不再赘述:

  • getFiled:访问公有的成员变量
  • getDeclaredField:所有已声明的成员变量,但不能得到其父类的成员变量

getFiledsgetDeclaredFields 方法用法同上(参照 Method)。

7、调用方法

当我们从类中获取了一个方法后,我们就可以用 invoke() 方法来调用这个方法。invoke 方法的原型为:

1
2
public Object invoke(Object obj, Object... args) 
throws IllegalAccessException, IllegalArgumentException, InvocationTargetException

下面是一个实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class test1 {   
public static void main(String[] args) throws IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException
{ Class<?> klass = methodClass.class;
//创建methodClass的实例
Object obj = klass.newInstance();
//获取methodClass类的add方法
Method method = klass.getMethod("add",int.class,int.class);
//调用method对应的方法 => add(1,4)
Object result = method.invoke(obj,1,4);
System.out.println(result);
}
}
class methodClass {
public final int fuck = 3;
public int add(int a,int b)
{
return a+b;
}
public int sub(int a,int b) {
return a+b;
}
}

8、利用反射创建数组

数组在Java里是比较特殊的一种类型,它可以赋值给一个Object Reference。下面我们看一看利用反射创建数组的例子:

1
2
3
4
5
6
7
8
9
10
11
12
public static void testArray() throws ClassNotFoundException {  
Class<?> cls = Class.forName("java.lang.String");
Object array = Array.newInstance(cls,25);
//往数组里添加内容
Array.set(array,0,"hello");
Array.set(array,1,"Java");
Array.set(array,2,"fuck");
Array.set(array,3,"Scala");
Array.set(array,4,"Clojure");
//获取某一项的内容
System.out.println(Array.get(array,3));
}

其中的Array类为java.lang.reflect.Array类。我们通过Array.newInstance()创建数组对象,它的原型是:

1
2
3
4
public static Object newInstance(Class<?> componentType, int length)        throws NegativeArraySizeException
{
return newArray(componentType, length);
}

newArray 方法是一个 native 方法,它在 HotSpot JVM 里的具体实现我们后边再研究,这里先把源码贴出来:

1
private static native Object newArray(Class<?> componentType, int length)        throws NegativeArraySizeException;

源码目录:openjdk\hotspot\src\share\vm\runtime\reflection.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
arrayOop Reflection::reflect_new_array(oop element_mirror, jint length, TRAPS)
{
if (element_mirror == NULL)
{
THROW_0(vmSymbols::java_lang_NullPointerException());
}
if (length < 0)
{
THROW_0(vmSymbols::java_lang_NegativeArraySizeException());
}
if (java_lang_Class::is_primitive(element_mirror))
{
Klass* tak = basic_type_mirror_to_arrayklass(element_mirror, CHECK_NULL); return TypeArrayKlass::cast(tak)->allocate(length, THREAD);
}
else
{
Klass* k = java_lang_Class::as_Klass(element_mirror);
if (k->oop_is_array() && ArrayKlass::cast(k)->dimension() >= MAX_DIM)
{
THROW_0(vmSymbols::java_lang_IllegalArgumentException());
}
return oopFactory::new_objArray(k, length, THREAD);
}
}

另外,Array 类的 setget 方法都为 native 方法,在 HotSpot JVM 里分别对应 Reflection::array_setReflection::array_get 方法,这里就不详细解析了。

反射的应用场景

JDBC 的数据库的连接

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class ConnectionJDBC {  

/**
* @param args
*/
//驱动程序就是之前在classpath中配置的JDBC的驱动程序的JAR 包中
public static final String DBDRIVER = "com.mysql.jdbc.Driver";
//连接地址是由各个数据库生产商单独提供的,所以需要单独记住
public static final String DBURL = "jdbc:mysql://localhost:3306/test";
//连接数据库的用户名
public static final String DBUSER = "root";
//连接数据库的密码
public static final String DBPASS = "";


public static void main(String[] args) throws Exception {
Connection con = null; //表示数据库的连接对象
Class.forName(DBDRIVER); //1、使用CLASS 类加载驱动程序 ,反射机制的体现
con = DriverManager.getConnection(DBURL,DBUSER,DBPASS); //2、连接数据库
System.out.println(con);
con.close(); // 3、关闭数据库
}

spring框架的使用

Spring 通过 XML 配置模式装载 Bean 的过程:

  1. 将程序内所有 XML 或 Properties 配置文件加载入内存中
  2. Java类里面解析xml或properties里面的内容,得到对应实体类的字节码字符串以及相关的属性信息
  3. 使用反射机制,根据这个字符串获得某个类的Class实例
  4. 动态配置实例的属性

Spring这样做的好处是:

  • 不用每一次都要在代码里面去new或者做其他的事情
  • 以后要改的话直接改配置文件,代码维护起来就很方便了
  • 有时为了适应某些需求,Java类里面不一定能直接调用另外的方法,可以通过反射机制来实现

模拟 Spring 加载 XML 配置文件:

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
public class BeanFactory {
private Map<String, Object> beanMap = new HashMap<String, Object>();
/**
* bean工厂的初始化.
* @param xml xml配置文件
*/
public void init(String xml) {
try {
//读取指定的配置文件
SAXReader reader = new SAXReader();
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
//从class目录下获取指定的xml文件
InputStream ins = classLoader.getResourceAsStream(xml);
Document doc = reader.read(ins);
Element root = doc.getRootElement();
Element foo;

//遍历bean
for (Iterator i = root.elementIterator("bean"); i.hasNext();) {
foo = (Element) i.next();
//获取bean的属性id和class
Attribute id = foo.attribute("id");
Attribute cls = foo.attribute("class");

//利用Java反射机制,通过class的名称获取Class对象
Class bean = Class.forName(cls.getText());

//获取对应class的信息
java.beans.BeanInfo info = java.beans.Introspecto

反射的一些注意事项

由于反射会额外消耗一定的系统资源,因此如果不需要动态地创建一个对象,那么就不需要用反射。

另外,反射调用方法时可以忽略权限检查,因此可能会破坏封装性而导致安全问题。

1、java的一些特点

1、简单:Java源自于C++,但做了很多的简化。例如,取消了多重继承、指针、自动地内存分配和垃回收。

2、面向对象:C语言是过程化的语句,把程序看做是对数据进行加工变换。Java是面向对象的语言,用对象来模拟现实世界。对象中包含了数据和操作数据的方法。

3、分布式:Java程序可以在多台计算机上协同计算。可以基于Java RMI,Java RPC编写分布式应用程序

4、解释性:Java的源程序被编译成字节码,在Java虚拟机上运行。Write once, run anywhere,因此效率不如C++。

5、健壮性:Java取消了指针,对数组下标越界进行检查,具有运行时的异常处理功能。

6、安全性:从网络上下载的Applet程序,在Java的安全机制保护下,不会破坏本地系统。

7、与体系结构无关:由于Java是解释性的,可以在任何操作系统上运行。(Write once, run anywhere)

8、可移植性:Java程序无需重新编译就可以在不同的平台上运行。在java语言中没有针对平台的特征,例如整数在不同平台上是长度相同的。(Write once, run anywhere)【java的源代码通过javac编译成字节码.class文件,使用java解释执行机器码(通过JVM)】。由于字节码不面向任何具体平台,只面向JVM,所以具有高移植性。

9、高性能:基于Java的分布式计算环境能够应付高并发的服务请求。Java程序已经可以和C++程序媲美。

10、多线程:Java支持多线程编程,在同一时间执行多个任务。

这里的引用调用和c++中的引用调用完全一样。

1.1 jvm、jdk、jre

1.1.1 JVM

Java 虚拟机(JVM)是运行 Java 字节码的虚拟机。JVM 有针对不同系统的特定实现(Windows,Linux,macOS),目的是使用相同的字节码,它们都会给出相同的结果。

什么是字节码?采用字节码的好处是什么?

在 Java 中,JVM 可以理解的代码就叫做字节码(即扩展名为 .class 的文件),它不面向任何特定的处理器,只面向虚拟机。Java 语言通过字节码的方式,在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点。所以 Java 程序运行时比较高效,而且,由于字节码并不针对一种特定的机器,因此,Java 程序无须重新编译便可在多种不同操作系统的计算机上运行。

Java 程序从源代码到运行一般有下面 3 步:

Java程序运行过程

我们需要格外注意的是 .class->机器码 这一步。在这一步 JVM 类加载器首先加载字节码文件,然后通过解释器逐行解释执行,这种方式的执行速度会相对比较慢。而且,有些方法和代码块是经常需要被调用的(也就是所谓的热点代码),所以后面引进了 JIT 编译器,而 JIT 属于运行时编译。当 JIT 编译器完成第一次编译后,其会将字节码对应的机器码保存下来,下次可以直接使用。而我们知道,机器码的运行效率肯定是高于 Java 解释器的。这也解释了我们为什么经常会说 Java 是编译与解释共存的语言。

HotSpot 采用了惰性评估(Lazy Evaluation)的做法,根据二八定律,消耗大部分系统资源的只有那一小部分的代码(热点代码),而这也就是 JIT 所需要编译的部分。JVM 会根据代码每次被执行的情况收集信息并相应地做出一些优化,因此执行的次数越多,它的速度就越快。JDK 9 引入了一种新的编译模式 AOT(Ahead of Time Compilation),它是直接将字节码编译成机器码,这样就避免了 JIT 预热等各方面的开销。JDK 支持分层编译和 AOT 协作使用。但是 ,AOT 编译器的编译质量是肯定比不上 JIT 编译器的。

总结:

Java 虚拟机(JVM)是运行 Java 字节码的虚拟机。JVM 有针对不同系统的特定实现(Windows,Linux,macOS),目的是使用相同的字节码,它们都会给出相同的结果。字节码和不同系统的 JVM 实现是 Java 语言“一次编译,随处可以运行”的关键所在。

1.1.2 JDK 和 JRE

JDK 是 Java Development Kit 缩写,它是功能齐全的 Java SDK。它拥有 JRE 所拥有的一切,还有编译器(javac)和工具(如 javadoc 和 jdb)。它能够创建和编译程序。

JRE 是 Java 运行时环境。它是运行已编译 Java 程序所需的所有内容的集合,包括 Java 虚拟机(JVM),Java 类库,java 命令和其他的一些基础构件。但是,它不能用于创建新程序。

如果你只是为了运行一下 Java 程序的话,那么你只需要安装 JRE 就可以了。如果你需要进行一些 Java 编程方面的工作,那么你就需要安装 JDK 了。但是,这不是绝对的。有时,即使您不打算在计算机上进行任何 Java 开发,仍然需要安装 JDK。例如,如果要使用 JSP 部署 Web 应用程序,那么从技术上讲,您只是在应用程序服务器中运行 Java 程序。那你为什么需要 JDK 呢?因为应用程序服务器会将 JSP 转换为 Java servlet,并且需要使用 JDK 来编译 servlet。

1.2 java和C++的区别

  • 都是面向对象的语言,都支持封装、继承和多态
  • Java 不提供指针来直接访问内存,程序内存更加安全
  • Java 的类是单继承的,C++ 支持多重继承;虽然 Java 的类不可以多继承,但是接口可以多继承。
  • Java 有自动内存管理垃圾回收机制(GC),不需要程序员手动释放无用内存
  • 在 C 语言中,字符串或字符数组最后都会有一个额外的字符'\0'来表示结束。但是,Java 语言中没有结束符这一概念。 这是一个值得深度思考的问题,具体原因推荐看这篇文章: https://blog.csdn.net/sszgg2006/article/details/49148189

1.3 为什么说 Java 语言“编译与解释并存”?

高级编程语言按照程序的执行方式分为编译型和解释型两种。

  • 编译型语言是指编译器针对特定的操作系统将源代码一次性翻译成可被该平台执行的机器码;
  • 解释型语言是指解释器对源程序逐行解释成特定平台的机器码并立即执行。

Java 语言既具有编译型语言的特征,也具有解释型语言的特征,因为 Java 程序要经过先编译,后解释两个步骤,由 Java 编写的程序需要先经过编译步骤,生成字节码(*.class 文件),这种字节码必须由 Java 解释器来解释执行。因此,我们可以认为 Java 语言编译与解释并存。

2、java语法

2.1 基本数据类型

boolean 只有两个值:true、false,可以使用 1 bit 来存储,但是具体大小没有明确规定。JVM 会在编译时期将 boolean 类型的数据转换为 int,使用 1 来表示 true,0 表示 false。JVM 支持 boolean 数组,但是是通过读写 byte 数组来实现的。

2.1.1 包装类型

基本类型都有对应的包装类型,基本类型与其对应的包装类型之间的赋值使用自动装箱与拆箱完成。

1
2
Integer x = 2;     // 装箱 调用了 Integer.valueOf(2)
int y = x; // 拆箱 调用了 X.intValue()

2.1.2 缓存池

Java 基本类型的包装类的大部分都实现了常量池技术,即 Byte,Short,Integer,Long,Character,Boolean;前面 4 种包装类默认创建了数值[-128,127] 的相应类型的缓存数据,Character 创建了数值在[0,127]范围的缓存数据,Boolean 直接返回 True Or False。如果超出对应范围仍然会去创建新的对象。

new Integer(123) 与 Integer.valueOf(123) 的区别在于:

  • new Integer(123) 每次都会新建一个对象;
  • Integer.valueOf(123) 会使用缓存池中的对象,多次调用会取得同一个对象的引用。
1
2
3
4
5
6
Integer x = new Integer(123);
Integer y = new Integer(123);
System.out.println(x == y); // false
Integer z = Integer.valueOf(123);
Integer k = Integer.valueOf(123);
System.out.println(z == k); // true

valueOf() 方法的实现比较简单,就是先判断值是否在缓存池中,如果在的话就直接返回缓存池的内容。

1
2
3
4
5
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}

编译器会在自动装箱过程调用 valueOf() 方法,因此多个值相同且值在缓存池范围内的 Integer 实例使用自动装箱来创建,那么就会引用相同的对象。

1
2
3
Integer m = 123;//Java 在编译的时候会直接将代码封装成 Integer m=Integer.valueOf(123);,从而使用常量池中的对象。
Integer n = 123;
System.out.println(m == n); // true

在使用这些基本类型对应的包装类型时,如果该数值范围在缓冲池范围内,就可以直接使用缓冲池中的对象。

两种浮点数类型的包装类 Float,Double 并没有实现常量池技术。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
  Integer i1 = 40;
Integer i2 = 40;
Integer i3 = 0;
Integer i4 = new Integer(40);
Integer i5 = new Integer(40);
Integer i6 = new Integer(0);

System.out.println("i1=i2 " + (i1 == i2));
System.out.println("i1=i2+i3 " + (i1 == i2 + i3));
System.out.println("i1=i4 " + (i1 == i4));
System.out.println("i4=i5 " + (i4 == i5));
System.out.println("i4=i5+i6 " + (i4 == i5 + i6));
System.out.println("40=i5+i6 " + (40 == i5 + i6));
/*
结果:
i1=i2 true
i1=i2+i3 true
i1=i4 false
i4=i5 false
i4=i5+i6 true
40=i5+i6 true
解释:
语句 i4 == i5 + i6,因为+这个操作符不适用于 Integer 对象,首先 i5 和 i6 进行自动拆箱操作,进行数值相加,即 i4 == 40。然后 Integer 对象无法与数值进行直接比较,所以 i4 自动拆箱转为 int 值 40,最终这条语句转为 40 == 40 进行数值比较。
*/

StackOverflow : Differences between new Integer(123), Integer.valueOf(123) and just 123

2.1.3 switch

从 Java 7 开始,可以在 switch 条件判断语句中使用 String 对象。

switch 不支持 long、float、double,是因为 switch 的设计初衷是对那些只有少数几个值的类型进行等值判断,如果值过于复杂,那么还是用 if 比较合适。

2.2 函数

2.2.1 值传递

  • 按值调用(call by value)表示方法接收的是调用者提供的值.

  • 按引用调用(call by reference)表示方法接收的是调用者提供的变量地址。

一个方法可以修改传递引用所对应的变量值,而不能修改传递值调用所对应的变量值。

Java 程序设计语言总是采用按值调用。

  • 对基本数据类型的调用是按值调用。和C/C++中按值调用一样
  • 对对象的引用————本质上是将对象的地址以值的方式传递到形参中。

2.2.2 方法的重载、重写(覆盖)、隐藏

重写:前提是继承,子类中定义的方法与父类中的方法具有相同的方法名字、相同的参数列表、相同的返回类型(允许子类中方法的返回值是父类中方法返回值的子类),即相同的方法签名和返回类型,至于方法修饰符,需要范围相同或者比父类的范围大即可;

  • 父类私有实例方法不能被子类覆盖
  • 静态方法不能被覆盖,只能被隐藏
  • 覆盖特性:一旦父类中的实例方法被子类覆盖,同时用父类型的引用变量引用了子类对象,这时不能通过这个父类型引用变量去访问被覆盖的父类方法(即这时被覆盖的父类方法不可再被发现)。因为实例方法具有多态性(晚期绑定)【强制转换都不行】

重载:同一个类中的多个方法具有相同的名字,但这些方法具有不同的参数列表或者有不同的返回类型(不管是实例方法还是静态方法)。

重写与重载之间的区别

区别点 重载方法 重写方法
参数列表 必须修改 一定不能修改
返回类型 可以修改 一定不能修改
异常 可以修改 可以减少或删除,一定不能抛出新的或者更广的异常
访问 可以修改 一定不能做更严格的限制(可以降低限制)

重写是基类与派生类之间多态性的一种表现,重载可以理解成多态的具体表现形式。【方法重载是一个类的多态性表现,而方法重写是派生类与基类的一种多态性表现。】

方法隐藏:发生在父类和子类之间,前提是继承。子类中定义的方法与父类中的方法具有相同的方法名字、相同的参数列表、相同的返回类型(也允许子类中方法的返回类型是父类中方法返回类型的子类)

方法隐藏:静态方法。如果子类中存在静态方法staticA的方法签名与父类中静态方法staticB的相同,则称staticA隐藏了staticB。

隐藏特性:指父类的变量(实例变量、静态变量)和静态方法在子类被重新定义,但由于类的变量和静态方法没有多态性,因此通过父类型引用变量访问的一定是父类变量、静态方法(即被隐藏的可再发现)。【可以通过强制类型转换】

2.2.2.1 继承和覆盖的关系

1.构造函数:
当子类继承一个父类时,构造子类时需要调用父类的构造函数,存在三种情况
(1)父类无构造函数或者一个无参数构造函数,子类若无构造函数或者有无参数构造函数,子类构造函数中不需要显式调用父类的构造函数,系统会自动在调用子类构造函数前调用父类的构造函数
(2)父类只有有参数构造函数,子类在构造方法中必须要显示调用父类的构造函数,否则编译出错
(3)父类既有无参数构造函数,也有有参构造函数,子类可以不在构造方法中调用父类的构造函数,这时使用的是父类的无参数构造函数

2.方法覆盖:
(1)子类覆盖父类的方法,必须有同样的参数返回类型,否则编译不能通过
(2)子类覆盖父类的方法,在jdk1.5后,参数返回类可以是父类方法返回类的子类
(3)子类覆盖父类方法,可以修改方法作用域修饰符,但只能把方法的作用域放大,而不能把public修改为private
(4)子类方法能够访问父类的protected作用域成员,不能够访问默认的作用域成员
(5)子类的静态方法不能隐藏同名的父类实例方法
(6)java与C++一样,继承的方法具有多态性

3.成员覆盖:
(1)当子类覆盖父类的成员变量时,父类方法使用的是父类的成员变量,子类方法使用的是子类的成员变量
这个听起来很容易理解的一回事,但是实际使用过程中很多人容易搞混:尤其是在多态的时候,调用一个被继承的方法,该方法访问是一个被覆盖的成员m,那么方法中到底是访问了父类的成员还是子类的成员m?结论是,若实际调用的是父类的方法,就使用了父类的该成员m,若实际调用的是子类的方法,就使用子类的成员m,记住一句,每个类使用成员都相当于在前面加了 一个this指针。

2.2.3 深拷贝和浅拷贝

  • 深拷贝:就是对基本数据类型进行值传递,对引用数据类型,创建一个新的对象,并复制其内容,此为深拷贝。

  • 浅拷贝:就是对基本数据类型进行值传递,对引用数据类型进行引用传递般的拷贝,此为浅拷贝。

2.2.3.1 深拷贝

参考文章

实现Cloneable接口,然后覆写Object类中的clone方法。

Object的clone()方法是浅拷贝的。

在java语言中,使用new操作创建一个对象与使用clone方法复制一个对象有什么不同?

使用new操作创建对象本意是分配内存。程序只领到new操作符时,首先去看new操作符后面的类型,因为知道了类型,才能知道要分配多大的内存空间。分配完内存之后,再调用构造函数,填充对象的各个域,这就叫对象的初始化。对象初始化完毕后,可以把引用发布到外部,在外部就可以使用这个引用操纵这个对象。

clone在第一步和new相似的,都是分配内存的,调用clone方法时,分配的内存和源对象(即调用clone方法的对象)相同,然后在使用原对象中对应的各个域,填充新对象的域,填充完成之后,clone方法返回,一个新的相同对象就能被创建,同样这个新对象的引用发布到外部。因为clone没有调用构造函数,所以其对象的域的引用地址还是没有变的,也就是浅拷贝。

如果想要深拷贝一个对象, 这个对象必须要实现Cloneable接口,实现clone方法,并且在clone方法内部,把该对象引用的其他对象也要clone一份 , 这就要求这个被引用的对象必须也要实现Cloneable接口并且实现clone方法。

【有一个问题就是:要是想彻底实现深拷贝,需要把引用链上的每一级对象都显式的拷贝,很麻烦】

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
/**
* 深拷贝和浅拷贝的测试
*/
//测试类1
class Person implements Cloneable{
String name;
int age;
Person(String name,int age){
this.name=name;
this.age=age;
}
@Override
public Object clone() {
try{
return super.clone();
}catch(CloneNotSupportedException e){
return null;
}
}
}
//测试类2

class Animal implements Cloneable{
Person host;//主人
int age;//年纪
Animal(Person person,int age){
this.host=person;
this.age=age;
}
@Override
public Object clone(){
try{
Animal animal=(Animal) super.clone();
animal.host=(Person)host.clone();//深拷贝处理
return animal;
}catch (CloneNotSupportedException e){
return null;
}
}
}

//测试
public class Main{
public static void main(String[] args) {
Person person1=new Person("cxh",26);
Person person2=(Person)person1.clone();
System.out.println("----------------浅拷贝--------------");
//测试Object的clone方法为浅拷贝
//String类用==测试内存地址是否一致
System.out.println("person1和person2的name内存地址是否相同:"+(person1.name==person2.name));



System.out.println("----------------深拷贝--------------");
//重写Object的clone方法,实现深拷贝
//还是用==查看两个对象的内存地址是否相等来确定是否为两个对象,如果是两个内存地址,那么就是深拷贝
Animal animal1=new Animal(new Person("cxh",26),3);
Animal animal2=(Animal) animal1.clone();
System.out.println("animal1和animal2的host内存地址是否相同:"+(animal1.host==animal2.host));
}
}
输出:
----------------浅拷贝--------------
person1和person2的name内存地址是否相同:true
----------------深拷贝--------------
animal1和animal2的host内存地址是否相同:false

Process finished with exit code 0

2.2.3.2 Serializable接口

通过序列化方式实现深拷贝:先将要拷贝对象写入到内存中的字节流中,然后再从这个字节流中读出刚刚存储的信息,作为一个新对象返回,那么这个新对象和原对象就不存在任何地址上的共享,自然实现了深拷贝。

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
import java.io.*;

/**
* 深拷贝和浅拷贝的测试
* 如何利用序列化来完成对象的拷贝呢?在内存中通过字节流的拷贝是比较容易实现的。把母对象写入到一个字节流中,再从字节流中将其读出来,
* 这样就可以创建一个新的对象了,并且该新对象与母对象之间并不存在引用共享的问题,真正实现对象的深拷贝。
*/
//工具类
class CloneUtil{
public static <T extends Serializable> T clone(T obj){
T cloneObj=null;
try{
//写入字节流
ByteArrayOutputStream baos=new ByteArrayOutputStream();
ObjectOutputStream oos=new ObjectOutputStream(baos);
oos.writeObject(obj);
oos.close();

//分配内存,写入原始对象,生成新对象
ByteArrayInputStream bais=new ByteArrayInputStream(baos.toByteArray());//获取上面的输出字节流
ObjectInputStream ois=new ObjectInputStream(bais);

//返回生成的新对象
cloneObj=(T)ois.readObject();
ois.close();
}catch (Exception e){
e.printStackTrace();
}
return cloneObj;
}
}

//测试类1
class Person implements Serializable{
String name;
int age;
Person(String name,int age){
this.name=name;
this.age=age;
}

}
//测试类2

class Animal implements Serializable{
Person host;//主人
int age;//年纪
Animal(Person person,int age){
this.host=person;
this.age=age;
}
}


//测试
public class Main{
public static void main(String[] args) {
System.out.println("----------------深拷贝--------------");
//重写Object的clone方法,实现深拷贝
//还是用==查看两个对象的内存地址是否相等来确定是否为两个对象,如果是两个内存地址,那么就是深拷贝
Animal animal1=new Animal(new Person("cxh",26),3);
Animal animal2=CloneUtil.clone(animal1);
System.out.println("animal1和animal2的host内存地址是否相同:"+(animal1.host==animal2.host));
}
}
输出:
----------------深拷贝--------------
animal1和animal2的host内存地址是否相同:false

2.2.3.3 深浅拷贝存在的问题

浅拷贝:对象值拷贝,对于拷贝而言,拷贝出来的对象仍然保留原对象的所有引用
问题:牵一发而动全身,只要任意一个拷贝对象(或原有对象)中的引用发生改变,所有对象均会受到影响【因为引用的变量还是同一个变量】
优点:效率高,相对于深拷贝节约空间
深拷贝:深拷贝出来的对象产生了所有引用的新的对象
问题:深拷贝效率低,且浪费空间。
优点:修改任意一个对象,不会对其他对象产生影响

2.3 关键字

2.3.1 final关键字

final修饰类:表示这个类不能被继承。

final修饰方法:父类的final方法是不可以被子类继承重写的。

  • 如果父类中的final方法是public修饰的,子类可以继承到此方法,子类会重写此方法,将会导致编译出错
  • 如果父类中的final方法是private修饰的,子类继承不到此方法,这时子类可以定义一个和父类中的final方法相同的方法,因为这个方法是属于子类重新定义的,所以编译不会出错

final修饰变量:final成员变量表示常量,一但被赋值,值不可以再被改变

  • 对于基本类型,final 使数值不变;
  • 对于引用类型,final 使其在初始化之后不能再指向其他对象,但是对象的内容是可以改变的。

2.3.2 static关键字

static关键字作用:在没有创建对象的情况下来调用方法/变量

static修饰的方法或变量不需要依赖对象进行访问,只要类被加载了就可以

static修饰方法:静态方法;

static修饰变量:静态变量,静态变量可以被所有对象共享。但是不能修饰局部变量

JVM只为静态分配一次内存,在加载类的过程中完成静态变量的内存分配。对于实例变量,每创建一个实例,JVM就为实例变量分配一次内存。放在方法区中(方法区包含所有的class和static变量)【和堆一样,被所有线程共享】

static修饰代码块::只会在类加载的时候执行一次。

final和static关键字

final强调的是不能改变,不能继承和重写

static强调的是只有一个可以直接使用,可以在初始化进行改变。它把某些和具体对象无关的东西剥离出来。

只能访问所属类的静态字段和静态方法,方法中不能有 this 和 super 关键字,因此这两个关键字与具体对象关联。

1
2
3
4
5
6
7
8
9
10
11
public class A {

private static int x;
private int y;

public static void func1(){
int a = x;
// int b = y; // Non-static field 'y' cannot be referenced from a static context
// int b = this.y; // 'A.this' cannot be referenced from a static context
}
}

final static 和static final关键字

static final和final static没什么区别,一般static写在前面。

static修饰的属性强调它们只有一个,final修饰的属性表明是一个常数(创建后不能被修改)。static final修饰的属性表示一旦给值,就不可修改,并且可以通过类名访问。

static final也可以修饰方法,表示该方法不能重写,可以在不new对象的情况下调用

2.4 String

String 被声明为 final,因此它不可被继承。(Integer 等包装类也不能被继承)

不可变的好处:

1. 可以缓存 hash 值

因为 String 的 hash 值经常被使用,例如 String 用做 HashMap 的 key。不可变的特性可以使得 hash 值也不可变,因此只需要进行一次计算。

2. String Pool 的需要

如果一个 String 对象已经被创建过了,那么就会从 String Pool 中取得引用。只有 String 是不可变的,才可能使用 String Pool。


3. 安全性

String 经常作为参数,String 不可变性可以保证参数不可变。例如在作为网络连接参数的情况下如果 String 是可变的,那么在网络连接过程中,String 被改变,改变 String 的那一方以为现在连接的是其它主机,而实际情况却不一定是。

4. 线程安全

String 不可变性天生具备线程安全,可以在多个线程中安全地使用。

2.4.1 String, StringBuffer and StringBuilder

可变性:

  • String 不可变
    • 使用 final 关键字修饰字符数组来保存字符串,private final char value[]
  • StringBuffer 和 StringBuilder 可变
    • 继承自 AbstractStringBuilder类,在AbstractStringBuilder 中也是使用字符数组保存字符串char[]value但是没有用 final关键字修饰

线程安全性:

  • String 不可变,因此是线程安全的
  • StringBuilder 不是线程安全的
  • StringBuffer 是线程安全的,内部使用 synchronized 进行同步

2.4.2 String Pool

字符串常量池(String Pool)保存着所有字符串字面量(literal strings),这些字面量在编译时期就确定。不仅如此,还可以使用 String 的 intern() 方法在运行过程将字符串添加到 String Pool 中。

当一个字符串调用 intern() 方法时,如果 String Pool 中已经存在一个字符串和该字符串值相等(使用 equals() 方法进行确定),那么就会返回 String Pool 中字符串的引用;否则,就会在 String Pool 中添加一个新的字符串,并返回这个新字符串的引用。

下面示例中,s1 和 s2 采用 new String() 的方式新建了两个不同字符串,而 s3 和 s4 是通过 s1.intern() 和 s2.intern() 方法取得同一个字符串引用。intern() 首先把 “aaa” 放到 String Pool 中,然后返回这个字符串引用,因此 s3 和 s4 引用的是同一个字符串。

1
2
3
4
5
6
String s1 = new String("aaa");
String s2 = new String("aaa");
System.out.println(s1 == s2); // false
String s3 = s1.intern();
String s4 = s2.intern();
System.out.println(s3 == s4); // true

如果是采用 “bbb” 这种字面量的形式创建字符串,会自动地将字符串放入 String Pool 中。

1
2
3
String s5 = "bbb";
String s6 = "bbb";
System.out.println(s5 == s6); // true

在 Java 7 之前,String Pool 被放在运行时常量池中,它属于永久代。而在 Java 7,String Pool 被移到堆中。这是因为永久代的空间有限,在大量使用字符串的场景下会导致 OutOfMemoryError 错误。

2.4.3 new String(“abc”)问题

使用这种方式一共会创建两个字符串对象(前提是 String Pool 中还没有 “abc” 字符串对象)。

  • “abc” 属于字符串字面量,因此编译时期会在 String Pool 中创建一个字符串对象,指向这个 “abc” 字符串字面量;
  • 而使用 new 的方式会在堆中创建一个字符串对象。

2.5 Object 通用方法

Object 类是一个特殊的类,是所有类的父类。它主要提供了以下 11 个方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public final native Class<?> getClass()//native方法,用于返回当前运行时对象的Class对象,使用了final关键字修饰,故不允许子类重写。

public native int hashCode() //native方法,用于返回对象的哈希码,主要使用在哈希表中,比如JDK中的HashMap。
public boolean equals(Object obj)//用于比较2个对象的内存地址是否相等,String类对该方法进行了重写用户比较字符串的值是否相等。

protected native Object clone() throws CloneNotSupportedException//naitive方法,用于创建并返回当前对象的一份拷贝。一般情况下,对于任何对象 x,表达式 x.clone() != x 为true,x.clone().getClass() == x.getClass() 为true。Object本身没有实现Cloneable接口,所以不重写clone方法并且进行调用的话会发生CloneNotSupportedException异常。

public String toString()//返回类的名字@实例的哈希码的16进制的字符串。建议Object所有的子类都重写这个方法。

public final native void notify()//native方法,并且不能重写。唤醒一个在此对象监视器上等待的线程(监视器相当于就是锁的概念)。如果有多个线程在等待只会任意唤醒一个。

public final native void notifyAll()//native方法,并且不能重写。跟notify一样,唯一的区别就是会唤醒在此对象监视器上等待的所有线程,而不是一个线程。

public final native void wait(long timeout) throws InterruptedException//native方法,并且不能重写。暂停线程的执行。注意:sleep方法没有释放锁,而wait方法释放了锁 。timeout是等待时间。

public final void wait(long timeout, int nanos) throws InterruptedException//多了nanos参数,这个参数表示额外时间(以毫微秒为单位,范围是 0-999999)。 所以超时的时间还需要加上nanos毫秒。

public final void wait() throws InterruptedException//跟之前的2个wait方法一样,只不过该方法一直等待,没有超时时间这个概念

protected void finalize() throws Throwable { }//实例被垃圾回收器回收的时候触发的操作

2.5.1 equals()和==(重要)

== : 它的作用是判断两个对象的地址是不是相等。即,判断两个对象是不是同一个对象(基本数据类型==比较的是值,引用数据类型==比较的是内存地址)。

equals() : 它的作用也是判断两个对象是否相等。但它一般有两种使用情况:

  • 情况 1:类没有覆盖 equals() 方法。则通过 equals() 比较该类的两个对象时,等价于通过“==”比较这两个对象。
  • 情况 2:类覆盖了 equals() 方法。一般,我们都覆盖 equals() 方法来比较两个对象的内容是否相等;若它们的内容相等,则返回 true (即,认为这两个对象相等)。
  • String 中的 equals 方法是被重写过的,因为 object 的 equals 方法是比较的对象的内存地址,而 String 的 equals 方法比较的是对象的值。
  • 当创建 String 类型的对象时,虚拟机会在常量池中查找有没有已经存在的值和要创建的值相同的对象,如果有就把它赋给当前引用。如果没有就在常量池中重新创建一个 String 对象。

2.5.2 equals()和hashcode(重要)

2.5.3 clone()

clone() 是 Object 的 protected 方法,它不是 public,一个类不显式去重写 clone(),其它类就不能直接去调用该类实例的 clone() 方法。

Cloneable 接口只是规定,如果一个类没有实现 Cloneable 接口又调用了 clone() 方法,就会抛出 CloneNotSupportedException。

2.6 类

2.6.1 成员变量与局部变量的区别有哪些?

  1. 从语法形式上看:成员变量是属于类的,而局部变量是在代码块或方法中定义的变量或是方法的参数;成员变量可以被 public,private,static 等修饰符所修饰,而局部变量不能被访问控制修饰符及 static 所修饰;但是,成员变量和局部变量都能被 final 所修饰。
  2. 从变量在内存中的存储方式来看:如果成员变量是使用static修饰的,那么这个成员变量是属于类的,如果没有使用static修饰,这个成员变量是属于实例的。而对象存在于堆内存,局部变量则存在于栈内存。
  3. 从变量在内存中的生存时间上看:成员变量是对象的一部分,它随着对象的创建而存在,而局部变量随着方法的调用而自动消失。
  4. 成员变量如果没有被赋初值:则会自动以类型的默认值而赋值(一种情况例外:被 final 修饰的成员变量也必须显式地赋值),而局部变量则不会自动赋值。

2.6.2 new运算符

new 运算符,new 创建对象实例(对象实例在堆内存中),对象引用指向对象实例(对象引用存放在栈内存中)。

在java语言中,使用new操作创建一个对象与使用clone方法复制一个对象有什么不同?

使用new操作创建对象本意是分配内存。程序只领到new操作符时,首先去看new操作符后面的类型,因为知道了类型,才能知道要分配多大的内存空间。分配完内存之后,再调用构造函数,填充对象的各个域,这就叫对象的初始化。对象初始化完毕后,可以把引用发布到外部,在外部就可以使用这个引用操纵这个对象。

clone在第一步和new相似的,都是分配内存的,调用clone方法时,分配的内存和源对象(即调用clone方法的对象)相同,然后在使用原对象中对应的各个域,填充新对象的域,填充完成之后,clone方法返回,一个新的相同对象就能被创建,同样这个新对象的引用发布到外部。因为clone没有调用构造函数,所以其对象的域的引用地址还是没有变的,也就是浅拷贝。

2.6.3 对象的相等与指向他们的引用相等,两者有什么不同?

对象的相等,比的是内存中存放的内容是否相等。而引用相等,比较的是他们指向的内存地址是否相等。

2.6.4 内部类

是在一个类的内部定义的类。仅能被其外部类使用,但是又不想暴露的类,我们一般都会定义为内部类。

内部类作用:如果一个类A仅仅被某一个类B使用,且A无需暴露出去,可以把A作为B的内部类实现,内部类也可以避免名字冲突:因为外部类多了一层名字空间的限定。例如类Wrapper1、Wrapper2可以定义同名的内部类A而不会导致冲突

2.6.4.1 内部静态类和内部实例类

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
class Wrapper{
private int x=0;
private static int z = 0;
//内部静态类
static class A{
int y=0;
//可以定义静态成员,
//不能访问外部类的实例成员x,可访问外部类静态成员z
static int q=0;
int g() { return ++q + ++y + ++z; }
}
//内部实例类,不能定义静态成员,
//内部实例类可访问外部类的静态成员如z,实例成员如x
class B{
int y=0;
static int q=0;//错误,不允许
public int g( ) {
x++; y++;z++;
return x+y;
}
public int getX(){return x;}
}
}
public static void main(String[] args){
Wrapper w = new Wrapper(); //w.x = 0;
//创建内部静态类实例,需要new外部类.静态内部类()
Wrapper.A a = new Wrapper.A(); //a.y=0, a.q=0;
Wrapper.A b = new Wrapper.A(); //b.y=0, b.q=0;
a.g();
//a,b的实例成员彼此无关,因此执行完a.g()后,a.y = 1, b.y = 0;
//a,b共享静态成员q,所以a.q=b.q = 1;

//创建内部实例类实例
//不能用new Wrapper.B();必须通过外部类对象去实例化内部类对象
Wrapper.B c = w.new B(); //类型声明还是外部类.内部类
c.y=0;
c.g(); //c.y = 1 ,c.gextX() = 1

//在外部类体外面,不能通过内部类对象访问外部类成员,只能在内部类里面访问,
//编译器在这里只能看到内部类成员
// System.out.println(a.z); //错误
// System.out.println(c.x); //错误
//不能通过c直接访问外部类的x,可通过c.gextX()
System.out.println(c.getX());
}

2.6.4.2 方法内部类

顾名思义,就是定义在外部类的方法中的内部类。

方法内部类只在该方法内可以使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Outer{//定义一个外部类
private static String msg="hello world!";
public void fun(int num) {
class Inner{
public void print() {
System.out.println("msg = "+msg);
System.out.println("num = "+num);
}
}
new Inner().print(); //产生内部类对象并调用方法
}

}
public class TestDemo {
public static void main(String args[]) {
new Outer().fun(100);// 产生外部类对象并调用方法
}
}

由于方法内部类不能在外部类的方法以外的地方使用,因此方法内部类不能使用访问控制符和 static 修饰符。

2.6.4.3 匿名内部类

没有名字的内部类。所以匿名内部类不可能有构造函数(没有类名),不能创建对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class Anonymous
{
public void test (Product p)
{
System.out.println(p.getName()+"--------"+p.getPrice());
}
public static void main(String [ ] args )
{
Anonymous as= new Anonymous ();
as.test(new Product( )//此处实现接口并实现抽象方法
{
public double getPrice( )//实现方法
{
return 8888;
}
public String getName( )//实现方法
{
return "I can do it ";
}

});
}
}

主要是方便偷懒。

2.6.5 枚举类

1.所有枚举类型都是Enum类的子类。例如:public enum color{a,b}

2.这些枚举类型继承了enum类的方法。

3.可以在枚举类型中添加一些构造器、方法和域。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public enum Color {

RED("红色", 1), GREEN("绿色", 2), BLANK("白色", 3), YELLO("黄色", 4);
private String name ;
private int index ;

private Color( String name , int index ){
this.name = name ;
this.index = index ;
}

public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getIndex() {
return index;
}
public void setIndex(intindex) {
this.index = index;
}
}

3、三大特征

3.1 继承

关于继承如下 3 点请记住:

  1. 子类拥有父类对象所有的属性和方法(包括私有属性和私有方法),但是父类中的私有属性和方法子类是无法访问,只是拥有
  2. 子类可以拥有自己属性和方法,即子类可以对父类进行扩展。
  3. 子类可以用自己的方式实现父类的方法。

3.1.1 接口和抽象类

3.1.1.1 抽象类

抽象类的使用原则如下:
(1)抽象方法必须为public或者protected(因为如果为private,则不能被子类继承,子类便无法实现该方法),缺省情况下默认为public;
(2)抽象类不能直接实例化,需要依靠子类采用向上转型的方式处理;
(3)抽象类必须有子类,使用extends继承,一个子类只能继承一个抽象类;
(4)子类(如果不是抽象类)则必须覆写抽象类之中的全部抽象方法(如果子类没有实现父类的抽象方法,则必须将子类也定义为为abstract类。);

abstract 关键字,和哪些关键字不能共存。
final:被final修饰的类不能有子类(不能被继承)。而被abstract修饰的类一定是一个父类(一定要被继承)。
private: 抽象类中的私有的抽象方法,不被子类所知,就无法被复写。
而抽象方法出现的就是需要被复写。
static:如果static可以修饰抽象方法,那么连对象都省了,直接类名调用就可以了。
可是抽象方法运行没意义。

抽象类的成员特点:

  • 成员变量:既可以是变量也可以是常量
  • 构造方法:用于子类访问父类的初始化
  • 成员方法:既可以是抽象的,也可以是非抽象的【只有实例方法才能成为抽象方法】

抽象类的成员方法特性:

  • A:抽象方法,强制要求子类去做的事情
  • B:非抽象方法 子类继承的事情,提高代码复用性

3.1.1.2 接口

接口是公共静态常量和公共抽象实例方法的集合。接口是能力、规范、协议的反映。

接口中的所有数据字段隐含为public static final

接口体中的所有方法隐含为public abstract

接口不是类:

(1)不能定义构造函数;
(2)接口之间可以多继承,类可implements多个接口。
(3)和抽象类一样,不能new一个接口

注意事项:

  • 接口不能实例化,因为接口是比抽象类抽象程度更高的类型
  • 一个类如果实现了某个接口,必须重写该接口中的所有方法
  • 接口中所有方法都公有的抽象方法
  • 接口中的所有字段必须都是公有的静态常量
  • 接口本身也是一种数据类型
  • 接口只是为实现它的类定义了规范,保证实现类方法签名和接口中对应方法一致。
  • 通过接口可以实现多继承
  • 一个接口中最好只定义一个方法,防止接口污染

存在的意义:

1、重要性:在Java语言中, abstract class 和interface 是支持抽象类定义的两种机制。正是由于这两种机制的存在,才赋予了Java强大的 面向对象能力。

2、简单、规范性:如果一个项目比较庞大,那么就需要一个能理清所有业务的架构师来定义一些主要的接口,这些接口不仅告诉开发人员你需要实现那些业务,而且也将命名规范限制住了(防止一些开发人员随便命名导致别的程序员无法看明白)。

3、维护、拓展性:比如你要做一个画板程序,其中里面有一个面板类,主要负责绘画功能,然后你就这样定义了这个类。

4、安全、严密性:接口是实现软件松耦合的重要手段,它描叙了系统对外的所有服务,而不涉及任何具体的实现细节。这样就比较安全、严密一些(一般软件服务商考虑的比较多)。

3.1.1.3 抽象类和接口比较

  • 接口是隐式抽象的,当声明一个接口的时候,不必使用abstract关键字

  • 接口中每一个方法也是隐式抽象的,声明时同样不需要abstract关键字

  • 接口中的方法都是公有的

  • 编译时自动为接口里定义的方法添加public abstract修饰符

  • Java接口里的成员变量只能是public static final共同修饰的,并且必须赋初始值,可以不写public static final,编译的时候会自动添加

1
2
3
4
5
6
7
8
9
public interface Temo{
//编译时自动为接口里定义的成员变量增加public static final修饰符
int INT_A =11;
public final static int INT_B = 11;
//编译时自动为接口里定义的方法添加public abstract修饰符
void sleep();
public abstract void running();
void test();
}

3.1.1.4 接口与抽象类的区别

接口(interface)和抽象类(abstract class)是支持抽象类定义的两种机制。

  • 接口里面不可以实现方法体,抽象类可以实现方法体

接口(interface)和抽象类(abstract class)是支持抽象类定义的两种机制。

接口是公开的,不能有私有的方法或变量,接口中的所有方法都没有方法体,通过关键字interface实现。

抽象类是可以有私有方法或私有变量的,通过把类或者类中的方法声明为abstract来表示一个类是抽象类,被声明为抽象的方法不能包含方法体。子类实现方法必须含有相同的或者更低的访问级别(public->protected->private)。抽象类的子类为父类中所有抽象方法的具体实现,否则也是抽象类。

接口可以被看作是抽象类的变体,接口中所有的方法都是抽象的,可以通过接口来间接的实现多重继承。接口中的成员变量都是static final类型,由于抽象类可以包含部分方法的实现,所以,在一些场合下抽象类比接口更有优势。

  • 接口可以多继承接口,抽象类不可以

    如果接口声明中提供了extends子句,那么该接口就继承了父接口的方法和常量。被继承的接口称为声明接口的直接父接口。

    任何实现该接口的类,必须实现该接口继承的其他接口。

  • 接口需要被子类实现,抽象类是被子类继承(单一继承)

  • 接口中只能有公有的方法和属性而且必须赋初始值,抽象类的方法一般是不能用private修饰的【因为子类继承的时候无法覆写,没有意义】

  • 接口中不能存在静态方法,但是属性可以是final,抽象类中方法中可以有静态方法,属性也可以

    不允许被扩展的类被称为final类。类中不想被覆盖的方法也可以使用关键字final

  • 接口被用于常用的功能,便于日后维护和添加删除,而抽象类更倾向于充当公共类的角色,不适用于日后重新对立面的代码修改。功能需要累积时用抽象类,不需要累积时用接口。

3.1.1.5 接口与抽象类的相同点

  • 不能被实例化
  • 接口的实现类或抽象类的子类都只有实现了接口或抽象类中的方法后才能实例化。
接口 抽象类
多重继承 一个接口可以继承多个接口 一个类只能继承(extends)一个抽象类
方法 接口不能提供任何代码 抽象类的非抽象函数可以提供完整代码
数据字段 只包含public static final常量,常量必须在声明时初始化。 可以包含实例变量和静态变量以及实例和静态常量。
含义 接口通常用于描述一个类的外围能力,而不是核心特征。类与接口之间的是-able或者can do的关系,有instanceof关系(实现了接口的具体类对象也是接口类型的实例)。 抽象类定义了它的后代的核心特征。例如Person类包含了Student类的核心特征。子类与抽象类之间是is-a的关系,也有instanceof关系(子类对象也是父类实例)。
简洁性 接口中的常量都被假定为public static final,可以省略。不能调用任何方法修改这些常量的初始值。接口中的方法被假定为public abstract。 可以在抽象类中放置共享代码。可以使用方法来修改实例和静态变量的初始值,但不能修改实例和静态常量的初始值。必须用abstract显式声明方法为抽象方法。
添加功能 如果为接口添加一个新的方法,则必须查找所有实现该接口的类,并为他们逐一提供该方法的实现,即使新方法没有被调用。 如果为抽象类提供一个新方法,可以选择提供一个缺省的实现,那么所有已存在的代码不需要修改就可以继续工作,因为新方法没有被调用。

3.1.2 this和super

1、引用构造函数

super(参数):调用父类中的某一个构造函数(应该为构造函数中的第一条语句)。

为什么在实例化子类的对象时,要调用父类的构造器?

子类在继承父类后,获取到父类的属性和方法,这些属性和方法必须先初始化再使用,所以需要先调用父类的构造器。

this(参数):调用本类中另一种形式的构造函数(应该为构造函数中的第一条语句)。

super和this的异同:

  • super(参数):调用基类中的某一个构造函数(应该为构造函数中的第一条语句)
  • this(参数):调用本类中另一种形成的构造函数(应该为构造函数中的第一条语句)
  • super: 它引用当前对象的直接父类中的成员(用来访问直接父类中被隐藏的父类中成员数据或函数,基类与派生类中有相同成员定义时如:super.变量名 super.成员函数据名(实参)
  • this:它代表当前对象名(在程序中易产生二义性之处,应使用this来指明当前对象;如果函数的形参与类中的成员数据同名,这时需用this来指明成员变量名)
  • 调用super()必须写在子类构造方法的第一行,否则编译不通过。每个子类构造方法的第一条语句,都是隐含地调用super(),如果父类没有这种形式的构造函数,那么在编译的时候就会报错。
  • super()和this()类似,区别是,==super()从子类中调用父类的构造方法,this()在同一类内调用其它方法。==
  • super()和this()均需放在构造方法内第一行。
  • 尽管可以用this调用一个构造器,但却不能调用两个。
  • this和super不能同时出现在一个构造函数里面,因为this必然会调用其它的构造函数,其它的构造函数必然也会有super语句的存在,所以在同一个构造函数里面有相同的语句,就失去了语句的意义,编译器也不会通过。
  • this()和super()都指的是对象,所以,均不可以在static环境中使用。包括:static变量,static方法,static语句块。
  • 从本质上讲,this是一个指向本对象的指针, 然而super是一个Java关键字。

3.2 多态

多态一般分为两种:重写式多态和重载式多态。重写和重载这两个知识点前面的文章已经详细将结果了,这里就不多说了。

  • 重载式多态,也叫编译时多态。也就是说这种多态再编译时已经确定好了。重载大家都知道,方法名相同而参数列表不同的一组方法就是重载。在调用这种重载的方法时,通过传入不同的参数最后得到不同的结果。【感觉这就不叫是多态】

  • 重写式多态,也叫运行时多态。这种多态通过动态绑定(dynamic binding)技术来实现,是指在执行期间判断所引用对象的实际类型,根据其实际的类型调用其相应的方法。也就是说,只有程序运行起来,你才知道调用的是哪个子类的方法。
    这种多态通过函数的重写以及向上转型来实现,我们上面代码中的例子就是一个完整的重写式多态。我们接下来讲的所有多态都是重写式多态,因为它才是面向对象编程中真正的多态。

其实多态就是父类可以使用其子类的方法(当然,该方法是子类覆写父类的方法)。这就是所谓的动态调用

多态就是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定,而是在程序运行期间才确定,即一个引用变量倒底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在由程序运行期间才能决定。因为在程序运行时才确定具体的类,这样,不用修改源程序代码,就可以让引用变量绑定到各种不同的类实现上,从而导致该引用调用的具体方法随之改变,即不修改程序代码就可以改变程序运行时所绑定的具体代码,让程序可以选择多个运行状态,这就是多态性。

3.2.1 向上转型

子类引用的对象转换为父类类型称为向上转型。通俗地说就是是将子类对象转为父类对象。此处父类对象可以是接口。

转型过程中需要注意的问题

  • 向上转型时,子类单独定义的方法会丢失。比如上面Dog类中定义的run方法,当animal引用指向Dog类实例时是访问不到run方法的,animal.run()会报错。

  • 子类引用不能指向父类对象。Cat c = (Cat)new Animal()这样是不行的。

向上转型的好处

  • 减少重复代码,使代码变得简洁。

  • 提高系统扩展性。

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
32
33
34
35
36
37
38
39
40
41
42
public class Animal {
public void eat(){
System.out.println("animal eatting...");
}
}

public class Cat extends Animal{

public void eat(){

System.out.println("我吃鱼");
}
}

public class Dog extends Animal{

public void eat(){

System.out.println("我吃骨头");
}

public void run(){
System.out.println("我会跑");
}
}

public class Main {

public static void main(String[] args) {

Animal animal = new Cat(); //向上转型
animal.eat();

animal = new Dog();
animal.eat();
}

}

//结果:
//我吃鱼
//我吃骨头
1
Animal animal = new Cat(); //向上转型

成员变量

编译看左边(基类),运行看左边(基类);无论如何都是访问基类的成员变量。

成员方法

编译看左边(基类),运行看右边(派生类),动态绑定。

Static方法

编译看左边(基类),运行看左边(基类)。

只有非静态的成员方法,编译看左边,运行看右边。

这样,我们也可以得出多态的局限:

不能使用派生类特有的成员属性和派生类特有的成员方法。

3.2.2 向下转型

  • 向下转型的前提是父类对象指向的是子类对象(也就是说,在向下转型之前,它得先向上转型)
  • 向下转型只能转型为本类对象(猫是不能变成狗的)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public void eat(Animal a){
if(a instanceof Dog){
Dog d = (Dog)a;
d.eat();
d.run();//狗有一个跑的方法
}
if(a instanceof Cat){
Cat c = (Cat)a;
c.eat();
System.out.println("我也想跑,但是不会"); //猫会抱怨
}
a.eat();//其他动物只会吃
}

eat(new Cat());
eat(new Cat());
eat(new Dog());
//.....

3.2.3 动态绑定和静态绑定

o当调用实例方法时,由Java虚拟机动态地决定所调用的方法,称为动态绑定(dynamic binding)或者晚期绑定或者延迟绑定(lazy binding)或者多态。

假定对象o是类C1的实例,C1是C2的子类,C2是C3的子类,…,Cn-1是Cn的子类。也就是说,Cn是最一般的类,C1是最具体的类。在Java中,Cn是Object类。如果调用继承链里子类型C1对象o的方法p,Java虚拟机按照C1、C2、…、Cn的顺序依次查找方法p的实现。一旦找到一个实现,将停止查找,并执行找到的第一个实现(覆盖的实例函数)。

静态绑定是在程序执行前就已经被绑定了(也就是在程序编译过程中就已经知道这个方法是哪个类中的方法)。

java当中的方法只有final、static、private修饰的方法和构造方法是静态绑定的。

private修饰的方法:private修饰的方法是不能被继承的,因此子类无法访问父类中private修饰的方法。所以只能通过父类对象来调用该方法体。因此可以说private方法和定义这个方法的类绑定在了一起。

final修饰的方法:可以被子类继承,但是不能被子类重写(覆盖),所以在子类中调用的实际是父类中定义的final方法。(使用final修饰方法的两个好处:(1)防止方法被覆盖;(2)关闭java中的动态绑定)。

static修饰的方法:可以被子类继承,但是不能被子类重写(覆盖),但是可以被子类隐藏。(这里意思是说如果父类里有一个static方法,它的子类里如果没有对应的方法,那么当子类对象调用这个方法时就会使用父类中的方法,而如果子类中定义了相同的方法,则会调用子类中定义的方法,唯一的不同就是:当子类对象向上类型转换为父类对象时,不论子类中有没有定义这个静态方法,该对象都会使用父类中的静态方法,因此这里说静态方法可以被隐藏而不能被覆盖。这与子类隐藏父类中的成员变量是一样的。隐藏和覆盖的区别在于,子类对象转换成父类对象后,能够访问父类被隐藏的变量和方法,而不能访问父类被覆盖的方法)。

构造方法:构造方法也是不能被继承的(因为子类是通过super方法调用父类的构造函数,或者是jvm自动调用父类的默认构造方法),因此编译时也可以知道这个构造方法方法到底是属于哪个类的。

因此,一个方法被继承,或者是被继承后不能被覆盖,那么这个方法就采用静态绑定

java中重载的方法使用静态绑定,重写的方法使用动态绑定。

3.2.4 多态的一道例子

参考文章

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
class A {
public String show(D obj) {
return ("A and D");
}

public String show(A obj) {
return ("A and A");
}

}

class B extends A{
public String show(B obj){
return ("B and B");
}

public String show(A obj){
return ("B and A");
}
}

class C extends B{

}

class D extends B{

}

public class Demo {
public static void main(String[] args) {
A a1 = new A();
A a2 = new B();
B b = new B();
C c = new C();
D d = new D();

System.out.println("1--" + a1.show(b));
System.out.println("2--" + a1.show(c));
System.out.println("3--" + a1.show(d));
System.out.println("4--" + a2.show(b));
System.out.println("5--" + a2.show(c));
System.out.println("6--" + a2.show(d));
System.out.println("7--" + b.show(b));
System.out.println("8--" + b.show(c));
System.out.println("9--" + b.show(d));
}
}
//结果:
//1--A and A
//解释:THIS(A).SHOW(B)->SUPER(A).SHOW(B)->THIS(A).SHOW(SUPER(B))->THIS(A).SHOW(A)->"A and A"

//2--A and A
//解释:THIS(A).SHOW(C)->>SUPER(A).SHOW(C)->THIS(A).SHOW(SUPER(C))->THIS(A).SHOW(B)->THIS(A).SHOW(SUPER(B))->THIS(A).SHOW(A)->"A and A"

//3--A and D

//4--B and A
/*解释:首先,a2是类型为A的引用类型,它指向类型为B的对象。A确定可调用的方法:show(D obj)和show(A obj)。
a2.show(b) ==> this.show(b),这里this指的是B。
然后.在B类中找show(B obj),找到了,可惜没用,因为show(B obj)方法不在可调用范围内【向上转型中子类的特有方法会失效】,this.show(O)失败,进入下一级别:super.show(O),super指的是A。
在A 中寻找show(B obj),失败,因为没用定义这个方法。进入第三级别:this.show((super)O),this指的是B。
在B中找show((A)O),找到了:show(A obj),选择调用该方法。
输出:B and A
*/

//5--B and A
//6--A and D
//7--B and B
//8--B and B
//9--A and D

继承链中对象方法的调用的优先级:this.show(O)、super.show(O)、this.show((super)O)、super.show((super)O)

  1. 先方法,后对象
  2. this.show(O)、super.show(O)、this.show((super)O)、super.show((super)O)

4、java其他知识

4.1 lambda表达式

编译器会把lambda表达式看待成是匿名内部类对象。

Lambda 允许把函数作为一个方法的参数(函数作为参数传递进方法中,函数式编程思想)。

1
2
3
4
5
6
7
8
9
10
11
12
13
//匿名内部类
btEnlarge.setOnAction(
new EventHandler<ActionEvent>()
{
@Override
public void handle(ActionEvnt e)
{//code}

});
//lambda表达式
btEnlarge.setOnAction(e->{
//code
});

java里规定Lambda表达式只能赋值给函数式接口。

lambda 表达式的语法格式如下:

(parameters) -> expression 或 (parameters) ->{ statements; }

【这其中:1.parameters是参数列表,和方法中的参数列表是一个意思。
2.expression是指lambda主体,也就是具体操作
3.这里面有一些需要注意的,如果你使用花括号,那么expression后面必须夹分号,如果要求有返回,那还必须加return(在花括号中)】

4.2 初始化模块

初始化块是Java类中可以出现的第四种成员(前三种包括属性、方法、构造函数),分为实例初始化块和静态初始化块。

4.2.1 实例初始化模块

实例初始化模块(instance initialization block,IIB)是一个用大括号括住的语句块,直接嵌套于类体中,不在方法内。

一个类可以有多个初始化模块,模块按照在类中出现的顺序执行

作用:

  • 简单的来说,就是初始化对象,实例初始化块优先于构造函数执行

  • 如果多个构造方法共享一段代码,并且每个构造方法不会调用其他构造方法,那么可以把这段公共代码放在初始化模块中。(感觉用处不大)

  • 初始化模块可以简化构造方法

  • 实例初始化模块可以截获异常

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    public class A{
    //在实例初始化块里初始化数据成员可以截获异常
    private InputStream fs = null;
    {
    try{ fs = new FileInputStream(new File(“C:\\1.txt”));}
    catch(Exception e){ …}
    }
    public A(){ … }

    }
  • 实例初始化模块最重要的作用是当我们需要写一个内部匿名类时:匿名类不可能有构造函数,这时可以用实例初始化块来初始化数据成员

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    interface ISay{ public abstract void sayHello(); }
    public class InstanceInitializationBlockTest {
    public static void main(String[] args){
    ISay say = new ISay()
    {
    //这里定义了一个实现了ISay接口的匿名类
    //final类型变量一般情况下必须马上初始化,一种例外是:final实例变量可以在构造函数里再初始化。
    //但是匿名类又不可能有构造函数,因此只能利用实例初始化块
    private final int j; //为了演示实例初始化块的作用,这里特意没有初始化常量j
    {
    j = 0; //在实例初始化块里初始化j
    }
    @Override
    public void sayHello()//内部匿名类
    {
    System.out.println("Hello");
    }
    };
    say.sayHello();
    }
    }

4.2.2 静态初始化模块

静态初始化模块是由static修饰的初始化模块{},只能访问类的静态成员,并且在JVM的Class Loader将类装入内存时调用。(类的装入和类的实例化是两个不同步骤,首先是将类装入内存,然后再实例化类的对象)。

在类体里直接定义静态变量相当于静态初始化块。

1
2
3
4
5
6
7
8
9
10
public class A{
//类的属性和方法定义
{
//实例初始化模块
}
static {
//静态初始化模块
}
public static int i = 0;//直接定义静态变量相当于静态初始化块
}

一个类可以有多个静态初始化块,类被加载时,这些模块按照在类中出现的顺序执行

初始化模块执行顺序

第一次使用类时装入类

​ 如果父类没装入则首先装入父类,这是个递归的过程,直到继承链上所有祖先类全部装入
​ 装入一个类时,类的静态数据成员和静态初始化模块按它们在类中出现的顺序执行

实例化类的对象

​ 首先构造父类对象,这是个递归过程,直到继承链上所有祖先类的对象构造好
​ 构造一个类的对象时,按在类中出现的顺序执行实例数据成员的初始化及实例初始化模块
​ 执行构造函数函数体

4.3 异常处理

以下三种类型的异常:

1、检查性异常:最具代表的检查性异常是用户错误或问题引起的异常,这是程序员无法预见的。例如要打开一个不存在文件时,一个异常就发生了,这些异常在编译时不能被简单地忽略。

2、运行时异常: 运行时异常是可能被程序员避免的异常。与检查性异常相反,运行时异常可以在编译时被忽略。

3、错误: 错误不是异常,而是脱离程序员控制的问题。错误在代码中通常被忽略。例如,当栈溢出时,一个错误就发生了,它们在编译也检查不到的。

4.3.1 异常的体系结构

在Java API中已经定义了许多异常类,这些异常类分为两大类,错误Error和异常Exception

可以看出所有异常类型都是内置类Throwable的子类,因而Throwable在异常类的层次结构的顶层。

4.3.2 Error和Exception

error:表示不希望被程序捕获或者是程序无法处理的错误。

Exception:表示用户程序可能捕捉的异常情况或者说是程序可以处理的异常。

Exception又分为运行时异常(RuntimeException)和非运行时异常。划分的依据是由程序错误导致的异常是RuntimeException;(除了RuntimeException及其子类以外,其他的Exception类及其子类都属于检查异常,当程序中可能出现这类异常,要么使用try-catch语句进行捕获,要么用throws子句抛出,否则编译无法通过。也可以说RuntimeException异常一定是自己的问题)程序本身没有问题的其他异常是非运行时异常。

不受检查异常(Error类和RuntimeException类)为编译器不要求强制处理的异常,检查异常则是编译器要求必须处置的异常。】

下面将详细讲述这些异常之间的区别与联系:

Error:Error类对象由 Java 虚拟机生成并抛出,大多数错误与代码编写者所执行的操作无关。例如,Java虚拟机运行错误(Virtual MachineError),当JVM不再有继续执行操作所需的内存资源时,将出现 OutOfMemoryError。这些异常发生时,Java虚拟机(JVM)一般会选择线程终止;还有发生在虚拟机试图执行应用时,如类定义错误(NoClassDefFoundError)、链接错误(LinkageError)。这些错误是不可查的,因为它们在应用程序的控制和处理能力之 外,而且绝大多数是程序运行时不允许出现的状况。对于设计合理的应用程序来说,即使确实发生了错误,本质上也不应该试图去处理它所引起的异常状况。在Java中,错误通常是使用Error的子类描述。

Exception:在Exception分支中有一个重要的子类RuntimeException(运行时异常),该类型的异常自动为你所编写的程序定义ArrayIndexOutOfBoundsException(数组下标越界)、NullPointerException(空指针异常)、ArithmeticException(算术异常)、MissingResourceException(丢失资源)、ClassNotFoundException(找不到类)等异常,这些异常是不检查异常,程序中可以选择捕获处理,也可以不处理。这些异常一般是由程序逻辑错误引起的,程序应该从逻辑角度尽可能避免这类异常的发生;而RuntimeException之外的异常我们统称为非运行时异常,类型上属于Exception类及其子类,从程序语法角度讲是必须进行处理的异常,如果不处理,程序就不能编译通过。如IOException、SQLException等以及用户自定义的Exception异常,一般情况下不自定义检查异常。

ErrorException的区别:Error通常是灾难性的致命的错误,是程序无法控制和处理的,当出现这些异常时,Java虚拟机(JVM)一般会选择终止线程;Exception通常情况下是可以被程序处理的,并且在程序中应该尽可能的去处理这些异常。

4.3.3 抛出异常和捕获异常

抛出异常:要理解抛出异常,首先要明白什么是异常情形(exception condition),它是指阻止当前方法或作用域继续执行的问题。其次把异常情形和普通问题相区分,普通问题是指在当前环境下能得到足够的信息,总能处理这个错误。对于异常情形,已经无法继续下去了,因为在当前环境下无法获得必要的信息来解决问题,你所能做的就是从当前环境中跳出,并把问题提交给上一级环境,这就是抛出异常时所发生的事情。抛出异常后,会有几件事随之发生。首先,是像创建普通的java对象一样将使用new在堆上创建一个异常对象;然后,当前的执行路径(已经无法继续下去了)被终止,并且从当前环境中弹出对异常对象的引用。此时,异常处理机制接管程序,并开始寻找一个恰当的地方继续执行程序,这个恰当的地方就是异常处理程序或者异常处理器,它的任务是将程序从错误状态中恢复,以使程序要么换一种方式运行,要么继续运行下去。

捕获异常:在方法抛出异常之后,运行时系统将转为寻找合适的异常处理器(exception handler)潜在的异常处理器是异常发生时依次存留在调用栈中的方法的集合。当异常处理器所能处理的异常类型与方法抛出的异常类型相符时,即为合适的异常处理器。运行时系统从发生异常的方法开始,依次回查调用栈中的方法,直至找到含有合适异常处理器的方法并执行。当运行时系统遍历调用栈而未找到合适的异常处理器,则运行时系统终止。同时,意味着Java程序的终止。

对于运行时异常、错误和检查异常,Java技术所要求的异常处理方式有所不同。

由于运行时异常及其子类的不可查性,为了更合理、更容易地实现应用程序,Java规定,运行时异常将由Java运行时系统自动抛出,允许应用程序忽略运行时异常

对于方法运行中可能出现的Error,当运行方法不欲捕捉时,Java允许该方法不做任何抛出声明。因为,大多数Error异常属于永远不能被允许发生的状况,也属于合理的应用程序不该捕捉的异常。

对于所有的检查异常,Java规定:一个方法必须捕捉,或者声明抛出方法之外。也就是说,当一个方法选择不捕捉检查异常时,它必须声明将抛出异常。

4.3.4 异常关键字

  • try – 用于监听。将要被监听的代码(可能抛出异常的代码)放在try语句块之内,当try语句块内发生异常时,异常就被抛出。

  • catch – 用于捕获异常。catch用来捕获try语句块中发生的异常。

  • finally – finally语句块总是会被执行。它主要用于回收在try块里打开的物力资源(如数据库连接、网络连接和磁盘文件)。只有finally块,执行完成之后,才会回来执行try或者catch块中的return或者throw语句,如果finally中使用了return或者throw等终止方法的语句,则就不会跳回执行,直接停止。

    如果抛出异常,即使没有catch子句匹配,finally也会执行。一个方法将从一个try/catch块返回到调用程序的任何时候,经过一个未捕获的异常或者是一个明确的返回语句,finally子句在方法返回之前仍将执行。这在关闭文件句柄和释放任何在方法开始时被分配的其他资源是很有用。

    finally子句是可选项,可以有也可以无,但是每个try语句至少需要一个catch或者finally子句。

    如果finally块与一个try联合使用,finally块将在try结束之前执行

  • throw – 用于抛出异常。

  • throws – 用在方法签名中,用于声明该方法可能抛出的异常。

4.3.5 异常声明、抛出和捕获

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class ThrowDeclaration1 {
//由于m1内部处理了所有异常,因此不用加throws声明
public void m1(){
try{
//执行可能抛出异常的语句
}
catch(Throwable e){ //由于Throwable是所有异常的父类,因此这里可以捕获所有异常
//处理异常
}
}

public void m2(){
m1(); //由于m1没有异常声明,因此m1的调用者不需要try/catch
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class ThrowDeclaration2 {
//m1内部可能抛出的异常没有处理,因此必须加throws声明
//throws声明就是告诉方法的调用者,调用本方法可能抛出什么异常
public void m1() throws IOException {

//执行可能抛出异常IOException的语句,但没有try/catch

}

public void m2(){
//由于m1有异常声明,因此m2调用m1时有第一个选择:1 用try/catch捕获和处理异常
//这时m2就不用加throws异常声明
try {
m1();
} catch (IOException e) {
e.printStackTrace();
}
}
}

4.3.6 异常捕获顺序

  • 每个catch根据自己的参数类型捕获相应的类型匹配的异常。
  • 由于父类引用参数可接受子类对象,因此,若把Throwable作为第1个catch子句的参数,它将捕获任何类型的异常,导致后续catch没有捕获机会。
  • 通常将继承链最底层的异常类型作为第1个catch子句参数,次底层异常类型作为第2个catch子句参数,以此类推。越在前面的catch子句其异常参数类型应该越具体。以便所有catch都有机会捕捉相应异常。
  • 无论何时,throw以后的语句都不会执行。
  • 无论同层catch子句是否捕获、处理本层的异常(即使在catch块里抛出或转发异常),同层的finally总是都会执行。
  • 一个catch捕获到异常后,同层其他catch都不会执行,然后执行同层finally。

4.4 泛型程序设计

java的泛型通过擦除法实现。编译时会用类型实参代替类型形参进行严格的语法检查,然后擦除类型参数、生成所有实例类型共享的唯一原始类型

泛型的一个优点就是在编译时而不是运行时检测出错误。运用泛型,指定集合中的对象类型,你可以在编译时发现类型不匹配的错误,并且取数据时不需要手动强转类型。

编程的时候,能在编译时发现并修改错误最好,等上线运行时报错才解决,则属于生产事故,且找到bug的位置需要花费更多的时间和精力。

所以泛型的优点有如下几点:

  • 简单来说,泛型可以帮助我们在编译的时候就检查出错误【使得程序更加安全】
  • 泛型可以省去类型强制转换。【加入泛型后,编译器会自动进行强制转换】

泛型的本质是参数化类型

1
2
3
public class Paly<T>{
T play(){}
}

泛型将所操作的数据类型作为参数。其中T就是作为一个类型参数在Play被实例化的时候所传递来的参数

1
Play<Integer> playInteger=new Play<>();//这里T就会被实例化为Integer

4.4.1 泛型类

泛型类可看作是普通类的工厂。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型
//在实例化泛型类时,必须指定T的具体类型
public class Generic<T>{
//key这个成员变量的类型为T,T的类型由外部指定
private T key;

public Generic(T key) { //泛型构造方法形参key的类型也为T,T的类型由外部指定
this.key = key;
}

public T getKey(){ //泛型方法getKey的返回值类型为T,T的类型由外部指定
return key;
}
}

具体实现:

1
2
3
4
5
6
7
8
9
10
11
12
//泛型的类型参数只能是类类型(包括自定义类),不能是简单类型
//传入的实参类型需与泛型的类型参数类型相同,即为Integer.
Generic<Integer> genericInteger = new Generic<Integer>(123456);

//传入的实参类型需与泛型的类型参数类型相同,即为String.
Generic<String> genericString = new Generic<String>("key_vlaue");
Log.d("泛型测试","key is " + genericInteger.getKey());
Log.d("泛型测试","key is " + genericString.getKey());

//运行结果:
//12-27 09:20:04.432 13063-13063/? D/泛型测试: key is 123456
//12-27 09:20:04.432 13063-13063/? D/泛型测试: key is key_vlaue

注意:

  • 泛型的类型参数只能是类类型,不能是简单类型。
  • 不能对确切的泛型类型使用instanceof操作。如下面的操作是非法的,编译时会出错。

4.4.2 泛型接口

泛型接口常被用在各种类的生产器中。

1
2
3
4
5
6
7
8
9
10
11
/**
* 未传入泛型实参时,与泛型类的定义相同,在声明类的时候,需将泛型的声明也一起加到类中
* 即:class FruitGenerator<T> implements Generator<T>{
* 如果不声明泛型,如:class FruitGenerator implements Generator<T>,编译器会报错:"Unknown class"
*/
class FruitGenerator<T> implements Generator<T>{
@Override
public T next() {
return null;
}
}

4.4.3 泛型通配符

类型通配符一般是使用?代替具体的类型实参。此处’?’是类型实参,而不是类型形参。此处的?和Number、String、Integer一样都是一种实际的类型,可以把?看成所有类型的父类。是一种真实的类型。

可以解决当具体类型不确定的时候,这个通配符就是 ? ;当操作类型时,不需要使用类型的具体功能时,只使用Object类中的功能。那么可以用 ? 通配符来表未知类型。

4.4.4 泛型方法

要注意泛型方法和普通的实例方法的区别:泛型方法的返回类型前面一定是一个泛型!!!与之对应的是函数的形参是一个泛型定义的。

在实际调用泛型方法的时候,调用泛型方法,将实际类型放于<>之中方法名之前;也可以不显式指定实际类型,而直接给实参调用,如
print(integers); print(strings);由编译器自动发现实际类型

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
public class GenericTest {
//这个类是个泛型类,在上面已经介绍过
public class Generic<T>{
private T key;

public Generic(T key) {
this.key = key;
}

//我想说的其实是这个,虽然在方法中使用了泛型,但是这并不是一个泛型方法。
//这只是类中一个普通的成员方法,只不过他的返回值是在声明泛型类已经声明过的泛型。
//所以在这个方法中才可以继续使用 T 这个泛型。
public T getKey(){
return key;
}

/**
* 这个方法显然是有问题的,在编译器会给我们提示这样的错误信息"cannot reslove symbol E"
* 因为在类的声明中并未声明泛型E,所以在使用E做形参和返回值类型时,编译器会无法识别。
public E setKey(E key){
this.key = keu
}
*/
}

/**
* 这才是一个真正的泛型方法。
* 首先在public与返回值之间的<T>必不可少,这表明这是一个泛型方法,并且声明了一个泛型T
* 这个T可以出现在这个泛型方法的任意位置.
* 泛型的数量也可以为任意多个
* 如:public <T,K> K showKeyName(Generic<T> container){
* ...
* }
*/
public <T> T showKeyName(Generic<T> container){
System.out.println("container key :" + container.getKey());
//当然这个例子举的不太合适,只是为了说明泛型方法的特性。
T test = container.getKey();
return test;
}

//这也不是一个泛型方法,这就是一个普通的方法,只是使用了Generic<Number>这个泛型类做形参而已。
public void showKeyValue1(Generic<Number> obj){
Log.d("泛型测试","key value is " + obj.getKey());
}

//这也不是一个泛型方法,这也是一个普通的方法,只不过使用了泛型通配符?
//同时这也印证了泛型通配符章节所描述的,?是一种类型实参,可以看做为Number等所有类的父类
public void showKeyValue2(Generic<?> obj){
Log.d("泛型测试","key value is " + obj.getKey());
}

/**
* 这个方法是有问题的,编译器会为我们提示错误信息:"UnKnown class 'E' "
* 虽然我们声明了<T>,也表明了这是一个可以处理泛型的类型的泛型方法。
* 但是只声明了泛型类型T,并未声明泛型类型E,因此编译器并不知道该如何处理E这个类型。
public <T> T showKeyName(Generic<E> container){
...
}
*/

/**
* 这个方法也是有问题的,编译器会为我们提示错误信息:"UnKnown class 'T' "
* 对于编译器来说T这个类型并未项目中声明过,因此编译也不知道该如何编译这个类。
* 所以这也不是一个正确的泛型方法声明。
public void showkey(T genericObj){

}
*/

public static void main(String[] args) {


}
}

4.4.5 静态方法和泛型

静态方法无法访问类上定义的泛型;如果静态方法操作的引用数据类型不确定的时候,必须要将泛型定义在方法上。

如果静态方法要使用泛型的话,必须将静态方法也定义成泛型方法 。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class StaticGenerator<T> {
....
....
/**
* 如果在类中定义使用泛型的静态方法,需要添加额外的泛型声明(将这个方法定义成泛型方法)
* 即使静态方法要使用泛型类中已经声明过的泛型也不可以。
* 如:public static void show(T t){..},此时编译器会提示错误信息:
"StaticGenerator cannot be refrenced from static context"
*/
public static <T> void show(T t){

}
}

4.4.6 泛型变量的限定

定义泛型变量的上界:public class NumberGeneric< T extends Number>

泛型变量上界的说明:上述方式的声明规定了NumberGeneric类所能处理的类型变量其类型和Number有继承关系;

extends关键字所声明的上界既可以是一个类,也可以是一个接口;当泛型变量这样声明时,在实例化一个泛型类时,需要明确类型必须为指定上界类型或者子类。

表示T应该是绑定类型的子类型(subtype)。T和绑定类型可以是类,也可以是接口。选择关键字extends的原因是更接近子类的概念。一个类型变量或通配符可以有多个限定,限定类型用“&”分割。例如:T extends Comparable & Serializable

但是至多一个类【如果用一个类作为限定,它必须是限定列表中的第一个】


定义泛型变量的下界:List<? super CashCard> cards = newArrayList();

泛型变量下界的说明:通过使用super关键字可以固定泛型参数的类型为某种类型或者其超类。当程序希望为一个方法的参数限定类型时,通常可以使用下限通配符。

​ public static void sort(T[] a, Comparator<? super T> c){ … }

4.4.7 擦除机制

参考文章

Java中的泛型,只在编译阶段有效。在编译过程中,正确检验泛型结果后,会将泛型的相关信息擦出,并且在对象进入和离开方法的边界处添加类型检查和类型转换的方法。也就是说,成功编译过后的class文件中是不包含任何泛型信息的。

虚拟机没有泛型类型对象——所有对象都属于普通类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Test {

public static void main(String[] args) {

ArrayList<String> list1 = new ArrayList<String>();
list1.add("abc");

ArrayList<Integer> list2 = new ArrayList<Integer>();
list2.add(123);

System.out.println(list1.getClass() == list2.getClass());
}

}
//运行结果:true

类型擦除:无论何时定义一个泛型类型,都自动提供了一个相应的原始类型。原始类型的名字就是删去类型参数后的泛型类型名。

擦除类型变量,并替换为限定类型(无限定的变量用Object)

  • 当编译泛型类、接口和方法时,会用Object代替非受限类型参数E。
  • 如果一个泛型的参数类型是受限的,编译器会用该受限类型来替换它。

值得注意的是泛型变量类型的检查是在编译之前进行的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Test {  

public static void main(String[] args) {

ArrayList<String> list1 = new ArrayList();
list1.add("1"); //编译通过
list1.add(1); //编译错误 ,因为list1使用了泛型
String str1 = list1.get(0); //返回类型就是String

ArrayList list2 = new ArrayList<String>();
list2.add("1"); //编译通过
list2.add(1); //编译通过 ,list2没有使用泛型
Object object = list2.get(0); //返回类型就是Object

new ArrayList<String>().add("11"); //编译通过
new ArrayList<String>().add(22); //编译错误

String str2 = new ArrayList<String>().get(0); //返回类型就是String
}

}

4.4.8 反射和泛型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//非泛化的Class引用(即不带类型参数的Class引用)可指向任何类型的Class对象,但这样不安全
Class clz ; //注意警告, Class is a raw type. References to generic type Class<T> should be parameterized
clz= Manager.class; //OK
clz = int.class; //OK

//有时我们需要限定Class引用能指向的类型:加上<类型参数>。这样可以可以强制编译器进行额外的类型检查
Class<Person> genericClz; //泛化Class引用,Class<Person>只能指向Person的类型信息, <Person>为类型参数
genericClz = Person.class; //OK
//genericClz = Manager.class; //Error,不能指向非Person类型信息。注意对于类型参数,编译器检测时不看继承关系。

//能否声明一个可用指向Person及其子类的Class对象的引用?为了放松泛化的限制,用通配符?表示任何类型,并且与extends结合,创建一个范围
Class<? extends Person> clz2; //引用clz2可以指向Person及其子类的类型信息
clz2 = Person.class;
clz2 = Employee.class;
clz2 = Manager.class;
//注意Class<?> 与Class效果一样,但本质不同,一个用了泛型,一个没有用泛型。 Class<?> 等价于Class<? extends Object >

4.4.9 使用泛型的限制

  • 使用泛型类型的限制:不能new泛型数组(数组元素是泛型),但可以声明【new是运行是发生的,因此new 后面一定不能出现类型形参E,运行时类型参数早没了】

    • 不能使用new A[ ]的数组形式,因为E已经被擦除
      ArrayList[ ] list = new ArrayList[10];//错误
  • E已经被擦除,只能用泛型的原始类型初始化数组, 必须改为new ArrayList[10]
    ArrayList [ ] list = new ArrayList[10];

    • 为什么这里不需要强制类型转换:参数化类型与原始类型的兼容性
    • 参数化类型对象可以被赋值为原始类型的对象,原始类型对象也可以被赋值为参数化类型对象
      ArrayList a1 = new ArrayList(); //原始类型
      ArrayList a2 = a1; //参数化类型
  • 异常类不能是泛型的。泛型类不能继承java.lang.Throwable。

    1
    public class MyException<T> extends Exception{}

    因为如果这么做的话,需要为MyException添加一个catch语句

    1
    2
    3
    try{
    }catch(MyException<T> ex){
    }

    但是JVM需要检查这个try语句中抛出来的异常以确定与catch语句中的异常类型匹配。但是,运行时的类型信息是不可获取的

  • 静态上下文中不允许使用泛型的类型参数。由于泛型类的所有实例类型都共享相同的运行时类,所以泛型类的静态变量和方法都被它的所有实例类型所共享。因此,在静态方法、数据域或者初始化语句中,使用泛型的参数类型是非法的。

    1
    2
    3
    public static void m(E o)//错误
    public static E o;//错误
    static {E o}//错误
  • 不能使用new E( );//只能想办法得到E的类型实参的Class信息,再newInstance(…)

基本概念

图灵机

计算机的理论基础是——图灵机。

图灵机在理论上证明了:只要对数据进行处理、存储、传输三种简单的操作就能解决一切可计算的数学问题。

  • 处理:按照有限规则对0,1的集合进行序列变换

图灵机的模型就是决定了计算机追求的就是一个字:快。(在规定的时间内完成对规律的表达)

冯诺依曼结构

结构基础

输出设备、输入设备、存储器、控制器、运算器

以运算器为中心。

在存储器中,指令和数据同样重要。

摩尔定律

物质基础

加快经常性事件

在计算机系统结构中加快的一个方式就是加快经常性事件。

要点:

  • 辨认经常性事件
  • 找出加快方法

CISC(复杂指令系统)

用硬件替代软件,用指令代替子程序。

但是容易使得硬件很冗余。

RISC(精简指令集)

找到频繁使用的代码。精简指令系统(不经常使用的就放弃掉),将多余的资源实现加快。可以把硬件部分增加寄存器的数量、做成多级的流水线等。

高速缓存Cache

利用局部性原理,将局部代码放在cache中,使得访存的速度与处理器的速度匹配。

Amdahl定律

计算机系统结构的总原则
$$
加速比:S_n=\frac{新速度}{旧速度}=\frac{老时间}{新时间}=\frac{T_o}{T_n}\
S_n:系统加速比\
T_o:原执行时间\
T_n:新执行时间\
$$

$$
基本的Amdal定律:S_n=\frac{1}{(1-F_e)+\frac{F_e}{S_e}}\
S_e:被改进部分的部件加速比\
F_e:被改进部分原执行时间占原来总时间的百分比
$$

为使系统能获得较高加速比,可改进部分必须占有较大的比例。

当对一个系统中的某个部件进行改进后,所能获得的整个系统性能的提高,受限于该部件的执行时间占总执行时间的百分比。

CPU性能公式

在设计CPU的时候,部件加速比往往是不能得到的。

一个程序在CPU上运行的时间不包括I/O时间。

CPU时间=执行程序所需的时钟周期x时钟周期时间。

  • 时钟周期时间:系统的时钟周期时间越短,相应的CPU性能越好(纯粹依赖硬件实现)

  • 时钟周期数:固有属性

    • 指令周期数CPI:平均每条指令耗费的时钟周期数
      $$
      CPI=\frac{执行程序所需的时钟周期数}{IC}\
      IC:所执行的指令条数
      $$

处理器性能优化的策略

$CPU时间=IC\times CPI\times 时钟周期时间$

  • 减少指令条数
  • 降低CPI
  • 减少时钟周期时间

局部性原理

  • 时间局部性:如果一个信息正在被访问,那么它很有可能即将被访问
  • 空间局部性:程序即将用到的信息很有可能与目前正在使用的信息在空间上相邻或者临近。

局部性原理最能在存储部分中体现出来。例如:经常性事件放置在cache中。

提高并行性的技术

并行性:同时性+并发性

  • 同时性:两个及以上事件在同一时刻发生
  • 并发性:两个及以上事件在同一时间间隔内发生

从处理数据的角度,并行性等级:

  • 字串位串:每次只对一个字的一位进行处理
  • 字串位并:同时对一个字的全部位进行处理,不同字之间是串行的
  • 字并位串:同时对许多字的同一位进行处理
  • 全并行:同时对许多字的全部位或部分位进行处理

从执行程序的角度,并行性等级:

  • 指令内部并行:单条指令中各微操作之间的并行
  • 指令级并行:并行执行多条指令
  • 线程级并行:并行执行多个线程
  • 任务级或过程级并行:并行执行多个过程、任务(程序段)
  • 作业、程序级并行:并行执行多个作业、程序

提高并行性的方法:

  1. 时间重叠:让多个处理过程轮流重叠的使用用一套硬件设备的各个部分,加快硬件周转
  2. 资源重复:重复设置硬件资源,大幅度地提高计算机系统的性能
  3. 资源共享:软件方法,使得多个任务按一定时间顺序轮流使同一套设备

单机系统下的并行性的发展

时间重叠

在高性能单处理机过程中,起主导作用的是时间重叠原理。

实现时间重叠的基础:部件功能专用化。

把一件工作按功能分割为多个部分,每个部分指定给专门的部件完成。

资源重复

主要运用在多体储存器、多操作部件和阵列处理机。

资源共享

本质是用单处理机模拟多处理机的功能,以形成虚拟机的功能。

计算机系统评价

  • 性能
    评价指标:响应时间(完成一个任务的全部时间)、吞吐率(单位时间完成的任务数)
    $$
    MIPS = \frac{指令条数}{执行时间X10^6}=\frac{f}{CPIX10^6}\
    程序执行时间T_c = \frac{指令条数}{MIPSX10^6}\
    MFLOPS = \frac{程序中的浮点操作数}{执行时间X10^6}
    $$

  • 成本
    售价构成

  • 能耗

  • 可靠性

冯诺依曼结构的发展改进

输入输出方式的改进

1、程序等待:CPU需要轮询I/O设备,造成CPU时间浪费;
程序中断:CPU与I/O设备可并行工作

2、DMA直接存储器访问:减少CPU对I/O的干预

3、专用的I/O处理机:

4、采用并行性技术:

5、存储器组织结构的发展:相联存储器、通用寄存器组、高速缓冲存储器Cache

问答

1、衡量计算机系统设计是否优化的最通用的标准包括性能和价格。