一、Map接口

在生活中我们经常成对的储存某些信息,Map就是用来存储“键(key)-值(value) 对”的。 Map类中存储的“键值对”通过键来标识,所以“键对象”不能重复。

 Map 接口的实现类有HashMap、TreeMap、HashTable、Properties等。

下面是Map接口常用的方法:

java中map底层如何实现 java map底层实现_数组长度

二、HashMap

 HashMap采用哈希算法实现,是Map接口最常用的实现类。 由于底层采用了哈希表存储数据,我们要求键不能重复,如果发生重复,新的键值对会替换旧的键值对。 HashMap在查找、删除、修改方面都有非常高的效率。

 HashTable类和HashMap用法几乎一样,底层实现几乎一样,只不过HashTable的方法添加了synchronized关键字确保线程同步检查,效率较低。

 1. HashMap: 线程不安全,效率高。允许key或value为null。

 2. HashTable: 线程安全,效率低。不允许key或value为null。

HashMap的底层实现:

  HashMap底层实现采用了哈希表,这是一种非常重要的数据结构。

  数据结构中由数组和链表来实现对数据的存储,他们各有特点。

(1) 数组:占用空间连续。 寻址容易,查询速度快。但是,增加和删除效率非常低。

(2) 链表:占用空间不连续。 寻址困难,查询速度慢。但是,增加和删除效率非常高。

那么,我们能不能结合数组和链表的优点(即查询快,增删效率也高)呢? 答案就是“哈希表”。 哈希表的本质就是“数组+链表”。

HashMap的底层基本结构

哈希表的基本结构就是“数组+链表”。我们打开HashMap源码,发现有如下两个核心内容:

java中map底层如何实现 java map底层实现_java中map底层如何实现_02

  其中的Entry[] table 就是HashMap的核心数组结构,我们也称之为“位桶数组”。我们再继续看Entry是什么,源码如下:

java中map底层如何实现 java map底层实现_java中map底层如何实现_03

一个Entry对象存储了:

1. key:键对象 value:值对象

2. next:下一个节点

3. hash: 键对象的hash值

显然每一个Entry对象就是一个单向链表结构,我们使用图形表示一个Entry对象的典型示意:

java中map底层如何实现 java map底层实现_java中map底层如何实现_04

 然后,我们画出Entry[]数组的结构(这也是HashMap的结构):

java中map底层如何实现 java map底层实现_数组_05

▪ 存储数据过程put(key,value)

 明白了HashMap的基本结构后,我们继续深入学习HashMap如何存储数据。此处的核心是如何产生hash值,该值用来对应数组的存储位置。

java中map底层如何实现 java map底层实现_链表_06

我们的目的是将”key-value两个对象”成对存放到HashMap的Entry[]数组中。参见以下步骤:

(1) 获得key对象的hashcode

首先调用key对象的hashcode()方法,获得hashcode。

(2) 根据hashcode计算出hash值(要求在[0, 数组长度-1]区间)

hashcode是一个整数,我们需要将它转化成[0, 数组长度-1]的范围。我们要求转化后的hash值尽量均匀地分布在[0,数组长度-1]这个区间,减少“hash冲突”

 i. 一种极端简单和低下的算法是:

hash值 = hashcode/hashcode;

也就是说,hash值总是1。意味着,键值对对象都会存储到数组索引1位置,这样就形成一个非常长的链表。相当于每存储一个对象都会发生“hash冲突”,HashMap也退化成了一个“链表”。

 ii. 一种简单和常用的算法是(相除取余算法):

 hash值 = hashcode%数组长度

这种算法可以让hash值均匀的分布在[0,数组长度-1]的区间。 早期的HashTable就是采用这种算法。但是,这种算法由于使用了“除法”,效率低下。JDK后来改进了算法。首先约定数组长度必须为2的整数幂,这样采用位运算即可实现取余的效果:hash值 = hashcode&(数组长度-1)。

(3) 生成Entry对象

          如上所述,一个Entry对象包含4部分:key对象、value对象、hash值、指向下一个Entry对象的引用。我们现在算出了hash值。下一个Entry对象的引用为null。

 (4) 将Entry对象放到table数组中

如果本Entry对象对应的数组索引位置还没有放Entry对象,则直接将Entry对象存储进数组;如果对应索引位置已经有Entry对象,则将已有Entry对象的next指向本Entry对象,形成链表。

▪ 取数据过程get(key)

我们需要通过key对象获得“键值对”对象,进而返回value对象。明白了存储数据过程,取数据就比较简单了,参见以下步骤:

(1) 获得key的hashcode,通过hash()散列算法得到hash值,进而定位到数组的位置。

(2) 在链表上挨个比较key对象。 调用equals()方法,将key对象和链表上所有节点的key对象进行比较,直到碰到返回true的节点对象为止。

3) 返回equals()为true的节点对象的value对象。

明白了存取数据的过程,我们再来看一下hashcode()和equals方法的关系:

Java中规定,两个内容相同(equals()为true)的对象必须具有相等的hashCode。因为如果equals()为true而两个对象的hashcode不同;那在整个存储过程中就发生了悖论。

▪ 扩容问题

HashMap的位桶数组,初始大小为16。实际使用时,显然大小是可变的。如果位桶数组中的元素达到(0.75*数组 length), 就重新调整数组大小变为原来2倍大小。

扩容很耗时。扩容的本质是定义新的更大的数组,并将旧数组内容挨个拷贝到新数组中。

▪ JDK8将链表在大于8情况下变为红黑二叉树

JDK8中,HashMap在存储一个元素时,当对应链表长度大于8时,链表就转换为红黑树,这样又大大提高了查找的效率。

下面是一个简单的实现get与put方法的一个代码

public class Hashmap<E,V> {
    Node[] table=new Node[16];//水桶数组
    int size;//存放键值对的个数
    public V get(E key){
        Object value=null;
        Node newNode=new Node();
        newNode.hash=myHash(key.hashCode(),table.length);
        if(table[newNode.hash]!=null){
        Node temp=table[newNode.hash];
        while(temp!=null){
            if(temp.Key.equals(key)){
                value=temp.value;
                break;
            }else{
                temp=temp.next;
            }
        }
        }
        return (V)value;
    }
    public void put(E key,V value){
        Node lastNode=new Node();
        Node newNode=new Node();
        newNode.hash=myHash(key.hashCode(),table.length);
        newNode.Key=key;
        newNode.value=value;
        Node temp=table[newNode.hash];
        //查找该数组位置是否为空
        if(temp==null){
            table[newNode.hash]=newNode;
        }
        else{
            while (temp!=null){
                if(temp.Key.equals(key)){
                    temp.value=value;
                    break;
                }else{
                lastNode=temp;
                temp=temp.next;
                }
            }
        }
        lastNode.next=newNode;
        size++;
    }
    public int myHash(int v,int length){
        return v&(length-1);
    }

    @Override
    public String toString() {
        StringBuilder sb=new StringBuilder("[");
        for(int i=0;i<table.length;i++){
            Node temp=table[i];
            while (temp!=null){
                sb.append(temp.Key+":"+temp.value+",");
                temp=temp.next;
            }
        }
        sb.append("]");
        return sb.toString();
    }

    public static void main(String[] args) {
        Hashmap<Integer,String> map=new Hashmap<>();
        map.put(0,"lan1");
        map.put(1,"lan2");

        System.out.println(map);
        System.out.println(map.get(0));
    }
}