ArrayList、HashSet,LinkedHashMap,LinkedHashSet源码阅读

image.png

Arraylist

ArraayList的结构和源码相对简单,是一个由数组组成的类,其查询快,增删慢,数据可重复,能保证数据的顺序性,线程不安全,初始化容量为10,扩容为1.5,不多BB直接看源码

Arraylist继承自AbstractList,

构造方法

这几Object数组就是arraylist的装载数据的媒介

    private static final Object[] EMPTY_ELEMENTDATA = {};
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
    transient Object[] elementData; //ArrayList实际的操作数组
//Arraylist默认的长度,如果数组超过10则发生第一次扩容
 private static final int DEFAULT_CAPACITY = 10;
//有参构造函数,返回指定大小数组的集合
  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);
        }
    }
//无参构造函数是返回一个空的数组集合,当add()会给数组容量赋为10
  public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }
//返回一个与c长度大小及元素一样的集合
 public ArrayList(Collection<? extends E> c) {
        elementData = c.toArray();//将参数集合转化为数组
        if ((size = elementData.length) != 0) {
            if (elementData.getClass() != Object[].class)
                elementData = Arrays.copyOf(elementData, size, Object[].class);
        } else {
            this.elementData = EMPTY_ELEMENTDATA;
        }
    }

常用核心方法

 public boolean add(E e) {
        ensureCapacityInternal(size + 1); //判断是否需要扩容
        elementData[size++] = e;//往数组中最后的一个元素末尾追加元素,size是记录了数组中的元素个数,而不是数组的长度
        return true;
    }

上图的ensureCapacityInternal(size + 1)的实现:

private void ensureCapacityInternal(int minCapacity) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            //DEFAULT_CAPACITY=10,这里是取两个值中较大的一个,如果elementData 是空数组,则取minCapacity =10
//这里获取DEFAULT_CAPACITY和minCapacity中最大的那个值
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        ensureExplicitCapacity(minCapacity);
    }
private void ensureExplicitCapacity(int minCapacity) {
//记录数组修改的次数
        modCount++;
//判断是否超过当前容量
        if (minCapacity - elementData.length > 0)
            //核心方法扩容机制
            grow(minCapacity);
    }
//刨根问底终于看到ArrayList最核心的方法
 private void grow(int minCapacity) {
//oldCapacity为当前数组的容量 
        int oldCapacity = elementData.length;
        //原来数组的容量扩大到1.5倍,如果数组是空的则newCapacity =0
        int newCapacity = oldCapacity + (oldCapacity >> 1);
   
//一般扩容的逻辑都在走这里,同时初始化时会用到,将minCapacity赋值给新容量变量newCapacity,如果不是初始化一般不进去这个if语句,
//add()方法数组添加数据,数组一般都是+1,oldCapacity+1不会比oldCapacity+0.5*oldCapacity 扩容一般是是按照15,22,33,49,73......以此类推扩容1.5倍下去
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        //如果容量扩大到1.5倍后 大于(Integer.MAX_VALUE -8)=2147483639则扩容至2147483639,这个情况一般没有,没有那么大的内存
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
          //扩容
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

get方法先检查索引范围是否超出实际现有数据的容量

 public E get(int index) {
        rangeCheck(index);
        return elementData(index);
    }
Arraylist的操作其实是对数组的操作,包括取元素,删元素基本是和数组一个特点,但是扩容操作,在添加的元素没有超过其容量时,增加元素不影响其性能,如果超过了其容量,则会对集合的数组进行扩容,性能因此受到影响,查询性能和数组一样非常高效

HashSet

底层是基于HashMap的,由于HashSet存储数据的方法调用的是HashMap的put方法,其存储的数据是存到HashMap的Key值中,key通过哈希函数可以算出HashMap的数组下标,所以相同key在HashMap对应的元素只有一个,而每一个类都有相应的hashCode方法,所以相同的类得到的哈希值是一样,所以hashSet的值是不重复的,hashSet无序性也是因为key通过哈希函数计算出HashMap的数组下标,由于是通过哈希计算,所以hashSet无法决定加入元素时的顺序性,但是却能保证这些元素是按照内部的顺序存储的,所以hashSet是一个无序的有序集合。。。
HashSet不重复性
从探究HashSet的不重复性,可以看到很多东西

    private transient HashMap<E,Object> map
//将PRESENT 作为hashmap的value存储,可以使hashmap每一个value值都指向这个PRESENT,所以节省了堆空间
    private static final Object PRESENT = new Object();
//从HashSet的构造函数我们看到HashSet,其实底层就是HashMap
    public HashSet() {
        map = new HashMap<>();
    }
//HashMap的add调用的是HashMap的put方法,并且是以存储的元素为key,PRESENT为value
 public boolean add(E e) {
        return map.put(e, PRESENT)==null;
    }

我们看看HashMap的put方法,其核心是putVal,我们先看看参数hash(key)

 public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
//这里调用了key这个对象的hashCode方法,如果两个对象相同,那么它们的hashCode值一定要相同
 static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
//这是HashMap最核心的方法
  final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
//这里是适用于HashMap首次put元素或者是容器被清空后,首次put table=null
        if ((tab = table) == null || (n = tab.length) == 0)
//这里的resize()在首次put时候会将table的容量设置为16,而哈希桶的扩容条件为0.75*16=12,
//哈希桶元素超过12则进行扩容,扩容为原来的两倍,下面会开始分析resize()具体源码
            n = (tab = resize()).length;
// 把元素存储到新的数组中,存储到数组的哪个位置需要根据hash值和数组长度来进行取模
//p为本次要插入的数组位置上的元素,判断是否发生了数组下标冲突,发生了冲突可能有两种情况:
//1、数组冲突,即key的通过hashcode和equals方法判断key值是同一个,则进行覆盖
//2、哈希冲突:即通过(n - 1) & hash计算在最大数组容量内(初始值是16)计算索引值,判断数组中此索引对应的数组位置是否被占用,然后在通过数组是否为树的结构,是则为哈希冲突
//【hash值  %   数组长度】   =    【  hash值   & (数组长度-1)】
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
//从这里我们看到了hashcode和equals方法,则判断key值是重复的,重复的则覆盖key在数组相应位置的v值
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
//如果哈希,判断数组p是否为树,为TreeNode的话,证明数组上的数据已经是红黑树结构了
            else if (p instanceof TreeNode)
//为树的结构,则按照红黑树的方式插入
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {//这里是判断了数组不为树而且哈希冲突的情况
//整个for的逻辑是转化为单链表的数组形式
                for (int binCount = 0; ; ++binCount) {
//链表p的下一个值e为null时将下个元素指向value
                    if ((e = p.next) == null) {
//将元素排成单链表
                        p.next = newNode(hash, key, value, null);
//TREEIFY_THRESHOLD 为8,可以得出结论,如果链表元素超过了8则转红黑树
                        if (binCount >= TREEIFY_THRESHOLD - 1) 
// 链表转化为红黑树
                            treeifyBin(tab, hash);
                        break;
                    }
//如果链表p的下一个值e和要插入的key是一摸一样的,则直接结束,等下面的步骤进行value值的覆盖
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
//将链表p指向下个不为空的值e,继续循环遍历
                    p = e;
                }
            }
//判断存在了重复的key值(依据hashcode和equals方法判断,上面已经判断过了),则进行v值的覆盖
            if (e != null) { 
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
//判断数组是否超过了加载因子*扩容前数组长度的值,超过则进行扩容,执行 resize()方法
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }
//我们看看这个扩容的核心方法
 final Node<K,V>[] resize() {
//获取旧的数组将其赋值到oldTab 
        Node<K,V>[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
        int newCap, newThr = 0;
        if (oldCap > 0) {//当旧的数组容量不为0时,也就是不是第一次执行put方法时
            if (oldCap >= MAXIMUM_CAPACITY) {
            // 原数组长度大于最大容量(1073741824) 则将threshold设为Integer.MAX_VALUE=2147483647
             // 接近MAXIMUM_CAPACITY的两倍
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
          //将新数组的容量设置为旧数组的两倍
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; //新阈值也设置为旧阈值的两倍
        }
        else if (oldThr > 0)
            newCap = oldThr; // 新阈值赋值给新数组容量
        else {   
 // 这里是初始化时调用的,hashmap初始化默认设置阈值为12,哈希桶为16,阈值=加载因子(默认0.75)*哈希桶容量(默认16)           
            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 
        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;
//将老数组赋值给e,并判断老数组是否为空
                if ((e = oldTab[j]) != null) {
//将老数组的引用置空
                    oldTab[j] = null;
// 如果元素没有有下一个节点,说明该元素不存在hash冲突
                    if (e.next == null)
  // 把元素存储到新的数组中,存储到数组的哪个位置需要根据hash值和数组长度来进行取模
  // 【hash值  %   数组长度】   =    【  hash值   & (数组长度-1)】
                        newTab[e.hash & (newCap - 1)] = e;
//如果数组为树,则构造红黑树
                    else if (e instanceof TreeNode)
                //构造红黑树
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    else {

                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        do {
                            next = e.next;
                            if ((e.hash & oldCap) == 0) {
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
                            else {
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }

LinkedHashMap

LinkedHashMap是继承了HashMap,其特点和hashMap差不多,其加载因子,扩容机制,初始化容量都是一样的,但是LinkedHashMap装载数据的节点Entry是在hashMap的Node节点的基础上增加了before, after用于指向前后添加的节点,保证LinkedHashMap的顺序性

LinkedHashMap的节点类

 static class Entry<K,V> extends HashMap.Node<K,V> {
        Entry<K,V> before, after;
        Entry(int hash, K key, V value, Node<K,V> next) {
            super(hash, key, value, next);
        }
    }

LinkedHashMap并没有重写继承HashMap的put方法,只是重写了put方法中的putVal方法调用的newNode方法,newNode方法保证了插入的顺序性

/**
 * 使用LinkedHashMap中内部类Entry
 */
Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
    LinkedHashMap.Entry<K,V> p = new LinkedHashMap.Entry<K,V>(hash, key, value, e);
    linkNodeLast(p);
    return p;
}
/**
 * 将新创建的节点p作为尾结点tail,
 * 当然如果存储的第一个节点,那么它即是head节点,也是tail节点,此时节点p的before和after都为null
 * 否则,建立与上一次尾结点的链表关系,将当前尾节点p的前一个节点(before)设置为上一次的尾结点last,
 * 将上一次尾节点last的后一个节点(after)设置为当前尾结点p
 * 通过此方法实现了双向链表功能,完成before,after,head,tail的值设置
 */
private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
    LinkedHashMap.Entry<K,V> last = tail;
    tail = p;
    if (last == null)
        head = p;
    else {
        p.before = last;
        last.after = p;
    }
}

LinkedHashSet

LinkedHashSet继承了HashSet,底层的结构是使用的LinkedHashMap的结构,其

//LinkedHashSet 的构造函数,super里是初始化LinkedHashSet
  public LinkedHashSet() {
        super(16, .75f, true);
    }

super的方法是HashSet的有参构造,返回的是LinkedHashMap

 HashSet(int initialCapacity, float loadFactor, boolean dummy) {
        map = new LinkedHashMap<>(initialCapacity, loadFactor);
    }

但是LinkedHashSet使用的add方法是使用从HashSet继承过来的方法,但是这个put方法使用的LinkedHashMap的put方法,由上面LinkedHashMap的分析我们知道LinkedHashMap的put方法是使用父类hashMap的,hashMap的put方法中调用了putVal方法,putVal方法调用了newNode方法,LinkedHashMap只是重写了newNode方法保证了其插入有序性,LinkedHashSet调用put方法是value值设置为了PRESENT,PRESENT是一个object对象,在hasmap源码中定义put(e, PRESENT)中的key值相等,并且hash相等就会将其覆盖,而只要key相等那么hash值必然是相等的,hashMap中是会将其进行覆盖,保证了唯一性

 public boolean add(E e) {
        return map.put(e, PRESENT)==null;
    }
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,076评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,658评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,732评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,493评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,591评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,598评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,601评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,348评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,797评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,114评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,278评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,953评论 5 339
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,585评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,202评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,442评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,180评论 2 367
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,139评论 2 352