List(ArrayList与LinkedList)

本文探讨了数组的性能,栈内存与堆内存的区别,并深入分析了ArrayList和LinkedList在Java中的实现,包括它们的add、remove、get和set操作。ArrayList适合于频繁的查找,而LinkedList在插入和删除操作上有优势。此外,还介绍了Java中的Collection接口、Iterable接口、可选操作以及List接口的实现。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1 数组

1.1 性能

  • printList打印表:线性时间 O(N);
  • find(x)返回指定位置上的元素:常数时间 O(1);
  • 插入和删除:最坏情况O(N), 最好情况O(1), 平均为线性时间;

所以数组一般通过在高端进行插入操作建成,之后只访问,不进行插入和删除,尤其是在表的前端。

1.2 栈内存与堆内存

新建的数组中保存的实际对象是保存在堆(heap)中的;如果引用该数组的数组引用变量是一个局部变量,那么它被保存在栈(stack)中。例如:

int[] test = new int[100];

test这个引用变量是保存在栈中的,那新建的100int的数据是保存在堆中的。虽然它还没有初始化,但是内存已经分配给它了。

当一个方法执行时,每个方法都会建立自己的内存栈,在这个方法内定义的局部变量都会放入这块内存里,随着方法的执行结束,这个方法的内存栈也将自然销毁。因此,所有在方法中定义的局部变量都是放在栈中的。

但是,程序中被new出来的东西一般都放在堆内存中以便程序反复利用,这个堆内存也叫作【运行时数据区】,这个称号更能表述它的性质不是吗。

堆中的对象是不会随方法的结束而立刻销毁,只要还有引用变量指向它,GC就不会回收。这一点在方法的参数传递中很常见。只有当堆中的对象没有一个引用变量指向它,GC才会在合适的时候回收它。所以在JDK源码中,我们经常看到一些没用了的引用变量被显式地赋值为null,就是为了让GC尽快回收被创建的对象。

Java 数组初始化的两种方法:
- 静态初始化: 程序员在初始化数组时为数组每个元素赋值;
- 动态初始化: 数组初始化时,程序员只指定数组的长度,由系统为每个元素赋初值。

public class ArrayInit {  
    public static void main(String[] args) {  

        //静态初始化数组:方法一  
        String cats[] = new String[] {  
                "Tom","Sam","Mimi"  
        };  

        //静态初始化数组:方法二  
        String dogs[] = {"Jimmy","Gougou","Doggy"};  

        //动态初始化数据  
        String books[] = new String[2];  
        books[0] = "Thinking in Java";  
        books[1] = "Effective Java";  

        System.out.println(cats.length);  
        System.out.println(dogs.length);  
        System.out.println(books.length);  
    }  
}  

典型错误是:

int[] nums = new int[6]{1,2,3,4,5,6};

1.3 多维数组

常规的说法是“Java支持多维数组”,但是,从本质上来讲,根本没用什么多维数组,不过是引用变量数组而已。当然,说法上不用太纠结,你开心就好^_^

这里就强调一下多维数组的新建,两种:

//先新建第一纬度,再新建第二纬度
        int[][] test = new int[4][];
        for(int i=0; i<test.length;i++){
            test[i] = new int[100];
        }
//两个维度同时新建
        int[][] test2 = new int[4][100];
        System.out.println(test2.length);
        System.out.println(test2[0].length);

这里写图片描述

1.4 Arrays

Arrays 在Java 8 的 java.util 包下,包含一些static的可以直接操纵数组的方法:binarySearch、copyOf、copyOfRange、equals、fill、sort、toString。使数组与String越来越像了。

2 简单链表

  • printList和find(x):线性时间
  • remove:遍历到该节点,修改一个next
  • insert:遍历,新建节点,调整两个引用

3 java.util.Collection接口:集合

这里写图片描述

Collection接口扩展了Iterable接口。

4 java.util.Iterable接口

实现了Iterable接口的类可以拥有增强的for循环:
for(AnyType item: coll)
这里写图片描述

5 什么是“可选操作”

简单说就是抽象类的的某些派生类实现里,或者接口的某个实现类里面,某个方法可能是无意义的,调用该方法会抛出一个异常。例如在collection的某些实现类里,里面的元素可能都是只读的,那么add这个接口是无意义的,调用会抛出UnspportedOperationException异常。
从设计的角度说,如果一个接口的方法设计为optional,表示这个方法不是为所有的实现而设定的,而只是为某一类的实现而设定的。

6 List

这里写图片描述
这里写图片描述

对List的实现有两种:
- ArrayList:可增长的数组。因为是数组嘛,get,set效率高,remove和insert代价高;
- LinkedList:双链表。insert和remove的代价相对于数组更高一些,但是平均时间开销也是线性的。更加不建议使用get和set。一般使用LinkedList进行最多的应该是两端的操作,所以它提供了addFirst,removeFirst,addLast,removeLast,以及getFirst,getLast等操作。
为了更好的理解这两个List,接下来会对它们的源码简要分析,然后着重看看add/remove,get/set这四个最常用的方法的源码。

7 ArrayList源码分析

首先,来看看它的成员变量与基本的构造函数,将它就是个数组的屌丝特性暴露无遗:
这里写图片描述

public ArrayList(int initialCapacity) {
        super();
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        this.elementData = new Object[initialCapacity];
    }
public ArrayList() {
        super();
        this.elementData = EMPTY_ELEMENTDATA;
    }

7.1 add

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

上述代码不难看出,add(E e) 是将新元素追加到数组末尾的。

    public void add(int index, E element) {
        rangeCheckForAdd(index);

        ensureCapacityInternal(size + 1);  // Increments modCount!!
        System.arraycopy(elementData, index, elementData, index + 1, size - index);
        elementData[index] = element;
        size++;
    }

这里的void java.lang.System.arraycopy(Object src, int srcPos, Object dest, int destPos, int length)即将数组从index到末尾的元素统一往后移了一位,然后将elementData[index]赋值为指定的数值,也就是典型的数组插入嘛。

7.2 remove

    public E remove(int index) {
        rangeCheck(index);

        modCount++;
        E oldValue = 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;
    }

检查index是否越界,保存要删除元素的值,数组shift,elementData[–size] = null,返回删除的值。

7.3 get

    public E get(int index) {
        rangeCheck(index);

        return elementData(index);
    }

数组取值O(1)爽啊!

7.4 set

    public E set(int index, E element) {
        rangeCheck(index);

        E oldValue = elementData(index);
        elementData[index] = element;
        return oldValue;
    }

数组赋值效率高啊!

8 LinkedList源码分析

首先,我把可以反映LinkedList本质的一些源码贴在一起:

    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;

    public LinkedList() {
    }

    public LinkedList(Collection<? extends E> c) {
        this();
        addAll(c);
    }

看到上面这些代码,我们应该立刻明白,几个特殊情况:

  • 如果list为空则first==null,last==null;
  • 如果list只有一个元素,则first==last==那个Node的指针;
  • 如果一个Node为表头,则node.prev==null;
  • 如果一个Node为表尾,则node.next==null;

再来看看内部有特点的方法,反映了处理双向链表的编程艺术,值得好好看看,而且只有把这些方法弄明白了,就可以把LinkedList理解透彻了。

linkFirst与linkLast需要注意的特殊情况是list是否为空:

 /**
     * Links e as first element.
     */
    private void linkFirst(E e) {
        final Node<E> f = first;
        //newNode prev为null,值为e,next为f
        final Node<E> newNode = new Node<>(null, e, f);
        first = newNode;
        //如果原list为空的话,把last也赋值为newNode;不为空则将原first的prev指向新first
        if (f == null)
            last = newNode;
        else
            f.prev = newNode;
        //时刻注意维护size
        size++;
        modCount++;
    }
    /**
     * Links e as last element.
     */
    void linkLast(E e) {
        final Node<E> l = last;
        //newNode prev为原last,值为e,next为null
        final Node<E> newNode = new Node<>(l, e, null);
        last = newNode;
        //如果为空
        if (l == null)
            first = newNode;
        //如果不为空
        else
            l.next = newNode;
        size++;
        modCount++;
    }

linkBefore需要注意的特殊情况是插入的位置是否在表头:

    /**
     * Inserts element e before non-null Node succ.
     */
    void linkBefore(E e, Node<E> succ) {
        // assert succ != null;
        final Node<E> pred = succ.prev;
        //一步就在succ与succ.prev之间插入了一个新Node,接下来调整引用
        final Node<E> newNode = new Node<>(pred, e, succ);
        succ.prev = newNode;
        //如果插入的位置为表头,则first需要更改
        if (pred == null)
            first = newNode;
        else
            pred.next = newNode;
        size++;
        modCount++;
    }

unlinkFirst需要注意的特殊情况是如果list只有一个Node,则last也需要修改

    /**
     * Unlinks non-null first node f.
     */
    private E unlinkFirst(Node<E> f) {
        // assert f == first && f != null;
        final E element = f.item;
        final Node<E> next = f.next;
        f.item = null;
        f.next = null; // help GC
        first = next;
        if (next == null)
            last = null;
        else
            next.prev = null;
        size--;
        modCount++;
        return element;
    }

unlinkLast需要注意的特殊情况是如果list只有一个元素,则first也需要修改

    /**
     * Unlinks non-null last node l.
     */
    private E unlinkLast(Node<E> l) {
        // assert l == last && l != null;
        final E element = l.item;
        final Node<E> prev = l.prev;
        l.item = null;
        l.prev = null; // help GC
        last = prev;
        if (prev == null)
            first = null;
        else
            prev.next = null;
        size--;
        modCount++;
        return element;
    }

unlink任意节点需要注意的特殊情况是这个节点在两端。一般的思想可能是如果是首节点,写一段代码;是尾节点写一段代码;在中间写一段代码。但这里的思路是以要操纵的节点为中心,考虑它的前一个节点后后一个节点的情况。代码量减少,思路更清晰,这样的思路更具有普遍意义。

    /**
     * Unlinks non-null node x.
     */
    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;
    }

然后来看看add/remove,get/set是怎么实现的,与ArrayList有什么区别。

8.1 add

    public boolean add(E e) {
        linkLast(e);
        return true;
    }

空间add就是添加在list的末尾。

8.2 remove

    public boolean remove(Object o) {
        //哈哈,果然源码是最准确的,原来remove(null)都可以,我才知道!!!
        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;
    }

//移除首节点除外
public E remove() {
        return removeFirst();
    }

8.3 get

如果index在前半段,从first开始遍历;如果在后半段,从last开始遍历

    /**
     * Returns the (non-null) Node at the specified element index.
     */
    Node<E> node(int index) {
        // assert isElementIndex(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;
        }
    }

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

8.4 set

    public E set(int index, E element) {
        checkElementIndex(index);
        Node<E> x = node(index);
        E oldVal = x.item;
        x.item = element;
        return oldVal;
    }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值