随着1998年JDK 1.2的发布,同时新增了常用的Collections集合类,包含了Collection和Map接口。而Dictionary类是在1996年JDK 1.0发布时就已经有了。它们都可以在rt.jar这个基础类库包中找到。全文以JDK8为例,尝试介绍Collections集合类的相关内容。
它们的关系如上图所示,标蓝的为抽象类,实线全箭头指的是extends(继承),虚线全箭头表示implement(实现),虚线半箭头依赖指的是这个类里面有依赖接口或者类的成员变量,比如HashSet类,继承AbstractSet抽象类,它里面又定义了HashMap的成员变量:
public class HashSet<E> extends AbstractSet<E> implements Set<E>, Cloneable, java.io.Serializable {
private transient HashMap<E,Object> map; ... }
一、Collection接口下的List和Set
首先Collection接口继承Iterable接口,主要定义了size、isEmpty、contains、toArray、add、remove、clear、equals、hashCode等方法
List接口继承Collection接口,新增了sort、get、set、indexof、lastindexof、subList等方法,Set接口也继承Collection接口,但没有List这些新增方法
先从语义上看,List表达列表的意思,Set表示集合的意思。两者区别在于:List元素有序、不唯一,Set元素无序、唯一。
List
List接口下有LinkedList、ArrayList、Vector实现类:
LinkedList在JDK1.7之后,从单向链表结构Entry变成了双向链表节点Node数据结构实现的,一个Node节点有前后节点,JDK源码如下(为节省篇幅,去掉了相关注释,挑了重点的成员变量和成员函数,后同)
public class LinkedList<E> extends AbstractSequentialList<E> implements List<E>, Deque<E>, Cloneable, java.io.Serializable { transient int size = 0;
transient Node<E> first;
transient Node<E> last; private void linkFirst(E e) { final Node<E> f = first; final Node<E> newNode = new Node<>(null, e, f); first = newNode; if (f == null) last = newNode; else f.prev = newNode; size++; modCount++; } void linkLast(E e) { final Node<E> l = last; final Node<E> newNode = new Node<>(l, e, null); last = newNode; if (l == null) first = newNode; else l.next = newNode; size++; modCount++; } public boolean add(E e) { linkLast(e); return true; } public void add(int index, E element) { checkPositionIndex(index); if (index == size) linkLast(element); else linkBefore(element, node(index)); } private static class Node<E> { E item; Node<E> next; Node<E> prev; Node(Node<E> prev, E element, Node<E> next) { this.item = element; this.next = next; this.prev = prev; } } ... }
ArrayList和Vector使用变长数组实现的,但ArrayList非线程安全,达到数组长度时每次扩大50%:
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
private static final int DEFAULT_CAPACITY = 10; transient Object[] elementData; // non-private to simplify nested class access private int size;
public void ensureCapacity(int minCapacity) { int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) ? 0 : DEFAULT_CAPACITY; if (minCapacity > minExpand) { ensureExplicitCapacity(minCapacity); } } private static int calculateCapacity(Object[] elementData, int minCapacity) { if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { return Math.max(DEFAULT_CAPACITY, minCapacity); } return minCapacity; } private void ensureCapacityInternal(int minCapacity) { ensureExplicitCapacity(calculateCapacity(elementData, minCapacity)); } private void ensureExplicitCapacity(int minCapacity) { modCount++; if (minCapacity - elementData.length > 0) grow(minCapacity); } private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8; private void grow(int minCapacity) { int oldCapacity = elementData.length; int newCapacity = oldCapacity + (oldCapacity >> 1); if (newCapacity - minCapacity < 0) newCapacity = minCapacity; if (newCapacity - MAX_ARRAY_SIZE > 0) newCapacity = hugeCapacity(minCapacity); elementData = Arrays.copyOf(elementData, newCapacity); } private static int hugeCapacity(int minCapacity) { if (minCapacity < 0) // overflow throw new OutOfMemoryError(); return (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE; } public boolean add(E e) { ensureCapacityInternal(size + 1); // Increments modCount!! elementData[size++] = e; return true; } public void add(int index, E element) { rangeCheckForAdd(index); ensureCapacityInternal(size + 1); // Increments modCount!! System.arraycopy(elementData, index, elementData, index + 1, size - index); elementData[index] = element; size++; } ... }
代码稍长,主要看add增加元素时调用ensureCapacityInternal,最后进入grow方法里
int newCapacity = oldCapacity + (oldCapacity >> 1);
这里newCapacity就是oldCapacity的1.5倍,再往下判断一下是否仍比传过来的新的长度minCapacity小(1增长到2时出现),再往下与最大整型Integer.MAX_VALUE相比进行处理,ArrayList数组最长也只能是Integer.MAX_VALUE。
Vector线程安全,达到数组长度时每次扩大一倍。除了public函数全部用synchronized修饰外,与ArrayList主要的不同的就是grow函数
private void grow(int minCapacity) { // overflow-conscious code int oldCapacity = elementData.length; int newCapacity = oldCapacity + ((capacityIncrement > 0) ? capacityIncrement : oldCapacity); if (newCapacity - minCapacity < 0) newCapacity = minCapacity; if (newCapacity - MAX_ARRAY_SIZE > 0) newCapacity = hugeCapacity(minCapacity); elementData = Arrays.copyOf(elementData, newCapacity); }
可以看到,如果我们没有定义capacityIncrement 增长步长的话,newCapacity就是oldCapacity+oldCapacity,就是原数组长度的两倍。
ArrayList和Vector每次插入元素都需要System.arraycopy或Arrays.copyOf一次,每次数组长度不足时就要扩长一次。所以,网上通常的说法是:
1. ArratList主要消耗在扩大数组长度和每次的copy上,随机插入删除的效率较低,但查询速度较快;
2. Vector由于线程安全效率更低,插入删除查询都比较慢;
3. LinkedList由于每个Node只知道前后Node,所以随机查询效率较低,每次都需要从first或last开始遍历查询,但插入删除效率较高,只需改变引用;
事实真的是这样吗?实际测试过才知道。
import java.util.*; public class Main { public static void main(String[] args) { List arrayList = new ArrayList(); Long start_time = System.currentTimeMillis(); for(int i = 0; i < 50000; i++){ arrayList.add(0,i); } Long end_time = System.currentTimeMillis(); System.out.println("ArrayList add time " + (end_time - start_time)); List linkedList = new LinkedList(); start_time = System.currentTimeMillis(); for(int i = 0; i < 50000; i++){ linkedList.add(0,i); } end_time = System.currentTimeMillis(); System.out.println("LinkedList add time " + (end_time - start_time)); start_time = System.currentTimeMillis(); for(int i = 0; i < 50000; i++){ arrayList.get(i); } end_time = System.currentTimeMillis(); System.out.println("ArrayList get time " + (end_time - start_time)); start_time = System.currentTimeMillis(); for(int i = 0; i < 50000; i++){ linkedList.get(i); } end_time = System.currentTimeMillis(); System.out.println("LinkedList get time " + (end_time - start_time)); } }
ArrayList add time 281
LinkedList add time 0
ArrayList get time 16
LinkedList get time 1500
以上是网上一般的测试例子。我们看结果确实是这样的,ArrayList比LinkedList插入慢,比LinkedList查询快。但是这里我有一个疑问,为什么要使用add(index,element)方法呢?而不直接用我们常用的add(element)方法?如果换成add(element)又是怎样的呢?
import java.util.*; public class Main { public static void main(String[] args) { List arrayList = new ArrayList(); Long start_time = System.currentTimeMillis(); for(int i = 0; i < 50000; i++){ arrayList.add(i); } Long end_time = System.currentTimeMillis(); System.out.println("ArrayList add time " + (end_time - start_time)); List linkedList = new LinkedList(); start_time = System.currentTimeMillis(); for(int i = 0; i < 50000; i++){ linkedList.add(i); } end_time = System.currentTimeMillis(); System.out.println("LinkedList add time " + (end_time - start_time)); } }
ArrayList add time 0
LinkedList add time 16
换成add(element)的时候,ArrayList比LinkedList快,那么ArrayList的add(index,element)是什么处理逻辑呢?我们再来看看ArrayList里的源码:
public boolean add(E e) { ensureCapacityInternal(size + 1); // Increments modCount!! elementData[size++] = e; return true; } public void add(int index, E element) { rangeCheckForAdd(index); ensureCapacityInternal(size + 1); // Increments modCount!! System.arraycopy(elementData, index, elementData, index + 1, size - index); elementData[index] = element; size++; }
可以看到,add(index,element)比add(e)主要多了System.arraycopy一句,这句的意思是把elementData数组在index下标后面的元素全部后移一位,我们例子里index是0,那么就是每次新增元素时,都要使eletmentData[0]后面的元素后移一位,然后把新元素放到eletmentData[0]上。
再来看看LinkedList的源码
public boolean add(E e) { linkLast(e); return true; } public void add(int index, E element) { checkPositionIndex(index); if (index == size) linkLast(element); else linkBefore(element, node(index)); } ... Node<E> node(int index) { // assert isElementIndex(index); if (index < (size >> 1)) { Node<E> x = first; for (int i = 0; i < index; i++) x = x.next; return x; } else { Node<E> x = last; for (int i = size - 1; i > index; i--) x = x.prev; return x; } }
可以看到,主要性能消耗是linkBefore(element, node(index))这句node(index),需要从first元素或者last元素(index与size相比,看index是在数组前半部分还是后半部分)找到特定index的node,拿到了这个node,修改前后元素的引用,修改引用消耗不大。所以如果index越靠近双向链表的中间,Linked的消耗越大。
这就很流氓了!add(0,element)是把ArrayList最坏的情况跟LinkedList最好的情况比较。我们回过头来看网上的说法,强调的是随机,那么我们再试试随机插入:
import java.util.*; public class Main { public static void main(String[] args) { Random random = new Random(); List arrayList = new ArrayList(); Long start_time = System.currentTimeMillis(); for(int i = 0; i < 50000; i++){ arrayList.add((arrayList.size() > 0) ? random.nextInt(arrayList.size()) : i, i); } Long end_time = System.currentTimeMillis(); System.out.println("ArrayList add time " + (end_time - start_time)); List linkedList = new LinkedList(); start_time = System.currentTimeMillis(); for(int i = 0; i < 50000; i++){ linkedList.add((linkedList.size() > 0) ? random.nextInt(linkedList.size()) : i,i); } end_time = System.currentTimeMillis(); System.out.println("LinkedList add time " + (end_time - start_time)); }
ArrayList add time 141
LinkedList add time 4187
以上例子按照ArrayList和LinkedList现有长度随机一个index去插入,我执行了多次,实际效果都是ArrayList比LinkedList快。
所以我的结论是:如果必须使用add(0,element)的时候,那就请使用LinkedList吧。
Set
Set下面主要有HashMap和TreeMap,先看看HashMap的源码
public class HashSet<E> extends AbstractSet<E> implements Set<E>, Cloneable, java.io.Serializable { private transient HashMap<E,Object> map; // Dummy value to associate with an Object in the backing Map private static final Object PRESENT = new Object(); public boolean add(E e) { return map.put(e, PRESENT)==null; } ... }
可以看到,HashSet实际就是利用HashMap存放元素的,将元素放入HashMap的Key里,利用HashMap的Key自动哈希散列来保证元素的唯一性,后面再详细聊聊HashMap如何进行存取元素的。
TreeSet目前没使用过,就先不说。后面研究过再补上。简要的理解是,TreeSet利用TreeMap实现,元素是有序的,和HashSet一样元素是唯一的。
回到上面所说的,List元素有序、不唯一,Set元素无序、唯一。唯一性大家应该能够理解,有序无序的意思是,LinkedList、ArrayList可以预知和控制元素排序,但HashSet根据元素哈希值存放的,不能根据插入的先后顺序控制。但这句话其实也是有问题的,因为TreeSet是有序的,估计因为不常用被忽略了吧。HashSet元素无序例子如下
import java.util.*; public class Main { public static void main(String[] args) { HashSet hashSet = new HashSet(); Random random = new Random(); for(int i = 0; i < 10; i++){ int r = random.nextInt(10000); System.out.print(r + " "); hashSet.add(r); } System.out.println("\n" + hashSet); } }
6192 4913 4415 6384 6593 1136 8666 2021 7117 8972 [6192, 6384, 1136, 4913, 6593, 2021, 8666, 8972, 7117, 4415]
二、Map接口下的HashMap和HashTable
Map接口主要定义了自身是Entry<key, value>这种键值对数据结构,包含size、isEmpty、containsKey、containsValue、get、put、remove、clear、keySet等方法。
Map接口下有AbstractMap抽象类,AbstractMap实现了部分方法,增加了SimpleEntry<key, value>数据结构
AbstractMap下面有HashMap、TreeMap、WeakHashMap这几个实现类。
而HashTable是继承Dictionary的和实现Map接口的。
先来看HashMap的源码:
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable { static class Node<K,V> implements Map.Entry<K,V> { final int hash; final K key; V value; Node<K,V> next; Node(int hash, K key, V value, Node<K,V> next) { this.hash = hash; this.key = key; this.value = value; this.next = next; } public final K getKey() { return key; } public final V getValue() { return value; } public final String toString() { return key + "=" + value; } public final int hashCode() { return Objects.hashCode(key) ^ Objects.hashCode(value); } public final V setValue(V newValue) { V oldValue = value; value = newValue; return oldValue; } public final boolean equals(Object o) { if (o == this) return true; if (o instanceof Map.Entry) { Map.Entry<?,?> e = (Map.Entry<?,?>)o; if (Objects.equals(key, e.getKey()) && Objects.equals(value, e.getValue())) return true; } return false; } } static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); } transient Node<K,V>[] table; transient Set<Map.Entry<K,V>> entrySet; transient int size; final float loadFactor; public HashMap() { this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted } public V get(Object key) { Node<K,V> e; return (e = getNode(hash(key), key)) == null ? null : e.value; } final Node<K,V> getNode(int hash, Object key) { Node<K,V>[] tab; Node<K,V> first, e; int n; K k; if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) { if (first.hash == hash && // always check first node ((k = first.key) == key || (key != null && key.equals(k)))) return first; if ((e = first.next) != null) { if (first instanceof TreeNode) return ((TreeNode<K,V>)first).getTreeNode(hash, key); do { if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } while ((e = e.next) != null); } } return null; } public boolean containsKey(Object key) { return getNode(hash(key), key) != null; } public V put(K key, V value) { return putVal(hash(key), key, value, false, true); } final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); else { Node<K,V> e; K k; if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; else if (p instanceof TreeNode) e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); else { for (int binCount = 0; ; ++binCount) { if ((e = p.next) == null) { p.next = newNode(hash, key, value, null); if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); break; } if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; } } ++modCount; if (++size > threshold) resize(); afterNodeInsertion(evict); return null; } public boolean containsValue(Object value) { Node<K,V>[] tab; V v; if ((tab = table) != null && size > 0) { for (int i = 0; i < tab.length; ++i) { for (Node<K,V> e = tab[i]; e != null; e = e.next) { if ((v = e.value) == value || (value != null && value.equals(v))) return true; } } } return false; } ... }
HashMap实现的复杂度跟前面的类不可同日而语,首先定义了一个静态的内部类Node,Node实现Map接口里的Entry<key, value>接口,并定义了hash变量和指向下一个Node的next变量,所以Node可以是一个单向链表,接着HashMap定义了Node<K,V>[] table 数组用来存放元素。首先看我们常用的存放元素函数put,它主要调用了putVal方法,概要的逻辑是:
1.判断初始化
2.利用元素的哈希值hash与(table长度-1)进行与&运算,得到新元素应该放在table数组的index下标
3.判断是否table数组index下标已经有值,有值则说明产生了碰撞,把新元素放在这个单向列表后端(深度最大为8,超过深度则转变成红黑树)
4.在获取table数组index下标的node时,和遍历单向链表时,如果发现key和已有的旧元素一致,则返回旧元素的值,不改变旧元素
5.判断是否需要扩容
这个putVal方法的详细逻辑是这样的:
if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length;
↑ 第一个if判断table和长度是否为空,为空就初始化扩容,顺便赋了值。
if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null);
↑ 第二个if使用元素的哈希值hash与(table长度-1)进行与&运算,得到一个index,如果这个tab[index]没有值,就直接把新的这个元素放到tab[index]上
if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p;
↑ 如果上面tab[index]上已经有值了,说明多个key的哈希值和(table长度-1)与运算的结果一样,产生了碰撞,需要把这个key放到这个tab[index]上p的单向链表上。else里第一个if判断原来tab[index]元素的hash与新元素的hash是否一样,如果它们的hash一样,那么判断key是不是一样的,如果连key也一样,那么保留为旧元素。
else if (p instanceof TreeNode) e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
↑ 第二个else if判断tab[index]上的p节点是否是红黑树TreeNode
else { for (int binCount = 0; ; ++binCount) { if ((e = p.next) == null) { p.next = newNode(hash, key, value, null); if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); break; } if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } }
↑ 这个第二层else表示如果不是TreeNode,则是单向链表节点,那么会进入这里,遍历这个单向链表,这里的第一个if判断单向链表tab[index]上的p的next是否为空,为空就直接p.next指向新元素,TREEIFY_THRESHOLD定义深度最大为8,如果达到最大深度,就把这个单向链表转换成红黑树TreeNode,break退出遍历。第二个if判断如果这个单向链表tab[index]上的p不为空,那么如果p的key和新元素的key一样,那么保留为旧元素,break退出遍历。最后如果p.next不为空,Key又不一致,那么p = e继续往下遍历。
if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; }
↑ 外层else最后这个if判断之前是否取出了相同hash和key的node节点,如果有已存在这样的node,就把旧node的value值返回。
++modCount; if (++size > threshold) resize(); afterNodeInsertion(evict); return null;
↑ 函数最后就是判断是否要扩容了。
扩容resize函数也值得研究一下,因为元素hash值与数组长度-1与运算的,一旦数组长度发送改变,那么按道理,原有元素的数组位置应该会发送改变,甚至单向链表、红黑树里的元素都会涉及重新调整位置。这部分后面研究过再写。
接着,我们来分析一下get方法,这个方法主要调用了getNode方法,我们知道了put是怎么把元素放到对应的位置的,那么getNode的时候就相应的把这个位置找到,就可以获取元素了:
1.根据key的hash值与table数组长度-1做与运算,获得数组下标index
2.判断是否和table[index]的元素的key一致,一致就返回
3.判断table[index]上是否是红黑树,是红黑树则去红黑树找这个元素
4.判断table[index]是一个单向链表,遍历单向链表找到这个元素
这个逻辑比putVal简单很多,就不详细一段一段代码分析了。
接下来看看HashTable,虽说HashTable实现了Map接口,但是这个类其实在JDK1.0就已经有了,应该只是后来重现实现了而已。
HashTable继承Dictionary抽象类,Dictionary与Map很相似,我的感觉是Dictionary是古老的Map,为了兼容以前的代码,HashTable才仍然继承Dictionary的,毕竟可能很多面向抽象编程这么定义:Dictionary hashTable = new HashTable();
先来看看HashTable的源码:
public class Hashtable<K,V> extends Dictionary<K,V> implements Map<K,V>, Cloneable, java.io.Serializable { private transient Entry<?,?>[] table; private transient int count; private int threshold; private float loadFactor; private transient int modCount = 0; public synchronized boolean contains(Object value) { if (value == null) { throw new NullPointerException(); } Entry<?,?> tab[] = table; for (int i = tab.length ; i-- > 0 ;) { for (Entry<?,?> e = tab[i] ; e != null ; e = e.next) { if (e.value.equals(value)) { return true; } } } return false; } public synchronized boolean containsKey(Object key) { Entry<?,?> tab[] = table; int hash = key.hashCode(); int index = (hash & 0x7FFFFFFF) % tab.length; for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) { if ((e.hash == hash) && e.key.equals(key)) { return true; } } return false; } public synchronized V get(Object key) { Entry<?,?> tab[] = table; int hash = key.hashCode(); int index = (hash & 0x7FFFFFFF) % tab.length; for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) { if ((e.hash == hash) && e.key.equals(key)) { return (V)e.value; } } return null; } private void addEntry(int hash, K key, V value, int index) { modCount++; Entry<?,?> tab[] = table; if (count >= threshold) { // Rehash the table if the threshold is exceeded rehash(); tab = table; hash = key.hashCode(); index = (hash & 0x7FFFFFFF) % tab.length; } // Creates the new entry. @SuppressWarnings("unchecked") Entry<K,V> e = (Entry<K,V>) tab[index]; tab[index] = new Entry<>(hash, key, value, e); count++; } 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. Entry<?,?> tab[] = table; int hash = key.hashCode(); int index = (hash & 0x7FFFFFFF) % tab.length; @SuppressWarnings("unchecked") Entry<K,V> entry = (Entry<K,V>)tab[index]; for(; entry != null ; entry = entry.next) { if ((entry.hash == hash) && entry.key.equals(key)) { V old = entry.value; entry.value = value; return old; } } addEntry(hash, key, value, index); return null; } private static class Entry<K,V> implements Map.Entry<K,V> { final int hash; final K key; V value; Entry<K,V> next; protected Entry(int hash, K key, V value, Entry<K,V> next) { this.hash = hash; this.key = key; this.value = value; this.next = next; } @SuppressWarnings("unchecked") protected Object clone() { return new Entry<>(hash, key, value, (next==null ? null : (Entry<K,V>) next.clone())); } // Map.Entry Ops public K getKey() { return key; } public V getValue() { return value; } public V setValue(V value) { if (value == null) throw new NullPointerException(); V oldValue = this.value; this.value = value; return oldValue; } public boolean equals(Object o) { if (!(o instanceof Map.Entry)) return false; Map.Entry<?,?> e = (Map.Entry<?,?>)o; return (key==null ? e.getKey()==null : key.equals(e.getKey())) && (value==null ? e.getValue()==null : value.equals(e.getValue())); } public int hashCode() { return hash ^ Objects.hashCode(value); } public String toString() { return key.toString()+"="+value.toString(); } } ... }
可以看到HashTable与HashMap很类似,HashTable的内部类Entry<key,value>也是实现Map的Entry<key,value>接口,也有一个hash成员变量和指向下一个Entry的next成员变量,也是一个单向链表。不同的在于,HashTable的public函数都用synchronized修饰,是线程安全的。
先来看看put函数,我们可以看到,重要的获取元素的数组下标Index的方式与HashMap有区别
index = (hash & 0x7FFFFFFF) % tab.length;
HashTable是这样处理的,0x7FFFFFFF的作用是,如果hash值是负数的话就把它变成正数,然后直接除数组长度tab.length取余,这样既保证index是正数,又保证比数组长度小。另外我们还可以看到,HashTable在put的时候产生碰撞时,并不会产生红黑树的处理。碰撞的时候先检查是否已有的key,如果遍历tab[index]单向链表里都没有,就把新的元素加在原来tab[index]上的元素的前面,tab[index]上就指向了新元素,新元素的next指向原来的元素。HashTable的单向链表没有深度控制。
get函数同样根据hash值找到index,遍历单向链表,找到元素。
TreeMap和WeakHashMap
TreeMap和WeakHashMap我也没有使用过,TreeMap和HashMap的区别主要特定是元素是有序的。WeakHashMap涉及弱引用,待了解后再补充吧。
由于本人知识水平有限,如有遗留错误之处请指正。