1 持有引用
java.lang.ref类库包含了一组类,这些类为垃圾回收提供了更大的灵活性。当存在可能会耗尽内存的大对象的时候,这些类显得特别有用。有三个继承自抽象类Reference的类:SoftReference、WeakReference和PhantomReference。当垃圾回收器正在考察的对象只能通过某个Reference对象才“可获得”时,上述这些不同的派生类为垃圾回收器提供了不同级别的间接性指示。
对象是可获得的(reachable),是指此对象可在程序中的某处找到。这意味着你在栈中有一个普通的引用,而它正指向此对象;也可能是你的引用指向某个对象,而那个对象含有另一个引用指向正在讨论的对象;也可能有更多的中间链接。如果一个对象是“可获得的”,垃圾回收器就不能释放它,因为它仍然为你的程序所用。如果一个对象不是“可获得的”,那么你的程序将无法使用它,所以将其回收是安全的。
如果想继续持有对某个对象的引用,希望以后还能够访问到该对象,但是也希望能够允许垃圾回收器释放它,这时就应该使用Reference对象。这样,你可以继续使用该对象,而在内存消耗殆尽的时候又允许释放该对象。
以Reference对象作为你和普通引用之间的媒介(代理),另外,一定不能有普通的引用指向那个对象,这样就能达到上述目的。(普通的引用指没有经Reference对象包装过的引用。)如果垃圾回收器发现某个对象通过普通引用是可获得的,该对象就不会被释放。
SoftReference、WeakReference和PhantomReference由强到弱排列,对应不同级别的“可获得性”。SoftReference用以实现内存敏感的高速缓存。WeakReference是为实现“规范映射”(canonicalizing mappings)而设计的,它不妨碍垃圾回收器回收映射的“键” (或“值”)。“规范映射”中对象的实例可以在程序的多处被同时使用,以节省存储空间。PhantomReference用以调度回收前的清理工作,它比java终止机制更灵活。
使用SoftReference和WeakReference时,可以选择是否要将它们放入ReferenceQueue(用作“回收前清理工作”的工具)。而PhantomReference只能依赖于ReferenceQueue。下面是一个简单的示例:
import java.lang.ref.PhantomReference;
import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.SoftReference;
import java.lang.ref.WeakReference;
import java.util.LinkedList;
class VeryBig {
private static final int SIZE = 10000;
private long[] la = new long[SIZE];
private String ident;
public VeryBig(String id) {
ident = id;
}
public String toString() {
return ident;
}
@Override
protected void finalize() throws Throwable {
System.out.println("Finalizing " + ident);
}
}
public class References {
private static ReferenceQueue<VeryBig> rq = new ReferenceQueue<>();
public static void checkQueue() {
Reference<? extends VeryBig> inq = rq.poll();
if (inq != null) {
System.out.println("In queue: " + inq.get());
}
}
public static void main(String[] args) {
int size = 10;
if (args.length > 0) {
size = new Integer(args[0]);
}
LinkedList<SoftReference<VeryBig>> sa = new LinkedList<>();
for (int i = 0; i < size; i++) {
sa.add(new SoftReference<VeryBig>(new VeryBig("Soft " + i), rq));
System.out.println("Just created: " + sa.getLast());
checkQueue();
}
LinkedList<WeakReference<VeryBig>> wa = new LinkedList<>();
for (int i = 0; i < size; i++) {
wa.add(new WeakReference<VeryBig>(new VeryBig("Weak " + i), rq));
System.out.println("Just created: " + wa.getLast());
checkQueue();
}
SoftReference<VeryBig> s = new SoftReference<VeryBig>(new VeryBig("Soft"));
WeakReference<VeryBig> w = new WeakReference<VeryBig>(new VeryBig("Weak"));
System.gc();
LinkedList<PhantomReference<VeryBig>> pa = new LinkedList<>();
for (int i = 0; i < size; i++) {
pa.add(new PhantomReference<VeryBig>(new VeryBig("Phantom " + i), rq));
System.out.println("Just created: " + pa.getLast());
checkQueue();
}
}
}
运行此程序可以看到(将输出重定向到一个文本文件中,便可以查看分页的输出),尽管还要通过Reference对象访问那些对象(使用get()取得实际的对象引用),但对象还是被垃圾回收器回收了。还可以看到,ReferenceQueue总是生成一个包含null对象的Reference。要利用此机制,可以继承特定的Reference类,然后为这个新类添加一些更有用的方法。
2 WeakHashMap
容器中有一种特殊的Map,即WeakHashMap,它被用来保存WeakReference。它使得规范映射更易于使用。在这种映射中,每个值只保存一份实例以节省存储空间。当程序需要那个“值”的时候,便在映射中查询现有的对象,然后使用它(而不是重新再创建)。映射可将值作为其初始化中的一部分,不过通常是在需要的时候才生成“值”。
这是一种节约存储空间的技术,因为WeakHashMap允许垃圾回收器自动清理键和值,所以它显得十分便利。对于向WeakHashMap添加键和值的操作,则没有什么特殊要求。映射会自动使用WeakReference包装它们。允许清理元素的触发条件是,不再需要此键了,如下所示:
import java.util.WeakHashMap;
class Element {
private String ident;
public Element(String ident) {
this.ident = ident;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((ident == null) ? 0 : ident.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Element other = (Element) obj;
if (ident == null) {
if (other.ident != null)
return false;
} else if (!ident.equals(other.ident))
return false;
return true;
}
@Override
protected void finalize() throws Throwable {
System.out.println("Finalizing " + getClass().getSimpleName() + " " + ident);
}
@Override
public String toString() {
return ident;
}
}
class Key extends Element {
public Key(String ident) {
super(ident);
}
}
class Value extends Element {
public Value(String ident) {
super(ident);
}
}
public class CanonicalMapping {
public static void main(String[] args) {
int size = 1000;
if (args.length > 0) {
size = new Integer(args[0]);
}
Key[] keys = new Key[size];
WeakHashMap<Key, Value> map = new WeakHashMap<>();
for (int i = 0; i < size; i++) {
Key k = new Key(Integer.toString(i));
Value v = new Value(Integer.toString(i));
if (i % 3 == 0) {
keys[i] = k;
}
map.put(k, v);
}
System.gc();
}
}
如同本章前面所述,Key类必须有hashCode()和equals(),因为在散列数据结构中,它被用作键。有关hashCode()的主题在本章前面部分已经描述过了。
运行此程序,会看到垃圾回收器每隔三个键就跳过一个,因为指向那个键的普通引用被存入了keys数组,所以那些对象不能被垃圾回收器回收。
3 java 1.0/1.1的容器
很不幸,许多老的代码是使用java 1.0/1.1的容器写成的,甚至有些新的程序也使用了这些类。因此,虽然在写新的程序时,决不应该使用旧的容器,但你仍然应该了解它们。不过旧容器功能有限,所以对它们也没太多可说的。毕竟它们都过时了,所以我也不想强调某些设计有多糟糕。
3.1 Vector和Enumeration
在java 1.0/1.1中,Vector是唯一可以自我扩展的序列,所以它被大量使用。它的缺点多到这里都难以描述。基本上,可将其看作ArrayList,但是具有又长又难记的方法名。在订正过的java容器类类库中,Vector被改造过,可将其归类为Collection和List。这样做有点不妥当,可能会让人误会Vector变得好用了,实际上这样做只是为了支持java2之前的代码。
java 1.0/1.1版本的迭代器发明了一个新名字——枚举,取代了为人熟知的术语(迭代器)。此Enumeration接口比Iterator小,只有两个名字很长的方法:一个为boolean hasMoreElements(),如果此枚举包含更多元素,该方法就返回true;另一个为Object nextElement(),该方法返回此枚举中的下一个元素(如果还有的话),否则抛出异常。
Enumeration只是接口而不是实现,所以有时新的类库仍然使用了旧的Enumeration,这令人十分遗憾,但通常不会造成伤害。虽然在你的代码中应该尽量使用Iterator,但也得有所准备,类库可能会返回给你一个Enumeration。
此外,还可以通过使用Collections.enumeration()方法来从Collection生成一个Enumeration,见下面的例子:
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.Vector;
import com.buba.util.Countries;
public class Enumerations {
public static void main(String[] args) {
Vector<String> v = new Vector<>(Countries.names(10));
Enumeration<String> e = v.elements();
while (e.hasMoreElements()) {
System.out.print(e.nextElement() + ", ");
}
e = Collections.enumeration(new ArrayList<String>());
}
}
可以调用elements()生成Enumeration,然后使用它进行前序遍历。最后一行代码创建了一个ArrayList,并且使用enumeration()将ArrayList的Iterator转换成了Enumeration。这样,即使有需要Enumeration的旧代码,你仍然可以使用新容器。
3.2 Hashtable
正如在前面性能比较中所看到的,基本的Hashtable与HashMap很相似,甚至方法名也相似。所以,在新的程序中,没有理由在使用Hashtable而不用HashMap。
3.3 Stack
前面在使用LinkedList时,已经介绍过“栈”的概念。java 1.0/1.1的Stack很奇怪,竟然不是用Vector来构建Stack,而是继承了Vector。所以它拥有Vector所有的特点和行为,再加上一些额外的Stack行为。很难了解设计者是否意识到这样做特别有用处,或者只是一个幼稚的设计。唯一清楚的是,在匆忙发布之前它没有经过仔细审查,因此这个糟糕的设计仍然挂在这里(但是你永远都不应该使用它)。
这里是Stack的一个简单示例,将enum中的每个String表示压入Stack。它还展示了你可以如何方便的将LinkedList,或者在持有对象章节中创建的Stack类用作栈:
import java.util.LinkedList;
import java.util.Stack;
enum Month {
JANUARY, FEBRUARY, MARCH, APRIL, MAY, JUNE, JULY, AUGUST, SEPTEMBER, OCTOBER, NOVEMBER
}
public class Stacks {
public static void main(String[] args) {
Stack<String> stack = new Stack<>();
for (Month m : Month.values()) {
stack.push(m.toString());
}
System.out.println("Stack = " + stack);
stack.addElement("The last line");
System.out.println("element 5 = " + stack.elementAt(5));
System.out.println("popping elements:");
while (!stack.empty()) {
System.out.print(stack.pop() + " ");
}
LinkedList<String> lstack = new LinkedList<String>();
for (Month m : Month.values()) {
lstack.addFirst(m.toString());
}
System.out.println("lstack = " + lstack);
while (!lstack.isEmpty()) {
System.out.print(lstack.removeFirst() + " ");
}
com.buba.util.Stack<String> stack2 = new com.buba.util.Stack<String>();
for (Month m : Month.values()) {
stack2.push(m.toString());
}
System.out.println("stack2 = " + stack2);
while (!stack2.empty()) {
System.out.print(stack2.pop() + " ");
}
}
}
String表示从Month enum常量中生成的,用push()插入Stack,然后再从栈的顶端弹出来(用pop())。这里要特别强调:可以在Stack对象上执行Vector的操作。这不会有任何问题,因为继承的作用使得Stack是一个Vector,因此所有可以对Vector执行的操作,都可以对Stack执行,例如elementAt()。
前面曾经说过,如果需要栈的行为,应该使用LinkedList,或者从LinkedList类中自己创建的uitl.Stack类。
3.4 BitSet
如果想要高效率的存储大量的“开/关”信息,BitSet是很好的选择。不过它的效率仅是对空间而言;如果需要高效的访问时间,BitSet比本地数组稍慢一点。
此外,BitSet的最小容量是long:64位。如果存储的内容比较小,例如8位,那么BitSet就浪费了一些空间。因此如果空间对你很重要,最好撰写自己的类,或者直接采用数组来存储你的标志信息(只有在创建包含开关信息列表的大量对象,并且促使你做出决定的依据仅仅是性能和其他度量因素时,才属于这种情况。如果你做出这个决定只是因为你认为某些对象太大了,那么你最终会产生不需要的复杂性,并会浪费掉大量的时间)。
普通的容器都会随着元素的加入而扩充其容量,BitSet也是,以下示范了BitSet是如何工作的:
import java.util.BitSet;
import java.util.Random;
public class Bits {
public static void printBitSet(BitSet b) {
System.out.println("bits: " + b);
StringBuilder bbits = new StringBuilder();
for (int i = 0; i < b.size(); i++) {
bbits.append(b.get(i) ? "1" : "0");
}
System.out.println("bit pattern: " + bbits);
}
public static void main(String[] args) {
Random r = new Random(47);
byte bt = (byte) r.nextInt();
BitSet bb = new BitSet();
for (int i = 7; i >= 0; i--) {
if (((1 << i) & bt) != 0) {
bb.set(i);
} else {
bb.clear(i);
}
}
System.out.println("byte value: " + bt);
printBitSet(bb);
short st = (short) r.nextInt();
BitSet bs = new BitSet();
for (int i = 15; i >= 0; i--) {
if (((1 << i) & st) != 0) {
bs.set(i);
} else {
bs.clear(i);
}
}
System.out.println("short value: " + st);
printBitSet(bs);
int it = r.nextInt();
BitSet bi = new BitSet();
for (int i = 31; i >= 0; i--) {
if (((1 << i) & st) != 0) {
bi.set(i);
} else {
bi.clear(i);
}
}
System.out.println("int value: " + it);
printBitSet(bi);
// Test bitsets >= 64 bits
BitSet b127 = new BitSet();
b127.set(127);
System.out.println("set bit 127: " + b127);
BitSet b255 = new BitSet(65);
b255.set(255);
System.out.println("set bit 255: " + b255);
BitSet b1023 = new BitSet(512);
b1023.set(1023);
b1023.set(1024);
System.out.println("set bit 1023: " + b1023);
}
}
随机数发生器被用来生成随机的byte、short和int,每一个都被转换为BitSet中相应的位模式。因为BitSet是64位的,所以任何生成的随机数都不会导致BitSet扩充容量。然后创建了一个更大的BitSet。你可以看到,BitSet在必要时会进行扩充。
如果拥有一个可以命名的固定的标志集合,那么EnumSet(在枚举类型中介绍)与BitSet相比,通常是一种更好的选择,因为EnumSet允许你按照名字而不是数字位的位置进行操作,因此可以减少错误。EnumSet还可以防止你因不注意而添加新的标志位置,,这种行为能够引发严重的、难以发现的缺陷。你应该使用BitSet而不是EnumSet的理由只包括:只有在运行时才知道需要多少个标志;对标志命名不合理;需要BitSet中的某种特殊操作(查看BitSet和EnumSet的JDK文档)。