Java--集合框架之Map接口

别等时光非礼了梦想. 提交于 2020-01-29 13:44:26

Java集合框架总图在这里插入图片描述
Map接口和具体实现类
在这里插入图片描述
Java集合总体分为两个根接口,Map和Collection,其中Collection是单列集合,Map是双列集合。
Map与List、Set接口不同,它并不继承自Collection,它是由一系列键值对组成的集合,提供了key到value的映射。在Map中一个key对应一个value,所以key的存储不可重复,但value可以。

哈希结构——通过关键码找到值的数据结构
哈希函数——建立关键字和值的映射关系
注:好的哈希函数能使值均匀的分步在哈希结构中

生成哈希函数的两种方式
(1)直接寻址法:f(x)=kx+b(k和b是常数)
(2)除留余数法:f(x)=x mod m (m<p)(m小于哈希长度p)
哈希冲突的两种解决方式
(1)链地址法:当发生哈希冲突时,将哈希到对应位置的值连接在该位置的数据后面。
(2)线性探测法

Map接口常用方法

int size();          //map集合中存储的键值对的个数
boolean isEmpty();   //判断map集合是否为空 true:空 false:不为空
boolean containsKey(Object key)  //判断集合中是否存在该键key
boolean containsValue(Object value); ////判断集合中是否存在该值value
V get(Object key)  //通过键获取对应的值
V put(K key, V value);  //添加键值对
V remove(Object key);  //删除操作 通过键删除键值对

常用三种遍历方式

System.out.println("通过键值对遍历");
        //1、通过键值对遍历
        Iterator <Map.Entry <String, String>> iterator = hashMap.entrySet().iterator();
        while (iterator.hasNext()) {
            Map.Entry <String, String> entry = iterator.next();
            String key = entry.getKey();
            String value = entry.getValue();
            System.out.println(key+":"+value);
        }
        System.out.println();
        System.out.println("通过键遍历");
        //通过键遍历
        Iterator <String> iterator1 = hashMap.keySet().iterator();
        while (iterator1.hasNext()) {
            System.out.println(iterator1.next());
        }
        System.out.println();
        System.out.println("通过值遍历");
        //通过值遍历
        Iterator <String> iterator2 = hashMap.values().iterator();
        while (iterator2.hasNext()) {
            System.out.println(iterator2.next());
        }

一、HashMap
HashMap底层是一个Entry数组,Entry是HashMap的基本组成单元,每一个Entry包含一个key-value键值对。当发生哈希冲突时,HashMap采用链表的方式来解决,在对应的数组位置存放链表的头结点。对链表而言,新加入的节点会从头结点加入。

1、继承关系

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

2、属性分析

//默认的初始容量是16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;

//最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;

//默认的填充因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;

//当桶(bucket)上的结点数大于这个值时会转成红黑树
static final int TREEIFY_THRESHOLD = 8;

//当桶(bucket)上的结点数小于这个值时树转链表
static final int UNTREEIFY_THRESHOLD = 6;

//桶中结构转化为红黑树对应的table的最小大小
static final int MIN_TREEIFY_CAPACITY = 64;

//存储元素的数组,总是2的幂次倍
transient Node<k,v>[] table;

//存放具体元素的集
transient Set<map.entry<k,v>> entrySet;

//存放元素的个数,注意这个不等于数组的长度。
transient int size;

//每次扩容和更改map结构的计数器
transient int modCount;

//临界值
//实际大小(容量*填充因子)超过临界值时,会进行扩容
//threshold = capacity * loadFactor,
//当Size>=threshold的时候,就要考虑对数组扩容
//即这是衡量数组是否需要扩容的一个标准
int threshold;

//加载因子
//用来控制数组存放数据的疏密程度
//loadFactor越趋近于1,数组中存放的数据(entry)就越多,也就越密,会让链表的长度增加
//loadFactor越小,趋近于0,数组中存放的数据(entry)也就越少,就越稀疏
//loadFactor太大导致查找元素效率低
//loadFactor太小导致数组的利用率低,存放的数据会很分散
//loadFactor的默认值为0.75f是官方给出的一个比较好的临界值
//给定的默认容量为16,负载因子为 0.75
//Map在使用过程中不断的往里面存放数据,当数量达到了16*0.75=12
//就需要将当前16的容量进行扩容
//扩容过程涉及到rehash、复制数据等操作,非常消耗性能
final float loadFactor;

HashMap内部定义了一个hash表数组,元素通过哈希转换函数将哈希地址转换成数组中存放的索引,如果有冲突,则使用散列链表的形式将所有相同哈希地址的元素串起来,通过查看HashMap.Entry的源码可以看出是一个单链表结构:

static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        V value;
        Node<K,V> next;

        Node(int hash, K key, V value, Node<K,V> next) {
            this.hash = hash;
            this.key = key;
            this.value = value;
            this.next = next;
        }

        public final K getKey()        { return key; }
        public final V getValue()      { return value; }
        public final String toString() { return key + "=" + value; }

        public final int hashCode() {
            return Objects.hashCode(key) ^ Objects.hashCode(value);
        }

        public final V setValue(V newValue) {
            V oldValue = value;
            value = newValue;
            return oldValue;
        }

        public final boolean equals(Object o) {
            if (o == this)
                return true;
            if (o instanceof Map.Entry) {
                Map.Entry<?,?> e = (Map.Entry<?,?>)o;
                if (Objects.equals(key, e.getKey()) &&
                    Objects.equals(value, e.getValue()))
                    return true;
            }
            return false;
        }
    }

3、构造函数

//如果指定了加载因子和初始容量,就调用这个构造方法
public HashMap(int initialCapacity, float loadFactor) {
          //判断合法性
          if (initialCapacity < 0)
              throw new IllegalArgumentException("Illegal initial capacity: " +
                                                initialCapacity);
          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
          //初始容量
         int capacity = 1;  
          //确保容量为2的n次幂,使capacity为大于initialCapacity的最小的2的n次幂
         while (capacity < initialCapacity)  
             capacity <<= 1;
 
         this.loadFactor = loadFactor;
         threshold = (int)(capacity * loadFactor);
         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);
         table = new Entry[DEFAULT_INITIAL_CAPACITY];
        init();
     }

4、常用方法
(1)put()
key不可重复但可以为空,value可重复可为空,插入无序。

public V put(K key, V value) {
        //如果table数组为空数组,进行数组填充(为table分配实际内存空间),入参为threshold,此时threshold为initialCapacity 默认是1<<4
        if (table == EMPTY_TABLE) {
            inflateTable(threshold);
        }
       //如果key为null,存储位置为table[0]或table[0]的冲突链上
        if (key == null)
            return putForNullKey(value);
        int hash = hash(key);//对key的hashcode进一步计算,确保散列均匀
        int i = indexFor(hash, table.length);//获取在table中的实际位置
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
        //如果该对应数据已存在,执行覆盖操作。用新value替换旧value,并返回旧value
            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++;//保证并发访问时,若HashMap内部结构发生变化,快速响应失败
        addEntry(hash, key, value, i);//新增一个entry
        return null;
    }

注:
①inflateTable方法
用于为主干数组table在内存中分配存储空间,通过roundUpToPowerOf2(toSize)可以确保capacity为大于或等于toSize的最接近toSize的二次幂,比如toSize=13,则capacity=16;to_size=16,capacity=16;to_size=17,capacity=32。

private void inflateTable(int toSize) {
        int capacity = roundUpToPowerOf2(toSize);//capacity一定是2的次幂
        threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);//此处为threshold赋值,取capacity*loadFactor和MAXIMUM_CAPACITY+1的最小值,capaticy一定不会超过MAXIMUM_CAPACITY,除非loadFactor大于1
        table = new Entry[capacity];
        initHashSeedAsNeeded(capacity);
    }

②roundUpToPowerOf2方法
使数组长度一定为2的次幂,Integer.highestOneBit用来获取最左边的bit(其他bit位为0)所代表的数值。

private static int roundUpToPowerOf2(int number) {
        // assert number >= 0 : "number must be non-negative";
        return number >= MAXIMUM_CAPACITY
                ? MAXIMUM_CAPACITY
                : (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1;
    }

③hash函数
用异或、移位等运算,对key的hashcode进一步计算以及并调整二进制位,保证最终获取的存储位置尽量分布均匀。

final int hash(Object k) {
        int h = hashSeed;
        if (0 != h && k instanceof String) {
            return sun.misc.Hashing.stringHash32((String) k);
        }

        h ^= k.hashCode();

        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
    }
    
 static int indexFor(int h, int length) {
       //h&(length-1)保证获取的index一定在数组范围内
        return h & (length-1);
    }

以上两步获取到当前元素要存储的哈希表的索引位置。

拓展:不同场景下的put
hash (通过 key 计算出的 int 值)
key (传入的 key)
value (传入的 value)

场景1:存储时 HashMap的 Node数组未初始化。
初始化数组,直接放入。

场景2:当前坐标下不存在值。
直接存放到0。

场景3:当前坐标下只有一个值并且存储的 key相等。
将旧值覆盖。

场景4:当前坐标下只有一个值并且存储的 key不相等。
将新值存到下标为0的next属性处,形成链表结构。

场景5:当前坐标下有多数值(小于8)且其中有一个值和新传入的 key相同。
替换旧值,并复制旧值的next属性给新值。

场景6:当前坐标下有多数值(小于8)且 key 都不相同。
直接接到最后面,若连完大于8则转为红黑树。

场景7:当前坐标下有多数值(大于等于8(链表长度大于等于8的时候,当前数组长度大于64时才会转换成红黑树)),数据结构已经变为红黑树了。

其余可查看链接:
http://www.freesion.com/article/6223261424/

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!