Android 常用数据结构解析

  

本着共同学习的原则,来一波常用数据结构源码走读。包括ArrayList、LinkedList、HashMap。对这三种常用到的数据结构进行源码分析,本次列车解析主要包含各数据集合增、删、改、查。

  ArrayList

  - ArrayList是List接口的可变数组的实现。实现了所有可选列表操作,并允许包括 null 在内的所有元素。

  数据结构:

  

  线性表的顺序存储结构具有两个基本特点:

  - 线性表中所有元素所占的存储空间是连续的;

  - 线性表中各数据元素在存储空间中是按逻辑顺序依次存放的。

  源码

  构造函数:

  private static final Object[] EMPTY_ELEMENTDATA = {};

  private static final int DEFAULT_CAPACITY = 10;

  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;

  }

  ArrayList有两个构造函数,一种是给定初始容量,一种是默认构造。使用默认构造函数是不会分配多余的空间的,由源码`private static final Object[] EMPTY_ELEMENTDATA = {};`可以看出。而对于已分配的空间数量不够情况会怎么办呢?在元素增加(执行add方法)的时候会调用`ensureCapacityInternal()`方法进行容量检查并扩充容量。下面就来Look下add、remove、set、get方法源码:

  public boolean add(E e) {

  ensureCapacityInternal(size + 1); // Increments modCount!!

  elementData[size++] = e;

  return true;

  }

  public void add(int index, E element) {

  if (index > size || index < 0)

  throw new IndexOutOfBoundsException(outOfBoundsMsg(index));

  ensureCapacityInternal(size + 1); // Increments modCount!!

  System.arraycopy(elementData, index, elementData, index + 1, size - index);

  elementData[index] = element;

  size++;

  }

  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;

  }

  public E get(int index) {

  if (index >= size)

  throw new IndexOutOfBoundsException(outOfBoundsMsg(index));

  return (E) elementData[index];

  }

  public E set(int index, E element) {

  if (index >= size)

  throw new IndexOutOfBoundsException(outOfBoundsMsg(index));

  E oldValue = (E) elementData[index];

  elementData[index] = element;

  return oldValue;

  }

  ArrayList的数据操作都是简单的数组操作,这里主要介绍下两个方法:`ensureCapacityInternal`和`System.arraycopy`。System.arraycopy是数组操作的核心方法,理解了它你就理解了数组世界。

  ensureCapacityInternal

  public void ensureCapacity(int minCapacity) {

  int minExpand = (elementData != EMPTY_ELEMENTDATA)

  // any size if real element table

  ? 0

  // larger than default for empty table. It's already supposed to be

  // at default size.

  : DEFAULT_CAPACITY;

  if (minCapacity > minExpand) {

  ensureExplicitCapacity(minCapacity);

  }

  }

  private void ensureCapacityInternal(int minCapacity) {

  if (elementData == EMPTY_ELEMENTDATA) {

  minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);

  }

  ensureExplicitCapacity(minCapacity);

  }

  private void ensureExplicitCapacity(int minCapacity) {

  modCount++;

  // overflow-conscious code

  if (minCapacity - elementData.length > 0)

  grow(minCapacity);

  }

  你阅读代码后发现,居然都是容量是否足够上的逻辑判断,容量不够的情况最后走到的是`grow()`方法。

  数组容量增长:

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

  }

  这里主要介绍下`Arrays.copyOf`方法,跟踪Arrays.copyOf代码可以看到核心还是System.arraycopy`方法。

  System.arraycopy:

  /*

  * @param src 源数组

  * @param srcPos 源数组复制的起始位置

  * @param dest 目的数组

  * @param destPos 目的数组放置的起始位置

  * @param length 复制的长度

  */

  public static native void arraycopy(Object src, int srcPos,

  Object dest, int destPos,

  int length);

  该方法是native的,不对底层的源码进行研究,知道方法用法和产生的结果就OK了。根据参数的解释很好理解可以达到的效果,有兴趣的朋友可以直接调这个函数进行试验。

  小结

  ArrayList实例都有一个容量,它总是至少等于列表的大小。随着向ArrayList中不断添加元素,其容量也自动增长。自动增长会带来数据向新数组的重新拷贝`Arrays.copyOf(elementData, newCapacity)`,因此,如果可预知数据量的多少,可在构造ArrayList时指定其容量。在添加大量元素前,应用程序也可以使用ensureCapacity操作来增加ArrayList实例的容量,这可以减少递增式再分配的数量。

  LinkedList

  - LinkedList 是一个继承于AbstractSequentialList的双向链表。它也可以被当作堆栈、队列或双端队列进行操作。

  - LinkedList 实现 List 接口,能对它进行队列操作。

  - LinkedList 实现 Deque 接口,即能将LinkedList当作双端队列使用。

  数据结构:

  ![双向链表](https://github.com/tuyc/tuyc.github.io/blob/master/images/WX20170719-003336@2x.png?raw=true)

  链表存储结构具有两个基本特点:

  - 链表是一种物理存储单元上非连续、非顺序的存储结构。

  - 数据元素的逻辑顺序是通过链表中的指针链接次序实现的。

  源码

  构造函数

  java

  public LinkedList() {

  }

  public LinkedList(Collection<? extends E> c) {

  this();

  addAll(c);

  }

  LinkedList的构造函数有两个:一种是默认构造和一种是给初始集合数据。使用有参构造函数会把Collection数据依次取出放到链表的尾部。这里先来看一下构造函数中使用到的`addAll()`方法:

  public boolean addAll(Collection<? extends E> c) {

  return addAll(size, c);

  }

  public boolean addAll(int index, Collection<? extends E> c) {

  checkPositionIndex(index);

  Object[] a = c.toArray();

  int numNew = a.length;

  if (numNew == 0)

  return false;

  Node<E> pred, succ;

  if (index == size) {

  succ = null;

  pred = last;

  } else {

  succ = node(index);

  pred = succ.prev;

  }

  for (Object o : a) {

  @SuppressWarnings("unchecked") E e = (E) o;

  Node<E> newNode = new Node<>(pred, e, null);

  if (pred == null)

  first = newNode;

  else

  pred.next = newNode;

  pred = newNode;

  }

  if (succ == null) {

  last = pred;

  } else {

  pred.next = succ;

  succ.prev = pred;

  }

  size += numNew;

  modCount++;

  return true;

  }

  这个方法的功能就是将Collection集合的全部数据拿出来放到index索引开始的链表上,可能是表头、表中、表尾,根据index参数决定。

  下面来Look下add、remove、set、get,以及队列和栈常用方法源码:

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

  }

  public E remove(int index) {

  checkElementIndex(index);

  return unlink(node(index));

  }

  public E set(int index, E element) {

  checkElementIndex(index);

  Node<E> x = node(index);

  E oldVal = x.item;

  x.item = element;

  return oldVal;

  }

  public E get(int index) {

  checkElementIndex(index);

  return node(index).item;

  }

  /*******************队列*****************/

  public E poll() {

  final Node<E> f = first;

  return (f == null) ? null : unlinkFirst(f);

  }

  public boolean offer(E e) {

  return add(e);

  }

  /****************栈******************/

  public void push(E e) {

  addFirst(e);

  }

  public E pop() {

  return removeFirst();

  }

  LinkedList的数据操作都是简单的链表操作,前提是熟悉链表操作,想当初的C语言学习链表时也是经历了懵懂的代价,成熟了就好。

  add方法会把元素加到表尾而不是表头;LinkedList可以当做栈和队列使用是因为实现了Deque接口,有队列poll、offer方法和栈push、pop方法,数据结构是基于链表来实现的;

  小结

  相比ArrayList而言,LinkedList数据的增加,删除,不会有增容和数组拷贝,效率更高效。对于查询和修改,LinkedList需要根据指针依次检查,ArrayList是随存随取,ArrayList会更高效。有一点需要注意,很多时候都会谈到性能问题,但是对于数据量小的情况影响真的不大,哪一种数据结构方便就使用哪种就好了,不用去纠结和比较。

  HashMap

  + HashMap 是一个采用哈希表(数组+链表)实现的键值对集合,继承自 AbstractMap,实现了 Map 接口 。

  + 两个关键因子:初始容量、加载因子

  + 插入、获取的时间复杂度基本是 O(1)

  数据结构:

  ![img](https://github.com/tuyc/tuyc.github.io/blob/master/images/WX20170719-001915@2x.png?raw=true)

  源码

  构造函数:

  static final int DEFAULT_INITIAL_CAPACITY = 4;

  static final float DEFAULT_LOAD_FACTOR = 0.75f;

  public HashMap(int initialCapacity, float loadFactor) {

  if (initialCapacity < 0)

  throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity);

  if (initialCapacity > MAXIMUM_CAPACITY) {

  initialCapacity = MAXIMUM_CAPACITY;

  } else if (initialCapacity < DEFAULT_INITIAL_CAPACITY) {

  initialCapacity = DEFAULT_INITIAL_CAPACITY;

  }

  if (loadFactor <= 0 || Float.isNaN(loadFactor))

  throw new IllegalArgumentException("Illegal load factor: " + loadFactor);

  threshold = initialCapacity;

  init();

  }

  public HashMap(int initialCapacity) {

  this(initialCapacity, DEFAULT_LOAD_FACTOR);

  }

  public HashMap() {

  this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);

  }

  public HashMap(Map<? extends K, ? extends V> m) {

  this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,

  DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);

  inflateTable(threshold);

  putAllForCreate(m);

  }

  HashMap的构造函数有四个,而最终都会执行`HashMap(int initialCapacity, float loadFactor)`构造函数,无论是默认构造、初始容量构造,还是初始集合构造。主要有2点:

  + DEFAULT_INITIAL_CAPACITY默认初始容量,DEFAULT_LOAD_FACTOR装载因子,这是个经验值。

  + 可以指定初始容量,以及装载因子。一般情况都会使用默认的装载因子。

  下面来Look下put(新增和修改)、remove、get源码:

  public V put(K key, V value) {

  if (table == EMPTY_TABLE) {

  inflateTable(threshold);

  }

  if (key == null)

  return putForNullKey(value);

  int hash = sun.misc.Hashing.singleWordWangJenkinsHash(key);

  int i = indexFor(hash, table.length);

  for (HashMapEntry<K,V> e = table[i]; e != null; e = e.next) {

  Object k;

  if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {

  V oldValue = e.value;

  e.value = value;

  e.recordAccess(this);

  return oldValue;

  }

  }

  modCount++;

  addEntry(hash, key, value, i);

  return null;

  }

  static int indexFor(int h, int length) {

  return h & (length-1);

  }

  void addEntry(int hash, K key, V value, int bucketIndex) {

  if ((size >= threshold) && (null != table[bucketIndex])) {

  resize(2 * table.length);

  hash = (null != key) ? sun.misc.Hashing.singleWordWangJenkinsHash(key) : 0;

  bucketIndex = indexFor(hash, table.length);

  }

  createEntry(hash, key, value, bucketIndex);

  }

  void createEntry(int hash, K key, V value, int bucketIndex) {

  HashMapEntry<K,V> e = table[bucketIndex];

  table[bucketIndex] = new HashMapEntry<>(hash, key, value, e);

  size++;

  }

  public V remove(Object key) {

  Entry<K,V> e = removeEntryForKey(key);

  return (e == null ? null : e.getValue());

  }

  public V get(Object key) {

  if (key == null)

  return getForNullKey();

  Entry<K,V> entry = getEntry(key);

  return null == entry ? null : entry.getValue();

  }

  + put函数

  if (key == null)

  return putForNullKey(value);

  由此可知key可以为空。

  int hash = sun.misc.Hashing.singleWordWangJenkinsHash(key);

  int i = indexFor(hash, table.length);

  hash的的算法不用care,只要理解根据key得到hash值即可;再根据hash值,`indexFor(int h, int length)`计算桶(数组)的下标;这一步任务就是定位桶的下标。

  for (HashMapEntry<K,V> e = table[i]; e != null; e = e.next) {

  Object k;

  if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {

  V oldValue = e.value;

  e.value = value;

  e.recordAccess(this);

  return oldValue;

  }

  }

  这里可以发现for循环初始值HashMapEntry<K,V> e=table[i](i是上一步计算出的下标),HashMapEntry是一个链表结点,并拥有next结点;滑动链表结点,依次比较,当`e.hash == hash && ((k = e.key) == key || key.equals(k))`条件满足就找到了相同的key,进行修改操作,否则就进行增加操作,如下:

  addEntry(hash, key, value, i);

  最后会执行到:

  void createEntry(int hash, K key, V value, int bucketIndex) {

  HashMapEntry<K,V> e = table[bucketIndex];

  table[bucketIndex] = new HashMapEntry<>(hash, key, value, e);

  size++;

  }

  由代码可以发现,新增的元素都是放在对应桶的链表的表头。由此可以得出不发生hash碰撞情况,插入操作的时间复杂度是O(1)的。

  + remove函数

  final Entry<K,V> removeEntryForKey(Object key) {

  if (size == 0) {

  return null;

  }

  int hash = (key == null) ? 0 : sun.misc.Hashing.singleWordWangJenkinsHash(key);

  int i = indexFor(hash, table.length);

  HashMapEntry<K,V> prev = table[i];

  HashMapEntry<K,V> e = prev;

  while (e != null) {

  HashMapEntry<K,V> next = e.next;

  Object k;

  if (e.hash == hash &&

  ((k = e.key) == key || (key != null && key.equals(k)))) {

  modCount++;

  size--;

  if (prev == e)

  table[i] = next;

  else

  prev.next = next;

  e.recordRemoval(this);

  return e;

  }

  prev = e;

  e = next;

  }

  return e;

  }

  可以发现主要的过程也是由key得到hash值,再定位桶的下标,继续遍历链表,寻找与之对应的key,之后都是链表操作就不再描述了。

  + get函数

  核心函数是:

  final Entry<K,V> getEntry(Object key) {

  if (size == 0) {

  return null;

  }

  int hash = (key == null) ? 0 : sun.misc.Hashing.singleWordWangJenkinsHash(key);

  for (HashMapEntry<K,V> e = table[indexFor(hash, table.length)];

  e != null;

  e = e.next) {

  Object k;

  if (e.hash == hash &&

  ((k = e.key) == key || (key != null && key.equals(k))))

  return e;

  }

  return null;

  }

  你会发现主要的过程还是根据key得到hash值,根据hash值定位桶的下标,继续遍历链表,寻找与之对应的key,找到了就返回对应的value。由此可以得出:在不发生hash碰撞的情况下,查找的时间复杂度为O(1)。因为不发生Hash碰撞,所要找的元素就是对应桶里的唯一元素,而桶的下标是根据key的hash值计算直接得到的。

  小结

  数组存储的特点:区间是连续的,占用内存严重,故空间复杂的很大。寻址容易,插入和删除困难。

  链表存储特点:区间离散,占用内存比较宽松,故空间复杂度很小,但时间复杂度很大,达O(N)。寻址困难,插入和删除容易。

  哈希表就是结合了两者特性,做出一种寻址容易,插入删除也容易的数据结构。

  

  本文作者:涂阳川(点融黑帮),从事Android开发。smile to the world!

  

声明:本文由入驻搜狐公众平台的作者撰写,除搜狐官方账号外,观点仅代表作者本人,不代表搜狐立场。
推荐阅读