《Java版数据结构 & 集合类剖析》集合框架的封装设计与顺序表:“从 Iterable 到 ArrayList:集合框架的‘职业树“

《Java版数据结构 & 集合类剖析》集合框架的封装设计与顺序表:"从 Iterable 到 ArrayList:集合框架的'职业树"

前情提要: 本篇先会对集合框架的封装和设计进行初步的理解,其实适合对数据结构有一定基础的读者,但是为了文章的整体安排,我放到了这里进行讲解,如果是数据结构小白可以直接跳转到顺序表的部分

1、集合框架

在具体介绍之前,我们先要对集合框架、集合类、容器,这三个名词进行解释

  • 这三个名词经常进行混在一起用,如果不那么严格来说他们的意思就是集合这一套东西,但严格的来说他们的边界还是有所不同的
  • 容器:只要能装对象的,都算
  • 集合框架:Java 官方设计的那一整套层次分明的接口和实现。
  • 集合类:集合框架里那些真正干活的、可以 new 出来的具体类。
    集合框架的树形关系图如下:

1.1、顶层接口Iterable

如上图所示,Iterable接口就是整个集合框架的顶层,为什么它要作为顶层呢?因为所有的容器本身都需要提供遍历的功能 ,我们不希望每一个不同的容器都以不同的方式来进行遍历,这样就能避免了使用这些容器的学习成本很高的情况。所以集合框架这一套的顶层接口就是Iterable,负责统一迭代器的实现。

如下就是该接口的源码:

java 复制代码
package java.lang;

import java.util.Iterator;
import java.util.Objects;
import java.util.Spliterator;
import java.util.Spliterators;
//支持lambda函数接口
import java.util.function.Consumer;

public interface Iterable<T> {
    Iterator<T> iterator();
    default void forEach(Consumer<? super T> action) {
        Objects.requireNonNull(action);
        //进行遍历操作
        for (T t : this) {
            action.accept(t);
        }
    }
    default Spliterator<T> spliterator() {
        return Spliterators.spliteratorUnknownSize(iterator(), 0);
    }
}

在下面对其进行分段讲解其中的含义:

java 复制代码
public interface Iterable<T> {
		//接口方法
    Iterator<T> iterator();
    //该接口类中的一个方法
    default void forEach(Consumer<? super T> action) {
        Objects.requireNonNull(action); //判断传入的action是否为null
       //进行遍历操作
        for (T t : this) {
            action.accept(t);
        }
    }
 }
  • 如上Iterator iterator(); 是该接口类中的 唯一一个接口方法 ,它不提供具体实现,而是强制所有子类必须实现这个方法,返回一个属于自己的迭代器。
  • default void forEach(Consumer<? super T> action)不需要子类强制重写 ,就能为所有 Iterable 的实现类提供一个通用的遍历操作入口 。它的参数是 Consumer<? super T>,表示接收一个消费型操作只接收数据做处理、不返回结果的行为)。这个方法允许你把"要对每个元素做什么"作为参数传入,而遍历的逻辑已经帮你封装好了。
  • Objects.requireNonNull(action); 该方法判断接收到的参数action是否是null,若为 null 则立即抛出NullPointerException
  • action.accept(t); action 是一个 Consumer 对象,accept 是它的唯一抽象方法,具体这个方法做什么,由调用者通过lambda表达式来提供

科普:函数式接口:有且仅有一个抽象方法的接口(它的实例可以用 Lambda 表达式 或 方法引用 来快速创建,而不必写匿名内部类。)

java 复制代码
default Spliterator<T> spliterator() {
        return Spliterators.spliteratorUnknownSize(iterator(), 0);
    }
  • 如上,spliterator() 是 Iterable 接口的默认方法,提供基础的可分割迭代器,让集合能快速对接 Stream API,这里不对其进行分析

接下来我们在对其源码继续分析:

上面的iterator()这个接口方法中主要有以下两个方法

java 复制代码
boolean hasNext();
E next();

具体的实现逻辑我们以ArrayList中的迭代器来简单了解一下

java 复制代码
private class Itr implements Iterator<E> {
    int cursor;       // 下一个要返回的元素的索引
    int lastRet = -1; // 上一次返回的元素的索引,-1 表示没有

    public boolean hasNext() {
        return cursor != size; // size 是 ArrayList 的元素个数
    }

    public E next() {
        if (cursor >= size)
            throw new NoSuchElementException();
        Object[] elementData = ArrayList.this.elementData; // 内部数组
        if (cursor >= elementData.length)
            throw new ConcurrentModificationException();
        lastRet = cursor;
        return (E) elementData[cursor++]; // 返回当前元素,游标后移
    }

    public void remove() {
        if (lastRet < 0)
            throw new IllegalStateException();
        ArrayList.this.remove(lastRet); // 调用集合自身的 remove
        cursor = lastRet;               // 游标回退
        lastRet = -1;
    }
  • 可以看到迭代器的逻辑是通过控制cursorlastRet这两个成员变量来实现的
  • hasNext() 方法用来判断元素是否遍历结束
  • next() 用来返回当前元素,并将游标推进到下一位
  • remove() 方法用来删除上一次 next() 返回的元素,这个位置由 lastRet 记录

1.2、 Collection接口

下面直接给出该接口的源码

java 复制代码
package java.util;
import java.util.function.Predicate;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
public interface Collection<E> extends Iterable<E> {
    int size();
    boolean isEmpty();
    boolean contains(Object o);
    Iterator<E> iterator();
    Object[] toArray();
    boolean add(E e);
    boolean remove(Object o);
    boolean containsAll(Collection<?> c);
    boolean removeAll(Collection<?> c);
    default boolean removeIf(Predicate<? super E> filter) {
        Objects.requireNonNull(filter);
        boolean removed = false;
        final Iterator<E> each = iterator();
        while (each.hasNext()) {
            if (filter.test(each.next())) {
                each.remove();
                removed = true;
            }
        }
        return removed;
    }
    boolean retainAll(Collection<?> c);
    void clear();
    int hashCode();
    @Override
    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);
    }
}
  • Collection翻译过来就是集合的意思,在上面的一些接口方法中,许多都是由Collection来作为参数接收的,目的就是接收某一个容器对象来进行操作
  • Collection中提供的规范接口方法属于最通用的几个 ,越往下层的接口就越精细化,直到某个类进行具体的实现。
    以下是首先我们需要掌握的Colleciton的接口方法

2、List接口

源码如下:

java 复制代码
package java.util;

import java.util.function.UnaryOperator;
public interface List<E> extends Collection<E> {
    <T> T[] toArray(T[] a);
    boolean addAll(Collection<? extends E> c);
    boolean addAll(int index, Collection<? extends E> c);
    default void replaceAll(UnaryOperator<E> operator) {
        Objects.requireNonNull(operator);
        final ListIterator<E> li = this.listIterator();
        while (li.hasNext()) {
            li.set(operator.apply(li.next()));
        }
    }
    default void sort(Comparator<? super E> c) {
        Object[] a = this.toArray();
        Arrays.sort(a, (Comparator) c);
        ListIterator<E> i = this.listIterator();
        for (Object e : a) {
            i.next();
            i.set((E) e);
        }
    }
    boolean equals(Object o);
    E get(int index);
    E set(int index, E element);
    void add(int index, E element);
    int indexOf(Object o);
    int lastIndexOf(Object o);
    ListIterator<E> listIterator();
    List<E> subList(int fromIndex, int toIndex);
    @Override
    default Spliterator<E> spliterator() {
        return Spliterators.spliterator(this, Spliterator.ORDERED);
    }
}
  • 可以看到List其实比Collection多了添加方法 add 和 addAll 查找方法 get , indexOf , set 等方法,并且支持index下标操作。

这里对List 和 Collection 两个接口进行对比:

  1. Collection是无序 的(默认你的物理空间不连续),不支持索引操作
  2. List中的迭代器为ListIterator ,Collection中的迭代器是普通的iterator
  3. List可以进行排序,所以List接口支持使用sort方法。
  4. 二者的Spliterator操作方式不一样。

3、ArrayList

接下来我会按五个模块讲解ArrayList的相关内容,先对其封装设计 进行分析,然后掌握其常用方法的使用 ,接着进行源码剖析 ,再对其进行模拟实现 、最后总结这类数据结构的特点

3.1、ArrayList封装设计

ArrayList类使用的是顺序表(Sequential List)这个数据结构实现的。

以上是 ArrayList 所继承的类和所实现的接口的图,以下是对这样设计进行分析:

  • AbstractCollection 这个类就是把Collection中的部分简单接口,对与其有关的集合类容器逻辑完全相同的方法先实现了,避免后面重复实现
  • AbstractList类就是更加精细化,在上面这个类的基础上有实现了list接口,还是避免后面具体的类重复实现。
  • RandomAccess 接口相当于急速索引的通行证,众所周知,ArrayList的遍历、尾插的时间复杂度为O(1),其中就是因为,它实现了RandomAccess,支持快速的随机访问,获取元素的速度就是O(1).
  • Serializable 接口提供了序列化标记 ,通俗的说,当我们写完代码,会将它存入硬盘,或者通过网络传输到某个服务器,有些协议必须会让你把代码转为二进制,那么有了这个接口的实现,就相当于给对象安装了一个"打包字节流的开关"
  • Cloneable 接口在前面介绍过,默认支持实现类的浅拷贝。

3.2、ArrayList常用方法

3.2.1、ArrayList的构造
方法 解释
ArrayList() 无参构造
ArayList(Collectino<? extends E> c) 利用其它的容器构造ArrayList
ArrayList(int initialCapacity) 指定顺序表的初始容量
java 复制代码
import java.util.ArrayList;
import java.util.Arrays;

public class Test {
    public static void main(String[] args) {
        // 1. 无参构造
        List<String> list1 = new ArrayList<>();
        list1.add("A");
        list1.add("B");
        System.out.println("无参构造: " + list1);

        // 2. 利用其他集合构造
        ArrayList<String> list2 = new ArrayList<>(list1);
        System.out.println("复制构造: " + list2);

        // 3. 指定初始容量
        ArrayList<Integer> list3 = new ArrayList<>(20);
        list3.add(100);
        System.out.println("指定容量构造(当前大小): " + list3.size());
    }
}

注意:

  • 第二个用容器来构造对象,只能是同样实现了Collection接口的容器才可以,如果是实现了Map接口的容器不能用来构造ArrayList对象,例如HashMap
  • 我们接收该创建好的对象既可以用ArrayList<>,也可以用List<>来接收(因为ArrayList实现了List接口)这两种接收方式带来的也有一些差别:
    • 用ArrayList接收:可以用所有的ArrayList的方法,但是灵活性不高,耦合度高。总的来说只有需要调用ArrayList特有方法,时才考虑用ArrayList<>来进行接收
    • 用List接收:代码灵活度高,方便更换底层实现,耦合度降低,不影响ArrayList的随机访问的特性。总的来说大部分情况下推荐用List来接收

List用来接收的优势具体用代码理解:

  • 面向接口编程,提高代码灵活性
  • 可以轻松更换底层实现:后续若需改用 LinkedList、Vector 或其他 List 实现,只需修改右边 new 的部分,左边无需改动。例如:
java 复制代码
List<String> list = new LinkedList<>();  //只需改这一处
  • 降低耦合,符合依赖倒置原则
  • 方法参数、返回值如果定义为 List 接口,则不依赖具体实现类,调用方可以传入任何 List 实现使代码更通用
java 复制代码
public void process(List<String> list) { ... }  // 好
public void process(ArrayList<String> list) { ... }  // 差,限制了调用方
  • 在Java中,这些容器中的容量大小只能通过反射来得到,正常我们只能获得元素的个数。

3.2.2、ArrayList的其它常见操作
方法 解释
boolean add(E e) 尾插 e
void add(int index, E element) 将 e 插入到 index 位置
boolean addAll(Collection<? extends E> c) 尾插 c 中的元素
E remove(int index) 删除 index 位置元素
boolean remove(Object o) 删除遇到的第一个 o
E get(int index) 获取下标 index 位置元素
E set(int index, E element) 将下标 index 位置元素设置为 element
void clear() 清空
boolean contains(Object o) 判断 o 是否在线性表中
int indexOf(Object o) 返回第一个 o 所在下标
int lastIndexOf(Object o) 返回最后一个 o 的下标
List<E> subList(int fromIndex, int toIndex) 截取部分 list

3.2.3、ArrayList的遍历

ArrayList可以用三种方式遍历:for循环 +下标,foreach、使用迭代器

java 复制代码
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

public class ArrayListTraverseDemo {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        list.add("A");
        list.add("B");
        list.add("C");

        // 1. for循环 + 下标
        System.out.println("for循环 + 下标:");
        for (int i = 0; i < list.size(); i++) {
            System.out.println(list.get(i));
        }

        // 2. foreach(增强for循环)
        System.out.println("\nforeach:");
        for (String s : list) {
            System.out.println(s);
        }

        // 3. 迭代器
        System.out.println("\n迭代器:");
        Iterator<String> it = list.iterator();
        while (it.hasNext()) {
            System.out.println(it.next());
        }
    }
}
  • 以上可以看到,java集合中没有像数组的\[\] 这种下标访问 ,所以当我们在写一些算法题时想用\[\]访问,可以把它转为数组:用String\[\] s1 = list.toArray(new String0) 这种方式转转换(前提是该数组元素不是基本类型,不然只能手动循环转换)
  • 其中我们最常使用的是 for 与 增强for 的迭代方式
  • 增强for的底层就是用迭代器实现的
  • 原生迭代器在删除元素、低效随机访问集合、通用遍历等方面更便捷。在日常开发中,若需要增删或操作复杂逻辑 ,显式使用迭代器是更好的选择。

3.3、ArrayList 源码剖析

这里我们仅仅只对部分源码进行分析,重在掌握其核心功能的思想,其它不重要的有兴趣可自行了解

3.3.1、 ArrayList的核心成员变量:
java 复制代码
private static final Object[] EMPTY_ELEMENTDATA = {};
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}
//真正存放元素的数组
transient Object[] elementData; // non-private to simplify nested class access
private int size;
private static final int DEFAULT_CAPACITY = 10;
  • EMPTY_ELEMENTDATADEFAULTCAPACITY_EMPTY_ELEMENTDATA 这两个静态空数组常量,是用在ArrayList的扩容机制上的,两个都表示初始常量为空 的情况来做标记,分成两种是因为他们为空的两种不同原因,第一个是由指定构造容量为 0 时进行标记(List list1 = new ArrayList<>(0) ),第二个是由默认构造 无任何指定 导致的初始容量为 0 进行标记(List list1 = new ArrayList<>()),具体到后面扩容机制再讲

  • elementData 数组就是真正存放元素的数组,可以看到 它被一个很少见的关键字进行修饰:transient:它的作用是被修饰的字段不参与序列化

    • 为什么不参与序列化呢? 上面讲到的Serializable接口 提供的序列化在该数组的实现上有问题,因为elementData的实际内容长度并不确定,大部分时候该数组后面很多都是空 ,那么直接进行序列化会导致那些空也被序列化,造成各种资源浪费(内存上,时间上,cpu上,网络传输时的带宽上)
    • 因此ArrayList 自己实现了 writeObjectreadObject 方法,只序列化 [0, size) 范围内的有效元素,反序列化时再重新分配数组
  • 最后的那个静态常量是 默认的容量 源码中设为 10


3.3.2、ArrayList的构造函数

三种构造源码如下:

java 复制代码
public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }
public ArrayList(Collection<? extends E> c) {
       Object[] a = c.toArray();
       if ((size = a.length) != 0) {
           if (c.getClass() == ArrayList.class) {
               elementData = a;
           } else {
               elementData = Arrays.copyOf(a, size, Object[].class);
           }
       } else {
           // replace with empty array.
           elementData = 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);
       }
   }
  • 默认构造: 可以看到,直接赋值了上面定义的静态常量数组进行标记,后续的扩容逻辑会用到
  • 容器构造 :可以看到,先将传过来的容器转为数组,看元素是否为空,再判断传入的容器类型和自身的类型是否相同 是否都是ArrayList类 (为了数组运行时类型是否相等,因为ArrayList中的数组运行时类型为Object 而像 HashSet 等容器的数组运行时类型可能为 String 类型) 如果相同则直接赋值,如果不相同则用Arrays.copyOf()方法处理再赋值。
  • 指定构造 :对传入指定的容量进行判断,如果指定为0 则用开头的静态常量进行标记,不为0直接new数组对象即可。
3.3.3、ArrayList的扩容机制

ArrayList的扩容机制在JDK8前后有写区别,这里我按照的是JDK17版本的扩容机制来分析:

  1. 首先增加元素来触发扩容机制,这里以add()方法举例:
java 复制代码
public boolean add(E e) {
        modCount++;
        add(e, elementData, size);
        return true;
    }
  • 注意ArrayList中有一个modCount的成员变量,表示该实例修改的次数。(所有集合中都有modCount这样一个记录修改次数的成员变量),每次增改添加都会增加一次ArrayList修改次数
  1. 私有的重载add方法:
java 复制代码
private void add(E e, Object[] elementData, int s) {
    if (s == elementData.length)   // 判断数组是否已满
        elementData = grow();      // 扩容(无参grow,默认minCapacity = s+1)
    elementData[s] = e;
    size = s + 1;
}
  1. 扩容核心:grow() 方法:
java 复制代码
// 无参版本,默认最小容量为当前size+1
private Object[] grow() {
    return grow(size + 1);
}

// 真正的扩容方法
private Object[] grow(int minCapacity) {
    int oldCapacity = elementData.length;
    if (oldCapacity > 0 || elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        // 情况1:不是首次添加(或已经扩容过)
        int newCapacity = ArraysSupport.newLength(oldCapacity,
                minCapacity - oldCapacity,  // 最小增长量
                oldCapacity >> 1            // 首选增长量(0.5倍)
        );
        return elementData = Arrays.copyOf(elementData, newCapacity);
    } else {
        // 情况2:首次添加,且使用无参构造(elementData是空数组)
        return elementData = new Object[Math.max(DEFAULT_CAPACITY, minCapacity)];
        // DEFAULT_CAPACITY = 10
    }
}
  1. 工具类 ArraysSupport 中的newLength 提供新的容量的计算
java 复制代码
// 返回新数组长度
public static int newLength(int oldLength, int minGrowth, int prefGrowth) {
    int prefLength = oldLength + Math.max(minGrowth, prefGrowth);  // 首选长度
    if (0 < prefLength && prefLength <= MAX_ARRAY_LENGTH) {
        return prefLength;        // 正常情况:1.5倍左右
    } else {
        // 处理超大容量或溢出
        return hugeLength(oldLength, minGrowth);
    }
}
  • 这里的逻辑就是将至少要增加的容量固定的 0.5倍扩容容量 来比较,哪个大就按哪个来算新增加的容量
3.3.4、ArrayList中的SubList:一个轻量的"子列表视图"

SubList中的ArrayList的一个非静态内部类 ,它不存储数据 ,而是作为原列表的某段连续区间的"视图"存在。通过subList(fromIndex, toIndex) 方法返回的就是这个内部类的实例。

为什么称这个叫做视图:

  • 不对数据进行复制:SubList 内部没有自己的数组,而是通过成员变量root(原始ArrayList)、offset(起始偏移量)、size(区间长度)来表示某一段区间
  • 对视图的操作会映射到原列表:对SubList对象的增删改查也会影响到原列表

SubList 的核心成员:

java 复制代码
private final ArrayList<E> root;   // 最底层的原始 ArrayList
private final SubList<E> parent;   // 上一级视图(支持嵌套)
private final int offset;           // 本视图第一个元素在 root 中的绝对索引
private int size;                   // 本视图的元素个数

其余的源码不进行介绍,其它的核心方法的在下面的模拟实现中体现,去除了复杂的设计封装,更易理解于体会。

3.4、ArrayList的模拟实现

以下是整个模拟实现,后面分段进行分析
MyArrayList 类:

java 复制代码
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Objects;

public class MyArrayList<T> implements IList<T>{
    private Object[] elementData;
    static final int DEFAULT_CAPACITY = 10;
    int usedSize = 0;

    public MyArrayList() {
        elementData = new Object[DEFAULT_CAPACITY];
    }
    public MyArrayList(Collection<? extends T> c){
        Object[]a = c.toArray();
        int size = a.length;
        if(size != 0){
            elementData = Arrays.copyOf(a,size,Object[].class);
        }else{
            elementData = new Object[DEFAULT_CAPACITY];
        }
    }
    public MyArrayList(int InitialSize){
        elementData = new Object[InitialSize];
    }

    private void checkPos(int pos){
        if(pos < 0 || pos > usedSize){
            throw new PosIllegal("越界访问");
        }else{
            return;
        }
    }
    private boolean isFull(){
        return usedSize == elementData.length;
    }
    @Override
    public void add(T data) {
        if(isFull()){
            grow();
        }
        elementData[usedSize] = data;
        usedSize++;
    }

    private void grow() {
        int oldCapacity = elementData.length;
        int newCapacity = oldCapacity == 0 ? DEFAULT_CAPACITY : oldCapacity * 2;
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

    @Override
    public void add(int pos, T data) {
        try {
            checkPos(pos);
            if(isFull()){
                grow();
            }
            for (int i = usedSize - 1; i >= pos; i++) {
                elementData[i + 1] = elementData[i];
            }
            elementData[pos] = data;
            usedSize++;
        }catch (PosIllegal e){
            e.printStackTrace();
            throw new IndexOutOfBoundsException("插入位置非法: " + pos);
        }
    }

    @Override
    public boolean contains(T toFind) {
        for (int i = 0; i < usedSize; i++) {
            if (Objects.equals(elementData[i], toFind)) {
                return true;
            }
        }
        return false;
    }

    @Override
    public int indexOf(T toFind) {
        for (int i = 0; i < usedSize; i++) {
            if (Objects.equals(elementData[i], toFind)) {
                return i;
            }
        }
        return -1;
    }
    private void checkPosForAccess(int pos) {
        if (pos < 0 || pos >= usedSize) {
            throw new IndexOutOfBoundsException("访问位置非法: " + pos);
        }
    }
    @Override
    public T get(int pos) {
        try {
            checkPosForAccess(pos);
            return (T) elementData[pos];
        }catch (PosIllegal e){
            e.printStackTrace();
            throw new IndexOutOfBoundsException("获取元素位置非法:" + pos);
        }
    }
    private void setCheckpos(int pos){
        if(pos < 0 || pos >= usedSize){
            throw new PosIllegal("set 位置不合法 "+ pos);
        }
    }
    @Override
    public void set(int pos, T data) {
        try{

            //java中的数组禁止隔空插入数据
            setCheckpos(pos);
            elementData[pos] = data;
            usedSize++;

        }catch (PosIllegal e){
            e.printStackTrace();
            throw new IndexOutOfBoundsException();
        }
    }


    @Override
    public void remove(T del) {
        int aim = -1;
        for (int i = 0; i < usedSize; i++) {
            if(elementData[i].equals(del)){
                aim = i;
                break;
            }
        }
        if(aim != -1){
            for (int i = aim; i < usedSize - 1; i++) {
                elementData[i] = elementData[i + 1];
            }
            usedSize--;
            elementData[usedSize] = null;

        }else{
            return;
        }
    }

    @Override
    public void clear() {
        for (int i = 0; i < usedSize; i++) {
            elementData[i] = null;
        }
        usedSize = 0;
    }

    @Override
    public void display() {
        for (int i = 0; i < usedSize; i++) {
            System.out.println(elementData[i] + " ");
        }
    }

    @Override
    public int size() {
        return usedSize;
    }

    @Override
    public void addFirst(T data) {
        if(isFull()){
            grow();
        }
        for (int i = usedSize; i > 0; i++) {
            elementData[i] = elementData[i - 1];
        }
        elementData[0] = data;
        usedSize++;
    }

    @Override
    public void removeFirst() {
        if (usedSize < 1){
            return;
        }
        for (int i = 0; i < usedSize - 1; i++) {
            elementData[i] = elementData[i + 1];
        }
        elementData[usedSize - 1] = null;
        usedSize--;
    }

    @Override
    public void removeLast() {
        elementData[usedSize - 1] = null;
        usedSize--;
    }
}

IList接口:

java 复制代码
public interface IList<T> {
    //尾插元素

    public void add(T data);

    //指定插入元素
    public void add(int pos,T data);
    //判定是否包含元素
    public boolean contains(T data);
    //查找某个元素对应的位置
    public int indexOf(T toFind);
    //读取某个位置的元素
    public T get(int pos);
    //与数组中某个位置的值进行替换
    public void set(int pos,T data);
    //删除指定元素
    public void remove(T del);
    //清空顺序表
    public void clear();
    //打印顺序表
    public void display();
    //返回元素个数
    public int size();


    //额外拓展 :

    //头插元素
    public void addFirst(T data);
    //头删元素
    public void removeFirst();
    //尾删元素
    public void removeLast();
}
}

PosIllegal类:

java 复制代码
public class PosIllegal extends IndexOutOfBoundsException{
    public PosIllegal(){}
    public PosIllegal(String msg){
        System.out.println(msg);
    }
}

可以看到我上面还写了IList接口,注意实现泛型时接口后面也要跟< T >

java 复制代码
 public class
MyArrayList<T> implements IList<T>{ 	private Object[] elementData;
    static final int DEFAULT_CAPACITY = 10;
    int usedSize = 0;
    }
  • 这三个分别指,存放元素的数组,默认的容量,已被使用的空间个数

构造方法跳过,逻辑和源码差不多,直接看重点:

java 复制代码
 private void checkPos(int pos){
       if(pos < 0 || pos > usedSize){
           throw new PosIllegal("越界访问");
       }else{
           return;
       }
   }
   private boolean isFull(){
       return usedSize == elementData.length;
   }
   @Override
   public void add(T data) {
       if(isFull()){
           grow();
       }
       elementData[usedSize] = data;
       usedSize++;
   }

   private void grow() {
       int oldCapacity = elementData.length;
       int newCapacity = oldCapacity == 0 ? DEFAULT_CAPACITY : oldCapacity * 2;
       elementData = Arrays.copyOf(elementData, newCapacity);
   }

   @Override
   public void add(int pos, T data) {
       try {
           checkPos(pos);
           if(isFull()){
               grow();
           }
           for (int i = usedSize - 1; i >= pos; i++) {
               elementData[i + 1] = elementData[i];
           }
           elementData[pos] = data;
           usedSize++;
       }catch (PosIllegal e){
           e.printStackTrace();
           throw new IndexOutOfBoundsException("插入位置非法: " + pos);
       }
   }
  • 实现数据插入时我们首先要关注两点:插入的位置是否合理, 空间是否足够
  • 在上面的尾插方法中,没有pos指针所以我们只需考虑空间是否足够。
  • isFull()方法判断空间是否已满 来选择是否调用grow()方法开辟内存空间
  • 在java中严格来说是没有手动开辟内存空间的方法的,内存空间的分配其实是由C/C++的本地方法实现的,所以这里只能用数组工具包中的方法Arrays.copyOf()来复制数组 ,顺便给一个新的内存空间长度来实现扩容
  • 在插入完数据要记得更新usedSize 属性
  • 在实现指定位置插入时,除了前面提到的两点,还需要将 pos 之后的所有元素往后移动一位 。这个操作虽简单,但细节很容易出错。分享一个小技巧:
    • 视角一:循环变量 i 表示"要移动的数据 "
      那么 i 从最后一个元素(usedSize - 1)开始,递减到 pos 位置,依次将 elementDatai 赋值给 elementDatai + 1
    • 视角二:循环变量 i 表示"移动后的目标位置 "
      那么 i 从 usedSize 开始,递减到 pos + 1 位置,依次将 elementDatai - 1 赋值给 elementDatai

java 复制代码
 public void clear() {
        for (int i = 0; i < usedSize; i++) {
            elementData[i] = null;
        }
        usedSize = 0;
    }
  • 这里的clear()方法是清理顺序表,将引用置空,方便jvm进行垃圾回收

java 复制代码
 public T get(int pos) {
        try {
            checkPosForAccess(pos);
            return (T) elementData[pos];
        }catch (PosIllegal e){
            e.printStackTrace();
            throw new IndexOutOfBoundsException("获取元素位置非法:" + pos);
        }
    }
    private void setCheckpos(int pos){
        if(pos < 0 || pos >= usedSize){
            throw new PosIllegal("set 位置不合法 "+ pos);
        }
    }
    @Override
    public void set(int pos, T data) {
        try{
            //java中的数组禁止隔空插入数据
            setCheckpos(pos);
            elementData[pos] = data;
            usedSize++;

        }catch (PosIllegal e){
            e.printStackTrace();
            throw new IndexOutOfBoundsException();
        }
    }
  • Java中的集合类没有像C++中的运算符重载机制,所以我们从外部获取某个位置的元素 或者将元素写入某个位置 不能考\[\]符号来进行。因此我们要实现get
    和set 方法:
  • 实现get 方法 首先还是看下标是否合理 ,然后直接用数组下标索引直接返回,没有遍历过程,效率相当高,时间复杂度只有O(1)
  • 实现set方法,我们还是先检查下标合理 ,然后直接数组索引定位进行修改,时间复杂度也为O(1)
  • 由此可见,顺序表这个数据结构""的效率是非常高的

java 复制代码
        int aim = -1;
        for (int i = 0; i < usedSize; i++) {
            if(elementData[i].equals(del)){
                aim = i;
                break;
            }
        }
        if(aim != -1){
            for (int i = aim; i < usedSize - 1; i++) {
                elementData[i] = elementData[i + 1];
            }
            usedSize--;
            elementData[usedSize] = null;

        }else{
            return;
        }
    }
  • 上面是删除数据的实现,可以看到其中有两次遍历,第一是查找是否存在要删除的数据,第二次遍历是覆盖挪动数据。

除了这个删除方法,我还拓展了以下两种java中不提供的删除方法:头删和尾删

java 复制代码
public void
removeFirst() {
       if (usedSize < 1){
           return;
       }
       for (int i = 0; i < usedSize - 1; i++) {
           elementData[i] = elementData[i + 1];
       }
       elementData[usedSize - 1] = null;
       usedSize--;
   }

   @Override
   public void removeLast() {
       elementData[usedSize - 1] = null;
       usedSize--;
   }
  • 可以看到这里两种删除,头删的时间复杂度尾O(N)尾删的时间复杂度为O(1)。明显尾删的效率远远高于头删,因为头删还要挪动数据。

下面我也拓展了头插:

java 复制代码
 public void addFirst(T data) {
       if(isFull()){
           grow();
       }
       for (int i = usedSize; i > 0; i++) {
           elementData[i] = elementData[i - 1];
       }
       elementData[0] = data;
       usedSize++;
   }
  • 可以看到头插的复杂度也高于尾插

3.5、顺序表性质:

3.5.1、基本定义
  • 顺序表 是一种线性表 ,采用一组地址连续的存储单元依次存储数据元素。
  • 逻辑上相邻的元素在物理内存中也相邻(数组实现)。
  • Java 中的典型代表:ArrayList、Vector
3.5.2、顺序表的优点:
  • 随机访问快:通过下标访问元素的时间复杂度为 O(1)。

  • 契合CPU的缓存行机制,CPU会一次性将连续的内存块(缓存行)加载到高速缓存中,顺序表的物理空间连续,所以正好让缓存的命中率变得很高

  • 尾部操作高效:尾插、尾删(均摊 O(1))。

3.5.3、顺序表的缺点
  • 中间/头部操作慢:插入或删除需要移动大量元素,平均 O(n)。

  • 空间浪费:实际元素个数可能远小于数组容量(尤其是频繁扩容后)。

  • 扩容成本高:扩容时需要复制整个数组,且可能造成内存碎片(JVM 中影响较小)。


  • 总的来说顺序表比较适合 "查" 操作比较多的情况,插入和删除操作多的情况,不适合用顺序表
相关推荐
孟华苏1 小时前
怎么快速排查内存泄漏问题
java·开发语言·python
zz34572981131 小时前
C语言中字符串常量存储位置
c语言·开发语言·算法·青少年编程
noipp1 小时前
推荐题目:洛谷 P16510 [GKS 2015 #C] gRanks
java·c语言·开发语言·c++·python·算法
flyinmind1 小时前
Java环境与Android环境中使用QuickJS
java·开发语言·javascript·quickjs
郑洁文1 小时前
基于Python的HTTP服务漏洞信息收集工具设计与实现
开发语言·python·http
不吃鱼的羊1 小时前
DaVinci Developer自动连接
java·开发语言
Evand J1 小时前
【MATLAB例程】VSIMM与IMM在机动目标跟踪中的性能对比,CV+CT双模型
开发语言·matlab·目标跟踪
farerboy1 小时前
15-Java while 和 do...while循环
java·后端
Meteors.1 小时前
Kotlin协程序使用技巧和应用场景
android·开发语言·kotlin