集合类解析(二):散列数据集合-HashMap(Set)源码拆解与Set,Map解析

原码拆解与流程图为原创,转载请注明。

本文为集合类解析(二),从根本上了解集合类,请先移步:

Collection 集合类、Iterator 迭代器、List解析移步:集合类解析(一):表结构与集合类Collection,Iterator,List基础讲解

 

集合类之Set与Map

阅读Set与Map的实现类源码即可发现,为何set与Map要一起描述,主要的Set实现类如HashSet和TreeSet,均是由其对应的Map结构实现而成。

 

Set

Java 源码中Set架构图:

  • Set是一个接口,继承于Collection,包含方法与Collection一致,是一个不允许有重复元素的集合。
  • AbstractSet是一个抽象类,继承AbstractCollection,并实现了Set接口。AbstractCollection实现了Set中的绝大部分函数,为Set的实现类提供了便利。
    public abstract class AbstractSet<E> extends AbstractCollection<E> implements Set<E>
  • HastSet 和 TreeSet 是Set的两个实现类,均由其对应的map实现而成。

 

HashSet

HashSet继承至AbstractSet,并实现Set,Cloneable,Serializable接口。

HashSet依赖于HashMap,其方法是通过HashMap实现的,同时HashSet中的元素是无序的。

public class HashSet<E>
    extends AbstractSet<E>
    implements Set<E>, Cloneable, java.io.Serializable

注意,此实现不是同步的。如果多个线程同时访问一个哈希 set,而其中至少一个线程修改了该 set,那么它必须 保持外部同步。这通常是通过对自然封装该 set 的对象执行同步操作来完成的。如果不存在这样的对象,则应该使用 Collections.synchronizedSet 方法来“包装” set。最好在创建时完成这一操作,以防止对该 set 进行意外的不同步访问:

   Set s = Collections.synchronizedSet(new HashSet(...));
  • 基本属性:
private transient HashMap<E,Object> map;  //map集合,HashSet存放元素的容器
private static final Object PRESENT = new Object(); //map中键对应的value值,常量,相当于占位符,逻辑上并不使用。
  • 构造方法:
//无参构造方法,创建要使用的map
public HashSet() {
    map = new HashMap<>();
}

//构造包含指定集合中的元素的新集合。
//使用默认的负载因子(0.75)和足以包含指定集合中的元素的初始容量创建。如果传入为空抛空指针异常
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的遍历操作,解释了HashSet对HashMap的使用方式。
 //HashSet调用了HashMap存放,因为HashSet并不是键值对存储,只是把Map中的键key设置为了Set的值,
 //在遍历HashSet的集合元素时,实际上是遍历的Map中Key的集合。
    public Iterator<E> iterator() {
        return map.keySet().iterator();
    }
  • 主要方法源码:
    public boolean isEmpty() {
        return map.isEmpty();
    }

    //contains使用map的containsKey方法来判断
    public boolean contains(Object o) {
        return map.containsKey(o);
    }

    //add时,放入传入的值,而map的value设置为常量,所以说在set中,PRESENT这个值用于占位,本身无意义
    public boolean add(E e) {
        return map.put(e, PRESENT)==null;
    }

    //与add()同理
    public boolean remove(Object o) {
        return map.remove(o)==PRESENT;
    }

    public void clear() {
        map.clear();
    }

    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();
        }
    }

    //输出流写方法
    private void writeObject(java.io.ObjectOutputStream s)
        throws java.io.IOException {
        ...
    }
    
    //输入流读取
    private void readObject(java.io.ObjectInputStream s)
        throws java.io.IOException, ClassNotFoundException {
        ...
        }
    }
  • LinkedHashSet

LinkedHashSet继承于HashSet,是具有可预知迭代顺序的 Set 接口的哈希表和链接列表实现。

此实现与 HashSet 的不同之处在于,后者维护着一个运行于所有条目的双重链接列表。此链接列表定义了迭代顺序,即按照将元素插入到 set 中的顺序(插入顺序)进行迭代。此实现可以让客户免遭未指定的、由 HashSet 提供的通常杂乱无章的排序工作,而又不致引起与 TreeSet 关联的成本增加。使用它可以生成一个与原来顺序相同的 set 副本,并且与原 set 的实现无关。

//LinkedHashSet使用其父类HashSet的构造方法
 public LinkedHashSet(int initialCapacity, float loadFactor) {
        super(initialCapacity, loadFactor, true);
    }

LinkedHashSet使用父类HashSet的构造方法,使用LinkedHashMap来处理LinkedHashSet操作。

  HashSet(int initialCapacity, float loadFactor, boolean dummy) {
        map = new LinkedHashMap<>(initialCapacity, loadFactor);
    }

 

Map

观察类图就会发现,Map的继承关系实际和Set的非常相似。也是由AbstractMap类实现大多数Map的基本操作。

而HashMap是HashSet的方法实现,LinkedHashMap是LinkedHashSet的实现方式。

 

HashMap

 

  • 基于哈希表的 Map 接口的实现。根据键的hashCode值存储数据,大多数情况下可以直接定位到它的值,具有很快的访问速度。
  • 此实现提供所有可选的映射操作,并允许使用 一个null键 key和多个null值value。
  • 此类不保证映射的顺序,同时不保证顺序不变。
  • 非线程安全。(除了非同步和允许使用 null 之外,HashMap 类与 Hashtable 大致相同。)
  • 如果需要满足线程安全,可以用 Collections的synchronizedMap方法使HashMap具有线程安全的能力,或者使用ConcurrentHashMap。

 

数据结构

HashMap的基本数据结构是一个由链表组成的数组。(在JDK1.8之后,当链表长度大于8时,转换为红黑树)

两张图示意:

举例来说:

 

HashMap重要的内部类 Entry

可以看出 Entry 实际上就是一个单向链表。这也是为什么说HashMap是通过拉链法解决哈希冲突的。

  static class Entry<K,V> implements Map.Entry<K,V> {
        final K key;
        V value;
        Entry<K,V> next;
        int hash;
        
     //Entry包含key,value,hash和next指针
        Entry(int h, K k, V v, Entry<K,V> n) {
            value = v;
            next = n;
            key = k;
            hash = h;
        }

     //Entry实现的方法
        public final K getKey() {return key; }
        public final V getValue() {return value;}
        public final V setValue(V newValue) {
            V oldValue = value;
            value = newValue;
            return oldValue;
        }

        public final boolean equals(Object o) {
            if (!(o instanceof Map.Entry))
                return false;
            Map.Entry e = (Map.Entry)o;
            Object k1 = getKey();
            Object k2 = e.getKey();
            if (k1 == k2 || (k1 != null && k1.equals(k2))) {
                Object v1 = getValue();
                Object v2 = e.getValue();
                if (v1 == v2 || (v1 != null && v1.equals(v2)))
                    return true;
            }
            return false;
        }

        public final int hashCode() {return Objects.hashCode(getKey()) ^ Objects.hashCode(getValue()); }
        public final String toString() { return getKey() + "=" + getValue();}
        void recordAccess(HashMap<K,V> m) {}
        void recordRemoval(HashMap<K,V> m) {}
    }
}

 

HashMap的核心代码在于put()操作,如果键值存在则更新数据,用新的value取代旧的value;如果键值不存在,调用addEntry(),将“key-value”添加到table中,此处便涉及hash数组和链表的插入操作。

//put方法
public V put(K key, V value) {
    // 若“key为null”,则将该键值对添加到table[0]中。
    if (key == null)
        return putForNullKey(value);
    // 若“key不为null”,则计算该key的哈希值(然后将其添加到该哈希值对应的链表中)
    int hash = hash(key.hashCode());
    
    //用indexFor来计算entry对象在table数组中的索引值,indexFor方法在下面单独列出
    int i = indexFor(hash, table.length);
    for (Entry<K,V> e = table[i]; e != null; e = e.next) {
        Object k;
        //遍历数组[i]位的链表,若“该key”对应的键值对已经存在,则用新的value取代旧的value。
        if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
            V oldValue = e.value;
            e.value = value;
            e.recordAccess(this);
            return oldValue;
        }
    }

    // 若“该key”对应的键值对不存在,则将“key-value”添加到table中
    modCount++;
    addEntry(hash, key, value, i);
    return null;
}


//增加键值,修改链表的关键操作
  void addEntry(int hash, K key, V value, int bucketIndex) {
        if ((size >= threshold) && (null != table[bucketIndex])) {
          // 若HashMap的实际大小 不小于 “阈值”,则调整HashMap的大小
            resize(2 * table.length);
            //如果key为null,添加至0位
            hash = (null != key) ? hash(key) : 0;
            //计算添加位置(数组下标)
            bucketIndex = indexFor(hash, table.length);
        }

        //1.7后,addEntry整合了creatEntry,执行添加节点操作
        createEntry(hash, key, value, bucketIndex);
    }


 void createEntry(int hash, K key, V value, int bucketIndex) {
        //在Entry数组table中获取bucketIndex位置的数据
        Entry<K,V> e = table[bucketIndex];
        
        //向table数组bucketIndex位置添加Entry对象,且放在链表第一位,把原值e设置成新数据的next
        //此操作的原因是通常情况下,后置入的数据有更多被访问的可能性,可提升性能
        table[bucketIndex] = new Entry<>(hash, key, value, e);
        size++;
    }

    //返回位置和值
    static int indexFor(int h, int length) {
        // assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
        return h & (length-1);
    }

}

上面的代码可以按此流程图来对照理解:

至此,HashMap的主要构建方法就可以理解了。

 

HashTable

  • Hashtable是遗留类,很多映射的常用功能与HashMap类似,不同的是它承自Dictionary类
  • 线程安全,实现线程安全的方式是在修改数据时锁住整个HashTable,效率低,ConcurrentHashMap做了相关优化
  • 底层数组+链表实现,无论key还是value都不能为null,除了非同步和不允许使用 null 之外,Hashtable与HashMap 类 大致相同。
  • 并发性不如ConcurrentHashMap,因为ConcurrentHashMap引入了分段锁。

 

ConcurrentHashMap

  • 底层采用分段的数组+链表实现,线程安全。
  • ConcurrentHashMap是使用了锁分段技术来保证线程安全的。ConcurrentHashMap中包括了“Segment(分段)数组”,每个Segment就是一个哈希表,也是可重入的互斥锁。通过把整个Map分为N个Segment,可以提供相同的线程安全,而且效率提升N倍,默认提升16倍。(读操作不加锁,由于HashEntry的value变量是 volatile的,也能保证读取到最新的值。)
    锁分段技术:首先将数据分成一段一段的存储,然后给每一段数据配一把锁,
    当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。
    而Hashtable的synchronized是针对整张Hash表的,即每次锁住整张表让线程独占,ConcurrentHashMap允许多个修改操作并发进行。
  • 有些方法需要跨段,比如size()和containsValue(),它们可能需要锁定整个表而而不仅仅是某个段,这需要按顺序锁定所有段,操作完毕后,又按顺序释放所有段的锁
  • 扩容:段内扩容(段内元素超过该段对应Entry数组长度的75%触发扩容,不会对整个Map进行扩容),插入前检测需不需要扩容,有效避免无效扩容。

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值