4

Java数据结构之LinkedList

 3 years ago
source link: http://yuanfentiank789.github.io/2016/06/28/java%E5%B8%B8%E7%94%A8%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%842/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client
Java数据结构之LinkedList - JackPeng博客

LinkedList也和ArrayList一样实现了List接口,但是它执行插入和删除操作时比ArrayList更加高效,因为它是基于链表的。基于链表也决定了它在随机访问方面要比ArrayList逊色一点。

除此之外,LinkedList还提供了一些可以使其作为栈、队列、双端队列的方法。这些方法中有些彼此之间只是名称的区别,以使得这些名字在特定的上下文中显得更加的合适。

先看LinkedList类的定义。

 public class LinkedList<E>
 extends AbstractSequentialList<E>
 implements List<E>, Deque<E>, Cloneable, java.io.Serializable

LinkedList继承自AbstractSequenceList、实现了List及Deque接口。其实AbstractSequenceList已经实现了List接口,这里标注出List只是更加清晰而已。AbstractSequenceList提供了List接口骨干性的实现以减少实现List接口的复杂度。Deque接口定义了双端队列的操作。

LinkedList中之定义了两个属性:

 private transient Entry<E> header = new Entry<E>(null, null, null);
 private transient int size = 0;

size肯定就是LinkedList对象里面存储的元素个数了。LinkedList既然是基于链表实现的,那么这个header肯定就是链表的头结点了,Entry就是节点对象了。一下是Entry类的代码。

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

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

只定义了存储的元素、前一个元素、后一个元素,这就是双向链表的节点的定义,每个节点只知道自己的前一个节点和后一个节点。

来看LinkedList的构造方法

 public LinkedList() {
   header.next = header.previous = header;
 }
 
  public LinkedList(Collection<? extends E> c) {
   this();
   addAll(c);
  }

LinkedList提供了两个构造方法。第一个构造方法不接受参数,只是将header节点的前一节点和后一节点都设置为自身(注意,这个是一个双向循环链表,如果不是循环链表,空链表的情况应该是header节点的前一节点和后一节点均为null),这样整个链表其实就只有header一个节点,用于表示一个空的链表。第二个构造方法接收一个Collection参数c,调用第一个构造方法构造一个空的链表,之后通过addAll将c中的元素全部添加到链表中。来看addAll的内容。

ArrayList的属性

ArrayList定义只定义类两个私有属性:

  /** 
  * The array buffer into which the elements of the ArrayList are stored. 
  * The capacity of the ArrayList is the length of this array buffer. 
  */  
 private transient Object[] elementData;  
   
 /** 
  * The size of the ArrayList (the number of elements it contains). 
  * 
  * @serial 
  */  
 private int size;  

很容易理解,elementData存储ArrayList内的元素,size表示它包含的元素的数量。

有个关键字需要解释:transient。

Java的serialization提供了一种持久化对象实例的机制。当持久化对象时,可能有一个特殊的对象数据成员,我们不想用serialization机制来保存它。为了在一个特定对象的一个域上关闭serialization,可以在这个域前加上关键字transient。
ansient是Java语言的关键字,用来表示一个域不是该对象串行化的一部分。当一个对象被串行化的时候,transient型变量的值不包括在串行化的表示中,然而非transient型的变量是被包括进去的。

有点抽象,看个例子应该能明白。

public class UserInfo implements Serializable {  
 private static final long serialVersionUID = 996890129747019948L;  
 private String name;  
 private transient String psw;  
   
 public UserInfo(String name, String psw) {  
     this.name = name;  
     this.psw = psw;  
 }  
   
 public String toString() {  
     return "name=" + name + ", psw=" + psw;  
 }  
 }  
   
public class TestTransient {  
 public static void main(String[] args) {  
     UserInfo userInfo = new UserInfo("张三", "123456");  
     System.out.println(userInfo);  
     try {  
         // 序列化,被设置为transient的属性没有被序列化  
         ObjectOutputStream o = new ObjectOutputStream(new FileOutputStream(  
                 "UserInfo.out"));  
         o.writeObject(userInfo);  
         o.close();  
     } catch (Exception e) {  
         // TODO: handle exception  
         e.printStackTrace();  
     }  
     try {  
         // 重新读取内容  
         ObjectInputStream in = new ObjectInputStream(new FileInputStream(  
                 "UserInfo.out"));  
         UserInfo readUserInfo = (UserInfo) in.readObject();  
         //读取后psw的内容为null  
         System.out.println(readUserInfo.toString());  
     } catch (Exception e) {  
         // TODO: handle exception  
         e.printStackTrace();  
     }  
 }  
}

被标记为transient的属性在对象被序列化的时候不会被保存。

ArrayList的构造方法

/** 
  * Constructs an empty list with the specified initial capacity. 
  */  
 public ArrayList(int initialCapacity) {  
 super();  
     if (initialCapacity < 0)  
         throw new IllegalArgumentException("Illegal Capacity: "+  
                                            initialCapacity);  
 this.elementData = new Object[initialCapacity];  
 }  
   
 /** 
  * Constructs an empty list with an initial capacity of ten. 
  */  
 public ArrayList() {  
 this(10);  
 }  
   
 /** 
  * Constructs a list containing the elements of the specified 
  * collection, in the order they are returned by the collection's 
  * iterator. 
  */  
 public ArrayList(Collection<? extends E> c) {  
 elementData = c.toArray();  
 size = elementData.length;  
 // c.toArray might (incorrectly) not return Object[] (see 6260652)  
 if (elementData.getClass() != Object[].class)  
     elementData = Arrays.copyOf(elementData, size, Object[].class);  
 }  

第一个构造方法使用提供的initialCapacity来初始化elementData数组的大小。第二个构造方法调用第一个构造方法并传入参数10,即默认elementData数组的大小为10。第三个构造方法则将提供的集合转成数组返回给elementData(返回若不是Object[]将调用Arrays.copyOf方法将其转为Object[])。

ArrayList的其他方法

add(E e)

add(E e)都知道是在尾部添加一个元素,如何实现的呢?

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

书上都说ArrayList是基于数组实现的,属性中也看到了数组,具体是怎么实现的呢?比如就这个添加元素的方法,如果数组大,则在将某个位置的值设置为指定元素即可,如果数组容量不够了呢?

看到add(E e)中先调用了ensureCapacity(size+1)方法,之后将元素的索引赋给elementData[size],而后size自增。例如初次添加时,size为0,add将elementData[0]赋值为e,然后size设置为1(类似执行以下两条语句elementData[0]=e;size=1)。将元素的索引赋给elementData[size]不是会出现数组越界的情况吗?这里关键就在ensureCapacity(size+1)中了。

根据ensureCapacity的方法名可以知道是确保容量用的。ensureCapacity(size+1)后面的注释可以明白是增加modCount的值(加了俩感叹号,应该蛮重要的,来看看)。

 /** 
  * Increases the capacity of this <tt>ArrayList</tt> instance, if 
  * necessary, to ensure that it can hold at least the number of elements 
  * specified by the minimum capacity argument. 
  * 
  * @param   minCapacity   the desired minimum capacity 
  */  
 public void ensureCapacity(int minCapacity) {  
 modCount++;  
 int oldCapacity = elementData.length;  
 if (minCapacity > oldCapacity) {  
     Object oldData[] = elementData;  
     int newCapacity = (oldCapacity * 3)/2 + 1;  
         if (newCapacity < minCapacity)  
     newCapacity = minCapacity;  
         // minCapacity is usually close to size, so this is a win:  
         elementData = Arrays.copyOf(elementData, newCapacity);  
 }  
 }  

The number of times this list has been structurally modified.

这是对modCount的解释,意为记录list结构被改变的次数(观察源码可以发现每次调用ensureCapacoty方法,modCount的值都将增加,但未必数组结构会改变,所以感觉对modCount的解释不是很到位)。

增加modCount之后,判断minCapacity(即size+1)是否大于oldCapacity(即elementData.length),若大于,则调整容量为max((oldCapacity*3)/2+1,minCapacity),调整elementData容量为新的容量,即将返回一个内容为原数组元素,大小为新容量的数组赋给elementData;否则不做操作。

所以调用ensureCapacity至少将elementData的容量增加的1,所以elementData[size]不会出现越界的情况。

容量的拓展将导致数组元素的复制,多次拓展容量将执行多次整个数组内容的复制。若提前能大致判断list的长度,调用ensureCapacity调整容量,将有效的提高运行速度。

可以理解提前分配好空间可以提高运行速度,但是测试发现提高的并不是很大,而且若list原本数据量就不会很大效果将更不明显。

Add方法

add(int index, E element)

add(int index,E element)在指定位置插入元素。

 public void add(int index, E element) {  
 if (index > size || index < 0)  
     throw new IndexOutOfBoundsException(  
     "Index: "+index+", Size: "+size);  
   
 ensureCapacity(size+1);  // Increments modCount!!  
 System.arraycopy(elementData, index, elementData, index + 1,  
          size - index);  
 elementData[index] = element;  
 size++;  
 }  

首先判断指定位置index是否超出elementData的界限,之后调用ensureCapacity调整容量(若容量足够则不会拓展),调用System.arraycopy将elementData从index开始的size-index个元素复制到index+1至size+1的位置(即index开始的元素都向后移动一个位置),然后将index位置的值指向element。

addAll(Collection<? extends E> c)

 public boolean addAll(int index, Collection<? extends E> c) {  
 if (index > size || index < 0)  
     throw new IndexOutOfBoundsException(  
     "Index: " + index + ", Size: " + size);  
   
 Object[] a = c.toArray();  
 int numNew = a.length;  
 ensureCapacity(size + numNew);  // Increments modCount  
   
 int numMoved = size - index;  
 if (numMoved > 0)  
     System.arraycopy(elementData, index, elementData, index + numNew,  
              numMoved);  
   
     System.arraycopy(a, 0, elementData, index, numNew);  
 size += numNew;  
 return numNew != 0;  
 }  

先判断index是否越界。其他内容与addAll(Collection<? extends E> c)基本一致,只是复制的时候先将index开始的元素向后移动X(c转为数组后的长度)个位置(也是一个复制的过程),之后将数组内容复制到elementData的index位置至index+X。

Clear方法

public void clear() {  
 modCount++;  
   
 // Let gc do its work  
 for (int i = 0; i < size; i++)  
     elementData[i] = null;  
   
 size = 0;  
 }  

clear的时候并没有修改elementData的长度(好不容易申请、拓展来的,凭什么释放,留着搞不好还有用呢。这使得确定不再修改list内容之后最好调用trimToSize来释放掉一些空间),只是将所有元素置为null,size设置为0。

clone()

返回此 ArrayList 实例的浅表副本。(不复制这些元素本身。)

 public Object clone() {  
 try {  
     ArrayList<E> v = (ArrayList<E>) super.clone();  
     v.elementData = Arrays.copyOf(elementData, size);  
     v.modCount = 0;  
     return v;  
 } catch (CloneNotSupportedException e) {  
     // this shouldn't happen, since we are Cloneable  
     throw new InternalError();  
 }  
 }  

调用父类的clone方法返回一个对象的副本,将返回对象的elementData数组的内容赋值为原对象elementData数组的内容,将副本的modCount设置为0。

contains(Object)

public boolean contains(Object o) {  
 return indexOf(o) >= 0;  
 }  

indexOf方法返回值与0比较来判断对象是否在list中。接着看indexOf。

indexOf(Object)

public int indexOf(Object o) {  
 if (o == null) {  
     for (int i = 0; i < size; i++)  
     if (elementData[i]==null)  
         return i;  
 } else {  
     for (int i = 0; i < size; i++)  
     if (o.equals(elementData[i]))  
         return i;  
 }  
 return -1;  
 }  

通过遍历elementData数组来判断对象是否在list中,若存在,返回index([0,size-1]),若不存在则返回-1。所以contains方法可以通过indexOf(Object)方法的返回值来判断对象是否被包含在list中。

既然看了indexOf(Object)方法,接着就看lastIndexOf,光看名字应该就明白了返回的是传入对象在elementData数组中最后出现的index值。

public int lastIndexOf(Object o) {  
 if (o == null) {  
     for (int i = size-1; i >= 0; i--)  
     if (elementData[i]==null)  
         return i;  
 } else {  
     for (int i = size-1; i >= 0; i--)  
     if (o.equals(elementData[i]))  
         return i;  
 }  
 return -1;  
 }  

采用了从后向前遍历element数组,若遇到Object则返回index值,若没有遇到,返回-1。

get(int index)

 public E get(int index) {  
 RangeCheck(index);  
  
 return (E) elementData[index];  
 }  

但看代码的时候看到调用了RangeCheck方法,而且还是大写的方法,看看究竟有什么内容吧。

  /** 
  * Checks if the given index is in range. 
  */  
 private void RangeCheck(int index) {  
   if (index >= size)  
     throw new IndexOutOfBoundsException(  
     "Index: "+index+", Size: "+size);  
  }  

isEmpty()

直接返回size是否等于0。

remove(int index)

 public E remove(int index) {  
 RangeCheck(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; // Let gc do its work  
 return oldValue;  
 }  

首先是检查范围,修改modCount,保留将要被移除的元素,将移除位置之后的元素向前挪动一个位置,将list末尾元素置空(null),返回被移除的元素。

remove(Object o)

 public boolean remove(Object o) {  
 if (o == null) {  
         for (int index = 0; index < size; index++)  
     if (elementData[index] == null) {  
         fastRemove(index);  
         return true;  
     }  
 } else {  
     for (int index = 0; index < size; index++)  
     if (o.equals(elementData[index])) {  
         fastRemove(index);  
         return true;  
     }  
     }  
 return false;  
 }  

首先通过代码可以看到,当移除成功后返回true,否则返回false。remove(Object o)中通过遍历element寻找是否存在传入对象,一旦找到就调用fastRemove移除对象。为什么找到了元素就知道了index,不通过remove(index)来移除元素呢?因为fastRemove跳过了判断边界的处理,因为找到元素就相当于确定了index不会超过边界,而且fastRemove并不返回被移除的元素。下面是fastRemove的代码,基本和remove(index)一致。

 private void fastRemove(int index) {  
     modCount++;  
     int numMoved = size - index - 1;  
     if (numMoved > 0)  
         System.arraycopy(elementData, index+1, elementData, index,  
                          numMoved);  
     elementData[--size] = null; // Let gc do its work  
 }  

removeRange(int fromIndex,int toIndex)

 protected void removeRange(int fromIndex, int toIndex) {  
 modCount++;  
 int numMoved = size - toIndex;  
     System.arraycopy(elementData, toIndex, elementData, fromIndex,  
                      numMoved);  
   
 // Let gc do its work  
 int newSize = size - (toIndex-fromIndex);  
 while (size != newSize)  
     elementData[--size] = null;  
 }  

执行过程是将elementData从toIndex位置开始的元素向前移动到fromIndex,然后将toIndex位置之后的元素全部置空顺便修改size。

这个方法是protected,及受保护的方法,为什么这个方法被定义为protected呢?

这是一个解释,但是可能不容易看明白。http://stackoverflow.com/questions/2289183/why-is-javas-abstractlists-removerange-method-protected

先看下面这个例子

  ArrayList<Integer> ints = new ArrayList<Integer>(Arrays.asList(0, 1, 2,  
             3, 4, 5, 6));  
     // fromIndex low endpoint (inclusive) of the subList  
     // toIndex high endpoint (exclusive) of the subList  
    ints.subList(2, 4).clear();  
     System.out.println(ints);  

输出结果是[0, 1, 4, 5, 6],结果是不是像调用了removeRange(int fromIndex,int toIndex)!哈哈哈,就是这样的。但是为什么效果相同呢?是不是调用了removeRange(int fromIndex,int toIndex)呢?

set(int index,E element)

 public E set(int index, E element) {  
 RangeCheck(index);  
   
 E oldValue = (E) elementData[index];  
 elementData[index] = element;  
 return oldValue;  
 }   首先检查范围,用新元素替换旧元素并返回旧元素。

toArray()

public Object[] toArray() {  
     return Arrays.copyOf(elementData, size);  
 }  

调用Arrays.copyOf将返回一个数组,数组内容是size个elementData的元素,即拷贝elementData从0至size-1位置的元素到新数组并返回。

toArray(T[] a)

public <T> T[] toArray(T[] a) {  
     if (a.length < size)  
         // Make a new array of a's runtime type, but my contents:  
         return (T[]) Arrays.copyOf(elementData, size, a.getClass());  
 System.arraycopy(elementData, 0, a, 0, size);  
     if (a.length > size)  
         a[size] = null;  
     return a;  
 }  

trimToSize()

 public void trimToSize() {  
 modCount++;  
 int oldCapacity = elementData.length;  
 if (size < oldCapacity) {  
         elementData = Arrays.copyOf(elementData, size);  
 }  
 }  

由于elementData的长度会被拓展,size标记的是其中包含的元素的个数。所以会出现size很小但elementData.length很大的情况,将出现空间的浪费。trimToSize将返回一个新的数组给elementData,元素内容保持不变,length很size相同,节省空间。

学习Java最好的方式还必须是读源码。读完源码你才会发现这东西为什么是这么玩的,有哪些限制,关键点在哪里等等。而且这些源码都是大牛们写的,你能从中学习到很多

http://blog.csdn.net/jzhf2012/article/details/8540410



About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK