转载请注明出处: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底层是一个哈希表。以链接法解决冲突。

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

watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvaml5aXFpbmxvdmV4eA==/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center" alt="" />

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表存储,链接法解决冲突,通过synchronizedkeyword保证线程安全,以下看看其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通过synchronizedkeyword简单粗暴地实现了一个线程安全的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();
}

Java容器:HashMap和HashSet解析的更多相关文章

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

    [原文]https://www.toutiao.com/i6593863882484220430/ HashMap和HashSet的内部工作机制 HashMap 和 HashSet 内部是如何工作的? ...

  2. 深入理解Java容器——HashMap

    目录 存储结构 初始化 put resize 树化 get 为什么equals和hashCode要同时重写? 为何HashMap的数组长度一定是2的次幂? 线程安全 参考 存储结构 JDK1.8前是数 ...

  3. [Java语言] HashMap,HashSet,Hashtable,Vector,ArrayList 的关系 <转>

    这么几个比较常用的但是比较容易混淆的概念同出于 java.util 包.本文仅作几个类的浅度解析. (本文基于JDK1.7,源码来自openjdk1.7.) ├── Collection │ ├── ...

  4. 基于Java的HashMap和HashSet实现

    一.Map接口类: import java.util.Iterator; public interface IMap<K, V> { /* 清除所有键值对 */ void clear(); ...

  5. 刷题upupup【Java中HashMap、HashSet用法总结】

    HashMap: 常用操作 1. containsKey() 判断HashMap是否包含key 2. containsValue() 判断HashMap是否包含“值为value”的元素 3. get( ...

  6. java容器HashMap原理

    1.为什么需要HashMap 前面我们说了ArrayList和LinkedList,它们对容器内的对象都能实现增.删.改.查.遍历等操作, 并且对应不同的情况,我们可以选择不同的List,用以提高效率 ...

  7. java遍历hashMap、hashSet、Hashtable

    一.遍历HashMap Map<Integer, String> map = new HashMap<Integer, String>(); 方法一:效率高 for(Entry ...

  8. java集合-HashMap源码解析

    HashMap 键值对集合 实现原理: HashMap 是基于数组 + 链表实现的. 通过hash值计算 数组索引,将键值对存到该数组中. 如果多个元素hash值相同,通过链表关联,再头部插入新添加的 ...

  9. Java容器深入浅出之HashSet、TreeSet和EnumSet

    Java集合中的Set接口,定义的是一类无顺序的.不可重复的对象集合.如果尝试添加相同的元素,add()方法会返回false,同时添加失败.Set接口包括3个主要的实现类:HashSet.TreeSe ...

随机推荐

  1. 网页图表控件Highcharts如何详细设置参数

    在下载了Highcharts范例之后,按照如下所示第一步第二步操作.在第二步中,默认并没有提供很多参数设置,比如如何去掉右下角的水印,如何自定义图标的高度宽度,背景颜色等等. 在我的另一篇文章中(Hi ...

  2. 主动通知Android系统图库进行更新

    项目中遇到调用图库进行图片的选择,因为不能主动及时更新,遂实现代码调用实现主动及时更新. 废话不多刷,看代码. 方式一,发送一个广播, sendBroadcast(new Intent(Intent. ...

  3. Android 事件分发

    引言 项目中涉及到的触摸事件分发较多,例如:歌词模式下,上下滑动滚动歌词,左右滑动切换歌曲.此时,理解事件分发机制显得尤为重要 , 既要保证下方的ViewPager能接收到,又要确保上层View能响应 ...

  4. 使用VisualSVN建立SVN服务器

    原地址:http://blog.csdn.net/happyjiang2009/article/details/5719988 以前使用官方Subversion搭建SVN版本控制环境,感觉很繁琐,需要 ...

  5. Loadrunner定时执行脚本

    # -*- coding: utf-8 -*- import timeimport os #格式为小时,分钟,脚本名称(包括盘符,最好是放在根目录下)#需要把LoadRunner安装路径的Bin加入系 ...

  6. 使用Monkeyrunner进行Android自动化的总结

    http://www.2cto.com/kf/201411/356056.html 使用Monkeyrunner进行Android自动化的总结 使用Android自动化的方式,不仅可以用来对Andro ...

  7. 乐鑫esp8266基于freeRtos实现私有服务器本地远程OTA升级

    目录 一.前言: 二.回顾下OTA的流程: 三.lwip网络框架的知识的使用: 四.如何处理服务器返回的数据? 五.扇区的擦除和烧写? 六.如何调用? 七.好好享用吧! 八.下载: 九.工程截图: 代 ...

  8. js可以关闭android页面上的键盘输入法

    尝试让获取焦点的元素失去焦点,document.activeElement.blur() js实现焦点进入文本框内关闭输入法:imeMode2011-05-26 11:23要用到的东西: imeMod ...

  9. mysql联合查询union

    mysql联合查询,对多表进行组合查询 使用 UNION ALL 命令实例 显示所有 使用UNION 过滤重复的 使方法 select * from (SELECT id,name as usenam ...

  10. 终端I/O termios属性设置 tcsetattr设置(转)

    终端I/O有两种不同的工作方式: 规范方式输入处理.在这种方式中,终端输入以行为单位进行处理.对于每个读要求,终端驱动程序最多返回一行. 非规范方式输入处理.输入字符不以行为单位进行装配. 如果不作特 ...