Java进阶之浅析List、Set集合

List集合

List接口有三个子类实现了它,分别是Vector、ArrayList、LinkedList

  • Vector
    和ArrayList一样底层是数组实现,只不过它是线程同步的 ,我看了下源码,它好像只有clear()这个方法没有加synchronized关键字,其余的方法都保证线程同步了,线程安全,效率比较低,线程工作起来要一个接一个获取锁,排队执行任务。
    • 线程安全,效率低
    • 由于底层是由数组实现的,所以元素增删慢、查找快
    • 元素存取有序,有索引
    • 可以存放null值以及重复的值
    public synchronized boolean add(E e) {
        modCount++;
        ensureCapacityHelper(elementCount + 1);
        elementData[elementCount++] = e;
        return true;
    }
  • ArrayList
    • 底层由数组实现,内存地址是连续的,所以查找快,增删慢,查找快是因为有索引的存在,增删慢是因为每一次的增删都要重新复制一个新数组,然后将原数组的数据选择性的拷贝过去。
    • 元素存取有序,有索引
    • 可以存放null值以及重复的值
    • 线程不安全,效率高
    • 默认的初始容量是10,每次扩容grow是原来数组容量的1.5倍,我建议在new的时候就在括号中显式的赋予初始容量,省的它再grow()这样一个过程,你不给他初始值,他会执行一次grow()(扩容)的。
    • 每增加一个元素,size+1,这个size就是当前这个arrayList容器已包含的元素数量,下面是源码,minCapacity追溯到源头其实是size+1,而elementData.length则是数组的初始长度,这个if如果满足则扩容,怎么满足?当前元素的数量+1大于当前数组的长度的时候,开始grow()。(想充分看源码,原谅这里只是讲个主要,还请小伙伴们自己动手去debug,这样印象会更加深刻,去亲自看他的执行过程)
    private void ensureExplicitCapacity(int minCapacity) {
        modCount++;

        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }
  • LinkedList
    • 底层是一个双向链表,双向链表意味着有指向上一个节点(前驱)的pre指针,指向下一个节点(后继)的next指针,本身还有个data区域存放数据,在内存中地址是不连续的,可以充分利用内存的碎片空间,另外多加了一条链表从而保证了存取数据的有序、一致 。
    public static void main(String[] args) {
        LinkedList<String> list = new LinkedList<>(); 
        // 输出结果[haha, hehe, heihei, xixi, lala]
        Collections.addAll(list, "haha","hehe","heihei","xixi","lala");
        System.out.println(list);
    }
  • 线程不安全,执行效率高,我看了下方法,比如addFirst,并没有使用什么同步机制,所以如果在使用LinkedList的情况下想保证线程同步,就如同jdk文档上所说,要保证外在的同步,即List list = Collections.synchronizedList(new LinkedList(...));

引用官方的一段话:

如果多个线程同时访问链接列表,并且至少有一个线程在结构上修改列表,则必须在外部进行同步。 (结构修改是添加或删除一个或多个元素的任何操作;仅设置元素的值不是结构修改。)

Set集合

HashSet

  • 不能保证元素的存入和取出的顺序是一致的,它内部是HashMap实例,用的还是Hashmap的方法,底层是哈希表(数组+链表,jdk1.8升级为链表的数量大于等于8个节点转红黑树,提高检索效率)实现的,存放的是一个个Entry<K,V>节点,而add()一个元素具体存放在哪个位置是根据key的hash算法计算得到的

先说hash算法

    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

这个是具体存放的数组位置

  			Node<K,V>[] tab; Node<K,V> p; int n, i;
        	if ((tab = table) == null || (n = tab.length) == 0)
        	// n等于扩容后数组的长度,扩容后面讲
            n = (tab = resize()).length;
			// 给这个i赋值,即索引赋值 i = (n - 1) & hash
			if ((p = tab[i = (n - 1) & hash]) == null)
			// 数组的第i个索引位置,存放这个节点对象
            tab[i] = newNode(hash, key, value, null);

在这里插入图片描述

  • 默认初始容量是16,每次扩容为之前的2
  • 线程不安全,要实现线程同步,必须保证外在的同步,本身的方法是不具备同步的特性的;
  • 允许加入的数据为null;
  • 不允许重复,你即使刻意加入重复数据,也只会存在一个数值;
    public static void main(String[] args) {
        HashSet<String> set = new HashSet<>();
        set.add(null);
        set.add("a");
        set.add("a");
        System.out.println(set);// [null, a]
    }
  • 接下来说下扩容resize()
   			newCap = DEFAULT_INITIAL_CAPACITY;// 默认容量16
   			// 负载因子0.75 * 默认初始容量16 = 12
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);

一开始初始化的时候,会确定一个newThr这个值,是负载因子和默认初始容量的乘积,当set集合中的元素的数量大于这个乘积的时候(每增加一个元素,size变量就会加一),集合就会扩容,上述说的是扩容的前提,下面说下扩容具体的过程,不多说,直接看源码。

 Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        table = newTab;
        // 如果原来的set集合不为空,也就是有元素,那么接下来开始转换元素到新的数组
        if (oldTab != null) {
            for (int j = 0; j < oldCap; ++j) {// 开始遍历原来的数组
                Node<K,V> e;
                if ((e = oldTab[j]) != null) {
                // 解除引用,原来的数组中的元素都被赋值为null,在此之间他把原来数组中的元素都赋值给了一个零时的Node<K,V> e;
                    oldTab[j] = null;
                    if (e.next == null)// 这个数组的内部呢,其实是一条一条的链表,如果之前在加入元素时发生过哈希碰撞的话。然后判断条件是当前的这个节点是否还有下一个节点,如果为空,就先把当前节点复制给新数组的经过hash计算后的相应位置
                        newTab[e.hash & (newCap - 1)] = e;
                    else if (e instanceof TreeNode)
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    else { // preserve order
                        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;
                            }
                            // 如果下个节点不是空才进行节点的往后挪,避免了NPE
                        } while ((e = next) != null);
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }

总而言之就是new了一个新的Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];然后遍历之间的数组,然后再转移给新的数组,当然了,新数组肯定更大,否则怎么能叫扩容呢,你如果有疑问,可以疑问疑问这个hash算法是怎么算的。

LinkedHashSet

  • 底层是由哈希表+链表实现,多了一条链表,维护了存取的顺序
  • 元素允许为null值
  • 不允许重复元素
    public static void main(String[] args) {
        LinkedHashSet<String> set = new LinkedHashSet<>();
        set.add(null);
        set.add("a");
        set.add("a");
        System.out.println(set);// [null, a]
    }
  • 线程不安全,性能略低于HashSet ,因为额外维护了一条链表,但有例外,看后续的一段话。
  • 其迭代的效率与实际集合大小成正比,也就是说与实际存放多少个元素成正比,而不与其容量成正比。
    官方的说法还是说的比较通俗易懂的,可以直接看一看

该类提供了所有可选的Set操作,并允许null元素。 像HashSet,它提供了基本操作(add,contains和remove)稳定的性能,假定散列函数散桶中适当的元件。 性能可能略低于HashSet ,由于维护链表的额外费用,但有一个例外:LinkedHashSet的迭代需要与集合的大小成比例的时间,无论其容量如何。 HashSet的迭代可能更昂贵,需要与其容量成比例的时间。
链接哈希集具有影响其性能的两个参数: 初始容量和负载因子 。 它们的定义精确到HashSet 。 但是请注意,该惩罚为初始容量选择非常高的值是该类比HashSet不太严重的,因为迭代次数对于这个类是由容量不受影响。

最后一句话,什么惩罚,意思大概是定义多高的初始容量对于迭代LinkedHashSet无关,因为它的迭代效率是跟其实际包含多少元素挂钩的。

TreeSet

  • 这个集合里面存放的元素都是有序的,如果你存放的是实体类,那么你必须实现Comparable接口,重写其中的compareTo方法不实现的话就会报类型转化异常,
  • 这个方法的返回值
    • 0,则只加入成功第一个元素
    • 1根据你插入的顺序,顺序排序。
    • 负1根据你插入的顺序,逆序排序。
    public static void main(String[] args) {
        TreeSet<String> treeSet = new TreeSet<>();
        treeSet.add("1");
        treeSet.add("3");
        treeSet.add("2");
        System.out.println(treeSet);// [1, 2, 3]
    }
  • 线程不同步

好的,关于List、Set这两个集合就浅析到这里,下一章分析Map集合,分析这点源码花费了不少时间,自己可能还需要更多的成长,如果不足就指出,用的多的还是ArrayList,可能还有个HashSet吧,虽然我全列举出来了,但别的我觉得了解了解就可以了,像这个vector都不怎么用了,不说了,赶紧去恰个中饭。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值