不惑JAVA之JAVA基础 - Set 和 List

来源:互联网 发布:阿飞 傅红雪 知乎 编辑:程序博客网 时间:2024/05/21 17:06

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

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

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

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

这里写图片描述
这里写图片描述
(上图来源于网友的总结)

这次重要讲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的前置节点之间插入新的节点。看下面的图:
这里写图片描述
好重点来讲一下这个,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的区别

这里写图片描述

下一篇会介绍map

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

0 0
原创粉丝点击