Java(Android)数据结构汇总(一)-- List(上)

传送门:Java(Android)数据结构汇总 -- 总纲

简介

Collection接口下面有三个扩展接口:ListSetQueue。它们在Collection的基础上增加了一些用来实现自己特性的一些方法。

List:可存放重复元素
Set:不可以存放重复元素
Queue:则实现了队列(FIFO)相关的功能

List接口的实现类

这篇文章我们先来整理在java.util包下基于List接口实现的数据结构类,主要有ArrayListLinkedListVectorStack四个。

一、ArrayList

ArrayList可以说是java中最常用也是我们接触最早的一种数据结构了。它内部是用数组实现的。数组的优点是存储和随机访问效率最高,而缺点有以下两个:

  1. 数组长度是固定的,无法动态扩容;
  2. 如果在数组中间插入/删除元素,则需要手动将该位置之后的元素进行集体后移/前移。

ArrayList的主要功能就是为了解决这两个缺点,对涉及到的操作进行了封装,实现了自动化。使其在使用上比数组更加简单灵活。

实现原理

举个例子,某个ArrayList对象内部数组的长度为10且已装满,这时如果在第5个位置添加一个元素,则步骤如下:

  1. 在内存中新建一个长度为15(每次扩容是增加原来容量的一半)的新数组;
  2. 将原来数组中的所有元素复制到新数组中;
  3. 将新数组中第5个位置到第10个位置的元素复制到第6个位置到第11个位置上(集体后移一个位置);
  4. 将第5个位置的值修改为新添加的值。

我们来看看源码(再次提醒:本文所有源码都是基于java1.8版本(Android API 27)):

public void add(int index, E element) {      
    // index是要插入的位置,size是当前元素的数量
    // index < 0:很简单,数组的下标是不能为负数的
    // index > size:表明数组中的元素必须挨着存放,不能有间隔
    if (index > size || index < 0)
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));

    // size是当前存储的元素数量
    // ensureCapacityInternal方法用来确保当前数组大小不能小于size + 1,如果小于这个值就进行扩容
    ensureCapacityInternal(size + 1);  // Increments modCount!!

    // 将插入位置和之后的元素都集体后移一位
    System.arraycopy(elementData, index, elementData, index + 1, size - index);

    // 在指定位置插入新元素
    elementData[index] = element;
    size++;
}
...
private void ensureCapacityInternal(int minCapacity) {
    // elementData就是ArrayList内部实现的数组
    // 检查数组是否是空数组,当新建ArrayList时如果没有指定容量,则elementData将是一个0长度的数组,
    // 当第一次添加元素的时候才真正为数组分配空间
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        // 确保数组的容量不能小于最小值DEFAULT_CAPACITY
        minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
    }

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

    //  如果需要的最小容量比当前数组容量大,说明数组容量不够用了,则进行扩容操作
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}
...
private void grow(int minCapacity) 
    // overflow-conscious code
    int oldCapacity = elementData.length;
    // 新的容量为当前容量的1.5倍(oldCapacity >> 1等价于oldCapacity除2)
    int newCapacity = oldCapacity + (oldCapacity >> 1);

    // 一些边界检查
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);

    // 新建一个newCapacity长度的数组,并将elementData里面的元素复制到新数组中,
    // 最后将返回的新数组重新赋值给elementData
    elementData = Arrays.copyOf(elementData, newCapacity);
}

上面就是ArrayList的核心实现了。主要是对数组的操作进行了封装,让我们在使用上变的简单。

由于数组需要的一个连续的内存空间,所以在内存的利用率上不是很高。

因为随着内存中对象的不停创建和释放,会导致大量的内存碎片(不连续的内存空间),比如GC的标记-清除算法就会让这个问题特别明显,虽然GC还有其他算法可以减少了内存碎片,但却无法杜绝。

对于那些容量比ArrayList中数组需要的容量小的内存碎片就无法被ArrayList使用。而如果当此时没有其他足够的可用空间时就会出现OOM,但其实此时的可用内存容量的总和是大于需求内存容量的。

二、LinkedList

LinkedList内部使用的双向链表来实现的。链表的最大特点就是添加/删除元素很快。只需要修改前后节点的节点指针(引用)即可,不需要像数组那样还需要移动元素。

相关源码如下:

public void add(int index, E element) {
    // 边界检查
    checkPositionIndex(index);

    if (index == size)
        // 将元素添加到链表结尾
        linkLast(element);
    else
        // 将元素添加到链表中间,node(index)的作用是查找index位置的节点元素
        linkBefore(element, node(index));
}
...
void linkLast(E e) {
    // first记录的是链表当前的第一个节点
    // last记录的是链表当前的最后一个节点
    final Node<E> l = last;
    // 将要添加的元素封装成一个Node对象,并在构造方法里面将prev指向last且next设置为null,
    // 表示让自己作为链表的最后一个元素
    final Node<E> newNode = new Node<>(l, e, null);
    // 将last重新指向这个新增的节点
    last = newNode;

    if (l == null)
        // 如果当前链表是一个空的(还没有元素),则该节点直接作为第一个节点
        first = newNode;
    else
       // 让开始的最后一个节点指向该节点
        l.next = newNode;

    size++;
    modCount++;
}
...
Node<E> node(int index) {
    // 判断index是否超过总大小的一半,没有超过则从前面开始遍历,否则从后面开始遍历,主要是为了减少遍历次数
    if (index < (size >> 1)) {
        // 从链表头部开始遍历,找到index位置的节点
        Node<E> x = first;
        for (int i = 0; i < index; i++)
             x = x.next;
        return x;
    } else {
        // 从链表结尾开始遍历,找到index位置的节点
        Node<E> x = last;
        for (int i = size - 1; i > index; i--)
            x = x.prev;
        return x;
    }
}
...
// 将e元素插入到succ节点的前面
void linkBefore(E e, Node<E> succ) {
    // assert succ != null;
    final Node<E> pred = succ.prev;
    // 将e封装成一个Node,它的prev指向succ的前一个节点,它的next指向succ
    final Node<E> newNode = new Node<>(pred, e, succ);
    // 修改succ的前节点指向:将succ的prev指向这个新的节点
    succ.prev = newNode;
 
    // 修改pred节点的next指向
    if (pred == null)
        // 如果pred为空,表示新元素是插入到链表头部的,则直接使用first指向这个新节点
        first = newNode;
    else
        // 修改pred的后节点指向:将pred的next 指向这个新的节点
        pred.next = newNode;

    size++;
    modCount++;
}

可见,插入元素时只修改了和该位置相邻的节点,重新修改了它们的prevnext指向,其他节点没有任何改动,所以这点比ArrayList要快。

由于LinkedList的每个元素都需要一个额外的节点对象来进行包装,所以在内存的占用上要大一些:需要额外的空间来存储这个对象。

总结

将ArrayList和LinkedList做一个对比来看下它们各自的特点和性能。

特点:

内部实现 内存占用 内存利用率 线程安全否
ArrayList 数组
LinkedList 双向链表

除了上述特点之外,他们还有个特点就是:LinkedList占用的内存会随着元素的增多而增多,也会随着元素的减少也减少。但是ArrayList只会随着元素的增多而增多,不会随着元素的减少而减少。也就是说ArrayList的占用的空间只会增大,不会降低(数组只进行了扩容,没有进行收缩)。

方法性能:

add(e) add(index, e) remove(index) remove(object) get(index)
ArrayList
LinkedList

当ArrayList不需要扩容的时候,它的add(e)方法比LinkedList的块,否则就会慢于LinkedList(扩容会涉及到新建数组、元素复制等)。
add(index, e)remove(index)操作的是最后一个位置时(不需要扩容时),ArrayList比LinkedList快。

三、Vector

Vector结构和ArrayList基本一致,区别是Vector的方法都实现了同步,所以Vector是线程安全的

public synchronized E lastElement() {
    ...
}
...
public synchronized E firstElement() {
    ...
}
...
public synchronized E elementAt(int index) {
    ...
}
...
 public synchronized int indexOf(Object o, int index) 
    ...
}
...
public synchronized void setElementAt(E obj, int index) {
    ...
}
...
public synchronized void removeElementAt(int index) {
    ...
}
...
public synchronized void insertElementAt(E obj, int index) {
    ...
}
...

可以看到,那些涉及到访问内部数组的方法都加上了synchronized关键之来实现了同步。但同步必然会带来性能上的开销,所以Vector在性能上比ArrayList要差一些

四、Stack

Stack(栈)继承至Vector,所以它有Vector的所有优缺点。栈的特点就是后进先出(LIFO),所以它新增了栈操作的相关接口,代码如下:

// 在栈顶添加一个元素
public E push(E item) {
    addElement(item);

    return item;
}
  
// 弹出栈顶元素
public synchronized E pop() {
    E obj;
    int len = size();

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

    return obj;
}

// 查看栈顶元素(注意:该元素不会出栈)
public synchronized E peek() {
    int len = size();

    if (len == 0)
        throw new EmptyStackException();
    return elementAt(len - 1);
}

// 判断栈是否为空
public boolean empty() {
    return size() == 0;
}

// 在栈里查找指定元素的位置索引
public synchronized int search(Object o) {
    int i = lastIndexOf(o);

    if (i >= 0) {
        return size() - i;
    }
    return -1;
}

Stack的代码非常少,就上面这么点,主要是添加了栈相关的操作方法。

《算法》里面建议是不要使用Stack类来作为栈。因为Stack的API是宽接口API,也就是除了栈的相关操作(pushpoppeek)外它还有队列的相关操作(如getset等对非栈顶操作的方法),如果使用不当容易破坏栈结构。这些额外操作看起来可能很有用,但他们其实是累赘。而且对于栈这种修改多查询少的特性,我们使用链表来实现性能会更好。

结语

关于List接口的数据结构类基本就这些了,只要大家明白了它们各自的实现原理也就知道了它们各自的区别和适合的使用场景。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,634评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,951评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,427评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,770评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,835评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,799评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,768评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,544评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,979评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,271评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,427评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,121评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,756评论 3 324
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,375评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,579评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,410评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,315评论 2 352

推荐阅读更多精彩内容