目录
继 承
利用继承
,
人们可以基于已存在的类构造一个新类
。
继承已存在的类就
是复用
(
继承
)
这些类的方法和域
。
在此基础上
,
还可以添加一些新的方法和域
,
以满足新
的需求
。
这是
Java
程序设计中的一项核心技术
。
反射是指在程序运行期间发现更多的类
及其属性的能力
。
这是一个功能强大的特性
,
使用起来也比较复杂
。
5.1 类、超类和子类
假设你在某个公司工作
,
这 个公司中经理的待遇与普通雇员的待遇存在着一些差异。
不过
,
他们之间也存在着很多相同 的地方,
例如
,
他们都领取薪水
。
只是普通雇员在完成本职任务之后仅领取薪水
,
而经理在 完成了预期的业绩之后还能得到奖金。
这种情形就需要使用继承
。
这是因为需要为经理定义 一个新类 Manager
,
以便增加一些新功能
。
但可以重用
Employee
类中已经编写的部分代码
, 并将其中的所有域保留下来。
从理论上讲
,
在
Manager
与
Employee
之间存在着明显的
“
is
- a”
(
是
)
关系
,
每个经理都是一名雇员
:“ is
-
a
”
关系是继承的一个明显特征
。
5.1.1 定义子类
下面是由继承
Employee
类来定义
Manager
类的格式
,
关键字
extends
表示继承
关键字
extends
表明正在构造的新类派生于一个已存在的类
。
已存在的类称为超类
(
superclass
)
、
基类
(
base
class
)
或父类
(
parent
class
)
;
新类称为子类
(
subclass
、
)
派生类
(
derived
class
)
或孩子类
(
child
class
)
。
超类和子类是
Java
程序员最常用的两个术语
,
而了解
其他语言的程序员可能更加偏爱使用父类和孩子类
,
这些都是继承时使用的术语
。
在通过扩展超类定义子类的时候
,
仅需要指出子类与超类的不同之处
。因此在设计类的
时候
,
应该将通用的方法放在超类中
,
而将具有特殊用途的方法放在子类中
,
这种将通用的
功能放到超类的做法
,
在面向对象程序设计中十分普遍
5.1.2 覆盖方法
然而
,
超类中的有些方法对子类
Manager
并不一定适用
。
具体来说
,
Manager
类中的
getSalary
方法应该返回薪水和奖金的总和
。
为此
,
需要提供一个新的方法来覆盖
(
override
)
超类中的这个方法
:
super.getSalary()上述语句调用的是 Employee 类中的 getSalary 方法。
注释
:
有些人认为
super
与
this
引用是类似的概念
,
实际上
,
这样比较并不太恰当
。
这是
因为
super
不是一个对象的引用
,
不能将
super
赋给另一个对象变量
,
它只是一个指示编
译器调用超类方法的特殊关键字
。
5.1.3 子类构造器
由于 Manager
类的构造器不能访问
Employee
类的私有域
,
所以必须利用
Employee
类
的构造器对这部分私有域进行初始化
,
我们可以通过
super
实现对超类构造器的调用
。
使用
super
调用构造器的语句必须是子类构造器的第一条语句
。
如果子类的构造器没有显式地调用超类的构造器,
则将自动地调用超类默认
(
没有参数
)
的构造器
。
如果超类没有不带参数的构造器
,
并且在子类的构造器中又没有显式地调用超类
的其他构造器’ 则
Java
编译器将报告错误
。
5.1.4 继承层次
继承并不仅限于一个层次
。
例如
,
可以由
Manager
类派生
Executive
类
。由一个公共超类派生出来的所有类的集合被称为继承层次(
inheritance
hierarchy ), 如图 5-1 所示。在继承层次中, 从某个特定的类到其祖先的路径被称为该类的继承链
(
inheritance
chain)
5 . 1.5 多 态
有一个用来判断是否应该设计为继承关系的简单规则,
这就是
“
is
-
a
”
规则,
它表明子类的每个对象也是超类的对象。
“
is
-
a
”
规则的另一种表述法是置换法则
。
它表明程序中出现超类对象的任何地方都可以
用子类对象置换
。
例如
,
可以将一个子类的对象赋给超类变量
。
在
Java
程序设计语言中
,
对象变量是多态的
。
一个
Employee
变量既可以引用一个
Employee
类对象
,
也可以引用一个
Employee
类的任何一个子类的对象
(
例如
,
Manager
、
Executive
、
Secretary
等
)
然而
,
不能将一个超类的引用赋给子类变量
。
原因很清楚
:不是所有的雇员都是经理
。
如果赋值成功
,
m
有可能引用了一个不是经理的
Employee
对象
,
当在后面调用
m
.
setBonus
(
.
.
.
)
时就有可能发生运行时错误
。
5.1.6 理解方法调用
弄清楚如何在对象上应用方法调用非常重要
。
下面假设要调用
x
.
f
(
args
,
)
隐式参数
x
声
明为类
C
的一个对象
。
下面是调用过程的详细描述:
1
)
编译器査看对象的声明类型和方法名
。
2
)
接下来
,
编译器将査看调用方法时提供的参数类型
。
由于允许类型转换
(
int
可以转换成
double
,
Manager
可以转换成
Employee
,
等等
,
)
所以这
个过程可能很复杂
。
如果编译器没有找到与参数类型匹配的方法
,
或者发现经过类型转换后
有多个方法与之匹配
,
就会报告一个错误
。
至此
,
编译器已获得需要调用的方法名字和参数类型
。
3
)
如果是
private
方法
、
static
方法
、
final
方法
(
有关
final
修饰符的含义将在下一节讲
述
)
或者构造器
,
那么编译器将可以准确地知道应该调用哪个方法
,
我们将这种调用方式称
为静态绑定
(
static
binding
)
。
与此对应的是
,
调用的方法依赖于隐式参数的实际类型
,
并且
在运行时实现动态绑定
。
在我们列举的示例中
,
编译器采用动态绑定的方式生成一条调用
f
(
String
)
的指令
。
注:Java中只有private、static和final修饰的方法以及构造方法是静态绑定。
a、private方法的特点是不能被继承,也就是不存在调用其子类的对象,只能调用对象自身,因此private方法和定义该方法的类绑定在一起。
b、static方法又称类方法,类方法属于类文件。它不依赖对象而存在,在调用的时候就已经知道是哪个类的,所以是类方法是属于静态绑定。
c、final方法:final方法可以被继承,但是不能被重写,所以也就是说final方法是属于静态绑定的,因为调用的方法是一样的。
总结:如果一个方法不可被继承或者继承后不可被覆盖,那么这个方法就采用的静态绑定。
4
)
当程序运行
,
并且采用动态绑定调用方法时
,
虚拟机一定调用与
x
所引用对象的实
际类型最合适的那个类的方法
。
假设
x
的实际类型是
D
,
它是
C
类的子类
。
如果
D
类定义了
方法
f
(
String
,
)
就直接调用它
;
否则
,
将在
D
类的超类中寻找
f
(
String
,
)
以此类推
。
动态绑定
编译器在每次调用方法时都要进行搜索,时间开销相当大。因此虚拟机会预先为每个类创建一个方发表(method table),其中列出了所有方法的签名和实际调用的方法。
动态绑定过程:
<1>虚拟机提取对象的实际类型的方法表。
<2>虚拟机搜索方法签名,此时虚拟机已经知道应该调用哪种方法。(PS:方法的签名包括了:1.方法名 2.参数的数量和类型~~~~返回类型不是签名的一部分。)
<3>虚拟机调用方法
动态绑定有一个非常重要的特性
:
无需对现存的代码进行修改
,
就可以对程序进行扩展
。
假设增加一个新类
Executive
,
并且变量
e 有可能引用这个类的对象
,
我们不需要对包含调用
e
.
getSalary
(
)
的代码进行重新编译
。
如果
e
恰好引用一个
Executive
类的对象
,
就会自动地调
用
Executive
.
getSalaryO
方法
5.1.7 阻止继承:final 类和方法
有时候
,
可能希望阻止人们利用某个类定义子类
。
不允许扩展的类被称为 final
类
。
如果
在定义类的时候使用了
final
修饰符就表明这个类是
final
类
。
例如
,
假设希望阻止人们定义
Executive
类的子类
,
就可以在定义这个类的时候
’
使用
final
修饰符声明
。
5.1.8 强制类型转换
进行类型转换的唯一原因是
:
在暂时忽视对象的实际类型之后
,
使用对象的全部功能
。
例如
,
在
managerTest
类中
,
由于某些项是普通雇员
,
所以
staff
数组必须是
Employee
对象
的数组
。
我们需要将数组中引用经理的元素复原成
Manager
类
,
以便能够访问新增加的所有
变量
(
需要注意
,
在前面的示例代码中
,
为了避免类型转换
,
我们做了一些特别的处理
,
即
将
boss
变量存入数组之前
,
先用
Manager
对象对它进行初始化
。
而为了设置经理的奖金
,
必
须使用正确的类型
。
最后
,
如果这个类型转换不可能成功
,
编译器就不会进行这个转换
。
例如
,
下面这个类
型转换
:
String
c
=
(
String
)
staff
[
1
];
将会产生编译错误
,
这是因为
String
不是
Employee
的子类
。
综上所述
:
•
只能在继承层次内进行类型转换
。
•
在将超类转换成子类之前
,
应该使用
instanceof
进行检查
。
5 . 1.9 抽象类
5.1.10 受保护访问
大家都知道,
最好将类中的域标记为
private
,
而方法标记为
public
。
任何声明为
private
的内容对其他类都是不可见的
。
前面已经看到
,
这对于子类来说也完全适用
,
即子类也不能
访问超类的私有域
。
然而,
在有些时候
,
人们希望超类中的某些方法允许被子类访问
,
或允许子类的方法访
问超类的某个域
。
为此
,
需要将这些方法或域声明为
protected
。
例如
,
如果将超类
Employee
中的
hireDay
声明为
proteced
,
而不是私有的
,
Manager
中的方法就可以直接地访问它
。
不过
,
Manager
类中的方法只能够访问
Manager
对象中的
hireDay
域
,
而不能访问其他
Employee
对象中的这个域
。
这种限制有助于避免滥用受保护机制
,
使得子类只能获得访问受
保护域的权利
。
在实际应用中,
要谨慎使用
protected
属性
。
假设需要将设计的类提供给其他程序员使
用
,
而在这个类中设置了一些受保护域
,
由于其他程序员可以由这个类再派生出新类
,
并访
问其中的受保护域
。
在这种情况下
,
如果需要对这个类的实现进行修改
,
就必须通知所有使
用这个类的程序员
。
这违背了
OOP
提倡的数据封装原则
。
下面归纳一下
Java
用于控制可见性的
4
个访问修饰符
:
1
)
仅对本类可见 private。
2
)
对所有类可见 public:
3
)
对本包和所有子类可见 protected。
4
) 对本包可见
默认(
很遗憾
,
)
不需要修饰符。
5.3 泛型数组列表(详细会出一个专题)
在许多程序设计语言中
,
特别是在
C
++
语言中
,
必须在编译时就确定整个数组的大小
。
程序员对此十分反感
,
因为这样做将迫使程序员做出一些不情愿的折中
。
例如
,
在一个部门
中有多少雇员
?
肯定不会超过丨
00
人
。
一旦出现一个拥有
150
名雇员的大型部门呢
?
愿意为
那些仅有
10
名雇员的部门浪费
90
名雇员占据的存储空间吗
?
在
Java
中
,
情况就好多了
。它允许在运行时确定数组的大小。
当然
,
这段代码并没有完全解决运行时动态更改数组的问题
。
一旦确定了数组的大小
,
改
变它就不太容易了
。
在
Java
中
,
解决这个问题最简单的方法是使用
Java
中另外一个被称为
ArrayList
的类
。
它使用起来有点像数组
,
但在添加或删除元素时
,
具有自动调节数组容量的
功能
,
而不需要为此编写任何代码
。
ArrayList
是一个采用类型参数
(
type
parameter
)
的泛型类
(
generic
class
)
。
为了指定数
组列表保存的元素对象类型
,
需要用一对尖括号将类名括起来加在后面
,
例如
,
ArrayList
<
Employee
>
。
在第
8
章中将可以看到如何自定义一个泛型类
,
这里并不需要了解任何技术细
节就可以使用
ArrayList
类型
。
下面声明和构造一个保存
Employee
对象的数组列表:
这被称为
“
菱形
”
语法
,
因为空尖括号
o
就像是一个菱形
。
可以结合
new
操作符使用菱形
语法
。
编译器会检查新值是什么
。
如果赋值给一个变量
,
或传递到某个方法
,
或者从某个方
法返回
,
编译器会检査这个变量
、
参数或方法的泛型类型
,
然后将这个类型放在
o
中
。
在
这个例子中
,
new
ArrayListo
(
)
将赋至一个类型为
ArrayList
<
Employee
>
的变量
,
所以泛型
类型为
Employee
。
5.4 对象包装器与自动装箱
有时,
需要将
int
这样的基本类型转换为对象
。
所有的基本类型都冇一个与之对应的类
。
例如
,
丨
nteger
类对应基本类型
int
。
通常
,
这些类称为包装器
(
wrapper
)
这些对象包装器类
拥有很明显的名字
:
Integer
、
Long
、
Float
、
Double
、
Short
、
Byte
、
Character
、
Void
和
Boolean
(
前
6 个类派生于公共的超类
Number
)
。
对象包装器类是不可变的
,
即一旦构造了包装器
,
就不
允许更改包装在其中的值
。
同时
,
对象包装器类还是
final
,
因此不能定义它们的子类
。
幸运的是
,
有一个很有用的特性
,
从而更加便于添加
int
类型的元素到
ArrayLisKlntegeP
中
。下面这个调用 list
.
add
(
3
)
;
将自动地变换成 list
.
add
(
Integer
.
value0
f
(
3
))
;
这种变换被称为自动装箱
(
autoboxing
。
相反地
,
当将一个
Integer
对象赋给一个
int
值时
,
将会自动地拆箱
。
也就是说
,
编译器
将下列语句:
int
n
=
list
.
get
(
i
)
;
翻译成
int
n
=
list
.get(
i
).intValue
(
)
最后强调一下
,
装箱和拆箱是编译器认可的
,
而不是虚拟机
。
编译器在生成类的字节码
时
,
插人必要的方法调用
。
虚拟机只是执行这些字节码
。
5.7 反射
反射库
(
reflection
library
)
提供了一个非常丰富且精心设计的工具集
,
以便编写能够动
态操纵
Java
代码的程序
。
这项功能被大量地应用于
JavaBeans
中
,
它是
Java
组件的体系结构
(
有关
JavaBeans
的详细内容在卷
II
中阐述
)
。
使用反射
,
Java
可以支持
Visual
Basic
用户习惯
使用的工具
。
特别是在设计或运行中添加新类时
,
能够快速地应用开发工具动态地查询新添
加类的能力
。
能够分析类能力的程序称为反射
(
reflective
)
。
反射机制的功能极其强大
,
在下面可以看
到
,
反射机制可以用来
:
•
在运行时分析类的能力
。
•
在运行时查看对象
,
例如
,
编写一个
toString
方法供所有类使用
。
•
实现通用的数组操作代码
。
•
利用
Method
对象
,
这个对象很像中的函数指针
5.7.1 Class 类
在程序运行期间
,
Java
运行时系统始终为所有的对象维护一个被称为运行时的类型标识
。
这个信息跟踪着每个对象所属的类
。
虚拟机利用运行时类型信息选择相应的方法执行
。
然而
, 可以通过专门的
Java
类访问这些信息
。
保存这些信息的类被称为
Class
,
这 个 名
字很容易让人混淆
。
Object
类中的
getClass
(
) 方法将会返回一个Class
类型的实例
。
如同用一个
Employee
对象表示一个特定的雇员属性一样
,
一个
Class
对象将表示一个特
定类的属性
。
最常用的
Class
方法是
getName
。
这个方法将返回类的名字
。
例如
,
下面这条
语句
:
还可以调用静态方法
forName
获得类名对应的
Class
对象
。(全限定类名)
String
dassName
=
"
java
.
util
.
Random
"
;
Class
cl
=
Cl
ass
.
forName
(
dassName
)
;
如果类名保存在字符串中
,
并可在运行中改变
,
就可以使用这个方法
。
当然
,
这个方法
只有在
dassName
是类名或接口名时才能够执行
。
否则
,
forName
方法将抛出一个
checked
exception
(
已检查异常
)
。
无论何时使用这个方法
,
都应该提供一个异常处理器
(
exception
handler
)
o
如何提供一个异常处理器
,
请参看下一节
。
获得
Class
类对象的第三种方法非常简单
。
如果
T 是任意的
Java
类型
(
或
void
关键字
,
)
T
.
class
将代表匹配的类对象
。例如:
请注意
,
一个
Class
对象实际上表示的是一个类型
,
而这个类型未必一定是一种类
。
例如
,
int
不是类
,
但
int
.
class
是一个
Class
类型的对象
。
还有一个很有用的方法
newlnstance
(
)
,
可以用来动态地创建一个类的实例例如
,
e
.
getClass
0
.
newlnstance
(
)
;
创建了一个与
e
具有相同类类型的实例
。
newlnstance
方法调用默认的构造器
(
没有参数的构
造器
)
初始化新创建的对象
。
如果这个类没有默认的构造器
,
就会抛出一个异常
_
将
forName
与
newlnstance
配合起来使用
,
可以根据存储在字符串中的类名创建一个对象
String
s
=
"
java
.
util
.
Random
"
;
Object
m
=
Cl
ass
.
forName
(
s
)
.
newlnstance
(
)
;
如果需要以这种方式向希望按名称创建的类的构造器提供参数
,
就不要使用上面
那条语句
,
而必须使用
Constructor
类中的
newlnstance
方法
。
5.7.3 利用反射分析类的能力
下面简要地介绍一下反射机制最重要的内容
—
检查类的结构
。
在
java
.
lang
.
reflect
包中有三个类
Field
、
Method
和
Constructor
分别用于描述类的域
、
方
法和构造器
。
这三个类都有一个叫做
getName
的方法
,
用来返回项目的名称
。
Held
类有一
个
getType
方法
,
用来返回描述域所属类型的
Class
对象
。
Method
和
Constructor
类有能够
报告参数类型的方法
,
Method
类还有一个可以报告返回类型的方法
。
这
<
个类还有一个叫
做
getModifiers
的方法
,
它将返回一个整型数值
,
用不同的位开关描述
public
和
static
这样
的修饰符使用状况
。
另外
,
还可以利用
java
.
lang
.
refleCt
包中的
Modifiei
•
类的静态方法分析
getModifiers
返回的整型数值
。
例如
,
可以使用
Modifier
类中的
isPublic
、
isPrivate
或
isFinal
判断方法或构造器是否是
public
、
private
或
final
。
我们需要做的全部工作就是调用
Modifier
类的相应方法
,
并对返回的整型数值进行分析
,
另外
,
还可以利用
Modifier
.
toString
方法将
修饰符打印出来
Class
类中的
getFields
、
getMethods
和
getConstructors
方 法 将 分 别 返 回 类 提 供 的
public
域
、
方法和构造器数组
,
其中包括超类的公有成员
。
Class
类的
getDeclareFields
、
getDeclareMethods
和
getDeclaredConstructors
方法将分别返回类中声明的全部域
、
方法和构
造器
,
其中包括私有和受保护成员
,
但不包括超类的成员
。