Java基础:HashMap和HashSet

转载请注明出处:jiq?钦‘s technical Blog

一、HashMap

HashMap,基于散列(哈希表)存储“Key-Value”对象引用的数据结构。

存入的键必须具备两个关键函数:

(1)equals():  判断两个Key是否相同,用来保证存入的Key的唯一性;

(2)hashCode(): 根据k-v对象的Key来计算其引用在散列表中存放的位置;

HashMap底层结构是一个数组:

transientEntry<K,V>[] table

而其中Entry<K,V>定义如下:

static classEntry<K,V> implements Map.Entry<K,V> {

final K key;

V value;

Entry<K,V> next;

int hash;

}

包含了key,value以及hash值,更重要的是还有一个指向下一个节点的next指针。

结合下面将要介绍的put方法可知HashMap底层是一个哈希表,以链接法解决冲突。

在网上找到一张画的比较好的图片:

1、public V put(K key, V value)

直接看代码:

public V put(K key,V value) {
        if (table == EMPTY_TABLE) {
            inflateTable(threshold);
        }

        //key为null时的插入
        if (key == null)
            return putForNullKey(value);

        //根据key计算hash值
        int hash = hash(key);

        //返回哈希表索引位置
        int i = indexFor(hash, table.length);

        //在哈希表中该索引处的链表中查找相同key的Entry<K,V>
        //注意table[i]是指向Entry<K,V>链表头结点的指针
        for (Entry<K,V> e = table[i]; e!= null; e = e.next) {
            Object k;
            if (e.hash == hash && ((k =e.key) == key || key.equals(k))) {
                   //通过equals方法判断找到相同key的节点,用新value覆盖旧value并返回旧value
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }

        modCount++;

        //创建一个新的Entry<K,V>实体,头插法插入到位置i处
        addEntry(hash, key, value, i);
        return null;
    }

其中针对key为null的处理如下:

private V putForNullKey(V value) {
//在hash表的第0个位置开始找是否已经有了key为null的节点
        for (Entry<K,V> e = table[0]; e!= null; e = e.next) {
            if (e.key == null) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }
        modCount++;

        //在hash表的第0个位置用头插法插入key为null的这个节点
        addEntry(0, null, value, 0);
        return null;
    }

其中通过key计算hash值方法如下:

final inthash(Object k) {
        int h = hashSeed;
        if (0 != h && k instanceofString) {
            returnsun.misc.Hashing.stringHash32((String) k);
        }

        //调用Key的hashCode()方法计算hash值
        h ^= k.hashCode();

        // This function ensures that hashCodesthat differ only by
        // constant multiples at each bitposition have a bounded
        // number of collisions (approximately8 at default load factor).
        h ^= (h >>> 20) ^ (h>>> 12);
        return h ^ (h >>> 7) ^ (h>>> 4);
    }

由此可以得出以下结论:

(1)当插入一个<Key, Value>,发现此Key已经存在时,将用新的value覆盖旧的value;

(2)当插入的<Key, Value>key为null时,将插入到hash表的位置0处,并且只会有一个key为null的节点;

(3)当插入一个<Key, Value>时通过Key的hashCode()方法计算在hash表中的索引,通过Key的equals()方法判断两个Entry<K,V>的Key是否相同,相同会覆盖,所以说插入的<Key, Value>中的Key必须实现这两个方法。

2、public V get(Object key)

根据Key返回Value方法就相对简单:

public V get(Objectkey) {
        if (key == null)
            return getForNullKey();
        Entry<K,V> entry = getEntry(key);

        return null == entry ? null :entry.getValue();
    }

其中getEntry(key)方法为主要实现:

final Entry<K,V> getEntry(Object key) {
        if (size == 0) {
            return null;
        }

        //根据Key计算hash值
        int hash = (key == null) ? 0 :hash(key);

        //indexFor(hash,table.length)根据hash值返回其在hash表中索引位置
        //在该索引位置指向的链表中查找Key相同的节点并返回其Value
        for (Entry<K,V> e =table[indexFor(hash, table.length)];
             e != null;
             e = e.next) {
            Object k;
            if (e.hash == hash &&
                ((k = e.key) == key || (key !=null && key.equals(k))))
                return e;
        }
        return null;
    }

3、散列表容量

HashMap有默认的装载因子loadFactor=0.75,默认的entry数组的长度为16。装载因子的意义在于使得entry数组有冗余,默认即允许25%的冗余,当HashMap的数据的个数超过12(16*0.75)时即会对entry数组进行第一次扩容,后面的再次扩容依次类推。

HashMap每次扩容一倍,resize时会将已存在的值从新进行数组下标的计算,这个是比较浪费时间的。在平时使用中,如果能估计出大概的HashMap的容量,可以合理的设置装载因子loadFactor和entry数组初始长度即可以避免resize操作,提高put的效率。

下面看看resize操作时如何进行的:

void resize(intnewCapacity) {
        Entry[] oldTable = table;
        int oldCapacity = oldTable.length;
        if (oldCapacity == MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return;
        }

        //根据新的容量重新创建hash表
        Entry[] newTable = newEntry[newCapacity];

        //逐个将节点拷贝至新hash表,较为耗时
        transfer(newTable,initHashSeedAsNeeded(newCapacity));
        table = newTable;
        threshold = (int)Math.min(newCapacity *loadFactor, MAXIMUM_CAPACITY + 1);
    }

4、类似结构

(1)Hashtable:

HashMap的早期版本,底层和HashMap类似,也是hash表存储,链接法解决冲突,通过synchronized关键字保证线程安全,下面看看其put方法,和HashMap的put方法很像:

public synchronizedV put(K key, V value) {
        //不允许Key为null的情况
        if (value == null) {
            throw new NullPointerException();
        }

        Entry tab[] = table;

        //利用key的hashCode()方法计算hash值
        int hash = hash(key);

        //除留余数法计算索引
        int index = (hash & 0x7FFFFFFF) %tab.length;

        //通过equals()方法找到key相同的节点覆盖
        for (Entry<K,V> e = tab[index] ;e != null ; e = e.next) {
            if ((e.hash == hash) &&e.key.equals(key)) {
                V old = e.value;
                e.value = value;
                return old;
            }
        }

        modCount++;
        if (count >= threshold) {
            // Rehash the table if thethreshold is exceeded
            rehash();

            tab = table;
            hash = hash(key);
            index = (hash & 0x7FFFFFFF) %tab.length;
        }

        //插入新节点
        Entry<K,V> e = tab[index];
        tab[index] = new Entry<>(hash,key, value, e);
        count++;
        return null;
    }

(2)ConcurrentHashMap

HashMap的线程安全版本,底层和HashMap几乎类似,也是采用hash表存储,链接法解决冲突,通过Key的hashCode()方法计算hash表索引,通过key的equals()方法判断两个Key是否相同。

不同点在于,ConcurrentHashMap针对hash表提出了一个“分段”的概念,每次插入一个<Key,Value>的时候,都先逐个分段请求获取锁,获取成功之后再执行在该hash表分段的插入操作。

总结:三者底层实现都是一样,但是不同之处在于是否线程安全,以及实现线程安全的方式。HashMap不支持线程安全,是一个简单高效的版本,Hashtable通过synchronized关键字简单粗暴地实现了一个线程安全的HashMap,而新的ConcurrentHashMap通过一种叫做分段的灵活的方式实现了线程安全的HashMap

所以无论出于什么原因,旧的Hashtable不建议再使用,若没有并发访问需求,推荐HashMap,否则推荐线程安全的ConcurrentHashMap。

二、HashSet

HashSet是为独立元素的存放而设计的哈希存储,优点是快速存取。

HashSet的设计较为“偷懒”,其直接在HashMap上封装而成

public classHashSet<E>
    extends AbstractSet<E>
    implements Set<E>, Cloneable,java.io.Serializable
{
    static final long serialVersionUID =-5024744406713321676L;

    private transient HashMap<E,Object>map;

    // Dummy value to associate with an Objectin the backing Map
    private static final Object PRESENT = newObject();

可以看到底层就是一个HashMap,Key存放放入集合的元素,而对应的Value则是一个任意对象PRESENT 。

下面是其Put方法:

public boolean add(Ee) {
        return map.put(e, PRESENT)==null;
    }

下面是返回迭代器的方法:

publicIterator<E> iterator() {
        return map.keySet().iterator();
    }

版权声明:本文为博主原创文章,未经博主允许不得转载。

时间: 2024-10-11 16:17:55

Java基础:HashMap和HashSet的相关文章

不惑JAVA之JAVA基础 - HashMap

HashMap应该是平时应用开发中或是框架设计中最为常用高效的容器.在介绍HashMap之前,先介绍两个常见的区别.后期会专门介绍CurrentHashMap. hashmap 和 hashtable 区别 HashMap和HashTable有什么区别,一个比较简单的回答是: HashMap是非线程安全的,HashTable是线程安全的. HashMap的键和值都允许有null值存在,而HashTable则不行. 因为线程安全的问题,HashMap效率比HashTable的要高. hashmap

Java容器 HashMap与HashSet的学习

Java学习中,看到HashMap,HashSet类,本着不止要停留在用的层面( 很多公司面试都要问底层 ),学习了JDK源码,记录下笔记. 源码来自jdk1.7下的src.zip HashMap是一种键值对类型,它提供一种Key-Value对应保存的数据结构,实现了Map接口,其中key的值唯一,即一个key某一时刻只能映射到唯一的值. 看其中几个成员(没列全) static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 sta

【转】Java学习---HashMap和HashSet的内部工作机制

[原文]https://www.toutiao.com/i6593863882484220430/ HashMap和HashSet的内部工作机制 HashMap 和 HashSet 内部是如何工作的?散列函数(hashing function)是什么? HashMap 不仅是一个常用的数据结构,在面试中也是热门话题. Q1. HashMap 如何存储数据? A1. 以键/值对(key/value)形式存储.你可以使用键(key)来存.取值. Q2. HashMap 查询时间的复杂度是怎样的? A

Java基础--HashMap面试题

import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; /** * HashMap问题 * @author 15735400536 * 使用HashMap,如果key是自定义的类,就必须重写hashcode()和equals() * 使用ArrayList,如果元素是自定义的类,要做相等判断,就必须重写hashCode()和equals() */ publ

java遍历hashMap、hashSet、Hashtable

一.遍历HashMap Map<Integer, String> map = new HashMap<Integer, String>(); 方法一:效率高 for(Entry<Integer, String> entry:map.entrySet()){ System.out.println(entry.getKey()+"="+entry.getValue()); } 方法二: for (Integer key : map.keySet()) {

[Java基础]HashMap的那些事

提到HashMap,使用Java语言的人来说,是再熟悉不过了.今天就简单聊聊我们认识的HashMap; 首先我们看一下Java中的HashMap类 public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable { private static final long serialVersionUID = 362498820763181265L;

java 基础 ---HashMap、HashTable

HashMap.HashTable区别. 1.HashMap线程不安全,HashTable线程安全: 2.HashMap的键和值都允许null值存在,而HashTable不允许: 3.HashMap的效率高于Hashtable * Hash table based implementation of the <tt>Map</tt> interface.  This * implementation provides all of the optional map operatio

Java基础——HashMap源码分析

本篇介绍的HashMap综合了ArrayList和LinkedList这两个集合的优势,它的底层是基于哈希表实现的,如果不考虑哈希冲突的话,HashMap在增删改查操作上的时间复杂度都能够达到惊人的O(1). 对于HashMap类源码中开头注释翻译: HashMap基于哈希表的 Map 接口的实现.此实现提供所有可选的映射操作,并允许使用 null 值和 null 键.(除了不同步和允许使用 null 之外,HashMap 类与 Hashtable 大致相同.)此类不保证映射的顺序,特别是它不保

java基础---HashMap和HashTable的异同之处

1:连个都实现了Map的接口,两者的底层数据结构相同,都是transient修饰的entry数组,transient修饰的变量不会序列化即不会持久化,序列化的过程中不会包含这个变量,这个变量的生命周期仅仅是在用户的调用过程中,不能持久化到内存磁盘中.这样便于一些敏感信息的保存 2:HashMap是不安全的,不能同步,不支持多线程并发,HashTable是安全的,有同步锁,但效率低. 3:HashMap从AbstractMap继承而来,HashTable是从Dictionary继承而来 4:Has

java基础-HashMap

jdk7的HashMap实现的思路比较简单,就是一个Entry数组,数组中每个Entry都是一个链表的起点(表头). 1 public V put(K key, V value) { 2 if (table == EMPTY_TABLE) { 3 inflateTable(threshold); 4 } 5 //如果key为null,则将该entry放在第0位 6 if (key == null) 7 return putForNullKey(value); 8 int hash = hash(