Java集合

这篇文章要包括的内容

  • 概览
  • 源码分析
  • 容器的设计模式

总结

这篇文章主要介绍了java集合的相关知识,
从概览到源码,同时结合一些面试题
希望自己能够对容器的使用更加熟练!!!

概览

容器主要包括 Collection 和 Map 两种,Collection 存储着对象的集合,而 Map 存储着键值对(两个对象)的映射表。Collection包括List、Queue、Set
Collection集合的基本功能

boolean add(E e)
boolean remove(Object o)    :移除指定元素
void clear()
boolean contains(Object o)    //判断是否包含给定对象
boolean isEmpty()
int size()
toArray()    将集合转化成 Object 数组

List

List 特有

void add(int index,E element); 往指定位置添加元素, 如果省略索引,则将元素添加到末尾   
E remove(int index)  不能省略索引
E get(int index)
Object obj = list.remove(i);     将删除的元素返回
E set(int index,E element)  修改指定位置的元素,将修改前的元素返回
boolean contains(Object o);    判断是有包含指定元素

LinkedList 特有

public void addFirst(E e)addLast(E e)
public E getFirst()getLast()
public E removeFirst()public E removeLast()
public E get(int index);

使用选择
查询多用ArrayList
增删多用LinkedList
如果都多ArrayList

Set

set的特点:存和取的顺序不一致,没有索引,不能存储重复元素

1、TreeSet:基于红黑树实现,支持有序性操作,例如根据一个范围查找元素的操作。但是查找效率不如HashSet,HashSet 查找的时间复杂度为 O(1),TreeSet 则为 O(logN)。
2、HashSet:基于哈希表实现,支持快速查找,但不支持有序性操作。并且失去了元素的插入顺序信息,也就是说使用 Iterator 遍历 HashSet 得到的结果是不确定的。内部按照哈希顺序存放,保证唯一性(效率高)
3、LinkedHashSet:具有 HashSet 的查找效率,且内部使用双向链表维护元素的插入顺序。存取顺序一致,保证唯一性
TreeSet: 内部有序,保证唯一性,改变构造函数的比较器的内容可以实现存储重复的值,
排序方式有:自然排序(Comparable)和比较器排序(Comparator)

Queue

LinkedList:可以用它来实现双向队列。
PriorityQueue:基于堆结构实现,可以用它来实现优先队列。

Map

基本功能

* a:添加功能
* V put(K key,V value):添加元素。
        * 如果键是第一次存储,就直接存储元素,返回null
        * 如果键不是第一次存在,就用值把以前的值替换掉,返回以前的值
* b:删除功能
    * void clear():移除所有的键值对元素
    * V remove(Object key):根据键删除键值对元素,并把值返回
* c:判断功能
    * boolean containsKey(Object key):判断集合是否包含指定的键
    * boolean containsValue(Object value):判断集合是否包含指定的值
    * boolean isEmpty():判断集合是否为空
* d:获取功能
    Entry<K,V>  entrySet, 获取所有的 entry 对

Map子类的特点

1、Treemap: 基于红黑树实现。 根据其键的自然顺序 进行排序。
2、HashMap: 基于哈希表实现。
3、HashTable: 和 HashMap 类似,但它是线程安全的,这意味着同一时刻多个线程可以同时写入 HashTable 并且不会导致数据不一致。它是遗留类,不应该去使用它。现在可以使用 ConcurrentHashMap 来支持线程安全,并且ConcurrentHashMap 的效率会更高,因为 ConcurrentHashMap 引入了分段锁。
4、LinkedHashMap: 使用双向链表来维护元素的顺序, 顺序为插入顺序或者最近最少使用(LRU)顺序。

源码分析

ArrayList

1、概览
基于数组实现,默认大小为10
2、扩容
需要使用 grow() 方法进行扩容,新容量的大小为 oldCapacity + (oldCapacity >> 1) ,也就是旧容量的 1.5 倍。

Vector

它的实现与 ArrayList 类似,但是使用了 synchronized 进行同步
Vector 是同步的,因此开销就比 ArrayList 要大,访问速度更慢。最好使用 ArrayList 而不是 Vector,因为同步操作完全可以由程序员自己来控制;
Vector 每次扩容请求其大小的 2 倍空间,而 ArrayList 是 1.5 倍。
可以使用 Collections.synchronizedList(); 得到一个线程安全的 ArrayList。
也可以使用 concurrent 并发包下的 CopyOnWriteArrayList 类。

CopyOnWriteArrayList

写操作在一个复制的数组上进行,读操作还是在原始数组中进行,读写分离,互不影响。
写操作需要加锁,防止并发写入时导致写入数据丢失。
写操作结束之后需要把原始数组指向新的复制数组。
但是 CopyOnWriteArrayList 有其缺陷:
内存占用:在写操作时需要复制一个新的数组,使得内存占用为原来的两倍左右;
数据不一致:读操作不能读取实时性的数据,因为部分写操作的数据还未同步到读数组中。
所以 CopyOnWriteArrayList 不适合内存敏感以及对实时性要求很高的场景

LinkedList

基于双向链表实现,使用 Node 存储链表节点信息。

HashMap

1、存储结构
内部包含了一个 Entry 类型的数组 table。
数组链表的结构,使用拉链法来解决冲突

2、扩容
设 HashMap 的 table 长度为 M,需要存储的键值对数量为 N,如果哈希函数满足均匀性的要求,那么每条链表的长度大约为 N/M,因此平均查找次数的复杂度为 O(N/M)。
为了让查找的成本降低,应该尽可能使得 N/M 尽可能小,因此需要保证 M 尽可能大,也就是说 table 要尽可能大。HashMap 采用动态扩容来根据当前的 N 值来调整 M 值,使得空间效率和时间效率都能得到保证。
和扩容相关的参数主要有:capacity、size、threshold 和 load_factor
1、capacity table 的容量大小,默认为 16。需要注意的是 capacity 必须保证为 2 的 n 次方。
2、size 键值对数量
3、threshold size 的临界值,当 size 大于等于 threshold 就必须进行扩容操作。
4、loadFactor 装载因子,table 能够使用的比例,threshold = capacity * loadFactor。
扩容过程
当需要扩容时,令 capacity 为原来的两倍。
扩容使用 resize() 实现,需要注意的是,扩容操作同样需要把 oldTable 的所有键值对重新插入 newTable 中,因此这一步是很费时的。
从 JDK 1.8 开始,一个桶存储的链表长度大于 8 时会将链表转换为红黑树。

3、 与 HashTable 的比较
1、HashTable 使用 synchronized 来进行同步。
2、HashMap 可以插入键为 null 的 Entry。
3、HashMap 的迭代器是 fail-fast 迭代器。
4、HashMap 不能保证随着时间的推移 Map 中的元素次序是不变的。

4、HashMap 的⻓度为什么是 2 的幂次⽅
为了能让 HashMap 存取⾼效,尽量较少碰撞,也就是要尽量把数据分配均匀。我们上⾯也讲到了过
了,Hash 值的范围值-2147483648 到 2147483647,前后加起来⼤概 40 亿的映射空间,只要哈希函数
映射得⽐较均匀松散,⼀般应⽤是很难出现碰撞的。但问题是⼀个 40 亿⻓度的数组,内存是放不下
的。所以这个散列值是不能直接拿来⽤的。⽤之前还要先做对数组的⻓度取模运算,得到的余数才能⽤
来要存放的位置也就是对应的数组下标。这个数组下标的计算⽅法是“ (n - 1) & hash ”。(n 代表
数组⻓度)。这也就解释了 HashMap 的⻓度为什么是 2 的幂次⽅。
比如 n的长度是4 ,n-1的二进制表示为 11 ,那么对hash做&操作就是取最低的两位
也就是说n-1的二进制必须全部是1,这样才能做&运算取到所有位。

ConcurrentHashMap

ConcurrentHashMap 和 HashMap 实现上类似,最主要的差别是 ConcurrentHashMap 采用了分段锁(Segment),每个分段锁维护着几个桶(HashEntry),多个线程可以同时访问不同分段锁上的桶,从而使其并发度更高(并发度就是 Segment 的个数)。
Segment 继承自 ReentrantLock。默认的并发级别为 16,也就是说默认创建 16 个 Segment。

JDK 1.7 使用分段锁机制来实现并发更新操作,核心类为 Segment,它继承自重入锁 ReentrantLock,并发度与Segment 数量相等。
JDK 1.8 使用了 对每个桶的头结点进行加锁来支持更高的并发度,具体的实现方式是 CAS 加上 synchronized。
并且 JDK 1.8 的实现也在链表过长时会转换为红黑树。具体的转换时机是:当数组大小已经超过64并且链表中的元素个数超过默认设定(8个)时,将链表转化为红黑树

JDK 1.8 以后,ConcrrentHashMap 采用的是在HashMap 的基础上对每个桶加锁的形式实现的,取代了原来 segment + ReetrantLock 的形式。分段锁的缺点是 在于分成很多段时会比较浪费内存空间(不连续,碎片化); 操作map时竞争同一个分段锁的概率非常小时,分段锁反而会造成更新等操作的长时间等待(因为加锁操作会涉及到用户态和内核态的切换); Segment的个数限制了并发级别,且随着段的增大,锁的粒度在增大,分段锁的性能会下降,但是 改成 CAS + synchroinzed 后,仍是一个完整的数组,不会有碎片化的问题,而且桶的个数代表了并发度,并发级别也比原来的高。
至于对每个桶进行加锁,选择的是 synchronized 而不是 reetrantLock, 这是因为在锁的粒度比较小的情况下,经过优化的synchronized 会比 ReetrantLock 有更好的性能。因为锁的粒度比较小的情况下,如果使用ReentrantLock则需要节点继承AQS来获得同步支持,增加内存开销,而synchronized则是JVM原生支持的,内存开销较小
目前currentHashMap的应用我在spring的的三级缓存中看到过。用来做第一级缓存的容器。

LinkedHashMap

1、存储结构
继承自 HashMap,因此具有和 HashMap 一样的快速查找特性。内部维护了一个双向链表,用来维护插入顺序或者 LRU 顺序。

accessOrder 决定了顺序,默认为 false,此时维护的是插入顺序。
LinkedHashMap 最重要的是以下用于维护顺序的函数,它们会在 put、get 等方法中调用。

1、void afterNodeAccess(Node<K,V> p) { }

当一个节点被访问时,如果 accessOrder 为 true,则会将该节点移到链表尾部。也就是说指定为 LRU 顺序之后,在每次访问一个节点时,会将这个节点移到链表尾部,保证链表尾部是最近访问的节点,那么链表首部就是最近最久未使用的节点。(类似于维护一个栈,每次把最新访问过的数据放到栈底,最近最久未使用的元素始终呈现在栈顶)

2、void afterNodeInsertion(boolean evict) { }

在 put 等操作之后执行,当 removeEldestEntry() 方法返回 true 时会移除最晚的节点,也就是链表首部节点 first。
evict 只有在构建 Map 的时候才为 false,在这里为 true。
removeEldestEntry() 默认为 false,如果需要让它为 true,需要继承 LinkedHashMap 并且覆盖这个方法的实现,这在实现 LRU 的缓存中特别有用,通过移除最近最久未使用的节点,从而保证缓存空间足够,并且缓存的数据都是热点数据。

利用LinkedHashMap 实现一个LRU缓存

  • 设定最大缓存空间 MAX_ENTRIES 为 3;
  • 使用 LinkedHashMap 的构造函数将 accessOrder 设置为 true,开启 LRU 顺序;
  • 覆盖 removeEldestEntry() 方法实现,在节点多于 MAX_ENTRIES 就会将最近最久未使用的数据移除。
class LRUCache<K, V> extends LinkedHashMap<K, V> {
    private static final int MAX_ENTRIES = 3;
    protected boolean removeEldestEntry(Map.Entry eldest) {
        return size() > MAX_ENTRIES;
    }
    LRUCache() {
        super(MAX_ENTRIES, 0.75f, true);
    }
}
public static void main(String[] args) {
    LRUCache<Integer, String> cache = new LRUCache<>();
    cache.put(1, "a");
    cache.put(2, "b");
    cache.put(3, "c");
    cache.get(1);
    cache.put(4, "d");
    System.out.println(cache.keySet());
}

WeakHashMap

Tomcat 中的 ConcurrentCache 使用了 WeakHashMap 来实现缓存功能。
ConcurrentCache 采取的是分代缓存

  • 经常使用的对象放入 eden 中,eden 使用 ConcurrentHashMap 实现,不用担心会被回收(伊甸园);
  • 不常用的对象放入 longterm,longterm 使用 WeakHashMap 实现,这些老对象会被垃圾收集器回收。
  • 当调用 get() 方法时,会先从 eden 区获取,如果没有找到的话再到 longterm 获取,当从 longterm 获取到就把对象放入 eden 中,从而保证经常被访问的节点不容易被回收。
  • 当调用 put() 方法时,如果 eden 的大小超过了 size,那么就将 eden 中的所有对象都放入 longterm 中,利用虚拟机回收掉一部分不经常使用的对象。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值