diff --git a/README.md b/README.md index ec11ef0ba..1f9869b51 100644 --- a/README.md +++ b/README.md @@ -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输入输出 diff --git a/docs/.vuepress/sidebar.ts b/docs/.vuepress/sidebar.ts index a5353e623..507cd9ad3 100644 --- a/docs/.vuepress/sidebar.ts +++ b/docs/.vuepress/sidebar.ts @@ -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", ], }, diff --git a/docs/collection/PriorityQueue.md b/docs/collection/PriorityQueue.md new file mode 100644 index 000000000..3d0686193 --- /dev/null +++ b/docs/collection/PriorityQueue.md @@ -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) \ No newline at end of file diff --git a/docs/collection/WeakHashMap.md b/docs/collection/WeakHashMap.md new file mode 100644 index 000000000..77a8f9863 --- /dev/null +++ b/docs/collection/WeakHashMap.md @@ -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 extends WeakReference implements + Map.Entry { + int hash; + boolean isNull; + V value; + Entry next; + interface Type { + R get(Map.Entry entry); + } + Entry(K key, V object, ReferenceQueue 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 toRemove; + while ((toRemove = (Entry) 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() + 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) \ No newline at end of file diff --git a/docs/collection/arraydeque.md b/docs/collection/arraydeque.md new file mode 100644 index 000000000..33a71e8a8 --- /dev/null +++ b/docs/collection/arraydeque.md @@ -0,0 +1,206 @@ +--- +title: 详解 Java 中的堆和队列(ArrayDeque附源码分析) +shortTitle: 详解ArrayDeque +category: + - Java核心 +tag: + - 集合框架(容器) +description: Java程序员进阶之路,小白的零基础Java教程,详解 Java 中的堆和队列(Stack and Queue 附源码分析) +head: + - - meta + - name: keywords + content: Java,Java SE,Java 基础,Java 教程,Java 程序员进阶之路,Java 入门,Java ArrayDeque +--- + +Java 里有一个叫做*Stack*的类,却没有叫做*Queue*的类(它是个接口名字)。当需要使用栈时,Java 已不推荐使用*Stack*,而是推荐使用更高效的*ArrayDeque*;既然*Queue*只是一个接口,当需要使用队列时也就首选*ArrayDeque*了(次选是[LinkedList](https://tobebetterjavaer.com/collection/linkedlist.html))。 + +## 总体介绍 + +要讲栈和队列,首先要讲*Deque*接口。*Deque*的含义是“double ended queue”,即双端队列,它既可以当作栈使用,也可以当作队列使用。下表列出了*Deque*与*Queue*相对应的接口: + +| Queue Method | Equivalent Deque Method | 说明 | +| ------------ | ----------------------- | -------------------------------------- | +| add(e) | addLast(e) | 向队尾插入元素,失败则抛出异常 | +| offer(e) | offerLast(e) | 向队尾插入元素,失败则返回`false` | +| remove() | removeFirst() | 获取并删除队首元素,失败则抛出异常 | +| poll() | pollFirst() | 获取并删除队首元素,失败则返回`null` | +| element() | getFirst() | 获取但不删除队首元素,失败则抛出异常 | +| peek() | peekFirst() | 获取但不删除队首元素,失败则返回`null` | + +下表列出了*Deque*与*Stack*对应的接口: + +| Stack Method | Equivalent Deque Method | 说明 | +| ------------ | ----------------------- | -------------------------------------- | +| push(e) | addFirst(e) | 向栈顶插入元素,失败则抛出异常 | +| 无 | offerFirst(e) | 向栈顶插入元素,失败则返回`false` | +| pop() | removeFirst() | 获取并删除栈顶元素,失败则抛出异常 | +| 无 | pollFirst() | 获取并删除栈顶元素,失败则返回`null` | +| peek() | peekFirst() | 获取但不删除栈顶元素,失败则抛出异常 | +| 无 | peekFirst() | 获取但不删除栈顶元素,失败则返回`null` | + +上面两个表共定义了*Deque*的 12 个接口。 + +添加,删除,取值都有两套接口,它们功能相同,区别是对失败情况的处理不同。 + +**一套接口遇到失败就会抛出异常,另一套遇到失败会返回特殊值(`false`或`null`)**。除非某种实现对容量有限制,大多数情况下,添加操作是不会失败的。 + +**虽然*Deque*的接口有 12 个之多,但无非就是对容器的两端进行操作,或添加,或删除,或查看**。明白了这一点讲解起来就会非常简单。 + +*ArrayDeque*和*LinkedList*是*Deque*的两个通用实现,由于官方更推荐使用*AarryDeque*用作栈和队列,加之上一篇已经讲解过[LinkedList](https://tobebetterjavaer.com/collection/linkedlist.html),本文将着重讲解*ArrayDeque*的具体实现。 + +从名字可以看出*ArrayDeque*底层通过数组实现,为了满足可以同时在数组两端插入或删除元素的需求,该数组还必须是循环的,即**循环数组(circular array)**,也就是说数组的任何一点都可能被看作起点或者终点。 + +*ArrayDeque*是非线程安全的(not thread-safe),当多个线程同时使用的时候,需要手动同步;另外,该容器不允许放入`null`元素。 + +![ArrayDeque_base.png](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/collection/arraydeque-1e7086a3-3d31-4553-aa16-5eaf2193649e.png) + + +上图中我们看到,**`head`指向首端第一个有效元素,`tail`指向尾端第一个可以插入元素的空位**。因为是循环数组,所以`head`不一定总等于 0,`tail`也不一定总是比`head`大。 + +## 方法剖析 + +### addFirst() + +`addFirst(E e)`的作用是在*Deque*的首端插入元素,也就是在`head`的前面插入元素,在空间足够且下标没有越界的情况下,只需要将`elements[--head] = e`即可。 + + +![ArrayDeque_addFirst.png](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/collection/arraydeque-459afbba-2778-4241-97fb-f01a29b79458.png) + +实际需要考虑: + +1. 空间是否够用,以及 +2. 下标是否越界的问题。 + +上图中,如果`head`为`0`之后接着调用`addFirst()`,虽然空余空间还够用,但`head`为`-1`,下标越界了。下列代码很好的解决了这两个问题。 + + +```java +//addFirst(E e) +public void addFirst(E e) { + if (e == null)//不允许放入null + throw new NullPointerException(); + elements[head = (head - 1) & (elements.length - 1)] = e;//2.下标是否越界 + if (head == tail)//1.空间是否够用 + doubleCapacity();//扩容 +} +``` + +``` +//addFirst(E e) +public void addFirst(E e) { + if (e == null)//不允许放入null + throw new NullPointerException(); + elements\[head = (head - 1) & (elements.length - 1)\] = e;//2.下标是否越界 + if (head == tail)//1.空间是否够用 + doubleCapacity();//扩容 +} +``` + +上述代码我们看到,**空间问题是在插入之后解决的**,因为`tail`总是指向下一个可插入的空位,也就意味着`elements`数组至少有一个空位,所以插入元素的时候不用考虑空间问题。 + +下标越界的处理解决起来非常简单,`head = (head - 1) & (elements.length - 1)`就可以了,**这段代码相当于取余,同时解决了`head`为负值的情况**。因为`elements.length`必需是`2`的指数倍,`elements - 1`就是二进制低位全`1`,跟`head - 1`相与之后就起到了取模的作用,如果`head - 1`为负数(其实只可能是-1),则相当于对其取相对于`elements.length`的补码。 + +下面再说说扩容函数`doubleCapacity()`,其逻辑是申请一个更大的数组(原数组的两倍),然后将原数组复制过去。过程如下图所示: + +![ArrayDeque_doubleCapacity.png](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/collection/arraydeque-f1386b63-10be-4998-bb6d-bf6560cca7ee.png) + +图中我们看到,复制分两次进行,第一次复制`head`右边的元素,第二次复制`head`左边的元素。 + +``` +//doubleCapacity() +private void doubleCapacity() { + assert head == tail; + int p = head; + int n = elements.length; + int r = n - p; // head右边元素的个数 + int newCapacity = n << 1;//原空间的2倍 + if (newCapacity < 0) + throw new IllegalStateException("Sorry, deque too big"); + Object\[\] a = new Object\[newCapacity\]; + System.arraycopy(elements, p, a, 0, r);//复制右半部分,对应上图中绿色部分 + System.arraycopy(elements, 0, a, r, p);//复制左半部分,对应上图中灰色部分 + elements = (E\[\])a; + head = 0; + tail = n; +} +``` + +### addLast() + +`addLast(E e)`的作用是在*Deque*的尾端插入元素,也就是在`tail`的位置插入元素,由于`tail`总是指向下一个可以插入的空位,因此只需要`elements[tail] = e;`即可。插入完成后再检查空间,如果空间已经用光,则调用`doubleCapacity()`进行扩容。 + +![ArrayDeque_addLast.png](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/collection/arraydeque-832c796a-6c24-4546-9f91-22ed39884363.png) + +``` +public void addLast(E e) { + if (e == null)//不允许放入null + throw new NullPointerException(); + elements\[tail\] = e;//赋值 + if ( (tail = (tail + 1) & (elements.length - 1)) == head)//下标越界处理 + doubleCapacity();//扩容 +} +``` + +下标越界处理方式`addFirt()`中已经讲过,不再赘述。 + +### pollFirst() + +`pollFirst()`的作用是删除并返回*Deque*首端元素,也即是`head`位置处的元素。如果容器不空,只需要直接返回`elements[head]`即可,当然还需要处理下标的问题。由于`ArrayDeque`中不允许放入`null`,当`elements[head] == null`时,意味着容器为空。 + +``` +public E pollFirst() { + E result = elements\[head\]; + if (result == null)//null值意味着deque为空 + return null; + elements\[h\] = null;//let GC work + head = (head + 1) & (elements.length - 1);//下标越界处理 + return result; +} +``` + +### pollLast() + +`pollLast()`的作用是删除并返回*Deque*尾端元素,也即是`tail`位置前面的那个元素。 + +``` +public E pollLast() { + int t = (tail - 1) & (elements.length - 1);//tail的上一个位置是最后一个元素 + E result = elements\[t\]; + if (result == null)//null值意味着deque为空 + return null; + elements\[t\] = null;//let GC work + tail = t; + return result; +} +``` + +### peekFirst() + +`peekFirst()`的作用是返回但不删除*Deque*首端元素,也即是`head`位置处的元素,直接返回`elements[head]`即可。 + +``` +public E peekFirst() { + return elements\[head\]; // elements\[head\] is null if deque empty +} +``` + +### peekLast() + +`peekLast()`的作用是返回但不删除*Deque*尾端元素,也即是`tail`位置前面的那个元素。 + +``` +public E peekLast() { + return elements\[(tail - 1) & (elements.length - 1)\]; +} +``` + +>参考链接:[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) \ No newline at end of file diff --git a/docs/collection/arraylist.md b/docs/collection/arraylist.md index a46f96ee3..1358573b1 100644 --- a/docs/collection/arraylist.md +++ b/docs/collection/arraylist.md @@ -1,6 +1,6 @@ --- title: Java ArrayList详解(附源码分析) -shortTitle: Java ArrayList详解 +shortTitle: ArrayList详解 category: - Java核心 tag: diff --git a/docs/collection/hashmap.md b/docs/collection/hashmap.md index e0e82e6e4..4baa568be 100644 --- a/docs/collection/hashmap.md +++ b/docs/collection/hashmap.md @@ -1,6 +1,6 @@ --- title: Java HashMap详解(附源码分析) -shortTitle: Java HashMap详解 +shortTitle: HashMap详解 category: - Java核心 tag: diff --git a/docs/collection/linkedhashmap.md b/docs/collection/linkedhashmap.md new file mode 100644 index 000000000..ff14813fc --- /dev/null +++ b/docs/collection/linkedhashmap.md @@ -0,0 +1,351 @@ +--- +title: Java LinkedHashMap详解(附源码分析) +shortTitle: LinkedHashMap详解 +category: + - Java核心 +tag: + - 集合框架(容器) +description: Java程序员进阶之路,小白的零基础Java教程,Java LinkedHashMap详解 +head: + - - meta + - name: keywords + content: Java,Java SE,Java 基础,Java 教程,Java 程序员进阶之路,Java 入门,Java LinkedHashMap +--- + +俗话说了,“金无足赤人无完人”,HashMap 也不例外,有一种需求它就满足不了,假如我们需要一个按照插入顺序来排列的键值对集合,那 HashMap 就无能为力了。那该怎么办呢?必须得上今天这篇文章的主角:LinkedHashMap。 + +同学们好啊,还记得 [HashMap](https://tobebetterjavaer.com/collection/hashmap.html) 那篇吗?我自己感觉写得非常棒啊,既通俗易懂,又深入源码,真的是分析得透透彻彻、清清楚楚、明明白白的。(一不小心又甩了三个成语,有文化吧?)HashMap 哪哪都好,真的,只要你想用键值对,第一时间就应该想到它。 + +为了提高查找效率,HashMap 在插入的时候对键做了一次哈希算法,这就导致插入的元素是无序的。 + +对这一点还不太明白的同学,可以再回到 [HashMap](https://tobebetterjavaer.com/collection/hashmap.html) 那一篇,看看我对 `put()` 方法的讲解。 + +``` +final V putVal(int hash, K key, V value, boolean onlyIfAbsent, + +               boolean evict) { +    HashMap.Node[] tab; HashMap.Node p; int n, i; +    // ①、数组 table 为 null 时,调用 resize 方法创建默认大小的数组 +    if ((tab = table) == null || (n = tab.length) == 0) +        n = (tab = resize()).length; +    // ②、计算下标,如果该位置上没有值,则填充 +    if ((p = tab[i = (n - 1) & hash]) == null) +        tab[i] = newNode(hash, key, value, null); +} +``` + +这个公式 `i = (n - 1) & hash` 计算后的值并不是按照 0、1、2、3、4、5 这样有序的下标将键值对插入到数组当中的,而是有一定的随机性。 + +那 LinkedHashMap 就是为这个需求应运而生的。LinkedHashMap 继承了 HashMap,所以 HashMap 有的关于键值对的功能,它也有了。 + +``` +public class LinkedHashMap + +    extends HashMap + +    implements Map{} +``` + +此外,LinkedHashMap 内部又追加了双向链表,来维护元素的插入顺序。注意下面代码中的 before 和 after,它俩就是用来维护当前元素的前一个元素和后一个元素的顺序的。 + +``` +static class Entry extends HashMap.Node { +    LinkedHashMap.Entry before, after; +    Entry(int hash, K key, V value, HashMap.Node next) { +        super(hash, key, value, next); +    } +} +``` + +关于双向链表,同学们可以回头看一遍我写的 [LinkedList](https://tobebetterjavaer.com/collection/linkedlist.html) 那篇文章,会对理解本篇的 LinkedHashMap 有很大的帮助。 + +## 01、插入顺序 + +在 [HashMap](https://tobebetterjavaer.com/collection/hashmap.html) 那篇文章里,我有讲解到一点,不知道同学们记不记得,就是 null 会插入到 HashMap 的第一位。 + +``` +Map hashMap = new HashMap<>(); +hashMap.put("沉", "沉默王二"); +hashMap.put("默", "沉默王二"); +hashMap.put("王", "沉默王二"); +hashMap.put("二", "沉默王二"); +hashMap.put(null, null); + +for (String key : hashMap.keySet()) { +    System.out.println(key + " : " + hashMap.get(key)); +} +``` + +输出的结果是: + +``` +null : null +默 : 沉默王二 +沉 : 沉默王二 +王 : 沉默王二 +二 : 沉默王二 +``` + +虽然 null 最后一位 put 进去的,但在遍历输出的时候,跑到了第一位。 + +那再来对比看一下 LinkedHashMap。 + +``` +Map linkedHashMap = new LinkedHashMap<>(); +linkedHashMap.put("沉", "沉默王二"); +linkedHashMap.put("默", "沉默王二"); +linkedHashMap.put("王", "沉默王二"); +linkedHashMap.put("二", "沉默王二"); +linkedHashMap.put(null, null); + +for (String key : linkedHashMap.keySet()) { +    System.out.println(key + " : " + linkedHashMap.get(key)); +} +``` + +输出结果是: + +``` +沉 : 沉默王二 +默 : 沉默王二 +王 : 沉默王二 +二 : 沉默王二 +null : null +``` + +null 在最后一位插入,在最后一位输出。 + +输出结果可以再次证明,HashMap 是无序的,LinkedHashMap 是可以维持插入顺序的。 + +那 LinkedHashMap 是如何做到这一点呢?我相信同学们和我一样,非常希望知道原因。 + +要想搞清楚,就需要深入研究一下 LinkedHashMap 的源码。LinkedHashMap 并未重写 HashMap 的 `put()` 方法,而是重写了 `put()` 方法需要调用的内部方法 `newNode()`。 + +``` +HashMap.Node newNode(int hash, K key, V value, HashMap.Node e) { +    LinkedHashMap.Entry p = +            new LinkedHashMap.Entry<>(hash, key, value, e); +    linkNodeLast(p); +    return p; +} +``` + +前面说了,LinkedHashMap.Entry 继承了 HashMap.Node,并且追加了两个字段 before 和 after。 + +那,紧接着来看看 `linkNodeLast()` 方法: + +``` +private void linkNodeLast(LinkedHashMap.Entry p) { +    LinkedHashMap.Entry last = tail; +    tail = p; +    if (last == null) +        head = p; +    else { +        p.before = last; +        last.after = p; +    } +} +``` + +看到了吧,LinkedHashMap 在添加第一个元素的时候,会把 head 赋值为第一个元素,等到第二个元素添加进来的时候,会把第二个元素的 before 赋值为第一个元素,第一个元素的 afer 赋值为第二个元素。 + +这就保证了键值对是按照插入顺序排列的,明白了吧? + +*注:这篇文章当时用到的 JDK 版本为 14(当时的最新版,建议使用 Java8 或者 Java 13)*。 + +## 02、访问顺序 + +LinkedHashMap 不仅能够维持插入顺序,还能够维持访问顺序。访问包括调用 `get()` 方法、`remove()` 方法和 `put()` 方法。 + +要维护访问顺序,需要我们在声明 LinkedHashMap 的时候指定三个参数。 + +``` +LinkedHashMap map = new LinkedHashMap<>(16, .75f, true); +``` + +第一个参数和第二个参数,看过 [HashMap](https://tobebetterjavaer.com/collection/hashmap.html) 的同学们应该很熟悉了,指的是初始容量和负载因子。 + +第三个参数如果为 true 的话,就表示 LinkedHashMap 要维护访问顺序;否则,维护插入顺序。默认是 false。 + +``` +Map linkedHashMap = new LinkedHashMap<>(16, .75f, true); +linkedHashMap.put("沉", "沉默王二"); +linkedHashMap.put("默", "沉默王二"); +linkedHashMap.put("王", "沉默王二"); +linkedHashMap.put("二", "沉默王二"); + +System.out.println(linkedHashMap); + +linkedHashMap.get("默"); +System.out.println(linkedHashMap); + +linkedHashMap.get("王"); +System.out.println(linkedHashMap); +``` + +输出的结果如下所示: + +``` +{沉=沉默王二, 默=沉默王二, 王=沉默王二, 二=沉默王二} +{沉=沉默王二, 王=沉默王二, 二=沉默王二, 默=沉默王二} +{沉=沉默王二, 二=沉默王二, 默=沉默王二, 王=沉默王二} +``` + +当我们使用 `get()` 方法访问键位“默”的元素后,输出结果中,`默=沉默王二` 在最后;当我们访问键位“王”的元素后,输出结果中,`王=沉默王二` 在最后,`默=沉默王二` 在倒数第二位。 + +也就是说,最不经常访问的放在头部,这就有意思了。有意思在哪呢? + +我们可以使用 LinkedHashMap 来实现 LRU 缓存,LRU 是 Least Recently Used 的缩写,即最近最少使用,是一种常用的页面置换算法,选择最近最久未使用的页面予以淘汰。 + +``` +public class MyLinkedHashMap extends LinkedHashMap { + +    private static final int MAX_ENTRIES = 5; + +    public MyLinkedHashMap( + +            int initialCapacity, float loadFactor, boolean accessOrder) { +        super(initialCapacity, loadFactor, accessOrder); +    } + +    @Override +    protected boolean removeEldestEntry(Map.Entry eldest) { +        return size() > MAX_ENTRIES; +    } + +} +``` + +MyLinkedHashMap 是一个自定义类,它继承了 LinkedHashMap,并且重写了 `removeEldestEntry()` 方法——使 Map 最多可容纳 5 个元素,超出后就淘汰。 + +我们来测试一下。 + +``` +MyLinkedHashMap map = new MyLinkedHashMap<>(16,0.75f,true); +map.put("沉", "沉默王二"); +map.put("默", "沉默王二"); +map.put("王", "沉默王二"); +map.put("二", "沉默王二"); +map.put("一枚有趣的程序员", "一枚有趣的程序员"); + +System.out.println(map); + +map.put("一枚有颜值的程序员", "一枚有颜值的程序员"); +System.out.println(map); + +map.put("一枚有才华的程序员","一枚有才华的程序员"); +System.out.println(map); +``` + +输出结果如下所示: + +``` +{沉=沉默王二, 默=沉默王二, 王=沉默王二, 二=沉默王二, 一枚有趣的程序员=一枚有趣的程序员} +{默=沉默王二, 王=沉默王二, 二=沉默王二, 一枚有趣的程序员=一枚有趣的程序员, 一枚有颜值的程序员=一枚有颜值的程序员} +{王=沉默王二, 二=沉默王二, 一枚有趣的程序员=一枚有趣的程序员, 一枚有颜值的程序员=一枚有颜值的程序员, 一枚有才华的程序员=一枚有才华的程序员} +``` + +`沉=沉默王二` 和 `默=沉默王二` 依次被淘汰出局。 + +假如在 put “一枚有才华的程序员”之前 get 了键位为“默”的元素: + +``` +MyLinkedHashMap map = new MyLinkedHashMap<>(16,0.75f,true); +map.put("沉", "沉默王二"); +map.put("默", "沉默王二"); +map.put("王", "沉默王二"); +map.put("二", "沉默王二"); +map.put("一枚有趣的程序员", "一枚有趣的程序员"); + +System.out.println(map); + +map.put("一枚有颜值的程序员", "一枚有颜值的程序员"); +System.out.println(map); + +map.get("默"); +map.put("一枚有才华的程序员","一枚有才华的程序员"); +System.out.println(map); +``` + +那输出结果就变了,对吧? + +``` +{沉=沉默王二, 默=沉默王二, 王=沉默王二, 二=沉默王二, 一枚有趣的程序员=一枚有趣的程序员} +{默=沉默王二, 王=沉默王二, 二=沉默王二, 一枚有趣的程序员=一枚有趣的程序员, 一枚有颜值的程序员=一枚有颜值的程序员} +{二=沉默王二, 一枚有趣的程序员=一枚有趣的程序员, 一枚有颜值的程序员=一枚有颜值的程序员, 默=沉默王二, 一枚有才华的程序员=一枚有才华的程序员} +``` + +`沉=沉默王二` 和 `王=沉默王二` 被淘汰出局了。 + +那 LinkedHashMap 是如何来维持访问顺序呢?同学们感兴趣的话,可以研究一下下面这三个方法。 + +``` +void afterNodeAccess(Node p) { } +void afterNodeInsertion(boolean evict) { } +void afterNodeRemoval(Node p) { } +``` + +`afterNodeAccess()` 会在调用 `get()` 方法的时候被调用,`afterNodeInsertion()` 会在调用 `put()` 方法的时候被调用,`afterNodeRemoval()` 会在调用 `remove()` 方法的时候被调用。 + +我来以 `afterNodeAccess()` 为例来讲解一下。 + +``` +void afterNodeAccess(HashMap.Node e) { // move node to last +    LinkedHashMap.Entry last; +    if (accessOrder && (last = tail) != e) { +        LinkedHashMap.Entry p = +                (LinkedHashMap.Entry)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; +    } +} +``` + +哪个元素被 get 就把哪个元素放在最后。了解了吧? + +那同学们可能还想知道,为什么 LinkedHashMap 能实现 LRU 缓存,把最不经常访问的那个元素淘汰? + +在插入元素的时候,需要调用 `put()` 方法,该方法最后会调用 `afterNodeInsertion()` 方法,这个方法被 LinkedHashMap 重写了。 + +``` +void afterNodeInsertion(boolean evict) { // possibly remove eldest +    LinkedHashMap.Entry first; +    if (evict && (first = head) != null && removeEldestEntry(first)) { +        K key = first.key; +        removeNode(hash(key), key, null, false, true); +    } +} +``` + +`removeEldestEntry()` 方法会判断第一个元素是否超出了可容纳的最大范围,如果超出,那就会调用 `removeNode()` 方法对最不经常访问的那个元素进行删除。 + +## 03、最后 + +由于 LinkedHashMap 要维护双向链表,所以 LinkedHashMap 在插入、删除操作的时候,花费的时间要比 HashMap 多一些。 + +这也是没办法的事,对吧,欲戴皇冠必承其重嘛。既然想要维护元素的顺序,总要付出点代价才行。 + + + +---- + +最近整理了一份牛逼的学习资料,包括但不限于Java基础部分(JVM、Java集合框架、多线程),还囊括了 **数据库、计算机网络、算法与数据结构、设计模式、框架类Spring、Netty、微服务(Dubbo,消息队列) 网关** 等等等等……详情戳:[可以说是2022年全网最全的学习和找工作的PDF资源了](https://tobebetterjavaer.com/pdf/programmer-111.html) + +关注二哥的原创公众号 **沉默王二**,回复**111** 即可免费领取。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) \ No newline at end of file diff --git a/docs/collection/linkedlist.md b/docs/collection/linkedlist.md index 09fca2348..e884c3c8a 100644 --- a/docs/collection/linkedlist.md +++ b/docs/collection/linkedlist.md @@ -1,6 +1,6 @@ --- title: Java LinkedList详解(附源码分析) -shortTitle: Java LinkedList详解 +shortTitle: LinkedList详解 category: - Java核心 tag: diff --git a/docs/collection/treemap.md b/docs/collection/treemap.md new file mode 100644 index 000000000..0fdd8e1f5 --- /dev/null +++ b/docs/collection/treemap.md @@ -0,0 +1,256 @@ +--- +title: Java TreeMap详解(附源码分析) +shortTitle: TreeMap详解 +category: + - Java核心 +tag: + - 集合框架(容器) +description: Java程序员进阶之路,小白的零基础Java教程,Java TreeMap详解 +head: + - - meta + - name: keywords + content: Java,Java SE,Java 基础,Java 教程,Java 程序员进阶之路,Java 入门,Java TreeMap +--- + + + +TreeMap,虽然也是个 Map,但存在感太低了。我做程序员这十多年里,HashMap 用了超过十年,TreeMap 只用了多字里那么一小会儿一小会儿,真的是,太惨了。 + +虽然 TreeMap 用得少,但还是有用处的。 + +之前 [LinkedHashMap](https://tobebetterjavaer.com/collection/linkedhashmap.html) 那篇文章里提到过了,HashMap 是无序的,所有有了 LinkedHashMap,加上了双向链表后,就可以保持元素的插入顺序和访问顺序,那 TreeMap 呢? + +TreeMap 由红黑树实现,可以保持元素的自然顺序,或者实现了 Comparator 接口的自定义顺序。 + +可能有些同学不知道红黑树,理解起来 TreeMap 就有点难度,那我先来普及一下: + +> 红黑树(英语:Red–black tree)是一种自平衡的二叉查找树(Binary Search Tree),结构复杂,但却有着良好的性能,完成查找、插入和删除的时间复杂度均为 log(n)。 + +二叉查找树又是什么呢? + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/collection/treemap-c5f29a61-91e0-47e8-9e16-055e1bea5b33.jpg) + +上图中这棵树,就是一颗典型的二叉查找树: + +1)左子树上所有节点的值均小于或等于它的根结点的值。 + +2)右子树上所有节点的值均大于或等于它的根结点的值。 + +3)左、右子树也分别为二叉排序树。 + +理解二叉查找树了吧?不过,二叉查找树有一个不足,就是容易变成瘸子,就是一侧多,一侧少,就像下图这样: + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/collection/treemap-6c3309a0-3737-4677-99c1-e8a156140d62.jpg) + + +查找的效率就要从 log(n) 变成 o(n) 了,对吧?必须要平衡一下,对吧?于是就有了平衡二叉树,左右两个子树的高度差的绝对值不超过 1,就像下图这样: + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/collection/treemap-b8f74a3d-baf9-4192-a0e0-cdd07955d784.jpg) + + +红黑树,顾名思义,就是节点是红色或者黑色的平衡二叉树,它通过颜色的约束来维持着二叉树的平衡: + +1)每个节点都只能是红色或者黑色 + +2)根节点是黑色 + +3)每个叶节点(NIL 节点,空节点)是黑色的。 + +4)如果一个节点是红色的,则它两个子节点都是黑色的。也就是说在一条路径上不能出现相邻的两个红色节点。 + +5)从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/collection/treemap-3373c98c-d82f-4a7f-949e-a7fe0d99314c.jpg) + + +那,关于红黑树,同学们就先了解到这,脑子里有个大概的印象,知道 TreeMap 是个什么玩意。 + +## 01、自然顺序 + +默认情况下,TreeMap 是根据 key 的自然顺序排列的。比如说整数,就是升序,1、2、3、4、5。 + +``` +TreeMap mapInt = new TreeMap<>(); +mapInt.put(3, "沉默王二"); +mapInt.put(2, "沉默王二"); +mapInt.put(1, "沉默王二"); +mapInt.put(5, "沉默王二"); +mapInt.put(4, "沉默王二"); + +System.out.println(mapInt); +``` + +输出结果如下所示: + +``` +{1=沉默王二, 2=沉默王二, 3=沉默王二, 4=沉默王二, 5=沉默王二} +``` + +TreeMap 是怎么做到的呢?想一探究竟,就得上源码了,来看 TreeMap 的 `put()` 方法(省去了一部分,版本为 JDK 14): + +``` +public V put(K key, V value) { +    TreeMap.Entry t = root; +    int cmp; +    TreeMap.Entry parent; +    // split comparator and comparable paths +    Comparator cpr = comparator; +    if (cpr != null) { +    } +    else { +        @SuppressWarnings("unchecked") +        Comparable k = (Comparable) key; +        do { +            parent = t; +            cmp = k.compareTo(t.key); +            if (cmp < 0) +                t = t.left; +            else if (cmp > 0) +                t = t.right; +            else +                return t.setValue(value); +        } while (t != null); +    } +    return null; +} +``` + +注意 `cmp = k.compareTo(t.key)` 这行代码,就是用来进行 key 的比较的,由于此时 key 是 int,所以就会调用 Integer 类的 `compareTo()` 方法进行比较。 + +``` +public int compareTo(Integer anotherInteger) { +    return compare(this.value, anotherInteger.value); +} + +public static int compare(int x, int y) { +    return (x < y) ? -1 : ((x == y) ? 0 : 1); +} +``` + +那相应的,如果 key 是字符串的话,也就会调用 String 类的 `compareTo()` 方法进行比较。 + +``` +public int compareTo(String anotherString) { +    byte v1[] = value; +    byte v2[] = anotherString.value; +    byte coder = coder(); +    if (coder == anotherString.coder()) { +        return coder == LATIN1 ? StringLatin1.compareTo(v1, v2) +                : StringUTF16.compareTo(v1, v2); +    } +    return coder == LATIN1 ? StringLatin1.compareToUTF16(v1, v2) +            : StringUTF16.compareToLatin1(v1, v2); +} +``` + +由于内部是由字符串的字节数组的字符进行比较的,是不是听起来很绕?对,就是很绕,所以使用中文字符串作为 key 的话,看不出来效果。 + +``` +TreeMap mapString = new TreeMap<>(); +mapString.put("c", "沉默王二"); +mapString.put("b", "沉默王二"); +mapString.put("a", "沉默王二"); +mapString.put("e", "沉默王二"); +mapString.put("d", "沉默王二"); + +System.out.println(mapString); +``` + +输出结果如下所示: + +``` +{a=沉默王二, b=沉默王二, c=沉默王二, d=沉默王二, e=沉默王二} +``` + +字母的升序,对吧? + +## 02、自定义排序 + +如果自然顺序不满足,那就可以在声明 TreeMap 对象的时候指定排序规则。 + +``` +TreeMap mapIntReverse = new TreeMap<>(Comparator.reverseOrder()); +mapIntReverse.put(3, "沉默王二"); +mapIntReverse.put(2, "沉默王二"); +mapIntReverse.put(1, "沉默王二"); +mapIntReverse.put(5, "沉默王二"); +mapIntReverse.put(4, "沉默王二"); + +System.out.println(mapIntReverse); +``` + +TreeMap 提供了可以指定排序规则的构造方法: + +``` +public TreeMap(Comparator comparator) { +    this.comparator = comparator; +} +``` + +`Comparator.reverseOrder()` 返回的是 ReverseComparator 对象,就是用来反转顺序的,非常方便。 + +所以,输出结果如下所示: + +``` +{5=沉默王二, 4=沉默王二, 3=沉默王二, 2=沉默王二, 1=沉默王二} +``` + +HashMap 是无序的,插入的顺序随着元素的增加会不停地变动。但 TreeMap 能够至始至终按照指定的顺序排列,这对于需要自定义排序的场景,实在是太有用了! + +## 03、排序的好处 + +既然 TreeMap 的元素是经过排序的,那找出最大的那个,最小的那个,或者找出所有大于或者小于某个值的键来说,就方便多了。 + +``` +Integer highestKey = mapInt.lastKey(); +Integer lowestKey = mapInt.firstKey(); +Set keysLessThan3 = mapInt.headMap(3).keySet(); +Set keysGreaterThanEqTo3 = mapInt.tailMap(3).keySet(); + +System.out.println(highestKey); +System.out.println(lowestKey); + +System.out.println(keysLessThan3); +System.out.println(keysGreaterThanEqTo3); +``` + +TreeMap 考虑得很周全,恰好就提供了 `lastKey()`、`firstKey()` 这样获取最后一个 key 和第一个 key 的方法。 + +`headMap()` 获取的是到指定 key 之前的 key;`tailMap()` 获取的是指定 key 之后的 key(包括指定 key)。 + +来看一下输出结果: + +``` +5 +1 +[1, 2] +[3, 4, 5] +``` + +## 04、如何选择 Map + +在学习 TreeMap 之前,我们已经学习了 [HashMap](https://tobebetterjavaer.com/collection/hashmap.html) 和 [LinkedHashMap](https://tobebetterjavaer.com/collection/linkedhashmap.html) ,那如何从它们三个中间选择呢? + +HashMap、LinkedHashMap、TreeMap 都实现了 Map 接口,并提供了几乎相同的功能(增删改查)。它们之间最大的区别就在于元素的顺序: + +HashMap 完全不保证元素的顺序,添加了新的元素,之前的顺序可能完全逆转。 + +LinkedHashMap 默认会保持元素的插入顺序。 + +TreeMap 默认会保持 key 的自然顺序(根据 `compareTo()` 方法)。 + +来个表格吧,一目了然。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/collection/treemap-f94219fb-b6ef-4192-8174-4759498f857f.jpg) + + + + +---- + +最近整理了一份牛逼的学习资料,包括但不限于Java基础部分(JVM、Java集合框架、多线程),还囊括了 **数据库、计算机网络、算法与数据结构、设计模式、框架类Spring、Netty、微服务(Dubbo,消息队列) 网关** 等等等等……详情戳:[可以说是2022年全网最全的学习和找工作的PDF资源了](https://tobebetterjavaer.com/pdf/programmer-111.html) + +关注二哥的原创公众号 **沉默王二**,回复**111** 即可免费领取。 + +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png) + diff --git a/docs/home.md b/docs/home.md index b20548145..2b9a239eb 100644 --- a/docs/home.md +++ b/docs/home.md @@ -160,6 +160,12 @@ head: - [海康威视一面:Java中Iterator和Iterable有什么区别?](collection/iterator-iterable.md) - [为什么阿里巴巴强制不要在foreach里执行删除操作?还不是因为fail-fast](collection/fail-fast.md) - [Java HashMap详解(附源码分析)](collection/hashmap.md) +- [Java LinkedHashMap详解(附源码分析)](collection/linkedhashmap.md) +- [Java TreeMap详解(附源码分析)](collection/treemap.md) +- [详解 Java 中的堆和队列(Stack and Queue 附源码分析)](collection/arraydeque.md) +- [详解 Java 中的优先级队列(PriorityQueue 附源码分析)](collection/PriorityQueue.md) +- [Java WeakHashMap详解(附源码分析)](collection/WeakHashMap.md) + ### Java输入输出 diff --git a/images/collection/PriorityQueue-0fb89aa7-c8fa-4fad-adbb-40c61c3bb0e9.png b/images/collection/PriorityQueue-0fb89aa7-c8fa-4fad-adbb-40c61c3bb0e9.png new file mode 100644 index 000000000..536f57def Binary files /dev/null and b/images/collection/PriorityQueue-0fb89aa7-c8fa-4fad-adbb-40c61c3bb0e9.png differ diff --git a/images/collection/PriorityQueue-5059f157-845e-4d1c-b993-5cfe539d5607.png b/images/collection/PriorityQueue-5059f157-845e-4d1c-b993-5cfe539d5607.png new file mode 100644 index 000000000..5ff196293 Binary files /dev/null and b/images/collection/PriorityQueue-5059f157-845e-4d1c-b993-5cfe539d5607.png differ diff --git a/images/collection/PriorityQueue-8dca2f55-a7c7-49e1-95a5-df1a34f2aef5.png b/images/collection/PriorityQueue-8dca2f55-a7c7-49e1-95a5-df1a34f2aef5.png new file mode 100644 index 000000000..78202ff57 Binary files /dev/null and b/images/collection/PriorityQueue-8dca2f55-a7c7-49e1-95a5-df1a34f2aef5.png differ diff --git a/images/collection/PriorityQueue-e25ba931-2e6f-4c17-84b8-9b959733d541.png b/images/collection/PriorityQueue-e25ba931-2e6f-4c17-84b8-9b959733d541.png new file mode 100644 index 000000000..1be1c0fcd Binary files /dev/null and b/images/collection/PriorityQueue-e25ba931-2e6f-4c17-84b8-9b959733d541.png differ diff --git a/images/collection/PriorityQueue-ed0d08d3-b38e-44a1-a710-ee7a01afda62.png b/images/collection/PriorityQueue-ed0d08d3-b38e-44a1-a710-ee7a01afda62.png new file mode 100644 index 000000000..4be6808b9 Binary files /dev/null and b/images/collection/PriorityQueue-ed0d08d3-b38e-44a1-a710-ee7a01afda62.png differ diff --git a/images/collection/arraydeque-1e7086a3-3d31-4553-aa16-5eaf2193649e.png b/images/collection/arraydeque-1e7086a3-3d31-4553-aa16-5eaf2193649e.png new file mode 100644 index 000000000..abd74f67a Binary files /dev/null and b/images/collection/arraydeque-1e7086a3-3d31-4553-aa16-5eaf2193649e.png differ diff --git a/images/collection/arraydeque-459afbba-2778-4241-97fb-f01a29b79458.png b/images/collection/arraydeque-459afbba-2778-4241-97fb-f01a29b79458.png new file mode 100644 index 000000000..2482b5ec3 Binary files /dev/null and b/images/collection/arraydeque-459afbba-2778-4241-97fb-f01a29b79458.png differ diff --git a/images/collection/arraydeque-832c796a-6c24-4546-9f91-22ed39884363.png b/images/collection/arraydeque-832c796a-6c24-4546-9f91-22ed39884363.png new file mode 100644 index 000000000..e1dc37b66 Binary files /dev/null and b/images/collection/arraydeque-832c796a-6c24-4546-9f91-22ed39884363.png differ diff --git a/images/collection/arraydeque-f1386b63-10be-4998-bb6d-bf6560cca7ee.png b/images/collection/arraydeque-f1386b63-10be-4998-bb6d-bf6560cca7ee.png new file mode 100644 index 000000000..005535460 Binary files /dev/null and b/images/collection/arraydeque-f1386b63-10be-4998-bb6d-bf6560cca7ee.png differ diff --git a/images/collection/treemap-3373c98c-d82f-4a7f-949e-a7fe0d99314c.jpg b/images/collection/treemap-3373c98c-d82f-4a7f-949e-a7fe0d99314c.jpg new file mode 100644 index 000000000..767fe6907 Binary files /dev/null and b/images/collection/treemap-3373c98c-d82f-4a7f-949e-a7fe0d99314c.jpg differ diff --git a/images/collection/treemap-6c3309a0-3737-4677-99c1-e8a156140d62.jpg b/images/collection/treemap-6c3309a0-3737-4677-99c1-e8a156140d62.jpg new file mode 100644 index 000000000..1d69e7b41 Binary files /dev/null and b/images/collection/treemap-6c3309a0-3737-4677-99c1-e8a156140d62.jpg differ diff --git a/images/collection/treemap-b8f74a3d-baf9-4192-a0e0-cdd07955d784.jpg b/images/collection/treemap-b8f74a3d-baf9-4192-a0e0-cdd07955d784.jpg new file mode 100644 index 000000000..10bcde967 Binary files /dev/null and b/images/collection/treemap-b8f74a3d-baf9-4192-a0e0-cdd07955d784.jpg differ diff --git a/images/collection/treemap-c5f29a61-91e0-47e8-9e16-055e1bea5b33.jpg b/images/collection/treemap-c5f29a61-91e0-47e8-9e16-055e1bea5b33.jpg new file mode 100644 index 000000000..ebf348915 Binary files /dev/null and b/images/collection/treemap-c5f29a61-91e0-47e8-9e16-055e1bea5b33.jpg differ diff --git a/images/collection/treemap-f94219fb-b6ef-4192-8174-4759498f857f.jpg b/images/collection/treemap-f94219fb-b6ef-4192-8174-4759498f857f.jpg new file mode 100644 index 000000000..5bb50c4ff Binary files /dev/null and b/images/collection/treemap-f94219fb-b6ef-4192-8174-4759498f857f.jpg differ diff --git a/images/nice-article/weixin-huanyldtreemapdtcdk.jpg b/images/nice-article/weixin-huanyldtreemapdtcdk.jpg new file mode 100644 index 000000000..98982023f Binary files /dev/null and b/images/nice-article/weixin-huanyldtreemapdtcdk.jpg differ diff --git a/images/nice-article/weixin-tingstxngbdjavadlinkedhashmapkx.jpg b/images/nice-article/weixin-tingstxngbdjavadlinkedhashmapkx.jpg new file mode 100644 index 000000000..84bfba71f Binary files /dev/null and b/images/nice-article/weixin-tingstxngbdjavadlinkedhashmapkx.jpg differ