基础问题
1. 几类数据结构的定义和区别是什么?
2. 容器的数据结构底层是怎么实现的?怎么进行扩容?
3. 容器的线程安全怎么实现?
一、List容器
数据有序,允许重复数据,线程不安全。
1. linkedList 底层用双向链表实现,操作速度快,可以在头、尾、[n]操作数据。
2. ArrayList 底层用数组实现,查询速度快,默认数组大小是10。可以通过new ArrayList<Object>(n)设置n的值来指定数组的size,这样可以节省空间并避免数组扩容引起的效率下降。
ArrayList的扩容:当数据大小超过数组大小时,arrayList通过ensureCapacityd 调grow方法进行扩容,以下是jdk 1.8源码
private void grow(int minCapacity) {
int oldCapacity = elementData.length;
//默认扩容量为原size的一半
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
//最大扩容到Intger.MAX_VALUE
newCapacity = hugeCapacity(minCapacity);
// 直接用数组的copy进行扩容
elementData = Arrays.copyOf(elementData, newCapacity);
}
二、Set容器
set保存的数据不重复,set的底层都是通过其对应的map来实现的,例如HashSet底层是HashMap实现。所以常见Set与其对应的map一样是非线程安全的,但guava里实现了线程安全的ConcurrentHashSet(线程安全原理见下方ConcurrentHashMap)。
1. HashSet() :快速的定位、读取,会根据hash值来存放,因此读取出来的顺序不是插入的顺序。通常用于数据去重。
Hashset 集合收进一个对象时,会调用对象的hashcode()得到其Hashcode值来决定他的存储位置。默认的hashCode方法,是对对象进行hashCode;默认的equals方法也是比较对象是否相等。所以如果不重写这两个方法的话,下方例子中p1 和p2 都会被保存 而不会被去重。
Person p1 = new Person("fan");
Person p2 = new Person("fan");
Set<Person> personHashSet = new HashSet<Person>();
Collections.addAll(personHashSet, p1, p2);
2. TreeSet():是按照hash值的顺序(红黑树)排列的,如果要把一个对象添加进TreeSet时,则该对象的类必须实现Comparable接口。 通常用于去重+排序。
例,下方Person类没实现Comparable,添加时会报错“Person cannot be cast to java.lang.Comparable”
Person p1= new Person("小明");
Person p2= new Person("小花");
TreeSet<Person> personTreeSet = new TreeSet<>();
personTreeSet.add(p1);
personTreeSet.add(p2);
3. LinkedHashSet():按照插入顺序保存数据。 用于去重+保留插入顺序。
三、Map容器
map存储 key-value形式数据,HashMap和TreeMap不是线程安全的,ConcurrentHashMap是线程安全的
1. HashMap(): 在底层数据结构上采用了数组+链表+红黑树数组成的,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的,当链表长度大宇8时转为红黑树。默认初始容量是16,负载因子0.75。
存储原理:Put键值对的时候会先计算对应Key的hash值通过hash值来确定存放的地址->如果空则存入一个新的节点(Node),反之根据前面得到的节点p的hash值以及key跟传入的hash值以及参数进行比较,如果一样则替覆盖,不一致则以链表形式保存,把当前传来的参数生成一个新的节点保存在前一节点中。若链表长度>8,则红黑树形式保存。
扩容:发生扩容的时候有两种情况,一种是元素达到阀值了,一种是HashMap准备树形化但又发现数组太短<64,均会发生扩容。
下方是树形化扩容的源码注释
/**
* Replaces all linked nodes in bin at index for given hash unless
* table is too small, in which case resizes instead.
*/
final void treeifyBin(Node<K,V>[] tab, int hash) {}
下方是扩容的源码注释
/**
* Initializes or doubles table size. If null, allocates in
* accord with initial capacity target held in field threshold.
* Otherwise, because we are using power-of-two expansion, the
* elements from each bin must either stay at same index, or move
* with a power of two offset in the new table.
* @return the table
*
*
* 初始化或者翻倍表大小。
* 如果表为null,则根据存放在threshold变量中的初始化capacity的值来分配table内存
* (这个注释说的很清楚,在实例化HashMap时,capacity其实是存放在了成员变量threshold中,
* 注意,HashMap中没有capacity这个成员变量)
* 。如果表不为null,由于我们使用2的幂来扩容,
* 则每个bin元素要么还是在原来的bucket中,要么在2的幂中
* 此方法功能:初始化或扩容
*/
final Node<K,V>[] resize() {}
2. TreeMap(): 有序的key-value集合,通过红黑树实现。红黑树是一颗平衡二叉查找树,其特点是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。也就是说该二叉树的任何一个子节点,其左右子树的高度都相近。
遍历:使用entrySet遍历方式要比keySet遍历方式快。entrySet遍历方式获取Value对象是直接从Entry对象中直接获得,时间复杂度T(n)=o(1);keySet遍历获取Value对象则要从Map中重新获取,时间复杂度T(n)=o(n);keySet遍历Map方式比entrySet遍历Map方式多了一次循环,多遍历了一次table,当Map的size越大时,遍历的效率差别就越大。
3.ConcurrentHashMap(): 线程安全的,JDK 1.8取消了Segment分段锁的数据结构,取而代之的是数组+链表(红黑树)的结构。对于锁的粒度,调整为对每个数组元素加锁synchronized(Node)。读操作不加锁。
扩容:段内扩容(段内元素超过该段对应Entry数组长度的75%触发扩容,不会对整个Map进行扩容),插入前检测需不需要扩容,有效避免无效扩容
问题:
1. hashmap什么时候扩容?怎么扩容?【答案见上方三.1】
2. A put到hashMap里,改下A的值再push到map里 会发生什么?【答案直接替换】
3. concurrent分段锁 get不加锁 写是加锁 1.8怎么实现的?【答案见三.3】
4. Q和stack的实现原理?
5. 怎样代码实现一个BlockingQ? 怎样代码实现一个线程池?
【实现线程池 https://www.cnblogs.com/wxwall/p/7050698.html】
6. list扩容要多久?
参考资料:
1.JDK 1.8 HashMap工作原理和扩容机制(源码解析)https://blog.csdn.net/u010890358/article/details/80496144
2.jdk1.8 HashMap的扩容resize()方法详解 http://www.cnblogs.com/shianliang/p/9233199.html
3.ConcurrentHashMap的JDK1.8实现 https://blog.csdn.net/fouy_yun/article/details/77816587