JAVA总结作业----集合

List列表
    List接口Collection接口的一个子接口,List集合类中元素有序、且可重复,集合中的每个元素都有其对应的顺序索引
    List的实现类有ArrayList、LinkedList、Vector

Vector
    Vactor是基于数组实现,同ArrayList相比Vactor的线程相对安全,他对自己的所有方法都加上了synchronized安全锁,所有效率要远低于ArrayList。还有在扩容上,ArrayList是扩容自己的1.5倍,而Vector会扩容到自己的两倍。

源码分析:
 

package java.util;

public class Vector<E>
    extends AbstractList<E>
    implements List<E>, RandomAccess, Cloneable, java.io.Serializable {

    //定义数组,存放元素
    protected Object[] elementData;

    //已经放入数组的元素数量
    protected int elementCount;

    //增长的系数
    protected int capacityIncrement;

    //可序列化版本号
    private static final long serialVersionUID = -2767605614048989439L;

    //构造方法,提供初始大小,和增长系数
    public Vector(int initialCapacity, int capacityIncrement) {
        super();
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        this.elementData = new Object[initialCapacity];
        this.capacityIncrement = capacityIncrement;
    }

    //构造方法,提供初始大小,增长系数为零
    public Vector(int initialCapacity) {
        this(initialCapacity, 0);
    }

    //无参构造方法
    public Vector() {
        this(10);
    }

    //构造方法,将指定的集合元素转化为Vector
    public Vector(Collection<? extends E> c) {
        elementData = c.toArray();
        elementCount = elementData.length;
        // c.toArray might (incorrectly) not return Object[] (see 6260652)
        //判断c.toArray是否是Object[]类型
        if (elementData.getClass() != Object[].class)
            elementData = Arrays.copyOf(elementData, elementCount, Object[].class);
    }

    //将elementData中的元素全部拷贝到anArray数组中
    public synchronized void copyInto(Object[] anArray) {
        System.arraycopy(elementData, 0, anArray, 0, elementCount);
    }

    //将数组长度设置为等于vector的个数
    public synchronized void trimToSize() {
        modCount++;
        int oldCapacity = elementData.length;
        if (elementCount < oldCapacity) {
            elementData = Arrays.copyOf(elementData, elementCount);
        }
    }

    //扩充容量
    public synchronized void ensureCapacity(int minCapacity) {
        if (minCapacity > 0) {
            modCount++;
            ensureCapacityHelper(minCapacity);
        }
    }

    //扩充容量帮助函数
    private void ensureCapacityHelper(int minCapacity) {
        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }

    //最大容量
    private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
    //扩充容量执行方法
    private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
        //根据capacityIncrement进行判断,capacityIncrement> 0 增加capacityIncrement个容量,否则容量扩充当前容量的一倍
        int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
                                         capacityIncrement : oldCapacity);
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        //扩容操作,生成已给新的数组,容量为newCapacity,并将elementData中的元素全部拷贝到新数组中,并将新生成的数组在赋值给elementData 
        elementData = Arrays.copyOf(elementData, newCapacity);
    }
  }

为什么尽量少使用Vector
    Vector和ArrayList的底层实现方式非常的相近,官方说少使用Vector,还是尽量少使用,通过源码可以看出,每个方法都添加了synchronized关键字来保证同步,所以它是线程安全的,但是正是这些方法的同步,让其效率大大的降低了。比ArrayList的效率要慢。

ArrayList

    ArrayList是实现List的可扩容数组(动态数组)继承自 AbstractList,底层基于数组实现容量大小动态变化。

    允许 null 的存在。 同时还实现了 RandomAccess、Cloneable、Serializable 接口,所以ArrayList 是支持快速访问、复制、序列化的。查询快,增删慢,线程不安全,效率高。

ArrayList的遍历操作
ArrayList有三种(四种)遍历方式,分别是:

for循环遍历
增强for循环(foreach)
Iterator迭代器(for、where)
第一种: 因为ArrayList底层是数组,实现了RandomAccess,所以可以通过索引访问到具体的元素。

ArrayList的构造方法有三种,分别是:

第一个构造方法用来返回一个初始容量为10的数组(add之后),第二个用来生成一个带数据的ArrayList,第三个构造方法就是自定义初始容量。

    DEFAULTCAPACITY_EMPTY_ELEMENTDATA这个常量在ArrayList源码中表示的是一个空数组,而elementData就是ArrayList实际存储数据的容器。由此可知,ArrayList在调用无参构造方法时创建的是一个长度为0的空数组,当调用add()方法添加元素时,ArrayList才会触发扩容机制:

可以看到,add的第一行就调用了ensureCapacityInternal方法,并把size+1的值传了进去,继续看 ensureCapacityInternal()

初始elementData就是一个空数组,条件成立,它会从DEFAULT_CAPACITY和minCapacity中选择一个最大值返回,其中DEFAULT_CAPACITY表示默认的初始容量,它的值为10。calculateCapacity()方法将返回10,之后调用ensureExplicitCapacity()方法:

此时minCapacity的值为10,elementData.length的值为0,条件成立,执行grow()方法

这一步就是扩容的核心操作。先将旧容量右移1位,再加上旧容量就得到了新容量,正数右移1位相当于除以2,在该基础上加旧容量,则等价于新容量 = 旧容量 * 1.5,所以才有ArrayList每次扩容为旧容量的1.5倍的说法。

LinkedList
    LinkedList是一个双向链表,可以存储任何元素(包括null), LinkedList底层的链表结构使它支持高效的插入和删除操作,但是要查询的话只能遍历查询,因此查询的效率低下。双向链表的每个节点用内部类Node表示。LinkedList通过first和last引用分别指向链表的第一个和最后一个元素。
    LinkedList同时实现了List接口和Deque对口,也就是收它既可以看作一个顺序容器,又可以看作一个队列(Queue),同时又可以看作一个栈(stack)。

ArrayList和LinkedList的区别
ArrayList 的底层是顺序表(数组),LinkedList 底层是链表
对于随机访问get和set,ArrayList要优于LinkedList,因为LinkedList要移动指针;
ArrayList实现了RandomAccess接口,所以ArrayList用for循环遍历的速度要比迭代器快。
在查找、删除元素的时候,如果元素基数大,ArrayList的效率要比LinkedList快一点,这取决于二者之间的实现方式。
    ArrayList想要get(int index)元素时,直接返回index位置上的元素,而LinkedList需要通过for循环进行查找,虽然LinkedList已经在查找方法上做了优化,比如index < size / 2,则从左边开始查找,反之从右边开始查找,但是还是比ArrayList要慢。ArrayList想要在指定位置插入或删除元素时,主要耗时的是System.arraycopy动作,会移动index后面所有的元素;LinkedList主耗时的是要先通过for循环找到index,然后直接插入或删除。

Set集合
    set作为Collection接口的子接口,他与list同级,set存储无序元素且不能重复,实现类主要是HashSet、LinkedHashSet和TreeSet。

HashSet
    HashSet和LinkedHashSet底层数据结构是哈希表。hashSet不是同步的,如果多个线程同时访问一个Set,只要有一个线程修改了Set中的值,就必须进行同步处理,通常通过同步封装这个Set对象来完成同步,如果不存在这样的对象,可以使用Collections.synchronizedSet()方法完成。

Set集合取出元素的方式可以采用:迭代器、增强for、流遍历。

    HashSet是根据对象的哈希值来确定元素在集合中的存储位置,因此具有良好的存取和查找性能。保证元素唯一性的方式依赖于:hashCode与equals方法。

哈希表底层结构
    在JDK1.8之前,哈希表底层采用 数组+链表 ,所以使用链表解决哈希冲突,同一hash值的链表都存储在一个链表里。但是当位于一个桶中的元素较多,即hash值相等的元素较多时,通过key值依次查找的效率较低。
    在JDK1.8之后,哈希表的底层结构是 数组+链表+红黑树 。当链表长度超过阈值(8)时,将链表转换为红黑树,这样大大减少了查找时间。

Hash的存储原理
调用对象的 hashCode() 方法,获得要存储元素的哈希值。
将哈希值与表的长度(即数组的长度)进行求余运算得到一个整数值,该值就是新元素要存放的位置(即是索引值)。
遍历该位置上的所有旧元素,依次比较每个旧元素的哈希值和新元素的哈希值是否相同。
比较新元素和旧元素的地址是否相同。如果地址值相同则用新的元素替换老的元素,停止比较。如果地址值不同,则新元素调用equals方法与旧元素比较内容是否相同。
说明没有重复,则将新元素存放到该位置上并让新元素记住之前该位置的元素。
如何保证元素的唯一性
HashSet调用add()方法添加元素时其实底层是调用了HashMap的put()方法,元素唯一性实现的方法还是有HashMap来实现,因为HashMap的key值也不能相同。其本质还是与哈希表的存储原理一致。

LinkedHashSet
    LinkedHashSet是HashSet的子类,继承了set集合的所有功能和特点,底层维护了一个哈希表+双向链表,同时把其中的重要特点给修改,把无序变为有序。

特点:

LinkedHashSet是具有可预知迭代顺序的Set接口的哈希表和链接列表实现。此实现与HashSet的不同之处在于,后者维护着一个运行于所有条目的双重链接列表。
LinkedHashSet底层使用LinkedHashMap来保存所有元素,它继承与HashSet,其所有的方法操作上又与HashSet相同。
LinkedHashSet也是非线程安全的。可以存储null。
LinkedHashSet根据元素的hashCode值来决定元素的存储位置,同时使用链表维护元素的次序,所以他是有序的。
 

Map
    Map,与Collection接口平级,用于保存具有映射关系的数据,Map集合里保存着两组值,一组用于保存Map的key,另一组保存着Map的value。主要实现类有HashMap、HashTable、TreeMap。

HashMap
HashMap的底层数据结构+为什么?
    HashMap的底层数据结构在JDK8之前是由数组+链表组成的,而在JDK8之后,由数组+链表+红黑树组成。

     那为什么要改成数组+链表+红黑树呢? 是因为要提升解决hash冲突时(list只能从next不断访问,所有list过长导致性能低下)的查找性能,使用链表的时间复杂度为O(n),而红黑树则为O(log n)。

     那什么时候用链表,什么时候用红黑树呢? 如果插入元素,默认情况下使用的是链表节点。当同一个索引位置的节点在大于8的时候,链表会转为红黑树。阈值为8。 而对于移除,当索引位置的节点小于6的时候,则会吧红黑树再转为链表。

     那为什么不直接用红黑树而是要和链表相互转换呢? 因为我们在设计方案的时候,要考虑到时间和空间,当然HashMap也不例外,阈值为8这是时间和空间权衡后的结果。在时间的消耗上红黑树的时间复杂度是O(log n),要比链表的O(n)快,但是在存储元素的空间上,红黑树需要进行左旋和右旋 而单链表不需要,所以在空间上TreeNodes是链表Nodes的两倍。 当存储的元素过少时,红黑树查找性能的优势并不明显,付出两倍的空间有点得不偿失。

     那为什么阈值要设置成8? 如果 hashCode 分布良好,也就是 hash 计算的结果离散好的话,那么红黑树这种形式是很少会被用到的,因为各个值都均匀分布,很少出现链表很长的情况。在理想情况下,链表长度符合泊松分布,各个长度的命中概率依次递减,当长度为 8 的时候,概率仅为 0.00000006(6E-8)。 这个概率足够低了,并且到8个节点时,红黑树的性能优势也会开始展现出来,因此8是一个较合理的数字。

     那为什么转回链表节点的阈值是6而不是8 如果我们设置节点多于8个转红黑树,少于8个就马上转链表,当节点个数在8徘徊时,就会频繁进行红黑树和链表的转换,造成性能的损耗。

     HashMap还有那些重要的属性,都是做什么的 用来储存节点的数组table、用来储存map长度的size、还有扩容的阈值threshold(当 HashMap 的个数达到该值,触发扩容)和负载因子loadFactor。其中扩容的阈值就等于 负载因子*容量。在我们新建 HashMap 对象时,threshold 除了用于存放扩容阈值还会被用来存初始化时的容量,HashMap 直到我们第一次插入节点时,才会对 table 进行初始化,避免不必要的空间浪费。

     HashMap 的默认初始容量是多少?HashMap 的容量有什么限制吗? HashMap的默认初始容量是16。对于HashMap而言,他的容量必须是2的N次方,HashMap 会根据我们传入的容量计算一个大于等于该容量的最小的2的N次方,例如传 9,容量为16。传17,容量为32。

     为什么容量必须为2的N次方 因为计算索引位置的公示为(n - 1) & hash (位运算,必须同为1才是1,否则为0)。当 n 为 2 的 N 次方时,n - 1 为低位全是 1 的值,此时高位任何值跟 n - 1 进行 & 运算的结果为该值的0(既然我们的 n-1 永远都是 1,那 ( n - 1 ) & hash 的计算结果就是 低位的hash 值。),不仅达到了和取模同样的效果,实现了均匀分布,减少了hash冲突。比%(取模)快了很多。

    为什么负载因子的值为0.75 这个也是在时间和空间上权衡的结果,如果负载因子值比较高,(比如是1) 虽然会减少空间开销,但是哈希冲突的概率会增大,增加查找的成本。而如果比较低,(比如是0.5)虽然哈希冲突的概率会减小,但是会浪费掉一半的空间,得不偿失。权衡之下,0.75是最合适的,他 * 2的N次方还都是整数,很人性化。

HashMap的插入流程
如果哈希表没有初始化,首先进行初始化,默认长度为16。
然后计算出key的哈希值,找出在在节点数组中对应的下标 i。
判断table[i]是否为空,若为空,直接插入;
若不为空,则判断当前的key 与 table[i] 保存的key 是否相同,若相同则直接覆盖;
若不同,则需看改Map的头结点是不是红黑树节点(判断红黑树还是链表)。
如果是红黑树,找到红黑树的根节点,然后开始遍历,找到key相同节点的位置,然后插入最后进行平衡调整。
如果是链表,则遍历链表,使用equals() 方法判断key是否存在,若存在则直接覆盖。否则插入链表末尾。如果节点个数超过8,则链表转红黑树。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值