本文将从数据结构底层原理 + 源码实现 + 应用实战三方面深入剖析 PriorityQueue,让你真正掌握优先队列的底层逻辑及其应用。

源码可视化视频:https://www.bilibili.com/video/BV12Ha5zjEcS/

在玩游戏的时候,发现需要购买的装备很多,而且不同的英雄需要购买的装备还不一样,打游戏的时候需要更多精力去关注买什么装备才行。这多少影响游戏体验,特别是我这种小白,打开装备栏挑了半天没找到自己要买的,然后就嘎了^。游戏难度太大(体验太差)玩不来,卸载了吧。

如果开始游戏前就先配置好我要玩的英雄的装备购买顺序,那岂不美哉。打游戏的时候,游戏自动提示你要购买的装备,直到你全部装备都买好了。

通过PriorityQueue便可轻松实现上面想要的功能。

开始游戏前配置装备优先级的过程为:入队操作过程;

打游戏提示购买装备顺序的过程为:出队操作过程。

文中实战部分会讲到这个案例如何实现,我们先从最简单的是什么开始。

1. 概述

1.1. PriorityQueue 是什么?

PriorityQueue 是一个 优先级的队列,元素按优先级从小到大排列(默认使用自然顺序,也可以传入比较器 Comparator)。

并不保证 FIFO 顺序,也不保证整个队列有序,而是每次出队时返回当前优先级最高的元素。说白了,仅保证每次出队的元素是最大值或最小值,默认是最小值。

1.2. 底层数据结构

PriorityQueue 使用一个动态数组来维护数据,底层处理逻辑为二叉堆,默认为小顶堆

学习PriorityQueue底层原理,就是学习怎么应用二叉堆实现优先队列

二叉堆为完全二叉树,按照从上到下、从左到右的顺序进行入堆,并且仅有最底层可能没有填满。

假设当前堆如下(PriorityQueue 数组下标从 0 开始):

在数组中的保存顺序为二叉树的层序遍历顺序:从根节点开始,从上到下、从左到右一个个遍历。

所以在数组表示为:[1, 3, 5, 7, 9]

1.3. 二叉堆的规律

这些规律将决定你的代码怎么去实现。

PriorityQueue的下标i是从0 开始计算的,以下公式也是基于i=0才成立的。

规律1,根据父节点计算子节点:父节点下标 i,那么左子节点就为 2*i+1,右子节点为 2*i+2

反过来,如果左子节点为i,那么父节点为(i-1)/2;如果右子节点为i,那么父节点为(i-2)/2;

计算父结点一定要两个公式??

左右节点相差1,必然会有一个是不能整除

比如:左节点i=1,那么右节点必为i=2,父节点为0

如果使用公式(i-1)/2,那么左节点计算结果为0,右节点计算为0.5,只需要向下取整即可。

如果使用公式(i-2)/2,那么左节点计算结果为-0.5,右节点计算为0,发现怎么取整都不可以。

规律2,根据子节点计算父节点:使用公式(i-1)/2计算并向下取整即可。

规律3小顶堆/最小堆的每个节点都比它的子节点小;大顶堆/最大堆的每个节点都比它的子节点大。

⚙️ 2. 入队相关操作

上面的规律如果明白了,那下面的源码就很好明白了。

2.1. 入队操作

先明确知道,插入节点的入堆顺序是按照层序遍历从上到下、从左到右,就像你写作文一样从左到右,从上到下地编写,也就是每次都放在数组元素的末端。

再结合小顶堆规律和二叉堆的计算公式:由子得父的简单计算公式。

涉及的关键源码:offer(e)siftUp

// 入队操作
public boolean offer(E e) {
if (e == null)
throw new NullPointerException();
modCount++;
int i = size;
// 容量满后才扩容
if (i >= queue.length)
grow(i + 1);
size = i + 1;
if (i == 0)
queue[0] = e;
else
siftUp(i, e);
return true;
}
// siftUp 上浮
private void siftUp(int k, E x) {
if (comparator != null)
// 使用了自定义比较器
siftUpUsingComparator(k, x);
else
// 使用类中的比较器
siftUpComparable(k, x);
}
// siftUp 源码逻辑(有 Comparator 时)
private void siftUpUsingComparator(int k, E x) {
while (k > 0) {
// 由子节点 计算出 父节点数组索引
int parent = (k - 1) >>> 1;
// 获取父节点对象
Object e = queue[parent];
// 比较大小:儿子>=父亲 就没得玩了
if (comparator.compare(x, (E) e) >= 0)
break;
// 儿子低位还小,继续上位,把老父亲换下去
queue[k] = e;
// 更新索引,继续比较
k = parent;
}
// 最终看实力上位到 k 位置,坐好
queue[k] = x;
}

使用了规律2(公式(i-1)/2计算,并向下取整),根据子节点计算父节点索引。

但源码的计算公式为:parent = (k - 1) >>> 1

源码采用无符号右移一位,相当于除于2,但位移运算余数不会被保存,也就无需向下取整

比如:k=4,那么k-1=3,3 的二进制为11,右移一位变为1

位移运算只会有整数部分参与运算,结果也只会有整数部分。

入队操作动图

2.2. 扩容操作

底层数据结构为数组,扩容操作跟ArrayList 基本一致。

扩容操作很简单,主要分为两步:计算新容量元素拷贝

PriorityArrayList的扩容主要区别为:容量计算有一点点差异。

在容量小于64前,Priority 扩容的新容量为:翻倍+2;后面的扩容都一致为:增加 50%容量。增长量 = oldCapacity >> 1,即 oldCapacity/2(向下取整)。

具体源码如下:

// 扩容
private void grow(int minCapacity) {
int oldCapacity = queue.length;
// 计算新容量
int newCapacity = oldCapacity + ((oldCapacity < 64) ?
(oldCapacity + 2) :
(oldCapacity >> 1));
// 防止溢出
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// 拷贝元素到新数组
queue = Arrays.copyOf(queue, newCapacity);
}
// 防止溢出
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}

3. 出队操作

出队操作分为三步:堆顶,堆顶,沉堆顶

取出堆的根节点--堆顶,即数组的第一个元素;

然后用最后一个节点替代堆顶

堆顶下沉操作,最后取左右子节点最小的跟父节点比较,父大于子,则交换节点,完成堆顶下沉。

具体源码如下:

// 出队操作
public E poll() {
if (size == 0)
return null;
int s = --size;
modCount++;
// 取出堆顶
E result = (E) queue[0];
// 用最后一个元素替代堆顶
E x = (E) queue[s];
// 移除最后一个节点
queue[s] = null;
if (s != 0)
// 替代堆顶节点下沉
siftDown(0, x);
return result;
}
// 下沉恢复堆结构
private void siftDown(int k, E x) {
if (comparator != null)
siftDownUsingComparator(k, x);
else
siftDownComparable(k, x);
}
// 下沉调整恢复堆结构
private void siftDownUsingComparator(int k, E x) {
// 大小除于2,half 为第一个 叶 子 节点
int half = size >>> 1;
// k 不是叶子节点,才需要上浮
while (k < half) {
// 由父得子:此为左节点公式
int child = (k << 1) + 1;
Object c = queue[child];
// 左加一,得右节点索引
int right = child + 1;
// 左右节点比较,取小的节点
if (right < size &&
comparator.compare((E) c, (E) queue[right]) > 0)
c = queue[child = right];
// 小儿子和冒牌顶替的父亲比较
if (comparator.compare(x, (E) c) <= 0)
break;
// 下沉交换
queue[k] = c;
k = child;
}
// 保存到合适的位置
queue[k] = x;
}

PriorityQueue 就是通过出队的方式实现优先级的。它可以只出队一部分,完成部分优先级操作;也可以全部都出队,此时出队结果便是堆排序结果。

认真看下源码,有没有发现什么端倪??

为什么循环条件是while (k < half)

因为所有叶子节点都没有孩子,也就没有可以在下沉可说了。

出队操作动图

️ 4. 批量建堆(heapify)

现在有n个元素,通过入队操作,一个个放入数组中,单个元素操作的时间复杂度为O(logn),n 个元素的时间复杂度就变为O(n*logn)

现在有个更高效的方式:批量建堆。当已有一整个数组时,采用一次建堆,时间复杂度为O(n)

批量建堆就像盖房子一样,从地基开始,一层一层的搭建好。

批量建堆的整体过程:自底向上、逐层建堆

详细的处理过程为:

  1. 先将元素直接全部拷贝到数组
  2. 再从最后一个非叶子节点开始逆层序遍历建堆,
  3. 对每个子树的堆顶节点做下沉操作,保证其子树满足小顶堆特性

当通过带初始集合的构造器创建 PriorityQueue 时,会执行一次 批量建堆,其核心代码在构造器末尾调用:

public PriorityQueue(Collection<? extends E> c) {
// … 将 c.toArray() 填充到 queue[]
this.size = queue.length;
heapify();
}

4.1. heapify 源码逻辑

private void heapify() {
int n = size;
// 从最后一个非叶子节点开始,依次向下调整
for (int i = (n >>> 1) - 1; i >= 0; i--) {
siftDown(i, (E) queue[i]);
}
}

这源码可能看得有点费劲,这样可能清晰点

private void heapify() {
// 找到最后一个非叶节点
int lastParent = (size >>> 1) - 1;
// 从这个节点开始,往前遍历到根节点
for (int i = lastParent; i >= 0; i--) {
// 把 queue[i] “下沉”到它在子树中正确的位置
siftDown(i, (E) queue[i]);
}
}

开始位置:由完全二叉树二叉堆的特性,可知最后一个非叶子节点下标为 (size >>> 1) - 1,这个索引往后的节点都是叶子节点,无需调整。

建堆过程:对每个节点调用 siftDown,保证其子树满足最小堆性质

建堆索引顺序图

黄色三角形内只需要一次调平就可完成,然后继续往上逐层调整

最后调整完成:

整个建堆过程为:从最后非叶子节点开始,自底向上,逐个下沉调整,直到堆顶。

批量建堆可视化

哪可不可以从堆顶开始调整,自顶向下,逐层下沉调整?

4.2. 为什么自底向上更快?

如果采用从堆顶开始调整,自顶向下,逐层下沉调整会怎样?

4.2.1. 为什么选择自底向上、逐层下沉?

从堆顶开始调整,自顶向下,逐层下沉调整。当你在 i=0(根节点)做 siftDown 时,左右子树尚未调整,它们可能不是堆。尽管上层节点调整好了,之后再去调整下层节点时,又可能会破坏你先前在上层建立的堆序性,最终很可能得到一个不合法的堆。如果要确保每层都合法,那就得重复计算上层已经调整好的堆,重复计算导致性能低下。

顺着不行只能“逆天而行”,自底向上、逐层建堆。从最后一个非叶子节点开始建堆,那它的左右孩子必为叶子节点,叶子节点相当于已经建好的堆。逐层往上,当你在父节点 i 上调用 siftDown 时,它的左右子树都已经是堆了(因为你先处理好更深层的节点)。这样,单次 siftDown 只需要把第 i 个节点“插入”到一个已经是堆的子树里,最终整棵树就成为堆。

相当于已经计算好的结果,可以应用到后面的计算,从而避免不必要的重复计算,大大提高建堆性能。

在线性时间内、一步到位地把数组“堆化”

4.2.2. 计算建堆时间复杂度

建堆总工作量分析计算:

在批量 heapify 中,只有非叶节点会执行下沉。完美二叉树上共有约 n/2 个非叶节点。

那么,可以下沉至少 1 层 的节点最多有多少呢?

最多是所有非叶节点都可以下沉1层,一共≤ n/2次单步下沉

可以下沉至少 2 层 的节点最多有多少?

只有距叶子深度 ≥2 的节点,一共 ≤n/4次单步下沉

可以下沉至少 3 层 的节点数最多有多少?

只有距叶子深度 ≥3 的节点,一共 ≤n/8次单步下沉

...

一直到堆顶,可能的最大下沉深度k层,一共 ≤n/2^k次单步下沉

总单步下沉数

把“下沉至少 k 层”的所有单步加起来,就是下沉总次数

元素拷贝: 将整个数组元素直接拷贝到内部 queue 数组,时间复杂度O(n)

最终总工作量=元素拷贝 + 建堆的时间复杂度=O(n) + O(n) = O(n)

批量建堆时间复杂度计算可视化:

对比入队和建堆的时间复杂度:

方法 时间复杂度 何时使用
入队+siftUp O(n log n) 动态地一个个插入元素
一次性 heapify+siftDown O(n) 已有一整个数组,一次建堆

5. 应用实战

5.1. 小顶堆

默认就是小顶堆

PriorityQueue<String> priorityQueue = new PriorityQueue<>();
priorityQueue.offer("1");
priorityQueue.offer("5");
priorityQueue.offer("3");
priorityQueue.offer("7");
priorityQueue.offer("9"); System.out.println(priorityQueue); priorityQueue.poll(); System.out.println(priorityQueue);

测试结果:

[1, 5, 3, 7, 9]
[3, 5, 9, 7]

5.2. 大顶堆

想实现最大堆?传入 Comparator 即可:

PriorityQueue<String> maxHeap = new PriorityQueue<>(Comparator.reverseOrder());
maxHeap.offer("1");
maxHeap.offer("5");
maxHeap.offer("3");
maxHeap.offer("7");
maxHeap.offer("9");
System.out.println(maxHeap); maxHeap.poll(); System.out.println(maxHeap);

测试结果

[9, 7, 3, 1, 5]
[7, 5, 3, 1]

5.3. 比较器

堆内元素为自定义对象Person,实现 默认按年龄降序 排序(即年龄越大优先)

具体源码如下:

public class Person implements Comparable<Person>{
private String name;
private Integer age; public String getName() {
return name;
} public void setName(String name) {
this.name = name;
} public Integer getAge() {
return age;
} public void setAge(Integer age) {
this.age = age;
} public Person(String name, Integer age) {
this.name = name;
this.age = age;
}
@Override
public int compareTo(Person other) {
// return this.age.compareTo(other.age); // 小顶堆:按年龄升序
// 大顶堆:降序:年龄大的排前面
return other.age.compareTo(this.age);
} @Override
public String toString() {
return name + " (" + age + ")";
} public static void main(String[] args) {
PriorityQueue<Person> personPriorityQueue = new PriorityQueue<>();
personPriorityQueue.offer(new Person("张三",28));
personPriorityQueue.offer(new Person("李四",21));
personPriorityQueue.offer(new Person("渊渟岳",18));
personPriorityQueue.offer(new Person("咿呀呀",19));
System.out.println(personPriorityQueue);
} }

执行结果:

[张三 (28), 李四 (21), 渊渟岳 (18), 咿呀呀 (19)]

同样可以对它做取反操作,变为小顶堆,从小到大的年龄排序

PriorityQueue<Person> personPriorityQueue = new PriorityQueue<>(Comparator.reverseOrder());
personPriorityQueue.offer(new Person("张三",28));
personPriorityQueue.offer(new Person("李四",21));
personPriorityQueue.offer(new Person("渊渟岳",18));
personPriorityQueue.offer(new Person("咿呀呀",19));
System.out.println(personPriorityQueue);

执行结果:

[渊渟岳 (18), 咿呀呀 (19), 李四 (21), 张三 (28)]

5.4. 找出年龄最大的前 K 个人

我们来实现一个典型的 Top-K 最大年龄的 Person 问题。

实现思路

使用 PriorityQueue<Person>,保存堆顶为当前第 K 大的元素,大小为 K;

遍历所有元素:如果堆未满:加入;如果堆满,且当前元素年龄大于堆顶:替换堆顶;

最终堆中就是 Top-K 最大年龄的 Person。

具体源码如下:

public class TopKDemo {
/**
* 获取数组中 前 K 个最小或最大的元素
* 这里实现的是 前 K 个年龄大的
*/
public static List<Person> getTopKByAge(List<Person> people, int k) {
if (people == null)
throw new RuntimeException("家里没有人");
// 小顶堆:堆顶是当前 Top-K 里最小年龄的,因为Person类是正序排序
PriorityQueue<Person> minHeap = new PriorityQueue<>(k); for (Person p : people) {
if (minHeap.size() < k) {
minHeap.offer(p);
}
// 堆满 K 个元素后,取堆顶判断元素大小
else if (p.compareTo(minHeap.peek())>0) {
minHeap.poll();
minHeap.offer(p);
}
}
// 转换回 List
List<Person> result = new ArrayList<>(minHeap);
return result;
} public static void main(String[] args) {
// 测试数据
List<Person> people = Arrays.asList(
new Person("张三", 28),
new Person("李四", 21),
new Person("渊渟岳", 18),
new Person("咿呀呀", 19),
new Person("王五", 33),
new Person("老六", 33)
); int k = 3;
List<Person> topK = getTopKByAge(people, k);
System.out.println("Top " + k + " 最老的人:");
topK.forEach(System.out::println);
}
}

执行结果

Top 3 最老的人:
张三 (28)
王五 (33)
老六 (33)

这里容易搞错的点是:大顶堆和小顶堆的选择上。

取前K个最大元素,得选用小顶堆,这样才能确保每次比较大小时,都是和堆内最小的元素进行比较,取出最小的,留下最大的元素在堆内。

PriorityQueue 适用于:取数组前 K 个最小或最大的元素

5.5. 购买游戏装备问题

先定义一个购买装备的配置对象,用来保持配置优先级信息,规定priority 数字越小优先购买。

public class EquipmentItem implements Comparable<EquipmentItem> {
private final String name;
private final int priority; // 数字越小优先购买 public EquipmentItem(String name, int priority) {
this.name = name;
this.priority = priority;
} public String getName() {
return name;
} @Override
public int compareTo(EquipmentItem other) {
return Integer.compare(this.priority, other.priority);
} @Override
public String toString() {
return String.format("%s (priority=%d)", name, priority);
}
}

接着完成管理单个英雄的装备购买顺序

/**
* 管理单个英雄的装备购买顺序
*/
public class HeroEquipmentPlan {
private final String heroName;
private final PriorityQueue<EquipmentItem> planQueue = new PriorityQueue<>(); public HeroEquipmentPlan(String heroName) {
this.heroName = heroName;
} /**
* 预先设置一系列装备及其优先级
*/
public void addItem(String itemName, int priority) {
planQueue.offer(new EquipmentItem(itemName, priority));
System.out.println("添加装备到购买计划: " + itemName + ",优先级为:" + priority);
} /**
* 获取下一个最优先购买的装备
*/
public EquipmentItem getNextItem() {
EquipmentItem next = planQueue.poll();
if (next == null) {
System.out.println("全部购买完成:" + heroName);
} else {
System.out.println("下一个购买项:" + heroName + ": " + next);
}
return next;
} /**
* 查看当前购买计划(不移除)
*/
public void previewPlan() {
System.out.println("当前购买计划:" + heroName + ":");
planQueue.stream()
.sorted()
.forEach(item -> System.out.println(" - " + item));
} }

测试装备购买顺序配置,再进行装备购买

public class Demo{
public static void main(String[] args) {
// 游戏开始前:为英雄设置购买优先级
HeroEquipmentPlan plan = new HeroEquipmentPlan("战士");
plan.addItem("长剑", 1);
plan.addItem("护甲", 2);
plan.addItem("靴子", 3);
plan.addItem("暴击手套", 4);
plan.addItem("巨人腰带", 5); // 开局预览
plan.previewPlan(); // 游戏中:当有足够金币时,依次购买下一个装备
EquipmentItem next;
while ((next = plan.getNextItem()) != null) {
// 在这里调用游戏购买接口或提醒玩家:
// buyItem(next.getName());
// 模拟购买间隔
try {
Thread.sleep(2000);
} catch (InterruptedException ignored) {}
} System.out.println("购买计划已完成,祝游戏愉快!");
}
}

测试结果:

添加装备到购买计划: 长剑,优先级为:1
添加装备到购买计划: 护甲,优先级为:2
添加装备到购买计划: 靴子,优先级为:3
添加装备到购买计划: 暴击手套,优先级为:4
添加装备到购买计划: 巨人腰带,优先级为:5
当前购买计划:战士:
- 长剑 (priority=1)
- 护甲 (priority=2)
- 靴子 (priority=3)
- 暴击手套 (priority=4)
- 巨人腰带 (priority=5)
下一个购买项:战士: 长剑 (priority=1)
下一个购买项:战士: 护甲 (priority=2)
下一个购买项:战士: 靴子 (priority=3)
下一个购买项:战士: 暴击手套 (priority=4)
下一个购买项:战士: 巨人腰带 (priority=5)
全部购买完成:战士
购买计划已完成,祝游戏愉快!

PriorityQueue 适用于:管理优先级任务调度

6. 汇总

6.1. 时间复杂度汇总

操作 时间复杂度
入队 offer() O(log n)
出队 poll() O(log n)
访问 peek() O(1)
建堆 heapify O(n)
循环入队式建堆 O(n log n)

6.2. 常见误区

误区 正解
PriorityQueue 是 FIFO? 它默认是最小堆,优先出最小元素
删除元素快? 仅堆顶是 O(log n),任意删除是 O(n)
支持 null 元素? 不允许插入 null,会抛异常
是线程安全的吗? 需要手动加锁或使用线程安全版本PriorityBlockingQueue

7. 总结

二叉堆就四句话:有批量选批量,没批量逐个建;获取最优数值,重建合法堆形

PriorityQueue 优先队列和普通的二叉堆一样,主要分为三部分:初建堆、取最值、重建堆。其中建堆分为两种情况:逐个入队建堆和批量建堆。

PriorityQueue 是建立在小顶堆结构上的非线程安全队列,底层数据结构为数组,核心处理逻辑为二叉堆,通过 siftUpsiftDown 动态维护堆结构,默认最小堆,但可以通过 Comparator 灵活定制。

适合优先级任务调度动态最值维护Top-K等场景。注意线程安全与迭代顺序的问题,在并发场景下使用 PriorityBlockingQueue

往期推荐

分类 往期文章
Java集合底层原理可视化 Java 集合--快速掌握涵盖三大场景实现的Set集合底层原理
ArrayDeque双端队列--底层原理可视化
“子弹弹夹”装弹和出弹的抽象原理实战:掌握栈的原理与实战
TreeMap集合--底层原理、源码阅读及它在Java集合框架中扮演什么角色?
LinkedHashMap集合--原理可视化
HashMap集合--基本操作流程的源码可视化
Java集合--HashMap底层原理可视化,秒懂扩容、链化、树化
Java集合--从本质出发理解HashMap
Java集合--LinkedList源码可视化
Java集合源码--ArrayList的可视化操作过程
设计模式秘籍
(已全部开源)
掌握设计模式的两个秘籍
往期设计模式文章的:设计模式
软件设计师 软考中级--软件设计师毫无保留的备考分享
软件设计师--案例分析题轻松上分,解决你的备考烦恼
通过软考后却领取不到实体证书?
2023年下半年软考考试重磅消息
Java学习路线
和相应资源
Java全栈学习路线、学习资源和面试题一条龙

原创不易,觉得还不错的,三连支持:点赞、分享、推荐↓

PriorityQueue 数据结构底层原理、源码实现可视化分析及应用实战的更多相关文章

  1. Redis核心设计原理(深入底层C源码)

    Redis 基本特性 1. 非关系型的键值对数据库,可以根据键以O(1) 的时间复杂度取出或插入关联值 2. Redis 的数据是存在内存中的 3. 键值对中键的类型可以是字符串,整型,浮点型等,且键 ...

  2. SharedPreferences 原理 源码 进程间通信 MD

    Markdown版本笔记 我的GitHub首页 我的博客 我的微信 我的邮箱 MyAndroidBlogs baiqiantao baiqiantao bqt20094 baiqiantao@sina ...

  3. Laya Timer原理 & 源码解析

    Laya Timer原理 & 源码解析 @author ixenos 2019-03-18 16:26:38 一.原理 1.将所有Handler注册到池中 1.普通Handler在handle ...

  4. 图解Janusgraph系列-图数据底层序列化源码分析(Data Serialize)

    图解Janusgraph系列-图数据底层序列化源码分析(Data Serialize) 大家好,我是洋仔,JanusGraph图解系列文章,实时更新~ 图数据库文章总目录: 整理所有图相关文章,请移步 ...

  5. 深入理解Faiss 原理&源码 (一) 编译

    目录 深入理解Faiss 原理&源码 (一) 编译 mac下安装 安装mac xcode工具包 安装 openblas 安装swig 安装libomp 编译faiss 附录 深入理解Faiss ...

  6. 第九节:从源码的角度分析MVC中的一些特性及其用法

    一. 前世今生 乍眼一看,该标题写的有点煽情,最近也是在不断反思,怎么能把博客写好,让人能读下去,通俗易懂,深入浅出. 接下来几个章节都是围绕框架本身提供特性展开,有MVC程序集提供的,也有其它程序集 ...

  7. java基础解析系列(十)---ArrayList和LinkedList源码及使用分析

    java基础解析系列(十)---ArrayList和LinkedList源码及使用分析 目录 java基础解析系列(一)---String.StringBuffer.StringBuilder jav ...

  8. 从源码的角度分析ViewGruop的事件分发

    从源码的角度分析ViewGruop的事件分发. 首先我们来探讨一下,什么是ViewGroup?它和普通的View有什么区别? 顾名思义,ViewGroup就是一组View的集合,它包含很多的子View ...

  9. 安卓图表引擎AChartEngine(二) - 示例源码概述和分析

    首先看一下示例中类之间的关系: 1. ChartDemo这个类是整个应用程序的入口,运行之后的效果显示一个list. 2. IDemoChart接口,这个接口定义了三个方法, getName()返回值 ...

  10. 通过官方API结合源码,如何分析程序流程

    通过官方API结合源码,如何分析程序流程通过官方API找到我们关注的API的某个方法,然后把整个流程执行起来,然后在idea中,把我们关注的方法打上断点,然后通过Step Out,从内向外一层一层分析 ...

随机推荐

  1. TypeScript实用类型之Omit

    概述 TypeScript Utility Types(实用工具类)包含一系列预定义的类型,用于简化类型操作,善用这些类型可以让我们的代码更加简洁优雅,今天来学习一下Omit类型.Omit类型可以优雅 ...

  2. FactoryBean 和BeanFactory的傻傻的总是分不清?

    引言 记得很久以前经常被问到这样一个面试题"FactoryBean 和BeanFactory它们有啥区别".在 Spring 框架中,BeanFactory 和 FactoryBe ...

  3. Centos设置Redis自启动(Systemd)

    前言 在生产环境中,我们在设置Redis服务自启动时有多种方式,这里采用Systemd管理方式进行设置Redis, Systemd管理起centos7版本中使用了systemd,systemd同时兼容 ...

  4. IOS内购数据拉取

    目标:拉取app store connect 内购数据拉取,自己做数据报表. 1:api秘钥 接口需要token,token生成需要秘钥.参考官方文档:https://developer.apple. ...

  5. windows杀占用端口

    通过命令查找某一特定端口,在命令窗口中输入命令中输入 netstat -ano |findstr "端口号",然后回车就可以看到这个端口被哪个应用占用.   查看到对应的进程id之 ...

  6. AI应用实战课学习总结(3)预测带货销售额

    大家好,我是Edison. 最近入坑黄佳老师的<AI应用实战课>,记录下我的学习之旅,也算是总结回顾. 今天是我们的第3站,了解下AI项目实践的5大环节,并通过一个预测直播带货销售额的案例 ...

  7. OLAP分析数据库适用场景及主流产品对比

    本文分享自天翼云开发者社区<OLAP分析数据库适用场景及主流产品对比>,作者:刘****鑫 随着企业数字化程度不断提升,数据分析场景越老越丰富,企业在以下几种场景下可能需要使用OLAP(O ...

  8. vscode prettier

    前言 与通过npm安装的prettier关联 vscode 的 prettier 扩展,会优先使用 你项目中明确安装的 prettier(既 npm i prettier --D) 的版本, 如果你没 ...

  9. vue3组合式api 和 react自定义hooks

    前言 其实两者是一个东西,只是叫法不通 而且显然这次是vue抄袭了react 为了对比,我举了两个例子,都用了一套数据 Mock.mock("/getStudent",option ...

  10. 关于 var 目录磁盘空间不足的解决方案

    参考博客 博客