图中非常清楚的展示了java集合类中的各种依赖继承关系。所有的元素都实现了Iterator接口,用于遍历集合元素。集合分两大类,Collection和Map,Collection中又分List和Set,Map接口下有HashMap,Hashtable,TreeMap等。
目录
Iterator
Iterator模式用于遍历所有集合类的元素,他的设计是为了把所有的集合遍历逻辑抽出来,从而避免向客户暴露集合的内部结构。
1. Iterator
boolean hasNext():这个方法是在遍历的时候,判断是否还有更多的元素
E next(): 返回下一个元素
default void remove():这里涉及到了jdk8的特性,在接口定义中,将方法描述为default-虚拟扩展方法,就可以在接口中进行默认实现,从而提高接口的扩展性,避免在接口扩展的时候,破坏原有的实现。
default void forEachRemaining(Consumer<? super E> action):这个方面一般都用不到,不做具体描述。
2. ListIterator
ListIterator 是对Iterator的扩展,他增加了
boolean hasPrevious()
E previous()
int previousIndex()
void set(E e)
void add(E e) 5个方法。通过前3个方法可以进行向前遍历元素,后面两个set和add 可以插入元素,但是set是将元素插入到链表的最后位置,add是插入到当前返回的元素之前。
Collection
Collection作为一个集合类的顶层接口,他没有直接的子类实现,而是通过实现它的子接口来实现集合容器。Collection的特点是作为一个容器,他可以轻松的帮用户实现数据存储,动态扩容,还有方便的元素遍历和快速的增删改查等操作,这些特点都在接口定义的方法中一一体现出来,相比我们用array来存储数据方便不少。
Collection的子接口主要是三大类分别是List,Set和Queue。这三个接口各有特点。
1.List
是一个顺序存放的容器,他会保存元素的插入顺序,当然元素也可以通过下标位置直接插入和删除。List容器同时允许重复的元素插入和多个null元素。List提供了一个特殊的iterator,叫做ListIterator,这个接口在上文中也有所描述,可以进行双向遍历元素。
2.Set
集合最大的特点是元素不能重复,所有元素都是唯一的存在。Set集合不保证维护元素的顺序。
3.Queue
顾名思义就是队列,队列最大的特点就是FIFO先进先出,与之对应的有栈Stack后进先出。Queue在Collection的基础之上又新增加了几个方法:
offer(E e)与add方法类似,但是当容器存量超出达到上限以后,会插入失败,而报异常。这个方法推荐使用。
poll()返回并且删除队列的头元素,如果队列为空,返回null
element()返回但不删除头元素,如果队列为空,会报异常。
peek()返回但不删除头元素,如果队列为空,返回null。
Map
和Collection一样,Map也是集合容器的一个顶层接口。Map是通过key-value方式存储数据,key值都是唯一的,但key是否能为空,则要看他的不同子类的实现。我们可以把Map看成一个小型的数字字典,通过key值的方式存储数据性能非常快,比如他的子类Hashmap,底层就是通过散列表来实现存储,他的时间复杂度是O(1)。另一个典型的子类Treemap是基于红黑树实现的,时间复杂度为O(log n)。以下将介绍部分方法。
method | desc |
---|---|
size() | 获取容器中元素的个数 |
isEmpty() | 判断容器中是否保函元素,如果不包含元素,返回true |
containsKey(Object key) | 判断是否保函key值 |
containsValue(Object value) | 判断是否保函Value |
get(Object key) | 通过key值获取元素 |
put(K key, V value) | 将一个元素通过键值对的方式存入容器,如果以存在这个key,则会替换value。 |
remove(Object key) | 移除一个元素 |
putAll(Map<? extends K, ? extends V> m) | 将容器中所有元素拷贝到当前容器。 |
interface Entry | 这个内部类其实也是一个顶层接口,所有存入容器的元素首先都会被包装成一个entry元素。Map的不同子类都会实现这个接口,而且实现各不相同。原因很简单,每个子类都有独特的数据结构来存储数据,TreeMap用的是红黑树,HashMap用的是散列表,而LinkedHashMap则是用散列表+双向链表实现的。 |
三个类对Entry的部分实现
#TreeMap:
K key;
V value;
Entry<K,V> left = null;
Entry<K,V> right = null;
Entry<K,V> parent;
boolean color = BLACK;
#HashMap:
final int hash;
final K key;
V value;
Node<K,V> next;
#LinkedHashMap
static class Entry<K,V> extends HashMap.Node<K,V> {
Entry<K,V> before, after;
Entry(int hash, K key, V value, Node<K,V> next) {
super(hash, key, value, next);
}
}
AbstractMap
AbstractMap这个类没特别的地方,他相当于一个骨架,对Map中的接口方法做了一个最简化的实现,设计他的目的是为了最小化接口实现的压力。
SortedMap
SortedMap是Map的子接口,他是一个排序接口,所有实现他的子类都事排序的,通常都是根据key进行排序。因此所有key值都必须实现Comparator接口。下面列举几个他特有的排序方法。
method | desc |
---|---|
firstKey() | 获取第一个元素 |
lastKey() | 获取最后一个元素 |
ArrayList & Vector
- ArrayList和Vector都是List的实现类,他两处于同一个地位上的。他们所实现的功能大同小异,源码相似度90%以上。
- 他俩的区别,ArrayList是非线程安全,而Vector是线程安全的,那么表现在源码上是怎么样的区别呢?就是在每个ArrayList的方法前,加上synchronized。
// Vector的add方法
public synchronized boolean add(E e) {
modCount++;
ensureCapacityHelper(elementCount + 1);
elementData[elementCount++] = e;
return true;
}
// ArrayList的add方法
public boolean add(E e) {
ensureCapacityInternal(size + 1);
elementData[size++] = e;
return true;
}
3.他俩都实现了Iterator和LinkIterator接口,具有相同的遍历方式。
4.ArrayList和Vector都具有动态扩容的特性,唯一的区别是,ArrayList扩容后是原来的1.5倍。Vector中有一个capacityIncrement变量,每次扩容都在原来大小基础上增加capacityIncrement。如果capacityIncrement==0,那么就在原大小基础上再扩充一倍。
5.Vector中有一个方法setSize(int newSize),而ArrayList并没有,我觉得这个方法有点鸡肋。setSize允许用户主动设置容器大小,如果newSize小于当前size,那么elementData数组中只会保留newSize个元素,多出来的会设为null。如果newSize大于当前size,那么就扩容到newSize大小,数组中多出来的部分设为null,以后添加元素的时候,之前多出来的部分就会以null的形式存在。
Vector<Integer> v2 = new Vector<Integer>();
v2.add(1);
v2.setSize(3);
v2.add(3);
System.out.println(v2.size());
setSize之前:
[1]
setSize之后:
[1, null, null]
当我再次添加一个元素后:
[1, null, null, 3]
所以我觉得这个方法并没有太大实用意义。而且会是用户困惑,出现一些不必要的错误。
6.因为Vector是同步的,所以性能上肯定不如ArrayList,所以在不需要考虑多线程的环境下,建议使用ArrayList。
LinkedList
一、双向链表
ArrayList是通过数组实现存储,而LinkedList则是通过链表来存储数据,而且他实现的是一个双向链表,简单的说一下什么是双向链表。双向链表是数据结构的一种形式,他的每个节点维护两个指针,prev指向上一个节点,next指向下一个节点。这种结构有什么特点呢?他可以实现双向遍历,这使得在链表中的数据读取变得非常灵活自由。同时,LinkedList中维护了两个指针,一个指向头部,一个指向尾部。维护这两个指针后,可以使得元素从头部插入,也可以使元素从尾部插入。基于方式,用户很容易就能实现FIFO(队列),LIFO(栈)等效果。
1.Node节点定义:
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;
}
}
2.FIFO(队列)实现原理:
队列的原理就是每次都从链表尾部添加元素,从链表头部获取元素,就像生活中的排队叫号,总是有个先来后到。
// 队列尾部添加一个元素,建议使用这个,约定俗成吧。
public boolean offer(E e) {
return add(e);
}
// 队列尾部添加一个元素
public boolean offerLast(E e) {
addLast(e);
return true;
}
// offer和offerLast底层调用的都是linkLast这个方法,顾名思义就是将元素添加到链表尾部。
void linkLast(E e) {
final Node<E> l = last;
// 创建一个节点,将prev指针指向链表的尾节点。
final Node<E> newNode = new Node<>(l, e, null);
// 将last指针指向新创建的这个节点。
last = newNode;
if (l == null)
// 如果当前链表为空,那么将头指针也指向这个节点。
first = newNode;
else
// 将链表的尾节点的next指针指向新建的节点,这样就完整的实现了在链表尾部添加一个元素的功能。
l.next = newNode;
size++;
modCount++;
}
// 在链表头部删除一个元素,建议用这个,别问我为什么,我也不知道
public E poll() {
final Node<E> f = first;
return (f == null) ? null : unlinkFirst(f);
}
// 在链表头部删除一个元素
public E pollFirst() {
final Node<E> f = first;
return (f == null) ? null : unlinkFirst(f);
}
// poll和pollFirst底层调用的就是这个方法,将链表的头元素删除。
private E unlinkFirst(Node<E> f) {
// assert f == first && f != null;
final E element = f.item;
final Node<E> next = f.next;
f.item = null;
f.next = null; // help GC
first = next;
if (next == null)
last = null;
else
next.prev = null;
size--;
modCount++;
return element;
}
// 获取头元素,但是不会删除他。
public E peek() {
final Node<E> f = first;
return (f == null) ? null : f.item;
}
3.LIFO(栈)实现原理:
栈的原理是每次从头部添加元素,也从头部获取元素,那么后进入的元素反而最先出来。就像我们平时叠盘子,洗好了就一个一个往上放,然后要用了就从上往下一个一个拿。
// 在链表的头部添加一个元素
public void push(E e) {
addFirst(e);
}
// addFirst调用的就是linkFirst,这段代码就是实现将元素添加的链表头部。
private void linkFirst(E e) {
final Node<E> f = first;
// 创建一个新元素,将元素的next指针指向当前的头结点
final Node<E> newNode = new Node<>(null, e, f);
// 将头指针指向这个节点。
first = newNode;
if (f == null)
// 如果当前节点为空,则把尾指针指向这个节点。
last = newNode;
else
// 将当前头结点的prev指针指向此结点。
f.prev = newNode;
size++;
modCount++;
}
// 弹出顶部结点。
public E pop() {
return removeFirst();
}
// removeFirst调用的就是unlinkFirst,unlinkFirst实现将链表顶部元素删除
private E unlinkFirst(Node<E> f) {
// assert f == first && f != null;
final E element = f.item;
final Node<E> next = f.next;
f.item = null;
f.next = null; // help GC
first = next;
if (next == null)
last = null;
else
next.prev = null;
size--;
modCount++;
return element;
}
// 获取顶部结点,但是不删除
public E peek() {
final Node<E> f = first;
return (f == null) ? null : f.item;
}
不管是栈也好,队列也好,元素都是从头部删除的unlinkFirst方法。但是用户在使用的过程中并不只用到上面两张方式,我们也可以从链表尾部删除元素如removeLast,peekLast,pollLast,unlinkLast等方法。
二、存取操作
上文讲到的功能,其实是实现了Deque接口,而现在要讲述的是实现与List的部分功能。那么最典型的操作就是直接对容器元素的读取,因为List容器的一大特点就是顺序存储,元素在容器中的位置和存入时是保持一致的,那么用户在读取元素的时候理所当然就可以通过元素下标来获取,下面就具体介绍这几种方法。
1.将元素插入容器的指定位置
// 将元素插入指定位置
public void add(int index, E element) {
checkPositionIndex(index);
if (index == size)
// 如果位于末尾,则直接插入链表尾部
linkLast(element);
else
// 如果不是位于末尾,那么就将元素插入指定位置的元素之前,
linkBefore(element, node(index));
}
// 将元素插入指定元素前,链表插入元素是数据结构中最基础的知识,这里就不再赘述了。
void linkBefore(E e, Node<E> succ) {
// assert succ != null;
final Node<E> pred = succ.prev;
final Node<E> newNode = new Node<>(pred, e, succ);
succ.prev = newNode;
if (pred == null)
first = newNode;
else
pred.next = newNode;
size++;
modCount++;
}
2.获取指定位置元素
public E get(int index) {
checkElementIndex(index);
return node(index).item;
}
// 获取指定位置的元素,使用的方式就是对链表进行顺序遍历,直到指定位置位置,不过他的处理越有一些小技巧值得学习,这个技巧也是利用了双向链表的特性。
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;
}
}
三、迭代器实现
LinkedList的迭代器实现有两个,一个是实现了Iterator接口的DescendingIterator,另一个则是实现了ListIterator接口的ListItr。
1.ListItr
ListItr遍历需要指定一个起始值
public ListIterator<E> listIterator(int index) {
checkPositionIndex(index);
return new ListItr(index);
}
ListItr会创建一个以index为起始值的迭代器,然后用户便可以以这个位置为起点,实现向前或者向后遍历。
ListItr(int index) {
// 实例化的时候,将next指针指向指定位置的元素
next = (index == size) ? null : node(index);
nextIndex = index;
}
// 向后遍历
public E next() {
checkForComodification();
if (!hasNext())
throw new NoSuchElementException();
lastReturned = next;
next = next.next;
nextIndex++;
return lastReturned.item;
}
// 向前遍历
public E previous() {
checkForComodification();
if (!hasPrevious())
throw new NoSuchElementException();
lastReturned = next = (next == null) ? last : next.prev;
nextIndex--;
return lastReturned.item;
}
2.DescendingIterator
DescendingIterator迭代器实现的是对链表从尾部向头部遍历的功能,他复用里ListItr中的previous方法,将当前位置指向链表尾部,然后逐个向前遍历。
private class DescendingIterator implements Iterator<E> {
private final ListItr itr = new ListItr(size());
public boolean hasNext() {
return itr.hasPrevious();
}
public E next() {
return itr.previous();
}
public void remove() {
itr.remove();
}
}
HashSet
前言:
HashSet继承了AbstractSet抽象类并实现了Set接口,AbstractSet的子类还包括TreeSet。
1.HashMap提供键值对的方式存储数据,而HashSet仅仅提供数据存储,并没有键值对应。他获取元素的方式也只能通过遍历的方式逐个获取
2.HashMap在存入数据的时候是更加key值的hash值判断,而HashSet需要重写hashCode和equals两个方法,如果不重写则会调用默认的实现,用户在使用HashSet的时候要特别注意元素的euqals判断,有必要的话要重写一个,以免出现问题。
一、HashSet创建
HashSet的创建其实就是实例化一个HashMap,可以创建默认大小的HashMap实例,也可以指定initialCapacity和loadFactor。
public HashSet() {
map = new HashMap<>();
}
public HashSet(Collection<? extends E> c) {
map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));
addAll(c);
}
public HashSet(int initialCapacity, float loadFactor) {
map = new HashMap<>(initialCapacity, loadFactor);
}
public HashSet(int initialCapacity) {
map = new HashMap<>(initialCapacity);
}
二、在HashSet中添加元素
HashMap中真正存放元素的地方就是HashMap,但是HashMap是K-V的形式,而HashSet中没有Key,那么他们是如何将HashSet中的元素映射到HashMap中的呢?原来作者将HashSet中的元素存放到了map的key中,而value则存放一个无意义的Object对象。这么做的好处还可以保证HashSet中的value值唯一。了解HashMap的同学知道,HashMap中校验key值唯一性的方式是通过hash值,然后根据hash值定位数组中的位置,但是也存在hash冲突的情况,那么解决hash冲突的方式就是在原来数组的位置增加一个链表,将hash值冲突,但是key值不同的元素存放在链表中。那么在判断key值是否相同的时候就用到了equals方法。从上面的描述中可以抓出几个关键点,第一hash值,第二equals方法。这就是为什么我们需要将存入HashSet的元素重写hashCode和euqals方法的根本原因。
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
三、HashSet遍历
我找了HashSet的源码中找了一通,然并没有找到get方法,那么我们将如何获取元素呢?我发现了iterator接口,这个接口返回的Iterator并不是HashSet自己维护的Iterator,而是通过返回HashMap的keySet().Iterator,这个迭代器遍历的是HashMap的key值。也就是HashSet中保存的value。
public Iterator<E> iterator() {
return map.keySet().iterator();
}
TreeSet
前言:
Set接口有三个实现类,HashSet,LinkedHashSet和TreeSet。内部通过维护一个HashMap存储数据,LinkedHashSet继承了HashSet,他与HashSet的唯一区别就是他维护的是一个LinkedHashMap,实现了双向链表的功能,维护数据插入的顺序,这部分内容我将在介绍Map类的时候详细介绍。TreeSet也就是本文要介绍的这个实现类,顾名思义他内部维护的是一个TreeMap,底层是红黑二叉树,他使得集合内都是有序的序列。
TreeSet实现的部分和HashSet非常类似,我不太希望重复描述哪些相似的地方,而把精力放在重点部分,读者可以结合HashSet一起阅读。
一、Comparator和TreeSet的创建
TreeSet的创建和HashSet类似,创建的是一个TreeMap实例。TreeMap是一个什么存在呢?简单的说,他是基于红黑树结构实现的,会对元素进行排序。
public TreeSet() {
this(new TreeMap<E,Object>());
}
public TreeSet(Comparator<? super E> comparator) {
this(new TreeMap<>(comparator));
}
public TreeSet(Collection<? extends E> c) {
this();
addAll(c);
}
public TreeSet(SortedSet<E> s) {
this(s.comparator());
addAll(s);
}
第二个,他通过Comparator实例来创建TreeMap,那么Comparator到底是何方神圣呢?通过阅读Comparator的源码发现,这是一个用于集合类排序的辅助接口,用户需要实现compare方法。如果用户用了这种方式创建TreeSet,那么集合元素就不需要做额外处理,否则集合元素都需要实现Comparable接口,因为Tree在排序的时候会调用compare或者compareTo方法(介绍TreeMap的时候会具体讲解)。
public class MyComparator implements Comparator<Person> {
@Override
public int compare(Person o1, Person o2) {
return o1.age - o2.age;
}
}
public class Person {
public Integer age;
public Person(Integer value) {
this.age = value;
}
}
public static void TreeSetTest() {
// TreeMap在底层put元素的时候会判断是否存在Comparator实例,如果存在,则每次添加元素排序比较的时候会调用compare接口。
TreeSet<Person> set = new TreeSet<Person>(new MyComparator());
Person p1 = new Person(1);
Person p2 = new Person(1);
Person p3 = new Person(5);
Person p4 = new Person(9);
Person p5 = new Person(10);
set.add(p1);
set.add(p2);
set.add(p3);
set.add(p4);
set.add(p5);
Iterator<Person> i = set.iterator();
while (i.hasNext()) {
Person p = (Person) i.next();
System.out.println(p.age);
}
}
打印结果:
1
5
9
10
二、NavigableSet接口介绍
// 返回比当前元素小的最近的一个元素
public E lower(E e) {
return m.lowerKey(e);
}
// 返回小于等于当前元素的最近一个元素
public E floor(E e) {
return m.floorKey(e);
}
// 返回大于等于当前元素的最近一个元素
public E ceiling(E e) {
return m.ceilingKey(e);
}
// 返回大于当前元素的最近一个元素
public E higher(E e) {
return m.higherKey(e);
}
HashMap
前言:
HashMap是Map的一个实现类,这个类很重要,是很多集合类的实现基础,底层用的就是他,比如前文中讲到的HashSet,下文要讲到的LinkedHashMap。我们可以将HashMap看成是一个小型的数字字典,他以key-value的方式保存数据,Key全局唯一,并且key和value都允许为null。
HashMap底层是通过维护一个数据来保存元素。当创建HashMap实例的时候,会通过指定的数组大小以及负载因子等参数创建一个空的数组,当在容器中添加元素的时候,首先会通过hash算法求得key的hash值,再根据hash值确定元素在数组中对应的位置,最后将元素放入数组对应的位置。在添加元素的过程中会出现hash冲突问题,冲突处理的方法就是判断key值是否相同,如果相同则表明是同一个元素,替换value值。如果key值不同,则把当前元素添加到链表尾部。这里引出了一个概念,就是HashMap的数据结构其实是:hash表+单向链表。通过链表的方式把所有冲突元素放在了数组的同一个位置。但是当链表过长的时候会影响HashMap的存取效率。因此我们在实际使用HashMap的时候就需要考虑到这个问题,那么该如何控制hash冲突的出现频率呢?HashMap中有一个负载因子(loadFactor)的概念。容器中实际存储元素的size = loadFactor * 数组长度,一旦容器元素超出了这个size,HashMap就会自动扩容,并对所有元素重新执行hash操作,调整位置。
一、Node结构介绍
Node类实现了Map.Entry接口,他是用于存放数据的实体,是容器中存放数据的最小单元。Node的数据结构是一个单向链表,为什么选用这种结构?那是因前文讲到的,HashMap存放数据的结构是:hash表+单向链表。
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;
}
}
这个结构非常简单,定义了一个hash和key,hash值是对key进行hash以后得到的。value保存实际要存储的对象。next指向下一个节点。当hash冲突以后,就会将冲突的元素放入这个单向链表中。
二、创建HashMap
创建HashMap实例有四个构造方法,看源码:
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity);
}
// HashMap的数组大小是有讲究的,他必须是2的幂,这里通过一个牛逼哄哄的位运算算法,找到大于或等于initialCapacity的最小的2的幂
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
构造方法中有两个参数,第一个initialCapacity定义map的数组大小,第二个loadFactor意为负载因子,他的作用就是当容器中存储的数据达到loadFactor限度以后,就开始扩容。如果不设定这样参数的话,loadFactor就等于默认值0.75。但是细心的你会发现,容器创建以后,并没有创建数组,原来table是在第一次被使用的时候才创建的,而这个时候threshold = initialCapacity * loadFactor。 这才是这个容器的真正的负载能力。
tableSizeFor这个方法的目的是找到大于或等于initialCapacity的最小的2的幂,这个算法写的非常妙,值得我们细细品味。
假设cap=7
第一步 n = cap -1 = 6 = 00000110
第二步 n|= n>>>1:
n>>>1表示无符号右移1位,那么二进制表示为00000011,此时00000110 | 00000011 = 00000111
第三步 n|=n>>>2:
00000111 & 00000001 = 00000111
第四部 n|=n>>>4:
00000111 & 00000000 = 00000111
第五步 n|=n>>>8;
00000111 & 00000000 = 00000111
第六步 n|=n>>>16;
00000111 & 00000000 = 00000111
最后 n + 1 = 00001000
其实他的原理很简单,第一步先对cap-1是因为如果cap原本就是一个2的幂,那么最后一步加1,会使得这个值变成原来的两倍,但事实上原来这个cap就是2的幂,就是我们想要的值。接下来后面的几步无符号右移操作是把高位的1补到低位,经过一系列的位运算以后的值必定是000011111…他的低位必定全是1,那么最后一步加1以后,这个值就会成为一个00010000…(2的幂次),这就是通过cap找到2的幂的方法。看到如此简约高效的算法,我服了。
三、put添加元素
添加一个元素是所有容器中的标配功能,但是至于添加方式那就各有千秋,Map添加元素的方式是通过put,向容器中存入一个Key-Value对。
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
// 获取key的hash值,这里讲hash值的高16位右移和低16位做异或操作,目的是为了减少hash冲突,使hash值能均匀分布。
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 如果是第一次添加元素,那么table是空的,首先创建一个指定大小的table。
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 通过对hash与数组长度的与操作,确定key对应的数组位置,然后读取该位置中的元素。
if ((p = tab[i = (n - 1) & hash]) == null)
// 如果当前位置为空,那么就在当前数组位置,为这个key-value创建一个节点。
tab[i] = newNode(hash, key, value, null);
else {
// 如果当前位置已经存在元素,那么就要逐个读取这条链表的元素。
Node<K,V> e; K k;
// 如果key和hash值都等于当前头元素,那么这存放的两个元素是相同的。
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
// 如果当前位置的链表类型是TreeNode,那么就讲当前元素以红黑树的形式存放。
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
// 遍历链表的所有元素,如果都未找到相同key的元素,那么说明这个元素并不在容器中存在,因此将他添加到链表尾部,并结束遍历。
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
// 如果在遍历过程中,发现了相同的key值,那么就结束遍历。
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
// 如果e != null 说明在当前容器中,存在一个相同的key值,那么就要替换key所对应的value
if (e != null) {
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
// 这是专门留给LinkedHashMap调用的回调函数,LinkedHashMap会实现这个方法。从这里可以看出,HashMap充分的考虑了他的扩展性。
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
// 这里判断当前元素的数量是否超过了容量的上限,如果超过了,就要重新进行扩容,并对当前元素重新hash,所以再次扩容以后的元素位置都是会改变的。
if (++size > threshold)
resize();
// 此方法也是HashMap留给LinkedHashMap实现的回调方法。透露一下,因为LinkedHashMap在插入元素以后,都会维护他的一个双向链表
afterNodeInsertion(evict);
return null;
}
四、get获取元素
使用HashMap有一个明显的优点,就是他的存取时间开销基本维持在O(1),除非在数据量大了以后hash冲突的元素多了以后,对其性能有一定的影响。那么现在介绍的get方法很好的体现了这个优势。
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
// 同一个key的hash值是相同的,通过hash就可以求出数组的下标,便可以在O(1)的时间内获取元素。
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
// 在容器不为空,并且对应位置也存在元素的情况下,那么就要遍历链表,找到相同key值的元素。
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
// 如果第一个元素的key值相同,那么这个元素就是我们要找的。
if (first.hash == hash &&
((k = first.key) == key || (key != null && key.equals(k))))
return first;
// 如果第一个元素不是我们要找的,接下来就遍历链表元素,如果遍历完了以后都没找到,说明不存在这个key值
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;
}
五、remove删除元素
删除元素的实现原理和put,get都类似。remove通过给定的key值,找到在hash表中对应的位置,然后找出相同key值的元素,对其删除。
public V remove(Object key) {
Node<K,V> e;
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
}
// 通过key的hash值定位元素位置,并对其删除。这里的实现和put基本相同,我只在不同的地方做一下解释。
final Node<K,V> removeNode(int hash, Object key, Object value,
boolean matchValue, boolean movable) {
Node<K,V>[] tab; Node<K,V> p; int n, index;
if ((tab = table) != null && (n = tab.length) > 0 &&
(p = tab[index = (n - 1) & hash]) != null) {
Node<K,V> node = null, e; K k; V v;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
node = p;
else if ((e = p.next) != null) {
if (p instanceof TreeNode)
node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
else {
do {
if (e.hash == hash &&
((k = e.key) == key ||
(key != null && key.equals(k)))) {
node = e;
break;
}
p = e;
} while ((e = e.next) != null);
}
}
// 如果找到了相同的key,接下来就要判断matchValue参数,matchValue如果是true的话,就说明
// 需要检查被删除的value是否相同,只有相同的情况下才能删除元素。如果matchValue是false的话
// 就不需要判断value是否相同。
if (node != null && (!matchValue || (v = node.value) == value ||
(value != null && value.equals(v)))) {
if (node instanceof TreeNode)
((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
else if (node == p)
tab[index] = node.next;
else
p.next = node.next;
++modCount;
--size;
afterNodeRemoval(node);
return node;
}
}
return null;
}
六、resize动态扩容
resize这个方法非常重要,他在添加元素的时候就会被调用到。resize的目的是在容器的容量达到上限的时候,对其扩容,使得元素可以继续被添加进来。这里需要关注两个参数threshold和loadFactor,threshold表示容量的上限,当容器中元素数量大于threshold的时候,就要扩容,并且每次扩容都是原来的两倍。loadFactor表示hash表的数组大小。这两个参数的配合使用可以有效的控制hash冲突数量。
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
// 如果容器并不是第一次扩容的话,那么oldCap必定会大于0
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
// threshold和数组大小cap共同扩大为原来的两倍
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1;
}
// 第一次扩容,并且设定了threshold值。
else if (oldThr > 0)
newCap = oldThr;
else {
// 如果在创建的时候并没有设置threshold值,那就用默认值
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {
// 第一次扩容的时候threshold = cap * loadFactor
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
// 创建数组
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
// 如果不是第一次扩容,那么hash表中必然存在数据,需要将这些数据重新hash
if (oldTab != null) {
// 遍历所有元素
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null)
// 重新计算在数组中的位置。
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
// 这里分两串,lo表示原先位置的所有,hi表示新的索引
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
// 因为cap都是2的幂次,假设oldCap == 10000,
// 假设e.hash= 01010 那么 e.hash & oldCap == 0。
// 老位置= e.hash & oldCap-1 = 01010 & 01111 = 01010
// newCap此时为100000,newCap-1=011111。
// 此时e.hash & newCap 任然等于01010,位置不变。
// 如果e.hash 假设为11010,那么 e.hash & oldCap != 0
// 原来的位置为 e.hash & oldCap-1 = 01010
// 新位置 e.hash & newCap-1 = 11010 & 011111 = 11010
// 此时 新位置 != 老位置 新位置=老位置+oldCap
// 因此这里分类两个索引的链表
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
七、遍历
HashMap遍历有三种方式,一种是对key遍历,还有一种是对entry遍历和对value遍历。这三种遍历方式都是基于对HashIterator的封装,三种实现方式大同小异,因此我着重介绍EntryIterator的实现。
// 对HashMap元素进行遍历。
public Set<Map.Entry<K,V>> entrySet() {
Set<Map.Entry<K,V>> es;
// 第一次遍历的时候,实例化entrySet。
return (es = entrySet) == null ? (entrySet = new EntrySet()) : es;
}
final class EntrySet extends AbstractSet<Map.Entry<K,V>> {
public final int size() { return size; }
public final void clear() { HashMap.this.clear(); }
public final Iterator<Map.Entry<K,V>> iterator() {
return new EntryIterator();
}
public final boolean contains(Object o) {
if (!(o instanceof Map.Entry))
return false;
Map.Entry<?,?> e = (Map.Entry<?,?>) o;
Object key = e.getKey();
Node<K,V> candidate = getNode(hash(key), key);
return candidate != null && candidate.equals(e);
}
public final boolean remove(Object o) {
if (o instanceof Map.Entry) {
Map.Entry<?,?> e = (Map.Entry<?,?>) o;
Object key = e.getKey();
Object value = e.getValue();
return removeNode(hash(key), key, value, true, true) != null;
}
return false;
}
public final Spliterator<Map.Entry<K,V>> spliterator() {
return new EntrySpliterator<>(HashMap.this, 0, -1, 0, 0);
}
public final void forEach(Consumer<? super Map.Entry<K,V>> action) {
Node<K,V>[] tab;
if (action == null)
throw new NullPointerException();
if (size > 0 && (tab = table) != null) {
int mc = modCount;
for (int i = 0; i < tab.length; ++i) {
for (Node<K,V> e = tab[i]; e != null; e = e.next)
action.accept(e);
}
if (modCount != mc)
throw new ConcurrentModificationException();
}
}
}
final class EntryIterator extends HashIterator
implements Iterator<Map.Entry<K,V>> {
public final Map.Entry<K,V> next() { return nextNode(); }
}
// HashMap自己实现的遍历方法。上面的所有方法都是围绕这个类展开的。下面具体讲解这个类的实现原理。
abstract class HashIterator {
Node<K,V> next; // 指向下一个元素
Node<K,V> current; // 指向当前元素
int expectedModCount;
int index; // 当前元素位置
HashIterator() {
expectedModCount = modCount;
Node<K,V>[] t = table;
current = next = null;
index = 0;
if (t != null && size > 0) { // 找到table中的第一个元素
do {} while (index < t.length && (next = t[index++]) == null);
}
}
public final boolean hasNext() {
return next != null;
}
final Node<K,V> nextNode() {
Node<K,V>[] t;
Node<K,V> e = next;
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
if (e == null)
throw new NoSuchElementException();
// 判断当前元素是否为链表中的最后一个元素,如果在链表尾部,那么就需要重新遍历table,
// 顺序找到下元素的位置。
if ((next = (current = e).next) == null && (t = table) != null) {
do {} while (index < t.length && (next = t[index++]) == null);
}
return e;
}
// 删除当前遍历的元素。
public final void remove() {
Node<K,V> p = current;
if (p == null)
throw new IllegalStateException();
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
current = null;
K key = p.key;
removeNode(hash(key), key, null, false, false);
expectedModCount = modCount;
}
}
LinkedHashMap
前言:
LinkedHashMap是HashMap的子类,他不仅实现了HashMap的所有功能,更是维护了元素的存储顺序。LinkedHashMap维护元素顺序的方式有两种,一种是维护他的存入顺序,另一种则是维护元素的读取顺序。基于这种功能,LinkedHashMap可以在用于LRU算法的实现。LRU为何物?LRU的全称是Least Recently Used翻译过来就是最近最久未使用,他通常应用于缓存的一种实现方式,当缓存数据满时,删除最少使用的缓存数据。
LinkedHashMap的结构是HashMap+双向链表。他通过继承HashMap得到了用hash表存储数据的能力,同时他又维护了一个双向链表实现了对元素的排序功能。HashMap部分上文已经介绍了,本文着重要介绍的是双向链表部分实现(这里有必要说明一下,我写的系列文章是基于jdk1.8的。jdk1.8和之前版本的实现有不少差异,LinkedHashMap部分就改动了不少,有兴趣的同学可以对照1.7的链表实现和1.8的链表实现,你会发现是两者差异很大,对于两种实现的优缺点可以自行思考哦)。
一、双向链表结构
jdk1.8的链表结构和1.7的差异很大,可以看出来1.8中的实现简化了不是,只维护了两个指针,befor和after。在整个链表中维护了head(头指针)和tail(尾指针)。这两个指针是有讲究的,head所指向的是eldest元素,也就是最老的元素,tail指向youngest元素,也就是最年轻的元素。在这个链表中,都是在队尾添加元素,队头删除元素,这种方式很像队列,但是还是有点区别。
static class Entry<K,V> extends HashMap.Node<K,V> {
Entry<K,V> before, after;
Entry(int hash, K key, V value, Node<K,V> next) {
super(hash, key, value, next);
}
}
// 指向eldest元素
transient LinkedHashMap.Entry<K,V> head;
// 指向youngest元素
transient LinkedHashMap.Entry<K,V> tail;
二、LinkedHashMap实例创建
LinkedHashMap的创建和HashMap没什么两样,就是这个构造方法中,加入了acessOrder的参数,告诉LinkedHashMap以哪种方式维护顺序。
// 元素遍历顺序,true维护元素的访问顺序,最新访问的放入队尾,false维护元素的插入顺序,最新插入的在队尾。
final boolean accessOrder;
public LinkedHashMap(int initialCapacity,
float loadFactor,
boolean accessOrder) {
super(initialCapacity, loadFactor);
this.accessOrder = accessOrder;
}
三、get获取元素
LinkedHashMap的get方法几乎就是复用了HashMap。唯一的区别就是多了一个accessOrder判断,如果accessOrder==true说明他需要维护元素的访问顺序,而afterNodeAccess是HashMap提供的回调方法,他也会在put元素的时候调用。afterNodeAccess方法的作用就是将当前访问的元素添加到队尾,因为这个链表都是从头部删除,因此这个元素会在最后才被删除。
public V get(Object key) {
Node<K,V> e;
if ((e = getNode(hash(key), key)) == null)
return null;
if (accessOrder)
afterNodeAccess(e);
return e.value;
}
void afterNodeAccess(Node<K,V> e) { // 将访问元素添加到队尾
LinkedHashMap.Entry<K,V> last;
if (accessOrder && (last = tail) != e) {
LinkedHashMap.Entry<K,V> p =
(LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
p.after = null;
// 如果当前元素是头元素,那么就将head指向他的下一个节点
if (b == null)
head = a;
else
b.after = a;
// 如果当前元素是尾元素,那么就将last指向他的上一个节点
if (a != null)
a.before = b;
else
last = b;
if (last == null)
head = p;
else {
p.before = last;
last.after = p;
}
tail = p;
++modCount;
}
}
四、put元素
LinkedHashMap并没有自己实现put方法,完完全全是复用了HashMap的,因为HashMap提供了两个回调方法作为他的扩展,LinkedHashMap只需要实现这两个方法即可,从这里也可以学到如何提供代码的扩展性,预先留出回调接口也是个不错的选择哦。在HashMap的put方法中,调用了两个回调方法,afterNodeAccess和afterNodeInsertion。第一个方法已经介绍了,下面就介绍afterNodeInsertion,这个方法的主要目的就是在map添加元素以后,维护链表的顺序,同时也会控制了对链表头元素的删除与否。
// 在插入元素以后,判断当前容器的元素是否已满,如果是的话,就删除当前最老的元素,也就是队头元素。
void afterNodeInsertion(boolean evict) {
LinkedHashMap.Entry<K,V> first;
if (evict && (first = head) != null && removeEldestEntry(first)) {
K key = first.key;
removeNode(hash(key), key, null, false, true);
}
}
// 这是用户实现的回调方法,判断当前最老的元素是否需要删除,如果为true,就删除链表头元素
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
return false;
}
五、删除元素
在删除元素以后,LinkedHashMap需要维护当前链表的指针,也就是双向链表的head和tail指针的指向问题
void afterNodeRemoval(Node<K,V> e) {
LinkedHashMap.Entry<K,V> p =
(LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
p.before = p.after = null;
// 如果当前元素是头元素,那么head指向他的下一个节点
if (b == null)
head = a;
else
b.after = a;
// 如果当前元素是尾元素,那么tail指向他的上一个节点
if (a == null)
tail = b;
else
a.before = b;
}
六、LinkedHashIterator遍历
容器的遍历是一个亘古不变的话题,然而LinkedHashMap的遍历方式有他的特殊性。因为他在hash表的基础之上又维护了一个双向链表,而这个链表维护这元素的遍历顺序,因为LinkedHashMap在遍历的时候,只能遍历这个链表,而不能像HashMap一样遍历hash表。
abstract class LinkedHashIterator {
LinkedHashMap.Entry<K,V> next;
LinkedHashMap.Entry<K,V> current;
int expectedModCount;
LinkedHashIterator() {
// 第一次从头开始遍历
next = head;
expectedModCount = modCount;
current = null;
}
public final boolean hasNext() {
return next != null;
}
// 对链表从头到尾开始遍历,顺序遍历的方式很简单就是next = e.after
final LinkedHashMap.Entry<K,V> nextNode() {
LinkedHashMap.Entry<K,V> e = next;
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
if (e == null)
throw new NoSuchElementException();
current = e;
next = e.after;
return e;
}
public final void remove() {
Node<K,V> p = current;
if (p == null)
throw new IllegalStateException();
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
current = null;
K key = p.key;
removeNode(hash(key), key, null, false, false);
expectedModCount = modCount;
}
}
WeakHashMap
前言:
WeakHashMap这个类我看了好久,一直不知道怎么写,有两点原因。第一:我怕把他写简单了,光从WeakHashMap的功能实现是描述,他和HashMap等非常的相似,无非也是用来hash表+单向链表的结构作为底层数据存储,再写一遍没太大意思。 第二:WeakHashMap的特点是以一种弱引用的关系存储数据,存储对象长期不用,可以被垃圾回收。讲这部分内容非常有意思,但是关于jvm部分了解不深,又怕讲不好。因此托了很长时间没写。
权衡以后,决定先讲解WeakHashMap弱引用的实现原理,垃圾回收部分先缓一缓。
一、与HashMap异同
WeakHashMap的实现和HashMap非常类似,他们有相同的数据结构,类似的存储机制。数据底层都是通过hash表+单向链表的结构存储数据。
1.get
获取元素的流程非常类似,首先对key进行hash处理,通过hash值寻找到对应的元素位置。数组元素保存的是单向链表,这是hash冲突导致的结果。然后元每个entry对比key值是否相同,最终找到对应的元素。
2.put
添加元素的流程也和HashMap类似,首先对key值hash处理,通过hash值求出对应的元素位置,然后通过对比单向链表中的key值,将元素添加到链表头部。最后会更加当前元素的数量与threshold对比,进行动态扩容。扩容部分的原理也和HashMap类似。
3.getTable
getTable方法是WeakHashMap特有的,这个方法是干什么用的呢?因为我们知道WeakHashMap的元素是通过弱引用的关系存储的。在容器中,有部分元素长时间未用,会被垃圾回收,getTable的作用就是清除被垃圾回收的元素。源码如下:
private Entry<K,V>[] getTable() {
expungeStaleEntries();
return table;
}
// 清除所有被垃圾回收的元素
private void expungeStaleEntries() {
// queue队列中保存的是已经被垃圾回收的元素key值。遍历队列
for (Object x; (x = queue.poll()) != null; ) {
synchronized (queue) {
@SuppressWarnings("unchecked")
Entry<K,V> e = (Entry<K,V>) x;
int i = indexFor(e.hash, table.length);
Entry<K,V> prev = table[i];
Entry<K,V> p = prev;
while (p != null) {
Entry<K,V> next = p.next;
if (p == e) {
if (prev == e)
table[i] = next;
else
prev.next = next;
e.value = null; // 有助于垃圾回收
size--;
break;
}
prev = p;
p = next;
}
}
}
}
从源码中可以看出,首先会遍历queue队列,队列中保存的元素是已经被垃圾回收的元素的key值。然后将该key从table中删除。这步方法在getTable,resize,size方法中使用到,并且在垃圾回收过程中也会使用,因此他是在多线程环境下调用的,所以该方法使用了同步方法,确保线程安全。
那么是谁将垃圾回收的后的元素放入queue中的呢?这就要从Entry的结构说起。
4.Entry结构介绍
WeakHashMap的Entry是Reference的子类。Entry实例化时会引用当前的queue,如果当前Entry被垃圾回收后,会将key注册的queue中。在后文中我会详细介绍Reference类。
Entry结构
private static class Entry<K,V> extends WeakReference<Object> implements Map.Entry<K,V> {
V value;
final int hash;
Entry<K,V> next;
Entry(Object key, V value,
ReferenceQueue<Object> queue,
int hash, Entry<K,V> next) {
super(key, queue);
this.value = value;
this.hash = hash;
this.next = next;
}
......
}
二、弱键引用
上文简单介绍了WeakHashMap和HashMap的异同点,大同小异,不想过多重复。我认为WeakHashMap的重点是对元素进行垃圾回收的部分。下面将结合源码进行讲解。
public abstract class Reference<T> {
private T referent;
// 将回收的元素添加到队列中。
volatile ReferenceQueue<? super T> queue;
@SuppressWarnings("rawtypes")
Reference next;
transient private Reference<T> discovered;
static private class Lock { };
private static Lock lock = new Lock();
// 垃圾收集器将回收的引用加入队列
private static Reference<Object> pending = null;
private static class ReferenceHandler extends Thread {
ReferenceHandler(ThreadGroup g, String name) {
super(g, name);
}
public void run() {
for (;;) {
Reference<Object> r;
synchronized (lock) {
if (pending != null) {
r = pending;
pending = r.discovered;
r.discovered = null;
} else {
try {
try {
lock.wait();
} catch (OutOfMemoryError x) { }
} catch (InterruptedException x) { }
continue;
}
}
// Fast path for cleaners
if (r instanceof Cleaner) {
((Cleaner)r).clean();
continue;
}
ReferenceQueue<Object> q = r.queue;
if (q != ReferenceQueue.NULL) q.enqueue(r);
}
}
}
static {
ThreadGroup tg = Thread.currentThread().getThreadGroup();
for (ThreadGroup tgn = tg;
tgn != null;
tg = tgn, tgn = tg.getParent());
Thread handler = new ReferenceHandler(tg, "Reference Handler");
handler.setPriority(Thread.MAX_PRIORITY);
handler.setDaemon(true);
handler.start();
}
}
Reference的实现原理:
第一步通过静态代码块启动ReferenceHandler线程,所有Reference实例共享该线程。pending会被虚拟机调用,当有元素被垃圾回收以后,会添加到该队列中。Reference线程通过无线循环检查pending是否存在元素。当发现有元素被垃圾回收以后,会将该元素添加到queue中。如果当前没有发现被回收的元素,也就是pending为null时,会通过lock.wait() 阻塞线程。直到有元素被回收以后,会调用nodify唤醒线程。
该过程涉及到多线程的知识,jvm垃圾回收的原理等,这部分比较复杂,暂时无法很好的讲解。将回收的元素加入到queue队列的部分实现用了消费者模式。为了读者更好的理解该原理,我对他进行模仿,实现了一个简单的消费者模式的demo。这个demo主要功能如下:
1.多个生产者不断的生产某件产品,当产品数量大于10以后就停止生产,只要当产品数量小于10,就会生产。
2.多个消费者不断的消费产品,知道消费结束。
3.当生产者生产到10件以后,就阻塞生产线,通知消费者消费。
4.当消费者消费完结束以后,阻塞消费,通知生产者生产。
public class ProducerCustomerDemo {
private static int index = 0;
private static int size = 0;
static private class Lock { };
private static Lock lock = new Lock();
private static Entry head = null;
public synchronized int getSize() {
return size;
}
public synchronized void addSize() {
ProducerCustomerDemo.size++;
}
public synchronized void minusSize() {
ProducerCustomerDemo.size--;
}
public synchronized int getIndex() {
return index;
}
public synchronized void addIndex() {
index++;
}
public synchronized void minusIndex() {
index--;
}
public static void main(String[] args) {
new Thread(new ProducerCustomerDemo().new Consumer("aaa")).start();
new Thread(new ProducerCustomerDemo().new Consumer("bbb")).start();
new Thread(new ProducerCustomerDemo().new Consumer("ccc")).start();
new Thread(new ProducerCustomerDemo().new Consumer("ddd")).start();
new Thread(new ProducerCustomerDemo().new Producer("张三")).start();
new Thread(new ProducerCustomerDemo().new Producer("李四")).start();
}
class Entry {
Entry next;
int value;
public Entry(int value) {
this.value = value;
}
}
class Consumer implements Runnable{
private String name;
public Consumer(String name) {
this.name = name;
}
@Override
public void run() {
for (;;) {
Entry temp = null;
synchronized (lock) {
if (head != null) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
temp = head;
head = temp.next;
temp.next = null;
minusSize();
System.out.println("消费,当前产品数"+getSize()+"--"+name+":"+ temp.value);
lock.notify();
} else {
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}
class Producer implements Runnable {
private String name;
public Producer(String name) {
this.name = name;
}
@Override
public void run() {
for (;;) {
synchronized (lock) {
if (getSize() < 10) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
addIndex();
Entry temp = new Entry(getIndex());
temp.next = head;
head = temp;
addSize();
System.out.println("生产,当前产品数"+getSize()+"--" + name + ":" + temp.value);
lock.notify();
} else {
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}
}
HashTable
前言:
前文中已对HashMap实现原理做了详细介绍,HashTable与HashMap的原理相同,实现方式也几乎一致。除了以下几点不同:
1.HashMap非线程安全,HashTable线程安全。HashMap与HashTable的实现方法几乎一致,区别是HashTable对所有的方法进行了同步操作,确保了线程安全。但是有需要注意的是,他只确保单个操作的原子性,如果需要在并发环境下执行复合操作,那用户需要自行同步,否则会出现问题。
2.HashMap的key和value可以为null, HashTable的Key和value不能为null。
可以参考HashMap的实现原理
TreeMap
前言:
TreeMap和HashMap一样实现的是Map接口,但两者的实现方式天差地别。HashMap的底层是hash表+单向链表的形式存储数据,TreeMap底层是通过红黑树存储数据。HashMap因为是基于散列表的实现,所以时间开销为O(1),TreeMap的时间开销是O(lgn)。TreeMap的优势在于他是基于key值排序的。
红黑树有五大特性:
1)每个结点要么是红的,要么是黑的。
2)根结点是黑的。
3)每个叶结点,即空结点(NIL)是黑的。
4)如果一个结点是红的,那么它的俩个儿子都是黑的。
5)对每个结点,从该结点到其子孙结点的所有路径上包含相同数目的黑结点。
他的所有操作都是围绕着这五大特性展开的。这五大特性的最终目的就是为了维持二叉树的相对平衡性。当每次二叉树操作以后,有可能会出现违反特性的情况(也就是出现了失衡状况),这时二叉树需要通过左旋,右旋,重新着色等系列操作,再次找到平衡点。
我的理解是:红黑色的操作就两个要点。第一:遵循二叉查找树的规范,对所有元素进行排序,但这里存在着不确定情况,有可能出现左右子树深度极其不对称的情况,导致最坏时间复杂度出现O(n)的情况。 第二:正因为存在二叉树严重不平衡的情况,所以就出现了红黑二叉树,通过标记每个节点的颜色,动态的调整二叉树的结构,使其始终维持在相对平衡的状态,这样做的好处就是查找性能始终维持在O(lgn)的较高水平。
一、添加元素
在TreeMap中插入元素,原理就是在二叉查找树中插入元素。通过对二叉树节点大小的对比插入相应的位置,时间复杂度为O(lgn)。但是在插入完毕以后,有可能会导致红黑树被破坏的情况,因此需要修复当前结构,重新着色。
put方法:
public V put(K key, V value) {
Entry<K,V> t = root;
if (t == null) {
compare(key, key); // type (and possibly null) check
root = new Entry<>(key, value, null);
size = 1;
modCount++;
return null;
}
int cmp;
Entry<K,V> parent;
// split comparator and comparable paths
Comparator<? super K> cpr = comparator;
if (cpr != null) {
do {
parent = t;
cmp = cpr.compare(key, t.key);
if (cmp < 0)
t = t.left;
else if (cmp > 0)
t = t.right;
else
return t.setValue(value);
} while (t != null);
}
else {
if (key == null)
throw new NullPointerException();
@SuppressWarnings("unchecked")
Comparable<? super K> k = (Comparable<? super K>) key;
do {
parent = t;
cmp = k.compareTo(t.key);
if (cmp < 0)
t = t.left;
else if (cmp > 0)
t = t.right;
else
return t.setValue(value);
} while (t != null);
}
Entry<K,V> e = new Entry<>(key, value, parent);
if (cmp < 0)
parent.left = e;
else
parent.right = e;
fixAfterInsertion(e);
size++;
modCount++;
return null;
}
插入元素后调整红黑树结构:
private void fixAfterInsertion(Entry<K,V> x) {
x.color = RED;
while (x != null && x != root && x.parent.color == RED) {
if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {
Entry<K,V> y = rightOf(parentOf(parentOf(x)));
if (colorOf(y) == RED) {
setColor(parentOf(x), BLACK);
setColor(y, BLACK);
setColor(parentOf(parentOf(x)), RED);
x = parentOf(parentOf(x));
} else {
if (x == rightOf(parentOf(x))) {
x = parentOf(x);
rotateLeft(x);
}
setColor(parentOf(x), BLACK);
setColor(parentOf(parentOf(x)), RED);
rotateRight(parentOf(parentOf(x)));
}
} else {
Entry<K,V> y = leftOf(parentOf(parentOf(x)));
if (colorOf(y) == RED) {
setColor(parentOf(x), BLACK);
setColor(y, BLACK);
setColor(parentOf(parentOf(x)), RED);
x = parentOf(parentOf(x));
} else {
if (x == leftOf(parentOf(x))) {
x = parentOf(x);
rotateRight(x);
}
setColor(parentOf(x), BLACK);
setColor(parentOf(parentOf(x)), RED);
rotateLeft(parentOf(parentOf(x)));
}
}
}
root.color = BLACK;
}