Java集合框架学习笔记
1. Java集合框架中各接口或子类的继承以及实现关系图:
2. 数组和集合类的区别整理:
数组:
1. 长度是固定的
2. 既可以存放基本数据类型又可以存放引用数据类型
3. 存放进数组的必须是相同类型的数据
VS
集合类:
1. 长度是可变的
2. 只能存放对象的引用
3. 存放进集合的可以是不同的数据类型
3. 集合类常用API源码分析
在之后的大数据学习中,灵活运用各种各样的数据结构可以说是一项基本技能了,因此,了解各种数据结构的底层源码将有助于用户更好地使用各种开源框架,以下将以ArrayList为例,详细地解读源码,其他各种数据结构以后也会陆续更新:
3.1 文档解读
那么首先,我们先摘录一段文档,从整体上把控一下ArrayList类的概况:
* Resizable-array implementation of the List interface. Implements
* all optional list operations, and permits all elements, including
* null. In addition to implementing the List interface,
* this class provides methods to manipulate the size of the array that is
* used internally to store the list. (This class is roughly equivalent to
* Vector, except that it is unsynchronized.)
(1) 这段话首先点明了ArrayList类是实现自List接口的可调整大小的数组,说明它的底层仍然是使用数组实现的,它实现了一切可选的有关list的操作,并且允许任何类型的元素进入该集合,包括null
(2) 除了实现List接口外,此类还提供了方法能够内部地操作数组的长度来存储list
(3) 此类与Vector基本一致,区别只是Vector类是线程安全的,而ArrayList不是
*The size, isEmpty, get, set,
* iterator, and listIterator operations run in constant
* time. The add operation runs in amortized constant time,
* that is, adding n elements requires O(n) time. All of the other operations
* run in linear time (roughly speaking). The constant factor is low compared
* to that for the LinkedList implementation.
(1) 这段话主要列举了一些方法的时间复杂度,首先是size,isEmpty,get,set,iterator和ListIterator的方法是常数时间的复杂度O(1)
(2) add方法的复杂度是“amortized constant time”,分段式的常数时间,意思就是说add方法的复杂度是需要分类讨论的,如果是add一个元素,那么时间复杂度是O(1),而如果是"adding n elements",时间复杂度就变成了O(n)
(3) 除上述两种情形,其他所有的操作都是线性时间的复杂度,而常数因子对于LinkedList的实现来说要低一些
*Each ArrayList instance has a capacity. The capacity is
* the size of the array used to store the elements in the list. It is always
* at least as large as the list size. As elements are added to an ArrayList,
* its capacity grows automatically. The details of the growth policy are not
* specified beyond the fact that adding an element has constant amortized
* time cost.
(1) 每一个ArrayList类的实例对象都有一个“容量”,容量的意思是用来在list中存放元素的数组的长度,而这个长度至少和list的长度一样大
(2) 当元素被添加到一个ArrayList的对象时,它的容量也会自动增长,然而,尽管之前提到增添元素的时间复杂度是分段式的常数时间,增长策略的细节是并不明确的
*An application can increase the capacity of an ArrayList instance
* before adding a large number of elements using the ensureCapacity
* operation. This may reduce the amount of incremental reallocation.
(1) 这段话提到了一个API,ensureCapacity方法,在把大量元素添加到ArrayList中去之前,使用这个API可以提高实例对象的容量
(2) 这种方法能够降低在增添元素时重新分配空间所产生的开销
*Note that this implementation is not synchronized.
* If multiple threads access an ArrayList instance concurrently,
* and at least one of the threads modifies the list structurally, it
* must be synchronized externally. (A structural modification is
* any operation that adds or deletes one or more elements, or explicitly
* resizes the backing array; merely setting the value of an element is not
* a structural modification.) This is typically accomplished by
* synchronizing on some object that naturally encapsulates the list.
* If no such object exists, the list should be "wrapped" using the
* {@link Collections#synchronizedList Collections.synchronizedList}
* method. This is best done at creation time, to prevent accidental
* unsynchronized access to the list:
* List list = Collections.synchronizedList(new ArrayList(...));
(1) 必须要注意的是ArrayList这一实现子类并非是线程安全的(Vector类线程安全),如果有多个线程并发地进入到一个ArrayList实例对象中去并且至少有一个线程结构上修改了这一实例对象,那么就必须在外部进行同步!!!
(2) 何为结构上改变了一个数据结构:仅仅是将这个集合中的某一个元素的值进行设置不能称之为结构化地改变一个集合,必须要添加或删除一个或多个元素,换言之使得这个集合的长度发生了改变才能叫做结构化地改变一个集合
(3) 文档中还推荐如果涉及到了多线程的场景,最好在创建对象的时候就使用同步的集合类,可以调用Collections工具类的静态方法实现,给出的例子是:List list = Collections.synchronizedList(new ArrayList(...)) ;
*
* The iterators returned by this class's {@link #iterator() iterator} and
* {@link #listIterator(int) listIterator} methods are fail-fast:
* if the list is structurally modified at any time after the iterator is
* created, in any way except through the iterator's own
* {@link ListIterator#remove() remove} or
* {@link ListIterator#add(Object) add} methods, the iterator will throw a
* {@link ConcurrentModificationException}. Thus, in the face of
* concurrent modification, the iterator fails quickly and cleanly, rather
* than risking arbitrary, non-deterministic behavior at an undetermined
* time in the future.
(1) 这段话提到了两个迭代器,Iterator和ListIterator,这两种迭代器都是“fail-fast”的
(2) 那么何为"fail-fast"呢?文档中又提到了一个叫做ConcurrentModificationException即“并发修改异常”的异常,当一个集合的迭代器对象被创建出来之后,当集合使用了它本身的方法进行了结构上的改变,比如,add,remove方法而没有使用迭代器的方法时,就会抛出这个异常;而迭代器的这种行为是"fail-fast"的,因为一旦遇到并发修改,迭代器将不会采取任何武断的,不明确的行为,而是“快速地”在采取下一步行动之前就抛出这个异常
3.2 API解读
首先我们看一下ArrayList类的成员变量以及之前提到过的那个ensureCapacity方法:
3.2.1 成员变量
/** * Default initial capacity. */ private static final int DEFAULT_CAPACITY = 10; /** * Shared empty array instance used for empty instances. */ private static final Object[] EMPTY_ELEMENTDATA = {}; /** * Shared empty array instance used for default sized empty instances. We * distinguish this from EMPTY_ELEMENTDATA to know how much to inflate when * first element is added. */ private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; /** * The array buffer into which the elements of the ArrayList are stored. * The capacity of the ArrayList is the length of this array buffer. Any * empty ArrayList with elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA * will be expanded to DEFAULT_CAPACITY when the first element is added. */ transient Object[] elementData; // non-private to simplify nested class access /** * The size of the ArrayList (the number of elements it contains). * * @serial */ private int size;
可以看到,ArrayList默认的容量是10个元素,并且准备了两个空的Object类型的数组,EMPTY_ELEMENTDATA以及DEFAULTCAPACITY_EMPTY_ELEMENTDATA,后者与前者的区别在于,后者可以知道ArrayList被添加了第一个元素之后,数组的长度应该要被被扩展到多长,这个长度是由DEFAULT_CAPACITY指定的,数值默认为10,elementData变量被transient所修饰,表明了它不能够被序列化(可能是为了节省存储空间),size变量指的是集合所存放的元素的个数
3.2.2 构造方法
/** * Constructs an empty list with the specified initial capacity. * * @param initialCapacity the initial capacity of the list * @throws IllegalArgumentException if the specified initial capacity * is negative */ 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); } } /** * Constructs an empty list with an initial capacity of ten. */ public ArrayList() { this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; } /** * Constructs a list containing the elements of the specified * collection, in the order they are returned by the collection's * iterator. * * @param c the collection whose elements are to be placed into this list * @throws NullPointerException if the specified collection is null */ public ArrayList(Collection extends E> c) { elementData = c.toArray(); if ((size = elementData.length) != 0) { // c.toArray might (incorrectly) not return Object[] (see 6260652) if (elementData.getClass() != Object[].class) elementData = Arrays.copyOf(elementData, size, Object[].class); } else { // replace with empty array. this.elementData = EMPTY_ELEMENTDATA; } }
构造方法一共有三种:
(1) public ArrayList(int initialCapacity):第一种构造方法指定了一个初始容量,如果初始容量大于0,则新建一个该长度的Object类型数组,如果等于0,则返回成员变量中的长度为0的数组变量,如果小于0,则抛异常
(2) public ArrayList():第二种构造方法是一个空参构造,使用这种方式,默认创建一个长度为10的数组
(3) public ArrayList():此外还提供了一个构造方法可以传入一个集合对象c,该构造方法的执行流程是首先调用toArray方法转换成数组对象赋给elementData,由于返回值有可能不是Object类型的数组,因此又在if判断中调用了Arrays工具类的copyOf方法将其转化成数组
3.2.3 ensureCapacity方法
/** * Trims the capacity of this ArrayList instance to be the * list's current size. An application can use this operation to minimize * the storage of an ArrayList instance. */ public void trimToSize() { modCount++; if (size < elementData.length) { elementData = (size == 0) ? EMPTY_ELEMENTDATA : Arrays.copyOf(elementData, size); } } /** * Increases the capacity of this ArrayList instance, if * necessary, to ensure that it can hold at least the number of elements * specified by the minimum capacity argument. * * @param minCapacity the desired minimum capacity */ public void ensureCapacity(int minCapacity) { int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) // any size if not default element table ? 0 // larger than default for default empty table. It's already // supposed to be at default size. : DEFAULT_CAPACITY; if (minCapacity > minExpand) { ensureExplicitCapacity(minCapacity); } } private void ensureCapacityInternal(int minCapacity) { if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity); } ensureExplicitCapacity(minCapacity); } private void ensureExplicitCapacity(int minCapacity) { modCount++; // overflow-conscious code if (minCapacity - elementData.length > 0) grow(minCapacity); } /** * The maximum size of array to allocate. * Some VMs reserve some header words in an array. * Attempts to allocate larger arrays may result in * OutOfMemoryError: Requested array size exceeds VM limit */ private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8; /** * Increases the capacity to ensure that it can hold at least the * number of elements specified by the minimum capacity argument. * * @param minCapacity the desired minimum capacity */ private void grow(int minCapacity) { // overflow-conscious code int oldCapacity = elementData.length; int newCapacity = oldCapacity + (oldCapacity >> 1); if (newCapacity - minCapacity < 0) newCapacity = minCapacity; if (newCapacity - MAX_ARRAY_SIZE > 0) newCapacity = hugeCapacity(minCapacity); // minCapacity is usually close to size, so this is a win: elementData = Arrays.copyOf(elementData, newCapacity); } private static int hugeCapacity(int minCapacity) { if (minCapacity < 0) // overflow throw new OutOfMemoryError(); return (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE; }
(1) 在讲解ensureCapacity方法之前,我们先来看一个叫做trimToSize的方法,这个方法可以看成是一个优化手段,如果elementData对象的长度是大于size的,那么就将它的长度调整至size大小,从而达到了节省空间的目的
(2) 在之后的API中,我们会反复看到modCount变量,查看了一下本类,并没有看到这个变量,说明我们应该去父类中找寻它,最终,在它的父类抽象类AbstractList中找到了它,根据文档可知,它其实是一个修改计数器,也就是之前提到过的"Structual modification",只有发生了结构化的改变才会触发这个变量的增加,很显然,上文的trimToSize引起了结构化的改变,因此导致了这一变量的自增1
(3) 现在我们正式开始查看ensureCapacity方法的代码,当用户传入的参数minCapacity大于10的时候,就会调用另一个方法,ensureExplicitCapacity(minCapacity),这个方法中,我们看到了注释,//overflow-conscious code,翻译过来就是防止出现溢出现象,也就是说,只有当你指定的最小容量是大于elementData.length的时候,才会触发扩容操作!
(4) 成员变量MAX_ARRAY_SIZE解读:由于虚拟机将某些"header words"转化到数组中去,因此这个值并非是Integer.MAX_VALUE,而是整型的最大值减8,一旦想要分配的数组的长度大于这个值,则会触发内存溢出错误,OutOfMemoryError
(5) 扩容操作的具体实现,grow(int minCapacity):首先oldCapacity变量记录了elementData原本的长度,然后将oldCapacity + (oldCapacity >> 1)也就是oldCapacity的1.5倍赋值给了变量newCapacity,如果扩了容后这个值都还比minCapacity小,那么就把minCapacity赋给newCapacity,如果newCapacity大于MAX_ARRAY_SIZE,就调用hugeCapacity()方法,在这个方法中,有可能会抛出OOM错误,最后使用Arrays.copyOf(elementData,newCapacity)方法实现了数组扩容
3.2.4 常用的API
contains方法
public boolean contains(Object o) { return indexOf(o) >= 0; } /** * Returns the index of the first occurrence of the specified element * in this list, or -1 if this list does not contain the element. * More formally, returns the lowest index i such that * (o==null ? get(i)==null : o.equals(get(i))), * or -1 if there is no such index. */ public int indexOf(Object o) { if (o == null) { for (int i = 0; i < size; i++) if (elementData[i]==null) return i; } else { for (int i = 0; i < size; i++) if (o.equals(elementData[i])) return i; } return -1; }
contains方法中调用了indexOf方法,通过这个方法的返回值是否大于等于0来判断list是否包含某元素,而查看indexOf方法可知,它是通过遍历这个elementData数组,如果equals方法返回true,则返回这个索引,如果找完了都没找到,则返回-1,由此可知,如果用户自定义了一个类,就必须要重写equals方法,那么下面,我们就举一个例子验证一下这个问题!
首先定义一个学生类Student
public class Student { private String name; private int age; private int id; public Student(String name, int age, int id) { this.name = name; this.age = age; this.id = id; } public Student() { } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } public int getId() { return id; } public void setId(int id) { this.id = id; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Student student = (Student) o; return age == student.age && id == student.id && name.equals(student.name); } }
然后写一个测试类:
import java.util.ArrayList; /* 测试ArrayList的contains方法 */ public class StudentTest { public static void main(String[] args) { ArrayListstudents = new ArrayList (); Student stu1 = new Student("tom", 10, 1); Student stu2 = new Student("alice", 20, 2); Student stu3 = new Student("peter", 25, 3); students.add(stu1); students.add(stu2); students.add(stu3); System.out.println(students.contains(new Student("tom",10,1))); } }
首先我们把equals方法注释起来,最终控制台输出的结果为false;然后将注释放开,结果变为了true,由此得证。
add方法
public boolean add(E e) { ensureCapacityInternal(size + 1); // Increments modCount!! elementData[size++] = e; return true; } private void ensureCapacityInternal(int minCapacity) { if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity); } ensureExplicitCapacity(minCapacity); }
add方法中是通过调用ensureCapacityInternal方法来实现数组的扩容的,而这个方法在之前讲解ensureCapacity时并未提及,那么,我们再回过头来查看这个方法的源码,可知,当原数组是个空数组时,会直接把长度扩容到10,然后执行语句elementData[size++] = e,注意,++是写在后面的,因此执行顺序应该是先在size的索引位置处添加上新元素,然后size再自增1;add语句不断执行,数组的长度不断增长,当size + 1大于10的时候,ensureExplicitCapacity方法中的防溢出代码就会触发grow操作,将原数组的长度扩张到1.5倍,然后继续执行相同流程
add方法的另一个重载
/** * Inserts the specified element at the specified position in this * list. Shifts the element currently at that position (if any) and * any subsequent elements to the right (adds one to their indices). * * @param index index at which the specified element is to be inserted * @param element element to be inserted * @throws IndexOutOfBoundsException {@inheritDoc} */ public void add(int index, E element) { rangeCheckForAdd(index); ensureCapacityInternal(size + 1); // Increments modCount!! System.arraycopy(elementData, index, elementData, index + 1, size - index); elementData[index] = element; size++; } /** * A version of rangeCheck used by add and addAll. */ private void rangeCheckForAdd(int index) { if (index > size || index < 0) throw new IndexOutOfBoundsException(outOfBoundsMsg(index)); }
整个过程使用下图即可解释:
比如我要在第二个索引位置处加上元素7,实际过程就是如上图所示,将3,4,5,6这四个元素往后移动一格,然后在空出来的那一位上填上7即可
addAll方法
public boolean addAll(Collection extends E> c) { Object[] a = c.toArray(); int numNew = a.length; ensureCapacityInternal(size + numNew); // Increments modCount System.arraycopy(a, 0, elementData, size, numNew); size += numNew; return numNew != 0;
}
addAll方法的实现原理是首先调用toArray方法将一个集合对象c转换成Object数组,之后获取到这个数组的长度,然后调用arraycopy方法将c中的每一个元素都加到ArrayList的实现子类对象中去,最后判断加的集合是否为空,空的话就返回false,非空表明添加成功,返回true。注:addAll方法与add方法的最大区别是add方法会将一整个集合看作一个元素进行添加,而addAll则会把一个集合中的元素打散了一个一个地进行添加
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;
}
同样画图演示:
根据源码算法,如果需要移除的是索引为2的元素,首先计算出需要移动的元素个数为3,然后使用数组拷贝方法将index + 1之后的所有元素拷贝到index的位置,这样再将最后一个索引置空交给java的垃圾回收机制处理即可,最后返回需要移除的索引值对应的元素
注意:当在遍历集合的同时删除元素时,由于会发生整体移动,因此需要注意remove之后将索引减一!
batchRemove方法:
private boolean batchRemove(Collection> c, boolean complement) { final Object[] elementData = this.elementData; int r = 0, w = 0; boolean modified = false; try { for (; r < size; r++) if (c.contains(elementData[r]) == complement) elementData[w++] = elementData[r]; } finally { // Preserve behavioral compatibility with AbstractCollection, // even if c.contains() throws. if (r != size) { System.arraycopy(elementData, r, elementData, w, size - r); w += size - r; } if (w != size) { // clear to let GC do its work for (int i = w; i < size; i++) elementData[i] = null; modCount += size - w; size = w; modified = true; } } return modified;
} public boolean removeAll(Collection> c) { Objects.requireNonNull(c); return batchRemove(c, false);
} public boolean retainAll(Collection> c) { Objects.requireNonNull(c); return batchRemove(c, true); }
(1) 查看removeAll以及retainAll方法可知,它们两个都是通过调用batchRemove方法来实现的,区别只是removeAll方法中complement参数是false,而retainAll方法是true,于是我们转向研究batchRemove方法,首先可以看到定义了两个局部变量r和w,可以理解为read 和 write,r负责遍历ArrayList中的元素,w负责将符合条件的元素进行写出,看到这里,我们就恍然大悟,原来complement参数指的是你要保留还是移除,如果指定的是true,即只有当集合中的元素和ArrayList中的相等时才写出,那么就等同于retainAll方法,而反之亦然
(2) 了解了上述这一点我们就能理解elementData[w++] = elementData[r]这句代码了,我们发现这个方法是套在try-finally框架中的,这就意味着,无论try里面的语句有没有发生异常,finally语句块中的语句是一定会被执行到的,那么我们转而去看一下finally中到底做了些什么吧!首先看第一个if块中的代码,if(r != size),我相信,大多数人在看到这里时都是懵逼的,try中的是一个循环语句,当r等于size的时候就会跳出循环,所以最终r应该是等于size的才对,那么这句语句为什么会被执行到呢?我们先跳过这个问题不谈,先看一下r = size的时候会发生什么?很明显,r = size的时候代码会运行到第二个if判断,即if(w != size),这段代码就相对好理解一些了,由于在之前的try语句块中我们已经找到了符合要求的元素并进行写出了,因此在第二个if语句块中,直接把w之后的元素直接置空,最后将size的值调整到w的值即可,而modified变量这时也变成了true,因为确确实实进行过了修改!
(3) 那么回到第二点中的遗留问题,什么时候才会出现try语句块中循环条件没有执行完的情况呢?不着急,先看一下finally语句块一上来的那两句注释,// Preserve behavioral compatibility with AbstractCollection,// even if c.contains() throws.
翻译过来的意思是,它要和AbstractCollection类保持兼容性,contains方法是有可能抛出异常的,这样一来循环条件执行不完这种情况就是有可能会发生的了,因此在finally语句块中第一个if判断就有可能被触发!我们再回过头来看这个if判断,可以发现实际上它就是把没有遍历到的那些元素(即size - r个元素)又拷贝到了w索引的后面,然后执行完w += size - r之后再判断w是否和size相等
3.2.5 迭代器
并发修改异常举例:
import java.util.ArrayList; import java.util.ListIterator; /* 演示并发修改异常 */ public class ConcurrentModificationExceptionDemo { public static void main(String[] args) { ArrayListlist = new ArrayList (); list.add(1); list.add(2); list.add(3); list.add(4); ListIterator it = list.listIterator(); while(it.hasNext()){ if(it.next().equals(1)){ list.add(5); } } } } Exception in thread "main" java.util.ConcurrentModificationException at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:901) at java.util.ArrayList$Itr.next(ArrayList.java:851) at com.lf.ConcurrentModificationExceptionDemo.main(ConcurrentModificationExceptionDemo.java:18)
在此例中,使用ListIterator进行集合的遍历,然而却调用了list自身的add方法进行元素的添加,结果抛出了"ConcurrentModificationException"的并发修改异常
三种集合迭代方法:注意,foreach的本质其实还是迭代器!!!
import java.util.ArrayList; import java.util.Iterator; /* 演示三种迭代集合的方法 */ public class IterateDemo { public static void main(String[] args) { ArrayListlist = new ArrayList (); list.add("tom"); list.add("alice"); list.add("peter"); list.add("mary"); //使用Iterator Iterator it = list.iterator(); while(it.hasNext()){ System.out.println(it.next()); } System.out.println("======================="); //使用索引法 for(int i = 0; i < list.size(); i++){ System.out.println(list.get(i)); } System.out.println("======================="); //增强for循环 for (String name : list) { System.out.println(name); } } }
3.2.6 泛型以及泛型方法
泛型的好处:
1. 对进入集合的元素进行了类型检查,将运行时期的异常提前到了编译时期
2. 避免了类型转换异常,ClassCastException
import java.util.Date; /* 演示泛型方法,定义一个泛型打印方法,可以打印任何数据类型 */ public class GenericMethodDemo { public static void main(String[] args) { //打印字符串 genericPrint("tom"); //打印整数 genericPrint(3); //打印当前日期 genericPrint(new Date()); } public staticvoid genericPrint(T t){ System.out.println(t); } }
泛型方法的定义方式和泛型类不同,需要把泛型写在返回值的前面,程序会根据用户传入的参数自定义地判断它是属于什么数据类型的
3.2.7 泛型通配符
/* 演示泛型通配符 */ import java.util.ArrayList; import java.util.Collection; class A{ } class B extends A{ } class Generic{ public void test0(Collection> c){ } public void test(Collection extends A> c){ } public void test2(Collection super A> c){ } } public class GenericDemo { public static void main(String[] args) { Generic gen = new Generic(); //任何类型都可以传入 gen.test0(new ArrayList()); //A以及A的子类都可以传入泛型中去 gen.test(new ArrayList()); //A以及A的父类都可以传入泛型中去 gen.test2(new ArrayList