Java集合源码解析


在这里插入图片描述

1、集合包

  集合包是Java中最常用的包,它最常用的有Collection和Map两个接口的实现类,Collection用于存放多个单对象,Map用子存放Key-Value形式的键值对。
  Collection中常用的又分为两种类型的接口:List和Set,两者最明显的差别为List支持放入重复的对象,而Set不支持。List接口常用的实现类有:ArrayList,LinkedList,Vector及Stack:Set接口常用的实现类有:HashSet,TreeSet,对于Collection的实现类而言,要重点掌握的为以下几点:

  • Collection的创建
    对应的为Collection实现类的构造器,需要掌握在构造器方法中Collection的实现类都做了些什么。
  • 往Collection中增加对象
    对应的为Collection中的add(E)方法,往Collection中增加对象时Collection的实现方式决定了此方法的性能。
  • 删除Collection中的对象
    对应的为Collection中的remove(E)方法,实现类的实现方式决定了此方法的性能。
  • 获取Collection中的单个对象
    对应的为Collection中的get(int index)方法,实现类的实现方式决定了此方法的性能。
  • 遍历Collection中的对象
    对应的是通过Collection的iterator方法获取迭代器,进而遍历。
  • 判断对象是否存在于Collection中
    对应的是Collection中的contains(E)方法,实现类的实现方式决定了此方法的性能。
  • Collection中对象的排片
    如何对Collection中对象合理地排序也是使用Collection对象时经常要考虑的问题,但由于排序主要取决于所采取的排序算法,在此就不多讲解了。

按照这几点来分析下常用的List和Set实现类的实现方式。

1.1 ArrayList

实现方式

  对应上述要掌握的几点,来看看ArrayList的实现方式。

创建:ArrayList()

  此默认构造器通过调用ArrayList(int)来完成ArrayList的创建,传入 的值为10,ArrayList(int)方法的实现代码为:

/**
     * 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) {
        super(); //父类中空的构造方法
        if (initialCapacity < 0)    //判断如果自定义大小的容量小于0,则报下面这个非法数据异常
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        this.elementData = new Object[initialCapacity]; //将自定义的容量大小当成初始化elementData的大小
    }

  super()调用的为AbstractList的默认构造器方法,该方法为一个空方法,因此这段代码中最关键的是实例化了一个Object的数组,并将此数组赋给了当前实例的elementData属性,此Object数组的大小即为传入的initialCapacity的值,因此在调用空构造器的情况下会创建一个大小为10的Object数组。据此也可看出,ArrayList采用的是数组的方式来存放对象。

插入对象:add(E)

  add方法简单来看就是将数组中某元素的值赋值为传入的对象,但在add时有个很明显的问题是:如果此时数组满了,该怎么办?带着这个问题,来看看ArrayList的实现方式。
  当调用ArrayList的add方法时,首先基于ArrayList中已有的元素数量加1,产生一个名为minCapacity的变量,然后比较此值和Obiect数组的大小,如果此值大于Object数组值,那么先将当前的Obiect数组值赋给一个数组对象,接着产生一个新的数组的容量值。此值的计算方法为当前数组值x1.5+1,如得出的容量值仍然小于minCapacity,那么就以minCapacity作为新的容量值,在得出这个容量值后,调用Arrays.copyOf来生成新的数组对象,如想调整容量的增长策略,可继承ArrayList,并覆盖ensureCapacity方法。
  Arrays.copyOf的实现方法简单来说,首先是创建一个新的数组对象,该数组对象的类型和之前ArrayList中元素的类型是一致的,在这里JDK做了个小优化。如果是Object类型,则直接通过new Object[newLength]的方式来创建。如不是Object类型,则通过Array.newInstance调用native方法来创建相应类型的数组;在创建完新的数组对象后,调用System.arraycopy通过native方法将之前数组中的对象复制到新的数组中。
  在确保有足够的空间放入新的元素后,可将数组的末尾赋值为传入的对象,例如目前数组值为10,已经有5个元素了,那么新加入的元素就自动放在数组6的位置。
  在Collection中增加对象时,ArrayList还提供了add(int,E)这样的方法,允许将元素直接插入指定的int位置上,这个方法的实现首先要确保插入的位置是目前的Array数组中存在的,之后还要确保数组的容量是够用的。在完成了这些动作后,和add(E)不同的地方就出现了,它要将当前的数组对象进行一次复制,即将目前index及其后的数据都往后挪动一位,然后才能将指定的index位置的赋值为传入的对象,可见这种方式要多付出一次复制数组的代价。
  除了add(int,E)这种方法可将对象插入指定的位置外,ArrayList还提供了set(int.E)这样的方法来替换指定位置上的对象。
  为了方便开发人员的使用,ArrayList还对外提供了addAll(Collection<?extends E>)及addAll(int,Collection<?extends E>)这样的方法,其实现方式和add(E),add(int,E)基本类似。

删除对象:remove(E)

  remove对于集合的性能而言也非常重要,当执行此方法时,ArrayList首先判断对象是否为null,如为null,则遍历数组中已有值的元素,并比较其是否为null,如为null,则调用fastRemove来删除相应位置的对象。fastRemove方法的实现方式为将index后的对象往前复制一位,并将数组中的最后一个元素的值设置为null,即释放了对此对象的引用;如对象非null,唯一的不同在于通过E的equals来比较元素的值是否相同,如相同则认为是需要删除对象的位置,然后同样是调用fastRemove来完成对象的删除。
  ArrayList中还另外提供了remove(int)这样的方法来删除指定位置的对象,remove(int)的实现比remove(E)多了一个数组范围的检测,但少了对象位置的查找,因此性能会更好。

获取单个对象:get(int)

get传入的参数为数组元素的位置,因此ArrayList仅须先做数组范围的检测,然后即可直接返回数组中位于此位置的对象

遍历对象:iterator()

  iterator由ArrayList的父类AbstractList实现,当每次调用iterator方法时,都会创建一个新的AbstractList内部类Itr的实例。当调用此实例的hasNext方法时,比较当前指向的数组的位置是否和数组中已有的元素大小相等,如相等则返回false,否则返回true
  当调用实例的next方法时,首先比较在创建此Iterator时获取的modCount与目前的modCount,如果这两个modCount不相等,则说明在获取next元素时,发生了对于集合大小产生影响(新增、删除)的动作。当发生这种情况时,则抛出ConcurrentModificationException。如果modCount相等,则调用get方法来获取相应位置的元素,当get获取不到时抛出IndexOutOBoundsException,在捕捉到IndexOutOfBoundsException后,检测modCount,如modCount相等,则抛出NoSuchElementException,如不相等,则抛出ConcurrentModificationException

判断对象是否存在:contains(E)

  为了判断E在ArrayList中是否存在,做法为遍历整个ArrayList中已有的元素,如E为null,则直接判断已有元素是否为null,如为null,则返回true:如E不为null,则通过判断E.equals和元素是否相等,如相等则返回true。
  indexOf和lastIndexOf是ArrayList中用于获取对象所在位置的方法,其中indexOf为从前往后寻找,而lastIndexOf为从后向前寻找。

注意要点

对于ArrayList而言,最须注意的有以下几点:

  • ArrayList基于数组方式实现,无容量的限制;
  • ArrayList在执行插入元素时可能要扩容,在删除元素时并不会减小数组的容量(如希望相应的缩小数组容量,可以调用ArrayList的trimToSize)),在查找元素时要遍历数组,对于非null的元素采取equals的方式寻找;
  • ArrayList是非线程安全的。

1.2 LinkedList

实现方式

  LinkedList也是常用的一种List实现,LinkedList基于双向链表机制,所谓双向链表’机制,就是集合中的每个元素都知道其前一个元素及其后一个元素的位置。在LinkedList中,以一个内部的Entry类来代表集合中的元素,元素的值赋给element属性,Entry中的next属性指向元素的后一个元素,Entry中的previous属性指向元素的前一个元素,基于这样的机制可以快速实现集合中元素的移动。LinkedList的具体实现方式如下:

LinkedList()

  在创建LinkedList对象时,应首先创建一个element属性为null,next属性为null及previous属性为null的Entry对象,并赋值给全局的header属性。
  在执行构造器时,LinkedList将header的next及previous都指向header,以形成双向链表所需的闭环

add(E)

  当向LinkedList中增加元素时,要做的就是创建一个Entry对象,并将此Entry对象的next指向header,previous指向header.previous。在完成自己的next,previous的设置后,同时将位于当前元素的后一元素的previous指向自己,并将位于当前元素的前一元素的next指向自己,这样就保持了双向链表的闭环。
  LinkedList的add方法不用像ArrayList那样,要考虑扩容及复制数组的问题,但它每增加一个元素,都要创建一个新的Entry对象,并要修改相邻的两个元素的属性。

remove(E)

  要删除LinkedList的一个元素,首先同样要遍历整个LinkedList中的元素,遍历和寻找匹配的元素的方法和ArrayList基本相同,寻找到相匹配的元素后,删除元素的方法比ArrayList简单很多。
  删除时只须直接删除链表上的当前元素,并将当前元素中的element,previous及next属性设置为null,即可完成对象的删除。
  这个动作比ArrayList删除元素简单很多,毕竟ArrayList还要将当前元素所在的位置后的元素通过复制往前移动一位

get(int)

  由于LinkedList的元素并没有存储在一个数组中,因此其get操作过程比ArrayList更为复杂,在执行get操作时,首先要判断传入的index值是否小于0或者大于等于当前LinkedList的size值。如符合这两个条件之一,则抛出IndexOutOBoundsException,如不符合,则进行下面的步骤。首先判断当前要获取的位置是否小于LinkedList值的一半,如小于,则从头一直找到index位置所对应的next元素;如大于,则从队列的尾部往前,一直找到index位置所对应的previous元素。

iterator()

  iterator方法由父类AbstractList实现,当调用iterator方法时,每次都会创建一个Listltr对象,创建时该对象负责保存cursor(光标,游标的意思)位置
  当调用iterator返回的遍历对象的hasNext方法时,判断当前cursor的位置是否等于LinkedList的size变量,如等于则返回true,不等于则返回false.
  当调用iterator返回的遍历对象的next方法时,调用get方法实现,传入cursor位置,即可获取到相应的元素对象。如果在遍历过程中,LinkedList中的元素增加或删除,则会抛出ConcurrentModificationException
  由于LinkedList是基于双向链接实现的,因此其在遍历时还可往前遍历,通过调用hasPrevious和previous来完成遍历过程。

contains(E)

  为了判断E是否存在于LinkedList中,LinkedList采用的方法是遍历所有元素。如传入的E为null,则判断元素是否为null,如找到为null的元素,则返回true:如传入的E非null,则判断E是否有equals元素,如找到equals的元素,则返回true,如遍历完仍未找到匹配的元素,则返回false

注意要点

对LinkedList而言,最要注意以下几点:

  • LinkedList基于双向链表机制实现:
  • LinkedList在插入元素时,须创建一个新的Entry对象,并切换相应元素的前后元素的引用;在查找元素时,须遍历链表;在删除元素时,要遍历链表,找到要删除的元素,然后从链表上将此元素删除即可:
  • LinkedList是非线程安全的。

1.3 Vector

实现方式

  Vector是从JDK 1.2就已提供的List实现,Vector和ArraylList一样,也是基于Object数组的方式来实现的,其具体实现如下。

Vector()

  默认创建一个大小为10的Object数组,并将capacitylncrement设置为0

add(E)

  Vector中的add方法是加了synchronized关键字的,因此此方法是线程安全的。除此之外,它和ArrayList基本相同,不同点是当数组大小不够时,其扩大数组的方法有所不同,Vector的策略为:
  如果capcacityIncrement大于0,则将Object数组的大小扩大为现有size加上capacitylncrement的值:如果capacitylncrement等于或小于0,则将Object数组的大小扩大为现有size的两倍,这种容量的控制策略比ArrayList更为可控。

remove(E)

  除了其调用的removeElement方法上有synchronized关键字外,和ArrayList完全相同。

get(int)

  此方法同样有synchronized关键字,实现和ArrayList相同。

iterator

  和ArrayList的实现完全相同。

contains(E)

  和ArrayList唯一的不同是contains中调用的indexOf方法是有synchronized关键字的。
根据上面的分析,可以看出,Vector除了扩大数组时采用的方法和ArrayList不同及线程安全外,其他实现完全相同。

注意要点

  对于Vector而言,最要注意的只有一点:Vector是基于Synchronized实现的线程安全的ArrayList,但在插入元素时容量扩充的机制和ArrayList稍有不同,并可通过传入capacitylIncrement来控制容量的扩充。

1.4 Stack

实现方式

  Stack继承于Vector,在其基础上实现了Stack所要求的后进先出(LIFO)的弹出及压入操作,其提供了push,pop,peek三个主要的方法:push push操作通过调用Vector中的addElement来完成。

push

push操作通过调用Vector中的addElement来完成。

pop

pop操作通过调用peek来获取元素,并同时删除数组的最后一个元素。

peek

peek操作通过获取当前Object数组的大小,并获取数组上的最后一个元素。

注意要点

对于Stack而言,要注意的只有一点:Stack基于Vector实现,支持LIFO.

1.5 Hashset

实现方式

HashSet是Set接口的实现,Set和List最明显的区别在于Set不允许元素重复,而List允许。Set为了做到不允许元素重复,采用的是基于HashMap来实现,HashMap在1.7节中再行讲解,在此先看看HashSet的实现方式。

HashSet()

此时所要做的为创建个HashMap对象。

add(E)

调用HashMap的putObject,Object)方法来完成此操作,将需要增加的元素作为Map中的key,value则传入一个之前已创建的Obiect对象

remove(E)

调用HashMap的remove(E)方法来完成此操作。

contains(E)

调用HashMap的containsKey(E)方法来完成此操作。

iterator()

调用HashMap的keySet的iterator方法来完成此操作。
HashSet不支持通过get(in)获取指定位置的元素,只能自行通过itrator方法来获取。

注意要点

对于HashSet而言,最要注意的有以下几点:HashSet基于HashMap实现,无容量限制:HashSet是非线程安全的。

1.6 TreeSet

实现方式

TreeSet和HashSet的主要不同在于TreeSet对于排序的支持,TreeSet基于TreeMap实现,来看看它的具体实现方式。

TreeSet()

此时所要做的是创建一个TreeMap对象。

add(E)

调用TreeMap的put(Object,Object)方法完成此操作,用要增加的元素作为key,用之前已创建的一个final的Object对象作为value.

remove(E)

调用TreeMap的remove(Object)方法完成此操作。

iterator()

调用TreeMap的navigablekeySet的iterator方法完成此操作。
综上所述,TreeSet和HashSet一样,也是完全基于Map来完成的,并且同样也不支持get(int)来获取指定位置的元素,只是TreeSet基于的是TreeMap,除了这些基本的Set实现外,TreeSet还提供了一些排序方面的支持。例如传入Comparator实现、descendingSet及descendingltrator等

注意要点

对于TreeSet而言,最要注意的有以下几点:TreeSet基于TreeMap实现,支持排序;TreeSet是非线程安全的。
以上分析了Java提供的若干常用单对象存储的集合对象之实现。除了这些单对象存储的集合对象外,Java还提供了key-value的集合对象,接口为Map,常用的主要有HashMap.TreeMap,接下来就来看看这两个对象的实现方法。

1.7 Hashmap (jdk1.7)

什么是哈希表

我们知道,数据结构的物理存储结构只有两种:顺序存储结构和链式存储结构(像栈,队列,树,图等是从逻辑结构去抽象的,映射到内存中,也这两种物理组织形式),而在上面我们提到过,在数组中根据下标查找某个元素,一次定位就可以达到,哈希表利用了这种特性,哈希表的主干就是数组。

比如我们要新增或查找某个元素,我们通过把当前元素的关键字 通过某个函数映射到数组中的某个位置,通过数组下标一次定位就可完成操作。

存储位置 = f(关键字)

其中,这个函数f一般称为哈希函数,这个函数的设计好坏会直接影响到哈希表的优劣。

哈希冲突

然而万事无完美,如果两个不同的元素,通过哈希函数得出的实际存储地址相同怎么办?也就是说,当我们对某个元素进行哈希运算,得到一个存储地址,然后要进行插入的时候,发现已经被其他元素占用了,其实这就是所谓的哈希冲突,也叫哈希碰撞。前面我们提到过,哈希函数的设计至关重要,好的哈希函数会尽可能地保证 计算简单和散列地址分布均匀,但是,我们需要清楚的是,数组是一块连续的固定长度的内存空间,再好的哈希函数也不能保证得到的存储地址绝对不发生冲突。那么哈希冲突如何解决呢?哈希冲突的解决方案有多种:开放定址法(发生冲突,继续寻找下一块未被占用的存储地址),再散列函数法,链地址法,而HashMap即是采用了链地址法,也就是数组+链表的方式,

HashMap实现原理

HashMap的主干是一个Entry数组。Entry是HashMap的基本组成单元,每一个Entry包含一个key-value键值对。
 简单来说,HashMap由数组+链表组成的,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的,如果定位到的数组位置不含链表(当前entry的next指向null),那么对于查找,添加等操作很快,仅需一次寻址即可;如果定位到的数组包含链表,对于添加操作,其时间复杂度为O(n),首先遍历链表,存在即覆盖,否则新增;对于查找操作来讲,仍需遍历链表,然后通过key对象的equals方法逐一比对查找。所以,性能考虑,HashMap中的链表出现越少,性能才会越好。

//实际存储的key-value键值对的个数
transient int size;
//阈值,当table == {}时,该值为初始容量(初始容量默认为16);当table被填充了,也就是为table分配内存空间后,threshold一般为 capacity*loadFactory。HashMap在进行扩容时需要参考threshold,后面会详细谈到
int threshold;
//负载因子,代表了table的填充度有多少,默认是0.75
final float loadFactor;
//用于快速失败,由于HashMap非线程安全,在对HashMap进行迭代时,如果期间其他线程的参与导致HashMap的结构发生变化了(比如put,remove等操作),需要抛出异常ConcurrentModificationException
transient int modCount;

HashMap有4个构造器,其他构造器如果用户没有传入initialCapacity 和loadFactor这两个参数,会使用默认值

initialCapacity默认为16,loadFactory默认为0.75

我们看下其中一个

public HashMap(int initialCapacity, float loadFactor) {     //此处对传入的初始容量进行校验,最大不能超过MAXIMUM_CAPACITY = 1<<30(230)
        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;
        threshold = initialCapacity;     
        init();//init方法在HashMap中没有实际实现,不过在其子类如 linkedHashMap中就会有对应实现
    }

从上面这段代码我们可以看出,在常规构造器中,没有为数组table分配内存空间(有一个入参为指定Map的构造器例外),而是在执行put操作的时候才真正构建table数组

OK,接下来我们来看看put操作的实现吧

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

Put:Put时如果table数组为空数组{},进行数组填充(为table分配实际内存空间),入参为threshold,此时threshold为initialCapacity 默认是1<<4(24=16);如果key为null,则调用putForNullKey方法,存储位置为table[0]或table[0]的冲突链上(我说的table就是HashMap中存的数组),若不为空则先int hash = hash(key);,然后int i = indexFor(hash, table.length);//获取在table中的实际位置i,然后for (Entry<K,V> e = table[i]; e != null; e = e.next),如果table数组在这个位置有元素,就会进入这个循环,遍历链表,比较是否存在相同的key,若存在则覆盖原来key的value,并返回旧value,否则将该元素保存在链头(保证插入O(1))(最先保存的元素放在链尾)。若table在该处没有元素,或者for循环后发现链表中没有我要put的key,就会往下执行addEntry,将“key-value”添加到table中。

addEntry()

void addEntry(int hash, K key, V value, int bucketIndex)
作用:添加键值对(Entry )到 HashMap中
插入前,先判断容量是否足够 若不足够(容量size > 阈值threshold),则进入resize方法,进行扩容(2倍)、重新计算Hash值、重新计算存储数组下标  
若容量足够,则向下执行createEntry方法,创建1个新的数组元素(Entry) 并放入到数组中
resize(2 * table.length)void resize(int newCapacity) {  
    
    // 1. 保存旧数组(old table) 
    Entry[] oldTable = table;  

    // 2. 保存旧容量(old capacity ),即数组长度
    int oldCapacity = oldTable.length; 

    // 3. 若旧容量已经是系统默认最大容量了,那么将阈值设置成整型的最大值,退出    
    if (oldCapacity == MAXIMUM_CAPACITY) {  
        threshold = Integer.MAX_VALUE;  
        return;  
    }  
  
    // 4. 根据新容量(2倍容量)新建1个数组,即新table  
    Entry[] newTable = new Entry[newCapacity];  

    // 5. 将旧数组上的数据(键值对)转移到新table中,从而完成扩容 ->>分析1.1 
    transfer(newTable); 

    // 6. 新数组table引用到HashMap的table属性上
    table = newTable;  

    // 7. 重新设置阈值  
    threshold = (int)(newCapacity * loadFactor); 
} 

————————————————————————————

transfer()

void transfer(Entry[] newTable) {
      // 1. src引用了旧数组
      Entry[] src = table; 

      // 2. 获取新数组的大小 = 获取新容量大小                 
      int newCapacity = newTable.length;

      // 3. 通过遍历 旧数组,将旧数组上的数据(键值对)转移到新数组中
      for (int j = 0; j < src.length; j++) { 
      	  // 3.1 取得旧数组的每个元素  
          Entry<K,V> e = src[j];           
          if (e != null) {
              // 3.2 释放旧数组的对象引用(for循环后,旧数组不再引用任何对象)
              src[j] = null; 

              do { 
                  // 3.3 遍历 以该数组元素为首 的链表
                  // 注:转移链表时,因是单链表,故要保存下1个结点,否则转移后链表会断开
                  Entry<K,V> next = e.next; 
                 // 3.4 重新计算每个元素的存储位置
                 int i = indexFor(e.hash, newCapacity); 
                 // 3.5 将元素放在数组上:采用单链表的头插入方式 = 在链表头上存放数据 = 将数组位置的原有数据放在后1个指针、将需放入的数据放到数组位置中
                 // 即 扩容后,可能出现逆序:按旧链表的正序遍历链表、在新链表的头部依次插入
                 e.next = newTable[i]; 
                 newTable[i] = e;  
                 // 3.6 访问下1个Entry链上的元素,如此不断循环,直到遍历完该链表上的所有节点
                 e = next;             
             } while (e != null);
             // 如此不断循环,直到遍历完数组上的所有数据元素
         }
     }
 }

最终存储位置的确定流程是这样的:
在这里插入图片描述

get()

get方法的实现相对简单,key(hashcode-返回int)–>hash–>indexFor–>最终索引位置,找到对应位置table[i],再查看是否有链表,遍历链表,通过key的equals方法比对查找对应的记录。(&length-1也将范围较大的hash值缩小到了length内)
我们知道java.util.HashMap不是线程安全的,因此如果在使用迭代器的过程中有其他线程修改了map,那么将抛出ConcurrentModificationException,这就是所谓fail-fast策略。这一策略在源码中的实现是通过modCount域,modCount顾名思义就是修改次数,对HashMap内容的修改都将增加这个值,那么在迭代器初始化过程中会将这个值赋给迭代器的expectedModCount。在迭代过程中,判断modCount跟expectedModCount是否相等,如果不相等就表示已经有其他线程修改了Map
int capacity = 1;
while (capacity < initialCapacity)
capacity <<= 1;
这段代码保证初始化时HashMap的容量总是2的n次方,即底层数组的长度总是为2的n次方。
由 Object 类定义的 hashCode 方法确实会针对不同的对象返回不同的整数。(这一般是通过将该对象的内部地址转换成一个整数来实现的)

1.8 TreeMap

实现方式

TreeMap是一个支持排序的Map实现,其实现方式和HashMap并不相同,下面具体看看它的实现方式。

TreeMap()

在此步TreeMap只是将comparator属性赋值为null,如希望控制TreeMap中元素的存储顺序,可使用带Comparator参数的构造器。

put(Object key,Object value)

当调用put时,先要判断root属性是否为null,如是,则创建一个新的Entry对象,并赋值给root属性。如不是,则首先判断是否传入了指定的Comparator实现,如己传入,则基于红黑树的方式遍历,基于comparator来比较key应放在树的左边还是右边,如找到相等的key,则直接替换其value,并返回结束put操作,如没有找到相等的key,则一直寻找到左边或右边节点为null的元素,如comparator实现为null,则判断key是否为null,是则抛出NullPointerException,并将key造型为Comparable,进行与上面同样的遍历和比较过程。
通过上面的步骤,如未找到相同的key,则进入以下过程,即创建一个新的Entry对象,并将其parent设置为上面所寻找到的元素,并根据和parent key比较的情况来设置parent的left或right属性。
综上所述,TreeMap是一个典型的基于红黑树的实现,因此它要求一定要有key比较的方法,要么传入Comparator实现,要么kev对象实现Comparable接口。

get(Object)

TreeMap在查找key时就是个典型的红黑树查找过程,从根对象开始往下比较,一直找到相等的key,并返回其value
和put时同样的处理方式,如未传入Comparator实现,当传入的Object为null时,则直接抛出NullPointerException

remove(Object)

remove首先要做的是getEntry,然后则是将此Entry从红黑树上删除,并重新调整树上的相关的节点。

containsKey(Object)

和get方法一样,都通过getEntry方法来完成,因此过程和get方法一样,只是containsKey直接判断返回的Entry是否为null,为null则返回false,不为null则返回true.

keySet()

调用keySet方法后返回TreeMap的内部类KeySet对象的实例,iterator的遍历从根开始,基于红黑树顺序完成。

注意要点

对TreeMap而言,最应了解的有以下几点:

  • TreeMap基于红黑树实现,无容量限制
  • TreeMap是非线程安全的。
  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值