集合类:(1面)
List:List中存储的数据是有顺序,并且允许重复;
Map:Map中存储的数据是无序的,其键是不能重复的,它的值可以重复
Set:Set里面不允许有重复的元素,即不能有两个相等的对象
List(ArrayList、Vector.LinkedList)
- ArrayList和Vector都是使用数组方式存储数据,查询快,增删慢,其中,Vector是线程安全的也就是它的方法之间是线程同步的,而ArrayList是线程不安全的。
LinkList:底层数据结构是链表,查询慢,增删快,线程不安全。
HashMap:
HashMap是Java采用数组散列+链表的方式用来储存键值对的数据结构,它是线程不安全的,可以储存null键值和值,HashTable不能;HashMap是非synchronized
HashMap的工作原理:
HashMap基于hashing原理,我们通过put(key, value)和get(key)方法储存和获取对象。当我们将键值对传递给put()方法时,它调用键对象的hashCode()方法来计算hashcode,然后返回的hashCode用于找到bucket位置来储存Entry对象当获取对象时调用get()方法,HashMap会使用键对象的hashcode找到bucket位置(Key值的HashCode值与HashMap的长度进行取模),遍历LinkedList(调用keys.equals()方法)直到找到值对象,然后返回值对象。
当两个对象的hashcode相同会发生什么?
因为hashcode相同,所以它们的bucket位置相同,‘碰撞’会发生。因为HashMap使用链表存储对象,这个Entry(包含有键值对的Map.Entry对象)会存储在链表中
如果两个键的hashcode相同,你如何获取值对象?
当我们调用get()方法,HashMap会使用键对象的hashcode找到bucket位置,然后获取值对象,会调用keys.equals()方法去找到链表中正确的节点,最终找到要找的值对象。
HashMap的死锁
由于HashMap的容量是有限的,如果HashMap中的数组的容量很小,假如只有2个,那么如果要放进10个keys的话,碰撞就会非常频繁,此时一个O(1)的查找算法,就变成了链表遍历,性能变成了O(n),这是Hash表的缺陷。
为了解决这个问题,HashMap设计了一个阈值,其值为容量的0.75,当HashMap所用容量超过了阈值后,就会自动扩充其容量。
在多线程的情况下,当重新调整HashMap大小的时候,就会存在条件竞争,因为如果两个线程都发现HashMap需要重新调整大小了,它们会同时试着调整大小。在调整大小的过程中,存储在链表中的元素的次序会反过来,因为移动到新的bucket位置的时候,HashMap并不会将元素放在链表的尾部,而是放在头部,这是为了避免尾部遍历。如果条件竞争发生了,那么就会产生死循环了
HashMap的长度
HaspMap的默认初始长度是16,并且每次扩展长度或者手动初始化时,长度必须是2的次幂。之所以是16,是为了服务于从Key值映射到index的hash算法。前面说到了,从Key值映射到数组中所对应的位置需要用到一个hash函数:index = hash("Java");
那么为了实现一个尽量分布均匀的hash函数,利用的是Key值的HashCode来做某种运算。因此问题来了,如何进行计算,才能让这个hash函数尽量分布均匀呢?
一种简单的方法是将Key值的HashCode值与HashMap的长度进行取模运算,即 index = HashCode(Key) % hashMap.length,但是,但是!这种取模方式运算固然简单,然而它的效率是很低的, 而且,如果使用了取模%, 那么HashMap在容量变为2倍时, 需要再次rehash确定每个链表元素的位置,浪费了性能。
因此为了实现高效的hash函数算法,HashMap的发明者采用了位运算的方式。那么如何进行位运算呢?可以按照下面的公式:
index = HashCode(Key) & (hashMap.length - 1);
接下来我们以Key值为“apple”的例子来演示这个过程:
-
计算“apple”的hashcode,结果为十进制的3029737,二进制的101110001110101110 1001。
-
HashMap默认初始长度是16,计算hashMap.Length-1的结果为十进制的15,二进制的1111。
-
把以上两个结果做 与运算,101110001110101110 1001 & 1111 = 1001,十进制是9,所以 index=9。
可以看出来,hash算法得到的index值完全取决与Key的HashCode的最后几位。这样做不但效果上等同于取模运算,而且大大提高了效率。
那么回到最初的问题,初始长度为什么是16或者2的次幂?如果不是会怎么样?
我们假设HaspMap的初始长度为10,重复前面的运算步骤:
单独看这个结果,表面上并没有问题。我们再来尝试一个新的HashCode 101110001110101110 1011 :
然后我们再换一个HashCode 101110001110101110 1111 试试 :
这样我们可以看到,虽然HashCode的倒数第二第三位从0变成了1,但是运算的结果都是1001。也就是说,当HashMap长度为10的时候,有些index结果的出现几率会更大,而有些index结果永远不会出现(比如0111)!
所以这样显然不符合Hash算法均匀分布的原则。
而长度是16或者其他2的次幂,Length - 1的值的所有二进制位全为1(如15的二进制是1111,31的二进制为11111),这种情况下,index的结果就等同于HashCode后几位的值。只要输入的HashCode本身分布均匀,Hash算法的结果就是均匀的。这也是HashMap设计的玄妙之处。