基础知识-HashMap知识点

天大地大妈咪最大 提交于 2020-03-16 18:01:16

某厂面试归来,发现自己落伍了!>>>

key值为null时的存取

    我们知道HashMap允许插入元素的key值为null,我们看下这部分的源代码:

private V putForNullKey(V value) {
    for (Entry<K,V> e = table[0]; e != null; e = e.next) {
        if (e.key == null) {
            V oldValue = e.value;
            e.value = value;
            e.recordAccess(this);
            return oldValue;
        }
    }
    modCount++;
    addEntry(0, null, value, 0);
    return null;
}

    可以看出,key=null时,对应的数据是保存在内部数组第一个位置的链表中。知道了它是如何保存的,那么获取也就简单了:编译内部数组第一个位置的列表,找到key=null的数据项,返回该数据项中的value即可。

private V getForNullKey() {
    for (Entry<K,V> e = table[0]; e != null; e = e.next) {
        if (e.key == null)
            return e.value;
    }
    return null;
}


hashCode和equals方法

    在上一篇博文:《基础知识-HashMap》原理剖析中可以知道,对于key必须实现其hashCode和equals方法,缺一不可。但是大家知道,这两个方法默认都是从Object对象中继承来的,下面看下Object的原生的实现方式:

public native int hashCode();

public boolean equals(Object obj) {
    return (this == obj);
}

    可以看到,hashCode方法使用了native关键字,表示其实现调用C/C++底层的函数来实现的,而equals方法则认为只有两个对象的引用指向同一个对象时,才认为它们是相等的。

    如果你自定义了一个类,且没有重新覆写equals方法和hashCode方法,而你又使用该类的对象作为key值保存到HashMap,那么在读取HashMap的时候,除非你使用一个与你保存时引用完全相同的对象作为key值,否则你再也得不到该key所对应的value。

    这里给出良好hashCode和equals的实现例子:

public final class PhoneNumber {
    private final short areaCode;
    private final short prefix;
    private final short lineNumber;

    public PhoneNumber(int areaCode, int prefix,
                       int lineNumber) {
        this.areaCode  = (short) areaCode;
        this.prefix  = (short) prefix;
        this.lineNumber = (short) lineNumber;
    }

    @Override 
    public boolean equals(Object o) {
        if (o == null)
            return false;
        if (o == this)
            return true;
        if (!(o instanceof PhoneNumber))
            return false;
        PhoneNumber pn = (PhoneNumber)o;
        return pn.lineNumber == lineNumber
            && pn.prefix  == prefix
            && pn.areaCode  == areaCode;
    }

    @Override 
    public int hashCode() {
        int result = 17;
        result = 31 * result + areaCode;
        result = 31 * result + prefix;
        result = 31 * result + lineNumber;
        return result;
    }
}

    下面给出hashCode的实现建议:

    1、把某个非零的常数值,比如17,保存在一个名为result的int类型的变量中。
    2、对于对象中每个关键域f(指equals方法中涉及的每个域),完成以下步骤:
        a、为该域计算int类型的散列码c:
            i、如果该域是boolean类型,则计算(f?1:0)。
            ii、如果该域是byte,char,short或者int类型,则计算(int)f。
            iii、如果该域是long类型,则计算(int)(f^(f>>>32))。
            iv、如果该域是float类型,则计算Float.floatToIntBits(f)。
            v、如果该域是double类型,则计算Double.doubleToLongBits(f),然后按照步骤2.a.iii,为得到的long类型值计算散列值。
            vi、如果该域是一个对象引用,并且该类的equals方法通过递归地调用equals的方式来比较这个域,则同样为这个域递归地调用hashCode。如果需要更复杂的比较,则为这个域计算一个范式(canonical representation),然后针对这个范式调用hashCode。如果这个域的值为null,则返回0(其他常数也行)。
            vii、如果该域是一个数组,则要把每一个元素当做单独的域来处理。也就是说,递归地应用上述规则,对每个重要的元素计算一个散列码,然后根据步骤2.b中的做法把这些散列值组合起来。如果数组域中的每个元素都很重要,可以利用发行版本1.5中增加的其中一个Arrays.hashCode方法。
        b、按照下面的公式,把步骤2.a中计算得到的散列码c合并到result中:result = 31 * result + c; //此处31是个奇素数,并且有个很好的特性,即用移位和减法来代替乘法,可以得到更好的性能:31*i == (i<<5) - i,现代JVM能自动完成此优化。
    3、返回result
    4、检验并测试该hashCode实现是否符合通用约定。
    注意:在计算过程中,冗余项要排除在外。必须排除可以通过其他域值计算出来或equals比较计算中没用的的任何域,否则有可能违反hashCode第二条约定。

Fast-Fail机制

    HashMap内部维护了一个实例变量modCount,该变量被声明为volatile,被volatile声明的变量表示任何线程都可以看到该变量被其他线程修改的结果。当使用迭代器(Iterator)进行迭代时,会将modCount的值赋给expectedModCount,在迭代过程中,通过每次比较两者是否相等来判断HashMap是否在内部或者被其他线程修改。而HashMap中很多方法都会改变ModCount,如:put,remove,clear。

    先看下HashMap内部迭代器的实现:

private abstract class HashIterator<E> implements Iterator<E> {
    Entry<K,V> next;    // next entry to return
    int expectedModCount;    // For fast-fail
    int index;        // current slot
    Entry<K,V> current;    // current entry

    HashIterator() {
        expectedModCount = modCount;
        if (size > 0) { // advance to first entry
            Entry[] t = table;
            while (index < t.length && (next = t[index++]) == null)
                ;
        }
    }

    public final boolean hasNext() {
        return next != null;
    }

    final Entry<K,V> nextEntry() {
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
        Entry<K,V> e = next;
        if (e == null)
            throw new NoSuchElementException();

        if ((next = e.next) == null) {
            Entry[] t = table;
            while (index < t.length && (next = t[index++]) == null)
                ;
        }
        current = e;
        return e;
    }

    //...
}

    从上面的实现可以看出,HashMap所采用的Fast-Fail机制本质上是一种乐观锁机制,通过检查modCount状态,没有问题则忽略,有问题则抛出异常的方式,来避免线程同步的开销。当我们在迭代的过程中,修改了HashMap内部的元素,导致modCount的值改变,代码就会抛出java.util.ConcurrentModificationException。有意思的是如果HashMap只有一个元素的时候, ConcurrentModificationException 异常并不会被抛出。需要注意的就是:注意,迭代器的快速失败行为不能得到保证,一般来说,存在非同步的并发修改时,不可能作出任何坚决的保证。快速失败迭代器尽最大努力抛出 ConcurrentModificationException。因此,编写依赖于此异常的程序的做法是错误的,正确做法是:迭代器的快速失败行为应该仅用于检测程序错误


HashMap内部数组的容量

    当调用默认的构造函数时:

 public HashMap() {
     this.loadFactor = DEFAULT_LOAD_FACTOR;
     threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);
     table = new Entry[DEFAULT_INITIAL_CAPACITY]; //数组的影子
     init();
 }

    table.length=16。

    当指定初始容量和加载因子时,源码如下:

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;
    while (capacity < initialCapacity)
        capacity <<= 1;
    //通过上面这个算法,找到最接近initialCapacity,且又满足2的整数次方
    this.loadFactor = loadFactor;
    threshold = (int)(capacity * loadFactor);
    table = new Entry[capacity]; //发现实际容量为capacity, 并非参数initialCapacity
    init();
}

    由此看出,构造函数中指定的initialCapacity并不一定是HashMap内部维护数组的初始大小,而永远都是2的N次方。

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