目录
***:Java集合的遍历以及快速失败机制fail-fast
***:hashtable和concurrenthashmap对比?
前言
带着问题学java系列博文之java基础篇。从问题出发,学习java知识。
Java的容器体系
在开始之前,我先讲一个争议点以及为什么我这里标题是“容器”体系。首先,关于map到底算不算集合,普遍存在争议点,有的人说map属于集合,有的人说map是键值对结构,不属于集合。咱们请出权威,《java编程思想》中第11章有写到list、set、queue和map都属于集合类;《java核心技术 卷一》第九版13.3节中有写到“集合有两个接口:Collection和Map”。我们之所以有争议,我觉得主要还是因为java对list、set和queue有一个公共的父接口Collection,而map是单独的Map接口。为了更好地理解,去除争议,我们不妨把它们都叫做容器,它们都是存储对象的容器。因此,我这里标题也是容器体系。以后遇到别人较真map的归属问题,可以淡定的说map属于容器--集合框架,不继承Collection接口。
1.Java集合框架图
2.Collection类型
- set接口具体实现类:HashSet、LinkedHashSet、TreeSet、AbstractSet、CopyOnWriteArraySet、EnumSet、JobStateReasons
- List接口具体实现类:ArrayList、LinkedList、Vector、AbstractList、CopyOnWriteArrayList、Stack、AttributeList、RoleList
- Queue接口具体实现类:LinkedList、ArrayBlockingQueue、LinkedBlockingQueue、DelayQueue、PriorityQueue、ArrayDeque、ConcurrentLinkedQueue、ConcurrentLinkedDeque、LinkedBlockingDeque、AbstractQueue
***:list和set的区别?
list:有序、元素可以重复、可以使用简单的for循环进行遍历(因为每个元素有下标);
set:无序、元素不可重复、不可以使用简单的for循环进行遍历(元素没有下标);
***:ArrayList和LinkedList对比?
都实现list接口,具备list特征。
ArrayList:底层实现是数组,增删慢(对比LinkedList,如果仅仅是列表的头尾增删元素,二者效率相同)、查询快;
LinkedList:底层实现是链表,增删快、查询慢;
***:ArrayList如何扩容?
ArrayList底层依赖数组实现,初始化的时候如果不指定大小,则默认创建一个长度为0的数组,当调用add方法向其中添加元素时:如果数组长度为0,则创建一个长度为10的数组,并添加元素;如果数组满了,则触发扩容,新建一个长度是原来数组长度1.5倍的数组,将原来数组的元素完整复制到新数组,然后添加元素。
***:Vector如何扩容?
和ArrayList类似,Vector底层也是依赖数组实现,它的扩容和ArrayList也一样,只是当数组满时,新建的数组长度是原来的2倍。
***:ArrayList和Vector对比?
1.两个都是基于数组实现的;
2.ArrayList是非线程安全的,效率更高;Vector是线程安全的,允许多线程并发增删操作,效率较低;
3.ArrayList的扩容机制是每次扩大0.5倍;Vector的扩容机制是每次扩大1倍。
***:HashSet实现原理?
HashSet底层依赖hashmap实现,存储时hashset的值作为hashmap的key,而hashmap中key对应的value是一个默认的常量PRESENT(空对象),其他细节可以参考hashmap的实现原理。
***:Stack实现原理?
从集合框架图可以看到,stack是继承了vector,而vector又继承了abstractlist、list、collection接口。所以stack底层是一个collection集合、只能从某一端插入和删除的线性表,从而具备了栈特有的属性“先进后出”.
***:CopyOnWriteArrayList是什么?
CopyOnWriteArrayList(免锁容器),底层依赖ArrayList实现,区别于ArrayList是当多个迭代器同时遍历和修改这个列表时,不会抛出 ConcurrentModificationException。CopyOnWriteArrayList 使用乐观锁机制(乐观锁、悲观锁详见《Java基础篇--多线程》),写入操作将导致创建整个底层数组的副本,而源数组将保留在原地,使得复制的数组在被修改时,读取操作可以安全地执行。适合读多写少的场景,且实时性不佳,写操作会消耗大量内存,性能不佳。
***:结果判断题1(for循环遍历)
public class Test {
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
for (int i = 0; i < 5; i++) {
list.add(i+"");
}
for (int i = 0; i < 5; i++) {
if (list.get(i).equals("3")){
System.out.println(list.remove(i));
}
}
System.out.println(list.size());
}
}
上面代码执行结果是?
先输出一个3,然后抛出IndexOutOfBoundsException异常。
public class Test {
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
for (int i = 0; i < 5; i++) {
list.add(i+"");
}
for (int i = 0; i < 5; i++) {
if (list.get(i).equals("3")){
System.out.println(list.remove(i));
break;
}
}
System.out.println(list.size());
}
}
上面代码执行结果是?
先输出一个3,然后再输出一个4。
为什么第一个会抛出数组越界异常呢?
通过对比两个代码的区别,我们可以看到,第一段代码for循环要执行5遍,当执行到第5遍的时候,抛出一个数组越界异常。这是因为我们代码在第4遍遍历的时候做了一个操作“list.remove()”。我们看下源码:
public E remove(int index) {
rangeCheck(index);
modCount++;
E oldValue = elementData(index);
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // clear to let GC do its work
return oldValue;
}
从源码可以看到,执行一次remove将会导致size自减(“--size”)。再看下get()方法的源码:
public E get(int index) {
rangeCheck(index);
return elementData(index);
}
private void rangeCheck(int index) {
if (index >= size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
从源码可以看到,第一段代码之所以抛出异常,就是因为当遍历第5遍的时候,index是4,而size已经从5自减成了4(remove操作导致的)。所以rangeCheck方法检查到数组越界,抛出IndexOutOfBoundsException异常。
***:结果判断题2(forEach遍历)
public class Test {
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
for (int i = 0; i < 5; i++) {
list.add(i+"");
}
for (String s : list) {
if (s.equals("2")) System.out.println(list.remove(s));
}
}
}
上面代码执行结果是?
先输出一个true,然后抛出ConcurrentModificationException异常。
public class Test {
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
for (int i = 0; i < 5; i++) {
list.add(i+"");
}
for (String s : list) {
if (s.equals("3")) System.out.println(list.remove(s));
}
}
}
上面代码执行结果是?
输出一个true。
两次代码完全相同,仅仅是更换了一个值,为什么一个抛出异常,一个不抛出异常呢?
首先我们要清楚,foreach循环其实就是iterator遍历,等价于这段代码:
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()){
String s = iterator.next();
……
第一段代码foreach循环执行到第三遍的时候,list执行了remove操作。我们看下源码:
public boolean remove(Object o) {
if (o == null) {
for (int index = 0; index < size; index++)
if (elementData[index] == null) {
fastRemove(index);
return true;
}
} else {
for (int index = 0; index < size; index++)
if (o.equals(elementData[index])) {
fastRemove(index);
return true;
}
}
return false;
}
private void fastRemove(int index) {
modCount++;
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // clear to let GC do its work
}
可以看到,又是size自减了,因此第一段代码继续执行第四遍循环,iterator.hasnext返回的是true(集合中有4个元素),进入到循环体,执行iterator的next方法,此时触发了java集合的快速失败机制fail-fast,抛出ConcurrentModificationException异常;而第二段代码是在第四遍循环时执行了remove操作,当第五遍循环时,iterator.hasnext返回false(集合中有4个元素,此时的cursor也到了下标4),退出while循环,所以第二段代码可以正常执行完毕。
***:Java集合的遍历以及快速失败机制fail-fast
set集合由于存储时无序、无下标,因此对set集合的遍历只能是foreach或者iterator;
list集合存储时有序、有下标,因此可以使用简单的for循环进行遍历,也可以使用foreach或者iterator遍历。
从上面的结果判断题引出了一个“快速失败机制fail-fast”,那它是什么呢?
它是java集合的一种错误检测机制,当线程对集合进行结构上的改变时,就有可能产生fail-fast。迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个 modCount 变量。集合在被遍历期间如果内容发生变化,就会改变modCount的值。每当迭代器使用hashNext()/next()遍历下一个元素之前,都会检测modCount变量是否为expectedmodCount值,是的话就返回遍历;否则抛出异常,终止遍历。还是看下源码最清晰:
//ArrayList的modCount变量
protected transient int modCount = 0;
//ArrayList的fastremove方法
private void fastRemove(int index) {
modCount++;
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,numMoved);
elementData[--size] = null; // clear to let GC do its work
}
//Arraylist的内部类Itr
private class Itr implements Iterator<E> {
int cursor; // index of next element to return
int lastRet = -1; // index of last element returned; -1 if no such
int expectedModCount = modCount;
Itr() {}
public boolean hasNext() {
return cursor != size;
}
@SuppressWarnings("unchecked")
public E next() {
checkForComodification();
int i = cursor;
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i + 1;
return (E) elementData[lastRet = i];
}
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
ArrayList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
@Override
@SuppressWarnings("unchecked")
public void forEachRemaining(Consumer<? super E> consumer) {
Objects.requireNonNull(consumer);
final int size = ArrayList.this.size;
int i = cursor;
if (i >= size) {
return;
}
final Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length) {
throw new ConcurrentModificationException();
}
while (i != size && modCount == expectedModCount) {
consumer.accept((E) elementData[i++]);
}
// update once at end of iteration to reduce heap write traffic
cursor = i;
lastRet = i - 1;
checkForComodification();
}
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
从源码可以看到,ArrayList定义了一个int型成员变量modCount,每次执行迭代器的next方法时,都会先执行checkForComodification方法,判断一下modCount和expectedModCount是否一致,不一致则抛出ConcurrentModificationException异常。而ArrayList自身的remove(Obejct o)方法的执行会改变modCount(“modCount++”),但是此时内部类Itr的成员变量expectedModCount并没有跟着一起改变,所以上面的结果判断题2才会抛出异常。因此,当我们需要在遍历集合的同时对集合进行结构上的改变,就要用iterator迭代器来实现,通过iterator的hasnext和next进行遍历,iterator的remove()来实现结构上的改变;而不能简单的使用for循环或者foreach循环,调用集合自身的remove方法。
***:队列的元素存取方法区别?
首先要区分非阻塞队列和阻塞队列:
非阻塞队列(Queue):add、remove、offer、poll、element(特有)、peek(特有)
阻塞队列(BlockingQueue):add、remove、offer(E)、offer(E,long,TimeUnit)(特有)、poll(long,TimeUnit)(特有)、put(特有)、take(特有)
上面标有“特有”的,表示仅该类队列特有方法。
方法解释:
element():非阻塞队列特有方法,返回队列头部元素,如果队列为空,则抛出NoSuchElementException异常;
peek():非阻塞队列特有方法,返回队列头部元素,如果队列为空,则返回null;
抛出异常 | 特殊值 | 阻塞 | 超时 | |
插入元素 | add(e) | offer(e) | put(e) | offer(e,time,timeUnit) |
移除元素 | remove() | poll() | take() | poll(time,timeUnit) |
add(e)和remove()是集合基本方法,当队列满时,add(e)方法向队列中添加元素失败,抛出IIIegaISlabEepeplian异常;当队列为空时,remove()方法从队列头部移除并返回元素失败,抛出NoSuchElementException异常;
offer(e)和poll()是队列基本方法,当队列满时,offer(e)方法向队列中添加元素失败,返回false;当队列为空时,poll()方法从队列头部移除并返回元素失败,返回null;
put(e)和take()是阻塞队列特有方法,当队列满时,put(e)方法向队列中添加元素被阻塞,一直等待队列中有空间,则存入;当队列为空时,take()方法从头部移除并返回元素被阻塞,一直等待队列中有元素,则移除。
offer(e,time,timeUnit)和poll(time,timeUnit)是阻塞队列特有方法,当队列满时,offer方法向队列中添加元素被阻塞,等待参数time和timeUnit设定的时长,超时不再等待;当队列为空时,poll方法从队列头部移除并返回元素被阻塞,等待参数time和timeUnit设定的时长,超时直接返回null。
3.Map类型
map接口继承数:
map类型区别于Collection类型,map是key-value键值对形式,key作为元素在容器中的定位,容器中完整存储key-value的Map.Entry。
***:HashMap的数据结构和数据存储过程?
Java8更新后的HashMap数据结构是:数组+链表,当链表中存储的元素超过一定长度(默认8)时,不再使用链表,扩容为红黑树。
hashmap数据存储过程:(hashmap数据结构是数组+链表,我们可以形象的理解为数组的每一个节点是一个桶,桶中可以存放key-value的Map.Entry)
首先根据key的hash值进行运算(hash & (数组length -1)),找到该key在数组中的索引index;然后分为三种情况:
- 如果此时该索引对应的桶没有任何数据,则生成一个节点的链表(相当于初始化桶),节点中保存Map.Entry<K,V>,包含key和value。
- 如果该索引对应的桶是一个链表,并且已存有数据,则根据key的hash值遍历链表,查找是否有相同key的节点。如果找到,则覆盖该节点,并返回节点中存储的值;没找到,则在链表尾部插入一个新节点,存储数据(涉及扩容)。
- 如果该索引对应的桶是一个红黑树(当一个桶中链表的节点数超过8(jdk默认)时,则进行扩容,改为红黑树存储),此时遍历方式有所变化,不过也是和链表一样,根据hash值找重复节点,有则覆盖节点,并返回覆盖前的value值,没有重复节点,则生成叶子节点存储数据;
***:HashMap的扩容机制?
默认初始化时,hashmap的桶数组初始容量为2^4(16);当数据过多时,以2倍扩容桶数组;注意当桶扩容了,需要对存储数据的链表或者红黑树进行重排。
***:hashmap和hashtable的区别?
- hashtable继承Dictionary类,是java早期的接口;hashmap继承AbstractMap类,是新版jdk提供的接口;二者都实现了map接口;
- hashtable不允许null键和null值,hashmap允许null键和null值;
- hashtable是线程安全的,存取数据效率较低;hashmap是非线程安全的,存取数据效率较高。
***:hashtable和concurrenthashmap对比?
hashtable底层依赖hashmap实现,它的线程同步依赖于synchronize关键字,内部对存取方法都加上了synchronize。因此它的存取方法会对整个容器进行加锁,导致效率低下;
concurrenthashmap采用了桶的概念,仅对同一个hash值下的链表或者红黑树进行同步,当多线程并发操作时,仅当涉及同一个桶中的数据操作,才会有加锁同步;不同hash值下的数据操作互不影响,也不需要锁等额外的同步消耗,所以它的效率较高,性能很好。当需要线程安全的map容器时,推荐使用concurrenthashmap,不建议使用hashtable。
4.Collections和Arrays工具类
Collections和Arrays是jdk给我们提供的两个静态工具类,里面有很多封装好的操作集合和数组的方法。
***:Collections常用方法
addAll(Collection<? super T> c, T... elements ):将所有元素添加到指定集合;
copy(List<? super T> dest, List<? extends T> src):将src列表中的所有元素复制到dest列表;
fill(List<? super T> list, T obj):用obj元素替换list中的所有元素;
list(Enumeration<T> e):将Enumeration中所有的元素按照枚举的顺序,添加到ArrayList,并返回该列表;
max(Collection<? extends T> coll):返回集合中最大的元素(不指定comparator,则调用对象的compare方法);
min(Collection<? extends T> coll):返回集合中最小的元素(不指定comparator,则调用对象的compare方法);
replaceAll(List<T> list, T oldVal, T newVal):将列表中所有的oldVal元素替换成newVal;
reverse(List<?> list):反转列表的元素顺序;
swap(List<?> list, int i, int j):交换列表指定下标的元素;
sort(List<T> list):对列表进行排序(不指定comparator,则调用对象的compare方法);
***:Arrays常用方法
asList(T... a):将a中的所有元素添加到ArrayList中,并返回该列表;
copyOf(T[] original, int newLength):复制数组original中的元素填充到新数组,复制newLength个,如果original数组中不存在这么多,则用null或者0填充,并返回新数组;
copyOfRange(T[] original, int from, int to):复制数组original中取值在from和to之间的元素填充到新数组,并返回新数组;
equals(Object[] a, Object[] a2):比较两个数组,仅当两个数组元素彼此完全相同时(顺序一致,值一致),返回true;
fill(Object[] a, Object val):用val替换数组a中的所有元素;
sort(Object[] a):对数组进行排序;
sort(Object[] a, int fromIndex, int toIndex):对数组指定下标范围的元素进行升序排序;
***:Collections和Arrays的排序算法
Collections.sort():LegacyMergeSort.userRequested 为true使用归并排序(将来有可能移除归并排序);不为true使用TimeSort算法排序(Timesort是结合归并和插入排序算法得到的排序算法);
Arrays.sort():数组长度length小于47,使用插入排序;47<=length<286,使用双轴快速排序;286<=length,且连续性较好,使用归并排序;连续性不好,使用双轴快速排序。
以上系个人理解,如果存在错误,欢迎大家指正。原创不易,转载请注明出处!