容器深入研究(12):持有引用、java1.0/1.1的容器

本文深入探讨Java中的引用类型,包括SoftReference、WeakReference和PhantomReference,以及它们在垃圾回收中的作用。同时,文章详细介绍了WeakHashMap的工作原理和使用场景,对比了Java 1.0/1.1容器如Vector、Hashtable、Stack和BitSet的特性和局限性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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文档)。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ywz欧卡

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值