目录
1 公共静态变量逸出
2 非私有方法逸出私有变量
3 this引用逸出
4 构造函数中的可覆盖方法调用逸出
发布(publishing)一个对象的意思是:使对象能够在当前作用域之外的代码中使用。例如,将一个指向该对象的引用保存到其他代码可以访问的地方,或者在某一个非私有的方法中返回该引用,或者将引用传递到其他类的方法中。
发布内部状态可能会破坏封装性,并使程序难以维持不变性条件。例如,如果在对象构造完成之前就发布该对象,就会破坏线程安全性。 当某个不应该发布的对象被发布时,这种情况就成为逸出(escape)。
简而言之,发布就是把对象暴露给他人使用,这就是为什么会需要用到封装;逸出就是把不应该发布的对象发布了,比如对象还没完成实例化,就被外界使用了。
1 公共静态变量逸出
发布对象的最常见方式就是将对象的引用保存到一个公有的静态变量中,任何类和线程都能看见该对象。如下代码所示,initialize方法实例化一个新的HashSet实例,并通过将它存储到knownSecrets引用,从而发布这个实例:
// 3-5 发布一个对象
public static Set<Secret> knownSecrets;
public void initialize() {
knownSecrets = new HashSet<Secret>();
}
当发布某个对象时,可能会间接地发布其他对象。如果将一个Secret对象添加到集合knownSecrets中,那么同样会发布这个对象,因为任何代码都可以遍历这个集合,并获得对这个新Secret对象的引用。
2 非私有方法逸出私有变量
从非私有方法中返回一个引用,也能发布返回的对象。下面的代码发布了包含洲名缩写的数组,而这个数组本应是私有的:
// 3-6 使内部可变状态逸出(不要这样做)
public class UnsafeStates {
private String[] states = new String[] { "AK", "AL", "LW" };
public String[] getStates() {
return states;
}
public static void main(String[] args) {
UnsafeStates us = new UnsafeStates();
System.out.println(Arrays.toString(us.getStates()));
us.getStates()[0] = "NY";
System.out.println(Arrays.toString(us.getStates()));
}
}
这样发布states会出现问题,因为任何调用者都能修改这个数组的内容。通过访问对象中的共有方法获取私有变量的值,然后更改内部数据,则导致变量逸出作用域。数组states已经逸出了它所在的作用域,这个本该私有的数据,事实上已经变成共有了。
发布一个对象时,该对象的非私有域中引用的所有对象同样会被发布。更一般的,一个已发布的对象中,那些非私有的引用链及方法调用链中的可获得对象也都会被发布。
3 this引用逸出
最后一种发布对象或其内部状态的机制就是发布一个内部的类实例。当ThisEscape发布内部类EvnetLister时,也隐含地发布了ThisEscape实例本身,因为在这个内部类的实例中也包含了对ThisEscape实例的隐含引用。
1、EventListener接口
public interface EventListener {
void onEvent(Object obj);
}
2、EventSource
public class EventSource<T> {
private final List<T> eventListeners;
public EventSource() {
eventListeners = new ArrayList<>();
}
public synchronized void registerListener(T eventListener) {
this.eventListeners.add(eventListener);
this.notifyAll();
}
public synchronized List<T> retrieveListeners() throws InterruptedException {
List<T> dest = null;
if (eventListeners.size() <= 0) {
this.wait();
}
dest = new ArrayList<>(eventListeners.size());
dest.addAll(eventListeners);
return dest;
}
}
3、ThisEscape
public class ThisEscape {
public final int id;
public final String name;
public ThisEscape(EventSource source) {
id = 100;
// ThisEscape尝试在构造函数中注册一个事件监听器
source.registerListener(new EventListener() {
@Override
public void onEvent(Object obj) {
System.out.println("id: " + ThisEscape.this.id);
System.out.println("name: " + ThisEscape.this.name);
}
});
try {
// 调用sleep模拟其他耗时的初始化操作
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
name = "ThisEscape初始化完成";
}
@Override
public String toString() {
return "ThisEscape{" +
"id=" + id +
", name='" + name + '\'' +
'}';
}
}
ThisEscape演示了一种重要的逸出特例,this引用在构造时逸出。发布的内部EventListener实例是一个封装的ThisEscape中的实例。但是这个对象只有通过构造器函数返回后,才处于可预言的、稳定的状态,所以从构造器函数内部发布的对象,只是一个未完成构造的对象。
内部类、匿名内部类都可以访问外部类的对象的域,因为内部类构造的时候,会把外部类的对象this隐式的作为一个参数传递给内部类的构造方法,这个工作是编译器做的,它会给内部类所有的构造方法添加这个参数,所以这个例子的匿名内部类在构造ThisEscape时就把ThisEscape创建的对象隐式的传给匿名内部类了。这样 source就持有ThisEscape的内部类EvenListener,而Evenlistener可能会带出ThisEscape中的保护数据引用,如果此时ThisEscape还未初始化完成,Evenlistener可能会访问到ThisEscape中未完成初始化的数据,因为this引用提前被EventListener实例对象拿到,这就是this引用的逸出。
public class ThisEscapeTest {
public static void main(String[] args) throws InterruptedException {
EventSource<EventListener> source = new EventSource<>();
new Thread(() -> {
try {
List<EventListener> listeners = source.retrieveListeners();
for(EventListener listener : listeners) {
listener.onEvent(new Object());
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}).start();
ThisEscape escape = new ThisEscape(source);
System.out.println("ThisEscape 构造完成结果:"+escape);
}
}
运行结果:
这个测试案例中,另一个线程在ThisEscape还未完成初始化时,就访问ThisEscape的内部数据了。
总结这个案例,造成this逸出,一个是在构造函数中创建内部类(EventListener) ,另一个是在构造函数中就把这个内部类给发布了出去(source.registerListener)。那么,对应的解决方法就是:如果要在构造函数中创建内部类,那么就不能在构造函数中将其发布了,应该在构造函数外发布,即等构造函数执行完毕,初始化工作已全部完成,再发布内部类。如果需要在构造函数中注册一个事件监听器或者启动线程,可以使用一个私有的构造函数和一个公共的工厂方法(Factory Method),从而避免不正确的构造过程。
public class SafeListener {
private final EventListener 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;
}
void doSomething(Event e) {
}
}
如上示例代码所示,注册监听在构造之后执行,保证onEvent()方法在SafeListener的构造之后才能被调用,对象正确初始化后再调用this引用指向的对象的方法修改属性就不是逸出,而是发布。
在构造函数过程中使this引用逸出的一个常见错误是,在构造器中启动一个线程。当对象在其构造函数中创建一个线程时,无论是显式创建还是隐式创建,this引用都会被新创建的线程共享。在对象尚未完全构造之前,新的线程就可以看见它。在构造函数中创建线程并没有错误,但最好不要立即启动它,而是通过一个start或initialize方法来启动。
4 构造函数中的可覆盖方法调用逸出
在构造函数中调用一个可覆盖的实例方法时(既不是private,也不是final的),同样会导致this引用在构造期间逸出。
Base类:
public abstract class Base {
Base() {
System.out.println("Base构造函数");
// 在构造函数中调用可重写的方法
overrideMe();
}
// 一个可重写的方法
abstract void overrideMe();
}
子类:
public class Child extends Base{
final int x;
Child(int x) {
System.out.println("Child构造函数");
this.x = x;
}
@Override
void overrideMe() {
System.out.println(x);
}
public static void main(String[] args) {
new Child(42);
}
}
在子类初始化时,会先调用父类Base的构造函数,而父类的构造函数中调用了可重写的方法,实际上调用的是子类中重载的方法,然而此时子类尚未完成初始化,造成的结果就是尚未完成初始化的父类逸出到子类中。
运行结果:
这里,当Base构造函数调用时overrideMe,Child尚未完成初始化final int x,并且该方法获取错误的值,这几乎肯定会导致错误。