不惑JAVA之JAVA基础 - Set 和 List

从学习JAVA到现在也有小十年了,工作一直在用但理论知识确是年年在忘,用了很多学了很多但都系统。人也快30了,而立之年何去何从?

最近看到csdn中一个博主写的java系列博客不错,我这人比较懒估计自己从0开始估计写不下来,所以站在“巨人”肩膀上开始自己的笔记,希望能比较系统的将java知识梳理一下。
此blog大量参考Java之美[从菜鸟到高手演变]之集合类,因为已经写得很不错了,此blog会对一些细节进行补充。

集合是我们开发中最长用的,常用集合类:

  1. 实现Collection接口:Set、List等;
  2. 实现Map接口:HashMap,HashTable等

不惑JAVA之JAVA基础 - Set 和 List_第1张图片
不惑JAVA之JAVA基础 - Set 和 List_第2张图片
(上图来源于网友的总结)

这次重要讲Set和List的源码实现。由于Java之美[从菜鸟到高手演变]之集合类已经讲的非常好了,我这里就只突出重点讲些知识点。

Set解析

Set 中的成员是不能重复的,无序的(但TreeSet是个例外是有序的)。
多说些,为什么set是无需的呢?以HashSet为例,我们先来看看为什么不能重复下面的问题就解开了。

以hashSet为例add方法的源码

    public boolean add(E e) {
        return map.put(e, PRESENT)==null;
    }

主要看e,e是什么呢?就是添加到set的元素。就是要添加的元素以map的key的形式添加进来,底层存储还是map的。PRESENT是凑数的。

再详细看一下put方法:

  public V put(K key, V value) {  
        if (key == null)  
            return putForNullKey(value);  
        int hash = hash(key.hashCode());//----------1---------- 
        int i = indexFor(hash, table.length);//-----------2--------- 
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {//-----------3------- 
            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;  
             }  
         }//------------------4-------------------- 
         modCount++;  
         addEntry(hash, key, value, i);  
         return null;  
     } 

向HashMap中添加元素的时候,首先计算元素的hashcode值,然后根据1处的代码计算出Hashcode的值,再根据2处的代码计算出这个元素的存储位置,如果这个位置为空,就将元素添加进去;如果不为空,则看3-4的代码,遍历索引为i的链上的元素,如果key重复,则替换并返回oldValue值。

当遍历时
由于e放到map的key中,底层只能通过e的hashCode确认放到哪个位置(位置的逻辑是:(元素的hashcode)%(HashMap集合的大小)+1),每个e的hashCode不是顺序的,所以iterator的时候也不能保证顺序。就是下面的源码**

    public Iterator<E> iterator() {    
       return map.keySet().iterator();    
    }  

具体HashMap的源码会在后期blog中详细介绍。

ArrayList解析

什么是ArrayList?
ArrayList是基于数组实现的,是一个动态数组,其容量能自动增长。

1. ArrayList不是线程安全的,只能用在单线程环境下,多线程环境下可以考虑用Collections.synchronizedList(List l)函数返回一个线程安全的ArrayList类,也可以使用concurrent并发包下的CopyOnWriteArrayList类;
2. 实现了Serializable接口,就支持了序列化;
3. 实现了RandomAccess接口,支持快速随机访问;
4. 实现了Cloneable接口,能被克隆;
5. 使用ensureCapacity操作进行容量的自增长。
 // 源码中有这么一行
 public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable ...... public void ensureCapacity(int paramInt) {
    this.modCount += 1;
    int i = this.elementData.length;
    if (paramInt > i) {
      Object[] arrayOfObject = this.elementData;
      int j = i * 3 / 2 + 1;
      if (j < paramInt)
        j = paramInt;
      this.elementData = Arrays.copyOf(this.elementData, j);
    }
  }

从上述代码中可以看出,数组进行扩容时,会将老数组中的元素重新拷贝一份到新的数组中,每次数组容量的增长大约是其原容量的1.5倍。这种操作的代价是很高的,因此在实际使用时,我们应该尽量避免数组容量的扩张。当我们可预知要保存的元素的多少时,要在构造ArrayList实例时,就指定其容量,以避免数组扩容的发生。或者根据实际需求,通过调用ensureCapacity方法来手动增加ArrayList实例的容量。可以通过ensureCapacity(int paramInt)方法可以提高ArrayList的初始化速度

私有属性

elementData存储ArrayList内的元素,size表示它包含的元素的数量

/** * The array buffer into which the elements of the ArrayList are stored. * The capacity of the ArrayList is the length of this array buffer. Any * empty ArrayList with elementData == EMPTY_ELEMENTDATA will be expanded to * DEFAULT_CAPACITY when the first element is added. */
private transient Object[] elementData; 

private int size;

transient什么意思呢
ava的serialization提供了一种持久化对象实例的机制。当持久化对象时,可能有一个特殊的对象数据成员,我们不想用serialization机制来保存它。为了在一个特定对象的一个域上关闭serialization,可以在这个域前加上关键字transient。可以参见java例子UserInfo和TestTransient。
简单的理解就是:被标记为transient的属性在对象被序列化的时候不会被保存。

元素存储

这个知识点比较重要,需要仔细阅读源码。

// 用指定的元素替代此列表中指定位置上的元素,并返回以前位于该位置上的元素。
 public E set(int index, E element) {  
    RangeCheck(index);  

    E oldValue = (E) elementData[index];  
    elementData[index] = element;  
    return oldValue;  
 }    
 // 将指定的元素添加到此列表的尾部。 
 public boolean add(E e) {  
    ensureCapacity(size + 1);   
    elementData[size++] = e;  
    return true;  
 }    
 // 将指定的元素插入此列表中的指定位置。 
 // 如果当前位置有元素,则向右移动当前位于该位置的元素以及所有后续元素(将其索引加1)。 
 public void add(int index, E element) {  
    if (index > size || index < 0)  
        throw new IndexOutOfBoundsException("Index: "+index+", Size: "+size);  
    // 如果数组长度不足,将进行扩容。 
    ensureCapacity(size+1);  // Increments modCount!! 
    // 将 elementData中从Index位置开始、长度为size-index的元素, 
    // 拷贝到从下标为index+1位置开始的新的elementData数组中。 
    // 即将当前位于该位置的元素以及所有后续元素右移一个位置。 
    System.arraycopy(elementData, index, elementData, index + 1, size - index);  
    elementData[index] = element;  
    size++;  
 }    
 // 按照指定collection的迭代器所返回的元素顺序,将该collection中的所有元素添加到此列表的尾部。 
 public boolean addAll(Collection<? extends E> c) {  
    Object[] a = c.toArray();  
    int numNew = a.length;  
    ensureCapacity(size + numNew);  // Increments modCount 
    System.arraycopy(a, 0, elementData, size, numNew);  
    size += numNew;  
    return numNew != 0;  
 }    
 // 从指定的位置开始,将指定collection中的所有元素插入到此列表中。 
 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;  
   }  

看到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)中了。

元素删除

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

简单的理解就是:

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

其中用到了public static void arraycopy(Object src, int srcPos, Object dest, int destPos,int length)结合API看看传入参数就好理解了

参数:
src - 源数组。
srcPos - 源数组中的起始位置。
dest - 目标数组。
destPos - 目标数据中的起始位置。
length - 要复制的数组元素的数量

LinkedList

底层采用双向循环列表实现,进行插入和删除操作时具有较高的速度。

private static class Entry<E> {  
    E element;  
    Entry<E> next;  
    Entry<E> previous;  

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

LinkedList的原始存储模型,一个数据data,两个指针,一个指向前一个节点,名为previous,一个指向下一个节点,名为next。

 public LinkedList() {  
         header.next = header.previous = header;  
     }  

头尾相等,就是说初始化的时候就已经设置成了循环的。

add操作

 public boolean add(E e) {  
    addBefore(e, header);  
    return true;  
 }  
private Entry<E> addBefore(E e, Entry<E> entry) {
        Entry<E> newEntry = new Entry<E>(e, entry, entry.previous);//-------1---------
        newEntry.previous.next = newEntry;
        newEntry.next.previous = newEntry;
        size++;
        modCount++;
        return newEntry;
    }

我们先来观察下上面给出的Entity类,构造方法有三个参数,第二个是她的next域,第三个是她的previous域,所以上述代码1行处将传进来的entry实体,即header对象作为newEntry的next域,而将entry.previous即header.previous作为previous域。也就是说在header节点和header的前置节点之间插入新的节点。看下面的图:
不惑JAVA之JAVA基础 - Set 和 List_第3张图片
好重点来讲一下这个,newEntry.previous.next = newEntry; 和 newEntry.next.previous = newEntry;对于刚接触底层代码的人来说有点费解。
拆分一下,newEntry.previous 是不是指得newEntry的上个entry,上个entry的next的是不是就是newEntry,串起来就是newEntry.previous.next = newEntry;
同理newEntry.next.previous = newEntry; 也一样。

移除操作

    public boolean remove(Object o) {
        if (o==null) {
            for (Entry<E> e = header.next; e != header; e = e.next) {
                if (e.element==null) {
                    remove(e);
                    return true;
                }
            }
        } else {
            for (Entry<E> e = header.next; e != header; e = e.next) {
                if (o.equals(e.element)) {
                    remove(e);
                    return true;
                }
            }
        }
        return false;
    }
        private E remove(Entry<E> e) {
            if (e == header)
                throw new NoSuchElementException();

            //保留被移除的元素:要返回
            E result = e.element;

            //将该节点的前一节点的next指向该节点后节点
            e.previous.next = e.next;
            //将该节点的后一节点的previous指向该节点的前节点
            //这两步就可以将该节点从链表从除去:在该链表中是无法遍历到该节点的
            e.next.previous = e.previous;
            //将该节点归空
            e.next = e.previous = null;
            e.element = null;
            size--;
            modCount++;
            return result;
        }
  1. 保留移除原始
  2. 进行指针修改
    • e.previous.next = e.next; 将移除节点后一个的next指向移除节点的next;
    • 同理e.next.previous = e.previous;也一样
  3. 将移除节点归空 e.next = e.previous = null;

Vector和ArrayList的区别

不惑JAVA之JAVA基础 - Set 和 List_第4张图片

下一篇会介绍map

参考资料:
Java之美[从菜鸟到高手演变]之集合类
Java提高篇(三二)—–List总结
java提高篇(二二)—LinkedList

你可能感兴趣的:(java)