Android高级面试题详解之HashMap实现原理

正文

在这里插入图片描述

请说一说ArrayList、HashMap、LinkedHashMap ?

这道题想考察什么?

1、ArrayList、HashMap和LinkedHashMap的底层原理?

考察的知识点

ArrayList、HashMap、LinkedHashMap的源码的理解

考生如何回答
ArrayList

ArrayList:底层结构是一个数组,初始长度为10 容量不足时,扩展为原来的1.5倍也就是扩展为15
ArrayList底层是一个双向链表,好处是不用扩容,坏处是当你要寻找第N个元素时,实践复杂度为O(n),就是遍历N个元素去找到他 而ArrayList的时间复杂度是 O(1)

List:元素有序 有序值的是在内存中存放,可重复

HashMap

底层结构是 一个元素为链表的数组 , 虽然是数组 但是是无序插入数组的。根据哈希值来插入。
当hash相同则需要用到链表结构 , 把新插入的但 hashcode值相同的 链在之前插入的后面形成链表,当连得太多 就会形成红黑树,新加入的元素形成连头,第一存放在位置上的就成链尾

LinkedHashMap

底层是一个元素为链表的数组 + 元素之间的形成的双向链表 , 即是单向链表形成的双向链表。 双向链表维护元素的次序,这样使得元素看起来是以插入的顺序保存的。也就是说,当遍历LinkedHashSet集合里元素时,HashSet将会按元素的添加顺序来访问集合里的元素,因此LinkedHashSet可以保证元素按插入顺序输出。

LinkedHashMap底层使用哈希表与双向链表来保存所有元素,它维护着一个运行于所有条目的双向链表(如果学过双向链表的同学会更好的理解它的源代码),此链表定义了迭代顺序,该迭代顺序可以是插入顺序或者是访问顺序

  • 按插入顺序的链表:在LinkedHashMap调用get方法后,输出的顺序和输入时的相同,这就是按插入顺序的链表,默认是按插入顺序排序。

  • 按访问顺序的链表:在LinkedHashMap调用get方法后,会将这次访问的元素移至链表尾部,不断访问可以形成按访问顺序排序的链表。简单的说,按最近最少访问的元素进行排序(类似LRU算法)。

public static void main(String[] args) {
    Map<String, String> map = new LinkedHashMap<String, String>();
    map.put("apple", "苹果");
    map.put("watermelon", "西瓜");
    map.put("banana", "香蕉");
    map.put("peach", "桃子");

    Iterator iter = map.entrySet().iterator();
    while (iter.hasNext()) {
        Map.Entry entry = (Map.Entry) iter.next();
        System.out.println(entry.getKey() + "=" + entry.getValue());
    }
}

// print
apple=苹果 
watermelon=西瓜 
banana=香蕉 
peach=桃子
LinkedList与ArrayList的区别
  • LinkedList底层是双向链表
    ArrayList底层是可变数组

  • LinkedList不允许随机访问,即查询效率低
    ArrayList允许随机访问,即查询效率高

  • LinkedList插入和删除效率快
    ArrayList插入和删除效率低

对于随机访问的两个方法,get和set函数,ArrayList优于LinkedList,因为LinkedList要移动指针;对于新增和删除两个方法,add和remove函数,LinedList比较占优势,因为ArrayList要移动数据。

1.6 请说一说HashMap实现原理,扩容的条件,链表转红黑树的条件是什么 ?

这道题想考察什么?

1、HashMap的底层原理?

2、HashMap的扩容条件以及链表转换红黑树的条件

考察的知识点

HashMap原理、HashMap扩容条件的理解

考生如何回答
HashMap实现原理

HashMap内部是使用一个默认容量为16的数组来存储数据的,而数组中每一个元素却又是一个链表的头结点,所以,更准确的来说,HashMap内部存储结构是使用哈希表的拉链结构(数组+链表)。

在这里插入图片描述

HashMap中默认的存储大小就是一个容量为16的数组,所以当我们创建出一个HashMap对象时,即使里面没有任何元素,也要分别一块内存空间给它,而且,我们再不断的向HashMap里put数据时,当达到一定的容量限制时,HashMap就会自动扩容。

HashMap扩容条件

HashMap 的实例有两个参数影响其性能:“初始容量” 和 “加载因子”。容量 是哈希表中桶的数量,初始容量 只是哈希表在创建时的容量。加载因子 是哈希表在其容量自动增加之前可以达到多满的一种尺度。当哈希表中的条目数超出了加载因子与当前容量的乘积时,并且要存放的位置已经有元素了(hash碰撞),必须满足这两个条件,才要对该哈希表进行 rehash 操作,会将容量扩大为原来两倍。通常,默认加载因子是 0.75, 这是在时间和空间成本上寻求一种折衷。加载因子过高虽然减少了空间开销,但同时也增加了查询成本。

我们看下HashMap的put函数方法:

public V put(K key, V value) {
    if (table == EMPTY_TABLE) {//如果散列表是空的
        inflateTable(threshold);//会去建一个表
    }
    if (key == null) //hashmap会把key为空的放在数组头部
        return putForNullKey(value);
    int hash = sun.misc.Hashing.singleWordWangJenkinsHash(key);//根据key生成hash值
    int i = indexFor(hash, table.length); //生成散列表中的索引,也就是数组下标
    for (HashMapEntry<K,V> e = table[i]; e != null; e = e.next) {//遍历链表,看是否存在key值一样的对象,如果有的话就替换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++;
    //如果没找到key值一样的,就添加
    addEntry(hash, key, value, i);
    return null;
}

如何根据hash值生成数组下标,看indexFor()函数:

static int indexFor(int h, int length) {
    // assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
    return h & (length-1);
   //为什么长度要是偶数,因为hash值与length的与值不能超过length -1,要不然数组/就越界了,例如hash值是11000111B,length = 1000B,h & (length-1) = 111B,这样得到数组索引肯定不会越界了
}



void resize(int newCapacity) {
    HashMapEntry[] oldTable = table;
    int oldCapacity = oldTable.length;
    if (oldCapacity == MAXIMUM_CAPACITY) {
        threshold = Integer.MAX_VALUE;
        return;
    }
    HashMapEntry[] newTable = new HashMapEntry[newCapacity];//新建一个数组
    transfer(newTable);//完成新旧数组拷贝
    table = newTable;
    threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}

我们在看看最后的扩容步骤:

void transfer(HashMapEntry[] newTable) {
    int newCapacity = newTable.length;
    for (HashMapEntry<K,V> e : table) {//遍历整个数组
        while(null != e) {//将同一个位置的元素按链表顺序取出
            HashMapEntry<K,V> next = e.next;//先将当前元素指向的下一个元素存起来,一个一个存放到新表的位置中,记住不一定是同一位置,因为长度变了
            int i = indexFor(e.hash, newCapacity);//根据新数组长度,重新生成数组索引
            e.next = newTable[i];//将当前位置的元素链表头指向即将新加入的元素,
            newTable[i] = e;//然后放入数组中,完成同一位置元素链表的拼接,最先添加的元素总在链表末尾
            e = next;//然后继续循环,拿出下一个元素
        }
    }
}
链表转红黑树的条件

首先通过源码来分析下问题:

 //用来衡量是否要转红黑树的重要参数 
 static final int TREEIFY_THRESHOLD = 8;
 //转红黑树需要的最小数组长度
static final int MIN_TREEIFY_CAPACITY = 64;
 for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        // TREEIFY_THRESHOLD - 1=7,也就是说一旦binCount=7时就会执行下面的转红黑树代码
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }

 final void treeifyBin(Node<K,V>[] tab, int hash) {
        int n, index; Node<K,V> e;
        //这里的tab指的是本HashMap中的数组,n为数字长度,如果数组为null或者数组长度小于64
        if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
            //则调用resize()方法直接扩容,不转红黑树
            resize();
        //否则走转红黑树逻辑    
        else if ((e = tab[index = (n - 1) & hash]) != null) {
            TreeNode<K,V> hd = null, tl = null;
            do {
                TreeNode<K,V> p = replacementTreeNode(e, null);
                if (tl == null)
                    hd = p;
                else {
                    p.prev = tl;
                    tl.next = p;
                }
                tl = p;
            } while ((e = e.next) != null);
            if ((tab[index] = hd) != null)
                hd.treeify(tab);
        }
    }

可以知道【TREEIFY_THRESHOLD - 1】=7,所以binCount=7时才会转红黑树,而binCount初始赋值是0,++count是先加再用,所以其实binCount是从1开始的,1,2,3,4,5,6,7,每一个数值对应的都会创建一个newNode,所以binCount到数值7时,创建了7个新的Node节点,但是情不要忘记,我们创建的节点都是p.next,也就是p的后继节点,所以加上原来的p节点,也可以理解成是链表首节点,7+1=8,就是8个节点,所以链表里元素数目到8个时,会开始转红黑树。

总结:

当链表元素数目到8个,同时HashMap的数组长度要大于64,链表才会转红黑树,否则都是做扩容。

文末

更多的Android面试题可以扫描免费领取!

↓↓↓【预览】↓↓↓

img

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值