java中集合(容器)介绍

提示:本章节为《java面试系列》
上一章:java中IO

容器关系总图

在这里插入图片描述

集合介绍

  • Set
    1、TreeSet:基于红黑树实现,支持有序性操作,例如根据一个范围查找元素的操作。但是查找效率不如 HashSet,HashSet 查找的时间复杂度为 O(1),TreeSet 则为 O(logN)。

    2、HashSet:基于哈希表实现,支持快速查找,但不是有序的。

    3、 LinkedHashSet:具有 HashSet 的查找效率,并且内部使用双向链表维护元素的插入顺序。


  • List
    1、 ArrayList:基于动态数组实现,支持随机访问。

    2、Vector:和 ArrayList 类似,但它是线程安全的。

    3、LinkedList:基于双向链表实现,只能顺序访问,但是可以快速地在链表中间插入和删除元素。不仅如此,LinkedList 还可以用作栈、队列和双向队列。

  • Queue
    1、LinkedList:可以用它来实现双向队列。

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

  • Map
    1、TreeMap:基于红黑树实现。

    2、HashMap:基于哈希表实现。

    3、HashTable:和 HashMap 类似,但它是线程安全的,这意味着同一时刻多个线程同时写入 HashTable 不会导致数据不一致。它是遗留类,不应该去使用它,而是使用 ConcurrentHashMap 来支持线程安全,ConcurrentHashMap 的效率会更高,因为 ConcurrentHashMap 引入了分段锁。
    4、 LinkedHashMap:使用双向链表来维护元素的顺序,顺序为插入顺序或者最近最少使用(LRU)顺序。

注意:
Java中的集合,从上层接口上看分为了两类,Map和Collection。也就是说,我们平时接触到的常用的集合,包括HashMap,ArrayList和HashSet等都直接或者间接的实现了这两个接口之一。而Collection接口的子接口又包括了Set和List接口。这样我们常见的Map,Set和List三大集合接口就出来了。接口类图如下所示:
在这里插入图片描述
Map是和Collection并列的集合上层接口,没有继承关系;List和Set是Collection的子接口。

集合中的设计模式

迭代

iterator()—(增强for循环)

适配器

把数组类型转换为 List 类型。如:
List list = Arrays.asList(3, 4, 5);

底层原理

对于JDK1.8

- ArrayList

1、概要

该集合是基于数组实现的,默认的大小是10,可以“自动”动态扩大容量,提供快速随机访问。

2、扩容

oldCapacity + (oldCapacity >> 1),为旧容量的 1.5 倍。扩容时把原数组复制到新数组,所以空间资源浪费较大。

3、删除元素

时间复杂度为n,移动删除元素后面的元素到前面。

4、序列化

对象转换为字节流并输出,反序列化就是相反。

5、速度

因为没有同步,所以访问等操作较快,但在多线程的情况下,不安全。

6、快速失败

modCount 用来记录 ArrayList 结构发生变化的次数。结构发生变化是指添加或者删除至少一个元素的所有操作,或者是调整内部数组的大小,仅仅只是设置元素的值不算结构发生变化。
在进行序列化或者迭代等操作时,需要比较操作前后 modCount 是否改变,如果改变了需要抛出 ConcurrentModificationException。

安全实现

List<String> list = new ArrayList<>();
List<String> synList = Collections.synchronizedList(list);

也可以使用 concurrent 并发包下的 CopyOnWriteArrayList 类。适合读多写少的应用场景。(但内存浪费,数据不能实时)

List<String> list = new CopyOnWriteArrayList<>();

- Vector

安全

使用关键字synchronized同步,如在add,get方法上。(不排除一些特殊情况,导致不安全。就是说不能说100%安全)

扩容

默认情况扩容时每次都令 capacity 为原来的两倍,具体情况查找源码。

性能

因为加了锁,所以进行操作的时候,要申请锁等资源,所以访问速度较ArrayList差了点,但在多线程的操作情况下一般是安全的。

-LinkedList

1、概要

它是基于双向链表实现的,在jdk1.8中用node存储。它和ArrayList的区别,也是链表与数组的区别。

- HashMap

主要介绍JDK1.7先

1. 存储结构

jdk1.7 数组+链表(jdk1.8 数组+链表+红黑树)

2.工作原理

  • 拉链法
    链表插入是通过头插法方式进行的,
  • 查找
    计算键值对所在的桶;
    在链表上顺序查找,时间复杂度显然和链表的长度成正比。

3.put操作

简单理解就是首先如果key存在,就替换旧值,否则就直接插入。
注意:HashMap 使用第 0 个桶存放键为 null 的键值对。(因为无法调用 null 的 hashCode() 方法)

4.put过程

4.1 桶下标值

确定一个键值对所在的桶下标。如下代码所示:

int hash = hash(key);
int i = indexFor(hash, table.length);
4.2 hash值

计算过程如下:

final int hash(Object k) {
    int h = hashSeed;
    if (0 != h && k instanceof String) {
        return sun.misc.Hashing.stringHash32((String) k);
    }

    h ^= k.hashCode();

    // This function ensures that hashCodes that differ only by
    // constant multiples at each bit position have a bounded
    // number of collisions (approximately 8 at default load factor).
    h ^= (h >>> 20) ^ (h >>> 12);
    return h ^ (h >>> 7) ^ (h >>> 4);
}
public final int hashCode() {
    return Objects.hashCode(key) ^ Objects.hashCode(value);
}
4.3 取模

key 的 hash 值对桶个数取模:hash%capacity。

5. 扩容原理

capacity:table 的容量大小,默认为 16。需要注意的是 capacity 必须保证为 2 的 n 次方。
size:键值对数量。
threshold:size 的临界值,当 size 大于等于 threshold 就必须进行扩容操作。
loadFactor:装载因子,table 能够使用的比例,threshold = (int)(capacity* loadFactor)。

当需要扩容时,令 capacity 为原来的两倍。

扩容使用 resize() 实现,需要注意的是,扩容操作同样需要把 oldTable 的所有键值对重新插入 newTable 中,因此这一步是很费时的。

6. 重新计算桶下标

在进行扩容时,需要把键值对重新计算桶下标,从而放到对应的桶上

7. 计算数组容量

HashMap 构造函数允许用户传入的容量不是 2 的 n 次方,因为它可以自动地将传入的容量转换为 2 的 n 次方。

8.链表转红黑树

JDK 1.8 开始,一个桶存储的链表长度大于等于 8 时会将链表转换为红黑树。

杂谈

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

-ConcurrentHashMap

结构

数组+链表+红黑树基于分段锁(Segment),CAS 。
Segment (默认16)继承自 ReentrantLock。

static final class Segment<K,V> extends ReentrantLock implements Serializable {

    private static final long serialVersionUID = 2249069246763182397L;

    static final int MAX_SCAN_RETRIES =
        Runtime.getRuntime().availableProcessors() > 1 ? 64 : 1;

    transient volatile HashEntry<K,V>[] table;

    transient int count;

    transient int modCount;

    transient int threshold;

    final float loadFactor;
}

size操作

执行 size 操作时,需要遍历所有 Segment 然后把 count 累计起来(基于CAS)。如果尝试的次数超过 3 次,就需要对每个 Segment 加锁。

新改进

JDK 1.7 使用分段锁机制来实现并发更新操作,核心类为 Segment,它继承自重入锁 ReentrantLock,并发度与 Segment 数量相等。

JDK 1.8 使用了 CAS 操作来支持更高的并发度,在 CAS 操作失败时使用内置锁 synchronized。

并且 JDK 1.8 的实现也在链表过长时会转换为红黑树。

-LinkedHashMap

结构

双向链表+红黑树。一个双向链表,用来维护插入顺序或者 LRU 顺序。

实现细节

afterNodeAccess(),afterNodeInsertion(),removeEldestEntry() 等函数感兴趣可以自己查看源码或者查阅资料。

  • LRU 缓存
    。。。

-WeakHashMap

结构

WeakHashMap 的 Entry 继承自 WeakReference,被 WeakReference 关联的对象在下一次垃圾回收时会被回收。

WeakHashMap 主要用来实现缓存,通过使用 WeakHashMap 来引用缓存对象,由 JVM 对这部分缓存进行回收。

private static class Entry<K,V> extends WeakReference<Object> implements Map.Entry<K,V>

-ConcurrentCache

Tomcat 中的 ConcurrentCache 使用了 WeakHashMap 来实现缓存功能。

ConcurrentCache 采取的是分代缓存:

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

    private final int size;

    private final Map<K, V> eden;

    private final Map<K, V> longterm;

    public ConcurrentCache(int size) {
        this.size = size;
        this.eden = new ConcurrentHashMap<>(size);
        this.longterm = new WeakHashMap<>(size);
    }

    public V get(K k) {
        V v = this.eden.get(k);
        if (v == null) {
            v = this.longterm.get(k);
            if (v != null)
                this.eden.put(k, v);
        }
        return v;
    }

    public void put(K k, V v) {
        if (this.eden.size() >= size) {
            this.longterm.putAll(this.eden);
            this.eden.clear();
        }
        this.eden.put(k, v);
    }
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值