昨天参加了支付宝的电话一面,备受打击,更多的是让自己明白了自己的缺陷在哪,以及了解了互联网公司面试是怎样问问题的。 从结果来看,肯定是挂了,因为只谈了46分钟。从自我来看,收获满满,4个字评价我自己,就是“一知半解”。很多名词都知道,也都看过一点相关的文章,但是让我自己说,我只能说一个大概,上次看了HashMap的源码之后,又看了其他一些集合类,但是没有总结和深入,比如:为什么HashMap用了红黑树?为什么提高了效率?我答不上来,因为我根本没有去看TreeNode
这个内部类的具体实现,对红黑树也不了解,然后被面试官成功带到了他挖好的坑里,归咎到底,还是自己实力不够硬。 略读JDK源码不会再写,要写就写深入JDK源码。
直接看源码:
/** * The table, initialized on first use, and resized as * necessary. When allocated, length is always a power of two. * (We also tolerate length zero in some Operations to allow * bootstrapping mechanics that are currently not needed.) */ transient Node<K,V>[] table; 既然看到了Node<K,V>
,那就看一下它的源码:
Node<K,V>
在每一个哈希表的桶里,放的都是这样的一个个Node(在桶内元素不大于8个时),如果桶内元素大于8个,那么就要将链表换成红黑树进行存储,为什么要换,原因等下说。接下来看一下红黑树的数据结构。
TreeNode<K,V>
首先说一下,这个TreeNode<K,V>
继承了LinkedHashMap.Entry<K,V>
这个类,而Entry<K,V>
这个类又继承了Node<K,V>
,但是添加了两个Node<K,V>
的属性 before 和 after,但是TreeNode<K,V>
并没有使用,是在LinkedHashMap
这个类中使用了,那么我们只看HashMap的话,就可以认为TreeNode<K,V>
继承了Node<K,V>
,不用管这个LinkedHashMap.Entry<K,V>
了。 看源码:
由于二叉树对某个元素的搜索是与该元素的距离树根结点的高度来决定的,而红黑树的高度不会超过2lg(n+1),因此可以在O(log n)时间内做查找,插入和删除,时间非常快,而二叉查找树通常情况不是一个平衡的二叉树,最坏情况下,树的高度可以达到n,因此查找的时间为O(n)。 这也就是为什么JDK1.8中如果链表结点数量超过8个以后要转化为红黑树的原因,链表的查找时间复杂度为O(n),而红黑树可以达到O(log n),我觉得是开发人员进行实验过后,当数量超过8个以后,红黑树的结构调整相比于链表的查询的效率低下,综合起来红黑树效率比链表高,所以在1.8中进行了调整。
HashMap中最常用的两个方法就是get和put了,先看put:
public V put(K key, V value) { return putVal(hash(key), key, value, false, true); }这里调用了一个putVal方法,查看源码:
/** * Implements Map.put and related methods * * @param hash hash for key * @param key the key * @param value the value to put * @param onlyIfAbsent if true, don't change existing value * @param evict if false, the table is in creation mode. * @return previous value, or null if none */ final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; // 如果原表为空或者大小为0,那么调整数组大小为默认大小,并且赋值n=0 if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; /* resize的作用 摘自 http://www.cnblogs.com/think-in-java/p/5170914.html 当HashMap中的元素越来越多的时候,hash冲突的几率也就越来越高,因为数组的长度是固定的。 所以为了提高查询的效率,就要对HashMap的数组进行扩容,原数组中的数据必须重新计算其在新 数组中的位置,并放进去,这就是resize。 那么HashMap什么时候进行扩容呢?当HashMap中的元素个数超过数组大小*loadFactor时, 就会进行数组扩容,loadFactor的默认值为0.75,这是一个折中的取值。也就是说,默认情况下, 数组大小为16,那么当HashMap中元素个数超过16*0.75=12的时候,就把数组的大小扩展为 2*16=32,即扩大一倍,然后重新计算每个元素在数组中的位置,而这是一个非常消耗性能的操作, 所以如果我们已经预知HashMap中元素的个数,那么预设元素的个数能够有效的提高HashMap的性能。 比如说,我们有1000个元素new HashMap(1000), 但是理论上来讲new HashMap(1024)更合适, 不过 上面annegu已经说过,即使是1000,hashmap也自动会将其设置为1024。 但是 new HashMap(1024)还不是更合适的,因为0.75*1000 < 1000, 也就是说为了让 0.75 * size > 1000, 我们必须这样new HashMap(2048)才最合适,既考虑了&的问题, 也避免了resize的问题。 */ // 如果当前hash值对应的位置为空,那么创建将要放置的Node放进去 if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); else { Node<K,V> e; K k; // 要放置的元素的key已经存在了,就将e=q,后面会处理 if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; // 如果是红黑树,那么就把数据按照红黑树插入的方式插入 else if (p instanceof TreeNode) e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); else { // 遍历链表 for (int binCount = 0; ; ++binCount) { // 找到最后一个结点,就把要插入的Node放进去 if ((e = p.next) == null) { p.next = newNode(hash, key, value, null); // 如果这条链的长度已经大于默认值(8),那么就把链表转化成红黑树进行存储 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); break; } // 判断链表中结点的key值与插入的元素的key值是否相等,相等就退出循环 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } // 找到key值、hash值与插入元素相等的结点,用新的值代替旧的值,并返回旧的值 if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeaccess(e); // 这个函数体内没有任何操作,文档上给的是回调函数,我不知道用处 return oldValue; } } /* 如果在使用迭代器的过程中有其他线程修改了map,那么将抛出ConcurrentModificationException, 这就是所谓fail-fast策略。在迭代过程中,判断modCount跟expectedModCount是否相等,如果不相等 就表示已经有其他线程修改了Map */ ++modCount; // 修改次数增加 if (++size > threshold) // 如果超出重构阈值,需要重新分配空间 resize(); afterNodeInsertion(evict); // 同afterNodeAccess() return null; }接下来看get函数。
源码如下:
public V get(Object key) { Node<K,V> e; return (e = getNode(hash(key), key)) == null ? null : e.value; } 查看getNode
方法源码:
还是看的不够仔细,只重点看了get和put,但是对大致原理有了一定的了解,而且java.util包中还有很多容器类,现在时间比较紧,要把这些重点看一遍,还要看一下Concurrent包下的类,以后有时间再仔细更加深入地看。
新闻热点
疑难解答