1 概论
【强制】Java中常见的集合。
Java集合主要包括Collection(集)和Map(映射)两大类,Collection接口又有Set(集合)和List(列表)两个子接口。
List接口的主要实现类有:ArrayList、LinkedList、Stack、Vector等。
Set接口的主要实现类有:HashSet、TreeSet、LinkedHashSet等。
Map接口的主要实现类有:HashMap、TreeMap、HashTable、ConcurrentHashMap等。
【强制】集合和数组的区别。
数组的长度固定,集合的长度可变。
数组可以存放基本数据类型,集合不可以。
【推荐】集合是否可以存储null?
List接口的实现类都可以存储多个null。
Set接口的实现类中,HashSet、LinkedHashSet可以存储一个null,TreeSet不能存储null。
Map接口的实现类中,HashMap、LinkedHashMap的key和value都可以为null,TreeMap的key不可用为null,value可以为null,HashTable、ConcurrentHashMap的key和value都不能为null。
【推荐】transient关键字。
用transient修饰的成员变量不参与序列化过程。
集合类通常会预留一些容量,当容量不足时再进行扩容,这样,有一部分空间就没有实际存储元素。
在集合类中使用transient关键字的目的是为了保证只序列化集合中实际存储的元素,而不是整个集合,从而节省时间和空间。
2 List
【强制】ArrayList。
1) ArrayList和LinkedList的区别。
ArrayList的底层实现是动态数组,随机访问的效率较高;
LinkedList的底层实现是双向链表,增加或删除结点的效率较高。并且实现了Deque接口,可以用作双端队列。
2) Vector和ArrayList的区别。
Vector是线程同步的,Vector的方法加了synchronized关键字;
ArrayList是线程不同步的。
3) 遍历ArrayList。
List<String> list = new ArrayList(); list.add("hello"); list.add("world"); // 使用迭代器遍历 Iterator<String> it = list.iterator(); while (it.hasNext()) { System.out.println(it.next()); } // foreach遍历 for (String s : list) { System.out.println(s); }
4) ArrayList的扩容机制。
当ArrayList对象第一次调用add方法时,初始化定义数组容量为10;
当ArrayList容量已满,再次调用add方法时就会发生扩容。
ArrayList扩容时,newCapacity = oldCapacity + (oldCapacity >> 1),即新容量是老容量的1.5倍。
当ArrayList的长度大于Integer.MAX_VALUE时,抛出OutOfMemoryError。
3 Map
【强制】HashMap。
1) 数据结构。
HashMap的数据结构为 数组+链表/红黑树。
HashMap中的每个结点(Node)都包含4个属性:key、value、next、hash。
2) 遍历HashMap的时间复杂度。
HashMap根据key查找数组下标的时间复杂度为O(1),不影响遍历整体的时间复杂度。
然后遍历链表的时间复杂度是O(n),遍历红黑树的时间复杂度是O(logn)。
3) 向HashMap中插入结点。
向HashMap中插入结点时,会先使用对象的key调用哈希函数(hashCode高低16位做异或运算)计算得到一个hash值。
然后使用hash值和[数组长度-1](数组长度恒为2的n次幂,所以[数组长度-1]的每一位都是1)做位与运算得到一个数组下标。
如果数组该下标的位置为空,就插入,如果下标位置上已有其他结点,说明发生了hash冲突(也叫hash碰撞),就把要插入的结点和该位置上的结点通过next属性连接起来形成链表。
链表的插入方式是尾插入。如果采用头插入方式,在并发场景下,扩容时可能会出现循环链表。
以上步骤完成后,判断当前HashMap中结点的数目是否超过了阈值,如果超过就调用resize方法进行扩容(先插入再扩容)。
4) HashMap的扩容机制。
新建的HashMap的容量默认为16。
HashMap扩容的阈值是容量乘以负载因子,负载因子默认是0.75。0.75是一个折衷的选择,如果负载因子较大,发生哈希碰撞的概率就更大,如果负载因子较小,存储消耗的空间就更多。
当HashMap中结点的数目超过了阈值时,将桶数组的大小扩充到2倍(实际是创建了一个新的数组)。
然后遍历已存储的结点,用结点的hash值与原数组大小做与运算(原数组大小是2的倍数,只有最高位是1,其他位都是0),若结果为0,将结点放到新数组的当前位置,若结果不为0,将结点放到新数组下标为 当前下标+原数组大小 的位置。这么做的好处是省略了重新计算每个结点下标的步骤。
5) HashMap是线程不安全的。
如果想在高并发环境下使用HashMap,有两种方案:
第一种方案是使用java.util.Collenctions里面提供的包装方法synchronizedMap(Map<K,V> m)来包装HashMap,得到一个SynchronizedMap。
第二种方案是使用java.util.concurrent.ConcurrentHashMap。
【强制】红黑树。
1) 数据结构。
红黑树是平衡的二叉搜索树。
二叉搜索树的规则是任意结点的左子树(如果有)上的所有结点的值均小于该结点的值,右子树(如果有)上的所有结点的值均大于该结点的值。
红黑树在二叉搜索树的基础上做了平衡,保证每个结点的左子树和右子树的高度差最大为2,如果超过了就进行调平衡。
调平衡操作包括左旋、右旋和变色。红黑树的任何不平衡问题都能在三次旋转之内解决。
2) HashMap中链表与红黑树之间的转换。
链表长度大于8且数组长度大于等于64时,链表转换成红黑树。
链表长度小于6时,红黑树转换成链表。
链表转红黑树的阈值设为8的原因是:理想情况下使用随机hashCode算法,所有桶中的结点遵循泊松分布,在同一个桶位发生8次哈希碰撞的概率微乎其微,将阈值设为8可以保证链表在大多数情况下不会转红黑树。
红黑树转链表的阈值设为6的原因是:避免红黑树和链表之间频繁地来回转换。
【强制】ConcurrentHashMap。
1) 数据结构。
JDK 8以后,数组结构为 数组+链表/红黑树。
对链表/红黑树的头/根结点加synchronized锁,在同一时间,只能有一个线程对该链表/红黑树进行操作。
2) ConcurrentHashMap的key和value不支持null。
在并发环境下,使用get方法得到了null,无法判断是value为null,还是没有找到对应的key。
【强制】LinkedHashMap。
LinkedHashMap.Entry除了继承HashMap.Node外,还有before和after两个指针,用于标识前置结点和后置结点。
【强制】TreeMap。
TreeMap的数据结构是一棵红黑树。
TreeMap的key按照自然顺序或者Comprator的顺序进行排列。
4 Set
【强制】HashSet。
HashSet本质上是一个HashMap的keySet。
private transient HashMap<E,Object> map; private static final Object PRESENT = new Object(); public HashSet() { map = new HashMap<>(); } public boolean add(E e) { return map.put(e, PRESENT)==null; } public boolean remove(Object o) { return map.remove(o)==PRESENT; } public boolean contains(Object o) { return map.containsKey(o); }