当前位置: 首页 > news >正文

八股文之jdk源码分析

JDK源码分析

String

通过加号拼接字符串分析

string字符串我们一般都是字面量赋值形式,声明的变量值放在常量池,一般字符串拼接,通过+形式,其实这是jdk做了优化,并不是表面看到连接符,比如说现在字符串a,字符串b,通过反编译对应字节码文件,加号变成 new stringbuilder,append方法,String 是不可变的对象(final), 因此在每次对 String 类型进行改变的时候其实都等同于在堆中生成了一个新的 String 对象,然后将指针指向新的 String 对象,这样不仅效率低下,而且大量浪费有限的内存空间,所以经常改变内容的字符串最好不要用 String 。因为每次生成对象都会对系统性能产生影响,特别是当内存中的无引用对象过多了以后, JVM 的 GC 开始工作,那速度是一定会相当慢的。另外当GC清理速度跟不上new String的速度时,还会导致内存溢出Error

image-20220907194214355

list

ArrayList

image-20220906031825505

初始化

重要属性

 	/**
     * Default initial capacity. 默认容量
     */
    private static final int DEFAULT_CAPACITY = 10;

    /**
     * Shared empty array instance used for empty instances.
     * 用于空实例的共享空数组实例
     */
    private static final Object[] EMPTY_ELEMENTDATA = {};

    /**
     * Shared empty array instance used for default sized empty instances. We
     * distinguish this from EMPTY_ELEMENTDATA to know how much to inflate when
     * first element is added.
     * 用于默认大小的空实例的共享空数组实例
     * 我们区分这从EMPTY_ELEMENTDATA直到多少膨胀时添加第一个元素。
     */
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

    /**
     * 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 == DEFAULTCAPACITY_EMPTY_ELEMENTDATA
     * will be expanded to DEFAULT_CAPACITY when the first element is added.
     * 数组列表的元素存储在数组缓冲区中。ArrayList的容量就是这个数组缓冲区的长度
     * 任何带elementData的空数组列表== DEFAULTCAPACITY_EMPTY_ELEMENTDATA将在添加第一个元素时扩展为DEFAULT_CAPACITY。
     */
    transient Object[] elementData; // non-private to simplify nested class access

    /**
     * The size of the ArrayList (the number of elements it contains).
     * 数组列表的大小
     * @serial
     */
    private int size;

构造方法

    /**
     * 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 an empty list with an initial capacity of ten.
     * 创建一个list初始容量10
     */
    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

add方法

    /**
     * Appends the specified element to the end of this list.
     * 追加元素在list
     * @param e element to be appended to this list
     * @return <tt>true</tt> (as specified by {@link Collection#add})
     */
    public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!增量 modCount
        elementData[size++] = e;
        return true;
    }

    /**
     * Inserts the specified element at the specified position in this
     * list. Shifts the element currently at that position (if any) and
     * any subsequent elements to the right (adds one to their indices).
     * 指定位置插入元素,后面元素向右移动
     * @param index index at which the specified element is to be inserted
     * @param element element to be inserted
     * @throws IndexOutOfBoundsException {@inheritDoc}
     */
    public void add(int index, E element) {
        rangeCheckForAdd(index);

        ensureCapacityInternal(size + 1);  // Increments modCount!!
        System.arraycopy(elementData, index, elementData, index + 1,
                         size - index);
        elementData[index] = element;
        size++;
    }

	/**
	 * 确保是否是第一次添加元素,如果是list初始化容量
	 * 否则判断是否需要扩容
	 **/
    private void ensureCapacityInternal(int minCapacity) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
        }

        ensureExplicitCapacity(minCapacity);
    }

    /**
     * 判断是否需要扩容
     **/
    private void ensureExplicitCapacity(int minCapacity) {
        modCount++;

        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }

   /**
     * Increases the capacity to ensure that it can hold at least the
     * number of elements specified by the minimum capacity argument.
     * 1.5倍扩容
     * @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第一次添加元素,调用add(E)方法,首先是判断是否是第一次添加元素,如果是初始化数组容量10

如果不是第一次元素,则判断添加该元素之后,是否容量是否够用;

如果不够用的话,进行扩容操作,原来数组元素+原来数组右移一位,调用arrays.copyOf(elementData,newCapacity);

get方法

    /**
         * Returns the element at the specified position in this list.
         * 返回数据指定位置的元素
         * @param  index index of the element to return 数组中索引位置
         * @return the element at the specified position in this list
         * @throws IndexOutOfBoundsException {@inheritDoc}
         */
    public E get(int index) {
        rangeCheck(index);

        return elementData(index);
    }

    /**
     * Checks if the given index is in range.  If not, throws an appropriate
     * runtime exception.  This method does *not* check if the index is
     * negative: It is always used immediately prior to an array access,
     * which throws an ArrayIndexOutOfBoundsException if index is negative.
     * 方法不检查索引是否为负数,它只检查索引是否大于或等于数组的长度。
     * 如果索引是负数,则数组访问会抛出ArrayIndexOutOfBoundsException。
     */
    private void rangeCheck(int index) {
        if (index >= size)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }

详细说明

如果数组是个空数组,size 为 0 ,而你又去取第一个元素 get(0),元素都没有,所以就会报错。

list根据指定位置获取元素,首先判断位置是否大于数组长度,如果大于抛出IndexOutOfBoundsException异常

否则返回指定位置的元素

remove方法

/**
     * Removes the element at the specified position in this list.
     * Shifts any subsequent elements to the left (subtracts one from their
     * indices).
     * 移除指定位置元素
     * 将所有后续元素向左移动(从它们的元素中减去1)*指数)。
     * @param index the index of the element to be removed
     * @return the element that was removed from the list
     * @throws IndexOutOfBoundsException {@inheritDoc}
     */
    public E remove(int index) {
        rangeCheck(index);
		// 数组结构性变化 modCount++
        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;
    }

	/**
     * Checks if the given index is in range.  If not, throws an appropriate
     * runtime exception.  This method does *not* check if the index is
     * negative: It is always used immediately prior to an array access,
     * which throws an ArrayIndexOutOfBoundsException if index is negative.
     * 方法不检查索引是否为负数,它只检查索引是否大于或等于数组的长度。
     * 如果索引是负数,则数组访问会抛出ArrayIndexOutOfBoundsException。
     */
    private void rangeCheck(int index) {
        if (index >= size)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }

 	/**
     * Removes the first occurrence of the specified element from this list,
     * if it is present.  If the list does not contain the element, it is
     * unchanged.  More formally, removes the element with the lowest index
     * <tt>i</tt> such that
     * <tt>(o==null&nbsp;?&nbsp;get(i)==null&nbsp;:&nbsp;o.equals(get(i)))</tt>
     * (if such an element exists).  Returns <tt>true</tt> if this list
     * contained the specified element (or equivalently, if this list
     * changed as a result of the call).
     *
     * @param o element to be removed from this list, if present
     * @return <tt>true</tt> if this list contained the specified element
     */
    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;
    }

    /*
     * Private remove method that skips bounds checking and does not
     * return the value removed.
     */
    private void fastRemove(int index) {
        modCount++;
        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
    }

详细说明

1、根据指定位置移除元素

首先判断指定位置是否合法,看它是否大于数组长度,如果大于抛出IndexOutOfBoundsException异常

否则,指定位置合适,之后modCount++,由于数组结构性变化,之后看一下移除元素之后是否需要

移动元素,计算数组长度-1-指定位置 如果大于0,通过

System.arraycopy(elementData, index+1, elementData, index,
                 numMoved);
左移,否则最后一个元素,不需要移动位置,最后一个位置置为null,这样方便之后将整个数组不再使用时,会被GC,数组长度-1


2、根据元素删除
首先判断指定元素是否null,如果
不为null,遍历数组找到元素对应索引位置,指定位置合适,之后modCount++,由于数组结构性变化,之后看一下移除元素之后是否需要

移动元素,计算数组长度-1-指定位置 如果大于0,通过System.arraycopy方法左移,否则最后一个元素,不需要移动位置,最后一个位置置为null,这样方便之后将整个数组不再使用时,会被GC ,数组长度-1

set方法

    /**
     * Replaces the element at the specified position in this list with
     * the specified element.
     * 使用指定元素替换指定位置的内容
     * @param index index of the element to replace
     * @param element element to be stored at the specified position
     * @return the element previously at the specified position
     * @throws IndexOutOfBoundsException {@inheritDoc}
     */
    public E set(int index, E element) {
        rangeCheck(index);

        E oldValue = elementData(index);
        elementData[index] = element;
        return oldValue;
    }

详细说明

首先判断指定位置是否合法,看它是否大于数组长度,如果大于抛出IndexOutOfBoundsException异常

否则,指定位置合适,直接赋值,返回老值

扩容grow方法

首先,对于ArrayList扩容,当调用无参构造new一个ArrayList时,此时size=0,数组为空。如果指定了数组大小的构造函数,或者是指定了collection元素的列表,这两个构造函数,初始值size就是自定义的了。
然后当执行add方法时,一定会先执行ensureCapacityInternal方法,参数叫做minCapacity(最小容量)=size+1。

    /**
     * Appends the specified element to the end of this list.
     * 追加元素在list
     * @param e element to be appended to this list
     * @return <tt>true</tt> (as specified by {@link Collection#add})
     */
    public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!增量 modCount
        elementData[size++] = e;
        return true;
    }

而ensureCapacityInternal()函数里,还会计算一次最小容量,主要是给空参创建的加个默认容量,如果已经有容量了,不会进行更改。

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

        ensureExplicitCapacity(minCapacity);
    }

扩容核心方法grow

    /**
     * The maximum size of array to allocate.
     * Some VMs reserve some header words in an array.
     * Attempts to allocate larger arrays may result in
     * OutOfMemoryError: Requested array size exceeds VM limit
     */
    private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

    /**
     * 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);
    }

可以看到newCapacity=oldCapacity+(oldCapacity >> 1);也就是旧容量的1.5倍。奇数的话最后一位的1会被消去。但是,新容量大小还要经历两次判断,最后才会执行copyOf函数。
一是如果新容量小于最小所需容量,可以知道,这种情况旧容量太大导致数值溢出,newCapacity数值溢出,小于minCapacity了。
二是判断新容量是不是大于最大数组长度(默认是Integer.MAX_VALUE-8),如果大于,就进到了这个函数

    private static int hugeCapacity(int minCapacity) {
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
        return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE :
            MAX_ARRAY_SIZE;
    }

hugeCapacity的逻辑主要是判断最小所需容量是不是已经<0了,会报OOM异常,大概是minCapacity=size+1.size=Integer.MAX_VALUE才会出现这种情况。
然后判断minCapacity是不是大于最大数组长度,如果是,新容量=Interger.MAX_VALUE(如果再次扩容,就会跑到上面的判断逻辑了,再次扩容 size+1会成负数了)。
如果还没有大于最大数组长度,就会新容量=最大数组长度。

到此,新容量的计算就完成了,最后执行复制数组方法。扩容完成。

clear方法

    /**
     * Removes all of the elements from this list.  The list will
     * be empty after this call returns.
     */
    public void clear() {
        modCount++;

        // clear to let GC do its work
        for (int i = 0; i < size; i++)
            elementData[i] = null;

        size = 0;
    }

遍历list,全部赋值null,不用的话让gc回收,最后数组长度变为0

modCount

问题复现

// 增强for循环 反编译之后发现是迭代器原型 ConcurrentModificationException
for (String item:list){
    if("abc".equals(item)){
        list.add("增强for循环添加元素");
    }
}
// 增强型for循环反编译之后就是迭代器遍历的
System.out.println(list);
// 迭代器遍历添加元素 ConcurrentModificationException
Iterator<String> listIterator = list.iterator();
while (listIterator.hasNext()) {
    if ("abc".equals(listIterator.next())) {
        list.add("迭代器添加元素");
    }
}

image-20220907080110484

作用

Fail-Fast快速失败机制,遍历集合元素时候,如果集合结构发生了修改立即抛出异常,以防止继续遍历。

记录了结构性改变的次数。结构性改变指的是那些修改了列表大小的操作,在迭代过程中可能会造成错误的结果。

解决方案

       ListIterator<String> addListIterator = list.listIterator();
        while (addListIterator.hasNext()){
            if("胡一刀".equals(addListIterator.next())){
                addListIterator.add("条小儿");
            }
        }
        System.out.println("添加元素之后:"+list);
        // 方法二
        for (int k = 0; k < list.size(); k++) {
            if ("苗人凤".equals(list.get(k))) {
                list.add( "10");
            }
        }
        System.out.println(list);

线程安全问题

    /**
     * @Description 验证ArrayList多线程问题
     */
    @Test
    public void validListMultiThreads() throws InterruptedException {
         List<Integer> list = new ArrayList<>();
        for (int i = 0; i < 4; i++) {
            new Thread(() -> {
                for (int j = 0; j <= 10000; j++) {
                    list.add(new Random().nextInt(100));
                }
            }).start();
        }
        // 暂停线程的操作,精确到秒,线程暂停3秒,将其挂起
        TimeUnit.SECONDS.sleep(3);
        System.out.println("size = " + list.size());
        for (int i = 0; i < list.size(); i++) {
            if (null == list.get(i)) {
                System.out.println("error==========");
            }
        }
        System.out.println("over===========");
    }

两个问题

数组越界?

前提条件: 当前size=2 数组长度为2.
1、线程1 判断数组是否越界.因为size=2 长度为2,没有越界.将进行赋值操作.但是因为时间片问题导致了中断.
2、线程2 判断数组是否越界.因为size=2 长度为2,没有越界.将进行赋值操作.但是因为时间片问题导致了中断.
3、线程1 重新获取到主动权.上文判断了长度刚刚好够用.进行赋值操作element[size]=1,并且size++
4、线程2 因为上文判断了数组没有越界.所以进行赋值操作.但是此时的size=3了.再执行element[3]=2. 导致了数组越界了.

数值为null?

线程1赋值操作element[0]=1,随后时间片用完了,中断,

线程2赋值操作element[0]=2,随后时间片用完了,中断

此处导致了之前所说的一个问题,后续线程将前面线程值覆盖了

线程1 自增 size++ size 2

线程2 自增 size size 3

此处导致了某些值为null的问题.因为原size=1, 但是因为线程1与线程2都将值赋值给了element[0],导致了element[1]内没有值,被跳过了.指针index指向了2.所以,导致了某些情况下值为null的情况.

解决方案一:同步

Collections.synchronizedList()方法其实底层也是在集合的所有方法之上加上了synchronized(默认使用的是同一个monitor对象,也可以自己指定)

public class UnsafeArrayList3 {

    public static void main(String[] args) {
        try {
            notThreadSafe();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void notThreadSafe() throws Exception{
        final List<Integer> list = Collections.synchronizedList(new ArrayList<>());
        for(int i=0;i<4;i++){
            new Thread(()->{
                for(int j=0;j<10000;j++){
                    list.add(new Random().nextInt(100));
                }
            }).start();
        }

        TimeUnit.SECONDS.sleep(3);
        System.out.println("size = "+list.size());
        for(int i=0;i<list.size();i++){
            if(null == list.get(i)){
                System.out.println("error==========");
            }
        }
        System.out.println("over===========");
    }
}

解决方案二:COW

Copy On Write 也是一种重要的思想,在写少读多的场景下,为了保证集合的线程安全性,我们完全可以在当前线程中得到原始数据的一份拷贝,然后进行操作。

JDK集合框架中为我们提供了 ArrayList 的这样一个实现:CopyOnWriteArrayList。但是如果不是写少读多的场景,使用 CopyOnWriteArrayList 开销比较大,因为每次对其更新操作(add/set/remove)都会做一次数组拷贝。

 /**
     * @Description 验证ArrayList多线程问题
     */
    @Test
    public void validListMultiThreads() throws InterruptedException {
        List<Integer> list = new CopyOnWriteArrayList<>();
        for (int i = 0; i < 4; i++) {
            new Thread(() -> {
                for (int j = 0; j <= 10000; j++) {
                    list.add(new Random().nextInt(100));
                }
            }).start();
        }
        // 暂停线程的操作,精确到秒,线程暂停3秒,将其挂起
        TimeUnit.SECONDS.sleep(3);
        System.out.println("size = " + list.size());
        for (int i = 0; i < list.size(); i++) {
            if (null == list.get(i)) {
                System.out.println("error==========");
            }
        }
        System.out.println("over===========");
    }

CopyOnWriteArrayList

属性介绍

 /** The lock protecting all mutators 保护所有变量的锁 */
    final transient ReentrantLock lock = new ReentrantLock();

    /** The array, accessed only via getArray/setArray. 
     * 这个数组只能通过getArray和setArray访问设置
     * 通过“volatile数组”来保存数据的。一个线程读取volatile数组时,总能看到其它线程对该volatile变量最后的写入;就这样,通过volatile提供了“读取到的数据总是最新的”这个机制的 保证
     **/
    private transient volatile Object[] array;

 /**
     * Gets the array.  Non-private so as to also be accessible
     * from CopyOnWriteArraySet class.
     */
    final Object[] getArray() {
        return array;
    }

    /**
     * Sets the array.
     */
    final void setArray(Object[] a) {
        array = a;
    }

初始化,构造方法

/**
 * @Description 无参构造
 **/
public CopyOnWriteArrayList() {
  setArray(new Object[0]);
}

/**
  * Creates a list containing the elements of the specified
  * collection, in the order they are returned by the collection's
  * iterator.
  *
  * @param c the collection of initially held elements
  * @throws NullPointerException if the specified collection is null
  */
public CopyOnWriteArrayList(Collection<? extends E> c) {
    Object[] elements;
    if (c.getClass() == CopyOnWriteArrayList.class)
        elements = ((CopyOnWriteArrayList<?>)c).getArray();
    else {
        elements = c.toArray();
        // c.toArray might (incorrectly) not return Object[] (see 6260652)
        if (elements.getClass() != Object[].class)
            elements = Arrays.copyOf(elements, elements.length, Object[].class);
    }
    setArray(elements);
}

/**
     * Creates a list holding a copy of the given array.
     * 建一个包含给定数组副本的列表。
     * @param toCopyIn the array (a copy of this array is used as the
     *        internal array)
     * @throws NullPointerException if the specified array is null
     */
public CopyOnWriteArrayList(E[] toCopyIn) {
    setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class));
}

说明:setArray()的作用是给array赋值;其中,array是volatile transient
Object[]类型,即array是“volatile数组”。
关于volatile关键字,我们知道“volatile能让变量变得可见”,即对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写入。正在由于这种特性,每次更新了“volatile数组”之后,其它线程都能看到对它所做的更新。
关于transient关键字,它是在序列化中才起作用,transient变量不会被自动序列化。

add

/**
     * Appends the specified element to the end of this list.
     * 将指定的元素追加到列表的末尾。
     * @param e element to be appended to this list
     * @return {@code true} (as specified by {@link Collection#add})
     */
    public boolean add(E e) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] elements = getArray();
            int len = elements.length;
            Object[] newElements = Arrays.copyOf(elements, len + 1);
            newElements[len] = e;
            setArray(newElements);
            return true;
        } finally {
            lock.unlock();
        }
    }

1 加锁(synchronized )。保证此时只有一个线程进入
2 获取原来的数组以及长度
3 复制新数据,并且长度+1
4 设置新数据(array使用volatile修饰,所以其他线程可见)
5 返回

get

	//根据index直接获取数组元素
    // 不加锁
    public E get(int index) {
        return elementAt(getArray(), index);
    }

set

 /**
     * Replaces the element at the specified position in this list with the
     * specified element.
     * 将列表中指定位置的元素替换为指定元素
     * @throws IndexOutOfBoundsException {@inheritDoc}
     */
    public E set(int index, E element) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] elements = getArray();
            E oldValue = get(elements, index);

            if (oldValue != element) {
                int len = elements.length;
                Object[] newElements = Arrays.copyOf(elements, len);
                newElements[index] = element;
                setArray(newElements);
            } else {
                // Not quite a no-op; ensures volatile write semantics
                setArray(elements);
            }
            return oldValue;
        } finally {
            lock.unlock();
        }
    }

首先获取锁ReentrantLock,加锁,根据指定索引获取旧值,判断指定位置的新值与旧值是否相同,

不相同的话,数组拷贝,得到新数组新数组指定位置赋值操作,会通过将数据更新到volatile数组中

解锁

remove

/**
     * Removes the element at the specified position in this list.
     * Shifts any subsequent elements to the left (subtracts one from their
     * indices).  Returns the element that was removed from the list.
     * 删除位于列表中指定位置的元素。
     * 将所有后续元素向左平移(从它们的下标减去1)
     * @throws IndexOutOfBoundsException {@inheritDoc}
     */
    public E remove(int index) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] elements = getArray();
            int len = elements.length;
            E oldValue = get(elements, index);
            int numMoved = len - index - 1;
            if (numMoved == 0)
                setArray(Arrays.copyOf(elements, len - 1));
            else {
                Object[] newElements = new Object[len - 1];
                System.arraycopy(elements, 0, newElements, 0, index);
                System.arraycopy(elements, index + 1, newElements, index,
                                 numMoved);
                setArray(newElements);
            }
            return oldValue;
        } finally {
            lock.unlock();
        }
    }

首先获取同一把锁,看一下怎么移动元素,根据 numMoved = len - index -1 ,如果删除最后一个元素直接返回新数组,新数组包含

除了最后一个元素的所有,如果不是最后一个元素的话,分两步拷贝首先将指定元素之前的内容拷贝新数组,之后将指定元素之后

所有内容拷贝到新数组,新数组赋值,解锁

// 关于Arrays.copyOf 数组拷贝
int[] arr=new int[]{1,2,3,4,5};
int[] ints = Arrays.copyOf(arr, 3);
for(int i=0;i<ints.length;i++){
    System.out.println(ints[i]);
}
// 1
// 2
// 3
// System.arraycopy 数组拷贝
int[] newArr=new int[4];
// arr:源数组 srcPos:源数组开始位置0,newArr:目标数组,destPos:目标数组开始位置0,length:拷贝长度2
// 拷贝所删除元素前面的元素到新数组
System.arraycopy(arr,0,newArr,0,2);
// newArr 1,2
// 拷贝所删除元素后面的元素到新数组
// arr:源数组 srcPos:源数组开始位置3,newArr:目标数组,destPos:目标数组开始位置2,length:拷贝长度2
System.arraycopy(arr,3,newArr,2,2);
//
for(int i=0;i<newArr.length;i++){
    System.out.println(newArr[i]);
}

遍历

/**
 * Returns an iterator over the elements in this list in proper sequence.
 * 返回一个迭代器,以正确的顺序遍历列表中的元素。
 * <p>The returned iterator provides a snapshot of the state of the list
 * when the iterator was constructed. No synchronization is needed while
 * traversing the iterator. The iterator does <em>NOT</em> support the
 * {@code remove} method.
 * 返回的迭代器提供了列表状态的快照 当迭代器被构造时。此时不需要同步
 * 遍历迭代器。迭代器不支持remove方法
 * {@code remove}方法。
 * @return an iterator over the elements in this list in proper sequence
 */
public Iterator<E> iterator() {
    return new COWIterator<E>(getArray(), 0);
}

 static final class COWIterator<E> implements ListIterator<E> {
        /** Snapshot of the array 快照数组 */
        private final Object[] snapshot;
        /** Index of element to be returned by subsequent call to next.  
         * 后续调用next返回的元素的索引
        */
        private int cursor;

        private COWIterator(Object[] elements, int initialCursor) {
            cursor = initialCursor;
            snapshot = elements;
        }

     @SuppressWarnings("unchecked")
        public E next() {
            if (! hasNext())
                throw new NoSuchElementException();
            return (E) snapshot[cursor++];
        }
     public boolean hasNext() {
            return cursor < snapshot.length;
        }
 }

对于遍历删除问题复现

 		System.out.println("遍历cowlist");
        CopyOnWriteArrayList<Integer> cowList = new CopyOnWriteArrayList<Integer>() {{
            add(1);
            add(1);
            add(1);
        }};
        Iterator<Integer> cowIterator = cowList.iterator();
        while (cowIterator.hasNext()) {
            if (1 == cowIterator.next()) {
                cowIterator.remove();
            }
        }

image-20220907215616817

对于remove方法

/**
 * Not supported. Always throws UnsupportedOperationException.
 * @throws UnsupportedOperationException always; {@code remove}
 *         is not supported by this iterator.
 * 迭代器遍历同时不支持删除操作
 */
public void remove() {
    throw new UnsupportedOperationException();
}

迭代器遍历同时修改,增加操作问题、

System.out.println("遍历cowlist");
        CopyOnWriteArrayList<Integer> cowList = new CopyOnWriteArrayList<Integer>() {{
            add(1);
            add(1);
            add(1);
        }};
        Iterator<Integer> cowIterator = cowList.iterator();
        while (cowIterator.hasNext()) {
            if (1 == cowIterator.next()) {
                int j = cowList.indexOf(new Integer(1));
                cowList.set(j,23);
                //  cowList.add(23);
                // cowIterator.remove()
            }
            System.out.println(cowIterator.next());
        }

image-20220907221316878

基本原理

当向集合添加、移除元素时,并不是直接操作当前集合,而是首先对原集合进行拷贝、复制到一个新集合中。然后对该新集合进行添加、移除元素的操作。最后,再将对原集合的引用指向新集合即可。该机制的好处就在于当我们访问、读取集合是不需要加锁的,即COW机制是读写分离思想的具体体现。当然其弊端也是显而易见的,一方面由于写操作需要复制操作,所以对于较大规模的集合而言,其耗费的内存也是较为可观的;另一方面,由于读操作访问的是原集合,即使写操作在此期间已经完成。对于前者(读操作)而言也是不可见的,即其保证的是最终一致性

LinkedList

img

概述

(1)LinkedList底层维护了一个双向链表

(2)LinkedList中维护了两个属性first和last分别指向首节点和尾节点

(3)每个节点(Node对象)里又维护了prev(指向前一节点),next(指向后一节点),item(用于保存数据)三个属性,从而实现双链表

(4)因此元素的添加和删除效率较高

属性参数

transient int size = 0;//记录节点个数

transient Node<E> first;//first引用指向头节点

transient Node<E> last;//last引用指向尾节点

构造函数

public LinkedList() {
}

public LinkedList(Collection<? extends E> c) {
    this();//调用无参构造器
    addAll(c);//批量插入方法
}

插入数据

public boolean addAll(Collection<? extends E> c) {
    return addAll(size, c);//调用插入方法——addAll(int, Collection)
}
public boolean addAll(int index, Collection<? extends E> c) {//传入节点个数和初始化参数
    checkPositionIndex(index);//检查下标合理性

    Object[] a = c.toArray();//将c集合转换为Object数组a
    int numNew = a.length;//记录数组长度
    if (numNew == 0)//数组长度为0
        return false;//返回false,初始化失败

    Node<E> pred, succ;//声明该节点的前驱节点和后继节点
    if (index == size) {//如果下标等于节点个数,即从尾部加入
        succ = null;//后继节点置空
        pred = last;//前驱节点指向老链表的last节点
    } else {//从指定位置添加节点
        succ = node(index);//后继节点指向原index处节点
        pred = succ.prev;//前驱节点指向原index处节点的前驱节点
    }

    for (Object o : a) {//遍历存放c集合的数组a
        @SuppressWarnings("unchecked") E e = (E) o;//向下转换
        Node<E> newNode = new Node<>(pred, e, null);//新建节点存放元素
        if (pred == null)//如果该节点的前驱节点为空,说明是头节点插入
            first = newNode;//更新头节点
        else
            pred.next = newNode;//将前驱节点和新节点关联
        pred = newNode;
    }

    if (succ == null) {//后继节点为空,说明尾部插入
        last = pred;//尾节点更新
    } else {//不为空
        pred.next = succ;//newNode指向后继节点
        succ.prev = pred;//后继节点指向newNode
    }

    size += numNew;//元素长度更新
    modCount++;//操作次数++
    return true;//插入成功
}
/**
 * 判断下标合理性
 **/
private void checkPositionIndex(int index) {
    if (!isPositionIndex(index))//若下标不合理,则抛出异常
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
private boolean isPositionIndex(int index) {//下标大于等于0且小于等于size返回true
    return index >= 0 && index <= size;
}

获取元素 返回原链表中index的节点

Node<E> node(int index) {//二分法查找法
    // assert isElementIndex(index);

    if (index < (size >> 1)) {//index小于size的二分之一,从头节点开始查找
        Node<E> x = first;//新建x指向first节点
        for (int i = 0; i < index; i++)//找到原链表中下标为index的节点
            x = x.next;
        return x;//返回原链表中下标为index的节点
    } else {//下标在后半部分,从尾节点开始查找
        Node<E> x = last;
        for (int i = size - 1; i > index; i--)
            x = x.prev;
        return x;
    }
}

set

HashSet

image-20220924201917438

变量以及参数

    private transient HashMap<E,Object> map;

    // Dummy value to associate with an Object in the backing Map
    private static final Object PRESENT = new Object();

构造方法

 /**
     * Constructs a new, empty set; the backing <tt>HashMap</tt> instance has
     * default initial capacity (16) and load factor (0.75).
     */
    public HashSet() {
        map = new HashMap<>();
    }

    /**
     * Constructs a new set containing the elements in the specified
     * collection.  The <tt>HashMap</tt> is created with default load factor
     * (0.75) and an initial capacity sufficient to contain the elements in
     * the specified collection.
     *
     * @param c the collection whose elements are to be placed into this set
     * @throws NullPointerException if the specified collection is null
     */
    public HashSet(Collection<? extends E> c) {
        map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));
        addAll(c);
    }

    /**
     * Constructs a new, empty set; the backing <tt>HashMap</tt> instance has
     * the specified initial capacity and the specified load factor.
     *
     * @param      initialCapacity   the initial capacity of the hash map
     * @param      loadFactor        the load factor of the hash map
     * @throws     IllegalArgumentException if the initial capacity is less
     *             than zero, or if the load factor is nonpositive
     */
    public HashSet(int initialCapacity, float loadFactor) {
        map = new HashMap<>(initialCapacity, loadFactor);
    }

    /**
     * Constructs a new, empty set; the backing <tt>HashMap</tt> instance has
     * the specified initial capacity and default load factor (0.75).
     *
     * @param      initialCapacity   the initial capacity of the hash table
     * @throws     IllegalArgumentException if the initial capacity is less
     *             than zero
     */
    public HashSet(int initialCapacity) {
        map = new HashMap<>(initialCapacity);
    }

    /**
     * Constructs a new, empty linked hash set.  (This package private
     * constructor is only used by LinkedHashSet.) The backing
     * HashMap instance is a LinkedHashMap with the specified initial
     * capacity and the specified load factor.
     *
     * @param      initialCapacity   the initial capacity of the hash map
     * @param      loadFactor        the load factor of the hash map
     * @param      dummy             ignored (distinguishes this
     *             constructor from other int, float constructor.)
     * @throws     IllegalArgumentException if the initial capacity is less
     *             than zero, or if the load factor is nonpositive
     */
    HashSet(int initialCapacity, float loadFactor, boolean dummy) {
        map = new LinkedHashMap<>(initialCapacity, loadFactor);
    }

add方法

    /**
     * Adds the specified element to this set if it is not already present.
     * More formally, adds the specified element <tt>e</tt> to this set if
     * this set contains no element <tt>e2</tt> such that
     * <tt>(e==null&nbsp;?&nbsp;e2==null&nbsp;:&nbsp;e.equals(e2))</tt>.
     * If this set already contains the element, the call leaves the set
     * unchanged and returns <tt>false</tt>.
     * @param e element to be added to this set
     * @return <tt>true</tt> if this set did not already contain the specified
     * element
     */
    public boolean add(E e) {
        return map.put(e, PRESENT)==null;
    }

    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

 final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            // 关键代码
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                ...
        }
        ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }
 * 如果指定的元素不存在,则将其添加到此集合
 * 更正式一些,如果满足以下条件,则将指定的元素<tt>e</tt>添加到此集合。
 * e == null ? e2 == null : e.equals(e2)
 * 如果集合set包含该元素,返回false

contains方法

    /**
     * Returns <tt>true</tt> if this set contains the specified element.
     * More formally, returns <tt>true</tt> if and only if this set
     * contains an element <tt>e</tt> such that
     * <tt>(o==null&nbsp;?&nbsp;e==null&nbsp;:&nbsp;o.equals(e))</tt>.
     *
     * @param o element whose presence in this set is to be tested
     * @return <tt>true</tt> if this set contains the specified element
     */
    public boolean contains(Object o) {
        return map.containsKey(o);
    }

去重原理

1.Java中HashSet是用散列表实现的,散列表的大小默认为16,加载因子为0.75.

2.去重原理:当hashset add一个元素A的时候,首先获取这个元素的散列码(hashcode的方法),即获取元素的哈希值。

情况一:如果计算出的元素的存储位置目前没有任何元素存储,那么该元素可以直接存储在该位置上。

情况二:如果算出该元素的存储位置目前已经存在有其他元素了,那么会调用该元素的equals方法与该位置的元素再比较一次,如果equals返回的值是true,那么该元素与这个位置上的元素就视为重复元素,不允许添加,如果equals方法返回的是false,那么该元素运行添加。

linkedhashset

image-20220924202209826

 /**
     * Constructs a new, empty linked hash set with the specified initial
     * capacity and load factor.
     *
     * @param      initialCapacity the initial capacity of the linked hash set
     * @param      loadFactor      the load factor of the linked hash set
     * @throws     IllegalArgumentException  if the initial capacity is less
     *               than zero, or if the load factor is nonpositive
     */
    public LinkedHashSet(int initialCapacity, float loadFactor) {
        super(initialCapacity, loadFactor, true);
    }

    /**
     * Constructs a new, empty linked hash set with the specified initial
     * capacity and the default load factor (0.75).
     *
     * @param   initialCapacity   the initial capacity of the LinkedHashSet
     * @throws  IllegalArgumentException if the initial capacity is less
     *              than zero
     */
    public LinkedHashSet(int initialCapacity) {
        super(initialCapacity, .75f, true);
    }

    /**
     * Constructs a new, empty linked hash set with the default initial
     * capacity (16) and load factor (0.75).
     */
    public LinkedHashSet() {
        super(16, .75f, true);
    }

    /**
     * Constructs a new linked hash set with the same elements as the
     * specified collection.  The linked hash set is created with an initial
     * capacity sufficient to hold the elements in the specified collection
     * and the default load factor (0.75).
     *
     * @param c  the collection whose elements are to be placed into
     *           this set
     * @throws NullPointerException if the specified collection is null
     */
    public LinkedHashSet(Collection<? extends E> c) {
        super(Math.max(2*c.size(), 11), .75f, true);
        addAll(c);
    }

map

HashMap

img

定位hash桶数组索引位置

&运算:按位与"&"功能是参与运算的两数各对应的二进位相与。只有对应的两个二进位均为1时,结果位才为1 ,否则为0

^运算:异或运算法则 如果a、b两个值不相同,则异或结果为1。如果a、b两个值相同,异或结果为0

初始化容量

    /**
     * Associates the specified value with the specified key in this map.
     * If the map previously contained a mapping for the key, the old
     * value is replaced.
     * 将指定的值与映射中的指定键关联。如果映射之前包含一个键的映射,旧的值被替换。
     * @param key key with which the specified value is to be associated
     * @param value value to be associated with the specified key
     * @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 put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
	// hashCode() native修饰的底层c函数,最后返回hash值是hashcode与16高低异或操作
    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
    /**
     * Implements Map.put and related methods
     *
     * @param hash hash for key
     * @param key the key
     * @param value the value to put
     * @param onlyIfAbsent if true, don't change existing value
     * @param evict if false, the table is in creation mode.
     * @return previous value, or null if none
     */
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        // 初始化hash桶,table数组是延迟加载插入数据之后再进行初始化
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        ...省略
        afterNodeInsertion(evict);
        return null;
    }
  /**
     * Initializes or doubles table size.  If null, allocates in
     * accord with initial capacity target held in field threshold.
     * Otherwise, because we are using power-of-two expansion, the
     * elements from each bin must either stay at same index, or move
     * with a power of two offset in the new table.
     *
     * @return the table
     */
    final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
        int newCap, newThr = 0;
        // cap是capacity缩写,表示容量,如果容量不为空,说明已经开始初始化
        if (oldCap > 0) {
            // 如果容量达到最大 1<<30则不再扩容
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            // 按照旧容量和阈值的2倍计算新容量和阈值
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold
        }
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;
        else {               // zero initial threshold signifies using defaults
            // 这一部分,源代码也有注释 初始阈值为零表示使用默认值
            // 调用无参构造,数组table默认容量1<<4,左移4位16 1 * 2^4
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        if (newThr == 0) {
            float ft = (float)newCap * loadFactor;
            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 = newTab;
        ...省略
        return newTab;
    }

初始化过程

img

public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

putVal()方法

    /**
     * The bin count threshold for using a tree rather than list for a
     * bin.  Bins are converted to trees when adding an element to a
     * bin with at least this many nodes. The value must be greater
     * than 2 and should be at least 8 to mesh with assumptions in
     * tree removal about conversion back to plain bins upon
     * shrinkage.
     */
    static final int TREEIFY_THRESHOLD = 8;

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        //判断数组是否未初始化
        if ((tab = table) == null || (n = tab.length) == 0)
            //如果未初始化,调用resize方法 进行初始化
            n = (tab = resize()).length;
        //通过 & 运算求出该数据(key)的数组下标并判断该下标位置是否有数据
        if ((p = tab[i = (n - 1) & hash]) == null)
            //如果没有,直接将数据放在该下标位置
            tab[i] = newNode(hash, key, value, null);
        //该数组下标有数据的情况
        else {
            Node<K,V> e; K k;
            //判断该位置数据的key和新来的数据是否一样
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                //如果一样,证明为修改操作,该节点的数据赋值给e,后边会用到
                e = p;
            //判断是不是红黑树
            else if (p instanceof TreeNode)
                //如果是红黑树的话,进行红黑树的操作
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            //新数据和当前数组既不相同,也不是红黑树节点,证明是链表
            else {
                //遍历链表
                for (int binCount = 0; ; ++binCount) {
                    //判断next节点,如果为空的话,证明遍历到链表尾部了
                    if ((e = p.next) == null) {
                        //把新值放入链表尾部
                        p.next = newNode(hash, key, value, null);
                        //因为新插入了一条数据,所以判断链表长度是不是大于等于8
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            //如果是,进行转换红黑树操作
                            treeifyBin(tab, hash);
                        break;
                    }
                    //判断链表当中有数据相同的值,如果一样,证明为修改操作
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    //把下一个节点赋值为当前节点
                    p = e;
                }
            }
            //判断e是否为空(e值为修改操作存放原数据的变量)
            if (e != null) { // existing mapping for key
                //不为空的话证明是修改操作,取出老值
                V oldValue = e.value;
                //一定会执行  onlyIfAbsent传进来的是false
                if (!onlyIfAbsent || oldValue == null)
                    //将新值赋值当前节点
                    e.value = value;
                afterNodeAccess(e);
                //返回老值
                return oldValue;
            }
        }
        //计数器,计算当前节点的修改次数
        ++modCount;
        //当前数组中的数据数量如果大于扩容阈值
        if (++size > threshold)
            //进行扩容操作
            resize();
        //空方法
        afterNodeInsertion(evict);
        //添加操作时 返回空值
        return null;
    }

获取元素

(1)、先调用k的hashCode()方法得出哈希值,并通过哈希算法转换成数组的下标。 (2)、在通过数组下标快速定位到某个位置上。重点理解如果这个位置上什么都没有,则返回null。如果这个位置上有单向链表,那么它就会拿着参数K和单向链表上的每一个节点的K进行equals,如果所有equals方法都返回false,则get方法返回null。如果其中一个节点的K和参数K进行equals返回true,那么此时该节点的value就是我们要找的value了,get方法最终返回这个要找的value。

public V get(Object key) {
        Node<K,V> e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }

final Node<K,V> getNode(int hash, Object key) {
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
        //判断数组不为null并且长度大于0,并且通过hash算出来的数组下标的位置不为空,证明有数据
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {
            //判断数组的位置的key的hash和内容是否等同与要查询的数据
            if (first.hash == hash && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k))))
                //相等的话直接返回
                return first;
            //判断是否有下个节点
            if ((e = first.next) != null) {
                //判断是否为为红黑树
                if (first instanceof TreeNode)
                    //进行红黑树查询操作
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);
                //链表查询操作
                do {
                    //循环链表,逐一判断
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        //发现key的话就返回
                        return e;
                } while ((e = e.next) != null);
            }
        }
        //没有查询到返回null
        return null;
    }

扩容

向HashMap中添加数据时,有三个条件会触发它的扩容行为:

  1. 如果数组为空,则进行首次扩容。

  2. 将元素接入链表后,如果链表长度达到8,并且数组长度小于64,则扩容。

  3. 添加后,如果数组中元素超过阈值,即比例超出限制(默认为0.75),则扩容。

  4. 每次扩容时都是将容量翻倍,即创建一个2倍大的新数组,然后再将旧数组中的数组迁移到新数组里。由于HashMap中数组的容量为2^N,所以可以用位移运算计算新容量,效率很高。

​ 5. 在数据迁移时,为了兼顾性能,不会重新计算一遍每个key的哈希值,而是根据位移运算后(左移翻倍)多出来的最高位来决定,如果高位为0则元素位置不变,如果高位为1则元素的位置是在原位置基础上加上旧的容量。

​ 举个例子,来演示一下数据迁移时,元素在新数组里位置的判定。假设旧数组长度为16(00010000),则翻倍后新数组的长度为32(00100000)。我们从十进制数字上看不出什么,但是从二进制数字却可以看出二者的明显差别,即翻倍后新值的高位多了1。

​ 如果我们把这两个值作为掩码,对key的哈希值做按位与运算,就能求出最高位那一位的差异。如果这一位是0则该元素的位置不变,否则该元素的位置就在原位置基础上加16。这个方式很巧妙,它省略了重新计算哈希值的时间,同时新增出来的一位是0或1是随机的,这样就把产生冲突的节点均匀的分散到新的槽里了。

//扩容、初始化数组
final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
        //如果当前数组为null的时候,把oldCap老数组容量设置为0
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        //老的扩容阈值
        int oldThr = threshold;
        int newCap, newThr = 0;
        //判断数组容量是否大于0,大于0说明数组已经初始化
        if (oldCap > 0) {
            //判断当前数组长度是否大于最大数组长度
            if (oldCap >= MAXIMUM_CAPACITY) {
                //如果是,将扩容阈值直接设置为int类型的最大数值并直接返回
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            //如果在最大长度范围内,则需要扩容  OldCap << 1等价于oldCap*2
            //运算过后判断是不是最大值并且oldCap需要大于16
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold  等价于oldThr*2
        }
        //如果oldCap<0,但是已经初始化了,像把元素删除完之后的情况,那么它的临界值肯定还存在,                如果是首次初始化,它的临界值则为0
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;
        //数组未初始化的情况,将阈值和扩容因子都设置为默认值
        else {               // zero initial threshold signifies using defaults
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        //初始化容量小于16的时候,扩容阈值是没有赋值的
        if (newThr == 0) {
            //创建阈值
            float ft = (float)newCap * loadFactor;
            //判断新容量和新阈值是否大于最大容量
            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 = newTab;
        //扩容操作,判断不为空证明不是初始化数组
        if (oldTab != null) {
            //遍历数组
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                //判断当前下标为j的数组如果不为空的话赋值个e,进行下一步操作
                if ((e = oldTab[j]) != null) {
                    //将数组位置置空
                    oldTab[j] = null;
                    //判断是否有下个节点
                    if (e.next == null)
                        //如果没有,就重新计算在新数组中的下标并放进去
                        newTab[e.hash & (newCap - 1)] = e;
                    //有下个节点的情况,并且判断是否已经树化
                    else if (e instanceof TreeNode)
                        //进行红黑树的操作
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    //有下个节点的情况,并且没有树化(链表形式)
                    else {
                        //比如老数组容量是16,那下标就为0-15
                        //扩容操作*2,容量就变为32,下标为0-31
                        //低位:0-15,高位16-31
                        //定义了四个变量
                        //        低位头          低位尾
                        Node<K,V> loHead = null, loTail = null;
                        //        高位头          高位尾
                        Node<K,V> hiHead = null, hiTail = null;
                        //下个节点
                        Node<K,V> next;
                        //循环遍历
                        do {
                            //取出next节点
                            next = e.next;
                            //通过 与操作 计算得出结果为0
                            if ((e.hash & oldCap) == 0) {
                                //如果低位尾为null,证明当前数组位置为空,没有任何数据
                                if (loTail == null)
                                    //将e值放入低位头
                                    loHead = e;
                                //低位尾不为null,证明已经有数据了
                                else
                                    //将数据放入next节点
                                    loTail.next = e;
                                //记录低位尾数据
                                loTail = e;
                            }
                            //通过 与操作 计算得出结果不为0
                            else {
                                 //如果高位尾为null,证明当前数组位置为空,没有任何数据
                                if (hiTail == null)
                                    //将e值放入高位头
                                    hiHead = e;
                                //高位尾不为null,证明已经有数据了
                                else
                                    //将数据放入next节点
                                    hiTail.next = e;
                               //记录高位尾数据
                                hiTail = e;
                            }
                            //如果e不为空,证明没有到链表尾部,继续执行循环
                        } while ((e = next) != null);
                        //低位尾如果记录的有数据,是链表
                        if (loTail != null) {
                            //将下一个元素置空
                            loTail.next = null;
                            //将低位头放入新数组的原下标位置
                            newTab[j] = loHead;
                        }
                        //高位尾如果记录的有数据,是链表
                        if (hiTail != null) {
                            //将下一个元素置空
                            hiTail.next = null;
                            //将高位头放入新数组的(原下标+原数组容量)位置
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        //返回新的数组对象
        return newTab;
    }

链表树化

将单向链表转化为红黑树。树化操作主要发生在put操作中。

当链表长度超过8,且容量达到64时,触发树化


for (int binCount = 0; ; ++binCount) {
    //这里是遍历到链表的最后一个
    if ((e = p.next) == null) {
        p.next = newNode(hash, key, value, null);
        /**
         * 这里这个树化阈值的判断需要仔细琢磨一下
         * 如果当前链表有八个节点,刚刚将newNode插入到了尾结点后面,也就是说
         * 此时的newNode是链表中的第九个节点,此时才会进行树化
         * 因为bitCount是从0开始的,也就是说bitCount为7的时候,才会树化,此时已经有了八个节点
         * newNode再插进来,当然是第九个了
         */
        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
            treeifyBin(tab, hash);
        break;
    }
    /**
     * 在遍历的过程中,如果找到相同的key,就退出遍历,进行覆盖
     * 但是不是在这里覆盖, 在下面判断中进行的覆盖
     * 可以整体看下这里的逻辑,如果进入到下面的if判断中,那此时就会中断for循环,这时 e != null
     * 如果这里的if判断一直没有进去,那就会进入到上面 if((e = p.next) == null)的判断,也就是说,e == null
     */
    if (e.hash == hash &&
        ((k = e.key) == key || (key != null && key.equals(k))))
        break;
    p = e;
}

// 这是put方法中的部分代码,在当前插入的元素大于8个时,会进行树化

/**
 * 链表转换红黑树,进行树化的目的是将链表中节点的查询效率提高
 * 1.需要满足两个条件:数组不为空且hashmap数组的长度超过64,否则,只会进行扩容,不会进行树化
 *  如果数组长度不超过64,就可以进行扩容,因为扩容之后,链表的长度就会变为一半
 * 2.e表示的是数组中某个链表对应的头结点
 *   2.1 do while循环,会把链表中每个node节点进行转换,转换成treeNode,然后将链表变成了双向链表,有prev和next,hd表示的是原链表的第一个节点,
 *   2.2 然后根据hd进行真正的红黑树转换,hd就是红黑树初始化的根节点
 *
 * 所以:链表转红黑树,首先会把当前的单向链表,转换成双向链表,然后再拿着双向链表的头结点hd去进行红黑树的转换
 *
 * @param tab
 * @param hash:待插入元素key值对应的hash
 */
final void treeifyBin(Node<K,V>[] tab, int hash) {
    int n, index; Node<K,V> e;
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
        resize();
    else if ((e = tab[index = (n - 1) & hash]) != null) {
        TreeNode<K,V> hd = null, tl = null;
        do {
            /**
             * replacementTreeNode:把e这个node节点变为双向链表中的节点 treeNode
             *
             * e:是未转变红黑树之前链表的第一个节点
             * hd:是e对应的treeNode
             * tl:是一个动态变量,永远表示的是最后一个节点
             *
             * 所以下面的代码,是把原本的链表转换成了treeNode,我们姑且可以认为是双向链表
             */
            TreeNode<K,V> p = replacementTreeNode(e, null);
            if (tl == null)
                hd = p;
            else {
                p.prev = tl;
                tl.next = p;
            }
            tl = p;
        } while ((e = e.next) != null);
        if ((tab[index] = hd) != null)
        /**
         * 这里的treeify是将双向链表转换为红黑树,这里只需要入参双向链表的头节点就可以了
         */
            hd.treeify(tab);
    }
}
// 在treeifyBin方法中,会先把当前链表中的元素替换成双向链表,然后从tab[0]位置,依次向红黑树中添加元素

/**
 * Forms tree of the nodes linked from this node.
 * 红黑树的根节点如果发生了变化,就需要把变化之后的根节点放到双向链表的head
 * 所以:我们可以认为:在树化之后,双向链表的头节点,就是红黑数的root节点,在每次向插入红黑树中插入数据之后,如果root节点发生了变化
 * 同时会维护双向链表的节点
 */
final void treeify(Node<K,V>[] tab) {
    TreeNode<K,V> root = null;
    /**
     * 这里会遍历前面维护好的的双向链表
     */
    for (TreeNode<K,V> x = this, next; x != null; x = next) {
        next = (TreeNode<K,V>)x.next;
        //首先将根节点的左右节点设置为null
        x.left = x.right = null;
        //把第一个hd,设置为root节点,默认是黑色的
        if (root == null) {
            x.parent = null;
            x.red = false;
            root = x;
        }
        else {
            /**
             * 这里的x就是要插入到树中的元素
             * 在每次插入到红黑树之前,是需要先判断当前要插入的节点是要插入到哪个位置?
             * 父节点是谁?
             */
            K k = x.key;
            int h = x.hash;
            Class<?> kc = null;
            /**
             * 遍历树中已经存在的节点,从root节点开始
             */
            for (TreeNode<K,V> p = root;;) {
                int dir, ph;
                K pk = p.key;
                /**
                 * 如果新结点的hash值小于树中已经存在的树节点,就是-1;如果新结点的hash值大于树中的结点对应的hash,就是1,意思就是,-1往左插入,1往右插入
                 * 所以,判断当前节点是要插入到节点的左边还是右边,就是根据这里来判断的
                 * 如果root节点大于要插入的节点,那就是往左边插入,反之,向右边插入
                 *
                 * 所以:这里在判断dir的值时,有以下几个逻辑
                 *  1.先判断key的hash值,如果hash值相等,就进行下一步判断
                 *  2.再判断key是否实现了Comparable接口,如果实现了,就通过compareTo
                 *  去比较,如果比较的值还是相等,就执行下一步
                 *  3.调用tieBreakOrder
                 *      先判断k和pk对应的className是否一样
                 *      然后再判断identityHashCode对应的值
                 *
                 *  这里为什么要先判断hashCode,hashCode相等之后,在最后再调用System.identityHashCode()
                 *  是为了防止程序员自己重写hashCode方法,返回了固定值,比如:为重修额hashCode
                 *  ,返回了1,那就区分不出来哪个key大 哪个key小了
                 */
                if ((ph = p.hash) > h)
                    dir = -1;
                else if (ph < h)
                    dir = 1;
                else if ((kc == null &&
                          (kc = comparableClassFor(k)) == null) ||
                         (dir = compareComparables(kc, k, pk)) == 0)
                    dir = tieBreakOrder(k, pk);

                TreeNode<K,V> xp = p;
                /**
                 * 这里是,根据上面的dir来进行遍历判断,for循环,一直找到最底层的孙子结点,判断条件就是p.left或者p.right为null,
                 * 就表示当前的p结点是最左边或者最右边的了 如果dir <= 0,就往左,否则 往右
                 *
                 */
                if ((p = (dir <= 0) ? p.left : p.right) == null) {
                    x.parent = xp;
                    if (dir <= 0)
                        xp.left = x;
                    else
                        xp.right = x;
                    /**
                     * 我们可以理解为上面的逻辑就是为了找到当前要插入的x节点对应的parent
                     *
                     * 这是把x节点插入到红黑树中,并返回插入之后的root节点,因为有可能在插入X
                     * 节点之后,红黑树进行了平衡或者红黑颜色的转换,导致root节点变化
                     * 所以:这个方法就是把x插入到root节点,并进行平衡或者红黑转换的过程,最后会返回转换之后的root节点
                     */
                    root = balanceInsertion(root, x);
                    break;
                }
            }
        }
    }
    /**
     * 下面这个方法中完成的操作:
     *  1.把红黑树的根节点移到tab[i]
     *  2.把root节点移到双向链表的第一个元素位置
     * 这里是把跟结点移到tab[i]上,因为在红黑树转换的过程中,root结点是可能发生转换的
     * 同时,把root结点对应的treeNode变成双线链表的头结点head,因为红黑树的root结点,很有可能是双向链表中的某个节点;要保证root的根节点对应的双线链表的头结点
     *
     */
    moveRootToFront(tab, root);
}

// 这个方法,我们可以简单认为,就做了两件事情
// 1、将当前要插入的节点,写入到红黑树中,就是最后一行前面的代码所做的事情
// 2、将当前最新的root节点,移动到双向链表的头部,就是最后一行代码做的事情
/**
 * Ensures that the given root is the first node of its bin.
 * 这个方法是把入参中的root节点,变为当前双向链表的root节点(头节点)
 */
static <K,V> void moveRootToFront(Node<K,V>[] tab, TreeNode<K,V> root) {
    int n;
    if (root != null && tab != null && (n = tab.length) > 0) {
        int index = (n - 1) & root.hash;
        TreeNode<K,V> first = (TreeNode<K,V>)tab[index];
        /**
         * 如果root节点和tab[i]位置的元素不一样,就进行转换
         *
         * 这里有一个点没看懂:tab数组是当前hashmap的数组,为什么是从当前数组中
         * 找到一个位置i对应的头结点?
         */
        if (root != first) {
            Node<K,V> rn;
            // 将root节点赋值到tab[i]位置
            tab[index] = root;
            //rp是root节点的上一个节点(有可能进行转换之后,root节点在双向链表中间某个位置)
            TreeNode<K,V> rp = root.prev;
            //如果root节点不是最后一个节点,就把root下一个节点的prev指向root的前一个节点
            if ((rn = root.next) != null)
                ((TreeNode<K,V>)rn).prev = rp;
            //如果root的上一个节点不为null,就把上一个节点的next指向root的下一个节点
            if (rp != null)
                rp.next = rn;
            //将现在tab[i]对应的元素设置为root的下一个节点,也就是把root节点放到first的前面
            if (first != null)
                first.prev = root;
            root.next = first;
            root.prev = null;
        }
        assert checkInvariants(root);
    }
}

线程安全问题

https://mp.weixin.qq.com/s?__biz=MzIxMjE5MTE1Nw==&mid=2653192000&idx=1&sn=118cee6d1c67e7b8e4f762af3e61643e&chksm=8c990d9abbee848c739aeaf25893ae4382eca90642f65fc9b8eb76d58d6e7adebe65da03f80d&scene=21#wechat_redirect

ConcurrentHashMap

https://blog.csdn.net/wwj17647590781/article/details/118151008?spm=1001.2014.3001.5501

构造器

不同种类的构造器

    /**
     * Creates a new, empty map with the default initial table size (16).
     * 通过无参构造方法创建map,初始化容量为16
     */
    public ConcurrentHashMap() {
    }

    /**
     * Creates a new, empty map with an initial table size
     * accommodating the specified number of elements without the need
     * to dynamically resize.
     * 指定初始容量的map,将指定的容量按照一定算法获取2的整数次幂
     * @param initialCapacity The implementation performs internal
     * sizing to accommodate this many elements.
     * @throws IllegalArgumentException if the initial capacity of
     * elements is negative
     */
    public ConcurrentHashMap(int initialCapacity) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException();
        int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ?
                   MAXIMUM_CAPACITY :
                   tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));
        this.sizeCtl = cap;
    }

    /**
     * Copies all of the mappings from the specified map to this one.
     * These mappings replace any mappings that this map had for any of the
     * keys currently in the specified map.
     * 根据指定map创建chm
     * @param m mappings to be stored in this map
     */
    public void putAll(Map<? extends K, ? extends V> m) {
        tryPresize(m.size());
        for (Map.Entry<? extends K, ? extends V> e : m.entrySet())
            putVal(e.getKey(), e.getValue(), false);
    }

    /**
     * Creates a new, empty map with an initial table size based on
     * the given number of elements ({@code initialCapacity}) and
     * initial table density ({@code loadFactor}).
     *
     * @param initialCapacity the initial capacity. The implementation
     * performs internal sizing to accommodate this many elements,
     * given the specified load factor.
     * @param loadFactor the load factor (table density) for
     * establishing the initial table size
     * @throws IllegalArgumentException if the initial capacity of
     * elements is negative or the load factor is nonpositive
     * 指定table初始容量和负载因子的构造器
     * @since 1.6
     */
    public ConcurrentHashMap(int initialCapacity, float loadFactor) {
        this(initialCapacity, loadFactor, 1);
    }

    /**
     * Creates a new, empty map with an initial table size based on
     * the given number of elements ({@code initialCapacity}), table
     * density ({@code loadFactor}), and number of concurrently
     * updating threads ({@code concurrencyLevel}).
     * 指定table初始容量、负载因子、并发级别的构造器
     * @param initialCapacity the initial capacity. The implementation
     * performs internal sizing to accommodate this many elements,
     * given the specified load factor.
     * @param loadFactor the load factor (table density) for
     * establishing the initial table size
     * @param concurrencyLevel the estimated number of concurrently
     * updating threads. The implementation may use this value as
     * a sizing hint.
     * @throws IllegalArgumentException if the initial capacity is
     * negative or the load factor or concurrencyLevel are
     * nonpositive
     */
    public ConcurrentHashMap(int initialCapacity,
                             float loadFactor, int concurrencyLevel) {
        if (!(loadFactor > 0.0f) || initialCapacity < 0 || concurrencyLevel <= 0)
            throw new IllegalArgumentException();
        if (initialCapacity < concurrencyLevel)   // Use at least as many bins
            initialCapacity = concurrencyLevel;   // as estimated threads
        long size = (long)(1.0 + (long)initialCapacity / loadFactor);
        int cap = (size >= (long)MAXIMUM_CAPACITY) ?
            MAXIMUM_CAPACITY : tableSizeFor((int)size);
        this.sizeCtl = cap;
    }

常量


//最大容量.
private static final int MAXIMUM_CAPACITY = 1 << 30;

//默认初始容量
private static final int DEFAULT_CAPACITY = 16;


static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

//负载因子,为了兼容JDK1.8以前的版本而保留,JDK1.8中的ConcurrentHashMap的负载因子恒定为0.75
private static final float LOAD_FACTOR = 0.75f;


//链表--->树的阈值:即链接结点数大于8时, 链表转换为树.
static final int TREEIFY_THRESHOLD = 8;

//树--->链表的阈值:即树结点树小于6时,树转换为链表.
static final int UNTREEIFY_THRESHOLD = 6;

/**
 * 在链表--->树之前,还会有一次判断:
 * 即只有键值对数量大于MIN_TREEIFY_CAPACITY,才会发生转换。
 * 这是为了避免在Table建立初期,多个键值对恰好被放入了同一个链表中而导致不必要的转化。
 */
static final int MIN_TREEIFY_CAPACITY = 64;

/**
 * 在树--->链表之前,还会有一次判断:
 * 即只有键值对数量小于MIN_TRANSFER_STRIDE,才会发生转换.
 */
private static final int MIN_TRANSFER_STRIDE = 16;

//用于在扩容时生成唯一的随机数.
private static int RESIZE_STAMP_BITS = 16;

//可同时进行扩容操作的最大线程数.
private static final int MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1;

/**
 * The bit shift for recording size stamp in sizeCtl.
 */
private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS;

/*
* hash值的意义
*/
static final int MOVED = -1;                // 标识ForwardingNode结点(在扩容时才会出现,不存储实际数据)
static final int TREEBIN = -2;              // 标识红黑树的根结点
static final int RESERVED = -3;             // 标识ReservationNode结点()
static final int HASH_BITS = 0x7fffffff;    // 标识Node结点,自然数0,1,2...

//CPU核心数,扩容时使用
static final int NCPU = Runtime.getRuntime().availableProcessors();

字段


//Node数组,标识整个Map,首次插入元素时创建,大小总是2的幂次.
transient volatile Node<K, V>[] table;

//扩容后的新Node数组,只有在扩容时才非空.
private transient volatile Node<K, V>[] nextTable;

/**
 * sizeCtl:控制table的初始化和扩容.很重要💦💦💦💦
 * 0  : 初始默认值
 * -1 : 有线程正在进行table的初始化
 * >0 : table初始化时使用的容量,或初始化/扩容完成后的阈值threshold
 * -(1 + nThreads) : 记录正在执行扩容任务的线程数
 */
private transient volatile int sizeCtl;

//扩容时需要用到的一个下标变量.
private transient volatile int transferIndex;

//计数基值,当没有线程竞争时,计数将直接加到该变量上。类似于LongAdder的base变量
private transient volatile long baseCount;

//计数数组,出现并发冲突时使用,这时候计数不能直接加到baseCount变量上,类似于LongAdder的cells数组
private transient volatile CounterCell[] counterCells;

// 自旋标识位,用于CounterCell[]扩容时使用。类似于LongAdder的cellsBusy变量
private transient volatile int cellsBusy;

// 视图相关字段
private transient KeySetView<K, V> keySet;
private transient ValuesView<K, V> values;
private transient EntrySetView<K, V> entrySet;

put方法源码分析

    /**
     * Maps the specified key to the specified value in this table.
     * Neither the key nor the value can be null.
     * 指定key,value,并且key,value都不能为null
     * <p>The value can be retrieved by calling the {@code get} method
     * with a key that is equal to the original key.
     * 可以通过调用该方法检索键值对
     * @param key key with which the specified value is to be associated
     * @param value value to be associated with the specified key
     * @return the previous value associated with {@code key}, or
     *         {@code null} if there was no mapping for {@code key}
     * @throws NullPointerException if the specified key or value is null
     */
    public V put(K key, V value) {
        return putVal(key, value, false);
    }

    /**
     * @Description 实际的插入操作
     * @param key 表示键值对=》键
     * @param value 表示键值对=》值
     * @param onlyIfAbsent true=>表示key不存在才进行插入操作
     *
     **/
    final V putVal(K key, V value, boolean onlyIfAbsent) {
        if (key == null || value == null) throw new NullPointerException();
        int hash = spread(key.hashCode());
        // 使用链表保存时,binCount记录table[i]这个桶中所保存的结点数
        // 使用红黑树保存时,binCount==2,保证put后更改计数值时能够进行扩容检查,同时不触发红黑树化操作
        int binCount = 0;
        // 死循环进行插入
        for (Node<K,V>[] tab = table;;) {
            Node<K,V> f; int n, i, fh;
            // 首次初始化
            if (tab == null || (n = tab.length) == 0)
                tab = initTable();
            // table[i]对应的桶为null,则直接插入结点
            // tabAt(tab, i)方法是调用Unsafe类的方法查看值,保证每次获取到的值都是最新的
            // table[i]的索引i的计算方式 (n - 1) & hash) 为什么这样计算
            // 这是因为当table.length为2的幂次时,(table.length-1) 的二进制形式的特点是除最高位外全部是1,配合这种索引计算方式可以实现key在table中
            // 的均匀分布 ,减少hash冲突——出现hash冲突时,结点就需要以链表或红黑树的形式链接到table[i],这样无论是插入还是查找都需要额外的时间

            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
                if (casTabAt(tab, i, null,
                             new Node<K,V>(hash, key, value, null)))
                    // 插入一个链表结点,成功返回
                    break;                   // no lock when adding to empty bin
            }
            // 发现ForwardingNode结点(通过MOVED标识ForwardingNode结点存在),说明此时table正在扩容,则尝试协助数据迁移
            else if ((fh = f.hash) == MOVED)
                tab = helpTransfer(tab, f);
            // 出现hash冲突,也就是table[i]桶中已经有了结点
            else {
                V oldVal = null;
                // 对table[i]结点上锁,避免并发情况
                synchronized (f) {
                    // 再判断一下节点 是不是在这个 table[i]上
                    if (tabAt(tab, i) == f) {
                        // fh >= 0--->table[i]是链表结点
                        if (fh >= 0) {
                            // 记录结点数,超过阈值后,需要转为红黑树,提高查找效率
                            binCount = 1;
                            for (Node<K,V> e = f;; ++binCount) {
                                K ek;
                                // 找到“相等”的结点,判断是否需要更新value值(onlyIfAbsent为false则可以插入)
                                if (e.hash == hash &&
                                    ((ek = e.key) == key ||
                                     (ek != null && key.equals(ek)))) {
                                    // 记录key冲突的原来对应的value值
                                    oldVal = e.val;
                                    // onlyIfAbsent标志仅当不存在时才插入
                                    if (!onlyIfAbsent)
                                        e.val = value;
                                    break;
                                }
                                // 如果没有找到,则直接插在尾部即可
                                Node<K,V> pred = e;
                                if ((e = e.next) == null) {
                                    pred.next = new Node<K,V>(hash, key,
                                                              value, null);
                                    break;
                                }
                            }
                        }
                        // 继续判断table[i]是不是红黑树结点
                        else if (f instanceof TreeBin) {
                            Node<K,V> p;
                            // 红黑树对应的基值,包括了treebin和root,所以以2为基础
                            binCount = 2;
                            if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                                           value)) != null) {
                                oldVal = p.val;
                                if (!onlyIfAbsent)
                                    p.val = value;
                            }
                        }
                    }
                }
                if (binCount != 0) {
                    // 超过阈值,链表 -> 红黑树 转换
                    if (binCount >= TREEIFY_THRESHOLD)
                        treeifyBin(tab, i);
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
        }
        // 表明本次操作增加了新值, 计数值加1
        addCount(1L, binCount);
        return null;
    }
	/**
	 * @Description 计算hash值的方法,防止并发情况
	 **/
    static final int spread(int h) {
        return (h ^ (h >>> 16)) & HASH_BITS;
    }

    /**
     * Initializes table, using the size recorded in sizeCtl.
     * 初始化table, 使用sizeCtl作为初始化容量.
     */
    private final Node<K,V>[] initTable() {
        Node<K,V>[] tab; int sc;
        // transient volatile Node<K,V>[] table;
        // Node数组,标识整个Map,首次插入元素时创建,大小总是2的幂次.
        // 自旋直到初始化成功(每次循环都获取最新的Node数组引用)
        while ((tab = table) == null || tab.length == 0) {
            // sizeCtl<0 :说明table已经正在初始化/扩容,所以让出CPU时间片
            if ((sc = sizeCtl) < 0)
                Thread.yield(); // lost initialization race; just spin
            // 将sizeCtl更新成-1,表示正在初始化中
            else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
                // 如果CAS操作成功了,代表本线程将负责初始化工作
                try {
                    // 再次检查table数组是否为空
                    if ((tab = table) == null || tab.length == 0) {
                        // 在初始化Map时,sizeCtl代表数组大小,默认16,所以此时n默认为16
                        int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
                        // 创建容量为16 node数组
                        @SuppressWarnings("unchecked")
                        Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                        table = tab = nt;
                        //  n - (n >>> 2) = n - n/4 = 0.75n = 12
                        // 相当于设置了阈值threshold
                        sc = n - (n >>> 2);
                    }
                } finally {
                    // 将计算后的sc=12直接赋值给sizeCtl,表示达到12长度就扩容,由于这里只会有一个线程在执行,直接赋值即可,没有线程安全问题
                    // 只需要保证可见性
                    sizeCtl = sc;
                }
                break;
            }
        }
        return tab;
    }
	/**
     * 更改计数值,并检查长度是否达到阈值
     */
    private final void addCount(long x, int check) {
        // 计数桶 as
        CounterCell[] as; long b, s;
        // !U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x):尝试更新baseCount
        // 如果counterCells不为null,则代表已经初始化了,直接进入if语句块
        // 若竞争不严重,counterCells有可能还未初始化,为null,先尝试CAS操作递增baseCount值
        if ((as = counterCells) != null ||
            !U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
            // 进入此语句块有两种可能:
   			// 1.counterCells被初始化完成了,不为null
    		// 2.CAS操作递增baseCount值失败了,说明出现并发冲突,则将计数值累加到Cell槽
            CounterCell a; long v; int m;
            // 标志是否存在竞争
            boolean uncontended = true;
             //1.先判断计数桶是否初始化,如果as=null,说明没有,进入语句块
 			 //2.判断计数桶长度是否为空,若是进入语句块
 			 //3.这里做了一个线程变量随机数,若桶的这个位置为空,进入语句块(根据线程hash值计算槽索引)
 			 //4.到这里说明桶已经初始化了,且随机的这个位置不为空,尝试CAS操作使桶加1,失败进入语句块
            if (as == null || (m = as.length - 1) < 0 ||
                (a = as[ThreadLocalRandom.getProbe() & m]) == null ||
                !(uncontended =
                  U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
                fullAddCount(x, uncontended);
                return;
            }
            if (check <= 1)
                return;
            s = sumCount();
        }
        //  检测是否扩容
        if (check >= 0) {
            Node<K,V>[] tab, nt; int n, sc;
            while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
                   (n = tab.length) < MAXIMUM_CAPACITY) {
                int rs = resizeStamp(n);
                if (sc < 0) {
                    if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
                        sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
                        transferIndex <= 0)
                        break;
                    if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
                        transfer(tab, nt);
                }
                else if (U.compareAndSwapInt(this, SIZECTL, sc,
                                             (rs << RESIZE_STAMP_SHIFT) + 2))
                    transfer(tab, null);
                s = sumCount();
            }
        }
    }
	/**
	 * 当出现了并发冲突,则不会再用CAS方式来计数了,直接使用桶方式,
	 * 从上面的addCount方法可以看出来,此时的countCell是为空的(或者不为空但CAS更新失败),最终一定会进入fullAddCount方法来进行初始化桶
	 **/
   private final void fullAddCount(long x, boolean wasUncontended) {
        int h;
        if ((h = ThreadLocalRandom.getProbe()) == 0) {
            ThreadLocalRandom.localInit();      // force initialization
            h = ThreadLocalRandom.getProbe();
            wasUncontended = true;
        }
        boolean collide = false;                // True if last slot nonempty
        for (;;) {
            CounterCell[] as; CounterCell a; int n; long v;
            ...
            //如果计数桶!=null,证明已经初始化,此时不走此语句块
            if ((as = counterCells) != null && (n = as.length) > 0) {
              ...
            }
            //进入此语句块进行计数桶的初始化
            //CAS设置cellsBusy=1,表示现在计数桶Busy中...
            else if (cellsBusy == 0 && counterCells == as &&
                     U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
                //若有线程同时初始化计数桶,由于CAS操作只有一个线程进入这里
                boolean init = false;
                try {                           // Initialize table
                    //再次确认计数桶为空
                    if (counterCells == as) {
                        //初始化一个长度为2的计数桶👌👌
                        CounterCell[] rs = new CounterCell[2];
                        //h为一个随机数,与上1则代表结果为0、1中随机的一个
                        //也就是在0、1下标中随便选一个计数桶,x=1,放入1的值代表增加1个容量
                        rs[h & 1] = new CounterCell(x);
                        //将初始化好的计数桶赋值给ConcurrentHashMap
                        counterCells = rs;
                        init = true;
                    }
                } finally {
                    //最后将busy标识设置为0,表示不busy了
                    cellsBusy = 0;
                }
                if (init)
                    break;
            }
            //若有线程同时来初始化计数桶,则没有抢到busy资格的线程就先来CAS递增baseCount
            else if (U.compareAndSwapLong(this, BASECOUNT, v = baseCount, v + x))
                break;                          // Fall back on using base
        }
    }

扩容机制

linkedhashmap

https://blog.csdn.net/qq_40050586/article/details/105851970

概述

LinkedHashMap和HashMap的存储结构基本相同,都是数组加链表。唯一不同的是,链表结点有两个指针,分别是before和after,指向该结点的直接后继和直接前驱,再通过双向链表表头结点,就可以实现从双向链表中的任意一个结点开始,很方便地访问它的前驱结点和后继结点。LinkedHashMap采用的hash算法和HashMap相同,但它扩展了Entry(结点,用于存储key和value)。LinkedHashMap中的Entry增加了 before 和 after两个指针,用于维护双向链表。

在这里插入图片描述

构造函数

  /**
     * Constructs an empty insertion-ordered <tt>LinkedHashMap</tt> instance
     * 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 LinkedHashMap(int initialCapacity, float loadFactor) {
        super(initialCapacity, loadFactor);
        accessOrder = false;
    }

    /**
     * 构造一个空的,按插入序(accessOrder = false)的LinkedHashMap,使用默认初始大小和负载因子0.75
     *
     * @param  initialCapacity the initial capacity
     * @throws IllegalArgumentException if the initial capacity is negative
     */
    public LinkedHashMap(int initialCapacity) {
        super(initialCapacity);
        accessOrder = false;
    }

    /**
     * 默认构造函数也是关闭了get改变顺序,使用插入序。
     */
    public LinkedHashMap() {
        super();
        accessOrder = false;
    }

    /**
     * Constructs an insertion-ordered <tt>LinkedHashMap</tt> instance with
     * the same mappings as the specified map.  The <tt>LinkedHashMap</tt>
     * instance is created with a default load factor (0.75) and an initial
     * capacity sufficient to hold the mappings in the specified map.
     *
     * @param  m the map whose mappings are to be placed in this map
     * @throws NullPointerException if the specified map is null
     */
    public LinkedHashMap(Map<? extends K, ? extends V> m) {
        super();
        accessOrder = false;
        putMapEntries(m, false);
    }

    /**
     * 这个构造方法指定了accessOrder
     *
     * @param  initialCapacity the initial capacity
     * @param  loadFactor      the load factor
     * @param  accessOrder     the ordering mode - <tt>true</tt> for
     *         access-order, <tt>false</tt> for insertion-order
     * @throws IllegalArgumentException if the initial capacity is negative
     *         or the load factor is nonpositive
     */
    public LinkedHashMap(int initialCapacity,
                         float loadFactor,
                         boolean accessOrder) {
        super(initialCapacity, loadFactor);
        this.accessOrder = accessOrder;
    }

维护链表关键

afterNodeRemoval,afterNodeInsertion,afterNodeAccess。这三个方法的主要作用是,在删除,插入,获取节点之后,对链表进行维护。简单来说,这三个方法中执行双向链表的操作

    //在节点删除后,维护链表,传入删除的节点
    void afterNodeRemoval(Node<K,V> e) { // unlink
        //p指向待删除元素,b执行前驱,a执行后驱
        LinkedHashMap.Entry<K,V> p =
            (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
        //这里执行双向链表删除p节点操作,很简单。
        p.before = p.after = null;
        if (b == null)
            head = a;
        else
            b.after = a;
        if (a == null)
            tail = b;
        else
            a.before = b;
    }
  //在节点被访问后根据accessOrder判断是否需要调整链表顺序
    void afterNodeAccess(Node<K,V> e) { // move node to last
        LinkedHashMap.Entry<K,V> last;
        //如果accessOrder为false,什么都不做
        if (accessOrder && (last = tail) != e) {
            //p指向待删除元素,b执行前驱,a执行后驱
            LinkedHashMap.Entry<K,V> p =
                (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
            //这里执行双向链表删除操作
            p.after = null;
            if (b == null)
                head = a;
            else
                b.after = a;
            if (a != null)
                a.before = b;
            else
                last = b;
            //这里执行将p放到尾部
            if (last == null)
                head = p;
            else {
                p.before = last;
                last.after = p;
            }
            tail = p;
            //保证并发读安全。
            ++modCount;
        }
    }

    void afterNodeInsertion(boolean evict) { // possibly remove eldest
        LinkedHashMap.Entry<K,V> first;
        //removeEldestEntry(first)默认返回false,所以afterNodeInsertion这个方法其实并不会执行
        if (evict && (first = head) != null && removeEldestEntry(first)) {
            K key = first.key;
            removeNode(hash(key), key, null, false, true);
        }
    }

    protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
        return false;
    }

为什么不执行也可以呢,这个要到put操作的时候就能看出来了。

那么removeEldestEntry这个方法有什么用呢,看名字可以知道是删除最久远的节点,也就是head节点,这个方法实际是给我们自己扩展的。默认是没有用的,接下来实现LRU的代码中将可以看到它的作用。

put源码分析

LinkedHashMap没有重写HashMap的put方法,所以执行put操作的时候,还是使用的是HashMap的put方法。那么这样如何保证链表的逻辑呢?原因就是HashMap的putVal方法中实际调用了维护链表的方法,下面是关键代码:HashMap的putVal()方法

    //默认的传入的evict是true
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
 			................
            ................
            ................
            ................
            if (e != null) { // existing mapping for key
                //如果e不为null,此时的e指向的就是在map中的那个插入点,所以这个时候来赋值。
                V oldValue = e.value;
                // onlyIfAbsent入口参数,为true,则不更新value(前面已说明)。
                //这个地方的主要作用主要控制如果map中已经有那个key了,是否需要需要更新值
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                //这里其实是插入成功后执行的,获得的效果就是将e放到了链表结尾。
                //所以afterNodeInsertion方法就算什么都不做也可以。
                //但是如果accessOrder为false,那么我们新插入的节点,都不会进入链表了
                afterNodeAccess(e);
                return oldValue;
            }
        }
        //fast-fail机制的实现,为了保证并发读安全。
        ++modCount;
        //容器中的键值对数自增,如果大于了阈值,开始扩容
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

在put方法中,HashMap会在合适的位置使用 afterNodeAccess(e),和afterNodeInsertion(evict);方法。因为在HashMap中也定义了这三个函数,但是都是为空函数,在LInkedHashMap中只是重写了这3个方法。我们在使用map.put(key,value)的时候,实际调用HashMap#putVal(key,value)方法,然后再调用afterNodeAccess方法,那么这个时候调用的会是子类的afterNodeAccess方法吗?

这个就要涉及到多态的知识了,可以从虚拟机方面去解释:在虚拟机加载类的解析过程中,对方法调用有两种分派方式,静态分派对应重载,动态分派对应重写。这里对应的就是动态分派。动态分配是在运行时发生的,它对于方法是按照实际类型来首先寻找的。找不到再往父类走。这里的实际类型其实值new 后面跟着的类。所以这里不用担心会调用到父类的方法。

afterNodeInsertion方法不是没有用,而是留给扩展用的,下面会展示。

还有一点,put操作中使用afterNodeAccess来将新插入的节点放到尾部。但是这个方法要受到accessOrder的控制,如果accessOrder为false(默认就为false)那么新插入的节点应该就不能插入到链表中了。这样设计有什么特殊的意义吗?

get源码分析

LinkedHashMap重写了HashMap的get方法,如下:

    /**
     * 调用hashmap的getNode方法获取到值之后,维护链表
     * @param key
     * @return
     */
    public V get(Object key) {
        Node<K,V> e;
        if ((e = getNode(hash(key), key)) == null)
            return null;
        if (accessOrder)
            afterNodeAccess(e);
        return e.value;
    }

remove源码分析

    final Node<K,V> removeNode(int hash, Object key, Object value,
                               boolean matchValue, boolean movable) {
        Node<K,V>[] tab; Node<K,V> p; int n, index;
        //判断table是否为空,该key对应的桶是否为空
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (p = tab[index = (n - 1) & hash]) != null) {
            Node<K,V> node = null, e; K k; V v;
            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 = e;
                    } while ((e = e.next) != null);
                }
            }
            //到这里了其实node就已经指向了要删除的节点了
            //matchValue的作用是指现在是否需要值匹配。因为可能没有传入value,所以判断一下
            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;
                //调用维护链表的操作
                afterNodeRemoval(node);
                return node;
            }
        }
        return null;
    }

实现LRU算法缓存

LRU,即最近最少使用。LRU中保存的数据如果满了,那么就会将最近最少使用的数据删除。

LinkedHashMap通过使accessOrder为true,可以满足这种特性。

class LRUCache extends LinkedHashMap {

    private int capacity;

    public LRUCache(int capacity) {
        //accessOrder为true
        super(capacity, 0.75F, true);
        this.capacity = capacity;
    }

    public int get(int key) {
        return (int)super.getOrDefault(key, -1);
    }

    public void put(int key, int value) {
        super.put(key, value);
    }

    protected boolean removeEldestEntry(Map.Entry eldest) {
        return size() > capacity;
    }
}

力扣算法LRU

这里重写了removeEldestEntry方法,然后removeEldestEntry方法在afterNodeInsertion中被调用,如果这个方法返回真,那么就会删除head指向的节点。根据每次get的节点都会放到尾部的特性,所以head指向的节点就是最久没有使用到的节点,所以可以删除。由于我们每次put完(HashMap#putVal())都会调用这个afterNodeInsertion方法,所以可以上面的设计可以使put过后如果size超了,将删除最久没有使用的一个节点,从而腾出空间给新的节点。

总结

一句话总结,LinkedHashMap就是HashMap中将其node维护成了一个双向链表。只要搞懂了HashMap就可以很容易搞懂LinkedHashMap。

TreeMap

image-20220924201533394

概念理解

TreeMap集合是基于红黑树(Red-Black tree)NavigableMap实现。该集合最重要的特点就是可排序,该映射根据其键的自然顺序进行排序,或者根据创建映射时提供的 Comparator 进行排序,具体取决于使用的构造方法。这句话是什么意思呢?就是说TreeMap可以对添加进来的元素进行排序,可以按照默认的排序方式,也可以自己创建排序器放入构造方法指定排序方式。

根据上一条,我们要想使用TreeMap存储并排序我们自定义的类(如User类),那么必须自己定义比较机制:一种方式是User类去实现java.lang.Comparable接口,并实现其compareTo()方法。另一种方式是写一个类(如MyCompatator)去实现java.util.Comparator接口,并实现compare()方法,然后将MyCompatator类实例对象作为TreeMap的构造方法参数进行传参(当然也可以使用匿名内部类)

类名及类成员变量

public class TreeMap<K,V>
    extends AbstractMap<K,V>
    implements NavigableMap<K,V>, Cloneable, java.io.Serializable
{
    // 比较器对象
    private final Comparator<? super K> comparator;

    // 根节点
    private transient Entry<K,V> root;

    // 集合大小
    private transient int size = 0;

    // 树结构被修改的次数
    private transient int modCount = 0;

    // 静态内部类用来表示节点类型
    static final class Entry<K,V> implements Map.Entry<K,V> {
        K key;     // 键
        V value;   // 值
        Entry<K,V> left;    // 指向左子树的引用(指针)
        Entry<K,V> right;   // 指向右子树的引用(指针)
        Entry<K,V> parent;  // 指向父节点的引用(指针)
        boolean color = BLACK; // 
    }
}

构造方法

    public TreeMap() {   // 1,无参构造方法
        comparator = null; // 默认比较机制
    }

    public TreeMap(Comparator<? super K> comparator) { // 2,自定义比较器的构造方法
        this.comparator = comparator;
    }

    public TreeMap(Map<? extends K, ? extends V> m) {  // 3,构造已知Map对象为TreeMap
        comparator = null; // 默认比较机制
        putAll(m);
    }

    public TreeMap(SortedMap<K, ? extends V> m) { // 4,构造已知的SortedMap对象为TreeMap
        comparator = m.comparator(); // 使用已知对象的构造器
        try {
            buildFromSorted(m.size(), m.entrySet().iterator(), null, null);
        } catch (java.io.IOException cannotHappen) {
        } catch (ClassNotFoundException cannotHappen) {
        }
    }

put方法

    public V put(K key, V value) {
        Entry<K,V> t = root;  // 获取根节点

        // 如果根节点为空,则该元素置为根节点 
        if (t == null) {
            compare(key, key); // type (and possibly null) check

            root = new Entry<>(key, value, null);
            size = 1;    // 集合大小为1
            modCount++;  // 结构修改次数自增
            return null;
        }

        int cmp;
        Entry<K,V> parent;
        Comparator<? super K> cpr = comparator;  // 比较器对象

        // 如果比较器对象不为空,也就是自定义了比较器
        if (cpr != null) {   
            do { // 循环比较并确定元素应插入的位置(也就是找到该元素的父节点)
                parent = t;  // t就是root

                // 调用比较器对象的compare()方法,该方法返回一个整数
                cmp = cpr.compare(key, t.key); 
                if (cmp < 0)      // 待插入元素的key"小于"当前位置元素的key,则查询左子树
                    t = t.left;
                else if (cmp > 0) // 待插入元素的key"大于"当前位置元素的key,则查询右子树
                    t = t.right;
                else              // "相等"则替换其value。
                    return t.setValue(value);
            } while (t != null);
        }

        // 如果比较器对象为空,使用默认的比较机制
        else {
            if (key == null)
                throw new NullPointerException();
            @SuppressWarnings("unchecked")
                Comparable<? super K> k = (Comparable<? super K>) key; // 取出比较器对象
            do {  // 同样是循环比较并确定元素应插入的位置(也就是找到该元素的父节点)
                parent = t;
                cmp = k.compareTo(t.key); // 同样调用比较方法并返回一个整数
                if (cmp < 0)       // 待插入元素的key"小于"当前位置元素的key,则查询左子树
                    t = t.left;
                else if (cmp > 0)  // 待插入元素的key"大于"当前位置元素的key,则查询右子树
                    t = t.right;
                else               // "相等"则替换其value。
                    return t.setValue(value);
            } while (t != null);
        }

        Entry<K,V> e = new Entry<>(key, value, parent);  // 根据key找到父节点后新建一个节点
        if (cmp < 0)  // 根据比较的结果来确定放在左子树还是右子树
            parent.left = e;
        else
            parent.right = e;
        fixAfterInsertion(e);
        size++;      // 集合大小+1
        modCount++;  // 集合结构被修改次数+1
        return null;
    }

getEntry,getFirstEntry,getLastEntry方法

final Entry<K,V> getEntry(Object key) {
        // 如果有自定义比较器对象,就按照自定义规则遍历二叉树
        if (comparator != null)
            return getEntryUsingComparator(key);
        if (key == null)
            throw new NullPointerException();
        @SuppressWarnings("unchecked")
            Comparable<? super K> k = (Comparable<? super K>) key;
        Entry<K,V> p = root;
        while (p != null) {    // 按照默认比较规则遍历二叉树
            int cmp = k.compareTo(p.key);
            if (cmp < 0)
                p = p.left;
            else if (cmp > 0)
                p = p.right;
            else
                return p;
        }
        return null;
    }
    final Entry<K,V> getFirstEntry() { // 获取第一个元素也就是最小的元素,一直遍历左子树
        Entry<K,V> p = root;
        if (p != null)
            while (p.left != null)
                p = p.left;
        return p;
    }

    final Entry<K,V> getLastEntry() { // 获取最后个元素也就是最大的元素,一直遍历右子树
        Entry<K,V> p = root;
        if (p != null)
            while (p.right != null)
                p = p.right;
        return p;
    }

排序方案

默认排序
    /**
     * 验证有序集合
     */
    @Test
    public void checkSortCollection() {
        // 默认排序,通过Comparable接口中compareTo方法
        log.info("默认排序,key是字符串类型");
        TreeMap<String, Object> map = new TreeMap<>();
        map.put("c", "v1");
        map.put("b", "v3");
        map.put("A", "v2");
        map.forEach((i, j) -> log.info("map:key->[{}],value:[{}]", i, j));
        log.info("默认排序,key是数值类型");
        TreeMap<Integer, Object> intMap = new TreeMap<>();
        intMap.put(11,"Three");
        intMap.put(1,"One");
        intMap.put(3,"Two");
        intMap.forEach((k,v)->log.info("key:[{}],value:[{}]",k,v));
    }

16:05:32.535 [main] INFO com.geekmice.staging.staging.dao.EveryQuestionTest - 默认排序,key是字符串类型
16:05:32.600 [main] INFO com.geekmice.staging.staging.dao.EveryQuestionTest - map:key->[A],value:[v2]
16:05:32.602 [main] INFO com.geekmice.staging.staging.dao.EveryQuestionTest - map:key->[b],value:[v3]
16:05:32.602 [main] INFO com.geekmice.staging.staging.dao.EveryQuestionTest - map:key->[c],value:[v1]
16:05:32.602 [main] INFO com.geekmice.staging.staging.dao.EveryQuestionTest - 默认排序,key是数值类型
16:05:32.603 [main] INFO com.geekmice.staging.staging.dao.EveryQuestionTest - key:[1],value:[One]
16:05:32.603 [main] INFO com.geekmice.staging.staging.dao.EveryQuestionTest - key:[3],value:[Two]
16:05:32.603 [main] INFO com.geekmice.staging.staging.dao.EveryQuestionTest - key:[11],value:[Three]

自定义排序
实现Comparable接口,重写compareTo()方法
    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    static class School implements Comparable<School> {
        private String schoolName;
        private String schoolAddr;
        private Integer num;

        /**
         * 实现Comparable接口在这里自定义排序规则
         * 如果什么不写的话,默认返回return 0;只能存储第一个被put的元素
         * 升序这个写法
         *
         * @param o
         * @return
         */
        @Override
        public int compareTo(School o) {
            if (num > o.getNum()) {
                return 1;
            } else if (num < o.getNum()) {
                return -1;
            }
            return 0;
        }
    }

案例说明

/**
 * 验证有序集合
 */
@Test
public void checkSortCollection() {

    log.info("自定义排序之key实现Comparable接口");
    TreeMap<School, Integer> customizeKeyMap = new TreeMap<>();
    customizeKeyMap.put(new School(UUID.randomUUID().toString(),UUID.randomUUID().toString(),10),1);
    customizeKeyMap.put(new School(UUID.randomUUID().toString(),UUID.randomUUID().toString(),11),1);
    customizeKeyMap.put(new School(UUID.randomUUID().toString(),UUID.randomUUID().toString(),1),1);
    customizeKeyMap.forEach((k,v)->log.info("key:[{}],value:[{}]",k,v));
}
创建排序器
static class MyComparator implements Comparator<School> {

    @Override
    public int compare(School o1, School o2) {
        if (o1.getNum() > o2.getNum()) {
            return 1;
        } else if (o1.getNum() < o2.getNum()) {
            return -1;
        }
        return 0;
    }
}

案例分析

/**
 * 验证有序集合
 */
@Test
public void checkSortCollection() {
    log.info("自定义排序之创建排序器实现Comparator接口");
    MyComparator myComparator = new MyComparator();
    TreeMap<School, Integer> customizeComparator = new TreeMap<>(myComparator);
    customizeComparator.put(new School(UUID.randomUUID().toString(),UUID.randomUUID().toString(),1),1);
    customizeComparator.put(new School(UUID.randomUUID().toString(),UUID.randomUUID().toString(),-1),1);
    customizeComparator.put(new School(UUID.randomUUID().toString(),UUID.randomUUID().toString(),11),1);
    customizeComparator.forEach((k,v)->log.info("key:[{}],value:[{}]",k,v));
}

image-20221203162549169

ConcurrentHashMap是如何实现线程安全的

大厂之路一由浅入深、并行基础、源码分析一 “J.U.C”之collections框架:ConcurrentHashMap扩容迁移等方法的源码分析

超详细LinkedHashMap解析

HashSet的去重原理—Java基础

相关文章:

  • 西门子博途WINCC动画之旋转运动
  • 揭秘 HTTP 代理:增强在线活动的安全性和匿名性
  • 运营商的mpls专线
  • 【java9】java9新特性概述
  • bestvike--study7之进项目
  • 华为OD介绍
  • 深度学习 精选笔记(5)多层感知机
  • 【风格迁移】CAST:对比学习,从图像特征而非其二阶统计量(Gram矩阵)中学习风格
  • 个人建站前端篇(七)vite + vue3企业级项目模板
  • CKA认证,开启您的云原生之旅!
  • Unity3D 物理引擎的基本配置详解
  • Spring-Cloud-Gateway集成Sentinel限流
  • Pytorch Bert 中文分类 运行代码时候遇到的问题
  • 计算机毕业设计Java点播影院运营系统(源码+系统+mysql数据库+lw文档)
  • 论文阅读-ATLAS: A Sequence-based Learning Approach for Attack Investigation
  • 社区系统项目复盘-8
  • 校园论坛(Java)—— 数据报表模块
  • 《MySQL实战45讲》——学习笔记19 “SQL查一行执行慢的排查、锁等待/一致性读“【建议收藏】
  • ica1靶机(hydra爆破)
  • 一次性分清zip、gzip、bzip2、tar命令
  • Python源码剖析1-整数对象PyIntObject
  • 【论文笔记】Radatron: Accurate Detection Using Multi-Resolution Cascaded MIMO Radar
  • 网课题库接口调用方法
  • [附源码]计算机毕业设计大学生心理健康测评系统
  • Blackmagic黑魔法摄像机braw视频文件修复方法
  • 【怎么理解回流与重绘?以及触发场景】
  • Java项目:SSM共享汽车租赁平台
  • Spring框架(十一):手动实现一个@Component,讲一讲Spring的工厂后处理器
  • 集合java
  • MAML:User Diverse Preference Modeling by Multimodal AttentiveMetric Learning
  • 基于MATLAB的一级倒立摆控制仿真,带GUI界面操作显示倒立摆动画,控制器控制输出
  • qt人员管理模块(模块化程序)功能块复制直接使用不冲突