java中的数据结构之第一节(HashMap部分源码分析)

149 阅读5分钟

前言

在java业界,有一些问题在面试的时候可以说基本上是必问的。比如:java中HashMap、HashTable、ConcurrentHashMap的区别,又或者是ArrayList 和 LinkedList 和 Vector 的区别。

而这些问题,我总结一下,基本上都是java中的数据结构问题,我们对这些数据结构的了解是非常浅薄的,比如说在1.8JDk版本前后的这些数据结构的变化是非常大的,但是我们对这些却依然不是很了解,不深入进去,很难明白到底发生了什么。那么,接下来的一系列博客,都让我们来分析一下java中的数据结构吧。

今天,我们先了解的是Hashmap。

ps : 我现在的jdk版本是1.8。

简介

在JDK1.6,JDK1.7中,HashMap采用位桶+链表实现,即使用链表处理冲突,同一hash值的链表都存储在一个链表里。但是当位于一个桶中的元素较多,即hash值相等的元素较多时,通过key值依次查找的效率较低。

而JDK1.8中,HashMap采用位桶+链表+红黑树实现,当链表长度超过阈值时,将链表转换为红黑树,这样大大减少了查找时间。

主要参数

先来看HashMap里面的一些参数:

//默认初始大小是16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;  
**************************************************************************************
//设置的最大容量是2的30次方
static final int MAXIMUM_CAPACITY = 1 << 30;    
**************************************************************************************
//设置的默认加载因子为0.75
static final float DEFAULT_LOAD_FACTOR = 0.75f;   
**************************************************************************************
 //当某个桶节点数量大于8时,会转换为红黑树。
static final int TREEIFY_THRESHOLD = 8;        
**************************************************************************************
//当某个桶节点数量小于6时,会转换为链表,前提是它当前是红黑树结构。
static final int UNTREEIFY_THRESHOLD = 6; 
**************************************************************************************
//当整个hashMap中元素数量大于64时,也会进行转为红黑树结构。
static final int MIN_TREEIFY_CAPACITY = 64;
*************************************************************************************
//也是加载因子,只不过这个是变量。
final float loadFactor;
*************************************************************************************
 //临界值,也就是元素数量达到临界值时,会进行扩容。
int threshold;

PS:HashMap默认的加载因子是0.75,最大容量是16,因此可以得出HashMap的默认容量是:0.75*16=12。

加载因子是表示Hsah表中元素的填满的程度.若:加载因子越大,填满的元素越多,好处是,空间利用率高了,但:冲突的机会加大了.反之,加载因子越小,填满的元素越少,好处是冲突的机会减小了,但空间浪费多了。

构造函数

看了HashMap的一些参数,现在我们来了解一下HashMap的构造函数:

我们看到在源码中,HashMap有四个构造函数,在其中,HashMap(int initialCapacity)这个构造函数,实际上指向了另外一个构造函数HashMap(int initialCapacity, float loadFactor)。我们还是把所有的构造函数都列出来吧。

构造函数1

 public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR; // 设置加载因子为默认值0.75
}

构造函数2

public HashMap(int initialCapacity) {
    this(initialCapacity, DEFAULT_LOAD_FACTOR);   //指向构造函数3,且加载因子为默认的0.75
}

构造函数3

public HashMap(int initialCapacity, float loadFactor) {
  //判断初始加载容量是否小于0,若是,直接抛出异常
    if (initialCapacity < 0)          
        throw new IllegalArgumentException("Illegal initial capacity: " +  initialCapacity);
  //判断初始加载容量是否大于最大容量,若是,设置初始加载容量为最大容量
    if (initialCapacity > MAXIMUM_CAPACITY)  
        initialCapacity = MAXIMUM_CAPACITY;
  //判断加载因子是否小于0或者判断是否为NaN,若是,抛出异常
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new IllegalArgumentException("Illegal load factor: " +loadFactor);
  //设置初始加载因子
    this.loadFactor = loadFactor;
  //对临界值进行初始化,tableSizeFor(t)这个方法会返回大于t值的,且离其最近的2次幂,例如t为29,则返回的值是32
    this.threshold = tableSizeFor(initialCapacity);
}

PS:NaN 实际上就是 Not a Number的简称。0.0f/0.0f的值就是NaN,从数学角度说,0/0就是一种未确定。

构造函数4

 public HashMap(Map<? extends K, ? extends V> m) {
    this.loadFactor = DEFAULT_LOAD_FACTOR;  //设置加载因子为默认的0.75
    putMapEntries(m, false);
}

  final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
    //获取该map的实际长度
    int s = m.size();
    if (s > 0) {
        //判断table是否初始化,如果没有初始化
        if (table == null) { // pre-size
            //求出需要的容量,因为实际使用的长度=容量*0.75得来的,+1是因为小数相除,基本都不会是整数,容量大小不能为小数的,后面转换为int,多余的小数就要被丢掉,所以+1,例如,map实际长度22,22/0.75=29.3,所需要的容量肯定为30,有人会问如果刚刚好除得整数呢,除得整数的话,容量大小多1也没什么影响
            float ft = ((float)s / loadFactor) + 1.0F;
            //判断该容量大小是否超出上限。
            int t = ((ft < (float)MAXIMUM_CAPACITY) ?
                     (int)ft : MAXIMUM_CAPACITY);
            //对临界值进行初始化,tableSizeFor(t)这个方法会返回大于t值的,且离其最近的2次幂,例如t为29,则返回的值是32
            if (t > threshold)
                threshold = tableSizeFor(t);
        }
        //如果table已经初始化,且map的实际长度大于临界值,则进行扩容操作,resize()就是扩容。
        else if (s > threshold)
            resize();
        //遍历,把map中的数据转到hashMap中。
        for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
            K key = e.getKey();
            V value = e.getValue();
            putVal(hash(key), key, value, false, evict);
        }
    }

HashMap的扩容方法非常复杂,等有机会了我们再来分析,我们只要明白在HashMap中的一些参数,就大概能明白HashMap的扩容方法的原理。

//当某个桶节点数量大于8时,会转换为红黑树。
static final int TREEIFY_THRESHOLD = 8;        
**************************************************************************************
//当某个桶节点数量小于6时,会转换为链表,前提是它当前是红黑树结构。
static final int UNTREEIFY_THRESHOLD = 6; 
**************************************************************************************
//当整个hashMap中元素数量大于64时,也会进行转为红黑树结构。
static final int MIN_TREEIFY_CAPACITY = 64;

JDK1.8中,HashMap采用位桶+链表+红黑树实现,当链表长度超过阈值时,将链表转换为红黑树,因此,在HashMap的扩容方法中,不仅仅是容量的变化,而且也有着数据结构本质上的变化。

总结

我们发现,java中的HashMap是在与时俱进,变化一直都在。

还有一些关于HashMap的put和get方法,我们在这里没有进行分析,但是由于本人的技术还不够,对于红黑树的不熟悉,因此不能深入解析,希望以后在了解了红黑树以后,能够回过头来更深入的了解HashMap。