Java集合框架总图
List接口与具体实现类
List接口继承Collection,允许重复,维护元素插入顺序。
一、ArrayList
1、继承关系
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
可以看出ArrayList继承自AbstractList父类,实现了List ,Cloneable, java.io.Serializable接口,即ArrayList支持克隆、序列化。
2、属性分析
private static final int DEFAULT_CAPACITY = 10;//ArrayList底层实现为数组,且默认大小10
private static final Object[] EMPTY_ELEMENTDATA = {};
private transient Object[] elementData;//ArrayList的底层存储使用的数组
private int size; //表示集合中存储的元素的个数
3、构造函数
//有参构造
public ArrayList(int initialCapacity) { //传入的参数initialCapacity表示初始化数组大小
super();
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal Capacity: "+initialCapacity);
//直接实例化数组的大小为:initialCapacity
this.elementData = new Object[initialCapacity];
}
//参数为Collection接口的实例对象
public ArrayList(Collection<? extends E> c) {
elementData = c.toArray();
size = elementData.length;
// c.toArray might (incorrectly) not return Object[] (see 6260652)
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
}
//无参构造
public ArrayList() {
super();
this.elementData = {} EMPTY_ELEMENTDATA;
}
4、常用方法
(1)add()
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
private void ensureCapacityInternal(int minCapacity) {
if (elementData == EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity > elementData.length)
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);
}
操作过程:判断ArrayList容量是否足够,不足则扩容(按原数组长度的1.5倍扩容),然后进行数据添加。
(2)get()
public E get(int index) {
rangeCheck(index);
return elementData(index);
}
//参数index的范围检查
private void rangeCheck(int index) {
if (index < 0 || index >= this.size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
操作过程:首先检查index的合法性问题,直接通过数据下标访问。
(3)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
}
操作过程:首先判断当前对象是否是null,非null值对象通过equals来判断集合中是否存在,然后进行数据移动,并将删除对象位置之后的数据依次前移。注:ArrayList中重复性存储的数据,调用删除方法时只会将找到的第一个元素删除,后面的重复数据不进行处理。
(4)set()
public E set(int index, E element) {
rangeCheck(index);
E oldValue = elementData(index);
elementData[index] = element;
return oldValue;
}
操作过程:判断下标合法性,直接通过下标将数据重置。
(5)removeAll()
arrayList.removeAll(arrayList2);
System.out.println(arrayList.contains(1)); //用contains()判断集合里是否包含指定元素,存在返回true,反之false
(6)retainAll()
//retainAll(a) 两集合的交集
b.retainAll(a);
//toArray() 将集合转化为数组
Integer[] array = (Integer[]) a.toArray();
5、使用示例
①增加
ArrayList<String> arrayList=new ArrayList<>();//若是Integer则arrayList里只能存放Integer类型的数据
for(int i=0;i<10;i++){
arrayList.add(i+"abc");
System.out.print(arrayList.get(i)+" ");
}
System.out.println();
②遍历
法1:普通遍历
for (int i = 0; i < 10; i++) {
System.out.print(arrayList.get(i) + " ");
}
System.out.println();
法2:foreach语句
for (String a : arrayList) {
System.out.print(a + " ");
}
System.out.println();
法3:迭代器
Iterator<String> aLIterator = arrayList.iterator();
while (aLIterator.hasNext()) { //通过hasNext()这判断是否还有下一个元素
String value = aLIterator.next(); //通过next() 去获取下一个元素
System.out.print(value + " ");
}
③删除
传具体的元素进行删除:arrayList.remove(“3abc”);
传下标进行删除:arrayList.remove(5);
④修改
arrayList.set(3, "200abc"); //通过下标直接改变元素的值
⑤判断某元素是否存在
boolean contains=arrayList.contains("200abc");
System.out.println(contains);
⑥批量增加:把blist加到alist里
ArrayList<Integer> alist = new ArrayList<>();
ArrayList<Integer> blist = new ArrayList<>();
for (int i = 0; i < 10; i++) { //0~9
alist.add(i);
}
for (int i = 0; i < 10; i++) { // 5~14
blist.add(i + 5);
}
alist.addAll(blist);
⑦批量删除
从alist里删掉blist有的元素(删掉交集)
alist.removeAll(blist);
⑧求交集
alist.retainAll(blist);
⑨求并集(无重复元素)
blist.removeAll(alist);
alist.addAll(blist);
for (Integer a : alist) {
System.out.print(a + " ");
}
System.out.println();
6、补充知识点
(1)ArrayList可以存放null。
(2)ArrayList中removeAll(collection c)和clear()的区别就是removeAll可以删除批量指定的元素,而clear是全是删除集合中的元素。
(3)ArrayList由于本质是数组,所以它在数据的查询方面会很快,而在插入删除这些方面,性能下降很多,有移动很多数据才能达到应有的效果。而ArrayList区别于数组的地方在于:ArrayList能够自动扩展大小,其中关键的方法就是gorw()方法,而数组创建时初始化大小之后不可修改。
(4)ArrayList实现了RandomAccess,所以在遍历它的时候推荐使用for循环。
二、LinkedList
1、继承关系
public class LinkedList<E> extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable
可以看出LinkedList继承自AbstractList的子类AbstractSequentialList(AbstractSequentialList是不同于AbstractList的另一套增删改查,底层是通过迭代器来完成相关操作的,而且LInkedList集合类底层是通过双端链表实现的,属于顺序访问的集合类,对于这种顺序访问的类,优先继承的是AbstractSequentialList类),实现了List、Deque、Cloneable, java.io.Serializable,所以LinkedList可以克隆、序列化,此外还可以头尾添加、头尾获取、头尾删除。
2、属性分析
transient int size = 0;//表示集合中元素个数
transient Node<E> first;//双向链表的头结点
transient Node<E> last;//双向链表的尾节点
private static class Node<E> {
E item;
Node<E> next;
Node<E> prev;
//可以看出LinkedList底层的数据结构是双向链表 ,每一个节点是Node
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
3、构造函数
//无参构造
public LinkedList() {
}
//通过Collection的集合实例来构造LInkedList实例
public LinkedList(Collection<? extends E> c) {
this();
addAll(c);
}
4、常用方法
(1)add()
public boolean add(E e) {
linkLast(e);
return true;
}
void linkLast(E e) {
final Node<E> l = last;
final Node<E> newNode = new Node<>(l, e, null);
last = newNode;
if (l == null)
//第一个插入的节点,将first头结点指向新节点
first = newNode;
else
//已经不是第一个节点
l.next = newNode;
size++;
modCount++;
}
操作过程:默认使用的是尾插法。
(2)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;
}
}
操作过程:先判断传入index合法性,然后采用二分查找方式(如果index小于size/2,从first节点开始往后遍历;如果index大于size/2,从last节点开始往前遍历,如果数据重复,直接从前或从后获取遇到的第一个元素返回,后续节点不再处理。
(3)remove(int index)
public E remove(int index) {
//检查参数合法性问题
checkElementIndex(index);
return unlink(node(index));
}
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;
}
}
E unlink(Node<E> x) {
// assert x != null;
final E element = x.item;
final Node<E> next = x.next;
final Node<E> prev = x.prev;
if (prev == null) {
first = next;
} else {
prev.next = next;
x.prev = null;
}
if (next == null) {
last = prev;
} else {
next.prev = prev;
x.next = null;
}
x.item = null;
size--;
modCount++;
return element;
}
操作过程:先检测index的合法性,然后通过index找到该位置节点Node,将该节点直接从双向链表中去掉(基本思想:若该节点为头节点,则使头节点下一个节点的前驱置空,并更新头节点;若为尾节点则使尾节点前一个节点的后继置空,并更新尾节点;若为中间节点,则直接连通该节点的前后节点),并使size减一。
(4)set()
public E set(int index, E element) {
checkElementIndex(index);
Node<E> x = node(index);
E oldVal = x.item;
x.item = element;
return oldVal;
}
操作过程:先进行下标检查,通过下标找到节点,获取该节点的值,重置为传入的数据。
三、ArrayList和LinkedList比较
1、共同点
(1)都是List接口的实现类。
(2)存取有序、有索引、可重复。
2、不同点
(1)底层实现
ArrayList底层实现是数组
LinkedList底层是双向链表
(2)效率
ArrayList查询快,增删慢。
①查询快:数组随机访问,通过数组地址和元素索引进行访问。
②增删慢:增删需要移动元素,增加元素时如果数组容量不足还需要扩容,影响效率。
③扩容:建立一个原数组1.5倍大小的新数组,将原数组的内容拷贝到新数组。
LinkedList查询慢,增删快。
①查询慢:从头或尾依次寻址。
②增删快:只需修改几个指针的指向。
思考:
(1)如何高效使用ArrayList和LinkedList?
答:业务中增删频繁就用LinkedList,查询频繁就用ArrayList。
(2)LinkedList索引查询的时候,是从头开始往后找,还是从尾开始往前找?
答:先判断要查找的元素距离头部近还是尾部近,距离哪个近就从哪边找。
(3)ArrayList和数组的区别是?
答:①ArrayList无需设定长度,而数组在初始化时需设置大小且不可改变;②ArrayList只可存储引用类型,而数组可以存储普通类型和引用类型;③ArrayList插入删除方便快捷,而数组不方便数据插入修改等。
四、Iterator和ListIterator
1、迭代器是一种提供了方法来访问容器中的元素,而不暴露容器中实现细节的设计模式。
2、Iterator
(1)Iterator接口包含三个方法:hasNext,next,remove
(2)ArrayList的Iterator实现:
import java.util.ArrayList;
import java.util.Iterator;
public class IteratorDemo {
public static void main(String[] args) {
ArrayList<Integer> arrayList=new ArrayList<>();
arrayList.add(13);
arrayList.add(67);
arrayList.add(2);
Iterator<Integer> iterator =arrayList.iterator();
while(iterator.hasNext()) {//判断集合是否还能继续遍历,true/false
Integer i=iterator.next();
if(i==67){
iterator.remove();
}
/*
注:此处不用arrayList.remove(Integer.valueOf(67));
是因为会抛出ConcurrentModificationException()异常
具体原因是该异常源码中:if (modCount != expectedModCount) throw new ConcurrentModificationException();
*/
System.out.print(i+" ");
}
System.out.println();
//验证是否删除成功,新建迭代器
Iterator<Integer> iterator1 =arrayList.iterator();
while(iterator1.hasNext()) {//判断集合是否还能继续遍历,true/false
Integer i1=iterator1.next();
System.out.print(i1+" ");
}
}
}
注:关于ConcurrentModificationException()异常
Iterator.remove()是唯一安全的方式来在迭代过程中修改集合;如果在迭代过程中以任何其它的方式修改了基本集合将会产生未知的行为。而且每调用一次next()方法,remove()方法只能被调用一次,如果违反这个规则将抛出一个异常。
在ArrayList中modCount是当前集合的版本号,每次修改集合都会加1,expectedModCount是当前迭代器的版本号,在迭代器实例化时初始化为modCount。当调用add()或者remove()方法时,会验证modCount值和expectedModCount值是否相等,而这种增删操作只更新了modCount的状态,而expectedModCount未同步,因此会导致再次调用Iterator.next()方法时抛出异常。而使用Iterator.remove()不抛出异常是因为Iterator的remove()中同步了expectedModCount的值。
(3)迭代出来的元素都是原来集合元素的拷贝。
Java集合中保存的元素实质是对象的引用,而非对象本身。 迭代出的对象也是引用的拷贝,结果还是引用。那么如果集合中保存的元素是可变类型的,那么可以通过迭代出的元素修改原集合中的对象。
(4)for循环和迭代器效率比较
①在ArrayList里使用for循环快。
因为ArrayList随机访问快,而for循环中使用get()方法采用的就是随机访问的方法。
②在LinkedList里使用Iterator较快。
因为LinkedList顺序访问快,而Iterator中的next()方法采用的就是顺序访问方法。
总之,因为for循环可以根据下标快速获取指定元素,更适合访问顺序结构;而迭代器通过next()和Pre()来定位,适合访问链式结构或没有顺序的集合。
2、ListIterator
(1)ListIterator继承于Iterator接口,只能用于各种List类型的访问。
(2)ListIterator提供了更多方法,从这些方法可以看出ListIterator可以:双向移动(向前/向后遍历)、产生相对于迭代器在列表中指向的当前位置的前一个和后一个元素的索引、可以使用set()方法替换它访问过的最后一个元素、可以使用add()方法在next()方法返回的元素之前或previous()方法返回的元素之后插入一个元素。
import java.util.*;
public class ListIteratorTest {
/*常用方法:
boolean hasNext()判断是否有后驱
E next()获取后驱节点
void remove()删除
ListIterator特有方法:
boolean hasPrevious判断是否有前驱
E previous获取前驱
int previousIndex()获取前驱节点位置
int nextIndex()获取后继节点位置
void add()添加元素
void set()修改元素
*/
public static void main(String[] args) {
ArrayList<String> arrayList = new ArrayList<String>();
arrayList.add("aaa");
arrayList.add("bbb");
arrayList.add("ccc");
System.out.println("原始ArrayList: " + arrayList);
ListIterator<String> it1 = arrayList.listIterator();
System.out.println("从前往后遍历:");
while (it1.hasNext()) {
System.out.print(it1.next() + " ");
}
System.out.println();
System.out.println("从前往后遍历:");
ListIterator<String> it2 = arrayList.listIterator(arrayList.size());
while (it2.hasPrevious()) {
System.out.print(it2.previous() + " ");
}
System.out.println();
it1 = arrayList.listIterator(1);//调用listIterator(n)方法创建一个一开始就指向列表索引为n的元素处的ListIterator。
while (it1.hasNext()) {
String t = it1.next();
if ("ccc".equals(t)) {
it1.set("nnn");//如果t是ccc就重置为nnn
}
}
System.out.println("操作后的ArrayList: " + arrayList);
}
}
3、两者区别
(1)ListIterator有add()方法,可以向List中添加对象,Iterator不能。
(2)ListIterator有hasPrevious()和previous()方法,可以实现逆向遍历,Iterator不可以。
(3)ListIterator可以通过nextIndex()和previousIndex()方法定位当前索引位置,Iterator不可以。
(4)ListIterator可以通过set()方法实现对象的修改,Iterator仅能遍历不能修改。