JAVA集合

/ JAVA面试 / 没有评论 / 468浏览

Java 集合框架概览

1.list

arraylist:动态数组、由于实现了RandomAccess标志性接口所以支持快速随机访问

vertor:动态数组,线程安全

linkedList:双向链表结构(1.6之前时循环链表)

(1).ArrayList

**默认初始容量是10:new ArrayList()**为0.初始化为10,也就是说初始其实是空数组 当添加第一个元素的时候数组容量才变成10

ArrayList 的底层是数组队列,相当于动态数组。与 Java 中的数组相比,它的容量能动态增长。

ArrayList扩容机制:每次1.5倍(oldCapacity 为偶数就是 1.5 倍,否则是 1.5 倍左右

*扩容发生在add()方法

将oldCapacity 右移一位,其效果相当于oldCapacity /2,
// 我们知道位运算的速度远远快于整除运算,整句运算式的结果就是将新容量更新为旧容量的1.5倍,
int newCapacity = oldCapacity + (oldCapacity >> 1);

然后检查新容量是否大于最小需要容量,若还是小于最小需要容量,那么就把最小需要容量当作数组的新容量,

这里补充一点比较重要,但是容易被忽视掉的知识点:


System.arraycopy()Arrays.copyOf()方法

阅读源码的话,我们就会发现 ArrayList 中大量调用了这两个方法。比如:我们上面讲的扩容操作以及add(int index, E element)toArray() 等方法中都用到了该方法!


联系:

看两者源代码可以发现 copyOf()内部实际调用了 System.arraycopy() 方法

区别:

arraycopy() 需要目标数组,将原数组拷贝到你自己定义的数组里或者原数组,而且可以选择拷贝的起点和长度以及放入新数组中的位置 copyOf() 是系统自动在内部新建一个数组,并返回该数组。


ArrayList 继承于 AbstractList ,实现了 ListRandomAccessCloneablejava.io.Serializable 这些接口。


ArrayList 和 Vector 的区别?(了解即可)


Arraylist 与 LinkedList 区别?

2.map

HashMap:JDK1.8 之前 HashMap 由数组+链表组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突)。JDK1.8 以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间


LinkedHashMapLinkedHashMap 继承自 HashMap,所以它的底层仍然是基于拉链式散列结构即由数组和链表或红黑树组成。另外,LinkedHashMap 在上面结构的基础上,增加了一条双向链表,使得上面的结构==可以保持键值对的插入顺序==。


treeMap:红黑树


(1).hashmap

1.数据结构:数组+链表+红黑树(当链表长度大于8并且数组长度大于64是,链表会转化为红黑树,主要是为了提升检索效率)

2.其中数组小于64时会进行扩容不会转化为红黑树。在移除数据时,当红黑树的节点移除到剩6个时,将红黑树转换成链表。(选择6个而不是8个,是为了避免频繁进行红黑树和链表的转换,造成性能的损耗。)

3.数组长度是2的n次幂:计算索引时效率更高

4.hashmap的寻址方法:

  1. 计算对象的hashcode()
  2. 调用hash()函数进行二次hash,然后右移动16位进行异或运算(让hash分布的更均匀)
  3. 最后(capocity-1)&hash 得到索引(hashmap的大小-1再&对象的hash)

5.HashMap的put操作过程:

 1. map.put(key, value),首先计算key的hash,得到一个int值。
    2.如果Node数组为空则初始化Node数组。这里注意,Node数组的长度length始终应该是2的n次方,比如默认的16, 还有32,64等
    3.用 hash&(length-1) 运算得到数组下标,这里要提一句,其实正常我们最容易想到的,而且也是我之前很长一段时间以为的,这一步应该进行的是求模运算: hash % length ,这样得到的正好是0~length-1之间的值,可以作为数组的下标, 那么为何此处是位与运算呢?
    先说结论: 上面提到数组的长度length始终是2^n,在这个前提下,hash & (length-1) 与hash % length是等价的。 而位与运算更快。这里后面会另开一遍进行详解。
    4.  如果Node[hash&(length-1)]处为空,用传入的的key, value创建Node对象,直接放入该下标;如果该下标处不为空,且对象为TreeNode类型,证明此下标处的元素们是按照红黑树的结构存储的,将传入的key,value作为新的红黑树的节点插入到红黑树;否则,此处为链表,用next找到链表的末尾,将新的元素插入。如果在遍历链表的过程中发现链表的长度超过了8,此时如果数组长度<64则进行扩容,否则转红黑树。
    5. 如果key的hash和key本身都相等则将该key对应的value更新为新的value
    6. 需要扩容的话则进行扩容。
注意:
    1. 如果key是null则返回的hash为0,也就是key为null的元素一直被放在数组下标为0的位置。
    2. 在JDK 1.8以前,链表是采用的头部插入的方式,从1.8改成了在链表尾部插入新元素的方式。 这么做是为了防止在扩容的时候,多线程时出现循环链表死循环。具体会新开一遍进行详细演绎。

6.HashMap的get操作过程:

1. map.get(key). 首先计算key的hash。
2. 根据hash&(length-1)定位到Node数组中的一个下标。如果该下标的元素(也就是链表/红黑树的第一个元素)中 key的hash的key本身 都和传入的key相同,则证明找到了元素,直接返回即可。
3.如果第一个元素不是要找的,如果第一个元素的类型是TreeNode,则按照红黑树的查找方法查找元素,如果不是则证明是链表,按照next指针找下去,直到找到或者到达队尾。

7.hashmap 数组长度为2的n次幂:

7.1 计算索引时效率更高,%运算的速度并没有&的操作速度快。而&操作能代替%运算,必须满足一定的条件,也就是a%b=a&(b-1)仅当b是2的n次方的时候方能成立

7.2 扩容时重新计算索引的效率更高,hash & oldCap(旧容量) == 0的元素保留在原位置,否则新位置 = 旧位置 + oldCap(旧容量)

8.hashmap寻址方法:

8.1 计算对象的hashCode()

8.2 然后调用hash()进行二次哈希,然后右移16位进行异或运算,让哈希分布的均匀 3.0 最后 (capacity - 1) & hash 得到索引

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

JDK1.7 及之前版本的 HashMap 在多线程环境下扩容操作可能存在死循环问题,这是由于当一个桶位中有多个元素需要进行扩容时,多个线程同时对链表进行操作,头插法可能会导致链表中的节点指向错误的位置,从而形成一个环形链表


JDK1.8 版本的 HashMap 采用了尾插法而不是头插法来避免链表倒置,使得插入的节点永远都是放在链表的末尾,避免了链表中的环形结构。

但是还是不建议在多线程下使用 HashMap,因为多线程下使用 HashMap 还是会存在数据覆盖的问题。并发环境下,推荐使用 ConcurrentHashMap 

10.HashMap 为什么线程不安全?

JDK1.7 及之前版本,在多线程环境下,HashMap 扩容时会造成死循环和数据丢失的问题。

JDK 1.8 后,在 HashMap 中,多个键值对可能会被分配到同一个桶(bucket),并以链表或红黑树的形式存储。多个线程对 HashMapput 操作会导致线程不安全,具体来说会有数据覆盖的风险。


(2).ConcurrentHashMap

JDK1.7

Java 7 ConcurrentHashMap 存储结构

JDK1.8

Java8 ConcurrentHashMap 存储结构(图片来自 javadoop)

数据结构:数组+链表+红黑树

1.8之后采用cas控制数组节点的添加,synchronized锁定链表或者红黑树的首个节点。size在并发激烈的时候,采用数组形式.

怎么保证线程安全:

ConcurrentHashMap 为什么 key 和 value 不能为 null?

ConcurrentHashMap 的 key 和 value 不能为 null 主要是为了避免二义性。null 是一个特殊的值,表示没有对象或没有引用。如果你用 null 作为键,那么你就无法区分这个键是否存在于 ConcurrentHashMap 中,还是根本没有这个键。同样,如果你用 null 作为值,那么你就无法区分这个值是否是真正存储在 ConcurrentHashMap 中的,还是因为找不到对应的键而返回的。

ConcurrentHashMap 能保证复合操作的原子性吗?

复合操作是指由多个基本操作(如putgetremovecontainsKey等)组成的操作,例如先判断某个键是否存在containsKey(key),然后根据结果进行插入或更新put(key, value)。这种操作在执行过程中可能会被其他线程打断,导致结果不符合预期。


如 putIfAbsentcomputecomputeIfAbsent 、computeIfPresentmerge

(3).other

HashMap与HashTable有什么区别?hashmap是线程不安全的,数组+链表+红黑树;hasttable是线程安全的,数组+链表,采用sync实现

fail-fast 机制:多个线程对 fail-fast 集合进行修改的时候,可能会抛出:

fail-fast 机制是java集合(Collection)中的一种错误机制。当多个线程对同一个集合的内容进行操作时,就可能会产生fail-fast事件。其实fail-fast机制并不是Java集合特有的机制,它是一个通用的系统设计思想。

通过反编译你会发现 foreach 语法底层其实还是依赖 Iterator 。不过, remove/add 操作直接调用的是集合自己的方法,而不是 Iteratorremove/add方法

这就导致 Iterator 莫名其妙地发现自己有元素被 remove/add ,然后,它就会抛出一个 ConcurrentModificationException 来提示用户发生了并发修改异常。这就是单线程状态下产生的 fail-fast


Java8 开始,可以使用 Collection#removeIf()方法删除满足特定条件的元素,如

(4).hashtable

hash表,线程安全的,