集合
1、List
-
ArrayList —ArrayList源码解读
ArrayList是基于数组来实现动态扩容。
size()为实际元素个数,elementData.length为容量。// 先看几个重要的成员变量和构造函数 // 默认初始容量 private static final int DEFAULT_CAPACITY = 10; // 空数组,有参构造参数为0时,即创建一个容量为0的list时使用 private static final Object[] EMPTY_ELEMENTDATA = {}; // 空数组,无参构造时使用 private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; // 无参构造函数 public ArrayList() { this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; } // 有参构造函数 public ArrayList(int initialCapacity) { if (initialCapacity > 0) { this.elementData = new Object[initialCapacity]; } else if (initialCapacity == 0) { this.elementData = EMPTY_ELEMENTDATA; } else { throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity); } }
重点来了
// add方法 public boolean add(E e) { // 判断list容量是否够,不够则扩容。 ensureCapacityInternal(size + 1); // Increments modCount!! elementData[size++] = e; return true; } private void ensureCapacityInternal(int minCapacity) { ensureExplicitCapacity(calculateCapacity(elementData, minCapacity)); } // 获取所需要的最小容量 private static int calculateCapacity(Object[] elementData, int minCapacity) { // 如果是无参构造创建的list,则直接用默认初始容量和最小所需容量比较获取较大者。 // 即如果创建list的时候没有指定大小,添加元素时list会默认扩容至10。 if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { return Math.max(DEFAULT_CAPACITY, minCapacity); } return minCapacity; } private void ensureExplicitCapacity(int minCapacity) { modCount++; if (minCapacity - elementData.length > 0) // 如果所需最小容量大于list的容量,则需要扩容。 grow(minCapacity); } // 开始扩容 private void grow(int minCapacity) { int oldCapacity = elementData.length; // 新容量为旧容量加上旧容量的一半,即扩容为原来的1.5倍。 int newCapacity = oldCapacity + (oldCapacity >> 1); if (newCapacity - minCapacity < 0) newCapacity = minCapacity; if (newCapacity - MAX_ARRAY_SIZE > 0) newCapacity = hugeCapacity(minCapacity); // 将数据拷贝到新数组中,这里会损失性能。 // 所以如果能确定一个list的大小,最好使用在创建时指定大小。 elementData = Arrays.copyOf(elementData, newCapacity); }
这里提供一个通过反射获取list容量的方法
public static int getArrayListLength(ArrayList list) throws Exception{ //获取Class对象 Class c = Class.forName("java.util.ArrayList"); //映射Class对象c所表示类(即Arraylist)的属性 Field field = c.getDeclaredField("elementData"); //设置访问状态表示为true field.setAccessible(true); //返回指定对象上此 Field 表示的字段的值 Object[] object = (Object[])field.get(list); return object.length; }
-
LinkedList
LinkedList比较简单,可以尝试自己写一个简易版本,然后慢慢补充细节。
public class MyLinkedList<T> { // 定义列表的头结点和尾结点 private Node<T> last; private Node<T> first; private int size; private static class Node<T> { // 定义结点类所需要的属性:值、上一个结点和下一个结点。 T value; Node<T> pre; Node<T> next; Node(T value, Node<T> pre, Node<T> next) { this.value = value; this.pre = pre; this.next = next; } } public void add(T value) { addLast(value); } public T get(int index) { return getNode(index).value; } private Node<T> getNode(int index) { Node<T> f; if (index < size / 2) { f = first; for (int i = 0; i < index; i++) { f = f.next; } } else { f = last; for (int i = size - 1; i > index; i--) { f = f.pre; } } return f; } private void addLast(T value) { // 定义一个新的指针指向尾结点,因为后续需要将尾结点指针指向新插入的结点。 Node<T> l = last; // 创建一个新结点,即我们新插入的结点。 Node<T> node = new Node<>(value, l, null); // 每次将新结点插到最后 last = node; if (l == null) { // 首次添加,first和last都指向同一结点 first = node; } else { // 之前的last结点的next需指向新结点 l.next = node; } size++; } public int size() { return size; } }
2、Map
- HashMap—hashmap相关问题—hashmap详解
hashmap数据结构为数组+链表,链表结点存储的是Entry对象,其有四个属性:hash、key、value、next;
数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的,如果定位到的数组位置不含链表(当前entry的next指向null),那么查找,添加等操作很快,仅需一次寻址即可;
如果定位到的数组包含链表,对于添加操作,其时间复杂度为O(n),首先遍历链表,存在即覆盖,否则新增;
对于查找操作来讲,仍需遍历链表,然后通过key对象的equals方法逐一比对查找。所以,性能考虑,HashMap中的链表出现越少,性能才会越好。
hashmap的默认容量为16,只有进行“put”操作时才会进行判断是否需要扩容,当发生哈希冲突并且size大于阈值的时候,需要进行数组扩容;扩容需要新建一个长度为之前数组2倍的新的数组,然后将当前的Entry数组中的元素全部传输过去,扩容后的新数组长度为之前的2倍,所以扩容相对来说是个耗资源的操作。
当链表长度的超过8时,链表会转成红黑树(
此时也需要满足table>=64,否则会进行扩容而不是树化
),利用红黑树快速增删改查的特点提高HashMap的性能,其中会用到红黑树的插入、删除、查找等算法。
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
// 如果此映射中包含的键值映射的数量小于64,则进行扩容
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
else if ((e = tab[index = (n - 1) & hash]) != null) {
TreeNode<K,V> hd = null, tl = null;
do {
TreeNode<K,V> p = replacementTreeNode(e, null);
if (tl == null)
hd = p;
else {
p.prev = tl;
tl.next = p;
}
tl = p;
} while ((e = e.next) != null);
if ((tab[index] = hd) != null)
hd.treeify(tab);
}
}
- concurrentHashmap–ConcurrentHashmap原理 || 相关面试题 || 视频 || 多线程扩容源码解析
concurrentHashmap:线程安全且高效
实现原理:JDK1.8 中的ConcurrentHashMap 选择了与 HashMap 相同的Node数组+链表+红黑树结构;在锁的实现上,抛弃了原有的 Segment 分段锁,采用CAS + synchronized实现更加细粒度的锁。
将锁的级别控制在了更细粒度的哈希桶数组元素级别,也就是说只需要锁住这个链表头节点(红黑树的根节点),就不会影响其他的哈希桶数组元素的读写,大大提高了并发度。
Map<Integer, Integer> chm = new ConcurrentHashMap<Integer, Integer>(32); //实际容量为64
// 在JDK1.8之后,创建ConcurrentHashMap对象时如果设置了初始容量32,
// 底层会计算真实容量为比((32+16)+1)大的最小2的次幂(即64)。
// tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));
扩容
常见问题:
1、数据结构
2、常用参数
3、源码
4、扩容流程
5、CAS+Synchronized
6、volatile
问:ConcurrentHashmap支持key、value为空吗?为什么?
答:不支持,会有二义性(不知道本来就是null,还是被其他线程改为null值)
问:ConcurrentHashmap的并发度是多少?
答:1.7为Segment[]的数组长度,默认是16;1.8后锁的细度为数组元素,所以并发度为数组长度。
问:ConcurrentHashmap的迭代器是强一致性还是弱一致性??
答:与 HashMap 迭代器是强一致性不同,ConcurrentHashMap 迭代器是弱一致性。
什么是弱一致性-->数据更新后,如果能容忍后续的访问只能访问到部分或者全部访问不到。
而弱一致性中最常用到的是最终一致性:
最终一致性-->不保证在任意时刻任意节点上的同一份数据都是相同的,
但是随着时间的迁移,不同节点上的同一份数据总是在向趋同的方向变化(最终会达到一致状态)。
ConcurrentHashMap 的迭代器创建后,就会按照哈希表结构遍历每个元素。
但在遍历过程中,内部元素可能会发生变化,如果变化发生在已遍历过的部分,迭代器就不会反映出来,
而如果变化发生在未遍历过的部分,迭代器就会发现并反映出来,这就是弱一致性。
这样迭代器线程可以使用原来老的数据,而写线程也可以并发的完成改变。
更重要的,这保证了多个线程并发执行的连续性和扩展性,是性能提升的关键。
- Linkedhashmap—Linkedhashmap—JDK1.8 || 彻头彻尾理解LinkedHashMap—JDK1.6
LinkedHashMap通过维护一个双向链表实现有序,accessOrder属性为true时按访问顺序迭代,accessOrder属性为false时按插入顺序迭代。
Linkedhashmap中的Entry继承了HashMap.node,并且添加了两个属性before、after用于维护整个双向链表。
须注意得是:next指针指向单向链表
中的下一节点;before和after指针指向双向链表
中的节点(由此来实现有序)。
static class Entry<K,V> extends HashMap.Node<K,V> {
Entry<K,V> before, after;
Entry(int hash, K key, V value, Node<K,V> next) {
super(hash, key, value, next);
}
}
- LRU算法(进阶)
1、通过LinkedHashMap实现LRU算法。
2、自行实现(HashMap --> LinedHashMap --> LRU)。
3、Set
Set集合使用最多的就是HashSet,其余set有兴趣可自行了解。
- HashSet
底层就是个hashmap,下面简单分析源码。
private transient HashMap<E,Object> map;
// 一个空的object(),用于map的value。
private static final Object PRESENT = new Object();
// 构造方法,创建一个HashMap
public HashSet() {
map = new HashMap<>();
}
public boolean add(E e) {
// 这里把添加的值存入map的key,因为key天然具有唯一性,value放一个静态变量。
return map.put(e, PRESENT)==null;
}