为什么用空元素数组创建ArrayList却用空表创建HashSet?

时间:2019-03-10 14:06:27

标签: java c#

也许是一个哲学问题。

在查看Java的 ArrayList 实现时,我注意到在创建新实例时,内部的“ elementData”数组(用于保存项目)被创建为新的空数组:

private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

但是,会使用表创建一个 HashSet (基于HashMap),而entreySet只是保留为空;

transient Node<K,V>[] table;
transient Set<Map.Entry<K,V>> entrySet;

public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}

这让我开始思考,所以我去查找了C#的List和HashSet: https://referencesource.microsoft.com/#mscorlib/system/collections/generic/list.cs,61f6a8d9f0c40f6e https://referencesource.microsoft.com/#System.Core/System/Collections/Generic/HashSet.cs,2d265edc718b158b

列表

static readonly T[]  _emptyArray = new T[0]; 

public List() {
        _items = _emptyArray;
}

哈希集

private int[] m_buckets;

public HashSet()
        : this(EqualityComparer<T>.Default) { }

public HashSet(IEqualityComparer<T> comparer) {
    if (comparer == null) {
        comparer = EqualityComparer<T>.Default;
    }

    this.m_comparer = comparer;
    m_lastIndex = 0;
    m_count = 0;
    m_freeList = -1;
    m_version = 0;
}

那么,为什么这两种语言的列表都为空而集合/映射为null是有充分的理由吗?

他们都为空数组技巧使用了“单个实例”,这很好,但是为什么不仅仅拥有一个空数组呢?

2 个答案:

答案 0 :(得分:1)

elementData初始化为ArrayList中的空数组可以避免在null方法中进行grow(int minCapacity)检查,该方法调用:

elementData = Arrays.copyOf(elementData, newCapacity);

增加后备阵列的容量。首次调用该方法时,该语句会将空数组“复制”到新数组的开头(实际上它不会复制任何内容)。

HashMap中,类似的策略将无济于事,因为当您调整存储桶数组的大小时,您无需将原始数组复制到新数组的开头,则必须遍历所有条目,并找到每个条目的新存储桶。因此,将buckets数组初始化为空数组而不是将其保持为null会要求您检查数组的length == 0而不是检查其是否为null。用另一种条件代替一个条件将不会有用。

答案 1 :(得分:1)

从C#角度进行回答。

对于一个空的ArrayList,如果您有一个空数组作为后备存储,您会发现所有逻辑(获取,添加,增长,...)都按原样工作。无需其他代码来处理未初始化的情况,这使整个实现变得更加整洁。而且由于空数组被缓存,因此不会导致额外的堆分配,因此您可以免费获得更干净的代码。

对于HashSet,这是不可能的,因为访问存储区是通过公式hashCode % m_buckets.Length完成的。尝试计算% 0被视为除以0,因此无效。这意味着您需要专门处理“未初始化”的情况,因此通过为空数组预分配字段不会获得任何好处。