forked from itwanger/toBeBetterJavaer
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
27 changed files
with
1,184 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,191 @@ | ||
--- | ||
title: 详解 Java 中的优先级队列(PriorityQueue 附源码分析) | ||
shortTitle: 详解PriorityQueue | ||
category: | ||
- Java核心 | ||
tag: | ||
- 集合框架(容器) | ||
description: Java程序员进阶之路,小白的零基础Java教程,详解 Java 中的优先级队列(PriorityQueue 附源码分析) | ||
head: | ||
- - meta | ||
- name: keywords | ||
content: Java,Java SE,Java 基础,Java 教程,Java 程序员进阶之路,Java 入门,Java PriorityQueue | ||
--- | ||
|
||
Java 中的 PriorityQueue 事通过二叉小顶堆实现的,可以用一棵完全二叉树表示。本文从 Queue 接口出发,结合生动的图解,深入浅出地分析 PriorityQueue 每个操作的具体过程和时间复杂度,让读者对 PriorityQueue 建立清晰而深入的认识。 | ||
|
||
## 总体介绍 | ||
|
||
前面以 Java [ArrayDeque](https://tobebetterjavaer.com/collection/arraydeque.html)为例讲解了*Stack*和*Queue*,其实还有一种特殊的队列叫做*PriorityQueue*,即优先队列。 | ||
|
||
**优先队列的作用是能保证每次取出的元素都是队列中权值最小的**(Java 的优先队列每次取最小元素,C++的优先队列每次取最大元素)。 | ||
|
||
这里牵涉到了大小关系,**元素大小的评判可以通过元素本身的自然顺序(_natural ordering_),也可以通过构造时传入的比较器**(_Comparator_,类似于 C++的仿函数)。 | ||
|
||
Java 中*PriorityQueue*实现了*Queue*接口,不允许放入`null`元素;其通过堆实现,具体说是通过完全二叉树(_complete binary tree_)实现的**小顶堆**(任意一个非叶子节点的权值,都不大于其左右子节点的权值),也就意味着可以通过数组来作为*PriorityQueue*的底层实现。 | ||
|
||
![PriorityQueue_base.png](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/collection/PriorityQueue-8dca2f55-a7c7-49e1-95a5-df1a34f2aef5.png) | ||
|
||
上图中我们给每个元素按照层序遍历的方式进行了编号,如果你足够细心,会发现父节点和子节点的编号是有联系的,更确切的说父子节点的编号之间有如下关系: | ||
|
||
``` | ||
leftNo = parentNo\*2+1 | ||
rightNo = parentNo\*2+2 | ||
parentNo = (nodeNo-1)/2 | ||
``` | ||
|
||
通过上述三个公式,可以轻易计算出某个节点的父节点以及子节点的下标。这也就是为什么可以直接用数组来存储堆的原因。 | ||
|
||
*PriorityQueue*的`peek()`和`element`操作是常数时间,`add()`, `offer()`, 无参数的`remove()`以及`poll()`方法的时间复杂度都是*log(N)*。 | ||
|
||
## 方法剖析 | ||
|
||
### add()和 offer() | ||
|
||
`add(E e)`和`offer(E e)`的语义相同,都是向优先队列中插入元素,只是`Queue`接口规定二者对插入失败时的处理不同,前者在插入失败时抛出异常,后则则会返回`false`。对于*PriorityQueue*这两个方法其实没什么差别。 | ||
|
||
![PriorityQueue_offer.png](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/collection/PriorityQueue-0fb89aa7-c8fa-4fad-adbb-40c61c3bb0e9.png) | ||
|
||
新加入的元素可能会破坏小顶堆的性质,因此需要进行必要的调整。 | ||
|
||
```Java | ||
//offer(E e) | ||
public boolean offer(E e) { | ||
if (e == null)//不允许放入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; | ||
} | ||
``` | ||
|
||
上述代码中,扩容函数`grow()`类似于`ArrayList`里的`grow()`函数,就是再申请一个更大的数组,并将原数组的元素复制过去,这里不再赘述。需要注意的是`siftUp(int k, E x)`方法,该方法用于插入元素`x`并维持堆的特性。 | ||
|
||
```Java | ||
//siftUp() | ||
private void siftUp(int k, E x) { | ||
while (k > 0) { | ||
int parent = (k - 1) >>> 1;//parentNo = (nodeNo-1)/2 | ||
Object e = queue[parent]; | ||
if (comparator.compare(x, (E) e) >= 0)//调用比较器的比较方法 | ||
break; | ||
queue[k] = e; | ||
k = parent; | ||
} | ||
queue[k] = x; | ||
} | ||
``` | ||
|
||
新加入的元素`x`可能会破坏小顶堆的性质,因此需要进行调整。调整的过程为:**从`k`指定的位置开始,将`x`逐层与当前点的`parent`进行比较并交换,直到满足`x >= queue[parent]`为止**。注意这里的比较可以是元素的自然顺序,也可以是依靠比较器的顺序。 | ||
|
||
### element()和 peek() | ||
|
||
`element()`和`peek()`的语义完全相同,都是获取但不删除队首元素,也就是队列中权值最小的那个元素,二者唯一的区别是当方法失败时前者抛出异常,后者返回`null`。根据小顶堆的性质,堆顶那个元素就是全局最小的那个;由于堆用数组表示,根据下标关系,`0`下标处的那个元素既是堆顶元素。所以**直接返回数组`0`下标处的那个元素即可**。 | ||
|
||
![PriorityQueue_peek.png](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/collection/PriorityQueue-5059f157-845e-4d1c-b993-5cfe539d5607.png) | ||
|
||
代码也就非常简洁: | ||
|
||
```Java | ||
//peek() | ||
public E peek() { | ||
if (size == 0) | ||
return null; | ||
return (E) queue[0];//0下标处的那个元素就是最小的那个 | ||
} | ||
``` | ||
|
||
### remove()和 poll() | ||
|
||
`remove()`和`poll()`方法的语义也完全相同,都是获取并删除队首元素,区别是当方法失败时前者抛出异常,后者返回`null`。由于删除操作会改变队列的结构,为维护小顶堆的性质,需要进行必要的调整。 | ||
|
||
![PriorityQueue_poll.png](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/collection/PriorityQueue-e25ba931-2e6f-4c17-84b8-9b959733d541.png) | ||
|
||
代码如下: | ||
|
||
```Java | ||
public E poll() { | ||
if (size == 0) | ||
return null; | ||
int s = --size; | ||
modCount++; | ||
E result = (E) queue[0];//0下标处的那个元素就是最小的那个 | ||
E x = (E) queue[s]; | ||
queue[s] = null; | ||
if (s != 0) | ||
siftDown(0, x);//调整 | ||
return result; | ||
} | ||
``` | ||
|
||
上述代码首先记录`0`下标处的元素,并用最后一个元素替换`0`下标位置的元素,之后调用`siftDown()`方法对堆进行调整,最后返回原来`0`下标处的那个元素(也就是最小的那个元素)。重点是`siftDown(int k, E x)`方法,该方法的作用是**从`k`指定的位置开始,将`x`逐层向下与当前点的左右孩子中较小的那个交换,直到`x`小于或等于左右孩子中的任何一个为止**。 | ||
|
||
```Java | ||
//siftDown() | ||
private void siftDown(int k, E x) { | ||
int half = size >>> 1; | ||
while (k < half) { | ||
//首先找到左右孩子中较小的那个,记录到c里,并用child记录其下标 | ||
int child = (k << 1) + 1;//leftNo = parentNo*2+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;//然后用c取代原来的值 | ||
k = child; | ||
} | ||
queue[k] = x; | ||
} | ||
``` | ||
|
||
### remove(Object o) | ||
|
||
`remove(Object o)`方法用于删除队列中跟`o`相等的某一个元素(如果有多个相等,只删除一个),该方法不是*Queue*接口内的方法,而是*Collection*接口的方法。由于删除操作会改变队列结构,所以要进行调整;又由于删除元素的位置可能是任意的,所以调整过程比其它函数稍加繁琐。具体来说,`remove(Object o)`可以分为 2 种情况:1. 删除的是最后一个元素。直接删除即可,不需要调整。2. 删除的不是最后一个元素,从删除点开始以最后一个元素为参照调用一次`siftDown()`即可。此处不再赘述。 | ||
|
||
![PriorityQueue_remove2.png](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/collection/PriorityQueue-ed0d08d3-b38e-44a1-a710-ee7a01afda62.png) | ||
|
||
具体代码如下: | ||
|
||
```Java | ||
//remove(Object o) | ||
public boolean remove(Object o) { | ||
//通过遍历数组的方式找到第一个满足o.equals(queue[i])元素的下标 | ||
int i = indexOf(o); | ||
if (i == -1) | ||
return false; | ||
int s = --size; | ||
if (s == i) //情况1 | ||
queue[i] = null; | ||
else { | ||
E moved = (E) queue[s]; | ||
queue[s] = null; | ||
siftDown(i, moved);//情况2 | ||
...... | ||
} | ||
return true; | ||
} | ||
``` | ||
|
||
> 参考链接:[https://github.com/CarpenterLee/JCFInternals](https://github.com/CarpenterLee/JCFInternals),作者:李豪,整理:沉默王二 | ||
|
||
|
||
---- | ||
|
||
最近整理了一份牛逼的学习资料,包括但不限于Java基础部分(JVM、Java集合框架、多线程),还囊括了 **数据库、计算机网络、算法与数据结构、设计模式、框架类Spring、Netty、微服务(Dubbo,消息队列) 网关** 等等等等……详情戳:[可以说是2022年全网最全的学习和找工作的PDF资源了](https://tobebetterjavaer.com/pdf/programmer-111.html) | ||
|
||
关注二哥的原创公众号 **沉默王二**,回复**111** 即可免费领取。 | ||
|
||
![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,161 @@ | ||
--- | ||
title: Java WeakHashMap详解(附源码分析) | ||
shortTitle: 详解WeakHashMap | ||
category: | ||
- Java核心 | ||
tag: | ||
- 集合框架(容器) | ||
description: Java程序员进阶之路,小白的零基础Java教程,Java WeakHashMap详解(附源码分析) | ||
head: | ||
- - meta | ||
- name: keywords | ||
content: Java,Java SE,Java 基础,Java 教程,Java 程序员进阶之路,Java 入门,Java WeakHashMap | ||
--- | ||
|
||
|
||
在Java中,我们一般都会使用到Map,比如[HashMap](https://tobebetterjavaer.com/collection/hashmap.html)这样的具体实现。更高级一点,我们可能会使用WeakHashMap。 | ||
|
||
WeakHashMap其实和HashMap大多数行为是一样的,只是WeakHashMap不会阻止GC回收key对象(不是value),那么WeakHashMap是怎么做到的呢,这就是我们研究的主要问题。 | ||
|
||
在开始WeakHashMap之前,我们先要对弱引用有一定的了解。 | ||
|
||
在Java中,有四种引用类型 | ||
|
||
* 强引用(Strong Reference),我们正常编码时默认的引用类型,强应用之所以为强,是因为如果一个对象到GC Roots强引用可到达,就可以阻止GC回收该对象 | ||
* 软引用(Soft Reference)阻止GC回收的能力相对弱一些,如果是软引用可以到达,那么这个对象会停留在内存更时间上长一些。当内存不足时垃圾回收器才会回收这些软引用可到达的对象 | ||
* 弱引用(WeakReference)无法阻止GC回收,如果一个对象时弱引用可到达,那么在下一个GC回收执行时,该对象就会被回收掉。 | ||
* 虚引用(Phantom Reference)十分脆弱,它的唯一作用就是当其指向的对象被回收之后,自己被加入到引用队列,用作记录该引用指向的对象已被销毁 | ||
|
||
这其中还有一个概念叫做引用队列(Reference Queue) | ||
|
||
* 一般情况下,一个对象标记为垃圾(并不代表回收了)后,会加入到引用队列。 | ||
* 对于虚引用来说,它指向的对象会只有被回收后才会加入引用队列,所以可以用作记录该引用指向的对象是否回收。 | ||
|
||
## WeakHashMap如何不阻止对象回收呢 | ||
|
||
|
||
```java | ||
private static final class Entry<K, V> extends WeakReference<K> implements | ||
Map.Entry<K, V> { | ||
int hash; | ||
boolean isNull; | ||
V value; | ||
Entry<K, V> next; | ||
interface Type<R, K, V> { | ||
R get(Map.Entry<K, V> entry); | ||
} | ||
Entry(K key, V object, ReferenceQueue<K> queue) { | ||
super(key, queue); | ||
isNull = key == null; | ||
hash = isNull ? 0 : key.hashCode(); | ||
value = object; | ||
} | ||
``` | ||
|
||
|
||
|
||
如源码所示, | ||
|
||
* WeakHashMap的Entry继承了WeakReference。 | ||
* 其中Key作为了WeakReference指向的对象 | ||
* 因此WeakHashMap利用了WeakReference的机制来实现不阻止GC回收Key | ||
|
||
## 如何删除被回收的key数据呢 | ||
|
||
在Javadoc中关于WeakHashMap有这样的描述,当key不再引用时,其对应的key/value也会被移除。 | ||
|
||
那么是如何移除的呢,这里我们通常有两种假设策略 | ||
|
||
* 当对象被回收的时候,进行通知 | ||
* WeakHashMap轮询处理时效的Entry | ||
|
||
而WeakHashMap采用的是轮询的形式,在其put/get/size等方法调用的时候都会预先调用一个poll的方法,来检查并删除失效的Entry | ||
|
||
```java | ||
void poll() { | ||
Entry<K, V> toRemove; | ||
while ((toRemove = (Entry<K, V>) referenceQueue.poll()) != null) { | ||
removeEntry(toRemove); | ||
Log.d(LOGTAG, "removeEntry=" + toRemove.value); | ||
} | ||
} | ||
``` | ||
|
||
|
||
为什么没有使用看似更好的通知呢,我想是因为在Java中没有一个可靠的通知回调,比如大家常说的finalize方法,其实也不是标准的,不同的JVM可以实现不同,甚至是不调用这个方法。 | ||
|
||
当然除了单纯的看源码,进行合理的验证是检验分析正确的一个重要方法。 | ||
|
||
这里首先,我们定义一个MyObject类,处理一下finalize方法(在我的测试机上可以正常调用,仅仅做为辅助验证手段) | ||
|
||
```java | ||
class MyObject(val id: String) : Any() { | ||
protected fun finalize() { | ||
Log.i("MainActivity", "Object($id) finalize method is called") | ||
} | ||
} | ||
``` | ||
|
||
|
||
|
||
然后是调用者的代码,如下 | ||
|
||
```java | ||
private val weakHashMap = WeakHashMap<Any, Int>() | ||
var count : Int = 0 | ||
override fun onCreate(savedInstanceState: Bundle?) { | ||
super.onCreate(savedInstanceState) | ||
setContentView(R.layout.activity_main) | ||
setSupportActionBar(toolbar) | ||
dumpWeakInfo() | ||
fab.setOnClickListener { view -> | ||
//System.gc()// this seldom works use Android studio force gc stop | ||
weakHashMap.put(MyObject(count.toString()), count) | ||
count ++ | ||
dumpWeakInfo() | ||
Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG) | ||
.setAction("Action", null).show() | ||
} | ||
} | ||
fun dumpWeakInfo() { | ||
Log.i("MainActivity", "dumpWeakInfo weakInfo.size=${weakHashMap.size}") | ||
} | ||
``` | ||
|
||
|
||
|
||
我们按照如下操作 | ||
|
||
* 点击fab控件,每次对WeakhashMap对象增加一个Entry,并打印WeakHashMap的size 执行3此 | ||
* 在没有强制触发GC时,WeakHashMap对象size一直会增加 | ||
* 手动出发Force GC,我们会看到MyObject有finalize方法被调用 | ||
* 再次点击fab空间,然后输出的WeakHashMap size急剧减少。 | ||
* 同样我们收到在WeakHashMap增加的日志也会输出 | ||
|
||
|
||
```java | ||
I/MainActivity(10202): dumpWeakInfo weakInfo.size=1 | ||
I/MainActivity(10202): dumpWeakInfo weakInfo.size=2 | ||
I/MainActivity(10202): dumpWeakInfo weakInfo.size=3 | ||
I/MainActivity(10202): Object(2) finalize method is called | ||
I/MainActivity(10202): Object(1) finalize method is called | ||
I/MainActivity(10202): Object(0) finalize method is called | ||
I/WeakHashMap(10202): removeEntry=2 | ||
I/WeakHashMap(10202): removeEntry=0 | ||
I/WeakHashMap(10202): removeEntry=1 | ||
I/MainActivity(10202): dumpWeakInfo weakInfo.size=1 | ||
``` | ||
|
||
|
||
注意:System.gc()并不一定可以工作,建议使用Android Studio的Force GC | ||
|
||
完整的测试代码可以访问这里 [https://github.com/androidyue/WeakHashMapSample](https://github.com/androidyue/WeakHashMapSample) | ||
|
||
|
||
---- | ||
|
||
最近整理了一份牛逼的学习资料,包括但不限于Java基础部分(JVM、Java集合框架、多线程),还囊括了 **数据库、计算机网络、算法与数据结构、设计模式、框架类Spring、Netty、微服务(Dubbo,消息队列) 网关** 等等等等……详情戳:[可以说是2022年全网最全的学习和找工作的PDF资源了](https://tobebetterjavaer.com/pdf/programmer-111.html) | ||
|
||
关注二哥的原创公众号 **沉默王二**,回复**111** 即可免费领取。 | ||
|
||
![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) |
Oops, something went wrong.