Java常见集合
Java集合类主要由两个根接口Collection和Map派生出来的,Collection派生出了三个子接口: List、Set、Queue (Java5新增的队列),因此Java集合大致也可分成List、Set、Queue、Map四种接口体系。
框架图如下:
上图中的绿色代表常用的实现类。
ArrayList
- 底层使用Object数组实现,不保证线程安全。
- 因为底层是数组,插入删除时间复杂度为O(n),可以快速随机访问,复杂度为O(1)。
- 列表的结尾会预留一定的容量空间。
ArrayList与Vector区别
- Vector是线程安全的,因为每个方法前都加了synchronized关键字。ArrayList线程不安全。
- 扩容:ArrayList默认扩容至原来的1.5倍,Vector默认扩容至原来的2倍。
ArrayList与LinkedList区别
- 两者都不保证线程安全
- ArrayList底层是数组,LinkedList底层是双向循环链表
ArrayList扩容机制
- 扩容本质是新建一个更大的数组,然后把原来数组的元素复制到新数组去。新建数组大小ArrayList默认是原来的1.5倍。
HashMap
- 底层是一个Node数组,数组每个元素可以看作一个key-value键值对。
- 数组长度为2的幂次,主要为了能通过位运算获取key的索引位置,提升计算效率。
- 产生哈希冲突时,使用拉链法,数组每个元素即为一个链表。要获取元素需要先经哈希计算得到数组下标,再从对应下标的链表上遍历找到相同key值的元素。遍历链表的时间复杂度为O(n),如果链表过长则效率不理想。所以当链表长度大于8并且整个数组容量大于64时,链表会转换成红黑树。但是如果数组长度小于64,会选择先进行数组扩容,而不是转换为红黑树。
HashMap的扩容机制
- 默认扩容为原数组长度的2倍。在初始化时如果没有指定容量大小,则从0扩容到16。如果指定了初始大小,则Hash表大小为最接近指定容量且大于指定容量的2的幂次。
- 扩容过程中需要重新哈希,重新哈希的过程巧用了2次幂的扩展之后,元素的位置要么在原位置,要么在原位置再移动2次幂的位置这个特点,直接将key的hash值和oldCap相与,如果为0,则保持原位,如果为1,则放⼊到原位+oldCap的位置。
HashMap的key
一般用Integer、String这种不可变类为key,String最为常用。
- 字符串不可变,在创建的时候hashcode被缓存,不需要重新计算。
- 获取对象时需要用到equals()和hashcode(),这两个方法需要正确被重写,在String类中已经很规范重写了这两个方法。
HashMap为什么线程不安全
- 多线程下put操作会导致数值被覆盖
- 多线程下一边扩容重新哈希一边get会导致get到null。
ConcurrentHashMap的实现原理
- jdk1.7使用分段锁
- jdk1.8抛弃分段锁,使用CAS+synchronized实现更低粒度的锁 ,只需要锁住链表头结点,就不会影响其它哈希桶元素的读写,大大提高了并发度。
- ConcurrentHashMap的put逻辑(jdk1.8):根据key计算hash值,判断是否需要初始化,定位到node,拿到首节点f,如果首节点为null,则通过CAS尝试添加元素,如果f.hash = MOVED = -1 ,说明其它线程在扩容,参与一起扩容。如果都不满足,就用sychronized锁住f节点,判断是链表还是红黑树,遍历插入。
- ConcurrentHashMap的get逻辑(jdk1.8):get方法不需要加锁,因为node的val和指针next都用了volatile修饰,修改val或者新增节点对其它线程都可见。