最近学了关于集合框架的知识,做一个小结,有些地方略有不足或出现错误的地方敬请指正~
Collection
- Collection是java.util包下的一个接口,它存放的是单一值,并且是无序,不唯一的对象。
- 可以存放不同类型的数据,而数组只能存放固定类型的数据。
- 在进行实例化的时候需要用子类进行实现,例如:
Collection collection = new ArrayList();
- 注意:其中在Colllection.add()方法中,要求必须传入的参数是Object对象,因此当写入基本数据类型的时候,包含了自动拆箱和自动装箱的过程。
其他还有一些基础的api操作再次就不赘述了。
List
存储一组不唯一,有序(插入顺序) 的对象。
ArrayList
ArrayList实现了长度可变的数组,在内存中分配连续的存储空间,存放的是的对象。如果了解数据结构的话那么就可以知道ArrayList的优点和缺点分别是:
- 遍历元素和随机访问元素的效率比较高。
- 当添加和删除元素时需要进行大量的数据移动操作,这样使得增删数据时效率会比较低,并且按照内容查询的效率也比较低。
那么在ArrayList当中有一个经常会被问到的点就是它的扩容机制,下面一起来看一下。
ArrayList的扩容机制
一般想到数组的扩容都是new一个新的数组然后复制元素,但如果要涉及到具体细节的话就需要我们来一起分析一下源码,首先我们先来了解一下源码当中的一些基本的成员变量的定义,方便后面的理解
// 表示了ArrayList的默认容量大小是10
private static final int DEFAULT_CAPACITY = 10;
// 当在进行ArrayList初始化时参数传入0,则赋予一个空的数组
private static final Object[] EMPTY_ELEMENTDATA = {};
// 默认初始化时的数组
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
// 存放元素的数组,也代表了ArrayLiat的实际大小
transient Object[] elementData;
// 当前ArrayList的元素个数
private int size;
接下来看一下ArrayList的构造方法,分别为无参构造函数和带参构造函数。
// 无参构造函数
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
// 当参数为一个集合
public ArrayList(Collection<? extends E> c) {
elementData = c.toArray();
if ((size = elementData.length) != 0) {
// 如果是非空集合则进行复制
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
// 传入空集合则初始化为空
this.elementData = 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);
}
}
好,了解完了一些基本的成员变量定义和构造函数后,我们会发现一个问题,那就是在构造函数中并没有出现DEFAULT_CAPACITY 这个参数,也就是说初始化时ArrayList的大小并没有赋值为10,那这个默认的容量大小究竟在哪里出现并且是如何进行扩容的,接下来我们来看一看ArrayList里面的add()方法吧。
public boolean add(E e) {
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) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// 此if判断成立的条件有两个
// 1.当第一次插入元素时初始化数组大小为10
// 2.当minCapacity(当前数组容量)超出默认容量大小(10)
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
// 进行扩容时是扩充为原来数组大小的1.5倍
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
// 当插入第一个元素时,MinCapacity会传入DEFAULT_CAPACITY
// 的值,即为10,在这一布,完成了默认容量大小的初始化
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
// 数组容量最大即为Integer的最大值
newCapacity = hugeCapacity(minCapacity);
// 将数据复制到扩容后的数组当中
elementData = Arrays.copyOf(elementData, newCapacity);
}
在上述代码的关键部分给予了注释,很直观的体现了ArrayList是怎样初始化默认大小为10还有怎样进行扩容和数据的转移,以上部分ArrarList的扩容机制就介绍完了,也不是很难,细心过一遍的话还是挺直观的,面试有可能会被问到。
fast-fail
快速失败是面试中经常会被问到的一个问题,那么它是什么呢,我们首先来介绍一下Iterator迭代器。
-
所有的集合类都默认实现了Iterable的接口,实现此接口意味着具备了增强for循环的能力,也就是for-each
-
增强for循环本质上使用的也是iterator的功能,iterator也是一个接口
-
在iterator的方法中,要求返回一个Iterator的接口子类(定义的集合)实例对象(内部类),此接口中包含了hasNext()和next()两个常用方法。
-
每次在使用此接口(list.iterator())时,就会返回一个iterator对象,并且具有指针可以实现上述两个功能(变成了迭代器)
// 如何利用Iterator进行遍历
ArrayList<Integer> list = new ArrayList<>();
Iterator iterator = list.iterator();
while(iterator.hasNext()){
System.out.println(iterator.next());
}
- iterator对象中含有两个指针(cursor、lastRet)。
public boolean hasNext() {
return cursor != size;
}
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];
}
可以看出如果在进行一个集合的遍历时,cursor和lastRet两个指针是同步进行更新的。
关键来了: 上述的遍历操作只是进行了简单的打印输出,如果我们要在遍历的过程中对集合中的元素进行添加或删除操作呢,例如:
ListIterator iterator = list.listIterator();
while(iterator.hasNext()){
Object p = iterator.next();
if(p.equals(3)){
list.remove(p);
}
System.out.println(p);
}
可以发现,当我们在利用迭代器进行遍历过程中根据判断条件然后利用集合list进行删除时,会报一个错为:java.util.ConcurrentModificationException(并发修改异常),这是为什么呢?
- 当遍历到指定元素进行删除的时候,lastRet指向的元素会被删除,后边的元素会向前移动,cursor的指针指向的数据就不是原来的数据,发生指针错乱,两个对象同时操作一个集合,所以会报并发修改异常错误。
解决方法:利用iterator.remove()进行删除,原因:
- 在每次进行删除时,将cursor指针指向lastRet的位置,lastRet指向-1,并在hasNext判断为True后的next操作里再次更新至cursor指针前面
- 但必须使用list.ListIterator()的方法,该接口可以向前/后遍历,向前遍历时需要进行向后遍历的操作。
上述的错误是通过下列方法进行实现的:
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
其中,modCount记录了集合的修改次数,对集合内容的修改都将增加这个值,那么在迭代器初始化过程中会将这个值赋给迭代器的expectedModCount。在迭代过程中,判断 modCount 跟expectedModCount 是否相等,如果不相等就表示已经有其他线程修改了 集合,并抛出ConcurrentModificationException异常,这就是fast-fail策略。
Iterator与for-each
For-each循环
- 增强的for循环,遍历array或Collection的时候相当简便。
- 无需获得集合和数组的长度,无需使用索引访问元素,无需循环条件。
- 遍历集合时底层调用Iterator完成操作。
For-each缺陷
- 数组:
-
- 不能方便的访问下标值。
-
- 不要在for-each中尝试对变量赋值,只是一个临时变量。
- 集合:
-
- 与使用Iterator相比,不能方便 的删除集合中的内容。
For-each总结:除了简单的遍历并读出其中的内容外,不建议使用增强for。
Vector
- Vector也是List接口的一个子类实现
- Vector跟ArrayList一样,底层都是使用数组进行实现的
- ArrayList是线程不安全的,效率高,Vector是线程安全的效率低
- ArrayList在进行扩容的时候,是扩容1.5倍,Vector扩容的时候是原来的两倍。
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
// 注意这个地方与ArrayList不同的是这里是两倍的扩容
int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
capacityIncrement : oldCapacity);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
elementData = Arrays.copyOf(elementData, newCapacity);
}
LinkedList
LinkedList采用链表的方式进行存储,其底层是利用双向链表实现的,存放的是的对象,同样如果了解数据结构的话就会发现它与ArrayList就能形成一个很好的优缺点对比:
- 插入、删除元素时效率比较高(不用进行元素的移动操作)
- 遍历和随机访问元素的效率低(不能对随机下标进行直接访问,需要从头进行遍历)
Set
Set接口存放的是一组无序,唯一的对象。常用的子类有HashSet与TreeSet
HashSet
采用了Hashtable哈希表存储结构,底层为HashMap,其特点为:
- 优点:添加速度快,查询速度快,删除速度快。
- 缺点:无序。
- LinkedHashSet:采用哈希表存储结构,并采用链表维护次序
-
- 有序(添加顺序)。
注意:在计算hashCode时为什么选择31作为乘子?
- 31N可以被编译器优化为左移5位后减1即31N = (N<<5)-1,有较高的性能。使用31的原因可能是为了更好的分配hash地址,并且31只占用5bits!
那么HashSet如何保证元素的唯一性呢?
- 通过元素的两个方法,hashCode和equals方法来完成
- 如果元素的HashCode值相同,才会判断equals是否为true
- 如果元素的hashCode值丌同,丌会调用equals方法
TreeSet
采用二叉树(红黑树的存储结构)
- 优点:有序(排序后的升序),查询速度比List快(因为是树结构)
- 缺点:查询速度没有HashSet快(直接计算Hash值)