腾讯、阿里、华为面试必问【容器】知识点

}

//3.指定初始已有数据
public ArrayList(Collection<? extends E> c) {
…//省略代码
elementData = Arrays.copyOf(elementData, size, Object[].class);
}

先介绍有几种初始化的方式,然后在说 add 操作。

2、当第一次调用 add(E) 函数存入数据,先取得最小可以扩容的大小 minCapacity = 10

3、如果我们希望的最小容量大于目前数组的长度(默认是空数组),那么就扩容

4、第一次扩容由于 elementData 其实是一个空数组,也就是 size 为 0 的数组,所以直接将默认 DEFAULT_CAPACITY=10 赋值给当前 newCapacity 新的扩容大小。

5、最后通过 System.arraycopy 函数,进行实例化一个默认大小为 10 的空数组。

6、如果当我们在 size = 10 的情况下,add[11],那么就会基于该公式检查 (size + 1)- size > 0 ,如果成立就会进行第二次扩容。

7、第二次扩容机制,是有一个计算公式,其实第一次也有只是可以忽略不计,因为算出来是 0 。大白话的计算公式为 当前数组大小 + (当前数组大小 / 2) = 10 + 5 也就是第二次扩容大小为 15 。

8、最后直接将需要添加的数据以 elementData[size++] = e 形式添加。

这就是一个详细的添加和扩容过程。这里也可以用一张图来进行说明(详细代码我就不贴了,主要讲解以什么思路来回答面试官),如下所示:


面试官:

嗯,添加和扩容机制了解挺细致的。 下面在说一下,ArrayList 的删除吧。

  • 删除机制 删除的话,ArrayList 给我们提供了 remove(index) 和 remove(obj) 等方式,平时我在项目中用的最多的就是这 2 个 API , 下面我分别来说下它们底层实现方式吧:

remove(index):

//根据数组下标去删除
public E remove(int index) {//比如 index = 5,size = 10
rangeCheck(index);

modCount++;
E oldValue = elementData(index);//[1,2,3,4,5,6,7,8,9,10] //删除 6

int numMoved = size - index - 1;//4
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);//直接从 [7,8,9,10] 往前移动一位
elementData[–size] = null; // clear to let GC do its work

return oldValue;
}

这个 API 它的意思就是根据索引来删除

  1. 检查索引是否越界

  2. 根据索引拿到删除的数据

  3. 将 index + 1 作为移动的起点, size - index -1 作为需要移动数组的长度

  4. 利用 System.arraycopy 数组,将后面的数据向前移动一位,并将最后一位置空。

  5. 最后返回删除的节点数据

remove(obj):

// 根据值去删除
public boolean remove(Object obj) {
// 如果值是空的,找到第一个值是空的删除
if (o == null) {
for (int index = 0; index < size; index++)
if (elementData[index] == null) {
fastRemove(index);
return true;
}
} else {
// 值不为空,找到第一个和入参相等的删除
for (int index = 0; index < size; index++)
// 这里是根据 equals 来判断值相等的
if (o.equals(elementData[index])) {
fastRemove(index);
return true;
}
}
return false;
}

这个 API 是删除 ArrayList 里面匹配到的对象

  1. 如果需要删除的 obj = null 那么就遍历集合将 elementData[index] == null 的节点全部删除,删除原理同 remove(index) 一样。

  2. 如果条件 1 不成立,那么遍历,判断 2 个对象的地址值是否指向同一块内存,如果成立,那么就删除,删除原理同 remove(index) 一样。

这里询问一下面试官,我描述的是否清晰? 面试官如果没有懂,那么我们就可以现场画一个图,如下所示:

面试官:

嗯,是的, 删除原理是这样的!

程序员:

但是在开发中,使用 ArrayList 还是有几点注意事项,比如:
1、不能使用 for 来进行删除,因为每删除一个对象 ,底层的索引对应关系就会发生改变, 导致会删除异常。解决的办法应该使用迭代器。
2、多线程并发也不能使用 ArrayList ,应该使用 CopyOnWriteArrayList 或者 Collections.synchronizedList(list) 来解决线程安全问题。
3、还有性能问题,添加多,查找少,应该选择 LinkedList 数据结构。避免频繁对数组的 copy

其实到这里,ArrayList 基本原理就介绍的差不多了,面试官也不可能每个 API 都问,一般都是问常用的。

2、有使用过 LinkedList 吗?说一下它的底层实现 ?

程序员:

有用过,它的底层数据结构是双向链表组成, 我还是画一下它的结构图吧。如下所示:

图解:

  • 链表每个节点我们叫做 Node,Node 有 prev 属性,代表前一个节点的位置,next 属性,代表后一个节点的位置;

  • first 是双向链表的头节点,它的前一个节点是 null。

  • last 是双向链表的尾节点,它的后一个节点是 null;

  • 当链表中没有数据时,first 和 last 是同一个节点,前后指向都是 null;

面试官:

嗯,基本架构差不多是这样,那你说说底层的 add , get ,remove 原理吧。

程序员:

好的,那我就依次来说一下它们各自实现机制。

add:

//添加数据
public boolean add(E e) {
linkLast(e);
return true;
}
// 从尾部开始追加节点
void linkLast(E e) {
// 把尾节点数据暂存
final Node l = last;
//新建新的节点,l 是前一个节点,e 是当前节点的值,后一个节点是 null
final Node newNode = new Node<>(l, e, null);
//新建的节点放在尾部
last = newNode;
//如果链表为空,头部和尾部是同一个节点,都是新建的节点
if (l == null)
first = newNode;
//否则把前尾节点的下一个节点,指向当前尾节点。
else
l.next = newNode;
//大小和版本更改
size++;
modCount++;
}

添加数据我常用的是 add(E) API ,我就基于它来说吧,它是基于尾结点来添加数据的, 总得来说有如下几个步骤:

  1. 拿到 Last 尾结点,赋值给一个临时变量 l 。

  2. 生成一个新的 newNode= Node[l,item,null] 的数据类型,l 代表的就是连接上一个尾结点,item 就是存入的数据,null 代表的是 item 的尾结点指向。

  3. 将生成的 newNode 赋值给 last 尾结点,这一步相当于更新 last 数据。

  4. 最后是判断临时变量的 l 是否为空,如果为空说明链表中还没有数据,然后将 newNode 赋值给 first 头节点。反之将 newNode 赋值给 l.last 节点。

  5. 最后更新链表 size ,还有修改的版本 modCount

可以由一个动图来说明以上步骤的操作, 如下所示:

添加的增加过程就是这样,还是比较简单,都是操作移动节点数据。

get:

public E get(int index) {
checkElementIndex(index);
return node(index).item;
}
// 根据搜索因为查询节点
Node node(int index) {
// index 处于队列的前半部分,从头开始找
if (index < (size >> 1)) {
Node x = first;
// 直到 for 循环到 index 的前一个 node
for (int i = 0; i < index; i++)
x = x.next;
return x;
} else {// index 处于队列的后半部分,从尾开始找
Node x = last;
// 直到 for 循环到 index 的后一个 node
for (int i = size - 1; i > index; i–)
x = x.prev;
return x;
}
}

获取数据我比较常用的是 get(index) API,我也基于它来说吧,总体来说它是分为两部分来查找数据,步骤有如下几步:

  1. 检查 index 是否越界。

  2. 根据 index > 或 < (size >> 1) 来判断在链表的上半部分还是下半部分。

  3. 如果是上半部分直接从 first 头节点开始遍历 index 次,然后拿到节点 item 数据。反之从下半部分 last 尾部开始向前遍历 index 次,然后拿到节点 item 数据。

总得来说获取数据的步骤就这 3 大步,因为它是链表结构没有索引对应关系,取数据只能挨个遍历。所以,如果对取数据操作频繁也可以使用 ArrayList 来弥补不足的性能。

remove:

删除数据,我常用的是 remove() 或者 remove(index) 它们的区别就是一个从头删除节点,一个是指定 index index 来删除节点,我就直接说一下根据索引删除的原理吧。

  1. 还是检查索引是否越界。

  2. 搜索节点上的数据原理同 get 的第二小点一样,都是分段搜索。

  3. 拿到当前需要删除的 node , 然后处理当前节点上的 prev,item , next 节点指向位置,还有将当前 item 的指针全部置空,避免内存泄漏。

删除也是 3 大步骤,相较于 ArrayList 删除 API 来说,LinkedList 删除的性能是比 ArrayList 要高的。 所以,如果有 增加和删除操作比较频繁的可以选择 LinkedList 数据结构。

3、你在工作中对 ArrayList 和 LinkedList 是怎么选型的?

程序员:

如果项目中有需要快速的查找匹配,但是新增删除不频繁我一般使用的是 ArrayList 数组结构,但是如果查询比较少,新增和删除比较多我一般用的是 LinkedList 链表结构。(ps:结合它们的原理回答为什么)

4、 ArrayList 在多线程使用应该注意什么?

程序员:

在多线程使用 List 要注意线程安全问题,解决的办法通常有两种来解决。第一种也是最简单的一种直接使用 Collections.synchronizedList(list) ,但是其性能不好,因为它的实现原理相当于委托模式,交于另一个类来处理,而且内部将每个函数都加了 synchronized , 另一种实现是 java.util.concurrent##CopyOnWriteArrayList 。

面试官:

那你用过 CopyOnWriteArrayList 吗?它是怎么实现线程安全的 ?

程序员:

有用过,它的基本原理和 ArrayList 是一致的,底层也是基于数组实现。它的基本特性总结有以下几点:

1、线程安全的,多线程环境下可以直接使用,无需加锁;

2、通过锁 + 数组拷贝 + volatile 关键字保证了线程安全;

3、每次数组操作,都会把数组拷贝一份出来,在新数组上进行操作,操作成功之后再赋值回去。

线程安全我从源码的 add 、remove 的实现来说一下它们各自怎么保证线程安全的吧?

这里一定要思路清晰,最好主动找常用的 API 来说明一下内部怎么实现线程安全,不要直接给答案,一定要先分析源码,最后给出总结。

面试官:

可以,那你分别说一下吧。

程序员:

1、底层数组是如何保证数据安全的?

在分析底层数组是如何保证其安全性之前,我先简单说一下 Java 内存模型,因为数组的线程安全会涉及到 volatile 关键字。

volatile 的意思是可见的,常用来修饰某个共享变量,意思是当共享变量的值被修改后,会及时通知到其它线程上,其它线程就能知道当前共享变量的值已经被修改了。

在多核 CPU 下,为了提高效率,线程在拿值时,是直接和 CPU 缓存打交道的,而不是内存。主要是因为 CPU 缓存执行速度更快,比如线程要拿值 C,会直接从 CPU 缓存中拿, CPU 缓存中没有,就会从内存中拿,所以线程读的操作永远都是拿 CPU 缓存的值。

这时候会产生一个问题,CPU 缓存中的值和内存中的值可能并不是时刻都同步,导致线程计算的值可能不是最新的,共享变量的值有可能已经被其它线程所修改了,但此时修改是机器内存的值,CPU 缓存的值还是老的,导致计算会出现问题。

这时候有个机制,就是内存会主动通知 CPU 缓存。当前共享变量的值已经失效了,你需要重新来拉取一份,CPU 缓存就会重新从内存中拿取一份最新的值。

volatile 关键字就会触发这种机制,加了 volatile 关键字的变量,就会被识别成共享变量,内存中值被修改后,会通知到各个 CPU 缓存,使 CPU 缓存中的值也对应被修改,从而保证线程从 CPU 缓存中拿取出来的值是最新的。

还是画一个图来说明一下:

从图中我们可以看到,线程 1 和线程 2 一开始都读取了 C 值,CPU 1 和 CPU 2 缓存中也都有了 C 值,然后线程 1 把 C 值修改了,这时候内存的值和 CPU 2 缓存中的 C 值就不等了,内存这时发现 C 值被 volatile 关键字修饰,发现其是共享变量,就会使 CPU 2 缓存中的 C 值状态置为无效,CPU 2 会从内存中重新拉取最新的值,这时候线程 2 再来读取 C 值时,读取的已经是内存中最新的值了。

volatile 原理知道了,那么通过源码我们知道 object[] 就是通过 volatile 关键字来修饰的,那么也就保证了它在内存中是可见的,具有安全的。

public class CopyOnWriteArrayList
implements List, RandomAccess, Cloneable, java.io.Serializable {
// volatile 关键字修饰,可见的
// array 只开放出 get set
private transient volatile Object[] array;
final Object[] getArray() {
return array;
}
//更新底层数组内存地址
final void setArray(Object[] a) {
array = a;
}
public CopyOnWriteArrayList() {
setArray(new Object[0]);
}

2、操作 add(E) API 是怎么保证数据安全的?

根据之前看源码,它是有如下几个步骤保证了操作 add(E) 的安全性。

// 添加元素到数组尾部
public boolean add(E e) {
final ReentrantLock lock = this.lock;
//加锁
lock.lock();
try {
// 得到所有的原数组
Object[] elements = getArray();
int len = elements.length;
//拷贝到新数组里面,新数组的长度是 + 1 的
Object[] newElements = Arrays.copyOf(elements, len + 1);
//在新数组中进行赋值,新元素直接放在数组的尾部
newElements[len] = e;
//替换原来的数组
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}

  • 通过可重入互斥锁 ReentrantLock对 add(E) 加锁

  • 通过 getArray() 方法得到已经存在的数组

  • 实例化一个长度为当前 size + 1 的数组,然后将 getArray 的数组放入新数组中

  • 最后将添加的数据存入新数组的最后索引中

  • 基于当前类中的 setArray(newElements); 来替换缓存中的数组数据,因为它在类中中被 volatile 修饰了,所以只要内存地址一变,那么就会立马通知,其它 CPU 的缓存让它得到更新。

  • 释放可重入互斥锁

所以 add(E) 方法是根据 ReentrantLock + 数组copy + update Object[] 内存地址 + volatile 来保证其数据安全性的。

面试官:

你刚刚说 add(E) 函数中是通过 ReentrantLock + 数组copy 等其它手段来实现的线程安全,那既然有了互斥锁保证了线程安全,为什么还要 copy 数组呢 ?

程序员:

的确,对 add(E) 进行加锁后,能够保证同一时刻,只有一个线程能对数组进行 add(E),在同单核 CPU 下的多线程环境下肯定没有问题,但我们现在的机器都是多核 CPU,如果我们不通过复制拷贝新建数组,修改原数组容器的内存地址的话,是无法触发 volatile 可见性效果的,那么其他 CPU 下的线程就无法感知数组原来已经被修改了,就会引发多核 CPU 下的线程安全问题。

假设我们不复制拷贝,而是在原来数组上直接修改值,数组的内存地址就不会变,而数组被 volatile 修饰时,必须当数组的内存地址变更时,才能及时的通知到其他线程,内存地址不变,仅仅是数组元素值发生变化时,是无法把数组元素值发生变动的事实,通知到其它线程的。

面试官:

嗯,看来你对这些机制都了解的挺清楚的,那你在说说 remove 是怎么保证的线程安全吧?

2、remove 是怎么保证线程安全的?

其实 remove 保证线程安全机制跟 add 思路都差不多,都是先加锁 +不同策略的数组拷贝最后是释放锁。

面试官:

add , remove 方法内部都实现了 copy ,在性能上你有什么优化建议吗?

程序员:

尽量使用 addAll、removeAll 方法,而不要在循环里面使用 add、remove 方法,主要是因为 for 循环里面使用 add 、remove 的方式,在每次操作时,都会进行一次数组的拷贝(甚至多次),非常耗性能,而 addAll、removeAll 方法底层做了优化,整个操作只会进行一次数组拷贝,由此可见,当批量操作的数据越多时,批量方法的高性能体现的越明显。

Map

1、说一下你对 HashMap 的了解

程序员:

HashMap 底层是数组 + 单链表 + 红黑树 组成的存储数据结构,简单来说当链表长度大于等于 8 并且数组长度大于 64 那么就会由链表转为红黑树,当红黑树的大小容量 <= 6 时又转换为 链表的一个底层结构。非线程安全的。

可以用一张图来解释 HashMap 底层结构,如下所示:

图解:

  1. 最左边 table 是 HashMap 的数组结构,允许 Node 的 value 值为 NULL

  2. 数组的扩容机制第一次默认扩容大小为 16 size, 扩容阀值为 threshold = size * loadFactor -> 12 = 16 * 0.75 ,只要 ++size > threshold 就按照 newCap = oldCap << 1 机制来扩容。

  3. 数组的下标有可能是一个链表、红黑树,也有可能只是一个 Node,只有当数组长度 > 64,链表长度 >= 8 才会将数组中的 Node 节点转为 TreeNode 节点。也只有当红黑树的大小 <= 6 时,才转为单链表结构。

程序员:

HashMap 底层的基本实现实现基本就是这样。

面试官:

嗯,那你描述一下 put(K key, V value) 这个 API 的存储过程 。

程序员:

  1. 好的,我先描述一下基本流程,最后我画一张流程图来总结一下

  2. 根据 key 通过该公式 (h = key.hashCode()) ^ (h >>> 16) 计算 hash 值

  3. 判断 HashMap table 数组是否已经初始化,如果没有初始化,那么就按照默认 16 的大小进行初始化,扩容阀值也将按照 size * 0.75 来定义

  4. 通过该公式 (n - 1) & hash 拿到存入 table 的 index 索引,判断当前索引下是否有值,如果没有值就进行直接赋值 tab[index] , 如果有值,那么就会发生 hash 碰撞 💥 ,也就是俗称 hash冲突 , 在 JDK中的解决是的办法有 2 个,其一是链表,其二是 红黑树。

  5. 当发送 hash 冲突 首先判断数组中已存入的 key 是否与当前存入的 key 相同,并且内存地址也一样,那么就直接默认直接覆盖 values

  6. 如果 key 不相等,那么先拿到 tab[index] 中的 Node是否是红黑树,如果是红黑树,那么就加入红黑树的节点;如果 Node 节点不是红黑树,那么就直接放入 node 的 next 下,形成单链表结构。

  7. 如果链表结构的长度 >= 8 就转为红黑树的结构。

  8. 最后检查扩容机制。

整个 put 流程就是这样,可以用一个流程图来进行总结,如下所示:

image

面试官:

嗯,理解的很透彻,刚刚你说解决 hash 冲突有 2 种办法,那你描述一下红黑树是怎么实现新增的?

程序员:

好的,基本流程有如下几步:

1、首先判断新增的节点在红黑树上是不是已经存在,判断手段有如下两种:

​ 1.1、如果节点没有实现 Comparable 接口,使用 equals 进行判断;

​ 1.2、如果节点自己实现了 Comparable 接口,使用 compareTo 进行判断。

2、新增的节点如果已经在红黑树上,直接返回;不在的话,判断新增节点是在当前节点的左边还是右边,左边值小,右边值大;

3、自旋递归 1 和 2 步,直到当前节点的左边或者右边的节点为空时,停止自旋,当前节点即为我们新增节点的父节点;

4、把新增节点放到当前节点的左边或右边为空的地方,并于当前节点建立父子节点关系;

5、进行着色和旋转,结束。

面试官:

你知道链表转红黑树定义的长度为什么是 8 吗?

程序员:

这个答案,我通过 HashMap 类中的注释有留意过,它大概描述的意思是链表查询的时间复杂度是 O (n),红黑树的查询复杂度是 O (log (n))。在链表数据不多的时候,使用链表进行遍历也比较快,只有当链表数据比较多的时候,才会转化成红黑树,但红黑树需要的占用空间是链表的 2 倍,考虑到转化时间和空间损耗,所以我们需要定义出转化的边界值。

在考虑设计 8 这个值的时候,我们参考了泊松分布概率函数,由泊松分布中得出结论,链表各个长度的命中概率为:

  • 0: 0.60653066
  • 1: 0.30326533
  • 2: 0.07581633
  • 3: 0.01263606
  • 4: 0.00157952
  • 5: 0.00015795
  • 6: 0.00001316
  • 7: 0.00000094
  • 8: 0.00000006

意思是,当链表的长度是 8 的时候,出现的概率是 0.00000006,不到千万分之一,所以说正常情况下,链表的长度不可能到达 8 ,而一旦到达 8 时,肯定是 hash 算法出了问题,所以在这种情况下,为了让 HashMap 仍然有较高的查询性能,所以让链表转化成红黑树,我们正常写代码,使用 HashMap 时,几乎不会碰到链表转化成红黑树的情况,毕竟概念只有千万分之一。

面试官:

嗯,那你在说说数组为什么每次都是以 2的幂次方扩容?

程序员:

好的。如下是我的理解。

HashMap 为了存取高效,要尽量减少碰撞,就是要尽量把数据分配均匀。

比如:

2 的 n 次方实际就是 1 后面 n 个 0,2 的 n 次方 -1,实际就是 n 个 1。

那么长度为 8 时候,3 & (8-1) = 3 ,2 & (8-1) = 2 ,不同位置上,不碰撞。

而长度为 5 的时候,3 & (5-1)= 0 , 2 & (5-1) = 0,都在 0 上,出现碰撞了。

//3 & 4
011
100
000

//2 & 4
010
100
000

所以,保证容积是 2 的 n 次方,是为了保证在做 (size-1) 的时候,每一位都能 & 1 ,也就是和 1111……1111111进行与运算。

面试官:

在考你一道扩容机制的题目,现在后台的图片数据有 1000 条, 当我请求下来也处理完了,现在我想要缓存到 Map 中,如果我直接调用 new HashMap(1000) 构造方法,内部还会扩容吗?

程序员:

你可以这样回答,其实如果直接给定 1000 的初始化容量,那么我们需要根据源码中的计算来分析,有如下几个步骤:

1、首先会在构造函数中调用 1024 = tableSizeFor(1000); 该 API 来计算扩容阀值。

你可不要认为,这里就是真正的扩容大小,它在扩容的时候还会有一个计算公式。

2、计算真正的扩容阀值。

那么根据第一次 put 数据的时候,判断 table 是否为空,如果为空那么就需要扩容。

final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table; //第一次进来为 null 那么就是 0 长度
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold; //这里其实就是 1024
int newCap, newThr = 0;
if (oldCap > 0) {
…//省略代码
} else if (oldThr > 0)
newCap = oldThr;// newCap = 1024
if (newThr == 0) {
float ft = (float)newCap * loadFactor;//1024 * 0.75 = 768.0
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE); //768
}
//更新扩容的阀值
threshold = newThr;
//实例化一个数组
@SuppressWarnings({“rawtypes”,“unchecked”})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
//正在扩容 newTab 的大小
table = newTab;
…//省略代码
}
return newTab;
}

可以看到其实真正的扩容阀门是 768。

3、判断扩容机制

那么只要添加到 768 的时候,就会发生扩容,如下代码所示:

if (++size > threshold)//769 > 768 需要扩容
resize();

所以当我们给定 1000 为初始化扩容容量的时候,是需要扩容的。因为底层并不会真正以 1024 来进行设置阀门,它还要乘以一个加载因子。这个时候其实我们可以有办法不让它扩容,那就是调用 new HashMap(1000,1f) 那么就不会扩容了。

这里你不仅给出了实际答案,还提供了解决办法。 面试官对你的回答肯定是满意的。

2、说一下你对 ArrayMap 的了解

程序员:

ArrayMap 底层通过两个数组来建立映射关系,其中 int[] mHashes 按大小顺序保存 Key 对象 hashCode 值,Object[] mArray 按 mHashes 的顺序用相邻位置保存 Key 对象和 Value 对象。mArray 长度 是 mHashes 长度的 2 倍。

存储数据是根据 key 的 hashcode() 方法得到 hash 值,计算出在 mArrays 的 index 值,然后利用二分查找找到对应的位置进行插入,当出现哈希冲突时,会在 inde 的相邻位置插入。

取数据是根据 key 的 hashcode() 方法得到 hash 值,然后通过 hash 值根据二分查找拿到 mHashes 的 index 索引,最后在根据 index + 1 索引拿到 mArrays 对应的 values 值。

3、你在工作中对 HashMap 和 ArrayMap 还有 SparseArray 是怎么选型的 ?

程序员:

好的,我总结了一套性能对比,每次需求我都是参考如下的总结。

4、有用过 LinkedHashMap 吗 ?底层怎么维护插入顺序的,又是怎么维护删除最少访问元素的 ?

ps: 由于内部存储机制都是散开的,如果按照散开的来连接,那图上连接线估计很乱,所以为了上图能够稍微好点一点,我就按照我自己的思路来绘制的,当然,内部结构还是不会变的。

程序员:

有用过,之前看 LruCache 底层也是基于 LinkedHashMap 实现的。那我还是按照我的思路来回答吧。

通过翻阅源码得知它是继承于 HashMap ,那么间接的它也拥有了 HashMap 的所有特性,而且在此基础上,还提供了两大特性,一个是增加了插入顺序和实现了最近最少访问的删除策略。

先来看下是怎么实现顺序插入:

LinkedHashMap 外部结构是一个双向链表结构,内部是一个 HashMap 结构,它就是相当于 HashMap + LinkedHashMap 的结合体。

其实 LinkedHashMap 的源码实现很简单,它就是重写了 HashMap##put 方法执行中调用的 newNode/newTreeNode 方法。然后在该函数内部中实现了链表的双向连接。如下图所示:

总结来说,LinkedHashMap 把新增的节点都使用双向链表连接起来,从而实现了插入顺序。然后核心的数据结构还是交于 HashMap 来处理维护的。

在来看下是怎么实现的访问最少删除功能:

其实访问最少删除功能的这种策略也叫做 LRU 算法,底层原理就是把经常使用的元素会被追加到当前链表的尾结点,而不经常使用的就自然都靠在链表的头节点,然后我们就可以设置删除策略,比如给当前 Map 设置一个策略大小,那么当存入的数据大于设置的大小时,就会从头节点开始删除。

在源码当中,将经常使用的 节点数据追加到链表的操作是在 get API 中,如下所示:

public V get(Object key) {

题外话

我在一线互联网企业工作十余年里,指导过不少同行后辈。帮助很多人得到了学习和成长。

我意识到有很多经验和知识值得分享给大家,也可以通过我们的能力和经验解答大家在IT学习中的很多困惑,所以在工作繁忙的情况下还是坚持各种整理和分享。但苦于知识传播途径有限,很多程序员朋友无法获得正确的资料得到学习提升,故此将并将重要的Android进阶资料包括自定义view、性能优化、MVC与MVP与MVVM三大框架的区别、NDK技术、阿里面试题精编汇总、常见源码分析等学习资料。

【Android思维脑图(技能树)】

知识不体系?这里还有整理出来的Android进阶学习的思维脑图,给大家参考一个方向。

希望我能够用我的力量帮助更多迷茫、困惑的朋友们,帮助大家在IT道路上学习和发展~

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化学习资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

助很多人得到了学习和成长。

我意识到有很多经验和知识值得分享给大家,也可以通过我们的能力和经验解答大家在IT学习中的很多困惑,所以在工作繁忙的情况下还是坚持各种整理和分享。但苦于知识传播途径有限,很多程序员朋友无法获得正确的资料得到学习提升,故此将并将重要的Android进阶资料包括自定义view、性能优化、MVC与MVP与MVVM三大框架的区别、NDK技术、阿里面试题精编汇总、常见源码分析等学习资料。

【Android思维脑图(技能树)】

知识不体系?这里还有整理出来的Android进阶学习的思维脑图,给大家参考一个方向。

[外链图片转存中…(img-kIBx79Id-1714686782535)]

希望我能够用我的力量帮助更多迷茫、困惑的朋友们,帮助大家在IT道路上学习和发展~

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化学习资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值