Collention集合基础知识

Array
  • 数组是一种连续的内存空间存储相同数据类型数据的线性数据结构
  1. 数组获取其他元素的地址值

寻址公式
a[i] = baseaddress + i*datatypesize

  1. 为什么数组索引从0开始 从1开始不行吗
    • 从0开始寻址公式

a[i] = baseaddress + i*datatypesize

  • 从1开始寻址公式

a[i] = baseaddress + (i-1)*datatypesize 效率没有从0开始高

  1. 对于cpu来说增加了一个减法指令

根据数组索引获取元素的时候会用索引和寻址公式来计算内存所对应的元素数据
寻址公式为a[i] = baseaddress + i*datatypesize
如果数组从1开始寻址公式 就需要增加一次减法操作 对于cpu来说就多了一个指令

  1. 数组查找时间复杂度
  • 索引查询

数组元素的访问是通过下标来访问 计算机通过数组的首地址和寻址公式 能够很快速的找到想要访问的元素

  • 未知索引查询

如果数组未排序 只能遍历查询每一个数组 O(n)

  1. 如果是已排序的数组 可以通过二分查询 O(logn)
  2. 操作数组的时间复杂度

数组是一段连续的内存空间 因此为了保证数组的连续性会使数组的插入和删除的效率变得很低O(n)

ArrayList

实现源码
成员变量

    /**
     * ArrayList的默认容量,为10。
     */
    private static final int DEFAULT_CAPACITY = 10;

    /**
     * 一个空的对象数组,用于表示空的ArrayList实例。
     */
    private static final Object[] EMPTY_ELEMENTDATA = {};

    /**
        也是一个空的对象数组,
        但用于表示默认大小的空ArrayList实例。
        当向这个ArrayList添加第一个元素时,它会被扩展到DEFAULT_CAPACITY
     */
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

    /**
        是一个transient类型的Object数组,用于存储ArrayList的元素。
        ArrayList的容量就是这个数组的长度
     */
    transient Object[] elementData; 
    /**
        ArrayList的大小,即它包含的元素数量。
     */
    private int size;

构造方法

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

参数initialCapacity指定列表的初始容量。

如果initialCapacity大于0,则创建一个具有指定容量的空元素数组。

如果initialCapacity等于0,则使用空元素数组EMPTY_ELEMENTDATA。

如果initialCapacity小于0,则抛出IllegalArgumentException异常,提示非法容量。


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

初始化一个空的ArrayList对象。它将元素数据elementData设置为一个默认容量的空数组DEFAULTCAPACITY_EMPTY_ELEMENTDATA,以便后续添加元素时能够存储。这个构造函数适用于在创建ArrayList对象时不需要指定初始容量的情况

    public ArrayList(Collection<? extends E> c) {
        Object[] a = c.toArray();
        if ((size = a.length) != 0) {
            if (c.getClass() == ArrayList.class) {
                elementData = a;
            } else {
                elementData = Arrays.copyOf(a, size, Object[].class);
            }
        } else {
            // replace with empty array.
            elementData = EMPTY_ELEMENTDATA;
        }
    }

该函数是ArrayList的构造函数,它接受一个Collection类型的参数c,并根据参数c的内容初始化一个ArrayList对象。首先将参数c转换为Object数组a,然后判断数组a的长度是否为0,如果不为0,则判断参数c的类型是否为ArrayList类型,如果是,则直接将数组a赋值给elementData成员变量;如果不是,则通过Arrays.copyOf方法创建一个新的Object数组,并将数组a的内容复制到新数组中,最后将新数组赋值给elementData成员变量。如果数组a的长度为0,则直接将EMPTY_ELEMENTDATA赋值给elementData成员变量

ArrayList.add(i) 分析
public class Array {
    public static void main(String[] args) {
        ArrayList<Integer> array = new ArrayList<>();
        array.add(-1);
    }
}

add()

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

调用ensureCapacityInternal(size + 1)方法来确保集合的容量足够容纳新元素

    private void ensureCapacityInternal(int minCapacity) {
                                计算容量
        ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
    }

计算容量

    private static int calculateCapacity(Object[] elementData, int minCapacity) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            return Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        return minCapacity;
    }
private void ensureExplicitCapacity(int minCapacity) {
    modCount++; // 1. 增加修改次数计数器modCount的值,这通常用于实现如ArrayList等集合的fail-fast机制,当在迭代过程中检测到modCount变化时会抛出 ConcurrentModificationException。

    // overflow-conscious code // 2. 注释提示此段代码考虑了溢出情况的处理
    if (minCapacity - elementData.length > 0) // 3. 检查传入的最小容量minCapacity是否大于当前数组elementData的长度
        grow(minCapacity); // 4. 如果确实需要扩容,则调用grow方法。grow方法会负责计算新的容量(通常是旧容量的1.5倍,并处理潜在的溢出问题),然后重新分配数组空间并将原数据复制到新数组中。
}

当之前扩容的值和当前元素数组长度相同进行扩容
容量不够进行扩容

    private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        // minCapacity is usually close to size, so this is a win:
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

步骤1: 获取旧容量
int oldCapacity = elementData.length;
这里 elementData 是 ArrayList 的内部数组,用于存储所有元素。oldCapacity 就是这个数组的当前长度,也就是 ArrayList 的当前容量。
步骤2: 计算新容量
int newCapacity = oldCapacity + (oldCapacity >> 1);
newCapacity 是计算出的新容量。这里使用了一个简单的数学公式:newCapacity = oldCapacity * 1.5。通过位运算 (oldCapacity >> 1) 实现除以2的操作,然后加上 oldCapacity 得到 1.5 * oldCapacity
步骤3: 检查新容量是否满足最小需求
if (newCapacity - minCapacity < 0)
如果新容量不足以满足 minCapacity 的需求,那么直接将 newCapacity 设置为 minCapacity。
步骤4: 检查新容量是否超过最大数组大小
if (newCapacity - MAX_ARRAY_SIZE > 0)
MAX_ARRAY_SIZE 是一个常量,代表数组的最大允许长度。如果 newCapacity 超过了这个限制,那么调用 hugeCapacity(minCapacity) 方法来处理。这个方法会根据 minCapacity 返回一个合适的容量值,确保不会超过 Integer.MAX_VALUE。
步骤5: 扩容并复制数据
elementData = Arrays.copyOf(elementData, newCapacity);
使用 Arrays.copyOf 方法创建一个新的数组,长度为 newCapacity,并将原数组 elementData 的所有元素复制到新数组中。之后,elementData 引用指向新数组,完成扩容过程。

  1. ArrayList
  • 底层数据结构

ArrayList底层是用动态的数组实现的

  • 初始容量

ArrayList初始容量为0,当第一次添加数据的时候才会初始化容量为10

  • 扩容逻辑

ArrayList在进行扩容的时候是原来容量的1.5倍,每次扩容都需要拷贝数组

  • 添加逻辑
    • 确保数组已使用长度(size)加1之后足够存下下一个数据
    • 计算数组的容量,如果当前数组已使用长度+1后的大于当前的数组长度,则调用grow方法扩容(原来的1.5倍)
    • 确保新增的数据有地方存储之后,则将新元素添加到位于size的位置上。
    • 返回添加成功布尔值。
  1. ArrayList list=new ArrayList(10)中的list扩容几次
    1. 调用grow方法的次数
    2. 带有参数的构造方法并没有调用这个方法 而是创建一个长度为10 的object数组赋值给elementdata
    3. 如果传入的参数为0会将elementdata指向空参empty_elementdata
  2. 如何实现数组和List之间的转换
        //数组转list
        String []strings = {"123","qwe","asd"};
        List<String> list = Arrays.asList(strings);
        //list 转数组
        List<String> array = new ArrayList<>();
        array.add("123");
        array.add("qwe");
        array.add("asd");
        String[] arrayString = list.toArray(new String[list.size()]);
  1. 用arrays.aslist转list后修改了数组内容list会受印象吗


Arrays.asList()转换list之后如果修改了数组内容 list受影响
虽然是new 了一个arrayList 但是这个内部类将我们传入的集合进行了包装 a = Objects.requireNonNull(array); 最终的指向还是同一块内存地址

    @SafeVarargs
    @SuppressWarnings("varargs")
    public static <T> List<T> asList(T... a) {
        return new ArrayList<>(a);
    }

    /**
     * @serial include
     */
    private static class ArrayList<E> extends AbstractList<E>
        implements RandomAccess, java.io.Serializable
    {
        private static final long serialVersionUID = -2764017481108945198L;
        private final E[] a;

        ArrayList(E[] array) {
            a = Objects.requireNonNull(array);
        }
    }
  1. List用toArray转数组后如果修改了List内容数组受印象吗

当调用了toArray以后在底层他还是进行了数组的拷贝

    public <T> T[] toArray(T[] a) {
        if (a.length < size)
            // Make a new array of a's runtime type, but my contents:
            return (T[]) Arrays.copyOf(elementData, size, a.getClass());
        System.arraycopy(elementData, 0, a, 0, size);
        if (a.length > size)
            a[size] = null;
        return a;
    }
  1. ArrayList和LinkedList区别
  2. 底层数据结构
    1. ArrayList是动态数组的数据结构
    2. LinkedList是双向链表的数据结构实现
  3. 操作的效率
    1. 查询
      1. ArrayList按照下标查询的时间复杂度为O(1)
      2. ArrayList 和LinkedList 都需要遍历 时间复杂度都是O(n)
    2. 删除和添加
      1. 删除指定的的都需要遍历链表时间复杂度为O(n)
    3. 内存占用
      1. ArrayList 底层是数组连续内存 节省内春
      2. linkedList是双向链表需要存储数据 和两个指针更占用内存
    4. 线程安全
      1. 都是线程不安全的
        1. 使用可以在方法内使用局部变量是线程安全的
        2. 使用collection.syinzed(new array())
HashMap

hash table 是根据key直接访问内存存储位置值的数据结构
将key映射为数组下标的函数为散列函数 hashvalue = hash(key)
散列函数的基本要求
散列函数计算的到的散列值必须是大于等于0的正整数 hashvlue为数组的下标
如果k1 = k2 那么经过hash后得到的哈希值也必须相同 hash(k1) = hash(k2)
如果k1!=k2那么经过hash后得到的hash值也不必相同 hash(k1)!=hash(k2)
散列冲突
在散列表总数组的每一个下标位置我们可以称之为桶 每一个桶会对应一条链表 所有散列值相同的元素我们都会放到相同槽位相对应的链表中

  1. hashmap实现原理
  • 当我们往hashmap中put元素时利用key的hashcode重新hash计算当前对象在数组中的下标
  • 存储时如果出现hash值现通的key
    • key相同覆盖原始值
    • key不同 将key则将key-vlaue放入链表或红黑树中
    • 1.7
      • 采用的是拉链法 将链表与数组结合 创建一个链表数组 数组中每一格就是一个链表 若遇到哈希冲突则将hash冲突的值加到链表中
    • 1.8
      • 当链表长度大于阈值8时且数组长度达到64时 将链表转化为红黑树 以减少搜索时间 扩容时 红黑树拆成树的节点树小于等于6个则退化成链表
  1. hashmap put 方法的具体流程外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

image.png

1
public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
2
    /**
     * 实现Map的put方法及其相关操作。
     * 此方法是Map.put操作及其变体的核心实现。
     *
     * @param hash   键的哈希值
     * @param key    键
     * @param value  要放入的值
     * @param onlyIfAbsent 如果为true,则仅在键不存在时才放入值
     * @param evict  如果为false,表示表处于创建模式(此参数目前未使用)
     * @return       与键关联的前一个值,如果之前没有值则返回null
     */
    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 {
                // 对哈希冲突进行线性探测
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        // 如果探测长度超过阈值,转换为树节点
                        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;
                }
            }
            // 如果找到现有节点,更新值或返回前一个值
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        // 更新大小和modCount,必要时调整大小
        ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

  1. 判断键值对数组table是否为空或为null,否则执行resize()进行扩容(初始化)
  2. 根据键值key计算hash值得到数组索引
  3. 判断table[i]==null,条件成立,直接新建节点添加
  4. 如果table[i]==null ,不成立4.1 判断table[i]的首个元素是否和key一样,如果相同直接覆盖value4.2 判断table[i] 是否为treeNode,即table[i] 是否是红黑树,如果是红黑树,则直接在树中插入键值对4.3 遍历table[i],链表的尾部插入数据,然后判断链表长度是否大于8,大于8的话把链表转换为红黑树,在红黑树中执行插入操 作,遍历过程中若发现key已经存在直接覆盖value
  5. 插入成功后,判断实际存在的键值对数量size是否超多了最大容量threshold(数组长度*0.75),如果超过,进行扩容。
  6. hashmap寻址算法
    1. 首先获取key的hashCode值,然后右移16位 异或运算 原来的hashCode值,主要作用就是使原来的hash值更加均匀,减少hash冲突
    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
  • 有了hash值之后,就很方便的去计算当前key的在数组中存储的下标
  • &hash : 得到数组中的索引,代替取模,性能更好,数组长度必须是2的n次幂
  1. hashmap扩容机制

image.png


//扩容、初始化数组
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;
    }
  • 在添加元素或初始化的时候需要调用resize方法进行扩容,第一次添加数据初始化数组长度为16,以后每次每次扩容都是达到了扩容阈值(数组长度 * 0.75)
  • 每次扩容的时候,都是扩容之前容量的2倍;
  • 扩容之后,会新创建一个数组,需要把老数组中的数据挪动到新的数组中
    • 没有hash冲突的节点,则直接使用 e.hash & (newCap - 1) 计算新数组的索引位置
    • 如果是红黑树,走红黑树的添加
    • 如果是链表,则需要遍历链表,可能需要拆分链表,判断(e.hash & oldCap)是否为0,该元素的位置要么停留在原始位置,要么移动到原始位置+增加的数组大小这个位置上
  1. hashmap的数组长度一定是2的次幂
    1. &hash : 得到数组中的索引,代替取模,性能更好,数组长度必须是2的n次幂
    2. 扩容时重新计算索引效率更高: hash & oldCap == 0 的元素留在原来位置 ,否则新位置 = 旧位置 + oldCap
  2. hashmap在1.7的情况下的多线程死循环问题
    1. jdk7的的数据结构是:数组+链表
    2. 在数组进行扩容的时候,因为链表是头插法,在进行数据迁移的过程中,有可能导致死循环
  • 变量e指向的是需要迁移的对象
  • 变量next指向的是下一个需要迁移的对象
  • Jdk1.7中的链表采用的头插法
  • 在数据迁移的过程中并没有新的对象产生,只是改变了对象的引用
  1. hashset 与hashmap的区别
  • HashSet实现了Set接口, 仅存储对象; HashMap实现了 Map接口, 存储的是键值对.
  • HashSet底层其实是用HashMap实现存储的, HashSet封装了一系列HashMap的方法. 依靠HashMap来存储元素值,(利用hashMap的key键进行存储), 而value值默认为Object对象. 所以HashSet也不允许出现重复值, 判断标准和HashMap判断标准相同, 两个元素的hashCode相等并且通过equals()方法返回true.
    | 区别 | HashTable | HashMap |
    | — | — | — |
    | 数据结构 | 数组+链表 | 数组+链表+红黑树 |
    | 是否可以为null | Key和value都不能为null | 可以为null |
    | hash算法 | key的hashCode() | 二次hash |
    | 扩容方式 | 当前容量翻倍 +1 | 当前容量翻倍 |
    | 线程安全 | 同步(synchronized)的,线程安全 | 非线程安全 |
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值