JAVA集合

常用的集合类

Collection接口和Map接口是所有集合框架的父接口:

  • Collection接口的子接口包括:Set接口、List接口和Queue接口
  • Map接口的实现类主要有:HashMap、TreeMap、Hashtable、ConcurrentHashMap以及Properties等
  • Set接口的实现类主要有:HashSet、TreeSet、LinkedHashSet等
  • List接口的实现类主要有:ArrayList、LinkedList、Stack以及Vector等
    在这里插入图片描述

Collection

collection接口:单列集合,用来存储一个一个的对象。

List

list接口:存储有序的可重复的数据。

  • ArrayList
    作为List接口的主要实现类;线程不安全,效率高;底层使用Object[ ] elementData存储

源码分析:

1. JDK 1.7

ArrayList list = new ArrayList();//底层创建了长度是10的Object[ ]数组elementData。如果添加数据时,底层elementData数组容量不够,则扩容。默认情况下,扩容为原来容量的1.5倍,同时需要将原有数组中的数据复制到新的数组中。

2. JDK 1.8

ArrayList list = new ArrayList();//底层Object[ ] elementData初始化为{ },并没有创建长度为10的数组。第一次调用add( )时,底层才创建了长度为10的数组,并将数据添加到elementData。后续的添加和扩容操作与jdk 7无异。

// 属性
private static final int DEFAULT_CAPACITY = 10;
private static final Object[] EMPTY_ELEMENTDATA = {};
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
transient Object[] elementData;
private int size;

//构造器
public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }
    
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);
        }
    }
    
//添加元素时,首先判断数组容量是否足够;若足够则添加成功
public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }
    
private void ensureCapacityInternal(int minCapacity) {
        ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
    }
//若elementData为默认的空数组{},则返回默认的容量10;若不是,则返回插入后元素的个数    
private static int calculateCapacity(Object[] elementData, int minCapacity) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            return Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        return minCapacity;
    }
//若插入元素后的个数大于数组的长度,则需要扩容    
private void ensureExplicitCapacity(int minCapacity) {
        modCount++;

        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }
//扩容为之前的1.5倍,并把原数组复制到新的数组   
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);
    }
    
//按照下标删除元素;首先检查下标是否越界,若未越界,则取出对应位置的元素作为返回结果。将index下标后面的元素移动到index开始的地方
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;
    }
    
private void rangeCheck(int index) {
        if (index >= size)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }
//意思是:将src数组里从索引为srcPos的元素开始, 复制到数组dest里的索引为destPos的位置, 复制的元素个数为length个. 
public static native void arraycopy(Object src,  int  srcPos,	Object dest, int destPos,	int length);

//删除指定元素,分为null和非null的情况,这样做是因为null不能调用equals方法,避免空指针异常。
public boolean remove(Object o) {
        if (o == null) {
            for (int index = 0; index < size; index++)
                if (elementData[index] == null) {
                    fastRemove(index);
                    return true;
                }
        } else {
            for (int index = 0; index < size; index++)
                if (o.equals(elementData[index])) {
                    fastRemove(index);
                    return true;
                }
        }
        return false;
    }

  • LinkedList

对于频繁的插入、删除操作、使用此类效率比ArrayList高;底层使用双向链表存储

//内部类
private static class Node<E> {
        E item;
        Node<E> next;
        Node<E> prev;

        Node(Node<E> prev, E element, Node<E> next) {
            this.item = element;
            this.next = next;
            this.prev = prev;
        }
    }
  • Vector

作为List接口最古老的实现类;线程安全的,在ArrayList方法基础上加了synchronized关键字,效率低;底层使用Object [ ] elementData存储,new Vector()//底层创建了长度是10的Object[ ]数组

Set

Set接口:存储无序的、不可重复的数据。

  1. 无序性:不等于随机性。存储的数据在底层数组中并非按照数组索引的顺序添加,而是根据哈希值。

  2. 不可重复性:保证添加的元素按照equals( )判断时,不能返回true。即:相同的元素只能添加一个。

  • HashSet

底层由HashMap实现,可以由源码看到,add,remove,contains方法都是调用的hashmap中的方法。

向HashSet中添加元素a,首先调用元素a所在类的hashCode( )方法,计算元素a的哈希值,此哈希值接着通过某种算法计算出在HashSet底层数组中的存放位置(即为:索引位置),判断数组此位置上是否已经有元素:
1.如果没有,则元素a添加成功。
2.如果有其他元素b(或以链表形式存在的多个元素),则比较元素a与元素b的哈希值:
①如果哈希值不相同,则元素a添加成功。
②如果哈希值相同,进而需要调用元素a所在类的equals方法:
–> equals返回true,元素a添加失败;
–> equals返回false,元素a添加成功;

//属性
//内部使用HashMap
private transient HashMap<E,Object> map;
//虚拟对象,用来作为value放到map中
private static final Object PRESENT = new Object();

//构造器
public HashSet() {
    map = new HashMap<>();
}
//添加元素时,把元素本身作为key,把PRESENT作为value,也就是这个map中所有的value都是一样的。
public boolean add(E e) {
        return map.put(e, PRESENT)==null;
    }
//hashmap中put方法
public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
//remove方法
public boolean remove(Object o) {
        return map.remove(o)==PRESENT;
    }
//contains方法
public boolean contains(Object o) {
        return map.containsKey(o);
    }

总结

  • HashSet:作为Set接口的主要实现类;线程不安全的;可以存储null值

LinkedHashSet:作为HashSet的子类;遍历其内部数据时,可以按照添加的顺序遍历;对于频繁的遍历操作,LinkedHashSet效率高于HashSet

  • TreeSet:可以按照添加对象的指定属性,进行排序

1.向TreeSet中添加的数据,要求是相同类的对象
2.两种排序方式:自然排序 (实现Comparable接口)和 定制排序(Comparator)
3.自然排序中,比较两个对象是否相同的标准为:compareTo( )返回0,不再是equals()

Map

Map接口:双列集合,用来存储一对(key - value)的数据。

HashMap

作为Map的主要实现类;线程不安全的,效率高;存储null的key和value;
底层:
数组 + 链表 ( jdk 1.7及之前 )
数组 + 链表 + 红黑树 ( jdk 1.8 )

源码分析

jdk 1.7
//属性
//Hashmap的初始化大小,初始化的值为16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

 //HashMap是动态扩容的,就是容量大小不能大于 1<<30
static final int MAXIMUM_CAPACITY = 1 << 30;

 //默认的扩容因子,这个值可以通过构造修改
static final float DEFAULT_LOAD_FACTOR = 0.75f;

 //空数据,默认构造的时候赋值为空的Entry数组,在添加元素的时候
 //会判断table=EMPTY_TABLE ,然后就扩容
static final Entry<?,?>[] EMPTY_TABLE = {};

 //表示一个空的Hashmap
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;

 //Hashmap的大小
transient int size;

//初始容量,如果构造传入的值是12,那么这个值就是12,如果没有传入就是默认的容量
//DEFAULT_INITIAL_CAPACITY=16
//扩容的阈值
int threshold;

 //扩容因子,没有传入就使用默认的DEFAULT_LOAD_FACTOR = 0.75f
final float loadFactor;

 //数据操作次数,用于迭代检查修改异常
transient int modCount;

static final int ALTERNATIVE_HASHING_THRESHOLD_DEFAULT = Integer.MAX_VALUE;


//构造方法
public HashMap() {
    this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}

public HashMap(int initialCapacity) {
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
//这个构造方法是传入初始容量和扩容因子
//当需要扩容的时候,根据扩容因子计算进行扩容
public HashMap(int initialCapacity, float loadFactor) {
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal initial capacity: " +
                                           initialCapacity);
                                           
      //当初始容量太大,大于了允许的最大值时,使用最大值                                     
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
        //判断加载因子必须是大于0,而且必须是数字
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new IllegalArgumentException("Illegal load factor: " +
                                           loadFactor);

    this.loadFactor = loadFactor;
    threshold = initialCapacity;
    init();//供子类扩展的方法
}


//初始化时将一个Map集合放入新创建的HashMap
public HashMap(Map<? extends K, ? extends V> m) {
    this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,
                  DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);
    inflateTable(threshold);

    putAllForCreate(m);
}

put()方法

  1. 首次调用put()方法时,table数组一定为空,进入inflateTable()方法。
  2. inflateTable()方法中首先利用roundUpToPowerOf2()方法,将table数组的容量赋值为大于等于传入参数的2的幂次方数。传入的值是16,则此时table数组的容量就是16。并将扩容阈值赋值为容量*负载因子。
  3. 判断要加入的键值对中key的值是否为null。若为null,进入putForNullKey方法。此方法将key为null的键值对都放在table[0]上,若table[0]的链表上已经存在key为null的键值对,则新的将会覆盖旧的,并将旧的value值返回。若不存在,则会用头插法插入链表头部,返回null。key为null的hash值固定为0。
  4. 利用hash()计算key的hash值,临时变量hashseed的值为0,此变量是为了使hash值更加散列。hash()方法中的一系列变换都是为了使hash值更加散列。然后利用indexFor()方法计算key对应在table数组中的下标。
  5. for循环,遍历table[i]位置的链表,相同key的键值对覆盖旧的。若没有找到相同的,则modCount++,然后执行addEntry()方法,即将键值对加入链表。
  6. addEntry()方法首先判断是否需要扩容,若需要扩容,则调用resize()方法。创建一个新的table[]数组,长度为之前的2倍。然后将旧的table数组中的Entry对象移入新的table[]数组中。
public V put(K key, V value) {
    if (table == EMPTY_TABLE) {//初始化数组,容量为16
        inflateTable(threshold);//初始化table数组,源码在下面
    }
    if (key == null)
        return putForNullKey(value);//如果key为空,则调用此方法
    int hash = hash(key);//根据key求出hash值
    int i = indexFor(hash, table.length);//求出在数组中的位置
    for (HashMap.Entry<K,V> e = table[i]; e != null; e = e.next) {//遍历链表找出对应的key,覆盖原有的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;
        }
    }
       //如果没找到,则新增一个Entry,结构改变,modCount加一	
    modCount++;
    addEntry(hash, key, value, i);//源码解释在下面
    return null;
}
private void inflateTable(int toSize) {
    // Find a power of 2 >= toSize
    int capacity = roundUpToPowerOf2(toSize);//初始化容量,默认为16

    threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);//初始化承载量
    table = new Entry[capacity];//初始化table数组
    initHashSeedAsNeeded(capacity);
}

private static int roundUpToPowerOf2(int number) {
    // assert number >= 0 : "number must be non-negative";
    return number >= MAXIMUM_CAPACITY
            ? MAXIMUM_CAPACITY
            : (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1;
}

public static int highestOneBit(int i) {
    // HD, Figure 3-1
    i |= (i >>  1);
    i |= (i >>  2);
    i |= (i >>  4);
    i |= (i >>  8);
    i |= (i >> 16);
    return i - (i >>> 1);
}
//取hash值得算法
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);
}
static int indexFor(int h, int length) {
    // assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
    return h & (length-1);//查找entry在数组中存放的位置
}
void addEntry(int hash, K key, V value, int bucketIndex) {
    if ((size >= threshold) && (null != table[bucketIndex])) {//判断是不是需要扩容,扩容以后需要重新算hash值和数组下标位置
        resize(2 * table.length);
        hash = (null != key) ? hash(key) : 0;
        bucketIndex = indexFor(hash, table.length);//源码向下看
    }

    createEntry(hash, key, value, bucketIndex);//源码向下看
}
private V putForNullKey(V value) {
    for (Entry<K,V> e = table[0]; e != null; e = e.next) {//遍历链表,如果找到key值为null,将value赋值给对应的value
        if (e.key == null) {
            V oldValue = e.value;
            e.value = value;
            e.recordAccess(this);
            return oldValue;
        }
    }
	//如果没找到,链表会增加一个节点,结构变化了,modCount加一
    modCount++;
    addEntry(0, null, value, 0);//添加一个key为null,值为value的Entry
    return null;
}
void createEntry(int hash, K key, V value, int bucketIndex) {
    Entry<K,V> e = table[bucketIndex];//bucketIndex位置值赋值给e
    table[bucketIndex] = new Entry<>(hash, key, value, e);//new 一个Entry放在bucketIndex
    size++;
}
//扩容 当元素数量>=承载量时,进行扩容
void resize(int newCapacity) {
    Entry[] oldTable = table;
    int oldCapacity = oldTable.length;
    if (oldCapacity == MAXIMUM_CAPACITY) {//容量为2的幂次,最大为2的30次方,所以一直扩容肯定有等于最大幂次的时候
        threshold = Integer.MAX_VALUE;//这时就把Integer的最大值给承载量
        return;
    }

    Entry[] newTable = new Entry[newCapacity];//创建新的table
    transfer(newTable, initHashSeedAsNeeded(newCapacity));//判断新的table中的元素是否需要重新求hash值,源码在下面
    table = newTable;//数组扩容赋值给table
    threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);//算出新的承载量
}
void transfer(Entry[] newTable, boolean rehash) {
    int newCapacity = newTable.length;
    for (Entry<K,V> e : table) {// 遍历旧表,如果需要重新求hash值就进行rehash操作
        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);
            e.next = newTable[i];
            newTable[i] = e;
            e = next;
        }
    }
}
final boolean initHashSeedAsNeeded(int capacity) {
    boolean currentAltHashing = hashSeed != 0;
    boolean useAltHashing = sun.misc.VM.isBooted() &&
            (capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
    boolean switching = currentAltHashing ^ useAltHashing;
    if (switching) {
        hashSeed = useAltHashing
            ? sun.misc.Hashing.randomHashSeed(this)
            : 0;
    }
    return switching;
}

remove()方法

public V remove(Object key) {
    Entry<K,V> e = removeEntryForKey(key);
    return (e == null ? null : e.value);
}

final HashMap.Entry<K,V> removeEntryForKey(Object key) {
    if (size == 0) {//size表示hashmap中 HashMap.Entry对象的个数,如果为零,表示空map
        return null;
    }
    int hash = (key == null) ? 0 : hash(key);//根据key求出hash值
    int i = indexFor(hash, table.length);//求出元素在哪个数组位置
    HashMap.Entry<K,V> prev = table[i];//取出对应的链表
    HashMap.Entry<K,V> e = prev;//遍历用,存储遍历元素的前一个元素或者当前entry

    while (e != null) {
        HashMap.Entry<K,V> next = e.next;
        Object k;
        if (e.hash == hash &&
                ((k = e.key) == key || (key != null && key.equals(k)))) {//遍历链表,找到对应的key
            modCount++;//找到对应的元素,map结构改变一次,modCount加一
            size--;//HashMap.Entry对象的个数减一
            if (prev == e)//如果当前entry就是要找的,直接将下一个entry放在数组对应位置,(要删除元素在链表头部)
                table[i] = next;
            else
                prev.next = next;//从中间删除节点,直接让被删元素上一个entry指向它的下一个entry
            e.recordRemoval(this);//这个不知道干嘛的
            return e;
        }
	   //如果当前节点不是要找的元素,继续遍历链表	
        prev = e;
        e = next;
    }

    return e;
}

get()方法

public V get(Object key) {
        //如果键为null,调用getForNullKey方法
        if (key == null)
            return getForNullKey();
        //键不为null,调用getEntry方法
        Entry<K,V> entry = getEntry(key);

        return null == entry ? null : entry.getValue();
    }

private V getForNullKey() {
        if (size == 0) {
            return null;
        }
        //键为null的插入在第一个桶中
        for (Entry<K,V> e = table[0]; e != null; e = e.next) {
            if (e.key == null)
                return e.value;
        }
        return null;
    }

 final Entry<K,V> getEntry(Object key) {
        if (size == 0) {
            return null;
        }

        //计算hash值
        int hash = (key == null) ? 0 : hash(key);
        //遍历桶中的链表
        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;
    }
jdk 1.8
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
//最大容量,2的30次方。
static final int MAXIMUM_CAPACITY = 1 << 30;
//加载因子,用于扩容使用。
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//当某个桶节点数量大于8时,且整个hashMap中元素数量大于64时,会转换为红黑树。
static final int TREEIFY_THRESHOLD = 8;
//当某个桶节点数量小于6时,会转换为链表,前提是它当前是红黑树结构。
static final int UNTREEIFY_THRESHOLD = 6;

static final int MIN_TREEIFY_CAPACITY = 64;
//存储元素的数组,transient关键字表示该属性不能被序列化
transient Node<K,V>[] table;
//将数据转换成set的另一种存储形式,这个变量主要用于迭代功能。
transient Set<Map.Entry<K,V>> entrySet;
//元素数量
transient int size;
//统计该map修改的次数
transient int modCount;
//临界值,也就是元素数量达到临界值时,会进行扩容。
int threshold;
//加载因子
final float loadFactor; 

//红黑树内部类
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
        TreeNode<K,V> parent;  
        TreeNode<K,V> left;
        TreeNode<K,V> right;
        TreeNode<K,V> prev;    
        boolean red;
        TreeNode(int hash, K key, V val, Node<K,V> next) {
            super(hash, key, val, next);
        }
}
//节点内部类
static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        V value;
        Node<K,V> next;
 
        Node(int hash, K key, V value, Node<K,V> next) {
            this.hash = hash;
            this.key = key;
            this.value = value;
            this.next = next;
        }
}

//构造器
public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; 
	}

public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }
    
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);
}

put()方法

//hash值计算方法
static final int hash(Object key) {
    int h;
    /**key的hashCode异或上key的hashCode进行无符号右移16位的结果,避免了只靠低位数据来计算哈希时导致的冲突,计算结果由高低位结合决定,使元素分布更均匀;**/
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
public V put(K key, V value) {
    /**四个参数,第一个hash值,第四个参数表示如果该key存在值,如果为null的话,则插入新的value,最后一个参数,在hashMap中没有用,可以不用管,使用默认的即可**/
    return putVal(hash(key), key, value, false, true);
}

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    //tab 哈希数组,p 该哈希桶的首节点,n hashMap的长度,i 计算出的数组下标
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    //获取长度并进行扩容,使用的是懒加载,table一开始是没有加载的,等put后才开始加载
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    /**如果计算出的该哈希桶的位置没有值,则把新插入的key-value放到此处,此处就算没有插入成功,也就是发生哈希冲突时也会把哈希桶的首节点赋予p**/
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    //发生哈希冲突的几种情况
    else {
        // e 临时节点的作用, k 存放该当前节点的key 
        Node<K,V> e; K k;
        //第一种,插入的key-value的hash值,key都与当前节点的相等,e = p,则表示为首节点
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        //第二种,hash值不等于首节点,判断该p是否属于红黑树的节点
        else if (p instanceof TreeNode)
            /**为红黑树的节点,则在红黑树中进行添加,如果该节点已经存在,则返回该节点(不为null),该值很重要,用来判断put操作是否成功,如果添加成功返回null**/
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        //第三种,hash值不等于首节点,不为红黑树的节点,则为链表的节点
        else {
            //遍历该链表
            for (int binCount = 0; ; ++binCount) {
                //如果找到尾部,则表明添加的key-value没有重复,在尾部进行添加
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    //判断是否要转换为红黑树结构
                    if (binCount >= TREEIFY_THRESHOLD - 1) 
                        treeifyBin(tab, hash);
                    break;
                }
                //如果链表中有重复的key,e则为当前重复的节点,结束循环
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        //有重复的key,则用待插入值进行覆盖,返回旧值。
        if (e != null) { 
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    //到了此步骤,则表明待插入的key-value是没有key的重复,因为插入成功e节点的值为null
    //修改次数+1
    ++modCount;
    //实际长度+1,判断是否大于临界值,大于则扩容
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    //添加成功
    return null;
}

扩容

final Node<K,V>[] resize() {
    //把没插入之前的哈希数组做我诶oldTal
    Node<K,V>[] oldTab = table;
    //old的长度
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    //old的临界值
    int oldThr = threshold;
    //初始化new的长度和临界值
    int newCap, newThr = 0;
    //oldCap > 0也就是说不是首次初始化,因为hashMap用的是懒加载
    if (oldCap > 0) {
        //大于最大值
        if (oldCap >= MAXIMUM_CAPACITY) {
            //临界值为整数的最大值
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        //标记##,其它情况,扩容两倍,并且扩容后的长度要小于最大值,old长度也要大于16
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            //临界值也扩容为old的临界值2倍
            newThr = oldThr << 1; 
    }
    /**如果oldCap<0,但是已经初始化了,像把元素删除完之后的情况,那么它的临界值肯定还存在,        
       如果是首次初始化,它的临界值则为0
    **/
    else if (oldThr > 0) 
        newCap = oldThr;
    //首次初始化,给与默认的值
    else {               
        newCap = DEFAULT_INITIAL_CAPACITY;
        //临界值等于容量*加载因子
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    //此处的if为上面标记##的补充,也就是初始化时容量小于默认值16的,此时newThr没有赋值
    if (newThr == 0) {
        //new的临界值
        float ft = (float)newCap * loadFactor;
        //判断是否new容量是否大于最大值,临界值是否大于最大值
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                  (int)ft : Integer.MAX_VALUE);
    }
    //把上面各种情况分析出的临界值,在此处真正进行改变,也就是容量和临界值都改变了。
    threshold = newThr;
    //表示忽略该警告
    @SuppressWarnings({"rawtypes","unchecked"})
        //初始化
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    //赋予当前的table
    table = newTab;
    //此处自然是把old中的元素,遍历到new中
    if (oldTab != null) {
        for (int j = 0; j < oldCap; ++j) {
            //临时变量
            Node<K,V> e;
            //当前哈希桶的位置值不为null,也就是数组下标处有值,因为有值表示可能会发生冲突
            if ((e = oldTab[j]) != null) {
                //把已经赋值之后的变量置位null,当然是为了好回收,释放内存
                oldTab[j] = null;
                //如果下标处的节点没有下一个元素
                if (e.next == null)
                    //把该变量的值存入newCap中,e.hash & (newCap - 1)并不等于j
                    newTab[e.hash & (newCap - 1)] = e;
                //该节点为红黑树结构,也就是存在哈希冲突,该哈希桶中有多个元素
                else if (e instanceof TreeNode)
                    //把此树进行转移到newCap中
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                else { /**此处表示为链表结构,同样把链表转移到newCap中,就是把链表遍历后,把值转过去,在置位null**/
                    Node<K,V> loHead = null, loTail = null;
                    Node<K,V> hiHead = null, hiTail = null;
                    Node<K,V> next;
                    do {
                        next = e.next;
                        if ((e.hash & oldCap) == 0) {
                            if (loTail == null)
                                loHead = e;
                            else
                                loTail.next = e;
                            loTail = e;
                        }
                        else {
                            if (hiTail == null)
                                hiHead = e;
                            else
                                hiTail.next = e;
                            hiTail = e;
                        }
                    } while ((e = next) != null);
                    if (loTail != null) {
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    //返回扩容后的hashMap
    return newTab;
}

remove()方法

public V remove(Object key) {
    //临时变量
    Node<K,V> e;
    /**调用removeNode(hash(key), key, null, false, true)进行删除,第三个value为null,表示,把key的节点直接都删除了,不需要用到值,如果设为值,则还需要去进行查找操作**/
    return (e = removeNode(hash(key), key, null, false, true)) == null ?
        null : e.value;
}

/**第一参数为哈希值,第二个为key,第三个value,第四个为是为true的话,则表示删除它key对应的value,不删除key,第四个如果为false,则表示删除后,不移动节点**/
final Node<K,V> removeNode(int hash, Object key, Object value,
                           boolean matchValue, boolean movable) {
    //tab 哈希数组,p 数组下标的节点,n 长度,index 当前数组下标
    Node<K,V>[] tab; Node<K,V> p; int n, index;
    //哈希数组不为null,且长度大于0,然后获得到要删除key的节点所在是数组下标位置
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (p = tab[index = (n - 1) & hash]) != null) {
        //nodee 存储要删除的节点,e 临时变量,k 当前节点的key,v 当前节点的value
        Node<K,V> node = null, e; K k; V v;
        //如果数组下标的节点正好是要删除的节点,把值赋给临时变量node
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            node = p;
        //也就是要删除的节点,在链表或者红黑树上,先判断是否为红黑树的节点
        else if ((e = p.next) != null) {
            if (p instanceof TreeNode)
                //遍历红黑树,找到该节点并返回
                node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
            else { //表示为链表节点,一样的遍历找到该节点
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key ||
                         (key != null && key.equals(k)))) {
                        node = e;
                        break;
                    }
                    /**注意,如果进入了链表中的遍历,那么此处的p不再是数组下标的节点,而是要删除结点的上一个结点**/
                    p = e;
                } while ((e = e.next) != null);
            }
        }
        //找到要删除的节点后,判断!matchValue,我们正常的remove删除,!matchValue都为true
        if (node != null && (!matchValue || (v = node.value) == value ||
                             (value != null && value.equals(v)))) {
            //如果删除的节点是红黑树结构,则去红黑树中删除
            if (node instanceof TreeNode)
                ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
            //如果是链表结构,且删除的节点为数组下标节点,也就是头结点,直接让下一个作为头
            else if (node == p)
                tab[index] = node.next;
            else /**为链表结构,删除的节点在链表中,把要删除的下一个结点设为上一个结点的下一个节点**/
                p.next = node.next;
            //修改计数器
            ++modCount;
            //长度减一
            --size;
            /**此方法在hashMap中是为了让子类去实现,主要是对删除结点后的链表关系进行处理**/
            afterNodeRemoval(node);
            //返回删除的节点
            return node;
        }
    }
    //返回null则表示没有该节点,删除失败
    return null;
}

ConcurrentHashMap

HashMap在多线程的情况下,会产生死循环和元素丢失的现象;ConcurrentHashMap在多线程的情况下使用

源码分析

jdk 1.7
//属性
final int segmentMask;  // 用于定位段,大小等于segments数组的大小减 1,是不可变的 
final int segmentShift;    // 用于定位段,大小等于32(hash值的位数)减去对segments的大小取以2为底的对数值,是不可变的
final Segment<K,V>[] segments;   // ConcurrentHashMap的底层结构是一个Segment数组
  • 段的定义

Segment 类继承于 ReentrantLock 类,从而使得 Segment 对象能充当锁的角色。每个 Segment 对象用来守护它的成员对象 table 中包含的若干个桶。table 是一个由 HashEntry 对象组成的链表数组,table 数组的每一个数组成员就是一个桶。
  在Segment类中,count 变量是一个计数器,它表示每个 Segment 对象管理的 table 数组包含的 HashEntry 对象的个数,也就是 Segment 中包含的 HashEntry 对象的总数。特别需要注意的是,之所以在每个 Segment 对象中包含一个计数器,而不是在 ConcurrentHashMap 中使用全局的计数器,是对 ConcurrentHashMap 并发性的考虑:因为这样当需要更新计数器时,不用锁定整个ConcurrentHashMap。事实上,每次对段进行结构上的改变,如在段中进行增加/删除节点(修改节点的值不算结构上的改变),都要更新count的值,此外,在JDK的实现中每次读取操作开始都要先读取count的值。特别需要注意的是,count是volatile的,这使得对count的任何更新对其它线程都是立即可见的。modCount用于统计段结构改变的次数,主要是为了检测对多个段进行遍历过程中某个段是否发生改变,这一点具体在谈到跨段操作时会详述。threashold用来表示段需要进行重哈希的阈值。loadFactor表示段的负载因子,其值等同于ConcurrentHashMap的负载因子的值。table是一个典型的链表数组,而且也是volatile的,这使得对table的任何更新对其它线程也都是立即可见的。段(Segment)的定义如下:

static final class Segment<K,V> extends ReentrantLock implements Serializable {

    transient volatile int count;    // Segment中元素的数量,可见的

    transient int modCount;  //对count的大小造成影响的操作的次数(比如put或者remove操作)

    transient int threshold;      // 阈值,段中元素的数量超过这个值就会对Segment进行扩容

    transient volatile HashEntry<K,V>[] table;  // 链表数组

    final float loadFactor;  // 段的负载因子,其值等同于ConcurrentHashMap的负载因子
    ...
}
  • HashEntry

HashEntry用来封装具体的键值对,是个典型的四元组。与HashMap中的Entry类似,HashEntry也包括同样的四个域,分别是key、hash、value和next。不同的是,在HashEntry类中,key,hash和next域都被声明为final的,value域被volatile所修饰,因此HashEntry对象几乎是不可变的,这是ConcurrentHashmap读操作并不需要加锁的一个重要原因。next域被声明为final本身就意味着我们不能从hash链的中间或尾部添加或删除节点,因为这需要修改next引用值,因此所有的节点的修改只能从头部开始。对于put操作,可以一律添加到Hash链的头部。但是对于remove操作,可能需要从中间删除一个节点,这就需要将要删除节点的前面所有节点整个复制(重新new)一遍,最后一个节点指向要删除结点的下一个结点(这在谈到ConcurrentHashMap的删除操作时还会详述)。特别地,由于value域被volatile修饰,所以其可以确保被读线程读到最新的值,这是ConcurrentHashmap读操作并不需要加锁的另一个重要原因。实际上,ConcurrentHashMap完全允许多个读操作并发进行,读操作并不需要加锁。HashEntry代表hash链中的一个节点,其结构如下所示:

static final class HashEntry<K,V> {
    final K key;                       // 声明 key 为 final 的
    final int hash;                   // 声明 hash 值为 final 的
    volatile V value;                // 声明 value 被volatile所修饰
    final HashEntry<K,V> next;      // 声明 next 为 final 的

    HashEntry(K key, int hash, HashEntry<K,V> next, V value) {
        this.key = key;
        this.hash = hash;
        this.next = next;
        this.value = value;
    }

    @SuppressWarnings("unchecked")
    static final <K,V> HashEntry<K,V>[] newArray(int i) {
        return new HashEntry[i];
    }
}
//构造器
/**
 * Creates a new, empty map with a default initial capacity (16),
 * load factor (0.75) and concurrencyLevel (16).
 */
@SuppressWarnings("unchecked")
public ConcurrentHashMap(int initialCapacity,
                         float loadFactor, int concurrencyLevel) {
    if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0)
        throw new IllegalArgumentException();
    if (concurrencyLevel > MAX_SEGMENTS)
        concurrencyLevel = MAX_SEGMENTS;
    // Find power-of-two sizes best matching arguments
    int sshift = 0;         // 大小为 lg(ssize)
    int ssize = 1;          // 段的数目,segments数组的大小(2的幂次方)
    while (ssize < concurrencyLevel) {
        ++sshift;
        ssize <<= 1;
    }
    this.segmentShift = 32 - sshift;    // 用于定位段
    this.segmentMask = ssize - 1;       // 用于定位段
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
    int c = initialCapacity / ssize;    // 总的桶数/总的段数
    if (c * ssize < initialCapacity)
        ++c;
    int cap = MIN_SEGMENT_TABLE_CAPACITY;   // 默认最少2个
    while (cap < c)
        cap <<= 1;      //求出大于c的最小2次幂数
    // create segments and segments[0]
    Segment<K,V> s0 =
        new Segment<K,V>(loadFactor, (int)(cap * loadFactor),
                         (HashEntry<K,V>[])new HashEntry[cap]);
    Segment<K,V>[] ss = (Segment<K,V>[])new Segment[ssize];
    UNSAFE.putOrderedObject(ss, SBASE, s0); // ordered write of segments[0]
    this.segments = ss;
}

//Segment构造方法
 Segment(float lf, int threshold, HashEntry<K,V>[] tab) {
            this.loadFactor = lf;
            this.threshold = threshold;
            this.table = tab;
        }

put()方法
在这里插入图片描述

向ConcurrentHashMap中put一个key/value时,首先获得key的hash值并对其再hash,然后根据最终的hash值定位到对应的segment段。定位段的方法segmentFor()会根据传入的hash值向右无符号右移segmentShift位,然后和segmentMask进行与操作就可以定位到特定的段。进一步地,我们就可以得出以下结论:根据key的hash值的高n位就可以确定元素到底在哪一个Segment中。然后,调用这个段的put()方法。
在将key/value对插入到Segment之前,首先会检查本次插入会不会导致Segment中元素的数量超过阈值threshold,如果会,那么就先对Segment进行扩容和重哈希操作,然后再进行插入。第8和第9行的操作就是定位到段中特定的桶并确定链表头部的位置。第12行的while循环用于检查该桶中是否存在相同key的结点,如果存在,就直接更新value值;如果没有找到,则进入21行生成一个新的HashEntry并且把它链到该桶中链表的表头,然后再更新count的值(由于count是volatile变量,所以count值的更新一定要放在最后一步)。

@SuppressWarnings("unchecked")
public V put(K key, V value) {
    Segment<K,V> s;
    if (value == null)
        throw new NullPointerException();
    int hash = hash(key);
    int j = (hash >>> segmentShift) & segmentMask;
    if ((s = (Segment<K,V>)UNSAFE.getObject          // nonvolatile; recheck
         (segments, (j << SSHIFT) + SBASE)) == null) //  in ensureSegment
        s = ensureSegment(j);
    return s.put(key, hash, value, false);
}

@SuppressWarnings("unchecked")
private Segment<K,V> ensureSegment(int k) {
    final Segment<K,V>[] ss = this.segments;
    long u = (k << SSHIFT) + SBASE; // raw offset
    Segment<K,V> seg;
    if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u)) == null) {
        Segment<K,V> proto = ss[0]; // use segment 0 as prototype
        int cap = proto.table.length;
        float lf = proto.loadFactor;
        int threshold = (int)(cap * lf);
        HashEntry<K,V>[] tab = (HashEntry<K,V>[])new HashEntry[cap];
        if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
            == null) { // recheck
            Segment<K,V> s = new Segment<K,V>(lf, threshold, tab);
            while ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
                   == null) {
                if (UNSAFE.compareAndSwapObject(ss, u, null, seg = s))
                    break;
            }
        }
    }
    return seg;
}

final V put(K key, int hash, V value, boolean onlyIfAbsent) {
    HashEntry<K,V> node = tryLock() ? null : scanAndLockForPut(key, hash, value);
    V oldValue;
    try {
        HashEntry<K,V>[] tab = table;
        int index = (tab.length - 1) & hash;
        HashEntry<K,V> first = entryAt(tab, index);
        for (HashEntry<K,V> e = first;;) {
            if (e != null) {
                K k;
                if ((k = e.key) == key ||
                    (e.hash == hash && key.equals(k))) {
                    oldValue = e.value;
                    if (!onlyIfAbsent) {
                        e.value = value;
                        ++modCount;
                    }
                    break;
                }
                e = e.next;
            }
            else {
                if (node != null)
                    node.setNext(first);
                else
                    node = new HashEntry<K,V>(hash, key, value, first);
                int c = count + 1;
                if (c > threshold && tab.length < MAXIMUM_CAPACITY)
                    rehash(node);
                else
                    setEntryAt(tab, index, node);
                ++modCount;
                count = c;
                oldValue = null;
                break;
            }
        }
    } finally {
        unlock();
    }
    return oldValue;
}

在ConcurrentHashMap中使用put操作插入Key/Value对之前,首先会检查本次插入会不会导致Segment中节点数量超过阈值threshold,如果会,那么就先对Segment进行扩容和重哈希操作。特别需要注意的是,ConcurrentHashMap的重哈希实际上是对ConcurrentHashMap的某个段的重哈希,因此ConcurrentHashMap的每个段所包含的桶位自然也就不尽相同。

当超过限制的时候会resize,然而又因为我们使用的是2次幂的扩展(指长度扩为原来2倍),所以,元素的位置要么是在原位置,要么是在原位置再移动2次幂的位置。我们可以消除不必要的节点创建,通过捕获哪些旧的节点是可以被重用的,因为它们的next引用不会改变。根据统计,在默认的阈值下,在double resize时仅有六分之一的节点需要进行复制操作。被它们替换的节点是将会被垃圾回收的,只要他们不再被任何在扩容中途的读线程引用。

private void rehash(HashEntry<K,V> node) {        
    HashEntry<K,V>[] oldTable = table;
    int oldCapacity = oldTable.length;
    int newCapacity = oldCapacity << 1;
    threshold = (int)(newCapacity * loadFactor);
    HashEntry<K,V>[] newTable =
        (HashEntry<K,V>[]) new HashEntry[newCapacity];
    int sizeMask = newCapacity - 1;
    for (int i = 0; i < oldCapacity ; i++) {
        HashEntry<K,V> e = oldTable[i];
        if (e != null) {
            HashEntry<K,V> next = e.next;
            int idx = e.hash & sizeMask;
            if (next == null)   //  Single node on list
                newTable[idx] = e;
            else { // Reuse consecutive sequence at same slot
                HashEntry<K,V> lastRun = e;
                int lastIdx = idx;
                for (HashEntry<K,V> last = next;
                     last != null;
                     last = last.next) {
                    int k = last.hash & sizeMask;
                    if (k != lastIdx) {
                        lastIdx = k;
                        lastRun = last;
                    }
                }
                newTable[lastIdx] = lastRun;
                // Clone remaining nodes
                for (HashEntry<K,V> p = e; p != lastRun; p = p.next) {
                    V v = p.value;
                    int h = p.hash;
                    int k = h & sizeMask;
                    HashEntry<K,V> n = newTable[k];
                    newTable[k] = new HashEntry<K,V>(h, p.key, v, n);
                }
            }
        }
    }
    int nodeIndex = node.hash & sizeMask; // add the new node
    node.setNext(newTable[nodeIndex]);
    newTable[nodeIndex] = node;
    table = newTable;
}

由于扩容是按照2的幂次方进行的,所以扩展前在同一个桶中的元素,现在要么还是在原来的序号的桶里,或者就是原来的序号再加上一个2的幂次方,就这两种选择。根据本文前面对HashEntry的介绍,我们知道链接指针next是final的,因此看起来我们好像只能把该桶的HashEntry链中的每个节点复制到新的桶中(这意味着我们要重新创建每个节点),但事实上JDK对其做了一定的优化。因为在理论上原桶里的HashEntry链可能存在一条子链,这条子链上的节点都会被重哈希到同一个新的桶中,这样我们只要拿到该子链的头结点就可以直接把该子链放到新的桶中,从而避免了一些节点不必要的创建,提升了一定的效率。因此,JDK为了提高效率,它会首先去查找这样的一个子链,而且这个子链的尾节点必须与原hash链的尾节点是同一个,那么就只需要把这个子链的头结点放到新的桶中,其后面跟的一串子节点自然也就连接上了。对于这个子链头结点之前的结点,JDK会挨个遍历并把它们复制到新桶的链头(只能在表头插入元素)中。

get()方法

在剖析ConcurrentHashMap的put操作时,我们就知道ConcurrentHashMap不同于HashMap,它既不允许key值为null,也不允许value值为null。但是,此处怎么会存在键值对存在且的Value值为null的情形呢?JDK官方给出的解释是,这种情形发生的场景是:初始化HashEntry时发生的指令重排序导致的,也就是在HashEntry初始化完成之前便返回了它的引用。这时,JDK给出的解决之道就是加锁重读。

public V get(Object key) {
    Segment<K,V> s; // manually integrate access methods to reduce overhead
    HashEntry<K,V>[] tab;
    int h = hash(key);
    long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;
    if ((s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)) != null &&
        (tab = s.table) != null) {
        for (HashEntry<K,V> e = (HashEntry<K,V>) UNSAFE.getObjectVolatile
             (tab, ((long)(((tab.length - 1) & h)) << TSHIFT) + TBASE);
             e != null; e = e.next) {
            K k;
            if ((k = e.key) == key || (e.hash == h && key.equals(k)))
                return e.value;
        }
    }
    return null;
}

ConcurrentHashMap 读操作不需要加锁的奥秘

HashEntry对象几乎是不可变的(只能改变Value的值),因为HashEntry中的key、hash和next指针都是final的。这意味着,我们不能把节点添加到链表的中间和尾部,也不能在链表的中间和尾部删除节点。这个特性可以保证:在访问某个节点时,这个节点之后的链接不会被改变,这个特性可以大大降低处理链表时的复杂性。与此同时,由于HashEntry类的value字段被声明是Volatile的,因此Java的内存模型就可以保证:某个写线程对value字段的写入马上就可以被后续的某个读线程看到。此外,由于在ConcurrentHashMap中不允许用null作为键和值,所以当读线程读到某个HashEntry的value为null时,便知道产生了冲突 —— 发生了重排序现象,此时便会加锁重新读入这个value值。这些特性互相配合,使得读线程即使在不加锁状态下,也能正确访问 ConcurrentHashMap。总的来说,ConcurrentHashMap读操作不需要加锁的奥秘在于以下三点:

  • 用HashEntery对象的不变性来降低读操作对加锁的需求;
  • 用Volatile变量协调读写线程间的内存可见性;
  • 若读时发生指令重排序现象,则加锁重读;

remove()方法

删除节点C之后的所有节点原样保留到新链表中;删除节点C之前的每个节点被克隆到新链表中(它们在新链表中的链接顺序被反转了)。因此,在执行remove操作时,原始链表并没有被修改,也就是说,读线程不会受同时执行 remove 操作的并发写线程的干扰。
在这里插入图片描述

public V remove(Object key) {
    int hash = hash(key);
    Segment<K,V> s = segmentForHash(hash);
    return s == null ? null : s.remove(key, hash, null);
}


final V remove(Object key, int hash, Object value) {
    if (!tryLock())
        scanAndLock(key, hash);
    V oldValue = null;
    try {
        HashEntry<K,V>[] tab = table;
        int index = (tab.length - 1) & hash;
        HashEntry<K,V> e = entryAt(tab, index);
        HashEntry<K,V> pred = null;
        while (e != null) {
            K k;
            HashEntry<K,V> next = e.next;
            if ((k = e.key) == key ||
                (e.hash == hash && key.equals(k))) {
                V v = e.value;
                if (value == null || value == v || value.equals(v)) {
                    if (pred == null)
                        setEntryAt(tab, index, next);
                    else
                        pred.setNext(next);
                    ++modCount;
                    --count;
                    oldValue = v;
                }
                break;
            }
            pred = e;
            e = next;
        }
    } finally {
        unlock();
    }
    return oldValue;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值