对象创建、发布
对象的创建,许多新手也都耳熟能详了,例如
public class School {
private static Person person = new Person();
}
但是因为private关键字的作用,其他类无法直接访问person对象,所以产生了“发布”对象概念。对象的“发布”指的是“使对象能够在当前作用域之外的代码使用”。改变private关键字就可以达到目的:
public class School {
public static Person person = new Person();
}
也有延迟创建方法,这样只有在调用init()函数时才实例化Person对象,相对来说更为优质:
public class School {
private static Person person;
public Person init() {
person = new Person();
return person;
}
}
类比面向对象的封装特性,可以说“封装”是一种更加严格规范的“发布”。
对象逸出
对象发布、创建最容易犯的错误就是对象逸出,“对象逸出”指的是某不应该发布的对象被发布的情况。
例如School类引用了Person类,但是假若要求不该让外界知道Person对象的具体内容,那么之前的操作都不应该进行。但是时常发生的是:你认为已经封装好了对象,但是不经意的操作仍然造成了“对象逸出”。根据形式不同可以分为“显式逸出”和“隐式逸出”。
显式逸出
显示逸出主要问题在于向外界暴露了对象的引用。比如向外发布一个数组,很多人会选择这么做:
public class School {
private Person[] personArray = new Person[] {new Person("Marco", 28), new Person("tremedous", 25)};
public Person[] getPersonArray() {
return personArray;
}
}
这么发布Person数组并不安全,因为调用getPersonArray()函数的地方会持有personArray在堆内存中的引用,可以间接暴露、改变原personArray中的元素。
所以假若你只是想定义一个数组,只想让外界读取数组元素,不能改写数组,getPersonArray()向外界返回的应该只是personArray数组的一个副本。
public class School {
private Person[] personArray = new Person[] {new Person("Marco", 28), new Person("tremedous", 25)};
public Person[] getPersonArray() {
//Person[] personArrayError = personArray; 错误,personArrayError仍然指向personArray数组引用地址
Person[] personArrayCopy = Arrays.copyOf(personArray, personArray.length);
return personArrayCopy;
}
}
要格外注意新手最容易犯的错,Person[] personArrayError = personArray并不是得到了复制数组,而是在栈中产生了一个新的personArrayError变量保存Person数组在堆中地址的引用。这是personArrayError语句执行的示意图:
显式逸出还有一种情况:一个私有的对象添加到一个集合中去,其他类仍可以通过遍历集合(visitList)获得该对象:
public class School {
private static Person person = new Person();
public List<Person> visitList = new ArrayList();
public List initList() {
visitList.add(person);
return visitList;
}
}
隐式逸出
隐式逸出主要问题在于新建其他对象时候暴露了this引用,例如:
public class School {
public static Person person;
public static Room room;
public School() {
this.person = new Person();
this.room = new Room();
}
}
这里造成了this引用在构造函数逸出。因为在person对象发布(实例化)的同时,也间接发布了this应用。这样School类的构造函数还未结束,就发布了一个尚未构造完成的School对象。单线程执行问题不大, 多线程要考虑竞态条件(线程执行时序),因而这是一种不安全的对象构造过程。
该如何规避呢?多看看一些别人的工程代码后,你就容易得到你的答案:
public class School {
public static Person person;
public static Room room;
public School() {
initParam();
}
private synchronized void initParam() {
this.person = new Person();
this.room = new Room();
}
}
首先,initParam()函数用了synchronized互斥锁,意味着同一时刻只能有一个线程来实例化person和room对象。
再者,调用initParam(),未到达方法出口,不会提前暴露this引用,也就不会造成未构造完成的School对象被提前发布了。