java 集合(1):Arraylist,Vector,Stack,HashMap,LinkedHashMap

概述

###

从上面的集合框架图可以看到,Java 集合框架主要包括两种类型的容器,一种是集合(Collection),存储一个元素集合,另一种是图(Map),存储键/值对映射。

  • Collection 接口又有 3 种子类型,List、Set 和 Queue,再下面是一些抽象类,最后是具体实现类,常用的有 ArrayList、LinkedList、HashSet、LinkedHashSet、HashMap、LinkedHashMap 等等。
接口接口描述
CollectionCollection 是最基本的集合接口,一个 Collection 代表一组 Object,即 Collection 的元素, Java不提供直接继承自Collection的类,只提供继承于的子接口(如List和set)。Collection 接口存储一组不唯一,无序的对象。
ListList接口是一个有序的 Collection,使用此接口能够精确的控制每个元素插入的位置,能够通过索引(元素在List中位置,类似于数组的下标)来访问List中的元素,第一个元素的索引为 0,而且允许有相同的元素。List 接口存储一组不唯一,有序(插入顺序)的对象。
SetSet 具有与 Collection 完全一样的接口,只是行为上不同,Set 不保存重复的元素。Set 接口存储一组唯一,无序的对象。
SortedSet继承于Set保存有序的集合。
Queue是Collection接口的子接口(队列接口),具有队列先入先出的特点。此接口的子类可以实现队列操作。
Map Map接口存储一组键值对象,提供key(键)到value(值)的映射。
Map.Entry描述在一个Map中的一个元素(键/值对)。是一个Map的内部类。
SortedMap继承于 Map,使 Key 保持在升序排列。
Enumeration这是一个传统的接口和定义的方法,通过它可以枚举(一次获得一个)对象集合中的元素。这个传统接口已被迭代器取代。

一、ArrayList类(多线程下不安全)

ArrayList是一个容量能够动态增长的动态数组。但是它又和数组不一样,它继承了AbstractList类,实现了List、RandomAccess、Cloneable、java.io.Serializable接口。

  • 继承了AbstractList,实现了List。它是一个数组队列,提供了相关的添加、删除、修改、遍历等功能。
  • 实现了RandmoAccess接口,即提供了随机访问功能。
  • 实现了Cloneable接口,即覆盖了函数clone(),能被克隆。
  • 实现java.io.Serializable接口,这意味着ArrayList支持序列化,能通过序列化去传输。
    和Vector不同,ArrayList中的操作不是线程安全的。所以,建议在单线程中才使用ArrayList,而在多线程中可以选择Vector或者CopyOnWriteArrayList

ArrayList和数组的区别

  • ArrayList底层是变长数组维护的,不需要定义其大小,如果长度不够了就会自动扩展为原来长度的一倍;而且提供了多种方法方便使用。
  • 数组的大小在定义的时候已经是个固定的值,不会自动扩展,数组的效率比集合的效率高,各有侧重点。

ArrayList的特点

ArrayList底层是通过维护了一个Object数组实现的,查询速度快,在中间插入或者删除时,后面的元素需要逐个移动速度比较慢,时间复杂度为 O(n);在尾部插入或者删除时速度比较快,时间复杂度为O(1);但并不是所有的删除都是慢的。

  • ArrayList中数据可以重复且有序(保证了数据的有序性)、可以向其中存储null值。

  • ArrayList实际上是通过一个数组去保存数据的,当我们构造ArrayList时,如果使用默认构造函数,ArrayList的默认容量大小是10。

  • 为防止集合实例化对象后长时间不使用,导致空间浪费,因此初始化时将集合初始化为空数组,待添加第一个元素时将数组大小扩容为默认大小。

    /**
     * Constructs an empty list with an initial capacity of ten.
     * 
     */
    private static final int DEFAULT_CAPACITY = 10;
    private static final Object[] EMPTY_ELEMENTDATA = {};
    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }
    
  • ArrayList还提供了两种构造方法。

    /**
     * Constructs an empty list with the specified initial capacity.
     *
     * @param  initialCapacity  the initial capacity of the list
     * @throws IllegalArgumentException if the specified initial capacity
     *         is negative
     */
    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);
        }
    }
    
    /**
     * Constructs a list containing the elements of the specified
     * collection, in the order they are returned by the collection's
     * iterator.
     *
     * @param c the collection whose elements are to be placed into this list
     * @throws NullPointerException if the specified collection is null
     */
    public ArrayList(Collection<? extends E> c) {
        elementData = c.toArray();
        if ((size = elementData.length) != 0) {
            // c.toArray might (incorrectly) not return Object[] (see 6260652)
            if (elementData.getClass() != Object[].class)
                elementData = Arrays.copyOf(elementData, size, Object[].class);
        } else {
            // replace with empty array.
            this.elementData = EMPTY_ELEMENTDATA;
        }
    }
    
  • 当ArrayList容量不足以容纳全部元素时,ArrayList会自动扩张容量,新的容量 = 原始容量 + 原始容量 / 2

    /**
     * Increases the capacity to ensure that it can hold at least the
     * number of elements specified by the minimum capacity argument.
     *
     * @param minCapacity the desired minimum capacity
     */
    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);
    }
    
  • ArrayList的克隆函数,即是将全部元素克隆到一个数组中

    /**
     * Returns a shallow copy of this <tt>ArrayList</tt> instance.  (The
     * elements themselves are not copied.)
     *
     * @return a clone of this <tt>ArrayList</tt> instance
     */
    public Object clone() {
        try {
            ArrayList<?> v = (ArrayList<?>) 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(e);
        }
    }
    
  • ArrayList实现java.io.Serializable的方式。当写入到输出流时,先写入“容量”,再依次写出“每一个元素”;当读出输入流时,先读取“容量”,再依次读取“每一个元素”。

二、迭代器

迭代器(iterator)是一种对象,它能够用来遍历标准模板库容器中的部分或全部元素,每个迭代器对象代表容器中的确定的地址

  • 迭代器修改了常规指针的接口,所谓迭代器是一种概念上的抽象:那些行为上像迭代器的东西都可以叫做迭代器。然而迭代器有很多不同的能力,它可以把抽象容器和通用算法有机的统一起来。

  • 迭代器作为一种设计模式,它提供了一种方法顺序访问一个聚合对象中的各个元素,而又无需暴露该对象的内部实现,这样既可以做到不暴露集合的内部结构,又可让外部代码透明地访问集合内部的数据。

  • Iterator是作为一个接口存在的,它定义了迭代器所具有的功能。

    package java.util;
    public interface Iterator<E> {
        boolean hasNext();
        E next();
        void remove();
    }
    

    这三个方法所实现的功能,字面意义就是了。hasNext()方法可以判断对象中是否还存在元素,next()方法可以获取当前元素并移动到下一个元素位置,remove()方法可以删除当前元素。

我们以ArrayList类为例,看看这三个方法如何实现。

/**
 * Returns an iterator over the elements in this list in proper sequence.
 *
 * <p>The returned iterator is <a href="#fail-fast"><i>fail-fast</i></a>.
 *
 * @return an iterator over the elements in this list in proper sequence
 */
public Iterator<E> iterator() {
    return new Itr();
}
/**
 * An optimized version of AbstractList.Itr
 */
private class Itr implements Iterator<E> {
    int cursor;       // index of next element to return
    int lastRet = -1; // index of last element returned; -1 if no such
    int expectedModCount = modCount;

    Itr() {}

    public boolean hasNext() {
        return cursor != size;
    }

    @SuppressWarnings("unchecked")
    public E next() {
        checkForComodification();
        int i = cursor;
        if (i >= size)
            throw new NoSuchElementException();
        Object[] elementData = ArrayList.this.elementData;
        if (i >= elementData.length)
            throw new ConcurrentModificationException();
        cursor = i + 1;
        return (E) elementData[lastRet = i];
    }

    public void remove() {
        if (lastRet < 0)
            throw new IllegalStateException();
        checkForComodification();

        try {
            ArrayList.this.remove(lastRet);
            cursor = lastRet;
            lastRet = -1;
            expectedModCount = modCount;
        } catch (IndexOutOfBoundsException ex) {
            throw new ConcurrentModificationException();
        }
    }
    final void checkForComodification() {
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
    }
}

expectedModCount:

对于上述的代码不难看懂,有点疑惑的是int expectedModCount = modCount;这句代码,其实这是集合迭代中的一种“快速失败”机制,这种机制提供迭代过程中集合的安全性。

  • 阅读源码就可以知道 ArrayList 中存在 modCount 对象,增删操作都会使 modCount++,通过两者的对比迭代器可以快速的知道迭代过程中是否存在list.add() 类似的操作,存在的话快速失败!

如下所示:

Iterator<Character> iterator = list.iterator();
while (iterator.hasNext()) {
    System.out.print(iterator.next() + " ");
    list.add('o');
}

在这里插入图片描述
它的主要作用是: expectedModCount保存的是遍历开始前的modCount,一旦开始遍历expectedModCount值不会发生改变,但是如果在遍历的时候又别的线程调用集合的 add() 或者 remove 方法就会引起 modCount++,从而触发快速失败机制。

迭代器的有点:
1、迭代器可以提供统一的迭代方式。
2、迭代器也可以在对客户端透明的情况下,提供各种不同的迭代方式。
3、迭代器提供一种快速失败机制,防止多线程下迭代的不安全操作

三、LinkedList类(多线程下不安全)

LinkedList和ArrayList有一些相似,我们知道ArrayList是以数组实现,它的优势是查询性能高,劣势是按顺序增删性能差。如果在不确定元素数量的情况时,不建议使用ArrayList。这种情况下,我们就可以使用LinkedList了。

  • LinkedList是以双向链表实现的。既然它是以链表来实现的,所以也会有链表的基本特性。又因为其是使用双向链表来实现的,所以重点还是在于双向链表的特性。

链表无容量限制,但双向链表本身使用了更多空间,也需要额外的链表指针操作。

  • 除了实现List接口外,LinkedList还为在列表的开头及结尾get、remove和insert元素提供了统一的命名方法。这些操作可以将链接列表当作栈,队列和双端队列来使用。

LinkedList继承的类与实现的接口如下:
在这里插入图片描述

  • Collection 接口、List 接口、Cloneable 接口、Serializable 接口、Deque 接口(5个接口)
  • AbstractCollection 类、AbstractList 类、AbstractSequentialList 类(3个类)
    其中Deque定义了一个线性Collection,支持在两端插入和删除元素。

特点:

  • LinkedList是通过双向链表去实现的。
  • 从LinkedList的实现方式中可以看出,因为它底层实现是链表,所以它不存在容量不足的问题。
  • LinkedList实现java.io.Serializable的方式。当写入到输出流时,先写入“容量”,再依次写出“每一个元素”;当读出输入流时,先读取“容量”,再依次读取“每一个元素”。
  • LinkdedList的克隆函数,即是将全部元素克隆到一个新的LinkedList中。
  • 由于LinkedList实现了Deque,而Deque接口定义了在双端队列两端访问元素的方法。提供插入、移除和检查元素的方法。
  • LinkedList可以作为FIFO(先进先出)的队列。
  • LinkedList可以作为LIFO(后进先出)的栈。
  • 遍历LinkedList时,使用removeFirst()或removeLast()效率最高。但是用它们遍历会删除原始数据;若只是单纯的取数据,而不删除,建议用迭代器方式或者foreach方式。
  • 无论如何,千万不要用随机访问去遍历LinkedList!因为这样的效率非常非常低。

LinkedList和ArrayList的对比:

  • 相同点:
    1、接口实现:都实现了List接口,都是线性列表的实现。
    2、线程安全:都是线程不安全的,都是基于fail-fast机制。
  • 不同点:
    1、底层:ArrayList内部是数组实现,而LinkedList内部实现是双向链表结构。
    2、接口:ArrayList实现了RandomAccess可以支持随机元素访问,而LinkedList实现了Deque可以当做队列使用。
    3、性能:新增、删除元素时ArrayList需要使用到拷贝原数组,而LinkedList只需移动指针,查找元素 ArrayList支持随机元素访问,而LinkedList只能一个结点一个结点的去遍历。

四、Vector类(多线程下安全)

Vector是矢量队列,是JDK1.0版本添加的类,他继承于AbstractList类,实现了接口库List,RandomAccess,和Cloneable。

  • Vector实现了List接口,所以它能够提供:增加,删除,修改,遍历等操作。
  • Vector实现RandomAccess接口,所以它能够实现快速访问(即通过索引值就能访问得到)。
  • Vector实现了Cloneable接口,所以它能够被克隆。
  • Vector和ArrayList不同,他的操作是线程安全的。

Vector类和ArrayList类的对比
通过源码分析,我们可以看到,Vector类和ArrayList类基本上是完全相同的,但是又有一些区别。

  • 相同点:
    1、ArrayList类出现于JDK1.2,而Vector类出现于JDK1.0。两者底层的数据存储都使用的Object数组实现,所以都具有查找快,增删慢的特点。
    2、继承的类和实现的接口都是一样的,都继承了AbstractList类(继承后可以使用迭代器遍历),实现了RandomAccess(标记接口,标明实现该接口的list支持快速随机访问),cloneable接口(标识接口,合法调用clone方法),serializable(序列化标识接口)。
  • 不同点:
    1、构造方法。
    ArrayList类的无参构造会首先构造一个空数组,等到添加第一个元素时,会将数组开辟10个空间大小;有参构造则会直接根据指定大小进行开辟数组。
    Vector类的无参构造会直接开辟10个空间大小的数组,并将增长因子设置为0;有参构造则可以根据指定大小开辟数组,并且可以指定增长因子的大小。
    2、扩容模式
    ArrayList类在首次添加元素将数组大小扩大为10个空间之后,以后的扩容操作仅会进行1.5倍扩容。
    Vector类中若是不指定增长因子的大小,会进行2倍扩容;若指定了增长因子后,仅仅会扩容增长因子大小个空间,这样可以大大减少空间的浪费
    3、Vector类的增删改查方法的实现结构,与ArrayList类没有区别,但是方法都用了synchronized来修饰。也就是说Vector类是线程安全的,当多线程访问Vector时,不会引起各种各样的错误。

五、Stack类

Stack类是对数据结构栈的封装容器,它继承于Vector类,由于Vector类是通过数组实现的,这就意味着,Stack也是通过数组实现的,而非链表。当然,我们也可以将LinkedList当作链栈来使用。

java.lang.Objectjava.util.AbstractCollection<E>java.util.AbstractList<E>java.util.Vector<E>java.util.Stack<E>

public class Stack<E> extends Vector<E> {}

在这里插入图片描述

  • 1、Stack类实际上也是通过数组去实现的。
    执行push时(即将元素推入栈中),是通过将元素追加的数组的末尾中。
    执行peek时(即,取出栈顶元素,不执行删除),是返回数组末尾的元素。
    执行pop时(即,取出栈顶元素,并将该元素从栈中删除),是取出数组末尾的元素,然后将该元素从数组中删除。
  • 2、Stack类继承于Vector类,意味着Vector类拥有的属性和功能,Stack类都拥有。

六、HashMap类(多线程不安全)

  • HashMap类是基于哈希表的 Map 接口的实现。此实现提供所有可选的映射操作,并允许使用 null 值和 null 键
  • 它继承于AbstractMap类,实现了Map、Cloneable,java.io.Serializable接口。

HashMap有两个参数影响其性能:初始容量加载因子

  • 初始容量是哈希表在创建时的容量,默认为16个大小。
  • 加载因子默认为0.75,当哈希表中的节点个数超过加载因子当前节点个数时,需要进行2倍扩容操作。

除了不同步和允许使用 null 之外,HashMap 类与 Hashtable 大致相同

从继承的接口来看,有如下特点:

  • 1、Map:是存放一对值的最大接口!即接口中的每个元素都是一对, 以key-value的形式保存,并且Key是不重复的,元素的存储位置由key决定。也就是可以通过key去寻找key-value的位置,从而得到value的值。适合做查找工作。
  • 2、Cloneable:Cloneable是标记型的接口,它们内部都没有方法和属性,实现 Cloneable来表示该对象能被克隆,能使用Object.clone()方法。如果没有实现 Cloneable的类对象调用clone()就会抛出CloneNotSupportedException。
  • 3、Serializable:public interface Serializable类通过实现 java.io.Serializable 接口以启用其序列化功能。
  • 4、Iterator:可以使用迭代器遍历。

HashMap的存储结构

我们都知道HashMap是通过key值进行哈希算法从而计算其所存储的位置的,但是,使用这种方法计算必定会引起哈希冲突。在HashMap中为了减少哈希冲突所带来的影响,HashMap采用了数组+链表形式的存储结构。如图所示:
在这里插入图片描述
其中数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的,

  • 如果定位到的数组位置不含链表(当前entry的next指向null),那么查找,添加等操作很快,仅需一次寻址即可;
  • 如果定位到的数组包含链表,对于添加操作,其时间复杂度为O(n),
  • 首先遍历链表,存在即覆盖,否则新增;对于查找操作来讲,仍需遍历链表,然后通过key对象的equals方法逐一比对查找。所以,性能考虑,HashMap中的链表出现越少,性能才会越好。

为了解决数组槽位上所链接的数据过多(即拉链过长的情况)导致性能下降的问题,

  • JDK1.8在JDK1.7的基础上增加了红黑树来进行优化。
  • 当链表超过8时,链表就转换为红黑树,利用红黑树快速增删改查的特点提高HashMap的性能,其中会用到红黑树的插入、删除、查找等算法。

HashMap的源码分析

1、主要的成员变量
/**实际存储的key-value键值对的个数*/
transient int size;

/**阈值,当table == {}时,该值为初始容量(初始容量默认为16);当table被填充了,也就是为table分配内存空间后,
threshold一般为 capacity*loadFactory。HashMap在进行扩容时需要参考threshold,后面会详细谈到*/
int threshold;

/**负载因子,代表了table的填充度有多少,默认是0.75
加载因子存在的原因,还是因为减缓哈希冲突,如果初始桶为16,等到满16个元素才扩容,某些桶里可能就有不止一个元素了。
所以加载因子默认为0.75,也就是说大小为16的HashMap,到了第13个元素,就会扩容成32。
*/
final float loadFactor;

/**HashMap被改变的次数,由于HashMap非线程安全,在对HashMap进行迭代时,
如果期间其他线程的参与导致HashMap的结构发生变化了(比如put,remove等操作),
需要抛出异常ConcurrentModificationException*/
transient int modCount;
2、构造函数

当采用无参构造时,HashMap会先将table数组赋值为空数组,待第一次添加元素时,会使用默认构造值:initialCapacity默认为16,loadFactory默认为0.75。

/**
 * Constructs an empty <tt>HashMap</tt> with the specified initial
 * capacity and load factor.
 *
 * @param  initialCapacity the initial capacity
 * @param  loadFactor      the load factor
 * @throws IllegalArgumentException if the initial capacity is negative
 *         or the load factor is nonpositive
 */
public HashMap(int initialCapacity, float loadFactor) {
 //此处对传入的初始容量进行校验,最大不能超过MAXIMUM_CAPACITY = 1<<30(230)
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal initial capacity: " +
                                           initialCapacity);
 //初始容量最大不能超过2的30次方
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
 //显然加载因子不能为负数  || 判断是不是一个数字
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new IllegalArgumentException("Illegal load factor: " +
                                           loadFactor);
    
    this.loadFactor = loadFactor;
    threshold = initialCapacity;
    init(); //init方法在HashMap中没有实际实现,不过在其子类如 linkedHashMap中就会有对应实现
}
3、添加函数
public V put(K key, V value) {
        //如果table数组为空数组{},进行数组填充(为table分配实际内存空间),入参为threshold,
        //此时threshold为initialCapacity 默认是1<<4(24=16)
        if (table == EMPTY_TABLE) {
            inflateTable(threshold);
        }
        //如果key为null,存储位置为table[0]或table[0]的冲突链上
        if (key == null)
            return putForNullKey(value);
        int hash = hash(key); //对key的hashcode进一步计算,确保散列均匀
        int i = indexFor(hash, table.length); //获取在table中的实际位置
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            //如果该对应数据已存在,执行覆盖操作。用新value替换旧value,并返回旧value
            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++; //保证并发访问时,若HashMap内部结构发生变化,快速响应失败
        addEntry(hash, key, value, i); //新增一个entry
        return null;
    }
  • inflateTable(threshold) 这个方法用于为主干数组table在内存中分配存储空间。如下:

    private void inflateTable(int toSize) {
            int capacity = roundUpToPowerOf2(toSize);//capacity一定是2的次幂
            /**此处为threshold赋值,取capacity*loadFactor和MAXIMUM_CAPACITY+1的最小值,
            capaticy一定不会超过MAXIMUM_CAPACITY,除非loadFactor大于1 */
            threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
            table = new Entry[capacity];
            initHashSeedAsNeeded(capacity);
        }
    

    通过roundUpToPowerOf2(toSize)可以确保capacity为大于或等于toSize的最接近toSize的二次幂,比如toSize=13,则capacity=16;to_size=16,capacity=16;to_size=17,capacity=32。
    上面代码中的 roundUpToPowerOf2 可以是 数组的长度一定为 2 的次幂。如下所示:

    private static int roundUpToPowerOf2(int number) {
      // assert number >= 0 : "number must be non-negative";
      return number >= MAXIMUM_CAPACITY
              ? MAXIMUM_CAPACITY
              //Integer.highestOneBit是用来获取最左边的bit(其他bit位为0)所代表的数值。
              : (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1;
    }
    
  • hash 函数

    /**这是一个神奇的函数,用了很多的异或,移位等运算
    对key的hashcode进一步进行计算以及二进制位的调整等来保证最终获取的存储位置尽量分布均匀*/
    final int hash(Object k) {
          int h = hashSeed;
          if (0 != h && k instanceof String) {
              return sun.misc.Hashing.stringHash32((String) k);
          }
          
          h ^= k.hashCode();
          
          h ^= (h >>> 20) ^ (h >>> 12);
          return h ^ (h >>> 7) ^ (h >>> 4);
      }
    
  • 以上hash函数计算出的值,通过indexFor进一步处理来获取实际的存储位置

    /**
    * 返回数组下标
    */
    static int indexFor(int h, int length) {
    //h &(length-1)保证获取的index一定在数组范围内,举个例子,默认容量16,length-1=15,h=18,转换成二进制计算为index=2。位运算对计算机来说,性能更高一些(HashMap中有大量位运算)。
      return h & (length-1);
    }
    
  • 所以最终存储位置的确定流程是这样的:
    在这里插入图片描述

  • addEntry 的实现

    void addEntry(int hash, K key, V value, int bucketIndex) {
            if ((size >= threshold) && (null != table[bucketIndex])) {
                resize(2 * table.length); //当size超过临界阈值threshold,并且即将发生哈希冲突时进行扩容
                hash = (null != key) ? hash(key) : 0;
                bucketIndex = indexFor(hash, table.length);
            }
            
            createEntry(hash, key, value, bucketIndex);
        }
    

    通过以上代码能够得知,当发生哈希冲突并且size大于阈值的时候,需要进行数组扩容,扩容时,需要新建一个长度为之前数组2倍的新的数组,然后将当前的Entry数组中的元素全部传输过去,扩容后的新数组长度为之前的2倍,所以扩容相对来说是个耗资源的操作。

    • 我们来继续看上面提到的resize方法

      void resize(int newCapacity) {
              Entry[] oldTable = table;
              int oldCapacity = oldTable.length;
              if (oldCapacity == MAXIMUM_CAPACITY) {
                  threshold = Integer.MAX_VALUE;
                  return;
              }
              
              Entry[] newTable = new Entry[newCapacity];
              transfer(newTable, initHashSeedAsNeeded(newCapacity));
              table = newTable;
              threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
          }
      
    • transfer: 如果数组进行扩容,数组长度发生变化,而存储位置 index = h & (length - 1),index也可能会发生变化,需要重新计算index。

      void transfer(Entry[] newTable, boolean rehash) {
              int newCapacity = newTable.length;
           //for循环中的代码,逐个遍历链表,重新计算索引位置,将老数组数据复制到新数组中去(数组不存储实际数据,所以仅仅是拷贝引用而已)
              for (Entry<K,V> e : table) {
                  while(null != e) {
                      Entry<K,V> next = e.next;
                      if (rehash) {
                          e.hash = null == e.key ? 0 : hash(e.key);
                      }
                      int i = indexFor(e.hash, newCapacity);
                      //将当前entry的next链指向新的索引位置,newTable[i]有可能为空,有可能也是个entry链,如果是entry链,直接在链表头部插入。
                      e.next = newTable[i];
                      newTable[i] = e;
                      e = next;
                  }
              }
          }
      
      
4、 hashmap 二倍扩容的原因

目的就是为了保证数组大小始终为2的幂次方,从而保证使用位运算计算index值时的正确性(因为index的求取方法使用时 return h & (length-1);)。当我们传入指定参数进行构造table的大小时,HashMap会调用private static int roundUpToPowerOf2(int number)方法来确保参数大小始终为2的幂次方

5、 get 方法
 public V get(Object key) {
     //如果key为null,则直接去table[0]处去检索即可。
        if (key == null)
            return getForNullKey();
        Entry<K,V> entry = getEntry(key);
        return null == entry ? null : entry.getValue();
 }

get方法通过key值返回对应value,如果key为null,直接去table[0]处检索。我们再看一下getEntry这个方法。

final Entry<K,V> getEntry(Object key) {
        if (size == 0) {
            return null;
        }
        //通过key的hashcode值计算hash值
        int hash = (key == null) ? 0 : hash(key);
        //indexFor (hash&length-1) 获取最终数组索引,然后遍历链表,通过equals方法比对找出对应记录
        for (Entry<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;
    }

可以看出,get方法的实现相对简单,key(hashcode)–>hash–>indexFor–>最终索引位置,找到对应位置table[i],再查看是否有链表,遍历链表,通过key的equals方法比对查找对应的记录。

  • 要注意的是,有人觉得上面在定位到数组位置之后然后遍历链表的时候,e.hash == hash 这个判断没必要,仅通过equals判断就可以。其实不然,试想一下,如果传入的key对象重写了equals方法却没有重写hashCode,而恰巧此对象定位到这个数组位置,如果仅仅用equals判断可能是相等的,但其hashCode和当前对象不一致,这种情况,根据Object的hashCode的约定,不能返回当前对象,而应该返回null。
6、删除函数
/**
     * Removes the mapping for the specified key from this map if present.
     *
     * @param  key key whose mapping is to be removed from the map
     * @return the previous value associated with <tt>key</tt>, or
     *         <tt>null</tt> if there was no mapping for <tt>key</tt>.
     *         (A <tt>null</tt> return can also indicate that the map
     *         previously associated <tt>null</tt> with <tt>key</tt>.)
     */
    public V remove(Object key) {
        Entry<K,V> e = removeEntryForKey(key);
        return (e == null ? null : e.value);
    }
/**
 * Removes and returns the entry associated with the specified key
 * in the HashMap.  Returns null if the HashMap contains no mapping
 * for this key.
 */
final Entry<K,V> removeEntryForKey(Object key) {
    if (size == 0) {
        return null;
    }
    int hash = (key == null) ? 0 : hash(key);
    int i = indexFor(hash, table.length);
    Entry<K,V> prev = table[i];
    Entry<K,V> e = prev;
    
    while (e != null) {
        Entry<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;
}

七、LinkedHashMap(多线程不安全)

大多数情况下,只要不涉及线程安全问题,Map基本都可以使用HashMap。不过HashMap有一个问题,就是迭代HashMap的顺序并不是HashMap插入的顺序,也就是HashMap在遍历时是无序的。

  • HashMap的这一缺点往往会给我们带来各种困扰,因为有些场景,我们期待一个有序的Map。这个时候,LinkedHashMap就闪亮登场了,它虽然增加了时间和空间上的开销,但是通过维护一个运行于所有条目的双向链表,LinkedHashMap保证了元素迭代的顺序。 该迭代顺序可以是插入顺序或者是访问顺序。

LinkedHashMap的结构

  • 从继承关系可以看到:LinkedHashMap继承于HashMap类,实现了Map类,因此它的特点与HashMap大致相同,唯一不同点即为插入顺序有序

    public class LinkedHashMap<K,V>
        extends HashMap<K,V>
        implements Map<K,V>
    {
        ......
    }
    
  • LinkedHashMap的存储结构:LinkedHashMap可以认为是HashMap+LinkedList,即它既使用HashMap操作数据结构,又使用LinkedList维护插入元素的先后顺序

  • LinkedHashMap的结点结构在继承于HashMap的基础上,增加了 before 和 after 属性来确保插入顺序。并且还维护了头结点 head 和尾结点 tail。如下所示:

    /**
     * The head (eldest) of the doubly linked list.
     */
    transient LinkedHashMap.Entry<K,V> head;
    
    /**
     * The tail (youngest) of the doubly linked list.
     */
    transient LinkedHashMap.Entry<K,V> tail;
    
    /**
     * The iteration ordering method for this linked hash map: <tt>true</tt>
     * for access-order, <tt>false</tt> for insertion-order.
     *
     * @serial
     */
    final boolean accessOrder;
    
    /**
     * HashMap.Node subclass for normal LinkedHashMap entries.
     */
    static class Entry<K,V> extends HashMap.Node<K,V> {
        Entry<K,V> before, after;
        Entry(int hash, K key, V value, Node<K,V> next) {
            super(hash, key, value, next);
        }
    }
    

    在插入数据时,不但需要通过哈希算法进行存储,还需要通过before和after模拟双向链表存储结构,进行插入顺序的维护。
    在这里插入图片描述
    在这里插入图片描述

第一张图为LinkedHashMap整体结构图,第二张图专门把循环双向链表抽取出来,直观一点,注意该循环双向链表的头部存放的是最久访问的节点或最先插入的节点尾部为最近访问的或最近插入的节点

  • 迭代器遍历方向是从链表的头部开始到链表尾部结束,在链表尾部有一个空的header节点,该节点不存放key-value内容,为LinkedHashMap类的成员属性,循环双向链表的入口。

  • LinkedHashMap的使用方法与HashMap完全一致,不同点在于LinkedHashMap重写了HashMap中新增结点的方法,用于维护其插入顺序。

    1、从table角度来看,新的Entry节点插入到数组对应的下标里,当有哈希冲突时,采用头插法将新的Entry插入到冲突链表的头部。头插法解决hash冲突
    2、从头结点head的角度来看,新的Entry节点插入到双向链表的尾部。尾插法解决双向链表问题

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值