java集合
list set map 各自实现类的区别
-
List(不唯一有序集合)
set方法会替换index位置的元素,返回替换前的元素
-
ArrayList:底层是数组,支持随机访问,读快写慢,线程不安全。
扩容策略:第一次为0,添加第一个元素后扩容为10,当满了之后扩容为(当前大小+当前大小 >> 1),增长1.5倍,如10->15->22->33->...。只可自定义初始大小。
-
Vector:底层是数组,读快写慢,因为加了写锁,线程安全,但多线程效率低。默认扩容为原来的2倍,可自定义初始大小和扩容数量。
-
Linkedlist:底层是双向链表,读慢写快,线程不安全。
-
-
Set(唯一无序集合)
当用add方法添加元素时,set中已有是无法添加成功的,add返回false,否则返回true。
- HashSet:底层是HashMap,都是存放链表的数组,允许有一个空值,值为PRESENT,线程不安全。
- TreeSet:底层是TreeMap,TreeMap是红黑树实现的,既然用到了树,最大的特点就是有序的,支持自然排序和自定义排序,但因为树结构复杂所以较HashSet读快写慢。
- LinkedHashSet:有序,和HashSet类似,但插入时是有序的,故较HashSet读快写慢。
-
Map
key-value形式组成Map.Entry对象的集合
-
HashMap
-
继承于AbstractMap,jdk1.2加入。
-
是个哈希表,Key-Value形式存储值,底层java8以前是数组+链表,java8以后若数组中元素数量大于8,就会将链表转成红黑树。
-
允许有一个null键。
-
线程不安全。多线程需要自己考虑同步机制。
又想线程安全又想效率高?降低锁的粒度。把hashmap分为若干个段,分别加锁,提升效率。
-
只保留了containsKey和containsValue,移除了contains因为容易误解。
-
在获取元素时,确定元素(Entry)在桶中位置的hash值需要重新计算!然后利用hash值对桶长度取模确定位置。 HashMap中规定容量大小是2^n,因此可以用位运算代替了取模运算来确定元素在哪个桶中。具体计算:index=hash & (length-1) 。
hash & (length-1)其实就是等价于 hash % length,但因为是位运算所以计算速度更快!但这个位运算前提是length必须是2^n,也是因为这个特性,所以HashMap要求桶大小必须为2的幂。
java8中hash的计算方式:
(key == null) ? 0 : (h = key.hashCode()) ^ (h >> 16);
为了避免容量过小时,hash & (length-1) 造成 严重hash碰撞,导致散列分布不均匀,所以需要用到扰动函数。扰动函数:右位移16位,正好是32bit的一半,自己的高半区和低半区做异或,就是为了混合原始哈希码的高位和低位,以此来加大低位的随机性。而且混合后的低位掺杂了高位的部分特征,这样高位的信息也被变相保留下来。
java7中hashMap利用的是indexFor函数来获取元素下标。
hash具体计算过程:若是字符串直接返回字符串hashCode。否则先异或hashCode,再异或高20位和低12位的异或,最终再异或对结果高7位和低4位的异或 然后返回hash值。
-
初始化长度大小(也叫桶大小(桶里面元素叫bin))为16,一定是2的幂(因为get元素时要利用位运算!)。当超过0.75(loadFactor扩容系数为0.75)倍长度时进行扩容(新容量=当前容量<<1)。
为啥是0.75?不是0.6?0.8?首先负载因子越小越不容易碰撞,因为能更快扩容,不能太小扩容太快,太大多碰撞。0.75正好,因为发现碰撞概率符合泊松分布,连碰8个概率很低,0.75以下概率太小了,以上又太大了,0.75是一个空间和效率的折中方案,经过了多年的测试和历练。
-
-
HashTable
- 继承于Dictionary,jdk1.0加入。
- 底层数据结构和HashMap底层一样。
- 键值都不允许为空。
- 加了写锁,是线程安全的,但效率较低。
- 有contains、containsKey和containsValue,contains和containsValue功能一样。
- 获取元素时,hash值直接就是对象的hashCode,然后对长度取模。此处直接用%运算符而不是位运算。
7. 初始长度大小为11,扩容方式:old*2+1
- TreeMap:是个有序集合,底层是红黑树,每个key-value都是红黑树的一个节点,根据Comparator或自然顺序确定在TreeMap中的位置。效率较HashMap慢,需要排序时才用TreeMap。
-
取模运算和取余运算的区别:
通常取模运算也称为取余运算。java中的其实是取余。当 x 和 y 的正负号一样的时候,两个函数结果是等同的;当 x 和 y 的符号不同时,rem 函数结果的符号和 x 的一样,而 mod 和 y 一样。
计算机取模运算其实开销较大,a%b:第一步先求商c=a/b,第二步求余 r=a-c*b。如果除数是2的幂,可以用 a & (b-1) 来优化,速度大大提升,因为在计算机内部 &运算开销是很小(几个时钟周期就能完成)的,但/除法或乘法的运算开销就较大(好几十个时钟周期才能完成除法运算)了。(n个机器节拍=n个时钟周期=1个状态周期,机器周期=2n个状态周期,指令周期=n个机器周期)。加减乘除需要的时钟周期。
HashMap为什么最大容量是2^30?
java中都是有符号数,int最大4字节32位,所以理论最大是231-1(为什么要-1?如127二进制是01111111,128(27)是10000000,因为最高位是符号位,所以最大能表示正整数应该是27-1)。但又因为对HashMap的容量大小要求是2的幂,所以就不能是231-1,因为不是2的幂,所以最大容量只能是2^30了。
HashMap扩容时,怎么确认链表中的元素的新位置
根据 e.hash & oldCap 来判断,若e.hash & oldCap==0,则说明在新数组中的位置没变;若e.hash & oldCap!=0,则说明位置变了,新位置=旧位置+oldCap。oldCap是原容量大小。
HashMap为什么不是线程安全?
因为没有加锁,HashMap的线程不安全主要体现在下面两个方面:
-
在JDK1.7中,当并发执行扩容操作时会造成环形链和数据丢失的情况。
1.7的transfer()扩容中重新定位每个桶的下标,并采用头插法将元素迁移到新数组中。头插法会将链表的顺序翻转,这也是形成死循环的关键点。而1.8中扩容移至resize()中,采用尾插法解决了死循环问题,但会有数据覆盖的问题。
-
在JDK1.8中,在并发执行put操作时会发生数据覆盖的情况。