Java集合(一) - Collection

Java集合系列文章包括:

本文主要介绍Java容器原理、特性及简单使用方式,主要包含Collection接口及其主要实现类。

1. 概览

Java 集合, 也叫作容器,主要是由两大接口派生而来:一个是 Collecton接口,主要用于存放单一元素;另一个是 Map 接口,主要用于存放键值对。对于Collection 接口,下面又有三个主要的子接口:List、Set 和 Queue

2. Collection接口

3. 常用实现类

  • ArrayList
    • List接口的主要实现类,利用数组存储数据,元素是有序的,它能够添加null元素,使用数组下标查找数据O(1),适用于频繁查找的场景
    • 插入元素时,当size == elementData.length的时候,就需要扩容。初始化的时候尽量指定容量,减少扩容次数
    • 线程不安全
  • LinkedList
    • 使用双向链表存储数据,元素是有序的,并且元素可以为null
    • 直接继承自AbstractSequentialList,实现了Deque(双端队列)接口,因此可以将其作为双端队列的一种实现
    • 查找耗时,插入删除比较快速,适合频繁增删的场景
    • 遍历推荐使用forEachIterator方式,for循环方式比较耗时(没有实现RandomAccess接口)
    • 线程不安全,如果想使LinkedList变成线程安全的,可以调用静态类Collections类中的synchronizedList方法
  • Vector
    • List的古老实现类,与ArrayList很像,它利用数组存储数据,包含了需求不属于集合的传统方法
    • 线程安全
  • Stack
    • 继承自VectorLIFO(last-in-first-out)先进后出的数据结构
    • 推荐使用Deque的实现类(如ArrayDeque或LinkedList)而非继承Stack,Deque vs Stack

4. ArrayList

1. 数据结构

数组

/**
 * 存储元素的数组,当size等于数组的长度,再次进行插入就需要扩容
 */
transient Object[] elementData;
/**
 * 存储的元素的个数,一般情况下,size != elementData.length
 */
private int size;

2. 添加元素(add)

1. 扩容

插入元素时,当size == elementData.length的时候,就需要扩容。扩容实际上是通过System.arraycopy方法将旧数组内容复制到新数组,新数组长度的大小逻辑如下:

  • 初始情况下,数组长度扩容为10(默认的)
  • 其他情况下,新数组的长度是原数组的1.5倍(oldCapacity + (oldCapacity >> 1)),数组长度最大为Integer.MAX_VALUE

2. 两种插入方式

private void add(E e, Object[] elementData, int s) {
  if (s == elementData.length) {
    elementData = grow(); // 扩容
  }
  elementData[s] = e;
  size = s + 1;
}

/**
 * 元素插入到数组末尾
 */
public boolean add(E e) {
  modCount++;
  add(e, elementData, size);
  return true;
}

/**
 * 在指定位置插入元素
 */
public void add(int index, E element) {
  rangeCheckForAdd(index);
  modCount++;
  final int s;
  Object[] elementData;
  if ((s = size) == (elementData = this.elementData).length) {
    elementData = grow();
  }
  System.arraycopy(elementData, index, elementData, index + 1, s - index);
  elementData[index] = element;
  size = s + 1;
}

private void rangeCheckForAdd(int index) {
  if (index > size || index < 0) {
    throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
  }
}
  • 元素添加到数组末尾。首先判断是否需要扩容,然后给elementData[size]赋值,时间复杂度为O(1)
  • 元素添加到指定位置。首先判断指定位置是否越界了([0, size]),然后判断是否需要扩容,然后将[index, size - 1]的元素移动到[index + 1, size],这里是通过System.arraycopy直接复制指定区间的元素,速度比循环更快,最后给elementData[index]赋值
  • ArrayList还提供了批量添加元素的方法(addAll)

因此,在中间位置插入元素效率比在末尾添加要低,所以我们在使用的时候,尽量不要在中间位置插入元素。

3. 访问元素(get)

public E get(int index) {
  Objects.checkIndex(index, size);
  return elementData(index);
}

E elementData(int index) {
  return (E) elementData[index];
}
  • 通过数组下标访问元素,时间复杂度为O(1)
  • ArrayList还提供了判断集合是否包含元素(contains)和查询元素在集合中的下标(indexOf)的方法

4. 删除元素(remove)

/**
 * 删除指定位置元素
 */
public E remove(int index) {
    Objects.checkIndex(index, size);
    final Object[] es = elementData;
    @SuppressWarnings("unchecked") E oldValue = (E) es[index];
    fastRemove(es, index);
    return oldValue;
}

/**
 * 删除指定元素
 */
public boolean remove(Object o) {
  final Object[] es = elementData;
  final int size = this.size;
  int i = 0;
  found: {
    if (o == null) {
      for (; i < size; i++) {
        if (es[i] == null) {
          break found;
        }
      }
    } else {
        for (; i < size; i++) {
          if (o.equals(es[i])) {
            break found;
          }
        }
    }
    return false;
  }
  fastRemove(es, i);
  return true;
}

private void fastRemove(Object[] es, int i) {
  modCount++;
  final int newSize;
  if ((newSize = size - 1) > i) {
    System.arraycopy(es, i + 1, es, i, newSize - i);
  }
  es[size = newSize] = null;
}
  • 删除指定位置元素:将指定元素后面的元素整个向前挪一个位置,再将最后一个元素置为null
  • 删除指定元素:定位指定元素下标,之后的操作与删除指定位置元素一致
  • ArrayList还提供了删除符合条件元素的方法(removeIf)

5. LinkedList

1. 数据结构

双向链表

//元素个数
transient int size = 0;
//双向链表的头指针
transient Node<E> first;
//双向链表的尾部指针
transient Node<E> last;

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

2. 添加元素

// 添加元素
public boolean add(E e) {
  linkLast(e);
  return true;
}
// 在LinkedList末尾插入元素 并移动头尾指针
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++; // 结构修改自增
}

3. 访问元素

//根据索引获取元素
public E get(int index) {
  checkElementIndex(index);//检查索引是否越界
  return node(index).item;//定位索引元素
}
//定位元素位置
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;
  }
}
  • 使用二分查找的思想,从链表头部或尾部开始遍历查找

4. 删除元素

public E remove(int index) {
  checkElementIndex(index);
  return unlink(node(index));
}

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

ArrayList vs LinkedList

  1. 是否保证线程安全: ArrayList 和 LinkedList 都是不同步的,也就是不保证线程安全
  2. 底层数据结构: Arraylist 底层使用的是 Object 数组;LinkedList 底层使用的是 双向链表 数据结构(JDK1.6 之前为循环链表,JDK1.7 取消了循环。注意双向链表和双向循环链表的区别,下面有介绍到!)
  3. 插入和删除是否受元素位置的影响:
    ArrayList 采用数组存储,所以插入和删除元素的时间复杂度受元素位置的影响。 比如:执行add(E e)方法的时候, ArrayList 会默认在将指定的元素追加到此列表的末尾,这种情况时间复杂度就是 O(1)。但是如果要在指定位置 i 插入和删除元素的话(add(int index, E element))时间复杂度就为 O(n-i)。因为在进行上述操作的时候集合中第 i 和第 i 个元素之后的(n-i)个元素都要执行向后位/向前移一位的操作。
    LinkedList 采用链表存储,所以,如果是在头尾插入或者删除元素不受元素位置的影响(add(E e)、addFirst(E e)、addLast(E e)、removeFirst() 、 removeLast()),近似 O(1),如果是要在指定位置 i 插入和删除元素的话(add(int index, E element),remove(Object o)) 时间复杂度近似为 O(n) ,因为需要先移动到指定位置再插入。
  4. 是否支持快速随机访问: LinkedList 不支持高效的随机元素访问,而 ArrayList 支持。快速随机访问就是通过元素的序号快速获取元素对象(对应于get(int index)方法)。
  5. 内存空间占用: ArrayList 的空间浪费主要体现在在 list 列表的结尾会预留一定的容量空间,而 LinkedList 的空间花费则体现在它的每一个元素都需要消耗比 ArrayList 更多的空间(因为要存放直接后继和直接前驱以及数据)。

6. Vector

Vector 类实现了一个动态数组。和ArrayList很相似,但是两者是不同的:

  • 是否保证线程安全:Vector保证线程安全,ArrayList不保证线程安全
  • Vector 包含了许多传统的方法,这些方法不属于集合框架

7. Queue

LinkedList是Queue的一个常用实现类,也是Deque(双端队列)的一个常用实现类。因此,可以将LinkedList作为栈来使用。

Queue和Deque的区别

Queue 是单端队列,只能从一端插入元素,另一端删除元素,实现上一般遵循先进先出(FIFO)规则。

Queue 扩展了Collection的接口,根据 因为容量问题而导致操作失败后处理方式的不同 可以分为两类方法: 一种在操作失败后会抛出异常,另一种则会返回特殊值。

Queue 接口 抛出异常 返回特殊值
插入队尾 add(E e) -> 容量限制时抛出IllegalStateException offer(E e)
删除队首 remove() -> 队列为空时抛出NoSuchElementException poll()
查询队首元素 element() -> 队列为空时抛出NoSuchElementException peek()

Deque 是双端队列,在队列的两端均可以插入或删除元素。

Deque 扩展了 Queue 的接口, 增加了在队首和队尾进行插入和删除的方法,同样根据失败后处理方式的不同分为两类:

Deque 接口 抛出异常 返回特殊值
插入队首 addFirst(E e) offerFirst(E e)
插入队尾 addLast(E e) offerLast(E e)
删除队首 removeFirst() pollFirst()
删除队尾 removeLast() pollLast()
查询队首元素 getFirst() peekFirst()
查询队尾元素 getLast() peekLast()
事实上,Deque 还提供有 push()pop()等其他方法,可用于模拟栈。

8. Stack

由于LinkedList底层由双端链表实现,因此,也可以将其作为栈来使用。

9. 常见问题

1. 有序性和不可重复性的含义是什么?

1、什么是有序性?说的是元素的插入先后,与元素在集合内存储的位置是否有前后对应关系。即有序、无序是指插入时,插入位置是否遵循先入在前后入在后的规则,若先插的位置在前,后插的位置在后,则可说此集合类是有序的,反之则无序。

实现了List接口的集合类全部有序,如ArrayList、LinkedList
实现了Map接口的集合类中,HashMap无序
实现了Set接口的集合类中,HashSet(基于HashMap实现)无序

2、什么是不可重复性?不可重复性是指添加的元素按照 equals()判断时,返回 false。需要同时重写 equals()方法和 hashCode()方法。

10. 参考

posted @ 2021-09-02 14:21  阿Jay  阅读(132)  评论(0)    收藏  举报