目录
面向对象程序设计概述
▲
使用预定义类
▲
用户自定义类
▲
静态域与静态方法
▲
方法参数
▲
对象构造
▲
包
▲
类路径
▲
文档注释
▲
类设计技巧
4.1 面向对象程序设计概述
面向对象程序设计
(
简称 OOP
)
是当今主流的程序设计范型
,
它已经取代了
20
世纪
70
年代的
“
结构化
”
过程化程序设计开发技术
。
Java
是完全面向对象的
,
必须熟悉
OOP
才能
够编写
Java
程序。在 OOP 中, 不必关心对象的具体实现,只要能够满足用户的需求即可。
对于一些规模较小的问题
,
将其分解为过程的开发方式比较理想。而面向对象更加适用 于解决规模较大的问题
4 . 1.1 类
类( class) 是构造对象的模板或蓝图。。由类构造(construct) 对象的过程称为创建类的实例 (instance ).
正如前面所看到的
,
用
Java
编写的所有代码都位于某个类的内部
标准的 Java
库提供
了几千个类
,
可以用于用户界面设计
、
日期
、
日历和网络程序设计
。
尽管如此
,
还是需要在
Java
程序中创建一些自己的类
,
以便描述应用程序所对应的问题域中的对象
。
封装
(
encapsulation
,
有时称为数据隐藏
)
是与对象有关的一个重要概念
。
从形式上看
,
封装不过是将数据和行为组合在一个包中, 并对对象的使用者隐藏了数据的实现方式。
对象
中的数据称为实例域
(
instance
field
)
,
操纵数据的过程称为方法
(
method
。
)
对于每个特定的
类实例
(
对象
)
都有一组特定的实例域值
。
实现封装的关键在于绝对不能让类中的方法直接地访问其他类的实例域。程序仅通过对 象的方法与对象数据进行交互。封装给对象赋予了“ 黑盒” 特征, 这是提高重用性和可靠性的关键。
OOP
的另一个原则会让用户自定义
Java
类变得轻而易举
,
这就是
:
可以通过扩展一个
类来建立另外一个新的类
。
事实上
,
在
Java
中
,
所有的类都源自于一个
“
神通广大的超类
”
,
它就是 Object。
在扩展一个已有的类时
,
这个扩展后的新类具有所扩展的类的全部属性和方法
。
在新类
中
,
只需提供适用于这个新类的新方法和数据域就可以了
。
通过扩展一个类来建立另外一个
类的过程称为继承
(
inheritance
,
)
4 . 1.2 对 象
要想使用
OOP
,—定要清楚对象的三个主要特性:
•
对象的行为
(
behavior)—可以对对象施加哪些操作,或可以对对象施加哪些方法?
•
对象的状态
(
state
)—当施加那些方法时,对象如何响应?
•
对象标识
(
identity
)—
如何辨别具有相同行为与状态的不同对象?
同一个类的所有对象实例
,
由于支持相同的行为而具有家族式的相似性
。
对象的行为是
用可调用的方法定义的
。
此外
,
每个对象都保存着描述当前特征的信息
。
这就是对象的状态
。
对象的状态可能会
随着时间而发生改变
,
但这种改变不会是自发的
。
对象状态的改变必须通过调用方法实现
(
如果不经过方法调用就可以改变对象状态
,
只能说明封装性遭到了破坏
。
对象的状态并不能完全描述一个对象
。
每个对象都有一个唯一的身份
(
identity
。
)
例
如
,
在一个订单处理系统中
,
任何两个订单都存在着不同之处
’
即使所订购的货物完全相同也是
如此
。
需要注意
作为一个类的实例,
每个对象的标识永远是不同的,状态常常也存在着差异。
4 . 1.3 识 别 类
传统的过程化程序设计
,
必须从顶部的
main
函数开始编写程序
。
在面向对象程序设计
时没有所谓的
“
顶部
”
。
对于学习
OOP
的初学者来说常常会感觉无从下手
。
答案是
:
首先从
设计类开始
,
然后再往每个类中添加方法
。
识别类的简单规则是在分析问题的过程中寻找名词
,
而方法对应着动词
。
例如
,
在订单处理系统中
,
有这样一些名词
:
接下来
,
查看动词
:
商品被添加到订单中
,
订单被发送或取消
,
订单货款被支付
。
对于
每一个动词如
:
“
添加
”
、
“
发送
”
、
“
取消
”
以及
“
支付
”
,
都要标识出主要负责完成相应动作
的对象
。
例如
,
当一个新的商品添加到订单中时
,
那个订单对象就是被指定的对象
,
因为它
知道如何存储商品以及如何对商品进行排序
。
也就是说
,
add
应该是
Order
类的一个方法
,
而
Item
对象是一个参数
。
4.1.4 类之间的关系
在类之间
,
最常见的关系有
•依赖(“ uses
-
a
”
)
•
聚合
(“ has
-
a
”
)
•继承(
“
is
-
a
”
)
依赖
(
dependence
)
,
即
“
uses
-
a
”
关系
,
是一种最明显的
、
最常见的关系
。
例如
,
Order
类使用
Account
类是因为
Order
对象需要访问
Account
对象查看信用状态
。
但是
Item
类不依
赖于
Account
类
,
这是因为
Item
对象与客户账户无关
。
因此
,
如果一个类的方法操纵另一个
类的对象
,
我们就说一个类依赖于另一个类
。
应该尽可能地将相互依赖的类减至最少
。
如果类
A
不知道
B
的存在
,
它就不会关心
B
的任何改变
(
这意味着
B
的改变不会导致
A
产生任何
bug
)
。
用软件工程的术语来说
,
就是
让类之间的耦合度最小
。
聚合
(
aggregation
)
,
即
“
has
-
a
”
关系
,
是一种具体且易于理解的关系
。
例如
,
一个
Order 对象包含一些 Item 对象。聚合关系意味着类 A 的对象包含类 B 的对象
继承
(
inheritance
)
,
即
“
is
-
a
”
关系
,
是一种用于表示特殊与一般关系的
。
例如
,
Rush
Ordei
•
类由
Order
类继承而来
。
在具有特殊性的
RushOrder
类中包含了一些用于优先处理的
特殊方法
,
以及一个计算运费的不同方法
;
而其他的方法
,
如添加商品
、
生成账单等都是从
Order
类继承来的
。
一般而言
,
如果类
A
扩展类
B
,
类
A
不但包含从类
B
继承的方法
,
还会
拥有一些额外的功能
4.2 使用预定义类
在
Java
中
,
没有类就无法做任何事情
,
我们前面曾经接触过几个类
。
然而
,
并不是所有
的类都具有面向对象特征
。
例如
,
Math
类
。
在程序中
,
可以使用
Math
类的方法
,
如
Math
,
random
,
并只需要知道方法名和参数
(
如果有的话
,
)
而不必了解它的具体实现过程
。
这正是
封装的关键所在
,
当然所有类都是这样
。
但遗憾的是
,
Math
类只封装了功能
,
它不需要也不
必隐藏数据
。
由于没有数据
,
因此也不必担心生成对象以及初始化实例域: 下一节将会给出一个更典型的类—
Date
类
,
从中可以看到如何构造对象, 以及如何调 用类的方法。
4 . 2.1 对象与对象变量
要想使用对象
,
就必须首先构造对象
,
并指定其初始状态
。
然后
,
Xt
对象应用方法
在
Java
程序设计语言中
,
使用构造器
(
constructor
) 构造新实例。构造器是一种特殊的方法
,
用来构造并初始化对象
构造器的名字应该与类名相同
。
因此
Date
类的构造器名为
Date
。
要想构造一个
Date
对
象
,
需要在构造器前面加上
new
操作符
,
如下所示:new
Date
(
)
这个表达式构造了一个新对象
。
这个对象被初始化为当前的日期和时间
。
如果需要的话
,
也可以将这个对象传递给一个方法
:
System
.
out
.
printTn
(
new
DateO
)
;
在这两个例子中
,
构造的对象仅使用了一次
。
通常
,
希望构造的对象可以多次使用
,
因
此
,
需要将对象存放在一个变量中
:
Date birthday = new Date();
一定要认识到
:
一个对象变量并没有实际包含一个对象
,
而仅仅引用一个对象
。
在
Java
中
,
任何对象变量的值都是对存储在另外一个地方的一个对象的引用
。
new
操作
符的返回值也是一个引用
。
下列语句
:
Date
deadline
=
new
Date
(
)
;
有两个部分
。
表达式
new
Date
(
)
构造了一个
Date
类型的对象
,
并且它的值是对新创建对象的
引用
。
这个引用存储在变量
deadline
中
。
4.2.2Java 类库中的 LocalDate 类
在前面的例子中
,
已经使用了
Java
标准类库中的
Date
类
。
Date
类的实例有一个状态
,
即特定的时间点
尽管在使用
Date
类时不必知道这一点
,
但时间是用距离一个固定时间点的毫秒数
(
可正
可负
)
表示的
,
这个点就是所谓的纪元
(
epoch
)
,
它 是
UTC
时间
1970
年
1
月
1
日
00
:
00
:
00
4.3 用户自定义类
4.3.9 final 实例域
可以将实例域定义为
final
。
构建对象时必须初始化这样的域
。
也就是说
,
必须确保在每
一个构造器执行之后
,
这个域的值被设置
,
并且在后面的操作中
,
不能够再对它进行修改
。
例如
,
可以将
Employee
类中的
name
域声明为
final
,
因为在对象构建之后
,
这个值不会再
被修改
,
即没有
setName
方法
final
修饰符大都应用于基本
(
primitive
)
类型域
,
或不可变
(
immutable
)
类的域
(
如果类
中的每个方法都不会改变其对象
,
这种类就是不可变的类
。
例如
,
String
类就是一个不可变
的类
)
。
4.4 静态域与静态方法
4 . 4.1 静态域
如果将域定义为
static
,
每个类中只有一个这样的域
。
而每一个对象对于所有的实例域
却都有自己的一份拷贝
。
现在
,
每一个雇员对象都有一个自己的
id
域
,
但这个类的所有实例将共享一个 iiextld
域
。
换句话说
,
如果有
1000
个
Employee
类的对象,
则有
1000
个实例域
id
。
但是
,
只有一
个静态域
nextld
。
即使没有一个雇员对象
,
静态域
nextld
也存在
。
它属于类
,
而不属于任何
独立的对象
。
4.4.2 静态常量
静态变量使用得比较少
,
但静态常量却使用得比较多
。
例如
,
在
Math
类中定义了一个
静态常量:
另一个多次使用的静态常量是
System
.
out
。
它在
System 类中声明:
前面曾经提到过
,
由于每个类对象都可以对公有域进行修改
,
所以
,
最好不要将域设计
为
public
。
然而
,
公有常量
(
即
final
域
)
却没问题
。
因为
out
被声明为
final
,
所以
,
不允许
再将其他打印流陚给它
4.4.3 静态方法
可以使用对象调用静态方法。, 如果 harry 是一个 Employee 对象, 可以用harry.getNextId( ) 代替 Employee.getNextId( 。) 不过,这种方式很容易造成混淆,其原因是 getNextld 方法计算的结果与 harry 毫无关系。我们建议使用类名, 而不是对象来调用静态方法。
4.4.4 工厂方法
静态方法还有另外一种常见的用途
。
类似
LocalDate
和
NumberFormat
的类使用静态工
厂方法
(
factory
methocO
来构造对象
。
你已经见过工厂方法
LocalDate
.
now
和
LocalDate
.
of
。
NumberFormat
类如下使用工厂方法生成不同风格的格式化对象
:
4 . 6 对 象 构 造
4.6.1 重载
有些类有多个构造器
。
例如
,
可以如下构造一个空的
StringBuilder
对象
:
StringBuilder
messages
=
new
StringBuilderO
;
或者
,
可以指定一个初始字符串
:
StringBuilder
todoList
=
new
StringBuilderC
'
To
do
:
\
n
"
;
这种特征叫做重载
(
overloading
。
)
如果多个方法
(
比如
,
StringBuilder
构造器方法
)
有
相同的名字
、
不同的参数
,
便产生了重载
。
编译器必须挑选出具体执行哪个方法
,
它通过用
各个方法给出的参数类型与特定方法调用所使用的值类型进行匹配来挑选出相应的方法
。
如
果编译器找不到匹配的参数
,
就会产生编译时错误
,
因为根本不存在匹配
,
或者没有一个比
其他的更好
。
(
这个过程被称为重载解析
(
overloading
resolution
)
。)
Java
允许重载任何方法
,
而不只是构造器方法
。
因此
,
要完整地描述一个方法
,
需要指出方法名以及参数类型
。
这叫做方法的签名
(
signature
)
。
例如
,
String
类有
4
个
称为
indexOf
的公有方法
。
它们的签名是
返回类型不是方法签名的一部分
。
也就是说
,
不能有两个名字相同
、
参数类型也相
同却返回不同类型值的方法
。
4.6.7 初始化块
前面已经讲过两种初始化数据域的方法
:
•
在构造器中设置值
•在声明中赋值
实际上
,
Java
还有第三种机制
,
称为初始化块
(
initializationblock
)
。
在一个类的声明中
,
可以包含多个代码块
。
只要构造类的对象
,
这些块就会被执行
。
例如
在这个示例中
,
无论使用哪个构造器构造对象
,
id
域都在对象初始化块中被初始化
。
首
先运行初始化块
,
然后才运行构造器的主体部分
。
4.6.8 对象析构与 finalize 方法
有些面向对象的程序设计语言,
特别是
C
+
+
,
有显式的析构器方法
,
其中放置一些当对
象不再使用时需要执行的清理代码
。
在析构器中
,
最常见的操作是回收分配给对象的存储空
间
。
由于
Java
有自动的垃圾回收器
,
不需要人工回收内存
,
所以
Java
不支持析构器
。
当然,
某些对象使用了内存之外的其他资源
,
例如
,
文件或使用了系统资源的另一个对
象的句柄
。
在这种情况下
,
当资源不再需要时
,
将其回收和再利用将显得十分重要
。
可以为任何一个类添加
finalize
方法
。
finalize
方法将在垃圾回收器清除对象之前调用
。
在实际应用中,
不要依赖于使用
finalize
方法回收任何短缺的资源
,
这是因为很难知道这个
方法什么时候才能够调用
。
4 . 7 包
Java 允许使用包
(
package
>
将类组织起来
。
借助于包可以方便地组织自己的代码
,
并将
自己的代码与别人提供的代码库分开管理
。
标准的 Java
类库分布在多个包中
,
包括
java
.
lang
、
java
.
util
和
java
.
net
等
。
标准的
Java
包具有一个层次结构
。
如同硬盘的目录嵌套一样
,
也可以使用嵌套层次组织包
。
所有标准的
Java
包都处于
java
和
javax
包层次中
。
使用包的主要原因是确保类名的唯一性。
假如两个程序员不约而同地建立了
Employee
类
。
只要将这些类放置在不同的包中
,
就不会产生冲突
。
事实上
,
为了保证包名的绝对
唯一性
,
Sun
公司建议将公司的因特网域名
(
这显然是独一无二的
)
以逆序的形式作为包
名
,
并且对于不同的项目使用不同的子包
。
例如
,
horstmann
.
com
是本书作者之一注册的域
名
。
逆序形式为
com
.
horstmann
。
这个包还可以被进一步地划分成子包
,
如
com
.
horstmann
.
corejava
。
4 . 7.2 静态导入
import
语句不仅可以导人类
,
还增加了导人静态方法和静态域的功能
。
例如
,
如果在源文件的顶部
,
添加一条指令
:
import
static
java
.
lang
.System.
*
;
就可以使用
System
类的静态方法和静态域
,
而不必加类名前缀
:
4 . 7.3 将类放入包中
要想将一个类放人包中
,
就必须将包的名字放在源文件的开头
,
包中定义类的代码之
前
。
例如
,
程序清单
4
-
7
中的文件
Employee
.
java
开头是这样的
:
4.9 文档注释
JDK 包含一个很有用的工具,
叫做
javadoc
,
它可以由源文件生成一个
HTML
文档
。
事
实上
,
在第
3
章讲述的联机
API
文档就是通过对标准
Java
类库的源代码运行
javadoc
生
成的
。
如果在源代码中添加以专用的定界符 /
*
*
开始的注释
,
那么可以很容易地生成一个看上
去具有专业水准的文档
。
这是一种很好的方式
,
因为这种方式可以将代码与注释保存在一个
地方
。
如果将文档存人一个独立的文件中
,
就有可能会随着时间的推移
,
出现代码和注释不
一致的问题
。
然而
,
由于文档注释与源代码在同一个文件中
,
在修改源代码的同时
,
重新运
行
javadoc
就可以轻而易举地保持两者的一致性
。
4.9.1 注释的插入
javadoc
实用程序
(
utility
)
从下面几个特性中抽取信息
:
•包
•
公有类与接口
•公有的和受保护的构造器及方法
•
公有的和受保护的域
应该为上面几部分编写注释、
注释应该放置在所描述特性的前面
。
注释以
/
**
开始
,
并
以
*
/
结束
。
每个 /
**
. . .
*
/
文档注释在标记之后紧跟着自由格式文本
(
free
-
form
text
)
。
标记由
@
开
始
,
如
@
author
或
@
param
。
4.9.2 类注释
类注释必须放在
import
语句之后
,
类定义之前。下面是一个类注释的例子
:
4.9.3 方法注释
每一个方法注释必须放在所描述的方法之前
。
除了通用标记之外
,
还可以使用下面的标记
:
•
@
param
变量描述
这个标记将对当前方法的
“
param
”
(
参数
)
部分添加一个条目
。
这个描述可以占据多
行
,
并可以使用
HTML
标记
。
一个方法的所有
@
param
标记必须放在一起
。
•
@
return
描述
这个标记将对当前方法添加
“
return
”
(
返回
)
部分
。
这个描述可以跨越多行
,
并可以
使用
HTML
标记
。
•
©
throws
类描述
这个标记将添加一个注释
,
用于表示这个方法有可能抛出异常
。
有关异常的详细内容
4.10 类设计技巧
我们不会面面俱到
,
也不希望过于沉闷
,
所以这一章结束之前
,
简单地介绍几点技巧
。
应用这些技巧可以使得设计出来的类更具有
OOP
的专业水准
。 \
1. 一定要保证数据私有
这是最重要的;
绝对不要破坏封装性
。
有时候
,
需要编写一个访问器方法或更改器方法
,
但是最好还是保持实例域的私有性
。
很多惨痛的经验告诉我们
,
数据的表示形式很可能会改
变
,
但它们的使用方式却不会经常发生变化
。
当数据保持私有时
,
它们的表示形式的变化不
会对类的使用者产生影响
,
即使出现
bug
也易于检测
。
2. 一定要对数据初始化
Java 不对局部变量进行初始化
,
但是会对对象的实例域进行初始化
。
最好不要依赖于系
统的默认值
,
而是应该显式地初始化所有的数据
,
具体的初始化方式可以是提供默认值
,
也
可以是在所有构造器中设置默认值
。
3. 不要在类中使用过多的基本类型
就是说
,
用其他的类代替多个相关的基本类型的使用
。
4. 不是所有的域都需要独立的域访问器和域更改器
5.将职责过多的类进行分解
6. 类名和方法名要能够体现它们的职责
7.优先使用不可变的类