面向对象编程(Object-Oriented Programming,OOP)实践了软件工程的三个主要目标:可维护性、可重用性和可扩展性。
2.1 OOP理念
面向过程的代码结构相对松散,强调如何流程化地解决问题;面向对象的思维更加内聚,强调高内聚、低耦合,先抽象模型,定义共性行为,再解决实际问题。
面向对象有三大特征:封装、继承、多态。本书明确将“抽象”作为面向对象的特性之一,支持面向对象“四大特征”的说法。
封装是在抽象基础上决定信息是否公开,以及公开等级,核心问题是以什么样的方式暴露哪些信息。
抽象是要找到属性和行为的共性,属性是行为的基本生产资料,具有一定的敏感性,不能直接对外暴露;封装的主要任务是对属性、数据、部分内容敏感行为实现隐藏。
迪米特法则就是对封装的具体要求,即A模块要使用B模块的某个接口行为,对B模块中除此行为外的其他信息知道得尽可能少。
继承是面向对象技术的基石,允许创建具有逻辑等级结构的类体系,形成一个继承树,让软件在业务多变的客观条件下,某些基础模块可以被直接复用、间接复用或增强复用,父类的能力通过这种方式赋予子类。
里氏替换原则是指任何父类能够出现的地方,子类都能出现。
继承滥用的危害性,即方法污染和方法爆炸。方法污染是指父类具备的行为,通过继承传递给子类,子类并不具备执行此行为的能力。方法爆炸是指继承树不断扩大,底层类拥有的方法虽然都能够执行,但是由于方法众多,其中部分方法并非与当前类的功能定位相关,很容易在实际编程中产生选择困难症。
提倡组合优先原则来扩展类的能力,即优先采用组合或聚合的类关系来复用其他类的能力,而不是继承。
多态是根据运行时的实际对象类型,同一个方法产生不同的运行结果,使同一个行为具有不同的表现形式。多态是指在编译层面无法确定最终调用的方法体,以覆写为基础来实现面向对象特性,在运行期由JVM进行动态绑定,调用合适的覆写方法来执行。
2.2 初识Java
JDK,Java Development Kit,Java开发工具包。
JIT,Just-in-time编译器。
JRE,Java Runtime Environment,Java运行环境。
JVM,Java Virtual Machine,Java虚拟机。
2.3 类
2.3.1 类的定义
类的访问级别有public和无访问控制符,类型分为class、interface和enum。
Java类主要由两部分组成:成员和方法。
2.3.2 接口与抽象类
抽象类在被继承时体现的是is-a关系,接口在被实现时体现的是can-do关系。
抽象类是模板式设计,而接口是契约式设计。
2.3.3 内部类
- 静态内部类
- 成员内部类
- 局部内部类,定义在方法或者表达式内部
- 匿名内部类
在JDK源码中,定义包内可见静态内部类的方式很常见,这样做的好处是:
- 作用域不会扩散到包外
- 可以通过“外部类.内部类”的方式直接访问
- 内部类可以访问外部类中的所有静态属性和方法
2.3.4 访问控制权限
- public:可以修饰外部类、属性、方法,表示公开的、无限制的,是访问限制最松的一级,被其修饰的类、属性和方法不仅可以被包内访问,还可以跨类、跨包访问,甚至允许跨工程访问
- protected:只能修饰属性和方法,表示受保护的、有限制的,被其修饰的属性和方法能被包内及包外子类访问
- 无:无访问权限控制符仅对包内可见
- private:只能修饰属性、方法、内部类,表示私有的,是访问限制最严格的一级,被其修饰的属性或方法只能在该类内部访问,子类、包内均不能访问,更不允许跨包访问
在定义类时,推荐访问控制级别从严处理:
- 如果不允许外部直接通过new创建对象,构造方法必须是private
- 工具类不允许有public或default构造方法
- 类非static成员变量并且与自类共享,必须是protected
- 类非static成员变量并且仅在本类使用,必须是private
- 类static成员变量如果仅在本类使用,必须是private
- 若是static成员变量,必须考虑是否为final
- 类成员方法只供类内部调用,必须是private
- 类成员方法只对继承类公开,那么限制为protected
2.3.5 this与super
一个实例变量可以通过this.赋值另一个实例变量;一个实例方法可以通过this.调用另一个实例方法;甚至一个构造方法都可以this.调用另一个构造方法。如果this和super指代构造方法,则必须位于方法体的第一行。在一个构造方法中,this和super只能出现一个,且只能出现一次,否则在实例化对象时,会因子类调用到多个父类构造方法而造成混乱。
由于this和super都在实例化阶段调用,所以不能在静态方法和静态代码块中使用this和super关键字。
2.3.6 类关系
关系是指事物之间存在单向或相互的作用力或影响力的状态。
类有关系的情况,包括:
- 【继承】extends(is-a)
- 【实现】implements(can-do)
- 【组合】类是成员变量(contains-a)
- 【聚合】类是成员变量(has-a)
- 【依赖】是除组合和聚合外的单向弱关系(depends-a)
- 【关联】是相互平等的依赖关系(links-a)
2.3.7 序列化
将数据对象转换为二进制的过程称为对象的序列化。将二进制流恢复为数据对象的过程称为反序列化。
常见的序列化方式有三种:
- Java原生序列化
- Java类通过实现Serializable接口来实现该类对象的序列化
- 实现Serializable接口的类一定要显式地定义serialVersionUID属性值。修改类时根据兼容性决定是否修改serialVersionUID值:
- 如果是兼容升级,请不要修改serialVersionUID字段,避免反序列化失败
- 如果不是兼容升级,需要修改serialVersionUID值,避免反序列化混乱
- Hessian序列化:Hessian序列化是一种支持动态类型、跨语言、基于对象传输的网络协议
- 自描述序列类型
- 语言无关,支持脚本语言
- 协议简单,比Java原生序列化高效
- JSON序列化:将数据对象转换为JSON字符串
- 可读性好,方便调试
敏感属性不需要进行序列化传输,可以加transient关键字,避免把此属性信息转化为序列化的二进制流。
2.4 方法
2.4.1 方法签名
方法签名包括方法名称和参数列表,是JVM标识方法的唯一索引。
2.4.2 参数
参数在方法中,属于方法签名的一部分,包括参数类型和参数个数。
参数预处理包括两种:
- 入参保护
- 参数校验
2.4.3 构造方法
构造方法是方法名与类名相同的特殊方法,在新建对象时调用,可以通过不同的构造方法实现不同方法的对象初始化。
- 构造方法名称必须与类名相同
- 构造方法是没有返回类型的,即使是void也不能有
- 构造方法不能被继承,不能被覆写,不能被直接调用
- 类定义时提供默认的无参构造方法
- 构造方法可以私有
在创建对象时,会先执行父类和子类的静态代码块,然后再执行父类和子类的构造方法。
2.4.4 类内方法
1、实例方法
又称为非静态方法。
2、静态方法
- 静态方法中不能使用实例成员变量和实例方法
- 静态方法不能使用super和this关键字,这两个关键字指代的都是需要被创建出来的对象
3、静态代码块
2.4.5 getter与setter
- 满足面向对象语言封装的特性
- 有利于统一控制
易出错的getter和setter方法定义方式:
- getter/setter中添加业务逻辑
- 同时定义isXXX()和getXXX()
- 相同的属性名容易带来歧义
2.4.6 同步与异步
同步调用是刚性调用,是阻塞式操作,必须等待调用方法体执行结束。而异步调用是柔性调用,是非阻塞式操作,在执行过程中,如调用其他方法,自己可以继续执行而不被阻塞等待方法调用完毕。
2.4.7 覆写
向上转型时,通过父类引用执行子类方法时需要注意以下两点:
- 无法调用到子类中存在而父类本身不存在的方法
- 可以调用到子类中覆写了父类的方法,这是一种多态实现
想要成功地覆写父类方法,需要满足以下四个条件:
- 访问权限不能变小
- 返回类型能够向上转型而成为父类的返回类型
- 异常也要能向上转型成为父类的异常
- 方法名、参数类型及个数必须严格一致
2.5 重载
在同一个类中,如果多个方法有相同的方法名称、不同的参数类型、参数个数、参数顺序,即称为重载。
JVM在重载方法中,选择合适的目标方法的顺序如下:
- 精确匹配
- 如果是基本数据类型,自动转换成更大表示范围的基本类型
- 通过自动拆箱与装箱
- 通过子类向上转型继承路线依次匹配
- 通过可变参数匹配
2.6 泛型
泛型的本质是类型参数化,解决不确定具体对象类型的问题。
- 尖括号里的每一个元素都指代一种未知类型
- 尖括号的位置非常讲究,必须在类名之后或方法返回值之前
- 泛型在定义处只具备执行Object方法的能力
- 对于编译之后的字节码指令,其实没有这些花头花脑的方法签名,充分说明了泛型只是一种编写代码时的语法检查
使用泛型的好处包括:
- 类型安全
- 提升可读性
- 代码重用
2.7 数据类型
2.7.1 基本数据类型
Java的9种基本数据类型包括boolean、byte、char、short、int、long、float、double和refvar。
refvar是面向对象世界中的引用变量,也叫引用句柄。
引用分成两种数据类型:引用变量本身和引用指向的对象。本书中把引用对象称为refvar,而把引用指向的实际对象简称为refobj。
对象分为三块存储区域:
- 对象头
- 占用12个字节,存储内容包括对象标记和类元信息
- 实例数据
- 对其填充
2.7.2 包装类型
推荐所有的包装类对象之间值的比较,全部使用equals()方法。
在选择使用包装类还是基础数据类型时,推荐使用如下方式:
- 所有的POJO类属性必须使用包装数据类型
- RPC方法的返回值和参数必须使用包装数据类型
- 所有的局部变量推荐使用基本数据类型