ArrayList和HashMap源码解毒

Java集合之ArrayList源码分析
https://www.cnblogs.com/xujian2014/p/4625346.html

面试必备:ArrayList源码解析(JDK8)
https://blog.csdn.net/zxt0601/article/details/77281231

小结:
ArrayList它是基于数组实现的List类。
相较于数组,它可以动态增长和缩减的索引序列。原理:该类封装了一个动态再分配的Object[]数组,每一个类对象都有一个capacity属性(表示它们所封装的Object[]数组的长度),当向ArrayList中添加元素时,该属性值会自动增加。
ArrayList的用法和Vector向类似,ArrayList和Vector的区别是:ArrayList是线程不安全的,当多条线程访问同一个ArrayList集合时,程序需要手动保证该集合的同步性,而Vector则是线程安全的。但是Vector是一个较老的集合,具有很多缺点(Vector 简介和优缺点 https://blog.csdn.net/mercy_ps/article/details/81098577),不建议使用。
ArrayList源码(这么看源码的方式的话,相当于在看API -_-||):常量的写法、三元运算符、三个构造方法、

  //将当前容量值设为当前实际元素大小
    public void trimToSize()
	  //将集合的capacit增加minCapacity
    public void ensureCapacity(int minCapacity)
//  变大/扩容
	  private void grow(int minCapacity)
    {
        int oldCapacity = elementData.length;
     //注意此处扩充capacity的方式是将其向右一位再加上原来的数,实际上是扩充了1.5倍
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        elementData = Arrays.copyOf(elementData, newCapacity);
    }
	  //返回ArrayList的大小
    public int size()
	//判断ArrayList是否为空
    public boolean isEmpty() {
	 //判断ArrayList中是否包含Object(o)
    public boolean contains(Object o) {
	  //正向查找,返回ArrayList中元素Object(o)的索引位置
    public int indexOf(Object o)
	 //逆向查找,返回返回ArrayList中元素Object(o)的索引位置
    public int lastIndexOf(Object o) {
	  //返回此 ArrayList实例的浅拷贝。
    public Object clone() 
	   //返回一个包含ArrayList中所有元素的数组
    public Object[] toArray() {
	  //返回至指定索引的值
    public E get(int index) 
	 //将指定索引上的值替换为新值,并返回旧值
    public E set(int index, E element)   
	  //将指定的元素添加到此列表的尾部
    public boolean add(E e) 
	  // 将element添加到ArrayList的指定位置   
    public void add(int index, E element) {
        rangeCheckForAdd(index);
        ensureCapacityInternal(size + 1); 
       
        //从指定源数组中复制一个数组,复制从指定的位置开始,到目标数组的指定位置结束。
        //arraycopy(被复制的数组, 从第几个元素开始复制, 要复制到的数组, 从第几个元素开始粘贴, 一共需要复制的元素个数)
        //即在数组elementData从index位置开始,复制到index+1位置,共复制size-index个元素
        System.arraycopy(elementData, index, elementData, index + 1,size - index);
        elementData[index] = element;
        size++;
    }
     //删除ArrayList指定位置的元素  
    public E remove(int index)
	//移除ArrayList中首次出现的指定元素(如果存在)。
    public boolean remove(Object o) {
	 //快速删除指定位置的元素
    private void fastRemove(int index)
	  //清空ArrayList,将全部的元素设为null
    public void clear() 
	  //按照c的迭代器所返回的元素顺序,将c中的所有元素添加到此列表的尾部
    public boolean addAll(Collection<? extends E> c) {
	  //从指定位置index开始,将指定c中的所有元素插入到此列表中
    public boolean addAll(int index, Collection<? extends E> c) {
        rangeCheckForAdd(index);
        Object[] a = c.toArray();
        int numNew = a.length;
        ensureCapacityInternal(size + numNew);  // Increments modCount
        int numMoved = size - index;
        if (numMoved > 0)
            //先将ArrayList中从index开始的numMoved个元素移动到起始位置为index+numNew的后面去
            System.arraycopy(elementData, index, elementData, index + numNew, numMoved);
        //再将c中的numNew个元素复制到起始位置为index的存储空间中去
        System.arraycopy(a, 0, elementData, index, numNew);
        size += numNew;
        return numNew != 0;
    }
	 //删除fromIndex到toIndex之间的全部元素
    protected void removeRange(int fromIndex, int toIndex)
    {
        modCount++;
        //numMoved为删除索引后面的元素个数
        int numMoved = size - toIndex;  
        //将删除索引后面的元素复制到以fromIndex为起始位置的存储空间中去
        System.arraycopy(elementData, toIndex, elementData, fromIndex,numMoved);
        int newSize = size - (toIndex-fromIndex);
        //将ArrayList后面(toIndex-fromIndex)个元素置为null
        for (int i = newSize; i < size; i++)
        {
            elementData[i] = null;
        }
        size = newSize;
    }
	 //检查索引是否越界
    private void rangeCheck(int index)
    {
	 //删除ArrayList中包含在c中的元素
    public boolean removeAll(Collection<?> c)
    {
	//删除ArrayList中除包含在c中的元素,和removeAll相反
    public boolean retainAll(Collection<?> c) 
	  //将ArrayList的“容量,所有的元素值”都写入到输出流中 
    private void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException
    {
        int expectedModCount = modCount;
        s.defaultWriteObject();
        //写入数组大小
        s.writeInt(size);
        //写入所有数组的元素
        for (int i=0; i<size; i++) {
            s.writeObject(elementData[i]);
        }
        if (modCount != expectedModCount) {
            throw new ConcurrentModificationException();
        }
    }
	 //先将ArrayList的“大小”读出,然后将“所有的元素值”读出
    private void readObject(java.io.ObjectInputStream s)
	

HashMap源码解析:
https://blog.csdn.net/zxt0601/article/details/77413921#comments

https://blog.csdn.net/zxt0601/article/category/6697194

小结:
HashMap 是一个关联数组、哈希表,它是线程不安全的,允许key为null,value为null。遍历时无序。
其底层数据结构是数组称之为哈希桶,每个桶里面放的是链表,链表中的每个节点,就是哈希表中的每个元素。
在JDK8中,当链表长度达到8,会转化成红黑树,以提升它的查询、插入效率,它实现了Map<K,V>, Cloneable, Serializable接口。
因其底层哈希桶的数据结构是数组,所以也会涉及到扩容的问题。当HashMap的容量达到threshold域值时,就会触发扩容。扩容前和后,哈希桶的长度都是2的次方。 也因此,在根据key的hash值寻找对应的哈希桶时,可以用位运算替代取余操作,更加高效。
而key的hash值,并不仅仅只是key对象的hashCode()方法的返回值,还会经过扰动函数的扰动,以使hash值更加均衡。
因为hashCode()是int类型,取值范围是40多亿,只要哈希函数映射的比较均匀松散,碰撞几率是很小的。 但就算原本的hashCode()取得很好,每个key的hashCode()不同,但是由于HashMap的哈希桶的长度远比hash取值范围小,默认是16,所以当对hash值以桶的长度取余,以找到存放该key的桶的下标时,由于取余是通过与操作完成的,会忽略hash值的高位。因此只有hashCode()的低位参加运算,发生不同的hash值,但是得到的index相同的情况的几率会大大增加,这种情况称之为hash碰撞。 即,碰撞率会增大。
扰动函数就是为了解决hash碰撞的。它会综合hash值高位和低位的特征,并存放在低位,因此在与运算时,相当于高低位一起参与了运算,以减少hash碰撞的概率。(在JDK8之前,扰动函数会扰动四次,JDK8简化了这个操作)

扩容操作时,会new一个新的Node数组作为哈希桶,然后将原哈希表中的所有数据(Node节点)移动到新的哈希桶中,相当于对原哈希表中所有的数据重新做了一个put操作。所以性能消耗很大,可想而知,在哈希表的容量越大时,性能消耗越明显。
扩容时,如果发生过哈希碰撞,节点数小于8个。则要根据链表上每个节点的哈希值,依次放入新哈希桶对应下标位置。
因为扩容是容量翻倍,所以原链表上的每个节点,现在可能存放在原来的下标,即low位, 或者扩容后的下标,即high位。 high位= low位+原哈希桶容量
如果追加节点后,链表数量》=8,则转化为红黑树

由迭代器的实现可以看出,遍历HashMap时,顺序是按照哈希桶从低到高,链表从前往后,依次遍历的。属于无序集合。

HashMap的源码中,充斥个各种位运算代替常规运算的地方,以提升效率:

  • 与运算替代模运算。用 hash & (table.length-1) 替代 hash % (table.length)

  • 用if ((e.hash & oldCap) == 0)判断扩容后,节点e处于低区还是高区。

  • 运算尽量都用位运算代替,更高效。

  • 对于扩容导致需要新建数组存放更多元素时,除了要将老数组中的元素迁移过来,也记得将老数组中的引用置null,以便GC

  • 取下标 是用 哈希值 与运算 (桶的长度-1) i = (n - 1) & hash。 由于桶的长度是2的n次方,这么做其实是等于 一个模运算。但是效率更高

  • 扩容时,如果发生过哈希碰撞,节点数小于8个。则要根据链表上每个节点的哈希值,依次放入新哈希桶对应下标位置。

  • 因为扩容是容量翻倍,所以原链表上的每个节点,现在可能存放在原来的下标,即low位, 或者扩容后的下标,即high位。 high位= low位+原哈希桶容量

  • 利用哈希值 与运算 旧的容量 ,if ((e.hash & oldCap) == 0),可以得到哈希值去模后,是大于等于oldCap还是小于oldCap,等于0代表小于oldCap,应该存放在低位,否则存放在高位。这里又是一个利用位运算 代替常规运算的高效点

  • 如果追加节点后,链表数量》=8,则转化为红黑树

  • 插入节点操作时,有一些空实现的函数,用作LinkedHashMap重写使用。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值