HashMap知识点
特点
collection(存放单值的最大接口)和map(存储双值)是集合框架库两个顶级接口。
- Map接口下特点:以Map<Key,Value>形式存储;(Key是不重复的,代表元素的存储位置,也就是可以通过key去寻找key.value的位置,从而得到value的值)适合做查找工作。
证明key不可重复:
public class HashmapTest {
public static void main(String[]args){
// HashMap map=new HashMap();//没有指定泛型,存储的数据就是任意的了
HashMap<String,Integer>map=new HashMap<>();//key:String;value:Integer
map.put("Tom",12);
map.put("Jack",15);
map.put("Jim",29);
map.put("Tom",69);//存储同样的key,新的value会替换老的value
//迭代器遍历 通过entrySet方法调用接口,map.EntrySet().Iterator();原因:数据实际上是存储在entry结构中
Iterator<Map.Entry<String, Integer>> iterator = map.entrySet().iterator();
while (iterator.hasNext()){
//iterator.next();next获取的值是Entry的结构
Map.Entry<String, Integer> next = iterator.next();
System.out.println("key:"+next.getKey()+"value:"+next.getValue());
}
}
}
运行结果截图如下:
证明可以为null的方法同上;
运行结果截图如下:
这也说明了HashMap中元素的顺序,不是插入顺序,因为key-value的存储位置,是由key决定的
如果想单独遍历key:
//只想遍历key时 map.keySet().iterator();
Iterator<String> iterator1 = map.keySet().iterator();
while (iterator1.hasNext()){
//iterator1.next();
// String next = iterator1.next();
String key = iterator1.next();
System.out.println("key:"+key);
}
运行结果截图如下:
如果只想要value时:
//只需要values时
Iterator<Integer> iterator2 = map.values().iterator();
while (iterator2.hasNext()){
Integer value=iterator2.next();
System.out.println("value:"+value);
}
运行结果:
通过foreach遍历:
//通过foreach遍历HashMap集合
for (Map.Entry<String,Integer>entry:map.entrySet()){
System.out.println("key:"+entry.getKey()+"value:"+entry.getValue());
}
其他的方法:
// map.get();//通过key查询对应的value的值
//查询
System.out.println(map.get("Tom"));//应该返回69;注意执行此方法时把下边的全屏蔽
//删除
map.remove("Tom");
//修改
map.replace("Jack",105);
- Cloneable:可以使用clone方法。
- Serializable:可以被序列化。
- Map.Entry:可以存储key-value的具体数值。
- Iterator:迭代器接口(只能从前往后遍历,只有list接口下集合能实现ListInterator)
使用场景
例:存储一个班的人名和成绩,如果用list没办法将成绩与人名相对应 ,而HashMap查询效率比较高,并且可以通过key查询到对应的value。
源码分析(源码的特点)
HashMap底层数据结构:数组+链表的结构(1.7版本);
链表类型的数组:数组下标下面存储的是一个个链表的头节点
为什么要使用这种结构:通过哈希算法计算key的存储位置(hash(key)----->index:存储位置);哈希算法要保证计算出的index一定要小于数组的length(如何保证? :在算法最后X%table.length=index),可能会产生多个key对应一个index的问题(哈希冲突:多个X取余以后的值相同),解决方案:链地址法。
链表没有下标,就需要遍历,有了数组以后,就可以通过下标快速找到。
public class MyHashMap<K,V> {
//HashMap特点1:初始容量是0,当添加第一个元素时容量变为了16
private Entry[]table;
Entry<?,?>[]EMPTY_TABLE={};
private int DEFAULT_CAPCITY=16;
int size;//存储数据的数量
float threshold=0.75f;//加载因子;加载因子越大,扩容时机越晚,哈希冲突发生的概率越大,空间利用率越高;加载因子越小,扩容时机越早,哈希冲突发生的概率越小,空间利用率越低
MyHashMap(){
table=EMPTY_TABLE;
}
public int hash(K key){//key对象
// return key.hashCode()%(table.length);//如果是null,空指针异常
//key.hashCode()是一个int类型 这个int类型有可能会造成数组下标越界
//我们将余数作为index
if (key==null){
return 0;
}
return key.hashCode();
}
public int indexOf(int hash){//保证最后的index一定不会越界
return hash &(table.length);//table.length扩容了
}
HashMap的添加方法:
1)判断是否是第一次添加数据,对存储元素的数组进行初始化;
2)通过key来得到index,table[index]访问的是 存储链表 然后使用链表头插法插入数据value,插入之前必须判断是否需要扩容,如果扩容了,带插入数据的index需要重新计算
3)判断key是否为空,若为空则采取putForNullKey,对于key为null的情况下 特殊处理 会把key为空的数据存储在 index=0的位置
4)是否需要扩容?:从集合容量是否够用的角度出发,index的重复率越高,hash冲突越高,(如何降低hash冲突:在某个条件成立时对数组进行扩容,扩容之后重新计算每一个key-value结构存储的位置)所以需要扩容(扩容条件是:size >= table.length * threshold && (null != table[0]))
//添加方法
public void put(K key,V value){//添加方法
//第一次添加数据 对存储元素的数组进行初始化
//如何判断是不是第一次添加数据
if(table==EMPTY_TABLE){
table=new Entry[DEFAULT_CAPCITY];
}
//将数据往数组中存储
//通过Key如何得到index(引用转换成int类型)然后进行添加
int hash=hash(key);//key==null hash=0
int index=indexOf(hash);//0和任何一个数取余的结果都是0
if (key==null){
putForNullKey(hash,value);//专门处理key为空的添加
}
//key不为null的情况
for (Entry<K,V>e=table[index];e!=null;e=e.next){
//先比较hashCode,如果&&前面的不成立,不会走到后边,直接退出;只有hashCode相等时 这两个对象才有可能是同一个,所以要接着判断
//如果说引用地址相同,即e.key==key,那么这两个对象一定是相同的--->为了提高效率
if (e.hash==hash &&(e.key==key||e.key.equals(key))){
//e.key.equals(key)
//说明重复,如果一对象重写了equals方法后:比较的就一定是对象底层的数据
e.value=value;//新值替换老值
return;
}
//如果重写了一个对象的equals方法,一定要重写对象hashCode方法
//已经遍历过了说明不重复
if(size>=table.length*threshold&&(null!=table[index])){
//扩容操作;2倍扩容;要重新计算每个数组的位置
}
Entry<K,V>head=table[index];
Entry<K,V>newEntry=new Entry<>(hash,key,value,head);
table[0]=newEntry;
size++;
}
//table[index]访问的是 存储链表 然后使用链表头插法插入数据
//对于key为null的情况下 特殊处理 会把key为空的数据存储在 index=0的位置
//key重复的时候 用新的value会代替老的value 在map中不会出现重复的key 判断一次key是否相同
//是否需要扩容 为什么?
}
private void putForNullKey(int hash,V value) {
// table[0];
//如果重复添加key=null的键值对
//遍历单链表
for (Entry<K, V> e = table[0]; e != null; e = e.next) {
if (e.key == null) {//说明重复
e.value = value;//新值替换老值
return;
}
}
//说明没有重复的元素:要真正开始添加元素了(需要将这个key value结构的数据添加到map中
if (size >= table.length * threshold && (null != table[0])) {
//扩容操作
}
Entry<K, V> head = table[0];
Entry<K, V> newEntry = new Entry<>(hash,null, value, head);
table[0] = newEntry;
size++;
}
HashMap的remove删除方法:
1)通过key计算出index;
//通过Key如何得到index(引用转换成int类型)然后进行添加
int hash=hash(key);//key==null hash=0
int index=indexOf(hash);//0和任何一个数取余的结果都是0
2)得到链表,找到key对应的节点;
for (Entry<K,V>e=table[index];e!=null;e=e.next){
//先比较hashCode,如果&&前面的不成立,不会走到后边,直接退出;只有hashCode相等时 这两个对象才有可能是同一个,所以要接着判断
//如果说引用地址相同,即e.key==key,那么这两个对象一定是相同的--->为了提高效率
if (e.hash==hash &&(e.key==key||e.key.equals(key))){
//e.key.equals(key)
//说明重复,如果一对象重写了equals方法后:比较的就一定是对象底层的数据
e.value=value;//新值替换老值
return;
}
3)单链表的删除
- HashMap中replace方法
思想与key重复时新值替换老值的思想是一样的 - HashMap迭代器
//HashMap迭代器 --->HashMapTest中第一个遍历方法
class HashIterator implements Iterator<Entry<K,V>>{
Entry<K,V> next;//保存的是下一个要被遍历的节点
int index;//当前遍历到的节点所处的下标
Entry<K,V> current;//保存当前节点
int exceptedModCount;//在遍历时要抛出异常
public HashIterator(){
exceptedModCount=modcount;
//将next指向下一个要遍历的节点
if (size>0){
Entry<K,V>[] t=table;
while (index<t.length &&(next=t[index+1])==null)
;//index开始是0,t[0]=null,index++循环往后走;目的是找第一个不为空的index,直到index越界,或者是找到所需要找的节点
}
}
@Override
public boolean hasNext() {
return next!=null;//只要不为空都能继续遍历
}
@Override
public Entry<K, V> next() {
if (modcount !=exceptedModCount){
throw new ConcurrentModificationException();
}
//返回当前要遍历的节点都并且让next再指向后一个节点
Entry<K,V> e=next;
//让next指向下一个节点
if (next==null){
//向后寻准下一个节点位置
Entry<K,V>[] t=table;
while (index<t.length &&(next=t[index+1])==null)
;
}
current=e;
return current;
}
@Override
public void remove() {//删除当前正在被遍历的节点
K key=current.key;
current=null;
MyHashMap.this.remove(key);
}
}
保证在进行迭代的时候不允许其他线程进来调用添加或者删除方法:
MyHashMap中 int modcount;
在添加或者删除操作时加上modcount++;
在迭代器中也要加上int exceptedModCount;
在遍历时也要抛出异常。
public Entry<K, V> next() {
if (modcount !=exceptedModCount){
throw new ConcurrentModificationException();
}
//...
}