用C++模拟Java核心容器:从底层实现到面试高频考点解析
1. 项目概述与核心价值最近在整理自己的技术笔记翻到了几年前为了深入理解Java集合框架而做的一个小项目。当时市面上关于Java容器Collection Framework的面试题和八股文已经很多了但总觉得背那些“ArrayList底层是数组LinkedList底层是链表”的答案隔靴搔痒知其然不知其所以然。于是我决定用C这个更贴近系统底层的语言去亲手模拟实现一遍Java核心容器的简化版。这个想法听起来有点“绕远路”但实践下来收获远超预期。它不仅让我对Java容器的内存管理、迭代器失效、扩容策略等核心机制有了刻骨铭心的理解更在后来面试腾讯等大厂C岗位时成为了我技术深度的有力佐证。面试官听到我用C模拟Java容器并讨论其设计差异时往往能引发更深层次的探讨。这个项目本质上是一个跨语言的设计模式与数据结构实践。它不追求功能完整而是聚焦于用C的语法和思想去还原Java容器设计中几个最经典、最常被问到的“灵魂”。比如如何用C的模板泛型模拟Java的泛型如何手动管理内存来模拟JVM的自动垃圾回收在容器中的表现迭代器在两种语言中的安全性和实现方式有何不同通过动手实现这些抽象的概念会变得无比具体。完成核心模拟后我还顺带探索了如何使用Doxygen等工具为C项目自动生成API文档这几乎是工业级C项目的标配技能。最后我会结合最新的面试趋势分享一些从这次实践中提炼出的、在2024年依然高频出现的C/C高级面试题及其解题思路。无论你是想深化对Java集合的理解还是准备C面试亦或是学习如何设计一个健壮的、有良好文档的库相信这篇长文都能给你带来实实在在的收获。2. 整体设计与思路拆解2.1 为什么用C模拟Java容器这可能是很多人的第一个疑问。直接看Java源码OpenJDK不就行了吗当然可以但存在一些门槛和视角局限。首先Java源码夹杂着大量的工程优化、历史兼容代码和JVM内部接口对于初学者或旨在理解核心设计的人来说信息噪音较大。其次Java的内存管理对开发者是透明的而理解容器特别是涉及扩容、拷贝、迭代器失效时内存的申请、释放、移动是关键。用C来实现迫使你必须显式地考虑这些细节比如new/delete的配对深拷贝与浅拷贝的选择这能让你真正体会到ArrayList在add时内部数组“不够用了”到底意味着什么操作成本有多高。更重要的是这是一种降维打击式的学习方法。C给了你更底层的控制权让你从“语言运行时”的层面跳脱出来以“系统设计者”的视角去看待一个容器库应该提供哪些接口、保证哪些异常安全性、如何平衡性能与易用性。当你用C的std::vector去模拟ArrayList时你会自然地去比较两者在扩容因子C通常是2倍Java的ArrayList是1.5倍、迭代器失效规则上的差异这种对比带来的理解是单看Java源码无法获得的。2.2 模拟目标与范围界定贪多嚼不烂。Java的java.util包下容器类众多我们不可能也没必要全部模拟。我们的目标是选取最具代表性、面试最高频的几个类实现其最核心的接口和行为逻辑。核心模拟目标MyArrayList模拟java.util.ArrayList。核心是动态数组管理重点实现动态扩容1.5倍因子、add/get/remove操作、迭代器及其失效行为、modCount机制快速失败机制。MyLinkedList模拟java.util.LinkedList。核心是双向链表重点实现头尾节点操作、在任意位置插入删除、双向迭代器。MyHashMap模拟java.util.HashMap。这是重中之重也是面试难点。核心是数组链表/红黑树的桶结构重点实现hashCode模拟、put/get操作、扩容2倍负载因子0.75、链表转红黑树简化版可只实现链表。接口设计原则我们将尽量模仿Java的接口命名风格但用C的方式实现。例如void add(const T element)对应boolean add(E e)T get(int index)对应E get(int index)Iterator begin()返回一个迭代器类对象。我们不会实现Java集合框架完整的继承树如Collection,List,AbstractList而是让每个类独立专注于其数据结构的本质。这能让我们把精力集中在核心算法和内存管理上。2.3 技术栈与工具选型语言C11/14。使用现代C的特性可以让代码更安全、简洁。例如使用智能指针std::unique_ptr辅助管理数组内存使用移动语义优化临时对象。编译与构建CMake。这是管理跨平台C项目的事实标准便于组织源文件、管理依赖和定义编译选项。API文档生成Doxygen。它可以从代码注释中自动生成HTML、LaTeX等格式的文档是C项目文档化的首选工具。测试简单的驱动程序main.cpp进行功能验证。对于更严谨的项目可以考虑Google Test框架。注意我们选择C标准库中的std::vector作为MyArrayList的内部数组容器吗不那样就失去了“模拟底层”的意义。我们将使用原始的指针和new[]/delete[]来手动管理动态数组这才是理解底层的关键。对于MyLinkedList的节点我们也使用new/delete来创建和销毁。3. 核心细节解析与实操要点3.1 MyArrayList动态数组的“灵魂”在于扩容ArrayList的核心是一个Object[] elementData。在C中我们用T* m_data来表示。它的难点和精华都在扩容。扩容策略详解Java的ArrayList默认初始容量是10扩容时新容量 旧容量 (旧容量 1)即1.5倍。为什么是1.5而不是2这是一个空间与时间的权衡。2倍扩容增长迅猛能减少扩容次数但可能导致更多的内存浪费。1.5倍是经验值在减少扩容次数和控制内存浪费之间取得了一个较好的平衡。在C的std::vector中通常采用2倍扩容这更倾向于性能优先。我们的实现步骤成员变量T* m_data;数组指针size_t m_size;当前元素数量size_t m_capacity;当前数组容量。add操作逻辑检查if (m_size m_capacity)如果满了则需要扩容。扩容计算新容量new_cap m_capacity (m_capacity 1);如果new_cap小于某个最小值如初始容量则设为该最小值。申请新内存T* new_data new T[new_cap];。关键步骤元素迁移。这里必须使用std::copy或循环进行拷贝构造对于非平凡类型直接内存拷贝如memcpy是危险的。std::copy会调用每个元素的拷贝构造函数或拷贝赋值运算符。释放旧内存delete[] m_data;。更新指针和容量m_data new_data; m_capacity new_cap;。在数组末尾构造新元素m_data[m_size] element;这里涉及T的拷贝赋值或原地构造更优的做法是使用placement new但为简化我们假设T有合适的赋值操作。m_size。迭代器与快速失败fail-fastJava的ArrayList内部有一个modCount修改次数字段。任何结构性修改增、删都会使其递增。迭代器在创建时会记录当前的modCount在每次next()或remove()操作前会检查迭代器记录的modCount是否与集合当前的modCount相等若不相等则抛出ConcurrentModificationException。这是为了在多线程环境下或单线程迭代过程中直接调用集合的remove方法快速发现并发修改避免产生未定义行为。在我们的C模拟中也需要实现这个机制虽然C标准库容器不提供这个保证它更依赖清晰的迭代器失效规则。我们可以在MyArrayList中添加一个int m_mod_count;在add、remove时递增。然后实现一个Iterator内部类它持有集合的引用或指针以及创建时记录的expected_mod_count。在解引用或前进前进行检查。class MyArrayList { // ... int m_mod_count 0; public: class Iterator { MyArrayList list; size_t index; int expected_mod_count; public: Iterator(MyArrayList lst, size_t idx) : list(lst), index(idx), expected_mod_count(lst.m_mod_count) {} T operator*() { if (expected_mod_count ! list.m_mod_count) { throw std::runtime_error(Concurrent modification detected); } // ... 边界检查 return list.m_data[index]; } // ... 其他操作符重载 }; };3.2 MyLinkedList指针操作的精准舞蹈链表的核心是节点Node和指针操作。相比数组链表在中间插入删除是O(1)如果已有位置指针但随机访问是O(n)。节点设计template typename T struct Node { T data; Node* prev; Node* next; Node(const T val, Node* p nullptr, Node* n nullptr) : data(val), prev(p), next(n) {} };我们使用双向链表方便前后遍历。MyLinkedList类内部维护Node* head;和Node* tail;指针可能还有size_t m_size;。插入与删除的指针操作这是链表最容易出错的地方。以在指定节点pos前插入一个新节点new_node为例正确的顺序是new_node-prev pos-prev;new_node-next pos;如果pos-prev不是nullptr即pos不是头节点pos-prev-next new_node;pos-prev new_node;如果pos是头节点需要更新head new_node;删除节点del_node时如果del_node-prev不是nullptrdel_node-prev-next del_node-next;如果del_node-next不是nullptrdel_node-next-prev del_node-prev;如果del_node是头节点更新head del_node-next;如果del_node是尾节点更新tail del_node-prev;delete del_node;实操心得画图在实现链表操作时一定要在纸上画出节点和指针的当前状态一步步推演指针的修改顺序。一个常见的错误是断链即先修改了某个指针导致无法找到其他节点。记住原则先建立新节点的连接再断开旧连接或者先备份即将被覆盖的指针。3.3 MyHashMap从哈希冲突到桶管理HashMap是面试中的“明星”它的实现涉及哈希函数、冲突解决、扩容再哈希等多个核心知识点。简化版设计我们实现一个基于数组单向链表的版本暂不模拟JDK 8之后的红黑树优化。桶数组std::vectorNode* m_table;或者Node** m_table;。初始容量为16或2的幂次方。节点struct Node { K key; V value; Node* next; };负载因子const float LOAD_FACTOR 0.75f;。当元素数量 容量 * 负载因子时触发扩容。put操作流程计算键的哈希码int hash std::hashK{}(key);。我们需要为自定义类型特化std::hash或提供哈希函数对象。计算桶索引int index hash (m_table.size() - 1);。这里要求容量始终为2的幂次方这样操作等价于%取模但效率更高。遍历该索引处的链表如果找到相同键使用key 比较则更新其值返回旧值或标志。如果没找到在链表头部插入新节点new Node(key, value, m_table[index])然后m_table[index] new_node;。头部插入是O(1)。m_size。检查是否需要扩容if (m_size m_table.size() * LOAD_FACTOR) { resize(); }resize扩容操作这是HashMap性能的关键也容易出错。创建新的桶数组容量是旧数组的两倍保持2的幂次方。重新哈希Rehash遍历旧数组的每一个桶链表。遍历桶中的每一个节点。根据节点的键和新的容量重新计算其在新数组中的索引。将该节点移动到新数组对应桶的链表头部。注意移动节点时是改变节点的next指针指向而不是创建新节点拷贝数据。这样可以避免不必要的拷贝构造提升性能。用新数组替换旧数组。为什么容量是2的幂次方除了用位运算代替取模%提升计算速度外更重要的是在扩容时元素在新表中的位置有一个非常巧妙的规律要么在原索引位置要么在原索引旧容量的位置。这是因为index hash (capacity-1)扩容后新的index是hash (2*capacity-1)。由于2*capacity-1的二进制比capacity-1多了一个高位的1所以新的索引取决于哈希值对应那一位是0还是1。如果是0索引不变如果是1索引变成原索引 旧容量。这个特性可以在resize时高效地拆分链表JDK 8的源码就利用了这一点。4. 实操过程与核心环节实现4.1 MyArrayList的完整实现示例下面是一个高度简化但包含核心逻辑的MyArrayList实现框架重点关注构造函数、析构函数、扩容和迭代器。template typename T class MyArrayList { private: T* m_data; // 动态数组指针 size_t m_size; // 当前元素数量 size_t m_capacity; // 当前数组容量 int m_mod_count; // 修改次数用于快速失败 void ensure_capacity(size_t min_capacity) { if (min_capacity m_capacity) { // 1.5倍扩容策略 size_t new_capacity m_capacity (m_capacity 1); if (new_capacity min_capacity) { new_capacity min_capacity; } // 考虑初始容量为0的情况 if (new_capacity 10) { new_capacity 10; } // 申请新内存 T* new_data new T[new_capacity]; // 迁移旧数据 for (size_t i 0; i m_size; i) { new_data[i] std::move(m_data[i]); // 使用移动语义提升性能 } // 释放旧内存 delete[] m_data; // 更新成员变量 m_data new_data; m_capacity new_capacity; m_mod_count; // 扩容也是结构性修改 } } public: // 构造函数 MyArrayList() : m_data(nullptr), m_size(0), m_capacity(0), m_mod_count(0) { ensure_capacity(10); // 默认初始容量 } explicit MyArrayList(size_t initial_capacity) : m_data(nullptr), m_size(0), m_capacity(0), m_mod_count(0) { ensure_capacity(initial_capacity); } // 析构函数 ~MyArrayList() { delete[] m_data; } // 拷贝构造函数深拷贝 MyArrayList(const MyArrayList other) : m_data(new T[other.m_capacity]), m_size(other.m_size), m_capacity(other.m_capacity), m_mod_count(0) { // mod_count 从0开始 for (size_t i 0; i m_size; i) { m_data[i] other.m_data[i]; } } // 添加元素 void add(const T element) { ensure_capacity(m_size 1); m_data[m_size] element; // 假设T有拷贝赋值运算符 m_size; m_mod_count; } void add(T element) { // 移动语义版本 ensure_capacity(m_size 1); m_data[m_size] std::move(element); m_size; m_mod_count; } // 获取元素 T get(size_t index) { if (index m_size) { throw std::out_of_range(Index out of bounds); } return m_data[index]; } const T get(size_t index) const { // ... 同上const版本 } // 删除元素 T remove(size_t index) { if (index m_size) throw std::out_of_range(...); T old_value std::move(m_data[index]); // 保存被删元素 // 将后续元素前移 for (size_t i index; i m_size - 1; i) { m_data[i] std::move(m_data[i 1]); } m_size--; // 注意对于最后一个元素我们移动后原位置的对象状态是已移动但析构时仍需调用。 // 更严谨的做法是在循环结束后显式调用 m_data[m_size].~T()如果T是非平凡类型。 // 这里简化处理依赖T的赋值运算符。 m_mod_count; return old_value; } // 迭代器类 class Iterator { private: MyArrayList m_list; size_t m_current_index; int m_expected_mod_count; void check_for_comodification() const { if (m_expected_mod_count ! m_list.m_mod_count) { throw std::runtime_error(MyArrayList concurrent modification); } } public: Iterator(MyArrayList list, size_t index) : m_list(list), m_current_index(index), m_expected_mod_count(list.m_mod_count) {} bool has_next() const { check_for_comodification(); return m_current_index m_list.m_size; } T next() { check_for_comodification(); if (m_current_index m_list.m_size) throw std::runtime_error(No such element); return m_list.m_data[m_current_index]; } void remove() { check_for_comodification(); if (m_current_index 0) throw std::runtime_error(Illegal state); m_list.remove(m_current_index - 1); // 删除刚刚返回的元素 m_current_index--; // 因为列表前移了 m_expected_mod_count m_list.m_mod_count; // 更新期望修改计数 } }; Iterator iterator() { return Iterator(*this, 0); } // ... 其他方法size(), clear(), isEmpty() 等 };4.2 使用Doxygen生成API文档代码写好了如何让别人或未来的自己快速了解你的类提供了哪些接口手动写文档太累且易过时。Doxygen可以根据特殊格式的注释自动生成文档。步骤安装Doxygen从官网下载安装或使用包管理器如apt-get install doxygen,brew install doxygen。编写Doxygen风格注释在头文件.hpp中对类、方法、变量进行注释。/** * brief 模拟Java ArrayList的简化C实现。 * * 本类使用动态数组存储元素支持自动扩容1.5倍因子。 * 实现了基本的增删查改操作以及迭代器并包含简单的快速失败机制。 * tparam T 容器中元素的类型。 */ template typename T class MyArrayList { public: /** * brief 在列表末尾添加指定元素。 * param element 要添加的元素。 * throw std::bad_alloc 当内存分配失败时抛出。 * note 此操作可能导致数组扩容平均时间复杂度为O(1)摊销。 */ void add(const T element); // ... };常用命令brief简要说明param参数说明return返回值说明throw抛出异常note注意事项tparam模板参数。生成配置文件在项目根目录运行doxygen -g Doxyfile生成配置文件。配置Doxyfile用文本编辑器打开Doxyfile修改关键配置PROJECT_NAME MyContainerSimulationOUTPUT_DIRECTORY ./docsINPUT ./include ./src(指定你的源代码目录)RECURSIVE YES(递归搜索子目录)EXTRACT_ALL YES(为所有实体生成文档)GENERATE_LATEX NO(如果你不需要LaTeX输出)生成文档运行doxygen Doxyfile。完成后在./docs/html目录下打开index.html就是完整的API文档网站了包含类列表、继承图、协作图等。实操心得将Doxygen集成到CMake中是个好习惯。可以在CMakeLists.txt中添加一个自定义目标这样只需执行make doc就能生成文档。find_package(Doxygen) if(DOXYGEN_FOUND) set(DOXYGEN_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/docs) doxygen_add_docs(docs ${PROJECT_SOURCE_DIR}/include COMMENT Generate API documentation) endif()5. 常见问题与排查技巧实录在实现和面试中会遇到一些典型问题。这里记录一些“踩坑”经验和排查思路。5.1 内存问题泄漏、越界与重复释放这是C手动管理内存的“重灾区”。问题表现程序运行一段时间后内存占用持续增长泄漏程序随机崩溃错误信息涉及malloc/free越界或重复释放。排查工具Valgrind (Linux/macOS):valgrind --leak-checkfull ./your_program。它能精准定位内存泄漏、非法读写、使用未初始化内存等问题。AddressSanitizer (ASan):在编译时添加-fsanitizeaddress标志GCC/Clang。它对性能影响小能实时检测内存错误是首选。我们的容器中常见陷阱MyArrayList的拷贝构造函数和赋值运算符必须实现“深拷贝”。如果只拷贝了指针m_data两个对象将共享同一块内存析构时会导致同一内存被delete[]两次重复释放。必须new出新数组并拷贝所有元素。MyLinkedList的析构函数必须遍历整个链表delete每一个节点。忘记写循环会导致链表节点全部泄漏。MyHashMap的resize在将节点从旧桶移到新桶时要正确更新节点的next指针防止链表断裂导致部分节点丢失内存泄漏。同时旧桶数组本身Node**需要被释放但桶内的节点已经移走不应再被delete。5.2 迭代器失效问题这是面试高频考点也是实际编码容易出错的地方。MyArrayList迭代器失效场景插入元素导致扩容扩容后内部数组地址改变所有之前获取的迭代器、指针、引用都立即失效。在中间插入或删除元素被修改位置之后的所有元素的索引都变了指向这些元素的迭代器在逻辑上失效虽然指针可能还能访问到某个地址但元素已不是原来的元素。MyLinkedList迭代器失效场景删除当前迭代器指向的节点该节点被delete迭代器持有的指针变成野指针。我们的Iterator::remove()方法在删除后主动将迭代器指向前一个节点这是一种安全的处理方式。链表结构被其他迭代器修改类似Java的快速失败机制我们需要用modCount来检测。最佳实践在文档中明确说明每种操作对迭代器的影响。在编码时尽量避免在迭代过程中直接通过容器对象修改结构。如果必须修改使用迭代器自身的remove方法如果提供。5.3 模板编译错误使用模板时编译器错误信息往往又长又晦涩。“未定义的引用”链接错误模板类的成员函数定义必须放在头文件.hpp中不能像普通类一样在.cpp中定义然后在头文件中声明。因为模板需要在编译时实例化。复杂的类型推导错误当嵌套模板或涉及自动类型推导时容易出错。例如MyArrayListMyLinkedListint注意两个之间要有空格在C11以前需要写成 。调试技巧当遇到看不懂的模板错误时先尝试将出错的代码简化或者显式指定模板参数类型看看错误是否消失从而定位问题范围。5.4 哈希表性能调优与问题哈希冲突严重如果所有键的哈希值都映射到同一个桶哈希表退化成链表性能从O(1)降到O(n)。检查哈希函数自定义类型的std::hash特化是否合理是否分布均匀检查键的equals方法在C中是operator。它必须与哈希函数一致如果两个键相等其哈希值必须相等反之哈希值相等键不一定相等哈希冲突。扩容开销大resize需要重新哈希所有元素。如果对性能有极致要求可以在创建HashMap时预估大小指定一个足够的初始容量避免或减少扩容。6. 从模拟实践到面试题解答通过亲手模拟很多经典的C/C/Java面试题就不再是死记硬背而是有了直观的理解。以下是一些2024年依然常见的高级面试题及其背后的原理我们可以从实现者的角度来回答。6.1 C相关Qstd::vector的底层实现和扩容机制与ArrayList有何异同Astd::vector底层是连续内存的动态数组。扩容时通常分配一块原容量2倍的新内存标准未规定但主流实现如此然后将旧元素移动或拷贝到新内存释放旧内存。这与ArrayList的1.5倍扩容不同。2倍扩容减少了扩容次数但可能浪费更多空间。两者都支持随机访问迭代器都可能因插入删除而失效。vector的迭代器是原生指针失效规则更严格ArrayList的迭代器通过modCount提供快速失败检测。QC中深拷贝与浅拷贝的区别在什么情况下必须实现深拷贝A浅拷贝只复制指针值导致多个对象共享同一块堆内存。深拷贝会复制指针指向的整个数据内容在新内存地址创建副本。当类成员包含指向堆内存的指针如我们的m_data并且拥有该内存的所有权时必须实现深拷贝自定义拷贝构造函数和拷贝赋值运算符否则会导致重复释放或内存泄漏。这就是“Rule of Three/Five/Zero”原则讨论的核心。Q智能指针unique_ptr,shared_ptr如何帮助管理资源能在容器中使用吗Aunique_ptr独占所有权自动释放资源禁止拷贝允许移动非常适合用来管理容器内的动态数组如vector的底层数组。shared_ptr共享所有权引用计数为0时释放。在容器中存储智能指针是安全的可以避免手动管理元素内存的麻烦。例如std::vectorstd::unique_ptrMyObject当vector析构时所有unique_ptr也会被析构从而自动释放它们管理的MyObject对象。6.2 Java容器底层基于我们的模拟理解QHashMap在JDK 1.7和JDK 1.8中有哪些重要优化A基于我们的模拟和阅读源码主要优化有1数据结构JDK 7是数组链表JDK 8引入了数组链表/红黑树当链表长度超过阈值默认8且数组容量大于64时链表转为红黑树将最差情况下的查找复杂度从O(n)降至O(log n)。2插入方式JDK 7头插法多线程下可能产生环形链表导致死循环JDK 8改为尾插法。3扩容时节点重哈希JDK 8优化了算法利用扩容后容量是2的幂次的特性节点的新位置要么是原索引要么是原索引旧容量避免了重新计算哈希只需判断哈希值新增的bit是0还是1。QConcurrentHashMap是如何实现线程安全的AJDK 7采用分段锁Segment每个段类似一个小的HashMap锁粒度较粗。JDK 8摒弃了分段锁改用**synchronized锁单个桶链表头或树根 CAS操作**。对于put操作如果桶为空用CAS尝试插入否则用synchronized锁住桶的头节点再进行操作。这种设计锁粒度更细并发度更高。我们的模拟不涉及线程安全但理解这个演进对回答并发容器问题至关重要。QArrayList的subList方法返回的列表和原列表是什么关系AsubList返回的是原列表的一个“视图”view而不是一个独立的拷贝。它内部持有原列表的引用和偏移量。对子列表的修改如set,add会直接反映到原列表上。反之如果在生成子列表后原列表发生了结构性修改非子列表范围内的add/remove再操作子列表可能会抛出ConcurrentModificationException。这类似于我们的迭代器持有modCount检查的原理。6.3 综合设计题Q如果让你设计一个支持LRU最近最少使用缓存淘汰策略的容器你会怎么设计这是一个结合了数据结构与算法设计的经典题。基于我们实现的HashMap和LinkedList可以给出一个高效的设计。A我会设计一个LRUCache类它结合了哈希表和双向链表。数据结构一个std::unordered_mapKey, Node* cache_map作为哈希表实现O(1)的查找。一个自定义的双向链表节点包含key,value,prev,next。链表头部是最近使用的节点尾部是最久未使用的节点。get(key)操作在cache_map中查找key。如果找到获取对应的链表节点。将该节点从链表中原位置移除并插入到链表头部更新为最近使用。返回节点的值。时间复杂度O(1)。put(key, value)操作如果key已存在更新值并将节点移到链表头部。如果key不存在如果缓存已满达到容量则删除链表尾部的节点最久未使用并在cache_map中删除对应的键。创建一个新节点放入链表头部并在cache_map中添加映射。时间复杂度O(1)。为什么是双向链表因为我们需要在O(1)时间内删除任意节点给定节点指针。单向链表无法在O(1)时间内找到前驱节点来完成删除。这个设计正是Java中LinkedHashMap在访问顺序accessOrdertrue模式下的实现原理也是许多实际LRU缓存库如Guava Cache的核心思想。通过这个例子可以看到对基础容器HashMap,LinkedList的深刻理解如何直接应用于解决更复杂的实际问题。