“发布(publish)”一个对象的意思是指,使对象能够在当前作用域之外的代码中使用。例如,将一个指向该对象的引用保存到其他代码可以访问的地方,或者在某一个非私有的方法中返回该引用,或者将引用传递到其他类的方法中。在许多情况中,我们要确保对象及其内部状态不被发布。而在某些情况下,我们又需要发布某个对象,但如果在发布时要确保线程安全性,则可能需要同步。发布内部状态可能会破坏封装性,并使得程序难以维持不变性条件。例如,如果在对象构造完成之前就发布该对象,就会破坏线程安全性。当某个不应该发布的对象被发布时,这种情况就被称为逸出(Escape)。
示例:
......
public static Set<Secret> knownSecrets;
public void initialize() {
knownSecrets = new HashSet<secret>();
}
......
如上例,发布对象的最简单方法是将对象的引用保存到一个公有的静态变量中,以便任何类和线程都能看见该对象。在initialize方法中实例化一个新的HashSet对象,并将对象的引用保存到knownSecrets中以发布该对象。
当发布某个对象时,可能会间接地发布其他对象。如果将一个Secret对象添加到集合knownSecrets中,那么同样会发布这个对象,因为任何代码都可以遍历这个集合,并获得对这个新Secret对象的引用。同样,如果从非私有方法中返回一个引用,那么同样会发布返回的对象。
见下面这段代码:
class UnsafeStates {
private String[] states = new String[] {"AK","AL","AM"};
public String[] getStates() {return states;}
}
UnsafeStates发布了本应为私有的状态数组。如果按照上述方式来发布states,就会出现问题,因为任何调用者都能修改这个数组的内容。在这个示例中,数组states已经逸出了它所在的作用域,因为这个本应是私有的变量已经被发布了。
当发布一个对象时,在该对象的非私有域中引用的所有对象同样会被发布。一般来说,如果一个已经发布的对象能够通过非私有的变量引用和方法调用到达其他的对象,那么这些对象也都会被发布。
假定有一个类C,对于C来说,“外部(Alien)方法”是指行为并不完全由C来规定的方法,包括其他类中定义的方法以及类C中可以被改写的方法(既不是私有【private】方法也不是终结【final】方法)。当把一个对象传递给某个外部方法时,就相当于发布了这个对象。你无法知道哪些代码会执行,也不知道在外部方法中究竟会发布这个对象,还是会保留对象的引用并在随后由另一个线程使用。
无论其他的线程会对已发布的引用执行何种操作,其实都不重要,因为误用该引用的风险始终存在(比如:如果有人窃取了你的密码并发布到网上,那么你的信息将“逸出":无论是否有人会恶意地使用这些个人信息,你的帐户都已经不再安全了)。当某个对象逸出后,你必须假设有某个类或线程可能会误用该对象。这正是需要使用封装的最主要原因:封装能够使得对程序的正确性进行分析变得可能,并使得无意中破坏设计约束条件变得更难。
最后一种发布对象或其内部状态的机制就是发布一个内部的类实例,如下代码:
public class ThisEscape {
public ThisEscape (EventSource source) {
source.registerListener (
new EventListener() {
public void onEvent(Event e) {
doSomething(e);
}
}
);
}
}
当ThisEscape发布EventListener时,也隐含地发布了ThisEscape实例本身,因为在这个内部类的实例中包含了对ThisEscap实例的隐含引用(因为内部类自动拥有对外围类所有成员的访问权是通过秘密捕获一个指向那个外围类对象的引用现实的,假如ThisEscape类中有个i变量,那么在EventListener内部类中就可以使用ThisEscape.this.i来访问外部类的变量i。)。
安全的对象构造过程
在ThisEscape中给出了逸出的一个特殊示例,即this引用在构造函数中逸出。当内部的EventListener实例发布时,在外部封装的ThisEscape实例也逸出了。当且仅当对象的构造函数返回时,对象才处于可预测的和一致的状态。因此,当从对象的构造函数中发布对象时,只是发布了一个尚未构造完成的对象。即使发布对象的语句位于构造函数的最后一行也是如此。如果this引用在构造过程中逸出,那么这种对象就被认为是不正确构造。注:不要在构造过程中使用this引用逸出。
在构造过程中使this引用逸出的一个常见错误是,在构造函数中启动一个线程。当对象在其构造函数中创建一个线程时,无论是显示创建(通过将它传给构造函数)还是隐式创建(由于Thread或Runnable是该对象的一个内部类),this引用都会被新创建的线程共享。在对象尚未完全构造之前,新的线程就可以看见它。在构造函数中创建线程并没有错误,但最好不要立即启动它,而是通过一个start或initialize方法来启动。在构造函数中调用一个可改写的实例方法时(即不是私有方法,也不是终结方法),同样会导致this引用在构造过程中逸出。
如果想在构造函数中注册一个事件监听器或启动线程,那么可以使用一个私有的构造函数和一个公共的工厂方法(Factory Method),从而避免不正确的构造过程,如下例:
public class SafeListener {
private final EventListense listener;
private SafeListener() {
listener = new EventListener() {
public void onEvent(Event e) {
doSomeThing(e);
}
};
}
public static SafeListener newInstance(EventSource source) {
SafeListener safe = new SafeListener();
source.registerListener(safe.listener);
return safe;
}
}