Java集合(总结,面试使用)

10 篇文章 0 订阅
7 篇文章 0 订阅

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对象)

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.7 HashMap

put方法总结(一定要看,不在桶里面的话,先判断需不需要扩容(扩容条件:size的值是不是大于等于阈值且那个Hash桶元素不为null))
  • 1、没初始化的时候进行初始化操作
  • 2、如果key为null的话,往桶0中去put元素
  • 3、不为null的话,计算应该放在哪个桶,遍历这个桶,如果存在key相同且hash相同的元素,修改返回原来的值,如果不存在的话。
  • 4、如果值不存在的话,先判断size的值是不是大于等于阈值,如果大于且那个Hash桶元素不为null,则进行扩容,存放元素。否则直接存放元素。
get方法总结
  • 1、如果key为null,则到桶0中去寻找元素
  • 2、如果不为null,先计算在哪个桶,再去那个桶中去找,看是否有key相同且hash值相同的元素

1.8HashMap

参考链接
自己整理的博客

  • 最大容量为2的30次方
  • 当一个桶中元素个数大于等于8的时候树化
  • 当一个桶中元素小于等于6的时候转化成链表
  • 当桶中元素个数达到64的时候才树化
  • 有modCount属性,用于迭代时候执行快速失败操作
put函数总结(1.7 单独判断key是否为null,1.8单独判断桶是否为null)
  • 1、如果没初始化的话,先初始化
  • 2、如果计算出的hash桶的位置为null,那么就会直接放进去
  • 3、如果不为null,如果桶的第一个元素的key值和hash值相同,则直接替换
  • 4、如果第一个不为null,会判断是树还是链表,如果是树,放入树,是链表放入链表(在遍历链表的时候要注意可能要转成红黑树,要求树的长度大于等于8,如果在转树的时候桶容量小于64,则会扩容到64)。
  • 5、插入之后,如果size大于阈值,则进行resize操作。

ConcurrentHashMap 1.7

put(总结,面试看懂这个即可)

定位Segment:(((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE

定位HashEntry:(((tab.length - 1) & h)) << TSHIFT) + TBASE

(先计算在哪个Entry(hash值的高sshift位和size-1进行按位与),再计算在Entry的哪个位置

put操作(先计算在哪个Entry(hash值的高sshift位和size-1进行按位与),再计算在Entry的哪个位置
  • 1、ConcurrentHashMap中包含Segment数组,每个Segment中又包含HashEntry数组,Segment继承自ReentrantLock
    在JDK1.7中,Java使用分段锁机制实现ConcurrentHashMap,在ConcurrentHashMap对象中有一个Segment数组(Segment类继承ReentrantLock类),即将整个Hash表划分成了多个Segment分段,即每个分段则类似于一个Hashtable,这样在执行put操作的时候首先根据hash算法定位到属于哪个Segment,然后对Segment加锁即可。因此ConcurrentHashMap在多线程并发执行的过程中实现了多线程put操作。

  • 2、segmentShift用于定位参与散列运算的位数,其等于32减去sshift,使用32是因为ConcurrentHashMap的hash()方法返回的最大数是32位的,hash >>> segmentShift所以就会只剩下低sshift位为1,其余都为0,因此,求key散列到长度为ssize的Segment数组的下标j,就是求key的hash值的高sshift位,总的来说,计算在哪个Segment就是计算hash的高sshift位和hash值进行按位与。

总的来说,计算在Segment的哪个位置就是计算Segment的高sshift位与segmentMask进行按位与得到元素位于位于Segment中的下标。

@SuppressWarnings("unchecked")
public ConcurrentHashMap(int initialCapacity,
                         float loadFactor, int concurrencyLevel) {
    if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0)
        throw new IllegalArgumentException();
    if (concurrencyLevel > MAX_SEGMENTS)
        concurrencyLevel = MAX_SEGMENTS;
    // Find power-of-two sizes best matching arguments
    
    int sshift = 0;
    //Segment的大小,分段数组的大小,如果小于concurrencyLevel
    //会被调整成大于或等于concurrencyLevel的最小的2的N次方值来作为segments数组的长度
    //2^sshift=ssize  
    int ssize = 1; 
    while (ssize < concurrencyLevel) {
        ++sshift;
        ssize <<= 1;
    }
    //一个键值对在Segment数组中下标j的计算公式为:
    //比如ssize=16,则segmentMask为1111b,
    //j = (hash >>> segmentShift) & segmentMask
  	//2^sshift=ssize  
    this.segmentShift = 32 - sshift;
    this.segmentMask = ssize - 1;
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
    int c = initialCapacity / ssize;
    if (c * ssize < initialCapacity)
        ++c;
    int cap = MIN_SEGMENT_TABLE_CAPACITY;
    while (cap < c)
        cap <<= 1;
    // create segments and segments[0]
    Segment<K,V> s0 =
        new Segment<K,V>(loadFactor, (int)(cap * loadFactor),
                         (HashEntry<K,V>[])new HashEntry[cap]);
    Segment<K,V>[] ss = (Segment<K,V>[])new Segment[ssize];
    UNSAFE.putOrderedObject(ss, SBASE, s0); // ordered write of segments[0]
    this.segments = ss;
}

get操作(不加锁)

JDK1.7的ConcurrentHashMap的get操作是不加锁的,因为在每个Segment中定义的HashEntry数组和在每个HashEntry中定义的value和next HashEntry节点都是volatile类型的,volatile类型的变量可以保证其在多线程之间的可见性,因此可以被多个线程同时读,从而不用加锁。而其get操作步骤也比较简单,定位Segment –> 定位HashEntry –> 通过getObjectVolatile()方法获取指定偏移量上的HashEntry –> 通过循环遍历链表获取对应值。

size函数

ConcurrentHashMap的size操作的实现方法也非常巧妙,一开始并不对Segment加锁(遍历segment),而是直接尝试将所有的Segment元素中的count相加,这样执行两次,然后将两次的结果对比,如果两次结果相等则直接返回;而如果两次结果不同,则再将所有Segment加锁,然后再执行统计得到对应的size值。

ConcurrentHashMap 1.8

sizeCtl
  • 1、-1,表示有线程正在进行初始化操作
  • 2、-(1 + nThreads),表示有n个线程正在一起扩容
  • 3、0,默认值,后续在真正初始化的时候使用默认容量
  • 4、> 0,初始化或扩容完成后下一次的扩容门槛
put总结
  • 1、如果桶数组未初始化,则初始化;
  • 2、如果待插入的元素所在的桶为空,则尝试把此元素直接插入到桶的第一个位置(通过CAS,初始化的时候也使用CAS锁控制只有一个线程初始化桶数组,sizeCtl在初始化后存储的是扩容门槛);
  • 3、如果正在扩容,则当前线程一起加入到扩容的过程中;
  • 4、如果待插入的元素所在的桶不为空且不在迁移元素,则锁住这个桶(分段锁);
  • 5、如果当前桶中元素以链表方式存储,则在链表中寻找该元素或者插入元素(需要判断是否要转化成红黑树);
  • 6、如果当前桶中元素以红黑树方式存储,则在红黑树中寻找该元素或者插入元素;
  • 7、如果元素存在,则返回旧值;
  • 8、如果元素不存在,整个Map的元素个数加1,并检查是否需要扩容(每次添加元素后,元素数量加1,并判断是否达到扩容门槛,达到了则进行扩容或协助扩容,判断是否需要扩容使用的LongAdder思想。);

使用的锁有自旋锁+CAS+synchronized+分段锁(相当于一个Node一个锁)

为什么使用synchronized而不是ReentrantLock?

  • 因为synchronized已经得到了极大地优化,在特定情况下并不比ReentrantLock差。

在判断是否需要扩容的时候,使用的是LongAdder的思想。

一些难点

(1)新桶数组大小是旧桶数组的两倍;

(2)迁移元素先从靠后的桶开始;

(3)迁移完成的桶在里面放置一ForwardingNode类型的元素,标记该桶迁移完成;

(4)迁移时根据hash&n是否等于0把桶中元素分化成两个链表或树;

(5)低位链表(树)存储在原来的位置;

(6)高们链表(树)存储在原来的位置加n的位置;

(7)迁移元素时会锁住当前桶,也是分段锁的思想;

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;
	}
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
提供的源码资源涵盖了Java应用等多个领域,每个领域都包含了丰富的实例和项目。这些源码都是基于各自平台的最新技术和标准编写,确保了在对应环境下能够无缝运行。同,源码配备了详细的注释和文档,帮助用户快速理解代码结构和实现逻辑。 适用人群: 适合毕业设计、课程设计作业。这些源码资源特别适合大学生群体。无论你是计算机相关专业的学生,还是对其他领域编程感兴趣的学生,这些资源都能为你提供宝贵的学习和实践机会。通过学习和运行这些源码,你可以掌握各平台开发的基础知识,提升编程能力和项目实战经验。 使用场景及目标: 在学习阶段,你可以利用这些源码资源进行课程实践、课外项目或毕业设计。通过分析和运行源码,你将深入了解各平台开发的技术细节和最佳实践,逐步培养起自己的项目开发和问题解决能力。此外,在求职或创业过程,具备跨平台开发能力的大学生将更具竞争力。 其他说明: 为了确保源码资源的可运行性和易用性,特别注意了以下几点:首先,每份源码都提供了详细的运行环境和依赖说明,确保用户能够轻松搭建起开发环境;其次,源码的注释和文档都非常完善,方便用户快速上手和理解代码;最后,我会定期更新这些源码资源,以适应各平台技术的最新发展和市场需求。 所有源码均经过严格测试,可以直接运行,可以放心下载使用。有任何使用问题欢迎随与博主沟通,第一间进行解答!

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值