常常看到一些总结,比如HashSet允许Null值,HashTable则不允许。死记硬背固然可以,但是根据艾宾浩斯遗忘曲线,对于不理解原理的知识我们总是最快遗忘的。正好最近在看集合框架相关的源码,所以想整理分享出来。

整个集合框架代码量非常大,如果事无巨细的全部罗列非常耗时,而且仍然避免不了看完就忘。针对集合类的特点,准备这样进行整理。首先以一个具体类为切入点,说明它的继承关系。接着,说明这个类内部的存储结构,比如HashMap,就说明它内部是怎么实现元素的存储和各种操作的。最后,回到文初提到的那些总结,从源码角度说明HashTable是怎么实现不允许Null值插入的。通过这样的学习过程,我们应该就会对这些集合类的重点了如指掌了。

HashSet

HashSet是什么
HashSet的本质是一个Set。Set的就是一类数据的集合,并且内部的数据不能重复。

HashSet的继承关系
HashSet
HashSet继承自AbstractSet类并实现了Set接口。

HashSet的内部实现
首先看一下HashSet的构造函数

1
2
3
public HashSet(int initialCapacity) {
map = new HashMap<>(initialCapacity);
}

在HashSet的构造函数中,新建了一个initialCapacity大小的HashMap。
再看HashSet的add方法,代码如下

1
2
3
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}

其实就是在刚才构造函数中新建的HashMap中插入了一个Key值,而PRESENT其实是

1
private static final Object PRESENT = new Object();

所以HashSet的所有元素都是在其内部成员变量HashMap的Key中进行储存和管理,而这些Key的Value其实都一样,就是一个Object()。
如此一来,就可以理解了,对HashSet的插入和删除等操作,都可以通过这个map的方法进行管理,可以看一下其他操作的一些实现。

1
2
3
4
5
6
7
public boolean remove(Object o) {
return map.remove(o)==PRESENT;
}
public void clear() {
map.clear();
}

那些难背的特性
开头的时候说了HashSet允许Null值但不允许重复的值。
HashSet的内部实现是一个HashMap的Key值存储的,所以问题转化为了HashMap的Key值是允许Null值但不允许重复的值。那接下来就看HashMap吧。

HashMap

HashMap是什么
HashMap的本质是一个Map。Map的就是一类键值对数据的集合,它有如下的特性。

  1. Key值不能重复
  2. Key值和Value值可以为Null

HashMap的继承关系
HashMap
HashMap继承自AbstractMap并实现了Map接口。

HashMap的内部实现
首先看一下HashMap的构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY) {
initialCapacity = MAXIMUM_CAPACITY;
} else if (initialCapacity < DEFAULT_INITIAL_CAPACITY) {
initialCapacity = DEFAULT_INITIAL_CAPACITY;
}
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
// Android-Note: We always use the default load factor of 0.75f.
// This might appear wrong but it's just awkward design. We always call
// inflateTable() when table == EMPTY_TABLE. That method will take "threshold"
// to mean "capacity" and then replace it with the real threshold (i.e, multiplied with
// the load factor).
threshold = initialCapacity;
init();
}
```
HashMap的构造函数其实就是检查了主要的两个参数,并给threshold赋值为initialCapacity,并未涉及到内部存储结构的初始化,因为init()是一个空方法我这边就不贴了。接下来看添加元素的方法。

public V put(K key, V value) {
    if (table == EMPTY_TABLE) {
        inflateTable(threshold);
    }
    if (key == null)
        return putForNullKey(value);
    int hash = sun.misc.Hashing.singleWordWangJenkinsHash(key);
    int i = indexFor(hash, table.length);
    for (HashMapEntry<K,V> e = table[i]; e != null; e = e.next) {
        Object k;
        if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
            V oldValue = e.value;
            e.value = value;
            e.recordAccess(this);
            return oldValue;
        }
    }

    modCount++;
    addEntry(hash, key, value, i);
    return null;
}
1
初次添加元素时table肯定是为空的,此时要调用inflateTable(threshold)方法。
private void inflateTable(int toSize) {
    // Find a power of 2 >= toSize
    int capacity = roundUpToPowerOf2(toSize);

    // Android-changed: Replace usage of Math.min() here because this method is
    // called from the <clinit> of runtime, at which point the native libraries
    // needed by Float.* might not be loaded.
    float thresholdFloat = capacity * loadFactor;
    if (thresholdFloat > MAXIMUM_CAPACITY + 1) {
        thresholdFloat = MAXIMUM_CAPACITY + 1;
    }

    threshold = (int) thresholdFloat;
    table = new HashMapEntry[capacity];
}
1
2
在最后一行可以看到此时初始化了一个HashMapEntry数组,没错,它就是HashMap内部存储元素的数据结构。
接下来我们具体看一下HashMapEntry这个类,它是HashMap的内部静态类,具体实现如下:
static class HashMapEntry<K,V> implements Map.Entry<K,V> {
    final K key;
    V value;
    HashMapEntry<K,V> next;
    int hash;

    /**
     * Creates new entry.
     */
    HashMapEntry(int h, K k, V v, HashMapEntry<K,V> n) {
        value = v;
        next = n;
        key = k;
        hash = h;
    }

    public final K getKey() {
        return key;
    }

    public final V getValue() {
        return value;
    }

    public final V setValue(V newValue) {
        V oldValue = value;
        value = newValue;
        return oldValue;
    }

    public final boolean equals(Object o) {
        if (!(o instanceof Map.Entry))
            return false;
        Map.Entry e = (Map.Entry)o;
        Object k1 = getKey();
        Object k2 = e.getKey();
        if (k1 == k2 || (k1 != null && k1.equals(k2))) {
            Object v1 = getValue();
            Object v2 = e.getValue();
            if (v1 == v2 || (v1 != null && v1.equals(v2)))
                return true;
        }
        return false;
    }
    ...
}
1
2
3
4
5
6
可以看到HashMapEntry实现了Map.Entry<K,V>的接口,通过内部的Key和Value来存储键值对,并提供了获取Key、Value和equals等方法。
而HashMap就是通过维护HashMapEntry数组来存储数据元素的。
***那些难背的特性***
HashMap允许Null值但不允许重复的值。而HashSet的内部实现是一个HashMap的Key值存储的,这时该解开谜底了。
还记得HashSet的添加元素调用了HashMap的put方法,HashMap的put方法我上面贴过了,刚刚只看了HashMapEntry数组初始化的处理,而在这之后,可以看到当添加元素的Key值为Null时,会调用putForNullKey(value)。
private V putForNullKey(V value) {
    for (HashMapEntry<K,V> e = table[0]; e != null; e = e.next) {
        if (e.key == null) {
            V oldValue = e.value;
            e.value = value;
            e.recordAccess(this);
            return oldValue;
        }
    }
    modCount++;
    addEntry(0, null, value, 0);
    return null;
}
1
2
3
4
5
6
7
8
9
10
11
12
首先会遍历HashMapEntry<K,V>数组,如果已经存在为Null的Key,则会用新的值去替换。如果目前数组中没有Null Key,则会添加这个key为null的键值对,这就说明了HashMap是允许key为null,但key值是不会重复的。而HashSet则允许元素为null但元素不能重复。
## HashTable
***HashTable是什么***
HashTable的本质是一个Map,是一类键值对的集合,并且内部的数据不能重复。
***HashTable的继承关系***
![HashTable](../images/HashTable.png)
HashTable实现了Dictionary接口。
***HashTable的内部实现***
首先看一下HashTable的构造函数
public Hashtable(int initialCapacity, float loadFactor) {
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal Capacity: "+
                                           initialCapacity);
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new IllegalArgumentException("Illegal Load: "+loadFactor);

    if (initialCapacity==0)
        initialCapacity = 1;
    this.loadFactor = loadFactor;
    table = new HashtableEntry[initialCapacity];
    threshold = (initialCapacity <= MAX_ARRAY_SIZE + 1) ? initialCapacity : MAX_ARRAY_SIZE + 1;
}
1
2
3
4
5
HashTable的内部实现是一个HashtableEntry[]数组,通过这个数组来实现内部元素的管理。它的实现其实和HashMapEntry比较类似,也是实现了Map.Entry<K, V>接口,内部管理了K,V的键值。
***那些难背的特性***
HashTable不允许键或值为null,下面从源码角度解释一下
看一下put方法的实现

public synchronized V put(K key, V value) {
// Make sure the value is not null
if (value == null) {
throw new NullPointerException();
}

// Makes sure the key is not already in the hashtable.
HashtableEntry tab[] = table;
int hash = hash(key);
int index = (hash & 0x7FFFFFFF) % tab.length;
...

}
```
当value为null时会抛出空指针异常,如果key为null呢,似乎没有处理。其实,在下面去key的hash值时,hash(key)的实现会调用key.hashCode(),可想而知如果key为null在这边就会因为空指针程序异常退出了。所以HashTable是不允许插入key或者value为null的元素的。

总结

这一次我们从源码角度分析了HashSet,HashMap,HashTable的内部存储结构和一些特性的原理解释,相信大家对这些概念的理解应该不只是停留在文字层面了。本篇初版暂时写这么多,后续有补充的话再进行更新。

String,StringBuilder 以及 StringBuffer 区别

这3个类我们处理字符串数据类型经常会碰到的类,但当面试官问它们之间的区别时,你是否能够快速答出并解释原理呢。如果答案是No的话,这篇文章值得一读。

String

String类实现了Serializable,Comparable和CharSequence接口,具体类图如下所示:
String代表了一个字符串,并且是不可变的。
String类的构造函数不可用,它提供了一系列操作字符串的方法,利用这些方法我们可以得心应手的处理字符串。
String类提供了2个比较方法,分别是

1
2
3
4
5
public native int compareTo(String anotherString);
public int compareToIgnoreCase(String str) {
return CASE_INSENSITIVE_ORDER.compare(this, str);
}

StringBuilder

StringBuilder是一个可变的字符串。相比于StringBuffer,StringBuilder大部分实现效率都更好。StringBuilder最常用的方法就是在尾部添加的append方法和在任意部分添加的insert方法。
要注意StringBuilder的实例是线程不安全的。如果有同步需求的话更推荐使用StringBuffer。
StringBuilder继承自AbstractStringBuilder,并实现了Serializable,Appendable和CharSequence接口
StringBuilder的内部存储结构是一个char型数组,具体实现在它的父类AbstractStringBuilder中。
这里看到了Arrays.copyOf(char[] original, int newLength)方法,此方法调用System.arraycopy()来实现数组拷贝,具体就是把orginal数组拷贝到以newLength为长度的dst数组,如果newLength

1
2
3
4
5
6
7
8
9
10
11
private StringBuilder append(StringBuilder sb) {
if (sb == null)
return append("null");
int len = sb.length();
int newcount = count + len;
if (newcount > value.length)
expandCapacity(newcount);
sb.getChars(0, len, value, count);
count = newcount;
return this;
}

sb.getChars内部调用的还是System.arraycopy()方法,通过更改char型数组来实现更改内部存储内容的目的。

StringBuffer

StringBuffer的继承关系和StringBuilder是一致的,内部同样通过char型数组来实现字符串的操作与管理。

总结

  1. String是不可变的,字符串改变时会创建新的String对象;StringBuilder和StringBuffer是可变的。
  2. StringBuilder是线程不安全的,而StringBuffer是线程安全的,适合多线程下使用。