Skip to content

Commit

Permalink
集合框架
Browse files Browse the repository at this point in the history
  • Loading branch information
itwanger committed Aug 17, 2022
1 parent 8c3200c commit 5d08dd3
Show file tree
Hide file tree
Showing 27 changed files with 1,184 additions and 3 deletions.
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,11 @@
- [海康威视一面:Java中Iterator和Iterable有什么区别?](docs/collection/iterator-iterable.md)
- [为什么阿里巴巴强制不要在foreach里执行删除操作?还不是因为fail-fast](docs/collection/fail-fast.md)
- [Java HashMap详解(附源码分析)](docs/collection/hashmap.md)
- [Java LinkedHashMap详解(附源码分析)](docs/collection/linkedhashmap.md)
- [Java TreeMap详解(附源码分析)](docs/collection/treemap.md)
- [详解 Java 中的堆和队列(Stack and Queue 附源码分析)](docs/collection/arraydeque.md)
- [详解 Java 中的优先级队列(PriorityQueue 附源码分析)](docs/collection/PriorityQueue.md)
- [Java WeakHashMap详解(附源码分析)](docs/collection/WeakHashMap.md)

## Java输入输出

Expand Down
5 changes: 5 additions & 0 deletions docs/.vuepress/sidebar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,11 @@ export const sidebarConfig = sidebar({
"collection/iterator-iterable",
"collection/fail-fast",
"collection/hashmap",
"collection/linkedhashmap",
"collection/treemap",
"collection/arraydeque",
"collection/PriorityQueue",
"collection/WeakHashMap",

],
},
Expand Down
191 changes: 191 additions & 0 deletions docs/collection/PriorityQueue.md
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)
161 changes: 161 additions & 0 deletions docs/collection/WeakHashMap.md
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;
}
```



如源码所示,

* WeakHashMapEntry继承了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 StudioForce GC

完整的测试代码可以访问这里 [https://github.com/androidyue/WeakHashMapSample](https://github.com/androidyue/WeakHashMapSample)


----

最近整理了一份牛逼的学习资料,包括但不限于Java基础部分(JVMJava集合框架、多线程),还囊括了 **数据库、计算机网络、算法与数据结构、设计模式、框架类SpringNetty、微服务(Dubbo,消息队列) 网关** 等等等等……详情戳:[可以说是2022年全网最全的学习和找工作的PDF资源了](https://tobebetterjavaer.com/pdf/programmer-111.html)

关注二哥的原创公众号 **沉默王二**,回复**111** 即可免费领取。

![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png)
Loading

0 comments on commit 5d08dd3

Please sign in to comment.