Java容器

152 阅读6分钟

二. 集合框架

  1. 以Map结尾的类都实现了 Map 接口,其他类实现了Collection接口
  2. 说说 List,Set,Map 三者的区别
  • List(对付顺序的好帮手): 存储的元素是有序的、可重复的。
  • Set(注重独一无二的性质): 存储的元素是无序的、不可重复的。
  • Map(用 Key 来搜索的专家): 使用键值对(key-value)存储,Key 是无序的、不可重复的,value 是无序的、可重复的,每个键最多映射到一个值。
  1. List,Set,Map三者底层的数据结构?
  • List: Arraylist、Vector: Object[]数组;LinkedList: 双向链表
  • Set: HashSet(无序,唯一): 基于 HashMap 实现的 ;TreeSet(有序,唯一):红黑树(自平衡的排序二叉树)
  • Map: HashMap: JDK1.8 之前 HashMap 由数组+链表组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突)。JDK1.8 以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)时,将链表转化为红黑树,以减少搜索时间 ; Hashtable: 数组+链表组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的 TreeMap: 红黑树(自平衡的排序二叉树)
  1. ArrayList的底层实现,为什么查询快,增删慢
    1. ArrayList 采用数组存储,所以插入和删除元素的时间复杂度受元素位置的影响。ArrayList 支持快速随机访问,通过元素的序号快速获取元素对象(对应于get(int index)方法)
  1. ArrayList的扩容策略,会无限扩容吗?
    1. 扩容策略
1.初容量DEFAULT_CAPACITY=10,minCapacity=size+1
2.当minCapacity>DEFAULT_CAPACITY时,就进入grow()方法进行扩容,具体:
    1.创建oldCapacity等于elementData数组的长度(原来的大小)
    2.创建newCapacity等于1.5倍的oldCapacity作为扩容后的大小
    3.当扩容后的大小仍小于minCapacity就使新的容量等于minCapacity
    4.MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
    5.若minCapacity大,将Integer.MAX_VALUE作为新数组的大小
    若MAX_ARRAY_SIZE大,将MAX_ARRAY_SIZE作为新数组的大小
    1. 不会无限扩容,上限是Int的上限,2^31-1。中间减 8 是为了容错
  1. HashSet 如何检查重复
  • 计算新对象的hashCode()值,会与其他已经加入的对象的 hashcode 值作比较,不同的话直接加入到HashSet。
  • 有相等的情况:继续调用 equals() 方法来检查 hashcode 相等的对象是否真的相同,如果两者相同,HashSet 就不会让其加入操作成功;不同的话,就加入到HashSet
  • 这样我们就大大减少了 equals 的次数,相应就大大提高了执行速度。
  1. HashMap
  • 底层数据结构:JDK1.8 之前 HashMap 由数组+链表组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突)。JDK1.8 以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)时,将链表转化为红黑树,以减少搜索时间
  • 几个重要字段
1. size:实际存储的key-value键值对的个数
2. loadFactor:负载因子,代表了table的填充度有多少,默认是0.753. capacity:容量(默认值164. threshold: 阈值,当table为空时,该值=capacity;当不为空时,threshold=Capacity * LoadFactor
5. modCount:HashMap被改变的次数
6. Resize():扩容。衡量HashMap是否进行Resize的条件如下:HashMap.Size   >=  Capacity * LoadFactor
  • HashMap 的长度为什么是 2 的幂次方?
计算数组下标首先会想到采用%取余的操作实现,如果 length 是 2 的 n 次方,则hash%length==hash&(length-1), 采用二进制位操作 &,相对于%能够提高运算效率。
  • 线程不安全的原因
总结:JDK1.7中 死循环、数据丢失 ;JDK1.8中 数据覆盖
JDK1.7中:Resize( )方法不是简单的把长度扩大,而是经过下面两个步骤 ①扩容:创建一个新的Entry空数组,长度是原数组的2倍。②ReHash:遍历原Entry数组,重新定位Entry的下标,并采用头插法将元素迁移到新数组中。头插法会将链表的顺序翻转,这也是形成死循环的关键点。ReHash在并发的情况下可能会形成链表环。当调用Get查找一个不存在的Key,而这个Key对应的位置带有环形链表时,程序将会进入死循环
JDK1.8中:采用尾插法解决了循环链表的问题。假设两个线程A、B都在进行put操作,并且hash函数计算出的插入下标是相同的,当线程A完成hash碰撞的判断后由于时间片耗尽导致被挂起,而线程B得到时间片后在该下标处插入了元素,完成了正常的插入,然后线程A获得时间片,由于之前已经进行了hash碰撞的判断,所有此时不会再进行判断,而是直接进行插入,这就导致了线程B插入的数据被线程A覆盖了
  1. ConcurrentHashMap 线程安全的具体实现方式
  • 在 JDK1.7 的时候,ConcurrentHashMap(分段锁) 对整个桶数组进行了分割分段(Segment),每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。
  • JDK1.8,ConcurrentHashMap 取消了 Segment 分段锁,采用 CAS 和 synchronized 来保证并发安全。synchronized 只锁定当前链表或红黑二叉树的首节点,这样只要 hash 不冲突,就不会产生并发
  • 与HashTable的区别:Hashtable(同一把锁) :使用 synchronized 来保证线程安全,效率非常低下。当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态
  • 补:在JDK1.7版本中,ConcurrentHashMap维护了一个Segment数组,Segment这个类继承了重入锁ReentrantLock,并且该类里面维护了一个 HashEntry<K,V>[] table数组,在写操作put,remove,扩容的时候,会对Segment加锁,所以仅仅影响这个Segment,不同的Segment还是可以并发的,所以解决了线程的安全问题,同时又采用了分段锁也提升了并发的效率。在JDK1.8版本中,ConcurrentHashMap摒弃了Segment的概念,而是直接用Node数组+链表+红黑树的数据结构来实现,并发控制使用Synchronized和CAS来操作,整个看起来就像是优化过且线程安全的HashMap。