这是一篇用于速记和通俗理解的文章,有不严谨的地方欢迎指出来,反正我不一定改。
先来挂一张Java集合的鸟瞰图(俯视图?框架图?)。
图片转载自:面试常被问到的 Java 集合知识点(详细)
接下来我们通俗的理解一下
我们使用Java语言进行开发,设计了类,那么肯定是需要一种东西来装载这些类的对象。于是Java设计了 集合 和 Map 来做这件事。
PS:Java容器里只能放对象,对于基本类型(int, long, float, double等),需要将其包装成对象类型后(Integer, Long, Float, Double等)才能放到容器里。很多时候拆包装和解包装能够自动完成。这虽然会导致额外的性能和空间开销,但简化了设计和编程。
集合(Collection)负责存储一个元素集合,而图(Map)负责存储键/值对映射。
但是集合和Map能表达的很有限。
- 我们对集合内元素的放置提出了要求:
- 来收集有序的且允许有重复元素的Collection,于是有了 List。
- 来收集无序的且不包含重复元素的Collection,于是有了 Set。
- 我们对集合的存取提出了要求:
- 希望读取的对象,顺序进入,顺序取出,即先进先出,于是有了 Queue。
- 希望读取的对象,顺序进入,逆序取出,即先进后出,于是有了 Stack。
- 我们对Map内元素的放置提出了要求:
- 如果按照 hash 的方式进行放置,于是有了 HashMap。
- 如果按照 二叉平衡树 的方式进行放置,于是有了 SortedMap 和 TreeMap。
好了,在上面,我们一共提到了六大种容器,分别是:List、Set、Queue、Stack、HashMap、TreeMap。
- 对于List,我们提出了更进一步的要求:
- 按照数组方式实现List,于是有了 ArrayList。
- 按照双向循环链表方式实现List,于是有了 LinkedList。
- 对于HashMap,我们提出了更进一步的要求:
- 既能够保留hash的存取方式,又能够像LinkedList一样记录元素的插入顺序,于是有了 LinkedHashMap。
- 对于一些缓存的场景,我们希望旧数据能自己消失,于是有了 WeakHashMap。
- 对于TreeMap,我们提不出更进一步的要求了。
- 对于Set,我们提出了更进一步的要求:
- 按照Hash的方式存取数据,于是有了 HashSet,本质上是包装了 HashMap。
- 按照AVL的方式存取数据,于是有了 TreeSet,本质上是包装了 TreeMap。
- 既有Hash的存取方式,又能够像LinkedList一样记录元素的插入顺序,于是有了 LinkedHashSet,,本质上是包装了 LinkedHashMap。
- 对于Queue,我们提出了更进一步的要求:
- 按照 优先队列 的方式存取数据,于是有了 PriorityQueue,
- 按照一个既可以当队列又可以当栈的方式存取数据,于是有了双端队列:DeQueue。
- DeQueue如果按照数组的方式实现,于是有了 ArrayDeque。
- 对于Stack,我们提不出更进一步的要求了,但是我们希望你用DeQueue来实现栈和队列。
专题说说面试常见的容器
- HashMap
- JDK 1.7 之前使用头插法、JDK 1.8 使用尾插法
- 当碰撞导致链表大于 TREEIFY_THRESHOLD = 8 时,就把链表转换成红黑树
- 转化前,桶的数量必须大于64,小于64的时候只会扩容
- 链表长度低于6,就把红黑树转回链表
- ConcurrentHashMap
- 没有初始化,就调用 initTable() 方法来进行初始化
- 没有 hash 冲突就直接 CAS 无锁插入
- 需要扩容,就先进行扩容
- 存在 hash 冲突,就加锁来保证线程安全,两种情况:一种是链表形式就直接遍历到尾端插入,一种是红黑树就按照红黑树结构插入
- 该链表的数量大于阀值 8,就要先转换成红黑树的结构,break 再一次进入循环
- 添加成功就调用 addCount() 方法统计 size,并且检查是否需要扩容
- 扩容方法 transfer():默认容量为 16,扩容时,容量变为原来的两倍,helpTransfer():调用多个工作线程一起帮助进行扩容,这样的效率就会更高
- hash 值,定位到该 table 索引位置,如果是首结点符合就返回,如果遇到扩容时,会调用标记正在扩容结点 ForwardingNode.find()方法,查找该结点,匹配就返回,以上都不符合的话,就往下遍历结点,匹配就返回,否则最后就返回 null。
上述ConcurrentHashMap部分内容引用于 互联网架构师
进入并发快车道
对于上述提及的容器,很多是不可以用在高并发环境下的,会有线程安全问题,对于常用的容器,我们在Java并发包下,列出了并发版本的容器。
-
先说说HashMap
- HashMap比较快,但是线程不安全,于是引入了 HashTable,通过Synchronize加锁的方式,变得线程安全,但是牺牲了效率,于是又引入 ConcurrentHashMap ,通过分段锁的方式,既保证了安全性有提升了效率。
- ConcurrentHashMap,在 JDK 1.7 中采用 分段锁的方式;JDK 1.8 中直接采用了CAS(无锁算法)+ synchronized
- JDK 1.7 中使用分段锁(ReentrantLock + Segment + HashEntry),相当于把一个 HashMap 分成多个段,每段分配一把锁,这样支持多线程访问。锁粒度:基于 Segment,包含多个 HashEntry。
- JDK 1.8 中使用 CAS + synchronized + Node + 红黑树。锁粒度:Node(首结点)(实现 Map.Entry)。锁粒度降低了。
- HashMap最多只允许一条记录的键为null,允许多条记录的值为null,而 HashTable不允许
- HashMap 需要重新计算 hash 值,而 HashTable 直接使用对象的 hashCode
-
还有ArrayList
- ArrayList不是线程安全的,于是有了 Vector,通过 Synchronize 加锁的方式,变得线程安全。
-
再说说Queue
- BlockingQueue、BlockingDeque、
- ArrayBlockingQueue、LinkedBlockingDeque、LinkedBlockingQueue、
- ConcurrentLinkedDeque、ConcurrentLinkedQueue、
- DelayQueue、PriorityBlockingQueue
-
最后说说List
- CopyOnWriteArrayList、CopyOnWriteArraySet