Java知识点整理2-集合

二、集合

1. ArrayList和LinkedList区别

  1. 是否线程安全:都不线程安全。
  2. 底层数据结构:ArrayList使用Object数组;LinkedList使用双向链表。
  3. 插入删除是否受元素位置影响:ArrayList:末尾追加时间复杂度为O(1),指定位置i时间复杂度为O(n-i);LinkedList:末尾追加时间复杂度近似O(1),指定位置i时间复杂度近似为O(n)。
  4. 是否支持快速随机访问:ArrayList支持,LinkedList不支持。
  5. 内存空间占用:ArrayList占用连续空间,末尾预留一定的容量空间;LinkedList链表链接,可以空间不连续,每个元素占用更多空间,因为需要额外存储前后指针变量。

RandomAccess接⼝:无定义,用于标识实现这个接口的类具有随机访问功能。
list遍历方式的选择:

  • 实现了RandomAccess接口的list,优先选择普通for循环,其次foreach。
  • 未实现RandomAccess接口的list,优先选择iterator遍历(foreach遍历底层也是通过iterator实现的),大size的数据,千万不要使用普通for循环。

2. ArrayList的扩容机制

https://github.com/Snailclimb/JavaGuide/blob/master/docs/java/collection/ArrayList-Grow.md

  1. ArrayList的三种构造方法:
  • 默认构造函数:使用初始容量10构造一个空列表。
  • 带初始容量参数的构造函数:用户指定容量。
  • 构造包含指定collection元素的列表,这些元素利用该集合的迭代器按顺序返回。
  1. 一步步分析:
  • add方法先调用ensureCapacityInternal()方法。
  • ensureCapacityInternal()方法调用ensureExplicitCapacity()方法。
  • ensureExplicitCapacity()方法判断是否调用grow()方法。
  • 如果newCapacity大于MAX_ARRAY_SIZE,则调用hugeCapacity()方法。
  1. System.arraycopy()和Arrays.copyOf()方法
  • 联系:copyOf()内部调用了System.arraycopy()方法。
  • 区别:arraycopy()需要目标数组,而且可以选择拷贝的起点和长度以及放入新数组的位置;copyOf()时系统自动在内部新建一个数组,并返回该数组。
  1. ensureCapacity()方法
    确保ArrayList实例容量满足入参的元素数大小。
    补充:
  • length属性针对数组
  • length()方法针对字符串
  • size()方法针对泛型集合

3. HashMap和Hashtable的区别

  1. 线程是否安全:HashMap非线程安全,Hashtable线程安全(synchronized实现)。
  2. 效率:HashMap比Hashtable效率高
  3. 对Null key和Null value的支持:HashMap,null可以作为key,只有1个,可以有多个value为null。Hashtable不支持null key。
  4. 初始容量大小和每次扩容大小的不同:
  • 创建时不指定容量初始值:Hashtable默认初始大小为11,之后扩充为2n+1;HashMap默认初始大小为16,扩充为2n。
  • 创建时指定容量初始值:Hashtable直接使用给定值;HashMap会扩充为2的幂次方大小(HashMap中的tableSizeFor()方法保证),也就是说,HashMap总是使用2的幂次方作为哈希表的大小。
  1. 底层数据结构:HashMap当链表长度大于阈值(默认为8)时,将链表转化为红黑树;Hashtable没有这样的机制。

4. HashMap的底层实现:

JDK1.8之前

数组+链表,即散列表。
通过key的hashCode经过扰动函数处理过后得到hash值,然后通过(n-1)&hash判断当前元素存放的位置(n指数组的长度),如果当前位置存在元素的话,就判断hash值和key是否相同,相同直接覆盖,不同则通过拉链法解决冲突。
扰动函数指的是HashMap的hash方法,目的是减少碰撞。

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

JEDK1.8之后

增加:链表长度大于阈值时,将链表转化为红黑树
TreeMap,TreeSet和JDK1.8之后的HashMap都用到了红黑树。红黑树是为了解决二叉查找树的缺陷,因为二叉查找树在某些情况下会退化成一个线性结构。

5. HashMap的长度为什么是2的幂次方

可以将%取余运算转化为位与操作:hash%length=hash&(length-1),提高了运算效率。

6. HashMap多线程操作导致死循环问题

并发下的Rehash会造成元素之间形成一个循环链表。jdk1.8之后已解决,但仍存在其他问题如数据丢失。
put->不存在key,addEntry->size超过阈值,resize->transfer->循环从旧表取元素放到新表->产生死循环。

7. ConcurrentHashMap和Hashtable的区别

主要体现在实现线程安全的方式上不同。

  • 底层数据结构:JDK1.7的ConcurrentHashMap底层采用分段的数组+链表实现,JDK1.8则采用数组+链表/红黑树。Hashtable采用数组+链表。
  • *实现线程安全的方式:
    • JDK1.7的时候,ConcurrentHashMap(分段锁)对整个桶数组进行了分割分段(Segment),每一把锁只锁容器中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。到了JDK1.8的时候已经摒弃了Segment的概念,而是直接用Node数组+链表+红黑树的数据结构来实现,并发控制使用synchronized和CAS来操作。(JDK1.6以后对synchronized锁做了很多优化)整个看起来就像是优化过且线程安全的HashMap,虽然在JDK1.8中还能看到Segment的数据结构,但是已经简化了属性,只是为了兼容旧版本
    • Hashtable(同一把锁):使用synchronized来保证线程安全,效率非常低下。

*8. ConcurrentHashMap底层的具体实现

  • HashMap实现原理
    https://www.cnblogs.com/chengxiao/p/6059914.html
    哈希冲突的解决方法:开放定址法,再散列函数法,链地址法。HashMap采用了链地址法,也就是数组+链表的方式。
    HashMap的主干是一个Entry数组。Entry是HashMap的基本组成单元,每一个Entry包含一个key-value键值对。
    在常规构造器中,没有为数组table分配内存空间(有一个入参为指定Map的构造器例外),而是在执行put操作的时候才真正构建table数组。
    new HashMap -> put: inflateTable, hash, indexFor, addEntry
    inflateTable: roundUpToPowerOf2, Integer.highestOneBit
    确定存储位置的流程:key(hashCode()) -> hashcode(hash()) -> h(indexFor(), 即h&(length-1)) -> 存储下标
    addEntry: resize -> indexFor -> createEnrty
    get -> getEntry: hash, indexFor, equals
  • ConcurrentHashMap实现原理
    JDK1.7:分段锁,继承可重入锁ReentrantLock
    https://www.cnblogs.com/chengxiao/p/6842045.html
    JDK1.8:CAS+synchronized,数组+链表+红黑树
    https://blog.csdn.net/programmer_at/article/details/79715177
    重要概念:
  1. table:默认为null,用来存储Node节点数据
  2. nextTable:默认为null,扩容是新生成的数组,其大小为原数组的两倍
  3. sizeCtl:默认为0,用来控制table的初始化和扩容操作
    • -1:表示table正在初始化
    • -N:表示有N-1个线程正在进行扩容操作
    • 如果table未初始化,表示table需要初始化的大小
    • 如果table初始化完成,表示table的容量,默认是table大小的0.75,计算公式为0.75*(n-(n>>>2))
  1. Node:保存key,value及key的hash值的数据结构。其中value和next都用volatile修饰,保证并发的可见性
  2. ForwardingNode:一个特殊的Node节点,hash值为-1,存储nextTable的引用。只有table发生扩容的时候,ForwardingNode才会发挥作用,作为一个占位符放在table中表示当前节点为null或已经被移动
  • 实例初始化->
  • table初始化(发生在第一次put操作,unsafe类的cas操作sizeCtl)->
  • put(不用table(index)而是用unsafe.getObjectVolatile()来获取table对应的索引元素,是为了保证每次拿到数据都是最新的;当前bucket为空时,使用cas将Node放入对应的bucket中;如果当前map正在扩容f.hash==MOVED,则跟其他线程一起进行扩容;出现hash冲突,使用synchronized关键字;链表长度大于8时,若数组长度小于64,则扩容数组,若大于等于64,该节点链表转为红黑树)->
  • treeifyBin(同一节点个数大于8个时调用)->
  • tryPresize(数组长度小于64时)->
  • transfer(非初始化扩容时,步长控制对CPU的使用,每个CPU最少处理16个长度的数组元素)
  • TreeBin()(treeifyBin中链表转为红黑树时调用)
    sun.misc.unsafe类的方法通过直接操作内存的方式来保证并发处理的安全性,使用的是硬件的安全机制。
    读操作支持并发的,没有使用同步机制,也没有使用unsafe方法
    多个线程如何同步处理?
    主要通过synchronized和unsafe两种方式
  • 在取得sizeCtl、某个位置的Node的时候,使用的都是unsafe的方法,来达到并发安全的目的。
  • 当需要在某个位置设置节点的时候,则会通过synchronized的同步机制来锁定该位置的节点。
  • 在数组扩容的时候,则通过处理的步长和fwd节点来达到并发安全的目的,通过设置hash值为MOVED。
  • 当把某个位置的节点复制到扩张后的table的时候,也通过synchronized的同步机制来保证线程安全。

9. comparable和Comparator的区别

  • comparable接口出自java.lang包,它有一个compareTo(Object obj)方法用来排序
  • comparator接口出自java.util包,它有一个compare(Object obj1, Object obj2)方法用来排序
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值