ArrayList、LinkedList、HashMap、HashTable集合源码分析

ArrayList、LinkedList、HashMap、HashTable集合源码分析
JDK1.8和JDK1.7及以下 两个版本中集合类区别还是很大的,所以就按这两个版本来说
一、ArrayList
1.JDK1.8和JDK1.7及以下版本中,ArrayList集合底层都是由数组实现,不同的是默认长度
2.JDK1.8中,在不指定集合长度时,默认长度为0,只有在添加第一个元素的时候,集合长度才会变为10

private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

默认构造方法给的是一个空的数组,长度为0

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

private static final int DEFAULT_CAPACITY = 10;


    private void ensureCapacityInternal(int minCapacity) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
        }

        ensureExplicitCapacity(minCapacity);
    }

add元素时,先调用ensureCapacityInternal(),在该方法中会先判断集合是否是一个空集合,是的话,则从默认长度10和minCapacity中选择一个最大的值作为集合长度,minCapacity表示集合当前元素个数+1,
当我们先调用默认构造方法,再add一个元素时,底层数据长度的变化是:先是一个长度为0的空数组,添加第一个元素的时候长度变为10.
3.在指定集合长度时,源码如下:

public ArrayList(int initialCapacity) {
        if (initialCapacity > 0) {
            this.elementData = new Object[initialCapacity];
        } else if (initialCapacity == 0) {
            this.elementData = EMPTY_ELEMENTDATA;
        } else {
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        }
    }

若给定的集合长度大于0,则数组长度为指定的长度
若给定的集合长度为0,则数组是一个空数组,长度为0,
若给定的集合长度小于0,则抛出参数非法的异常
4.JDK1.8和JDK1.7及以下版本中,ArrayList的扩容机制是相同的,都是扩容为原来的1.5倍。看下源码是怎么进行扩容的。

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


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);
    }
    
public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
        @SuppressWarnings("unchecked")
        T[] copy = ((Object)newType == (Object)Object[].class)
            ? (T[]) new Object[newLength]
            : (T[]) Array.newInstance(newType.getComponentType(), newLength);
        System.arraycopy(original, 0, copy, 0,
                         Math.min(original.length, newLength));
        return copy;
    }

size为当前集合中元素的个数,+1表示添加新元素后的元素个数,注意目前还没有添加元素
(1)先通过ensureCapacityInternal方法判断集合中当前元素个数+1后是否大于集合容量,大的话则进行扩容,否则采用原数组长度,扩容时会将旧数组元素复制到新的数组中
(2)执行代码elementData[size++] = e;数组容量确定后,开始添加新的元素,先把新元素e添加到size索引处,再将元素个数+1
多线程环境下,ArrayList是线程不安全的,主要问题就出现在add()方法中,想要线程安全的话,可以使用Vector,在Vector中,添加元素的方法加了锁synchronized,可以保证多线程下的线程安全。

public synchronized void addElement(E obj) {
        modCount++;
        ensureCapacityHelper(elementCount + 1);
        elementData[elementCount++] = obj;
    }

5.基于数组的ArrayList查找效率高,知道索引的前提下,一次就可以查到,时间复杂度我O(1),增加和删除的效率低,增删会涉及到元素的移动,最慢是O(n),正常情况是O(n-i) ,i是索引。
二、LinkedList
JDK1.8和JDK1.7及以下版本LinkedList没有区别,LinkedList用的也不多,说下和ArrayList的区别:
1.LinkedList底层是双向链表。
2.相比ArrayList,LinkedList查询效率低,因为要从第一个元素开始向下寻找,时间复杂度为O(n),但是增删的效率高,除尾节点外,ArrayList增加或删除一个元素都需要移动其他元素,而LinkedList只需要修改节点的前后指针。
3.LinkedList占用内存更多,因为每一个节点都有头尾指针。
三、HashMap
1.JDK1.8和JDK1.7及以下版本中HashMap区别较大,主要是在底层实现上。

JDK1.7及以下版本中,HashMap底层由数组+链表实现,
JDK1.8中,HashMap底层由数组+链表+红黑树实现

HashMap是以键值对key-value的形式存储元素的,在执行put方法时,会先通过

int hash = hash(key);
int i = indexFor(hash,length)

计算key的hash值,得到int类型的数据,再把hash值和集合长度取模,最后得到的数就是键值对存储在数组中的索引位置,当集合中元素过多时,不同的key可能得到的索引是相同的,数组中一个索引位置就有多个键值对,这就是Hash冲突,链表结构的引入就是为了解决Hash冲突,把索引相同的键值对放在一条链表上,当再有冲突时,直接把键值对加到链表尾部,但是这样又引出一个问题,当链表长度过长时,查询的效率会变低,几乎是线性查找了,时间复杂度为O(n),所以针对这个问题,JDK1.8版本中引入了红黑树,当链表长度大于8时,链表转为红黑树,以此来提高查询的效率。
2.初始化一个HashMap,不指定集合长度,看下它的构造方法如下:

static final float DEFAULT_LOAD_FACTOR = 0.75f;

  public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }

构造方法中只是指定了负载因子为0.75,没有指定集合的默认大小,我们通过反射来看下HashMap默认的长度是多少。

  HashMap<String,String> map = new HashMap<>();
  Class mapClass = map.getClass();
  Method method = mapClass.getDeclaredMethod("capacity");
  method.setAccessible(true);
  System.out.println(method.invoke(map));

执行结果如下:
在这里插入图片描述
从结果中可以看出,HashMap若不指定集合长度,则默认长度为16,负载因子为0.75,也就是说,当集合中的元素个数达到集合长度的0.75时,就会进行扩容,扩容到原来的两倍。可是为什么会是16呢,原来JDK的开发工程师在计算存储的索引位置时,把原来的对集合长度的取模运算改进为位运算,提高计算效率,那既然是位运算,就要保证数组的长度是2的次幂,过大不好,过小也不好,经过大量的反复认证,觉得16就合适。
那我们如果初始化集合的时候指定集合长度为不是2的次幂的一个数,比如7,那么集合的长度会是多少呢,再使用反射验证下,返回的结果如下:
在这里插入图片描述
结果竟然是8,再指定长度为11,其真实集合长度为16,也就是说,HashMap的长度一定是2的次幂,如果指定的长度不是2的次幂,那么底层计算出的长度就是比指定数值要大的2的次幂的第一个数。

capacity方法会返回集合的长度,源码如下:

final int capacity() {
        return (table != null) ? table.length :
            (threshold > 0) ? threshold :
            DEFAULT_INITIAL_CAPACITY;
    }


static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; 

默认长度就是2的4次幂,16
当然初始化HashMap的时候也可以指定长度和负载因子,源码如下:

 public HashMap(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);
        this.loadFactor = loadFactor;
        this.threshold = tableSizeFor(initialCapacity);
    }

static final int MAXIMUM_CAPACITY = 1 << 30;

这个代码也比较好懂,其中有一点,当指定的集合长度大于2的30次幂时,集合长度也就是2的30次幂了,也就是说集合最大长度为2的30次幂。
3.HashMap是线程不安全的,多线程下可以使用ConcurrentHashMap来保证线程安全,该类的put方法中加了锁,如下:
在这里插入图片描述
四、HashTable
1.初始化一个HashTable,不指定长度的话,其默认长度为11,负载因子为0.75,构造方法如下:

public Hashtable() {
        this(11, 0.75f);
    }

HashTable底层数据结构为数组+链表,所以put元素时和HashMap原理相差不大,具体方法如下:

public synchronized V put(K key, V value) {
        // Make sure the value is not null
        if (value == null) {
            throw new NullPointerException();
        }

        // Makes sure the key is not already in the hashtable.

        //通过key计算出存储键值对的索引
        Entry<?,?> tab[] = table;
        int hash = key.hashCode();
        int index = (hash & 0x7FFFFFFF) % tab.length;
        @SuppressWarnings("unchecked")
        Entry<K,V> entry = (Entry<K,V>)tab[index];
        for(; entry != null ; entry = entry.next) {
            if ((entry.hash == hash) && entry.key.equals(key)) {
                V old = entry.value;
                entry.value = value;
                return old;
            }
        }

        //添加元素
        addEntry(hash, key, value, index);
        return null;
    }

int hash = key.hashCode();HashTable中key不能为null,否则会报空指针异常

private void addEntry(int hash, K key, V value, int index) {
        modCount++;

        Entry<?,?> tab[] = table;
  
       //判断是否需要扩容,需要的话执行rehash()方法进行扩容
        if (count >= threshold) {
            // Rehash the table if the threshold is exceeded
            //扩容方法
            rehash();

            tab = table;
            hash = key.hashCode();
            index = (hash & 0x7FFFFFFF) % tab.length;
        }

        // Creates the new entry.
        @SuppressWarnings("unchecked")
        Entry<K,V> e = (Entry<K,V>) tab[index];
        tab[index] = new Entry<>(hash, key, value, e);
        count++;
    }
protected void rehash() {
        int oldCapacity = table.length;
        Entry<?,?>[] oldMap = table;

        // overflow-conscious code
        int newCapacity = (oldCapacity << 1) + 1;
        if (newCapacity - MAX_ARRAY_SIZE > 0) {
            if (oldCapacity == MAX_ARRAY_SIZE)
                // Keep running with MAX_ARRAY_SIZE buckets
                return;
            newCapacity = MAX_ARRAY_SIZE;
        }
        Entry<?,?>[] newMap = new Entry<?,?>[newCapacity];

        modCount++;
        threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
        table = newMap;

        for (int i = oldCapacity ; i-- > 0 ;) {
            for (Entry<K,V> old = (Entry<K,V>)oldMap[i] ; old != null ; ) {
                Entry<K,V> e = old;
                old = old.next;

                int index = (e.hash & 0x7FFFFFFF) % newCapacity;
                e.next = (Entry<K,V>)newMap[index];
                newMap[index] = e;
            }
        }
    }

int newCapacity = (oldCapacity << 1) + 1; 从这行代码中可以看出,HashTable扩容时会扩容到原来的2倍+1
2.初始化集合时指定长度

 public Hashtable(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal Load: "+loadFactor);

        if (initialCapacity==0)
            initialCapacity = 1;
        this.loadFactor = loadFactor;
        table = new Entry<?,?>[initialCapacity];
        threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
    }

当集合长度小于0时,抛出非法参数的异常
当集合长度等于0时,集合长度为1
当集合长度大于0时,为指定长度大小。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值