为什么我们要使用私有的实例变量呢?因为我们不希望其他类直接的依赖于这些变量。而且在心血来潮时,我们还可以灵活的修改变量类型和实现。然而,为什么程序员们都自动在对象中加入getter和setter方法,以此对外暴露私有变量,就如同这些变量是公有的一样。
存取方法(又被称为getters和setters)是一些可以用来读写对象实例变量值的方法。
1
2
3
4
5
6
7
8
9
10
11
|
public
class
AccessorExample {
private
String attribute;
public
String getAttribute() {
return
attribute;
}
public
void
setAttribute(String attribute) {
this
.attribute = attribute;
}
}
|
在类中使用存取方法而非直接暴露属性是有理由的。
Getter和Setter使得API更加的稳定。比如,假设类中有一个公共属性,它可以被其他类直接存取。一段时间后,你想要在读取或保存这个公共属性的时候添加额外的逻辑。这将影响到已经使用这个API的类。所以对这个公共属性的任何改变都会导致引用这个属性的其他类的改变。相反,使用存取方法,我们可以随后很容易的添加其他的一些逻辑,比如缓存数据,延迟加载。而且,如果新的属性值与旧的属性值不同,我们还可以触发属性改变事件。所有这些对于通过使用存取方法获取值的类来说都是透明的。
属性可以被声明为包级私有或是私有嵌套类可见。在这些类中,相对于使用存取方法而言,对外直接暴露属性字段可以减少类定义和调用代码中的视觉混乱。
如果一个类是包级私有或是私有嵌套类可见,假设它的属性字段很好的描述了类所提供的数据,那么对外暴露这些属性字段本质上是没有问题的。
这样的类被限制在类所声明的包内,同时调用代码受限于类内部表示。我们可以修改这个类,而不用改变任何包外的代码。而且,对于私有嵌套类,改动的范围进一步的被缩小到被嵌套类里。
使用公共属性的另一个例子是JavaSpace 请求对象。Ken Arnold讲述了他们决定使用公共属性,而不是带存取方法的私有属性的经历()
人们被告知不要使用公共属性,公共属性不好,有时这会让人们感觉不舒服,而且时常人们会使用不容置疑的语气来论述。但是我们不是非常虔诚的那些人。制定规则是有理由的。对于私有属性规则的理由并不适用于这个特例。这是一个特殊的例外,我也告诉人们不要在他们的类中使用公共属性,但也存在例外。这就是这个规则的一个例外,因为仅仅说它是一个属性会更加简单和安全。我们退一步想一想:既然这样,为什么要这条规则呢?它是否适用呢?在这个例外中,它并不适用。
考虑下面的例子
1
2
3
|
public
class
A {
public
int
a;
}
|
我们通常都认为以上是糟糕的代码风格,因为它破坏了封装性。替代方法是:
1
2
3
4
5
6
7
8
9
10
11
|
public
class
A {
private
int
a;
public
void
setA(
int
a) {
this
.a =a;
}
public
int
getA() {
return
this
.a;
}
}
|
有人认为这样封装了属性。这真的实现了封装吗?
实际上,Getter/Setter和封装性没有任何关系。数据并没有比使用公共属性获得更多隐蔽或封装。其他的类对这个类的内部细节仍然了如指掌。类的改动可能会蔓延,迫使依赖它的其他类做出相应的修改。以这种方式使用的Getter和Setter通常破坏了封装性。一个真正完整封装的类是没有setter方法的,而且最好也没有getter方法。类应该负责使用自身的数据计算并返回结果,而不是从某个类获得数据并计算这些数据。
看下面的例子,
1
2
3
4
5
6
7
8
9
10
11
12
|
public
class
Screens {
private
Map screens =
new
HashMap();
public
Map getScreens() {
return
screens;
}
public
void
setScreens(Map screens) {
this
.screens = screens;
}
// remaining code here
}
|
如果我们需要获得一个特殊的页面,我们会编写以下的代码,
1
|
Screen s = (Screen)screens.get(screenId);
|
这里值得注意的是:
客户端代码需要从Map里获得一个对象并把它转换为合适的类型。而且,更糟糕的是Map的任何客户端代码都可以清空这个Map,这通常是我们所不希望的。
相同逻辑的替代实现方法是:
1
2
3
4
5
6
7
8
|
public
class
Screens {
private
Map screens =
new
HashMap();
public
Screen getById(String id) {
return
(Screen) screens.get(id);
}
// remaining code here
}
|
这样隐藏了Map实例和交互接口(Map)。
创建私有属性,随后通过IDE自动生成所有这些属性的getters和setters方法,这和直接使用公共属性是一样的糟糕。
过度使用的一个原因是现在在IDE中仅仅需要使用几个点击事件就可以创建这些存取方法。这些完全无意义的getter/setter代码有时会比类的逻辑代码本身还要长,你会多次阅读这些代码,虽然你并不想这么做。
所有的属性都应该保持私有,但对不可改变的属性仅仅增加setter方法。增加一个不必要的getter会暴露内部结构,这也增加了代码耦合的机会。避免方案是在每次增加存取方法的时候,我们应该分析是否可以通过封装行为来替代存取方法。
让我们看看另一个例子,
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
public
class
Money {
private
double
amount;
public
double
getAmount() {
return
amount;
}
public
void
setAmount(
double
amount) {
this
.amount = amount;
}
//client
Money pocketMoney =
new
Money();
pocketMoney.setAmount(15d);
double
amount = pocketMoney.getAmount();
// we know its double
pocketMoney.setAmount(amount + 10d);
}
|
依据以上的逻辑,假设我们随后认为数据类型double不够合适,而是应该使用BigDecimal,这样那些已经使用了这个类的客户端代码也会失效。
让我们重建上面的例子,
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
public
class
Money {
private
BigDecimal amount;
public
Money(String amount) {
this
.amount =
new
BigDecimal(amount);
}
public
void
add(Money toAdd) {
amount = amount.add(toAdd.amount);
}
// client
Money balance1 =
new
Money(
"10.0"
);
Money balance2 =
new
Money(
"6.0"
);
balance1.add(balance2);
}
|
与之前直接请求数据不同,类负责增加它自己的值。使用这种方式,将来任何改变数据类型的请求都不需要改变任何客户端代码。这样,不仅仅封装了数据,而且也封装了数据的保存方式甚至数据是否存在的事实。
通过使用存取方法来限制对属性变量的访问要优于直接使用公共属性变量。但是,为每一个属性都创建getter和setter方法确实有些极端。而且这也要根据具体的情况来定,有些时候你仅仅希望有一个单纯的数据对象而已。应该为真正需要的属性添加存取方法。一个类应该使用它自身的属性,并对外提供强大的功能,而不是仅仅作为一个被其他类操作的存储状态属性的存储池。