【面试必备】Java容器

容器主要包含两类

 

Collection及Map,前者存储对象,后者存储键值对。

 

Collection

 

Set:

TreeSet:基于红黑树实现,支持有序操作。如根据范围查找元素,但效率不如HashSet,复杂度为O(logN)

HashSet:基于哈希表实现,支持快速查找,不支持有序操作。不维护元素的插入先后顺序信息。

LinkedHashSet:和hashSet一样的查找效率,而且内部采用双向链表维护插入顺序。

 

List:

ArrayList:采用动态数组实现,支持随机访问。

Vector:与ArrayList类似,但是是线程安全的。

LinkedList:基于双向链表实现,只能顺序访问,可以实现快速插入删除,可以用作栈,队列,双向队列。

 

Queue:

LinkedList:用以实现双向队列

PriorityQueue:基于堆结构实现,实现优先队列。

 

Map

 

TreeMap:基于红黑树实现

HashMap:基于哈希表实现

HashTable:与HashMap类似,但是线程安全。现在使用ConcurrentHashMap代替此类。

LinkedHashMap:使用双向链表维护插入顺序,插入顺序或RLU(最近最少使用)顺序

 

 

迭代器模式

 

对于Collection接口的实现都可以采用迭代器进行遍历

其中,增强for就是内置了迭代器:

List<String> list = new ArrayList<>();
list.add("a");
list.add("b");
for (String item : list) {
    System.out.println(item);
}

 

适配器模式

 

可以将数组转换成List

Integer[] arr = {1, 2, 3};
List list = Arrays.asList(arr);

List list = Arrays.asList(1, 2, 3);

 

 

ArrayList

 

支持随机访问(来自RandomAccess接口),基于数组实现的。

public class ArrayList<E> extends AbstractList<E> 

implements List<E>, RandomAccess, Cloneable, java.io.Serializable

数组默认大小10

private static final int DEFAULT_CAPACITY = 10;

添加元素的时候,使用EnsureCapacityInternal确保容量足够

如果容量不够,需要扩容,扩展到原来大小的1.5倍:old + (old >>1)

扩容的时候会将原来的数组复制到新的数组,势必造成耗时,所以如果预知数组大小,尽量提前指定。

删除元素,是将待删除元素后面的元素复制到当前的位置,也非常耗时:

不支持并发增删元素

在操作前保存结构变化次数,相当于一个version。操作后比较一下,如果不匹配,将抛出ConcurrentModificationException异常。

序列化与反序列化均只涉及到已经用到的数组位置,不是将数组直接序列化:

 反序列化:

序列化:

使用ObjectOutputStream包装FileOutputStream进行序列化,ObjectInputStream包装FileInputStream进行反序列化

ArrayList list = new ArrayList();
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(file));
oos.writeObject(list);

 

Vector

 

与ArrayList实现类似,但是采用Synchronized进行同步:

Vector每次扩容的大小是原来的2倍

想要实现同步的List,可以采用以下两种替代方案:

1.通过Collections的synchronizedList方法获取一个同步的List

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

2. 使用concurrent包下的CopyOnWriteArrayList

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

 

CopyOnWriteArrayList

 

实现读写分离,写操作在一个复制的数组上进行,读操作还是在原来的数组上进行。

写操作需要加锁。

写操作结束后,将原来的数组指向操作后的数组。

public boolean add(E e) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        Object[] elements = getArray();
        int len = elements.length;
        Object[] newElements = Arrays.copyOf(elements, len + 1);
        newElements[len] = e;
        setArray(newElements);
        return true;
    } finally {
        lock.unlock();
    }
} 

final void setArray(Object[] a) {
    array = a;
}
@
SuppressWarnings("unchecked")
private E get(Object[] a, int index) {
    return (E) a[index];
}

适合读多写少的情况。

 

缺点:

内存占用是原来的两倍

读取的数据实时性不够,新插入的数据是读不到的。

 

LinkedList

 

使用双向链表保存次序信息

private static class Node<E> {
        E item;
        Node<E> next;
        Node<E> prev;
}

每个链表存储着首指针和尾指针:

transient Node<E> first;
transient Node<E> last; 

 

 

HashMap

 

内部维护着Entry数组:

transient Entry[] table;

每个Entry包含四个字段:

hashCode、next、key、value

采用拉链的方式,对于相同hash与桶模运算结果的键值对通过链表存储在相同的数组位置:

 

static class Entry<K,V> implements Map.Entry<K,V> {
    final K key;
    V value;
    Entry<K,V> next;
    int hash;

    Entry(int h, K k, V v, Entry<K,V> n) {
        value = v;
        next = n;
        key = k;
        hash = h;
    } 

    public final K getKey() {
        return key;
    } 

    public final V getValue() {
        return value;
    } 

    public final V setValue(V newValue) {
        V oldValue = value;
        value = newValue;
        return oldValue;
    } 

    public final boolean equals(Object o) {
        if (!(o instanceof Map.Entry))
            return false;
        Map.Entry e = (Map.Entry)o;
        Object k1 = getKey();
        Object k2 = e.getKey();
        if (k1 == k2 || (k1 != null && k1.equals(k2))) {
            Object v1 = getValue();
            Object v2 = e.getValue();
            if (v1 == v2 || (v1 != null && v1.equals(v2)))
            return true;
        } 
        return false;
    }
    
    public final int hashCode() {
        return Objects.hashCode(getKey()) ^ Objects.hashCode(getValue());
    } 

    public final String toString() {
        return getKey() + "=" + getValue();
    }
}

 

采用拉链式存储:

HashMap<String, String> map = new HashMap<>();
map.put("K1", "V1");
map.put("K2", "V2");
map.put("K3", "V3");

默认桶的大小为16,

插入K1的时候,计算位置是hash%16 位置是3

插入K2,计算位置是6

插入K3的位置也是6,这时采用头插的方式,将元素插到K2的前面。

 

插入元素:

键为空会强制插入到0的位置。

找出是否存在键为key的键值对,存在直接更新。否则执行头插法。

确定位置:

static int indexFor(int h, int length) {
        return h & (length-1);
}

因为桶的长度是可以保证为2的指数次大小,所以上述的与运算可以代替取模运算。

 

扩容:

 

当数组元素个数达到临界,需要扩容。

容量扩大为原来的两倍:

扩容还是将原有的数组复制:

void resize(int newCapacity) {
    Entry[] oldTable = table;
    int oldCapacity = oldTable.length;
    if (oldCapacity == MAXIMUM_CAPACITY) {
        threshold = Integer.MAX_VALUE;
        return;
    } 
    Entry[] newTable = new Entry[newCapacity];
    transfer(newTable);
    table = newTable;
    threshold = (int)(newCapacity * loadFactor);
} 

void transfer(Entry[] newTable) {
    Entry[] src = table;
    int newCapacity = newTable.length;
    for (int j = 0; j < src.length; j++) {
        Entry<K,V> e = src[j];
        if (e != null) {
            src[j] = null;
            do {
                Entry<K,V> next = e.next;
                int i = indexFor(e.hash, newCapacity);
                e.next = newTable[i];
                newTable[i] = e;
                e = next;
            } while (e != null);
        }
    }
}

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

 

与HashTable相比:

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

 

ConcurrentHashMap

 

与HashMap非常相似,

但采用分段锁提高并发访问的性能。

final Segment<K,V>[] segments;

默认并发数量16:

static final int DEFAULT_CONCURRENCY_LEVEL = 16;

求size的时候是把所有的分段锁里维护的键值对数量count进行累加计算得到:

static final int RETRIES_BEFORE_LOCK = 2;
public int size() {
// Try a few times to get accurate count. On failure due to
// continuous async changes in table, resort to locking.
    final Segment<K,V>[] segments = this.segments;
    int size;
    boolean overflow; // true if size overflows 32 bits
    long sum; // sum of modCounts
    long last = 0L; // previous sum
    int retries = -1; // first iteration isn't retry
    try {
        for (;;) {
    // 超过尝试次数,则对每个 Segment 加锁
            if (retries++ == RETRIES_BEFORE_LOCK) {
                for (int j = 0; j < segments.length; ++j)
                ensureSegment(j).lock(); // force creation
            } 
            sum = 0L;
            size = 0;
            overflow = false;
            for (int j = 0; j < segments.length; ++j) {
                Segment<K,V> seg = segmentAt(segments, j);
                if (seg != null) {
                    sum += seg.modCount;
                    int c = seg.count;
                    if (c < 0 || (size += c) < 0)
                        overflow = true;
                    }
                } 
            // 连续两次得到的结果一致,则认为这个结果是正确的
            if (sum == last)
                break;
            last = sum;
        }
    } finally {
        if (retries > RETRIES_BEFORE_LOCK) {
            for (int j = 0; j < segments.length; ++j)
                segmentAt(segments, j).unlock();
            }
    } 
    return overflow ? Integer.MAX_VALUE : size;
}

两次读取数据一致,认为是正确的,否则执行第三次,超过三次要加锁。

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

 

LinkedHashMap

 

继承自HashMap,具备告诉查询性能。

public class LinkedHashMap<K,V> extends HashMap<K,V> implements Map<K,V>

内部是用双向链表维护插入次序:

/**
* The head (eldest) of the doubly linked list.
*/
transient LinkedHashMap.Entry<K,V> head;
/**
* The tail (youngest) of the doubly linked list.
*/
transient LinkedHashMap.Entry<K,V> tail;

次序分为两种:

插入次序或最近最少使用次序,默认是插入次序

final boolean accessOrder; 

使用以下函数维护次序:

如果是最近最少使用次序,在afterNodeAccess后会将元素插入到链表的末尾,使得最少使用的在链表头部,最近使用的在尾部

void afterNodeAccess(Node<K,V> e) { // move node to last
    LinkedHashMap.Entry<K,V> last;
    if (accessOrder && (last = tail) != e) {
        LinkedHashMap.Entry<K,V> p = (LinkedHashMap.Entry<K,V>)e, b = p.before, a = after;
        p.after = null;
        if (b == null)
            head = a;
        else
            b.after = a;
        if (a != null)
            a.before = b;
        else
            last = b;
        if (last == null)
            head = p;
        else {
            p.before = last;
            last.after = p;
        } 
        tail = p;
        ++modCount;
    }
}

插入元素以后,检测是否移除最近不使用的节点:

void afterNodeInsertion(boolean evict) { // possibly remove eldest
    LinkedHashMap.Entry<K,V> first;
    if (evict && (first = head) != null && removeEldestEntry(first)) {
        K key = first.key;
        removeNode(hash(key), key, null, false, true);
    }
}

默认情况下evict在构建Map的时候是false,其他是true

removeEldestEntry默认是false,可以通过继承LinkedHashMap覆盖方法实现

protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
    return false;
}

 

LRU的一种实现:

对超过3个元素的时候,移除最不常用的元素

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);
    }
}

测试:

 

WeakHashMap

 

继承自weakReference,用以实现缓存,被其引用的对象下一次垃圾回收期间会被垃圾回收。

JVM的ConcurrentCache实现原理就是如此。

将经常使用的对象放置到eden中,使用ConcurrentHashMap实现,不会轻易被垃圾回收。

不常使用的放入longterm,使用weakHashMap实现,会被垃圾回收。

调用get时,先到eden获取,找不到回去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);
    }
}


 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值