文章目录
LinkedHashMap有顺序的map数据结构
概述
LinkedHashMap是HashMap的子类,它的大部分实现与HashMap相同,两者最大的区别在于,HashMap的对哈希表进行迭代时是无序的,而 LinkedHashMap对哈希表迭代是有序的,LinkedHashMap默认的规则是,迭代输出的结果保持和插入key-value pair的顺序一致(当然具体迭代规则可以修改)。LinkedHashMap除了像HashMap一样用数组、单链表和红黑树来组织数据外,还额外维护了一个 双向链表,每次向linkedHashMap插入键值对,除了将其插入到哈希表的对应位置之外,还要将其插入到双向循环链表的尾部。它是由 数组 + 一个单项链表+一个双向链表组成。在原HashMap的数据结构基础上加一个双向链表
- 底层是散列表和双向链表
- 允许为null,不同步
- 插入的顺序是有序的(底层链表致使有序)
- 装载因子和初始容量对LinkedHashMap影响是很大的~
LinkedHashMap比HashMap多了一个双向链表的维护,在数据结构而言它要复杂一些,阅读源码起来比较轻松一些,因为大多都由HashMap实现了…
阅读源码的时候我们会发现多态是无处不在的~子类用父类的方法,子类重写了父类的部分方法即可达到不一样的效果!
比如:LinkedHashMap并没有重写put方法,而put方法内部的newNode()方法重写了。LinkedHashMap调用父类的put方法,里面回调的是重写后的newNode(),从而达到目的!
LinkedHashMap可以设置两种遍历顺序:
- 访问顺序(access-ordered)
- 插入顺序(insertion-ordered)
- 默认是插入顺序的
对于访问顺序,它是LRU(最近最少使用)算法的实现,要使用它要么重写LinkedListMap的几个方法(removeEldestEntry(Map.Entry<K,V> eldest)和afterNodeInsertion(boolean evict)),要么是扩展成LRUMap来使用,不然设置为访问顺序(access-ordered)的用处不大,对于LinkedHashMap的LRU这边没有深入了,因为确实用的不多,但是大家有兴趣的可以深入研究。
LinkedHashMap遍历的是内部维护的双向链表,所以说初始容量对LinkedHashMap遍历是不受影响的
一句话。LinkedHashMap就是继承HashMap的加上双向链表的HashMap
基本属性
//双向链表的头节点
transient LinkedHashMap.Entry<K,V> head;
//双向链表的尾节点
transient LinkedHashMap.Entry<K,V> tail;
//排序的规则,false按插入顺序排序,true访问顺序排序
final boolean accessOrder;
//所以说accessOrder的作用就是控制访问顺序,设置为true后每次访问一个元素,就将该元素所在的Node变成最后一个节点,
改变该元素在LinkedHashMap中的存储顺序。
构造方法
//LinkedHashMap的构造方法,都是通过调用父类的构造方法来实现,大部分accessOrder默认为false
public LinkedHashMap(int initialCapacity, float loadFactor) {
super(initialCapacity, loadFactor);
accessOrder = false;
}
public LinkedHashMap(int initialCapacity) {
super(initialCapacity);
accessOrder = false;
}
public LinkedHashMap() {
super();
accessOrder = false;
}
public LinkedHashMap(Map<? extends K, ? extends V> m) {
super(m);
accessOrder = false;
}
public LinkedHashMap(int initialCapacity,
float loadFactor,
boolean accessOrder) {
super(initialCapacity, loadFactor);
this.accessOrder = accessOrder;
}
上面是LinkedHashMap的构造方法,通过传入初始化参数和代码看出,LinkedHashMap的构造方法和父类的构造方法,是一一对应的。也是通过super()关键字来调用父类的构造方法来进行初始化,唯一的不同是最后一个构造方法,提供了AccessOrder参数,用来指定LinkedHashMap的排序方式,accessOrder =false -> 插入顺序进行排序 , accessOrder = true -> 访问顺序进行排序。
Entry定义
这个比较重要
private static class Entry<K,V> extends HashMap.Entry<K,V> {
//定义Entry类型的两个变量,或者称之为前后的两个指针
Entry<K,V> before, after;
//构造方法与HashMap的没有区别,也是调用父类的Entry构造方法
Entry(int hash, K key, V value, HashMap.Entry<K,V> next) {
super(hash, key, value, next);
}
//删除
private void remove() {
before.after = after;
after.before = before;
}
//插入节点到指定的节点之前
private void addBefore(Entry<K,V> existingEntry) {
after = existingEntry;
before = existingEntry.before;
before.after = this;
after.before = this;
}
//方法重写,HashMap中为空
void recordAccess(HashMap<K,V> m) {
LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;
if (lm.accessOrder) {
lm.modCount++;
remove();
addBefore(lm.header);
}
}
//方法重写 ,HashMap中方法为空
void recordRemoval(HashMap<K,V> m) {
remove();
}
}
到这里已经可以知道,相对比HashMap,LinkedHashMap内部不光是使用HashMap中的哈希表来存储Entry对象,还另外维护了一个LinkedHashMapEntry,这些LinkedHashMapEntry内部又保存了前驱跟后继的引用,可以确定这是个双向链表。而这个LinkedHashMapEntry提供了对象的增加删除方法都是去更改节点的前驱后继指向。
put()方法
LinkedHashMap并没有重写父类的put()方法,说明调用put方法时实际上调用的是父类的put方法。HashMap的put这里就不多说了中有这样一个方法
LinkedHashMap 重写了2个方法
ode<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
LinkedHashMap.Entry<K,V> p =
new LinkedHashMap.Entry<K,V>(hash, key, value, e);
linkNodeLast(p);
return p;
}
private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
//用临时变量last记录尾节点tail
LinkedHashMap.Entry<K,V> last = tail;
//将尾节点设为当前插入的节点p
tail = p;
//如果原先尾节点为null,表示当前链表为空
if (last == null)
//头结点也为当前插入节点
head = p;
else {
//原始链表不为空,那么将当前节点的上节点指向原始尾节点
p.before = last;
//原始尾节点的下一个节点指向当前插入节点
last.after = p;
}
}
//把当前节点放到双向链表的尾部
void afterNodeAccess(HashMap.Node<K,V> e) { // move node to last
LinkedHashMap.Entry<K,V> last;
//当 accessOrder = true 并且当前节点不等于尾节点tail。这里将last节点赋值为tail节点
if (accessOrder && (last = tail) != e) {
//记录当前节点的上一个节点b和下一个节点a
LinkedHashMap.Entry<K,V> p =
(LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
//释放当前节点和后一个节点的关系
p.after = null;
//如果当前节点的前一个节点为null
if (b == null)
//头节点=当前节点的下一个节点
head = a;
else
//否则b的后节点指向a
b.after = a;
//如果a != null
if (a != null)
//a的前一个节点指向b
a.before = b;
else
//b设为尾节点
last = b;
//如果尾节点为null
if (last == null)
//头节点设为p
head = p;
else {
//否则将p放到双向链表的最后
p.before = last;
last.after = p;
}
//将尾节点设为p
tail = p;
//LinkedHashMap对象操作次数+1,用于快速失败校验
++modCount;
}
}
通过重写put方法,来维护一个双向链表,使得存取有序。
get()方法
public V get(Object key) {
Entry<K,V> e = (Entry<K,V>)getEntry(key); //调用父类的getEntry()方法
if (e == null)
return null;
e.recordAccess(this); //判断排序方式,如果accessOrder = true , 删除当前e节点
return e.value;
}
相比于 HashMap 的 get 方法,这里多出了第 5,6行代码,当 accessOrder = true 时,即表示按照最近访问的迭代顺序,会将访问过的元素放在链表后面。
TreeMap 自定义排序规则的红黑树map数据结构
- TreeMap集合结构特点
- 键的数据结构是红黑树,可保证键的排序和唯一性
- 排序分为自然排序和比较器排序,如果使用的是自然排序,对元素有要求,要求这个元素需要实现 Comparable 接口
- 线程是不安全的效率比较高
之前已经学习过HashMap和LinkedHashMap了,HashMap不保证数据有序,LinkedHashMap保证数据可以保持插入顺序,而如果我们希望Map可以保持key的大小顺序的时候,我们就需要利用TreeMap了。public TreeMap(): 自然排序 public TreeMap(Comparator<? super K> comparator): 使用的是比较器排序
put函数源码
- 如果存在的话,old value被替换;如果不存在的话,则新添一个节点,然后对做红黑树的平衡操作。
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; }
get获取函数源码
- get函数则相对来说比较简单,以log(n)的复杂度进行get。博客
final Entry<K,V> getEntry(Object key) { // Offload comparator-based version for sake of performance if (comparator != null) return getEntryUsingComparator(key); if (key == null) throw new NullPointerException(); @SuppressWarnings("unchecked") Comparable<? super K> k = (Comparable<? super K>) key; Entry<K,V> p = root; // 按照二叉树搜索的方式进行搜索,搜到返回 while (p != null) { int cmp = k.compareTo(p.key); if (cmp < 0) p = p.left; else if (cmp > 0) p = p.right; else return p; } return null; } public V get(Object key) { Entry<K,V> p = getEntry(key); return (p==null ? null : p.value); }
如何保证有序性
- TreeMap是如何保证其迭代输出是有序的呢?
- 其实从宏观上来讲,就相当于树的中序遍历(LDR)。我们先看一下迭代输出的步骤
for(Entry<Integer, String> entry : tmap.entrySet()) { System.out.println(entry.getKey() + ": " + entry.getValue()); }
- for语句会做如下转换为:
for(Iterator<Map.Entry<String, String>> it = tmap.entrySet().iterator() ; tmap.hasNext(); ) { Entry<Integer, String> entry = it.next(); System.out.println(entry.getKey() + ": " + entry.getValue()); }
- 在it.next()的调用中会使用nextEntry调用
successor
这个是过的后继的重点。
- 然后看一下successor函数
static <K,V> TreeMap.Entry<K,V> successor(Entry<K,V> t) { if (t == null) return null; else if (t.right != null) { // 有右子树的节点,后继节点就是右子树的“最左节点” // 因为“最左子树”是右子树的最小节点 Entry<K,V> p = t.right; while (p.left != null) p = p.left; return p; } else { // 如果右子树为空,则寻找当前节点所在左子树的第一个祖先节点 // 因为左子树找完了,根据LDR该D了 Entry<K,V> p = t.parent; Entry<K,V> ch = t; // 保证左子树 while (p != null && ch == p.right) { ch = p; p = p.parent; } return p; } }
- 怎么理解这个successor呢?只要记住,这个是中序遍历就好了,L-D-R。具体细节如下:
- a. 空节点,没有后继
- b. 有右子树的节点,后继就是右子树的“最左节点”
- c. 无右子树的节点,后继就是该节点所在左子树的第一个祖先节点
- 有右子树的节点,节点的下一个节点,肯定在右子树中,而右子树中“最左”的那个节点则是右树中最小的一个,那么当然是右子树的“最左节点”,
- 无右子树的节点,先找到这个节点所在的左子树(右图),那么这个节点所在的左子树的父节点(绿色节点),就是下一个节点。
- 怎么理解这个successor呢?只要记住,这个是中序遍历就好了,L-D-R。具体细节如下:
HashSet
- HashSet 实现了 Set 接口,不允许插入重复的元素,允许包含 null 元素,且不保证元素迭代顺序,特别是不保证该顺序恒久不变
- HashSet 的代码十分简单,去掉注释后的代码不到两百行。HashSet 底层是通过 HashMap 来实现的。
- HashSet是根据hashCode来决定存储位置的,是通过HashMap实现的,所以对象必须实现hashCode()方法,存储的数据无序不能重复,可以存储null,但是只能存一个。
HashSet如何去重
- 在向 HashSet 添加元素时,HashSet 会将该操作转换为向 HashMap 添加键值对,如果 HashMap 中包含 key 值与待插入元素相等的键值对(hashCode() 方法返回值相等,通过 equals() 方法比较也返回 true),则待添加的键值对的 value 会覆盖原有数据,但 key 不会有所改变,因此如果向 HashSet 添加一个已存在的元素时,元素不会被存入 HashMap 中,从而实现了 HashSet 元素不重复的特征。
源码分析
public class HashSet<E>
extends AbstractSet<E>
implements Set<E>, Cloneable, java.io.Serializable{
//序列化ID
static final long serialVersionUID = -5024744406713321676L;
//HashSet 底层用 HashMap 来存放数据
//Key值由外部传入,Value则由 HashSet 内部来维护
private transient HashMap<E,Object> map;
//HashMap 中所有键值对都共享同一个值
//即所有存入 HashMap 的键值对都是使用这个对象作为值
private static final Object PRESENT = new Object();
//无参构造函数,HashMap 使用默认的初始化大小和装载因子
public HashSet() {
map = new HashMap<>();
}
//使用默认的装载因子,并以此来计算 HashMap 的初始化大小
//+1 是为了弥补精度损失
public HashSet(Collection<? extends E> c) {
map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));
addAll(c);
}
//为 HashMap 自定义初始化大小和装载因子
public HashSet(int initialCapacity, float loadFactor) {
map = new HashMap<>(initialCapacity, loadFactor);
}
//为 HashMap 自定义初始化大小
public HashSet(int initialCapacity) {
map = new HashMap<>(initialCapacity);
}
//此构造函数为包访问权限,只用于对 LinkedHashSet 的支持
HashSet(int initialCapacity, float loadFactor, boolean dummy) {
map = new LinkedHashMap<>(initialCapacity, loadFactor);
}
//将对 HashSet 的迭代转换为对 HashMap 的 Key 值的迭代
public Iterator<E> iterator() {
return map.keySet().iterator();
}
//获取集合中的元素数量
public int size() {
return map.size();
}
//判断集合是否为空
public boolean isEmpty() {
return map.isEmpty();
}
//判断集合是否包含指定元素
public boolean contains(Object o) {
return map.containsKey(o);
}
//如果 HashSet 中不包含元素 e,则添加该元素,并返回 true
//如果 HashSet 中包含元素 e,则不会影响 HashSet ,并返回 false
//该方法将向 HashSet 添加元素 e 的操作转换为向 HashMap 添加键值对
//如果 HashMap 中包含 key 值与 e 相等的结点(hashCode() 方法返回值相等,通过 equals() 方法比较也返回 true)
//则新添加的结点的 value 会覆盖原有数据,但 key 不会有所改变
//因此如果向 HashSet 添加一个已存在的元素时,元素不会被存入 HashMap 中
//从而实现了 HashSet 元素不重复的特征
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
//移除集合中的元素 o
//如果集合不包含元素 o,则返回 false
public boolean remove(Object o) {
return map.remove(o)==PRESENT;
}
//清空集合中的元素
public void clear() {
map.clear();
}
@SuppressWarnings("unchecked")
public Object clone() {
try {
HashSet<E> newSet = (HashSet<E>) super.clone();
newSet.map = (HashMap<E, Object>) map.clone();
return newSet;
} catch (CloneNotSupportedException e) {
throw new InternalError(e);
}
}
private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException {
// Write out any hidden serialization magic
s.defaultWriteObject();
// Write out HashMap capacity and load factor
s.writeInt(map.capacity());
s.writeFloat(map.loadFactor());
// Write out size
s.writeInt(map.size());
// Write out all elements in the proper order.
for (E e : map.keySet())
s.writeObject(e);
}
/**
* Reconstitute the <tt>HashSet</tt> instance from a stream (that is,
* deserialize it).
*/
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
// Read in any hidden serialization magic
s.defaultReadObject();
// Read capacity and verify non-negative.
int capacity = s.readInt();
if (capacity < 0) {
throw new InvalidObjectException("Illegal capacity: " +
capacity);
}
// Read load factor and verify positive and non NaN.
float loadFactor = s.readFloat();
if (loadFactor <= 0 || Float.isNaN(loadFactor)) {
throw new InvalidObjectException("Illegal load factor: " +
loadFactor);
}
// Read size and verify non-negative.
int size = s.readInt();
if (size < 0) {
throw new InvalidObjectException("Illegal size: " +
size);
}
// Set the capacity according to the size and load factor ensuring that
// the HashMap is at least 25% full but clamping to maximum capacity.
capacity = (int) Math.min(size * Math.min(1 / loadFactor, 4.0f),
HashMap.MAXIMUM_CAPACITY);
// Create backing HashMap
map = (((HashSet<?>)this) instanceof LinkedHashSet ?
new LinkedHashMap<E,Object>(capacity, loadFactor) :
new HashMap<E,Object>(capacity, loadFactor));
// Read in all elements in the proper order.
for (int i=0; i<size; i++) {
@SuppressWarnings("unchecked")
E e = (E) s.readObject();
map.put(e, PRESENT);
}
}
//为了并行遍历数据源中的元素而设计的迭代器
public Spliterator<E> spliterator() {
return new HashMap.KeySpliterator<E,Object>(map, 0, -1, 0, 0);
}
}
LinkedHashSet
-
想要理解LinkedHashSet,则需要对 HashMap 、HashSet 和 LinkedHashMap 的源码有所了解,因为 LinkedHashSet 的内部实现都是来自于这三个容器类,其内部源码十分简单,简单到它只有一个成员变量、四个构造函数、一个 Set 接口的方法。
-
LinkedHashSet的所有源码
public class LinkedHashSet<E> extends HashSet<E> implements Set<E>, Cloneable, java.io.Serializable { //序列化ID private static final long serialVersionUID = -2851667679971038690L; //自定义初始容量与装载因子 public LinkedHashSet(int initialCapacity, float loadFactor) { super(initialCapacity, loadFactor, true); } //自定义初始容量 public LinkedHashSet(int initialCapacity) { super(initialCapacity, .75f, true); } //使用默认的初始容量以及装载因子 public LinkedHashSet() { super(16, .75f, true); } //使用初始数据、默认的初始容量以及装载因子 public LinkedHashSet(Collection<? extends E> c) { super(Math.max(2*c.size(), 11), .75f, true); addAll(c); } //并行遍历迭代器 @Override public Spliterator<E> spliterator() { return Spliterators.spliterator(this, Spliterator.DISTINCT | Spliterator.ORDERED); } }
-
LinkedHashSet 继承于 HashSet,而 LinkedHashSet 调用的父类构造函数均是
private transient HashMap<E,Object> map; HashSet(int initialCapacity, float loadFactor, boolean dummy) { map = new LinkedHashMap<>(initialCapacity, loadFactor); }
-
即 LinkedHashSet 底层是依靠 LinkedHashMap 来实现数据存取的,而 LinkedHashMap 继承于 HashMap,在内部自己维护了一条双向链表用于保存元素的插入顺序。
-
因此使得 LinkedHashSet 也具有了存取有序,元素唯一的特点。
TreeSet
TreeSet是根据二叉树实现的,也就是TreeMap, 放入数据不能重复且不能为null,可以重写compareTo()方法来确定元素大小,从而进行升序排序。TJava中的TreeSet是基于红黑树实现的Set集合。它继承自AbstractSet类,实现了NavigableSet接口,提供了按自然顺序或自定义比较器排序的元素迭代器。
TreeSet提供了一些常用的方法,例如add()、remove()、contains()、size()等方法。这些方法的时间复杂度都是O(log n),其中n是集合中元素的数量。
除了基本的集合操作,TreeSet还提供了一些高级的API,例如descendingSet()、tailSet()、subSet()等方法,它们返回的是一个新的TreeSet对象,但是按照不同的顺序进行排序。
需要注意的是,TreeSet中的元素必须实现Comparable接口或者提供一个Comparator对象来进行比较。如果元素没有实现Comparable接口,那么默认情况下会使用元素的自然顺序进行排序。如果需要自定义排序规则,则需要提供一个Comparator对象来实现比较逻辑。
常用方法:
TreeSet():默认构造函数,创建一个空的TreeSet集合。
TreeSet(Collection<? extends E> c):构造函数,创建一个包含指定集合元素的TreeSet集合。
add(E e):添加一个元素到TreeSet集合中。如果该元素已经存在,则不进行任何操作;否则,将该元素插入到合适的位置以保持有序性。
remove(Object o):删除指定元素,如果该元素存在,则删除并返回true;否则,返回false。
contains(Object o):检查TreeSet集合中是否包含指定元素,如果存在则返回true;否则,返回false。
first():返回TreeSet集合中的最小元素(按自然顺序排列)。如果该集合为空,则返回null。
last():返回TreeSet集合中的最大元素(按自然顺序排列)。如果该集合为空,则返回null。
headSet(E toElement):返回一个视图,包含TreeSet集合中小于指定元素的子集。该方法可以用于截取子集。
tailSet(E fromElement):返回一个视图,包含TreeSet集合中大于等于指定元素的子集。该方法可以用于截取子集。
subSet(E fromElement, E toElement):返回一个视图,包含TreeSet集合中在指定范围内的子集。该方法可以用于截取子集。
iterator():返回一个迭代器,用于遍历TreeSet集合中的元素。迭代器支持add操作,但每次迭代只能修改一次集合。
size():返回TreeSet集合中元素的数量。
isEmpty():检查TreeSet集合是否为空,如果为空则返回true;否则,返回false。
clear():清空TreeSet集合中的所有元素。
Iterator迭代器应对多线程并发修改的fail_fast机制
Iterator迭代器应对多线程并发修改的fail_fast机制是指在迭代过程中,如果集合的结构发生了改变(例如添加、删除元素),则迭代器会抛出ConcurrentModificationException异常。
fail_fast机制是一种避免多线程并发修改时出现不可预知结果的机制。当迭代器检测到集合结构发生改变时,它会立即抛出ConcurrentModificationException异常,而不是继续迭代下去。
这种机制可以保证数据的一致性和正确性,因为在多线程并发修改的情况下,可能会出现数据不一致的情况,例如一个线程正在遍历集合,而另一个线程在同时修改集合,这样就会导致迭代器遍历到不正确的数据。
需要注意的是,fail_fast机制并不是绝对安全的,它只能在一定程度上避免多线程并发修改时出现不可预知的结果。如果需要更加安全的数据操作,可以考虑使用线程安全的集合类,例如CopyOnWriteArrayList、ConcurrentHashMap等。