Java基础之HashMap原理

相比于C++等语言,许多人都觉得Java很简单、容易上手。但是简单并不是理所当然的,简单是因为jdk为我们夯实了底层基础,我们才得以轻松搭建宏伟的上层建筑。作为Java程序员,如果仅仅局限于会使用jdk为我们封装好的工具而怠于知其细节,那么个人提升永远是有所缺失的。我们不重复造轮子,但是得了解轮子是怎么造出来的。从事Java开发一年半,对于各种框架渐渐有所了解,却猛然发现对于Java基础还有不少欠缺,因此准备着手写Java基础系列博文,仅作自我学习,也希望阅读者给予指教。

今天的主角是HashMap,在此之前,先捋一捋涉及到的更基础的知识点。

基本数据类型

数据类型 存储大小 范围 说明
byte 8 bit -2^7 ~ 2^7-1 字节
short 16 bit -2^15 ~ 2^15-1 短整型
int 32 bit -2^31 ~ 2^31-1 整型
long 64 bit -2^63 ~ 2^63-1 长整型
float 32 bit ±(1.4E-45 ~ 3.4028235E38) 浮点型,后缀f
double 64 bit ±(4.9E-324 ~ 1.7976931348623157E308) 双精度浮点型
char 16 bit 2^16-1 字符型
boolean     布尔型

数值类型在计算机中的存储方式

整型以补码的形式存储(正数的补码与原码相同):

如int 1为:0000 0000 0000 0000 0000 0000 0000 0001

int -1为:1111 1111 1111 1111 1111 1111 1111 1111

byte 1为:0000 0001

byte -1为:1111 1111

浮点数在计算机中的存储分为三个部分:符号位、指数位和尾数位

float类型符号位占1bit,指数位占8bit,尾数位占23bit

double类型符号位占1bit,指数位占11bit,尾数位占52bit

参考http://www.cnblogs.com/xugang/archive/2010/05/04/1727431.html

位操作

<< 左移位,低位补零,每移一位相当于×2

>> 右移位,用符号位填充高位,每移一位相当于÷2

>>> 无符号右移位,用0填充高位

对于int类型的移位操作,运算符右侧参数需要进行模32运算,即移动1位跟移动33位效果是相同的;同理,对于long类型,移动位数要进行模64运算。

% 模运算,取余

^ 位异或,相同取0,不同取1

& 与,同为1取1,有0取0

| 或,有1取1,同为0取0

~ 非,取反

hashCode()方法

hashCode是由对象导出的一个整数值,通常跟对象的内容有关,如果没有重写该方法,那么hashCode()返回的是对象的内存地址。hashCode存在的一个重要原因是用于HashMap中提高查找的效率。

如果a.equals(b),那么a.hashCode() == b.hashCode(),反之则不一定成立;不同的对象也可能拥有同样的散列码。

组合多个hashCode可以通过Objects.hash方法,并提供多个参数;数组对象的hashCode可以通过Arrays.hashCode方法计算。

String类型的hashCode()方法

    public int hashCode() {
        int h = hash;
        if (h == 0 && value.length > 0) {
            char val[] = value;
            // 之所以与31相乘,是因为它是一个奇素数。如果乘数是偶数,且乘法溢出,则信息会丢失,与2                
            // 相乘相当于移位。使用素数的好处并不明显,只是出于传统。另外,31*i=(i<<5)-i,jvm能够 
            // 自动做这种优化提高运算速度
            for (int i = 0; i < value.length; i++) {
                h = 31 * h + val[i];
            }
            hash = h;
        }
        return h;
    }

HashMap

数据结构

数组是最基本的数据结构,通过索引存取,非常便利,但是数组对象在创建时便要定义大小,扩容的代价很高。相反,链表是一种扩容方便,但是访问较为复杂的数据结构。而HashMap则综合了这两种数据结构的优点,既拥有便利的存取操作,同时又便于扩容。

HashMap中维护了一个数组 Node[] table,Node是内部类,实现了Map.Entry接口,是一个链表。数组的每个位置存放的是链表的表头。

put(K k, V v)

使用hash(key)&(n-1)计算得待插入节点的数组下标,如果该桶位没有存放其它节点,则将新节点放在该桶位。如果该桶位已经有节点,则遍历该链表,若存在某个节点的key值与新k相同

if (e.hash == hash &&
    ((k = e.key) == key || (key != null && key.equals(k))))

则根据 onlyIfAbsent 参数决定是否使用新的节点取代已存在的节点

if (!onlyIfAbsent || oldValue == null)

如果链表中不存在相同key,则将新节点放在链尾。

get(K k)

使用hash(key)&(n-1)计算得数组下标,如果该桶位存放的节点key值与k相同,则返回该节点的value,否则遍历该链表,直到找到key值与k相同的节点,并返回该节点的value。

hash(Object key)

    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

因为使用hash(key)&(n-1)计算数组下标,而数组长度n是2的幂,因此一些只在高bit位有区别的hashCode参与hashCode&(n-1)运算后,肯定会产生碰撞。因此,将hashCode的高位下移到低位,另一方面,为了避免高位没有参与到计算下标的运算当中,对移位后的hashCode与原hashCode进行异或操作。

resize()

当数组中的元素越来越多,产生碰撞的概率就越来越大。当元素个数超过阈值的话,会将数组容量扩大一倍。这个阈值是cap*loadFactor。容量cap的默认值是16,loadFactor默认值是0.75,因此当元素数量达到12后,数组大小将扩容到32。

为什么HashMap的容量是2的幂

因为我们使用hash(key)&(n-1)计算数组下标,当HashMap容量是2的幂时,n-1用二进制表示全为1,可作为掩码,这样,我们能最大程度保证散列值本身的分布均匀性。考虑如果n=0,n-1二进制为1001,对于hashCode:1111、1001、1101、1011,得到的下标值都是1001,这显然会导致性能下降。

为什么使用&代替模运算来计算下标

a%b == a&(b-1),当b为2的幂的时候,等式成立,而&运算相比%运算要快得多。


HashMap、LinkedHashMap和TreeMap的区别(番外篇,转自https://blog.csdn.net/fg2006/article/details/6411200)

Map主要用于存储健值对,根据键得到值,因此不允许键重复(重复了覆盖了),但允许值重复。

 

HashMap

  HashMap 是一个最常用的Map,它根据键的HashCode 值存储数据,根据键可以直接获取它的值,具有很快的访问速度。遍历时,取得数据的顺序是完全随机的。
  HashMap最多只允许一条记录的键为Null;允许多条记录的值为 Null。
  HashMap不支持线程的同步(即任一时刻可以有多个线程同时写HashMap),可能会导致数据的不一致。如果需要同步,可以用 Collections的synchronizedMap方法使HashMap具有同步的能力,或者使用ConcurrentHashMap。
  Hashtable与 HashMap类似,它继承自Dictionary类。不同的是:它不允许记录的键或者值为空;它支持线程的同步(即任一时刻只有一个线程能写Hashtable),因此也导致了 Hashtable在写入时会比较慢。

 

LinkedHashMap

  LinkedHashMap保存了记录的插入顺序,在用Iterator遍历LinkedHashMap时,先得到的记录肯定是先插入的。也可以在构造时带参数,按照应用次数排序。
在遍历的时候会比HashMap慢,不过有种情况例外:当HashMap容量很大,实际数据较少时,遍历起来可能会比LinkedHashMap慢。因为LinkedHashMap的遍历速度只和实际数据有关,和容量无关,而HashMap的遍历速度和他的容量有关。

 

TreeMap

  TreeMap实现SortMap接口,能够把它保存的记录根据键排序。
  默认是按键值的升序排序,也可以指定排序的比较器,当用Iterator 遍历TreeMap时,得到的记录是排过序的。

 

三种类型分别在什么时候使用

  1、一般情况下,我们用的最多的是HashMap。HashMap里面存入的键值对在取出的时候是随机的,它根据键的HashCode值存储数据,根据键可以直接获取它的值,具有很快的访问速度。在Map 中插入、删除和定位元素,HashMap 是最好的选择。
  2、TreeMap取出来的是排序后的键值对。但如果您要按自然顺序或自定义顺序遍历键,那么TreeMap会更好。
  3、LinkedHashMap 是HashMap的一个子类,如果需要输出的顺序和输入的相同,那么用LinkedHashMap可以实现,它还可以按读取顺序来排列,像连接池中可以应用。
 

其他

1. HashSet是通过HashMap实现的,TreeSet是通过TreeMap实现的,只不过Set用的只是Map的key
2. Map的key和Set都有一个共同的特性就是集合的唯一性.TreeMap更是多了一个排序的功能.
3. hashCode和equal()是HashMap用的, 因为无需排序所以只需要关注定位和唯一性即可.
  a. hashCode是用来计算hash值的,hash值是用来确定hash表索引的.
  b. hash表中的一个索引处存放的是一张链表, 所以还要通过equal方法循环比较链上的每一个对象才可以真正定位到键值对应的Entry.
  c. put时,如果hash表中没定位到,就在链表前加一个Entry,如果定位到了,则更换Entry中的value,并返回旧value
4. 由于TreeMap需要排序,所以需要一个Comparator为键值进行大小比较.当然也是用Comparator定位的.
  a. Comparator可以在创建TreeMap时指定
  b. 如果创建时没有确定,那么就会使用key.compareTo()方法,这就要求key必须实现Comparable接口.
  c. TreeMap是使用Tree数据结构实现的,所以使用compare接口就可以完成定位了.
 
 
注意:

  1、Collection没有get()方法来取得某个元素。只能通过iterator()遍历元素。 
  2、Set和Collection拥有一模一样的接口。 
  3、List,可以通过get()方法来一次取出一个元素。使用数字来选择一堆对象中的一个,get(0)...。(add/get) 
  4、一般使用ArrayList。用LinkedList构造堆栈stack、队列queue。 
  5、Map用 put(k,v) / get(k),还可以使用containsKey()/containsValue()来检查其中是否含有某个key/value。 
        HashMap会利用对象的hashCode来快速找到key。哈希码就是将对象的信息经过一些转变形成一个独一无二的int值,这个值存储在一个array中。我们都知道所有存储结构中,array查找速度是最快的。所以,可以加速查找。发生碰撞时,让array指向多个values。即,数组每个位置上又生成一个梿表。 
  6、Map中元素,可以将key序列、value序列单独抽取出来。 
    使用keySet()抽取key序列,将map中的所有keys生成一个Set。 
    使用values()抽取value序列,将map中的所有values生成一个Collection。 
    为什么一个生成Set,一个生成Collection?那是因为,key总是独一无二的,value允许重复。

LinkedHashMap

LinkedHashMap继承于HashMap,区别在于LinkedHashMap记录了元素插入的次序,因此使用Iterator遍历的时候,

你可能感兴趣的:(java基础)