List、ArrayList、LinkedList、Vector、Stack详解

先上图
这里写图片描述


图里面的框框,实线框代表类,小点点的框代表接口,虚线框代表抽象类

先看三个接口,Iterable、Collection、List三个接口,选择先说这个,是因为图中的子类都直接或间接实现了这三个接口。在这里只说一些主要的方法,不会把全部的方法都列举出来

1.Iterable、Collection、List

Iterable接口,在刚学习Java的时候应该都接触过这个接口,迭代器,主要是遍历数据时候用的

//获取迭代器
Iterator<T> iterator();
//遍历集合元素
default void forEach(Consumer<? super T> action) {
        Objects.requireNonNull(action);
        for (T t : this) {
            action.accept(t);
        }
    }

第二个方法的default关键字是在,JDK8才有的,就是为了接口可以提供一个默认的实现。Consumer可以理解是一个帮助接口,给用户提供一个accept方法。如果你懒得写foreach,可以直接就这个方法,比如

List<String> list = new ArrayList<>();
list.add("sun");
list.add("xiao");
list.add("bing");
list.forEach(new Consumer<String>() {
    @Override
    public void accept(String s) {
        System.out.println(s);
    }
});

这段代码将会依次打印出集合的数据
Collection接口和List接口方法都差不多

2.ArrayList

ArrayList继承于AbstractList抽象类,AbstractList又继承于AbstractCollection抽象类,好吧,那就先说AbstractCollection吧

就像AbstractCollection的名字一样,它只是简单的实现了Collection的几个方法
看一下常用的几个方法
isEmpty、contains

public boolean isEmpty() {
        return size() == 0;
}

很easy

public boolean contains(Object o) {
        Iterator<E> it = iterator();
        if (o==null) {
            while (it.hasNext())
                if (it.next()==null)
                    return true;
        } else {
            while (it.hasNext())
                if (o.equals(it.next()))
                    return true;
        }
        return false;
}

这个方法的实现我看的时候吃了一鲸,还会判断对象为null的情况,后来我试了一下,还真的返回true

AbstractList也是一样,对List接口简单的实现了几个方法,这几个方法,ArrayList基本都会重写,所以就不列出来了,这个类比较重要的就是实现了List的listIterator方法,这个方法返回一个实现了ListIterator的ListItr类,这个类下面用的的时候在说吧

ArrayList集合的实现是通过类的内部维护一个数组,初始化是数组大小是10,可以通过构造方法指定数组大小
先看一下常用的add方法

public boolean add(E e) {
        ensureCapacityInternal(size + 1);
        elementData[size++] = e;
        return true;
}

ensureCapacityInternal方法会判断如果需要会增加数组的大小,elementData就是ArrayList内部维护的数组,size就是当前数据的个数,看一下是如何增长数组大小的

private static final int DEFAULT_CAPACITY = 10;
private static final Object[] EMPTY_ELEMENTDATA = {};
private void ensureCapacityInternal(int minCapacity) {
        if (elementData == EMPTY_ELEMENTDATA) {
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
        }

        ensureExplicitCapacity(minCapacity);
}

继续看ensureExplicitCapacity

private void ensureExplicitCapacity(int minCapacity) {
        modCount++;

        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
}

modCount后面在细说,它的作用是,如果有多个线程操作,则会抛出异常。
判断如果minCapacity大于当前内部数组的大小责调用grow方法

private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
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);
}

这段代码也挺好理解的,就是每次分配当前数组大小的一半大小,这就是第一个if判断的,上面传过来的是size+1,肯定不能每添加一个数据就扩展一次数组。第二个方法判断是否还有剩余的可分配空间,然后调用hugeCapacity方法。最后通过Arrays.copyOf扩展数组。
先看一下hugeCapacity方法,然后再看Arrays.copyOf方法是如何扩展数组的

private static int hugeCapacity(int minCapacity) {
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
        return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE :
            MAX_ARRAY_SIZE;
}

没什么可说的,主要就是如果溢出了会抛一个异常

public static <T> T[] copyOf(T[] original, int newLength) {
        return (T[]) copyOf(original, newLength, original.getClass());
}
public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
        T[] copy = ((Object)newType == (Object)Object[].class)
            ? (T[]) new Object[newLength]
            : (T[]) Array.newInstance(newType.getComponentType(), newLength);
        System.arraycopy(original, 0, copy, 0,
                         Math.min(original.length, newLength));
        return copy;
}

这里是关键,在这里看到,调用Array.newInstance方法根据新的大小创建了一个新的数组,然后把以前的数据拷贝到新的数组里面

public static native void arraycopy(Object src,  int  srcPos,
                                        Object dest, int destPos,
                                        int length);

最终调用的了System类的arraycopy方法,写成本地的方法,可想而知,如果数据越来越多的话,当然还是用指针和汇编的速度比较快。
src是源数组、是从原数组的srcPos位置开始复制
dest是目标数组、是从destPos开始赋值
length复制的个数
还有一点特别重要,arraycopy复制的是地址。假如有两个数组,把数组一复制到数组二,那么当数组一的值改变是,数组二也会跟着改变
就到这里了jvm代码就不分析了就兴趣的可以看看这个
http://www.360doc.com/content/14/0713/19/1073512_394157835.shtml

String[] strs1 = new String[]{"1", "2", "3"};
String[] strs2 = new String[]{"4", "5", "6"};

System.arraycopy(strs1, 1, strs2, 1, 2);
strs2[1] = "jianpan";
for (String s : strs2){
    System.out.println(s);
}

猜猜这段代码会输出什么?

ArrayList的add方法到这里就结束了,基本就是判断内部数组是否有空余,如果没有就在分配,分配的大小是当前数组的一半,分配的方式是通过创建新的数据并把以前的数据复制过来,最后把add的参数设置到数组里面

在看一下常用的remove方法

如果我们在写这个ArrayList,我们怎么才能删除这个数组任意位置的元素呢?用ArrayList的时候可能没想过,它是怎么删除的,反正我们直接掉个remove方法就把元素删除了。
remove还是通过刚才讲的System.arraycopy方法

public E remove(int index) {
    if (index >= size)
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));

    modCount++;
    E oldValue = (E) elementData[index];

    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

    return oldValue;
}

如果理解了System.arraycopy方法,那么看这段代码应该很easy

ArrayList的方法就说这些了,知道了add和remove方法想去看其他方法的源码应该不是很难了。ArrayList还有一个线程操作问题,如果有多个线程同时对ArrayList执行添加操作,那么就可能出现,一个线程刚好给内部数组最后一个元素赋值,而恰巧另一个线程给下一个元素赋值,结果就会抛个异常了。如果对线程有同步要求的话,可以通过这种方式

List list = Collections.synchronizedList(new ArrayList(...));

还有就是上面说的modCount字段的同步问题,这个字段在ArrayList内部实现的迭代器里面的方法才会用到,如果你不用这个迭代器的话,那么不关注这个也行,看一小段代码吧

if (modCount != expectedModCount)
      throw new ConcurrentModificationException();

恩,在内部的迭代器就是这么用的

3.LinkedList

LinkedList的内部实现和ArrayList区别还是挺大的。LinkedList是通过在内部维护一个链表来实现列表的,以前是不是都听过LinkedList添加快,查询慢接下来就看看为什么,我们就从add、remove、get方法入手。而LinkedList还实现了Deque接口的方法,如addFirst、addLast等方法,LinkedList的这些方法的实现都是操作内部的链表,就不重复讲了,看完add、remove、get方法会发现其实都差不多

先看一个内部类

private static class Node<E> {
        E item;
        Node<E> next;
        Node<E> prev;

        Node(Node<E> prev, E element, Node<E> next) {
            this.item = element;
            this.next = next;
            this.prev = prev;
        }
}

就是一个普通的链表,只不过我们一般写这个,不会加上泛型

还有几个关键的字段

transient int size = 0;
transient Node<E> first;
transient Node<E> last;

一看名字就知道了,内部链表的大小,链表头,链表尾
现在可以看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 = newNode;
        else
            l.next = newNode;
        size++;
        modCount++;
}

比ArrayList的add方法简单好多
没什么可说滴,知道链表,看这段代码就很easy

再看remove方法

public E removeFirst() {
        final Node<E> f = first;
        if (f == null)
            throw new NoSuchElementException();
        return unlinkFirst(f);
}
public boolean remove(Object o) {
        if (o == null) {
            for (Node<E> x = first; x != null; x = x.next) {
                if (x.item == null) {
                    unlink(x);
                    return true;
                }
            }
        } else {
            for (Node<E> x = first; x != null; x = x.next) {
                if (o.equals(x.item)) {
                    unlink(x);
                    return true;
                }
            }
        }
        return false;
}

通过这里就可以看出来查询慢了,当我要删除一个数据的时候,就需要遍历链表然后调用unlink方法删除。如果我们就1w条数据时,删除的恰巧是最后一个,那么就要遍历所有。

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;
}

很简单,只不过需要先知道链表,链表应该都会的吧~

这么快就最后一个方法了get

public E get(int index) {
        checkElementIndex(index);
        return node(index).item;
}

首先检查是否越界,然后调用node方法获取Node对象,然后获取对象的item元素
先看下是怎么判断的,然后在看node方法是怎么获取Node对象的

private void checkElementIndex(int index) {
        if (!isElementIndex(index))
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
private boolean isElementIndex(int index) {
        return index >= 0 && index < size;
}
Node<E> node(int 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小于当前链表大小的一半,就从first开始查,如果大于一半就从last开始查。相当于ArrayList的get方法这个可是慢多了,ArrayList直接就获取了数组的元素

public E get(int index) {
        if (index >= size)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));

        return (E) elementData[index];
}

到这里LinkedList就结束了,留个小问题,你知道LinkedList的get方法的时间复杂度是什么嘛?欢迎留言~

3.Vector

Vector的实现可以说和ArrayList一样,没太大区别,就是Vector操作元素的方法都是线程同步的,所以这个就不多说了

4.Stack

Stack继承于Vector类,所以Stack类的内部也是维护一个数组,听名字就知道是栈,所以必定会提供入栈和出栈方法。入栈方法push和出栈方法pop,都很简单,先看push方法然后再看pop方法

public E push(E item) {
        addElement(item);

        return item;
}

push方法只是添加一个元素,所以这里就调用的父类的addElement方法了

public synchronized void addElement(E obj) {
        modCount++;
        ensureCapacityHelper(elementCount + 1);
        elementData[elementCount++] = obj;
}

跟ArrayList的add方法一样,不多说

pop方法

public synchronized E pop() {
    E       obj;
    int     len = size();

    obj = peek();
    removeElementAt(len - 1);

    return obj;
}

先通过内部的peek方法获取最后一个对象,也就是栈低的元素,然后调用removeElementAt方法删除栈低元素,先看peek方法,然后再看removeElementAt方法

public synchronized E peek() {
        int     len = size();

        if (len == 0)
            throw new EmptyStackException();
        return elementAt(len - 1);
}
public synchronized E firstElement() {
        if (elementCount == 0) {
            throw new NoSuchElementException();
        }
        return elementData(0);
}
E elementData(int index) {
        return (E) elementData[index];
}

最后其实就是根据index获取内部数组的元素

removeElementAt方法

public synchronized void removeElementAt(int index) {
    modCount++;
    if (index >= elementCount) {
        throw new ArrayIndexOutOfBoundsException(index + " >= " +
                                                 elementCount);
    }
    else if (index < 0) {
        throw new ArrayIndexOutOfBoundsException(index);
    }
    int j = elementCount - index - 1;
    if (j > 0) {
        System.arraycopy(elementData, index + 1, elementData, index, j);
    }
    elementCount--;
    elementData[elementCount] = null; /* to let gc do its work */
}

跟ArrayList的方法差不多,也是通过System.arraycopy方法

恩到这里就结束咯

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
【优质项目推荐】 1、项目代码均经过严格本地测试,运行OK,确保功能稳定后才上传平台。可放心下载并立即投入使用,若遇到任何使用问题,随时欢迎私信反馈与沟通,博主会第一时间回复。 2、项目适用于计算机相关专业(如计科、信息安全、数据科学、人工智能、通信、物联网、自动化、电子信息等)的在校学生、专业教师,或企业员工,小白入门等都适用。 3、该项目不仅具有很高的学习借鉴价值,对于初学者来说,也是入门进阶的绝佳选择;当然也可以直接用于 毕设、课设、期末大作业或项目初期立项演示等。 3、开放创新:如果您有一定基础,且热爱探索钻研,可以在此代码基础上二次开发,进行修改、扩展,创造出属于自己的独特应用。 欢迎下载使用优质资源!欢迎借鉴使用,并欢迎学习交流,共同探索编程的无穷魅力! 基于业务逻辑生成特征变量python实现源码+数据集+超详细注释.zip基于业务逻辑生成特征变量python实现源码+数据集+超详细注释.zip基于业务逻辑生成特征变量python实现源码+数据集+超详细注释.zip基于业务逻辑生成特征变量python实现源码+数据集+超详细注释.zip基于业务逻辑生成特征变量python实现源码+数据集+超详细注释.zip基于业务逻辑生成特征变量python实现源码+数据集+超详细注释.zip基于业务逻辑生成特征变量python实现源码+数据集+超详细注释.zip 基于业务逻辑生成特征变量python实现源码+数据集+超详细注释.zip 基于业务逻辑生成特征变量python实现源码+数据集+超详细注释.zip
提供的源码资源涵盖了安卓应用、小程序、Python应用和Java应用等多个领域,每个领域都包含了丰富的实例和项目。这些源码都是基于各自平台的最新技术和标准编写,确保了在对应环境下能够无缝运行。同时,源码中配备了详细的注释和文档,帮助用户快速理解代码结构和实现逻辑。 适用人群: 这些源码资源特别适合大学生群体。无论你是计算机相关专业的学生,还是对其他领域编程感兴趣的学生,这些资源都能为你提供宝贵的学习和实践机会。通过学习和运行这些源码,你可以掌握各平台开发的基础知识,提升编程能力和项目实战经验。 使用场景及目标: 在学习阶段,你可以利用这些源码资源进行课程实践、课外项目或毕业设计。通过分析和运行源码,你将深入了解各平台开发的技术细节和最佳实践,逐步培养起自己的项目开发和问题解决能力。此外,在求职或创业过程中,具备跨平台开发能力的大学生将更具竞争力。 其他说明: 为了确保源码资源的可运行性和易用性,特别注意了以下几点:首先,每份源码都提供了详细的运行环境和依赖说明,确保用户能够轻松搭建起开发环境;其次,源码中的注释和文档都非常完善,方便用户快速上手和理解代码;最后,我会定期更新这些源码资源,以适应各平台技术的最新发展和市场需求。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值