java容器(1):ArrayList以及迭代器

前言:前面几篇博客简单的叙述了几个常用算法的思想,现在,要详细的分析它们;

在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()会重新分配一个数组,大小刚好为实际内容的长度,调用这个方法可以节省数组占用的空间;

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

renshuguo123723

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值