Java集合5——Map的实现类HashMap

HashMap学习

基于JDK 1.7版本学习源码实现(1.7和1.8的区别)

public class HashMap<K,V>
       extends AbstractMap<K,V>
       implements Map<K,V>, Cloneable, Serializable

继承了AbstractMap,主要是实现map接口,当前HashMap也能实现克隆及序列化
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
默认初始容量:初始容量即可以传参给定,也可以给默认值,主要作用是对数据大小进行初始化
static final int MAXIMUM_CAPACITY = 1 << 30;
容量最大值:容量的大小是对数组设定上限
static final float DEFAULT_LOAD_FACTOR = 0.75f;
加载因子:在扩容时使用(使用方法put),默认值大小是0.75
static final Entry<?,?>[] EMPTY_TABLE = {};
底层数据是数组,数组的数据类型是Entry

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,还有entry类型的next属性,这可以看出entry数据是要通过链表来组织连接
底层数据结构就是:数组+链表
threshold = (int) Math.min(capacity * loadFactor
threshold扩容阈值:计算方式如上:容量*加载因子得到
1、成员变量

 // 默认的初始容量是16,必须是2的幂。
static final int DEFAULT_INITIAL_CAPACITY = 16;

 // 最大容量(必须是2的幂且小于2的30次方,传入容量过大将被这个值替换)

static final int MAXIMUM_CAPACITY = 1 << 30;

  // 默认加载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;

 // 存储数据的Entry数组,长度是2的幂。
 // HashMap是采用拉链法实现的,每一个Entry本质上是一个单向链表
transient Entry[] table;

  // HashMap的大小,它是HashMap保存的键值对的数量
transient int size;

 // HashMap的阈值,用于判断是否需要调整HashMap的容量(threshold = 容量*加载因子)
int threshold;


 // 加载因子实际大小
final float loadFactor;

  // HashMap被改变的次数,由于HashMap非线程安全,在对HashMap进行迭代时,
  如果期间其他线程参与导致HashMap的结构发生变化了(比如put,remove等操作),需要抛出异常
transient volatile int modCount;

2、构造方法
//指定“容量大小”和“加载因子”的构造函数
public HashMap(int initialCapacity, float loadFactor) {
//容量小于0则抛异常
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
//指定最大容量只能是MAXIMUM_CAPACITY
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);

    // Find a power of 2 >= initialCapacity
    //找出“大于”initialCapacity的最小的2的幂
    int capacity = 1;
    while (capacity < initialCapacity)
        capacity <<= 1;
	//设置加载因子
    this.loadFactor = loadFactor;
	//设置阈值,阈值=容量乘以加载因子,当HashMap中存储数据的数量达到threshold时,
	  //就需要将HashMap的容量增加
    threshold = (int)(capacity * loadFactor);
	//创建Entry数组,用来保存数据
    table = new Entry[capacity];
    init();
}


 //指定容量大小的构造函数
public HashMap(int initialCapacity) {
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
}


 //默认构造函数
public HashMap() {
    //默认加载因子
    this.loadFactor = DEFAULT_LOAD_FACTOR;
	//默认阈值
    threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);
	//默认Entry数组的大小
    table = new Entry[DEFAULT_INITIAL_CAPACITY];
    init();
}


 //包含子Map的构造函数
public HashMap(Map<? extends K, ? extends V> m) {
    this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,
                  DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);
    //将m中的全部元素逐个添加到HashMap中
    putAllForCreate(m);
}

3、put方法(添加元素)

**public V put(K key, V value) {
    if (table == EMPTY_TABLE) {
        inflateTable(threshold);
    }
    if (key == null)
        return putForNullKey(value);
    int hash = hash(key);
    int i = indexFor(hash, table.length);
    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))) {
            V oldValue = e.value;
            e.value = value;
            e.recordAccess(this);
            return oldValue;
        }
    }
    modCount++;
    addEntry(hash, key, value, i);
    return null;
}**

4、put添加元素过程中
1、判断table是否为空数组,是则创建数组(注意:threshold值得改变)
2、key为null的添加操作特殊处理,将该key对应的entry实体放入table[0]的位置(存在该key为null的实体则实体value值覆盖操作)
3、哈希过程通过key进行哈希
4、通过indexfor方法来定位数据存储的索引位置
5、遍历该位置的链表,判断是否存在该key的entry实体(k.hashcode == equals),有则将value替换
6、将该元素插入到对应索引的链表中
扩容(2*table.length)方式扩容(扩容时机)
插入位置(第一个位置)

hash算法:[https://www.cnblogs.com/killbug/p/4560000.html]

5、get获取元素流程

注意点:
哪些情况返回null

正常处理流程:
先对key进行哈希,并找到key存在的数组table的索引位置
对该索引位置的链表进行遍历,判断key是否相等(key的Hashcode,== equals)
remove删除元素过程
1、数组元素size为0,即不存在元素,直接返回
2、对key进行哈希,找到在数组table中的索引位置
3、对该位置的链表进行从头开始遍历
4、prev,e,next定义变量,找出key的位置
分两种情况:
key对应的entry实体在链表头节点,直接将该节点的next的置为头结点
entry实体在链表中,该判断结点的后一个节点的next直接指向该节点的next节点

containsValue判断key是否存在方法

containsKey判断value是否存在方法

两者区别点:
1、判断相等方式不同:containsKey中key需要比较Hashcode、== equals
containsValue中value直接equals
2、遍历数组大小不同:containsKey中通过key找到该key存在的数组索引位置,遍历该索引位置的链表即可
containsValue中需要对该链表所有数组的所有链表全部遍历

6、手动实现HashMap

import java.util.Arrays;

/**
 * Description: 自定义hashmap
 *
 * @Author jr
 * @Data 2018/10/28
 */
public class MyHashMap {
    /**
     *内部类,Entry  用来存储key value数据,和Entry类型的next节点.
     */
    class Entry{
        int key;
        String value;
        Entry next;
        public Entry(int k, String v,Entry next){
            this.key = k;
            this.value = v;
            this.next = next;
        }
    }

    //  用来存储Entry的数组
    Entry[] table;
    // 记录table中每个索引位置的链表长度
    int[] linkLen;
    //各链表最长长度的限定
    int maxLen;

    //  默认构造
    public MyHashMap(){
        this(16);
    }

    //  带参数构造
    public MyHashMap(int size){
        this.table = new Entry[size];
        this.linkLen = new int[size];
        maxLen = size;
    }

    /**
     *  哈希函数
     */
    public int hash(int key,Entry[] table){
        if(key < 0){
            throw new IllegalArgumentException("wrong key!");
        }
        return key % table.length;
    }

    /**
     *  使用key和value插入的方法
     */
    public String put(int key,String value){
        return put(key,value,this.table,this.linkLen);
    }

    /**
     * 内部使用的插入方法
     */
    private String put(int key,String value,Entry[] table,int[] linkLen){
        //1.先得到在数组中的索引位置
        int hash = hash(key,table);
        //2.得到索引位置的第一个节点,供后面遍历
        Entry entry = table[hash];

        //3.对该节点的链表进行遍历.如果找到了key对应的节点,对value进行覆盖
        for (;entry != null;entry = entry.next) {
            if(key == entry.key){
                String oldValue = entry.value;
                entry.value = value;
                return oldValue;
            }
        }

        // 考虑扩容
        if(linkLen[hash] == maxLen){
            resize();
        }
        //4.如果走到这一步,上面的肯定没有return,表示没有找到key,所以要插入到头节点
        Entry e = new Entry(key,value,table[hash]);
        table[hash] = e;
        linkLen[hash]++;
        return table[hash].value;
    }

    /**
     *  扩容原数组长度并且将里面的数据重新哈希.
     */
    private void resize(){
        //1.先创建一个新数组,长度为原来数组的二倍
        Entry[] newTable = new Entry[table.length*2];
        //1.定义一个新的数组,来记录新table的各个节点链表长度.
        int[] newLinkLen = new int[table.length*2];
        //1.更改每个链表最长长度也乘以2
        maxLen = table.length*2;

        //2.对原来table进行循环遍历,把每个节点都重新哈希插入到新的数组中.
        for (int i = 0; i < table.length; i++) {
            Entry entry = table[i];    //得到原table的每个位置的头节点;
            while (entry != null){
                int key = entry.key;
                String value = entry.value;
                put(key,value,newTable,newLinkLen);
                entry = entry.next;
            }
        }

        //3.数据重新插入完后,将新数组地址赋值给原数组.
        table = newTable;
        linkLen = newLinkLen;
    }

    public String remove(int key){
        if(key < 0){
            throw new IllegalArgumentException("wrong key!");
        }

        //找到索引位置
        int hash = hash(key,table);
        if(linkLen[hash] == 0){
            throw new IllegalArgumentException("No such key!");
        }

        Entry entryPre = table[hash];  //父节点
        Entry entry = table[hash];     //遍历的节点

        while (entry != null){
            if(entry.key == key){
                if(entryPre == entry){
                    table[hash] = entry.next;
                }else{
                    entryPre.next = entry.next;
                    return entry.value;
                }
                linkLen[hash]--;
            }
            entryPre = entry;
            entry = entry.next;
        }
        return null;
    }

    public static void main(String[] args) {
        MyHashMap myHashMap = new MyHashMap(3);
        myHashMap.put(1,"a");
        myHashMap.put(4,"b");
        myHashMap.put(7,"c");
        System.out.println(myHashMap.linkLen[1]);
        myHashMap.put(10,"d");
        System.out.println(myHashMap.linkLen[1]);
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: JavaMap接口提供了一种将键映射到值的对象。其一种实现HashMap,它使用哈希表来实现HashMap允许 null 键和 null 值,并且没有顺序保证。HashMap 的操作复杂度为 O(1) 平均复杂度,因此在许多情况下非常高效。 ### 回答2: Java集合Map用来保存键-值对,HashMap是其的一种实现方式。HashMap的内部实现是基于哈希表的,它可以将任意对象作为键,并且保证键的唯一性。在HashMap,键和值都允许为null,但是同一个HashMap,键不能重复,如果重复了,新的value会覆盖旧的value。 HashMap内部是通过hashCode()和equals()方法来判断键是否相等的。当我们向HashMap添加一个键-值对时,系统会先计算出键的hashCode值,然后用该值来确定存放该键值对的位置。如果有多个键的hashCode值相同,称为哈希冲突,那么HashMap会在这些键值对所在的位置上,以链表的形式组成一个链表存储。 HashMap的优点在于插入、删除和查找都比较快,时间复杂度均为O(1),对于大量的数据,它的效率优于List或Set等集合。但是,在内存使用上,HashMap会比List或Set等集合耗费更多的内存空间,因为它需要额外的空间来存储哈希值和链表指针。 值得注意的是,在多线程环境下,HashMap是不安全的,会出现并发修改导致的数据不一致问题,这时可以使用ConcurrentHashMap或者加锁机制来保证线程安全。 总之,HashMapJava非常实用的集合,适用于大量键值对的存储和检索。我们应该了解HashMap的内部实现原理,并且在使用过程需要注意其线程安全性等问题。 ### 回答3: JavaMap是一种键值对的集合,其每个元素都由一个键和一个值组成。在Map,每个键必须是唯一的,而值可以重复。 在MapHashMap是最常用的实现之一。它是基于哈希表实现的,可以通过键快速查找值。哈希表是一种支持常数时间快速查找的数据结构,因为它可以将键映射到与其对应的值的位置,因此查找时只需要计算键的哈希码即可找到对应的值。 HashMap实现使用了两个数组来存储键值对:一个数组用于存储键,另一个数组用于存储值。当插入键值对时,首先会将键的哈希码计算出来,然后通过哈希码将键映射到键数组的位置,将值存储在值数组的相同位置。当需要查找一个键时,只需计算其哈希码并定位到键数组的位置,然后从值数组取出对应的值即可。 与集合一样,HashMap也是线程不安全的,因此在多线程环境下需要使用ConcurrentHashMap或通过synchronized关键字来保证线程安全性。此外,在使用HashMap时应该尽量避免使用null作为键,因为null的哈希码为0,可能与其他非null键的哈希码相同,导致哈希碰撞,影响HashMap的性能。 总之,HashMap是一种高效的键值对集合,通过哈希表实现快速的查找和插入操作。在正确使用和注意安全性的前提下,使用HashMap可以大大提升代码效率和性能。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值