Java面试题-Collection框架
1,ArrayList中基础数据域Object[] elementData使用transient修饰的,那它是怎么序列化的?为什么?
答案:ArrayList在被序列化为文件、网络io等时,会调用被序列化类中的writeObject方法,找不到writeObject则调用工具类中defaultWriteFields方法,在ArrayList中writeObject私有的,序列化时使用反射调用。之所以不使用类中属性elementData直接来序列化,主要原因是elementData是一个缓存数组,为了性能考虑,它通常会预留一些容量,因此可能有大量空间没有实际存储元素。不直接序列化elementData可以保证序列化实际有值的那些元素,而不是序列化整个数组。
2,ArrayList的扩容机制是什么?
答案:ArrayList在创建的时候,如果不指定初始容量,默认容量minCapacity为0,在add方法中,首先调用ensureInternalCapacity(如果minCapacity小于10,设置为10),然后调用ensureExplicitCapacity(如果现需容量minCapacity大于elementData容量,则扩容),grow的逻辑很简单。首先找出当前容量,把新容量设置为旧容量的1.5倍(通过位运算左移实现),如果扩容后容量newCapacity比minCapacity还小,则设置newCapacity=minCapacity,如果newCapacity比极限容量(Integer.MAX_VALUE-8)要大,则设置新容量=Integer.MAX_VALUE,接着使用该新容量初始化一个新的数组,使用Arrays.copyOf(elementData, newCapacity)将原有elementData中的元素等位复制为一个新数组。
引申:
在ArrayList中使用了大量的Arrays.copyOf和System.arrayCopy方法,它们的主要区别为前者默认创建一个新数组,而后者需要指定目标数组。
1 //elementData:源数组;index:源数组中的起始位置;elementData:目标数组;index + 1:目标数组中的起始位置; size - index:要复制的数组元素的数量;
2 System.arraycopy(elementData, index, elementData, index + 1, size - index);
3 //elementData:要复制的数组;size:要复制的长度
4 Arrays.copyOf(elementData, size);
ArrayList使用的优化:
- 在new ArrayList时指定初始化容量,以减少扩容次数
- 在add大量元素之前手动调用ensureCapacity方法,以减少增量重新分配次数
3,LinkedList和ArrayList有什么区别?
答案:LinkedList底层是双向链表结构,属于顺序访问序列,ArrayList底层结构是数组,属于随机访问序列。LinkedList增删效率高,ArrayList改查效率高。存储同样大小的数据,LinkedList需要更多的内存。
引申:
LinkedList继承AbstractSequentialList,提供了顺序访问的存储结构。还实现了Deque双向队列接口,因为Queue特性是FIFO,而Deque则可以同时在头尾处完成读写操作,而LinkedList还可以操作头尾之间的任意节点。
4,介绍一下Vector和Stack
答案:Vector的实现和ArrayList基本一致,vector的所有public方法都是用了synchronized关键字,保证线程安全。Vector根据capacityIncrement的数值扩容(new时指定),而不是每次扩容1.5倍。Stack时Vector的子类,提供了一些栈操作的方法。
引申:
Vector和Stack使用了大量synchronized关键字来保证线程安全,但这并不是当下推荐的方式,因为这种实现效率比较低,在java.util.Collections工具类中提供了synchronizedList方法可以提供线程安全的列表,拥有更好的性能,因此这两个类被看作已经过时的容器。
5,谈谈对Hash函数的认识以及JDK1.8中hash算法的实现。
Hash函数要尽可能保证哈希松散,所谓哈希松散,就是指数值尽可能平均分布的hashCode,在Java语言中,一般会认为hashCode是一个int值,int是一个32bit整型数,比如一个8位16进制数1A54F2C0就比C8600000要松散。HashMap的默认容量是16,hashCode如果超过length-1,就会执行取余运算。如果一组数据其hashCode集中在000A0000-FFFF0000之间,那么计算indexFor时其取余结果全部为0。这种hashCode重复的现象称为哈希碰撞,当哈希碰撞发生,碰撞的键值对会被存入到同一条链表中,导致HashMap效率低下。松散哈西可以减少哈希碰撞。JDK1.8之前利用大量异或操作与无符号右移操作构建特定哈希松散算法hash(Object k),JDK1.8后,简化了哈希松散算法,之前版本多次位移异或并不能避免过多哈希碰撞,反倒增加了计算次数,Hashmap的效率问题主要集中在链表部分的遍历,这是一个经验性质的改进。
1 //JDK1.8的hash算法
2 static final int hash(Object key) {
3 int h;
4 return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
5 }
6,jdk1.8之前和之后的HashMap底层数据结构有什么不同?他们put的流程各是什么?
答案:在jdk1.8以前,HashMap的底层实现是数组和链表,而从1.8开始使用数组+链表+红黑树来实现。
Jkd1.8以前的put流程:
a) 计算键key的hash值。
b) 根据hash值和table长度来确定下标。(利用hashCode与table的长度使用&运算构建索引算法indexFor(int,int),检测哈希冲突,确定数据地址),根据key值和hash值对比,确定是存入数组,还是创建链表节点还是替代之前的链表值。
c) 通过size>=threshold,确定是否需要扩容resize(2*table.length),确定下一个扩容阈值size*loadFactor(加载因子,常量0.75),确定useAltHashing改变算法标志,决定是否需要rehash重构哈希,使用新容量构建新的Entry<K,V>table数组,调用transfer(newTable,rehash)来重新计算所有节点并转移到新数组table下。
Jdk1.8以后的put流程:
a) 获取key值的hashCode。
b) 调用putVal方法进行存值。该方法主要流程为:使用table的长度-1&hash来计算下标,table为空或超过阈值进行扩容,根据下标位置的节点类型保存数据,决定插入数组还是链节点或树节点,如果链表深度超过建树阈值(TREEIFY_THRESHOLD-1),即7,则调用treeifyBin把整个链表重构成树。
c) Resize即重新规划table长度和阈值,如果数量超过阈值,扩容两倍(最小16,最大1<<30),阈值为容量*加载因子0.75。重新排列数据节点,遍历所以节点普通节点重新计算哈希值,树节点调用split方法,决定是否需要将其退化为链表。
对Hash函数的认识:
引申:
HashMap使用的优化:
- 在new HashMap时指定容量,合适的initCapacity=(需要存储的元素个数/加载因子)+1
7,TreeMap和HashMap有什么区别?
答案:TreeMap是有序Map,需要指定比较器或使用默认比较器。与HashMap组合了数组、链表、红黑树不同,TreeMap时完全有红黑树实现的。
引申:
TreeMap的Put方法:
a) 如果TreeMap是空的,那么指定数据作为根节点。
b) 如果comparator不为空,使用comparator决定插入位置,否则认为key实现了Comparable,调用compareTo决定插入位置。
c) 插入完成,修复红黑树。
8,LinkedHashMap和HashMap有什么区别?
答案:LinkedHashMap的存储中包含了一个额外的双向链表结构,private transient Entry<K,V>header既是头又是尾(JDK1.8后有单独的tail),可以看成是一个环状链表,Hash桶中的每个节点都被这个环状链表的某个节点引用,从而达到有序访问目的。
9,HashTable与HashMap有什么区别?
答案:HashTable的实现方式被synchronized修饰,因此HashTable是线程安全的,而HashMap不是。此外HashTable不能存放null作为key,HashMap会把null key存在下标为0的位置。
引申:
虽然Hashtable是线程安全的,但在多线程环境下并不推荐使用,因为采用synchronized方式实现的多线程安全容器在大量并发的情况下效率比较低,Java还引入了专门在大并发量情况下使用的并发容器,这种容器由于在实现时使用了更加细粒度的锁,由此在大并发量的情况下有着更好的性能。
10,什么是Java中Collectors的fast-fail特性?
答案:Java集合框架Iterator具有fast-fail的特性,在创建Iterator后,其它线程对Collection做了修改或当前线程调用非Iterator接口对集合做了修改,导致类似modCount!=expectedModCount,那么会抛出ConcurrentModificationException异常,产生fast-fail事件。
引申:
针对ArrayList,fast-fail的实现是在其父类AbastractList中,当执行add、remove、clear等方法时,modCount会发生改变,expectedodCount不变,调用checkForModification检测是否modCount==expectedModCount。而JUC中的CopyOnWriteArrayList没有继承AbstractList,仅仅实现了List接口,进行增删改时没有所谓checkForModification。