• Java 集合总结


    7月18日更:

    hashMap 是懒加载 只有put的时候 才创建数组 线程不安全主要体现在rehash,也就是扩容时候出现链表成环。链表插入的时候(jdk1.8是在尾部插入,1.8之前是头部插入)

    hashMap的存入过程: Object--->hashCode(hashCode ^ hashCode>>>16 (无符号右移16位 相当于除以2的16次方))高16位和hash自身异或--->hash-->index (hash&(length-1))

        扩容时,变成原来的2倍,那么新索引要么和原来的索引一样,要么增加原数组长度,JDK1.8中,判断hash值新增的那一位(扩大2倍,相当于左移一位)是0还是1, 是0新索引不变,是1增加原数组长度  为什么容量是2的次幂?答:减少hash冲突,2的次幂-1 后面每一位都是1  比如8-1 (1000 ——>  0111) 这样h&(length-1)也就是h&0111可以使数组每个位置都能添加到元素,假设是h&0101 那么第二个位置就一定是0 所以对应的数组这个位置也一直为空。

        取余(%)操作中如果除数是2的幂次 则等同于与其除数减一的与(&)操作。位操作效率比算数运算效率高

        当数组中链表长度大于8,并且数组size大于64,链表变成红黑树,当树长度小于6,恢复成链表

    Java集合是Java基础里面最重要的知识点之一,刚接触时觉得用的地方不多,而且很累赘,但随着写的代码量的增加,越来越觉得集合类是最重要的基础,做算法题的时候,合理利用集合可以更快更有效的AC,因为它提供了很多的方法,只需直接调用即可。做项目的时候,会频繁的用到集合,比如参数的传递,实体的转化等等,都需要map,list等。想深入理解最好还是去看看源码。这里综合网上的很多文章对常用集合做一个总结。

    Java集合可以分为两大类,实现Collection接口的类和实现Map的类,

    Collection接口提供的方法有:增(add,addAll),删(remove,removeAll),查(size,contains)       Map提供的方法有:增(put,putAll),删(remove),查(size,get,KeySet,ValueSet,EntrySet,contains)

    (实际上Collection也继承了Iterable这个接口),实现Collection的又有三个接口,List,Set,Queue。所以集合类基本就分成四部分,Map,list,Set,Queue.

    1.List接口:list表示一个列表,可以是数组,链表,栈,队列,元素可重复     常用方法:

      ArrayList:   底层维护动态数组,允许为值为null 查找复杂度是 O(1),通过下标可以直接查找,增加或删除时需要移动大量元素

           默认初始长度是10,每次通过Arrays.copyOf()方法扩充为原来的1.5倍,如果设置后的新容量还不够,则直接新容量设置为传入的参数(也就是所需的容量)

           Arrays。copyOf()最底层调用了本地方法System.arraycopy(),最终调用了C语言的memmove()函数,因此它可以保证同一个数组内元素的正确复制和移动,比一般的复制方法的实现效率要高很多

            每次增加元素时,都会调用ensureCapacity方法确保足够容量

           线程不安全,可以通过Collections.synchronized(new ArrayList())方法转换为线程安全的

                      ArrayList在迭代的时候如果同时对其进行修改就会抛出java.util.ConcurrentModificationException异常 原因是调用list.remove()方法导致modCount和expectedModCount的值不一致。expectedModCount:表示对ArrayList修改次数的期望值,它的初始值为modCount。modCount是AbstractList类中的一个成员变量 初始为0 所以迭代器移除某个元素的时候 用iterator.remove(),而不是list.remove 因为 iteartor.remove 有 expectedModCount = modCount;

          线程不安全 主要表现在:1 多个线程进行add操作时可能会导致elementData数组越界

               原因是 :add方法分两步 1 检查容量 需要是扩容 2 设置值 索引+1。 如果有两个线程同时检查了扩容 判断都不需要扩容(假设数组容量是10 此时长度是9) 一个线程set值之后 刚好达到最大容量 另一个线程再set就会越界。

           2. 一个线程的值覆盖另一个线程添加的值。因为 

    1. elementData[size] = e;  2 size = size + 1;   这两步不是原子操作。A,B线程同时add一个元素,导致的结果可能是size = 1 但是B线程覆盖了A线程添加的值。

         代码如下

    public static void main(String[] args) throws InterruptedException{
           final List<Integer> list = new ArrayList<>();
    
            new Thread(() -> {
                for (int i = 0 ; i < 1000 ;i ++){
                    list.add(i);
                    try {
                        Thread.sleep(1);
                    }catch (InterruptedException e){
                        e.printStackTrace();
                    }
                }
            }).start();
    
            new Thread(() -> {
                for (int i = 1000 ; i < 2000 ;i ++){
                    list.add(i);
                    try {
                        Thread.sleep(1);
                    }catch (InterruptedException e){
                        e.printStackTrace();
                    }
                }
            }).start();
    
            Thread.sleep(1000);
    
            for( int i = 0 ; i < list.size();i++){
                System.out.println("" + (i+1) + "个元素是 " + list.get(i));
            }
    
    
        }
    //最后会出现 第 X 个元素 是 null的结果

     如果想要用线程安全的arrayList  , 可以使用CopyOnWriteArrayList。内部实现是 在进行add操作的时候 使用 Lock 加锁。

    如果想要使用线程安全的linkedList 可以使用 ConcurrentLinkedQueue。

           可被序列化,随机访问,能被克隆

          参考博客:http://blog.csdn.net/ns_code/article/details/35568011

      LinkedList:  1.底层维护双向链表(1.6是双向循环链表,1.7取消了循环 ),允许值为null,在头尾增加删除操作时很方便,内部只维护size,first,last三个变量或指针,当栈 push,pop,peek;  当队列,offer,poll,peek(peek会自动根据添加元素时的方法判断栈还是队列,默认队列)

            2.可用作栈,队列,双端队列

            3.线程不安全,可被序列化,能被克隆

            4.内部单元是Node<E> 

               

     E item;
            Node<E> next;
            Node<E> prev;

     

             参考博客:http://blog.csdn.net/ns_code/article/details/35787253

          vector:   1 和ArrayList一样,线程安全,同Synchronized关键字提供了方法粒度级别的同步机制

            2. 不支持序列化,可以克隆

       Stack :栈  push ,pop ,peek empty 

    2.set接口:  无序且不重复

       HashSet: 可存储null(占一个位置),线程不安全,适合添加,查询

            底层采用hashMap实现,hashSet存储元素实际为hashMap中的Key,map中key不可重复,所以决定了set中元素不可重复,value为一个静态Object对象

            可用foreach ,iterator 迭代

       TreeSet: 有序集,基于TreeMap,线程不安全

            默认排序是升序

            treeSet需要额外的红黑树算法来维护集合的次序,性能最次。

            方法:first,last,lower(E e):返回小于e的元素中最大元素,higher(E e)同理   subSet(E from, E to)

       LinkedHashSet:基于链表的哈希表,具有迭代顺序的Set集合,保证存储顺序和取出顺序一致,线程不安全

               迭代访问时效率高

               更适合于插入、删除以及遍历操作

       EnumSet:  为枚举类设计的集合类,所有元素都必须是指定枚举类型的枚举值,元素有序

            使用EnumSet.of方法新建

       EnumSet性能>HashSet性能>LinkedHashSet>TreeSet性能

    3.queue接口:队列,FIFO Deque接口是Queue接口的子接口,它代表一个双端队列

       PriorityQueue:从小到大排列,本质上动态数组,维护最小堆,顶点是最小值,与ArrayList一致,默认初始容量是11

                非线程安全,不允许插入Null

       ArrayDeque:底层是循环数组,默认长度16

    4.map接口:维护k,V键值对

        HashMap:  内部是数组,数组的每个元素是一条单链表的头结点,链表是用来解决冲突的,同的key映射到了数组的同一位置处,就将其放入单链表中,key是唯一的,重复插入同一个键,value会被覆盖,Java8是用红黑树代替链表处理冲突(链表长度大于8,否则仍是链表)

              初始容量是16,系统默认加载因子为0.75,加载因子是哈希表在其容量自动增加之前可以达到多满的一种尺度,当哈希表中的条目数超出了加载因子与当前容量的乘积时,则要对该哈希表进行 resize 操作

              HashMap中key和value都允许为null,get方法:如果key为null,则直接从哈希表的第一个位置table[0]对应的链表上查找。

              记住,key为null的键值对永远都放在以table[0]为头结点的链表中,当然不一定是存放在头结点table[0]中。

              如果key不为null,则先求的key的hash值,根据hash值找到在table中的索引,在该索引对应的单链表中查找是否有键值对的key与目标key相等,有就返回对应的value,没有则返回null

              put方法:如果key为null,则将其添加到table[0]对应的链表中,key不为null,求出key的hash,遍历对应的单链表,如果单链表中存在与目标key相等的键值对,

              则将新的value覆盖旧的value,比将旧的value返回,如果找不到与目标key相等的键值对,或者该单链表为空,则将该键值对插入到改单链表的头结点位置

              每次put键值对的时候,总是将新的该键值对放在table[bucketIndex]处(即头结点处)。

              length取2的整数次幂,是为了使不同hash值发生碰撞的概率较小,这样就能使元素在哈希表中均匀地散列。

              h&(length-1)的方法来代替取模,同样实现了均匀的散列

              内部结构为Node<K,V>

              

     final int hash;
            final K key;
            V value;
            Node<K,V> next;

              参考博客:http://blog.csdn.net/ns_code/article/details/36034955

      hashtable: 线程安全,支持序列化,能被克隆

            HashTable在不指定容量的情况下的默认容量为11,而HashMap为16,Hashtable不要求底层数组的容量一定要为2的整数次幂,而HashMap则要求一定为2的整数次幂

              Hashtable中key和value都不允许为null,而HashMap中key和value都允许为null(key只能有一个为null,而value则可以有多个为null)

              Hashtable扩容时,将容量变为原来的2倍加1,而HashMap扩容时,将容量变为原来的2倍。

            Hashtable计算hash值,直接用key的hashCode(),而HashMap重新计算了key的hash值,Hashtable在求hash值对应的位置索引时,用取模运算,而HashMap在求位置索引时,则用与运算

            参考博客:http://blog.csdn.net/ns_code/article/details/36191279

      TreeMap; 基于红黑树实现的

            TreeMap是根据key进行排序的,它的排序和定位需要依赖比较器或覆写Comparable接口,也因此不需要key覆写hashCode方法和equals方法,

            就可以排除掉重复的key,而HashMap的key则需要通过覆写hashCode方法和equals方法来确保没有重复的key。

            查询、插入、删除效率均没有HashMap高,一般只有要对key排序时才使用TreeMap

            TreeMap的key不能为null,而HashMap的key可以为null

            参考博客:http://blog.csdn.net/ns_code/article/details/36421085

      LinkedHashMap:  与HashMap有着同样的存储结构,但它加入了一个双向链表的头结点,将所有put到LinkedHashmap的节点一一串成了一个双向循环链表,

                因此它保留了节点插入的顺序,可以使节点的输出顺序与输入顺序相同。

              非线程安全的

              HashMap和LinkedList两个集合类的存储结构的结合。在LinkedHashMapMap中,

              所有put进来的Entry都保存在哈希表中,但它又额外定义了一个以head为头结点的空的双向循环链表,

              每次put进来Entry,除了将其保存到对哈希表中对应的位置上外,还要将其插入到双向循环链表的尾部。

              说白了就是用两个数据结构保存同一个对象

              参考博客:http://blog.csdn.net/ns_code/article/details/37867985

      

  • 相关阅读:
    二柱子四则运算程序
    测绘软件使用感受
    二分图的最大匹配、完美匹配和匈牙利算法(转载)
    serialVersionUID的用处(转载)
    RMQ(模板)
    codeforces 825E
    红黑树
    SQL 范式(转载)
    java 移位运算
    [Hnoi2010]Bounce 弹飞绵羊
  • 原文地址:https://www.cnblogs.com/team42/p/6960117.html
Copyright © 2020-2023  润新知