【Go】-倒排索引的简单实现

news2024/12/21 2:36:57

目录

什么是倒排索引

定义

基本结构和原理

分词在倒排索引中的重要性

简单倒排索引的实现

接口定义

简单数据库的实现

倒排索引

正排索引

测试

总结


什么是倒排索引

定义

  • 倒排索引(Inverted Index)是一种索引数据结构,它是文档检索系统中最常用的数据结构之一。在信息检索领域,它用于快速地定位包含给定查询词的文档。与正向索引(Forward Index)相对,正向索引是从文档到词汇的映射,而倒排索引是从词汇到文档的映射。

基本结构和原理

  • 倒排索引主要由两部分组成:词汇表(Vocabulary)和倒排记录表(Postings List)。

  • 词汇表:包含了文档集合中出现的所有不同的词汇(或词条)。每个词汇都有一个指向其对应的倒排记录表的指针。例如,在一个包含多篇新闻文章的文档集合中,词汇表可能包含 “经济”“政治”“科技” 等词汇。

  • 倒排记录表:对于词汇表中的每个词汇,倒排记录表记录了包含该词汇的所有文档的标识符(Document ID)以及可能的其他相关信息,如词汇在文档中的位置、出现的频率等。比如,对于词汇 “科技”,其倒排记录表可能包含文档 ID 为 1、3、5 的记录,表示这三篇文档中都出现了 “科技” 这个词汇。

    • 假如现在有三份数据文档,内容分别是:

       Doc 1:Java is the best programming language
       ​
       Doc 2:PHP is the best programming language
       ​
       Doc 3:Javascript is the best programming language

      为了创建索引,通过分词器将每个文档的内容拆成单独的词,再将这些词条创建成不含重复词条的排序列表,然后列出每个词条出现在哪个文档,结果如下:

      termDoc 1Doc 2Doc 3
      Java
      is
      the
      best
      programming
      language
      PHP
      Javascript

              这种结构由文档中所有不重复的词的列表构成,对于其中每个词都有至少一个文档与与之关联。这种由属性值来确定记录的位置的结构就是倒排索引,带有倒排索引的文件被称为倒排文件。

              将上表转为更直观的图片来展示倒排索引:

      image-20241211221224035

分词在倒排索引中的重要性

  • 建立索引基础:倒排索引是一种用于快速检索的数据结构。它的核心是将文档中的关键词提取出来,建立关键词到文档的映射关系。分词就是这个提取关键词的过程,只有通过分词,才能将文本内容分解为一个个有意义的词汇单元,为建立倒排索引提供基础。例如,对于一篇文档 “我爱自然语言处理技术”,如果不分词,这个文档就会被当作一个整体,很难进行有效的关键词检索;而通过分词得到 “我”“爱”“自然语言处理”“技术” 这些词汇后,就可以分别建立它们与该文档的索引关系。

  • 提高检索效率和准确性:当用户进行查询时,倒排索引会根据用户输入的关键词来查找相关文档。精确的分词可以确保查询词和索引中的词汇准确匹配,提高检索的准确性。例如,在搜索引擎中,如果用户输入 “自然语言处理”,经过良好分词的倒排索引能够快速定位到包含这个词汇的文档,而不会因为没有正确分词而错过相关文档。同时,合理的分词还可以减少索引的大小,提高检索效率。如果将一些无意义的组合词也作为索引词,会增加索引的复杂度和存储量,而通过分词去除不必要的组合,只保留有意义的词汇,可以使索引更加紧凑,检索速度更快。

中文分词面临的挑战:

  • 词汇的复杂性

    • 词的歧义性:中文中存在大量的歧义现象。例如 “下雨天留客天留我不留”,不同的断句(分词)方式会产生不同的意思。可以是 “下雨天,留客天,留我不?留。” 也可以是 “下雨天留客,天留,我不留。” 这种歧义给中文分词带来了很大的困难。

    • 新词不断涌现:随着社会的发展和科技的进步,新的词汇不断出现,如 “区块链”“人工智能”“元宇宙” 等。对于分词系统来说,需要及时识别这些新词,才能保证分词的准确性和完整性。

  • 缺乏明显的分隔符:与英文等语言不同,中文句子中词与词之间没有明显的分隔符(如英文中的空格)。这使得计算机很难直观地判断一个词的起始和结束位置,需要通过复杂的算法和规则来进行分词。

  • 语言的灵活性和多样性:中文有丰富的表达方式,包括成语、俗语、古诗词等。这些特殊的语言形式也给分词带来了挑战。例如,成语 “胸有成竹” 如果被错误地分割为 “胸有”“成竹”,就会失去原有的语义,影响分词的质量。

常用的中文分词库

  • jieba 分词库

    • 特点:jieba 是一个非常流行的中文分词库,它具有多种分词模式,包括精确模式、全模式和搜索引擎模式。精确模式试图将句子最精确地切开,适合文本分析等场景;全模式把句子中所有的可以成词的词语都扫描出来,速度快但可能会产生冗余;搜索引擎模式在精确模式的基础上,对长词再次切分,提高搜索引擎召回率。例如,对于句子 “中华人民共和国”,精确模式会分为 “中华人民共和国”,全模式会分为 “中华”“华人”“人民”“共和”“共和国” 等,搜索引擎模式会在精确模式的基础上对 “中华人民共和国” 进一步切分,以适应搜索引擎的需求。

    • 应用场景:广泛应用于文本挖掘、信息检索、机器翻译等领域。例如,在文本挖掘中,可以使用 jieba 对文本进行分词,然后进行词频统计等分析。

  • THULAC(清华大学自然语言处理与社会人文计算实验室)

    • 特点:它是由清华大学开发的中文词法分析工具包,具有较高的分词准确性。它提供了词性标注等功能,不仅可以分词,还可以标注每个词的词性,如名词、动词、形容词等。例如,对于句子 “他高兴地跑了”,除了将句子分为 “他”“高兴”“地”“跑”“了”,还可以标注出 “他” 是代词,“高兴” 是形容词,“地” 是助词,“跑” 是动词,“了” 是语气词。

    • 应用场景:在自然语言处理研究和需要词性标注的应用场景中使用较多,如情感分析中,结合词性标注可以更好地分析句子的情感倾向。


简单倒排索引的实现

接口定义

        定义三个接口,分别是 数据库,正排索引,倒排索引;规定都要实现GetAdd功能

// DB接口定义了数据库的基本操作,用于获取和添加数据。
 type DB interface {
     Get(string) []string
     Add(string)
 }
 ​
 // ForwardIndexer 用于根据给定的文档ID列表获取对应的原始字符串内容。
 type ForwardIndexer interface {
     Get([]int64) []string
     Add(int64, string)
 }
 ​
 // InvertedIndexer 用于根据给定的字符串获取对应的文档ID列表以及添加倒排索引数据。
 type InvertedIndexer interface {
     Get(string) []int64
     Add(string, int64)
 }

简单数据库的实现

        定义简单数据库结构体,由id,正排索引和倒排索引组成。

   Get方法是先通过倒排索引由字符串找出id,然后在通过正排索引由id找出匹配的字符串

   Add方法把字符串存入倒排和正排

// SimpleDatabase结构体实现了DB接口,内部整合了正向索引和倒排索引来管理数据。
 type SimpleDatabase struct {
     id int64
     fi ForwardIndexer
     ii InvertedIndexer
 }
 ​
 // NewSimpleDatabase创建一个新的SimpleDatabase实例,初始化其正向索引和倒排索引相关组件。
 func NewSimpleDatabase() DB {
     return &SimpleDatabase{
         id: 0, // 递增文档ID
         fi: NewForwardIndex(),
         ii: NewSimpleInverted(),
     }
 }
 ​
 func (sd *SimpleDatabase) Get(s string) []string {
     // 先通过倒排索引根据输入字符串获取对应的文档ID列表
     ids := sd.ii.Get(s)
     // 再通过正排索引根据获取到的文档ID列表查找对应的原字符串
     return sd.fi.Get(ids)
 }
 ​
 func (sd *SimpleDatabase) Add(s string) {
     atomic.AddInt64(&sd.id, 1)
     id := sd.id
     addToIndexes(sd.fi, sd.ii, id, s)
 }
 ​
 func addToIndexes(fi ForwardIndexer, ii InvertedIndexer, id int64, s string) {
     // 倒排存入
     ii.Add(s, id)
     // 正排存入
     fi.Add(id, s)
 }

倒排索引

        倒排索引结构由读写锁,分词器和map组成

   Get方法查找data返回id数组

   Add先分词,然后把分词后的结构以及对应id存入map,ES中的分词器一般会大写转小写,但是这里我偷个懒就直接存了

// SimpleInverted结构体实现了InvertedIndexer接口,用于管理倒排索引数据。
 type SimpleInverted struct {
     sync.RWMutex
     data     map[string][]int64
     analyzer Analyzer
 }
 ​
 // NewSimpleInverted创建一个新的SimpleInverted实例,初始化倒排索引数据存储结构和分析器。
 func NewSimpleInverted() InvertedIndexer {
     return &SimpleInverted{
         data:     make(map[string][]int64),
         analyzer: NewSimpleAnalyzer(),
     }
 }
 ​
 func (si *SimpleInverted) Get(s string) []int64 {
     si.RLock()
     result := si.data[s]
     si.RUnlock()
     return result
 }
 ​
 func (si *SimpleInverted) Add(s string, id int64) {
     words := si.analyzer.Analyze(s)
     si.Lock()
     for _, word := range words {
         si.data[word] = append(si.data[word], id)
     }
     si.Unlock()
 }

分词器

        这里分词器的实现比较简单,直接逐个拆开来存了,在实际中分词器比这更加复杂和优雅,往往伴随着一些分词的算法

        这里用使用了两层嵌套的 for 循环来生成输入字符串的所有可能子串,并将这些子串作为键存入一个 map 类型的变量 word 中。外层循环控制起始位置 i,内层循环控制结束位置 j,通过切片操作 su[i:j] 取出从位置 i 到位置 j(不包含 j)的子串,然后将其转换为字符串作为 map 的键,对应的值使用了空结构体 struct{}{}

        这样做虽然能实现分词,但是非常暴力而且浪费空间。因为中文分词不像英文,可以使用空格或者,进行简单切分,一般的中文分词器都会采用词典分词,因为我们是简单实现,所以这里就采用了这种暴力写法(其实是太菜了不会更好的分词方法)

 // Analyzer接口定义了文本分析(例如分词等操作)的基本操作方法。
 type Analyzer interface {
     Analyze(s string) []string
 }
 ​
 // SimpleAnalyzer结构体实现了Analyzer接口,简单地进行字符串分析(示例中较简单的逻辑,可优化)。
 type SimpleAnalyzer struct{}
 ​
 // NewSimpleAnalyzer创建一个新的SimpleAnalyzer实例。
 func NewSimpleAnalyzer() Analyzer {
     return &SimpleAnalyzer{}
 }
 ​
 func (l *SimpleAnalyzer) Analyze(s string) (re []string) {
     // 转为rune可以有效处理中英文字符的字节大小问题
     su := []rune(s)
     sl := len(su)
     word := make(map[string]struct{})
     for i := 0; i < sl; i++ {
         for j := i + 1; j <= sl; j++ {
             word[string(su[i:j])] = struct{}{}
         }
     }
     re = make([]string, len(word))
     num := 0
     for index := range word {
         re[num] = index
         num++
     }
     return
 }

正排索引

        这个比起倒排简单很多,没啥好讲的

// forwardIndex结构体实现了ForwardIndexer接口,用于管理正向索引数据。
 type forwardIndex struct {
     sync.RWMutex
     data map[int64]string
 }
 ​
 // NewForwardIndex创建一个新的forwardIndex实例,初始化正向索引数据存储结构。
 func NewForwardIndex() ForwardIndexer {
     return &forwardIndex{
         data: make(map[int64]string),
     }
 }
 ​
 func (fi *forwardIndex) Get(ids []int64) (re []string) {
     re = make([]string, len(ids))
     fi.RLock()
     for k, v := range ids {
         re[k] = fi.data[v]
     }
     fi.RUnlock()
     return
 }
 ​
 func (fi *forwardIndex) Add(id int64, s string) {
     fi.Lock()
     fi.data[id] = s
     fi.Unlock()
 }

测试

        单元测试代码如下,一首《春江花月夜》来试试效果

func TestSimpleDatabaseWithChunJiangHuaYueYe(t *testing.T) {
     // 创建数据库实例
     db := NewSimpleDatabase()
 ​
     // 添加《春江花月夜》的诗句(假设逐句添加)
     lines := []string{
         "春江潮水连海平,海上明月共潮生。",
         "江流宛转绕芳甸,月照花林皆似霰。",
         "空里流霜不觉飞,汀上白沙看不见。",
         "江天一色无纤尘,皎皎空中孤月轮。",
         "江畔何人初见月?江月何年初照人?",
         "人生代代无穷已,江月年年望相似。",
         "不知江月待何人,但见长江送流水。",
         "白云一片去悠悠,青枫浦上不胜愁。",
         "谁家今夜扁舟子?何处相思明月楼?",
         "可怜楼上月徘徊,应照离人妆镜台。",
         "玉户帘中卷不去,捣衣砧上拂还来。",
         "此时相望不相闻,愿逐月华流照君。",
         "鸿雁长飞光不度,鱼龙潜跃水成文。",
         "昨夜闲潭梦落花,可怜春半不还家。",
         "江水流春去欲尽,江潭落月复西斜。",
         "斜月沉沉藏海雾,碣石潇湘无限路。",
         "不知乘月几人归,落月摇情满江树。",
     }
     for _, line := range lines {
         db.Add(line)
     }
 ​
     // 测试获取包含“江”字的字符串
     expectedJiang := []string{
         "春江潮水连海平,海上明月共潮生。",
         "江流宛转绕芳甸,月照花林皆似霰。",
         "江天一色无纤尘,皎皎空中孤月轮。",
         "江畔何人初见月?江月何年初照人?",
         "人生代代无穷已,江月年年望相似。",
         "不知江月待何人,但见长江送流水。",
         "江水流春去欲尽,江潭落月复西斜。",
         "不知乘月几人归,落月摇情满江树。",
     }
     actualJiang := db.Get("江")
     if !reflect.DeepEqual(actualJiang, expectedJiang) {
         t.Errorf("Get for '江' failed. Expected: %v, Got: %v", expectedJiang, actualJiang)
     } else {
         fmt.Println("========江=========")
         for _, s := range actualJiang {
             fmt.Println(s)
         }
     }
 ​
     // 测试获取包含“月”字的字符串
     expectedYue := []string{
         "春江潮水连海平,海上明月共潮生。",
         "江流宛转绕芳甸,月照花林皆似霰。",
         "江天一色无纤尘,皎皎空中孤月轮。",
         "江畔何人初见月?江月何年初照人?",
         "人生代代无穷已,江月年年望相似。",
         "不知江月待何人,但见长江送流水。",
         "谁家今夜扁舟子?何处相思明月楼?",
         "可怜楼上月徘徊,应照离人妆镜台。",
         "此时相望不相闻,愿逐月华流照君。",
         "江水流春去欲尽,江潭落月复西斜。",
         "斜月沉沉藏海雾,碣石潇湘无限路。",
         "不知乘月几人归,落月摇情满江树。",
     }
     actualYue := db.Get("月")
     if !reflect.DeepEqual(actualYue, expectedYue) {
         t.Errorf("Get for '月' failed. Expected: %v, Got: %v", expectedYue, actualYue)
     } else {
         fmt.Println("========月=========")
         for _, s := range actualYue {
             fmt.Println(s)
         }
     }
 ​
     // 测试获取包含“花”字的字符串
     expectedHua := []string{
         "江流宛转绕芳甸,月照花林皆似霰。",
         "昨夜闲潭梦落花,可怜春半不还家。",
     }
     actualHua := db.Get("花")
     if !reflect.DeepEqual(actualHua, expectedHua) {
         t.Errorf("Get for '花' failed. Expected: %v, Got: %v", expectedHua, actualHua)
     } else {
         fmt.Println("========花=========")
         for _, s := range actualHua {
             fmt.Println(s)
         }
     }
 ​
     // 测试获取包含“海”字的字符串
     expectedHai := []string{
         "春江潮水连海平,海上明月共潮生。",
         "斜月沉沉藏海雾,碣石潇湘无限路。",
     }
     actualHai := db.Get("海")
     if !reflect.DeepEqual(actualHai, expectedHai) {
         t.Errorf("Get for '海' failed. Expected: %v, Got: %v", expectedHai, actualHai)
     } else {
         fmt.Println("========海=========")
         for _, s := range actualHai {
             fmt.Println(s)
         }
     }
 }

        测试结果通过,可喜可贺

image-20241212205028259


总结

        Go简单实现了一下倒排索引,感觉分词还是很重要的,直接决定了整个倒排索引的表现,还是要多学习一些厉害的分词器是怎么实现的~

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2263025.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

php生成图片

前提 开启dg2库 去掉前面的;注释&#xff0c;有的可能会带.dll后缀影响不大 extensiongd2代码 <?php $file imagecreate(100,50); //先生成图片资源$color imagecolorallocate($file,255,255,255); //白色$c imagecolorallocate($file,0,100,255);imagefill($file,0…

MySQL数据库——门诊管理系统数据库数据表

门诊系统数据库his 使用图形化工具或SQL语句在简明门诊管理系统数据库his中创建数据表&#xff0c;数据表结构见表2-3-9&#xff5e;表2-3-15所示。 表2-3-9 department&#xff08;科室信息表&#xff09; 字段名称 数据类型 长度 是否为空 说明 dep_ID int 否 科室…

02、10个富士胶片模拟的设置

二色彩 1、色彩的加减控制全局的饱和度增减&#xff1b; 2、色彩效果只提升暖色系饱和度&#xff1b; 3、FX蓝色大幅度提升蓝色系饱和度&#xff1b; 4、三个参数都不改变颜色的色相。 2.1 色彩 色彩调整的是拍摄画面整体的色彩饱和程度 2.2色彩效果 调整的是画面中暖色…

java全栈day19--Web后端实战(java操作数据库3)

一、MyBatis 1.1介绍 前提引入&#xff1a; controller(控制层)作用&#xff1a;接受请求&#xff0c;响应数据 service(业务层)作用&#xff1a;负责具体的逻辑处理 dao(持久层)作用&#xff1a;数据访问层 一般的访问流程&#xff1a;浏览器发起请求过来&#xff0c;先…

以太网帧、IP数据报图解

注&#xff1a;本文为 “以太网帧、IP数据报”图解相关文章合辑。 未整理去重。 以太网帧、IP数据报的图解格式&#xff08;包含相关例题讲解&#xff09; Rebecca.Yan已于 2023-05-27 14:13:19 修改 一、基础知识 UDP 段、IP 数据包&#xff0c;以太网帧图示 通信过程中&…

Android Vendor Overlay机制

背景介绍&#xff1a; 看Android 15版本更新时&#xff0c;"Android 15 deprecates vendor overlay"。 猜想这个vendor overlay是之前用过的settings overlay&#xff0c; 不过具体是怎么回事呢&#xff1f; 目录 Vendor Overlay介绍 Vendor Overlay工作原理 Ven…

联发科MTK8788_MT8788安卓核心板安兔兔跑分_安卓主板方案商

MT8788安卓核心板具有集成的蓝牙、fm、WLAN和gps模块&#xff0c;是一个高度集成的基带平台&#xff0c;包括调制解调器和应用处理子系统&#xff0c;启用LTE/LTE-A和C2K智能设备应用程序。该芯片集成了工作在2.0GHz的ARM Cortex-A73、最高可达2.0GHz的ARM Cortex-A53和功能强大…

uniapp连接蓝牙操作(蓝牙设备地锁)

介绍&#xff1a; 本文采用uni-app框架来创建一个简单的用户界面&#xff0c;用于搜索、连接和发送命令给蓝牙设备。 1.打开蓝牙适配器 function openBluetooth() {uni.openBluetoothAdapter({success() {uni.offBluetoothDeviceFound();// 监听新设备发现事件uni.onBlueto…

谁说C比C++快?

看到这个问题&#xff0c;我我得说&#xff1a;这事儿没有那么简单。 1. 先把最大的误区打破 "C永远比C快" —— 某位1990年代的程序员 这种说法就像"自行车永远比汽车省油"一样荒谬。我们来看个例子&#xff1a; // C风格 char* str (char*)malloc(100…

宝塔SSL证书申请失败,报错:申请SSL证书错误 module ‘OpenSSL.crypto‘ has no attribute ‘sign‘(已解决)

刚安装宝塔申请SSL就报错&#xff1a;申请SSL证书错误 module OpenSSL.crypto has no attribute sign 面板、插件版本&#xff1a;9.2.0 系统版本&#xff1a;Alibaba Cloud Linux 3.2104 LTS 问题&#xff1a;申请SSL证书错误 module OpenSSL.crypto has no attribute sign…

华为OD-计算正方形数量

华为OD-计算正方形数量 题目解题思路源码实现 题目 解题思路 由于是构成正方形 再结合用例1 很容易直接写出第一行代码 const pointCount await readline();if (pointCount < 4) {console.log(0);return;}然后就是典型的数学题,什么才能构成正方形(这题题主不会,看了解析之…

使用 rvest 包快速抓取网页数据:从入门到精通

介绍 随着大数据和数据科学的迅速发展&#xff0c;互联网数据的抓取已经成为重要的信息获取手段之一。网页抓取&#xff08;Web Scraping&#xff09;可以帮助我们自动化地从网页中提取有价值的数据&#xff0c;应用广泛&#xff0c;包括新闻热点分析、金融数据采集等。在本篇…

【机器学习】【集成学习——决策树、随机森林】从零起步:掌握决策树、随机森林与GBDT的机器学习之旅

这里写目录标题 一、引言机器学习中集成学习的重要性 二、决策树 (Decision Tree)2.1 基本概念2.2 组成元素2.3 工作原理分裂准则 2.4 决策树的构建过程2.5 决策树的优缺点&#xff08;1&#xff09;决策树的优点&#xff08;2&#xff09;决策树的缺点&#xff08;3&#xff0…

【Rust自学】4.2. 所有权规则、内存与分配

4.2.0 写在正文之前 在学习了Rust的通用编程概念后&#xff0c;就来到了整个Rust的重中之重——所有权&#xff0c;它跟其他语言都不太一样&#xff0c;很多初学者觉得学起来很难。这个章节就旨在让初学者能够完全掌握这个特性。 本章有三小节&#xff1a; 所有权&#xff1…

Mamba安装环境和使用,anaconda环境打包

什么是mamba Mamba是一个极速版本的conda&#xff0c;它是conda的C重新实现&#xff0c;使用多线程并行处理来加速包和依赖项的下载。 Mamba旨在提高安装、更新和卸载Python包的速度&#xff0c;同时保持与conda相同的兼容性和命令行接口。 Mamba的核心部分使用C实现&#xff…

Vue前端开发-数据缓存

完成全局性的axios实例对象配置后&#xff0c;则可以在任意一个组件中直接调用这个对象&#xff0c;发送异步请求&#xff0c;获取服务端返回的数据&#xff0c;同时&#xff0c;针对那些不经常变化的数据&#xff0c;可以在请求过程中&#xff0c;进行数据缓存&#xff0c;并根…

Composer指定php版本执行(windows)

✔️指定php版本执行&#xff08;windows&#xff09; 正常用法如下 /usr/bin/php7.1 /usr/local/bin/composer require xxxx 通过alias 简化指定PHP版本的路径 alias .php7_composer‘/d/application/phpstudy_pro/Extensions/php/php7.3.4nts/php /d/application/phpstudy_pr…

搭建私有链

文章目录 1. 准备工作2. 创建创世区块配置文件2.1 创建数据目录2.2 创建创世区块配置文件1. “config”部分2. “alloc”部分3. “coinbase”4. “difficulty”5. “extraData”6. “gasLimit”7. “nonce”8. “mixhash”9. “parentHash”10. “timestamp” 3. 初始化&#x…

游戏AI实现-寻路算法(BFS)

广度优先搜索算法&#xff08;英语&#xff1a;Breadth-first search&#xff0c;缩写&#xff1a;BFS&#xff09;&#xff0c;又译作宽度优先搜索&#xff0c;或横向优先搜索&#xff0c;是一种图形搜索算法。 寻路地图搭建&#xff1a; 游戏AI实现-寻路地图搭建-CSDN博客 …

k-均值聚类(k-Means Clustering)详解

✅作者简介&#xff1a;2022年博客新星 第八。热爱国学的Java后端开发者&#xff0c;修心和技术同步精进。 &#x1f34e;个人主页&#xff1a;Java Fans的博客 &#x1f34a;个人信条&#xff1a;不迁怒&#xff0c;不贰过。小知识&#xff0c;大智慧。 &#x1f49e;当前专栏…