java集合

一、集合体系图

在这里插入图片描述

二、ArrayList

ArrayList

ArrayList 是由数组实现的,ArrayList是有序的,可以插入重复数据的,ArrayList可以插入多个null,ArrayList是线程不安全的
ArrayList底层维护的是一个Object数组elementData

ArrayList扩容规则:

使用无参构造初始化,初始长度为0;
使用带有int类型参数构造初始化,初始长度为int参数值;
使用Collection类型参数进行初始化,初始长度为Collection.size();
1、add方法扩容规则

无参构造创建ArrayList后扩容规则:
当数组元素超过现有长度最大值,进行第一次扩容,原有长度+10;
以后每次扩容长度(length)为,length右移一位+length,即length*1.5后向下取整;
初始长度为0;那么经过10次扩容后的长度分别为:
0---》【10,15,22,33,49,73,109,163,244,366
带参构造创建ArrayList后扩容规则:
当数组元素超过现有长度最大值,扩容长度(length)为,length右移一位+length,即length*1.5后向下取整;

ArrayList扩容源码
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

最后一层一层的返回执行elementData[size++]=e,进行赋值

2、addAll方法扩容规则
当数组中没有时,扩容的长度为(10和实际元素个数的较大值);
有元素时扩容的长度(length)为,length右移一位+length

三、Vector

在这里插入图片描述
Vector也是由数组实现的,是线程安全的,效率低
通过无参构造创建vector默认初始10个容量,以后每次进行2倍扩容
通过带参构造创建vector每次进行2倍扩容

下面了解一下vector的构造器

	//这个就是无参构造,通过构造器一层一层的调用最后会生成一个初始10容量的数组
	public Vector() {
        this(10);
    }
	//带一个参数的构造,传进来的参数就是数组的初始容量
    public Vector(int initialCapacity) {
        this(initialCapacity, 0);
    }

前两个都还好吧,主要看一下这个构造器:

    //这个构造器的第一个参数,还是和数组容量相关,不多赘述
    //第二个参数,以后每次增加多少个容量
    public Vector(int initialCapacity, int capacityIncrement) {
        super();
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal Capacity: "+
                    initialCapacity);
        this.elementData = new Object[initialCapacity];
        this.capacityIncrement = capacityIncrement;
    }

在这里插入图片描述

其余的源码和ArrayList相似度99%,只有扩容机制略有不同

四、LinkedList

底层是双向链表,可以添加任意元素包括null,元素可以重复,
线程不安全

LinkedList源码解读:

先看下成员属性,和内部类信息

public class LinkedList<E> {
	//容量
    transient int size = 0;
	//头节点
    transient java.util.LinkedList.Node<E> first;
	//尾节点
    transient java.util.LinkedList.Node<E> last;
	//每个节点的信息
    private static class Node<E> {
    	//数据内容
        E item;
        //当前节点的后一个节点
        LinkedList.Node<E> next;
        //当前节点的前一个节点
        LinkedList.Node<E> prev;
	
        Node(LinkedList.Node<E> prev, E element, LinkedList.Node<E> next) {
            this.item = element;
            this.next = next;
            this.prev = prev;
        }
    }
}

构造器
在这里插入图片描述

当LinkedList执行crud操作时,其实就是对Node中的信息进行操作
add方法:
在这里插入图片描述
remove(index);通过索引删除操作
在这里插入图片描述

E unlink(Node<E> x) {
        // assert x != null;
        final E element = x.item;
        final Node<E> next = x.next;
        final Node<E> prev = x.prev;

        if (prev == null) {
            first = next;
        } else {
            prev.next = next;
            x.prev = null;
        }

        if (next == null) {
            last = prev;
        } else {
            next.prev = prev;
            x.next = null;
        }

        x.item = null;
        size--;
        modCount++;
        return element;
    }

在这里插入图片描述
方法太多了,不一个一个写了,搞清双向链表结构,在看源码so easy!!!

五、LinkedList vs ArrayList 性能比较

ArrayList:
1、基于数组,需要连续内存
2、查询速度快
3、尾部插入、尾部删除性能可以,其他部分插入删除速度慢(涉及到数据移动所以慢)
4、可以利用cpu缓存,局部性原理

LinkedList:
1、基于双向链表,无需连续内存
2、查询速度慢(要沿着链表进行遍历)
3、头尾插入、删除性能高
4、占用内存多

六、HashSet

HashSet实现了Set接口,HashSet底层实际上是HashMap,元素的值作为map的key,创建一个虚拟的new Object对象作为map的value,进行存储
在这里插入图片描述

可以存放null值,HashSet元素顺序根据Hash值决定,不能有重复的元素

构造器:

//        构造一个新的空集;支持的HashMap实例具有默认的初始容量 (16) 和加载因子 (0.75)。
        public HashSet() {
            map = new HashMap<>();
        }
//        构造一个包含指定集合中元素的新集合。 HashMap是使用默认加载因子 (0.75) 和足以包含指定集合中的元素的初始容量创建的。
//        参形:
//        c – 其元素要放入此集合的集合
//        抛出:
//        NullPointerException – 如果指定的集合为空
        public HashSet(Collection<? extends E> c) {
            map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));
            addAll(c);
        }
//        构造一个新的空集;后备HashMap实例具有指定的初始容量和指定的负载因子。
//        参形:
//        initialCapacity - 哈希映射的初始容量 
//        loadFactor - 哈希映射的负载因子
//        抛出:
//        IllegalArgumentException – 如果初始容量小于零,或者负载因子为非正数
        public HashSet(int initialCapacity, float loadFactor) {
            map = new HashMap<>(initialCapacity, loadFactor);
        }
//        构造一个新的空集;支持HashMap实例具有指定的初始容量和默认加载因子 (0.75)。
//        参形:
//        initialCapacity - 哈希表的初始容量
//        抛出:
//        IllegalArgumentException – 如果初始容量小于零
        public HashSet(int initialCapacity) {
            map = new HashMap<>(initialCapacity);
        }
//        构造一个新的空链接哈希集。 (此包私有构造函数仅由 LinkedHashSet 使用。)支持 HashMap 实例是具有指定初始容量和指定负载因子的 LinkedHashMap。
//        参形:
//        initialCapacity - 哈希映射的初始容量 
//        loadFactor - 哈希映射的负载因子 
//        dummy – 忽略(将此构造函数与其他 int、float 构造函数区分开来。)
//        抛出:
//        IllegalArgumentException – 如果初始容量小于零,或者负载因子为非正数
        HashSet(int initialCapacity, float loadFactor, boolean dummy) {
            map = new LinkedHashMap<>(initialCapacity, loadFactor);
        }

请参考hashmap!!!

七、TreeSet

底层有TreeMap实现,可以通过构造器传入一个比较器,实现有序

八、LinkenHashSet

底层有LinkedHashMap实现,底层维护的是一个数组+双向链表

九、HashMap

在这里插入图片描述

1、数据的特点

Map中的key不允许重复,value可以重复。
key可以为null,当时只能有一个。
value也可以为null,可以有多个。

2、底层数据结构,1.7/1.8有什么不同?

1.7:数组+链表
1.8:数组+(链表/红黑树)

HashMap中的静态属性:

 		//默认初始容量 - 必须是 2 的幂。
        static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
        //最大容量。必须是 2 <= 1<<30 的幂
        static final int MAXIMUM_CAPACITY = 1 << 30;
        //负载因子,当数组容量达到指定负载因子倍数的时候,会进行扩容
        // 例16 * 0.75 = 12 16容量的数组put第13个元素的时候会进行扩容
        static final float DEFAULT_LOAD_FACTOR = 0.75f;
        //树化阈值(条件一):
        static final int TREEIFY_THRESHOLD = 8;
        //树化阈值(条件二):
        //这两个条件同时满足,链表转换红黑树
        static final int MIN_TREEIFY_CAPACITY = 64;
        //反树化阈值(条件之一):达到该值,红黑树转换链表
        static final int UNTREEIFY_THRESHOLD = 6;

HashMap中的成员属性:

		//HashMap中的数组部分,用于存放链表node节点
        transient HashMap.Node<K,V>[] table;
        //保存缓存的 entrySet()。
        transient Set<Map.Entry<K,V>> entrySet;
        //键值映射的数量。
        transient int size;
        //记录HashMap的修改次数
        transient int modCount;
        //扩容的阈值(当前容量 * 负载因子)
        int threshold;
        //负载因子。
        final float loadFactor;

hashMap:数组+链表转换数组+红黑树图示。
条件1:当某一桶内元素个数超过阈值8,并且数组长度小于64时,优先进行扩容;
条件2:当扩容后的长度达到64,并且某一桶内的元素个数仍然大于8的时候,进行转换
在这里插入图片描述

在这里插入图片描述

3、为什么要用红黑树,为什么不直接用红黑树,树化阈值为什么是8,什么时候树化,什么时候退化为链表?

为什么要用红黑树:当链表过长的时候影响hashmap性能,进行引用红黑树;

为什么不直接使用红黑树:当链表短的时候性能基本等价于红黑树,并且红黑树更占用内存,所以没必要;

为什么树化阈值为8:红黑树主要用来防止dos攻击,hash值如果足够随机,在负载因子0.75(元素个数与数组长度的比例大于负载因子时,进行扩容)的情况下,长度超过8的链表出现概率极低,选择8是为了树化几率更小

何时会树化:链表长度大于8,数组长度大于64的时候树化

何时退化为链表:
1、在扩容后进行树拆分时,树元素个数<=6则会退化为链表
2、remove树节点时,若root、root.left、root.right、root.left.left有一个为null,也会退化成链表

在这里插入图片描述

4、HashMap源码跟进

Map map = new HashMap();
map.put(“key”,“value”);源码跟进
注:源码中的代码太长了,我把执行该方法走过的代码留下其余全删掉了,简单记录了一下执行流程!!!

在这里插入图片描述

//判断当前数组是否为空,数组长度是否为0,如果某一条件满足,执行resize()方法
//resize()方法,就是一个扩容的方法
if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
    final HashMap.Node<K,V>[] resize() {
//        将table对象赋给局部变量 oldTab,当前为NULL;
        HashMap.Node<K,V>[] oldTab = table;
//        将当前键值对的数量赋值给oldCap局部变量,当前为0
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
//        将上一次的扩容阈值赋给oldThr局部变量,这个值用于判断是否扩容的标准
//        当前值为0
        int oldThr = threshold;
        int newCap, newThr = 0;
//        false
        if (oldCap > 0) {}
//        false
        else if (oldThr > 0){}
        //此处作者给了一段注释,意思前两个条件不满足,代表就是第一次进行容量初始化默认值为16
        else {               // zero initial threshold signifies using defaults
//          将默认数组初始容量值赋给newCap,newCap=16;
            newCap = DEFAULT_INITIAL_CAPACITY;
//          设置扩容阈值,newThr=12;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
//        将扩容阈值,交给成员变量保存
        threshold = newThr;
        @SuppressWarnings({"rawtypes","unchecked"})
//        重新创建了一个长度为为16的数组,上边给newCap赋了一个初始容量的值
        HashMap.Node<K,V>[] newTab = (HashMap.Node<K,V>[])new HashMap.Node[newCap];
//        将创建好的新数组交给成员变量table保存
        table = newTab;
        //当前方法执行完毕后
        //成员变量threshold(扩容的阈值)=12,
        //table = new HashMap.Node[16];
        return newTab;//返回newTab是一个16容量的空数组
    }
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        HashMap.Node<K,V>[] tab; HashMap.Node<K,V> p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        //上边if执行完毕后,tab = 16容量的空数组,p=null,n=16,i=0;
        //(n - 1) & hash 执行一个n-1后的值 和 hash 值的一个按位与运算重新得到一个hash值(也就是对应的桶下标),赋值给i
        //tab[对应的桶下标]赋值给p
        //如果p为null,代表当前位置没有数据,创建一个node节点,并添加到当前位置
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        //集合操作次数+1
        ++modCount;
        //添加完元素后进行一个 ++size 操作
        // 在判断当前键值数量是否超过12(上一次执行resize()方法,保存的成员变量threshold值为12,忘记了请看上一张图)
        // 如果超过12,执行resize()方法,在进行扩容操作
        if (++size > threshold)
            resize();
        // Callbacks to allow LinkedHashMap post-actions
//        void afterNodeAccess(Node<K,V> p) { }
//        void afterNodeInsertion(boolean evict) { }
//        void afterNodeRemoval(Node<K,V> p) { }
        //这三个方法是HashMap留给LinkedHashMap用的,所以没有对其进行任何操作处理
        afterNodeInsertion(evict);
//        当该方法执行完毕后,记录一下成员变量的值
//        threshold=12;
//        table = new HashMap.Node[16];
//        modCunt = 1;
//        size=1;
        return null;

    }

至此,第一次的put操作源码结束


执行第二次put先添加一个不同的键值对,看下源码的执行流程
Map map = new HashMap();
map.put(“key”,“value”);
map.put(“2”,“2”);
System.out.println(map);

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        HashMap.Node<K,V>[] tab; HashMap.Node<K,V> p; int n, i;
        //走到这个if判断的时候,还是对key的值根据算法进行一个桶下标的计算
        //计算完以后,如果p的值等于null,证明当前桶内没有元素,执行一次添加元素的操作
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);//走完if以后else当然不进去直接到底下
        ++modCount;
        //这块注意了哈,虽然if判断的结果为false,但是有个++size别忘了
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
//        再记录一下成员变量的值
//        threshold=12;
//        table = new HashMap.Node[16];
//        modCunt = 2;
//        size=2;
        return null;
    }


执行第三次put,添加一个相同的键值对
Map map = new HashMap();
map.put(“key”,“value”);
map.put(“2”,“2”);
map.put(“2”,“3”);
System.out.println(map);

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        HashMap.Node<K, V>[] tab;
        HashMap.Node<K, V> p;
        int n, i;
        //这一步,比较简单,就是判断一下当前的数组是不是null,数组里边键值对数量 是否等于0
        //除了第一次可以直接跳过这步
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        //当再次执行这个if判断时,这次计算出来的p=tab[桶下标]里边明显是有值的就是上一次添加进来的2-2那个键值对
        //所以这次不为null跳过这步,执行else
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            //先记录一下当前的局部变量的值
            /*
                tab 为 16长度的Node类型数组
                p  为上次添加进来的2-2键值对
                n=16,i=2
             */
            HashMap.Node<K, V> e;
            K k;
            //条件1、计算一下p的hash值,是否等于方法参数传进来的hash值
            /*
                说明一下p的hash值是怎么来的呢,是执行
                if ((p = tab[i = (n - 1) & hash]) == null)
                tab[i] = newNode(hash, key, value, null);
                这段代码,通过newNode传进去的,Node为HashMap的局部内部类,newNode为HashMap的成员方法
                可以通过ctrl+鼠标左键点进去看一下
            */
            //条件2、
            //这段代码要注意括号哈,这里会优先进行p.key 赋值 k的操作,后面才能进行key.equals(k)的比较
            //这段if主要用于判断后来put这个值的key和hash值是否等于现在桶内已有元素的key和hash值,下边会详细说明一下这个判断
            if (p.hash == hash &&
                    ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            //当然if结束后不走这段哈可以跳过,这里简单说明一下这段代码
            //这里就是判断一下当前这个p是否是TreeNode类型,TreeNode英文翻译一下为树节点
                // 也就是判断一下当前桶内元素数据类型是否为红黑树,如果是,执行红黑树对应的方法
            else if (p instanceof HashMap.TreeNode)
                e = ((HashMap.TreeNode<K, V>) p).putTreeVal(this, tab, hash, key, value);
            //如果key和hash值都不同,也不是树节点,说明当前桶内数据依然是链表形式
            else {//当然这个也可以跳过

                for (int binCount = 0; ; ++binCount) {
                    //这个p = tab[i = (n - 1) & hash],p是从这来的哈
                    //首先判断一下p后边是否还有元素,如果没有p后边没有元素了,进去
                    if ((e = p.next) == null) {
                        //把方法传进来这个新的键值对参数利用上,添加到p的后边
                        p.next = newNode(hash, key, value, null);
                        //这里相当于一个循环的退出条件
                        //当把新元素添加到链表后,立即判断原来的链表长度是否大于等于7
                        //因为树化的阈值为8,新添加的元素不在binCOunt的记数范围内所以判断条件为7
                        //如果条件满足,说明满足树化的第一个条件,执行对应方法再进行第二个条件的判断是否树化,并退出循环
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    //这里依然是一个key和hash的判断,这里e的值,是p.next给的
                    //也就是说这里的循环是在遍历这个链表
                    //看一下链表上的每一个元素的key和hash是否等于要put的这个值得key和hash
                    //如果有一个是相同的,则退出循环
                    if (e.hash == hash &&
                            ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    //如果都没有条件满足e赋值给p退出else
                    p = e;
                }
            }
            //最后这段代码,其实就是将当前put传进来的value值做一个返回
            //他这代码里边将方法参数的value传递给了一个方法,供linkedList使用
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                //这个onlyIfAbsent是参数传进来的值,默认为false取反则为true 第二条件不做判断
                if (!onlyIfAbsent || oldValue == null)
                    //注当前的e,是上边的p给的值
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }
//对这段代码做一个简单的说明
//这段代码用于判断put进来的元素和当前的桶内元素做一个比较
/*
	这里比较的两个元素:一个是p,不做再多的解释了回顾一下源码的p是怎么来的
					另一个就是咱们putVal方法传进来的一系列的参数值
	首先比较p的hash和方法参数的hash是否相等,因为咱们传的key都是2所以当前的hash值是相等的p.hash == hash --- true
	然后再判断key是否相等,和hash同理(k = p.key) == key --- true
	这里最最重要的是这个equals方法,这个方法的返回值取决于传入的参数类型key的类型
	如果说参数key是一个String类型,那么是比较两个字符串的值是否相等
	如果是其他类型(比如自己写的类,作为key)那么你就要重写equals方法,来自定义这个比较,进行返回
	如果没有重写,默认调用object的equals比较的是两个对象的地址
	当然他这里用的是 短路或 的操作如果 (k = p.key) == key 已然为true 后面不做判断
*/
if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;

hashmap先写到这把,以后有精力会对数组扩容和红黑树的部分进行补充

十、HashTable

特点:
1、键和值不能为null,否则抛空指针
2、HashTable是线程安全的,HashMap是线程不安全的
3、使用方法基本和HashMap相同
4、底层有数组HashTable$Entry[],初始化大小11
5、扩容值为2倍+1
6、负载因子 0.75
7、效率比HashMap低

J、Properties

1、继承HashTable实现Map接口,
2、使用特点和HashTable类似
3、该类通常用于加载 以properties结尾的文件(文件内容以键值对形式存储)

Q、TreeMap

TreeMap 是一个有序的key-value集合,它是通过红黑树实现的。
TreeMap基于红黑树实现。根据其键的自然顺序排序,或者由创建时提供的Comparator比较器排序,具体取决于使用的构造函数。
TreeMap的基本操作 containsKey、get、put 和 remove 的时间复杂度是 log(n) 。
另外,TreeMap是非同步的。 它的iterator 方法返回的迭代器是fail-fastl的。
TreeMap不是线程安全的


通过Comparator构造TreeMap,会将第一个put进来的元素作为root保存

Entry<K,V> t = root;
if (t == null) {
        compare(key, key); // type (and possibly null) check

        root = new Entry<>(key, value, null);
        size = 1;
        modCount++;
}

以后每次put根据比较器的排序规则选择left或right节点保存

		int cmp;
        Entry<K,V> parent;
        // split comparator and comparable paths
        Comparator<? super K> cpr = comparator;
        //有Comparator执行的代码
        if (cpr != null) {
            do {//遍历所有的key,给当前key找到合适位置
                parent = t;
                cmp = cpr.compare(key, t.key);//动态绑定到我们实现的Comparator
                if (cmp < 0)
                    t = t.left;
                else if (cmp > 0)
                    t = t.right;
                //如果遍历过程中,发现准备添加key 和当前已有的key相等(由我们自己写的Comparator实现方式来决定),就不添加
                else
                    return t.setValue(value);
            } while (t != null);
        }
        //没有Comparator执行的代码
        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)
                    t = t.left;
                else if (cmp > 0)
                    t = t.right;
                else
                    return t.setValue(value);
            } while (t != null);
        }
        Entry<K,V> e = new Entry<>(key, value, parent);
        if (cmp < 0)
            parent.left = e;
        else
            parent.right = e;
        fixAfterInsertion(e);
        size++;
        modCount++;
        return null;

K、LinkedHashMap

不想自己画了,从网上借了一个图
在这里插入图片描述

Map接口的哈希表和链表实现,具有可预测的迭代顺序。此实现与HashMap的不同之处在于它维护一个双向链表,该列表贯穿其所有条目。这个链表定义了迭代顺序,通常是键插入映射的顺序(插入顺序)。请注意,如果将键重新插入,则插入顺序不会受到影响。 (如果在调用m.containsKey(k)将在调用之前立即返回true时调用m.put(k, v),则将键k重新插入到映射m中。)

王、Iterator 迭代机制

1、FailFast

不允许遍历的同时进行修改,如果修改抛出并发修改异常;

2、FailSafe

允许遍历的同时进行修改,牺牲一致性完成遍历

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值