ArrayList的实现

本节给出的实现代码,将不提供上层接口的实现,是独立的一段代码。

一、实现细节

  1. MyArrayList将保持基础数组,数组的容量,以及存储在MyArrayList中的当前项数
  2. MyArrayList将提供改变基础数组容量的机制,通过获得新数组并将老数组内容拷贝,实现变长扩容
  3. MyArrayList将提供get、set方法实现
  4. MyArrayList将提供size、isEmpty等功能,还提供remove和可扩容的add
  5. MyArrayList将提供一个实现Iterator接口的类,并提供对应方法

二、实现代码

package ColletionsAPI.ListInterface;

import java.util.Iterator;
import java.util.NoSuchElementException;

public class MyArrayList<T> implements Iterable<T>{
    //数组默认长度
    private static final int DEFAULT_CAPACITY = 10;

    //设置表长、数组参数
    private int theSize;
    private T [] theItems;

    //构造方法,构造时保证为空表
    public MyArrayList(){
        clear();
    }

    //清空表,并重新设置数组长度
    private void clear() {
        theSize = 0;
        ensureCapacity(DEFAULT_CAPACITY);
    }

    //获取表长度
    public int size(){
        return theSize;
    }
    //判断是否为空表
    public boolean isEmpty(){
        return size()==0;
    }
    //修改数组容量为当前表长
    public void trimToSize(){
        ensureCapacity(size());
    }

    //获得指定索引处元素
    public T get(int index){
        if (index < 0 || index >= size() ){
            throw new ArrayIndexOutOfBoundsException();
        }else {
            return theItems[index];
        }
    }

    //将指定索引处的元素重新赋值,并返回赋值前的元素
    public T set(int index, T newVal){
        if (index < 0 || index >= size()){
            throw new ArrayIndexOutOfBoundsException();
        }else {
            T old = theItems[index];
            theItems[index] = newVal;
            return old;
        }
    }


    //设置数组长度
    private void ensureCapacity(int newCapacity) {
        if (newCapacity < theSize){
            //新长度小于表长,返回
            return;
        }else {
            //创建新数组
            T[] old = this.theItems;
            theItems = (T[]) new Object[newCapacity];
            //遍历表,并把原数据重新赋值给新数组
            for (int i = 0 ; i < size() ; i++){
                theItems[i] = old[i];
            }
        }
    }

    //表尾添加
    public boolean add(T x){
        //调用指定位置添加
        add(size(),x);
        return true;
    }

    //指定位置添加
    private void add(int index, T x) {
        if (theItems.length == size()){
            //如果当前表容量已满,设置新数组长度为原来的2倍+1,并
            ensureCapacity(size() * 2 + 1);
        }else {
            //将原索引后的元素全部后移一位
            for (int i = theSize ; i < index ; i--){
                theItems[i] = theItems[i-1];
            }
            //在指定索引处添加元素
            theItems[index] = x;

            //表长自增
            theSize++;
        }

    }

    //删除指定索引处元素
    public T remove(int index){
        //获得指定索引处元素
        T removeItem = this.theItems[index];
        //将指定索引之后的全部元素前移一位
        for (int i = index ; i < size()-1 ; i++){
            theItems[i] = theItems[i+1];
        }
        //表长自减
        theSize--;
        //返回被删除的元素值
        return removeItem;
    }

 
    /*实现迭代器*/
    //存储当前位置的概念
    @Override
    public Iterator<T> iterator() {
        return new ArrayListIterator();
    }

    //迭代器
    private class ArrayListIterator implements Iterator<T>{
        //要被查看的下一个元素的索引,起始为0
        private int current = 0;
        
        //判断是否有下个元素
        @Override
        public boolean hasNext() {
            return size() < current;
        }

        
        //获得下一个元素
        @Override
        public T next() {
            if (!hasNext()){
                throw new NoSuchElementException();
            }
            return theItems[current++];
        }

       //删除元素
        @Override
        public void remove() {
            //因为current给出的是下一个元素索引,所以先自减
            MyArrayList.this.remove(--current);
        }
    }
}

这里给出的注释已经比较充分,不再做详细赘述。

值得注意的是ArrayList的底层是使用数组存储,数组的长度和表长并非一个概念;另外,迭代器中的current变量提供的是下一个访问索引,而非当前索引。

三、迭代器代码分析

上述代码中,相信表的实现部分应该较好理解,然而关于迭代器的部分可能会有读者进行不同的实现尝试。首先,我们把上述代码中的迭代器部分取出。

    //迭代器
    private class ArrayListIterator implements Iterator<T>{
        //要被查看的下一个元素的索引,起始为0
        private int current = 0;
        
        //判断是否有下个元素
        @Override
        public boolean hasNext() {
            return size() < current;
        }

        
        //获得下一个元素
        @Override
        public T next() {
            if (!hasNext()){
                throw new NoSuchElementException();
            }
            return theItems[current++];
        }

       //删除元素
        @Override
        public void remove() {
            //因为current给出的是下一个元素索引,所以先自减
            MyArrayList.this.remove(--current);
        }
    }

相信,在看到迭代器的实现代码时,肯定有读者会产生尝试另外创建一个类来实现的想法。笔者一开始,也是试图进行这样的实现,但是在实现过程中发现了不妥。


当我们直接另外创建一个类来实现迭代器时,迭代器会因为不能使用theItem和size()而出现错误。这是理所当然的,因为它们并不是迭代器类的一部分,这时编译器会直接报错。

对于上述问题有一个很常见的解决方案,将MyArrayList作为成员变量放入迭代器类中。这样的做法看似解决了问题,编译器不在有错误提示,但是这种方案仍有隐患。

因为theItem在MyArrayList中是私有的,而因为不是同一个类,所以在访问私有域时必然是非法的。

解决可见性问题的最简单方法就是直接修改权限修饰符,我们可以将private改为public或使用默认。

虽然这时迭代器已经可以正常使用了,但是,使用这种做法却大大降低了数据的安全性。良好的面向对象编程的基本原则之一就是,要求数据尽可能的隐蔽。

四、嵌套类和内部类

通过上述的分析,可以肯定,将迭代器类单独实现是不太可能的,这时就要考虑到嵌套了和内部类了。

1)嵌套类

我们通过将迭代器类放入 MyArrayList 类的内部,使它成为一个嵌套类,相应的
MyArrayList 就被称为外部类

 @Override
    public Iterator<T> iterator() {
        return new ArrayListIterator<T>();
    }


    //迭代器
    private class ArrayListIterator<T> implements Iterator<T>{
        //要被查看的下一个元素的索引,起始为0
        private int current = 0;
        //将MyArrayList<T>做成员变量
        private MyArrayList<T> theList;

        //判断是否有下个元素
        @Override
        public boolean hasNext() {
            return theList.size() < current;
        }


        //获得下一个元素
        @Override
        public T next() {
            if (!hasNext()){
                throw new NoSuchElementException();
            }
            return theList.theItems[current++];
        }

        //删除元素
        @Override
        public void remove() {
            //因为current给出的是下一个元素索引,所以先自减
            MyArrayList.this.remove(--current);
        }
    }

注意对比这段代码与上一节的代码,这里的迭代器类被修饰为static,并且通过将MyArrayList作为成员变量来使用MyArrayList中的私有属性。

嵌套类的问题在于,当编写theItems而不引用其所在的MyArrayList类时,代码看起来还可以,但却是无效的,因为编译器不可能计算出哪个MyArrayList在被引用。为此,我们要自己声明引用了哪个类。

2)内部类

当声明一个内部类时,编译器会添加对外部类对象的一个隐式引用,该对象引起内部类对象的构造。例如,外部类名为Outer,则隐式引用为Outer.this。

当我们的迭代器类作为一个内部类而没有被static修饰时,那么MyArrayList.this和theList就都会引用同一个MyArrayList。这样我们就不必再声明MyArrayList作为成员变量。代码就回归为最初的版本。

发表评论

邮箱地址不会被公开。 必填项已用*标注