Java集合框架1-List和Set
集合框架简介
Java提供了一系列的集合,主要包括util包下边的ArrayList,LinkedList,HashMap,HashTable,HashSet,LinkedHashMap,LinkedHashSet,TreeMap,TreeSet,ArrayDeque,PriorityQueue, EnumMap,Vector,Stack
Concurrent包中的ConcurrentHashMap,CopyOnWriteArrayList,CopyOnWriteArraySet,ArrayBlockingQueue,ConcurrentLinkedDequeue,ConcurrentLinkedQueue
从上边的结构构可以看出来,Set和List最底层都是实现了Collection接口的,Collection接口z主要方法如下:
int size();
boolean isEmpty();
boolean contains(Object o);
Iterator<E> iterator();
Object[] toArray();
<T> T[] toArray(T[] a);
boolean add(E e);
boolean remove(Object o);
boolean containsAll(Collection<?> c);
boolean addAll(Collection<? extends E> c);
boolean removeAll(Collection<?> c);
boolean retainAll(Collection<?> c);
void clear();
default boolean removeIf(Predicate<? super E> filter)
可以看到,除了常见方法之外,由于Collection继承了Iterable,所以还有一个iterator()方法,也就是说,Set和List都可以通过Iterator来遍历。
ArrayList
ArrayList可以说是最常用的集合了。它的构造方法如下:
private static final int DEFAULT_CAPACITY = 10;
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
private static final Object[] 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);
}
}
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
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)
//c.toArray()返回的可能不是Object[],而ArrayList存储数据用的就是Object[],这时候就需要将elementData转换成Object[]
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
// replace with empty array.
this.elementData = EMPTY_ELEMENTDATA;
}
}
三个构造方法,第一二两个,实际上是创建了一个可以指定容量的空的Object数组。区别在于,无参构造函数将存储数组elementData
指向了DEFAULTCAPACITY_EMPTY_ELEMENTDATA
,它和EMPTY_ELEMENTDATA
的区别马上我们就可以看到。
第三个不常用的构造函数,可以接受一个实现了Collection接口的集合作为参数,并将它的值全部复制进来
List创建完成以后,添加数据使用的add方法有以下几个:
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
public void add(int index, E element) {
if (index > size || index < 0)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
ensureCapacityInternal(size + 1); // Increments modCount!!
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
elementData[index] = element;
size++;
}
两个方法基本相同,第二个方法可以指定位置插入元素,插入之后,后边的元素index全部增加+1,长度同时增加,不指定位置的时候,在数组最后一位插入数据,插入数据之前,需要通过ensureCapacityInternal
方法来扩容,源码如下:
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);
}
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);
}
在ensureCapacityInternal
方法中,第一步先判断了是不是DEFAULTCAPACITY_EMPTY_ELEMENTDATA
,如果是的话,说明list是通过无参构造方法创建的,扩容值取DEFAULT_CAPACITY
和 size+1
中的较大者,DEFAULT_CAPACITY
值为10,这里就清楚了:
通过new ArrayList(0)
和new ArrayList()
创建的list,初始容量都是0,但是在add一个元素的时候,前者容量会变成0+1=1,而后者会直接直接将容量变成默认值:10
扩容最终是通过grow方法实现的:新的容量先确定为原来的1.5倍,同时确保扩容的容量不能比原来小,也不能超过最大值,确定了长度之后,再把数据拷贝进来。
再来看一下移除数据的remove方法:
public E remove(int index) {
if (index >= size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
modCount++;
E oldValue = (E) 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;
}
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
}
相比add方法,remove就比较简单了,传入int参数,表示按索引移除,传入Object表示按值移除,按值移除时,需要先判断值是不是null,null用 == 来判断,否则用 equals 方法判断,最终的移除是通过System.arraycopy(elementData, index+1, elementData, index,numMoved);
来实现的:保留index之前的数据不变,从 index+1 开始复制数据到 index 的位置,将最后一位赋值为null(释放对象)
LinkedList
LinkedList内部实现使用链表的形式,构造方法如下:
public LinkedList() {
}
public LinkedList(Collection<? extends E> c) {
this();
addAll(c);
}
非常简单,除了复制数据之外,没有任何初始化工作。,下边看add方法:
public boolean add(E e) {
linkLast(e);
return true;
}
public void add(int index, E element) {
checkPositionIndex(index);
if (index == size)
linkLast(element);
else
linkBefore(element, node(index));
}
void linkLast(E e) {
final Node<E> l = last;//新建对象 l 指向原来的链表尾部数据(last是指向尾部数据的成员变量)
final Node<E> newNode = new Node<>(l, e, null);//新建一个节点,pre指针指向原来的尾部 l
last = newNode;//成员变量last指向新的尾部:newNode
if (l == null)//如果 l 是空的,说明原来没有数据,所以,新建的节点是头部
first = newNode;
else //不是空的说明原来有数据,将原来的尾部数据 l 的next指针指向新的尾部: newNode
l.next = newNode;
size++;
modCount++;
}
void linkBefore(E e, Node<E> succ) {
// assert succ != null;
final Node<E> pred = succ.prev;
final Node<E> newNode = new Node<>(pred, e, succ);
succ.prev = newNode;
if (pred == null)
first = newNode;
else
pred.next = newNode;
size++;
modCount++;
}
如果不指定index,直接将元素添加在链表尾部,否则添加在指定位置。
接下来看取值方法get:
public E get(int index) {
checkElementIndex(index);
return node(index).item;
}
Node<E> node(int index) {
// assert isElementIndex(index);
if (index < (size >> 1)) {
Node<E> x = first;
for (int i = 0; i < index; i++)
x = x.next;
return x;
} else {
Node<E> x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}
采用链表形式存储数据,不能像数组一样直接取值了,最终通过node
方法实现,实现方式是二分法,遍历取值。
HashSet和LinkedHashSet
HashSet构造方法如下:
public HashSet() {
map = new HashMap<>();
}
初始化了一个HashMap,也就是说,HashSet存储数据实际上使用了HashMap。
对数据的操作方法如下:
private static final Object PRESENT = new Object();
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
public boolean remove(Object o) {
return map.remove(o)==PRESENT;
}
可以说是非常简单,全部调用了HashMap的方法,数据作为map的key,value统一为PRESENT。
再看一下LinkedHashSet:
public LinkedHashSet() {
super(16, .75f, true);
}
public LinkedHashSet(int initialCapacity) {
super(initialCapacity, .75f, true);
}
public LinkedHashSet(int initialCapacity, float loadFactor) {
super(initialCapacity, loadFactor, true);
}
HashSet(int initialCapacity, float loadFactor, boolean dummy) {
map = new LinkedHashMap<>(initialCapacity, loadFactor);
}
实际上LinkedHashSet创建的时候,调用了HashSet的构造方法,创建了一个指定初始容量(默认16)和负载因子(默认0.75)的LinkedHashMap。
其他方法全部都是调用父类HashSet,所以他们两个唯一的区别就是:HashSet是使用HashMap,而LinkedHashSet使用LinkedHashMap。
同样的,TreeSet数据存储使用的是TreeMap。而ArraySet则是Android提供的,和ArrayMap一样,都是采用了数组的形式存储数据,占用内存更小一些。
List 和 Set 的遍历以及数据操作
List 和 Set 都实现了Iteratorable接口,使用Iterator遍历方法如下:
ArrayList<String> s = new ArrayList<>();
s.add("a");
s.add("b");
s.add("c");
s.add("d");
s.add("e");
Iterator<String> it = s.iterator();
while (it.hasNext()){
String s1 = it.next();
System.out.println(s1);
}
如果在遍历的过程中对数据进行操作,看下边的例子:
Iterator<String> it = s.iterator();
while (it.hasNext()){
String s1 = it.next();
if("a".equals(s1)){
s.remove(s1);
}
}
new Thread(new Runnable() {
@Override
public void run() {
Iterator<String> it = s.iterator();
while (it.hasNext()) {
String s1 = it.next();
System.out.println(s1);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
Iterator<String> it = s.iterator();
while (it.hasNext()) {
String s1 = it.next();
if ("e".equals(s1)) {
it.remove();
}
}
}
}).start();
运行以后会抛出下边的错误:
Exception in thread "main" java.util.ConcurrentModificationException
第一种情况是在使用Iterator的时候对集合进行操作,使用集合的remove或者add方法,第二种情况是在不同线程同时使用Iterator遍历,使用Iterator的remove方法,这两种情况都会抛出ConcurrentModificationException
异常。
下面看ArrayList的源码:
int expectedModCount = modCount;
public Iterator<E> iterator() {
return new Itr();
}
private class Itr implements Iterator<E> {
public E next() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
int i = cursor;
if (i >= limit)
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();
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
try {
ArrayList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
expectedModCount = modCount;
limit--;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
}
可以看到,每次使用next取值之前,都会检查modCount
和expectedModCount
是否相等,expectedModCount
是Itr的成员变量,在创建Iterator
对象时它的值就确定了,Itr
类内部也不会对它进行操作,它的值时不变的,就是创建对象时的modCount
,抛出异常说明modCount
的值变了。再回头去看一下ArrayList
的add,remove方法,就会发现,每次操作modCount
的值都会改变,所以才导致异常。
如果是单线程中,可以使用Iterator
的remove方法,可以看到,在remove方法删除元素之后,又对expectedModCount
进行了复制,所以并不会报错。
如果是多线程,Iterator
对象是不同的,一个对象调用remove之后,仅仅能保证本对象中的expectedModCount
正确,其他线程中的Iterator
对象是不会更新的,所以仍然会报错。多线程可以用concurrent包下的并发容器,
另外,数组实现的ArrayList,查找会比较快,但是数据量大的时候插入数据会比较慢,因为每次都要拷贝数据,而链表实现的LinkedList,查找时需要对链表进行遍历吗,所以会比较慢,而插入数据很快。