zoukankan      html  css  js  c++  java
  • Java集合

    集合

    集合分为两大类:Collection和Map,集合进行增加/删除元素的时候会将内部属性modCount进行记数,作用是在iterator进行遍历的时候判断集合内数据是否发生变化

    迭代器

    iterator.next()会越过下一个元素,并返回越过元素的引用;iterator.remove()会删除越过的上一个元素

    可以认为next()永远指向两个元素中间的位置

    可以通过集合 instanceof RandomAccess来判断集合是否支持高效随机访问

    当集合被另一个线程修改(增加删除元素),则当前遍历(通过迭代器)会出现异常(Concurrent ModificationException);检测方式是集合记录当前增加/删除元素操作的次数,并且该集合的迭代器单独记录增加/删除元素的次数;在迭代器开始处检查自己计数是否和迭代器计数一致,如果不一致则抛出Concurrent ModificationException异常

    散列表(HashSet)

    在Java8之前通过数组和链表实现,在Java8之后如果通过equals方法找到桶后发生冲突(碰撞)则会在桶之后连接一个平衡二叉树(AVL),整体上是通过开散列实现的,底层通过HashMap实现value都是同一个Object对象

    开散列 就是经过哈希函数后产生冲突,在桶位置上连接另外的数据结构存储相同哈希值数据;而 闭散列 在发生哈希冲突后不会连接新的数据结构,可能会加上一个随机值后重新计算哈希值或者寻找当前桶的下一个位置直到找到空桶 桶的大小是2的整数次幂(任何值都会自动转换为2的下一个幂);如果能够预计元素个数,则桶的个数是元素个数的75%~150%最佳 Java中散列表默认装载因子是0.75,即表中75%的桶上有数据会创建一个桶数更多的表(默认是原来的2倍)

    散列表中的 视图,即:keySet()、values()、entrySet()并不是返回一个新的集合将元素添加进去并返回,而是返回实现Set接口的对象对原Map集合中数据进行映射,而并没有重新拷贝原值生成新集合对象

    TreeSet and TreeMap

    使用红黑树存储,插入数据后自动排序

    为什么使用红黑树而不是AVL?

    因为AVL可能需要O(lgn)次旋转达到平衡,而红黑树最多两次旋转即可达到平衡(但需要O(lgn)检查节点寻找旋转位置);并且AVL树的旋转更加复杂,在每个节点需要额外存储平衡因子,而红黑树可以用一位表示颜色;AVL树查找比红黑树快

    枚举集合(EnumSet)

    EnumSet因为元素是枚举类型有限,内部使用位图实现

    优先级队列(PriorityQueue)

    使用小根堆存储(自我调整的二叉树),添加/删除可以让最小的元素移动到根

    HashMap

    允许键和值为null,在JDK1.8之前使用数组+链表实现,在1.8之后在链表长度大于8并且数组长度大于64之后会将链表转换为红黑树;在链表长度小于6的时候会转化为链表,在JDK1.8及以前扩容链表采用头插法,会生成逆序链表,多线程插入后再次get会有死循环问题,容易造成CPU占用100%;JDK1.8及以后采用尾插法,避免了死循环问题。

    //  JDK1.7以前扩容
    void transfer(Entry[] newTable, boolean rehash) {
       int newCapacity = newTable.length;
       for (Entry<K,V> e : table) {
           /*
          1.取当前元素,将当前元素的next指针指向新链表的头节点
          2.将当前元素设置为新链表的头节点(设置桶的第一个元素)
          3.取下一个元素循环
           */
           while(null != e) {
               /*
              此行发生时间片切换后,线程1中e=链表头元素,next=链表头元素的下一个元素;线程2执行完resize后,返回到线程1
              因为JDK1.7及以前resize链表采用头插法,所以返回到线程以后,链表会逆置,此时线程1的e=当前链表的尾元素,next是 当前链表尾元素的上一个元素;继续执行,相当于e进行两次插入,修改next(第一次的时候e的真实next应该为null,但是因为时间片发生了轮转,因此next保存的元素变成了e的上一个元素),第二次插入e的元素时候,修改next就会形成闭环链表
              (两个线程调用resize会产生两个新容量数组)
               */
               Entry<K,V> next = e.next;
               if (rehash) {
                   e.hash = null == e.key ? 0 : hash(e.key);
              }
               int i = indexFor(e.hash, newCapacity);
               e.next = newTable[i];
               newTable[i] = e;
               e = next;
          }
      }
    }7

    HashMap使用key的HashCode值结合数组长度进行右移动、异或、与运算计算出数组索引位置,还可以采用平方取中法、取余法、伪随机数法计算索引;位运算效率最高

    1. 初始化:HashMap中数组长度最好设置为2的n次幂,因为底层计算哈希值是通过key的hashCode&(length-1)计算,在长度(length)是2的n次方-1时,能够保证发生碰撞的概率最小(因为此时length-1低位全是1,而1与任何数还是任何数,如果是0的话则该位置固定为0,会缩小桶的范围)

    2. 添加元素:通过元素的hashCode低16位与高16位做异或运算的到哈希值,找到哈希表的桶位置;遍历链表/红黑树,比较key哈希值是否==或者key.equals,如果相等则替换;如果都不想则会添加到链表/红黑树中,添加过程先判断链表元素是否大于8,如果大于8再判断数组元素长度是否大于64,如果数组长度小于64则扩容后重新哈希;如果数组大于等于64则将链表转换成红黑树

    3. 扩容时机:1.当HashMap中数组中的元素个数超过 负载因子*容量 会进行扩容;2.当数组拉链后链表元素个数大于8,但数组元素容量小于64会进行扩容

    4. HashMap的kv都可以为null(哈希值为0)

    5. rehash(重哈希)策略:每次重哈希都会将数组长度扩充为原来的2倍(原来容量逻辑左移一位);原哈希表的桶如果没有链表则直接与现容量-1做哈希运算(&操作,即:还是放到原来位置上);原哈希表如果有链表则遍历链表,判断每个元素的原容量位置上的位是0还是1来决定将该元素放到新数组的原位置,还是放到新数组的原位置+原容量(将链表上的每个元素的的哈希值与原容量做&运算结果为0,则还是放在和原数组相同的位置上;如果运算结果是1则放到原位置+原容量的位置上)

    6. Jdk1.8之后在初始化HashMap时候没有指定容量(数组长度)则会在第一次put时初始化为16

    7. 哈希值计算,通过元素的hashcode高16位和低16位异或,如果key是null则哈希值为0

    建议第一次初始化时指定容量数(数组)大于16,因为如果在扩容(resize)的时候会进行判断,如果扩容后的容量小于等于16,则扩容阈值不会加倍;即:如果初是容量过小,阈值会位置在一个较小的值发生频繁扩容

    链表长度大于8会转化为红黑树,这是因为红黑树的节点容量是链表容量的2倍,根据泊松分布链表上有8个元素的概率接近于0,所以选择8为阈值;当红黑树节点数量小于6的时候会转化为单向链表

    // HashMap成员遍历
    // 初始容量
    static final int DEFAULT_INITIAL_CAPACITY = 1<<16;
    弱引用散列表(WeakHashMap)

    WeakHashMap使用弱引用保存Map映射的键,当WeakHashMap中的Key没有其他引用时,在GC之后会将其中的对象回收掉

    链接散列表(LinkedHashMap)

    底层还是通过数组和链表实现,不过元素之间多了双向链表链接;遍历时也是按照双向链表的方式遍历;从链接散列表中删除数据,将要删除的元素放到双向链表尾部,但是实际的存储位置不会发生改变(对应的桶并且在桶中链表的位置不变)改变的只是双向链表的指针指向

    数组集合(ArrayList)

    Jdk1.8之后无参构造不再创建指定大小的数组,而是在第一次添加元素时扩容为容量为10的数组,之后在每次集合长度越界时会进行扩容;每次扩容都是如果指定容量比原容量的1.5倍小则会扩容为原容量的1.5倍,如果比原容量的1.5倍大,则会扩充为指定容量;每次扩容都是原容量的1.5倍,只有在容量不够的时候才会进行扩容(通过addAll方法如果添加的集合元素个数超过原集合的1.5倍,则会扩容为 原集合长度+增加集合的长度,而不是原集合的1.5倍)

    在指定位置添加集合:1.记录数组长度numNew=size、要移动元素个数movedNum=size-index、调用System.arraycopy(源数组,index,源数组,index+插入数组长度,movedNum),将原数组指定索引位置之后的元素后移动添加集合的长度调用System.arraycopy(目的数组,0,源数组,index,movedNum)将目的数组的元素复制到源数组的空出位置

    toString:底层调用iterator循环迭代,通过StringBuilder拼接字符串

    remove/add:通过集合的remove/add会将集合操作次数(modCount)加一,但是迭代器(iterator)的期望修改次数(expectedModCount)不会增加,在集合删除元素后再通过迭代器(Iterator)遍历(next)会检查会修改次数,如果不同会抛出异常(ConcurrentModificationException)

    remove:每次删除元素都会将后面的元素前移

    clear:将修改次数(modCount)+1,遍历集合将每一个元素设置为null,并且数组长度(size)设置为0

    foreach实现会调用迭代器,ArrayList使用迭代器的遍历效率没有随机访问效率高,因为使用的普通的for循环遍历的效率更好;

    modCount只在集合容量修改时增1(扩容缩容),而iterator是在第一次由集合创建时赋值,iterator.remove重新赋值

    通过iterator遍历时,通过集合对象增删元素会抛出异常(ConcurrentModificationException),但是有一个例外;通过集合对象的删除(list.remove)倒数第二个元素不会抛出异常,因为调用iterator.hasNext方法每次会比较iterator中暂存的光标位置和数组长度size,如果相同则停止遍历(因为iterator递增,当其等于size的时候就是到数组对象末尾)如果删除倒数第二个元素,则光标指向数组长度-1位置处,并且数组长度-1,此时不会再次进入循环,因此不会抛出异常ConcurrentHashMap

    链表集合(LinkedList)

    底层通过双向链表实现,初始化为0;从头或者从尾查找元素效率较高;通过下标获取元素,如果下表大于长度的1/2则会倒序遍历检索元素

    线程安全的集合:Hashtable VS ConcurrentHashMap VS CopyOnWriteArrayList
    1. Hashtable:对散列表的所有操作全部加上synchronized,并发性能很差,例:一个线程对集合进行读取,另一个线程则不能修改集合,并且数量等都是每次添加时写入,序列化后会消失

    2. Collections.synchronizedMap(map):同上,返回一个所用方法都用synchronized修饰的map视图,并发性很差

    3. ConcurrentHashMap:如果没指定容量,第一次put时候初始化,默认大小是16;加锁粒度更加细化,get等不改变集合的方法不加锁,value通过volatile修饰保证多线程获取的可见性,set使用 分段锁 技术,JDK1.8之后,ConcurrentHashMap对桶节点加锁(数组的头节点加synchronized),size方法最多返回Integer最大值,最好使用mappingCount方法;具体实现是,每次添加元素的时候,如果没有发生线程竞争(因为是分段锁,所以可能会并发添加),则不会初始化CounterCell[],put之后调用addCount方法,统计元素增加到baseCount属性;如果发生线程竞争以后,初始化CounterCell[],通过线程的哈希值&CounterCell.length,计算线程位置后循环cas设置容量,调用size/mappingCount方法时候会循环计算CounterCell[]以及baseCount

    4. CopyOnWriteArrayList:不改变集合的操作不会加锁,直接返回数组元素,改变集合的操作会通过ReentrantLock加锁,并且通过Arrays.copyOf()创建一个新数组操作之后,取代旧数组,适用于读多写少的场景读写分离,不支持快速失败,即不检查modCount

    5. ConcurrentLinkedQueue:通过CAS+自旋操作保证元素正确添加

    6. Juc包没有提供ConcurrentHashSet,可以通过ConcurrentHashMap.newKeySet()创建一个ConcurrentHashSet(value值都是true的ConcurrentHashMap);map.keySet()返回一个不可添加元素的关于key的set视图,如果想要添加元素,则在调用keySet(默认值)指定一个默认的value

    7. ConcurrentLinkedQueue:通过cas添加/删除元素

    视图

    视图操作的还是原集合,只是对原集合进行了包装处理,Collection可以创建很多不可修改视图,视图只是包装了接口因此只能访问对应迭代接口方法,而具体特定实现的方法无法使用

    最好不要使用Collections.synchronizedXXXX等方法转换线程集合,因为该方法返回一个原集合的视图(包装原集合),并且在集合的操作上加上synchronized对象锁,多线程环境下效率很低;如果需要保证多线程环境下安全访问集合,最好使用JUC包下的相关集合类

    // 返回视图的方法:
    // 返回一个包装数组的ArrayList视图
    Arrays.asList(数组);
    // 返回HashMap的key实现的Set视图
    hashMap.keySet();
    // 返回HashMap的kv实现的Set视图
    hashMap.entrySet();
    // 返回HashMap的value实现的Collection集合
    hashMap.valuse();
    // 返回一个100个重复"DEFAULT"的不可修改List视图,如果修改会抛出运行时异常UnsupportedOperationException
    List<String> list = Collections.nCopies(100, "DEFAULT");
    // 集合子范围都返回一个视图
    list.subList(start,end);

    排序

    Collections.shufle(List);判断List是否实现了RandomAccess(随机访问是否高效),如果支持高效随机访问则从尾部开始与随机索引的值进行交换;如果不支持高效随机访问则会将List复制到Object数组内从尾部开始与随机索引值得交换最后再复制(set)到原List中

    List/Set.toArray();返回一个Object[]数组,如果知道具体类型和数组长度则可以通过List/Set.toArray(具体类型[长度]);返回一个具体类型的数组

  • 相关阅读:
    FirstAFNetWorking
    JSONModel 简单例子
    KVO
    KVC
    关于UITableView的性能优化(历上最全面的优化分析)
    浅拷贝和深拷贝
    UI2_异步下载
    UI2_同步下载
    算法图解学习笔记02:递归和栈
    算法图解学习笔记01:二分查找&大O表示法
  • 原文地址:https://www.cnblogs.com/leon618/p/13783406.html
Copyright © 2011-2022 走看看