Java集合(总结)

文章目录

  • Java容器
    • List
      • 一、ArrayList
        • fail-fast(快速失败)
        • 序列化和反序列化
        • Vector和ArrayList的相同和不同
      • 二、LinkedList
        • LinkedList对于查找的优化
      • 三、CopyOnWriteArrayList(有一个属性是ReentrantLock对象)
        • add方法(会加锁)
        • get方法(未加锁)
        • 总结
    • Map
      • 1.8HashMap
      • LinkedHashMap
        • 重要的三个函数
        • 总结
        • 使用LinkedHashMap实现LRU

Java容器

List

一、ArrayList

  • 1、初始大小为10,扩容为现在的1.5
  • 2、扩容使用Arrays.copyOf()把原来的整个数组复制到新的数组中。
  • 3、变量的方式有
    for循环遍历
    增强for循环
    迭代器

fail-fast(快速失败)

使用modCount记录list发生变化(增加删除)的次数

if (modCount != expectedModCount)
    throw new ConcurrentModificationException();

序列化和反序列化

private void readObject(java.io.ObjectInputStream s)
private void writeObject(java.io.ObjectOutputStream s)

Vector和ArrayList的相同和不同

相同点:

  • 底层都是数组实现的
  • 默认长度都是10

不同点:

  • Vector是线程安全的,因为方法上加了Synchronized
  • 扩容容量,Vector是两倍,ArrayList是1.5倍

二、LinkedList

  • LinkedList是一个双向链表实现的List
  • LinkedList是一个双端队列,可以实现队列、双端队列、栈的特点
  • 包含头、尾引用

LinkedList对于查找的优化

若index < 双向链表长度的1/2,则从前向后查找; 否则,从后向前查找。

public E get(int index) 
{
    checkElementIndex(index);
    return node(index).item;
}

Node<E> node(int index) 
{
  // assert isElementIndex(index);

    if (index < (size >> 1)) {
        Node<E> x = first;
        for (int i = 0; i < index; i++)
            x = x.next;
        return x;
    } else {
        Node<E> x = last;
        for (int i = size - 1; i > index; i--)
            x = x.prev;
        return x;
    }
}

三、CopyOnWriteArrayList(有一个属性是ReentrantLock对象)

public class CopyOnWriteArrayList<E>
    implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
    final transient ReentrantLock lock = new ReentrantLock();
    private transient volatile Object[] array;
    
    public CopyOnWriteArrayList() // 构造一个空数组 setArray(new Object[0]);
    public CopyOnWriteArrayList(Collection<? extends E> c) // 将 传入的 Collection 转为Object[] 赋值给 array
	public CopyOnWriteArrayList(E[] toCopyIn) // 将传入的数组 toCopyIn 赋值给 array
}

add方法(会加锁)

先将数组拷贝到一个容量为之前数组容量+1的数组中,其他线程如果并发遍历的时候,可能就是遍历的是原数组,而不是新的数组。

public boolean add(E e)

public void add(int index, E element) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        Object[] elements = getArray();
        int len = elements.length;
        //检查越界情况
        if (index > len || index < 0)
            throw new IndexOutOfBoundsException("Index: "+index+
                                                ", Size: "+len);
        Object[] newElements;
        //移动的元素的个数
        int numMoved = len - index;
        if (numMoved == 0) //插在末尾
            newElements = Arrays.copyOf(elements, len + 1);
        else {
            newElements = new Object[len + 1];
            System.arraycopy(elements, 0, newElements, 0, index);
            System.arraycopy(elements, index, newElements, index + 1,
                             numMoved);
        }
        newElements[index] = element;
        setArray(newElements);
    } finally {
        lock.unlock();
    }
}

public boolean addIfAbsent(E e) 
private boolean addIfAbsent(E e, Object[] snapshot) ---加锁的添加方法,详情看 jdk souorceCode注释

remove和add一样,先复制再删除再赋值。

get方法(未加锁)

public E get(int index) {
    return get(getArray(), index);
}

总结

  • 1、CopyOnWriteArrayList使用ReentrantLock进行枷锁,保证线程安全。
  • 2、CopyOnWriteArrayList的写操作都要先拷贝一份新数组,在新数组中做修改,修改完了再用新数组替换老数组,性能比较低下;
  • 3、CopyOnWriteArrayList的读操作支持随机访问,时间复杂度为O(1);
  • 4、CopyOnWriteArrayList采用读写分离的思想,读操作不加锁,写操作加锁,且写操作占用较大内存空间,所以适用于读多写少的场合;
  • CopyOnWriteArrayList只保证最终一致性,不保证实时一致性;缺陷,对于边读边写的情况,不一定能实时的读到最新的数据

Map

1.8HashMap

参考链接
自己整理的博客

  • 最大容量为2的30次方
  • 当一个桶中元素个数大于等于8的时候树化
  • 当一个桶中元素小于等于6的时候转化成链表
  • 当桶中元素个数达到64的时候才树化
  • 有modCount属性,用于迭代时候执行快速失败操作

====

LinkedHashMap

数组+红黑树+单链表+双向链表
要重写下面的这个函数才能实现LRU,比如重写成:

public boolean removeEldestEntry(Map.Entry<K,V>eldest)
{
	// 当元素个数大于了缓存的容量, 就移除元素
    return size()>this.capacity;
}

参考博客

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

重要的三个函数

  • 1、afterNodeAccess
    在节点被访问后调用,主要在put已经存在的元素的或者get()时候被调用,如果accessOrder为true,调用这个方法把访问到的节点移动到双向链表的末尾
  • 2、afterNodeInsertion
    在HashMap的putVal方法中被调用,可以看到HashMap中这个方法的实现为空,如果evict为true,则移除最老的头节点。
  • 3、afterNodeRemoval
    在节点被删除的时候调用,从双链表中将节点删除。
//HashMap 中,这三个方法都是没实现的,在 LinkedHashMap 中实现来维护结点顺序
//
void afterNodeAccess(Node<K,V> p) { }
void afterNodeInsertion(boolean evict) { }
void afterNodeRemoval(Node<K,V> p) { }

//LinkedHashMap
/*
	在节点访问之后被调用,主要在put()已经存在的元素或get()时被调用,
	如果accessOrder为true,调用这个方法把访问到的节点移动到双向链表的末尾。
*/
void afterNodeAccess(Node<K,V> e) { // move node to last
    LinkedHashMap.Entry<K,V> last;
    // accessOrder = true则执行,否则结束
    // accessOrder = true, e 不是 tail 尾结点
    if (accessOrder && (last = tail) != e) {
        LinkedHashMap.Entry<K,V> p =
            (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.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;
    }
}

/*
	在节点插入之后做些什么,在HashMap中的putVal()方法中被调用,可以看到HashMap中这个方法的实现为空。
	evict:驱逐的意思
	如果 evict 为 true,则移除最老的元素(head)
	默认removeEldestEntry()方法返回false,也就是不删除元素。
*/
void afterNodeInsertion(boolean evict) { // possibly remove eldest
    LinkedHashMap.Entry<K,V> first;
    //如果evict为true,且头节点不为空,且 确定移除最老的元素,即移除 head    
    //head 为 双向链表的头结点
    if (evict && (first = head) != null && removeEldestEntry(first)) {
        K key = first.key;
        //HashMap.removeNode()从HashMap中把这个节点移除之后,会调用 afterNodeRemoval() 方法;
        removeNode(hash(key), key, null, false, true);
    }
}
//传进来的参数 是 双向链表的头结点 (即最老的结点)
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
    return false;
}

/*
	在节点被删除之后调用的方法 afterNodeInsertion -> HashMap.removeNode() -> afterNodeRemoval
	从双向链表中 删除结点 e
*/
void afterNodeRemoval(Node<K,V> e) { // unlink
    LinkedHashMap.Entry<K,V> p =
        (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
    // 把节点p从双向链表中删除。
    p.before = p.after = null;
    if (b == null)
        head = a;
    else
        b.after = a;
    if (a == null)
        tail = b;
    else
        a.before = b;
}

/*
	因此使用 LinkedHashMap 实现 LRU, 
	1) 设置 accessOrder 为 true --> 把最近访问的结点移动到尾部
	2) 重写 removeEldestEntry 方法  --> 返回 true 会删除该结点, false 不删除
*/

总结

(1)LinkedHashMap继承自HashMap,具有HashMap的所有特性;

(2)LinkedHashMap内部维护了一个双向链表存储所有的元素;

(3)如果accessOrder为false,则可以按插入元素的顺序遍历元素;

(4)如果accessOrder为true,则可以按访问元素的顺序遍历元素;

(5)LinkedHashMap的实现非常精妙,很多方法都是在HashMap中留的钩子(Hook),直接实现这些Hook就可以实现对应的功能了,并不需要再重写put()等方法;

(6)默认的LinkedHashMap并不会移除旧元素,如果需要移除旧元素,则需要重写removeEldestEntry()方法设定移除策略;

(7)LinkedHashMap可以用来实现LRU缓存淘汰策略;

使用LinkedHashMap实现LRU

LinkedHashMap如何实现LRU缓存淘汰策略呢?

首先,我们先来看看LRU是个什么鬼。LRU,Least Recently Used,最近最少使用,也就是优先淘汰最近最少使用的元素。

如果使用LinkedHashMap,我们把accessOrder设置为true是不是就差不多能实现这个策略了呢?答案是肯定的。请看下面的代码:

public class LRUTest
{
	public static void main(String[] args)
	{
		LRU<Integer,Integer> lru = new LRU(5,0.75f);
		lru.put(1,1);
		lru.put(2,2);
		lru.put(3,3);
		lru.put(4,4);
		lru.put(5,5);
		lru.put(6,6);
		lru.put(7,7);
		System.out.println(lru.get(4));
		lru.put(6,666);
		System.out.println(lru);
	}
}
class LRU extends LinkedHashMap<K,V>
{
	private int capacity;
	public LRU(int capacity,int loadFactor)
	{
		super(capacity,loadFactor,true);
		this.capacity = capacity;
	}
	/**
	* 重写removeEldestEntry()方法设置何时移除旧元素
    * @param eldest
    * @return 
	*/
	public boolean removeEldestEntry(Map.Entry<K,V>eldest)
	{
		// 当元素个数大于了缓存的容量, 就移除元素
        return size()>this.capacity;
	}
}

你可能感兴趣的:(面试,源码,Java)