前言:前面几篇博客简单的叙述了几个常用算法的思想,现在,要详细的分析它们;
在java中,算法和数据结构大部分是体现在java容器上面,所以,接下来,我们要开始分析容器了;
第一篇分析的是两种最简单的数据结构之一:数组;
一提到使用数组作为数据结构的容器,我们首先就会想到ArrayList,ArrayList的底层设计就是数组, 对ArrayList的操作实际上就是对数组进行操作;
关于ArrayList的原理,其实也没啥好说的,它的增删改查也很简单,这里就不详细讲了,我们来看下ArrayList的声明:
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
从ArrayList的声明中,可以看出,它实现了两个主要的接口:List和RandomAccess,而List又继承了Collection,Collection又继承了Iterable,所以,ArrayList实际上实现了4个接口,分别是:
①Iterable;
②Collection;
③List;
④RandomAccess;
接下来就从这4个接口来分析ArrayList;
1.Iterable,迭代
我们来看段代码:
ArrayList<Integer> intList = new ArrayList<Integer>();
intList.add(123);
intList.add(456);
intList.add(789);
//foreach语法
for(Integer a : intList){
System.out.println(a);
}
上述代码中,使用了foreach语法循环打印ArrayList中每个元素;foreach语法适用于各种容器,那它的原理是什么呢?
其实,当我们使用foreach语法时,编译器会将它转化为类似如下代码:
Iterator<Integer> it = intList.iterator();
while(it.hasNext()){
System.out.println(it.next());
}
Iterator就是我们要分析的迭代;
①迭代器接口
ArrayList实现了Iterable接口, Iterable表示可迭代:
public interface Iterable<T> {
Iterator<T> iterator();
}
定义很简单,就是要求实现iterator方法,返回一个实现了Iterator接口的对象,Iterator又是怎么定义的呢?
public interface Iterator<E> {
boolean hasNext();
E next();
void remove();
}
Iterator接口中有3个方法需要被实现:
hasNext():判断是否还有元素未访问;
next():返回下一个元素;
remove():删除最后返回的元素;
②Iterable原理
我们来看下ArrayList是如何实现iterator方法的:
public Iterator< E> iterator() {
return new Itr();
}
新建了一个Itr对象,Itr是ArrayList的内部类,它实现了Iterator接口, 声明如下:
private class Itr implements Iterator<E> {
//limit就是ArrayList的size
protected int limit = ArrayList.this.size;
int cursor; //下一个要返回的元素位置
int lastRet = -1; //最后一个返回的索引位置,如果没有,为-1;
//expectedModCount表示期望的修改次数,初始化为外部类当前的修改次数modCount,
//每次发生结构性变化的时候modCount都会增加,而每次迭代器操作的时候
//都会检查expectedModCount是否与modCount相同,这样就能检测出结构性变化;
int expectedModCount = modCount;
//cursor与size比较
public boolean hasNext() {
return cursor < limit;
}
public E next() {
//检查是否发生了结构性变化
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
//如果没有发生结构性变化,就更新cursor和lastRet的值,以保持其语义
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的remove方法
ArrayList.this.remove(lastRet);
// 更新cursor、lastRet和expectedModCount的值,所以它可以正确删除
cursor = lastRet;
lastRet = -1;
expectedModCount = modCount;
limit--;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
}
点评: 从Itr的代码实现中,可以看出:
(1)调用remove方法前必须先调用next,调用next是为了给lastRet设置正确的值,否则lastRet = -1;
比如, 通过迭代器删除所有元素, 直觉上,可以这么写:
public static void removeAll(ArrayList<Integer> list){
Iterator<Integer> it = list.iterator();
while(it.hasNext()){
it.remove();
}
}
实际运行,会抛出异常java. lang. IllegalStateException, 正确写法是:
public static void removeAll(ArrayList<Integer> list){
Iterator<Integer> it = list.iterator();
while(it.hasNext()){
it.next();
it.remove();
}
}
当然,如果只是要删除所有元素,ArrayList有现成的方法clear();
(2)Itr.remove()方法在调用ArrayList.remove()删除元素之前都会进行modCount != expectedModCount的判断,当调用ArrayList的add()和remove()方法时modCount都会+1,由于foreach语句的原理就是迭代器,并且所以在foreach的迭代过程中不能使用容器的删除,添加等改变容器内部结构的操作,如下面的代码就是个错误示范:
public void change(ArrayList<Integer> list){
for(Integer a : list){
if(a<=100){
//以下两种写法都是错误的;
//list.remove()和list.add()方法都会导致modCount++,
//这样在迭代过程中就会导致expectedModCount与modCount不等,一旦不等,就会报异常
//list.remove(a);
//list.add(3);
}
}
}
③迭代器的好处
为什么我们要使用迭代器呢?
其实迭代器的设计运用关注点分离的思想,我们知道List的子类有很多个,比如现在分析的ArrayList和接下来要分析的LinkedList,这些List的子类都实现了自己的增删改查的方法,当我们要访问这些子类容器时,是不是非得确定它是什么容器,然后调用指定容器的独有方法?有没有统一的访问接口呢?有,那就是迭代器。
举个例子:
List<Integer> list1 = new ArrayList<Integer>;
List<Integer> list2 = new LinkedList<Integer>;
list1.add(1);
list1.add(2);
list2.add(1);
list2.add(2);
for(Integer a : list1){
System.out.println(a);
}
for(Integer a : list2){
System.out.println(a);
}
我们发现,list1和list2都可以使用foreach语句,因为ArrayList和LinkedList都实现了Iterator接口,这就使得我们在不知道容器内部结构的情况下,就可以直接使用它的foreach语句,这就是面向编程设计的6大原则中的一个:里氏替换原则;
2.Collection
Collection的定义如下:
public interface Collection<E> extends Iterable<E> {
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();
boolean equals(Object o);
int hashCode();
default Spliterator<E> spliterator() {
return Spliterators.spliterator(this, 0);
}
default Stream<E> stream() {
return StreamSupport.stream(spliterator(), false);
}
default Stream<E> parallelStream() {
return StreamSupport.stream(spliterator(), true);
}
}
它表示一个数据集合,数据间没有位置或顺序的概念;
3.List
List表示有顺序或位置的数据集合,扩展了Collection,增加的主要方法有:
public interface List<E> extends Collection<E> {
E get(int index);
E set(int index, E element);
void add(int index, E element);
E remove(int index);
int indexOf(Object o);
int lastIndexOf(Object o);
ListIterator<E> listIterator();
ListIterator<E> listIterator(int index);
List<E> subList(int fromIndex, int toIndex);
}
它与Collection的最大区别,就是Collection的数据间没有位置或顺序的概念,而List是有顺序或位置的数据集合,这也是为什么List的大多数方法参数中有index下标的原因;
4.RandomAccess;
RandomAccess只是一个标记接口,它的定义中没有任何代码;
实现了RandomAccess接口的类表示可以随机访问,可随机访问就是具备类似数组那样的特性,数据在内存中是连续存放的,根据索引值就可以直接定位到具体的元素,访问效率很高;LinkedList就没有实现这个RandomAccess接口,所以它不具备随机访问的功能;
5.上面介绍完了ArrayList的定义,接下来再看几个ArrayList的几个其他方法,这几个方法比较重要但是容易被忽略;
①两个不常用的构造方法:
public ArrayList(int initialCapacity);
public ArrayList(Collection<? extends E> c);
第一个方法以指定的大小初始化内部的数组大小,在事先知道元素长度的情况下,或者,预先知道长度上限的情况下,使用这个构造方法可以避免重新分配和复制数组;
第二个构造方法以一个已有的Collection构建,数据会新复制一份。
②List转换成数组 :
public Object[] toArray();
public <T> T[] toArray(T[] a);
第一个toArray()方法返回是Object数组,代码为:
public Object[] toArray() {
return Arrays.copyOf(elementData,size);
}
第二个方法返回对应类型的数组,如果参数数组长度足以容纳所有元素,就使用该数组,否则就新建一个数组,比如:
ArrayList<Integer> intList = new ArrayList<Integer>();
intList.add(123);
intList.add(456);
intList.add(789);
Integer[] arrA = new Integer[3];
//arrA的长度为3,intList的长度也为3,那就直接使用arrA
intList.toArray(arrA);
//intList的长度3,new Integer[0]数组的长度为0,,很明显数组的长度不足以容纳intList;
//那intList.toArray()方法内部会重新创建一个长度足以容纳intList的数组,arrB指向该数组
Integer[] arrB = intList.toArray(new Integer[0]);
System.out.println(Arrays.equals(arrA,arrB));
输出为true,表示两种方式都是可以的。
③数组转化成List:
Arrays中有一个静态方法asList可以返回对应的List,如:
Integer[] a = {1, 2, 3};
List<Integer> list = Arrays.asList(a);
需要注意的是,这个方法返回的List,它的实现类并不是ArrayList, 而是Arrays类的一个内部类,在这个内部类的实现中,内部用的数组就是传入的数组,没有拷贝,也不会动态改变大小,所以对数组的修改也会反映到List中,对List调用add、remove方法会抛出异常。
如果要使用ArrayList完整的方法,应该新建一个ArrayList, 如:
List<Integer> list = new ArrayList<Integer>(Arrays.asList(a));
这也是数组转化为List的最典型的做法;
④可以控制内部使用的数组大小的两个public方法:
public void ensureCapacity(int minCapacity);
public void trimToSize();
ensureCapacity()可以确保数组的大小至少为minCapacity,如果不够,会进行扩展,如果已经预知ArrayList需要比较大的容量,调用这个方法可以减少ArrayList内部分配和扩展的次数;
trimToSize()会重新分配一个数组,大小刚好为实际内容的长度,调用这个方法可以节省数组占用的空间;