随着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涉及弱引用,待了解后再补充吧。

由于本人知识水平有限,如有遗留错误之处请指正。

java中的Collection集合类的更多相关文章

  1. Java中关于泛型集合类存储的总结

    集合类存储在任何编程语言中都是很重要的内容,只因有这样的存储数据结构才让我们可以在内存中轻易的操作数据,那么在Java中这些存储类集合结构都有哪些?内部实现是怎么样?有什么用途呢?下面分享一些我的总结 ...

  2. java中集合Collection转list对象

    参考:java中集合Collection转list对象 首先我的需求是获取到购物车列表,购物车列表是一个Map对象,构造方法获取购物项,这里购物项是Collection对象 // 购物项集合,K商品I ...

  3. JAVA 中的 Collection 和 Map 以及相关派生类的概念

    JAVA中Collection接口和Map接口的主要实现类   Collection接口 Collection是最基本的集合接口,一个Collection代表一组Object,即Collection的 ...

  4. java中的collection小结

    Collection 来源于Java.util包,是非常实用常用的数据结构!!!!!字面意思就是容器.具体的继承实现关系如下图,先整体有个印象,再依次介绍各个部分的方法,注意事项,以及应用场景.   ...

  5. ——Java中的collection和collections的区别

    1.java.util.Collection 是一个集合接口(集合类的一个顶级接口).它提供了对集合对象进行基本操作的通用接口方法.Collection接口在Java 类库中有很多具体的实现.Coll ...

  6. JAVA中的数据结构——集合类(序):枚举器、拷贝、集合类的排序

    枚举器与数据操作 1)枚举器为我们提供了访问集合的方法,而且解决了访问对象的“数据类型不确定”的难题.这是面向对象“多态”思想的应用.其实是通过抽象不同集合对象的共同代码,将相同的功能代码封装到了枚举 ...

  7. JAVA中的数据结构——集合类(线性表:Vector、Stack、LinkedList、set接口;键值对:Hashtable、Map接口<HashMap类、TreeMap类>)

    Java的集合可以分为两种,第一种是以数组为代表的线性表,基类是Collection:第二种是以Hashtable为代表的键值对. ... 线性表,基类是Collection: 数组类: person ...

  8. java 中的Collection

    /* *一. Collection?-------->容器! * * 1.来源于java.util包 非常实用的数据结构! * *二. 方法? * * void clear()删除集合中所有元素 ...

  9. java中的Collection和Collections

    Collection是集合类的上级接口,继承他的接口主要有Set和List. Collections是针对集合类的一个帮助类,他提供一系列静态方法实现对各种集合的搜索.排序.线程安全化等操作.

随机推荐

  1. jQuery时间格式插件-moment.js的使用

    jQuery时间格式插件-moment.js的使用 moment.js插件的使用,使用之前在页面引入对应的js文件: 详细的操作可见moment中文官网:http://momentjs.cn/ 日期格 ...

  2. JVM内存划分简介

    参考:深入理解JAVA虚拟机(第二版)

  3. Spring @Scheduled 在tomcat容器里面执行两次

    今天在用spring里面的@Scheduled执行定时任务,但是发现到触发定时任务的时间点总会执行两次.原因是修改了tomcat conf包下面的server.xml文件导致的.配置如下: <H ...

  4. java—— finall 关键词

    _ *{ margin: 0; padding: 0; } .on2{ margin: 10px 0; cursor: pointer; user-select: none; color: white ...

  5. python_如何对字典进行排序?

    案例: 某班英语成绩以字典的形式存储为: {'lili':78, 'jin':50, 'liming': 30, ......} 依据成绩高低,进行学生成绩排名 如何对字典排序? 方法1: #!/us ...

  6. CDuiString和String的转换

    很多时候 难免用到CDuiString和string的转换. 我们应该注意到,CDuiString类有个方法: LPCTSTR GetData() const; 可以通过这个方法,把CDuiStrin ...

  7. jquery取前、后、父、子元素

    前.prev(); 后.next(); 父.parent(); 子.children(); 注意:前的前是.prev().prev(),例如前元素无i,但前的前的i元素有i,不能写成.prev('i' ...

  8. asp.net web api 向客户端返回错误信息

    1使用Http状态码 ASP.NET Web Api框架提供了Http状态码的值,如下图所示. 虽然有这些预定义的状态码,但在实际项目中使用自定状态码结合预定义状态码更有优势. 通过在适当的位置抛出异 ...

  9. 查询集API -- Django从入门到精通系列教程

    该系列教程系个人原创,并完整发布在个人官网刘江的博客和教程 所有转载本文者,需在顶部显著位置注明原作者及www.liujiangblog.com官网地址. Python及Django学习QQ群:453 ...

  10. BZOJ 3265: 志愿者招募加强版 [单纯形法]

    传送门 一个人多段区间,一样.... 不过国家队论文上说这道题好像不能保证整数解.... #include <iostream> #include <cstdio> #incl ...