JAVA提高十九:WeakHashMap&EnumMap&LinkedHashMap&LinkedHashSet深入分析

因为最近工作太忙了,连续的晚上支撑和上班,因此没有精力来写下这篇博客,今天上午正好有一点空,因此来复习一下不太常用的集合体系大家族中的几个类:WeakHashMap&EnumMap&LinkedHashMap&LinkedHashSet,以便用到的时候不至于是什么都不知道。好了,言归正传,下面我们开始依次学习下:

一、WeakHashMap

1.WeakHashMap的结构

java.lang.Object
↳ java.util.AbstractMap<K, V>
↳ java.util.WeakHashMap<K, V> public class WeakHashMap<K,V>
extends AbstractMap<K,V>
implements Map<K,V> {}

关系图:

JAVA提高十九:WeakHashMap&EnumMap&LinkedHashMap&LinkedHashSet深入分析

从图中可以看出:
(01) WeakHashMap继承于AbstractMap,并且实现了Map接口。
(02) WeakHashMap是哈希表,但是它的键是"弱键"。WeakHashMap中保护几个重要的成员变量:table, size, threshold, loadFactor, modCount, queue。
  table是一个Entry[]数组类型,而Entry实际上就是一个单向链表。哈希表的"key-value键值对"都是存储在Entry数组中的。
  size是Hashtable的大小,它是Hashtable保存的键值对的数量。
  threshold是Hashtable的阈值,用于判断是否需要调整Hashtable的容量。threshold的值="容量*加载因子"。
  loadFactor就是加载因子。
  modCount是用来实现fail-fast机制的
  queue保存的是“已被GC清除”的“弱引用的键”。

上面我们提到了,"弱键",那么有必要解释一下:

1)强引用,任何时候都不会被垃圾回收器回收,如果内存不足,宁愿抛出OutOfMemoryError。
2)软引用,只有在内存将满的时候才会被垃圾回收器回收,如果还有可用内存,垃圾回收器不会回收。
3)弱引用,只要垃圾回收器运行,就肯定会被回收,不管还有没有可用内存。
4)虚引用,虚引用等于没有引用,任何时候都有可能被垃圾回收。

详细的了解,等JVM分析的时候进行讲解,在这里只需要有一个概念就好,因为本章的重点不是讲JVM。如果提前想了解可以参考:

WeakHashMap实现了Map接口,是HashMap的一种实现,它比HashMap多了一个引用队列:

private final ReferenceQueue<Object> queue = new ReferenceQueue<>();

对比WeakHashMap和HashMap的源码,会发现WeakHashMap中方法的实现方式基本和HashMap的一样,注意“基本”两个字,除了没有实现Cloneable和Serializable这两个标记接口,最大的区别在于在于expungeStaleEntries()这个方法,这个是整个WeakHashMap的精髓,我们稍后进行阐述。 
它使用弱引用作为内部数据的存储方案。WeakHashMap是弱引用的一种典型应用,它可以为简单的缓存表解决方案。

2.WeakHashMap使用

我们举一个简单的例子来说明一下WeakHashMap的使用:

package com.pony1223;

import java.util.Map;
import java.util.WeakHashMap; public class WeakHashMapDemo
{
public static void main(String[] args)
{
Map<String,Integer> map = new WeakHashMap<String,Integer>();
map.put("s1", 1);
map.put("s2", 2);
map.put("s3", 3);
map.put("s4", 4);
map.put("s5", 5);
map.put(null, 9);
map.put("s6", 6);
map.put("s7", 7);
map.put("s8", 8);
map.put(null, 11);
for(Map.Entry<String,Integer> entry:map.entrySet())
{
System.out.println(entry.getKey()+":"+entry.getValue());
}
System.out.println(map);
} }

运行结果:

JAVA提高十九:WeakHashMap&EnumMap&LinkedHashMap&LinkedHashSet深入分析

WeakHashMap和HashMap一样key和value的值都可以为null,并且也是无序的。但是HashMap的null是存在table[0]中的,这是固定的,并且null的hash为0,而在WeakHashMap中的null却没有存入table[0]中。

public V put(K key, V value) {
K k = (K) maskNull(key);
int h = HashMap.hash(k.hashCode());
Entry[] tab = getTable();
int i = indexFor(h, tab.length); for (Entry<K,V> e = tab[i]; e != null; e = e.next) {
if (h == e.hash && eq(k, e.get())) {
V oldValue = e.value;
if (value != oldValue)
e.value = value;
return oldValue;
}
}

第一步的操作方法为:maskNull

private static final Object NULL_KEY = new Object();
private static Object maskNull(Object key) {
return (key == null) ? NULL_KEY : key;
}
static Object unmaskNull(Object key) {
return (key == NULL_KEY) ? null : key;
}

当对map进行put和get操作的时候,将null值标记为NULL_KEY,然后对NULL_KEY即对new Object()进行与其他对象一视同仁的hash,这样就使得null和其他非null的值毫无区别。(做了一次包装)

3.关键源码分析

 首先看一下Entry<K,V>这个静态内部类:

private static class Entry<K,V> extends WeakReference<Object> implements Map.Entry<K,V> {
V value;
int hash;
Entry<K,V> next; /**
* Creates new entry.
*/
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;
}
//其余代码略
}

可以看到Entry继承扩展了WeakReference类(有关Java的引用类型暂不深入分析)。并在其构造函数中,构造了key的弱引用。

此外,在WeakHashMap的各项操作中,比如get()、put()、size()都间接或者直接调用了expungeStaleEntries()方法,以清理持有弱引用的key的表象。

以put为例:

 public V put(K key, V value) {
K k = (K) maskNull(key);
int h = HashMap.hash(k.hashCode());
Entry[] tab = getTable();
int i = indexFor(h, tab.length); for (Entry<K,V> e = tab[i]; e != null; e = e.next) {
if (h == e.hash && eq(k, e.get())) {
V oldValue = e.value;
if (value != oldValue)
e.value = value;
return oldValue;
}
} modCount++;
Entry<K,V> e = tab[i];
tab[i] = new Entry<K,V>(k, value, queue, h, e);
if (++size >= threshold)
resize(tab.length * 2);
return null;
}

其中resize方法中:

void resize(int newCapacity) {
Entry[] oldTable = getTable();
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
} Entry[] newTable = new Entry[newCapacity];
transfer(oldTable, newTable);
table = newTable; /*
* If ignoring null elements and processing ref queue caused massive
* shrinkage, then restore old table. This should be rare, but avoids
* unbounded expansion of garbage-filled tables.
*/
if (size >= threshold / 2) {
threshold = (int)(newCapacity * loadFactor);
} else {
expungeStaleEntries();
transfer(newTable, oldTable);
table = oldTable;
}
}

可以看到其间接的调用了expungeStaleEntries(); 方法的实现如下:

private void expungeStaleEntries() {
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;
// Must not null out e.next;
// stale entries may be in use by a HashIterator
e.value = null; // Help GC
size--;
break;
}
prev = p;
p = next;
}
}
}
}

可以看到每调用一次expungeStaleEntries()方法,就会在引用队列中寻找是否有被清楚的key对象,如果有则在table中找到其值,并将value设置为null,next指针也设置为null,让GC去回收这些资源。

4.场景应用

如果在一个普通的HashMap中存储一些比较大的值如下:

Map<Integer,Object> map = new HashMap<>();
for(int i=0;i<10000;i++)
{
Integer ii = new Integer(i);
map.put(ii, new byte[i]);
}

运行参数:-Xmx5M

运行结果:

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at collections.WeakHashMapTest.main(WeakHashMapTest.java:39)

同样我们将HashMap换成WeakHashMap其余都不变:

 Map<Integer,Object> map = new WeakHashMap<>();
for(int i=0;i<10000;i++)
{
Integer ii = new Integer(i);
map.put(ii, new byte[i]);
}

运行结果:(无任何报错)

通过上面的对比可以看到WeakHashMap的功效,如果在系统中需要一张很大的Map表,Map中的表项作为缓存只用,这也意味着即使没能从该Map中取得相应的数据,系统也可以通过候选方案获取这些数据。虽然这样会消耗更多的时间,但是不影响系统的正常运行。

在这种场景下,使用WeakHashMap是最合适的。因为WeakHashMap会在系统内存范围内,保存所有表项,而一旦内存不够,在GC时,没有被引用的表项又会很快被清除掉,从而避免系统内存溢出。

我们这里稍微改变一下上面的代码(加了一个List):

Map<Integer,Object> map = new WeakHashMap<>();
List<Integer> list = new ArrayList<>();
for(int i=0;i<10000;i++)
{
Integer ii = new Integer(i);
list.add(ii);
map.put(ii, new byte[i]);
}

运行结果:

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at collections.WeakHashMapTest.main(WeakHashMapTest.java:43)

如果存放在WeakHashMap中的key都存在强引用,那么WeakHashMap就会退化成HashMap。如果在系统中希望通过WeakHashMap自动清楚数据,请尽量不要在系统的其他地方强引用WeakHashMap的key,否则,这些key就不会被回收,WeakHashMap也就无法正常释放它们所占用的表项

因此,要想WeakHashMap能够释放掉key被GC的value的对象,尽可能的多调用下put/size/get等操作,因为这些方法会调用expungeStaleEntries方法,expungeStaleEntries方法是关键,而如果不操作WeakHashMap,以企图WeakHashMap“自动”释放内存是不可取的,这里的“自动”是指譬如map.put(obj,new byte[10M]);之后obj=null了,之后再也没掉用过map的任何方法,那么new出来的10M空间是不会释放的。

二、EnumMap

 1.定义

package java.util;

import java.util.Map.Entry;
import sun.misc.SharedSecrets;
public class EnumMap<K extends Enum<K>, V> extends AbstractMap<K, V>
implements java.io.Serializable, Cloneable{
private final Class<K> keyType;
private transient K[] keyUniverse;
private transient Object[] vals;
private transient int size = 0;
}

keyType变量是EnumMap的key泛型的类对象,EnumMap根据这个类型,可以获得keyUniverse的内容,vals存放的是与keyUniverse映射的值,如果没有映射则为null,如果映射为null则会特殊处理成NULL,NULL的定义如下:

 private static final Object NULL = new Object() {
public int hashCode() {
return 0;
} public String toString() {
return "java.util.EnumMap.NULL";
}
};

对于值NULL的处理类似WeakHashMap的特殊处理,会有两个方法:

private Object maskNull(Object value) {
return (value == null ? NULL : value);
} private V unmaskNull(Object value) {
return (V) (value == NULL ? null : value);
}

这样可以区分vals中是null(即没有映射)还是NULL(即映射为null); 
EnumMap的size是根据vals中的非null(包括NULL)的值的个数确定的,比如put方法:

public V put(K key, V value) {
typeCheck(key); int index = key.ordinal();
Object oldValue = vals[index];
vals[index] = maskNull(value);
if (oldValue == null)
size++;
return unmaskNull(oldValue);
}

typeCheck判断key的类对象或者父类对象是否与keyType相等,如果不相等则抛出ClassCastException异常。 
注意EnumMap并没有类似HashMap的resize的过程,也没有加载因子的概念,因为在一个EnumMap创建的时候,keyUniverse和vals的大小就固定。

2.EnumMap使用

先看一个例子:

package com.pony1223;

import java.util.EnumMap;
import java.util.Map; public class EnumMapDemo
{
public enum Color
{
RED,BLUE,BLACK,YELLOW,GREEN;
} public static void main(String[] args)
{
EnumMap<Color,String> map = new EnumMap<Color,String>(Color.class); map.put(Color.YELLOW, "黄色");
map.put(Color.RED, "红色");
map.put(Color.BLUE, null);
// map.put(null, "无"); //会报NullPonitException的错误
map.put(Color.BLACK, "黑色");
map.put(Color.GREEN, "绿色"); for(Map.Entry<Color,String> entry:map.entrySet())
{
System.out.println(entry.getKey()+":"+entry.getValue());
}
System.out.println(map);
} }

运行结果:

JAVA提高十九:WeakHashMap&EnumMap&LinkedHashMap&LinkedHashSet深入分析

EnumMap的key不允许为null,value可以为null,按照key在enum中的顺序进行保存,非线程安全。可以用工具类Collections进行包装成线程安全的:

Map<EnumKey, V> m = Collections.synchronizedMap(new EnumMap<EnumKey, V>(...));

EnumMap的基本操作都比较快,都在常量时间内完成,基本上(但不保证)比HashMap快。

EnumMap有三个构造函数:

  • public EnumMap(Class<K> keyType);
  • public EnumMap(EnumMap<K, ? extends V> m);
  • public EnumMap(Map<K, ? extends V> m) ;

前两个构造函数一目了然,对第三个构造函数进行分析:

        Map<Integer,Integer> map1 = new HashMap<Integer,Integer>();
map1.put(1, 1);
map1.put(3, 3);
map1.put(2, 2);
Map<Integer,Integer> map2 = new EnumMap<>(map1);//编译器提示错误:Cannot infer type arguments for EnumMap<>

这个是因为Integer并不是extends Enum; 这里变换一下:

 Map<Enum,Integer> map1 = new HashMap<Enum,Integer>();
map1.put(Color.YELLOW, 1);
map1.put(Color.RED, 3);
map1.put(Color.BLUE, 2);
Map<Enum,Integer> map2 = new EnumMap<Enum,Integer>(map1); for(Map.Entry entry:map2.entrySet())
{
System.out.println(entry.getKey()+":"+entry.getValue());
}
System.out.println(map2);
System.out.println(map2.size());

能够正常运行,输出结果:

RED:3
BLUE:2
YELLOW:1
{RED=3, BLUE=2, YELLOW=1}
3

 3.场景应用

《Effective Java》中作者建议用EnumMap代替叙述索引,最好不要用序数来索引数组,而要使用EnumMap。这里采用《Effective Java》书中的例子来举例。

    public static class Herb
{
public enum Type
{
ANNUAL, PERENNIAL, BIENNTAL
} private final String name;
private final Type type; public Herb(String name, Type type)
{
this.name = name;
this.type = type;
} public Type getType()
{
return type;
} @Override
public String toString()
{
return name;
}
}

现在用一座种满香草的花园,想要按照类型(一年生、多年生、两年生,即上面Type的类型)进行组织之后将这些植物列出来。如果使用数组实现的话,需要构建三个集合,每种类型一个,并且遍历整座花园,将每种香草放到相应的集合中。

Herb[] garden = new Herb[]{new Herb("f1",Herb.Type.ANNUAL),new Herb("f2",Herb.Type.PERENNIAL),new Herb("f3",Herb.Type.BIENNTAL),
new Herb("f4",Herb.Type.PERENNIAL),new Herb("f5",Herb.Type.ANNUAL),new Herb("f6",Herb.Type.BIENNTAL),
new Herb("f7",Herb.Type.ANNUAL),new Herb("f8",Herb.Type.BIENNTAL),new Herb("f9",Herb.Type.PERENNIAL)}; Set<Herb>[] herbsByType = (Set<Herb>[]) new Set[Herb.Type.values().length];
for(int i=0;i<herbsByType.length;i++)
{
herbsByType[i] = new HashSet<Herb>();
}
for(Herb h:garden)
{
herbsByType[h.type.ordinal()].add(h);
}
for(int i=0;i<herbsByType.length;i++)
{
System.out.printf("%s:%s%n", Herb.Type.values()[i],herbsByType[i]);
}

运行结果:

ANNUAL:[f5, f7, f1]
PERENNIAL:[f4, f2, f9]
BIENNTAL:[f8, f3, f6]

这种方法确实可行,但是影藏着许多问题。因为数组不能和泛型兼容,程序需要进行未受检的转换,并且不能正确无误地进行编译。因为数组不知道它的索引代表着什么,你必须手工标注这些索引的输出。但是这种方法最严重的问题在于,当你访问一个按照枚举的叙述进行索引的数组时,使用正确的int值就是你的职责了,int不能提供枚举的类型安全。 
但是你可以用EnumMap改善这个程序:

Herb[] garden = new Herb[]{new Herb("f1",Herb.Type.ANNUAL),new Herb("f2",Herb.Type.PERENNIAL),new Herb("f3",Herb.Type.BIENNTAL),
new Herb("f4",Herb.Type.PERENNIAL),new Herb("f5",Herb.Type.ANNUAL),new Herb("f6",Herb.Type.BIENNTAL),
new Herb("f7",Herb.Type.ANNUAL),new Herb("f8",Herb.Type.BIENNTAL),new Herb("f9",Herb.Type.PERENNIAL)};
Map<Herb.Type, Set<Herb>> herbsByType = new EnumMap<>(Herb.Type.class);
for(Herb.Type t : Herb.Type.values())
{
herbsByType.put(t, new HashSet<Herb>());
}
for(Herb h:garden)
{
herbsByType.get(h.type).add(h);
}
System.out.println(herbsByType);

运行结果:

{ANNUAL=[f7, f1, f5], PERENNIAL=[f4, f2, f9], BIENNTAL=[f8, f6, f3]}

这段程序更剪短、更清楚,也更安全,运行速度方面可以与使用序数的数组相媲美。注意EnumMap构造器采用键类型的Class对象:这是一个有限制的类型令牌,它提供了运行时的泛型信息

小节:

EnumMap是专门为枚举类型量身定做的Map实现。虽然使用其它的Map实现(如HashMap)也能完成枚举类型实例到值得映射,但是使用EnumMap会更加高效:它只能接收同一枚举类型的实例作为键值,并且由于枚举类型实例的数量相对固定并且有限,所以EnumMap使用数组来存放与枚举类型对应的值。这使得EnumMap的效率非常高。EnumMap在内部使用枚举类型的ordinal()得到当前实例的声明次序,并使用这个次序维护枚举类型实例对应值在数组的位置。

三、LinkedHashMap

1.简单概述

LinkedHashMap是Map接口的哈希表和链接列表实现,具有可预知的迭代顺序。此实现提供所有可选的映射操作,并允许使用null值和null键。此类不保证映射的顺序,特别是它不保证该顺序恒久不变。
LinkedHashMap实现与HashMap的不同之处在于,前者维护着一个运行于所有条目的双重链接列表。此链接列表定义了迭代顺序,该迭代顺序可以是插入顺序或者是访问顺序。
注意,此实现不是同步的。如果多个线程同时访问链接的哈希映射,而其中至少一个线程从结构上修改了该映射,则它必须保持外部同步。

2. LinkedHashMap的实现

对于LinkedHashMap而言,它继承与HashMap、底层使用哈希表与双向链表来保存所有元素。其基本操作与父类HashMap相似,它通过重写父类相关的方法,来实现自己的链接列表特性。下面我们来分析LinkedHashMap的源代码:

1) Entry元素:

LinkedHashMap采用的hash算法和HashMap相同,但是它重新定义了数组中保存的元素Entry,该Entry除了保存当前对象的引用外,还保存了其上一个元素before和下一个元素after的引用,从而在哈希表的基础上又构成了双向链接列表。看源代码:

/**
* 双向链表的表头元素。
*/
private transient Entry<K,V> header; /**
* LinkedHashMap的Entry元素。
* 继承HashMap的Entry元素,又保存了其上一个元素before和下一个元素after的引用。
*/
private static class Entry<K,V> extends HashMap.Entry<K,V> {
Entry<K,V> before, after;
……
}

2) 初始化:
通过源代码可以看出,在LinkedHashMap的构造方法中,实际调用了父类HashMap的相关构造方法来构造一个底层存放的table数组。如:

public LinkedHashMap(int initialCapacity, float loadFactor) {
super(initialCapacity, loadFactor);
accessOrder = false;
}

我们已经知道LinkedHashMap的Entry元素继承HashMap的Entry,提供了双向链表的功能。在上述HashMap的构造器中,最后会调用init()方法,进行相关的初始化,这个方法在HashMap的实现中并无意义,只是提供给子类实现相关的初始化调用。LinkedHashMap重写了init()方法,在调用父类的构造方法完成构造后,进一步实现了对其元素Entry的初始化操作。

void init() {
header = new Entry<K,V>(-1, null, null, null);
header.before = header.after = header;
}

3) 存储:
LinkedHashMap并未重写父类HashMap的put方法,而是重写了父类HashMap的put方法调用的子方法void addEntry(int hash, K key, V value, int bucketIndex) 和void createEntry(int hash, K key, V value, int bucketIndex),提供了自己特有的双向链接列表的实现。

void addEntry(int hash, K key, V value, int bucketIndex) {
// 调用create方法,将新元素以双向链表的的形式加入到映射中。
createEntry(hash, key, value, bucketIndex); // 删除最近最少使用元素的策略定义
Entry<K,V> eldest = header.after;
if (removeEldestEntry(eldest)) {
removeEntryForKey(eldest.key);
} else {
if (size >= threshold)
resize(2 * table.length);
}
} void createEntry(int hash, K key, V value, int bucketIndex) {
HashMap.Entry<K,V> old = table[bucketIndex];
Entry<K,V> e = new Entry<K,V>(hash, key, value, old);
table[bucketIndex] = e;
// 调用元素的addBrefore方法,将元素加入到哈希、双向链接列表。
e.addBefore(header);
size++;
} private void addBefore(Entry<K,V> existingEntry) {
after = existingEntry;
before = existingEntry.before;
before.after = this;
after.before = this;
}

4) 读取:

LinkedHashMap重写了父类HashMap的get方法,实际在调用父类getEntry()方法取得查找的元素后,再判断当排序模式accessOrder为true时,记录访问顺序,将最新访问的元素添加到双向链表的表头,并从原来的位置删除。由于的链表的增加、删除操作是常量级的,故并不会带来性能的损失。

public V get(Object key) {
// 调用父类HashMap的getEntry()方法,取得要查找的元素。
Entry<K,V> e = (Entry<K,V>)getEntry(key);
if (e == null)
return null;
// 记录访问顺序。
e.recordAccess(this);
return e.value;
} void recordAccess(HashMap<K,V> m) {
LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;
// 如果定义了LinkedHashMap的迭代顺序为访问顺序,
// 则删除以前位置上的元素,并将最新访问的元素添加到链表表头。
if (lm.accessOrder) {
lm.modCount++;
remove();
addBefore(lm.header);
}
}

3.使用

通过上面我们知道了,LinkedHashMap是Hash表和链表的实现,并且依靠着双向链表保证了迭代顺序是插入的顺序。非线程安全

Map<String,Integer> map = new LinkedHashMap<String,Integer>();
map.put("s1", 1);
map.put("s2", 2);
map.put("s3", 3);
map.put("s4", 4);
map.put("s5", 5);
map.put(null, 9);
map.put("s6", 6);
map.put("s7", 7);
map.put("s8", 8);
map.put(null, 11);
for(Map.Entry<String,Integer> entry:map.entrySet())
{
System.out.println(entry.getKey()+":"+entry.getValue());
}
System.out.println(map);

结果:

s1:1
s2:2
s3:3
s4:4
s5:5
null:11
s6:6
s7:7
s8:8
{s1=1, s2=2, s3=3, s4=4, s5=5, null=11, s6=6, s7=7, s8=8}
关  注  点 结      论
LinkedHashMap是否允许空 Key和Value都允许空
LinkedHashMap是否允许重复数据 Key重复会覆盖、Value允许重复
LinkedHashMap是否有序 有序
LinkedHashMap是否线程安全 非线程安全

总结:

其实 LinkedHashMap 几乎和 HashMap 一样:从技术上来说,不同的是它定义了一个 Entry<K,V> header,这个 header 不是放在 Table 里,它是额外独立出来的。LinkedHashMap 通过继承 hashMap 中的 Entry<K,V>,并添加两个属性 Entry<K,V> before,after,和 header 结合起来组成一个双向链表,来实现按插入顺序或访问顺序排序。
在写关于 LinkedHashMap 的过程中,记起来之前面试的过程中遇到的一个问题,也是问我 Map 的哪种实现可以做到按照插入顺序进行迭代?当时脑子是突然短路的,但现在想想,也只能怪自己对这个知识点还是掌握的不够扎实,所以又从头认真的把代码看了一遍。
不过,我的建议是,大家首先首先需要记住的是:LinkedHashMap 能够做到按照插入顺序或者访问顺序进行迭代,这样在我们以后的开发中遇到相似的问题,才能想到用 LinkedHashMap 来解决,否则就算对其内部结构非常了解,不去使用也是没有什么用的。我们学习的目的是为了更好的应用。

四、LinkedHashSet

1.LinkedHashSet 概述

之前的博文中,分别写了 HashMap 和 HashSet,然后我们可以看到 HashSet 的方法基本上都是基于 HashMap 来实现的,说白了,HashSet内部的数据结构就是一个 HashMap,其方法的内部几乎就是在调用 HashMap 的方法。LinkedHashSet 首先我们需要知道的是它是一个 Set 的实现,所以它其中存的肯定不是键值对,而是值。此实现与 HashSet 的不同之处在于,LinkedHashSet 维护着一个运行于所有条目的双重链接列表。此链接列表定义了迭代顺序,该迭代顺序可为插入顺序或是访问顺序。
看到上面的介绍,是不是感觉其与 HashMap 和 LinkedHashMap 的关系很像?
注意,此实现不是同步的。如果多个线程同时访问链接的哈希Set,而其中至少一个线程修改了该 Set,则它必须保持外部同步。

2.LinkedHashSet 的实现

对于 LinkedHashSet 而言,它继承与 HashSet、又基于 LinkedHashMap 来实现的。
LinkedHashSet 底层使用 LinkedHashMap 来保存所有元素,它继承与 HashSet,其所有的方法操作上又与 HashSet 相同,因此 LinkedHashSet 的实现上非常简单,只提供了四个构造方法,并通过传递一个标识参数,调用父类的构造器,底层构造一个 LinkedHashMap 来实现,在相关操作上与父类 HashSet 的操作相同,直接调用父类 HashSet 的方法即可。LinkedHashSet 的源代码如下:

public class LinkedHashSet<E>
extends HashSet<E>
implements Set<E>, Cloneable, java.io.Serializable { private static final long serialVersionUID = -2851667679971038690L; /**
* 构造一个带有指定初始容量和加载因子的新空链接哈希set。
*
* 底层会调用父类的构造方法,构造一个有指定初始容量和加载因子的LinkedHashMap实例。
* @param initialCapacity 初始容量。
* @param loadFactor 加载因子。
*/
public LinkedHashSet(int initialCapacity, float loadFactor) {
super(initialCapacity, loadFactor, true);
} /**
* 构造一个带指定初始容量和默认加载因子0.75的新空链接哈希set。
*
* 底层会调用父类的构造方法,构造一个带指定初始容量和默认加载因子0.75的LinkedHashMap实例。
* @param initialCapacity 初始容量。
*/
public LinkedHashSet(int initialCapacity) {
super(initialCapacity, .75f, true);
} /**
* 构造一个带默认初始容量16和加载因子0.75的新空链接哈希set。
*
* 底层会调用父类的构造方法,构造一个带默认初始容量16和加载因子0.75的LinkedHashMap实例。
*/
public LinkedHashSet() {
super(16, .75f, true);
} /**
* 构造一个与指定collection中的元素相同的新链接哈希set。
*
* 底层会调用父类的构造方法,构造一个足以包含指定collection
* 中所有元素的初始容量和加载因子为0.75的LinkedHashMap实例。
* @param c 其中的元素将存放在此set中的collection。
*/
public LinkedHashSet(Collection<? extends E> c) {
super(Math.max(2*c.size(), 11), .75f, true);
addAll(c);
}
}

以上几乎就是 LinkedHashSet 的全部代码了,那么读者可能就会怀疑了,不是说 LinkedHashSet 是基于 LinkedHashMap 实现的吗?那我为什么在源码中甚至都没有看到出现过 LinkedHashMap。不要着急,我们可以看到在 LinkedHashSet 的构造方法中,其调用了父类的构造方法。我们可以进去看一下:

/**
* 以指定的initialCapacity和loadFactor构造一个新的空链接哈希集合。
* 此构造函数为包访问权限,不对外公开,实际只是是对LinkedHashSet的支持。
*
* 实际底层会以指定的参数构造一个空LinkedHashMap实例来实现。
* @param initialCapacity 初始容量。
* @param loadFactor 加载因子。
* @param dummy 标记。
*/
HashSet(int initialCapacity, float loadFactor, boolean dummy) {
map = new LinkedHashMap<E,Object>(initialCapacity, loadFactor);
}

在父类 HashSet 中,专为 LinkedHashSet 提供的构造方法如下,该方法为包访问权限,并未对外公开。
由上述源代码可见,LinkedHashSet 通过继承 HashSet,底层使用 LinkedHashMap,以很简单明了的方式来实现了其自身的所有功能。

总结
以上就是关于 LinkedHashSet 的内容,我们只是从概述上以及构造方法这几个方面介绍了,并不是我们不想去深入其读取或者写入方法,而是其本身没有实现,只是继承于父类 HashSet 的方法。
所以我们需要注意的点是:
LinkedHashSet 是 Set 的一个具体实现,其维护着一个运行于所有条目的双重链接列表。此链接列表定义了迭代顺序,该迭代顺序可为插入顺序或是访问顺序。
LinkedHashSet 继承与 HashSet,并且其内部是通过 LinkedHashMap 来实现的。有点类似于我们之前说的LinkedHashMap 其内部是基于 Hashmap 实现一样,不过还是有一点点区别的(具体的区别大家可以自己去思考一下)。
如果我们需要迭代的顺序为插入顺序或者访问顺序,那么 LinkedHashSet 是需要你首先考虑的。

上一篇:MySQL 5.6 my.cnf优化后的标准配置(4核 16G Centos6.5 x64)


下一篇:[BZOJ5073] [Lydsy1710月赛]小A的咒语 后缀数组+dp+贪心