WeakHashMap的keySet条目永远不会为空吗?

9
如果我在WeakHashMap的键集上进行迭代,是否需要检查空值?
WeakHashMap<MyObject, WeakReference<MyObject>> hm
    = new WeakHashMap<MyObject, WeakReference<MyObject>>();

for ( MyObject item : hm.keySet() ) {
    if ( item != null ) { // <- Is this test necessary?
        // Do something...
    } 
}

换句话说,在我遍历WeakHashMap的过程中,其中的元素是否可能被收集?
编辑:为了回答这个问题,可以假设哈希映射中没有添加任何null条目。
5个回答

6

我对 WeakHashMap 不熟悉,但你可能有一个空对象。看看这个例子:

public static void main(String[] args)
{
    WeakHashMap<Object, WeakReference<Object>> hm
    = new WeakHashMap<Object, WeakReference<Object>>();
    hm.put(null, null);
    for ( Object item : hm.keySet() ) {
        if ( item == null ) { 
          System.out.println("null object exists");  
        } 
    }
}

1
好的,但是如果我们知道没有空条目被添加到映射表中呢? - Jérôme Verstrynge
我认为这是最正确的答案(不是上面被接受的答案)。一些映射允许使用“null”作为键,而有些则不允许(WeakHashMap允许)。当存在疑问时,请阅读文档。如果您没有将null放入映射中,则不会获得null作为返回值。 - JimN
我同意JimN的观点。这是目前为止对问题的最佳、最完整的回答......尤其是如果有人费心阅读评论的话。 - corlettk

3

以下是从WeakHashMap javadoc中摘录的内容:

基于哈希表实现的Map,具有弱键。当一个key不再被普通使用时,WeakHashMap中的条目将自动被删除。更准确地说,给定key的映射的存在不会阻止垃圾回收器丢弃该key,即使它被做为最终化对象、最终化并被回收。当一个key被丢弃时,它的条目从map中有效地删除,因此,这个类的行为与其他Map实现有所不同。

我的理解是:是的... 当WeakHaskMap中的某个Key没有剩余的外部引用时,那么该Key可能被GC回收,使得相关的Value也无法访问,所以它也(假设没有直接对其进行外部引用)可以被GC回收。

我将测试这个理论。这只是我对文档的解释... 我没有任何关于WeakHashMap的经验... 但我立刻看到了它作为“内存安全”对象缓存的潜力。

谢谢。 Keith。


编辑:探索WeakHashMap... 具体测试我的理论,即外部引用到特定key会导致该key被保留... 这是纯属胡说八道 ;-)

我的测试工具:

package forums;

import java.util.Set;
import java.util.Map;
import java.util.WeakHashMap;
import krc.utilz.Random;

public class WeakCache<K,V> extends WeakHashMap<K,V>
{
  private static final int NUM_ITEMS = 2000;
  private static final Random RANDOM = new Random();

  private static void runTest() {
    Map<String, String> cache = new WeakCache<String, String>();
    String key; // Let's retain a reference to the last key object
    for (int i=0; i<NUM_ITEMS; ++i ) {
      /*String*/ key = RANDOM.nextString();
      cache.put(key, RANDOM.nextString());
    }

    System.out.println("There are " + cache.size() + " items of " + NUM_ITEMS + " in the cache before GC.");

    // try holding a reference to the keys
    Set<String> keys = cache.keySet();
    System.out.println("There are " + keys.size() + " keys");

    // a hint that now would be a good time to run the GC. Note that this
    // does NOT guarantee that the Garbage Collector has actually run, or
    // that it's done anything if it did run!
    System.gc();

    System.out.println("There are " + cache.size() + " items of " + NUM_ITEMS + " remaining after GC");
    System.out.println("There are " + keys.size() + " keys");
  }

  public static void main(String[] args) {
    try {
      for (int i=0; i<20; ++i ) {
        runTest();
        System.out.println();
      }
    } catch (Exception e) {
      e.printStackTrace();
    }
  }
}

一个测试运行的结果(我认为相当令人困惑):

There are 1912 items of 2000 in the cache before GC.
There are 1378 keys
There are 1378 items of 2000 remaining after GC
There are 909 keys

There are 2000 items of 2000 in the cache before GC.
There are 2000 keys
There are 1961 items of 2000 remaining after GC
There are 1588 keys

There are 2000 items of 2000 in the cache before GC.
There are 2000 keys
There are 1936 items of 2000 remaining after GC
There are 1471 keys

There are 2000 items of 2000 in the cache before GC.
There are 2000 keys
There are 2000 items of 2000 remaining after GC
There are 1669 keys

There are 2000 items of 2000 in the cache before GC.
There are 2000 keys
There are 2000 items of 2000 remaining after GC
There are 1264 keys

There are 2000 items of 2000 in the cache before GC.
There are 2000 keys
There are 2000 items of 2000 remaining after GC
There are 1770 keys

There are 2000 items of 2000 in the cache before GC.
There are 2000 keys
There are 2000 items of 2000 remaining after GC
There are 1679 keys

There are 2000 items of 2000 in the cache before GC.
There are 2000 keys
There are 2000 items of 2000 remaining after GC
There are 1774 keys

There are 2000 items of 2000 in the cache before GC.
There are 2000 keys
There are 2000 items of 2000 remaining after GC
There are 1668 keys

There are 2000 items of 2000 in the cache before GC.
There are 2000 keys
There are 2000 items of 2000 remaining after GC
There are 0 keys

There are 2000 items of 2000 in the cache before GC.
There are 2000 keys
There are 2000 items of 2000 remaining after GC
There are 1834 keys

There are 2000 items of 2000 in the cache before GC.
There are 2000 keys
There are 2000 items of 2000 remaining after GC
There are 0 keys

There are 2000 items of 2000 in the cache before GC.
There are 2000 keys
There are 2000 items of 2000 remaining after GC
There are 0 keys

There are 2000 items of 2000 in the cache before GC.
There are 2000 keys
There are 2000 items of 2000 remaining after GC
There are 0 keys

There are 2000 items of 2000 in the cache before GC.
There are 2000 keys
There are 2000 items of 2000 remaining after GC
There are 0 keys

There are 2000 items of 2000 in the cache before GC.
There are 2000 keys
There are 2000 items of 2000 remaining after GC
There are 0 keys

There are 2000 items of 2000 in the cache before GC.
There are 2000 keys
There are 2000 items of 2000 remaining after GC
There are 0 keys

There are 2000 items of 2000 in the cache before GC.
There are 2000 keys
There are 429 items of 2000 remaining after GC
There are 0 keys

There are 2000 items of 2000 in the cache before GC.
There are 2000 keys
There are 0 items of 2000 remaining after GC
There are 0 keys

There are 2000 items of 2000 in the cache before GC.
There are 2000 keys
There are 0 items of 2000 remaining after GC
There are 0 keys

看起来在我的代码执行过程中,密钥仍然会消失......可能需要在GC提示之后进行微睡眠,以便让GC有时间完成它的工作。不管怎样,这种“不稳定性”是有趣的行为。


编辑2:是的,在System.gc();之后直接添加try{Thread.sleep(10);}catch(Exception e){}可以使结果“更可预测”。

There are 1571 items of 2000 in the cache before GC.
There are 1359 keys
There are 0 items of 2000 remaining after GC
There are 0 keys

There are 2000 items of 2000 in the cache before GC.
There are 2000 keys
There are 0 items of 2000 remaining after GC
There are 0 keys

There are 2000 items of 2000 in the cache before GC.
There are 2000 keys
There are 0 items of 2000 remaining after GC
There are 0 keys

There are 2000 items of 2000 in the cache before GC.
There are 2000 keys
There are 0 items of 2000 remaining after GC
There are 0 keys

.... and so on for 20 runs ...

嗯...当垃圾回收在真实应用程序中任意时间触发时,完全消失的缓存...没有多大用处...嗯...我想WeakHashMap是用来干什么的?;-)
最后编辑,我保证 这是我的krc/utilz/Random(在上面的测试中使用)
package krc.utilz;

import java.io.Serializable;
import java.nio.charset.Charset;

/**
 * Generates random values. Extends java.util.Random to do all that plus:<ul>
 * <li>generate random values in a given range, and
 * <li>generate Strings of random characters and random length.
 * </ul>
 * <p>
 * Motivation: I wanted to generate random Strings of random length for test 
 *  data in some jUnit tests, and was suprised to find no such ability in the
 *  standard libraries... so I googled it, and came up with Glen McCluskey's
 *  randomstring function at http://www.glenmccl.com/tip_010.htm. Then I thought
 *  aha, that's pretty cool, but if we just extended it a bit, and packaged it
 *  properly then it'd be useful, and reusable. Cool!
 * See: http://www.glenmccl.com/tip_010.htm
 * See: http://forum.java.sun.com/thread.jspa?threadID=5117756&messageID=9406164
 */
public class Random extends java.util.Random  implements Serializable
{

  private static final long serialVersionUID = 34324;
  public static final int DEFAULT_MIN_STRING_LENGTH = 5;
  public static final int DEFAULT_MAX_STRING_LENGTH = 25;

  public Random() {
    super();
  }

  public Random(long seed) {
    super(seed);
  }

  public double nextDouble(double lo, double hi) {
    double n = hi - lo;
    double i = super.nextDouble() % n;
    if (i < 0) i*=-1.0;
    return lo + i;
  }

  /**
   * @returns a random int between lo and hi, inclusive.
   */
  public int nextInt(int lo, int hi) 
    throws IllegalArgumentException
  {
    if(lo >= hi) throw new IllegalArgumentException("lo must be < hi");
    int n = hi - lo + 1;
    int i = super.nextInt() % n;
    if (i < 0) i = -i;
    return lo + i;
  }

  /**
   * @returns a random int between lo and hi (inclusive), but exluding values
   *  between xlo and xhi (inclusive).
   */
  public int nextInt(int lo, int hi, int xlo, int xhi) 
    throws IllegalArgumentException
  {
    if(xlo < lo) throw new IllegalArgumentException("xlo must be >= lo");
    if(xhi > hi) throw new IllegalArgumentException("xhi must be =< hi");
    if(xlo > xhi) throw new IllegalArgumentException("xlo must be >= xhi");
    int i;
    do {
      i = nextInt(lo, hi);
    } while(i>=xlo && i<=xhi);
    return(i);
  }

  /**
   * @returns a string (of between 5 and 25 characters, inclusive) 
   *  consisting of random alpha-characters [a-z]|[A-Z].
   */
  public String nextString()
    throws IllegalArgumentException
  {
    return(nextString(DEFAULT_MIN_STRING_LENGTH, DEFAULT_MAX_STRING_LENGTH));
  }

  /**
   * @returns a String (of between minLen and maxLen chars, inclusive) 
   *  which consists of random alpha-characters. The returned string matches
   *  the regex "[A-Za-z]{$minLen,$maxLan}". 
   * @nb: excludes the chars "[\]^_`" between 'Z' and 'a', ie chars (91..96).
   * @see: http://www.neurophys.wisc.edu/comp/docs/ascii.html
   */
  public String nextString(int minLen, int maxLen)
    throws IllegalArgumentException
  {
    if(minLen < 0) throw new IllegalArgumentException("minLen must be >= 0");
    if(minLen > maxLen) throw new IllegalArgumentException("minLen must be <= maxLen");
    return(nextString(minLen, maxLen, 'A', 'z', '[', '`'));
  }

  /**
   * @does: generates a String (of between minLen and maxLen chars, inclusive) 
   *  which consists of characters between lo and hi, inclusive.
   */
  public String nextString(int minLen, int maxLen, char lo, char hi)
    throws IllegalArgumentException
  {
    if(lo < 0) throw new IllegalArgumentException("lo must be >= 0");
    String retval = null;
    try {
      int n = minLen==maxLen ? maxLen : nextInt(minLen, maxLen);
      byte b[] = new byte[n];
      for (int i=0; i<n; i++)
        b[i] = (byte)nextInt((int)lo, (int)hi);
      retval = new String(b, Charset.defaultCharset().name());
    } catch (Exception e) {
      e.printStackTrace();
    }
    return retval;
  }

  /**
   * @does: generates a String (of between minLen and maxLen chars, inclusive) 
   *  which consists of characters between lo and hi, inclusive, but excluding
   *  character between 
   */
  public String nextString(int minLen, int maxLen, char lo, char hi, char xlo, char xhi) 
    throws IllegalArgumentException
  {
    if(lo < 0) throw new IllegalArgumentException("lo must be >= 0");
    String retval = null;
    try {
      int n = minLen==maxLen ? maxLen : nextInt(minLen, maxLen);
      byte b[] = new byte[n];
      for (int i=0; i<n; i++) {
        b[i] = (byte)nextInt((int)lo, (int)hi, (int)xlo, (int)xhi);
      }
      retval = new String(b, Charset.defaultCharset().name());
    } catch (Exception e) {
      e.printStackTrace();
    }
    return retval;
  }

}

文件还提到这个集合是由映射支持的,因此对映射的更改会反映在集合中。因此,这可能意味着我的问题的答案是:是的,测试是必要的... - Jérôme Verstrynge
好的,这证明了观点。测试是必要的。非常感谢你的努力!!! - Jérôme Verstrynge
2
不,这并不能证明你的观点。如果你从地图中取出一个键,那么它将是你放入的键之一。如果你没有将“null”作为键放入,那么你就不会取回“null”。 - JimN
@Jimmy:嗯... 好观点!!! 我没有想到过。看起来我们需要一个在“垃圾回收器正在运行”时迭代缓存的测试...我仍然不希望返回空键,但我怀疑在我开始迭代(并且GC启动)之前会有少量条目/键。叹气。 - corlettk
@JimN "如果你从映射中获取一个键,那它将是你放入的键之一。"好的,但你确定在这之间不会进行垃圾回收吗?你确定for each不会返回空值,因为底层集合已被修改了吗? - Jérôme Verstrynge
WeakHashMap是非常差的缓存。弱引用的对象会立即被回收。你必须使用软引用... - Viliam

1
假设您没有将null键值插入到WeakHashMap中,那么在遍历键集时,无需检查迭代的键值是否为null。在遍历条目集时,也无需检查对迭代的Map.Entry实例调用getKey()是否为null。这两个都有文档保证,但有点间接;是Iterator.hasNext()的契约提供了这些保证。 WeakHashMap的JavaDoc声明:
每个WeakHashMap中的键对象都作为弱引用的引用者间接存储。因此,只有在垃圾收集器清除了它内部和外部的所有弱引用后,才会自动删除键。
Iterator.hasNext()的JavaDoc声明:

如果迭代器有更多元素,则返回true。(换句话说,如果next()将返回一个元素而不是抛出异常,则返回true。)

因为键集和条目集视图符合Set协议(根据WeakHashMap实现的Map协议所需),所以Set.iterator()方法返回的迭代器必须符合Iterator协议。

当 hasNext() 返回 true 时,Iterator 协议要求在 Iterator 实例上对 next() 的下一次调用必须返回有效值。WeakHashMap 满足 Iterator 协议的唯一方法是使 hasNext() 实现在返回 true 时保留对下一个 key 的强引用,从而防止 WeakHashMap 持有的 key 值的弱引用被垃圾回收器清除,并且,因此,防止条目自动从 WeakHashMap 中删除,以便 next() 有一个值可返回。

实际上,如果你查看WeakHashMap的源代码,你会发现HashIterator内部类(由键、值和条目迭代器实现使用)具有一个currentKey字段,它保存对当前键值的强引用,以及一个nextKey字段,它保存对下一个键值的强引用。 currentKey字段允许HashIterator完全遵守Iterator.remove()方法的合同。 nextKey字段允许HashIterator满足hasNext()的合同。

话虽如此,假设您想通过调用toArray()收集映射中的所有键值,然后迭代该键值的快照。 有几种情况需要考虑:

  1. If you call the no-argument toArray() method that returns Object[] or pass in a zero-length array, as in:

    final Set<MyObject> items = hm.keySet();
    for (final MyObject item : items.toArray(new MyObject[0])) {
        // Do something...
    }
    

    .. then you do not need to check whether item is null because in both cases, the array returned will be trimmed to hold the exact number of elements that were returned by the iterator.

  2. If you pass in an array of length >= the WeakHashMap's then-current size, as in:

    final Set<MyObject> items = hm.keySet();
    for (final MyObject item : items.toArray(new MyObject[items.size()])) {
        if (null == item) {
            break;
        }
        // Do something...
    }
    

    .. then the null check is necessary. The reason is that, in the time between when size() returned a value (which is used to create the array to store the keys) and toArray() finishes iterating through the keys of the WeakHashMap, an entry may have been automatically removed. This is the "room to spare" case mentioned in the JavaDoc for Collection.toArray():

    If this collection fits in the specified array with room to spare (i.e., the array has more elements than this collection), the element in the array immediately following the end of the collection is set to null. (This is useful in determining the length of this collection only if the caller knows that this collection does not contain any null elements.)

    Because you know that you have not inserted a null key value into the WeakHashMap, you can break upon seeing the first null value (if you see a null).

  3. A slight variant of the previous case, if you pass in an array of non-zero length, then you need the null check for the reason that the "room to spare" case might happen at runtime.


1

WeakHashMap允许使用null作为键和值。您可以添加null键和值。因此,如果您没有插入null条目,则不需要添加null检查


0
根据WeakHashMap的文档,放入哈希映射表中的键是一个模板类型,这意味着它继承自java.lang.object。因此,它可能为空。所以,一个键可能为空。

@Zach 好的,但是如果我们知道地图中没有添加空条目怎么办? - Jérôme Verstrynge
不知道你所说的模板类型是什么意思。仅仅因为键的类型是java.lang.Object(或某个子类型),并不意味着null是一个可接受的键。 - JimN
@JVerstry:如果您知道没有任何键是空的,那么keySet将不会给您任何空值。 - Zach Rattner
@Zach 你能百分之百确定返回的键不会为空吗?WeakHashMap中的条目可以随时被GC删除。问题是,它是否会影响keySet以及如何影响? - Jérôme Verstrynge
@JVerstry:来自http://download.oracle.com/javase/6/docs/api/java/util/WeakHashMap.html#keySet(), "该集合由映射支持,因此对映射的更改会反映在该集合中,反之亦然。" - Zach Rattner
2
我非常确定。GC可以删除整个条目,但如果条目存在(即未被删除),它将包含插入的键和值。请查看内部接口Map.Entry。 - JimN

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接