Java 集合
问题1: Collections工具类和Arrays工具类常用方法?
Collections工具:
// 排序,
// 反转
void reverse(List list);
// 随机排序
void shuffle(List list);
// 按自然排序的升序排列
void sort(List list);
// 定制排序,由Comparator控制排序逻辑
void sort(List list, Comparator c);
// 交换两个索引位置的元素
void swap(List list, int i, int j);
// 旋转,distance为正数,将list后distance个元素整体移到前面
void rotate(List list, int distance);
// 查找,替换操作
int binarySearch(List list, Object key); // 对List进行二分查找,返回索引,注意List有序
int max(Collection c); // 根据元素的自然顺序,返回最大的元素;
int max(Collection c, Comparator comp); // 定制排序
void fill(List list, Object obj); // 用指定的元素代替指定list中所有元素
int frequency(Collection c, Object o); // 统计元素出现的次数
int indexOfSubList(List list, List target); // 统计target在list中第一次出现的索引
boolean replaceAll(List list, Object oldVal, Object newVal); // 用新元素替换旧元素
// 同步控制,效率低,需要线程安全的集合考虑JUC包下的并发集合
// HashSet, TreeSet, ArrayList, LinkedList, HashMap, TreeMap都是线程不安全的。Collections提供了多个静态方法将他们包装成线程同步的集合。
Collection.synchronizedCollection(Collections<T> c); // 返回指定c支持同步集合
Collection.synchronizedList(List<T> list); // 返回指定列表支持的同步List
Collection.synchronizedMap(Map<K, V> m); // 返回由指定映射支持的同步Map
Collection.synchronizedSet(Set<T> s); // 返回指定set支持的同步set
Arrays类的常见操作
Arrays.sort(Object[] array); // 排序
Arrays.binarySearch(Object[] array,element); // 查找
Arrays.equals(); // 比较
Arrays.fill(Object[] array,Object object); // 填充
Arrays.asList(); // 转列表
Arrays.toString(Object[] array); // 转字符串
Arrays.copyOf()
问题2:深拷贝与浅拷贝?
浅拷贝:对基本数据进行值传递,对引用数据类型进行引用传递的拷贝;
深拷贝:对基本数据进行值传递,对引用数据类型创建一个新的对象,并赋值其内容。
问题3:说说List、Set、Map三者的区别?
List接口存储一组不唯一、有序的对象;Set不允许重复的集合;Map使用键值对存储,两个Key可以引用相同的对象,但Key不能重复。
问题4: ArrayList与LinkedList区别?
-
都不是同步的,不保证线程安全;
-
ArrayList底层使用的是Object数组,默认容量为10,每次扩充1.5倍;LinkedList底层使用的双向链表;
private int newCapacity(int minCapacity) { // overflow-conscious code int oldCapacity = elementData.length; int newCapacity = oldCapacity + (oldCapacity >> 1); if (newCapacity - minCapacity <= 0) { if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) return Math.max(DEFAULT_CAPACITY, minCapacity); if (minCapacity < 0) // overflow throw new OutOfMemoryError(); return minCapacity; } return (newCapacity - MAX_ARRAY_SIZE <= 0) ? newCapacity : hugeCapacity(minCapacity); }
private static class Node<E> { E item; Node<E> next; Node<E> prev; Node(Node<E> prev, E element, Node<E> next) { this.item = element; this.next = next; this.prev = prev; } }
-
ArrayList采用数组存储,所以插入和删除元素的时间复杂度受位置影响;LinkedList采用链表存储,删除不受元素位置影响,如果指定位置插入删除为
O(n)
。 -
LinkedList不支持高效的随机元素访问,ArrayList支持;实现RandomAccess接口,单纯的标志接口;
-
LinkedList提供了 List 接口中没有定义的方法,专门用于操作表头和表尾元素,可以当作堆栈、队列和双向队列使用;
-
ArrayList的空间浪费体现在list列表的结尾会预留一定的容量空间,而LinkedList的空间花费体现在每个元素都需要消耗更多的空间。
实现了RandomAccess接口,优先选择普通for循环,其次是foreach;
未实现RandomAccess接口的list,优先选择iterator遍历,大size的数据,不使用普通for循环。
问题5:说说ArrayList与Vector的区别?ArrayList的扩容机制?
Vector类的所有方法都是同步的,同步操作上消耗大量时间;ArrayList不是同步的。
ArrayList扩容方法为1.5倍,Vector扩容为每次的2倍:
/**
* ArrayList扩容的核心方法。
*/
private void newCapacity(int minCapacity) {
// oldCapacity为旧容量,newCapacity为新容量
int oldCapacity = elementData.length;
//将oldCapacity 右移一位,其效果相当于oldCapacity /2,
//我们知道位运算的速度远远快于整除运算,整句运算式的结果就是将新容量更新为旧容量的1.5倍,
int newCapacity = oldCapacity + (oldCapacity >> 1);
// ...
}
/**
* Vector扩容的核心方法。
*/
private int newCapacity(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
capacityIncrement : oldCapacity);
// ...
}
ArrayList基于动态数组,并不是所有的空间都被使用。因此使用transient修饰可防止被自动序列化,且ArrayList自定义了序列化和反序列化操作,只序列化被使用的数据。
// ArrayList
transient Object elementData[];
// Vector
protected Object[] elementData;
问题6:说说HashMap、HashSet和HashTable的区别?
-
HashMap、HashSet是非线程安全的,HashTable是线程安全的,HashTable内部的方法经过synchronized修饰;ConcurrentHashMap是线程安全的。
-
HashMap效率比HashTable高,HashTable基本被淘汰了;
-
HashMap中,null可以作为键,只有一个;HashTable不允许;
-
初始化未给定容量:HashTable默认大小为11,每次扩容为
2n+1
;HashMap默认初始化带下为16,每次扩容为2倍;给定容量:HashTable会直接使用给大小,HashMap会扩充为2的幂次;这样是为了位运算的方便,位与运算比算数计算的效率高了很多,之所以选择16,是为了服务将Key映射到index的算法。 -
HashMap在JDK 8之前由数组+链表组成,使用拉链法解决哈希冲突;JDK 8之后使用数组+红黑树(自平衡排序二叉树);HashSet底层基于HashMap实现。
-
迭代器不同,HashMap中的迭代器是
fail-fast
的,当遍历过程中修改HashMap结构会抛出ConcurrentModificationException异常。
问题7:谈谈HashMap的底层实现?
-
JDK 1.8之前:HashMap底层是数组和链表结合,HashMap通过key的hashCode经过扰动函数处理得到hash值,然后通过
(n-1)&hash
判断当前元素存放的位置。如果存在key相同,则直接覆盖;否则通过拉链法解决冲突。 -
JDK 1.8之后:在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间。
-
容量:默认容量是16,必须是2的整数幂(原因:提升计算效率,更快算出元素的位置;减少哈希碰撞,使得元素分布均匀),扩容每次为2倍。
-
尾插法当发生hash冲突时,采用
尾插法
,使用头插法会改变链表上的顺序,在多线程操作时造成死循环。底层数组容量有限,多次插入到一定数量就会进行扩容;扩容时机:超过Capacity * LoadFactor
如何扩容:
1、新建Entry空数组,长度为原数组的2倍
2、遍历原Entry数组,把所有Entry重新计算Hash到新数组。长度扩大之后,hash规则也改变了,不能直接复制。
-
fail-fast原理,和fast-safe区别
如果在使用迭代器的过程中有其他线程修改了map,那么将抛
ConcurrentModificationgException
,这就是所谓 fail-fast 策略。java.util
包下的集合都是快速失败的。迭代器在遍历集合中,使用一个modCount变量。如果在遍历期间内容变化就会改变modCount值。迭代器在使用hashNext()/next()方法之前,都会检查modCount是否等于expectedmodCount;java.util.concurrent
包下的集合是安全失败的,在迭代的时候会去底层集合做一个拷贝,所以你在修改上层集合的时候是不会受影响的,不会抛ConcurrentModification
异常。
问题8:说说ConcurrentHashMap的知识?
-
JDK 1.7 之前,是由 Segment 数组、HashEntry 组成,和 HashMap 一样,仍然是数组加链表。和 HashMap 非常类似,唯一的区别就是其中的核心数据如 value ,以及链表都是
volatile
修饰的,保证了获取时的可见性。ConcurrentHashMap采用了分段锁技术,其中Segment继承于ReentrantLock。
-
JDK 1.8 之后,抛弃了原有的 Segment 分段锁,而采用了
CAS + synchronized
来保证并发安全性。put方法:
- 根据 key 计算出 hashcode 。
- 判断是否需要进行初始化。
f
即为当前 key 定位出的 Node,如果为空表示当前位置可以写入数据,利用 CAS 尝试写入,失败则自旋保证成功。- 如果当前位置的
hashcode == MOVED == -1
,则需要进行扩容。 - 如果都不满足,则利用
synchronized
锁写入数据。 - 如果数量大于
TREEIFY_THRESHOLD
则要转换为红黑树。
问题9:说说LinkedHashMap?
LinkedHashMap 继承自 HashMap,所以它的底层仍然是基于拉链式散列结构即由数组和链表或红黑树组成。另外,LinkedHashMap 在上面结构的基础上,增加了一条双向链表,使得上面的结构可以保持键值对的插入顺序。同时通过对链表进行相应的操作,实现了访问顺序相关逻辑。
问题10:谈谈TreeMap和TreeSet
TreeMap是一个有序的key-value集合,它是通过红黑树实现的。该映射根据其键的自然顺序进行排序,或者根据创建映射时提供的 Comparable 进行排序(实现其compareTo进行排序)。