HashMap是Map接口下的集合。
HashMap继承于AbstractMap,实现了Map、Cloneable、java.io.Serializable接口。
HashMap有两个参数影响其性能:初始容量和加载因子,初始容量是哈希表在创建时的容量,默认为16个大小。加载因子默认为0.75,当哈希表中的节点个数超过加载因子*当前节点个数时,需要进行2倍扩容操作。
Map
key是不重复的,且key可以为null,元素的存储位置由key决定
通过key去寻找key-value存储的位置,得到key对应的value值,适合做查询
//没有指定数据类型,存储的类型就为任意的
HashMap hashMap1 = new HashMap();
HashMap hashMap = new HashMap<>();
hashMap.put("tom",34);
//允许存储null值
hashMap.put(null,null);
//添加元素时,key已存在,会修改对应的value值
hashMap.put(null,5);
//删除key为null的元素
//public V remove(Object key)
hashMap.remove(null);
//public void replace(Object key,V value)
hashMap.replace("tom",88);
//public V get(Object key) 获取key对应的value
System.out.println(hashMap.get("tom"));
//判断key是否存在
System.out.println(hashMap.containsKey("tom"));
通过entrySet,获得HashMap的“键值对”的Set集合
//entrySet迭代器遍历
Iterator> iterator = hashMap.entrySet().iterator();
while(iterator.hasNext()){
Map.Entry next = iterator.next();
System.out.println("key:" + next.getKey()+" "+"value:"+next.getValue());
}
通过keySet,获得HashMap的“键”的Set集合
// keySet迭代器遍历
Iterator iterator1 = hashMap.keySet().iterator();
while(iterator1.hasNext()){
String key = iterator1.next();
System.out.println("key:" + key);
}
通过values,获得HashMap的“值”的集合
// values迭代器遍历
Iterator iterator2 = hashMap.values().iterator();
while(iterator2.hasNext()){
Integer value = iterator2.next();
System.out.println("value" + value);
}
foreach遍历entrySet()
for(Map.Entry next : hashMap.entrySet()){
System.out.println("key:" + next.getKey()+" "+"value:"+next.getValue());
}
foreach遍历keySet()
for(String key: hashMap.keySet()){
System.out.println("key:" + key);
}
foreach遍历values()
for(Integer value:hashMap.values()){
System.out.println("value" + value);
}
HashMap是实现Map接口下的集合,以key-value的形式存储数据。
实现了Map.Entry
节点的参数:key、value、next
static class Entry{
K key;
V value;
Entry next;
int hash;
public Entry(K key, V value, Entry next,int hash) {
this.key = key;
this.value = value;
this.next = next;
this.hash = hash; //存储扰动处理后的hashcode即h,没有&(table.length)
}
}
如果key为null,对应的数组下标为0。
a. 求出hashcode值:int h = key.hashCode(); //求出来的h有可能为负
b. 扰动处理:降低hashCode的重复率,即降低index的重复率
( >>>) 解决index为负数的问题
c. & (table.length) 或者 % table.length:
两者相等的前提是:table.length必须是2的幂,所有默认table每次都是2被增长
使用& (table.length) ,因为位运算的效率高于算数运算
//扰动处理后的hash
private int hash(K key){
//如果key为空,其对应的下标为0
if(key == null){
return 0;
}
//index有可能为负数
int h = key.hashCode();
//扰动处理:降低hashCode的重复率,即降低index的重复率
// >>> 解决index为负数的问题
h ^= (h >>> 20) ^ (h >>> 12);
h = h ^ (h >>> 7) ^ (h >>> 4);
//位运算的效率高于算数运算,前提是table.length是2的幂,两者才相等
return h;
}
//取余table长度后index值下标
private int indexOf(int h){
return h & (table.length-1);
}
为什么要扩容?
扩容不是因为没有数据存储空间,为了提高查询效率
因为当数据量越大,哈希冲突越高
扩容之后要重新计算每一个节点对应的index,哈希冲突概率降低。
如何判需要进行扩容?
加载因子:默认的加载因子 = 0.75
当前键值对的个数 >= 容量 * 加载因子
为什么要保证2倍扩容?
因为要保证table.length是2的幂,因为经过扰动处理的哈希值,需要进行与运算。
//扩容方法
private void resize(){
int old_lenght = table.length;
int new_lenght = old_lenght * 2;
//定义一个数组存放以前的table
Entry[] old_table = table;
table = new Entry[new_lenght];
//对元素进行重新哈希
for(Entry e : old_table){
while(e != null){
Entry next = e.next;
int h = hash(e.key);
int index = indexOf(h);
e.next = table[index];
table[index] = e;
e = next;
}
}
}
注意1 :
HashMap允许存储key为null,或者value为null的元素,因此需要注意处理key为null的情况,避免出现空点访问操作。
对于key为空进行特殊处理
注意2 :
比较链表判断key是否重复:若重复新value替换老的value,如果不重复,使用头插法将新的节点插入到链表中。
添加操作步骤:
a. 判断如果第一次添加元素,将数组扩容至16个大小;
b. 通过哈希计算key对应的数组下标
c. 通过index定位到该下标对应的链表,遍历链表,如果key存在则用新的value覆盖旧的value。
先判断hashcode,再判断引用地址是否相同,最后判断equals
先比较hashcode,如果hashcode不同,那么key一定不相同,如果hashcode相同这两个key有可能是相同的key,然后再去比对引用地址或者equals相同。
if(e.hash == h && (e.key == key || (key != null && key.equals(e.key))))
d. 不存在key需要插入新节点,首先判断是否需要扩容;
e. 头插法插入新节点,size++。
//添加
public void put(K key,V value){
//第一次添加数据,进行扩容
if(table == EMPTY_TABLE){
table = new Entry[DEFAULT_CAPCITY];
}
//计算key对应的数组下标
int h = hash(key);
int index = indexOf(h);
//找是否存在相等key,存在则替换value
for(Entry e = table[index];e != null;e = e.next){
// if(hash(e.key) == index && (e.key == key || key.equals(e.key))){
if(e.hash == h && (e.key == key || (key != null && key.equals(e.key)))){
e.value = value;
modCount++;
return;
}
}
//判断是否需要扩容
if(size >= table.length * threshold){
resize();
}
//不存在key,头插插入新节点key
table[index] = new Entry(key,value,table[index],h);
modCount++;
size++;
}
删除步骤:
a. 计算key对应的数组下标,找到对应的链表
b. 遍历链表找出key相等的节点前驱 (key为空特殊判断)
c. 需要考虑删除的是头结点,删除其他节点,节点的next域指向下一个节点
//删除操作
public void remove(K key){
if(size <= 0){
return;
}
//计算key对应的数组下标
int h = hash(key);
int index = indexOf(h);
//链表为null,不存在元素
if(table[index] == null){
return;
}
// 遍历找到key相同的节点前驱
Entry pre = table[index];
Entry e = table[index];
while(e != null){
Entry next = e.next;
if(e.hash == h && (e.key == key || (key != null && key.equals(e.key)))){
if(e == pre){ //删除的是头结点
table[index] = next;
}else{ //删除后面的节点
pre.next = next;
}
e.key = null;
e.value = null;
e.next = null;
size--;
modCount++;
return;
}
pre = e;
e = next;
}
}
通过key获得对应的value
// 找到对应的key,然后将value值返回
public V get(K key){
int h = hash(key);
int index = indexOf(h);
Entry e = table[index];
while(e != null){
if(e.hash == h && (e.key == key || (key != null && key.equals(e.key)))){
return e.value;
}
e = e.next;
}
return null;
}
通过key修改对应的value值
//找到key对应的结点,替换成新的value
public void replace(K key,V value){
int h = hash(key);
int index = indexOf(h);
if(table[index] == null){
return;
}
Entry e = table[index];
while(e != null){
if(e.hash == h && (e.key == key || (key != null && key.equals(e.key)))){
e.value = value;
return;
}
e = e.next;
}
}
//获取迭代器对象
public Iterator> iterator(){
return new HashIterator();
}
//迭代器实现
private class HashIterator implements Iterator>{
private Entry next;//当前节点
private int expectedModCount; //快速失败
private int index; //记录当前数组的下标
public HashIterator() {
this.expectedModCount = modCount;
//找第一个节点
if(size > 0){
Entry[] t = table;
while(index < t.length && (next = t[index++]) == null){
;
}
}
}
@Override
public boolean hasNext() {
return next != null;
}
@Override
public Entry next() {
checkForComodification();
Entry e = next;
next = e.next;
//找下一个节点
if(next == null){
Entry[] t = table;
while(index < t.length && (next = table[index++]) == null){
;
}
}
return e;
}
@Override
public void remove() {
checkForComodification();
MyHashMap.this.remove(next.key);
}
final void checkForComodification(){
if(expectedModCount != modCount){
throw new ConcurrentModificationException();
}
}
}
//测试迭代器
Iterator> hashIterator = hashMap.iterator();
while(hashIterator.hasNext()){
// hashIterator.remove();
Entry p = hashIterator.next();
System.out.println("key:" + p.getKey() + " "+ "value:" + p.getValue());
}
HashMap是Map接口下的集合,可以存放key - value键值对
当我们存储的元素需要有唯一标识,且对应一定的元素,可以使用HashMap,因为key为唯一的,且对应的value可以是一个元素,也可以是个对象。
浪费空间
HashMap的初始容量为16,加载因子为0.75,当元素存储到12,需要进行扩容。
哈希冲突最大时:所以元素占用一个下标
哈希冲突最低时:每一个元素都占一个下标
数组容量为16时,数组的利用率为1 - 12
所以HashMap采用空间换时间的方式,因为空间利用率越高,存储元素越多,哈希冲突就高,查询效率变低。
HashMap的优势:
查询效率时间复杂度近似于O(1)
HashMap将查询效率放第一位,空间换时间
HashMap可以自定义初始容量和加载因子,因此可以根据具体的使用场景,定义初始容量和加载因子
依赖哈希算法
HashMap比较依赖哈希算法。
哈希算法设计的好,查询效率高,
哈希算法设计的不好,查询效率低。
HashMap是无序的,插入节点是没有顺序的
使用LinkedHashMap可以得到插入有序
LinkedHashMap实现插入有序的原理:
(1)LinkedHashMap在HashMap的基础上维护了一个双向链表
LinkedHashMap = HashMap + 双向链表
(2)LinkedHashMap的节点构成
Entry
相对于HashMap的节点多了after和before,Entry
before:指向该节点之前插入的节点;
after:指向该节点之后插入的节点
继承了HashMap,添加等使用的都是HashMap,重写部分方法,维护before,after。
(3)定义头指针和尾指针,维护双链表,采用头插和尾插添加元素。
插入无序,无法按照key值得大小进行排序
使用TreeMap可以维护key - value结构的大小顺序。
TreeMap:key-value的存储结构是根据key的大小比较排序得来的,而是不是通过key计算对应的哈希值。插入时,通过比较key插入到红黑树中,小的走左子树,大的走右子树。插入和删除都使用红黑树的规则。
底层结构:红黑树,将key-value作为一个节点存储在红黑树的节点中。
TreeMap需要使用比较器进行比较,给类提供比较原则。